mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
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:
committed by
GitHub
Unverified
parent
6506765168
commit
d0fa2d81d8
@@ -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);
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@ use super::*;
|
||||
use crate::config::CONFIG_TOML_FILE;
|
||||
use crate::config::ConfigBuilder;
|
||||
use codex_config::AppRequirementToml;
|
||||
use codex_config::AppToolRequirementToml;
|
||||
use codex_config::AppToolsRequirementsToml;
|
||||
use codex_config::AppsRequirementsToml;
|
||||
use codex_config::CloudRequirementsLoader;
|
||||
use codex_config::ConfigLayerStack;
|
||||
@@ -359,6 +361,7 @@ fn app_tool_policy_uses_global_defaults_for_destructive_hints() {
|
||||
"events/create",
|
||||
/*tool_title*/ None,
|
||||
Some(&annotations(Some(true), /*open_world_hint*/ None)),
|
||||
/*managed_approval*/ None,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
@@ -387,6 +390,7 @@ fn app_tool_policy_defaults_missing_destructive_hint_to_true() {
|
||||
"events/create",
|
||||
/*tool_title*/ None,
|
||||
Some(&annotations(/*destructive_hint*/ None, Some(false))),
|
||||
/*managed_approval*/ None,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
@@ -415,6 +419,7 @@ fn app_tool_policy_defaults_missing_open_world_hint_to_true() {
|
||||
"events/create",
|
||||
/*tool_title*/ None,
|
||||
Some(&annotations(Some(false), /*open_world_hint*/ None)),
|
||||
/*managed_approval*/ None,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
@@ -483,6 +488,7 @@ fn requirements_disabled_connector_overrides_enabled_connector() {
|
||||
"connector_123123".to_string(),
|
||||
AppRequirementToml {
|
||||
enabled: Some(false),
|
||||
tools: None,
|
||||
},
|
||||
)]),
|
||||
};
|
||||
@@ -515,6 +521,7 @@ fn requirements_enabled_does_not_override_disabled_connector() {
|
||||
"connector_123123".to_string(),
|
||||
AppRequirementToml {
|
||||
enabled: Some(true),
|
||||
tools: None,
|
||||
},
|
||||
)]),
|
||||
};
|
||||
@@ -548,6 +555,7 @@ enabled = true
|
||||
"connector_123123".to_string(),
|
||||
AppRequirementToml {
|
||||
enabled: Some(false),
|
||||
tools: None,
|
||||
},
|
||||
)]),
|
||||
}),
|
||||
@@ -591,6 +599,7 @@ async fn cloud_requirements_disable_connector_applies_without_user_apps_table()
|
||||
"connector_123123".to_string(),
|
||||
AppRequirementToml {
|
||||
enabled: Some(false),
|
||||
tools: None,
|
||||
},
|
||||
)]),
|
||||
}),
|
||||
@@ -641,6 +650,7 @@ async fn local_requirements_disable_connector_overrides_user_apps_config() {
|
||||
"connector_123123".to_string(),
|
||||
AppRequirementToml {
|
||||
enabled: Some(false),
|
||||
tools: None,
|
||||
},
|
||||
)]),
|
||||
}),
|
||||
@@ -692,6 +702,7 @@ async fn local_requirements_disable_connector_applies_without_user_apps_table()
|
||||
"connector_123123".to_string(),
|
||||
AppRequirementToml {
|
||||
enabled: Some(false),
|
||||
tools: None,
|
||||
},
|
||||
)]),
|
||||
}),
|
||||
@@ -733,6 +744,7 @@ async fn with_app_enabled_state_preserves_unrelated_disabled_connector() {
|
||||
"connector_drive".to_string(),
|
||||
AppRequirementToml {
|
||||
enabled: Some(false),
|
||||
tools: None,
|
||||
},
|
||||
)]),
|
||||
}),
|
||||
@@ -773,6 +785,7 @@ fn app_tool_policy_honors_default_app_enabled_false() {
|
||||
Some(&annotations(
|
||||
/*destructive_hint*/ None, /*open_world_hint*/ None,
|
||||
)),
|
||||
/*managed_approval*/ None,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
@@ -784,6 +797,210 @@ fn app_tool_policy_honors_default_app_enabled_false() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn app_tool_policy_uses_managed_approval_without_apps_config() {
|
||||
let policy = app_tool_policy_from_apps_config(
|
||||
/*apps_config*/ None,
|
||||
Some("calendar"),
|
||||
"events/list",
|
||||
/*tool_title*/ None,
|
||||
/*annotations*/ None,
|
||||
Some(AppToolApproval::Approve),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
policy,
|
||||
AppToolPolicy {
|
||||
enabled: true,
|
||||
approval: AppToolApproval::Approve,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
fn app_tool_requirements(
|
||||
app_id: &str,
|
||||
tool_name: &str,
|
||||
approval_mode: AppToolApproval,
|
||||
) -> AppsRequirementsToml {
|
||||
AppsRequirementsToml {
|
||||
apps: BTreeMap::from([(
|
||||
app_id.to_string(),
|
||||
AppRequirementToml {
|
||||
enabled: None,
|
||||
tools: Some(AppToolsRequirementsToml {
|
||||
tools: BTreeMap::from([(
|
||||
tool_name.to_string(),
|
||||
AppToolRequirementToml {
|
||||
approval_mode: Some(approval_mode),
|
||||
},
|
||||
)]),
|
||||
}),
|
||||
},
|
||||
)]),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn managed_app_tool_approval_uses_raw_tool_name() {
|
||||
let requirements_apps = app_tool_requirements(
|
||||
"connector_123123",
|
||||
"calendar/list_events",
|
||||
AppToolApproval::Approve,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
managed_app_tool_approval(
|
||||
Some(&requirements_apps),
|
||||
Some("connector_123123"),
|
||||
"calendar/list_events",
|
||||
),
|
||||
Some(AppToolApproval::Approve)
|
||||
);
|
||||
assert_eq!(
|
||||
managed_app_tool_approval(
|
||||
Some(&requirements_apps),
|
||||
Some("connector_123123"),
|
||||
"calendar/create_event",
|
||||
),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn cloud_requirements_tool_approval_overrides_user_apps_config() {
|
||||
let codex_home = tempdir().expect("tempdir should succeed");
|
||||
std::fs::write(
|
||||
codex_home.path().join(CONFIG_TOML_FILE),
|
||||
r#"
|
||||
[apps.connector_123123.tools."calendar/list_events"]
|
||||
approval_mode = "prompt"
|
||||
"#,
|
||||
)
|
||||
.expect("write config");
|
||||
|
||||
let requirements = ConfigRequirementsToml {
|
||||
apps: Some(app_tool_requirements(
|
||||
"connector_123123",
|
||||
"calendar/list_events",
|
||||
AppToolApproval::Approve,
|
||||
)),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let config = ConfigBuilder::default()
|
||||
.codex_home(codex_home.path().to_path_buf())
|
||||
.fallback_cwd(Some(codex_home.path().to_path_buf()))
|
||||
.cloud_requirements(CloudRequirementsLoader::new(async move {
|
||||
Ok(Some(requirements))
|
||||
}))
|
||||
.build()
|
||||
.await
|
||||
.expect("config should build");
|
||||
|
||||
let policy = app_tool_policy(
|
||||
&config,
|
||||
Some("connector_123123"),
|
||||
"calendar/list_events",
|
||||
/*tool_title*/ None,
|
||||
/*annotations*/ None,
|
||||
);
|
||||
assert_eq!(
|
||||
policy,
|
||||
AppToolPolicy {
|
||||
enabled: true,
|
||||
approval: AppToolApproval::Approve,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn local_requirements_tool_approval_overrides_user_apps_config() {
|
||||
let codex_home = tempdir().expect("tempdir should succeed");
|
||||
let config_toml_path =
|
||||
AbsolutePathBuf::try_from(codex_home.path().join(CONFIG_TOML_FILE)).expect("abs path");
|
||||
let mut config = ConfigBuilder::default()
|
||||
.codex_home(codex_home.path().to_path_buf())
|
||||
.fallback_cwd(Some(codex_home.path().to_path_buf()))
|
||||
.build()
|
||||
.await
|
||||
.expect("config should build");
|
||||
|
||||
let requirements = ConfigRequirementsToml {
|
||||
apps: Some(app_tool_requirements(
|
||||
"connector_123123",
|
||||
"calendar/list_events",
|
||||
AppToolApproval::Approve,
|
||||
)),
|
||||
..Default::default()
|
||||
};
|
||||
config.config_layer_stack =
|
||||
ConfigLayerStack::new(Vec::new(), ConfigRequirements::default(), requirements)
|
||||
.expect("requirements stack")
|
||||
.with_user_config(
|
||||
&config_toml_path,
|
||||
toml::from_str::<toml::Value>(
|
||||
r#"
|
||||
[apps.connector_123123.tools."calendar/list_events"]
|
||||
approval_mode = "prompt"
|
||||
"#,
|
||||
)
|
||||
.expect("apps config"),
|
||||
);
|
||||
|
||||
let policy = app_tool_policy(
|
||||
&config,
|
||||
Some("connector_123123"),
|
||||
"calendar/list_events",
|
||||
/*tool_title*/ None,
|
||||
/*annotations*/ None,
|
||||
);
|
||||
assert_eq!(
|
||||
policy,
|
||||
AppToolPolicy {
|
||||
enabled: true,
|
||||
approval: AppToolApproval::Approve,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn local_requirements_tool_approval_does_not_match_tool_title() {
|
||||
let codex_home = tempdir().expect("tempdir should succeed");
|
||||
let mut config = ConfigBuilder::default()
|
||||
.codex_home(codex_home.path().to_path_buf())
|
||||
.fallback_cwd(Some(codex_home.path().to_path_buf()))
|
||||
.build()
|
||||
.await
|
||||
.expect("config should build");
|
||||
|
||||
let requirements = ConfigRequirementsToml {
|
||||
apps: Some(app_tool_requirements(
|
||||
"connector_123123",
|
||||
"calendar/list_events",
|
||||
AppToolApproval::Approve,
|
||||
)),
|
||||
..Default::default()
|
||||
};
|
||||
config.config_layer_stack =
|
||||
ConfigLayerStack::new(Vec::new(), ConfigRequirements::default(), requirements)
|
||||
.expect("requirements stack");
|
||||
|
||||
let policy = app_tool_policy(
|
||||
&config,
|
||||
Some("connector_123123"),
|
||||
"calendar/create_event",
|
||||
Some("calendar/list_events"),
|
||||
/*annotations*/ None,
|
||||
);
|
||||
assert_eq!(
|
||||
policy,
|
||||
AppToolPolicy {
|
||||
enabled: true,
|
||||
approval: AppToolApproval::Auto,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn app_tool_policy_allows_per_app_enable_when_default_is_disabled() {
|
||||
let apps_config = AppsConfigToml {
|
||||
@@ -813,6 +1030,7 @@ fn app_tool_policy_allows_per_app_enable_when_default_is_disabled() {
|
||||
Some(&annotations(
|
||||
/*destructive_hint*/ None, /*open_world_hint*/ None,
|
||||
)),
|
||||
/*managed_approval*/ None,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
@@ -855,6 +1073,7 @@ fn app_tool_policy_per_tool_enabled_true_overrides_app_level_disable_flags() {
|
||||
"events/create",
|
||||
/*tool_title*/ None,
|
||||
Some(&annotations(Some(true), Some(true))),
|
||||
/*managed_approval*/ None,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
@@ -889,6 +1108,7 @@ fn app_tool_policy_default_tools_enabled_true_overrides_app_level_tool_hints() {
|
||||
"events/create",
|
||||
/*tool_title*/ None,
|
||||
Some(&annotations(Some(true), Some(true))),
|
||||
/*managed_approval*/ None,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
@@ -925,6 +1145,7 @@ fn app_tool_policy_default_tools_enabled_false_overrides_app_level_tool_hints()
|
||||
Some(&annotations(
|
||||
/*destructive_hint*/ None, /*open_world_hint*/ None,
|
||||
)),
|
||||
/*managed_approval*/ None,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
@@ -963,6 +1184,7 @@ fn app_tool_policy_uses_default_tools_approval_mode() {
|
||||
Some(&annotations(
|
||||
/*destructive_hint*/ None, /*open_world_hint*/ None,
|
||||
)),
|
||||
/*managed_approval*/ None,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
@@ -1005,6 +1227,7 @@ fn app_tool_policy_matches_prefix_stripped_tool_name_for_tool_config() {
|
||||
"calendar_events/create",
|
||||
Some("events/create"),
|
||||
Some(&annotations(Some(true), Some(true))),
|
||||
/*managed_approval*/ None,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
|
||||
Reference in New Issue
Block a user