From f0ce10bbf3598a105b8f050f5709544830b72c3c Mon Sep 17 00:00:00 2001 From: Matthew Zeng Date: Mon, 15 Jun 2026 21:47:13 -0700 Subject: [PATCH] Add curated plugin catalog revision markers --- codex-rs/core-plugins/src/startup_sync.rs | 28 +++++++++++++++++++ .../core-plugins/src/startup_sync_tests.rs | 22 ++++++++++++++- 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/codex-rs/core-plugins/src/startup_sync.rs b/codex-rs/core-plugins/src/startup_sync.rs index c5965f2212eb..abb9c1fb79e9 100644 --- a/codex-rs/core-plugins/src/startup_sync.rs +++ b/codex-rs/core-plugins/src/startup_sync.rs @@ -27,6 +27,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,6 +68,10 @@ fn curated_plugins_sha_path(codex_home: &Path) -> PathBuf { codex_home.join(CURATED_PLUGINS_SHA_FILE) } +pub(crate) 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 { sync_openai_plugins_repo_with_transport_overrides( codex_home, @@ -164,6 +169,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); } @@ -196,6 +202,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) @@ -299,6 +306,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); } @@ -306,6 +314,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) @@ -329,6 +338,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) @@ -564,6 +574,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, diff --git a/codex-rs/core-plugins/src/startup_sync_tests.rs b/codex-rs/core-plugins/src/startup_sync_tests.rs index f73e4ad9d129..a372950d1fad 100644 --- a/codex-rs/core-plugins/src/startup_sync_tests.rs +++ b/codex-rs/core-plugins/src/startup_sync_tests.rs @@ -21,6 +21,14 @@ fn write_file(path: &Path, contents: &str) { std::fs::write(path, contents).unwrap(); } +fn assert_curated_catalog_revision(repo_path: &Path, expected_revision: &str) { + assert_eq!( + std::fs::read_to_string(curated_plugins_catalog_revision_path(repo_path)) + .expect("read catalog revision"), + format!("{expected_revision}\n") + ); +} + fn write_curated_plugin(root: &Path, plugin_name: &str) { let plugin_root = root.join("plugins").join(plugin_name); write_file( @@ -343,6 +351,7 @@ exit 1 let repo_path = curated_plugins_repo_path(tmp.path()); assert!(repo_path.join(".git").is_dir()); assert_curated_gmail_repo(&repo_path); + assert_curated_catalog_revision(&repo_path, sha); assert_eq!(read_curated_plugins_sha(tmp.path()).as_deref(), Some(sha)); let invocations = std::fs::read_to_string(invocation_log).expect("read invocation log"); assert_eq!( @@ -451,7 +460,9 @@ fn sync_openai_plugins_repo_via_git_succeeds_with_local_rewritten_remote() { .expect("git sync should succeed"); assert_eq!(synced_sha, sha); - assert_curated_gmail_repo(&curated_plugins_repo_path(tmp.path())); + let curated_repo_path = curated_plugins_repo_path(tmp.path()); + assert_curated_gmail_repo(&curated_repo_path); + assert_curated_catalog_revision(&curated_repo_path, &sha); assert_eq!( read_curated_plugins_sha(tmp.path()).as_deref(), Some(sha.as_str()) @@ -505,6 +516,7 @@ fn sync_openai_plugins_repo_via_git_succeeds_with_local_rewritten_remote() { .expect("incremental git sync should succeed"); assert_eq!(synced_sha, updated_sha); + assert_curated_catalog_revision(&curated_plugins_repo_path(tmp.path()), &updated_sha); assert!( curated_plugins_repo_path(tmp.path()) .join("plugins/linear/.codex-plugin/plugin.json") @@ -556,11 +568,16 @@ fn sync_openai_plugins_repo_via_git_succeeds_with_local_rewritten_remote() { assert!(!has_plugins_clone_dirs(tmp.path())); let unchanged_sync_invocation_count = invocation_log_contents.lines().count(); + std::fs::remove_file(curated_plugins_catalog_revision_path( + &curated_plugins_repo_path(tmp.path()), + )) + .expect("remove catalog revision before unchanged sync"); let synced_sha = sync_openai_plugins_repo_via_git(tmp.path(), git_wrapper.to_str().expect("utf8 path")) .expect("unchanged git sync should succeed"); assert_eq!(synced_sha, updated_sha); + assert_curated_catalog_revision(&curated_plugins_repo_path(tmp.path()), &updated_sha); let invocation_log = std::fs::read_to_string(&invocation_log).expect("read sync invocations"); let unchanged_sync_invocations = invocation_log .lines() @@ -599,6 +616,7 @@ async fn sync_openai_plugins_repo_falls_back_to_http_when_git_is_unavailable() { let repo_path = curated_plugins_repo_path(tmp.path()); assert_eq!(synced_sha, sha); assert_curated_gmail_repo(&repo_path); + assert_curated_catalog_revision(&repo_path, sha); assert_eq!(read_curated_plugins_sha(tmp.path()).as_deref(), Some(sha)); } @@ -796,6 +814,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_curated_catalog_revision(&repo_path, sha); } #[tokio::test] @@ -824,6 +843,7 @@ async fn sync_openai_plugins_repo_falls_back_to_export_archive_when_no_snapshot_ let repo_path = curated_plugins_repo_path(tmp.path()); assert_eq!(synced_sha, export_sha); assert_curated_gmail_repo(&repo_path); + assert_curated_catalog_revision(&repo_path, export_sha); assert_eq!( read_curated_plugins_sha(tmp.path()).as_deref(), Some(export_sha)