From ed2f985a26eee9a59cde0fdefd20f69b45bc25f5 Mon Sep 17 00:00:00 2001 From: sayan-oai Date: Wed, 5 Aug 2026 06:33:55 +0000 Subject: [PATCH] Use turn environment permissions for context and discovery (#37040) ## What changed - Build filesystem and permission context from the primary turn environment's permission profile, working directory, and workspace roots, falling back to the thread context when no primary environment is available. - Build capability-discovery sandbox contexts from each environment's permission profile and treat discovery as restricted when any selected environment has restricted filesystem access. ## Testing - Add regression tests covering environment-specific capability discovery and permission context updates. GitOrigin-RevId: 6bbbd9b88aafa5e6583971ce4e427fc7557f2d93 --- .../src/context/world_state/environment.rs | 15 +-- codex-rs/core/src/session/mcp.rs | 28 ++++- codex-rs/core/src/session/tests.rs | 110 ++++++++++++++++++ codex-rs/core/src/session/world_state.rs | 22 +++- 4 files changed, 158 insertions(+), 17 deletions(-) diff --git a/codex-rs/core/src/context/world_state/environment.rs b/codex-rs/core/src/context/world_state/environment.rs index c039544ca0..6e5aa9908a 100644 --- a/codex-rs/core/src/context/world_state/environment.rs +++ b/codex-rs/core/src/context/world_state/environment.rs @@ -6,7 +6,6 @@ use crate::context::environment_context::NetworkContext; use crate::context::environment_context::push_xml_escaped_text; use crate::environment_selection::TurnEnvironmentSnapshot; use crate::session::turn_context::TurnContext; -use crate::session::turn_context::TurnEnvironment; use codex_utils_path_uri::PathUri; use serde::Deserialize; use serde::Serialize; @@ -29,19 +28,17 @@ impl EnvironmentsState { environments: &TurnEnvironmentSnapshot, current_date: Option, ) -> Self { - let workspace_roots = environments - .primary() - .map(TurnEnvironment::workspace_roots) - .unwrap_or_default(); Self { environments: environment_states(environments), current_date, timezone: turn_context.timezone.clone(), network: network_from_turn_context(turn_context), - filesystem: Some(FileSystemContext::from_permission_profile( - turn_context.config.permissions.permission_profile(), - workspace_roots, - )), + filesystem: environments.primary().map(|environment| { + FileSystemContext::from_permission_profile( + environment.permission_profile(), + environment.workspace_roots(), + ) + }), subagents: None, } } diff --git a/codex-rs/core/src/session/mcp.rs b/codex-rs/core/src/session/mcp.rs index 5a37e874d2..81edaa4f43 100644 --- a/codex-rs/core/src/session/mcp.rs +++ b/codex-rs/core/src/session/mcp.rs @@ -317,10 +317,28 @@ impl Session { environments: &TurnEnvironmentSnapshot, windows_sandbox_level: WindowsSandboxLevel, ) -> Option> { - let restricted_file_system = !config - .permissions - .file_system_sandbox_policy() - .has_full_disk_read_access(); + // Capability roots can currently be selected independently of turn environments, so a + // root may be ready when there is no primary `TurnEnvironment`. Keep using the thread + // policy in that case so restricted discovery fails closed below. Once every selected + // root belongs to a thread/environment attachment whose `EnvironmentConfig` is installed + // before the root becomes ready, discovery can use the root owner's policy and this + // fallback can be removed. + let restricted_file_system = environments.primary().map_or_else( + || { + !config + .permissions + .file_system_sandbox_policy() + .has_full_disk_read_access() + }, + |_| { + environments.turn_environments().any(|environment| { + !environment + .permission_profile() + .file_system_sandbox_policy() + .has_full_disk_read_access() + }) + }, + ); if !restricted_file_system && !config .features @@ -333,7 +351,7 @@ impl Session { .turn_environments() .map(|environment| { let mut sandbox = FileSystemSandboxContext::from_permission_profile_with_cwd( - config.permissions.permission_profile().clone(), + environment.permission_profile().clone(), environment.cwd().clone(), ); sandbox.workspace_roots = environment.workspace_roots().to_vec(); diff --git a/codex-rs/core/src/session/tests.rs b/codex-rs/core/src/session/tests.rs index 73079158b3..6d542f79b8 100644 --- a/codex-rs/core/src/session/tests.rs +++ b/codex-rs/core/src/session/tests.rs @@ -8591,6 +8591,52 @@ async fn conflicting_ready_environment_root_ids_keep_first_location() { }); } +#[tokio::test] +async fn capability_discovery_uses_environment_permission_profile() { + let (session, mut turn_context) = make_session_and_context().await; + Arc::make_mut(&mut turn_context.config) + .permissions + .set_permission_profile(PermissionProfile::Disabled) + .expect("unrestricted permission profile should be allowed"); + let mut environment = turn_context + .environments + .primary() + .expect("primary environment") + .clone(); + let mut file_system_policy = PermissionProfile::read_only().file_system_sandbox_policy(); + file_system_policy.entries.push(FileSystemSandboxEntry { + path: FileSystemPath::GlobPattern { + pattern: "**/*.env".to_string(), + }, + access: FileSystemAccessMode::Deny, + missing_path_behavior: None, + }); + environment.config.permission_profile = + PermissionProfileSnapshot::legacy(PermissionProfile::from_runtime_permissions( + &file_system_policy, + NetworkSandboxPolicy::Restricted, + )); + let expected_sandbox = turn_context + .file_system_sandbox_context(/*additional_permissions*/ None, &environment); + let environment_id = environment.environment_id.clone(); + turn_context.environments.environments[0] = TurnEnvironmentState::Ready(environment); + + let discovery = session + .executor_capability_discovery_for_step( + &turn_context.config, + /*ready_selected_capability_roots*/ &[], + &turn_context.environments, + turn_context.windows_sandbox_level, + ) + .await + .expect("restricted environment should trigger capability discovery"); + + assert_eq!( + discovery.sandbox_contexts().get(&environment_id), + Some(&expected_sandbox) + ); +} + #[tokio::test] async fn step_context_keeps_its_mcp_runtime_for_tools() -> anyhow::Result<()> { let (session, turn_context) = make_session_and_context().await; @@ -8786,6 +8832,70 @@ async fn record_context_updates_emits_environment_item_for_cwd_changes() { assert!(!environment_update.contains("")); } +#[tokio::test] +async fn record_context_updates_use_environment_permission_profile_and_workspace_roots() { + let (session, mut previous_context) = make_session_and_context().await; + Arc::make_mut(&mut previous_context.config) + .permissions + .set_permission_profile(PermissionProfile::Disabled) + .expect("unrestricted permission profile should be allowed"); + let mut previous_environment = previous_context + .environments + .primary() + .expect("primary environment") + .clone(); + previous_environment.config.permission_profile = + PermissionProfileSnapshot::legacy(PermissionProfile::Disabled); + previous_context.environments.environments[0] = + TurnEnvironmentState::Ready(previous_environment); + let previous_context = Arc::new(previous_context); + let mut current_context = previous_context + .with_model( + previous_context.model_info.slug.clone(), + &session.services.models_manager, + ) + .await; + let environment = current_context + .environments + .primary() + .expect("primary environment") + .clone(); + let cwd = environment.cwd().clone(); + let workspace_root = current_context.config.cwd.join("selected-workspace"); + let mut environment_config = environment.config; + environment_config.permission_profile = + PermissionProfileSnapshot::legacy(PermissionProfile::workspace_write()); + current_context.environments.environments[0] = + TurnEnvironmentState::Ready(TurnEnvironment::new( + environment.environment_id, + environment.environment, + cwd, + vec![PathUri::from_abs_path(&workspace_root)], + environment.shell, + environment_config, + )); + + let update_items = + record_context_update_items(&session, previous_context, current_context).await; + let permissions_update = developer_input_texts(&update_items) + .into_iter() + .find(|text| text.contains("")) + .expect("permissions update should be emitted"); + assert!( + permissions_update.contains(workspace_root.to_string_lossy().as_ref()), + "selected workspace root should be visible in permissions: {permissions_update}" + ); + let environment_update = user_input_texts(&update_items) + .into_iter() + .find(|text| text.contains("")) + .expect("environment update should be emitted"); + assert!( + environment_update.contains("") + && environment_update.contains(workspace_root.to_string_lossy().as_ref()), + "selected environment permissions should be visible: {environment_update}" + ); +} + #[tokio::test] async fn record_context_updates_emits_environment_item_for_time_changes() { let (session, previous_context) = make_session_and_context().await; diff --git a/codex-rs/core/src/session/world_state.rs b/codex-rs/core/src/session/world_state.rs index f9b660c55e..d5cdac746d 100644 --- a/codex-rs/core/src/session/world_state.rs +++ b/codex-rs/core/src/session/world_state.rs @@ -114,7 +114,24 @@ impl Session { )); world_state.add_section(AgentsMdState::new(step_context.loaded_agents_md.as_deref())); if turn_context.config.include_permissions_instructions { - let permission_profile = turn_context.permission_profile(); + let environment = step_context.environments.primary(); + let permission_profile = environment + .map(|environment| { + let workspace_roots = environment + .workspace_roots() + .iter() + .filter_map(|workspace_root| workspace_root.to_abs_path().ok()) + .collect::>(); + environment + .permission_profile() + .clone() + .materialize_project_roots_with_workspace_roots(&workspace_roots) + }) + .unwrap_or_else(|| turn_context.permission_profile()); + #[allow(deprecated)] + let cwd = environment + .and_then(|environment| environment.cwd().to_abs_path().ok()) + .unwrap_or_else(|| turn_context.cwd.clone()); let model_messages = turn_context.model_info.model_messages.as_ref(); let exec_policy = self.services.exec_policy.current(); world_state.add_section(PermissionsState::new( @@ -126,8 +143,7 @@ impl Session { model_messages.and_then(|messages| messages.permissions.as_ref()), ), exec_policy.as_ref(), - #[allow(deprecated)] - &turn_context.cwd, + &cwd, turn_context .config .features