mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
Surface admin-disabled remote plugin status (#20298)
## Summary Remote plugin-service returns plugin availability separately from a user's installed/enabled state. This adds `PluginAvailabilityStatus` to the app-server protocol, propagates remote catalog `status` into `PluginSummary`, and rejects install attempts for remote plugins marked `DISABLED_BY_ADMIN` before downloading or caching the bundle. This is the `openai/codex` half of the change. The companion `openai/openai` webview PR is https://github.com/openai/openai/pull/873269. ## Validation - `cargo run -p codex-app-server-protocol --bin write_schema_fixtures` - `cargo test -p codex-app-server --test all plugin_list_marks_remote_plugin_disabled_by_admin` - `cargo test -p codex-app-server --test all plugin_list_includes_remote_marketplaces_when_remote_plugin_enabled` - `cargo test -p codex-app-server --test all plugin_install_rejects_remote_plugin_disabled_by_admin_before_download` - `cargo test -p codex-app-server-protocol schema_fixtures`
This commit is contained in:
committed by
GitHub
Unverified
parent
c39824c2fd
commit
bb60b78c46
+26
@@ -11932,6 +11932,23 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"PluginAvailability": {
|
||||
"oneOf": [
|
||||
{
|
||||
"enum": [
|
||||
"DISABLED_BY_ADMIN"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "Plugin-service currently sends `\"ENABLED\"` for available remote plugins. Codex app-server exposes `\"AVAILABLE\"` in its API; the alias keeps decoding compatible with that upstream response.",
|
||||
"enum": [
|
||||
"AVAILABLE"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"PluginDetail": {
|
||||
"properties": {
|
||||
"apps": {
|
||||
@@ -12458,6 +12475,15 @@
|
||||
"authPolicy": {
|
||||
"$ref": "#/definitions/v2/PluginAuthPolicy"
|
||||
},
|
||||
"availability": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/v2/PluginAvailability"
|
||||
}
|
||||
],
|
||||
"default": "AVAILABLE",
|
||||
"description": "Availability state for installing and using the plugin."
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
|
||||
+26
@@ -8585,6 +8585,23 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"PluginAvailability": {
|
||||
"oneOf": [
|
||||
{
|
||||
"enum": [
|
||||
"DISABLED_BY_ADMIN"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "Plugin-service currently sends `\"ENABLED\"` for available remote plugins. Codex app-server exposes `\"AVAILABLE\"` in its API; the alias keeps decoding compatible with that upstream response.",
|
||||
"enum": [
|
||||
"AVAILABLE"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"PluginDetail": {
|
||||
"properties": {
|
||||
"apps": {
|
||||
@@ -9111,6 +9128,15 @@
|
||||
"authPolicy": {
|
||||
"$ref": "#/definitions/PluginAuthPolicy"
|
||||
},
|
||||
"availability": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/PluginAvailability"
|
||||
}
|
||||
],
|
||||
"default": "AVAILABLE",
|
||||
"description": "Availability state for installing and using the plugin."
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
|
||||
@@ -38,6 +38,23 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"PluginAvailability": {
|
||||
"oneOf": [
|
||||
{
|
||||
"enum": [
|
||||
"DISABLED_BY_ADMIN"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "Plugin-service currently sends `\"ENABLED\"` for available remote plugins. Codex app-server exposes `\"AVAILABLE\"` in its API; the alias keeps decoding compatible with that upstream response.",
|
||||
"enum": [
|
||||
"AVAILABLE"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"PluginInstallPolicy": {
|
||||
"enum": [
|
||||
"NOT_AVAILABLE",
|
||||
@@ -299,6 +316,15 @@
|
||||
"authPolicy": {
|
||||
"$ref": "#/definitions/PluginAuthPolicy"
|
||||
},
|
||||
"availability": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/PluginAvailability"
|
||||
}
|
||||
],
|
||||
"default": "AVAILABLE",
|
||||
"description": "Availability state for installing and using the plugin."
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
|
||||
@@ -44,6 +44,23 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"PluginAvailability": {
|
||||
"oneOf": [
|
||||
{
|
||||
"enum": [
|
||||
"DISABLED_BY_ADMIN"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "Plugin-service currently sends `\"ENABLED\"` for available remote plugins. Codex app-server exposes `\"AVAILABLE\"` in its API; the alias keeps decoding compatible with that upstream response.",
|
||||
"enum": [
|
||||
"AVAILABLE"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"PluginDetail": {
|
||||
"properties": {
|
||||
"apps": {
|
||||
@@ -318,6 +335,15 @@
|
||||
"authPolicy": {
|
||||
"$ref": "#/definitions/PluginAuthPolicy"
|
||||
},
|
||||
"availability": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/PluginAvailability"
|
||||
}
|
||||
],
|
||||
"default": "AVAILABLE",
|
||||
"description": "Availability state for installing and using the plugin."
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
|
||||
@@ -12,6 +12,23 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"PluginAvailability": {
|
||||
"oneOf": [
|
||||
{
|
||||
"enum": [
|
||||
"DISABLED_BY_ADMIN"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "Plugin-service currently sends `\"ENABLED\"` for available remote plugins. Codex app-server exposes `\"AVAILABLE\"` in its API; the alias keeps decoding compatible with that upstream response.",
|
||||
"enum": [
|
||||
"AVAILABLE"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"PluginInstallPolicy": {
|
||||
"enum": [
|
||||
"NOT_AVAILABLE",
|
||||
@@ -234,6 +251,15 @@
|
||||
"authPolicy": {
|
||||
"$ref": "#/definitions/PluginAuthPolicy"
|
||||
},
|
||||
"availability": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/PluginAvailability"
|
||||
}
|
||||
],
|
||||
"default": "AVAILABLE",
|
||||
"description": "Availability state for installing and using the plugin."
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type PluginAvailability = "AVAILABLE" | "DISABLED_BY_ADMIN";
|
||||
@@ -2,8 +2,13 @@
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { PluginAuthPolicy } from "./PluginAuthPolicy";
|
||||
import type { PluginAvailability } from "./PluginAvailability";
|
||||
import type { PluginInstallPolicy } from "./PluginInstallPolicy";
|
||||
import type { PluginInterface } from "./PluginInterface";
|
||||
import type { PluginSource } from "./PluginSource";
|
||||
|
||||
export type PluginSummary = { id: string, name: string, source: PluginSource, installed: boolean, enabled: boolean, installPolicy: PluginInstallPolicy, authPolicy: PluginAuthPolicy, interface: PluginInterface | null, };
|
||||
export type PluginSummary = { id: string, name: string, source: PluginSource, installed: boolean, enabled: boolean, installPolicy: PluginInstallPolicy, authPolicy: PluginAuthPolicy,
|
||||
/**
|
||||
* Availability state for installing and using the plugin.
|
||||
*/
|
||||
availability: PluginAvailability, interface: PluginInterface | null, };
|
||||
|
||||
@@ -270,6 +270,7 @@ export type { PermissionsRequestApprovalParams } from "./PermissionsRequestAppro
|
||||
export type { PermissionsRequestApprovalResponse } from "./PermissionsRequestApprovalResponse";
|
||||
export type { PlanDeltaNotification } from "./PlanDeltaNotification";
|
||||
export type { PluginAuthPolicy } from "./PluginAuthPolicy";
|
||||
export type { PluginAvailability } from "./PluginAvailability";
|
||||
export type { PluginDetail } from "./PluginDetail";
|
||||
export type { PluginInstallParams } from "./PluginInstallParams";
|
||||
export type { PluginInstallPolicy } from "./PluginInstallPolicy";
|
||||
|
||||
@@ -4827,6 +4827,21 @@ pub enum PluginAuthPolicy {
|
||||
OnUse,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Default, JsonSchema, TS)]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub enum PluginAvailability {
|
||||
/// Plugin-service currently sends `"ENABLED"` for available remote plugins.
|
||||
/// Codex app-server exposes `"AVAILABLE"` in its API; the alias keeps
|
||||
/// decoding compatible with that upstream response.
|
||||
#[serde(rename = "AVAILABLE", alias = "ENABLED")]
|
||||
#[ts(rename = "AVAILABLE")]
|
||||
#[default]
|
||||
Available,
|
||||
#[serde(rename = "DISABLED_BY_ADMIN")]
|
||||
#[ts(rename = "DISABLED_BY_ADMIN")]
|
||||
DisabledByAdmin,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
@@ -4838,6 +4853,9 @@ pub struct PluginSummary {
|
||||
pub enabled: bool,
|
||||
pub install_policy: PluginInstallPolicy,
|
||||
pub auth_policy: PluginAuthPolicy,
|
||||
/// Availability state for installing and using the plugin.
|
||||
#[serde(default)]
|
||||
pub availability: PluginAvailability,
|
||||
pub interface: Option<PluginInterface>,
|
||||
}
|
||||
|
||||
@@ -10725,6 +10743,7 @@ mod tests {
|
||||
enabled: false,
|
||||
install_policy: PluginInstallPolicy::Available,
|
||||
auth_policy: PluginAuthPolicy::OnUse,
|
||||
availability: PluginAvailability::Available,
|
||||
interface: None,
|
||||
}],
|
||||
})
|
||||
@@ -10738,12 +10757,41 @@ mod tests {
|
||||
"enabled": false,
|
||||
"installPolicy": "AVAILABLE",
|
||||
"authPolicy": "ON_USE",
|
||||
"availability": "AVAILABLE",
|
||||
"interface": null,
|
||||
}],
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plugin_summary_defaults_missing_availability_to_available() {
|
||||
let summary: PluginSummary = serde_json::from_value(json!({
|
||||
"id": "plugins~Plugin_00000000000000000000000000000000",
|
||||
"name": "gmail",
|
||||
"source": { "type": "remote" },
|
||||
"installed": false,
|
||||
"enabled": false,
|
||||
"installPolicy": "AVAILABLE",
|
||||
"authPolicy": "ON_USE",
|
||||
"interface": null,
|
||||
}))
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(summary.availability, PluginAvailability::Available);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plugin_availability_deserializes_enabled_alias() {
|
||||
let availability: PluginAvailability = serde_json::from_value(json!("ENABLED")).unwrap();
|
||||
|
||||
assert_eq!(availability, PluginAvailability::Available);
|
||||
assert_eq!(
|
||||
serde_json::to_value(availability).unwrap(),
|
||||
json!("AVAILABLE")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plugin_uninstall_params_serialization_omits_force_remote_sync() {
|
||||
assert_eq!(
|
||||
|
||||
@@ -201,7 +201,7 @@ Example with notification opt-out:
|
||||
- `marketplace/add` — add a remote plugin marketplace from an HTTP(S) Git URL, SSH Git URL, or GitHub `owner/repo` shorthand, then persist it into the user marketplace config. Returns the installed root path plus whether the marketplace was already present.
|
||||
- `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, 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/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**).
|
||||
- `skills/changed` — notification emitted when watched local skill files change.
|
||||
- `app/list` — list available apps.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use super::*;
|
||||
use crate::error_code::internal_error;
|
||||
use crate::error_code::invalid_request;
|
||||
use codex_app_server_protocol::PluginAvailability;
|
||||
use codex_app_server_protocol::PluginInstallPolicy;
|
||||
|
||||
impl CodexMessageProcessor {
|
||||
@@ -77,6 +78,7 @@ impl CodexMessageProcessor {
|
||||
source: marketplace_plugin_source_to_info(plugin.source),
|
||||
install_policy: plugin.policy.installation.into(),
|
||||
auth_policy: plugin.policy.authentication.into(),
|
||||
availability: PluginAvailability::Available,
|
||||
interface: plugin.interface.map(local_plugin_interface_to_info),
|
||||
})
|
||||
.collect(),
|
||||
@@ -243,6 +245,7 @@ impl CodexMessageProcessor {
|
||||
enabled: outcome.plugin.enabled,
|
||||
install_policy: outcome.plugin.policy.installation.into(),
|
||||
auth_policy: outcome.plugin.policy.authentication.into(),
|
||||
availability: PluginAvailability::Available,
|
||||
interface: outcome.plugin.interface.map(local_plugin_interface_to_info),
|
||||
},
|
||||
description: outcome.plugin.description,
|
||||
@@ -537,6 +540,12 @@ impl CodexMessageProcessor {
|
||||
"read remote plugin details before install",
|
||||
)
|
||||
})?;
|
||||
if remote_detail.summary.availability == PluginAvailability::DisabledByAdmin {
|
||||
let remote_plugin_id = &remote_detail.summary.id;
|
||||
return Err(invalid_request(format!(
|
||||
"remote plugin {remote_plugin_id} is disabled by admin"
|
||||
)));
|
||||
}
|
||||
if remote_detail.summary.install_policy == PluginInstallPolicy::NotAvailable {
|
||||
return Err(invalid_request(format!(
|
||||
"remote plugin {remote_plugin_id} is not available for install"
|
||||
@@ -859,6 +868,7 @@ fn remote_plugin_summary_to_info(summary: RemoteCatalogPluginSummary) -> PluginS
|
||||
enabled: summary.enabled,
|
||||
install_policy: summary.install_policy,
|
||||
auth_policy: summary.auth_policy,
|
||||
availability: summary.availability,
|
||||
interface: summary.interface,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ use codex_app_server_protocol::AppInfo;
|
||||
use codex_app_server_protocol::AppSummary;
|
||||
use codex_app_server_protocol::JSONRPCResponse;
|
||||
use codex_app_server_protocol::PluginAuthPolicy;
|
||||
use codex_app_server_protocol::PluginAvailability;
|
||||
use codex_app_server_protocol::PluginInstallParams;
|
||||
use codex_app_server_protocol::PluginInstallResponse;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
@@ -407,6 +408,66 @@ async fn plugin_install_rejects_invalid_remote_plugin_name() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugin_install_rejects_remote_plugin_disabled_by_admin_before_download() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let server = MockServer::start().await;
|
||||
let bundle_url = mount_remote_plugin_bundle(
|
||||
&server,
|
||||
/*status_code*/ 200,
|
||||
remote_plugin_bundle_tar_gz_bytes("linear")?,
|
||||
)
|
||||
.await;
|
||||
configure_remote_plugin_test(codex_home.path(), &server)?;
|
||||
mount_remote_plugin_detail_with_status(
|
||||
&server,
|
||||
REMOTE_PLUGIN_ID,
|
||||
"1.2.3",
|
||||
Some(&bundle_url),
|
||||
PluginAvailability::DisabledByAdmin,
|
||||
)
|
||||
.await;
|
||||
mount_empty_remote_installed_plugins(&server).await;
|
||||
|
||||
let mut mcp = McpProcess::new_with_env(
|
||||
codex_home.path(),
|
||||
&[(TEST_ALLOW_HTTP_REMOTE_PLUGIN_BUNDLE_DOWNLOADS, Some("1"))],
|
||||
)
|
||||
.await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = send_remote_plugin_install_request(&mut mcp, REMOTE_PLUGIN_ID).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("disabled by admin"));
|
||||
wait_for_remote_plugin_request_count(
|
||||
&server,
|
||||
"GET",
|
||||
"/bundles/linear.tar.gz",
|
||||
/*expected_count*/ 0,
|
||||
)
|
||||
.await?;
|
||||
wait_for_remote_plugin_request_count(
|
||||
&server,
|
||||
"POST",
|
||||
&format!("/ps/plugins/{REMOTE_PLUGIN_ID}/install"),
|
||||
/*expected_count*/ 0,
|
||||
)
|
||||
.await?;
|
||||
assert!(
|
||||
!codex_home
|
||||
.path()
|
||||
.join("plugins/cache/chatgpt-global/linear")
|
||||
.exists()
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugin_install_rejects_when_workspace_codex_plugins_disabled() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
@@ -1272,6 +1333,27 @@ async fn mount_remote_plugin_detail(
|
||||
release_version: &str,
|
||||
bundle_download_url: Option<&str>,
|
||||
) {
|
||||
mount_remote_plugin_detail_with_status(
|
||||
server,
|
||||
remote_plugin_id,
|
||||
release_version,
|
||||
bundle_download_url,
|
||||
PluginAvailability::Available,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn mount_remote_plugin_detail_with_status(
|
||||
server: &MockServer,
|
||||
remote_plugin_id: &str,
|
||||
release_version: &str,
|
||||
bundle_download_url: Option<&str>,
|
||||
status: PluginAvailability,
|
||||
) {
|
||||
let status = match status {
|
||||
PluginAvailability::Available => "ENABLED",
|
||||
PluginAvailability::DisabledByAdmin => "DISABLED_BY_ADMIN",
|
||||
};
|
||||
let bundle_download_url_field = bundle_download_url
|
||||
.map(|url| format!(r#" "bundle_download_url": "{url}","#))
|
||||
.unwrap_or_default();
|
||||
@@ -1282,6 +1364,7 @@ async fn mount_remote_plugin_detail(
|
||||
"scope": "GLOBAL",
|
||||
"installation_policy": "AVAILABLE",
|
||||
"authentication_policy": "ON_USE",
|
||||
"status": "{status}",
|
||||
"release": {{
|
||||
"version": "{release_version}",
|
||||
{bundle_download_url_field}
|
||||
|
||||
@@ -244,6 +244,7 @@ async fn plugin_list_keeps_valid_marketplaces_when_another_marketplace_fails_to_
|
||||
enabled: false,
|
||||
install_policy: PluginInstallPolicy::Available,
|
||||
auth_policy: PluginAuthPolicy::OnInstall,
|
||||
availability: codex_app_server_protocol::PluginAvailability::Available,
|
||||
interface: None,
|
||||
}],
|
||||
}]
|
||||
@@ -527,6 +528,7 @@ async fn plugin_list_uses_alternate_discoverable_manifest_and_keeps_undiscoverab
|
||||
enabled: false,
|
||||
install_policy: PluginInstallPolicy::Available,
|
||||
auth_policy: PluginAuthPolicy::OnInstall,
|
||||
availability: codex_app_server_protocol::PluginAvailability::Available,
|
||||
interface: Some(codex_app_server_protocol::PluginInterface {
|
||||
display_name: Some("Valid Plugin".to_string()),
|
||||
short_description: None,
|
||||
@@ -559,6 +561,7 @@ async fn plugin_list_uses_alternate_discoverable_manifest_and_keeps_undiscoverab
|
||||
enabled: false,
|
||||
install_policy: PluginInstallPolicy::Available,
|
||||
auth_policy: PluginAuthPolicy::OnInstall,
|
||||
availability: codex_app_server_protocol::PluginAvailability::Available,
|
||||
interface: None,
|
||||
},
|
||||
],
|
||||
@@ -1287,6 +1290,7 @@ async fn plugin_list_includes_remote_marketplaces_when_remote_plugin_enabled() -
|
||||
"scope": "GLOBAL",
|
||||
"installation_policy": "AVAILABLE",
|
||||
"authentication_policy": "ON_USE",
|
||||
"status": "ENABLED",
|
||||
"release": {
|
||||
"display_name": "Linear",
|
||||
"description": "Track work in Linear",
|
||||
@@ -1321,6 +1325,7 @@ async fn plugin_list_includes_remote_marketplaces_when_remote_plugin_enabled() -
|
||||
"scope": "GLOBAL",
|
||||
"installation_policy": "AVAILABLE",
|
||||
"authentication_policy": "ON_USE",
|
||||
"status": "ENABLED",
|
||||
"release": {
|
||||
"display_name": "Linear",
|
||||
"description": "Track work in Linear",
|
||||
@@ -1414,6 +1419,10 @@ async fn plugin_list_includes_remote_marketplaces_when_remote_plugin_enabled() -
|
||||
assert_eq!(remote_marketplace.plugins[0].source, PluginSource::Remote);
|
||||
assert_eq!(remote_marketplace.plugins[0].installed, true);
|
||||
assert_eq!(remote_marketplace.plugins[0].enabled, true);
|
||||
assert_eq!(
|
||||
remote_marketplace.plugins[0].availability,
|
||||
codex_app_server_protocol::PluginAvailability::Available
|
||||
);
|
||||
assert_eq!(
|
||||
remote_marketplace.plugins[0]
|
||||
.interface
|
||||
@@ -1425,6 +1434,138 @@ async fn plugin_list_includes_remote_marketplaces_when_remote_plugin_enabled() -
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugin_list_marks_remote_plugin_disabled_by_admin() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let server = MockServer::start().await;
|
||||
write_remote_plugin_catalog_config(
|
||||
codex_home.path(),
|
||||
&format!("{}/backend-api/", server.uri()),
|
||||
)?;
|
||||
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 global_directory_body = r#"{
|
||||
"plugins": [
|
||||
{
|
||||
"id": "plugins~Plugin_00000000000000000000000000000000",
|
||||
"name": "linear",
|
||||
"scope": "GLOBAL",
|
||||
"installation_policy": "AVAILABLE",
|
||||
"authentication_policy": "ON_USE",
|
||||
"status": "DISABLED_BY_ADMIN",
|
||||
"release": {
|
||||
"display_name": "Linear",
|
||||
"description": "Track work in Linear",
|
||||
"app_ids": [],
|
||||
"interface": {},
|
||||
"skills": []
|
||||
}
|
||||
}
|
||||
],
|
||||
"pagination": {
|
||||
"limit": 50,
|
||||
"next_page_token": null
|
||||
}
|
||||
}"#;
|
||||
let global_installed_body = r#"{
|
||||
"plugins": [
|
||||
{
|
||||
"id": "plugins~Plugin_00000000000000000000000000000000",
|
||||
"name": "linear",
|
||||
"scope": "GLOBAL",
|
||||
"installation_policy": "AVAILABLE",
|
||||
"authentication_policy": "ON_USE",
|
||||
"status": "DISABLED_BY_ADMIN",
|
||||
"release": {
|
||||
"display_name": "Linear",
|
||||
"description": "Track work in Linear",
|
||||
"app_ids": [],
|
||||
"interface": {},
|
||||
"skills": []
|
||||
},
|
||||
"enabled": true,
|
||||
"disabled_skill_names": []
|
||||
}
|
||||
],
|
||||
"pagination": {
|
||||
"limit": 50,
|
||||
"next_page_token": null
|
||||
}
|
||||
}"#;
|
||||
let empty_page_body = r#"{
|
||||
"plugins": [],
|
||||
"pagination": {
|
||||
"limit": 50,
|
||||
"next_page_token": null
|
||||
}
|
||||
}"#;
|
||||
|
||||
for (scope, body) in [
|
||||
("GLOBAL", global_directory_body),
|
||||
("WORKSPACE", empty_page_body),
|
||||
] {
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/backend-api/ps/plugins/list"))
|
||||
.and(query_param("scope", scope))
|
||||
.and(query_param("limit", "200"))
|
||||
.and(header("authorization", "Bearer chatgpt-token"))
|
||||
.and(header("chatgpt-account-id", "account-123"))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_string(body))
|
||||
.mount(&server)
|
||||
.await;
|
||||
}
|
||||
for (scope, body) in [
|
||||
("GLOBAL", global_installed_body),
|
||||
("WORKSPACE", empty_page_body),
|
||||
] {
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/backend-api/ps/plugins/installed"))
|
||||
.and(query_param("scope", scope))
|
||||
.and(header("authorization", "Bearer chatgpt-token"))
|
||||
.and(header("chatgpt-account-id", "account-123"))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_string(body))
|
||||
.mount(&server)
|
||||
.await;
|
||||
}
|
||||
|
||||
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: None })
|
||||
.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 remote_marketplace = response
|
||||
.marketplaces
|
||||
.into_iter()
|
||||
.find(|marketplace| marketplace.name == "chatgpt-global")
|
||||
.expect("expected ChatGPT remote marketplace");
|
||||
let plugin = remote_marketplace
|
||||
.plugins
|
||||
.first()
|
||||
.expect("expected remote plugin");
|
||||
assert_eq!(plugin.installed, true);
|
||||
assert_eq!(plugin.enabled, true);
|
||||
assert_eq!(
|
||||
plugin.availability,
|
||||
codex_app_server_protocol::PluginAvailability::DisabledByAdmin
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugin_list_remote_marketplace_replaces_local_marketplace_with_same_name() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
|
||||
@@ -177,6 +177,7 @@ async fn plugin_share_list_returns_created_workspace_plugins() -> Result<()> {
|
||||
enabled: true,
|
||||
install_policy: PluginInstallPolicy::Available,
|
||||
auth_policy: PluginAuthPolicy::OnUse,
|
||||
availability: codex_app_server_protocol::PluginAvailability::Available,
|
||||
interface: Some(expected_plugin_interface()),
|
||||
}],
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use crate::store::PLUGINS_CACHE_DIR;
|
||||
use crate::store::PluginStore;
|
||||
use codex_app_server_protocol::PluginAuthPolicy;
|
||||
use codex_app_server_protocol::PluginAvailability;
|
||||
use codex_app_server_protocol::PluginInstallPolicy;
|
||||
use codex_app_server_protocol::PluginInterface;
|
||||
use codex_app_server_protocol::SkillInterface;
|
||||
@@ -67,6 +68,7 @@ pub struct RemotePluginSummary {
|
||||
pub enabled: bool,
|
||||
pub install_policy: PluginInstallPolicy,
|
||||
pub auth_policy: PluginAuthPolicy,
|
||||
pub availability: PluginAvailability,
|
||||
pub interface: Option<PluginInterface>,
|
||||
}
|
||||
|
||||
@@ -265,6 +267,8 @@ struct RemotePluginDirectoryItem {
|
||||
scope: RemotePluginScope,
|
||||
installation_policy: PluginInstallPolicy,
|
||||
authentication_policy: PluginAuthPolicy,
|
||||
#[serde(rename = "status", default)]
|
||||
availability: PluginAvailability,
|
||||
release: RemotePluginReleaseResponse,
|
||||
}
|
||||
|
||||
@@ -661,6 +665,7 @@ fn build_remote_plugin_summary(
|
||||
enabled: installed_plugin.is_some_and(|plugin| plugin.enabled),
|
||||
install_policy: plugin.installation_policy,
|
||||
auth_policy: plugin.authentication_policy,
|
||||
availability: plugin.availability,
|
||||
interface: remote_plugin_interface_to_info(plugin),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -381,6 +381,7 @@ async fn list_remote_plugin_shares_fetches_created_workspace_plugins() {
|
||||
enabled: false,
|
||||
install_policy: PluginInstallPolicy::Available,
|
||||
auth_policy: PluginAuthPolicy::OnUse,
|
||||
availability: PluginAvailability::Available,
|
||||
interface: Some(expected_plugin_interface()),
|
||||
},
|
||||
RemotePluginSummary {
|
||||
@@ -390,6 +391,7 @@ async fn list_remote_plugin_shares_fetches_created_workspace_plugins() {
|
||||
enabled: true,
|
||||
install_policy: PluginInstallPolicy::Available,
|
||||
auth_policy: PluginAuthPolicy::OnUse,
|
||||
availability: PluginAvailability::Available,
|
||||
interface: Some(expected_plugin_interface()),
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use super::*;
|
||||
use codex_app_server_protocol::PluginAvailability;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
pub(super) async fn test_config() -> Config {
|
||||
@@ -1422,6 +1423,7 @@ pub(super) fn plugins_test_summary(
|
||||
enabled,
|
||||
install_policy,
|
||||
auth_policy: PluginAuthPolicy::OnInstall,
|
||||
availability: PluginAvailability::Available,
|
||||
interface: Some(plugins_test_interface(
|
||||
display_name,
|
||||
description,
|
||||
|
||||
Reference in New Issue
Block a user