check app auth in plugin/install (#13685)

#### What
on `plugin/install`, check if installed apps are already authed on
chatgpt, and return list of all apps that are not. clients can use this
list to trigger auth workflows as needed.

checks are best effort based on `codex_apps` loading, much like
`app/list`.

#### Tests
Added integration tests, tested locally.
This commit is contained in:
sayan-oai
2026-03-05 22:45:00 -08:00
committed by GitHub
parent 4c9b1c38f6
commit 014a59fb0b
13 changed files with 715 additions and 17 deletions

View File

@@ -22,6 +22,7 @@ use codex_app_server_protocol::AccountLoginCompletedNotification;
use codex_app_server_protocol::AccountUpdatedNotification;
use codex_app_server_protocol::AppInfo;
use codex_app_server_protocol::AppListUpdatedNotification;
use codex_app_server_protocol::AppSummary;
use codex_app_server_protocol::AppsListParams;
use codex_app_server_protocol::AppsListResponse;
use codex_app_server_protocol::AskForApproval;
@@ -187,6 +188,8 @@ use codex_core::config::edit::ConfigEdit;
use codex_core::config::edit::ConfigEditsBuilder;
use codex_core::config::types::McpServerTransportConfig;
use codex_core::config_loader::CloudRequirementsLoader;
use codex_core::connectors::filter_disallowed_connectors;
use codex_core::connectors::merge_plugin_apps;
use codex_core::default_client::set_default_client_residency_requirement;
use codex_core::error::CodexErr;
use codex_core::exec::ExecParams;
@@ -203,10 +206,12 @@ use codex_core::mcp::collect_mcp_snapshot;
use codex_core::mcp::group_tools_by_server;
use codex_core::models_manager::collaboration_mode_presets::CollaborationModesConfig;
use codex_core::parse_cursor;
use codex_core::plugins::AppConnectorId;
use codex_core::plugins::MarketplaceError;
use codex_core::plugins::MarketplacePluginSourceSummary;
use codex_core::plugins::PluginInstallError as CorePluginInstallError;
use codex_core::plugins::PluginInstallRequest;
use codex_core::plugins::load_plugin_apps;
use codex_core::read_head_for_summary;
use codex_core::read_session_meta_line;
use codex_core::rollout_date_parts;
@@ -468,10 +473,14 @@ impl CodexMessageProcessor {
}
}
async fn load_latest_config(&self) -> Result<Config, JSONRPCErrorError> {
async fn load_latest_config(
&self,
fallback_cwd: Option<PathBuf>,
) -> Result<Config, JSONRPCErrorError> {
let cloud_requirements = self.current_cloud_requirements();
let mut config = codex_core::config::ConfigBuilder::default()
.cli_overrides(self.cli_overrides.clone())
.fallback_cwd(fallback_cwd)
.cloud_requirements(cloud_requirements)
.build()
.await
@@ -3913,7 +3922,7 @@ impl CodexMessageProcessor {
params: ExperimentalFeatureListParams,
) {
let ExperimentalFeatureListParams { cursor, limit } = params;
let config = match self.load_latest_config().await {
let config = match self.load_latest_config(None).await {
Ok(config) => config,
Err(error) => {
self.outgoing.send_error(request_id, error).await;
@@ -4028,7 +4037,7 @@ impl CodexMessageProcessor {
}
async fn mcp_server_refresh(&self, request_id: ConnectionRequestId, _params: Option<()>) {
let config = match self.load_latest_config().await {
let config = match self.load_latest_config(None).await {
Ok(config) => config,
Err(error) => {
self.outgoing.send_error(request_id, error).await;
@@ -4087,7 +4096,7 @@ impl CodexMessageProcessor {
request_id: ConnectionRequestId,
params: McpServerOauthLoginParams,
) {
let config = match self.load_latest_config().await {
let config = match self.load_latest_config(None).await {
Ok(config) => config,
Err(error) => {
self.outgoing.send_error(request_id, error).await;
@@ -4193,7 +4202,7 @@ impl CodexMessageProcessor {
let request = request_id.clone();
let outgoing = Arc::clone(&self.outgoing);
let config = match self.load_latest_config().await {
let config = match self.load_latest_config(None).await {
Ok(config) => config,
Err(error) => {
self.outgoing.send_error(request, error).await;
@@ -4616,7 +4625,7 @@ impl CodexMessageProcessor {
}
async fn apps_list(&self, request_id: ConnectionRequestId, params: AppsListParams) {
let mut config = match self.load_latest_config().await {
let mut config = match self.load_latest_config(None).await {
Ok(config) => config,
Err(error) => {
self.outgoing.send_error(request_id, error).await;
@@ -4847,6 +4856,36 @@ impl CodexMessageProcessor {
connectors::merge_connectors_with_accessible(all, accessible, all_connectors_loaded)
}
fn plugin_apps_needing_auth(
all_connectors: &[AppInfo],
accessible_connectors: &[AppInfo],
plugin_apps: &[AppConnectorId],
codex_apps_ready: bool,
) -> Vec<AppSummary> {
if !codex_apps_ready {
return Vec::new();
}
let accessible_ids = accessible_connectors
.iter()
.map(|connector| connector.id.as_str())
.collect::<HashSet<_>>();
let plugin_app_ids = plugin_apps
.iter()
.map(|connector_id| connector_id.0.as_str())
.collect::<HashSet<_>>();
all_connectors
.iter()
.filter(|connector| {
plugin_app_ids.contains(connector.id.as_str())
&& !accessible_ids.contains(connector.id.as_str())
})
.cloned()
.map(AppSummary::from)
.collect()
}
fn should_send_app_list_updated_notification(
connectors: &[AppInfo],
accessible_loaded: bool,
@@ -4963,7 +5002,7 @@ impl CodexMessageProcessor {
let plugins_manager = self.thread_manager.plugins_manager();
let roots = params.cwds.unwrap_or_default();
let config = match self.load_latest_config().await {
let config = match self.load_latest_config(None).await {
Ok(config) => config,
Err(err) => {
self.outgoing.send_error(request_id, err).await;
@@ -5132,6 +5171,7 @@ impl CodexMessageProcessor {
marketplace_path,
plugin_name,
} = params;
let config_cwd = marketplace_path.as_path().parent().map(Path::to_path_buf);
let plugins_manager = self.thread_manager.plugins_manager();
let request = PluginInstallRequest {
@@ -5140,11 +5180,84 @@ impl CodexMessageProcessor {
};
match plugins_manager.install_plugin(request).await {
Ok(_) => {
Ok(result) => {
let config = match self.load_latest_config(config_cwd).await {
Ok(config) => config,
Err(err) => {
warn!(
"failed to reload config after plugin install, using current config: {err:?}"
);
self.config.as_ref().clone()
}
};
let plugin_apps = load_plugin_apps(&result.installed_path);
let apps_needing_auth = if plugin_apps.is_empty()
|| !config.features.enabled(Feature::Apps)
{
Vec::new()
} else {
let (all_connectors_result, accessible_connectors_result) = tokio::join!(
connectors::list_all_connectors_with_options(&config, true),
connectors::list_accessible_connectors_from_mcp_tools_with_options_and_status(
&config, true
),
);
let all_connectors = match all_connectors_result {
Ok(connectors) => filter_disallowed_connectors(merge_plugin_apps(
connectors,
plugin_apps.clone(),
)),
Err(err) => {
warn!(
plugin = result.plugin_id.as_key(),
"failed to load app metadata after plugin install: {err:#}"
);
filter_disallowed_connectors(merge_plugin_apps(
connectors::list_cached_all_connectors(&config)
.await
.unwrap_or_default(),
plugin_apps.clone(),
))
}
};
let (accessible_connectors, codex_apps_ready) =
match accessible_connectors_result {
Ok(status) => (status.connectors, status.codex_apps_ready),
Err(err) => {
warn!(
plugin = result.plugin_id.as_key(),
"failed to load accessible apps after plugin install: {err:#}"
);
(
connectors::list_cached_accessible_connectors_from_mcp_tools(
&config,
)
.await
.unwrap_or_default(),
false,
)
}
};
if !codex_apps_ready {
warn!(
plugin = result.plugin_id.as_key(),
"codex_apps MCP not ready after plugin install; skipping appsNeedingAuth check"
);
}
Self::plugin_apps_needing_auth(
&all_connectors,
&accessible_connectors,
&plugin_apps,
codex_apps_ready,
)
};
plugins_manager.clear_cache();
self.thread_manager.skills_manager().clear_cache();
self.outgoing
.send_response(request_id, PluginInstallResponse {})
.send_response(request_id, PluginInstallResponse { apps_needing_auth })
.await;
}
Err(err) => {
@@ -7370,6 +7483,35 @@ mod tests {
validate_dynamic_tools(&tools).expect("valid schema");
}
#[test]
fn plugin_apps_needing_auth_returns_empty_when_codex_apps_is_not_ready() {
let all_connectors = vec![AppInfo {
id: "alpha".to_string(),
name: "Alpha".to_string(),
description: Some("Alpha connector".to_string()),
logo_url: None,
logo_url_dark: None,
distribution_channel: None,
branding: None,
app_metadata: None,
labels: None,
install_url: Some("https://chatgpt.com/apps/alpha/alpha".to_string()),
is_accessible: false,
is_enabled: true,
plugin_display_names: Vec::new(),
}];
assert_eq!(
CodexMessageProcessor::plugin_apps_needing_auth(
&all_connectors,
&[],
&[AppConnectorId("alpha".to_string())],
false,
),
Vec::<AppSummary>::new()
);
}
#[test]
fn collect_resume_override_mismatches_includes_service_tier() {
let request = ThreadResumeParams {