Skip to content

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions codex-rs/app-server-protocol/src/protocol/v2/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<AppToolApproval>,
}

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
Expand Down
9 changes: 9 additions & 0 deletions codex-rs/app-server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1824,15 +1824,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 `$<app-slug>` 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://<connector-id>` path rather than guessing by name. Plugins use the same `mention` item shape, but with `plugin://<plugin-name>@<marketplace-name>` paths from `plugin/installed` or `plugin/list`.

Example:
Expand Down
12 changes: 12 additions & 0 deletions codex-rs/app-server/tests/suite/v2/config_rpc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(),
Expand All @@ -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 {
Expand Down
4 changes: 4 additions & 0 deletions codex-rs/config/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<AppToolApproval>,
}

/// Per-tool settings for a single app tool.
Expand Down
6 changes: 6 additions & 0 deletions codex-rs/connectors/src/app_tool_policy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(|| {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we avoid applying apps._default when connector_id is missing? Execution re-looks metadata up from the mutable Apps cache, so an already-registered handler can lose its connector after a hard refresh

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

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) {
Expand Down
55 changes: 53 additions & 2 deletions codex-rs/connectors/src/app_tool_policy_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -720,5 +770,6 @@ fn defaults(
approvals_reviewer: None,
destructive_enabled,
open_world_enabled,
default_tools_approval_mode: None,
}
}
10 changes: 9 additions & 1 deletion codex-rs/core/config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -5274,4 +5282,4 @@
},
"title": "ConfigToml",
"type": "object"
}
}
11 changes: 5 additions & 6 deletions codex-rs/core/tests/suite/mcp_turn_metadata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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}"
"#
))
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand Down
Loading