mirror of
https://github.com/openai/codex.git
synced 2026-10-27 08:01:52 +00:00
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:
@@ -10,7 +10,6 @@ pub use codex_connectors::AppInfo;
|
||||
pub use codex_connectors::AppMetadata;
|
||||
use codex_connectors::ConnectorDirectoryCacheContext;
|
||||
use codex_connectors::ConnectorDirectoryCacheKey;
|
||||
use codex_connectors::app_is_enabled;
|
||||
use codex_connectors::apps_config_from_layer_stack;
|
||||
use codex_connectors::connector_runtime_context_key;
|
||||
use codex_exec_server::EnvironmentManager;
|
||||
@@ -490,32 +489,6 @@ fn accessible_connectors_for_app_list_from_mcp_tools(mcp_tools: &[ToolInfo]) ->
|
||||
collect_accessible_connectors_from_mcp_tools(non_synthetic_tools)
|
||||
}
|
||||
|
||||
pub fn with_app_enabled_state(mut connectors: Vec<AppInfo>, config: &Config) -> Vec<AppInfo> {
|
||||
let user_apps_config = apps_config_from_layer_stack(&config.config_layer_stack);
|
||||
let requirements_apps_config = config.config_layer_stack.requirements_toml().apps.as_ref();
|
||||
if user_apps_config.is_none() && requirements_apps_config.is_none() {
|
||||
return connectors;
|
||||
}
|
||||
|
||||
for connector in &mut connectors {
|
||||
if let Some(apps_config) = user_apps_config.as_ref()
|
||||
&& (apps_config.default.is_some()
|
||||
|| apps_config.apps.contains_key(connector.id.as_str()))
|
||||
{
|
||||
connector.is_enabled = app_is_enabled(apps_config, Some(connector.id.as_str()));
|
||||
}
|
||||
|
||||
if requirements_apps_config
|
||||
.and_then(|apps| apps.apps.get(connector.id.as_str()))
|
||||
.is_some_and(|app| app.enabled == Some(false))
|
||||
{
|
||||
connector.is_enabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
connectors
|
||||
}
|
||||
|
||||
pub fn with_app_plugin_sources(
|
||||
mut connectors: Vec<AppInfo>,
|
||||
tool_plugin_provenance: &ToolPluginProvenance,
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
use super::*;
|
||||
use crate::config::CONFIG_TOML_FILE;
|
||||
use crate::config::ConfigBuilder;
|
||||
use codex_config::AppRequirementToml;
|
||||
use codex_config::AppsRequirementsToml;
|
||||
use codex_config::ConfigLayerStack;
|
||||
use codex_config::ConfigRequirements;
|
||||
use codex_config::ConfigRequirementsToml;
|
||||
use codex_config::test_support::CloudConfigBundleFixture;
|
||||
use codex_config::types::ApprovalsReviewer;
|
||||
use codex_connectors::merge::plugin_connector_to_app_info;
|
||||
@@ -19,31 +14,10 @@ use pretty_assertions::assert_eq;
|
||||
use rmcp::model::JsonObject;
|
||||
use rmcp::model::MetaObject;
|
||||
use rmcp::model::Tool;
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::HashSet;
|
||||
use std::sync::Arc;
|
||||
use tempfile::tempdir;
|
||||
|
||||
fn app(id: &str) -> 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,
|
||||
install_url: None,
|
||||
branding: None,
|
||||
app_metadata: None,
|
||||
labels: None,
|
||||
is_accessible: false,
|
||||
is_enabled: true,
|
||||
plugin_display_names: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn plugin_names(names: &[&str]) -> Vec<String> {
|
||||
names.iter().map(ToString::to_string).collect()
|
||||
}
|
||||
@@ -452,44 +426,6 @@ approvals_reviewer = "user"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn with_app_enabled_state_preserves_unrelated_disabled_connector() {
|
||||
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(AppsRequirementsToml {
|
||||
apps: BTreeMap::from([(
|
||||
"connector_drive".to_string(),
|
||||
AppRequirementToml {
|
||||
enabled: Some(false),
|
||||
tools: None,
|
||||
},
|
||||
)]),
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
config.config_layer_stack =
|
||||
ConfigLayerStack::new(Vec::new(), ConfigRequirements::default(), requirements)
|
||||
.expect("requirements stack");
|
||||
|
||||
let mut slack = app("connector_slack");
|
||||
slack.is_enabled = false;
|
||||
|
||||
let mut drive = app("connector_drive");
|
||||
drive.is_enabled = false;
|
||||
|
||||
assert_eq!(
|
||||
with_app_enabled_state(vec![slack.clone(), app("connector_drive")], &config),
|
||||
vec![slack, drive]
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn tool_suggest_connector_ids_include_configured_tool_suggest_discoverables() {
|
||||
let codex_home = tempdir().expect("tempdir should succeed");
|
||||
|
||||
@@ -71,6 +71,7 @@ use codex_analytics::InvocationType;
|
||||
use codex_analytics::TurnResolvedConfigFact;
|
||||
use codex_analytics::build_track_events_context;
|
||||
use codex_async_utils::OrCancelExt;
|
||||
use codex_connectors::AppToolPolicyEvaluator;
|
||||
use codex_core_plugins::RecommendedPluginCandidatesInput;
|
||||
use codex_core_skills::injection::InjectedHostSkillPrompts;
|
||||
use codex_extension_api::ExtensionData;
|
||||
@@ -754,7 +755,8 @@ async fn build_skills_and_plugins(
|
||||
.map(|connector_id| connector_id.0.clone()),
|
||||
connectors::accessible_connectors_from_mcp_tools(mcp_tools),
|
||||
);
|
||||
connectors::with_app_enabled_state(connectors, &turn_context.config)
|
||||
AppToolPolicyEvaluator::new(&turn_context.config.config_layer_stack)
|
||||
.apply_app_enabled_state(connectors)
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
@@ -1476,21 +1478,6 @@ pub(crate) async fn built_tools(
|
||||
let apps_enabled = turn_context.apps_enabled();
|
||||
let accessible_connectors =
|
||||
apps_enabled.then(|| connectors::accessible_connectors_from_mcp_tools(all_mcp_tools));
|
||||
let connectors = if apps_enabled {
|
||||
let connectors = codex_connectors::merge::merge_plugin_connectors_with_accessible(
|
||||
connector_snapshot
|
||||
.connector_ids()
|
||||
.iter()
|
||||
.map(|connector_id| connector_id.0.clone()),
|
||||
accessible_connectors.clone().unwrap_or_default(),
|
||||
);
|
||||
Some(connectors::with_app_enabled_state(
|
||||
connectors,
|
||||
&turn_context.config,
|
||||
))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let tool_suggest_is_enabled = tool_suggest_enabled(turn_context);
|
||||
let PreparedToolRecommendations {
|
||||
auth,
|
||||
@@ -1510,7 +1497,7 @@ pub(crate) async fn built_tools(
|
||||
.collect::<Vec<_>>();
|
||||
async {
|
||||
if apps_enabled && tool_suggest_is_enabled {
|
||||
if let Some(accessible_connectors) = connectors.as_ref() {
|
||||
if let Some(accessible_connectors) = accessible_connectors.as_ref() {
|
||||
match connectors::list_tool_suggest_discoverable_tools_with_auth(
|
||||
&turn_context.config,
|
||||
sess.services.plugins_manager.as_ref(),
|
||||
|
||||
@@ -19,6 +19,7 @@ use crate::context::world_state::PluginsInstructionsState;
|
||||
use crate::context::world_state::RealtimeState;
|
||||
use crate::context::world_state::ToolsState;
|
||||
use crate::context::world_state::WorldState;
|
||||
use codex_connectors::AppToolPolicyEvaluator;
|
||||
use codex_extension_api::WorldStateContributionInput;
|
||||
use codex_features::Feature;
|
||||
use codex_protocol::error::CodexErr;
|
||||
@@ -178,12 +179,12 @@ impl Session {
|
||||
));
|
||||
let apps_available =
|
||||
if turn_context.config.include_apps_instructions && turn_context.apps_enabled() {
|
||||
connectors::with_app_enabled_state(
|
||||
connectors::accessible_connectors_from_mcp_tools(step_context.mcp.tools()),
|
||||
&turn_context.config,
|
||||
)
|
||||
.into_iter()
|
||||
.any(|connector| connector.is_accessible && connector.is_enabled)
|
||||
AppToolPolicyEvaluator::new(&turn_context.config.config_layer_stack)
|
||||
.apply_app_enabled_state(connectors::accessible_connectors_from_mcp_tools(
|
||||
step_context.mcp.tools(),
|
||||
))
|
||||
.into_iter()
|
||||
.any(|connector| connector.is_accessible && connector.is_enabled)
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
@@ -456,20 +456,15 @@ async fn refresh_missing_requested_connectors(
|
||||
}
|
||||
|
||||
let mcp_tools = mcp.tools();
|
||||
let accessible_connectors = connectors::with_app_enabled_state(
|
||||
connectors::accessible_connectors_from_mcp_tools(mcp_tools),
|
||||
&turn.config,
|
||||
);
|
||||
let accessible_connectors = connectors::accessible_connectors_from_mcp_tools(mcp_tools);
|
||||
if all_requested_connectors_picked_up(expected_connector_ids, &accessible_connectors) {
|
||||
return Some(accessible_connectors);
|
||||
}
|
||||
|
||||
match session.hard_refresh_latest_codex_apps_tools().await {
|
||||
Ok(mcp_tools) => {
|
||||
let accessible_connectors = connectors::with_app_enabled_state(
|
||||
connectors::accessible_connectors_from_mcp_tools(&mcp_tools),
|
||||
&turn.config,
|
||||
);
|
||||
let accessible_connectors =
|
||||
connectors::accessible_connectors_from_mcp_tools(&mcp_tools);
|
||||
connectors::refresh_accessible_connectors_cache_from_mcp_tools(
|
||||
&turn.config,
|
||||
auth,
|
||||
|
||||
Reference in New Issue
Block a user