diff --git a/codex-rs/core-plugins/src/executor_hooks_tests.rs b/codex-rs/core-plugins/src/executor_hooks_tests.rs index d3e031dff7..b0e5f8c35a 100644 --- a/codex-rs/core-plugins/src/executor_hooks_tests.rs +++ b/codex-rs/core-plugins/src/executor_hooks_tests.rs @@ -292,6 +292,30 @@ fn resolves_apps_hook_metadata_from_the_registered_connector() { routing.clone(), true, ), + ( + "registered interrupted app", + "Interrupt", + Some("connector_openai_browser"), + json!({}), + routing.clone(), + true, + ), + ( + "colliding interrupt tool name", + "Interrupt", + Some("connector_other_browser"), + json!({}), + routing.clone(), + false, + ), + ( + "interrupt manifest arguments", + "Interrupt", + Some("connector_openai_browser"), + json!({ "untrusted": "manifest-provided input" }), + routing.clone(), + false, + ), ( "colliding subagent tool name", "SubagentStop", diff --git a/codex-rs/core/tests/suite/hooks_executor.rs b/codex-rs/core/tests/suite/hooks_executor.rs index 8dd2ef42c3..772426c31b 100644 --- a/codex-rs/core/tests/suite/hooks_executor.rs +++ b/codex-rs/core/tests/suite/hooks_executor.rs @@ -415,6 +415,7 @@ async fn executor_stop_hook_rejects_mismatched_environment() -> Result<()> { } #[test_case("Stop", "", "", 1; "enabled")] +#[test_case("Interrupt", "", "", 1; "interrupt_enabled")] #[test_case("SubagentStop", "", "", 1; "subagent_enabled")] #[test_case( "Stop", @@ -431,7 +432,7 @@ async fn executor_stop_hook_rejects_mismatched_environment() -> Result<()> { "managed_disabled" )] #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn executor_browser_and_computer_use_stop_hooks_use_separate_mcp_routes( +async fn executor_browser_and_computer_use_cleanup_hooks_use_separate_mcp_routes( hook_event: &'static str, user_config: &'static str, requirements: &'static str, @@ -482,6 +483,12 @@ async fn executor_browser_and_computer_use_stop_hooks_use_separate_mcp_routes( "tool": "browser.turn_ended", "input": {}, }] }], + "Interrupt": [{ "hooks": [{ + "type": "mcp_tool", + "server": "codex_apps", + "tool": "browser.turn_ended", + "input": {}, + }] }], "SubagentStop": [{ "hooks": [{ "type": "mcp_tool", "server": "codex_apps", @@ -503,7 +510,11 @@ async fn executor_browser_and_computer_use_stop_hooks_use_separate_mcp_routes( CloudConfigBundleFixture::loader_with_enterprise_requirement(requirements), ), &plugins, - vec![completed_turn_response("browser-turn")], + vec![if hook_event == "Interrupt" { + completed_turn_response("browser-turn").set_delay(Duration::from_secs(60)) + } else { + completed_turn_response("browser-turn") + }], ) .await?; if hook_event == "SubagentStop" { @@ -540,7 +551,30 @@ async fn executor_browser_and_computer_use_stop_hooks_use_separate_mcp_routes( ) .await?; } - fixture.test.submit_text_turn("finish browsing").await?; + if hook_event == "Interrupt" { + fixture + .test + .codex + .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text { + text: "interrupt browsing".to_string(), + text_elements: Vec::new(), + }])) + .await?; + tokio::time::timeout(Duration::from_secs(10), async { + while fixture.responses.requests().is_empty() { + tokio::task::yield_now().await; + } + }) + .await + .context("interrupted turn should reach the model request")?; + fixture.test.codex.submit(Op::Interrupt).await?; + wait_for_event(&fixture.test.codex, |event| { + matches!(event, EventMsg::TurnAborted(_)) + }) + .await; + } else { + fixture.test.submit_text_turn("finish browsing").await?; + } fixture.wait_for_hook_call().await?; let node_calls = fixture.calls().await?; let expected_node_tools = if hook_event == "SubagentStop" { diff --git a/codex-rs/plugin/src/bundled_hooks.rs b/codex-rs/plugin/src/bundled_hooks.rs index a39644e427..47a4eeef28 100644 --- a/codex-rs/plugin/src/bundled_hooks.rs +++ b/codex-rs/plugin/src/bundled_hooks.rs @@ -98,7 +98,11 @@ const ALLOWLISTED_BUNDLED_HOOKS: &[BundledHook] = &[ }, BundledHook { plugin_id: "browser@openai-curated-remote", - events: &[HookEventName::Stop, HookEventName::SubagentStop], + events: &[ + HookEventName::Stop, + HookEventName::Interrupt, + HookEventName::SubagentStop, + ], target: BundledHookTarget::App { server: "codex_apps", connector_id: "connector_openai_browser",