Move workspace roots onto thread/session state and stop using active permission profile modifications as an overlay for writable roots. Existing app-server threads now preserve their persisted PermissionProfile value across resume, fork, and turn updates; permissions requests on existing threads only update the active named profile after validating it exists. Workspace roots can be updated independently, and SandboxPolicy::WorkspaceWrite no longer stores its own writable_roots.

This commit is contained in:
Michael Bolin
2026-05-08 13:44:26 -07:00
parent 7c9731c9af
commit f7df27bd44
136 changed files with 1804 additions and 1982 deletions
+4 -9
View File
@@ -97,6 +97,7 @@ pub struct SandboxTransformRequest<'a> {
// to make shared ownership explicit across runtime/sandbox plumbing.
pub network: Option<&'a NetworkProxy>,
pub sandbox_policy_cwd: &'a Path,
pub workspace_roots: &'a [AbsolutePathBuf],
pub codex_linux_sandbox_exe: Option<&'a Path>,
pub use_legacy_landlock: bool,
pub windows_sandbox_level: WindowsSandboxLevel,
@@ -176,6 +177,7 @@ impl SandboxManager {
enforce_managed_network,
network,
sandbox_policy_cwd,
workspace_roots,
codex_linux_sandbox_exe,
use_legacy_landlock,
windows_sandbox_level,
@@ -183,7 +185,8 @@ impl SandboxManager {
} = request;
let additional_permissions = command.additional_permissions.take();
let effective_permission_profile =
effective_permission_profile(permissions, additional_permissions.as_ref());
effective_permission_profile(permissions, additional_permissions.as_ref())
.materialize_project_roots_with_workspace_roots(workspace_roots);
let (effective_file_system_policy, effective_network_policy) =
effective_permission_profile.to_runtime_permissions();
let mut argv = Vec::with_capacity(1 + command.args.len());
@@ -278,13 +281,6 @@ fn compatibility_workspace_write_policy(
network_policy: NetworkSandboxPolicy,
cwd: &Path,
) -> SandboxPolicy {
let cwd_abs = AbsolutePathBuf::from_absolute_path(cwd).ok();
let writable_roots = file_system_policy
.get_writable_roots_with_cwd(cwd)
.into_iter()
.map(|root| root.root)
.filter(|root| cwd_abs.as_ref() != Some(root))
.collect();
let tmpdir_writable = std::env::var_os("TMPDIR")
.filter(|tmpdir| !tmpdir.is_empty())
.and_then(|tmpdir| {
@@ -297,7 +293,6 @@ fn compatibility_workspace_write_policy(
&& file_system_policy.can_write_path_with_cwd(slash_tmp, cwd);
SandboxPolicy::WorkspaceWrite {
writable_roots,
network_access: network_policy.is_enabled(),
exclude_tmpdir_env_var: !tmpdir_writable,
exclude_slash_tmp: !slash_tmp_writable,
+4
View File
@@ -91,6 +91,7 @@ fn transform_preserves_unrestricted_file_system_policy_for_restricted_network()
enforce_managed_network: false,
network: None,
sandbox_policy_cwd: cwd.as_path(),
workspace_roots: std::slice::from_ref(&cwd),
codex_linux_sandbox_exe: None,
use_legacy_landlock: false,
windows_sandbox_level: WindowsSandboxLevel::Disabled,
@@ -142,6 +143,7 @@ fn transform_additional_permissions_enable_network_for_external_sandbox() {
enforce_managed_network: false,
network: None,
sandbox_policy_cwd: cwd.as_path(),
workspace_roots: std::slice::from_ref(&cwd),
codex_linux_sandbox_exe: None,
use_legacy_landlock: false,
windows_sandbox_level: WindowsSandboxLevel::Disabled,
@@ -210,6 +212,7 @@ fn transform_additional_permissions_preserves_denied_entries() {
enforce_managed_network: false,
network: None,
sandbox_policy_cwd: cwd.as_path(),
workspace_roots: std::slice::from_ref(&cwd),
codex_linux_sandbox_exe: None,
use_legacy_landlock: false,
windows_sandbox_level: WindowsSandboxLevel::Disabled,
@@ -263,6 +266,7 @@ fn transform_linux_seccomp_request(
enforce_managed_network: false,
network: None,
sandbox_policy_cwd: cwd.as_path(),
workspace_roots: std::slice::from_ref(&cwd),
codex_linux_sandbox_exe: Some(codex_linux_sandbox_exe),
use_legacy_landlock: false,
windows_sandbox_level: WindowsSandboxLevel::Disabled,
+61 -57
View File
@@ -164,7 +164,6 @@ fn create_seatbelt_args_routes_network_through_proxy_ports() {
fn dynamic_network_policy_allows_tls_without_darwin_user_cache_write() {
let policy = dynamic_network_policy(
&SandboxPolicy::WorkspaceWrite {
writable_roots: vec![],
network_access: true,
exclude_tmpdir_env_var: false,
exclude_slash_tmp: false,
@@ -437,7 +436,6 @@ fn create_seatbelt_args_allows_local_binding_when_explicitly_enabled() {
fn dynamic_network_policy_preserves_restricted_policy_when_proxy_config_without_ports() {
let policy = dynamic_network_policy(
&SandboxPolicy::WorkspaceWrite {
writable_roots: vec![],
network_access: true,
exclude_tmpdir_env_var: false,
exclude_slash_tmp: false,
@@ -473,7 +471,6 @@ fn dynamic_network_policy_preserves_restricted_policy_when_proxy_config_without_
fn dynamic_network_policy_blocks_dns_when_local_binding_has_no_proxy_ports() {
let policy = dynamic_network_policy(
&SandboxPolicy::WorkspaceWrite {
writable_roots: vec![],
network_access: true,
exclude_tmpdir_env_var: false,
exclude_slash_tmp: false,
@@ -501,7 +498,6 @@ fn dynamic_network_policy_blocks_dns_when_local_binding_has_no_proxy_ports() {
fn dynamic_network_policy_preserves_restricted_policy_for_managed_network_without_proxy_config() {
let policy = dynamic_network_policy(
&SandboxPolicy::WorkspaceWrite {
writable_roots: vec![],
network_access: true,
exclude_tmpdir_env_var: false,
exclude_slash_tmp: false,
@@ -791,7 +787,6 @@ fn create_seatbelt_args_allows_all_unix_sockets_when_enabled() {
fn create_seatbelt_args_full_network_with_proxy_is_still_proxy_only() {
let policy = dynamic_network_policy(
&SandboxPolicy::WorkspaceWrite {
writable_roots: vec![],
network_access: true,
exclude_tmpdir_env_var: false,
exclude_slash_tmp: false,
@@ -838,15 +833,15 @@ fn create_seatbelt_args_with_read_only_git_and_codex_subpaths() {
// Build a policy that only includes the two test roots as writable and
// does not automatically include defaults TMPDIR or /tmp.
let policy = SandboxPolicy::WorkspaceWrite {
writable_roots: vec![vulnerable_root, empty_root]
.into_iter()
.map(|p| p.try_into().unwrap())
.collect(),
network_access: false,
exclude_tmpdir_env_var: true,
exclude_slash_tmp: true,
};
let writable_roots = vec![vulnerable_root, empty_root]
.into_iter()
.map(|path| AbsolutePathBuf::from_absolute_path(path).expect("absolute writable root"))
.collect::<Vec<_>>();
let file_system_sandbox_policy = FileSystemSandboxPolicy::workspace_write(
&writable_roots,
/*exclude_tmpdir_env_var*/ true,
/*exclude_slash_tmp*/ true,
);
// Create the Seatbelt command to wrap a shell command that tries to
// write to .codex/config.toml in the vulnerable root.
@@ -863,13 +858,15 @@ fn create_seatbelt_args_with_read_only_git_and_codex_subpaths() {
.iter()
.map(std::string::ToString::to_string)
.collect();
let args = create_seatbelt_command_args_for_legacy_policy(
shell_command.clone(),
&policy,
&cwd,
/*enforce_managed_network*/ false,
/*network*/ None,
);
let args = create_seatbelt_command_args(CreateSeatbeltCommandArgsParams {
command: shell_command.clone(),
file_system_sandbox_policy: &file_system_sandbox_policy,
network_sandbox_policy: NetworkSandboxPolicy::Restricted,
sandbox_policy_cwd: &cwd,
enforce_managed_network: false,
network: None,
extra_allow_unix_sockets: &[],
});
let policy_text = seatbelt_policy_arg(&args);
assert!(
@@ -1002,13 +999,15 @@ fn create_seatbelt_args_with_read_only_git_and_codex_subpaths() {
.iter()
.map(std::string::ToString::to_string)
.collect();
let write_hooks_file_args = create_seatbelt_command_args_for_legacy_policy(
shell_command_git,
&policy,
&cwd,
/*enforce_managed_network*/ false,
/*network*/ None,
);
let write_hooks_file_args = create_seatbelt_command_args(CreateSeatbeltCommandArgsParams {
command: shell_command_git,
file_system_sandbox_policy: &file_system_sandbox_policy,
network_sandbox_policy: NetworkSandboxPolicy::Restricted,
sandbox_policy_cwd: &cwd,
enforce_managed_network: false,
network: None,
extra_allow_unix_sockets: &[],
});
let output = Command::new(MACOS_PATH_TO_SEATBELT_EXECUTABLE)
.args(&write_hooks_file_args)
.current_dir(&cwd)
@@ -1038,13 +1037,15 @@ fn create_seatbelt_args_with_read_only_git_and_codex_subpaths() {
.iter()
.map(std::string::ToString::to_string)
.collect();
let write_allowed_file_args = create_seatbelt_command_args_for_legacy_policy(
shell_command_allowed,
&policy,
&cwd,
/*enforce_managed_network*/ false,
/*network*/ None,
);
let write_allowed_file_args = create_seatbelt_command_args(CreateSeatbeltCommandArgsParams {
command: shell_command_allowed,
file_system_sandbox_policy: &file_system_sandbox_policy,
network_sandbox_policy: NetworkSandboxPolicy::Restricted,
sandbox_policy_cwd: &cwd,
enforce_managed_network: false,
network: None,
extra_allow_unix_sockets: &[],
});
let output = Command::new(MACOS_PATH_TO_SEATBELT_EXECUTABLE)
.args(&write_allowed_file_args)
.current_dir(&cwd)
@@ -1085,7 +1086,6 @@ fn create_seatbelt_args_block_first_time_dot_codex_creation_with_metadata_name_r
let dot_codex = repo_root.join(".codex");
let config_toml = dot_codex.join("config.toml");
let policy = SandboxPolicy::WorkspaceWrite {
writable_roots: vec![repo_root.as_path().try_into().expect("absolute repo root")],
network_access: false,
exclude_tmpdir_env_var: true,
exclude_slash_tmp: true,
@@ -1137,12 +1137,13 @@ fn create_seatbelt_args_with_read_only_git_pointer_file() {
let cwd = tmp.path().join("cwd");
fs::create_dir_all(&cwd).expect("create cwd");
let policy = SandboxPolicy::WorkspaceWrite {
writable_roots: vec![worktree_root.try_into().expect("worktree_root is absolute")],
network_access: false,
exclude_tmpdir_env_var: true,
exclude_slash_tmp: true,
};
let worktree_root =
AbsolutePathBuf::from_absolute_path(worktree_root).expect("worktree_root is absolute");
let file_system_sandbox_policy = FileSystemSandboxPolicy::workspace_write(
std::slice::from_ref(&worktree_root),
/*exclude_tmpdir_env_var*/ true,
/*exclude_slash_tmp*/ true,
);
let shell_command: Vec<String> = [
"bash",
@@ -1154,13 +1155,15 @@ fn create_seatbelt_args_with_read_only_git_pointer_file() {
.iter()
.map(std::string::ToString::to_string)
.collect();
let args = create_seatbelt_command_args_for_legacy_policy(
shell_command,
&policy,
&cwd,
/*enforce_managed_network*/ false,
/*network*/ None,
);
let args = create_seatbelt_command_args(CreateSeatbeltCommandArgsParams {
command: shell_command,
file_system_sandbox_policy: &file_system_sandbox_policy,
network_sandbox_policy: NetworkSandboxPolicy::Restricted,
sandbox_policy_cwd: &cwd,
enforce_managed_network: false,
network: None,
extra_allow_unix_sockets: &[],
});
let output = Command::new(MACOS_PATH_TO_SEATBELT_EXECUTABLE)
.args(&args)
@@ -1190,13 +1193,15 @@ fn create_seatbelt_args_with_read_only_git_pointer_file() {
.iter()
.map(std::string::ToString::to_string)
.collect();
let gitdir_args = create_seatbelt_command_args_for_legacy_policy(
shell_command_gitdir,
&policy,
&cwd,
/*enforce_managed_network*/ false,
/*network*/ None,
);
let gitdir_args = create_seatbelt_command_args(CreateSeatbeltCommandArgsParams {
command: shell_command_gitdir,
file_system_sandbox_policy: &file_system_sandbox_policy,
network_sandbox_policy: NetworkSandboxPolicy::Restricted,
sandbox_policy_cwd: &cwd,
enforce_managed_network: false,
network: None,
extra_allow_unix_sockets: &[],
});
let output = Command::new(MACOS_PATH_TO_SEATBELT_EXECUTABLE)
.args(&gitdir_args)
.current_dir(&cwd)
@@ -1234,7 +1239,6 @@ fn create_seatbelt_args_for_cwd_as_git_repo() {
// use the default ones (cwd and TMPDIR) and verifies the protected
// metadata checks are done properly for cwd.
let policy = SandboxPolicy::WorkspaceWrite {
writable_roots: vec![],
network_access: false,
exclude_tmpdir_env_var: false,
exclude_slash_tmp: false,