mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
support plugin/list. (#13540)
Introduce a plugin/list which reads from local marketplace.json. Also update the signature for plugin/install.
This commit is contained in:
@@ -148,12 +148,13 @@ Example with notification opt-out:
|
||||
- `experimentalFeature/list` — list feature flags with stage metadata (`beta`, `underDevelopment`, `stable`, etc.), enabled/default-enabled state, and cursor pagination. For non-beta flags, `displayName`/`description`/`announcement` are `null`.
|
||||
- `collaborationMode/list` — list available collaboration mode presets (experimental, no pagination). This response omits built-in developer instructions; clients should either pass `settings.developer_instructions: null` when setting a mode to use Codex's built-in instructions, or provide their own instructions explicitly.
|
||||
- `skills/list` — list skills for one or more `cwd` values (optional `forceReload`).
|
||||
- `plugin/list` — list discovered marketplaces reachable from optional `cwds` (unioned into a single list). When `cwds` is omitted, only home-scoped marketplaces are considered. Includes each plugin's current `enabled` state from config (**under development; do not call from production clients yet**).
|
||||
- `skills/changed` — notification emitted when watched local skill files change.
|
||||
- `skills/remote/list` — list public remote skills (**under development; do not call from production clients yet**).
|
||||
- `skills/remote/export` — download a remote skill by `hazelnutId` into `skills` under `codex_home` (**under development; do not call from production clients yet**).
|
||||
- `app/list` — list available apps.
|
||||
- `skills/config/write` — write user-level skill config by path.
|
||||
- `plugin/install` — install a plugin from a discovered marketplace entry by `pluginName` and `marketplaceName` (**under development; do not call from production clients yet**).
|
||||
- `plugin/install` — install a plugin from a discovered marketplace entry by `pluginName` and `marketplacePath` (**under development; do not call from production clients yet**).
|
||||
- `mcpServer/oauth/login` — start an OAuth login for a configured MCP server; returns an `authorization_url` and later emits `mcpServer/oauthLogin/completed` once the browser flow finishes.
|
||||
- `tool/requestUserInput` — prompt the user with 1–3 short questions for a tool call and return their answers (experimental).
|
||||
- `config/mcpServer/reload` — reload MCP server config from disk and queue a refresh for loaded threads (applied on each thread's next active turn); returns `{}`. Use this after editing `config.toml` without restarting the server.
|
||||
|
||||
@@ -79,6 +79,11 @@ use codex_app_server_protocol::ModelListParams;
|
||||
use codex_app_server_protocol::ModelListResponse;
|
||||
use codex_app_server_protocol::PluginInstallParams;
|
||||
use codex_app_server_protocol::PluginInstallResponse;
|
||||
use codex_app_server_protocol::PluginListParams;
|
||||
use codex_app_server_protocol::PluginListResponse;
|
||||
use codex_app_server_protocol::PluginMarketplaceEntry;
|
||||
use codex_app_server_protocol::PluginSource;
|
||||
use codex_app_server_protocol::PluginSummary;
|
||||
use codex_app_server_protocol::ProductSurface as ApiProductSurface;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
use codex_app_server_protocol::ReviewDelivery as ApiReviewDelivery;
|
||||
@@ -198,6 +203,8 @@ 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::MarketplaceError;
|
||||
use codex_core::plugins::MarketplacePluginSourceSummary;
|
||||
use codex_core::plugins::PluginInstallError as CorePluginInstallError;
|
||||
use codex_core::plugins::PluginInstallRequest;
|
||||
use codex_core::read_head_for_summary;
|
||||
@@ -646,6 +653,10 @@ impl CodexMessageProcessor {
|
||||
self.skills_list(to_connection_request_id(request_id), params)
|
||||
.await;
|
||||
}
|
||||
ClientRequest::PluginList { request_id, params } => {
|
||||
self.plugin_list(to_connection_request_id(request_id), params)
|
||||
.await;
|
||||
}
|
||||
ClientRequest::SkillsRemoteList { request_id, params } => {
|
||||
self.skills_remote_list(to_connection_request_id(request_id), params)
|
||||
.await;
|
||||
@@ -4318,6 +4329,30 @@ impl CodexMessageProcessor {
|
||||
self.outgoing.send_error(request_id, error).await;
|
||||
}
|
||||
|
||||
async fn send_marketplace_error(
|
||||
&self,
|
||||
request_id: ConnectionRequestId,
|
||||
err: MarketplaceError,
|
||||
action: &str,
|
||||
) {
|
||||
match err {
|
||||
MarketplaceError::MarketplaceNotFound { .. } => {
|
||||
self.send_invalid_request_error(request_id, err.to_string())
|
||||
.await;
|
||||
}
|
||||
MarketplaceError::Io { .. } => {
|
||||
self.send_internal_error(request_id, format!("failed to {action}: {err}"))
|
||||
.await;
|
||||
}
|
||||
MarketplaceError::InvalidMarketplaceFile { .. }
|
||||
| MarketplaceError::PluginNotFound { .. }
|
||||
| MarketplaceError::InvalidPlugin(_) => {
|
||||
self.send_invalid_request_error(request_id, err.to_string())
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn wait_for_thread_shutdown(thread: &Arc<CodexThread>) -> ThreadShutdownResult {
|
||||
match thread.submit(Op::Shutdown).await {
|
||||
Ok(_) => {
|
||||
@@ -4924,6 +4959,66 @@ impl CodexMessageProcessor {
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn plugin_list(&self, request_id: ConnectionRequestId, params: PluginListParams) {
|
||||
let plugins_manager = self.thread_manager.plugins_manager();
|
||||
let roots = params.cwds.unwrap_or_default();
|
||||
|
||||
let config = match self.load_latest_config().await {
|
||||
Ok(config) => config,
|
||||
Err(err) => {
|
||||
self.outgoing.send_error(request_id, err).await;
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let data = match tokio::task::spawn_blocking(move || {
|
||||
let marketplaces = plugins_manager.list_marketplaces_for_config(&config, &roots)?;
|
||||
Ok::<Vec<PluginMarketplaceEntry>, MarketplaceError>(
|
||||
marketplaces
|
||||
.into_iter()
|
||||
.map(|marketplace| PluginMarketplaceEntry {
|
||||
name: marketplace.name,
|
||||
path: marketplace.path,
|
||||
plugins: marketplace
|
||||
.plugins
|
||||
.into_iter()
|
||||
.map(|plugin| PluginSummary {
|
||||
enabled: plugin.enabled,
|
||||
name: plugin.name,
|
||||
source: match plugin.source {
|
||||
MarketplacePluginSourceSummary::Local { path } => {
|
||||
PluginSource::Local { path }
|
||||
}
|
||||
},
|
||||
})
|
||||
.collect(),
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(Ok(data)) => data,
|
||||
Ok(Err(err)) => {
|
||||
self.send_marketplace_error(request_id, err, "list marketplace plugins")
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
Err(err) => {
|
||||
self.send_internal_error(
|
||||
request_id,
|
||||
format!("failed to list marketplace plugins: {err}"),
|
||||
)
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
self.outgoing
|
||||
.send_response(request_id, PluginListResponse { marketplaces: data })
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn skills_remote_list(
|
||||
&self,
|
||||
request_id: ConnectionRequestId,
|
||||
@@ -5034,16 +5129,14 @@ impl CodexMessageProcessor {
|
||||
|
||||
async fn plugin_install(&self, request_id: ConnectionRequestId, params: PluginInstallParams) {
|
||||
let PluginInstallParams {
|
||||
marketplace_name,
|
||||
marketplace_path,
|
||||
plugin_name,
|
||||
cwd,
|
||||
} = params;
|
||||
|
||||
let plugins_manager = self.thread_manager.plugins_manager();
|
||||
let request = PluginInstallRequest {
|
||||
plugin_name,
|
||||
marketplace_name,
|
||||
cwd: cwd.unwrap_or_else(|| self.config.cwd.clone()),
|
||||
marketplace_path,
|
||||
};
|
||||
|
||||
match plugins_manager.install_plugin(request).await {
|
||||
@@ -5062,6 +5155,10 @@ impl CodexMessageProcessor {
|
||||
}
|
||||
|
||||
match err {
|
||||
CorePluginInstallError::Marketplace(err) => {
|
||||
self.send_marketplace_error(request_id, err, "install plugin")
|
||||
.await;
|
||||
}
|
||||
CorePluginInstallError::Config(err) => {
|
||||
self.send_internal_error(
|
||||
request_id,
|
||||
@@ -5076,7 +5173,13 @@ impl CodexMessageProcessor {
|
||||
)
|
||||
.await;
|
||||
}
|
||||
CorePluginInstallError::Marketplace(_) | CorePluginInstallError::Store(_) => {}
|
||||
CorePluginInstallError::Store(err) => {
|
||||
self.send_internal_error(
|
||||
request_id,
|
||||
format!("failed to install plugin: {err}"),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +35,8 @@ use codex_app_server_protocol::JSONRPCResponse;
|
||||
use codex_app_server_protocol::LoginAccountParams;
|
||||
use codex_app_server_protocol::MockExperimentalMethodParams;
|
||||
use codex_app_server_protocol::ModelListParams;
|
||||
use codex_app_server_protocol::PluginInstallParams;
|
||||
use codex_app_server_protocol::PluginListParams;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
use codex_app_server_protocol::ReviewStartParams;
|
||||
use codex_app_server_protocol::ServerRequest;
|
||||
@@ -439,6 +441,33 @@ impl McpProcess {
|
||||
self.send_request("skills/list", params).await
|
||||
}
|
||||
|
||||
/// Send a `plugin/install` JSON-RPC request.
|
||||
pub async fn send_plugin_install_request(
|
||||
&mut self,
|
||||
params: PluginInstallParams,
|
||||
) -> anyhow::Result<i64> {
|
||||
let params = Some(serde_json::to_value(params)?);
|
||||
self.send_request("plugin/install", params).await
|
||||
}
|
||||
|
||||
/// Send a `plugin/list` JSON-RPC request.
|
||||
pub async fn send_plugin_list_request(
|
||||
&mut self,
|
||||
params: PluginListParams,
|
||||
) -> anyhow::Result<i64> {
|
||||
let params = Some(serde_json::to_value(params)?);
|
||||
self.send_request("plugin/list", params).await
|
||||
}
|
||||
|
||||
/// Send a JSON-RPC request with raw params for protocol-level validation tests.
|
||||
pub async fn send_raw_request(
|
||||
&mut self,
|
||||
method: &str,
|
||||
params: Option<serde_json::Value>,
|
||||
) -> anyhow::Result<i64> {
|
||||
self.send_request(method, params).await
|
||||
}
|
||||
|
||||
/// Send a `collaborationMode/list` JSON-RPC request.
|
||||
pub async fn send_list_collaboration_modes_request(
|
||||
&mut self,
|
||||
|
||||
@@ -15,6 +15,8 @@ mod mcp_server_elicitation;
|
||||
mod model_list;
|
||||
mod output_schema;
|
||||
mod plan_item;
|
||||
mod plugin_install;
|
||||
mod plugin_list;
|
||||
mod rate_limits;
|
||||
mod realtime_conversation;
|
||||
mod request_user_input;
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Result;
|
||||
use app_test_support::McpProcess;
|
||||
use codex_app_server_protocol::PluginInstallParams;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use pretty_assertions::assert_eq;
|
||||
use tempfile::TempDir;
|
||||
use tokio::time::timeout;
|
||||
|
||||
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugin_install_rejects_relative_marketplace_paths() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let mut mcp = McpProcess::new(codex_home.path()).await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
.send_raw_request(
|
||||
"plugin/install",
|
||||
Some(serde_json::json!({
|
||||
"marketplacePath": "relative-marketplace.json",
|
||||
"pluginName": "missing-plugin",
|
||||
})),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let err = timeout(
|
||||
DEFAULT_TIMEOUT,
|
||||
mcp.read_stream_until_error_message(RequestId::Integer(request_id)),
|
||||
)
|
||||
.await??;
|
||||
|
||||
assert_eq!(err.error.code, -32600);
|
||||
assert!(err.error.message.contains("Invalid request"));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugin_install_returns_invalid_request_for_missing_marketplace_file() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let mut mcp = McpProcess::new(codex_home.path()).await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
.send_plugin_install_request(PluginInstallParams {
|
||||
marketplace_path: AbsolutePathBuf::try_from(
|
||||
codex_home.path().join("missing-marketplace.json"),
|
||||
)?,
|
||||
plugin_name: "missing-plugin".to_string(),
|
||||
})
|
||||
.await?;
|
||||
|
||||
let err = timeout(
|
||||
DEFAULT_TIMEOUT,
|
||||
mcp.read_stream_until_error_message(RequestId::Integer(request_id)),
|
||||
)
|
||||
.await??;
|
||||
|
||||
assert_eq!(err.error.code, -32600);
|
||||
assert!(err.error.message.contains("marketplace file"));
|
||||
assert!(err.error.message.contains("does not exist"));
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,299 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Result;
|
||||
use app_test_support::McpProcess;
|
||||
use app_test_support::to_response;
|
||||
use codex_app_server_protocol::JSONRPCResponse;
|
||||
use codex_app_server_protocol::PluginListParams;
|
||||
use codex_app_server_protocol::PluginListResponse;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
use codex_core::config::set_project_trust_level;
|
||||
use codex_protocol::config_types::TrustLevel;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use pretty_assertions::assert_eq;
|
||||
use tempfile::TempDir;
|
||||
use tokio::time::timeout;
|
||||
|
||||
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugin_list_returns_invalid_request_for_invalid_marketplace_file() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let repo_root = TempDir::new()?;
|
||||
std::fs::create_dir_all(repo_root.path().join(".git"))?;
|
||||
std::fs::create_dir_all(repo_root.path().join(".agents/plugins"))?;
|
||||
std::fs::write(
|
||||
repo_root.path().join(".agents/plugins/marketplace.json"),
|
||||
"{not json",
|
||||
)?;
|
||||
|
||||
let home = codex_home.path().to_string_lossy().into_owned();
|
||||
let mut mcp = McpProcess::new_with_env(
|
||||
codex_home.path(),
|
||||
&[
|
||||
("HOME", Some(home.as_str())),
|
||||
("USERPROFILE", Some(home.as_str())),
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
.send_plugin_list_request(PluginListParams {
|
||||
cwds: Some(vec![AbsolutePathBuf::try_from(repo_root.path())?]),
|
||||
})
|
||||
.await?;
|
||||
|
||||
let err = timeout(
|
||||
DEFAULT_TIMEOUT,
|
||||
mcp.read_stream_until_error_message(RequestId::Integer(request_id)),
|
||||
)
|
||||
.await??;
|
||||
|
||||
assert_eq!(err.error.code, -32600);
|
||||
assert!(err.error.message.contains("invalid marketplace file"));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugin_list_rejects_relative_cwds() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let mut mcp = McpProcess::new(codex_home.path()).await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
.send_raw_request(
|
||||
"plugin/list",
|
||||
Some(serde_json::json!({
|
||||
"cwds": ["relative-root"],
|
||||
})),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let err = timeout(
|
||||
DEFAULT_TIMEOUT,
|
||||
mcp.read_stream_until_error_message(RequestId::Integer(request_id)),
|
||||
)
|
||||
.await??;
|
||||
|
||||
assert_eq!(err.error.code, -32600);
|
||||
assert!(err.error.message.contains("Invalid request"));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugin_list_accepts_omitted_cwds() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
std::fs::create_dir_all(codex_home.path().join(".agents/plugins"))?;
|
||||
std::fs::write(
|
||||
codex_home.path().join(".agents/plugins/marketplace.json"),
|
||||
r#"{
|
||||
"name": "codex-curated",
|
||||
"plugins": [
|
||||
{
|
||||
"name": "home-plugin",
|
||||
"source": {
|
||||
"source": "local",
|
||||
"path": "./home-plugin"
|
||||
}
|
||||
}
|
||||
]
|
||||
}"#,
|
||||
)?;
|
||||
let home = codex_home.path().to_string_lossy().into_owned();
|
||||
let mut mcp = McpProcess::new_with_env(
|
||||
codex_home.path(),
|
||||
&[
|
||||
("HOME", Some(home.as_str())),
|
||||
("USERPROFILE", Some(home.as_str())),
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
.send_plugin_list_request(PluginListParams { cwds: None })
|
||||
.await?;
|
||||
|
||||
let response: JSONRPCResponse = timeout(
|
||||
DEFAULT_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
|
||||
)
|
||||
.await??;
|
||||
let _: PluginListResponse = to_response(response)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugin_list_includes_enabled_state_from_config() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let repo_root = TempDir::new()?;
|
||||
std::fs::create_dir_all(repo_root.path().join(".git"))?;
|
||||
std::fs::create_dir_all(repo_root.path().join(".agents/plugins"))?;
|
||||
std::fs::write(
|
||||
repo_root.path().join(".agents/plugins/marketplace.json"),
|
||||
r#"{
|
||||
"name": "codex-curated",
|
||||
"plugins": [
|
||||
{
|
||||
"name": "enabled-plugin",
|
||||
"source": {
|
||||
"source": "local",
|
||||
"path": "./enabled-plugin"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "disabled-plugin",
|
||||
"source": {
|
||||
"source": "local",
|
||||
"path": "./disabled-plugin"
|
||||
}
|
||||
}
|
||||
]
|
||||
}"#,
|
||||
)?;
|
||||
std::fs::write(
|
||||
codex_home.path().join("config.toml"),
|
||||
r#"[features]
|
||||
plugins = true
|
||||
|
||||
[plugins."enabled-plugin@codex-curated"]
|
||||
enabled = true
|
||||
|
||||
[plugins."disabled-plugin@codex-curated"]
|
||||
enabled = false
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let mut mcp = McpProcess::new(codex_home.path()).await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
.send_plugin_list_request(PluginListParams {
|
||||
cwds: Some(vec![AbsolutePathBuf::try_from(repo_root.path())?]),
|
||||
})
|
||||
.await?;
|
||||
|
||||
let response: JSONRPCResponse = timeout(
|
||||
DEFAULT_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
|
||||
)
|
||||
.await??;
|
||||
let response: PluginListResponse = to_response(response)?;
|
||||
|
||||
let marketplace = response
|
||||
.marketplaces
|
||||
.into_iter()
|
||||
.find(|marketplace| {
|
||||
marketplace.path == repo_root.path().join(".agents/plugins/marketplace.json")
|
||||
})
|
||||
.expect("expected repo marketplace entry");
|
||||
|
||||
assert_eq!(marketplace.name, "codex-curated");
|
||||
assert_eq!(marketplace.plugins.len(), 2);
|
||||
assert_eq!(marketplace.plugins[0].name, "enabled-plugin");
|
||||
assert_eq!(marketplace.plugins[0].enabled, true);
|
||||
assert_eq!(marketplace.plugins[1].name, "disabled-plugin");
|
||||
assert_eq!(marketplace.plugins[1].enabled, false);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugin_list_uses_home_config_for_enabled_state() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
std::fs::create_dir_all(codex_home.path().join(".agents/plugins"))?;
|
||||
std::fs::write(
|
||||
codex_home.path().join(".agents/plugins/marketplace.json"),
|
||||
r#"{
|
||||
"name": "codex-curated",
|
||||
"plugins": [
|
||||
{
|
||||
"name": "shared-plugin",
|
||||
"source": {
|
||||
"source": "local",
|
||||
"path": "./shared-plugin"
|
||||
}
|
||||
}
|
||||
]
|
||||
}"#,
|
||||
)?;
|
||||
std::fs::write(
|
||||
codex_home.path().join("config.toml"),
|
||||
r#"[features]
|
||||
plugins = true
|
||||
|
||||
[plugins."shared-plugin@codex-curated"]
|
||||
enabled = true
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let workspace_enabled = TempDir::new()?;
|
||||
std::fs::create_dir_all(workspace_enabled.path().join(".git"))?;
|
||||
std::fs::create_dir_all(workspace_enabled.path().join(".agents/plugins"))?;
|
||||
std::fs::write(
|
||||
workspace_enabled
|
||||
.path()
|
||||
.join(".agents/plugins/marketplace.json"),
|
||||
r#"{
|
||||
"name": "codex-curated",
|
||||
"plugins": [
|
||||
{
|
||||
"name": "shared-plugin",
|
||||
"source": {
|
||||
"source": "local",
|
||||
"path": "./shared-plugin"
|
||||
}
|
||||
}
|
||||
]
|
||||
}"#,
|
||||
)?;
|
||||
std::fs::create_dir_all(workspace_enabled.path().join(".codex"))?;
|
||||
std::fs::write(
|
||||
workspace_enabled.path().join(".codex/config.toml"),
|
||||
r#"[plugins."shared-plugin@codex-curated"]
|
||||
enabled = false
|
||||
"#,
|
||||
)?;
|
||||
set_project_trust_level(
|
||||
codex_home.path(),
|
||||
workspace_enabled.path(),
|
||||
TrustLevel::Trusted,
|
||||
)?;
|
||||
|
||||
let workspace_default = TempDir::new()?;
|
||||
let home = codex_home.path().to_string_lossy().into_owned();
|
||||
let mut mcp = McpProcess::new_with_env(
|
||||
codex_home.path(),
|
||||
&[
|
||||
("HOME", Some(home.as_str())),
|
||||
("USERPROFILE", Some(home.as_str())),
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
.send_plugin_list_request(PluginListParams {
|
||||
cwds: Some(vec![
|
||||
AbsolutePathBuf::try_from(workspace_enabled.path())?,
|
||||
AbsolutePathBuf::try_from(workspace_default.path())?,
|
||||
]),
|
||||
})
|
||||
.await?;
|
||||
|
||||
let response: JSONRPCResponse = timeout(
|
||||
DEFAULT_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
|
||||
)
|
||||
.await??;
|
||||
let response: PluginListResponse = to_response(response)?;
|
||||
|
||||
let shared_plugin = response
|
||||
.marketplaces
|
||||
.iter()
|
||||
.flat_map(|marketplace| marketplace.plugins.iter())
|
||||
.find(|plugin| plugin.name == "shared-plugin")
|
||||
.expect("expected shared-plugin entry");
|
||||
assert_eq!(shared_plugin.enabled, true);
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user