use crate::common::*; fn printenv_inside(args: &[&str], vars: &[(&str, &str)], query: &[&str]) -> String { let script = query .iter() .map(|v| format!("printenv {v} || echo MISSING:{v}")) .collect::>() .join("; "); let mut cmd = Sandbox::new(args); for (k, v) in vars { cmd.env(k, v); } let output = cmd .args(["--", "bash", "-c", &script]) .output() .expect("agent-sandbox binary failed to execute"); String::from_utf8_lossy(&output.stdout).into_owned() } #[test] fn whitelist_keeps_identity_and_terminal_vars() { let stdout = printenv_inside( &["--whitelist"], &[("TERM", "xterm-test"), ("LANG", "C.UTF-8")], &["HOME", "PATH", "TERM", "LANG"], ); assert!(!stdout.contains("MISSING:HOME"), "HOME stripped: {stdout}"); assert!(!stdout.contains("MISSING:PATH"), "PATH stripped: {stdout}"); assert!(stdout.contains("xterm-test"), "TERM stripped: {stdout}"); assert!(stdout.contains("C.UTF-8"), "LANG stripped: {stdout}"); } #[test] fn whitelist_strips_arbitrary_host_var() { let stdout = printenv_inside( &["--whitelist"], &[("SOME_RANDOM_NOISE_VAR", "leak")], &["SOME_RANDOM_NOISE_VAR"], ); assert!( stdout.contains("MISSING:SOME_RANDOM_NOISE_VAR"), "expected arbitrary host var to be stripped, got: {stdout}" ); assert!(!stdout.contains("leak")); } #[test] fn whitelist_keeps_vendor_prefixes() { let stdout = printenv_inside( &["--whitelist"], &[ ("CLAUDE_FOO", "claude-val"), ("ANTHROPIC_MODEL", "anthropic-val"), ("OPENAI_API_KEY", "openai-val"), ("CODEX_FOO", "codex-val"), ("GEMINI_API_KEY", "gemini-val"), ("OTEL_SERVICE_NAME", "otel-val"), ], &[ "CLAUDE_FOO", "ANTHROPIC_MODEL", "OPENAI_API_KEY", "CODEX_FOO", "GEMINI_API_KEY", "OTEL_SERVICE_NAME", ], ); for expected in [ "claude-val", "anthropic-val", "openai-val", "codex-val", "gemini-val", "otel-val", ] { assert!( stdout.contains(expected), "expected {expected} in output, got: {stdout}" ); } assert!(!stdout.contains("MISSING:"), "unexpected strip: {stdout}"); } #[test] fn whitelist_keeps_lc_prefix() { let stdout = printenv_inside( &["--whitelist"], &[("LC_TIME", "en_US.UTF-8")], &["LC_TIME"], ); assert!(stdout.contains("en_US.UTF-8"), "LC_TIME missing: {stdout}"); } #[test] fn whitelist_keeps_non_gui_xdg_vars() { let stdout = printenv_inside( &["--whitelist"], &[ ("XDG_CONFIG_HOME", "/cfg"), ("XDG_DATA_HOME", "/data"), ("XDG_CACHE_HOME", "/cache"), ("XDG_STATE_HOME", "/state"), ("XDG_CONFIG_DIRS", "/etc/xdg"), ("XDG_DATA_DIRS", "/usr/share"), ], &[ "XDG_CONFIG_HOME", "XDG_DATA_HOME", "XDG_CACHE_HOME", "XDG_STATE_HOME", "XDG_CONFIG_DIRS", "XDG_DATA_DIRS", ], ); assert!( !stdout.contains("MISSING:"), "XDG non-GUI stripped: {stdout}" ); } #[test] fn whitelist_strips_gui_xdg_vars() { let stdout = printenv_inside( &["--whitelist"], &[ ("XDG_RUNTIME_DIR", "/run/user/1000"), ("XDG_SESSION_ID", "1"), ("XDG_CURRENT_DESKTOP", "KDE"), ("XDG_SEAT", "seat0"), ], &[ "XDG_RUNTIME_DIR", "XDG_SESSION_ID", "XDG_CURRENT_DESKTOP", "XDG_SEAT", ], ); for var in [ "XDG_RUNTIME_DIR", "XDG_SESSION_ID", "XDG_CURRENT_DESKTOP", "XDG_SEAT", ] { assert!( stdout.contains(&format!("MISSING:{var}")), "expected {var} stripped, got: {stdout}" ); } } #[test] fn whitelist_strips_dbus_vars() { let stdout = printenv_inside( &["--whitelist"], &[ ("DBUS_SESSION_BUS_ADDRESS", "unix:path=/foo"), ("DBUS_SYSTEM_BUS_ADDRESS", "unix:path=/bar"), ], &["DBUS_SESSION_BUS_ADDRESS", "DBUS_SYSTEM_BUS_ADDRESS"], ); assert!( stdout.contains("MISSING:DBUS_SESSION_BUS_ADDRESS"), "expected DBUS_SESSION stripped: {stdout}" ); assert!( stdout.contains("MISSING:DBUS_SYSTEM_BUS_ADDRESS"), "expected DBUS_SYSTEM stripped: {stdout}" ); } #[test] fn whitelist_env_sets_user_var() { let stdout = printenv_inside( &["--whitelist", "--env", "USER_INJECTED=forced"], &[], &["USER_INJECTED"], ); assert!(stdout.contains("forced"), "env not applied: {stdout}"); } #[test] fn whitelist_env_keep_passes_through_host_var() { let stdout = printenv_inside( &["--whitelist", "--env", "PASSED_THROUGH"], &[("PASSED_THROUGH", "from-host")], &["PASSED_THROUGH"], ); assert!( stdout.contains("from-host"), "expected --env KEY to pass host value through: {stdout}" ); } #[test] fn whitelist_env_keep_absent_host_var_is_skipped() { let stdout = printenv_inside( &["--whitelist", "--env", "NEVER_SET_ON_HOST"], &[], &["NEVER_SET_ON_HOST"], ); assert!( stdout.contains("MISSING:NEVER_SET_ON_HOST"), "expected absent keep-var to remain unset: {stdout}" ); } #[test] fn whitelist_unsetenv_overrides_kept_var() { let stdout = printenv_inside( &["--whitelist", "--unsetenv", "TERM"], &[("TERM", "xterm-test")], &["TERM"], ); assert!( stdout.contains("MISSING:TERM"), "expected --unsetenv to strip kept var: {stdout}" ); } #[test] fn blacklist_drops_token_and_secret_vars() { let stdout = printenv_inside( &["--blacklist"], &[ ("GH_TOKEN", "gh-secret"), ("AWS_SECRET_ACCESS_KEY", "aws-secret"), ("MY_PASSWORD", "pw"), ("FOO_API_KEY", "fookey"), ], &[ "GH_TOKEN", "AWS_SECRET_ACCESS_KEY", "MY_PASSWORD", "FOO_API_KEY", ], ); for var in [ "GH_TOKEN", "AWS_SECRET_ACCESS_KEY", "MY_PASSWORD", "FOO_API_KEY", ] { assert!( stdout.contains(&format!("MISSING:{var}")), "expected {var} stripped in blacklist mode, got: {stdout}" ); } for leaked in ["gh-secret", "aws-secret", "pw", "fookey"] { assert!(!stdout.contains(leaked), "{leaked} leaked: {stdout}"); } } #[test] fn blacklist_carves_out_vendor_api_keys() { let stdout = printenv_inside( &["--blacklist"], &[ ("ANTHROPIC_API_KEY", "anthropic-key"), ("OPENAI_API_KEY", "openai-key"), ("GEMINI_API_KEY", "gemini-key"), ], &["ANTHROPIC_API_KEY", "OPENAI_API_KEY", "GEMINI_API_KEY"], ); for expected in ["anthropic-key", "openai-key", "gemini-key"] { assert!( stdout.contains(expected), "expected {expected} to survive carve-out, got: {stdout}" ); } assert!(!stdout.contains("MISSING:"), "carve-out failed: {stdout}"); } #[test] fn blacklist_suffix_match_does_not_catch_substring() { let stdout = printenv_inside( &["--blacklist"], &[ ("TOKENIZER_PATH", "/opt/tok"), ("MY_TOKEN_HOLDER", "holder"), ], &["TOKENIZER_PATH", "MY_TOKEN_HOLDER"], ); assert!( stdout.contains("/opt/tok"), "TOKENIZER_PATH stripped: {stdout}" ); assert!( stdout.contains("holder"), "MY_TOKEN_HOLDER stripped: {stdout}" ); } #[test] fn blacklist_keeps_unrelated_host_var() { let stdout = printenv_inside( &["--blacklist"], &[("MY_NICE_VAR", "hello")], &["MY_NICE_VAR"], ); assert!(stdout.contains("hello"), "MY_NICE_VAR stripped: {stdout}"); } #[test] fn blacklist_keeps_dbus_vars() { let stdout = printenv_inside( &["--blacklist"], &[ ("DBUS_SESSION_BUS_ADDRESS", "unix:path=/tmp/fake"), ("DBUS_SYSTEM_BUS_ADDRESS", "unix:path=/tmp/fake-system"), ], &["DBUS_SESSION_BUS_ADDRESS", "DBUS_SYSTEM_BUS_ADDRESS"], ); assert!(stdout.contains("unix:path=/tmp/fake")); assert!(stdout.contains("unix:path=/tmp/fake-system")); } #[test] fn no_env_filter_whitelist_keeps_arbitrary_host_var() { let stdout = printenv_inside( &["--whitelist", "--no-env-filter"], &[("SOME_RANDOM_NOISE_VAR", "kept")], &["SOME_RANDOM_NOISE_VAR"], ); assert!( stdout.contains("kept"), "expected --no-env-filter to pass host var through, got: {stdout}" ); } #[test] fn no_env_filter_blacklist_keeps_secrets() { let stdout = printenv_inside( &["--blacklist", "--no-env-filter"], &[("GH_TOKEN", "kept")], &["GH_TOKEN"], ); assert!( stdout.contains("kept"), "expected --no-env-filter to pass secrets through, got: {stdout}" ); } #[test] fn no_env_filter_still_honors_user_env() { let stdout = printenv_inside( &["--no-env-filter", "--env", "FORCED=yes"], &[], &["FORCED"], ); assert!( stdout.contains("yes"), "expected user --env to still work with --no-env-filter, got: {stdout}" ); } #[test] fn blacklist_env_overrides_builtin_deny() { let stdout = printenv_inside( &["--blacklist", "--env", "GH_TOKEN=overridden"], &[("GH_TOKEN", "original")], &["GH_TOKEN"], ); assert!( stdout.contains("overridden"), "expected --env to override deny, got: {stdout}" ); assert!(!stdout.contains("original")); }