Test workspace-root patch protection for metadata (#36171)

## What changed

Add integration coverage for `apply_patch` with unified exec workspace roots. The test verifies that a normal file can be added while writes under existing `.git`, `.agents`, and `.codex` directories are rejected and do not create files.

GitOrigin-RevId: 21ad5ce7fb06e7c47b4086c1439f12d52d9235de
This commit is contained in:
jif
2026-07-30 14:34:10 +00:00
committed by copyberry
parent b445967cc0
commit 578c1b2230
@@ -1,5 +1,6 @@
use anyhow::Context;
use anyhow::Result;
use codex_exec_server::CreateDirectoryOptions;
use codex_exec_server::RemoveOptions;
use codex_features::Feature;
use codex_protocol::models::PermissionProfile;
@@ -187,6 +188,96 @@ async fn workspace_roots_allow_file_and_command_writes() -> Result<()> {
remove_files(&test, &[&patch_path, &command_path]).await
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn workspace_roots_allow_patches_but_protect_metadata_directories() -> Result<()> {
const PATCH_CONTENTS: &str = "workspace root patch access";
const PROTECTED_METADATA_DIRECTORIES: [&str; 3] = [".git", ".agents", ".codex"];
skip_if_wine_exec!(
Ok(()),
"Wine does not emulate Windows restricted-token and ACL sandbox semantics"
);
let server = start_mock_server().await;
let test = workspace_roots_test(&server).await?;
let cwd = PathUri::from_abs_path(&test.config.cwd);
let allowed_path = cwd.join("workspace-root-allowed-patch.txt")?;
for directory in PROTECTED_METADATA_DIRECTORIES {
test.fs()
.create_directory(
&cwd.join(directory)?,
CreateDirectoryOptions { recursive: true },
/*sandbox*/ None,
)
.await?;
}
let allowed_patch = format!(
"*** Begin Patch\n*** Add File: workspace-root-allowed-patch.txt\n+{PATCH_CONTENTS}\n*** End Patch\n"
);
let mut patch_calls = vec![
ev_response_created("resp-1"),
ev_apply_patch_custom_tool_call(PATCH_CALL_ID, &allowed_patch),
];
for directory in PROTECTED_METADATA_DIRECTORIES {
let call_id = format!("workspace-root-protected-{directory}");
let patch = format!(
"*** Begin Patch\n*** Add File: {directory}/protected.txt\n+metadata write\n*** End Patch\n"
);
patch_calls.push(ev_apply_patch_custom_tool_call(&call_id, &patch));
}
patch_calls.push(ev_completed("resp-1"));
let response_mock = mount_sse_sequence(
&server,
vec![
sse(patch_calls),
sse(vec![
ev_response_created("resp-2"),
ev_assistant_message("msg-1", "done"),
ev_completed("resp-2"),
]),
],
)
.await;
submit_workspace_turn(&test, "patch the workspace and protected metadata").await?;
let request = response_mock
.last_request()
.context("model should receive every workspace-root patch result")?;
let (_, patch_success) = request
.custom_tool_call_output_content_and_success(PATCH_CALL_ID)
.context("workspace patch result should be present")?;
assert_ne!(patch_success, Some(false));
assert_eq!(
read_file(&test, &allowed_path).await?,
format!("{PATCH_CONTENTS}\n")
);
for directory in PROTECTED_METADATA_DIRECTORIES {
let call_id = format!("workspace-root-protected-{directory}");
let (output, success) = request
.custom_tool_call_output_content_and_success(&call_id)
.with_context(|| format!("{directory} patch result should be present"))?;
assert_ne!(success, Some(true));
assert!(
output
.as_deref()
.is_some_and(|output| output.contains("outside of the project")),
"{directory} patch should be denied, got {output:?}"
);
let protected_path = cwd.join(directory)?.join("protected.txt")?;
let error = test
.fs()
.get_metadata(&protected_path, /*sandbox*/ None)
.await
.expect_err("protected metadata file should not be created");
assert_eq!(error.kind(), std::io::ErrorKind::NotFound);
}
remove_files(&test, &[&allowed_path]).await
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn workspace_roots_deny_file_and_command_writes_outside_roots() -> Result<()> {
const PATCH_CONTENTS: &str = "outside workspace root patch";