mirror of
https://github.com/openai/codex.git
synced 2026-09-07 15:40:00 +00:00
## Why Executor plugin manifests are unsigned, so cleanup hooks must remain limited to explicitly trusted plugin identities and MCP targets. ## What changed - Allow the bundled Browser, Chrome, Chrome Dev, and Chrome Internal plugins to run the existing `Stop` hook for `node_repl.turn_ended`. - Continue allowing the bundled Computer Use plugin and rejecting unlisted or lookalike marketplace identities. ## Testing - Extend manifest-filtering coverage for unlisted bundled plugins and marketplace name variants. GitOrigin-RevId: 1ad5d679a1530994df5a9628c2117ce509fec9ad
146 lines
4.9 KiB
Rust
146 lines
4.9 KiB
Rust
use codex_config::HookEventsToml;
|
|
use codex_config::HookHandlerConfig;
|
|
use codex_config::MatcherGroup;
|
|
use codex_exec_server::ExecutorCapabilityDiscoverySnapshot;
|
|
use codex_plugin::ExecutorPluginHookSource;
|
|
use codex_plugin::PluginId;
|
|
use codex_plugin::manifest::PluginManifestHooks;
|
|
use codex_protocol::capabilities::CapabilityRootLocation;
|
|
use codex_protocol::protocol::HookEventName;
|
|
|
|
use crate::manifest::parse_plugin_manifest_uri;
|
|
|
|
struct AllowlistedExecutorPluginHook {
|
|
plugin_id: &'static str,
|
|
event: HookEventName,
|
|
server: &'static str,
|
|
tool: &'static str,
|
|
}
|
|
|
|
// Executor plugin manifests are unsigned, so temporarily hardcode the expected
|
|
// bundled plugin identities and cleanup MCP target until plugin signing lands.
|
|
const ALLOWLISTED_EXECUTOR_PLUGIN_HOOKS: &[AllowlistedExecutorPluginHook] = &[
|
|
AllowlistedExecutorPluginHook {
|
|
plugin_id: "browser@openai-bundled",
|
|
event: HookEventName::Stop,
|
|
server: "node_repl",
|
|
tool: "turn_ended",
|
|
},
|
|
AllowlistedExecutorPluginHook {
|
|
plugin_id: "chrome@openai-bundled",
|
|
event: HookEventName::Stop,
|
|
server: "node_repl",
|
|
tool: "turn_ended",
|
|
},
|
|
AllowlistedExecutorPluginHook {
|
|
plugin_id: "chrome-dev@openai-bundled",
|
|
event: HookEventName::Stop,
|
|
server: "node_repl",
|
|
tool: "turn_ended",
|
|
},
|
|
AllowlistedExecutorPluginHook {
|
|
plugin_id: "chrome-internal@openai-bundled",
|
|
event: HookEventName::Stop,
|
|
server: "node_repl",
|
|
tool: "turn_ended",
|
|
},
|
|
AllowlistedExecutorPluginHook {
|
|
plugin_id: "computer-use@openai-bundled",
|
|
event: HookEventName::Stop,
|
|
server: "node_repl",
|
|
tool: "turn_ended",
|
|
},
|
|
];
|
|
|
|
/// Returns accepted inline hook sources from executor-discovered plugin manifests.
|
|
///
|
|
/// Executor scoped hooks are best-effort because executor capabilities can become available
|
|
/// after earlier lifecycle events have passed.
|
|
///
|
|
/// Note: Executor manifests are not signed yet, so temporarily we only admit the known cleanup
|
|
/// hook from the bundled Browser, Chrome, Chrome Dev, Chrome Internal, and Computer Use plugins.
|
|
pub fn executor_plugin_hook_sources(
|
|
snapshot: &ExecutorCapabilityDiscoverySnapshot,
|
|
) -> Vec<ExecutorPluginHookSource> {
|
|
let mut sources = Vec::new();
|
|
|
|
for entry in snapshot.roots() {
|
|
let Ok(plugin_id) = PluginId::parse(&entry.selected_root.id) else {
|
|
continue;
|
|
};
|
|
let CapabilityRootLocation::Environment {
|
|
environment_id,
|
|
path: plugin_root,
|
|
} = &entry.selected_root.location;
|
|
let Ok(discovery) = &entry.result else {
|
|
continue;
|
|
};
|
|
let Some(plugin) = &discovery.plugin else {
|
|
continue;
|
|
};
|
|
let Ok(manifest) = parse_plugin_manifest_uri(
|
|
plugin_root,
|
|
&plugin.manifest.path,
|
|
&plugin.manifest.contents,
|
|
) else {
|
|
continue;
|
|
};
|
|
// Only inline hooks are supported for now, so skip any other source types.
|
|
let Some(PluginManifestHooks::Inline(hook_files)) = manifest.paths.hooks else {
|
|
continue;
|
|
};
|
|
|
|
for (hook_index, hook_file) in hook_files.into_iter().enumerate() {
|
|
let manifest_relative_path = plugin
|
|
.manifest
|
|
.path
|
|
.relative_path_from(plugin_root)
|
|
.unwrap_or_else(|| plugin.manifest.path.to_string());
|
|
|
|
sources.push(ExecutorPluginHookSource {
|
|
plugin_id: plugin_id.clone(),
|
|
environment_id: environment_id.clone(),
|
|
plugin_root: plugin_root.clone(),
|
|
manifest_path: plugin.manifest.path.clone(),
|
|
source_relative_path: format!("{manifest_relative_path}#hooks[{hook_index}]"),
|
|
hooks: hook_file.hooks,
|
|
});
|
|
}
|
|
}
|
|
|
|
// FIXME: Remove this temporary filter once executor plugin hooks can be trusted.
|
|
sources.into_iter().filter_map(allowlisted_source).collect()
|
|
}
|
|
|
|
fn allowlisted_source(mut source: ExecutorPluginHookSource) -> Option<ExecutorPluginHookSource> {
|
|
let allowlisted_hook = ALLOWLISTED_EXECUTOR_PLUGIN_HOOKS.iter().find(|hook| {
|
|
hook.plugin_id == source.plugin_id.as_key() && hook.event == HookEventName::Stop
|
|
})?;
|
|
let handler = source
|
|
.hooks
|
|
.stop
|
|
.into_iter()
|
|
.filter(|group| group.matcher.is_none())
|
|
.flat_map(|group| group.hooks)
|
|
.find(|handler| {
|
|
matches!(
|
|
handler,
|
|
HookHandlerConfig::McpTool { server, tool, .. }
|
|
if server == allowlisted_hook.server && tool == allowlisted_hook.tool
|
|
)
|
|
})?;
|
|
|
|
source.hooks = HookEventsToml {
|
|
stop: vec![MatcherGroup {
|
|
matcher: None,
|
|
hooks: vec![handler],
|
|
}],
|
|
..Default::default()
|
|
};
|
|
Some(source)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[path = "executor_hooks_tests.rs"]
|
|
mod tests;
|