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:
jif
2026-06-19 14:42:26 +02:00
committed by GitHub
parent 3a2712ea14
commit 81b000421d
20 changed files with 405 additions and 20 deletions
@@ -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