From c78911e37f0e59b01703af1c0f3c8e033b66255f Mon Sep 17 00:00:00 2001 From: Alex Zamoshchin Date: Wed, 17 Jun 2026 08:50:39 -0700 Subject: [PATCH] [ez][codex-rs] Support apps._default.default_tools_approval_mode (#27965) [from codex] ## Summary - add `default_tools_approval_mode` to `[apps._default]` and expose it through app-server v2 `config/read` - apply it after managed, per-tool, and per-app approval settings, before the built-in `auto` fallback - document the precedence, regenerate config/app-server schemas, and add unit plus end-to-end approval coverage ## Configuration ```toml [apps._default] default_tools_approval_mode = "prompt" ``` The effective precedence is managed requirements, tool-specific `approval_mode`, app-specific `default_tools_approval_mode`, `apps._default.default_tools_approval_mode`, then `auto`. ## Test plan - `just write-config-schema` - `just write-app-server-schema` - `just write-app-server-schema --experimental` - `just test -p codex-core app_tool_policy` - `just test -p codex-core mcp_turn_metadata` - `just test -p codex-config` - `just test -p codex-app-server-protocol` - `just test -p codex-app-server config_read_includes_apps` - `just fix -p codex-config -p codex-core -p codex-app-server-protocol -p codex-app-server` - `just fmt` --- .../codex_app_server_protocol.schemas.json | 10 ++++ .../codex_app_server_protocol.v2.schemas.json | 10 ++++ .../schema/json/v2/ConfigReadResponse.json | 10 ++++ .../schema/typescript/v2/AppsDefaultConfig.ts | 3 +- .../src/protocol/v2/config.rs | 1 + codex-rs/app-server/README.md | 9 +++ .../app-server/tests/suite/v2/config_rpc.rs | 12 ++++ codex-rs/config/src/types.rs | 4 ++ codex-rs/connectors/src/app_tool_policy.rs | 6 ++ .../connectors/src/app_tool_policy_tests.rs | 55 ++++++++++++++++++- codex-rs/core/config.schema.json | 8 +++ .../core/tests/suite/mcp_turn_metadata.rs | 11 ++-- 12 files changed, 130 insertions(+), 9 deletions(-) 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 bd894898d..60966655b 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 @@ -6696,6 +6696,16 @@ } ] }, + "default_tools_approval_mode": { + "anyOf": [ + { + "$ref": "#/definitions/v2/AppToolApproval" + }, + { + "type": "null" + } + ] + }, "destructive_enabled": { "default": true, "type": "boolean" diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json index fdc3a342c..9a121fa87 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json @@ -898,6 +898,16 @@ } ] }, + "default_tools_approval_mode": { + "anyOf": [ + { + "$ref": "#/definitions/AppToolApproval" + }, + { + "type": "null" + } + ] + }, "destructive_enabled": { "default": true, "type": "boolean" 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 3bc0a8e0a..c7fa35294 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ConfigReadResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ConfigReadResponse.json @@ -143,6 +143,16 @@ } ] }, + "default_tools_approval_mode": { + "anyOf": [ + { + "$ref": "#/definitions/AppToolApproval" + }, + { + "type": "null" + } + ] + }, "destructive_enabled": { "default": true, "type": "boolean" diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/AppsDefaultConfig.ts b/codex-rs/app-server-protocol/schema/typescript/v2/AppsDefaultConfig.ts index 8277320e6..6b841ef35 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/AppsDefaultConfig.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/AppsDefaultConfig.ts @@ -1,6 +1,7 @@ // 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"; import type { ApprovalsReviewer } from "./ApprovalsReviewer"; -export type AppsDefaultConfig = { enabled: boolean, approvals_reviewer: ApprovalsReviewer | null, destructive_enabled: boolean, open_world_enabled: boolean, }; +export type AppsDefaultConfig = { enabled: boolean, approvals_reviewer: ApprovalsReviewer | null, destructive_enabled: boolean, open_world_enabled: boolean, default_tools_approval_mode: AppToolApproval | null, }; diff --git a/codex-rs/app-server-protocol/src/protocol/v2/config.rs b/codex-rs/app-server-protocol/src/protocol/v2/config.rs index 7190e7876..4f299d022 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/config.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/config.rs @@ -177,6 +177,7 @@ pub struct AppsDefaultConfig { pub destructive_enabled: bool, #[serde(default = "default_enabled")] pub open_world_enabled: bool, + pub default_tools_approval_mode: Option, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index 336f769d7..d1a464858 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -1825,15 +1825,24 @@ approvals_reviewer = "auto_review" [apps._default] approvals_reviewer = "user" +default_tools_approval_mode = "prompt" [apps.demo-app] approvals_reviewer = "auto_review" +default_tools_approval_mode = "approve" ``` Setting the app value to `"user"` routes its approval prompts to the user instead of Guardian; setting it to `"auto_review"` opts that app into Guardian review when allowed by configuration requirements. +Use `apps._default.default_tools_approval_mode` to set the approval mode for +tools without a per-app or per-tool override. Supported values are `"auto"`, +`"prompt"`, and `"approve"`. Tool-level `approval_mode` takes precedence over +the per-app `default_tools_approval_mode`, which takes precedence over the +`apps._default` value. Managed tool requirements take precedence over all of +these settings. When none are configured, the mode defaults to `"auto"`. + Invoke an app by inserting `$` in the text input. The slug is derived from the app name and lowercased with non-alphanumeric characters replaced by `-` (for example, "Demo App" becomes `$demo-app`). Add a `mention` input item (recommended) so the server uses the exact `app://` path rather than guessing by name. Plugins use the same `mention` item shape, but with `plugin://@` paths from `plugin/installed` or `plugin/list`. Example: 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 bfb7d8172..81ef145ef 100644 --- a/codex-rs/app-server/tests/suite/v2/config_rpc.rs +++ b/codex-rs/app-server/tests/suite/v2/config_rpc.rs @@ -363,6 +363,7 @@ async fn config_read_includes_apps() -> Result<()> { r#" [apps._default] approvals_reviewer = "auto_review" +default_tools_approval_mode = "approve" [apps.app1] enabled = false @@ -402,6 +403,7 @@ default_tools_approval_mode = "prompt" approvals_reviewer: Some(ApprovalsReviewer::AutoReview), destructive_enabled: true, open_world_enabled: true, + default_tools_approval_mode: Some(AppToolApproval::Approve), }), apps: std::collections::HashMap::from([( "app1".to_string(), @@ -427,6 +429,16 @@ default_tools_approval_mode = "prompt" profile: None, } ); + assert_eq!( + origins + .get("apps._default.default_tools_approval_mode") + .expect("origin") + .name, + ConfigLayerSource::User { + file: user_file.clone(), + profile: None, + } + ); assert_eq!( origins.get("apps.app1.enabled").expect("origin").name, ConfigLayerSource::User { diff --git a/codex-rs/config/src/types.rs b/codex-rs/config/src/types.rs index 044d09ca8..e459f9585 100644 --- a/codex-rs/config/src/types.rs +++ b/codex-rs/config/src/types.rs @@ -417,6 +417,10 @@ pub struct AppsDefaultConfig { skip_serializing_if = "std::clone::Clone::clone" )] pub open_world_enabled: bool, + + /// Approval mode for tools unless overridden by per-app or per-tool settings. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub default_tools_approval_mode: Option, } /// Per-tool settings for a single app tool. diff --git a/codex-rs/connectors/src/app_tool_policy.rs b/codex-rs/connectors/src/app_tool_policy.rs index 5f1e512be..fbc53693b 100644 --- a/codex-rs/connectors/src/app_tool_policy.rs +++ b/codex-rs/connectors/src/app_tool_policy.rs @@ -162,6 +162,12 @@ fn app_tool_policy_from_apps_config( let approval = managed_approval .or_else(|| tool_config.and_then(|tool| tool.approval_mode)) .or_else(|| app.and_then(|app| app.default_tools_approval_mode)) + .or_else(|| { + input + .connector_id + .and(apps_config.default.as_ref()) + .and_then(|defaults| defaults.default_tools_approval_mode) + }) .unwrap_or(AppToolApproval::Auto); if !app_is_enabled(apps_config, input.connector_id) { diff --git a/codex-rs/connectors/src/app_tool_policy_tests.rs b/codex-rs/connectors/src/app_tool_policy_tests.rs index 45e01cabd..6338e6c86 100644 --- a/codex-rs/connectors/src/app_tool_policy_tests.rs +++ b/codex-rs/connectors/src/app_tool_policy_tests.rs @@ -521,9 +521,59 @@ fn default_tools_enable_overrides_app_level_hints() { } #[test] -fn evaluator_uses_default_tools_approval_mode() { +fn evaluator_uses_apps_default_tools_approval_mode_only_with_connector_id() { let apps_config = AppsConfigToml { - default: None, + default: Some(AppsDefaultConfig { + default_tools_approval_mode: Some(AppToolApproval::Prompt), + ..defaults( + /*enabled*/ true, /*destructive_enabled*/ true, + /*open_world_enabled*/ true, + ) + }), + apps: HashMap::new(), + }; + + assert_eq!( + [ + policy_from_apps_config( + Some(&apps_config), + Some("calendar"), + "events/list", + /*tool_title*/ None, + /*destructive_hint*/ None, + /*open_world_hint*/ None, + /*managed_approval*/ None, + ), + policy_from_apps_config( + Some(&apps_config), + /*connector_id*/ None, + "events/list", + /*tool_title*/ None, + /*destructive_hint*/ None, + /*open_world_hint*/ None, + /*managed_approval*/ None, + ), + ], + [ + AppToolPolicy { + enabled: true, + approval: AppToolApproval::Prompt, + }, + AppToolPolicy::default(), + ] + ); +} + +#[test] +fn evaluator_prefers_app_default_tools_approval_mode_over_apps_default() { + let apps_config = AppsConfigToml { + default: Some(AppsDefaultConfig { + default_tools_approval_mode: Some(AppToolApproval::Approve), + ..defaults( + /*enabled*/ true, /*destructive_enabled*/ true, + /*open_world_enabled*/ true, + ) + }), apps: HashMap::from([( "calendar".to_string(), AppConfig { @@ -720,5 +770,6 @@ fn defaults( approvals_reviewer: None, destructive_enabled, open_world_enabled, + default_tools_approval_mode: None, } } diff --git a/codex-rs/core/config.schema.json b/codex-rs/core/config.schema.json index 66325bf0a..9944c2994 100644 --- a/codex-rs/core/config.schema.json +++ b/codex-rs/core/config.schema.json @@ -218,6 +218,14 @@ ], "description": "Reviewer for approval prompts unless overridden by per-app settings." }, + "default_tools_approval_mode": { + "allOf": [ + { + "$ref": "#/definitions/AppToolApproval" + } + ], + "description": "Approval mode for tools unless overridden by per-app or per-tool settings." + }, "destructive_enabled": { "description": "Whether tools with `destructive_hint = true` are allowed by default.", "type": "boolean" diff --git a/codex-rs/core/tests/suite/mcp_turn_metadata.rs b/codex-rs/core/tests/suite/mcp_turn_metadata.rs index 7fe0a95f4..0d403180d 100644 --- a/codex-rs/core/tests/suite/mcp_turn_metadata.rs +++ b/codex-rs/core/tests/suite/mcp_turn_metadata.rs @@ -60,7 +60,7 @@ default_tools_approval_mode = "{approval_mode}" .with_user_config(&user_config_path, user_config); } -fn set_calendar_approval_mode_and_default_reviewer( +fn set_default_app_approval_mode_and_reviewer( config: &mut Config, approval_mode: AppToolApproval, default_approvals_reviewer: ApprovalsReviewer, @@ -75,8 +75,6 @@ fn set_calendar_approval_mode_and_default_reviewer( r#" [apps._default] approvals_reviewer = "{default_approvals_reviewer}" - -[apps.calendar] default_tools_approval_mode = "{approval_mode}" "# )) @@ -167,7 +165,7 @@ async fn approved_mcp_tool_call_metadata_records_prior_user_input_request() -> R .features .enable(Feature::ToolCallMcpElicitation) .expect("test config should allow feature update"); - set_calendar_approval_mode_and_default_reviewer( + set_default_app_approval_mode_and_reviewer( config, AppToolApproval::Prompt, ApprovalsReviewer::User, @@ -231,7 +229,8 @@ async fn approved_mcp_tool_call_metadata_records_prior_user_input_request() -> R } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn apps_default_auto_review_routes_actual_mcp_approval_to_guardian() -> Result<()> { +async fn apps_default_prompt_with_auto_review_routes_actual_mcp_approval_to_guardian() -> Result<()> +{ skip_if_no_network!(Ok(())); let server = start_mock_server().await; @@ -285,7 +284,7 @@ async fn apps_default_auto_review_routes_actual_mcp_approval_to_guardian() -> Re .features .enable(Feature::ToolCallMcpElicitation) .expect("test config should allow feature update"); - set_calendar_approval_mode_and_default_reviewer( + set_default_app_approval_mode_and_reviewer( config, AppToolApproval::Prompt, ApprovalsReviewer::AutoReview,