Compare commits

...

3 Commits

Author SHA1 Message Date
Conrad Kramer
94985eefa1 codex: fix CI failure on PR #19218 2026-04-23 15:49:24 -07:00
Conrad Kramer
286974b02f cli: fix debug sandbox clippy lint 2026-04-23 15:29:03 -07:00
Conrad Kramer
7d599b1ebe cli: add macOS seatbelt debug flags for Mach services and Apple events 2026-04-23 15:20:15 -07:00
6 changed files with 353 additions and 20 deletions

View File

@@ -59,7 +59,7 @@ To test to see what happens when a command is run under the sandbox provided by
```
# macOS
codex sandbox macos [--full-auto] [--log-denials] [COMMAND]...
codex sandbox macos [--full-auto] [--log-denials] [--allow-mach-service SERVICE]... [--allow-appleevent-destination BUNDLE_ID]... [--allow-lsopen] [COMMAND]...
# Linux
codex sandbox linux [--full-auto] [COMMAND]...
@@ -68,10 +68,16 @@ codex sandbox linux [--full-auto] [COMMAND]...
codex sandbox windows [--full-auto] [COMMAND]...
# Legacy aliases
codex debug seatbelt [--full-auto] [--log-denials] [COMMAND]...
codex debug seatbelt [--full-auto] [--log-denials] [--allow-mach-service SERVICE]... [--allow-appleevent-destination BUNDLE_ID]... [--allow-lsopen] [COMMAND]...
codex debug landlock [--full-auto] [COMMAND]...
```
On macOS, `codex sandbox macos` also supports targeted sandbox exceptions for local debugging:
- `--allow-mach-service SERVICE` adds `mach-lookup` permission for a specific global service name.
- `--allow-appleevent-destination BUNDLE_ID` allows AppleEvent delivery to that destination bundle ID and includes the standard AppleEvents daemon lookup used by system profiles.
- `--allow-lsopen` allows LaunchServices open APIs.
### Selecting a sandbox policy via `--sandbox`
The Rust CLI exposes a dedicated `--sandbox` (`-s`) flag that lets you pick the sandbox policy **without** having to reach for the generic `-c/--config` option:

View File

