mirror of
https://github.com/openai/codex.git
synced 2026-04-28 08:34:54 +00:00
Protect first-time project .codex creation across Linux and macOS sandboxes (#15067)
## Problem Codex already treated an existing top-level project `./.codex` directory as protected, but there was a gap on first creation. If `./.codex` did not exist yet, a turn could create files under it, such as `./.codex/config.toml`, without going through the same approval path as later modifications. That meant the initial write could bypass the intended protection for project-local Codex state. ## What this changes This PR closes that first-creation gap in the Unix enforcement layers: - `codex-protocol` - treat the top-level project `./.codex` path as a protected carveout even when it does not exist yet - avoid injecting the default carveout when the user already has an explicit rule for that exact path - macOS Seatbelt - deny writes to both the exact protected path and anything beneath it, so creating `./.codex` itself is blocked in addition to writes inside it - Linux bubblewrap - preserve the same protected-path behavior for first-time creation under `./.codex` - tests - add protocol regressions for missing `./.codex` and explicit-rule collisions - add Unix sandbox coverage for blocking first-time `./.codex` creation - tighten Seatbelt policy assertions around excluded subpaths ## Scope This change is intentionally scoped to protecting the top-level project `.codex` subtree from agent writes. It does not make `.codex` unreadable, and it does not change the product behavior around loading project skills from `.codex` when project config is untrusted. ## Why this shape The fix is pointed rather than broad: - it preserves the current model of “project `.codex` is protected from writes” - it closes the security-relevant first-write hole - it avoids folding a larger permissions-model redesign into this PR ## Validation - `cargo test -p codex-protocol` - `cargo test -p codex-sandboxing seatbelt` - `cargo test -p codex-exec --test all sandbox_blocks_first_time_dot_codex_creation -- --nocapture` --------- Co-authored-by: Michael Bolin <mbolin@openai.com>
This commit is contained in:
@@ -1064,13 +1064,20 @@ 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| WritableRoot {
|
||||
read_only_subpaths: default_read_only_subpaths_for_writable_root(
|
||||
&writable_root,
|
||||
),
|
||||
root: writable_root,
|
||||
.map(|writable_root| {
|
||||
let protect_missing_dot_codex = 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_dot_codex,
|
||||
),
|
||||
root: writable_root,
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@@ -1080,6 +1087,7 @@ impl SandboxPolicy {
|
||||
|
||||
fn default_read_only_subpaths_for_writable_root(
|
||||
writable_root: &AbsolutePathBuf,
|
||||
protect_missing_dot_codex: bool,
|
||||
) -> Vec<AbsolutePathBuf> {
|
||||
let mut subpaths: Vec<AbsolutePathBuf> = Vec::new();
|
||||
#[allow(clippy::expect_used)]
|
||||
@@ -1101,14 +1109,20 @@ fn default_read_only_subpaths_for_writable_root(
|
||||
subpaths.push(top_level_git);
|
||||
}
|
||||
|
||||
// Make .agents/skills and .codex/config.toml and related files read-only
|
||||
// to the agent, by default.
|
||||
for subdir in &[".agents", ".codex"] {
|
||||
#[allow(clippy::expect_used)]
|
||||
let top_level_codex = writable_root.join(subdir).expect("valid relative path");
|
||||
if top_level_codex.as_path().is_dir() {
|
||||
subpaths.push(top_level_codex);
|
||||
}
|
||||
#[allow(clippy::expect_used)]
|
||||
let top_level_agents = writable_root.join(".agents").expect("valid relative path");
|
||||
if top_level_agents.as_path().is_dir() {
|
||||
subpaths.push(top_level_agents);
|
||||
}
|
||||
|
||||
// Keep top-level project metadata 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
|
||||
// protected-path approval flow.
|
||||
#[allow(clippy::expect_used)]
|
||||
let top_level_codex = writable_root.join(".codex").expect("valid relative path");
|
||||
if protect_missing_dot_codex || top_level_codex.as_path().is_dir() {
|
||||
subpaths.push(top_level_codex);
|
||||
}
|
||||
|
||||
let mut deduped = Vec::with_capacity(subpaths.len());
|
||||
@@ -4095,6 +4109,8 @@ mod tests {
|
||||
let expected_docs_public =
|
||||
AbsolutePathBuf::from_absolute_path(canonical_cwd.join("docs/public"))
|
||||
.expect("canonical docs/public");
|
||||
let expected_dot_codex = AbsolutePathBuf::from_absolute_path(canonical_cwd.join(".codex"))
|
||||
.expect("canonical .codex");
|
||||
let policy = FileSystemSandboxPolicy::restricted(vec![
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Special {
|
||||
@@ -4116,7 +4132,13 @@ mod tests {
|
||||
assert_eq!(
|
||||
sorted_writable_roots(policy.get_writable_roots_with_cwd(cwd.path())),
|
||||
vec![
|
||||
(canonical_cwd, vec![expected_docs.to_path_buf()]),
|
||||
(
|
||||
canonical_cwd,
|
||||
vec![
|
||||
expected_dot_codex.to_path_buf(),
|
||||
expected_docs.to_path_buf()
|
||||
],
|
||||
),
|
||||
(expected_docs_public.to_path_buf(), Vec::new()),
|
||||
]
|
||||
);
|
||||
@@ -4128,6 +4150,8 @@ mod tests {
|
||||
let docs =
|
||||
AbsolutePathBuf::resolve_path_against_base("docs", cwd.path()).expect("resolve docs");
|
||||
let canonical_cwd = cwd.path().canonicalize().expect("canonicalize cwd");
|
||||
let expected_dot_codex = AbsolutePathBuf::from_absolute_path(canonical_cwd.join(".codex"))
|
||||
.expect("canonical .codex");
|
||||
let policy = SandboxPolicy::WorkspaceWrite {
|
||||
writable_roots: vec![],
|
||||
read_only_access: ReadOnlyAccess::Restricted {
|
||||
@@ -4144,7 +4168,7 @@ mod tests {
|
||||
FileSystemSandboxPolicy::from_legacy_sandbox_policy(&policy, cwd.path())
|
||||
.get_writable_roots_with_cwd(cwd.path())
|
||||
),
|
||||
vec![(canonical_cwd, Vec::new())]
|
||||
vec![(canonical_cwd, vec![expected_dot_codex.to_path_buf()])]
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user