mirror of
https://github.com/openai/codex.git
synced 2026-10-28 18:58:46 +00:00
Add per-account approval settings for apps (#42047)
## What changed - Add `apps.<app_id>.links.<link_id>` configuration for overriding `approvals_reviewer` and `default_tools_approval_mode` per connected account. - Expose link settings through the app-server protocol and generated JSON and TypeScript schemas. ## Testing - Extend `config/read` coverage for populated, empty, and absent `links` sections. GitOrigin-RevId: 43004d2722439060fdadc5e5cd9ed3108774ecaf
This commit is contained in:
committed by
copyberry
parent
90ae0c4ef9
commit
0ec375eb70
+41
@@ -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.",
|
||||
|
||||
+41
@@ -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.",
|
||||
|
||||
@@ -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",
|
||||
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -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, } };
|
||||
@@ -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, } });
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -209,6 +209,24 @@ pub struct AppToolsConfig {
|
||||
pub tools: HashMap<String, AppToolConfig>,
|
||||
}
|
||||
|
||||
/// 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<ApprovalsReviewer>,
|
||||
pub default_tools_approval_mode: Option<AppToolApproval>,
|
||||
}
|
||||
|
||||
/// 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<String, AppLinkConfig>,
|
||||
}
|
||||
|
||||
#[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<AppToolApproval>,
|
||||
pub default_tools_enabled: Option<bool>,
|
||||
pub tools: Option<AppToolsConfig>,
|
||||
/// Per-account approval settings keyed by link ID.
|
||||
pub links: Option<AppLinksConfig>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
|
||||
@@ -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.<app_id>.links.<link_id>` 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
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
)]),
|
||||
})
|
||||
|
||||
@@ -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!(
|
||||
|
||||
@@ -455,6 +455,28 @@ pub struct AppToolsConfig {
|
||||
pub tools: HashMap<String, AppToolConfig>,
|
||||
}
|
||||
|
||||
/// 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<ApprovalsReviewer>,
|
||||
|
||||
/// Approval mode for this account unless a tool override exists.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub default_tools_approval_mode: Option<AppToolApproval>,
|
||||
}
|
||||
|
||||
/// 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<String, AppLinkConfig>,
|
||||
}
|
||||
|
||||
/// 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<AppToolsConfig>,
|
||||
|
||||
/// Per-account approval settings keyed by link ID.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub links: Option<AppLinksConfig>,
|
||||
}
|
||||
|
||||
/// App/connector settings loaded from `config.toml`.
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -2065,6 +2065,7 @@ async fn persist_codex_app_tool_approval_writes_tool_override() {
|
||||
},
|
||||
)]),
|
||||
}),
|
||||
links: None,
|
||||
},
|
||||
)]),
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user