mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
c78911e37f
[from codex] ## Summary - add `default_tools_approval_mode` to `[apps._default]` and expose it through app-server v2 `config/read` - apply it after managed, per-tool, and per-app approval settings, before the built-in `auto` fallback - document the precedence, regenerate config/app-server schemas, and add unit plus end-to-end approval coverage ## Configuration ```toml [apps._default] default_tools_approval_mode = "prompt" ``` The effective precedence is managed requirements, tool-specific `approval_mode`, app-specific `default_tools_approval_mode`, `apps._default.default_tools_approval_mode`, then `auto`. ## Test plan - `just write-config-schema` - `just write-app-server-schema` - `just write-app-server-schema --experimental` - `just test -p codex-core app_tool_policy` - `just test -p codex-core mcp_turn_metadata` - `just test -p codex-config` - `just test -p codex-app-server-protocol` - `just test -p codex-app-server config_read_includes_apps` - `just fix -p codex-config -p codex-core -p codex-app-server-protocol -p codex-app-server` - `just fmt`
214 lines
6.7 KiB
Rust
214 lines
6.7 KiB
Rust
use codex_config::AppsRequirementsToml;
|
|
use codex_config::ConfigLayerStack;
|
|
use codex_config::types::AppToolApproval;
|
|
use codex_config::types::AppsConfigToml;
|
|
use serde::Deserialize;
|
|
|
|
/// The effective enablement and approval policy for one app tool.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub struct AppToolPolicy {
|
|
pub enabled: bool,
|
|
pub approval: AppToolApproval,
|
|
}
|
|
|
|
impl Default for AppToolPolicy {
|
|
fn default() -> Self {
|
|
Self {
|
|
enabled: true,
|
|
approval: AppToolApproval::Auto,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Connector-owned metadata used to evaluate one app tool.
|
|
#[derive(Debug, Clone, Copy)]
|
|
pub struct AppToolPolicyInput<'a> {
|
|
pub connector_id: Option<&'a str>,
|
|
pub tool_name: &'a str,
|
|
pub tool_title: Option<&'a str>,
|
|
pub destructive_hint: Option<bool>,
|
|
pub open_world_hint: Option<bool>,
|
|
}
|
|
|
|
/// Resolves app tool policy against one immutable config snapshot.
|
|
///
|
|
/// Callers should construct one evaluator and reuse it for every tool in the
|
|
/// same exposure build so config layers are merged and decoded only once.
|
|
pub struct AppToolPolicyEvaluator<'a> {
|
|
apps_config: Option<AppsConfigToml>,
|
|
requirements_apps_config: Option<&'a AppsRequirementsToml>,
|
|
}
|
|
|
|
impl<'a> AppToolPolicyEvaluator<'a> {
|
|
pub fn new(config_layer_stack: &'a ConfigLayerStack) -> Self {
|
|
let apps_config = apps_config_from_layer_stack(config_layer_stack);
|
|
let requirements_apps_config = config_layer_stack.requirements_toml().apps.as_ref();
|
|
Self::from_parts(apps_config, requirements_apps_config)
|
|
}
|
|
|
|
pub fn policy(&self, input: AppToolPolicyInput<'_>) -> AppToolPolicy {
|
|
let managed_approval = managed_app_tool_approval(
|
|
self.requirements_apps_config,
|
|
input.connector_id,
|
|
input.tool_name,
|
|
);
|
|
app_tool_policy_from_apps_config(self.apps_config.as_ref(), input, managed_approval)
|
|
}
|
|
|
|
fn from_parts(
|
|
apps_config: Option<AppsConfigToml>,
|
|
requirements_apps_config: Option<&'a AppsRequirementsToml>,
|
|
) -> Self {
|
|
Self {
|
|
apps_config: effective_apps_config(apps_config, requirements_apps_config),
|
|
requirements_apps_config,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Reads the merged, unmanaged Apps configuration from a config-layer stack.
|
|
pub fn apps_config_from_layer_stack(
|
|
config_layer_stack: &ConfigLayerStack,
|
|
) -> Option<AppsConfigToml> {
|
|
config_layer_stack
|
|
.effective_config()
|
|
.as_table()
|
|
.and_then(|table| table.get("apps"))
|
|
.cloned()
|
|
.and_then(|value| AppsConfigToml::deserialize(value).ok())
|
|
}
|
|
|
|
pub fn app_is_enabled(apps_config: &AppsConfigToml, connector_id: Option<&str>) -> bool {
|
|
let default_enabled = apps_config
|
|
.default
|
|
.as_ref()
|
|
.map(|defaults| defaults.enabled)
|
|
.unwrap_or(true);
|
|
|
|
connector_id
|
|
.and_then(|connector_id| apps_config.apps.get(connector_id))
|
|
.map(|app| app.enabled)
|
|
.unwrap_or(default_enabled)
|
|
}
|
|
|
|
fn effective_apps_config(
|
|
apps_config: Option<AppsConfigToml>,
|
|
requirements_apps_config: Option<&AppsRequirementsToml>,
|
|
) -> Option<AppsConfigToml> {
|
|
let had_apps_config = apps_config.is_some();
|
|
let mut apps_config = apps_config.unwrap_or_default();
|
|
apply_requirements_apps_constraints(&mut apps_config, requirements_apps_config);
|
|
if had_apps_config || apps_config.default.is_some() || !apps_config.apps.is_empty() {
|
|
Some(apps_config)
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
fn apply_requirements_apps_constraints(
|
|
apps_config: &mut AppsConfigToml,
|
|
requirements_apps_config: Option<&AppsRequirementsToml>,
|
|
) {
|
|
let Some(requirements_apps_config) = requirements_apps_config else {
|
|
return;
|
|
};
|
|
|
|
for (app_id, requirement) in &requirements_apps_config.apps {
|
|
if requirement.enabled == Some(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_tool_policy_from_apps_config(
|
|
apps_config: Option<&AppsConfigToml>,
|
|
input: AppToolPolicyInput<'_>,
|
|
managed_approval: Option<AppToolApproval>,
|
|
) -> AppToolPolicy {
|
|
let Some(apps_config) = apps_config else {
|
|
return AppToolPolicy {
|
|
approval: managed_approval.unwrap_or(AppToolApproval::Auto),
|
|
..Default::default()
|
|
};
|
|
};
|
|
|
|
let app = input
|
|
.connector_id
|
|
.and_then(|connector_id| apps_config.apps.get(connector_id));
|
|
let tools = app.and_then(|app| app.tools.as_ref());
|
|
let tool_config = tools.and_then(|tools| {
|
|
tools
|
|
.tools
|
|
.get(input.tool_name)
|
|
.or_else(|| input.tool_title.and_then(|title| tools.tools.get(title)))
|
|
});
|
|
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(|| {
|
|
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) {
|
|
return AppToolPolicy {
|
|
enabled: false,
|
|
approval,
|
|
};
|
|
}
|
|
|
|
if let Some(enabled) = tool_config.and_then(|tool| tool.enabled) {
|
|
return AppToolPolicy { enabled, approval };
|
|
}
|
|
|
|
if let Some(enabled) = app.and_then(|app| app.default_tools_enabled) {
|
|
return AppToolPolicy { enabled, approval };
|
|
}
|
|
|
|
let app_defaults = apps_config.default.as_ref();
|
|
let destructive_enabled = app
|
|
.and_then(|app| app.destructive_enabled)
|
|
.unwrap_or_else(|| {
|
|
app_defaults
|
|
.map(|defaults| defaults.destructive_enabled)
|
|
.unwrap_or(true)
|
|
});
|
|
let open_world_enabled = app
|
|
.and_then(|app| app.open_world_enabled)
|
|
.unwrap_or_else(|| {
|
|
app_defaults
|
|
.map(|defaults| defaults.open_world_enabled)
|
|
.unwrap_or(true)
|
|
});
|
|
let destructive_hint = input.destructive_hint.unwrap_or(true);
|
|
let open_world_hint = input.open_world_hint.unwrap_or(true);
|
|
let enabled =
|
|
(destructive_enabled || !destructive_hint) && (open_world_enabled || !open_world_hint);
|
|
|
|
AppToolPolicy { enabled, approval }
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[path = "app_tool_policy_tests.rs"]
|
|
mod tests;
|