mirror of
https://github.com/openai/codex.git
synced 2026-03-06 22:53:23 +00:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8ede18011a | ||
|
|
9a4787c240 |
23
.github/workflows/rust-release.yml
vendored
23
.github/workflows/rust-release.yml
vendored
@@ -643,6 +643,29 @@ jobs:
|
||||
exit "${publish_status}"
|
||||
done
|
||||
|
||||
winget:
|
||||
name: winget
|
||||
needs: release
|
||||
# Only publish stable/mainline releases to WinGet; pre-releases include a
|
||||
# '-' in the semver string (e.g., 1.2.3-alpha.1).
|
||||
if: ${{ !contains(needs.release.outputs.version, '-') }}
|
||||
# This job only invokes a GitHub Action to open/update the winget-pkgs PR;
|
||||
# it does not execute Windows-only tooling, so Linux is sufficient.
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- name: Publish to WinGet
|
||||
uses: vedantmgoyal9/winget-releaser@19e706d4c9121098010096f9c495a70a7518b30f
|
||||
with:
|
||||
identifier: OpenAI.Codex
|
||||
version: ${{ needs.release.outputs.version }}
|
||||
release-tag: ${{ needs.release.outputs.tag }}
|
||||
fork-user: openai-oss-forks
|
||||
installers-regex: '^codex-(?:x86_64|aarch64)-pc-windows-msvc\.exe\.zip$'
|
||||
token: ${{ secrets.WINGET_PUBLISH_PAT }}
|
||||
|
||||
update-branch:
|
||||
name: Update latest-alpha-cli branch
|
||||
permissions:
|
||||
|
||||
@@ -1534,19 +1534,9 @@ impl CodexMessageProcessor {
|
||||
};
|
||||
|
||||
let requested_policy = params.sandbox_policy.map(|policy| policy.to_core());
|
||||
let (
|
||||
effective_policy,
|
||||
effective_file_system_sandbox_policy,
|
||||
effective_network_sandbox_policy,
|
||||
) = match requested_policy {
|
||||
let effective_policy = match requested_policy {
|
||||
Some(policy) => match self.config.permissions.sandbox_policy.can_set(&policy) {
|
||||
Ok(()) => {
|
||||
let file_system_sandbox_policy =
|
||||
codex_protocol::protocol::FileSystemSandboxPolicy::from(&policy);
|
||||
let network_sandbox_policy =
|
||||
codex_protocol::protocol::NetworkSandboxPolicy::from(&policy);
|
||||
(policy, file_system_sandbox_policy, network_sandbox_policy)
|
||||
}
|
||||
Ok(()) => policy,
|
||||
Err(err) => {
|
||||
let error = JSONRPCErrorError {
|
||||
code: INVALID_REQUEST_ERROR_CODE,
|
||||
@@ -1557,11 +1547,7 @@ impl CodexMessageProcessor {
|
||||
return;
|
||||
}
|
||||
},
|
||||
None => (
|
||||
self.config.permissions.sandbox_policy.get().clone(),
|
||||
self.config.permissions.file_system_sandbox_policy.clone(),
|
||||
self.config.permissions.network_sandbox_policy,
|
||||
),
|
||||
None => self.config.permissions.sandbox_policy.get().clone(),
|
||||
};
|
||||
|
||||
let codex_linux_sandbox_exe = self.arg0_paths.codex_linux_sandbox_exe.clone();
|
||||
@@ -1576,8 +1562,6 @@ impl CodexMessageProcessor {
|
||||
match codex_core::exec::process_exec_tool_call(
|
||||
exec_params,
|
||||
&effective_policy,
|
||||
&effective_file_system_sandbox_policy,
|
||||
effective_network_sandbox_policy,
|
||||
sandbox_cwd.as_path(),
|
||||
&codex_linux_sandbox_exe,
|
||||
use_linux_sandbox_bwrap,
|
||||
|
||||
@@ -568,30 +568,6 @@
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"FileSystemAccessMode": {
|
||||
"enum": [
|
||||
"none",
|
||||
"read",
|
||||
"write"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"FilesystemPermissionToml": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/FileSystemAccessMode"
|
||||
},
|
||||
{
|
||||
"additionalProperties": {
|
||||
"$ref": "#/definitions/FileSystemAccessMode"
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"FilesystemPermissionsToml": {
|
||||
"type": "object"
|
||||
},
|
||||
"ForcedLoginMethod": {
|
||||
"enum": [
|
||||
"chatgpt",
|
||||
@@ -1113,28 +1089,18 @@
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"PermissionProfileNetworkToml": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"PermissionProfileToml": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"filesystem": {
|
||||
"$ref": "#/definitions/FilesystemPermissionsToml"
|
||||
},
|
||||
"network": {
|
||||
"$ref": "#/definitions/PermissionProfileNetworkToml"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"PermissionsToml": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"network": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/NetworkToml"
|
||||
}
|
||||
],
|
||||
"description": "Network proxy settings from `[permissions.network]`. User config can enable the proxy; managed requirements may still constrain values."
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"Personality": {
|
||||
@@ -1698,10 +1664,6 @@
|
||||
"description": "Compact prompt used for history compaction.",
|
||||
"type": "string"
|
||||
},
|
||||
"default_permissions": {
|
||||
"description": "Default named permissions profile to apply from the `[permissions]` table.",
|
||||
"type": "string"
|
||||
},
|
||||
"developer_instructions": {
|
||||
"default": null,
|
||||
"description": "Developer instructions inserted as a `developer` role message.",
|
||||
@@ -2070,15 +2032,6 @@
|
||||
],
|
||||
"description": "Optional verbosity control for GPT-5 models (Responses API `text.verbosity`)."
|
||||
},
|
||||
"network": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/NetworkToml"
|
||||
}
|
||||
],
|
||||
"default": null,
|
||||
"description": "Top-level network proxy settings."
|
||||
},
|
||||
"notice": {
|
||||
"allOf": [
|
||||
{
|
||||
@@ -2114,7 +2067,7 @@
|
||||
}
|
||||
],
|
||||
"default": null,
|
||||
"description": "Named permissions profiles."
|
||||
"description": "Nested permissions settings."
|
||||
},
|
||||
"personality": {
|
||||
"allOf": [
|
||||
|
||||
@@ -40,7 +40,6 @@ pub(crate) async fn apply_patch(
|
||||
&action,
|
||||
turn_context.approval_policy.value(),
|
||||
turn_context.sandbox_policy.get(),
|
||||
&turn_context.file_system_sandbox_policy,
|
||||
&turn_context.cwd,
|
||||
turn_context.windows_sandbox_level,
|
||||
) {
|
||||
|
||||
@@ -223,12 +223,10 @@ use crate::protocol::ErrorEvent;
|
||||
use crate::protocol::Event;
|
||||
use crate::protocol::EventMsg;
|
||||
use crate::protocol::ExecApprovalRequestEvent;
|
||||
use crate::protocol::FileSystemSandboxPolicy;
|
||||
use crate::protocol::McpServerRefreshConfig;
|
||||
use crate::protocol::ModelRerouteEvent;
|
||||
use crate::protocol::ModelRerouteReason;
|
||||
use crate::protocol::NetworkApprovalContext;
|
||||
use crate::protocol::NetworkSandboxPolicy;
|
||||
use crate::protocol::Op;
|
||||
use crate::protocol::PlanDeltaEvent;
|
||||
use crate::protocol::RateLimitSnapshot;
|
||||
@@ -490,8 +488,6 @@ impl Codex {
|
||||
compact_prompt: config.compact_prompt.clone(),
|
||||
approval_policy: config.permissions.approval_policy.clone(),
|
||||
sandbox_policy: config.permissions.sandbox_policy.clone(),
|
||||
file_system_sandbox_policy: config.permissions.file_system_sandbox_policy.clone(),
|
||||
network_sandbox_policy: config.permissions.network_sandbox_policy,
|
||||
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
|
||||
cwd: config.cwd.clone(),
|
||||
codex_home: config.codex_home.clone(),
|
||||
@@ -687,8 +683,6 @@ pub(crate) struct TurnContext {
|
||||
pub(crate) personality: Option<Personality>,
|
||||
pub(crate) approval_policy: Constrained<AskForApproval>,
|
||||
pub(crate) sandbox_policy: Constrained<SandboxPolicy>,
|
||||
pub(crate) file_system_sandbox_policy: FileSystemSandboxPolicy,
|
||||
pub(crate) network_sandbox_policy: NetworkSandboxPolicy,
|
||||
pub(crate) network: Option<NetworkProxy>,
|
||||
pub(crate) windows_sandbox_level: WindowsSandboxLevel,
|
||||
pub(crate) shell_environment_policy: ShellEnvironmentPolicy,
|
||||
@@ -779,8 +773,6 @@ impl TurnContext {
|
||||
personality: self.personality,
|
||||
approval_policy: self.approval_policy.clone(),
|
||||
sandbox_policy: self.sandbox_policy.clone(),
|
||||
file_system_sandbox_policy: self.file_system_sandbox_policy.clone(),
|
||||
network_sandbox_policy: self.network_sandbox_policy,
|
||||
network: self.network.clone(),
|
||||
windows_sandbox_level: self.windows_sandbox_level,
|
||||
shell_environment_policy: self.shell_environment_policy.clone(),
|
||||
@@ -886,8 +878,6 @@ pub(crate) struct SessionConfiguration {
|
||||
approval_policy: Constrained<AskForApproval>,
|
||||
/// How to sandbox commands executed in the system
|
||||
sandbox_policy: Constrained<SandboxPolicy>,
|
||||
file_system_sandbox_policy: FileSystemSandboxPolicy,
|
||||
network_sandbox_policy: NetworkSandboxPolicy,
|
||||
windows_sandbox_level: WindowsSandboxLevel,
|
||||
|
||||
/// Working directory that should be treated as the *root* of the
|
||||
@@ -954,10 +944,6 @@ impl SessionConfiguration {
|
||||
}
|
||||
if let Some(sandbox_policy) = updates.sandbox_policy.clone() {
|
||||
next_configuration.sandbox_policy.set(sandbox_policy)?;
|
||||
next_configuration.file_system_sandbox_policy =
|
||||
FileSystemSandboxPolicy::from(next_configuration.sandbox_policy.get());
|
||||
next_configuration.network_sandbox_policy =
|
||||
NetworkSandboxPolicy::from(next_configuration.sandbox_policy.get());
|
||||
}
|
||||
if let Some(windows_sandbox_level) = updates.windows_sandbox_level {
|
||||
next_configuration.windows_sandbox_level = windows_sandbox_level;
|
||||
@@ -1170,8 +1156,6 @@ impl Session {
|
||||
personality: session_configuration.personality,
|
||||
approval_policy: session_configuration.approval_policy.clone(),
|
||||
sandbox_policy: session_configuration.sandbox_policy.clone(),
|
||||
file_system_sandbox_policy: session_configuration.file_system_sandbox_policy.clone(),
|
||||
network_sandbox_policy: session_configuration.network_sandbox_policy,
|
||||
network,
|
||||
windows_sandbox_level: session_configuration.windows_sandbox_level,
|
||||
shell_environment_policy: per_turn_config.permissions.shell_environment_policy.clone(),
|
||||
@@ -4999,8 +4983,6 @@ async fn spawn_review_thread(
|
||||
personality: parent_turn_context.personality,
|
||||
approval_policy: parent_turn_context.approval_policy.clone(),
|
||||
sandbox_policy: parent_turn_context.sandbox_policy.clone(),
|
||||
file_system_sandbox_policy: parent_turn_context.file_system_sandbox_policy.clone(),
|
||||
network_sandbox_policy: parent_turn_context.network_sandbox_policy,
|
||||
network: parent_turn_context.network.clone(),
|
||||
windows_sandbox_level: parent_turn_context.windows_sandbox_level,
|
||||
shell_environment_policy: parent_turn_context.shell_environment_policy.clone(),
|
||||
|
||||
@@ -1416,8 +1416,6 @@ async fn set_rate_limits_retains_previous_credits() {
|
||||
compact_prompt: config.compact_prompt.clone(),
|
||||
approval_policy: config.permissions.approval_policy.clone(),
|
||||
sandbox_policy: config.permissions.sandbox_policy.clone(),
|
||||
file_system_sandbox_policy: config.permissions.file_system_sandbox_policy.clone(),
|
||||
network_sandbox_policy: config.permissions.network_sandbox_policy,
|
||||
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
|
||||
cwd: config.cwd.clone(),
|
||||
codex_home: config.codex_home.clone(),
|
||||
@@ -1512,8 +1510,6 @@ async fn set_rate_limits_updates_plan_type_when_present() {
|
||||
compact_prompt: config.compact_prompt.clone(),
|
||||
approval_policy: config.permissions.approval_policy.clone(),
|
||||
sandbox_policy: config.permissions.sandbox_policy.clone(),
|
||||
file_system_sandbox_policy: config.permissions.file_system_sandbox_policy.clone(),
|
||||
network_sandbox_policy: config.permissions.network_sandbox_policy,
|
||||
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
|
||||
cwd: config.cwd.clone(),
|
||||
codex_home: config.codex_home.clone(),
|
||||
@@ -1866,8 +1862,6 @@ pub(crate) async fn make_session_configuration_for_tests() -> SessionConfigurati
|
||||
compact_prompt: config.compact_prompt.clone(),
|
||||
approval_policy: config.permissions.approval_policy.clone(),
|
||||
sandbox_policy: config.permissions.sandbox_policy.clone(),
|
||||
file_system_sandbox_policy: config.permissions.file_system_sandbox_policy.clone(),
|
||||
network_sandbox_policy: config.permissions.network_sandbox_policy,
|
||||
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
|
||||
cwd: config.cwd.clone(),
|
||||
codex_home: config.codex_home.clone(),
|
||||
@@ -1925,8 +1919,6 @@ async fn session_new_fails_when_zsh_fork_enabled_without_zsh_path() {
|
||||
compact_prompt: config.compact_prompt.clone(),
|
||||
approval_policy: config.permissions.approval_policy.clone(),
|
||||
sandbox_policy: config.permissions.sandbox_policy.clone(),
|
||||
file_system_sandbox_policy: config.permissions.file_system_sandbox_policy.clone(),
|
||||
network_sandbox_policy: config.permissions.network_sandbox_policy,
|
||||
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
|
||||
cwd: config.cwd.clone(),
|
||||
codex_home: config.codex_home.clone(),
|
||||
@@ -2017,8 +2009,6 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
|
||||
compact_prompt: config.compact_prompt.clone(),
|
||||
approval_policy: config.permissions.approval_policy.clone(),
|
||||
sandbox_policy: config.permissions.sandbox_policy.clone(),
|
||||
file_system_sandbox_policy: config.permissions.file_system_sandbox_policy.clone(),
|
||||
network_sandbox_policy: config.permissions.network_sandbox_policy,
|
||||
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
|
||||
cwd: config.cwd.clone(),
|
||||
codex_home: config.codex_home.clone(),
|
||||
@@ -2424,8 +2414,6 @@ pub(crate) async fn make_session_and_context_with_dynamic_tools_and_rx(
|
||||
compact_prompt: config.compact_prompt.clone(),
|
||||
approval_policy: config.permissions.approval_policy.clone(),
|
||||
sandbox_policy: config.permissions.sandbox_policy.clone(),
|
||||
file_system_sandbox_policy: config.permissions.file_system_sandbox_policy.clone(),
|
||||
network_sandbox_policy: config.permissions.network_sandbox_policy,
|
||||
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
|
||||
cwd: config.cwd.clone(),
|
||||
codex_home: config.codex_home.clone(),
|
||||
@@ -3853,15 +3841,11 @@ async fn rejects_escalated_permissions_when_policy_not_on_request() {
|
||||
|
||||
// Now retry the same command WITHOUT escalated permissions; should succeed.
|
||||
// Force DangerFullAccess to avoid platform sandbox dependencies in tests.
|
||||
let turn_context_mut = Arc::get_mut(&mut turn_context).expect("unique turn context Arc");
|
||||
turn_context_mut
|
||||
Arc::get_mut(&mut turn_context)
|
||||
.expect("unique turn context Arc")
|
||||
.sandbox_policy
|
||||
.set(SandboxPolicy::DangerFullAccess)
|
||||
.expect("test setup should allow updating sandbox policy");
|
||||
turn_context_mut.file_system_sandbox_policy =
|
||||
FileSystemSandboxPolicy::from(turn_context_mut.sandbox_policy.get());
|
||||
turn_context_mut.network_sandbox_policy =
|
||||
NetworkSandboxPolicy::from(turn_context_mut.sandbox_policy.get());
|
||||
|
||||
let resp2 = handler
|
||||
.handle(ToolInvocation {
|
||||
|
||||
@@ -13,10 +13,6 @@ use crate::config_loader::RequirementSource;
|
||||
use crate::features::Feature;
|
||||
use assert_matches::assert_matches;
|
||||
use codex_config::CONFIG_TOML_FILE;
|
||||
use codex_protocol::permissions::FileSystemAccessMode;
|
||||
use codex_protocol::permissions::FileSystemPath;
|
||||
use codex_protocol::permissions::FileSystemSandboxEntry;
|
||||
use codex_protocol::permissions::FileSystemSpecialPath;
|
||||
use serde::Deserialize;
|
||||
use tempfile::tempdir;
|
||||
|
||||
@@ -201,9 +197,9 @@ fn runtime_config_defaults_model_availability_nux() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_toml_deserializes_network() {
|
||||
fn config_toml_deserializes_permissions_network() {
|
||||
let toml = r#"
|
||||
[network]
|
||||
[permissions.network]
|
||||
enabled = true
|
||||
proxy_url = "http://127.0.0.1:43128"
|
||||
enable_socks5 = false
|
||||
@@ -211,10 +207,12 @@ allow_upstream_proxy = false
|
||||
allowed_domains = ["openai.com"]
|
||||
"#;
|
||||
let cfg: ConfigToml =
|
||||
toml::from_str(toml).expect("TOML deserialization should succeed for [network]");
|
||||
toml::from_str(toml).expect("TOML deserialization should succeed for permissions.network");
|
||||
|
||||
assert_eq!(
|
||||
cfg.network.expect("[network] should deserialize"),
|
||||
cfg.permissions
|
||||
.and_then(|permissions| permissions.network)
|
||||
.expect("permissions.network should deserialize"),
|
||||
NetworkToml {
|
||||
enabled: Some(true),
|
||||
proxy_url: Some("http://127.0.0.1:43128".to_string()),
|
||||
@@ -234,14 +232,16 @@ allowed_domains = ["openai.com"]
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn network_enabled_populates_runtime_network_proxy_spec() -> std::io::Result<()> {
|
||||
fn permissions_network_enabled_populates_runtime_network_proxy_spec() -> std::io::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let cfg = ConfigToml {
|
||||
network: Some(NetworkToml {
|
||||
enabled: Some(true),
|
||||
proxy_url: Some("http://127.0.0.1:43128".to_string()),
|
||||
enable_socks5: Some(false),
|
||||
..Default::default()
|
||||
permissions: Some(PermissionsToml {
|
||||
network: Some(NetworkToml {
|
||||
enabled: Some(true),
|
||||
proxy_url: Some("http://127.0.0.1:43128".to_string()),
|
||||
enable_socks5: Some(false),
|
||||
..Default::default()
|
||||
}),
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
@@ -255,7 +255,7 @@ fn network_enabled_populates_runtime_network_proxy_spec() -> std::io::Result<()>
|
||||
.permissions
|
||||
.network
|
||||
.as_ref()
|
||||
.expect("enabled [network] should produce a NetworkProxySpec");
|
||||
.expect("enabled permissions.network should produce a NetworkProxySpec");
|
||||
|
||||
assert_eq!(network.proxy_host_and_port(), "127.0.0.1:43128");
|
||||
assert!(!network.socks_enabled());
|
||||
@@ -263,12 +263,14 @@ fn network_enabled_populates_runtime_network_proxy_spec() -> std::io::Result<()>
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn network_disabled_by_default_does_not_start_proxy() -> std::io::Result<()> {
|
||||
fn permissions_network_disabled_by_default_does_not_start_proxy() -> std::io::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let cfg = ConfigToml {
|
||||
network: Some(NetworkToml {
|
||||
allowed_domains: Some(vec!["openai.com".to_string()]),
|
||||
..Default::default()
|
||||
permissions: Some(PermissionsToml {
|
||||
network: Some(NetworkToml {
|
||||
allowed_domains: Some(vec!["openai.com".to_string()]),
|
||||
..Default::default()
|
||||
}),
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
@@ -282,365 +284,6 @@ fn network_disabled_by_default_does_not_start_proxy() -> std::io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_toml_deserializes_permission_profiles() {
|
||||
let toml = r#"
|
||||
default_permissions = "workspace"
|
||||
|
||||
[permissions.workspace.filesystem]
|
||||
":minimal" = "read"
|
||||
|
||||
[permissions.workspace.filesystem.":project_roots"]
|
||||
"." = "write"
|
||||
"docs" = "read"
|
||||
"#;
|
||||
let cfg: ConfigToml =
|
||||
toml::from_str(toml).expect("TOML deserialization should succeed for permissions profiles");
|
||||
|
||||
assert_eq!(cfg.default_permissions.as_deref(), Some("workspace"));
|
||||
assert_eq!(
|
||||
cfg.permissions.expect("[permissions] should deserialize"),
|
||||
PermissionsToml {
|
||||
entries: BTreeMap::from([(
|
||||
"workspace".to_string(),
|
||||
PermissionProfileToml {
|
||||
filesystem: Some(FilesystemPermissionsToml {
|
||||
entries: BTreeMap::from([
|
||||
(
|
||||
":minimal".to_string(),
|
||||
FilesystemPermissionToml::Access(FileSystemAccessMode::Read),
|
||||
),
|
||||
(
|
||||
":project_roots".to_string(),
|
||||
FilesystemPermissionToml::Scoped(BTreeMap::from([
|
||||
(".".to_string(), FileSystemAccessMode::Write),
|
||||
("docs".to_string(), FileSystemAccessMode::Read),
|
||||
])),
|
||||
),
|
||||
]),
|
||||
}),
|
||||
network: None,
|
||||
},
|
||||
)]),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_permissions_profile_populates_runtime_sandbox_policy() -> std::io::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let cwd = TempDir::new()?;
|
||||
std::fs::create_dir_all(cwd.path().join("docs"))?;
|
||||
std::fs::write(cwd.path().join(".git"), "gitdir: nowhere")?;
|
||||
|
||||
let cfg = ConfigToml {
|
||||
default_permissions: Some("workspace".to_string()),
|
||||
permissions: Some(PermissionsToml {
|
||||
entries: BTreeMap::from([(
|
||||
"workspace".to_string(),
|
||||
PermissionProfileToml {
|
||||
filesystem: Some(FilesystemPermissionsToml {
|
||||
entries: BTreeMap::from([
|
||||
(
|
||||
":minimal".to_string(),
|
||||
FilesystemPermissionToml::Access(FileSystemAccessMode::Read),
|
||||
),
|
||||
(
|
||||
":project_roots".to_string(),
|
||||
FilesystemPermissionToml::Scoped(BTreeMap::from([
|
||||
(".".to_string(), FileSystemAccessMode::Write),
|
||||
("docs".to_string(), FileSystemAccessMode::Read),
|
||||
])),
|
||||
),
|
||||
]),
|
||||
}),
|
||||
network: None,
|
||||
},
|
||||
)]),
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let config = Config::load_from_base_config_with_overrides(
|
||||
cfg,
|
||||
ConfigOverrides {
|
||||
cwd: Some(cwd.path().to_path_buf()),
|
||||
..Default::default()
|
||||
},
|
||||
codex_home.path().to_path_buf(),
|
||||
)?;
|
||||
|
||||
let memories_root = AbsolutePathBuf::try_from(codex_home.path().join("memories")).unwrap();
|
||||
assert_eq!(
|
||||
config.permissions.file_system_sandbox_policy,
|
||||
FileSystemSandboxPolicy::restricted(vec![
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Special {
|
||||
value: FileSystemSpecialPath::Minimal,
|
||||
},
|
||||
access: FileSystemAccessMode::Read,
|
||||
},
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Special {
|
||||
value: FileSystemSpecialPath::project_roots(None),
|
||||
},
|
||||
access: FileSystemAccessMode::Write,
|
||||
},
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Special {
|
||||
value: FileSystemSpecialPath::project_roots(Some("docs".into())),
|
||||
},
|
||||
access: FileSystemAccessMode::Read,
|
||||
},
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Path {
|
||||
path: memories_root.clone(),
|
||||
},
|
||||
access: FileSystemAccessMode::Write,
|
||||
},
|
||||
]),
|
||||
);
|
||||
assert_eq!(
|
||||
config.permissions.sandbox_policy.get(),
|
||||
&SandboxPolicy::WorkspaceWrite {
|
||||
writable_roots: vec![memories_root],
|
||||
read_only_access: ReadOnlyAccess::Restricted {
|
||||
include_platform_defaults: true,
|
||||
readable_roots: vec![
|
||||
AbsolutePathBuf::try_from(cwd.path().join("docs")).expect("absolute docs path"),
|
||||
],
|
||||
},
|
||||
network_access: false,
|
||||
exclude_tmpdir_env_var: true,
|
||||
exclude_slash_tmp: true,
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
config.permissions.network_sandbox_policy,
|
||||
NetworkSandboxPolicy::Restricted
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn permissions_profiles_require_default_permissions() -> std::io::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let cwd = TempDir::new()?;
|
||||
std::fs::write(cwd.path().join(".git"), "gitdir: nowhere")?;
|
||||
|
||||
let err = Config::load_from_base_config_with_overrides(
|
||||
ConfigToml {
|
||||
permissions: Some(PermissionsToml {
|
||||
entries: BTreeMap::from([(
|
||||
"workspace".to_string(),
|
||||
PermissionProfileToml {
|
||||
filesystem: Some(FilesystemPermissionsToml {
|
||||
entries: BTreeMap::from([(
|
||||
":minimal".to_string(),
|
||||
FilesystemPermissionToml::Access(FileSystemAccessMode::Read),
|
||||
)]),
|
||||
}),
|
||||
network: None,
|
||||
},
|
||||
)]),
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
ConfigOverrides {
|
||||
cwd: Some(cwd.path().to_path_buf()),
|
||||
..Default::default()
|
||||
},
|
||||
codex_home.path().to_path_buf(),
|
||||
)
|
||||
.expect_err("missing default_permissions should be rejected");
|
||||
|
||||
assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
|
||||
assert_eq!(
|
||||
err.to_string(),
|
||||
"config defines `[permissions]` profiles but does not set `default_permissions`"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn permissions_profiles_reject_writes_outside_workspace_root() -> std::io::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let cwd = TempDir::new()?;
|
||||
std::fs::write(cwd.path().join(".git"), "gitdir: nowhere")?;
|
||||
let external_write_path = if cfg!(windows) { r"C:\temp" } else { "/tmp" };
|
||||
|
||||
let err = Config::load_from_base_config_with_overrides(
|
||||
ConfigToml {
|
||||
default_permissions: Some("workspace".to_string()),
|
||||
permissions: Some(PermissionsToml {
|
||||
entries: BTreeMap::from([(
|
||||
"workspace".to_string(),
|
||||
PermissionProfileToml {
|
||||
filesystem: Some(FilesystemPermissionsToml {
|
||||
entries: BTreeMap::from([(
|
||||
external_write_path.to_string(),
|
||||
FilesystemPermissionToml::Access(FileSystemAccessMode::Write),
|
||||
)]),
|
||||
}),
|
||||
network: None,
|
||||
},
|
||||
)]),
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
ConfigOverrides {
|
||||
cwd: Some(cwd.path().to_path_buf()),
|
||||
..Default::default()
|
||||
},
|
||||
codex_home.path().to_path_buf(),
|
||||
)
|
||||
.expect_err("writes outside the workspace root should be rejected");
|
||||
|
||||
assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
|
||||
assert!(
|
||||
err.to_string()
|
||||
.contains("filesystem writes outside the workspace root"),
|
||||
"{err}"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn permissions_profiles_reject_nested_entries_for_non_project_roots() -> std::io::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let cwd = TempDir::new()?;
|
||||
std::fs::write(cwd.path().join(".git"), "gitdir: nowhere")?;
|
||||
|
||||
let err = Config::load_from_base_config_with_overrides(
|
||||
ConfigToml {
|
||||
default_permissions: Some("workspace".to_string()),
|
||||
permissions: Some(PermissionsToml {
|
||||
entries: BTreeMap::from([(
|
||||
"workspace".to_string(),
|
||||
PermissionProfileToml {
|
||||
filesystem: Some(FilesystemPermissionsToml {
|
||||
entries: BTreeMap::from([(
|
||||
":minimal".to_string(),
|
||||
FilesystemPermissionToml::Scoped(BTreeMap::from([(
|
||||
"docs".to_string(),
|
||||
FileSystemAccessMode::Read,
|
||||
)])),
|
||||
)]),
|
||||
}),
|
||||
network: None,
|
||||
},
|
||||
)]),
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
ConfigOverrides {
|
||||
cwd: Some(cwd.path().to_path_buf()),
|
||||
..Default::default()
|
||||
},
|
||||
codex_home.path().to_path_buf(),
|
||||
)
|
||||
.expect_err("nested entries outside :project_roots should be rejected");
|
||||
|
||||
assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
|
||||
assert_eq!(
|
||||
err.to_string(),
|
||||
"filesystem path `:minimal` does not support nested entries"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn permissions_profiles_reject_project_root_parent_traversal() -> std::io::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let cwd = TempDir::new()?;
|
||||
std::fs::write(cwd.path().join(".git"), "gitdir: nowhere")?;
|
||||
|
||||
let err = Config::load_from_base_config_with_overrides(
|
||||
ConfigToml {
|
||||
default_permissions: Some("workspace".to_string()),
|
||||
permissions: Some(PermissionsToml {
|
||||
entries: BTreeMap::from([(
|
||||
"workspace".to_string(),
|
||||
PermissionProfileToml {
|
||||
filesystem: Some(FilesystemPermissionsToml {
|
||||
entries: BTreeMap::from([(
|
||||
":project_roots".to_string(),
|
||||
FilesystemPermissionToml::Scoped(BTreeMap::from([(
|
||||
"../sibling".to_string(),
|
||||
FileSystemAccessMode::Read,
|
||||
)])),
|
||||
)]),
|
||||
}),
|
||||
network: None,
|
||||
},
|
||||
)]),
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
ConfigOverrides {
|
||||
cwd: Some(cwd.path().to_path_buf()),
|
||||
..Default::default()
|
||||
},
|
||||
codex_home.path().to_path_buf(),
|
||||
)
|
||||
.expect_err("parent traversal should be rejected for project root subpaths");
|
||||
|
||||
assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
|
||||
assert_eq!(
|
||||
err.to_string(),
|
||||
"filesystem subpath `../sibling` must be a descendant path without `.` or `..` components"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn permissions_profiles_allow_network_enablement() -> std::io::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let cwd = TempDir::new()?;
|
||||
std::fs::write(cwd.path().join(".git"), "gitdir: nowhere")?;
|
||||
|
||||
let config = Config::load_from_base_config_with_overrides(
|
||||
ConfigToml {
|
||||
default_permissions: Some("workspace".to_string()),
|
||||
permissions: Some(PermissionsToml {
|
||||
entries: BTreeMap::from([(
|
||||
"workspace".to_string(),
|
||||
PermissionProfileToml {
|
||||
filesystem: Some(FilesystemPermissionsToml {
|
||||
entries: BTreeMap::from([(
|
||||
":minimal".to_string(),
|
||||
FilesystemPermissionToml::Access(FileSystemAccessMode::Read),
|
||||
)]),
|
||||
}),
|
||||
network: Some(PermissionProfileNetworkToml {
|
||||
enabled: Some(true),
|
||||
}),
|
||||
},
|
||||
)]),
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
ConfigOverrides {
|
||||
cwd: Some(cwd.path().to_path_buf()),
|
||||
..Default::default()
|
||||
},
|
||||
codex_home.path().to_path_buf(),
|
||||
)?;
|
||||
|
||||
assert!(
|
||||
config.permissions.network_sandbox_policy.is_enabled(),
|
||||
"expected network sandbox policy to be enabled",
|
||||
);
|
||||
assert!(
|
||||
config
|
||||
.permissions
|
||||
.sandbox_policy
|
||||
.get()
|
||||
.has_full_network_access()
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tui_theme_deserializes_from_toml() {
|
||||
let cfg = r#"
|
||||
@@ -3010,10 +2653,6 @@ fn test_precedence_fixture_with_o3_profile() -> std::io::Result<()> {
|
||||
permissions: Permissions {
|
||||
approval_policy: Constrained::allow_any(AskForApproval::Never),
|
||||
sandbox_policy: Constrained::allow_any(SandboxPolicy::new_read_only_policy()),
|
||||
file_system_sandbox_policy: FileSystemSandboxPolicy::from(
|
||||
&SandboxPolicy::new_read_only_policy(),
|
||||
),
|
||||
network_sandbox_policy: NetworkSandboxPolicy::Restricted,
|
||||
network: None,
|
||||
allow_login_shell: true,
|
||||
shell_environment_policy: ShellEnvironmentPolicy::default(),
|
||||
@@ -3143,10 +2782,6 @@ fn test_precedence_fixture_with_gpt3_profile() -> std::io::Result<()> {
|
||||
permissions: Permissions {
|
||||
approval_policy: Constrained::allow_any(AskForApproval::UnlessTrusted),
|
||||
sandbox_policy: Constrained::allow_any(SandboxPolicy::new_read_only_policy()),
|
||||
file_system_sandbox_policy: FileSystemSandboxPolicy::from(
|
||||
&SandboxPolicy::new_read_only_policy(),
|
||||
),
|
||||
network_sandbox_policy: NetworkSandboxPolicy::Restricted,
|
||||
network: None,
|
||||
allow_login_shell: true,
|
||||
shell_environment_policy: ShellEnvironmentPolicy::default(),
|
||||
@@ -3274,10 +2909,6 @@ fn test_precedence_fixture_with_zdr_profile() -> std::io::Result<()> {
|
||||
permissions: Permissions {
|
||||
approval_policy: Constrained::allow_any(AskForApproval::OnFailure),
|
||||
sandbox_policy: Constrained::allow_any(SandboxPolicy::new_read_only_policy()),
|
||||
file_system_sandbox_policy: FileSystemSandboxPolicy::from(
|
||||
&SandboxPolicy::new_read_only_policy(),
|
||||
),
|
||||
network_sandbox_policy: NetworkSandboxPolicy::Restricted,
|
||||
network: None,
|
||||
allow_login_shell: true,
|
||||
shell_environment_policy: ShellEnvironmentPolicy::default(),
|
||||
@@ -3391,10 +3022,6 @@ fn test_precedence_fixture_with_gpt5_profile() -> std::io::Result<()> {
|
||||
permissions: Permissions {
|
||||
approval_policy: Constrained::allow_any(AskForApproval::OnFailure),
|
||||
sandbox_policy: Constrained::allow_any(SandboxPolicy::new_read_only_policy()),
|
||||
file_system_sandbox_policy: FileSystemSandboxPolicy::from(
|
||||
&SandboxPolicy::new_read_only_policy(),
|
||||
),
|
||||
network_sandbox_policy: NetworkSandboxPolicy::Restricted,
|
||||
network: None,
|
||||
allow_login_shell: true,
|
||||
shell_environment_policy: ShellEnvironmentPolicy::default(),
|
||||
|
||||
@@ -27,7 +27,6 @@ use crate::config::types::WindowsSandboxModeToml;
|
||||
use crate::config::types::WindowsToml;
|
||||
use crate::config_loader::CloudRequirementsLoader;
|
||||
use crate::config_loader::ConfigLayerStack;
|
||||
use crate::config_loader::ConfigLayerStackOrdering;
|
||||
use crate::config_loader::ConfigRequirements;
|
||||
use crate::config_loader::ConstrainedWithSource;
|
||||
use crate::config_loader::LoaderOverrides;
|
||||
@@ -73,8 +72,6 @@ use codex_protocol::config_types::WindowsSandboxLevel;
|
||||
use codex_protocol::models::MacOsSeatbeltProfileExtensions;
|
||||
use codex_protocol::openai_models::ModelsResponse;
|
||||
use codex_protocol::openai_models::ReasoningEffort;
|
||||
use codex_protocol::permissions::FileSystemSandboxPolicy;
|
||||
use codex_protocol::permissions::NetworkSandboxPolicy;
|
||||
use codex_rmcp_client::OAuthCredentialsStoreMode;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_absolute_path::AbsolutePathBufGuard;
|
||||
@@ -89,8 +86,7 @@ use std::io::ErrorKind;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::config::permissions::compile_permission_profile;
|
||||
use crate::config::permissions::network_proxy_config_from_network;
|
||||
use crate::config::permissions::network_proxy_config_from_permissions;
|
||||
use crate::config::profile::ConfigProfile;
|
||||
use toml::Value as TomlValue;
|
||||
use toml_edit::DocumentMut;
|
||||
@@ -111,11 +107,7 @@ pub use codex_network_proxy::NetworkProxyAuditMetadata;
|
||||
pub use managed_features::ManagedFeatures;
|
||||
pub use network_proxy_spec::NetworkProxySpec;
|
||||
pub use network_proxy_spec::StartedNetworkProxy;
|
||||
pub use permissions::FilesystemPermissionToml;
|
||||
pub use permissions::FilesystemPermissionsToml;
|
||||
pub use permissions::NetworkToml;
|
||||
pub use permissions::PermissionProfileNetworkToml;
|
||||
pub use permissions::PermissionProfileToml;
|
||||
pub use permissions::PermissionsToml;
|
||||
pub use service::ConfigService;
|
||||
pub use service::ConfigServiceError;
|
||||
@@ -145,9 +137,11 @@ fn resolve_sqlite_home_env(resolved_cwd: &Path) -> Option<PathBuf> {
|
||||
Some(resolved_cwd.join(path))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn test_config() -> Config {
|
||||
let codex_home = tempfile::tempdir().expect("create temp dir");
|
||||
use tempfile::tempdir;
|
||||
let codex_home = tempdir().expect("create temp dir");
|
||||
Config::load_from_base_config_with_overrides(
|
||||
ConfigToml::default(),
|
||||
ConfigOverrides::default(),
|
||||
@@ -163,12 +157,6 @@ pub struct Permissions {
|
||||
pub approval_policy: Constrained<AskForApproval>,
|
||||
/// Effective sandbox policy used for shell/unified exec.
|
||||
pub sandbox_policy: Constrained<SandboxPolicy>,
|
||||
/// Effective filesystem sandbox policy, including entries that cannot yet
|
||||
/// be fully represented by the legacy [`SandboxPolicy`] projection.
|
||||
pub file_system_sandbox_policy: FileSystemSandboxPolicy,
|
||||
/// Effective network sandbox policy split out from the legacy
|
||||
/// [`SandboxPolicy`] projection.
|
||||
pub network_sandbox_policy: NetworkSandboxPolicy,
|
||||
/// Effective network configuration applied to all spawned processes.
|
||||
pub network: Option<NetworkProxySpec>,
|
||||
/// Whether the model may request a login shell for shell-based tools.
|
||||
@@ -1057,18 +1045,10 @@ pub struct ConfigToml {
|
||||
/// Sandbox configuration to apply if `sandbox` is `WorkspaceWrite`.
|
||||
pub sandbox_workspace_write: Option<SandboxWorkspaceWrite>,
|
||||
|
||||
/// Default named permissions profile to apply from the `[permissions]`
|
||||
/// table.
|
||||
pub default_permissions: Option<String>,
|
||||
|
||||
/// Named permissions profiles.
|
||||
/// Nested permissions settings.
|
||||
#[serde(default)]
|
||||
pub permissions: Option<PermissionsToml>,
|
||||
|
||||
/// Top-level network proxy settings.
|
||||
#[serde(default)]
|
||||
pub network: Option<NetworkToml>,
|
||||
|
||||
/// Optional external command to spawn for end-user notifications.
|
||||
#[serde(default)]
|
||||
pub notify: Option<Vec<String>>,
|
||||
@@ -1583,78 +1563,6 @@ impl ConfigToml {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum PermissionConfigSyntax {
|
||||
Legacy,
|
||||
Profiles,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Default)]
|
||||
struct PermissionSelectionToml {
|
||||
default_permissions: Option<String>,
|
||||
sandbox_mode: Option<SandboxMode>,
|
||||
}
|
||||
|
||||
fn resolve_permission_config_syntax(
|
||||
config_layer_stack: &ConfigLayerStack,
|
||||
cfg: &ConfigToml,
|
||||
sandbox_mode_override: Option<SandboxMode>,
|
||||
profile_sandbox_mode: Option<SandboxMode>,
|
||||
) -> Option<PermissionConfigSyntax> {
|
||||
if sandbox_mode_override.is_some() || profile_sandbox_mode.is_some() {
|
||||
return Some(PermissionConfigSyntax::Legacy);
|
||||
}
|
||||
|
||||
let mut selection = None;
|
||||
for layer in
|
||||
config_layer_stack.get_layers(ConfigLayerStackOrdering::LowestPrecedenceFirst, false)
|
||||
{
|
||||
let Ok(layer_selection) = layer.config.clone().try_into::<PermissionSelectionToml>() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if layer_selection.sandbox_mode.is_some() {
|
||||
selection = Some(PermissionConfigSyntax::Legacy);
|
||||
}
|
||||
if layer_selection.default_permissions.is_some() {
|
||||
selection = Some(PermissionConfigSyntax::Profiles);
|
||||
}
|
||||
}
|
||||
|
||||
selection.or_else(|| {
|
||||
if cfg.default_permissions.is_some() {
|
||||
Some(PermissionConfigSyntax::Profiles)
|
||||
} else if cfg.sandbox_mode.is_some() {
|
||||
Some(PermissionConfigSyntax::Legacy)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn add_additional_file_system_writes(
|
||||
file_system_sandbox_policy: &mut FileSystemSandboxPolicy,
|
||||
additional_writable_roots: &[AbsolutePathBuf],
|
||||
) {
|
||||
for path in additional_writable_roots {
|
||||
let exists = file_system_sandbox_policy.entries.iter().any(|entry| {
|
||||
matches!(
|
||||
&entry.path,
|
||||
codex_protocol::permissions::FileSystemPath::Path { path: existing }
|
||||
if existing == path && entry.access == codex_protocol::permissions::FileSystemAccessMode::Write
|
||||
)
|
||||
});
|
||||
if !exists {
|
||||
file_system_sandbox_policy.entries.push(
|
||||
codex_protocol::permissions::FileSystemSandboxEntry {
|
||||
path: codex_protocol::permissions::FileSystemPath::Path { path: path.clone() },
|
||||
access: codex_protocol::permissions::FileSystemAccessMode::Write,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Optional overrides for user configuration (e.g., from CLI flags).
|
||||
#[derive(Default, Debug, Clone)]
|
||||
pub struct ConfigOverrides {
|
||||
@@ -1843,7 +1751,7 @@ impl Config {
|
||||
None => ConfigProfile::default(),
|
||||
};
|
||||
let configured_network_proxy_config =
|
||||
network_proxy_config_from_network(cfg.network.as_ref());
|
||||
network_proxy_config_from_permissions(cfg.permissions.as_ref());
|
||||
|
||||
let feature_overrides = FeatureOverrides {
|
||||
include_apply_patch_tool: include_apply_patch_tool_override,
|
||||
@@ -1871,113 +1779,42 @@ impl Config {
|
||||
}
|
||||
}
|
||||
});
|
||||
let mut additional_writable_roots: Vec<AbsolutePathBuf> = additional_writable_roots
|
||||
let additional_writable_roots: Vec<AbsolutePathBuf> = additional_writable_roots
|
||||
.into_iter()
|
||||
.map(|path| AbsolutePathBuf::resolve_path_against_base(path, &resolved_cwd))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
let active_project = cfg
|
||||
.get_active_project(&resolved_cwd)
|
||||
.unwrap_or(ProjectConfig { trust_level: None });
|
||||
let permission_config_syntax = resolve_permission_config_syntax(
|
||||
&config_layer_stack,
|
||||
&cfg,
|
||||
sandbox_mode,
|
||||
config_profile.sandbox_mode,
|
||||
);
|
||||
let has_permission_profiles = cfg
|
||||
.permissions
|
||||
.as_ref()
|
||||
.is_some_and(|profiles| !profiles.is_empty());
|
||||
if has_permission_profiles
|
||||
&& !matches!(
|
||||
permission_config_syntax,
|
||||
Some(PermissionConfigSyntax::Legacy)
|
||||
)
|
||||
&& cfg.default_permissions.is_none()
|
||||
{
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
"config defines `[permissions]` profiles but does not set `default_permissions`",
|
||||
));
|
||||
}
|
||||
|
||||
let windows_sandbox_level = match windows_sandbox_mode {
|
||||
Some(WindowsSandboxModeToml::Elevated) => WindowsSandboxLevel::Elevated,
|
||||
Some(WindowsSandboxModeToml::Unelevated) => WindowsSandboxLevel::RestrictedToken,
|
||||
None => WindowsSandboxLevel::from_features(&features),
|
||||
};
|
||||
let memories_root = memory_root(&codex_home);
|
||||
std::fs::create_dir_all(&memories_root)?;
|
||||
let memories_root = AbsolutePathBuf::from_absolute_path(&memories_root)?;
|
||||
if !additional_writable_roots
|
||||
.iter()
|
||||
.any(|existing| existing == &memories_root)
|
||||
{
|
||||
additional_writable_roots.push(memories_root);
|
||||
let mut sandbox_policy = cfg.derive_sandbox_policy(
|
||||
sandbox_mode,
|
||||
config_profile.sandbox_mode,
|
||||
windows_sandbox_level,
|
||||
&resolved_cwd,
|
||||
Some(&constrained_sandbox_policy),
|
||||
);
|
||||
if let SandboxPolicy::WorkspaceWrite { writable_roots, .. } = &mut sandbox_policy {
|
||||
let memories_root = memory_root(&codex_home);
|
||||
std::fs::create_dir_all(&memories_root)?;
|
||||
let memories_root = AbsolutePathBuf::from_absolute_path(&memories_root)?;
|
||||
if !writable_roots
|
||||
.iter()
|
||||
.any(|existing| existing == &memories_root)
|
||||
{
|
||||
writable_roots.push(memories_root);
|
||||
}
|
||||
for path in additional_writable_roots {
|
||||
if !writable_roots.iter().any(|existing| existing == &path) {
|
||||
writable_roots.push(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let profiles_are_active = matches!(
|
||||
permission_config_syntax,
|
||||
Some(PermissionConfigSyntax::Profiles)
|
||||
) || (permission_config_syntax.is_none()
|
||||
&& has_permission_profiles);
|
||||
let (sandbox_policy, file_system_sandbox_policy, network_sandbox_policy) =
|
||||
if profiles_are_active {
|
||||
let permissions = cfg.permissions.as_ref().ok_or_else(|| {
|
||||
std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
"default_permissions requires a `[permissions]` table",
|
||||
)
|
||||
})?;
|
||||
let default_permissions = cfg.default_permissions.as_deref().ok_or_else(|| {
|
||||
std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
"default_permissions requires a named permissions profile",
|
||||
)
|
||||
})?;
|
||||
let (mut file_system_sandbox_policy, network_sandbox_policy) =
|
||||
compile_permission_profile(permissions, default_permissions)?;
|
||||
let mut sandbox_policy = file_system_sandbox_policy
|
||||
.to_legacy_sandbox_policy(network_sandbox_policy, &resolved_cwd)?;
|
||||
if matches!(sandbox_policy, SandboxPolicy::WorkspaceWrite { .. }) {
|
||||
add_additional_file_system_writes(
|
||||
&mut file_system_sandbox_policy,
|
||||
&additional_writable_roots,
|
||||
);
|
||||
sandbox_policy = file_system_sandbox_policy
|
||||
.to_legacy_sandbox_policy(network_sandbox_policy, &resolved_cwd)?;
|
||||
}
|
||||
(
|
||||
sandbox_policy,
|
||||
file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
)
|
||||
} else {
|
||||
let mut sandbox_policy = cfg.derive_sandbox_policy(
|
||||
sandbox_mode,
|
||||
config_profile.sandbox_mode,
|
||||
windows_sandbox_level,
|
||||
&resolved_cwd,
|
||||
Some(&constrained_sandbox_policy),
|
||||
);
|
||||
if let SandboxPolicy::WorkspaceWrite { writable_roots, .. } = &mut sandbox_policy {
|
||||
for path in &additional_writable_roots {
|
||||
if !writable_roots.iter().any(|existing| existing == path) {
|
||||
writable_roots.push(path.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
let file_system_sandbox_policy = FileSystemSandboxPolicy::from(&sandbox_policy);
|
||||
let network_sandbox_policy = NetworkSandboxPolicy::from(&sandbox_policy);
|
||||
(
|
||||
sandbox_policy,
|
||||
file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
)
|
||||
};
|
||||
let approval_policy_was_explicit = approval_policy_override.is_some()
|
||||
|| config_profile.approval_policy.is_some()
|
||||
|| cfg.approval_policy.is_some();
|
||||
let mut approval_policy = approval_policy_override
|
||||
.or(config_profile.approval_policy)
|
||||
.or(cfg.approval_policy)
|
||||
@@ -1990,9 +1827,7 @@ impl Config {
|
||||
AskForApproval::default()
|
||||
}
|
||||
});
|
||||
if !approval_policy_was_explicit
|
||||
&& let Err(err) = constrained_approval_policy.can_set(&approval_policy)
|
||||
{
|
||||
if let Err(err) = constrained_approval_policy.can_set(&approval_policy) {
|
||||
tracing::warn!(
|
||||
error = %err,
|
||||
"default approval policy is disallowed by requirements; falling back to required default"
|
||||
@@ -2237,7 +2072,6 @@ impl Config {
|
||||
.map(AbsolutePathBuf::to_path_buf)
|
||||
.or_else(|| resolve_sqlite_home_env(&resolved_cwd))
|
||||
.unwrap_or_else(|| codex_home.to_path_buf());
|
||||
let original_sandbox_policy = sandbox_policy.clone();
|
||||
|
||||
apply_requirement_constrained_value(
|
||||
"approval_policy",
|
||||
@@ -2285,19 +2119,6 @@ impl Config {
|
||||
} else {
|
||||
network.enabled().then_some(network)
|
||||
};
|
||||
let effective_sandbox_policy = constrained_sandbox_policy.value.get().clone();
|
||||
let effective_file_system_sandbox_policy =
|
||||
if effective_sandbox_policy == original_sandbox_policy {
|
||||
file_system_sandbox_policy
|
||||
} else {
|
||||
FileSystemSandboxPolicy::from(&effective_sandbox_policy)
|
||||
};
|
||||
let effective_network_sandbox_policy =
|
||||
if effective_sandbox_policy == original_sandbox_policy {
|
||||
network_sandbox_policy
|
||||
} else {
|
||||
NetworkSandboxPolicy::from(&effective_sandbox_policy)
|
||||
};
|
||||
|
||||
let config = Self {
|
||||
model,
|
||||
@@ -2312,8 +2133,6 @@ impl Config {
|
||||
permissions: Permissions {
|
||||
approval_policy: constrained_approval_policy.value,
|
||||
sandbox_policy: constrained_sandbox_policy.value,
|
||||
file_system_sandbox_policy: effective_file_system_sandbox_policy,
|
||||
network_sandbox_policy: effective_network_sandbox_policy,
|
||||
network,
|
||||
allow_login_shell,
|
||||
shell_environment_policy,
|
||||
|
||||
@@ -1,64 +1,15 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::io;
|
||||
use std::path::Component;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use codex_network_proxy::NetworkMode;
|
||||
use codex_network_proxy::NetworkProxyConfig;
|
||||
use codex_protocol::permissions::FileSystemAccessMode;
|
||||
use codex_protocol::permissions::FileSystemPath;
|
||||
use codex_protocol::permissions::FileSystemSandboxEntry;
|
||||
use codex_protocol::permissions::FileSystemSandboxPolicy;
|
||||
use codex_protocol::permissions::FileSystemSpecialPath;
|
||||
use codex_protocol::permissions::NetworkSandboxPolicy;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)]
|
||||
#[schemars(deny_unknown_fields)]
|
||||
pub struct PermissionsToml {
|
||||
#[serde(flatten)]
|
||||
pub entries: BTreeMap<String, PermissionProfileToml>,
|
||||
}
|
||||
|
||||
impl PermissionsToml {
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.entries.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)]
|
||||
#[schemars(deny_unknown_fields)]
|
||||
pub struct PermissionProfileToml {
|
||||
pub filesystem: Option<FilesystemPermissionsToml>,
|
||||
pub network: Option<PermissionProfileNetworkToml>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)]
|
||||
pub struct FilesystemPermissionsToml {
|
||||
#[serde(flatten)]
|
||||
pub entries: BTreeMap<String, FilesystemPermissionToml>,
|
||||
}
|
||||
|
||||
impl FilesystemPermissionsToml {
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.entries.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema)]
|
||||
#[serde(untagged)]
|
||||
pub enum FilesystemPermissionToml {
|
||||
Access(FileSystemAccessMode),
|
||||
Scoped(BTreeMap<String, FileSystemAccessMode>),
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)]
|
||||
#[schemars(deny_unknown_fields)]
|
||||
pub struct PermissionProfileNetworkToml {
|
||||
pub enabled: Option<bool>,
|
||||
/// Network proxy settings from `[permissions.network]`.
|
||||
/// User config can enable the proxy; managed requirements may still constrain values.
|
||||
pub network: Option<NetworkToml>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)]
|
||||
@@ -140,168 +91,13 @@ impl NetworkToml {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn network_proxy_config_from_network(
|
||||
network: Option<&NetworkToml>,
|
||||
pub(crate) fn network_proxy_config_from_permissions(
|
||||
permissions: Option<&PermissionsToml>,
|
||||
) -> NetworkProxyConfig {
|
||||
network.map_or_else(
|
||||
NetworkProxyConfig::default,
|
||||
NetworkToml::to_network_proxy_config,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn compile_permission_profile(
|
||||
permissions: &PermissionsToml,
|
||||
profile_name: &str,
|
||||
) -> io::Result<(FileSystemSandboxPolicy, NetworkSandboxPolicy)> {
|
||||
let profile = permissions.entries.get(profile_name).ok_or_else(|| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
format!("default_permissions refers to undefined profile `{profile_name}`"),
|
||||
permissions
|
||||
.and_then(|permissions| permissions.network.as_ref())
|
||||
.map_or_else(
|
||||
NetworkProxyConfig::default,
|
||||
NetworkToml::to_network_proxy_config,
|
||||
)
|
||||
})?;
|
||||
|
||||
let filesystem = profile.filesystem.as_ref().ok_or_else(|| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
format!(
|
||||
"permissions profile `{profile_name}` must define a `[permissions.{profile_name}.filesystem]` table"
|
||||
),
|
||||
)
|
||||
})?;
|
||||
|
||||
if filesystem.is_empty() {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
format!(
|
||||
"permissions profile `{profile_name}` must define at least one filesystem entry"
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
let mut entries = Vec::new();
|
||||
for (path, permission) in &filesystem.entries {
|
||||
compile_filesystem_permission(path, permission, &mut entries)?;
|
||||
}
|
||||
|
||||
let network_sandbox_policy = compile_network_sandbox_policy(profile.network.as_ref());
|
||||
|
||||
Ok((
|
||||
FileSystemSandboxPolicy::restricted(entries),
|
||||
network_sandbox_policy,
|
||||
))
|
||||
}
|
||||
|
||||
fn compile_network_sandbox_policy(
|
||||
network: Option<&PermissionProfileNetworkToml>,
|
||||
) -> NetworkSandboxPolicy {
|
||||
let Some(network) = network else {
|
||||
return NetworkSandboxPolicy::Restricted;
|
||||
};
|
||||
|
||||
match network.enabled {
|
||||
Some(true) => NetworkSandboxPolicy::Enabled,
|
||||
_ => NetworkSandboxPolicy::Restricted,
|
||||
}
|
||||
}
|
||||
|
||||
fn compile_filesystem_permission(
|
||||
path: &str,
|
||||
permission: &FilesystemPermissionToml,
|
||||
entries: &mut Vec<FileSystemSandboxEntry>,
|
||||
) -> io::Result<()> {
|
||||
match permission {
|
||||
FilesystemPermissionToml::Access(access) => entries.push(FileSystemSandboxEntry {
|
||||
path: compile_filesystem_path(path)?,
|
||||
access: *access,
|
||||
}),
|
||||
FilesystemPermissionToml::Scoped(scoped_entries) => {
|
||||
for (subpath, access) in scoped_entries {
|
||||
entries.push(FileSystemSandboxEntry {
|
||||
path: compile_scoped_filesystem_path(path, subpath)?,
|
||||
access: *access,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn compile_filesystem_path(path: &str) -> io::Result<FileSystemPath> {
|
||||
if let Some(special) = parse_special_path(path)? {
|
||||
return Ok(FileSystemPath::Special { value: special });
|
||||
}
|
||||
|
||||
let path = parse_absolute_path(path)?;
|
||||
Ok(FileSystemPath::Path { path })
|
||||
}
|
||||
|
||||
fn compile_scoped_filesystem_path(path: &str, subpath: &str) -> io::Result<FileSystemPath> {
|
||||
if subpath == "." {
|
||||
return compile_filesystem_path(path);
|
||||
}
|
||||
|
||||
if let Some(special) = parse_special_path(path)? {
|
||||
if !matches!(special, FileSystemSpecialPath::ProjectRoots { .. }) {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
format!("filesystem path `{path}` does not support nested entries"),
|
||||
));
|
||||
}
|
||||
return Ok(FileSystemPath::Special {
|
||||
value: FileSystemSpecialPath::project_roots(Some(parse_relative_subpath(subpath)?)),
|
||||
});
|
||||
}
|
||||
|
||||
let subpath = parse_relative_subpath(subpath)?;
|
||||
let base = parse_absolute_path(path)?;
|
||||
let path = AbsolutePathBuf::resolve_path_against_base(&subpath, base.as_path())?;
|
||||
Ok(FileSystemPath::Path { path })
|
||||
}
|
||||
|
||||
fn parse_special_path(path: &str) -> io::Result<Option<FileSystemSpecialPath>> {
|
||||
let special = match path {
|
||||
":root" => Some(FileSystemSpecialPath::Root),
|
||||
":minimal" => Some(FileSystemSpecialPath::Minimal),
|
||||
":project_roots" => Some(FileSystemSpecialPath::project_roots(None)),
|
||||
":tmpdir" => Some(FileSystemSpecialPath::Tmpdir),
|
||||
_ if path.starts_with(':') => {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
format!("unknown filesystem special path `{path}`"),
|
||||
));
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
Ok(special)
|
||||
}
|
||||
|
||||
fn parse_absolute_path(path: &str) -> io::Result<AbsolutePathBuf> {
|
||||
let path_ref = Path::new(path);
|
||||
if !path_ref.is_absolute() && path != "~" && !path.starts_with("~/") {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
format!("filesystem path `{path}` must be absolute, use `~/...`, or start with `:`"),
|
||||
));
|
||||
}
|
||||
AbsolutePathBuf::from_absolute_path(path_ref)
|
||||
}
|
||||
|
||||
fn parse_relative_subpath(subpath: &str) -> io::Result<PathBuf> {
|
||||
let path = Path::new(subpath);
|
||||
if !subpath.is_empty()
|
||||
&& path
|
||||
.components()
|
||||
.all(|component| matches!(component, Component::Normal(_)))
|
||||
{
|
||||
return Ok(path.to_path_buf());
|
||||
}
|
||||
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
format!(
|
||||
"filesystem subpath `{}` must be a descendant path without `.` or `..` components",
|
||||
path.display()
|
||||
),
|
||||
))
|
||||
}
|
||||
|
||||
@@ -24,15 +24,11 @@ use crate::protocol::Event;
|
||||
use crate::protocol::EventMsg;
|
||||
use crate::protocol::ExecCommandOutputDeltaEvent;
|
||||
use crate::protocol::ExecOutputStream;
|
||||
use crate::protocol::FileSystemSandboxKind;
|
||||
use crate::protocol::FileSystemSandboxPolicy;
|
||||
use crate::protocol::NetworkSandboxPolicy;
|
||||
use crate::protocol::SandboxPolicy;
|
||||
use crate::sandboxing::CommandSpec;
|
||||
use crate::sandboxing::ExecRequest;
|
||||
use crate::sandboxing::SandboxManager;
|
||||
use crate::sandboxing::SandboxPermissions;
|
||||
use crate::sandboxing::should_require_platform_sandbox;
|
||||
use crate::spawn::SpawnChildRequest;
|
||||
use crate::spawn::StdioPolicy;
|
||||
use crate::spawn::spawn_child_async;
|
||||
@@ -153,12 +149,9 @@ pub struct StdoutStream {
|
||||
pub tx_event: Sender<Event>,
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn process_exec_tool_call(
|
||||
params: ExecParams,
|
||||
sandbox_policy: &SandboxPolicy,
|
||||
file_system_sandbox_policy: &FileSystemSandboxPolicy,
|
||||
network_sandbox_policy: NetworkSandboxPolicy,
|
||||
sandbox_cwd: &Path,
|
||||
codex_linux_sandbox_exe: &Option<PathBuf>,
|
||||
use_linux_sandbox_bwrap: bool,
|
||||
@@ -166,17 +159,22 @@ pub async fn process_exec_tool_call(
|
||||
) -> Result<ExecToolCallOutput> {
|
||||
let windows_sandbox_level = params.windows_sandbox_level;
|
||||
let enforce_managed_network = params.network.is_some();
|
||||
let sandbox_type = if should_require_platform_sandbox(
|
||||
file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
enforce_managed_network,
|
||||
) {
|
||||
get_platform_sandbox(
|
||||
let sandbox_type = match &sandbox_policy {
|
||||
SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. } => {
|
||||
if enforce_managed_network {
|
||||
get_platform_sandbox(
|
||||
windows_sandbox_level
|
||||
!= codex_protocol::config_types::WindowsSandboxLevel::Disabled,
|
||||
)
|
||||
.unwrap_or(SandboxType::None)
|
||||
} else {
|
||||
SandboxType::None
|
||||
}
|
||||
}
|
||||
_ => get_platform_sandbox(
|
||||
windows_sandbox_level != codex_protocol::config_types::WindowsSandboxLevel::Disabled,
|
||||
)
|
||||
.unwrap_or(SandboxType::None)
|
||||
} else {
|
||||
SandboxType::None
|
||||
.unwrap_or(SandboxType::None),
|
||||
};
|
||||
tracing::debug!("Sandbox type: {sandbox_type:?}");
|
||||
|
||||
@@ -217,8 +215,6 @@ pub async fn process_exec_tool_call(
|
||||
.transform(crate::sandboxing::SandboxTransformRequest {
|
||||
spec,
|
||||
policy: sandbox_policy,
|
||||
file_system_policy: file_system_sandbox_policy,
|
||||
network_policy: network_sandbox_policy,
|
||||
sandbox: sandbox_type,
|
||||
enforce_managed_network,
|
||||
network: network.as_ref(),
|
||||
@@ -251,12 +247,9 @@ pub(crate) async fn execute_exec_request(
|
||||
windows_sandbox_level,
|
||||
sandbox_permissions,
|
||||
sandbox_policy: _sandbox_policy_from_env,
|
||||
file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
justification,
|
||||
arg0,
|
||||
} = exec_request;
|
||||
let _ = _sandbox_policy_from_env;
|
||||
|
||||
let params = ExecParams {
|
||||
command,
|
||||
@@ -271,16 +264,7 @@ pub(crate) async fn execute_exec_request(
|
||||
};
|
||||
|
||||
let start = Instant::now();
|
||||
let raw_output_result = exec(
|
||||
params,
|
||||
sandbox,
|
||||
sandbox_policy,
|
||||
&file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
stdout_stream,
|
||||
after_spawn,
|
||||
)
|
||||
.await;
|
||||
let raw_output_result = exec(params, sandbox, sandbox_policy, stdout_stream, after_spawn).await;
|
||||
let duration = start.elapsed();
|
||||
finalize_exec_result(raw_output_result, sandbox, duration)
|
||||
}
|
||||
@@ -709,17 +693,16 @@ async fn exec(
|
||||
params: ExecParams,
|
||||
sandbox: SandboxType,
|
||||
sandbox_policy: &SandboxPolicy,
|
||||
file_system_sandbox_policy: &FileSystemSandboxPolicy,
|
||||
network_sandbox_policy: NetworkSandboxPolicy,
|
||||
stdout_stream: Option<StdoutStream>,
|
||||
after_spawn: Option<Box<dyn FnOnce() + Send>>,
|
||||
) -> Result<RawExecToolCallOutput> {
|
||||
#[cfg(target_os = "windows")]
|
||||
if should_use_windows_restricted_token_sandbox(
|
||||
sandbox,
|
||||
sandbox_policy,
|
||||
file_system_sandbox_policy,
|
||||
) {
|
||||
if sandbox == SandboxType::WindowsRestrictedToken
|
||||
&& !matches!(
|
||||
sandbox_policy,
|
||||
SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. }
|
||||
)
|
||||
{
|
||||
return exec_windows_sandbox(params, sandbox_policy).await;
|
||||
}
|
||||
let ExecParams {
|
||||
@@ -748,7 +731,7 @@ async fn exec(
|
||||
args: args.into(),
|
||||
arg0: arg0_ref,
|
||||
cwd,
|
||||
network_sandbox_policy,
|
||||
sandbox_policy,
|
||||
// The environment already has attempt-scoped proxy settings from
|
||||
// apply_to_env_for_attempt above. Passing network here would reapply
|
||||
// non-attempt proxy vars and drop attempt correlation metadata.
|
||||
@@ -763,20 +746,6 @@ async fn exec(
|
||||
consume_truncated_output(child, expiration, stdout_stream).await
|
||||
}
|
||||
|
||||
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
|
||||
fn should_use_windows_restricted_token_sandbox(
|
||||
sandbox: SandboxType,
|
||||
sandbox_policy: &SandboxPolicy,
|
||||
file_system_sandbox_policy: &FileSystemSandboxPolicy,
|
||||
) -> bool {
|
||||
sandbox == SandboxType::WindowsRestrictedToken
|
||||
&& file_system_sandbox_policy.kind == FileSystemSandboxKind::Restricted
|
||||
&& !matches!(
|
||||
sandbox_policy,
|
||||
SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. }
|
||||
)
|
||||
}
|
||||
|
||||
/// Consumes the output of a child process, truncating it so it is suitable for
|
||||
/// use as the output of a `shell` tool call. Also enforces specified timeout.
|
||||
async fn consume_truncated_output(
|
||||
@@ -1129,38 +1098,6 @@ mod tests {
|
||||
assert_eq!(aggregated.truncated_after_lines, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn windows_restricted_token_skips_external_sandbox_policies() {
|
||||
let policy = SandboxPolicy::ExternalSandbox {
|
||||
network_access: codex_protocol::protocol::NetworkAccess::Restricted,
|
||||
};
|
||||
let file_system_policy = FileSystemSandboxPolicy::restricted(vec![]);
|
||||
|
||||
assert_eq!(
|
||||
should_use_windows_restricted_token_sandbox(
|
||||
SandboxType::WindowsRestrictedToken,
|
||||
&policy,
|
||||
&file_system_policy,
|
||||
),
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn windows_restricted_token_runs_for_legacy_restricted_policies() {
|
||||
let policy = SandboxPolicy::new_read_only_policy();
|
||||
let file_system_policy = FileSystemSandboxPolicy::restricted(vec![]);
|
||||
|
||||
assert_eq!(
|
||||
should_use_windows_restricted_token_sandbox(
|
||||
SandboxType::WindowsRestrictedToken,
|
||||
&policy,
|
||||
&file_system_policy,
|
||||
),
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn sandbox_detection_flags_sigsys_exit_code() {
|
||||
@@ -1203,8 +1140,6 @@ mod tests {
|
||||
params,
|
||||
SandboxType::None,
|
||||
&SandboxPolicy::new_read_only_policy(),
|
||||
&FileSystemSandboxPolicy::from(&SandboxPolicy::new_read_only_policy()),
|
||||
NetworkSandboxPolicy::Restricted,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
@@ -1261,8 +1196,6 @@ mod tests {
|
||||
let result = process_exec_tool_call(
|
||||
params,
|
||||
&SandboxPolicy::DangerFullAccess,
|
||||
&FileSystemSandboxPolicy::from(&SandboxPolicy::DangerFullAccess),
|
||||
NetworkSandboxPolicy::Enabled,
|
||||
cwd.as_path(),
|
||||
&None,
|
||||
false,
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
use crate::protocol::FileSystemSandboxPolicy;
|
||||
use crate::protocol::NetworkSandboxPolicy;
|
||||
use crate::protocol::SandboxPolicy;
|
||||
use crate::spawn::SpawnChildRequest;
|
||||
use crate::spawn::StdioPolicy;
|
||||
@@ -15,9 +13,9 @@ use tokio::process::Child;
|
||||
/// isolation plus seccomp for network restrictions.
|
||||
///
|
||||
/// Unlike macOS Seatbelt where we directly embed the policy text, the Linux
|
||||
/// helper is a separate executable. We pass the legacy [`SandboxPolicy`] plus
|
||||
/// split filesystem/network policies as JSON so the helper can migrate
|
||||
/// incrementally without breaking older call sites.
|
||||
/// helper accepts a list of `--sandbox-permission`/`-s` flags mirroring the
|
||||
/// public CLI. We convert the internal [`SandboxPolicy`] representation into
|
||||
/// the equivalent CLI options.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn spawn_command_under_linux_sandbox<P>(
|
||||
codex_linux_sandbox_exe: P,
|
||||
@@ -33,13 +31,9 @@ pub async fn spawn_command_under_linux_sandbox<P>(
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
let file_system_sandbox_policy = FileSystemSandboxPolicy::from(sandbox_policy);
|
||||
let network_sandbox_policy = NetworkSandboxPolicy::from(sandbox_policy);
|
||||
let args = create_linux_sandbox_command_args_for_policies(
|
||||
let args = create_linux_sandbox_command_args(
|
||||
command,
|
||||
sandbox_policy,
|
||||
&file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
sandbox_policy_cwd,
|
||||
use_bwrap_sandbox,
|
||||
allow_network_for_proxy(false),
|
||||
@@ -50,7 +44,7 @@ where
|
||||
args,
|
||||
arg0,
|
||||
cwd: command_cwd,
|
||||
network_sandbox_policy,
|
||||
sandbox_policy,
|
||||
network,
|
||||
stdio_policy,
|
||||
env,
|
||||
@@ -65,55 +59,13 @@ pub(crate) fn allow_network_for_proxy(enforce_managed_network: bool) -> bool {
|
||||
enforce_managed_network
|
||||
}
|
||||
|
||||
/// Converts the sandbox policies into the CLI invocation for
|
||||
/// `codex-linux-sandbox`.
|
||||
/// Converts the sandbox policy into the CLI invocation for `codex-linux-sandbox`.
|
||||
///
|
||||
/// The helper performs the actual sandboxing (bubblewrap + seccomp) after
|
||||
/// parsing these arguments. See `docs/linux_sandbox.md` for the Linux semantics.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) fn create_linux_sandbox_command_args_for_policies(
|
||||
command: Vec<String>,
|
||||
sandbox_policy: &SandboxPolicy,
|
||||
file_system_sandbox_policy: &FileSystemSandboxPolicy,
|
||||
network_sandbox_policy: NetworkSandboxPolicy,
|
||||
sandbox_policy_cwd: &Path,
|
||||
use_bwrap_sandbox: bool,
|
||||
allow_network_for_proxy: bool,
|
||||
) -> Vec<String> {
|
||||
#[expect(clippy::expect_used)]
|
||||
let sandbox_policy_json =
|
||||
serde_json::to_string(sandbox_policy).expect("Failed to serialize SandboxPolicy to JSON");
|
||||
#[expect(clippy::expect_used)]
|
||||
let file_system_policy_json = serde_json::to_string(file_system_sandbox_policy)
|
||||
.expect("Failed to serialize FileSystemSandboxPolicy to JSON");
|
||||
#[expect(clippy::expect_used)]
|
||||
let network_policy_json = serde_json::to_string(&network_sandbox_policy)
|
||||
.expect("Failed to serialize NetworkSandboxPolicy to JSON");
|
||||
|
||||
let mut linux_cmd = create_linux_sandbox_command_args(
|
||||
command,
|
||||
sandbox_policy_cwd,
|
||||
use_bwrap_sandbox,
|
||||
allow_network_for_proxy,
|
||||
);
|
||||
linux_cmd.splice(
|
||||
2..2,
|
||||
[
|
||||
"--sandbox-policy".to_string(),
|
||||
sandbox_policy_json,
|
||||
"--file-system-sandbox-policy".to_string(),
|
||||
file_system_policy_json,
|
||||
"--network-sandbox-policy".to_string(),
|
||||
network_policy_json,
|
||||
],
|
||||
);
|
||||
linux_cmd
|
||||
}
|
||||
|
||||
/// Converts the sandbox cwd and execution options into the CLI invocation for
|
||||
/// `codex-linux-sandbox`.
|
||||
pub(crate) fn create_linux_sandbox_command_args(
|
||||
command: Vec<String>,
|
||||
sandbox_policy: &SandboxPolicy,
|
||||
sandbox_policy_cwd: &Path,
|
||||
use_bwrap_sandbox: bool,
|
||||
allow_network_for_proxy: bool,
|
||||
@@ -124,7 +76,16 @@ pub(crate) fn create_linux_sandbox_command_args(
|
||||
.expect("cwd must be valid UTF-8")
|
||||
.to_string();
|
||||
|
||||
let mut linux_cmd: Vec<String> = vec!["--sandbox-policy-cwd".to_string(), sandbox_policy_cwd];
|
||||
#[expect(clippy::expect_used)]
|
||||
let sandbox_policy_json =
|
||||
serde_json::to_string(sandbox_policy).expect("Failed to serialize SandboxPolicy to JSON");
|
||||
|
||||
let mut linux_cmd: Vec<String> = vec![
|
||||
"--sandbox-policy-cwd".to_string(),
|
||||
sandbox_policy_cwd,
|
||||
"--sandbox-policy".to_string(),
|
||||
sandbox_policy_json,
|
||||
];
|
||||
if use_bwrap_sandbox {
|
||||
linux_cmd.push("--use-bwrap-sandbox".to_string());
|
||||
}
|
||||
@@ -151,14 +112,16 @@ mod tests {
|
||||
fn bwrap_flags_are_feature_gated() {
|
||||
let command = vec!["/bin/true".to_string()];
|
||||
let cwd = Path::new("/tmp");
|
||||
let policy = SandboxPolicy::new_read_only_policy();
|
||||
|
||||
let with_bwrap = create_linux_sandbox_command_args(command.clone(), cwd, true, false);
|
||||
let with_bwrap =
|
||||
create_linux_sandbox_command_args(command.clone(), &policy, cwd, true, false);
|
||||
assert_eq!(
|
||||
with_bwrap.contains(&"--use-bwrap-sandbox".to_string()),
|
||||
true
|
||||
);
|
||||
|
||||
let without_bwrap = create_linux_sandbox_command_args(command, cwd, false, false);
|
||||
let without_bwrap = create_linux_sandbox_command_args(command, &policy, cwd, false, false);
|
||||
assert_eq!(
|
||||
without_bwrap.contains(&"--use-bwrap-sandbox".to_string()),
|
||||
false
|
||||
@@ -169,46 +132,15 @@ mod tests {
|
||||
fn proxy_flag_is_included_when_requested() {
|
||||
let command = vec!["/bin/true".to_string()];
|
||||
let cwd = Path::new("/tmp");
|
||||
let policy = SandboxPolicy::new_read_only_policy();
|
||||
|
||||
let args = create_linux_sandbox_command_args(command, cwd, true, true);
|
||||
let args = create_linux_sandbox_command_args(command, &policy, cwd, true, true);
|
||||
assert_eq!(
|
||||
args.contains(&"--allow-network-for-proxy".to_string()),
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn split_policy_flags_are_included() {
|
||||
let command = vec!["/bin/true".to_string()];
|
||||
let cwd = Path::new("/tmp");
|
||||
let sandbox_policy = SandboxPolicy::new_read_only_policy();
|
||||
let file_system_sandbox_policy = FileSystemSandboxPolicy::from(&sandbox_policy);
|
||||
let network_sandbox_policy = NetworkSandboxPolicy::from(&sandbox_policy);
|
||||
|
||||
let args = create_linux_sandbox_command_args_for_policies(
|
||||
command,
|
||||
&sandbox_policy,
|
||||
&file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
cwd,
|
||||
true,
|
||||
false,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
args.windows(2).any(|window| {
|
||||
window[0] == "--file-system-sandbox-policy" && !window[1].is_empty()
|
||||
}),
|
||||
true
|
||||
);
|
||||
assert_eq!(
|
||||
args.windows(2)
|
||||
.any(|window| window[0] == "--network-sandbox-policy"
|
||||
&& window[1] == "\"restricted\""),
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn proxy_network_requires_managed_requirements() {
|
||||
assert_eq!(allow_network_for_proxy(false), false);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use crate::config::NetworkToml;
|
||||
use crate::config::PermissionsToml;
|
||||
use crate::config::find_codex_home;
|
||||
use crate::config_loader::CloudRequirementsLoader;
|
||||
use crate::config_loader::ConfigLayerStack;
|
||||
@@ -120,6 +121,12 @@ fn network_constraints_from_trusted_layers(
|
||||
if let Some(network) = parsed.network {
|
||||
apply_network_constraints(network, &mut constraints);
|
||||
}
|
||||
if let Some(network) = parsed
|
||||
.permissions
|
||||
.and_then(|permissions| permissions.network)
|
||||
{
|
||||
apply_network_constraints(network, &mut constraints);
|
||||
}
|
||||
}
|
||||
Ok(constraints)
|
||||
}
|
||||
@@ -159,6 +166,7 @@ fn apply_network_constraints(network: NetworkToml, constraints: &mut NetworkProx
|
||||
#[derive(Debug, Clone, Default, Deserialize)]
|
||||
struct NetworkTablesToml {
|
||||
network: Option<NetworkToml>,
|
||||
permissions: Option<PermissionsToml>,
|
||||
}
|
||||
|
||||
fn network_tables_from_toml(value: &toml::Value) -> Result<NetworkTablesToml> {
|
||||
@@ -172,6 +180,12 @@ fn apply_network_tables(config: &mut NetworkProxyConfig, parsed: NetworkTablesTo
|
||||
if let Some(network) = parsed.network {
|
||||
network.apply_to_network_proxy_config(config);
|
||||
}
|
||||
if let Some(network) = parsed
|
||||
.permissions
|
||||
.and_then(|permissions| permissions.network)
|
||||
{
|
||||
network.apply_to_network_proxy_config(config);
|
||||
}
|
||||
}
|
||||
|
||||
fn config_from_layers(
|
||||
@@ -296,10 +310,10 @@ mod tests {
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn higher_precedence_network_table_beats_lower_network_table() {
|
||||
let lower_network: toml::Value = toml::from_str(
|
||||
fn higher_precedence_network_table_beats_lower_permissions_network_table() {
|
||||
let lower_permissions: toml::Value = toml::from_str(
|
||||
r#"
|
||||
[network]
|
||||
[permissions.network]
|
||||
allowed_domains = ["lower.example.com"]
|
||||
"#,
|
||||
)
|
||||
@@ -315,7 +329,7 @@ allowed_domains = ["higher.example.com"]
|
||||
let mut config = NetworkProxyConfig::default();
|
||||
apply_network_tables(
|
||||
&mut config,
|
||||
network_tables_from_toml(&lower_network).expect("lower layer should deserialize"),
|
||||
network_tables_from_toml(&lower_permissions).expect("lower layer should deserialize"),
|
||||
);
|
||||
apply_network_tables(
|
||||
&mut config,
|
||||
|
||||
@@ -9,7 +9,6 @@ use crate::exec::SandboxType;
|
||||
use crate::util::resolve_path;
|
||||
|
||||
use crate::protocol::AskForApproval;
|
||||
use crate::protocol::FileSystemSandboxPolicy;
|
||||
use crate::protocol::SandboxPolicy;
|
||||
use codex_protocol::config_types::WindowsSandboxLevel;
|
||||
|
||||
@@ -29,7 +28,6 @@ pub fn assess_patch_safety(
|
||||
action: &ApplyPatchAction,
|
||||
policy: AskForApproval,
|
||||
sandbox_policy: &SandboxPolicy,
|
||||
file_system_sandbox_policy: &FileSystemSandboxPolicy,
|
||||
cwd: &Path,
|
||||
windows_sandbox_level: WindowsSandboxLevel,
|
||||
) -> SafetyCheck {
|
||||
@@ -62,7 +60,7 @@ pub fn assess_patch_safety(
|
||||
// Even though the patch appears to be constrained to writable paths, it is
|
||||
// possible that paths in the patch are hard links to files outside the
|
||||
// writable roots, so we should still run `apply_patch` in a sandbox in that case.
|
||||
if is_write_patch_constrained_to_writable_paths(action, file_system_sandbox_policy, cwd)
|
||||
if is_write_patch_constrained_to_writable_paths(action, sandbox_policy, cwd)
|
||||
|| matches!(policy, AskForApproval::OnFailure)
|
||||
{
|
||||
if matches!(
|
||||
@@ -124,9 +122,20 @@ pub fn get_platform_sandbox(windows_sandbox_enabled: bool) -> Option<SandboxType
|
||||
|
||||
fn is_write_patch_constrained_to_writable_paths(
|
||||
action: &ApplyPatchAction,
|
||||
file_system_sandbox_policy: &FileSystemSandboxPolicy,
|
||||
sandbox_policy: &SandboxPolicy,
|
||||
cwd: &Path,
|
||||
) -> bool {
|
||||
// Early‑exit if there are no declared writable roots.
|
||||
let writable_roots = match sandbox_policy {
|
||||
SandboxPolicy::ReadOnly { .. } => {
|
||||
return false;
|
||||
}
|
||||
SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. } => {
|
||||
return true;
|
||||
}
|
||||
SandboxPolicy::WorkspaceWrite { .. } => sandbox_policy.get_writable_roots_with_cwd(cwd),
|
||||
};
|
||||
|
||||
// Normalize a path by removing `.` and resolving `..` without touching the
|
||||
// filesystem (works even if the file does not exist).
|
||||
fn normalize(path: &Path) -> Option<PathBuf> {
|
||||
@@ -143,9 +152,6 @@ fn is_write_patch_constrained_to_writable_paths(
|
||||
Some(out)
|
||||
}
|
||||
|
||||
let unreadable_roots = file_system_sandbox_policy.get_unreadable_roots_with_cwd(cwd);
|
||||
let writable_roots = file_system_sandbox_policy.get_writable_roots_with_cwd(cwd);
|
||||
|
||||
// Determine whether `path` is inside **any** writable root. Both `path`
|
||||
// and roots are converted to absolute, normalized forms before the
|
||||
// prefix check.
|
||||
@@ -156,17 +162,6 @@ fn is_write_patch_constrained_to_writable_paths(
|
||||
None => return false,
|
||||
};
|
||||
|
||||
if unreadable_roots
|
||||
.iter()
|
||||
.any(|root| abs.starts_with(root.as_path()))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if file_system_sandbox_policy.has_full_disk_write_access() {
|
||||
return true;
|
||||
}
|
||||
|
||||
writable_roots
|
||||
.iter()
|
||||
.any(|writable_root| writable_root.is_path_writable(&abs))
|
||||
@@ -198,11 +193,6 @@ fn is_write_patch_constrained_to_writable_paths(
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use codex_protocol::protocol::FileSystemAccessMode;
|
||||
use codex_protocol::protocol::FileSystemPath;
|
||||
use codex_protocol::protocol::FileSystemSandboxEntry;
|
||||
use codex_protocol::protocol::FileSystemSpecialPath;
|
||||
use codex_protocol::protocol::FileSystemSpecialPathKind;
|
||||
use codex_protocol::protocol::RejectConfig;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use tempfile::TempDir;
|
||||
@@ -233,13 +223,13 @@ mod tests {
|
||||
|
||||
assert!(is_write_patch_constrained_to_writable_paths(
|
||||
&add_inside,
|
||||
&FileSystemSandboxPolicy::from(&policy_workspace_only),
|
||||
&policy_workspace_only,
|
||||
&cwd,
|
||||
));
|
||||
|
||||
assert!(!is_write_patch_constrained_to_writable_paths(
|
||||
&add_outside,
|
||||
&FileSystemSandboxPolicy::from(&policy_workspace_only),
|
||||
&policy_workspace_only,
|
||||
&cwd,
|
||||
));
|
||||
|
||||
@@ -254,7 +244,7 @@ mod tests {
|
||||
};
|
||||
assert!(is_write_patch_constrained_to_writable_paths(
|
||||
&add_outside,
|
||||
&FileSystemSandboxPolicy::from(&policy_with_parent),
|
||||
&policy_with_parent,
|
||||
&cwd,
|
||||
));
|
||||
}
|
||||
@@ -274,7 +264,6 @@ mod tests {
|
||||
&add_inside,
|
||||
AskForApproval::OnRequest,
|
||||
&policy,
|
||||
&FileSystemSandboxPolicy::from(&policy),
|
||||
&cwd,
|
||||
WindowsSandboxLevel::Disabled
|
||||
),
|
||||
@@ -305,7 +294,6 @@ mod tests {
|
||||
&add_outside,
|
||||
AskForApproval::OnRequest,
|
||||
&policy_workspace_only,
|
||||
&FileSystemSandboxPolicy::from(&policy_workspace_only),
|
||||
&cwd,
|
||||
WindowsSandboxLevel::Disabled,
|
||||
),
|
||||
@@ -320,7 +308,6 @@ mod tests {
|
||||
mcp_elicitations: false,
|
||||
}),
|
||||
&policy_workspace_only,
|
||||
&FileSystemSandboxPolicy::from(&policy_workspace_only),
|
||||
&cwd,
|
||||
WindowsSandboxLevel::Disabled,
|
||||
),
|
||||
@@ -352,7 +339,6 @@ mod tests {
|
||||
mcp_elicitations: false,
|
||||
}),
|
||||
&policy_workspace_only,
|
||||
&FileSystemSandboxPolicy::from(&policy_workspace_only),
|
||||
&cwd,
|
||||
WindowsSandboxLevel::Disabled,
|
||||
),
|
||||
@@ -362,52 +348,4 @@ mod tests {
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_unreadable_paths_prevent_auto_approval_for_external_sandbox() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let cwd = tmp.path().to_path_buf();
|
||||
let blocked_path = cwd.join("blocked.txt");
|
||||
let action = ApplyPatchAction::new_add_for_test(&blocked_path, "".to_string());
|
||||
let sandbox_policy = SandboxPolicy::ExternalSandbox {
|
||||
network_access: codex_protocol::protocol::NetworkAccess::Restricted,
|
||||
};
|
||||
let file_system_sandbox_policy = FileSystemSandboxPolicy::restricted(vec![
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Special {
|
||||
value: FileSystemSpecialPath {
|
||||
kind: FileSystemSpecialPathKind::Root,
|
||||
subpath: None,
|
||||
},
|
||||
},
|
||||
access: FileSystemAccessMode::Write,
|
||||
},
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Special {
|
||||
value: FileSystemSpecialPath {
|
||||
kind: FileSystemSpecialPathKind::CurrentWorkingDirectory,
|
||||
subpath: Some(PathBuf::from("blocked.txt")),
|
||||
},
|
||||
},
|
||||
access: FileSystemAccessMode::None,
|
||||
},
|
||||
]);
|
||||
|
||||
assert!(!is_write_patch_constrained_to_writable_paths(
|
||||
&action,
|
||||
&file_system_sandbox_policy,
|
||||
&cwd,
|
||||
));
|
||||
assert_eq!(
|
||||
assess_patch_safety(
|
||||
&action,
|
||||
AskForApproval::OnRequest,
|
||||
&sandbox_policy,
|
||||
&file_system_sandbox_policy,
|
||||
&cwd,
|
||||
WindowsSandboxLevel::Disabled,
|
||||
),
|
||||
SafetyCheck::AskUser,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,18 +14,12 @@ use crate::exec::SandboxType;
|
||||
use crate::exec::StdoutStream;
|
||||
use crate::exec::execute_exec_request;
|
||||
use crate::landlock::allow_network_for_proxy;
|
||||
use crate::landlock::create_linux_sandbox_command_args_for_policies;
|
||||
use crate::protocol::FileSystemAccessMode;
|
||||
use crate::protocol::FileSystemPath;
|
||||
use crate::protocol::FileSystemSandboxEntry;
|
||||
use crate::protocol::FileSystemSandboxKind;
|
||||
use crate::protocol::FileSystemSandboxPolicy;
|
||||
use crate::protocol::NetworkSandboxPolicy;
|
||||
use crate::landlock::create_linux_sandbox_command_args;
|
||||
use crate::protocol::SandboxPolicy;
|
||||
#[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;
|
||||
use crate::seatbelt::create_seatbelt_command_args_with_extensions;
|
||||
#[cfg(target_os = "macos")]
|
||||
use crate::spawn::CODEX_SANDBOX_ENV_VAR;
|
||||
use crate::spawn::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR;
|
||||
@@ -36,7 +30,6 @@ use codex_protocol::models::FileSystemPermissions;
|
||||
use codex_protocol::models::MacOsSeatbeltProfileExtensions;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
pub use codex_protocol::models::SandboxPermissions;
|
||||
use codex_protocol::protocol::NetworkAccess;
|
||||
use codex_protocol::protocol::ReadOnlyAccess;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use dunce::canonicalize;
|
||||
@@ -69,8 +62,6 @@ pub struct ExecRequest {
|
||||
pub windows_sandbox_level: WindowsSandboxLevel,
|
||||
pub sandbox_permissions: SandboxPermissions,
|
||||
pub sandbox_policy: SandboxPolicy,
|
||||
pub file_system_sandbox_policy: FileSystemSandboxPolicy,
|
||||
pub network_sandbox_policy: NetworkSandboxPolicy,
|
||||
pub justification: Option<String>,
|
||||
pub arg0: Option<String>,
|
||||
}
|
||||
@@ -81,8 +72,6 @@ pub struct ExecRequest {
|
||||
pub(crate) struct SandboxTransformRequest<'a> {
|
||||
pub spec: CommandSpec,
|
||||
pub policy: &'a SandboxPolicy,
|
||||
pub file_system_policy: &'a FileSystemSandboxPolicy,
|
||||
pub network_policy: NetworkSandboxPolicy,
|
||||
pub sandbox: SandboxType,
|
||||
pub enforce_managed_network: bool,
|
||||
// TODO(viyatb): Evaluate switching this to Option<Arc<NetworkProxy>>
|
||||
@@ -214,39 +203,6 @@ fn additional_permission_roots(
|
||||
)
|
||||
}
|
||||
|
||||
fn merge_file_system_policy_with_additional_permissions(
|
||||
file_system_policy: &FileSystemSandboxPolicy,
|
||||
extra_reads: Vec<AbsolutePathBuf>,
|
||||
extra_writes: Vec<AbsolutePathBuf>,
|
||||
) -> FileSystemSandboxPolicy {
|
||||
match file_system_policy.kind {
|
||||
FileSystemSandboxKind::Restricted => {
|
||||
let mut merged_policy = file_system_policy.clone();
|
||||
for path in extra_reads {
|
||||
let entry = FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Path { path },
|
||||
access: FileSystemAccessMode::Read,
|
||||
};
|
||||
if !merged_policy.entries.contains(&entry) {
|
||||
merged_policy.entries.push(entry);
|
||||
}
|
||||
}
|
||||
for path in extra_writes {
|
||||
let entry = FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Path { path },
|
||||
access: FileSystemAccessMode::Write,
|
||||
};
|
||||
if !merged_policy.entries.contains(&entry) {
|
||||
merged_policy.entries.push(entry);
|
||||
}
|
||||
}
|
||||
merged_policy
|
||||
}
|
||||
FileSystemSandboxKind::Unrestricted | FileSystemSandboxKind::ExternalSandbox => {
|
||||
file_system_policy.clone()
|
||||
}
|
||||
}
|
||||
}
|
||||
fn merge_read_only_access_with_additional_reads(
|
||||
read_only_access: &ReadOnlyAccess,
|
||||
extra_reads: Vec<AbsolutePathBuf>,
|
||||
@@ -290,17 +246,9 @@ fn sandbox_policy_with_additional_permissions(
|
||||
let (extra_reads, extra_writes) = additional_permission_roots(additional_permissions);
|
||||
|
||||
match sandbox_policy {
|
||||
SandboxPolicy::DangerFullAccess => SandboxPolicy::DangerFullAccess,
|
||||
SandboxPolicy::ExternalSandbox { network_access } => SandboxPolicy::ExternalSandbox {
|
||||
network_access: if merge_network_access(
|
||||
network_access.is_enabled(),
|
||||
additional_permissions,
|
||||
) {
|
||||
NetworkAccess::Enabled
|
||||
} else {
|
||||
NetworkAccess::Restricted
|
||||
},
|
||||
},
|
||||
SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. } => {
|
||||
sandbox_policy.clone()
|
||||
}
|
||||
SandboxPolicy::WorkspaceWrite {
|
||||
writable_roots,
|
||||
read_only_access,
|
||||
@@ -349,27 +297,6 @@ fn sandbox_policy_with_additional_permissions(
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn should_require_platform_sandbox(
|
||||
file_system_policy: &FileSystemSandboxPolicy,
|
||||
network_policy: NetworkSandboxPolicy,
|
||||
has_managed_network_requirements: bool,
|
||||
) -> bool {
|
||||
if has_managed_network_requirements {
|
||||
return true;
|
||||
}
|
||||
|
||||
if !network_policy.is_enabled() {
|
||||
return !matches!(
|
||||
file_system_policy.kind,
|
||||
FileSystemSandboxKind::ExternalSandbox
|
||||
);
|
||||
}
|
||||
|
||||
match file_system_policy.kind {
|
||||
FileSystemSandboxKind::Restricted => !file_system_policy.has_full_disk_write_access(),
|
||||
FileSystemSandboxKind::Unrestricted | FileSystemSandboxKind::ExternalSandbox => false,
|
||||
}
|
||||
}
|
||||
#[derive(Default)]
|
||||
pub struct SandboxManager;
|
||||
|
||||
@@ -380,8 +307,7 @@ impl SandboxManager {
|
||||
|
||||
pub(crate) fn select_initial(
|
||||
&self,
|
||||
file_system_policy: &FileSystemSandboxPolicy,
|
||||
network_policy: NetworkSandboxPolicy,
|
||||
policy: &SandboxPolicy,
|
||||
pref: SandboxablePreference,
|
||||
windows_sandbox_level: WindowsSandboxLevel,
|
||||
has_managed_network_requirements: bool,
|
||||
@@ -396,20 +322,22 @@ impl SandboxManager {
|
||||
)
|
||||
.unwrap_or(SandboxType::None)
|
||||
}
|
||||
SandboxablePreference::Auto => {
|
||||
if should_require_platform_sandbox(
|
||||
file_system_policy,
|
||||
network_policy,
|
||||
has_managed_network_requirements,
|
||||
) {
|
||||
crate::safety::get_platform_sandbox(
|
||||
windows_sandbox_level != WindowsSandboxLevel::Disabled,
|
||||
)
|
||||
.unwrap_or(SandboxType::None)
|
||||
} else {
|
||||
SandboxType::None
|
||||
SandboxablePreference::Auto => match policy {
|
||||
SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. } => {
|
||||
if has_managed_network_requirements {
|
||||
crate::safety::get_platform_sandbox(
|
||||
windows_sandbox_level != WindowsSandboxLevel::Disabled,
|
||||
)
|
||||
.unwrap_or(SandboxType::None)
|
||||
} else {
|
||||
SandboxType::None
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => crate::safety::get_platform_sandbox(
|
||||
windows_sandbox_level != WindowsSandboxLevel::Disabled,
|
||||
)
|
||||
.unwrap_or(SandboxType::None),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -420,8 +348,6 @@ impl SandboxManager {
|
||||
let SandboxTransformRequest {
|
||||
mut spec,
|
||||
policy,
|
||||
file_system_policy,
|
||||
network_policy,
|
||||
sandbox,
|
||||
enforce_managed_network,
|
||||
network,
|
||||
@@ -434,36 +360,16 @@ impl SandboxManager {
|
||||
} = request;
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
let macos_seatbelt_profile_extensions = None;
|
||||
let additional_permissions = spec.additional_permissions.take();
|
||||
let EffectiveSandboxPermissions {
|
||||
sandbox_policy: effective_policy,
|
||||
macos_seatbelt_profile_extensions: effective_macos_seatbelt_profile_extensions,
|
||||
} = EffectiveSandboxPermissions::new(
|
||||
let effective_permissions = EffectiveSandboxPermissions::new(
|
||||
policy,
|
||||
macos_seatbelt_profile_extensions,
|
||||
additional_permissions.as_ref(),
|
||||
spec.additional_permissions.as_ref(),
|
||||
);
|
||||
let (effective_file_system_policy, effective_network_policy) =
|
||||
if let Some(additional_permissions) = additional_permissions {
|
||||
let (extra_reads, extra_writes) =
|
||||
additional_permission_roots(&additional_permissions);
|
||||
let file_system_sandbox_policy =
|
||||
if extra_reads.is_empty() && extra_writes.is_empty() {
|
||||
file_system_policy.clone()
|
||||
} else {
|
||||
merge_file_system_policy_with_additional_permissions(
|
||||
file_system_policy,
|
||||
extra_reads,
|
||||
extra_writes,
|
||||
)
|
||||
};
|
||||
let network_sandbox_policy = NetworkSandboxPolicy::from(&effective_policy);
|
||||
(file_system_sandbox_policy, network_sandbox_policy)
|
||||
} else {
|
||||
(file_system_policy.clone(), network_policy)
|
||||
};
|
||||
let mut env = spec.env;
|
||||
if !effective_network_policy.is_enabled() {
|
||||
if !effective_permissions
|
||||
.sandbox_policy
|
||||
.has_full_network_access()
|
||||
{
|
||||
env.insert(
|
||||
CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR.to_string(),
|
||||
"1".to_string(),
|
||||
@@ -480,14 +386,15 @@ impl SandboxManager {
|
||||
SandboxType::MacosSeatbelt => {
|
||||
let mut seatbelt_env = HashMap::new();
|
||||
seatbelt_env.insert(CODEX_SANDBOX_ENV_VAR.to_string(), "seatbelt".to_string());
|
||||
let mut args = create_seatbelt_command_args_for_policies_with_extensions(
|
||||
let mut args = create_seatbelt_command_args_with_extensions(
|
||||
command.clone(),
|
||||
&effective_file_system_policy,
|
||||
effective_network_policy,
|
||||
&effective_permissions.sandbox_policy,
|
||||
sandbox_policy_cwd,
|
||||
enforce_managed_network,
|
||||
network,
|
||||
effective_macos_seatbelt_profile_extensions.as_ref(),
|
||||
effective_permissions
|
||||
.macos_seatbelt_profile_extensions
|
||||
.as_ref(),
|
||||
);
|
||||
let mut full_command = Vec::with_capacity(1 + args.len());
|
||||
full_command.push(MACOS_PATH_TO_SEATBELT_EXECUTABLE.to_string());
|
||||
@@ -500,11 +407,9 @@ impl SandboxManager {
|
||||
let exe = codex_linux_sandbox_exe
|
||||
.ok_or(SandboxTransformError::MissingLinuxSandboxExecutable)?;
|
||||
let allow_proxy_network = allow_network_for_proxy(enforce_managed_network);
|
||||
let mut args = create_linux_sandbox_command_args_for_policies(
|
||||
let mut args = create_linux_sandbox_command_args(
|
||||
command.clone(),
|
||||
&effective_policy,
|
||||
&effective_file_system_policy,
|
||||
effective_network_policy,
|
||||
&effective_permissions.sandbox_policy,
|
||||
sandbox_policy_cwd,
|
||||
use_linux_sandbox_bwrap,
|
||||
allow_proxy_network,
|
||||
@@ -539,9 +444,7 @@ impl SandboxManager {
|
||||
sandbox,
|
||||
windows_sandbox_level,
|
||||
sandbox_permissions: spec.sandbox_permissions,
|
||||
sandbox_policy: effective_policy,
|
||||
file_system_sandbox_policy: effective_file_system_policy,
|
||||
network_sandbox_policy: effective_network_policy,
|
||||
sandbox_policy: effective_permissions.sandbox_policy,
|
||||
justification: spec.justification,
|
||||
arg0: arg0_override,
|
||||
})
|
||||
@@ -574,19 +477,9 @@ mod tests {
|
||||
#[cfg(target_os = "macos")]
|
||||
use super::EffectiveSandboxPermissions;
|
||||
use super::SandboxManager;
|
||||
use super::merge_file_system_policy_with_additional_permissions;
|
||||
use super::normalize_additional_permissions;
|
||||
use super::sandbox_policy_with_additional_permissions;
|
||||
use super::should_require_platform_sandbox;
|
||||
use crate::exec::SandboxType;
|
||||
use crate::protocol::FileSystemAccessMode;
|
||||
use crate::protocol::FileSystemPath;
|
||||
use crate::protocol::FileSystemSandboxEntry;
|
||||
use crate::protocol::FileSystemSandboxPolicy;
|
||||
use crate::protocol::FileSystemSpecialPath;
|
||||
use crate::protocol::FileSystemSpecialPathKind;
|
||||
use crate::protocol::NetworkAccess;
|
||||
use crate::protocol::NetworkSandboxPolicy;
|
||||
use crate::protocol::ReadOnlyAccess;
|
||||
use crate::protocol::SandboxPolicy;
|
||||
use crate::tools::sandboxing::SandboxablePreference;
|
||||
@@ -603,15 +496,13 @@ mod tests {
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use dunce::canonicalize;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::collections::HashMap;
|
||||
use tempfile::TempDir;
|
||||
|
||||
#[test]
|
||||
fn danger_full_access_defaults_to_no_sandbox_without_network_requirements() {
|
||||
let manager = SandboxManager::new();
|
||||
let sandbox = manager.select_initial(
|
||||
&FileSystemSandboxPolicy::unrestricted(),
|
||||
NetworkSandboxPolicy::Enabled,
|
||||
&SandboxPolicy::DangerFullAccess,
|
||||
SandboxablePreference::Auto,
|
||||
WindowsSandboxLevel::Disabled,
|
||||
false,
|
||||
@@ -624,8 +515,7 @@ mod tests {
|
||||
let manager = SandboxManager::new();
|
||||
let expected = crate::safety::get_platform_sandbox(false).unwrap_or(SandboxType::None);
|
||||
let sandbox = manager.select_initial(
|
||||
&FileSystemSandboxPolicy::unrestricted(),
|
||||
NetworkSandboxPolicy::Enabled,
|
||||
&SandboxPolicy::DangerFullAccess,
|
||||
SandboxablePreference::Auto,
|
||||
WindowsSandboxLevel::Disabled,
|
||||
true,
|
||||
@@ -633,136 +523,6 @@ mod tests {
|
||||
assert_eq!(sandbox, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn restricted_file_system_uses_platform_sandbox_without_managed_network() {
|
||||
let manager = SandboxManager::new();
|
||||
let expected = crate::safety::get_platform_sandbox(false).unwrap_or(SandboxType::None);
|
||||
let sandbox = manager.select_initial(
|
||||
&FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Special {
|
||||
value: FileSystemSpecialPath {
|
||||
kind: FileSystemSpecialPathKind::Root,
|
||||
subpath: None,
|
||||
},
|
||||
},
|
||||
access: FileSystemAccessMode::Read,
|
||||
}]),
|
||||
NetworkSandboxPolicy::Enabled,
|
||||
SandboxablePreference::Auto,
|
||||
WindowsSandboxLevel::Disabled,
|
||||
false,
|
||||
);
|
||||
assert_eq!(sandbox, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn full_access_restricted_policy_skips_platform_sandbox_when_network_is_enabled() {
|
||||
let policy = FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Special {
|
||||
value: FileSystemSpecialPath {
|
||||
kind: FileSystemSpecialPathKind::Root,
|
||||
subpath: None,
|
||||
},
|
||||
},
|
||||
access: FileSystemAccessMode::Write,
|
||||
}]);
|
||||
|
||||
assert_eq!(
|
||||
should_require_platform_sandbox(&policy, NetworkSandboxPolicy::Enabled, false),
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn root_write_policy_with_carveouts_still_uses_platform_sandbox() {
|
||||
let policy = FileSystemSandboxPolicy::restricted(vec![
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Special {
|
||||
value: FileSystemSpecialPath {
|
||||
kind: FileSystemSpecialPathKind::Root,
|
||||
subpath: None,
|
||||
},
|
||||
},
|
||||
access: FileSystemAccessMode::Write,
|
||||
},
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Special {
|
||||
value: FileSystemSpecialPath {
|
||||
kind: FileSystemSpecialPathKind::CurrentWorkingDirectory,
|
||||
subpath: Some("blocked".into()),
|
||||
},
|
||||
},
|
||||
access: FileSystemAccessMode::None,
|
||||
},
|
||||
]);
|
||||
|
||||
assert_eq!(
|
||||
should_require_platform_sandbox(&policy, NetworkSandboxPolicy::Enabled, false),
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn full_access_restricted_policy_still_uses_platform_sandbox_for_restricted_network() {
|
||||
let policy = FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Special {
|
||||
value: FileSystemSpecialPath {
|
||||
kind: FileSystemSpecialPathKind::Root,
|
||||
subpath: None,
|
||||
},
|
||||
},
|
||||
access: FileSystemAccessMode::Write,
|
||||
}]);
|
||||
|
||||
assert_eq!(
|
||||
should_require_platform_sandbox(&policy, NetworkSandboxPolicy::Restricted, false),
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transform_preserves_unrestricted_file_system_policy_for_restricted_network() {
|
||||
let manager = SandboxManager::new();
|
||||
let cwd = std::env::current_dir().expect("current dir");
|
||||
let exec_request = manager
|
||||
.transform(super::SandboxTransformRequest {
|
||||
spec: super::CommandSpec {
|
||||
program: "true".to_string(),
|
||||
args: Vec::new(),
|
||||
cwd: cwd.clone(),
|
||||
env: HashMap::new(),
|
||||
expiration: crate::exec::ExecExpiration::DefaultTimeout,
|
||||
sandbox_permissions: super::SandboxPermissions::UseDefault,
|
||||
additional_permissions: None,
|
||||
justification: None,
|
||||
},
|
||||
policy: &SandboxPolicy::ExternalSandbox {
|
||||
network_access: crate::protocol::NetworkAccess::Restricted,
|
||||
},
|
||||
file_system_policy: &FileSystemSandboxPolicy::unrestricted(),
|
||||
network_policy: NetworkSandboxPolicy::Restricted,
|
||||
sandbox: SandboxType::None,
|
||||
enforce_managed_network: false,
|
||||
network: None,
|
||||
sandbox_policy_cwd: cwd.as_path(),
|
||||
#[cfg(target_os = "macos")]
|
||||
macos_seatbelt_profile_extensions: None,
|
||||
codex_linux_sandbox_exe: None,
|
||||
use_linux_sandbox_bwrap: false,
|
||||
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
||||
})
|
||||
.expect("transform");
|
||||
|
||||
assert_eq!(
|
||||
exec_request.file_system_sandbox_policy,
|
||||
FileSystemSandboxPolicy::unrestricted()
|
||||
);
|
||||
assert_eq!(
|
||||
exec_request.network_sandbox_policy,
|
||||
NetworkSandboxPolicy::Restricted
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalize_additional_permissions_preserves_network() {
|
||||
let temp_dir = TempDir::new().expect("create temp dir");
|
||||
@@ -864,6 +624,7 @@ mod tests {
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[test]
|
||||
fn effective_permissions_merge_macos_extensions_with_additional_permissions() {
|
||||
@@ -918,141 +679,4 @@ mod tests {
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn external_sandbox_additional_permissions_can_enable_network() {
|
||||
let temp_dir = TempDir::new().expect("create temp dir");
|
||||
let path = AbsolutePathBuf::from_absolute_path(
|
||||
canonicalize(temp_dir.path()).expect("canonicalize temp dir"),
|
||||
)
|
||||
.expect("absolute temp dir");
|
||||
let policy = sandbox_policy_with_additional_permissions(
|
||||
&SandboxPolicy::ExternalSandbox {
|
||||
network_access: NetworkAccess::Restricted,
|
||||
},
|
||||
&PermissionProfile {
|
||||
network: Some(NetworkPermissions {
|
||||
enabled: Some(true),
|
||||
}),
|
||||
file_system: Some(FileSystemPermissions {
|
||||
read: Some(vec![path]),
|
||||
write: Some(Vec::new()),
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
policy,
|
||||
SandboxPolicy::ExternalSandbox {
|
||||
network_access: NetworkAccess::Enabled,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transform_additional_permissions_enable_network_for_external_sandbox() {
|
||||
let manager = SandboxManager::new();
|
||||
let cwd = std::env::current_dir().expect("current dir");
|
||||
let temp_dir = TempDir::new().expect("create temp dir");
|
||||
let path = AbsolutePathBuf::from_absolute_path(
|
||||
canonicalize(temp_dir.path()).expect("canonicalize temp dir"),
|
||||
)
|
||||
.expect("absolute temp dir");
|
||||
let exec_request = manager
|
||||
.transform(super::SandboxTransformRequest {
|
||||
spec: super::CommandSpec {
|
||||
program: "true".to_string(),
|
||||
args: Vec::new(),
|
||||
cwd: cwd.clone(),
|
||||
env: HashMap::new(),
|
||||
expiration: crate::exec::ExecExpiration::DefaultTimeout,
|
||||
sandbox_permissions: super::SandboxPermissions::WithAdditionalPermissions,
|
||||
additional_permissions: Some(PermissionProfile {
|
||||
network: Some(NetworkPermissions {
|
||||
enabled: Some(true),
|
||||
}),
|
||||
file_system: Some(FileSystemPermissions {
|
||||
read: Some(vec![path]),
|
||||
write: Some(Vec::new()),
|
||||
}),
|
||||
..Default::default()
|
||||
}),
|
||||
justification: None,
|
||||
},
|
||||
policy: &SandboxPolicy::ExternalSandbox {
|
||||
network_access: NetworkAccess::Restricted,
|
||||
},
|
||||
file_system_policy: &FileSystemSandboxPolicy::unrestricted(),
|
||||
network_policy: NetworkSandboxPolicy::Restricted,
|
||||
sandbox: SandboxType::None,
|
||||
enforce_managed_network: false,
|
||||
network: None,
|
||||
sandbox_policy_cwd: cwd.as_path(),
|
||||
#[cfg(target_os = "macos")]
|
||||
macos_seatbelt_profile_extensions: None,
|
||||
codex_linux_sandbox_exe: None,
|
||||
use_linux_sandbox_bwrap: false,
|
||||
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
||||
})
|
||||
.expect("transform");
|
||||
|
||||
assert_eq!(
|
||||
exec_request.sandbox_policy,
|
||||
SandboxPolicy::ExternalSandbox {
|
||||
network_access: NetworkAccess::Enabled,
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
exec_request.network_sandbox_policy,
|
||||
NetworkSandboxPolicy::Enabled
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge_file_system_policy_with_additional_permissions_preserves_unreadable_roots() {
|
||||
let temp_dir = TempDir::new().expect("create temp dir");
|
||||
let cwd = AbsolutePathBuf::from_absolute_path(
|
||||
canonicalize(temp_dir.path()).expect("canonicalize temp dir"),
|
||||
)
|
||||
.expect("absolute temp dir");
|
||||
let allowed_path = cwd.join("allowed").expect("allowed path");
|
||||
let denied_path = cwd.join("denied").expect("denied path");
|
||||
let merged_policy = merge_file_system_policy_with_additional_permissions(
|
||||
&FileSystemSandboxPolicy::restricted(vec![
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Special {
|
||||
value: FileSystemSpecialPath {
|
||||
kind: FileSystemSpecialPathKind::Root,
|
||||
subpath: None,
|
||||
},
|
||||
},
|
||||
access: FileSystemAccessMode::Read,
|
||||
},
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Path {
|
||||
path: denied_path.clone(),
|
||||
},
|
||||
access: FileSystemAccessMode::None,
|
||||
},
|
||||
]),
|
||||
vec![allowed_path.clone()],
|
||||
Vec::new(),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
merged_policy.entries.contains(&FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Path { path: denied_path },
|
||||
access: FileSystemAccessMode::None,
|
||||
}),
|
||||
true
|
||||
);
|
||||
assert_eq!(
|
||||
merged_policy.entries.contains(&FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Path { path: allowed_path },
|
||||
access: FileSystemAccessMode::Read,
|
||||
}),
|
||||
true
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,8 +15,6 @@ use tokio::process::Child;
|
||||
use tracing::warn;
|
||||
use url::Url;
|
||||
|
||||
use crate::protocol::FileSystemSandboxPolicy;
|
||||
use crate::protocol::NetworkSandboxPolicy;
|
||||
use crate::protocol::SandboxPolicy;
|
||||
use crate::seatbelt_permissions::MacOsSeatbeltProfileExtensions;
|
||||
use crate::seatbelt_permissions::build_seatbelt_extensions;
|
||||
@@ -53,7 +51,7 @@ pub async fn spawn_command_under_seatbelt(
|
||||
args,
|
||||
arg0,
|
||||
cwd: command_cwd,
|
||||
network_sandbox_policy: NetworkSandboxPolicy::from(sandbox_policy),
|
||||
sandbox_policy,
|
||||
network,
|
||||
stdio_policy,
|
||||
env,
|
||||
@@ -261,23 +259,10 @@ fn unix_socket_policy(proxy: &ProxyPolicyInputs) -> String {
|
||||
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;
|
||||
@@ -302,19 +287,7 @@ fn dynamic_network_policy_for_network(
|
||||
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() {
|
||||
if sandbox_policy.has_full_network_access() {
|
||||
// No proxy env is configured: retain the existing full-network behavior.
|
||||
format!(
|
||||
"(allow network-outbound)\n(allow network-inbound)\n{MACOS_SEATBELT_NETWORK_POLICY}"
|
||||
@@ -331,28 +304,9 @@ pub(crate) fn create_seatbelt_command_args(
|
||||
enforce_managed_network: bool,
|
||||
network: Option<&NetworkProxy>,
|
||||
) -> Vec<String> {
|
||||
create_seatbelt_command_args_for_policies(
|
||||
create_seatbelt_command_args_with_extensions(
|
||||
command,
|
||||
&FileSystemSandboxPolicy::from(sandbox_policy),
|
||||
NetworkSandboxPolicy::from(sandbox_policy),
|
||||
sandbox_policy_cwd,
|
||||
enforce_managed_network,
|
||||
network,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn create_seatbelt_command_args_for_policies(
|
||||
command: Vec<String>,
|
||||
file_system_sandbox_policy: &FileSystemSandboxPolicy,
|
||||
network_sandbox_policy: NetworkSandboxPolicy,
|
||||
sandbox_policy_cwd: &Path,
|
||||
enforce_managed_network: bool,
|
||||
network: Option<&NetworkProxy>,
|
||||
) -> Vec<String> {
|
||||
create_seatbelt_command_args_for_policies_with_extensions(
|
||||
command,
|
||||
file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
sandbox_policy,
|
||||
sandbox_policy_cwd,
|
||||
enforce_managed_network,
|
||||
network,
|
||||
@@ -360,64 +314,6 @@ pub(crate) fn create_seatbelt_command_args_for_policies(
|
||||
)
|
||||
}
|
||||
|
||||
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,
|
||||
@@ -426,132 +322,101 @@ pub(crate) fn create_seatbelt_command_args_with_extensions(
|
||||
network: Option<&NetworkProxy>,
|
||||
extensions: Option<&MacOsSeatbeltProfileExtensions>,
|
||||
) -> Vec<String> {
|
||||
create_seatbelt_command_args_for_policies_with_extensions(
|
||||
command,
|
||||
&FileSystemSandboxPolicy::from(sandbox_policy),
|
||||
NetworkSandboxPolicy::from(sandbox_policy),
|
||||
sandbox_policy_cwd,
|
||||
enforce_managed_network,
|
||||
network,
|
||||
extensions,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) 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> {
|
||||
create_seatbelt_command_args_from_policies_inner(
|
||||
command,
|
||||
file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
sandbox_policy_cwd,
|
||||
enforce_managed_network,
|
||||
network,
|
||||
extensions,
|
||||
)
|
||||
}
|
||||
|
||||
fn create_seatbelt_command_args_from_policies_inner(
|
||||
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_write_policy, file_write_dir_params) = {
|
||||
if sandbox_policy.has_full_disk_write_access() {
|
||||
// Allegedly, this is more permissive than `(allow file-write*)`.
|
||||
(
|
||||
r#"(allow file-write* (regex #"^/"))"#.to_string(),
|
||||
Vec::new(),
|
||||
)
|
||||
};
|
||||
|
||||
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 writable_roots = sandbox_policy.get_writable_roots_with_cwd(sandbox_policy_cwd);
|
||||
|
||||
let mut writable_folder_policies: Vec<String> = Vec::new();
|
||||
let mut file_write_params = Vec::new();
|
||||
|
||||
for (index, wr) in writable_roots.iter().enumerate() {
|
||||
// Canonicalize to avoid mismatches like /var vs /private/var on macOS.
|
||||
let canonical_root = wr
|
||||
.root
|
||||
.as_path()
|
||||
.canonicalize()
|
||||
.unwrap_or_else(|_| wr.root.to_path_buf());
|
||||
let root_param = format!("WRITABLE_ROOT_{index}");
|
||||
file_write_params.push((root_param.clone(), canonical_root));
|
||||
|
||||
if wr.read_only_subpaths.is_empty() {
|
||||
writable_folder_policies.push(format!("(subpath (param \"{root_param}\"))"));
|
||||
} else {
|
||||
// Add parameters for each read-only subpath and generate
|
||||
// the `(require-not ...)` clauses.
|
||||
let mut require_parts: Vec<String> = Vec::new();
|
||||
require_parts.push(format!("(subpath (param \"{root_param}\"))"));
|
||||
for (subpath_index, ro) in wr.read_only_subpaths.iter().enumerate() {
|
||||
let canonical_ro = ro
|
||||
.as_path()
|
||||
.canonicalize()
|
||||
.unwrap_or_else(|_| ro.to_path_buf());
|
||||
let ro_param = format!("WRITABLE_ROOT_{index}_RO_{subpath_index}");
|
||||
require_parts
|
||||
.push(format!("(require-not (subpath (param \"{ro_param}\")))"));
|
||||
file_write_params.push((ro_param, canonical_ro));
|
||||
}
|
||||
let policy_component = format!("(require-all {} )", require_parts.join(" "));
|
||||
writable_folder_policies.push(policy_component);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if writable_folder_policies.is_empty() {
|
||||
("".to_string(), Vec::new())
|
||||
} else {
|
||||
let file_write_policy = format!(
|
||||
"(allow file-write*\n{}\n)",
|
||||
writable_folder_policies.join(" ")
|
||||
);
|
||||
(file_write_policy, file_write_params)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let (file_read_policy, file_read_dir_params) = if sandbox_policy.has_full_disk_read_access() {
|
||||
(
|
||||
"; allow read-only file operations\n(allow file-read*)".to_string(),
|
||||
Vec::new(),
|
||||
)
|
||||
} else {
|
||||
let mut readable_roots_policies: Vec<String> = Vec::new();
|
||||
let mut file_read_params = Vec::new();
|
||||
for (index, root) in sandbox_policy
|
||||
.get_readable_roots_with_cwd(sandbox_policy_cwd)
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
{
|
||||
// Canonicalize to avoid mismatches like /var vs /private/var on macOS.
|
||||
let canonical_root = root
|
||||
.as_path()
|
||||
.canonicalize()
|
||||
.unwrap_or_else(|_| root.to_path_buf());
|
||||
let root_param = format!("READABLE_ROOT_{index}");
|
||||
file_read_params.push((root_param.clone(), canonical_root));
|
||||
readable_roots_policies.push(format!("(subpath (param \"{root_param}\"))"));
|
||||
}
|
||||
|
||||
if readable_roots_policies.is_empty() {
|
||||
("".to_string(), Vec::new())
|
||||
} else {
|
||||
(
|
||||
format!(
|
||||
"; allow read-only file operations\n(allow file-read*\n{}\n)",
|
||||
readable_roots_policies.join(" ")
|
||||
),
|
||||
file_read_params,
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
let proxy = proxy_policy_inputs(network);
|
||||
let network_policy =
|
||||
dynamic_network_policy_for_network(network_sandbox_policy, enforce_managed_network, &proxy);
|
||||
let network_policy = dynamic_network_policy(sandbox_policy, enforce_managed_network, &proxy);
|
||||
let seatbelt_extensions = extensions.map_or_else(
|
||||
|| {
|
||||
// Backward-compatibility default when no extension profile is provided.
|
||||
@@ -560,7 +425,7 @@ fn create_seatbelt_command_args_from_policies_inner(
|
||||
build_seatbelt_extensions,
|
||||
);
|
||||
|
||||
let include_platform_defaults = file_system_sandbox_policy.include_platform_defaults();
|
||||
let include_platform_defaults = sandbox_policy.include_platform_defaults();
|
||||
let mut policy_sections = vec![
|
||||
MACOS_SEATBELT_BASE_POLICY.to_string(),
|
||||
file_read_policy,
|
||||
@@ -627,18 +492,12 @@ mod tests {
|
||||
use super::ProxyPolicyInputs;
|
||||
use super::UnixDomainSocketPolicy;
|
||||
use super::create_seatbelt_command_args;
|
||||
use super::create_seatbelt_command_args_for_policies;
|
||||
use super::create_seatbelt_command_args_with_extensions;
|
||||
use super::dynamic_network_policy;
|
||||
use super::macos_dir_params;
|
||||
use super::normalize_path_for_sandbox;
|
||||
use super::unix_socket_dir_params;
|
||||
use super::unix_socket_policy;
|
||||
use crate::protocol::FileSystemAccessMode;
|
||||
use crate::protocol::FileSystemPath;
|
||||
use crate::protocol::FileSystemSandboxEntry;
|
||||
use crate::protocol::FileSystemSandboxPolicy;
|
||||
use crate::protocol::NetworkSandboxPolicy;
|
||||
use crate::protocol::SandboxPolicy;
|
||||
use crate::seatbelt::MACOS_PATH_TO_SEATBELT_EXECUTABLE;
|
||||
use crate::seatbelt_permissions::MacOsAutomationPermission;
|
||||
@@ -666,15 +525,6 @@ mod tests {
|
||||
AbsolutePathBuf::from_absolute_path(Path::new(path)).expect("absolute path")
|
||||
}
|
||||
|
||||
fn seatbelt_policy_arg(args: &[String]) -> &str {
|
||||
let policy_index = args
|
||||
.iter()
|
||||
.position(|arg| arg == "-p")
|
||||
.expect("seatbelt args should include -p");
|
||||
args.get(policy_index + 1)
|
||||
.expect("seatbelt args should include policy text")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn base_policy_allows_node_cpu_sysctls() {
|
||||
assert!(
|
||||
@@ -722,96 +572,6 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_unreadable_paths_are_excluded_from_full_disk_read_and_write_access() {
|
||||
let unreadable = absolute_path("/tmp/codex-unreadable");
|
||||
let file_system_policy = FileSystemSandboxPolicy::restricted(vec![
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Special {
|
||||
value: crate::protocol::FileSystemSpecialPath {
|
||||
kind: crate::protocol::FileSystemSpecialPathKind::Root,
|
||||
subpath: None,
|
||||
},
|
||||
},
|
||||
access: FileSystemAccessMode::Write,
|
||||
},
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Path { path: unreadable },
|
||||
access: FileSystemAccessMode::None,
|
||||
},
|
||||
]);
|
||||
|
||||
let args = create_seatbelt_command_args_for_policies(
|
||||
vec!["/bin/true".to_string()],
|
||||
&file_system_policy,
|
||||
NetworkSandboxPolicy::Restricted,
|
||||
Path::new("/"),
|
||||
false,
|
||||
None,
|
||||
);
|
||||
|
||||
let policy = seatbelt_policy_arg(&args);
|
||||
assert!(
|
||||
policy.contains("(require-not (subpath (param \"READABLE_ROOT_0_RO_0\")))"),
|
||||
"expected read carveout in policy:\n{policy}"
|
||||
);
|
||||
assert!(
|
||||
policy.contains("(require-not (subpath (param \"WRITABLE_ROOT_0_RO_0\")))"),
|
||||
"expected write carveout in policy:\n{policy}"
|
||||
);
|
||||
assert!(
|
||||
args.iter()
|
||||
.any(|arg| arg == "-DREADABLE_ROOT_0_RO_0=/tmp/codex-unreadable"),
|
||||
"expected read carveout parameter in args: {args:#?}"
|
||||
);
|
||||
assert!(
|
||||
args.iter()
|
||||
.any(|arg| arg == "-DWRITABLE_ROOT_0_RO_0=/tmp/codex-unreadable"),
|
||||
"expected write carveout parameter in args: {args:#?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_unreadable_paths_are_excluded_from_readable_roots() {
|
||||
let root = absolute_path("/tmp/codex-readable");
|
||||
let unreadable = absolute_path("/tmp/codex-readable/private");
|
||||
let file_system_policy = FileSystemSandboxPolicy::restricted(vec![
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Path { path: root },
|
||||
access: FileSystemAccessMode::Read,
|
||||
},
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Path { path: unreadable },
|
||||
access: FileSystemAccessMode::None,
|
||||
},
|
||||
]);
|
||||
|
||||
let args = create_seatbelt_command_args_for_policies(
|
||||
vec!["/bin/true".to_string()],
|
||||
&file_system_policy,
|
||||
NetworkSandboxPolicy::Restricted,
|
||||
Path::new("/"),
|
||||
false,
|
||||
None,
|
||||
);
|
||||
|
||||
let policy = seatbelt_policy_arg(&args);
|
||||
assert!(
|
||||
policy.contains("(require-not (subpath (param \"READABLE_ROOT_0_RO_0\")))"),
|
||||
"expected read carveout in policy:\n{policy}"
|
||||
);
|
||||
assert!(
|
||||
args.iter()
|
||||
.any(|arg| arg == "-DREADABLE_ROOT_0=/tmp/codex-readable"),
|
||||
"expected readable root parameter in args: {args:#?}"
|
||||
);
|
||||
assert!(
|
||||
args.iter()
|
||||
.any(|arg| arg == "-DREADABLE_ROOT_0_RO_0=/tmp/codex-readable/private"),
|
||||
"expected read carveout parameter in args: {args:#?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn seatbelt_args_include_macos_permission_extensions() {
|
||||
let cwd = std::env::temp_dir();
|
||||
@@ -1230,7 +990,7 @@ sys.exit(0 if allowed else 13)
|
||||
; allow read-only file operations
|
||||
(allow file-read*)
|
||||
(allow file-write*
|
||||
(subpath (param "WRITABLE_ROOT_0")) (require-all (subpath (param "WRITABLE_ROOT_1")) (require-not (subpath (param "WRITABLE_ROOT_1_RO_0"))) (require-not (subpath (param "WRITABLE_ROOT_1_RO_1"))) ) (subpath (param "WRITABLE_ROOT_2"))
|
||||
(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
|
||||
@@ -1243,51 +1003,43 @@ sys.exit(0 if allowed else 13)
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_eq!(seatbelt_policy_arg(&args), expected_policy);
|
||||
|
||||
let expected_definitions = [
|
||||
let mut expected_args = vec![
|
||||
"-p".to_string(),
|
||||
expected_policy,
|
||||
format!(
|
||||
"-DWRITABLE_ROOT_0={}",
|
||||
vulnerable_root_canonical.to_string_lossy()
|
||||
),
|
||||
format!(
|
||||
"-DWRITABLE_ROOT_0_RO_0={}",
|
||||
dot_git_canonical.to_string_lossy()
|
||||
),
|
||||
format!(
|
||||
"-DWRITABLE_ROOT_0_RO_1={}",
|
||||
dot_codex_canonical.to_string_lossy()
|
||||
),
|
||||
format!(
|
||||
"-DWRITABLE_ROOT_1={}",
|
||||
empty_root_canonical.to_string_lossy()
|
||||
),
|
||||
format!(
|
||||
"-DWRITABLE_ROOT_2={}",
|
||||
cwd.canonicalize()
|
||||
.expect("canonicalize cwd")
|
||||
.to_string_lossy()
|
||||
),
|
||||
format!(
|
||||
"-DWRITABLE_ROOT_1={}",
|
||||
vulnerable_root_canonical.to_string_lossy()
|
||||
),
|
||||
format!(
|
||||
"-DWRITABLE_ROOT_1_RO_0={}",
|
||||
dot_git_canonical.to_string_lossy()
|
||||
),
|
||||
format!(
|
||||
"-DWRITABLE_ROOT_1_RO_1={}",
|
||||
dot_codex_canonical.to_string_lossy()
|
||||
),
|
||||
format!(
|
||||
"-DWRITABLE_ROOT_2={}",
|
||||
empty_root_canonical.to_string_lossy()
|
||||
),
|
||||
];
|
||||
for expected_definition in expected_definitions {
|
||||
assert!(
|
||||
args.contains(&expected_definition),
|
||||
"expected definition arg `{expected_definition}` in {args:#?}"
|
||||
);
|
||||
}
|
||||
for (key, value) in macos_dir_params() {
|
||||
let expected_definition = format!("-D{key}={}", value.to_string_lossy());
|
||||
assert!(
|
||||
args.contains(&expected_definition),
|
||||
"expected definition arg `{expected_definition}` in {args:#?}"
|
||||
);
|
||||
}
|
||||
|
||||
let command_index = args
|
||||
.iter()
|
||||
.position(|arg| arg == "--")
|
||||
.expect("seatbelt args should include command separator");
|
||||
assert_eq!(args[command_index + 1..], shell_command);
|
||||
expected_args.extend(
|
||||
macos_dir_params()
|
||||
.into_iter()
|
||||
.map(|(key, value)| format!("-D{key}={value}", value = value.to_string_lossy())),
|
||||
);
|
||||
|
||||
expected_args.push("--".to_string());
|
||||
expected_args.extend(shell_command);
|
||||
|
||||
assert_eq!(expected_args, args);
|
||||
|
||||
// Verify that .codex/config.toml cannot be modified under the generated
|
||||
// Seatbelt policy.
|
||||
|
||||
@@ -6,13 +6,13 @@ use tokio::process::Child;
|
||||
use tokio::process::Command;
|
||||
use tracing::trace;
|
||||
|
||||
use crate::protocol::NetworkSandboxPolicy;
|
||||
use crate::protocol::SandboxPolicy;
|
||||
|
||||
/// Experimental environment variable that will be set to some non-empty value
|
||||
/// if both of the following are true:
|
||||
///
|
||||
/// 1. The process was spawned by Codex as part of a shell tool call.
|
||||
/// 2. NetworkSandboxPolicy is restricted for the tool call.
|
||||
/// 2. SandboxPolicy.has_full_network_access() was false for the tool call.
|
||||
///
|
||||
/// We may try to have just one environment variable for all sandboxing
|
||||
/// attributes, so this may change in the future.
|
||||
@@ -33,15 +33,15 @@ pub enum StdioPolicy {
|
||||
/// ensuring the args and environment variables used to create the `Command`
|
||||
/// (and `Child`) honor the configuration.
|
||||
///
|
||||
/// For now, we take `NetworkSandboxPolicy` as a parameter to spawn_child()
|
||||
/// because we need to determine whether to set the
|
||||
/// For now, we take `SandboxPolicy` as a parameter to spawn_child() because
|
||||
/// we need to determine whether to set the
|
||||
/// `CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR` environment variable.
|
||||
pub(crate) struct SpawnChildRequest<'a> {
|
||||
pub program: PathBuf,
|
||||
pub args: Vec<String>,
|
||||
pub arg0: Option<&'a str>,
|
||||
pub cwd: PathBuf,
|
||||
pub network_sandbox_policy: NetworkSandboxPolicy,
|
||||
pub sandbox_policy: &'a SandboxPolicy,
|
||||
pub network: Option<&'a NetworkProxy>,
|
||||
pub stdio_policy: StdioPolicy,
|
||||
pub env: HashMap<String, String>,
|
||||
@@ -53,14 +53,14 @@ pub(crate) async fn spawn_child_async(request: SpawnChildRequest<'_>) -> std::io
|
||||
args,
|
||||
arg0,
|
||||
cwd,
|
||||
network_sandbox_policy,
|
||||
sandbox_policy,
|
||||
network,
|
||||
stdio_policy,
|
||||
mut env,
|
||||
} = request;
|
||||
|
||||
trace!(
|
||||
"spawn_child_async: {program:?} {args:?} {arg0:?} {cwd:?} {network_sandbox_policy:?} {stdio_policy:?} {env:?}"
|
||||
"spawn_child_async: {program:?} {args:?} {arg0:?} {cwd:?} {sandbox_policy:?} {stdio_policy:?} {env:?}"
|
||||
);
|
||||
|
||||
let mut cmd = Command::new(&program);
|
||||
@@ -74,7 +74,7 @@ pub(crate) async fn spawn_child_async(request: SpawnChildRequest<'_>) -> std::io
|
||||
cmd.env_clear();
|
||||
cmd.envs(env);
|
||||
|
||||
if !network_sandbox_policy.is_enabled() {
|
||||
if !sandbox_policy.has_full_network_access() {
|
||||
cmd.env(CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR, "1");
|
||||
}
|
||||
|
||||
|
||||
@@ -22,8 +22,6 @@ use crate::protocol::ExecCommandBeginEvent;
|
||||
use crate::protocol::ExecCommandEndEvent;
|
||||
use crate::protocol::ExecCommandSource;
|
||||
use crate::protocol::ExecCommandStatus;
|
||||
use crate::protocol::FileSystemSandboxPolicy;
|
||||
use crate::protocol::NetworkSandboxPolicy;
|
||||
use crate::protocol::SandboxPolicy;
|
||||
use crate::protocol::TurnStartedEvent;
|
||||
use crate::sandboxing::ExecRequest;
|
||||
@@ -169,8 +167,6 @@ pub(crate) async fn execute_user_shell_command(
|
||||
windows_sandbox_level: turn_context.windows_sandbox_level,
|
||||
sandbox_permissions: SandboxPermissions::UseDefault,
|
||||
sandbox_policy: sandbox_policy.clone(),
|
||||
file_system_sandbox_policy: FileSystemSandboxPolicy::from(&sandbox_policy),
|
||||
network_sandbox_policy: NetworkSandboxPolicy::from(&sandbox_policy),
|
||||
justification: None,
|
||||
arg0: None,
|
||||
};
|
||||
|
||||
@@ -852,8 +852,7 @@ impl JsReplManager {
|
||||
.network
|
||||
.is_some();
|
||||
let sandbox_type = sandbox.select_initial(
|
||||
&turn.file_system_sandbox_policy,
|
||||
turn.network_sandbox_policy,
|
||||
&turn.sandbox_policy,
|
||||
SandboxablePreference::Auto,
|
||||
turn.windows_sandbox_level,
|
||||
has_managed_network_requirements,
|
||||
@@ -862,8 +861,6 @@ impl JsReplManager {
|
||||
.transform(crate::sandboxing::SandboxTransformRequest {
|
||||
spec,
|
||||
policy: &turn.sandbox_policy,
|
||||
file_system_policy: &turn.file_system_sandbox_policy,
|
||||
network_policy: turn.network_sandbox_policy,
|
||||
sandbox: sandbox_type,
|
||||
enforce_managed_network: has_managed_network_requirements,
|
||||
network: None,
|
||||
@@ -1750,16 +1747,6 @@ mod tests {
|
||||
use std::path::Path;
|
||||
use tempfile::tempdir;
|
||||
|
||||
fn set_danger_full_access(turn: &mut crate::codex::TurnContext) {
|
||||
turn.sandbox_policy
|
||||
.set(SandboxPolicy::DangerFullAccess)
|
||||
.expect("test setup should allow updating sandbox policy");
|
||||
turn.file_system_sandbox_policy =
|
||||
crate::protocol::FileSystemSandboxPolicy::from(turn.sandbox_policy.get());
|
||||
turn.network_sandbox_policy =
|
||||
crate::protocol::NetworkSandboxPolicy::from(turn.sandbox_policy.get());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn node_version_parses_v_prefix_and_suffix() {
|
||||
let version = NodeVersion::parse("v25.1.0-nightly.2024").unwrap();
|
||||
@@ -2477,7 +2464,9 @@ mod tests {
|
||||
turn.approval_policy
|
||||
.set(AskForApproval::Never)
|
||||
.expect("test setup should allow updating approval policy");
|
||||
set_danger_full_access(&mut turn);
|
||||
turn.sandbox_policy
|
||||
.set(SandboxPolicy::DangerFullAccess)
|
||||
.expect("test setup should allow updating sandbox policy");
|
||||
|
||||
let session = Arc::new(session);
|
||||
let turn = Arc::new(turn);
|
||||
@@ -2529,7 +2518,9 @@ console.log("cell-complete");
|
||||
turn.approval_policy
|
||||
.set(AskForApproval::Never)
|
||||
.expect("test setup should allow updating approval policy");
|
||||
set_danger_full_access(&mut turn);
|
||||
turn.sandbox_policy
|
||||
.set(SandboxPolicy::DangerFullAccess)
|
||||
.expect("test setup should allow updating sandbox policy");
|
||||
|
||||
let session = Arc::new(session);
|
||||
let turn = Arc::new(turn);
|
||||
@@ -2585,7 +2576,9 @@ console.log(out.type);
|
||||
turn.approval_policy
|
||||
.set(AskForApproval::Never)
|
||||
.expect("test setup should allow updating approval policy");
|
||||
set_danger_full_access(&mut turn);
|
||||
turn.sandbox_policy
|
||||
.set(SandboxPolicy::DangerFullAccess)
|
||||
.expect("test setup should allow updating sandbox policy");
|
||||
|
||||
let session = Arc::new(session);
|
||||
let turn = Arc::new(turn);
|
||||
|
||||
@@ -169,8 +169,7 @@ impl ToolOrchestrator {
|
||||
let initial_sandbox = match tool.sandbox_mode_for_first_attempt(req) {
|
||||
SandboxOverride::BypassSandboxFirstAttempt => crate::exec::SandboxType::None,
|
||||
SandboxOverride::NoOverride => self.sandbox.select_initial(
|
||||
&turn_ctx.file_system_sandbox_policy,
|
||||
turn_ctx.network_sandbox_policy,
|
||||
&turn_ctx.sandbox_policy,
|
||||
tool.sandbox_preference(),
|
||||
turn_ctx.windows_sandbox_level,
|
||||
has_managed_network_requirements,
|
||||
@@ -183,8 +182,6 @@ impl ToolOrchestrator {
|
||||
let initial_attempt = SandboxAttempt {
|
||||
sandbox: initial_sandbox,
|
||||
policy: &turn_ctx.sandbox_policy,
|
||||
file_system_policy: &turn_ctx.file_system_sandbox_policy,
|
||||
network_policy: turn_ctx.network_sandbox_policy,
|
||||
enforce_managed_network: has_managed_network_requirements,
|
||||
manager: &self.sandbox,
|
||||
sandbox_cwd: &turn_ctx.cwd,
|
||||
@@ -299,8 +296,6 @@ impl ToolOrchestrator {
|
||||
let escalated_attempt = SandboxAttempt {
|
||||
sandbox: crate::exec::SandboxType::None,
|
||||
policy: &turn_ctx.sandbox_policy,
|
||||
file_system_policy: &turn_ctx.file_system_sandbox_policy,
|
||||
network_policy: turn_ctx.network_sandbox_policy,
|
||||
enforce_managed_network: has_managed_network_requirements,
|
||||
manager: &self.sandbox,
|
||||
sandbox_cwd: &turn_ctx.cwd,
|
||||
|
||||
@@ -25,9 +25,7 @@ use codex_protocol::config_types::WindowsSandboxLevel;
|
||||
use codex_protocol::models::MacOsSeatbeltProfileExtensions;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::protocol::AskForApproval;
|
||||
use codex_protocol::protocol::FileSystemSandboxPolicy;
|
||||
use codex_protocol::protocol::NetworkPolicyRuleAction;
|
||||
use codex_protocol::protocol::NetworkSandboxPolicy;
|
||||
use codex_protocol::protocol::RejectConfig;
|
||||
use codex_protocol::protocol::ReviewDecision;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
@@ -100,8 +98,6 @@ pub(super) async fn try_run_zsh_fork(
|
||||
windows_sandbox_level,
|
||||
sandbox_permissions,
|
||||
sandbox_policy,
|
||||
file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
justification,
|
||||
arg0,
|
||||
} = sandbox_exec_request;
|
||||
@@ -117,8 +113,6 @@ pub(super) async fn try_run_zsh_fork(
|
||||
command,
|
||||
cwd: sandbox_cwd,
|
||||
sandbox_policy,
|
||||
file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
sandbox,
|
||||
env: sandbox_env,
|
||||
network: sandbox_network,
|
||||
@@ -226,8 +220,6 @@ pub(crate) async fn prepare_unified_exec_zsh_fork(
|
||||
command: exec_request.command.clone(),
|
||||
cwd: exec_request.cwd.clone(),
|
||||
sandbox_policy: exec_request.sandbox_policy.clone(),
|
||||
file_system_sandbox_policy: exec_request.file_system_sandbox_policy.clone(),
|
||||
network_sandbox_policy: exec_request.network_sandbox_policy,
|
||||
sandbox: exec_request.sandbox,
|
||||
env: exec_request.env.clone(),
|
||||
network: exec_request.network.clone(),
|
||||
@@ -736,8 +728,6 @@ struct CoreShellCommandExecutor {
|
||||
command: Vec<String>,
|
||||
cwd: PathBuf,
|
||||
sandbox_policy: SandboxPolicy,
|
||||
file_system_sandbox_policy: FileSystemSandboxPolicy,
|
||||
network_sandbox_policy: NetworkSandboxPolicy,
|
||||
sandbox: SandboxType,
|
||||
env: HashMap<String, String>,
|
||||
network: Option<codex_network_proxy::NetworkProxy>,
|
||||
@@ -757,8 +747,6 @@ struct PrepareSandboxedExecParams<'a> {
|
||||
workdir: &'a AbsolutePathBuf,
|
||||
env: HashMap<String, String>,
|
||||
sandbox_policy: &'a SandboxPolicy,
|
||||
file_system_sandbox_policy: &'a FileSystemSandboxPolicy,
|
||||
network_sandbox_policy: NetworkSandboxPolicy,
|
||||
additional_permissions: Option<PermissionProfile>,
|
||||
#[cfg(target_os = "macos")]
|
||||
macos_seatbelt_profile_extensions: Option<&'a MacOsSeatbeltProfileExtensions>,
|
||||
@@ -794,8 +782,6 @@ impl ShellCommandExecutor for CoreShellCommandExecutor {
|
||||
windows_sandbox_level: self.windows_sandbox_level,
|
||||
sandbox_permissions: self.sandbox_permissions,
|
||||
sandbox_policy: self.sandbox_policy.clone(),
|
||||
file_system_sandbox_policy: self.file_system_sandbox_policy.clone(),
|
||||
network_sandbox_policy: self.network_sandbox_policy,
|
||||
justification: self.justification.clone(),
|
||||
arg0: self.arg0.clone(),
|
||||
},
|
||||
@@ -842,8 +828,6 @@ impl ShellCommandExecutor for CoreShellCommandExecutor {
|
||||
workdir,
|
||||
env,
|
||||
sandbox_policy: &self.sandbox_policy,
|
||||
file_system_sandbox_policy: &self.file_system_sandbox_policy,
|
||||
network_sandbox_policy: self.network_sandbox_policy,
|
||||
additional_permissions: None,
|
||||
#[cfg(target_os = "macos")]
|
||||
macos_seatbelt_profile_extensions: self
|
||||
@@ -861,8 +845,6 @@ impl ShellCommandExecutor for CoreShellCommandExecutor {
|
||||
workdir,
|
||||
env,
|
||||
sandbox_policy: &self.sandbox_policy,
|
||||
file_system_sandbox_policy: &self.file_system_sandbox_policy,
|
||||
network_sandbox_policy: self.network_sandbox_policy,
|
||||
additional_permissions: Some(permission_profile),
|
||||
#[cfg(target_os = "macos")]
|
||||
macos_seatbelt_profile_extensions: self
|
||||
@@ -872,17 +854,11 @@ impl ShellCommandExecutor for CoreShellCommandExecutor {
|
||||
}
|
||||
EscalationExecution::Permissions(EscalationPermissions::Permissions(permissions)) => {
|
||||
// Use a fully specified sandbox policy instead of merging into the turn policy.
|
||||
let file_system_sandbox_policy =
|
||||
FileSystemSandboxPolicy::from(&permissions.sandbox_policy);
|
||||
let network_sandbox_policy =
|
||||
NetworkSandboxPolicy::from(&permissions.sandbox_policy);
|
||||
self.prepare_sandboxed_exec(PrepareSandboxedExecParams {
|
||||
command,
|
||||
workdir,
|
||||
env,
|
||||
sandbox_policy: &permissions.sandbox_policy,
|
||||
file_system_sandbox_policy: &file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
additional_permissions: None,
|
||||
#[cfg(target_os = "macos")]
|
||||
macos_seatbelt_profile_extensions: permissions
|
||||
@@ -897,7 +873,6 @@ impl ShellCommandExecutor for CoreShellCommandExecutor {
|
||||
}
|
||||
|
||||
impl CoreShellCommandExecutor {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn prepare_sandboxed_exec(
|
||||
&self,
|
||||
params: PrepareSandboxedExecParams<'_>,
|
||||
@@ -907,8 +882,6 @@ impl CoreShellCommandExecutor {
|
||||
workdir,
|
||||
env,
|
||||
sandbox_policy,
|
||||
file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
additional_permissions,
|
||||
#[cfg(target_os = "macos")]
|
||||
macos_seatbelt_profile_extensions,
|
||||
@@ -918,8 +891,7 @@ impl CoreShellCommandExecutor {
|
||||
.ok_or_else(|| anyhow::anyhow!("prepared command must not be empty"))?;
|
||||
let sandbox_manager = crate::sandboxing::SandboxManager::new();
|
||||
let sandbox = sandbox_manager.select_initial(
|
||||
file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
sandbox_policy,
|
||||
SandboxablePreference::Auto,
|
||||
self.windows_sandbox_level,
|
||||
self.network.is_some(),
|
||||
@@ -941,8 +913,6 @@ impl CoreShellCommandExecutor {
|
||||
justification: self.justification.clone(),
|
||||
},
|
||||
policy: sandbox_policy,
|
||||
file_system_policy: file_system_sandbox_policy,
|
||||
network_policy: network_sandbox_policy,
|
||||
sandbox,
|
||||
enforce_managed_network: self.network.is_some(),
|
||||
network: self.network.as_ref(),
|
||||
|
||||
@@ -31,10 +31,6 @@ use codex_protocol::models::FileSystemPermissions;
|
||||
use codex_protocol::models::MacOsPreferencesPermission;
|
||||
use codex_protocol::models::MacOsSeatbeltProfileExtensions;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
#[cfg(target_os = "macos")]
|
||||
use codex_protocol::protocol::FileSystemSandboxPolicy;
|
||||
#[cfg(target_os = "macos")]
|
||||
use codex_protocol::protocol::NetworkSandboxPolicy;
|
||||
use codex_protocol::protocol::SkillScope;
|
||||
use codex_shell_escalation::EscalationExecution;
|
||||
use codex_shell_escalation::EscalationPermissions;
|
||||
@@ -478,10 +474,6 @@ async fn prepare_escalated_exec_turn_default_preserves_macos_seatbelt_extensions
|
||||
network: None,
|
||||
sandbox: SandboxType::None,
|
||||
sandbox_policy: SandboxPolicy::new_read_only_policy(),
|
||||
file_system_sandbox_policy: FileSystemSandboxPolicy::from(
|
||||
&SandboxPolicy::new_read_only_policy(),
|
||||
),
|
||||
network_sandbox_policy: NetworkSandboxPolicy::Restricted,
|
||||
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
||||
sandbox_permissions: SandboxPermissions::UseDefault,
|
||||
justification: None,
|
||||
@@ -532,8 +524,6 @@ async fn prepare_escalated_exec_permissions_preserve_macos_seatbelt_extensions()
|
||||
network: None,
|
||||
sandbox: SandboxType::None,
|
||||
sandbox_policy: SandboxPolicy::DangerFullAccess,
|
||||
file_system_sandbox_policy: FileSystemSandboxPolicy::from(&SandboxPolicy::DangerFullAccess),
|
||||
network_sandbox_policy: NetworkSandboxPolicy::Enabled,
|
||||
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
||||
sandbox_permissions: SandboxPermissions::UseDefault,
|
||||
justification: None,
|
||||
@@ -547,10 +537,6 @@ async fn prepare_escalated_exec_permissions_preserve_macos_seatbelt_extensions()
|
||||
let permissions = Permissions {
|
||||
approval_policy: Constrained::allow_any(AskForApproval::Never),
|
||||
sandbox_policy: Constrained::allow_any(SandboxPolicy::new_read_only_policy()),
|
||||
file_system_sandbox_policy: codex_protocol::permissions::FileSystemSandboxPolicy::from(
|
||||
&SandboxPolicy::new_read_only_policy(),
|
||||
),
|
||||
network_sandbox_policy: codex_protocol::permissions::NetworkSandboxPolicy::Restricted,
|
||||
network: None,
|
||||
allow_login_shell: true,
|
||||
shell_environment_policy: ShellEnvironmentPolicy::default(),
|
||||
@@ -598,16 +584,13 @@ async fn prepare_escalated_exec_permissions_preserve_macos_seatbelt_extensions()
|
||||
#[tokio::test]
|
||||
async fn prepare_escalated_exec_permission_profile_unions_turn_and_requested_macos_extensions() {
|
||||
let cwd = AbsolutePathBuf::from_absolute_path(std::env::temp_dir()).unwrap();
|
||||
let sandbox_policy = SandboxPolicy::new_read_only_policy();
|
||||
let executor = CoreShellCommandExecutor {
|
||||
command: vec!["echo".to_string(), "ok".to_string()],
|
||||
cwd: cwd.to_path_buf(),
|
||||
env: HashMap::new(),
|
||||
network: None,
|
||||
sandbox: SandboxType::None,
|
||||
sandbox_policy: sandbox_policy.clone(),
|
||||
file_system_sandbox_policy: FileSystemSandboxPolicy::from(&sandbox_policy),
|
||||
network_sandbox_policy: NetworkSandboxPolicy::from(&sandbox_policy),
|
||||
sandbox_policy: SandboxPolicy::new_read_only_policy(),
|
||||
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
||||
sandbox_permissions: SandboxPermissions::UseDefault,
|
||||
justification: None,
|
||||
|
||||
@@ -7,8 +7,6 @@
|
||||
use crate::codex::Session;
|
||||
use crate::codex::TurnContext;
|
||||
use crate::error::CodexErr;
|
||||
use crate::protocol::FileSystemSandboxPolicy;
|
||||
use crate::protocol::NetworkSandboxPolicy;
|
||||
use crate::protocol::SandboxPolicy;
|
||||
use crate::sandboxing::CommandSpec;
|
||||
use crate::sandboxing::SandboxManager;
|
||||
@@ -320,8 +318,6 @@ pub(crate) trait ToolRuntime<Req, Out>: Approvable<Req> + Sandboxable {
|
||||
pub(crate) struct SandboxAttempt<'a> {
|
||||
pub sandbox: crate::exec::SandboxType,
|
||||
pub policy: &'a crate::protocol::SandboxPolicy,
|
||||
pub file_system_policy: &'a FileSystemSandboxPolicy,
|
||||
pub network_policy: NetworkSandboxPolicy,
|
||||
pub enforce_managed_network: bool,
|
||||
pub(crate) manager: &'a SandboxManager,
|
||||
pub(crate) sandbox_cwd: &'a Path,
|
||||
@@ -340,8 +336,6 @@ impl<'a> SandboxAttempt<'a> {
|
||||
.transform(crate::sandboxing::SandboxTransformRequest {
|
||||
spec,
|
||||
policy: self.policy,
|
||||
file_system_policy: self.file_system_policy,
|
||||
network_policy: self.network_policy,
|
||||
sandbox: self.sandbox,
|
||||
enforce_managed_network: self.enforce_managed_network,
|
||||
network,
|
||||
|
||||
@@ -205,10 +205,6 @@ mod tests {
|
||||
turn.sandbox_policy
|
||||
.set(SandboxPolicy::DangerFullAccess)
|
||||
.expect("test setup should allow updating sandbox policy");
|
||||
turn.file_system_sandbox_policy =
|
||||
crate::protocol::FileSystemSandboxPolicy::from(turn.sandbox_policy.get());
|
||||
turn.network_sandbox_policy =
|
||||
crate::protocol::NetworkSandboxPolicy::from(turn.sandbox_policy.get());
|
||||
(Arc::new(session), Arc::new(turn))
|
||||
}
|
||||
|
||||
|
||||
@@ -2234,7 +2234,7 @@ async fn denying_network_policy_amendment_persists_policy_and_skips_future_netwo
|
||||
let home = Arc::new(TempDir::new()?);
|
||||
fs::write(
|
||||
home.path().join("config.toml"),
|
||||
r#"[network]
|
||||
r#"[permissions.network]
|
||||
enabled = true
|
||||
mode = "limited"
|
||||
allow_local_binding = true
|
||||
|
||||
@@ -10,8 +10,6 @@ use codex_core::exec::process_exec_tool_call;
|
||||
use codex_core::sandboxing::SandboxPermissions;
|
||||
use codex_core::spawn::CODEX_SANDBOX_ENV_VAR;
|
||||
use codex_protocol::config_types::WindowsSandboxLevel;
|
||||
use codex_protocol::protocol::FileSystemSandboxPolicy;
|
||||
use codex_protocol::protocol::NetworkSandboxPolicy;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
use tempfile::TempDir;
|
||||
|
||||
@@ -47,17 +45,7 @@ async fn run_test_cmd(tmp: TempDir, cmd: Vec<&str>) -> Result<ExecToolCallOutput
|
||||
|
||||
let policy = SandboxPolicy::new_read_only_policy();
|
||||
|
||||
process_exec_tool_call(
|
||||
params,
|
||||
&policy,
|
||||
&FileSystemSandboxPolicy::from(&policy),
|
||||
NetworkSandboxPolicy::from(&policy),
|
||||
tmp.path(),
|
||||
&None,
|
||||
false,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
process_exec_tool_call(params, &policy, tmp.path(), &None, false, None).await
|
||||
}
|
||||
|
||||
/// Command succeeds with exit code 0 normally
|
||||
|
||||
@@ -5,7 +5,6 @@ use codex_protocol::protocol::EventMsg;
|
||||
use codex_protocol::protocol::ExecCommandEndEvent;
|
||||
use codex_protocol::protocol::ExecCommandSource;
|
||||
use codex_protocol::protocol::ExecOutputStream;
|
||||
use codex_protocol::protocol::NetworkSandboxPolicy;
|
||||
use codex_protocol::protocol::Op;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
use codex_protocol::protocol::TurnAbortReason;
|
||||
@@ -24,7 +23,6 @@ use core_test_support::test_codex::test_codex;
|
||||
use core_test_support::wait_for_event;
|
||||
use core_test_support::wait_for_event_match;
|
||||
use core_test_support::wait_for_event_with_timeout;
|
||||
use pretty_assertions::assert_eq;
|
||||
use regex_lite::escape;
|
||||
use std::path::PathBuf;
|
||||
use tempfile::TempDir;
|
||||
@@ -330,35 +328,6 @@ async fn user_shell_command_history_is_persisted_and_shared_with_model() -> anyh
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn user_shell_command_does_not_set_network_sandbox_env_var() -> anyhow::Result<()> {
|
||||
let server = responses::start_mock_server().await;
|
||||
let mut builder = core_test_support::test_codex::test_codex().with_config(|config| {
|
||||
config.permissions.network_sandbox_policy = NetworkSandboxPolicy::Restricted;
|
||||
});
|
||||
let test = builder.build(&server).await?;
|
||||
|
||||
#[cfg(windows)]
|
||||
let command = r#"$val = $env:CODEX_SANDBOX_NETWORK_DISABLED; if ([string]::IsNullOrEmpty($val)) { $val = 'not-set' } ; [System.Console]::Write($val)"#.to_string();
|
||||
#[cfg(not(windows))]
|
||||
let command =
|
||||
r#"sh -c "printf '%s' \"${CODEX_SANDBOX_NETWORK_DISABLED:-not-set}\"""#.to_string();
|
||||
|
||||
test.codex
|
||||
.submit(Op::RunUserShellCommand { command })
|
||||
.await?;
|
||||
|
||||
let end_event = wait_for_event_match(&test.codex, |ev| match ev {
|
||||
EventMsg::ExecCommandEnd(event) => Some(event.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.await;
|
||||
assert_eq!(end_event.exit_code, 0);
|
||||
assert_eq!(end_event.stdout.trim(), "not-set");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
#[cfg(not(target_os = "windows"))] // TODO: unignore on windows
|
||||
async fn user_shell_command_output_is_truncated_in_history() -> anyhow::Result<()> {
|
||||
|
||||
@@ -8,7 +8,6 @@ use std::path::Path;
|
||||
use codex_core::error::CodexErr;
|
||||
use codex_core::error::Result;
|
||||
use codex_core::error::SandboxErr;
|
||||
use codex_protocol::protocol::NetworkSandboxPolicy;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
|
||||
@@ -41,14 +40,13 @@ use seccompiler::apply_filter;
|
||||
/// Filesystem restrictions are intentionally handled by bubblewrap.
|
||||
pub(crate) fn apply_sandbox_policy_to_current_thread(
|
||||
sandbox_policy: &SandboxPolicy,
|
||||
network_sandbox_policy: NetworkSandboxPolicy,
|
||||
cwd: &Path,
|
||||
apply_landlock_fs: bool,
|
||||
allow_network_for_proxy: bool,
|
||||
proxy_routed_network: bool,
|
||||
) -> Result<()> {
|
||||
let network_seccomp_mode = network_seccomp_mode(
|
||||
network_sandbox_policy,
|
||||
sandbox_policy,
|
||||
allow_network_for_proxy,
|
||||
proxy_routed_network,
|
||||
);
|
||||
@@ -93,20 +91,20 @@ enum NetworkSeccompMode {
|
||||
}
|
||||
|
||||
fn should_install_network_seccomp(
|
||||
network_sandbox_policy: NetworkSandboxPolicy,
|
||||
sandbox_policy: &SandboxPolicy,
|
||||
allow_network_for_proxy: bool,
|
||||
) -> bool {
|
||||
// Managed-network sessions should remain fail-closed even for policies that
|
||||
// would normally grant full network access (for example, DangerFullAccess).
|
||||
!network_sandbox_policy.is_enabled() || allow_network_for_proxy
|
||||
!sandbox_policy.has_full_network_access() || allow_network_for_proxy
|
||||
}
|
||||
|
||||
fn network_seccomp_mode(
|
||||
network_sandbox_policy: NetworkSandboxPolicy,
|
||||
sandbox_policy: &SandboxPolicy,
|
||||
allow_network_for_proxy: bool,
|
||||
proxy_routed_network: bool,
|
||||
) -> Option<NetworkSeccompMode> {
|
||||
if !should_install_network_seccomp(network_sandbox_policy, allow_network_for_proxy) {
|
||||
if !should_install_network_seccomp(sandbox_policy, allow_network_for_proxy) {
|
||||
None
|
||||
} else if proxy_routed_network {
|
||||
Some(NetworkSeccompMode::ProxyRouted)
|
||||
|
||||
@@ -14,9 +14,6 @@ use crate::proxy_routing::activate_proxy_routes_in_netns;
|
||||
use crate::proxy_routing::prepare_host_proxy_route_spec;
|
||||
use crate::vendored_bwrap::exec_vendored_bwrap;
|
||||
use crate::vendored_bwrap::run_vendored_bwrap_main;
|
||||
use codex_protocol::protocol::FileSystemSandboxPolicy;
|
||||
use codex_protocol::protocol::NetworkSandboxPolicy;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
/// CLI surface for the Linux sandbox helper.
|
||||
@@ -29,18 +26,8 @@ pub struct LandlockCommand {
|
||||
#[arg(long = "sandbox-policy-cwd")]
|
||||
pub sandbox_policy_cwd: PathBuf,
|
||||
|
||||
/// Legacy compatibility policy.
|
||||
///
|
||||
/// Newer callers pass split filesystem/network policies as well so the
|
||||
/// helper can migrate incrementally without breaking older invocations.
|
||||
#[arg(long = "sandbox-policy", hide = true)]
|
||||
pub sandbox_policy: Option<SandboxPolicy>,
|
||||
|
||||
#[arg(long = "file-system-sandbox-policy", hide = true)]
|
||||
pub file_system_sandbox_policy: Option<FileSystemSandboxPolicy>,
|
||||
|
||||
#[arg(long = "network-sandbox-policy", hide = true)]
|
||||
pub network_sandbox_policy: Option<NetworkSandboxPolicy>,
|
||||
#[arg(long = "sandbox-policy")]
|
||||
pub sandbox_policy: codex_protocol::protocol::SandboxPolicy,
|
||||
|
||||
/// Opt-in: use the bubblewrap-based Linux sandbox pipeline.
|
||||
///
|
||||
@@ -90,8 +77,6 @@ pub fn run_main() -> ! {
|
||||
let LandlockCommand {
|
||||
sandbox_policy_cwd,
|
||||
sandbox_policy,
|
||||
file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
use_bwrap_sandbox,
|
||||
apply_seccomp_then_exec,
|
||||
allow_network_for_proxy,
|
||||
@@ -104,16 +89,6 @@ pub fn run_main() -> ! {
|
||||
panic!("No command specified to execute.");
|
||||
}
|
||||
ensure_inner_stage_mode_is_valid(apply_seccomp_then_exec, use_bwrap_sandbox);
|
||||
let EffectiveSandboxPolicies {
|
||||
sandbox_policy,
|
||||
file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
} = resolve_sandbox_policies(
|
||||
sandbox_policy_cwd.as_path(),
|
||||
sandbox_policy,
|
||||
file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
);
|
||||
|
||||
// Inner stage: apply seccomp/no_new_privs after bubblewrap has already
|
||||
// established the filesystem view.
|
||||
@@ -129,7 +104,6 @@ pub fn run_main() -> ! {
|
||||
let proxy_routing_active = allow_network_for_proxy;
|
||||
if let Err(e) = apply_sandbox_policy_to_current_thread(
|
||||
&sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
&sandbox_policy_cwd,
|
||||
false,
|
||||
allow_network_for_proxy,
|
||||
@@ -140,10 +114,9 @@ pub fn run_main() -> ! {
|
||||
exec_or_panic(command);
|
||||
}
|
||||
|
||||
if file_system_sandbox_policy.has_full_disk_write_access() && !allow_network_for_proxy {
|
||||
if sandbox_policy.has_full_disk_write_access() && !allow_network_for_proxy {
|
||||
if let Err(e) = apply_sandbox_policy_to_current_thread(
|
||||
&sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
&sandbox_policy_cwd,
|
||||
false,
|
||||
allow_network_for_proxy,
|
||||
@@ -169,8 +142,6 @@ pub fn run_main() -> ! {
|
||||
let inner = build_inner_seccomp_command(
|
||||
&sandbox_policy_cwd,
|
||||
&sandbox_policy,
|
||||
&file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
use_bwrap_sandbox,
|
||||
allow_network_for_proxy,
|
||||
proxy_route_spec,
|
||||
@@ -179,7 +150,6 @@ pub fn run_main() -> ! {
|
||||
run_bwrap_with_proc_fallback(
|
||||
&sandbox_policy_cwd,
|
||||
&sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
inner,
|
||||
!no_proc,
|
||||
allow_network_for_proxy,
|
||||
@@ -189,7 +159,6 @@ pub fn run_main() -> ! {
|
||||
// Legacy path: Landlock enforcement only, when bwrap sandboxing is not enabled.
|
||||
if let Err(e) = apply_sandbox_policy_to_current_thread(
|
||||
&sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
&sandbox_policy_cwd,
|
||||
true,
|
||||
allow_network_for_proxy,
|
||||
@@ -200,53 +169,6 @@ pub fn run_main() -> ! {
|
||||
exec_or_panic(command);
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct EffectiveSandboxPolicies {
|
||||
sandbox_policy: SandboxPolicy,
|
||||
file_system_sandbox_policy: FileSystemSandboxPolicy,
|
||||
network_sandbox_policy: NetworkSandboxPolicy,
|
||||
}
|
||||
|
||||
fn resolve_sandbox_policies(
|
||||
sandbox_policy_cwd: &Path,
|
||||
sandbox_policy: Option<SandboxPolicy>,
|
||||
file_system_sandbox_policy: Option<FileSystemSandboxPolicy>,
|
||||
network_sandbox_policy: Option<NetworkSandboxPolicy>,
|
||||
) -> EffectiveSandboxPolicies {
|
||||
match (
|
||||
sandbox_policy,
|
||||
file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
) {
|
||||
(Some(sandbox_policy), Some(file_system_sandbox_policy), Some(network_sandbox_policy)) => {
|
||||
EffectiveSandboxPolicies {
|
||||
sandbox_policy,
|
||||
file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
}
|
||||
}
|
||||
(Some(sandbox_policy), None, None) => EffectiveSandboxPolicies {
|
||||
file_system_sandbox_policy: FileSystemSandboxPolicy::from(&sandbox_policy),
|
||||
network_sandbox_policy: NetworkSandboxPolicy::from(&sandbox_policy),
|
||||
sandbox_policy,
|
||||
},
|
||||
(None, Some(file_system_sandbox_policy), Some(network_sandbox_policy)) => {
|
||||
let sandbox_policy = file_system_sandbox_policy
|
||||
.to_legacy_sandbox_policy(network_sandbox_policy, sandbox_policy_cwd)
|
||||
.unwrap_or_else(|err| {
|
||||
panic!("failed to derive legacy sandbox policy from split policies: {err}")
|
||||
});
|
||||
EffectiveSandboxPolicies {
|
||||
sandbox_policy,
|
||||
file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
}
|
||||
}
|
||||
(None, None, None) => panic!("missing sandbox policy configuration"),
|
||||
_ => panic!("file-system and network sandbox policies must be provided together"),
|
||||
}
|
||||
}
|
||||
|
||||
fn ensure_inner_stage_mode_is_valid(apply_seccomp_then_exec: bool, use_bwrap_sandbox: bool) {
|
||||
if apply_seccomp_then_exec && !use_bwrap_sandbox {
|
||||
panic!("--apply-seccomp-then-exec requires --use-bwrap-sandbox");
|
||||
@@ -255,13 +177,12 @@ fn ensure_inner_stage_mode_is_valid(apply_seccomp_then_exec: bool, use_bwrap_san
|
||||
|
||||
fn run_bwrap_with_proc_fallback(
|
||||
sandbox_policy_cwd: &Path,
|
||||
sandbox_policy: &SandboxPolicy,
|
||||
network_sandbox_policy: NetworkSandboxPolicy,
|
||||
sandbox_policy: &codex_protocol::protocol::SandboxPolicy,
|
||||
inner: Vec<String>,
|
||||
mount_proc: bool,
|
||||
allow_network_for_proxy: bool,
|
||||
) -> ! {
|
||||
let network_mode = bwrap_network_mode(network_sandbox_policy, allow_network_for_proxy);
|
||||
let network_mode = bwrap_network_mode(sandbox_policy, allow_network_for_proxy);
|
||||
let mut mount_proc = mount_proc;
|
||||
|
||||
if mount_proc && !preflight_proc_mount_support(sandbox_policy_cwd, sandbox_policy, network_mode)
|
||||
@@ -279,12 +200,12 @@ fn run_bwrap_with_proc_fallback(
|
||||
}
|
||||
|
||||
fn bwrap_network_mode(
|
||||
network_sandbox_policy: NetworkSandboxPolicy,
|
||||
sandbox_policy: &codex_protocol::protocol::SandboxPolicy,
|
||||
allow_network_for_proxy: bool,
|
||||
) -> BwrapNetworkMode {
|
||||
if allow_network_for_proxy {
|
||||
BwrapNetworkMode::ProxyOnly
|
||||
} else if network_sandbox_policy.is_enabled() {
|
||||
} else if sandbox_policy.has_full_network_access() {
|
||||
BwrapNetworkMode::FullAccess
|
||||
} else {
|
||||
BwrapNetworkMode::Isolated
|
||||
@@ -293,7 +214,7 @@ fn bwrap_network_mode(
|
||||
|
||||
fn build_bwrap_argv(
|
||||
inner: Vec<String>,
|
||||
sandbox_policy: &SandboxPolicy,
|
||||
sandbox_policy: &codex_protocol::protocol::SandboxPolicy,
|
||||
sandbox_policy_cwd: &Path,
|
||||
options: BwrapOptions,
|
||||
) -> Vec<String> {
|
||||
@@ -316,7 +237,7 @@ fn build_bwrap_argv(
|
||||
|
||||
fn preflight_proc_mount_support(
|
||||
sandbox_policy_cwd: &Path,
|
||||
sandbox_policy: &SandboxPolicy,
|
||||
sandbox_policy: &codex_protocol::protocol::SandboxPolicy,
|
||||
network_mode: BwrapNetworkMode,
|
||||
) -> bool {
|
||||
let preflight_argv =
|
||||
@@ -327,7 +248,7 @@ fn preflight_proc_mount_support(
|
||||
|
||||
fn build_preflight_bwrap_argv(
|
||||
sandbox_policy_cwd: &Path,
|
||||
sandbox_policy: &SandboxPolicy,
|
||||
sandbox_policy: &codex_protocol::protocol::SandboxPolicy,
|
||||
network_mode: BwrapNetworkMode,
|
||||
) -> Vec<String> {
|
||||
let preflight_command = vec![resolve_true_command()];
|
||||
@@ -440,9 +361,7 @@ fn is_proc_mount_failure(stderr: &str) -> bool {
|
||||
/// Build the inner command that applies seccomp after bubblewrap.
|
||||
fn build_inner_seccomp_command(
|
||||
sandbox_policy_cwd: &Path,
|
||||
sandbox_policy: &SandboxPolicy,
|
||||
file_system_sandbox_policy: &FileSystemSandboxPolicy,
|
||||
network_sandbox_policy: NetworkSandboxPolicy,
|
||||
sandbox_policy: &codex_protocol::protocol::SandboxPolicy,
|
||||
use_bwrap_sandbox: bool,
|
||||
allow_network_for_proxy: bool,
|
||||
proxy_route_spec: Option<String>,
|
||||
@@ -456,14 +375,6 @@ fn build_inner_seccomp_command(
|
||||
Ok(json) => json,
|
||||
Err(err) => panic!("failed to serialize sandbox policy: {err}"),
|
||||
};
|
||||
let file_system_policy_json = match serde_json::to_string(file_system_sandbox_policy) {
|
||||
Ok(json) => json,
|
||||
Err(err) => panic!("failed to serialize filesystem sandbox policy: {err}"),
|
||||
};
|
||||
let network_policy_json = match serde_json::to_string(&network_sandbox_policy) {
|
||||
Ok(json) => json,
|
||||
Err(err) => panic!("failed to serialize network sandbox policy: {err}"),
|
||||
};
|
||||
|
||||
let mut inner = vec![
|
||||
current_exe.to_string_lossy().to_string(),
|
||||
@@ -471,10 +382,6 @@ fn build_inner_seccomp_command(
|
||||
sandbox_policy_cwd.to_string_lossy().to_string(),
|
||||
"--sandbox-policy".to_string(),
|
||||
policy_json,
|
||||
"--file-system-sandbox-policy".to_string(),
|
||||
file_system_policy_json,
|
||||
"--network-sandbox-policy".to_string(),
|
||||
network_policy_json,
|
||||
];
|
||||
if use_bwrap_sandbox {
|
||||
inner.push("--use-bwrap-sandbox".to_string());
|
||||
|
||||
@@ -1,13 +1,7 @@
|
||||
#[cfg(test)]
|
||||
use super::*;
|
||||
#[cfg(test)]
|
||||
use codex_protocol::protocol::FileSystemSandboxPolicy;
|
||||
#[cfg(test)]
|
||||
use codex_protocol::protocol::NetworkSandboxPolicy;
|
||||
#[cfg(test)]
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
#[cfg(test)]
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn detects_proc_mount_invalid_argument_failure() {
|
||||
@@ -97,25 +91,22 @@ fn inserts_unshare_net_when_proxy_only_network_mode_requested() {
|
||||
|
||||
#[test]
|
||||
fn proxy_only_mode_takes_precedence_over_full_network_policy() {
|
||||
let mode = bwrap_network_mode(NetworkSandboxPolicy::Enabled, true);
|
||||
let mode = bwrap_network_mode(&SandboxPolicy::DangerFullAccess, true);
|
||||
assert_eq!(mode, BwrapNetworkMode::ProxyOnly);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn managed_proxy_preflight_argv_is_wrapped_for_full_access_policy() {
|
||||
let mode = bwrap_network_mode(NetworkSandboxPolicy::Enabled, true);
|
||||
let mode = bwrap_network_mode(&SandboxPolicy::DangerFullAccess, true);
|
||||
let argv = build_preflight_bwrap_argv(Path::new("/"), &SandboxPolicy::DangerFullAccess, mode);
|
||||
assert!(argv.iter().any(|arg| arg == "--"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn managed_proxy_inner_command_includes_route_spec() {
|
||||
let sandbox_policy = SandboxPolicy::new_read_only_policy();
|
||||
let args = build_inner_seccomp_command(
|
||||
Path::new("/tmp"),
|
||||
&sandbox_policy,
|
||||
&FileSystemSandboxPolicy::from(&sandbox_policy),
|
||||
NetworkSandboxPolicy::Restricted,
|
||||
&SandboxPolicy::new_read_only_policy(),
|
||||
true,
|
||||
true,
|
||||
Some("{\"routes\":[]}".to_string()),
|
||||
@@ -126,32 +117,11 @@ fn managed_proxy_inner_command_includes_route_spec() {
|
||||
assert!(args.iter().any(|arg| arg == "{\"routes\":[]}"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inner_command_includes_split_policy_flags() {
|
||||
let sandbox_policy = SandboxPolicy::new_read_only_policy();
|
||||
let args = build_inner_seccomp_command(
|
||||
Path::new("/tmp"),
|
||||
&sandbox_policy,
|
||||
&FileSystemSandboxPolicy::from(&sandbox_policy),
|
||||
NetworkSandboxPolicy::Restricted,
|
||||
true,
|
||||
false,
|
||||
None,
|
||||
vec!["/bin/true".to_string()],
|
||||
);
|
||||
|
||||
assert!(args.iter().any(|arg| arg == "--file-system-sandbox-policy"));
|
||||
assert!(args.iter().any(|arg| arg == "--network-sandbox-policy"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_managed_inner_command_omits_route_spec() {
|
||||
let sandbox_policy = SandboxPolicy::new_read_only_policy();
|
||||
let args = build_inner_seccomp_command(
|
||||
Path::new("/tmp"),
|
||||
&sandbox_policy,
|
||||
&FileSystemSandboxPolicy::from(&sandbox_policy),
|
||||
NetworkSandboxPolicy::Restricted,
|
||||
&SandboxPolicy::new_read_only_policy(),
|
||||
true,
|
||||
false,
|
||||
None,
|
||||
@@ -164,12 +134,9 @@ fn non_managed_inner_command_omits_route_spec() {
|
||||
#[test]
|
||||
fn managed_proxy_inner_command_requires_route_spec() {
|
||||
let result = std::panic::catch_unwind(|| {
|
||||
let sandbox_policy = SandboxPolicy::new_read_only_policy();
|
||||
build_inner_seccomp_command(
|
||||
Path::new("/tmp"),
|
||||
&sandbox_policy,
|
||||
&FileSystemSandboxPolicy::from(&sandbox_policy),
|
||||
NetworkSandboxPolicy::Restricted,
|
||||
&SandboxPolicy::new_read_only_policy(),
|
||||
true,
|
||||
true,
|
||||
None,
|
||||
@@ -179,59 +146,6 @@ fn managed_proxy_inner_command_requires_route_spec() {
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_sandbox_policies_derives_split_policies_from_legacy_policy() {
|
||||
let sandbox_policy = SandboxPolicy::new_read_only_policy();
|
||||
|
||||
let resolved =
|
||||
resolve_sandbox_policies(Path::new("/tmp"), Some(sandbox_policy.clone()), None, None);
|
||||
|
||||
assert_eq!(resolved.sandbox_policy, sandbox_policy.clone());
|
||||
assert_eq!(
|
||||
resolved.file_system_sandbox_policy,
|
||||
FileSystemSandboxPolicy::from(&sandbox_policy)
|
||||
);
|
||||
assert_eq!(
|
||||
resolved.network_sandbox_policy,
|
||||
NetworkSandboxPolicy::from(&sandbox_policy)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_sandbox_policies_derives_legacy_policy_from_split_policies() {
|
||||
let sandbox_policy = SandboxPolicy::new_read_only_policy();
|
||||
let file_system_sandbox_policy = FileSystemSandboxPolicy::from(&sandbox_policy);
|
||||
let network_sandbox_policy = NetworkSandboxPolicy::from(&sandbox_policy);
|
||||
|
||||
let resolved = resolve_sandbox_policies(
|
||||
Path::new("/tmp"),
|
||||
None,
|
||||
Some(file_system_sandbox_policy.clone()),
|
||||
Some(network_sandbox_policy),
|
||||
);
|
||||
|
||||
assert_eq!(resolved.sandbox_policy, sandbox_policy);
|
||||
assert_eq!(
|
||||
resolved.file_system_sandbox_policy,
|
||||
file_system_sandbox_policy
|
||||
);
|
||||
assert_eq!(resolved.network_sandbox_policy, network_sandbox_policy);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_sandbox_policies_rejects_partial_split_policies() {
|
||||
let result = std::panic::catch_unwind(|| {
|
||||
resolve_sandbox_policies(
|
||||
Path::new("/tmp"),
|
||||
Some(SandboxPolicy::new_read_only_policy()),
|
||||
Some(FileSystemSandboxPolicy::default()),
|
||||
None,
|
||||
)
|
||||
});
|
||||
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apply_seccomp_then_exec_without_bwrap_panics() {
|
||||
let result = std::panic::catch_unwind(|| ensure_inner_stage_mode_is_valid(true, false));
|
||||
|
||||
@@ -9,8 +9,6 @@ use codex_core::exec::process_exec_tool_call;
|
||||
use codex_core::exec_env::create_env;
|
||||
use codex_core::sandboxing::SandboxPermissions;
|
||||
use codex_protocol::config_types::WindowsSandboxLevel;
|
||||
use codex_protocol::protocol::FileSystemSandboxPolicy;
|
||||
use codex_protocol::protocol::NetworkSandboxPolicy;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use pretty_assertions::assert_eq;
|
||||
@@ -104,8 +102,6 @@ async fn run_cmd_result_with_writable_roots(
|
||||
process_exec_tool_call(
|
||||
params,
|
||||
&sandbox_policy,
|
||||
&FileSystemSandboxPolicy::from(&sandbox_policy),
|
||||
NetworkSandboxPolicy::from(&sandbox_policy),
|
||||
sandbox_cwd.as_path(),
|
||||
&codex_linux_sandbox_exe,
|
||||
use_bwrap_sandbox,
|
||||
@@ -337,8 +333,6 @@ async fn assert_network_blocked(cmd: &[&str]) {
|
||||
let result = process_exec_tool_call(
|
||||
params,
|
||||
&sandbox_policy,
|
||||
&FileSystemSandboxPolicy::from(&sandbox_policy),
|
||||
NetworkSandboxPolicy::from(&sandbox_policy),
|
||||
sandbox_cwd.as_path(),
|
||||
&codex_linux_sandbox_exe,
|
||||
false,
|
||||
|
||||
@@ -36,6 +36,8 @@ mitm = false
|
||||
# CA cert/key are managed internally under $CODEX_HOME/proxy/ (ca.pem + ca.key).
|
||||
|
||||
# Hosts must match the allowlist (unless denied).
|
||||
# Use exact hosts or scoped wildcards like `*.openai.com` or `**.openai.com`.
|
||||
# The global `*` wildcard is rejected.
|
||||
# If `allowed_domains` is empty, the proxy blocks requests until an allowlist is configured.
|
||||
allowed_domains = ["*.openai.com", "localhost", "127.0.0.1", "::1"]
|
||||
denied_domains = ["evil.example"]
|
||||
@@ -185,6 +187,7 @@ This section documents the protections implemented by `codex-network-proxy`, and
|
||||
what it can reasonably guarantee.
|
||||
|
||||
- Allowlist-first policy: if `allowed_domains` is empty, requests are blocked until an allowlist is configured.
|
||||
- Domain patterns: exact hosts plus scoped wildcards (`*.example.com`, `**.example.com`) are supported; the global `*` wildcard is rejected.
|
||||
- Deny wins: entries in `denied_domains` always override the allowlist.
|
||||
- Local/private network protection: when `allow_local_binding = false`, the proxy blocks loopback
|
||||
and common private/link-local ranges. Explicit allowlisting of local IP literals (or `localhost`)
|
||||
|
||||
@@ -83,7 +83,7 @@ async fn mitm_policy_rejects_host_mismatch() {
|
||||
#[tokio::test]
|
||||
async fn mitm_policy_rechecks_local_private_target_after_connect() {
|
||||
let app_state = Arc::new(network_proxy_state_for_policy(NetworkProxySettings {
|
||||
allowed_domains: vec!["*".to_string()],
|
||||
allowed_domains: vec!["example.com".to_string()],
|
||||
allow_local_binding: false,
|
||||
..NetworkProxySettings::default()
|
||||
}));
|
||||
|
||||
@@ -144,16 +144,26 @@ fn normalize_pattern(pattern: &str) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn is_global_wildcard_domain_pattern(pattern: &str) -> bool {
|
||||
let normalized = normalize_pattern(pattern);
|
||||
expand_domain_pattern(&normalized)
|
||||
.iter()
|
||||
.any(|candidate| candidate == "*")
|
||||
}
|
||||
|
||||
pub(crate) fn compile_globset(patterns: &[String]) -> Result<GlobSet> {
|
||||
let mut builder = GlobSetBuilder::new();
|
||||
let mut seen = HashSet::new();
|
||||
for pattern in patterns {
|
||||
ensure!(
|
||||
!is_global_wildcard_domain_pattern(pattern),
|
||||
"unsupported global wildcard domain pattern \"*\"; use exact hosts or scoped wildcards like *.example.com or **.example.com"
|
||||
);
|
||||
let pattern = normalize_pattern(pattern);
|
||||
// Supported domain patterns:
|
||||
// - "example.com": match the exact host
|
||||
// - "*.example.com": match any subdomain (not the apex)
|
||||
// - "**.example.com": match the apex and any subdomain
|
||||
// - "*": match any host
|
||||
for candidate in expand_domain_pattern(&pattern) {
|
||||
if !seen.insert(candidate.clone()) {
|
||||
continue;
|
||||
@@ -170,7 +180,6 @@ pub(crate) fn compile_globset(patterns: &[String]) -> Result<GlobSet> {
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) enum DomainPattern {
|
||||
Any,
|
||||
ApexAndSubdomains(String),
|
||||
SubdomainsOnly(String),
|
||||
Exact(String),
|
||||
@@ -186,9 +195,7 @@ impl DomainPattern {
|
||||
if input.is_empty() {
|
||||
return Self::Exact(String::new());
|
||||
}
|
||||
if input == "*" {
|
||||
Self::Any
|
||||
} else if let Some(domain) = input.strip_prefix("**.") {
|
||||
if let Some(domain) = input.strip_prefix("**.") {
|
||||
Self::parse_domain(domain, Self::ApexAndSubdomains)
|
||||
} else if let Some(domain) = input.strip_prefix("*.") {
|
||||
Self::parse_domain(domain, Self::SubdomainsOnly)
|
||||
@@ -203,9 +210,6 @@ impl DomainPattern {
|
||||
if input.is_empty() {
|
||||
return Self::Exact(String::new());
|
||||
}
|
||||
if input == "*" {
|
||||
return Self::Any;
|
||||
}
|
||||
if let Some(domain) = input.strip_prefix("**.") {
|
||||
return Self::ApexAndSubdomains(parse_domain_for_constraints(domain));
|
||||
}
|
||||
@@ -225,13 +229,11 @@ impl DomainPattern {
|
||||
|
||||
pub(crate) fn allows(&self, candidate: &DomainPattern) -> bool {
|
||||
match self {
|
||||
DomainPattern::Any => true,
|
||||
DomainPattern::Exact(domain) => match candidate {
|
||||
DomainPattern::Exact(candidate) => domain_eq(candidate, domain),
|
||||
_ => false,
|
||||
},
|
||||
DomainPattern::SubdomainsOnly(domain) => match candidate {
|
||||
DomainPattern::Any => false,
|
||||
DomainPattern::Exact(candidate) => is_strict_subdomain(candidate, domain),
|
||||
DomainPattern::SubdomainsOnly(candidate) => {
|
||||
is_subdomain_or_equal(candidate, domain)
|
||||
@@ -241,7 +243,6 @@ impl DomainPattern {
|
||||
}
|
||||
},
|
||||
DomainPattern::ApexAndSubdomains(domain) => match candidate {
|
||||
DomainPattern::Any => false,
|
||||
DomainPattern::Exact(candidate) => is_subdomain_or_equal(candidate, domain),
|
||||
DomainPattern::SubdomainsOnly(candidate) => {
|
||||
is_subdomain_or_equal(candidate, domain)
|
||||
@@ -275,7 +276,6 @@ fn parse_domain_for_constraints(domain: &str) -> String {
|
||||
|
||||
fn expand_domain_pattern(pattern: &str) -> Vec<String> {
|
||||
match DomainPattern::parse(pattern) {
|
||||
DomainPattern::Any => vec![pattern.to_string()],
|
||||
DomainPattern::Exact(domain) => vec![domain],
|
||||
DomainPattern::SubdomainsOnly(domain) => {
|
||||
vec![format!("?*.{domain}")]
|
||||
|
||||
@@ -821,6 +821,7 @@ mod tests {
|
||||
use crate::config::NetworkProxySettings;
|
||||
use crate::policy::compile_globset;
|
||||
use crate::state::NetworkProxyConstraints;
|
||||
use crate::state::build_config_state;
|
||||
use crate::state::validate_policy_against_constraints;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
@@ -1014,34 +1015,6 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn host_blocked_rejects_loopback_when_allowlist_is_wildcard() {
|
||||
let state = network_proxy_state_for_policy(NetworkProxySettings {
|
||||
allowed_domains: vec!["*".to_string()],
|
||||
allow_local_binding: false,
|
||||
..NetworkProxySettings::default()
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
state.host_blocked("127.0.0.1", 80).await.unwrap(),
|
||||
HostBlockDecision::Blocked(HostBlockReason::NotAllowedLocal)
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn host_blocked_rejects_private_ip_literal_when_allowlist_is_wildcard() {
|
||||
let state = network_proxy_state_for_policy(NetworkProxySettings {
|
||||
allowed_domains: vec!["*".to_string()],
|
||||
allow_local_binding: false,
|
||||
..NetworkProxySettings::default()
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
state.host_blocked("10.0.0.1", 80).await.unwrap(),
|
||||
HostBlockDecision::Blocked(HostBlockReason::NotAllowedLocal)
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn host_blocked_allows_loopback_when_explicitly_allowlisted_and_local_binding_disabled() {
|
||||
let state = network_proxy_state_for_policy(NetworkProxySettings {
|
||||
@@ -1198,6 +1171,62 @@ mod tests {
|
||||
assert!(validate_policy_against_constraints(&config, &constraints).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_policy_against_constraints_rejects_global_wildcard_in_managed_allowlist() {
|
||||
let constraints = NetworkProxyConstraints {
|
||||
allowed_domains: Some(vec!["*".to_string()]),
|
||||
..NetworkProxyConstraints::default()
|
||||
};
|
||||
|
||||
let config = NetworkProxyConfig {
|
||||
network: NetworkProxySettings {
|
||||
enabled: true,
|
||||
allowed_domains: vec!["api.example.com".to_string()],
|
||||
..NetworkProxySettings::default()
|
||||
},
|
||||
};
|
||||
|
||||
assert!(validate_policy_against_constraints(&config, &constraints).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_policy_against_constraints_rejects_bracketed_global_wildcard_in_managed_allowlist()
|
||||
{
|
||||
let constraints = NetworkProxyConstraints {
|
||||
allowed_domains: Some(vec!["[*]".to_string()]),
|
||||
..NetworkProxyConstraints::default()
|
||||
};
|
||||
|
||||
let config = NetworkProxyConfig {
|
||||
network: NetworkProxySettings {
|
||||
enabled: true,
|
||||
allowed_domains: vec!["api.example.com".to_string()],
|
||||
..NetworkProxySettings::default()
|
||||
},
|
||||
};
|
||||
|
||||
assert!(validate_policy_against_constraints(&config, &constraints).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_policy_against_constraints_rejects_double_wildcard_bracketed_global_wildcard_in_managed_allowlist()
|
||||
{
|
||||
let constraints = NetworkProxyConstraints {
|
||||
allowed_domains: Some(vec!["**.[*]".to_string()]),
|
||||
..NetworkProxyConstraints::default()
|
||||
};
|
||||
|
||||
let config = NetworkProxyConfig {
|
||||
network: NetworkProxySettings {
|
||||
enabled: true,
|
||||
allowed_domains: vec!["api.example.com".to_string()],
|
||||
..NetworkProxySettings::default()
|
||||
},
|
||||
};
|
||||
|
||||
assert!(validate_policy_against_constraints(&config, &constraints).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_policy_against_constraints_requires_managed_denied_domains_entries() {
|
||||
let constraints = NetworkProxyConstraints {
|
||||
@@ -1349,11 +1378,21 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compile_globset_matches_all_with_star() {
|
||||
fn compile_globset_rejects_global_wildcard() {
|
||||
let patterns = vec!["*".to_string()];
|
||||
let set = compile_globset(&patterns).unwrap();
|
||||
assert!(set.is_match("openai.com"));
|
||||
assert!(set.is_match("api.openai.com"));
|
||||
assert!(compile_globset(&patterns).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compile_globset_rejects_bracketed_global_wildcard() {
|
||||
let patterns = vec!["[*]".to_string()];
|
||||
assert!(compile_globset(&patterns).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compile_globset_rejects_double_wildcard_bracketed_global_wildcard() {
|
||||
let patterns = vec!["**.[*]".to_string()];
|
||||
assert!(compile_globset(&patterns).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1371,6 +1410,60 @@ mod tests {
|
||||
assert!(compile_globset(&patterns).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_config_state_rejects_global_wildcard_allowed_domains() {
|
||||
let config = NetworkProxyConfig {
|
||||
network: NetworkProxySettings {
|
||||
enabled: true,
|
||||
allowed_domains: vec!["*".to_string()],
|
||||
..NetworkProxySettings::default()
|
||||
},
|
||||
};
|
||||
|
||||
assert!(build_config_state(config, NetworkProxyConstraints::default()).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_config_state_rejects_bracketed_global_wildcard_allowed_domains() {
|
||||
let config = NetworkProxyConfig {
|
||||
network: NetworkProxySettings {
|
||||
enabled: true,
|
||||
allowed_domains: vec!["[*]".to_string()],
|
||||
..NetworkProxySettings::default()
|
||||
},
|
||||
};
|
||||
|
||||
assert!(build_config_state(config, NetworkProxyConstraints::default()).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_config_state_rejects_global_wildcard_denied_domains() {
|
||||
let config = NetworkProxyConfig {
|
||||
network: NetworkProxySettings {
|
||||
enabled: true,
|
||||
allowed_domains: vec!["example.com".to_string()],
|
||||
denied_domains: vec!["*".to_string()],
|
||||
..NetworkProxySettings::default()
|
||||
},
|
||||
};
|
||||
|
||||
assert!(build_config_state(config, NetworkProxyConstraints::default()).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_config_state_rejects_bracketed_global_wildcard_denied_domains() {
|
||||
let config = NetworkProxyConfig {
|
||||
network: NetworkProxySettings {
|
||||
enabled: true,
|
||||
allowed_domains: vec!["example.com".to_string()],
|
||||
denied_domains: vec!["[*]".to_string()],
|
||||
..NetworkProxySettings::default()
|
||||
},
|
||||
};
|
||||
|
||||
assert!(build_config_state(config, NetworkProxyConstraints::default()).is_err());
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[tokio::test]
|
||||
async fn unix_socket_allowlist_is_respected_on_macos() {
|
||||
|
||||
@@ -3,6 +3,7 @@ use crate::config::NetworkProxyConfig;
|
||||
use crate::mitm::MitmState;
|
||||
use crate::policy::DomainPattern;
|
||||
use crate::policy::compile_globset;
|
||||
use crate::policy::is_global_wildcard_domain_pattern;
|
||||
use crate::runtime::ConfigState;
|
||||
use serde::Deserialize;
|
||||
use std::collections::HashSet;
|
||||
@@ -56,6 +57,10 @@ pub fn build_config_state(
|
||||
constraints: NetworkProxyConstraints,
|
||||
) -> anyhow::Result<ConfigState> {
|
||||
crate::config::validate_unix_socket_allowlist_paths(&config)?;
|
||||
validate_domain_patterns("network.allowed_domains", &config.network.allowed_domains)
|
||||
.map_err(NetworkProxyConstraintError::into_anyhow)?;
|
||||
validate_domain_patterns("network.denied_domains", &config.network.denied_domains)
|
||||
.map_err(NetworkProxyConstraintError::into_anyhow)?;
|
||||
let deny_set = compile_globset(&config.network.denied_domains)?;
|
||||
let allow_set = compile_globset(&config.network.allowed_domains)?;
|
||||
let mitm = if config.network.mitm {
|
||||
@@ -100,6 +105,8 @@ pub fn validate_policy_against_constraints(
|
||||
}
|
||||
|
||||
let enabled = config.network.enabled;
|
||||
validate_domain_patterns("network.allowed_domains", &config.network.allowed_domains)?;
|
||||
validate_domain_patterns("network.denied_domains", &config.network.denied_domains)?;
|
||||
if let Some(max_enabled) = constraints.enabled {
|
||||
validate(enabled, move |candidate| {
|
||||
if *candidate && !max_enabled {
|
||||
@@ -199,6 +206,7 @@ pub fn validate_policy_against_constraints(
|
||||
}
|
||||
|
||||
if let Some(allowed_domains) = &constraints.allowed_domains {
|
||||
validate_domain_patterns("network.allowed_domains", allowed_domains)?;
|
||||
let managed_patterns: Vec<DomainPattern> = allowed_domains
|
||||
.iter()
|
||||
.map(|entry| DomainPattern::parse_for_constraints(entry))
|
||||
@@ -227,6 +235,7 @@ pub fn validate_policy_against_constraints(
|
||||
}
|
||||
|
||||
if let Some(denied_domains) = &constraints.denied_domains {
|
||||
validate_domain_patterns("network.denied_domains", denied_domains)?;
|
||||
let required_set: HashSet<String> = denied_domains
|
||||
.iter()
|
||||
.map(|s| s.to_ascii_lowercase())
|
||||
@@ -281,6 +290,24 @@ pub fn validate_policy_against_constraints(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_domain_patterns(
|
||||
field_name: &'static str,
|
||||
patterns: &[String],
|
||||
) -> Result<(), NetworkProxyConstraintError> {
|
||||
if let Some(pattern) = patterns
|
||||
.iter()
|
||||
.find(|pattern| is_global_wildcard_domain_pattern(pattern))
|
||||
{
|
||||
return Err(NetworkProxyConstraintError::InvalidValue {
|
||||
field_name,
|
||||
candidate: pattern.trim().to_string(),
|
||||
allowed: "exact hosts or scoped wildcards like *.example.com or **.example.com"
|
||||
.to_string(),
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
|
||||
pub enum NetworkProxyConstraintError {
|
||||
#[error("invalid value for {field_name}: {candidate} (allowed {allowed})")]
|
||||
|
||||
@@ -12,7 +12,6 @@ pub mod models;
|
||||
pub mod num_format;
|
||||
pub mod openai_models;
|
||||
pub mod parse_command;
|
||||
pub mod permissions;
|
||||
pub mod plan_tool;
|
||||
pub mod protocol;
|
||||
pub mod request_user_input;
|
||||
|
||||
@@ -1,743 +0,0 @@
|
||||
use std::collections::HashSet;
|
||||
use std::ffi::OsStr;
|
||||
use std::io;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use strum_macros::Display;
|
||||
use tracing::error;
|
||||
use ts_rs::TS;
|
||||
|
||||
use crate::protocol::NetworkAccess;
|
||||
use crate::protocol::ReadOnlyAccess;
|
||||
use crate::protocol::SandboxPolicy;
|
||||
use crate::protocol::WritableRoot;
|
||||
|
||||
#[derive(
|
||||
Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Display, Default, JsonSchema, TS,
|
||||
)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
#[strum(serialize_all = "kebab-case")]
|
||||
pub enum NetworkSandboxPolicy {
|
||||
#[default]
|
||||
Restricted,
|
||||
Enabled,
|
||||
}
|
||||
|
||||
impl NetworkSandboxPolicy {
|
||||
pub fn is_enabled(self) -> bool {
|
||||
matches!(self, NetworkSandboxPolicy::Enabled)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Display, JsonSchema, TS)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
#[strum(serialize_all = "lowercase")]
|
||||
pub enum FileSystemAccessMode {
|
||||
None,
|
||||
Read,
|
||||
Write,
|
||||
}
|
||||
|
||||
impl FileSystemAccessMode {
|
||||
pub fn can_read(self) -> bool {
|
||||
!matches!(self, FileSystemAccessMode::None)
|
||||
}
|
||||
|
||||
pub fn can_write(self) -> bool {
|
||||
matches!(self, FileSystemAccessMode::Write)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)]
|
||||
#[serde(tag = "kind", rename_all = "snake_case")]
|
||||
#[ts(tag = "kind")]
|
||||
pub enum FileSystemSpecialPath {
|
||||
Root,
|
||||
Minimal,
|
||||
CurrentWorkingDirectory,
|
||||
ProjectRoots {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
subpath: Option<PathBuf>,
|
||||
},
|
||||
Tmpdir,
|
||||
SlashTmp,
|
||||
}
|
||||
|
||||
impl FileSystemSpecialPath {
|
||||
pub fn project_roots(subpath: Option<PathBuf>) -> Self {
|
||||
Self::ProjectRoots { subpath }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)]
|
||||
pub struct FileSystemSandboxEntry {
|
||||
pub path: FileSystemPath,
|
||||
pub access: FileSystemAccessMode,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Display, Default, JsonSchema, TS,
|
||||
)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
#[strum(serialize_all = "kebab-case")]
|
||||
pub enum FileSystemSandboxKind {
|
||||
#[default]
|
||||
Restricted,
|
||||
Unrestricted,
|
||||
ExternalSandbox,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)]
|
||||
pub struct FileSystemSandboxPolicy {
|
||||
pub kind: FileSystemSandboxKind,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub entries: Vec<FileSystemSandboxEntry>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
#[ts(tag = "type")]
|
||||
pub enum FileSystemPath {
|
||||
Path { path: AbsolutePathBuf },
|
||||
Special { value: FileSystemSpecialPath },
|
||||
}
|
||||
|
||||
impl Default for FileSystemSandboxPolicy {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
kind: FileSystemSandboxKind::Restricted,
|
||||
entries: vec![FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Special {
|
||||
value: FileSystemSpecialPath::Root,
|
||||
},
|
||||
access: FileSystemAccessMode::Read,
|
||||
}],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FileSystemSandboxPolicy {
|
||||
fn has_root_access(&self, predicate: impl Fn(FileSystemAccessMode) -> bool) -> bool {
|
||||
matches!(self.kind, FileSystemSandboxKind::Restricted)
|
||||
&& self.entries.iter().any(|entry| {
|
||||
matches!(
|
||||
&entry.path,
|
||||
FileSystemPath::Special { value }
|
||||
if value.kind == FileSystemSpecialPathKind::Root && predicate(entry.access)
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn has_explicit_deny_entries(&self) -> bool {
|
||||
matches!(self.kind, FileSystemSandboxKind::Restricted)
|
||||
&& self
|
||||
.entries
|
||||
.iter()
|
||||
.any(|entry| entry.access == FileSystemAccessMode::None)
|
||||
}
|
||||
|
||||
pub fn unrestricted() -> Self {
|
||||
Self {
|
||||
kind: FileSystemSandboxKind::Unrestricted,
|
||||
entries: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn external_sandbox() -> Self {
|
||||
Self {
|
||||
kind: FileSystemSandboxKind::ExternalSandbox,
|
||||
entries: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn restricted(entries: Vec<FileSystemSandboxEntry>) -> Self {
|
||||
Self {
|
||||
kind: FileSystemSandboxKind::Restricted,
|
||||
entries,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true when filesystem reads are unrestricted.
|
||||
pub fn has_full_disk_read_access(&self) -> bool {
|
||||
match self.kind {
|
||||
FileSystemSandboxKind::Unrestricted | FileSystemSandboxKind::ExternalSandbox => true,
|
||||
FileSystemSandboxKind::Restricted => {
|
||||
self.has_root_access(FileSystemAccessMode::can_read)
|
||||
&& !self.has_explicit_deny_entries()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true when filesystem writes are unrestricted.
|
||||
pub fn has_full_disk_write_access(&self) -> bool {
|
||||
match self.kind {
|
||||
FileSystemSandboxKind::Unrestricted | FileSystemSandboxKind::ExternalSandbox => true,
|
||||
FileSystemSandboxKind::Restricted => {
|
||||
self.has_root_access(FileSystemAccessMode::can_write)
|
||||
&& !self.has_explicit_deny_entries()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true when platform-default readable roots should be included.
|
||||
pub fn include_platform_defaults(&self) -> bool {
|
||||
!self.has_full_disk_read_access()
|
||||
&& matches!(self.kind, FileSystemSandboxKind::Restricted)
|
||||
&& self.entries.iter().any(|entry| {
|
||||
matches!(
|
||||
&entry.path,
|
||||
FileSystemPath::Special { value }
|
||||
if value.kind == FileSystemSpecialPathKind::Minimal
|
||||
&& entry.access.can_read()
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns the explicit readable roots resolved against the provided cwd.
|
||||
pub fn get_readable_roots_with_cwd(&self, cwd: &Path) -> Vec<AbsolutePathBuf> {
|
||||
if self.has_full_disk_read_access() {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let cwd_absolute = AbsolutePathBuf::from_absolute_path(cwd).ok();
|
||||
let mut readable_roots = Vec::new();
|
||||
if self.has_root_access(FileSystemAccessMode::can_read)
|
||||
&& let Some(cwd_absolute) = cwd_absolute.as_ref()
|
||||
{
|
||||
readable_roots.push(absolute_root_path_for_cwd(cwd_absolute));
|
||||
}
|
||||
|
||||
dedup_absolute_paths(
|
||||
readable_roots
|
||||
.into_iter()
|
||||
.chain(
|
||||
self.entries
|
||||
.iter()
|
||||
.filter(|entry| entry.access.can_read())
|
||||
.filter_map(|entry| {
|
||||
resolve_file_system_path(&entry.path, cwd_absolute.as_ref())
|
||||
}),
|
||||
)
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns the writable roots together with read-only carveouts resolved
|
||||
/// against the provided cwd.
|
||||
pub fn get_writable_roots_with_cwd(&self, cwd: &Path) -> Vec<WritableRoot> {
|
||||
if self.has_full_disk_write_access() {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let cwd_absolute = AbsolutePathBuf::from_absolute_path(cwd).ok();
|
||||
let unreadable_roots = self.get_unreadable_roots_with_cwd(cwd);
|
||||
let mut writable_roots = Vec::new();
|
||||
if self.has_root_access(FileSystemAccessMode::can_write)
|
||||
&& let Some(cwd_absolute) = cwd_absolute.as_ref()
|
||||
{
|
||||
writable_roots.push(absolute_root_path_for_cwd(cwd_absolute));
|
||||
}
|
||||
|
||||
dedup_absolute_paths(
|
||||
writable_roots
|
||||
.into_iter()
|
||||
.chain(
|
||||
self.entries
|
||||
.iter()
|
||||
.filter(|entry| entry.access.can_write())
|
||||
.filter_map(|entry| {
|
||||
resolve_file_system_path(&entry.path, cwd_absolute.as_ref())
|
||||
}),
|
||||
)
|
||||
.collect(),
|
||||
)
|
||||
.into_iter()
|
||||
.map(|root| {
|
||||
let mut read_only_subpaths = default_read_only_subpaths_for_writable_root(&root);
|
||||
read_only_subpaths.extend(
|
||||
unreadable_roots
|
||||
.iter()
|
||||
.filter(|path| path.as_path().starts_with(root.as_path()))
|
||||
.cloned(),
|
||||
);
|
||||
WritableRoot {
|
||||
root,
|
||||
read_only_subpaths: dedup_absolute_paths(read_only_subpaths),
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Returns explicit unreadable roots resolved against the provided cwd.
|
||||
pub fn get_unreadable_roots_with_cwd(&self, cwd: &Path) -> Vec<AbsolutePathBuf> {
|
||||
if !matches!(self.kind, FileSystemSandboxKind::Restricted) {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let cwd_absolute = AbsolutePathBuf::from_absolute_path(cwd).ok();
|
||||
dedup_absolute_paths(
|
||||
self.entries
|
||||
.iter()
|
||||
.filter(|entry| entry.access == FileSystemAccessMode::None)
|
||||
.filter_map(|entry| resolve_file_system_path(&entry.path, cwd_absolute.as_ref()))
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn to_legacy_sandbox_policy(
|
||||
&self,
|
||||
network_policy: NetworkSandboxPolicy,
|
||||
cwd: &Path,
|
||||
) -> io::Result<SandboxPolicy> {
|
||||
Ok(match self.kind {
|
||||
FileSystemSandboxKind::ExternalSandbox => SandboxPolicy::ExternalSandbox {
|
||||
network_access: if network_policy.is_enabled() {
|
||||
NetworkAccess::Enabled
|
||||
} else {
|
||||
NetworkAccess::Restricted
|
||||
},
|
||||
},
|
||||
FileSystemSandboxKind::Unrestricted => {
|
||||
if network_policy.is_enabled() {
|
||||
SandboxPolicy::DangerFullAccess
|
||||
} else {
|
||||
SandboxPolicy::ExternalSandbox {
|
||||
network_access: NetworkAccess::Restricted,
|
||||
}
|
||||
}
|
||||
}
|
||||
FileSystemSandboxKind::Restricted => {
|
||||
let cwd_absolute = AbsolutePathBuf::from_absolute_path(cwd).ok();
|
||||
let mut include_platform_defaults = false;
|
||||
let mut has_full_disk_read_access = false;
|
||||
let mut has_full_disk_write_access = false;
|
||||
let mut workspace_root_writable = false;
|
||||
let mut writable_roots = Vec::new();
|
||||
let mut readable_roots = Vec::new();
|
||||
let mut tmpdir_writable = false;
|
||||
let mut slash_tmp_writable = false;
|
||||
|
||||
for entry in &self.entries {
|
||||
match &entry.path {
|
||||
FileSystemPath::Path { path } => {
|
||||
if entry.access.can_write() {
|
||||
if cwd_absolute.as_ref().is_some_and(|cwd| cwd == path) {
|
||||
workspace_root_writable = true;
|
||||
} else {
|
||||
writable_roots.push(path.clone());
|
||||
}
|
||||
} else if entry.access.can_read() {
|
||||
readable_roots.push(path.clone());
|
||||
}
|
||||
}
|
||||
FileSystemPath::Special { value } => match value {
|
||||
FileSystemSpecialPath::Root => match entry.access {
|
||||
FileSystemAccessMode::None => {}
|
||||
FileSystemAccessMode::Read => has_full_disk_read_access = true,
|
||||
FileSystemAccessMode::Write => {
|
||||
has_full_disk_read_access = true;
|
||||
has_full_disk_write_access = true;
|
||||
}
|
||||
},
|
||||
FileSystemSpecialPath::Minimal => {
|
||||
if entry.access.can_read() {
|
||||
include_platform_defaults = true;
|
||||
}
|
||||
}
|
||||
FileSystemSpecialPath::CurrentWorkingDirectory => {
|
||||
if entry.access.can_write() {
|
||||
workspace_root_writable = true;
|
||||
} else if entry.access.can_read()
|
||||
&& let Some(path) = resolve_file_system_special_path(
|
||||
value,
|
||||
cwd_absolute.as_ref(),
|
||||
)
|
||||
{
|
||||
readable_roots.push(path);
|
||||
}
|
||||
}
|
||||
FileSystemSpecialPath::ProjectRoots { subpath } => {
|
||||
if subpath.is_none() && entry.access.can_write() {
|
||||
workspace_root_writable = true;
|
||||
} else if let Some(path) =
|
||||
resolve_file_system_special_path(value, cwd_absolute.as_ref())
|
||||
{
|
||||
if entry.access.can_write() {
|
||||
writable_roots.push(path);
|
||||
} else if entry.access.can_read() {
|
||||
readable_roots.push(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
FileSystemSpecialPath::Tmpdir => {
|
||||
if entry.access.can_write() {
|
||||
tmpdir_writable = true;
|
||||
} else if entry.access.can_read()
|
||||
&& let Some(path) = resolve_file_system_special_path(
|
||||
value,
|
||||
cwd_absolute.as_ref(),
|
||||
)
|
||||
{
|
||||
readable_roots.push(path);
|
||||
}
|
||||
}
|
||||
FileSystemSpecialPath::SlashTmp => {
|
||||
if entry.access.can_write() {
|
||||
slash_tmp_writable = true;
|
||||
} else if entry.access.can_read()
|
||||
&& let Some(path) = resolve_file_system_special_path(
|
||||
value,
|
||||
cwd_absolute.as_ref(),
|
||||
)
|
||||
{
|
||||
readable_roots.push(path);
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if has_full_disk_write_access {
|
||||
return Ok(if network_policy.is_enabled() {
|
||||
SandboxPolicy::DangerFullAccess
|
||||
} else {
|
||||
SandboxPolicy::ExternalSandbox {
|
||||
network_access: NetworkAccess::Restricted,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let read_only_access = if has_full_disk_read_access {
|
||||
ReadOnlyAccess::FullAccess
|
||||
} else {
|
||||
ReadOnlyAccess::Restricted {
|
||||
include_platform_defaults,
|
||||
readable_roots: dedup_absolute_paths(readable_roots),
|
||||
}
|
||||
};
|
||||
|
||||
if workspace_root_writable {
|
||||
SandboxPolicy::WorkspaceWrite {
|
||||
writable_roots: dedup_absolute_paths(writable_roots),
|
||||
read_only_access,
|
||||
network_access: network_policy.is_enabled(),
|
||||
exclude_tmpdir_env_var: !tmpdir_writable,
|
||||
exclude_slash_tmp: !slash_tmp_writable,
|
||||
}
|
||||
} else if !writable_roots.is_empty() || tmpdir_writable || slash_tmp_writable {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"permissions profile requests filesystem writes outside the workspace root, which is not supported until the runtime enforces FileSystemSandboxPolicy directly",
|
||||
));
|
||||
} else {
|
||||
SandboxPolicy::ReadOnly {
|
||||
access: read_only_access,
|
||||
network_access: network_policy.is_enabled(),
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&SandboxPolicy> for NetworkSandboxPolicy {
|
||||
fn from(value: &SandboxPolicy) -> Self {
|
||||
if value.has_full_network_access() {
|
||||
NetworkSandboxPolicy::Enabled
|
||||
} else {
|
||||
NetworkSandboxPolicy::Restricted
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&SandboxPolicy> for FileSystemSandboxPolicy {
|
||||
fn from(value: &SandboxPolicy) -> Self {
|
||||
match value {
|
||||
SandboxPolicy::DangerFullAccess => FileSystemSandboxPolicy::unrestricted(),
|
||||
SandboxPolicy::ExternalSandbox { .. } => FileSystemSandboxPolicy::external_sandbox(),
|
||||
SandboxPolicy::ReadOnly { access, .. } => {
|
||||
let mut entries = Vec::new();
|
||||
match access {
|
||||
ReadOnlyAccess::FullAccess => entries.push(FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Special {
|
||||
value: FileSystemSpecialPath::Root,
|
||||
},
|
||||
access: FileSystemAccessMode::Read,
|
||||
}),
|
||||
ReadOnlyAccess::Restricted {
|
||||
include_platform_defaults,
|
||||
readable_roots,
|
||||
} => {
|
||||
entries.push(FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Special {
|
||||
value: FileSystemSpecialPath::CurrentWorkingDirectory,
|
||||
},
|
||||
access: FileSystemAccessMode::Read,
|
||||
});
|
||||
if *include_platform_defaults {
|
||||
entries.push(FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Special {
|
||||
value: FileSystemSpecialPath::Minimal,
|
||||
},
|
||||
access: FileSystemAccessMode::Read,
|
||||
});
|
||||
}
|
||||
entries.extend(readable_roots.iter().cloned().map(|path| {
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Path { path },
|
||||
access: FileSystemAccessMode::Read,
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
FileSystemSandboxPolicy::restricted(entries)
|
||||
}
|
||||
SandboxPolicy::WorkspaceWrite {
|
||||
writable_roots,
|
||||
read_only_access,
|
||||
exclude_tmpdir_env_var,
|
||||
exclude_slash_tmp,
|
||||
..
|
||||
} => {
|
||||
let mut entries = Vec::new();
|
||||
match read_only_access {
|
||||
ReadOnlyAccess::FullAccess => entries.push(FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Special {
|
||||
value: FileSystemSpecialPath::Root,
|
||||
},
|
||||
access: FileSystemAccessMode::Read,
|
||||
}),
|
||||
ReadOnlyAccess::Restricted {
|
||||
include_platform_defaults,
|
||||
readable_roots,
|
||||
} => {
|
||||
if *include_platform_defaults {
|
||||
entries.push(FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Special {
|
||||
value: FileSystemSpecialPath::Minimal,
|
||||
},
|
||||
access: FileSystemAccessMode::Read,
|
||||
});
|
||||
}
|
||||
entries.extend(readable_roots.iter().cloned().map(|path| {
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Path { path },
|
||||
access: FileSystemAccessMode::Read,
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
entries.push(FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Special {
|
||||
value: FileSystemSpecialPath::CurrentWorkingDirectory,
|
||||
},
|
||||
access: FileSystemAccessMode::Write,
|
||||
});
|
||||
if !exclude_slash_tmp {
|
||||
entries.push(FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Special {
|
||||
value: FileSystemSpecialPath::SlashTmp,
|
||||
},
|
||||
access: FileSystemAccessMode::Write,
|
||||
});
|
||||
}
|
||||
if !exclude_tmpdir_env_var {
|
||||
entries.push(FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Special {
|
||||
value: FileSystemSpecialPath::Tmpdir,
|
||||
},
|
||||
access: FileSystemAccessMode::Write,
|
||||
});
|
||||
}
|
||||
entries.extend(
|
||||
writable_roots
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|path| FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Path { path },
|
||||
access: FileSystemAccessMode::Write,
|
||||
}),
|
||||
);
|
||||
FileSystemSandboxPolicy::restricted(entries)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_file_system_path(
|
||||
path: &FileSystemPath,
|
||||
cwd: Option<&AbsolutePathBuf>,
|
||||
) -> Option<AbsolutePathBuf> {
|
||||
match path {
|
||||
FileSystemPath::Path { path } => Some(path.clone()),
|
||||
FileSystemPath::Special { value } => resolve_file_system_special_path(value, cwd),
|
||||
}
|
||||
}
|
||||
|
||||
fn absolute_root_path_for_cwd(cwd: &AbsolutePathBuf) -> AbsolutePathBuf {
|
||||
let root = cwd
|
||||
.as_path()
|
||||
.ancestors()
|
||||
.last()
|
||||
.unwrap_or_else(|| panic!("cwd must have a filesystem root"));
|
||||
AbsolutePathBuf::from_absolute_path(root)
|
||||
.unwrap_or_else(|err| panic!("cwd root must be an absolute path: {err}"))
|
||||
}
|
||||
|
||||
fn resolve_file_system_special_path(
|
||||
value: &FileSystemSpecialPath,
|
||||
cwd: Option<&AbsolutePathBuf>,
|
||||
) -> Option<AbsolutePathBuf> {
|
||||
match value {
|
||||
FileSystemSpecialPath::Root | FileSystemSpecialPath::Minimal => None,
|
||||
FileSystemSpecialPath::CurrentWorkingDirectory => {
|
||||
let cwd = cwd?;
|
||||
Some(cwd.clone())
|
||||
}
|
||||
FileSystemSpecialPath::ProjectRoots { subpath } => {
|
||||
let cwd = cwd?;
|
||||
match subpath.as_ref() {
|
||||
Some(subpath) => {
|
||||
AbsolutePathBuf::resolve_path_against_base(subpath, cwd.as_path()).ok()
|
||||
}
|
||||
None => Some(cwd.clone()),
|
||||
}
|
||||
}
|
||||
FileSystemSpecialPath::Tmpdir => {
|
||||
let tmpdir = std::env::var_os("TMPDIR")?;
|
||||
if tmpdir.is_empty() {
|
||||
None
|
||||
} else {
|
||||
let tmpdir = AbsolutePathBuf::from_absolute_path(PathBuf::from(tmpdir)).ok()?;
|
||||
Some(tmpdir)
|
||||
}
|
||||
}
|
||||
FileSystemSpecialPath::SlashTmp => {
|
||||
#[allow(clippy::expect_used)]
|
||||
let slash_tmp = AbsolutePathBuf::from_absolute_path("/tmp").expect("/tmp is absolute");
|
||||
if !slash_tmp.as_path().is_dir() {
|
||||
return None;
|
||||
}
|
||||
Some(slash_tmp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn dedup_absolute_paths(paths: Vec<AbsolutePathBuf>) -> Vec<AbsolutePathBuf> {
|
||||
let mut deduped = Vec::with_capacity(paths.len());
|
||||
let mut seen = HashSet::new();
|
||||
for path in paths {
|
||||
if seen.insert(path.to_path_buf()) {
|
||||
deduped.push(path);
|
||||
}
|
||||
}
|
||||
deduped
|
||||
}
|
||||
|
||||
fn default_read_only_subpaths_for_writable_root(
|
||||
writable_root: &AbsolutePathBuf,
|
||||
) -> Vec<AbsolutePathBuf> {
|
||||
let mut subpaths: Vec<AbsolutePathBuf> = Vec::new();
|
||||
#[allow(clippy::expect_used)]
|
||||
let top_level_git = writable_root
|
||||
.join(".git")
|
||||
.expect(".git is a valid relative path");
|
||||
// This applies to typical repos (directory .git), worktrees/submodules
|
||||
// (file .git with gitdir pointer), and bare repos when the gitdir is the
|
||||
// writable root itself.
|
||||
let top_level_git_is_file = top_level_git.as_path().is_file();
|
||||
let top_level_git_is_dir = top_level_git.as_path().is_dir();
|
||||
if top_level_git_is_dir || top_level_git_is_file {
|
||||
if top_level_git_is_file
|
||||
&& is_git_pointer_file(&top_level_git)
|
||||
&& let Some(gitdir) = resolve_gitdir_from_file(&top_level_git)
|
||||
{
|
||||
subpaths.push(gitdir);
|
||||
}
|
||||
subpaths.push(top_level_git);
|
||||
}
|
||||
|
||||
// Make .agents/skills and .codex/config.toml and related files read-only
|
||||
// to the agent, by default.
|
||||
for subdir in &[".agents", ".codex"] {
|
||||
#[allow(clippy::expect_used)]
|
||||
let top_level_codex = writable_root.join(subdir).expect("valid relative path");
|
||||
if top_level_codex.as_path().is_dir() {
|
||||
subpaths.push(top_level_codex);
|
||||
}
|
||||
}
|
||||
|
||||
dedup_absolute_paths(subpaths)
|
||||
}
|
||||
|
||||
fn is_git_pointer_file(path: &AbsolutePathBuf) -> bool {
|
||||
path.as_path().is_file() && path.as_path().file_name() == Some(OsStr::new(".git"))
|
||||
}
|
||||
|
||||
fn resolve_gitdir_from_file(dot_git: &AbsolutePathBuf) -> Option<AbsolutePathBuf> {
|
||||
let contents = match std::fs::read_to_string(dot_git.as_path()) {
|
||||
Ok(contents) => contents,
|
||||
Err(err) => {
|
||||
error!(
|
||||
"Failed to read {path} for gitdir pointer: {err}",
|
||||
path = dot_git.as_path().display()
|
||||
);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
let trimmed = contents.trim();
|
||||
let (_, gitdir_raw) = match trimmed.split_once(':') {
|
||||
Some(parts) => parts,
|
||||
None => {
|
||||
error!(
|
||||
"Expected {path} to contain a gitdir pointer, but it did not match `gitdir: <path>`.",
|
||||
path = dot_git.as_path().display()
|
||||
);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
let gitdir_raw = gitdir_raw.trim();
|
||||
if gitdir_raw.is_empty() {
|
||||
error!(
|
||||
"Expected {path} to contain a gitdir pointer, but it was empty.",
|
||||
path = dot_git.as_path().display()
|
||||
);
|
||||
return None;
|
||||
}
|
||||
let base = match dot_git.as_path().parent() {
|
||||
Some(base) => base,
|
||||
None => {
|
||||
error!(
|
||||
"Unable to resolve parent directory for {path}.",
|
||||
path = dot_git.as_path().display()
|
||||
);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
let gitdir_path = match AbsolutePathBuf::resolve_path_against_base(gitdir_raw, base) {
|
||||
Ok(path) => path,
|
||||
Err(err) => {
|
||||
error!(
|
||||
"Failed to resolve gitdir path {gitdir_raw} from {path}: {err}",
|
||||
path = dot_git.as_path().display()
|
||||
);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
if !gitdir_path.as_path().exists() {
|
||||
error!(
|
||||
"Resolved gitdir path {path} does not exist.",
|
||||
path = gitdir_path.as_path().display()
|
||||
);
|
||||
return None;
|
||||
}
|
||||
Some(gitdir_path)
|
||||
}
|
||||
@@ -61,14 +61,6 @@ pub use crate::approvals::NetworkApprovalContext;
|
||||
pub use crate::approvals::NetworkApprovalProtocol;
|
||||
pub use crate::approvals::NetworkPolicyAmendment;
|
||||
pub use crate::approvals::NetworkPolicyRuleAction;
|
||||
pub use crate::permissions::FileSystemAccessMode;
|
||||
pub use crate::permissions::FileSystemPath;
|
||||
pub use crate::permissions::FileSystemSandboxEntry;
|
||||
pub use crate::permissions::FileSystemSandboxKind;
|
||||
pub use crate::permissions::FileSystemSandboxPolicy;
|
||||
pub use crate::permissions::FileSystemSpecialPath;
|
||||
pub use crate::permissions::FileSystemSpecialPathKind;
|
||||
pub use crate::permissions::NetworkSandboxPolicy;
|
||||
pub use crate::request_user_input::RequestUserInputEvent;
|
||||
|
||||
/// Open/close tags for special user-input blocks. Used across crates to avoid
|
||||
@@ -550,6 +542,7 @@ impl NetworkAccess {
|
||||
matches!(self, NetworkAccess::Enabled)
|
||||
}
|
||||
}
|
||||
|
||||
fn default_include_platform_defaults() -> bool {
|
||||
true
|
||||
}
|
||||
@@ -728,22 +721,6 @@ impl FromStr for SandboxPolicy {
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for FileSystemSandboxPolicy {
|
||||
type Err = serde_json::Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
serde_json::from_str(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for NetworkSandboxPolicy {
|
||||
type Err = serde_json::Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
serde_json::from_str(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl SandboxPolicy {
|
||||
/// Returns a policy with read-only disk access and no network.
|
||||
pub fn new_read_only_policy() -> Self {
|
||||
@@ -906,11 +883,45 @@ impl SandboxPolicy {
|
||||
// For each root, compute subpaths that should remain read-only.
|
||||
roots
|
||||
.into_iter()
|
||||
.map(|writable_root| WritableRoot {
|
||||
read_only_subpaths: default_read_only_subpaths_for_writable_root(
|
||||
&writable_root,
|
||||
),
|
||||
root: writable_root,
|
||||
.map(|writable_root| {
|
||||
let mut subpaths: Vec<AbsolutePathBuf> = Vec::new();
|
||||
#[allow(clippy::expect_used)]
|
||||
let top_level_git = writable_root
|
||||
.join(".git")
|
||||
.expect(".git is a valid relative path");
|
||||
// This applies to typical repos (directory .git), worktrees/submodules
|
||||
// (file .git with gitdir pointer), and bare repos when the gitdir is the
|
||||
// writable root itself.
|
||||
let top_level_git_is_file = top_level_git.as_path().is_file();
|
||||
let top_level_git_is_dir = top_level_git.as_path().is_dir();
|
||||
if top_level_git_is_dir || top_level_git_is_file {
|
||||
if top_level_git_is_file
|
||||
&& is_git_pointer_file(&top_level_git)
|
||||
&& let Some(gitdir) = resolve_gitdir_from_file(&top_level_git)
|
||||
&& !subpaths
|
||||
.iter()
|
||||
.any(|subpath| subpath.as_path() == gitdir.as_path())
|
||||
{
|
||||
subpaths.push(gitdir);
|
||||
}
|
||||
subpaths.push(top_level_git);
|
||||
}
|
||||
|
||||
// Make .agents/skills and .codex/config.toml and
|
||||
// related files read-only to the agent, by default.
|
||||
for subdir in &[".agents", ".codex"] {
|
||||
#[allow(clippy::expect_used)]
|
||||
let top_level_codex =
|
||||
writable_root.join(subdir).expect("valid relative path");
|
||||
if top_level_codex.as_path().is_dir() {
|
||||
subpaths.push(top_level_codex);
|
||||
}
|
||||
}
|
||||
|
||||
WritableRoot {
|
||||
root: writable_root,
|
||||
read_only_subpaths: subpaths,
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@@ -918,49 +929,6 @@ impl SandboxPolicy {
|
||||
}
|
||||
}
|
||||
|
||||
fn default_read_only_subpaths_for_writable_root(
|
||||
writable_root: &AbsolutePathBuf,
|
||||
) -> Vec<AbsolutePathBuf> {
|
||||
let mut subpaths: Vec<AbsolutePathBuf> = Vec::new();
|
||||
#[allow(clippy::expect_used)]
|
||||
let top_level_git = writable_root
|
||||
.join(".git")
|
||||
.expect(".git is a valid relative path");
|
||||
// This applies to typical repos (directory .git), worktrees/submodules
|
||||
// (file .git with gitdir pointer), and bare repos when the gitdir is the
|
||||
// writable root itself.
|
||||
let top_level_git_is_file = top_level_git.as_path().is_file();
|
||||
let top_level_git_is_dir = top_level_git.as_path().is_dir();
|
||||
if top_level_git_is_dir || top_level_git_is_file {
|
||||
if top_level_git_is_file
|
||||
&& is_git_pointer_file(&top_level_git)
|
||||
&& let Some(gitdir) = resolve_gitdir_from_file(&top_level_git)
|
||||
{
|
||||
subpaths.push(gitdir);
|
||||
}
|
||||
subpaths.push(top_level_git);
|
||||
}
|
||||
|
||||
// Make .agents/skills and .codex/config.toml and related files read-only
|
||||
// to the agent, by default.
|
||||
for subdir in &[".agents", ".codex"] {
|
||||
#[allow(clippy::expect_used)]
|
||||
let top_level_codex = writable_root.join(subdir).expect("valid relative path");
|
||||
if top_level_codex.as_path().is_dir() {
|
||||
subpaths.push(top_level_codex);
|
||||
}
|
||||
}
|
||||
|
||||
let mut deduped = Vec::with_capacity(subpaths.len());
|
||||
let mut seen = HashSet::new();
|
||||
for path in subpaths {
|
||||
if seen.insert(path.to_path_buf()) {
|
||||
deduped.push(path);
|
||||
}
|
||||
}
|
||||
deduped
|
||||
}
|
||||
|
||||
fn is_git_pointer_file(path: &AbsolutePathBuf) -> bool {
|
||||
path.as_path().is_file() && path.as_path().file_name() == Some(OsStr::new(".git"))
|
||||
}
|
||||
@@ -3184,18 +3152,10 @@ mod tests {
|
||||
use crate::items::ImageGenerationItem;
|
||||
use crate::items::UserMessageItem;
|
||||
use crate::items::WebSearchItem;
|
||||
use crate::permissions::FileSystemAccessMode;
|
||||
use crate::permissions::FileSystemPath;
|
||||
use crate::permissions::FileSystemSandboxEntry;
|
||||
use crate::permissions::FileSystemSandboxPolicy;
|
||||
use crate::permissions::FileSystemSpecialPath;
|
||||
use crate::permissions::FileSystemSpecialPathKind;
|
||||
use crate::permissions::NetworkSandboxPolicy;
|
||||
use anyhow::Result;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::json;
|
||||
use tempfile::NamedTempFile;
|
||||
use tempfile::TempDir;
|
||||
|
||||
#[test]
|
||||
fn external_sandbox_reports_full_access_flags() {
|
||||
@@ -3276,198 +3236,6 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn restricted_file_system_policy_reports_full_access_from_root_entries() {
|
||||
let read_only = FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Special {
|
||||
value: FileSystemSpecialPath {
|
||||
kind: FileSystemSpecialPathKind::Root,
|
||||
subpath: None,
|
||||
},
|
||||
},
|
||||
access: FileSystemAccessMode::Read,
|
||||
}]);
|
||||
assert!(read_only.has_full_disk_read_access());
|
||||
assert!(!read_only.has_full_disk_write_access());
|
||||
assert!(!read_only.include_platform_defaults());
|
||||
|
||||
let writable = FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Special {
|
||||
value: FileSystemSpecialPath {
|
||||
kind: FileSystemSpecialPathKind::Root,
|
||||
subpath: None,
|
||||
},
|
||||
},
|
||||
access: FileSystemAccessMode::Write,
|
||||
}]);
|
||||
assert!(writable.has_full_disk_read_access());
|
||||
assert!(writable.has_full_disk_write_access());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn restricted_file_system_policy_treats_root_with_carveouts_as_scoped_access() {
|
||||
let cwd = TempDir::new().expect("tempdir");
|
||||
let cwd_absolute =
|
||||
AbsolutePathBuf::from_absolute_path(cwd.path()).expect("absolute tempdir");
|
||||
let root = cwd_absolute
|
||||
.as_path()
|
||||
.ancestors()
|
||||
.last()
|
||||
.and_then(|path| AbsolutePathBuf::from_absolute_path(path).ok())
|
||||
.expect("filesystem root");
|
||||
let blocked = AbsolutePathBuf::resolve_path_against_base("blocked", cwd.path())
|
||||
.expect("resolve blocked");
|
||||
let policy = FileSystemSandboxPolicy::restricted(vec![
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Special {
|
||||
value: FileSystemSpecialPath {
|
||||
kind: FileSystemSpecialPathKind::Root,
|
||||
subpath: None,
|
||||
},
|
||||
},
|
||||
access: FileSystemAccessMode::Write,
|
||||
},
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Special {
|
||||
value: FileSystemSpecialPath {
|
||||
kind: FileSystemSpecialPathKind::CurrentWorkingDirectory,
|
||||
subpath: Some(PathBuf::from("blocked")),
|
||||
},
|
||||
},
|
||||
access: FileSystemAccessMode::None,
|
||||
},
|
||||
]);
|
||||
|
||||
assert!(!policy.has_full_disk_read_access());
|
||||
assert!(!policy.has_full_disk_write_access());
|
||||
assert_eq!(
|
||||
policy.get_readable_roots_with_cwd(cwd.path()),
|
||||
vec![root.clone()]
|
||||
);
|
||||
assert_eq!(
|
||||
policy.get_unreadable_roots_with_cwd(cwd.path()),
|
||||
vec![blocked.clone()]
|
||||
);
|
||||
|
||||
let writable_roots = policy.get_writable_roots_with_cwd(cwd.path());
|
||||
assert_eq!(writable_roots.len(), 1);
|
||||
assert_eq!(writable_roots[0].root, root);
|
||||
assert!(
|
||||
writable_roots[0]
|
||||
.read_only_subpaths
|
||||
.iter()
|
||||
.any(|path| path.as_path() == blocked.as_path())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn restricted_file_system_policy_derives_effective_paths() {
|
||||
let cwd = TempDir::new().expect("tempdir");
|
||||
std::fs::create_dir_all(cwd.path().join(".agents")).expect("create .agents");
|
||||
std::fs::create_dir_all(cwd.path().join(".codex")).expect("create .codex");
|
||||
let cwd_absolute =
|
||||
AbsolutePathBuf::from_absolute_path(cwd.path()).expect("absolute tempdir");
|
||||
let secret = AbsolutePathBuf::resolve_path_against_base("secret", cwd.path())
|
||||
.expect("resolve unreadable path");
|
||||
let agents = AbsolutePathBuf::resolve_path_against_base(".agents", cwd.path())
|
||||
.expect("resolve .agents");
|
||||
let codex = AbsolutePathBuf::resolve_path_against_base(".codex", cwd.path())
|
||||
.expect("resolve .codex");
|
||||
let policy = FileSystemSandboxPolicy::restricted(vec![
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Special {
|
||||
value: FileSystemSpecialPath {
|
||||
kind: FileSystemSpecialPathKind::Minimal,
|
||||
subpath: None,
|
||||
},
|
||||
},
|
||||
access: FileSystemAccessMode::Read,
|
||||
},
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Special {
|
||||
value: FileSystemSpecialPath {
|
||||
kind: FileSystemSpecialPathKind::CurrentWorkingDirectory,
|
||||
subpath: None,
|
||||
},
|
||||
},
|
||||
access: FileSystemAccessMode::Write,
|
||||
},
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Special {
|
||||
value: FileSystemSpecialPath {
|
||||
kind: FileSystemSpecialPathKind::CurrentWorkingDirectory,
|
||||
subpath: Some(PathBuf::from("secret")),
|
||||
},
|
||||
},
|
||||
access: FileSystemAccessMode::None,
|
||||
},
|
||||
]);
|
||||
|
||||
assert!(!policy.has_full_disk_read_access());
|
||||
assert!(!policy.has_full_disk_write_access());
|
||||
assert!(policy.include_platform_defaults());
|
||||
assert_eq!(
|
||||
policy.get_readable_roots_with_cwd(cwd.path()),
|
||||
vec![cwd_absolute]
|
||||
);
|
||||
assert_eq!(
|
||||
policy.get_unreadable_roots_with_cwd(cwd.path()),
|
||||
vec![secret.clone()]
|
||||
);
|
||||
|
||||
let writable_roots = policy.get_writable_roots_with_cwd(cwd.path());
|
||||
assert_eq!(writable_roots.len(), 1);
|
||||
assert_eq!(writable_roots[0].root.as_path(), cwd.path());
|
||||
assert!(
|
||||
writable_roots[0]
|
||||
.read_only_subpaths
|
||||
.iter()
|
||||
.any(|path| path.as_path() == secret.as_path())
|
||||
);
|
||||
assert!(
|
||||
writable_roots[0]
|
||||
.read_only_subpaths
|
||||
.iter()
|
||||
.any(|path| path.as_path() == agents.as_path())
|
||||
);
|
||||
assert!(
|
||||
writable_roots[0]
|
||||
.read_only_subpaths
|
||||
.iter()
|
||||
.any(|path| path.as_path() == codex.as_path())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn file_system_policy_rejects_legacy_bridge_for_non_workspace_writes() {
|
||||
let cwd = if cfg!(windows) {
|
||||
Path::new(r"C:\workspace")
|
||||
} else {
|
||||
Path::new("/tmp/workspace")
|
||||
};
|
||||
let external_write_path = if cfg!(windows) {
|
||||
AbsolutePathBuf::from_absolute_path(r"C:\temp").expect("absolute windows temp path")
|
||||
} else {
|
||||
AbsolutePathBuf::from_absolute_path("/tmp").expect("absolute tmp path")
|
||||
};
|
||||
let policy = FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Path {
|
||||
path: external_write_path,
|
||||
},
|
||||
access: FileSystemAccessMode::Write,
|
||||
}]);
|
||||
|
||||
let err = policy
|
||||
.to_legacy_sandbox_policy(NetworkSandboxPolicy::Restricted, cwd)
|
||||
.expect_err("non-workspace writes should be rejected");
|
||||
|
||||
assert!(
|
||||
err.to_string()
|
||||
.contains("filesystem writes outside the workspace root"),
|
||||
"{err}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn item_started_event_from_web_search_emits_begin_event() {
|
||||
let event = ItemStartedEvent {
|
||||
|
||||
Reference in New Issue
Block a user