Skip to content

Commit e33205d

Browse files
committed
Reserve missing .git without creating it
1 parent 78835d7 commit e33205d

File tree

4 files changed

+146
-81
lines changed

4 files changed

+146
-81
lines changed

codex-rs/linux-sandbox/src/bwrap.rs

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -515,6 +515,13 @@ fn append_read_only_subpath_args(
515515
if let Some(first_missing_component) = find_first_non_existent_component(subpath)
516516
&& is_within_allowed_write_paths(&first_missing_component, allowed_write_paths)
517517
{
518+
// Keep missing top level `.git` reserved in the logical policy
519+
// without materializing it in the host workspace at sandbox
520+
// startup. Existing `.git` paths are remounted read-only by the
521+
// branch below once they exist.
522+
if first_missing_component.file_name() == Some(std::ffi::OsStr::new(".git")) {
523+
return;
524+
}
518525
args.push("--ro-bind".to_string());
519526
args.push("/dev/null".to_string());
520527
args.push(path_to_string(&first_missing_component));
@@ -1054,6 +1061,10 @@ mod tests {
10541061
Path::new("/"),
10551062
)
10561063
.expect("bwrap fs args");
1064+
assert!(
1065+
!args.args.iter().any(|arg| arg == "/.git"),
1066+
"missing /.git should stay logical only and should not be materialized in bwrap args",
1067+
);
10571068
assert_eq!(
10581069
args.args,
10591070
vec![
@@ -1068,9 +1079,10 @@ mod tests {
10681079
"--bind".to_string(),
10691080
"/".to_string(),
10701081
"/".to_string(),
1071-
// Mask the default protected .codex subpath under that writable
1072-
// root. Because the root is `/` in this test, the carveout path
1073-
// appears as `/.codex`.
1082+
// Keep the missing `/.git` reservation logical only, but mask
1083+
// the default protected `.codex` subpath under that writable
1084+
// root. Because the root is `/` in this test, the carveout
1085+
// path appears as `/.codex`.
10741086
"--ro-bind".to_string(),
10751087
"/dev/null".to_string(),
10761088
"/.codex".to_string(),

codex-rs/protocol/src/permissions.rs

Lines changed: 49 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1113,26 +1113,24 @@ fn default_read_only_subpaths_for_writable_root(
11131113
// (file .git with gitdir pointer), and bare repos when the gitdir is the
11141114
// writable root itself.
11151115
let top_level_git_is_file = top_level_git.as_path().is_file();
1116-
let top_level_git_is_dir = top_level_git.as_path().is_dir();
1117-
if top_level_git_is_dir || top_level_git_is_file {
1118-
if top_level_git_is_file
1119-
&& is_git_pointer_file(&top_level_git)
1120-
&& let Some(gitdir) = resolve_gitdir_from_file(&top_level_git)
1121-
{
1122-
subpaths.push(gitdir);
1123-
}
1124-
subpaths.push(top_level_git);
1116+
if top_level_git_is_file
1117+
&& is_git_pointer_file(&top_level_git)
1118+
&& let Some(gitdir) = resolve_gitdir_from_file(&top_level_git)
1119+
{
1120+
subpaths.push(gitdir);
11251121
}
1122+
subpaths.push(top_level_git);
11261123

11271124
let top_level_agents = writable_root.join(".agents");
11281125
if top_level_agents.as_path().is_dir() {
11291126
subpaths.push(top_level_agents);
11301127
}
11311128

1132-
// Keep top-level project metadata under .codex read-only to the agent by
1133-
// default. For the workspace root itself, protect it even before the
1134-
// directory exists so first-time creation still goes through the
1135-
// protected-path approval flow.
1129+
// Keep top level project metadata under .git and .codex read-only to the
1130+
// agent by default. Existing .git pointer files still protect their
1131+
// resolved gitdir targets when present. For the workspace root itself,
1132+
// protect .codex even before it exists so first time creation still goes
1133+
// through the protected path approval flow.
11361134
let top_level_codex = writable_root.join(".codex");
11371135
if protect_missing_dot_codex || top_level_codex.as_path().is_dir() {
11381136
subpaths.push(top_level_codex);
@@ -1290,12 +1288,13 @@ mod tests {
12901288

12911289
#[cfg(unix)]
12921290
#[test]
1293-
fn writable_roots_proactively_protect_missing_dot_codex() {
1291+
fn writable_roots_reserve_missing_project_metadata_paths() {
12941292
let cwd = TempDir::new().expect("tempdir");
12951293
let expected_root = AbsolutePathBuf::from_absolute_path(
12961294
cwd.path().canonicalize().expect("canonicalize cwd"),
12971295
)
12981296
.expect("absolute canonical root");
1297+
let expected_dot_git = expected_root.join(".git");
12991298
let expected_dot_codex = expected_root.join(".codex");
13001299

13011300
let policy = FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry {
@@ -1308,6 +1307,11 @@ mod tests {
13081307
let writable_roots = policy.get_writable_roots_with_cwd(cwd.path());
13091308
assert_eq!(writable_roots.len(), 1);
13101309
assert_eq!(writable_roots[0].root, expected_root);
1310+
assert!(
1311+
writable_roots[0]
1312+
.read_only_subpaths
1313+
.contains(&expected_dot_git)
1314+
);
13111315
assert!(
13121316
writable_roots[0]
13131317
.read_only_subpaths
@@ -1383,6 +1387,13 @@ mod tests {
13831387
#[test]
13841388
fn legacy_workspace_write_projection_accepts_relative_cwd() {
13851389
let relative_cwd = Path::new("workspace");
1390+
let expected_dot_git = AbsolutePathBuf::from_absolute_path(
1391+
std::env::current_dir()
1392+
.expect("current dir")
1393+
.join(relative_cwd)
1394+
.join(".git"),
1395+
)
1396+
.expect("absolute dot git");
13861397
let expected_dot_codex = AbsolutePathBuf::from_absolute_path(
13871398
std::env::current_dir()
13881399
.expect("current dir")
@@ -1413,6 +1424,12 @@ mod tests {
14131424
},
14141425
access: FileSystemAccessMode::Write,
14151426
},
1427+
FileSystemSandboxEntry {
1428+
path: FileSystemPath::Path {
1429+
path: expected_dot_git,
1430+
},
1431+
access: FileSystemAccessMode::Read,
1432+
},
14161433
FileSystemSandboxEntry {
14171434
path: FileSystemPath::Path {
14181435
path: expected_dot_codex,
@@ -1498,6 +1515,7 @@ mod tests {
14981515
let expected_root =
14991516
AbsolutePathBuf::from_absolute_path(&link_root).expect("absolute symlinked root");
15001517
let expected_blocked = link_blocked.clone();
1518+
let expected_git = expected_root.join(".git");
15011519
let expected_agents = expected_root.join(".agents");
15021520
let expected_codex = expected_root.join(".codex");
15031521

@@ -1542,6 +1560,7 @@ mod tests {
15421560
.read_only_subpaths
15431561
.contains(&expected_agents)
15441562
);
1563+
assert!(writable_roots[0].read_only_subpaths.contains(&expected_git));
15451564
assert!(
15461565
writable_roots[0]
15471566
.read_only_subpaths
@@ -1560,6 +1579,13 @@ mod tests {
15601579
symlink_dir(&decoy, &dot_codex).expect("create .codex symlink");
15611580

15621581
let root = AbsolutePathBuf::from_absolute_path(&root).expect("absolute root");
1582+
let expected_dot_git = AbsolutePathBuf::from_absolute_path(
1583+
root.as_path()
1584+
.canonicalize()
1585+
.expect("canonicalize root")
1586+
.join(".git"),
1587+
)
1588+
.expect("absolute .git path");
15631589
let expected_dot_codex = AbsolutePathBuf::from_absolute_path(
15641590
root.as_path()
15651591
.canonicalize()
@@ -1580,7 +1606,7 @@ mod tests {
15801606
assert_eq!(writable_roots.len(), 1);
15811607
assert_eq!(
15821608
writable_roots[0].read_only_subpaths,
1583-
vec![expected_dot_codex]
1609+
vec![expected_dot_git, expected_dot_codex]
15841610
);
15851611
assert!(
15861612
!writable_roots[0]
@@ -1626,7 +1652,7 @@ mod tests {
16261652
assert_eq!(writable_roots[0].root, expected_root);
16271653
assert_eq!(
16281654
writable_roots[0].read_only_subpaths,
1629-
vec![expected_linked_private]
1655+
vec![expected_root.join(".git"), expected_linked_private]
16301656
);
16311657
assert!(
16321658
!writable_roots[0]
@@ -1673,7 +1699,7 @@ mod tests {
16731699
assert_eq!(writable_roots[0].root, expected_root);
16741700
assert_eq!(
16751701
writable_roots[0].read_only_subpaths,
1676-
vec![expected_linked_private]
1702+
vec![expected_root.join(".git"), expected_linked_private]
16771703
);
16781704
assert!(
16791705
!writable_roots[0]
@@ -1713,7 +1739,10 @@ mod tests {
17131739
let writable_roots = policy.get_writable_roots_with_cwd(cwd.path());
17141740
assert_eq!(writable_roots.len(), 1);
17151741
assert_eq!(writable_roots[0].root, expected_root);
1716-
assert_eq!(writable_roots[0].read_only_subpaths, vec![expected_alias]);
1742+
assert_eq!(
1743+
writable_roots[0].read_only_subpaths,
1744+
vec![expected_root.join(".git"), expected_alias]
1745+
);
17171746
}
17181747

17191748
#[cfg(unix)]
@@ -1751,6 +1780,7 @@ mod tests {
17511780
let expected_root =
17521781
AbsolutePathBuf::from_absolute_path(&link_tmpdir).expect("absolute symlinked tmpdir");
17531782
let expected_blocked = link_blocked.clone();
1783+
let expected_git = expected_root.join(".git");
17541784
let expected_codex = expected_root.join(".codex");
17551785

17561786
unsafe {
@@ -1783,6 +1813,7 @@ mod tests {
17831813
.read_only_subpaths
17841814
.contains(&expected_blocked)
17851815
);
1816+
assert!(writable_roots[0].read_only_subpaths.contains(&expected_git));
17861817
assert!(
17871818
writable_roots[0]
17881819
.read_only_subpaths

codex-rs/protocol/src/protocol.rs

Lines changed: 32 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1287,26 +1287,24 @@ fn default_read_only_subpaths_for_writable_root(
12871287
// (file .git with gitdir pointer), and bare repos when the gitdir is the
12881288
// writable root itself.
12891289
let top_level_git_is_file = top_level_git.as_path().is_file();
1290-
let top_level_git_is_dir = top_level_git.as_path().is_dir();
1291-
if top_level_git_is_dir || top_level_git_is_file {
1292-
if top_level_git_is_file
1293-
&& is_git_pointer_file(&top_level_git)
1294-
&& let Some(gitdir) = resolve_gitdir_from_file(&top_level_git)
1295-
{
1296-
subpaths.push(gitdir);
1297-
}
1298-
subpaths.push(top_level_git);
1290+
if top_level_git_is_file
1291+
&& is_git_pointer_file(&top_level_git)
1292+
&& let Some(gitdir) = resolve_gitdir_from_file(&top_level_git)
1293+
{
1294+
subpaths.push(gitdir);
12991295
}
1296+
subpaths.push(top_level_git);
13001297

13011298
let top_level_agents = writable_root.join(".agents");
13021299
if top_level_agents.as_path().is_dir() {
13031300
subpaths.push(top_level_agents);
13041301
}
13051302

1306-
// Keep top-level project metadata under .codex read-only to the agent by
1307-
// default. For the workspace root itself, protect it even before the
1308-
// directory exists so first-time creation still goes through the
1309-
// protected-path approval flow.
1303+
// Keep top level project metadata under .git and .codex read-only to the
1304+
// agent by default. Existing .git pointer files still protect their
1305+
// resolved gitdir targets when present. For the workspace root itself,
1306+
// protect .codex even before it exists so first time creation still goes
1307+
// through the protected path approval flow.
13101308
let top_level_codex = writable_root.join(".codex");
13111309
if protect_missing_dot_codex || top_level_codex.as_path().is_dir() {
13121310
subpaths.push(top_level_codex);
@@ -4284,6 +4282,8 @@ mod tests {
42844282
let secret = AbsolutePathBuf::resolve_path_against_base("secret", cwd.path());
42854283
let expected_secret = AbsolutePathBuf::from_absolute_path(canonical_cwd.join("secret"))
42864284
.expect("canonical secret");
4285+
let expected_git = AbsolutePathBuf::from_absolute_path(canonical_cwd.join(".git"))
4286+
.expect("canonical .git");
42874287
let expected_agents = AbsolutePathBuf::from_absolute_path(canonical_cwd.join(".agents"))
42884288
.expect("canonical .agents");
42894289
let expected_codex = AbsolutePathBuf::from_absolute_path(canonical_cwd.join(".codex"))
@@ -4328,6 +4328,12 @@ mod tests {
43284328
.iter()
43294329
.any(|path| path.as_path() == expected_secret.as_path())
43304330
);
4331+
assert!(
4332+
writable_roots[0]
4333+
.read_only_subpaths
4334+
.iter()
4335+
.any(|path| path.as_path() == expected_git.as_path())
4336+
);
43314337
assert!(
43324338
writable_roots[0]
43334339
.read_only_subpaths
@@ -4354,8 +4360,12 @@ mod tests {
43544360
let expected_docs_public =
43554361
AbsolutePathBuf::from_absolute_path(canonical_cwd.join("docs/public"))
43564362
.expect("canonical docs/public");
4357-
let expected_dot_codex = AbsolutePathBuf::from_absolute_path(canonical_cwd.join(".codex"))
4358-
.expect("canonical .codex");
4363+
let mut expected_cwd_reserved_paths = vec![
4364+
canonical_cwd.join(".git"),
4365+
canonical_cwd.join(".codex"),
4366+
expected_docs.to_path_buf(),
4367+
];
4368+
expected_cwd_reserved_paths.sort();
43594369
let policy = FileSystemSandboxPolicy::restricted(vec![
43604370
FileSystemSandboxEntry {
43614371
path: FileSystemPath::Special {
@@ -4377,14 +4387,11 @@ mod tests {
43774387
assert_eq!(
43784388
sorted_writable_roots(policy.get_writable_roots_with_cwd(cwd.path())),
43794389
vec![
4390+
(canonical_cwd, expected_cwd_reserved_paths),
43804391
(
4381-
canonical_cwd,
4382-
vec![
4383-
expected_dot_codex.to_path_buf(),
4384-
expected_docs.to_path_buf()
4385-
],
4392+
expected_docs_public.to_path_buf(),
4393+
vec![expected_docs_public.join(".git").to_path_buf()],
43864394
),
4387-
(expected_docs_public.to_path_buf(), Vec::new()),
43884395
]
43894396
);
43904397
}
@@ -4395,8 +4402,9 @@ mod tests {
43954402
let docs = AbsolutePathBuf::resolve_path_against_base("docs", cwd.path());
43964403
let canonical_cwd = codex_utils_absolute_path::canonicalize_preserving_symlinks(cwd.path())
43974404
.expect("canonicalize cwd");
4398-
let expected_dot_codex = AbsolutePathBuf::from_absolute_path(canonical_cwd.join(".codex"))
4399-
.expect("canonical .codex");
4405+
let mut expected_reserved_paths =
4406+
vec![canonical_cwd.join(".git"), canonical_cwd.join(".codex")];
4407+
expected_reserved_paths.sort();
44004408
let policy = SandboxPolicy::WorkspaceWrite {
44014409
writable_roots: vec![],
44024410
read_only_access: ReadOnlyAccess::Restricted {
@@ -4413,7 +4421,7 @@ mod tests {
44134421
FileSystemSandboxPolicy::from_legacy_sandbox_policy(&policy, cwd.path())
44144422
.get_writable_roots_with_cwd(cwd.path())
44154423
),
4416-
vec![(canonical_cwd, vec![expected_dot_codex.to_path_buf()])]
4424+
vec![(canonical_cwd, expected_reserved_paths)]
44174425
);
44184426
}
44194427

0 commit comments

Comments
 (0)