Compare commits

...

16 Commits

Author SHA1 Message Date
viyatb-oai
1e6e8c84c6 fix(protocol): preserve legacy workspace-write semantics 2026-03-08 01:22:00 -08:00
viyatb-oai
5cc63a7535 docs(linux-sandbox): clarify bwrap carveout masking 2026-03-07 23:09:26 -08:00
viyatb-oai
3830ce989a fix(linux-sandbox): remove stale lint expectation 2026-03-07 22:43:45 -08:00
viyatb-oai
c978612cbc fix(linux-sandbox): remove unused import 2026-03-07 22:38:11 -08:00
viyatb-oai
47a613c37e test(linux-sandbox): fix bwrap mount order expectation 2026-03-07 22:29:51 -08:00
viyatb-oai
e7eccd035a fix(linux-sandbox): enforce root read carveouts in bwrap 2026-03-07 22:14:56 -08:00
viyatb-oai
aec2a01f7a fix(linux-sandbox): update bwrap split policy tests 2026-03-07 21:42:44 -08:00
viyatb-oai
133fde3869 chore: merge origin/main into pr13453 2026-03-07 21:24:24 -08:00
Michael Bolin
de146fcdaa linux-sandbox: honor split filesystem policies in bwrap 2026-03-06 16:24:56 -08:00
Michael Bolin
3f86dc5d6e protocol: keep root carveouts sandboxed 2026-03-06 16:24:56 -08:00
Michael Bolin
c00ff8b8ed sandboxing: preserve denied paths when widening permissions 2026-03-06 16:24:56 -08:00
Michael Bolin
30aae07ce3 linux-sandbox: plumb split sandbox policies through helper 2026-03-06 16:23:59 -08:00
Michael Bolin
e24e36ec77 seatbelt: honor split filesystem sandbox policies 2026-03-06 16:23:59 -08:00
Michael Bolin
fd4299609a safety: honor filesystem policy carveouts in apply_patch 2026-03-06 16:23:58 -08:00
Michael Bolin
8a70b181dc protocol: derive effective file access from filesystem policies 2026-03-06 16:23:58 -08:00
Michael Bolin
a065a40c38 sandboxing: plumb split sandbox policies through runtime 2026-03-06 16:23:58 -08:00
12 changed files with 704 additions and 113 deletions

View File

