use std::fs;
use std::os::unix::fs::PermissionsExt;
use std::path::PathBuf;
use assert_cmd::Command as AssertCmd;
use serial_test::serial;
use tempfile::TempDir;
fn spawn_capture_claude_env() -> (TempDir, PathBuf, PathBuf) {
let dir = TempDir::new().expect("TempDir::new");
let env_dump_path = dir.path().join("captured_env.txt");
let script_path = dir.path().join("claude");
let script = format!(
r#"#!/usr/bin/env bash
set -euo pipefail
env > {}
exit 0
"#,
env_dump_path.display()
);
fs::write(&script_path, script).expect("write claude script");
let mut perms = fs::metadata(&script_path).expect("stat").permissions();
perms.set_mode(0o755);
fs::set_permissions(&script_path, perms).expect("chmod 755");
(dir, script_path, env_dump_path)
}
fn read_captured_env(path: &std::path::Path) -> Vec<(String, String)> {
let contents = fs::read_to_string(path).expect("read env dump");
contents
.lines()
.filter_map(|line| {
line.split_once('=')
.map(|(k, v)| (k.to_string(), v.to_string()))
})
.collect()
}
fn env_lacks(env: &[(String, String)], forbidden: &[&str]) -> bool {
!forbidden.iter().any(|k| env.iter().any(|(ak, _)| ak == *k))
}
#[test]
#[serial(env)]
fn claude_subprocess_inherits_custom_anthropic_provider_env() {
}
#[test]
#[serial(env)]
fn claude_subprocess_rejects_prohibited_anthropic_api_key() {
unsafe {
std::env::set_var("ANTHROPIC_API_KEY", "sk-ant-violation-test");
std::env::remove_var("ANTHROPIC_AUTH_TOKEN");
std::env::remove_var("ANTHROPIC_BASE_URL");
}
let (_dir, script_path, env_dump) = spawn_capture_claude_env();
let path_with_mock = format!(
"{}:{}",
script_path.parent().unwrap().display(),
"/usr/bin:/bin"
);
let output = AssertCmd::new(assert_cmd::cargo::cargo_bin!("sqlite-graphrag"))
.args([
"remember",
"--name",
"test-v183-rejection",
"--body",
"validation body",
])
.env("PATH", path_with_mock)
.env("ANTHROPIC_API_KEY", "sk-ant-violation-test")
.env("HOME", _dir.path())
.timeout(std::time::Duration::from_secs(30))
.output()
.expect("spawn sqlite-graphrag");
unsafe {
std::env::remove_var("ANTHROPIC_API_KEY");
}
let exit_ok = output.status.success();
let env_present = env_dump.exists();
if env_present {
let env = read_captured_env(&env_dump);
assert!(
env_lacks(&env, &["ANTHROPIC_API_KEY"]),
"ANTHROPIC_API_KEY must NEVER reach the subprocess (exit={:?})",
output.status.code()
);
}
assert!(
!exit_ok || !env_present,
"OAuth-only guard should abort spawn with non-zero exit (got {:?})",
output.status.code()
);
}
#[test]
#[serial(env)]
fn codex_subprocess_inherits_openai_base_url() {
unsafe {
std::env::set_var("OPENAI_BASE_URL", "https://api.openrouter.ai/v1");
std::env::remove_var("OPENAI_API_KEY");
std::env::remove_var("ANTHROPIC_API_KEY");
}
let dir = TempDir::new().expect("TempDir::new");
let env_dump_path = dir.path().join("captured_env.txt");
let script_path = dir.path().join("codex");
let script = format!(
r#"#!/usr/bin/env bash
set -euo pipefail
env > "{}"
exit 0
"#,
env_dump_path.display()
);
fs::write(&script_path, script).expect("write codex script");
let mut perms = fs::metadata(&script_path).expect("stat").permissions();
perms.set_mode(0o755);
fs::set_permissions(&script_path, perms).expect("chmod 755");
let path_with_mock = format!("{}:{}", dir.path().display(), "/usr/bin:/bin");
let _output = AssertCmd::new(assert_cmd::cargo::cargo_bin!("sqlite-graphrag"))
.args([
"remember",
"--name",
"test-v183-codex-base-url",
"--body",
"validation body",
])
.env("PATH", path_with_mock)
.env("OPENAI_BASE_URL", "https://api.openrouter.ai/v1")
.env("HOME", dir.path())
.timeout(std::time::Duration::from_secs(30))
.output()
.expect("spawn sqlite-graphrag");
unsafe {
std::env::remove_var("OPENAI_BASE_URL");
}
if env_dump_path.exists() {
let env = read_captured_env(&env_dump_path);
let has_openai_url = env
.iter()
.any(|(k, v)| k == "OPENAI_BASE_URL" && v == "https://api.openrouter.ai/v1");
assert!(
has_openai_url,
"OPENAI_BASE_URL not inherited by codex subprocess"
);
}
}
#[test]
#[serial(env)]
fn strict_env_clear_drops_custom_provider_credentials() {
unsafe {
std::env::set_var("ANTHROPIC_AUTH_TOKEN", "sk-cp-strict-test");
std::env::set_var("SQLITE_GRAPHRAG_STRICT_ENV_CLEAR", "1");
}
let (dir, script_path, env_dump) = spawn_capture_claude_env();
let path_with_mock = format!(
"{}:{}",
script_path.parent().unwrap().display(),
"/usr/bin:/bin"
);
let _output = AssertCmd::new(assert_cmd::cargo::cargo_bin!("sqlite-graphrag"))
.args([
"remember",
"--name",
"test-v183-strict-mode",
"--body",
"validation body",
])
.env("PATH", path_with_mock)
.env("ANTHROPIC_AUTH_TOKEN", "sk-cp-strict-test")
.env("SQLITE_GRAPHRAG_STRICT_ENV_CLEAR", "1")
.env("HOME", dir.path())
.timeout(std::time::Duration::from_secs(30))
.output()
.expect("spawn sqlite-graphrag");
unsafe {
std::env::remove_var("ANTHROPIC_AUTH_TOKEN");
std::env::remove_var("SQLITE_GRAPHRAG_STRICT_ENV_CLEAR");
}
if env_dump.exists() {
let env = read_captured_env(&env_dump);
assert!(
env_lacks(&env, &["ANTHROPIC_AUTH_TOKEN"]),
"strict mode must drop ANTHROPIC_AUTH_TOKEN (env dump: {:?})",
env.iter().map(|(k, _)| k.as_str()).collect::<Vec<_>>()
);
assert!(
env.iter().any(|(k, _)| k == "PATH"),
"strict mode must preserve PATH"
);
}
}
#[test]
#[serial(env)]
fn audit_no_token_leak_in_subprocess_stderr() {
let secret_token = "sk-cp-secret-value-XYZ-12345";
unsafe {
std::env::set_var("ANTHROPIC_AUTH_TOKEN", secret_token);
std::env::set_var("RUST_LOG", "trace");
}
let (dir, script_path, _env_dump) = spawn_capture_claude_env();
let path_with_mock = format!(
"{}:{}",
script_path.parent().unwrap().display(),
"/usr/bin:/bin"
);
let output = AssertCmd::new(assert_cmd::cargo::cargo_bin!("sqlite-graphrag"))
.args([
"remember",
"--name",
"test-v183-no-leak",
"--body",
"validation body",
])
.env("PATH", path_with_mock)
.env("ANTHROPIC_AUTH_TOKEN", secret_token)
.env("RUST_LOG", "trace")
.env("HOME", dir.path())
.timeout(std::time::Duration::from_secs(30))
.output()
.expect("spawn sqlite-graphrag");
unsafe {
std::env::remove_var("ANTHROPIC_AUTH_TOKEN");
std::env::remove_var("RUST_LOG");
}
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
!stdout.contains(secret_token),
"token leaked to stdout: {stdout}"
);
assert!(
!stderr.contains(secret_token),
"token leaked to stderr: {stderr}"
);
}