diff --git a/codex-rs/core/src/session/tests.rs b/codex-rs/core/src/session/tests.rs index 07ad83c584..e7099ca19f 100644 --- a/codex-rs/core/src/session/tests.rs +++ b/codex-rs/core/src/session/tests.rs @@ -7012,7 +7012,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(), }; @@ -7065,7 +7067,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(), }; @@ -9092,7 +9096,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 c746cd088e..d3c50c58bc 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; @@ -101,6 +102,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 6fdad216fa..988d69d75f 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); 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(&[