mirror of
https://github.com/openai/codex.git
synced 2026-09-14 11:57:03 +00:00
## What changed - Centralize the allowlist for bundled MCP cleanup hooks and use it for both local and executor-discovered plugins, including `unified-computer-use` cleanup through `cua_repl`. - Mark matching cleanup hooks as trusted built-ins so they run without saved hook trust and remain active when regular hooks or their per-hook state are disabled. Plugin enablement and managed-only policy still apply. - Hide built-in cleanup hooks from hook listings and lifecycle notifications while retaining their metrics. Keep the built-in classification out of serialized protocol data. ## Testing - Cover allowlist boundaries, trust and enablement behavior, inline and file-based hook declarations, MCP success and error responses, hook listing, lifecycle notifications, metrics, and protocol serialization. GitOrigin-RevId: f93b7bc99f4ed9694f529def8ec383b45f31430e
153 lines
5.6 KiB
Rust
153 lines
5.6 KiB
Rust
//! Executor-discovered plugin hook admission and trusted MCP routing.
|
|
|
|
use codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID;
|
|
use codex_config::HookHandlerConfig;
|
|
use codex_exec_server::ExecutorCapabilityDiscoverySnapshot;
|
|
use codex_mcp::CODEX_APPS_MCP_SERVER_NAME;
|
|
use codex_mcp::MCP_TOOL_CODEX_APPS_META_KEY;
|
|
use codex_mcp::ToolInfo;
|
|
use codex_plugin::ExecutorPluginHookSource;
|
|
use codex_plugin::PluginId;
|
|
use codex_plugin::is_allowlisted_bundled_cleanup_hook;
|
|
use codex_plugin::manifest::PluginManifestHooks;
|
|
use codex_protocol::capabilities::CapabilityRootLocation;
|
|
use serde_json::Map;
|
|
use serde_json::Value;
|
|
|
|
use crate::manifest::parse_plugin_manifest_uri;
|
|
|
|
/// Returns accepted inline hook sources from executor-discovered plugin manifests.
|
|
/// Each source carries its trusted MCP routing metadata. `lookup_enabled_tool` must use a
|
|
/// consistent catalog/config snapshot, include app-only tools, and exclude tools disabled by
|
|
/// app policy.
|
|
///
|
|
/// 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
|
|
/// hooks from the bundled cleanup allowlist and the remote Browser plugin.
|
|
pub fn executor_plugin_hook_sources<'a>(
|
|
snapshot: &ExecutorCapabilityDiscoverySnapshot,
|
|
lookup_enabled_tool: impl Fn(&str, &str) -> Option<&'a ToolInfo>,
|
|
) -> 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(),
|
|
mcp_environment_id: None,
|
|
mcp_metadata: None,
|
|
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(|mut source| {
|
|
let plugin_id = source.plugin_id.as_key();
|
|
for (event, groups) in source.hooks.matcher_groups_mut() {
|
|
groups.retain_mut(|group| {
|
|
group.hooks.retain(|handler| {
|
|
let app_connector_id = match handler {
|
|
HookHandlerConfig::McpTool { server, tool, .. }
|
|
if server == CODEX_APPS_MCP_SERVER_NAME =>
|
|
{
|
|
lookup_enabled_tool(server, tool)
|
|
.and_then(|info| info.connector_id.as_deref())
|
|
}
|
|
_ => None,
|
|
};
|
|
is_allowlisted_bundled_cleanup_hook(
|
|
&plugin_id,
|
|
event,
|
|
group.matcher.as_deref(),
|
|
handler,
|
|
app_connector_id,
|
|
)
|
|
});
|
|
!group.hooks.is_empty()
|
|
});
|
|
}
|
|
(!source.hooks.is_empty()).then_some(source)
|
|
})
|
|
.filter_map(|source| resolve_mcp_routing(source, &lookup_enabled_tool))
|
|
.collect()
|
|
}
|
|
|
|
/// Resolves routing for an admitted MCP hook source.
|
|
fn resolve_mcp_routing<'a>(
|
|
mut source: ExecutorPluginHookSource,
|
|
lookup_enabled_tool: &impl Fn(&str, &str) -> Option<&'a ToolInfo>,
|
|
) -> Option<ExecutorPluginHookSource> {
|
|
let HookHandlerConfig::McpTool { server, tool, .. } = source
|
|
.hooks
|
|
.matcher_groups_mut()
|
|
.into_iter()
|
|
.find_map(|(_, groups)| groups.first())?
|
|
.hooks
|
|
.first()?
|
|
else {
|
|
return None;
|
|
};
|
|
if server != CODEX_APPS_MCP_SERVER_NAME {
|
|
return Some(source);
|
|
}
|
|
|
|
let tool_info = lookup_enabled_tool(server, tool)?;
|
|
let routing_metadata = tool_info
|
|
.tool
|
|
.meta
|
|
.as_ref()?
|
|
.get(MCP_TOOL_CODEX_APPS_META_KEY)?
|
|
.as_object()?;
|
|
routing_metadata.get("resource_uri")?.as_str()?;
|
|
source.mcp_environment_id = Some(DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string());
|
|
source.mcp_metadata = Some(Map::from_iter([(
|
|
MCP_TOOL_CODEX_APPS_META_KEY.to_string(),
|
|
Value::Object(routing_metadata.clone()),
|
|
)]));
|
|
Some(source)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[path = "executor_hooks_tests.rs"]
|
|
mod tests;
|