apollo/tools/
child_proc.rs1const SECRET_MARKERS: &[&str] = &[
19 "KEY",
20 "TOKEN",
21 "SECRET",
22 "PASSWORD",
23 "PASSWD",
24 "CREDENTIAL",
25 "AUTH",
26 "SESSION",
27 "COOKIE",
28 "PRIVATE",
29 "SIGNATURE",
30];
31
32const DENIED_PREFIXES: &[&str] = &["APOLLO_"];
35
36const MARKER_EXCEPTIONS: &[&str] = &[
39 "SSH_AUTH_SOCK",
40 "GPG_AGENT_INFO",
41 "XDG_SESSION_TYPE",
42 "XDG_SESSION_CLASS",
43 "XDG_SESSION_ID",
44 "XDG_SESSION_DESKTOP",
45 "KEYBOARD_LAYOUT",
46];
47
48pub fn is_secret_env_name(name: &str) -> bool {
50 let upper = name.to_ascii_uppercase();
51 if DENIED_PREFIXES.iter().any(|p| upper.starts_with(p)) {
52 return true;
53 }
54 if MARKER_EXCEPTIONS.contains(&upper.as_str()) {
55 return false;
56 }
57 SECRET_MARKERS.iter().any(|m| upper.contains(m))
58}
59
60pub fn child_env() -> Vec<(String, String)> {
62 std::env::vars()
63 .filter(|(name, _)| !is_secret_env_name(name))
64 .collect()
65}
66
67pub fn scrub(command: &mut tokio::process::Command) -> &mut tokio::process::Command {
70 command.env_clear().envs(child_env())
71}
72
73pub async fn wait_with_timeout(
81 child: &mut tokio::process::Child,
82 timeout: std::time::Duration,
83) -> std::io::Result<Option<std::process::Output>> {
84 use tokio::io::AsyncReadExt;
85
86 let mut stdout_pipe = child.stdout.take();
87 let mut stderr_pipe = child.stderr.take();
88
89 let collected = {
90 let drain = async {
91 let mut stdout = Vec::new();
92 let mut stderr = Vec::new();
93 let status = {
94 let read_out = async {
95 if let Some(pipe) = stdout_pipe.as_mut() {
96 let _ = pipe.read_to_end(&mut stdout).await;
97 }
98 };
99 let read_err = async {
100 if let Some(pipe) = stderr_pipe.as_mut() {
101 let _ = pipe.read_to_end(&mut stderr).await;
102 }
103 };
104 let (_, _, status) = tokio::join!(read_out, read_err, child.wait());
105 status?
106 };
107 Ok::<_, std::io::Error>(std::process::Output {
108 status,
109 stdout,
110 stderr,
111 })
112 };
113 tokio::time::timeout(timeout, drain).await
114 };
115
116 match collected {
117 Ok(output) => output.map(Some),
118 Err(_) => {
119 let _ = child.kill().await;
120 Ok(None)
121 }
122 }
123}
124
125#[cfg(test)]
126mod tests {
127 use super::*;
128
129 #[test]
130 fn denies_the_secrets_the_sandbox_refuses_to_read() {
131 for name in [
132 "ANTHROPIC_API_KEY",
133 "OPENAI_API_KEY",
134 "GITHUB_TOKEN",
135 "APOLLO_TELEGRAM_TOKEN",
136 "APOLLO_DISCORD_TOKEN",
137 "apollo_telegram_token",
138 "AWS_SECRET_ACCESS_KEY",
139 "AWS_ACCESS_KEY_ID",
140 "DB_PASSWORD",
141 "NPM_TOKEN",
142 "GOOGLE_APPLICATION_CREDENTIALS",
143 "SSH_PRIVATE_KEY",
144 "SLACK_SIGNING_SECRET",
145 "HF_TOKEN",
146 ] {
147 assert!(is_secret_env_name(name), "should be denied: {name}");
148 }
149 }
150
151 #[test]
152 fn keeps_what_ordinary_tool_use_needs() {
153 for name in [
154 "PATH",
155 "HOME",
156 "USER",
157 "SHELL",
158 "TERM",
159 "LANG",
160 "LC_ALL",
161 "TMPDIR",
162 "PWD",
163 "CARGO_HOME",
164 "RUSTUP_HOME",
165 "GOPATH",
166 "PKG_CONFIG_PATH",
167 "VIRTUAL_ENV",
168 "HTTPS_PROXY",
169 "SSH_AUTH_SOCK",
170 "XDG_SESSION_TYPE",
171 ] {
172 assert!(!is_secret_env_name(name), "should be kept: {name}");
173 }
174 }
175
176 #[test]
177 fn child_env_carries_path_and_no_secret_names() {
178 let env = child_env();
179 assert!(env.iter().any(|(k, _)| k == "PATH"));
180 assert!(
181 !env.iter().any(|(k, _)| is_secret_env_name(k)),
182 "the filtered environment must contain no secret-bearing names"
183 );
184 }
185}