swarm repositories / source
aboutsummaryrefslogtreecommitdiff
path: root/crates/phone-opus/tests/mcp_hardening.rs
diff options
context:
space:
mode:
authormain <main@swarm.moe>2026-03-23 16:51:01 -0400
committermain <main@swarm.moe>2026-03-23 16:51:01 -0400
commit10d4e08bc5d18daa59ddec19a3e2bf345331ccfc (patch)
treee0a702e4abff8059dfc7a72bbef599e1e79f896b /crates/phone-opus/tests/mcp_hardening.rs
parentc3ad44cf3ec3bcd080f62c19d915ac1749576302 (diff)
downloadphone_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.rs158
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",