use std::process::Command;
pub const PRESERVED_ENV_VARS: &[&str] = &[
"PATH",
"HOME",
"USER",
"SHELL",
"TERM",
"LANG",
"XDG_CONFIG_HOME",
"XDG_DATA_HOME",
"XDG_RUNTIME_DIR",
"XDG_CACHE_HOME",
"TMPDIR",
"TMP",
"TEMP",
"DYLD_FALLBACK_LIBRARY_PATH",
"CLAUDE_CONFIG_DIR",
"ANTHROPIC_AUTH_TOKEN",
"ANTHROPIC_BASE_URL",
"CLAUDE_CODE_ENTRYPOINT",
"CODEX_ACCESS_TOKEN",
"OPENAI_BASE_URL",
"DISABLE_TELEMETRY",
"OTEL_EXPORTER_OTLP_ENDPOINT",
];
#[cfg(windows)]
pub const PRESERVED_ENV_VARS_WINDOWS: &[&str] = &[
"LOCALAPPDATA",
"APPDATA",
"USERPROFILE",
"SystemRoot",
"COMSPEC",
"PATHEXT",
"HOMEPATH",
"HOMEDRIVE",
];
pub fn apply_env_whitelist(cmd: &mut Command, strict: bool) {
cmd.env_clear();
if strict {
if let Ok(path) = std::env::var("PATH") {
cmd.env("PATH", path);
}
return;
}
for var in PRESERVED_ENV_VARS {
if let Ok(val) = std::env::var(var) {
cmd.env(var, val);
}
}
#[cfg(windows)]
for var in PRESERVED_ENV_VARS_WINDOWS {
if let Ok(val) = std::env::var(var) {
cmd.env(var, val);
}
}
}
pub fn is_strict_env_clear() -> bool {
matches!(
std::env::var("SQLITE_GRAPHRAG_STRICT_ENV_CLEAR")
.ok()
.as_deref(),
Some("1") | Some("true") | Some("TRUE") | Some("True") | Some("yes") | Some("YES")
)
}
#[cfg(test)]
mod tests {
use super::*;
fn captured_env(cmd: &Command) -> Vec<(String, String)> {
cmd.get_envs()
.filter_map(|(k, v)| {
let k = k.to_str()?.to_string();
let v = v?.to_str()?.to_string();
Some((k, v))
})
.collect()
}
#[test]
#[serial_test::serial(env)]
fn whitelist_includes_custom_provider_vars() {
unsafe {
std::env::set_var("ANTHROPIC_AUTH_TOKEN", "sk-cp-test");
std::env::set_var("ANTHROPIC_BASE_URL", "https://api.minimax.io/anthropic");
std::env::set_var("OPENAI_BASE_URL", "https://api.openrouter.ai/v1");
}
let mut cmd = std::process::Command::new("/usr/bin/false");
apply_env_whitelist(&mut cmd, false);
let envs = captured_env(&cmd);
let has_token = envs
.iter()
.any(|(k, v)| k == "ANTHROPIC_AUTH_TOKEN" && v == "sk-cp-test");
let has_anthropic_url = envs
.iter()
.any(|(k, v)| k == "ANTHROPIC_BASE_URL" && v == "https://api.minimax.io/anthropic");
let has_openai_url = envs
.iter()
.any(|(k, v)| k == "OPENAI_BASE_URL" && v == "https://api.openrouter.ai/v1");
unsafe {
std::env::remove_var("ANTHROPIC_AUTH_TOKEN");
std::env::remove_var("ANTHROPIC_BASE_URL");
std::env::remove_var("OPENAI_BASE_URL");
}
assert!(has_token, "ANTHROPIC_AUTH_TOKEN not preserved");
assert!(has_anthropic_url, "ANTHROPIC_BASE_URL not preserved");
assert!(has_openai_url, "OPENAI_BASE_URL not preserved");
}
#[test]
#[serial_test::serial(env)]
fn whitelist_excludes_api_key_vars() {
unsafe {
std::env::set_var("ANTHROPIC_API_KEY", "sk-ant-violation");
std::env::set_var("OPENAI_API_KEY", "sk-violation");
}
let mut cmd = std::process::Command::new("/usr/bin/false");
apply_env_whitelist(&mut cmd, false);
let envs = captured_env(&cmd);
let has_anthropic_key = envs.iter().any(|(k, _)| k == "ANTHROPIC_API_KEY");
let has_openai_key = envs.iter().any(|(k, _)| k == "OPENAI_API_KEY");
unsafe {
std::env::remove_var("ANTHROPIC_API_KEY");
std::env::remove_var("OPENAI_API_KEY");
}
assert!(
!has_anthropic_key,
"ANTHROPIC_API_KEY must NEVER reach subprocess"
);
assert!(
!has_openai_key,
"OPENAI_API_KEY must NEVER reach subprocess"
);
}
#[test]
#[serial_test::serial(env)]
fn strict_mode_drops_credentials() {
unsafe {
std::env::set_var("ANTHROPIC_AUTH_TOKEN", "sk-cp-strict-test");
std::env::set_var("PATH", "/usr/bin:/bin");
}
let mut cmd = std::process::Command::new("/usr/bin/false");
apply_env_whitelist(&mut cmd, true);
let envs = captured_env(&cmd);
let has_token = envs.iter().any(|(k, _)| k == "ANTHROPIC_AUTH_TOKEN");
let has_path = envs
.iter()
.any(|(k, v)| k == "PATH" && v == "/usr/bin:/bin");
unsafe {
std::env::remove_var("ANTHROPIC_AUTH_TOKEN");
}
assert!(!has_token, "strict mode must drop credentials");
assert!(has_path, "strict mode preserves PATH only");
}
}