agent-dev-env 0.1.0-canary.2.fbbd044

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