Skip to content

Commit 7b4aec2

Browse files
Fix Windows runtime bootstrap/start-vm edge cases
1 parent 02df106 commit 7b4aec2

6 files changed

Lines changed: 106 additions & 5 deletions

File tree

docs/RUNBOOKS.md

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
owner: Mike Jenkins
3-
last_verified: 2026-02-18
3+
last_verified: 2026-02-20
44
---
55

66
# Runbooks
@@ -74,6 +74,25 @@ Backend spec:
7474
- Validate project path argument handling with spaces.
7575
- Validate selected runtime mode is reflected in diagnostics output.
7676

77+
## Local Preflight (Before Windows VM Smoke)
78+
Use this deterministic preflight to validate launcher wiring before VM testing. Windows VM smoke remains `scripts/runtime/windows/smoke-check.cmd`.
79+
80+
1. Syntax + onboarding checks:
81+
```bash
82+
node --check scripts/pcoder.cjs
83+
scripts/pcoder setup --init --show
84+
```
85+
2. Doctor with stubbed runners:
86+
```bash
87+
PCODER_CODEX_CMD=node PCODER_CLAUDE_CMD=node scripts/pcoder doctor
88+
```
89+
3. API-mode host-native launch checks with stubbed runners:
90+
```bash
91+
scripts/pcoder setup --codex-auth api --claude-auth api --windows-mode host-native --show
92+
OPENAI_API_KEY=pcoder-preflight PCODER_CODEX_CMD=node scripts/pcoder run codex --mode host-native -- --version
93+
ANTHROPIC_AUTH_TOKEN=pcoder-preflight PCODER_CLAUDE_CMD=node scripts/pcoder run claude --mode host-native -- --version
94+
```
95+
7796
## Incident Recovery
7897
- If runtime corruption is detected, clear `runtime/` and rerun bootstrap.
7998
- If profile corruption is detected, restore `profiles/profiles.json` from version control.

docs/exec-plans/active/EP-001-portable-coder-foundation-and-multi-provider-mvp.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
owner: Mike Jenkins
3-
last_verified: 2026-02-18
3+
last_verified: 2026-02-20
44
---
55

66
# EP-001 - Portable Coder Foundation and Multi-Provider MVP
@@ -39,7 +39,10 @@ Initial user target providers/tools:
3939
- [x] (2026-02-18) Implement onboarding/settings and dual auth mode support (`pcoder setup`, `pcoder auth`, persistent portable state)
4040
- [x] (2026-02-18) Add Windows smoke checklist scripts for VM boot/SSH/guest tool validation
4141
- [x] (2026-02-18) Add Windows runtime bootstrap installer (`bootstrap-runtime`) to fetch QEMU/image and generate SSH keys
42-
- [ ] (2026-02-18) Validate Codex + Claude launches in one portable layout
42+
- [x] (2026-02-20) Validate Codex + Claude launches in one portable layout
43+
- [x] (2026-02-20) Fix Windows bootstrap `ssh-keygen` argument handling for empty-passphrase key generation
44+
- [x] (2026-02-20) Fix Windows VM start cloud-init port selection to fallback when default range is exhausted
45+
- [x] (2026-02-20) Add Windows helper script to patch legacy `start-vm.ps1` cloud-init port fallback on existing local clones
4346
- [ ] (2026-02-18) Document setup/runbook and close out EP-001
4447

4548
## Context and Orientation
@@ -114,6 +117,10 @@ Acceptance criteria for EP-001:
114117
- 2026-02-18: MVP project handoff uses SCP sync in/out of VM instead of shared-folder mounts to reduce host dependency assumptions.
115118
- 2026-02-18: OAuth + API dual-mode support required explicit onboarding state and per-tool auth-mode persistence.
116119
- 2026-02-18: Profile resolution needed per-tool default fallback to avoid cross-provider env validation errors in API mode.
120+
- 2026-02-20: Host-native and stubbed CI-equivalent validation passed on Linux host for Codex + Claude launch flows.
121+
- 2026-02-20: Windows VM smoke validation remains target-specific and is still tracked separately from Linux-host checks.
122+
- 2026-02-20: PowerShell `Start-Process -ArgumentList` rejected empty elements, so `ssh-keygen -N` required explicit empty-string handling.
123+
- 2026-02-20: Some Windows hosts had no free ports in `38080-38120`; cloud-init server startup now needs fallback port allocation.
117124

