Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions codex-rs/core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,30 @@ 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.

Network access and filesystem read/write roots are controlled by
`SandboxPolicy`. Seatbelt consumes the resolved policy and enforces it.

Seatbelt also supports macOS permission-profile extensions layered on top of
`SandboxPolicy`:

- no extension profile provided:
keeps legacy default preferences read access (`user-preference-read`).
- extension profile provided with no `macos_preferences` grant:
does not add preferences access clauses.
- `macos_preferences = "readonly"`:
enables cfprefs read clauses and `user-preference-read`.
- `macos_preferences = "readwrite"`:
includes readonly clauses plus `user-preference-write` and cfprefs shm write
clauses.
- `macos_automation = true`:
enables broad Apple Events send permissions.
- `macos_automation = ["com.apple.Notes", ...]`:
enables Apple Events send only to listed bundle IDs.
- `macos_accessibility = true`:
enables `com.apple.axserver` mach lookup.
- `macos_calendar = true`:
enables `com.apple.CalendarAgent` mach lookup.

### Linux

Expects the binary containing `codex-core` to run the equivalent of `codex sandbox linux` (legacy alias: `codex debug landlock`) when `arg0` is `codex-linux-sandbox`. See the `codex-arg0` crate for details.
Expand Down
1 change: 1 addition & 0 deletions codex-rs/core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ pub use model_provider_info::create_oss_provider_with_base_url;
mod event_mapping;
pub mod review_format;
pub mod review_prompts;
mod seatbelt_permissions;
mod thread_manager;
pub mod web_search;
pub mod windows_sandbox_read_grants;
Expand Down
125 changes: 122 additions & 3 deletions codex-rs/core/src/seatbelt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ use tokio::process::Child;
use url::Url;

use crate::protocol::SandboxPolicy;
use crate::seatbelt_permissions::MacOsPreferencesPermission;
use crate::seatbelt_permissions::MacOsSeatbeltProfileExtensions;
use crate::seatbelt_permissions::build_seatbelt_extensions;
use crate::spawn::CODEX_SANDBOX_ENV_VAR;
use crate::spawn::SpawnChildRequest;
use crate::spawn::StdioPolicy;
Expand Down Expand Up @@ -179,6 +182,24 @@ pub(crate) fn create_seatbelt_command_args(
sandbox_policy_cwd: &Path,
enforce_managed_network: bool,
network: Option<&NetworkProxy>,
) -> Vec<String> {
create_seatbelt_command_args_with_extensions(
command,
sandbox_policy,
sandbox_policy_cwd,
enforce_managed_network,
network,
None,
)
}

