diff --git a/codex-rs/core/src/agent/mod.rs b/codex-rs/core/src/agent/mod.rs index e9b035f4f0..228396ae17 100644 --- a/codex-rs/core/src/agent/mod.rs +++ b/codex-rs/core/src/agent/mod.rs @@ -14,6 +14,5 @@ pub(crate) use registry::exceeds_thread_spawn_depth_limit; pub(crate) use registry::next_thread_spawn_depth; pub(crate) use status::agent_status_from_event; pub(crate) use subagent_config::SubAgentConfigBuilder; -pub(crate) use subagent_config::SubAgentExtensionInheritance; pub(crate) use subagent_config::SubAgentPromptInheritance; pub(crate) use subagent_config::SubAgentPromptSelection; diff --git a/codex-rs/core/src/agent/subagent_config.rs b/codex-rs/core/src/agent/subagent_config.rs index f15e885154..792c874a64 100644 --- a/codex-rs/core/src/agent/subagent_config.rs +++ b/codex-rs/core/src/agent/subagent_config.rs @@ -1,10 +1,7 @@ use anyhow::Context; -use codex_features::Feature; use crate::codex::TurnContext; use crate::config::Config; -use crate::config::Constrained; -use crate::initial_context::InitialContextInclusions; #[derive(Debug, Clone, Copy)] #[allow(dead_code)] @@ -16,76 +13,16 @@ pub(crate) enum SubAgentPromptInheritance { #[derive(Debug, Clone, Copy)] pub(crate) struct SubAgentPromptSelection { - pub(crate) model_update: bool, pub(crate) base_instructions: bool, pub(crate) user_instructions: bool, pub(crate) project_docs: bool, pub(crate) developer_instructions: bool, - pub(crate) separate_developer_instructions: bool, pub(crate) compact_prompt: bool, pub(crate) permissions: bool, - pub(crate) memory: bool, - pub(crate) collaboration: bool, - pub(crate) realtime: bool, - pub(crate) personality: bool, pub(crate) apps: bool, - pub(crate) skills: bool, - pub(crate) plugins: bool, - pub(crate) commit: bool, pub(crate) environment_context: bool, } -impl SubAgentPromptSelection { - pub(crate) fn initial_context_inclusions(self) -> InitialContextInclusions { - let mut inclusions = InitialContextInclusions::none(); - inclusions.model_update = self.model_update; - inclusions.permissions = self.permissions; - inclusions.developer_instructions = self.developer_instructions; - inclusions.separate_developer_instructions = self.separate_developer_instructions; - inclusions.memory = self.memory; - inclusions.collaboration = self.collaboration; - inclusions.realtime = self.realtime; - inclusions.personality = self.personality; - inclusions.apps = self.apps; - inclusions.skills = self.skills; - inclusions.plugins = self.plugins; - inclusions.commit = self.commit; - inclusions.user_instructions = self.user_instructions || self.project_docs; - inclusions.environment_context = self.environment_context; - inclusions - } -} - -#[derive(Debug, Clone, Copy)] -#[allow(dead_code)] -pub(crate) enum SubAgentExtensionInheritance { - InheritParent, - None, - Select(SubAgentExtensionSelection), -} - -#[derive(Debug, Clone, Copy)] -pub(crate) struct SubAgentExtensionSelection { - pub(crate) mcp_servers: bool, - pub(crate) apps: bool, - pub(crate) plugins: bool, - pub(crate) tool_search: bool, - pub(crate) web_search: bool, - pub(crate) js_repl: bool, - pub(crate) code_mode: bool, - pub(crate) memory: bool, - pub(crate) apply_patch: bool, - pub(crate) subagents: bool, - pub(crate) image_generation: bool, - pub(crate) request_permissions: bool, - pub(crate) skill_dependencies: bool, - pub(crate) shell_snapshot: bool, - pub(crate) shell_zsh_fork: bool, - pub(crate) unified_exec: bool, - pub(crate) request_rule: bool, - pub(crate) git_commit: bool, -} - pub(crate) struct SubAgentConfigBuilder { config: Config, } @@ -153,22 +90,6 @@ impl SubAgentConfigBuilder { self } - pub(crate) fn extension_inheritance( - mut self, - inheritance: SubAgentExtensionInheritance, - ) -> anyhow::Result { - match inheritance { - SubAgentExtensionInheritance::InheritParent => {} - SubAgentExtensionInheritance::None => { - self.apply_extension_selection(SubAgentExtensionSelection::none())?; - } - SubAgentExtensionInheritance::Select(selection) => { - self.apply_extension_selection(selection)?; - } - } - Ok(self) - } - pub(crate) fn build(self) -> Config { self.config } @@ -195,122 +116,4 @@ impl SubAgentConfigBuilder { self.config.include_apps_instructions = selection.apps; self.config.include_environment_context = selection.environment_context; } - - fn apply_extension_selection( - &mut self, - selection: SubAgentExtensionSelection, - ) -> anyhow::Result<()> { - if !selection.mcp_servers { - self.config.mcp_servers = Constrained::allow_only(Default::default()); - } - if !selection.apps { - self.disable_feature(Feature::Apps)?; - } - if !selection.plugins { - self.disable_feature(Feature::Plugins)?; - } - if !selection.tool_search { - self.disable_feature(Feature::ToolSearch)?; - self.disable_feature(Feature::ToolSuggest)?; - } - if !selection.web_search { - self.disable_feature(Feature::WebSearchRequest)?; - self.disable_feature(Feature::WebSearchCached)?; - } - if !selection.js_repl { - self.config.js_repl_node_path = None; - self.config.js_repl_node_module_dirs = Vec::new(); - self.disable_feature(Feature::JsReplToolsOnly)?; - self.disable_feature(Feature::JsRepl)?; - } - if !selection.code_mode { - self.disable_feature(Feature::CodeModeOnly)?; - self.disable_feature(Feature::CodeMode)?; - } - if !selection.memory { - self.disable_feature(Feature::MemoryTool)?; - } - if !selection.apply_patch { - self.config.include_apply_patch_tool = false; - self.disable_feature(Feature::ApplyPatchFreeform)?; - } - if !selection.subagents { - self.disable_feature(Feature::SpawnCsv)?; - self.disable_feature(Feature::MultiAgentV2)?; - self.disable_feature(Feature::Collab)?; - self.disable_feature(Feature::ChildAgentsMd)?; - } - if !selection.image_generation { - self.disable_feature(Feature::ImageGeneration)?; - } - if !selection.request_permissions { - self.disable_feature(Feature::RequestPermissionsTool)?; - self.disable_feature(Feature::ExecPermissionApprovals)?; - } - if !selection.skill_dependencies { - self.disable_feature(Feature::SkillMcpDependencyInstall)?; - self.disable_feature(Feature::SkillEnvVarDependencyPrompt)?; - } - if !selection.shell_snapshot { - self.disable_feature(Feature::ShellSnapshot)?; - } - if !selection.shell_zsh_fork { - self.config.zsh_path = None; - self.disable_feature(Feature::ShellZshFork)?; - } - if !selection.unified_exec { - self.config.use_experimental_unified_exec_tool = false; - self.config.main_execve_wrapper_exe = None; - self.disable_feature(Feature::UnifiedExec)?; - } - if !selection.request_rule { - self.disable_feature(Feature::RequestRule)?; - } - if !selection.git_commit { - self.disable_feature(Feature::CodexGitCommit)?; - } - - Ok(()) - } - - fn disable_feature(&mut self, feature: Feature) -> anyhow::Result<()> { - self.config.features.disable(feature).map_err(|err| { - anyhow::anyhow!( - "subagent config could not disable `features.{}`: {err}", - feature.key() - ) - })?; - if self.config.features.enabled(feature) { - anyhow::bail!( - "subagent config requires `features.{}` to be disabled", - feature.key() - ); - } - Ok(()) - } -} - -impl SubAgentExtensionSelection { - const fn none() -> Self { - Self { - mcp_servers: false, - apps: false, - plugins: false, - tool_search: false, - web_search: false, - js_repl: false, - code_mode: false, - memory: false, - apply_patch: false, - subagents: false, - image_generation: false, - request_permissions: false, - skill_dependencies: false, - shell_snapshot: false, - shell_zsh_fork: false, - unified_exec: false, - request_rule: false, - git_commit: false, - } - } } diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index c00f713baf..6feb8f0439 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -106,6 +106,7 @@ use codex_protocol::mcp::CallToolResult; use codex_protocol::models::BaseInstructions; use codex_protocol::models::PermissionProfile; use codex_protocol::models::format_allow_prefixes; +use codex_protocol::openai_models::ConfigShellToolType; use codex_protocol::openai_models::ModelInfo; use codex_protocol::permissions::FileSystemSandboxPolicy; use codex_protocol::permissions::NetworkSandboxPolicy; @@ -134,6 +135,8 @@ use codex_rmcp_client::ElicitationResponse; use codex_rollout::state_db; use codex_shell_command::parse_command::parse_command; use codex_terminal_detection::user_agent; +use codex_tools::ShellCommandBackendConfig; +use codex_tools::UnifiedExecShellMode; use codex_tools::filter_tool_suggest_discoverable_tools_for_client; use codex_utils_output_truncation::TruncationPolicy; use codex_utils_stream_parser::AssistantTextChunk; @@ -268,7 +271,6 @@ use crate::hook_runtime::record_additional_contexts; use crate::hook_runtime::record_pending_input; use crate::hook_runtime::run_pending_session_start_hooks; use crate::hook_runtime::run_user_prompt_submit_hooks; -use crate::initial_context::InitialContextInclusions; use crate::injection::ToolMentionKind; use crate::injection::app_id_from_path; use crate::injection::tool_kind_for_path; @@ -285,7 +287,8 @@ use crate::network_policy_decision::execpolicy_network_rule_amendment; use crate::plugins::PluginsManager; use crate::plugins::build_plugin_injections; use crate::plugins::render_plugins_section; -use crate::project_doc::get_user_instructions; +use crate::project_doc::UserInstructionExtras; +use crate::project_doc::get_user_instructions_with_extras; use crate::resolve_skill_dependencies_for_turn; use crate::rollout::RolloutRecorder; use crate::rollout::RolloutRecorderParams; @@ -293,6 +296,8 @@ use crate::rollout::map_session_init_error; use crate::rollout::metadata; use crate::rollout::policy::EventPersistenceMode; use crate::session_startup_prewarm::SessionStartupPrewarmHandle; +use crate::session_surface::CapabilityInclusions; +use crate::session_surface::SessionSurfacePolicy; use crate::shell; use crate::shell_snapshot::ShellSnapshot; use crate::skills_watcher::SkillsWatcher; @@ -395,6 +400,55 @@ fn image_generation_tool_auth_allowed(auth_manager: Option<&AuthManager>) -> boo ) } +fn apply_capabilities_to_tools_config( + mut tools_config: ToolsConfig, + capabilities: CapabilityInclusions, +) -> ToolsConfig { + if !capabilities.web_search { + tools_config.search_tool = false; + tools_config.web_search_mode = Some(WebSearchMode::Disabled); + tools_config.web_search_config = None; + } + if !capabilities.apps || !capabilities.plugins { + tools_config.tool_suggest = false; + } + if !capabilities.image_generation { + tools_config.image_gen_tool = false; + tools_config.can_request_original_image_detail = false; + } + if !capabilities.apply_patch { + tools_config.apply_patch_tool_type = None; + } + if !capabilities.request_permissions { + tools_config.exec_permission_approvals_enabled = false; + tools_config.request_permissions_tool_enabled = false; + } + if !capabilities.code_mode { + tools_config.code_mode_enabled = false; + tools_config.code_mode_only_enabled = false; + } + if !capabilities.js_repl { + tools_config.js_repl_enabled = false; + tools_config.js_repl_tools_only = false; + } + if !capabilities.shell_zsh_fork { + tools_config.shell_command_backend = ShellCommandBackendConfig::Classic; + tools_config.unified_exec_shell_mode = UnifiedExecShellMode::Direct; + } + if !capabilities.unified_exec && tools_config.shell_type == ConfigShellToolType::UnifiedExec { + tools_config.shell_type = ConfigShellToolType::ShellCommand; + tools_config.unified_exec_shell_mode = UnifiedExecShellMode::Direct; + } + if !capabilities.subagents { + tools_config.collab_tools = false; + tools_config.multi_agent_v2 = false; + tools_config.spawn_agent_usage_hint = false; + tools_config.agent_jobs_tools = false; + tools_config.agent_jobs_worker_tools = false; + } + tools_config +} + /// The high-level interface to the Codex system. /// It operates as a queue pair where you send submissions and receive events. pub struct Codex { @@ -420,7 +474,7 @@ pub struct CodexSpawnOk { pub(crate) struct CodexSpawnArgs { pub(crate) config: Config, - pub(crate) initial_context_inclusions: InitialContextInclusions, + pub(crate) surface_policy: SessionSurfacePolicy, pub(crate) auth_manager: Arc, pub(crate) models_manager: Arc, pub(crate) environment_manager: Arc, @@ -475,7 +529,7 @@ impl Codex { async fn spawn_internal(args: CodexSpawnArgs) -> CodexResult { let CodexSpawnArgs { mut config, - initial_context_inclusions, + surface_policy, auth_manager, models_manager, environment_manager, @@ -497,17 +551,24 @@ impl Codex { let (tx_sub, rx_sub) = async_channel::bounded(SUBMISSION_CHANNEL_CAPACITY); let (tx_event, rx_event) = async_channel::unbounded(); - let plugin_outcome = plugins_manager.plugins_for_config(&config); - let effective_skill_roots = plugin_outcome.effective_skill_roots(); - let skills_input = skills_load_input_from_config(&config, effective_skill_roots); - let loaded_skills = skills_manager.skills_for_config(&skills_input); + if surface_policy.capabilities.skills { + let effective_skill_roots = if surface_policy.capabilities.plugins { + plugins_manager + .plugins_for_config(&config) + .effective_skill_roots() + } else { + Vec::new() + }; + let skills_input = skills_load_input_from_config(&config, effective_skill_roots); + let loaded_skills = skills_manager.skills_for_config(&skills_input); - for err in &loaded_skills.errors { - error!( - "failed to load skill {}: {}", - err.path.display(), - err.message - ); + for err in &loaded_skills.errors { + error!( + "failed to load skill {}: {}", + err.path.display(), + err.message + ); + } } if let SessionSource::SubAgent(SubAgentSource::ThreadSpawn { depth, .. }) = session_source @@ -517,7 +578,8 @@ impl Codex { let _ = config.features.disable(Feature::Collab); } - if config.features.enabled(Feature::JsRepl) + if surface_policy.capabilities.js_repl + && config.features.enabled(Feature::JsRepl) && let Err(err) = resolve_compatible_node(config.js_repl_node_path.as_deref()).await { let _ = config.features.disable(Feature::JsRepl); @@ -534,7 +596,8 @@ impl Codex { warn!("{message}"); config.startup_warnings.push(message); } - if config.features.enabled(Feature::CodeMode) + if surface_policy.capabilities.code_mode + && config.features.enabled(Feature::CodeMode) && let Err(err) = resolve_compatible_node(config.js_repl_node_path.as_deref()).await { let message = format!( @@ -549,7 +612,16 @@ impl Codex { .current() .await .map_err(|err| CodexErr::Fatal(format!("failed to create environment: {err}")))?; - let user_instructions = get_user_instructions(&config, environment.as_deref()).await; + let user_instruction_extras = UserInstructionExtras { + js_repl: surface_policy.capabilities.js_repl, + child_agents_md: surface_policy.capabilities.subagents, + }; + let user_instructions = get_user_instructions_with_extras( + &config, + environment.as_deref(), + user_instruction_extras, + ) + .await; let exec_policy = if crate::guardian::is_guardian_reviewer_source(&session_source) { // Guardian review should rely on the built-in shell safety checks, @@ -598,7 +670,9 @@ impl Codex { // Respect thread-start tools. When missing (resumed/forked threads), read from the db // first, then fall back to rollout-file tools. - let persisted_tools = if dynamic_tools.is_empty() { + let persisted_tools = if surface_policy.capabilities.dynamic_tools + && dynamic_tools.is_empty() + { let thread_id = match &conversation_history { InitialHistory::Resumed(resumed) => Some(resumed.conversation_id), InitialHistory::Forked(_) => conversation_history.forked_from_id(), @@ -615,7 +689,9 @@ impl Codex { } else { None }; - let dynamic_tools = if dynamic_tools.is_empty() { + let dynamic_tools = if !surface_policy.capabilities.dynamic_tools { + Vec::new() + } else if dynamic_tools.is_empty() { persisted_tools .or_else(|| conversation_history.get_dynamic_tools()) .unwrap_or_default() @@ -643,7 +719,7 @@ impl Codex { personality: config.personality, base_instructions, compact_prompt: config.compact_prompt.clone(), - initial_context_inclusions, + surface_policy, approval_policy: config.permissions.approval_policy.clone(), approvals_reviewer: config.approvals_reviewer, sandbox_policy: config.permissions.sandbox_policy.clone(), @@ -896,6 +972,7 @@ pub(crate) struct TurnContext { pub(crate) shell_environment_policy: ShellEnvironmentPolicy, pub(crate) tools_config: ToolsConfig, pub(crate) features: ManagedFeatures, + pub(crate) surface_policy: SessionSurfacePolicy, pub(crate) ghost_snapshot: GhostSnapshotConfig, pub(crate) final_output_json_schema: Option, pub(crate) codex_self_exe: Option, @@ -923,7 +1000,8 @@ impl TurnContext { .and_then(AuthManager::auth_cached) .as_ref() .is_some_and(CodexAuth::is_chatgpt_auth); - self.features.apps_enabled_for_auth(is_chatgpt_auth) + self.surface_policy.capabilities.apps + && self.features.apps_enabled_for_auth(is_chatgpt_auth) } pub(crate) async fn with_model(&self, model: String, models_manager: &ModelsManager) -> Self { @@ -961,30 +1039,33 @@ impl TurnContext { /*developer_instructions*/ None, ); let features = self.features.clone(); - let tools_config = ToolsConfig::new(&ToolsConfigParams { - model_info: &model_info, - available_models: &models_manager - .list_models(RefreshStrategy::OnlineIfUncached) - .await, - features: &features, - image_generation_tool_auth_allowed: image_generation_tool_auth_allowed( - self.auth_manager.as_deref(), - ), - web_search_mode: self.tools_config.web_search_mode, - session_source: self.session_source.clone(), - sandbox_policy: self.sandbox_policy.get(), - windows_sandbox_level: self.windows_sandbox_level, - }) - .with_unified_exec_shell_mode(self.tools_config.unified_exec_shell_mode.clone()) - .with_web_search_config(self.tools_config.web_search_config.clone()) - .with_allow_login_shell(self.tools_config.allow_login_shell) - .with_has_environment(self.tools_config.has_environment) - .with_spawn_agent_usage_hint(config.multi_agent_v2.usage_hint_enabled) - .with_spawn_agent_usage_hint_text(config.multi_agent_v2.usage_hint_text.clone()) - .with_hide_spawn_agent_metadata(config.multi_agent_v2.hide_spawn_agent_metadata) - .with_agent_type_description(crate::agent::role::spawn_tool_spec::build( - &config.agent_roles, - )); + let tools_config = apply_capabilities_to_tools_config( + ToolsConfig::new(&ToolsConfigParams { + model_info: &model_info, + available_models: &models_manager + .list_models(RefreshStrategy::OnlineIfUncached) + .await, + features: &features, + image_generation_tool_auth_allowed: image_generation_tool_auth_allowed( + self.auth_manager.as_deref(), + ), + web_search_mode: self.tools_config.web_search_mode, + session_source: self.session_source.clone(), + sandbox_policy: self.sandbox_policy.get(), + windows_sandbox_level: self.windows_sandbox_level, + }) + .with_unified_exec_shell_mode(self.tools_config.unified_exec_shell_mode.clone()) + .with_web_search_config(self.tools_config.web_search_config.clone()) + .with_allow_login_shell(self.tools_config.allow_login_shell) + .with_has_environment(self.tools_config.has_environment) + .with_spawn_agent_usage_hint(config.multi_agent_v2.usage_hint_enabled) + .with_spawn_agent_usage_hint_text(config.multi_agent_v2.usage_hint_text.clone()) + .with_hide_spawn_agent_metadata(config.multi_agent_v2.hide_spawn_agent_metadata) + .with_agent_type_description(crate::agent::role::spawn_tool_spec::build( + &config.agent_roles, + )), + self.surface_policy.capabilities, + ); Self { sub_id: self.sub_id.clone(), @@ -1020,6 +1101,7 @@ impl TurnContext { shell_environment_policy: self.shell_environment_policy.clone(), tools_config, features, + surface_policy: self.surface_policy, ghost_snapshot: self.ghost_snapshot.clone(), final_output_json_schema: self.final_output_json_schema.clone(), codex_self_exe: self.codex_self_exe.clone(), @@ -1124,12 +1206,12 @@ pub(crate) struct SessionConfiguration { /// Compact prompt override. compact_prompt: Option, - /// Session-scoped policy for which startup context blocks are model-visible. + /// Session-scoped policy for model-visible startup context and runtime surfaces. /// /// This is chosen at spawn time. It is intentionally separate from the /// loaded user config so internally spawned subagents can have narrower - /// startup prompts without mutating config.toml-derived settings. - initial_context_inclusions: InitialContextInclusions, + /// prompts/tools without mutating config.toml-derived settings. + surface_policy: SessionSurfacePolicy, /// When to escalate for approval for execution approval_policy: Constrained, @@ -1497,30 +1579,37 @@ impl Session { let auth_manager_for_context = auth_manager; let provider_for_context = provider; let session_telemetry_for_context = session_telemetry; - let tools_config = ToolsConfig::new(&ToolsConfigParams { - model_info: &model_info, - available_models: &models_manager.try_list_models().unwrap_or_default(), - features: &per_turn_config.features, - image_generation_tool_auth_allowed, - web_search_mode: Some(per_turn_config.web_search_mode.value()), - session_source: session_source.clone(), - sandbox_policy: session_configuration.sandbox_policy.get(), - windows_sandbox_level: session_configuration.windows_sandbox_level, - }) - .with_unified_exec_shell_mode_for_session( - crate::tools::spec::tool_user_shell_type(user_shell), - shell_zsh_path, - main_execve_wrapper_exe, - ) - .with_web_search_config(per_turn_config.web_search_config.clone()) - .with_allow_login_shell(per_turn_config.permissions.allow_login_shell) - .with_has_environment(environment.is_some()) - .with_spawn_agent_usage_hint(per_turn_config.multi_agent_v2.usage_hint_enabled) - .with_spawn_agent_usage_hint_text(per_turn_config.multi_agent_v2.usage_hint_text.clone()) - .with_hide_spawn_agent_metadata(per_turn_config.multi_agent_v2.hide_spawn_agent_metadata) - .with_agent_type_description(crate::agent::role::spawn_tool_spec::build( - &per_turn_config.agent_roles, - )); + let tools_config = apply_capabilities_to_tools_config( + ToolsConfig::new(&ToolsConfigParams { + model_info: &model_info, + available_models: &models_manager.try_list_models().unwrap_or_default(), + features: &per_turn_config.features, + image_generation_tool_auth_allowed, + web_search_mode: Some(per_turn_config.web_search_mode.value()), + session_source: session_source.clone(), + sandbox_policy: session_configuration.sandbox_policy.get(), + windows_sandbox_level: session_configuration.windows_sandbox_level, + }) + .with_unified_exec_shell_mode_for_session( + crate::tools::spec::tool_user_shell_type(user_shell), + shell_zsh_path, + main_execve_wrapper_exe, + ) + .with_web_search_config(per_turn_config.web_search_config.clone()) + .with_allow_login_shell(per_turn_config.permissions.allow_login_shell) + .with_has_environment(environment.is_some()) + .with_spawn_agent_usage_hint(per_turn_config.multi_agent_v2.usage_hint_enabled) + .with_spawn_agent_usage_hint_text( + per_turn_config.multi_agent_v2.usage_hint_text.clone(), + ) + .with_hide_spawn_agent_metadata( + per_turn_config.multi_agent_v2.hide_spawn_agent_metadata, + ) + .with_agent_type_description(crate::agent::role::spawn_tool_spec::build( + &per_turn_config.agent_roles, + )), + session_configuration.surface_policy.capabilities, + ); let cwd = session_configuration.cwd.clone(); @@ -1564,6 +1653,7 @@ impl Session { shell_environment_policy: per_turn_config.permissions.shell_environment_policy.clone(), tools_config, features: per_turn_config.features.clone(), + surface_policy: session_configuration.surface_policy, ghost_snapshot: per_turn_config.ghost_snapshot.clone(), final_output_json_schema: None, codex_self_exe: per_turn_config.codex_self_exe.clone(), @@ -1701,9 +1791,14 @@ impl Session { let auth_manager_clone = Arc::clone(&auth_manager); let config_for_mcp = Arc::clone(&config); let mcp_manager_for_mcp = Arc::clone(&mcp_manager); + let surface_policy_for_mcp = session_configuration.surface_policy; let auth_and_mcp_fut = async move { let auth = auth_manager_clone.auth().await; - let mcp_servers = mcp_manager_for_mcp.effective_servers(&config_for_mcp, auth.as_ref()); + let mcp_servers = if surface_policy_for_mcp.capabilities.mcp { + mcp_manager_for_mcp.effective_servers(&config_for_mcp, auth.as_ref()) + } else { + HashMap::new() + }; let auth_statuses = compute_auth_statuses( mcp_servers.iter(), config_for_mcp.mcp_oauth_credentials_store_mode, @@ -1851,7 +1946,11 @@ impl Session { config.active_profile.clone(), ); - let use_zsh_fork_shell = config.features.enabled(Feature::ShellZshFork); + let use_zsh_fork_shell = session_configuration + .surface_policy + .capabilities + .shell_zsh_fork + && config.features.enabled(Feature::ShellZshFork); let mut default_shell = if let Some(user_shell_override) = session_configuration.user_shell_override.clone() { @@ -1873,7 +1972,12 @@ impl Session { shell::default_user_shell() }; // Create the mutable state for the Session. - let shell_snapshot_tx = if config.features.enabled(Feature::ShellSnapshot) { + let shell_snapshot_tx = if session_configuration + .surface_policy + .capabilities + .shell_snapshot + && config.features.enabled(Feature::ShellSnapshot) + { if let Some(snapshot) = session_configuration.inherited_shell_snapshot.clone() { let (tx, rx) = watch::channel(Some(snapshot)); default_shell.shell_snapshot = rx; @@ -2122,7 +2226,11 @@ impl Session { required_mcp_servers.sort(); let enabled_mcp_server_count = mcp_servers.values().filter(|server| server.enabled).count(); let required_mcp_server_count = required_mcp_servers.len(); - let tool_plugin_provenance = mcp_manager.tool_plugin_provenance(config.as_ref()); + let tool_plugin_provenance = if session_configuration.surface_policy.capabilities.plugins { + mcp_manager.tool_plugin_provenance(config.as_ref()) + } else { + codex_mcp::ToolPluginProvenance::default() + }; { let mut cancel_guard = sess.services.mcp_startup_cancellation_token.lock().await; cancel_guard.cancel(); @@ -2608,17 +2716,26 @@ impl Session { &per_turn_config.to_models_manager_config(), ) .await; - let plugin_outcome = self - .services - .plugins_manager - .plugins_for_config(&per_turn_config); - let effective_skill_roots = plugin_outcome.effective_skill_roots(); - let skills_input = skills_load_input_from_config(&per_turn_config, effective_skill_roots); - let skills_outcome = Arc::new( - self.services - .skills_manager - .skills_for_config(&skills_input), - ); + let skills_outcome = if session_configuration.surface_policy.capabilities.skills { + let effective_skill_roots = if session_configuration.surface_policy.capabilities.plugins + { + self.services + .plugins_manager + .plugins_for_config(&per_turn_config) + .effective_skill_roots() + } else { + Vec::new() + }; + let skills_input = + skills_load_input_from_config(&per_turn_config, effective_skill_roots); + Arc::new( + self.services + .skills_manager + .skills_for_config(&skills_input), + ) + } else { + Arc::new(SkillLoadOutcome::default()) + }; let mut turn_context: TurnContext = Self::make_turn_context( self.conversation_id, Some(Arc::clone(&self.services.auth_manager)), @@ -3696,7 +3813,7 @@ impl Session { previous_turn_settings, collaboration_mode, base_instructions, - initial_context, + surface_policy, ) = { let state = self.state.lock().await; ( @@ -3704,10 +3821,12 @@ impl Session { state.previous_turn_settings(), state.session_configuration.collaboration_mode.clone(), state.session_configuration.base_instructions.clone(), - state.session_configuration.initial_context_inclusions, + state.session_configuration.surface_policy, ) }; - if initial_context.model_update + let prompt_context = surface_policy.prompt; + let capabilities = surface_policy.capabilities; + if prompt_context.model_update && let Some(model_switch_message) = crate::context_manager::updates::build_model_instructions_update_item( previous_turn_settings.as_ref(), @@ -3716,7 +3835,7 @@ impl Session { { developer_sections.push(model_switch_message.into_text()); } - if initial_context.permissions && turn_context.config.include_permissions_instructions { + if prompt_context.permissions && turn_context.config.include_permissions_instructions { developer_sections.push( DeveloperInstructions::from_policy( turn_context.sandbox_policy.get(), @@ -3736,14 +3855,14 @@ impl Session { } // Some internally spawned subagents keep their role policy separate from the // aggregated developer bundle so their top-level instructions are easy to audit. - if initial_context.developer_instructions - && !initial_context.separate_developer_instructions + if prompt_context.developer_instructions + && !prompt_context.separate_developer_instructions && let Some(developer_instructions) = turn_context.developer_instructions.as_deref() { developer_sections.push(developer_instructions.to_string()); } // Add developer instructions for memories. - if initial_context.memory + if capabilities.memory && turn_context.features.enabled(Feature::MemoryTool) && turn_context.config.memories.use_memories && let Some(memory_prompt) = @@ -3752,13 +3871,13 @@ impl Session { developer_sections.push(memory_prompt); } // Add developer instructions from collaboration_mode if they exist and are non-empty - if initial_context.collaboration + if prompt_context.collaboration && let Some(collab_instructions) = DeveloperInstructions::from_collaboration_mode(&collaboration_mode) { developer_sections.push(collab_instructions.into_text()); } - if initial_context.realtime + if prompt_context.realtime && let Some(realtime_update) = crate::context_manager::updates::build_initial_realtime_item( reference_context_item.as_ref(), @@ -3768,7 +3887,7 @@ impl Session { { developer_sections.push(realtime_update.into_text()); } - if initial_context.personality + if prompt_context.personality && self.features.enabled(Feature::Personality) && let Some(personality) = turn_context.personality { @@ -3788,7 +3907,7 @@ impl Session { ); } } - if initial_context.apps + if capabilities.apps && turn_context.config.include_apps_instructions && turn_context.apps_enabled() { @@ -3803,7 +3922,7 @@ impl Session { developer_sections.push(apps_section); } } - if initial_context.skills { + if capabilities.skills { let implicit_skills = turn_context .turn_skills .outcome @@ -3812,7 +3931,7 @@ impl Session { developer_sections.push(skills_section); } } - if initial_context.plugins { + if capabilities.plugins { let loaded_plugins = self .services .plugins_manager @@ -3823,7 +3942,8 @@ impl Session { developer_sections.push(plugin_section); } } - if initial_context.commit + if prompt_context.commit + && capabilities.git_commit && turn_context.features.enabled(Feature::CodexGitCommit) && let Some(commit_message_instruction) = commit_message_trailer_instruction( turn_context.config.commit_attribution.as_deref(), @@ -3831,7 +3951,7 @@ impl Session { { developer_sections.push(commit_message_instruction); } - if initial_context.user_instructions + if prompt_context.user_instructions && let Some(user_instructions) = turn_context.user_instructions.as_deref() { contextual_user_sections.push( @@ -3842,7 +3962,7 @@ impl Session { .serialize_to_text(), ); } - if initial_context.environment_context && turn_context.config.include_environment_context { + if prompt_context.environment_context && turn_context.config.include_environment_context { let subagents = self .services .agent_control @@ -3867,8 +3987,8 @@ impl Session { items.push(contextual_user_message); } // Emit selected role policy as a separate developer item. - if initial_context.developer_instructions - && initial_context.separate_developer_instructions + if prompt_context.developer_instructions + && prompt_context.separate_developer_instructions && let Some(developer_instructions) = turn_context.developer_instructions.as_deref() && let Some(guardian_developer_message) = crate::context_manager::updates::build_developer_update_item(vec![ @@ -4466,6 +4586,9 @@ impl Session { mcp_servers: HashMap, store_mode: OAuthCredentialsStoreMode, ) { + if !turn_context.surface_policy.capabilities.mcp { + return; + } let auth = self.services.auth_manager.auth().await; let config = self.get_config().await; let mcp_config = config.to_mcp_config(self.services.plugins_manager.as_ref()); @@ -5793,6 +5916,7 @@ async fn spawn_review_thread( environment: parent_turn_context.environment.clone(), tools_config, features: parent_turn_context.features.clone(), + surface_policy: parent_turn_context.surface_policy, ghost_snapshot: parent_turn_context.ghost_snapshot.clone(), current_date: parent_turn_context.current_date.clone(), timezone: parent_turn_context.timezone.clone(), @@ -5941,39 +6065,46 @@ pub(crate) async fn run_turn( client_session.reset_websocket_session(); } - let skills_outcome = Some(turn_context.turn_skills.outcome.as_ref()); + let capabilities = turn_context.surface_policy.capabilities; + let skills_outcome = capabilities + .skills + .then_some(turn_context.turn_skills.outcome.as_ref()); sess.record_context_updates_and_set_reference_context_item(turn_context.as_ref()) .await; - let loaded_plugins = sess - .services - .plugins_manager - .plugins_for_config(&turn_context.config); + let loaded_plugins = if capabilities.plugins { + sess.services + .plugins_manager + .plugins_for_config(&turn_context.config) + } else { + Default::default() + }; // Structured plugin:// mentions are resolved from the current session's // enabled plugins, then converted into turn-scoped guidance below. let mentioned_plugins = collect_explicit_plugin_mentions(&input, loaded_plugins.capability_summaries()); - let mcp_tools = if turn_context.apps_enabled() || !mentioned_plugins.is_empty() { - // Plugin mentions need raw MCP/app inventory even when app tools - // are normally hidden so we can describe the plugin's currently - // usable capabilities for this turn. - match sess - .services - .mcp_connection_manager - .read() - .await - .list_all_tools() - .or_cancel(&cancellation_token) - .await - { - Ok(mcp_tools) => mcp_tools, - Err(_) if turn_context.apps_enabled() => return None, - Err(_) => HashMap::new(), - } - } else { - HashMap::new() - }; + let mcp_tools = + if capabilities.mcp && (turn_context.apps_enabled() || !mentioned_plugins.is_empty()) { + // Plugin mentions need raw MCP/app inventory even when app tools + // are normally hidden so we can describe the plugin's currently + // usable capabilities for this turn. + match sess + .services + .mcp_connection_manager + .read() + .await + .list_all_tools() + .or_cancel(&cancellation_token) + .await + { + Ok(mcp_tools) => mcp_tools, + Err(_) if turn_context.apps_enabled() => return None, + Err(_) => HashMap::new(), + } + } else { + HashMap::new() + }; let available_connectors = if turn_context.apps_enabled() { let connectors = connectors::merge_plugin_apps_with_accessible( loaded_plugins.effective_apps(), @@ -5998,21 +6129,24 @@ pub(crate) async fn run_turn( ) }); let config = turn_context.config.clone(); - if config - .features - .enabled(Feature::SkillEnvVarDependencyPrompt) + if capabilities.skill_dependencies + && config + .features + .enabled(Feature::SkillEnvVarDependencyPrompt) { let env_var_dependencies = collect_env_var_dependencies(&mentioned_skills); resolve_skill_dependencies_for_turn(&sess, &turn_context, &env_var_dependencies).await; } - maybe_prompt_and_install_mcp_dependencies( - sess.as_ref(), - turn_context.as_ref(), - &cancellation_token, - &mentioned_skills, - ) - .await; + if capabilities.skill_dependencies { + maybe_prompt_and_install_mcp_dependencies( + sess.as_ref(), + turn_context.as_ref(), + &cancellation_token, + &mentioned_skills, + ) + .await; + } let session_telemetry = turn_context.session_telemetry.clone(); let thread_id = sess.conversation_id.to_string(); @@ -6734,16 +6868,19 @@ async fn run_sampling_request( Arc::clone(&turn_context), Arc::clone(&turn_diff_tracker), ); - let _code_mode_worker = sess - .services - .code_mode_service - .start_turn_worker( - &sess, - &turn_context, - Arc::clone(&router), - Arc::clone(&turn_diff_tracker), - ) - .await; + let _code_mode_worker = if turn_context.surface_policy.capabilities.code_mode { + sess.services + .code_mode_service + .start_turn_worker( + &sess, + &turn_context, + Arc::clone(&router), + Arc::clone(&turn_diff_tracker), + ) + .await + } else { + None + }; let mut retries = 0; loop { let err = match try_run_sampling_request( @@ -6841,17 +6978,25 @@ pub(crate) async fn built_tools( skills_outcome: Option<&SkillLoadOutcome>, cancellation_token: &CancellationToken, ) -> CodexResult> { - let mcp_connection_manager = sess.services.mcp_connection_manager.read().await; - let has_mcp_servers = mcp_connection_manager.has_servers(); - let mut mcp_tools = mcp_connection_manager - .list_all_tools() - .or_cancel(cancellation_token) - .await?; - drop(mcp_connection_manager); - let loaded_plugins = sess - .services - .plugins_manager - .plugins_for_config(&turn_context.config); + let capabilities = turn_context.surface_policy.capabilities; + let (has_mcp_servers, mut mcp_tools) = if capabilities.mcp { + let mcp_connection_manager = sess.services.mcp_connection_manager.read().await; + let has_mcp_servers = mcp_connection_manager.has_servers(); + let mcp_tools = mcp_connection_manager + .list_all_tools() + .or_cancel(cancellation_token) + .await?; + (has_mcp_servers, mcp_tools) + } else { + (false, HashMap::new()) + }; + let loaded_plugins = if capabilities.plugins { + sess.services + .plugins_manager + .plugins_for_config(&turn_context.config) + } else { + Default::default() + }; let mut effective_explicitly_enabled_connectors = explicitly_enabled_connectors.clone(); effective_explicitly_enabled_connectors.extend(sess.get_connector_selection().await); @@ -6958,7 +7103,11 @@ pub(crate) async fn built_tools( .map(|inputs| inputs.tool_namespaces.clone()), app_tools, discoverable_tools, - dynamic_tools: turn_context.dynamic_tools.as_slice(), + dynamic_tools: if capabilities.dynamic_tools { + turn_context.dynamic_tools.as_slice() + } else { + &[] + }, }, ))) } diff --git a/codex-rs/core/src/codex_delegate.rs b/codex-rs/core/src/codex_delegate.rs index e02ddfed79..b1acebf88e 100644 --- a/codex-rs/core/src/codex_delegate.rs +++ b/codex-rs/core/src/codex_delegate.rs @@ -41,13 +41,13 @@ use crate::config::Config; use crate::guardian::GuardianApprovalRequest; use crate::guardian::review_approval_request_with_cancel; use crate::guardian::routes_approval_to_guardian; -use crate::initial_context::InitialContextInclusions; use crate::mcp_tool_call::MCP_TOOL_APPROVAL_ACCEPT; use crate::mcp_tool_call::MCP_TOOL_APPROVAL_ACCEPT_FOR_SESSION; use crate::mcp_tool_call::MCP_TOOL_APPROVAL_DECLINE_SYNTHETIC; use crate::mcp_tool_call::build_guardian_mcp_tool_review_request; use crate::mcp_tool_call::is_mcp_tool_approval_question_id; use crate::mcp_tool_call::lookup_mcp_tool_metadata; +use crate::session_surface::SessionSurfacePolicy; use codex_login::AuthManager; use codex_models_manager::manager::ModelsManager; use codex_protocol::error::CodexErr; @@ -64,7 +64,7 @@ use crate::codex::completed_session_loop_termination; #[allow(clippy::too_many_arguments)] pub(crate) async fn run_codex_thread_interactive( config: Config, - initial_context_inclusions: InitialContextInclusions, + surface_policy: SessionSurfacePolicy, auth_manager: Arc, models_manager: Arc, parent_session: Arc, @@ -78,7 +78,7 @@ pub(crate) async fn run_codex_thread_interactive( let CodexSpawnOk { codex, .. } = Codex::spawn(CodexSpawnArgs { config, - initial_context_inclusions, + surface_policy, auth_manager, models_manager, environment_manager: Arc::new(EnvironmentManager::from_environment( @@ -174,7 +174,7 @@ pub(crate) async fn run_codex_thread_one_shot( let child_cancel = cancel_token.child_token(); let io = run_codex_thread_interactive( config, - InitialContextInclusions::full(), + SessionSurfacePolicy::full(), auth_manager, models_manager, parent_session, diff --git a/codex-rs/core/src/codex_tests.rs b/codex-rs/core/src/codex_tests.rs index cc33c69139..7d625027ae 100644 --- a/codex-rs/core/src/codex_tests.rs +++ b/codex-rs/core/src/codex_tests.rs @@ -1993,7 +1993,7 @@ async fn set_rate_limits_retains_previous_credits() { .clone() .unwrap_or_else(|| model_info.get_model_instructions(config.personality)), compact_prompt: config.compact_prompt.clone(), - initial_context_inclusions: InitialContextInclusions::full(), + surface_policy: SessionSurfacePolicy::full(), approval_policy: config.permissions.approval_policy.clone(), approvals_reviewer: config.approvals_reviewer, sandbox_policy: config.permissions.sandbox_policy.clone(), @@ -2096,7 +2096,7 @@ async fn set_rate_limits_updates_plan_type_when_present() { .clone() .unwrap_or_else(|| model_info.get_model_instructions(config.personality)), compact_prompt: config.compact_prompt.clone(), - initial_context_inclusions: InitialContextInclusions::full(), + surface_policy: SessionSurfacePolicy::full(), approval_policy: config.permissions.approval_policy.clone(), approvals_reviewer: config.approvals_reviewer, sandbox_policy: config.permissions.sandbox_policy.clone(), @@ -2446,7 +2446,7 @@ pub(crate) async fn make_session_configuration_for_tests() -> SessionConfigurati .clone() .unwrap_or_else(|| model_info.get_model_instructions(config.personality)), compact_prompt: config.compact_prompt.clone(), - initial_context_inclusions: InitialContextInclusions::full(), + surface_policy: SessionSurfacePolicy::full(), approval_policy: config.permissions.approval_policy.clone(), approvals_reviewer: config.approvals_reviewer, sandbox_policy: config.permissions.sandbox_policy.clone(), @@ -2710,7 +2710,7 @@ async fn session_new_fails_when_zsh_fork_enabled_without_zsh_path() { .clone() .unwrap_or_else(|| model_info.get_model_instructions(config.personality)), compact_prompt: config.compact_prompt.clone(), - initial_context_inclusions: InitialContextInclusions::full(), + surface_policy: SessionSurfacePolicy::full(), approval_policy: config.permissions.approval_policy.clone(), approvals_reviewer: config.approvals_reviewer, sandbox_policy: config.permissions.sandbox_policy.clone(), @@ -2814,7 +2814,7 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) { .clone() .unwrap_or_else(|| model_info.get_model_instructions(config.personality)), compact_prompt: config.compact_prompt.clone(), - initial_context_inclusions: InitialContextInclusions::full(), + surface_policy: SessionSurfacePolicy::full(), approval_policy: config.permissions.approval_policy.clone(), approvals_reviewer: config.approvals_reviewer, sandbox_policy: config.permissions.sandbox_policy.clone(), @@ -3659,7 +3659,7 @@ pub(crate) async fn make_session_and_context_with_dynamic_tools_and_rx( .clone() .unwrap_or_else(|| model_info.get_model_instructions(config.personality)), compact_prompt: config.compact_prompt.clone(), - initial_context_inclusions: InitialContextInclusions::full(), + surface_policy: SessionSurfacePolicy::full(), approval_policy: config.permissions.approval_policy.clone(), approvals_reviewer: config.approvals_reviewer, sandbox_policy: config.permissions.sandbox_policy.clone(), diff --git a/codex-rs/core/src/codex_tests_guardian.rs b/codex-rs/core/src/codex_tests_guardian.rs index 6e6e498738..7bfa9d2926 100644 --- a/codex-rs/core/src/codex_tests_guardian.rs +++ b/codex-rs/core/src/codex_tests_guardian.rs @@ -7,8 +7,8 @@ use crate::exec::ExecCapturePolicy; use crate::exec::ExecParams; use crate::exec_policy::ExecPolicyManager; use crate::guardian::GUARDIAN_REVIEWER_NAME; -use crate::initial_context::InitialContextInclusions; use crate::sandboxing::SandboxPermissions; +use crate::session_surface::SessionSurfacePolicy; use crate::tools::context::FunctionToolOutput; use crate::turn_diff_tracker::TurnDiffTracker; use codex_app_server_protocol::ConfigLayerSource; @@ -245,11 +245,7 @@ async fn process_compacted_history_preserves_separate_guardian_developer_message { let mut state = session.state.lock().await; state.session_configuration.session_source = guardian_source.clone(); - state.session_configuration.initial_context_inclusions = InitialContextInclusions { - developer_instructions: true, - separate_developer_instructions: true, - ..InitialContextInclusions::none() - }; + state.session_configuration.surface_policy = SessionSurfacePolicy::guardian_review(); } turn_context.session_source = guardian_source; turn_context.developer_instructions = Some(guardian_policy.clone()); @@ -440,7 +436,7 @@ async fn guardian_subagent_does_not_inherit_parent_exec_policy_rules() { let CodexSpawnOk { codex, .. } = Codex::spawn(CodexSpawnArgs { config, - initial_context_inclusions: InitialContextInclusions::full(), + surface_policy: SessionSurfacePolicy::full(), auth_manager, models_manager, environment_manager: Arc::new(EnvironmentManager::new(/*exec_server_url*/ None)), diff --git a/codex-rs/core/src/guardian/review_session.rs b/codex-rs/core/src/guardian/review_session.rs index d475e19ee9..33d7f8a838 100644 --- a/codex-rs/core/src/guardian/review_session.rs +++ b/codex-rs/core/src/guardian/review_session.rs @@ -25,7 +25,6 @@ use tokio_util::sync::CancellationToken; use tracing::warn; use crate::agent::SubAgentConfigBuilder; -use crate::agent::SubAgentExtensionInheritance; use crate::agent::SubAgentPromptInheritance; use crate::agent::SubAgentPromptSelection; use crate::codex::Codex; @@ -36,8 +35,8 @@ use crate::config::Config; use crate::config::Constrained; use crate::config::NetworkProxySpec; use crate::config::Permissions; -use crate::initial_context::InitialContextInclusions; use crate::rollout::recorder::RolloutRecorder; +use crate::session_surface::SessionSurfacePolicy; use codex_model_provider_info::ModelProviderInfo; use super::GUARDIAN_REVIEW_TIMEOUT; @@ -449,7 +448,7 @@ async fn spawn_guardian_review_session( let has_prior_review = initial_history.is_some(); let codex = run_codex_thread_interactive( spawn_config, - guardian_review_initial_context_inclusions(), + SessionSurfacePolicy::guardian_review(), params.parent_session.services.auth_manager.clone(), params.parent_session.services.models_manager.clone(), Arc::clone(¶ms.parent_session), @@ -627,7 +626,6 @@ pub(crate) fn build_guardian_review_session_config( .prompt_inheritance(SubAgentPromptInheritance::Select( guardian_review_prompt_selection(), )) - .extension_inheritance(SubAgentExtensionInheritance::None)? .build(); guardian_config.model = Some(active_model.to_string()); guardian_config.model_reasoning_effort = reasoning_effort; @@ -669,28 +667,15 @@ pub(crate) fn build_guardian_review_session_config( Ok(guardian_config) } -pub(crate) fn guardian_review_initial_context_inclusions() -> InitialContextInclusions { - guardian_review_prompt_selection().initial_context_inclusions() -} - const fn guardian_review_prompt_selection() -> SubAgentPromptSelection { SubAgentPromptSelection { - model_update: false, base_instructions: false, user_instructions: false, project_docs: true, developer_instructions: true, - separate_developer_instructions: true, compact_prompt: false, permissions: false, - memory: false, - collaboration: false, - realtime: false, - personality: false, apps: false, - skills: false, - plugins: false, - commit: false, environment_context: true, } } diff --git a/codex-rs/core/src/guardian/tests.rs b/codex-rs/core/src/guardian/tests.rs index 8ff02f0950..794c81a823 100644 --- a/codex-rs/core/src/guardian/tests.rs +++ b/codex-rs/core/src/guardian/tests.rs @@ -1208,39 +1208,19 @@ fn guardian_review_session_config_strips_parent_prompt_and_tooling_surface() { assert!(!guardian_config.include_apps_instructions); assert!(guardian_config.include_environment_context); assert_eq!(guardian_config.project_doc_max_bytes, 4096); - assert_eq!( - super::review_session::guardian_review_initial_context_inclusions(), - crate::initial_context::InitialContextInclusions { - developer_instructions: true, - separate_developer_instructions: true, - user_instructions: true, - environment_context: true, - ..crate::initial_context::InitialContextInclusions::none() - }, - ); - assert!(guardian_config.mcp_servers.is_empty()); - assert_eq!(guardian_config.js_repl_node_path, None); - assert_eq!( - guardian_config.js_repl_node_module_dirs, - Vec::::new() - ); - assert_eq!(guardian_config.zsh_path, None); - assert_eq!(guardian_config.main_execve_wrapper_exe, None); + let guardian_surface = crate::session_surface::SessionSurfacePolicy::guardian_review(); + assert!(guardian_surface.prompt.developer_instructions); + assert!(guardian_surface.prompt.separate_developer_instructions); + assert!(guardian_surface.prompt.user_instructions); + assert!(guardian_surface.prompt.environment_context); + assert!(!guardian_surface.capabilities.mcp); + assert!(!guardian_surface.capabilities.apps); + assert!(!guardian_surface.capabilities.plugins); + assert!(!guardian_surface.capabilities.skills); + assert!(!guardian_surface.capabilities.memory); assert!(!guardian_config.include_apply_patch_tool); assert!(!guardian_config.use_experimental_unified_exec_tool); assert!(!guardian_config.permissions.allow_login_shell); - for disabled_feature in [ - Feature::JsRepl, - Feature::CodeMode, - Feature::MemoryTool, - Feature::Collab, - Feature::SpawnCsv, - Feature::ChildAgentsMd, - Feature::ApplyPatchFreeform, - Feature::CodexGitCommit, - ] { - assert!(!guardian_config.features.enabled(disabled_feature)); - } } #[test] @@ -1288,32 +1268,33 @@ fn guardian_review_session_config_uses_live_network_proxy_state() { } #[test] -fn guardian_review_session_config_rejects_pinned_collab_feature() { +fn guardian_review_session_config_allows_pinned_parent_extension_features() { let mut parent_config = test_config(); parent_config.features = ManagedFeatures::from_configured( parent_config.features.get().clone(), Some(Sourced { value: FeatureRequirementsToml { - entries: BTreeMap::from([("multi_agent".to_string(), true)]), + entries: BTreeMap::from([ + ("apps".to_string(), true), + ("multi_agent".to_string(), true), + ]), }, source: RequirementSource::Unknown, }), ) .expect("managed features"); - let err = build_guardian_review_session_config_for_test( + let _guardian_config = build_guardian_review_session_config_for_test( &parent_config, /*live_network_config*/ None, "active-model", /*reasoning_effort*/ None, ) - .expect_err("guardian config should fail when collab is pinned on"); + .expect("guardian config should not fail when parent extension features are pinned on"); - let err = err.to_string(); - assert!( - err.contains("`features.multi_agent`"), - "error should name the pinned feature: {err}" - ); + let guardian_surface = crate::session_surface::SessionSurfacePolicy::guardian_review(); + assert!(!guardian_surface.capabilities.apps); + assert!(!guardian_surface.capabilities.subagents); } #[test] diff --git a/codex-rs/core/src/initial_context.rs b/codex-rs/core/src/initial_context.rs deleted file mode 100644 index 3dafa22e78..0000000000 --- a/codex-rs/core/src/initial_context.rs +++ /dev/null @@ -1,57 +0,0 @@ -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) struct InitialContextInclusions { - pub(crate) model_update: bool, - pub(crate) permissions: bool, - pub(crate) developer_instructions: bool, - pub(crate) separate_developer_instructions: bool, - pub(crate) memory: bool, - pub(crate) collaboration: bool, - pub(crate) realtime: bool, - pub(crate) personality: bool, - pub(crate) apps: bool, - pub(crate) skills: bool, - pub(crate) plugins: bool, - pub(crate) commit: bool, - pub(crate) user_instructions: bool, - pub(crate) environment_context: bool, -} - -impl InitialContextInclusions { - pub(crate) const fn full() -> Self { - Self { - model_update: true, - permissions: true, - developer_instructions: true, - separate_developer_instructions: false, - memory: true, - collaboration: true, - realtime: true, - personality: true, - apps: true, - skills: true, - plugins: true, - commit: true, - user_instructions: true, - environment_context: true, - } - } - - pub(crate) const fn none() -> Self { - Self { - model_update: false, - permissions: false, - developer_instructions: false, - separate_developer_instructions: false, - memory: false, - collaboration: false, - realtime: false, - personality: false, - apps: false, - skills: false, - plugins: false, - commit: false, - user_instructions: false, - environment_context: false, - } - } -} diff --git a/codex-rs/core/src/lib.rs b/codex-rs/core/src/lib.rs index 627ca184bf..9a4b4d99c4 100644 --- a/codex-rs/core/src/lib.rs +++ b/codex-rs/core/src/lib.rs @@ -38,10 +38,10 @@ mod flags; mod git_info_tests; mod guardian; mod hook_runtime; -mod initial_context; mod installation_id; pub(crate) mod instructions; pub(crate) mod landlock; +mod session_surface; pub use landlock::spawn_command_under_linux_sandbox; pub(crate) mod mcp; mod mcp_skill_dependencies; diff --git a/codex-rs/core/src/project_doc.rs b/codex-rs/core/src/project_doc.rs index e7321695a7..64b4b9c333 100644 --- a/codex-rs/core/src/project_doc.rs +++ b/codex-rs/core/src/project_doc.rs @@ -42,6 +42,12 @@ pub const LOCAL_PROJECT_DOC_FILENAME: &str = "AGENTS.override.md"; /// be concatenated with the following separator. const PROJECT_DOC_SEPARATOR: &str = "\n\n--- project-doc ---\n\n"; +#[derive(Debug, Clone, Copy)] +pub(crate) struct UserInstructionExtras { + pub(crate) js_repl: bool, + pub(crate) child_agents_md: bool, +} + fn render_js_repl_instructions(config: &Config) -> Option { if !config.features.enabled(Feature::JsRepl) { return None; @@ -78,17 +84,19 @@ fn render_js_repl_instructions(config: &Config) -> Option { /// Combines `Config::instructions` and `AGENTS.md` (if present) into a single /// string of instructions. -pub(crate) async fn get_user_instructions( +pub(crate) async fn get_user_instructions_with_extras( config: &Config, environment: Option<&Environment>, + extras: UserInstructionExtras, ) -> Option { let fs = environment?.get_filesystem(); - get_user_instructions_with_fs(config, fs.as_ref()).await + get_user_instructions_with_fs(config, fs.as_ref(), extras).await } pub(crate) async fn get_user_instructions_with_fs( config: &Config, fs: &dyn ExecutorFileSystem, + extras: UserInstructionExtras, ) -> Option { let project_docs = read_project_docs_with_fs(config, fs).await; @@ -111,14 +119,16 @@ pub(crate) async fn get_user_instructions_with_fs( } }; - if let Some(js_repl_section) = render_js_repl_instructions(config) { + if extras.js_repl + && let Some(js_repl_section) = render_js_repl_instructions(config) + { if !output.is_empty() { output.push_str("\n\n"); } output.push_str(&js_repl_section); } - if config.features.enabled(Feature::ChildAgentsMd) { + if extras.child_agents_md && config.features.enabled(Feature::ChildAgentsMd) { if !output.is_empty() { output.push_str("\n\n"); } diff --git a/codex-rs/core/src/project_doc_tests.rs b/codex-rs/core/src/project_doc_tests.rs index c8caf2ff9a..deae97bb5f 100644 --- a/codex-rs/core/src/project_doc_tests.rs +++ b/codex-rs/core/src/project_doc_tests.rs @@ -11,7 +11,15 @@ use std::path::PathBuf; use tempfile::TempDir; async fn get_user_instructions(config: &Config) -> Option { - super::get_user_instructions_with_fs(config, LOCAL_FS.as_ref()).await + super::get_user_instructions_with_fs( + config, + LOCAL_FS.as_ref(), + super::UserInstructionExtras { + js_repl: true, + child_agents_md: true, + }, + ) + .await } async fn discover_project_doc_paths(config: &Config) -> std::io::Result> { @@ -101,7 +109,15 @@ async fn no_environment_returns_none() { let tmp = tempfile::tempdir().expect("tempdir"); let config = make_config(&tmp, /*limit*/ 4096, Some("user instructions")).await; - let res = super::get_user_instructions(&config, /*environment*/ None).await; + let res = super::get_user_instructions_with_extras( + &config, + /*environment*/ None, + super::UserInstructionExtras { + js_repl: true, + child_agents_md: true, + }, + ) + .await; assert_eq!(res, None); } @@ -247,6 +263,33 @@ async fn js_repl_image_detail_original_does_not_change_instructions() { assert_eq!(res, expected); } +#[tokio::test] +async fn instruction_extras_can_be_omitted_while_keeping_project_docs() { + let tmp = tempfile::tempdir().expect("tempdir"); + fs::write(tmp.path().join("AGENTS.md"), "project guidance").unwrap(); + let mut cfg = make_config(&tmp, /*limit*/ 4096, /*instructions*/ None).await; + let mut features = cfg.features.get().clone(); + features + .enable(Feature::JsRepl) + .enable(Feature::ChildAgentsMd); + cfg.features + .set(features) + .expect("test config should allow instruction extras"); + + let res = super::get_user_instructions_with_fs( + &cfg, + LOCAL_FS.as_ref(), + super::UserInstructionExtras { + js_repl: false, + child_agents_md: false, + }, + ) + .await + .expect("project doc expected"); + + assert_eq!(res, "project guidance"); +} + /// When both system instructions *and* a project doc are present the two /// should be concatenated with the separator. #[tokio::test] diff --git a/codex-rs/core/src/session_surface.rs b/codex-rs/core/src/session_surface.rs new file mode 100644 index 0000000000..b5df0f7766 --- /dev/null +++ b/codex-rs/core/src/session_surface.rs @@ -0,0 +1,136 @@ +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct SessionSurfacePolicy { + pub(crate) prompt: PromptContextInclusions, + pub(crate) capabilities: CapabilityInclusions, +} + +impl SessionSurfacePolicy { + pub(crate) const fn full() -> Self { + Self { + prompt: PromptContextInclusions::full(), + capabilities: CapabilityInclusions::full(), + } + } + + pub(crate) const fn guardian_review() -> Self { + Self { + prompt: PromptContextInclusions { + model_update: false, + permissions: false, + developer_instructions: true, + separate_developer_instructions: true, + collaboration: false, + realtime: false, + personality: false, + commit: false, + user_instructions: true, + environment_context: true, + }, + capabilities: CapabilityInclusions::minimal_local(), + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct PromptContextInclusions { + pub(crate) model_update: bool, + pub(crate) permissions: bool, + pub(crate) developer_instructions: bool, + pub(crate) separate_developer_instructions: bool, + pub(crate) collaboration: bool, + pub(crate) realtime: bool, + pub(crate) personality: bool, + pub(crate) commit: bool, + pub(crate) user_instructions: bool, + pub(crate) environment_context: bool, +} + +impl PromptContextInclusions { + pub(crate) const fn full() -> Self { + Self { + model_update: true, + permissions: true, + developer_instructions: true, + separate_developer_instructions: false, + collaboration: true, + realtime: true, + personality: true, + commit: true, + user_instructions: true, + environment_context: true, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct CapabilityInclusions { + pub(crate) mcp: bool, + pub(crate) apps: bool, + pub(crate) plugins: bool, + pub(crate) skills: bool, + pub(crate) memory: bool, + pub(crate) dynamic_tools: bool, + pub(crate) web_search: bool, + pub(crate) js_repl: bool, + pub(crate) code_mode: bool, + pub(crate) apply_patch: bool, + pub(crate) subagents: bool, + pub(crate) image_generation: bool, + pub(crate) request_permissions: bool, + pub(crate) skill_dependencies: bool, + pub(crate) shell_snapshot: bool, + pub(crate) shell_zsh_fork: bool, + pub(crate) unified_exec: bool, + pub(crate) request_rule: bool, + pub(crate) git_commit: bool, +} + +impl CapabilityInclusions { + pub(crate) const fn full() -> Self { + Self { + mcp: true, + apps: true, + plugins: true, + skills: true, + memory: true, + dynamic_tools: true, + web_search: true, + js_repl: true, + code_mode: true, + apply_patch: true, + subagents: true, + image_generation: true, + request_permissions: true, + skill_dependencies: true, + shell_snapshot: true, + shell_zsh_fork: true, + unified_exec: true, + request_rule: true, + git_commit: true, + } + } + + pub(crate) const fn minimal_local() -> Self { + Self { + mcp: false, + apps: false, + plugins: false, + skills: false, + memory: false, + dynamic_tools: false, + web_search: false, + js_repl: false, + code_mode: false, + apply_patch: false, + subagents: false, + image_generation: false, + request_permissions: false, + skill_dependencies: false, + shell_snapshot: false, + shell_zsh_fork: false, + unified_exec: false, + request_rule: false, + git_commit: false, + } + } +} diff --git a/codex-rs/core/src/thread_manager.rs b/codex-rs/core/src/thread_manager.rs index 3c0babc77e..af687adc8b 100644 --- a/codex-rs/core/src/thread_manager.rs +++ b/codex-rs/core/src/thread_manager.rs @@ -7,11 +7,11 @@ use crate::codex::INITIAL_SUBMIT_ID; use crate::codex_thread::CodexThread; use crate::config::Config; use crate::file_watcher::FileWatcher; -use crate::initial_context::InitialContextInclusions; use crate::mcp::McpManager; use crate::plugins::PluginsManager; use crate::rollout::RolloutRecorder; use crate::rollout::truncation; +use crate::session_surface::SessionSurfacePolicy; use crate::shell_snapshot::ShellSnapshot; use crate::skills_watcher::SkillsWatcher; use crate::skills_watcher::SkillsWatcherEvent; @@ -900,7 +900,7 @@ impl ThreadManagerState { codex, thread_id, .. } = Codex::spawn(CodexSpawnArgs { config, - initial_context_inclusions: InitialContextInclusions::full(), + surface_policy: SessionSurfacePolicy::full(), auth_manager, models_manager: Arc::clone(&self.models_manager), environment_manager: Arc::clone(&self.environment_manager), diff --git a/codex-rs/core/src/tools/handlers/multi_agents_common.rs b/codex-rs/core/src/tools/handlers/multi_agents_common.rs index 75a55e9761..fd2a3b5f49 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents_common.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents_common.rs @@ -1,6 +1,5 @@ use crate::agent::AgentStatus; use crate::agent::SubAgentConfigBuilder; -use crate::agent::SubAgentExtensionInheritance; use crate::agent::SubAgentPromptInheritance; use crate::codex::Session; use crate::codex::TurnContext; @@ -227,12 +226,7 @@ fn build_agent_shared_config(turn: &TurnContext) -> Result