From ca23b0da5b75e8f5638a95cfc3f3b6a8d12e9de7 Mon Sep 17 00:00:00 2001 From: zerone0x Date: Wed, 21 Jan 2026 06:53:14 +0800 Subject: [PATCH 1/6] fix(cli): add execute permission to bin/codex.js (#9532) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Fixes #9520 The `bin/codex.js` file was missing execute permissions (`644` instead of `755`), causing the `codex` command to fail after npm global installation. ## Changes - Added execute permission (`+x`) to `codex-cli/bin/codex.js` ## Verification After this fix, npm tarballs will include the correct file permissions: ```bash # Before: -rw-r--r-- (644) # After: -rwxr-xr-x (755) ``` --- 🤖 Generated with Claude Code Co-authored-by: Claude --- codex-cli/bin/codex.js | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 codex-cli/bin/codex.js diff --git a/codex-cli/bin/codex.js b/codex-cli/bin/codex.js old mode 100644 new mode 100755 From 64678f895ac2ca0e625f11259078a16a21091ad7 Mon Sep 17 00:00:00 2001 From: charley-oai Date: Tue, 20 Jan 2026 14:54:49 -0800 Subject: [PATCH 2/6] Improve UI spacing for queued messages (#9162) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Despite good spacing between queued messages and assistant message text: Screenshot 2026-01-12 at 4 54 50 PM Codex has confusing spacing between queued messages and shimmering status text (making the queued message seem like a sub-item of the shimmering status text) Screenshot 2026-01-12 at 4 54 18 PM This PR changes the spacing between the queued message(s) and shimmering status text to make it less confusing: Screenshot 2026-01-13 at 11 20 36 AM While working on the status/queued spacing change, we noticed two paste‑burst tests were timing‑sensitive and could fail on slower CI. We added a small test‑only helper to keep the paste‑burst state active and refreshed during these tests. This removes dependence on tight timing and makes the tests deterministic without affecting runtime behavior. --- codex-rs/tui/src/bottom_pane/mod.rs | 65 +++++++++++++++++-- ...s_visible_when_status_hidden_snapshot.snap | 1 - ...__status_and_queued_messages_snapshot.snap | 2 +- ...tom_pane__tests__status_only_snapshot.snap | 10 +++ ..._details_and_queued_messages_snapshot.snap | 14 ++++ ...i__chatwidget__tests__chatwidget_tall.snap | 3 +- ..._review_queues_user_messages_snapshot.snap | 3 +- codex-rs/tui2/src/bottom_pane/mod.rs | 61 ++++++++++++++++- ...s_visible_when_status_hidden_snapshot.snap | 1 - ...__status_and_queued_messages_snapshot.snap | 2 +- ...tom_pane__tests__status_only_snapshot.snap | 10 +++ ..._details_and_queued_messages_snapshot.snap | 14 ++++ ...2__chatwidget__tests__chatwidget_tall.snap | 3 +- 13 files changed, 176 insertions(+), 13 deletions(-) create mode 100644 codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__tests__status_only_snapshot.snap create mode 100644 codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__tests__status_with_details_and_queued_messages_snapshot.snap create mode 100644 codex-rs/tui2/src/bottom_pane/snapshots/codex_tui2__bottom_pane__tests__status_only_snapshot.snap create mode 100644 codex-rs/tui2/src/bottom_pane/snapshots/codex_tui2__bottom_pane__tests__status_with_details_and_queued_messages_snapshot.snap diff --git a/codex-rs/tui/src/bottom_pane/mod.rs b/codex-rs/tui/src/bottom_pane/mod.rs index ee23cad334..3057544197 100644 --- a/codex-rs/tui/src/bottom_pane/mod.rs +++ b/codex-rs/tui/src/bottom_pane/mod.rs @@ -701,11 +701,14 @@ impl BottomPane { if !self.unified_exec_footer.is_empty() { flex.push(0, RenderableItem::Borrowed(&self.unified_exec_footer)); } + let has_queued_messages = !self.queued_user_messages.messages.is_empty(); + let has_status_or_footer = + self.status.is_some() || !self.unified_exec_footer.is_empty(); + if has_queued_messages && has_status_or_footer { + flex.push(0, RenderableItem::Owned("".into())); + } flex.push(1, RenderableItem::Borrowed(&self.queued_user_messages)); - if self.status.is_some() - || !self.unified_exec_footer.is_empty() - || !self.queued_user_messages.messages.is_empty() - { + if !has_queued_messages && has_status_or_footer { flex.push(0, RenderableItem::Owned("".into())); } let mut flex2 = FlexRenderable::new(); @@ -951,6 +954,60 @@ mod tests { ); } + #[test] + fn status_only_snapshot() { + let (tx_raw, _rx) = unbounded_channel::(); + let tx = AppEventSender::new(tx_raw); + let mut pane = BottomPane::new(BottomPaneParams { + app_event_tx: tx, + frame_requester: FrameRequester::test_dummy(), + has_input_focus: true, + enhanced_keys_supported: false, + placeholder_text: "Ask Codex to do anything".to_string(), + disable_paste_burst: false, + animations_enabled: true, + skills: Some(Vec::new()), + }); + + pane.set_task_running(true); + + let width = 48; + let height = pane.desired_height(width); + let area = Rect::new(0, 0, width, height); + assert_snapshot!("status_only_snapshot", render_snapshot(&pane, area)); + } + + #[test] + fn status_with_details_and_queued_messages_snapshot() { + let (tx_raw, _rx) = unbounded_channel::(); + let tx = AppEventSender::new(tx_raw); + let mut pane = BottomPane::new(BottomPaneParams { + app_event_tx: tx, + frame_requester: FrameRequester::test_dummy(), + has_input_focus: true, + enhanced_keys_supported: false, + placeholder_text: "Ask Codex to do anything".to_string(), + disable_paste_burst: false, + animations_enabled: true, + skills: Some(Vec::new()), + }); + + pane.set_task_running(true); + pane.update_status( + "Working".to_string(), + Some("First detail line\nSecond detail line".to_string()), + ); + pane.set_queued_user_messages(vec!["Queued follow-up question".to_string()]); + + let width = 48; + let height = pane.desired_height(width); + let area = Rect::new(0, 0, width, height); + assert_snapshot!( + "status_with_details_and_queued_messages_snapshot", + render_snapshot(&pane, area) + ); + } + #[test] fn queued_messages_visible_when_status_hidden_snapshot() { let (tx_raw, _rx) = unbounded_channel::(); diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__tests__queued_messages_visible_when_status_hidden_snapshot.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__tests__queued_messages_visible_when_status_hidden_snapshot.snap index 123a5eb3a3..5aea415190 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__tests__queued_messages_visible_when_status_hidden_snapshot.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__tests__queued_messages_visible_when_status_hidden_snapshot.snap @@ -5,7 +5,6 @@ expression: "render_snapshot(&pane, area)" ↳ Queued follow-up question ⌥ + ↑ edit - › Ask Codex to do anything 100% context left · ? for shortcuts diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__tests__status_and_queued_messages_snapshot.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__tests__status_and_queued_messages_snapshot.snap index 27df671e4d..e651ec9274 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__tests__status_and_queued_messages_snapshot.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__tests__status_and_queued_messages_snapshot.snap @@ -3,10 +3,10 @@ source: tui/src/bottom_pane/mod.rs expression: "render_snapshot(&pane, area)" --- • Working (0s • esc to interrupt) + ↳ Queued follow-up question ⌥ + ↑ edit - › Ask Codex to do anything 100% context left · ? for shortcuts diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__tests__status_only_snapshot.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__tests__status_only_snapshot.snap new file mode 100644 index 0000000000..79e1e126eb --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__tests__status_only_snapshot.snap @@ -0,0 +1,10 @@ +--- +source: tui/src/bottom_pane/mod.rs +expression: "render_snapshot(&pane, area)" +--- +• Working (0s • esc to interrupt) + + +› Ask Codex to do anything + + 100% context left · ? for shortcuts diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__tests__status_with_details_and_queued_messages_snapshot.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__tests__status_with_details_and_queued_messages_snapshot.snap new file mode 100644 index 0000000000..12090d09e9 --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__tests__status_with_details_and_queued_messages_snapshot.snap @@ -0,0 +1,14 @@ +--- +source: tui/src/bottom_pane/mod.rs +expression: "render_snapshot(&pane, area)" +--- +• Working (0s • esc to interrupt) + └ First detail line + Second detail line + + ↳ Queued follow-up question + ⌥ + ↑ edit + +› Ask Codex to do anything + + 100% context left · ? for shortcuts diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__chatwidget_tall.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__chatwidget_tall.snap index 6d9aa515b1..64361e90f9 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__chatwidget_tall.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__chatwidget_tall.snap @@ -2,7 +2,9 @@ source: tui/src/chatwidget/tests.rs expression: term.backend().vt100().screen().contents() --- + • Working (0s • esc to interrupt) + ↳ Hello, world! 0 ↳ Hello, world! 1 ↳ Hello, world! 2 @@ -21,7 +23,6 @@ expression: term.backend().vt100().screen().contents() ↳ Hello, world! 15 ↳ Hello, world! 16 - › Ask Codex to do anything 100% context left · ? for shortcuts diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__review_queues_user_messages_snapshot.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__review_queues_user_messages_snapshot.snap index 2a7717df7c..1c02350a6d 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__review_queues_user_messages_snapshot.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__review_queues_user_messages_snapshot.snap @@ -1,6 +1,5 @@ --- source: tui/src/chatwidget/tests.rs -assertion_line: 3840 expression: term.backend().vt100().screen().contents() --- @@ -14,10 +13,10 @@ expression: term.backend().vt100().screen().contents() • Working (0s • esc to interrupt) + ↳ Queued while /review is running. ⌥ + ↑ edit - › Ask Codex to do anything 100% context left · ? for shortcuts diff --git a/codex-rs/tui2/src/bottom_pane/mod.rs b/codex-rs/tui2/src/bottom_pane/mod.rs index 83764d55b8..4fa0ae5fe9 100644 --- a/codex-rs/tui2/src/bottom_pane/mod.rs +++ b/codex-rs/tui2/src/bottom_pane/mod.rs @@ -688,8 +688,13 @@ impl BottomPane { if let Some(status) = &self.status { flex.push(0, RenderableItem::Borrowed(status)); } + let has_queued_messages = !self.queued_user_messages.messages.is_empty(); + let has_status = self.status.is_some(); + if has_queued_messages && has_status { + flex.push(0, RenderableItem::Owned("".into())); + } flex.push(1, RenderableItem::Borrowed(&self.queued_user_messages)); - if self.status.is_some() || !self.queued_user_messages.messages.is_empty() { + if !has_queued_messages && has_status { flex.push(0, RenderableItem::Owned("".into())); } let mut flex2 = FlexRenderable::new(); @@ -931,6 +936,60 @@ mod tests { ); } + #[test] + fn status_only_snapshot() { + let (tx_raw, _rx) = unbounded_channel::(); + let tx = AppEventSender::new(tx_raw); + let mut pane = BottomPane::new(BottomPaneParams { + app_event_tx: tx, + frame_requester: FrameRequester::test_dummy(), + has_input_focus: true, + enhanced_keys_supported: false, + placeholder_text: "Ask Codex to do anything".to_string(), + disable_paste_burst: false, + animations_enabled: true, + skills: Some(Vec::new()), + }); + + pane.set_task_running(true); + + let width = 48; + let height = pane.desired_height(width); + let area = Rect::new(0, 0, width, height); + assert_snapshot!("status_only_snapshot", render_snapshot(&pane, area)); + } + + #[test] + fn status_with_details_and_queued_messages_snapshot() { + let (tx_raw, _rx) = unbounded_channel::(); + let tx = AppEventSender::new(tx_raw); + let mut pane = BottomPane::new(BottomPaneParams { + app_event_tx: tx, + frame_requester: FrameRequester::test_dummy(), + has_input_focus: true, + enhanced_keys_supported: false, + placeholder_text: "Ask Codex to do anything".to_string(), + disable_paste_burst: false, + animations_enabled: true, + skills: Some(Vec::new()), + }); + + pane.set_task_running(true); + pane.update_status( + "Working".to_string(), + Some("First detail line\nSecond detail line".to_string()), + ); + pane.set_queued_user_messages(vec!["Queued follow-up question".to_string()]); + + let width = 48; + let height = pane.desired_height(width); + let area = Rect::new(0, 0, width, height); + assert_snapshot!( + "status_with_details_and_queued_messages_snapshot", + render_snapshot(&pane, area) + ); + } + #[test] fn queued_messages_visible_when_status_hidden_snapshot() { let (tx_raw, _rx) = unbounded_channel::(); diff --git a/codex-rs/tui2/src/bottom_pane/snapshots/codex_tui2__bottom_pane__tests__queued_messages_visible_when_status_hidden_snapshot.snap b/codex-rs/tui2/src/bottom_pane/snapshots/codex_tui2__bottom_pane__tests__queued_messages_visible_when_status_hidden_snapshot.snap index 71504561db..efc74d42ba 100644 --- a/codex-rs/tui2/src/bottom_pane/snapshots/codex_tui2__bottom_pane__tests__queued_messages_visible_when_status_hidden_snapshot.snap +++ b/codex-rs/tui2/src/bottom_pane/snapshots/codex_tui2__bottom_pane__tests__queued_messages_visible_when_status_hidden_snapshot.snap @@ -5,7 +5,6 @@ expression: "render_snapshot(&pane, area)" ↳ Queued follow-up question ⌥ + ↑ edit - › Ask Codex to do anything 100% context left · ? for shortcuts diff --git a/codex-rs/tui2/src/bottom_pane/snapshots/codex_tui2__bottom_pane__tests__status_and_queued_messages_snapshot.snap b/codex-rs/tui2/src/bottom_pane/snapshots/codex_tui2__bottom_pane__tests__status_and_queued_messages_snapshot.snap index 6ac4296833..cdea8a17d8 100644 --- a/codex-rs/tui2/src/bottom_pane/snapshots/codex_tui2__bottom_pane__tests__status_and_queued_messages_snapshot.snap +++ b/codex-rs/tui2/src/bottom_pane/snapshots/codex_tui2__bottom_pane__tests__status_and_queued_messages_snapshot.snap @@ -3,10 +3,10 @@ source: tui2/src/bottom_pane/mod.rs expression: "render_snapshot(&pane, area)" --- • Working (0s • esc to interrupt) + ↳ Queued follow-up question ⌥ + ↑ edit - › Ask Codex to do anything 100% context left · ? for shortcuts diff --git a/codex-rs/tui2/src/bottom_pane/snapshots/codex_tui2__bottom_pane__tests__status_only_snapshot.snap b/codex-rs/tui2/src/bottom_pane/snapshots/codex_tui2__bottom_pane__tests__status_only_snapshot.snap new file mode 100644 index 0000000000..3b53b4d862 --- /dev/null +++ b/codex-rs/tui2/src/bottom_pane/snapshots/codex_tui2__bottom_pane__tests__status_only_snapshot.snap @@ -0,0 +1,10 @@ +--- +source: tui2/src/bottom_pane/mod.rs +expression: "render_snapshot(&pane, area)" +--- +• Working (0s • esc to interrupt) + + +› Ask Codex to do anything + + 100% context left · ? for shortcuts diff --git a/codex-rs/tui2/src/bottom_pane/snapshots/codex_tui2__bottom_pane__tests__status_with_details_and_queued_messages_snapshot.snap b/codex-rs/tui2/src/bottom_pane/snapshots/codex_tui2__bottom_pane__tests__status_with_details_and_queued_messages_snapshot.snap new file mode 100644 index 0000000000..51236bb69b --- /dev/null +++ b/codex-rs/tui2/src/bottom_pane/snapshots/codex_tui2__bottom_pane__tests__status_with_details_and_queued_messages_snapshot.snap @@ -0,0 +1,14 @@ +--- +source: tui2/src/bottom_pane/mod.rs +expression: "render_snapshot(&pane, area)" +--- +• Working (0s • esc to interrupt) + └ First detail line + Second detail line + + ↳ Queued follow-up question + ⌥ + ↑ edit + +› Ask Codex to do anything + + 100% context left · ? for shortcuts diff --git a/codex-rs/tui2/src/chatwidget/snapshots/codex_tui2__chatwidget__tests__chatwidget_tall.snap b/codex-rs/tui2/src/chatwidget/snapshots/codex_tui2__chatwidget__tests__chatwidget_tall.snap index 3cc0b593d4..57a590c46c 100644 --- a/codex-rs/tui2/src/chatwidget/snapshots/codex_tui2__chatwidget__tests__chatwidget_tall.snap +++ b/codex-rs/tui2/src/chatwidget/snapshots/codex_tui2__chatwidget__tests__chatwidget_tall.snap @@ -2,7 +2,9 @@ source: tui2/src/chatwidget/tests.rs expression: term.backend().vt100().screen().contents() --- + • Working (0s • esc to interrupt) + ↳ Hello, world! 0 ↳ Hello, world! 1 ↳ Hello, world! 2 @@ -21,7 +23,6 @@ expression: term.backend().vt100().screen().contents() ↳ Hello, world! 15 ↳ Hello, world! 16 - › Ask Codex to do anything 100% context left · ? for shortcuts From de4980d2ac3a0653ba6b952e74d8e31eb6d4943d Mon Sep 17 00:00:00 2001 From: Ahmed Ibrahim Date: Tue, 20 Jan 2026 15:17:22 -0800 Subject: [PATCH 3/6] Enable remote models (#9554) --- codex-rs/core/src/features.rs | 2 +- codex-rs/core/tests/suite/cli_stream.rs | 3 +- codex-rs/core/tests/suite/client.rs | 2 +- .../core/tests/suite/compact_resume_fork.rs | 5 + codex-rs/core/tests/suite/list_models.rs | 150 +++++++++++++----- ...twidget__tests__model_selection_popup.snap | 10 +- ...twidget__tests__model_selection_popup.snap | 10 +- 7 files changed, 128 insertions(+), 54 deletions(-) diff --git a/codex-rs/core/src/features.rs b/codex-rs/core/src/features.rs index 0b1563bd5e..94610f0b75 100644 --- a/codex-rs/core/src/features.rs +++ b/codex-rs/core/src/features.rs @@ -405,7 +405,7 @@ pub const FEATURES: &[FeatureSpec] = &[ id: Feature::RemoteModels, key: "remote_models", stage: Stage::Beta, - default_enabled: false, + default_enabled: true, }, FeatureSpec { id: Feature::PowershellUtf8, diff --git a/codex-rs/core/tests/suite/cli_stream.rs b/codex-rs/core/tests/suite/cli_stream.rs index 9108f263f9..12be98723c 100644 --- a/codex-rs/core/tests/suite/cli_stream.rs +++ b/codex-rs/core/tests/suite/cli_stream.rs @@ -1,5 +1,6 @@ use assert_cmd::Command as AssertCommand; use codex_core::RolloutRecorder; +use codex_core::auth::CODEX_API_KEY_ENV_VAR; use codex_core::protocol::GitInfo; use codex_utils_cargo_bin::find_resource; use core_test_support::fs_wait; @@ -239,7 +240,7 @@ async fn integration_creates_and_checks_session_file() -> anyhow::Result<()> { .arg(&repo_root) .arg(&prompt); cmd.env("CODEX_HOME", home.path()) - .env("OPENAI_API_KEY", "dummy") + .env(CODEX_API_KEY_ENV_VAR, "dummy") .env("CODEX_RS_SSE_FIXTURE", &fixture) // Required for CLI arg parsing even though fixture short-circuits network usage. .env("OPENAI_BASE_URL", "http://unused.local"); diff --git a/codex-rs/core/tests/suite/client.rs b/codex-rs/core/tests/suite/client.rs index cfae930d95..1b43f7932f 100644 --- a/codex-rs/core/tests/suite/client.rs +++ b/codex-rs/core/tests/suite/client.rs @@ -826,7 +826,7 @@ async fn includes_no_effort_in_request() -> anyhow::Result<()> { .get("reasoning") .and_then(|t| t.get("effort")) .and_then(|v| v.as_str()), - None + Some("medium") ); Ok(()) diff --git a/codex-rs/core/tests/suite/compact_resume_fork.rs b/codex-rs/core/tests/suite/compact_resume_fork.rs index e054908aec..b7ce49abbe 100644 --- a/codex-rs/core/tests/suite/compact_resume_fork.rs +++ b/codex-rs/core/tests/suite/compact_resume_fork.rs @@ -278,6 +278,7 @@ async fn compact_resume_and_fork_preserve_model_history_view() { "tool_choice": "auto", "parallel_tool_calls": false, "reasoning": { + "effort": "medium", "summary": "auto" }, "store": false, @@ -348,6 +349,7 @@ async fn compact_resume_and_fork_preserve_model_history_view() { "tool_choice": "auto", "parallel_tool_calls": false, "reasoning": { + "effort": "medium", "summary": "auto" }, "store": false, @@ -409,6 +411,7 @@ async fn compact_resume_and_fork_preserve_model_history_view() { "tool_choice": "auto", "parallel_tool_calls": false, "reasoning": { + "effort": "medium", "summary": "auto" }, "store": false, @@ -511,6 +514,7 @@ async fn compact_resume_and_fork_preserve_model_history_view() { "tool_choice": "auto", "parallel_tool_calls": false, "reasoning": { + "effort": "medium", "summary": "auto" }, "store": false, @@ -634,6 +638,7 @@ async fn compact_resume_and_fork_preserve_model_history_view() { "tool_choice": "auto", "parallel_tool_calls": false, "reasoning": { + "effort": "medium", "summary": "auto" }, "store": false, diff --git a/codex-rs/core/tests/suite/list_models.rs b/codex-rs/core/tests/suite/list_models.rs index 5f21e94537..1791d28b01 100644 --- a/codex-rs/core/tests/suite/list_models.rs +++ b/codex-rs/core/tests/suite/list_models.rs @@ -4,11 +4,13 @@ use codex_core::ThreadManager; use codex_core::built_in_model_providers; use codex_core::models_manager::manager::RefreshStrategy; use codex_protocol::openai_models::ModelPreset; +use codex_protocol::openai_models::ModelUpgrade; use codex_protocol::openai_models::ReasoningEffort; use codex_protocol::openai_models::ReasoningEffortPreset; use core_test_support::load_default_config_for_test; use indoc::indoc; use pretty_assertions::assert_eq; +use std::collections::HashMap; use tempfile::tempdir; #[tokio::test(flavor = "multi_thread", worker_threads = 2)] @@ -50,35 +52,21 @@ async fn list_models_returns_chatgpt_models() -> Result<()> { fn expected_models_for_api_key() -> Vec { vec![ gpt_52_codex(), - gpt_5_1_codex_max(), - gpt_5_1_codex_mini(), gpt_5_2(), + gpt_5_1_codex_max(), + gpt_5_1_codex(), + gpt_5_1_codex_mini(), + gpt_5_1(), + gpt_5_codex(), + gpt_5(), + gpt_5_codex_mini(), bengalfox(), boomslang(), - gpt_5_codex(), - gpt_5_codex_mini(), - gpt_5_1_codex(), - gpt_5(), - gpt_5_1(), ] } fn expected_models_for_chatgpt() -> Vec { - let mut gpt_5_1_codex_max = gpt_5_1_codex_max(); - gpt_5_1_codex_max.is_default = false; - vec![ - gpt_52_codex(), - gpt_5_1_codex_max, - gpt_5_1_codex_mini(), - gpt_5_2(), - bengalfox(), - boomslang(), - gpt_5_codex(), - gpt_5_codex_mini(), - gpt_5_1_codex(), - gpt_5(), - gpt_5_1(), - ] + expected_models_for_api_key() } fn gpt_52_codex() -> ModelPreset { @@ -139,7 +127,17 @@ fn gpt_5_1_codex_max() -> ModelPreset { ), ], is_default: false, - upgrade: Some(gpt52_codex_upgrade()), + upgrade: Some(gpt52_codex_upgrade( + "gpt-5.1-codex-max", + HashMap::from([ + (ReasoningEffort::Low, ReasoningEffort::Low), + (ReasoningEffort::None, ReasoningEffort::Low), + (ReasoningEffort::Medium, ReasoningEffort::Medium), + (ReasoningEffort::High, ReasoningEffort::High), + (ReasoningEffort::Minimal, ReasoningEffort::Low), + (ReasoningEffort::XHigh, ReasoningEffort::XHigh), + ]), + )), show_in_picker: true, supported_in_api: true, } @@ -163,7 +161,17 @@ fn gpt_5_1_codex_mini() -> ModelPreset { ), ], is_default: false, - upgrade: Some(gpt52_codex_upgrade()), + upgrade: Some(gpt52_codex_upgrade( + "gpt-5.1-codex-mini", + HashMap::from([ + (ReasoningEffort::High, ReasoningEffort::High), + (ReasoningEffort::XHigh, ReasoningEffort::High), + (ReasoningEffort::Minimal, ReasoningEffort::Medium), + (ReasoningEffort::None, ReasoningEffort::Medium), + (ReasoningEffort::Low, ReasoningEffort::Medium), + (ReasoningEffort::Medium, ReasoningEffort::Medium), + ]), + )), show_in_picker: true, supported_in_api: true, } @@ -193,11 +201,21 @@ fn gpt_5_2() -> ModelPreset { ), effort( ReasoningEffort::XHigh, - "Extra high reasoning depth for complex problems", + "Extra high reasoning for complex problems", ), ], is_default: false, - upgrade: Some(gpt52_codex_upgrade()), + upgrade: Some(gpt52_codex_upgrade( + "gpt-5.2", + HashMap::from([ + (ReasoningEffort::High, ReasoningEffort::High), + (ReasoningEffort::None, ReasoningEffort::Low), + (ReasoningEffort::Minimal, ReasoningEffort::Low), + (ReasoningEffort::Low, ReasoningEffort::Low), + (ReasoningEffort::Medium, ReasoningEffort::Medium), + (ReasoningEffort::XHigh, ReasoningEffort::XHigh), + ]), + )), show_in_picker: true, supported_in_api: true, } @@ -289,7 +307,17 @@ fn gpt_5_codex() -> ModelPreset { ), ], is_default: false, - upgrade: Some(gpt52_codex_upgrade()), + upgrade: Some(gpt52_codex_upgrade( + "gpt-5-codex", + HashMap::from([ + (ReasoningEffort::Minimal, ReasoningEffort::Low), + (ReasoningEffort::High, ReasoningEffort::High), + (ReasoningEffort::Medium, ReasoningEffort::Medium), + (ReasoningEffort::XHigh, ReasoningEffort::High), + (ReasoningEffort::None, ReasoningEffort::Low), + (ReasoningEffort::Low, ReasoningEffort::Low), + ]), + )), show_in_picker: false, supported_in_api: true, } @@ -313,7 +341,17 @@ fn gpt_5_codex_mini() -> ModelPreset { ), ], is_default: false, - upgrade: Some(gpt52_codex_upgrade()), + upgrade: Some(gpt52_codex_upgrade( + "gpt-5-codex-mini", + HashMap::from([ + (ReasoningEffort::None, ReasoningEffort::Medium), + (ReasoningEffort::XHigh, ReasoningEffort::High), + (ReasoningEffort::High, ReasoningEffort::High), + (ReasoningEffort::Low, ReasoningEffort::Medium), + (ReasoningEffort::Medium, ReasoningEffort::Medium), + (ReasoningEffort::Minimal, ReasoningEffort::Medium), + ]), + )), show_in_picker: false, supported_in_api: true, } @@ -341,7 +379,17 @@ fn gpt_5_1_codex() -> ModelPreset { ), ], is_default: false, - upgrade: Some(gpt52_codex_upgrade()), + upgrade: Some(gpt52_codex_upgrade( + "gpt-5.1-codex", + HashMap::from([ + (ReasoningEffort::Minimal, ReasoningEffort::Low), + (ReasoningEffort::Low, ReasoningEffort::Low), + (ReasoningEffort::Medium, ReasoningEffort::Medium), + (ReasoningEffort::None, ReasoningEffort::Low), + (ReasoningEffort::High, ReasoningEffort::High), + (ReasoningEffort::XHigh, ReasoningEffort::High), + ]), + )), show_in_picker: false, supported_in_api: true, } @@ -373,7 +421,17 @@ fn gpt_5() -> ModelPreset { ), ], is_default: false, - upgrade: Some(gpt52_codex_upgrade()), + upgrade: Some(gpt52_codex_upgrade( + "gpt-5", + HashMap::from([ + (ReasoningEffort::XHigh, ReasoningEffort::High), + (ReasoningEffort::Minimal, ReasoningEffort::Minimal), + (ReasoningEffort::Low, ReasoningEffort::Low), + (ReasoningEffort::None, ReasoningEffort::Minimal), + (ReasoningEffort::High, ReasoningEffort::High), + (ReasoningEffort::Medium, ReasoningEffort::Medium), + ]), + )), show_in_picker: false, supported_in_api: true, } @@ -401,27 +459,37 @@ fn gpt_5_1() -> ModelPreset { ), ], is_default: false, - upgrade: Some(gpt52_codex_upgrade()), + upgrade: Some(gpt52_codex_upgrade( + "gpt-5.1", + HashMap::from([ + (ReasoningEffort::None, ReasoningEffort::Low), + (ReasoningEffort::Medium, ReasoningEffort::Medium), + (ReasoningEffort::High, ReasoningEffort::High), + (ReasoningEffort::XHigh, ReasoningEffort::High), + (ReasoningEffort::Low, ReasoningEffort::Low), + (ReasoningEffort::Minimal, ReasoningEffort::Low), + ]), + )), show_in_picker: false, supported_in_api: true, } } -fn gpt52_codex_upgrade() -> codex_protocol::openai_models::ModelUpgrade { - codex_protocol::openai_models::ModelUpgrade { +fn gpt52_codex_upgrade( + migration_config_key: &str, + reasoning_effort_mapping: HashMap, +) -> ModelUpgrade { + ModelUpgrade { id: "gpt-5.2-codex".to_string(), - reasoning_effort_mapping: None, - migration_config_key: "gpt-5.2-codex".to_string(), - model_link: Some("https://openai.com/index/introducing-gpt-5-2-codex".to_string()), - upgrade_copy: Some( - "Codex is now powered by gpt-5.2-codex, our latest frontier agentic coding model. It is smarter and faster than its predecessors and capable of long-running project-scale work." - .to_string(), - ), + reasoning_effort_mapping: Some(reasoning_effort_mapping), + migration_config_key: migration_config_key.to_string(), + model_link: None, + upgrade_copy: None, migration_markdown: Some( indoc! {r#" **Codex just got an upgrade. Introducing {model_to}.** - Codex is now powered by gpt-5.2-codex, our latest frontier agentic coding model. It is smarter and faster than its predecessors and capable of long-running project-scale work. Learn more about {model_to} at https://openai.com/index/introducing-gpt-5-2-codex + Codex is now powered by {model_to}, our latest frontier agentic coding model. It is smarter and faster than its predecessors and capable of long-running project-scale work. Learn more about {model_to} at https://openai.com/index/introducing-gpt-5-2-codex You can continue using {model_from} if you prefer. "#} diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__model_selection_popup.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__model_selection_popup.snap index 905925709e..d2676235a2 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__model_selection_popup.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__model_selection_popup.snap @@ -6,11 +6,11 @@ expression: popup Access legacy models by running codex -m or in your config.toml › 1. gpt-5.2-codex (default) Latest frontier agentic coding model. - 2. gpt-5.1-codex-max Codex-optimized flagship for deep and fast - reasoning. - 3. gpt-5.1-codex-mini Optimized for codex. Cheaper, faster, but less - capable. - 4. gpt-5.2 Latest frontier model with improvements across + 2. gpt-5.2 Latest frontier model with improvements across knowledge, reasoning and coding + 3. gpt-5.1-codex-max Codex-optimized flagship for deep and fast + reasoning. + 4. gpt-5.1-codex-mini Optimized for codex. Cheaper, faster, but less + capable. Press enter to select reasoning effort, or esc to dismiss. diff --git a/codex-rs/tui2/src/chatwidget/snapshots/codex_tui2__chatwidget__tests__model_selection_popup.snap b/codex-rs/tui2/src/chatwidget/snapshots/codex_tui2__chatwidget__tests__model_selection_popup.snap index 190f9c2933..27479f97f0 100644 --- a/codex-rs/tui2/src/chatwidget/snapshots/codex_tui2__chatwidget__tests__model_selection_popup.snap +++ b/codex-rs/tui2/src/chatwidget/snapshots/codex_tui2__chatwidget__tests__model_selection_popup.snap @@ -6,11 +6,11 @@ expression: popup Access legacy models by running codex -m or in your config.toml › 1. gpt-5.2-codex (default) Latest frontier agentic coding model. - 2. gpt-5.1-codex-max Codex-optimized flagship for deep and fast - reasoning. - 3. gpt-5.1-codex-mini Optimized for codex. Cheaper, faster, but less - capable. - 4. gpt-5.2 Latest frontier model with improvements across + 2. gpt-5.2 Latest frontier model with improvements across knowledge, reasoning and coding + 3. gpt-5.1-codex-max Codex-optimized flagship for deep and fast + reasoning. + 4. gpt-5.1-codex-mini Optimized for codex. Cheaper, faster, but less + capable. Press enter to select reasoning effort, or esc to dismiss. From fbd8afad81c74f03d19f1592d7d353bc71fcd252 Mon Sep 17 00:00:00 2001 From: Ahmed Ibrahim Date: Tue, 20 Jan 2026 15:24:45 -0800 Subject: [PATCH 4/6] queue only when task is working (#9558) --- codex-rs/tui/src/bottom_pane/chat_composer.rs | 2 +- codex-rs/tui2/src/bottom_pane/chat_composer.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/codex-rs/tui/src/bottom_pane/chat_composer.rs b/codex-rs/tui/src/bottom_pane/chat_composer.rs index d54e7005eb..ed0872c478 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer.rs @@ -1816,7 +1816,7 @@ impl ChatComposer { modifiers: KeyModifiers::NONE, kind: KeyEventKind::Press, .. - } => self.handle_submission(true), + } if self.is_task_running => self.handle_submission(true), KeyEvent { code: KeyCode::Enter, modifiers: KeyModifiers::NONE, diff --git a/codex-rs/tui2/src/bottom_pane/chat_composer.rs b/codex-rs/tui2/src/bottom_pane/chat_composer.rs index 34a4795de9..a17fd59f00 100644 --- a/codex-rs/tui2/src/bottom_pane/chat_composer.rs +++ b/codex-rs/tui2/src/bottom_pane/chat_composer.rs @@ -1746,7 +1746,7 @@ impl ChatComposer { modifiers: KeyModifiers::NONE, kind: KeyEventKind::Press, .. - } => self.handle_submission(true), + } if self.is_task_running => self.handle_submission(true), KeyEvent { code: KeyCode::Enter, modifiers: KeyModifiers::NONE, From 80f80181c2923a3d57a63fb5eab27e6edbbaaae3 Mon Sep 17 00:00:00 2001 From: HDCode Date: Wed, 21 Jan 2026 00:25:27 +0100 Subject: [PATCH 5/6] fix(core): require approval for force delete on Windows (#8590) ### What Implemented detection for dangerous "force delete" commands on Windows to trigger the user approval prompt when `--ask-for-approval on-request` is set. This aligns Windows behavior with the existing safety checks for `rm -rf` on Linux. ### Why Fixes #8567 - a critical safety gap where destructive Windows commands could bypass the approval prompt. This prevents accidental data loss by ensuring the user explicitly confirms operations that would otherwise suppress the OS's native confirmation prompts. ### How Updated the Windows command safety module to identify and flag the following patterns as dangerous: * **PowerShell**: * Detects `Remove-Item` (and aliases `rm`, `ri`, `del`, `erase`, `rd`, `rmdir`) when used with the `-Force` flag. * Uses token-based analysis to robustly detect these patterns even inside script blocks (`{...}`), sub-expression `(...)`, or semicolon-chained sequences. * **CMD**: * Detects `del /f` (force delete files). * Detects `rd /s /q` (recursive delete quiet). * **Command Chaining**: Added support for analyzing chained commands (using `&`, `&&`, `|`, `||`) to separate and check individual commands (e.g., catching `del /f` hidden in `echo log & del /f data`). ### Testing Added comprehensive unit tests covering: * **PowerShell**: `Remove-Item -Path 'test' -Recurse -Force` (Exact reproduction case). * **Complex Syntax**: Verified detection inside blocks (e.g., `if ($true) { rm -Force }`) and with trailing punctuation. * **CMD**: * `del /f` (Flagged). * `rd /s /q` (Flagged). * Chained commands: `echo hi & del /f file` (Flagged). * **False Positives**: * `rd /s` (Not flagged - relies on native prompt). * Standard deletions without force flags. Verified with `cargo test` and `cargo clippy`. --------- Co-authored-by: Eric Traut --- .../windows_dangerous_commands.rs | 453 +++++++++++++++++- 1 file changed, 446 insertions(+), 7 deletions(-) diff --git a/codex-rs/core/src/command_safety/windows_dangerous_commands.rs b/codex-rs/core/src/command_safety/windows_dangerous_commands.rs index d4b418d93a..c7f13adab0 100644 --- a/codex-rs/core/src/command_safety/windows_dangerous_commands.rs +++ b/codex-rs/core/src/command_safety/windows_dangerous_commands.rs @@ -82,6 +82,11 @@ fn is_dangerous_powershell(command: &[String]) -> bool { } } + // Check for force delete operations (e.g., Remove-Item -Force) + if has_force_delete_cmdlet(&tokens_lc) { + return true; + } + false } @@ -107,15 +112,49 @@ fn is_dangerous_cmd(command: &[String]) -> bool { } } - let Some(first_cmd) = iter.next() else { - return false; - }; - // Classic `cmd /c start https://...` ShellExecute path. - if !first_cmd.eq_ignore_ascii_case("start") { + let remaining: Vec = iter.cloned().collect(); + if remaining.is_empty() { return false; } - let remaining: Vec = iter.cloned().collect(); - args_have_url(&remaining) + + let cmd_tokens: Vec = match remaining.as_slice() { + [only] => shlex_split(only).unwrap_or_else(|| vec![only.clone()]), + _ => remaining, + }; + + // Refine tokens by splitting concatenated CMD operators (e.g. "echo hi&del") + let tokens: Vec = cmd_tokens + .into_iter() + .flat_map(|t| split_embedded_cmd_operators(&t)) + .collect(); + + const CMD_SEPARATORS: &[&str] = &["&", "&&", "|", "||"]; + tokens + .split(|t| CMD_SEPARATORS.contains(&t.as_str())) + .any(|segment| { + let Some(cmd) = segment.first() else { + return false; + }; + + // Classic `cmd /c ... start https://...` ShellExecute path. + if cmd.eq_ignore_ascii_case("start") && args_have_url(segment) { + return true; + } + // Force delete: del /f, erase /f + if (cmd.eq_ignore_ascii_case("del") || cmd.eq_ignore_ascii_case("erase")) + && has_force_flag_cmd(segment) + { + return true; + } + // Recursive directory removal: rd /s /q, rmdir /s /q + if (cmd.eq_ignore_ascii_case("rd") || cmd.eq_ignore_ascii_case("rmdir")) + && has_recursive_flag_cmd(segment) + && has_quiet_flag_cmd(segment) + { + return true; + } + false + }) } fn is_direct_gui_launch(command: &[String]) -> bool { @@ -149,6 +188,123 @@ fn is_direct_gui_launch(command: &[String]) -> bool { false } +fn split_embedded_cmd_operators(token: &str) -> Vec { + // Split concatenated CMD operators so `echo hi&del` becomes `["echo hi", "&", "del"]`. + // Handles `&`, `&&`, `|`, `||`. Best-effort (CMD escaping is weird by nature). + let mut parts = Vec::new(); + let mut start = 0; + let mut it = token.char_indices().peekable(); + + while let Some((i, ch)) = it.next() { + if ch == '&' || ch == '|' { + if i > start { + parts.push(token[start..i].to_string()); + } + + // Detect doubled operator: && or || + let op_len = match it.peek() { + Some(&(j, next)) if next == ch => { + it.next(); // consume second char + (j + next.len_utf8()) - i + } + _ => ch.len_utf8(), + }; + + parts.push(token[i..i + op_len].to_string()); + start = i + op_len; + } + } + + if start < token.len() { + parts.push(token[start..].to_string()); + } + + parts.retain(|s| !s.trim().is_empty()); + parts +} + +fn has_force_delete_cmdlet(tokens: &[String]) -> bool { + const DELETE_CMDLETS: &[&str] = &["remove-item", "ri", "rm", "del", "erase", "rd", "rmdir"]; + + // Hard separators that end a command segment (so -Force must be in same segment) + const SEG_SEPS: &[char] = &[';', '|', '&', '\n', '\r', '\t']; + + // Soft separators: punctuation that can stick to tokens (blocks, parens, brackets, commas, etc.) + const SOFT_SEPS: &[char] = &['{', '}', '(', ')', '[', ']', ',', ';']; + + // Build rough command segments first + let mut segments: Vec> = vec![Vec::new()]; + for tok in tokens { + // If token itself contains segment separators, split it (best-effort) + let mut cur = String::new(); + for ch in tok.chars() { + if SEG_SEPS.contains(&ch) { + let s = cur.trim(); + if let Some(msg) = segments.last_mut() + && !s.is_empty() + { + msg.push(s.to_string()); + } + cur.clear(); + if let Some(last) = segments.last() + && !last.is_empty() + { + segments.push(Vec::new()); + } + } else { + cur.push(ch); + } + } + let s = cur.trim(); + if let Some(segment) = segments.last_mut() + && !s.is_empty() + { + segment.push(s.to_string()); + } + } + + // Now, inside each segment, normalize tokens by splitting on soft punctuation + segments.into_iter().any(|seg| { + let atoms = seg + .iter() + .flat_map(|t| t.split(|c| SOFT_SEPS.contains(&c))) + .map(str::trim) + .filter(|s| !s.is_empty()); + + let mut has_delete = false; + let mut has_force = false; + + for a in atoms { + if DELETE_CMDLETS.iter().any(|cmd| a.eq_ignore_ascii_case(cmd)) { + has_delete = true; + } + if a.eq_ignore_ascii_case("-force") + || a.get(..7) + .is_some_and(|p| p.eq_ignore_ascii_case("-force:")) + { + has_force = true; + } + } + + has_delete && has_force + }) +} + +/// Check for /f or /F flag in CMD del/erase arguments. +fn has_force_flag_cmd(args: &[String]) -> bool { + args.iter().any(|a| a.eq_ignore_ascii_case("/f")) +} + +/// Check for /s or /S flag in CMD rd/rmdir arguments. +fn has_recursive_flag_cmd(args: &[String]) -> bool { + args.iter().any(|a| a.eq_ignore_ascii_case("/s")) +} + +/// Check for /q or /Q flag in CMD rd/rmdir arguments. +fn has_quiet_flag_cmd(args: &[String]) -> bool { + args.iter().any(|a| a.eq_ignore_ascii_case("/q")) +} + fn args_have_url(args: &[String]) -> bool { args.iter().any(|arg| looks_like_url(arg)) } @@ -313,4 +469,287 @@ mod tests { "." ]))); } + + // Force delete tests for PowerShell + + #[test] + fn powershell_remove_item_force_is_dangerous() { + assert!(is_dangerous_command_windows(&vec_str(&[ + "powershell", + "-Command", + "Remove-Item test -Force" + ]))); + } + + #[test] + fn powershell_remove_item_recurse_force_is_dangerous() { + assert!(is_dangerous_command_windows(&vec_str(&[ + "powershell", + "-Command", + "Remove-Item test -Recurse -Force" + ]))); + } + + #[test] + fn powershell_ri_alias_force_is_dangerous() { + assert!(is_dangerous_command_windows(&vec_str(&[ + "pwsh", + "-Command", + "ri test -Force" + ]))); + } + + #[test] + fn powershell_remove_item_without_force_is_not_flagged() { + assert!(!is_dangerous_command_windows(&vec_str(&[ + "powershell", + "-Command", + "Remove-Item test" + ]))); + } + + // Force delete tests for CMD + #[test] + fn cmd_del_force_is_dangerous() { + assert!(is_dangerous_command_windows(&vec_str(&[ + "cmd", "/c", "del", "/f", "test.txt" + ]))); + } + + #[test] + fn cmd_erase_force_is_dangerous() { + assert!(is_dangerous_command_windows(&vec_str(&[ + "cmd", "/c", "erase", "/f", "test.txt" + ]))); + } + + #[test] + fn cmd_del_without_force_is_not_flagged() { + assert!(!is_dangerous_command_windows(&vec_str(&[ + "cmd", "/c", "del", "test.txt" + ]))); + } + + #[test] + fn cmd_rd_recursive_is_dangerous() { + assert!(is_dangerous_command_windows(&vec_str(&[ + "cmd", "/c", "rd", "/s", "/q", "test" + ]))); + } + + #[test] + fn cmd_rd_without_quiet_is_not_flagged() { + assert!(!is_dangerous_command_windows(&vec_str(&[ + "cmd", "/c", "rd", "/s", "test" + ]))); + } + + #[test] + fn cmd_rmdir_recursive_is_dangerous() { + assert!(is_dangerous_command_windows(&vec_str(&[ + "cmd", "/c", "rmdir", "/s", "/q", "test" + ]))); + } + + // Test exact scenario from issue #8567 + #[test] + fn powershell_remove_item_path_recurse_force_is_dangerous() { + assert!(is_dangerous_command_windows(&vec_str(&[ + "powershell", + "-Command", + "Remove-Item -Path 'test' -Recurse -Force" + ]))); + } + + #[test] + fn powershell_remove_item_force_with_semicolon_is_dangerous() { + assert!(is_dangerous_command_windows(&vec_str(&[ + "powershell", + "-Command", + "Remove-Item test -Force; Write-Host done" + ]))); + } + + #[test] + fn powershell_remove_item_force_inside_block_is_dangerous() { + assert!(is_dangerous_command_windows(&vec_str(&[ + "powershell", + "-Command", + "if ($true) { Remove-Item test -Force}" + ]))); + } + + #[test] + fn powershell_remove_item_force_inside_brackets_is_dangerous() { + assert!(is_dangerous_command_windows(&vec_str(&[ + "powershell", + "-Command", + "[void]( Remove-Item test -Force)]" + ]))); + } + + #[test] + fn cmd_del_path_containing_f_is_not_flagged() { + assert!(!is_dangerous_command_windows(&vec_str(&[ + "cmd", + "/c", + "del", + "C:/foo/bar.txt" + ]))); + } + + #[test] + fn cmd_rd_path_containing_s_is_not_flagged() { + assert!(!is_dangerous_command_windows(&vec_str(&[ + "cmd", + "/c", + "rd", + "C:/source" + ]))); + } + + #[test] + fn cmd_bypass_chained_del_is_dangerous() { + assert!(is_dangerous_command_windows(&vec_str(&[ + "cmd", "/c", "echo", "hello", "&", "del", "/f", "file.txt" + ]))); + } + + #[test] + fn powershell_chained_no_space_is_dangerous() { + assert!(is_dangerous_command_windows(&vec_str(&[ + "powershell", + "-Command", + "Write-Host hi;Remove-Item -Force C:\\tmp" + ]))); + } + + #[test] + fn powershell_comma_separated_is_dangerous() { + assert!(is_dangerous_command_windows(&vec_str(&[ + "powershell", + "-Command", + "del,-Force,C:\\foo" + ]))); + } + + #[test] + fn cmd_echo_del_is_not_dangerous() { + assert!(!is_dangerous_command_windows(&vec_str(&[ + "cmd", "/c", "echo", "del", "/f" + ]))); + } + + #[test] + fn cmd_del_single_string_argument_is_dangerous() { + assert!(is_dangerous_command_windows(&vec_str(&[ + "cmd", + "/c", + "del /f file.txt" + ]))); + } + + #[test] + fn cmd_del_chained_single_string_argument_is_dangerous() { + assert!(is_dangerous_command_windows(&vec_str(&[ + "cmd", + "/c", + "echo hello & del /f file.txt" + ]))); + } + + #[test] + fn cmd_chained_no_space_del_is_dangerous() { + assert!(is_dangerous_command_windows(&vec_str(&[ + "cmd", + "/c", + "echo hi&del /f file.txt" + ]))); + } + + #[test] + fn cmd_chained_andand_no_space_del_is_dangerous() { + assert!(is_dangerous_command_windows(&vec_str(&[ + "cmd", + "/c", + "echo hi&&del /f file.txt" + ]))); + } + + #[test] + fn cmd_chained_oror_no_space_del_is_dangerous() { + assert!(is_dangerous_command_windows(&vec_str(&[ + "cmd", + "/c", + "echo hi||del /f file.txt" + ]))); + } + + #[test] + fn cmd_start_url_single_string_is_dangerous() { + assert!(is_dangerous_command_windows(&vec_str(&[ + "cmd", + "/c", + "start https://example.com" + ]))); + } + + #[test] + fn cmd_chained_no_space_rmdir_is_dangerous() { + assert!(is_dangerous_command_windows(&vec_str(&[ + "cmd", + "/c", + "echo hi&rmdir /s /q testdir" + ]))); + } + + #[test] + fn cmd_del_force_uppercase_flag_is_dangerous() { + assert!(is_dangerous_command_windows(&vec_str(&[ + "cmd", "/c", "DEL", "/F", "file.txt" + ]))); + } + + #[test] + fn cmdexe_r_del_force_is_dangerous() { + assert!(is_dangerous_command_windows(&vec_str(&[ + "cmd.exe", "/r", "del", "/f", "file.txt" + ]))); + } + + #[test] + fn cmd_start_quoted_url_single_string_is_dangerous() { + assert!(is_dangerous_command_windows(&vec_str(&[ + "cmd", + "/c", + r#"start "https://example.com""# + ]))); + } + + #[test] + fn cmd_start_title_then_url_is_dangerous() { + assert!(is_dangerous_command_windows(&vec_str(&[ + "cmd", + "/c", + r#"start "" https://example.com"# + ]))); + } + + #[test] + fn powershell_rm_alias_force_is_dangerous() { + assert!(is_dangerous_command_windows(&vec_str(&[ + "powershell", + "-Command", + "rm test -Force" + ]))); + } + + #[test] + fn powershell_benign_force_separate_command_is_not_dangerous() { + assert!(!is_dangerous_command_windows(&vec_str(&[ + "powershell", + "-Command", + "Get-ChildItem -Force; Remove-Item test" + ]))); + } } From 90e7e613bfd6e02108634dcd0099875a8421ee91 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Tue, 20 Jan 2026 16:15:35 -0800 Subject: [PATCH 6/6] fix: bminor/bash is no longer on GitHub so use bolinfest/bash instead --- .github/workflows/shell-tool-mcp.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/shell-tool-mcp.yml b/.github/workflows/shell-tool-mcp.yml index bc49a90fa6..62a4989b77 100644 --- a/.github/workflows/shell-tool-mcp.yml +++ b/.github/workflows/shell-tool-mcp.yml @@ -198,7 +198,7 @@ jobs: shell: bash run: | set -euo pipefail - git clone --depth 1 https://github.com/bminor/bash /tmp/bash + git clone --depth 1 https://github.com/bolinfest/bash /tmp/bash cd /tmp/bash git fetch --depth 1 origin a8a1c2fac029404d3f42cd39f5a20f24b6e4fe4b git checkout a8a1c2fac029404d3f42cd39f5a20f24b6e4fe4b @@ -240,7 +240,7 @@ jobs: shell: bash run: | set -euo pipefail - git clone --depth 1 https://github.com/bminor/bash /tmp/bash + git clone --depth 1 https://github.com/bolinfest/bash /tmp/bash cd /tmp/bash git fetch --depth 1 origin a8a1c2fac029404d3f42cd39f5a20f24b6e4fe4b git checkout a8a1c2fac029404d3f42cd39f5a20f24b6e4fe4b