agent-dev-env 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/LICENSE +21 -0
  2. package/dist/assets/bridge/bridge.js +202 -0
  3. package/dist/assets/guest/guest-agent-mac.js +586 -0
  4. package/dist/assets/guest/guest-agent-ubuntu.js +481 -0
  5. package/dist/assets/guest/guest-agent-windows.js +466 -0
  6. package/dist/assets/images/mac/CHANGELOG.md +265 -0
  7. package/dist/assets/images/mac/README.md +63 -0
  8. package/dist/assets/images/mac/sandbox.pkr.hcl +468 -0
  9. package/dist/assets/images/mac/vars/sandbox-macos-tahoe.pkrvars.hcl +39 -0
  10. package/dist/assets/images/ubuntu-arm64-vmware/CHANGELOG.md +239 -0
  11. package/dist/assets/images/ubuntu-arm64-vmware/README.md +177 -0
  12. package/dist/assets/images/ubuntu-arm64-vmware/autoinstall/meta-data +2 -0
  13. package/dist/assets/images/ubuntu-arm64-vmware/autoinstall/user-data +34 -0
  14. package/dist/assets/images/ubuntu-arm64-vmware/sandbox.pkr.hcl +776 -0
  15. package/dist/assets/images/ubuntu-arm64-vmware/vars/sandbox-ubuntu-24-04-arm64-vmware.pkrvars.hcl +71 -0
  16. package/dist/assets/images/windows-arm64-qemu/CHANGELOG.md +209 -0
  17. package/dist/assets/images/windows-arm64-qemu/README.md +142 -0
  18. package/dist/assets/images/windows-arm64-qemu/autounattend.xml +325 -0
  19. package/dist/assets/images/windows-arm64-qemu/qemu-with-tpm.sh +170 -0
  20. package/dist/assets/images/windows-arm64-qemu/sandbox.pkr.hcl +1012 -0
  21. package/dist/assets/images/windows-arm64-qemu/vars/sandbox-windows-11-arm64-qemu.pkrvars.hcl +83 -0
  22. package/dist/assets/images/windows-arm64-vmware/CHANGELOG.md +270 -0
  23. package/dist/assets/images/windows-arm64-vmware/README.md +171 -0
  24. package/dist/assets/images/windows-arm64-vmware/autounattend.xml +311 -0
  25. package/dist/assets/images/windows-arm64-vmware/sandbox.pkr.hcl +1036 -0
  26. package/dist/assets/images/windows-arm64-vmware/vars/sandbox-windows-11-arm64-vmware.pkrvars.hcl +83 -0
  27. package/dist/assets/rules/agent-rules-linux.md +68 -0
  28. package/dist/assets/rules/agent-rules.md +71 -0
  29. package/dist/assets/watchdog/watch-build-ocr.swift +40 -0
  30. package/dist/assets/watchdog/watch-build.py +275 -0
  31. package/dist/cli.js +52 -0
  32. package/dist/commands/delete.js +179 -0
  33. package/dist/commands/doctor.js +165 -0
  34. package/dist/commands/list.js +38 -0
  35. package/dist/commands/not-yet.js +15 -0
  36. package/dist/commands/register.js +150 -0
  37. package/dist/commands/run.js +65 -0
  38. package/dist/commands/status.js +142 -0
  39. package/dist/commands/stop.js +186 -0
  40. package/dist/commands/sync.js +98 -0
  41. package/dist/lib/exec.js +250 -0
  42. package/dist/lib/ghcr.js +82 -0
  43. package/dist/lib/git.js +100 -0
  44. package/dist/lib/logger.js +87 -0
  45. package/dist/lib/network.js +63 -0
  46. package/dist/lib/paths.js +116 -0
  47. package/dist/lib/platform.js +87 -0
  48. package/dist/lib/prompt.js +66 -0
  49. package/dist/lib/qemu.js +373 -0
  50. package/dist/lib/regex.js +9 -0
  51. package/dist/lib/ssh.js +230 -0
  52. package/dist/lib/tart.js +210 -0
  53. package/dist/lib/template.js +35 -0
  54. package/dist/lib/vars.js +99 -0
  55. package/dist/lib/vmrun.js +334 -0
  56. package/dist/lifecycle/build-macos.js +23 -0
  57. package/dist/lifecycle/build-qemu.js +204 -0
  58. package/dist/lifecycle/build-shared.js +355 -0
  59. package/dist/lifecycle/build-ubuntu.js +184 -0
  60. package/dist/lifecycle/build-watchdog.js +146 -0
  61. package/dist/lifecycle/build-windows-vmware.js +112 -0
  62. package/dist/lifecycle/build.js +55 -0
  63. package/dist/lifecycle/catalog.js +165 -0
  64. package/dist/lifecycle/deploy.js +161 -0
  65. package/dist/lifecycle/tag.js +77 -0
  66. package/dist/lifecycle/watch-build.js +64 -0
  67. package/dist/runners/bridges.js +153 -0
  68. package/dist/runners/framework.js +66 -0
  69. package/dist/runners/macos-bridges.js +143 -0
  70. package/dist/runners/macos-guest.js +76 -0
  71. package/dist/runners/macos-rules.js +77 -0
  72. package/dist/runners/macos-summary.js +145 -0
  73. package/dist/runners/macos.js +258 -0
  74. package/dist/runners/openchamber.js +60 -0
  75. package/dist/runners/options.js +56 -0
  76. package/dist/runners/qemu-image.js +183 -0
  77. package/dist/runners/rules.js +49 -0
  78. package/dist/runners/ubuntu-bridges.js +154 -0
  79. package/dist/runners/ubuntu-guest.js +136 -0
  80. package/dist/runners/ubuntu-image.js +26 -0
  81. package/dist/runners/ubuntu-rules.js +86 -0
  82. package/dist/runners/ubuntu-shared.js +65 -0
  83. package/dist/runners/ubuntu-summary.js +141 -0
  84. package/dist/runners/ubuntu.js +117 -0
  85. package/dist/runners/vmware-common.js +158 -0
  86. package/dist/runners/vmware-image.js +242 -0
  87. package/dist/runners/windows-autologon.js +119 -0
  88. package/dist/runners/windows-bridges.js +136 -0
  89. package/dist/runners/windows-guest.js +213 -0
  90. package/dist/runners/windows-image.js +23 -0
  91. package/dist/runners/windows-qemu-summary.js +85 -0
  92. package/dist/runners/windows-qemu.js +189 -0
  93. package/dist/runners/windows-shared.js +68 -0
  94. package/dist/runners/windows-summary.js +104 -0
  95. package/dist/runners/windows.js +96 -0
  96. package/dist/settings/common.js +107 -0
  97. package/dist/settings/macos-copy.js +223 -0
  98. package/dist/settings/macos.js +41 -0
  99. package/dist/settings/ubuntu-copy.js +223 -0
  100. package/dist/settings/ubuntu.js +68 -0
  101. package/package.json +52 -0
