Compare commits

...

2 Commits

Author SHA1 Message Date
Dylan Hurd
3bf785b47d Add local network toggle for workspace-write sandbox 2026-01-20 00:17:15 -08:00
Dylan Hurd
8a73f26285 fix(core) allow loopback by default in sandbox 2026-01-19 11:53:12 -08:00
32 changed files with 121 additions and 1 deletions

View File

@@ -382,6 +382,7 @@ pub struct SandboxSettings {
#[serde(default)]
pub writable_roots: Vec<AbsolutePathBuf>,
pub network_access: Option<bool>,
pub local_network: Option<bool>,
pub exclude_tmpdir_env_var: Option<bool>,
pub exclude_slash_tmp: Option<bool>,
}

View File

@@ -309,6 +309,8 @@ pub struct SandboxWorkspaceWrite {
#[serde(default)]
pub network_access: bool,
#[serde(default)]
pub local_network: bool,
#[serde(default)]
pub exclude_tmpdir_env_var: bool,
#[serde(default)]
pub exclude_slash_tmp: bool,
@@ -571,6 +573,8 @@ pub enum SandboxPolicy {
#[serde(default)]
network_access: bool,
#[serde(default)]
local_network: bool,
#[serde(default)]
exclude_tmpdir_env_var: bool,
#[serde(default)]
exclude_slash_tmp: bool,
@@ -595,11 +599,13 @@ impl SandboxPolicy {
SandboxPolicy::WorkspaceWrite {
writable_roots,
network_access,
local_network,
exclude_tmpdir_env_var,
exclude_slash_tmp,
} => codex_protocol::protocol::SandboxPolicy::WorkspaceWrite {
writable_roots: writable_roots.clone(),
network_access: *network_access,
local_network: *local_network,
exclude_tmpdir_env_var: *exclude_tmpdir_env_var,
exclude_slash_tmp: *exclude_slash_tmp,
},
@@ -625,11 +631,13 @@ impl From<codex_protocol::protocol::SandboxPolicy> for SandboxPolicy {
codex_protocol::protocol::SandboxPolicy::WorkspaceWrite {
writable_roots,
network_access,
local_network,
exclude_tmpdir_env_var,
exclude_slash_tmp,
} => SandboxPolicy::WorkspaceWrite {
writable_roots,
network_access,
local_network,
exclude_tmpdir_env_var,
exclude_slash_tmp,
},

View File

@@ -415,6 +415,7 @@ async fn test_send_user_turn_updates_sandbox_and_cwd_between_turns() -> Result<(
sandbox_policy: SandboxPolicy::WorkspaceWrite {
writable_roots: vec![first_cwd.try_into()?],
network_access: false,
local_network: false,
exclude_tmpdir_env_var: false,
exclude_slash_tmp: false,
},

View File

@@ -88,6 +88,7 @@ async fn get_config_toml_parses_all_fields() -> Result<()> {
sandbox_settings: Some(SandboxSettings {
writable_roots: vec![writable_root],
network_access: Some(true),
local_network: Some(false),
exclude_tmpdir_env_var: Some(true),
exclude_slash_tmp: Some(true),
}),

View File

@@ -770,6 +770,7 @@ async fn turn_start_updates_sandbox_and_cwd_between_turns_v2() -> Result<()> {
sandbox_policy: Some(codex_app_server_protocol::SandboxPolicy::WorkspaceWrite {
writable_roots: vec![first_cwd.try_into()?],
network_access: false,
local_network: false,
exclude_tmpdir_env_var: false,
exclude_slash_tmp: false,
}),

View File

@@ -15,6 +15,7 @@ pub fn summarize_sandbox_policy(sandbox_policy: &SandboxPolicy) -> String {
SandboxPolicy::WorkspaceWrite {
writable_roots,
network_access,
local_network: _,
exclude_tmpdir_env_var,
exclude_slash_tmp,
} => {
@@ -72,6 +73,7 @@ mod tests {
let summary = summarize_sandbox_policy(&SandboxPolicy::WorkspaceWrite {
writable_roots: vec![writable_root.clone()],
network_access: true,
local_network: false,
exclude_tmpdir_env_var: true,
exclude_slash_tmp: true,
});

View File

@@ -1195,6 +1195,10 @@
"default": false,
"type": "boolean"
},
"local_network": {
"default": false,
"type": "boolean"
},
"network_access": {
"default": false,
"type": "boolean"

View File

@@ -1076,11 +1076,13 @@ impl ConfigToml {
Some(SandboxWorkspaceWrite {
writable_roots,
network_access,
local_network,
exclude_tmpdir_env_var,
exclude_slash_tmp,
}) => SandboxPolicy::WorkspaceWrite {
writable_roots: writable_roots.clone(),
network_access: *network_access,
local_network: *local_network,
exclude_tmpdir_env_var: *exclude_tmpdir_env_var,
exclude_slash_tmp: *exclude_slash_tmp,
},
@@ -1913,6 +1915,7 @@ exclude_slash_tmp = true
policy: SandboxPolicy::WorkspaceWrite {
writable_roots: vec![writable_root.clone()],
network_access: false,
local_network: false,
exclude_tmpdir_env_var: true,
exclude_slash_tmp: true,
},
@@ -1961,6 +1964,7 @@ trust_level = "trusted"
policy: SandboxPolicy::WorkspaceWrite {
writable_roots: vec![writable_root],
network_access: false,
local_network: false,
exclude_tmpdir_env_var: true,
exclude_slash_tmp: true,
},

View File

@@ -622,6 +622,8 @@ pub struct SandboxWorkspaceWrite {
#[serde(default)]
pub network_access: bool,
#[serde(default)]
pub local_network: bool,
#[serde(default)]
pub exclude_tmpdir_env_var: bool,
#[serde(default)]
pub exclude_slash_tmp: bool,
@@ -632,6 +634,7 @@ impl From<SandboxWorkspaceWrite> for codex_app_server_protocol::SandboxSettings
Self {
writable_roots: sandbox_workspace_write.writable_roots,
network_access: Some(sandbox_workspace_write.network_access),
local_network: Some(sandbox_workspace_write.local_network),
exclude_tmpdir_env_var: Some(sandbox_workspace_write.exclude_tmpdir_env_var),
exclude_slash_tmp: Some(sandbox_workspace_write.exclude_slash_tmp),
}

View File

@@ -524,6 +524,7 @@ mod tests {
.can_set(&SandboxPolicy::WorkspaceWrite {
writable_roots: vec![AbsolutePathBuf::from_absolute_path(root)?],
network_access: false,
local_network: false,
exclude_tmpdir_env_var: false,
exclude_slash_tmp: false,
})

View File

@@ -250,6 +250,7 @@ allowed_sandbox_modes = ["read-only"]
.can_set(&SandboxPolicy::WorkspaceWrite {
writable_roots: Vec::new(),
network_access: false,
local_network: false,
exclude_tmpdir_env_var: false,
exclude_slash_tmp: false,
})

View File

@@ -235,6 +235,7 @@ mod tests {
let policy_workspace_only = SandboxPolicy::WorkspaceWrite {
writable_roots: vec![],
network_access: false,
local_network: false,
exclude_tmpdir_env_var: true,
exclude_slash_tmp: true,
};
@@ -256,6 +257,7 @@ mod tests {
let policy_with_parent = SandboxPolicy::WorkspaceWrite {
writable_roots: vec![AbsolutePathBuf::try_from(parent).unwrap()],
network_access: false,
local_network: false,
exclude_tmpdir_env_var: true,
exclude_slash_tmp: true,
};

View File

@@ -12,6 +12,8 @@ use crate::spawn::StdioPolicy;
use crate::spawn::spawn_child_async;
const MACOS_SEATBELT_BASE_POLICY: &str = include_str!("seatbelt_base_policy.sbpl");
const MACOS_SEATBELT_LOCAL_NETWORK_POLICY: &str =
include_str!("seatbelt_local_network_policy.sbpl");
const MACOS_SEATBELT_NETWORK_POLICY: &str = include_str!("seatbelt_network_policy.sbpl");
/// When working with `sandbox-exec`, only consider `sandbox-exec` in `/usr/bin`
@@ -117,9 +119,17 @@ pub(crate) fn create_seatbelt_command_args(
} else {
""
};
let local_network_policy = match sandbox_policy {
SandboxPolicy::WorkspaceWrite {
local_network: true,
network_access: false,
..
} => MACOS_SEATBELT_LOCAL_NETWORK_POLICY,
_ => "",
};
let full_policy = format!(
"{MACOS_SEATBELT_BASE_POLICY}\n{file_read_policy}\n{file_write_policy}\n{network_policy}"
"{MACOS_SEATBELT_BASE_POLICY}\n{file_read_policy}\n{file_write_policy}\n{local_network_policy}\n{network_policy}"
);
let dir_params = [file_write_dir_params, macos_dir_params()].concat();
@@ -163,6 +173,7 @@ fn macos_dir_params() -> Vec<(String, PathBuf)> {
#[cfg(test)]
mod tests {
use super::MACOS_SEATBELT_BASE_POLICY;
use super::MACOS_SEATBELT_LOCAL_NETWORK_POLICY;
use super::create_seatbelt_command_args;
use super::macos_dir_params;
use crate::protocol::SandboxPolicy;
@@ -184,6 +195,42 @@ mod tests {
);
}
#[test]
fn seatbelt_policy_includes_local_network_when_enabled() {
let policy = SandboxPolicy::WorkspaceWrite {
writable_roots: vec![],
network_access: false,
local_network: true,
exclude_tmpdir_env_var: true,
exclude_slash_tmp: true,
};
let args = create_seatbelt_command_args(vec!["true".to_string()], &policy, Path::new("/"));
let policy_text = args.get(1).expect("policy").as_str();
assert_eq!(
policy_text.contains(MACOS_SEATBELT_LOCAL_NETWORK_POLICY),
true
);
}
#[test]
fn seatbelt_policy_skips_local_network_when_disabled() {
let policy = SandboxPolicy::WorkspaceWrite {
writable_roots: vec![],
network_access: false,
local_network: false,
exclude_tmpdir_env_var: true,
exclude_slash_tmp: true,
};
let args = create_seatbelt_command_args(vec!["true".to_string()], &policy, Path::new("/"));
let policy_text = args.get(1).expect("policy").as_str();
assert_eq!(
policy_text.contains(MACOS_SEATBELT_LOCAL_NETWORK_POLICY),
false
);
}
#[test]
fn create_seatbelt_args_with_read_only_git_and_codex_subpaths() {
// Create a temporary workspace with two writable roots: one containing
@@ -208,6 +255,7 @@ mod tests {
.map(|p| p.try_into().unwrap())
.collect(),
network_access: false,
local_network: false,
exclude_tmpdir_env_var: true,
exclude_slash_tmp: true,
};
@@ -392,6 +440,7 @@ mod tests {
let policy = SandboxPolicy::WorkspaceWrite {
writable_roots: vec![worktree_root.try_into().expect("worktree_root is absolute")],
network_access: false,
local_network: false,
exclude_tmpdir_env_var: true,
exclude_slash_tmp: true,
};
@@ -475,6 +524,7 @@ mod tests {
let policy = SandboxPolicy::WorkspaceWrite {
writable_roots: vec![],
network_access: false,
local_network: false,
exclude_tmpdir_env_var: false,
exclude_slash_tmp: false,
};

View File

@@ -0,0 +1,7 @@
; When local network access is enabled, allow loopback-only sockets for local servers/clients.
(allow network-bind
(local ip "localhost:*"))
(allow network-inbound
(local ip "localhost:*"))
(allow network-outbound
(remote ip "localhost:*"))

View File

@@ -1,6 +1,9 @@
; when network access is enabled, these policies are added after those in seatbelt_base_policy.sbpl
; Ref https://source.chromium.org/chromium/chromium/src/+/main:sandbox/policy/mac/network.sb;drc=f8f264d5e4e7509c913f4c60c2639d15905a07e4
(allow network-bind
(local ip "localhost:*"))
(allow network-outbound)
(allow network-inbound)
(allow system-socket)

View File

@@ -578,6 +578,7 @@ async fn apply_patch_cli_rejects_path_traversal_outside_workspace(
let sandbox_policy = SandboxPolicy::WorkspaceWrite {
writable_roots: vec![],
network_access: false,
local_network: false,
exclude_tmpdir_env_var: true,
exclude_slash_tmp: true,
};
@@ -634,6 +635,7 @@ async fn apply_patch_cli_rejects_move_path_traversal_outside_workspace(
let sandbox_policy = SandboxPolicy::WorkspaceWrite {
writable_roots: vec![],
network_access: false,
local_network: false,
exclude_tmpdir_env_var: true,
exclude_slash_tmp: true,
};

View File

@@ -628,6 +628,7 @@ fn scenarios() -> Vec<ScenarioSpec> {
let workspace_write = |network_access| SandboxPolicy::WorkspaceWrite {
writable_roots: vec![],
network_access,
local_network: false,
exclude_tmpdir_env_var: false,
exclude_slash_tmp: false,
};
@@ -1573,6 +1574,7 @@ async fn approving_apply_patch_for_session_skips_future_prompts_for_same_file()
let sandbox_policy = SandboxPolicy::WorkspaceWrite {
writable_roots: vec![],
network_access: false,
local_network: false,
exclude_tmpdir_env_var: false,
exclude_slash_tmp: false,
};

View File

@@ -394,6 +394,7 @@ async fn permissions_message_includes_writable_roots() -> Result<()> {
let sandbox_policy = SandboxPolicy::WorkspaceWrite {
writable_roots: vec![writable_root],
network_access: false,
local_network: false,
exclude_tmpdir_env_var: false,
exclude_slash_tmp: false,
};

View File

@@ -336,6 +336,7 @@ async fn overrides_turn_context_but_keeps_cached_prefix_and_key_constant() -> an
let new_policy = SandboxPolicy::WorkspaceWrite {
writable_roots: vec![writable.path().try_into().unwrap()],
network_access: true,
local_network: false,
exclude_tmpdir_env_var: true,
exclude_slash_tmp: true,
};
@@ -557,6 +558,7 @@ async fn per_turn_overrides_keep_cached_prefix_and_key_constant() -> anyhow::Res
let new_policy = SandboxPolicy::WorkspaceWrite {
writable_roots: vec![AbsolutePathBuf::try_from(writable.path()).unwrap()],
network_access: true,
local_network: false,
exclude_tmpdir_env_var: true,
exclude_slash_tmp: true,
};

View File

@@ -78,6 +78,7 @@ async fn if_parent_of_repo_is_writable_then_dot_git_folder_is_writable() {
let policy = SandboxPolicy::WorkspaceWrite {
writable_roots: vec![test_scenario.repo_parent.as_path().try_into().unwrap()],
network_access: false,
local_network: false,
exclude_tmpdir_env_var: true,
exclude_slash_tmp: true,
};
@@ -104,6 +105,7 @@ async fn if_git_repo_is_writable_root_then_dot_git_folder_is_read_only() {
let policy = SandboxPolicy::WorkspaceWrite {
writable_roots: vec![test_scenario.repo_root.as_path().try_into().unwrap()],
network_access: false,
local_network: false,
exclude_tmpdir_env_var: true,
exclude_slash_tmp: true,
};

View File

@@ -110,6 +110,7 @@ where
// when the sandbox policy is expanded.
writable_roots: vec![],
network_access: false,
local_network: false,
// Disable writes to temp dir because this is a test, so
// writable_folder is likely also under /tmp and we want to be
// strict about what is writable.

View File

@@ -72,6 +72,7 @@ async fn python_multiprocessing_lock_works_under_sandbox() {
let policy = SandboxPolicy::WorkspaceWrite {
writable_roots,
network_access: false,
local_network: false,
exclude_tmpdir_env_var: false,
exclude_slash_tmp: false,
};
@@ -171,6 +172,7 @@ async fn sandbox_distinguishes_command_and_policy_cwds() {
let policy = SandboxPolicy::WorkspaceWrite {
writable_roots: vec![],
network_access: false,
local_network: false,
exclude_tmpdir_env_var: true,
exclude_slash_tmp: true,
};

View File

@@ -70,6 +70,7 @@ async fn run_cmd_output(
.map(|p| AbsolutePathBuf::try_from(p.as_path()).unwrap())
.collect(),
network_access: false,
local_network: false,
// Exclude tmp-related folders from writable roots because we need a
// folder that is writable by tests but that we intentionally disallow
// writing to in the sandbox.

View File

@@ -835,6 +835,7 @@ mod tests {
let policy = SandboxPolicy::WorkspaceWrite {
writable_roots: vec![],
network_access: true,
local_network: false,
exclude_tmpdir_env_var: false,
exclude_slash_tmp: false,
};

View File

@@ -353,6 +353,11 @@ pub enum SandboxPolicy {
#[serde(default)]
network_access: bool,
/// When set to `true`, loopback-only local network access is allowed.
/// `false` by default.
#[serde(default)]
local_network: bool,
/// When set to `true`, will NOT include the per-user `TMPDIR`
/// environment variable among the default writable roots. Defaults to
/// `false`.
@@ -418,6 +423,7 @@ impl SandboxPolicy {
SandboxPolicy::WorkspaceWrite {
writable_roots: vec![],
network_access: false,
local_network: false,
exclude_tmpdir_env_var: false,
exclude_slash_tmp: false,
}
@@ -458,6 +464,7 @@ impl SandboxPolicy {
writable_roots,
exclude_tmpdir_env_var,
exclude_slash_tmp,
local_network: _,
network_access: _,
} => {
// Start from explicitly configured writable roots.

View File

@@ -2140,6 +2140,7 @@ async fn preset_matching_ignores_extra_writable_roots() {
let current_sandbox = SandboxPolicy::WorkspaceWrite {
writable_roots: vec![AbsolutePathBuf::try_from("C:\\extra").unwrap()],
network_access: false,
local_network: false,
exclude_tmpdir_env_var: false,
exclude_slash_tmp: false,
};

View File

@@ -102,6 +102,7 @@ async fn status_snapshot_includes_reasoning_details() {
.set(SandboxPolicy::WorkspaceWrite {
writable_roots: Vec::new(),
network_access: false,
local_network: false,
exclude_tmpdir_env_var: false,
exclude_slash_tmp: false,
})

View File

@@ -1880,6 +1880,7 @@ async fn preset_matching_ignores_extra_writable_roots() {
let current_sandbox = SandboxPolicy::WorkspaceWrite {
writable_roots: vec![AbsolutePathBuf::try_from("C:\\extra").unwrap()],
network_access: false,
local_network: false,
exclude_tmpdir_env_var: false,
exclude_slash_tmp: false,
};

View File

@@ -102,6 +102,7 @@ async fn status_snapshot_includes_reasoning_details() {
.set(SandboxPolicy::WorkspaceWrite {
writable_roots: Vec::new(),
network_access: false,
local_network: false,
exclude_tmpdir_env_var: false,
exclude_slash_tmp: false,
})

View File

@@ -109,6 +109,7 @@ mod tests {
let policy = SandboxPolicy::WorkspaceWrite {
writable_roots: vec![AbsolutePathBuf::try_from(extra_root.as_path()).unwrap()],
network_access: false,
local_network: false,
exclude_tmpdir_env_var: false,
exclude_slash_tmp: false,
};
@@ -135,6 +136,7 @@ mod tests {
let policy = SandboxPolicy::WorkspaceWrite {
writable_roots: vec![],
network_access: false,
local_network: false,
exclude_tmpdir_env_var: true,
exclude_slash_tmp: false,
};
@@ -162,6 +164,7 @@ mod tests {
let policy = SandboxPolicy::WorkspaceWrite {
writable_roots: vec![],
network_access: false,
local_network: false,
exclude_tmpdir_env_var: true,
exclude_slash_tmp: false,
};
@@ -187,6 +190,7 @@ mod tests {
let policy = SandboxPolicy::WorkspaceWrite {
writable_roots: vec![],
network_access: false,
local_network: false,
exclude_tmpdir_env_var: true,
exclude_slash_tmp: false,
};

View File

@@ -467,6 +467,7 @@ mod windows_impl {
SandboxPolicy::WorkspaceWrite {
writable_roots: Vec::new(),
network_access,
local_network: false,
exclude_tmpdir_env_var: false,
exclude_slash_tmp: false,
}

View File

@@ -442,6 +442,7 @@ mod windows_impl {
SandboxPolicy::WorkspaceWrite {
writable_roots: Vec::new(),
network_access,
local_network: false,
exclude_tmpdir_env_var: false,
exclude_slash_tmp: false,
}