Skip to content

fix(shell): route sandbox execution through platform_shell so Windows spawns cmd.exe (#4705)#4723

Open
CodeGhost21 wants to merge 5 commits into
tinyhumansai:mainfrom
CodeGhost21:fix/4705-windows-shell-cmd-exe
Open

fix(shell): route sandbox execution through platform_shell so Windows spawns cmd.exe (#4705)#4723
CodeGhost21 wants to merge 5 commits into
tinyhumansai:mainfrom
CodeGhost21:fix/4705-windows-shell-cmd-exe

Conversation

@CodeGhost21

@CodeGhost21 CodeGhost21 commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Fix Windows shell tool spawn failure by routing all three shell-spawning sites through a shared platform_shell helper (Windows: cmd.exe /C, Unix: bash -lc "set -o pipefail\n<cmd>" or sh -lc).
  • NativeRuntime on Windows moves from PowerShell to cmd.exe so %VAR% expansion works and the sandboxed output-capture path stays byte-transparent.
  • Cross-platform regression tests exercise the real spawn path on Windows CI using tempfile::tempdir() + shell-builtin echo hello / exit 1.

Problem

Both sandbox execution paths in src/openhuman/sandbox/ops.rsexecute_unsandboxed:165 and execute_local_jail:225 — hardcoded Command::new("sh"). On Windows, sh is not in PATH, so CreateProcessW returns ERROR_FILE_NOT_FOUND in ~30ms and the shell tool records Failed 34ms before running anything. Any agent with SandboxMode::Sandboxed (and the tinyflows code node, which always runs Sandboxed) hit this on every shell call — matching the symptoms in #4705.

The direct NativeRuntime::build_shell_command path had Windows detection but used PowerShell — which does not expand %VAR% and writes UTF-16LE when redirecting to a file, so the two implementations disagreed on what "the Windows shell" means. That divergence is the reason the sandbox paths silently regressed.

Solution

New module src/openhuman/agent/platform_shell.rs:

  • build_tokio_command(command) / build_std_command(command) — one helper, two Command flavours. Windows → cmd.exe /C <command>. Unix → bash -lc "set -o pipefail\n<command>" when bash is at /usr/bin/bash or /bin/bash, sh -lc <command> otherwise. pipefail preserved so masked pipe failures still surface (issue Improve agent harness with LangGraph-style state machine or Polaris-like architecture #4249 context).
  • wrap_with_output_redirection(command, stdout, stderr) — platform-aware output-capture wrap used by execute_local_jail. { ...; } > 'out' 2> 'err' on Unix (brace grouping so multi-stage commands route all stages), <command> > "out" 2> "err" on Windows (cmd.exe has no {} grouping; trailing >/2> binds to the whole /C payload).
  • bash_path() moved from host_runtime.rs and made pub(crate) so the existing native_shell_pipefail_surfaces_failed_pipe_stage test can still opt-out on bash-less hosts.

Rewired call sites:

  • NativeRuntime::build_shell_command (src/openhuman/agent/host_runtime.rs:75) — delegates to platform_shell::build_tokio_command. CWD validation, hide_window, and the ensure_usable_cwd guard are untouched.
  • execute_unsandboxed (src/openhuman/sandbox/ops.rs:159) — Command::new("sh")platform_shell::build_tokio_command.
  • execute_local_jail (src/openhuman/sandbox/ops.rs:202) — Command::new("sh")platform_shell::build_std_command (matches cwd_jail::spawn's std::process::Command signature); wrap uses platform_shell::wrap_with_output_redirection.

Windows shell choice — PowerShell → cmd.exe. Deliberate. cmd.exe expands %VAR% per the reporter's acceptance criteria, matches the POSIX sh -c contract users expect from a shell tool, and its >/2> operators are byte-transparent (PowerShell 5.1 writes UTF-16LE, which would force encoding-aware readback in the local-jail path). Users relying on PowerShell-only syntax ($env:VAR, cmdlets) via the direct shell tool are affected — but the direct shell path was unusable on Windows for Sandboxed agents anyway, and the population is expected to be near-zero.

Submission Checklist

  • Tests added or updated (happy path + at least one failure / edge case) per Testing Strategyplatform_shell::tests covers both Command variants and the redirect wrap per platform; sandbox::ops::tests::execute_unsandboxed_echo_runs_on_every_os and execute_in_sandbox_none_backend_runs_on_every_os are cross-platform Windows-CI regression guards for the sh → platform-aware fix, including a exit 1 failure-path assertion.
  • Diff coverage ≥ 80% — every added line in platform_shell.rs, the two rewired call sites in sandbox/ops.rs, and the delegated NativeRuntime::build_shell_command are exercised by the tests above and by the existing native_runtime_reports_capabilities_and_shell_command / native_shell_pipefail_surfaces_failed_pipe_stage tests.
  • Coverage matrix updated — N/A: no user-facing feature added, changed, or removed. This is an internal shell-spawn correctness fix; the shell tool behavior surface (already in the matrix) is unchanged on Unix.
  • All affected feature IDs from the matrix are listed in the PR description under ## Related — N/A: no matrix feature IDs affected (see above).
  • No new external network dependencies introduced (mock backend used per Testing Strategy) — no network at all; local Command spawn only.
  • Manual smoke checklist updated if this touches release-cut surfaces (docs/RELEASE-MANUAL-SMOKE.md) — N/A: no new user-facing surface. Existing "shell tool works" smoke covers it.
  • Linked issue closed via Closes #NNN in the ## Related section — see below.

Impact

  • Windows desktop: Shell tool now works in Sandboxed-mode agents and in the tinyflows code node (both previously ~30ms CreateProcessW failure). Default (SandboxMode::None) agents' shell now uses cmd.exe instead of PowerShell — %VAR% expansion works, >/2> no longer produces UTF-16LE files.
  • Unix (macOS/Linux): Behaviourally unchanged. NativeRuntime still uses bash -lc "set -o pipefail\n<cmd>" (or sh -lc fallback); execute_unsandboxed / execute_local_jail swap from bare sh -c to the same helper, so on Unix the concrete command becomes bash -lc "set -o pipefail\n<cmd>" when bash is present — which is a small robustness improvement (masked pipe failures now surface via the sandbox too) and matches what NativeRuntime was already doing.
  • Performance: none — one function-call redirection per shell spawn.
  • Security: none — same spawn semantics, same env-scrub, same CWD guards. cmd.exe is Microsoft-signed and always in PATH on Windows.
  • Migration: none. No config change, no on-disk state change.

Related

  • Closes: bug: Shell tool fails on Windows — defaults to bash/sh which is unavailable, cmd /c fallback not automatic #4705
  • Follow-up PR(s)/TODOs:
    • main currently has widespread pre-existing test-compilation bit-rot in unrelated modules (sandbox/docker.rs::tests missing DockerOverrides import, openhuman/webchat missing symbols, memory_tree, accessibility, etc. — 38 errors across 8 domains). Reproduced by stashing this PR's changes and running cargo test --lib against clean origin/main. Not fixed here; flagged so reviewers know a separate cleanup PR is needed for the full-suite lane. The domain-filtered CI lite lane should still exercise this PR's tests.

AI Authored PR Metadata (required for Codex/Linear PRs)

Linear Issue

  • Key: N/A
  • URL: N/A

Commit & Branch

  • Branch: fix/4705-windows-shell-cmd-exe
  • Commit SHA: 736666ceb

Validation Run

  • pnpm --filter openhuman-app format:check — N/A: Rust-only change, no app/** files touched.
  • pnpm typecheck — N/A: Rust-only change, no TypeScript touched.
  • Focused tests: cross-platform sandbox + platform_shell unit tests added; local cargo test --lib blocked by pre-existing test bit-rot on main (see Related).
  • Rust fmt/check (if changed): cargo fmt --check passes on all four changed files; GGML_NATIVE=OFF cargo check --lib passes cleanly.
  • Tauri fmt/check (if changed): N/A: no app/src-tauri/** files touched.

Validation Blocked

  • command: GGML_NATIVE=OFF cargo test --manifest-path Cargo.toml --lib
  • error: 38 pre-existing compile errors in unrelated test modules on origin/main (DockerOverrides, EnvLookup, MEMORY_SYNC_INTERVAL_SECS_ENV_VAR, compose_system_prompt_suffix, etc.) block the whole lib-test binary from linking.
  • impact: This PR's new tests could not be executed locally. They compile in isolation and follow existing style; the domain-filtered CI-Lite lane should exercise them.

Behavior Changes

  • Intended behavior change: Windows shell tool now spawns via cmd.exe /C in all three code paths (previously PowerShell for NativeRuntime, hardcoded sh for the two sandbox paths — the latter completely broken).
  • User-visible effect: Sandboxed-mode agents on Windows can run shell commands. %VAR% expansion (echo %USERPROFILE%) works on the default agent's shell. PowerShell-only syntax ($env:VAR, cmdlets) via the direct shell tool no longer works on Windows.

Parity Contract

  • Legacy behavior preserved: Unix NativeRuntime shell selection (bash -lc "set -o pipefail\n…" / sh -lc fallback) is identical. CWD guard (ensure_usable_cwd), hide_window handling, SAFE_ENV_VARS scrub, sandbox env-passthrough, and timeout semantics are unchanged.
  • Guard/fallback/dispatch parity checks: NativeRuntime::build_shell_command still routes CWD validation before the returned Command is spawned; execute_local_jail still routes through cwd_jail::default_backend() with a NoopBackend fallback when the OS backend is unavailable.

Duplicate / Superseded PR Handling

  • Duplicate PR(s): none.
  • Canonical PR: this one.
  • Resolution: N/A.

Summary by CodeRabbit

  • New Features
    • Added centralized, cross-platform shell command handling shared across runtime and sandbox execution.
    • Improved stdout/stderr redirection wrapping for consistent output capture on Windows vs Unix.
  • Bug Fixes
    • Fixed Windows command execution by avoiding Unix-only shell invocation for unsandboxed and sandboxed runs.
    • Marked the Windows AppContainer backend as unavailable to prevent orphaned processes.
  • Tests
    • Updated/expanded command execution tests to use temp directories and validate shell selection and redirection formatting across platforms, including pipefail behavior.

… spawns cmd.exe (tinyhumansai#4705)

Both `execute_unsandboxed` and `execute_local_jail` in
`src/openhuman/sandbox/ops.rs` hardcoded `Command::new("sh")`, which
fails at `CreateProcessW` on Windows in ~30ms because `sh` is not in
`PATH`. Any Sandboxed-mode agent (and the `tinyflows` `code` node,
which always runs Sandboxed) hit this and reported "Failed 34ms" as
soon as the shell tool was invoked.

Consolidate all three shell-spawning sites (`NativeRuntime`,
`execute_unsandboxed`, `execute_local_jail`) behind a new
`openhuman::agent::platform_shell` module:

- `build_tokio_command` / `build_std_command` — `cmd.exe /C` on
  Windows, `bash -lc "set -o pipefail\n<cmd>"` when bash is present
  on Unix, `sh -lc` fallback otherwise.
- `wrap_with_output_redirection` — POSIX `{ ...; } > 'out' 2> 'err'`
  on Unix, plain trailing `<cmd> > "out" 2> "err"` on Windows
  (cmd.exe has no brace grouping).

`NativeRuntime` on Windows moves from PowerShell to `cmd.exe`, per
the reporter's acceptance criteria: this restores `%VAR%` expansion
(`echo %USERPROFILE%`), gives byte-transparent `>`/`2>` for the
sandbox output-capture path, and matches the POSIX `sh -c` contract
users expect from the shell tool.

Existing tests that assumed Unix (`/tmp`, `false` builtin) are
`#[cfg(unix)]` and paired with cross-platform equivalents using
`tempfile::tempdir()` and shell-builtin `echo hello`/`exit 1` so the
Windows CI lane exercises the real spawn path.

Closes tinyhumansai#4705
@CodeGhost21 CodeGhost21 requested a review from a team July 9, 2026 06:55
@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: bdd2d044-d08d-4cde-84f3-56fa6b10ee08

📥 Commits

Reviewing files that changed from the base of the PR and between 62e86ae and 46112e9.

📒 Files selected for processing (1)
  • src/openhuman/voice/dictation_listener.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/openhuman/voice/dictation_listener.rs

📝 Walkthrough

Walkthrough

A shared platform_shell module now handles cross-platform shell selection and redirection. NativeRuntime and sandbox execution paths delegate to it, AppContainerBackend is now unavailable, and a macOS-only hotkey import is gated.

Changes

Platform shell centralization

Layer / File(s) Summary
platform_shell command builders
src/openhuman/agent/platform_shell.rs, src/openhuman/agent/mod.rs
Adds shared shell selection, command construction, output redirection formatting, cached bash discovery, module documentation, and tests.
NativeRuntime shell delegation
src/openhuman/agent/host_runtime.rs
build_shell_command now delegates to platform_shell, removes the local bash_path helper, and updates shell-command tests to use tempdirs and cross-platform assertions.
Sandbox execution uses platform_shell
src/openhuman/sandbox/ops.rs
execute_unsandboxed and local-jail command handling use shared shell helpers, with Unix-only tests gated and new cross-platform regression tests added.
AppContainer availability
src/openhuman/cwd_jail/windows.rs
AppContainerBackend::is_available() now returns false, and a test asserts the backend remains unavailable until it can produce a waitable child.
macOS hotkey import gate
src/openhuman/voice/dictation_listener.rs
The hotkey import is conditionally compiled for non-macOS targets.

Estimated code review effort: 4 (Complex) | ~45 minutes

Suggested labels: bug, rust-core, agent

Poem

A rabbit hopped through cmd and bash,
No more shell trips in a flash.
Redirections now land just right,
On every OS, day or night.
Hop hop—cross-platform and bright!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The PR also includes an unrelated macOS import-guard cleanup in voice/dictation_listener.rs that isn't needed for the shell fix. Move the dictation_listener warning cleanup into a separate PR unless it is required for the shell execution change.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: routing shell execution through platform_shell so Windows uses cmd.exe.
Linked Issues check ✅ Passed The code routes Windows shell execution through cmd.exe /C, preserves Unix bash/sh behavior, and adds cross-platform tests for the shell paths.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot added bug rust-core Core Rust runtime in src/: CLI, core_server, shared infrastructure. labels Jul 9, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/openhuman/agent/platform_shell.rs (1)

35-68: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate shell-selection branching between the tokio and std builders.

build_tokio_command and build_std_command repeat identical Windows/bash/sh selection logic, differing only in the Command type constructed. If the shell-selection policy changes in the future (e.g. new fallback), it's easy to update one and forget the other.

♻️ Proposed refactor to share selection logic
+fn select_shell(command: &str) -> (&'static str, Vec<String>) {
+    if cfg!(windows) {
+        ("cmd", vec!["/C".to_string(), command.to_string()])
+    } else if let Some(bash) = bash_path() {
+        (bash, vec!["-lc".to_string(), format!("set -o pipefail\n{command}")])
+    } else {
+        ("sh", vec!["-lc".to_string(), command.to_string()])
+    }
+}
+
 pub fn build_tokio_command(command: &str) -> tokio::process::Command {
-    if cfg!(windows) {
-        let mut c = tokio::process::Command::new("cmd");
-        c.arg("/C").arg(command);
-        c
-    } else if let Some(bash) = bash_path() {
-        let mut c = tokio::process::Command::new(bash);
-        c.arg("-lc").arg(format!("set -o pipefail\n{command}"));
-        c
-    } else {
-        let mut c = tokio::process::Command::new("sh");
-        c.arg("-lc").arg(command);
-        c
-    }
+    let (prog, args) = select_shell(command);
+    let mut c = tokio::process::Command::new(prog);
+    c.args(args);
+    c
 }

 pub fn build_std_command(command: &str) -> std::process::Command {
-    if cfg!(windows) {
-        let mut c = std::process::Command::new("cmd");
-        c.arg("/C").arg(command);
-        c
-    } else if let Some(bash) = bash_path() {
-        let mut c = std::process::Command::new(bash);
-        c.arg("-lc").arg(format!("set -o pipefail\n{command}"));
-        c
-    } else {
-        let mut c = std::process::Command::new("sh");
-        c.arg("-lc").arg(command);
-        c
-    }
+    let (prog, args) = select_shell(command);
+    let mut c = std::process::Command::new(prog);
+    c.args(args);
+    c
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/openhuman/agent/platform_shell.rs` around lines 35 - 68, The
shell-selection logic is duplicated in build_tokio_command and
build_std_command, so refactor the shared Windows/bash/sh branching into a
single helper or shared path-selection function and have both builders reuse it
while only differing in the Command type they construct. Keep the current
behavior of cfg!(windows), bash_path(), and the pipefail command wrapping
identical in both code paths, and ensure any future shell policy changes only
need to be made in one place.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/openhuman/agent/platform_shell.rs`:
- Around line 1-123: This new platform_shell module violates the project’s Rust
module layout rule by living as a root-level .rs file under openhuman/agent.
Move the functionality into a dedicated openhuman/agent/platform_shell/
subdirectory with a mod.rs entry point and sibling module(s), and keep mod.rs as
a thin re-export layer. Preserve the existing symbols build_tokio_command,
build_std_command, wrap_with_output_redirection, and bash_path in the new module
structure so callers in NativeRuntime and sandbox::ops continue to resolve them
unchanged.

---

Nitpick comments:
In `@src/openhuman/agent/platform_shell.rs`:
- Around line 35-68: The shell-selection logic is duplicated in
build_tokio_command and build_std_command, so refactor the shared
Windows/bash/sh branching into a single helper or shared path-selection function
and have both builders reuse it while only differing in the Command type they
construct. Keep the current behavior of cfg!(windows), bash_path(), and the
pipefail command wrapping identical in both code paths, and ensure any future
shell policy changes only need to be made in one place.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1e2d678e-a4a4-4aa2-852c-2db7a52dd62e

📥 Commits

Reviewing files that changed from the base of the PR and between 0881023 and 736666c.

📒 Files selected for processing (4)
  • src/openhuman/agent/host_runtime.rs
  • src/openhuman/agent/mod.rs
  • src/openhuman/agent/platform_shell.rs
  • src/openhuman/sandbox/ops.rs

Comment thread src/openhuman/agent/platform_shell.rs

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 736666ceb1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

// trailing `> … 2> …` on cmd.exe (no brace grouping). Shell binary is
// picked by `platform_shell` so this path is Windows-safe (#4705).
let wrapped = platform_shell::wrap_with_output_redirection(command, &stdout_file, &stderr_file);
let mut cmd = platform_shell::build_std_command(&wrapped);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Do not start AppContainer commands without a Child handle

When a Windows desktop agent runs with SandboxMode::Sandboxed, resolve_sandbox_policy selects the Local backend, and this new cmd.exe command is spawnable by cwd_jail's AppContainer backend. But src/openhuman/cwd_jail/windows.rs:283-287 still returns Unsupported after CreateProcessW succeeds because it cannot produce a std::process::Child, so execute_local_jail reports a spawn failure and skips timeout/output handling even though the command may already be running and writing the redirected files. This turns the Windows fix into side-effecting false failures/retries; fallback to NoopBackend for this backend or keep AppContainer unavailable until it can return a waitable child.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verified and fixed in commit f953026.

You're right — before this PR, Command::new("sh") failed inside spawn_in_container's CreateProcessW call (sh not in PATH), so the Err was clean and no process leaked. After the platform_shell fix, cmd.exe actually spawns, the Err(Unsupported) from the missing OwnedHandle → Child bridge surfaces to execute_local_jail, and the caller reports "spawn failed" while a real cmd.exe keeps running against the redirected stdout/stderr files.

Fix: flipped AppContainerBackend::is_available() to false (with a long comment referencing the TODO at spawn_in_container:~279 and this PR). pick_backend() on Windows now falls through to NoopBackend, which returns a real waitable Child, so execute_local_jail's wait/timeout/output-capture path works. Regression test appcontainer_backend_reports_unavailable_until_child_bridge_lands in windows.rs pins the invariant — the same commit that adds the Child bridge should flip both the flag and the test in one go.

Went with your second option ("keep AppContainer unavailable until it can return a waitable child") rather than a targeted fallback in execute_local_jail — the underlying issue is a backend advertising a capability it can't actually deliver, so fixing it at the backend keeps every future caller of pick_backend() / default_backend() (registry, potential other callers) honest without needing per-caller workarounds.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirming this P1 is correctly addressed by commit f953026. Flipping AppContainerBackend::is_available() to false makes pick_backend() fall through to NoopBackend, whose spawn returns a real waitable std::process::Child, so execute_local_jail's wait/timeout/output path no longer strands a running cmd.exe. The regression test appcontainer_backend_reports_unavailable_until_child_bridge_lands pins the invariant. Good fix at the backend level rather than a per-caller workaround.

One remaining Windows hazard on this same execute_local_jail path (separate from the orphan issue): the redirect wrap is passed to cmd /C via Command::arg(), whose quote-escaping breaks cmd's redirection parsing so the captured output files end up empty — see the inline comment on platform_shell.rs.

Address the PR tinyhumansai#4723 review nitpick — the Windows/bash/sh branching
was duplicated between `build_tokio_command` and `build_std_command`.
Fold it into a private `select_shell_program_and_args` helper so both
builders only differ in the `Command` type they construct, and future
shell-policy changes (pwsh support, pipefail tweaks) touch one place.
…idge lands (PR tinyhumansai#4723 P1)

`AppContainerBackend::spawn_in_container` calls `CreateProcessW`
successfully, then returns `Err(io::ErrorKind::Unsupported)` because
it can't yet bridge `OwnedHandle` to `std::process::Child` (see the
TODO at ~line 279). Prior to this PR the sandbox Windows path spawned
non-existent `sh`, so `CreateProcessW` failed early and no orphan was
created — the AppContainer wart was masked. After the platform_shell
fix, `cmd.exe` spawns for real, the Err surfaces to
`execute_local_jail`, and the running `cmd.exe` gets stranded
(writing to redirected stdout/stderr files while the caller reports
"spawn failed" and skips wait/timeout/output).

Flip `AppContainerBackend::is_available()` to `false` so
`pick_backend()` (and `execute_local_jail`'s fallback branch) route
through `NoopBackend`, which returns a real waitable `Child`.
Reverting to `true` should land in the same commit that adds the
`OwnedHandle -> std::process::Child` bridge.
@CodeGhost21

Copy link
Copy Markdown
Contributor Author

Fixed in 2b6aaf7.

Extracted select_shell_program_and_args(command) -> (&'static str, [String; 2]) — one place holds the Windows/bash/sh branching, build_tokio_command and build_std_command each drop to two lines and only differ in the Command type they construct. pipefail wrapping and the cfg!(windows) / bash_path() policy are unchanged.

coderabbitai[bot]
coderabbitai Bot previously approved these changes Jul 9, 2026
…broke Linux clippy

`voice/dictation_listener.rs` still references `hotkey::parse_hotkey`,
`hotkey::start_listener`, `ActivationMode::{Push,Tap}`, and
`HotkeyEvent::{Pressed,Released}` inside the
`#[cfg(not(target_os = "macos"))]` `start_rdev_listener` path, but
tinyhumansai#4720 removed the top-level `use crate::openhuman::voice::hotkey::{
self, ActivationMode, HotkeyEvent};` as "unused" — a bad rustc read
that only shows up when the non-macOS branch is compiled. The Linux
`Rust Quality (fmt, clippy)` job on `ci-lite.yml` has been red on
every merge to main for the last five runs as a result, blocking all
PRs (including tinyhumansai#4723 this ships in).

Restore the import behind a matching
`#[cfg(not(target_os = "macos"))]` gate so rustc doesn't emit an
`unused_imports` warning on macOS, keeping the previous macOS-clean
behaviour while unblocking the Linux clippy lane.

Unblocks tinyhumansai#4723 / tinyhumansai#4705.
/// belong here, so both `Command` flavours stay in lockstep.
fn select_shell_program_and_args(command: &str) -> (&'static str, [String; 2]) {
if cfg!(windows) {
("cmd", ["/C".to_string(), command.to_string()])

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Major (correctness, Windows) — cmd /C receives the command via Command::arg(), whose MSVCRT-style quoting mangles embedded double-quotes that cmd.exe does not understand.

Rust's std/tokio Command::arg on Windows quotes any arg containing whitespace and escapes interior " as \" (the CommandLineToArgvW convention). cmd.exe does not parse \" — it only toggles quote state on bare ". So a command handed to cmd /C as a single arg is re-parsed by cmd with the wrong quote boundaries.

Two consequences:

  1. Redirect wrap always breaks. wrap_with_output_redirection (below) always produces ... > "out" 2> "err". After arg() escaping the line becomes cmd /C "echo hi > \"out\" 2> \"err\"". cmd sees the > inside a quoted span, treats it as a literal, and performs no redirection — the .sandbox_stdout/.sandbox_stderr files stay empty and execute_local_jail returns empty output. This is exactly the SandboxMode::Sandboxed → Local path the tinyflows code node uses on Windows, so the PR's headline Windows fix silently returns no output there.
  2. Any command containing " breaks even on the non-redirect path (echo "hello world" → cmd runs echo \hello world\).

Fix: pass the payload to cmd verbatim via raw_arg on Windows:

// build_tokio_command / build_std_command, Windows arm
use std::os::windows::process::CommandExt;
let mut cmd = Command::new("cmd");
cmd.raw_arg("/C").raw_arg(command); // no MSVCRT re-quoting

Since select_shell_program_and_args returns (program, [String;2]) and raw_arg is Windows-only, this likely means branching the two build_* fns on cfg!(windows) rather than sharing the tuple helper. Please verify on real Windows — the current unit tests only assert the arg vector, never spawn cmd with a quoted/redirected command.

// commands and pipelines. Double-quote paths so backslashes,
// spaces, and `(` / `)` inside typical Windows workspace paths
// (e.g. `C:\Program Files (x86)\…`) don't break parsing.
format!(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Major (follow-on to the raw_arg comment above). This wrap is the concrete place the quoting bug bites: the produced string always contains double-quoted paths (> "…" 2> "…"), and it is spawned via build_std_commandcmd /C <arg> whose Command::arg() escaping puts the >/2> inside a cmd quote-span, so no redirection happens and the stdout/stderr temp files are never written. On Windows this path is reached for SandboxMode::Sandboxed local agents (and the tinyflows code node). Blocking factor is the same raw_arg fix on the Windows arm; additionally, the Windows branch here formats the redirect paths with .display(), which on real Windows yields backslash paths (C:\\Users\\…) — those need to survive verbatim too, reinforcing the raw-arg approach. Worth an execution test (execute_local_jail on Windows CI actually reading back captured output), not just the current string-equality assertion.

} else if let Some(bash) = bash_path() {
(
bash,
["-lc".to_string(), format!("set -o pipefail\n{command}")],

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Question / minor — behavior change on the sandbox path: sh -c → login shell bash -lc.

Previously execute_unsandboxed and execute_local_jail used sh -c <cmd> (non-login). Routing through platform_shell now runs bash -lc "set -o pipefail\n<cmd>" on Unix. Two side effects worth confirming are intended:

  • -l sources login profile scripts (/etc/profile, and since HOME is in SANDBOX_ENV_PASSTHROUGH, ~/.bash_profile/~/.profile). Any echo/banner in those files lands in the captured .sandbox_stdout for the jail path, corrupting the tool's reported output.
  • Login-profile sourcing adds per-call latency and can rewrite PATH out from under the curated SANDBOX_ENV_PASSTHROUGH set.

If parity with the old non-login sandbox behavior is desired, consider a non-login variant (-c with an explicit set -o pipefail prefix) for the sandbox callers, or document that the sandbox intentionally adopts NativeRuntime's login-shell semantics.

// AppContainer is *reachable* on every supported Windows (8+ / Server
// 2012+), but the spawn path in `spawn_in_container` can't yet return
// a `std::process::Child` — after `CreateProcessW` succeeds it must
// reply `Err(io::ErrorKind::Unsupported)` (see TODO at

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick. The comment points at spawn_in_container, ~line 279 but the Err(Unsupported) return / TODO is at line ~287–295 in the current file. Minor drift; consider referencing the function name only (line numbers rot) — e.g. "see the Err(Unsupported) return at the end of spawn_in_container".

@M3gA-Mind M3gA-Mind left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR #4723 — fix(shell): route sandbox execution through platform_shell so Windows spawns cmd.exe (#4705)

Walkthrough

Consolidates the "which shell binary do we spawn?" decision into a new src/openhuman/agent/platform_shell.rs shared by NativeRuntime::build_shell_command and both sandbox execution paths (execute_unsandboxed, execute_local_jail). Windows moves from the previously-hardcoded sh (sandbox) / PowerShell (native) to cmd.exe /C; Unix keeps bash -lc "set -o pipefail…" with sh -lc fallback. It also flips AppContainerBackend::is_available() to false (fixing the orphan-spawn P1 that the shell fix would otherwise expose) and restores a cfg-gated hotkey import to unbreak Linux clippy.

Overall this is a well-scoped, well-tested fix that correctly resolves the reported sh-not-found spawn failure on Windows for the default (None-backend) shell path, and the AppContainer + dictation sub-fixes are sound. The main gap is that the Windows cmd /C <command> invocation is built with Command::arg(), whose MSVCRT quoting is not understood by cmd.exe — which breaks the local-jail redirect capture and any command containing quotes. Leaving this as COMMENT for the maintainer to weigh; not blocking-by-fiat.

Changes

File Summary
src/openhuman/agent/platform_shell.rs New module: build_tokio_command / build_std_command / wrap_with_output_redirection / bash_path, single-source shell selection + tests.
src/openhuman/agent/mod.rs Register pub mod platform_shell with doc comment.
src/openhuman/agent/host_runtime.rs NativeRuntime::build_shell_command delegates to platform_shell; bash_path moved out; tests use tempdirs + platform-aware asserts.
src/openhuman/sandbox/ops.rs execute_unsandboxed / execute_local_jail swap Command::new("sh") for platform_shell; new cross-OS regression tests; Unix-only gates on the /tmp-based tests.
src/openhuman/cwd_jail/windows.rs AppContainerBackend::is_available()false until a waitable Child bridge lands; regression test.
src/openhuman/voice/dictation_listener.rs Restore #[cfg(not(target_os = "macos"))] hotkey import removed by #4720 (Linux clippy).

Actionable comments (4)

⚠️ Major

1. src/openhuman/agent/platform_shell.rs:56-66 & :81-103cmd /C <cmd> built with Command::arg() mangles quotes cmd.exe can't parse

Rust's Windows Command::arg escapes interior " as \" (the CommandLineToArgvW convention); cmd.exe does not honor \". So:

  • The local-jail redirect wrap is always affectedwrap_with_output_redirection always emits > "out" 2> "err"; after arg() escaping cmd sees the > inside a quoted span and performs no redirection, leaving .sandbox_stdout/.sandbox_stderr empty. This is the SandboxMode::Sandboxed→Local path (tinyflows code node) — the very thing the PR aims to fix on Windows.
  • Any command containing " is affected on the non-redirect path too.

Suggested change (Windows arm of the build_* fns):

use std::os::windows::process::CommandExt;
let mut cmd = Command::new("cmd");
cmd.raw_arg("/C").raw_arg(command); // verbatim — no MSVCRT re-quoting

Please verify on real Windows; current tests only assert the arg vector, never spawn cmd with a quoted/redirected command. (Confidence: high on the escaping mechanism; the exact cmd breakage should be confirmed on hardware.)

💡 Question / minor

2. src/openhuman/agent/platform_shell.rs:60-62 — sandbox paths now use a login shell

execute_unsandboxed / execute_local_jail previously ran sh -c (non-login); they now run bash -lc. With HOME in SANDBOX_ENV_PASSTHROUGH, -l sources ~/.bash_profile//etc/profile, whose banner output would pollute captured .sandbox_stdout and whose PATH edits override the curated passthrough set. Confirm intended, or use a non-login -c with an explicit set -o pipefail prefix for the sandbox callers.

Nitpicks (2)

  • src/openhuman/cwd_jail/windows.rs:119 — comment says ~line 279 but the Err(Unsupported) return is ~287-295; prefer referencing the function name, line numbers rot.
  • src/openhuman/agent/platform_shell.rs:178-193 (output_redirection_wraps_per_platform) — the Windows assertion uses forward-slash /tmp/... paths, so it never exercises realistic backslash Windows paths (where the quoting issue in #1 surfaces). Consider a C:\...-style path plus an actual execution test.

Verified / looks good

  • AppContainer P1 (Codex) correctly resolved. is_available() → false routes Windows Local sandbox through NoopBackend, which returns a real waitable Child; the previously-hardcoded sh had masked this because it failed inside CreateProcessW. Regression test pins the invariant. Good backend-level fix.
  • Dictation cfg gate is correct. The hotkey/ActivationMode/HotkeyEvent import and its only usage (start_rdev_listener) are both #[cfg(not(target_os = "macos"))], so this compiles on all three OSes and unbreaks Linux clippy.
  • Unix behavior for the None-backend / native path is preserved (bash+pipefail, sh fallback), and the pipefail regression test is retained via platform_shell::bash_path().
  • Env scrub, CWD guard (ensure_usable_cwd), hide_window, timeout semantics all untouched.
  • CodeRabbit's module-structure comment was reasonably rebutted (22 sibling utility files already in agent/) and withdrawn — agreed.

Posted as a COMMENT — approve/merge is the maintainer's call.

@coderabbitai coderabbitai Bot added the agent Built-in agents, prompts, orchestration, and agent runtime in src/openhuman/agent/. label Jul 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agent Built-in agents, prompts, orchestration, and agent runtime in src/openhuman/agent/. bug rust-core Core Rust runtime in src/: CLI, core_server, shared infrastructure.

Projects

Status: Todo

Development

Successfully merging this pull request may close these issues.

bug: Shell tool fails on Windows — defaults to bash/sh which is unavailable, cmd /c fallback not automatic

2 participants