feat(connectors): support managed app tool approval requirements (#21061)

## Why

Managed requirements can already centrally disable apps, but they could
not express the per-tool app approval rules that normal config already
supports. That left admins without a way to enforce connector tool
approvals through `/etc/codex/requirements.toml` or cloud requirements.

## What changed

- Extend app requirements with per-tool `approval_mode` entries.
- Merge managed app tool requirements across managed sources while
preserving higher-precedence exact tool settings.
- Apply managed tool approvals separately from user app config so
managed policy is matched only on raw MCP `tool.name`, while user config
keeps the existing raw-name-then-title convenience fallback.
- Add coverage for local requirements, cloud requirements parsing,
managed-over-user precedence, and a title-collision case that must not
widen managed auto-approval.

## Configuration shape

Local `/etc/codex/requirements.toml` and cloud requirements use the same
TOML shape:

```toml
[apps.connector_123123.tools."calendar/list_events"]
approval_mode = "approve"
```

This is a per-tool approval rule keyed by app ID and raw MCP tool name,
not an app-level boolean such as `apps.connector_123123.approve = true`.
This commit is contained in:
viyatb-oai authored and GitHub committed 2026-05-11 19:08:26 +00:00
1 parent 6506765168
commit d0fa2d81d8
6 files changed
+460 -24

No files matched your search

+32 -7
View File
@@ -535,12 +535,18 @@ pub(crate) fn app_tool_policy(
annotations: Option<&ToolAnnotations>,
) -> AppToolPolicy {
let apps_config = read_apps_config(config);
let managed_approval = managed_app_tool_approval(
config.config_layer_stack.requirements_toml().apps.as_ref(),
connector_id,
tool_name,
);
app_tool_policy_from_apps_config(
apps_config.as_ref(),
connector_id,
tool_name,
tool_title,
annotations,
managed_approval,
)
}
@@ -593,14 +599,29 @@ fn apply_requirements_apps_constraints(
};
for (app_id, requirement) in &requirements_apps_config.apps {
if requirement.enabled != Some(false) {
continue;
if requirement.enabled == Some(false) {
let app = apps_config.apps.entry(app_id.clone()).or_default();
app.enabled = false;
}
let app = apps_config.apps.entry(app_id.clone()).or_default();
app.enabled = false;
}
}
fn managed_app_tool_approval(
requirements_apps_config: Option<&AppsRequirementsToml>,
connector_id: Option<&str>,
tool_name: &str,
) -> Option<AppToolApproval> {
let connector_id = connector_id?;
requirements_apps_config?
.apps
.get(connector_id)?
.tools
.as_ref()?
.tools
.get(tool_name)?
.approval_mode
}
fn app_is_enabled(apps_config: &AppsConfigToml, connector_id: Option<&str>) -> bool {
let default_enabled = apps_config
.default
@@ -620,9 +641,13 @@ fn app_tool_policy_from_apps_config(
tool_name: &str,
tool_title: Option<&str>,
annotations: Option<&ToolAnnotations>,
managed_approval: Option<AppToolApproval>,
) -> AppToolPolicy {
let Some(apps_config) = apps_config else {
return AppToolPolicy::default();
return AppToolPolicy {
approval: managed_approval.unwrap_or(AppToolApproval::Auto),
..Default::default()
};
};
let app = connector_id.and_then(|connector_id| apps_config.apps.get(connector_id));
@@ -633,8 +658,8 @@ fn app_tool_policy_from_apps_config(
.get(tool_name)
.or_else(|| tool_title.and_then(|title| tools.tools.get(title)))
});
let approval = tool_config
.and_then(|tool| tool.approval_mode)
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))
.unwrap_or(AppToolApproval::Auto);