Centralize app enabled-state evaluation (#36916)

## What changed

- Add `AppToolPolicyEvaluator::apply_app_enabled_state` and use it when
  presenting app lists, building plugin context, and deciding whether app
  instructions are available.
- Preserve each app's source state unless local or managed configuration
  explicitly overrides it.
- Keep connector discovery and post-install refresh checks based on raw
  accessibility rather than configured enablement.

## Testing

- Cover default enablement, per-app overrides, managed disablement, and
  preservation of unconfigured source state.

GitOrigin-RevId: f1a62d55e7cc48b37113848e3baa0d69d8d9c8a8
This commit is contained in:
jif
2026-08-04 15:34:08 +00:00
committed by copyberry
parent 2999b8e831
commit c8e255e7f8
9 changed files with 123 additions and 136 deletions

View File

@@ -4,6 +4,8 @@ use codex_config::types::AppToolApproval;
use codex_config::types::AppsConfigToml;
use serde::Deserialize;
use crate::AppInfo;
/// The effective enablement and approval policy for one app tool.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AppToolPolicy {
@@ -63,6 +65,21 @@ impl<'a> AppToolPolicyEvaluator<'a> {
.unwrap_or(true)
}
/// Applies app policy without overriding source state for unconfigured apps.
pub fn apply_app_enabled_state(&self, mut apps: Vec<AppInfo>) -> Vec<AppInfo> {
let Some(apps_config) = self.apps_config.as_ref() else {
return apps;
};
for app in &mut apps {
if apps_config.default.is_some() || apps_config.apps.contains_key(app.id.as_str()) {
app.is_enabled = self.app_enabled(app.id.as_str());
}
}
apps
}
fn from_parts(
apps_config: Option<AppsConfigToml>,
requirements_apps_config: Option<&'a AppsRequirementsToml>,

View File

@@ -189,6 +189,61 @@ fn app_enablement_uses_defaults_and_per_app_overrides() {
],
[true, false, false]
);
let evaluator = AppToolPolicyEvaluator::from_parts(
Some(apps_config),
/*requirements_apps_config*/ None,
);
assert_eq!(
evaluator.apply_app_enabled_state(vec![
app("calendar", /*enabled*/ false),
app("drive", /*enabled*/ true),
]),
vec![
app("calendar", /*enabled*/ true),
app("drive", /*enabled*/ false),
]
);
}
#[test]
fn app_enablement_preserves_source_state_and_honors_local_and_managed_overrides() {
let apps_config = AppsConfigToml {
default: None,
apps: HashMap::from([
(
"calendar".to_string(),
AppConfig {
enabled: true,
..Default::default()
},
),
(
"drive".to_string(),
AppConfig {
enabled: true,
..Default::default()
},
),
]),
};
let requirements = app_enabled_requirement("drive", /*enabled*/ false);
let evaluator = AppToolPolicyEvaluator::from_parts(Some(apps_config), Some(&requirements));
assert_eq!(
evaluator.apply_app_enabled_state(vec![
app("calendar", /*enabled*/ false),
app("drive", /*enabled*/ true),
app("slack", /*enabled*/ false),
app("gmail", /*enabled*/ true),
]),
vec![
app("calendar", /*enabled*/ true),
app("drive", /*enabled*/ false),
app("slack", /*enabled*/ false),
app("gmail", /*enabled*/ true),
]
);
}
#[test]
@@ -657,6 +712,26 @@ fn input<'a>(tool_name: &'a str, tool_title: Option<&'a str>) -> AppToolPolicyIn
}
}
fn app(id: &str, enabled: bool) -> AppInfo {
AppInfo {
id: id.to_string(),
name: id.to_string(),
description: None,
logo_url: None,
logo_url_dark: None,
icon_assets: None,
icon_dark_assets: None,
distribution_channel: None,
branding: None,
app_metadata: None,
labels: None,
install_url: None,
is_accessible: true,
is_enabled: enabled,
plugin_display_names: Vec::new(),
}
}
fn policy_from_apps_config(
apps_config: Option<&AppsConfigToml>,
connector_id: Option<&str>,