permissions: derive compatibility policies from profiles (#19392)

## Why

After #19391, `PermissionProfile` and the split filesystem/network
policies could still be stored in parallel. That creates drift risk: a
profile can preserve deny globs, external enforcement, or split
filesystem entries while a cached projection silently loses those
details. This PR makes the profile the runtime source and derives
compatibility views from it.

## What Changed

- Removes stored filesystem/network sandbox projections from
`Permissions` and `SessionConfiguration`; their accessors now derive
from the canonical `PermissionProfile`.
- Derives legacy `SandboxPolicy` snapshots from profiles only where an
older API still needs that field.
- Updates MCP connection and elicitation state to track
`PermissionProfile` instead of `SandboxPolicy` for auto-approval
decisions.
- Adds semantic filesystem-policy comparison so cwd changes can preserve
richer profiles while still recognizing equivalent legacy projections
independent of entry ordering.
- Updates config/session tests to assert profile-derived projections
instead of parallel stored fields.

## Verification

- `cargo test -p codex-core direct_write_roots`
- `cargo test -p codex-core runtime_roots_to_legacy_projection`
- `cargo test -p codex-app-server
requested_permissions_trust_project_uses_permission_profile_intent`



































































---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/openai/codex/pull/19392).
* #19395
* #19394
* #19393
* __->__ #19392
This commit is contained in:
Michael Bolin
2026-04-26 15:06:42 -07:00
committed by GitHub
Unverified
parent 4d7ce3447d
commit deaa307fb2
39 changed files with 568 additions and 439 deletions
@@ -2278,9 +2278,11 @@ impl CodexMessageProcessor {
codex_protocol::models::PermissionProfile::from(permission_profile);
let (mut file_system_sandbox_policy, network_sandbox_policy) =
permission_profile.to_runtime_permissions();
let configured_file_system_sandbox_policy =
self.config.permissions.file_system_sandbox_policy();
Self::preserve_configured_deny_read_restrictions(
&mut file_system_sandbox_policy,
&self.config.permissions.file_system_sandbox_policy,
&configured_file_system_sandbox_policy,
);
let effective_permission_profile =
codex_protocol::models::PermissionProfile::from_runtime_permissions_with_enforcement(
+16 -14
View File
@@ -189,22 +189,23 @@ async fn run_command_under_sandbox(
let mut child = match sandbox_type {
#[cfg(target_os = "macos")]
SandboxType::Seatbelt => {
let file_system_sandbox_policy = config.permissions.file_system_sandbox_policy();
let network_sandbox_policy = config.permissions.network_sandbox_policy();
let args = create_seatbelt_command_args(CreateSeatbeltCommandArgsParams {
command,
file_system_sandbox_policy: &config.permissions.file_system_sandbox_policy,
network_sandbox_policy: config.permissions.network_sandbox_policy,
file_system_sandbox_policy: &file_system_sandbox_policy,
network_sandbox_policy,
sandbox_policy_cwd: sandbox_policy_cwd.as_path(),
enforce_managed_network: false,
network: network.as_ref(),
extra_allow_unix_sockets: allow_unix_sockets,
});
let network_policy = config.permissions.network_sandbox_policy;
spawn_debug_sandbox_child(
PathBuf::from("/usr/bin/sandbox-exec"),
args,
/*arg0*/ None,
cwd.to_path_buf(),
network_policy,
network_sandbox_policy,
env,
|env_map| {
env_map.insert(CODEX_SANDBOX_ENV_VAR.to_string(), "seatbelt".to_string());
@@ -221,23 +222,24 @@ async fn run_command_under_sandbox(
.codex_linux_sandbox_exe
.expect("codex-linux-sandbox executable not found");
let use_legacy_landlock = config.features.use_legacy_landlock();
let file_system_sandbox_policy = config.permissions.file_system_sandbox_policy();
let network_sandbox_policy = config.permissions.network_sandbox_policy();
let args = create_linux_sandbox_command_args_for_policies(
command,
cwd.as_path(),
config.permissions.sandbox_policy.get(),
&config.permissions.file_system_sandbox_policy,
config.permissions.network_sandbox_policy,
&file_system_sandbox_policy,
network_sandbox_policy,
sandbox_policy_cwd.as_path(),
use_legacy_landlock,
/*allow_network_for_proxy*/ false,
);
let network_policy = config.permissions.network_sandbox_policy;
spawn_debug_sandbox_child(
codex_linux_sandbox_exe,
args,
Some("codex-linux-sandbox"),
cwd.to_path_buf(),
network_policy,
network_sandbox_policy,
env,
|env_map| {
if let Some(network) = network.as_ref() {
@@ -715,17 +717,17 @@ mod tests {
assert!(config_uses_permission_profiles(&config));
assert!(
profile_config.permissions.file_system_sandbox_policy
!= legacy_config.permissions.file_system_sandbox_policy,
profile_config.permissions.file_system_sandbox_policy()
!= legacy_config.permissions.file_system_sandbox_policy(),
"test fixture should distinguish profile syntax from legacy sandbox_mode"
);
assert_eq!(
config.permissions.file_system_sandbox_policy,
profile_config.permissions.file_system_sandbox_policy,
config.permissions.file_system_sandbox_policy(),
profile_config.permissions.file_system_sandbox_policy(),
);
assert_ne!(
config.permissions.file_system_sandbox_policy,
legacy_config.permissions.file_system_sandbox_policy,
config.permissions.file_system_sandbox_policy(),
legacy_config.permissions.file_system_sandbox_policy(),
);
Ok(())
+14 -9
View File
@@ -26,10 +26,10 @@ use codex_plugin::PluginCapabilitySummary;
use codex_protocol::mcp::Resource;
use codex_protocol::mcp::ResourceTemplate;
use codex_protocol::mcp::Tool;
use codex_protocol::models::PermissionProfile;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::McpAuthStatus;
use codex_protocol::protocol::McpListToolsResponseEvent;
use codex_protocol::protocol::SandboxPolicy;
use rmcp::model::ReadResourceRequestParams;
use rmcp::model::ReadResourceResult;
use serde_json::Value;
@@ -66,13 +66,18 @@ pub fn qualified_mcp_tool_name_prefix(server_name: &str) -> String {
/// of being shown to the user.
pub fn mcp_permission_prompt_is_auto_approved(
approval_policy: AskForApproval,
sandbox_policy: &SandboxPolicy,
permission_profile: &PermissionProfile,
) -> bool {
approval_policy == AskForApproval::Never
&& matches!(
sandbox_policy,
SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. }
)
if approval_policy != AskForApproval::Never {
return false;
}
match permission_profile {
PermissionProfile::Disabled | PermissionProfile::External { .. } => true,
PermissionProfile::Managed { file_system, .. } => {
file_system.to_sandbox_policy().has_full_disk_write_access()
}
}
}
/// MCP runtime settings derived from `codex_core::config::Config`.
@@ -229,7 +234,7 @@ pub async fn read_mcp_resource(
&config.approval_policy,
String::new(),
tx_event,
SandboxPolicy::new_read_only_policy(),
PermissionProfile::default(),
runtime_environment,
config.codex_home.clone(),
codex_apps_tools_cache_key(auth),
@@ -294,7 +299,7 @@ pub async fn collect_mcp_server_status_snapshot_with_detail(
&config.approval_policy,
submit_id,
tx_event,
SandboxPolicy::new_read_only_policy(),
PermissionProfile::default(),
runtime_environment,
config.codex_home.clone(),
codex_apps_tools_cache_key(auth),
+31
View File
@@ -3,6 +3,9 @@ use codex_config::Constrained;
use codex_login::CodexAuth;
use codex_plugin::AppConnectorId;
use codex_plugin::PluginCapabilitySummary;
use codex_protocol::models::ManagedFileSystemPermissions;
use codex_protocol::models::PermissionProfile;
use codex_protocol::permissions::NetworkSandboxPolicy;
use codex_protocol::protocol::AskForApproval;
use pretty_assertions::assert_eq;
use std::collections::HashMap;
@@ -33,6 +36,34 @@ fn qualified_mcp_tool_name_prefix_sanitizes_server_names_without_lowercasing() {
);
}
#[test]
fn mcp_prompt_auto_approval_honors_unrestricted_managed_profiles() {
assert!(mcp_permission_prompt_is_auto_approved(
AskForApproval::Never,
&PermissionProfile::Managed {
file_system: ManagedFileSystemPermissions::Unrestricted,
network: NetworkSandboxPolicy::Enabled,
},
));
assert!(mcp_permission_prompt_is_auto_approved(
AskForApproval::Never,
&PermissionProfile::Managed {
file_system: ManagedFileSystemPermissions::Unrestricted,
network: NetworkSandboxPolicy::Restricted,
},
));
assert!(!mcp_permission_prompt_is_auto_approved(
AskForApproval::Never,
&PermissionProfile::read_only(),
));
assert!(!mcp_permission_prompt_is_auto_approved(
AskForApproval::OnRequest,
&PermissionProfile::Managed {
file_system: ManagedFileSystemPermissions::Unrestricted,
network: NetworkSandboxPolicy::Enabled,
},
));
}
#[test]
fn tool_plugin_provenance_collects_app_and_mcp_sources() {
let provenance = ToolPluginProvenance::from_capability_summaries(&[
@@ -334,15 +334,15 @@ fn can_auto_accept_elicitation(elicitation: &CreateElicitationRequestParams) ->
struct ElicitationRequestManager {
requests: Arc<Mutex<ResponderMap>>,
approval_policy: Arc<StdMutex<AskForApproval>>,
sandbox_policy: Arc<StdMutex<SandboxPolicy>>,
permission_profile: Arc<StdMutex<PermissionProfile>>,
}
impl ElicitationRequestManager {
fn new(approval_policy: AskForApproval, sandbox_policy: SandboxPolicy) -> Self {
fn new(approval_policy: AskForApproval, permission_profile: PermissionProfile) -> Self {
Self {
requests: Arc::new(Mutex::new(HashMap::new())),
approval_policy: Arc::new(StdMutex::new(approval_policy)),
sandbox_policy: Arc::new(StdMutex::new(sandbox_policy)),
permission_profile: Arc::new(StdMutex::new(permission_profile)),
}
}
@@ -364,23 +364,23 @@ impl ElicitationRequestManager {
fn make_sender(&self, server_name: String, tx_event: Sender<Event>) -> SendElicitation {
let elicitation_requests = self.requests.clone();
let approval_policy = self.approval_policy.clone();
let sandbox_policy = self.sandbox_policy.clone();
let permission_profile = self.permission_profile.clone();
Box::new(move |id, elicitation| {
let elicitation_requests = elicitation_requests.clone();
let tx_event = tx_event.clone();
let server_name = server_name.clone();
let approval_policy = approval_policy.clone();
let sandbox_policy = sandbox_policy.clone();
let permission_profile = permission_profile.clone();
async move {
let approval_policy = approval_policy
.lock()
.map(|policy| *policy)
.unwrap_or(AskForApproval::Never);
let sandbox_policy = sandbox_policy
let permission_profile = permission_profile
.lock()
.map(|policy| policy.clone())
.unwrap_or_else(|_| SandboxPolicy::new_read_only_policy());
if mcp_permission_prompt_is_auto_approved(approval_policy, &sandbox_policy)
.map(|profile| profile.clone())
.unwrap_or_default();
if mcp_permission_prompt_is_auto_approved(approval_policy, &permission_profile)
&& can_auto_accept_elicitation(&elicitation)
{
return Ok(ElicitationResponse {
@@ -666,14 +666,14 @@ impl AsyncManagedClient {
impl McpConnectionManager {
pub fn new_uninitialized(
approval_policy: &Constrained<AskForApproval>,
sandbox_policy: &Constrained<SandboxPolicy>,
permission_profile: &Constrained<PermissionProfile>,
) -> Self {
Self {
clients: HashMap::new(),
server_origins: HashMap::new(),
elicitation_requests: ElicitationRequestManager::new(
approval_policy.value(),
sandbox_policy.get().clone(),
permission_profile.get().clone(),
),
}
}
@@ -692,9 +692,9 @@ impl McpConnectionManager {
}
}
pub fn set_sandbox_policy(&self, sandbox_policy: &SandboxPolicy) {
if let Ok(mut policy) = self.elicitation_requests.sandbox_policy.lock() {
*policy = sandbox_policy.clone();
pub fn set_permission_profile(&self, permission_profile: PermissionProfile) {
if let Ok(mut profile) = self.elicitation_requests.permission_profile.lock() {
*profile = permission_profile;
}
}
@@ -706,7 +706,7 @@ impl McpConnectionManager {
approval_policy: &Constrained<AskForApproval>,
submit_id: String,
tx_event: Sender<Event>,
initial_sandbox_policy: SandboxPolicy,
initial_permission_profile: PermissionProfile,
runtime_environment: McpRuntimeEnvironment,
codex_home: PathBuf,
codex_apps_tools_cache_key: CodexAppsToolsCacheKey,
@@ -718,7 +718,7 @@ impl McpConnectionManager {
let mut server_origins = HashMap::new();
let mut join_set = JoinSet::new();
let elicitation_requests =
ElicitationRequestManager::new(approval_policy.value(), initial_sandbox_policy);
ElicitationRequestManager::new(approval_policy.value(), initial_permission_profile);
let tool_plugin_provenance = Arc::new(tool_plugin_provenance);
let startup_submit_id = submit_id.clone();
let codex_apps_auth_provider = auth
@@ -1,5 +1,6 @@
use super::*;
use codex_protocol::ToolName;
use codex_protocol::models::PermissionProfile;
use codex_protocol::protocol::GranularApprovalConfig;
use codex_protocol::protocol::McpAuthStatus;
use pretty_assertions::assert_eq;
@@ -179,9 +180,9 @@ fn elicitation_granular_policy_respects_never_and_config() {
}
#[tokio::test]
async fn full_access_auto_accepts_elicitation_with_empty_form_schema() {
async fn disabled_permissions_auto_accept_elicitation_with_empty_form_schema() {
let manager =
ElicitationRequestManager::new(AskForApproval::Never, SandboxPolicy::DangerFullAccess);
ElicitationRequestManager::new(AskForApproval::Never, PermissionProfile::Disabled);
let (tx_event, _rx_event) = async_channel::bounded(1);
let sender = manager.make_sender("server".to_string(), tx_event);
@@ -209,9 +210,9 @@ async fn full_access_auto_accepts_elicitation_with_empty_form_schema() {
}
#[tokio::test]
async fn full_access_does_not_auto_accept_elicitation_with_requested_fields() {
async fn disabled_permissions_do_not_auto_accept_elicitation_with_requested_fields() {
let manager =
ElicitationRequestManager::new(AskForApproval::Never, SandboxPolicy::DangerFullAccess);
ElicitationRequestManager::new(AskForApproval::Never, PermissionProfile::Disabled);
let (tx_event, _rx_event) = async_channel::bounded(1);
let sender = manager.make_sender("server".to_string(), tx_event);
@@ -627,8 +628,9 @@ async fn list_all_tools_uses_startup_snapshot_while_client_is_pending() {
.boxed()
.shared();
let approval_policy = Constrained::allow_any(AskForApproval::OnFailure);
let sandbox_policy = Constrained::allow_any(SandboxPolicy::new_read_only_policy());
let mut manager = McpConnectionManager::new_uninitialized(&approval_policy, &sandbox_policy);
let permission_profile = Constrained::allow_any(PermissionProfile::default());
let mut manager =
McpConnectionManager::new_uninitialized(&approval_policy, &permission_profile);
manager.clients.insert(
CODEX_APPS_MCP_SERVER_NAME.to_string(),
AsyncManagedClient {
@@ -654,8 +656,9 @@ async fn resolve_tool_info_accepts_canonical_namespaced_tool_names() {
.boxed()
.shared();
let approval_policy = Constrained::allow_any(AskForApproval::OnFailure);
let sandbox_policy = Constrained::allow_any(SandboxPolicy::new_read_only_policy());
let mut manager = McpConnectionManager::new_uninitialized(&approval_policy, &sandbox_policy);
let permission_profile = Constrained::allow_any(PermissionProfile::default());
let mut manager =
McpConnectionManager::new_uninitialized(&approval_policy, &permission_profile);
manager.clients.insert(
"rmcp".to_string(),
AsyncManagedClient {
@@ -689,8 +692,9 @@ async fn list_all_tools_blocks_while_client_is_pending_without_startup_snapshot(
.boxed()
.shared();
let approval_policy = Constrained::allow_any(AskForApproval::OnFailure);
let sandbox_policy = Constrained::allow_any(SandboxPolicy::new_read_only_policy());
let mut manager = McpConnectionManager::new_uninitialized(&approval_policy, &sandbox_policy);
let permission_profile = Constrained::allow_any(PermissionProfile::default());
let mut manager =
McpConnectionManager::new_uninitialized(&approval_policy, &permission_profile);
manager.clients.insert(
CODEX_APPS_MCP_SERVER_NAME.to_string(),
AsyncManagedClient {
@@ -712,8 +716,9 @@ async fn list_all_tools_does_not_block_when_startup_snapshot_cache_hit_is_empty(
.boxed()
.shared();
let approval_policy = Constrained::allow_any(AskForApproval::OnFailure);
let sandbox_policy = Constrained::allow_any(SandboxPolicy::new_read_only_policy());
let mut manager = McpConnectionManager::new_uninitialized(&approval_policy, &sandbox_policy);
let permission_profile = Constrained::allow_any(PermissionProfile::default());
let mut manager =
McpConnectionManager::new_uninitialized(&approval_policy, &permission_profile);
manager.clients.insert(
CODEX_APPS_MCP_SERVER_NAME.to_string(),
AsyncManagedClient {
@@ -744,8 +749,9 @@ async fn list_all_tools_uses_startup_snapshot_when_client_startup_fails() {
.boxed()
.shared();
let approval_policy = Constrained::allow_any(AskForApproval::OnFailure);
let sandbox_policy = Constrained::allow_any(SandboxPolicy::new_read_only_policy());
let mut manager = McpConnectionManager::new_uninitialized(&approval_policy, &sandbox_policy);
let permission_profile = Constrained::allow_any(PermissionProfile::default());
let mut manager =
McpConnectionManager::new_uninitialized(&approval_policy, &permission_profile);
let startup_complete = Arc::new(std::sync::atomic::AtomicBool::new(true));
manager.clients.insert(
CODEX_APPS_MCP_SERVER_NAME.to_string(),
+2 -1
View File
@@ -35,10 +35,11 @@ pub(crate) async fn apply_patch(
file_system_sandbox_policy: &FileSystemSandboxPolicy,
action: ApplyPatchAction,
) -> InternalApplyPatchInvocation {
let sandbox_policy = turn_context.sandbox_policy();
match assess_patch_safety(
&action,
turn_context.approval_policy.value(),
turn_context.sandbox_policy.get(),
&sandbox_policy,
file_system_sandbox_policy,
&turn_context.cwd,
turn_context.windows_sandbox_level,
+14 -30
View File
@@ -776,7 +776,7 @@ async fn default_permissions_profile_populates_runtime_sandbox_policy() -> std::
let memories_root = codex_home.path().join("memories").abs();
assert_eq!(
config.permissions.file_system_sandbox_policy,
config.permissions.file_system_sandbox_policy(),
FileSystemSandboxPolicy::restricted(vec![
FileSystemSandboxEntry {
path: FileSystemPath::Special {
@@ -814,7 +814,7 @@ async fn default_permissions_profile_populates_runtime_sandbox_policy() -> std::
}
);
assert_eq!(
config.permissions.network_sandbox_policy,
config.permissions.network_sandbox_policy(),
NetworkSandboxPolicy::Restricted
);
Ok(())
@@ -1082,7 +1082,7 @@ async fn project_root_glob_none_compiles_to_filesystem_pattern_entry() -> std::i
assert_eq!(
config
.permissions
.file_system_sandbox_policy
.file_system_sandbox_policy()
.glob_scan_max_depth,
Some(2)
);
@@ -1092,7 +1092,7 @@ async fn project_root_glob_none_compiles_to_filesystem_pattern_entry() -> std::i
assert!(
config
.permissions
.file_system_sandbox_policy
.file_system_sandbox_policy()
.entries
.contains(&FileSystemSandboxEntry {
path: FileSystemPath::GlobPattern {
@@ -1104,7 +1104,7 @@ async fn project_root_glob_none_compiles_to_filesystem_pattern_entry() -> std::i
assert!(
!config
.permissions
.file_system_sandbox_policy
.file_system_sandbox_policy()
.entries
.iter()
.any(|entry| matches!(
@@ -1304,7 +1304,7 @@ async fn permissions_profiles_allow_unknown_special_paths() -> std::io::Result<(
.await?;
assert_eq!(
config.permissions.file_system_sandbox_policy,
config.permissions.file_system_sandbox_policy(),
FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry {
path: FileSystemPath::Special {
value: FileSystemSpecialPath::unknown(
@@ -1350,7 +1350,7 @@ async fn permissions_profiles_allow_unknown_special_paths_with_nested_entries()
.await?;
assert_eq!(
config.permissions.file_system_sandbox_policy,
config.permissions.file_system_sandbox_policy(),
FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry {
path: FileSystemPath::Special {
value: FileSystemSpecialPath::unknown(":future_special_path", Some("docs".into())),
@@ -1377,7 +1377,7 @@ async fn permissions_profiles_allow_missing_filesystem_with_warning() -> std::io
.await?;
assert_eq!(
config.permissions.file_system_sandbox_policy,
config.permissions.file_system_sandbox_policy(),
FileSystemSandboxPolicy::restricted(Vec::new())
);
assert_eq!(
@@ -1408,7 +1408,7 @@ async fn permissions_profiles_allow_empty_filesystem_with_warning() -> std::io::
.await?;
assert_eq!(
config.permissions.file_system_sandbox_policy,
config.permissions.file_system_sandbox_policy(),
FileSystemSandboxPolicy::restricted(Vec::new())
);
assert!(
@@ -1505,7 +1505,7 @@ async fn permissions_profiles_allow_network_enablement() -> std::io::Result<()>
.await?;
assert!(
config.permissions.network_sandbox_policy.is_enabled(),
config.permissions.network_sandbox_policy().is_enabled(),
"expected network sandbox policy to be enabled",
);
assert!(
@@ -1800,20 +1800,20 @@ exclude_slash_tmp = true
let sandbox_policy = config.permissions.sandbox_policy.get();
assert_eq!(
config.permissions.file_system_sandbox_policy,
config.permissions.file_system_sandbox_policy(),
FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(sandbox_policy, cwd.path()),
"case `{name}` should preserve filesystem semantics from legacy config"
);
assert_eq!(
config.permissions.network_sandbox_policy,
config.permissions.network_sandbox_policy(),
NetworkSandboxPolicy::from(sandbox_policy),
"case `{name}` should preserve network semantics from legacy config"
);
assert_eq!(
config
.permissions
.file_system_sandbox_policy
.to_legacy_sandbox_policy(config.permissions.network_sandbox_policy, cwd.path())
.file_system_sandbox_policy()
.to_legacy_sandbox_policy(config.permissions.network_sandbox_policy(), cwd.path())
.unwrap_or_else(|err| panic!("case `{name}` should round-trip: {err}")),
sandbox_policy.clone(),
"case `{name}` should round-trip through split policies without drift"
@@ -5449,10 +5449,6 @@ async fn test_precedence_fixture_with_o3_profile() -> std::io::Result<()> {
approval_policy: Constrained::allow_any(AskForApproval::Never),
permission_profile: Constrained::allow_any(PermissionProfile::read_only()),
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(),
@@ -5647,10 +5643,6 @@ async fn test_precedence_fixture_with_gpt3_profile() -> std::io::Result<()> {
approval_policy: Constrained::allow_any(AskForApproval::UnlessTrusted),
permission_profile: Constrained::allow_any(PermissionProfile::read_only()),
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(),
@@ -5799,10 +5791,6 @@ async fn test_precedence_fixture_with_zdr_profile() -> std::io::Result<()> {
approval_policy: Constrained::allow_any(AskForApproval::OnFailure),
permission_profile: Constrained::allow_any(PermissionProfile::read_only()),
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(),
@@ -5936,10 +5924,6 @@ async fn test_precedence_fixture_with_gpt5_profile() -> std::io::Result<()> {
approval_policy: Constrained::allow_any(AskForApproval::OnFailure),
permission_profile: Constrained::allow_any(PermissionProfile::read_only()),
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(),
+4 -22
View File
@@ -200,18 +200,6 @@ pub struct Permissions {
/// Legacy projection retained while runtime call sites migrate to
/// `permission_profile`.
pub sandbox_policy: Constrained<SandboxPolicy>,
/// Effective filesystem sandbox policy, including entries that cannot yet
/// be fully represented by the legacy [`SandboxPolicy`] projection.
///
/// Runtime projection retained while callers migrate to
/// `permission_profile`.
pub file_system_sandbox_policy: FileSystemSandboxPolicy,
/// Effective network sandbox policy split out from the legacy
/// [`SandboxPolicy`] projection.
///
/// Runtime projection retained while callers migrate to
/// `permission_profile`.
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.
@@ -239,14 +227,14 @@ impl Permissions {
self.permission_profile.get().clone()
}
/// Effective filesystem sandbox policy projection.
/// Effective filesystem sandbox policy derived from the canonical profile.
pub fn file_system_sandbox_policy(&self) -> FileSystemSandboxPolicy {
self.file_system_sandbox_policy.clone()
self.permission_profile.get().file_system_sandbox_policy()
}
/// Effective network sandbox policy projection.
/// Effective network sandbox policy derived from the canonical profile.
pub fn network_sandbox_policy(&self) -> NetworkSandboxPolicy {
self.network_sandbox_policy
self.permission_profile.get().network_sandbox_policy()
}
/// Replace permissions from a legacy sandbox policy and keep every
@@ -269,8 +257,6 @@ impl Permissions {
self.sandbox_policy.set(sandbox_policy)?;
self.permission_profile.set(permission_profile)?;
self.file_system_sandbox_policy = file_system_sandbox_policy;
self.network_sandbox_policy = network_sandbox_policy;
Ok(())
}
@@ -294,8 +280,6 @@ impl Permissions {
self.permission_profile.set(permission_profile)?;
self.sandbox_policy.set(sandbox_policy)?;
self.file_system_sandbox_policy = file_system_sandbox_policy;
self.network_sandbox_policy = network_sandbox_policy;
Ok(())
}
}
@@ -2493,8 +2477,6 @@ impl Config {
approval_policy: constrained_approval_policy.value,
permission_profile: constrained_permission_profile,
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,
@@ -89,7 +89,7 @@ async fn restricted_read_implicitly_allows_helper_executables() -> std::io::Resu
let expected_zsh = AbsolutePathBuf::try_from(zsh_path)?;
let expected_allowed_arg0_dir = AbsolutePathBuf::try_from(allowed_arg0_dir)?;
let expected_sibling_arg0_dir = AbsolutePathBuf::try_from(sibling_arg0_dir)?;
let policy = &config.permissions.file_system_sandbox_policy;
let policy = config.permissions.file_system_sandbox_policy();
assert!(
policy.can_read_path_with_cwd(expected_zsh.as_path(), &cwd),
+2 -2
View File
@@ -17,7 +17,7 @@ use codex_connectors::DirectoryListResponse;
use codex_exec_server::EnvironmentManager;
use codex_exec_server::EnvironmentManagerArgs;
use codex_exec_server::ExecServerRuntimePaths;
use codex_protocol::protocol::SandboxPolicy;
use codex_protocol::models::PermissionProfile;
use codex_tools::DiscoverableTool;
use rmcp::model::ToolAnnotations;
use serde::Deserialize;
@@ -274,7 +274,7 @@ pub async fn list_accessible_connectors_from_mcp_tools_with_environment_manager(
&config.permissions.approval_policy,
INITIAL_SUBMIT_ID.to_owned(),
tx_event,
SandboxPolicy::new_read_only_policy(),
PermissionProfile::default(),
McpRuntimeEnvironment::new(environment, config.cwd.to_path_buf()),
config.codex_home.to_path_buf(),
codex_apps_tools_cache_key(auth.as_ref()),
+1 -1
View File
@@ -221,7 +221,7 @@ async fn should_install_mcp_dependencies(
) -> bool {
if mcp_permission_prompt_is_auto_approved(
turn_context.approval_policy.value(),
turn_context.sandbox_policy.get(),
&turn_context.permission_profile(),
) {
return true;
}
+2 -2
View File
@@ -524,7 +524,7 @@ async fn augment_mcp_tool_request_meta_with_sandbox_state(
let sandbox_state = serde_json::to_value(SandboxState {
permission_profile: Some(turn_context.permission_profile()),
sandbox_policy: turn_context.sandbox_policy.get().clone(),
sandbox_policy: turn_context.sandbox_policy(),
codex_linux_sandbox_exe: turn_context.codex_linux_sandbox_exe.clone(),
sandbox_cwd: turn_context.cwd.to_path_buf(),
use_legacy_landlock: turn_context.features.use_legacy_landlock(),
@@ -830,7 +830,7 @@ async fn maybe_request_mcp_tool_approval(
) -> Option<McpToolApprovalDecision> {
if mcp_permission_prompt_is_auto_approved(
turn_context.approval_policy.value(),
turn_context.sandbox_policy.get(),
&turn_context.permission_profile(),
) {
return None;
}
+2 -5
View File
@@ -16,8 +16,8 @@ use codex_config::types::McpServerToolConfig;
use codex_hooks::Hooks;
use codex_hooks::HooksConfig;
use codex_model_provider::create_model_provider;
use codex_protocol::models::PermissionProfile;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::SandboxPolicy;
use core_test_support::PathExt;
use core_test_support::responses::ev_assistant_message;
use core_test_support::responses::ev_completed;
@@ -2162,10 +2162,7 @@ async fn full_access_mode_skips_arc_monitor_for_all_approval_modes() {
.approval_policy
.set(AskForApproval::Never)
.expect("test setup should allow updating approval policy");
turn_context
.sandbox_policy
.set(SandboxPolicy::DangerFullAccess)
.expect("test setup should allow updating sandbox policy");
turn_context.permission_profile = PermissionProfile::Disabled;
let mut config = (*turn_context.config).clone();
config.chatgpt_base_url = server.uri();
turn_context.config = Arc::new(config);
+33 -23
View File
@@ -432,6 +432,7 @@ mod phase2 {
use codex_login::CodexAuth;
use codex_protocol::AgentPath;
use codex_protocol::ThreadId;
use codex_protocol::models::PermissionProfile;
use codex_protocol::permissions::FileSystemSandboxPolicy;
use codex_protocol::permissions::NetworkSandboxPolicy;
use codex_protocol::protocol::AskForApproval;
@@ -482,8 +483,14 @@ mod phase2 {
codex_utils_absolute_path::AbsolutePathBuf::from_absolute_path(codex_home.path())
.expect("codex home is absolute");
config.cwd = config.codex_home.clone();
config.permissions.file_system_sandbox_policy = FileSystemSandboxPolicy::unrestricted();
config.permissions.network_sandbox_policy = NetworkSandboxPolicy::Enabled;
let permission_profile = PermissionProfile::from_runtime_permissions(
&FileSystemSandboxPolicy::unrestricted(),
NetworkSandboxPolicy::Enabled,
);
config
.permissions
.set_permission_profile(permission_profile, config.cwd.as_path())
.expect("permissions are configurable");
configure(&mut config);
let config = Arc::new(config);
@@ -712,14 +719,16 @@ mod phase2 {
memory_root(&harness.config.codex_home).as_path()
);
match &config_snapshot.sandbox_policy {
SandboxPolicy::WorkspaceWrite {
writable_roots,
network_access,
..
} => {
SandboxPolicy::WorkspaceWrite { network_access, .. } => {
assert!(!*network_access);
let effective_writable_roots: Vec<_> = config_snapshot
.sandbox_policy
.get_writable_roots_with_cwd(config_snapshot.cwd.as_path())
.into_iter()
.map(|root| root.root)
.collect();
pretty_assertions::assert_eq!(
writable_roots.as_slice(),
effective_writable_roots.as_slice(),
[memory_root(&harness.config.codex_home)],
"consolidation subagent should only be able to write the memory root"
);
@@ -740,34 +749,35 @@ mod phase2 {
"memory consolidation should not be registered in the root collab agent registry"
);
let turn_context = subagent.codex.session.new_default_turn().await;
pretty_assertions::assert_eq!(
turn_context.file_system_sandbox_policy,
let file_system_sandbox_policy = turn_context.file_system_sandbox_policy();
let legacy_file_system_sandbox_policy =
FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(
&config_snapshot.sandbox_policy,
config_snapshot.cwd.as_path(),
);
assert!(
file_system_sandbox_policy.is_semantically_equivalent_to(
&legacy_file_system_sandbox_policy,
config_snapshot.cwd.as_path(),
),
"consolidation subagent split filesystem policy should match the memory-root legacy policy"
);
assert!(
turn_context
.file_system_sandbox_policy
.can_write_path_with_cwd(
memory_root(&harness.config.codex_home).as_path(),
config_snapshot.cwd.as_path(),
),
file_system_sandbox_policy.can_write_path_with_cwd(
memory_root(&harness.config.codex_home).as_path(),
config_snapshot.cwd.as_path(),
),
"consolidation subagent should be able to write the memory root"
);
assert!(
!turn_context
.file_system_sandbox_policy
.can_write_path_with_cwd(
harness.config.codex_home.join("config.toml").as_path(),
config_snapshot.cwd.as_path(),
),
!file_system_sandbox_policy.can_write_path_with_cwd(
harness.config.codex_home.join("config.toml").as_path(),
config_snapshot.cwd.as_path(),
),
"consolidation subagent should not inherit codex_home write access"
);
pretty_assertions::assert_eq!(
turn_context.network_sandbox_policy,
turn_context.network_sandbox_policy(),
NetworkSandboxPolicy::Restricted,
"consolidation subagent split network policy should preserve no-network sandboxing"
);
+1 -1
View File
@@ -233,7 +233,7 @@ impl Session {
&turn_context.approval_policy,
turn_context.sub_id.clone(),
self.get_tx_event(),
turn_context.sandbox_policy.get().clone(),
turn_context.permission_profile(),
McpRuntimeEnvironment::new(
turn_context
.environment
+4 -8
View File
@@ -125,7 +125,6 @@ use codex_rollout::state_db;
use codex_rollout_trace::AgentResultTracePayload;
use codex_rollout_trace::ThreadStartedTraceMetadata;
use codex_rollout_trace::ThreadTraceContext;
use codex_sandboxing::compatibility_sandbox_policy_for_permission_profile;
use codex_sandboxing::policy_transforms::intersect_permission_profiles;
use codex_shell_command::parse_command::parse_command;
use codex_terminal_detection::user_agent;
@@ -606,9 +605,6 @@ impl Codex {
approval_policy: config.permissions.approval_policy.clone(),
approvals_reviewer: config.approvals_reviewer,
permission_profile: config.permissions.permission_profile.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(),
@@ -939,8 +935,7 @@ impl Session {
return;
};
let spec = match spec
.recompute_for_sandbox_policy(session_configuration.sandbox_policy.get())
let spec = match spec.recompute_for_sandbox_policy(&session_configuration.sandbox_policy())
{
Ok(spec) => spec,
Err(err) => {
@@ -1301,8 +1296,9 @@ impl Session {
};
let previous_cwd = state.session_configuration.cwd.clone();
let sandbox_policy_changed =
state.session_configuration.sandbox_policy != updated.sandbox_policy;
let previous_sandbox_policy = state.session_configuration.sandbox_policy();
let updated_sandbox_policy = updated.sandbox_policy();
let sandbox_policy_changed = previous_sandbox_policy != updated_sandbox_policy;
let next_cwd = updated.cwd.clone();
let codex_home = updated.codex_home.clone();
let session_source = updated.session_source.clone();
-3
View File
@@ -129,9 +129,6 @@ pub(super) async fn spawn_review_thread(
personality: parent_turn_context.personality,
approval_policy: parent_turn_context.approval_policy.clone(),
permission_profile: parent_turn_context.permission_profile(),
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(),
@@ -67,7 +67,7 @@ async fn record_initial_history_resumed_bare_turn_context_does_not_hydrate_previ
current_date: turn_context.current_date.clone(),
timezone: turn_context.timezone.clone(),
approval_policy: turn_context.approval_policy.value(),
sandbox_policy: turn_context.sandbox_policy.get().clone(),
sandbox_policy: turn_context.sandbox_policy(),
permission_profile: None,
network: None,
file_system_sandbox_policy: None,
@@ -108,7 +108,7 @@ async fn record_initial_history_resumed_hydrates_previous_turn_settings_from_lif
current_date: turn_context.current_date.clone(),
timezone: turn_context.timezone.clone(),
approval_policy: turn_context.approval_policy.value(),
sandbox_policy: turn_context.sandbox_policy.get().clone(),
sandbox_policy: turn_context.sandbox_policy(),
permission_profile: None,
network: None,
file_system_sandbox_policy: None,
@@ -918,7 +918,7 @@ async fn record_initial_history_resumed_turn_context_after_compaction_reestablis
current_date: turn_context.current_date.clone(),
timezone: turn_context.timezone.clone(),
approval_policy: turn_context.approval_policy.value(),
sandbox_policy: turn_context.sandbox_policy.get().clone(),
sandbox_policy: turn_context.sandbox_policy(),
permission_profile: None,
network: None,
file_system_sandbox_policy: None,
@@ -996,7 +996,7 @@ async fn record_initial_history_resumed_turn_context_after_compaction_reestablis
current_date: turn_context.current_date.clone(),
timezone: turn_context.timezone.clone(),
approval_policy: turn_context.approval_policy.value(),
sandbox_policy: turn_context.sandbox_policy.get().clone(),
sandbox_policy: turn_context.sandbox_policy(),
permission_profile: None,
network: None,
file_system_sandbox_policy: None,
@@ -1027,7 +1027,7 @@ async fn record_initial_history_resumed_aborted_turn_without_id_clears_active_tu
current_date: turn_context.current_date.clone(),
timezone: turn_context.timezone.clone(),
approval_policy: turn_context.approval_policy.value(),
sandbox_policy: turn_context.sandbox_policy.get().clone(),
sandbox_policy: turn_context.sandbox_policy(),
permission_profile: None,
network: None,
file_system_sandbox_policy: None,
@@ -1142,7 +1142,7 @@ async fn record_initial_history_resumed_unmatched_abort_preserves_active_turn_fo
current_date: turn_context.current_date.clone(),
timezone: turn_context.timezone.clone(),
approval_policy: turn_context.approval_policy.value(),
sandbox_policy: turn_context.sandbox_policy.get().clone(),
sandbox_policy: turn_context.sandbox_policy(),
permission_profile: None,
network: None,
file_system_sandbox_policy: None,
@@ -1256,7 +1256,7 @@ async fn record_initial_history_resumed_trailing_incomplete_turn_compaction_clea
current_date: turn_context.current_date.clone(),
timezone: turn_context.timezone.clone(),
approval_policy: turn_context.approval_policy.value(),
sandbox_policy: turn_context.sandbox_policy.get().clone(),
sandbox_policy: turn_context.sandbox_policy(),
permission_profile: None,
network: None,
file_system_sandbox_policy: None,
@@ -1408,7 +1408,7 @@ async fn record_initial_history_resumed_replaced_incomplete_compacted_turn_clear
current_date: turn_context.current_date.clone(),
timezone: turn_context.timezone.clone(),
approval_policy: turn_context.approval_policy.value(),
sandbox_policy: turn_context.sandbox_policy.get().clone(),
sandbox_policy: turn_context.sandbox_policy(),
permission_profile: None,
network: None,
file_system_sandbox_policy: None,
+67 -48
View File
@@ -1,5 +1,7 @@
use super::*;
use crate::goals::GoalRuntimeState;
use codex_protocol::permissions::FileSystemPath;
use codex_protocol::permissions::FileSystemSpecialPath;
use tokio::sync::Semaphore;
/// Context for an initialized model agent
@@ -58,12 +60,6 @@ pub(crate) struct SessionConfiguration {
pub(super) approvals_reviewer: ApprovalsReviewer,
/// Canonical permission profile for the session.
pub(super) permission_profile: Constrained<PermissionProfile>,
/// Legacy sandbox projection retained while lower-level callers migrate.
pub(super) sandbox_policy: Constrained<SandboxPolicy>,
/// Filesystem sandbox projection of `permission_profile`.
pub(super) file_system_sandbox_policy: FileSystemSandboxPolicy,
/// Network sandbox projection of `permission_profile`.
pub(super) network_sandbox_policy: NetworkSandboxPolicy,
pub(super) windows_sandbox_level: WindowsSandboxLevel,
/// Absolute working directory that should be treated as the *root* of the
@@ -102,12 +98,25 @@ impl SessionConfiguration {
}
pub(super) fn sandbox_policy(&self) -> SandboxPolicy {
self.sandbox_policy.get().clone()
self.permission_profile()
.to_legacy_sandbox_policy(&self.cwd)
.unwrap_or_else(|_| {
let file_system_sandbox_policy = self.file_system_sandbox_policy();
codex_sandboxing::compatibility_sandbox_policy_for_permission_profile(
self.permission_profile.get(),
&file_system_sandbox_policy,
self.network_sandbox_policy(),
&self.cwd,
)
})
}
#[cfg(test)]
pub(super) fn file_system_sandbox_policy(&self) -> FileSystemSandboxPolicy {
self.file_system_sandbox_policy.clone()
self.permission_profile.get().file_system_sandbox_policy()
}
pub(super) fn network_sandbox_policy(&self) -> NetworkSandboxPolicy {
self.permission_profile.get().network_sandbox_policy()
}
pub(super) fn thread_config_snapshot(&self) -> ThreadConfigSnapshot {
@@ -129,11 +138,30 @@ impl SessionConfiguration {
pub(crate) fn apply(&self, updates: &SessionSettingsUpdate) -> ConstraintResult<Self> {
let mut next_configuration = self.clone();
let file_system_policy_matches_legacy = self.file_system_sandbox_policy
== FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(
self.sandbox_policy.get(),
let current_sandbox_policy = self.sandbox_policy();
let current_file_system_sandbox_policy = self.file_system_sandbox_policy();
let current_network_sandbox_policy = self.network_sandbox_policy();
let legacy_file_system_projection =
FileSystemSandboxPolicy::from_legacy_sandbox_policy_preserving_deny_entries(
&current_sandbox_policy,
&self.cwd,
&current_file_system_sandbox_policy,
);
let file_system_policy_matches_legacy = current_file_system_sandbox_policy
.is_semantically_equivalent_to(&legacy_file_system_projection, &self.cwd);
let file_system_policy_has_rebindable_cwd_write = current_file_system_sandbox_policy
.entries
.iter()
.any(|entry| {
entry.access.can_write()
&& matches!(
&entry.path,
FileSystemPath::Special {
value: FileSystemSpecialPath::CurrentWorkingDirectory
| FileSystemSpecialPath::ProjectRoots { subpath: None },
}
)
});
if let Some(collaboration_mode) = updates.collaboration_mode.clone() {
next_configuration.collaboration_mode = collaboration_mode;
}
@@ -181,42 +209,41 @@ impl SessionConfiguration {
if let Some(permission_profile) = updates.permission_profile.clone() {
next_configuration.set_permission_profile_projection(
permission_profile,
Some(&self.file_system_sandbox_policy),
Some(&current_file_system_sandbox_policy),
)?;
} else if let Some(sandbox_policy) = updates.sandbox_policy.clone() {
next_configuration.sandbox_policy.set(sandbox_policy)?;
next_configuration.file_system_sandbox_policy =
let file_system_sandbox_policy =
FileSystemSandboxPolicy::from_legacy_sandbox_policy_preserving_deny_entries(
next_configuration.sandbox_policy.get(),
&sandbox_policy,
&next_configuration.cwd,
&self.file_system_sandbox_policy,
&current_file_system_sandbox_policy,
);
next_configuration.network_sandbox_policy =
NetworkSandboxPolicy::from(next_configuration.sandbox_policy.get());
let network_sandbox_policy = NetworkSandboxPolicy::from(&sandbox_policy);
next_configuration.permission_profile.set(
PermissionProfile::from_runtime_permissions_with_enforcement(
SandboxEnforcement::from_legacy_sandbox_policy(
next_configuration.sandbox_policy.get(),
),
&next_configuration.file_system_sandbox_policy,
next_configuration.network_sandbox_policy,
SandboxEnforcement::from_legacy_sandbox_policy(&sandbox_policy),
&file_system_sandbox_policy,
network_sandbox_policy,
),
)?;
} else if cwd_changed && file_system_policy_matches_legacy {
} else if cwd_changed
&& file_system_policy_matches_legacy
&& file_system_policy_has_rebindable_cwd_write
{
// Preserve richer split policies across cwd-only updates; only
// rederive when the session is already using the legacy bridge.
next_configuration.file_system_sandbox_policy =
FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(
next_configuration.sandbox_policy.get(),
// rederive when the session is already using a structurally
// cwd-bound legacy bridge.
let file_system_sandbox_policy =
FileSystemSandboxPolicy::from_legacy_sandbox_policy_preserving_deny_entries(
&current_sandbox_policy,
&next_configuration.cwd,
&current_file_system_sandbox_policy,
);
next_configuration.permission_profile.set(
PermissionProfile::from_runtime_permissions_with_enforcement(
SandboxEnforcement::from_legacy_sandbox_policy(
next_configuration.sandbox_policy.get(),
),
&next_configuration.file_system_sandbox_policy,
next_configuration.network_sandbox_policy,
SandboxEnforcement::from_legacy_sandbox_policy(&current_sandbox_policy),
&file_system_sandbox_policy,
current_network_sandbox_policy,
),
)?;
}
@@ -247,16 +274,7 @@ impl SessionConfiguration {
&file_system_sandbox_policy,
network_sandbox_policy,
);
let sandbox_policy = compatibility_sandbox_policy_for_permission_profile(
&effective_permission_profile,
&file_system_sandbox_policy,
network_sandbox_policy,
self.cwd.as_path(),
);
self.permission_profile.set(effective_permission_profile)?;
self.sandbox_policy.set(sandbox_policy)?;
self.file_system_sandbox_policy = file_system_sandbox_policy;
self.network_sandbox_policy = network_sandbox_policy;
Ok(())
}
}
@@ -487,7 +505,7 @@ impl Session {
model: session_configuration.collaboration_mode.model().to_string(),
provider_name: config.model_provider_id.clone(),
approval_policy: session_configuration.approval_policy.value().to_string(),
sandbox_policy: format!("{:?}", session_configuration.sandbox_policy.get()),
sandbox_policy: format!("{:?}", session_configuration.sandbox_policy()),
};
let rollout_thread_trace = if matches!(
session_configuration.session_source,
@@ -768,7 +786,7 @@ impl Session {
// setup is straightforward enough and performs well.
mcp_connection_manager: Arc::new(RwLock::new(McpConnectionManager::new_uninitialized(
&config.permissions.approval_policy,
&config.permissions.sandbox_policy,
&config.permissions.permission_profile,
))),
mcp_startup_cancellation_token: Mutex::new(CancellationToken::new()),
unified_exec_manager: UnifiedExecProcessManager::new(
@@ -847,6 +865,7 @@ impl Session {
// Dispatch the SessionConfiguredEvent first and then report any errors.
// If resuming, include converted initial messages in the payload so UIs can render them immediately.
let initial_messages = initial_history.get_event_msgs();
let session_sandbox_policy = session_configuration.sandbox_policy();
let events = std::iter::once(Event {
id: INITIAL_SUBMIT_ID.to_owned(),
msg: EventMsg::SessionConfigured(SessionConfiguredEvent {
@@ -858,7 +877,7 @@ impl Session {
service_tier: session_configuration.service_tier,
approval_policy: session_configuration.approval_policy.value(),
approvals_reviewer: session_configuration.approvals_reviewer,
sandbox_policy: session_configuration.sandbox_policy.get().clone(),
sandbox_policy: session_sandbox_policy.clone(),
permission_profile: Some(session_configuration.permission_profile()),
cwd: session_configuration.cwd.clone(),
reasoning_effort: session_configuration.collaboration_mode.reasoning_effort(),
@@ -867,7 +886,7 @@ impl Session {
initial_messages,
network_proxy: session_network_proxy.filter(|_| {
Self::managed_network_proxy_active_for_sandbox_policy(
session_configuration.sandbox_policy.get(),
&session_sandbox_policy,
)
}),
rollout_path,
@@ -901,7 +920,7 @@ impl Session {
&session_configuration.approval_policy,
INITIAL_SUBMIT_ID.to_owned(),
tx_event.clone(),
session_configuration.sandbox_policy.get().clone(),
session_configuration.permission_profile(),
McpRuntimeEnvironment::new(
sess.services
.environment_manager
+148 -84
View File
@@ -38,6 +38,7 @@ use codex_protocol::models::FileSystemPermissions;
use codex_protocol::models::FunctionCallOutputBody;
use codex_protocol::models::FunctionCallOutputPayload;
use codex_protocol::models::PermissionProfile;
use codex_protocol::models::SandboxEnforcement;
use codex_protocol::permissions::FileSystemAccessMode;
use codex_protocol::permissions::FileSystemPath;
use codex_protocol::permissions::FileSystemSandboxEntry;
@@ -779,11 +780,19 @@ async fn new_turn_refreshes_managed_network_proxy_for_sandbox_change() -> anyhow
let mut state = session.state.lock().await;
let mut config = (*state.session_configuration.original_config_do_not_use).clone();
config.permissions.network = Some(spec);
config.permissions.sandbox_policy =
codex_config::Constrained::allow_any(initial_policy.clone());
let cwd = config.cwd.clone();
config
.permissions
.set_legacy_sandbox_policy(initial_policy.clone(), cwd.as_path())
.expect("test setup should allow sandbox policy");
state.session_configuration.original_config_do_not_use = Arc::new(config);
state.session_configuration.sandbox_policy =
codex_config::Constrained::allow_any(initial_policy);
state
.session_configuration
.permission_profile
.set(PermissionProfile::from_legacy_sandbox_policy(
&initial_policy,
))
.expect("test setup should allow permission profile");
}
session.services.network_proxy = Some(started_proxy);
@@ -829,6 +838,8 @@ async fn danger_full_access_turns_do_not_expose_managed_network_proxy() -> anyho
let session = make_session_with_config(move |config| {
config.permissions.sandbox_policy =
codex_config::Constrained::allow_any(SandboxPolicy::DangerFullAccess);
config.permissions.permission_profile =
codex_config::Constrained::allow_any(PermissionProfile::Disabled);
config.permissions.network = Some(network_spec);
})
.await?;
@@ -892,6 +903,8 @@ async fn danger_full_access_tool_attempts_do_not_enforce_managed_network() -> an
let session = make_session_with_config(move |config| {
config.permissions.sandbox_policy =
codex_config::Constrained::allow_any(SandboxPolicy::DangerFullAccess);
config.permissions.permission_profile =
codex_config::Constrained::allow_any(PermissionProfile::Disabled);
config.permissions.network = Some(network_spec);
let layers = config
@@ -1492,12 +1505,10 @@ async fn session_configured_reports_permission_profile_for_external_sandbox() ->
let expected_sandbox_policy = sandbox_policy.clone();
let mut builder = test_codex().with_config(move |config| {
config.permissions.sandbox_policy = codex_config::Constrained::allow_any(sandbox_policy);
config.permissions.file_system_sandbox_policy = FileSystemSandboxPolicy::external_sandbox();
config.permissions.network_sandbox_policy = NetworkSandboxPolicy::Restricted;
config.permissions.permission_profile =
codex_config::Constrained::allow_any(PermissionProfile::from_runtime_permissions(
&config.permissions.file_system_sandbox_policy,
config.permissions.network_sandbox_policy,
&FileSystemSandboxPolicy::external_sandbox(),
NetworkSandboxPolicy::Restricted,
));
});
@@ -1654,7 +1665,7 @@ async fn record_initial_history_forked_hydrates_previous_turn_settings() {
current_date: turn_context.current_date.clone(),
timezone: turn_context.timezone.clone(),
approval_policy: turn_context.approval_policy.value(),
sandbox_policy: turn_context.sandbox_policy.get().clone(),
sandbox_policy: turn_context.sandbox_policy(),
permission_profile: None,
network: None,
file_system_sandbox_policy: None,
@@ -2253,9 +2264,6 @@ async fn set_rate_limits_retains_previous_credits() {
approval_policy: config.permissions.approval_policy.clone(),
approvals_reviewer: config.approvals_reviewer,
permission_profile: config.permissions.permission_profile.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(),
@@ -2358,9 +2366,6 @@ async fn set_rate_limits_updates_plan_type_when_present() {
approval_policy: config.permissions.approval_policy.clone(),
approvals_reviewer: config.approvals_reviewer,
permission_profile: config.permissions.permission_profile.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(),
@@ -2808,9 +2813,6 @@ pub(crate) async fn make_session_configuration_for_tests() -> SessionConfigurati
approval_policy: config.permissions.approval_policy.clone(),
approvals_reviewer: config.approvals_reviewer,
permission_profile: config.permissions.permission_profile.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(),
@@ -2840,7 +2842,7 @@ fn turn_environments_for_tests(
}
#[tokio::test]
async fn session_configuration_apply_preserves_split_file_system_policy_on_cwd_only_update() {
async fn session_configuration_apply_preserves_profile_file_system_policy_on_cwd_only_update() {
let mut session_configuration = make_session_configuration_for_tests().await;
let workspace = tempfile::tempdir().expect("create temp dir");
let project_root = workspace.path().join("project");
@@ -2850,14 +2852,13 @@ async fn session_configuration_apply_preserves_split_file_system_policy_on_cwd_o
let docs_dir = docs_dir.abs();
session_configuration.cwd = original_cwd.abs();
session_configuration.sandbox_policy =
codex_config::Constrained::allow_any(SandboxPolicy::WorkspaceWrite {
writable_roots: Vec::new(),
network_access: false,
exclude_tmpdir_env_var: true,
exclude_slash_tmp: true,
});
session_configuration.file_system_sandbox_policy = FileSystemSandboxPolicy::restricted(vec![
let sandbox_policy = SandboxPolicy::WorkspaceWrite {
writable_roots: Vec::new(),
network_access: false,
exclude_tmpdir_env_var: true,
exclude_slash_tmp: true,
};
let file_system_sandbox_policy = FileSystemSandboxPolicy::restricted(vec![
FileSystemSandboxEntry {
path: FileSystemPath::Special {
value: FileSystemSpecialPath::CurrentWorkingDirectory,
@@ -2869,6 +2870,14 @@ async fn session_configuration_apply_preserves_split_file_system_policy_on_cwd_o
access: FileSystemAccessMode::Read,
},
]);
let network_sandbox_policy = NetworkSandboxPolicy::from(&sandbox_policy);
session_configuration.permission_profile = codex_config::Constrained::allow_any(
PermissionProfile::from_runtime_permissions_with_enforcement(
SandboxEnforcement::from_legacy_sandbox_policy(&sandbox_policy),
&file_system_sandbox_policy,
network_sandbox_policy,
),
);
let updated = session_configuration
.apply(&SessionSettingsUpdate {
@@ -2878,8 +2887,8 @@ async fn session_configuration_apply_preserves_split_file_system_policy_on_cwd_o
.expect("cwd-only update should succeed");
assert_eq!(
updated.file_system_sandbox_policy,
session_configuration.file_system_sandbox_policy
updated.file_system_sandbox_policy(),
file_system_sandbox_policy
);
}
@@ -2890,8 +2899,6 @@ async fn session_configuration_apply_permission_profile_preserves_existing_deny_
session_configuration.cwd = cwd.path().abs();
let workspace_policy = SandboxPolicy::new_workspace_write_policy();
session_configuration.sandbox_policy =
codex_config::Constrained::allow_any(workspace_policy.clone());
let deny_entry = FileSystemSandboxEntry {
path: FileSystemPath::GlobPattern {
pattern: "**/*.env".to_string(),
@@ -2905,7 +2912,13 @@ async fn session_configuration_apply_permission_profile_preserves_existing_deny_
);
existing_file_system_policy.glob_scan_max_depth = Some(2);
existing_file_system_policy.entries.push(deny_entry.clone());
session_configuration.file_system_sandbox_policy = existing_file_system_policy;
session_configuration.permission_profile = codex_config::Constrained::allow_any(
PermissionProfile::from_runtime_permissions_with_enforcement(
SandboxEnforcement::from_legacy_sandbox_policy(&workspace_policy),
&existing_file_system_policy,
NetworkSandboxPolicy::Restricted,
),
);
let requested_file_system_policy = FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(
&workspace_policy,
@@ -2926,7 +2939,7 @@ async fn session_configuration_apply_permission_profile_preserves_existing_deny_
expected_file_system_policy.glob_scan_max_depth = Some(2);
expected_file_system_policy.entries.push(deny_entry);
assert_eq!(
updated.file_system_sandbox_policy,
updated.file_system_sandbox_policy(),
expected_file_system_policy
);
}
@@ -3070,18 +3083,23 @@ async fn session_configuration_apply_rederives_legacy_file_system_policy_on_cwd_
let project_root = workspace.path().join("project");
let original_cwd = project_root.join("subdir");
session_configuration.cwd = original_cwd.abs();
session_configuration.sandbox_policy =
codex_config::Constrained::allow_any(SandboxPolicy::WorkspaceWrite {
writable_roots: Vec::new(),
network_access: false,
exclude_tmpdir_env_var: true,
exclude_slash_tmp: true,
});
session_configuration.file_system_sandbox_policy =
FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(
session_configuration.sandbox_policy.get(),
&session_configuration.cwd,
);
let sandbox_policy = SandboxPolicy::WorkspaceWrite {
writable_roots: Vec::new(),
network_access: false,
exclude_tmpdir_env_var: true,
exclude_slash_tmp: true,
};
let file_system_sandbox_policy = FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(
&sandbox_policy,
&session_configuration.cwd,
);
session_configuration.permission_profile = codex_config::Constrained::allow_any(
PermissionProfile::from_runtime_permissions_with_enforcement(
SandboxEnforcement::from_legacy_sandbox_policy(&sandbox_policy),
&file_system_sandbox_policy,
NetworkSandboxPolicy::from(&sandbox_policy),
),
);
let updated = session_configuration
.apply(&SessionSettingsUpdate {
@@ -3090,12 +3108,73 @@ async fn session_configuration_apply_rederives_legacy_file_system_policy_on_cwd_
})
.expect("cwd-only update should succeed");
let expected_file_system_policy = FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(
&updated.sandbox_policy(),
&project_root,
);
assert!(
updated
.file_system_sandbox_policy()
.is_semantically_equivalent_to(&expected_file_system_policy, &project_root),
"cwd-only update should rederive the legacy filesystem policy for the new cwd"
);
}
#[tokio::test]
async fn session_configuration_apply_preserves_absolute_cwd_write_root_on_cwd_update() {
let mut session_configuration = make_session_configuration_for_tests().await;
let workspace = tempfile::tempdir().expect("create temp dir");
let original_cwd = workspace.path().join("repo-a");
let next_cwd = workspace.path().join("repo-b");
std::fs::create_dir_all(&original_cwd).expect("create original cwd");
std::fs::create_dir_all(&next_cwd).expect("create next cwd");
let original_cwd = original_cwd.abs();
session_configuration.cwd = original_cwd.clone();
let file_system_sandbox_policy = FileSystemSandboxPolicy::restricted(vec![
FileSystemSandboxEntry {
path: FileSystemPath::Special {
value: FileSystemSpecialPath::Root,
},
access: FileSystemAccessMode::Read,
},
FileSystemSandboxEntry {
path: FileSystemPath::Path {
path: original_cwd.clone(),
},
access: FileSystemAccessMode::Write,
},
]);
session_configuration.permission_profile = codex_config::Constrained::allow_any(
PermissionProfile::from_runtime_permissions_with_enforcement(
SandboxEnforcement::Managed,
&file_system_sandbox_policy,
NetworkSandboxPolicy::Restricted,
),
);
let updated = session_configuration
.apply(&SessionSettingsUpdate {
cwd: Some(next_cwd.clone()),
..Default::default()
})
.expect("cwd-only update should succeed");
assert_eq!(
updated.file_system_sandbox_policy,
FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(
updated.sandbox_policy.get(),
&project_root,
)
updated.file_system_sandbox_policy(),
file_system_sandbox_policy
);
assert!(
updated
.file_system_sandbox_policy()
.can_write_path_with_cwd(original_cwd.as_path(), updated.cwd.as_path()),
"absolute grant to the old cwd must remain writable"
);
assert!(
!updated
.file_system_sandbox_policy()
.can_write_path_with_cwd(next_cwd.as_path(), updated.cwd.as_path()),
"cwd-only update must not reinterpret an absolute old-cwd grant as :cwd"
);
}
@@ -3170,9 +3249,6 @@ async fn session_new_fails_when_zsh_fork_enabled_without_zsh_path() {
approval_policy: config.permissions.approval_policy.clone(),
approvals_reviewer: config.approvals_reviewer,
permission_profile: config.permissions.permission_profile.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(),
@@ -3277,9 +3353,6 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
approval_policy: config.permissions.approval_policy.clone(),
approvals_reviewer: config.approvals_reviewer,
permission_profile: config.permissions.permission_profile.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(),
@@ -3325,7 +3398,7 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
let services = SessionServices {
mcp_connection_manager: Arc::new(RwLock::new(McpConnectionManager::new_uninitialized(
&config.permissions.approval_policy,
&config.permissions.sandbox_policy,
&config.permissions.permission_profile,
))),
mcp_startup_cancellation_token: Mutex::new(CancellationToken::new()),
unified_exec_manager: UnifiedExecProcessManager::new(
@@ -3492,9 +3565,6 @@ async fn make_session_with_config_and_rx(
approval_policy: config.permissions.approval_policy.clone(),
approvals_reviewer: config.approvals_reviewer,
permission_profile: config.permissions.permission_profile.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(),
@@ -4642,9 +4712,6 @@ where
approval_policy: config.permissions.approval_policy.clone(),
approvals_reviewer: config.approvals_reviewer,
permission_profile: config.permissions.permission_profile.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(),
@@ -4690,7 +4757,7 @@ where
let services = SessionServices {
mcp_connection_manager: Arc::new(RwLock::new(McpConnectionManager::new_uninitialized(
&config.permissions.approval_policy,
&config.permissions.sandbox_policy,
&config.permissions.permission_profile,
))),
mcp_startup_cancellation_token: Mutex::new(CancellationToken::new()),
unified_exec_manager: UnifiedExecProcessManager::new(
@@ -5572,7 +5639,7 @@ async fn build_initial_context_restates_realtime_start_when_reference_context_is
fn file_system_policy_with_unreadable_glob(turn_context: &TurnContext) -> FileSystemSandboxPolicy {
let mut policy = FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(
turn_context.sandbox_policy.get(),
&turn_context.sandbox_policy(),
&turn_context.cwd,
);
policy.entries.push(FileSystemSandboxEntry {
@@ -5586,12 +5653,7 @@ fn file_system_policy_with_unreadable_glob(turn_context: &TurnContext) -> FileSy
#[tokio::test]
async fn turn_context_item_omits_legacy_equivalent_file_system_sandbox_policy() {
let (_session, mut turn_context) = make_session_and_context().await;
turn_context.file_system_sandbox_policy =
FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(
turn_context.sandbox_policy.get(),
&turn_context.cwd,
);
let (_session, turn_context) = make_session_and_context().await;
let item = turn_context.to_turn_context_item();
@@ -5606,7 +5668,11 @@ async fn turn_context_item_omits_legacy_equivalent_file_system_sandbox_policy()
async fn turn_context_item_stores_split_file_system_sandbox_policy_when_different() {
let (_session, mut turn_context) = make_session_and_context().await;
let file_system_sandbox_policy = file_system_policy_with_unreadable_glob(&turn_context);
turn_context.file_system_sandbox_policy = file_system_sandbox_policy.clone();
turn_context.permission_profile = PermissionProfile::from_runtime_permissions_with_enforcement(
turn_context.permission_profile.enforcement(),
&file_system_sandbox_policy,
turn_context.network_sandbox_policy(),
);
let item = turn_context.to_turn_context_item();
@@ -5743,7 +5809,11 @@ async fn record_context_updates_and_set_reference_context_item_persists_split_fi
{
let (mut session, mut turn_context) = make_session_and_context().await;
let file_system_sandbox_policy = file_system_policy_with_unreadable_glob(&turn_context);
turn_context.file_system_sandbox_policy = file_system_sandbox_policy.clone();
turn_context.permission_profile = PermissionProfile::from_runtime_permissions_with_enforcement(
turn_context.permission_profile.enforcement(),
&file_system_sandbox_policy,
turn_context.network_sandbox_policy(),
);
let rollout_path = attach_thread_persistence(&mut session).await;
session
@@ -7674,7 +7744,6 @@ async fn rejects_escalated_permissions_when_policy_not_on_request() {
use crate::tools::sandboxing::ExecApprovalRequirement;
use crate::turn_diff_tracker::TurnDiffTracker;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::SandboxPolicy;
use std::collections::HashMap;
let (session, mut turn_context_raw) = make_session_and_context().await;
@@ -7761,23 +7830,18 @@ async fn rejects_escalated_permissions_when_policy_not_on_request() {
// command. Force DangerFullAccess so this check stays focused on approval
// policy rather than platform-specific sandbox behavior.
let turn_context_mut = Arc::get_mut(&mut turn_context).expect("unique turn context Arc");
turn_context_mut
.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());
turn_context_mut.permission_profile = PermissionProfile::Disabled;
let file_system_sandbox_policy = turn_context.file_system_sandbox_policy();
let sandbox_policy = turn_context.sandbox_policy();
let exec_approval_requirement = session
.services
.exec_policy
.create_exec_approval_requirement_for_command(ExecApprovalRequest {
command: &params.command,
approval_policy: turn_context.approval_policy.value(),
sandbox_policy: turn_context.sandbox_policy.get(),
file_system_sandbox_policy: &turn_context.file_system_sandbox_policy,
sandbox_policy: &sandbox_policy,
file_system_sandbox_policy: &file_system_sandbox_policy,
sandbox_permissions: SandboxPermissions::UseDefault,
prefix_rule: None,
})
@@ -25,8 +25,6 @@ use codex_protocol::models::ContentItem;
use codex_protocol::models::NetworkPermissions;
use codex_protocol::models::ResponseItem;
use codex_protocol::models::function_call_output_content_items_to_text;
use codex_protocol::permissions::FileSystemSandboxPolicy;
use codex_protocol::permissions::NetworkSandboxPolicy;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::request_permissions::PermissionGrantScope;
use codex_protocol::request_permissions::RequestPermissionProfile;
@@ -274,17 +272,7 @@ async fn guardian_allows_shell_additional_permissions_requests_past_policy_valid
.features
.enable(Feature::ExecPermissionApprovals)
.expect("test setup should allow enabling request permissions");
turn_context_raw
.sandbox_policy
.set(SandboxPolicy::DangerFullAccess)
.expect("test setup should allow updating sandbox policy");
// This test is about request-permissions validation, not managed sandbox
// policy enforcement. Widen the derived sandbox policies directly so the
// command runs without depending on a platform sandbox binary.
turn_context_raw.file_system_sandbox_policy =
FileSystemSandboxPolicy::from(turn_context_raw.sandbox_policy.get());
turn_context_raw.network_sandbox_policy =
NetworkSandboxPolicy::from(turn_context_raw.sandbox_policy.get());
turn_context_raw.permission_profile = codex_protocol::models::PermissionProfile::Disabled;
let mut config = (*turn_context_raw.config).clone();
config.model_provider.base_url = Some(format!("{}/v1", server.uri()));
let config = Arc::new(config);
@@ -429,14 +417,7 @@ async fn strict_auto_review_turn_grant_forces_guardian_for_shell_policy_skip() {
.approval_policy
.set(AskForApproval::OnFailure)
.expect("test setup should allow updating approval policy");
turn_context_raw
.sandbox_policy
.set(SandboxPolicy::DangerFullAccess)
.expect("test setup should allow updating sandbox policy");
turn_context_raw.file_system_sandbox_policy =
FileSystemSandboxPolicy::from(turn_context_raw.sandbox_policy.get());
turn_context_raw.network_sandbox_policy =
NetworkSandboxPolicy::from(turn_context_raw.sandbox_policy.get());
turn_context_raw.permission_profile = codex_protocol::models::PermissionProfile::Disabled;
let mut config = (*turn_context_raw.config).clone();
config.approvals_reviewer = ApprovalsReviewer::User;
config.model_provider.base_url = Some(format!("{}/v1", server.uri()));
+3 -3
View File
@@ -692,13 +692,13 @@ async fn track_turn_resolved_config_analytics(
session_source: thread_config.session_source,
model: turn_context.model_info.slug.clone(),
model_provider: turn_context.config.model_provider_id.clone(),
sandbox_policy: turn_context.sandbox_policy.get().clone(),
sandbox_policy: turn_context.sandbox_policy(),
reasoning_effort: turn_context.reasoning_effort,
reasoning_summary: Some(turn_context.reasoning_summary),
service_tier: turn_context.config.service_tier,
approval_policy: turn_context.approval_policy.value(),
approvals_reviewer: turn_context.config.approvals_reviewer,
sandbox_network_access: turn_context.network_sandbox_policy.is_enabled(),
sandbox_network_access: turn_context.network_sandbox_policy().is_enabled(),
collaboration_mode: turn_context.collaboration_mode.mode,
personality: turn_context.personality,
is_first_turn,
@@ -1871,7 +1871,7 @@ async fn try_run_sampling_request(
feedback_tags!(
model = turn_context.model_info.slug.clone(),
approval_policy = turn_context.approval_policy.value(),
sandbox_policy = turn_context.sandbox_policy.get(),
sandbox_policy = &turn_context.sandbox_policy(),
effort = turn_context.reasoning_effort,
auth_mode = sess.services.auth_manager.auth_mode(),
features = sess.features.enabled_features(),
+40 -28
View File
@@ -3,6 +3,7 @@ use codex_model_provider::SharedModelProvider;
use codex_model_provider::create_model_provider;
use codex_protocol::models::AdditionalPermissionProfile;
use codex_protocol::protocol::TurnEnvironmentSelection;
use codex_sandboxing::compatibility_sandbox_policy_for_permission_profile;
use codex_sandboxing::policy_transforms::effective_file_system_sandbox_policy;
use codex_sandboxing::policy_transforms::effective_network_sandbox_policy;
use std::sync::atomic::AtomicBool;
@@ -73,9 +74,6 @@ pub(crate) struct TurnContext {
pub(crate) personality: Option<Personality>,
pub(crate) approval_policy: Constrained<AskForApproval>,
pub(crate) permission_profile: PermissionProfile,
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,
@@ -99,6 +97,25 @@ impl TurnContext {
self.permission_profile.clone()
}
pub(crate) fn file_system_sandbox_policy(&self) -> FileSystemSandboxPolicy {
self.permission_profile.file_system_sandbox_policy()
}
pub(crate) fn network_sandbox_policy(&self) -> NetworkSandboxPolicy {
self.permission_profile.network_sandbox_policy()
}
pub(crate) fn sandbox_policy(&self) -> SandboxPolicy {
let file_system_sandbox_policy = self.file_system_sandbox_policy();
let network_sandbox_policy = self.network_sandbox_policy();
compatibility_sandbox_policy_for_permission_profile(
&self.permission_profile,
&file_system_sandbox_policy,
network_sandbox_policy,
&self.cwd,
)
}
pub(crate) fn model_context_window(&self) -> Option<i64> {
let effective_context_window_percent = self.model_info.effective_context_window_percent;
self.model_info
@@ -210,9 +227,6 @@ impl TurnContext {
personality: self.personality,
approval_policy: self.approval_policy.clone(),
permission_profile: self.permission_profile.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(),
@@ -246,12 +260,14 @@ impl TurnContext {
&self,
additional_permissions: Option<AdditionalPermissionProfile>,
) -> FileSystemSandboxContext {
let (base_file_system_sandbox_policy, base_network_sandbox_policy) =
self.permission_profile.to_runtime_permissions();
let file_system_sandbox_policy = effective_file_system_sandbox_policy(
&self.file_system_sandbox_policy,
&base_file_system_sandbox_policy,
additional_permissions.as_ref(),
);
let network_sandbox_policy = effective_network_sandbox_policy(
self.network_sandbox_policy,
base_network_sandbox_policy,
additional_permissions.as_ref(),
);
let permissions = PermissionProfile::from_runtime_permissions_with_enforcement(
@@ -278,11 +294,12 @@ impl TurnContext {
// this comparison and the legacy projection should go away.
let legacy_file_system_sandbox_policy =
FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(
self.sandbox_policy.get(),
&self.sandbox_policy(),
&self.cwd,
);
(self.file_system_sandbox_policy != legacy_file_system_sandbox_policy)
.then(|| self.file_system_sandbox_policy.clone())
let file_system_sandbox_policy = self.file_system_sandbox_policy();
(file_system_sandbox_policy != legacy_file_system_sandbox_policy)
.then_some(file_system_sandbox_policy)
}
pub(crate) fn compact_prompt(&self) -> &str {
@@ -299,7 +316,7 @@ impl TurnContext {
current_date: self.current_date.clone(),
timezone: self.timezone.clone(),
approval_policy: self.approval_policy.value(),
sandbox_policy: self.sandbox_policy.get().clone(),
sandbox_policy: self.sandbox_policy(),
permission_profile: Some(self.permission_profile()),
network: self.turn_context_network_item(),
file_system_sandbox_policy: self.non_legacy_file_system_sandbox_policy(),
@@ -366,15 +383,11 @@ impl Session {
per_turn_config.approvals_reviewer = session_configuration.approvals_reviewer;
per_turn_config.permissions.permission_profile =
session_configuration.permission_profile.clone();
per_turn_config.permissions.sandbox_policy = session_configuration.sandbox_policy.clone();
per_turn_config.permissions.file_system_sandbox_policy =
session_configuration.file_system_sandbox_policy.clone();
per_turn_config.permissions.network_sandbox_policy =
session_configuration.network_sandbox_policy;
let resolved_web_search_mode = resolve_web_search_mode_for_turn(
&per_turn_config.web_search_mode,
session_configuration.sandbox_policy.get(),
);
let sandbox_policy = session_configuration.sandbox_policy();
per_turn_config.permissions.sandbox_policy =
Constrained::allow_only(sandbox_policy.clone());
let resolved_web_search_mode =
resolve_web_search_mode_for_turn(&per_turn_config.web_search_mode, &sandbox_policy);
if let Err(err) = per_turn_config
.web_search_mode
.set(resolved_web_search_mode)
@@ -489,9 +502,6 @@ impl Session {
personality: session_configuration.personality,
approval_policy: session_configuration.approval_policy.clone(),
permission_profile: session_configuration.permission_profile(),
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(),
@@ -528,8 +538,9 @@ impl Session {
let turn_environments =
self.resolve_turn_environments(&effective_environments)?;
let previous_cwd = state.session_configuration.cwd.clone();
let sandbox_policy_changed =
state.session_configuration.sandbox_policy != next.sandbox_policy;
let previous_sandbox_policy = state.session_configuration.sandbox_policy();
let next_sandbox_policy = next.sandbox_policy();
let sandbox_policy_changed = previous_sandbox_policy != next_sandbox_policy;
let codex_home = next.codex_home.clone();
let session_source = next.session_source.clone();
state.session_configuration = next.clone();
@@ -635,7 +646,8 @@ impl Session {
{
let mcp_connection_manager = self.services.mcp_connection_manager.read().await;
mcp_connection_manager.set_approval_policy(&session_configuration.approval_policy);
mcp_connection_manager.set_sandbox_policy(session_configuration.sandbox_policy.get());
mcp_connection_manager
.set_permission_profile(session_configuration.permission_profile());
}
let model_info = self
@@ -680,7 +692,7 @@ impl Session {
.as_ref()
.and_then(|started_proxy| {
Self::managed_network_proxy_active_for_sandbox_policy(
session_configuration.sandbox_policy.get(),
&session_configuration.sandbox_policy(),
)
.then(|| started_proxy.proxy())
}),
@@ -272,8 +272,9 @@ async fn effective_patch_permissions(
session.granted_session_permissions().await.as_ref(),
session.granted_turn_permissions().await.as_ref(),
);
let base_file_system_sandbox_policy = turn.file_system_sandbox_policy();
let file_system_sandbox_policy = effective_file_system_sandbox_policy(
&turn.file_system_sandbox_policy,
&base_file_system_sandbox_policy,
granted_permissions.as_ref(),
);
let effective_additional_permissions = apply_granted_turn_permissions(
+2 -1
View File
@@ -99,7 +99,8 @@ impl ToolHandler for ListDirHandler {
"dir_path must be an absolute path".to_string(),
));
}
let read_deny_matcher = ReadDenyMatcher::new(&turn.file_system_sandbox_policy, &turn.cwd);
let file_system_sandbox_policy = turn.file_system_sandbox_policy();
let read_deny_matcher = ReadDenyMatcher::new(&file_system_sandbox_policy, &turn.cwd);
if read_deny_matcher
.as_ref()
.is_some_and(|matcher| matcher.is_read_denied(&path))
@@ -37,6 +37,9 @@ use codex_protocol::openai_models::ReasoningEffort;
use codex_protocol::protocol::AgentStatus;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::FileSystemAccessMode;
use codex_protocol::protocol::FileSystemPath;
use codex_protocol::protocol::FileSystemSandboxEntry;
use codex_protocol::protocol::FileSystemSandboxPolicy;
use codex_protocol::protocol::InitialHistory;
use codex_protocol::protocol::InterAgentCommunication;
@@ -2074,21 +2077,6 @@ async fn multi_agent_v2_spawn_surfaces_task_name_validation_errors() {
#[tokio::test]
async fn spawn_agent_reapplies_runtime_sandbox_after_role_config() {
fn pick_allowed_sandbox_policy(
constraint: &crate::config::Constrained<SandboxPolicy>,
base: SandboxPolicy,
) -> SandboxPolicy {
let candidates = [
SandboxPolicy::DangerFullAccess,
SandboxPolicy::new_workspace_write_policy(),
SandboxPolicy::new_read_only_policy(),
];
candidates
.into_iter()
.find(|candidate| *candidate != base && constraint.can_set(candidate).is_ok())
.unwrap_or(base)
}
#[derive(Debug, Deserialize)]
struct SpawnAgentResult {
agent_id: String,
@@ -2098,12 +2086,17 @@ async fn spawn_agent_reapplies_runtime_sandbox_after_role_config() {
let (mut session, mut turn) = make_session_and_context().await;
let manager = thread_manager();
session.services.agent_control = manager.agent_control();
let expected_sandbox = pick_allowed_sandbox_policy(
&turn.config.permissions.sandbox_policy,
turn.config.permissions.sandbox_policy.get().clone(),
);
let expected_file_system_sandbox_policy =
let expected_sandbox = turn.config.permissions.sandbox_policy.get().clone();
let mut expected_file_system_sandbox_policy =
FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(&expected_sandbox, &turn.cwd);
expected_file_system_sandbox_policy
.entries
.push(FileSystemSandboxEntry {
path: FileSystemPath::GlobPattern {
pattern: "**/.env".to_string(),
},
access: FileSystemAccessMode::None,
});
let expected_network_sandbox_policy = NetworkSandboxPolicy::from(&expected_sandbox);
let expected_permission_profile = PermissionProfile::from_runtime_permissions_with_enforcement(
SandboxEnforcement::from_legacy_sandbox_policy(&expected_sandbox),
@@ -2113,16 +2106,11 @@ async fn spawn_agent_reapplies_runtime_sandbox_after_role_config() {
turn.approval_policy
.set(AskForApproval::OnRequest)
.expect("approval policy should be set");
turn.sandbox_policy
.set(expected_sandbox.clone())
.expect("sandbox policy should be set");
turn.file_system_sandbox_policy = expected_file_system_sandbox_policy.clone();
turn.network_sandbox_policy = expected_network_sandbox_policy;
turn.permission_profile = expected_permission_profile.clone();
assert_ne!(
expected_sandbox,
turn.config.permissions.sandbox_policy.get().clone(),
"test requires a runtime sandbox override that differs from base config"
expected_permission_profile,
turn.config.permissions.permission_profile(),
"test requires a runtime profile override that differs from base config"
);
let invocation = invocation(
@@ -2164,11 +2152,11 @@ async fn spawn_agent_reapplies_runtime_sandbox_after_role_config() {
.expect("spawned agent thread should exist");
let child_turn = child_thread.codex.session.new_default_turn().await;
assert_eq!(
child_turn.file_system_sandbox_policy,
child_turn.file_system_sandbox_policy(),
expected_file_system_sandbox_policy
);
assert_eq!(
child_turn.network_sandbox_policy,
child_turn.network_sandbox_policy(),
expected_network_sandbox_policy
);
assert_eq!(child_turn.permission_profile(), expected_permission_profile);
@@ -3637,11 +3625,6 @@ async fn build_agent_spawn_config_uses_turn_context_values() {
&file_system_sandbox_policy,
network_sandbox_policy,
);
turn.sandbox_policy
.set(sandbox_policy)
.expect("sandbox policy set");
turn.file_system_sandbox_policy = file_system_sandbox_policy;
turn.network_sandbox_policy = network_sandbox_policy;
turn.permission_profile = permission_profile.clone();
turn.approval_policy
.set(AskForApproval::OnRequest)
@@ -3718,7 +3701,7 @@ async fn build_agent_resume_config_clears_base_instructions() {
expected
.permissions
.sandbox_policy
.set(turn.sandbox_policy.get().clone())
.set(turn.sandbox_policy())
.expect("sandbox policy set");
assert_eq!(config, expected);
}
+4 -2
View File
@@ -513,14 +513,16 @@ impl ShellHandler {
);
emitter.begin(event_ctx).await;
let file_system_sandbox_policy = turn.file_system_sandbox_policy();
let sandbox_policy = turn.sandbox_policy();
let exec_approval_requirement = session
.services
.exec_policy
.create_exec_approval_requirement_for_command(ExecApprovalRequest {
command: &exec_params.command,
approval_policy: turn.approval_policy.value(),
sandbox_policy: turn.sandbox_policy.get(),
file_system_sandbox_policy: &turn.file_system_sandbox_policy,
sandbox_policy: &sandbox_policy,
file_system_sandbox_policy: &file_system_sandbox_policy,
sandbox_permissions: if effective_additional_permissions.permissions_preapproved {
codex_protocol::models::SandboxPermissions::UseDefault
} else {
+2 -1
View File
@@ -359,7 +359,8 @@ impl NetworkApprovalService {
.await;
return NetworkDecision::deny(REASON_NOT_ALLOWED);
};
if !sandbox_policy_allows_network_approval_flow(turn_context.sandbox_policy.get()) {
let sandbox_policy = turn_context.sandbox_policy();
if !sandbox_policy_allows_network_approval_flow(&sandbox_policy) {
pending.set_decision(PendingApprovalDecision::Deny).await;
self.pending_host_approvals.lock().await.remove(&key);
self.record_outcome_for_single_active_call(NetworkApprovalOutcome::DeniedByPolicy(
+6 -4
View File
@@ -122,8 +122,10 @@ impl ToolOrchestrator {
// 1) Approval
let mut already_approved = false;
let file_system_sandbox_policy = turn_ctx.file_system_sandbox_policy();
let network_sandbox_policy = turn_ctx.network_sandbox_policy();
let requirement = tool.exec_approval_requirement(req).unwrap_or_else(|| {
default_exec_approval_requirement(approval_policy, &turn_ctx.file_system_sandbox_policy)
default_exec_approval_requirement(approval_policy, &file_system_sandbox_policy)
});
match requirement {
ExecApprovalRequirement::Skip { .. } => {
@@ -194,8 +196,8 @@ impl ToolOrchestrator {
let initial_sandbox = match tool.sandbox_mode_for_first_attempt(req) {
SandboxOverride::BypassSandboxFirstAttempt => SandboxType::None,
SandboxOverride::NoOverride => self.sandbox.select_initial(
&turn_ctx.file_system_sandbox_policy,
turn_ctx.network_sandbox_policy,
&file_system_sandbox_policy,
network_sandbox_policy,
tool.sandbox_preference(),
turn_ctx.windows_sandbox_level,
managed_network_active,
@@ -268,7 +270,7 @@ impl ToolOrchestrator {
&& matches!(
default_exec_approval_requirement(
approval_policy,
&turn_ctx.file_system_sandbox_policy
&file_system_sandbox_policy
),
ExecApprovalRequirement::NeedsApproval { .. }
);
@@ -390,11 +390,12 @@ async fn execve_permission_request_hook_short_circuits_prompt() -> anyhow::Resul
..HooksConfig::default()
});
let sandbox_policy = SandboxPolicy::new_read_only_policy();
turn_context.approval_policy = Constrained::allow_any(AskForApproval::OnRequest);
turn_context.sandbox_policy = Constrained::allow_any(sandbox_policy.clone());
turn_context.file_system_sandbox_policy = read_only_file_system_sandbox_policy();
turn_context.network_sandbox_policy = NetworkSandboxPolicy::Restricted;
turn_context.permission_profile = PermissionProfile::from_runtime_permissions(
&read_only_file_system_sandbox_policy(),
NetworkSandboxPolicy::Restricted,
);
let sandbox_policy = SandboxPolicy::new_read_only_policy();
let workdir = AbsolutePathBuf::try_from(std::env::current_dir()?)?;
let target = std::env::temp_dir().join("execve-hook-short-circuit.txt");
@@ -788,6 +788,8 @@ impl UnifiedExecProcessManager {
self,
context.turn.tools_config.unified_exec_shell_mode.clone(),
);
let file_system_sandbox_policy = context.turn.file_system_sandbox_policy();
let sandbox_policy = context.turn.sandbox_policy();
let exec_approval_requirement = context
.session
.services
@@ -795,8 +797,8 @@ impl UnifiedExecProcessManager {
.create_exec_approval_requirement_for_command(ExecApprovalRequest {
command: &request.command,
approval_policy: context.turn.approval_policy.value(),
sandbox_policy: context.turn.sandbox_policy.get(),
file_system_sandbox_policy: &context.turn.file_system_sandbox_policy,
sandbox_policy: &sandbox_policy,
file_system_sandbox_policy: &file_system_sandbox_policy,
sandbox_permissions: if request.additional_permissions_preapproved {
crate::sandboxing::SandboxPermissions::UseDefault
} else {
+7 -1
View File
@@ -14,10 +14,12 @@ use codex_config::types::McpServerConfig;
use codex_config::types::McpServerTransportConfig;
use codex_core::sandboxing::SandboxPermissions;
use codex_features::Feature;
use codex_protocol::models::PermissionProfile;
use codex_protocol::permissions::FileSystemAccessMode;
use codex_protocol::permissions::FileSystemPath;
use codex_protocol::permissions::FileSystemSandboxEntry;
use codex_protocol::permissions::FileSystemSandboxPolicy;
use codex_protocol::permissions::NetworkSandboxPolicy;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::SandboxPolicy;
use codex_protocol::protocol::TurnEnvironmentSelection;
@@ -560,7 +562,11 @@ async fn shell_enforces_glob_deny_read_policy() -> Result<()> {
},
access: FileSystemAccessMode::None,
});
config.permissions.file_system_sandbox_policy = file_system_sandbox_policy;
config.permissions.permission_profile =
Constrained::allow_any(PermissionProfile::from_runtime_permissions(
&file_system_sandbox_policy,
NetworkSandboxPolicy::Restricted,
));
});
let fixture = builder.build(&server).await?;
+7 -1
View File
@@ -2527,10 +2527,12 @@ async fn unified_exec_runs_under_sandbox() -> Result<()> {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn unified_exec_enforces_glob_deny_read_policy() -> Result<()> {
use codex_config::Constrained;
use codex_protocol::models::PermissionProfile;
use codex_protocol::permissions::FileSystemAccessMode;
use codex_protocol::permissions::FileSystemPath;
use codex_protocol::permissions::FileSystemSandboxEntry;
use codex_protocol::permissions::FileSystemSandboxPolicy;
use codex_protocol::permissions::NetworkSandboxPolicy;
skip_if_no_network!(Ok(()));
skip_if_sandbox!(Ok(()));
@@ -2553,7 +2555,11 @@ async fn unified_exec_enforces_glob_deny_read_policy() -> Result<()> {
},
access: FileSystemAccessMode::None,
});
config.permissions.file_system_sandbox_policy = file_system_sandbox_policy;
config.permissions.permission_profile =
Constrained::allow_any(PermissionProfile::from_runtime_permissions(
&file_system_sandbox_policy,
NetworkSandboxPolicy::Restricted,
));
});
let TestCodex {
codex,
+7 -1
View File
@@ -1,5 +1,6 @@
use anyhow::Context;
use codex_features::Feature;
use codex_protocol::models::PermissionProfile;
use codex_protocol::permissions::NetworkSandboxPolicy;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::EventMsg;
@@ -338,7 +339,12 @@ async fn user_shell_command_history_is_persisted_and_shared_with_model() -> anyh
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 file_system_sandbox_policy = config.permissions.file_system_sandbox_policy();
config.permissions.permission_profile =
codex_config::Constrained::allow_any(PermissionProfile::from_runtime_permissions(
&file_system_sandbox_policy,
NetworkSandboxPolicy::Restricted,
));
});
let test = builder.build(&server).await?;
+12 -4
View File
@@ -482,10 +482,7 @@ impl PermissionProfile {
FileSystemSandboxKind::ExternalSandbox => Self::External {
network: network_sandbox_policy,
},
FileSystemSandboxKind::Unrestricted
if enforcement == SandboxEnforcement::Disabled
&& network_sandbox_policy.is_enabled() =>
{
FileSystemSandboxKind::Unrestricted if enforcement == SandboxEnforcement::Disabled => {
Self::Disabled
}
FileSystemSandboxKind::Restricted | FileSystemSandboxKind::Unrestricted => {
@@ -1867,6 +1864,17 @@ mod tests {
Ok(())
}
#[test]
fn disabled_permission_profile_ignores_runtime_network_policy() {
let permission_profile = PermissionProfile::from_runtime_permissions_with_enforcement(
SandboxEnforcement::Disabled,
&FileSystemSandboxPolicy::unrestricted(),
NetworkSandboxPolicy::Restricted,
);
assert_eq!(permission_profile, PermissionProfile::Disabled);
}
#[test]
fn permission_profile_from_runtime_permissions_preserves_external_sandbox() {
let permission_profile = PermissionProfile::from_runtime_permissions(
+48 -3
View File
@@ -631,6 +631,12 @@ impl FileSystemSandboxPolicy {
.semantic_signature(cwd)
}
/// Returns true when two policies resolve to the same filesystem access
/// model for `cwd`, ignoring incidental entry ordering.
pub fn is_semantically_equivalent_to(&self, other: &Self, cwd: &Path) -> bool {
self.semantic_signature(cwd) == other.semantic_signature(cwd)
}
/// 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() {
@@ -949,9 +955,9 @@ impl FileSystemSandboxPolicy {
has_full_disk_read_access: self.has_full_disk_read_access(),
has_full_disk_write_access: self.has_full_disk_write_access(),
include_platform_defaults: self.include_platform_defaults(),
readable_roots: self.get_readable_roots_with_cwd(cwd),
writable_roots: self.get_writable_roots_with_cwd(cwd),
unreadable_roots: self.get_unreadable_roots_with_cwd(cwd),
readable_roots: sorted_absolute_paths(self.get_readable_roots_with_cwd(cwd)),
writable_roots: sorted_writable_roots(self.get_writable_roots_with_cwd(cwd)),
unreadable_roots: sorted_absolute_paths(self.get_unreadable_roots_with_cwd(cwd)),
unreadable_globs: self.get_unreadable_globs_with_cwd(cwd),
}
}
@@ -1257,6 +1263,20 @@ fn dedup_absolute_paths(
deduped
}
fn sorted_absolute_paths(mut paths: Vec<AbsolutePathBuf>) -> Vec<AbsolutePathBuf> {
paths.sort_by(|left, right| left.as_path().cmp(right.as_path()));
paths
}
fn sorted_writable_roots(mut roots: Vec<WritableRoot>) -> Vec<WritableRoot> {
for root in &mut roots {
root.read_only_subpaths =
sorted_absolute_paths(std::mem::take(&mut root.read_only_subpaths));
}
roots.sort_by(|left, right| left.root.as_path().cmp(right.root.as_path()));
roots
}
fn normalize_effective_absolute_path(path: AbsolutePathBuf) -> AbsolutePathBuf {
let raw_path = path.to_path_buf();
for ancestor in raw_path.ancestors() {
@@ -2145,6 +2165,31 @@ mod tests {
);
}
#[test]
fn legacy_projection_runtime_enforcement_ignores_entry_order() {
let cwd = TempDir::new().expect("tempdir");
let legacy_policy = SandboxPolicy::WorkspaceWrite {
writable_roots: Vec::new(),
network_access: false,
exclude_tmpdir_env_var: true,
exclude_slash_tmp: true,
};
let legacy_order =
FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(&legacy_policy, cwd.path());
let mut reordered_entries = legacy_order.entries.clone();
reordered_entries.reverse();
let reordered = FileSystemSandboxPolicy::restricted(reordered_entries);
assert!(
legacy_order.is_semantically_equivalent_to(&reordered, cwd.path()),
"entry order should not affect filesystem semantics"
);
assert!(
!reordered
.needs_direct_runtime_enforcement(NetworkSandboxPolicy::Restricted, cwd.path())
);
}
#[test]
fn root_write_with_read_only_child_is_not_full_disk_write() {
let cwd = TempDir::new().expect("tempdir");
+7 -31
View File
@@ -2379,37 +2379,13 @@ impl ChatWidget {
tracing::warn!(%err, "failed to sync permissions from SessionConfigured");
self.config.permissions.sandbox_policy =
Constrained::allow_only(event.sandbox_policy.clone());
match event.permission_profile.clone() {
Some(permission_profile) => {
let (file_system_sandbox_policy, network_sandbox_policy) =
permission_profile.to_runtime_permissions();
self.config.permissions.permission_profile =
Constrained::allow_only(permission_profile);
self.config.permissions.file_system_sandbox_policy = file_system_sandbox_policy;
self.config.permissions.network_sandbox_policy = network_sandbox_policy;
}
None => {
self.config.permissions.file_system_sandbox_policy =
codex_protocol::permissions::FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(
&event.sandbox_policy,
&event.cwd,
);
self.config.permissions.network_sandbox_policy =
codex_protocol::permissions::NetworkSandboxPolicy::from(
&event.sandbox_policy,
);
let permission_profile =
codex_protocol::models::PermissionProfile::from_runtime_permissions_with_enforcement(
codex_protocol::models::SandboxEnforcement::from_legacy_sandbox_policy(
&event.sandbox_policy,
),
&self.config.permissions.file_system_sandbox_policy,
self.config.permissions.network_sandbox_policy,
);
self.config.permissions.permission_profile =
Constrained::allow_only(permission_profile);
}
}
let permission_profile = event.permission_profile.clone().unwrap_or_else(|| {
codex_protocol::models::PermissionProfile::from_legacy_sandbox_policy(
&event.sandbox_policy,
)
});
self.config.permissions.permission_profile =
Constrained::allow_only(permission_profile);
}
self.config.approvals_reviewer = event.approvals_reviewer;
self.status_line_project_root_name_cache = None;
@@ -380,12 +380,12 @@ async fn session_configured_external_sandbox_keeps_external_runtime_policy() {
assert_eq!(
chat.config_ref()
.permissions
.file_system_sandbox_policy
.file_system_sandbox_policy()
.kind,
FileSystemSandboxKind::ExternalSandbox,
);
assert_eq!(
chat.config_ref().permissions.network_sandbox_policy,
chat.config_ref().permissions.network_sandbox_policy(),
NetworkSandboxPolicy::Restricted,
);
}