#![cfg(target_os = "macos")]
use crate::defaults::{
CREDENTIAL_CONFIG_FULL_DENY, CREDENTIAL_CONFIG_SUBDIRS, CREDENTIAL_FILES, CREDENTIAL_SUBDIRS,
PROTECTED_PROJECT_SUBDIRS,
};
use crate::policy::SandboxPolicy;
use anyhow::Result;
use std::path::Path;
use tokio::process::Command;
pub fn build_command(
command: &str,
project_root: &Path,
policy: &SandboxPolicy,
) -> Result<Command> {
build_command_inner(command, project_root, policy, None)
}
pub fn build_command_with_proxy(
command: &str,
project_root: &Path,
policy: &SandboxPolicy,
proxy_port: u16,
allow_local_binding: bool,
weaker_macos_isolation: bool,
) -> Result<Command> {
build_command_inner(
command,
project_root,
policy,
Some((proxy_port, allow_local_binding, weaker_macos_isolation)),
)
}
fn build_command_inner(
command: &str,
project_root: &Path,
policy: &SandboxPolicy,
proxy: Option<(u16, bool, bool)>,
) -> Result<Command> {
let canonical = project_root
.canonicalize()
.unwrap_or_else(|_| project_root.to_path_buf());
let root = canonical.to_string_lossy();
let home = std::env::var("HOME").unwrap_or_else(|_| "/Users".into());
validate_seatbelt_path(&root)?;
validate_seatbelt_path(&home)?;
let key = crate::seatbelt_cache::ProfileKey {
canonical_root: canonical.clone(),
home: home.clone(),
proxy,
policy: policy.clone(),
};
let overlay = policy_overlay_rules(policy)?;
let profile = crate::seatbelt_cache::get_or_compute(key, |k| {
let root = k.canonical_root.to_string_lossy();
let home = &k.home;
let mut profile = match k.proxy {
Some((port, allow_local_binding, weaker_macos_isolation)) => {
build_proxied_profile_string(
&root,
home,
port,
allow_local_binding,
weaker_macos_isolation,
)
}
None => build_profile_string(&root, home),
};
profile.push_str(&protected_subdir_deny_rules(&root));
profile.push_str(&credential_deny_rules(home));
profile.push_str(&overlay);
profile
});
let mut cmd = Command::new("sandbox-exec");
cmd.arg("-p")
.arg(profile)
.arg("sh")
.arg("-c")
.arg(command)
.current_dir(project_root);
Ok(cmd)
}
pub fn is_available() -> bool {
use std::sync::OnceLock;
static AVAILABLE: OnceLock<bool> = OnceLock::new();
*AVAILABLE.get_or_init(|| {
build_command(
"true",
Path::new("/tmp"),
&crate::policy::SandboxPolicy::strict_default(),
)
.is_ok()
})
}
fn validate_seatbelt_path(s: &str) -> Result<()> {
const FORBIDDEN: &[char] = &['"', '\\', '(', ')', '\0'];
if let Some(c) = s.chars().find(|c| FORBIDDEN.contains(c)) {
anyhow::bail!("Path contains character {c:?} unsafe for seatbelt profile: {s:?}");
}
Ok(())
}
pub(crate) fn home_path(home: &str, rel: &str) -> String {
let p = Path::new(home).join(rel);
p.canonicalize().unwrap_or(p).to_string_lossy().into_owned()
}
pub(crate) fn build_profile_string_with_network(
root: &str,
home: &str,
network_rules: &str,
) -> String {
format!(
"(version 1)\n\
(deny default)\n\
(allow file-read*)\n\
(allow file-write*\n\
(subpath \"{root}\")\n\
(subpath \"/private/tmp\")\n\
(subpath \"/tmp\")\n\
(subpath \"{home}/.cargo\")\n\
(subpath \"{home}/.npm\")\n\
(subpath \"{home}/.cache\")\n\
(literal \"/dev/null\")\n\
(literal \"/dev/stderr\")\n\
(literal \"/dev/stdout\")\n\
(literal \"/dev/urandom\"))\n\
{network_rules}\
(allow process-exec*)\n\
(allow process-fork)\n\
(allow sysctl-read)\n\
(allow ipc-posix*)\n\
(allow mach*)\n"
)
}
pub(crate) fn build_profile_string(root: &str, home: &str) -> String {
build_profile_string_with_network(root, home, &network_open_rules())
}
pub(crate) fn network_open_rules() -> String {
"(allow network*)\n".to_string()
}
pub fn network_proxied_rules(
proxy_port: u16,
allow_local_binding: bool,
weaker_macos_isolation: bool,
) -> String {
let mut rules = String::new();
rules.push_str(&format!(
"(allow network-outbound (remote tcp \"localhost:{proxy_port}\"))\n"
));
rules.push_str("(allow network-outbound (remote unix-socket))\n");
rules.push_str("(allow network-inbound (local ip \"localhost:*\"))\n");
rules.push_str("(allow network-bind (local ip \"localhost:*\"))\n");
if allow_local_binding {
rules.push_str("(allow network-bind (local ip \"*:*\"))\n");
}
if weaker_macos_isolation {
rules.push_str(&trustd_mach_lookup_rules());
}
rules
}
fn trustd_mach_lookup_rules() -> String {
let mut rules = String::from(
"; \u{2500}\u{2500} weaker_macos_isolation: Apple trustd for Go-style out-of-process TLS \u{2500}\u{2500}\n",
);
rules.push_str("(allow mach-lookup (global-name \"com.apple.trustd\"))\n");
rules.push_str("(allow mach-lookup (global-name \"com.apple.trustd.agent\"))\n");
rules
}
pub fn build_proxied_profile_string(
root: &str,
home: &str,
proxy_port: u16,
allow_local_binding: bool,
weaker_macos_isolation: bool,
) -> String {
let net = network_proxied_rules(proxy_port, allow_local_binding, weaker_macos_isolation);
build_profile_string_with_network(root, home, &net)
}
pub(crate) fn protected_subdir_deny_rules(root: &str) -> String {
let mut rules = String::from(
"; ── deny writes to protected project subdirs (.koda/agents, .koda/skills) ──\n",
);
for rel in PROTECTED_PROJECT_SUBDIRS {
let p = Path::new(root).join(rel);
let canonical = p.canonicalize().unwrap_or(p).to_string_lossy().into_owned();
rules.push_str(&format!("(deny file-write* (subpath \"{canonical}\"))\n"));
}
rules
}
pub(crate) fn credential_deny_rules(home: &str) -> String {
let mut rules = String::from("; ── strict: write-protect credential dirs (reads allowed) ──\n");
for rel in CREDENTIAL_SUBDIRS {
let p = home_path(home, rel);
rules.push_str(&format!("(deny file-write* (subpath \"{p}\"))\n"));
}
for rel in CREDENTIAL_CONFIG_SUBDIRS {
let p = home_path(home, &format!(".config/{rel}"));
rules.push_str(&format!("(deny file-write* (subpath \"{p}\"))\n"));
}
for rel in CREDENTIAL_FILES {
let p = home_path(home, rel);
rules.push_str(&format!("(deny file-write* (literal \"{p}\"))\n"));
}
rules.push_str("; ── strict: full deny for koda-internal secrets ─────────────\n");
for rel in CREDENTIAL_CONFIG_FULL_DENY {
let p = home_path(home, &format!(".config/{rel}"));
rules.push_str(&format!(
"(deny file-read* file-write* (subpath \"{p}\"))\n"
));
}
rules
}
pub(crate) fn policy_overlay_rules(policy: &SandboxPolicy) -> Result<String> {
let fs = &policy.fs;
if fs.deny_read.is_empty()
&& fs.allow_read_within_deny.is_empty()
&& fs.allow_write.is_empty()
&& fs.deny_write_within_allow.is_empty()
{
return Ok(String::new());
}
let mut rules =
String::from("; \u{2500}\u{2500} policy overlay (Phase 1b of #934) \u{2500}\u{2500}\n");
for p in &fs.deny_read {
let s = p.as_path().to_string_lossy();
validate_seatbelt_path(&s)?;
rules.push_str(&format!("(deny file-read* (subpath \"{s}\"))\n"));
}
for p in &fs.allow_read_within_deny {
let s = p.as_path().to_string_lossy();
validate_seatbelt_path(&s)?;
rules.push_str(&format!("(allow file-read* (subpath \"{s}\"))\n"));
}
for p in &fs.allow_write {
let s = p.as_path().to_string_lossy();
validate_seatbelt_path(&s)?;
rules.push_str(&format!("(allow file-write* (subpath \"{s}\"))\n"));
}
for p in &fs.deny_write_within_allow {
let s = p.as_path().to_string_lossy();
validate_seatbelt_path(&s)?;
rules.push_str(&format!("(deny file-write* (subpath \"{s}\"))\n"));
}
Ok(rules)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn strict_profile_write_protects_ssh_dir() {
let home = std::env::var("HOME").unwrap_or_else(|_| "/Users/test".into());
let rules = credential_deny_rules(&home);
let ssh = home_path(&home, ".ssh");
assert!(
rules.contains(&format!("(deny file-write* (subpath \"{ssh}\"))")),
"strict profile must write-protect ~/.ssh"
);
assert!(
!rules.contains(&format!(
"(deny file-read* file-write* (subpath \"{ssh}\"))"
)),
"strict profile must NOT read-deny ~/.ssh (breaks ssh/git)"
);
}
#[test]
fn strict_profile_write_protects_aws_dir() {
let home = std::env::var("HOME").unwrap_or_else(|_| "/Users/test".into());
let rules = credential_deny_rules(&home);
let aws = home_path(&home, ".aws");
assert!(
rules.contains(&format!("(deny file-write* (subpath \"{aws}\"))")),
"strict profile must write-protect ~/.aws"
);
assert!(
!rules.contains(&format!(
"(deny file-read* file-write* (subpath \"{aws}\"))"
)),
"strict profile must NOT read-deny ~/.aws (breaks aws CLI)"
);
}
#[test]
fn strict_profile_write_protects_gh_dir() {
let home = std::env::var("HOME").unwrap_or_else(|_| "/Users/test".into());
let rules = credential_deny_rules(&home);
let gh = home_path(&home, ".config/gh");
assert!(
rules.contains(&format!("(deny file-write* (subpath \"{gh}\"))")),
"strict profile must write-protect ~/.config/gh"
);
assert!(
!rules.contains(&format!("(deny file-read* file-write* (subpath \"{gh}\"))")),
"strict profile must NOT read-deny ~/.config/gh (breaks gh CLI, #855)"
);
}
#[test]
fn strict_profile_write_protects_claude_dir() {
let home = std::env::var("HOME").unwrap_or_else(|_| "/Users/test".into());
let rules = credential_deny_rules(&home);
let claude = home_path(&home, ".claude");
assert!(
rules.contains(&format!("(deny file-write* (subpath \"{claude}\"))")),
"strict profile must write-protect ~/.claude"
);
}
#[test]
fn strict_profile_write_protects_android_dir() {
let home = std::env::var("HOME").unwrap_or_else(|_| "/Users/test".into());
let rules = credential_deny_rules(&home);
let android = home_path(&home, ".android");
assert!(
rules.contains(&format!("(deny file-write* (subpath \"{android}\"))")),
"strict profile must write-protect ~/.android"
);
}
#[test]
fn strict_profile_write_protects_netlify_dir() {
let home = std::env::var("HOME").unwrap_or_else(|_| "/Users/test".into());
let rules = credential_deny_rules(&home);
let netlify = home_path(&home, ".config/netlify");
assert!(
rules.contains(&format!("(deny file-write* (subpath \"{netlify}\"))")),
"strict profile must write-protect ~/.config/netlify"
);
}
#[test]
fn strict_profile_write_protects_vercel_dir() {
let home = std::env::var("HOME").unwrap_or_else(|_| "/Users/test".into());
let rules = credential_deny_rules(&home);
let vercel = home_path(&home, ".config/vercel");
assert!(
rules.contains(&format!("(deny file-write* (subpath \"{vercel}\"))")),
"strict profile must write-protect ~/.config/vercel"
);
}
#[test]
fn strict_profile_fully_denies_koda_db() {
let home = std::env::var("HOME").unwrap_or_else(|_| "/Users/test".into());
let rules = credential_deny_rules(&home);
let koda_db = home_path(&home, ".config/koda/db");
assert!(
rules.contains(&format!(
"(deny file-read* file-write* (subpath \"{koda_db}\"))"
)),
"strict profile must fully deny ~/.config/koda/db (plaintext API keys, #847)"
);
}
#[test]
fn strict_profile_write_protects_credential_files() {
let home = std::env::var("HOME").unwrap_or_else(|_| "/Users/test".into());
let rules = credential_deny_rules(&home);
let netrc = home_path(&home, ".netrc");
assert!(
rules.contains(&format!("(deny file-write* (literal \"{netrc}\"))")),
"strict profile must write-protect ~/.netrc"
);
assert!(
!rules.contains(&format!(
"(deny file-read* file-write* (literal \"{netrc}\"))"
)),
"strict profile must NOT read-deny ~/.netrc (breaks curl/wget)"
);
}
#[test]
fn strict_deny_rules_come_after_broad_allow() {
let home = std::env::var("HOME").unwrap_or_else(|_| "/Users/test".into());
let project = Path::new("/tmp/test-project");
let root = project.to_string_lossy().into_owned();
let profile = build_profile_string(&root, &home);
let deny_rules = credential_deny_rules(&home);
let full = format!("{profile}{deny_rules}");
let allow_pos = full.find("(allow file-read*)").unwrap();
let deny_pos = full.find("(deny file-read* file-write*").unwrap();
assert!(
deny_pos > allow_pos,
"deny rules must appear after the broad allow (last-match-wins)"
);
let write_deny_pos = full.find("(deny file-write*").unwrap();
assert!(
write_deny_pos > allow_pos,
"write-deny rules must appear after the broad allow"
);
}
#[test]
fn validate_seatbelt_path_rejects_quote() {
assert!(validate_seatbelt_path("/tmp/evil\"').rs").is_err());
}
#[test]
fn validate_seatbelt_path_rejects_paren() {
assert!(validate_seatbelt_path("/tmp/(evil)").is_err());
}
#[test]
fn validate_seatbelt_path_accepts_normal() {
assert!(validate_seatbelt_path("/Users/me/Projects/koda").is_ok());
}
#[test]
fn policy_overlay_rules_empty_for_default_policy() {
let overlay = policy_overlay_rules(&SandboxPolicy::strict_default()).unwrap();
assert_eq!(overlay, "", "default policy must add zero rules");
}
#[test]
fn policy_overlay_rules_emit_deny_read_first_then_allow_within() {
let mut policy = SandboxPolicy::strict_default();
policy.fs.deny_read = vec!["/secrets".into()];
policy.fs.allow_read_within_deny = vec!["/secrets/public".into()];
let overlay = policy_overlay_rules(&policy).unwrap();
let deny_pos = overlay
.find(r#"(deny file-read* (subpath "/secrets"))"#)
.expect("deny rule expected");
let allow_pos = overlay
.find(r#"(allow file-read* (subpath "/secrets/public"))"#)
.expect("allow rule expected");
assert!(
deny_pos < allow_pos,
"deny must come before allow-within: {overlay}"
);
}
#[test]
fn policy_overlay_rules_emit_allow_write_then_deny_within() {
let mut policy = SandboxPolicy::strict_default();
policy.fs.allow_write = vec!["/work".into()];
policy.fs.deny_write_within_allow = vec!["/work/.git/config".into()];
let overlay = policy_overlay_rules(&policy).unwrap();
let allow_pos = overlay
.find(r#"(allow file-write* (subpath "/work"))"#)
.expect("allow rule expected");
let deny_pos = overlay
.find(r#"(deny file-write* (subpath "/work/.git/config"))"#)
.expect("deny rule expected");
assert!(
allow_pos < deny_pos,
"allow must come before deny-within: {overlay}"
);
}
#[test]
fn policy_overlay_rules_validates_paths_to_prevent_injection() {
let mut policy = SandboxPolicy::strict_default();
policy.fs.deny_read = vec![r#"/foo")(allow file-write*"#.into()];
let result = policy_overlay_rules(&policy);
assert!(result.is_err(), "injection-y path must be rejected");
}
#[test]
fn policy_overlay_rules_is_appended_after_baseline_in_build_command() {
let mut policy = SandboxPolicy::strict_default();
policy.fs.allow_read_within_deny = vec!["/etc/koda-marker".into()];
let cmd = build_command("true", Path::new("/tmp"), &policy).unwrap();
let args: Vec<String> = cmd
.as_std()
.get_args()
.map(|a| a.to_string_lossy().into_owned())
.collect();
let profile = &args[1];
let overlay_pos = profile
.find("policy overlay (Phase 1b of #934)")
.expect("overlay header missing");
let credential_pos = profile
.find("strict: write-protect credential dirs")
.expect("credential header missing");
assert!(
credential_pos < overlay_pos,
"policy overlay must come after credential rules"
);
}
#[test]
fn proxied_profile_omits_open_network_allow() {
let p = build_proxied_profile_string("/work", "/Users/x", 8877, false, false);
assert!(
!p.contains("(allow network*)\n"),
"proxied profile must not include the open-network allow\nprofile:\n{p}"
);
}
#[test]
fn proxied_profile_allows_only_proxy_port_outbound_tcp() {
let p = build_proxied_profile_string("/work", "/Users/x", 8877, false, false);
assert!(
p.contains("(allow network-outbound (remote tcp \"localhost:8877\"))"),
"profile must include the localhost:proxy_port allow; got:\n{p}"
);
assert!(
!p.contains("127.0.0.1:8877"),
"profile must NOT include literal 127.0.0.1 — SBPL rejects it; got:\n{p}"
);
let count = p.matches("(allow network-outbound (remote tcp ").count();
assert_eq!(
count, 1,
"expected exactly one (localhost) outbound rule; profile:\n{p}"
);
}
#[test]
fn proxied_profile_always_allows_unix_socket_outbound() {
let p = build_proxied_profile_string("/work", "/Users/x", 8877, false, false);
assert!(p.contains("(allow network-outbound (remote unix-socket))"));
}
#[test]
fn proxied_profile_allows_localhost_inbound_for_debuggers() {
let p = build_proxied_profile_string("/work", "/Users/x", 8877, false, false);
assert!(p.contains("(allow network-inbound (local ip \"localhost:*\"))"));
}
#[test]
fn proxied_profile_omits_wildcard_bind_when_local_binding_disabled() {
let p = build_proxied_profile_string("/work", "/Users/x", 8877, false, false);
assert!(p.contains("(allow network-bind (local ip \"localhost:*\"))"));
assert!(
!p.contains("(allow network-bind (local ip \"*:*\"))"),
"wildcard bind must be gated on allow_local_binding"
);
}
#[test]
fn proxied_profile_includes_wildcard_bind_when_local_binding_enabled() {
let p = build_proxied_profile_string("/work", "/Users/x", 8877, true, false);
assert!(p.contains("(allow network-bind (local ip \"*:*\"))"));
}
#[test]
fn proxied_profile_keeps_baseline_file_writes_unchanged() {
let p = build_proxied_profile_string("/work", "/Users/x", 8877, false, false);
assert!(p.contains("(subpath \"/work\")"));
assert!(p.contains("(subpath \"/private/tmp\")"));
assert!(p.contains("(subpath \"/Users/x/.cargo\")"));
assert!(p.contains("(subpath \"/Users/x/.npm\")"));
assert!(p.contains("(subpath \"/Users/x/.cache\")"));
}
#[test]
fn proxied_and_open_differ_only_in_network_section() {
let open = build_profile_string("/work", "/Users/x");
let proxied = build_proxied_profile_string("/work", "/Users/x", 8877, false, false);
let tail = "(allow process-exec*)\n(allow process-fork)\n(allow sysctl-read)\n(allow ipc-posix*)\n(allow mach*)\n";
assert!(open.ends_with(tail), "open profile tail mismatch:\n{open}");
assert!(
proxied.ends_with(tail),
"proxied profile tail mismatch:\n{proxied}"
);
}
#[test]
fn build_command_with_proxy_uses_proxied_profile() {
let cmd = build_command_with_proxy(
"true",
Path::new("/tmp"),
&SandboxPolicy::default(),
8877,
false,
false,
)
.unwrap();
let args: Vec<String> = cmd
.as_std()
.get_args()
.map(|a| a.to_string_lossy().into_owned())
.collect();
let profile = &args[1];
assert!(profile.contains("localhost:8877"));
assert!(!profile.contains("(allow network*)\n"));
}
#[test]
fn build_command_without_proxy_keeps_open_network() {
let cmd = build_command("true", Path::new("/tmp"), &SandboxPolicy::default()).unwrap();
let args: Vec<String> = cmd
.as_std()
.get_args()
.map(|a| a.to_string_lossy().into_owned())
.collect();
let profile = &args[1];
assert!(profile.contains("(allow network*)\n"));
assert!(!profile.contains("127.0.0.1:"));
}
#[test]
fn build_command_with_proxy_local_binding_propagates() {
let cmd = build_command_with_proxy(
"true",
Path::new("/tmp"),
&SandboxPolicy::default(),
8877,
true,
false,
)
.unwrap();
let args: Vec<String> = cmd
.as_std()
.get_args()
.map(|a| a.to_string_lossy().into_owned())
.collect();
let profile = &args[1];
assert!(profile.contains("(allow network-bind (local ip \"*:*\"))"));
}
#[test]
fn proxied_profile_omits_trustd_by_default() {
let p = build_proxied_profile_string("/work", "/Users/x", 8877, false, false);
assert!(
!p.contains("com.apple.trustd"),
"strict default must not permit trustd lookups; got:\n{p}"
);
assert!(
!p.contains("mach-lookup"),
"strict default must not contain any mach-lookup allow; got:\n{p}"
);
}
#[test]
fn proxied_profile_permits_trustd_when_weaker_isolation_set() {
let p = build_proxied_profile_string("/work", "/Users/x", 8877, false, true);
assert!(
p.contains("(allow mach-lookup (global-name \"com.apple.trustd\"))"),
"weaker isolation must permit com.apple.trustd; got:\n{p}"
);
assert!(
p.contains("(allow mach-lookup (global-name \"com.apple.trustd.agent\"))"),
"weaker isolation must permit com.apple.trustd.agent; got:\n{p}"
);
}
#[test]
fn weaker_isolation_does_not_widen_network_egress() {
let p = build_proxied_profile_string("/work", "/Users/x", 8877, false, true);
assert!(
!p.contains("(allow network*)\n"),
"weaker isolation must not include the open-network blanket allow; got:\n{p}"
);
assert!(
p.contains("(allow network-outbound (remote tcp \"localhost:8877\"))"),
"single-port loopback rule must remain intact; got:\n{p}"
);
}
#[test]
fn weaker_isolation_propagates_through_build_command_with_proxy() {
let cmd = build_command_with_proxy(
"true",
Path::new("/tmp"),
&SandboxPolicy::default(),
8877,
false,
true,
)
.unwrap();
let profile = cmd
.as_std()
.get_args()
.nth(1)
.unwrap()
.to_string_lossy()
.into_owned();
assert!(
profile.contains("com.apple.trustd"),
"weaker isolation flag must reach the profile; got:\n{profile}"
);
}
#[test]
fn trustd_rules_are_idempotent_on_section_header() {
let rules = trustd_mach_lookup_rules();
assert!(
rules.starts_with("; "),
"trustd block must lead with an SBPL comment; got:\n{rules}"
);
assert!(
rules.contains("weaker_macos_isolation"),
"comment must mention the policy field name for grep-ability; got:\n{rules}"
);
}
#[test]
fn build_command_returns_byte_identical_profile_on_repeated_calls() {
let policy = SandboxPolicy::default();
let cmd1 = build_command("true", Path::new("/tmp"), &policy).unwrap();
let cmd2 = build_command("true", Path::new("/tmp"), &policy).unwrap();
let p1 = cmd1
.as_std()
.get_args()
.nth(1)
.unwrap()
.to_string_lossy()
.into_owned();
let p2 = cmd2
.as_std()
.get_args()
.nth(1)
.unwrap()
.to_string_lossy()
.into_owned();
assert_eq!(p1, p2, "repeated build_command must emit identical SBPL");
assert!(
p1.contains("(version 1)"),
"profile must look real, not empty"
);
}
#[test]
fn cached_profile_still_includes_credential_denies() {
let policy = SandboxPolicy::default();
let _warm = build_command("true", Path::new("/tmp"), &policy).unwrap();
let cmd = build_command("true", Path::new("/tmp"), &policy).unwrap();
let profile = cmd
.as_std()
.get_args()
.nth(1)
.unwrap()
.to_string_lossy()
.into_owned();
assert!(
profile.contains(".ssh"),
"cached profile must still carry the credential deny rules; got:\n{profile}"
);
}
#[test]
fn proxy_and_open_profiles_do_not_share_cache_entry() {
let policy = SandboxPolicy::default();
let cmd_open = build_command("true", Path::new("/tmp"), &policy).unwrap();
let cmd_proxied =
build_command_with_proxy("true", Path::new("/tmp"), &policy, 8877, false, false)
.unwrap();
let p_open = cmd_open
.as_std()
.get_args()
.nth(1)
.unwrap()
.to_string_lossy()
.into_owned();
let p_proxied = cmd_proxied
.as_std()
.get_args()
.nth(1)
.unwrap()
.to_string_lossy()
.into_owned();
assert!(
p_open.contains("(allow network*)\n"),
"open profile must contain blanket network allow"
);
assert!(
!p_proxied.contains("(allow network*)\n"),
"proxied profile must NOT contain blanket allow (would bypass kernel egress enforcement)"
);
assert!(
p_proxied.contains("localhost:8877"),
"proxied profile must pin to the loopback proxy port"
);
}
}