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 6cef24bf3c..bc3a3bc591 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 @@ -6999,6 +6999,17 @@ "default": true, "type": "boolean" }, + "links": { + "anyOf": [ + { + "$ref": "#/definitions/v2/AppLinksConfig" + }, + { + "type": "null" + } + ], + "description": "Per-account approval settings keyed by link ID." + }, "open_world_enabled": { "type": [ "boolean", @@ -7127,6 +7138,36 @@ ], "type": "object" }, + "AppLinkConfig": { + "description": "Approval settings for a connected account within an app.", + "properties": { + "approvals_reviewer": { + "anyOf": [ + { + "$ref": "#/definitions/v2/ApprovalsReviewer" + }, + { + "type": "null" + } + ] + }, + "default_tools_approval_mode": { + "anyOf": [ + { + "$ref": "#/definitions/v2/AppToolApproval" + }, + { + "type": "null" + } + ] + } + }, + "type": "object" + }, + "AppLinksConfig": { + "description": "Account settings for a single app.", + "type": "object" + }, "AppListUpdatedNotification": { "$schema": "http://json-schema.org/draft-07/schema#", "description": "EXPERIMENTAL - notification emitted when the app list changes.", 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 1538744ee9..189ca84107 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 @@ -490,6 +490,17 @@ "default": true, "type": "boolean" }, + "links": { + "anyOf": [ + { + "$ref": "#/definitions/AppLinksConfig" + }, + { + "type": "null" + } + ], + "description": "Per-account approval settings keyed by link ID." + }, "open_world_enabled": { "type": [ "boolean", @@ -618,6 +629,36 @@ ], "type": "object" }, + "AppLinkConfig": { + "description": "Approval settings for a connected account within an app.", + "properties": { + "approvals_reviewer": { + "anyOf": [ + { + "$ref": "#/definitions/ApprovalsReviewer" + }, + { + "type": "null" + } + ] + }, + "default_tools_approval_mode": { + "anyOf": [ + { + "$ref": "#/definitions/AppToolApproval" + }, + { + "type": "null" + } + ] + } + }, + "type": "object" + }, + "AppLinksConfig": { + "description": "Account settings for a single app.", + "type": "object" + }, "AppListUpdatedNotification": { "$schema": "http://json-schema.org/draft-07/schema#", "description": "EXPERIMENTAL - notification emitted when the app list changes.", 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 c85ec90dde..0a8930db24 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ConfigReadResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ConfigReadResponse.json @@ -62,6 +62,17 @@ "default": true, "type": "boolean" }, + "links": { + "anyOf": [ + { + "$ref": "#/definitions/AppLinksConfig" + }, + { + "type": "null" + } + ], + "description": "Per-account approval settings keyed by link ID." + }, "open_world_enabled": { "type": [ "boolean", @@ -81,6 +92,36 @@ }, "type": "object" }, + "AppLinkConfig": { + "description": "Approval settings for a connected account within an app.", + "properties": { + "approvals_reviewer": { + "anyOf": [ + { + "$ref": "#/definitions/ApprovalsReviewer" + }, + { + "type": "null" + } + ] + }, + "default_tools_approval_mode": { + "anyOf": [ + { + "$ref": "#/definitions/AppToolApproval" + }, + { + "type": "null" + } + ] + } + }, + "type": "object" + }, + "AppLinksConfig": { + "description": "Account settings for a single app.", + "type": "object" + }, "AppToolApproval": { "enum": [ "auto", diff --git a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst index 2206603c71..ea1835a891 100644 Binary files a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst and b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst differ diff --git a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst index ffafe4350e..c3b3cedeb4 100644 Binary files a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst and b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst differ diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/AppLinksConfig.ts b/codex-rs/app-server-protocol/schema/typescript/v2/AppLinksConfig.ts new file mode 100644 index 0000000000..c016c924bd --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/AppLinksConfig.ts @@ -0,0 +1,10 @@ +// 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"; + +/** + * Account settings for a single app. + */ +export type AppLinksConfig = { [key in string]?: { approvals_reviewer: ApprovalsReviewer | null, default_tools_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 a4a0f22081..ea07dc3214 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/AppsConfig.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/AppsConfig.ts @@ -1,9 +1,14 @@ // 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 { AppLinksConfig } from "./AppLinksConfig"; import type { AppToolApproval } from "./AppToolApproval"; import type { AppToolsConfig } from "./AppToolsConfig"; import type { ApprovalsReviewer } from "./ApprovalsReviewer"; import type { AppsDefaultConfig } from "./AppsDefaultConfig"; -export type AppsConfig = { _default: AppsDefaultConfig | null, } & ({ [key in string]?: { enabled: boolean, approvals_reviewer: ApprovalsReviewer | null, destructive_enabled: boolean | null, open_world_enabled: boolean | null, default_tools_approval_mode: AppToolApproval | null, default_tools_enabled: boolean | null, tools: AppToolsConfig | null, } }); +export type AppsConfig = { _default: AppsDefaultConfig | null, } & ({ [key in string]?: { enabled: boolean, approvals_reviewer: ApprovalsReviewer | null, destructive_enabled: boolean | null, open_world_enabled: boolean | null, default_tools_approval_mode: AppToolApproval | null, default_tools_enabled: boolean | null, tools: AppToolsConfig | null, +/** + * Per-account approval settings keyed by link ID. + */ +links: AppLinksConfig | null, } }); 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 88bfd411f9..34d4cba17b 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts @@ -20,6 +20,7 @@ export type { AllowDenyRequirement } from "./AllowDenyRequirement"; export type { AnalyticsConfig } from "./AnalyticsConfig"; export type { AppBranding } from "./AppBranding"; export type { AppInfo } from "./AppInfo"; +export type { AppLinksConfig } from "./AppLinksConfig"; export type { AppListUpdatedNotification } from "./AppListUpdatedNotification"; export type { AppMetadata } from "./AppMetadata"; export type { AppReview } from "./AppReview"; 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 e36240c4ac..aade147244 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/config.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/config.rs @@ -209,6 +209,24 @@ pub struct AppToolsConfig { pub tools: HashMap, } +/// Approval settings for a connected account within an app. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "snake_case")] +#[ts(export_to = "v2/")] +pub struct AppLinkConfig { + pub approvals_reviewer: Option, + pub default_tools_approval_mode: Option, +} + +/// Account settings for a single app. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "snake_case")] +#[ts(export_to = "v2/")] +pub struct AppLinksConfig { + #[serde(default, flatten)] + pub links: HashMap, +} + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "snake_case")] #[ts(export_to = "v2/")] @@ -221,6 +239,8 @@ pub struct AppConfig { pub default_tools_approval_mode: Option, pub default_tools_enabled: Option, pub tools: Option, + /// Per-account approval settings keyed by link ID. + pub links: 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 d6e29d7ae7..bf5bae6b9f 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -2416,6 +2416,11 @@ 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. +Per-account approval configuration uses `apps..links.` with +`approvals_reviewer` and `default_tools_approval_mode`. Like `tools`, `links` is +an optional section: `config/read` returns `null` when it is absent, `{}` when +it is explicitly empty, and a map keyed by link ID when accounts are configured. + 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"`, `"writes"`, and `"approve"`. The `"writes"` mode prompts for tools diff --git a/codex-rs/app-server/src/config_manager_service_tests.rs b/codex-rs/app-server/src/config_manager_service_tests.rs index 4020d04a8f..c7d88939df 100644 --- a/codex-rs/app-server/src/config_manager_service_tests.rs +++ b/codex-rs/app-server/src/config_manager_service_tests.rs @@ -357,6 +357,7 @@ async fn write_value_supports_nested_app_paths() -> Result<()> { default_tools_approval_mode: Some(AppToolApproval::Prompt), default_tools_enabled: None, tools: None, + links: None, }, )]), }) 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 3f681f7336..68c77cc665 100644 --- a/codex-rs/app-server/tests/suite/v2/config_rpc.rs +++ b/codex-rs/app-server/tests/suite/v2/config_rpc.rs @@ -4,6 +4,8 @@ use app_test_support::test_path_buf_with_windows; use app_test_support::test_tmp_path_buf; use codex_app_server_protocol::AllowDenyRequirement; use codex_app_server_protocol::AppConfig; +use codex_app_server_protocol::AppLinkConfig; +use codex_app_server_protocol::AppLinksConfig; use codex_app_server_protocol::AppToolApproval; use codex_app_server_protocol::ApprovalsReviewer; use codex_app_server_protocol::AppsConfig; @@ -1020,6 +1022,18 @@ enabled = false approvals_reviewer = "user" destructive_enabled = false default_tools_approval_mode = "prompt" + +[apps.app1.links.link_work] +approvals_reviewer = "auto_review" +default_tools_approval_mode = "approve" + +[apps.app1.links.link_personal] +default_tools_approval_mode = "writes" + +[apps.app_without_links] +enabled = true + +[apps.app_with_empty_links.links] "#, )?; let codex_home_path = codex_home.path().canonicalize()?; @@ -1037,11 +1051,21 @@ default_tools_approval_mode = "prompt" cwd: None, }) .await?; + let response: serde_json::Value = + timeout(DEFAULT_READ_TIMEOUT, mcp.read_response(request_id)).await??; + assert_eq!( + response["config"]["apps"]["app_without_links"].get("links"), + Some(&json!(null)), + ); + assert_eq!( + response["config"]["apps"]["app_with_empty_links"].get("links"), + Some(&json!({})), + ); let ConfigReadResponse { config, origins, layers, - } = timeout(DEFAULT_READ_TIMEOUT, mcp.read_response(request_id)).await??; + } = serde_json::from_value(response)?; assert_eq!( config.apps, @@ -1053,18 +1077,66 @@ default_tools_approval_mode = "prompt" open_world_enabled: true, default_tools_approval_mode: Some(AppToolApproval::Writes), }), - apps: std::collections::HashMap::from([( - "app1".to_string(), - AppConfig { - enabled: false, - approvals_reviewer: Some(ApprovalsReviewer::User), - destructive_enabled: Some(false), - open_world_enabled: None, - default_tools_approval_mode: Some(AppToolApproval::Prompt), - default_tools_enabled: None, - tools: None, - }, - )]), + apps: std::collections::HashMap::from([ + ( + "app1".to_string(), + AppConfig { + enabled: false, + approvals_reviewer: Some(ApprovalsReviewer::User), + destructive_enabled: Some(false), + open_world_enabled: None, + default_tools_approval_mode: Some(AppToolApproval::Prompt), + default_tools_enabled: None, + tools: None, + links: Some(AppLinksConfig { + links: std::collections::HashMap::from([ + ( + "link_work".to_string(), + AppLinkConfig { + approvals_reviewer: Some(ApprovalsReviewer::AutoReview), + default_tools_approval_mode: Some(AppToolApproval::Approve), + }, + ), + ( + "link_personal".to_string(), + AppLinkConfig { + approvals_reviewer: None, + default_tools_approval_mode: Some(AppToolApproval::Writes), + }, + ), + ]), + }), + }, + ), + ( + "app_without_links".to_string(), + AppConfig { + enabled: true, + approvals_reviewer: None, + destructive_enabled: None, + open_world_enabled: None, + default_tools_approval_mode: None, + default_tools_enabled: None, + tools: None, + links: None, + }, + ), + ( + "app_with_empty_links".to_string(), + AppConfig { + enabled: true, + approvals_reviewer: None, + destructive_enabled: None, + open_world_enabled: None, + default_tools_approval_mode: None, + default_tools_enabled: None, + tools: None, + links: Some(AppLinksConfig { + links: std::collections::HashMap::new(), + }), + }, + ), + ]), }) ); assert_eq!( diff --git a/codex-rs/config/src/types.rs b/codex-rs/config/src/types.rs index 8dbe34df76..853805c27d 100644 --- a/codex-rs/config/src/types.rs +++ b/codex-rs/config/src/types.rs @@ -455,6 +455,28 @@ pub struct AppToolsConfig { pub tools: HashMap, } +/// Approval settings for a connected account within an app. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema)] +#[schemars(deny_unknown_fields)] +pub struct AppLinkConfig { + /// Reviewer for approval prompts from this account, overriding the app default. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub approvals_reviewer: Option, + + /// Approval mode for this account unless a tool override exists. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub default_tools_approval_mode: Option, +} + +/// Account settings for a single app. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema)] +#[schemars(deny_unknown_fields)] +pub struct AppLinksConfig { + /// Per-account approval settings keyed by link ID. + #[serde(default, flatten)] + pub links: HashMap, +} + /// Config values for a single app/connector. #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema)] #[schemars(deny_unknown_fields)] @@ -486,6 +508,10 @@ pub struct AppConfig { /// Per-tool settings for this app. #[serde(default, skip_serializing_if = "Option::is_none")] pub tools: Option, + + /// Per-account approval settings keyed by link ID. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub links: Option, } /// App/connector settings loaded from `config.toml`. diff --git a/codex-rs/core/config.schema.json b/codex-rs/core/config.schema.json index c43aa5315e..0be6b437c3 100644 --- a/codex-rs/core/config.schema.json +++ b/codex-rs/core/config.schema.json @@ -147,6 +147,14 @@ "description": "When `false`, Codex does not surface this app.", "type": "boolean" }, + "links": { + "allOf": [ + { + "$ref": "#/definitions/AppLinksConfig" + } + ], + "description": "Per-account approval settings keyed by link ID." + }, "open_world_enabled": { "description": "Whether tools with `open_world_hint = true` are allowed for this app.", "type": "boolean" @@ -162,6 +170,36 @@ }, "type": "object" }, + "AppLinkConfig": { + "additionalProperties": false, + "description": "Approval settings for a connected account within an app.", + "properties": { + "approvals_reviewer": { + "allOf": [ + { + "$ref": "#/definitions/ApprovalsReviewer" + } + ], + "description": "Reviewer for approval prompts from this account, overriding the app default." + }, + "default_tools_approval_mode": { + "allOf": [ + { + "$ref": "#/definitions/AppToolApproval" + } + ], + "description": "Approval mode for this account unless a tool override exists." + } + }, + "type": "object" + }, + "AppLinksConfig": { + "additionalProperties": { + "$ref": "#/definitions/AppLinkConfig" + }, + "description": "Account settings for a single app.", + "type": "object" + }, "AppToolApproval": { "enum": [ "auto", diff --git a/codex-rs/core/src/mcp_tool_call_tests.rs b/codex-rs/core/src/mcp_tool_call_tests.rs index 80b0794b7a..735d83c29e 100644 --- a/codex-rs/core/src/mcp_tool_call_tests.rs +++ b/codex-rs/core/src/mcp_tool_call_tests.rs @@ -2065,6 +2065,7 @@ async fn persist_codex_app_tool_approval_writes_tool_override() { }, )]), }), + links: None, }, )]), })