mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
[codex] Add created-by-me remote plugin marketplace (#28203)
## Summary - add the `created-by-me-remote` marketplace backed by paginated `scope=USER` plugin directory and installed-plugin requests - include USER plugins in installed-plugin caching, bundle sync, and stale-cache cleanup without client-side discoverability filtering - expose the marketplace through app-server v2 and regenerate the protocol schemas ## Testing - `cargo build -p codex-app-server --bin codex-app-server` - production-auth `plugin/list` smoke test for `created-by-me-remote` (returned the expected USER plugin as installed and enabled) - `just test -p codex-core-plugins` (221 passed) - `just test -p codex-app-server-protocol` (231 passed) - `just test -p codex-app-server suite::v2::plugin_list::` (37 passed) - `just fix -p codex-core-plugins -p codex-app-server-protocol -p codex-app-server` - `just fmt`
This commit is contained in:
committed by
GitHub
Unverified
parent
040dafa32d
commit
709f19e111
@@ -628,7 +628,7 @@ remote_plugin = true
|
||||
.await
|
||||
.expect("remote plugin catalog cache should write");
|
||||
|
||||
for scope in ["GLOBAL", "WORKSPACE"] {
|
||||
for scope in ["GLOBAL", "USER", "WORKSPACE"] {
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/backend-api/ps/plugins/installed"))
|
||||
.and(query_param("scope", scope))
|
||||
|
||||
@@ -51,6 +51,7 @@ pub use share::save_remote_plugin_share;
|
||||
pub use share::update_remote_plugin_share_targets;
|
||||
|
||||
pub const REMOTE_GLOBAL_MARKETPLACE_NAME: &str = "openai-curated-remote";
|
||||
pub const REMOTE_CREATED_BY_ME_MARKETPLACE_NAME: &str = "created-by-me-remote";
|
||||
pub const REMOTE_WORKSPACE_MARKETPLACE_NAME: &str = "workspace-directory";
|
||||
pub const REMOTE_WORKSPACE_SHARED_WITH_ME_MARKETPLACE_NAME: &str = "workspace-shared-with-me";
|
||||
pub const REMOTE_WORKSPACE_SHARED_WITH_ME_PRIVATE_MARKETPLACE_NAME: &str =
|
||||
@@ -58,6 +59,7 @@ pub const REMOTE_WORKSPACE_SHARED_WITH_ME_PRIVATE_MARKETPLACE_NAME: &str =
|
||||
pub const REMOTE_WORKSPACE_SHARED_WITH_ME_UNLISTED_MARKETPLACE_NAME: &str =
|
||||
"workspace-shared-with-me-unlisted";
|
||||
pub const REMOTE_GLOBAL_MARKETPLACE_DISPLAY_NAME: &str = "OpenAI Curated Remote";
|
||||
pub const REMOTE_CREATED_BY_ME_MARKETPLACE_DISPLAY_NAME: &str = "Created by me";
|
||||
pub const REMOTE_WORKSPACE_MARKETPLACE_DISPLAY_NAME: &str = "Workspace Directory";
|
||||
pub const REMOTE_WORKSPACE_SHARED_WITH_ME_PRIVATE_MARKETPLACE_DISPLAY_NAME: &str = "Shared with me";
|
||||
pub const REMOTE_WORKSPACE_SHARED_WITH_ME_UNLISTED_MARKETPLACE_DISPLAY_NAME: &str =
|
||||
@@ -71,11 +73,15 @@ const REMOTE_PLUGIN_LIST_PAGE_LIMIT: u32 = 200;
|
||||
const MAX_REMOTE_DEFAULT_PROMPT_COUNT: usize = 3;
|
||||
const MAX_REMOTE_DEFAULT_PROMPT_LEN: usize = 128;
|
||||
const INVALID_REQUEST_ERROR_CODE: i64 = -32600;
|
||||
const REMOTE_INSTALLED_MARKETPLACE_DISPLAY_ORDER: [(&str, &str); 5] = [
|
||||
const REMOTE_INSTALLED_MARKETPLACE_DISPLAY_ORDER: [(&str, &str); 6] = [
|
||||
(
|
||||
REMOTE_GLOBAL_MARKETPLACE_NAME,
|
||||
REMOTE_GLOBAL_MARKETPLACE_DISPLAY_NAME,
|
||||
),
|
||||
(
|
||||
REMOTE_CREATED_BY_ME_MARKETPLACE_NAME,
|
||||
REMOTE_CREATED_BY_ME_MARKETPLACE_DISPLAY_NAME,
|
||||
),
|
||||
(
|
||||
REMOTE_WORKSPACE_MARKETPLACE_NAME,
|
||||
REMOTE_WORKSPACE_MARKETPLACE_DISPLAY_NAME,
|
||||
@@ -109,6 +115,7 @@ pub struct RemoteMarketplace {
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum RemoteMarketplaceSource {
|
||||
Global,
|
||||
CreatedByMeRemote,
|
||||
WorkspaceDirectory,
|
||||
SharedWithMe,
|
||||
}
|
||||
@@ -338,6 +345,8 @@ pub enum RemotePluginCatalogError {
|
||||
pub enum RemotePluginScope {
|
||||
#[serde(rename = "GLOBAL")]
|
||||
Global,
|
||||
#[serde(rename = "USER")]
|
||||
User,
|
||||
#[serde(rename = "WORKSPACE")]
|
||||
Workspace,
|
||||
}
|
||||
@@ -346,6 +355,7 @@ impl RemotePluginScope {
|
||||
fn api_value(self) -> &'static str {
|
||||
match self {
|
||||
Self::Global => "GLOBAL",
|
||||
Self::User => "USER",
|
||||
Self::Workspace => "WORKSPACE",
|
||||
}
|
||||
}
|
||||
@@ -353,6 +363,7 @@ impl RemotePluginScope {
|
||||
fn marketplace_name(self) -> &'static str {
|
||||
match self {
|
||||
Self::Global => REMOTE_GLOBAL_MARKETPLACE_NAME,
|
||||
Self::User => REMOTE_CREATED_BY_ME_MARKETPLACE_NAME,
|
||||
Self::Workspace => REMOTE_WORKSPACE_MARKETPLACE_NAME,
|
||||
}
|
||||
}
|
||||
@@ -360,6 +371,7 @@ impl RemotePluginScope {
|
||||
fn marketplace_display_name(self) -> &'static str {
|
||||
match self {
|
||||
Self::Global => REMOTE_GLOBAL_MARKETPLACE_DISPLAY_NAME,
|
||||
Self::User => REMOTE_CREATED_BY_ME_MARKETPLACE_DISPLAY_NAME,
|
||||
Self::Workspace => REMOTE_WORKSPACE_MARKETPLACE_DISPLAY_NAME,
|
||||
}
|
||||
}
|
||||
@@ -367,6 +379,7 @@ impl RemotePluginScope {
|
||||
fn from_marketplace_name(name: &str) -> Option<Self> {
|
||||
match name {
|
||||
REMOTE_GLOBAL_MARKETPLACE_NAME => Some(Self::Global),
|
||||
REMOTE_CREATED_BY_ME_MARKETPLACE_NAME => Some(Self::User),
|
||||
REMOTE_WORKSPACE_MARKETPLACE_NAME
|
||||
| REMOTE_WORKSPACE_SHARED_WITH_ME_MARKETPLACE_NAME
|
||||
| REMOTE_WORKSPACE_SHARED_WITH_ME_PRIVATE_MARKETPLACE_NAME
|
||||
@@ -500,6 +513,7 @@ fn remote_plugin_canonical_marketplace_name(
|
||||
) -> Result<&'static str, RemotePluginCatalogError> {
|
||||
match plugin.scope {
|
||||
RemotePluginScope::Global => Ok(REMOTE_GLOBAL_MARKETPLACE_NAME),
|
||||
RemotePluginScope::User => Ok(REMOTE_CREATED_BY_ME_MARKETPLACE_NAME),
|
||||
RemotePluginScope::Workspace => match workspace_plugin_discoverability(plugin)? {
|
||||
RemotePluginShareDiscoverability::Listed => Ok(REMOTE_WORKSPACE_MARKETPLACE_NAME),
|
||||
RemotePluginShareDiscoverability::Private
|
||||
@@ -631,6 +645,22 @@ pub async fn fetch_remote_marketplaces(
|
||||
);
|
||||
}
|
||||
}
|
||||
RemoteMarketplaceSource::CreatedByMeRemote => {
|
||||
let scope = RemotePluginScope::User;
|
||||
let (directory_plugins, installed_plugins) = tokio::try_join!(
|
||||
fetch_directory_plugins_for_scope(config, auth, scope),
|
||||
fetch_installed_plugins_for_scope(config, auth, scope),
|
||||
)?;
|
||||
if let Some(marketplace) = build_remote_marketplace(
|
||||
scope.marketplace_name(),
|
||||
scope.marketplace_display_name(),
|
||||
directory_plugins,
|
||||
installed_plugins,
|
||||
/*include_installed_only*/ false,
|
||||
)? {
|
||||
marketplaces.push(marketplace);
|
||||
}
|
||||
}
|
||||
RemoteMarketplaceSource::WorkspaceDirectory => {
|
||||
let scope = RemotePluginScope::Workspace;
|
||||
let directory_plugins =
|
||||
@@ -838,9 +868,14 @@ pub(crate) async fn fetch_remote_installed_plugins(
|
||||
let installed_plugins = fetch_installed_plugins_for_scope(config, auth, scope).await?;
|
||||
Ok::<_, RemotePluginCatalogError>((scope, installed_plugins))
|
||||
};
|
||||
let user = async {
|
||||
let scope = RemotePluginScope::User;
|
||||
let installed_plugins = fetch_installed_plugins_for_scope(config, auth, scope).await?;
|
||||
Ok::<_, RemotePluginCatalogError>((scope, installed_plugins))
|
||||
};
|
||||
|
||||
let (global, workspace) = tokio::try_join!(global, workspace)?;
|
||||
let mut installed_plugins = [global, workspace]
|
||||
let (global, workspace, user) = tokio::try_join!(global, workspace, user)?;
|
||||
let mut installed_plugins = [global, workspace, user]
|
||||
.into_iter()
|
||||
.flat_map(|(_scope, plugins)| plugins)
|
||||
.map(|plugin| remote_installed_plugin_to_cache_entry(&plugin))
|
||||
@@ -1266,7 +1301,7 @@ fn remote_plugin_share_context(
|
||||
plugin: &RemotePluginDirectoryItem,
|
||||
) -> Result<Option<RemotePluginShareContext>, RemotePluginCatalogError> {
|
||||
match plugin.scope {
|
||||
RemotePluginScope::Global => Ok(None),
|
||||
RemotePluginScope::Global | RemotePluginScope::User => Ok(None),
|
||||
RemotePluginScope::Workspace => {
|
||||
let discoverability = workspace_plugin_discoverability(plugin)?;
|
||||
Ok(Some(RemotePluginShareContext {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use super::REMOTE_CREATED_BY_ME_MARKETPLACE_NAME;
|
||||
use super::REMOTE_GLOBAL_MARKETPLACE_NAME;
|
||||
use super::REMOTE_WORKSPACE_MARKETPLACE_NAME;
|
||||
use super::REMOTE_WORKSPACE_SHARED_WITH_ME_MARKETPLACE_NAME;
|
||||
@@ -144,12 +145,24 @@ pub async fn sync_remote_installed_plugin_bundles_once(
|
||||
.await?;
|
||||
Ok::<_, RemotePluginCatalogError>((scope, installed_plugins))
|
||||
};
|
||||
let user = async {
|
||||
let scope = RemotePluginScope::User;
|
||||
let installed_plugins = fetch_installed_plugins_for_scope_with_download_url(
|
||||
config, auth, scope, /*include_download_urls*/ true,
|
||||
)
|
||||
.await?;
|
||||
Ok::<_, RemotePluginCatalogError>((scope, installed_plugins))
|
||||
};
|
||||
|
||||
let (global, workspace) = tokio::try_join!(global, workspace)?;
|
||||
let (global, workspace, user) = tokio::try_join!(global, workspace, user)?;
|
||||
let store = PluginStore::try_new(codex_home.clone())?;
|
||||
let mut installed_plugin_names_by_marketplace =
|
||||
BTreeMap::<String, BTreeSet<String>>::from_iter([
|
||||
(REMOTE_GLOBAL_MARKETPLACE_NAME.to_string(), BTreeSet::new()),
|
||||
(
|
||||
REMOTE_CREATED_BY_ME_MARKETPLACE_NAME.to_string(),
|
||||
BTreeSet::new(),
|
||||
),
|
||||
(
|
||||
REMOTE_WORKSPACE_MARKETPLACE_NAME.to_string(),
|
||||
BTreeSet::new(),
|
||||
@@ -170,7 +183,7 @@ pub async fn sync_remote_installed_plugin_bundles_once(
|
||||
let mut installed_plugin_ids = BTreeSet::new();
|
||||
let mut failed_remote_plugin_ids = BTreeSet::new();
|
||||
|
||||
for (_scope, installed_plugins) in [global, workspace] {
|
||||
for (_scope, installed_plugins) in [global, workspace, user] {
|
||||
for installed_plugin in installed_plugins {
|
||||
let plugin = installed_plugin.plugin;
|
||||
let marketplace_name = remote_plugin_canonical_marketplace_name(&plugin)?.to_string();
|
||||
@@ -308,6 +321,7 @@ fn remove_stale_remote_plugin_caches(
|
||||
let mut removed_cache_plugin_ids = Vec::new();
|
||||
for marketplace_name in [
|
||||
REMOTE_GLOBAL_MARKETPLACE_NAME,
|
||||
REMOTE_CREATED_BY_ME_MARKETPLACE_NAME,
|
||||
REMOTE_WORKSPACE_MARKETPLACE_NAME,
|
||||
REMOTE_WORKSPACE_SHARED_WITH_ME_MARKETPLACE_NAME,
|
||||
REMOTE_WORKSPACE_SHARED_WITH_ME_PRIVATE_MARKETPLACE_NAME,
|
||||
@@ -517,8 +531,27 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stale_remote_plugin_cleanup_removes_old_shared_with_me_cache_and_keeps_canonical_cache() {
|
||||
fn stale_remote_plugin_cleanup_removes_stale_marketplace_caches_and_keeps_canonical_cache() {
|
||||
let codex_home = tempfile::tempdir().expect("create codex home");
|
||||
let created_by_me_cached_manifest = codex_home
|
||||
.path()
|
||||
.join(PLUGINS_CACHE_DIR)
|
||||
.join(REMOTE_CREATED_BY_ME_MARKETPLACE_NAME)
|
||||
.join("created-by-me-plugin")
|
||||
.join("1.2.3")
|
||||
.join(".codex-plugin")
|
||||
.join("plugin.json");
|
||||
std::fs::create_dir_all(
|
||||
created_by_me_cached_manifest
|
||||
.parent()
|
||||
.expect("manifest parent"),
|
||||
)
|
||||
.expect("create cached plugin manifest parent");
|
||||
std::fs::write(
|
||||
&created_by_me_cached_manifest,
|
||||
r#"{"name":"created-by-me-plugin"}"#,
|
||||
)
|
||||
.expect("write cached plugin manifest");
|
||||
let cached_manifest = codex_home
|
||||
.path()
|
||||
.join(PLUGINS_CACHE_DIR)
|
||||
@@ -546,6 +579,10 @@ mod tests {
|
||||
let installed_plugin_names_by_marketplace =
|
||||
BTreeMap::<String, BTreeSet<String>>::from_iter([
|
||||
(REMOTE_GLOBAL_MARKETPLACE_NAME.to_string(), BTreeSet::new()),
|
||||
(
|
||||
REMOTE_CREATED_BY_ME_MARKETPLACE_NAME.to_string(),
|
||||
BTreeSet::new(),
|
||||
),
|
||||
(
|
||||
REMOTE_WORKSPACE_MARKETPLACE_NAME.to_string(),
|
||||
BTreeSet::new(),
|
||||
@@ -572,8 +609,12 @@ mod tests {
|
||||
|
||||
assert_eq!(
|
||||
removed,
|
||||
vec!["private-plugin@workspace-shared-with-me-private".to_string()]
|
||||
vec![
|
||||
"created-by-me-plugin@created-by-me-remote".to_string(),
|
||||
"private-plugin@workspace-shared-with-me-private".to_string(),
|
||||
]
|
||||
);
|
||||
assert!(!created_by_me_cached_manifest.exists());
|
||||
assert!(!cached_manifest.exists());
|
||||
assert!(canonical_cached_manifest.is_file());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user