fix(parallels): send guest prep scripts over stdin - #2401
saariuslystoned wants to merge 3 commits into
Conversation
|
🦞👀 Pull request received. I will update this pull request when review starts. ClawSweeper review completeClawSweeper finished reviewing this revision. The review result is being finalized. |
|
Codex review: blocked before merge. Reviewed September 21, 2026, 3:56 AM ET / 07:56 UTC (Revision 5). ClawSweeper reviewWhat this changesThe PR streams Parallels guest preparation and SSH-key installation scripts through stdin, protects that input from child commands, and adds regression tests and documentation. Merge readiness⛔ Blocked before merge - 3 items remain The fix remains necessary, and no introduced correctness defect was found. The latest documentation establishes fail-fast intent; Linux compatibility coverage and the merge conflict remain unresolved. Priority: P2 Review scores
Verification
How this fits togetherCrabbox prepares cloned Parallels VMs before using them for SSH-based execution. These scripts install the lease key and prepare the guest through local prlctl or SSH to a remote Parallels host. flowchart TD
A[Clone and lease settings] --> B[Generate guest scripts]
B --> C{Parallels host location}
C --> D[Local prlctl]
C --> E[SSH to remote prlctl]
D --> F[Guest shell reads stdin]
E --> F
F --> G{Preparation succeeds}
G --> H[SSH readiness or lease cleanup]
Decision needed
Why: The intended fail-fast behavior is now documented, but the previous Linux compatibility requirement remains unanswered and needs an explicit evidence-risk decision. Before merge
Agent review detailsSecurityNone. Review metrics
Root-cause clusterRelationship: Members:
Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything. Merge-risk optionsMaintainer options:
Technical reviewBest possible solution: Retain the shared stdin transport and explicit best-effort exceptions, with Linux fresh-template and upgrade compatibility demonstrated or its coverage limit explicitly accepted. Do we have a high-confidence way to reproduce the issue? Yes: current main retains the argv transport, and the supplied native control demonstrates swallowed shell failures. This review inspected that path without independently executing it. Is this the best way to solve the issue? Yes: stdin delivery follows the existing SSH bootstrap pattern and directly avoids argv reconstruction; protecting child input addresses the resulting script-consumption hazard. AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning medium; reviewed against 64cf0ed6adc3. LabelsLabel justifications:
EvidenceWhat I checked:
Likely related people:
Rank-up movesOptional improvements that raise the rating; they are not merge blockers.
Rating scale
Overall follows the weaker of proof and patch quality. Workflow
HistoryReview history (4 earlier review cycles)
|
7007df0 to
e29afd0
Compare
`prlctl exec` does not preserve argv boundaries. It joins its arguments into one string and re-parses that string with a shell inside the guest, so the preparation scripts crabbox passes as a single argv element after `/bin/sh -lc` lose their word boundaries: the guest effectively runs `/bin/sh -lc set` with `-eu` as positional arguments, and the remainder of the script executes in the guest's outer shell with abort-on-error never in effect. The runner is mocked here, so these tests cannot observe the guest-side reconstruction. They pin the part crabbox controls -- both scripts must reach the guest shell on stdin, which `prlctl exec` preserves verbatim, and must not appear in argv at all -- plus the stdin-consumption rule that stdin delivery imposes on every child either script runs, including the SSH-listener probe added by openclaw#2399. Both fail on this commit's parent. Refs openclaw#2396 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`prlctl exec` joins its arguments into one string and re-parses that string with a shell inside the guest, so the preparation scripts passed as a single argv element after `/bin/sh -lc` never arrived intact: the guest ran `/bin/sh -lc set` with `-eu` as positional arguments and executed the remainder in its own outer shell, with abort-on-error never in effect. Guards the scripts are written to rely on, such as `[ -x "$kickstart" ]` in the macOS desktop branch, fell through instead of aborting. Deliver both scripts on stdin, which `prlctl exec` preserves verbatim, the same shape `BootstrapMacOSOverSSH` already uses. `prlctlWithStdin` carries the reader through both routes -- local prlctl and the remote route where ssh forwards stdin to prlctl on the Parallels host -- and leaves host selection, key handling, the remote PATH prefix and the password-filtered child environment untouched. Stdin delivery means any child that reads stdin swallows the rest of the script while the shell still exits 0, so every child in both scripts now takes its stdin from /dev/null. openclaw#2387 established this for the macOS Node children; the Linux branch reaches stdin for the first time here and `apt-get` is the obvious offender. The readiness gate matters just as much, along with the two probes that now guard it: the desktop VNC check and the macOS SSH-listener check from openclaw#2399. All three run mid-script. The `nc` inside the generated `crabbox-ready` helper needs no guard -- the prep script invokes that helper with stdin already on /dev/null. Commands that are intentionally non-fatal keep their `|| true`. Fixes openclaw#2396 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
e29afd0 to
47459e3
Compare
|
@clawsweeper re-review Rebased onto current |
|
🦞🧹 I asked ClawSweeper to review this item again. Re-review progress:
|
Document the behavior restored by openclaw#2401 and credit @saariuslystoned in the Unreleased changelog.
Document the behavior restored by #2401 and credit its contributor in the Unreleased changelog.
Document the behavior restored by #2401 and credit its contributor in the Unreleased changelog.
* fix(parallels): send guest prep scripts over stdin Integrate the contributor fix from #2401 for #2396. Preserve the original production code and regression tests unchanged. Co-authored-by: Bobby Bones <saariuscrypto@gmail.com> * docs(parallels): record fail-fast guest preparation Document the behavior restored by #2401 and credit its contributor in the Unreleased changelog. --------- Co-authored-by: Bobby Bones <saariuscrypto@gmail.com>
|
Landed via #2431: your commits (authorship intact) rebased onto |
Problem
prlctl execdoes not preserve argv boundaries. It joins its arguments into one string and re-parses that string with a shell inside the guest.EnsureGuestReadyandInstallSSHKeyboth handed the whole generated POSIX script over as a single argv element after/bin/sh -lc, so the guest effectively ran/bin/sh -lc setwith-euas positional arguments — consuming line 1 — and executed every remaining line in its outer shell, withset -eunever in effect.Guest preparation therefore ran with abort-on-error disabled, defeating guards the scripts are written to rely on, such as
[ -x "$kickstart" ]in the macOS--desktopbranch.Fixes #2396.
What this does
Deliver both scripts on stdin, which
prlctl execpreserves verbatim — the shapeBootstrapMacOSOverSSHalready uses (sudo -n /bin/sh -s).prlctlWithStdincarries the reader through both routes.prlctlis now a thin wrapper over it, so host selection, key handling, the remotePATH=prefix and the password-filtered child environment are untouched on the remote/proxy route; ssh forwards stdin toprlctlon the Parallels host.Stdin delivery imposes a rule on the scripts themselves: any child that reads stdin swallows the remainder of the script while the shell still exits 0. #2387 established
</dev/nullfor the macOS Node children; this extends it to every child in both scripts. Three groups matter most:apt-get update, bothapt-get installcalls,systemctl daemon-reload/enable --now./usr/local/bin/crabbox-ready </dev/null >/tmp/crabbox-ready.log 2>&1, which runs mid-script: anything it consumed would be lost.nc -z 127.0.0.1 5900,systemctl is-active) and, after the rebase, the macOS SSH-listener check from fix(parallels): verify the macOS SSH listener during guest preparation #2399.Commands that are intentionally non-fatal keep their
|| true.Commits
bc6ea35747459e3eRebased onto current
main(2ad31a5a); the rebase was clean, and on itgo vet ./...is clean andgo test -race ./internal/cli -run 'TestParallels|TestManagedMacOSNodeBaseline|TestMacOSNode'andgo test -race ./internal/providers/parallels/both pass. Previously rebased ontof7bed55dafter #2399 merged. That PR added the macOS SSH-listener probecrabbox_ssh_listening, which runs mid-script inside the readiness gate, so itsnc -z 127.0.0.1 "$port"is now covered by the same</dev/nullrule and asserted by the regression test. Thencinside the generatedcrabbox-readyhelper needs no guard: the prep script already invokes that helper with stdin on/dev/null.The regression commit fails on its parent, and only through its own two tests — the rest of
TestParallelsis green there.The runner is mocked, so the tests cannot observe guest-side reconstruction; they pin the transport crabbox chooses, which is the part that is wrong. Native evidence below covers the guest side.
Verification
In tree
Native, on a real Apple-silicon Parallels macOS 26.5.2 guest
Run twice: once against the pre-rebase head (the transport probes below), and again against the rebased head
e29afd08, so the combination with #2399 listener probe is covered rather than assumed.Disposable full clone of the host's macOS template, deleted afterwards; template left stopped with all 7 snapshots and its current snapshot unchanged.
The mechanism, both shapes, same guest. Old argv shape —
set -euis swallowed, the rest leaks, exit 0:New stdin shape —
set -euis in effect, nothing leaks:Argv flattening is inherent to
prlctl execand is not fixed by moving the script to stdin — it is simply no longer in the way. Positional arguments still split:Exit status propagates (
exit 7→exit=7), and a 60 KB script over stdin arrives whole.The real generated scripts, dumped from this branch and piped to
prlctl exec <vm> /bin/sh -son a fresh clone:install-keyexit 0,ensure-readyexit 0.End to end, this branch's binary (
GOOS=darwin GOARCH=arm64):After the rebase, on a fresh full clone, the generated scripts from
e29afd08piped toprlctl exec <vm> /bin/sh -s:Template left stopped with all 7 snapshots and its current snapshot unchanged; every clone deleted.
An unrelated flake I hit, with a control
Some runs on that host fail at
prlctl execwithPrlJob_GetRetCode: Invalid argument/PrlJob_GetResult: Invalid argument, at whichever exec comes first. I ranorigin/mainbuilt the same way as a control, alternating. Pre-rebase: this branch 2 pass / 2 fail,origin/main1 pass / 2 fail. Post-rebase againstf7bed55d: both binaries passed round 1 and both failed round 2, on the same round. It is pre-existing and not introduced here, but worth knowing if CI or another native run trips on it.Native, remote-host route (
--parallels-host)Earlier revisions of this body listed the remote route as unexercised; this section covers it. Crabbox ran on an operator Mac and drove a remote Apple-silicon Parallels host (macOS 26.5.2,
prlctl27.0.1) over--parallels-host, cloning a macOS template with--parallels-clone-mode full. The branch is rebased onto currentmain(2ad31a5a). The control is an unmodifiedmainbinary built the same way.While each lease ran, a sampler on the Parallels host recorded the argv of every
prlctl execprocess, so the transport is observed on the host rather than inferred.Fault injection without touching the template.
CRABBOX_WORK_ROOT=/var/lib/crabbox/ssh.usernameis a path thatInstallSSHKeycreates as a regular root-owned file just before guest prep. InEnsureGuestReady,mkdir -p "$work_root"then fails mid-script. The finalcrabbox-readystill passes, becausetest -won a file is true for root. The fault can only be caught ifset -euis really in effect.prlctl exec <vm> /bin/sh -sGUEST_OK, exit 0prlctl exec <vm> /bin/sh -sparallels guest prep: exit status 1: mkdir: /var/lib/crabbox/ssh.username: File existsmainprlctl exec <vm> /bin/sh -lc set -eu\012user=…\012work_root=…(whole script in argv)provisioned, then fails later ascreate remote workdir: exit status 1Happy path, this PR:
Fault, this PR. It fails at the step that failed, and crabbox deletes the clone:
Fault,
main. The failure is swallowed and surfaces later under the wrong name:So ssh does forward stdin to the remote
prlctlintact. Both scripts reached the guest whole: the happy path provisioned and ran, andset -euaborted the fault run at the right line.One PR fault attempt died with the pre-existing
PrlJob_GetRetCode: Invalid argumentflake described above, while another VM was running on the host. The retry is the run shown.Cleanup. Every clone was deleted, with a readback of
prlctl list --allafter each run. The template was left stopped, with the same snapshot count and the same current snapshot as before, compared against a pre-run readback.Linux: there is no Linux Parallels template on this host either, so the Linux
apt-getpath still has no native coverage. That limit is unchanged, and accepting it is the maintainer's call.Scope limits
Please read the native evidence as covering exactly this and no more:
prlctl) route and the remote route (--parallels-host, section above), both on a macOS guest, on one Apple-silicon host and one template.apt-getstdin-consumption path — the single largest behavioral surface this change opens — has no native coverage at all. It is asserted in tree by string match only. Dropping-lalso means a Linux guest now runs the script under/bin/shproper (dash on Debian/Ubuntu) rather than whatever outer shell the flattened form landed in;TestParallelsEnsureReadyScriptParsesUnderPOSIXShellcovers that syntactically, and on a Linux CI runner that check is dash.set -eugenuinely taking effect is the point of the fix, but it means preparation can now abort where it previously ran on. Anything meant to stay non-fatal must carry|| true; I audited both scripts and left the existing ones alone, but a guest-side failure that used to be silently survivable will now surface as a failed prep.Contributor PR — no
CHANGELOG.mdedit, perAGENTS.md.🤖 Generated with Claude Code