Address preserved path review feedback

This commit is contained in:
Eva Wong
2026-04-22 09:20:54 -07:00
parent 90d5f4304f
commit c5ddf213fe
12 changed files with 1768 additions and 357 deletions

View File

@@ -5,7 +5,6 @@
use std::collections::HashMap;
use std::collections::HashSet;
use std::ffi::OsStr;
use std::fmt;
use std::ops::Mul;
use std::path::Path;
@@ -84,6 +83,7 @@ pub use crate::permissions::FileSystemSandboxKind;
pub use crate::permissions::FileSystemSandboxPolicy;
pub use crate::permissions::FileSystemSpecialPath;
pub use crate::permissions::NetworkSandboxPolicy;
use crate::permissions::default_read_only_subpaths_for_writable_root;
pub use crate::request_permissions::RequestPermissionsArgs;
pub use crate::request_user_input::RequestUserInputEvent;
@@ -1249,17 +1249,13 @@ impl SandboxPolicy {
}
// For each root, compute subpaths that should remain read-only.
let cwd_root = AbsolutePathBuf::from_absolute_path(cwd).ok();
roots
.into_iter()
.map(|writable_root| {
let protect_missing_preserved_paths = cwd_root
.as_ref()
.is_some_and(|cwd_root| cwd_root == &writable_root);
WritableRoot {
read_only_subpaths: default_read_only_subpaths_for_writable_root(
&writable_root,
protect_missing_preserved_paths,
/*protect_missing_preserved_paths*/ true,
),
root: writable_root,
}
@@ -1270,107 +1266,6 @@ impl SandboxPolicy {
}
}
fn default_read_only_subpaths_for_writable_root(
writable_root: &AbsolutePathBuf,
protect_missing_preserved_paths: bool,
) -> Vec<AbsolutePathBuf> {
let mut subpaths: Vec<AbsolutePathBuf> = Vec::new();
let top_level_git = writable_root.join(".git");
// This applies to typical repos (directory .git), worktrees/submodules
// (file .git with gitdir pointer), and bare repos when the gitdir is the
// writable root itself.
let top_level_git_is_file = top_level_git.as_path().is_file();
let top_level_git_is_dir = top_level_git.as_path().is_dir();
if top_level_git_is_dir || top_level_git_is_file || protect_missing_preserved_paths {
if top_level_git_is_file
&& is_git_pointer_file(&top_level_git)
&& let Some(gitdir) = resolve_gitdir_from_file(&top_level_git)
{
subpaths.push(gitdir);
}
subpaths.push(top_level_git);
}
let top_level_agents = writable_root.join(".agents");
if protect_missing_preserved_paths || top_level_agents.as_path().is_dir() {
subpaths.push(top_level_agents);
}
// Keep top-level preserved paths under .codex read-only to the agent by
// default. For the workspace root itself, protect it even before the
// directory exists so first-time creation still goes through the
// preserved path approval flow.
let top_level_codex = writable_root.join(".codex");
if protect_missing_preserved_paths || top_level_codex.as_path().is_dir() {
subpaths.push(top_level_codex);
}
let mut deduped = Vec::with_capacity(subpaths.len());
let mut seen = HashSet::new();
for path in subpaths {
if seen.insert(path.to_path_buf()) {
deduped.push(path);
}
}
deduped
}
fn is_git_pointer_file(path: &AbsolutePathBuf) -> bool {
path.as_path().is_file() && path.as_path().file_name() == Some(OsStr::new(".git"))
}
fn resolve_gitdir_from_file(dot_git: &AbsolutePathBuf) -> Option<AbsolutePathBuf> {
let contents = match std::fs::read_to_string(dot_git.as_path()) {
Ok(contents) => contents,
Err(err) => {
error!(
"Failed to read {path} for gitdir pointer: {err}",
path = dot_git.as_path().display()
);
return None;
}
};
let trimmed = contents.trim();
let (_, gitdir_raw) = match trimmed.split_once(':') {
Some(parts) => parts,
None => {
error!(
"Expected {path} to contain a gitdir pointer, but it did not match `gitdir: <path>`.",
path = dot_git.as_path().display()
);
return None;
}
};
let gitdir_raw = gitdir_raw.trim();
if gitdir_raw.is_empty() {
error!(
"Expected {path} to contain a gitdir pointer, but it was empty.",
path = dot_git.as_path().display()
);
return None;
}
let base = match dot_git.as_path().parent() {
Some(base) => base,
None => {
error!(
"Unable to resolve parent directory for {path}.",
path = dot_git.as_path().display()
);
return None;
}
};
let gitdir_path = AbsolutePathBuf::resolve_path_against_base(gitdir_raw, base);
if !gitdir_path.as_path().exists() {
error!(
"Resolved gitdir path {path} does not exist.",
path = gitdir_path.as_path().display()
);
return None;
}
Some(gitdir_path)
}
/// Event Queue Entry - events from agent
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Event {
@@ -4453,6 +4348,15 @@ mod tests {
let expected_docs_public =
AbsolutePathBuf::from_absolute_path(canonical_cwd.join("docs/public"))
.expect("canonical docs/public");
let expected_docs_public_dot_agents =
AbsolutePathBuf::from_absolute_path(canonical_cwd.join("docs/public/.agents"))
.expect("canonical docs/public/.agents");
let expected_docs_public_dot_codex =
AbsolutePathBuf::from_absolute_path(canonical_cwd.join("docs/public/.codex"))
.expect("canonical docs/public/.codex");
let expected_docs_public_dot_git =
AbsolutePathBuf::from_absolute_path(canonical_cwd.join("docs/public/.git"))
.expect("canonical docs/public/.git");
let expected_dot_codex = AbsolutePathBuf::from_absolute_path(canonical_cwd.join(".codex"))
.expect("canonical .codex");
let expected_dot_git = AbsolutePathBuf::from_absolute_path(canonical_cwd.join(".git"))
@@ -4490,7 +4394,92 @@ mod tests {
expected_docs.to_path_buf()
],
),
(expected_docs_public.to_path_buf(), Vec::new()),
(
expected_docs_public.to_path_buf(),
vec![
expected_docs_public_dot_agents.to_path_buf(),
expected_docs_public_dot_codex.to_path_buf(),
expected_docs_public_dot_git.to_path_buf(),
],
),
]
);
}
#[test]
fn workspace_write_keeps_missing_git_absent_under_parent_repo() {
let repo = TempDir::new().expect("tempdir");
std::fs::create_dir(repo.path().join(".git")).expect("create parent .git");
let cwd = repo.path().join("sub");
std::fs::create_dir(&cwd).expect("create subdir");
let expected_root = AbsolutePathBuf::from_absolute_path(&cwd).expect("absolute cwd");
let expected_dot_codex =
AbsolutePathBuf::from_absolute_path(cwd.join(".codex")).expect("canonical .codex");
let expected_dot_agents =
AbsolutePathBuf::from_absolute_path(cwd.join(".agents")).expect("canonical .agents");
let policy = SandboxPolicy::WorkspaceWrite {
writable_roots: vec![],
read_only_access: ReadOnlyAccess::Restricted {
include_platform_defaults: false,
readable_roots: vec![],
},
network_access: false,
exclude_tmpdir_env_var: true,
exclude_slash_tmp: true,
};
assert_eq!(
sorted_writable_roots(policy.get_writable_roots_with_cwd(&cwd)),
vec![(
expected_root.to_path_buf(),
vec![
expected_dot_agents.to_path_buf(),
expected_dot_codex.to_path_buf()
],
)]
);
}
#[test]
fn workspace_write_reserves_missing_preserved_paths_under_configured_writable_roots() {
let root = TempDir::new().expect("tempdir");
let cwd = root.path().join("cwd");
let extra = root.path().join("extra");
std::fs::create_dir_all(&cwd).expect("create cwd");
std::fs::create_dir_all(&extra).expect("create extra writable root");
let expected_cwd = AbsolutePathBuf::from_absolute_path(&cwd).expect("absolute cwd");
let expected_extra =
AbsolutePathBuf::from_absolute_path(&extra).expect("absolute extra root");
let policy = SandboxPolicy::WorkspaceWrite {
writable_roots: vec![expected_extra.clone()],
read_only_access: ReadOnlyAccess::Restricted {
include_platform_defaults: false,
readable_roots: vec![],
},
network_access: false,
exclude_tmpdir_env_var: true,
exclude_slash_tmp: true,
};
assert_eq!(
sorted_writable_roots(policy.get_writable_roots_with_cwd(&cwd)),
vec![
(
expected_cwd.to_path_buf(),
vec![
expected_cwd.join(".agents").to_path_buf(),
expected_cwd.join(".codex").to_path_buf(),
expected_cwd.join(".git").to_path_buf()
],
),
(
expected_extra.to_path_buf(),
vec![
expected_extra.join(".agents").to_path_buf(),
expected_extra.join(".codex").to_path_buf(),
expected_extra.join(".git").to_path_buf()
],
),
]
);
}