mirror of
https://github.com/openai/codex.git
synced 2026-09-20 12:47:38 +00:00
Allow browser app cleanup hooks on interrupt (#46029)
## Why The cleanup-hook allowlist for `browser@openai-curated-remote` accepted only `Stop` and `SubagentStop`, leaving its cleanup hook unavailable when a turn was interrupted. ## What changed Allow `Interrupt` to invoke `browser.turn_ended` through `codex_apps`, retaining the registered browser connector and empty-input requirements. ## Testing Extend hook metadata tests to cover interrupts, rejecting mismatched connectors and manifest-provided arguments. Extend executor integration coverage to interrupt an active turn and verify that browser app and computer-use cleanup calls use their separate MCP routes. GitOrigin-RevId: 355801f9ca3086bae80d1e623d0be7e1772a39e1
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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" {
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user