diff --git a/codex-rs/app-server/tests/suite/v2/guardian_sync_session_tests.rs b/codex-rs/app-server/tests/suite/v2/guardian_sync_session_tests.rs index a226cd900b..4012493637 100644 --- a/codex-rs/app-server/tests/suite/v2/guardian_sync_session_tests.rs +++ b/codex-rs/app-server/tests/suite/v2/guardian_sync_session_tests.rs @@ -124,6 +124,12 @@ async fn managed_reviewers_reuse_fork_and_resume_after_parent_shutdown( // The seed review finished; pause the parent before it requests concurrent approvals. timeout(TIMEOUT, server.wait_for_request_count(/*count*/ 3)).await?; let seed: Value = serde_json::from_slice(&server.requests().await[1])?; + assert!( + !seed["tools"] + .to_string() + .contains(&format!("mcp__{TEST_SERVER_NAME}")), + "reviewers must not inherit the parent's configured MCP tools" + ); let reviewer_id = seed["client_metadata"]["thread_id"] .as_str() .expect("reviewer ID") diff --git a/codex-rs/core/src/codex_delegate.rs b/codex-rs/core/src/codex_delegate.rs index 83908c5173..d5795df0ff 100644 --- a/codex-rs/core/src/codex_delegate.rs +++ b/codex-rs/core/src/codex_delegate.rs @@ -56,6 +56,7 @@ pub(crate) async fn run_codex_thread_interactive( parent_environments: TurnEnvironmentSnapshot, cancel_token: CancellationToken, subagent_source: SubAgentSource, + isolation: codex_extension_api::SessionIsolation, initial_history: Option, git_enrichment_policy: GitEnrichmentPolicy, windows_sandbox_proxy_settings_mode: codex_sandboxing::WindowsSandboxProxySettingsMode, @@ -79,7 +80,7 @@ pub(crate) async fn run_codex_thread_interactive( }; let session_source = SessionSource::SubAgent(subagent_source.clone()); let is_guardian_reviewer = crate::guardian::is_basic_session_source(&session_source); - let extensions = if is_guardian_reviewer { + let extensions = if isolation == codex_extension_api::SessionIsolation::Isolated { codex_extension_api::empty_extension_registry() } else { Arc::clone(&parent_session.services.extensions) @@ -87,6 +88,7 @@ pub(crate) async fn run_codex_thread_interactive( // Inline delegates never register with ThreadManager or receive on_thread_ready. // Bind their standalone spawn path before inherited extensions run. let mut thread_extension_init = codex_extension_api::ExtensionDataInit::default(); + thread_extension_init.insert(isolation); thread_extension_init.insert(crate::guardian::GuardianReviewSessionHost::default()); let (session, io) = Session::spawn(SessionSpawnArgs { config, @@ -213,6 +215,7 @@ pub(crate) async fn run_codex_thread_one_shot( parent_environments, child_cancel.clone(), subagent_source, + codex_extension_api::SessionIsolation::Inherit, initial_history, GitEnrichmentPolicy::Fresh, codex_sandboxing::WindowsSandboxProxySettingsMode::Reconcile, diff --git a/codex-rs/core/src/codex_delegate_tests.rs b/codex-rs/core/src/codex_delegate_tests.rs index f79ff413fb..5c31a2531b 100644 --- a/codex-rs/core/src/codex_delegate_tests.rs +++ b/codex-rs/core/src/codex_delegate_tests.rs @@ -202,6 +202,7 @@ async fn run_codex_thread_interactive_respects_pre_cancelled_spawn() { parent_environments, cancel_token, SubAgentSource::Review, + codex_extension_api::SessionIsolation::Inherit, /*initial_history*/ None, crate::session::GitEnrichmentPolicy::Fresh, codex_sandboxing::WindowsSandboxProxySettingsMode::Reconcile, @@ -217,7 +218,7 @@ async fn run_codex_thread_interactive_respects_pre_cancelled_spawn() { } #[tokio::test] -async fn guardian_delegates_do_not_inherit_parent_extensions() { +async fn delegate_isolation_does_not_depend_on_attribution() { let (mut parent_session, parent_ctx, _rx_events) = crate::session::tests::make_session_and_context_with_rx().await; let thread_starts = Arc::new(AtomicUsize::new(0)); @@ -229,13 +230,25 @@ async fn guardian_delegates_do_not_inherit_parent_extensions() { .services .extensions = Arc::new(extensions.build()); - for (subagent_source, expected_thread_starts, expected_thread_source) in [ + for (subagent_source, isolation, expected_thread_starts, expected_thread_source) in [ ( SubAgentSource::Other(crate::guardian::GUARDIAN_REVIEWER_NAME.to_string()), + codex_extension_api::SessionIsolation::Isolated, 0, ThreadSource::GuardianReview, ), - (SubAgentSource::Review, 1, ThreadSource::Subagent), + ( + SubAgentSource::Review, + codex_extension_api::SessionIsolation::Isolated, + 0, + ThreadSource::Subagent, + ), + ( + SubAgentSource::Review, + codex_extension_api::SessionIsolation::Inherit, + 1, + ThreadSource::Subagent, + ), ] { let mut config = parent_ctx.config.as_ref().clone(); config.permissions.approval_policy = Constrained::allow_only(AskForApproval::Never); @@ -248,6 +261,7 @@ async fn guardian_delegates_do_not_inherit_parent_extensions() { parent_ctx.environments.clone(), CancellationToken::new(), subagent_source, + isolation, /*initial_history*/ None, crate::session::GitEnrichmentPolicy::Fresh, codex_sandboxing::WindowsSandboxProxySettingsMode::Reconcile, @@ -291,6 +305,7 @@ async fn run_codex_thread_interactive_rejects_approval_policy_that_can_prompt() parent_environments, CancellationToken::new(), SubAgentSource::Review, + codex_extension_api::SessionIsolation::Inherit, /*initial_history*/ None, crate::session::GitEnrichmentPolicy::Fresh, codex_sandboxing::WindowsSandboxProxySettingsMode::Reconcile, diff --git a/codex-rs/core/src/guardian/review_session_factory.rs b/codex-rs/core/src/guardian/review_session_factory.rs index b58db97bc9..aa4ff2dd23 100644 --- a/codex-rs/core/src/guardian/review_session_factory.rs +++ b/codex-rs/core/src/guardian/review_session_factory.rs @@ -128,6 +128,7 @@ impl ReviewerSessionFactory for PreparedSession { self.context.environments().clone(), cancellation.clone(), SubAgentSource::Other(GUARDIAN_REVIEWER_NAME.to_owned()), + codex_extension_api::SessionIsolation::Isolated, initial_history, GitEnrichmentPolicy::Skip, codex_sandboxing::WindowsSandboxProxySettingsMode::Preserve, diff --git a/codex-rs/core/src/guardian/review_session_threads.rs b/codex-rs/core/src/guardian/review_session_threads.rs index 660b5bcc8b..e2cc14ca28 100644 --- a/codex-rs/core/src/guardian/review_session_threads.rs +++ b/codex-rs/core/src/guardian/review_session_threads.rs @@ -67,7 +67,10 @@ impl ManagedReviewerThreads { config.permissions.approval_policy = Constrained::allow_only(AskForApproval::Never); config.model_provider.supports_websockets &= parent.services.model_client.responses_websocket_enabled(); + let mut thread_extension_init = codex_extension_api::ExtensionDataInit::default(); + thread_extension_init.insert(codex_extension_api::SessionIsolation::Isolated); let options = StartThreadOptions { + thread_extension_init, session_source: Some(SessionSource::Internal(InternalSessionSource::Guardian)), thread_source: Some(ThreadSource::GuardianReview), environments: Some(context.environments().to_selections()), diff --git a/codex-rs/core/src/session/mcp.rs b/codex-rs/core/src/session/mcp.rs index d4505eace2..adc4740b98 100644 --- a/codex-rs/core/src/session/mcp.rs +++ b/codex-rs/core/src/session/mcp.rs @@ -135,12 +135,7 @@ impl Session { ) .await; let mcp_config = self - .project_selected_environment_mcp_servers( - &session_source, - config, - &environments, - mcp_projection, - ) + .project_selected_environment_mcp_servers(config, &environments, mcp_projection) .await .config; let local_process_cwd = environments @@ -312,7 +307,6 @@ impl Session { .await; let mcp_projection = self .project_selected_environment_mcp_servers( - &desired.session_source, &desired.config, &desired.environments, mcp_projection, diff --git a/codex-rs/core/src/session/mcp_runtime.rs b/codex-rs/core/src/session/mcp_runtime.rs index c3c329e89c..e14034de7b 100644 --- a/codex-rs/core/src/session/mcp_runtime.rs +++ b/codex-rs/core/src/session/mcp_runtime.rs @@ -143,13 +143,12 @@ impl Session { /// Adds effective executor-owned configuration from this exact thread snapshot. pub(super) fn project_selected_environment_mcp_servers<'a>( &'a self, - session_source: &'a SessionSource, config: &'a Config, environments: &'a TurnEnvironmentSnapshot, mut projection: McpRuntimeProjection, ) -> BoxFuture<'a, McpRuntimeProjection> { Box::pin(async move { - if crate::guardian::is_basic_session_source(session_source) { + if self.isolation == codex_extension_api::SessionIsolation::Isolated { return projection; } @@ -306,7 +305,6 @@ impl Session { ) { let mcp_projection = self .project_selected_environment_mcp_servers( - &desired.session_source, &desired.config, &desired.environments, mcp_projection, diff --git a/codex-rs/core/src/session/mod.rs b/codex-rs/core/src/session/mod.rs index d997aa1527..98bfa792b2 100644 --- a/codex-rs/core/src/session/mod.rs +++ b/codex-rs/core/src/session/mod.rs @@ -580,7 +580,11 @@ impl Session { config .startup_warnings .extend(user_instruction_provider_warnings); - let exec_policy = if crate::guardian::is_basic_session_source(&session_source) { + let isolation = thread_extension_init + .get::() + .map(|policy| *policy) + .unwrap_or_default(); + let exec_policy = if isolation == codex_extension_api::SessionIsolation::Isolated { let managed_policy = config .config_layer_stack .requirements() diff --git a/codex-rs/core/src/session/session.rs b/codex-rs/core/src/session/session.rs index fa3835d601..ffe66620c4 100644 --- a/codex-rs/core/src/session/session.rs +++ b/codex-rs/core/src/session/session.rs @@ -58,6 +58,7 @@ pub(crate) struct Session { /// session. pub(super) features: ManagedFeatures, pub(crate) guardian_context_mode: GuardianContextMode, + pub(super) isolation: codex_extension_api::SessionIsolation, pub(crate) windows_sandbox_proxy_settings_mode: codex_sandboxing::WindowsSandboxProxySettingsMode, pub(super) multi_agent_version: OnceLock, @@ -860,6 +861,10 @@ impl Session { // Publish the already resolved model before extensions make startup decisions. // Turn construction refreshes this attachment when the selected model changes. thread_extension_init.insert(model_info); + let isolation = thread_extension_init + .get::() + .map(|policy| *policy) + .unwrap_or_default(); let mcp_thread_init = thread_extension_init.clone(); let thread_extension_data = codex_extension_api::ExtensionData::new_with_init( thread_id.to_string(), @@ -1582,6 +1587,7 @@ impl Session { managed_network_proxy_refresh_lock: Semaphore::new(/*permits*/ 1), features: config.features.clone(), guardian_context_mode, + isolation, windows_sandbox_proxy_settings_mode, multi_agent_version, mcp_refresh: McpRefresh::new(), diff --git a/codex-rs/core/src/session/tests.rs b/codex-rs/core/src/session/tests.rs index be539ca4f4..2089299b59 100644 --- a/codex-rs/core/src/session/tests.rs +++ b/codex-rs/core/src/session/tests.rs @@ -6701,6 +6701,7 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) { managed_network_proxy_refresh_lock: Semaphore::new(/*permits*/ 1), features: config.features.clone(), guardian_context_mode: GuardianContextMode::from_features(&config.features), + isolation: codex_extension_api::SessionIsolation::Inherit, windows_sandbox_proxy_settings_mode: codex_sandboxing::WindowsSandboxProxySettingsMode::Reconcile, multi_agent_version: OnceLock::from(config.multi_agent_version_from_features()), @@ -8874,6 +8875,7 @@ where managed_network_proxy_refresh_lock: Semaphore::new(/*permits*/ 1), features: config.features.clone(), guardian_context_mode: GuardianContextMode::from_features(&config.features), + isolation: codex_extension_api::SessionIsolation::Inherit, windows_sandbox_proxy_settings_mode: codex_sandboxing::WindowsSandboxProxySettingsMode::Reconcile, multi_agent_version: OnceLock::from(config.multi_agent_version_from_features()), diff --git a/codex-rs/core/src/session/tests/guardian_tests.rs b/codex-rs/core/src/session/tests/guardian_tests.rs index 81f836e001..d51009d986 100644 --- a/codex-rs/core/src/session/tests/guardian_tests.rs +++ b/codex-rs/core/src/session/tests/guardian_tests.rs @@ -1257,6 +1257,8 @@ async fn guardian_subagent_does_not_inherit_parent_exec_policy_rules() { /*state_db*/ None, )); + let mut thread_extension_init = codex_extension_api::ExtensionDataInit::default(); + thread_extension_init.insert(codex_extension_api::SessionIsolation::Isolated); let (session, io) = Session::spawn(SessionSpawnArgs { config, allow_provider_model_fallback: false, @@ -1291,7 +1293,7 @@ async fn guardian_subagent_does_not_inherit_parent_exec_policy_rules() { user_shell_override: None, parent_trace: None, environment_selections: Vec::new(), - thread_extension_init: codex_extension_api::ExtensionDataInit::default(), + thread_extension_init, client_mcp_extensions: ClientMcpExtensions::default(), reserved_thread_id: None, analytics_events_client: None, diff --git a/codex-rs/core/src/thread_manager.rs b/codex-rs/core/src/thread_manager.rs index b202e15fc6..7fc5f3ac72 100644 --- a/codex-rs/core/src/thread_manager.rs +++ b/codex-rs/core/src/thread_manager.rs @@ -1932,13 +1932,26 @@ impl ThreadManagerState { environments, inherited_environments: captured_environments, user_instructions: supplied_user_instructions, - thread_extension_init, + mut thread_extension_init, client_mcp_extensions, reserved_thread_id, disabled_plugin_ids, } = options; let inherited_environments = captured_environments.or(inherited_environments); let session_source = session_source.unwrap_or_else(|| self.session_source.clone()); + // Older callers and saved reviewers identify isolation through their source. + // New internal callers supply an explicit runtime policy before startup. + let isolation = thread_extension_init + .get::() + .map(|policy| *policy) + .unwrap_or_else(|| { + if crate::guardian::is_basic_session_source(&session_source) { + codex_extension_api::SessionIsolation::Isolated + } else { + codex_extension_api::SessionIsolation::Inherit + } + }); + thread_extension_init.insert(isolation); let environments = environments.unwrap_or_else(|| { default_thread_environment_selections( self.environment_manager.as_ref(), @@ -1989,7 +2002,7 @@ impl ThreadManagerState { extensions, mcp_manager, multi_agent_version, - ) = if crate::guardian::is_basic_session_source(&session_source) { + ) = if isolation == codex_extension_api::SessionIsolation::Isolated { ( LoadedUserInstructions::default(), None, diff --git a/codex-rs/ext/extension-api/src/lib.rs b/codex-rs/ext/extension-api/src/lib.rs index f92d36bc00..0fab8a01a6 100644 --- a/codex-rs/ext/extension-api/src/lib.rs +++ b/codex-rs/ext/extension-api/src/lib.rs @@ -1,10 +1,13 @@ mod capabilities; mod contributors; mod registry; +mod session_isolation; mod state; mod turn_admission; mod user_instructions; +pub use session_isolation::SessionIsolation; + pub use capabilities::AgentSpawnFuture; pub use capabilities::AgentSpawner; pub use capabilities::ConversationHistorySnapshot; diff --git a/codex-rs/ext/extension-api/src/session_isolation.rs b/codex-rs/ext/extension-api/src/session_isolation.rs new file mode 100644 index 0000000000..8318517a54 --- /dev/null +++ b/codex-rs/ext/extension-api/src/session_isolation.rs @@ -0,0 +1,15 @@ +//! Host-supplied isolation for internal runtimes, independent of their attribution. +//! Isolation only removes inherited capabilities; it never grants review authority. + +/// Runtime policy supplied through `ExtensionDataInit` before session startup. +/// The host captures this value once so later extension-state changes cannot alter it. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub enum SessionIsolation { + /// Use the host's ordinary instruction providers, extensions and execution rules. + #[default] + Inherit, + /// Start without inherited instruction providers or extensions, retain only managed + /// execution rules, and omit executor-discovered MCP servers. Explicitly supplied + /// instructions and permissions remain the responsibility of the internal caller. + Isolated, +}