mirror of
https://github.com/openai/codex.git
synced 2026-11-01 16:17:58 +00:00
feat: Expose plugin versions and gate plugin sharing (#22397)
- Adds localVersion to plugin summaries and remoteVersion to share
context, including generated API schemas.
- Hydrates local and remote plugin versions from manifests and remote
release metadata.
- Adds default-on plugin_sharing gate for shared-with-me listing and
plugin/share/save, with disabled-path errors
and focused coverage.
This commit is contained in:
@@ -111,6 +111,7 @@ fn share_context_for_source(
|
||||
.cloned()
|
||||
.map(|remote_plugin_id| PluginShareContext {
|
||||
remote_plugin_id,
|
||||
remote_version: None,
|
||||
discoverability: None,
|
||||
share_url: None,
|
||||
creator_account_user_id: None,
|
||||
@@ -483,6 +484,7 @@ impl PluginRequestProcessor {
|
||||
PluginSummary {
|
||||
id: plugin.id,
|
||||
remote_plugin_id: None,
|
||||
local_version: plugin.local_version,
|
||||
installed: plugin.installed,
|
||||
enabled: plugin.enabled,
|
||||
name: plugin.name,
|
||||
@@ -533,7 +535,9 @@ impl PluginRequestProcessor {
|
||||
if marketplace_kinds.contains(&PluginListMarketplaceKind::WorkspaceDirectory) {
|
||||
remote_sources.push(RemoteMarketplaceSource::WorkspaceDirectory);
|
||||
}
|
||||
if marketplace_kinds.contains(&PluginListMarketplaceKind::SharedWithMe) {
|
||||
if marketplace_kinds.contains(&PluginListMarketplaceKind::SharedWithMe)
|
||||
&& config.features.enabled(Feature::PluginSharing)
|
||||
{
|
||||
remote_sources.push(RemoteMarketplaceSource::SharedWithMe);
|
||||
}
|
||||
if !remote_sources.is_empty() {
|
||||
@@ -673,17 +677,21 @@ impl PluginRequestProcessor {
|
||||
)
|
||||
.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(Some(remote_share_context)) => {
|
||||
if remote_share_context.share_principals.is_some() {
|
||||
Some(remote_plugin_share_context_to_info(remote_share_context))
|
||||
} else {
|
||||
let remote_version = remote_share_context.remote_version;
|
||||
let remote_plugin_id = context.remote_plugin_id.clone();
|
||||
warn!(
|
||||
remote_plugin_id = %remote_plugin_id,
|
||||
"remote shared plugin detail did not include share principals; returning local share mapping context with remote version"
|
||||
);
|
||||
Some(PluginShareContext {
|
||||
remote_version,
|
||||
..context
|
||||
})
|
||||
}
|
||||
}
|
||||
Ok(None) => {
|
||||
warn!(
|
||||
@@ -725,6 +733,7 @@ impl PluginRequestProcessor {
|
||||
summary: PluginSummary {
|
||||
id: outcome.plugin.id,
|
||||
remote_plugin_id: None,
|
||||
local_version: outcome.plugin.local_version,
|
||||
name: outcome.plugin.name,
|
||||
share_context,
|
||||
source: marketplace_plugin_source_to_info(outcome.plugin.source),
|
||||
@@ -840,6 +849,9 @@ impl PluginRequestProcessor {
|
||||
params: PluginShareSaveParams,
|
||||
) -> Result<PluginShareSaveResponse, JSONRPCErrorError> {
|
||||
let (config, auth) = self.load_plugin_share_config_and_auth().await?;
|
||||
if !config.features.enabled(Feature::PluginSharing) {
|
||||
return Err(invalid_request("plugin sharing is disabled"));
|
||||
}
|
||||
let PluginShareSaveParams {
|
||||
plugin_path,
|
||||
remote_plugin_id,
|
||||
@@ -1589,6 +1601,7 @@ fn remote_plugin_summary_to_info(summary: RemoteCatalogPluginSummary) -> PluginS
|
||||
PluginSummary {
|
||||
id: summary.id,
|
||||
remote_plugin_id: Some(summary.remote_plugin_id),
|
||||
local_version: None,
|
||||
name: summary.name,
|
||||
share_context: summary
|
||||
.share_context
|
||||
@@ -1609,6 +1622,7 @@ fn remote_plugin_share_context_to_info(
|
||||
) -> PluginShareContext {
|
||||
PluginShareContext {
|
||||
remote_plugin_id: context.remote_plugin_id,
|
||||
remote_version: context.remote_version,
|
||||
discoverability: Some(remote_plugin_share_discoverability_to_info(
|
||||
context.discoverability,
|
||||
)),
|
||||
|
||||
@@ -241,6 +241,7 @@ async fn plugin_list_keeps_valid_marketplaces_when_another_marketplace_fails_to_
|
||||
plugins: vec![PluginSummary {
|
||||
id: "valid-plugin@valid-marketplace".to_string(),
|
||||
remote_plugin_id: None,
|
||||
local_version: None,
|
||||
name: "valid-plugin".to_string(),
|
||||
share_context: None,
|
||||
source: PluginSource::Local {
|
||||
@@ -531,6 +532,7 @@ async fn plugin_list_uses_alternate_discoverable_manifest_and_keeps_undiscoverab
|
||||
PluginSummary {
|
||||
id: "valid-plugin@alternate-marketplace".to_string(),
|
||||
remote_plugin_id: None,
|
||||
local_version: None,
|
||||
name: "valid-plugin".to_string(),
|
||||
share_context: None,
|
||||
source: PluginSource::Local {
|
||||
@@ -565,6 +567,7 @@ async fn plugin_list_uses_alternate_discoverable_manifest_and_keeps_undiscoverab
|
||||
PluginSummary {
|
||||
id: "missing-plugin@alternate-marketplace".to_string(),
|
||||
remote_plugin_id: None,
|
||||
local_version: None,
|
||||
name: "missing-plugin".to_string(),
|
||||
share_context: None,
|
||||
source: PluginSource::Local {
|
||||
@@ -660,7 +663,7 @@ async fn plugin_list_returns_share_context_for_shared_local_plugin() -> Result<(
|
||||
)?;
|
||||
std::fs::write(
|
||||
plugin_root.join(".codex-plugin/plugin.json"),
|
||||
r#"{"name":"demo-plugin"}"#,
|
||||
r#"{"name":"demo-plugin","version":"1.2.3"}"#,
|
||||
)?;
|
||||
write_plugin_share_local_path_mapping(
|
||||
codex_home.path(),
|
||||
@@ -692,11 +695,13 @@ async fn plugin_list_returns_share_context_for_shared_local_plugin() -> Result<(
|
||||
.find(|plugin| plugin.name == "demo-plugin")
|
||||
.expect("expected demo-plugin entry");
|
||||
assert_eq!(plugin.remote_plugin_id, None);
|
||||
assert_eq!(plugin.local_version.as_deref(), Some("1.2.3"));
|
||||
let share_context = plugin
|
||||
.share_context
|
||||
.as_ref()
|
||||
.expect("expected share context");
|
||||
assert_eq!(share_context.remote_plugin_id, "plugins_123");
|
||||
assert_eq!(share_context.remote_version, None);
|
||||
assert_eq!(share_context.discoverability, None);
|
||||
assert_eq!(share_context.share_url, None);
|
||||
assert_eq!(share_context.creator_account_user_id, None);
|
||||
@@ -1873,6 +1878,7 @@ async fn plugin_list_fetches_shared_with_me_kind() -> Result<()> {
|
||||
share_context.remote_plugin_id,
|
||||
"plugins~Plugin_22222222222222222222222222222222"
|
||||
);
|
||||
assert_eq!(share_context.remote_version.as_deref(), Some("1.2.3"));
|
||||
assert_eq!(
|
||||
share_context.discoverability,
|
||||
Some(PluginShareDiscoverability::Private)
|
||||
@@ -1891,6 +1897,65 @@ async fn plugin_list_fetches_shared_with_me_kind() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugin_list_omits_shared_with_me_kind_when_plugin_sharing_disabled() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let server = MockServer::start().await;
|
||||
std::fs::write(
|
||||
codex_home.path().join("config.toml"),
|
||||
format!(
|
||||
r#"chatgpt_base_url = "{}/backend-api/"
|
||||
|
||||
[features]
|
||||
plugins = true
|
||||
plugin_sharing = false
|
||||
"#,
|
||||
server.uri()
|
||||
),
|
||||
)?;
|
||||
write_chatgpt_auth(
|
||||
codex_home.path(),
|
||||
ChatGptAuthFixture::new("chatgpt-token")
|
||||
.account_id("account-123")
|
||||
.chatgpt_user_id("user-123")
|
||||
.chatgpt_account_id("account-123"),
|
||||
AuthCredentialsStoreMode::File,
|
||||
)?;
|
||||
|
||||
let mut mcp = McpProcess::new(codex_home.path()).await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
.send_plugin_list_request(PluginListParams {
|
||||
cwds: None,
|
||||
marketplace_kinds: Some(vec![PluginListMarketplaceKind::SharedWithMe]),
|
||||
})
|
||||
.await?;
|
||||
|
||||
let response: JSONRPCResponse = timeout(
|
||||
DEFAULT_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
|
||||
)
|
||||
.await??;
|
||||
let response: PluginListResponse = to_response(response)?;
|
||||
|
||||
assert_eq!(
|
||||
response,
|
||||
PluginListResponse {
|
||||
marketplaces: Vec::new(),
|
||||
marketplace_load_errors: Vec::new(),
|
||||
featured_plugin_ids: Vec::new(),
|
||||
}
|
||||
);
|
||||
wait_for_remote_plugin_request_count(
|
||||
&server,
|
||||
"/ps/plugins/workspace/shared",
|
||||
/*expected_count*/ 0,
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugin_list_marks_remote_plugin_disabled_by_admin() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
@@ -2434,6 +2499,7 @@ fn workspace_remote_plugin_page_body(
|
||||
}}
|
||||
],
|
||||
"release": {{
|
||||
"version": "1.2.3",
|
||||
"display_name": "{display_name}",
|
||||
"description": "Track work",
|
||||
"app_ids": [],
|
||||
|
||||
@@ -262,6 +262,7 @@ async fn plugin_read_returns_share_context_for_shared_remote_plugin() -> Result<
|
||||
"installation_policy": "AVAILABLE",
|
||||
"authentication_policy": "ON_USE",
|
||||
"release": {
|
||||
"version": "2.3.4",
|
||||
"display_name": "Shared Linear",
|
||||
"description": "Track shared work",
|
||||
"app_ids": [],
|
||||
@@ -330,6 +331,7 @@ async fn plugin_read_returns_share_context_for_shared_remote_plugin() -> Result<
|
||||
share_context.remote_plugin_id,
|
||||
"plugins~Plugin_11111111111111111111111111111111"
|
||||
);
|
||||
assert_eq!(share_context.remote_version.as_deref(), Some("2.3.4"));
|
||||
assert_eq!(
|
||||
share_context.discoverability,
|
||||
Some(PluginShareDiscoverability::Private)
|
||||
@@ -800,7 +802,7 @@ async fn plugin_read_returns_share_context_for_shared_local_plugin() -> Result<(
|
||||
repo_root
|
||||
.path()
|
||||
.join("demo-plugin/.codex-plugin/plugin.json"),
|
||||
r#"{"name":"demo-plugin"}"#,
|
||||
r#"{"name":"demo-plugin","version":"1.2.3"}"#,
|
||||
)?;
|
||||
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)?;
|
||||
@@ -833,6 +835,7 @@ async fn plugin_read_returns_share_context_for_shared_local_plugin() -> Result<(
|
||||
"installation_policy": "AVAILABLE",
|
||||
"authentication_policy": "ON_USE",
|
||||
"release": {
|
||||
"version": "1.2.4",
|
||||
"display_name": "Demo Plugin",
|
||||
"description": "Shared local plugin",
|
||||
"app_ids": [],
|
||||
@@ -865,6 +868,10 @@ async fn plugin_read_returns_share_context_for_shared_local_plugin() -> Result<(
|
||||
let response: PluginReadResponse = to_response(response)?;
|
||||
|
||||
assert_eq!(response.plugin.summary.remote_plugin_id, None);
|
||||
assert_eq!(
|
||||
response.plugin.summary.local_version.as_deref(),
|
||||
Some("1.2.3")
|
||||
);
|
||||
let share_context = response
|
||||
.plugin
|
||||
.summary
|
||||
@@ -872,6 +879,7 @@ 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.remote_version.as_deref(), Some("1.2.4"));
|
||||
assert_eq!(
|
||||
share_context.discoverability,
|
||||
Some(PluginShareDiscoverability::Unlisted)
|
||||
@@ -905,6 +913,107 @@ async fn plugin_read_returns_share_context_for_shared_local_plugin() -> Result<(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugin_read_keeps_remote_version_when_share_principals_are_missing() -> 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",
|
||||
"demo-plugin",
|
||||
"./demo-plugin",
|
||||
)?;
|
||||
std::fs::create_dir_all(repo_root.path().join("demo-plugin/.codex-plugin"))?;
|
||||
std::fs::write(
|
||||
repo_root
|
||||
.path()
|
||||
.join("demo-plugin/.codex-plugin/plugin.json"),
|
||||
r#"{"name":"demo-plugin","version":"1.2.3"}"#,
|
||||
)?;
|
||||
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": null,
|
||||
"installation_policy": "AVAILABLE",
|
||||
"authentication_policy": "ON_USE",
|
||||
"release": {
|
||||
"version": "1.2.4",
|
||||
"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)?;
|
||||
|
||||
assert_eq!(response.plugin.summary.remote_plugin_id, None);
|
||||
assert_eq!(
|
||||
response.plugin.summary.local_version.as_deref(),
|
||||
Some("1.2.3")
|
||||
);
|
||||
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.remote_version.as_deref(), Some("1.2.4"));
|
||||
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_principals, None);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugin_read_falls_back_to_local_share_context_without_remote_auth() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
@@ -941,6 +1050,7 @@ async fn plugin_read_falls_back_to_local_share_context_without_remote_auth() ->
|
||||
let response: PluginReadResponse = to_response(response)?;
|
||||
|
||||
assert_eq!(response.plugin.summary.remote_plugin_id, None);
|
||||
assert_eq!(response.plugin.summary.local_version, None);
|
||||
let share_context = response
|
||||
.plugin
|
||||
.summary
|
||||
@@ -948,6 +1058,7 @@ async fn plugin_read_falls_back_to_local_share_context_without_remote_auth() ->
|
||||
.as_ref()
|
||||
.expect("expected share context");
|
||||
assert_eq!(share_context.remote_plugin_id, "plugins_123");
|
||||
assert_eq!(share_context.remote_version, None);
|
||||
assert_eq!(share_context.discoverability, None);
|
||||
assert_eq!(share_context.share_url, None);
|
||||
assert_eq!(share_context.creator_account_user_id, None);
|
||||
|
||||
@@ -164,6 +164,7 @@ async fn plugin_share_save_uploads_local_plugin() -> Result<()> {
|
||||
plugin: PluginSummary {
|
||||
id: "demo-plugin@shared-with-me".to_string(),
|
||||
remote_plugin_id: Some("plugins_123".to_string()),
|
||||
local_version: None,
|
||||
name: "demo-plugin".to_string(),
|
||||
share_context: Some(expected_share_context("plugins_123")),
|
||||
source: PluginSource::Remote,
|
||||
@@ -322,6 +323,64 @@ async fn plugin_share_save_rejects_listed_discoverability() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugin_share_save_rejects_when_plugin_sharing_disabled() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let plugin_root = TempDir::new()?;
|
||||
let plugin_path = write_test_plugin(plugin_root.path(), "demo-plugin")?;
|
||||
let server = MockServer::start().await;
|
||||
std::fs::write(
|
||||
codex_home.path().join("config.toml"),
|
||||
format!(
|
||||
r#"
|
||||
chatgpt_base_url = "{}/backend-api"
|
||||
|
||||
[features]
|
||||
plugins = true
|
||||
remote_plugin = true
|
||||
plugin_sharing = false
|
||||
"#,
|
||||
server.uri()
|
||||
),
|
||||
)?;
|
||||
write_chatgpt_auth(
|
||||
codex_home.path(),
|
||||
ChatGptAuthFixture::new("chatgpt-token")
|
||||
.account_id("account-123")
|
||||
.chatgpt_user_id("user-123")
|
||||
.chatgpt_account_id("account-123"),
|
||||
AuthCredentialsStoreMode::File,
|
||||
)?;
|
||||
|
||||
let mut mcp = McpProcess::new(codex_home.path()).await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
let request_id = mcp
|
||||
.send_raw_request(
|
||||
"plugin/share/save",
|
||||
Some(json!({
|
||||
"pluginPath": AbsolutePathBuf::try_from(plugin_path)?,
|
||||
})),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let error: JSONRPCError = timeout(
|
||||
DEFAULT_TIMEOUT,
|
||||
mcp.read_stream_until_error_message(RequestId::Integer(request_id)),
|
||||
)
|
||||
.await??;
|
||||
|
||||
assert_eq!(error.error.code, -32600);
|
||||
assert_eq!(error.error.message, "plugin sharing is disabled");
|
||||
assert!(
|
||||
server
|
||||
.received_requests()
|
||||
.await
|
||||
.expect("wiremock should record requests")
|
||||
.is_empty()
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugin_share_rejects_workspace_targets_from_client() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
@@ -509,6 +568,7 @@ async fn plugin_share_list_returns_created_workspace_plugins() -> Result<()> {
|
||||
plugin: PluginSummary {
|
||||
id: "demo-plugin@shared-with-me".to_string(),
|
||||
remote_plugin_id: Some("plugins_123".to_string()),
|
||||
local_version: None,
|
||||
name: "demo-plugin".to_string(),
|
||||
share_context: Some(expected_share_context("plugins_123")),
|
||||
source: PluginSource::Remote,
|
||||
@@ -729,6 +789,7 @@ async fn plugin_share_delete_removes_created_workspace_plugin() -> Result<()> {
|
||||
plugin: PluginSummary {
|
||||
id: "demo-plugin@shared-with-me".to_string(),
|
||||
remote_plugin_id: Some("plugins_123".to_string()),
|
||||
local_version: None,
|
||||
name: "demo-plugin".to_string(),
|
||||
share_context: Some(expected_share_context("plugins_123")),
|
||||
source: PluginSource::Remote,
|
||||
@@ -786,6 +847,7 @@ fn remote_plugin_json(plugin_id: &str) -> serde_json::Value {
|
||||
"installation_policy": "AVAILABLE",
|
||||
"authentication_policy": "ON_USE",
|
||||
"release": {
|
||||
"version": "0.1.0",
|
||||
"display_name": "Demo Plugin",
|
||||
"description": "Demo plugin description",
|
||||
"interface": {
|
||||
@@ -838,6 +900,7 @@ fn expected_plugin_interface() -> PluginInterface {
|
||||
fn expected_share_context(plugin_id: &str) -> PluginShareContext {
|
||||
PluginShareContext {
|
||||
remote_plugin_id: plugin_id.to_string(),
|
||||
remote_version: Some("0.1.0".to_string()),
|
||||
discoverability: Some(PluginShareDiscoverability::Private),
|
||||
share_url: Some("https://chatgpt.example/plugins/share/share-key-1".to_string()),
|
||||
creator_account_user_id: None,
|
||||
|
||||
Reference in New Issue
Block a user