Files
Kigi-CLI/crates/codegen/kigi-sandbox/src/lib.rs
T
ZacharyZhang-NY 6f31415ed6 §9 acceptance: grep-zero sweep — every internal x.ai/grok identifier renamed
The PRD's first acceptance gate now holds: grep -RinE '\bx\.ai\b|grok'
crates/ --include='*.rs' → 0 matches (exempt: NOTICE and third-party
license archives, README provenance, and the required 'Based on Grok
Build Open Source' attribution, now sourced from version_attribution.txt).

Wire-visible renames (both sides in this repo, changed in lockstep):
- Auth method id 'grok.com' → 'kimi-code' (AuthMethodKind::KimiCode).
- Every x.ai/* and _x.ai/* ACP ext method and meta key → kigi/* /
  _kigi/* (~200 names; grokShell → kigiShell). Session-file replay keeps
  a read-side alias for the legacy '_x.ai/session/update' method so
  existing updates.jsonl histories load; writes emit only the new name
  (both directions test-pinned).
- Agent types grok-build* → kigi* with a documented legacy-prefix alias
  at resolution time so persisted sessions keep resolving.
- ToolNamespace/BuiltinAgentName GrokBuild* → Kigi* (wire snake_case
  kigi/kigi_concise/kigi_hashline; schema regenerated); grok_build
  implementation dirs renamed to kigi*.
- x-grok-* headers → x-kigi-*, __GROK_* sentinels → __KIGI_*, themes
  grokday/groknight → kigiday/kiginight (old persisted values fall back
  to the default theme), web_fetch allowlist xAI hosts → kimi.com +
  moonshot platforms, changelog CDN → this repo, grok-build changelog
  archives deleted.
- BYOK default endpoint removed: [endpoints] api_base_url is now truly
  optional with NO default — consumers fail fast with the flag name when
  unset (no silent x.ai egress). Mock harnesses inject it explicitly.
- System-prompt identity fixed: 'released by xAI' → 'an unofficial
  community CLI for Kimi' (template + regenerated encrypted form).

Also repaired pre-existing grok-era test debt found by the sweep: the
stale trace_classify default-model pin, the grok-pager UA label test,
pty-harness stale-binary reuse and non-hermetic moonshot routing (a PTY
test could previously reach the real api.moonshot.cn), and the outdated
oauth fixture scope key.

Gates: §9 grep 0; fmt clean; workspace check/clippy 0/0 (-D warnings);
FULL cargo test --workspace: 234 suites, 21,961 passed, 0 failed;
deny advisories ok.
2026-07-18 02:48:46 -04:00

734 lines
28 KiB
Rust

#![allow(
unused_imports,
unused_variables,
unused_mut,
unreachable_code,
dead_code
)]
//! OS-level sandboxing for Kigi via [nono](https://crates.io/crates/nono).
//!
//! Applied once at process startup. Covers in-process `tokio::fs` calls
//! and child processes. Network is left open at the process level (agent
//! needs LLM API); child network is blocked per-subprocess via seccomp.
//!
//! The `enforce` feature (on by default) pulls in `nono` for
//! kernel-enforced sandboxing (Landlock/Seatbelt). When disabled, the
//! crate still provides lightweight helpers (`log_violation`,
//! `should_restrict_child_network`, `child_net`) that compile on all
//! targets including musl.
//!
//! ```rust,no_run
//! use kigi_sandbox::{SandboxManager, ProfileName};
//! use std::path::Path;
//!
//! let workspace = Path::new("/home/user/project");
//! let mut sandbox = SandboxManager::new(ProfileName::Workspace, workspace);
//! sandbox.apply(workspace).expect("sandbox apply failed");
//! sandbox.install();
//! ```
pub mod child_net;
mod deny;
mod logging;
mod paths;
mod profiles;
mod types;
pub use logging::SandboxLogger;
#[cfg(all(feature = "enforce", unix))]
use nono::Sandbox;
pub use profiles::{
ProfileName, SandboxConfig, SandboxProfile, load_sandbox_config, sandbox_profile_conflicts,
};
use std::path::Path;
#[cfg(any(target_os = "linux", all(feature = "enforce", test)))]
use std::path::PathBuf;
use std::sync::OnceLock;
use std::sync::atomic::{AtomicBool, Ordering};
pub use types::{SandboxEvent, SandboxEventType, SandboxMetrics};
static SANDBOX: OnceLock<GlobalSandboxState> = OnceLock::new();
static CONFIGURED_PROFILE: OnceLock<String> = OnceLock::new();
static RESTRICT_CHILD_NETWORK: AtomicBool = AtomicBool::new(false);
static AUTO_ALLOW_BASH: AtomicBool = AtomicBool::new(false);
const BWRAP_ENV_VAR: &str = "__KIGI_INSIDE_BWRAP";
pub fn is_inside_bwrap() -> bool {
std::env::var(BWRAP_ENV_VAR).is_ok()
}
pub fn trust_bwrap_marker_for_devbox() -> bool {
false
}
struct GlobalSandboxState {
profile: String,
logger: SandboxLogger,
applied: bool,
}
/// Whether child subprocesses should have network blocked via seccomp.
pub fn should_restrict_child_network() -> bool {
RESTRICT_CHILD_NETWORK.load(Ordering::Relaxed)
}
/// Whether bash commands should be auto-approved when the sandbox is active.
pub fn should_auto_allow_bash() -> bool {
AUTO_ALLOW_BASH.load(Ordering::Relaxed) && is_active()
}
pub fn set_auto_allow_bash(enabled: bool) {
AUTO_ALLOW_BASH.store(enabled, Ordering::Relaxed);
}
/// Record the resolved sandbox profile at process startup (including `"off"`).
pub fn set_configured_profile(name: impl Into<String>) {
let _ = CONFIGURED_PROFILE.set(name.into());
}
/// Resolved sandbox profile from startup, or `None` if `set_configured_profile` was never called.
pub fn configured_profile_name() -> Option<&'static str> {
CONFIGURED_PROFILE.get().map(|s| s.as_str())
}
/// Whether the sandbox was successfully applied to this process.
pub fn is_active() -> bool {
SANDBOX.get().is_some_and(|s| s.applied)
}
/// The active sandbox profile name, or `None` if sandbox is not applied.
pub fn profile_name() -> Option<&'static str> {
SANDBOX
.get()
.filter(|s| s.applied)
.map(|s| s.profile.as_str())
}
/// Log a sandbox violation. Immediately flushed to disk.
/// No-op if sandbox is not active.
pub fn log_violation(target: &str, operation: &str) {
if let Some(state) = SANDBOX.get() {
state.logger.log(SandboxEvent::fs_violation(
&state.profile,
target,
operation,
));
let _ = state.logger.flush_to_disk();
}
}
/// Flush sandbox events to disk. No-op if not initialized.
pub fn flush() {
if let Some(state) = SANDBOX.get()
&& let Err(e) = state.logger.flush_to_disk()
{
tracing::warn!(error = % e, "Failed to flush sandbox events to disk");
}
}
/// Violation metrics, or `None` if sandbox is not active.
pub fn metrics() -> Option<&'static SandboxMetrics> {
SANDBOX.get().map(|s| s.logger.metrics())
}
/// Manages the OS-level sandbox. Call `apply()` then `install()`.
pub struct SandboxManager {
profile: ProfileName,
logger: SandboxLogger,
net_restricted: bool,
applied: bool,
}
impl SandboxManager {
/// Create a sandbox manager. Does not apply until `apply()` is called.
pub fn new(profile: ProfileName, _workspace: &Path) -> Self {
let net_restricted = profile.restricts_network();
Self {
profile,
logger: SandboxLogger::new(),
net_restricted,
applied: false,
}
}
/// Apply the sandbox to the current process. **Irreversible.**
/// Degrades gracefully if the platform doesn't support it.
#[cfg(all(feature = "enforce", unix))]
pub fn apply(&mut self, workspace: &Path) -> anyhow::Result<()> {
if self.profile == ProfileName::Off {
tracing::info!("Sandbox disabled (profile: off)");
return Ok(());
}
let support = Sandbox::support_info();
if !support.is_supported {
tracing::warn!(
details = % support.details,
"Sandbox not supported on this platform, continuing without sandbox"
);
self.logger.log(SandboxEvent::apply_failed(
&self.profile.to_string(),
workspace,
&support.details,
));
return Ok(());
}
let config = profiles::load_sandbox_config(workspace);
let caps = self
.profile
.to_capability_set_with_config(workspace, &config)?;
let mut resolved = self.profile.resolve_profile(workspace, &config)?;
resolved.deny = deny::effective_deny_paths(workspace, &resolved.deny);
self.net_restricted = self.profile.restricts_network_resolved(&config);
match Sandbox::apply(&caps) {
Ok(_) => {
self.applied = true;
if self.net_restricted {
RESTRICT_CHILD_NETWORK.store(true, Ordering::Relaxed);
}
self.logger.log(SandboxEvent::profile_applied(
&self.profile.to_string(),
workspace,
&resolved,
));
tracing::info!(
profile = % self.profile, workspace = % workspace.display(),
restrict_network = self.net_restricted,
"Sandbox applied (kernel-enforced, irreversible)"
);
Ok(())
}
Err(e) => {
tracing::warn!(
profile = % self.profile, error = % e,
"Sandbox could not be applied, continuing without sandbox"
);
self.logger.log(SandboxEvent::apply_failed(
&self.profile.to_string(),
workspace,
&e,
));
Ok(())
}
}
}
/// Stub when `enforce` feature is disabled — sandbox is not applied.
#[cfg(not(all(feature = "enforce", unix)))]
pub fn apply(&mut self, _workspace: &Path) -> anyhow::Result<()> {
tracing::info!(
profile = % self.profile,
"Sandbox enforcement unavailable (built without 'enforce' feature)"
);
Ok(())
}
/// Store globally for session-lifetime violation logging.
pub fn install(self) {
let _ = self.logger.flush_to_disk();
let _ = SANDBOX.set(GlobalSandboxState {
profile: self.profile.to_string(),
logger: self.logger,
applied: self.applied,
});
}
/// Check whether the current platform supports sandboxing.
#[cfg(all(feature = "enforce", unix))]
pub fn support_info() -> nono::SupportInfo {
Sandbox::support_info()
}
/// Whether the sandbox was successfully applied.
pub fn is_applied(&self) -> bool {
self.applied
}
/// Whether child subprocesses should have network blocked.
pub fn restrict_child_network(&self) -> bool {
self.applied && self.net_restricted
}
/// The active profile name.
pub fn profile(&self) -> &ProfileName {
&self.profile
}
/// Access the sandbox event logger (before `install()`).
pub fn logger(&self) -> &SandboxLogger {
&self.logger
}
}
/// Build a bwrap command that re-execs the current process with
/// `deny_write` paths mounted read-only and `deny_read` paths bound
/// over with an unreadable placeholder (EPERM on read).
///
/// Returns `None` if already inside bwrap. Caller should `cmd.exec()` the result.
pub fn bwrap_reexec_command(
deny_write: &[&str],
deny_read: &[&str],
) -> Option<std::process::Command> {
if is_inside_bwrap() {
return None;
}
let self_exe = std::env::current_exe().ok()?;
let args: Vec<String> = std::env::args().skip(1).collect();
let mut cmd = std::process::Command::new("bwrap");
cmd.arg("--bind").arg("/").arg("/");
for path in deny_write {
if Path::new(path).exists() {
cmd.arg("--ro-bind").arg(path).arg(path);
}
}
#[cfg(target_os = "linux")]
if !deny_read.is_empty() {
for path in deny_read {
let Some(blocked) = bwrap_blocked_source_for_path(Path::new(path)) else {
eprintln!(
"error: could not create bwrap placeholder for read-deny path {path}; \
refusing to start with a partial sandbox"
);
return None;
};
cmd.arg("--ro-bind").arg(&blocked).arg(path);
}
}
#[cfg(not(target_os = "linux"))]
let _ = deny_read;
cmd.arg("--dev-bind").arg("/dev").arg("/dev");
cmd.arg("--proc").arg("/proc");
cmd.env(BWRAP_ENV_VAR, "1");
cmd.arg("--").arg(self_exe).args(args);
Some(cmd)
}
/// Choose file vs directory placeholder for a deny path (existing dirs need a dir bind).
#[cfg(all(feature = "enforce", target_os = "linux"))]
fn bwrap_blocked_source_for_path(path: &Path) -> Option<PathBuf> {
if deny::deny_path_is_dir(path) {
bwrap_blocked_placeholder("sandbox-blocked-dir", true)
} else {
bwrap_blocked_placeholder("sandbox-blocked", false)
}
}
/// Without kernel enforcement there are no read-deny placeholders to bind over.
#[cfg(all(not(feature = "enforce"), target_os = "linux"))]
fn bwrap_blocked_source_for_path(_path: &Path) -> Option<PathBuf> {
None
}
/// chmod a placeholder to mode 000 so a bwrap bind-over yields EPERM on read.
#[cfg(all(feature = "enforce", target_os = "linux"))]
fn chmod_000(path: &Path) -> Option<()> {
use std::os::unix::fs::PermissionsExt;
let mut perms = std::fs::metadata(path).ok()?.permissions();
perms.set_mode(0o000);
std::fs::set_permissions(path, perms).ok()?;
Some(())
}
/// Zero-permission placeholder (file or dir) under `kigi_home` used by bwrap bind-over.
///
/// The placeholder name is suffixed with the current PID so concurrent kigi
/// processes don't race each other's create/remove/chmod on a shared path (which
/// could yield `None` and the silent dropped-bind fail-open this avoids).
#[cfg(all(feature = "enforce", target_os = "linux"))]
fn bwrap_blocked_placeholder(name: &str, want_dir: bool) -> Option<PathBuf> {
use std::fs::OpenOptions;
let path = paths::kigi_home().join(format!("{name}.{}", std::process::id()));
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).ok()?;
}
if path.exists() {
if path.is_dir() == want_dir {
chmod_000(&path)?;
return Some(path);
}
if path.is_dir() {
std::fs::remove_dir_all(&path).ok()?;
} else {
std::fs::remove_file(&path).ok()?;
}
}
if want_dir {
std::fs::create_dir(&path).ok()?;
} else {
OpenOptions::new()
.create(true)
.truncate(true)
.write(true)
.open(&path)
.ok()?;
}
chmod_000(&path)?;
Some(path)
}
/// Whether a profile write-denies `/data` via the devbox bwrap bind (built-in
/// `devbox` or a custom profile that `extends = "devbox"`). This is a pure mount,
/// so it applies even WITHOUT the `enforce` feature.
#[cfg(target_os = "linux")]
fn is_devbox_based(profile: &ProfileName, config: &SandboxConfig) -> bool {
match profile {
ProfileName::Devbox => true,
ProfileName::Custom(name) => {
config.profiles.get(name).and_then(|p| p.extends.as_deref()) == Some("devbox")
}
_ => false,
}
}
/// Whether kernel read-deny enforcement is required. The single source of truth
/// for this classification so callers (e.g. the shell's fail-closed startup path)
/// cannot drift and silently fail open.
///
/// Decided directly from the profile config (a `Custom` profile with a non-empty
/// `deny`) — NOT from the resolved/expanded deny set, which returns empty on
/// failure. Keying "requires" on that empty-on-error result would silently
/// downgrade to fail-open (Linux) when resolution hiccups; this intrinsic check
/// stays fail-closed.
#[cfg(all(feature = "enforce", unix))]
pub fn requires_read_deny(profile: &ProfileName, workspace: &Path) -> bool {
match profile {
ProfileName::Custom(name) => {
let config = profiles::load_sandbox_config(workspace);
config
.profiles
.get(name)
.is_some_and(|p| !p.deny.is_empty())
}
_ => false,
}
}
/// Stub when `enforce` is unavailable — nothing is kernel-enforced.
#[cfg(not(all(feature = "enforce", unix)))]
pub fn requires_read_deny(_profile: &ProfileName, _workspace: &Path) -> bool {
false
}
/// A profile's resolved bwrap deny plan: read-only mounts (`deny_write`),
/// bound-over unreadable placeholders (`deny_read`), and whether the profile
/// carries deny globs (`has_globs`, so the re-exec proceeds even with zero
/// current matches — globs are best-effort on Linux).
#[cfg(target_os = "linux")]
struct BwrapDenyPlan {
deny_write: Vec<String>,
deny_read: Vec<String>,
has_globs: bool,
}
/// Resolve a profile's full [`BwrapDenyPlan`] in ONE config read: the `/data`
/// write-deny (devbox and devbox-extending customs), the exact read-deny paths,
/// and the launch-time glob expansion. Returns `None` (fail closed) if a deny
/// glob blows past the expansion caps or is invalid, so
/// [`bwrap_reexec_for_profile`] refuses to start.
///
/// Best-effort on Linux: a mount namespace can't glob at runtime, so globs are
/// expanded once here at launch — files matching them that are created LATER are
/// NOT covered (macOS Seatbelt enforces the same globs as runtime regexes).
#[cfg(all(feature = "enforce", target_os = "linux"))]
fn bwrap_deny_plan(profile: &ProfileName, workspace: &Path) -> Option<BwrapDenyPlan> {
let config = profiles::load_sandbox_config(workspace);
let deny_write: Vec<String> = if is_devbox_based(profile, &config) {
vec!["/data".to_string()]
} else {
Vec::new()
};
let entries = if *profile == ProfileName::Off {
Vec::new()
} else {
profile
.resolve_profile(workspace, &config)
.map(|r| r.deny)
.unwrap_or_default()
};
let (exact, globs) = deny::partition_deny_entries(&entries);
let mut deny_read = deny::exact_deny_path_strings(workspace, &exact);
let has_globs = !globs.is_empty();
if has_globs {
tracing::warn!(
count = globs.len(),
"sandbox deny globs are enforced best-effort on Linux (expanded at launch); \
files matching them that are created later are NOT covered"
);
deny_read.extend(deny::expand_deny_globs(
workspace,
&globs,
deny::DENY_GLOB_MAX_DEPTH,
deny::DENY_GLOB_MAX_MATCHES,
deny::DENY_GLOB_MAX_ENTRIES,
)?);
}
Some(BwrapDenyPlan {
deny_write,
deny_read,
has_globs,
})
}
/// Stub when `enforce` is unavailable on Linux: read-deny needs nono, so there is
/// none — but the devbox `/data` write-deny is a plain bwrap mount and MUST still
/// apply (devbox `/data` is always sandboxed), so it is preserved here.
#[cfg(all(not(feature = "enforce"), target_os = "linux"))]
fn bwrap_deny_plan(profile: &ProfileName, workspace: &Path) -> Option<BwrapDenyPlan> {
let config = profiles::load_sandbox_config(workspace);
let deny_write: Vec<String> = if is_devbox_based(profile, &config) {
vec!["/data".to_string()]
} else {
Vec::new()
};
Some(BwrapDenyPlan {
deny_write,
deny_read: Vec::new(),
has_globs: false,
})
}
/// Build the bwrap re-exec command needed on Linux, or `None` if no mount-namespace
/// enforcement is needed (or we are already inside bwrap). Canonical routing:
/// devbox — and a custom profile that `extends = "devbox"` — gets write-deny on
/// `/data`; any profile gets read-deny on its own `deny` set. These compose, so a
/// devbox-based custom profile with a `deny` list write-denies `/data` AND
/// read-denies its deny paths in one re-exec.
///
/// Glob deny entries are expanded to concrete existing matches at launch and
/// bound over too (best-effort; post-launch matches are not covered on Linux).
/// Returns `None` (fail closed) if a glob blows past the expansion caps, so the
/// shell's startup refuses to run with a broad glob under-enforced.
#[cfg(target_os = "linux")]
pub fn bwrap_reexec_for_profile(
profile: &ProfileName,
workspace: &Path,
) -> Option<std::process::Command> {
let BwrapDenyPlan {
deny_write,
deny_read,
has_globs,
} = bwrap_deny_plan(profile, workspace)?;
if deny_write.is_empty() && deny_read.is_empty() && !has_globs {
return None;
}
let write_refs: Vec<&str> = deny_write.iter().map(String::as_str).collect();
let read_refs: Vec<&str> = deny_read.iter().map(String::as_str).collect();
bwrap_reexec_command(&write_refs, &read_refs)
}
#[cfg(test)]
mod tests {
use super::*;
use serial_test::serial;
/// Save, set/remove, and auto-restore an env var on drop.
struct EnvGuard {
key: &'static str,
prev: Option<String>,
}
impl EnvGuard {
fn set(key: &'static str, val: &str) -> Self {
let prev = std::env::var(key).ok();
unsafe { std::env::set_var(key, val) };
Self { key, prev }
}
fn remove(key: &'static str) -> Self {
let prev = std::env::var(key).ok();
unsafe { std::env::remove_var(key) };
Self { key, prev }
}
}
impl Drop for EnvGuard {
fn drop(&mut self) {
match &self.prev {
Some(v) => unsafe { std::env::set_var(self.key, v) },
None => unsafe { std::env::remove_var(self.key) },
}
}
}
#[test]
#[serial(bwrap_env)]
fn bwrap_reexec_returns_none_inside_bwrap() {
let _g = EnvGuard::set(BWRAP_ENV_VAR, "1");
let result = bwrap_reexec_command(&["/data"], &[]);
assert!(
result.is_none(),
"should return None when already inside bwrap"
);
}
#[test]
#[serial(bwrap_env)]
fn bwrap_reexec_returns_some_outside_bwrap() {
let _g = EnvGuard::remove(BWRAP_ENV_VAR);
let result = bwrap_reexec_command(&["/tmp"], &[]);
assert!(result.is_some(), "should return Some when not inside bwrap");
let cmd = result.unwrap();
assert_eq!(cmd.get_program(), "bwrap", "program should be bwrap");
}
#[test]
#[serial(bwrap_env)]
fn trust_bwrap_marker_for_devbox_tracks_env_when_feature_on() {
let _g = EnvGuard::set(BWRAP_ENV_VAR, "1");
assert!(
!trust_bwrap_marker_for_devbox(),
"without bwrap-marker the hatch must stay closed"
);
}
#[test]
#[serial(bwrap_env)]
fn trust_bwrap_marker_for_devbox_false_outside_bwrap() {
let _g = EnvGuard::remove(BWRAP_ENV_VAR);
assert!(!trust_bwrap_marker_for_devbox());
assert!(!is_inside_bwrap());
}
#[test]
#[serial(bwrap_env)]
fn bwrap_reexec_skips_nonexistent_paths() {
let _g = EnvGuard::remove(BWRAP_ENV_VAR);
let result = bwrap_reexec_command(&["/nonexistent-test-path-xyz-12345"], &[]);
let cmd = result.unwrap();
let args: Vec<String> = cmd
.get_args()
.map(|a| a.to_string_lossy().to_string())
.collect();
assert!(
!args.iter().any(|a| a == "/nonexistent-test-path-xyz-12345"),
"should skip non-existent deny_write paths, got args: {args:?}"
);
}
#[test]
#[serial(bwrap_env)]
#[cfg(all(feature = "enforce", target_os = "linux"))]
fn bwrap_reexec_binds_nonexistent_deny_read_paths() {
let _g = EnvGuard::remove(BWRAP_ENV_VAR);
let missing = "/nonexistent-deny-read-path-xyz-12345";
let result = bwrap_reexec_command(&[], &[missing]);
let cmd = result.unwrap();
let args: Vec<String> = cmd
.get_args()
.map(|a| a.to_string_lossy().to_string())
.collect();
let has_bind = args
.windows(3)
.any(|w| w[0] == "--ro-bind" && w[2] == missing);
assert!(
has_bind,
"should bind-over non-existent deny_read paths, got args: {args:?}"
);
}
#[test]
#[serial(bwrap_env)]
fn bwrap_reexec_mounts_existing_paths_read_only() {
let _g = EnvGuard::remove(BWRAP_ENV_VAR);
let result = bwrap_reexec_command(&["/tmp"], &[]);
let cmd = result.unwrap();
let args: Vec<String> = cmd
.get_args()
.map(|a| a.to_string_lossy().to_string())
.collect();
let has_ro_bind = args.windows(3).any(|w| w == ["--ro-bind", "/tmp", "/tmp"]);
assert!(
has_ro_bind,
"should mount existing paths as --ro-bind, got args: {args:?}"
);
}
#[test]
#[serial(bwrap_env)]
fn bwrap_reexec_uses_dev_bind() {
let _g = EnvGuard::remove(BWRAP_ENV_VAR);
let result = bwrap_reexec_command(&[], &[]);
let cmd = result.unwrap();
let args: Vec<String> = cmd
.get_args()
.map(|a| a.to_string_lossy().to_string())
.collect();
let has_dev_bind = args.windows(3).any(|w| w == ["--dev-bind", "/dev", "/dev"]);
assert!(
has_dev_bind,
"should use --dev-bind for /dev passthrough, got args: {args:?}"
);
}
#[test]
fn configured_profile_is_recorded() {
set_configured_profile("read-only");
assert_eq!(configured_profile_name(), Some("read-only"));
}
/// Create a temp workspace whose `.kigi/sandbox.toml` contains `toml_body`.
/// Returns the workspace path (caller removes it).
#[cfg(all(feature = "enforce", unix))]
fn temp_workspace_with_sandbox_toml(tag: &str, toml_body: &str) -> PathBuf {
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos();
let ws = std::env::temp_dir().join(format!("kigi-{tag}-{}-{nanos}", std::process::id()));
let kigi = ws.join(".kigi");
std::fs::create_dir_all(&kigi).unwrap();
std::fs::write(kigi.join("sandbox.toml"), toml_body).unwrap();
ws
}
/// Create a temp workspace defining a `denytest` profile (extends `workspace`)
/// with the given `deny` list. `deny_toml` is the raw TOML array body
/// (e.g. `"\".env\""`).
#[cfg(all(feature = "enforce", unix))]
fn temp_workspace_with_deny(tag: &str, deny_toml: &str) -> PathBuf {
temp_workspace_with_sandbox_toml(
tag,
&format!("[profiles.denytest]\nextends = \"workspace\"\ndeny = [{deny_toml}]\n"),
)
}
#[test]
#[cfg(all(feature = "enforce", unix))]
fn requires_read_deny_only_for_custom_profile_with_deny() {
let ws = temp_workspace_with_deny("requires-deny", "\".env\"");
assert!(requires_read_deny(
&ProfileName::Custom("denytest".to_string()),
&ws
));
assert!(!requires_read_deny(
&ProfileName::Custom("undefined".to_string()),
&ws
));
assert!(!requires_read_deny(&ProfileName::Workspace, &ws));
assert!(!requires_read_deny(&ProfileName::Strict, &ws));
assert!(!requires_read_deny(&ProfileName::Devbox, &ws));
assert!(!requires_read_deny(&ProfileName::Off, &ws));
let _ = std::fs::remove_dir_all(&ws);
}
#[test]
#[serial(bwrap_env)]
#[cfg(all(feature = "enforce", target_os = "linux"))]
fn bwrap_reexec_uses_dir_placeholder_for_directories() {
let _g = EnvGuard::remove(BWRAP_ENV_VAR);
let dir = std::env::temp_dir().join(format!("kigi-deny-dir-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let dir_str = dir.to_string_lossy().to_string();
let result = bwrap_reexec_command(&[], &[&dir_str]);
let cmd = result.unwrap();
let args: Vec<String> = cmd
.get_args()
.map(|a| a.to_string_lossy().to_string())
.collect();
let blocked_dir = paths::kigi_home()
.join(format!("sandbox-blocked-dir.{}", std::process::id()))
.to_string_lossy()
.to_string();
let has_dir_bind = args
.windows(3)
.any(|w| w[0] == "--ro-bind" && w[1] == blocked_dir && w[2] == dir_str);
assert!(
has_dir_bind,
"existing directories should bind over sandbox-blocked-dir, got args: {args:?}"
);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
#[serial(bwrap_env)]
#[cfg(all(feature = "enforce", target_os = "linux"))]
fn bwrap_reexec_for_profile_devbox_extends_composes_data_and_read_deny() {
let _g = EnvGuard::remove(BWRAP_ENV_VAR);
let ws = temp_workspace_with_sandbox_toml(
"devbox-compose",
"[profiles.devcustom]\nextends = \"devbox\"\ndeny = [\"secret.pem\"]\n",
);
let cmd = bwrap_reexec_for_profile(&ProfileName::Custom("devcustom".to_string()), &ws)
.expect("devbox-extending custom with deny should build a re-exec command");
let args: Vec<String> = cmd
.get_args()
.map(|a| a.to_string_lossy().to_string())
.collect();
let deny_path = ws.join("secret.pem").to_string_lossy().to_string();
assert!(
args.windows(3)
.any(|w| w[0] == "--ro-bind" && w[2] == deny_path),
"expected read-deny bind for {deny_path}, got args: {args:?}"
);
if Path::new("/data").exists() {
assert!(
args.windows(3)
.any(|w| w == ["--ro-bind", "/data", "/data"]),
"expected /data write-deny ro-bind, got args: {args:?}"
);
}
let _ = std::fs::remove_dir_all(&ws);
let ws_empty = temp_workspace_with_sandbox_toml(
"devbox-empty",
"[profiles.devempty]\nextends = \"devbox\"\n",
);
assert!(
bwrap_reexec_for_profile(&ProfileName::Custom("devempty".to_string()), &ws_empty)
.is_some(),
"devbox-extending custom must compose the /data write-deny re-exec"
);
let _ = std::fs::remove_dir_all(&ws_empty);
let ws_ws = temp_workspace_with_sandbox_toml(
"ws-empty",
"[profiles.wsempty]\nextends = \"workspace\"\n",
);
assert!(
bwrap_reexec_for_profile(&ProfileName::Custom("wsempty".to_string()), &ws_ws).is_none(),
"non-devbox custom with no deny needs no re-exec"
);
let _ = std::fs::remove_dir_all(&ws_ws);
}
}