swarm repositories / source
aboutsummaryrefslogtreecommitdiff
path: root/crates
diff options
context:
space:
mode:
authormain <main@swarm.moe>2026-03-24 01:19:25 -0400
committermain <main@swarm.moe>2026-03-24 01:19:25 -0400
commit690b4851ea0afd8b214ddaa5450eec3a8c3a7ec9 (patch)
tree837b41dd59ebc2e8bbf5db2ff72b9c44d7dd477e /crates
parentd64d1bd730aec23bcc5b01a78a8945863ea4d5a7 (diff)
downloadphone_opus-690b4851ea0afd8b214ddaa5450eec3a8c3a7ec9.zip
Share live Claude credentials with sandbox
Diffstat (limited to 'crates')
-rw-r--r--crates/phone-opus/src/mcp/service.rs42
-rw-r--r--crates/phone-opus/tests/mcp_hardening.rs35
2 files changed, 73 insertions, 4 deletions
diff --git a/crates/phone-opus/src/mcp/service.rs b/crates/phone-opus/src/mcp/service.rs
index 64cb778..d958a81 100644
--- a/crates/phone-opus/src/mcp/service.rs
+++ b/crates/phone-opus/src/mcp/service.rs
@@ -1,6 +1,8 @@
use std::collections::{BTreeMap, BTreeSet};
use std::fs;
use std::io::{self, BufRead, Write};
+#[cfg(unix)]
+use std::os::unix::fs::symlink;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
@@ -543,8 +545,7 @@ const CONSULT_OUTPUT_KEEP_COUNT: usize = 256;
const CONSULT_OUTPUT_MAX_AGE: Duration = Duration::from_secs(7 * 24 * 60 * 60);
const CONSULT_TIMESTAMP_FORMAT: &[time::format_description::FormatItem<'static>] =
time::macros::format_description!("[year][month][day]T[hour][minute][second]Z");
-const CLAUDE_SEED_FILES: [&str; 5] = [
- ".credentials.json",
+const CLAUDE_MIRROR_FILES: [&str; 4] = [
"settings.json",
"settings.local.json",
".claude/settings.local.json",
@@ -622,15 +623,24 @@ impl ClaudeSandbox {
self.source_home.join(".claude")
}
+ fn source_credentials_path(&self) -> PathBuf {
+ self.source_claude_dir().join(".credentials.json")
+ }
+
+ fn destination_credentials_path(&self) -> PathBuf {
+ self.claude_config_dir().join(".credentials.json")
+ }
+
fn sync_seed_claude_files(&self) -> io::Result<()> {
let source_root = self.source_claude_dir();
let destination_root = self.claude_config_dir();
- for relative in CLAUDE_SEED_FILES {
+ for relative in CLAUDE_MIRROR_FILES {
sync_optional_seed_file(
source_root.join(relative).as_path(),
destination_root.join(relative).as_path(),
)?;
}
+ self.sync_live_credentials()?;
Ok(())
}
@@ -653,6 +663,9 @@ impl ClaudeSandbox {
fn read_write_paths(&self) -> BTreeSet<PathBuf> {
let mut paths = BTreeSet::new();
let _ = paths.insert(self.state_root.clone());
+ if self.source_credentials_path().exists() {
+ let _ = paths.insert(self.source_credentials_path());
+ }
for root in SHARED_TMP_ROOTS {
let _ = paths.insert(PathBuf::from(root));
}
@@ -700,6 +713,20 @@ impl ClaudeSandbox {
}
environment
}
+
+ fn sync_live_credentials(&self) -> io::Result<()> {
+ let source = self.source_credentials_path();
+ let destination = self.destination_credentials_path();
+ if !source.exists() {
+ remove_optional_path(destination.as_path())?;
+ return Ok(());
+ }
+ if fs::read_link(destination.as_path()).ok().as_ref() == Some(&source) {
+ return Ok(());
+ }
+ remove_optional_path(destination.as_path())?;
+ symlink(source.as_path(), destination.as_path())
+ }
}
#[derive(Debug, Clone)]
@@ -1329,6 +1356,15 @@ fn sync_optional_seed_file(source: &Path, destination: &Path) -> io::Result<()>
Ok(())
}
+fn remove_optional_path(path: &Path) -> io::Result<()> {
+ match fs::symlink_metadata(path) {
+ Ok(metadata) if metadata.is_dir() => fs::remove_dir_all(path),
+ Ok(_) => fs::remove_file(path),
+ Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(()),
+ Err(error) => Err(error),
+ }
+}
+
fn write_bytes_file(path: &Path, bytes: &[u8]) -> io::Result<()> {
let parent = path.parent().ok_or_else(|| {
io::Error::new(
diff --git a/crates/phone-opus/tests/mcp_hardening.rs b/crates/phone-opus/tests/mcp_hardening.rs
index 107a578..06861f8 100644
--- a/crates/phone-opus/tests/mcp_hardening.rs
+++ b/crates/phone-opus/tests/mcp_hardening.rs
@@ -209,6 +209,15 @@ if [ -n "${PHONE_OPUS_TEST_CWD_WRITE_PROBE_FILE:-}" ]; then
printf 'write_failed\n' >"$PHONE_OPUS_TEST_CWD_WRITE_PROBE_FILE"
fi
fi
+if [ -n "${PHONE_OPUS_TEST_CREDENTIAL_WRITE_PROBE_FILE:-}" ]; then
+ credentials_target="${HOME}/.claude/.credentials.json"
+ credentials_error="${PHONE_OPUS_TEST_CREDENTIAL_WRITE_ERROR_FILE:-/tmp/phone-opus-credentials.err}"
+ if : >>"$credentials_target" 2>"$credentials_error"; then
+ printf 'write_succeeded\n' >"$PHONE_OPUS_TEST_CREDENTIAL_WRITE_PROBE_FILE"
+ else
+ printf 'write_failed\n' >"$PHONE_OPUS_TEST_CREDENTIAL_WRITE_PROBE_FILE"
+ fi
+fi
if [ -n "${PHONE_OPUS_TEST_STDERR:-}" ]; then
printf '%s\n' "$PHONE_OPUS_TEST_STDERR" >&2
fi
@@ -331,6 +340,8 @@ fn consult_can_resume_a_prior_session_with_read_only_toolset_and_requested_worki
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 credential_probe_file = root.join("credential-write-probe.txt");
+ let credential_probe_error_file = root.join("credential-write-probe.err");
let resumed_session = "81f218eb-568b-409b-871b-f6e86d8f666f";
write_fake_claude_script(&fake_claude)?;
must(
@@ -372,6 +383,8 @@ fn consult_can_resume_a_prior_session_with_read_only_toolset_and_requested_worki
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 credential_probe_path = credential_probe_file.display().to_string();
+ let credential_probe_error_path = credential_probe_error_file.display().to_string();
let caller_home_path = caller_home.display().to_string();
let env = [
("HOME", caller_home_path.as_str()),
@@ -388,6 +401,14 @@ fn consult_can_resume_a_prior_session_with_read_only_toolset_and_requested_worki
"PHONE_OPUS_TEST_CWD_WRITE_ERROR_FILE",
cwd_probe_error_path.as_str(),
),
+ (
+ "PHONE_OPUS_TEST_CREDENTIAL_WRITE_PROBE_FILE",
+ credential_probe_path.as_str(),
+ ),
+ (
+ "PHONE_OPUS_TEST_CREDENTIAL_WRITE_ERROR_FILE",
+ credential_probe_error_path.as_str(),
+ ),
];
let mut harness = McpHarness::spawn(&state_home, &env)?;
let _ = harness.initialize()?;
@@ -494,8 +515,15 @@ fn consult_can_resume_a_prior_session_with_read_only_toolset_and_requested_worki
assert_eq!(
must(
+ fs::read_link(claude_home.join(".claude").join(".credentials.json")),
+ "read credentials symlink"
+ )?,
+ caller_home.join(".claude").join(".credentials.json")
+ );
+ assert_eq!(
+ must(
fs::read_to_string(claude_home.join(".claude").join(".credentials.json")),
- "read mirrored credentials"
+ "read linked credentials"
)?,
"{\n \"auth\": \"token\"\n}\n"
);
@@ -538,6 +566,11 @@ fn consult_can_resume_a_prior_session_with_read_only_toolset_and_requested_worki
"read cwd write probe result",
)?;
assert_eq!(cwd_probe.trim(), "write_failed");
+ let credential_probe = must(
+ fs::read_to_string(&credential_probe_file),
+ "read credential write probe result",
+ )?;
+ assert_eq!(credential_probe.trim(), "write_succeeded");
let telemetry = harness.call_tool(4, "telemetry_snapshot", json!({}))?;
assert_tool_ok(&telemetry);