@@ -1644,7 +1644,7 @@ impl CodexMessageProcessor {
};
let exec_params = ExecParams {
command,
cwd,
cwd: cwd.clone(),
expiration,
env,
network: started_network_proxy
@@ -1665,7 +1665,7 @@ impl CodexMessageProcessor {
Some(policy) => match self.config.permissions.sandbox_policy.can_set(&policy) {
Ok(()) => {
let file_system_sandbox_policy =
codex_protocol::permissions::FileSystemSandboxPolicy::from(&policy);
codex_protocol::permissions::FileSystemSandboxPolicy::from_legacy_sandbox_policy(&policy, &cwd);
let network_sandbox_policy =
codex_protocol::permissions::NetworkSandboxPolicy::from(&policy);
(policy, file_system_sandbox_policy, network_sandbox_policy)

View File

@@ -960,18 +960,26 @@ impl SessionConfiguration {
if let Some(approval_policy) = updates.approval_policy {
next_configuration.approval_policy.set(approval_policy)?;
}
let mut sandbox_or_cwd_changed = false;
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());
sandbox_or_cwd_changed = true;
}
if let Some(windows_sandbox_level) = updates.windows_sandbox_level {
next_configuration.windows_sandbox_level = windows_sandbox_level;
}
if let Some(cwd) = updates.cwd.clone() {
next_configuration.cwd = cwd;
sandbox_or_cwd_changed = true;
}
if sandbox_or_cwd_changed {
next_configuration.file_system_sandbox_policy =
FileSystemSandboxPolicy::from_legacy_sandbox_policy(
next_configuration.sandbox_policy.get(),
&next_configuration.cwd,
);
}
if let Some(app_server_client_name) = updates.app_server_client_name.clone() {
next_configuration.app_server_client_name = Some(app_server_client_name);

View File

@@ -2004,7 +2004,8 @@ impl Config {
}
}
}
let file_system_sandbox_policy = FileSystemSandboxPolicy::from(&sandbox_policy);
let file_system_sandbox_policy =
FileSystemSandboxPolicy::from_legacy_sandbox_policy(&sandbox_policy, &resolved_cwd);
let network_sandbox_policy = NetworkSandboxPolicy::from(&sandbox_policy);
(
configured_network_proxy_config,
@@ -2330,7 +2331,10 @@ impl Config {
if effective_sandbox_policy == original_sandbox_policy {
file_system_sandbox_policy
} else {
FileSystemSandboxPolicy::from(&effective_sandbox_policy)
FileSystemSandboxPolicy::from_legacy_sandbox_policy(
&effective_sandbox_policy,
&resolved_cwd,
)
};
let effective_network_sandbox_policy =
if effective_sandbox_policy == original_sandbox_policy {

View File

@@ -33,7 +33,8 @@ pub async fn spawn_command_under_linux_sandbox<P>(
where
P: AsRef<Path>,
{
let file_system_sandbox_policy = FileSystemSandboxPolicy::from(sandbox_policy);
let file_system_sandbox_policy =
FileSystemSandboxPolicy::from_legacy_sandbox_policy(sandbox_policy, sandbox_policy_cwd);
let network_sandbox_policy = NetworkSandboxPolicy::from(sandbox_policy);
let args = create_linux_sandbox_command_args_for_policies(
command,

View File

@@ -204,6 +204,7 @@ mod tests {
use codex_protocol::protocol::FileSystemSpecialPath;
use codex_protocol::protocol::RejectConfig;
use codex_utils_absolute_path::AbsolutePathBuf;
use pretty_assertions::assert_eq;
use tempfile::TempDir;
#[test]
@@ -403,4 +404,47 @@ mod tests {
SafetyCheck::AskUser,
);
}
#[test]
fn explicit_read_only_subpaths_prevent_auto_approval_for_external_sandbox() {
let tmp = TempDir::new().unwrap();
let cwd = tmp.path().to_path_buf();
let blocked_path = cwd.join("docs").join("blocked.txt");
let docs_absolute = AbsolutePathBuf::resolve_path_against_base("docs", &cwd).unwrap();
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::CurrentWorkingDirectory,
},
access: FileSystemAccessMode::Write,
},
FileSystemSandboxEntry {
path: FileSystemPath::Path {
path: docs_absolute,
},
access: FileSystemAccessMode::Read,
},
]);
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,
);
}
}

View File

@@ -10,12 +10,14 @@
//! - seccomp + `PR_SET_NO_NEW_PRIVS` applied in-process, and
//! - bubblewrap used to construct the filesystem view before exec.
use std::collections::BTreeSet;
use std::fs::File;
use std::os::fd::AsRawFd;
use std::path::Path;
use std::path::PathBuf;
use codex_core::error::CodexErr;
use codex_core::error::Result;
use codex_protocol::protocol::SandboxPolicy;
use codex_protocol::protocol::FileSystemSandboxPolicy;
use codex_protocol::protocol::WritableRoot;
/// Linux "platform defaults" that keep common system binaries and dynamic
@@ -76,6 +78,12 @@ impl BwrapNetworkMode {
}
}
#[derive(Debug)]
pub(crate) struct BwrapArgs {
pub args: Vec<String>,
pub preserved_files: Vec<File>,
}
/// Wrap a command with bubblewrap so the filesystem is read-only by default,
/// with explicit writable roots and read-only subpaths layered afterward.
///
@@ -85,22 +93,25 @@ impl BwrapNetworkMode {
/// namespace restrictions apply while preserving full filesystem access.
pub(crate) fn create_bwrap_command_args(
command: Vec<String>,
sandbox_policy: &SandboxPolicy,
file_system_sandbox_policy: &FileSystemSandboxPolicy,
cwd: &Path,
options: BwrapOptions,
) -> Result<Vec<String>> {
if sandbox_policy.has_full_disk_write_access() {
) -> Result<BwrapArgs> {
if file_system_sandbox_policy.has_full_disk_write_access() {
return if options.network_mode == BwrapNetworkMode::FullAccess {
Ok(command)
Ok(BwrapArgs {
args: command,
preserved_files: Vec::new(),
})
} else {
Ok(create_bwrap_flags_full_filesystem(command, options))
};
}
create_bwrap_flags(command, sandbox_policy, cwd, options)
create_bwrap_flags(command, file_system_sandbox_policy, cwd, options)
}
fn create_bwrap_flags_full_filesystem(command: Vec<String>, options: BwrapOptions) -> Vec<String> {
fn create_bwrap_flags_full_filesystem(command: Vec<String>, options: BwrapOptions) -> BwrapArgs {
let mut args = vec![
"--new-session".to_string(),
"--die-with-parent".to_string(),
@@ -121,20 +132,27 @@ fn create_bwrap_flags_full_filesystem(command: Vec<String>, options: BwrapOption
}
args.push("--".to_string());
args.extend(command);
args
BwrapArgs {
args,
preserved_files: Vec::new(),
}
}
/// Build the bubblewrap flags (everything after `argv[0]`).
fn create_bwrap_flags(
command: Vec<String>,
sandbox_policy: &SandboxPolicy,
file_system_sandbox_policy: &FileSystemSandboxPolicy,
cwd: &Path,
options: BwrapOptions,
) -> Result<Vec<String>> {
) -> Result<BwrapArgs> {
let BwrapArgs {
args: filesystem_args,
preserved_files,
} = create_filesystem_args(file_system_sandbox_policy, cwd)?;
let mut args = Vec::new();
args.push("--new-session".to_string());
args.push("--die-with-parent".to_string());
args.extend(create_filesystem_args(sandbox_policy, cwd)?);
args.extend(filesystem_args);
// Request a user namespace explicitly rather than relying on bubblewrap's
// auto-enable behavior, which is skipped when the caller runs as uid 0.
args.push("--unshare-user".to_string());
@@ -150,25 +168,35 @@ fn create_bwrap_flags(
}
args.push("--".to_string());
args.extend(command);
Ok(args)
Ok(BwrapArgs {
args,
preserved_files,
})
}
/// Build the bubblewrap filesystem mounts for a given sandbox policy.
/// Build the bubblewrap filesystem mounts for a given filesystem policy.
///
/// The mount order is important:
/// 1. Full-read policies use `--ro-bind / /`; restricted-read policies start
/// from `--tmpfs /` and layer scoped `--ro-bind` mounts.
/// 1. Full-read policies, and restricted policies that explicitly read `/`,
/// use `--ro-bind / /`; other restricted-read policies start from
/// `--tmpfs /` and layer scoped `--ro-bind` mounts.
/// 2. `--dev /dev` mounts a minimal writable `/dev` with standard device nodes
/// (including `/dev/urandom`) even under a read-only root.
/// 3. `--bind <root> <root>` re-enables writes for allowed roots, including
/// writable subpaths under `/dev` (for example, `/dev/shm`).
/// 4. `--ro-bind <subpath> <subpath>` re-applies read-only protections under
/// those writable roots so protected subpaths win.
fn create_filesystem_args(sandbox_policy: &SandboxPolicy, cwd: &Path) -> Result<Vec<String>> {
let writable_roots = sandbox_policy.get_writable_roots_with_cwd(cwd);
/// 5. Explicit unreadable roots are masked last so deny carveouts still win
/// even when the readable baseline includes `/`.
fn create_filesystem_args(
file_system_sandbox_policy: &FileSystemSandboxPolicy,
cwd: &Path,
) -> Result<BwrapArgs> {
let writable_roots = file_system_sandbox_policy.get_writable_roots_with_cwd(cwd);
let unreadable_roots = file_system_sandbox_policy.get_unreadable_roots_with_cwd(cwd);
ensure_mount_targets_exist(&writable_roots)?;
let mut args = if sandbox_policy.has_full_disk_read_access() {
let mut args = if file_system_sandbox_policy.has_full_disk_read_access() {
// Read-only root, then mount a minimal device tree.
// In bubblewrap (`bubblewrap.c`, `SETUP_MOUNT_DEV`), `--dev /dev`
// creates the standard minimal nodes: null, zero, full, random,
@@ -191,12 +219,12 @@ fn create_filesystem_args(sandbox_policy: &SandboxPolicy, cwd: &Path) -> Result<
"/dev".to_string(),
];
let mut readable_roots: BTreeSet<PathBuf> = sandbox_policy
let mut readable_roots: BTreeSet<PathBuf> = file_system_sandbox_policy
.get_readable_roots_with_cwd(cwd)
.into_iter()
.map(PathBuf::from)
.collect();
if sandbox_policy.include_platform_defaults() {
if file_system_sandbox_policy.include_platform_defaults() {
readable_roots.extend(
LINUX_PLATFORM_DEFAULT_READ_ROOTS
.iter()
@@ -206,7 +234,8 @@ fn create_filesystem_args(sandbox_policy: &SandboxPolicy, cwd: &Path) -> Result<
}
// A restricted policy can still explicitly request `/`, which is
// semantically equivalent to broad read access.
// the broad read baseline. Explicit unreadable carveouts are
// re-applied later.
if readable_roots.iter().any(|root| root == Path::new("/")) {
args = vec![
"--ro-bind".to_string(),
@@ -228,6 +257,7 @@ fn create_filesystem_args(sandbox_policy: &SandboxPolicy, cwd: &Path) -> Result<
args
};
let mut preserved_files = Vec::new();
for writable_root in &writable_roots {
let root = writable_root.root.as_path();
@@ -271,7 +301,44 @@ fn create_filesystem_args(sandbox_policy: &SandboxPolicy, cwd: &Path) -> Result<
}
}
Ok(args)
if !unreadable_roots.is_empty() {
// Apply explicit deny carveouts after all readable and writable mounts
// so they win even when the broader baseline includes `/` or a writable
// parent path.
let null_file = File::open("/dev/null")?;
let null_fd = null_file.as_raw_fd().to_string();
for unreadable_root in unreadable_roots {
let unreadable_root = unreadable_root.as_path();
if unreadable_root.is_dir() {
// Bubblewrap cannot bind `/dev/null` over a directory, so mask
// denied directories by overmounting them with an empty tmpfs
// and then remounting that tmpfs read-only.
args.push("--perms".to_string());
args.push("000".to_string());
args.push("--tmpfs".to_string());
args.push(path_to_string(unreadable_root));
args.push("--remount-ro".to_string());
args.push(path_to_string(unreadable_root));
continue;
}
// For files, bind a stable null-file payload over the original path
// so later reads do not expose host contents. `--ro-bind-data`
// expects a live fd number, so keep the backing file open until we
// exec bubblewrap below.
args.push("--perms".to_string());
args.push("000".to_string());
args.push("--ro-bind-data".to_string());
args.push(null_fd.clone());
args.push(path_to_string(unreadable_root));
}
preserved_files.push(null_file);
}
Ok(BwrapArgs {
args,
preserved_files,
})
}
/// Collect unique read-only subpaths across all writable roots.
@@ -386,6 +453,11 @@ fn find_first_non_existent_component(target_path: &Path) -> Option<PathBuf> {
#[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::FileSystemSandboxPolicy;
use codex_protocol::protocol::FileSystemSpecialPath;
use codex_protocol::protocol::ReadOnlyAccess;
use codex_protocol::protocol::SandboxPolicy;
use codex_utils_absolute_path::AbsolutePathBuf;
@@ -397,7 +469,7 @@ mod tests {
let command = vec!["/bin/true".to_string()];
let args = create_bwrap_command_args(
command.clone(),
&SandboxPolicy::DangerFullAccess,
&FileSystemSandboxPolicy::from(&SandboxPolicy::DangerFullAccess),
Path::new("/"),
BwrapOptions {
mount_proc: true,
@@ -406,7 +478,7 @@ mod tests {
)
.expect("create bwrap args");
assert_eq!(args, command);
assert_eq!(args.args, command);
}
#[test]
@@ -414,7 +486,7 @@ mod tests {
let command = vec!["/bin/true".to_string()];
let args = create_bwrap_command_args(
command,
&SandboxPolicy::DangerFullAccess,
&FileSystemSandboxPolicy::from(&SandboxPolicy::DangerFullAccess),
Path::new("/"),
BwrapOptions {
mount_proc: true,
@@ -424,7 +496,7 @@ mod tests {
.expect("create bwrap args");
assert_eq!(
args,
args.args,
vec![
"--new-session".to_string(),
"--die-with-parent".to_string(),
@@ -452,9 +524,13 @@ mod tests {
exclude_slash_tmp: true,
};
let args = create_filesystem_args(&sandbox_policy, Path::new("/")).expect("bwrap fs args");
let args = create_filesystem_args(
&FileSystemSandboxPolicy::from(&sandbox_policy),
Path::new("/"),
)
.expect("bwrap fs args");
assert_eq!(
args,
args.args,
vec![
"--ro-bind".to_string(),
"/".to_string(),
@@ -462,11 +538,11 @@ mod tests {
"--dev".to_string(),
"/dev".to_string(),
"--bind".to_string(),
"/dev".to_string(),
"/dev".to_string(),
"/".to_string(),
"/".to_string(),
"--bind".to_string(),
"/".to_string(),
"/".to_string(),
"/dev".to_string(),
"/dev".to_string(),
]
);
}
@@ -488,12 +564,13 @@ mod tests {
network_access: false,
};
let args = create_filesystem_args(&policy, temp_dir.path()).expect("filesystem args");
let args = create_filesystem_args(&FileSystemSandboxPolicy::from(&policy), temp_dir.path())
.expect("filesystem args");
assert_eq!(args[0..4], ["--tmpfs", "/", "--dev", "/dev"]);
assert_eq!(args.args[0..4], ["--tmpfs", "/", "--dev", "/dev"]);
let readable_root_str = path_to_string(&readable_root);
assert!(args.windows(3).any(|window| {
assert!(args.args.windows(3).any(|window| {
window
== [
"--ro-bind",
@@ -517,15 +594,138 @@ mod tests {
// `ReadOnlyAccess::Restricted` always includes `cwd` as a readable
// root. Using `"/"` here would intentionally collapse to broad read
// access, so use a non-root cwd to exercise the restricted path.
let args = create_filesystem_args(&policy, temp_dir.path()).expect("filesystem args");
let args = create_filesystem_args(&FileSystemSandboxPolicy::from(&policy), temp_dir.path())
.expect("filesystem args");
assert!(args.starts_with(&["--tmpfs".to_string(), "/".to_string()]));
assert!(
args.args
.starts_with(&["--tmpfs".to_string(), "/".to_string()])
);
if Path::new("/usr").exists() {
assert!(
args.windows(3)
args.args
.windows(3)
.any(|window| window == ["--ro-bind", "/usr", "/usr"])
);
}
}
#[test]
fn split_policy_reapplies_unreadable_carveouts_after_writable_binds() {
let temp_dir = TempDir::new().expect("temp dir");
let writable_root = temp_dir.path().join("workspace");
let blocked = writable_root.join("blocked");
std::fs::create_dir_all(&blocked).expect("create blocked dir");
let writable_root =
AbsolutePathBuf::from_absolute_path(&writable_root).expect("absolute writable root");
let blocked = AbsolutePathBuf::from_absolute_path(&blocked).expect("absolute blocked dir");
let policy = FileSystemSandboxPolicy::restricted(vec![
FileSystemSandboxEntry {
path: FileSystemPath::Path {
path: writable_root.clone(),
},
access: FileSystemAccessMode::Write,
},
FileSystemSandboxEntry {
path: FileSystemPath::Path {
path: blocked.clone(),
},
access: FileSystemAccessMode::None,
},
]);
let args = create_filesystem_args(&policy, temp_dir.path()).expect("filesystem args");
let writable_root_str = path_to_string(writable_root.as_path());
let blocked_str = path_to_string(blocked.as_path());
assert!(args.args.windows(3).any(|window| {
window
== [
"--bind",
writable_root_str.as_str(),
writable_root_str.as_str(),
]
}));
assert!(
args.args.windows(3).any(|window| {
window == ["--ro-bind", blocked_str.as_str(), blocked_str.as_str()]
})
);
}
#[test]
fn split_policy_masks_root_read_directory_carveouts() {
let temp_dir = TempDir::new().expect("temp dir");
let blocked = temp_dir.path().join("blocked");
std::fs::create_dir_all(&blocked).expect("create blocked dir");
let blocked = AbsolutePathBuf::from_absolute_path(&blocked).expect("absolute blocked dir");
let policy = FileSystemSandboxPolicy::restricted(vec![
FileSystemSandboxEntry {
path: FileSystemPath::Special {
value: FileSystemSpecialPath::Root,
},
access: FileSystemAccessMode::Read,
},
FileSystemSandboxEntry {
path: FileSystemPath::Path {
path: blocked.clone(),
},
access: FileSystemAccessMode::None,
},
]);
let args = create_filesystem_args(&policy, temp_dir.path()).expect("filesystem args");
let blocked_str = path_to_string(blocked.as_path());
assert!(
args.args
.windows(3)
.any(|window| window == ["--ro-bind", "/", "/"])
);
assert!(
args.args
.windows(4)
.any(|window| { window == ["--perms", "000", "--tmpfs", blocked_str.as_str()] })
);
assert!(
args.args
.windows(2)
.any(|window| window == ["--remount-ro", blocked_str.as_str()])
);
}
#[test]
fn split_policy_masks_root_read_file_carveouts() {
let temp_dir = TempDir::new().expect("temp dir");
let blocked_file = temp_dir.path().join("blocked.txt");
std::fs::write(&blocked_file, "secret").expect("create blocked file");
let blocked_file =
AbsolutePathBuf::from_absolute_path(&blocked_file).expect("absolute blocked file");
let policy = FileSystemSandboxPolicy::restricted(vec![
FileSystemSandboxEntry {
path: FileSystemPath::Special {
value: FileSystemSpecialPath::Root,
},
access: FileSystemAccessMode::Read,
},
FileSystemSandboxEntry {
path: FileSystemPath::Path {
path: blocked_file.clone(),
},
access: FileSystemAccessMode::None,
},
]);
let args = create_filesystem_args(&policy, temp_dir.path()).expect("filesystem args");
let blocked_file_str = path_to_string(blocked_file.as_path());
assert_eq!(args.preserved_files.len(), 1);
assert!(args.args.windows(5).any(|window| {
window[0] == "--perms"
&& window[1] == "000"
&& window[2] == "--ro-bind-data"
&& window[4] == blocked_file_str
}));
}
}

View File

@@ -178,7 +178,7 @@ pub fn run_main() -> ! {
});
run_bwrap_with_proc_fallback(
&sandbox_policy_cwd,
&sandbox_policy,
&file_system_sandbox_policy,
network_sandbox_policy,
inner,
!no_proc,
@@ -233,7 +233,10 @@ fn resolve_sandbox_policies(
}
}
(Some(sandbox_policy), None) => EffectiveSandboxPolicies {
file_system_sandbox_policy: FileSystemSandboxPolicy::from(&sandbox_policy),
file_system_sandbox_policy: FileSystemSandboxPolicy::from_legacy_sandbox_policy(
&sandbox_policy,
sandbox_policy_cwd,
),
network_sandbox_policy: NetworkSandboxPolicy::from(&sandbox_policy),
sandbox_policy,
},
@@ -261,7 +264,7 @@ 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,
file_system_sandbox_policy: &FileSystemSandboxPolicy,
network_sandbox_policy: NetworkSandboxPolicy,
inner: Vec<String>,
mount_proc: bool,
@@ -270,7 +273,12 @@ fn run_bwrap_with_proc_fallback(
let network_mode = bwrap_network_mode(network_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)
if mount_proc
&& !preflight_proc_mount_support(
sandbox_policy_cwd,
file_system_sandbox_policy,
network_mode,
)
{
eprintln!("codex-linux-sandbox: bwrap could not mount /proc; retrying with --no-proc");
mount_proc = false;
@@ -280,8 +288,13 @@ fn run_bwrap_with_proc_fallback(
mount_proc,
network_mode,
};
let argv = build_bwrap_argv(inner, sandbox_policy, sandbox_policy_cwd, options);
exec_vendored_bwrap(argv);
let bwrap_args = build_bwrap_argv(
inner,
file_system_sandbox_policy,
sandbox_policy_cwd,
options,
);
exec_vendored_bwrap(bwrap_args.args, bwrap_args.preserved_files);
}
fn bwrap_network_mode(
@@ -299,47 +312,56 @@ fn bwrap_network_mode(
fn build_bwrap_argv(
inner: Vec<String>,
sandbox_policy: &SandboxPolicy,
file_system_sandbox_policy: &FileSystemSandboxPolicy,
sandbox_policy_cwd: &Path,
options: BwrapOptions,
) -> Vec<String> {
let mut args = create_bwrap_command_args(inner, sandbox_policy, sandbox_policy_cwd, options)
.unwrap_or_else(|err| panic!("error building bubblewrap command: {err:?}"));
) -> crate::bwrap::BwrapArgs {
let mut bwrap_args = create_bwrap_command_args(
inner,
file_system_sandbox_policy,
sandbox_policy_cwd,
options,
)
.unwrap_or_else(|err| panic!("error building bubblewrap command: {err:?}"));
let command_separator_index = args
let command_separator_index = bwrap_args
.args
.iter()
.position(|arg| arg == "--")
.unwrap_or_else(|| panic!("bubblewrap argv is missing command separator '--'"));
args.splice(
bwrap_args.args.splice(
command_separator_index..command_separator_index,
["--argv0".to_string(), "codex-linux-sandbox".to_string()],
);
let mut argv = vec!["bwrap".to_string()];
argv.extend(args);
argv
argv.extend(bwrap_args.args);
crate::bwrap::BwrapArgs {
args: argv,
preserved_files: bwrap_args.preserved_files,
}
}
fn preflight_proc_mount_support(
sandbox_policy_cwd: &Path,
sandbox_policy: &SandboxPolicy,
file_system_sandbox_policy: &FileSystemSandboxPolicy,
network_mode: BwrapNetworkMode,
) -> bool {
let preflight_argv =
build_preflight_bwrap_argv(sandbox_policy_cwd, sandbox_policy, network_mode);
build_preflight_bwrap_argv(sandbox_policy_cwd, file_system_sandbox_policy, network_mode);
let stderr = run_bwrap_in_child_capture_stderr(preflight_argv);
!is_proc_mount_failure(stderr.as_str())
}
fn build_preflight_bwrap_argv(
sandbox_policy_cwd: &Path,
sandbox_policy: &SandboxPolicy,
file_system_sandbox_policy: &FileSystemSandboxPolicy,
network_mode: BwrapNetworkMode,
) -> Vec<String> {
) -> crate::bwrap::BwrapArgs {
let preflight_command = vec![resolve_true_command()];
build_bwrap_argv(
preflight_command,
sandbox_policy,
file_system_sandbox_policy,
sandbox_policy_cwd,
BwrapOptions {
mount_proc: true,
@@ -368,7 +390,7 @@ fn resolve_true_command() -> String {
/// - We capture stderr from that preflight to match known mount-failure text.
/// We do not stream it because this is a one-shot probe with a trivial
/// command, and reads are bounded to a fixed max size.
fn run_bwrap_in_child_capture_stderr(argv: Vec<String>) -> String {
fn run_bwrap_in_child_capture_stderr(bwrap_args: crate::bwrap::BwrapArgs) -> String {
const MAX_PREFLIGHT_STDERR_BYTES: u64 = 64 * 1024;
let mut pipe_fds = [0; 2];
@@ -397,7 +419,7 @@ fn run_bwrap_in_child_capture_stderr(argv: Vec<String>) -> String {
close_fd_or_panic(write_fd, "close write end in bubblewrap child");
}
let exit_code = run_vendored_bwrap_main(&argv);
let exit_code = run_vendored_bwrap_main(&bwrap_args.args, &bwrap_args.preserved_files);
std::process::exit(exit_code);
}

View File

@@ -35,15 +35,17 @@ fn ignores_non_proc_mount_errors() {
#[test]
fn inserts_bwrap_argv0_before_command_separator() {
let sandbox_policy = SandboxPolicy::new_read_only_policy();
let argv = build_bwrap_argv(
vec!["/bin/true".to_string()],
&SandboxPolicy::new_read_only_policy(),
&FileSystemSandboxPolicy::from(&sandbox_policy),
Path::new("/"),
BwrapOptions {
mount_proc: true,
network_mode: BwrapNetworkMode::FullAccess,
},
);
)
.args;
assert_eq!(
argv,
vec![
@@ -69,29 +71,33 @@ fn inserts_bwrap_argv0_before_command_separator() {
#[test]
fn inserts_unshare_net_when_network_isolation_requested() {
let sandbox_policy = SandboxPolicy::new_read_only_policy();
let argv = build_bwrap_argv(
vec!["/bin/true".to_string()],
&SandboxPolicy::new_read_only_policy(),
&FileSystemSandboxPolicy::from(&sandbox_policy),
Path::new("/"),
BwrapOptions {
mount_proc: true,
network_mode: BwrapNetworkMode::Isolated,
},
);
)
.args;
assert!(argv.contains(&"--unshare-net".to_string()));
}
#[test]
fn inserts_unshare_net_when_proxy_only_network_mode_requested() {
let sandbox_policy = SandboxPolicy::new_read_only_policy();
let argv = build_bwrap_argv(
vec!["/bin/true".to_string()],
&SandboxPolicy::new_read_only_policy(),
&FileSystemSandboxPolicy::from(&sandbox_policy),
Path::new("/"),
BwrapOptions {
mount_proc: true,
network_mode: BwrapNetworkMode::ProxyOnly,
},
);
)
.args;
assert!(argv.contains(&"--unshare-net".to_string()));
}
@@ -104,7 +110,12 @@ fn proxy_only_mode_takes_precedence_over_full_network_policy() {
#[test]
fn managed_proxy_preflight_argv_is_wrapped_for_full_access_policy() {
let mode = bwrap_network_mode(NetworkSandboxPolicy::Enabled, true);
let argv = build_preflight_bwrap_argv(Path::new("/"), &SandboxPolicy::DangerFullAccess, mode);
let argv = build_preflight_bwrap_argv(
Path::new("/"),
&FileSystemSandboxPolicy::from(&SandboxPolicy::DangerFullAccess),
mode,
)
.args;
assert!(argv.iter().any(|arg| arg == "--"));
}

View File

@@ -6,6 +6,7 @@
#[cfg(vendored_bwrap_available)]
mod imp {
use std::ffi::CString;
use std::fs::File;
use std::os::raw::c_char;
unsafe extern "C" {
@@ -27,7 +28,10 @@ mod imp {
///
/// On success, bubblewrap will `execve` into the target program and this
/// function will never return. A return value therefore implies failure.
pub(crate) fn run_vendored_bwrap_main(argv: &[String]) -> libc::c_int {
pub(crate) fn run_vendored_bwrap_main(
argv: &[String],
_preserved_files: &[File],
) -> libc::c_int {
let cstrings = argv_to_cstrings(argv);
let mut argv_ptrs: Vec<*const c_char> = cstrings.iter().map(|arg| arg.as_ptr()).collect();
@@ -39,16 +43,21 @@ mod imp {
}
/// Execute the build-time bubblewrap `main` function with the given argv.
pub(crate) fn exec_vendored_bwrap(argv: Vec<String>) -> ! {
let exit_code = run_vendored_bwrap_main(&argv);
pub(crate) fn exec_vendored_bwrap(argv: Vec<String>, preserved_files: Vec<File>) -> ! {
let exit_code = run_vendored_bwrap_main(&argv, &preserved_files);
std::process::exit(exit_code);
}
}
#[cfg(not(vendored_bwrap_available))]
mod imp {
use std::fs::File;
/// Panics with a clear error when the build-time bwrap path is not enabled.
pub(crate) fn run_vendored_bwrap_main(_argv: &[String]) -> libc::c_int {
pub(crate) fn run_vendored_bwrap_main(
_argv: &[String],
_preserved_files: &[File],
) -> libc::c_int {
panic!(
r#"build-time bubblewrap is not available in this build.
codex-linux-sandbox should always compile vendored bubblewrap on Linux targets.
@@ -60,8 +69,8 @@ Notes:
}
/// Panics with a clear error when the build-time bwrap path is not enabled.
pub(crate) fn exec_vendored_bwrap(_argv: Vec<String>) -> ! {
let _ = run_vendored_bwrap_main(&[]);
pub(crate) fn exec_vendored_bwrap(_argv: Vec<String>, _preserved_files: Vec<File>) -> ! {
let _ = run_vendored_bwrap_main(&[], &[]);
unreachable!("run_vendored_bwrap_main should always panic in this configuration")
}
}

View File

@@ -9,8 +9,13 @@ 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::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_protocol::protocol::ReadOnlyAccess;
use codex_protocol::protocol::SandboxPolicy;
use codex_utils_absolute_path::AbsolutePathBuf;
use pretty_assertions::assert_eq;
@@ -63,13 +68,47 @@ async fn run_cmd_output(
.expect("sandboxed command should execute")
}
#[expect(clippy::expect_used)]
async fn run_cmd_result_with_writable_roots(
cmd: &[&str],
writable_roots: &[PathBuf],
timeout_ms: u64,
use_bwrap_sandbox: bool,
network_access: bool,
) -> Result<codex_core::exec::ExecToolCallOutput> {
let sandbox_policy = SandboxPolicy::WorkspaceWrite {
writable_roots: writable_roots
.iter()
.map(|p| AbsolutePathBuf::try_from(p.as_path()).unwrap())
.collect(),
read_only_access: Default::default(),
network_access,
// 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.
exclude_tmpdir_env_var: true,
exclude_slash_tmp: true,
};
let file_system_sandbox_policy = FileSystemSandboxPolicy::from(&sandbox_policy);
let network_sandbox_policy = NetworkSandboxPolicy::from(&sandbox_policy);
run_cmd_result_with_policies(
cmd,
sandbox_policy,
file_system_sandbox_policy,
network_sandbox_policy,
timeout_ms,
use_bwrap_sandbox,
)
.await
}
#[expect(clippy::expect_used)]
async fn run_cmd_result_with_policies(
cmd: &[&str],
sandbox_policy: SandboxPolicy,
file_system_sandbox_policy: FileSystemSandboxPolicy,
network_sandbox_policy: NetworkSandboxPolicy,
timeout_ms: u64,
use_bwrap_sandbox: bool,
) -> Result<codex_core::exec::ExecToolCallOutput> {
let cwd = std::env::current_dir().expect("cwd should exist");
let sandbox_cwd = cwd.clone();
@@ -84,28 +123,14 @@ async fn run_cmd_result_with_writable_roots(
justification: None,
arg0: None,
};
let sandbox_policy = SandboxPolicy::WorkspaceWrite {
writable_roots: writable_roots
.iter()
.map(|p| AbsolutePathBuf::try_from(p.as_path()).unwrap())
.collect(),
read_only_access: Default::default(),
network_access,
// 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.
exclude_tmpdir_env_var: true,
exclude_slash_tmp: true,
};
let sandbox_program = env!("CARGO_BIN_EXE_codex-linux-sandbox");
let codex_linux_sandbox_exe = Some(PathBuf::from(sandbox_program));
process_exec_tool_call(
params,
&sandbox_policy,
&FileSystemSandboxPolicy::from(&sandbox_policy),
NetworkSandboxPolicy::from(&sandbox_policy),
&file_system_sandbox_policy,
network_sandbox_policy,
sandbox_cwd.as_path(),
&codex_linux_sandbox_exe,
use_bwrap_sandbox,
@@ -479,6 +504,110 @@ async fn sandbox_blocks_codex_symlink_replacement_attack() {
assert_ne!(codex_output.exit_code, 0);
}
#[tokio::test]
async fn sandbox_blocks_explicit_split_policy_carveouts_under_bwrap() {
if should_skip_bwrap_tests().await {
eprintln!("skipping bwrap test: bwrap sandbox prerequisites are unavailable");
return;
}
let tmpdir = tempfile::tempdir().expect("tempdir");
let blocked = tmpdir.path().join("blocked");
std::fs::create_dir_all(&blocked).expect("create blocked dir");
let blocked_target = blocked.join("secret.txt");
let sandbox_policy = SandboxPolicy::WorkspaceWrite {
writable_roots: vec![AbsolutePathBuf::try_from(tmpdir.path()).expect("absolute tempdir")],
read_only_access: Default::default(),
network_access: true,
exclude_tmpdir_env_var: true,
exclude_slash_tmp: true,
};
let file_system_sandbox_policy = FileSystemSandboxPolicy::restricted(vec![
FileSystemSandboxEntry {
path: FileSystemPath::Path {
path: AbsolutePathBuf::try_from(tmpdir.path()).expect("absolute tempdir"),
},
access: FileSystemAccessMode::Write,
},
FileSystemSandboxEntry {
path: FileSystemPath::Path {
path: AbsolutePathBuf::try_from(blocked.as_path()).expect("absolute blocked dir"),
},
access: FileSystemAccessMode::None,
},
]);
let output = expect_denied(
run_cmd_result_with_policies(
&[
"bash",
"-lc",
&format!("echo denied > {}", blocked_target.to_string_lossy()),
],
sandbox_policy,
file_system_sandbox_policy,
NetworkSandboxPolicy::Enabled,
LONG_TIMEOUT_MS,
true,
)
.await,
"explicit split-policy carveout should be denied under bubblewrap",
);
assert_ne!(output.exit_code, 0);
}
#[tokio::test]
async fn sandbox_blocks_root_read_carveouts_under_bwrap() {
if should_skip_bwrap_tests().await {
eprintln!("skipping bwrap test: bwrap sandbox prerequisites are unavailable");
return;
}
let tmpdir = tempfile::tempdir().expect("tempdir");
let blocked = tmpdir.path().join("blocked");
std::fs::create_dir_all(&blocked).expect("create blocked dir");
let blocked_target = blocked.join("secret.txt");
std::fs::write(&blocked_target, "secret").expect("seed blocked file");
let sandbox_policy = SandboxPolicy::ReadOnly {
access: ReadOnlyAccess::FullAccess,
network_access: true,
};
let file_system_sandbox_policy = FileSystemSandboxPolicy::restricted(vec![
FileSystemSandboxEntry {
path: FileSystemPath::Special {
value: FileSystemSpecialPath::Root,
},
access: FileSystemAccessMode::Read,
},
FileSystemSandboxEntry {
path: FileSystemPath::Path {
path: AbsolutePathBuf::try_from(blocked.as_path()).expect("absolute blocked dir"),
},
access: FileSystemAccessMode::None,
},
]);
let output = expect_denied(
run_cmd_result_with_policies(
&[
"bash",
"-lc",
&format!("cat {}", blocked_target.to_string_lossy()),
],
sandbox_policy,
file_system_sandbox_policy,
NetworkSandboxPolicy::Enabled,
LONG_TIMEOUT_MS,
true,
)
.await,
"root-read carveout should be denied under bubblewrap",
);
assert_ne!(output.exit_code, 0);
}
#[tokio::test]
async fn sandbox_blocks_ssh() {
// Force ssh to attempt a real TCP connection but fail quickly. `BatchMode`

View File

@@ -163,6 +163,34 @@ impl FileSystemSandboxPolicy {
}
}
/// Converts a legacy sandbox policy into an equivalent filesystem policy
/// for the provided cwd.
///
/// Legacy `WorkspaceWrite` policies may list readable roots that live
/// under an already-writable root. Those paths were redundant in the
/// legacy model and should not become read-only carveouts when projected
/// into split filesystem policy.
pub fn from_legacy_sandbox_policy(sandbox_policy: &SandboxPolicy, cwd: &Path) -> Self {
let mut file_system_policy = Self::from(sandbox_policy);
if matches!(sandbox_policy, SandboxPolicy::WorkspaceWrite { .. }) {
let legacy_writable_roots = sandbox_policy.get_writable_roots_with_cwd(cwd);
file_system_policy.entries.retain(|entry| {
if entry.access != FileSystemAccessMode::Read {
return true;
}
match &entry.path {
FileSystemPath::Path { path } => !legacy_writable_roots
.iter()
.any(|root| root.is_path_writable(path.as_path())),
FileSystemPath::Special { .. } => true,
}
});
}
file_system_policy
}
/// Returns true when filesystem reads are unrestricted.
pub fn has_full_disk_read_access(&self) -> bool {
match self.kind {
@@ -236,7 +264,13 @@ impl FileSystemSandboxPolicy {
}
let cwd_absolute = AbsolutePathBuf::from_absolute_path(cwd).ok();
let unreadable_roots = self.get_unreadable_roots_with_cwd(cwd);
let read_only_roots = dedup_absolute_paths(
self.entries
.iter()
.filter(|entry| !entry.access.can_write())
.filter_map(|entry| resolve_file_system_path(&entry.path, cwd_absolute.as_ref()))
.collect(),
);
let mut writable_roots = Vec::new();
if self.has_root_access(FileSystemAccessMode::can_write)
&& let Some(cwd_absolute) = cwd_absolute.as_ref()
@@ -260,9 +294,13 @@ impl FileSystemSandboxPolicy {
.into_iter()
.map(|root| {
let mut read_only_subpaths = default_read_only_subpaths_for_writable_root(&root);
// Narrower explicit non-write entries carve out broader writable roots.
// More specific write entries still remain writable because they appear
// as separate WritableRoot values and are checked independently.
read_only_subpaths.extend(
unreadable_roots
read_only_roots
.iter()
.filter(|path| path.as_path() != root.as_path())
.filter(|path| path.as_path().starts_with(root.as_path()))
.cloned(),
);

View File

@@ -3197,12 +3197,6 @@ mod tests {
use tempfile::NamedTempFile;
use tempfile::TempDir;
fn sorted_paths(paths: Vec<AbsolutePathBuf>) -> Vec<PathBuf> {
let mut sorted: Vec<PathBuf> = paths.into_iter().map(|path| path.to_path_buf()).collect();
sorted.sort();
sorted
}
fn sorted_writable_roots(roots: Vec<WritableRoot>) -> Vec<(PathBuf, Vec<PathBuf>)> {
let mut sorted_roots: Vec<(PathBuf, Vec<PathBuf>)> = roots
.into_iter()
@@ -3220,6 +3214,53 @@ mod tests {
sorted_roots
}
fn sandbox_policy_allows_read(policy: &SandboxPolicy, path: &Path, cwd: &Path) -> bool {
if policy.has_full_disk_read_access() {
return true;
}
policy
.get_readable_roots_with_cwd(cwd)
.iter()
.any(|root| path.starts_with(root.as_path()))
|| policy
.get_writable_roots_with_cwd(cwd)
.iter()
.any(|root| path.starts_with(root.root.as_path()))
}
fn sandbox_policy_allows_write(policy: &SandboxPolicy, path: &Path, cwd: &Path) -> bool {
if policy.has_full_disk_write_access() {
return true;
}
policy
.get_writable_roots_with_cwd(cwd)
.iter()
.any(|root| root.is_path_writable(path))
}
fn sandbox_policy_probe_paths(policy: &SandboxPolicy, cwd: &Path) -> Vec<PathBuf> {
let mut paths = vec![cwd.to_path_buf()];
paths.extend(
policy
.get_readable_roots_with_cwd(cwd)
.into_iter()
.map(|path| path.to_path_buf()),
);
for root in policy.get_writable_roots_with_cwd(cwd) {
paths.push(root.root.to_path_buf());
paths.extend(
root.read_only_subpaths
.into_iter()
.map(|path| path.to_path_buf()),
);
}
paths.sort();
paths.dedup();
paths
}
fn assert_same_sandbox_policy_semantics(
expected: &SandboxPolicy,
actual: &SandboxPolicy,
@@ -3241,14 +3282,25 @@ mod tests {
actual.include_platform_defaults(),
expected.include_platform_defaults()
);
assert_eq!(
sorted_paths(actual.get_readable_roots_with_cwd(cwd)),
sorted_paths(expected.get_readable_roots_with_cwd(cwd))
);
assert_eq!(
sorted_writable_roots(actual.get_writable_roots_with_cwd(cwd)),
sorted_writable_roots(expected.get_writable_roots_with_cwd(cwd))
);
let mut probe_paths = sandbox_policy_probe_paths(expected, cwd);
probe_paths.extend(sandbox_policy_probe_paths(actual, cwd));
probe_paths.sort();
probe_paths.dedup();
for path in probe_paths {
assert_eq!(
sandbox_policy_allows_read(actual, &path, cwd),
sandbox_policy_allows_read(expected, &path, cwd),
"read access mismatch for {}",
path.display()
);
assert_eq!(
sandbox_policy_allows_write(actual, &path, cwd),
sandbox_policy_allows_write(expected, &path, cwd),
"write access mismatch for {}",
path.display()
);
}
}
#[test]
@@ -3471,6 +3523,67 @@ mod tests {
);
}
#[test]
fn restricted_file_system_policy_treats_read_entries_as_read_only_subpaths() {
let cwd = TempDir::new().expect("tempdir");
let docs =
AbsolutePathBuf::resolve_path_against_base("docs", cwd.path()).expect("resolve docs");
let docs_public = AbsolutePathBuf::resolve_path_against_base("docs/public", cwd.path())
.expect("resolve docs/public");
let policy = FileSystemSandboxPolicy::restricted(vec![
FileSystemSandboxEntry {
path: FileSystemPath::Special {
value: FileSystemSpecialPath::CurrentWorkingDirectory,
},
access: FileSystemAccessMode::Write,
},
FileSystemSandboxEntry {
path: FileSystemPath::Path { path: docs.clone() },
access: FileSystemAccessMode::Read,
},
FileSystemSandboxEntry {
path: FileSystemPath::Path {
path: docs_public.clone(),
},
access: FileSystemAccessMode::Write,
},
]);
assert!(!policy.has_full_disk_write_access());
assert_eq!(
sorted_writable_roots(policy.get_writable_roots_with_cwd(cwd.path())),
vec![
(cwd.path().to_path_buf(), vec![docs.to_path_buf()]),
(docs_public.to_path_buf(), Vec::new()),
]
);
}
#[test]
fn legacy_workspace_write_nested_readable_root_stays_writable() {
let cwd = TempDir::new().expect("tempdir");
let docs =
AbsolutePathBuf::resolve_path_against_base("docs", cwd.path()).expect("resolve docs");
let policy = SandboxPolicy::WorkspaceWrite {
writable_roots: vec![],
read_only_access: ReadOnlyAccess::Restricted {
include_platform_defaults: true,
readable_roots: vec![docs],
},
network_access: false,
exclude_tmpdir_env_var: true,
exclude_slash_tmp: true,
};
assert_eq!(
sorted_writable_roots(
FileSystemSandboxPolicy::from_legacy_sandbox_policy(&policy, cwd.path())
.get_writable_roots_with_cwd(cwd.path())
),
vec![(cwd.path().to_path_buf(), Vec::new())]
);
}
#[test]
fn file_system_policy_rejects_legacy_bridge_for_non_workspace_writes() {
let cwd = if cfg!(windows) {
@@ -3508,6 +3621,8 @@ mod tests {
.expect("resolve readable root");
let writable_root = AbsolutePathBuf::resolve_path_against_base("writable", cwd.path())
.expect("resolve writable root");
let nested_readable_root = AbsolutePathBuf::resolve_path_against_base("docs", cwd.path())
.expect("resolve nested readable root");
let policies = [
SandboxPolicy::DangerFullAccess,
SandboxPolicy::ExternalSandbox {
@@ -3544,10 +3659,20 @@ mod tests {
exclude_tmpdir_env_var: false,
exclude_slash_tmp: true,
},
SandboxPolicy::WorkspaceWrite {
writable_roots: vec![],
read_only_access: ReadOnlyAccess::Restricted {
include_platform_defaults: true,
readable_roots: vec![nested_readable_root],
},
network_access: false,
exclude_tmpdir_env_var: true,
exclude_slash_tmp: true,
},
];
for expected in policies {
let actual = FileSystemSandboxPolicy::from(&expected)
let actual = FileSystemSandboxPolicy::from_legacy_sandbox_policy(&expected, cwd.path())
.to_legacy_sandbox_policy(NetworkSandboxPolicy::from(&expected), cwd.path())
.expect("legacy bridge should preserve legacy policy semantics");