Files
codex/codex-rs/core-plugins/src/test_support.rs
T
felixxia-oai 02dce8eb8d [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.
2026-06-16 01:16:11 +00:00

166 lines
4.4 KiB
Rust

use std::fs;
use std::path::Path;
use crate::OPENAI_API_CURATED_MARKETPLACE_NAME;
use crate::OPENAI_CURATED_MARKETPLACE_NAME;
use crate::PluginsConfigInput;
use codex_config::LoaderOverrides;
use codex_config::NoopThreadConfigLoader;
use codex_config::loader::load_config_layers_state;
use codex_exec_server::LOCAL_FS;
use codex_utils_absolute_path::AbsolutePathBuf;
use toml::Value;
pub(crate) const TEST_CURATED_PLUGIN_SHA: &str = "0123456789abcdef0123456789abcdef01234567";
pub(crate) const TEST_CURATED_PLUGIN_CACHE_VERSION: &str = "01234567";
pub(crate) fn write_file(path: &Path, contents: &str) {
fs::create_dir_all(path.parent().expect("file should have a parent")).unwrap();
fs::write(path, contents).unwrap();
}
pub(crate) fn write_curated_plugin(root: &Path, plugin_name: &str) {
let plugin_root = root.join("plugins").join(plugin_name);
write_file(
&plugin_root.join(".codex-plugin/plugin.json"),
&format!(
r#"{{
"name": "{plugin_name}",
"description": "Plugin that includes skills, MCP servers, and app connectors"
}}"#
),
);
write_file(
&plugin_root.join("skills/SKILL.md"),
"---\nname: sample\ndescription: sample\n---\n",
);
write_file(
&plugin_root.join(".mcp.json"),
r#"{
"mcpServers": {
"sample-docs": {
"type": "http",
"url": "https://sample.example/mcp"
}
}
}"#,
);
write_file(
&plugin_root.join(".app.json"),
r#"{
"apps": {
"calendar": {
"id": "connector_calendar"
}
}
}"#,
);
}
pub(crate) fn write_openai_curated_marketplace(root: &Path, plugin_names: &[&str]) {
write_curated_marketplace(
root,
"marketplace.json",
OPENAI_CURATED_MARKETPLACE_NAME,
/*display_name*/ None,
plugin_names,
);
}
pub(crate) fn write_openai_api_curated_marketplace(root: &Path, plugin_names: &[&str]) {
write_curated_marketplace(
root,
"api_marketplace.json",
OPENAI_API_CURATED_MARKETPLACE_NAME,
Some("OpenAI Curated"),
plugin_names,
);
}
fn write_curated_marketplace(
root: &Path,
manifest_name: &str,
marketplace_name: &str,
display_name: Option<&str>,
plugin_names: &[&str],
) {
let plugins = plugin_names
.iter()
.map(|plugin_name| {
format!(
r#"{{
"name": "{plugin_name}",
"source": {{
"source": "local",
"path": "./plugins/{plugin_name}"
}}
}}"#
)
})
.collect::<Vec<_>>()
.join(",\n");
let interface = display_name
.map(|display_name| {
format!(
r#"
"interface": {{
"displayName": "{display_name}"
}},"#
)
})
.unwrap_or_default();
write_file(
&root.join(".agents/plugins").join(manifest_name),
&format!(
r#"{{
"name": "{marketplace_name}",{interface}
"plugins": [
{plugins}
]
}}"#
),
);
for plugin_name in plugin_names {
write_curated_plugin(root, plugin_name);
}
}
pub(crate) fn write_curated_plugin_sha_with(codex_home: &Path, sha: &str) {
write_file(&codex_home.join(".tmp/plugins.sha"), &format!("{sha}\n"));
}
pub(crate) async fn load_plugins_config(codex_home: &Path, cwd: &Path) -> PluginsConfigInput {
let codex_home = AbsolutePathBuf::try_from(codex_home).expect("codex home should be absolute");
let cwd = AbsolutePathBuf::try_from(cwd).expect("cwd should be absolute");
let config_layer_stack = load_config_layers_state(
LOCAL_FS.as_ref(),
codex_home.as_path(),
Some(cwd),
&[],
LoaderOverrides::without_managed_config_for_tests(),
&NoopThreadConfigLoader,
)
.await
.expect("config should load");
let effective_config = config_layer_stack.effective_config();
PluginsConfigInput::new(
config_layer_stack,
feature_enabled(&effective_config, "plugins", /*default_enabled*/ true),
feature_enabled(
&effective_config,
"remote_plugin",
/*default_enabled*/ false,
),
"https://chatgpt.com/backend-api/".to_string(),
)
}
fn feature_enabled(config: &Value, key: &str, default_enabled: bool) -> bool {
config
.get("features")
.and_then(Value::as_table)
.and_then(|features| features.get(key))
.and_then(Value::as_bool)
.unwrap_or(default_enabled)
}