Compare commits

...

1 Commits

Author SHA1 Message Date
xli-oai
f45d2ef316 Clarify local and remote plugin selectors 2026-05-01 19:34:56 -07:00
17 changed files with 381 additions and 192 deletions

View File

@@ -2119,7 +2119,7 @@
},
"PluginInstallParams": {
"properties": {
"marketplacePath": {
"localMarketplacePath": {
"anyOf": [
{
"$ref": "#/definitions/AbsolutePathBuf"
@@ -2129,19 +2129,25 @@
}
]
},
"pluginName": {
"type": "string"
"localPluginName": {
"type": [
"string",
"null"
]
},
"remoteMarketplaceName": {
"type": [
"string",
"null"
]
},
"remotePluginId": {
"type": [
"string",
"null"
]
}
},
"required": [
"pluginName"
],
"type": "object"
},
"PluginListParams": {
@@ -2161,7 +2167,7 @@
},
"PluginReadParams": {
"properties": {
"marketplacePath": {
"localMarketplacePath": {
"anyOf": [
{
"$ref": "#/definitions/AbsolutePathBuf"
@@ -2171,19 +2177,25 @@
}
]
},
"pluginName": {
"type": "string"
"localPluginName": {
"type": [
"string",
"null"
]
},
"remoteMarketplaceName": {
"type": [
"string",
"null"
]
},
"remotePluginId": {
"type": [
"string",
"null"
]
}
},
"required": [
"pluginName"
],
"type": "object"
},
"PluginShareDeleteParams": {

View File

@@ -12028,7 +12028,7 @@
"PluginInstallParams": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"marketplacePath": {
"localMarketplacePath": {
"anyOf": [
{
"$ref": "#/definitions/v2/AbsolutePathBuf"
@@ -12038,19 +12038,25 @@
}
]
},
"pluginName": {
"type": "string"
"localPluginName": {
"type": [
"string",
"null"
]
},
"remoteMarketplaceName": {
"type": [
"string",
"null"
]
},
"remotePluginId": {
"type": [
"string",
"null"
]
}
},
"required": [
"pluginName"
],
"title": "PluginInstallParams",
"type": "object"
},
@@ -12301,7 +12307,7 @@
"PluginReadParams": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"marketplacePath": {
"localMarketplacePath": {
"anyOf": [
{
"$ref": "#/definitions/v2/AbsolutePathBuf"
@@ -12311,19 +12317,25 @@
}
]
},
"pluginName": {
"type": "string"
"localPluginName": {
"type": [
"string",
"null"
]
},
"remoteMarketplaceName": {
"type": [
"string",
"null"
]
},
"remotePluginId": {
"type": [
"string",
"null"
]
}
},
"required": [
"pluginName"
],
"title": "PluginReadParams",
"type": "object"
},

View File

@@ -8681,7 +8681,7 @@
"PluginInstallParams": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"marketplacePath": {
"localMarketplacePath": {
"anyOf": [
{
"$ref": "#/definitions/AbsolutePathBuf"
@@ -8691,19 +8691,25 @@
}
]
},
"pluginName": {
"type": "string"
"localPluginName": {
"type": [
"string",
"null"
]
},
"remoteMarketplaceName": {
"type": [
"string",
"null"
]
},
"remotePluginId": {
"type": [
"string",
"null"
]
}
},
"required": [
"pluginName"
],
"title": "PluginInstallParams",
"type": "object"
},
@@ -8954,7 +8960,7 @@
"PluginReadParams": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"marketplacePath": {
"localMarketplacePath": {
"anyOf": [
{
"$ref": "#/definitions/AbsolutePathBuf"
@@ -8964,19 +8970,25 @@
}
]
},
"pluginName": {
"type": "string"
"localPluginName": {
"type": [
"string",
"null"
]
},
"remoteMarketplaceName": {
"type": [
"string",
"null"
]
},
"remotePluginId": {
"type": [
"string",
"null"
]
}
},
"required": [
"pluginName"
],
"title": "PluginReadParams",
"type": "object"
},

View File

@@ -7,7 +7,7 @@
}
},
"properties": {
"marketplacePath": {
"localMarketplacePath": {
"anyOf": [
{
"$ref": "#/definitions/AbsolutePathBuf"
@@ -17,19 +17,25 @@
}
]
},
"pluginName": {
"type": "string"
"localPluginName": {
"type": [
"string",
"null"
]
},
"remoteMarketplaceName": {
"type": [
"string",
"null"
]
},
"remotePluginId": {
"type": [
"string",
"null"
]
}
},
"required": [
"pluginName"
],
"title": "PluginInstallParams",
"type": "object"
}

View File

@@ -7,7 +7,7 @@
}
},
"properties": {
"marketplacePath": {
"localMarketplacePath": {
"anyOf": [
{
"$ref": "#/definitions/AbsolutePathBuf"
@@ -17,19 +17,25 @@
}
]
},
"pluginName": {
"type": "string"
"localPluginName": {
"type": [
"string",
"null"
]
},
"remoteMarketplaceName": {
"type": [
"string",
"null"
]
},
"remotePluginId": {
"type": [
"string",
"null"
]
}
},
"required": [
"pluginName"
],
"title": "PluginReadParams",
"type": "object"
}

View File

@@ -3,4 +3,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { AbsolutePathBuf } from "../AbsolutePathBuf";
export type PluginInstallParams = { marketplacePath?: AbsolutePathBuf | null, remoteMarketplaceName?: string | null, pluginName: string, };
export type PluginInstallParams = { localMarketplacePath?: AbsolutePathBuf | null, remoteMarketplaceName?: string | null, localPluginName?: string | null, remotePluginId?: string | null, };

View File

@@ -3,4 +3,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { AbsolutePathBuf } from "../AbsolutePathBuf";
export type PluginReadParams = { marketplacePath?: AbsolutePathBuf | null, remoteMarketplaceName?: string | null, pluginName: string, };
export type PluginReadParams = { localMarketplacePath?: AbsolutePathBuf | null, remoteMarketplaceName?: string | null, localPluginName?: string | null, remotePluginId?: string | null, };

View File

@@ -1595,9 +1595,10 @@ mod tests {
let plugin_install = ClientRequest::PluginInstall {
request_id: request_id(),
params: v2::PluginInstallParams {
marketplace_path: Some(absolute_path("/tmp/marketplace")),
local_marketplace_path: Some(absolute_path("/tmp/marketplace")),
remote_marketplace_name: None,
plugin_name: "plugin-a".to_string(),
local_plugin_name: Some("plugin-a".to_string()),
remote_plugin_id: None,
},
};
assert_eq!(

View File

@@ -4597,10 +4597,13 @@ pub struct MarketplaceLoadErrorInfo {
#[ts(export_to = "v2/")]
pub struct PluginReadParams {
#[ts(optional = nullable)]
pub marketplace_path: Option<AbsolutePathBuf>,
pub local_marketplace_path: Option<AbsolutePathBuf>,
#[ts(optional = nullable)]
pub remote_marketplace_name: Option<String>,
pub plugin_name: String,
#[ts(optional = nullable)]
pub local_plugin_name: Option<String>,
#[ts(optional = nullable)]
pub remote_plugin_id: Option<String>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
@@ -4987,10 +4990,13 @@ pub struct SkillsConfigWriteResponse {
#[ts(export_to = "v2/")]
pub struct PluginInstallParams {
#[ts(optional = nullable)]
pub marketplace_path: Option<AbsolutePathBuf>,
pub local_marketplace_path: Option<AbsolutePathBuf>,
#[ts(optional = nullable)]
pub remote_marketplace_name: Option<String>,
pub plugin_name: String,
#[ts(optional = nullable)]
pub local_plugin_name: Option<String>,
#[ts(optional = nullable)]
pub remote_plugin_id: Option<String>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
@@ -10658,42 +10664,46 @@ mod tests {
let marketplace_path_json = marketplace_path.as_path().display().to_string();
assert_eq!(
serde_json::to_value(PluginReadParams {
marketplace_path: Some(marketplace_path.clone()),
local_marketplace_path: Some(marketplace_path.clone()),
remote_marketplace_name: None,
plugin_name: "gmail".to_string(),
local_plugin_name: Some("gmail".to_string()),
remote_plugin_id: None,
})
.unwrap(),
json!({
"marketplacePath": marketplace_path_json,
"localMarketplacePath": marketplace_path_json,
"remoteMarketplaceName": null,
"pluginName": "gmail",
"localPluginName": "gmail",
"remotePluginId": null,
}),
);
assert_eq!(
serde_json::from_value::<PluginReadParams>(json!({
"marketplacePath": marketplace_path_json,
"pluginName": "gmail",
"localMarketplacePath": marketplace_path_json,
"localPluginName": "gmail",
"forceRemoteSync": true,
}))
.unwrap(),
PluginReadParams {
marketplace_path: Some(marketplace_path),
local_marketplace_path: Some(marketplace_path),
remote_marketplace_name: None,
plugin_name: "gmail".to_string(),
local_plugin_name: Some("gmail".to_string()),
remote_plugin_id: None,
},
);
assert_eq!(
serde_json::from_value::<PluginReadParams>(json!({
"remoteMarketplaceName": "openai-curated",
"pluginName": "gmail",
"remotePluginId": "plugins~Plugin_gmail",
}))
.unwrap(),
PluginReadParams {
marketplace_path: None,
local_marketplace_path: None,
remote_marketplace_name: Some("openai-curated".to_string()),
plugin_name: "gmail".to_string(),
local_plugin_name: None,
remote_plugin_id: Some("plugins~Plugin_gmail".to_string()),
},
);
}
@@ -10709,43 +10719,47 @@ mod tests {
let marketplace_path_json = marketplace_path.as_path().display().to_string();
assert_eq!(
serde_json::to_value(PluginInstallParams {
marketplace_path: Some(marketplace_path.clone()),
local_marketplace_path: Some(marketplace_path.clone()),
remote_marketplace_name: None,
plugin_name: "gmail".to_string(),
local_plugin_name: Some("gmail".to_string()),
remote_plugin_id: None,
})
.unwrap(),
json!({
"marketplacePath": marketplace_path_json,
"localMarketplacePath": marketplace_path_json,
"remoteMarketplaceName": null,
"pluginName": "gmail",
"localPluginName": "gmail",
"remotePluginId": null,
}),
);
assert_eq!(
serde_json::from_value::<PluginInstallParams>(json!({
"marketplacePath": marketplace_path_json,
"pluginName": "gmail",
"localMarketplacePath": marketplace_path_json,
"localPluginName": "gmail",
"forceRemoteSync": true,
}))
.unwrap(),
PluginInstallParams {
marketplace_path: Some(marketplace_path),
local_marketplace_path: Some(marketplace_path),
remote_marketplace_name: None,
plugin_name: "gmail".to_string(),
local_plugin_name: Some("gmail".to_string()),
remote_plugin_id: None,
},
);
assert_eq!(
serde_json::from_value::<PluginInstallParams>(json!({
"remoteMarketplaceName": "openai-curated",
"pluginName": "gmail",
"remotePluginId": "plugins~Plugin_gmail",
"forceRemoteSync": true,
}))
.unwrap(),
PluginInstallParams {
marketplace_path: None,
local_marketplace_path: None,
remote_marketplace_name: Some("openai-curated".to_string()),
plugin_name: "gmail".to_string(),
local_plugin_name: None,
remote_plugin_id: Some("plugins~Plugin_gmail".to_string()),
},
);
}

View File

@@ -202,7 +202,7 @@ Example with notification opt-out:
- `marketplace/remove` — remove a configured marketplace by name from the user marketplace config, and delete its installed marketplace root when one exists.
- `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/read` — read one plugin by `marketplacePath` plus `pluginName`, returning marketplace info, a list-style `summary`, manifest descriptions/interface metadata, and bundled skills/apps/MCP server names. Returned plugin skills include their current `enabled` state after local config filtering. 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 `localMarketplacePath` plus `localPluginName`, or by `remoteMarketplaceName` plus `remotePluginId`, returning marketplace info, a list-style `summary`, manifest descriptions/interface metadata, and bundled skills/apps/MCP server names. Returned plugin skills include their current `enabled` state after local config filtering. Plugin app summaries also include `needsAuth` when the server can determine 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.
@@ -211,7 +211,7 @@ Example with notification opt-out:
- `device/key/sign` — sign one of the accepted structured payload variants with a controller-local device key. The only accepted payload today is `remoteControlClientConnection`, which binds a server-issued `/client` websocket challenge to the enrolled controller device without signing the bearer token itself; this is intentionally not an arbitrary-byte signing API.
- `remoteControl/status/changed` — notification emitted when the remote-control status or client-visible environment id changes. `status` is one of `disabled`, `connecting`, `connected`, or `errored`; `environmentId` is a string when the app-server has a current enrollment and `null` when that enrollment is cleared, invalidated, or remote control is disabled. Newly initialized app-server clients always receive the current status snapshot.
- `skills/config/write` — write user-level skill config by name or absolute path.
- `plugin/install` — install a plugin from a discovered marketplace entry, rejecting marketplace entries marked unavailable for install, install MCPs if any, and return the effective plugin auth policy plus any apps that still need auth (**under development; do not call from production clients yet**).
- `plugin/install` — install a local plugin from a discovered marketplace entry by `localMarketplacePath` plus `localPluginName`, or install a remote ChatGPT plugin by `remoteMarketplaceName` plus backend `remotePluginId`, rejecting marketplace entries marked unavailable for install, install MCPs if any, and return the effective plugin auth policy plus any apps that still need auth (**under development; do not call from production clients yet**).
- `plugin/uninstall` — uninstall a local plugin by `pluginId` in `<plugin>@<marketplace>` form by removing its cached files and clearing its user-level config entry, or uninstall a remote ChatGPT plugin by backend `pluginId` by forwarding the uninstall to the ChatGPT plugin backend and removing any downloaded remote-plugin cache (**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 13 short questions for a tool call and return their answers (experimental).

View File

@@ -188,30 +188,35 @@ impl CodexMessageProcessor {
) -> Result<PluginReadResponse, JSONRPCErrorError> {
let plugins_manager = self.thread_manager.plugins_manager();
let PluginReadParams {
marketplace_path,
local_marketplace_path,
remote_marketplace_name,
plugin_name,
local_plugin_name,
remote_plugin_id,
} = params;
let read_source = match (marketplace_path, remote_marketplace_name) {
(Some(marketplace_path), None) => Ok(marketplace_path),
(None, Some(remote_marketplace_name)) => Err(remote_marketplace_name),
(Some(_), Some(_)) | (None, None) => {
return Err(invalid_request(
"plugin/read requires exactly one of marketplacePath or remoteMarketplaceName",
));
}
let read_source = plugin_request_source(
"plugin/read",
local_marketplace_path,
remote_marketplace_name,
local_plugin_name,
remote_plugin_id,
)?;
let config_cwd = match &read_source {
PluginRequestSource::Local {
marketplace_path, ..
} => marketplace_path.as_path().parent().map(Path::to_path_buf),
PluginRequestSource::Remote { .. } => None,
};
let config_cwd = read_source.as_ref().ok().and_then(|marketplace_path| {
marketplace_path.as_path().parent().map(Path::to_path_buf)
});
let config = self.load_latest_config(config_cwd).await?;
let plugins_input = config.plugins_config_input();
let plugin = match read_source {
Ok(marketplace_path) => {
PluginRequestSource::Local {
marketplace_path,
local_plugin_name,
} => {
let request = PluginReadRequest {
plugin_name,
plugin_name: local_plugin_name,
marketplace_path,
};
let outcome = plugins_manager
@@ -259,7 +264,10 @@ impl CodexMessageProcessor {
mcp_servers: outcome.plugin.mcp_server_names,
}
}
Err(remote_marketplace_name) => {
PluginRequestSource::Remote {
remote_marketplace_name,
remote_plugin_id,
} => {
if !config.features.enabled(Feature::Plugins)
|| !config.features.enabled(Feature::RemotePlugin)
{
@@ -271,12 +279,12 @@ impl CodexMessageProcessor {
let remote_plugin_service_config = RemotePluginServiceConfig {
chatgpt_base_url: config.chatgpt_base_url.clone(),
};
validate_remote_plugin_id(&plugin_name)?;
validate_remote_plugin_id(&remote_plugin_id)?;
let remote_detail = codex_core_plugins::remote::fetch_remote_plugin_detail(
&remote_plugin_service_config,
auth.as_ref(),
&remote_marketplace_name,
&plugin_name,
&remote_plugin_id,
)
.await
.map_err(|err| {
@@ -503,22 +511,31 @@ impl CodexMessageProcessor {
params: PluginInstallParams,
) -> Result<PluginInstallResponse, JSONRPCErrorError> {
let PluginInstallParams {
marketplace_path,
local_marketplace_path,
remote_marketplace_name,
plugin_name,
local_plugin_name,
remote_plugin_id,
} = params;
let marketplace_path = match (marketplace_path, remote_marketplace_name) {
(Some(marketplace_path), None) => marketplace_path,
(None, Some(remote_marketplace_name)) => {
let install_source = plugin_request_source(
"plugin/install",
local_marketplace_path,
remote_marketplace_name,
local_plugin_name,
remote_plugin_id,
)?;
let (marketplace_path, local_plugin_name) = match install_source {
PluginRequestSource::Local {
marketplace_path,
local_plugin_name,
} => (marketplace_path, local_plugin_name),
PluginRequestSource::Remote {
remote_marketplace_name,
remote_plugin_id,
} => {
return self
.remote_plugin_install_response(remote_marketplace_name, plugin_name)
.remote_plugin_install_response(remote_marketplace_name, remote_plugin_id)
.await;
}
(Some(_), Some(_)) | (None, None) => {
return Err(invalid_request(
"plugin/install requires exactly one of marketplacePath or remoteMarketplaceName",
));
}
};
let config_cwd = marketplace_path.as_path().parent().map(Path::to_path_buf);
let config = self.load_latest_config(config_cwd.clone()).await?;
@@ -535,7 +552,7 @@ impl CodexMessageProcessor {
let plugins_manager = self.thread_manager.plugins_manager();
let request = PluginInstallRequest {
plugin_name,
plugin_name: local_plugin_name,
marketplace_path,
};
@@ -907,13 +924,70 @@ impl CodexMessageProcessor {
}
}
fn is_valid_remote_uninstall_plugin_id(plugin_name: &str) -> bool {
is_valid_remote_plugin_id(plugin_name)
&& (plugin_name.starts_with("plugins~")
|| plugin_name.starts_with("plugins_")
|| plugin_name.starts_with("app_")
|| plugin_name.starts_with("asdk_app_")
|| plugin_name.starts_with("connector_"))
enum PluginRequestSource {
Local {
marketplace_path: AbsolutePathBuf,
local_plugin_name: String,
},
Remote {
remote_marketplace_name: String,
remote_plugin_id: String,
},
}
fn plugin_request_source(
method: &str,
local_marketplace_path: Option<AbsolutePathBuf>,
remote_marketplace_name: Option<String>,
local_plugin_name: Option<String>,
remote_plugin_id: Option<String>,
) -> Result<PluginRequestSource, JSONRPCErrorError> {
match (local_marketplace_path, remote_marketplace_name) {
(Some(marketplace_path), None) => {
if remote_plugin_id.is_some() {
return Err(invalid_request(format!(
"{method} with localMarketplacePath requires localPluginName, not remotePluginId"
)));
}
let Some(local_plugin_name) = local_plugin_name else {
return Err(invalid_request(format!(
"{method} with localMarketplacePath requires localPluginName"
)));
};
Ok(PluginRequestSource::Local {
marketplace_path,
local_plugin_name,
})
}
(None, Some(remote_marketplace_name)) => {
if local_plugin_name.is_some() {
return Err(invalid_request(format!(
"{method} with remoteMarketplaceName requires remotePluginId, not localPluginName"
)));
}
let Some(remote_plugin_id) = remote_plugin_id else {
return Err(invalid_request(format!(
"{method} with remoteMarketplaceName requires remotePluginId"
)));
};
Ok(PluginRequestSource::Remote {
remote_marketplace_name,
remote_plugin_id,
})
}
(Some(_), Some(_)) | (None, None) => Err(invalid_request(format!(
"{method} requires exactly one of localMarketplacePath or remoteMarketplaceName"
))),
}
}
fn is_valid_remote_uninstall_plugin_id(plugin_id: &str) -> bool {
is_valid_remote_plugin_id(plugin_id)
&& (plugin_id.starts_with("plugins~")
|| plugin_id.starts_with("plugins_")
|| plugin_id.starts_with("app_")
|| plugin_id.starts_with("asdk_app_")
|| plugin_id.starts_with("connector_"))
}
fn remote_marketplace_to_info(marketplace: RemoteMarketplace) -> PluginMarketplaceEntry {

View File

@@ -75,8 +75,8 @@ async fn plugin_install_rejects_relative_marketplace_paths() -> Result<()> {
.send_raw_request(
"plugin/install",
Some(serde_json::json!({
"marketplacePath": "relative-marketplace.json",
"pluginName": "missing-plugin",
"localMarketplacePath": "relative-marketplace.json",
"localPluginName": "missing-plugin",
})),
)
.await?;
@@ -100,9 +100,10 @@ async fn plugin_install_rejects_missing_install_source() -> Result<()> {
let request_id = mcp
.send_plugin_install_request(PluginInstallParams {
marketplace_path: None,
local_marketplace_path: None,
remote_marketplace_name: None,
plugin_name: "sample-plugin".to_string(),
local_plugin_name: Some("sample-plugin".to_string()),
remote_plugin_id: None,
})
.await?;
@@ -116,7 +117,7 @@ async fn plugin_install_rejects_missing_install_source() -> Result<()> {
assert!(
err.error
.message
.contains("requires exactly one of marketplacePath or remoteMarketplaceName")
.contains("requires exactly one of localMarketplacePath or remoteMarketplaceName")
);
Ok(())
}
@@ -129,11 +130,12 @@ async fn plugin_install_rejects_multiple_install_sources() -> Result<()> {
let request_id = mcp
.send_plugin_install_request(PluginInstallParams {
marketplace_path: Some(AbsolutePathBuf::try_from(
local_marketplace_path: Some(AbsolutePathBuf::try_from(
codex_home.path().join("marketplace.json"),
)?),
remote_marketplace_name: Some("openai-curated".to_string()),
plugin_name: "sample-plugin".to_string(),
local_plugin_name: None,
remote_plugin_id: Some("sample-plugin".to_string()),
})
.await?;
@@ -147,7 +149,7 @@ async fn plugin_install_rejects_multiple_install_sources() -> Result<()> {
assert!(
err.error
.message
.contains("requires exactly one of marketplacePath or remoteMarketplaceName")
.contains("requires exactly one of localMarketplacePath or remoteMarketplaceName")
);
Ok(())
}
@@ -160,9 +162,10 @@ async fn plugin_install_rejects_remote_marketplace_when_remote_plugin_is_disable
let request_id = mcp
.send_plugin_install_request(PluginInstallParams {
marketplace_path: None,
local_marketplace_path: None,
remote_marketplace_name: Some("chatgpt-global".to_string()),
plugin_name: "plugins~Plugin_22222222222222222222222222222222".to_string(),
local_plugin_name: None,
remote_plugin_id: Some("plugins~Plugin_22222222222222222222222222222222".to_string()),
})
.await?;
@@ -383,7 +386,7 @@ async fn plugin_install_rejects_invalid_remote_release_version() -> Result<()> {
}
#[tokio::test]
async fn plugin_install_rejects_invalid_remote_plugin_name() -> Result<()> {
async fn plugin_install_rejects_invalid_remote_plugin_id() -> Result<()> {
let codex_home = TempDir::new()?;
write_remote_plugin_catalog_config(codex_home.path(), "https://example.invalid/backend-api/")?;
let mut mcp = McpProcess::new(codex_home.path()).await?;
@@ -391,9 +394,10 @@ async fn plugin_install_rejects_invalid_remote_plugin_name() -> Result<()> {
let request_id = mcp
.send_plugin_install_request(PluginInstallParams {
marketplace_path: None,
local_marketplace_path: None,
remote_marketplace_name: Some("chatgpt-global".to_string()),
plugin_name: "linear/../../oops".to_string(),
local_plugin_name: None,
remote_plugin_id: Some("linear/../../oops".to_string()),
})
.await?;
@@ -514,9 +518,10 @@ async fn plugin_install_rejects_when_workspace_codex_plugins_disabled() -> Resul
let request_id = mcp
.send_plugin_install_request(PluginInstallParams {
marketplace_path: Some(marketplace_path),
local_marketplace_path: Some(marketplace_path),
remote_marketplace_name: None,
plugin_name: "sample-plugin".to_string(),
local_plugin_name: Some("sample-plugin".to_string()),
remote_plugin_id: None,
})
.await?;
@@ -543,11 +548,12 @@ async fn plugin_install_returns_invalid_request_for_missing_marketplace_file() -
let request_id = mcp
.send_plugin_install_request(PluginInstallParams {
marketplace_path: Some(AbsolutePathBuf::try_from(
local_marketplace_path: Some(AbsolutePathBuf::try_from(
codex_home.path().join("missing-marketplace.json"),
)?),
remote_marketplace_name: None,
plugin_name: "missing-plugin".to_string(),
local_plugin_name: Some("missing-plugin".to_string()),
remote_plugin_id: None,
})
.await?;
@@ -584,9 +590,10 @@ async fn plugin_install_returns_invalid_request_for_not_available_plugin() -> Re
let request_id = mcp
.send_plugin_install_request(PluginInstallParams {
marketplace_path: Some(marketplace_path),
local_marketplace_path: Some(marketplace_path),
remote_marketplace_name: None,
plugin_name: "sample-plugin".to_string(),
local_plugin_name: Some("sample-plugin".to_string()),
remote_plugin_id: None,
})
.await?;
@@ -634,9 +641,10 @@ async fn plugin_install_returns_invalid_request_for_disallowed_product_plugin()
let request_id = mcp
.send_plugin_install_request(PluginInstallParams {
marketplace_path: Some(marketplace_path),
local_marketplace_path: Some(marketplace_path),
remote_marketplace_name: None,
plugin_name: "sample-plugin".to_string(),
local_plugin_name: Some("sample-plugin".to_string()),
remote_plugin_id: None,
})
.await?;
@@ -683,9 +691,10 @@ async fn plugin_install_tracks_analytics_event() -> Result<()> {
let request_id = mcp
.send_plugin_install_request(PluginInstallParams {
marketplace_path: Some(marketplace_path),
local_marketplace_path: Some(marketplace_path),
remote_marketplace_name: None,
plugin_name: "sample-plugin".to_string(),
local_plugin_name: Some("sample-plugin".to_string()),
remote_plugin_id: None,
})
.await?;
let response: JSONRPCResponse = timeout(
@@ -890,9 +899,10 @@ async fn plugin_install_returns_apps_needing_auth() -> Result<()> {
let request_id = mcp
.send_plugin_install_request(PluginInstallParams {
marketplace_path: Some(marketplace_path),
local_marketplace_path: Some(marketplace_path),
remote_marketplace_name: None,
plugin_name: "sample-plugin".to_string(),
local_plugin_name: Some("sample-plugin".to_string()),
remote_plugin_id: None,
})
.await?;
@@ -974,9 +984,10 @@ async fn plugin_install_filters_disallowed_apps_needing_auth() -> Result<()> {
let request_id = mcp
.send_plugin_install_request(PluginInstallParams {
marketplace_path: Some(marketplace_path),
local_marketplace_path: Some(marketplace_path),
remote_marketplace_name: None,
plugin_name: "sample-plugin".to_string(),
local_plugin_name: Some("sample-plugin".to_string()),
remote_plugin_id: None,
})
.await?;
@@ -1041,9 +1052,10 @@ async fn plugin_install_makes_bundled_mcp_servers_available_to_followup_requests
let request_id = mcp
.send_plugin_install_request(PluginInstallParams {
marketplace_path: Some(marketplace_path),
local_marketplace_path: Some(marketplace_path),
remote_marketplace_name: None,
plugin_name: "sample-plugin".to_string(),
local_plugin_name: Some("sample-plugin".to_string()),
remote_plugin_id: None,
})
.await?;
let response: JSONRPCResponse = timeout(
@@ -1459,9 +1471,10 @@ async fn send_remote_plugin_install_request(
remote_plugin_id: &str,
) -> Result<i64> {
mcp.send_plugin_install_request(PluginInstallParams {
marketplace_path: None,
local_marketplace_path: None,
remote_marketplace_name: Some("caller-marketplace-is-ignored".to_string()),
plugin_name: remote_plugin_id.to_string(),
local_plugin_name: None,
remote_plugin_id: Some(remote_plugin_id.to_string()),
})
.await
}

View File

@@ -63,9 +63,10 @@ async fn plugin_read_rejects_missing_read_source() -> Result<()> {
let request_id = mcp
.send_plugin_read_request(PluginReadParams {
marketplace_path: None,
local_marketplace_path: None,
remote_marketplace_name: None,
plugin_name: "sample-plugin".to_string(),
local_plugin_name: Some("sample-plugin".to_string()),
remote_plugin_id: None,
})
.await?;
@@ -79,7 +80,7 @@ async fn plugin_read_rejects_missing_read_source() -> Result<()> {
assert!(
err.error
.message
.contains("requires exactly one of marketplacePath or remoteMarketplaceName")
.contains("requires exactly one of localMarketplacePath or remoteMarketplaceName")
);
Ok(())
}
@@ -92,11 +93,12 @@ async fn plugin_read_rejects_multiple_read_sources() -> Result<()> {
let request_id = mcp
.send_plugin_read_request(PluginReadParams {
marketplace_path: Some(AbsolutePathBuf::try_from(
local_marketplace_path: Some(AbsolutePathBuf::try_from(
codex_home.path().join("marketplace.json"),
)?),
remote_marketplace_name: Some("openai-curated".to_string()),
plugin_name: "sample-plugin".to_string(),
local_plugin_name: None,
remote_plugin_id: Some("sample-plugin".to_string()),
})
.await?;
@@ -110,7 +112,7 @@ async fn plugin_read_rejects_multiple_read_sources() -> Result<()> {
assert!(
err.error
.message
.contains("requires exactly one of marketplacePath or remoteMarketplaceName")
.contains("requires exactly one of localMarketplacePath or remoteMarketplaceName")
);
Ok(())
}
@@ -123,9 +125,10 @@ async fn plugin_read_rejects_remote_marketplace_when_remote_plugin_is_disabled()
let request_id = mcp
.send_plugin_read_request(PluginReadParams {
marketplace_path: None,
local_marketplace_path: None,
remote_marketplace_name: Some("chatgpt-global".to_string()),
plugin_name: "sample-plugin".to_string(),
local_plugin_name: None,
remote_plugin_id: Some("sample-plugin".to_string()),
})
.await?;
@@ -254,9 +257,10 @@ async fn plugin_read_reads_remote_plugin_details_when_remote_plugin_enabled() ->
let request_id = mcp
.send_plugin_read_request(PluginReadParams {
marketplace_path: None,
local_marketplace_path: None,
remote_marketplace_name: Some("chatgpt-global".to_string()),
plugin_name: "plugins~Plugin_00000000000000000000000000000000".to_string(),
local_plugin_name: None,
remote_plugin_id: Some("plugins~Plugin_00000000000000000000000000000000".to_string()),
})
.await?;
@@ -383,9 +387,10 @@ async fn plugin_read_maps_missing_remote_plugin_to_invalid_request() -> Result<(
let request_id = mcp
.send_plugin_read_request(PluginReadParams {
marketplace_path: None,
local_marketplace_path: None,
remote_marketplace_name: Some("chatgpt-global".to_string()),
plugin_name: "plugins~Plugin_missing".to_string(),
local_plugin_name: None,
remote_plugin_id: Some("plugins~Plugin_missing".to_string()),
})
.await?;
@@ -435,9 +440,10 @@ remote_plugin = true
let request_id = mcp
.send_plugin_read_request(PluginReadParams {
marketplace_path: None,
local_marketplace_path: None,
remote_marketplace_name: Some("chatgpt-global".to_string()),
plugin_name: "linear".to_string(),
local_plugin_name: None,
remote_plugin_id: Some("linear".to_string()),
})
.await?;
@@ -457,7 +463,7 @@ remote_plugin = true
}
#[tokio::test]
async fn plugin_read_rejects_invalid_remote_plugin_name() -> Result<()> {
async fn plugin_read_rejects_invalid_remote_plugin_id() -> Result<()> {
let codex_home = TempDir::new()?;
write_remote_plugin_catalog_config(codex_home.path(), "https://example.invalid/backend-api/")?;
let mut mcp = McpProcess::new(codex_home.path()).await?;
@@ -465,9 +471,10 @@ async fn plugin_read_rejects_invalid_remote_plugin_name() -> Result<()> {
let request_id = mcp
.send_plugin_read_request(PluginReadParams {
marketplace_path: None,
local_marketplace_path: None,
remote_marketplace_name: Some("chatgpt-global".to_string()),
plugin_name: "linear/../../oops".to_string(),
local_plugin_name: None,
remote_plugin_id: Some("linear/../../oops".to_string()),
})
.await?;
@@ -525,9 +532,10 @@ enabled = true
AbsolutePathBuf::try_from(repo_root.path().join(".agents/plugins/marketplace.json"))?;
let request_id = mcp
.send_plugin_read_request(PluginReadParams {
marketplace_path: Some(marketplace_path.clone()),
local_marketplace_path: Some(marketplace_path.clone()),
remote_marketplace_name: None,
plugin_name: "demo-plugin".to_string(),
local_plugin_name: Some("demo-plugin".to_string()),
remote_plugin_id: None,
})
.await?;
@@ -679,9 +687,10 @@ enabled = true
AbsolutePathBuf::try_from(repo_root.path().join(".agents/plugins/marketplace.json"))?;
let request_id = mcp
.send_plugin_read_request(PluginReadParams {
marketplace_path: Some(marketplace_path.clone()),
local_marketplace_path: Some(marketplace_path.clone()),
remote_marketplace_name: None,
plugin_name: "demo-plugin".to_string(),
local_plugin_name: Some("demo-plugin".to_string()),
remote_plugin_id: None,
})
.await?;
@@ -827,9 +836,10 @@ async fn plugin_read_returns_app_needs_auth() -> Result<()> {
let request_id = mcp
.send_plugin_read_request(PluginReadParams {
marketplace_path: Some(marketplace_path),
local_marketplace_path: Some(marketplace_path),
remote_marketplace_name: None,
plugin_name: "sample-plugin".to_string(),
local_plugin_name: Some("sample-plugin".to_string()),
remote_plugin_id: None,
})
.await?;
@@ -894,11 +904,12 @@ async fn plugin_read_accepts_legacy_string_default_prompt() -> Result<()> {
let request_id = mcp
.send_plugin_read_request(PluginReadParams {
marketplace_path: Some(AbsolutePathBuf::try_from(
local_marketplace_path: Some(AbsolutePathBuf::try_from(
repo_root.path().join(".agents/plugins/marketplace.json"),
)?),
remote_marketplace_name: None,
plugin_name: "demo-plugin".to_string(),
local_plugin_name: Some("demo-plugin".to_string()),
remote_plugin_id: None,
})
.await?;
@@ -956,11 +967,12 @@ async fn plugin_read_describes_uninstalled_git_source_without_cloning() -> Resul
let request_id = mcp
.send_plugin_read_request(PluginReadParams {
marketplace_path: Some(AbsolutePathBuf::try_from(
local_marketplace_path: Some(AbsolutePathBuf::try_from(
repo_root.path().join(".agents/plugins/marketplace.json"),
)?),
remote_marketplace_name: None,
plugin_name: "toolkit".to_string(),
local_plugin_name: Some("toolkit".to_string()),
remote_plugin_id: None,
})
.await?;
@@ -1019,11 +1031,12 @@ async fn plugin_read_returns_invalid_request_when_plugin_is_missing() -> Result<
let request_id = mcp
.send_plugin_read_request(PluginReadParams {
marketplace_path: Some(AbsolutePathBuf::try_from(
local_marketplace_path: Some(AbsolutePathBuf::try_from(
repo_root.path().join(".agents/plugins/marketplace.json"),
)?),
remote_marketplace_name: None,
plugin_name: "missing-plugin".to_string(),
local_plugin_name: Some("missing-plugin".to_string()),
remote_plugin_id: None,
})
.await?;
@@ -1072,11 +1085,12 @@ async fn plugin_read_returns_invalid_request_when_plugin_manifest_is_missing() -
let request_id = mcp
.send_plugin_read_request(PluginReadParams {
marketplace_path: Some(AbsolutePathBuf::try_from(
local_marketplace_path: Some(AbsolutePathBuf::try_from(
repo_root.path().join(".agents/plugins/marketplace.json"),
)?),
remote_marketplace_name: None,
plugin_name: "demo-plugin".to_string(),
local_plugin_name: Some("demo-plugin".to_string()),
remote_plugin_id: None,
})
.await?;

View File

@@ -733,9 +733,10 @@ pub(super) async fn fetch_plugin_install(
.request_typed(ClientRequest::PluginInstall {
request_id,
params: PluginInstallParams {
marketplace_path: Some(marketplace_path),
local_marketplace_path: Some(marketplace_path),
remote_marketplace_name: None,
plugin_name,
local_plugin_name: Some(plugin_name),
remote_plugin_id: None,
},
})
.await

View File

@@ -578,9 +578,10 @@ impl App {
app_server,
cwd,
PluginReadParams {
marketplace_path: Some(marketplace_path),
local_marketplace_path: Some(marketplace_path),
remote_marketplace_name: None,
plugin_name,
local_plugin_name: Some(plugin_name),
remote_plugin_id: None,
},
);
}

View File

@@ -1818,9 +1818,10 @@ impl ChatWidget {
tx.send(AppEvent::FetchPluginDetail {
cwd: cwd.clone(),
params: codex_app_server_protocol::PluginReadParams {
marketplace_path: Some(marketplace_path.clone()),
local_marketplace_path: Some(marketplace_path.clone()),
remote_marketplace_name: None,
plugin_name: plugin_name.clone(),
local_plugin_name: Some(plugin_name.clone()),
remote_plugin_id: None,
},
});
})]

View File

@@ -2428,17 +2428,23 @@ class PluginAuthPolicy(Enum):
on_use = "ON_USE"
class PluginAvailability(Enum):
disabled_by_admin = "DISABLED_BY_ADMIN"
available = "AVAILABLE"
class PluginInstallParams(BaseModel):
model_config = ConfigDict(
populate_by_name=True,
)
marketplace_path: Annotated[
AbsolutePathBuf | None, Field(alias="marketplacePath")
local_marketplace_path: Annotated[
AbsolutePathBuf | None, Field(alias="localMarketplacePath")
] = None
plugin_name: Annotated[str, Field(alias="pluginName")]
local_plugin_name: Annotated[str | None, Field(alias="localPluginName")] = None
remote_marketplace_name: Annotated[
str | None, Field(alias="remoteMarketplaceName")
] = None
remote_plugin_id: Annotated[str | None, Field(alias="remotePluginId")] = None
class PluginInstallPolicy(Enum):
@@ -2531,13 +2537,14 @@ class PluginReadParams(BaseModel):
model_config = ConfigDict(
populate_by_name=True,
)
marketplace_path: Annotated[
AbsolutePathBuf | None, Field(alias="marketplacePath")
local_marketplace_path: Annotated[
AbsolutePathBuf | None, Field(alias="localMarketplacePath")
] = None
plugin_name: Annotated[str, Field(alias="pluginName")]
local_plugin_name: Annotated[str | None, Field(alias="localPluginName")] = None
remote_marketplace_name: Annotated[
str | None, Field(alias="remoteMarketplaceName")
] = None
remote_plugin_id: Annotated[str | None, Field(alias="remotePluginId")] = None
class PluginShareDeleteParams(BaseModel):
@@ -2631,6 +2638,10 @@ class PluginSummary(BaseModel):
populate_by_name=True,
)
auth_policy: Annotated[PluginAuthPolicy, Field(alias="authPolicy")]
availability: Annotated[
PluginAvailability | None,
Field(description="Availability state for installing and using the plugin."),
] = "AVAILABLE"
enabled: bool
id: str
install_policy: Annotated[PluginInstallPolicy, Field(alias="installPolicy")]
@@ -6443,11 +6454,22 @@ class PluginReadResponse(BaseModel):
plugin: PluginDetail
class PluginShareListItem(BaseModel):
model_config = ConfigDict(
populate_by_name=True,
)
local_plugin_path: Annotated[
AbsolutePathBuf | None, Field(alias="localPluginPath")
] = None
plugin: PluginSummary
share_url: Annotated[str, Field(alias="shareUrl")]
class PluginShareListResponse(BaseModel):
model_config = ConfigDict(
populate_by_name=True,
)
data: list[PluginSummary]
data: list[PluginShareListItem]
class RateLimitSnapshot(BaseModel):