mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
[codex] Remove redundant plugin app auth state (#27465)
## Summary - remove the redundant `needsAuth` field from `AppSummary` and generated app-server schemas - stop `plugin/read` from querying Apps MCP solely to hydrate unused connector auth state - preserve `plugin/install.appsNeedingAuth` membership and `app/list.isAccessible` as the authentication signals ## Why Codex App and TUI do not consume `plugin/read.plugin.apps[].needsAuth`. Hydrating it could establish an Apps MCP connection and discover tools on a cold `plugin/read` request, adding avoidable latency. The plugin APIs are still marked under development, so removing this wire field is preferable to retaining a misleading default. ## Verification - `just write-app-server-schema` - `just fmt` - `just test -p codex-app-server-protocol` - `just test -p codex-app-server plugin_install_uses_remote_apps_needing_auth_response` - `just test -p codex-app-server plugin_install_returns_apps_needing_auth` - `just test -p codex-app-server plugin_read_returns_plugin_details_with_bundle_contents` - `just test -p codex-tui plugin_detail_popup_snapshot_shows_install_actions_and_capability_summaries` - `$xin-build` simplify and debug reviews
This commit is contained in:
committed by
GitHub
Unverified
parent
b389b950e1
commit
1a9efd473b
+1
-5
@@ -6433,15 +6433,11 @@
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"needsAuth": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"name",
|
||||
"needsAuth"
|
||||
"name"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
|
||||
+1
-5
@@ -711,15 +711,11 @@
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"needsAuth": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"name",
|
||||
"needsAuth"
|
||||
"name"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
|
||||
@@ -21,15 +21,11 @@
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"needsAuth": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"name",
|
||||
"needsAuth"
|
||||
"name"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
|
||||
@@ -25,15 +25,11 @@
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"needsAuth": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"name",
|
||||
"needsAuth"
|
||||
"name"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
|
||||
@@ -5,4 +5,4 @@
|
||||
/**
|
||||
* EXPERIMENTAL - app metadata summary for plugin responses.
|
||||
*/
|
||||
export type AppSummary = { id: string, name: string, description: string | null, installUrl: string | null, needsAuth: boolean, };
|
||||
export type AppSummary = { id: string, name: string, description: string | null, installUrl: string | null, };
|
||||
|
||||
@@ -111,7 +111,6 @@ pub struct AppSummary {
|
||||
pub name: String,
|
||||
pub description: Option<String>,
|
||||
pub install_url: Option<String>,
|
||||
pub needs_auth: bool,
|
||||
}
|
||||
|
||||
impl From<AppInfo> for AppSummary {
|
||||
@@ -121,7 +120,6 @@ impl From<AppInfo> for AppSummary {
|
||||
name: value.name,
|
||||
description: value.description,
|
||||
install_url: value.install_url,
|
||||
needs_auth: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -206,7 +206,7 @@ Example with notification opt-out:
|
||||
- `marketplace/upgrade` — upgrade all configured Git plugin marketplaces, or one named marketplace when `marketplaceName` is provided. Returns selected marketplace names, upgraded roots, and per-marketplace errors.
|
||||
- `plugin/list` — list discovered plugin marketplaces and plugin state, including effective marketplace install/auth policy metadata, plugin `availability` (`AVAILABLE` by default or `DISABLED_BY_ADMIN` for remote plugins blocked upstream), fail-open `marketplaceLoadErrors` entries for marketplace files that could not be parsed or loaded, and best-effort `featuredPluginIds` for the official curated marketplace. `interface.category` uses the marketplace category when present; otherwise it falls back to the plugin manifest category (**under development; do not call from production clients yet**).
|
||||
- `plugin/installed` — list installed plugin rows plus any explicitly requested local install-suggestion plugin names, without fetching the broader remote catalog. Mention surfaces can use this narrower view when they need plugin mention payloads rather than plugin-page discovery data (**under development; do not call from production clients yet**).
|
||||
- `plugin/read` — read one plugin by `marketplacePath` plus `pluginName`, returning marketplace info, a list-style `summary`, manifest descriptions/interface metadata, and bundled skills/hooks/apps/MCP server names. Returned plugin skills include their current `enabled` state after local config filtering; bundled hooks are returned as lightweight declaration summaries keyed for correlation with `hooks/list`. Plugin app summaries also include `needsAuth` when the server can determine connector accessibility (**under development; do not call from production clients yet**).
|
||||
- `plugin/read` — read one plugin by `marketplacePath` plus `pluginName`, returning marketplace info, a list-style `summary`, manifest descriptions/interface metadata, and bundled skills/hooks/apps/MCP server names. Returned plugin skills include their current `enabled` state after local config filtering; bundled hooks are returned as lightweight declaration summaries keyed for correlation with `hooks/list`. Use `plugin/install`'s `appsNeedingAuth` to drive post-install authentication and `app/list`'s `isAccessible` to determine current connector accessibility (**under development; do not call from production clients yet**).
|
||||
- `plugin/skill/read` — read remote plugin skill markdown on demand by `remoteMarketplaceName`, `remotePluginId`, and `skillName`. This lets clients preview uninstalled remote plugin skills without downloading the plugin bundle.
|
||||
- `skills/changed` — notification emitted when watched local skill files change.
|
||||
- `app/list` — list available apps.
|
||||
|
||||
@@ -1035,14 +1035,7 @@ impl PluginRequestProcessor {
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
let environment_manager = self.thread_manager.environment_manager();
|
||||
let app_summaries = load_plugin_app_summaries(
|
||||
&config,
|
||||
&outcome.plugin.apps,
|
||||
Arc::clone(&environment_manager),
|
||||
self.thread_manager.mcp_manager(),
|
||||
)
|
||||
.await;
|
||||
let app_summaries = load_plugin_app_summaries(&config, &outcome.plugin.apps).await;
|
||||
let visible_skills = outcome
|
||||
.plugin
|
||||
.skills
|
||||
@@ -1118,14 +1111,7 @@ impl PluginRequestProcessor {
|
||||
.cloned()
|
||||
.map(codex_plugin::AppConnectorId)
|
||||
.collect::<Vec<_>>();
|
||||
let environment_manager = self.thread_manager.environment_manager();
|
||||
let app_summaries = load_plugin_app_summaries(
|
||||
&config,
|
||||
&plugin_apps,
|
||||
Arc::clone(&environment_manager),
|
||||
self.thread_manager.mcp_manager(),
|
||||
)
|
||||
.await;
|
||||
let app_summaries = load_plugin_app_summaries(&config, &plugin_apps).await;
|
||||
remote_plugin_detail_to_info(remote_detail, app_summaries)
|
||||
}
|
||||
};
|
||||
@@ -1582,7 +1568,6 @@ impl PluginRequestProcessor {
|
||||
name: connector.name,
|
||||
description: connector.description,
|
||||
install_url: connector.install_url,
|
||||
needs_auth: true,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@@ -1887,8 +1872,6 @@ impl PluginRequestProcessor {
|
||||
async fn load_plugin_app_summaries(
|
||||
config: &Config,
|
||||
plugin_apps: &[codex_plugin::AppConnectorId],
|
||||
environment_manager: Arc<EnvironmentManager>,
|
||||
mcp_manager: Arc<McpManager>,
|
||||
) -> Vec<AppSummary> {
|
||||
if plugin_apps.is_empty() {
|
||||
return Vec::new();
|
||||
@@ -1906,49 +1889,9 @@ async fn load_plugin_app_summaries(
|
||||
};
|
||||
|
||||
let plugin_connectors = connectors::connectors_for_plugin_apps(connectors, plugin_apps);
|
||||
|
||||
let accessible_connectors =
|
||||
match connectors::list_accessible_connectors_from_mcp_tools_with_mcp_manager(
|
||||
config,
|
||||
/*force_refetch*/ false,
|
||||
environment_manager,
|
||||
mcp_manager,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(status) if status.codex_apps_ready => status.connectors,
|
||||
Ok(_) => {
|
||||
return plugin_connectors
|
||||
.into_iter()
|
||||
.map(AppSummary::from)
|
||||
.collect();
|
||||
}
|
||||
Err(err) => {
|
||||
warn!("failed to load app auth state for plugin/read: {err:#}");
|
||||
return plugin_connectors
|
||||
.into_iter()
|
||||
.map(AppSummary::from)
|
||||
.collect();
|
||||
}
|
||||
};
|
||||
|
||||
let accessible_ids = accessible_connectors
|
||||
.iter()
|
||||
.map(|connector| connector.id.as_str())
|
||||
.collect::<HashSet<_>>();
|
||||
|
||||
plugin_connectors
|
||||
.into_iter()
|
||||
.map(|connector| {
|
||||
let needs_auth = !accessible_ids.contains(connector.id.as_str());
|
||||
AppSummary {
|
||||
id: connector.id,
|
||||
name: connector.name,
|
||||
description: connector.description,
|
||||
install_url: connector.install_url,
|
||||
needs_auth,
|
||||
}
|
||||
})
|
||||
.map(AppSummary::from)
|
||||
.collect()
|
||||
}
|
||||
|
||||
@@ -1983,7 +1926,6 @@ fn plugin_apps_needing_auth(
|
||||
name: connector.name,
|
||||
description: connector.description,
|
||||
install_url: connector.install_url,
|
||||
needs_auth: true,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
@@ -327,7 +327,6 @@ async fn plugin_install_uses_remote_apps_needing_auth_response() -> Result<()> {
|
||||
name: "alpha".to_string(),
|
||||
description: None,
|
||||
install_url: Some("https://chatgpt.com/apps/alpha/alpha".to_string()),
|
||||
needs_auth: true,
|
||||
}],
|
||||
}
|
||||
);
|
||||
@@ -1001,7 +1000,6 @@ async fn plugin_install_returns_apps_needing_auth() -> Result<()> {
|
||||
name: "Alpha".to_string(),
|
||||
description: Some("Alpha connector".to_string()),
|
||||
install_url: Some("https://chatgpt.com/apps/alpha/alpha".to_string()),
|
||||
needs_auth: true,
|
||||
}],
|
||||
}
|
||||
);
|
||||
@@ -1089,7 +1087,6 @@ async fn plugin_install_filters_disallowed_apps_needing_auth() -> Result<()> {
|
||||
name: "Alpha".to_string(),
|
||||
description: Some("Alpha connector".to_string()),
|
||||
install_url: Some("https://chatgpt.com/apps/alpha/alpha".to_string()),
|
||||
needs_auth: true,
|
||||
}],
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
use std::borrow::Cow;
|
||||
use std::sync::Arc;
|
||||
use std::sync::Mutex as StdMutex;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Result;
|
||||
@@ -8,15 +5,6 @@ use app_test_support::ChatGptAuthFixture;
|
||||
use app_test_support::TestAppServer;
|
||||
use app_test_support::to_response;
|
||||
use app_test_support::write_chatgpt_auth;
|
||||
use axum::Json;
|
||||
use axum::Router;
|
||||
use axum::extract::State;
|
||||
use axum::http::HeaderMap;
|
||||
use axum::http::StatusCode;
|
||||
use axum::http::Uri;
|
||||
use axum::http::header::AUTHORIZATION;
|
||||
use axum::routing::get;
|
||||
use codex_app_server_protocol::AppInfo;
|
||||
use codex_app_server_protocol::AppTemplateSummary;
|
||||
use codex_app_server_protocol::AppTemplateUnavailableReason;
|
||||
use codex_app_server_protocol::HookEventName;
|
||||
@@ -37,21 +25,8 @@ use codex_app_server_protocol::RequestId;
|
||||
use codex_config::types::AuthCredentialsStoreMode;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use pretty_assertions::assert_eq;
|
||||
use rmcp::handler::server::ServerHandler;
|
||||
use rmcp::model::JsonObject;
|
||||
use rmcp::model::ListToolsResult;
|
||||
use rmcp::model::Meta;
|
||||
use rmcp::model::ServerCapabilities;
|
||||
use rmcp::model::ServerInfo;
|
||||
use rmcp::model::Tool;
|
||||
use rmcp::model::ToolAnnotations;
|
||||
use rmcp::transport::StreamableHttpServerConfig;
|
||||
use rmcp::transport::StreamableHttpService;
|
||||
use rmcp::transport::streamable_http_server::session::local::LocalSessionManager;
|
||||
use serde_json::json;
|
||||
use tempfile::TempDir;
|
||||
use tokio::net::TcpListener;
|
||||
use tokio::task::JoinHandle;
|
||||
use tokio::time::timeout;
|
||||
use wiremock::Mock;
|
||||
use wiremock::MockServer;
|
||||
@@ -1483,104 +1458,11 @@ enabled = false
|
||||
response.plugin.apps[0].install_url.as_deref(),
|
||||
Some("https://chatgpt.com/apps/gmail/gmail")
|
||||
);
|
||||
assert_eq!(response.plugin.apps[0].needs_auth, true);
|
||||
assert_eq!(response.plugin.mcp_servers.len(), 1);
|
||||
assert_eq!(response.plugin.mcp_servers[0], "demo");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugin_read_returns_app_needs_auth() -> Result<()> {
|
||||
let connectors = vec![
|
||||
AppInfo {
|
||||
id: "alpha".to_string(),
|
||||
name: "Alpha".to_string(),
|
||||
description: Some("Alpha connector".to_string()),
|
||||
logo_url: Some("https://example.com/alpha.png".to_string()),
|
||||
logo_url_dark: None,
|
||||
distribution_channel: Some("featured".to_string()),
|
||||
branding: None,
|
||||
app_metadata: None,
|
||||
labels: None,
|
||||
install_url: None,
|
||||
is_accessible: false,
|
||||
is_enabled: true,
|
||||
plugin_display_names: Vec::new(),
|
||||
},
|
||||
AppInfo {
|
||||
id: "beta".to_string(),
|
||||
name: "Beta".to_string(),
|
||||
description: Some("Beta connector".to_string()),
|
||||
logo_url: None,
|
||||
logo_url_dark: None,
|
||||
distribution_channel: None,
|
||||
branding: None,
|
||||
app_metadata: None,
|
||||
labels: None,
|
||||
install_url: None,
|
||||
is_accessible: false,
|
||||
is_enabled: true,
|
||||
plugin_display_names: Vec::new(),
|
||||
},
|
||||
];
|
||||
let tools = vec![connector_tool("beta", "Beta App")?];
|
||||
let (server_url, server_handle) = start_apps_server(connectors, tools).await?;
|
||||
|
||||
let codex_home = TempDir::new()?;
|
||||
write_connectors_config(codex_home.path(), &server_url)?;
|
||||
write_chatgpt_auth(
|
||||
codex_home.path(),
|
||||
ChatGptAuthFixture::new("chatgpt-token")
|
||||
.account_id("account-123")
|
||||
.chatgpt_user_id("user-123")
|
||||
.chatgpt_account_id("account-123"),
|
||||
AuthCredentialsStoreMode::File,
|
||||
)?;
|
||||
|
||||
let repo_root = TempDir::new()?;
|
||||
write_plugin_marketplace(
|
||||
repo_root.path(),
|
||||
"debug",
|
||||
"sample-plugin",
|
||||
"./sample-plugin",
|
||||
)?;
|
||||
write_plugin_source(repo_root.path(), "sample-plugin", &["alpha", "beta"])?;
|
||||
let marketplace_path =
|
||||
AbsolutePathBuf::try_from(repo_root.path().join(".agents/plugins/marketplace.json"))?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
.send_plugin_read_request(PluginReadParams {
|
||||
marketplace_path: Some(marketplace_path),
|
||||
remote_marketplace_name: None,
|
||||
plugin_name: "sample-plugin".to_string(),
|
||||
})
|
||||
.await?;
|
||||
|
||||
let response: JSONRPCResponse = timeout(
|
||||
DEFAULT_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
|
||||
)
|
||||
.await??;
|
||||
let response: PluginReadResponse = to_response(response)?;
|
||||
|
||||
assert_eq!(
|
||||
response
|
||||
.plugin
|
||||
.apps
|
||||
.iter()
|
||||
.map(|app| (app.id.as_str(), app.needs_auth))
|
||||
.collect::<Vec<_>>(),
|
||||
vec![("alpha", true), ("beta", false)]
|
||||
);
|
||||
|
||||
server_handle.abort();
|
||||
let _ = server_handle.await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugin_read_accepts_legacy_string_default_prompt() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
@@ -1846,151 +1728,6 @@ plugins = true
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct AppsServerState {
|
||||
response: Arc<StdMutex<serde_json::Value>>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct PluginReadMcpServer {
|
||||
tools: Arc<StdMutex<Vec<Tool>>>,
|
||||
}
|
||||
|
||||
impl ServerHandler for PluginReadMcpServer {
|
||||
fn get_info(&self) -> ServerInfo {
|
||||
ServerInfo::new(ServerCapabilities::builder().enable_tools().build())
|
||||
}
|
||||
|
||||
fn list_tools(
|
||||
&self,
|
||||
_request: Option<rmcp::model::PaginatedRequestParams>,
|
||||
_context: rmcp::service::RequestContext<rmcp::service::RoleServer>,
|
||||
) -> impl std::future::Future<Output = Result<ListToolsResult, rmcp::ErrorData>> + Send + '_
|
||||
{
|
||||
let tools = self.tools.clone();
|
||||
async move {
|
||||
let tools = tools
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.clone();
|
||||
Ok(ListToolsResult {
|
||||
tools,
|
||||
next_cursor: None,
|
||||
meta: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn start_apps_server(
|
||||
connectors: Vec<AppInfo>,
|
||||
tools: Vec<Tool>,
|
||||
) -> Result<(String, JoinHandle<()>)> {
|
||||
let state = Arc::new(AppsServerState {
|
||||
response: Arc::new(StdMutex::new(
|
||||
json!({ "apps": connectors, "next_token": null }),
|
||||
)),
|
||||
});
|
||||
let tools = Arc::new(StdMutex::new(tools));
|
||||
|
||||
let listener = TcpListener::bind("127.0.0.1:0").await?;
|
||||
let addr = listener.local_addr()?;
|
||||
let mcp_service = StreamableHttpService::new(
|
||||
{
|
||||
let tools = tools.clone();
|
||||
move || {
|
||||
Ok(PluginReadMcpServer {
|
||||
tools: tools.clone(),
|
||||
})
|
||||
}
|
||||
},
|
||||
Arc::new(LocalSessionManager::default()),
|
||||
StreamableHttpServerConfig::default(),
|
||||
);
|
||||
let router = Router::new()
|
||||
.route("/connectors/directory/list", get(list_directory_connectors))
|
||||
.route(
|
||||
"/connectors/directory/list_workspace",
|
||||
get(list_directory_connectors),
|
||||
)
|
||||
.with_state(state)
|
||||
.nest_service("/api/codex/ps/mcp", mcp_service);
|
||||
|
||||
let handle = tokio::spawn(async move {
|
||||
let _ = axum::serve(listener, router).await;
|
||||
});
|
||||
|
||||
Ok((format!("http://{addr}"), handle))
|
||||
}
|
||||
|
||||
async fn list_directory_connectors(
|
||||
State(state): State<Arc<AppsServerState>>,
|
||||
headers: HeaderMap,
|
||||
uri: Uri,
|
||||
) -> Result<impl axum::response::IntoResponse, StatusCode> {
|
||||
let bearer_ok = headers
|
||||
.get(AUTHORIZATION)
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.is_some_and(|value| value == "Bearer chatgpt-token");
|
||||
let account_ok = headers
|
||||
.get("chatgpt-account-id")
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.is_some_and(|value| value == "account-123");
|
||||
let external_logos_ok = uri
|
||||
.query()
|
||||
.is_some_and(|query| query.split('&').any(|pair| pair == "external_logos=true"));
|
||||
|
||||
if !bearer_ok || !account_ok {
|
||||
Err(StatusCode::UNAUTHORIZED)
|
||||
} else if !external_logos_ok {
|
||||
Err(StatusCode::BAD_REQUEST)
|
||||
} else {
|
||||
let response = state
|
||||
.response
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.clone();
|
||||
Ok(Json(response))
|
||||
}
|
||||
}
|
||||
|
||||
fn connector_tool(connector_id: &str, connector_name: &str) -> Result<Tool> {
|
||||
let schema: JsonObject = serde_json::from_value(json!({
|
||||
"type": "object",
|
||||
"additionalProperties": false
|
||||
}))?;
|
||||
let mut tool = Tool::new(
|
||||
Cow::Owned(format!("connector_{connector_id}")),
|
||||
Cow::Borrowed("Connector test tool"),
|
||||
Arc::new(schema),
|
||||
);
|
||||
tool.annotations = Some(ToolAnnotations::new().read_only(true));
|
||||
|
||||
let mut meta = Meta::new();
|
||||
meta.0
|
||||
.insert("connector_id".to_string(), json!(connector_id));
|
||||
meta.0
|
||||
.insert("connector_name".to_string(), json!(connector_name));
|
||||
tool.meta = Some(meta);
|
||||
Ok(tool)
|
||||
}
|
||||
|
||||
fn write_connectors_config(codex_home: &std::path::Path, base_url: &str) -> std::io::Result<()> {
|
||||
std::fs::write(
|
||||
codex_home.join("config.toml"),
|
||||
format!(
|
||||
r#"
|
||||
chatgpt_base_url = "{base_url}"
|
||||
mcp_oauth_credentials_store = "file"
|
||||
|
||||
[features]
|
||||
plugins = true
|
||||
connectors = true
|
||||
"#
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fn write_remote_plugin_catalog_config(
|
||||
codex_home: &std::path::Path,
|
||||
base_url: &str,
|
||||
|
||||
@@ -1414,7 +1414,7 @@ pub(super) fn plugins_test_detail(
|
||||
description: Option<&str>,
|
||||
skills: &[&str],
|
||||
hooks: &[(codex_app_server_protocol::HookEventName, usize)],
|
||||
apps: &[(&str, bool)],
|
||||
apps: &[&str],
|
||||
mcp_servers: &[&str],
|
||||
) -> PluginDetail {
|
||||
PluginDetail {
|
||||
@@ -1449,12 +1449,11 @@ pub(super) fn plugins_test_detail(
|
||||
.collect(),
|
||||
apps: apps
|
||||
.iter()
|
||||
.map(|(name, needs_auth)| AppSummary {
|
||||
.map(|name| AppSummary {
|
||||
id: format!("{name}-id"),
|
||||
name: (*name).to_string(),
|
||||
description: Some(format!("{name} app")),
|
||||
install_url: Some(format!("https://example.test/{name}")),
|
||||
needs_auth: *needs_auth,
|
||||
})
|
||||
.collect(),
|
||||
app_templates: Vec::new(),
|
||||
|
||||
@@ -654,7 +654,7 @@ async fn plugin_detail_popup_snapshot_shows_install_actions_and_capability_summa
|
||||
(codex_app_server_protocol::HookEventName::PreToolUse, 1),
|
||||
(codex_app_server_protocol::HookEventName::Stop, 2),
|
||||
],
|
||||
&[("Figma", true), ("Slack", false)],
|
||||
&["Figma", "Slack"],
|
||||
&["figma-mcp", "docs-mcp"],
|
||||
),
|
||||
}),
|
||||
@@ -698,7 +698,7 @@ async fn plugin_detail_popup_hides_disclosure_for_installed_plugins() {
|
||||
(codex_app_server_protocol::HookEventName::PreToolUse, 1),
|
||||
(codex_app_server_protocol::HookEventName::Stop, 2),
|
||||
],
|
||||
&[("Figma", true), ("Slack", false)],
|
||||
&["Figma", "Slack"],
|
||||
&["figma-mcp", "docs-mcp"],
|
||||
),
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user