diff options
| author | main <main@swarm.moe> | 2026-03-23 16:51:01 -0400 |
|---|---|---|
| committer | main <main@swarm.moe> | 2026-03-23 16:51:01 -0400 |
| commit | 10d4e08bc5d18daa59ddec19a3e2bf345331ccfc (patch) | |
| tree | e0a702e4abff8059dfc7a72bbef599e1e79f896b /crates/phone-opus/tests/mcp_hardening.rs | |
| parent | c3ad44cf3ec3bcd080f62c19d915ac1749576302 (diff) | |
| download | phone_opus-10d4e08bc5d18daa59ddec19a3e2bf345331ccfc.zip | |
Externalize Claude sandboxing with systemd-run
Diffstat (limited to 'crates/phone-opus/tests/mcp_hardening.rs')
| -rw-r--r-- | crates/phone-opus/tests/mcp_hardening.rs | 158 |
1 files changed, 156 insertions, 2 deletions
diff --git a/crates/phone-opus/tests/mcp_hardening.rs b/crates/phone-opus/tests/mcp_hardening.rs index a1fb6ae..f65c254 100644 --- a/crates/phone-opus/tests/mcp_hardening.rs +++ b/crates/phone-opus/tests/mcp_hardening.rs @@ -12,6 +12,7 @@ use libmcp_testkit::read_json_lines; use serde as _; use serde_json::{Value, json}; use thiserror as _; +use users as _; use uuid as _; use phone_opus_test_support::PROMPT_PREFIX; @@ -184,9 +185,27 @@ set -eu if [ -n "${PHONE_OPUS_TEST_PWD_FILE:-}" ]; then pwd >"$PHONE_OPUS_TEST_PWD_FILE" fi +if [ -n "${PHONE_OPUS_TEST_ENV_FILE:-}" ]; then + { + printf 'HOME=%s\n' "${HOME:-}" + printf 'XDG_CONFIG_HOME=%s\n' "${XDG_CONFIG_HOME:-}" + printf 'XDG_CACHE_HOME=%s\n' "${XDG_CACHE_HOME:-}" + printf 'XDG_STATE_HOME=%s\n' "${XDG_STATE_HOME:-}" + } >"$PHONE_OPUS_TEST_ENV_FILE" +fi if [ -n "${PHONE_OPUS_TEST_ARGS_FILE:-}" ]; then printf '%s\n' "$@" >"$PHONE_OPUS_TEST_ARGS_FILE" fi +if [ -n "${PHONE_OPUS_TEST_CWD_WRITE_PROBE_FILE:-}" ]; then + probe_target="${PWD}/.phone_opus_write_probe" + probe_error="${PHONE_OPUS_TEST_CWD_WRITE_ERROR_FILE:-/tmp/phone-opus-write.err}" + if printf probe >"$probe_target" 2>"$probe_error"; then + printf 'write_succeeded\n' >"$PHONE_OPUS_TEST_CWD_WRITE_PROBE_FILE" + rm -f "$probe_target" + else + printf 'write_failed\n' >"$PHONE_OPUS_TEST_CWD_WRITE_PROBE_FILE" + fi +fi if [ -n "${PHONE_OPUS_TEST_STDERR:-}" ]; then printf '%s\n' "$PHONE_OPUS_TEST_STDERR" >&2 fi @@ -205,6 +224,50 @@ exit "${PHONE_OPUS_TEST_EXIT_CODE:-0}" Ok(()) } +fn seed_caller_claude_home(home: &Path) -> TestResult { + let claude_root = home.join(".claude"); + must( + fs::create_dir_all(claude_root.join(".claude")), + "create caller .claude tree", + )?; + must( + fs::write( + claude_root.join(".credentials.json"), + "{\n \"auth\": \"token\"\n}\n", + ), + "write caller credentials", + )?; + must( + fs::write( + claude_root.join("settings.json"), + "{\n \"theme\": \"default\"\n}\n", + ), + "write caller settings", + )?; + must( + fs::write( + claude_root.join("settings.local.json"), + "{\n \"profile\": \"local\"\n}\n", + ), + "write caller local settings", + )?; + must( + fs::write( + claude_root.join(".claude").join("settings.local.json"), + "{\n \"sandbox\": \"read-only\"\n}\n", + ), + "write nested caller local settings", + )?; + must( + fs::write( + claude_root.join("CLAUDE.md"), + "Global Claude instructions for phone_opus tests.\n", + ), + "write caller CLAUDE.md", + )?; + Ok(()) +} + #[test] fn cold_start_exposes_consult_and_ops_tools() -> TestResult { let root = temp_root("cold_start")?; @@ -239,13 +302,19 @@ fn consult_can_resume_a_prior_session_with_read_only_toolset_and_requested_worki let root = temp_root("consult_success")?; let state_home = root.join("state-home"); let sandbox = root.join("sandbox"); + let caller_home = root.join("caller-home"); must(fs::create_dir_all(&state_home), "create state home")?; must(fs::create_dir_all(&sandbox), "create sandbox")?; + must(fs::create_dir_all(&caller_home), "create caller home")?; + seed_caller_claude_home(&caller_home)?; let fake_claude = root.join("claude"); let stdout_file = root.join("stdout.json"); let args_file = root.join("args.txt"); let pwd_file = root.join("pwd.txt"); + let env_file = root.join("env.txt"); + let cwd_probe_file = root.join("cwd-write-probe.txt"); + let cwd_probe_error_file = root.join("cwd-write-probe.err"); let resumed_session = "81f218eb-568b-409b-871b-f6e86d8f666f"; write_fake_claude_script(&fake_claude)?; must( @@ -284,11 +353,25 @@ fn consult_can_resume_a_prior_session_with_read_only_toolset_and_requested_worki let stdout_path = stdout_file.display().to_string(); let args_path = args_file.display().to_string(); let pwd_path = pwd_file.display().to_string(); + let env_path = env_file.display().to_string(); + let cwd_probe_path = cwd_probe_file.display().to_string(); + let cwd_probe_error_path = cwd_probe_error_file.display().to_string(); + let caller_home_path = caller_home.display().to_string(); let env = [ + ("HOME", caller_home_path.as_str()), ("PHONE_OPUS_CLAUDE_BIN", claude_bin.as_str()), ("PHONE_OPUS_TEST_STDOUT_FILE", stdout_path.as_str()), ("PHONE_OPUS_TEST_ARGS_FILE", args_path.as_str()), ("PHONE_OPUS_TEST_PWD_FILE", pwd_path.as_str()), + ("PHONE_OPUS_TEST_ENV_FILE", env_path.as_str()), + ( + "PHONE_OPUS_TEST_CWD_WRITE_PROBE_FILE", + cwd_probe_path.as_str(), + ), + ( + "PHONE_OPUS_TEST_CWD_WRITE_ERROR_FILE", + cwd_probe_error_path.as_str(), + ), ]; let mut harness = McpHarness::spawn(&state_home, &env)?; let _ = harness.initialize()?; @@ -347,8 +430,9 @@ fn consult_can_resume_a_prior_session_with_read_only_toolset_and_requested_worki assert!(lines.contains(&"max")); assert!(lines.contains(&"--tools")); assert!(lines.contains(&"Bash,Read,Grep,Glob,LS,WebFetch,WebSearch")); - assert!(lines.contains(&"--permission-mode")); - assert!(lines.contains(&"dontAsk")); + assert!(lines.contains(&"--dangerously-skip-permissions")); + assert!(!lines.contains(&"--permission-mode")); + assert!(!lines.contains(&"dontAsk")); assert!(lines.contains(&"--resume")); assert!(lines.contains(&resumed_session)); assert!(lines.contains(&"--max-turns")); @@ -359,6 +443,64 @@ fn consult_can_resume_a_prior_session_with_read_only_toolset_and_requested_worki let user_prompt_index = must_some(args.find("say oracle"), "user prompt inside args")?; assert!(prefix_index < user_prompt_index); + let env_dump = must(fs::read_to_string(&env_file), "read fake env file")?; + let state_root = state_home.join("phone_opus"); + let claude_home = state_root.join("claude-home"); + let xdg_config_home = state_root.join("xdg-config"); + let xdg_cache_home = state_root.join("xdg-cache"); + let xdg_state_home = state_root.join("xdg-state"); + assert!(env_dump.contains(format!("HOME={}", claude_home.display()).as_str())); + assert!(env_dump.contains(format!("XDG_CONFIG_HOME={}", xdg_config_home.display()).as_str())); + assert!(env_dump.contains(format!("XDG_CACHE_HOME={}", xdg_cache_home.display()).as_str())); + assert!(env_dump.contains(format!("XDG_STATE_HOME={}", xdg_state_home.display()).as_str())); + + assert_eq!( + must( + fs::read_to_string(claude_home.join(".claude").join(".credentials.json")), + "read mirrored credentials" + )?, + "{\n \"auth\": \"token\"\n}\n" + ); + assert_eq!( + must( + fs::read_to_string(claude_home.join(".claude").join("settings.json")), + "read mirrored settings" + )?, + "{\n \"theme\": \"default\"\n}\n" + ); + assert_eq!( + must( + fs::read_to_string(claude_home.join(".claude").join("settings.local.json")), + "read mirrored local settings" + )?, + "{\n \"profile\": \"local\"\n}\n" + ); + assert_eq!( + must( + fs::read_to_string( + claude_home + .join(".claude") + .join(".claude") + .join("settings.local.json") + ), + "read mirrored nested local settings" + )?, + "{\n \"sandbox\": \"read-only\"\n}\n" + ); + assert_eq!( + must( + fs::read_to_string(claude_home.join(".claude").join("CLAUDE.md")), + "read mirrored CLAUDE.md" + )?, + "Global Claude instructions for phone_opus tests.\n" + ); + + let cwd_probe = must( + fs::read_to_string(&cwd_probe_file), + "read cwd write probe result", + )?; + assert_eq!(cwd_probe.trim(), "write_failed"); + let telemetry = harness.call_tool(4, "telemetry_snapshot", json!({}))?; assert_tool_ok(&telemetry); let hot_methods = tool_content(&telemetry)["hot_methods"] @@ -378,8 +520,10 @@ fn consult_can_run_in_background_and_be_polled() -> TestResult { let root = temp_root("consult_background")?; let state_home = root.join("state-home"); let sandbox = root.join("sandbox"); + let caller_home = root.join("caller-home"); must(fs::create_dir_all(&state_home), "create state home")?; must(fs::create_dir_all(&sandbox), "create sandbox")?; + must(fs::create_dir_all(&caller_home), "create caller home")?; let fake_claude = root.join("claude"); let stdout_file = root.join("stdout.json"); @@ -422,7 +566,9 @@ fn consult_can_run_in_background_and_be_polled() -> TestResult { let stdout_path = stdout_file.display().to_string(); let args_path = args_file.display().to_string(); let pwd_path = pwd_file.display().to_string(); + let caller_home_path = caller_home.display().to_string(); let env = [ + ("HOME", caller_home_path.as_str()), ("PHONE_OPUS_CLAUDE_BIN", claude_bin.as_str()), ("PHONE_OPUS_TEST_STDOUT_FILE", stdout_path.as_str()), ("PHONE_OPUS_TEST_ARGS_FILE", args_path.as_str()), @@ -527,11 +673,15 @@ fn consult_surfaces_downstream_cli_failures() -> TestResult { let root = temp_root("consult_failure")?; let state_home = root.join("state-home"); let fake_claude = root.join("claude"); + let caller_home = root.join("caller-home"); must(fs::create_dir_all(&state_home), "create state home")?; + must(fs::create_dir_all(&caller_home), "create caller home")?; write_fake_claude_script(&fake_claude)?; let claude_bin = fake_claude.display().to_string(); + let caller_home_path = caller_home.display().to_string(); let env = [ + ("HOME", caller_home_path.as_str()), ("PHONE_OPUS_CLAUDE_BIN", claude_bin.as_str()), ("PHONE_OPUS_TEST_EXIT_CODE", "17"), ("PHONE_OPUS_TEST_STDERR", "permission denied by fake claude"), @@ -559,11 +709,15 @@ fn consult_never_replays_after_worker_transport_failure() -> TestResult { let root = temp_root("consult_no_replay")?; let state_home = root.join("state-home"); let fake_claude = root.join("claude"); + let caller_home = root.join("caller-home"); must(fs::create_dir_all(&state_home), "create state home")?; + must(fs::create_dir_all(&caller_home), "create caller home")?; write_fake_claude_script(&fake_claude)?; let claude_bin = fake_claude.display().to_string(); + let caller_home_path = caller_home.display().to_string(); let env = [ + ("HOME", caller_home_path.as_str()), ("PHONE_OPUS_CLAUDE_BIN", claude_bin.as_str()), ( "PHONE_OPUS_MCP_TEST_WORKER_CRASH_ONCE_KEY", |