#![cfg_attr(
not(test),
expect(
dead_code,
reason = "SL-182 PHASE-02 pure jail core; consumed by the PHASE-03 pretooluse shell"
)
)]
use std::ffi::OsString;
use std::path::{Path, PathBuf};
use base64::Engine;
use serde::{Deserialize, Serialize};
const BWRAP: &str = "bwrap";
const FLAG_RO_BIND: &str = "--ro-bind";
const FLAG_DEV: &str = "--dev";
const FLAG_PROC: &str = "--proc";
const FLAG_TMPFS: &str = "--tmpfs";
const FLAG_BIND: &str = "--bind";
const FLAG_CHDIR: &str = "--chdir";
const FLAG_DIE_WITH_PARENT: &str = "--die-with-parent";
const FLAG_UNSHARE_NET: &str = "--unshare-net";
const FLAG_ARG_SEP: &str = "--";
const PATH_ROOT: &str = "/";
const PATH_DEV: &str = "/dev";
const PATH_PROC: &str = "/proc";
const PATH_TMP: &str = "/tmp";
const SHELL_BIN: &str = "bash";
const SHELL_CMD_FLAG: &str = "-c";
const REASON_NOT_WORKTREE: &str = "cwd-not-a-worktree";
const REASON_NO_FILE_PATH: &str = "no-file-path";
const REASON_ESCAPES_WORKTREE: &str = "escapes-worktree";
const REASON_NO_BACKEND: &str = "no-jail-backend";
const REASON_MAC_NOT_WORKTREE: &str = "seatbelt-cwd-not-a-worktree";
const REASON_MAC_MAIN_CHECKOUT: &str = "seatbelt-cwd-is-main-checkout";
const REASON_MAC_AMBIGUOUS_GITDIRS: &str = "seatbelt-ambiguous-gitdirs";
const REASON_MAC_POLICY_MISSING: &str = "seatbelt-policy-missing";
const REASON_MAC_POLICY_MALFORMED: &str = "seatbelt-policy-malformed";
const GIT_DIR: &str = ".git";
const WT_TMP_SUBDIR: &str = ".tmp";
const SANDBOX_EXEC: &str = "sandbox-exec";
const FLAG_D: &str = "-D";
const FLAG_F: &str = "-f";
const ENV_BIN: &str = "env";
const PARAM_WT: &str = "WT";
const PARAM_TMP: &str = "TMP";
const PARAM_PTMP: &str = "PTMP";
const PARAM_DUTMP: &str = "DUTMP";
const PARAM_RW_PREFIX: &str = "RW";
const ENV_TMPDIR: &str = "TMPDIR";
const SEATBELT_PROFILE_FILE: &str = "jail.sb";
const PTMP_LITERAL: &str = "/private/tmp";
const SB_VERSION: &str = "(version 1)";
const SB_ALLOW_DEFAULT: &str = "(allow default)";
const SB_DENY_WRITE_FLOOR: &str = "(deny file-write*)";
const DEVICE_SINK_ALLOWS: &[&str] = &[
r#"(allow file-write* (literal "/dev/null"))"#,
r#"(allow file-write* (literal "/dev/zero"))"#,
r#"(allow file-write* (literal "/dev/random"))"#,
r#"(allow file-write* (literal "/dev/urandom"))"#,
r#"(allow file-write* (regex #"^/dev/tty"))"#,
];
const XCRUN_DB_REGEX: &str = r#"#"/xcrun_db[^/]*$""#;
const DENY_NETWORK: &str = "(deny network*)";
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum Decision {
PassThrough,
Deny {
reason: String,
},
WrapBash {
command: String,
description: String,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum Target {
Orchestrator,
Jail(PathBuf),
Reject(String),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum Backend {
Bwrap,
Seatbelt(ResolvedMac),
Deny { reason: String },
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub(crate) struct ResolvedMac {
pub wt: PathBuf,
pub tmp: PathBuf,
pub dutmp: PathBuf,
pub extra_rw: Vec<PathBuf>,
pub network: bool,
pub profile_path: PathBuf,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub(crate) struct JailPolicy {
#[serde(default)]
pub extra_rw: Vec<PathBuf>,
#[serde(default = "default_true")]
pub network: bool,
}
fn default_true() -> bool {
true
}
impl Default for JailPolicy {
fn default() -> Self {
JailPolicy {
extra_rw: vec![],
network: true,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum PolicyError {
IsRoot,
AncestorOfMainRoot,
TouchesGit,
Malformed(String),
}
impl JailPolicy {
pub(crate) fn from_toml_str(body: &str) -> Result<Self, PolicyError> {
toml::from_str(body).map_err(|e| PolicyError::Malformed(e.to_string()))
}
}
pub(crate) fn resolve_target(
agent_id: Option<&str>,
cwd: &Path,
cwd_is_project_worktree: bool,
) -> Target {
match agent_id {
None => Target::Orchestrator,
Some(_) if cwd_is_project_worktree => Target::Jail(cwd.to_path_buf()),
Some(_) => Target::Reject(format!("{REASON_NOT_WORKTREE}: {}", cwd.display())),
}
}
pub(crate) fn pathcheck(real: &Path, wt: &Path, extra_rw: &[PathBuf]) -> bool {
real.starts_with(wt) || extra_rw.iter().any(|allowed| real.starts_with(allowed))
}
pub(crate) fn validate_policy(policy: &JailPolicy, main_root: &Path) -> Result<(), PolicyError> {
for allowed in &policy.extra_rw {
if allowed == Path::new(PATH_ROOT) {
return Err(PolicyError::IsRoot);
}
if main_root.starts_with(allowed) {
return Err(PolicyError::AncestorOfMainRoot);
}
if allowed.components().any(|c| c.as_os_str() == GIT_DIR) {
return Err(PolicyError::TouchesGit);
}
}
Ok(())
}
pub(crate) fn opaque_wrap(orig_cmd: &str, argv: &[OsString]) -> String {
let b64 = base64::engine::general_purpose::STANDARD.encode(orig_cmd.as_bytes());
let payload = format!("printf %s {b64} | base64 -d | bash");
let mut parts: Vec<String> = argv
.iter()
.map(|a| shell_single_quote(a.to_string_lossy().as_ref()))
.collect();
parts.push(SHELL_BIN.to_string());
parts.push(SHELL_CMD_FLAG.to_string());
parts.push(shell_single_quote(&payload));
parts.join(" ")
}
pub(crate) fn shell_single_quote(s: &str) -> String {
let escaped = s.replace('\'', "'\\''");
format!("'{escaped}'")
}
pub(crate) trait Jailer {
fn wrap_argv(&self, wt: &Path, policy: &JailPolicy) -> Vec<OsString>;
}
pub(crate) struct Bwrap;
impl Jailer for Bwrap {
fn wrap_argv(&self, wt: &Path, policy: &JailPolicy) -> Vec<OsString> {
bwrap_argv(wt, policy)
}
}
pub(crate) struct Seatbelt {
resolved: ResolvedMac,
}
impl Jailer for Seatbelt {
fn wrap_argv(&self, _wt: &Path, _policy: &JailPolicy) -> Vec<OsString> {
sandbox_exec_argv(&self.resolved)
}
}
pub(crate) fn bwrap_core_argv(wt: &Path) -> Vec<OsString> {
let wt_os = wt.as_os_str().to_os_string();
vec![
FLAG_RO_BIND.into(),
PATH_ROOT.into(),
PATH_ROOT.into(),
FLAG_DEV.into(),
PATH_DEV.into(),
FLAG_PROC.into(),
PATH_PROC.into(),
FLAG_TMPFS.into(),
PATH_TMP.into(),
FLAG_BIND.into(),
wt_os.clone(),
wt_os.clone(),
FLAG_CHDIR.into(),
wt_os,
FLAG_DIE_WITH_PARENT.into(),
]
}
pub(crate) fn bwrap_argv(wt: &Path, policy: &JailPolicy) -> Vec<OsString> {
let mut argv: Vec<OsString> = vec![BWRAP.into()];
argv.extend(bwrap_core_argv(wt));
for allowed in &policy.extra_rw {
let allowed_os = allowed.as_os_str().to_os_string();
argv.push(FLAG_BIND.into());
argv.push(allowed_os.clone());
argv.push(allowed_os);
}
if !policy.network {
argv.push(FLAG_UNSHARE_NET.into());
}
argv.push(FLAG_ARG_SEP.into());
argv
}
pub(crate) fn seatbelt_profile(resolved: &ResolvedMac) -> String {
let mut lines: Vec<String> = vec![
SB_VERSION.to_string(),
SB_ALLOW_DEFAULT.to_string(),
SB_DENY_WRITE_FLOOR.to_string(),
format!(r#"(deny file-write* (subpath (param "{PARAM_PTMP}")))"#),
format!(r#"(deny file-write* (subpath (param "{PARAM_DUTMP}")))"#),
];
lines.extend(DEVICE_SINK_ALLOWS.iter().map(|s| (*s).to_string()));
lines.push(format!(
r#"(allow file-write* (subpath (param "{PARAM_WT}")))"#
));
lines.push(format!(
r#"(allow file-write* (subpath (param "{PARAM_TMP}")))"#
));
lines.push(format!(
r#"(allow file-write* (require-all (subpath (param "{PARAM_DUTMP}")) (regex {XCRUN_DB_REGEX})))"#
));
for (n, _) in resolved.extra_rw.iter().enumerate() {
lines.push(format!(
r#"(allow file-write* (subpath (param "{PARAM_RW_PREFIX}{n}")))"#
));
}
if !resolved.network {
lines.push(DENY_NETWORK.to_string());
}
lines.push(String::new()); lines.join("\n")
}
pub(crate) fn sandbox_exec_argv(resolved: &ResolvedMac) -> Vec<OsString> {
let mut argv: Vec<OsString> = vec![SANDBOX_EXEC.into()];
let mut bind = |name: &str, value: &Path| {
argv.push(FLAG_D.into());
let mut pair = OsString::from(name);
pair.push("=");
pair.push(value.as_os_str());
argv.push(pair);
};
bind(PARAM_WT, &resolved.wt);
bind(PARAM_TMP, &resolved.tmp);
bind(PARAM_PTMP, Path::new(PTMP_LITERAL));
bind(PARAM_DUTMP, &resolved.dutmp);
for (n, rw) in resolved.extra_rw.iter().enumerate() {
bind(&format!("{PARAM_RW_PREFIX}{n}"), rw);
}
argv.push(FLAG_F.into());
argv.push(resolved.profile_path.as_os_str().to_os_string());
argv.push(FLAG_ARG_SEP.into());
argv.push(ENV_BIN.into());
let mut tmpdir = OsString::from(ENV_TMPDIR);
tmpdir.push("=");
tmpdir.push(resolved.tmp.as_os_str());
argv.push(tmpdir);
argv
}
pub(crate) fn select_jailer(backend: &Backend) -> Option<Box<dyn Jailer>> {
match backend {
Backend::Bwrap => Some(Box::new(Bwrap)),
Backend::Seatbelt(mac) => Some(Box::new(Seatbelt {
resolved: mac.clone(),
})),
Backend::Deny { .. } => None,
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum ResolveDeny {
NotAWorktree,
IsMainCheckout,
AmbiguousGitDirs,
PolicyMissing,
PolicyMalformed(String),
}
impl ResolveDeny {
pub(crate) fn reason(&self) -> String {
match self {
ResolveDeny::NotAWorktree => REASON_MAC_NOT_WORKTREE.to_string(),
ResolveDeny::IsMainCheckout => REASON_MAC_MAIN_CHECKOUT.to_string(),
ResolveDeny::AmbiguousGitDirs => REASON_MAC_AMBIGUOUS_GITDIRS.to_string(),
ResolveDeny::PolicyMissing => REASON_MAC_POLICY_MISSING.to_string(),
ResolveDeny::PolicyMalformed(detail) => {
format!("{REASON_MAC_POLICY_MALFORMED}: {detail}")
}
}
}
}
pub(crate) trait ResolveEnv {
fn worktree_topology(&self, cwd: &Path) -> Result<Topology, ResolveDeny>;
fn getconf_dutmp(&self) -> Result<PathBuf, ResolveDeny>;
fn realpath(&self, path: &Path) -> Result<PathBuf, ResolveDeny>;
fn ensure_dir(&self, path: &Path) -> Result<PathBuf, ResolveDeny>;
fn read_policy(&self, basename: &std::ffi::OsStr) -> std::io::Result<Option<String>>;
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct Topology {
pub toplevel: PathBuf,
pub is_linked: bool,
}
pub(crate) fn resolve_inputs(
cwd: &Path,
main_root: &Path,
env: &dyn ResolveEnv,
) -> Result<ResolvedMac, ResolveDeny> {
let topo = env.worktree_topology(cwd)?;
if !topo.is_linked {
return Err(ResolveDeny::IsMainCheckout);
}
let wt = topo.toplevel;
let basename = wt
.file_name()
.ok_or(ResolveDeny::PolicyMissing)?
.to_os_string();
let Ok(Some(body)) = env.read_policy(&basename) else {
return Err(ResolveDeny::PolicyMissing);
};
let policy = JailPolicy::from_toml_str(&body)
.map_err(|e| ResolveDeny::PolicyMalformed(format!("{e:?}")))?;
validate_policy(&policy, main_root)
.map_err(|e| ResolveDeny::PolicyMalformed(format!("{e:?}")))?;
let dutmp = env.getconf_dutmp()?;
let tmp = env.ensure_dir(&wt.join(WT_TMP_SUBDIR))?;
let mut extra_rw = Vec::with_capacity(policy.extra_rw.len());
for grant in &policy.extra_rw {
extra_rw.push(env.realpath(grant)?);
}
let profile_path = tmp.join(SEATBELT_PROFILE_FILE);
Ok(ResolvedMac {
wt,
tmp,
dutmp,
extra_rw,
network: policy.network,
profile_path,
})
}
pub(crate) fn seatbelt_backend(resolved: Result<ResolvedMac, ResolveDeny>) -> Backend {
match resolved {
Ok(mac) => Backend::Seatbelt(mac),
Err(deny) => Backend::Deny {
reason: deny.reason(),
},
}
}
const POLICY_DIR_SEGMENTS: &[&str] = &[".doctrine", "state", "dispatch", "jail"];
const POLICY_FILE_EXT: &str = "toml";
pub(crate) struct RealEnv {
pub main_root: PathBuf,
}
impl ResolveEnv for RealEnv {
fn worktree_topology(&self, cwd: &Path) -> Result<Topology, ResolveDeny> {
let toplevel_raw = crate::git::git_text(cwd, &["rev-parse", "--show-toplevel"])
.map_err(|_e| ResolveDeny::NotAWorktree)?;
let toplevel =
std::fs::canonicalize(&toplevel_raw).map_err(|_e| ResolveDeny::NotAWorktree)?;
let is_linked = crate::worktree::is_linked_worktree(&toplevel)
.map_err(|_e| ResolveDeny::AmbiguousGitDirs)?;
Ok(Topology {
toplevel,
is_linked,
})
}
fn getconf_dutmp(&self) -> Result<PathBuf, ResolveDeny> {
let out = std::process::Command::new("getconf")
.arg("DARWIN_USER_TEMP_DIR")
.output()
.map_err(|_e| ResolveDeny::NotAWorktree)?;
if !out.status.success() {
return Err(ResolveDeny::NotAWorktree);
}
let raw = String::from_utf8(out.stdout)
.map_err(|_e| ResolveDeny::NotAWorktree)?
.trim()
.to_string();
std::fs::canonicalize(&raw).map_err(|_e| ResolveDeny::NotAWorktree)
}
fn realpath(&self, path: &Path) -> Result<PathBuf, ResolveDeny> {
std::fs::canonicalize(path).map_err(|_e| ResolveDeny::PolicyMissing)
}
fn ensure_dir(&self, path: &Path) -> Result<PathBuf, ResolveDeny> {
std::fs::create_dir_all(path).map_err(|_e| ResolveDeny::PolicyMissing)?;
std::fs::canonicalize(path).map_err(|_e| ResolveDeny::PolicyMissing)
}
fn read_policy(&self, basename: &std::ffi::OsStr) -> std::io::Result<Option<String>> {
let mut path = self.main_root.clone();
for seg in POLICY_DIR_SEGMENTS {
path.push(seg);
}
path.push(basename);
path.set_extension(POLICY_FILE_EXT);
match std::fs::read_to_string(&path) {
Ok(body) => Ok(Some(body)),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(e) => Err(e),
}
}
}
pub(crate) fn decide_bash(
target: &Target,
cmd: &str,
desc: &str,
policy: &JailPolicy,
backend: &Backend,
) -> Decision {
match target {
Target::Orchestrator => Decision::PassThrough,
Target::Reject(reason) => Decision::Deny {
reason: reason.clone(),
},
Target::Jail(wt) => match select_jailer(backend) {
Some(jailer) => Decision::WrapBash {
command: opaque_wrap(cmd, &jailer.wrap_argv(wt, policy)),
description: desc.to_string(),
},
None => Decision::Deny {
reason: match backend {
Backend::Deny { reason } => reason.clone(),
_ => REASON_NO_BACKEND.to_string(),
},
},
},
}
}
pub(crate) fn decide_write(target: &Target, real: Option<&Path>, policy: &JailPolicy) -> Decision {
match target {
Target::Orchestrator => Decision::PassThrough,
Target::Reject(reason) => Decision::Deny {
reason: reason.clone(),
},
Target::Jail(wt) => match real {
None => Decision::Deny {
reason: REASON_NO_FILE_PATH.to_string(),
},
Some(real) if pathcheck(real, wt, &policy.extra_rw) => Decision::PassThrough,
Some(real) => Decision::Deny {
reason: format!("{REASON_ESCAPES_WORKTREE}: {}", real.display()),
},
},
}
}
#[cfg(test)]
mod tests {
use super::*;
fn pb(s: &str) -> PathBuf {
PathBuf::from(s)
}
#[test]
fn resolve_target_no_agent_is_orchestrator() {
assert_eq!(
resolve_target(None, &pb("/anywhere"), false),
Target::Orchestrator
);
assert_eq!(
resolve_target(None, &pb("/anywhere"), true),
Target::Orchestrator
);
}
#[test]
fn resolve_target_agent_in_project_worktree_is_jail() {
let wt = pb("/root/.worktrees/agent-1");
assert_eq!(
resolve_target(Some("agent-1"), &wt, true),
Target::Jail(wt.clone())
);
}
#[test]
fn resolve_target_agent_in_sibling_repo_worktree_is_reject() {
let sibling = pb("/other-repo/.worktrees/agent-9");
match resolve_target(Some("agent-9"), &sibling, false) {
Target::Reject(reason) => assert!(reason.contains(REASON_NOT_WORKTREE)),
other => panic!("expected Reject, got {other:?}"),
}
}
#[test]
fn pathcheck_inside_worktree_passes() {
assert!(pathcheck(&pb("/wt/src/main.rs"), &pb("/wt"), &[]));
assert!(pathcheck(&pb("/wt"), &pb("/wt"), &[])); }
#[test]
fn pathcheck_escape_denies() {
assert!(!pathcheck(&pb("/etc/passwd"), &pb("/wt"), &[]));
assert!(!pathcheck(&pb("/home/u/.ssh/id_rsa"), &pb("/wt"), &[]));
}
#[test]
fn pathcheck_extra_rw_hit_passes() {
let extra = vec![pb("/opt/cache")];
assert!(pathcheck(&pb("/opt/cache/blob"), &pb("/wt"), &extra));
assert!(!pathcheck(&pb("/opt/other/blob"), &pb("/wt"), &extra));
}
#[test]
fn pathcheck_sibling_prefix_denies() {
assert!(!pathcheck(&pb("/wt-evil/x"), &pb("/wt"), &[]));
assert!(!pathcheck(&pb("/wt-evil"), &pb("/wt"), &[]));
}
#[test]
fn pathcheck_dotgit_under_worktree_passes() {
assert!(pathcheck(&pb("/wt/.git/config"), &pb("/wt"), &[]));
}
#[test]
fn jail_policy_default_is_permissive_floor() {
let d = JailPolicy::default();
assert!(d.extra_rw.is_empty());
assert!(d.network); }
#[test]
fn from_toml_str_empty_is_default_floor() {
let p = JailPolicy::from_toml_str("").expect("empty parses to default");
assert_eq!(p, JailPolicy::default());
}
#[test]
fn from_toml_str_present_values() {
let p = JailPolicy::from_toml_str("network = false\nextra_rw = [\"/opt/x\"]")
.expect("present body parses");
assert!(!p.network);
assert_eq!(p.extra_rw, vec![pb("/opt/x")]);
}
#[test]
fn from_toml_str_malformed_is_err() {
assert!(matches!(
JailPolicy::from_toml_str("network = 12"),
Err(PolicyError::Malformed(_))
));
assert!(matches!(
JailPolicy::from_toml_str("= = ="),
Err(PolicyError::Malformed(_))
));
}
#[test]
fn from_toml_str_unknown_field_is_err() {
assert!(matches!(
JailPolicy::from_toml_str("extra_rws = []"),
Err(PolicyError::Malformed(_))
));
}
#[test]
fn to_toml_string_round_trips_through_from_toml_str() {
let p = JailPolicy {
extra_rw: vec![pb("/nix/store"), pb("/cache")],
network: false,
};
let text = toml::to_string(&p).expect("serialize policy to TOML");
assert_eq!(JailPolicy::from_toml_str(&text).expect("re-parse"), p);
}
fn pi_spawn_core_tokens() -> Vec<String> {
let path = crate::test_support::repo_root().join("scripts/pi-spawn-confined.sh");
let raw = std::fs::read_to_string(&path).expect("read pi-spawn-confined.sh");
let code = raw
.lines()
.filter(|l| !l.trim_start().starts_with('#'))
.collect::<Vec<_>>()
.join("\n")
.replace("\\\n", " ");
let toks: Vec<String> = code.split_whitespace().map(str::to_string).collect();
let start = toks
.iter()
.position(|t| t == "bwrap")
.expect("bwrap invocation token");
let between: Vec<String> = toks[start + 1..]
.iter()
.take_while(|t| t.as_str() != "pi")
.map(|t| t.trim_matches('"').to_string())
.collect();
let mut out = Vec::new();
let mut i = 0;
while i < between.len() {
let t = &between[i];
if t == FLAG_BIND && i + 2 < between.len() && between[i + 1].contains(".pi") {
i += 3;
continue;
}
if t == "--setenv" && i + 2 < between.len() {
i += 3;
continue;
}
out.push(t.clone());
i += 1;
}
out
}
fn to_strings(argv: &[OsString]) -> Vec<String> {
argv.iter()
.map(|a| a.to_string_lossy().into_owned())
.collect()
}
#[test]
fn bwrap_core_argv_matches_pi_spawn_core_flags() {
let ours = to_strings(&bwrap_core_argv(Path::new("$D")));
assert_eq!(ours, pi_spawn_core_tokens());
}
#[test]
fn bwrap_argv_wraps_core_with_extra_rw_and_unshare_net() {
let policy = JailPolicy {
extra_rw: vec![pb("/opt/cache")],
network: false,
};
let argv = to_strings(&bwrap_argv(Path::new("/wt"), &policy));
assert_eq!(argv.first().map(String::as_str), Some(BWRAP));
assert_eq!(argv.last().map(String::as_str), Some(FLAG_ARG_SEP));
assert!(argv.iter().any(|t| t == FLAG_RO_BIND));
assert!(argv.iter().any(|t| t == FLAG_CHDIR));
let bind_targets: Vec<&String> = argv
.iter()
.zip(argv.iter().skip(1))
.filter(|(f, _)| *f == FLAG_BIND)
.map(|(_, t)| t)
.collect();
assert!(bind_targets.iter().any(|t| t.as_str() == "/opt/cache"));
assert!(argv.iter().any(|t| t == FLAG_UNSHARE_NET));
}
#[test]
fn bwrap_argv_default_network_has_no_unshare_net() {
let argv = to_strings(&bwrap_argv(Path::new("/wt"), &JailPolicy::default()));
assert!(!argv.iter().any(|t| t == FLAG_UNSHARE_NET));
}
#[test]
fn opaque_wrap_roundtrips_and_executes_space_and_quote_path() {
let tricky = "/x/a b'c/wt";
let argv = vec![OsString::from("env"), OsString::from(format!("P={tricky}"))];
let orig = r#"printf %s "$P""#;
let wrapped = opaque_wrap(orig, &argv);
let out = std::process::Command::new("sh")
.arg("-c")
.arg(&wrapped)
.output()
.expect("run assembled shell string");
assert!(
out.status.success(),
"wrapped command failed: {}",
String::from_utf8_lossy(&out.stderr)
);
assert_eq!(String::from_utf8_lossy(&out.stdout), tricky);
}
#[test]
fn opaque_wrap_appends_base64_bash_payload() {
let wrapped = opaque_wrap("rm -rf /", &[OsString::from(BWRAP)]);
assert!(!wrapped.contains("rm -rf /"));
assert!(wrapped.contains("base64 -d | bash"));
let b64 = base64::engine::general_purpose::STANDARD.encode(b"rm -rf /");
assert!(wrapped.contains(&b64));
}
const MAIN_ROOT: &str = "/home/u/project";
#[test]
fn validate_policy_rejects_root() {
let policy = JailPolicy {
extra_rw: vec![pb("/")],
network: true,
};
assert_eq!(
validate_policy(&policy, Path::new(MAIN_ROOT)),
Err(PolicyError::IsRoot)
);
}
#[test]
fn validate_policy_rejects_ancestor_of_main_root() {
let policy = JailPolicy {
extra_rw: vec![pb("/home/u")], network: true,
};
assert_eq!(
validate_policy(&policy, Path::new(MAIN_ROOT)),
Err(PolicyError::AncestorOfMainRoot)
);
}
#[test]
fn validate_policy_rejects_dotgit() {
let policy = JailPolicy {
extra_rw: vec![pb("/home/u/project/.git")],
network: true,
};
assert_eq!(
validate_policy(&policy, Path::new(MAIN_ROOT)),
Err(PolicyError::TouchesGit)
);
}
#[test]
fn validate_policy_accepts_benign_extra_rw() {
let policy = JailPolicy {
extra_rw: vec![pb("/opt/cache"), pb("/tmp/scratch")],
network: true,
};
assert_eq!(validate_policy(&policy, Path::new(MAIN_ROOT)), Ok(()));
}
#[test]
fn validate_policy_empty_extra_rw_is_ok() {
assert_eq!(
validate_policy(&JailPolicy::default(), Path::new(MAIN_ROOT)),
Ok(())
);
}
#[test]
fn select_jailer_is_a_pure_map_over_backend() {
assert!(select_jailer(&Backend::Bwrap).is_some());
assert!(select_jailer(&Backend::Seatbelt(ResolvedMac::default())).is_some());
assert!(
select_jailer(&Backend::Deny {
reason: "bwrap-unavailable".to_string()
})
.is_none()
);
}
#[test]
fn decide_bash_orchestrator_passes_through() {
assert_eq!(
decide_bash(
&Target::Orchestrator,
"ls",
"list",
&JailPolicy::default(),
&Backend::Bwrap
),
Decision::PassThrough
);
}
#[test]
fn decide_bash_reject_denies_with_reason() {
let target = Target::Reject("cwd-not-a-worktree: /x".to_string());
assert_eq!(
decide_bash(
&target,
"ls",
"list",
&JailPolicy::default(),
&Backend::Bwrap
),
Decision::Deny {
reason: "cwd-not-a-worktree: /x".to_string()
}
);
}
#[test]
fn decide_bash_jail_with_bwrap_wraps() {
let target = Target::Jail(pb("/wt"));
match decide_bash(
&target,
"echo hi",
"greet",
&JailPolicy::default(),
&Backend::Bwrap,
) {
Decision::WrapBash {
command,
description,
} => {
assert_eq!(description, "greet");
assert!(command.starts_with(&format!("'{BWRAP}'")));
assert!(!command.contains("echo hi")); }
other => panic!("expected WrapBash, got {other:?}"),
}
}
#[test]
fn decide_bash_jail_with_deny_backend_denies_never_passes_through() {
let target = Target::Jail(pb("/wt"));
let backend = Backend::Deny {
reason: "bwrap-unavailable".to_string(),
};
assert_eq!(
decide_bash(
&target,
"echo hi",
"greet",
&JailPolicy::default(),
&backend
),
Decision::Deny {
reason: "bwrap-unavailable".to_string()
}
);
}
#[test]
fn seatbelt_resolve_deny_degrades_to_bash_deny_never_wraps_or_passes() {
let target = Target::Jail(pb("/wt"));
let branches = [
ResolveDeny::NotAWorktree,
ResolveDeny::IsMainCheckout,
ResolveDeny::AmbiguousGitDirs,
ResolveDeny::PolicyMissing,
ResolveDeny::PolicyMalformed("unknown key `frobnicate`".to_string()),
];
for branch in branches {
let expected_reason = branch.reason();
let backend = seatbelt_backend(Err(branch.clone()));
assert_eq!(
backend,
Backend::Deny {
reason: expected_reason.clone()
},
"seatbelt_backend must map Err({branch:?}) to Backend::Deny"
);
match decide_bash(
&target,
"echo hi",
"greet",
&JailPolicy::default(),
&backend,
) {
Decision::Deny { reason } => assert_eq!(
reason, expected_reason,
"degrade must carry the macOS reason for {branch:?}"
),
other => panic!(
"expected Decision::Deny for {branch:?}, got {other:?} \
(fail-open regression: never WrapBash/PassThrough on a degraded arm)"
),
}
}
}
#[test]
fn decide_write_inside_worktree_passes_escape_denies() {
let target = Target::Jail(pb("/wt"));
let policy = JailPolicy::default();
assert_eq!(
decide_write(&target, Some(&pb("/wt/src/x.rs")), &policy),
Decision::PassThrough
);
match decide_write(&target, Some(&pb("/etc/passwd")), &policy) {
Decision::Deny { reason } => assert!(reason.contains(REASON_ESCAPES_WORKTREE)),
other => panic!("expected Deny, got {other:?}"),
}
}
#[test]
fn decide_write_no_path_denies() {
let target = Target::Jail(pb("/wt"));
match decide_write(&target, None, &JailPolicy::default()) {
Decision::Deny { reason } => assert!(reason.contains(REASON_NO_FILE_PATH)),
other => panic!("expected Deny, got {other:?}"),
}
}
#[test]
fn decide_write_honours_extra_rw() {
let target = Target::Jail(pb("/wt"));
let policy = JailPolicy {
extra_rw: vec![pb("/opt/cache")],
network: true,
};
assert_eq!(
decide_write(&target, Some(&pb("/opt/cache/blob")), &policy),
Decision::PassThrough
);
}
fn resolved_mac() -> ResolvedMac {
ResolvedMac {
wt: pb("/private/tmp/wt-abc"),
tmp: pb("/private/tmp/wt-abc/.tmp"),
dutmp: pb("/private/var/folders/xy/T"),
extra_rw: vec![],
network: true,
profile_path: pb("/private/tmp/wt-abc/.tmp/jail.sb"),
}
}
fn at(hay: &str, needle: &str) -> usize {
hay.find(needle)
.unwrap_or_else(|| panic!("profile missing token: {needle:?}\n---\n{hay}"))
}
#[test]
fn seatbelt_profile_orders_deny_coarse_first_allow_specific_last() {
let p = seatbelt_profile(&resolved_mac());
let floor = at(&p, SB_DENY_WRITE_FLOOR);
let ptmp = at(&p, r#"(deny file-write* (subpath (param "PTMP")))"#);
let dutmp = at(&p, r#"(deny file-write* (subpath (param "DUTMP")))"#);
let dev = at(&p, DEVICE_SINK_ALLOWS[0]);
let wt = at(&p, r#"(subpath (param "WT"))"#);
let tmp = at(&p, r#"(subpath (param "TMP"))"#);
let xcrun = at(&p, XCRUN_DB_REGEX);
assert!(
floor < ptmp && ptmp < dutmp && dutmp < dev && dev < wt && wt < tmp && tmp < xcrun,
"rule ordering violated (F-A): floor={floor} ptmp={ptmp} dutmp={dutmp} dev={dev} wt={wt} tmp={tmp} xcrun={xcrun}"
);
assert!(at(&p, SB_ALLOW_DEFAULT) < floor);
assert!(p.starts_with(SB_VERSION));
}
#[test]
fn seatbelt_profile_scopes_xcrun_db_reallow_with_require_all() {
let p = seatbelt_profile(&resolved_mac());
assert!(
p.contains(&format!(
r#"(require-all (subpath (param "{PARAM_DUTMP}")) (regex {XCRUN_DB_REGEX}))"#
)),
"xcrun_db re-allow not require-all-scoped to DUTMP:\n{p}"
);
}
#[test]
fn seatbelt_profile_emits_device_sinks() {
let p = seatbelt_profile(&resolved_mac());
for sink in DEVICE_SINK_ALLOWS {
assert!(p.contains(sink), "device sink missing: {sink}\n{p}");
}
}
#[test]
fn seatbelt_profile_network_line_is_conditional() {
let mut open = resolved_mac();
open.network = true;
assert!(
!seatbelt_profile(&open).contains(DENY_NETWORK),
"network=true must NOT emit the network deny (default open)"
);
let mut closed = resolved_mac();
closed.network = false;
assert!(
seatbelt_profile(&closed).contains(DENY_NETWORK),
"network=false MUST emit {DENY_NETWORK}"
);
}
#[test]
fn seatbelt_profile_emits_one_rw_allow_per_extra_rw() {
let mut none = resolved_mac();
none.extra_rw = vec![];
let p0 = seatbelt_profile(&none);
assert!(!p0.contains(&format!(r#"(param "{PARAM_RW_PREFIX}0")"#)));
let mut two = resolved_mac();
two.extra_rw = vec![pb("/opt/a"), pb("/opt/b")];
let p2 = seatbelt_profile(&two);
assert!(p2.contains(&format!(
r#"(allow file-write* (subpath (param "{PARAM_RW_PREFIX}0")))"#
)));
assert!(p2.contains(&format!(
r#"(allow file-write* (subpath (param "{PARAM_RW_PREFIX}1")))"#
)));
assert!(!p2.contains(&format!(r#"(param "{PARAM_RW_PREFIX}2")"#)));
}
fn argv_str(argv: &[OsString]) -> String {
argv.iter()
.map(|a| a.to_string_lossy().into_owned())
.collect::<Vec<_>>()
.join(" ")
}
#[test]
fn sandbox_exec_argv_binds_realpathd_d_params() {
let r = resolved_mac();
let argv = sandbox_exec_argv(&r);
let s = argv_str(&argv);
assert!(s.contains(&format!("{PARAM_WT}={}", r.wt.display())));
assert!(s.contains(&format!("{PARAM_TMP}={}", r.tmp.display())));
assert!(s.contains(&format!("{PARAM_PTMP}={PTMP_LITERAL}")));
assert!(s.contains(&format!("{PARAM_DUTMP}={}", r.dutmp.display())));
assert!(argv.iter().any(|a| a == FLAG_D));
}
#[test]
fn sandbox_exec_argv_binds_one_rw_param_per_extra_rw() {
let mut r = resolved_mac();
r.extra_rw = vec![pb("/opt/a"), pb("/opt/b")];
let s = argv_str(&sandbox_exec_argv(&r));
assert!(s.contains(&format!("{PARAM_RW_PREFIX}0=/opt/a")));
assert!(s.contains(&format!("{PARAM_RW_PREFIX}1=/opt/b")));
}
#[test]
fn sandbox_exec_argv_references_profile_and_terminates() {
let r = resolved_mac();
let argv = sandbox_exec_argv(&r);
assert_eq!(
argv.first().map(|a| a.as_os_str()),
Some(OsString::from(SANDBOX_EXEC).as_os_str())
);
let s = argv_str(&argv);
assert!(s.contains(FLAG_F));
assert!(argv.iter().any(|a| a == r.profile_path.as_os_str()));
assert!(argv.iter().any(|a| a == ENV_BIN));
assert!(s.contains(&format!("{ENV_TMPDIR}={}", r.tmp.display())));
let sep = argv
.iter()
.position(|a| a == FLAG_ARG_SEP)
.expect("no -- separator");
let envpos = argv
.iter()
.position(|a| a == ENV_BIN)
.expect("no env token");
assert!(sep < envpos, "env/body tail must follow --");
}
#[test]
fn seatbelt_wrap_argv_delegates_to_sandbox_exec_argv() {
let r = resolved_mac();
let jailer = Seatbelt {
resolved: r.clone(),
};
assert_eq!(
jailer.wrap_argv(&r.wt, &JailPolicy::default()),
sandbox_exec_argv(&r)
);
}
#[test]
fn sandbox_exec_argv_never_splices_paths_into_profile_body() {
let r = resolved_mac();
let argv = sandbox_exec_argv(&r);
let s = argv_str(&argv);
assert!(
!s.contains("file-write*"),
"argv must not carry profile-body tokens"
);
assert!(
!s.contains("subpath"),
"argv must not carry profile-body tokens"
);
}
struct FakeEnv {
topology: Result<Topology, ResolveDeny>,
dutmp: Result<PathBuf, ResolveDeny>,
policy: std::io::Result<Option<String>>,
realpath_ok: bool,
ensure_ok: bool,
}
impl Default for FakeEnv {
fn default() -> Self {
FakeEnv {
topology: Ok(Topology {
toplevel: pb("/private/tmp/wt-abc"),
is_linked: true,
}),
dutmp: Ok(pb("/private/var/folders/xy/T")),
policy: Ok(Some(String::new())), realpath_ok: true,
ensure_ok: true,
}
}
}
impl ResolveEnv for FakeEnv {
fn worktree_topology(&self, _cwd: &Path) -> Result<Topology, ResolveDeny> {
self.topology.clone()
}
fn getconf_dutmp(&self) -> Result<PathBuf, ResolveDeny> {
self.dutmp.clone()
}
fn realpath(&self, path: &Path) -> Result<PathBuf, ResolveDeny> {
if self.realpath_ok {
Ok(path.to_path_buf())
} else {
Err(ResolveDeny::PolicyMissing)
}
}
fn ensure_dir(&self, path: &Path) -> Result<PathBuf, ResolveDeny> {
if self.ensure_ok {
Ok(path.to_path_buf())
} else {
Err(ResolveDeny::PolicyMissing)
}
}
fn read_policy(&self, _basename: &std::ffi::OsStr) -> std::io::Result<Option<String>> {
match &self.policy {
Ok(o) => Ok(o.clone()),
Err(e) => Err(std::io::Error::new(e.kind(), e.to_string())),
}
}
}
const MAC_MAIN: &str = "/home/u/project";
fn resolve(env: &FakeEnv) -> Result<ResolvedMac, ResolveDeny> {
resolve_inputs(&pb("/private/tmp/wt-abc"), Path::new(MAC_MAIN), env)
}
#[test]
fn resolve_inputs_branch_a_not_a_worktree_denies() {
let env = FakeEnv {
topology: Err(ResolveDeny::NotAWorktree),
..Default::default()
};
assert_eq!(resolve(&env), Err(ResolveDeny::NotAWorktree));
assert_eq!(resolve(&env).unwrap_err().reason(), REASON_MAC_NOT_WORKTREE);
}
#[test]
fn resolve_inputs_branch_b_main_checkout_denies() {
let env = FakeEnv {
topology: Ok(Topology {
toplevel: pb("/private/tmp/wt-abc"),
is_linked: false,
}),
..Default::default()
};
assert_eq!(resolve(&env), Err(ResolveDeny::IsMainCheckout));
}
#[test]
fn resolve_inputs_branch_d_ambiguous_gitdirs_denies() {
let env = FakeEnv {
topology: Err(ResolveDeny::AmbiguousGitDirs),
..Default::default()
};
assert_eq!(resolve(&env), Err(ResolveDeny::AmbiguousGitDirs));
}
#[test]
fn resolve_inputs_branch_e_policy_absent_denies() {
let env = FakeEnv {
policy: Ok(None),
..Default::default()
};
assert_eq!(resolve(&env), Err(ResolveDeny::PolicyMissing));
}
#[test]
fn resolve_inputs_policy_unreadable_denies() {
let env = FakeEnv {
policy: Err(std::io::Error::new(
std::io::ErrorKind::PermissionDenied,
"boom",
)),
..Default::default()
};
assert_eq!(resolve(&env), Err(ResolveDeny::PolicyMissing));
}
#[test]
fn resolve_inputs_branch_f_malformed_policy_denies() {
let env = FakeEnv {
policy: Ok(Some("network = \"maybe\"\n".to_string())), ..Default::default()
};
match resolve(&env) {
Err(ResolveDeny::PolicyMalformed(_)) => {}
other => panic!("expected PolicyMalformed, got {other:?}"),
}
let env2 = FakeEnv {
policy: Ok(Some("network_egress = true\n".to_string())),
..Default::default()
};
match resolve(&env2) {
Err(ResolveDeny::PolicyMalformed(_)) => {}
other => panic!("expected PolicyMalformed for unknown key, got {other:?}"),
}
}
#[test]
fn resolve_inputs_dangerous_extra_rw_denies() {
let env = FakeEnv {
policy: Ok(Some("extra_rw = [\"/\"]\n".to_string())),
..Default::default()
};
match resolve(&env) {
Err(ResolveDeny::PolicyMalformed(_)) => {}
other => panic!("expected Deny for root extra_rw, got {other:?}"),
}
}
#[test]
fn resolve_inputs_happy_path_builds_resolved_mac() {
let env = FakeEnv::default();
let mac = resolve(&env).expect("happy path resolves");
assert_eq!(mac.wt, pb("/private/tmp/wt-abc"));
assert_eq!(mac.tmp, pb("/private/tmp/wt-abc/.tmp"));
assert_eq!(mac.dutmp, pb("/private/var/folders/xy/T"));
assert!(mac.extra_rw.is_empty());
assert!(mac.network, "a policy omitting network defaults OPEN");
assert_eq!(mac.profile_path, pb("/private/tmp/wt-abc/.tmp/jail.sb"));
}
#[test]
fn resolve_inputs_happy_path_carries_validated_extra_rw() {
let env = FakeEnv {
policy: Ok(Some("extra_rw = [\"/opt/cache\"]\n".to_string())),
..Default::default()
};
let mac = resolve(&env).expect("valid extra_rw resolves");
assert_eq!(mac.extra_rw, vec![pb("/opt/cache")]);
}
#[test]
fn seatbelt_backend_ok_routes_to_seatbelt_jailer() {
let mac = resolve(&FakeEnv::default()).unwrap();
let backend = seatbelt_backend(Ok(mac.clone()));
assert_eq!(backend, Backend::Seatbelt(mac.clone()));
let jailer = select_jailer(&backend).expect("Seatbelt ⇒ Some");
assert_eq!(
jailer.wrap_argv(&mac.wt, &JailPolicy::default()),
sandbox_exec_argv(&mac)
);
}
#[test]
fn seatbelt_backend_err_denies_never_passes_through() {
let backend = seatbelt_backend(Err(ResolveDeny::PolicyMalformed("bad".into())));
assert!(matches!(backend, Backend::Deny { .. }));
assert!(select_jailer(&backend).is_none());
let decision = decide_bash(
&Target::Jail(pb("/private/tmp/wt-abc")),
"rm -rf /",
"danger",
&JailPolicy::default(),
&backend,
);
match decision {
Decision::Deny { reason } => {
assert!(reason.starts_with(REASON_MAC_POLICY_MALFORMED));
}
other => panic!("malformed policy MUST deny, got {other:?}"),
}
}
#[test]
fn network_bool_flows_from_policy_to_profile() {
let closed = FakeEnv {
policy: Ok(Some("network = false\n".to_string())),
..Default::default()
};
let mac = resolve(&closed).expect("valid closed-network policy");
assert!(!mac.network);
assert!(
seatbelt_profile(&mac).contains(DENY_NETWORK),
"network=false MUST emit the deny line"
);
let open = resolve(&FakeEnv::default()).unwrap();
assert!(open.network);
assert!(
!seatbelt_profile(&open).contains(DENY_NETWORK),
"default-open policy MUST NOT emit the deny line"
);
}
use crate::worktree::test_helpers::{git, init_repo};
#[test]
fn real_env_topology_main_checkout_is_branch_b() {
let tmp = tempfile::tempdir().unwrap();
let primary = init_repo(&tmp.path().join("src"));
let env = RealEnv {
main_root: primary.clone(),
};
let topo = env
.worktree_topology(&primary)
.expect("primary is a worktree");
assert!(!topo.is_linked, "primary tree is the main checkout");
assert_eq!(
resolve_inputs(&primary, &primary, &env),
Err(ResolveDeny::IsMainCheckout)
);
}
#[test]
fn real_env_topology_non_git_is_branch_a() {
let tmp = tempfile::tempdir().unwrap();
let plain = tmp.path().join("not-a-repo");
std::fs::create_dir_all(&plain).unwrap();
let env = RealEnv {
main_root: plain.clone(),
};
assert_eq!(
env.worktree_topology(&plain),
Err(ResolveDeny::NotAWorktree)
);
}
#[test]
fn real_env_linked_worktree_then_policy_absent_is_branch_e() {
let tmp = tempfile::tempdir().unwrap();
let primary = init_repo(&tmp.path().join("src"));
let fork = tmp.path().join("wt-xyz");
git(
&primary,
&[
"worktree",
"add",
"-q",
"-b",
"feat",
fork.to_str().unwrap(),
],
);
let fork = std::fs::canonicalize(&fork).unwrap();
let env = RealEnv {
main_root: primary.clone(),
};
let topo = env.worktree_topology(&fork).expect("fork is a worktree");
assert!(topo.is_linked, "a linked worktree");
assert_eq!(
resolve_inputs(&fork, &primary, &env),
Err(ResolveDeny::PolicyMissing)
);
}
#[test]
fn real_env_read_policy_present_absent_malformed() {
let tmp = tempfile::tempdir().unwrap();
let main = init_repo(&tmp.path().join("src"));
let mut jail_dir = main.clone();
for seg in POLICY_DIR_SEGMENTS {
jail_dir.push(seg);
}
std::fs::create_dir_all(&jail_dir).unwrap();
std::fs::write(jail_dir.join("wt-good.toml"), "network = false\n").unwrap();
std::fs::write(jail_dir.join("wt-bad.toml"), "network = \"x\"\n").unwrap();
let env = RealEnv {
main_root: main.clone(),
};
assert_eq!(
env.read_policy(std::ffi::OsStr::new("wt-good")).unwrap(),
Some("network = false\n".to_string())
);
assert_eq!(
env.read_policy(std::ffi::OsStr::new("wt-missing")).unwrap(),
None
);
let bad = env
.read_policy(std::ffi::OsStr::new("wt-bad"))
.unwrap()
.unwrap();
assert!(JailPolicy::from_toml_str(&bad).is_err());
}
}