mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
feat: Add role-aware plugin share context APIs (#21867)
Expose discoverability and full share principals in share context, carry roles through save/updateTargets, hydrate local shared plugin reads, and keep share URLs only under plugin.shareContext.
This commit is contained in:
committed by
GitHub
Unverified
parent
c579da41b1
commit
479491ed89
+12
-1
@@ -2088,14 +2088,25 @@
|
||||
},
|
||||
"principalType": {
|
||||
"$ref": "#/definitions/PluginSharePrincipalType"
|
||||
},
|
||||
"role": {
|
||||
"$ref": "#/definitions/PluginShareTargetRole"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"principalId",
|
||||
"principalType"
|
||||
"principalType",
|
||||
"role"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"PluginShareTargetRole": {
|
||||
"enum": [
|
||||
"reader",
|
||||
"editor"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"PluginShareUpdateDiscoverability": {
|
||||
"enum": [
|
||||
"UNLISTED",
|
||||
|
||||
+37
-8
@@ -12270,10 +12270,20 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"discoverability": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/v2/PluginShareDiscoverability"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"remotePluginId": {
|
||||
"type": "string"
|
||||
},
|
||||
"shareTargets": {
|
||||
"sharePrincipals": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/v2/PluginSharePrincipal"
|
||||
},
|
||||
@@ -12334,14 +12344,10 @@
|
||||
},
|
||||
"plugin": {
|
||||
"$ref": "#/definitions/v2/PluginSummary"
|
||||
},
|
||||
"shareUrl": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"plugin",
|
||||
"shareUrl"
|
||||
"plugin"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
@@ -12376,15 +12382,27 @@
|
||||
},
|
||||
"principalType": {
|
||||
"$ref": "#/definitions/v2/PluginSharePrincipalType"
|
||||
},
|
||||
"role": {
|
||||
"$ref": "#/definitions/v2/PluginSharePrincipalRole"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"name",
|
||||
"principalId",
|
||||
"principalType"
|
||||
"principalType",
|
||||
"role"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"PluginSharePrincipalRole": {
|
||||
"enum": [
|
||||
"reader",
|
||||
"editor",
|
||||
"owner"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"PluginSharePrincipalType": {
|
||||
"enum": [
|
||||
"user",
|
||||
@@ -12455,14 +12473,25 @@
|
||||
},
|
||||
"principalType": {
|
||||
"$ref": "#/definitions/v2/PluginSharePrincipalType"
|
||||
},
|
||||
"role": {
|
||||
"$ref": "#/definitions/v2/PluginShareTargetRole"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"principalId",
|
||||
"principalType"
|
||||
"principalType",
|
||||
"role"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"PluginShareTargetRole": {
|
||||
"enum": [
|
||||
"reader",
|
||||
"editor"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"PluginShareUpdateDiscoverability": {
|
||||
"enum": [
|
||||
"UNLISTED",
|
||||
|
||||
+37
-8
@@ -8819,10 +8819,20 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"discoverability": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/PluginShareDiscoverability"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"remotePluginId": {
|
||||
"type": "string"
|
||||
},
|
||||
"shareTargets": {
|
||||
"sharePrincipals": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/PluginSharePrincipal"
|
||||
},
|
||||
@@ -8883,14 +8893,10 @@
|
||||
},
|
||||
"plugin": {
|
||||
"$ref": "#/definitions/PluginSummary"
|
||||
},
|
||||
"shareUrl": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"plugin",
|
||||
"shareUrl"
|
||||
"plugin"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
@@ -8925,15 +8931,27 @@
|
||||
},
|
||||
"principalType": {
|
||||
"$ref": "#/definitions/PluginSharePrincipalType"
|
||||
},
|
||||
"role": {
|
||||
"$ref": "#/definitions/PluginSharePrincipalRole"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"name",
|
||||
"principalId",
|
||||
"principalType"
|
||||
"principalType",
|
||||
"role"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"PluginSharePrincipalRole": {
|
||||
"enum": [
|
||||
"reader",
|
||||
"editor",
|
||||
"owner"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"PluginSharePrincipalType": {
|
||||
"enum": [
|
||||
"user",
|
||||
@@ -9004,14 +9022,25 @@
|
||||
},
|
||||
"principalType": {
|
||||
"$ref": "#/definitions/PluginSharePrincipalType"
|
||||
},
|
||||
"role": {
|
||||
"$ref": "#/definitions/PluginShareTargetRole"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"principalId",
|
||||
"principalType"
|
||||
"principalType",
|
||||
"role"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"PluginShareTargetRole": {
|
||||
"enum": [
|
||||
"reader",
|
||||
"editor"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"PluginShareUpdateDiscoverability": {
|
||||
"enum": [
|
||||
"UNLISTED",
|
||||
|
||||
@@ -246,10 +246,20 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"discoverability": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/PluginShareDiscoverability"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"remotePluginId": {
|
||||
"type": "string"
|
||||
},
|
||||
"shareTargets": {
|
||||
"sharePrincipals": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/PluginSharePrincipal"
|
||||
},
|
||||
@@ -270,6 +280,14 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"PluginShareDiscoverability": {
|
||||
"enum": [
|
||||
"LISTED",
|
||||
"UNLISTED",
|
||||
"PRIVATE"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"PluginSharePrincipal": {
|
||||
"properties": {
|
||||
"name": {
|
||||
@@ -280,15 +298,27 @@
|
||||
},
|
||||
"principalType": {
|
||||
"$ref": "#/definitions/PluginSharePrincipalType"
|
||||
},
|
||||
"role": {
|
||||
"$ref": "#/definitions/PluginSharePrincipalRole"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"name",
|
||||
"principalId",
|
||||
"principalType"
|
||||
"principalType",
|
||||
"role"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"PluginSharePrincipalRole": {
|
||||
"enum": [
|
||||
"reader",
|
||||
"editor",
|
||||
"owner"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"PluginSharePrincipalType": {
|
||||
"enum": [
|
||||
"user",
|
||||
|
||||
@@ -300,10 +300,20 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"discoverability": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/PluginShareDiscoverability"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"remotePluginId": {
|
||||
"type": "string"
|
||||
},
|
||||
"shareTargets": {
|
||||
"sharePrincipals": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/PluginSharePrincipal"
|
||||
},
|
||||
@@ -324,6 +334,14 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"PluginShareDiscoverability": {
|
||||
"enum": [
|
||||
"LISTED",
|
||||
"UNLISTED",
|
||||
"PRIVATE"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"PluginSharePrincipal": {
|
||||
"properties": {
|
||||
"name": {
|
||||
@@ -334,15 +352,27 @@
|
||||
},
|
||||
"principalType": {
|
||||
"$ref": "#/definitions/PluginSharePrincipalType"
|
||||
},
|
||||
"role": {
|
||||
"$ref": "#/definitions/PluginSharePrincipalRole"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"name",
|
||||
"principalId",
|
||||
"principalType"
|
||||
"principalType",
|
||||
"role"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"PluginSharePrincipalRole": {
|
||||
"enum": [
|
||||
"reader",
|
||||
"editor",
|
||||
"owner"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"PluginSharePrincipalType": {
|
||||
"enum": [
|
||||
"user",
|
||||
|
||||
@@ -181,10 +181,20 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"discoverability": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/PluginShareDiscoverability"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"remotePluginId": {
|
||||
"type": "string"
|
||||
},
|
||||
"shareTargets": {
|
||||
"sharePrincipals": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/PluginSharePrincipal"
|
||||
},
|
||||
@@ -205,6 +215,14 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"PluginShareDiscoverability": {
|
||||
"enum": [
|
||||
"LISTED",
|
||||
"UNLISTED",
|
||||
"PRIVATE"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"PluginShareListItem": {
|
||||
"properties": {
|
||||
"localPluginPath": {
|
||||
@@ -219,14 +237,10 @@
|
||||
},
|
||||
"plugin": {
|
||||
"$ref": "#/definitions/PluginSummary"
|
||||
},
|
||||
"shareUrl": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"plugin",
|
||||
"shareUrl"
|
||||
"plugin"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
@@ -240,15 +254,27 @@
|
||||
},
|
||||
"principalType": {
|
||||
"$ref": "#/definitions/PluginSharePrincipalType"
|
||||
},
|
||||
"role": {
|
||||
"$ref": "#/definitions/PluginSharePrincipalRole"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"name",
|
||||
"principalId",
|
||||
"principalType"
|
||||
"principalType",
|
||||
"role"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"PluginSharePrincipalRole": {
|
||||
"enum": [
|
||||
"reader",
|
||||
"editor",
|
||||
"owner"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"PluginSharePrincipalType": {
|
||||
"enum": [
|
||||
"user",
|
||||
|
||||
@@ -28,13 +28,24 @@
|
||||
},
|
||||
"principalType": {
|
||||
"$ref": "#/definitions/PluginSharePrincipalType"
|
||||
},
|
||||
"role": {
|
||||
"$ref": "#/definitions/PluginShareTargetRole"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"principalId",
|
||||
"principalType"
|
||||
"principalType",
|
||||
"role"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"PluginShareTargetRole": {
|
||||
"enum": [
|
||||
"reader",
|
||||
"editor"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
|
||||
+12
-1
@@ -16,14 +16,25 @@
|
||||
},
|
||||
"principalType": {
|
||||
"$ref": "#/definitions/PluginSharePrincipalType"
|
||||
},
|
||||
"role": {
|
||||
"$ref": "#/definitions/PluginShareTargetRole"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"principalId",
|
||||
"principalType"
|
||||
"principalType",
|
||||
"role"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"PluginShareTargetRole": {
|
||||
"enum": [
|
||||
"reader",
|
||||
"editor"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"PluginShareUpdateDiscoverability": {
|
||||
"enum": [
|
||||
"UNLISTED",
|
||||
|
||||
+13
-1
@@ -19,15 +19,27 @@
|
||||
},
|
||||
"principalType": {
|
||||
"$ref": "#/definitions/PluginSharePrincipalType"
|
||||
},
|
||||
"role": {
|
||||
"$ref": "#/definitions/PluginSharePrincipalRole"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"name",
|
||||
"principalId",
|
||||
"principalType"
|
||||
"principalType",
|
||||
"role"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"PluginSharePrincipalRole": {
|
||||
"enum": [
|
||||
"reader",
|
||||
"editor",
|
||||
"owner"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"PluginSharePrincipalType": {
|
||||
"enum": [
|
||||
"user",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// 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.
|
||||
import type { PluginShareDiscoverability } from "./PluginShareDiscoverability";
|
||||
import type { PluginSharePrincipal } from "./PluginSharePrincipal";
|
||||
|
||||
export type PluginShareContext = { remotePluginId: string, shareUrl: string | null, creatorAccountUserId: string | null, creatorName: string | null, shareTargets: Array<PluginSharePrincipal> | null, };
|
||||
export type PluginShareContext = { remotePluginId: string, discoverability: PluginShareDiscoverability | null, shareUrl: string | null, creatorAccountUserId: string | null, creatorName: string | null, sharePrincipals: Array<PluginSharePrincipal> | null, };
|
||||
|
||||
@@ -4,4 +4,4 @@
|
||||
import type { AbsolutePathBuf } from "../AbsolutePathBuf";
|
||||
import type { PluginSummary } from "./PluginSummary";
|
||||
|
||||
export type PluginShareListItem = { plugin: PluginSummary, shareUrl: string, localPluginPath: AbsolutePathBuf | null, };
|
||||
export type PluginShareListItem = { plugin: PluginSummary, localPluginPath: AbsolutePathBuf | null, };
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// 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.
|
||||
import type { PluginSharePrincipalRole } from "./PluginSharePrincipalRole";
|
||||
import type { PluginSharePrincipalType } from "./PluginSharePrincipalType";
|
||||
|
||||
export type PluginSharePrincipal = { principalType: PluginSharePrincipalType, principalId: string, name: string, };
|
||||
export type PluginSharePrincipal = { principalType: PluginSharePrincipalType, principalId: string, role: PluginSharePrincipalRole, name: string, };
|
||||
|
||||
+5
@@ -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 PluginSharePrincipalRole = "reader" | "editor" | "owner";
|
||||
@@ -2,5 +2,6 @@
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { PluginSharePrincipalType } from "./PluginSharePrincipalType";
|
||||
import type { PluginShareTargetRole } from "./PluginShareTargetRole";
|
||||
|
||||
export type PluginShareTarget = { principalType: PluginSharePrincipalType, principalId: string, };
|
||||
export type PluginShareTarget = { principalType: PluginSharePrincipalType, principalId: string, role: PluginShareTargetRole, };
|
||||
|
||||
@@ -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 PluginShareTargetRole = "reader" | "editor";
|
||||
@@ -285,10 +285,12 @@ export type { PluginShareListItem } from "./PluginShareListItem";
|
||||
export type { PluginShareListParams } from "./PluginShareListParams";
|
||||
export type { PluginShareListResponse } from "./PluginShareListResponse";
|
||||
export type { PluginSharePrincipal } from "./PluginSharePrincipal";
|
||||
export type { PluginSharePrincipalRole } from "./PluginSharePrincipalRole";
|
||||
export type { PluginSharePrincipalType } from "./PluginSharePrincipalType";
|
||||
export type { PluginShareSaveParams } from "./PluginShareSaveParams";
|
||||
export type { PluginShareSaveResponse } from "./PluginShareSaveResponse";
|
||||
export type { PluginShareTarget } from "./PluginShareTarget";
|
||||
export type { PluginShareTargetRole } from "./PluginShareTargetRole";
|
||||
export type { PluginShareUpdateDiscoverability } from "./PluginShareUpdateDiscoverability";
|
||||
export type { PluginShareUpdateTargetsParams } from "./PluginShareUpdateTargetsParams";
|
||||
export type { PluginShareUpdateTargetsResponse } from "./PluginShareUpdateTargetsResponse";
|
||||
|
||||
@@ -259,7 +259,6 @@ pub struct PluginShareDeleteResponse {}
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct PluginShareListItem {
|
||||
pub plugin: PluginSummary,
|
||||
pub share_url: String,
|
||||
pub local_plugin_path: Option<AbsolutePathBuf>,
|
||||
}
|
||||
|
||||
@@ -308,6 +307,7 @@ pub enum PluginSharePrincipalType {
|
||||
pub struct PluginShareTarget {
|
||||
pub principal_type: PluginSharePrincipalType,
|
||||
pub principal_id: String,
|
||||
pub role: PluginShareTargetRole,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
@@ -316,9 +316,29 @@ pub struct PluginShareTarget {
|
||||
pub struct PluginSharePrincipal {
|
||||
pub principal_type: PluginSharePrincipalType,
|
||||
pub principal_id: String,
|
||||
pub role: PluginSharePrincipalRole,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
#[ts(rename_all = "lowercase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub enum PluginShareTargetRole {
|
||||
Reader,
|
||||
Editor,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
#[ts(rename_all = "lowercase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub enum PluginSharePrincipalRole {
|
||||
Reader,
|
||||
Editor,
|
||||
Owner,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[ts(rename_all = "snake_case")]
|
||||
@@ -539,10 +559,11 @@ pub struct PluginSummary {
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct PluginShareContext {
|
||||
pub remote_plugin_id: String,
|
||||
pub discoverability: Option<PluginShareDiscoverability>,
|
||||
pub share_url: Option<String>,
|
||||
pub creator_account_user_id: Option<String>,
|
||||
pub creator_name: Option<String>,
|
||||
pub share_targets: Option<Vec<PluginSharePrincipal>>,
|
||||
pub share_principals: Option<Vec<PluginSharePrincipal>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
|
||||
@@ -2896,10 +2896,12 @@ fn plugin_share_params_and_response_serialization_use_camel_case_fields() {
|
||||
PluginShareTarget {
|
||||
principal_type: PluginSharePrincipalType::User,
|
||||
principal_id: "user-1".to_string(),
|
||||
role: PluginShareTargetRole::Reader,
|
||||
},
|
||||
PluginShareTarget {
|
||||
principal_type: PluginSharePrincipalType::Workspace,
|
||||
principal_id: "workspace-1".to_string(),
|
||||
principal_type: PluginSharePrincipalType::Group,
|
||||
principal_id: "group-1".to_string(),
|
||||
role: PluginShareTargetRole::Reader,
|
||||
},
|
||||
]),
|
||||
})
|
||||
@@ -2912,10 +2914,12 @@ fn plugin_share_params_and_response_serialization_use_camel_case_fields() {
|
||||
{
|
||||
"principalType": "user",
|
||||
"principalId": "user-1",
|
||||
"role": "reader",
|
||||
},
|
||||
{
|
||||
"principalType": "workspace",
|
||||
"principalId": "workspace-1",
|
||||
"principalType": "group",
|
||||
"principalId": "group-1",
|
||||
"role": "reader",
|
||||
},
|
||||
],
|
||||
}),
|
||||
@@ -2940,6 +2944,7 @@ fn plugin_share_params_and_response_serialization_use_camel_case_fields() {
|
||||
share_targets: vec![PluginShareTarget {
|
||||
principal_type: PluginSharePrincipalType::Group,
|
||||
principal_id: "group-1".to_string(),
|
||||
role: PluginShareTargetRole::Editor,
|
||||
}],
|
||||
})
|
||||
.unwrap(),
|
||||
@@ -2949,6 +2954,7 @@ fn plugin_share_params_and_response_serialization_use_camel_case_fields() {
|
||||
"shareTargets": [{
|
||||
"principalType": "group",
|
||||
"principalId": "group-1",
|
||||
"role": "editor",
|
||||
}],
|
||||
}),
|
||||
);
|
||||
@@ -2958,6 +2964,7 @@ fn plugin_share_params_and_response_serialization_use_camel_case_fields() {
|
||||
principals: vec![PluginSharePrincipal {
|
||||
principal_type: PluginSharePrincipalType::User,
|
||||
principal_id: "user-1".to_string(),
|
||||
role: PluginSharePrincipalRole::Owner,
|
||||
name: "Gavin".to_string(),
|
||||
}],
|
||||
discoverability: PluginShareDiscoverability::Unlisted,
|
||||
@@ -2967,6 +2974,7 @@ fn plugin_share_params_and_response_serialization_use_camel_case_fields() {
|
||||
"principals": [{
|
||||
"principalType": "user",
|
||||
"principalId": "user-1",
|
||||
"role": "owner",
|
||||
"name": "Gavin",
|
||||
}],
|
||||
"discoverability": "UNLISTED",
|
||||
@@ -3007,7 +3015,6 @@ fn plugin_share_list_response_serializes_share_items() {
|
||||
interface: None,
|
||||
keywords: Vec::new(),
|
||||
},
|
||||
share_url: "https://chatgpt.example/plugins/share/share-key-1".to_string(),
|
||||
local_plugin_path: None,
|
||||
}],
|
||||
})
|
||||
@@ -3027,7 +3034,6 @@ fn plugin_share_list_response_serializes_share_items() {
|
||||
"interface": null,
|
||||
"keywords": [],
|
||||
},
|
||||
"shareUrl": "https://chatgpt.example/plugins/share/share-key-1",
|
||||
"localPluginPath": null,
|
||||
}],
|
||||
}),
|
||||
|
||||
@@ -3,6 +3,8 @@ use crate::error_code::internal_error;
|
||||
use crate::error_code::invalid_request;
|
||||
use codex_app_server_protocol::PluginAvailability;
|
||||
use codex_app_server_protocol::PluginInstallPolicy;
|
||||
use codex_app_server_protocol::PluginSharePrincipalRole;
|
||||
use codex_app_server_protocol::PluginShareTargetRole;
|
||||
use codex_config::types::McpServerConfig;
|
||||
use codex_core_plugins::remote::is_valid_remote_plugin_id;
|
||||
use codex_core_plugins::remote::validate_remote_plugin_id;
|
||||
@@ -88,13 +90,14 @@ fn marketplace_plugin_source_to_info(source: MarketplacePluginSource) -> PluginS
|
||||
|
||||
fn load_shared_plugin_ids_by_local_path(
|
||||
config: &Config,
|
||||
) -> std::collections::BTreeMap<AbsolutePathBuf, String> {
|
||||
) -> Result<std::collections::BTreeMap<AbsolutePathBuf, String>, JSONRPCErrorError> {
|
||||
codex_core_plugins::remote::load_plugin_share_remote_ids_by_local_path(
|
||||
config.codex_home.as_path(),
|
||||
)
|
||||
.unwrap_or_else(|err| {
|
||||
warn!("failed to load plugin share local path mapping: {err}");
|
||||
std::collections::BTreeMap::new()
|
||||
.map_err(|err| {
|
||||
internal_error(format!(
|
||||
"failed to load plugin share local path mapping: {err}"
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -108,10 +111,11 @@ fn share_context_for_source(
|
||||
.cloned()
|
||||
.map(|remote_plugin_id| PluginShareContext {
|
||||
remote_plugin_id,
|
||||
discoverability: None,
|
||||
share_url: None,
|
||||
creator_account_user_id: None,
|
||||
creator_name: None,
|
||||
share_targets: None,
|
||||
share_principals: None,
|
||||
}),
|
||||
MarketplacePluginSource::Git { .. } => None,
|
||||
}
|
||||
@@ -160,6 +164,35 @@ fn validate_client_plugin_share_targets(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn remote_plugin_share_target_role(
|
||||
role: PluginShareTargetRole,
|
||||
) -> codex_core_plugins::remote::RemotePluginShareTargetRole {
|
||||
match role {
|
||||
PluginShareTargetRole::Reader => {
|
||||
codex_core_plugins::remote::RemotePluginShareTargetRole::Reader
|
||||
}
|
||||
PluginShareTargetRole::Editor => {
|
||||
codex_core_plugins::remote::RemotePluginShareTargetRole::Editor
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn plugin_share_principal_role_from_remote(
|
||||
role: codex_core_plugins::remote::RemotePluginSharePrincipalRole,
|
||||
) -> PluginSharePrincipalRole {
|
||||
match role {
|
||||
codex_core_plugins::remote::RemotePluginSharePrincipalRole::Reader => {
|
||||
PluginSharePrincipalRole::Reader
|
||||
}
|
||||
codex_core_plugins::remote::RemotePluginSharePrincipalRole::Editor => {
|
||||
PluginSharePrincipalRole::Editor
|
||||
}
|
||||
codex_core_plugins::remote::RemotePluginSharePrincipalRole::Owner => {
|
||||
PluginSharePrincipalRole::Owner
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn remote_plugin_share_targets(
|
||||
targets: Vec<PluginShareTarget>,
|
||||
) -> Vec<codex_core_plugins::remote::RemotePluginShareTarget> {
|
||||
@@ -179,6 +212,7 @@ fn remote_plugin_share_targets(
|
||||
}
|
||||
},
|
||||
principal_id: target.principal_id,
|
||||
role: remote_plugin_share_target_role(target.role),
|
||||
},
|
||||
)
|
||||
.collect()
|
||||
@@ -200,6 +234,7 @@ fn plugin_share_principal_from_remote(
|
||||
}
|
||||
},
|
||||
principal_id: principal.principal_id,
|
||||
role: plugin_share_principal_role_from_remote(principal.role),
|
||||
name: principal.name,
|
||||
}
|
||||
}
|
||||
@@ -415,7 +450,7 @@ impl PluginRequestProcessor {
|
||||
|
||||
let config_for_marketplace_listing = plugins_input.clone();
|
||||
let plugins_manager_for_marketplace_listing = plugins_manager.clone();
|
||||
let shared_plugin_ids_by_local_path = load_shared_plugin_ids_by_local_path(&config);
|
||||
let shared_plugin_ids_by_local_path = load_shared_plugin_ids_by_local_path(&config)?;
|
||||
match tokio::task::spawn_blocking(move || {
|
||||
let outcome = plugins_manager_for_marketplace_listing
|
||||
.list_marketplaces_for_config(&config_for_marketplace_listing, &roots)?;
|
||||
@@ -526,10 +561,25 @@ impl PluginRequestProcessor {
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(
|
||||
err @ (RemotePluginCatalogError::AuthRequired
|
||||
| RemotePluginCatalogError::UnsupportedAuthMode),
|
||||
) if explicit_marketplace_kinds => {
|
||||
return Err(remote_plugin_catalog_error_to_jsonrpc(
|
||||
err,
|
||||
"list remote plugin catalog",
|
||||
));
|
||||
}
|
||||
Err(
|
||||
RemotePluginCatalogError::AuthRequired
|
||||
| RemotePluginCatalogError::UnsupportedAuthMode,
|
||||
) => {}
|
||||
Err(err) if explicit_marketplace_kinds => {
|
||||
return Err(remote_plugin_catalog_error_to_jsonrpc(
|
||||
err,
|
||||
"list remote plugin catalog",
|
||||
));
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(
|
||||
error = %err,
|
||||
@@ -603,11 +653,56 @@ impl PluginRequestProcessor {
|
||||
.read_plugin_for_config(&plugins_input, &request)
|
||||
.await
|
||||
.map_err(|err| Self::marketplace_error(err, "read plugin details"))?;
|
||||
let shared_plugin_ids_by_local_path = load_shared_plugin_ids_by_local_path(&config);
|
||||
let shared_plugin_ids_by_local_path =
|
||||
load_shared_plugin_ids_by_local_path(&config)?;
|
||||
let share_context = share_context_for_source(
|
||||
&outcome.plugin.source,
|
||||
&shared_plugin_ids_by_local_path,
|
||||
);
|
||||
let share_context = match share_context {
|
||||
Some(context) => {
|
||||
let auth = self.auth_manager.auth().await;
|
||||
let remote_plugin_service_config = RemotePluginServiceConfig {
|
||||
chatgpt_base_url: config.chatgpt_base_url.clone(),
|
||||
};
|
||||
match codex_core_plugins::remote::fetch_remote_plugin_share_context(
|
||||
&remote_plugin_service_config,
|
||||
auth.as_ref(),
|
||||
&context.remote_plugin_id,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(Some(remote_share_context))
|
||||
if remote_share_context.share_principals.is_some() =>
|
||||
{
|
||||
Some(remote_plugin_share_context_to_info(remote_share_context))
|
||||
}
|
||||
Ok(Some(_)) => {
|
||||
warn!(
|
||||
remote_plugin_id = %context.remote_plugin_id,
|
||||
"remote shared plugin detail did not include share principals; returning local share mapping context"
|
||||
);
|
||||
Some(context)
|
||||
}
|
||||
Ok(None) => {
|
||||
warn!(
|
||||
remote_plugin_id = %context.remote_plugin_id,
|
||||
"remote shared plugin detail did not include share context; returning local share mapping context"
|
||||
);
|
||||
Some(context)
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(
|
||||
remote_plugin_id = %context.remote_plugin_id,
|
||||
error = %err,
|
||||
"failed to hydrate local plugin share context; returning local share mapping context"
|
||||
);
|
||||
Some(context)
|
||||
}
|
||||
}
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
let environment_manager = self.thread_manager.environment_manager();
|
||||
let app_summaries =
|
||||
load_plugin_app_summaries(&config, &outcome.plugin.apps, &environment_manager)
|
||||
@@ -807,7 +902,6 @@ impl PluginRequestProcessor {
|
||||
return Err(invalid_request("invalid remote plugin id"));
|
||||
}
|
||||
validate_client_plugin_share_targets(&share_targets)?;
|
||||
let requested_share_targets = share_targets.clone();
|
||||
|
||||
let remote_plugin_service_config = RemotePluginServiceConfig {
|
||||
chatgpt_base_url: config.chatgpt_base_url.clone(),
|
||||
@@ -829,12 +923,6 @@ impl PluginRequestProcessor {
|
||||
.principals
|
||||
.into_iter()
|
||||
.map(plugin_share_principal_from_remote)
|
||||
.filter(|principal| {
|
||||
requested_share_targets.iter().any(|target| {
|
||||
target.principal_type == principal.principal_type
|
||||
&& target.principal_id == principal.principal_id
|
||||
})
|
||||
})
|
||||
.collect(),
|
||||
discoverability: remote_plugin_share_discoverability_to_info(result.discoverability),
|
||||
})
|
||||
@@ -859,13 +947,11 @@ impl PluginRequestProcessor {
|
||||
.map(|summary| {
|
||||
let RemoteCatalogPluginShareSummary {
|
||||
summary,
|
||||
share_url,
|
||||
local_plugin_path,
|
||||
} = summary;
|
||||
let plugin = remote_plugin_summary_to_info(summary);
|
||||
PluginShareListItem {
|
||||
plugin,
|
||||
share_url: share_url.unwrap_or_default(),
|
||||
local_plugin_path,
|
||||
}
|
||||
})
|
||||
@@ -1521,11 +1607,14 @@ fn remote_plugin_share_context_to_info(
|
||||
) -> PluginShareContext {
|
||||
PluginShareContext {
|
||||
remote_plugin_id: context.remote_plugin_id,
|
||||
discoverability: Some(remote_plugin_share_discoverability_to_info(
|
||||
context.discoverability,
|
||||
)),
|
||||
share_url: context.share_url,
|
||||
creator_account_user_id: context.creator_account_user_id,
|
||||
creator_name: context.creator_name,
|
||||
share_targets: context.share_targets.map(|targets| {
|
||||
targets
|
||||
share_principals: context.share_principals.map(|principals| {
|
||||
principals
|
||||
.into_iter()
|
||||
.map(plugin_share_principal_from_remote)
|
||||
.collect()
|
||||
|
||||
@@ -13,8 +13,7 @@ use codex_app_server_protocol::PluginListMarketplaceKind;
|
||||
use codex_app_server_protocol::PluginListParams;
|
||||
use codex_app_server_protocol::PluginListResponse;
|
||||
use codex_app_server_protocol::PluginMarketplaceEntry;
|
||||
use codex_app_server_protocol::PluginSharePrincipal;
|
||||
use codex_app_server_protocol::PluginSharePrincipalType;
|
||||
use codex_app_server_protocol::PluginShareDiscoverability;
|
||||
use codex_app_server_protocol::PluginSource;
|
||||
use codex_app_server_protocol::PluginSummary;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
@@ -694,10 +693,11 @@ async fn plugin_list_returns_share_context_for_shared_local_plugin() -> Result<(
|
||||
.as_ref()
|
||||
.expect("expected share context");
|
||||
assert_eq!(share_context.remote_plugin_id, "plugins_123");
|
||||
assert_eq!(share_context.discoverability, None);
|
||||
assert_eq!(share_context.share_url, None);
|
||||
assert_eq!(share_context.creator_account_user_id, None);
|
||||
assert_eq!(share_context.creator_name, None);
|
||||
assert_eq!(share_context.share_targets, None);
|
||||
assert_eq!(share_context.share_principals, None);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1680,12 +1680,15 @@ async fn plugin_list_fetches_shared_with_me_kind() -> Result<()> {
|
||||
AuthCredentialsStoreMode::File,
|
||||
)?;
|
||||
|
||||
let shared_plugin_body = workspace_remote_plugin_page_body(
|
||||
"plugins~Plugin_22222222222222222222222222222222",
|
||||
"shared-linear",
|
||||
"Shared Linear",
|
||||
/*enabled*/ None,
|
||||
);
|
||||
let mut shared_plugin_body: serde_json::Value =
|
||||
serde_json::from_str(&workspace_remote_plugin_page_body(
|
||||
"plugins~Plugin_22222222222222222222222222222222",
|
||||
"shared-linear",
|
||||
"Shared Linear",
|
||||
/*enabled*/ None,
|
||||
))?;
|
||||
shared_plugin_body["plugins"][0]["share_principals"] = serde_json::Value::Null;
|
||||
let shared_plugin_body = serde_json::to_string(&shared_plugin_body)?;
|
||||
let workspace_installed_body = workspace_remote_plugin_page_body(
|
||||
"plugins~Plugin_22222222222222222222222222222222",
|
||||
"shared-linear",
|
||||
@@ -1734,6 +1737,10 @@ async fn plugin_list_fetches_shared_with_me_kind() -> Result<()> {
|
||||
share_context.remote_plugin_id,
|
||||
"plugins~Plugin_22222222222222222222222222222222"
|
||||
);
|
||||
assert_eq!(
|
||||
share_context.discoverability,
|
||||
Some(PluginShareDiscoverability::Private)
|
||||
);
|
||||
assert_eq!(
|
||||
share_context.creator_account_user_id.as_deref(),
|
||||
Some("user-gavin__account-123")
|
||||
@@ -1743,14 +1750,7 @@ async fn plugin_list_fetches_shared_with_me_kind() -> Result<()> {
|
||||
share_context.share_url.as_deref(),
|
||||
Some("https://chatgpt.example/plugins/share/share-key-1")
|
||||
);
|
||||
assert_eq!(
|
||||
share_context.share_targets,
|
||||
Some(vec![PluginSharePrincipal {
|
||||
principal_type: PluginSharePrincipalType::User,
|
||||
principal_id: "user-ada__account-123".to_string(),
|
||||
name: "Ada".to_string(),
|
||||
}])
|
||||
);
|
||||
assert_eq!(share_context.share_principals, None);
|
||||
wait_for_remote_plugin_request_count(&server, "/ps/plugins/list", /*expected_count*/ 0).await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -2275,6 +2275,7 @@ fn workspace_remote_plugin_page_body(
|
||||
"id": "{remote_plugin_id}",
|
||||
"name": "{plugin_name}",
|
||||
"scope": "WORKSPACE",
|
||||
"discoverability": "PRIVATE",
|
||||
"creator_account_user_id": "user-gavin__account-123",
|
||||
"share_url": "https://chatgpt.example/plugins/share/share-key-1",
|
||||
"installation_policy": "AVAILABLE",
|
||||
|
||||
@@ -18,12 +18,15 @@ use axum::http::header::AUTHORIZATION;
|
||||
use axum::routing::get;
|
||||
use codex_app_server_protocol::AppInfo;
|
||||
use codex_app_server_protocol::HookEventName;
|
||||
use codex_app_server_protocol::JSONRPCError;
|
||||
use codex_app_server_protocol::JSONRPCResponse;
|
||||
use codex_app_server_protocol::PluginAuthPolicy;
|
||||
use codex_app_server_protocol::PluginInstallPolicy;
|
||||
use codex_app_server_protocol::PluginReadParams;
|
||||
use codex_app_server_protocol::PluginReadResponse;
|
||||
use codex_app_server_protocol::PluginShareDiscoverability;
|
||||
use codex_app_server_protocol::PluginSharePrincipal;
|
||||
use codex_app_server_protocol::PluginSharePrincipalRole;
|
||||
use codex_app_server_protocol::PluginSharePrincipalType;
|
||||
use codex_app_server_protocol::PluginSkillReadParams;
|
||||
use codex_app_server_protocol::PluginSkillReadResponse;
|
||||
@@ -237,6 +240,7 @@ async fn plugin_read_returns_share_context_for_shared_remote_plugin() -> Result<
|
||||
"id": "plugins~Plugin_11111111111111111111111111111111",
|
||||
"name": "shared-linear",
|
||||
"scope": "WORKSPACE",
|
||||
"discoverability": "PRIVATE",
|
||||
"creator_account_user_id": "user-gavin__account-123",
|
||||
"creator_name": "Gavin",
|
||||
"share_url": "https://chatgpt.example/plugins/share/share-key-1",
|
||||
@@ -319,6 +323,10 @@ async fn plugin_read_returns_share_context_for_shared_remote_plugin() -> Result<
|
||||
share_context.remote_plugin_id,
|
||||
"plugins~Plugin_11111111111111111111111111111111"
|
||||
);
|
||||
assert_eq!(
|
||||
share_context.discoverability,
|
||||
Some(PluginShareDiscoverability::Private)
|
||||
);
|
||||
assert_eq!(
|
||||
share_context.creator_account_user_id.as_deref(),
|
||||
Some("user-gavin__account-123")
|
||||
@@ -329,12 +337,21 @@ async fn plugin_read_returns_share_context_for_shared_remote_plugin() -> Result<
|
||||
Some("https://chatgpt.example/plugins/share/share-key-1")
|
||||
);
|
||||
assert_eq!(
|
||||
share_context.share_targets,
|
||||
Some(vec![PluginSharePrincipal {
|
||||
principal_type: PluginSharePrincipalType::User,
|
||||
principal_id: "user-ada__account-123".to_string(),
|
||||
name: "Ada".to_string(),
|
||||
}])
|
||||
share_context.share_principals,
|
||||
Some(vec![
|
||||
PluginSharePrincipal {
|
||||
principal_type: PluginSharePrincipalType::User,
|
||||
principal_id: "user-gavin__account-123".to_string(),
|
||||
role: PluginSharePrincipalRole::Owner,
|
||||
name: "Gavin".to_string(),
|
||||
},
|
||||
PluginSharePrincipal {
|
||||
principal_type: PluginSharePrincipalType::User,
|
||||
principal_id: "user-ada__account-123".to_string(),
|
||||
role: PluginSharePrincipalRole::Reader,
|
||||
name: "Ada".to_string(),
|
||||
},
|
||||
])
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
@@ -751,6 +768,19 @@ enabled = true
|
||||
async fn plugin_read_returns_share_context_for_shared_local_plugin() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let repo_root = 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,
|
||||
)?;
|
||||
write_plugin_marketplace(
|
||||
repo_root.path(),
|
||||
"codex-curated",
|
||||
@@ -764,7 +794,120 @@ async fn plugin_read_returns_share_context_for_shared_local_plugin() -> Result<(
|
||||
.join("demo-plugin/.codex-plugin/plugin.json"),
|
||||
r#"{"name":"demo-plugin"}"#,
|
||||
)?;
|
||||
let plugin_path = AbsolutePathBuf::try_from(repo_root.path().join("demo-plugin"))?;
|
||||
write_plugin_share_local_path_mapping(codex_home.path(), "plugins_123", &plugin_path)?;
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/backend-api/ps/plugins/plugins_123"))
|
||||
.and(header("authorization", "Bearer chatgpt-token"))
|
||||
.and(header("chatgpt-account-id", "account-123"))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
|
||||
"id": "plugins_123",
|
||||
"name": "demo-plugin",
|
||||
"scope": "WORKSPACE",
|
||||
"discoverability": "UNLISTED",
|
||||
"creator_account_user_id": "user-owner__account-123",
|
||||
"creator_name": "Owner",
|
||||
"share_url": "https://chatgpt.example/plugins/share/share-key-1",
|
||||
"share_principals": [
|
||||
{
|
||||
"principal_type": "user",
|
||||
"principal_id": "user-owner__account-123",
|
||||
"role": "owner",
|
||||
"name": "Owner",
|
||||
},
|
||||
{
|
||||
"principal_type": "user",
|
||||
"principal_id": "user-editor__account-123",
|
||||
"role": "editor",
|
||||
"name": "Editor",
|
||||
},
|
||||
],
|
||||
"installation_policy": "AVAILABLE",
|
||||
"authentication_policy": "ON_USE",
|
||||
"release": {
|
||||
"display_name": "Demo Plugin",
|
||||
"description": "Shared local plugin",
|
||||
"app_ids": [],
|
||||
"keywords": [],
|
||||
"interface": {},
|
||||
"skills": []
|
||||
}
|
||||
})))
|
||||
.expect(1)
|
||||
.mount(&server)
|
||||
.await;
|
||||
let mut mcp = McpProcess::new(codex_home.path()).await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
.send_plugin_read_request(PluginReadParams {
|
||||
marketplace_path: Some(AbsolutePathBuf::try_from(
|
||||
repo_root.path().join(".agents/plugins/marketplace.json"),
|
||||
)?),
|
||||
remote_marketplace_name: None,
|
||||
plugin_name: "demo-plugin".to_string(),
|
||||
})
|
||||
.await?;
|
||||
|
||||
let response: JSONRPCResponse = timeout(
|
||||
DEFAULT_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
|
||||
)
|
||||
.await??;
|
||||
let response: PluginReadResponse = to_response(response)?;
|
||||
|
||||
let share_context = response
|
||||
.plugin
|
||||
.summary
|
||||
.share_context
|
||||
.as_ref()
|
||||
.expect("expected share context");
|
||||
assert_eq!(share_context.remote_plugin_id, "plugins_123");
|
||||
assert_eq!(
|
||||
share_context.discoverability,
|
||||
Some(PluginShareDiscoverability::Unlisted)
|
||||
);
|
||||
assert_eq!(
|
||||
share_context.share_url.as_deref(),
|
||||
Some("https://chatgpt.example/plugins/share/share-key-1")
|
||||
);
|
||||
assert_eq!(
|
||||
share_context.creator_account_user_id.as_deref(),
|
||||
Some("user-owner__account-123")
|
||||
);
|
||||
assert_eq!(share_context.creator_name.as_deref(), Some("Owner"));
|
||||
assert_eq!(
|
||||
share_context.share_principals,
|
||||
Some(vec![
|
||||
PluginSharePrincipal {
|
||||
principal_type: PluginSharePrincipalType::User,
|
||||
principal_id: "user-owner__account-123".to_string(),
|
||||
role: PluginSharePrincipalRole::Owner,
|
||||
name: "Owner".to_string(),
|
||||
},
|
||||
PluginSharePrincipal {
|
||||
principal_type: PluginSharePrincipalType::User,
|
||||
principal_id: "user-editor__account-123".to_string(),
|
||||
role: PluginSharePrincipalRole::Editor,
|
||||
name: "Editor".to_string(),
|
||||
},
|
||||
])
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugin_read_falls_back_to_local_share_context_without_remote_auth() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let repo_root = TempDir::new()?;
|
||||
write_plugins_enabled_config(&codex_home)?;
|
||||
write_plugin_marketplace(
|
||||
repo_root.path(),
|
||||
"codex-curated",
|
||||
"demo-plugin",
|
||||
"./demo-plugin",
|
||||
)?;
|
||||
write_plugin_source(repo_root.path(), "demo-plugin", &[])?;
|
||||
let plugin_path = AbsolutePathBuf::try_from(repo_root.path().join("demo-plugin"))?;
|
||||
write_plugin_share_local_path_mapping(codex_home.path(), "plugins_123", &plugin_path)?;
|
||||
|
||||
@@ -795,10 +938,60 @@ async fn plugin_read_returns_share_context_for_shared_local_plugin() -> Result<(
|
||||
.as_ref()
|
||||
.expect("expected share context");
|
||||
assert_eq!(share_context.remote_plugin_id, "plugins_123");
|
||||
assert_eq!(share_context.discoverability, None);
|
||||
assert_eq!(share_context.share_url, None);
|
||||
assert_eq!(share_context.creator_account_user_id, None);
|
||||
assert_eq!(share_context.creator_name, None);
|
||||
assert_eq!(share_context.share_targets, None);
|
||||
assert_eq!(share_context.share_principals, None);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugin_read_fails_on_malformed_share_mapping() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let repo_root = TempDir::new()?;
|
||||
write_plugins_enabled_config(&codex_home)?;
|
||||
write_plugin_marketplace(
|
||||
repo_root.path(),
|
||||
"codex-curated",
|
||||
"demo-plugin",
|
||||
"./demo-plugin",
|
||||
)?;
|
||||
write_plugin_source(repo_root.path(), "demo-plugin", &[])?;
|
||||
std::fs::create_dir_all(codex_home.path().join(".tmp"))?;
|
||||
std::fs::write(
|
||||
codex_home
|
||||
.path()
|
||||
.join(".tmp/plugin-share-local-paths-v1.json"),
|
||||
"not valid json\n",
|
||||
)?;
|
||||
|
||||
let mut mcp = McpProcess::new(codex_home.path()).await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
.send_plugin_read_request(PluginReadParams {
|
||||
marketplace_path: Some(AbsolutePathBuf::try_from(
|
||||
repo_root.path().join(".agents/plugins/marketplace.json"),
|
||||
)?),
|
||||
remote_marketplace_name: None,
|
||||
plugin_name: "demo-plugin".to_string(),
|
||||
})
|
||||
.await?;
|
||||
|
||||
let error: JSONRPCError = timeout(
|
||||
DEFAULT_TIMEOUT,
|
||||
mcp.read_stream_until_error_message(RequestId::Integer(request_id)),
|
||||
)
|
||||
.await??;
|
||||
|
||||
assert_eq!(error.error.code, -32603);
|
||||
assert!(
|
||||
error
|
||||
.error
|
||||
.message
|
||||
.contains("failed to load plugin share local path mapping")
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -14,9 +14,11 @@ use codex_app_server_protocol::PluginInstallPolicy;
|
||||
use codex_app_server_protocol::PluginInterface;
|
||||
use codex_app_server_protocol::PluginShareContext;
|
||||
use codex_app_server_protocol::PluginShareDeleteResponse;
|
||||
use codex_app_server_protocol::PluginShareDiscoverability;
|
||||
use codex_app_server_protocol::PluginShareListItem;
|
||||
use codex_app_server_protocol::PluginShareListResponse;
|
||||
use codex_app_server_protocol::PluginSharePrincipal;
|
||||
use codex_app_server_protocol::PluginSharePrincipalRole;
|
||||
use codex_app_server_protocol::PluginSharePrincipalType;
|
||||
use codex_app_server_protocol::PluginShareSaveResponse;
|
||||
use codex_app_server_protocol::PluginShareUpdateTargetsResponse;
|
||||
@@ -172,7 +174,6 @@ async fn plugin_share_save_uploads_local_plugin() -> Result<()> {
|
||||
interface: Some(expected_plugin_interface()),
|
||||
keywords: Vec::new(),
|
||||
},
|
||||
share_url: "https://chatgpt.example/plugins/share/share-key-1".to_string(),
|
||||
local_plugin_path: Some(expected_plugin_path),
|
||||
}],
|
||||
}
|
||||
@@ -224,10 +225,12 @@ async fn plugin_share_save_forwards_access_policy() -> Result<()> {
|
||||
{
|
||||
"principal_type": "user",
|
||||
"principal_id": "user-1",
|
||||
"role": "editor",
|
||||
},
|
||||
{
|
||||
"principal_type": "workspace",
|
||||
"principal_id": "account-123",
|
||||
"role": "reader",
|
||||
},
|
||||
],
|
||||
})))
|
||||
@@ -252,6 +255,7 @@ async fn plugin_share_save_forwards_access_policy() -> Result<()> {
|
||||
{
|
||||
"principalType": "user",
|
||||
"principalId": "user-1",
|
||||
"role": "editor",
|
||||
},
|
||||
],
|
||||
})),
|
||||
@@ -345,6 +349,7 @@ async fn plugin_share_rejects_workspace_targets_from_client() -> Result<()> {
|
||||
{
|
||||
"principalType": "workspace",
|
||||
"principalId": "account-123",
|
||||
"role": "reader",
|
||||
},
|
||||
],
|
||||
})),
|
||||
@@ -373,6 +378,7 @@ async fn plugin_share_rejects_workspace_targets_from_client() -> Result<()> {
|
||||
{
|
||||
"principalType": "workspace",
|
||||
"principalId": "account-123",
|
||||
"role": "reader",
|
||||
},
|
||||
],
|
||||
})),
|
||||
@@ -422,6 +428,7 @@ async fn plugin_share_save_rejects_access_policy_for_existing_plugin() -> Result
|
||||
{
|
||||
"principalType": "user",
|
||||
"principalId": "user-1",
|
||||
"role": "reader",
|
||||
},
|
||||
],
|
||||
})),
|
||||
@@ -511,7 +518,6 @@ async fn plugin_share_list_returns_created_workspace_plugins() -> Result<()> {
|
||||
interface: Some(expected_plugin_interface()),
|
||||
keywords: Vec::new(),
|
||||
},
|
||||
share_url: "https://chatgpt.example/plugins/share/share-key-1".to_string(),
|
||||
local_plugin_path: None,
|
||||
}],
|
||||
}
|
||||
@@ -543,10 +549,12 @@ async fn plugin_share_update_targets_updates_share_targets() -> Result<()> {
|
||||
{
|
||||
"principal_type": "user",
|
||||
"principal_id": "user-1",
|
||||
"role": "editor",
|
||||
},
|
||||
{
|
||||
"principal_type": "workspace",
|
||||
"principal_id": "account-123",
|
||||
"role": "reader",
|
||||
},
|
||||
],
|
||||
})))
|
||||
@@ -555,19 +563,23 @@ async fn plugin_share_update_targets_updates_share_targets() -> Result<()> {
|
||||
{
|
||||
"principal_type": "user",
|
||||
"principal_id": "owner-1",
|
||||
"role": "owner",
|
||||
"name": "Owner",
|
||||
},
|
||||
{
|
||||
"principal_type": "user",
|
||||
"principal_id": "user-1",
|
||||
"role": "editor",
|
||||
"name": "Gavin",
|
||||
},
|
||||
{
|
||||
"principal_type": "workspace",
|
||||
"principal_id": "account-123",
|
||||
"role": "reader",
|
||||
"name": "Workspace",
|
||||
},
|
||||
],
|
||||
"discoverability": "UNLISTED",
|
||||
})))
|
||||
.expect(1)
|
||||
.mount(&server)
|
||||
@@ -585,6 +597,7 @@ async fn plugin_share_update_targets_updates_share_targets() -> Result<()> {
|
||||
{
|
||||
"principalType": "user",
|
||||
"principalId": "user-1",
|
||||
"role": "editor",
|
||||
},
|
||||
],
|
||||
})),
|
||||
@@ -601,11 +614,26 @@ async fn plugin_share_update_targets_updates_share_targets() -> Result<()> {
|
||||
assert_eq!(
|
||||
response,
|
||||
PluginShareUpdateTargetsResponse {
|
||||
principals: vec![PluginSharePrincipal {
|
||||
principal_type: PluginSharePrincipalType::User,
|
||||
principal_id: "user-1".to_string(),
|
||||
name: "Gavin".to_string(),
|
||||
}],
|
||||
principals: vec![
|
||||
PluginSharePrincipal {
|
||||
principal_type: PluginSharePrincipalType::User,
|
||||
principal_id: "owner-1".to_string(),
|
||||
role: PluginSharePrincipalRole::Owner,
|
||||
name: "Owner".to_string(),
|
||||
},
|
||||
PluginSharePrincipal {
|
||||
principal_type: PluginSharePrincipalType::User,
|
||||
principal_id: "user-1".to_string(),
|
||||
role: PluginSharePrincipalRole::Editor,
|
||||
name: "Gavin".to_string(),
|
||||
},
|
||||
PluginSharePrincipal {
|
||||
principal_type: PluginSharePrincipalType::Workspace,
|
||||
principal_id: "account-123".to_string(),
|
||||
role: PluginSharePrincipalRole::Reader,
|
||||
name: "Workspace".to_string(),
|
||||
},
|
||||
],
|
||||
discoverability: codex_app_server_protocol::PluginShareDiscoverability::Unlisted,
|
||||
}
|
||||
);
|
||||
@@ -709,7 +737,6 @@ async fn plugin_share_delete_removes_created_workspace_plugin() -> Result<()> {
|
||||
interface: Some(expected_plugin_interface()),
|
||||
keywords: Vec::new(),
|
||||
},
|
||||
share_url: "https://chatgpt.example/plugins/share/share-key-1".to_string(),
|
||||
local_plugin_path: None,
|
||||
}],
|
||||
}
|
||||
@@ -737,7 +764,22 @@ fn remote_plugin_json(plugin_id: &str) -> serde_json::Value {
|
||||
"id": plugin_id,
|
||||
"name": "demo-plugin",
|
||||
"scope": "WORKSPACE",
|
||||
"discoverability": "PRIVATE",
|
||||
"share_url": "https://chatgpt.example/plugins/share/share-key-1",
|
||||
"share_principals": [
|
||||
{
|
||||
"principal_type": "user",
|
||||
"principal_id": "user-owner__account-123",
|
||||
"role": "owner",
|
||||
"name": "Owner"
|
||||
},
|
||||
{
|
||||
"principal_type": "user",
|
||||
"principal_id": "user-reader__account-123",
|
||||
"role": "reader",
|
||||
"name": "Reader"
|
||||
}
|
||||
],
|
||||
"installation_policy": "AVAILABLE",
|
||||
"authentication_policy": "ON_USE",
|
||||
"release": {
|
||||
@@ -793,10 +835,24 @@ fn expected_plugin_interface() -> PluginInterface {
|
||||
fn expected_share_context(plugin_id: &str) -> PluginShareContext {
|
||||
PluginShareContext {
|
||||
remote_plugin_id: plugin_id.to_string(),
|
||||
discoverability: Some(PluginShareDiscoverability::Private),
|
||||
share_url: Some("https://chatgpt.example/plugins/share/share-key-1".to_string()),
|
||||
creator_account_user_id: None,
|
||||
creator_name: None,
|
||||
share_targets: None,
|
||||
share_principals: Some(vec![
|
||||
PluginSharePrincipal {
|
||||
principal_type: PluginSharePrincipalType::User,
|
||||
principal_id: "user-owner__account-123".to_string(),
|
||||
role: PluginSharePrincipalRole::Owner,
|
||||
name: "Owner".to_string(),
|
||||
},
|
||||
PluginSharePrincipal {
|
||||
principal_type: PluginSharePrincipalType::User,
|
||||
principal_id: "user-reader__account-123".to_string(),
|
||||
role: PluginSharePrincipalRole::Reader,
|
||||
name: "Reader".to_string(),
|
||||
},
|
||||
]),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -32,9 +32,11 @@ pub use remote_installed_plugin_sync::sync_remote_installed_plugin_bundles_once;
|
||||
pub use share::RemotePluginShareAccessPolicy;
|
||||
pub use share::RemotePluginShareDiscoverability;
|
||||
pub use share::RemotePluginSharePrincipal;
|
||||
pub use share::RemotePluginSharePrincipalRole;
|
||||
pub use share::RemotePluginSharePrincipalType;
|
||||
pub use share::RemotePluginShareSaveResult;
|
||||
pub use share::RemotePluginShareTarget;
|
||||
pub use share::RemotePluginShareTargetRole;
|
||||
pub use share::RemotePluginShareUpdateDiscoverability;
|
||||
pub use share::RemotePluginShareUpdateTargetsResult;
|
||||
pub use share::delete_remote_plugin_share;
|
||||
@@ -99,16 +101,16 @@ pub struct RemotePluginSummary {
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct RemotePluginShareContext {
|
||||
pub remote_plugin_id: String,
|
||||
pub discoverability: RemotePluginShareDiscoverability,
|
||||
pub share_url: Option<String>,
|
||||
pub creator_account_user_id: Option<String>,
|
||||
pub creator_name: Option<String>,
|
||||
pub share_targets: Option<Vec<RemotePluginSharePrincipal>>,
|
||||
pub share_principals: Option<Vec<RemotePluginSharePrincipal>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct RemotePluginShareSummary {
|
||||
pub summary: RemotePluginSummary,
|
||||
pub share_url: Option<String>,
|
||||
pub local_plugin_path: Option<AbsolutePathBuf>,
|
||||
}
|
||||
|
||||
@@ -361,6 +363,8 @@ struct RemotePluginDirectoryItem {
|
||||
name: String,
|
||||
scope: RemotePluginScope,
|
||||
#[serde(default)]
|
||||
discoverability: Option<RemotePluginShareDiscoverability>,
|
||||
#[serde(default)]
|
||||
creator_account_user_id: Option<String>,
|
||||
#[serde(default)]
|
||||
creator_name: Option<String>,
|
||||
@@ -379,8 +383,7 @@ struct RemotePluginDirectoryItem {
|
||||
struct RemotePluginDirectorySharePrincipal {
|
||||
principal_type: RemotePluginSharePrincipalType,
|
||||
principal_id: String,
|
||||
#[serde(default)]
|
||||
role: Option<String>,
|
||||
role: RemotePluginSharePrincipalRole,
|
||||
name: String,
|
||||
}
|
||||
|
||||
@@ -444,7 +447,7 @@ pub async fn fetch_remote_marketplaces(
|
||||
directory_plugins,
|
||||
installed_plugins,
|
||||
/*include_installed_only*/ true,
|
||||
)
|
||||
)?
|
||||
}
|
||||
RemoteMarketplaceSource::WorkspaceDirectory => {
|
||||
let scope = RemotePluginScope::Workspace;
|
||||
@@ -456,7 +459,7 @@ pub async fn fetch_remote_marketplaces(
|
||||
directory_plugins,
|
||||
workspace_installed_plugins.clone().unwrap_or_default(),
|
||||
/*include_installed_only*/ false,
|
||||
)
|
||||
)?
|
||||
}
|
||||
RemoteMarketplaceSource::SharedWithMe => build_remote_marketplace(
|
||||
REMOTE_SHARED_WITH_ME_MARKETPLACE_NAME,
|
||||
@@ -464,7 +467,7 @@ pub async fn fetch_remote_marketplaces(
|
||||
fetch_shared_workspace_plugins(config, auth).await?,
|
||||
workspace_installed_plugins.clone().unwrap_or_default(),
|
||||
/*include_installed_only*/ false,
|
||||
),
|
||||
)?,
|
||||
};
|
||||
if let Some(marketplace) = marketplace {
|
||||
marketplaces.push(marketplace);
|
||||
@@ -480,7 +483,7 @@ fn build_remote_marketplace(
|
||||
directory_plugins: Vec<RemotePluginDirectoryItem>,
|
||||
installed_plugins: Vec<RemotePluginInstalledItem>,
|
||||
include_installed_only: bool,
|
||||
) -> Option<RemoteMarketplace> {
|
||||
) -> Result<Option<RemoteMarketplace>, RemotePluginCatalogError> {
|
||||
let directory_plugins = directory_plugins
|
||||
.into_iter()
|
||||
.map(|plugin| (plugin.id.clone(), plugin))
|
||||
@@ -500,7 +503,7 @@ fn build_remote_marketplace(
|
||||
.cloned()
|
||||
.collect::<BTreeSet<_>>();
|
||||
if plugin_ids.is_empty() {
|
||||
return None;
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let mut plugins = plugin_ids
|
||||
@@ -510,9 +513,10 @@ fn build_remote_marketplace(
|
||||
let installed_plugin = installed_plugins.get(&plugin_id);
|
||||
directory_plugin
|
||||
.or_else(|| installed_plugin.map(|plugin| &plugin.plugin))
|
||||
.map(|plugin| build_remote_plugin_summary(plugin, installed_plugin))
|
||||
.map(|plugin| (plugin, installed_plugin))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
.map(|(plugin, installed_plugin)| build_remote_plugin_summary(plugin, installed_plugin))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
plugins.sort_by(|left, right| {
|
||||
remote_plugin_display_name(left)
|
||||
.to_ascii_lowercase()
|
||||
@@ -520,11 +524,11 @@ fn build_remote_marketplace(
|
||||
.then_with(|| remote_plugin_display_name(left).cmp(remote_plugin_display_name(right)))
|
||||
.then_with(|| left.id.cmp(&right.id))
|
||||
});
|
||||
Some(RemoteMarketplace {
|
||||
Ok(Some(RemoteMarketplace {
|
||||
name: name.to_string(),
|
||||
display_name: display_name.to_string(),
|
||||
plugins,
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
pub async fn fetch_remote_installed_plugins(
|
||||
@@ -576,6 +580,19 @@ pub async fn fetch_remote_plugin_detail(
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn fetch_remote_plugin_share_context(
|
||||
config: &RemotePluginServiceConfig,
|
||||
auth: Option<&CodexAuth>,
|
||||
plugin_id: &str,
|
||||
) -> Result<Option<RemotePluginShareContext>, RemotePluginCatalogError> {
|
||||
let auth = ensure_chatgpt_auth(auth)?;
|
||||
let plugin = fetch_plugin_detail(
|
||||
config, auth, plugin_id, /*include_download_urls*/ false,
|
||||
)
|
||||
.await?;
|
||||
remote_plugin_share_context(&plugin)
|
||||
}
|
||||
|
||||
pub async fn fetch_remote_plugin_detail_with_download_urls(
|
||||
config: &RemotePluginServiceConfig,
|
||||
auth: Option<&CodexAuth>,
|
||||
@@ -687,7 +704,7 @@ async fn build_remote_plugin_detail(
|
||||
Ok(RemotePluginDetail {
|
||||
marketplace_name,
|
||||
marketplace_display_name: scope.marketplace_display_name().to_string(),
|
||||
summary: build_remote_plugin_summary(&plugin, installed_plugin.as_ref()),
|
||||
summary: build_remote_plugin_summary(&plugin, installed_plugin.as_ref())?,
|
||||
description: non_empty_string(Some(&plugin.release.description)),
|
||||
release_version: plugin.release.version,
|
||||
bundle_download_url: plugin.release.bundle_download_url,
|
||||
@@ -823,11 +840,11 @@ fn remove_remote_plugin_cache(
|
||||
fn build_remote_plugin_summary(
|
||||
plugin: &RemotePluginDirectoryItem,
|
||||
installed_plugin: Option<&RemotePluginInstalledItem>,
|
||||
) -> RemotePluginSummary {
|
||||
RemotePluginSummary {
|
||||
) -> Result<RemotePluginSummary, RemotePluginCatalogError> {
|
||||
Ok(RemotePluginSummary {
|
||||
id: plugin.id.clone(),
|
||||
name: plugin.name.clone(),
|
||||
share_context: remote_plugin_share_context(plugin),
|
||||
share_context: remote_plugin_share_context(plugin)?,
|
||||
installed: installed_plugin.is_some(),
|
||||
enabled: installed_plugin.is_some_and(|plugin| plugin.enabled),
|
||||
install_policy: plugin.installation_policy,
|
||||
@@ -835,31 +852,40 @@ fn build_remote_plugin_summary(
|
||||
availability: plugin.availability,
|
||||
interface: remote_plugin_interface_to_info(plugin),
|
||||
keywords: plugin.release.keywords.clone(),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn remote_plugin_share_context(
|
||||
plugin: &RemotePluginDirectoryItem,
|
||||
) -> Option<RemotePluginShareContext> {
|
||||
) -> Result<Option<RemotePluginShareContext>, RemotePluginCatalogError> {
|
||||
match plugin.scope {
|
||||
RemotePluginScope::Global => None,
|
||||
RemotePluginScope::Workspace => Some(RemotePluginShareContext {
|
||||
remote_plugin_id: plugin.id.clone(),
|
||||
share_url: plugin.share_url.clone(),
|
||||
creator_account_user_id: plugin.creator_account_user_id.clone(),
|
||||
creator_name: plugin.creator_name.clone(),
|
||||
share_targets: plugin.share_principals.as_ref().map(|principals| {
|
||||
principals
|
||||
.iter()
|
||||
.filter(|principal| principal.role.as_deref() == Some("reader"))
|
||||
.map(|principal| RemotePluginSharePrincipal {
|
||||
principal_type: principal.principal_type,
|
||||
principal_id: principal.principal_id.clone(),
|
||||
name: principal.name.clone(),
|
||||
})
|
||||
.collect()
|
||||
}),
|
||||
}),
|
||||
RemotePluginScope::Global => Ok(None),
|
||||
RemotePluginScope::Workspace => {
|
||||
let discoverability = plugin.discoverability.ok_or_else(|| {
|
||||
RemotePluginCatalogError::UnexpectedResponse(format!(
|
||||
"workspace plugin `{}` did not include discoverability",
|
||||
plugin.id
|
||||
))
|
||||
})?;
|
||||
Ok(Some(RemotePluginShareContext {
|
||||
remote_plugin_id: plugin.id.clone(),
|
||||
discoverability,
|
||||
share_url: plugin.share_url.clone(),
|
||||
creator_account_user_id: plugin.creator_account_user_id.clone(),
|
||||
creator_name: plugin.creator_name.clone(),
|
||||
share_principals: plugin.share_principals.as_ref().map(|share_principals| {
|
||||
share_principals
|
||||
.iter()
|
||||
.map(|principal| RemotePluginSharePrincipal {
|
||||
principal_type: principal.principal_type,
|
||||
principal_id: principal.principal_id.clone(),
|
||||
role: principal.role,
|
||||
name: principal.name.clone(),
|
||||
})
|
||||
.collect()
|
||||
}),
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -59,15 +59,32 @@ pub enum RemotePluginSharePrincipalType {
|
||||
pub struct RemotePluginShareTarget {
|
||||
pub principal_type: RemotePluginSharePrincipalType,
|
||||
pub principal_id: String,
|
||||
pub role: RemotePluginShareTargetRole,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
|
||||
pub struct RemotePluginSharePrincipal {
|
||||
pub principal_type: RemotePluginSharePrincipalType,
|
||||
pub principal_id: String,
|
||||
pub role: RemotePluginSharePrincipalRole,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum RemotePluginShareTargetRole {
|
||||
Reader,
|
||||
Editor,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum RemotePluginSharePrincipalRole {
|
||||
Reader,
|
||||
Editor,
|
||||
Owner,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct RemotePluginShareUpdateTargetsResult {
|
||||
pub principals: Vec<RemotePluginSharePrincipal>,
|
||||
@@ -115,7 +132,7 @@ struct RemotePluginShareUpdateTargetsRequest {
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
|
||||
struct RemotePluginShareUpdateTargetsResponse {
|
||||
principals: Vec<RemotePluginSharePrincipal>,
|
||||
discoverability: Option<RemotePluginShareDiscoverability>,
|
||||
discoverability: RemotePluginShareDiscoverability,
|
||||
}
|
||||
|
||||
pub async fn save_remote_plugin_share(
|
||||
@@ -203,33 +220,54 @@ pub async fn list_remote_plugin_shares(
|
||||
.map(|plugin| (plugin.plugin.id.clone(), plugin))
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
let local_plugin_paths =
|
||||
local_paths::load_plugin_share_local_paths(codex_home).unwrap_or_else(|err| {
|
||||
warn!("failed to load plugin share local path mapping: {err}");
|
||||
BTreeMap::new()
|
||||
});
|
||||
local_paths::load_plugin_share_local_paths(codex_home).map_err(|err| {
|
||||
RemotePluginCatalogError::UnexpectedResponse(format!(
|
||||
"failed to load plugin share local path mapping: {err}"
|
||||
))
|
||||
})?;
|
||||
|
||||
Ok(created_plugins
|
||||
created_plugins
|
||||
.into_iter()
|
||||
.map(|plugin| {
|
||||
let summary = build_remote_plugin_summary(&plugin, installed_by_id.get(&plugin.id));
|
||||
let local_plugin_path = local_plugin_paths.get(&plugin.id).cloned();
|
||||
RemotePluginShareSummary {
|
||||
summary,
|
||||
share_url: plugin.share_url,
|
||||
local_plugin_path,
|
||||
let summary = build_remote_plugin_summary(&plugin, installed_by_id.get(&plugin.id))?;
|
||||
if summary
|
||||
.share_context
|
||||
.as_ref()
|
||||
.and_then(|context| context.share_principals.as_ref())
|
||||
.is_none()
|
||||
{
|
||||
return Err(RemotePluginCatalogError::UnexpectedResponse(format!(
|
||||
"created workspace plugin `{}` did not include share_principals",
|
||||
plugin.id
|
||||
)));
|
||||
}
|
||||
let local_plugin_path = local_plugin_paths.get(&plugin.id).cloned();
|
||||
Ok(RemotePluginShareSummary {
|
||||
summary,
|
||||
local_plugin_path,
|
||||
})
|
||||
})
|
||||
.collect())
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn load_plugin_share_remote_ids_by_local_path(
|
||||
codex_home: &Path,
|
||||
) -> io::Result<BTreeMap<AbsolutePathBuf, String>> {
|
||||
let local_paths = local_paths::load_plugin_share_local_paths(codex_home)?;
|
||||
Ok(local_paths
|
||||
local_paths
|
||||
.into_iter()
|
||||
.map(|(remote_plugin_id, local_plugin_path)| (local_plugin_path, remote_plugin_id))
|
||||
.collect())
|
||||
.map(|(remote_plugin_id, local_plugin_path)| {
|
||||
if !is_valid_remote_plugin_id(&remote_plugin_id) {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!(
|
||||
"invalid remote plugin id in share local path mapping: {remote_plugin_id}"
|
||||
),
|
||||
));
|
||||
}
|
||||
Ok((local_plugin_path, remote_plugin_id))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub async fn delete_remote_plugin_share(
|
||||
@@ -284,9 +322,7 @@ pub async fn update_remote_plugin_share_targets(
|
||||
let response: RemotePluginShareUpdateTargetsResponse = send_and_decode(request, &url).await?;
|
||||
Ok(RemotePluginShareUpdateTargetsResult {
|
||||
principals: response.principals,
|
||||
// TODO: Remove this fallback once deployed plugin-service responses always include
|
||||
// discoverability per the API schema.
|
||||
discoverability: response.discoverability.unwrap_or(target_discoverability),
|
||||
discoverability: response.discoverability,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -311,6 +347,7 @@ fn ensure_unlisted_workspace_target(
|
||||
targets.push(RemotePluginShareTarget {
|
||||
principal_type: RemotePluginSharePrincipalType::Workspace,
|
||||
principal_id: account_id,
|
||||
role: RemotePluginShareTargetRole::Reader,
|
||||
});
|
||||
}
|
||||
Ok(Some(targets))
|
||||
|
||||
@@ -116,6 +116,7 @@ fn remote_plugin_json_with_share_url_and_principals(
|
||||
let serde_json::Value::Object(fields) = &mut plugin else {
|
||||
unreachable!("plugin json should be an object");
|
||||
};
|
||||
fields.insert("discoverability".to_string(), json!("PRIVATE"));
|
||||
fields.insert("share_url".to_string(), json!(share_url));
|
||||
fields.insert("share_principals".to_string(), share_principals);
|
||||
plugin
|
||||
@@ -209,10 +210,12 @@ async fn save_remote_plugin_share_creates_workspace_plugin() {
|
||||
{
|
||||
"principal_type": "user",
|
||||
"principal_id": "user-1",
|
||||
"role": "reader",
|
||||
},
|
||||
{
|
||||
"principal_type": "workspace",
|
||||
"principal_id": "account_id",
|
||||
"role": "reader",
|
||||
},
|
||||
],
|
||||
})))
|
||||
@@ -235,6 +238,7 @@ async fn save_remote_plugin_share_creates_workspace_plugin() {
|
||||
share_targets: Some(vec![RemotePluginShareTarget {
|
||||
principal_type: RemotePluginSharePrincipalType::User,
|
||||
principal_id: "user-1".to_string(),
|
||||
role: RemotePluginShareTargetRole::Reader,
|
||||
}]),
|
||||
},
|
||||
)
|
||||
@@ -404,14 +408,17 @@ async fn update_remote_plugin_share_targets_updates_targets() {
|
||||
{
|
||||
"principal_type": "user",
|
||||
"principal_id": "user-1",
|
||||
"role": "editor",
|
||||
},
|
||||
{
|
||||
"principal_type": "group",
|
||||
"principal_id": "group-1",
|
||||
"role": "reader",
|
||||
},
|
||||
{
|
||||
"principal_type": "workspace",
|
||||
"principal_id": "account_id",
|
||||
"role": "reader",
|
||||
},
|
||||
],
|
||||
})))
|
||||
@@ -420,11 +427,13 @@ async fn update_remote_plugin_share_targets_updates_targets() {
|
||||
{
|
||||
"principal_type": "user",
|
||||
"principal_id": "user-1",
|
||||
"role": "editor",
|
||||
"name": "Gavin",
|
||||
},
|
||||
{
|
||||
"principal_type": "group",
|
||||
"principal_id": "group-1",
|
||||
"role": "reader",
|
||||
"name": "Engineering",
|
||||
},
|
||||
],
|
||||
@@ -442,10 +451,12 @@ async fn update_remote_plugin_share_targets_updates_targets() {
|
||||
RemotePluginShareTarget {
|
||||
principal_type: RemotePluginSharePrincipalType::User,
|
||||
principal_id: "user-1".to_string(),
|
||||
role: RemotePluginShareTargetRole::Editor,
|
||||
},
|
||||
RemotePluginShareTarget {
|
||||
principal_type: RemotePluginSharePrincipalType::Group,
|
||||
principal_id: "group-1".to_string(),
|
||||
role: RemotePluginShareTargetRole::Reader,
|
||||
},
|
||||
],
|
||||
RemotePluginShareUpdateDiscoverability::Unlisted,
|
||||
@@ -460,11 +471,13 @@ async fn update_remote_plugin_share_targets_updates_targets() {
|
||||
RemotePluginSharePrincipal {
|
||||
principal_type: RemotePluginSharePrincipalType::User,
|
||||
principal_id: "user-1".to_string(),
|
||||
role: RemotePluginSharePrincipalRole::Editor,
|
||||
name: "Gavin".to_string(),
|
||||
},
|
||||
RemotePluginSharePrincipal {
|
||||
principal_type: RemotePluginSharePrincipalType::Group,
|
||||
principal_id: "group-1".to_string(),
|
||||
role: RemotePluginSharePrincipalRole::Reader,
|
||||
name: "Engineering".to_string(),
|
||||
},
|
||||
],
|
||||
@@ -473,64 +486,6 @@ async fn update_remote_plugin_share_targets_updates_targets() {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn update_remote_plugin_share_targets_falls_back_to_requested_discoverability() {
|
||||
let server = MockServer::start().await;
|
||||
let config = test_config(&server);
|
||||
let auth = test_auth();
|
||||
|
||||
Mock::given(method("PUT"))
|
||||
.and(path("/backend-api/ps/plugins/plugins_123/shares"))
|
||||
.and(header("authorization", "Bearer Access Token"))
|
||||
.and(header("chatgpt-account-id", "account_id"))
|
||||
.and(body_json(json!({
|
||||
"discoverability": "PRIVATE",
|
||||
"targets": [
|
||||
{
|
||||
"principal_type": "user",
|
||||
"principal_id": "user-1",
|
||||
},
|
||||
],
|
||||
})))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
|
||||
"principals": [
|
||||
{
|
||||
"principal_type": "user",
|
||||
"principal_id": "user-1",
|
||||
"name": "Gavin",
|
||||
},
|
||||
],
|
||||
})))
|
||||
.expect(1)
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let result = update_remote_plugin_share_targets(
|
||||
&config,
|
||||
Some(&auth),
|
||||
"plugins_123",
|
||||
vec![RemotePluginShareTarget {
|
||||
principal_type: RemotePluginSharePrincipalType::User,
|
||||
principal_id: "user-1".to_string(),
|
||||
}],
|
||||
RemotePluginShareUpdateDiscoverability::Private,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
result,
|
||||
RemotePluginShareUpdateTargetsResult {
|
||||
principals: vec![RemotePluginSharePrincipal {
|
||||
principal_type: RemotePluginSharePrincipalType::User,
|
||||
principal_id: "user-1".to_string(),
|
||||
name: "Gavin".to_string(),
|
||||
}],
|
||||
discoverability: RemotePluginShareDiscoverability::Private,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn list_remote_plugin_shares_fetches_created_workspace_plugins() {
|
||||
let codex_home = TempDir::new().unwrap();
|
||||
@@ -602,11 +557,6 @@ async fn list_remote_plugin_shares_fetches_created_workspace_plugins() {
|
||||
"role": "editor",
|
||||
"name": "Editor",
|
||||
},
|
||||
{
|
||||
"principal_type": "user",
|
||||
"principal_id": "user-missing-role",
|
||||
"name": "Missing Role",
|
||||
},
|
||||
]),
|
||||
)],
|
||||
"pagination": empty_pagination_json(),
|
||||
@@ -638,16 +588,26 @@ async fn list_remote_plugin_shares_fetches_created_workspace_plugins() {
|
||||
name: "demo-plugin".to_string(),
|
||||
share_context: Some(RemotePluginShareContext {
|
||||
remote_plugin_id: "plugins_123".to_string(),
|
||||
discoverability: RemotePluginShareDiscoverability::Private,
|
||||
share_url: Some(
|
||||
"https://chatgpt.example/plugins/share/share-key-1".to_string(),
|
||||
),
|
||||
creator_account_user_id: None,
|
||||
creator_name: None,
|
||||
share_targets: Some(vec![RemotePluginSharePrincipal {
|
||||
principal_type: RemotePluginSharePrincipalType::User,
|
||||
principal_id: "user-reader".to_string(),
|
||||
name: "Reader".to_string(),
|
||||
}]),
|
||||
share_principals: Some(vec![
|
||||
RemotePluginSharePrincipal {
|
||||
principal_type: RemotePluginSharePrincipalType::User,
|
||||
principal_id: "user-owner".to_string(),
|
||||
role: RemotePluginSharePrincipalRole::Owner,
|
||||
name: "Owner".to_string(),
|
||||
},
|
||||
RemotePluginSharePrincipal {
|
||||
principal_type: RemotePluginSharePrincipalType::User,
|
||||
principal_id: "user-reader".to_string(),
|
||||
role: RemotePluginSharePrincipalRole::Reader,
|
||||
name: "Reader".to_string(),
|
||||
},
|
||||
]),
|
||||
}),
|
||||
installed: false,
|
||||
enabled: false,
|
||||
@@ -657,7 +617,6 @@ async fn list_remote_plugin_shares_fetches_created_workspace_plugins() {
|
||||
interface: Some(expected_plugin_interface()),
|
||||
keywords: Vec::new(),
|
||||
},
|
||||
share_url: Some("https://chatgpt.example/plugins/share/share-key-1".to_string()),
|
||||
local_plugin_path: Some(local_plugin_path),
|
||||
},
|
||||
RemotePluginShareSummary {
|
||||
@@ -666,10 +625,24 @@ async fn list_remote_plugin_shares_fetches_created_workspace_plugins() {
|
||||
name: "demo-plugin".to_string(),
|
||||
share_context: Some(RemotePluginShareContext {
|
||||
remote_plugin_id: "plugins_456".to_string(),
|
||||
discoverability: RemotePluginShareDiscoverability::Private,
|
||||
share_url: None,
|
||||
creator_account_user_id: None,
|
||||
creator_name: None,
|
||||
share_targets: Some(Vec::new()),
|
||||
share_principals: Some(vec![
|
||||
RemotePluginSharePrincipal {
|
||||
principal_type: RemotePluginSharePrincipalType::User,
|
||||
principal_id: "user-owner".to_string(),
|
||||
role: RemotePluginSharePrincipalRole::Owner,
|
||||
name: "Owner".to_string(),
|
||||
},
|
||||
RemotePluginSharePrincipal {
|
||||
principal_type: RemotePluginSharePrincipalType::User,
|
||||
principal_id: "user-editor".to_string(),
|
||||
role: RemotePluginSharePrincipalRole::Editor,
|
||||
name: "Editor".to_string(),
|
||||
},
|
||||
]),
|
||||
}),
|
||||
installed: true,
|
||||
enabled: true,
|
||||
@@ -679,7 +652,6 @@ async fn list_remote_plugin_shares_fetches_created_workspace_plugins() {
|
||||
interface: Some(expected_plugin_interface()),
|
||||
keywords: Vec::new(),
|
||||
},
|
||||
share_url: None,
|
||||
local_plugin_path: None,
|
||||
}
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user