This commit is contained in:
celia-oai
2026-02-18 16:33:56 -08:00
parent c622cbef35
commit 13ec48ccb7
5 changed files with 400 additions and 233 deletions
+2
View File
@@ -5,6 +5,8 @@ Build platform wrappers and produce ExecRequest for execution. Owns low-level
sandbox placement and transformation of portable CommandSpec into a
ready‑to‑spawn environment.
*/
mod policy_merge;
pub(crate) use policy_merge::extend_sandbox_policy;
use crate::exec::ExecExpiration;
use crate::exec::ExecToolCallOutput;
@@ -0,0 +1,396 @@
use std::collections::HashSet;
use codex_utils_absolute_path::AbsolutePathBuf;
use crate::protocol::NetworkAccess;
use crate::protocol::ReadOnlyAccess;
use crate::protocol::SandboxPolicy;
pub(crate) fn extend_sandbox_policy(
base: &SandboxPolicy,
extension: &SandboxPolicy,
) -> SandboxPolicy {
// Merge by intersection of capabilities: the combined policy must satisfy
// restrictions from both `base` and `extension`.
match (base, extension) {
(SandboxPolicy::DangerFullAccess, other) | (other, SandboxPolicy::DangerFullAccess) => {
other.clone()
}
(
SandboxPolicy::ExternalSandbox {
network_access: base_network,
},
SandboxPolicy::ExternalSandbox {
network_access: extension_network,
},
) => SandboxPolicy::ExternalSandbox {
network_access: restrict_network_access(*base_network, *extension_network),
},
(SandboxPolicy::ExternalSandbox { .. }, SandboxPolicy::ReadOnly { access })
| (SandboxPolicy::ReadOnly { access }, SandboxPolicy::ExternalSandbox { .. }) => {
SandboxPolicy::ReadOnly {
access: access.clone(),
}
}
(
SandboxPolicy::ExternalSandbox {
network_access: external_network_access,
},
SandboxPolicy::WorkspaceWrite {
writable_roots,
read_only_access,
network_access,
exclude_tmpdir_env_var,
exclude_slash_tmp,
},
)
| (
SandboxPolicy::WorkspaceWrite {
writable_roots,
read_only_access,
network_access,
exclude_tmpdir_env_var,
exclude_slash_tmp,
},
SandboxPolicy::ExternalSandbox {
network_access: external_network_access,
},
) => SandboxPolicy::WorkspaceWrite {
writable_roots: writable_roots.clone(),
read_only_access: read_only_access.clone(),
network_access: *network_access && external_network_access.is_enabled(),
exclude_tmpdir_env_var: *exclude_tmpdir_env_var,
exclude_slash_tmp: *exclude_slash_tmp,
},
(
SandboxPolicy::ReadOnly {
access: base_access,
},
SandboxPolicy::ReadOnly {
access: extension_access,
},
) => SandboxPolicy::ReadOnly {
access: intersect_read_only_access(base_access, extension_access),
},
(
SandboxPolicy::ReadOnly {
access: base_access,
},
SandboxPolicy::WorkspaceWrite {
read_only_access, ..
},
) => SandboxPolicy::ReadOnly {
access: intersect_read_only_access(base_access, read_only_access),
},
(
SandboxPolicy::WorkspaceWrite {
read_only_access, ..
},
SandboxPolicy::ReadOnly {
access: extension_access,
},
) => SandboxPolicy::ReadOnly {
access: intersect_read_only_access(read_only_access, extension_access),
},
(
SandboxPolicy::WorkspaceWrite {
writable_roots: base_writable_roots,
read_only_access: base_read_only_access,
network_access: base_network_access,
exclude_tmpdir_env_var: base_exclude_tmpdir_env_var,
exclude_slash_tmp: base_exclude_slash_tmp,
},
SandboxPolicy::WorkspaceWrite {
writable_roots: extension_writable_roots,
read_only_access: extension_read_only_access,
network_access: extension_network_access,
exclude_tmpdir_env_var: extension_exclude_tmpdir_env_var,
exclude_slash_tmp: extension_exclude_slash_tmp,
},
) => SandboxPolicy::WorkspaceWrite {
writable_roots: intersect_absolute_roots(base_writable_roots, extension_writable_roots),
read_only_access: intersect_read_only_access(
base_read_only_access,
extension_read_only_access,
),
network_access: *base_network_access && *extension_network_access,
exclude_tmpdir_env_var: *base_exclude_tmpdir_env_var
|| *extension_exclude_tmpdir_env_var,
exclude_slash_tmp: *base_exclude_slash_tmp || *extension_exclude_slash_tmp,
},
}
}
fn restrict_network_access(base: NetworkAccess, extension: NetworkAccess) -> NetworkAccess {
if base.is_enabled() && extension.is_enabled() {
NetworkAccess::Enabled
} else {
NetworkAccess::Restricted
}
}
fn intersect_read_only_access(base: &ReadOnlyAccess, extension: &ReadOnlyAccess) -> ReadOnlyAccess {
match (base, extension) {
(ReadOnlyAccess::FullAccess, access) | (access, ReadOnlyAccess::FullAccess) => {
access.clone()
}
(
ReadOnlyAccess::Restricted {
include_platform_defaults: base_include_platform_defaults,
readable_roots: base_readable_roots,
},
ReadOnlyAccess::Restricted {
include_platform_defaults: extension_include_platform_defaults,
readable_roots: extension_readable_roots,
},
) => ReadOnlyAccess::Restricted {
include_platform_defaults: *base_include_platform_defaults
&& *extension_include_platform_defaults,
readable_roots: intersect_absolute_roots(base_readable_roots, extension_readable_roots),
},
}
}
fn intersect_absolute_roots(
base_roots: &[AbsolutePathBuf],
extension_roots: &[AbsolutePathBuf],
) -> Vec<AbsolutePathBuf> {
let extension_roots_set: HashSet<_> = extension_roots
.iter()
.map(AbsolutePathBuf::to_path_buf)
.collect();
let mut roots = Vec::new();
let mut seen = HashSet::new();
for root in base_roots {
let root_path = root.to_path_buf();
if extension_roots_set.contains(&root_path) && seen.insert(root_path) {
roots.push(root.clone());
}
}
roots
}
#[cfg(test)]
mod tests {
use super::extend_sandbox_policy;
use crate::protocol::NetworkAccess;
use crate::protocol::ReadOnlyAccess;
use crate::protocol::SandboxPolicy;
use codex_utils_absolute_path::AbsolutePathBuf;
use pretty_assertions::assert_eq;
#[test]
fn extend_sandbox_policy_combines_read_only_and_workspace_write_as_read_only() {
let tempdir = tempfile::tempdir().expect("tempdir");
let base_read_root =
AbsolutePathBuf::try_from(tempdir.path().join("base-read")).expect("absolute path");
let merged = extend_sandbox_policy(
&SandboxPolicy::ReadOnly {
access: ReadOnlyAccess::Restricted {
include_platform_defaults: false,
readable_roots: vec![base_read_root],
},
},
&SandboxPolicy::WorkspaceWrite {
writable_roots: vec![],
read_only_access: ReadOnlyAccess::Restricted {
include_platform_defaults: true,
readable_roots: Vec::new(),
},
network_access: true,
exclude_tmpdir_env_var: false,
exclude_slash_tmp: false,
},
);
assert_eq!(
merged,
SandboxPolicy::ReadOnly {
access: ReadOnlyAccess::Restricted {
include_platform_defaults: false,
readable_roots: Vec::new(),
},
}
);
}
#[test]
fn extend_sandbox_policy_uses_extension_when_base_is_danger_full_access() {
let tempdir = tempfile::tempdir().expect("tempdir");
let extension_root =
AbsolutePathBuf::try_from(tempdir.path().join("extension")).expect("absolute path");
let merged = extend_sandbox_policy(
&SandboxPolicy::DangerFullAccess,
&SandboxPolicy::WorkspaceWrite {
writable_roots: vec![extension_root.clone()],
read_only_access: ReadOnlyAccess::FullAccess,
network_access: false,
exclude_tmpdir_env_var: false,
exclude_slash_tmp: true,
},
);
assert_eq!(
merged,
SandboxPolicy::WorkspaceWrite {
writable_roots: vec![extension_root],
read_only_access: ReadOnlyAccess::FullAccess,
network_access: false,
exclude_tmpdir_env_var: false,
exclude_slash_tmp: true,
}
);
}
#[test]
fn extend_sandbox_policy_external_and_workspace_write_keeps_workspace_write_restrictions() {
let tempdir = tempfile::tempdir().expect("tempdir");
let workspace_root =
AbsolutePathBuf::try_from(tempdir.path().join("workspace")).expect("absolute path");
let read_root = AbsolutePathBuf::try_from(tempdir.path().join("read")).expect("absolute");
let merged = extend_sandbox_policy(
&SandboxPolicy::ExternalSandbox {
network_access: NetworkAccess::Restricted,
},
&SandboxPolicy::WorkspaceWrite {
writable_roots: vec![workspace_root.clone()],
read_only_access: ReadOnlyAccess::Restricted {
include_platform_defaults: true,
readable_roots: vec![read_root.clone()],
},
network_access: true,
exclude_tmpdir_env_var: false,
exclude_slash_tmp: false,
},
);
assert_eq!(
merged,
SandboxPolicy::WorkspaceWrite {
writable_roots: vec![workspace_root],
read_only_access: ReadOnlyAccess::Restricted {
include_platform_defaults: true,
readable_roots: vec![read_root],
},
network_access: false,
exclude_tmpdir_env_var: false,
exclude_slash_tmp: false,
}
);
}
#[test]
fn extend_sandbox_policy_external_and_read_only_returns_read_only() {
let tempdir = tempfile::tempdir().expect("tempdir");
let read_root = AbsolutePathBuf::try_from(tempdir.path().join("read")).expect("absolute");
let merged = extend_sandbox_policy(
&SandboxPolicy::ExternalSandbox {
network_access: NetworkAccess::Enabled,
},
&SandboxPolicy::ReadOnly {
access: ReadOnlyAccess::Restricted {
include_platform_defaults: true,
readable_roots: vec![read_root.clone()],
},
},
);
assert_eq!(
merged,
SandboxPolicy::ReadOnly {
access: ReadOnlyAccess::Restricted {
include_platform_defaults: true,
readable_roots: vec![read_root],
},
}
);
}
#[test]
fn extend_sandbox_policy_intersects_workspace_roots_and_restricts_network_access() {
let tempdir = tempfile::tempdir().expect("tempdir");
let shared_root =
AbsolutePathBuf::try_from(tempdir.path().join("shared")).expect("absolute path");
let base_root =
AbsolutePathBuf::try_from(tempdir.path().join("base")).expect("absolute path");
let extension_root =
AbsolutePathBuf::try_from(tempdir.path().join("extension")).expect("absolute path");
let merged = extend_sandbox_policy(
&SandboxPolicy::WorkspaceWrite {
writable_roots: vec![shared_root.clone(), base_root],
read_only_access: ReadOnlyAccess::FullAccess,
network_access: false,
exclude_tmpdir_env_var: true,
exclude_slash_tmp: true,
},
&SandboxPolicy::WorkspaceWrite {
writable_roots: vec![shared_root.clone(), extension_root.clone()],
read_only_access: ReadOnlyAccess::Restricted {
include_platform_defaults: false,
readable_roots: vec![extension_root.clone()],
},
network_access: true,
exclude_tmpdir_env_var: false,
exclude_slash_tmp: false,
},
);
assert_eq!(
merged,
SandboxPolicy::WorkspaceWrite {
writable_roots: vec![shared_root],
read_only_access: ReadOnlyAccess::Restricted {
include_platform_defaults: false,
readable_roots: vec![extension_root],
},
network_access: false,
exclude_tmpdir_env_var: true,
exclude_slash_tmp: true,
}
);
}
#[test]
fn extend_sandbox_policy_keeps_network_access_enabled_only_when_both_policies_enable_it() {
let tempdir = tempfile::tempdir().expect("tempdir");
let shared_root =
AbsolutePathBuf::try_from(tempdir.path().join("shared")).expect("absolute path");
let base_root =
AbsolutePathBuf::try_from(tempdir.path().join("base")).expect("absolute path");
let extension_root =
AbsolutePathBuf::try_from(tempdir.path().join("extension")).expect("absolute path");
let merged = extend_sandbox_policy(
&SandboxPolicy::WorkspaceWrite {
writable_roots: vec![base_root, shared_root.clone()],
read_only_access: ReadOnlyAccess::FullAccess,
network_access: true,
exclude_tmpdir_env_var: true,
exclude_slash_tmp: true,
},
&SandboxPolicy::WorkspaceWrite {
writable_roots: vec![shared_root.clone(), extension_root],
read_only_access: ReadOnlyAccess::FullAccess,
network_access: true,
exclude_tmpdir_env_var: true,
exclude_slash_tmp: true,
},
);
assert_eq!(
merged,
SandboxPolicy::WorkspaceWrite {
writable_roots: vec![shared_root],
read_only_access: ReadOnlyAccess::FullAccess,
network_access: true,
exclude_tmpdir_env_var: true,
exclude_slash_tmp: true,
}
);
}
}
-231
View File
@@ -13,7 +13,6 @@ use crate::config::Constrained;
use crate::config::Permissions;
use crate::config::types::ShellEnvironmentPolicy;
use crate::protocol::AskForApproval;
use crate::protocol::NetworkAccess;
use crate::protocol::ReadOnlyAccess;
use crate::protocol::SandboxPolicy;
#[cfg(target_os = "macos")]
@@ -119,150 +118,6 @@ pub(crate) fn compile_permission_profile(
})
}
pub(crate) fn extend_sandbox_policy(
base: &SandboxPolicy,
extension: &SandboxPolicy,
) -> SandboxPolicy {
match (base, extension) {
(SandboxPolicy::DangerFullAccess, _) | (_, SandboxPolicy::DangerFullAccess) => {
SandboxPolicy::DangerFullAccess
}
(
SandboxPolicy::ExternalSandbox {
network_access: base_network,
},
SandboxPolicy::ExternalSandbox {
network_access: extension_network,
},
) => SandboxPolicy::ExternalSandbox {
network_access: if base_network.is_enabled() || extension_network.is_enabled() {
NetworkAccess::Enabled
} else {
NetworkAccess::Restricted
},
},
(SandboxPolicy::ExternalSandbox { .. }, _) | (_, SandboxPolicy::ExternalSandbox { .. }) => {
SandboxPolicy::ExternalSandbox {
network_access: if base.has_full_network_access()
|| extension.has_full_network_access()
{
NetworkAccess::Enabled
} else {
NetworkAccess::Restricted
},
}
}
(
SandboxPolicy::ReadOnly {
access: base_access,
},
SandboxPolicy::ReadOnly {
access: extension_access,
},
) => SandboxPolicy::ReadOnly {
access: extend_read_only_access(base_access, extension_access),
},
(
SandboxPolicy::ReadOnly {
access: base_access,
},
SandboxPolicy::WorkspaceWrite {
writable_roots,
read_only_access,
network_access,
exclude_tmpdir_env_var,
exclude_slash_tmp,
},
) => SandboxPolicy::WorkspaceWrite {
writable_roots: writable_roots.clone(),
read_only_access: extend_read_only_access(base_access, read_only_access),
network_access: *network_access,
exclude_tmpdir_env_var: *exclude_tmpdir_env_var,
exclude_slash_tmp: *exclude_slash_tmp,
},
(
SandboxPolicy::WorkspaceWrite {
writable_roots,
read_only_access,
network_access,
exclude_tmpdir_env_var,
exclude_slash_tmp,
},
SandboxPolicy::ReadOnly {
access: extension_access,
},
) => SandboxPolicy::WorkspaceWrite {
writable_roots: writable_roots.clone(),
read_only_access: extend_read_only_access(read_only_access, extension_access),
network_access: *network_access,
exclude_tmpdir_env_var: *exclude_tmpdir_env_var,
exclude_slash_tmp: *exclude_slash_tmp,
},
(
SandboxPolicy::WorkspaceWrite {
writable_roots: base_writable_roots,
read_only_access: base_read_only_access,
network_access: base_network_access,
exclude_tmpdir_env_var: base_exclude_tmpdir_env_var,
exclude_slash_tmp: base_exclude_slash_tmp,
},
SandboxPolicy::WorkspaceWrite {
writable_roots: extension_writable_roots,
read_only_access: extension_read_only_access,
network_access: extension_network_access,
exclude_tmpdir_env_var: extension_exclude_tmpdir_env_var,
exclude_slash_tmp: extension_exclude_slash_tmp,
},
) => SandboxPolicy::WorkspaceWrite {
writable_roots: extend_absolute_roots(base_writable_roots, extension_writable_roots),
read_only_access: extend_read_only_access(
base_read_only_access,
extension_read_only_access,
),
network_access: *base_network_access || *extension_network_access,
exclude_tmpdir_env_var: *base_exclude_tmpdir_env_var
&& *extension_exclude_tmpdir_env_var,
exclude_slash_tmp: *base_exclude_slash_tmp && *extension_exclude_slash_tmp,
},
}
}
fn extend_read_only_access(base: &ReadOnlyAccess, extension: &ReadOnlyAccess) -> ReadOnlyAccess {
match (base, extension) {
(ReadOnlyAccess::FullAccess, _) | (_, ReadOnlyAccess::FullAccess) => {
ReadOnlyAccess::FullAccess
}
(
ReadOnlyAccess::Restricted {
include_platform_defaults: base_include_platform_defaults,
readable_roots: base_readable_roots,
},
ReadOnlyAccess::Restricted {
include_platform_defaults: extension_include_platform_defaults,
readable_roots: extension_readable_roots,
},
) => ReadOnlyAccess::Restricted {
include_platform_defaults: *base_include_platform_defaults
|| *extension_include_platform_defaults,
readable_roots: extend_absolute_roots(base_readable_roots, extension_readable_roots),
},
}
}
fn extend_absolute_roots(
base_roots: &[AbsolutePathBuf],
extension_roots: &[AbsolutePathBuf],
) -> Vec<AbsolutePathBuf> {
let mut roots = Vec::new();
let mut seen = HashSet::new();
for root in base_roots.iter().chain(extension_roots) {
if seen.insert(root.to_path_buf()) {
roots.push(root.clone());
}
}
roots
}
fn normalize_permission_paths(
skill_dir: &Path,
values: &[String],
@@ -434,7 +289,6 @@ mod tests {
use super::SkillManifestMacOsPermissions;
use super::SkillManifestPermissions;
use super::compile_permission_profile;
use super::extend_sandbox_policy;
use crate::config::Constrained;
use crate::config::Permissions;
use crate::config::types::ShellEnvironmentPolicy;
@@ -597,91 +451,6 @@ mod tests {
);
}
#[test]
fn extend_sandbox_policy_merges_read_only_into_workspace_write() {
let tempdir = tempfile::tempdir().expect("tempdir");
let base_read_root =
AbsolutePathBuf::try_from(tempdir.path().join("base-read")).expect("absolute path");
let extension_write_root =
AbsolutePathBuf::try_from(tempdir.path().join("extension-write"))
.expect("absolute path");
let merged = extend_sandbox_policy(
&SandboxPolicy::ReadOnly {
access: ReadOnlyAccess::Restricted {
include_platform_defaults: false,
readable_roots: vec![base_read_root.clone()],
},
},
&SandboxPolicy::WorkspaceWrite {
writable_roots: vec![extension_write_root.clone()],
read_only_access: ReadOnlyAccess::Restricted {
include_platform_defaults: true,
readable_roots: Vec::new(),
},
network_access: true,
exclude_tmpdir_env_var: false,
exclude_slash_tmp: false,
},
);
assert_eq!(
merged,
SandboxPolicy::WorkspaceWrite {
writable_roots: vec![extension_write_root],
read_only_access: ReadOnlyAccess::Restricted {
include_platform_defaults: true,
readable_roots: vec![base_read_root],
},
network_access: true,
exclude_tmpdir_env_var: false,
exclude_slash_tmp: false,
}
);
}
#[test]
fn extend_sandbox_policy_unions_workspace_roots_and_network_access() {
let tempdir = tempfile::tempdir().expect("tempdir");
let shared_root =
AbsolutePathBuf::try_from(tempdir.path().join("shared")).expect("absolute path");
let base_root =
AbsolutePathBuf::try_from(tempdir.path().join("base")).expect("absolute path");
let extension_root =
AbsolutePathBuf::try_from(tempdir.path().join("extension")).expect("absolute path");
let merged = extend_sandbox_policy(
&SandboxPolicy::WorkspaceWrite {
writable_roots: vec![shared_root.clone(), base_root.clone()],
read_only_access: ReadOnlyAccess::FullAccess,
network_access: false,
exclude_tmpdir_env_var: true,
exclude_slash_tmp: true,
},
&SandboxPolicy::WorkspaceWrite {
writable_roots: vec![shared_root.clone(), extension_root.clone()],
read_only_access: ReadOnlyAccess::Restricted {
include_platform_defaults: false,
readable_roots: Vec::new(),
},
network_access: true,
exclude_tmpdir_env_var: false,
exclude_slash_tmp: false,
},
);
assert_eq!(
merged,
SandboxPolicy::WorkspaceWrite {
writable_roots: vec![shared_root, base_root, extension_root],
read_only_access: ReadOnlyAccess::FullAccess,
network_access: true,
exclude_tmpdir_env_var: false,
exclude_slash_tmp: false,
}
);
}
#[cfg(target_os = "macos")]
#[test]
fn compile_permission_profile_builds_macos_permission_file() {
+1 -1
View File
@@ -12,8 +12,8 @@ use crate::exec_policy::ExecApprovalRequest;
use crate::function_tool::FunctionCallError;
use crate::is_safe_command::is_known_safe_command;
use crate::protocol::ExecCommandSource;
use crate::sandboxing::extend_sandbox_policy;
use crate::shell::Shell;
use crate::skills::permissions::extend_sandbox_policy;
use crate::skills::resolve_skill_sandbox_extension_for_command;
use crate::tools::context::ToolInvocation;
use crate::tools::context::ToolOutput;
@@ -16,7 +16,7 @@ use crate::exec_env::create_env;
use crate::exec_policy::ExecApprovalRequest;
use crate::protocol::ExecCommandSource;
use crate::sandboxing::ExecRequest;
use crate::skills::permissions::extend_sandbox_policy;
use crate::sandboxing::extend_sandbox_policy;
use crate::skills::resolve_skill_sandbox_extension_for_command;
use crate::tools::events::ToolEmitter;
use crate::tools::events::ToolEventCtx;