mirror of
https://github.com/openai/codex.git
synced 2026-11-14 21:40:39 +00:00
Add revision-gated runtime tool catalog
This commit is contained in:
@@ -9,8 +9,10 @@ use tracing::warn;
|
||||
|
||||
use crate::OPENAI_API_CURATED_MARKETPLACE_NAME;
|
||||
use crate::OPENAI_CURATED_MARKETPLACE_NAME;
|
||||
use crate::PluginCatalogRevision;
|
||||
use crate::PluginsConfigInput;
|
||||
use crate::PluginsManager;
|
||||
use crate::marketplace::MarketplaceListOutcome;
|
||||
use crate::marketplace::MarketplacePluginInstallPolicy;
|
||||
use crate::remote::REMOTE_GLOBAL_MARKETPLACE_NAME;
|
||||
|
||||
@@ -66,11 +68,34 @@ pub struct ToolSuggestDiscoverablePlugin {
|
||||
pub app_connector_ids: Vec<String>,
|
||||
}
|
||||
|
||||
/// Selects the plugin catalog input used by the legacy tool-suggestion projection.
|
||||
pub enum ToolSuggestPluginCatalog<'a> {
|
||||
RebuildAll,
|
||||
Revisioned {
|
||||
revision: &'a PluginCatalogRevision,
|
||||
marketplaces: &'a MarketplaceListOutcome,
|
||||
},
|
||||
}
|
||||
|
||||
impl PluginsManager {
|
||||
pub async fn list_tool_suggest_discoverable_plugins(
|
||||
&self,
|
||||
input: &ToolSuggestPluginDiscoveryInput,
|
||||
auth: Option<&CodexAuth>,
|
||||
) -> anyhow::Result<Vec<ToolSuggestDiscoverablePlugin>> {
|
||||
self.list_tool_suggest_discoverable_plugins_with_catalog(
|
||||
input,
|
||||
auth,
|
||||
ToolSuggestPluginCatalog::RebuildAll,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn list_tool_suggest_discoverable_plugins_with_catalog(
|
||||
&self,
|
||||
input: &ToolSuggestPluginDiscoveryInput,
|
||||
auth: Option<&CodexAuth>,
|
||||
catalog: ToolSuggestPluginCatalog<'_>,
|
||||
) -> anyhow::Result<Vec<ToolSuggestDiscoverablePlugin>> {
|
||||
if !input.plugins.plugins_enabled {
|
||||
return Ok(Vec::new());
|
||||
@@ -78,14 +103,22 @@ impl PluginsManager {
|
||||
|
||||
let use_remote_global_catalog =
|
||||
input.plugins.remote_plugin_enabled && auth.is_some_and(CodexAuth::uses_codex_backend);
|
||||
let marketplaces = self
|
||||
.list_marketplaces_for_config(
|
||||
let include_openai_curated = !use_remote_global_catalog;
|
||||
let marketplaces = match catalog {
|
||||
ToolSuggestPluginCatalog::RebuildAll => {
|
||||
self.list_marketplaces_for_config(&input.plugins, &[], include_openai_curated)
|
||||
}
|
||||
ToolSuggestPluginCatalog::Revisioned {
|
||||
revision,
|
||||
marketplaces,
|
||||
} => self.list_marketplaces_for_config_with_revisioned_plugin_catalog(
|
||||
&input.plugins,
|
||||
&[],
|
||||
/*include_openai_curated*/ !use_remote_global_catalog,
|
||||
)
|
||||
.context("failed to list plugin marketplaces for tool suggestions")?
|
||||
.marketplaces;
|
||||
include_openai_curated,
|
||||
(revision, marketplaces),
|
||||
),
|
||||
}
|
||||
.context("failed to list plugin marketplaces for tool suggestions")?
|
||||
.marketplaces;
|
||||
let remote_installed_marketplaces = if use_remote_global_catalog {
|
||||
self.build_remote_installed_plugin_marketplaces_from_cache(&[
|
||||
REMOTE_GLOBAL_MARKETPLACE_NAME,
|
||||
|
||||
@@ -9,6 +9,7 @@ pub mod marketplace_add;
|
||||
pub mod marketplace_remove;
|
||||
pub mod marketplace_upgrade;
|
||||
mod plugin_bundle_archive;
|
||||
mod plugin_catalog_revision;
|
||||
mod provider;
|
||||
pub mod remote;
|
||||
pub mod remote_bundle;
|
||||
@@ -33,6 +34,7 @@ pub type PluginLoadOutcome = codex_plugin::PluginLoadOutcome<codex_config::McpSe
|
||||
|
||||
pub use app_mcp_routing::apps_route_available;
|
||||
pub use discoverable::ToolSuggestDiscoverablePlugin;
|
||||
pub use discoverable::ToolSuggestPluginCatalog;
|
||||
pub use discoverable::ToolSuggestPluginDiscoveryInput;
|
||||
pub use loader::PluginHookLoadOutcome;
|
||||
pub use manager::ConfiguredMarketplace;
|
||||
@@ -52,6 +54,7 @@ pub use manager::PluginsManager;
|
||||
pub use manager::RecommendedPluginCandidatesInput;
|
||||
pub use marketplace_upgrade::ConfiguredMarketplaceUpgradeError as PluginMarketplaceUpgradeError;
|
||||
pub use marketplace_upgrade::ConfiguredMarketplaceUpgradeOutcome as PluginMarketplaceUpgradeOutcome;
|
||||
pub use plugin_catalog_revision::PluginCatalogRevision;
|
||||
pub use provider::ExecutorPluginProvider;
|
||||
pub use provider::ExecutorPluginProviderError;
|
||||
pub use provider::ResolvedExecutorPlugin;
|
||||
|
||||
@@ -32,12 +32,15 @@ use crate::marketplace::MarketplacePluginSource;
|
||||
use crate::marketplace::ResolvedMarketplacePlugin;
|
||||
use crate::marketplace::find_installable_marketplace_plugin;
|
||||
use crate::marketplace::find_marketplace_plugin;
|
||||
use crate::marketplace::home_dir;
|
||||
use crate::marketplace::list_marketplaces;
|
||||
use crate::marketplace::list_marketplaces_with_home;
|
||||
use crate::marketplace::plugin_interface_with_marketplace_category;
|
||||
use crate::marketplace_upgrade::ConfiguredMarketplaceUpgradeError;
|
||||
use crate::marketplace_upgrade::ConfiguredMarketplaceUpgradeOutcome;
|
||||
use crate::marketplace_upgrade::configured_git_marketplace_names;
|
||||
use crate::marketplace_upgrade::upgrade_configured_git_marketplaces;
|
||||
use crate::plugin_catalog_revision::PluginCatalogRevision;
|
||||
use crate::remote::RecommendedPluginsMode;
|
||||
use crate::remote::RemoteInstalledPlugin;
|
||||
use crate::remote::RemotePluginCatalogError;
|
||||
@@ -46,6 +49,7 @@ use crate::remote_legacy::RemotePluginFetchError;
|
||||
use crate::remote_legacy::RemotePluginMutationError;
|
||||
use crate::startup_sync::curated_plugins_api_marketplace_path;
|
||||
use crate::startup_sync::curated_plugins_repo_path;
|
||||
use crate::startup_sync::read_curated_plugins_catalog_revision;
|
||||
use crate::startup_sync::read_curated_plugins_sha;
|
||||
use crate::startup_sync::sync_openai_plugins_repo;
|
||||
use crate::store::PluginInstallResult as StorePluginInstallResult;
|
||||
@@ -1235,10 +1239,96 @@ impl PluginsManager {
|
||||
return Ok(ConfiguredMarketplaceListOutcome::default());
|
||||
}
|
||||
|
||||
let (installed_plugins, enabled_plugins) = self.configured_plugin_states(config);
|
||||
let marketplace_roots =
|
||||
self.marketplace_roots(config, additional_roots, include_openai_curated);
|
||||
let marketplace_outcome = list_marketplaces(&marketplace_roots)?;
|
||||
Ok(self.configured_marketplaces_from_outcome(config, marketplace_outcome))
|
||||
}
|
||||
|
||||
pub fn revisioned_plugin_catalog_revision_for_config(
|
||||
&self,
|
||||
config: &PluginsConfigInput,
|
||||
include_openai_curated: bool,
|
||||
) -> Option<PluginCatalogRevision> {
|
||||
if !config.plugins_enabled
|
||||
|| !include_openai_curated
|
||||
|| matches!(
|
||||
self.auth_mode(),
|
||||
Some(AuthMode::ApiKey | AuthMode::BedrockApiKey)
|
||||
)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let curated_repo_root = curated_plugins_repo_path(self.codex_home.as_path());
|
||||
read_curated_plugins_catalog_revision(&curated_repo_root)
|
||||
}
|
||||
|
||||
pub fn build_revisioned_plugin_catalog(
|
||||
&self,
|
||||
revision: &PluginCatalogRevision,
|
||||
) -> Result<MarketplaceListOutcome, MarketplaceError> {
|
||||
let incomplete = || {
|
||||
MarketplaceError::InvalidPlugin(
|
||||
"revisioned plugin catalog did not load completely".to_string(),
|
||||
)
|
||||
};
|
||||
if !revision.is_current() {
|
||||
return Err(incomplete());
|
||||
}
|
||||
let root = AbsolutePathBuf::try_from(revision.source_root().to_path_buf())
|
||||
.map_err(|_| incomplete())?;
|
||||
let outcome =
|
||||
list_marketplaces_with_home(std::slice::from_ref(&root), /*home_dir*/ None)?;
|
||||
if !revision.is_current() || outcome.marketplaces.is_empty() || !outcome.errors.is_empty() {
|
||||
return Err(incomplete());
|
||||
}
|
||||
Ok(outcome)
|
||||
}
|
||||
|
||||
pub(crate) fn list_marketplaces_for_config_with_revisioned_plugin_catalog(
|
||||
&self,
|
||||
config: &PluginsConfigInput,
|
||||
include_openai_curated: bool,
|
||||
revisioned_catalog: (&PluginCatalogRevision, &MarketplaceListOutcome),
|
||||
) -> Result<ConfiguredMarketplaceListOutcome, MarketplaceError> {
|
||||
if !config.plugins_enabled {
|
||||
return Ok(ConfiguredMarketplaceListOutcome::default());
|
||||
}
|
||||
|
||||
let Some(current_revision) =
|
||||
self.revisioned_plugin_catalog_revision_for_config(config, include_openai_curated)
|
||||
else {
|
||||
return self.list_marketplaces_for_config(config, &[], include_openai_curated);
|
||||
};
|
||||
let (revision, revisioned_catalog) = revisioned_catalog;
|
||||
if current_revision != *revision || !revision.is_current() {
|
||||
return self.list_marketplaces_for_config(config, &[], include_openai_curated);
|
||||
}
|
||||
let roots = self.marketplace_roots(config, &[], include_openai_curated);
|
||||
let Some(revisioned_index) = roots
|
||||
.iter()
|
||||
.position(|root| root.as_path() == current_revision.source_root())
|
||||
else {
|
||||
return Err(MarketplaceError::InvalidPlugin(
|
||||
"revisioned plugin catalog source is missing".to_string(),
|
||||
));
|
||||
};
|
||||
let mut marketplace_outcome =
|
||||
list_marketplaces_with_home(&roots[..revisioned_index], home_dir().as_deref())?;
|
||||
extend_marketplace_outcome(&mut marketplace_outcome, revisioned_catalog.clone());
|
||||
let remaining =
|
||||
list_marketplaces_with_home(&roots[revisioned_index + 1..], /*home_dir*/ None)?;
|
||||
extend_marketplace_outcome(&mut marketplace_outcome, remaining);
|
||||
|
||||
Ok(self.configured_marketplaces_from_outcome(config, marketplace_outcome))
|
||||
}
|
||||
|
||||
fn configured_marketplaces_from_outcome(
|
||||
&self,
|
||||
config: &PluginsConfigInput,
|
||||
marketplace_outcome: MarketplaceListOutcome,
|
||||
) -> ConfiguredMarketplaceListOutcome {
|
||||
let (installed_plugins, enabled_plugins) = self.configured_plugin_states(config);
|
||||
let mut seen_plugin_keys = HashSet::new();
|
||||
let marketplaces = marketplace_outcome
|
||||
.marketplaces
|
||||
@@ -1310,10 +1400,10 @@ impl PluginsManager {
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(ConfiguredMarketplaceListOutcome {
|
||||
ConfiguredMarketplaceListOutcome {
|
||||
marketplaces,
|
||||
errors: marketplace_outcome.errors,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn discover_marketplaces_for_config(
|
||||
@@ -2136,6 +2226,34 @@ fn remote_plugin_install_required_description(source: &MarketplacePluginSource)
|
||||
)
|
||||
}
|
||||
|
||||
fn extend_marketplace_outcome(
|
||||
aggregate: &mut MarketplaceListOutcome,
|
||||
outcome: MarketplaceListOutcome,
|
||||
) {
|
||||
let mut marketplace_paths = aggregate
|
||||
.marketplaces
|
||||
.iter()
|
||||
.map(|marketplace| marketplace.path.clone())
|
||||
.collect::<HashSet<_>>();
|
||||
aggregate.marketplaces.extend(
|
||||
outcome
|
||||
.marketplaces
|
||||
.into_iter()
|
||||
.filter(|marketplace| marketplace_paths.insert(marketplace.path.clone())),
|
||||
);
|
||||
let mut error_paths = aggregate
|
||||
.errors
|
||||
.iter()
|
||||
.map(|error| error.path.clone())
|
||||
.collect::<HashSet<_>>();
|
||||
aggregate.errors.extend(
|
||||
outcome
|
||||
.errors
|
||||
.into_iter()
|
||||
.filter(|error| error_paths.insert(error.path.clone())),
|
||||
);
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum PluginInstallError {
|
||||
#[error("{0}")]
|
||||
|
||||
@@ -3163,6 +3163,77 @@ plugins = true
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn curated_catalog_reuses_only_a_current_proven_revision() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let curated_root = curated_plugins_repo_path(tmp.path());
|
||||
write_file(
|
||||
&tmp.path().join(CONFIG_TOML_FILE),
|
||||
r#"[features]
|
||||
plugins = true
|
||||
"#,
|
||||
);
|
||||
write_openai_curated_marketplace(&curated_root, &["linear"]);
|
||||
let config = load_config(tmp.path(), tmp.path()).await;
|
||||
let manager = PluginsManager::new(tmp.path().to_path_buf());
|
||||
let plugin_ids = |outcome: ConfiguredMarketplaceListOutcome| {
|
||||
outcome
|
||||
.marketplaces
|
||||
.into_iter()
|
||||
.flat_map(|marketplace| marketplace.plugins)
|
||||
.map(|plugin| plugin.id)
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
|
||||
let first = manager
|
||||
.list_marketplaces_for_config(&config, &[], /*include_openai_curated*/ true)
|
||||
.expect("first unrevisioned read");
|
||||
write_openai_curated_marketplace(&curated_root, &["github"]);
|
||||
let second = manager
|
||||
.list_marketplaces_for_config(&config, &[], /*include_openai_curated*/ true)
|
||||
.expect("second unrevisioned read");
|
||||
assert_eq!(plugin_ids(first), vec!["linear@openai-curated"]);
|
||||
assert_eq!(plugin_ids(second), vec!["github@openai-curated"]);
|
||||
|
||||
write_file(
|
||||
&curated_root.join(".codex-plugin-catalog-revision"),
|
||||
"revision-one\n",
|
||||
);
|
||||
let revision = manager
|
||||
.revisioned_plugin_catalog_revision_for_config(
|
||||
&config, /*include_openai_curated*/ true,
|
||||
)
|
||||
.expect("revisioned curated source");
|
||||
|
||||
let outcome = manager
|
||||
.build_revisioned_plugin_catalog(&revision)
|
||||
.expect("complete revisioned catalog");
|
||||
assert_eq!(outcome.marketplaces.len(), 1);
|
||||
write_openai_curated_marketplace(&curated_root, &["linear"]);
|
||||
let reused = manager
|
||||
.list_marketplaces_for_config_with_revisioned_plugin_catalog(
|
||||
&config,
|
||||
/*include_openai_curated*/ true,
|
||||
(&revision, &outcome),
|
||||
)
|
||||
.expect("reuse revisioned catalog");
|
||||
assert_eq!(plugin_ids(reused), vec!["github@openai-curated"]);
|
||||
|
||||
write_file(
|
||||
&curated_root.join(".codex-plugin-catalog-revision"),
|
||||
"revision-two\n",
|
||||
);
|
||||
assert!(manager.build_revisioned_plugin_catalog(&revision).is_err());
|
||||
let rebuilt = manager
|
||||
.list_marketplaces_for_config_with_revisioned_plugin_catalog(
|
||||
&config,
|
||||
/*include_openai_curated*/ true,
|
||||
(&revision, &outcome),
|
||||
)
|
||||
.expect("rebuild stale catalog");
|
||||
assert_eq!(plugin_ids(rebuilt), vec!["linear@openai-curated"]);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn list_marketplaces_can_skip_openai_curated_before_loading() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// A revision marker materialized inside the plugin catalog source it identifies.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct PluginCatalogRevision {
|
||||
source_root: PathBuf,
|
||||
marker_path: PathBuf,
|
||||
marker_contents: String,
|
||||
}
|
||||
|
||||
impl PluginCatalogRevision {
|
||||
pub(crate) fn read(source_root: &Path, marker_path: PathBuf) -> Option<Self> {
|
||||
let marker_contents = fs::read_to_string(&marker_path).ok()?;
|
||||
Some(Self {
|
||||
source_root: source_root.to_path_buf(),
|
||||
marker_path,
|
||||
marker_contents,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn source_root(&self) -> &Path {
|
||||
&self.source_root
|
||||
}
|
||||
|
||||
pub fn marker_contents(&self) -> &str {
|
||||
&self.marker_contents
|
||||
}
|
||||
|
||||
pub(crate) fn is_current(&self) -> bool {
|
||||
fs::read_to_string(&self.marker_path).is_ok_and(|contents| contents == self.marker_contents)
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ use tempfile::TempDir;
|
||||
use tracing::warn;
|
||||
use zip::ZipArchive;
|
||||
|
||||
use crate::plugin_catalog_revision::PluginCatalogRevision;
|
||||
use codex_login::default_client::build_reqwest_client;
|
||||
|
||||
const GITHUB_API_BASE_URL: &str = "https://api.github.com";
|
||||
@@ -27,6 +28,7 @@ const OPENAI_PLUGINS_GIT_URL: &str = "https://github.com/openai/plugins.git";
|
||||
const CURATED_PLUGINS_FETCH_REF: &str = "refs/codex/curated-sync";
|
||||
const CURATED_PLUGINS_RELATIVE_DIR: &str = ".tmp/plugins";
|
||||
const CURATED_PLUGINS_SHA_FILE: &str = ".tmp/plugins.sha";
|
||||
const CURATED_PLUGINS_CATALOG_REVISION_FILE: &str = ".codex-plugin-catalog-revision";
|
||||
const CURATED_PLUGINS_SYNC_LOCK_FILE: &str = ".tmp/plugins.sync.lock";
|
||||
const CURATED_PLUGINS_BACKUP_ARCHIVE_FALLBACK_VERSION: &str = "export-backup";
|
||||
const CURATED_PLUGINS_GIT_TIMEOUT: Duration = Duration::from_secs(30);
|
||||
@@ -67,10 +69,20 @@ pub fn read_curated_plugins_sha(codex_home: &Path) -> Option<String> {
|
||||
read_sha_file(curated_plugins_sha_path(codex_home).as_path())
|
||||
}
|
||||
|
||||
pub(crate) fn read_curated_plugins_catalog_revision(
|
||||
repo_path: &Path,
|
||||
) -> Option<PluginCatalogRevision> {
|
||||
PluginCatalogRevision::read(repo_path, curated_plugins_catalog_revision_path(repo_path))
|
||||
}
|
||||
|
||||
fn curated_plugins_sha_path(codex_home: &Path) -> PathBuf {
|
||||
codex_home.join(CURATED_PLUGINS_SHA_FILE)
|
||||
}
|
||||
|
||||
fn curated_plugins_catalog_revision_path(repo_path: &Path) -> PathBuf {
|
||||
repo_path.join(CURATED_PLUGINS_CATALOG_REVISION_FILE)
|
||||
}
|
||||
|
||||
pub fn sync_openai_plugins_repo(codex_home: &Path) -> Result<String, String> {
|
||||
sync_openai_plugins_repo_with_transport_overrides(
|
||||
codex_home,
|
||||
@@ -168,6 +180,7 @@ fn sync_openai_plugins_repo_via_git(codex_home: &Path, git_binary: &str) -> Resu
|
||||
let local_sha = read_local_git_or_sha_file(&repo_path, &sha_path, git_binary);
|
||||
|
||||
if local_sha.as_deref() == Some(remote_sha.as_str()) && repo_path.join(".git").is_dir() {
|
||||
ensure_curated_plugins_catalog_revision(&repo_path, &remote_sha)?;
|
||||
return Ok(remote_sha);
|
||||
}
|
||||
|
||||
@@ -200,6 +213,7 @@ fn sync_openai_plugins_repo_via_git(codex_home: &Path, git_binary: &str) -> Resu
|
||||
}
|
||||
|
||||
ensure_marketplace_manifest_exists(staged_repo_dir.path())?;
|
||||
write_curated_plugins_catalog_revision(staged_repo_dir.path(), &remote_sha)?;
|
||||
activate_curated_repo(&repo_path, staged_repo_dir)?;
|
||||
write_curated_plugins_sha(&sha_path, &remote_sha)?;
|
||||
Ok(remote_sha)
|
||||
@@ -303,6 +317,7 @@ fn sync_openai_plugins_repo_via_http(
|
||||
let local_sha = read_sha_file(&sha_path);
|
||||
|
||||
if local_sha.as_deref() == Some(remote_sha.as_str()) && repo_path.is_dir() {
|
||||
ensure_curated_plugins_catalog_revision(&repo_path, &remote_sha)?;
|
||||
return Ok(remote_sha);
|
||||
}
|
||||
|
||||
@@ -310,6 +325,7 @@ fn sync_openai_plugins_repo_via_http(
|
||||
let zipball_bytes = runtime.block_on(fetch_curated_repo_zipball(api_base_url, &remote_sha))?;
|
||||
extract_zipball_to_dir(&zipball_bytes, staged_repo_dir.path())?;
|
||||
ensure_marketplace_manifest_exists(staged_repo_dir.path())?;
|
||||
write_curated_plugins_catalog_revision(staged_repo_dir.path(), &remote_sha)?;
|
||||
activate_curated_repo(&repo_path, staged_repo_dir)?;
|
||||
write_curated_plugins_sha(&sha_path, &remote_sha)?;
|
||||
Ok(remote_sha)
|
||||
@@ -333,6 +349,7 @@ fn sync_openai_plugins_repo_via_backup_archive(
|
||||
ensure_marketplace_manifest_exists(staged_repo_dir.path())?;
|
||||
let export_version = read_extracted_backup_archive_git_sha(staged_repo_dir.path())?
|
||||
.unwrap_or_else(|| CURATED_PLUGINS_BACKUP_ARCHIVE_FALLBACK_VERSION.to_string());
|
||||
write_curated_plugins_catalog_revision(staged_repo_dir.path(), &export_version)?;
|
||||
activate_curated_repo(&repo_path, staged_repo_dir)?;
|
||||
write_curated_plugins_sha(&sha_path, &export_version)?;
|
||||
Ok(export_version)
|
||||
@@ -568,6 +585,24 @@ fn write_curated_plugins_sha(sha_path: &Path, remote_sha: &str) -> Result<(), St
|
||||
})
|
||||
}
|
||||
|
||||
fn ensure_curated_plugins_catalog_revision(repo_path: &Path, revision: &str) -> Result<(), String> {
|
||||
let revision_path = curated_plugins_catalog_revision_path(repo_path);
|
||||
if read_sha_file(&revision_path).as_deref() == Some(revision) {
|
||||
return Ok(());
|
||||
}
|
||||
write_curated_plugins_catalog_revision(repo_path, revision)
|
||||
}
|
||||
|
||||
fn write_curated_plugins_catalog_revision(repo_path: &Path, revision: &str) -> Result<(), String> {
|
||||
let revision_path = curated_plugins_catalog_revision_path(repo_path);
|
||||
std::fs::write(&revision_path, format!("{revision}\n")).map_err(|err| {
|
||||
format!(
|
||||
"failed to write curated plugins catalog revision file {}: {err}",
|
||||
revision_path.display()
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn read_local_git_or_sha_file(
|
||||
repo_path: &Path,
|
||||
sha_path: &Path,
|
||||
|
||||
@@ -600,6 +600,12 @@ async fn sync_openai_plugins_repo_falls_back_to_http_when_git_is_unavailable() {
|
||||
assert_eq!(synced_sha, sha);
|
||||
assert_curated_gmail_repo(&repo_path);
|
||||
assert_eq!(read_curated_plugins_sha(tmp.path()).as_deref(), Some(sha));
|
||||
let revision = read_curated_plugins_catalog_revision(&repo_path)
|
||||
.expect("synced repository should carry a catalog revision");
|
||||
assert_eq!(revision.marker_contents(), format!("{sha}\n"));
|
||||
assert!(revision.is_current());
|
||||
write_curated_plugins_catalog_revision(&repo_path, "replacement").expect("replace revision");
|
||||
assert!(!revision.is_current());
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
@@ -796,6 +802,7 @@ async fn sync_openai_plugins_repo_skips_archive_download_when_sha_matches() {
|
||||
|
||||
assert_eq!(read_curated_plugins_sha(tmp.path()).as_deref(), Some(sha));
|
||||
assert!(repo_path.join(".agents/plugins/marketplace.json").is_file());
|
||||
assert!(read_curated_plugins_catalog_revision(&repo_path).is_some());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
Reference in New Issue
Block a user