From fbc26be7af8af19883e133892ad24278e4784be1 Mon Sep 17 00:00:00 2001 From: Alex Gorevski Date: Tue, 17 Feb 2026 13:30:42 -0800 Subject: [PATCH] fix(policy): treat git branch listing as read-only operation Remove 'branch' from requires_write_access() to resolve the contradiction where branch listing was classified as both read-only and write-requiring. Branch listing only enumerates local refs and has no side effects, so it should remain available under ReadOnly autonomy mode. Add regression tests: - branch_is_not_write_gated: verifies classification consistency - allows_branch_listing_in_readonly_mode: verifies end-to-end execution under ReadOnly autonomy - is_read_only_detection: now explicitly asserts branch is read-only Resolves zeroclaw-labs/zeroclaw#612 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/tools/git_operations.rs | 41 ++++++++++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/src/tools/git_operations.rs b/src/tools/git_operations.rs index 21440ba..04d6f54 100644 --- a/src/tools/git_operations.rs +++ b/src/tools/git_operations.rs @@ -53,7 +53,7 @@ impl GitOperationsTool { fn requires_write_access(&self, operation: &str) -> bool { matches!( operation, - "commit" | "add" | "checkout" | "branch" | "stash" | "reset" | "revert" + "commit" | "add" | "checkout" | "stash" | "reset" | "revert" ) } @@ -666,6 +666,16 @@ mod tests { assert!(!tool.requires_write_access("log")); } + #[test] + fn branch_is_not_write_gated() { + let tmp = TempDir::new().unwrap(); + let tool = test_tool(tmp.path()); + + // Branch listing is read-only; it must not require write access + assert!(!tool.requires_write_access("branch")); + assert!(tool.is_read_only("branch")); + } + #[test] fn is_read_only_detection() { let tmp = TempDir::new().unwrap(); @@ -674,6 +684,7 @@ mod tests { assert!(tool.is_read_only("status")); assert!(tool.is_read_only("diff")); assert!(tool.is_read_only("log")); + assert!(tool.is_read_only("branch")); assert!(!tool.is_read_only("commit")); assert!(!tool.is_read_only("add")); @@ -708,6 +719,34 @@ mod tests { .contains("higher autonomy")); } + #[tokio::test] + async fn allows_branch_listing_in_readonly_mode() { + let tmp = TempDir::new().unwrap(); + // Initialize a git repository so the command can succeed + std::process::Command::new("git") + .args(["init"]) + .current_dir(tmp.path()) + .output() + .unwrap(); + + let security = Arc::new(SecurityPolicy { + autonomy: AutonomyLevel::ReadOnly, + ..SecurityPolicy::default() + }); + let tool = GitOperationsTool::new(security, tmp.path().to_path_buf()); + + let result = tool + .execute(json!({"operation": "branch"})) + .await + .unwrap(); + // Branch listing must not be blocked by read-only autonomy + let error_msg = result.error.as_deref().unwrap_or(""); + assert!( + !error_msg.contains("read-only") && !error_msg.contains("higher autonomy"), + "branch listing should not be blocked in read-only mode, got: {error_msg}" + ); + } + #[tokio::test] async fn allows_readonly_ops_in_readonly_mode() { let tmp = TempDir::new().unwrap();