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:
@@ -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