@@ -0,0 +1,1012 @@
1
+ packer {
2
+ required_version = ">= 1.10.0"
3
+
4
+ required_plugins {
5
+ qemu = {
6
+ version = ">= 1.1.0"
7
+ source = "github.com/hashicorp/qemu"
8
+ }
9
+ }
10
+ }
11
+
12
+ # ---------------------------------------------------------------------------
13
+ # Variables
14
+ # ---------------------------------------------------------------------------
15
+ #
16
+ # Most variables mirror the macOS template (images/mac/sandbox.pkr.hcl) so
17
+ # the vars files look and feel the same. The Windows build differs in the
18
+ # mechanics: the OS comes from a bring-your-own ISO instead of a GHCR base
19
+ # image, provisioning happens over WinRM instead of SSH, and the image is a
20
+ # qcow2 disk instead of a Tart VM.
21
+
22
+ variable "windows_version" {
23
+ type = string
24
+ description = "Windows guest version, e.g. '11'; part of the image name (sandbox-windows-<windows_version>-arm64-qemu)."
25
+ }
26
+
27
+ variable "image_version" {
28
+ type = string
29
+ description = "Semantic version this image is published under; bump it + add a CHANGELOG.md entry per release."
30
+ }
31
+
32
+ # --- Build layout ---
33
+ #
34
+ # Built images and their scratch land in a top-level build/ directory:
35
+ # build/windows-arm64-qemu/output (Packer's output_directory) and
36
+ # build/windows-arm64-qemu/drivers/ (staged into the unattend CD).
37
+ # The agent-dev-env CLI build flow computes the directory and passes it
38
+ # in; the default keeps a bare `packer build` from the platform dir
39
+ # working.
40
+
41
+ variable "build_dir" {
42
+ type = string
43
+ default = "."
44
+ description = "Per-image build directory: <build_dir>/output holds the built image and <build_dir>/drivers the staged unattend-CD drivers. Set by `agent-dev-env build` to build/windows-arm64-qemu/."
45
+ }
46
+
47
+ # --- Windows install ISO (bring-your-own, see images/windows-arm64-qemu/README.md) ---
48
+
49
+ variable "iso_path" {
50
+ type = string
51
+ description = "Absolute path to the Windows 11 ARM64 ISO. Set by the agent-dev-env CLI build flow (PKR_VAR_iso_path); the ISO is not redistributable so it cannot live in the repo. Required — validate with `-var iso_path=/path/to/iso`."
52
+ }
53
+
54
+ variable "iso_sha256" {
55
+ type = string
56
+ default = ""
57
+ description = "SHA256 of the Windows ISO as published on the Microsoft download page. Read by the agent-dev-env CLI build flow for verification, not by Packer itself (iso_checksum is 'none')."
58
+ }
59
+
60
+ # --- virtio-win drivers (ARM64) ---
61
+
62
+ variable "virtio_win_iso_path" {
63
+ type = string
64
+ default = ""
65
+ description = "Absolute path to virtio-win.iso (release 0.1.240+). Set by the agent-dev-env CLI build flow; the flow stages the ARM64 driver subset into drivers/staging/ and qemu-with-tpm.sh attaches the full ISO as a CD."
66
+ }
67
+
68
+ variable "virtio_win_url" {
69
+ type = string
70
+ default = "https://fedorapeople.org/groups/virt/virtio-win/direct-downloads/stable-virtio/virtio-win.iso"
71
+ description = "Download URL used by the agent-dev-env CLI build flow when VIRTIO_WIN_ISO_PATH is unset."
72
+ }
73
+
74
+ variable "virtio_win_sha256" {
75
+ type = string
76
+ default = ""
77
+ description = "SHA256 of virtio-win.iso. Read by the agent-dev-env CLI build flow for verification (empty = skip)."
78
+ }
79
+
80
+ # --- Toolchain versions (installed via Chocolatey) ---
81
+
82
+ variable "nodejs_version" {
83
+ type = string
84
+ description = "Node.js version, e.g. '26.8.1' (choco package version)."
85
+ }
86
+
87
+ variable "python_version" {
88
+ type = string
89
+ description = "Python version, e.g. '3.13.1' (choco package version)."
90
+ }
91
+
92
+ variable "github_cli_version" {
93
+ type = string
94
+ description = "GitHub CLI version, e.g. '2.97.0' (choco package version)."
95
+ }
96
+
97
+ variable "ripgrep_version" {
98
+ type = string
99
+ description = "ripgrep version, e.g. '15.2.0' (choco package version)."
100
+ }
101
+
102
+ variable "git_version" {
103
+ type = string
104
+ description = "Git version, e.g. '2.55.0.4' (choco package version)."
105
+ }
106
+
107
+ variable "jq_version" {
108
+ type = string
109
+ description = "jq version, e.g. '1.8.1' (choco package version)."
110
+ }
111
+
112
+ variable "open_code_review_version" {
113
+ type = string
114
+ description = "open-code-review (ocr) version installed via npm."
115
+ }
116
+
117
+ variable "chrome_version" {
118
+ type = string
119
+ description = "Google Chrome version (Chrome for Testing snapshot, e.g. '152.0.7977.54')."
120
+ }
121
+
122
+ variable "chrome_sha256" {
123
+ type = string
124
+ description = "SHA256 of the CfT chrome-win64.zip for chrome_version."
125
+ }
126
+
127
+ # --- C/C++ + cross-language toolchains (brought over from AdGuard's
128
+ # build-agent-images windows2022-vs2022 image) ---
129
+
130
+ variable "vs_buildtools_version" {
131
+ type = string
132
+ description = "VS2022 Build Tools choco package version, e.g. '117.14.37' (the installer is then finalized with setup.exe + the .NET SDK / VC++ / Win11 SDK components)."
133
+ }
134
+
135
+ variable "go_version" {
136
+ type = string
137
+ description = "Go version (choco package version)."
138
+ }
139
+
140
+ variable "rust_version" {
141
+ type = string
142
+ description = "Rust toolchain version installed via rustup (e.g. '1.95'; rustup resolves the latest patch)."
143
+ }
144
+
145
+ variable "wixtoolset_version" {
146
+ type = string
147
+ description = "WiX Toolset version (choco package version)."
148
+ }
149
+
150
+ variable "protoc_version" {
151
+ type = string
152
+ description = "protobuf compiler (protoc) version (choco package version)."
153
+ }
154
+
155
+ variable "nasm_version" {
156
+ type = string
157
+ description = "NASM assembler version (choco package version)."
158
+ }
159
+
160
+ variable "llvm_version" {
161
+ type = string
162
+ description = "LLVM version (choco package version)."
163
+ }
164
+
165
+ variable "vim_version" {
166
+ type = string
167
+ description = "Vim version (choco package version)."
168
+ }
169
+
170
+ variable "nuget_version" {
171
+ type = string
172
+ description = "NuGet command-line client version (choco package version)."
173
+ }
174
+
175
+ variable "mingw_version" {
176
+ type = string
177
+ description = "MinGW-w64 GCC toolchain version (choco package version; provides make + mingw gcc)."
178
+ }
179
+
180
+ variable "make_version" {
181
+ type = string
182
+ description = "GNU make version (choco package version)."
183
+ }
184
+
185
+ # --- VM resources ---
186
+
187
+ variable "disk_size" {
188
+ type = number
189
+ default = 100
190
+ description = "VM disk size in GB."
191
+ }
192
+
193
+ variable "cpu_count" {
194
+ type = number
195
+ default = 4
196
+ description = "CPU count of the VM."
197
+ }
198
+
199
+ variable "memory_gb" {
200
+ type = number
201
+ default = 8
202
+ description = "RAM of the VM in GB."
203
+ }
204
+
205
+ # --- WinRM credentials (baked into autounattend.xml — keep in sync) ---
206
+
207
+ variable "winrm_username" {
208
+ type = string
209
+ default = "Administrator"
210
+ description = "User used for WinRM provisioning; must match the account autounattend.xml creates."
211
+ }
212
+
213
+ variable "winrm_password" {
214
+ type = string
215
+ default = "sandbox1"
216
+ sensitive = true
217
+ description = "Password of the provisioning account; must match autounattend.xml."
218
+ }
219
+
220
+ # --- OpenChamber web UI ---
221
+
222
+ variable "openchamber_ui_password" {
223
+ type = string
224
+ default = "sandbox"
225
+ description = "Password protecting the OpenChamber web UI; required because the server binds to 0.0.0.0 (host access: http://127.0.0.1:4000)."
226
+ }
227
+
228
+ variable "openchamber_port" {
229
+ type = number
230
+ default = 4000
231
+ description = "TCP port the OpenChamber web UI listens on inside the guest."
232
+ }
233
+
234
+ # --- QEMU specifics ---
235
+
236
+ variable "qemu_binary" {
237
+ type = string
238
+ default = "./qemu-with-tpm.sh"
239
+ description = "qemu binary (or wrapper) Packer invokes. The wrapper appends the TPM/ramfb/USB/CD rewrites Packer's qemuargs option cannot express (it replaces the generated args)."
240
+ }
241
+
242
+ variable "efi_firmware_code" {
243
+ type = string
244
+ default = "/opt/homebrew/share/qemu/edk2-aarch64-code.fd"
245
+ description = "UEFI firmware (read-only code image) for the ARM64 guest; ships with Homebrew's qemu."
246
+ }
247
+
248
+ variable "efi_firmware_vars" {
249
+ type = string
250
+ default = "/opt/homebrew/share/qemu/edk2-arm-vars.fd"
251
+ description = "UEFI firmware template for the writable NVRAM store; Packer copies it per build."
252
+ }
253
+
254
+ # ---------------------------------------------------------------------------
255
+ # Source: QEMU (aarch64 + Apple Silicon HVF)
256
+ # ---------------------------------------------------------------------------
257
+ #
258
+ # The recipe is proven on Apple Silicon (HVF can only virtualize ARM64
259
+ # guests, so x86_64 Windows would run under slow TCG emulation):
260
+ # - machine "virt,gic-version=max", cpu "host" (required by HVF; do NOT
261
+ # add virtualization=on — HVF cannot pass nested virt through),
262
+ # - UEFI via edk2 AAVMF firmware + swtpm TPM 2.0 (Win11 system
263
+ # requirements; swtpm is started by the build flow),
264
+ # - all CD-ROMs attached as usb-storage (the virt machine has no
265
+ # IDE/SATA controller and WinPE has in-box xHCI drivers) — done by
266
+ # qemu-with-tpm.sh because the plugin's qemuargs option would replace
267
+ # its auto-generated args,
268
+ # - the ARM64 virtio drivers (viostor/vioscsi/NetKVM) are staged into
269
+ # the same CD as autounattend.xml (drivers/staging/), because WinPE
270
+ # drive-letter enumeration on ARM64 + EFI is non-deterministic.
271
+ #
272
+ # See images/windows-arm64-qemu/README.md and DEVELOPMENT.md for the full flow.
273
+
274
+ source "qemu" "windows" {
275
+ vm_name = "sandbox-windows-${var.windows_version}-arm64-qemu.qcow2"
276
+ output_directory = "${var.build_dir}/output"
277
+
278
+ # The build flow (agent-dev-env build) verifies the ISO against
279
+ # var.iso_sha256 before Packer runs; iso_checksum is "none" so Packer
280
+ # does not re-verify.
281
+ iso_url = var.iso_path
282
+ iso_checksum = "none"
283
+
284
+ cpus = var.cpu_count
285
+ memory = var.memory_gb * 1024
286
+ disk_size = "${var.disk_size}G"
287
+ format = "qcow2"
288
+ disk_interface = "virtio"
289
+ net_device = "virtio-net-pci"
290
+
291
+ machine_type = "virt,gic-version=max"
292
+ accelerator = "hvf"
293
+ cpu_model = "host"
294
+
295
+ efi_boot = true
296
+ efi_firmware_code = var.efi_firmware_code
297
+ efi_firmware_vars = var.efi_firmware_vars
298
+
299
+ qemu_binary = var.qemu_binary
300
+
301
+ # Unattend CD: Windows Setup probes attached media for Autounattend.xml
302
+ # at the root and applies it before the language picker, so no keyboard
303
+ # input is needed — except for the firmware's ~5 s "Press any key to
304
+ # boot from CD" prompt, hence the enter-spam below.
305
+ cd_files = ["./autounattend.xml", "${var.build_dir}/drivers"]
306
+ cd_label = "UNATTEND"
307
+
308
+ # WinRM, not SSH: it comes up via autounattend's FirstLogonCommands
309
+ # before any OpenSSH server exists, and DISM online servicing needs the
310
+ # elevated token the provisioners request.
311
+ communicator = "winrm"
312
+ winrm_username = var.winrm_username
313
+ winrm_password = var.winrm_password
314
+ winrm_timeout = "90m"
315
+ winrm_port = 5985
316
+
317
+ # Headless: no QEMU window, so the cocoa quit-confirmation dialog can
318
+ # never stall a build (it popped up on every windowed run). Progress is
319
+ # still watchable via the plugin's VNC server (e.g. vncdotool captures).
320
+ headless = true
321
+
322
+ # VNC server for the build watchdog (bundled watch-build.py): pinned to a
323
+ # single port so the build flow can start the watchdog before
324
+ # `packer build` without scanning for the port.
325
+ vnc_bind_address = "127.0.0.1"
326
+ vnc_port_min = 5901
327
+ vnc_port_max = 5901
328
+
329
+ # Enter-spam: answers the firmware's "Press ESC in 1 second" shell prompt
330
+ # AND Windows' "Press any key to boot from CD or DVD" prompt on the first
331
+ # boot. Without it, a boot that lands in the EFI shell stalls forever
332
+ # (Windows cannot be booted reliably from the shell). The keys are typed
333
+ # within the first ~15 s, long before Setup's UI appears, so the Cancel
334
+ # dialog they trigger on the "Installing Windows 11" screen is auto-
335
+ # dismissed by the build watchdog (bundled watch-build.py on the host).
336
+ boot_wait = "1s"
337
+ boot_command = [
338
+ "<enter><wait1><enter><wait1><enter><wait1><enter><wait1><enter><wait1><enter><wait1><enter><wait1><enter><wait1><enter><wait1><enter><wait1><enter><wait1><enter><wait1><enter><wait1><enter><wait1><enter>",
339
+ ]
340
+ }
341
+
342
+ # ---------------------------------------------------------------------------
343
+ # Build
344
+ # ---------------------------------------------------------------------------
345
+
346
+ build {
347
+ sources = ["source.qemu.windows"]
348
+
349
+ # ===== Preamble: settle after OOBE, dump versions =====
350
+ provisioner "powershell" {
351
+ elevated_user = var.winrm_username
352
+ elevated_password = var.winrm_password
353
+ inline = [
354
+ "$ErrorActionPreference = 'Stop'",
355
+ "$ProgressPreference = 'SilentlyContinue'",
356
+ "$os = Get-CimInstance Win32_OperatingSystem",
357
+ "Write-Host \"Windows install complete: $($os.Caption) build $($os.BuildNumber) ($env:PROCESSOR_ARCHITECTURE)\"",
358
+ "Write-Host 'Waiting 30s for post-OOBE first-logon scripts to settle...'",
359
+ "Start-Sleep -Seconds 30",
360
+ ]
361
+ }
362
+
363
+ # ===== VirtIO guest tools (full driver suite + qemu guest agent) =====
364
+ # The MSI lives on the virtio-win CD attached by qemu-with-tpm.sh (the
365
+ # last usb-storage device). WinPE got the boot-critical drivers from the
366
+ # unattend CD; this registers everything else in the installed OS.
367
+ provisioner "powershell" {
368
+ elevated_user = var.winrm_username
369
+ elevated_password = var.winrm_password
370
+ inline = [
371
+ "$ErrorActionPreference = 'Stop'",
372
+ # The virtio-win CD may not be mounted yet right after first logon;
373
+ # retry for up to ~2 min before giving up.
374
+ "$msi = $null",
375
+ "for ($try = 0; $try -lt 8 -and -not $msi; $try++) {",
376
+ " $cdroms = Get-WmiObject Win32_CDROMDrive | Select-Object -ExpandProperty Drive",
377
+ " foreach ($drive in $cdroms) {",
378
+ " $msi = Get-ChildItem $drive -Filter 'virtio-win-guest-tools*.msi' -ErrorAction SilentlyContinue | Select-Object -First 1",
379
+ " if ($msi) { break }",
380
+ " }",
381
+ " if (-not $msi) { Start-Sleep -Seconds 15 }",
382
+ "}",
383
+ "if (-not $msi) {",
384
+ " Write-Warning 'virtio-win-guest-tools MSI not found on any CD-ROM; skipping guest tools'",
385
+ "} else {",
386
+ " Write-Host \"Installing $($msi.Name)\"",
387
+ " Start-Process msiexec.exe -ArgumentList \"/i `\"$($msi.FullName)`\" /qn /norestart\" -Wait",
388
+ " Start-Sleep -Seconds 5",
389
+ "}",
390
+ "$ga = Get-Service -Name 'QEMU-GA' -ErrorAction SilentlyContinue",
391
+ "if ($ga) { Set-Service 'QEMU-GA' -StartupType Automatic; Start-Service 'QEMU-GA' -ErrorAction SilentlyContinue }",
392
+ ]
393
+ }
394
+
395
+ # ===== Chocolatey + .NET strong crypto =====
396
+ provisioner "powershell" {
397
+ elevated_user = var.winrm_username
398
+ elevated_password = var.winrm_password
399
+ inline = [
400
+ "$ErrorActionPreference = 'Stop'",
401
+ "[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072",
402
+ "iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))",
403
+ "if ($LASTEXITCODE -ne 0) { throw \"chocolatey install failed: $LASTEXITCODE\" }",
404
+ "$chocoInstall = [System.Environment]::GetEnvironmentVariable('ChocolateyInstall','Machine')",
405
+ "if (-not $chocoInstall) { $chocoInstall = 'C:\\ProgramData\\chocolatey' }",
406
+ "$chocoBin = Join-Path $chocoInstall 'bin'",
407
+ "# Persist the Chocolatey bin dir into the machine PATH ourselves: the",
408
+ "# bootstrapper's compiled Install-ChocolateyPath cmdlet asserts UAC",
409
+ "# before writing and can fail to persist the registry value in the",
410
+ "# elevated WinRM context (observed as 'choco' not recognized in the",
411
+ "# toolchain phase after the reboot, even with PATH re-read).",
412
+ "$machinePath = [System.Environment]::GetEnvironmentVariable('Path','Machine').TrimEnd(';')",
413
+ "if (-not (($machinePath -split ';') -contains $chocoBin)) {",
414
+ " [System.Environment]::SetEnvironmentVariable('Path', $machinePath + ';' + $chocoBin, 'Machine')",
415
+ "}",
416
+ "# Run choco by its full path too — this session's PATH is not",
417
+ "# guaranteed to contain the bin dir above.",
418
+ "$choco = Join-Path $chocoBin 'choco.exe'",
419
+ "if (-not (Test-Path $choco)) { throw \"choco.exe not found at $choco\" }",
420
+ "& $choco feature enable -n allowGlobalConfirmation",
421
+ "if ($LASTEXITCODE -ne 0) { throw \"choco feature enable failed: $LASTEXITCODE\" }",
422
+ "# Legacy .NET strong-crypto: without it, older TLS stacks fail against modern hosts",
423
+ "reg add 'HKLM\\SOFTWARE\\Microsoft\\.NETFramework\\v4.0.30319' /v SchUseStrongCrypto /t REG_DWORD /d 1 /f",
424
+ "reg add 'HKLM\\SOFTWARE\\Wow6432Node\\Microsoft\\.NETFramework\\v4.0.30319' /v SchUseStrongCrypto /t REG_DWORD /d 1 /f",
425
+ ]
426
+ }
427
+
428
+ # ===== Reboot: clear the tools-install pending reboot =====
429
+ # The virtio-win guest-tools MSI install leaves a pending reboot, which
430
+ # makes `choco install` return 3010 and makes the .NET Framework 4.8
431
+ # Developer Pack installer fail with exit code 1 (it refuses to run
432
+ # while a reboot is pending). Restart once and wait for WinRM, so the
433
+ # toolchain + VS phases run on a cleanly booted machine.
434
+ provisioner "windows-restart" {
435
+ restart_timeout = "30m"
436
+ }
437
+
438
+ # ===== Toolchain via Chocolatey (versions from the vars file) =====
439
+ provisioner "powershell" {
440
+ elevated_user = var.winrm_username
441
+ elevated_password = var.winrm_password
442
+ inline = [<<-END
443
+ $ErrorActionPreference = 'Stop'
444
+ $ProgressPreference = 'SilentlyContinue'
445
+ # Choco is called by its full path, never via PATH: the bootstrapper
446
+ # can fail to persist the machine PATH (see the Chocolatey
447
+ # provisioner), and after the guest-tools reboot a fresh WinRM
448
+ # process can inherit a stale PATH — both observed as 'choco' not
449
+ # recognized. The PATH is still re-read from the registry below for
450
+ # the tools this phase installs.
451
+ $chocoInstall = [System.Environment]::GetEnvironmentVariable('ChocolateyInstall','Machine')
452
+ if (-not $chocoInstall) { $chocoInstall = 'C:\ProgramData\chocolatey' }
453
+ $choco = Join-Path (Join-Path $chocoInstall 'bin') 'choco.exe'
454
+ if (-not (Test-Path $choco)) { throw "choco.exe not found at $choco (Chocolatey install failed?)" }
455
+ $env:Path = [System.Environment]::GetEnvironmentVariable('Path','Machine') + ';' + [System.Environment]::GetEnvironmentVariable('Path','User')
456
+ & $choco install nodejs --version=${var.nodejs_version} -y
457
+ if ($LASTEXITCODE -ne 0) { throw "choco nodejs failed: $LASTEXITCODE" }
458
+ & $choco install gh --version=${var.github_cli_version} -y
459
+ if ($LASTEXITCODE -ne 0) { throw "choco gh failed: $LASTEXITCODE" }
460
+ & $choco install ripgrep --version=${var.ripgrep_version} -y
461
+ if ($LASTEXITCODE -ne 0) { throw "choco ripgrep failed: $LASTEXITCODE" }
462
+ & $choco install git --version=${var.git_version} -y
463
+ if ($LASTEXITCODE -ne 0) { throw "choco git failed: $LASTEXITCODE" }
464
+ & $choco install jq --version=${var.jq_version} -y
465
+ if ($LASTEXITCODE -ne 0) { throw "choco jq failed: $LASTEXITCODE" }
466
+ & $choco install python --version=${var.python_version} -y
467
+ if ($LASTEXITCODE -ne 0) { throw "choco python failed: $LASTEXITCODE" }
468
+ & $choco install firefox curl docker-cli docker-compose -y
469
+ if ($LASTEXITCODE -ne 0) { throw "choco browsers/docker failed: $LASTEXITCODE" }
470
+
471
+ # C/C++ + cross-language toolchains (brought over from AdGuard's
472
+ # build-agent-images windows2022-vs2022 / windows2022-go images).
473
+ # VS2022 Build Tools (with its .NET/VC++ workloads) and Rust are
474
+ # installed by their own provisioners below.
475
+ & $choco install golang --version=${var.go_version} -y
476
+ if ($LASTEXITCODE -ne 0) { throw "choco golang failed: $LASTEXITCODE" }
477
+ & $choco install mingw --version=${var.mingw_version} -y
478
+ if ($LASTEXITCODE -ne 0) { throw "choco mingw failed: $LASTEXITCODE" }
479
+ & $choco install make --version=${var.make_version} -y
480
+ if ($LASTEXITCODE -ne 0) { throw "choco make failed: $LASTEXITCODE" }
481
+ & $choco install vim --version=${var.vim_version} -y
482
+ if ($LASTEXITCODE -ne 0) { throw "choco vim failed: $LASTEXITCODE" }
483
+ & $choco install nuget.commandline --version=${var.nuget_version} -y
484
+ if ($LASTEXITCODE -ne 0) { throw "choco nuget.commandline failed: $LASTEXITCODE" }
485
+ & $choco install protoc --version=${var.protoc_version} -y
486
+ if ($LASTEXITCODE -ne 0) { throw "choco protoc failed: $LASTEXITCODE" }
487
+ & $choco install nasm --version=${var.nasm_version} -y
488
+ if ($LASTEXITCODE -ne 0) { throw "choco nasm failed: $LASTEXITCODE" }
489
+ & $choco install llvm --version=${var.llvm_version} -y
490
+ if ($LASTEXITCODE -ne 0) { throw "choco llvm failed: $LASTEXITCODE" }
491
+ & $choco install wixtoolset --version=${var.wixtoolset_version} -y
492
+ if ($LASTEXITCODE -ne 0) { throw "choco wixtoolset failed: $LASTEXITCODE" }
493
+
494
+ # ===== Google Chrome (CfT snapshot, hash-pinned) =====
495
+ # choco's googlechrome package downloads the live dl.google.com MSI,
496
+ # whose binary rotates with every Chrome release — the pinned hash
497
+ # breaks between releases (observed with 152.0.7977.54). Instead we
498
+ # fetch the versioned Chrome for Testing archive, verify the SHA256,
499
+ # and extract it into the standard install location.
500
+ $chromeUrl = "https://storage.googleapis.com/chrome-for-testing-public/${var.chrome_version}/win64/chrome-win64.zip"
501
+ $chromeZip = "$env:TEMP\chrome-win64.zip"
502
+ $chromeAppDir = 'C:\Program Files\Google\Chrome\Application'
503
+ $chromeExe = Join-Path $chromeAppDir 'chrome.exe'
504
+ # 202 MB over usermode networking can drop mid-transfer; retry.
505
+ $downloaded = $false
506
+ for ($try = 1; $try -le 3 -and -not $downloaded; $try++) {
507
+ try {
508
+ Invoke-WebRequest -UseBasicParsing -Uri $chromeUrl -OutFile $chromeZip -ErrorAction Stop
509
+ $downloaded = $true
510
+ } catch {
511
+ Write-Host "chrome download attempt $try failed: $($_.Exception.Message)"
512
+ if ($try -lt 3) { Start-Sleep -Seconds 10 }
513
+ }
514
+ }
515
+ if (-not $downloaded) { throw "chrome download failed after 3 attempts" }
516
+ $actual = (Get-FileHash -Path $chromeZip -Algorithm SHA256).Hash.ToLowerInvariant()
517
+ if ($actual -ne '${var.chrome_sha256}') {
518
+ throw "chrome checksum mismatch: expected ${var.chrome_sha256}, got $actual"
519
+ }
520
+ New-Item -ItemType Directory -Force -Path "$env:TEMP\chrome-x" | Out-Null
521
+ Expand-Archive -Path $chromeZip -DestinationPath "$env:TEMP\chrome-x" -Force
522
+ New-Item -ItemType Directory -Force -Path $chromeAppDir | Out-Null
523
+ Copy-Item -Path "$env:TEMP\chrome-x\chrome-win64\*" -Destination $chromeAppDir -Recurse -Force
524
+ Remove-Item -Path "$env:TEMP\chrome-x", $chromeZip -Recurse -Force
525
+ # Register App Paths so `start chrome` / shell launchers resolve it
526
+ $appPaths = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\chrome.exe'
527
+ New-Item -Path $appPaths -Force | Out-Null
528
+ New-ItemProperty -Path $appPaths -Name '(Default)' -Value $chromeExe -PropertyType String -Force | Out-Null
529
+ if (-not (Test-Path $chromeExe)) { throw "chrome.exe not found after extraction" }
530
+ $toolPaths = @(
531
+ 'C:\Program Files\nodejs',
532
+ 'C:\ProgramData\chocolatey\bin',
533
+ 'C:\Program Files\Git\cmd',
534
+ 'C:\Program Files\Git\bin',
535
+ 'C:\Program Files\Google\Chrome\Application',
536
+ 'C:\Program Files\Go\bin',
537
+ 'C:\Program Files\LLVM\bin',
538
+ 'C:\Program Files\NASM',
539
+ # WiX 3.14.x: newer package versions install the MSI under
540
+ # 'Program Files\WiX Toolset v3.14\bin' (older ones put the
541
+ # binaries in the choco lib) — add both, the guards skip absent.
542
+ 'C:\Program Files (x86)\WiX Toolset v3.14\bin',
543
+ 'C:\ProgramData\chocolatey\lib\wixtoolset\tools',
544
+ 'C:\mingw64\bin',
545
+ "$env:APPDATA\npm"
546
+ )
547
+ $machinePath = [System.Environment]::GetEnvironmentVariable('Path','Machine').TrimEnd(';')
548
+ foreach ($toolPath in $toolPaths) {
549
+ if ((Test-Path $toolPath) -and -not (($machinePath -split ';') -contains $toolPath)) {
550
+ $machinePath = $machinePath + ';' + $toolPath
551
+ }
552
+ }
553
+ [System.Environment]::SetEnvironmentVariable('Path', $machinePath, 'Machine')
554
+ $env:Path = $machinePath + ';' + [System.Environment]::GetEnvironmentVariable('Path','User')
555
+ node --version
556
+ git --version
557
+ gh --version
558
+ rg --version
559
+ jq --version
560
+ python --version
561
+ go version
562
+ END
563
+ ]
564
+ }
565
+
566
+ # ===== Visual Studio 2022 Build Tools (MSVC + .NET SDKs + CMake + Win11 SDK) =====
567
+ # Brought over from AdGuard's build-agent-images windows2022-base-vs2022
568
+ # image: the choco package installs the VS bootstrapper, then setup.exe
569
+ # adds the .NET 4.8 + .NET Core SDKs, the VC++ workload (x86/x64/ARM/
570
+ # ARM64 — ARM64 matters for this ARM64 guest) and the Windows 11 SDK.
571
+ # On ARM64 the bootstrapper runs as an x86 binary under emulation, which
572
+ # VS2022 supports.
573
+ provisioner "powershell" {
574
+ elevated_user = var.winrm_username
575
+ elevated_password = var.winrm_password
576
+ timeout = "90m"
577
+ inline = [<<-END
578
+ $ErrorActionPreference = 'Stop'
579
+ $ProgressPreference = 'SilentlyContinue'
580
+ # Choco is called by its full path, never via PATH (see the
581
+ # toolchain provisioner). The PATH is still re-read for the tools
582
+ # this phase installs.
583
+ $chocoInstall = [System.Environment]::GetEnvironmentVariable('ChocolateyInstall','Machine')
584
+ if (-not $chocoInstall) { $chocoInstall = 'C:\ProgramData\chocolatey' }
585
+ $choco = Join-Path (Join-Path $chocoInstall 'bin') 'choco.exe'
586
+ if (-not (Test-Path $choco)) { throw "choco.exe not found at $choco (Chocolatey install failed?)" }
587
+ $env:Path = [System.Environment]::GetEnvironmentVariable('Path','Machine') + ';' + [System.Environment]::GetEnvironmentVariable('Path','User')
588
+ # Standalone .NET Framework 4.8 Developer Pack (latest), as in the
589
+ # AdGuard image — MSBuild/.NET 4.8 targets need it. 3010 = success,
590
+ # reboot required, which choco also returns for the KB dep.
591
+ & $choco install netfx-4.8-devpack -y --norestart
592
+ if ($LASTEXITCODE -ne 0 -and $LASTEXITCODE -ne 3010) { throw "choco netfx-4.8-devpack failed: $LASTEXITCODE" }
593
+ & $choco install visualstudio2022buildtools --version=${var.vs_buildtools_version} -y --norestart --wait --nocache --noUpdateInstaller
594
+ if ($LASTEXITCODE -ne 0 -and $LASTEXITCODE -ne 3010) { throw "choco visualstudio2022buildtools failed: $LASTEXITCODE" }
595
+ $setup = 'C:\Program Files (x86)\Microsoft Visual Studio\Installer\setup.exe'
596
+ if (-not (Test-Path $setup)) { throw "VS installer not found: $setup" }
597
+ # .NET 4.8 SDK + .NET Core SDK workloads
598
+ & $setup modify --quiet --norestart `
599
+ --installPath 'C:\Program Files (x86)\Microsoft Visual Studio\2022\BuildTools' `
600
+ --add Microsoft.Net.Component.4.8.SDK `
601
+ --add Microsoft.NetCore.Component.SDK `
602
+ | Out-Host
603
+ if ($LASTEXITCODE -ne 0 -and $LASTEXITCODE -ne 3010) { throw "vs .NET workloads failed: $LASTEXITCODE" }
604
+ # VC++ workload (x86/x64/ARM/ARM64) + CMake project support
605
+ & $setup modify --quiet --norestart `
606
+ --installPath 'C:\Program Files (x86)\Microsoft Visual Studio\2022\BuildTools' `
607
+ --add Microsoft.VisualStudio.Workload.VCTools `
608
+ --add Microsoft.VisualStudio.Component.VC.CMake.Project `
609
+ --add Microsoft.VisualStudio.Component.VC.Tools.x86.x64 `
610
+ --add Microsoft.VisualStudio.Component.VC.Tools.ARM `
611
+ --add Microsoft.VisualStudio.Component.VC.Tools.ARM64 `
612
+ | Out-Host
613
+ if ($LASTEXITCODE -ne 0 -and $LASTEXITCODE -ne 3010) { throw "vs VC++ workloads failed: $LASTEXITCODE" }
614
+ # Windows 11 SDK (the components the Win11 toolchain expects)
615
+ & $setup modify --quiet --norestart `
616
+ --installPath 'C:\Program Files (x86)\Microsoft Visual Studio\2022\BuildTools' `
617
+ --add Microsoft.VisualStudio.Component.Windows11SDK.22621 `
618
+ | Out-Host
619
+ if ($LASTEXITCODE -ne 0 -and $LASTEXITCODE -ne 3010) { throw "vs Windows11SDK failed: $LASTEXITCODE" }
620
+ $vcPath = 'C:\Program Files (x86)\Microsoft Visual Studio\2022\BuildTools\VC\Auxiliary\Build'
621
+ $machinePath = [System.Environment]::GetEnvironmentVariable('Path','Machine').TrimEnd(';')
622
+ if (-not (($machinePath -split ';') -contains $vcPath)) {
623
+ [System.Environment]::SetEnvironmentVariable('Path', $machinePath + ';' + $vcPath, 'Machine')
624
+ }
625
+ Write-Host 'Visual Studio 2022 Build Tools + .NET SDKs installed'
626
+ END
627
+ ]
628
+ }
629
+
630
+ # ===== Rust via rustup (ARM64 host toolchain + MSVC cross targets) =====
631
+ # Brought over from AdGuard's build-agent-images windows2022-vs2022
632
+ # image, adapted for ARM64: win.rustup.rs/aarch64 ships the aarch64
633
+ # rustup-init (win.rustup.rs/x86_64 is the x86_64 host only). The
634
+ # default-toolchain is resolved by rustup (e.g. '1.95' -> 1.95.x), and
635
+ # the MSVC targets cover x86_64/i686/aarch64 linking.
636
+ provisioner "powershell" {
637
+ elevated_user = var.winrm_username
638
+ elevated_password = var.winrm_password
639
+ timeout = "30m"
640
+ inline = [<<-END
641
+ $ErrorActionPreference = 'Stop'
642
+ $ProgressPreference = 'SilentlyContinue'
643
+ $rustupInit = "$env:TEMP\rustup-init.exe"
644
+ $downloaded = $false
645
+ for ($try = 1; $try -le 3 -and -not $downloaded; $try++) {
646
+ try {
647
+ Invoke-WebRequest -UseBasicParsing -Uri 'https://win.rustup.rs/aarch64' -OutFile $rustupInit -ErrorAction Stop
648
+ $downloaded = $true
649
+ } catch {
650
+ Write-Host "rustup-init download attempt $try failed: $($_.Exception.Message)"
651
+ if ($try -lt 3) { Start-Sleep -Seconds 10 }
652
+ }
653
+ }
654
+ if (-not $downloaded) { throw "rustup-init download failed after 3 attempts" }
655
+ & $rustupInit -y --default-toolchain ${var.rust_version}
656
+ if ($LASTEXITCODE -ne 0) { throw "rustup-init failed: $LASTEXITCODE" }
657
+ Remove-Item $rustupInit -Force
658
+ $cargoBin = "$env:USERPROFILE\.cargo\bin"
659
+ $machinePath = [System.Environment]::GetEnvironmentVariable('Path','Machine').TrimEnd(';')
660
+ if (-not (($machinePath -split ';') -contains $cargoBin)) {
661
+ [System.Environment]::SetEnvironmentVariable('Path', $machinePath + ';' + $cargoBin, 'Machine')
662
+ }
663
+ $env:Path = [System.Environment]::GetEnvironmentVariable('Path','Machine') + ';' + [System.Environment]::GetEnvironmentVariable('Path','User')
664
+ rustup target add x86_64-pc-windows-msvc i686-pc-windows-msvc aarch64-pc-windows-msvc
665
+ if ($LASTEXITCODE -ne 0) { throw "rustup target add failed: $LASTEXITCODE" }
666
+ Write-Host "Rust ${var.rust_version} installed with MSVC targets"
667
+ END
668
+ ]
669
+ }
670
+
671
+ # ===== Visual Studio Code (native arm64, direct download) =====
672
+ provisioner "powershell" {
673
+ elevated_user = var.winrm_username
674
+ elevated_password = var.winrm_password
675
+ inline = [<<-END
676
+ $ErrorActionPreference = 'Stop'
677
+ $ProgressPreference = 'SilentlyContinue'
678
+ $installer = "$env:TEMP\vscode-arm64-user-setup.exe"
679
+ $downloaded = $false
680
+ for ($try = 1; $try -le 3 -and -not $downloaded; $try++) {
681
+ try {
682
+ Invoke-WebRequest -UseBasicParsing -Uri 'https://update.code.visualstudio.com/latest/win32-arm64-user/stable' -OutFile $installer -ErrorAction Stop
683
+ $downloaded = $true
684
+ } catch {
685
+ Write-Host "vscode download attempt $try failed: $($_.Exception.Message)"
686
+ if ($try -lt 3) { Start-Sleep -Seconds 10 }
687
+ }
688
+ }
689
+ if (-not $downloaded) { throw "vscode download failed after 3 attempts" }
690
+ Start-Process $installer -ArgumentList '/VERYSILENT /NORESTART /MERGETASKS=!runcode' -Wait
691
+ Remove-Item $installer -Force
692
+ $codeBin = "$env:LOCALAPPDATA\Programs\Microsoft VS Code\bin"
693
+ if (Test-Path $codeBin) {
694
+ $machinePath = [System.Environment]::GetEnvironmentVariable('Path','Machine').TrimEnd(';')
695
+ if (-not (($machinePath -split ';') -contains $codeBin)) {
696
+ [System.Environment]::SetEnvironmentVariable('Path', $machinePath + ';' + $codeBin, 'Machine')
697
+ }
698
+ }
699
+ END
700
+ ]
701
+ }
702
+
703
+ # ===== OpenCode, OpenCodeReview, OpenChamber web UI =====
704
+ provisioner "powershell" {
705
+ elevated_user = var.winrm_username
706
+ elevated_password = var.winrm_password
707
+ inline = [<<-END
708
+ # EAP=Continue, not Stop: this script runs many native commands
709
+ # (schtasks, npm, openchamber) whose stderr lines become terminating
710
+ # errors under Stop in Windows PowerShell 5.1 (the 2>$null / 2>&1
711
+ # native-stderr bug). All failure handling is explicit below.
712
+ $ErrorActionPreference = 'Continue'
713
+ $ProgressPreference = 'SilentlyContinue'
714
+ # The image ships with Windows' default Restricted execution policy —
715
+ # every build-time powershell invocation passes -ExecutionPolicy
716
+ # Bypass, so nothing is persisted. But opencode/ocr are npm shims
717
+ # (opencode.ps1 in %APPDATA%\npm): a Restricted shell refuses to run
718
+ # them ("running scripts is disabled on this system"). Bake in
719
+ # RemoteSigned machine-wide (this provisioner runs elevated, so the
720
+ # LocalMachine scope persists in the image) and keep the runners'
721
+ # runtime set as a fallback for images built before this change.
722
+ # The build passes -ExecutionPolicy Bypass at Process scope, so
723
+ # setting only LocalMachine triggers Set-ExecutionPolicy's "overridden
724
+ # by a more specific scope" notice, which Windows PowerShell 5.1 under
725
+ # WinRM surfaces as a terminating error that aborts the build even
726
+ # though the machine policy was updated fine (observed). Set the
727
+ # Process scope first (no override, no notice), then the machine.
728
+ try {
729
+ Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope Process -Force -ErrorAction SilentlyContinue
730
+ Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope LocalMachine -Force -ErrorAction SilentlyContinue
731
+ } catch {
732
+ Write-Warning "Set-ExecutionPolicy failed: $($_.Exception.Message)"
733
+ }
734
+ Write-Host "PowerShell execution policy: $(Get-ExecutionPolicy -Scope LocalMachine)"
735
+ $machinePath = [System.Environment]::GetEnvironmentVariable('Path','Machine').TrimEnd(';')
736
+ $env:Path = $machinePath + ';' + [System.Environment]::GetEnvironmentVariable('Path','User')
737
+ # The openchamber scheduled task launches at logon and locks its
738
+ # node_modules — kill it before reinstalling, and retry npm installs
739
+ # (the guest's usermode networking drops large transfers).
740
+ try { schtasks /End /TN dev.openchamber.web 2>&1 | Out-Null } catch {}
741
+ Stop-Process -Name node -Force -ErrorAction SilentlyContinue
742
+ function Invoke-NpmRetry([string]$argsLine) {
743
+ for ($try = 1; $try -le 3; $try++) {
744
+ cmd /c "npm $argsLine" 2>&1 | Out-Null
745
+ if ($LASTEXITCODE -eq 0) { return }
746
+ Write-Host "npm $argsLine attempt $try failed ($LASTEXITCODE); retrying"
747
+ Start-Sleep -Seconds 10
748
+ }
749
+ throw "npm $argsLine failed after 3 attempts"
750
+ }
751
+ Invoke-NpmRetry "install -g opencode-ai"
752
+ New-Item -ItemType Directory -Force -Path C:\npm-global | Out-Null
753
+ Invoke-NpmRetry "install -g --prefix C:\npm-global --ignore-scripts @alibaba-group/open-code-review@${var.open_code_review_version}"
754
+ foreach ($toolPath in @('C:\npm-global')) {
755
+ if ((Test-Path $toolPath) -and -not (($machinePath -split ';') -contains $toolPath)) {
756
+ $machinePath = $machinePath + ';' + $toolPath
757
+ }
758
+ }
759
+ [System.Environment]::SetEnvironmentVariable('Path', $machinePath, 'Machine')
760
+ Invoke-NpmRetry "install -g @openchamber/web"
761
+ $env:Path = [System.Environment]::GetEnvironmentVariable('Path','Machine') + ';' + [System.Environment]::GetEnvironmentVariable('Path','User')
762
+ opencode --version
763
+ ocr --version
764
+ # Web UI as a native service (Windows: scheduled task). Snapshot the
765
+ # absolute opencode path like the macOS template does.
766
+ $opencodeBin = (Get-Command opencode -ErrorAction Stop).Source
767
+ $env:OPENCODE_BINARY = $opencodeBin
768
+ # openchamber's own `startup enable` writes startup.env but then fails
769
+ # to register the task — its schtasks /TR command exceeds the 261-char
770
+ # limit and the failure aborts the whole provisioner. So: let it write
771
+ # the env file, ignore the failure, and register a compact task that
772
+ # runs a small wrapper script instead of a giant inline command.
773
+ try {
774
+ openchamber startup enable --port ${var.openchamber_port} --lan --ui-password "${var.openchamber_ui_password}" 2>$null | Out-Null
775
+ } catch {}
776
+ $LASTEXITCODE = 0
777
+ $envFile = 'C:\Users\Administrator\.config\openchamber\startup.env'
778
+ if (-not (Test-Path $envFile)) {
779
+ New-Item -ItemType Directory -Force -Path (Split-Path $envFile) | Out-Null
780
+ @"
781
+ OPENCHAMBER_UI_PASSWORD='${var.openchamber_ui_password}'
782
+ OPENCODE_BINARY='$opencodeBin'
783
+ "@ | Set-Content -Path $envFile -Encoding UTF8
784
+ }
785
+ New-Item -ItemType Directory -Force -Path C:\tools | Out-Null
786
+ $wrapper = @'
787
+ $envFile = 'C:\Users\Administrator\.config\openchamber\startup.env'
788
+ if (Test-Path $envFile) {
789
+ Get-Content $envFile | ForEach-Object {
790
+ if ($_ -match '^([^=]+)=(.*)$') {
791
+ $v = $matches[2]
792
+ if ($v.StartsWith("'") -and $v.EndsWith("'")) {
793
+ $v = $v.Substring(1, $v.Length - 2).Replace("'\''", "'")
794
+ }
795
+ [Environment]::SetEnvironmentVariable($matches[1], $v, 'Process')
796
+ }
797
+ }
798
+ }
799
+ & 'C:\Program Files\nodejs\node.exe' 'C:\Users\Administrator\AppData\Roaming\npm\node_modules\@openchamber\web\bin\cli.js' serve --foreground --port ${var.openchamber_port} --host 0.0.0.0
800
+ '@
801
+ Set-Content -Path C:\tools\openchamber-startup.ps1 -Value $wrapper -Encoding UTF8
802
+ # Register the logon task. schtasks.exe from the elevated WinRM
803
+ # session can be flaky (silent /Create failures, /Run "file not
804
+ # found"); fall back to the PowerShell-native Register-ScheduledTask
805
+ # (same mechanism the build-agent-images repo uses). Never fail the
806
+ # build here — worst case the web UI starts at first login.
807
+ $taskRegistered = $false
808
+ try {
809
+ schtasks /Create /TN dev.openchamber.web /SC ONLOGON /RL LIMITED /F /TR "powershell.exe -NoProfile -ExecutionPolicy Bypass -File C:\tools\openchamber-startup.ps1" 2>&1 | Out-Null
810
+ if ($LASTEXITCODE -eq 0) {
811
+ schtasks /Run /TN dev.openchamber.web 2>&1 | Out-Null
812
+ if ($LASTEXITCODE -eq 0) { $taskRegistered = $true }
813
+ }
814
+ $LASTEXITCODE = 0
815
+ } catch {
816
+ Write-Warning "schtasks registration failed ($($_.Exception.Message)); trying Register-ScheduledTask"
817
+ $LASTEXITCODE = 0
818
+ }
819
+ if (-not $taskRegistered) {
820
+ try {
821
+ $action = New-ScheduledTaskAction -Execute 'powershell.exe' -Argument '-NoProfile -ExecutionPolicy Bypass -File C:\tools\openchamber-startup.ps1'
822
+ $trigger = New-ScheduledTaskTrigger -AtLogOn
823
+ Register-ScheduledTask -TaskName 'dev.openchamber.web' -Action $action -Trigger $trigger -Force | Out-Null
824
+ Start-ScheduledTask -TaskName 'dev.openchamber.web'
825
+ $taskRegistered = $true
826
+ } catch {
827
+ Write-Warning "Register-ScheduledTask also failed: $($_.Exception.Message); the web UI will start at first login instead"
828
+ $LASTEXITCODE = 0
829
+ }
830
+ }
831
+ if ($taskRegistered) { Write-Host "OpenChamber task registered: $taskRegistered" }
832
+ # Windows' firewall "TCP/UDP Query User" rules for node.exe default to
833
+ # BLOCK when the allow prompt goes unanswered (headless build) — the
834
+ # web UI then only answers on the guest loopback. Remove them and
835
+ # allow port 4000 explicitly so the host can reach the UI.
836
+ Get-NetFirewallRule -Direction Inbound -Action Block -ErrorAction SilentlyContinue |
837
+ Where-Object { $_.DisplayName -like '*Query User*node.exe*' } |
838
+ Remove-NetFirewallRule -ErrorAction SilentlyContinue
839
+ New-NetFirewallRule -DisplayName 'OpenChamber' -Direction Inbound -Action Allow -Protocol TCP -LocalPort ${var.openchamber_port} -Profile Any -ErrorAction SilentlyContinue | Out-Null
840
+ $LASTEXITCODE = 0
841
+ END
842
+ ]
843
+ }
844
+
845
+ # ===== OpenSSH Server + RDP =====
846
+ # sshd gives the host a management channel (agent-dev-env run windows-qemu
847
+ # forwards guest 22 to host 2222); RDP gives a desktop session. Password
848
+ # auth is enabled for the Administrator account so the fixed sandbox
849
+ # credentials from the vars file work.
850
+ provisioner "powershell" {
851
+ elevated_user = var.winrm_username
852
+ elevated_password = var.winrm_password
853
+ inline = [<<-END
854
+ $ErrorActionPreference = 'Stop'
855
+ # Note: Add-WindowsCapability is a cmdlet — it does NOT set
856
+ # $LASTEXITCODE (a stale $null made the old `if ($LASTEXITCODE -ne 0)`
857
+ # check false-fire and abort the build even on success). Failures
858
+ # surface as terminating errors via -ErrorAction Stop instead.
859
+ Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0 -ErrorAction Stop
860
+ Set-Service -Name sshd -StartupType Automatic
861
+ $cfg = 'C:\ProgramData\ssh\sshd_config'
862
+ if (Test-Path $cfg) {
863
+ (Get-Content $cfg) -replace '#PasswordAuthentication yes','PasswordAuthentication yes' `
864
+ -replace 'Match Group administrators','#Match Group administrators' `
865
+ -replace ' AuthorizedKeysFile __PROGRAMDATA__/ssh/administrators_authorized_keys','# AuthorizedKeysFile __PROGRAMDATA__/ssh/administrators_authorized_keys' `
866
+ | Set-Content $cfg
867
+ }
868
+ # PowerShell as the default SSH shell
869
+ New-ItemProperty -Path 'HKLM:\SOFTWARE\OpenSSH' -Name DefaultShell -Value 'C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe' -PropertyType String -Force | Out-Null
870
+ New-NetFirewallRule -Name sshd -DisplayName 'OpenSSH Server' -Enabled True -Direction Inbound -Protocol TCP -Action Allow -LocalPort 22 | Out-Null
871
+ Start-Service sshd
872
+ # RDP
873
+ Set-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server' -Name fDenyTSConnections -Value 0
874
+ Enable-NetFirewallRule -DisplayGroup 'Remote Desktop'
875
+ END
876
+ ]
877
+ }
878
+
879
+ # ===== Bridge tooling: socat + npiperelay (best-effort) =====
880
+ # Used by the SSH-agent bridge on the host side of the sandbox runner
881
+ # (see docs/windows-qemu.md once the runner lands). Not load-bearing for the
882
+ # image itself, so a download failure only warns.
883
+ provisioner "powershell" {
884
+ elevated_user = var.winrm_username
885
+ elevated_password = var.winrm_password
886
+ inline = [<<-END
887
+ $ErrorActionPreference = 'Stop'
888
+ $ProgressPreference = 'SilentlyContinue'
889
+ try {
890
+ New-Item -ItemType Directory -Force -Path C:\tools | Out-Null
891
+ Invoke-WebRequest -UseBasicParsing -Uri 'https://github.com/jstarks/npiperelay/releases/download/v0.1.0/npiperelay_windows_amd64.zip' -OutFile "$env:TEMP\npiperelay.zip"
892
+ Expand-Archive "$env:TEMP\npiperelay.zip" -DestinationPath C:\tools -Force
893
+ Remove-Item "$env:TEMP\npiperelay.zip" -Force
894
+ Invoke-WebRequest -UseBasicParsing -Uri 'https://github.com/valorisa/socat-1.8.0.1_for_Windows/raw/main/socat.exe' -OutFile 'C:\tools\socat.exe'
895
+ $machinePath = [System.Environment]::GetEnvironmentVariable('Path','Machine').TrimEnd(';')
896
+ if (-not (($machinePath -split ';') -contains 'C:\tools')) {
897
+ [System.Environment]::SetEnvironmentVariable('Path', $machinePath + ';C:\tools', 'Machine')
898
+ }
899
+ Write-Host 'Bridge tooling installed: C:\tools\npiperelay.exe, C:\tools\socat.exe'
900
+ } catch {
901
+ Write-Warning "Bridge tooling install failed (bridges will be unavailable): $($_.Exception.Message)"
902
+ }
903
+ END
904
+ ]
905
+ }
906
+
907
+ # ===== Performance & privacy hardening =====
908
+ provisioner "powershell" {
909
+ elevated_user = var.winrm_username
910
+ elevated_password = var.winrm_password
911
+ inline = [<<-END
912
+ $ErrorActionPreference = 'Stop'
913
+ # --- Disable Defender realtime scanning (speed + flakiness) ---
914
+ Set-MpPreference -DisableRealtimeMonitoring $true
915
+ reg add 'HKLM\SOFTWARE\Policies\Microsoft\Windows Defender' /v DisableAntiSpyware /t REG_DWORD /d 1 /f
916
+ # --- Disable heavyweight / telemetry services ---
917
+ $disableServices = @(
918
+ 'WSearch', # Windows Search indexer
919
+ 'SysMain', # Superfetch
920
+ 'DiagTrack', # Diagnostics Tracking (telemetry)
921
+ 'dmwappushservice',
922
+ 'MapsBroker', # Downloaded Maps Manager
923
+ 'lfsvc', # Geolocation
924
+ 'RetailDemo',
925
+ 'wisvc' # Windows Insider
926
+ )
927
+ foreach ($svc in $disableServices) {
928
+ $s = Get-Service $svc -ErrorAction SilentlyContinue
929
+ if ($s) {
930
+ Stop-Service $svc -Force -ErrorAction SilentlyContinue
931
+ Set-Service $svc -StartupType Disabled -ErrorAction SilentlyContinue
932
+ }
933
+ }
934
+ # --- Scheduled telemetry tasks ---
935
+ @(
936
+ '\\Microsoft\\Windows\\Application Experience\\',
937
+ '\\Microsoft\\Windows\\Customer Experience Improvement Program\\'
938
+ ) | ForEach-Object {
939
+ Get-ScheduledTask -TaskPath $_ -ErrorAction SilentlyContinue | Disable-ScheduledTask -ErrorAction SilentlyContinue | Out-Null
940
+ }
941
+ # --- Filesystem + boot + power ---
942
+ fsutil behavior set disablelastaccess 1 | Out-Null
943
+ fsutil behavior set disable8dot3 1 | Out-Null
944
+ bcdedit /timeout 0 | Out-Null
945
+ powercfg /h off | Out-Null
946
+ Set-TimeZone -Id 'UTC'
947
+ END
948
+ ]
949
+ }
950
+
951
+ # ===== Final verification & cleanup =====
952
+ provisioner "powershell" {
953
+ elevated_user = var.winrm_username
954
+ elevated_password = var.winrm_password
955
+ inline = [<<-END
956
+ $ErrorActionPreference = 'Stop'
957
+ Write-Host '=== Versions ==='
958
+ $os = Get-CimInstance Win32_OperatingSystem
959
+ Write-Host "Windows: $($os.Caption) build $($os.BuildNumber)"
960
+ Write-Host "PowerShell execution policy: $(Get-ExecutionPolicy -Scope LocalMachine)"
961
+ node --version
962
+ npm --version
963
+ python --version
964
+ git --version
965
+ gh --version
966
+ rg --version
967
+ jq --version
968
+ opencode --version
969
+ ocr --version
970
+ openchamber --version
971
+ docker --version
972
+ docker compose version
973
+ # New toolchains: check-and-warn. A missing helper must never fail
974
+ # the build — e.g. llvm-config is not shipped by every LLVM Windows
975
+ # build, and choco packages do not shim every binary.
976
+ $checkTools = @(
977
+ @('go', 'version'),
978
+ @('rustc', '--version'),
979
+ @('cargo', '--version'),
980
+ @('protoc', '--version'),
981
+ @('nasm', '--version'),
982
+ @('clang', '--version'),
983
+ @('x86_64-w64-mingw32-gcc', '--version')
984
+ )
985
+ foreach ($tool in $checkTools) {
986
+ $cmd = Get-Command $tool[0] -ErrorAction SilentlyContinue
987
+ if ($cmd) {
988
+ & $cmd.Source $tool[1]
989
+ if ($LASTEXITCODE -ne 0) { Write-Warning "$($tool[0]) --version failed ($LASTEXITCODE)" }
990
+ } else {
991
+ Write-Warning "$($tool[0]) not found on PATH"
992
+ }
993
+ }
994
+ if (Test-Path 'C:\Program Files (x86)\Microsoft Visual Studio\2022\BuildTools\VC\Tools\MSVC') {
995
+ Write-Host "MSVC: $('C:\Program Files (x86)\Microsoft Visual Studio\2022\BuildTools\VC\Tools\MSVC')"
996
+ }
997
+ Write-Host "Chrome: $(Test-Path 'C:\Program Files\Google\Chrome\Application\chrome.exe')"
998
+ Write-Host "Firefox: $(Test-Path 'C:\Program Files\Mozilla Firefox\firefox.exe')"
999
+ Write-Host "VS Code: $(Test-Path "$env:LOCALAPPDATA\Programs\Microsoft VS Code\bin\code.cmd")"
1000
+ # Cleanup — must never fail the build: choco cleanup can exit non-zero
1001
+ # (locked files etc.) and that $LASTEXITCODE would otherwise propagate
1002
+ # as the script's exit code even though everything succeeded. The
1003
+ # redirect happens inside cmd so PowerShell 5.1 never turns choco's
1004
+ # stderr into a terminating error under $ErrorActionPreference='Stop'.
1005
+ cmd /c "choco cleanup -y > NUL 2>&1"
1006
+ $LASTEXITCODE = 0
1007
+ Remove-Item -Path $env:TEMP\* -Recurse -Force -ErrorAction SilentlyContinue
1008
+ Write-Host 'Windows sandbox image complete'
1009
+ END
1010
+ ]
1011
+ }
1012
+ }