[codex] Make plugin details capability aware (#27958)

## Summary

Makes plugin details/read flows capability-aware so auth-filtered plugin
surfaces report the same usable app/MCP/skill shape as the marketplace
and install flows.

## Validation

Not run; this change was rebased onto the current plugin auth stack and
pushed as a draft PR.

**Manual test**
1. set up a local marketplace with a plugin that has both app and mcp
declarations

```
// .app.json
{
  "apps": {
    "linear": {
      "id": "some_id"
    }
  }
}

```

```
// .mcp.json
{
  "mcpServers": {
    "linear": {
      "type": "http",
      "url": "https://mcp.linear.app/mcp",
      "oauth_resource": "https://mcp.linear.app/mcp"
    },
    "linear2": {
      "type": "http",
      "url": "https://mcp.linear2.app/mcp",
      "oauth_resource": "https://mcp.linear2.app/mcp"
    }
  }
}
```

2a. **login in with api key** and observe plugin details page which
shows no apps (note we don't show "app not available due to api key log
in as there's no way to differentiate between no apps and app without
substitute mcp exists" without significantly more code changes, i've
separated this to a follow up if we want that behaviour.
<img width="1170" height="279" alt="Screenshot 2026-06-15 at 23 45 40"
src="https://github.com/user-attachments/assets/d36cb160-fbec-461e-9643-9c761dbae7bb"
/>
<img width="975" height="640" alt="Screenshot 2026-06-15 at 18 40 30"
src="https://github.com/user-attachments/assets/90ec0bc8-7506-4b90-bbd3-070720de799e"
/>


2b. **log in with chat** and observe intended conflict resolution logic
<img width="1165" height="224" alt="Screenshot 2026-06-15 at 17 17 30"
src="https://github.com/user-attachments/assets/80adfbf2-7dac-4f08-8b76-8eeeab6c95e7"
/>
<img width="968" height="567" alt="Screenshot 2026-06-15 at 18 38 59"
src="https://github.com/user-attachments/assets/9ea92c5e-535b-4aa4-8ad0-ee513b57bc3c"
/>
This commit is contained in:
felixxia-oai
2026-06-16 01:25:22 +00:00
committed by GitHub
parent 02dce8eb8d
commit d959664420
5 changed files with 204 additions and 11 deletions
+12 -5
View File
@@ -1314,7 +1314,17 @@ impl PluginsManager {
event_name: hook.event_name,
})
.collect();
let app_declarations = load_plugin_apps(source_path.as_path()).await;
let auth_mode = self.auth_mode();
let mut app_declarations = load_plugin_apps(source_path.as_path()).await;
let mut mcp_servers = load_plugin_mcp_servers(source_path.as_path(), auth_mode).await;
if auth_mode.is_some() {
apply_app_mcp_routing_policy(
&mut app_declarations,
&mut mcp_servers,
auth_mode,
/*plugin_active*/ true,
);
}
let apps = app_connector_ids_from_declarations(&app_declarations);
let mut seen_app_connector_ids = HashSet::new();
let mut app_category_by_id = HashMap::new();
@@ -1325,10 +1335,7 @@ impl PluginsManager {
app_category_by_id.insert(app.connector_id.0.clone(), category.clone());
}
}
let mut mcp_server_names = load_plugin_mcp_servers(source_path.as_path(), self.auth_mode())
.await
.into_keys()
.collect::<Vec<_>>();
let mut mcp_server_names = mcp_servers.into_keys().collect::<Vec<_>>();
mcp_server_names.sort_unstable();
mcp_server_names.dedup();
@@ -2493,6 +2493,10 @@ plugins = true
chatgpt_outcome.plugin.mcp_server_names,
vec!["other-mcp".to_string()]
);
assert_eq!(
chatgpt_outcome.plugin.apps,
vec![AppConnectorId("connector_sample".to_string())]
);
let api_key_outcome = PluginsManager::new_with_options(
tmp.path().to_path_buf(),
@@ -2506,6 +2510,7 @@ plugins = true
api_key_outcome.plugin.mcp_server_names,
vec!["other-mcp".to_string(), "sample-mcp".to_string()]
);
assert!(api_key_outcome.plugin.apps.is_empty());
}
#[tokio::test]
+37 -3
View File
@@ -1,3 +1,5 @@
use crate::app_mcp_routing::apply_app_mcp_routing_policy;
use crate::loader::plugin_app_declarations_from_value;
use crate::store::PLUGINS_CACHE_DIR;
use crate::store::PluginStore;
use codex_app_server_protocol::JSONRPCErrorError;
@@ -8,7 +10,10 @@ use codex_app_server_protocol::PluginInterface;
use codex_app_server_protocol::SkillInterface;
use codex_login::CodexAuth;
use codex_login::default_client::build_reqwest_client;
use codex_plugin::AppConnectorId;
use codex_plugin::AppDeclaration;
use codex_plugin::PluginId;
use codex_plugin::app_connector_ids_from_declarations;
use codex_utils_absolute_path::AbsolutePathBuf;
use reqwest::RequestBuilder;
use serde::Deserialize;
@@ -16,6 +21,7 @@ use serde::Serialize;
use serde_json::Value as JsonValue;
use std::collections::BTreeMap;
use std::collections::BTreeSet;
use std::collections::HashMap;
use std::collections::HashSet;
use std::fs;
use std::path::Path;
@@ -1064,12 +1070,29 @@ async fn build_remote_plugin_detail(
enabled: !disabled_skill_names.contains(&skill.name),
})
.collect();
let mut app_declarations = plugin
.release
.app_manifest
.as_ref()
.map(plugin_app_declarations_from_value)
.unwrap_or_else(|| app_declarations_from_remote_app_ids(&plugin.release.app_ids));
let mut mcp_servers = plugin
.release
.mcp_servers
.iter()
.map(|server| server.key.clone())
.collect::<Vec<_>>();
.map(|server| (server.key.clone(), ()))
.collect::<HashMap<_, _>>();
apply_app_mcp_routing_policy(
&mut app_declarations,
&mut mcp_servers,
Some(auth.api_auth_mode()),
/*plugin_active*/ true,
);
let app_ids = app_connector_ids_from_declarations(&app_declarations)
.into_iter()
.map(|app_id| app_id.0)
.collect();
let mut mcp_servers = mcp_servers.into_keys().collect::<Vec<_>>();
mcp_servers.sort_unstable();
mcp_servers.dedup();
@@ -1083,7 +1106,7 @@ async fn build_remote_plugin_detail(
bundle_download_url: plugin.release.bundle_download_url,
app_manifest: plugin.release.app_manifest,
skills,
app_ids: plugin.release.app_ids,
app_ids,
app_templates: plugin
.release
.app_templates
@@ -1104,6 +1127,17 @@ async fn build_remote_plugin_detail(
})
}
fn app_declarations_from_remote_app_ids(app_ids: &[String]) -> Vec<AppDeclaration> {
app_ids
.iter()
.map(|app_id| AppDeclaration {
name: app_id.clone(),
connector_id: AppConnectorId(app_id.clone()),
category: None,
})
.collect()
}
pub async fn install_remote_plugin(
config: &RemotePluginServiceConfig,
auth: Option<&CodexAuth>,