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 authored and GitHub committed 2026-04-26 15:06:42 -07:00
1 parent 4d7ce3447d
commit deaa307fb2
39 files changed
+568 -439

No files matched your search

@@ -2278,9 +2278,11 @@ impl CodexMessageProcessor {
codex_protocol::models::PermissionProfile::from(permission_profile); codex_protocol::models::PermissionProfile::from(permission_profile);
let (mut file_system_sandbox_policy, network_sandbox_policy) = let (mut file_system_sandbox_policy, network_sandbox_policy) =
permission_profile.to_runtime_permissions(); permission_profile.to_runtime_permissions();
let configured_file_system_sandbox_policy =
self.config.permissions.file_system_sandbox_policy();
Self::preserve_configured_deny_read_restrictions( Self::preserve_configured_deny_read_restrictions(
&mut file_system_sandbox_policy, &mut file_system_sandbox_policy,
&self.config.permissions.file_system_sandbox_policy, &configured_file_system_sandbox_policy,
); );
let effective_permission_profile = let effective_permission_profile =
codex_protocol::models::PermissionProfile::from_runtime_permissions_with_enforcement( 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 { let mut child = match sandbox_type {
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
SandboxType::Seatbelt => { 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 { let args = create_seatbelt_command_args(CreateSeatbeltCommandArgsParams {
command, command,
file_system_sandbox_policy: &config.permissions.file_system_sandbox_policy, file_system_sandbox_policy: &file_system_sandbox_policy,
network_sandbox_policy: config.permissions.network_sandbox_policy, network_sandbox_policy,
sandbox_policy_cwd: sandbox_policy_cwd.as_path(), sandbox_policy_cwd: sandbox_policy_cwd.as_path(),
enforce_managed_network: false, enforce_managed_network: false,
network: network.as_ref(), network: network.as_ref(),
extra_allow_unix_sockets: allow_unix_sockets, extra_allow_unix_sockets: allow_unix_sockets,
}); });
let network_policy = config.permissions.network_sandbox_policy;
spawn_debug_sandbox_child( spawn_debug_sandbox_child(
PathBuf::from("/usr/bin/sandbox-exec"), PathBuf::from("/usr/bin/sandbox-exec"),
args, args,
/*arg0*/ None, /*arg0*/ None,
cwd.to_path_buf(), cwd.to_path_buf(),
network_policy, network_sandbox_policy,
env, env,
|env_map| { |env_map| {
env_map.insert(CODEX_SANDBOX_ENV_VAR.to_string(), "seatbelt".to_string()); 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 .codex_linux_sandbox_exe
.expect("codex-linux-sandbox executable not found"); .expect("codex-linux-sandbox executable not found");
let use_legacy_landlock = config.features.use_legacy_landlock(); 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( let args = create_linux_sandbox_command_args_for_policies(
command, command,
cwd.as_path(), cwd.as_path(),
config.permissions.sandbox_policy.get(), config.permissions.sandbox_policy.get(),
&config.permissions.file_system_sandbox_policy, &file_system_sandbox_policy,
config.permissions.network_sandbox_policy, network_sandbox_policy,
sandbox_policy_cwd.as_path(), sandbox_policy_cwd.as_path(),
use_legacy_landlock, use_legacy_landlock,
/*allow_network_for_proxy*/ false, /*allow_network_for_proxy*/ false,
); );
let network_policy = config.permissions.network_sandbox_policy;
spawn_debug_sandbox_child( spawn_debug_sandbox_child(
codex_linux_sandbox_exe, codex_linux_sandbox_exe,
args, args,
Some("codex-linux-sandbox"), Some("codex-linux-sandbox"),
cwd.to_path_buf(), cwd.to_path_buf(),
network_policy, network_sandbox_policy,
env, env,
|env_map| { |env_map| {
if let Some(network) = network.as_ref() { if let Some(network) = network.as_ref() {
@@ -715,17 +717,17 @@ mod tests {
assert!(config_uses_permission_profiles(&config)); assert!(config_uses_permission_profiles(&config));
assert!( assert!(
profile_config.permissions.file_system_sandbox_policy profile_config.permissions.file_system_sandbox_policy()
!= legacy_config.permissions.file_system_sandbox_policy, != legacy_config.permissions.file_system_sandbox_policy(),
"test fixture should distinguish profile syntax from legacy sandbox_mode" "test fixture should distinguish profile syntax from legacy sandbox_mode"
); );
assert_eq!( assert_eq!(
config.permissions.file_system_sandbox_policy, config.permissions.file_system_sandbox_policy(),
profile_config.permissions.file_system_sandbox_policy, profile_config.permissions.file_system_sandbox_policy(),
); );
assert_ne!( assert_ne!(
config.permissions.file_system_sandbox_policy, config.permissions.file_system_sandbox_policy(),
legacy_config.permissions.file_system_sandbox_policy, legacy_config.permissions.file_system_sandbox_policy(),
); );
Ok(()) Ok(())
+14 -9
View File
@@ -26,10 +26,10 @@ use codex_plugin::PluginCapabilitySummary;
use codex_protocol::mcp::Resource; use codex_protocol::mcp::Resource;
use codex_protocol::mcp::ResourceTemplate; use codex_protocol::mcp::ResourceTemplate;
use codex_protocol::mcp::Tool; use codex_protocol::mcp::Tool;
use codex_protocol::models::PermissionProfile;
use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::McpAuthStatus; use codex_protocol::protocol::McpAuthStatus;
use codex_protocol::protocol::McpListToolsResponseEvent; use codex_protocol::protocol::McpListToolsResponseEvent;
use codex_protocol::protocol::SandboxPolicy;
use rmcp::model::ReadResourceRequestParams; use rmcp::model::ReadResourceRequestParams;
use rmcp::model::ReadResourceResult; use rmcp::model::ReadResourceResult;
use serde_json::Value; 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. /// of being shown to the user.
pub fn mcp_permission_prompt_is_auto_approved( pub fn mcp_permission_prompt_is_auto_approved(
approval_policy: AskForApproval, approval_policy: AskForApproval,
sandbox_policy: &SandboxPolicy, permission_profile: &PermissionProfile,
) -> bool { ) -> bool {
approval_policy == AskForApproval::Never if approval_policy != AskForApproval::Never {
&& matches!( return false;
sandbox_policy, }
SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. }
) 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`. /// MCP runtime settings derived from `codex_core::config::Config`.
@@ -229,7 +234,7 @@ pub async fn read_mcp_resource(
&config.approval_policy, &config.approval_policy,
String::new(), String::new(),
tx_event, tx_event,
SandboxPolicy::new_read_only_policy(), PermissionProfile::default(),
runtime_environment, runtime_environment,
config.codex_home.clone(), config.codex_home.clone(),
codex_apps_tools_cache_key(auth), codex_apps_tools_cache_key(auth),
@@ -294,7 +299,7 @@ pub async fn collect_mcp_server_status_snapshot_with_detail(
&config.approval_policy, &config.approval_policy,
submit_id, submit_id,
tx_event, tx_event,
SandboxPolicy::new_read_only_policy(), PermissionProfile::default(),
runtime_environment, runtime_environment,
config.codex_home.clone(), config.codex_home.clone(),
codex_apps_tools_cache_key(auth), codex_apps_tools_cache_key(auth),
+31
View File
@@ -3,6 +3,9 @@ use codex_config::Constrained;
use codex_login::CodexAuth; use codex_login::CodexAuth;
use codex_plugin::AppConnectorId; use codex_plugin::AppConnectorId;
use codex_plugin::PluginCapabilitySummary; 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 codex_protocol::protocol::AskForApproval;
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
use std::collections::HashMap; 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] #[test]
fn tool_plugin_provenance_collects_app_and_mcp_sources() { fn tool_plugin_provenance_collects_app_and_mcp_sources() {
let provenance = ToolPluginProvenance::from_capability_summaries(&[ let provenance = ToolPluginProvenance::from_capability_summaries(&[
@@ -334,15 +334,15 @@ fn can_auto_accept_elicitation(elicitation: &CreateElicitationRequestParams) ->
struct ElicitationRequestManager { struct ElicitationRequestManager {
requests: Arc<Mutex<ResponderMap>>, requests: Arc<Mutex<ResponderMap>>,
approval_policy: Arc<StdMutex<AskForApproval>>, approval_policy: Arc<StdMutex<AskForApproval>>,
sandbox_policy: Arc<StdMutex<SandboxPolicy>>, permission_profile: Arc<StdMutex<PermissionProfile>>,
} }
impl ElicitationRequestManager { impl ElicitationRequestManager {
fn new(approval_policy: AskForApproval, sandbox_policy: SandboxPolicy) -> Self { fn new(approval_policy: AskForApproval, permission_profile: PermissionProfile) -> Self {
Self { Self {
requests: Arc::new(Mutex::new(HashMap::new())), requests: Arc::new(Mutex::new(HashMap::new())),
approval_policy: Arc::new(StdMutex::new(approval_policy)), 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 { fn make_sender(&self, server_name: String, tx_event: Sender<Event>) -> SendElicitation {
let elicitation_requests = self.requests.clone(); let elicitation_requests = self.requests.clone();
let approval_policy = self.approval_policy.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| { Box::new(move |id, elicitation| {
let elicitation_requests = elicitation_requests.clone(); let elicitation_requests = elicitation_requests.clone();
let tx_event = tx_event.clone(); let tx_event = tx_event.clone();
let server_name = server_name.clone(); let server_name = server_name.clone();
let approval_policy = approval_policy.clone(); let approval_policy = approval_policy.clone();
let sandbox_policy = sandbox_policy.clone(); let permission_profile = permission_profile.clone();
async move { async move {
let approval_policy = approval_policy let approval_policy = approval_policy
.lock() .lock()
.map(|policy| *policy) .map(|policy| *policy)
.unwrap_or(AskForApproval::Never); .unwrap_or(AskForApproval::Never);
let sandbox_policy = sandbox_policy let permission_profile = permission_profile
.lock() .lock()
.map(|policy| policy.clone()) .map(|profile| profile.clone())
.unwrap_or_else(|_| SandboxPolicy::new_read_only_policy()); .unwrap_or_default();
if mcp_permission_prompt_is_auto_approved(approval_policy, &sandbox_policy) if mcp_permission_prompt_is_auto_approved(approval_policy, &permission_profile)
&& can_auto_accept_elicitation(&elicitation) && can_auto_accept_elicitation(&elicitation)
{ {
return Ok(ElicitationResponse { return Ok(ElicitationResponse {
@@ -666,14 +666,14 @@ impl AsyncManagedClient {
impl McpConnectionManager { impl McpConnectionManager {
pub fn new_uninitialized( pub fn new_uninitialized(
approval_policy: &Constrained<AskForApproval>, approval_policy: &Constrained<AskForApproval>,
sandbox_policy: &Constrained<SandboxPolicy>, permission_profile: &Constrained<PermissionProfile>,
) -> Self { ) -> Self {
Self { Self {
clients: HashMap::new(), clients: HashMap::new(),
server_origins: HashMap::new(), server_origins: HashMap::new(),
elicitation_requests: ElicitationRequestManager::new( elicitation_requests: ElicitationRequestManager::new(
approval_policy.value(), 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) { pub fn set_permission_profile(&self, permission_profile: PermissionProfile) {
if let Ok(mut policy) = self.elicitation_requests.sandbox_policy.lock() { if let Ok(mut profile) = self.elicitation_requests.permission_profile.lock() {
*policy = sandbox_policy.clone(); *profile = permission_profile;
} }
} }
@@ -706,7 +706,7 @@ impl McpConnectionManager {
approval_policy: &Constrained<AskForApproval>, approval_policy: &Constrained<AskForApproval>,
submit_id: String, submit_id: String,
tx_event: Sender<Event>, tx_event: Sender<Event>,
initial_sandbox_policy: SandboxPolicy, initial_permission_profile: PermissionProfile,
runtime_environment: McpRuntimeEnvironment, runtime_environment: McpRuntimeEnvironment,
codex_home: PathBuf, codex_home: PathBuf,
codex_apps_tools_cache_key: CodexAppsToolsCacheKey, codex_apps_tools_cache_key: CodexAppsToolsCacheKey,
@@ -718,7 +718,7 @@ impl McpConnectionManager {
let mut server_origins = HashMap::new(); let mut server_origins = HashMap::new();
let mut join_set = JoinSet::new(); let mut join_set = JoinSet::new();
let elicitation_requests = 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 tool_plugin_provenance = Arc::new(tool_plugin_provenance);
let startup_submit_id = submit_id.clone(); let startup_submit_id = submit_id.clone();
let codex_apps_auth_provider = auth let codex_apps_auth_provider = auth
@@ -1,5 +1,6 @@
use super::*; use super::*;
use codex_protocol::ToolName; use codex_protocol::ToolName;
use codex_protocol::models::PermissionProfile;
use codex_protocol::protocol::GranularApprovalConfig; use codex_protocol::protocol::GranularApprovalConfig;
use codex_protocol::protocol::McpAuthStatus; use codex_protocol::protocol::McpAuthStatus;
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
@@ -179,9 +180,9 @@ fn elicitation_granular_policy_respects_never_and_config() {
} }
#[tokio::test] #[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 = let manager =
ElicitationRequestManager::new(AskForApproval::Never, SandboxPolicy::DangerFullAccess); ElicitationRequestManager::new(AskForApproval::Never, PermissionProfile::Disabled);
let (tx_event, _rx_event) = async_channel::bounded(1); let (tx_event, _rx_event) = async_channel::bounded(1);
let sender = manager.make_sender("server".to_string(), tx_event); 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] #[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 = let manager =
ElicitationRequestManager::new(AskForApproval::Never, SandboxPolicy::DangerFullAccess); ElicitationRequestManager::new(AskForApproval::Never, PermissionProfile::Disabled);
let (tx_event, _rx_event) = async_channel::bounded(1); let (tx_event, _rx_event) = async_channel::bounded(1);
let sender = manager.make_sender("server".to_string(), tx_event); 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() .boxed()
.shared(); .shared();
let approval_policy = Constrained::allow_any(AskForApproval::OnFailure); let approval_policy = Constrained::allow_any(AskForApproval::OnFailure);
let sandbox_policy = Constrained::allow_any(SandboxPolicy::new_read_only_policy()); let permission_profile = Constrained::allow_any(PermissionProfile::default());
let mut manager = McpConnectionManager::new_uninitialized(&approval_policy, &sandbox_policy); let mut manager =
McpConnectionManager::new_uninitialized(&approval_policy, &permission_profile);
manager.clients.insert( manager.clients.insert(
CODEX_APPS_MCP_SERVER_NAME.to_string(), CODEX_APPS_MCP_SERVER_NAME.to_string(),
AsyncManagedClient { AsyncManagedClient {
@@ -654,8 +656,9 @@ async fn resolve_tool_info_accepts_canonical_namespaced_tool_names() {
.boxed() .boxed()
.shared(); .shared();
let approval_policy = Constrained::allow_any(AskForApproval::OnFailure); let approval_policy = Constrained::allow_any(AskForApproval::OnFailure);
let sandbox_policy = Constrained::allow_any(SandboxPolicy::new_read_only_policy()); let permission_profile = Constrained::allow_any(PermissionProfile::default());
let mut manager = McpConnectionManager::new_uninitialized(&approval_policy, &sandbox_policy); let mut manager =
McpConnectionManager::new_uninitialized(&approval_policy, &permission_profile);
manager.clients.insert( manager.clients.insert(
"rmcp".to_string(), "rmcp".to_string(),
AsyncManagedClient { AsyncManagedClient {
@@ -689,8 +692,9 @@ async fn list_all_tools_blocks_while_client_is_pending_without_startup_snapshot(
.boxed() .boxed()
.shared(); .shared();
let approval_policy = Constrained::allow_any(AskForApproval::OnFailure); let approval_policy = Constrained::allow_any(AskForApproval::OnFailure);
let sandbox_policy = Constrained::allow_any(SandboxPolicy::new_read_only_policy()); let permission_profile = Constrained::allow_any(PermissionProfile::default());
let mut manager = McpConnectionManager::new_uninitialized(&approval_policy, &sandbox_policy); let mut manager =
McpConnectionManager::new_uninitialized(&approval_policy, &permission_profile);
manager.clients.insert( manager.clients.insert(
CODEX_APPS_MCP_SERVER_NAME.to_string(), CODEX_APPS_MCP_SERVER_NAME.to_string(),
AsyncManagedClient { AsyncManagedClient {
@@ -712,8 +716,9 @@ async fn list_all_tools_does_not_block_when_startup_snapshot_cache_hit_is_empty(
.boxed() .boxed()
.shared(); .shared();
let approval_policy = Constrained::allow_any(AskForApproval::OnFailure); let approval_policy = Constrained::allow_any(AskForApproval::OnFailure);
let sandbox_policy = Constrained::allow_any(SandboxPolicy::new_read_only_policy()); let permission_profile = Constrained::allow_any(PermissionProfile::default());
let mut manager = McpConnectionManager::new_uninitialized(&approval_policy, &sandbox_policy); let mut manager =
McpConnectionManager::new_uninitialized(&approval_policy, &permission_profile);
manager.clients.insert( manager.clients.insert(
CODEX_APPS_MCP_SERVER_NAME.to_string(), CODEX_APPS_MCP_SERVER_NAME.to_string(),
AsyncManagedClient { AsyncManagedClient {
@@ -744,8 +749,9 @@ async fn list_all_tools_uses_startup_snapshot_when_client_startup_fails() {
.boxed() .boxed()
.shared(); .shared();
let approval_policy = Constrained::allow_any(AskForApproval::OnFailure); let approval_policy = Constrained::allow_any(AskForApproval::OnFailure);
let sandbox_policy = Constrained::allow_any(SandboxPolicy::new_read_only_policy()); let permission_profile = Constrained::allow_any(PermissionProfile::default());
let mut manager = McpConnectionManager::new_uninitialized(&approval_policy, &sandbox_policy); let mut manager =
McpConnectionManager::new_uninitialized(&approval_policy, &permission_profile);
let startup_complete = Arc::new(std::sync::atomic::AtomicBool::new(true)); let startup_complete = Arc::new(std::sync::atomic::AtomicBool::new(true));
manager.clients.insert( manager.clients.insert(
CODEX_APPS_MCP_SERVER_NAME.to_string(), 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, file_system_sandbox_policy: &FileSystemSandboxPolicy,
action: ApplyPatchAction, action: ApplyPatchAction,
) -> InternalApplyPatchInvocation { ) -> InternalApplyPatchInvocation {
let sandbox_policy = turn_context.sandbox_policy();
match assess_patch_safety( match assess_patch_safety(
&action, &action,
turn_context.approval_policy.value(), turn_context.approval_policy.value(),
turn_context.sandbox_policy.get(), &sandbox_policy,
file_system_sandbox_policy, file_system_sandbox_policy,
&turn_context.cwd, &turn_context.cwd,
turn_context.windows_sandbox_level, 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(); let memories_root = codex_home.path().join("memories").abs();
assert_eq!( assert_eq!(
config.permissions.file_system_sandbox_policy, config.permissions.file_system_sandbox_policy(),
FileSystemSandboxPolicy::restricted(vec![ FileSystemSandboxPolicy::restricted(vec![
FileSystemSandboxEntry { FileSystemSandboxEntry {
path: FileSystemPath::Special { path: FileSystemPath::Special {
@@ -814,7 +814,7 @@ async fn default_permissions_profile_populates_runtime_sandbox_policy() -> std::
} }
); );
assert_eq!( assert_eq!(
config.permissions.network_sandbox_policy, config.permissions.network_sandbox_policy(),
NetworkSandboxPolicy::Restricted NetworkSandboxPolicy::Restricted
); );
Ok(()) Ok(())
@@ -1082,7 +1082,7 @@ async fn project_root_glob_none_compiles_to_filesystem_pattern_entry() -> std::i
assert_eq!( assert_eq!(
config config
.permissions .permissions
.file_system_sandbox_policy .file_system_sandbox_policy()
.glob_scan_max_depth, .glob_scan_max_depth,
Some(2) Some(2)
); );
@@ -1092,7 +1092,7 @@ async fn project_root_glob_none_compiles_to_filesystem_pattern_entry() -> std::i
assert!( assert!(
config config
.permissions .permissions
.file_system_sandbox_policy .file_system_sandbox_policy()
.entries .entries
.contains(&FileSystemSandboxEntry { .contains(&FileSystemSandboxEntry {
path: FileSystemPath::GlobPattern { path: FileSystemPath::GlobPattern {
@@ -1104,7 +1104,7 @@ async fn project_root_glob_none_compiles_to_filesystem_pattern_entry() -> std::i
assert!( assert!(
!config !config
.permissions .permissions
.file_system_sandbox_policy .file_system_sandbox_policy()
.entries .entries
.iter() .iter()
.any(|entry| matches!( .any(|entry| matches!(
@@ -1304,7 +1304,7 @@ async fn permissions_profiles_allow_unknown_special_paths() -> std::io::Result<(
.await?; .await?;
assert_eq!( assert_eq!(
config.permissions.file_system_sandbox_policy, config.permissions.file_system_sandbox_policy(),
FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry { FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry {
path: FileSystemPath::Special { path: FileSystemPath::Special {
value: FileSystemSpecialPath::unknown( value: FileSystemSpecialPath::unknown(
@@ -1350,7 +1350,7 @@ async fn permissions_profiles_allow_unknown_special_paths_with_nested_entries()
.await?; .await?;
assert_eq!( assert_eq!(
config.permissions.file_system_sandbox_policy, config.permissions.file_system_sandbox_policy(),
FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry { FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry {
path: FileSystemPath::Special { path: FileSystemPath::Special {
value: FileSystemSpecialPath::unknown(":future_special_path", Some("docs".into())), 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?; .await?;
assert_eq!( assert_eq!(
config.permissions.file_system_sandbox_policy, config.permissions.file_system_sandbox_policy(),
FileSystemSandboxPolicy::restricted(Vec::new()) FileSystemSandboxPolicy::restricted(Vec::new())
); );
assert_eq!( assert_eq!(
@@ -1408,7 +1408,7 @@ async fn permissions_profiles_allow_empty_filesystem_with_warning() -> std::io::
.await?; .await?;
assert_eq!( assert_eq!(
config.permissions.file_system_sandbox_policy, config.permissions.file_system_sandbox_policy(),
FileSystemSandboxPolicy::restricted(Vec::new()) FileSystemSandboxPolicy::restricted(Vec::new())
); );
assert!( assert!(
@@ -1505,7 +1505,7 @@ async fn permissions_profiles_allow_network_enablement() -> std::io::Result<()>
.await?; .await?;
assert!( assert!(
config.permissions.network_sandbox_policy.is_enabled(), config.permissions.network_sandbox_policy().is_enabled(),
"expected network sandbox policy to be enabled", "expected network sandbox policy to be enabled",
); );
assert!( assert!(
@@ -1800,20 +1800,20 @@ exclude_slash_tmp = true
let sandbox_policy = config.permissions.sandbox_policy.get(); let sandbox_policy = config.permissions.sandbox_policy.get();
assert_eq!( 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()), FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(sandbox_policy, cwd.path()),
"case `{name}` should preserve filesystem semantics from legacy config" "case `{name}` should preserve filesystem semantics from legacy config"
); );
assert_eq!( assert_eq!(
config.permissions.network_sandbox_policy, config.permissions.network_sandbox_policy(),
NetworkSandboxPolicy::from(sandbox_policy), NetworkSandboxPolicy::from(sandbox_policy),
"case `{name}` should preserve network semantics from legacy config" "case `{name}` should preserve network semantics from legacy config"
); );
assert_eq!( assert_eq!(
config config
.permissions .permissions
.file_system_sandbox_policy .file_system_sandbox_policy()
.to_legacy_sandbox_policy(config.permissions.network_sandbox_policy, cwd.path()) .to_legacy_sandbox_policy(config.permissions.network_sandbox_policy(), cwd.path())
.unwrap_or_else(|err| panic!("case `{name}` should round-trip: {err}")), .unwrap_or_else(|err| panic!("case `{name}` should round-trip: {err}")),
sandbox_policy.clone(), sandbox_policy.clone(),
"case `{name}` should round-trip through split policies without drift" "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), approval_policy: Constrained::allow_any(AskForApproval::Never),
permission_profile: Constrained::allow_any(PermissionProfile::read_only()), permission_profile: Constrained::allow_any(PermissionProfile::read_only()),
sandbox_policy: Constrained::allow_any(SandboxPolicy::new_read_only_policy()), 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, network: None,
allow_login_shell: true, allow_login_shell: true,
shell_environment_policy: ShellEnvironmentPolicy::default(), 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), approval_policy: Constrained::allow_any(AskForApproval::UnlessTrusted),
permission_profile: Constrained::allow_any(PermissionProfile::read_only()), permission_profile: Constrained::allow_any(PermissionProfile::read_only()),
sandbox_policy: Constrained::allow_any(SandboxPolicy::new_read_only_policy()), 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, network: None,
allow_login_shell: true, allow_login_shell: true,
shell_environment_policy: ShellEnvironmentPolicy::default(), 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), approval_policy: Constrained::allow_any(AskForApproval::OnFailure),
permission_profile: Constrained::allow_any(PermissionProfile::read_only()), permission_profile: Constrained::allow_any(PermissionProfile::read_only()),
sandbox_policy: Constrained::allow_any(SandboxPolicy::new_read_only_policy()), 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, network: None,
allow_login_shell: true, allow_login_shell: true,
shell_environment_policy: ShellEnvironmentPolicy::default(), 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), approval_policy: Constrained::allow_any(AskForApproval::OnFailure),
permission_profile: Constrained::allow_any(PermissionProfile::read_only()), permission_profile: Constrained::allow_any(PermissionProfile::read_only()),
sandbox_policy: Constrained::allow_any(SandboxPolicy::new_read_only_policy()), 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, network: None,
allow_login_shell: true, allow_login_shell: true,
shell_environment_policy: ShellEnvironmentPolicy::default(), 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 /// Legacy projection retained while runtime call sites migrate to
/// `permission_profile`. /// `permission_profile`.
pub sandbox_policy: Constrained<SandboxPolicy>, 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. /// Effective network configuration applied to all spawned processes.
pub network: Option<NetworkProxySpec>, pub network: Option<NetworkProxySpec>,
/// Whether the model may request a login shell for shell-based tools. /// Whether the model may request a login shell for shell-based tools.
@@ -239,14 +227,14 @@ impl Permissions {
self.permission_profile.get().clone() 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 { 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 { 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 /// Replace permissions from a legacy sandbox policy and keep every
@@ -269,8 +257,6 @@ impl Permissions {
self.sandbox_policy.set(sandbox_policy)?; self.sandbox_policy.set(sandbox_policy)?;
self.permission_profile.set(permission_profile)?; self.permission_profile.set(permission_profile)?;
self.file_system_sandbox_policy = file_system_sandbox_policy;
self.network_sandbox_policy = network_sandbox_policy;
Ok(()) Ok(())
} }
@@ -294,8 +280,6 @@ impl Permissions {
self.permission_profile.set(permission_profile)?; self.permission_profile.set(permission_profile)?;
self.sandbox_policy.set(sandbox_policy)?; self.sandbox_policy.set(sandbox_policy)?;
self.file_system_sandbox_policy = file_system_sandbox_policy;
self.network_sandbox_policy = network_sandbox_policy;
Ok(()) Ok(())
} }
} }
@@ -2493,8 +2477,6 @@ impl Config {
approval_policy: constrained_approval_policy.value, approval_policy: constrained_approval_policy.value,
permission_profile: constrained_permission_profile, permission_profile: constrained_permission_profile,
sandbox_policy: constrained_sandbox_policy.value, sandbox_policy: constrained_sandbox_policy.value,
file_system_sandbox_policy: effective_file_system_sandbox_policy,
network_sandbox_policy: effective_network_sandbox_policy,
network, network,
allow_login_shell, allow_login_shell,
shell_environment_policy, 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_zsh = AbsolutePathBuf::try_from(zsh_path)?;
let expected_allowed_arg0_dir = AbsolutePathBuf::try_from(allowed_arg0_dir)?; let expected_allowed_arg0_dir = AbsolutePathBuf::try_from(allowed_arg0_dir)?;
let expected_sibling_arg0_dir = AbsolutePathBuf::try_from(sibling_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!( assert!(
policy.can_read_path_with_cwd(expected_zsh.as_path(), &cwd), 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::EnvironmentManager;
use codex_exec_server::EnvironmentManagerArgs; use codex_exec_server::EnvironmentManagerArgs;
use codex_exec_server::ExecServerRuntimePaths; use codex_exec_server::ExecServerRuntimePaths;
use codex_protocol::protocol::SandboxPolicy; use codex_protocol::models::PermissionProfile;
use codex_tools::DiscoverableTool; use codex_tools::DiscoverableTool;
use rmcp::model::ToolAnnotations; use rmcp::model::ToolAnnotations;
use serde::Deserialize; use serde::Deserialize;
@@ -274,7 +274,7 @@ pub async fn list_accessible_connectors_from_mcp_tools_with_environment_manager(
&config.permissions.approval_policy, &config.permissions.approval_policy,
INITIAL_SUBMIT_ID.to_owned(), INITIAL_SUBMIT_ID.to_owned(),
tx_event, tx_event,
SandboxPolicy::new_read_only_policy(), PermissionProfile::default(),
McpRuntimeEnvironment::new(environment, config.cwd.to_path_buf()), McpRuntimeEnvironment::new(environment, config.cwd.to_path_buf()),
config.codex_home.to_path_buf(), config.codex_home.to_path_buf(),
codex_apps_tools_cache_key(auth.as_ref()), codex_apps_tools_cache_key(auth.as_ref()),
+1 -1
View File
@@ -221,7 +221,7 @@ async fn should_install_mcp_dependencies(
) -> bool { ) -> bool {
if mcp_permission_prompt_is_auto_approved( if mcp_permission_prompt_is_auto_approved(
turn_context.approval_policy.value(), turn_context.approval_policy.value(),
turn_context.sandbox_policy.get(), &turn_context.permission_profile(),
) { ) {
return true; 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 { let sandbox_state = serde_json::to_value(SandboxState {
permission_profile: Some(turn_context.permission_profile()), 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(), codex_linux_sandbox_exe: turn_context.codex_linux_sandbox_exe.clone(),
sandbox_cwd: turn_context.cwd.to_path_buf(), sandbox_cwd: turn_context.cwd.to_path_buf(),
use_legacy_landlock: turn_context.features.use_legacy_landlock(), use_legacy_landlock: turn_context.features.use_legacy_landlock(),
@@ -830,7 +830,7 @@ async fn maybe_request_mcp_tool_approval(
) -> Option<McpToolApprovalDecision> { ) -> Option<McpToolApprovalDecision> {
if mcp_permission_prompt_is_auto_approved( if mcp_permission_prompt_is_auto_approved(
turn_context.approval_policy.value(), turn_context.approval_policy.value(),
turn_context.sandbox_policy.get(), &turn_context.permission_profile(),
) { ) {
return None; return None;
} }
+2 -5
View File
@@ -16,8 +16,8 @@ use codex_config::types::McpServerToolConfig;
use codex_hooks::Hooks; use codex_hooks::Hooks;
use codex_hooks::HooksConfig; use codex_hooks::HooksConfig;
use codex_model_provider::create_model_provider; use codex_model_provider::create_model_provider;
use codex_protocol::models::PermissionProfile;
use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::SandboxPolicy;
use core_test_support::PathExt; use core_test_support::PathExt;
use core_test_support::responses::ev_assistant_message; use core_test_support::responses::ev_assistant_message;
use core_test_support::responses::ev_completed; 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 .approval_policy
.set(AskForApproval::Never) .set(AskForApproval::Never)
.expect("test setup should allow updating approval policy"); .expect("test setup should allow updating approval policy");
turn_context turn_context.permission_profile = PermissionProfile::Disabled;
.sandbox_policy
.set(SandboxPolicy::DangerFullAccess)
.expect("test setup should allow updating sandbox policy");
let mut config = (*turn_context.config).clone(); let mut config = (*turn_context.config).clone();
config.chatgpt_base_url = server.uri(); config.chatgpt_base_url = server.uri();
turn_context.config = Arc::new(config); turn_context.config = Arc::new(config);
+33 -23
View File
@@ -432,6 +432,7 @@ mod phase2 {
use codex_login::CodexAuth; use codex_login::CodexAuth;
use codex_protocol::AgentPath; use codex_protocol::AgentPath;
use codex_protocol::ThreadId; use codex_protocol::ThreadId;
use codex_protocol::models::PermissionProfile;
use codex_protocol::permissions::FileSystemSandboxPolicy; use codex_protocol::permissions::FileSystemSandboxPolicy;
use codex_protocol::permissions::NetworkSandboxPolicy; use codex_protocol::permissions::NetworkSandboxPolicy;
use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::AskForApproval;
@@ -482,8 +483,14 @@ mod phase2 {
codex_utils_absolute_path::AbsolutePathBuf::from_absolute_path(codex_home.path()) codex_utils_absolute_path::AbsolutePathBuf::from_absolute_path(codex_home.path())
.expect("codex home is absolute"); .expect("codex home is absolute");
config.cwd = config.codex_home.clone(); config.cwd = config.codex_home.clone();
config.permissions.file_system_sandbox_policy = FileSystemSandboxPolicy::unrestricted(); let permission_profile = PermissionProfile::from_runtime_permissions(
config.permissions.network_sandbox_policy = NetworkSandboxPolicy::Enabled; &FileSystemSandboxPolicy::unrestricted(),
NetworkSandboxPolicy::Enabled,
);
config
.permissions
.set_permission_profile(permission_profile, config.cwd.as_path())
.expect("permissions are configurable");
configure(&mut config); configure(&mut config);
let config = Arc::new(config); let config = Arc::new(config);
@@ -712,14 +719,16 @@ mod phase2 {
memory_root(&harness.config.codex_home).as_path() memory_root(&harness.config.codex_home).as_path()
); );
match &config_snapshot.sandbox_policy { match &config_snapshot.sandbox_policy {
SandboxPolicy::WorkspaceWrite { SandboxPolicy::WorkspaceWrite { network_access, .. } => {
writable_roots,
network_access,
..
} => {
assert!(!*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!( pretty_assertions::assert_eq!(
writable_roots.as_slice(), effective_writable_roots.as_slice(),
[memory_root(&harness.config.codex_home)], [memory_root(&harness.config.codex_home)],
"consolidation subagent should only be able to write the memory root" "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" "memory consolidation should not be registered in the root collab agent registry"
); );
let turn_context = subagent.codex.session.new_default_turn().await; let turn_context = subagent.codex.session.new_default_turn().await;
pretty_assertions::assert_eq!( let file_system_sandbox_policy = turn_context.file_system_sandbox_policy();
turn_context.file_system_sandbox_policy, let legacy_file_system_sandbox_policy =
FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd( FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(
&config_snapshot.sandbox_policy, &config_snapshot.sandbox_policy,
config_snapshot.cwd.as_path(), 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" "consolidation subagent split filesystem policy should match the memory-root legacy policy"
); );
assert!( assert!(
turn_context file_system_sandbox_policy.can_write_path_with_cwd(
.file_system_sandbox_policy memory_root(&harness.config.codex_home).as_path(),
.can_write_path_with_cwd( config_snapshot.cwd.as_path(),
memory_root(&harness.config.codex_home).as_path(), ),
config_snapshot.cwd.as_path(),
),
"consolidation subagent should be able to write the memory root" "consolidation subagent should be able to write the memory root"
); );
assert!( assert!(
!turn_context !file_system_sandbox_policy.can_write_path_with_cwd(
.file_system_sandbox_policy harness.config.codex_home.join("config.toml").as_path(),
.can_write_path_with_cwd( config_snapshot.cwd.as_path(),
harness.config.codex_home.join("config.toml").as_path(), ),
config_snapshot.cwd.as_path(),
),
"consolidation subagent should not inherit codex_home write access" "consolidation subagent should not inherit codex_home write access"
); );
pretty_assertions::assert_eq!( pretty_assertions::assert_eq!(
turn_context.network_sandbox_policy, turn_context.network_sandbox_policy(),
NetworkSandboxPolicy::Restricted, NetworkSandboxPolicy::Restricted,
"consolidation subagent split network policy should preserve no-network sandboxing" "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.approval_policy,
turn_context.sub_id.clone(), turn_context.sub_id.clone(),
self.get_tx_event(), self.get_tx_event(),
turn_context.sandbox_policy.get().clone(), turn_context.permission_profile(),
McpRuntimeEnvironment::new( McpRuntimeEnvironment::new(
turn_context turn_context
.environment .environment
+4 -8
View File
@@ -125,7 +125,6 @@ use codex_rollout::state_db;
use codex_rollout_trace::AgentResultTracePayload; use codex_rollout_trace::AgentResultTracePayload;
use codex_rollout_trace::ThreadStartedTraceMetadata; use codex_rollout_trace::ThreadStartedTraceMetadata;
use codex_rollout_trace::ThreadTraceContext; use codex_rollout_trace::ThreadTraceContext;
use codex_sandboxing::compatibility_sandbox_policy_for_permission_profile;
use codex_sandboxing::policy_transforms::intersect_permission_profiles; use codex_sandboxing::policy_transforms::intersect_permission_profiles;
use codex_shell_command::parse_command::parse_command; use codex_shell_command::parse_command::parse_command;
use codex_terminal_detection::user_agent; use codex_terminal_detection::user_agent;
@@ -606,9 +605,6 @@ impl Codex {
approval_policy: config.permissions.approval_policy.clone(), approval_policy: config.permissions.approval_policy.clone(),
approvals_reviewer: config.approvals_reviewer, approvals_reviewer: config.approvals_reviewer,
permission_profile: config.permissions.permission_profile.clone(), 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), windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
cwd: config.cwd.clone(), cwd: config.cwd.clone(),
codex_home: config.codex_home.clone(), codex_home: config.codex_home.clone(),
@@ -939,8 +935,7 @@ impl Session {
return; return;
}; };
let spec = match spec let spec = match spec.recompute_for_sandbox_policy(&session_configuration.sandbox_policy())
.recompute_for_sandbox_policy(session_configuration.sandbox_policy.get())
{ {
Ok(spec) => spec, Ok(spec) => spec,
Err(err) => { Err(err) => {
@@ -1301,8 +1296,9 @@ impl Session {
}; };
let previous_cwd = state.session_configuration.cwd.clone(); let previous_cwd = state.session_configuration.cwd.clone();
let sandbox_policy_changed = let previous_sandbox_policy = state.session_configuration.sandbox_policy();
state.session_configuration.sandbox_policy != updated.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 next_cwd = updated.cwd.clone();
let codex_home = updated.codex_home.clone(); let codex_home = updated.codex_home.clone();
let session_source = updated.session_source.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, personality: parent_turn_context.personality,
approval_policy: parent_turn_context.approval_policy.clone(), approval_policy: parent_turn_context.approval_policy.clone(),
permission_profile: parent_turn_context.permission_profile(), 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(), network: parent_turn_context.network.clone(),
windows_sandbox_level: parent_turn_context.windows_sandbox_level, windows_sandbox_level: parent_turn_context.windows_sandbox_level,
shell_environment_policy: parent_turn_context.shell_environment_policy.clone(), 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(), current_date: turn_context.current_date.clone(),
timezone: turn_context.timezone.clone(), timezone: turn_context.timezone.clone(),
approval_policy: turn_context.approval_policy.value(), approval_policy: turn_context.approval_policy.value(),
sandbox_policy: turn_context.sandbox_policy.get().clone(), sandbox_policy: turn_context.sandbox_policy(),
permission_profile: None, permission_profile: None,
network: None, network: None,
file_system_sandbox_policy: 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(), current_date: turn_context.current_date.clone(),
timezone: turn_context.timezone.clone(), timezone: turn_context.timezone.clone(),
approval_policy: turn_context.approval_policy.value(), approval_policy: turn_context.approval_policy.value(),
sandbox_policy: turn_context.sandbox_policy.get().clone(), sandbox_policy: turn_context.sandbox_policy(),
permission_profile: None, permission_profile: None,
network: None, network: None,
file_system_sandbox_policy: 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(), current_date: turn_context.current_date.clone(),
timezone: turn_context.timezone.clone(), timezone: turn_context.timezone.clone(),
approval_policy: turn_context.approval_policy.value(), approval_policy: turn_context.approval_policy.value(),
sandbox_policy: turn_context.sandbox_policy.get().clone(), sandbox_policy: turn_context.sandbox_policy(),
permission_profile: None, permission_profile: None,
network: None, network: None,
file_system_sandbox_policy: 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(), current_date: turn_context.current_date.clone(),
timezone: turn_context.timezone.clone(), timezone: turn_context.timezone.clone(),
approval_policy: turn_context.approval_policy.value(), approval_policy: turn_context.approval_policy.value(),
sandbox_policy: turn_context.sandbox_policy.get().clone(), sandbox_policy: turn_context.sandbox_policy(),
permission_profile: None, permission_profile: None,
network: None, network: None,
file_system_sandbox_policy: 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(), current_date: turn_context.current_date.clone(),
timezone: turn_context.timezone.clone(), timezone: turn_context.timezone.clone(),
approval_policy: turn_context.approval_policy.value(), approval_policy: turn_context.approval_policy.value(),
sandbox_policy: turn_context.sandbox_policy.get().clone(), sandbox_policy: turn_context.sandbox_policy(),
permission_profile: None, permission_profile: None,
network: None, network: None,
file_system_sandbox_policy: 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(), current_date: turn_context.current_date.clone(),
timezone: turn_context.timezone.clone(), timezone: turn_context.timezone.clone(),
approval_policy: turn_context.approval_policy.value(), approval_policy: turn_context.approval_policy.value(),
sandbox_policy: turn_context.sandbox_policy.get().clone(), sandbox_policy: turn_context.sandbox_policy(),
permission_profile: None, permission_profile: None,
network: None, network: None,
file_system_sandbox_policy: 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(), current_date: turn_context.current_date.clone(),
timezone: turn_context.timezone.clone(), timezone: turn_context.timezone.clone(),
approval_policy: turn_context.approval_policy.value(), approval_policy: turn_context.approval_policy.value(),
sandbox_policy: turn_context.sandbox_policy.get().clone(), sandbox_policy: turn_context.sandbox_policy(),
permission_profile: None, permission_profile: None,
network: None, network: None,
file_system_sandbox_policy: 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(), current_date: turn_context.current_date.clone(),
timezone: turn_context.timezone.clone(), timezone: turn_context.timezone.clone(),
approval_policy: turn_context.approval_policy.value(), approval_policy: turn_context.approval_policy.value(),
sandbox_policy: turn_context.sandbox_policy.get().clone(), sandbox_policy: turn_context.sandbox_policy(),
permission_profile: None, permission_profile: None,
network: None, network: None,
file_system_sandbox_policy: None, file_system_sandbox_policy: None,
+67 -48
View File
@@ -1,5 +1,7 @@
use super::*; use super::*;
use crate::goals::GoalRuntimeState; use crate::goals::GoalRuntimeState;
use codex_protocol::permissions::FileSystemPath;
use codex_protocol::permissions::FileSystemSpecialPath;
use tokio::sync::Semaphore; use tokio::sync::Semaphore;
/// Context for an initialized model agent /// Context for an initialized model agent
@@ -58,12 +60,6 @@ pub(crate) struct SessionConfiguration {
pub(super) approvals_reviewer: ApprovalsReviewer, pub(super) approvals_reviewer: ApprovalsReviewer,
/// Canonical permission profile for the session. /// Canonical permission profile for the session.
pub(super) permission_profile: Constrained<PermissionProfile>, 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, pub(super) windows_sandbox_level: WindowsSandboxLevel,
/// Absolute working directory that should be treated as the *root* of the /// 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 { 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 { 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 { pub(super) fn thread_config_snapshot(&self) -> ThreadConfigSnapshot {
@@ -129,11 +138,30 @@ impl SessionConfiguration {
pub(crate) fn apply(&self, updates: &SessionSettingsUpdate) -> ConstraintResult<Self> { pub(crate) fn apply(&self, updates: &SessionSettingsUpdate) -> ConstraintResult<Self> {
let mut next_configuration = self.clone(); let mut next_configuration = self.clone();
let file_system_policy_matches_legacy = self.file_system_sandbox_policy let current_sandbox_policy = self.sandbox_policy();
== FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd( let current_file_system_sandbox_policy = self.file_system_sandbox_policy();
self.sandbox_policy.get(), 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, &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() { if let Some(collaboration_mode) = updates.collaboration_mode.clone() {
next_configuration.collaboration_mode = collaboration_mode; next_configuration.collaboration_mode = collaboration_mode;
} }
@@ -181,42 +209,41 @@ impl SessionConfiguration {
if let Some(permission_profile) = updates.permission_profile.clone() { if let Some(permission_profile) = updates.permission_profile.clone() {
next_configuration.set_permission_profile_projection( next_configuration.set_permission_profile_projection(
permission_profile, permission_profile,
Some(&self.file_system_sandbox_policy), Some(&current_file_system_sandbox_policy),
)?; )?;
} else if let Some(sandbox_policy) = updates.sandbox_policy.clone() { } else if let Some(sandbox_policy) = updates.sandbox_policy.clone() {
next_configuration.sandbox_policy.set(sandbox_policy)?; let file_system_sandbox_policy =
next_configuration.file_system_sandbox_policy =
FileSystemSandboxPolicy::from_legacy_sandbox_policy_preserving_deny_entries( FileSystemSandboxPolicy::from_legacy_sandbox_policy_preserving_deny_entries(
next_configuration.sandbox_policy.get(), &sandbox_policy,
&next_configuration.cwd, &next_configuration.cwd,
&self.file_system_sandbox_policy, &current_file_system_sandbox_policy,
); );
next_configuration.network_sandbox_policy = let network_sandbox_policy = NetworkSandboxPolicy::from(&sandbox_policy);
NetworkSandboxPolicy::from(next_configuration.sandbox_policy.get());
next_configuration.permission_profile.set( next_configuration.permission_profile.set(
PermissionProfile::from_runtime_permissions_with_enforcement( PermissionProfile::from_runtime_permissions_with_enforcement(
SandboxEnforcement::from_legacy_sandbox_policy( SandboxEnforcement::from_legacy_sandbox_policy(&sandbox_policy),
next_configuration.sandbox_policy.get(), &file_system_sandbox_policy,
), network_sandbox_policy,
&next_configuration.file_system_sandbox_policy,
next_configuration.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 // Preserve richer split policies across cwd-only updates; only
// rederive when the session is already using the legacy bridge. // rederive when the session is already using a structurally
next_configuration.file_system_sandbox_policy = // cwd-bound legacy bridge.
FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd( let file_system_sandbox_policy =
next_configuration.sandbox_policy.get(), FileSystemSandboxPolicy::from_legacy_sandbox_policy_preserving_deny_entries(
&current_sandbox_policy,
&next_configuration.cwd, &next_configuration.cwd,
&current_file_system_sandbox_policy,
); );
next_configuration.permission_profile.set( next_configuration.permission_profile.set(
PermissionProfile::from_runtime_permissions_with_enforcement( PermissionProfile::from_runtime_permissions_with_enforcement(
SandboxEnforcement::from_legacy_sandbox_policy( SandboxEnforcement::from_legacy_sandbox_policy(&current_sandbox_policy),
next_configuration.sandbox_policy.get(), &file_system_sandbox_policy,
), current_network_sandbox_policy,
&next_configuration.file_system_sandbox_policy,
next_configuration.network_sandbox_policy,
), ),
)?; )?;
} }
@@ -247,16 +274,7 @@ impl SessionConfiguration {
&file_system_sandbox_policy, &file_system_sandbox_policy,
network_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.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(()) Ok(())
} }
} }
@@ -487,7 +505,7 @@ impl Session {
model: session_configuration.collaboration_mode.model().to_string(), model: session_configuration.collaboration_mode.model().to_string(),
provider_name: config.model_provider_id.clone(), provider_name: config.model_provider_id.clone(),
approval_policy: session_configuration.approval_policy.value().to_string(), 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!( let rollout_thread_trace = if matches!(
session_configuration.session_source, session_configuration.session_source,
@@ -768,7 +786,7 @@ impl Session {
// setup is straightforward enough and performs well. // setup is straightforward enough and performs well.
mcp_connection_manager: Arc::new(RwLock::new(McpConnectionManager::new_uninitialized( mcp_connection_manager: Arc::new(RwLock::new(McpConnectionManager::new_uninitialized(
&config.permissions.approval_policy, &config.permissions.approval_policy,
&config.permissions.sandbox_policy, &config.permissions.permission_profile,
))), ))),
mcp_startup_cancellation_token: Mutex::new(CancellationToken::new()), mcp_startup_cancellation_token: Mutex::new(CancellationToken::new()),
unified_exec_manager: UnifiedExecProcessManager::new( unified_exec_manager: UnifiedExecProcessManager::new(
@@ -847,6 +865,7 @@ impl Session {
// Dispatch the SessionConfiguredEvent first and then report any errors. // Dispatch the SessionConfiguredEvent first and then report any errors.
// If resuming, include converted initial messages in the payload so UIs can render them immediately. // If resuming, include converted initial messages in the payload so UIs can render them immediately.
let initial_messages = initial_history.get_event_msgs(); let initial_messages = initial_history.get_event_msgs();
let session_sandbox_policy = session_configuration.sandbox_policy();
let events = std::iter::once(Event { let events = std::iter::once(Event {
id: INITIAL_SUBMIT_ID.to_owned(), id: INITIAL_SUBMIT_ID.to_owned(),
msg: EventMsg::SessionConfigured(SessionConfiguredEvent { msg: EventMsg::SessionConfigured(SessionConfiguredEvent {
@@ -858,7 +877,7 @@ impl Session {
service_tier: session_configuration.service_tier, service_tier: session_configuration.service_tier,
approval_policy: session_configuration.approval_policy.value(), approval_policy: session_configuration.approval_policy.value(),
approvals_reviewer: session_configuration.approvals_reviewer, 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()), permission_profile: Some(session_configuration.permission_profile()),
cwd: session_configuration.cwd.clone(), cwd: session_configuration.cwd.clone(),
reasoning_effort: session_configuration.collaboration_mode.reasoning_effort(), reasoning_effort: session_configuration.collaboration_mode.reasoning_effort(),
@@ -867,7 +886,7 @@ impl Session {
initial_messages, initial_messages,
network_proxy: session_network_proxy.filter(|_| { network_proxy: session_network_proxy.filter(|_| {
Self::managed_network_proxy_active_for_sandbox_policy( Self::managed_network_proxy_active_for_sandbox_policy(
session_configuration.sandbox_policy.get(), &session_sandbox_policy,
) )
}), }),
rollout_path, rollout_path,
@@ -901,7 +920,7 @@ impl Session {
&session_configuration.approval_policy, &session_configuration.approval_policy,
INITIAL_SUBMIT_ID.to_owned(), INITIAL_SUBMIT_ID.to_owned(),
tx_event.clone(), tx_event.clone(),
session_configuration.sandbox_policy.get().clone(), session_configuration.permission_profile(),
McpRuntimeEnvironment::new( McpRuntimeEnvironment::new(
sess.services sess.services
.environment_manager .environment_manager
+148 -84
View File
@@ -38,6 +38,7 @@ use codex_protocol::models::FileSystemPermissions;
use codex_protocol::models::FunctionCallOutputBody; use codex_protocol::models::FunctionCallOutputBody;
use codex_protocol::models::FunctionCallOutputPayload; use codex_protocol::models::FunctionCallOutputPayload;
use codex_protocol::models::PermissionProfile; use codex_protocol::models::PermissionProfile;
use codex_protocol::models::SandboxEnforcement;
use codex_protocol::permissions::FileSystemAccessMode; use codex_protocol::permissions::FileSystemAccessMode;
use codex_protocol::permissions::FileSystemPath; use codex_protocol::permissions::FileSystemPath;
use codex_protocol::permissions::FileSystemSandboxEntry; 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 state = session.state.lock().await;
let mut config = (*state.session_configuration.original_config_do_not_use).clone(); let mut config = (*state.session_configuration.original_config_do_not_use).clone();
config.permissions.network = Some(spec); config.permissions.network = Some(spec);
config.permissions.sandbox_policy = let cwd = config.cwd.clone();
codex_config::Constrained::allow_any(initial_policy.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.original_config_do_not_use = Arc::new(config);
state.session_configuration.sandbox_policy = state
codex_config::Constrained::allow_any(initial_policy); .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); 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| { let session = make_session_with_config(move |config| {
config.permissions.sandbox_policy = config.permissions.sandbox_policy =
codex_config::Constrained::allow_any(SandboxPolicy::DangerFullAccess); codex_config::Constrained::allow_any(SandboxPolicy::DangerFullAccess);
config.permissions.permission_profile =
codex_config::Constrained::allow_any(PermissionProfile::Disabled);
config.permissions.network = Some(network_spec); config.permissions.network = Some(network_spec);
}) })
.await?; .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| { let session = make_session_with_config(move |config| {
config.permissions.sandbox_policy = config.permissions.sandbox_policy =
codex_config::Constrained::allow_any(SandboxPolicy::DangerFullAccess); codex_config::Constrained::allow_any(SandboxPolicy::DangerFullAccess);
config.permissions.permission_profile =
codex_config::Constrained::allow_any(PermissionProfile::Disabled);
config.permissions.network = Some(network_spec); config.permissions.network = Some(network_spec);
let layers = config 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 expected_sandbox_policy = sandbox_policy.clone();
let mut builder = test_codex().with_config(move |config| { let mut builder = test_codex().with_config(move |config| {
config.permissions.sandbox_policy = codex_config::Constrained::allow_any(sandbox_policy); 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 = config.permissions.permission_profile =
codex_config::Constrained::allow_any(PermissionProfile::from_runtime_permissions( codex_config::Constrained::allow_any(PermissionProfile::from_runtime_permissions(
&config.permissions.file_system_sandbox_policy, &FileSystemSandboxPolicy::external_sandbox(),
config.permissions.network_sandbox_policy, NetworkSandboxPolicy::Restricted,
)); ));
}); });
@@ -1654,7 +1665,7 @@ async fn record_initial_history_forked_hydrates_previous_turn_settings() {
current_date: turn_context.current_date.clone(), current_date: turn_context.current_date.clone(),
timezone: turn_context.timezone.clone(), timezone: turn_context.timezone.clone(),
approval_policy: turn_context.approval_policy.value(), approval_policy: turn_context.approval_policy.value(),
sandbox_policy: turn_context.sandbox_policy.get().clone(), sandbox_policy: turn_context.sandbox_policy(),
permission_profile: None, permission_profile: None,
network: None, network: None,
file_system_sandbox_policy: 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(), approval_policy: config.permissions.approval_policy.clone(),
approvals_reviewer: config.approvals_reviewer, approvals_reviewer: config.approvals_reviewer,
permission_profile: config.permissions.permission_profile.clone(), 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), windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
cwd: config.cwd.clone(), cwd: config.cwd.clone(),
codex_home: config.codex_home.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(), approval_policy: config.permissions.approval_policy.clone(),
approvals_reviewer: config.approvals_reviewer, approvals_reviewer: config.approvals_reviewer,
permission_profile: config.permissions.permission_profile.clone(), 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), windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
cwd: config.cwd.clone(), cwd: config.cwd.clone(),
codex_home: config.codex_home.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(), approval_policy: config.permissions.approval_policy.clone(),
approvals_reviewer: config.approvals_reviewer, approvals_reviewer: config.approvals_reviewer,
permission_profile: config.permissions.permission_profile.clone(), 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), windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
cwd: config.cwd.clone(), cwd: config.cwd.clone(),
codex_home: config.codex_home.clone(), codex_home: config.codex_home.clone(),
@@ -2840,7 +2842,7 @@ fn turn_environments_for_tests(
} }
#[tokio::test] #[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 mut session_configuration = make_session_configuration_for_tests().await;
let workspace = tempfile::tempdir().expect("create temp dir"); let workspace = tempfile::tempdir().expect("create temp dir");
let project_root = workspace.path().join("project"); 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(); let docs_dir = docs_dir.abs();
session_configuration.cwd = original_cwd.abs(); session_configuration.cwd = original_cwd.abs();
session_configuration.sandbox_policy = let sandbox_policy = SandboxPolicy::WorkspaceWrite {
codex_config::Constrained::allow_any(SandboxPolicy::WorkspaceWrite { writable_roots: Vec::new(),
writable_roots: Vec::new(), network_access: false,
network_access: false, exclude_tmpdir_env_var: true,
exclude_tmpdir_env_var: true, exclude_slash_tmp: true,
exclude_slash_tmp: true, };
}); let file_system_sandbox_policy = FileSystemSandboxPolicy::restricted(vec![
session_configuration.file_system_sandbox_policy = FileSystemSandboxPolicy::restricted(vec![
FileSystemSandboxEntry { FileSystemSandboxEntry {
path: FileSystemPath::Special { path: FileSystemPath::Special {
value: FileSystemSpecialPath::CurrentWorkingDirectory, value: FileSystemSpecialPath::CurrentWorkingDirectory,
@@ -2869,6 +2870,14 @@ async fn session_configuration_apply_preserves_split_file_system_policy_on_cwd_o
access: FileSystemAccessMode::Read, 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 let updated = session_configuration
.apply(&SessionSettingsUpdate { .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"); .expect("cwd-only update should succeed");
assert_eq!( assert_eq!(
updated.file_system_sandbox_policy, updated.file_system_sandbox_policy(),
session_configuration.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(); session_configuration.cwd = cwd.path().abs();
let workspace_policy = SandboxPolicy::new_workspace_write_policy(); let workspace_policy = SandboxPolicy::new_workspace_write_policy();
session_configuration.sandbox_policy =
codex_config::Constrained::allow_any(workspace_policy.clone());
let deny_entry = FileSystemSandboxEntry { let deny_entry = FileSystemSandboxEntry {
path: FileSystemPath::GlobPattern { path: FileSystemPath::GlobPattern {
pattern: "**/*.env".to_string(), 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.glob_scan_max_depth = Some(2);
existing_file_system_policy.entries.push(deny_entry.clone()); 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( let requested_file_system_policy = FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(
&workspace_policy, &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.glob_scan_max_depth = Some(2);
expected_file_system_policy.entries.push(deny_entry); expected_file_system_policy.entries.push(deny_entry);
assert_eq!( assert_eq!(
updated.file_system_sandbox_policy, updated.file_system_sandbox_policy(),
expected_file_system_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 project_root = workspace.path().join("project");
let original_cwd = project_root.join("subdir"); let original_cwd = project_root.join("subdir");
session_configuration.cwd = original_cwd.abs(); session_configuration.cwd = original_cwd.abs();
session_configuration.sandbox_policy = let sandbox_policy = SandboxPolicy::WorkspaceWrite {
codex_config::Constrained::allow_any(SandboxPolicy::WorkspaceWrite { writable_roots: Vec::new(),
writable_roots: Vec::new(), network_access: false,
network_access: false, exclude_tmpdir_env_var: true,
exclude_tmpdir_env_var: true, exclude_slash_tmp: true,
exclude_slash_tmp: true, };
}); let file_system_sandbox_policy = FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(
session_configuration.file_system_sandbox_policy = &sandbox_policy,
FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd( &session_configuration.cwd,
session_configuration.sandbox_policy.get(), );
&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 let updated = session_configuration
.apply(&SessionSettingsUpdate { .apply(&SessionSettingsUpdate {
@@ -3090,12 +3108,73 @@ async fn session_configuration_apply_rederives_legacy_file_system_policy_on_cwd_
}) })
.expect("cwd-only update should succeed"); .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!( assert_eq!(
updated.file_system_sandbox_policy, updated.file_system_sandbox_policy(),
FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd( file_system_sandbox_policy
updated.sandbox_policy.get(), );
&project_root, 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(), approval_policy: config.permissions.approval_policy.clone(),
approvals_reviewer: config.approvals_reviewer, approvals_reviewer: config.approvals_reviewer,
permission_profile: config.permissions.permission_profile.clone(), 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), windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
cwd: config.cwd.clone(), cwd: config.cwd.clone(),
codex_home: config.codex_home.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(), approval_policy: config.permissions.approval_policy.clone(),
approvals_reviewer: config.approvals_reviewer, approvals_reviewer: config.approvals_reviewer,
permission_profile: config.permissions.permission_profile.clone(), 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), windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
cwd: config.cwd.clone(), cwd: config.cwd.clone(),
codex_home: config.codex_home.clone(), codex_home: config.codex_home.clone(),
@@ -3325,7 +3398,7 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
let services = SessionServices { let services = SessionServices {
mcp_connection_manager: Arc::new(RwLock::new(McpConnectionManager::new_uninitialized( mcp_connection_manager: Arc::new(RwLock::new(McpConnectionManager::new_uninitialized(
&config.permissions.approval_policy, &config.permissions.approval_policy,
&config.permissions.sandbox_policy, &config.permissions.permission_profile,
))), ))),
mcp_startup_cancellation_token: Mutex::new(CancellationToken::new()), mcp_startup_cancellation_token: Mutex::new(CancellationToken::new()),
unified_exec_manager: UnifiedExecProcessManager::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(), approval_policy: config.permissions.approval_policy.clone(),
approvals_reviewer: config.approvals_reviewer, approvals_reviewer: config.approvals_reviewer,
permission_profile: config.permissions.permission_profile.clone(), 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), windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
cwd: config.cwd.clone(), cwd: config.cwd.clone(),
codex_home: config.codex_home.clone(), codex_home: config.codex_home.clone(),
@@ -4642,9 +4712,6 @@ where
approval_policy: config.permissions.approval_policy.clone(), approval_policy: config.permissions.approval_policy.clone(),
approvals_reviewer: config.approvals_reviewer, approvals_reviewer: config.approvals_reviewer,
permission_profile: config.permissions.permission_profile.clone(), 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), windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
cwd: config.cwd.clone(), cwd: config.cwd.clone(),
codex_home: config.codex_home.clone(), codex_home: config.codex_home.clone(),
@@ -4690,7 +4757,7 @@ where
let services = SessionServices { let services = SessionServices {
mcp_connection_manager: Arc::new(RwLock::new(McpConnectionManager::new_uninitialized( mcp_connection_manager: Arc::new(RwLock::new(McpConnectionManager::new_uninitialized(
&config.permissions.approval_policy, &config.permissions.approval_policy,
&config.permissions.sandbox_policy, &config.permissions.permission_profile,
))), ))),
mcp_startup_cancellation_token: Mutex::new(CancellationToken::new()), mcp_startup_cancellation_token: Mutex::new(CancellationToken::new()),
unified_exec_manager: UnifiedExecProcessManager::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 { fn file_system_policy_with_unreadable_glob(turn_context: &TurnContext) -> FileSystemSandboxPolicy {
let mut policy = FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd( let mut policy = FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(
turn_context.sandbox_policy.get(), &turn_context.sandbox_policy(),
&turn_context.cwd, &turn_context.cwd,
); );
policy.entries.push(FileSystemSandboxEntry { policy.entries.push(FileSystemSandboxEntry {
@@ -5586,12 +5653,7 @@ fn file_system_policy_with_unreadable_glob(turn_context: &TurnContext) -> FileSy
#[tokio::test] #[tokio::test]
async fn turn_context_item_omits_legacy_equivalent_file_system_sandbox_policy() { async fn turn_context_item_omits_legacy_equivalent_file_system_sandbox_policy() {
let (_session, mut turn_context) = make_session_and_context().await; let (_session, 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 item = turn_context.to_turn_context_item(); 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() { async fn turn_context_item_stores_split_file_system_sandbox_policy_when_different() {
let (_session, mut turn_context) = make_session_and_context().await; let (_session, mut turn_context) = make_session_and_context().await;
let file_system_sandbox_policy = file_system_policy_with_unreadable_glob(&turn_context); 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(); 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 (mut session, mut turn_context) = make_session_and_context().await;
let file_system_sandbox_policy = file_system_policy_with_unreadable_glob(&turn_context); 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; let rollout_path = attach_thread_persistence(&mut session).await;
session session
@@ -7674,7 +7744,6 @@ async fn rejects_escalated_permissions_when_policy_not_on_request() {
use crate::tools::sandboxing::ExecApprovalRequirement; use crate::tools::sandboxing::ExecApprovalRequirement;
use crate::turn_diff_tracker::TurnDiffTracker; use crate::turn_diff_tracker::TurnDiffTracker;
use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::SandboxPolicy;
use std::collections::HashMap; use std::collections::HashMap;
let (session, mut turn_context_raw) = make_session_and_context().await; 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 // command. Force DangerFullAccess so this check stays focused on approval
// policy rather than platform-specific sandbox behavior. // policy rather than platform-specific sandbox behavior.
let turn_context_mut = Arc::get_mut(&mut turn_context).expect("unique turn context Arc"); let turn_context_mut = Arc::get_mut(&mut turn_context).expect("unique turn context Arc");
turn_context_mut turn_context_mut.permission_profile = PermissionProfile::Disabled;
.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());
let file_system_sandbox_policy = turn_context.file_system_sandbox_policy();
let sandbox_policy = turn_context.sandbox_policy();
let exec_approval_requirement = session let exec_approval_requirement = session
.services .services
.exec_policy .exec_policy
.create_exec_approval_requirement_for_command(ExecApprovalRequest { .create_exec_approval_requirement_for_command(ExecApprovalRequest {
command: &params.command, command: &params.command,
approval_policy: turn_context.approval_policy.value(), approval_policy: turn_context.approval_policy.value(),
sandbox_policy: turn_context.sandbox_policy.get(), sandbox_policy: &sandbox_policy,
file_system_sandbox_policy: &turn_context.file_system_sandbox_policy, file_system_sandbox_policy: &file_system_sandbox_policy,
sandbox_permissions: SandboxPermissions::UseDefault, sandbox_permissions: SandboxPermissions::UseDefault,
prefix_rule: None, prefix_rule: None,
}) })
@@ -25,8 +25,6 @@ use codex_protocol::models::ContentItem;
use codex_protocol::models::NetworkPermissions; use codex_protocol::models::NetworkPermissions;
use codex_protocol::models::ResponseItem; use codex_protocol::models::ResponseItem;
use codex_protocol::models::function_call_output_content_items_to_text; 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::protocol::AskForApproval;
use codex_protocol::request_permissions::PermissionGrantScope; use codex_protocol::request_permissions::PermissionGrantScope;
use codex_protocol::request_permissions::RequestPermissionProfile; use codex_protocol::request_permissions::RequestPermissionProfile;
@@ -274,17 +272,7 @@ async fn guardian_allows_shell_additional_permissions_requests_past_policy_valid
.features .features
.enable(Feature::ExecPermissionApprovals) .enable(Feature::ExecPermissionApprovals)
.expect("test setup should allow enabling request permissions"); .expect("test setup should allow enabling request permissions");
turn_context_raw turn_context_raw.permission_profile = codex_protocol::models::PermissionProfile::Disabled;
.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());
let mut config = (*turn_context_raw.config).clone(); let mut config = (*turn_context_raw.config).clone();
config.model_provider.base_url = Some(format!("{}/v1", server.uri())); config.model_provider.base_url = Some(format!("{}/v1", server.uri()));
let config = Arc::new(config); let config = Arc::new(config);
@@ -429,14 +417,7 @@ async fn strict_auto_review_turn_grant_forces_guardian_for_shell_policy_skip() {
.approval_policy .approval_policy
.set(AskForApproval::OnFailure) .set(AskForApproval::OnFailure)
.expect("test setup should allow updating approval policy"); .expect("test setup should allow updating approval policy");
turn_context_raw turn_context_raw.permission_profile = codex_protocol::models::PermissionProfile::Disabled;
.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());
let mut config = (*turn_context_raw.config).clone(); let mut config = (*turn_context_raw.config).clone();
config.approvals_reviewer = ApprovalsReviewer::User; config.approvals_reviewer = ApprovalsReviewer::User;
config.model_provider.base_url = Some(format!("{}/v1", server.uri())); 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, session_source: thread_config.session_source,
model: turn_context.model_info.slug.clone(), model: turn_context.model_info.slug.clone(),
model_provider: turn_context.config.model_provider_id.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_effort: turn_context.reasoning_effort,
reasoning_summary: Some(turn_context.reasoning_summary), reasoning_summary: Some(turn_context.reasoning_summary),
service_tier: turn_context.config.service_tier, service_tier: turn_context.config.service_tier,
approval_policy: turn_context.approval_policy.value(), approval_policy: turn_context.approval_policy.value(),
approvals_reviewer: turn_context.config.approvals_reviewer, 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, collaboration_mode: turn_context.collaboration_mode.mode,
personality: turn_context.personality, personality: turn_context.personality,
is_first_turn, is_first_turn,
@@ -1871,7 +1871,7 @@ async fn try_run_sampling_request(
feedback_tags!( feedback_tags!(
model = turn_context.model_info.slug.clone(), model = turn_context.model_info.slug.clone(),
approval_policy = turn_context.approval_policy.value(), 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, effort = turn_context.reasoning_effort,
auth_mode = sess.services.auth_manager.auth_mode(), auth_mode = sess.services.auth_manager.auth_mode(),
features = sess.features.enabled_features(), 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_model_provider::create_model_provider;
use codex_protocol::models::AdditionalPermissionProfile; use codex_protocol::models::AdditionalPermissionProfile;
use codex_protocol::protocol::TurnEnvironmentSelection; 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_file_system_sandbox_policy;
use codex_sandboxing::policy_transforms::effective_network_sandbox_policy; use codex_sandboxing::policy_transforms::effective_network_sandbox_policy;
use std::sync::atomic::AtomicBool; use std::sync::atomic::AtomicBool;
@@ -73,9 +74,6 @@ pub(crate) struct TurnContext {
pub(crate) personality: Option<Personality>, pub(crate) personality: Option<Personality>,
pub(crate) approval_policy: Constrained<AskForApproval>, pub(crate) approval_policy: Constrained<AskForApproval>,
pub(crate) permission_profile: PermissionProfile, 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) network: Option<NetworkProxy>,
pub(crate) windows_sandbox_level: WindowsSandboxLevel, pub(crate) windows_sandbox_level: WindowsSandboxLevel,
pub(crate) shell_environment_policy: ShellEnvironmentPolicy, pub(crate) shell_environment_policy: ShellEnvironmentPolicy,
@@ -99,6 +97,25 @@ impl TurnContext {
self.permission_profile.clone() 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> { pub(crate) fn model_context_window(&self) -> Option<i64> {
let effective_context_window_percent = self.model_info.effective_context_window_percent; let effective_context_window_percent = self.model_info.effective_context_window_percent;
self.model_info self.model_info
@@ -210,9 +227,6 @@ impl TurnContext {
personality: self.personality, personality: self.personality,
approval_policy: self.approval_policy.clone(), approval_policy: self.approval_policy.clone(),
permission_profile: self.permission_profile.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(), network: self.network.clone(),
windows_sandbox_level: self.windows_sandbox_level, windows_sandbox_level: self.windows_sandbox_level,
shell_environment_policy: self.shell_environment_policy.clone(), shell_environment_policy: self.shell_environment_policy.clone(),
@@ -246,12 +260,14 @@ impl TurnContext {
&self, &self,
additional_permissions: Option<AdditionalPermissionProfile>, additional_permissions: Option<AdditionalPermissionProfile>,
) -> FileSystemSandboxContext { ) -> 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( let file_system_sandbox_policy = effective_file_system_sandbox_policy(
&self.file_system_sandbox_policy, &base_file_system_sandbox_policy,
additional_permissions.as_ref(), additional_permissions.as_ref(),
); );
let network_sandbox_policy = effective_network_sandbox_policy( let network_sandbox_policy = effective_network_sandbox_policy(
self.network_sandbox_policy, base_network_sandbox_policy,
additional_permissions.as_ref(), additional_permissions.as_ref(),
); );
let permissions = PermissionProfile::from_runtime_permissions_with_enforcement( let permissions = PermissionProfile::from_runtime_permissions_with_enforcement(
@@ -278,11 +294,12 @@ impl TurnContext {
// this comparison and the legacy projection should go away. // this comparison and the legacy projection should go away.
let legacy_file_system_sandbox_policy = let legacy_file_system_sandbox_policy =
FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd( FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(
self.sandbox_policy.get(), &self.sandbox_policy(),
&self.cwd, &self.cwd,
); );
(self.file_system_sandbox_policy != legacy_file_system_sandbox_policy) let file_system_sandbox_policy = self.file_system_sandbox_policy();
.then(|| self.file_system_sandbox_policy.clone()) (file_system_sandbox_policy != legacy_file_system_sandbox_policy)
.then_some(file_system_sandbox_policy)
} }
pub(crate) fn compact_prompt(&self) -> &str { pub(crate) fn compact_prompt(&self) -> &str {
@@ -299,7 +316,7 @@ impl TurnContext {
current_date: self.current_date.clone(), current_date: self.current_date.clone(),
timezone: self.timezone.clone(), timezone: self.timezone.clone(),
approval_policy: self.approval_policy.value(), approval_policy: self.approval_policy.value(),
sandbox_policy: self.sandbox_policy.get().clone(), sandbox_policy: self.sandbox_policy(),
permission_profile: Some(self.permission_profile()), permission_profile: Some(self.permission_profile()),
network: self.turn_context_network_item(), network: self.turn_context_network_item(),
file_system_sandbox_policy: self.non_legacy_file_system_sandbox_policy(), 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.approvals_reviewer = session_configuration.approvals_reviewer;
per_turn_config.permissions.permission_profile = per_turn_config.permissions.permission_profile =
session_configuration.permission_profile.clone(); session_configuration.permission_profile.clone();
per_turn_config.permissions.sandbox_policy = session_configuration.sandbox_policy.clone(); let sandbox_policy = session_configuration.sandbox_policy();
per_turn_config.permissions.file_system_sandbox_policy = per_turn_config.permissions.sandbox_policy =
session_configuration.file_system_sandbox_policy.clone(); Constrained::allow_only(sandbox_policy.clone());
per_turn_config.permissions.network_sandbox_policy = let resolved_web_search_mode =
session_configuration.network_sandbox_policy; resolve_web_search_mode_for_turn(&per_turn_config.web_search_mode, &sandbox_policy);
let resolved_web_search_mode = resolve_web_search_mode_for_turn(
&per_turn_config.web_search_mode,
session_configuration.sandbox_policy.get(),
);
if let Err(err) = per_turn_config if let Err(err) = per_turn_config
.web_search_mode .web_search_mode
.set(resolved_web_search_mode) .set(resolved_web_search_mode)
@@ -489,9 +502,6 @@ impl Session {
personality: session_configuration.personality, personality: session_configuration.personality,
approval_policy: session_configuration.approval_policy.clone(), approval_policy: session_configuration.approval_policy.clone(),
permission_profile: session_configuration.permission_profile(), 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, network,
windows_sandbox_level: session_configuration.windows_sandbox_level, windows_sandbox_level: session_configuration.windows_sandbox_level,
shell_environment_policy: per_turn_config.permissions.shell_environment_policy.clone(), shell_environment_policy: per_turn_config.permissions.shell_environment_policy.clone(),
@@ -528,8 +538,9 @@ impl Session {
let turn_environments = let turn_environments =
self.resolve_turn_environments(&effective_environments)?; self.resolve_turn_environments(&effective_environments)?;
let previous_cwd = state.session_configuration.cwd.clone(); let previous_cwd = state.session_configuration.cwd.clone();
let sandbox_policy_changed = let previous_sandbox_policy = state.session_configuration.sandbox_policy();
state.session_configuration.sandbox_policy != next.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 codex_home = next.codex_home.clone();
let session_source = next.session_source.clone(); let session_source = next.session_source.clone();
state.session_configuration = next.clone(); state.session_configuration = next.clone();
@@ -635,7 +646,8 @@ impl Session {
{ {
let mcp_connection_manager = self.services.mcp_connection_manager.read().await; 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_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 let model_info = self
@@ -680,7 +692,7 @@ impl Session {
.as_ref() .as_ref()
.and_then(|started_proxy| { .and_then(|started_proxy| {
Self::managed_network_proxy_active_for_sandbox_policy( Self::managed_network_proxy_active_for_sandbox_policy(
session_configuration.sandbox_policy.get(), &session_configuration.sandbox_policy(),
) )
.then(|| started_proxy.proxy()) .then(|| started_proxy.proxy())
}), }),
@@ -272,8 +272,9 @@ async fn effective_patch_permissions(
session.granted_session_permissions().await.as_ref(), session.granted_session_permissions().await.as_ref(),
session.granted_turn_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( let file_system_sandbox_policy = effective_file_system_sandbox_policy(
&turn.file_system_sandbox_policy, &base_file_system_sandbox_policy,
granted_permissions.as_ref(), granted_permissions.as_ref(),
); );
let effective_additional_permissions = apply_granted_turn_permissions( 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(), "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 if read_deny_matcher
.as_ref() .as_ref()
.is_some_and(|matcher| matcher.is_read_denied(&path)) .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::AgentStatus;
use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::EventMsg; 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::FileSystemSandboxPolicy;
use codex_protocol::protocol::InitialHistory; use codex_protocol::protocol::InitialHistory;
use codex_protocol::protocol::InterAgentCommunication; use codex_protocol::protocol::InterAgentCommunication;
@@ -2074,21 +2077,6 @@ async fn multi_agent_v2_spawn_surfaces_task_name_validation_errors() {
#[tokio::test] #[tokio::test]
async fn spawn_agent_reapplies_runtime_sandbox_after_role_config() { 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)] #[derive(Debug, Deserialize)]
struct SpawnAgentResult { struct SpawnAgentResult {
agent_id: String, 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 (mut session, mut turn) = make_session_and_context().await;
let manager = thread_manager(); let manager = thread_manager();
session.services.agent_control = manager.agent_control(); session.services.agent_control = manager.agent_control();
let expected_sandbox = pick_allowed_sandbox_policy( let expected_sandbox = turn.config.permissions.sandbox_policy.get().clone();
&turn.config.permissions.sandbox_policy, let mut expected_file_system_sandbox_policy =
turn.config.permissions.sandbox_policy.get().clone(),
);
let expected_file_system_sandbox_policy =
FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(&expected_sandbox, &turn.cwd); 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_network_sandbox_policy = NetworkSandboxPolicy::from(&expected_sandbox);
let expected_permission_profile = PermissionProfile::from_runtime_permissions_with_enforcement( let expected_permission_profile = PermissionProfile::from_runtime_permissions_with_enforcement(
SandboxEnforcement::from_legacy_sandbox_policy(&expected_sandbox), SandboxEnforcement::from_legacy_sandbox_policy(&expected_sandbox),
@@ -2113,16 +2106,11 @@ async fn spawn_agent_reapplies_runtime_sandbox_after_role_config() {
turn.approval_policy turn.approval_policy
.set(AskForApproval::OnRequest) .set(AskForApproval::OnRequest)
.expect("approval policy should be set"); .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(); turn.permission_profile = expected_permission_profile.clone();
assert_ne!( assert_ne!(
expected_sandbox, expected_permission_profile,
turn.config.permissions.sandbox_policy.get().clone(), turn.config.permissions.permission_profile(),
"test requires a runtime sandbox override that differs from base config" "test requires a runtime profile override that differs from base config"
); );
let invocation = invocation( let invocation = invocation(
@@ -2164,11 +2152,11 @@ async fn spawn_agent_reapplies_runtime_sandbox_after_role_config() {
.expect("spawned agent thread should exist"); .expect("spawned agent thread should exist");
let child_turn = child_thread.codex.session.new_default_turn().await; let child_turn = child_thread.codex.session.new_default_turn().await;
assert_eq!( assert_eq!(
child_turn.file_system_sandbox_policy, child_turn.file_system_sandbox_policy(),
expected_file_system_sandbox_policy expected_file_system_sandbox_policy
); );
assert_eq!( assert_eq!(
child_turn.network_sandbox_policy, child_turn.network_sandbox_policy(),
expected_network_sandbox_policy expected_network_sandbox_policy
); );
assert_eq!(child_turn.permission_profile(), expected_permission_profile); 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, &file_system_sandbox_policy,
network_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.permission_profile = permission_profile.clone();
turn.approval_policy turn.approval_policy
.set(AskForApproval::OnRequest) .set(AskForApproval::OnRequest)
@@ -3718,7 +3701,7 @@ async fn build_agent_resume_config_clears_base_instructions() {
expected expected
.permissions .permissions
.sandbox_policy .sandbox_policy
.set(turn.sandbox_policy.get().clone()) .set(turn.sandbox_policy())
.expect("sandbox policy set"); .expect("sandbox policy set");
assert_eq!(config, expected); assert_eq!(config, expected);
} }
+4 -2
View File
@@ -513,14 +513,16 @@ impl ShellHandler {
); );
emitter.begin(event_ctx).await; 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 let exec_approval_requirement = session
.services .services
.exec_policy .exec_policy
.create_exec_approval_requirement_for_command(ExecApprovalRequest { .create_exec_approval_requirement_for_command(ExecApprovalRequest {
command: &exec_params.command, command: &exec_params.command,
approval_policy: turn.approval_policy.value(), approval_policy: turn.approval_policy.value(),
sandbox_policy: turn.sandbox_policy.get(), sandbox_policy: &sandbox_policy,
file_system_sandbox_policy: &turn.file_system_sandbox_policy, file_system_sandbox_policy: &file_system_sandbox_policy,
sandbox_permissions: if effective_additional_permissions.permissions_preapproved { sandbox_permissions: if effective_additional_permissions.permissions_preapproved {
codex_protocol::models::SandboxPermissions::UseDefault codex_protocol::models::SandboxPermissions::UseDefault
} else { } else {
+2 -1
View File
@@ -359,7 +359,8 @@ impl NetworkApprovalService {
.await; .await;
return NetworkDecision::deny(REASON_NOT_ALLOWED); 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; pending.set_decision(PendingApprovalDecision::Deny).await;
self.pending_host_approvals.lock().await.remove(&key); self.pending_host_approvals.lock().await.remove(&key);
self.record_outcome_for_single_active_call(NetworkApprovalOutcome::DeniedByPolicy( self.record_outcome_for_single_active_call(NetworkApprovalOutcome::DeniedByPolicy(
+6 -4
View File
@@ -122,8 +122,10 @@ impl ToolOrchestrator {
// 1) Approval // 1) Approval
let mut already_approved = false; 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(|| { 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 { match requirement {
ExecApprovalRequirement::Skip { .. } => { ExecApprovalRequirement::Skip { .. } => {
@@ -194,8 +196,8 @@ impl ToolOrchestrator {
let initial_sandbox = match tool.sandbox_mode_for_first_attempt(req) { let initial_sandbox = match tool.sandbox_mode_for_first_attempt(req) {
SandboxOverride::BypassSandboxFirstAttempt => SandboxType::None, SandboxOverride::BypassSandboxFirstAttempt => SandboxType::None,
SandboxOverride::NoOverride => self.sandbox.select_initial( SandboxOverride::NoOverride => self.sandbox.select_initial(
&turn_ctx.file_system_sandbox_policy, &file_system_sandbox_policy,
turn_ctx.network_sandbox_policy, network_sandbox_policy,
tool.sandbox_preference(), tool.sandbox_preference(),
turn_ctx.windows_sandbox_level, turn_ctx.windows_sandbox_level,
managed_network_active, managed_network_active,
@@ -268,7 +270,7 @@ impl ToolOrchestrator {
&& matches!( && matches!(
default_exec_approval_requirement( default_exec_approval_requirement(
approval_policy, approval_policy,
&turn_ctx.file_system_sandbox_policy &file_system_sandbox_policy
), ),
ExecApprovalRequirement::NeedsApproval { .. } ExecApprovalRequirement::NeedsApproval { .. }
); );
@@ -390,11 +390,12 @@ async fn execve_permission_request_hook_short_circuits_prompt() -> anyhow::Resul
..HooksConfig::default() ..HooksConfig::default()
}); });
let sandbox_policy = SandboxPolicy::new_read_only_policy();
turn_context.approval_policy = Constrained::allow_any(AskForApproval::OnRequest); turn_context.approval_policy = Constrained::allow_any(AskForApproval::OnRequest);
turn_context.sandbox_policy = Constrained::allow_any(sandbox_policy.clone()); turn_context.permission_profile = PermissionProfile::from_runtime_permissions(
turn_context.file_system_sandbox_policy = read_only_file_system_sandbox_policy(); &read_only_file_system_sandbox_policy(),
turn_context.network_sandbox_policy = NetworkSandboxPolicy::Restricted; NetworkSandboxPolicy::Restricted,
);
let sandbox_policy = SandboxPolicy::new_read_only_policy();
let workdir = AbsolutePathBuf::try_from(std::env::current_dir()?)?; let workdir = AbsolutePathBuf::try_from(std::env::current_dir()?)?;
let target = std::env::temp_dir().join("execve-hook-short-circuit.txt"); let target = std::env::temp_dir().join("execve-hook-short-circuit.txt");
@@ -788,6 +788,8 @@ impl UnifiedExecProcessManager {
self, self,
context.turn.tools_config.unified_exec_shell_mode.clone(), 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 let exec_approval_requirement = context
.session .session
.services .services
@@ -795,8 +797,8 @@ impl UnifiedExecProcessManager {
.create_exec_approval_requirement_for_command(ExecApprovalRequest { .create_exec_approval_requirement_for_command(ExecApprovalRequest {
command: &request.command, command: &request.command,
approval_policy: context.turn.approval_policy.value(), approval_policy: context.turn.approval_policy.value(),
sandbox_policy: context.turn.sandbox_policy.get(), sandbox_policy: &sandbox_policy,
file_system_sandbox_policy: &context.turn.file_system_sandbox_policy, file_system_sandbox_policy: &file_system_sandbox_policy,
sandbox_permissions: if request.additional_permissions_preapproved { sandbox_permissions: if request.additional_permissions_preapproved {
crate::sandboxing::SandboxPermissions::UseDefault crate::sandboxing::SandboxPermissions::UseDefault
} else { } else {
+7 -1
View File
@@ -14,10 +14,12 @@ use codex_config::types::McpServerConfig;
use codex_config::types::McpServerTransportConfig; use codex_config::types::McpServerTransportConfig;
use codex_core::sandboxing::SandboxPermissions; use codex_core::sandboxing::SandboxPermissions;
use codex_features::Feature; use codex_features::Feature;
use codex_protocol::models::PermissionProfile;
use codex_protocol::permissions::FileSystemAccessMode; use codex_protocol::permissions::FileSystemAccessMode;
use codex_protocol::permissions::FileSystemPath; use codex_protocol::permissions::FileSystemPath;
use codex_protocol::permissions::FileSystemSandboxEntry; use codex_protocol::permissions::FileSystemSandboxEntry;
use codex_protocol::permissions::FileSystemSandboxPolicy; use codex_protocol::permissions::FileSystemSandboxPolicy;
use codex_protocol::permissions::NetworkSandboxPolicy;
use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::SandboxPolicy; use codex_protocol::protocol::SandboxPolicy;
use codex_protocol::protocol::TurnEnvironmentSelection; use codex_protocol::protocol::TurnEnvironmentSelection;
@@ -560,7 +562,11 @@ async fn shell_enforces_glob_deny_read_policy() -> Result<()> {
}, },
access: FileSystemAccessMode::None, 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?; 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)] #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn unified_exec_enforces_glob_deny_read_policy() -> Result<()> { async fn unified_exec_enforces_glob_deny_read_policy() -> Result<()> {
use codex_config::Constrained; use codex_config::Constrained;
use codex_protocol::models::PermissionProfile;
use codex_protocol::permissions::FileSystemAccessMode; use codex_protocol::permissions::FileSystemAccessMode;
use codex_protocol::permissions::FileSystemPath; use codex_protocol::permissions::FileSystemPath;
use codex_protocol::permissions::FileSystemSandboxEntry; use codex_protocol::permissions::FileSystemSandboxEntry;
use codex_protocol::permissions::FileSystemSandboxPolicy; use codex_protocol::permissions::FileSystemSandboxPolicy;
use codex_protocol::permissions::NetworkSandboxPolicy;
skip_if_no_network!(Ok(())); skip_if_no_network!(Ok(()));
skip_if_sandbox!(Ok(())); skip_if_sandbox!(Ok(()));
@@ -2553,7 +2555,11 @@ async fn unified_exec_enforces_glob_deny_read_policy() -> Result<()> {
}, },
access: FileSystemAccessMode::None, 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 { let TestCodex {
codex, codex,
+7 -1
View File
@@ -1,5 +1,6 @@
use anyhow::Context; use anyhow::Context;
use codex_features::Feature; use codex_features::Feature;
use codex_protocol::models::PermissionProfile;
use codex_protocol::permissions::NetworkSandboxPolicy; use codex_protocol::permissions::NetworkSandboxPolicy;
use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::EventMsg; 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<()> { async fn user_shell_command_does_not_set_network_sandbox_env_var() -> anyhow::Result<()> {
let server = responses::start_mock_server().await; let server = responses::start_mock_server().await;
let mut builder = core_test_support::test_codex::test_codex().with_config(|config| { 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?; let test = builder.build(&server).await?;
+12 -4
View File
@@ -482,10 +482,7 @@ impl PermissionProfile {
FileSystemSandboxKind::ExternalSandbox => Self::External { FileSystemSandboxKind::ExternalSandbox => Self::External {
network: network_sandbox_policy, network: network_sandbox_policy,
}, },
FileSystemSandboxKind::Unrestricted FileSystemSandboxKind::Unrestricted if enforcement == SandboxEnforcement::Disabled => {
if enforcement == SandboxEnforcement::Disabled
&& network_sandbox_policy.is_enabled() =>
{
Self::Disabled Self::Disabled
} }
FileSystemSandboxKind::Restricted | FileSystemSandboxKind::Unrestricted => { FileSystemSandboxKind::Restricted | FileSystemSandboxKind::Unrestricted => {
@@ -1867,6 +1864,17 @@ mod tests {
Ok(()) 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] #[test]
fn permission_profile_from_runtime_permissions_preserves_external_sandbox() { fn permission_profile_from_runtime_permissions_preserves_external_sandbox() {
let permission_profile = PermissionProfile::from_runtime_permissions( let permission_profile = PermissionProfile::from_runtime_permissions(
+48 -3
View File
@@ -631,6 +631,12 @@ impl FileSystemSandboxPolicy {
.semantic_signature(cwd) .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. /// Returns the explicit readable roots resolved against the provided cwd.
pub fn get_readable_roots_with_cwd(&self, cwd: &Path) -> Vec<AbsolutePathBuf> { pub fn get_readable_roots_with_cwd(&self, cwd: &Path) -> Vec<AbsolutePathBuf> {
if self.has_full_disk_read_access() { 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_read_access: self.has_full_disk_read_access(),
has_full_disk_write_access: self.has_full_disk_write_access(), has_full_disk_write_access: self.has_full_disk_write_access(),
include_platform_defaults: self.include_platform_defaults(), include_platform_defaults: self.include_platform_defaults(),
readable_roots: self.get_readable_roots_with_cwd(cwd), readable_roots: sorted_absolute_paths(self.get_readable_roots_with_cwd(cwd)),
writable_roots: self.get_writable_roots_with_cwd(cwd), writable_roots: sorted_writable_roots(self.get_writable_roots_with_cwd(cwd)),
unreadable_roots: self.get_unreadable_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), unreadable_globs: self.get_unreadable_globs_with_cwd(cwd),
} }
} }
@@ -1257,6 +1263,20 @@ fn dedup_absolute_paths(
deduped 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 { fn normalize_effective_absolute_path(path: AbsolutePathBuf) -> AbsolutePathBuf {
let raw_path = path.to_path_buf(); let raw_path = path.to_path_buf();
for ancestor in raw_path.ancestors() { 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] #[test]
fn root_write_with_read_only_child_is_not_full_disk_write() { fn root_write_with_read_only_child_is_not_full_disk_write() {
let cwd = TempDir::new().expect("tempdir"); 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"); tracing::warn!(%err, "failed to sync permissions from SessionConfigured");
self.config.permissions.sandbox_policy = self.config.permissions.sandbox_policy =
Constrained::allow_only(event.sandbox_policy.clone()); Constrained::allow_only(event.sandbox_policy.clone());
match event.permission_profile.clone() { let permission_profile = event.permission_profile.clone().unwrap_or_else(|| {
Some(permission_profile) => { codex_protocol::models::PermissionProfile::from_legacy_sandbox_policy(
let (file_system_sandbox_policy, network_sandbox_policy) = &event.sandbox_policy,
permission_profile.to_runtime_permissions(); )
self.config.permissions.permission_profile = });
Constrained::allow_only(permission_profile); self.config.permissions.permission_profile =
self.config.permissions.file_system_sandbox_policy = file_system_sandbox_policy; Constrained::allow_only(permission_profile);
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);
}
}
} }
self.config.approvals_reviewer = event.approvals_reviewer; self.config.approvals_reviewer = event.approvals_reviewer;
self.status_line_project_root_name_cache = None; self.status_line_project_root_name_cache = None;
@@ -380,12 +380,12 @@ async fn session_configured_external_sandbox_keeps_external_runtime_policy() {
assert_eq!( assert_eq!(
chat.config_ref() chat.config_ref()
.permissions .permissions
.file_system_sandbox_policy .file_system_sandbox_policy()
.kind, .kind,
FileSystemSandboxKind::ExternalSandbox, FileSystemSandboxKind::ExternalSandbox,
); );
assert_eq!( assert_eq!(
chat.config_ref().permissions.network_sandbox_policy, chat.config_ref().permissions.network_sandbox_policy(),
NetworkSandboxPolicy::Restricted, NetworkSandboxPolicy::Restricted,
); );
} }