Uh oh!
There was an error while loading. Please reload this page.
fix(installer): escape inner quotes in Invoke-BoundedProcess arg builder (backend#2455) - #845
Conversation
…der (backend#2455) $psi.Arguments is one flat command line, so each arg has to survive CommandLineToArgvW re-splitting it back into argv. The old joiner wrapped whitespace-bearing args in quotes but never escaped an inner `"`, so any arg carrying BOTH a space and a quote (and even a quote with no space, which took the raw pass-through branch) reached the child with its quotes silently consumed and merged into adjacent tokens. #817 dodged this for one call site by never passing a quoted arg; this fixes the general helper. - Add ConvertTo-Win32Arg, which follows the exact CommandLineToArgvW/MSVCRT rules: escape `"` as \", double a run of backslashes before a quote (2N+1) and a trailing run before the close quote (2N), and leave a safe arg untouched. Invoke-BoundedProcess now delegates every arg to it. - Drop the fragile `^".*"$` "already-quoted, leave alone" escape hatch and its one dependent call site: Set-NodeGpuCapacity now passes $patchFile raw and lets the helper quote it (a spaced temp path was the only reason it self-quoted). Swept all ~27 call sites; the env-derived docker-login username is the other arg that can now carry a quote safely. - Replace the source-guard tests that pinned the buggy behavior with golden encodings plus a round-trip test (whitespace, embedded quote, whitespace + quote, empty string, backslashes-before-quote, trailing backslash) that re-splits via a from-spec CommandLineToArgvW parser and, on Windows, the real shell32 API — asserting each arg comes back as one original token. - Regenerate scripts/manifest.sha256 for the edited installer. Verified locally with pwsh 7.6.5 + Pester 6.1.0: install-k8s suite 765 passed / 0 failed / 14 skipped; installer-parity/install/telemetry 158/0; gen-manifest + check-drift + installer-parity bats green; manifest drift gate clean. Found by @saadqbal during client#817 review. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
LukasWodka
left a comment
There was a problem hiding this comment.
The escaping work looks right and the CommandLineToArgvW rules are correctly stated — the 2N+1 before a quote, 2N on a trailing run, backslashes left literal elsewhere, "" for the empty string. Sweeping all ~27 call sites and removing the ^".*"$ self-quoting hatch so callers pass raw args is the right direction; a hatch like that is how the next caller re-introduces the bug.
But Pester (windows-latest) is red — 939 passed, 1 failed — and the one failure is the test that matters most here:
[-] the encoder agrees with the real shell32!CommandLineToArgvW (Windows only)
It is not an encoding disagreement. It is a harness bug, and it means that cross-check has never verified anything. I reproduced it locally; detail inline.
Why this is worth a round rather than a follow-up: the primary evidence for the encoder is a round-trip against your own from-spec reimplementation of CommandLineToArgvW. That is two artifacts written from the same reading of the same spec, so agreement between them is self-consistency, not correctness — a shared misreading passes both. The Windows cross-check against the real shell32 export is precisely what breaks that circularity, and it is the one that has never executed. Everything green in the suite today is compatible with the encoder being wrong in exactly the way the cross-check exists to catch.
That is CLAUDE.md rule 5's "an inert mutation and good coverage look identical in a log", one level up: an inert oracle. Worth fixing while the reason it went unnoticed is fresh — a macOS run skips the Windows-only block, so a local 765 passed / 0 failed looks complete and isn't.
Nothing else from me. Fix the harness, let the real API confirm the encoder, and this should be straightforward.
Uh oh!
There was an error while loading. Please reload this page.
…ases (backend#2455) The Windows-only "encoder agrees with real shell32!CommandLineToArgvW" test built its cases as @((,@(...)), ...) (comma-separated), which nests each case one level: $argv iterated as an Object[]-of-Object[], so ConvertTo-Win32Arg was handed an array and threw ParameterBindingArgumentTransformationException before any comparison ran. macOS skips the block, so a local "765 passed" hid it while windows-latest went red (939 passed / 1 failed) and this cross-check -- the one that breaks the circularity of the from-spec reimplementation -- never actually verified the encoder (LukasWodka on #845). Switch to the newline-separated ,@(...) shape the round-trip test already uses so each $argv is a flat [string[]]. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
aptracebloc
commented
Aug 26, 2026
@LukasWodka — addressed on The Windows-only cases were built as Fixed by switching to the newline-separated I can't execute the Windows block locally (no shell32), so — drafted with Claude Code |
Uh oh!
There was an error while loading. Please reload this page.
LukasWodka
left a comment
There was a problem hiding this comment.
Re-reviewed d028e981. The nesting fix is correct — I verified both shapes on pwsh 7.5.2:
OLD @((,@('a b"c')), …) -> elem=Object[] ParameterBindingArgumentTransformationException (all cases)
NEW @(\n ,@('a b"c')\n …) -> elem=String ok, empty string included
So the cross-check now genuinely runs — 38 ms this time rather than dying at 21 ms.
And it's still red, which is the interesting part: there's a second harness bug underneath the first, and I don't think the encoder is at fault.
The encoder looks right. I ran all five cases against the MSVCRT rules by hand:
a b"c -> "a b\"c" quote escaped, no backslash run
a\"b -> "a\\\"b" 2N+1 = 3 backslashes before the quote
C:\a b\ -> "C:\a b\\" trailing run doubled so the close quote survives
(empty) -> "" present-but-empty
x -> x untouched, no whitespace or quote
Each of those parses back to its input under the documented rules. I can't run the real shell32 from here, so that's a hand-check against the spec rather than against ground truth — but it means the failure needs another explanation.
Here it is. Line 5442 discards its own input:
$real=@(realArgv ("prog.exe "+$line) |Select-Object-Skip 1)realArgv ends with return ,@($r) — the comma is there deliberately, to stop a single-element result collapsing to a scalar. But piping a function call directly doesn't unwrap that wrapper: the pipeline receives one item, which is the entire array, and -Skip 1 skips it. Measured:
realArgvShape @('prog.exe','x') | ForEach-Object { … }
one pipeline item: type=Object[] count=2 ← the whole argv, as a single object
So $real.Count is 0 for every case, and $real.Count | Should -Be $argv.Count fails on the first one. Not empty-string-specific — general.
Two fixes, both verified here:
# assign first (assignment unwraps the ,@() wrapper), then pipe$all= realArgv ("prog.exe "+$line)
$real=@($all|Select-Object-Skip 1)# or skip the pipeline entirely$all= realArgv ("prog.exe "+$line)
$real=@($all[1..($all.Count-1)])BROKEN pipe the call directly : count=0
FIXED assign first, then pipe : count=1 first=[a b"c]
FIXED empty-arg case : count=1 first=[]
FIXED slice form : count=1 first=[]
Both handle the empty argument, which is the one I'd have expected to be fragile.
Worth stepping back for a second, because this is the third layer of the same thing.
The round-trip test compares the encoder against your own from-spec reimplementation — self-consistency, so a shared misreading passes. The shell32 cross-check exists to break that circularity, and it has now been inert twice for two different reasons, both invisible on macOS where the block is skipped. A green local 765 passed has meant nothing about this check on either occasion.
Once it's actually comparing, please mutation-prove it: change 2N+1 to 2N in ConvertTo-Win32Arg and confirm this test reddens. That's the only way to tell a working oracle from a third quiet failure — and given the history here, I'd want to see it rather than assume it.
My change-request stands on the red check, not on the encoder.
…ckend#2455) With the case array fixed, the Windows-only real-shell32 oracle finally ran and caught a harness bug: realArgv returned ,@($r) and the caller piped it through | Select-Object -Skip 1, which dropped a trailing empty argument -- so the empty """" case saw 0 recovered tokens instead of 1 (windows-latest: 939 passed / 1 failed). Collect into a List[string] and return via the ,$arr idiom, then assign (not pipe) and slice off argv[0] by index, which preserves empty and 0/1-element results. Verified the array logic on the empty and mid-empty cases. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
aptracebloc
commented
Aug 26, 2026
Follow-up on So it's doing precisely what you flagged it should: validating the encoder against the OS, not just my from-spec reimplementation. windows-latest is re-running to confirm green. — drafted with Claude Code |
LukasWodka
left a comment
There was a problem hiding this comment.
Re-reviewed 3d915ed2. The harness fix is correct — I verified it rather than eyeballing it. Simulating the new realArgv shape (typed List[string] → ToArray() → ,$arr) plus the index slice on pwsh 7.5.2:
parsed=2 -> full=2 got=1 expected=1 OK [a b"c]
parsed=2 -> full=2 got=1 expected=1 OK [] <- trailing empty survives
parsed=3 -> full=3 got=2 expected=2 OK [x] []
parsed=1 -> full=1 got=0 expected=0 OK <- argv[0]-only, no index error
got[0] is null? False got[0] type: String got[0] -eq '' : True
The empty element comes back as a real String rather than $null, which is what Should -BeExactly '' needs. Both of the shape bugs are closed.
And now the oracle is finally saying something. The failure has changed:
Expected exactly 'a b"c', but got a.
at … install-k8s.Tests.ps1:5464
That's the -BeExactly line, not the count line above it — so $got.Count matched $argv.Count at 1, and $got[0] came back as a. Which means the real CommandLineToArgvW returned exactly two arguments for that command line: prog.exe and a.
That's what you'd get from the command line prog.exe a … — the quoting gone before the API ever saw it. It is not what prog.exe "a b\"c" produces: I traced that by hand against the MSVCRT rules and it yields a b"c in one argument, and the encoder does emit "a b\"c" for that input (I ran it: in=[a b"c] -> out=["a b\"c"]).
So the two live candidates are:
- The encoder output isn't reaching the command line intact — something between
ConvertTo-Win32Argand the"prog.exe " + $lineconcatenation is losing the quotes in the Windows run. - The real API genuinely diverges from the documented rules on this input — which is exactly the possibility this cross-check exists to surface, and would be the most interesting outcome.
The assertion can't tell those apart, and that's fixable in the same edit. Right now it reports the decoded value and nothing about what was fed in:
$got.Count| Should -Be $argv.Count-Because "line was [$line]; argv=[$($full-join'|')]"for ($k=0; $k-lt$argv.Count; $k++) {
$got[$k] | Should -BeExactly $argv[$k] -Because "line was [$line]; argv=[$($full-join'|')]"
}One run with that and you'll know which side is wrong, instead of another round of inference from a one-line message. Given this check has been inert twice and is now red for a third distinct reason, I'd want it to say what it saw.
Worth noting the check is doing its job. The round-trip test against your own from-spec parser is green; this one is red on the same input. That's precisely the circularity the ground-truth oracle was added to break, and it took three fixes to get it into a state where it could break it. Whatever the answer turns out to be, the check has earned its place.
My change-request stands on the red, not on the harness — that part is done.
…#2455) The Windows-only real-shell32 oracle disagrees on the space+quote case in a way not reproducible on macOS (no shell32), and "got a" alone is not debuggable. Surface arg / encoded line / real shell32 tokens in the failure -Because and a Write-Host so the next windows-latest run shows exactly what CommandLineToArgvW returned. Diagnostic only -- no change to the encoder or the assertions. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
LukasWodka
left a comment
There was a problem hiding this comment.
c43a1dbc — that's the diagnostic, and the Write-Host is better than what I suggested: the dump lands in the log on a pass too, so the next person doesn't have to redden the check to learn what shell32 actually returns.
arg=[…] encoded=[…] shell32=[…] (n=…)
Those three fields are exactly the three that separate "the encoder is wrong" from "the line was mangled before the API saw it", which is the question the previous message couldn't answer.
CI is still running on this head, so the answer isn't in yet. I'll pick it up next pass — nothing needed from you until the run reports.
…backend#2455)
The real-shell32 diagnostic confirmed the encoder is correct: CommandLineToArgvW
returns ["prog.exe","a b\"c"] exactly as intended (shell32=[prog.exe|a b\"c] n=2).
The remaining failure was the harness -- $full[1..($full.Count-1)] collapses to a
SCALAR string when it selects a single element under Windows PowerShell, so
$got[$k] then indexed into that string chars ("got a" for "a b\"c"). Replace the
range slice with an explicit index loop that keeps $got a real array on every
host; verified it returns whole tokens (incl. the empty arg) for 1- and multi-arg
cases. Keeps a concise -Because dumping the real shell32 output for future debug.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
LukasWodka
left a comment
There was a problem hiding this comment.
fe8bf6a2 — the diagnostic paid for itself immediately, and the answer is the good one: the encoder was right all along, shell32 returned the argument intact, and the bug was a third harness defect.
First, a correction I owe you. Last round I said "the harness fix is correct — I verified it rather than eyeballing it." My verification was insufficient, and it was insufficient at exactly the point that mattered. I checked .Count and a joined dump — both of which are identical for a scalar string and a one-element array:
OLD (assign from the if-block):
type : String
.Count : 1 <- 1 either way, which is why my check couldn't tell
got[0] : [a] <- the discriminator I didn't look at
is array? : False
$got[0] was the only property that separates them, and it's the one I skipped. So my "verified" was worth less than it sounded. You found this; I'd have signed it off.
Your diagnosis is right and the loop fixes it:
NEW (explicit index loop):
type : Object[] .Count : 1 got[0] : [a b"c] is array? : True
trailing-empty case: .Count : 2 got[0]=[x] got[1]=[]
One correction to the comment, because it will mislead someone later. It says the slice "collapses to a SCALAR string under Windows PowerShell". It isn't Windows-specific — I reproduced it on pwsh 7.5.2 on macOS. The cause isn't the slice, it's the assignment from an if block: $x = if (…) { @(…) } sends the array to the output stream, which unrolls it, and a single emitted object lands as a scalar. @() inside the block can't prevent that.
The reason macOS looked clean is simply that the whole block is Windows-only and never ran there. Worth fixing the wording, because "safe on pwsh 7" is the belief that puts this pattern back somewhere else.
So the tally on this cross-check: three defects, none in the code under test. Nested case arrays that threw on binding; a pipeline that discarded its input; and an assignment that collapsed the result to a string. Each one made the oracle report something that wasn't about the encoder, and each was invisible on the only platform anyone runs locally.
Once it's green, the mutation is worth more here than usual — flip 2N+1 to 2N in ConvertTo-Win32Arg and confirm this reddens. Three false alarms in, "it passes" is not yet evidence that it can fail.
25 checks still pending on this head. Nothing outstanding from me.
aptracebloc
commented
Aug 26, 2026
The diagnostic confirmed your instinct paid off:
Ready for your re-review; the rest of the matrix is still running. Thanks for holding the line on the oracle — it earned its keep. — drafted with Claude Code |
Uh oh!
There was an error while loading. Please reload this page.
ConvertTo-Win32Arg appended $Arg[$i] (a [char] from string indexing) to the StringBuilder. Under Windows PowerShell 5.1 -- the host the installer relaunches into -- the Append overload binder can bind a [char] to a numeric overload and write the code point instead of the character, corrupting a quoted arg (e.g. a spaced --patch-file); pwsh-7 CI and the golden tests do not see it. Cast to [string] so Append(string) is selected unambiguously on every host. Output is byte-identical on pwsh 7 (golden vectors unchanged). (Bugbot High on #845.) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
aptracebloc
commented
Aug 26, 2026
bugbot run |
LukasWodka
left a comment
There was a problem hiding this comment.
15ff1bf4 — the cross-check earned itself. Three harness fixes to get it running, and the first thing it did once it could actually compare was find a real defect in the code under test, on the exact platform this installer targets, that nothing else could see.
The fix is safe, and I checked the property that matters for a fix like this — that it changes nothing where the code already worked:
runtime: .NET 9.0.6
input old ([char]) new ([string]) same?
[a b"c] ["a b\"c"] ["a b\"c"] True
[a\"b] ["a\\\"b"] ["a\\\"b"] True
[C:\a b\] ["C:\a b\\"] ["C:\a b\\"] True
[] [""] [""] True
[x] [x] [x] True
[p q] ["p q"] ["p q"] True
Byte-identical on every case. And on this runtime $sb.Append('abc'[0]) produces [a] — the binder picks Append(char) correctly here, which is precisely why a pwsh-7 suite could never have caught it.
Append([string]…) is also the right shape independent of the diagnosis. Whatever the binder is doing on 5.1, [string] has exactly one matching overload, so it cannot mis-bind. A fix that removes the ambiguity beats one that argues about which branch the ambiguity resolved to.
Worth writing down what this arc actually demonstrated, because it's a better argument for ground-truth oracles than any amount of principle.
The round-trip test compares the encoder against a from-spec reimplementation — two artifacts from one reading of one spec. It was green throughout. It was green while the encoder wrote code points instead of characters on the installer's stated floor, because the reimplementation was written by someone with the same (correct) understanding of the spec and the bug wasn't in the understanding, it was in the host.
Only the real shell32 export could tell the difference, and getting to the point where it could took: nested case arrays that threw on binding, a pipeline that discarded its input, and an assignment that collapsed the result to a scalar. Three fixes to an oracle, each of which made it report something that wasn't about the encoder — and every one invisible on macOS, where the block is skipped.
So the mutation is worth more here than usual.2N+1 → 2N, confirm it reddens. Three false alarms and one true positive in, "it passes" still isn't evidence it can fail — and this check has just proved it's the only thing standing between the encoder and a silent Windows-only corruption.
21 checks still running on this head, zero open threads. I'll pick it up next pass.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 15ff1bf. Configure here.
Uh oh!
There was an error while loading. Please reload this page.
…d#2455)
The Invoke-BoundedProcess source guard matched the bare name ConvertTo-Win32Arg,
which a comment in the same function body also contains -- so deleting the actual
call would still pass. Match the call expression (ForEach-Object { ConvertTo-Win32Arg $_)
so the guard can detect its own removal. (Bugbot Medium on #845.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
LukasWodka
left a comment
There was a problem hiding this comment.
E2E auth-proxy (squid) went red on 15ff1bf4 — the head I reviewed, no new push. I don't think it's yours, and I'd re-run before spending time on it. Reasons, in the order I checked them:
It's a timeout, not an assertion.
The job has exceeded the maximum execution time of 30m0s
It never left Cluster up through an authenticated proxy — I looked in on it twice while it was still running and it was on that step both times.
The change on this head can't reach that test.15ff1bf4 is fe8bf6a2 plus a one-line [string] cast in ConvertTo-Win32Arg — install-k8s.ps1, PowerShell. e2e-proxy.sh runs the bash installer on ubuntu-latest. No path between them.
fe8bf6a2 passed this exact workflow at 09:58 (run 32955857325), and develop passed it twice at 10:19 and 10:22 — after your run timed out. So the job is healthy on either side.
And the obvious environmental suspect is already ruled out. I checked whether the squid image could have moved under you — it's pinned by digest, with the comment explaining precisely why (Canonical publishes only under moving channel tags, and the tag name doesn't match the shipped version). That trap is already closed here.
What's left is the failure mode the script itself documents at line 15: "If the credentials get mangled, squid answers 407, the image pull hangs." A hang on cluster bring-up behind the proxy is what this test looks like when the pull stalls — which on a clean head, with develop green either side, reads as an infrastructure blip rather than a regression.
So: re-run the job. If it times out again on the same head, that's a different conversation and worth looking at properly — a proxy e2e that burns a full 30-minute budget twice on an unchanged commit isn't a flake any more.
Everything else here is ready: 34 checks pass, zero open threads, and the shell32 cross-check is green on Windows after finding the real encoder bug. This is the last thing between it and my approval.
aptracebloc
commented
Aug 26, 2026
Note: the red |
LukasWodka
left a comment
There was a problem hiding this comment.
f762f6f8 — good catch, and it's the same class as everything else this PR has been circling.
-$fn| Should -Match'ConvertTo-Win32Arg'+$fn| Should -Match'ForEach-Object \{ ConvertTo-Win32Arg \$_'The old guard matched the name, and the function body mentions that name in a comment — so deleting the actual call left the guard green. A source guard that can't detect the removal of the thing it guards is doing nothing, and from a CI log it's indistinguishable from one that works.
That's now four separate instances on this PR of a check that couldn't fail:
- nested case arrays that threw on binding, so the cross-check never compared;
- a pipeline that discarded its own input;
- an assignment that collapsed the result to a scalar;
- and a source guard matching a comment.
Every one of them looked green or looked like a code failure, and none was about the encoder. The encoder bug that did exist — Append([char]) binding a numeric overload on PS 5.1 — was only ever findable by the real shell32 export, and only after all four were cleared. Worth remembering next time a ground-truth oracle looks like more trouble than it's worth.
One check left (Cursor Bugbot), zero open threads, and the E2E auth-proxy (squid) timeout from the previous head is gone on this one. I'll approve as soon as it lands — nothing outstanding from me.
LukasWodka
left a comment
There was a problem hiding this comment.
I came here to approve and can't — this went CONFLICTING while I was watching, and it isn't your doing.
develop moved to f259f5ed, which is #844 merging. Intersecting the two changesets gives exactly one file:
#845 touches: scripts/install-k8s.ps1, scripts/manifest.sha256, scripts/tests/install-k8s.Tests.ps1
#844 touched: … scripts/manifest.sha256 …
overlap: scripts/manifest.sha256
Both regenerate the signed manifest, so they conflict on it by construction — nothing to do with the PowerShell work.
And the conflict has taken your CI with it. The whole check list on this head is now one line:
pass Cursor Bugbot
Everything else — Pester (windows-latest), Unit tests, bats, the Prereqs and PATH persist matrices — isn't running, because GitHub can't build a merge ref. Worth flagging because the aggregate rollup reads as green in that state: an empty check list looks identical to a passing one from anything that only reads the summary. I nearly took it at face value; the per-PR pull is what caught it.
So the earlier E2E auth-proxy (squid) timeout is moot too — that run belonged to a head that no longer merges cleanly.
Fix is mechanical: merge develop, regenerate scripts/manifest.sha256 so it carries both #844's entries and this PR's install-k8s.ps1 hash, push. Then the matrix actually runs and I can finish this off.
Everything on the merits is still settled from my side: zero open threads, the shell32 cross-check green on Windows after finding the real Append([char]) bug, and the source guard now matching the call rather than a comment. This is purely a rebase-and-regenerate.
…rocess-arg-escaping # Conflicts: # scripts/manifest.sha256
aptracebloc
commented
Aug 26, 2026
@LukasWodka — I believe everything you raised is addressed: all four Bugbot findings fixed + threads resolved, the source guard hardened to match the actual call, required checks green (the only red is the non-required #2350 squid flake), and |
LukasWodka
left a comment
There was a problem hiding this comment.
Approving 1e126310 — change-request lifted. 45 checks pass, zero open threads, MERGEABLE.
The delta since my last review is purely the develop merge, which is what I asked for. Nothing in the PR's own contribution moved:
install-k8s.ps1 75 +/-
manifest.sha256 2 +/-
tests/install-k8s.Tests.ps1 196 +/-
So the PowerShell work is byte-identical to f762f6f8, which I'd already satisfied myself on — the shell32 cross-check green on Windows after it found the real Append([char]) binder bug, and the source guard anchored to the call rather than to a comment.
I verified the manifest resolution rather than eyeballing it, because that's the file that conflicted and the one where a bad merge is silent. Two ways:
recompute every listed hash → checked=20 mismatched=0
bash scripts/gen-manifest.sh → reproduces the committed file byte-for-byte
The second is the one that matters. Hand-resolving a manifest conflict can easily produce a file that's internally consistent but not what the generator would emit — one stale entry, or a dropped line for a file develop added. Running the real generator and getting a byte-identical result means the resolution carries #833's, #844's, #849's and #850's entries and this PR's install-k8s.ps1 hash, with nothing hand-patched. Derived, not restated.
Aside, for my own record: my first pass at this reported all 20 hashes mismatching. That was my error — I ran the loop from scripts/ while the manifest's paths are repo-root-relative, so every shasum silently failed on a nonexistent path and compared empty against the digest. Worth naming because a wrong-in-the-safe-direction check like that is the one that gets believed: "everything mismatches" reads as a catastrophic finding rather than as a broken harness. Fixing the working directory gave 0 mismatches.
Nothing outstanding from me on this one.
Uh oh!
There was an error while loading. Please reload this page.
develop advanced again while addressing review. Only conflict was scripts/manifest.sha256 — both #845 and this PR re-hash install-k8s.ps1 — so resolved by regenerating the manifest from the merged files (gen-manifest.sh --check passes). install-k8s.ps1 auto-merged: #845's arg-builder quoting fix and this PR's floor-derived fallback are independent. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…LineToArgvW (client#858 review) LukasWodka on #858: the new backend#2545 Describe's Split-Cmdline2545 is the ORACLE the builder round-trip tests trust, but -- unlike the backend#2455 Split-LikeArgvW -- it was only asserted in a comment to "mirror" the real API, never checked against it. If the two from-spec CommandLineToArgvW decoders drift, every encoder test here would validate ConvertTo-Win32Arg against a decoder that no longer matches Windows, and the cross-check on the OTHER copy says nothing about this one. Add the same Windows-gated shell32!CommandLineToArgvW cross-check #845 uses, pointed at Split-Cmdline2545 (distinct namespace TbWin32b so both blocks' Add-Type calls coexist), so this decoder is pinned to the real API too. Keeps the deliberate per-Describe independence (Pester cross-BeforeAll ordering is fragile) while closing the gap. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…rg builders (backend#2545) (#858) * fix(installer): escape inner quotes in docker-buildx and k3d-create arg builders (backend#2545) #2455 (client#845) fixed CommandLineToArgvW arg-escaping in the shared Invoke-BoundedProcess joiner, but two Start-Process command lines built inline in install-k8s.ps1 kept the naive space-only quoting with the inner `"` unescaped: `docker buildx build` (Build-GpuNodeImage) and `k3d cluster create` (New-K3dCluster). -ArgumentList <one string> reaches the child verbatim like $psi.Arguments, so an arg carrying BOTH a space and a `"` had its inner quotes silently consumed by the OS re-split and merged into the adjacent token. - Delegate both builders to ConvertTo-Win32Arg (the canonical CommandLineToArgvW encoder from #2455), so an arg with a space and a quote survives as one token. - Drop the k3d builder's `@` quote-branch: `@` is not special to CommandLineToArgvW, so a bare `host:node@all` re-splits to the identical single token whether quoted or not -- the change is a no-op there and the fix is escaping the quote the old branch ignored. - Add round-trip tests that push a whitespace+quote arg through each builder's EXACT shipped expression and prove it recovers as one token; regenerate scripts/manifest.sha256. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(installer): pin the backend#2545 decoder to real shell32!CommandLineToArgvW (client#858 review) LukasWodka on #858: the new backend#2545 Describe's Split-Cmdline2545 is the ORACLE the builder round-trip tests trust, but -- unlike the backend#2455 Split-LikeArgvW -- it was only asserted in a comment to "mirror" the real API, never checked against it. If the two from-spec CommandLineToArgvW decoders drift, every encoder test here would validate ConvertTo-Win32Arg against a decoder that no longer matches Windows, and the cross-check on the OTHER copy says nothing about this one. Add the same Windows-gated shell32!CommandLineToArgvW cross-check #845 uses, pointed at Split-Cmdline2545 (distinct namespace TbWin32b so both blocks' Add-Type calls coexist), so this decoder is pinned to the real API too. Keeps the deliberate per-Describe independence (Pester cross-BeforeAll ordering is fragile) while closing the gap. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

What
Invoke-BoundedProcessjoins its args into one flat$psi.Argumentsstring but the joiner only wrapped whitespace-bearing args in quotes — it never escaped an inner". So any arg carrying both a space and a"(e.g. a Go-template--format "{{.Names}} {{.Label "k3d.role"}}") reached the child as a single token with its inner quotes silently consumed byCommandLineToArgvW, and an arg with a"but no space took the raw pass-through branch where the bare quote was read as a quoting toggle. This is the corruption client#817 dodged (by never passing a quoted arg) rather than fixed.Fix
Add
ConvertTo-Win32Arg, which quotes one argument following the exactCommandLineToArgvW/ MSVCRT rules:"→ passed through untouched;", with"→\", a run of N\before a quote →2N+1\, a trailing run of N\→2N\(so the closing quote isn't escaped),\elsewhere left literal;""(survives as a present-but-empty arg).Invoke-BoundedProcessnow maps every arg through it. The old^".*"$self-quoting escape hatch is removed — callers pass raw args (seeSet-NodeGpuCapacity, which no longer pre-quotes--patch-file).Call-site sweep
All ~27 call sites (direct + via
Invoke-DockerCli) swept; onlySet-NodeGpuCapacitypre-quoted (fixed).docker login -u $regUser(env-derived, can carry a quote) is now safe; the password stays on stdin, never argv.Tests (pwsh 7.6.5 / Pester 6.1.0)
encode(argv)re-split ==argvas single tokens) driven by a from-specCommandLineToArgvWreimplementation, plus a Windows-only cross-check against the realshell32!CommandLineToArgvW.install-k8s.Tests.ps1765 passed / 0 failed / 14 skipped; installer-parity + install + telemetry 158/0; bats drift suites green;manifest.sha256regenerated andgen-manifest.sh --checkclean (install-k8s.ps1 is in the signed integrity manifest).Follow-up (not in this PR)
The same naive space-only quoting lives at two other
Start-Process -ArgumentListsites —docker buildx build(install-k8s.ps1:2408) andk3d cluster create(:4002) — which don't go throughInvoke-BoundedProcess. Low practical risk (those args rarely embed quotes); tracked separately to keep this change scoped to the helper.Builds on client#817 (bug found by @saadqbal during that review).
Fixes tracebloc/backend#2455 (cross-repo — keyword won't auto-close; the backend issue is closed by hand on merge).
— drafted with Claude Code
Note
Medium Risk
Touches the central process-spawning path used for docker/kubectl and many installer steps on Windows; incorrect escaping could still break rare argv shapes, but behavior is heavily tested and scoped to arg encoding.
Overview
Fixes incorrect command-line quoting in
Invoke-BoundedProcess, which previously wrapped space-containing args in quotes but did not escape embedded"or trailing backslashes, soCommandLineToArgvWcould merge or mangle tokens (e.g. Go-template--formatstrings and paths with spaces).Adds
ConvertTo-Win32Argwith full MSVCRT/CommandLineToArgvWrules and routes every bounded child-process arg through it; removes the old “already quoted” escape hatch.Set-NodeGpuCapacitynow passes the patch file path unquoted to--patch-file.Tests replace the old whitespace-only guards with golden encodings, a spec-based round-trip, and a Windows-only
shell32!CommandLineToArgvWcross-check; related Pester setups load the new helper.manifest.sha256is updated forinstall-k8s.ps1.Reviewed by Cursor Bugbot for commit 1e12631. Bugbot is set up for automated code reviews on this repo. Configure here.