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,776 @@
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 Ubuntu build reuses the vmware-iso mechanics of
17
+ # images/windows-arm64-vmware/sandbox.pkr.hcl (Fusion on Apple Silicon,
18
+ # NAT + vmxnet3 + EFI, tar.gz OCI artifact) with a Linux twist: it is
19
+ # installed from the Ubuntu Server ARM64 ISO via Subiquity autoinstall
20
+ # (cloud-init seed served over HTTP), provisioned over SSH, and uses
21
+ # open-vm-tools from the Ubuntu archive instead of a VMware Tools ISO —
22
+ # Fusion ships no Linux tools for arm64 guests (open-vm-tools are the
23
+ # only in-guest tools available for arm64 Linux VMs).
24
+
25
+ variable "ubuntu_version" {
26
+ type = string
27
+ description = "Ubuntu guest version, e.g. '24-04'; part of the image name (sandbox-ubuntu-<ubuntu_version>-vmware)."
28
+ }
29
+
30
+ variable "image_version" {
31
+ type = string
32
+ description = "Semantic version this image is published under; bump it + add a CHANGELOG.md entry per release."
33
+ }
34
+
35
+ # --- Build layout ---
36
+ #
37
+ # Built images and their scratch land in a top-level build/ directory:
38
+ # build/ubuntu-arm64-vmware/output (Packer's output_directory). The
39
+ # agent-dev-env CLI build flow computes the directory and passes it
40
+ # in; the default keeps a bare `packer build` from the platform dir working.
41
+
42
+ variable "build_dir" {
43
+ type = string
44
+ default = "."
45
+ description = "Per-image build directory: <build_dir>/output holds the built VM. Set by `agent-dev-env build` to build/ubuntu-arm64-vmware/."
46
+ }
47
+
48
+ # --- Ubuntu install ISO (bring-your-own, see images/ubuntu-arm64-vmware/README.md) ---
49
+
50
+ variable "iso_path" {
51
+ type = string
52
+ description = "Absolute path to the Ubuntu Server 24.04 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`."
53
+ }
54
+
55
+ variable "iso_sha256" {
56
+ type = string
57
+ default = ""
58
+ description = "SHA256 of the Ubuntu ISO as published in the release SHA256SUMS. Read by the agent-dev-env CLI build flow for verification, not by Packer itself (iso_checksum is 'none')."
59
+ }
60
+
61
+ # --- SSH credentials (baked into autoinstall/user-data — keep in sync) ---
62
+
63
+ variable "ssh_username" {
64
+ type = string
65
+ default = "admin"
66
+ description = "User created by the autoinstall seed; used for SSH provisioning and as the sandbox login."
67
+ }
68
+
69
+ variable "ssh_password" {
70
+ type = string
71
+ default = "sandbox1"
72
+ sensitive = true
73
+ description = "Password of the provisioning account; must match the crypt hash in autoinstall/user-data."
74
+ }
75
+
76
+ # --- Toolchain versions ---
77
+ #
78
+ # Pinned by direct download (like the Windows image's Chrome CfT approach):
79
+ # the URL and SHA256 of each artifact are in the vars file; apt-installed
80
+ # tools (gcc, git, python3, ...) come from the Ubuntu archive. Node.js is
81
+ # installed via nvm (like the macOS image) and Rust via rustup (like the
82
+ # Windows image), so only the major/minor version is pinned here.
83
+
84
+ variable "node_version" {
85
+ type = string
86
+ description = "Node.js major version installed via nvm and set as the default, e.g. '22' (nvm resolves the latest patch)."
87
+ }
88
+
89
+ variable "python_version" {
90
+ type = string
91
+ description = "Python version from the Ubuntu archive that is installed, e.g. '3.12' (informational — apt resolves the exact version)."
92
+ }
93
+
94
+ variable "github_cli_version" {
95
+ type = string
96
+ description = "GitHub CLI version, e.g. '2.98.0' (linux-arm64 deb from the GitHub releases)."
97
+ }
98
+
99
+ variable "github_cli_sha256" {
100
+ type = string
101
+ description = "SHA256 of the gh_<version>_linux_arm64.deb for github_cli_version."
102
+ }
103
+
104
+ variable "open_code_review_version" {
105
+ type = string
106
+ description = "open-code-review (ocr) version installed via npm."
107
+ }
108
+
109
+ variable "go_version" {
110
+ type = string
111
+ description = "Go version, e.g. '1.27.0' (linux-arm64 tarball from go.dev/dl)."
112
+ }
113
+
114
+ variable "go_sha256" {
115
+ type = string
116
+ description = "SHA256 of the go<version>.linux-arm64.tar.gz for go_version."
117
+ }
118
+
119
+ variable "rust_version" {
120
+ type = string
121
+ description = "Rust toolchain version installed via rustup (e.g. '1.95'; rustup resolves the latest patch)."
122
+ }
123
+
124
+ variable "vscode_version" {
125
+ type = string
126
+ description = "Visual Studio Code version, e.g. '1.134.0' (linux-deb-arm64 from update.code.visualstudio.com)."
127
+ }
128
+
129
+ variable "vscode_sha256" {
130
+ type = string
131
+ description = "SHA256 of the Code <version> arm64 deb for vscode_version."
132
+ }
133
+
134
+ variable "firefox_version" {
135
+ type = string
136
+ description = "Mozilla Firefox version, e.g. '154.0' (linux-aarch64 en-US tar.xz from ftp.mozilla.org)."
137
+ }
138
+
139
+ variable "firefox_sha256" {
140
+ type = string
141
+ description = "SHA256 of the firefox-<version>.tar.xz for firefox_version."
142
+ }
143
+
144
+ variable "docker_version" {
145
+ type = string
146
+ description = "Docker CLI version, e.g. '29.7.2' (linux/static/stable/aarch64 tarball from download.docker.com)."
147
+ }
148
+
149
+ variable "docker_sha256" {
150
+ type = string
151
+ description = "SHA256 of the docker-<version>.tgz for docker_version."
152
+ }
153
+
154
+ variable "docker_compose_version" {
155
+ type = string
156
+ description = "Docker Compose plugin version, e.g. '5.5.0' (docker-compose-linux-aarch64 from the GitHub releases)."
157
+ }
158
+
159
+ variable "docker_compose_sha256" {
160
+ type = string
161
+ description = "SHA256 of the docker-compose-linux-aarch64 binary for docker_compose_version."
162
+ }
163
+
164
+ variable "docker_buildx_version" {
165
+ type = string
166
+ description = "Docker Buildx plugin version, e.g. '0.36.1' (buildx-<v>.linux-arm64 from the GitHub releases)."
167
+ }
168
+
169
+ variable "docker_buildx_sha256" {
170
+ type = string
171
+ description = "SHA256 of the buildx-<v>.linux-arm64 binary for docker_buildx_version."
172
+ }
173
+
174
+ # --- VM resources ---
175
+
176
+ variable "disk_size" {
177
+ type = number
178
+ default = 100
179
+ description = "VM disk size in GB."
180
+ }
181
+
182
+ variable "cpu_count" {
183
+ type = number
184
+ default = 4
185
+ description = "CPU count of the VM."
186
+ }
187
+
188
+ variable "memory_gb" {
189
+ type = number
190
+ default = 8
191
+ description = "RAM of the VM in GB."
192
+ }
193
+
194
+ # --- OpenChamber web UI ---
195
+
196
+ variable "openchamber_ui_password" {
197
+ type = string
198
+ default = "sandbox"
199
+ description = "Password protecting the OpenChamber web UI; required because the server binds to 0.0.0.0 (host access: http://<guest-ip>:4000)."
200
+ }
201
+
202
+ variable "openchamber_port" {
203
+ type = number
204
+ default = 4000
205
+ description = "TCP port the OpenChamber web UI listens on inside the guest."
206
+ }
207
+
208
+ # ---------------------------------------------------------------------------
209
+ # Source: VMware (Fusion on Apple Silicon, ARM64 guest)
210
+ # ---------------------------------------------------------------------------
211
+ #
212
+ # Ubuntu Server ARM64 runs natively on VMware Fusion on Apple Silicon —
213
+ # Fusion supports Ubuntu 24.04 LTS, and the guest uses the in-box
214
+ # open-vm-tools (Fusion ships no Linux tools ISO at all for arm64 guests:
215
+ # isoimages/arm64/ contains only windows.iso; open-vm-tools are the
216
+ # VMware-recommended tools for Linux and the only in-guest tools available
217
+ # for arm64 Linux VMs). The recipe mirrors the reference implementation in
218
+ # gusztavvargadr/packer (src/ubuntu/source.vmware.pkr.hcl, Unlicense),
219
+ # adapted to this repo's conventions and the Ubuntu Server ARM64 installer:
220
+ #
221
+ # - guest_os_type "arm-ubuntu-64" (Fusion's identifier for Ubuntu 64-bit
222
+ # ARM — verified in the VMX binary's guestOS table), hardware version
223
+ # 20, NVMe disk (Ubuntu's kernel has an in-box NVMe driver), vmxnet3
224
+ # NIC under NAT (vmxnet3.ko ships in the base linux-modules package,
225
+ # verified for 24.04 arm64), EFI firmware — the proven ARM64 combo,
226
+ # - the autoinstall seed is served over HTTP (autoinstall/ directory,
227
+ # http_directory) and passed to the installer kernel with
228
+ # ds=nocloud-net — the proven way to autoinstall Ubuntu Server under
229
+ # the vmware-iso builder; the boot_command is kept minimal (no wait
230
+ # tokens) so it cannot overrun grub's 30 s menu auto-boot,
231
+ # - Subiquity takes care of the partition layout (LVM over the whole
232
+ # disk), the network profile (DHCP on the NAT), and reboots into the
233
+ # installed system; Packer provisions over SSH afterwards,
234
+ # - open-vm-tools (installed by the autoinstall seed) is what makes
235
+ # vmrun's getGuestIPAddress and HGFS work in the sandbox runner.
236
+ #
237
+ # No snapshot is created (snapshot_name): the sandbox runner makes a full
238
+ # vmrun clone as its working VM, and a snapshot inside the published image
239
+ # would only complicate disk compaction and re-clones.
240
+
241
+ source "vmware-iso" "ubuntu" {
242
+ vm_name = "sandbox-ubuntu-${var.ubuntu_version}-arm64-vmware"
243
+ output_directory = "${var.build_dir}/output"
244
+
245
+ # The build flow (agent-dev-env build) verifies the ISO
246
+ # against var.iso_sha256 before Packer runs; iso_checksum is "none" so
247
+ # Packer does not re-verify.
248
+ iso_url = var.iso_path
249
+ iso_checksum = "none"
250
+
251
+ cpus = var.cpu_count
252
+ memory = var.memory_gb * 1024
253
+ disk_size = var.disk_size * 1024
254
+
255
+ # --- ARM64 guest wiring (identical shape to the Windows template) ---
256
+ version = 20
257
+ guest_os_type = "arm-ubuntu-64"
258
+ disk_type_id = "0"
259
+ disk_adapter_type = "nvme"
260
+ network = "nat"
261
+ network_adapter_type = "vmxnet3"
262
+ firmware = "efi"
263
+ cdrom_adapter_type = "sata"
264
+
265
+ # Autoinstall seed: user-data + meta-data, served by the build flow's
266
+ # own HTTP server (the flow runs `python3 -m http.server 8004` over
267
+ # autoinstall/; the installer kernel fetches it via ds=nocloud-net). The
268
+ # flow's server is used instead of Packer's http_directory: its port is
269
+ # random and the vmware plugin does not accept an http_port override,
270
+ # while the autoinstall URL has to be known at grub-typing time (see the
271
+ # watchdog note below). The guest reaches the server at the vmnet8 host
272
+ # address (x.y.z.1 — vmnetd delivers guest traffic to the host's
273
+ # loopback, proven by the bridge runners).
274
+
275
+ vmx_data = {
276
+ # The install ISO rides on the SATA controller.
277
+ "sata1.present" = "TRUE"
278
+ # USB 3.x controller — the plugin auto-enables USB on Apple Silicon
279
+ # for its own VNC typer; keeping it present makes USB input work.
280
+ "usb_xhci.present" = "TRUE"
281
+ # Boot the install CD before the (empty) NVMe disk, so the firmware
282
+ # reaches the ISO's EFI/grub boot menu as early as possible.
283
+ "bios.bootorder" = "cdrom,hdd"
284
+ }
285
+
286
+ # SSH, not WinRM: the installed system ships openssh-server (the
287
+ # autoinstall enables it), and the VM is on a NAT network where the
288
+ # builder can reach it directly. The password must match the crypt hash
289
+ # in autoinstall/user-data (identity.password).
290
+ communicator = "ssh"
291
+ ssh_username = var.ssh_username
292
+ ssh_password = var.ssh_password
293
+ ssh_timeout = "60m"
294
+ ssh_port = 22
295
+
296
+ # Headless: no Fusion window, VNC pinned for the build watchdog
297
+ # (the bundled VNC build watchdog) — same as the Windows templates.
298
+ headless = true
299
+
300
+ vnc_bind_address = "127.0.0.1"
301
+ vnc_port_min = 5901
302
+ vnc_port_max = 5901
303
+ vnc_disable_password = true
304
+
305
+ # No boot_command: the VNC typing raced the firmware's variable-length
306
+ # No-Media/PXE probe cycle (the CD only becomes bootable after a retry,
307
+ # 20-40 s in), and stray keys at the probe screen make the firmware
308
+ # attempt devices from the probe. The BUILD WATCHDOG does the typing
309
+ # instead (the bundled watch-build.py + WATCH_BUILD_BOOT_CMD, set by
310
+ # the build flow): it waits for the grub menu or
311
+ # shell in the OCR and then types the autoinstall command. The VNC is
312
+ # still opened (pinned below) for the watchdog, and the plugin's
313
+ # boot_wait simply gives the VM a moment to power on.
314
+ boot_wait = "5s"
315
+ boot_command = []
316
+
317
+ shutdown_command = "echo '${var.ssh_password}' | sudo -S shutdown -P now"
318
+ shutdown_timeout = "10m"
319
+ }
320
+
321
+ # ---------------------------------------------------------------------------
322
+ # Build
323
+ # ---------------------------------------------------------------------------
324
+ #
325
+ # All provisioners run as root: the execute_command pipes the sandbox
326
+ # password into sudo. User-level installs (nvm, rustup, npm globals, the
327
+ # OpenChamber systemd user service) are run as the sandbox user explicitly
328
+ # (`sudo -H -u <user>`), inside a single bash -c so $HOME, PATH and
329
+ # XDG_RUNTIME_DIR stay correct.
330
+ #
331
+ # "$" characters in the heredocs are plain bash variables: HCL only
332
+ # interpolates "${...}", so do NOT double them to "$$" (bash would expand
333
+ # "$$" to its PID — observed as "4330HOME" in a built image).
334
+
335
+ build {
336
+ sources = ["source.vmware-iso.ubuntu"]
337
+
338
+ # ===== Preamble: base system setup =====
339
+ provisioner "shell" {
340
+ execute_command = "echo '${var.ssh_password}' | sudo -S -E bash '{{.Path}}'"
341
+ inline = [<<-END
342
+ set -e -x
343
+ # --- timezone: UTC, like every other sandbox image ---
344
+ timedatectl set-timezone UTC
345
+ # --- apt maintenance: a sandbox reboots rarely and must boot fast ---
346
+ systemctl disable --now apt-daily.timer apt-daily-upgrade.timer \
347
+ unattended-upgrades.service fstrim.timer motd-news.service \
348
+ 2>/dev/null || true
349
+ # --- open-vm-tools: the guest tools (no Fusion tools ISO exists for
350
+ # arm64 Linux); vmtoolsd + vmhgfs-fuse live here ---
351
+ systemctl enable --now open-vm-tools 2>/dev/null || true
352
+ # --- shared-folder mount point (the sandbox runner mounts HGFS here) ---
353
+ mkdir -p /mnt/hgfs/work
354
+ # --- linger: the sandbox user's systemd user services (OpenChamber,
355
+ # bridge relays) must start at boot without a login ---
356
+ loginctl enable-linger ${var.ssh_username}
357
+ # --- home ownership safety: a root-owned path inside the sandbox
358
+ # user's home breaks first-run client writes (EACCES observed with
359
+ # opencode's ~/.local/share) — normalize before any user-level step ---
360
+ chown -R ${var.ssh_username}:${var.ssh_username} /home/${var.ssh_username} || true
361
+ echo "base system ready"
362
+ END
363
+ ]
364
+ }
365
+
366
+ # ===== Base toolchain via apt =====
367
+ provisioner "shell" {
368
+ execute_command = "echo '${var.ssh_password}' | sudo -S -E bash '{{.Path}}'"
369
+ inline = [<<-END
370
+ set -e -x
371
+ export DEBIAN_FRONTEND=noninteractive
372
+ apt-get update -y
373
+ apt-get install -y --no-install-recommends \
374
+ build-essential pkg-config make cmake autoconf automake \
375
+ git curl wget jq ripgrep vim tmux unzip zip xz-utils \
376
+ ca-certificates openssl gnupg \
377
+ python3 python3-pip python3-venv python3-dev \
378
+ ruby ruby-dev \
379
+ socat \
380
+ libfuse2t64 libfuse3-3 \
381
+ libnss3 libnspr4 libatk1.0-0 libatk-bridge2.0-0 libcups2 \
382
+ libdrm2 libxkbcommon0 libatspi2.0-0 libxcomposite1 libxdamage1 \
383
+ libxfixes3 libxrandr2 libgbm1 libasound2t64 libpango-1.0-0 libcairo2 \
384
+ libgtk-3-0t64 libdbus-glib-1-2 libglib2.0-0
385
+ # apt's nodejs/npm are ancient on 24.04 — Node comes via nvm below.
386
+ python3 --version
387
+ git --version
388
+ echo "apt toolchain ready"
389
+ END
390
+ ]
391
+ }
392
+
393
+ # ===== GNOME desktop (minimal) =====
394
+ # The base installer is Ubuntu Server, so the guest boots to a text
395
+ # console by default. A desktop is part of the sandbox: GNOME Shell under
396
+ # GDM3 with automatic login for the sandbox user — the Fusion window is
397
+ # the human interface for VS Code, Firefox and OpenChamber in a browser.
398
+ # Graphics are software-rendered (llvmpipe — Fusion's arm64 guests get no
399
+ # GPU acceleration), and the Xorg session is the default (the virtualhw
400
+ # SVGA works better with the open-vm-tools input drivers than Wayland's
401
+ # compositor here).
402
+ provisioner "shell" {
403
+ execute_command = "echo '${var.ssh_password}' | sudo -S -E bash '{{.Path}}'"
404
+ inline = [<<-END
405
+ set -e -x
406
+ export DEBIAN_FRONTEND=noninteractive
407
+ apt-get update -y
408
+ # ubuntu-desktop-minimal (GNOME Shell + GDM3 + core apps, arm64) and
409
+ # open-vm-tools-desktop (the SVGA Xorg driver, clipboard and
410
+ # drag-and-drop). Recommends stay ON: a desktop without its fonts and
411
+ # theme pieces is a broken desktop (the toolchain installs above keep
412
+ # --no-install-recommends on purpose).
413
+ apt-get install -y ubuntu-desktop-minimal open-vm-tools-desktop
414
+ # Boot straight to a login-free desktop: the VM window is the primary
415
+ # human interface, so the sandbox user must not have to log in first
416
+ # (SSH does not go through the GUI anyway).
417
+ cat >/etc/gdm3/custom.conf <<'GDM'
418
+ [daemon]
419
+ AutomaticLoginEnable=true
420
+ AutomaticLogin=${var.ssh_username}
421
+ WaylandEnable=false
422
+ GDM
423
+ systemctl set-default graphical.target
424
+ systemctl enable gdm3
425
+ echo "GNOME desktop ready"
426
+ END
427
+ ]
428
+ }
429
+
430
+ # ===== GitHub CLI (pinned deb) =====
431
+ provisioner "shell" {
432
+ execute_command = "echo '${var.ssh_password}' | sudo -S -E bash '{{.Path}}'"
433
+ inline = [<<-END
434
+ set -e -x
435
+ cd /tmp
436
+ DEB="gh_${var.github_cli_version}_linux_arm64.deb"
437
+ curl -fsSL -o "$DEB" "https://github.com/cli/cli/releases/download/v${var.github_cli_version}/$DEB"
438
+ echo "${var.github_cli_sha256} $DEB" | sha256sum -c -
439
+ dpkg -i "$DEB" || apt-get install -y -f
440
+ rm -f "$DEB"
441
+ gh --version
442
+ END
443
+ ]
444
+ }
445
+
446
+ # ===== Go (pinned tarball) =====
447
+ provisioner "shell" {
448
+ execute_command = "echo '${var.ssh_password}' | sudo -S -E bash '{{.Path}}'"
449
+ inline = [<<-END
450
+ set -e -x
451
+ cd /tmp
452
+ TARBALL="go${var.go_version}.linux-arm64.tar.gz"
453
+ curl -fsSL -o "$TARBALL" "https://go.dev/dl/$TARBALL"
454
+ echo "${var.go_sha256} $TARBALL" | sha256sum -c -
455
+ rm -rf /usr/local/go
456
+ tar -C /usr/local -xzf "$TARBALL"
457
+ rm -f "$TARBALL"
458
+ # Persist for login shells (profile.d runs before every login shell).
459
+ echo 'export PATH=$PATH:/usr/local/go/bin' | tee /etc/profile.d/agent-dev-env-go.sh
460
+ /usr/local/go/bin/go version
461
+ END
462
+ ]
463
+ }
464
+
465
+ # ===== Node.js via nvm (pinned major) =====
466
+ # Same approach as the macOS image: nvm picks the version and makes it
467
+ # the default; the npm global installs (opencode, openchamber) land in
468
+ # the nvm dir. Runs as the sandbox user — npm caches and user configs
469
+ # must not be owned by root.
470
+ provisioner "shell" {
471
+ execute_command = "echo '${var.ssh_password}' | sudo -S -E bash '{{.Path}}'"
472
+ inline = [<<-END
473
+ set -e -x
474
+ sudo -H -u ${var.ssh_username} bash -c '
475
+ set -e
476
+ export NVM_DIR="$HOME/.nvm"
477
+ curl -fsSL https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash
478
+ . "$NVM_DIR/nvm.sh"
479
+ nvm install ${var.node_version}
480
+ nvm alias default ${var.node_version}
481
+ node --version
482
+ npm --version
483
+ '
484
+ echo "node ready"
485
+ END
486
+ ]
487
+ }
488
+
489
+ # ===== Rust via rustup (pinned toolchain) =====
490
+ provisioner "shell" {
491
+ execute_command = "echo '${var.ssh_password}' | sudo -S -E bash '{{.Path}}'"
492
+ inline = [<<-END
493
+ set -e -x
494
+ sudo -H -u ${var.ssh_username} bash -c '
495
+ set -e
496
+ curl -fsSL https://sh.rustup.rs | sh -s -- -y --default-toolchain ${var.rust_version}
497
+ . "$HOME/.cargo/env"
498
+ rustc --version
499
+ cargo --version
500
+ '
501
+ echo 'export PATH=$PATH:$HOME/.cargo/bin' | tee /etc/profile.d/agent-dev-env-rust.sh
502
+ echo "rust ready"
503
+ END
504
+ ]
505
+ }
506
+
507
+ # ===== Visual Studio Code (pinned arm64 deb) =====
508
+ provisioner "shell" {
509
+ execute_command = "echo '${var.ssh_password}' | sudo -S -E bash '{{.Path}}'"
510
+ inline = [<<-END
511
+ set -e -x
512
+ cd /tmp
513
+ DEB="code_${var.vscode_version}_arm64.deb"
514
+ curl -fsSL -o "$DEB" "https://update.code.visualstudio.com/${var.vscode_version}/linux-deb-arm64/stable"
515
+ echo "${var.vscode_sha256} $DEB" | sha256sum -c -
516
+ dpkg -i "$DEB" || apt-get install -y -f
517
+ rm -f "$DEB"
518
+ ln -sf /usr/bin/code /usr/local/bin/code
519
+ code --version | head -n1
520
+ END
521
+ ]
522
+ }
523
+
524
+ # ===== Mozilla Firefox (pinned linux-aarch64 release) =====
525
+ # No Google Chrome: Chrome for Testing publishes no linux-arm64 build
526
+ # (only x86_64 linux64) and Ubuntu's chromium is snap-only, so Firefox
527
+ # is the browser in the image (linux-aarch64 releases are official).
528
+ provisioner "shell" {
529
+ execute_command = "echo '${var.ssh_password}' | sudo -S -E bash '{{.Path}}'"
530
+ inline = [<<-END
531
+ set -e -x
532
+ cd /tmp
533
+ ARCHIVE="firefox-${var.firefox_version}.tar.xz"
534
+ curl -fsSL -o "$ARCHIVE" "https://ftp.mozilla.org/pub/firefox/releases/${var.firefox_version}/linux-aarch64/en-US/$ARCHIVE"
535
+ echo "${var.firefox_sha256} $ARCHIVE" | sha256sum -c -
536
+ rm -rf /opt/firefox
537
+ mkdir -p /opt/firefox
538
+ tar -xJf "$ARCHIVE" -C /opt/firefox --strip-components=1
539
+ rm -f "$ARCHIVE"
540
+ ln -sf /opt/firefox/firefox /usr/local/bin/firefox
541
+ firefox --version
542
+ END
543
+ ]
544
+ }
545
+
546
+ # ===== Docker CLI + plugins (client only, pinned static binaries) =====
547
+ # The guest never runs a container engine (it is bridged to the host's);
548
+ # only the CLI + compose + buildx are installed. Plugin search paths:
549
+ # the CLI checks /usr/local/lib/docker/cli-plugins after the user dirs.
550
+ provisioner "shell" {
551
+ execute_command = "echo '${var.ssh_password}' | sudo -S -E bash '{{.Path}}'"
552
+ inline = [<<-END
553
+ set -e -x
554
+ cd /tmp
555
+ TGZ="docker-${var.docker_version}.tgz"
556
+ curl -fsSL -o "$TGZ" "https://download.docker.com/linux/static/stable/aarch64/$TGZ"
557
+ echo "${var.docker_sha256} $TGZ" | sha256sum -c -
558
+ tar -xzf "$TGZ"
559
+ install -Dm755 "docker/docker" /usr/local/bin/docker
560
+ rm -rf docker "$TGZ"
561
+ mkdir -p /usr/local/lib/docker/cli-plugins
562
+ curl -fsSL -o /usr/local/lib/docker/cli-plugins/docker-compose \
563
+ "https://github.com/docker/compose/releases/download/v${var.docker_compose_version}/docker-compose-linux-aarch64"
564
+ echo "${var.docker_compose_sha256} /usr/local/lib/docker/cli-plugins/docker-compose" | sha256sum -c -
565
+ chmod 755 /usr/local/lib/docker/cli-plugins/docker-compose
566
+ curl -fsSL -o /usr/local/lib/docker/cli-plugins/docker-buildx \
567
+ "https://github.com/docker/buildx/releases/download/v${var.docker_buildx_version}/buildx-v${var.docker_buildx_version}.linux-arm64"
568
+ echo "${var.docker_buildx_sha256} /usr/local/lib/docker/cli-plugins/docker-buildx" | sha256sum -c -
569
+ chmod 755 /usr/local/lib/docker/cli-plugins/docker-buildx
570
+ # The static CLI looks for plugins in $HOME/.docker/cli-plugins (and
571
+ # the system dir next to /usr/local/bin); mirror the plugins into the
572
+ # caller's (root) and the sandbox user's plugin dirs so both root and
573
+ # admin shells see `docker compose` / `docker buildx`.
574
+ for pdir in "$HOME/.docker/cli-plugins" "/home/${var.ssh_username}/.docker/cli-plugins"; do
575
+ mkdir -p "$pdir"
576
+ ln -sf /usr/local/lib/docker/cli-plugins/docker-compose "$pdir/docker-compose"
577
+ ln -sf /usr/local/lib/docker/cli-plugins/docker-buildx "$pdir/docker-buildx"
578
+ done
579
+ # docker context 'host' pointing at the runtime bridge socket (the
580
+ # runner's guest bridge creates /tmp/docker.sock); nice-to-have only —
581
+ # the profile.d DOCKER_HOST export (written by the runner) covers
582
+ # clients that ignore contexts, so a context failure must not fail
583
+ # the build.
584
+ sudo -H -u ${var.ssh_username} bash -c '
585
+ docker context create host --description "Host engine via /tmp/docker.sock bridge" \
586
+ --docker "host=unix:///tmp/docker.sock" 2>&1 || true
587
+ docker context use host 2>&1 || true
588
+ '
589
+ docker --version
590
+ docker compose version
591
+ docker buildx version
592
+ END
593
+ ]
594
+ }
595
+
596
+ # ===== OpenCode, OpenCodeReview, OpenChamber web UI =====
597
+ # npm globals as the sandbox user. OpenChamber runs as a systemd user
598
+ # service (systemd is the Linux equivalent of the macOS LaunchAgent /
599
+ # Windows ONLOGON task): a deterministic unit is written by this image
600
+ # (framed exactly like the Windows image's wrapper approach — the
601
+ # openchamber CLI's own `startup enable` prefers an interactive user
602
+ # session to detect the service backend). The unit pins PATH (systemd
603
+ # user services start with a minimal environment — nvm/node and cargo
604
+ # paths must be explicit), OPENCODE_BINARY, OPENCHAMBER_UI_PASSWORD and
605
+ # SSH_AUTH_SOCK / DOCKER_HOST (the runner's bridge sockets).
606
+ provisioner "shell" {
607
+ execute_command = "echo '${var.ssh_password}' | sudo -S -E bash '{{.Path}}'"
608
+ inline = [<<-END
609
+ set -e -x
610
+ # nvm dir of the sandbox user (node lives there; nvm only loads in
611
+ # interactive shells, so non-login shells need the path directly).
612
+ NPM_BIN=$(sudo -H -u ${var.ssh_username} bash -c 'ls -d $HOME/.nvm/versions/node/v* 2>/dev/null | tail -n1')/bin
613
+ [ -n "$NPM_BIN" ] && [ -x "$NPM_BIN/npm" ] || { echo "nvm node dir not found"; exit 1; }
614
+ # Root-side guarantees (the user shell below has privileges to write
615
+ # only what its owner owns): create the XDG dirs with explicit
616
+ # ownership so first-run writes (opencode's ~/.local/share) can
617
+ # never hit an EACCES. `.local` must be listed as its own operand:
618
+ # install -d only applies -o/-g to the operands, and an intermediate
619
+ # `.local` would be created root-owned (breaking `tar -C $HOME` and
620
+ # any write the sandbox user makes directly into ~/.local).
621
+ install -d -o ${var.ssh_username} -g ${var.ssh_username} \
622
+ /home/${var.ssh_username}/.local \
623
+ /home/${var.ssh_username}/.local/share \
624
+ /home/${var.ssh_username}/.local/state \
625
+ /home/${var.ssh_username}/.config /home/${var.ssh_username}/.cache
626
+ sudo -H -u ${var.ssh_username} bash -c '
627
+ set -e
628
+ export NVM_DIR="$HOME/.nvm"
629
+ . "$NVM_DIR/nvm.sh"
630
+ nvm use default >/dev/null
631
+ # opencode via the official installer: the npm package postinstall
632
+ # mis-selects the arm64-musl binary on glibc systems (EBADPLATFORM).
633
+ curl -fsSL https://opencode.ai/install | bash
634
+ export PATH="$HOME/.opencode/bin:$PATH"
635
+ opencode --version
636
+ npm install -g @alibaba-group/open-code-review@${var.open_code_review_version}
637
+ npm install -g @openchamber/web
638
+ ocr --version
639
+ openchamber --version
640
+ '
641
+ OPENCODE_BIN="$ADMIN_HOME/.opencode/bin/opencode"
642
+ ADMIN_HOME=$(sudo -H -u ${var.ssh_username} bash -c 'printf %s "$HOME"')
643
+ # Keep the CLI's own env file consistent (openchamber status/logs
644
+ # read it; the runtime service unit is ours).
645
+ sudo -H -u ${var.ssh_username} mkdir -p "$ADMIN_HOME/.config/openchamber"
646
+ cat > /tmp/startup.env <<EOF
647
+ OPENCHAMBER_UI_PASSWORD='${var.openchamber_ui_password}'
648
+ OPENCODE_BINARY='$OPENCODE_BIN'
649
+ EOF
650
+ sudo -H -u ${var.ssh_username} cp /tmp/startup.env "$ADMIN_HOME/.config/openchamber/startup.env"
651
+ rm -f /tmp/startup.env
652
+
653
+ # systemd user unit — the deterministic service the sandbox runner
654
+ # also restarts after the bridges come up.
655
+ sudo -H -u ${var.ssh_username} mkdir -p "$ADMIN_HOME/.config/systemd/user"
656
+ cat > /tmp/agent-dev-env-openchamber.service <<EOF
657
+ [Unit]
658
+ Description=OpenChamber Web Server
659
+ After=network.target
660
+
661
+ [Service]
662
+ Type=simple
663
+ WorkingDirectory=%h
664
+ ExecStart=$NPM_BIN/openchamber serve --port ${var.openchamber_port} --host 0.0.0.0 --foreground
665
+ Environment=PATH=$NPM_BIN:$ADMIN_HOME/.opencode/bin:/usr/local/go/bin:$ADMIN_HOME/.cargo/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
666
+ Environment=OPENCODE_BINARY=$OPENCODE_BIN
667
+ Environment=OPENCHAMBER_UI_PASSWORD=${var.openchamber_ui_password}
668
+ Environment=SSH_AUTH_SOCK=/tmp/ssh-agent.sock
669
+ Environment=DOCKER_HOST=unix:///tmp/docker.sock
670
+ Restart=on-failure
671
+ RestartSec=5
672
+
673
+ [Install]
674
+ WantedBy=default.target
675
+ EOF
676
+ sudo -H -u ${var.ssh_username} cp /tmp/agent-dev-env-openchamber.service \
677
+ "$ADMIN_HOME/.config/systemd/user/agent-dev-env-openchamber.service"
678
+ rm -f /tmp/agent-dev-env-openchamber.service
679
+ sudo -H -u ${var.ssh_username} bash -c '
680
+ export XDG_RUNTIME_DIR="/run/user/$(id -u)"
681
+ systemctl --user daemon-reload
682
+ systemctl --user enable --now agent-dev-env-openchamber
683
+ '
684
+ END
685
+ ]
686
+ }
687
+
688
+ # ===== OpenChamber health check + cleanup =====
689
+ # The service runs as a systemd user unit (linger enabled in the base
690
+ # provisioner): poll the HTTP endpoint until it answers (the first start
691
+ # may take a couple of minutes — npm modules + OpenCode spawn).
692
+ provisioner "shell" {
693
+ execute_command = "echo '${var.ssh_password}' | sudo -S -E bash '{{.Path}}'"
694
+ inline = [<<-END
695
+ set -e -x
696
+ sudo -H -u ${var.ssh_username} bash -c '
697
+ export XDG_RUNTIME_DIR="/run/user/$(id -u)"
698
+ systemctl --user is-active agent-dev-env-openchamber
699
+ '
700
+ n=0
701
+ while [ "$n" -lt 60 ]; do
702
+ if curl -fsS --connect-timeout 2 --max-time 3 "http://127.0.0.1:${var.openchamber_port}" >/dev/null 2>&1; then
703
+ echo "OpenChamber web UI is up on port ${var.openchamber_port}"
704
+ break
705
+ fi
706
+ n=$((n + 1))
707
+ sleep 5
708
+ done
709
+ if [ "$n" -ge 60 ]; then
710
+ echo "WARN: OpenChamber did not answer on 127.0.0.1:${var.openchamber_port}"
711
+ fi
712
+ apt-get autoclean -y || true
713
+ apt-get autoremove -y || true
714
+ rm -rf /var/lib/apt/lists/* /tmp/* 2>/dev/null || true
715
+ echo "Ubuntu sandbox image complete"
716
+ END
717
+ ]
718
+ }
719
+
720
+ # ===== Final verification =====
721
+ provisioner "shell" {
722
+ execute_command = "echo '${var.ssh_password}' | sudo -S -E bash '{{.Path}}'"
723
+ inline = [<<-END
724
+ set -e -x
725
+ echo "=== Versions ==="
726
+ lsb_release -ds
727
+ uname -m
728
+ systemctl is-active open-vm-tools
729
+ # Desktop (GNOME): the packages and the boot target — run as root,
730
+ # no display needed for the checks.
731
+ dpkg -s ubuntu-desktop-minimal open-vm-tools-desktop >/dev/null
732
+ systemctl is-enabled gdm3
733
+ default_target=$(systemctl get-default)
734
+ [ "$default_target" = "graphical.target" ] || echo "WARN: default target is $default_target"
735
+ # Node/npm/opencode/ocr/openchamber live in the sandbox user's home
736
+ # (nvm + .opencode/bin) — the verification shell runs as root, so
737
+ # those have to be checked as the user.
738
+ sudo -H -u ${var.ssh_username} bash -c '
739
+ export PATH="$HOME/.opencode/bin:$PATH"
740
+ export NVM_DIR="$HOME/.nvm"
741
+ . "$NVM_DIR/nvm.sh" >/dev/null 2>&1
742
+ nvm use default >/dev/null
743
+ node --version
744
+ npm --version
745
+ opencode --version | head -n1
746
+ ocr --version
747
+ openchamber --version | head -n1
748
+ '
749
+ python3 --version
750
+ git --version
751
+ gh --version
752
+ rg --version
753
+ jq --version
754
+ /usr/local/go/bin/go version
755
+ sudo -H -u ${var.ssh_username} bash -c '. "$HOME/.cargo/env"; rustc --version; cargo --version'
756
+ code --version | head -n1
757
+ docker --version
758
+ docker compose version
759
+ docker buildx version
760
+ # check-and-warn: a missing helper must never fail the build
761
+ for tool in firefox; do
762
+ if command -v "$tool" >/dev/null 2>&1; then
763
+ "$tool" --version 2>/dev/null | head -n1 || true
764
+ else
765
+ echo "WARN: $tool not found on PATH"
766
+ fi
767
+ done
768
+ sudo -H -u ${var.ssh_username} bash -c '
769
+ export XDG_RUNTIME_DIR="/run/user/$(id -u)"
770
+ systemctl --user list-unit-files | grep agent-dev-env-openchamber || true
771
+ docker context ls | grep host || true
772
+ '
773
+ END
774
+ ]
775
+ }
776
+ }