fix: harden plugin feature gating (#15104)

Resubmit https://github.com/openai/codex/pull/15020 with correct
content.

1. Use requirement-resolved config.features as the plugin gate.
2. Guard plugin/list, plugin/read, and related flows behind that gate.
3. Skip bad marketplace.json files instead of failing the whole list.
4. Simplify plugin state and caching.
This commit is contained in:
xl-openai
2026-03-19 00:03:37 +00:00
committed by GitHub
parent 56d0c6bf67
commit dcd5e08269
13 changed files with 337 additions and 108 deletions
@@ -4842,6 +4842,7 @@ impl CodexMessageProcessor {
MarketplaceError::InvalidMarketplaceFile { .. }
| MarketplaceError::PluginNotFound { .. }
| MarketplaceError::PluginNotAvailable { .. }
| MarketplaceError::PluginsDisabled
| MarketplaceError::InvalidPlugin(_) => {
self.send_invalid_request_error(request_id, err.to_string())
.await;
@@ -5363,6 +5364,13 @@ impl CodexMessageProcessor {
.extend(valid_extra_roots);
}
let config = match self.load_latest_config(/*fallback_cwd*/ None).await {
Ok(config) => config,
Err(error) => {
self.outgoing.send_error(request_id, error).await;
return;
}
};
let skills_manager = self.thread_manager.skills_manager();
let mut data = Vec::new();
for cwd in cwds {
@@ -5370,7 +5378,7 @@ impl CodexMessageProcessor {
.get(&cwd)
.map_or(&[][..], std::vec::Vec::as_slice);
let outcome = skills_manager
.skills_for_cwd_with_extra_user_roots(&cwd, force_reload, extra_roots)
.skills_for_cwd_with_extra_user_roots(&cwd, &config, force_reload, extra_roots)
.await;
let errors = errors_to_info(&outcome.errors);
let skills = skills_to_info(&outcome.skills, &outcome.disabled_paths);
@@ -28,12 +28,22 @@ use wiremock::matchers::path;
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(10);
const TEST_CURATED_PLUGIN_SHA: &str = "0123456789abcdef0123456789abcdef01234567";
fn write_plugins_enabled_config(codex_home: &std::path::Path) -> std::io::Result<()> {
std::fs::write(
codex_home.join("config.toml"),
r#"[features]
plugins = true
"#,
)
}
#[tokio::test]
async fn plugin_list_returns_invalid_request_for_invalid_marketplace_file() -> Result<()> {
async fn plugin_list_skips_invalid_marketplace_file() -> Result<()> {
let codex_home = TempDir::new()?;
let repo_root = TempDir::new()?;
std::fs::create_dir_all(repo_root.path().join(".git"))?;
std::fs::create_dir_all(repo_root.path().join(".agents/plugins"))?;
write_plugins_enabled_config(codex_home.path())?;
std::fs::write(
repo_root.path().join(".agents/plugins/marketplace.json"),
"{not json",
@@ -57,14 +67,23 @@ async fn plugin_list_returns_invalid_request_for_invalid_marketplace_file() -> R
})
.await?;
let err = timeout(
let response: JSONRPCResponse = timeout(
DEFAULT_TIMEOUT,
mcp.read_stream_until_error_message(RequestId::Integer(request_id)),
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
)
.await??;
let response: PluginListResponse = to_response(response)?;
assert_eq!(err.error.code, -32600);
assert!(err.error.message.contains("invalid marketplace file"));
assert!(
response.marketplaces.iter().all(|marketplace| {
marketplace.path
!= AbsolutePathBuf::try_from(
repo_root.path().join(".agents/plugins/marketplace.json"),
)
.expect("absolute marketplace path")
}),
"invalid marketplace should be skipped"
);
Ok(())
}
@@ -98,6 +117,7 @@ async fn plugin_list_rejects_relative_cwds() -> Result<()> {
async fn plugin_list_accepts_omitted_cwds() -> Result<()> {
let codex_home = TempDir::new()?;
std::fs::create_dir_all(codex_home.path().join(".agents/plugins"))?;
write_plugins_enabled_config(codex_home.path())?;
std::fs::write(
codex_home.path().join(".agents/plugins/marketplace.json"),
r#"{
@@ -385,6 +405,7 @@ async fn plugin_list_returns_plugin_interface_with_absolute_asset_paths() -> Res
std::fs::create_dir_all(repo_root.path().join(".git"))?;
std::fs::create_dir_all(repo_root.path().join(".agents/plugins"))?;
std::fs::create_dir_all(plugin_root.join(".codex-plugin"))?;
write_plugins_enabled_config(codex_home.path())?;
std::fs::write(
repo_root.path().join(".agents/plugins/marketplace.json"),
r#"{
@@ -518,6 +539,7 @@ async fn plugin_list_accepts_legacy_string_default_prompt() -> Result<()> {
std::fs::create_dir_all(repo_root.path().join(".git"))?;
std::fs::create_dir_all(repo_root.path().join(".agents/plugins"))?;
std::fs::create_dir_all(plugin_root.join(".codex-plugin"))?;
write_plugins_enabled_config(codex_home.path())?;
std::fs::write(
repo_root.path().join(".agents/plugins/marketplace.json"),
r#"{
@@ -232,6 +232,7 @@ async fn plugin_read_accepts_legacy_string_default_prompt() -> Result<()> {
}
}"##,
)?;
write_plugins_enabled_config(&codex_home)?;
let mut mcp = McpProcess::new(codex_home.path()).await?;
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
@@ -285,6 +286,7 @@ async fn plugin_read_returns_invalid_request_when_plugin_is_missing() -> Result<
]
}"#,
)?;
write_plugins_enabled_config(&codex_home)?;
let mut mcp = McpProcess::new(codex_home.path()).await?;
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
@@ -336,6 +338,7 @@ async fn plugin_read_returns_invalid_request_when_plugin_manifest_is_missing() -
]
}"#,
)?;
write_plugins_enabled_config(&codex_home)?;
let mut mcp = McpProcess::new(codex_home.path()).await?;
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
@@ -382,3 +385,13 @@ fn write_installed_plugin(
)?;
Ok(())
}
fn write_plugins_enabled_config(codex_home: &TempDir) -> Result<()> {
std::fs::write(
codex_home.path().join("config.toml"),
r#"[features]
plugins = true
"#,
)?;
Ok(())
}