const SECRET_MARKERS: &[&str] = &[
"KEY",
"TOKEN",
"SECRET",
"PASSWORD",
"PASSWD",
"CREDENTIAL",
"AUTH",
"SESSION",
"COOKIE",
"PRIVATE",
"SIGNATURE",
];
const DENIED_PREFIXES: &[&str] = &["APOLLO_"];
const MARKER_EXCEPTIONS: &[&str] = &[
"SSH_AUTH_SOCK",
"GPG_AGENT_INFO",
"XDG_SESSION_TYPE",
"XDG_SESSION_CLASS",
"XDG_SESSION_ID",
"XDG_SESSION_DESKTOP",
"KEYBOARD_LAYOUT",
];
pub fn is_secret_env_name(name: &str) -> bool {
let upper = name.to_ascii_uppercase();
if DENIED_PREFIXES.iter().any(|p| upper.starts_with(p)) {
return true;
}
if MARKER_EXCEPTIONS.contains(&upper.as_str()) {
return false;
}
SECRET_MARKERS.iter().any(|m| upper.contains(m))
}
pub fn child_env() -> Vec<(String, String)> {
std::env::vars()
.filter(|(name, _)| !is_secret_env_name(name))
.collect()
}
pub fn scrub(command: &mut tokio::process::Command) -> &mut tokio::process::Command {
command.env_clear().envs(child_env())
}
pub async fn wait_with_timeout(
child: &mut tokio::process::Child,
timeout: std::time::Duration,
) -> std::io::Result<Option<std::process::Output>> {
use tokio::io::AsyncReadExt;
let mut stdout_pipe = child.stdout.take();
let mut stderr_pipe = child.stderr.take();
let collected = {
let drain = async {
let mut stdout = Vec::new();
let mut stderr = Vec::new();
let status = {
let read_out = async {
if let Some(pipe) = stdout_pipe.as_mut() {
let _ = pipe.read_to_end(&mut stdout).await;
}
};
let read_err = async {
if let Some(pipe) = stderr_pipe.as_mut() {
let _ = pipe.read_to_end(&mut stderr).await;
}
};
let (_, _, status) = tokio::join!(read_out, read_err, child.wait());
status?
};
Ok::<_, std::io::Error>(std::process::Output {
status,
stdout,
stderr,
})
};
tokio::time::timeout(timeout, drain).await
};
match collected {
Ok(output) => output.map(Some),
Err(_) => {
let _ = child.kill().await;
Ok(None)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn denies_the_secrets_the_sandbox_refuses_to_read() {
for name in [
"ANTHROPIC_API_KEY",
"OPENAI_API_KEY",
"GITHUB_TOKEN",
"APOLLO_TELEGRAM_TOKEN",
"APOLLO_DISCORD_TOKEN",
"apollo_telegram_token",
"AWS_SECRET_ACCESS_KEY",
"AWS_ACCESS_KEY_ID",
"DB_PASSWORD",
"NPM_TOKEN",
"GOOGLE_APPLICATION_CREDENTIALS",
"SSH_PRIVATE_KEY",
"SLACK_SIGNING_SECRET",
"HF_TOKEN",
] {
assert!(is_secret_env_name(name), "should be denied: {name}");
}
}
#[test]
fn keeps_what_ordinary_tool_use_needs() {
for name in [
"PATH",
"HOME",
"USER",
"SHELL",
"TERM",
"LANG",
"LC_ALL",
"TMPDIR",
"PWD",
"CARGO_HOME",
"RUSTUP_HOME",
"GOPATH",
"PKG_CONFIG_PATH",
"VIRTUAL_ENV",
"HTTPS_PROXY",
"SSH_AUTH_SOCK",
"XDG_SESSION_TYPE",
] {
assert!(!is_secret_env_name(name), "should be kept: {name}");
}
}
#[test]
fn child_env_carries_path_and_no_secret_names() {
let env = child_env();
assert!(env.iter().any(|(k, _)| k == "PATH"));
assert!(
!env.iter().any(|(k, _)| is_secret_env_name(k)),
"the filtered environment must contain no secret-bearing names"
);
}
}