diff --git a/crates/mind-body/src/self_run.rs b/crates/mind-body/src/self_run.rs index 70767f9..a69c4f9 100644 --- a/crates/mind-body/src/self_run.rs +++ b/crates/mind-body/src/self_run.rs @@ -90,6 +90,9 @@ pub const SEED_VARIABLE: &str = "MIND_SELF_CONFIG_SEED"; const TOKEN_FD_VARIABLE: &str = "CLAUDE_CODE_OAUTH_TOKEN_FILE_DESCRIPTOR"; /// Tells `claude` to scrub credentials from the environment of what it starts. const ENV_SCRUB_VARIABLE: &str = "CLAUDE_CODE_SUBPROCESS_ENV_SCRUB"; +/// Optional, set by the unit: `off` turns that scrub off for a run whose user holds root, where it +/// protects nothing and its sandbox's no_new_privs would only stop `sudo`. Anything else keeps it on. +const SCRUB_VARIABLE: &str = "MIND_SELF_SCRUB"; /// The name the start message tells her to admit as. pub const AGENT: &str = "mind-self-run"; /// The namespace of the session id: the run id is the name in it. @@ -761,6 +764,11 @@ fn child_environment(unit: &[(OsString, OsString)]) -> Vec<(&OsString, &OsString unit.iter().filter(|(name, _)| child_may_see(name)).map(|(name, value)| (name, value)).collect() } +/// Whether the unit turned the child's subprocess scrub off: only `MIND_SELF_SCRUB=off` does. +fn scrub_is_off(unit: &[(OsString, OsString)]) -> bool { + unit.iter().any(|(name, value)| name == SCRUB_VARIABLE && value == "off") +} + /// Starts `program` with a duplicate of the token descriptor open in it, and /// closes that duplicate here the moment the child exists. The lease's own /// descriptor is close-on-exec, so this duplicate is the child's only way to the @@ -773,7 +781,7 @@ fn spawn_program(program: &Path, args: &[String], token: &TokenFd, unit: &[(OsSt command.args(args).stdin(Stdio::piped()).stdout(Stdio::piped()).stderr(Stdio::inherit()); command.env_clear().envs(child_environment(unit)); command.env("HOME", &config.0).env(CONFIG_DIR_VARIABLE, &config.0); - command.env(TOKEN_FD_VARIABLE, inheritable.number().to_string()).env(ENV_SCRUB_VARIABLE, "1"); + command.env(TOKEN_FD_VARIABLE, inheritable.number().to_string()).env(ENV_SCRUB_VARIABLE, if scrub_is_off(unit) { "0" } else { "1" }); // Its own process group, so that a signal reaches what the child started too. #[cfg(unix)] std::os::unix::process::CommandExt::process_group(&mut command, 0); diff --git a/crates/mind-body/src/self_run/tests/process/lease_child.rs b/crates/mind-body/src/self_run/tests/process/lease_child.rs index a64c422..eb9aa72 100644 --- a/crates/mind-body/src/self_run/tests/process/lease_child.rs +++ b/crates/mind-body/src/self_run/tests/process/lease_child.rs @@ -156,3 +156,22 @@ fn the_parent_closes_the_duplicate_it_lent_the_child() { child.stop(Stop::Kill); drain(&mut *child); } + +#[test] +fn the_child_scrubs_its_subprocesses_unless_the_unit_says_off() { + let dir = tempfile::tempdir().unwrap(); + let program = script(&dir, "echo \"scrub=$CLAUDE_CODE_SUBPROCESS_ENV_SCRUB\""); + let scrub_with = |value: Option<&str>| { + let mut unit = unit_environment(); + unit.extend(value.map(|value| (SCRUB_VARIABLE.into(), value.into()))); + let config = FreshConfig::create(dir.path(), None).unwrap(); + let mut child = spawn_program(&program, &[], &token_holding("tok-canary-7f3a"), &unit, config).unwrap(); + child.close_input(); + let (lines, _) = drain(&mut *child); + field(&lines, "scrub") + }; + assert_eq!(scrub_with(None), "1", "unset keeps the scrub on"); + assert_eq!(scrub_with(Some("on")), "1", "any other value keeps it on"); + assert_eq!(scrub_with(Some("OFF")), "1", "only the exact word turns it off"); + assert_eq!(scrub_with(Some("off")), "0", "a unit for a user that holds root turns it off"); +}