[codex] Load API curated marketplace by auth (#28383)

## Summary
- choose the local OpenAI curated marketplace manifest based on auth:
Codex backend auth gets the existing marketplace, direct provider auth
gets `api_marketplace.json`
- include Bedrock API key auth in the direct-provider API marketplace
path
- safely skip the API marketplace when `api_marketplace.json` is absent

## Validation
- `just fmt`
- `git diff --check origin/main...HEAD`
- CI should run the full validation

## Manual Testing

### - New api marketplace not available for API key sign
1. Safely not display anything from api marketplace
<img width="1161" height="289" alt="Screenshot 2026-06-15 at 21 37 43"
src="https://github.com/user-attachments/assets/a5f16642-8a20-4ac1-a0de-1274a4c7b5b2"
/>

### - New api marketplace for API key sign in
1. Setup api_marketplace.json
```
{
  "name": "openai-curated",
  "interface": {
    "displayName": "Codex official"
  },
  "plugins": [
    {
      "name": "linear",
      "source": {
        "source": "local",
        "path": "./plugins/linear"
      },
      "policy": {
        "installation": "AVAILABLE",
        "authentication": "ON_INSTALL"
      },
      "category": "Productivity"
    }
  ]
}
```

2. Log in with API key, observe that only the defined plugin from
api_marketplace.json is available from "Codex Official" (outside of
local testing marketplaces)
<img width="1167" height="446" alt="Screenshot 2026-06-15 at 21 16 53"
src="https://github.com/user-attachments/assets/7cf61477-d826-4ef6-bc05-0a23ac1c0259"
/>

also checked functionality on codex app

### - SiWC users 
Still uses 'default' marketplace.json and renders all plugins
<img width="1171" height="502" alt="Screenshot 2026-06-15 at 21 40 25"
src="https://github.com/user-attachments/assets/d212ea9b-0aa5-470b-8ea4-450efe65bb2b"
/>

also checked functionality on codex app


## Notes
- `just test -p codex-core-plugins` was started locally before splitting
branches, but I stopped relying on local tests per follow-up and left
final validation to PR CI.
This commit is contained in:
felixxia-oai authored and GitHub committed 2026-06-16 01:16:11 +00:00
1 parent 6e50b22e55
commit 02dce8eb8d
16 files changed
+682 -114

No files matched your search

+188 -1
View File
@@ -1,5 +1,7 @@
use super::*;
use crate::LoadedPlugin;
use crate::OPENAI_API_CURATED_MARKETPLACE_NAME;
use crate::OPENAI_CURATED_MARKETPLACE_NAME;
use crate::PluginLoadOutcome;
use crate::installed_marketplaces::marketplace_install_root;
use crate::loader::load_plugins_from_layer_stack;
@@ -16,6 +18,7 @@ use crate::test_support::TEST_CURATED_PLUGIN_SHA;
use crate::test_support::load_plugins_config as load_plugins_config_input;
use crate::test_support::write_curated_plugin_sha_with as write_curated_plugin_sha;
use crate::test_support::write_file;
use crate::test_support::write_openai_api_curated_marketplace;
use crate::test_support::write_openai_curated_marketplace;
use codex_app_server_protocol::AuthMode;
use codex_app_server_protocol::ConfigLayerSource;
@@ -778,11 +781,15 @@ remote_plugin = true
[plugins."linear@openai-curated"]
enabled = true
[plugins."linear@openai-api-curated"]
enabled = true
[plugins."calendar@openai-curated"]
enabled = true
"#,
);
write_cached_plugin(codex_home.path(), "openai-curated", "linear");
write_cached_plugin(codex_home.path(), "openai-api-curated", "linear");
write_cached_plugin(codex_home.path(), "openai-curated", "calendar");
write_cached_plugin(codex_home.path(), "openai-curated-remote", "linear");
write_cached_plugin(codex_home.path(), "openai-curated-remote", "remote-only");
@@ -3037,6 +3044,130 @@ plugins = true
);
}
#[tokio::test]
async fn list_marketplaces_uses_api_curated_manifest_when_selected() {
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_file(
&curated_root.join(".agents/plugins/marketplace.json"),
r#"{
"name": "openai-curated",
"plugins": [
{
"name": "siwc-plugin",
"source": {
"source": "local",
"path": "./plugins/siwc-plugin"
}
}
]
}"#,
);
write_file(
&curated_root.join(".agents/plugins/api_marketplace.json"),
r#"{
"name": "openai-api-curated",
"interface": {
"displayName": "OpenAI Curated"
},
"plugins": [
{
"name": "api-plugin",
"source": {
"source": "local",
"path": "./plugins/api-plugin"
}
}
]
}"#,
);
let config = load_config(tmp.path(), tmp.path()).await;
let manager = PluginsManager::new(tmp.path().to_path_buf());
manager.set_auth_mode(Some(AuthMode::ApiKey));
let marketplaces = manager
.list_marketplaces_for_config(&config, &[], /*include_openai_curated*/ true)
.unwrap()
.marketplaces;
let curated_marketplace = marketplaces
.into_iter()
.find(|marketplace| marketplace.name == OPENAI_API_CURATED_MARKETPLACE_NAME)
.expect("API curated marketplace should be listed");
assert_eq!(
curated_marketplace,
ConfiguredMarketplace {
name: "openai-api-curated".to_string(),
path: AbsolutePathBuf::try_from(
curated_root.join(".agents/plugins/api_marketplace.json")
)
.unwrap(),
interface: Some(MarketplaceInterface {
display_name: Some("OpenAI Curated".to_string()),
}),
plugins: vec![ConfiguredMarketplacePlugin {
id: "api-plugin@openai-api-curated".to_string(),
name: "api-plugin".to_string(),
local_version: None,
installed_version: None,
source: MarketplacePluginSource::Local {
path: AbsolutePathBuf::try_from(curated_root.join("plugins/api-plugin"))
.unwrap(),
},
policy: MarketplacePluginPolicy {
installation: MarketplacePluginInstallPolicy::Available,
authentication: MarketplacePluginAuthPolicy::OnInstall,
products: None,
},
interface: None,
keywords: Vec::new(),
installed: false,
enabled: false,
}],
}
);
}
#[tokio::test]
async fn list_marketplaces_skips_missing_api_curated_manifest() {
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_file(
&curated_root.join(".agents/plugins/marketplace.json"),
"{not valid json",
);
let config = load_config(tmp.path(), tmp.path()).await;
let manager = PluginsManager::new(tmp.path().to_path_buf());
manager.set_auth_mode(Some(AuthMode::BedrockApiKey));
let outcome = manager
.list_marketplaces_for_config(&config, &[], /*include_openai_curated*/ true)
.unwrap();
assert_eq!(outcome.errors, Vec::new());
assert_eq!(
outcome
.marketplaces
.iter()
.any(|marketplace| marketplace.name == OPENAI_API_CURATED_MARKETPLACE_NAME),
false
);
}
#[tokio::test]
async fn list_marketplaces_includes_installed_marketplace_roots() {
let tmp = tempfile::tempdir().unwrap();
@@ -3591,6 +3722,56 @@ fn refresh_curated_plugin_cache_reinstalls_missing_configured_plugin_with_curren
);
}
#[test]
fn refresh_curated_plugin_cache_reinstalls_missing_api_curated_plugin() {
let tmp = tempfile::tempdir().unwrap();
let curated_root = curated_plugins_repo_path(tmp.path());
write_openai_curated_marketplace(&curated_root, &[]);
write_openai_api_curated_marketplace(&curated_root, &["api-only"]);
write_curated_plugin_sha(tmp.path(), TEST_CURATED_PLUGIN_SHA);
let plugin_id = PluginId::new(
"api-only".to_string(),
OPENAI_API_CURATED_MARKETPLACE_NAME.to_string(),
)
.unwrap();
assert!(
refresh_curated_plugin_cache(tmp.path(), TEST_CURATED_PLUGIN_SHA, &[plugin_id])
.expect("cache refresh should recreate missing configured API curated plugin")
);
assert!(
tmp.path()
.join(format!(
"plugins/cache/openai-api-curated/api-only/{TEST_CURATED_PLUGIN_CACHE_VERSION}"
))
.is_dir()
);
}
#[test]
fn refresh_curated_plugin_cache_leaves_api_curated_plugin_when_api_manifest_missing() {
let tmp = tempfile::tempdir().unwrap();
let curated_root = curated_plugins_repo_path(tmp.path());
write_openai_curated_marketplace(&curated_root, &[]);
write_cached_plugin(tmp.path(), OPENAI_API_CURATED_MARKETPLACE_NAME, "api-only");
let plugin_id = PluginId::new(
"api-only".to_string(),
OPENAI_API_CURATED_MARKETPLACE_NAME.to_string(),
)
.unwrap();
assert!(
!refresh_curated_plugin_cache(tmp.path(), TEST_CURATED_PLUGIN_SHA, &[plugin_id])
.expect("cache refresh should skip missing API curated manifest")
);
assert!(
tmp.path()
.join("plugins/cache/openai-api-curated/api-only/local")
.is_dir()
);
}
#[test]
fn refresh_curated_plugin_cache_removes_cache_for_plugin_removed_from_marketplace() {
let tmp = tempfile::tempdir().unwrap();
@@ -3629,6 +3810,9 @@ plugins = true
[plugins."slack@openai-curated"]
enabled = true
[plugins."api-only@openai-api-curated"]
enabled = true
[plugins."sample@debug"]
enabled = true
"#,
@@ -3639,7 +3823,10 @@ enabled = true
.into_iter()
.map(|plugin_id| plugin_id.as_key())
.collect::<Vec<_>>(),
vec!["slack@openai-curated".to_string()]
vec![
"api-only@openai-api-curated".to_string(),
"slack@openai-curated".to_string(),
]
);
write_file(