118125
## Decision Log
119126
- 2026-02-18: Adopt harness-first planning model before implementation.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
@echo off
2+
setlocal EnableExtensions
3+
4+
set "SCRIPT_DIR=%~dp0"
5+
set "PATCH_PS1=%SCRIPT_DIR%apply-start-vm-port-fix.ps1"
6+
7+
if not exist "%PATCH_PS1%" (
8+
echo Error: missing patch script: %PATCH_PS1%
9+
exit /b 1
10+
)
11+
12+
powershell -NoProfile -ExecutionPolicy Bypass -File "%PATCH_PS1%"
13+
exit /b %errorlevel%
14+
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
param()
2+
3+
$ErrorActionPreference = 'Stop'
4+
5+
$startVmPath = Join-Path $PSScriptRoot 'start-vm.ps1'
6+
if (-not (Test-Path $startVmPath)) {
7+
throw "Missing target script: $startVmPath"
8+
}
9+
10+
$text = Get-Content -Path $startVmPath -Raw -Encoding UTF8
11+
$marker = 'Preferred cloud-init port range 38080-38120 unavailable; using ephemeral port $cloudInitPort.'
12+
13+
if ($text.Contains($marker)) {
14+
Write-Host "Patch already present in $startVmPath"
15+
exit 0
16+
}
17+
18+
$oldLine = '$cloudInitPort = Get-FreeTcpPort -StartPort 38080 -EndPort 38120'
19+
$replacement = @'
20+
$cloudInitPort = 0
21+
try {
22+
$cloudInitPort = Get-FreeTcpPort -StartPort 38080 -EndPort 38120
23+
} catch {
24+
$listener = [System.Net.Sockets.TcpListener]::new([System.Net.IPAddress]::Loopback, 0)
25+
try {
26+
$listener.Start()
27+
$cloudInitPort = ([System.Net.IPEndPoint]$listener.LocalEndpoint).Port
28+
} finally {
29+
try { $listener.Stop() } catch {}
30+
}
31+
Write-Host "Preferred cloud-init port range 38080-38120 unavailable; using ephemeral port $cloudInitPort."
32+
}
33+
'@
34+
35+
if (-not $text.Contains($oldLine)) {
36+
throw "Could not find expected line to patch in $startVmPath"
37+
}
38+
39+
$updated = $text.Replace($oldLine, $replacement)
40+
Set-Content -Path $startVmPath -Value $updated -Encoding UTF8
41+
42+
Write-Host "Applied cloud-init port fallback patch to $startVmPath"
43+

scripts/runtime/windows/bootstrap-runtime.ps1

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,8 @@ if (-not (Test-Path $sshPrivate) -or -not (Test-Path $sshPublic)) {
168168
throw "Missing ssh-keygen in PATH. Install OpenSSH client feature or set up runtime/linux/ssh/id_ed25519 manually."
169169
}
170170

171-
$genArgs = @('-t', 'ed25519', '-N', '', '-f', $sshPrivate, '-C', 'portable-coder')
171+
# Start-Process rejects empty ArgumentList elements, so pass -N "" in one string.
172+
$genArgs = "-t ed25519 -N `"`" -f `"$sshPrivate`" -C portable-coder"
172173
$keyProc = Start-Process -FilePath $sshKeygen -ArgumentList $genArgs -PassThru -Wait -NoNewWindow
173174
if ($keyProc.ExitCode -ne 0) {
174175
throw "ssh-keygen failed with exit code $($keyProc.ExitCode)"

scripts/runtime/windows/start-vm.ps1

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,17 @@ function Get-FreeTcpPort {
5151
throw "No free TCP port found in range $StartPort-$EndPort"
5252
}
5353

54+
function Get-EphemeralTcpPort {
55+
$listener = [System.Net.Sockets.TcpListener]::new([System.Net.IPAddress]::Loopback, 0)
56+
try {
57+
$listener.Start()
58+
$endpoint = [System.Net.IPEndPoint]$listener.LocalEndpoint
59+
return $endpoint.Port
60+
} finally {
61+
try { $listener.Stop() } catch {}
62+
}
63+
}
64+
5465
function Stop-CloudInitServer {
5566
if (Test-Path $cloudInitPidFile) {
5667
$raw = Get-Content $cloudInitPidFile -ErrorAction SilentlyContinue | Select-Object -First 1
@@ -188,7 +199,13 @@ if ($requestedPortRaw) {
188199
$sshPort = Get-FreeTcpPort -StartPort 2222 -EndPort 2299
189200
}
190201

191-
$cloudInitPort = Get-FreeTcpPort -StartPort 38080 -EndPort 38120
202+
$cloudInitPort = 0
203+
try {
204+
$cloudInitPort = Get-FreeTcpPort -StartPort 38080 -EndPort 38120
205+
} catch {
206+
$cloudInitPort = Get-EphemeralTcpPort
207+
Write-Host "Preferred cloud-init port range 38080-38120 unavailable; using ephemeral port $cloudInitPort."
208+
}
192209
$guestUser = if ($env:PCODER_VM_USER) { $env:PCODER_VM_USER } else { 'portable' }
193210
$pubKeyValue = (Get-Content $sshPublicKey -Raw).Trim()
194211
Write-CloudInitSeed -PublicKey $pubKeyValue -UserName $guestUser

0 commit comments

Comments
 (0)