mirror of
https://github.com/openai/codex.git
synced 2026-11-10 00:42:33 +00:00
Move macOS sandbox builders into codex-sandboxing (#15593)
## Summary - move macOS permission merging/intersection logic and tests from `codex-core` into `codex-sandboxing` - move seatbelt policy builders, permissions logic, SBPL assets, and their tests into `codex-sandboxing` - keep `codex-core` owning only the seatbelt spawn wrapper and switch call sites to import the moved APIs directly ## Notes - no re-exports added - moved the seatbelt tests with the implementation so internal helpers could stay private - local verification is still finishing while this PR is open
This commit is contained in:
1 parent
2227248cd6
commit
431af0807c
19 files changed
+650
-601
No files matched your search
Generated
+7
@@ -2468,9 +2468,16 @@ dependencies = [
|
|||||||
name = "codex-sandboxing"
|
name = "codex-sandboxing"
|
||||||
version = "0.0.0"
|
version = "0.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"codex-network-proxy",
|
||||||
"codex-protocol",
|
"codex-protocol",
|
||||||
|
"codex-utils-absolute-path",
|
||||||
|
"dirs",
|
||||||
|
"libc",
|
||||||
"pretty_assertions",
|
"pretty_assertions",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"tempfile",
|
||||||
|
"tracing",
|
||||||
|
"url",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
+1
-1
@@ -290,7 +290,7 @@ supports-color = "3.0.2"
|
|||||||
syntect = "5"
|
syntect = "5"
|
||||||
sys-locale = "0.3.2"
|
sys-locale = "0.3.2"
|
||||||
tempfile = "3.23.0"
|
tempfile = "3.23.0"
|
||||||
tar = "0.4.44"
|
tar = "0.4.45"
|
||||||
test-log = "0.2.19"
|
test-log = "0.2.19"
|
||||||
textwrap = "0.16.2"
|
textwrap = "0.16.2"
|
||||||
thiserror = "2.0.17"
|
thiserror = "2.0.17"
|
||||||
|
|||||||
@@ -12,13 +12,13 @@ use codex_core::config::ConfigOverrides;
|
|||||||
use codex_core::config::NetworkProxyAuditMetadata;
|
use codex_core::config::NetworkProxyAuditMetadata;
|
||||||
use codex_core::exec_env::create_env;
|
use codex_core::exec_env::create_env;
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
use codex_core::seatbelt::create_seatbelt_command_args_for_policies_with_extensions;
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
use codex_core::spawn::CODEX_SANDBOX_ENV_VAR;
|
use codex_core::spawn::CODEX_SANDBOX_ENV_VAR;
|
||||||
use codex_core::spawn::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR;
|
use codex_core::spawn::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR;
|
||||||
use codex_protocol::config_types::SandboxMode;
|
use codex_protocol::config_types::SandboxMode;
|
||||||
use codex_protocol::permissions::NetworkSandboxPolicy;
|
use codex_protocol::permissions::NetworkSandboxPolicy;
|
||||||
use codex_sandboxing::landlock::create_linux_sandbox_command_args_for_policies;
|
use codex_sandboxing::landlock::create_linux_sandbox_command_args_for_policies;
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
use codex_sandboxing::seatbelt::create_seatbelt_command_args_for_policies_with_extensions;
|
||||||
use codex_utils_cli::CliConfigOverrides;
|
use codex_utils_cli::CliConfigOverrides;
|
||||||
use tokio::process::Child;
|
use tokio::process::Child;
|
||||||
use tokio::process::Command as TokioCommand;
|
use tokio::process::Command as TokioCommand;
|
||||||
|
|||||||
@@ -93,7 +93,6 @@ mod event_mapping;
|
|||||||
mod response_debug_context;
|
mod response_debug_context;
|
||||||
pub mod review_format;
|
pub mod review_format;
|
||||||
pub mod review_prompts;
|
pub mod review_prompts;
|
||||||
mod seatbelt_permissions;
|
|
||||||
mod thread_manager;
|
mod thread_manager;
|
||||||
pub mod web_search;
|
pub mod web_search;
|
||||||
pub mod windows_sandbox_read_grants;
|
pub mod windows_sandbox_read_grants;
|
||||||
|
|||||||
@@ -6,8 +6,6 @@ sandbox placement and transformation of portable CommandSpec into a
|
|||||||
ready‑to‑spawn environment.
|
ready‑to‑spawn environment.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
pub(crate) mod macos_permissions;
|
|
||||||
|
|
||||||
use crate::exec::ExecCapturePolicy;
|
use crate::exec::ExecCapturePolicy;
|
||||||
use crate::exec::ExecExpiration;
|
use crate::exec::ExecExpiration;
|
||||||
use crate::exec::ExecToolCallOutput;
|
use crate::exec::ExecToolCallOutput;
|
||||||
@@ -16,10 +14,6 @@ use crate::exec::StdoutStream;
|
|||||||
use crate::exec::execute_exec_request;
|
use crate::exec::execute_exec_request;
|
||||||
use crate::protocol::SandboxPolicy;
|
use crate::protocol::SandboxPolicy;
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
use crate::seatbelt::MACOS_PATH_TO_SEATBELT_EXECUTABLE;
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
use crate::seatbelt::create_seatbelt_command_args_for_policies_with_extensions;
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
use crate::spawn::CODEX_SANDBOX_ENV_VAR;
|
use crate::spawn::CODEX_SANDBOX_ENV_VAR;
|
||||||
use crate::spawn::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR;
|
use crate::spawn::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR;
|
||||||
use crate::tools::sandboxing::SandboxablePreference;
|
use crate::tools::sandboxing::SandboxablePreference;
|
||||||
@@ -40,10 +34,14 @@ use codex_protocol::protocol::NetworkAccess;
|
|||||||
use codex_protocol::protocol::ReadOnlyAccess;
|
use codex_protocol::protocol::ReadOnlyAccess;
|
||||||
use codex_sandboxing::landlock::allow_network_for_proxy;
|
use codex_sandboxing::landlock::allow_network_for_proxy;
|
||||||
use codex_sandboxing::landlock::create_linux_sandbox_command_args_for_policies;
|
use codex_sandboxing::landlock::create_linux_sandbox_command_args_for_policies;
|
||||||
|
use codex_sandboxing::macos_permissions::intersect_macos_seatbelt_profile_extensions;
|
||||||
|
use codex_sandboxing::macos_permissions::merge_macos_seatbelt_profile_extensions;
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
use codex_sandboxing::seatbelt::MACOS_PATH_TO_SEATBELT_EXECUTABLE;
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
use codex_sandboxing::seatbelt::create_seatbelt_command_args_for_policies_with_extensions;
|
||||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||||
use dunce::canonicalize;
|
use dunce::canonicalize;
|
||||||
use macos_permissions::intersect_macos_seatbelt_profile_extensions;
|
|
||||||
use macos_permissions::merge_macos_seatbelt_profile_extensions;
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|||||||
+11
-556
@@ -1,40 +1,19 @@
|
|||||||
#![cfg(target_os = "macos")]
|
#![cfg(target_os = "macos")]
|
||||||
|
|
||||||
use codex_network_proxy::NetworkProxy;
|
|
||||||
use codex_network_proxy::PROXY_URL_ENV_KEYS;
|
|
||||||
use codex_network_proxy::has_proxy_url_env_vars;
|
|
||||||
use codex_network_proxy::proxy_url_env_value;
|
|
||||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
|
||||||
use std::collections::BTreeMap;
|
|
||||||
use std::collections::BTreeSet;
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::ffi::CStr;
|
|
||||||
use std::path::Path;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
use tokio::process::Child;
|
|
||||||
use tracing::warn;
|
|
||||||
use url::Url;
|
|
||||||
|
|
||||||
use crate::protocol::SandboxPolicy;
|
use crate::protocol::SandboxPolicy;
|
||||||
use crate::seatbelt_permissions::MacOsSeatbeltProfileExtensions;
|
|
||||||
use crate::seatbelt_permissions::build_seatbelt_extensions;
|
|
||||||
use crate::spawn::CODEX_SANDBOX_ENV_VAR;
|
use crate::spawn::CODEX_SANDBOX_ENV_VAR;
|
||||||
use crate::spawn::SpawnChildRequest;
|
use crate::spawn::SpawnChildRequest;
|
||||||
use crate::spawn::StdioPolicy;
|
use crate::spawn::StdioPolicy;
|
||||||
use crate::spawn::spawn_child_async;
|
use crate::spawn::spawn_child_async;
|
||||||
|
use codex_network_proxy::NetworkProxy;
|
||||||
use codex_protocol::permissions::FileSystemSandboxPolicy;
|
use codex_protocol::permissions::FileSystemSandboxPolicy;
|
||||||
use codex_protocol::permissions::NetworkSandboxPolicy;
|
use codex_protocol::permissions::NetworkSandboxPolicy;
|
||||||
|
use codex_sandboxing::seatbelt::MACOS_PATH_TO_SEATBELT_EXECUTABLE;
|
||||||
const MACOS_SEATBELT_BASE_POLICY: &str = include_str!("seatbelt_base_policy.sbpl");
|
use codex_sandboxing::seatbelt::create_seatbelt_command_args_for_policies_with_extensions;
|
||||||
const MACOS_SEATBELT_NETWORK_POLICY: &str = include_str!("seatbelt_network_policy.sbpl");
|
use std::collections::HashMap;
|
||||||
const MACOS_RESTRICTED_READ_ONLY_PLATFORM_DEFAULTS: &str =
|
use std::path::Path;
|
||||||
include_str!("restricted_read_only_platform_defaults.sbpl");
|
use std::path::PathBuf;
|
||||||
|
use tokio::process::Child;
|
||||||
/// When working with `sandbox-exec`, only consider `sandbox-exec` in `/usr/bin`
|
|
||||||
/// to defend against an attacker trying to inject a malicious version on the
|
|
||||||
/// PATH. If /usr/bin/sandbox-exec has been tampered with, then the attacker
|
|
||||||
/// already has root access.
|
|
||||||
pub const MACOS_PATH_TO_SEATBELT_EXECUTABLE: &str = "/usr/bin/sandbox-exec";
|
|
||||||
|
|
||||||
pub async fn spawn_command_under_seatbelt(
|
pub async fn spawn_command_under_seatbelt(
|
||||||
command: Vec<String>,
|
command: Vec<String>,
|
||||||
@@ -45,12 +24,14 @@ pub async fn spawn_command_under_seatbelt(
|
|||||||
network: Option<&NetworkProxy>,
|
network: Option<&NetworkProxy>,
|
||||||
mut env: HashMap<String, String>,
|
mut env: HashMap<String, String>,
|
||||||
) -> std::io::Result<Child> {
|
) -> std::io::Result<Child> {
|
||||||
let args = create_seatbelt_command_args(
|
let args = create_seatbelt_command_args_for_policies_with_extensions(
|
||||||
command,
|
command,
|
||||||
sandbox_policy,
|
&FileSystemSandboxPolicy::from_legacy_sandbox_policy(sandbox_policy, sandbox_policy_cwd),
|
||||||
|
NetworkSandboxPolicy::from(sandbox_policy),
|
||||||
sandbox_policy_cwd,
|
sandbox_policy_cwd,
|
||||||
/*enforce_managed_network*/ false,
|
/*enforce_managed_network*/ false,
|
||||||
network,
|
network,
|
||||||
|
/*extensions*/ None,
|
||||||
);
|
);
|
||||||
let arg0 = None;
|
let arg0 = None;
|
||||||
env.insert(CODEX_SANDBOX_ENV_VAR.to_string(), "seatbelt".to_string());
|
env.insert(CODEX_SANDBOX_ENV_VAR.to_string(), "seatbelt".to_string());
|
||||||
@@ -66,529 +47,3 @@ pub async fn spawn_command_under_seatbelt(
|
|||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_loopback_host(host: &str) -> bool {
|
|
||||||
host.eq_ignore_ascii_case("localhost") || host == "127.0.0.1" || host == "::1"
|
|
||||||
}
|
|
||||||
|
|
||||||
fn proxy_scheme_default_port(scheme: &str) -> u16 {
|
|
||||||
match scheme {
|
|
||||||
"https" => 443,
|
|
||||||
"socks5" | "socks5h" | "socks4" | "socks4a" => 1080,
|
|
||||||
_ => 80,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn proxy_loopback_ports_from_env(env: &HashMap<String, String>) -> Vec<u16> {
|
|
||||||
let mut ports = BTreeSet::new();
|
|
||||||
for key in PROXY_URL_ENV_KEYS {
|
|
||||||
let Some(proxy_url) = proxy_url_env_value(env, key) else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
let trimmed = proxy_url.trim();
|
|
||||||
if trimmed.is_empty() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let candidate = if trimmed.contains("://") {
|
|
||||||
trimmed.to_string()
|
|
||||||
} else {
|
|
||||||
format!("http://{trimmed}")
|
|
||||||
};
|
|
||||||
let Ok(parsed) = Url::parse(&candidate) else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
let Some(host) = parsed.host_str() else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
if !is_loopback_host(host) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let scheme = parsed.scheme().to_ascii_lowercase();
|
|
||||||
let port = parsed
|
|
||||||
.port()
|
|
||||||
.unwrap_or_else(|| proxy_scheme_default_port(scheme.as_str()));
|
|
||||||
ports.insert(port);
|
|
||||||
}
|
|
||||||
ports.into_iter().collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
|
||||||
struct ProxyPolicyInputs {
|
|
||||||
ports: Vec<u16>,
|
|
||||||
has_proxy_config: bool,
|
|
||||||
allow_local_binding: bool,
|
|
||||||
unix_domain_socket_policy: UnixDomainSocketPolicy,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
// Keep allow-all and allowlist modes disjoint so we don't carry ignored state.
|
|
||||||
enum UnixDomainSocketPolicy {
|
|
||||||
AllowAll,
|
|
||||||
Restricted { allowed: Vec<AbsolutePathBuf> },
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for UnixDomainSocketPolicy {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::Restricted { allowed: vec![] }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
struct UnixSocketPathParam {
|
|
||||||
index: usize,
|
|
||||||
path: AbsolutePathBuf,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn proxy_policy_inputs(network: Option<&NetworkProxy>) -> ProxyPolicyInputs {
|
|
||||||
if let Some(network) = network {
|
|
||||||
let mut env = HashMap::new();
|
|
||||||
network.apply_to_env(&mut env);
|
|
||||||
let unix_domain_socket_policy = if network.dangerously_allow_all_unix_sockets() {
|
|
||||||
UnixDomainSocketPolicy::AllowAll
|
|
||||||
} else {
|
|
||||||
let allowed = network
|
|
||||||
.allow_unix_sockets()
|
|
||||||
.iter()
|
|
||||||
.filter_map(
|
|
||||||
|socket_path| match normalize_path_for_sandbox(Path::new(socket_path)) {
|
|
||||||
Some(path) => Some((path.to_string_lossy().to_string(), path)),
|
|
||||||
None => {
|
|
||||||
warn!(
|
|
||||||
"ignoring network.allow_unix_sockets entry because it could not be normalized: {socket_path}"
|
|
||||||
);
|
|
||||||
None
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.collect::<BTreeMap<_, _>>()
|
|
||||||
.into_values()
|
|
||||||
.collect();
|
|
||||||
UnixDomainSocketPolicy::Restricted { allowed }
|
|
||||||
};
|
|
||||||
return ProxyPolicyInputs {
|
|
||||||
ports: proxy_loopback_ports_from_env(&env),
|
|
||||||
has_proxy_config: has_proxy_url_env_vars(&env),
|
|
||||||
allow_local_binding: network.allow_local_binding(),
|
|
||||||
unix_domain_socket_policy,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
ProxyPolicyInputs::default()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn normalize_path_for_sandbox(path: &Path) -> Option<AbsolutePathBuf> {
|
|
||||||
// `AbsolutePathBuf::from_absolute_path()` normalizes relative paths against the current
|
|
||||||
// working directory, so keep the explicit check to avoid silently accepting relative entries.
|
|
||||||
if !path.is_absolute() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let absolute_path = AbsolutePathBuf::from_absolute_path(path).ok()?;
|
|
||||||
let normalized_path = absolute_path
|
|
||||||
.as_path()
|
|
||||||
.canonicalize()
|
|
||||||
.ok()
|
|
||||||
.and_then(|canonical_path| AbsolutePathBuf::from_absolute_path(canonical_path).ok());
|
|
||||||
normalized_path.or(Some(absolute_path))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn unix_socket_path_params(proxy: &ProxyPolicyInputs) -> Vec<UnixSocketPathParam> {
|
|
||||||
let mut deduped_paths: BTreeMap<String, AbsolutePathBuf> = BTreeMap::new();
|
|
||||||
let UnixDomainSocketPolicy::Restricted { allowed } = &proxy.unix_domain_socket_policy else {
|
|
||||||
return vec![];
|
|
||||||
};
|
|
||||||
for path in allowed {
|
|
||||||
deduped_paths
|
|
||||||
.entry(path.to_string_lossy().to_string())
|
|
||||||
.or_insert_with(|| path.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
deduped_paths
|
|
||||||
.into_values()
|
|
||||||
.enumerate()
|
|
||||||
.map(|(index, path)| UnixSocketPathParam { index, path })
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn unix_socket_path_param_key(index: usize) -> String {
|
|
||||||
format!("UNIX_SOCKET_PATH_{index}")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn unix_socket_dir_params(proxy: &ProxyPolicyInputs) -> Vec<(String, PathBuf)> {
|
|
||||||
unix_socket_path_params(proxy)
|
|
||||||
.into_iter()
|
|
||||||
.map(|param| {
|
|
||||||
(
|
|
||||||
unix_socket_path_param_key(param.index),
|
|
||||||
param.path.into_path_buf(),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns zero or more complete Seatbelt policy lines for unix socket rules.
|
|
||||||
/// When non-empty, the returned string is newline-terminated so callers can
|
|
||||||
/// append it directly to larger policy blocks.
|
|
||||||
fn unix_socket_policy(proxy: &ProxyPolicyInputs) -> String {
|
|
||||||
let socket_params = unix_socket_path_params(proxy);
|
|
||||||
let has_unix_socket_access = matches!(
|
|
||||||
proxy.unix_domain_socket_policy,
|
|
||||||
UnixDomainSocketPolicy::AllowAll
|
|
||||||
) || !socket_params.is_empty();
|
|
||||||
if !has_unix_socket_access {
|
|
||||||
return String::new();
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut policy = String::new();
|
|
||||||
policy.push_str("(allow system-socket (socket-domain AF_UNIX))\n");
|
|
||||||
if matches!(
|
|
||||||
proxy.unix_domain_socket_policy,
|
|
||||||
UnixDomainSocketPolicy::AllowAll
|
|
||||||
) {
|
|
||||||
// Keep AllowAll genuinely broad here; path qualifiers look narrower
|
|
||||||
// without a clear macOS behavioral benefit.
|
|
||||||
policy.push_str("(allow network-bind (local unix-socket))\n");
|
|
||||||
policy.push_str("(allow network-outbound (remote unix-socket))\n");
|
|
||||||
return policy;
|
|
||||||
}
|
|
||||||
|
|
||||||
for param in socket_params {
|
|
||||||
let key = unix_socket_path_param_key(param.index);
|
|
||||||
// Use subpath so allowlists cover sockets created beneath approved directories.
|
|
||||||
policy.push_str(&format!(
|
|
||||||
"(allow network-bind (local unix-socket (subpath (param \"{key}\"))))\n"
|
|
||||||
));
|
|
||||||
policy.push_str(&format!(
|
|
||||||
"(allow network-outbound (remote unix-socket (subpath (param \"{key}\"))))\n"
|
|
||||||
));
|
|
||||||
}
|
|
||||||
policy
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg_attr(not(test), allow(dead_code))]
|
|
||||||
fn dynamic_network_policy(
|
|
||||||
sandbox_policy: &SandboxPolicy,
|
|
||||||
enforce_managed_network: bool,
|
|
||||||
proxy: &ProxyPolicyInputs,
|
|
||||||
) -> String {
|
|
||||||
dynamic_network_policy_for_network(
|
|
||||||
NetworkSandboxPolicy::from(sandbox_policy),
|
|
||||||
enforce_managed_network,
|
|
||||||
proxy,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn dynamic_network_policy_for_network(
|
|
||||||
network_policy: NetworkSandboxPolicy,
|
|
||||||
enforce_managed_network: bool,
|
|
||||||
proxy: &ProxyPolicyInputs,
|
|
||||||
) -> String {
|
|
||||||
let should_use_restricted_network_policy =
|
|
||||||
!proxy.ports.is_empty() || proxy.has_proxy_config || enforce_managed_network;
|
|
||||||
if should_use_restricted_network_policy {
|
|
||||||
let mut policy = String::new();
|
|
||||||
if proxy.allow_local_binding {
|
|
||||||
policy.push_str("; allow loopback local binding and loopback traffic\n");
|
|
||||||
policy.push_str("(allow network-bind (local ip \"localhost:*\"))\n");
|
|
||||||
policy.push_str("(allow network-inbound (local ip \"localhost:*\"))\n");
|
|
||||||
policy.push_str("(allow network-outbound (remote ip \"localhost:*\"))\n");
|
|
||||||
}
|
|
||||||
for port in &proxy.ports {
|
|
||||||
policy.push_str(&format!(
|
|
||||||
"(allow network-outbound (remote ip \"localhost:{port}\"))\n"
|
|
||||||
));
|
|
||||||
}
|
|
||||||
let unix_socket_policy = unix_socket_policy(proxy);
|
|
||||||
if !unix_socket_policy.is_empty() {
|
|
||||||
policy.push_str("; allow unix domain sockets for local IPC\n");
|
|
||||||
policy.push_str(&unix_socket_policy);
|
|
||||||
}
|
|
||||||
return format!("{policy}{MACOS_SEATBELT_NETWORK_POLICY}");
|
|
||||||
}
|
|
||||||
|
|
||||||
if proxy.has_proxy_config {
|
|
||||||
// Proxy configuration is present but we could not infer any valid loopback endpoints.
|
|
||||||
// Fail closed to avoid silently widening network access in proxy-enforced sessions.
|
|
||||||
return String::new();
|
|
||||||
}
|
|
||||||
|
|
||||||
if enforce_managed_network {
|
|
||||||
// Managed network requirements are active but no usable proxy endpoints
|
|
||||||
// are available. Fail closed for network access.
|
|
||||||
return String::new();
|
|
||||||
}
|
|
||||||
|
|
||||||
if network_policy.is_enabled() {
|
|
||||||
// No proxy env is configured: retain the existing full-network behavior.
|
|
||||||
format!(
|
|
||||||
"(allow network-outbound)\n(allow network-inbound)\n{MACOS_SEATBELT_NETWORK_POLICY}"
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
String::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg_attr(not(test), allow(dead_code))]
|
|
||||||
pub(crate) fn create_seatbelt_command_args(
|
|
||||||
command: Vec<String>,
|
|
||||||
sandbox_policy: &SandboxPolicy,
|
|
||||||
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,
|
|
||||||
/*extensions*/ None,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn root_absolute_path() -> AbsolutePathBuf {
|
|
||||||
match AbsolutePathBuf::from_absolute_path(Path::new("/")) {
|
|
||||||
Ok(path) => path,
|
|
||||||
Err(err) => panic!("root path must be absolute: {err}"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
struct SeatbeltAccessRoot {
|
|
||||||
root: AbsolutePathBuf,
|
|
||||||
excluded_subpaths: Vec<AbsolutePathBuf>,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_seatbelt_access_policy(
|
|
||||||
action: &str,
|
|
||||||
param_prefix: &str,
|
|
||||||
roots: Vec<SeatbeltAccessRoot>,
|
|
||||||
) -> (String, Vec<(String, PathBuf)>) {
|
|
||||||
let mut policy_components = Vec::new();
|
|
||||||
let mut params = Vec::new();
|
|
||||||
|
|
||||||
for (index, access_root) in roots.into_iter().enumerate() {
|
|
||||||
let root =
|
|
||||||
normalize_path_for_sandbox(access_root.root.as_path()).unwrap_or(access_root.root);
|
|
||||||
let root_param = format!("{param_prefix}_{index}");
|
|
||||||
params.push((root_param.clone(), root.into_path_buf()));
|
|
||||||
|
|
||||||
if access_root.excluded_subpaths.is_empty() {
|
|
||||||
policy_components.push(format!("(subpath (param \"{root_param}\"))"));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut require_parts = vec![format!("(subpath (param \"{root_param}\"))")];
|
|
||||||
for (excluded_index, excluded_subpath) in
|
|
||||||
access_root.excluded_subpaths.into_iter().enumerate()
|
|
||||||
{
|
|
||||||
let excluded_subpath =
|
|
||||||
normalize_path_for_sandbox(excluded_subpath.as_path()).unwrap_or(excluded_subpath);
|
|
||||||
let excluded_param = format!("{param_prefix}_{index}_RO_{excluded_index}");
|
|
||||||
params.push((excluded_param.clone(), excluded_subpath.into_path_buf()));
|
|
||||||
require_parts.push(format!(
|
|
||||||
"(require-not (subpath (param \"{excluded_param}\")))"
|
|
||||||
));
|
|
||||||
}
|
|
||||||
policy_components.push(format!("(require-all {} )", require_parts.join(" ")));
|
|
||||||
}
|
|
||||||
|
|
||||||
if policy_components.is_empty() {
|
|
||||||
(String::new(), Vec::new())
|
|
||||||
} else {
|
|
||||||
(
|
|
||||||
format!("(allow {action}\n{}\n)", policy_components.join(" ")),
|
|
||||||
params,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg_attr(not(test), allow(dead_code))]
|
|
||||||
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> {
|
|
||||||
create_seatbelt_command_args_for_policies_with_extensions(
|
|
||||||
command,
|
|
||||||
&FileSystemSandboxPolicy::from_legacy_sandbox_policy(sandbox_policy, sandbox_policy_cwd),
|
|
||||||
NetworkSandboxPolicy::from(sandbox_policy),
|
|
||||||
sandbox_policy_cwd,
|
|
||||||
enforce_managed_network,
|
|
||||||
network,
|
|
||||||
extensions,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn create_seatbelt_command_args_for_policies_with_extensions(
|
|
||||||
command: Vec<String>,
|
|
||||||
file_system_sandbox_policy: &FileSystemSandboxPolicy,
|
|
||||||
network_sandbox_policy: NetworkSandboxPolicy,
|
|
||||||
sandbox_policy_cwd: &Path,
|
|
||||||
enforce_managed_network: bool,
|
|
||||||
network: Option<&NetworkProxy>,
|
|
||||||
extensions: Option<&MacOsSeatbeltProfileExtensions>,
|
|
||||||
) -> Vec<String> {
|
|
||||||
let unreadable_roots =
|
|
||||||
file_system_sandbox_policy.get_unreadable_roots_with_cwd(sandbox_policy_cwd);
|
|
||||||
let (file_write_policy, file_write_dir_params) =
|
|
||||||
if file_system_sandbox_policy.has_full_disk_write_access() {
|
|
||||||
if unreadable_roots.is_empty() {
|
|
||||||
// Allegedly, this is more permissive than `(allow file-write*)`.
|
|
||||||
(
|
|
||||||
r#"(allow file-write* (regex #"^/"))"#.to_string(),
|
|
||||||
Vec::new(),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
build_seatbelt_access_policy(
|
|
||||||
"file-write*",
|
|
||||||
"WRITABLE_ROOT",
|
|
||||||
vec![SeatbeltAccessRoot {
|
|
||||||
root: root_absolute_path(),
|
|
||||||
excluded_subpaths: unreadable_roots.clone(),
|
|
||||||
}],
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
build_seatbelt_access_policy(
|
|
||||||
"file-write*",
|
|
||||||
"WRITABLE_ROOT",
|
|
||||||
file_system_sandbox_policy
|
|
||||||
.get_writable_roots_with_cwd(sandbox_policy_cwd)
|
|
||||||
.into_iter()
|
|
||||||
.map(|root| SeatbeltAccessRoot {
|
|
||||||
root: root.root,
|
|
||||||
excluded_subpaths: root.read_only_subpaths,
|
|
||||||
})
|
|
||||||
.collect(),
|
|
||||||
)
|
|
||||||
};
|
|
||||||
|
|
||||||
let (file_read_policy, file_read_dir_params) =
|
|
||||||
if file_system_sandbox_policy.has_full_disk_read_access() {
|
|
||||||
if unreadable_roots.is_empty() {
|
|
||||||
(
|
|
||||||
"; allow read-only file operations\n(allow file-read*)".to_string(),
|
|
||||||
Vec::new(),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
let (policy, params) = build_seatbelt_access_policy(
|
|
||||||
"file-read*",
|
|
||||||
"READABLE_ROOT",
|
|
||||||
vec![SeatbeltAccessRoot {
|
|
||||||
root: root_absolute_path(),
|
|
||||||
excluded_subpaths: unreadable_roots,
|
|
||||||
}],
|
|
||||||
);
|
|
||||||
(
|
|
||||||
format!("; allow read-only file operations\n{policy}"),
|
|
||||||
params,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let (policy, params) = build_seatbelt_access_policy(
|
|
||||||
"file-read*",
|
|
||||||
"READABLE_ROOT",
|
|
||||||
file_system_sandbox_policy
|
|
||||||
.get_readable_roots_with_cwd(sandbox_policy_cwd)
|
|
||||||
.into_iter()
|
|
||||||
.map(|root| SeatbeltAccessRoot {
|
|
||||||
excluded_subpaths: unreadable_roots
|
|
||||||
.iter()
|
|
||||||
.filter(|path| path.as_path().starts_with(root.as_path()))
|
|
||||||
.cloned()
|
|
||||||
.collect(),
|
|
||||||
root,
|
|
||||||
})
|
|
||||||
.collect(),
|
|
||||||
);
|
|
||||||
if policy.is_empty() {
|
|
||||||
(String::new(), params)
|
|
||||||
} else {
|
|
||||||
(
|
|
||||||
format!("; allow read-only file operations\n{policy}"),
|
|
||||||
params,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let proxy = proxy_policy_inputs(network);
|
|
||||||
let network_policy =
|
|
||||||
dynamic_network_policy_for_network(network_sandbox_policy, enforce_managed_network, &proxy);
|
|
||||||
let seatbelt_extensions = extensions.map_or_else(
|
|
||||||
|| {
|
|
||||||
// Backward-compatibility default when no extension profile is provided.
|
|
||||||
build_seatbelt_extensions(&MacOsSeatbeltProfileExtensions::default())
|
|
||||||
},
|
|
||||||
build_seatbelt_extensions,
|
|
||||||
);
|
|
||||||
|
|
||||||
let include_platform_defaults = file_system_sandbox_policy.include_platform_defaults();
|
|
||||||
let mut policy_sections = vec![
|
|
||||||
MACOS_SEATBELT_BASE_POLICY.to_string(),
|
|
||||||
file_read_policy,
|
|
||||||
file_write_policy,
|
|
||||||
network_policy,
|
|
||||||
];
|
|
||||||
if include_platform_defaults {
|
|
||||||
policy_sections.push(MACOS_RESTRICTED_READ_ONLY_PLATFORM_DEFAULTS.to_string());
|
|
||||||
}
|
|
||||||
if !seatbelt_extensions.policy.is_empty() {
|
|
||||||
policy_sections.push(seatbelt_extensions.policy.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
let full_policy = policy_sections.join("\n");
|
|
||||||
|
|
||||||
let dir_params = [
|
|
||||||
file_read_dir_params,
|
|
||||||
file_write_dir_params,
|
|
||||||
macos_dir_params(),
|
|
||||||
unix_socket_dir_params(&proxy),
|
|
||||||
seatbelt_extensions.dir_params,
|
|
||||||
]
|
|
||||||
.concat();
|
|
||||||
|
|
||||||
let mut seatbelt_args: Vec<String> = vec!["-p".to_string(), full_policy];
|
|
||||||
let definition_args = dir_params
|
|
||||||
.into_iter()
|
|
||||||
.map(|(key, value)| format!("-D{key}={value}", value = value.to_string_lossy()));
|
|
||||||
seatbelt_args.extend(definition_args);
|
|
||||||
seatbelt_args.push("--".to_string());
|
|
||||||
seatbelt_args.extend(command);
|
|
||||||
seatbelt_args
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Wraps libc::confstr to return a String.
|
|
||||||
fn confstr(name: libc::c_int) -> Option<String> {
|
|
||||||
let mut buf = vec![0_i8; (libc::PATH_MAX as usize) + 1];
|
|
||||||
let len = unsafe { libc::confstr(name, buf.as_mut_ptr(), buf.len()) };
|
|
||||||
if len == 0 {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
// confstr guarantees NUL-termination when len > 0.
|
|
||||||
let cstr = unsafe { CStr::from_ptr(buf.as_ptr()) };
|
|
||||||
cstr.to_str().ok().map(ToString::to_string)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Wraps confstr to return a canonicalized PathBuf.
|
|
||||||
fn confstr_path(name: libc::c_int) -> Option<PathBuf> {
|
|
||||||
let s = confstr(name)?;
|
|
||||||
let path = PathBuf::from(s);
|
|
||||||
path.canonicalize().ok().or(Some(path))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn macos_dir_params() -> Vec<(String, PathBuf)> {
|
|
||||||
if let Some(p) = confstr_path(libc::_CS_DARWIN_USER_CACHE_DIR) {
|
|
||||||
return vec![("DARWIN_USER_CACHE_DIR".to_string(), p)];
|
|
||||||
}
|
|
||||||
vec![]
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
#[path = "seatbelt_tests.rs"]
|
|
||||||
mod tests;
|
|
||||||
@@ -20,8 +20,6 @@ use crate::protocol::GranularApprovalConfig;
|
|||||||
use crate::protocol::ReadOnlyAccess;
|
use crate::protocol::ReadOnlyAccess;
|
||||||
use crate::protocol::SandboxPolicy;
|
use crate::protocol::SandboxPolicy;
|
||||||
use crate::sandboxing::SandboxPermissions;
|
use crate::sandboxing::SandboxPermissions;
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
use crate::seatbelt::MACOS_PATH_TO_SEATBELT_EXECUTABLE;
|
|
||||||
use crate::skills::SkillMetadata;
|
use crate::skills::SkillMetadata;
|
||||||
use codex_execpolicy::Decision;
|
use codex_execpolicy::Decision;
|
||||||
use codex_execpolicy::Evaluation;
|
use codex_execpolicy::Evaluation;
|
||||||
@@ -40,6 +38,8 @@ use codex_protocol::permissions::FileSystemSandboxPolicy;
|
|||||||
use codex_protocol::permissions::FileSystemSpecialPath;
|
use codex_protocol::permissions::FileSystemSpecialPath;
|
||||||
use codex_protocol::permissions::NetworkSandboxPolicy;
|
use codex_protocol::permissions::NetworkSandboxPolicy;
|
||||||
use codex_protocol::protocol::SkillScope;
|
use codex_protocol::protocol::SkillScope;
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
use codex_sandboxing::seatbelt::MACOS_PATH_TO_SEATBELT_EXECUTABLE;
|
||||||
use codex_shell_escalation::EscalationExecution;
|
use codex_shell_escalation::EscalationExecution;
|
||||||
use codex_shell_escalation::EscalationPermissions;
|
use codex_shell_escalation::EscalationPermissions;
|
||||||
use codex_shell_escalation::ExecResult;
|
use codex_shell_escalation::ExecResult;
|
||||||
|
|||||||
@@ -3,4 +3,9 @@ load("//:defs.bzl", "codex_rust_crate")
|
|||||||
codex_rust_crate(
|
codex_rust_crate(
|
||||||
name = "sandboxing",
|
name = "sandboxing",
|
||||||
crate_name = "codex_sandboxing",
|
crate_name = "codex_sandboxing",
|
||||||
|
compile_data = [
|
||||||
|
"src/restricted_read_only_platform_defaults.sbpl",
|
||||||
|
"src/seatbelt_base_policy.sbpl",
|
||||||
|
"src/seatbelt_network_policy.sbpl",
|
||||||
|
],
|
||||||
)
|
)
|
||||||
@@ -12,8 +12,15 @@ path = "src/lib.rs"
|
|||||||
workspace = true
|
workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
codex-network-proxy = { workspace = true }
|
||||||
codex-protocol = { workspace = true }
|
codex-protocol = { workspace = true }
|
||||||
|
codex-utils-absolute-path = { workspace = true }
|
||||||
|
dirs = { workspace = true }
|
||||||
|
libc = { workspace = true }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
|
tracing = { workspace = true, features = ["log"] }
|
||||||
|
url = { workspace = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
pretty_assertions = { workspace = true }
|
pretty_assertions = { workspace = true }
|
||||||
|
tempfile = { workspace = true }
|
||||||
@@ -1 +1,6 @@
|
|||||||
pub mod landlock;
|
pub mod landlock;
|
||||||
|
pub mod macos_permissions;
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
pub mod seatbelt;
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
mod seatbelt_permissions;
|
||||||
+2
-2
@@ -7,7 +7,7 @@ use codex_protocol::models::MacOsSeatbeltProfileExtensions;
|
|||||||
|
|
||||||
/// Merges macOS seatbelt profile extensions by taking the permissive union of
|
/// Merges macOS seatbelt profile extensions by taking the permissive union of
|
||||||
/// each permission field.
|
/// each permission field.
|
||||||
pub(crate) fn merge_macos_seatbelt_profile_extensions(
|
pub fn merge_macos_seatbelt_profile_extensions(
|
||||||
base: Option<&MacOsSeatbeltProfileExtensions>,
|
base: Option<&MacOsSeatbeltProfileExtensions>,
|
||||||
permissions: Option<&MacOsSeatbeltProfileExtensions>,
|
permissions: Option<&MacOsSeatbeltProfileExtensions>,
|
||||||
) -> Option<MacOsSeatbeltProfileExtensions> {
|
) -> Option<MacOsSeatbeltProfileExtensions> {
|
||||||
@@ -38,7 +38,7 @@ pub(crate) fn merge_macos_seatbelt_profile_extensions(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn intersect_macos_seatbelt_profile_extensions(
|
pub fn intersect_macos_seatbelt_profile_extensions(
|
||||||
requested: Option<MacOsSeatbeltProfileExtensions>,
|
requested: Option<MacOsSeatbeltProfileExtensions>,
|
||||||
granted: Option<MacOsSeatbeltProfileExtensions>,
|
granted: Option<MacOsSeatbeltProfileExtensions>,
|
||||||
) -> Option<MacOsSeatbeltProfileExtensions> {
|
) -> Option<MacOsSeatbeltProfileExtensions> {
|
||||||
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,540 @@
|
|||||||
|
use codex_network_proxy::NetworkProxy;
|
||||||
|
use codex_network_proxy::PROXY_URL_ENV_KEYS;
|
||||||
|
use codex_network_proxy::has_proxy_url_env_vars;
|
||||||
|
use codex_network_proxy::proxy_url_env_value;
|
||||||
|
use codex_protocol::models::MacOsSeatbeltProfileExtensions;
|
||||||
|
use codex_protocol::permissions::FileSystemSandboxPolicy;
|
||||||
|
use codex_protocol::permissions::NetworkSandboxPolicy;
|
||||||
|
use codex_protocol::protocol::SandboxPolicy;
|
||||||
|
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
use std::collections::BTreeSet;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::ffi::CStr;
|
||||||
|
use std::path::Path;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use tracing::warn;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
use crate::seatbelt_permissions::build_seatbelt_extensions;
|
||||||
|
|
||||||
|
const MACOS_SEATBELT_BASE_POLICY: &str = include_str!("seatbelt_base_policy.sbpl");
|
||||||
|
const MACOS_SEATBELT_NETWORK_POLICY: &str = include_str!("seatbelt_network_policy.sbpl");
|
||||||
|
const MACOS_RESTRICTED_READ_ONLY_PLATFORM_DEFAULTS: &str =
|
||||||
|
include_str!("restricted_read_only_platform_defaults.sbpl");
|
||||||
|
|
||||||
|
/// When working with `sandbox-exec`, only consider `sandbox-exec` in `/usr/bin`
|
||||||
|
/// to defend against an attacker trying to inject a malicious version on the
|
||||||
|
/// PATH. If /usr/bin/sandbox-exec has been tampered with, then the attacker
|
||||||
|
/// already has root access.
|
||||||
|
pub const MACOS_PATH_TO_SEATBELT_EXECUTABLE: &str = "/usr/bin/sandbox-exec";
|
||||||
|
|
||||||
|
fn is_loopback_host(host: &str) -> bool {
|
||||||
|
host.eq_ignore_ascii_case("localhost") || host == "127.0.0.1" || host == "::1"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn proxy_scheme_default_port(scheme: &str) -> u16 {
|
||||||
|
match scheme {
|
||||||
|
"https" => 443,
|
||||||
|
"socks5" | "socks5h" | "socks4" | "socks4a" => 1080,
|
||||||
|
_ => 80,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn proxy_loopback_ports_from_env(env: &HashMap<String, String>) -> Vec<u16> {
|
||||||
|
let mut ports = BTreeSet::new();
|
||||||
|
for key in PROXY_URL_ENV_KEYS {
|
||||||
|
let Some(proxy_url) = proxy_url_env_value(env, key) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let trimmed = proxy_url.trim();
|
||||||
|
if trimmed.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let candidate = if trimmed.contains("://") {
|
||||||
|
trimmed.to_string()
|
||||||
|
} else {
|
||||||
|
format!("http://{trimmed}")
|
||||||
|
};
|
||||||
|
let Ok(parsed) = Url::parse(&candidate) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let Some(host) = parsed.host_str() else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if !is_loopback_host(host) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let scheme = parsed.scheme().to_ascii_lowercase();
|
||||||
|
let port = parsed
|
||||||
|
.port()
|
||||||
|
.unwrap_or_else(|| proxy_scheme_default_port(scheme.as_str()));
|
||||||
|
ports.insert(port);
|
||||||
|
}
|
||||||
|
ports.into_iter().collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
struct ProxyPolicyInputs {
|
||||||
|
ports: Vec<u16>,
|
||||||
|
has_proxy_config: bool,
|
||||||
|
allow_local_binding: bool,
|
||||||
|
unix_domain_socket_policy: UnixDomainSocketPolicy,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
// Keep allow-all and allowlist modes disjoint so we don't carry ignored state.
|
||||||
|
enum UnixDomainSocketPolicy {
|
||||||
|
AllowAll,
|
||||||
|
Restricted { allowed: Vec<AbsolutePathBuf> },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for UnixDomainSocketPolicy {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::Restricted { allowed: vec![] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
struct UnixSocketPathParam {
|
||||||
|
index: usize,
|
||||||
|
path: AbsolutePathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn proxy_policy_inputs(network: Option<&NetworkProxy>) -> ProxyPolicyInputs {
|
||||||
|
if let Some(network) = network {
|
||||||
|
let mut env = HashMap::new();
|
||||||
|
network.apply_to_env(&mut env);
|
||||||
|
let unix_domain_socket_policy = if network.dangerously_allow_all_unix_sockets() {
|
||||||
|
UnixDomainSocketPolicy::AllowAll
|
||||||
|
} else {
|
||||||
|
let allowed = network
|
||||||
|
.allow_unix_sockets()
|
||||||
|
.iter()
|
||||||
|
.filter_map(
|
||||||
|
|socket_path| match normalize_path_for_sandbox(Path::new(socket_path)) {
|
||||||
|
Some(path) => Some((path.to_string_lossy().to_string(), path)),
|
||||||
|
None => {
|
||||||
|
warn!(
|
||||||
|
"ignoring network.allow_unix_sockets entry because it could not be normalized: {socket_path}"
|
||||||
|
);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.collect::<BTreeMap<_, _>>()
|
||||||
|
.into_values()
|
||||||
|
.collect();
|
||||||
|
UnixDomainSocketPolicy::Restricted { allowed }
|
||||||
|
};
|
||||||
|
return ProxyPolicyInputs {
|
||||||
|
ports: proxy_loopback_ports_from_env(&env),
|
||||||
|
has_proxy_config: has_proxy_url_env_vars(&env),
|
||||||
|
allow_local_binding: network.allow_local_binding(),
|
||||||
|
unix_domain_socket_policy,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
ProxyPolicyInputs::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn normalize_path_for_sandbox(path: &Path) -> Option<AbsolutePathBuf> {
|
||||||
|
// `AbsolutePathBuf::from_absolute_path()` normalizes relative paths against the current
|
||||||
|
// working directory, so keep the explicit check to avoid silently accepting relative entries.
|
||||||
|
if !path.is_absolute() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let absolute_path = AbsolutePathBuf::from_absolute_path(path).ok()?;
|
||||||
|
let normalized_path = absolute_path
|
||||||
|
.as_path()
|
||||||
|
.canonicalize()
|
||||||
|
.ok()
|
||||||
|
.and_then(|canonical_path| AbsolutePathBuf::from_absolute_path(canonical_path).ok());
|
||||||
|
normalized_path.or(Some(absolute_path))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unix_socket_path_params(proxy: &ProxyPolicyInputs) -> Vec<UnixSocketPathParam> {
|
||||||
|
let mut deduped_paths: BTreeMap<String, AbsolutePathBuf> = BTreeMap::new();
|
||||||
|
let UnixDomainSocketPolicy::Restricted { allowed } = &proxy.unix_domain_socket_policy else {
|
||||||
|
return vec![];
|
||||||
|
};
|
||||||
|
for path in allowed {
|
||||||
|
deduped_paths
|
||||||
|
.entry(path.to_string_lossy().to_string())
|
||||||
|
.or_insert_with(|| path.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
deduped_paths
|
||||||
|
.into_values()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(index, path)| UnixSocketPathParam { index, path })
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unix_socket_path_param_key(index: usize) -> String {
|
||||||
|
format!("UNIX_SOCKET_PATH_{index}")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unix_socket_dir_params(proxy: &ProxyPolicyInputs) -> Vec<(String, PathBuf)> {
|
||||||
|
unix_socket_path_params(proxy)
|
||||||
|
.into_iter()
|
||||||
|
.map(|param| {
|
||||||
|
(
|
||||||
|
unix_socket_path_param_key(param.index),
|
||||||
|
param.path.into_path_buf(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns zero or more complete Seatbelt policy lines for unix socket rules.
|
||||||
|
/// When non-empty, the returned string is newline-terminated so callers can
|
||||||
|
/// append it directly to larger policy blocks.
|
||||||
|
fn unix_socket_policy(proxy: &ProxyPolicyInputs) -> String {
|
||||||
|
let socket_params = unix_socket_path_params(proxy);
|
||||||
|
let has_unix_socket_access = matches!(
|
||||||
|
proxy.unix_domain_socket_policy,
|
||||||
|
UnixDomainSocketPolicy::AllowAll
|
||||||
|
) || !socket_params.is_empty();
|
||||||
|
if !has_unix_socket_access {
|
||||||
|
return String::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut policy = String::new();
|
||||||
|
policy.push_str("(allow system-socket (socket-domain AF_UNIX))\n");
|
||||||
|
if matches!(
|
||||||
|
proxy.unix_domain_socket_policy,
|
||||||
|
UnixDomainSocketPolicy::AllowAll
|
||||||
|
) {
|
||||||
|
// Keep AllowAll genuinely broad here; path qualifiers look narrower
|
||||||
|
// without a clear macOS behavioral benefit.
|
||||||
|
policy.push_str("(allow network-bind (local unix-socket))\n");
|
||||||
|
policy.push_str("(allow network-outbound (remote unix-socket))\n");
|
||||||
|
return policy;
|
||||||
|
}
|
||||||
|
|
||||||
|
for param in socket_params {
|
||||||
|
let key = unix_socket_path_param_key(param.index);
|
||||||
|
// Use subpath so allowlists cover sockets created beneath approved directories.
|
||||||
|
policy.push_str(&format!(
|
||||||
|
"(allow network-bind (local unix-socket (subpath (param \"{key}\"))))\n"
|
||||||
|
));
|
||||||
|
policy.push_str(&format!(
|
||||||
|
"(allow network-outbound (remote unix-socket (subpath (param \"{key}\"))))\n"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
policy
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg_attr(not(test), allow(dead_code))]
|
||||||
|
fn dynamic_network_policy(
|
||||||
|
sandbox_policy: &SandboxPolicy,
|
||||||
|
enforce_managed_network: bool,
|
||||||
|
proxy: &ProxyPolicyInputs,
|
||||||
|
) -> String {
|
||||||
|
dynamic_network_policy_for_network(
|
||||||
|
NetworkSandboxPolicy::from(sandbox_policy),
|
||||||
|
enforce_managed_network,
|
||||||
|
proxy,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dynamic_network_policy_for_network(
|
||||||
|
network_policy: NetworkSandboxPolicy,
|
||||||
|
enforce_managed_network: bool,
|
||||||
|
proxy: &ProxyPolicyInputs,
|
||||||
|
) -> String {
|
||||||
|
let should_use_restricted_network_policy =
|
||||||
|
!proxy.ports.is_empty() || proxy.has_proxy_config || enforce_managed_network;
|
||||||
|
if should_use_restricted_network_policy {
|
||||||
|
let mut policy = String::new();
|
||||||
|
if proxy.allow_local_binding {
|
||||||
|
policy.push_str("; allow loopback local binding and loopback traffic\n");
|
||||||
|
policy.push_str("(allow network-bind (local ip \"localhost:*\"))\n");
|
||||||
|
policy.push_str("(allow network-inbound (local ip \"localhost:*\"))\n");
|
||||||
|
policy.push_str("(allow network-outbound (remote ip \"localhost:*\"))\n");
|
||||||
|
}
|
||||||
|
for port in &proxy.ports {
|
||||||
|
policy.push_str(&format!(
|
||||||
|
"(allow network-outbound (remote ip \"localhost:{port}\"))\n"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let unix_socket_policy = unix_socket_policy(proxy);
|
||||||
|
if !unix_socket_policy.is_empty() {
|
||||||
|
policy.push_str("; allow unix domain sockets for local IPC\n");
|
||||||
|
policy.push_str(&unix_socket_policy);
|
||||||
|
}
|
||||||
|
return format!("{policy}{MACOS_SEATBELT_NETWORK_POLICY}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if proxy.has_proxy_config {
|
||||||
|
// Proxy configuration is present but we could not infer any valid loopback endpoints.
|
||||||
|
// Fail closed to avoid silently widening network access in proxy-enforced sessions.
|
||||||
|
return String::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
if enforce_managed_network {
|
||||||
|
// Managed network requirements are active but no usable proxy endpoints
|
||||||
|
// are available. Fail closed for network access.
|
||||||
|
return String::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
if network_policy.is_enabled() {
|
||||||
|
// No proxy env is configured: retain the existing full-network behavior.
|
||||||
|
format!(
|
||||||
|
"(allow network-outbound)\n(allow network-inbound)\n{MACOS_SEATBELT_NETWORK_POLICY}"
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
String::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn root_absolute_path() -> AbsolutePathBuf {
|
||||||
|
match AbsolutePathBuf::from_absolute_path(Path::new("/")) {
|
||||||
|
Ok(path) => path,
|
||||||
|
Err(err) => panic!("root path must be absolute: {err}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
struct SeatbeltAccessRoot {
|
||||||
|
root: AbsolutePathBuf,
|
||||||
|
excluded_subpaths: Vec<AbsolutePathBuf>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_seatbelt_access_policy(
|
||||||
|
action: &str,
|
||||||
|
param_prefix: &str,
|
||||||
|
roots: Vec<SeatbeltAccessRoot>,
|
||||||
|
) -> (String, Vec<(String, PathBuf)>) {
|
||||||
|
let mut policy_components = Vec::new();
|
||||||
|
let mut params = Vec::new();
|
||||||
|
|
||||||
|
for (index, access_root) in roots.into_iter().enumerate() {
|
||||||
|
let root =
|
||||||
|
normalize_path_for_sandbox(access_root.root.as_path()).unwrap_or(access_root.root);
|
||||||
|
let root_param = format!("{param_prefix}_{index}");
|
||||||
|
params.push((root_param.clone(), root.into_path_buf()));
|
||||||
|
|
||||||
|
if access_root.excluded_subpaths.is_empty() {
|
||||||
|
policy_components.push(format!("(subpath (param \"{root_param}\"))"));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut require_parts = vec![format!("(subpath (param \"{root_param}\"))")];
|
||||||
|
for (excluded_index, excluded_subpath) in
|
||||||
|
access_root.excluded_subpaths.into_iter().enumerate()
|
||||||
|
{
|
||||||
|
let excluded_subpath =
|
||||||
|
normalize_path_for_sandbox(excluded_subpath.as_path()).unwrap_or(excluded_subpath);
|
||||||
|
let excluded_param = format!("{param_prefix}_{index}_RO_{excluded_index}");
|
||||||
|
params.push((excluded_param.clone(), excluded_subpath.into_path_buf()));
|
||||||
|
require_parts.push(format!(
|
||||||
|
"(require-not (subpath (param \"{excluded_param}\")))"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
policy_components.push(format!("(require-all {} )", require_parts.join(" ")));
|
||||||
|
}
|
||||||
|
|
||||||
|
if policy_components.is_empty() {
|
||||||
|
(String::new(), Vec::new())
|
||||||
|
} else {
|
||||||
|
(
|
||||||
|
format!("(allow {action}\n{}\n)", policy_components.join(" ")),
|
||||||
|
params,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg_attr(not(test), allow(dead_code))]
|
||||||
|
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> {
|
||||||
|
create_seatbelt_command_args_for_policies_with_extensions(
|
||||||
|
command,
|
||||||
|
&FileSystemSandboxPolicy::from_legacy_sandbox_policy(sandbox_policy, sandbox_policy_cwd),
|
||||||
|
NetworkSandboxPolicy::from(sandbox_policy),
|
||||||
|
sandbox_policy_cwd,
|
||||||
|
enforce_managed_network,
|
||||||
|
network,
|
||||||
|
extensions,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create_seatbelt_command_args_for_policies_with_extensions(
|
||||||
|
command: Vec<String>,
|
||||||
|
file_system_sandbox_policy: &FileSystemSandboxPolicy,
|
||||||
|
network_sandbox_policy: NetworkSandboxPolicy,
|
||||||
|
sandbox_policy_cwd: &Path,
|
||||||
|
enforce_managed_network: bool,
|
||||||
|
network: Option<&NetworkProxy>,
|
||||||
|
extensions: Option<&MacOsSeatbeltProfileExtensions>,
|
||||||
|
) -> Vec<String> {
|
||||||
|
let unreadable_roots =
|
||||||
|
file_system_sandbox_policy.get_unreadable_roots_with_cwd(sandbox_policy_cwd);
|
||||||
|
let (file_write_policy, file_write_dir_params) =
|
||||||
|
if file_system_sandbox_policy.has_full_disk_write_access() {
|
||||||
|
if unreadable_roots.is_empty() {
|
||||||
|
// Allegedly, this is more permissive than `(allow file-write*)`.
|
||||||
|
(
|
||||||
|
r#"(allow file-write* (regex #"^/"))"#.to_string(),
|
||||||
|
Vec::new(),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
build_seatbelt_access_policy(
|
||||||
|
"file-write*",
|
||||||
|
"WRITABLE_ROOT",
|
||||||
|
vec![SeatbeltAccessRoot {
|
||||||
|
root: root_absolute_path(),
|
||||||
|
excluded_subpaths: unreadable_roots.clone(),
|
||||||
|
}],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
build_seatbelt_access_policy(
|
||||||
|
"file-write*",
|
||||||
|
"WRITABLE_ROOT",
|
||||||
|
file_system_sandbox_policy
|
||||||
|
.get_writable_roots_with_cwd(sandbox_policy_cwd)
|
||||||
|
.into_iter()
|
||||||
|
.map(|root| SeatbeltAccessRoot {
|
||||||
|
root: root.root,
|
||||||
|
excluded_subpaths: root.read_only_subpaths,
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
let (file_read_policy, file_read_dir_params) =
|
||||||
|
if file_system_sandbox_policy.has_full_disk_read_access() {
|
||||||
|
if unreadable_roots.is_empty() {
|
||||||
|
(
|
||||||
|
"; allow read-only file operations\n(allow file-read*)".to_string(),
|
||||||
|
Vec::new(),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
let (policy, params) = build_seatbelt_access_policy(
|
||||||
|
"file-read*",
|
||||||
|
"READABLE_ROOT",
|
||||||
|
vec![SeatbeltAccessRoot {
|
||||||
|
root: root_absolute_path(),
|
||||||
|
excluded_subpaths: unreadable_roots,
|
||||||
|
}],
|
||||||
|
);
|
||||||
|
(
|
||||||
|
format!("; allow read-only file operations\n{policy}"),
|
||||||
|
params,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let (policy, params) = build_seatbelt_access_policy(
|
||||||
|
"file-read*",
|
||||||
|
"READABLE_ROOT",
|
||||||
|
file_system_sandbox_policy
|
||||||
|
.get_readable_roots_with_cwd(sandbox_policy_cwd)
|
||||||
|
.into_iter()
|
||||||
|
.map(|root| SeatbeltAccessRoot {
|
||||||
|
excluded_subpaths: unreadable_roots
|
||||||
|
.iter()
|
||||||
|
.filter(|path| path.as_path().starts_with(root.as_path()))
|
||||||
|
.cloned()
|
||||||
|
.collect(),
|
||||||
|
root,
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
);
|
||||||
|
if policy.is_empty() {
|
||||||
|
(String::new(), params)
|
||||||
|
} else {
|
||||||
|
(
|
||||||
|
format!("; allow read-only file operations\n{policy}"),
|
||||||
|
params,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let proxy = proxy_policy_inputs(network);
|
||||||
|
let network_policy =
|
||||||
|
dynamic_network_policy_for_network(network_sandbox_policy, enforce_managed_network, &proxy);
|
||||||
|
let seatbelt_extensions = extensions.map_or_else(
|
||||||
|
|| {
|
||||||
|
// Backward-compatibility default when no extension profile is provided.
|
||||||
|
build_seatbelt_extensions(&MacOsSeatbeltProfileExtensions::default())
|
||||||
|
},
|
||||||
|
build_seatbelt_extensions,
|
||||||
|
);
|
||||||
|
|
||||||
|
let include_platform_defaults = file_system_sandbox_policy.include_platform_defaults();
|
||||||
|
let mut policy_sections = vec![
|
||||||
|
MACOS_SEATBELT_BASE_POLICY.to_string(),
|
||||||
|
file_read_policy,
|
||||||
|
file_write_policy,
|
||||||
|
network_policy,
|
||||||
|
];
|
||||||
|
if include_platform_defaults {
|
||||||
|
policy_sections.push(MACOS_RESTRICTED_READ_ONLY_PLATFORM_DEFAULTS.to_string());
|
||||||
|
}
|
||||||
|
if !seatbelt_extensions.policy.is_empty() {
|
||||||
|
policy_sections.push(seatbelt_extensions.policy.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
let full_policy = policy_sections.join("\n");
|
||||||
|
|
||||||
|
let dir_params = [
|
||||||
|
file_read_dir_params,
|
||||||
|
file_write_dir_params,
|
||||||
|
macos_dir_params(),
|
||||||
|
unix_socket_dir_params(&proxy),
|
||||||
|
seatbelt_extensions.dir_params,
|
||||||
|
]
|
||||||
|
.concat();
|
||||||
|
|
||||||
|
let mut seatbelt_args: Vec<String> = vec!["-p".to_string(), full_policy];
|
||||||
|
let definition_args = dir_params
|
||||||
|
.into_iter()
|
||||||
|
.map(|(key, value): (String, PathBuf)| {
|
||||||
|
format!("-D{key}={value}", value = value.to_string_lossy())
|
||||||
|
});
|
||||||
|
seatbelt_args.extend(definition_args);
|
||||||
|
seatbelt_args.push("--".to_string());
|
||||||
|
seatbelt_args.extend(command);
|
||||||
|
seatbelt_args
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wraps libc::confstr to return a String.
|
||||||
|
fn confstr(name: libc::c_int) -> Option<String> {
|
||||||
|
let mut buf = vec![0_i8; (libc::PATH_MAX as usize) + 1];
|
||||||
|
let len = unsafe { libc::confstr(name, buf.as_mut_ptr(), buf.len()) };
|
||||||
|
if len == 0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
// confstr guarantees NUL-termination when len > 0.
|
||||||
|
let cstr = unsafe { CStr::from_ptr(buf.as_ptr()) };
|
||||||
|
cstr.to_str().ok().map(ToString::to_string)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wraps confstr to return a canonicalized PathBuf.
|
||||||
|
fn confstr_path(name: libc::c_int) -> Option<PathBuf> {
|
||||||
|
let s = confstr(name)?;
|
||||||
|
let path = PathBuf::from(s);
|
||||||
|
path.canonicalize().ok().or(Some(path))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn macos_dir_params() -> Vec<(String, PathBuf)> {
|
||||||
|
if let Some(p) = confstr_path(libc::_CS_DARWIN_USER_CACHE_DIR) {
|
||||||
|
return vec![("DARWIN_USER_CACHE_DIR".to_string(), p)];
|
||||||
|
}
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[path = "seatbelt_tests.rs"]
|
||||||
|
mod tests;
|
||||||
File renamed without changes.
File renamed without changes.
+4
-6
@@ -1,12 +1,10 @@
|
|||||||
#![cfg(target_os = "macos")]
|
|
||||||
|
|
||||||
use std::collections::BTreeSet;
|
use std::collections::BTreeSet;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
pub use codex_protocol::models::MacOsAutomationPermission;
|
use codex_protocol::models::MacOsAutomationPermission;
|
||||||
pub use codex_protocol::models::MacOsContactsPermission;
|
use codex_protocol::models::MacOsContactsPermission;
|
||||||
pub use codex_protocol::models::MacOsPreferencesPermission;
|
use codex_protocol::models::MacOsPreferencesPermission;
|
||||||
pub use codex_protocol::models::MacOsSeatbeltProfileExtensions;
|
use codex_protocol::models::MacOsSeatbeltProfileExtensions;
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||||
pub(crate) struct SeatbeltExtensionPolicy {
|
pub(crate) struct SeatbeltExtensionPolicy {
|
||||||
File renamed without changes.
@@ -1,7 +1,7 @@
|
|||||||
|
use super::MACOS_PATH_TO_SEATBELT_EXECUTABLE;
|
||||||
use super::MACOS_SEATBELT_BASE_POLICY;
|
use super::MACOS_SEATBELT_BASE_POLICY;
|
||||||
use super::ProxyPolicyInputs;
|
use super::ProxyPolicyInputs;
|
||||||
use super::UnixDomainSocketPolicy;
|
use super::UnixDomainSocketPolicy;
|
||||||
use super::create_seatbelt_command_args;
|
|
||||||
use super::create_seatbelt_command_args_for_policies_with_extensions;
|
use super::create_seatbelt_command_args_for_policies_with_extensions;
|
||||||
use super::create_seatbelt_command_args_with_extensions;
|
use super::create_seatbelt_command_args_with_extensions;
|
||||||
use super::dynamic_network_policy;
|
use super::dynamic_network_policy;
|
||||||
@@ -9,18 +9,18 @@ use super::macos_dir_params;
|
|||||||
use super::normalize_path_for_sandbox;
|
use super::normalize_path_for_sandbox;
|
||||||
use super::unix_socket_dir_params;
|
use super::unix_socket_dir_params;
|
||||||
use super::unix_socket_policy;
|
use super::unix_socket_policy;
|
||||||
use crate::protocol::ReadOnlyAccess;
|
use codex_protocol::models::MacOsAutomationPermission;
|
||||||
use crate::protocol::SandboxPolicy;
|
use codex_protocol::models::MacOsContactsPermission;
|
||||||
use crate::seatbelt::MACOS_PATH_TO_SEATBELT_EXECUTABLE;
|
use codex_protocol::models::MacOsPreferencesPermission;
|
||||||
use crate::seatbelt_permissions::MacOsAutomationPermission;
|
use codex_protocol::models::MacOsSeatbeltProfileExtensions;
|
||||||
use crate::seatbelt_permissions::MacOsContactsPermission;
|
|
||||||
use crate::seatbelt_permissions::MacOsPreferencesPermission;
|
|
||||||
use crate::seatbelt_permissions::MacOsSeatbeltProfileExtensions;
|
|
||||||
use codex_protocol::permissions::FileSystemAccessMode;
|
use codex_protocol::permissions::FileSystemAccessMode;
|
||||||
use codex_protocol::permissions::FileSystemPath;
|
use codex_protocol::permissions::FileSystemPath;
|
||||||
use codex_protocol::permissions::FileSystemSandboxEntry;
|
use codex_protocol::permissions::FileSystemSandboxEntry;
|
||||||
use codex_protocol::permissions::FileSystemSandboxPolicy;
|
use codex_protocol::permissions::FileSystemSandboxPolicy;
|
||||||
|
use codex_protocol::permissions::FileSystemSpecialPath;
|
||||||
use codex_protocol::permissions::NetworkSandboxPolicy;
|
use codex_protocol::permissions::NetworkSandboxPolicy;
|
||||||
|
use codex_protocol::protocol::ReadOnlyAccess;
|
||||||
|
use codex_protocol::protocol::SandboxPolicy;
|
||||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
@@ -105,7 +105,7 @@ fn explicit_unreadable_paths_are_excluded_from_full_disk_read_and_write_access()
|
|||||||
let file_system_policy = FileSystemSandboxPolicy::restricted(vec![
|
let file_system_policy = FileSystemSandboxPolicy::restricted(vec![
|
||||||
FileSystemSandboxEntry {
|
FileSystemSandboxEntry {
|
||||||
path: FileSystemPath::Special {
|
path: FileSystemPath::Special {
|
||||||
value: crate::protocol::FileSystemSpecialPath::Root,
|
value: FileSystemSpecialPath::Root,
|
||||||
},
|
},
|
||||||
access: FileSystemAccessMode::Write,
|
access: FileSystemAccessMode::Write,
|
||||||
},
|
},
|
||||||
@@ -277,12 +277,13 @@ sys.exit(0 if allowed else 13)
|
|||||||
#[test]
|
#[test]
|
||||||
fn seatbelt_args_without_extension_profile_keep_legacy_preferences_read_access() {
|
fn seatbelt_args_without_extension_profile_keep_legacy_preferences_read_access() {
|
||||||
let cwd = std::env::temp_dir();
|
let cwd = std::env::temp_dir();
|
||||||
let args = create_seatbelt_command_args(
|
let args = create_seatbelt_command_args_with_extensions(
|
||||||
vec!["echo".to_string(), "ok".to_string()],
|
vec!["echo".to_string(), "ok".to_string()],
|
||||||
&SandboxPolicy::new_read_only_policy(),
|
&SandboxPolicy::new_read_only_policy(),
|
||||||
cwd.as_path(),
|
cwd.as_path(),
|
||||||
false,
|
/*enforce_managed_network*/ false,
|
||||||
None,
|
None,
|
||||||
|
/*extensions*/ None,
|
||||||
);
|
);
|
||||||
let policy = &args[1];
|
let policy = &args[1];
|
||||||
assert!(policy.contains("(allow user-preference-read)"));
|
assert!(policy.contains("(allow user-preference-read)"));
|
||||||
@@ -295,7 +296,7 @@ fn seatbelt_legacy_workspace_write_nested_readable_root_stays_writable() {
|
|||||||
let cwd = tmp.path().join("workspace");
|
let cwd = tmp.path().join("workspace");
|
||||||
fs::create_dir_all(cwd.join("docs")).expect("create docs");
|
fs::create_dir_all(cwd.join("docs")).expect("create docs");
|
||||||
let docs = AbsolutePathBuf::from_absolute_path(cwd.join("docs")).expect("absolute docs");
|
let docs = AbsolutePathBuf::from_absolute_path(cwd.join("docs")).expect("absolute docs");
|
||||||
let args = create_seatbelt_command_args(
|
let args = create_seatbelt_command_args_with_extensions(
|
||||||
vec!["/bin/true".to_string()],
|
vec!["/bin/true".to_string()],
|
||||||
&SandboxPolicy::WorkspaceWrite {
|
&SandboxPolicy::WorkspaceWrite {
|
||||||
writable_roots: vec![],
|
writable_roots: vec![],
|
||||||
@@ -308,8 +309,9 @@ fn seatbelt_legacy_workspace_write_nested_readable_root_stays_writable() {
|
|||||||
exclude_slash_tmp: true,
|
exclude_slash_tmp: true,
|
||||||
},
|
},
|
||||||
cwd.as_path(),
|
cwd.as_path(),
|
||||||
false,
|
/*enforce_managed_network*/ false,
|
||||||
None,
|
None,
|
||||||
|
/*extensions*/ None,
|
||||||
);
|
);
|
||||||
|
|
||||||
let docs_param = format!("-DWRITABLE_ROOT_0_RO_0={}", docs.as_path().display());
|
let docs_param = format!("-DWRITABLE_ROOT_0_RO_0={}", docs.as_path().display());
|
||||||
@@ -636,7 +638,14 @@ fn create_seatbelt_args_with_read_only_git_and_codex_subpaths() {
|
|||||||
.iter()
|
.iter()
|
||||||
.map(std::string::ToString::to_string)
|
.map(std::string::ToString::to_string)
|
||||||
.collect();
|
.collect();
|
||||||
let args = create_seatbelt_command_args(shell_command.clone(), &policy, &cwd, false, None);
|
let args = create_seatbelt_command_args_with_extensions(
|
||||||
|
shell_command.clone(),
|
||||||
|
&policy,
|
||||||
|
&cwd,
|
||||||
|
/*enforce_managed_network*/ false,
|
||||||
|
None,
|
||||||
|
/*extensions*/ None,
|
||||||
|
);
|
||||||
|
|
||||||
// Build the expected policy text using a raw string for readability.
|
// Build the expected policy text using a raw string for readability.
|
||||||
// Note that the policy includes:
|
// Note that the policy includes:
|
||||||
@@ -740,8 +749,14 @@ fn create_seatbelt_args_with_read_only_git_and_codex_subpaths() {
|
|||||||
.iter()
|
.iter()
|
||||||
.map(std::string::ToString::to_string)
|
.map(std::string::ToString::to_string)
|
||||||
.collect();
|
.collect();
|
||||||
let write_hooks_file_args =
|
let write_hooks_file_args = create_seatbelt_command_args_with_extensions(
|
||||||
create_seatbelt_command_args(shell_command_git, &policy, &cwd, false, None);
|
shell_command_git,
|
||||||
|
&policy,
|
||||||
|
&cwd,
|
||||||
|
/*enforce_managed_network*/ false,
|
||||||
|
None,
|
||||||
|
/*extensions*/ None,
|
||||||
|
);
|
||||||
let output = Command::new(MACOS_PATH_TO_SEATBELT_EXECUTABLE)
|
let output = Command::new(MACOS_PATH_TO_SEATBELT_EXECUTABLE)
|
||||||
.args(&write_hooks_file_args)
|
.args(&write_hooks_file_args)
|
||||||
.current_dir(&cwd)
|
.current_dir(&cwd)
|
||||||
@@ -771,8 +786,14 @@ fn create_seatbelt_args_with_read_only_git_and_codex_subpaths() {
|
|||||||
.iter()
|
.iter()
|
||||||
.map(std::string::ToString::to_string)
|
.map(std::string::ToString::to_string)
|
||||||
.collect();
|
.collect();
|
||||||
let write_allowed_file_args =
|
let write_allowed_file_args = create_seatbelt_command_args_with_extensions(
|
||||||
create_seatbelt_command_args(shell_command_allowed, &policy, &cwd, false, None);
|
shell_command_allowed,
|
||||||
|
&policy,
|
||||||
|
&cwd,
|
||||||
|
/*enforce_managed_network*/ false,
|
||||||
|
None,
|
||||||
|
/*extensions*/ None,
|
||||||
|
);
|
||||||
let output = Command::new(MACOS_PATH_TO_SEATBELT_EXECUTABLE)
|
let output = Command::new(MACOS_PATH_TO_SEATBELT_EXECUTABLE)
|
||||||
.args(&write_allowed_file_args)
|
.args(&write_allowed_file_args)
|
||||||
.current_dir(&cwd)
|
.current_dir(&cwd)
|
||||||
@@ -833,7 +854,14 @@ fn create_seatbelt_args_with_read_only_git_pointer_file() {
|
|||||||
.iter()
|
.iter()
|
||||||
.map(std::string::ToString::to_string)
|
.map(std::string::ToString::to_string)
|
||||||
.collect();
|
.collect();
|
||||||
let args = create_seatbelt_command_args(shell_command, &policy, &cwd, false, None);
|
let args = create_seatbelt_command_args_with_extensions(
|
||||||
|
shell_command,
|
||||||
|
&policy,
|
||||||
|
&cwd,
|
||||||
|
/*enforce_managed_network*/ false,
|
||||||
|
None,
|
||||||
|
/*extensions*/ None,
|
||||||
|
);
|
||||||
|
|
||||||
let output = Command::new(MACOS_PATH_TO_SEATBELT_EXECUTABLE)
|
let output = Command::new(MACOS_PATH_TO_SEATBELT_EXECUTABLE)
|
||||||
.args(&args)
|
.args(&args)
|
||||||
@@ -863,8 +891,14 @@ fn create_seatbelt_args_with_read_only_git_pointer_file() {
|
|||||||
.iter()
|
.iter()
|
||||||
.map(std::string::ToString::to_string)
|
.map(std::string::ToString::to_string)
|
||||||
.collect();
|
.collect();
|
||||||
let gitdir_args =
|
let gitdir_args = create_seatbelt_command_args_with_extensions(
|
||||||
create_seatbelt_command_args(shell_command_gitdir, &policy, &cwd, false, None);
|
shell_command_gitdir,
|
||||||
|
&policy,
|
||||||
|
&cwd,
|
||||||
|
/*enforce_managed_network*/ false,
|
||||||
|
None,
|
||||||
|
/*extensions*/ None,
|
||||||
|
);
|
||||||
let output = Command::new(MACOS_PATH_TO_SEATBELT_EXECUTABLE)
|
let output = Command::new(MACOS_PATH_TO_SEATBELT_EXECUTABLE)
|
||||||
.args(&gitdir_args)
|
.args(&gitdir_args)
|
||||||
.current_dir(&cwd)
|
.current_dir(&cwd)
|
||||||
@@ -921,12 +955,13 @@ fn create_seatbelt_args_for_cwd_as_git_repo() {
|
|||||||
.iter()
|
.iter()
|
||||||
.map(std::string::ToString::to_string)
|
.map(std::string::ToString::to_string)
|
||||||
.collect();
|
.collect();
|
||||||
let args = create_seatbelt_command_args(
|
let args = create_seatbelt_command_args_with_extensions(
|
||||||
shell_command.clone(),
|
shell_command.clone(),
|
||||||
&policy,
|
&policy,
|
||||||
vulnerable_root.as_path(),
|
vulnerable_root.as_path(),
|
||||||
false,
|
/*enforce_managed_network*/ false,
|
||||||
None,
|
None,
|
||||||
|
/*extensions*/ None,
|
||||||
);
|
);
|
||||||
|
|
||||||
let tmpdir_env_var = std::env::var("TMPDIR")
|
let tmpdir_env_var = std::env::var("TMPDIR")
|
||||||
Reference in new issue
Block a user