fix(cli): spawn agent shadow commands through a shell on Windows#730
Open
Koh0920 wants to merge 2 commits into
Open
fix(cli): spawn agent shadow commands through a shell on Windows#730Koh0920 wants to merge 2 commits into
Koh0920 wants to merge 2 commits into
Conversation
The self-healing agent (run_v03_lifecycle_steps) ran shadow lifecycle
commands via `Command::new("npm")` directly with a `\\?\` verbatim
working directory. Both break on Windows:
- `std::process::Command` only appends `.exe`, never consults PATHEXT,
so it cannot resolve the `npm.cmd` / `bun.cmd` / `pnpm.cmd` shims and
the process fails to spawn (E999 / exit code 2 / process_crash).
- `std::fs::canonicalize` returns a `\\?\` extended-length path which
cmd.exe and the npm shims reject as a current directory.
Align `run_shadow_command` with the proven `run_lifecycle_shell_command`
path: wrap the (still allowlist-validated) command in `cmd /D /S /C` on
Windows and `sh -c` elsewhere. Switch `absolute_path` to
`dunce::canonicalize` so the verbatim prefix never reaches downstream
spawns. Adds a regression test guarding the prefix strip.
Review follow-up: the previous revision validated the command with
parse_safe_command but then discarded the parsed tokens and handed the
raw string to `cmd /C` / `sh -c`. That reintroduced shell interpretation,
so `npm install & evil`, `npm install $(...)` and backtick substitution
would execute despite the allowlist — a shell-injection regression.
Keep execution strictly argv-based instead:
- `build_shadow_spawn` validates + allowlists, then resolves the program
to a concrete path. On Windows `resolve_shadow_program` resolves the
`.cmd`/`.exe` shim via `which` (std cannot find a bare-name `.cmd`); std
then spawns the resolved `.cmd` through cmd.exe with each argv element
individually escaped. No shell ever sees the command string.
- Harden `parse_safe_command` to also reject single `&`, backtick, `$(`,
`${` (defense in depth across cmd.exe and POSIX sh).
Tests: parse_safe_command_rejects_single_ampersand,
parse_safe_command_rejects_command_substitution,
build_shadow_spawn_keeps_argv_and_rejects_shell_ops, plus an accept-path
test for plain package-manager commands. Also makes the pre-existing
session_store artifact-dir assertion separator-agnostic so it passes on
Windows.
Contributor
Author
Addressed: shell-injection regression (blocker)You're right — handing the raw string to Changes in
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Fixes a Windows-only crash when the self-healing agent (
run_v03_lifecycle_steps) tries to refresh dependencies in the shadow workspace. Onato-storecapsule runs the agent reported:→
exit code 2/process_crash, no readiness signal.Root cause
AgentSessionExecutor::run_shadow_commandspawned the package manager differently from the proven lifecycle runner. Two Windows-specific defects:std::process::Command::new("npm")cannot launch thenpm.cmdshim. On Windows the std spawner only appends.exeand never consultsPATHEXT, sonpm/pnpm/bun(all.cmdshims) fail to spawn. The working provision path (run_lifecycle_shell_command) avoids this by going throughcmd /D /S /C.\?\verbatim working directory.absolute_pathusedstd::fs::canonicalize, which on Windows returns a\?\extended-length path.cmd.exeand the npm shims reject it as a current directory.Fix
run_shadow_commandnow wraps the command incmd /D /S /C "<cmd>"on Windows andsh -c "<cmd>"elsewhere — mirroringrun_lifecycle_shell_command. The allowlist + shell-operator validation (parse_safe_command) is preserved; only the spawn mechanism changes.absolute_pathswitches todunce::canonicalizeso the\?\prefix never reaches downstream spawns.dunce = "1.0"as a direct dependency (already present transitively in the lockfile).absolute_path_does_not_return_verbatim_prefix.Testing
cargo check -p ato-cli— passes, no new warnings.cargo test -p ato-cli --lib agent::tests::absolute_path_does_not_return_verbatim_prefix— passes.