mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
Add config toggles for orchestrator skills and MCP (#28942)
## Why Orchestrator-provided skills and Codex Apps MCP tools add model-visible instructions, resources, and tools beyond the local workspace. Hosts need config-level switches to disable those orchestrator-owned surfaces independently, without disabling regular skills or regular MCP servers. ## What changed - Adds `[orchestrator.skills].enabled` and `[orchestrator.mcp].enabled` config entries, both defaulting to `true`. - Includes the new settings in `config.schema.json` and in the config lock so resolved thread configuration preserves the same orchestrator exposure decisions. - Threads `orchestrator.skills.enabled` through the app-server skills extension so disabled orchestrator skills do not expose the `skills` namespace or inject orchestrator skill context. - Gates Codex Apps MCP exposure, app instructions, and app auth eligibility on `orchestrator.mcp.enabled` while leaving non-Codex-Apps MCP tools available. - Updates the thread-manager sample config to disable both orchestrator-owned surfaces. ## Verification - Added config parsing, loading, defaulting, and schema coverage for the new settings. - Added MCP exposure coverage that `orchestrator.mcp.enabled = false` removes Codex Apps tools while preserving regular MCP tools. - Added app-server coverage that `orchestrator.skills.enabled = false` prevents orchestrator skill tools, prompts, and resource reads from reaching the model turn.
This commit is contained in:
@@ -9111,6 +9111,39 @@ apps_mcp_product_sku = "tpp"
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn config_loads_orchestrator_settings_from_toml() -> std::io::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let cfg: ConfigToml = toml::from_str(
|
||||
r#"
|
||||
model = "gpt-5.4"
|
||||
|
||||
[orchestrator.skills]
|
||||
enabled = false
|
||||
|
||||
[orchestrator.mcp]
|
||||
enabled = false
|
||||
"#,
|
||||
)
|
||||
.expect("TOML deserialization should succeed for orchestrator settings");
|
||||
|
||||
let config = Config::load_from_base_config_with_overrides(
|
||||
cfg,
|
||||
ConfigOverrides::default(),
|
||||
codex_home.abs(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
assert_eq!(
|
||||
(
|
||||
config.orchestrator_skills_enabled,
|
||||
config.orchestrator_mcp_enabled
|
||||
),
|
||||
(false, false)
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn config_loads_mcp_oauth_callback_url_from_toml() -> std::io::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
|
||||
@@ -690,6 +690,12 @@ pub struct Config {
|
||||
/// Whether to inject the `<skills_instructions>` developer block.
|
||||
pub include_skill_instructions: bool,
|
||||
|
||||
/// Whether orchestrator-owned skills are exposed to the model.
|
||||
pub orchestrator_skills_enabled: bool,
|
||||
|
||||
/// Whether orchestrator-owned MCP tools are exposed to the model.
|
||||
pub orchestrator_mcp_enabled: bool,
|
||||
|
||||
/// Whether to inject the `<environment_context>` user block.
|
||||
pub include_environment_context: bool,
|
||||
|
||||
@@ -2425,6 +2431,12 @@ fn resolve_experimental_request_user_input_enabled(config_toml: &ConfigToml) ->
|
||||
.is_none_or(|config| config.enabled)
|
||||
}
|
||||
|
||||
fn resolve_orchestrator_feature_enabled(
|
||||
feature: Option<&codex_config::config_toml::OrchestratorFeatureToml>,
|
||||
) -> bool {
|
||||
feature.and_then(|feature| feature.enabled).unwrap_or(true)
|
||||
}
|
||||
|
||||
fn resolve_code_mode_config(config_toml: &ConfigToml) -> CodeModeConfig {
|
||||
let base = code_mode_toml_config(config_toml.features.as_ref());
|
||||
|
||||
@@ -2812,6 +2824,11 @@ impl Config {
|
||||
|
||||
validate_model_providers(&cfg.model_providers)
|
||||
.map_err(|message| std::io::Error::new(std::io::ErrorKind::InvalidInput, message))?;
|
||||
let orchestrator = cfg.orchestrator.as_ref();
|
||||
let orchestrator_skills_enabled =
|
||||
resolve_orchestrator_feature_enabled(orchestrator.and_then(|value| value.skills.as_ref()));
|
||||
let orchestrator_mcp_enabled =
|
||||
resolve_orchestrator_feature_enabled(orchestrator.and_then(|value| value.mcp.as_ref()));
|
||||
// Ensure that every field of ConfigRequirements is applied to the final
|
||||
// Config.
|
||||
let ConfigRequirements {
|
||||
@@ -3688,6 +3705,8 @@ impl Config {
|
||||
include_apps_instructions,
|
||||
include_collaboration_mode_instructions,
|
||||
include_skill_instructions,
|
||||
orchestrator_skills_enabled,
|
||||
orchestrator_mcp_enabled,
|
||||
include_environment_context,
|
||||
// The config.toml omits "_mode" because it's a config file. However, "_mode"
|
||||
// is important in code to differentiate the mode from the store implementation.
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use anyhow::Context;
|
||||
use codex_config::config_toml::ConfigLockfileToml;
|
||||
use codex_config::config_toml::ConfigToml;
|
||||
use codex_config::config_toml::OrchestratorFeatureToml;
|
||||
use codex_config::config_toml::OrchestratorToml;
|
||||
use codex_config::types::MemoriesToml;
|
||||
use codex_features::CurrentTimeReminderConfigToml;
|
||||
use codex_features::Feature;
|
||||
@@ -183,6 +185,18 @@ fn save_config_resolved_fields(
|
||||
.skills
|
||||
.get_or_insert_with(Default::default)
|
||||
.include_instructions = Some(config.include_skill_instructions);
|
||||
lock_config
|
||||
.orchestrator
|
||||
.get_or_insert_with(OrchestratorToml::default)
|
||||
.skills
|
||||
.get_or_insert_with(OrchestratorFeatureToml::default)
|
||||
.enabled = Some(config.orchestrator_skills_enabled);
|
||||
lock_config
|
||||
.orchestrator
|
||||
.get_or_insert_with(OrchestratorToml::default)
|
||||
.mcp
|
||||
.get_or_insert_with(Default::default)
|
||||
.enabled = Some(config.orchestrator_mcp_enabled);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -204,6 +204,7 @@ impl TurnContext {
|
||||
self.config
|
||||
.features
|
||||
.apps_enabled_for_auth(uses_codex_backend)
|
||||
&& self.config.orchestrator_mcp_enabled
|
||||
}
|
||||
|
||||
pub(crate) fn tool_environment_mode(&self) -> ToolEnvironmentMode {
|
||||
|
||||
@@ -2,6 +2,7 @@ use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use codex_mcp::CODEX_APPS_MCP_SERVER_NAME;
|
||||
use codex_protocol::items::McpToolCallError;
|
||||
use codex_protocol::items::McpToolCallItem;
|
||||
use codex_protocol::items::McpToolCallStatus;
|
||||
@@ -33,6 +34,23 @@ pub use list_mcp_resource_templates::ListMcpResourceTemplatesHandler;
|
||||
pub use list_mcp_resources::ListMcpResourcesHandler;
|
||||
pub use read_mcp_resource::ReadMcpResourceHandler;
|
||||
|
||||
fn model_can_access_mcp_server(turn: &TurnContext, server: &str) -> bool {
|
||||
turn.config.orchestrator_mcp_enabled || server != CODEX_APPS_MCP_SERVER_NAME
|
||||
}
|
||||
|
||||
fn ensure_model_can_access_mcp_server(
|
||||
turn: &TurnContext,
|
||||
server: &str,
|
||||
) -> Result<(), FunctionCallError> {
|
||||
if model_can_access_mcp_server(turn, server) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(FunctionCallError::RespondToModel(format!(
|
||||
"MCP server '{server}' is disabled by `orchestrator.mcp.enabled`"
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Default)]
|
||||
struct ListResourcesArgs {
|
||||
/// Lists all resources from all servers if not specified.
|
||||
|
||||
@@ -19,6 +19,8 @@ use super::ListResourceTemplatesPayload;
|
||||
use super::call_tool_result_from_content;
|
||||
use super::emit_tool_call_begin;
|
||||
use super::emit_tool_call_end;
|
||||
use super::ensure_model_can_access_mcp_server;
|
||||
use super::model_can_access_mcp_server;
|
||||
use super::normalize_optional_string;
|
||||
use super::parse_args_with_default;
|
||||
use super::parse_arguments;
|
||||
@@ -83,6 +85,7 @@ impl ListMcpResourceTemplatesHandler {
|
||||
|
||||
let payload_result: Result<ListResourceTemplatesPayload, FunctionCallError> = async {
|
||||
if let Some(server_name) = server.clone() {
|
||||
ensure_model_can_access_mcp_server(turn.as_ref(), &server_name)?;
|
||||
let params = cursor
|
||||
.clone()
|
||||
.map(|value| PaginatedRequestParams::default().with_cursor(Some(value)));
|
||||
@@ -109,7 +112,9 @@ impl ListMcpResourceTemplatesHandler {
|
||||
.services
|
||||
.mcp_connection_manager
|
||||
.load_full()
|
||||
.list_all_resource_templates()
|
||||
.list_all_resource_templates(|server_name| {
|
||||
model_can_access_mcp_server(turn.as_ref(), server_name)
|
||||
})
|
||||
.await;
|
||||
Ok(ListResourceTemplatesPayload::from_all_servers(templates))
|
||||
}
|
||||
|
||||
@@ -19,6 +19,8 @@ use super::ListResourcesPayload;
|
||||
use super::call_tool_result_from_content;
|
||||
use super::emit_tool_call_begin;
|
||||
use super::emit_tool_call_end;
|
||||
use super::ensure_model_can_access_mcp_server;
|
||||
use super::model_can_access_mcp_server;
|
||||
use super::normalize_optional_string;
|
||||
use super::parse_args_with_default;
|
||||
use super::parse_arguments;
|
||||
@@ -83,6 +85,7 @@ impl ListMcpResourcesHandler {
|
||||
|
||||
let payload_result: Result<ListResourcesPayload, FunctionCallError> = async {
|
||||
if let Some(server_name) = server.clone() {
|
||||
ensure_model_can_access_mcp_server(turn.as_ref(), &server_name)?;
|
||||
let params = cursor
|
||||
.clone()
|
||||
.map(|value| PaginatedRequestParams::default().with_cursor(Some(value)));
|
||||
@@ -107,7 +110,9 @@ impl ListMcpResourcesHandler {
|
||||
.services
|
||||
.mcp_connection_manager
|
||||
.load_full()
|
||||
.list_all_resources()
|
||||
.list_all_resources(|server_name| {
|
||||
model_can_access_mcp_server(turn.as_ref(), server_name)
|
||||
})
|
||||
.await;
|
||||
Ok(ListResourcesPayload::from_all_servers(resources))
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ use super::ReadResourcePayload;
|
||||
use super::call_tool_result_from_content;
|
||||
use super::emit_tool_call_begin;
|
||||
use super::emit_tool_call_end;
|
||||
use super::ensure_model_can_access_mcp_server;
|
||||
use super::normalize_required_string;
|
||||
use super::parse_args;
|
||||
use super::parse_arguments;
|
||||
@@ -82,6 +83,7 @@ impl ReadMcpResourceHandler {
|
||||
let start = Instant::now();
|
||||
|
||||
let payload_result: Result<ReadResourcePayload, FunctionCallError> = async {
|
||||
ensure_model_can_access_mcp_server(turn.as_ref(), &server)?;
|
||||
let result = session
|
||||
.read_resource(&server, ReadResourceRequestParams::new(uri.clone()))
|
||||
.await
|
||||
|
||||
Reference in New Issue
Block a user