mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
chore: wire through plugin policies + category from marketplace.json (#14305)
wire plugin marketplace metadata through app-server endpoints: - `plugin/list` has `installPolicy` and `authPolicy` - `plugin/install` has plugin-level `authPolicy` `plugin/install` also now enforces `NOT_AVAILABLE` `installPolicy` when installing. added tests.
This commit is contained in:
committed by
Michael Bolin
Unverified
parent
fa1242c83b
commit
7b2cee53db
@@ -12749,6 +12749,13 @@
|
|||||||
],
|
],
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"PluginAuthPolicy": {
|
||||||
|
"enum": [
|
||||||
|
"ON_INSTALL",
|
||||||
|
"ON_USE"
|
||||||
|
],
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"PluginInstallParams": {
|
"PluginInstallParams": {
|
||||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -12766,6 +12773,14 @@
|
|||||||
"title": "PluginInstallParams",
|
"title": "PluginInstallParams",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
|
"PluginInstallPolicy": {
|
||||||
|
"enum": [
|
||||||
|
"NOT_AVAILABLE",
|
||||||
|
"AVAILABLE",
|
||||||
|
"INSTALLED_BY_DEFAULT"
|
||||||
|
],
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"PluginInstallResponse": {
|
"PluginInstallResponse": {
|
||||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -12774,6 +12789,16 @@
|
|||||||
"$ref": "#/definitions/v2/AppSummary"
|
"$ref": "#/definitions/v2/AppSummary"
|
||||||
},
|
},
|
||||||
"type": "array"
|
"type": "array"
|
||||||
|
},
|
||||||
|
"authPolicy": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/v2/PluginAuthPolicy"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
@@ -12974,12 +12999,32 @@
|
|||||||
},
|
},
|
||||||
"PluginSummary": {
|
"PluginSummary": {
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"authPolicy": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/v2/PluginAuthPolicy"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
"enabled": {
|
"enabled": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
"id": {
|
"id": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"installPolicy": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/v2/PluginInstallPolicy"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
"installed": {
|
"installed": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -9097,6 +9097,13 @@
|
|||||||
],
|
],
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"PluginAuthPolicy": {
|
||||||
|
"enum": [
|
||||||
|
"ON_INSTALL",
|
||||||
|
"ON_USE"
|
||||||
|
],
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"PluginInstallParams": {
|
"PluginInstallParams": {
|
||||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -9114,6 +9121,14 @@
|
|||||||
"title": "PluginInstallParams",
|
"title": "PluginInstallParams",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
|
"PluginInstallPolicy": {
|
||||||
|
"enum": [
|
||||||
|
"NOT_AVAILABLE",
|
||||||
|
"AVAILABLE",
|
||||||
|
"INSTALLED_BY_DEFAULT"
|
||||||
|
],
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"PluginInstallResponse": {
|
"PluginInstallResponse": {
|
||||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -9122,6 +9137,16 @@
|
|||||||
"$ref": "#/definitions/AppSummary"
|
"$ref": "#/definitions/AppSummary"
|
||||||
},
|
},
|
||||||
"type": "array"
|
"type": "array"
|
||||||
|
},
|
||||||
|
"authPolicy": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/PluginAuthPolicy"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
@@ -9322,12 +9347,32 @@
|
|||||||
},
|
},
|
||||||
"PluginSummary": {
|
"PluginSummary": {
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"authPolicy": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/PluginAuthPolicy"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
"enabled": {
|
"enabled": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
"id": {
|
"id": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"installPolicy": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/PluginInstallPolicy"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
"installed": {
|
"installed": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -28,6 +28,13 @@
|
|||||||
"name"
|
"name"
|
||||||
],
|
],
|
||||||
"type": "object"
|
"type": "object"
|
||||||
|
},
|
||||||
|
"PluginAuthPolicy": {
|
||||||
|
"enum": [
|
||||||
|
"ON_INSTALL",
|
||||||
|
"ON_USE"
|
||||||
|
],
|
||||||
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -36,6 +43,16 @@
|
|||||||
"$ref": "#/definitions/AppSummary"
|
"$ref": "#/definitions/AppSummary"
|
||||||
},
|
},
|
||||||
"type": "array"
|
"type": "array"
|
||||||
|
},
|
||||||
|
"authPolicy": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/PluginAuthPolicy"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
|
|||||||
@@ -5,6 +5,21 @@
|
|||||||
"description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.",
|
"description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"PluginAuthPolicy": {
|
||||||
|
"enum": [
|
||||||
|
"ON_INSTALL",
|
||||||
|
"ON_USE"
|
||||||
|
],
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"PluginInstallPolicy": {
|
||||||
|
"enum": [
|
||||||
|
"NOT_AVAILABLE",
|
||||||
|
"AVAILABLE",
|
||||||
|
"INSTALLED_BY_DEFAULT"
|
||||||
|
],
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"PluginInterface": {
|
"PluginInterface": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"brandColor": {
|
"brandColor": {
|
||||||
@@ -154,12 +169,32 @@
|
|||||||
},
|
},
|
||||||
"PluginSummary": {
|
"PluginSummary": {
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"authPolicy": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/PluginAuthPolicy"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
"enabled": {
|
"enabled": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
"id": {
|
"id": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"installPolicy": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/PluginInstallPolicy"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
"installed": {
|
"installed": {
|
||||||
"type": "boolean"
|
"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 PluginAuthPolicy = "ON_INSTALL" | "ON_USE";
|
||||||
@@ -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 PluginInstallPolicy = "NOT_AVAILABLE" | "AVAILABLE" | "INSTALLED_BY_DEFAULT";
|
||||||
@@ -2,5 +2,6 @@
|
|||||||
|
|
||||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
import type { AppSummary } from "./AppSummary";
|
import type { AppSummary } from "./AppSummary";
|
||||||
|
import type { PluginAuthPolicy } from "./PluginAuthPolicy";
|
||||||
|
|
||||||
export type PluginInstallResponse = { appsNeedingAuth: Array<AppSummary>, };
|
export type PluginInstallResponse = { authPolicy: PluginAuthPolicy | null, appsNeedingAuth: Array<AppSummary>, };
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
// 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.
|
// 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 { PluginInstallPolicy } from "./PluginInstallPolicy";
|
||||||
import type { PluginInterface } from "./PluginInterface";
|
import type { PluginInterface } from "./PluginInterface";
|
||||||
import type { PluginSource } from "./PluginSource";
|
import type { PluginSource } from "./PluginSource";
|
||||||
|
|
||||||
export type PluginSummary = { id: string, name: string, source: PluginSource, installed: boolean, enabled: boolean, interface: PluginInterface | null, };
|
export type PluginSummary = { id: string, name: string, source: PluginSource, installed: boolean, enabled: boolean, installPolicy: PluginInstallPolicy | null, authPolicy: PluginAuthPolicy | null, interface: PluginInterface | null, };
|
||||||
|
|||||||
@@ -175,7 +175,9 @@ export type { PermissionGrantScope } from "./PermissionGrantScope";
|
|||||||
export type { PermissionsRequestApprovalParams } from "./PermissionsRequestApprovalParams";
|
export type { PermissionsRequestApprovalParams } from "./PermissionsRequestApprovalParams";
|
||||||
export type { PermissionsRequestApprovalResponse } from "./PermissionsRequestApprovalResponse";
|
export type { PermissionsRequestApprovalResponse } from "./PermissionsRequestApprovalResponse";
|
||||||
export type { PlanDeltaNotification } from "./PlanDeltaNotification";
|
export type { PlanDeltaNotification } from "./PlanDeltaNotification";
|
||||||
|
export type { PluginAuthPolicy } from "./PluginAuthPolicy";
|
||||||
export type { PluginInstallParams } from "./PluginInstallParams";
|
export type { PluginInstallParams } from "./PluginInstallParams";
|
||||||
|
export type { PluginInstallPolicy } from "./PluginInstallPolicy";
|
||||||
export type { PluginInstallResponse } from "./PluginInstallResponse";
|
export type { PluginInstallResponse } from "./PluginInstallResponse";
|
||||||
export type { PluginInterface } from "./PluginInterface";
|
export type { PluginInterface } from "./PluginInterface";
|
||||||
export type { PluginListParams } from "./PluginListParams";
|
export type { PluginListParams } from "./PluginListParams";
|
||||||
|
|||||||
@@ -3053,6 +3053,31 @@ pub struct PluginMarketplaceEntry {
|
|||||||
pub plugins: Vec<PluginSummary>,
|
pub plugins: Vec<PluginSummary>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
|
||||||
|
#[ts(export_to = "v2/")]
|
||||||
|
pub enum PluginInstallPolicy {
|
||||||
|
#[serde(rename = "NOT_AVAILABLE")]
|
||||||
|
#[ts(rename = "NOT_AVAILABLE")]
|
||||||
|
NotAvailable,
|
||||||
|
#[serde(rename = "AVAILABLE")]
|
||||||
|
#[ts(rename = "AVAILABLE")]
|
||||||
|
Available,
|
||||||
|
#[serde(rename = "INSTALLED_BY_DEFAULT")]
|
||||||
|
#[ts(rename = "INSTALLED_BY_DEFAULT")]
|
||||||
|
InstalledByDefault,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
|
||||||
|
#[ts(export_to = "v2/")]
|
||||||
|
pub enum PluginAuthPolicy {
|
||||||
|
#[serde(rename = "ON_INSTALL")]
|
||||||
|
#[ts(rename = "ON_INSTALL")]
|
||||||
|
OnInstall,
|
||||||
|
#[serde(rename = "ON_USE")]
|
||||||
|
#[ts(rename = "ON_USE")]
|
||||||
|
OnUse,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
#[ts(export_to = "v2/")]
|
#[ts(export_to = "v2/")]
|
||||||
@@ -3062,6 +3087,8 @@ pub struct PluginSummary {
|
|||||||
pub source: PluginSource,
|
pub source: PluginSource,
|
||||||
pub installed: bool,
|
pub installed: bool,
|
||||||
pub enabled: bool,
|
pub enabled: bool,
|
||||||
|
pub install_policy: Option<PluginInstallPolicy>,
|
||||||
|
pub auth_policy: Option<PluginAuthPolicy>,
|
||||||
pub interface: Option<PluginInterface>,
|
pub interface: Option<PluginInterface>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3122,6 +3149,7 @@ pub struct PluginInstallParams {
|
|||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
#[ts(export_to = "v2/")]
|
#[ts(export_to = "v2/")]
|
||||||
pub struct PluginInstallResponse {
|
pub struct PluginInstallResponse {
|
||||||
|
pub auth_policy: Option<PluginAuthPolicy>,
|
||||||
pub apps_needing_auth: Vec<AppSummary>,
|
pub apps_needing_auth: Vec<AppSummary>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -157,13 +157,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`.
|
- `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.
|
- `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`).
|
- `skills/list` — list skills for one or more `cwd` values (optional `forceReload`).
|
||||||
- `plugin/list` — list discovered plugin marketplaces and plugin state. Pass `forceRemoteSync: true` to refresh curated plugin state before listing (**under development; do not call from production clients yet**).
|
- `plugin/list` — list discovered plugin marketplaces and plugin state, including marketplace install/auth policy metadata. `interface.category` uses the marketplace category when present; otherwise it falls back to the plugin manifest category. Pass `forceRemoteSync: true` to refresh curated plugin state before listing (**under development; do not call from production clients yet**).
|
||||||
- `skills/changed` — notification emitted when watched local skill files change.
|
- `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/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**).
|
- `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.
|
- `app/list` — list available apps.
|
||||||
- `skills/config/write` — write user-level skill config by path.
|
- `skills/config/write` — write user-level skill config by path.
|
||||||
- `plugin/install` — install a plugin from a discovered marketplace entry and return any apps that still need auth (**under development; do not call from production clients yet**).
|
- `plugin/install` — install a plugin from a discovered marketplace entry, rejecting marketplace entries marked unavailable for install, and return the plugin auth policy plus any apps that still need auth (**under development; do not call from production clients yet**).
|
||||||
- `plugin/uninstall` — uninstall a plugin by id by removing its cached files and clearing its user-level config entry (**under development; do not call from production clients yet**).
|
- `plugin/uninstall` — uninstall a plugin by id by removing its cached files and clearing its user-level config entry (**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.
|
- `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).
|
- `tool/requestUserInput` — prompt the user with 1–3 short questions for a tool call and return their answers (experimental).
|
||||||
|
|||||||
@@ -4685,6 +4685,7 @@ impl CodexMessageProcessor {
|
|||||||
}
|
}
|
||||||
MarketplaceError::InvalidMarketplaceFile { .. }
|
MarketplaceError::InvalidMarketplaceFile { .. }
|
||||||
| MarketplaceError::PluginNotFound { .. }
|
| MarketplaceError::PluginNotFound { .. }
|
||||||
|
| MarketplaceError::PluginNotAvailable { .. }
|
||||||
| MarketplaceError::InvalidPlugin(_) => {
|
| MarketplaceError::InvalidPlugin(_) => {
|
||||||
self.send_invalid_request_error(request_id, err.to_string())
|
self.send_invalid_request_error(request_id, err.to_string())
|
||||||
.await;
|
.await;
|
||||||
@@ -5399,6 +5400,8 @@ impl CodexMessageProcessor {
|
|||||||
PluginSource::Local { path }
|
PluginSource::Local { path }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
install_policy: plugin.install_policy.map(Into::into),
|
||||||
|
auth_policy: plugin.auth_policy.map(Into::into),
|
||||||
interface: plugin.interface.map(|interface| PluginInterface {
|
interface: plugin.interface.map(|interface| PluginInterface {
|
||||||
display_name: interface.display_name,
|
display_name: interface.display_name,
|
||||||
short_description: interface.short_description,
|
short_description: interface.short_description,
|
||||||
@@ -5648,7 +5651,13 @@ impl CodexMessageProcessor {
|
|||||||
|
|
||||||
self.clear_plugin_related_caches();
|
self.clear_plugin_related_caches();
|
||||||
self.outgoing
|
self.outgoing
|
||||||
.send_response(request_id, PluginInstallResponse { apps_needing_auth })
|
.send_response(
|
||||||
|
request_id,
|
||||||
|
PluginInstallResponse {
|
||||||
|
auth_policy: result.auth_policy.map(Into::into),
|
||||||
|
apps_needing_auth,
|
||||||
|
},
|
||||||
|
)
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ use axum::routing::get;
|
|||||||
use codex_app_server_protocol::AppInfo;
|
use codex_app_server_protocol::AppInfo;
|
||||||
use codex_app_server_protocol::AppSummary;
|
use codex_app_server_protocol::AppSummary;
|
||||||
use codex_app_server_protocol::JSONRPCResponse;
|
use codex_app_server_protocol::JSONRPCResponse;
|
||||||
|
use codex_app_server_protocol::PluginAuthPolicy;
|
||||||
use codex_app_server_protocol::PluginInstallParams;
|
use codex_app_server_protocol::PluginInstallParams;
|
||||||
use codex_app_server_protocol::PluginInstallResponse;
|
use codex_app_server_protocol::PluginInstallResponse;
|
||||||
use codex_app_server_protocol::RequestId;
|
use codex_app_server_protocol::RequestId;
|
||||||
@@ -98,6 +99,43 @@ async fn plugin_install_returns_invalid_request_for_missing_marketplace_file() -
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn plugin_install_returns_invalid_request_for_not_available_plugin() -> Result<()> {
|
||||||
|
let codex_home = TempDir::new()?;
|
||||||
|
let repo_root = TempDir::new()?;
|
||||||
|
write_plugin_marketplace(
|
||||||
|
repo_root.path(),
|
||||||
|
"debug",
|
||||||
|
"sample-plugin",
|
||||||
|
"./sample-plugin",
|
||||||
|
Some("NOT_AVAILABLE"),
|
||||||
|
None,
|
||||||
|
)?;
|
||||||
|
write_plugin_source(repo_root.path(), "sample-plugin", &[])?;
|
||||||
|
let marketplace_path =
|
||||||
|
AbsolutePathBuf::try_from(repo_root.path().join(".agents/plugins/marketplace.json"))?;
|
||||||
|
|
||||||
|
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,
|
||||||
|
plugin_name: "sample-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("not available for install"));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn plugin_install_returns_apps_needing_auth() -> Result<()> {
|
async fn plugin_install_returns_apps_needing_auth() -> Result<()> {
|
||||||
let connectors = vec![
|
let connectors = vec![
|
||||||
@@ -152,6 +190,8 @@ async fn plugin_install_returns_apps_needing_auth() -> Result<()> {
|
|||||||
"debug",
|
"debug",
|
||||||
"sample-plugin",
|
"sample-plugin",
|
||||||
"./sample-plugin",
|
"./sample-plugin",
|
||||||
|
None,
|
||||||
|
Some("ON_INSTALL"),
|
||||||
)?;
|
)?;
|
||||||
write_plugin_source(repo_root.path(), "sample-plugin", &["alpha", "beta"])?;
|
write_plugin_source(repo_root.path(), "sample-plugin", &["alpha", "beta"])?;
|
||||||
let marketplace_path =
|
let marketplace_path =
|
||||||
@@ -177,6 +217,7 @@ async fn plugin_install_returns_apps_needing_auth() -> Result<()> {
|
|||||||
assert_eq!(
|
assert_eq!(
|
||||||
response,
|
response,
|
||||||
PluginInstallResponse {
|
PluginInstallResponse {
|
||||||
|
auth_policy: Some(PluginAuthPolicy::OnInstall),
|
||||||
apps_needing_auth: vec![AppSummary {
|
apps_needing_auth: vec![AppSummary {
|
||||||
id: "alpha".to_string(),
|
id: "alpha".to_string(),
|
||||||
name: "Alpha".to_string(),
|
name: "Alpha".to_string(),
|
||||||
@@ -227,6 +268,8 @@ async fn plugin_install_filters_disallowed_apps_needing_auth() -> Result<()> {
|
|||||||
"debug",
|
"debug",
|
||||||
"sample-plugin",
|
"sample-plugin",
|
||||||
"./sample-plugin",
|
"./sample-plugin",
|
||||||
|
None,
|
||||||
|
Some("ON_USE"),
|
||||||
)?;
|
)?;
|
||||||
write_plugin_source(
|
write_plugin_source(
|
||||||
repo_root.path(),
|
repo_root.path(),
|
||||||
@@ -256,6 +299,7 @@ async fn plugin_install_filters_disallowed_apps_needing_auth() -> Result<()> {
|
|||||||
assert_eq!(
|
assert_eq!(
|
||||||
response,
|
response,
|
||||||
PluginInstallResponse {
|
PluginInstallResponse {
|
||||||
|
auth_policy: Some(PluginAuthPolicy::OnUse),
|
||||||
apps_needing_auth: vec![AppSummary {
|
apps_needing_auth: vec![AppSummary {
|
||||||
id: "alpha".to_string(),
|
id: "alpha".to_string(),
|
||||||
name: "Alpha".to_string(),
|
name: "Alpha".to_string(),
|
||||||
@@ -422,7 +466,15 @@ fn write_plugin_marketplace(
|
|||||||
marketplace_name: &str,
|
marketplace_name: &str,
|
||||||
plugin_name: &str,
|
plugin_name: &str,
|
||||||
source_path: &str,
|
source_path: &str,
|
||||||
|
install_policy: Option<&str>,
|
||||||
|
auth_policy: Option<&str>,
|
||||||
) -> std::io::Result<()> {
|
) -> std::io::Result<()> {
|
||||||
|
let install_policy = install_policy
|
||||||
|
.map(|install_policy| format!(",\n \"installPolicy\": \"{install_policy}\""))
|
||||||
|
.unwrap_or_default();
|
||||||
|
let auth_policy = auth_policy
|
||||||
|
.map(|auth_policy| format!(",\n \"authPolicy\": \"{auth_policy}\""))
|
||||||
|
.unwrap_or_default();
|
||||||
std::fs::create_dir_all(repo_root.join(".git"))?;
|
std::fs::create_dir_all(repo_root.join(".git"))?;
|
||||||
std::fs::create_dir_all(repo_root.join(".agents/plugins"))?;
|
std::fs::create_dir_all(repo_root.join(".agents/plugins"))?;
|
||||||
std::fs::write(
|
std::fs::write(
|
||||||
@@ -436,7 +488,7 @@ fn write_plugin_marketplace(
|
|||||||
"source": {{
|
"source": {{
|
||||||
"source": "local",
|
"source": "local",
|
||||||
"path": "{source_path}"
|
"path": "{source_path}"
|
||||||
}}
|
}}{install_policy}{auth_policy}
|
||||||
}}
|
}}
|
||||||
]
|
]
|
||||||
}}"#
|
}}"#
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ use app_test_support::McpProcess;
|
|||||||
use app_test_support::to_response;
|
use app_test_support::to_response;
|
||||||
use app_test_support::write_chatgpt_auth;
|
use app_test_support::write_chatgpt_auth;
|
||||||
use codex_app_server_protocol::JSONRPCResponse;
|
use codex_app_server_protocol::JSONRPCResponse;
|
||||||
|
use codex_app_server_protocol::PluginAuthPolicy;
|
||||||
|
use codex_app_server_protocol::PluginInstallPolicy;
|
||||||
use codex_app_server_protocol::PluginListParams;
|
use codex_app_server_protocol::PluginListParams;
|
||||||
use codex_app_server_protocol::PluginListResponse;
|
use codex_app_server_protocol::PluginListResponse;
|
||||||
use codex_app_server_protocol::RequestId;
|
use codex_app_server_protocol::RequestId;
|
||||||
@@ -358,7 +360,10 @@ async fn plugin_list_returns_plugin_interface_with_absolute_asset_paths() -> Res
|
|||||||
"source": {
|
"source": {
|
||||||
"source": "local",
|
"source": "local",
|
||||||
"path": "./plugins/demo-plugin"
|
"path": "./plugins/demo-plugin"
|
||||||
}
|
},
|
||||||
|
"installPolicy": "AVAILABLE",
|
||||||
|
"authPolicy": "ON_INSTALL",
|
||||||
|
"category": "Design"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}"#,
|
}"#,
|
||||||
@@ -413,6 +418,8 @@ async fn plugin_list_returns_plugin_interface_with_absolute_asset_paths() -> Res
|
|||||||
assert_eq!(plugin.id, "demo-plugin@codex-curated");
|
assert_eq!(plugin.id, "demo-plugin@codex-curated");
|
||||||
assert_eq!(plugin.installed, false);
|
assert_eq!(plugin.installed, false);
|
||||||
assert_eq!(plugin.enabled, false);
|
assert_eq!(plugin.enabled, false);
|
||||||
|
assert_eq!(plugin.install_policy, Some(PluginInstallPolicy::Available));
|
||||||
|
assert_eq!(plugin.auth_policy, Some(PluginAuthPolicy::OnInstall));
|
||||||
let interface = plugin
|
let interface = plugin
|
||||||
.interface
|
.interface
|
||||||
.as_ref()
|
.as_ref()
|
||||||
@@ -421,6 +428,7 @@ async fn plugin_list_returns_plugin_interface_with_absolute_asset_paths() -> Res
|
|||||||
interface.display_name.as_deref(),
|
interface.display_name.as_deref(),
|
||||||
Some("Plugin Display Name")
|
Some("Plugin Display Name")
|
||||||
);
|
);
|
||||||
|
assert_eq!(interface.category.as_deref(), Some("Design"));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
interface.website_url.as_deref(),
|
interface.website_url.as_deref(),
|
||||||
Some("https://openai.com/")
|
Some("https://openai.com/")
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ use super::curated_plugins_repo_path;
|
|||||||
use super::load_plugin_manifest;
|
use super::load_plugin_manifest;
|
||||||
use super::manifest::PluginManifestInterfaceSummary;
|
use super::manifest::PluginManifestInterfaceSummary;
|
||||||
use super::marketplace::MarketplaceError;
|
use super::marketplace::MarketplaceError;
|
||||||
|
use super::marketplace::MarketplacePluginAuthPolicy;
|
||||||
|
use super::marketplace::MarketplacePluginInstallPolicy;
|
||||||
use super::marketplace::MarketplacePluginSourceSummary;
|
use super::marketplace::MarketplacePluginSourceSummary;
|
||||||
use super::marketplace::list_marketplaces;
|
use super::marketplace::list_marketplaces;
|
||||||
use super::marketplace::load_marketplace_summary;
|
use super::marketplace::load_marketplace_summary;
|
||||||
@@ -12,7 +14,7 @@ use super::plugin_manifest_paths;
|
|||||||
use super::store::DEFAULT_PLUGIN_VERSION;
|
use super::store::DEFAULT_PLUGIN_VERSION;
|
||||||
use super::store::PluginId;
|
use super::store::PluginId;
|
||||||
use super::store::PluginIdError;
|
use super::store::PluginIdError;
|
||||||
use super::store::PluginInstallResult;
|
use super::store::PluginInstallResult as StorePluginInstallResult;
|
||||||
use super::store::PluginStore;
|
use super::store::PluginStore;
|
||||||
use super::store::PluginStoreError;
|
use super::store::PluginStoreError;
|
||||||
use super::sync_openai_plugins_repo;
|
use super::sync_openai_plugins_repo;
|
||||||
@@ -68,6 +70,14 @@ pub struct PluginInstallRequest {
|
|||||||
pub marketplace_path: AbsolutePathBuf,
|
pub marketplace_path: AbsolutePathBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct PluginInstallOutcome {
|
||||||
|
pub plugin_id: PluginId,
|
||||||
|
pub plugin_version: String,
|
||||||
|
pub installed_path: AbsolutePathBuf,
|
||||||
|
pub auth_policy: Option<MarketplacePluginAuthPolicy>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct ConfiguredMarketplaceSummary {
|
pub struct ConfiguredMarketplaceSummary {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
@@ -80,6 +90,8 @@ pub struct ConfiguredMarketplacePluginSummary {
|
|||||||
pub id: String,
|
pub id: String,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub source: MarketplacePluginSourceSummary,
|
pub source: MarketplacePluginSourceSummary,
|
||||||
|
pub install_policy: Option<MarketplacePluginInstallPolicy>,
|
||||||
|
pub auth_policy: Option<MarketplacePluginAuthPolicy>,
|
||||||
pub interface: Option<PluginManifestInterfaceSummary>,
|
pub interface: Option<PluginManifestInterfaceSummary>,
|
||||||
pub installed: bool,
|
pub installed: bool,
|
||||||
pub enabled: bool,
|
pub enabled: bool,
|
||||||
@@ -380,10 +392,11 @@ impl PluginsManager {
|
|||||||
pub async fn install_plugin(
|
pub async fn install_plugin(
|
||||||
&self,
|
&self,
|
||||||
request: PluginInstallRequest,
|
request: PluginInstallRequest,
|
||||||
) -> Result<PluginInstallResult, PluginInstallError> {
|
) -> Result<PluginInstallOutcome, PluginInstallError> {
|
||||||
let resolved = resolve_marketplace_plugin(&request.marketplace_path, &request.plugin_name)?;
|
let resolved = resolve_marketplace_plugin(&request.marketplace_path, &request.plugin_name)?;
|
||||||
|
let auth_policy = resolved.auth_policy;
|
||||||
let store = self.store.clone();
|
let store = self.store.clone();
|
||||||
let result = tokio::task::spawn_blocking(move || {
|
let result: StorePluginInstallResult = tokio::task::spawn_blocking(move || {
|
||||||
store.install(resolved.source_path, resolved.plugin_id)
|
store.install(resolved.source_path, resolved.plugin_id)
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
@@ -403,7 +416,12 @@ impl PluginsManager {
|
|||||||
.map(|_| ())
|
.map(|_| ())
|
||||||
.map_err(PluginInstallError::from)?;
|
.map_err(PluginInstallError::from)?;
|
||||||
|
|
||||||
Ok(result)
|
Ok(PluginInstallOutcome {
|
||||||
|
plugin_id: result.plugin_id,
|
||||||
|
plugin_version: result.plugin_version,
|
||||||
|
installed_path: result.installed_path,
|
||||||
|
auth_policy,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn uninstall_plugin(&self, plugin_id: String) -> Result<(), PluginUninstallError> {
|
pub async fn uninstall_plugin(&self, plugin_id: String) -> Result<(), PluginUninstallError> {
|
||||||
@@ -634,6 +652,8 @@ impl PluginsManager {
|
|||||||
.unwrap_or(false),
|
.unwrap_or(false),
|
||||||
name: plugin.name,
|
name: plugin.name,
|
||||||
source: plugin.source,
|
source: plugin.source,
|
||||||
|
install_policy: plugin.install_policy,
|
||||||
|
auth_policy: plugin.auth_policy,
|
||||||
interface: plugin.interface,
|
interface: plugin.interface,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -760,6 +780,7 @@ impl PluginInstallError {
|
|||||||
MarketplaceError::MarketplaceNotFound { .. }
|
MarketplaceError::MarketplaceNotFound { .. }
|
||||||
| MarketplaceError::InvalidMarketplaceFile { .. }
|
| MarketplaceError::InvalidMarketplaceFile { .. }
|
||||||
| MarketplaceError::PluginNotFound { .. }
|
| MarketplaceError::PluginNotFound { .. }
|
||||||
|
| MarketplaceError::PluginNotAvailable { .. }
|
||||||
| MarketplaceError::InvalidPlugin(_)
|
| MarketplaceError::InvalidPlugin(_)
|
||||||
) | Self::Store(PluginStoreError::Invalid(_))
|
) | Self::Store(PluginStoreError::Invalid(_))
|
||||||
)
|
)
|
||||||
@@ -1925,7 +1946,8 @@ mod tests {
|
|||||||
"source": {
|
"source": {
|
||||||
"source": "local",
|
"source": "local",
|
||||||
"path": "./sample-plugin"
|
"path": "./sample-plugin"
|
||||||
}
|
},
|
||||||
|
"authPolicy": "ON_USE"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}"#,
|
}"#,
|
||||||
@@ -1946,10 +1968,11 @@ mod tests {
|
|||||||
let installed_path = tmp.path().join("plugins/cache/debug/sample-plugin/local");
|
let installed_path = tmp.path().join("plugins/cache/debug/sample-plugin/local");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
result,
|
result,
|
||||||
PluginInstallResult {
|
PluginInstallOutcome {
|
||||||
plugin_id: PluginId::new("sample-plugin".to_string(), "debug".to_string()).unwrap(),
|
plugin_id: PluginId::new("sample-plugin".to_string(), "debug".to_string()).unwrap(),
|
||||||
plugin_version: "local".to_string(),
|
plugin_version: "local".to_string(),
|
||||||
installed_path: AbsolutePathBuf::try_from(installed_path).unwrap(),
|
installed_path: AbsolutePathBuf::try_from(installed_path).unwrap(),
|
||||||
|
auth_policy: Some(MarketplacePluginAuthPolicy::OnUse),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -2079,6 +2102,8 @@ enabled = false
|
|||||||
path: AbsolutePathBuf::try_from(tmp.path().join("repo/enabled-plugin"))
|
path: AbsolutePathBuf::try_from(tmp.path().join("repo/enabled-plugin"))
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
},
|
},
|
||||||
|
install_policy: None,
|
||||||
|
auth_policy: None,
|
||||||
interface: None,
|
interface: None,
|
||||||
installed: true,
|
installed: true,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
@@ -2092,6 +2117,8 @@ enabled = false
|
|||||||
)
|
)
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
},
|
},
|
||||||
|
install_policy: None,
|
||||||
|
auth_policy: None,
|
||||||
interface: None,
|
interface: None,
|
||||||
installed: true,
|
installed: true,
|
||||||
enabled: false,
|
enabled: false,
|
||||||
@@ -2157,6 +2184,8 @@ enabled = false
|
|||||||
path: AbsolutePathBuf::try_from(curated_root.join("plugins/linear"))
|
path: AbsolutePathBuf::try_from(curated_root.join("plugins/linear"))
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
},
|
},
|
||||||
|
install_policy: None,
|
||||||
|
auth_policy: None,
|
||||||
interface: None,
|
interface: None,
|
||||||
installed: false,
|
installed: false,
|
||||||
enabled: false,
|
enabled: false,
|
||||||
@@ -2255,6 +2284,8 @@ enabled = false
|
|||||||
source: MarketplacePluginSourceSummary::Local {
|
source: MarketplacePluginSourceSummary::Local {
|
||||||
path: AbsolutePathBuf::try_from(tmp.path().join("repo-a/from-a")).unwrap(),
|
path: AbsolutePathBuf::try_from(tmp.path().join("repo-a/from-a")).unwrap(),
|
||||||
},
|
},
|
||||||
|
install_policy: None,
|
||||||
|
auth_policy: None,
|
||||||
interface: None,
|
interface: None,
|
||||||
installed: false,
|
installed: false,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
@@ -2279,6 +2310,8 @@ enabled = false
|
|||||||
source: MarketplacePluginSourceSummary::Local {
|
source: MarketplacePluginSourceSummary::Local {
|
||||||
path: AbsolutePathBuf::try_from(tmp.path().join("repo-b/from-b-only")).unwrap(),
|
path: AbsolutePathBuf::try_from(tmp.path().join("repo-b/from-b-only")).unwrap(),
|
||||||
},
|
},
|
||||||
|
install_policy: None,
|
||||||
|
auth_policy: None,
|
||||||
interface: None,
|
interface: None,
|
||||||
installed: false,
|
installed: false,
|
||||||
enabled: false,
|
enabled: false,
|
||||||
@@ -2356,6 +2389,8 @@ enabled = true
|
|||||||
path: AbsolutePathBuf::try_from(tmp.path().join("repo/sample-plugin"))
|
path: AbsolutePathBuf::try_from(tmp.path().join("repo/sample-plugin"))
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
},
|
},
|
||||||
|
install_policy: None,
|
||||||
|
auth_policy: None,
|
||||||
interface: None,
|
interface: None,
|
||||||
installed: false,
|
installed: false,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ pub struct PluginManifestPaths {
|
|||||||
pub apps: Option<AbsolutePathBuf>,
|
pub apps: Option<AbsolutePathBuf>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||||
pub struct PluginManifestInterfaceSummary {
|
pub struct PluginManifestInterfaceSummary {
|
||||||
pub display_name: Option<String>,
|
pub display_name: Option<String>,
|
||||||
pub short_description: Option<String>,
|
pub short_description: Option<String>,
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ use super::plugin_manifest_interface;
|
|||||||
use super::store::PluginId;
|
use super::store::PluginId;
|
||||||
use super::store::PluginIdError;
|
use super::store::PluginIdError;
|
||||||
use crate::git_info::get_git_repo_root;
|
use crate::git_info::get_git_repo_root;
|
||||||
|
use codex_app_server_protocol::PluginAuthPolicy;
|
||||||
|
use codex_app_server_protocol::PluginInstallPolicy;
|
||||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||||
use dirs::home_dir;
|
use dirs::home_dir;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
@@ -19,6 +21,7 @@ const MARKETPLACE_RELATIVE_PATH: &str = ".agents/plugins/marketplace.json";
|
|||||||
pub struct ResolvedMarketplacePlugin {
|
pub struct ResolvedMarketplacePlugin {
|
||||||
pub plugin_id: PluginId,
|
pub plugin_id: PluginId,
|
||||||
pub source_path: AbsolutePathBuf,
|
pub source_path: AbsolutePathBuf,
|
||||||
|
pub auth_policy: Option<MarketplacePluginAuthPolicy>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
@@ -32,6 +35,8 @@ pub struct MarketplaceSummary {
|
|||||||
pub struct MarketplacePluginSummary {
|
pub struct MarketplacePluginSummary {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub source: MarketplacePluginSourceSummary,
|
pub source: MarketplacePluginSourceSummary,
|
||||||
|
pub install_policy: Option<MarketplacePluginInstallPolicy>,
|
||||||
|
pub auth_policy: Option<MarketplacePluginAuthPolicy>,
|
||||||
pub interface: Option<PluginManifestInterfaceSummary>,
|
pub interface: Option<PluginManifestInterfaceSummary>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,6 +45,43 @@ pub enum MarketplacePluginSourceSummary {
|
|||||||
Local { path: AbsolutePathBuf },
|
Local { path: AbsolutePathBuf },
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
|
||||||
|
pub enum MarketplacePluginInstallPolicy {
|
||||||
|
#[serde(rename = "NOT_AVAILABLE")]
|
||||||
|
NotAvailable,
|
||||||
|
#[serde(rename = "AVAILABLE")]
|
||||||
|
Available,
|
||||||
|
#[serde(rename = "INSTALLED_BY_DEFAULT")]
|
||||||
|
InstalledByDefault,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
|
||||||
|
pub enum MarketplacePluginAuthPolicy {
|
||||||
|
#[serde(rename = "ON_INSTALL")]
|
||||||
|
OnInstall,
|
||||||
|
#[serde(rename = "ON_USE")]
|
||||||
|
OnUse,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<MarketplacePluginInstallPolicy> for PluginInstallPolicy {
|
||||||
|
fn from(value: MarketplacePluginInstallPolicy) -> Self {
|
||||||
|
match value {
|
||||||
|
MarketplacePluginInstallPolicy::NotAvailable => Self::NotAvailable,
|
||||||
|
MarketplacePluginInstallPolicy::Available => Self::Available,
|
||||||
|
MarketplacePluginInstallPolicy::InstalledByDefault => Self::InstalledByDefault,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<MarketplacePluginAuthPolicy> for PluginAuthPolicy {
|
||||||
|
fn from(value: MarketplacePluginAuthPolicy) -> Self {
|
||||||
|
match value {
|
||||||
|
MarketplacePluginAuthPolicy::OnInstall => Self::OnInstall,
|
||||||
|
MarketplacePluginAuthPolicy::OnUse => Self::OnUse,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub enum MarketplaceError {
|
pub enum MarketplaceError {
|
||||||
#[error("{context}: {source}")]
|
#[error("{context}: {source}")]
|
||||||
@@ -61,6 +103,14 @@ pub enum MarketplaceError {
|
|||||||
marketplace_name: String,
|
marketplace_name: String,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
#[error(
|
||||||
|
"plugin `{plugin_name}` is not available for install in marketplace `{marketplace_name}`"
|
||||||
|
)]
|
||||||
|
PluginNotAvailable {
|
||||||
|
plugin_name: String,
|
||||||
|
marketplace_name: String,
|
||||||
|
},
|
||||||
|
|
||||||
#[error("{0}")]
|
#[error("{0}")]
|
||||||
InvalidPlugin(String),
|
InvalidPlugin(String),
|
||||||
}
|
}
|
||||||
@@ -91,12 +141,27 @@ pub fn resolve_marketplace_plugin(
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
let plugin_id = PluginId::new(plugin.name, marketplace_name).map_err(|err| match err {
|
let MarketplacePlugin {
|
||||||
|
name,
|
||||||
|
source,
|
||||||
|
install_policy,
|
||||||
|
auth_policy,
|
||||||
|
..
|
||||||
|
} = plugin;
|
||||||
|
if install_policy == Some(MarketplacePluginInstallPolicy::NotAvailable) {
|
||||||
|
return Err(MarketplaceError::PluginNotAvailable {
|
||||||
|
plugin_name: name,
|
||||||
|
marketplace_name,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let plugin_id = PluginId::new(name, marketplace_name).map_err(|err| match err {
|
||||||
PluginIdError::Invalid(message) => MarketplaceError::InvalidPlugin(message),
|
PluginIdError::Invalid(message) => MarketplaceError::InvalidPlugin(message),
|
||||||
})?;
|
})?;
|
||||||
Ok(ResolvedMarketplacePlugin {
|
Ok(ResolvedMarketplacePlugin {
|
||||||
plugin_id,
|
plugin_id,
|
||||||
source_path: resolve_plugin_source_path(marketplace_path, plugin.source)?,
|
source_path: resolve_plugin_source_path(marketplace_path, source)?,
|
||||||
|
auth_policy,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,16 +178,31 @@ pub(crate) fn load_marketplace_summary(
|
|||||||
let mut plugins = Vec::new();
|
let mut plugins = Vec::new();
|
||||||
|
|
||||||
for plugin in marketplace.plugins {
|
for plugin in marketplace.plugins {
|
||||||
let source_path = resolve_plugin_source_path(path, plugin.source)?;
|
let MarketplacePlugin {
|
||||||
|
name,
|
||||||
|
source,
|
||||||
|
install_policy,
|
||||||
|
auth_policy,
|
||||||
|
category,
|
||||||
|
} = plugin;
|
||||||
|
let source_path = resolve_plugin_source_path(path, source)?;
|
||||||
let source = MarketplacePluginSourceSummary::Local {
|
let source = MarketplacePluginSourceSummary::Local {
|
||||||
path: source_path.clone(),
|
path: source_path.clone(),
|
||||||
};
|
};
|
||||||
let interface = load_plugin_manifest(source_path.as_path())
|
let mut interface = load_plugin_manifest(source_path.as_path())
|
||||||
.and_then(|manifest| plugin_manifest_interface(&manifest, source_path.as_path()));
|
.and_then(|manifest| plugin_manifest_interface(&manifest, source_path.as_path()));
|
||||||
|
if let Some(category) = category {
|
||||||
|
// Marketplace taxonomy wins when both sources provide a category.
|
||||||
|
interface
|
||||||
|
.get_or_insert_with(PluginManifestInterfaceSummary::default)
|
||||||
|
.category = Some(category);
|
||||||
|
}
|
||||||
|
|
||||||
plugins.push(MarketplacePluginSummary {
|
plugins.push(MarketplacePluginSummary {
|
||||||
name: plugin.name,
|
name,
|
||||||
source,
|
source,
|
||||||
|
install_policy,
|
||||||
|
auth_policy,
|
||||||
interface,
|
interface,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -280,9 +360,16 @@ struct MarketplaceFile {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
struct MarketplacePlugin {
|
struct MarketplacePlugin {
|
||||||
name: String,
|
name: String,
|
||||||
source: MarketplacePluginSource,
|
source: MarketplacePluginSource,
|
||||||
|
#[serde(default)]
|
||||||
|
install_policy: Option<MarketplacePluginInstallPolicy>,
|
||||||
|
#[serde(default)]
|
||||||
|
auth_policy: Option<MarketplacePluginAuthPolicy>,
|
||||||
|
#[serde(default)]
|
||||||
|
category: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
@@ -333,6 +420,7 @@ mod tests {
|
|||||||
plugin_id: PluginId::new("local-plugin".to_string(), "codex-curated".to_string())
|
plugin_id: PluginId::new("local-plugin".to_string(), "codex-curated".to_string())
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
source_path: AbsolutePathBuf::try_from(repo_root.join("plugin-1")).unwrap(),
|
source_path: AbsolutePathBuf::try_from(repo_root.join("plugin-1")).unwrap(),
|
||||||
|
auth_policy: None,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -439,6 +527,8 @@ mod tests {
|
|||||||
path: AbsolutePathBuf::try_from(home_root.join("home-shared"))
|
path: AbsolutePathBuf::try_from(home_root.join("home-shared"))
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
},
|
},
|
||||||
|
install_policy: None,
|
||||||
|
auth_policy: None,
|
||||||
interface: None,
|
interface: None,
|
||||||
},
|
},
|
||||||
MarketplacePluginSummary {
|
MarketplacePluginSummary {
|
||||||
@@ -447,6 +537,8 @@ mod tests {
|
|||||||
path: AbsolutePathBuf::try_from(home_root.join("home-only"))
|
path: AbsolutePathBuf::try_from(home_root.join("home-only"))
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
},
|
},
|
||||||
|
install_policy: None,
|
||||||
|
auth_policy: None,
|
||||||
interface: None,
|
interface: None,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -464,6 +556,8 @@ mod tests {
|
|||||||
path: AbsolutePathBuf::try_from(repo_root.join("repo-shared"))
|
path: AbsolutePathBuf::try_from(repo_root.join("repo-shared"))
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
},
|
},
|
||||||
|
install_policy: None,
|
||||||
|
auth_policy: None,
|
||||||
interface: None,
|
interface: None,
|
||||||
},
|
},
|
||||||
MarketplacePluginSummary {
|
MarketplacePluginSummary {
|
||||||
@@ -472,6 +566,8 @@ mod tests {
|
|||||||
path: AbsolutePathBuf::try_from(repo_root.join("repo-only"))
|
path: AbsolutePathBuf::try_from(repo_root.join("repo-only"))
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
},
|
},
|
||||||
|
install_policy: None,
|
||||||
|
auth_policy: None,
|
||||||
interface: None,
|
interface: None,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -542,6 +638,8 @@ mod tests {
|
|||||||
source: MarketplacePluginSourceSummary::Local {
|
source: MarketplacePluginSourceSummary::Local {
|
||||||
path: AbsolutePathBuf::try_from(home_root.join("home-plugin")).unwrap(),
|
path: AbsolutePathBuf::try_from(home_root.join("home-plugin")).unwrap(),
|
||||||
},
|
},
|
||||||
|
install_policy: None,
|
||||||
|
auth_policy: None,
|
||||||
interface: None,
|
interface: None,
|
||||||
}],
|
}],
|
||||||
},
|
},
|
||||||
@@ -553,6 +651,8 @@ mod tests {
|
|||||||
source: MarketplacePluginSourceSummary::Local {
|
source: MarketplacePluginSourceSummary::Local {
|
||||||
path: AbsolutePathBuf::try_from(repo_root.join("repo-plugin")).unwrap(),
|
path: AbsolutePathBuf::try_from(repo_root.join("repo-plugin")).unwrap(),
|
||||||
},
|
},
|
||||||
|
install_policy: None,
|
||||||
|
auth_policy: None,
|
||||||
interface: None,
|
interface: None,
|
||||||
}],
|
}],
|
||||||
},
|
},
|
||||||
@@ -617,6 +717,8 @@ mod tests {
|
|||||||
source: MarketplacePluginSourceSummary::Local {
|
source: MarketplacePluginSourceSummary::Local {
|
||||||
path: AbsolutePathBuf::try_from(repo_root.join("plugin")).unwrap(),
|
path: AbsolutePathBuf::try_from(repo_root.join("plugin")).unwrap(),
|
||||||
},
|
},
|
||||||
|
install_policy: None,
|
||||||
|
auth_policy: None,
|
||||||
interface: None,
|
interface: None,
|
||||||
}],
|
}],
|
||||||
}]
|
}]
|
||||||
@@ -641,7 +743,10 @@ mod tests {
|
|||||||
"source": {
|
"source": {
|
||||||
"source": "local",
|
"source": "local",
|
||||||
"path": "./plugins/demo-plugin"
|
"path": "./plugins/demo-plugin"
|
||||||
}
|
},
|
||||||
|
"installPolicy": "AVAILABLE",
|
||||||
|
"authPolicy": "ON_INSTALL",
|
||||||
|
"category": "Design"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}"#,
|
}"#,
|
||||||
@@ -653,6 +758,7 @@ mod tests {
|
|||||||
"name": "demo-plugin",
|
"name": "demo-plugin",
|
||||||
"interface": {
|
"interface": {
|
||||||
"displayName": "Demo",
|
"displayName": "Demo",
|
||||||
|
"category": "Productivity",
|
||||||
"capabilities": ["Interactive", "Write"],
|
"capabilities": ["Interactive", "Write"],
|
||||||
"composerIcon": "./assets/icon.png",
|
"composerIcon": "./assets/icon.png",
|
||||||
"logo": "./assets/logo.png",
|
"logo": "./assets/logo.png",
|
||||||
@@ -666,6 +772,14 @@ mod tests {
|
|||||||
list_marketplaces_with_home(&[AbsolutePathBuf::try_from(repo_root).unwrap()], None)
|
list_marketplaces_with_home(&[AbsolutePathBuf::try_from(repo_root).unwrap()], None)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
marketplaces[0].plugins[0].install_policy,
|
||||||
|
Some(MarketplacePluginInstallPolicy::Available)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
marketplaces[0].plugins[0].auth_policy,
|
||||||
|
Some(MarketplacePluginAuthPolicy::OnInstall)
|
||||||
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
marketplaces[0].plugins[0].interface,
|
marketplaces[0].plugins[0].interface,
|
||||||
Some(PluginManifestInterfaceSummary {
|
Some(PluginManifestInterfaceSummary {
|
||||||
@@ -673,7 +787,7 @@ mod tests {
|
|||||||
short_description: None,
|
short_description: None,
|
||||||
long_description: None,
|
long_description: None,
|
||||||
developer_name: None,
|
developer_name: None,
|
||||||
category: None,
|
category: Some("Design".to_string()),
|
||||||
capabilities: vec!["Interactive".to_string(), "Write".to_string()],
|
capabilities: vec!["Interactive".to_string(), "Write".to_string()],
|
||||||
website_url: None,
|
website_url: None,
|
||||||
privacy_policy_url: None,
|
privacy_policy_url: None,
|
||||||
@@ -754,6 +868,8 @@ mod tests {
|
|||||||
screenshots: Vec::new(),
|
screenshots: Vec::new(),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
assert_eq!(marketplaces[0].plugins[0].install_policy, None);
|
||||||
|
assert_eq!(marketplaces[0].plugins[0].auth_policy, None);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ pub use manager::ConfiguredMarketplaceSummary;
|
|||||||
pub use manager::LoadedPlugin;
|
pub use manager::LoadedPlugin;
|
||||||
pub use manager::PluginCapabilitySummary;
|
pub use manager::PluginCapabilitySummary;
|
||||||
pub use manager::PluginInstallError;
|
pub use manager::PluginInstallError;
|
||||||
|
pub use manager::PluginInstallOutcome;
|
||||||
pub use manager::PluginInstallRequest;
|
pub use manager::PluginInstallRequest;
|
||||||
pub use manager::PluginLoadOutcome;
|
pub use manager::PluginLoadOutcome;
|
||||||
pub use manager::PluginRemoteSyncError;
|
pub use manager::PluginRemoteSyncError;
|
||||||
@@ -30,8 +31,9 @@ pub(crate) use manifest::plugin_manifest_interface;
|
|||||||
pub(crate) use manifest::plugin_manifest_name;
|
pub(crate) use manifest::plugin_manifest_name;
|
||||||
pub(crate) use manifest::plugin_manifest_paths;
|
pub(crate) use manifest::plugin_manifest_paths;
|
||||||
pub use marketplace::MarketplaceError;
|
pub use marketplace::MarketplaceError;
|
||||||
|
pub use marketplace::MarketplacePluginAuthPolicy;
|
||||||
|
pub use marketplace::MarketplacePluginInstallPolicy;
|
||||||
pub use marketplace::MarketplacePluginSourceSummary;
|
pub use marketplace::MarketplacePluginSourceSummary;
|
||||||
pub(crate) use render::render_explicit_plugin_instructions;
|
pub(crate) use render::render_explicit_plugin_instructions;
|
||||||
pub(crate) use render::render_plugins_section;
|
pub(crate) use render::render_plugins_section;
|
||||||
pub use store::PluginId;
|
pub use store::PluginId;
|
||||||
pub use store::PluginInstallResult;
|
|
||||||
|
|||||||
Reference in New Issue
Block a user