diff --git a/codex-rs/README.md b/codex-rs/README.md index 18bffd9f64..3d290b08f1 100644 --- a/codex-rs/README.md +++ b/codex-rs/README.md @@ -86,6 +86,12 @@ codex --sandbox danger-full-access ``` In `workspace-write`, Codex also includes `~/.codex/memories` in its writable roots so memory maintenance does not require an extra approval. +Set the following to allow Git metadata writes under writable roots while keeping Git config and hooks read-only: + +```toml +[sandbox_workspace_write] +allow_limited_git_writes = true +``` ## Code Organization diff --git a/codex-rs/app-server-protocol/schema/json/ClientRequest.json b/codex-rs/app-server-protocol/schema/json/ClientRequest.json index dc5dd6ad60..01fe794591 100644 --- a/codex-rs/app-server-protocol/schema/json/ClientRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ClientRequest.json @@ -2864,6 +2864,10 @@ }, { "properties": { + "allowLimitedGitWrites": { + "default": false, + "type": "boolean" + }, "excludeSlashTmp": { "default": false, "type": "boolean" diff --git a/codex-rs/app-server-protocol/schema/json/ServerNotification.json b/codex-rs/app-server-protocol/schema/json/ServerNotification.json index 4eb5cae387..ed5fe6d359 100644 --- a/codex-rs/app-server-protocol/schema/json/ServerNotification.json +++ b/codex-rs/app-server-protocol/schema/json/ServerNotification.json @@ -3013,6 +3013,10 @@ }, { "properties": { + "allowLimitedGitWrites": { + "default": false, + "type": "boolean" + }, "excludeSlashTmp": { "default": false, "type": "boolean" diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index b97c1f8fb7..02b52ce7db 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json @@ -14716,6 +14716,10 @@ }, { "properties": { + "allowLimitedGitWrites": { + "default": false, + "type": "boolean" + }, "excludeSlashTmp": { "default": false, "type": "boolean" @@ -14753,6 +14757,10 @@ }, "SandboxWorkspaceWrite": { "properties": { + "allow_limited_git_writes": { + "default": false, + "type": "boolean" + }, "exclude_slash_tmp": { "default": false, "type": "boolean" diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json index f8261c86d6..5d794cfb4d 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json @@ -11245,6 +11245,10 @@ }, { "properties": { + "allowLimitedGitWrites": { + "default": false, + "type": "boolean" + }, "excludeSlashTmp": { "default": false, "type": "boolean" @@ -11282,6 +11286,10 @@ }, "SandboxWorkspaceWrite": { "properties": { + "allow_limited_git_writes": { + "default": false, + "type": "boolean" + }, "exclude_slash_tmp": { "default": false, "type": "boolean" diff --git a/codex-rs/app-server-protocol/schema/json/v2/CommandExecParams.json b/codex-rs/app-server-protocol/schema/json/v2/CommandExecParams.json index d00a0b60e5..619127fa56 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/CommandExecParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/CommandExecParams.json @@ -98,6 +98,10 @@ }, { "properties": { + "allowLimitedGitWrites": { + "default": false, + "type": "boolean" + }, "excludeSlashTmp": { "default": false, "type": "boolean" diff --git a/codex-rs/app-server-protocol/schema/json/v2/ConfigReadResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ConfigReadResponse.json index e714c0975d..5156c23d0e 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ConfigReadResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ConfigReadResponse.json @@ -698,6 +698,10 @@ }, "SandboxWorkspaceWrite": { "properties": { + "allow_limited_git_writes": { + "default": false, + "type": "boolean" + }, "exclude_slash_tmp": { "default": false, "type": "boolean" diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json index 0d3a3fe8db..8c8274ffa3 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json @@ -793,6 +793,10 @@ }, { "properties": { + "allowLimitedGitWrites": { + "default": false, + "type": "boolean" + }, "excludeSlashTmp": { "default": false, "type": "boolean" diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json index 3a9e51f2a7..c763454d33 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json @@ -793,6 +793,10 @@ }, { "properties": { + "allowLimitedGitWrites": { + "default": false, + "type": "boolean" + }, "excludeSlashTmp": { "default": false, "type": "boolean" diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadSettingsUpdatedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadSettingsUpdatedNotification.json index 42a76ae364..d1b3e7b96e 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadSettingsUpdatedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadSettingsUpdatedNotification.json @@ -219,6 +219,10 @@ }, { "properties": { + "allowLimitedGitWrites": { + "default": false, + "type": "boolean" + }, "excludeSlashTmp": { "default": false, "type": "boolean" diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json index 8640c4e9ed..470a21f1fb 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json @@ -793,6 +793,10 @@ }, { "properties": { + "allowLimitedGitWrites": { + "default": false, + "type": "boolean" + }, "excludeSlashTmp": { "default": false, "type": "boolean" diff --git a/codex-rs/app-server-protocol/schema/json/v2/TurnStartParams.json b/codex-rs/app-server-protocol/schema/json/v2/TurnStartParams.json index f997976aca..e28ccb2ead 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/TurnStartParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/TurnStartParams.json @@ -249,6 +249,10 @@ }, { "properties": { + "allowLimitedGitWrites": { + "default": false, + "type": "boolean" + }, "excludeSlashTmp": { "default": false, "type": "boolean" diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/SandboxPolicy.ts b/codex-rs/app-server-protocol/schema/typescript/v2/SandboxPolicy.ts index 5575701ff2..8bcfac6baa 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/SandboxPolicy.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/SandboxPolicy.ts @@ -4,4 +4,4 @@ import type { AbsolutePathBuf } from "../AbsolutePathBuf"; import type { NetworkAccess } from "./NetworkAccess"; -export type SandboxPolicy = { "type": "dangerFullAccess" } | { "type": "readOnly", networkAccess: boolean, } | { "type": "externalSandbox", networkAccess: NetworkAccess, } | { "type": "workspaceWrite", writableRoots: Array, networkAccess: boolean, excludeTmpdirEnvVar: boolean, excludeSlashTmp: boolean, }; +export type SandboxPolicy = { "type": "dangerFullAccess" } | { "type": "readOnly", networkAccess: boolean, } | { "type": "externalSandbox", networkAccess: NetworkAccess, } | { "type": "workspaceWrite", writableRoots: Array, networkAccess: boolean, allowLimitedGitWrites: boolean, excludeTmpdirEnvVar: boolean, excludeSlashTmp: boolean, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/SandboxWorkspaceWrite.ts b/codex-rs/app-server-protocol/schema/typescript/v2/SandboxWorkspaceWrite.ts index cd19d83f1f..0f1a6354ad 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/SandboxWorkspaceWrite.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/SandboxWorkspaceWrite.ts @@ -2,4 +2,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type SandboxWorkspaceWrite = { writable_roots: Array, network_access: boolean, exclude_tmpdir_env_var: boolean, exclude_slash_tmp: boolean, }; +export type SandboxWorkspaceWrite = { writable_roots: Array, network_access: boolean, allow_limited_git_writes: boolean, exclude_tmpdir_env_var: boolean, exclude_slash_tmp: boolean, }; diff --git a/codex-rs/app-server-protocol/src/protocol/v1.rs b/codex-rs/app-server-protocol/src/protocol/v1.rs index f83674d4c3..d0866cf2f7 100644 --- a/codex-rs/app-server-protocol/src/protocol/v1.rs +++ b/codex-rs/app-server-protocol/src/protocol/v1.rs @@ -223,6 +223,7 @@ pub struct SandboxSettings { #[serde(default)] pub writable_roots: Vec, pub network_access: Option, + pub allow_limited_git_writes: Option, pub exclude_tmpdir_env_var: Option, pub exclude_slash_tmp: Option, } diff --git a/codex-rs/app-server-protocol/src/protocol/v2/config.rs b/codex-rs/app-server-protocol/src/protocol/v2/config.rs index 473bb1e03c..29fb35ec03 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/config.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/config.rs @@ -136,6 +136,8 @@ pub struct SandboxWorkspaceWrite { #[serde(default)] pub network_access: bool, #[serde(default)] + pub allow_limited_git_writes: bool, + #[serde(default)] pub exclude_tmpdir_env_var: bool, #[serde(default)] pub exclude_slash_tmp: bool, diff --git a/codex-rs/app-server-protocol/src/protocol/v2/permissions.rs b/codex-rs/app-server-protocol/src/protocol/v2/permissions.rs index dfc1942276..3a84ac2356 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/permissions.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/permissions.rs @@ -449,6 +449,8 @@ pub enum SandboxPolicy { #[serde(default)] network_access: bool, #[serde(default)] + allow_limited_git_writes: bool, + #[serde(default)] exclude_tmpdir_env_var: bool, #[serde(default)] exclude_slash_tmp: bool, @@ -480,6 +482,8 @@ enum SandboxPolicyDeserialize { #[serde(default)] network_access: bool, #[serde(default)] + allow_limited_git_writes: bool, + #[serde(default)] exclude_tmpdir_env_var: bool, #[serde(default)] exclude_slash_tmp: bool, @@ -518,6 +522,7 @@ impl<'de> Deserialize<'de> for SandboxPolicy { writable_roots, read_only_access, network_access, + allow_limited_git_writes, exclude_tmpdir_env_var, exclude_slash_tmp, } => { @@ -529,6 +534,7 @@ impl<'de> Deserialize<'de> for SandboxPolicy { Ok(SandboxPolicy::WorkspaceWrite { writable_roots, network_access, + allow_limited_git_writes, exclude_tmpdir_env_var, exclude_slash_tmp, }) @@ -559,11 +565,13 @@ impl SandboxPolicy { SandboxPolicy::WorkspaceWrite { writable_roots, network_access, + allow_limited_git_writes, exclude_tmpdir_env_var, exclude_slash_tmp, } => codex_protocol::protocol::SandboxPolicy::WorkspaceWrite { writable_roots: writable_roots.clone(), network_access: *network_access, + allow_limited_git_writes: *allow_limited_git_writes, exclude_tmpdir_env_var: *exclude_tmpdir_env_var, exclude_slash_tmp: *exclude_slash_tmp, }, @@ -591,11 +599,13 @@ impl From for SandboxPolicy { codex_protocol::protocol::SandboxPolicy::WorkspaceWrite { writable_roots, network_access, + allow_limited_git_writes, exclude_tmpdir_env_var, exclude_slash_tmp, } => SandboxPolicy::WorkspaceWrite { writable_roots, network_access, + allow_limited_git_writes, exclude_tmpdir_env_var, exclude_slash_tmp, }, diff --git a/codex-rs/app-server-protocol/src/protocol/v2/tests.rs b/codex-rs/app-server-protocol/src/protocol/v2/tests.rs index 1e7558363d..dc41d61cb0 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/tests.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/tests.rs @@ -2086,6 +2086,7 @@ fn sandbox_policy_round_trips_workspace_write_access() { let v2_policy = SandboxPolicy::WorkspaceWrite { writable_roots: vec![], network_access: true, + allow_limited_git_writes: true, exclude_tmpdir_env_var: false, exclude_slash_tmp: false, }; @@ -2096,6 +2097,7 @@ fn sandbox_policy_round_trips_workspace_write_access() { codex_protocol::protocol::SandboxPolicy::WorkspaceWrite { writable_roots: vec![], network_access: true, + allow_limited_git_writes: true, exclude_tmpdir_env_var: false, exclude_slash_tmp: false, } @@ -2142,6 +2144,7 @@ fn sandbox_policy_deserializes_legacy_workspace_write_full_access_field() { SandboxPolicy::WorkspaceWrite { writable_roots: vec![absolute_path("/workspace")], network_access: true, + allow_limited_git_writes: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, } diff --git a/codex-rs/app-server/tests/suite/v2/turn_start.rs b/codex-rs/app-server/tests/suite/v2/turn_start.rs index 2ae4561d86..ae1b02d553 100644 --- a/codex-rs/app-server/tests/suite/v2/turn_start.rs +++ b/codex-rs/app-server/tests/suite/v2/turn_start.rs @@ -2183,6 +2183,7 @@ async fn turn_start_updates_sandbox_and_cwd_between_turns_v2() -> Result<()> { sandbox_policy: Some(codex_app_server_protocol::SandboxPolicy::WorkspaceWrite { writable_roots: vec![first_cwd.try_into()?], network_access: false, + allow_limited_git_writes: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, }), diff --git a/codex-rs/config/src/config_toml.rs b/codex-rs/config/src/config_toml.rs index 0765819fb7..21f5bd9e6b 100644 --- a/codex-rs/config/src/config_toml.rs +++ b/codex-rs/config/src/config_toml.rs @@ -772,6 +772,7 @@ impl ConfigToml { Some(SandboxWorkspaceWrite { writable_roots, network_access, + allow_limited_git_writes, exclude_tmpdir_env_var, exclude_slash_tmp, }) => { @@ -780,11 +781,12 @@ impl ConfigToml { } else { NetworkSandboxPolicy::Restricted }; - PermissionProfile::workspace_write_with( + PermissionProfile::workspace_write_with_limited_git_writes( writable_roots, network_policy, *exclude_tmpdir_env_var, *exclude_slash_tmp, + *allow_limited_git_writes, ) } None => PermissionProfile::workspace_write(), diff --git a/codex-rs/config/src/types.rs b/codex-rs/config/src/types.rs index 598b7fdee7..9e6a867475 100644 --- a/codex-rs/config/src/types.rs +++ b/codex-rs/config/src/types.rs @@ -883,6 +883,8 @@ pub struct SandboxWorkspaceWrite { #[serde(default)] pub network_access: bool, #[serde(default)] + pub allow_limited_git_writes: bool, + #[serde(default)] pub exclude_tmpdir_env_var: bool, #[serde(default)] pub exclude_slash_tmp: bool, @@ -893,6 +895,7 @@ impl From for codex_app_server_protocol::SandboxSettings Self { writable_roots: sandbox_workspace_write.writable_roots, network_access: Some(sandbox_workspace_write.network_access), + allow_limited_git_writes: Some(sandbox_workspace_write.allow_limited_git_writes), exclude_tmpdir_env_var: Some(sandbox_workspace_write.exclude_tmpdir_env_var), exclude_slash_tmp: Some(sandbox_workspace_write.exclude_slash_tmp), } diff --git a/codex-rs/core/README.md b/codex-rs/core/README.md index 57b9e53f6b..16c78e6005 100644 --- a/codex-rs/core/README.md +++ b/codex-rs/core/README.md @@ -12,7 +12,10 @@ Expects `/usr/bin/sandbox-exec` to be present. When using the workspace-write sandbox policy, the Seatbelt profile allows writes under the configured writable roots while keeping `.git` (directory or -pointer file), the resolved `gitdir:` target, and `.codex` read-only. +pointer file), the resolved `gitdir:` target, and `.codex` read-only by +default. When `allow_limited_git_writes` is enabled, Git metadata writes are allowed, +but `.git/config`, `.git/hooks`, and resolved worktree gitdir config/hooks +remain read-only. Network access and filesystem read/write roots are controlled by `SandboxPolicy`. Seatbelt consumes the resolved policy and enforces it. diff --git a/codex-rs/core/config.schema.json b/codex-rs/core/config.schema.json index 6dd85dffc2..1ee26b80a0 100644 --- a/codex-rs/core/config.schema.json +++ b/codex-rs/core/config.schema.json @@ -2514,6 +2514,10 @@ "SandboxWorkspaceWrite": { "additionalProperties": false, "properties": { + "allow_limited_git_writes": { + "default": false, + "type": "boolean" + }, "exclude_slash_tmp": { "default": false, "type": "boolean" diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs index 70719843ba..7401b28669 100644 --- a/codex-rs/core/src/config/config_tests.rs +++ b/codex-rs/core/src/config/config_tests.rs @@ -795,6 +795,46 @@ async fn runtime_config_uses_tui_raw_output_mode() { assert!(cfg.tui_raw_output_mode); } +#[tokio::test] +async fn sandbox_workspace_write_allow_limited_git_writes_keeps_config_and_hooks_read_only() +-> std::io::Result<()> { + let codex_home = TempDir::new()?; + let cwd = TempDir::new()?; + let git_dir = cwd.path().join(".git"); + let git_config = git_dir.join("config"); + let git_hooks = git_dir.join("hooks"); + std::fs::create_dir_all(&git_hooks)?; + std::fs::write(&git_config, "[core]\n")?; + + let config = Config::load_from_base_config_with_overrides( + ConfigToml { + sandbox_mode: Some(SandboxMode::WorkspaceWrite), + sandbox_workspace_write: Some(SandboxWorkspaceWrite { + writable_roots: Vec::new(), + network_access: false, + allow_limited_git_writes: true, + exclude_tmpdir_env_var: true, + exclude_slash_tmp: true, + }), + ..Default::default() + }, + ConfigOverrides { + cwd: Some(cwd.path().to_path_buf()), + ..Default::default() + }, + codex_home.abs(), + ) + .await?; + let file_system = config.permissions.file_system_sandbox_policy(); + + assert!(file_system.can_write_path_with_cwd(git_dir.join("index").as_path(), cwd.path())); + assert!(!file_system.can_write_path_with_cwd(git_config.as_path(), cwd.path())); + assert!( + !file_system.can_write_path_with_cwd(git_hooks.join("pre-commit").as_path(), cwd.path()) + ); + Ok(()) +} + #[test] fn config_toml_deserializes_permission_profiles() { let toml = r#" @@ -1654,6 +1694,7 @@ async fn default_permissions_profile_populates_runtime_sandbox_policy() -> std:: &SandboxPolicy::WorkspaceWrite { writable_roots: vec![], network_access: false, + allow_limited_git_writes: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, } @@ -1926,6 +1967,7 @@ async fn permission_profile_override_keeps_memories_root_out_of_legacy_projectio &SandboxPolicy::WorkspaceWrite { writable_roots: vec![], network_access: false, + allow_limited_git_writes: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, } @@ -2312,6 +2354,7 @@ async fn explicit_builtin_workspace_profile_ignores_legacy_workspace_write_setti sandbox_workspace_write: Some(SandboxWorkspaceWrite { writable_roots: vec![extra_root.path().abs()], network_access: true, + allow_limited_git_writes: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, }), @@ -2528,6 +2571,7 @@ async fn implicit_builtin_workspace_profile_preserves_sandbox_workspace_write_se sandbox_workspace_write: Some(SandboxWorkspaceWrite { writable_roots: vec![extra_root.clone()], network_access: true, + allow_limited_git_writes: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: false, }), @@ -2564,6 +2608,7 @@ async fn implicit_builtin_workspace_profile_preserves_sandbox_workspace_write_se SandboxPolicy::WorkspaceWrite { writable_roots, network_access, + allow_limited_git_writes: _, exclude_tmpdir_env_var, exclude_slash_tmp, } => { @@ -2833,6 +2878,7 @@ async fn permissions_profiles_allow_direct_write_roots_outside_workspace_root() &SandboxPolicy::WorkspaceWrite { writable_roots: vec![external_write_path], network_access: false, + allow_limited_git_writes: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, } @@ -3554,6 +3600,7 @@ trust_level = "trusted" SandboxPolicy::WorkspaceWrite { writable_roots: vec![writable_root.clone()], network_access: false, + allow_limited_git_writes: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, } @@ -3594,6 +3641,7 @@ exclude_slash_tmp = true SandboxPolicy::WorkspaceWrite { writable_roots: vec![writable_root], network_access: false, + allow_limited_git_writes: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, } diff --git a/codex-rs/core/src/config/permissions.rs b/codex-rs/core/src/config/permissions.rs index a8546858f1..b7292ce3fb 100644 --- a/codex-rs/core/src/config/permissions.rs +++ b/codex-rs/core/src/config/permissions.rs @@ -77,9 +77,10 @@ pub(crate) fn builtin_permission_profile( Some(SandboxWorkspaceWrite { writable_roots: _, network_access, + allow_limited_git_writes, exclude_tmpdir_env_var, exclude_slash_tmp, - }) => PermissionProfile::workspace_write_with( + }) => PermissionProfile::workspace_write_with_limited_git_writes( &[], if *network_access { NetworkSandboxPolicy::Enabled @@ -88,6 +89,7 @@ pub(crate) fn builtin_permission_profile( }, *exclude_tmpdir_env_var, *exclude_slash_tmp, + *allow_limited_git_writes, ), None => PermissionProfile::workspace_write(), }), diff --git a/codex-rs/core/src/exec_tests.rs b/codex-rs/core/src/exec_tests.rs index 1c71d02651..24211b3c74 100644 --- a/codex-rs/core/src/exec_tests.rs +++ b/codex-rs/core/src/exec_tests.rs @@ -472,6 +472,7 @@ fn windows_restricted_token_allows_legacy_workspace_write_policies() { let policy = SandboxPolicy::WorkspaceWrite { writable_roots: vec![], network_access: false, + allow_limited_git_writes: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, }; @@ -530,6 +531,7 @@ fn windows_restricted_token_rejects_split_only_filesystem_policies() { let policy = SandboxPolicy::WorkspaceWrite { writable_roots: vec![], network_access: false, + allow_limited_git_writes: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, }; @@ -575,6 +577,7 @@ fn windows_restricted_token_rejects_root_write_read_only_carveouts() { let policy = SandboxPolicy::WorkspaceWrite { writable_roots: vec![], network_access: false, + allow_limited_git_writes: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, }; @@ -621,6 +624,7 @@ fn windows_restricted_token_supports_full_read_split_write_read_carveouts() { let policy = SandboxPolicy::WorkspaceWrite { writable_roots: vec![], network_access: false, + allow_limited_git_writes: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, }; @@ -679,6 +683,7 @@ fn windows_restricted_token_rejects_unreadable_split_carveouts() { let policy = SandboxPolicy::WorkspaceWrite { writable_roots: vec![], network_access: false, + allow_limited_git_writes: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, }; @@ -766,6 +771,7 @@ fn windows_elevated_supports_split_write_read_carveouts() { let policy = SandboxPolicy::WorkspaceWrite { writable_roots: vec![], network_access: false, + allow_limited_git_writes: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, }; @@ -824,6 +830,7 @@ fn windows_elevated_supports_unreadable_split_carveouts() { let policy = SandboxPolicy::WorkspaceWrite { writable_roots: vec![], network_access: false, + allow_limited_git_writes: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, }; @@ -887,6 +894,7 @@ fn windows_elevated_supports_unreadable_globs() { let policy = SandboxPolicy::WorkspaceWrite { writable_roots: vec![], network_access: false, + allow_limited_git_writes: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, }; @@ -944,6 +952,7 @@ fn windows_elevated_rejects_reopened_writable_descendants() { let policy = SandboxPolicy::WorkspaceWrite { writable_roots: vec![], network_access: false, + allow_limited_git_writes: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, }; diff --git a/codex-rs/core/src/session/tests.rs b/codex-rs/core/src/session/tests.rs index 953f830f4c..d538400cc3 100644 --- a/codex-rs/core/src/session/tests.rs +++ b/codex-rs/core/src/session/tests.rs @@ -3770,6 +3770,7 @@ async fn session_configuration_apply_preserves_profile_file_system_policy_on_cwd let sandbox_policy = SandboxPolicy::WorkspaceWrite { writable_roots: Vec::new(), network_access: false, + allow_limited_git_writes: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, }; @@ -3906,6 +3907,7 @@ async fn session_configuration_apply_permission_profile_accepts_direct_write_roo SandboxPolicy::WorkspaceWrite { writable_roots: vec![external_write_path], network_access: false, + allow_limited_git_writes: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, } @@ -4204,6 +4206,7 @@ async fn session_configuration_apply_retargets_legacy_workspace_root_on_cwd_upda let sandbox_policy = SandboxPolicy::WorkspaceWrite { writable_roots: Vec::new(), network_access: false, + allow_limited_git_writes: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, }; diff --git a/codex-rs/core/tests/suite/approvals.rs b/codex-rs/core/tests/suite/approvals.rs index a91196a0b9..6cc6737e36 100644 --- a/codex-rs/core/tests/suite/approvals.rs +++ b/codex-rs/core/tests/suite/approvals.rs @@ -844,6 +844,7 @@ fn scenarios() -> Vec { let workspace_write = |network_access| SandboxPolicy::WorkspaceWrite { writable_roots: vec![], network_access, + allow_limited_git_writes: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, }; @@ -2064,6 +2065,7 @@ async fn approving_apply_patch_for_session_skips_future_prompts_for_same_file() let sandbox_policy = SandboxPolicy::WorkspaceWrite { writable_roots: vec![], network_access: false, + allow_limited_git_writes: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, }; @@ -2993,6 +2995,7 @@ allow_local_binding = true let sandbox_policy = SandboxPolicy::WorkspaceWrite { writable_roots: vec![], network_access: true, + allow_limited_git_writes: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, }; @@ -3274,6 +3277,7 @@ allow_local_binding = true let sandbox_policy = SandboxPolicy::WorkspaceWrite { writable_roots: vec![], network_access: true, + allow_limited_git_writes: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, }; @@ -3479,6 +3483,7 @@ allow_local_binding = true let turn_sandbox_policy = SandboxPolicy::WorkspaceWrite { writable_roots: vec![], network_access: true, + allow_limited_git_writes: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, }; diff --git a/codex-rs/core/tests/suite/guardian_review.rs b/codex-rs/core/tests/suite/guardian_review.rs index df38fb4353..4c27e677cc 100644 --- a/codex-rs/core/tests/suite/guardian_review.rs +++ b/codex-rs/core/tests/suite/guardian_review.rs @@ -39,6 +39,7 @@ async fn guardian_review_session_does_not_inherit_legacy_notify() -> Result<()> let sandbox_policy = SandboxPolicy::WorkspaceWrite { writable_roots: vec![], network_access: false, + allow_limited_git_writes: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, }; diff --git a/codex-rs/exec/src/lib_tests.rs b/codex-rs/exec/src/lib_tests.rs index d9bebb1b32..8d8e78d2bd 100644 --- a/codex-rs/exec/src/lib_tests.rs +++ b/codex-rs/exec/src/lib_tests.rs @@ -727,6 +727,7 @@ fn sample_thread_start_response() -> ThreadStartResponse { sandbox: codex_app_server_protocol::SandboxPolicy::WorkspaceWrite { writable_roots: vec![], network_access: false, + allow_limited_git_writes: false, exclude_tmpdir_env_var: false, exclude_slash_tmp: false, }, diff --git a/codex-rs/memories/write/src/phase2.rs b/codex-rs/memories/write/src/phase2.rs index c78032d9c2..18816242f4 100644 --- a/codex-rs/memories/write/src/phase2.rs +++ b/codex-rs/memories/write/src/phase2.rs @@ -326,6 +326,7 @@ mod agent { let consolidation_sandbox_policy = SandboxPolicy::WorkspaceWrite { writable_roots, network_access: false, + allow_limited_git_writes: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, }; diff --git a/codex-rs/protocol/src/models.rs b/codex-rs/protocol/src/models.rs index cbf8895ddb..8ffa950fa7 100644 --- a/codex-rs/protocol/src/models.rs +++ b/codex-rs/protocol/src/models.rs @@ -413,10 +413,27 @@ impl PermissionProfile { exclude_tmpdir_env_var: bool, exclude_slash_tmp: bool, ) -> Self { - let file_system = FileSystemSandboxPolicy::workspace_write( + Self::workspace_write_with_limited_git_writes( + writable_roots, + network, + exclude_tmpdir_env_var, + exclude_slash_tmp, + /*allow_limited_git_writes*/ false, + ) + } + + pub fn workspace_write_with_limited_git_writes( + writable_roots: &[AbsolutePathBuf], + network: NetworkSandboxPolicy, + exclude_tmpdir_env_var: bool, + exclude_slash_tmp: bool, + allow_limited_git_writes: bool, + ) -> Self { + let file_system = FileSystemSandboxPolicy::workspace_write_with_limited_git_writes( writable_roots, exclude_tmpdir_env_var, exclude_slash_tmp, + allow_limited_git_writes, ); Self::Managed { file_system: ManagedFileSystemPermissions::from_sandbox_policy(&file_system), diff --git a/codex-rs/protocol/src/permissions.rs b/codex-rs/protocol/src/permissions.rs index f90859a92b..ded838f84d 100644 --- a/codex-rs/protocol/src/permissions.rs +++ b/codex-rs/protocol/src/permissions.rs @@ -520,6 +520,20 @@ impl FileSystemSandboxPolicy { writable_roots: &[AbsolutePathBuf], exclude_tmpdir_env_var: bool, exclude_slash_tmp: bool, + ) -> Self { + Self::workspace_write_with_limited_git_writes( + writable_roots, + exclude_tmpdir_env_var, + exclude_slash_tmp, + /*allow_limited_git_writes*/ false, + ) + } + + pub fn workspace_write_with_limited_git_writes( + writable_roots: &[AbsolutePathBuf], + exclude_tmpdir_env_var: bool, + exclude_slash_tmp: bool, + allow_limited_git_writes: bool, ) -> Self { let mut entries = vec![FileSystemSandboxEntry { path: FileSystemPath::Special { @@ -560,13 +574,39 @@ impl FileSystemSandboxPolicy { }), ); - append_default_read_only_project_root_subpath_if_no_explicit_rule(&mut entries, ".git"); + if allow_limited_git_writes { + entries.push(FileSystemSandboxEntry { + path: FileSystemPath::Special { + value: FileSystemSpecialPath::project_roots(Some(".git".into())), + }, + access: FileSystemAccessMode::Write, + }); + append_default_read_only_project_root_subpath_if_no_explicit_rule( + &mut entries, + ".git/config", + ); + append_default_read_only_project_root_subpath_if_no_explicit_rule( + &mut entries, + ".git/hooks", + ); + } else { + append_default_read_only_project_root_subpath_if_no_explicit_rule(&mut entries, ".git"); + } append_default_read_only_project_root_subpath_if_no_explicit_rule(&mut entries, ".agents"); append_default_read_only_project_root_subpath_if_no_explicit_rule(&mut entries, ".codex"); for writable_root in writable_roots { + if allow_limited_git_writes { + entries.push(FileSystemSandboxEntry { + path: FileSystemPath::Path { + path: writable_root.join(".git"), + }, + access: FileSystemAccessMode::Write, + }); + } for protected_path in default_read_only_subpaths_for_writable_root( writable_root, /*protect_missing_dot_codex*/ false, + allow_limited_git_writes, ) { append_default_read_only_path_if_no_explicit_rule(&mut entries, protected_path); } @@ -584,10 +624,17 @@ impl FileSystemSandboxPolicy { /// into split filesystem policy. pub fn from_legacy_sandbox_policy_for_cwd(sandbox_policy: &SandboxPolicy, cwd: &Path) -> Self { let mut file_system_policy = Self::from(sandbox_policy); - if let SandboxPolicy::WorkspaceWrite { writable_roots, .. } = sandbox_policy { + if let SandboxPolicy::WorkspaceWrite { + writable_roots, + allow_limited_git_writes, + .. + } = sandbox_policy + { if let Ok(cwd_root) = AbsolutePathBuf::from_absolute_path(cwd) { for protected_path in default_read_only_subpaths_for_writable_root( - &cwd_root, /*protect_missing_dot_codex*/ true, + &cwd_root, + /*protect_missing_dot_codex*/ true, + *allow_limited_git_writes, ) { append_default_read_only_path_if_no_explicit_rule( &mut file_system_policy.entries, @@ -599,6 +646,7 @@ impl FileSystemSandboxPolicy { for protected_path in default_read_only_subpaths_for_writable_root( writable_root, /*protect_missing_dot_codex*/ false, + *allow_limited_git_writes, ) { append_default_read_only_path_if_no_explicit_rule( &mut file_system_policy.entries, @@ -879,6 +927,7 @@ impl FileSystemSandboxPolicy { for protected_path in default_read_only_subpaths_for_writable_root( path, /*protect_missing_dot_codex*/ false, + /*allow_limited_git_writes*/ false, ) { append_default_read_only_path_if_no_explicit_rule( &mut self.entries, @@ -973,11 +1022,23 @@ impl FileSystemSandboxPolicy { let protect_missing_dot_codex = AbsolutePathBuf::from_absolute_path(cwd) .ok() .is_some_and(|cwd| normalize_effective_absolute_path(cwd) == root); + let allow_limited_git_writes = std::iter::once(&root) + .chain(raw_writable_roots.iter().copied()) + .any(|raw_root| { + let dot_git = raw_root.join(PROTECTED_METADATA_GIT_PATH_NAME); + resolved_entries + .iter() + .any(|entry| entry.access.can_write() && entry.path == dot_git) + }); let mut read_only_subpaths: Vec = - default_read_only_subpaths_for_writable_root(&root, protect_missing_dot_codex) - .into_iter() - .filter(|path| !has_explicit_resolved_path_entry(&resolved_entries, path)) - .collect(); + default_read_only_subpaths_for_writable_root( + &root, + protect_missing_dot_codex, + allow_limited_git_writes, + ) + .into_iter() + .filter(|path| !has_explicit_resolved_path_entry(&resolved_entries, path)) + .collect(); // Narrower explicit non-write entries carve out broader writable roots. // More specific write entries still remain writable because they appear // as separate WritableRoot values and are checked independently. @@ -1193,6 +1254,7 @@ impl FileSystemSandboxPolicy { /*normalize_effective_paths*/ false, ), network_access: network_policy.is_enabled(), + allow_limited_git_writes: false, exclude_tmpdir_env_var: !tmpdir_writable, exclude_slash_tmp: !slash_tmp_writable, } @@ -1267,13 +1329,15 @@ impl From<&SandboxPolicy> for FileSystemSandboxPolicy { } SandboxPolicy::WorkspaceWrite { writable_roots, + allow_limited_git_writes, exclude_tmpdir_env_var, exclude_slash_tmp, .. - } => FileSystemSandboxPolicy::workspace_write( + } => FileSystemSandboxPolicy::workspace_write_with_limited_git_writes( writable_roots, *exclude_tmpdir_env_var, *exclude_slash_tmp, + *allow_limited_git_writes, ), } } @@ -1542,6 +1606,7 @@ fn normalize_effective_absolute_path(path: AbsolutePathBuf) -> AbsolutePathBuf { pub(crate) fn default_read_only_subpaths_for_writable_root( writable_root: &AbsolutePathBuf, protect_missing_dot_codex: bool, + allow_limited_git_writes: bool, ) -> Vec { let mut subpaths: Vec = Vec::new(); let top_level_git = writable_root.join(PROTECTED_METADATA_GIT_PATH_NAME); @@ -1556,9 +1621,19 @@ pub(crate) fn default_read_only_subpaths_for_writable_root( && is_git_pointer_file(&top_level_git) && let Some(gitdir) = resolve_gitdir_from_file(&top_level_git) { - subpaths.push(gitdir); + if allow_limited_git_writes { + subpaths.extend(protected_git_subpaths_when_writable(&gitdir)); + } else { + subpaths.push(gitdir); + } + } + if allow_limited_git_writes { + if top_level_git_is_dir { + subpaths.extend(protected_git_subpaths_when_writable(&top_level_git)); + } + } else { + subpaths.push(top_level_git); } - subpaths.push(top_level_git); } let top_level_agents = writable_root.join(PROTECTED_METADATA_AGENTS_PATH_NAME); @@ -1591,6 +1666,7 @@ fn legacy_runtime_file_system_policy_for_cwd( ) -> FileSystemSandboxPolicy { let SandboxPolicy::WorkspaceWrite { writable_roots, + allow_limited_git_writes, exclude_tmpdir_env_var, exclude_slash_tmp, .. @@ -1642,7 +1718,9 @@ fn legacy_runtime_file_system_policy_for_cwd( if let Ok(cwd_root) = AbsolutePathBuf::from_absolute_path(cwd) { for protected_path in default_read_only_subpaths_for_writable_root( - &cwd_root, /*protect_missing_dot_codex*/ true, + &cwd_root, + /*protect_missing_dot_codex*/ true, + *allow_limited_git_writes, ) { append_default_read_only_path_if_no_explicit_rule(&mut entries, protected_path); } @@ -1651,6 +1729,7 @@ fn legacy_runtime_file_system_policy_for_cwd( for protected_path in default_read_only_subpaths_for_writable_root( writable_root, /*protect_missing_dot_codex*/ false, + *allow_limited_git_writes, ) { append_default_read_only_path_if_no_explicit_rule(&mut entries, protected_path); } @@ -1659,6 +1738,13 @@ fn legacy_runtime_file_system_policy_for_cwd( FileSystemSandboxPolicy::restricted(entries) } +fn protected_git_subpaths_when_writable(gitdir: &AbsolutePathBuf) -> Vec { + ["config", "hooks"] + .into_iter() + .map(|subpath| gitdir.join(subpath)) + .collect() +} + fn append_default_read_only_project_root_subpath_if_no_explicit_rule( entries: &mut Vec, subpath: impl Into, @@ -1945,6 +2031,7 @@ mod tests { let policy = SandboxPolicy::WorkspaceWrite { writable_roots: Vec::new(), network_access: false, + allow_limited_git_writes: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, }; @@ -1986,6 +2073,64 @@ mod tests { ); } + #[test] + fn workspace_write_with_limited_git_writes_protects_only_config_and_hooks() { + let cwd = TempDir::new().expect("tempdir"); + let git_dir = cwd.path().join(".git"); + let git_config = git_dir.join("config"); + let git_hooks = git_dir.join("hooks"); + std::fs::create_dir_all(&git_hooks).expect("create git hooks"); + std::fs::write(&git_config, "[core]\n").expect("write git config"); + + let policy = FileSystemSandboxPolicy::workspace_write_with_limited_git_writes( + &[], + /*exclude_tmpdir_env_var*/ true, + /*exclude_slash_tmp*/ true, + /*allow_limited_git_writes*/ true, + ); + + assert!(policy.can_write_path_with_cwd(git_dir.join("index").as_path(), cwd.path())); + assert!(!policy.can_write_path_with_cwd(git_config.as_path(), cwd.path())); + assert!( + !policy.can_write_path_with_cwd(git_hooks.join("pre-commit").as_path(), cwd.path()) + ); + + let root = AbsolutePathBuf::from_absolute_path( + cwd.path().canonicalize().expect("canonicalize cwd"), + ) + .expect("absolute cwd"); + let writable_root = policy + .get_writable_roots_with_cwd(cwd.path()) + .into_iter() + .find(|writable_root| writable_root.root == root) + .expect("workspace writable root"); + + assert!( + !writable_root + .read_only_subpaths + .contains(&root.join(".git")), + ".git should remain writable" + ); + assert!( + writable_root + .read_only_subpaths + .contains(&root.join(".git").join("config")), + ".git/config should remain read-only" + ); + assert!( + writable_root + .read_only_subpaths + .contains(&root.join(".git").join("hooks")), + ".git/hooks should remain read-only" + ); + assert!( + !writable_root + .protected_metadata_names + .contains(&".git".to_string()), + ".git should not be protected as an all-or-nothing metadata directory" + ); + } + #[test] fn legacy_current_working_directory_special_path_deserializes_as_project_roots() -> serde_json::Result<()> { @@ -2101,6 +2246,7 @@ mod tests { let policy = SandboxPolicy::WorkspaceWrite { writable_roots: vec![], network_access: false, + allow_limited_git_writes: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, }; @@ -2134,6 +2280,7 @@ mod tests { default_read_only_subpaths_for_writable_root( &expected_root, /*protect_missing_dot_codex*/ true, + /*allow_limited_git_writes*/ false, ) .into_iter() .map(|path| FileSystemSandboxEntry { @@ -2617,6 +2764,7 @@ mod tests { let legacy_policy = SandboxPolicy::WorkspaceWrite { writable_roots: Vec::new(), network_access: false, + allow_limited_git_writes: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, }; @@ -2644,6 +2792,7 @@ mod tests { let legacy_policy = SandboxPolicy::WorkspaceWrite { writable_roots: Vec::new(), network_access: false, + allow_limited_git_writes: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, }; diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index 7c91223f72..c58a2f23a4 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -915,6 +915,11 @@ pub enum SandboxPolicy { #[serde(default)] network_access: bool, + /// When set to `true`, Git metadata under writable roots can be written + /// while Git config and hooks remain read-only. + #[serde(default)] + allow_limited_git_writes: bool, + /// When set to `true`, will NOT include the per-user `TMPDIR` /// environment variable among the default writable roots. Defaults to /// `false`. @@ -1021,6 +1026,7 @@ impl SandboxPolicy { SandboxPolicy::WorkspaceWrite { writable_roots: vec![], network_access: false, + allow_limited_git_writes: false, exclude_tmpdir_env_var: false, exclude_slash_tmp: false, } @@ -1058,6 +1064,7 @@ impl SandboxPolicy { SandboxPolicy::ReadOnly { .. } => Vec::new(), SandboxPolicy::WorkspaceWrite { writable_roots, + allow_limited_git_writes, exclude_tmpdir_env_var, exclude_slash_tmp, network_access: _, @@ -1131,6 +1138,7 @@ impl SandboxPolicy { read_only_subpaths: default_read_only_subpaths_for_writable_root( &writable_root, protect_missing_dot_codex, + *allow_limited_git_writes, ), protected_metadata_names: Vec::new(), root: writable_root, @@ -4518,12 +4526,14 @@ mod tests { SandboxPolicy::WorkspaceWrite { writable_roots: vec![], network_access: false, + allow_limited_git_writes: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, }, SandboxPolicy::WorkspaceWrite { writable_roots: vec![writable_root], network_access: true, + allow_limited_git_writes: true, exclude_tmpdir_env_var: false, exclude_slash_tmp: true, }, diff --git a/codex-rs/sandboxing/src/manager.rs b/codex-rs/sandboxing/src/manager.rs index 82c49f7908..b24ef5dfee 100644 --- a/codex-rs/sandboxing/src/manager.rs +++ b/codex-rs/sandboxing/src/manager.rs @@ -299,6 +299,7 @@ fn compatibility_workspace_write_policy( SandboxPolicy::WorkspaceWrite { writable_roots, network_access: network_policy.is_enabled(), + allow_limited_git_writes: false, exclude_tmpdir_env_var: !tmpdir_writable, exclude_slash_tmp: !slash_tmp_writable, } diff --git a/codex-rs/sandboxing/src/seatbelt_tests.rs b/codex-rs/sandboxing/src/seatbelt_tests.rs index d905d0adcf..8e2390a470 100644 --- a/codex-rs/sandboxing/src/seatbelt_tests.rs +++ b/codex-rs/sandboxing/src/seatbelt_tests.rs @@ -166,6 +166,7 @@ fn dynamic_network_policy_allows_tls_without_darwin_user_cache_write() { &SandboxPolicy::WorkspaceWrite { writable_roots: vec![], network_access: true, + allow_limited_git_writes: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, }, @@ -439,6 +440,7 @@ fn dynamic_network_policy_preserves_restricted_policy_when_proxy_config_without_ &SandboxPolicy::WorkspaceWrite { writable_roots: vec![], network_access: true, + allow_limited_git_writes: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, }, @@ -475,6 +477,7 @@ fn dynamic_network_policy_blocks_dns_when_local_binding_has_no_proxy_ports() { &SandboxPolicy::WorkspaceWrite { writable_roots: vec![], network_access: true, + allow_limited_git_writes: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, }, @@ -503,6 +506,7 @@ fn dynamic_network_policy_preserves_restricted_policy_for_managed_network_withou &SandboxPolicy::WorkspaceWrite { writable_roots: vec![], network_access: true, + allow_limited_git_writes: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, }, @@ -793,6 +797,7 @@ fn create_seatbelt_args_full_network_with_proxy_is_still_proxy_only() { &SandboxPolicy::WorkspaceWrite { writable_roots: vec![], network_access: true, + allow_limited_git_writes: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, }, @@ -844,6 +849,7 @@ fn create_seatbelt_args_with_read_only_git_and_codex_subpaths() { .map(|p| p.try_into().unwrap()) .collect(), network_access: false, + allow_limited_git_writes: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, }; @@ -1087,6 +1093,7 @@ fn create_seatbelt_args_block_first_time_dot_codex_creation_with_metadata_name_r let policy = SandboxPolicy::WorkspaceWrite { writable_roots: vec![repo_root.as_path().try_into().expect("absolute repo root")], network_access: false, + allow_limited_git_writes: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, }; @@ -1140,6 +1147,7 @@ fn create_seatbelt_args_with_read_only_git_pointer_file() { let policy = SandboxPolicy::WorkspaceWrite { writable_roots: vec![worktree_root.try_into().expect("worktree_root is absolute")], network_access: false, + allow_limited_git_writes: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, }; @@ -1236,6 +1244,7 @@ fn create_seatbelt_args_for_cwd_as_git_repo() { let policy = SandboxPolicy::WorkspaceWrite { writable_roots: vec![], network_access: false, + allow_limited_git_writes: false, exclude_tmpdir_env_var: false, exclude_slash_tmp: false, }; diff --git a/codex-rs/thread-store/src/local/read_thread.rs b/codex-rs/thread-store/src/local/read_thread.rs index e960bbd626..72933ae67e 100644 --- a/codex-rs/thread-store/src/local/read_thread.rs +++ b/codex-rs/thread-store/src/local/read_thread.rs @@ -846,6 +846,7 @@ mod tests { let legacy_policy = SandboxPolicy::WorkspaceWrite { writable_roots: Vec::new(), network_access: false, + allow_limited_git_writes: false, exclude_tmpdir_env_var: false, exclude_slash_tmp: false, }; diff --git a/codex-rs/utils/sandbox-summary/src/sandbox_summary.rs b/codex-rs/utils/sandbox-summary/src/sandbox_summary.rs index 47e801bec4..a330f6bbbc 100644 --- a/codex-rs/utils/sandbox-summary/src/sandbox_summary.rs +++ b/codex-rs/utils/sandbox-summary/src/sandbox_summary.rs @@ -133,6 +133,7 @@ mod tests { let summary = summarize_sandbox_policy(&SandboxPolicy::WorkspaceWrite { writable_roots: vec![writable_root.clone()], network_access: true, + allow_limited_git_writes: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, });