@@ -42,20 +42,26 @@ pub async fn run_command_under_seatbelt(
) -> anyhow::Result<()> {
let SeatbeltCommand {
full_auto,
allow_mach_services,
allow_appleevent_bundle_ids,
allow_lsopen,
allow_unix_sockets,
log_denials,
config_overrides,
command,
} = command;
run_command_under_sandbox(
run_command_under_sandbox(RunCommandUnderSandboxParams {
full_auto,
command,
config_overrides,
codex_linux_sandbox_exe,
SandboxType::Seatbelt,
sandbox_type: SandboxType::Seatbelt,
log_denials,
&allow_unix_sockets,
)
allow_mach_services: &allow_mach_services,
allow_appleevent_bundle_ids: &allow_appleevent_bundle_ids,
allow_lsopen,
allow_unix_sockets: &allow_unix_sockets,
})
.await
}
@@ -76,15 +82,18 @@ pub async fn run_command_under_landlock(
config_overrides,
command,
} = command;
run_command_under_sandbox(
run_command_under_sandbox(RunCommandUnderSandboxParams {
full_auto,
command,
config_overrides,
codex_linux_sandbox_exe,
SandboxType::Landlock,
/*log_denials*/ false,
&[],
)
sandbox_type: SandboxType::Landlock,
log_denials: false,
allow_mach_services: &[],
allow_appleevent_bundle_ids: &[],
allow_lsopen: false,
allow_unix_sockets: &[],
})
.await
}
@@ -97,15 +106,18 @@ pub async fn run_command_under_windows(
config_overrides,
command,
} = command;
run_command_under_sandbox(
run_command_under_sandbox(RunCommandUnderSandboxParams {
full_auto,
command,
config_overrides,
codex_linux_sandbox_exe,
SandboxType::Windows,
/*log_denials*/ false,
&[],
)
sandbox_type: SandboxType::Windows,
log_denials: false,
allow_mach_services: &[],
allow_appleevent_bundle_ids: &[],
allow_lsopen: false,
allow_unix_sockets: &[],
})
.await
}
@@ -116,16 +128,43 @@ enum SandboxType {
Windows,
}
async fn run_command_under_sandbox(
struct RunCommandUnderSandboxParams<'a> {
full_auto: bool,
command: Vec<String>,
config_overrides: CliConfigOverrides,
codex_linux_sandbox_exe: Option<PathBuf>,
sandbox_type: SandboxType,
log_denials: bool,
#[cfg_attr(not(target_os = "macos"), allow(unused_variables))]
allow_unix_sockets: &[AbsolutePathBuf],
) -> anyhow::Result<()> {
#[cfg_attr(not(target_os = "macos"), allow(dead_code))]
allow_mach_services: &'a [String],
#[cfg_attr(not(target_os = "macos"), allow(dead_code))]
allow_appleevent_bundle_ids: &'a [String],
#[cfg_attr(not(target_os = "macos"), allow(dead_code))]
allow_lsopen: bool,
#[cfg_attr(not(target_os = "macos"), allow(dead_code))]
allow_unix_sockets: &'a [AbsolutePathBuf],
}
async fn run_command_under_sandbox(params: RunCommandUnderSandboxParams<'_>) -> anyhow::Result<()> {
let RunCommandUnderSandboxParams {
full_auto,
command,
config_overrides,
codex_linux_sandbox_exe,
sandbox_type,
log_denials,
allow_mach_services,
allow_appleevent_bundle_ids,
allow_lsopen,
allow_unix_sockets,
} = params;
#[cfg(not(target_os = "macos"))]
let _ = (
allow_mach_services,
allow_appleevent_bundle_ids,
allow_lsopen,
allow_unix_sockets,
);
let config = load_debug_sandbox_config(
config_overrides
.parse_overrides()
@@ -196,6 +235,9 @@ async fn run_command_under_sandbox(
sandbox_policy_cwd: sandbox_policy_cwd.as_path(),
enforce_managed_network: false,
network: network.as_ref(),
extra_mach_services: allow_mach_services,
extra_appleevent_bundle_ids: allow_appleevent_bundle_ids,
allow_lsopen,
extra_allow_unix_sockets: allow_unix_sockets,
});
let network_policy = config.permissions.network_sandbox_policy;

View File

@@ -23,6 +23,18 @@ pub struct SeatbeltCommand {
#[arg(long = "full-auto", default_value_t = false)]
pub full_auto: bool,
/// Allow the sandboxed command to look up this Mach service name. Repeat to allow multiple services.
#[arg(long = "allow-mach-service", value_name = "SERVICE", value_parser = parse_non_empty_string)]
pub allow_mach_services: Vec<String>,
/// Allow the sandboxed command to send AppleEvents to this destination bundle ID. Repeat to allow multiple destinations.
#[arg(long = "allow-appleevent-destination", value_name = "BUNDLE_ID", value_parser = parse_non_empty_string)]
pub allow_appleevent_bundle_ids: Vec<String>,
/// Allow the sandboxed command to use LaunchServices open APIs.
#[arg(long = "allow-lsopen", default_value_t = false)]
pub allow_lsopen: bool,
/// Allow the sandboxed command to bind/connect AF_UNIX sockets rooted at this path. Relative paths are resolved against the current directory. Repeat to allow multiple paths.
#[arg(long = "allow-unix-socket", value_parser = parse_allow_unix_socket_path)]
pub allow_unix_sockets: Vec<AbsolutePathBuf>,
@@ -44,6 +56,15 @@ fn parse_allow_unix_socket_path(raw: &str) -> Result<AbsolutePathBuf, String> {
.map_err(|err| format!("invalid path {raw}: {err}"))
}
fn parse_non_empty_string(raw: &str) -> Result<String, String> {
let trimmed = raw.trim();
if trimmed.is_empty() {
Err("value must not be empty".to_string())
} else {
Ok(trimmed.to_string())
}
}
#[derive(Debug, Parser)]
pub struct LandlockCommand {
/// Convenience alias for low-friction sandboxed automatic execution (network-disabled sandbox that can write to cwd and TMPDIR)
@@ -71,3 +92,45 @@ pub struct WindowsCommand {
#[arg(trailing_var_arg = true)]
pub command: Vec<String>,
}
#[cfg(test)]
mod tests {
use super::SeatbeltCommand;
use clap::Parser;
use pretty_assertions::assert_eq;
#[test]
fn seatbelt_command_parses_additional_allowlist_flags() {
let command = SeatbeltCommand::try_parse_from([
"seatbelt",
"--allow-mach-service",
"com.apple.foo",
"--allow-mach-service",
"com.apple.bar",
"--allow-appleevent-destination",
"com.apple.finder",
"--allow-lsopen",
"--allow-unix-socket",
"/tmp/codex.sock",
"--",
"/bin/echo",
"hi",
])
.expect("parse");
assert_eq!(
command.allow_mach_services,
vec!["com.apple.foo".to_string(), "com.apple.bar".to_string()]
);
assert_eq!(
command.allow_appleevent_bundle_ids,
vec!["com.apple.finder".to_string()]
);
assert!(command.allow_lsopen);
assert_eq!(command.allow_unix_sockets.len(), 1);
assert_eq!(
command.command,
vec!["/bin/echo".to_string(), "hi".to_string()]
);
}
}

View File

@@ -215,6 +215,9 @@ impl SandboxManager {
sandbox_policy_cwd,
enforce_managed_network,
network,
extra_mach_services: &[],
extra_appleevent_bundle_ids: &[],
allow_lsopen: false,
extra_allow_unix_sockets: &[],
});
let mut full_command = Vec::with_capacity(1 + args.len());

View File

@@ -240,6 +240,61 @@ fn unix_socket_policy(proxy: &ProxyPolicyInputs) -> String {
policy
}
fn seatbelt_string_literal(value: &str) -> String {
value.replace('\\', "\\\\").replace('"', "\\\"")
}
fn extra_mach_lookup_policy(extra_mach_services: &[String]) -> String {
let services = extra_mach_services
.iter()
.map(|service| service.trim())
.filter(|service| !service.is_empty())
.collect::<BTreeSet<_>>();
if services.is_empty() {
return String::new();
}
let services = services
.into_iter()
.map(|service| format!(" (global-name \"{}\")", seatbelt_string_literal(service)))
.collect::<Vec<_>>()
.join("\n");
format!("(allow mach-lookup\n{services}\n)")
}
fn extra_appleevent_policy(extra_appleevent_bundle_ids: &[String]) -> String {
let bundle_ids = extra_appleevent_bundle_ids
.iter()
.map(|bundle_id| bundle_id.trim())
.filter(|bundle_id| !bundle_id.is_empty())
.collect::<BTreeSet<_>>();
if bundle_ids.is_empty() {
return String::new();
}
let destinations = bundle_ids
.into_iter()
.map(|bundle_id| {
format!(
" (appleevent-destination \"{}\")",
seatbelt_string_literal(bundle_id)
)
})
.collect::<Vec<_>>()
.join("\n");
format!(
"(allow mach-lookup\n (global-name \"com.apple.coreservices.appleevents\"))\n(allow appleevent-send\n{destinations}\n)"
)
}
fn lsopen_policy(allow_lsopen: bool) -> String {
if allow_lsopen {
"(allow lsopen)".to_string()
} else {
String::new()
}
}
#[cfg_attr(not(test), allow(dead_code))]
fn dynamic_network_policy(
sandbox_policy: &SandboxPolicy,
@@ -541,6 +596,9 @@ fn create_seatbelt_command_args_for_legacy_policy(
sandbox_policy_cwd,
enforce_managed_network,
network,
extra_mach_services: &[],
extra_appleevent_bundle_ids: &[],
allow_lsopen: false,
extra_allow_unix_sockets: &[],
})
}
@@ -553,6 +611,9 @@ pub struct CreateSeatbeltCommandArgsParams<'a> {
pub sandbox_policy_cwd: &'a Path,
pub enforce_managed_network: bool,
pub network: Option<&'a NetworkProxy>,
pub extra_mach_services: &'a [String],
pub extra_appleevent_bundle_ids: &'a [String],
pub allow_lsopen: bool,
pub extra_allow_unix_sockets: &'a [AbsolutePathBuf],
}
@@ -564,6 +625,9 @@ pub fn create_seatbelt_command_args(args: CreateSeatbeltCommandArgsParams<'_>) -
sandbox_policy_cwd,
enforce_managed_network,
network,
extra_mach_services,
extra_appleevent_bundle_ids,
allow_lsopen,
extra_allow_unix_sockets,
} = args;
@@ -653,6 +717,9 @@ pub fn create_seatbelt_command_args(args: CreateSeatbeltCommandArgsParams<'_>) -
let proxy = proxy_policy_inputs(network, extra_allow_unix_sockets);
let network_policy =
dynamic_network_policy_for_network(network_sandbox_policy, enforce_managed_network, &proxy);
let mach_lookup_policy = extra_mach_lookup_policy(extra_mach_services);
let appleevent_policy = extra_appleevent_policy(extra_appleevent_bundle_ids);
let lsopen_policy = lsopen_policy(allow_lsopen);
let include_platform_defaults = file_system_sandbox_policy.include_platform_defaults();
let deny_read_policy =
@@ -664,6 +731,15 @@ pub fn create_seatbelt_command_args(args: CreateSeatbeltCommandArgsParams<'_>) -
deny_read_policy,
network_policy,
];
if !mach_lookup_policy.is_empty() {
policy_sections.push(mach_lookup_policy);
}
if !appleevent_policy.is_empty() {
policy_sections.push(appleevent_policy);
}
if !lsopen_policy.is_empty() {
policy_sections.push(lsopen_policy);
}
if include_platform_defaults {
policy_sections.push(MACOS_RESTRICTED_READ_ONLY_PLATFORM_DEFAULTS.to_string());
}

View File

@@ -164,6 +164,9 @@ fn explicit_unreadable_paths_are_excluded_from_full_disk_read_and_write_access()
sandbox_policy_cwd: Path::new("/"),
enforce_managed_network: false,
network: None,
extra_mach_services: &[],
extra_appleevent_bundle_ids: &[],
allow_lsopen: false,
extra_allow_unix_sockets: &[],
});
@@ -230,6 +233,9 @@ fn explicit_unreadable_paths_are_excluded_from_readable_roots() {
sandbox_policy_cwd: Path::new("/"),
enforce_managed_network: false,
network: None,
extra_mach_services: &[],
extra_appleevent_bundle_ids: &[],
allow_lsopen: false,
extra_allow_unix_sockets: &[],
});
@@ -573,6 +579,9 @@ fn create_seatbelt_args_allowlists_explicit_unix_socket_paths_without_proxy() {
sandbox_policy_cwd: cwd.path(),
enforce_managed_network: false,
network: None,
extra_mach_services: &[],
extra_appleevent_bundle_ids: &[],
allow_lsopen: false,
extra_allow_unix_sockets: &extra_allow_unix_sockets,
});
let policy = seatbelt_policy_arg(&args);
@@ -631,6 +640,9 @@ async fn create_seatbelt_args_merges_proxy_and_explicit_unix_socket_paths() -> a
sandbox_policy_cwd: cwd.path(),
enforce_managed_network: false,
network: Some(&network_proxy),
extra_mach_services: &[],
extra_appleevent_bundle_ids: &[],
allow_lsopen: false,
extra_allow_unix_sockets: &extra_allow_unix_sockets,
});
@@ -672,6 +684,9 @@ fn create_seatbelt_args_preserves_full_network_with_explicit_unix_socket_paths()
sandbox_policy_cwd: cwd.path(),
enforce_managed_network: false,
network: None,
extra_mach_services: &[],
extra_appleevent_bundle_ids: &[],
allow_lsopen: false,
extra_allow_unix_sockets: &extra_allow_unix_sockets,
});
let policy = seatbelt_policy_arg(&args);
@@ -715,6 +730,134 @@ fn unix_socket_policy_non_empty_output_is_newline_terminated() {
);
}
#[test]
fn create_seatbelt_args_allowlists_extra_mach_services() {
let cwd = TempDir::new().expect("temp cwd");
let file_system_policy = FileSystemSandboxPolicy::from_legacy_sandbox_policy(
&SandboxPolicy::new_read_only_policy(),
cwd.path(),
);
let extra_mach_services = vec![
"com.apple.beta".to_string(),
"com.apple.alpha".to_string(),
"com.apple.beta".to_string(),
];
let args = create_seatbelt_command_args(CreateSeatbeltCommandArgsParams {
command: vec!["/usr/bin/true".to_string()],
file_system_sandbox_policy: &file_system_policy,
network_sandbox_policy: NetworkSandboxPolicy::Restricted,
sandbox_policy_cwd: cwd.path(),
enforce_managed_network: false,
network: None,
extra_mach_services: &extra_mach_services,
extra_appleevent_bundle_ids: &[],
allow_lsopen: false,
extra_allow_unix_sockets: &[],
});
let policy = seatbelt_policy_arg(&args);
assert!(
policy.contains(
"(allow mach-lookup\n (global-name \"com.apple.alpha\")\n (global-name \"com.apple.beta\")\n)"
),
"policy should allow the requested Mach services in stable order:\n{policy}"
);
assert_eq!(
policy.matches("(global-name \"com.apple.alpha\")").count(),
1
);
assert_eq!(
policy.matches("(global-name \"com.apple.beta\")").count(),
1
);
}
#[test]
fn create_seatbelt_args_allowlists_appleevent_destinations() {
let cwd = TempDir::new().expect("temp cwd");
let file_system_policy = FileSystemSandboxPolicy::from_legacy_sandbox_policy(
&SandboxPolicy::new_read_only_policy(),
cwd.path(),
);
let extra_appleevent_bundle_ids = vec![
"com.apple.mail".to_string(),
"com.apple.finder".to_string(),
"com.apple.mail".to_string(),
];
let args = create_seatbelt_command_args(CreateSeatbeltCommandArgsParams {
command: vec!["/usr/bin/true".to_string()],
file_system_sandbox_policy: &file_system_policy,
network_sandbox_policy: NetworkSandboxPolicy::Restricted,
sandbox_policy_cwd: cwd.path(),
enforce_managed_network: false,
network: None,
extra_mach_services: &[],
extra_appleevent_bundle_ids: &extra_appleevent_bundle_ids,
allow_lsopen: false,
extra_allow_unix_sockets: &[],
});
let policy = seatbelt_policy_arg(&args);
assert!(
policy.contains(
"(allow mach-lookup\n (global-name \"com.apple.coreservices.appleevents\"))"
),
"policy should allow lookup of the AppleEvents service:\n{policy}"
);
assert!(
policy.contains(
"(allow appleevent-send\n (appleevent-destination \"com.apple.finder\")\n (appleevent-destination \"com.apple.mail\")\n)"
),
"policy should allow the requested AppleEvent destinations in stable order:\n{policy}"
);
assert_eq!(
policy
.matches("(global-name \"com.apple.coreservices.appleevents\")")
.count(),
1
);
assert_eq!(
policy
.matches("(appleevent-destination \"com.apple.finder\")")
.count(),
1
);
assert_eq!(
policy
.matches("(appleevent-destination \"com.apple.mail\")")
.count(),
1
);
}
#[test]
fn create_seatbelt_args_allows_lsopen_when_requested() {
let cwd = TempDir::new().expect("temp cwd");
let file_system_policy = FileSystemSandboxPolicy::from_legacy_sandbox_policy(
&SandboxPolicy::new_read_only_policy(),
cwd.path(),
);
let args = create_seatbelt_command_args(CreateSeatbeltCommandArgsParams {
command: vec!["/usr/bin/true".to_string()],
file_system_sandbox_policy: &file_system_policy,
network_sandbox_policy: NetworkSandboxPolicy::Restricted,
sandbox_policy_cwd: cwd.path(),
enforce_managed_network: false,
network: None,
extra_mach_services: &[],
extra_appleevent_bundle_ids: &[],
allow_lsopen: true,
extra_allow_unix_sockets: &[],
});
let policy = seatbelt_policy_arg(&args);
assert!(
policy.contains("(allow lsopen)"),
"policy should allow lsopen when requested:\n{policy}"
);
assert_eq!(policy.matches("(allow lsopen)").count(), 1);
}
#[test]
fn unix_socket_dir_params_use_stable_param_names() {
let params = unix_socket_dir_params(&ProxyPolicyInputs {