Files
codex/codex-rs/plugin/src/bundled_hooks.rs
T
teddywyly-oai c7c824dce4 Treat bundled cleanup hooks as built-ins (#42110)
## 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
2026-09-01 18:02:27 +00:00

153 lines
4.6 KiB
Rust

//! Temporary cleanup-hook allowlist shared by local and executor plugin discovery.
//! This narrowly authorizes known MCP cleanup calls; it does not verify plugin signatures.
use codex_config::HookHandlerConfig;
use codex_protocol::protocol::HookEventName;
struct BundledHook {
plugin_id: &'static str,
events: &'static [HookEventName],
target: BundledHookTarget,
}
enum BundledHookTarget {
McpServer {
server: &'static str,
tool: &'static str,
},
App {
server: &'static str,
connector_id: &'static str,
tool: &'static str,
},
}
// Keep unsigned plugin exceptions together so they can be removed as signing lands.
const ALLOWLISTED_BUNDLED_HOOKS: &[BundledHook] = &[
BundledHook {
plugin_id: "browser@openai-bundled",
events: &[
HookEventName::Stop,
HookEventName::Interrupt,
HookEventName::SubagentStop,
],
target: BundledHookTarget::McpServer {
server: "node_repl",
tool: "turn_ended",
},
},
BundledHook {
plugin_id: "chrome@openai-bundled",
events: &[
HookEventName::Stop,
HookEventName::Interrupt,
HookEventName::SubagentStop,
],
target: BundledHookTarget::McpServer {
server: "node_repl",
tool: "turn_ended",
},
},
BundledHook {
plugin_id: "chrome-dev@openai-bundled",
events: &[
HookEventName::Stop,
HookEventName::Interrupt,
HookEventName::SubagentStop,
],
target: BundledHookTarget::McpServer {
server: "node_repl",
tool: "turn_ended",
},
},
BundledHook {
plugin_id: "chrome-internal@openai-bundled",
events: &[
HookEventName::Stop,
HookEventName::Interrupt,
HookEventName::SubagentStop,
],
target: BundledHookTarget::McpServer {
server: "node_repl",
tool: "turn_ended",
},
},
BundledHook {
plugin_id: "computer-use@openai-bundled",
events: &[
HookEventName::Stop,
HookEventName::Interrupt,
HookEventName::SubagentStop,
],
target: BundledHookTarget::McpServer {
server: "node_repl",
tool: "turn_ended",
},
},
BundledHook {
plugin_id: "unified-computer-use@openai-bundled",
events: &[
HookEventName::Stop,
HookEventName::Interrupt,
HookEventName::SubagentStop,
],
target: BundledHookTarget::McpServer {
server: "cua_repl",
tool: "turn_ended",
},
},
BundledHook {
plugin_id: "browser@openai-curated-remote",
events: &[HookEventName::Stop, HookEventName::SubagentStop],
target: BundledHookTarget::App {
server: "codex_apps",
connector_id: "connector_openai_browser",
tool: "browser.turn_ended",
},
},
];
/// Matches a temporary unsigned cleanup exception. App targets additionally require the
/// connector identity for this handler's server and tool from the caller's enabled tool
/// catalog; callers without that catalog must pass `None`.
pub fn is_allowlisted_bundled_cleanup_hook(
plugin_id: &str,
event: HookEventName,
matcher: Option<&str>,
handler: &HookHandlerConfig,
app_connector_id: Option<&str>,
) -> bool {
let HookHandlerConfig::McpTool {
server,
tool,
input,
..
} = handler
else {
return false;
};
matcher.is_none()
&& ALLOWLISTED_BUNDLED_HOOKS.iter().any(|hook| {
hook.plugin_id == plugin_id
&& hook.events.contains(&event)
&& match hook.target {
BundledHookTarget::McpServer {
server: expected_server,
tool: expected_tool,
} => server == expected_server && tool == expected_tool,
BundledHookTarget::App {
server: expected_server,
connector_id,
tool: expected_tool,
} => {
// Raw Apps tool names can collide; require the registered connector.
server == expected_server
&& tool == expected_tool
&& input.is_empty()
&& app_connector_id == Some(connector_id)
}
}
})
}