pub(crate) fn create_seatbelt_command_args_with_extensions(
command: Vec<String>,
sandbox_policy: &SandboxPolicy,
sandbox_policy_cwd: &Path,
enforce_managed_network: bool,
network: Option<&NetworkProxy>,
extensions: Option<&MacOsSeatbeltProfileExtensions>,
) -> Vec<String> {
let (file_write_policy, file_write_dir_params) = {
if sandbox_policy.has_full_disk_write_access() {
Expand Down Expand Up @@ -275,15 +296,33 @@ pub(crate) fn create_seatbelt_command_args(

let proxy = proxy_policy_inputs(network);
let network_policy = dynamic_network_policy(sandbox_policy, enforce_managed_network, &proxy);

let full_policy = format!(
"{MACOS_SEATBELT_BASE_POLICY}\n{file_read_policy}\n{file_write_policy}\n{network_policy}"
let seatbelt_extensions = extensions.map_or_else(
|| {
// Backward-compatibility default when no extension profile is provided.
build_seatbelt_extensions(&MacOsSeatbeltProfileExtensions {
macos_preferences: MacOsPreferencesPermission::ReadOnly,
..Default::default()
})
},
build_seatbelt_extensions,
);

let full_policy = if seatbelt_extensions.policy.is_empty() {
format!(
"{MACOS_SEATBELT_BASE_POLICY}\n{file_read_policy}\n{file_write_policy}\n{network_policy}"
)
} else {
format!(
"{MACOS_SEATBELT_BASE_POLICY}\n{file_read_policy}\n{file_write_policy}\n{network_policy}\n{}",
seatbelt_extensions.policy
)
};

let dir_params = [
file_read_dir_params,
file_write_dir_params,
macos_dir_params(),
seatbelt_extensions.dir_params,
]
.concat();

Expand Down Expand Up @@ -328,10 +367,14 @@ mod tests {
use super::MACOS_SEATBELT_BASE_POLICY;
use super::ProxyPolicyInputs;
use super::create_seatbelt_command_args;
use super::create_seatbelt_command_args_with_extensions;
use super::dynamic_network_policy;
use super::macos_dir_params;
use crate::protocol::SandboxPolicy;
use crate::seatbelt::MACOS_PATH_TO_SEATBELT_EXECUTABLE;
use crate::seatbelt_permissions::MacOsAutomationPermission;
use crate::seatbelt_permissions::MacOsPreferencesPermission;
use crate::seatbelt_permissions::MacOsSeatbeltProfileExtensions;
use pretty_assertions::assert_eq;
use std::fs;
use std::path::Path;
Expand Down Expand Up @@ -395,6 +438,66 @@ mod tests {
);
}

#[test]
fn seatbelt_args_include_macos_permission_extensions() {
let cwd = std::env::temp_dir();
let args = create_seatbelt_command_args_with_extensions(
vec!["echo".to_string(), "ok".to_string()],
&SandboxPolicy::new_read_only_policy(),
cwd.as_path(),
false,
None,
Some(&MacOsSeatbeltProfileExtensions {
macos_preferences: MacOsPreferencesPermission::ReadWrite,
macos_automation: MacOsAutomationPermission::BundleIds(vec![
"com.apple.Notes".to_string(),
]),
macos_accessibility: true,
macos_calendar: true,
}),
);
let policy = &args[1];

assert!(policy.contains("(allow user-preference-write)"));
assert!(policy.contains("(appleevent-destination \"com.apple.Notes\")"));
assert!(policy.contains("com.apple.axserver"));
assert!(policy.contains("com.apple.CalendarAgent"));
}

#[test]
fn seatbelt_args_without_extension_profile_keep_legacy_preferences_read_access() {
let cwd = std::env::temp_dir();
let args = create_seatbelt_command_args(
vec!["echo".to_string(), "ok".to_string()],
&SandboxPolicy::new_read_only_policy(),
cwd.as_path(),
false,
None,
);
let policy = &args[1];
assert!(policy.contains("(allow user-preference-read)"));
assert!(!policy.contains("(allow user-preference-write)"));
}

#[test]
fn seatbelt_args_omit_macos_extensions_when_profile_is_empty() {
let cwd = std::env::temp_dir();
let args = create_seatbelt_command_args_with_extensions(
vec!["echo".to_string(), "ok".to_string()],
&SandboxPolicy::new_read_only_policy(),
cwd.as_path(),
false,
None,
Some(&MacOsSeatbeltProfileExtensions::default()),
);
let policy = &args[1];
assert!(!policy.contains("appleevent-send"));
assert!(!policy.contains("com.apple.axserver"));
assert!(!policy.contains("com.apple.CalendarAgent"));
assert!(!policy.contains("user-preference-read"));
assert!(!policy.contains("user-preference-write"));
}

#[test]
fn create_seatbelt_args_allows_local_binding_when_explicitly_enabled() {
let policy = dynamic_network_policy(
Expand Down Expand Up @@ -564,6 +667,14 @@ mod tests {
(allow file-write*
(require-all (subpath (param "WRITABLE_ROOT_0")) (require-not (subpath (param "WRITABLE_ROOT_0_RO_0"))) (require-not (subpath (param "WRITABLE_ROOT_0_RO_1"))) ) (subpath (param "WRITABLE_ROOT_1")) (subpath (param "WRITABLE_ROOT_2"))
)

; macOS permission profile extensions
(allow ipc-posix-shm-read* (ipc-posix-name-prefix "apple.cfprefs."))
(allow mach-lookup
(global-name "com.apple.cfprefsd.daemon")
(global-name "com.apple.cfprefsd.agent")
(local-name "com.apple.cfprefsd.agent"))
(allow user-preference-read)
"#,
);

Expand Down Expand Up @@ -851,6 +962,14 @@ mod tests {
(allow file-write*
(require-all (subpath (param "WRITABLE_ROOT_0")) (require-not (subpath (param "WRITABLE_ROOT_0_RO_0"))) (require-not (subpath (param "WRITABLE_ROOT_0_RO_1"))) ) (subpath (param "WRITABLE_ROOT_1")){tempdir_policy_entry}
)

; macOS permission profile extensions
(allow ipc-posix-shm-read* (ipc-posix-name-prefix "apple.cfprefs."))
(allow mach-lookup
(global-name "com.apple.cfprefsd.daemon")
(global-name "com.apple.cfprefsd.agent")
(local-name "com.apple.cfprefsd.agent"))
(allow user-preference-read)
"#,
);

Expand Down
3 changes: 0 additions & 3 deletions codex-rs/core/src/seatbelt_base_policy.sbpl
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,6 @@
(allow process-fork)
(allow signal (target same-sandbox))

; Allow cf prefs to work.
(allow user-preference-read)

; process-info
(allow process-info* (target same-sandbox))

Expand Down
Loading
Loading