From 3ff0494cafe07a5df3fdb18f67f688ceb63f4cdf Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Sun, 8 Mar 2026 21:05:27 -0400 Subject: [PATCH] Exempt /tmp/ paths from read-before-write guard The ReadBeforeWriteSandbox was blocking engine writes to /tmp/arc-commit-msg on the second git checkpoint because the file already existed from the first checkpoint. Temp files don't need source-file protection. Co-Authored-By: Claude Opus 4.6 --- .../src/read_before_write_sandbox.rs | 57 ++++++++++++------- crates/arc-agent/src/sandbox.rs | 2 +- crates/arc-agent/src/test_support.rs | 4 +- 3 files changed, 39 insertions(+), 24 deletions(-) diff --git a/crates/arc-agent/src/read_before_write_sandbox.rs b/crates/arc-agent/src/read_before_write_sandbox.rs index 5da4a648f..6189e9313 100644 --- a/crates/arc-agent/src/read_before_write_sandbox.rs +++ b/crates/arc-agent/src/read_before_write_sandbox.rs @@ -61,6 +61,10 @@ impl ReadBeforeWriteSandbox { } async fn guard_write(&self, path: &str) -> Result<(), String> { + let normalized = self.normalize_path(path); + if normalized.starts_with("/tmp/") { + return Ok(()); + } let exists = self.inner.file_exists(path).await?; if exists && !self.has_read(path) { warn!(path = %path, "Write blocked: file not read by agent"); @@ -99,13 +103,18 @@ mod tests { use crate::test_support::MockSandbox; use std::collections::HashMap; + fn mock_with_files(files: HashMap) -> MockSandbox { + MockSandbox { + files, + working_dir: "/work", + ..Default::default() + } + } + // Cycle 1: write to existing unread file → error #[tokio::test] async fn write_to_existing_unread_file_returns_error() { - let mock = MockSandbox { - files: HashMap::from([("a.ts".into(), "content".into())]), - ..Default::default() - }; + let mock = mock_with_files(HashMap::from([("a.ts".into(), "content".into())])); let env = ReadBeforeWriteSandbox::new(Arc::new(mock)); let result = env.write_file("a.ts", "new content").await; @@ -119,7 +128,7 @@ mod tests { // Cycle 2: write to non-existent file → success #[tokio::test] async fn write_to_nonexistent_file_succeeds() { - let mock = MockSandbox::default(); + let mock = mock_with_files(HashMap::new()); let env = ReadBeforeWriteSandbox::new(Arc::new(mock)); let result = env.write_file("new.ts", "content").await; @@ -130,10 +139,7 @@ mod tests { // Cycle 3: mark_agent_read then write → success #[tokio::test] async fn read_then_write_succeeds() { - let mock = MockSandbox { - files: HashMap::from([("a.ts".into(), "content".into())]), - ..Default::default() - }; + let mock = mock_with_files(HashMap::from([("a.ts".into(), "content".into())])); let env = ReadBeforeWriteSandbox::new(Arc::new(mock)); env.mark_agent_read("a.ts"); @@ -145,10 +151,7 @@ mod tests { // Cycle 4: read_file alone does NOT satisfy guard #[tokio::test] async fn read_file_alone_does_not_satisfy_guard() { - let mock = MockSandbox { - files: HashMap::from([("a.ts".into(), "content".into())]), - ..Default::default() - }; + let mock = mock_with_files(HashMap::from([("a.ts".into(), "content".into())])); let env = ReadBeforeWriteSandbox::new(Arc::new(mock)); env.read_file("a.ts", None, None).await.unwrap(); @@ -163,6 +166,7 @@ mod tests { let mock = MockSandbox { files: HashMap::from([("b.ts".into(), "content".into())]), grep_results: vec!["b.ts:1:content".into()], + working_dir: "/work", ..Default::default() }; let env = ReadBeforeWriteSandbox::new(Arc::new(mock)); @@ -181,6 +185,7 @@ mod tests { let mock = MockSandbox { files: HashMap::from([("b.ts".into(), "content".into())]), grep_results: vec!["b.ts:1:content".into()], + working_dir: "/work", ..Default::default() }; let env = ReadBeforeWriteSandbox::new(Arc::new(mock)); @@ -197,6 +202,7 @@ mod tests { let mock = MockSandbox { files: HashMap::from([("c.ts".into(), "content".into())]), glob_results: vec!["c.ts".into()], + working_dir: "/work", ..Default::default() }; let env = ReadBeforeWriteSandbox::new(Arc::new(mock)); @@ -229,10 +235,7 @@ mod tests { // Cycle 9: delete unread file → error #[tokio::test] async fn delete_unread_file_returns_error() { - let mock = MockSandbox { - files: HashMap::from([("d.ts".into(), "content".into())]), - ..Default::default() - }; + let mock = mock_with_files(HashMap::from([("d.ts".into(), "content".into())])); let env = ReadBeforeWriteSandbox::new(Arc::new(mock)); let result = env.delete_file("d.ts").await; @@ -243,10 +246,7 @@ mod tests { // Cycle 10: error message is actionable #[tokio::test] async fn error_message_is_actionable() { - let mock = MockSandbox { - files: HashMap::from([("main.rs".into(), "fn main() {}".into())]), - ..Default::default() - }; + let mock = mock_with_files(HashMap::from([("main.rs".into(), "fn main() {}".into())])); let env = ReadBeforeWriteSandbox::new(Arc::new(mock)); let err = env.write_file("main.rs", "new").await.unwrap_err(); @@ -254,4 +254,19 @@ mod tests { assert!(err.contains("main.rs")); assert!(err.contains("read_file")); } + + // Cycle 11: write to /tmp bypasses guard + #[tokio::test] + async fn write_to_tmp_bypasses_guard() { + let mock = MockSandbox { + files: HashMap::from([("/tmp/arc-commit-msg".into(), "old".into())]), + working_dir: "/work", + ..Default::default() + }; + let env = ReadBeforeWriteSandbox::new(Arc::new(mock)); + + let result = env.write_file("/tmp/arc-commit-msg", "new").await; + + assert!(result.is_ok()); + } } diff --git a/crates/arc-agent/src/sandbox.rs b/crates/arc-agent/src/sandbox.rs index 21516a479..c4e93c03b 100644 --- a/crates/arc-agent/src/sandbox.rs +++ b/crates/arc-agent/src/sandbox.rs @@ -449,7 +449,7 @@ mod tests { fn mock_env_platform() { let env = MockSandbox::default(); assert_eq!(env.platform(), "darwin"); - assert_eq!(env.working_directory(), "/tmp/test"); + assert_eq!(env.working_directory(), "/work"); assert_eq!(env.os_version(), "Darwin 24.0.0"); } diff --git a/crates/arc-agent/src/test_support.rs b/crates/arc-agent/src/test_support.rs index d2b352002..b8836b98e 100644 --- a/crates/arc-agent/src/test_support.rs +++ b/crates/arc-agent/src/test_support.rs @@ -71,7 +71,7 @@ impl Default for MockSandbox { }, grep_results: vec![], glob_results: vec![], - working_dir: "/tmp/test", + working_dir: "/work", platform_str: "darwin", os_version_str: "Darwin 24.0.0".into(), apply_read_offset_limit: false, @@ -360,7 +360,7 @@ impl Sandbox for MutableMockSandbox { } fn working_directory(&self) -> &'static str { - "/tmp" + "/work" } fn platform(&self) -> &'static str {