From 9ea38136b00937a87c94cbcaab872342b2adb6d9 Mon Sep 17 00:00:00 2001 From: Chris Bookholt Date: Thu, 14 May 2026 06:28:34 -0700 Subject: [PATCH 1/2] [codex] treat PowerShell stop-parsing forms as unsupported (#22643) ## Summary - Treat PowerShell stop-parsing token forms as unsupported in the AST-backed command flattener. - Add focused regressions at the parser layer and Windows command-safety layer. ## Why The command-safety parser lowers PowerShell AST elements into argv-like words. Stop-parsing syntax preserves a native-command argument shape that this lowering does not model, so these forms should stay on the conservative unsupported path. ## Validation - `cargo fmt --manifest-path codex-rs/Cargo.toml --all --check` - `cargo test --manifest-path codex-rs/Cargo.toml -p codex-shell-command` --- .../src/command_safety/powershell_parser.ps1 | 9 +++++++++ .../src/command_safety/powershell_parser.rs | 14 ++++++++++++++ .../src/command_safety/windows_safe_commands.rs | 10 ++++++++++ 3 files changed, 33 insertions(+) diff --git a/codex-rs/shell-command/src/command_safety/powershell_parser.ps1 b/codex-rs/shell-command/src/command_safety/powershell_parser.ps1 index 9f19f172eb..de696c88bd 100644 --- a/codex-rs/shell-command/src/command_safety/powershell_parser.ps1 +++ b/codex-rs/shell-command/src/command_safety/powershell_parser.ps1 @@ -42,6 +42,15 @@ function Invoke-ParseRequest { return @{ id = $RequestId; status = 'parse_errors' } } + # PowerShell's stop-parsing marker hands the remaining source text to native + # commands with runtime argument handling that does not match the AST shape we + # flatten below. Keep that form out of the argv-like lowering path entirely. + foreach ($token in $tokens) { + if ($token.Text -eq '--%') { + return @{ id = $RequestId; status = 'unsupported' } + } + } + # Only accept AST shapes we can flatten into a list of argv-like command words. # Anything more dynamic than that becomes "unsupported" instead of being guessed at. $commands = [System.Collections.ArrayList]::new() diff --git a/codex-rs/shell-command/src/command_safety/powershell_parser.rs b/codex-rs/shell-command/src/command_safety/powershell_parser.rs index 580f177f9f..ddcbdf5727 100644 --- a/codex-rs/shell-command/src/command_safety/powershell_parser.rs +++ b/codex-rs/shell-command/src/command_safety/powershell_parser.rs @@ -296,4 +296,18 @@ mod tests { ]), ); } + + #[test] + fn parser_process_rejects_stop_parsing_forms() { + let Some(powershell) = try_find_powershell_executable_blocking() else { + return; + }; + let powershell = powershell.as_path().to_str().unwrap(); + let mut parser = PowershellParserProcess::spawn(powershell).unwrap(); + + let parsed = parser + .parse("git log --% HEAD --output=codex_poc.txt") + .unwrap(); + assert_eq!(parsed, PowershellParseOutcome::Unsupported); + } } diff --git a/codex-rs/shell-command/src/command_safety/windows_safe_commands.rs b/codex-rs/shell-command/src/command_safety/windows_safe_commands.rs index 8ef3f8e8f9..a8dcef6de6 100644 --- a/codex-rs/shell-command/src/command_safety/windows_safe_commands.rs +++ b/codex-rs/shell-command/src/command_safety/windows_safe_commands.rs @@ -402,6 +402,16 @@ mod tests { ); } + #[test] + fn rejects_stop_parsing_git_forms() { + assert!(!is_safe_command_windows(&vec_str(&[ + "powershell.exe", + "-NoProfile", + "-Command", + "git log --% HEAD --output=codex_poc.txt", + ]))); + } + #[test] fn rejects_powershell_commands_with_side_effects() { assert!(!is_safe_command_windows(&vec_str(&[ From 12bfb57139f169c1a1822d57a6fdb2a3d08e0c53 Mon Sep 17 00:00:00 2001 From: jif-oai Date: Thu, 14 May 2026 16:00:06 +0200 Subject: [PATCH 2/2] Fix turn extension data task plumbing (#22646) ## Summary - carry the per-turn extension data through RunningTask so abort handling can rebuild SessionTaskContext - update stale test ExtensionData::new() callsites to pass the turn id ## Testing - Not run after PR branch creation; CI will cover. --- codex-rs/core/src/session/tests.rs | 10 +++++++--- codex-rs/core/src/session/turn_tests.rs | 2 +- codex-rs/core/src/state/turn.rs | 2 ++ codex-rs/core/src/stream_events_utils_tests.rs | 10 +++++----- codex-rs/core/src/tasks/mod.rs | 2 ++ 5 files changed, 17 insertions(+), 9 deletions(-) diff --git a/codex-rs/core/src/session/tests.rs b/codex-rs/core/src/session/tests.rs index e06dba9b68..176ea9413f 100644 --- a/codex-rs/core/src/session/tests.rs +++ b/codex-rs/core/src/session/tests.rs @@ -7009,7 +7009,9 @@ async fn handle_output_item_done_records_image_save_history_message() { let mut ctx = HandleOutputCtx { sess: Arc::clone(&session), turn_context: Arc::clone(&turn_context), - turn_store: Arc::new(codex_extension_api::ExtensionData::new()), + turn_store: Arc::new(codex_extension_api::ExtensionData::new( + turn_context.sub_id.clone(), + )), tool_runtime: test_tool_runtime(Arc::clone(&session), Arc::clone(&turn_context)), cancellation_token: CancellationToken::new(), }; @@ -7062,7 +7064,9 @@ async fn handle_output_item_done_skips_image_save_message_when_save_fails() { let mut ctx = HandleOutputCtx { sess: Arc::clone(&session), turn_context: Arc::clone(&turn_context), - turn_store: Arc::new(codex_extension_api::ExtensionData::new()), + turn_store: Arc::new(codex_extension_api::ExtensionData::new( + turn_context.sub_id.clone(), + )), tool_runtime: test_tool_runtime(Arc::clone(&session), Arc::clone(&turn_context)), cancellation_token: CancellationToken::new(), }; @@ -8950,7 +8954,7 @@ async fn tool_calls_reopen_mailbox_delivery_for_current_turn() { let mut ctx = HandleOutputCtx { sess: Arc::clone(&sess), turn_context: Arc::clone(&tc), - turn_store: Arc::new(codex_extension_api::ExtensionData::new()), + turn_store: Arc::new(codex_extension_api::ExtensionData::new(tc.sub_id.clone())), tool_runtime: test_tool_runtime(Arc::clone(&sess), Arc::clone(&tc)), cancellation_token: CancellationToken::new(), }; diff --git a/codex-rs/core/src/session/turn_tests.rs b/codex-rs/core/src/session/turn_tests.rs index 4f19474d38..f1cf86b46c 100644 --- a/codex-rs/core/src/session/turn_tests.rs +++ b/codex-rs/core/src/session/turn_tests.rs @@ -43,7 +43,7 @@ async fn plan_mode_uses_contributed_turn_item_for_last_agent_message() { let mut builder = codex_extension_api::ExtensionRegistryBuilder::new(); builder.turn_item_contributor(Arc::new(RewriteAgentMessageContributor)); session.services.extensions = Arc::new(builder.build()); - let turn_store = ExtensionData::new(); + let turn_store = ExtensionData::new(turn_context.sub_id.clone()); let mut state = PlanModeStreamState::new(&turn_context.sub_id); let mut last_agent_message = None; let item = assistant_output_text("original assistant text"); diff --git a/codex-rs/core/src/state/turn.rs b/codex-rs/core/src/state/turn.rs index 0dd0c09379..70aae74e87 100644 --- a/codex-rs/core/src/state/turn.rs +++ b/codex-rs/core/src/state/turn.rs @@ -9,6 +9,7 @@ use tokio::sync::Notify; use tokio_util::sync::CancellationToken; use tokio_util::task::AbortOnDropHandle; +use codex_extension_api::ExtensionData; use codex_protocol::dynamic_tools::DynamicToolResponse; use codex_protocol::models::ResponseInputItem; use codex_protocol::request_permissions::RequestPermissionProfile; @@ -75,6 +76,7 @@ pub(crate) struct RunningTask { pub(crate) cancellation_token: CancellationToken, pub(crate) handle: AbortOnDropHandle<()>, pub(crate) turn_context: Arc, + pub(crate) turn_extension_data: Arc, // Timer recorded when the task drops to capture the full turn duration. pub(crate) _timer: Option, } diff --git a/codex-rs/core/src/stream_events_utils_tests.rs b/codex-rs/core/src/stream_events_utils_tests.rs index 46c7ffed19..2d095906ed 100644 --- a/codex-rs/core/src/stream_events_utils_tests.rs +++ b/codex-rs/core/src/stream_events_utils_tests.rs @@ -214,7 +214,7 @@ async fn handle_non_tool_response_item_runs_turn_item_contributors_only_when_req let mut builder = codex_extension_api::ExtensionRegistryBuilder::new(); builder.turn_item_contributor(Arc::new(TestTurnItemContributor)); session.services.extensions = Arc::new(builder.build()); - let turn_store = ExtensionData::new(); + let turn_store = ExtensionData::new(turn_context.sub_id.clone()); let item = assistant_output_text( "helloignored by memory parser world", ); @@ -288,8 +288,8 @@ async fn handle_output_item_done_returns_contributed_last_agent_message() { let item = assistant_output_text("original assistant text"); let mut ctx = HandleOutputCtx { sess: session, - turn_context, - turn_store: Arc::new(ExtensionData::new()), + turn_context: Arc::clone(&turn_context), + turn_store: Arc::new(ExtensionData::new(turn_context.sub_id.clone())), tool_runtime, cancellation_token: CancellationToken::new(), }; @@ -310,7 +310,7 @@ async fn finalized_turn_item_defers_mailbox_for_contributed_visible_text() { let mut builder = codex_extension_api::ExtensionRegistryBuilder::new(); builder.turn_item_contributor(Arc::new(RewriteAgentMessageContributor)); session.services.extensions = Arc::new(builder.build()); - let turn_store = ExtensionData::new(); + let turn_store = ExtensionData::new(turn_context.sub_id.clone()); let item = assistant_output_text("hidden only"); let finalized = finalize_non_tool_response_item( @@ -336,7 +336,7 @@ async fn finalized_turn_item_keeps_mailbox_open_for_commentary_text() { let mut builder = codex_extension_api::ExtensionRegistryBuilder::new(); builder.turn_item_contributor(Arc::new(RewriteAgentMessageContributor)); session.services.extensions = Arc::new(builder.build()); - let turn_store = ExtensionData::new(); + let turn_store = ExtensionData::new(turn_context.sub_id.clone()); let item = assistant_output_text_with_phase("still working", Some(MessagePhase::Commentary)); let finalized = finalize_non_tool_response_item( diff --git a/codex-rs/core/src/tasks/mod.rs b/codex-rs/core/src/tasks/mod.rs index 5e6b931500..58ead0fd25 100644 --- a/codex-rs/core/src/tasks/mod.rs +++ b/codex-rs/core/src/tasks/mod.rs @@ -367,6 +367,7 @@ impl Session { } self.emit_turn_start_lifecycle(turn_context.extension_data.as_ref()); + let turn_extension_data = Arc::clone(&turn_context.extension_data); let mut active = self.active_turn.lock().await; let turn = active.get_or_insert_with(ActiveTurn::default); debug_assert!(turn.tasks.is_empty()); @@ -439,6 +440,7 @@ impl Session { task, cancellation_token, turn_context: Arc::clone(&turn_context), + turn_extension_data, _timer: timer, }; turn.add_task(running_task);