From aa121a115e257cff4ace1075e1a0411d48f22a59 Mon Sep 17 00:00:00 2001 From: Matthew Zeng Date: Fri, 20 Feb 2026 12:05:21 -0800 Subject: [PATCH] [apps] Implement apps configs. (#12086) - [x] Implement apps configs. --- .../codex_app_server_protocol.schemas.json | 101 +++- .../schema/json/v2/ConfigReadResponse.json | 103 +++- ...ppDisabledReason.ts => AppToolApproval.ts} | 2 +- .../schema/typescript/v2/AppToolsConfig.ts | 6 + .../schema/typescript/v2/AppsConfig.ts | 6 +- .../schema/typescript/v2/AppsDefaultConfig.ts | 5 + .../schema/typescript/v2/index.ts | 4 +- .../app-server-protocol/src/protocol/v2.rs | 44 +- .../app-server/tests/suite/v2/config_rpc.rs | 23 +- codex-rs/core/config.schema.json | 89 +++- codex-rs/core/src/codex.rs | 19 +- codex-rs/core/src/config/service.rs | 15 +- codex-rs/core/src/config/types.rs | 84 ++- codex-rs/core/src/connectors.rs | 488 +++++++++++++++++- codex-rs/core/src/mcp_tool_call.rs | 159 ++++-- .../src/tools/handlers/search_tool_bm25.rs | 1 + 16 files changed, 1043 insertions(+), 106 deletions(-) rename codex-rs/app-server-protocol/schema/typescript/v2/{AppDisabledReason.ts => AppToolApproval.ts} (71%) create mode 100644 codex-rs/app-server-protocol/schema/typescript/v2/AppToolsConfig.ts create mode 100644 codex-rs/app-server-protocol/schema/typescript/v2/AppsDefaultConfig.ts diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index 5a840f0bd..7a01b04b5 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json @@ -10665,30 +10665,51 @@ }, "AppConfig": { "properties": { - "disabled_reason": { + "default_tools_approval_mode": { "anyOf": [ { - "$ref": "#/definitions/v2/AppDisabledReason" + "$ref": "#/definitions/v2/AppToolApproval" }, { "type": "null" } ] }, + "default_tools_enabled": { + "type": [ + "boolean", + "null" + ] + }, + "destructive_enabled": { + "type": [ + "boolean", + "null" + ] + }, "enabled": { "default": true, "type": "boolean" + }, + "open_world_enabled": { + "type": [ + "boolean", + "null" + ] + }, + "tools": { + "anyOf": [ + { + "$ref": "#/definitions/v2/AppToolsConfig" + }, + { + "type": "null" + } + ] } }, "type": "object" }, - "AppDisabledReason": { - "enum": [ - "unknown", - "user" - ], - "type": "string" - }, "AppInfo": { "description": "EXPERIMENTAL - app metadata returned by app-list APIs.", "properties": { @@ -10914,7 +10935,69 @@ ], "type": "object" }, + "AppToolApproval": { + "enum": [ + "auto", + "prompt", + "approve" + ], + "type": "string" + }, + "AppToolConfig": { + "properties": { + "approval_mode": { + "anyOf": [ + { + "$ref": "#/definitions/v2/AppToolApproval" + }, + { + "type": "null" + } + ] + }, + "enabled": { + "type": [ + "boolean", + "null" + ] + } + }, + "type": "object" + }, + "AppToolsConfig": { + "type": "object" + }, "AppsConfig": { + "properties": { + "_default": { + "anyOf": [ + { + "$ref": "#/definitions/v2/AppsDefaultConfig" + }, + { + "type": "null" + } + ], + "default": null + } + }, + "type": "object" + }, + "AppsDefaultConfig": { + "properties": { + "destructive_enabled": { + "default": true, + "type": "boolean" + }, + "enabled": { + "default": true, + "type": "boolean" + }, + "open_world_enabled": { + "default": true, + "type": "boolean" + } + }, "type": "object" }, "AppsListParams": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ConfigReadResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ConfigReadResponse.json index 293fe7dc3..4511515d1 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ConfigReadResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ConfigReadResponse.json @@ -19,10 +19,65 @@ }, "AppConfig": { "properties": { - "disabled_reason": { + "default_tools_approval_mode": { "anyOf": [ { - "$ref": "#/definitions/AppDisabledReason" + "$ref": "#/definitions/AppToolApproval" + }, + { + "type": "null" + } + ] + }, + "default_tools_enabled": { + "type": [ + "boolean", + "null" + ] + }, + "destructive_enabled": { + "type": [ + "boolean", + "null" + ] + }, + "enabled": { + "default": true, + "type": "boolean" + }, + "open_world_enabled": { + "type": [ + "boolean", + "null" + ] + }, + "tools": { + "anyOf": [ + { + "$ref": "#/definitions/AppToolsConfig" + }, + { + "type": "null" + } + ] + } + }, + "type": "object" + }, + "AppToolApproval": { + "enum": [ + "auto", + "prompt", + "approve" + ], + "type": "string" + }, + "AppToolConfig": { + "properties": { + "approval_mode": { + "anyOf": [ + { + "$ref": "#/definitions/AppToolApproval" }, { "type": "null" @@ -30,20 +85,48 @@ ] }, "enabled": { - "default": true, - "type": "boolean" + "type": [ + "boolean", + "null" + ] } }, "type": "object" }, - "AppDisabledReason": { - "enum": [ - "unknown", - "user" - ], - "type": "string" + "AppToolsConfig": { + "type": "object" }, "AppsConfig": { + "properties": { + "_default": { + "anyOf": [ + { + "$ref": "#/definitions/AppsDefaultConfig" + }, + { + "type": "null" + } + ], + "default": null + } + }, + "type": "object" + }, + "AppsDefaultConfig": { + "properties": { + "destructive_enabled": { + "default": true, + "type": "boolean" + }, + "enabled": { + "default": true, + "type": "boolean" + }, + "open_world_enabled": { + "default": true, + "type": "boolean" + } + }, "type": "object" }, "AskForApproval": { diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/AppDisabledReason.ts b/codex-rs/app-server-protocol/schema/typescript/v2/AppToolApproval.ts similarity index 71% rename from codex-rs/app-server-protocol/schema/typescript/v2/AppDisabledReason.ts rename to codex-rs/app-server-protocol/schema/typescript/v2/AppToolApproval.ts index bc44835a7..e92cd8e28 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/AppDisabledReason.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/AppToolApproval.ts @@ -2,4 +2,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type AppDisabledReason = "unknown" | "user"; +export type AppToolApproval = "auto" | "prompt" | "approve"; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/AppToolsConfig.ts b/codex-rs/app-server-protocol/schema/typescript/v2/AppToolsConfig.ts new file mode 100644 index 000000000..16a1c22c6 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/AppToolsConfig.ts @@ -0,0 +1,6 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AppToolApproval } from "./AppToolApproval"; + +export type AppToolsConfig = { [key in string]?: { enabled: boolean | null, approval_mode: AppToolApproval | null, } }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/AppsConfig.ts b/codex-rs/app-server-protocol/schema/typescript/v2/AppsConfig.ts index ae3e45520..b22997de9 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/AppsConfig.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/AppsConfig.ts @@ -1,6 +1,8 @@ // GENERATED CODE! DO NOT MODIFY BY HAND! // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AppDisabledReason } from "./AppDisabledReason"; +import type { AppToolApproval } from "./AppToolApproval"; +import type { AppToolsConfig } from "./AppToolsConfig"; +import type { AppsDefaultConfig } from "./AppsDefaultConfig"; -export type AppsConfig = { [key in string]?: { enabled: boolean, disabled_reason: AppDisabledReason | null, } }; +export type AppsConfig = { _default: AppsDefaultConfig | null, } & ({ [key in string]?: { enabled: boolean, destructive_enabled: boolean | null, open_world_enabled: boolean | null, default_tools_approval_mode: AppToolApproval | null, default_tools_enabled: boolean | null, tools: AppToolsConfig | null, } }); diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/AppsDefaultConfig.ts b/codex-rs/app-server-protocol/schema/typescript/v2/AppsDefaultConfig.ts new file mode 100644 index 000000000..e73386027 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/AppsDefaultConfig.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type AppsDefaultConfig = { enabled: boolean, destructive_enabled: boolean, open_world_enabled: boolean, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts index db94051fa..c2f849478 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts @@ -7,13 +7,15 @@ export type { AccountUpdatedNotification } from "./AccountUpdatedNotification"; export type { AgentMessageDeltaNotification } from "./AgentMessageDeltaNotification"; export type { AnalyticsConfig } from "./AnalyticsConfig"; export type { AppBranding } from "./AppBranding"; -export type { AppDisabledReason } from "./AppDisabledReason"; export type { AppInfo } from "./AppInfo"; export type { AppListUpdatedNotification } from "./AppListUpdatedNotification"; export type { AppMetadata } from "./AppMetadata"; export type { AppReview } from "./AppReview"; export type { AppScreenshot } from "./AppScreenshot"; +export type { AppToolApproval } from "./AppToolApproval"; +export type { AppToolsConfig } from "./AppToolsConfig"; export type { AppsConfig } from "./AppsConfig"; +export type { AppsDefaultConfig } from "./AppsDefaultConfig"; export type { AppsListParams } from "./AppsListParams"; export type { AppsListResponse } from "./AppsListResponse"; export type { AskForApproval } from "./AskForApproval"; diff --git a/codex-rs/app-server-protocol/src/protocol/v2.rs b/codex-rs/app-server-protocol/src/protocol/v2.rs index 2641d7c2a..103b5f56a 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -398,12 +398,41 @@ pub struct AnalyticsConfig { pub additional: HashMap, } +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "snake_case")] +#[ts(export_to = "v2/")] +pub enum AppToolApproval { + Auto, + Prompt, + Approve, +} + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "snake_case")] #[ts(export_to = "v2/")] -pub enum AppDisabledReason { - Unknown, - User, +pub struct AppsDefaultConfig { + #[serde(default = "default_enabled")] + pub enabled: bool, + #[serde(default = "default_enabled")] + pub destructive_enabled: bool, + #[serde(default = "default_enabled")] + pub open_world_enabled: bool, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "snake_case")] +#[ts(export_to = "v2/")] +pub struct AppToolConfig { + pub enabled: Option, + pub approval_mode: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "snake_case")] +#[ts(export_to = "v2/")] +pub struct AppToolsConfig { + #[serde(default, flatten)] + pub tools: HashMap, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] @@ -412,15 +441,20 @@ pub enum AppDisabledReason { pub struct AppConfig { #[serde(default = "default_enabled")] pub enabled: bool, - pub disabled_reason: Option, + pub destructive_enabled: Option, + pub open_world_enabled: Option, + pub default_tools_approval_mode: Option, + pub default_tools_enabled: Option, + pub tools: Option, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "snake_case")] #[ts(export_to = "v2/")] pub struct AppsConfig { + #[serde(default, rename = "_default")] + pub default: Option, #[serde(default, flatten)] - #[schemars(with = "HashMap")] pub apps: HashMap, } diff --git a/codex-rs/app-server/tests/suite/v2/config_rpc.rs b/codex-rs/app-server/tests/suite/v2/config_rpc.rs index de4c51cde..cd7471087 100644 --- a/codex-rs/app-server/tests/suite/v2/config_rpc.rs +++ b/codex-rs/app-server/tests/suite/v2/config_rpc.rs @@ -4,7 +4,7 @@ use app_test_support::test_path_buf_with_windows; use app_test_support::test_tmp_path_buf; use app_test_support::to_response; use codex_app_server_protocol::AppConfig; -use codex_app_server_protocol::AppDisabledReason; +use codex_app_server_protocol::AppToolApproval; use codex_app_server_protocol::AppsConfig; use codex_app_server_protocol::AskForApproval; use codex_app_server_protocol::ConfigBatchWriteParams; @@ -156,7 +156,8 @@ async fn config_read_includes_apps() -> Result<()> { r#" [apps.app1] enabled = false -disabled_reason = "user" +destructive_enabled = false +default_tools_approval_mode = "prompt" "#, )?; let codex_home_path = codex_home.path().canonicalize()?; @@ -185,11 +186,16 @@ disabled_reason = "user" assert_eq!( config.apps, Some(AppsConfig { + default: None, apps: std::collections::HashMap::from([( "app1".to_string(), AppConfig { enabled: false, - disabled_reason: Some(AppDisabledReason::User), + destructive_enabled: Some(false), + open_world_enabled: None, + default_tools_approval_mode: Some(AppToolApproval::Prompt), + default_tools_enabled: None, + tools: None, }, )]), }) @@ -202,7 +208,16 @@ disabled_reason = "user" ); assert_eq!( origins - .get("apps.app1.disabled_reason") + .get("apps.app1.destructive_enabled") + .expect("origin") + .name, + ConfigLayerSource::User { + file: user_file.clone(), + } + ); + assert_eq!( + origins + .get("apps.app1.default_tools_approval_mode") .expect("origin") .name, ConfigLayerSource::User { diff --git a/codex-rs/core/config.schema.json b/codex-rs/core/config.schema.json index 97ce03cad..e73a71bb3 100644 --- a/codex-rs/core/config.schema.json +++ b/codex-rs/core/config.schema.json @@ -85,34 +85,111 @@ "additionalProperties": false, "description": "Config values for a single app/connector.", "properties": { - "disabled_reason": { + "default_tools_approval_mode": { "allOf": [ { - "$ref": "#/definitions/AppDisabledReason" + "$ref": "#/definitions/AppToolApproval" } ], - "description": "Reason this app was disabled." + "description": "Approval mode for tools in this app unless a tool override exists." + }, + "default_tools_enabled": { + "description": "Whether tools are enabled by default for this app.", + "type": "boolean" + }, + "destructive_enabled": { + "description": "Whether tools with `destructive_hint = true` are allowed for this app.", + "type": "boolean" }, "enabled": { "default": true, "description": "When `false`, Codex does not surface this app.", "type": "boolean" + }, + "open_world_enabled": { + "description": "Whether tools with `open_world_hint = true` are allowed for this app.", + "type": "boolean" + }, + "tools": { + "allOf": [ + { + "$ref": "#/definitions/AppToolsConfig" + } + ], + "description": "Per-tool settings for this app." } }, "type": "object" }, - "AppDisabledReason": { + "AppToolApproval": { "enum": [ - "unknown", - "user" + "auto", + "prompt", + "approve" ], "type": "string" }, + "AppToolConfig": { + "additionalProperties": false, + "description": "Per-tool settings for a single app tool.", + "properties": { + "approval_mode": { + "allOf": [ + { + "$ref": "#/definitions/AppToolApproval" + } + ], + "description": "Approval mode for this tool." + }, + "enabled": { + "description": "Whether this tool is enabled. `Some(true)` explicitly allows this tool.", + "type": "boolean" + } + }, + "type": "object" + }, + "AppToolsConfig": { + "additionalProperties": { + "$ref": "#/definitions/AppToolConfig" + }, + "description": "Tool settings for a single app.", + "type": "object" + }, "AppsConfigToml": { "additionalProperties": { "$ref": "#/definitions/AppConfig" }, "description": "App/connector settings loaded from `config.toml`.", + "properties": { + "_default": { + "allOf": [ + { + "$ref": "#/definitions/AppsDefaultConfig" + } + ], + "description": "Default settings for all apps." + } + }, + "type": "object" + }, + "AppsDefaultConfig": { + "additionalProperties": false, + "description": "Default settings that apply to all apps.", + "properties": { + "destructive_enabled": { + "description": "Whether tools with `destructive_hint = true` are allowed by default.", + "type": "boolean" + }, + "enabled": { + "default": true, + "description": "When `false`, apps are disabled unless overridden by per-app settings.", + "type": "boolean" + }, + "open_world_enabled": { + "description": "Whether tools with `open_world_hint = true` are allowed by default.", + "type": "boolean" + } + }, "type": "object" }, "AskForApproval": { diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 2d5d283d6..fcddf4e83 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -4834,6 +4834,7 @@ fn connector_inserted_in_messages( fn filter_codex_apps_mcp_tools( mcp_tools: &HashMap, connectors: &[connectors::AppInfo], + config: &Config, ) -> HashMap { let allowed: HashSet<&str> = connectors .iter() @@ -4849,7 +4850,7 @@ fn filter_codex_apps_mcp_tools( let Some(connector_id) = codex_apps_connector_id(tool) else { return false; }; - allowed.contains(connector_id) + allowed.contains(connector_id) && connectors::codex_app_tool_is_enabled(config, tool) }) .map(|(name, tool)| (name.clone(), tool.clone())) .collect() @@ -5023,9 +5024,9 @@ async fn built_tools( None }; - let app_tools = connectors - .as_ref() - .map(|connectors| filter_codex_apps_mcp_tools(&mcp_tools, connectors)); + let app_tools = connectors.as_ref().map(|connectors| { + filter_codex_apps_mcp_tools(&mcp_tools, connectors, &turn_context.config) + }); if let Some(connectors) = connectors.as_ref() { let skill_name_counts_lower = skills_outcome.map_or_else(HashMap::new, |outcome| { @@ -5050,7 +5051,8 @@ async fn built_tools( explicitly_enabled.as_ref(), )); - mcp_tools = selected_mcp_tools; + mcp_tools = + connectors::filter_codex_apps_tools_by_policy(selected_mcp_tools, &turn_context.config); } Ok(Arc::new(ToolRouter::from_config( @@ -6001,7 +6003,7 @@ mod tests { let config_toml_path = codex_home.join(CONFIG_TOML_FILE); std::fs::write( &config_toml_path, - "[apps.calendar]\nenabled = false\ndisabled_reason = \"user\"\n", + "[apps.calendar]\nenabled = false\ndestructive_enabled = false\n", ) .expect("write user config"); @@ -6023,10 +6025,7 @@ mod tests { .expect("calendar app config exists"); assert!(!app.enabled); - assert_eq!( - app.disabled_reason, - Some(crate::config::types::AppDisabledReason::User) - ); + assert_eq!(app.destructive_enabled, Some(false)); } #[test] diff --git a/codex-rs/core/src/config/service.rs b/codex-rs/core/src/config/service.rs index 02a9da23f..1033db728 100644 --- a/codex-rs/core/src/config/service.rs +++ b/codex-rs/core/src/config/service.rs @@ -701,7 +701,7 @@ mod tests { use super::*; use anyhow::Result; use codex_app_server_protocol::AppConfig; - use codex_app_server_protocol::AppDisabledReason; + use codex_app_server_protocol::AppToolApproval; use codex_app_server_protocol::AppsConfig; use codex_app_server_protocol::AskForApproval; use codex_utils_absolute_path::AbsolutePathBuf; @@ -827,13 +827,13 @@ personality = true service .write_value(ConfigValueWriteParams { file_path: Some(tmp.path().join(CONFIG_TOML_FILE).display().to_string()), - key_path: "apps.app1.disabled_reason".to_string(), - value: serde_json::json!("user"), + key_path: "apps.app1.default_tools_approval_mode".to_string(), + value: serde_json::json!("prompt"), merge_strategy: MergeStrategy::Replace, expected_version: None, }) .await - .expect("write apps.app1.disabled_reason succeeds"); + .expect("write apps.app1.default_tools_approval_mode succeeds"); let read = service .read(ConfigReadParams { @@ -846,11 +846,16 @@ personality = true assert_eq!( read.config.apps, Some(AppsConfig { + default: None, apps: std::collections::HashMap::from([( "app1".to_string(), AppConfig { enabled: false, - disabled_reason: Some(AppDisabledReason::User), + destructive_enabled: None, + open_world_enabled: None, + default_tools_approval_mode: Some(AppToolApproval::Prompt), + default_tools_enabled: None, + tools: None, }, )]), }) diff --git a/codex-rs/core/src/config/types.rs b/codex-rs/core/src/config/types.rs index ca90414c2..2e3573f77 100644 --- a/codex-rs/core/src/config/types.rs +++ b/codex-rs/core/src/config/types.rs @@ -425,20 +425,58 @@ impl From for MemoriesConfig { } } -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema)] +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Default, JsonSchema)] #[serde(rename_all = "snake_case")] -pub enum AppDisabledReason { - Unknown, - User, +pub enum AppToolApproval { + #[default] + Auto, + Prompt, + Approve, } -impl fmt::Display for AppDisabledReason { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - AppDisabledReason::Unknown => write!(f, "unknown"), - AppDisabledReason::User => write!(f, "user"), - } - } +/// Default settings that apply to all apps. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default, JsonSchema)] +#[schemars(deny_unknown_fields)] +pub struct AppsDefaultConfig { + /// When `false`, apps are disabled unless overridden by per-app settings. + #[serde(default = "default_enabled")] + pub enabled: bool, + + /// Whether tools with `destructive_hint = true` are allowed by default. + #[serde( + default = "default_enabled", + skip_serializing_if = "std::clone::Clone::clone" + )] + pub destructive_enabled: bool, + + /// Whether tools with `open_world_hint = true` are allowed by default. + #[serde( + default = "default_enabled", + skip_serializing_if = "std::clone::Clone::clone" + )] + pub open_world_enabled: bool, +} + +/// Per-tool settings for a single app tool. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default, JsonSchema)] +#[schemars(deny_unknown_fields)] +pub struct AppToolConfig { + /// Whether this tool is enabled. `Some(true)` explicitly allows this tool. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub enabled: Option, + + /// Approval mode for this tool. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub approval_mode: Option, +} + +/// Tool settings for a single app. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default, JsonSchema)] +#[schemars(deny_unknown_fields)] +pub struct AppToolsConfig { + /// Per-tool overrides keyed by tool name (for example `repos/list`). + #[serde(default, flatten)] + pub tools: HashMap, } /// Config values for a single app/connector. @@ -449,15 +487,35 @@ pub struct AppConfig { #[serde(default = "default_enabled")] pub enabled: bool, - /// Reason this app was disabled. + /// Whether tools with `destructive_hint = true` are allowed for this app. #[serde(default, skip_serializing_if = "Option::is_none")] - pub disabled_reason: Option, + pub destructive_enabled: Option, + + /// Whether tools with `open_world_hint = true` are allowed for this app. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub open_world_enabled: Option, + + /// Approval mode for tools in this app unless a tool override exists. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub default_tools_approval_mode: Option, + + /// Whether tools are enabled by default for this app. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub default_tools_enabled: Option, + + /// Per-tool settings for this app. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub tools: Option, } /// App/connector settings loaded from `config.toml`. #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema)] #[schemars(deny_unknown_fields)] pub struct AppsConfigToml { + /// Default settings for all apps. + #[serde(default, rename = "_default", skip_serializing_if = "Option::is_none")] + pub default: Option, + /// Per-app settings keyed by app ID (for example `[apps.google_drive]`). #[serde(default, flatten)] pub apps: HashMap, diff --git a/codex-rs/core/src/connectors.rs b/codex-rs/core/src/connectors.rs index a34477850..33b003770 100644 --- a/codex-rs/core/src/connectors.rs +++ b/codex-rs/core/src/connectors.rs @@ -11,6 +11,7 @@ pub use codex_app_server_protocol::AppBranding; pub use codex_app_server_protocol::AppInfo; pub use codex_app_server_protocol::AppMetadata; use codex_protocol::protocol::SandboxPolicy; +use rmcp::model::ToolAnnotations; use serde::Deserialize; use tracing::warn; @@ -18,6 +19,7 @@ use crate::AuthManager; use crate::CodexAuth; use crate::SandboxState; use crate::config::Config; +use crate::config::types::AppToolApproval; use crate::config::types::AppsConfigToml; use crate::default_client::is_first_party_chat_originator; use crate::default_client::originator; @@ -32,6 +34,21 @@ use crate::token_data::TokenData; pub const CONNECTORS_CACHE_TTL: Duration = Duration::from_secs(3600); const CONNECTORS_READY_TIMEOUT_ON_EMPTY_TOOLS: Duration = Duration::from_secs(30); +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct AppToolPolicy { + pub enabled: bool, + pub approval: AppToolApproval, +} + +impl Default for AppToolPolicy { + fn default() -> Self { + Self { + enabled: true, + approval: AppToolApproval::Auto, + } + } +} + #[derive(Clone, PartialEq, Eq)] struct AccessibleConnectorsCacheKey { chatgpt_base_url: String, @@ -337,15 +354,58 @@ pub fn merge_connectors( } pub fn with_app_enabled_state(mut connectors: Vec, config: &Config) -> Vec { - let apps = read_apps_config(config).map(|apps_config| apps_config.apps); - for connector in &mut connectors { - if let Some(app) = apps.as_ref().and_then(|apps| apps.get(&connector.id)) { - connector.is_enabled = app.enabled; + let apps_config = read_apps_config(config); + if let Some(apps_config) = apps_config.as_ref() { + for connector in &mut connectors { + connector.is_enabled = app_is_enabled(apps_config, Some(connector.id.as_str())); } } connectors } +pub(crate) fn app_tool_policy( + config: &Config, + connector_id: Option<&str>, + tool_name: &str, + tool_title: Option<&str>, + annotations: Option<&ToolAnnotations>, +) -> AppToolPolicy { + let apps_config = read_apps_config(config); + app_tool_policy_from_apps_config( + apps_config.as_ref(), + connector_id, + tool_name, + tool_title, + annotations, + ) +} + +pub(crate) fn codex_app_tool_is_enabled( + config: &Config, + tool_info: &crate::mcp_connection_manager::ToolInfo, +) -> bool { + if tool_info.server_name != CODEX_APPS_MCP_SERVER_NAME { + return true; + } + + app_tool_policy( + config, + tool_info.connector_id.as_deref(), + &tool_info.tool_name, + tool_info.tool.title.as_deref(), + tool_info.tool.annotations.as_ref(), + ) + .enabled +} + +pub(crate) fn filter_codex_apps_tools_by_policy( + mut mcp_tools: HashMap, + config: &Config, +) -> HashMap { + mcp_tools.retain(|_, tool_info| codex_app_tool_is_enabled(config, tool_info)); + mcp_tools +} + const DISALLOWED_CONNECTOR_IDS: &[&str] = &[ "asdk_app_6938a94a61d881918ef32cb999ff937c", "connector_2b0a9009c9c64bf9933a3dae3f2b1254", @@ -394,6 +454,85 @@ fn read_apps_config(config: &Config) -> Option { AppsConfigToml::deserialize(apps_config).ok() } +fn app_is_enabled(apps_config: &AppsConfigToml, connector_id: Option<&str>) -> bool { + let default_enabled = apps_config + .default + .as_ref() + .map(|defaults| defaults.enabled) + .unwrap_or(true); + + connector_id + .and_then(|connector_id| apps_config.apps.get(connector_id)) + .map(|app| app.enabled) + .unwrap_or(default_enabled) +} + +fn app_tool_policy_from_apps_config( + apps_config: Option<&AppsConfigToml>, + connector_id: Option<&str>, + tool_name: &str, + tool_title: Option<&str>, + annotations: Option<&ToolAnnotations>, +) -> AppToolPolicy { + let Some(apps_config) = apps_config else { + return AppToolPolicy::default(); + }; + + let app = connector_id.and_then(|connector_id| apps_config.apps.get(connector_id)); + let tools = app.and_then(|app| app.tools.as_ref()); + let tool_config = tools.and_then(|tools| { + tools + .tools + .get(tool_name) + .or_else(|| tool_title.and_then(|title| tools.tools.get(title))) + }); + let approval = tool_config + .and_then(|tool| tool.approval_mode) + .or_else(|| app.and_then(|app| app.default_tools_approval_mode)) + .unwrap_or(AppToolApproval::Auto); + + if !app_is_enabled(apps_config, connector_id) { + return AppToolPolicy { + enabled: false, + approval, + }; + } + + if let Some(enabled) = tool_config.and_then(|tool| tool.enabled) { + return AppToolPolicy { enabled, approval }; + } + + if let Some(enabled) = app.and_then(|app| app.default_tools_enabled) { + return AppToolPolicy { enabled, approval }; + } + + let app_defaults = apps_config.default.as_ref(); + let destructive_enabled = app + .and_then(|app| app.destructive_enabled) + .unwrap_or_else(|| { + app_defaults + .map(|defaults| defaults.destructive_enabled) + .unwrap_or(true) + }); + let open_world_enabled = app + .and_then(|app| app.open_world_enabled) + .unwrap_or_else(|| { + app_defaults + .map(|defaults| defaults.open_world_enabled) + .unwrap_or(true) + }); + let destructive_hint = annotations + .and_then(|annotations| annotations.destructive_hint) + .unwrap_or(false); + let open_world_hint = annotations + .and_then(|annotations| annotations.open_world_hint) + .unwrap_or(false); + let enabled = + (destructive_enabled || !destructive_hint) && (open_world_enabled || !open_world_hint); + + AppToolPolicy { enabled, approval } +} + fn collect_accessible_connectors(tools: I) -> Vec where I: IntoIterator)>, @@ -472,8 +611,25 @@ fn format_connector_label(name: &str, _id: &str) -> String { #[cfg(test)] mod tests { use super::*; + use crate::config::types::AppConfig; + use crate::config::types::AppToolConfig; + use crate::config::types::AppToolsConfig; + use crate::config::types::AppsDefaultConfig; use pretty_assertions::assert_eq; + fn annotations( + destructive_hint: Option, + open_world_hint: Option, + ) -> ToolAnnotations { + ToolAnnotations { + destructive_hint, + idempotent_hint: None, + open_world_hint, + read_only_hint: None, + title: None, + } + } + fn app(id: &str) -> AppInfo { AppInfo { id: id.to_string(), @@ -491,6 +647,328 @@ mod tests { } } + #[test] + fn app_tool_policy_uses_global_defaults_for_destructive_hints() { + let apps_config = AppsConfigToml { + default: Some(AppsDefaultConfig { + enabled: true, + destructive_enabled: false, + open_world_enabled: true, + }), + apps: HashMap::new(), + }; + + let policy = app_tool_policy_from_apps_config( + Some(&apps_config), + Some("calendar"), + "events/create", + None, + Some(&annotations(Some(true), None)), + ); + + assert_eq!( + policy, + AppToolPolicy { + enabled: false, + approval: AppToolApproval::Auto, + } + ); + } + + #[test] + fn app_is_enabled_uses_default_for_unconfigured_apps() { + let apps_config = AppsConfigToml { + default: Some(AppsDefaultConfig { + enabled: false, + destructive_enabled: true, + open_world_enabled: true, + }), + apps: HashMap::new(), + }; + + assert!(!app_is_enabled(&apps_config, Some("calendar"))); + assert!(!app_is_enabled(&apps_config, None)); + } + + #[test] + fn app_is_enabled_prefers_per_app_override_over_default() { + let apps_config = AppsConfigToml { + default: Some(AppsDefaultConfig { + enabled: false, + destructive_enabled: true, + open_world_enabled: true, + }), + apps: HashMap::from([( + "calendar".to_string(), + AppConfig { + enabled: true, + destructive_enabled: None, + open_world_enabled: None, + default_tools_approval_mode: None, + default_tools_enabled: None, + tools: None, + }, + )]), + }; + + assert!(app_is_enabled(&apps_config, Some("calendar"))); + assert!(!app_is_enabled(&apps_config, Some("drive"))); + } + + #[test] + fn app_tool_policy_honors_default_app_enabled_false() { + let apps_config = AppsConfigToml { + default: Some(AppsDefaultConfig { + enabled: false, + destructive_enabled: true, + open_world_enabled: true, + }), + apps: HashMap::new(), + }; + + let policy = app_tool_policy_from_apps_config( + Some(&apps_config), + Some("calendar"), + "events/list", + None, + Some(&annotations(None, None)), + ); + + assert_eq!( + policy, + AppToolPolicy { + enabled: false, + approval: AppToolApproval::Auto, + } + ); + } + + #[test] + fn app_tool_policy_allows_per_app_enable_when_default_is_disabled() { + let apps_config = AppsConfigToml { + default: Some(AppsDefaultConfig { + enabled: false, + destructive_enabled: true, + open_world_enabled: true, + }), + apps: HashMap::from([( + "calendar".to_string(), + AppConfig { + enabled: true, + destructive_enabled: None, + open_world_enabled: None, + default_tools_approval_mode: None, + default_tools_enabled: None, + tools: None, + }, + )]), + }; + + let policy = app_tool_policy_from_apps_config( + Some(&apps_config), + Some("calendar"), + "events/list", + None, + Some(&annotations(None, None)), + ); + + assert_eq!( + policy, + AppToolPolicy { + enabled: true, + approval: AppToolApproval::Auto, + } + ); + } + + #[test] + fn app_tool_policy_per_tool_enabled_true_overrides_app_level_disable_flags() { + let apps_config = AppsConfigToml { + default: None, + apps: HashMap::from([( + "calendar".to_string(), + AppConfig { + enabled: true, + destructive_enabled: Some(false), + open_world_enabled: Some(false), + default_tools_approval_mode: None, + default_tools_enabled: None, + tools: Some(AppToolsConfig { + tools: HashMap::from([( + "events/create".to_string(), + AppToolConfig { + enabled: Some(true), + approval_mode: None, + }, + )]), + }), + }, + )]), + }; + + let policy = app_tool_policy_from_apps_config( + Some(&apps_config), + Some("calendar"), + "events/create", + None, + Some(&annotations(Some(true), Some(true))), + ); + + assert_eq!( + policy, + AppToolPolicy { + enabled: true, + approval: AppToolApproval::Auto, + } + ); + } + + #[test] + fn app_tool_policy_default_tools_enabled_true_overrides_app_level_tool_hints() { + let apps_config = AppsConfigToml { + default: None, + apps: HashMap::from([( + "calendar".to_string(), + AppConfig { + enabled: true, + destructive_enabled: Some(false), + open_world_enabled: Some(false), + default_tools_approval_mode: None, + default_tools_enabled: Some(true), + tools: None, + }, + )]), + }; + + let policy = app_tool_policy_from_apps_config( + Some(&apps_config), + Some("calendar"), + "events/create", + None, + Some(&annotations(Some(true), Some(true))), + ); + + assert_eq!( + policy, + AppToolPolicy { + enabled: true, + approval: AppToolApproval::Auto, + } + ); + } + + #[test] + fn app_tool_policy_default_tools_enabled_false_overrides_app_level_tool_hints() { + let apps_config = AppsConfigToml { + default: None, + apps: HashMap::from([( + "calendar".to_string(), + AppConfig { + enabled: true, + destructive_enabled: Some(true), + open_world_enabled: Some(true), + default_tools_approval_mode: Some(AppToolApproval::Approve), + default_tools_enabled: Some(false), + tools: None, + }, + )]), + }; + + let policy = app_tool_policy_from_apps_config( + Some(&apps_config), + Some("calendar"), + "events/list", + None, + Some(&annotations(None, None)), + ); + + assert_eq!( + policy, + AppToolPolicy { + enabled: false, + approval: AppToolApproval::Approve, + } + ); + } + + #[test] + fn app_tool_policy_uses_default_tools_approval_mode() { + let apps_config = AppsConfigToml { + default: None, + apps: HashMap::from([( + "calendar".to_string(), + AppConfig { + enabled: true, + destructive_enabled: None, + open_world_enabled: None, + default_tools_approval_mode: Some(AppToolApproval::Prompt), + default_tools_enabled: None, + tools: Some(AppToolsConfig { + tools: HashMap::new(), + }), + }, + )]), + }; + + let policy = app_tool_policy_from_apps_config( + Some(&apps_config), + Some("calendar"), + "events/list", + None, + Some(&annotations(None, None)), + ); + + assert_eq!( + policy, + AppToolPolicy { + enabled: true, + approval: AppToolApproval::Prompt, + } + ); + } + + #[test] + fn app_tool_policy_matches_prefix_stripped_tool_name_for_tool_config() { + let apps_config = AppsConfigToml { + default: None, + apps: HashMap::from([( + "calendar".to_string(), + AppConfig { + enabled: true, + destructive_enabled: Some(false), + open_world_enabled: Some(false), + default_tools_approval_mode: Some(AppToolApproval::Auto), + default_tools_enabled: Some(false), + tools: Some(AppToolsConfig { + tools: HashMap::from([( + "events/create".to_string(), + AppToolConfig { + enabled: Some(true), + approval_mode: Some(AppToolApproval::Approve), + }, + )]), + }), + }, + )]), + }; + + let policy = app_tool_policy_from_apps_config( + Some(&apps_config), + Some("calendar"), + "calendar_events/create", + Some("events/create"), + Some(&annotations(Some(true), Some(true))), + ); + + assert_eq!( + policy, + AppToolPolicy { + enabled: true, + approval: AppToolApproval::Approve, + } + ); + } + #[test] fn filter_disallowed_connectors_allows_non_disallowed_connectors() { let filtered = filter_disallowed_connectors(vec![app("asdk_app_hidden"), app("alpha")]); @@ -528,7 +1006,7 @@ mod tests { ); assert_eq!( filtered, - vec![app("asdk_app_6938a94a61d881918ef32cb999ff937c"),] + vec![app("asdk_app_6938a94a61d881918ef32cb999ff937c")] ); } } diff --git a/codex-rs/core/src/mcp_tool_call.rs b/codex-rs/core/src/mcp_tool_call.rs index 5f7734118..00327386b 100644 --- a/codex-rs/core/src/mcp_tool_call.rs +++ b/codex-rs/core/src/mcp_tool_call.rs @@ -7,6 +7,8 @@ use crate::analytics_client::AppInvocation; use crate::analytics_client::build_track_events_context; use crate::codex::Session; use crate::codex::TurnContext; +use crate::config::types::AppToolApproval; +use crate::connectors; use crate::mcp::CODEX_APPS_MCP_SERVER_NAME; use crate::protocol::EventMsg; use crate::protocol::McpInvocation; @@ -64,9 +66,51 @@ pub(crate) async fn handle_mcp_tool_call( arguments: arguments_value.clone(), }; - if let Some(decision) = - maybe_request_mcp_tool_approval(sess.as_ref(), turn_context, &call_id, &server, &tool_name) - .await + let metadata = lookup_mcp_tool_metadata(sess.as_ref(), &server, &tool_name).await; + let app_tool_policy = if server == CODEX_APPS_MCP_SERVER_NAME { + connectors::app_tool_policy( + &turn_context.config, + metadata + .as_ref() + .and_then(|metadata| metadata.connector_id.as_deref()), + &tool_name, + metadata + .as_ref() + .and_then(|metadata| metadata.tool_title.as_deref()), + metadata + .as_ref() + .and_then(|metadata| metadata.annotations.as_ref()), + ) + } else { + connectors::AppToolPolicy::default() + }; + + if server == CODEX_APPS_MCP_SERVER_NAME && !app_tool_policy.enabled { + let result = notify_mcp_tool_call_skip( + sess.as_ref(), + turn_context, + &call_id, + invocation, + "MCP tool call blocked by app configuration".to_string(), + ) + .await; + let status = if result.is_ok() { "ok" } else { "error" }; + turn_context + .otel_manager + .counter("codex.mcp.call", 1, &[("status", status)]); + return ResponseInputItem::McpToolCallOutput { call_id, result }; + } + + if let Some(decision) = maybe_request_mcp_tool_approval( + sess.as_ref(), + turn_context, + &call_id, + &server, + &tool_name, + metadata.as_ref(), + app_tool_policy.approval, + ) + .await { let result = match decision { McpToolApprovalDecision::Accept | McpToolApprovalDecision::AcceptAndRemember => { @@ -267,7 +311,7 @@ enum McpToolApprovalDecision { } struct McpToolApprovalMetadata { - annotations: ToolAnnotations, + annotations: Option, connector_id: Option, connector_name: Option, tool_title: Option, @@ -292,21 +336,39 @@ async fn maybe_request_mcp_tool_approval( call_id: &str, server: &str, tool_name: &str, + metadata: Option<&McpToolApprovalMetadata>, + approval_mode: AppToolApproval, ) -> Option { - if is_full_access_mode(turn_context) { + if approval_mode == AppToolApproval::Approve { return None; } + let annotations = metadata.and_then(|metadata| metadata.annotations.as_ref()); + if approval_mode == AppToolApproval::Auto { + if is_full_access_mode(turn_context) { + return None; + } + if !annotations.is_some_and(requires_mcp_tool_approval) { + return None; + } + } - let metadata = lookup_mcp_tool_metadata(sess, server, tool_name).await?; - if !requires_mcp_tool_approval(&metadata.annotations) { - return None; - } - let approval_key = McpToolApprovalKey { - server: server.to_string(), - connector_id: metadata.connector_id.clone(), - tool_name: tool_name.to_string(), + let approval_key = if approval_mode == AppToolApproval::Auto { + let connector_id = metadata.and_then(|metadata| metadata.connector_id.clone()); + if server == CODEX_APPS_MCP_SERVER_NAME && connector_id.is_none() { + None + } else { + Some(McpToolApprovalKey { + server: server.to_string(), + connector_id, + tool_name: tool_name.to_string(), + }) + } + } else { + None }; - if mcp_tool_approval_is_remembered(sess, &approval_key).await { + if let Some(key) = approval_key.as_ref() + && mcp_tool_approval_is_remembered(sess, key).await + { return Some(McpToolApprovalDecision::Accept); } @@ -315,10 +377,10 @@ async fn maybe_request_mcp_tool_approval( question_id.clone(), server, tool_name, - metadata.tool_title.as_deref(), - metadata.connector_name.as_deref(), - &metadata.annotations, - true, + metadata.and_then(|metadata| metadata.tool_title.as_deref()), + metadata.and_then(|metadata| metadata.connector_name.as_deref()), + annotations, + approval_key.is_some(), ); let args = RequestUserInputArgs { questions: vec![question], @@ -326,9 +388,14 @@ async fn maybe_request_mcp_tool_approval( let response = sess .request_user_input(turn_context, call_id.to_string(), args) .await; - let decision = parse_mcp_tool_approval_response(response, &question_id); - if matches!(decision, McpToolApprovalDecision::AcceptAndRemember) { - remember_mcp_tool_approval(sess, approval_key).await; + let decision = normalize_approval_decision_for_mode( + parse_mcp_tool_approval_response(response, &question_id), + approval_mode, + ); + if matches!(decision, McpToolApprovalDecision::AcceptAndRemember) + && let Some(key) = approval_key + { + remember_mcp_tool_approval(sess, key).await; } Some(decision) } @@ -356,15 +423,12 @@ async fn lookup_mcp_tool_metadata( tools.into_values().find_map(|tool_info| { if tool_info.server_name == server && tool_info.tool_name == tool_name { - tool_info - .tool - .annotations - .map(|annotations| McpToolApprovalMetadata { - annotations, - connector_id: tool_info.connector_id, - connector_name: tool_info.connector_name, - tool_title: tool_info.tool.title, - }) + Some(McpToolApprovalMetadata { + annotations: tool_info.tool.annotations, + connector_id: tool_info.connector_id, + connector_name: tool_info.connector_name, + tool_title: tool_info.tool.title, + }) } else { None } @@ -402,11 +466,12 @@ fn build_mcp_tool_approval_question( tool_name: &str, tool_title: Option<&str>, connector_name: Option<&str>, - annotations: &ToolAnnotations, + annotations: Option<&ToolAnnotations>, allow_remember_option: bool, ) -> RequestUserInputQuestion { - let destructive = annotations.destructive_hint == Some(true); - let open_world = annotations.open_world_hint == Some(true); + let destructive = + annotations.and_then(|annotations| annotations.destructive_hint) == Some(true); + let open_world = annotations.and_then(|annotations| annotations.open_world_hint) == Some(true); let reason = match (destructive, open_world) { (true, true) => "may modify data and access external systems", (true, false) => "may modify or delete data", @@ -493,6 +558,19 @@ fn parse_mcp_tool_approval_response( } } +fn normalize_approval_decision_for_mode( + decision: McpToolApprovalDecision, + approval_mode: AppToolApproval, +) -> McpToolApprovalDecision { + if approval_mode == AppToolApproval::Prompt + && decision == McpToolApprovalDecision::AcceptAndRemember + { + McpToolApprovalDecision::Accept + } else { + decision + } +} + async fn mcp_tool_approval_is_remembered(sess: &Session, key: &McpToolApprovalKey) -> bool { let store = sess.services.tool_approvals.lock().await; matches!(store.get(key), Some(ReviewDecision::ApprovedForSession)) @@ -568,6 +646,17 @@ mod tests { assert_eq!(requires_mcp_tool_approval(&annotations), false); } + #[test] + fn prompt_mode_does_not_allow_session_remember() { + assert_eq!( + normalize_approval_decision_for_mode( + McpToolApprovalDecision::AcceptAndRemember, + AppToolApproval::Prompt, + ), + McpToolApprovalDecision::Accept + ); + } + #[test] fn custom_mcp_tool_question_mentions_server_name() { let question = build_mcp_tool_approval_question( @@ -576,7 +665,7 @@ mod tests { "run_action", Some("Run Action"), None, - &annotations(Some(false), Some(true), None), + Some(&annotations(Some(false), Some(true), None)), true, ); @@ -603,7 +692,7 @@ mod tests { "run_action", Some("Run Action"), None, - &annotations(Some(false), Some(true), None), + Some(&annotations(Some(false), Some(true), None)), true, ); diff --git a/codex-rs/core/src/tools/handlers/search_tool_bm25.rs b/codex-rs/core/src/tools/handlers/search_tool_bm25.rs index c5e267cb4..a4878b73a 100644 --- a/codex-rs/core/src/tools/handlers/search_tool_bm25.rs +++ b/codex-rs/core/src/tools/handlers/search_tool_bm25.rs @@ -124,6 +124,7 @@ impl ToolHandler for SearchToolBm25Handler { &turn.config, ); let mcp_tools = filter_codex_apps_mcp_tools(mcp_tools, &connectors); + let mcp_tools = connectors::filter_codex_apps_tools_by_policy(mcp_tools, &turn.config); let mut entries: Vec = mcp_tools .into_iter()