agent-dev-env 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/assets/bridge/bridge.js +202 -0
- package/dist/assets/guest/guest-agent-mac.js +586 -0
- package/dist/assets/guest/guest-agent-ubuntu.js +481 -0
- package/dist/assets/guest/guest-agent-windows.js +466 -0
- package/dist/assets/images/mac/CHANGELOG.md +265 -0
- package/dist/assets/images/mac/README.md +63 -0
- package/dist/assets/images/mac/sandbox.pkr.hcl +468 -0
- package/dist/assets/images/mac/vars/sandbox-macos-tahoe.pkrvars.hcl +39 -0
- package/dist/assets/images/ubuntu-arm64-vmware/CHANGELOG.md +239 -0
- package/dist/assets/images/ubuntu-arm64-vmware/README.md +177 -0
- package/dist/assets/images/ubuntu-arm64-vmware/autoinstall/meta-data +2 -0
- package/dist/assets/images/ubuntu-arm64-vmware/autoinstall/user-data +34 -0
- package/dist/assets/images/ubuntu-arm64-vmware/sandbox.pkr.hcl +776 -0
- package/dist/assets/images/ubuntu-arm64-vmware/vars/sandbox-ubuntu-24-04-arm64-vmware.pkrvars.hcl +71 -0
- package/dist/assets/images/windows-arm64-qemu/CHANGELOG.md +209 -0
- package/dist/assets/images/windows-arm64-qemu/README.md +142 -0
- package/dist/assets/images/windows-arm64-qemu/autounattend.xml +325 -0
- package/dist/assets/images/windows-arm64-qemu/qemu-with-tpm.sh +170 -0
- package/dist/assets/images/windows-arm64-qemu/sandbox.pkr.hcl +1012 -0
- package/dist/assets/images/windows-arm64-qemu/vars/sandbox-windows-11-arm64-qemu.pkrvars.hcl +83 -0
- package/dist/assets/images/windows-arm64-vmware/CHANGELOG.md +270 -0
- package/dist/assets/images/windows-arm64-vmware/README.md +171 -0
- package/dist/assets/images/windows-arm64-vmware/autounattend.xml +311 -0
- package/dist/assets/images/windows-arm64-vmware/sandbox.pkr.hcl +1036 -0
- package/dist/assets/images/windows-arm64-vmware/vars/sandbox-windows-11-arm64-vmware.pkrvars.hcl +83 -0
- package/dist/assets/rules/agent-rules-linux.md +68 -0
- package/dist/assets/rules/agent-rules.md +71 -0
- package/dist/assets/watchdog/watch-build-ocr.swift +40 -0
- package/dist/assets/watchdog/watch-build.py +275 -0
- package/dist/cli.js +52 -0
- package/dist/commands/delete.js +179 -0
- package/dist/commands/doctor.js +165 -0
- package/dist/commands/list.js +38 -0
- package/dist/commands/not-yet.js +15 -0
- package/dist/commands/register.js +150 -0
- package/dist/commands/run.js +65 -0
- package/dist/commands/status.js +142 -0
- package/dist/commands/stop.js +186 -0
- package/dist/commands/sync.js +98 -0
- package/dist/lib/exec.js +250 -0
- package/dist/lib/ghcr.js +82 -0
- package/dist/lib/git.js +100 -0
- package/dist/lib/logger.js +87 -0
- package/dist/lib/network.js +63 -0
- package/dist/lib/paths.js +116 -0
- package/dist/lib/platform.js +87 -0
- package/dist/lib/prompt.js +66 -0
- package/dist/lib/qemu.js +373 -0
- package/dist/lib/regex.js +9 -0
- package/dist/lib/ssh.js +230 -0
- package/dist/lib/tart.js +210 -0
- package/dist/lib/template.js +35 -0
- package/dist/lib/vars.js +99 -0
- package/dist/lib/vmrun.js +334 -0
- package/dist/lifecycle/build-macos.js +23 -0
- package/dist/lifecycle/build-qemu.js +204 -0
- package/dist/lifecycle/build-shared.js +355 -0
- package/dist/lifecycle/build-ubuntu.js +184 -0
- package/dist/lifecycle/build-watchdog.js +146 -0
- package/dist/lifecycle/build-windows-vmware.js +112 -0
- package/dist/lifecycle/build.js +55 -0
- package/dist/lifecycle/catalog.js +165 -0
- package/dist/lifecycle/deploy.js +161 -0
- package/dist/lifecycle/tag.js +77 -0
- package/dist/lifecycle/watch-build.js +64 -0
- package/dist/runners/bridges.js +153 -0
- package/dist/runners/framework.js +66 -0
- package/dist/runners/macos-bridges.js +143 -0
- package/dist/runners/macos-guest.js +76 -0
- package/dist/runners/macos-rules.js +77 -0
- package/dist/runners/macos-summary.js +145 -0
- package/dist/runners/macos.js +258 -0
- package/dist/runners/openchamber.js +60 -0
- package/dist/runners/options.js +56 -0
- package/dist/runners/qemu-image.js +183 -0
- package/dist/runners/rules.js +49 -0
- package/dist/runners/ubuntu-bridges.js +154 -0
- package/dist/runners/ubuntu-guest.js +136 -0
- package/dist/runners/ubuntu-image.js +26 -0
- package/dist/runners/ubuntu-rules.js +86 -0
- package/dist/runners/ubuntu-shared.js +65 -0
- package/dist/runners/ubuntu-summary.js +141 -0
- package/dist/runners/ubuntu.js +117 -0
- package/dist/runners/vmware-common.js +158 -0
- package/dist/runners/vmware-image.js +242 -0
- package/dist/runners/windows-autologon.js +119 -0
- package/dist/runners/windows-bridges.js +136 -0
- package/dist/runners/windows-guest.js +213 -0
- package/dist/runners/windows-image.js +23 -0
- package/dist/runners/windows-qemu-summary.js +85 -0
- package/dist/runners/windows-qemu.js +189 -0
- package/dist/runners/windows-shared.js +68 -0
- package/dist/runners/windows-summary.js +104 -0
- package/dist/runners/windows.js +96 -0
- package/dist/settings/common.js +107 -0
- package/dist/settings/macos-copy.js +223 -0
- package/dist/settings/macos.js +41 -0
- package/dist/settings/ubuntu-copy.js +223 -0
- package/dist/settings/ubuntu.js +68 -0
- package/package.json +52 -0
package/dist/assets/images/windows-arm64-vmware/vars/sandbox-windows-11-arm64-vmware.pkrvars.hcl
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# Windows 11 (ARM64) sandbox image — VMware (Fusion) build.
|
|
2
|
+
#
|
|
3
|
+
# Built with the Packer vmware-iso plugin on Apple Silicon (the vmware-iso
|
|
4
|
+
# builder drives VMware Fusion, which virtualizes ARM64 guests natively):
|
|
5
|
+
# Windows 11 ARM64 ISO + Fusion's ARM64 boot drivers + VMware Tools. See
|
|
6
|
+
# images/windows-arm64-vmware/README.md for the full build flow — the
|
|
7
|
+
# Windows ISO is bring-your-own (Microsoft does not permit redistribution),
|
|
8
|
+
# so it is not part of this repo.
|
|
9
|
+
|
|
10
|
+
windows_version = "11"
|
|
11
|
+
|
|
12
|
+
# SHA256 of the Windows 11 ARM64 ISO. Microsoft publishes the hash on the
|
|
13
|
+
# download page (https://www.microsoft.com/software-download/windows11arm64);
|
|
14
|
+
# paste it here to enable integrity verification. Set WINDOWS_ISO_PATH to
|
|
15
|
+
# the local ISO path when building. Empty = skip verification.
|
|
16
|
+
iso_sha256 = "638AA2C88E94385B00F4F178D071E3DF0B7D9E335577A83BD533B7F2EB65ADF0"
|
|
17
|
+
|
|
18
|
+
# VMware Fusion installation that supplies the ARM64 boot drivers
|
|
19
|
+
# (Contents/Library/isoimages/arm64/drivers-arm64.zip) and the ARM64 VMware
|
|
20
|
+
# Tools ISO (Contents/Library/isoimages/arm64/windows.iso) during the build;
|
|
21
|
+
# the sandbox runner needs the same Fusion to run the VM.
|
|
22
|
+
# Fusion 13.6+ is required (the Packer vmware plugin's minimum).
|
|
23
|
+
vmware_fusion_app_path = "/Applications/VMware Fusion.app"
|
|
24
|
+
|
|
25
|
+
# Toolchain versions installed via Chocolatey (choco package versions —
|
|
26
|
+
# must exist in the community repository).
|
|
27
|
+
nodejs_version = "26.8.1"
|
|
28
|
+
python_version = "3.13.15"
|
|
29
|
+
github_cli_version = "2.97.0"
|
|
30
|
+
ripgrep_version = "15.2.0"
|
|
31
|
+
git_version = "2.55.0.4"
|
|
32
|
+
jq_version = "1.8.1"
|
|
33
|
+
open_code_review_version = "1.9.5"
|
|
34
|
+
|
|
35
|
+
# C/C++ + cross-language toolchains (brought over from AdGuard's
|
|
36
|
+
# build-agent-images windows2022-vs2022 / windows2022-go images).
|
|
37
|
+
# VS2022 Build Tools (choco package version; the finalizer adds the .NET
|
|
38
|
+
# SDKs + VC++ workload + Win11 SDK) and Rust (via rustup, not choco) are
|
|
39
|
+
# installed by dedicated provisioners.
|
|
40
|
+
go_version = "1.27.0"
|
|
41
|
+
rust_version = "1.95"
|
|
42
|
+
wixtoolset_version = "3.14.1.20250415"
|
|
43
|
+
protoc_version = "36.0.0"
|
|
44
|
+
nasm_version = "3.2.0"
|
|
45
|
+
llvm_version = "22.1.7"
|
|
46
|
+
vim_version = "9.2.995"
|
|
47
|
+
nuget_version = "7.9.0"
|
|
48
|
+
mingw_version = "16.1.0"
|
|
49
|
+
make_version = "4.4.1"
|
|
50
|
+
vs_buildtools_version = "117.14.37"
|
|
51
|
+
|
|
52
|
+
# Google Chrome: installed from the Chrome for Testing (CfT) snapshot
|
|
53
|
+
# archive instead of choco — choco's googlechrome package always downloads
|
|
54
|
+
# the live dl.google.com MSI whose hash rotates on every Chrome release,
|
|
55
|
+
# so the pinned package hash breaks between releases. CfT serves versioned
|
|
56
|
+
# zips at storage.googleapis.com/chrome-for-testing-public/<version>/,
|
|
57
|
+
# which stay downloadable and hash-stable. The x64 build runs under Windows
|
|
58
|
+
# on ARM emulation, like the choco MSI did.
|
|
59
|
+
chrome_version = "152.0.7977.54"
|
|
60
|
+
chrome_sha256 = "91850065e6b80bba0c752e17a150fe1b9e39bba51ed705640c1273f565950dda"
|
|
61
|
+
|
|
62
|
+
# VM resources
|
|
63
|
+
disk_size = 100
|
|
64
|
+
cpu_count = 4
|
|
65
|
+
memory_gb = 8
|
|
66
|
+
|
|
67
|
+
# WinRM credentials used for provisioning. They are baked into
|
|
68
|
+
# images/windows-arm64-vmware/autounattend.xml (Administrator password) and
|
|
69
|
+
# become the sandbox's login (SSH/RDP) — keep the two files in sync.
|
|
70
|
+
winrm_username = "Administrator"
|
|
71
|
+
winrm_password = "sandbox1"
|
|
72
|
+
|
|
73
|
+
# OpenChamber web UI password + port. The runner advertises the UI at
|
|
74
|
+
# http://<guest-ip>:4000 (password "sandbox" by default). OpenChamber
|
|
75
|
+
# refuses to serve on the network without a password.
|
|
76
|
+
openchamber_ui_password = "sandbox"
|
|
77
|
+
openchamber_port = 4000
|
|
78
|
+
|
|
79
|
+
# Semantic version this image is published under (also the GHCR push tag,
|
|
80
|
+
# besides :latest). For every release: bump it, add a CHANGELOG.md entry,
|
|
81
|
+
# and create the windows-arm64-vmware-v<version> git tag
|
|
82
|
+
# (npx agent-dev-env tag <image>).
|
|
83
|
+
image_version = "1.0.0"
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# Sandbox VM environment
|
|
2
|
+
|
|
3
|
+
You are running inside an Ubuntu sandbox VM. Your code lives on the host Mac
|
|
4
|
+
and is shared into the VM; the toolchain (Go, Rust, Node, Python, VS Code,
|
|
5
|
+
opencode, ...) runs in the VM. The environment differs from a normal dev
|
|
6
|
+
machine in a few ways that matter when you run commands or start services.
|
|
7
|
+
|
|
8
|
+
## Paths
|
|
9
|
+
|
|
10
|
+
- When the runner was started with `--work-dir`, the shared working
|
|
11
|
+
directory is mounted in the VM at `{{GUEST_MOUNT}}` and corresponds to
|
|
12
|
+
the host's `{{HOST_WORK_DIR}}`. Paths quoted from the host — in issues,
|
|
13
|
+
commits, or host-side terminal output — use the host form: map them to
|
|
14
|
+
the guest form when you open files, and back to the host form when you
|
|
15
|
+
return paths to the user.
|
|
16
|
+
- The VM's user is `admin` (home `/home/admin`); your git identity comes
|
|
17
|
+
from the host's git config (the runner shares it into the guest).
|
|
18
|
+
|
|
19
|
+
## Docker
|
|
20
|
+
|
|
21
|
+
The VM ships the Docker CLI but **no container engine** — the runner
|
|
22
|
+
bridges the *host's* engine into the VM instead:
|
|
23
|
+
|
|
24
|
+
- Docker talks to the host engine through `unix:///tmp/docker.sock`
|
|
25
|
+
(`DOCKER_HOST` is exported; the `host` docker context is the default).
|
|
26
|
+
- **Never try to install or start a local engine** (`dockerd`, Docker
|
|
27
|
+
Desktop, ...) — it cannot work here. If `docker info` fails, the bridge
|
|
28
|
+
or the host engine is down: tell the user to start the engine on the
|
|
29
|
+
host and/or re-run `agent-dev-env run ubuntu-vmware`, and continue with
|
|
30
|
+
work that does not need Docker.
|
|
31
|
+
|
|
32
|
+
### Containers you launch
|
|
33
|
+
|
|
34
|
+
- Containers run on the **host engine**, and the Docker daemon resolves
|
|
35
|
+
volume-mount paths on the **host's filesystem** — the guest mount
|
|
36
|
+
(`{{GUEST_MOUNT}}`) does not exist there. Bind mounts must use **host
|
|
37
|
+
paths**: mount `{{HOST_WORK_DIR}}/<project>/...` into the container, not
|
|
38
|
+
`{{GUEST_MOUNT}}/<project>/...`. This applies to `docker run -v` and to
|
|
39
|
+
compose `volumes:` entries alike; named volumes are unaffected.
|
|
40
|
+
- Published ports are bound on the **host**. From inside the VM they are
|
|
41
|
+
reachable at `{{NAT_GATEWAY}}:<port>` — **not** at `localhost:<port>`.
|
|
42
|
+
Check a container you started with `curl http://{{NAT_GATEWAY}}:<port>`,
|
|
43
|
+
and tell the user the port is available as `http://localhost:<port>` on
|
|
44
|
+
the host itself.
|
|
45
|
+
- Image builds run on the host engine: `docker build` and `docker buildx`
|
|
46
|
+
work as usual.
|
|
47
|
+
|
|
48
|
+
## Services you start
|
|
49
|
+
|
|
50
|
+
- A server you start inside the VM (e.g. a dev server) is reachable from
|
|
51
|
+
the host at `http://<vm-ip>:<port>` — the VM's IP is printed when the
|
|
52
|
+
sandbox starts. OpenChamber itself listens on port 4000.
|
|
53
|
+
|
|
54
|
+
## Config changes
|
|
55
|
+
|
|
56
|
+
- After editing opencode or Copilot settings in the VM, restart OpenChamber
|
|
57
|
+
with `systemctl --user restart agent-sandbox-openchamber` (the CLI
|
|
58
|
+
restarts it automatically when it brings the bridges up). To pull the
|
|
59
|
+
host's versions of the settings instead, the user runs
|
|
60
|
+
`agent-dev-env sync ubuntu-vmware`.
|
|
61
|
+
|
|
62
|
+
## SSH agent bridge
|
|
63
|
+
|
|
64
|
+
- The runner bridges the host's SSH agent into the VM, so `git push`, `git
|
|
65
|
+
fetch`, `gh`, `ssh` and `scp` work against any host the user's agent
|
|
66
|
+
knows, without keys on disk. `SSH_AUTH_SOCK` points at the bridged
|
|
67
|
+
socket (`/tmp/ssh-agent.sock`), and `~/.ssh/config` sets `IdentityAgent`
|
|
68
|
+
for tools that don't read the export.
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# Sandbox VM environment
|
|
2
|
+
|
|
3
|
+
You are running inside a macOS sandbox VM. Your code lives on the host Mac and
|
|
4
|
+
is shared into the VM; the toolchain (Xcode, Homebrew, Node, Python, VS Code,
|
|
5
|
+
opencode, ...) runs in the VM. The environment differs from a normal dev
|
|
6
|
+
machine in a few ways that matter when you run commands or start services.
|
|
7
|
+
|
|
8
|
+
## Paths
|
|
9
|
+
|
|
10
|
+
- The shared working directory is mounted in the VM at `{{GUEST_MOUNT}}` and
|
|
11
|
+
corresponds to the host's `{{HOST_WORK_DIR}}`. Paths quoted from the host —
|
|
12
|
+
in issues, commits, or host-side terminal output — use the host form: map
|
|
13
|
+
them to the guest form when you open files, and back to the host form when
|
|
14
|
+
you return paths to the user.
|
|
15
|
+
- The VM's user is `admin` (home `/Users/admin`); your git identity, aliases
|
|
16
|
+
and signing config come from `~/.gitconfig` (synced from the host with the
|
|
17
|
+
user settings).
|
|
18
|
+
|
|
19
|
+
## Docker
|
|
20
|
+
|
|
21
|
+
The VM ships the Docker CLI but **no container engine** — macOS guests cannot
|
|
22
|
+
nest VMs, so Docker Desktop, Colima and OrbStack do not run inside the VM. The
|
|
23
|
+
runner bridges the *host's* engine into the VM instead:
|
|
24
|
+
|
|
25
|
+
- The `host` docker context is the default; `docker`, `docker compose` and
|
|
26
|
+
`docker buildx` talk to the host engine via `~/.docker/run/docker.sock`
|
|
27
|
+
(`DOCKER_HOST` and `TESTCONTAINERS_HOST_OVERRIDE` are exported for clients
|
|
28
|
+
that ignore contexts, e.g. testcontainers).
|
|
29
|
+
- **Never try to install or start a local engine** (`colima start`, Docker
|
|
30
|
+
Desktop, ...) — it cannot work here. If `docker info` fails, the bridge or
|
|
31
|
+
the host engine is down: tell the user to start the engine on the host
|
|
32
|
+
and/or re-run `agent-dev-env run macos`, and continue with work that does
|
|
33
|
+
not need Docker.
|
|
34
|
+
|
|
35
|
+
### Containers you launch
|
|
36
|
+
|
|
37
|
+
- Containers run on the **host engine**, and the Docker daemon resolves
|
|
38
|
+
volume-mount paths on the **host's filesystem** — the guest mount
|
|
39
|
+
(`{{GUEST_MOUNT}}`) does not exist there. Bind mounts must use **host
|
|
40
|
+
paths**: mount `{{HOST_WORK_DIR}}/<project>/...` into the container, not
|
|
41
|
+
`{{GUEST_MOUNT}}/<project>/...`. This applies to `docker run -v` and to
|
|
42
|
+
compose `volumes:` entries alike (compose resolves relative mount paths in
|
|
43
|
+
the guest before the daemon sees them); named volumes are unaffected.
|
|
44
|
+
- Published ports are bound on the **host**. From inside the VM they are
|
|
45
|
+
reachable at the NAT gateway — `192.168.64.1:<port>` (verify with `route -n
|
|
46
|
+
get default`) — **not** at `localhost:<port>`. Check a container you
|
|
47
|
+
started with `curl http://192.168.64.1:<port>`, and tell the user the port
|
|
48
|
+
is available as `http://localhost:<port>` on the host itself.
|
|
49
|
+
- Image builds run on the host engine: `docker build` and `docker buildx`
|
|
50
|
+
work as usual.
|
|
51
|
+
|
|
52
|
+
## Services you start
|
|
53
|
+
|
|
54
|
+
- A server you start inside the VM (e.g. a dev server) is reachable from the
|
|
55
|
+
host at `http://<vm-ip>:<port>` — the VM's IP is printed when the sandbox
|
|
56
|
+
starts, or ask the user to run `tart ip <vm>`. OpenChamber itself listens
|
|
57
|
+
on port 4000.
|
|
58
|
+
|
|
59
|
+
## Config changes
|
|
60
|
+
|
|
61
|
+
- After editing opencode or Copilot settings in the VM, restart OpenChamber
|
|
62
|
+
with `openchamber restart`; to pull the host's versions of the settings
|
|
63
|
+
instead, the user runs `agent-dev-env sync macos`.
|
|
64
|
+
|
|
65
|
+
## SSH agent bridge
|
|
66
|
+
|
|
67
|
+
- The runner bridges the host's SSH agent into the VM, so `git push`, `git
|
|
68
|
+
fetch`, `gh`, `ssh` and `scp` work against any host the user's agent knows,
|
|
69
|
+
without keys on disk. `SSH_AUTH_SOCK` points at the bridged socket
|
|
70
|
+
(`/tmp/ssh-agent.sock`), and `~/.ssh/config` sets `IdentityAgent` for tools
|
|
71
|
+
that don't read the export.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// watch-build-ocr.swift — OCR helper for the Windows sandbox build watchdog.
|
|
2
|
+
//
|
|
3
|
+
// Compiled on first use by the CLI's `watch-build` command (swiftc -O)
|
|
4
|
+
// into the watchdog outdir; the Python worker (watch-build.py) runs it on
|
|
5
|
+
// each captured VNC frame. Uses Apple's Vision framework — no external OCR
|
|
6
|
+
// dependency, and the Xcode command line tools are already a build
|
|
7
|
+
// prerequisite on the host.
|
|
8
|
+
//
|
|
9
|
+
// Prints one line per recognized text:
|
|
10
|
+
// <text> | center=(<x>,<y>) box=(<x>,<y> <w>x<h>)
|
|
11
|
+
// with pixel coordinates in the image's own resolution (top-left origin),
|
|
12
|
+
// so the worker can click OCR'd buttons without assuming a framebuffer
|
|
13
|
+
// size.
|
|
14
|
+
|
|
15
|
+
import Vision
|
|
16
|
+
import AppKit
|
|
17
|
+
|
|
18
|
+
let url = URL(fileURLWithPath: CommandLine.arguments[1])
|
|
19
|
+
guard let img = NSImage(contentsOf: url),
|
|
20
|
+
let cg = img.cgImage(forProposedRect: nil, context: nil, hints: nil) else {
|
|
21
|
+
FileHandle.standardError.write("cannot load image\n".data(using: .utf8)!)
|
|
22
|
+
exit(1)
|
|
23
|
+
}
|
|
24
|
+
let w = cg.width
|
|
25
|
+
let h = cg.height
|
|
26
|
+
let request = VNRecognizeTextRequest()
|
|
27
|
+
request.recognitionLevel = .accurate
|
|
28
|
+
request.usesLanguageCorrection = false
|
|
29
|
+
let handler = VNImageRequestHandler(cgImage: cg, options: [:])
|
|
30
|
+
try handler.perform([request])
|
|
31
|
+
for obs in request.results ?? [] {
|
|
32
|
+
if let c = obs.topCandidates(1).first {
|
|
33
|
+
let bb = obs.boundingBox // normalized, origin bottom-left
|
|
34
|
+
let x = Int(bb.origin.x * CGFloat(w))
|
|
35
|
+
let y = Int((1 - bb.origin.y - bb.size.height) * CGFloat(h))
|
|
36
|
+
let bw = Int(bb.size.width * CGFloat(w))
|
|
37
|
+
let bh = Int(bb.size.height * CGFloat(h))
|
|
38
|
+
print("\(c.string) | center=(\(x + bw/2),\(y + bh/2)) box=(\(x),\(y) \(bw)x\(bh))")
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Watchdog for the headless sandbox builds (Windows + Ubuntu).
|
|
3
|
+
|
|
4
|
+
Why it exists: the Packer build's boot_command types Enter 15 times to
|
|
5
|
+
answer the firmware's boot prompts. The stray keys can hit "Cancel" on
|
|
6
|
+
Windows Setup's "Installing Windows 11" screen — Setup then asks "Are you
|
|
7
|
+
sure you want to quit?" and, headless, nothing dismisses it: the build
|
|
8
|
+
stalls forever. Boot races can also land in the UEFI shell. This watchdog
|
|
9
|
+
polls the build's VNC framebuffer, OCRs each frame (Apple Vision via
|
|
10
|
+
watch-build-ocr.swift), and:
|
|
11
|
+
|
|
12
|
+
- clicks "No" on the quit-confirmation dialog (at the OCR'd button
|
|
13
|
+
position; falls back to measured coordinates for the 800x600 buffer),
|
|
14
|
+
- presses a key when "Press any key to boot from CD or DVD" is on screen,
|
|
15
|
+
- boots the ISO from the UEFI shell (fs0: + EFI\\BOOT\\BOOTAA64.EFI),
|
|
16
|
+
- logs loudly when Windows Setup's "Windows could not complete the
|
|
17
|
+
installation" dialog is on screen (the build then hangs at WinRM —
|
|
18
|
+
nothing to click; the root cause is in the guest's Panther logs),
|
|
19
|
+
- Ubuntu builds: types the autoinstall kernel command into grub when
|
|
20
|
+
the grub menu or shell appears (WATCH_BUILD_BOOT_CMD env var, one
|
|
21
|
+
grub command per line; the env var is read per frame, so it can be
|
|
22
|
+
filled in after the build started, e.g. once the HTTP port is known).
|
|
23
|
+
While the command is still untyped (no .boot-typed marker in the frame
|
|
24
|
+
directory), the supervisor polls every 3 s — grub's menu countdown is
|
|
25
|
+
~20 s wide and the slow cadence (~2 min per frame) can miss it, which
|
|
26
|
+
makes the interactive Subiquity installer boot instead of autoinstall
|
|
27
|
+
and the build hang waiting for SSH. Once the command is typed (or the
|
|
28
|
+
fast-poll cap passes), the slow cadence resumes. The grub command is
|
|
29
|
+
typed only once per build (a marker file in the frame directory
|
|
30
|
+
records it; the build wrapper removes the marker when the watchdog
|
|
31
|
+
starts), and only after grub is actually on screen — the firmware's
|
|
32
|
+
(variable-length) No-Media/PXE probe cycle no longer races the Packer
|
|
33
|
+
boot_command typing.
|
|
34
|
+
|
|
35
|
+
The supervisor runs every capture in a subprocess with a hard timeout, so a
|
|
36
|
+
hung VNC/OCR cycle cannot stall the watch. See the CLI's `watch-build`
|
|
37
|
+
command for the entry point and prerequisites (python3 + vncdotool +
|
|
38
|
+
swiftc).
|
|
39
|
+
|
|
40
|
+
Usage:
|
|
41
|
+
watch-build.py <vnc-port> <outdir> <ocr-binary>
|
|
42
|
+
watch-build.py --worker <vnc-port> <outdir> <ocr-binary> <frame>
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
import os
|
|
46
|
+
import re
|
|
47
|
+
import subprocess
|
|
48
|
+
import sys
|
|
49
|
+
import time
|
|
50
|
+
|
|
51
|
+
# Fallback "No" button center for the 800x600 framebuffer (top-left origin),
|
|
52
|
+
# used when the OCR did not return a position for the button.
|
|
53
|
+
NO_BUTTON_FALLBACK = (487, 381)
|
|
54
|
+
|
|
55
|
+
# --- poll pacing -----------------------------------------------------------
|
|
56
|
+
#
|
|
57
|
+
# Until the Ubuntu autoinstall command is typed (or the hard cap below
|
|
58
|
+
# passes), the supervisor polls fast so at least one worker lands inside
|
|
59
|
+
# grub's ~20 s menu countdown. The slow cadence (90 s worker + 20 s sleep)
|
|
60
|
+
# scans a frame roughly every 2 minutes, so it can miss the menu entirely —
|
|
61
|
+
# the default entry then boots the interactive Subiquity installer and the
|
|
62
|
+
# build hangs waiting for SSH ("Timeout waiting for SSH" after the 60 m
|
|
63
|
+
# ssh_timeout; observed in the Ubuntu 24.04 build on 2026-08-29).
|
|
64
|
+
# Once .boot-typed exists the slow cadence resumes — the only screens left
|
|
65
|
+
# are the installer / Windows dialogs, which change on the minute timescale.
|
|
66
|
+
# The worker timeout stays at 90 s even in the fast phase: the worker's own
|
|
67
|
+
# OCR timeout (50 s) and connect timeout (10 s) bound a slow worker, and a
|
|
68
|
+
# kill mid-typing would corrupt the grub shell input line.
|
|
69
|
+
FAST_POLL_INTERVAL = 3
|
|
70
|
+
FAST_POLL_MAX_SECONDS = 240
|
|
71
|
+
SLOW_POLL_INTERVAL = 20
|
|
72
|
+
SLOW_POLL_WORKER_TIMEOUT = 90
|
|
73
|
+
|
|
74
|
+
# Ubuntu autoinstall boot command (rendered by the build wrapper; see the
|
|
75
|
+
# module docstring). Read per worker invocation, so the value can be
|
|
76
|
+
# provided after the watchdog started.
|
|
77
|
+
BOOT_CMD = os.environ.get('WATCH_BUILD_BOOT_CMD', '')
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def log(msg):
|
|
81
|
+
print(time.ctime(), msg, flush=True)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
# --- worker: capture one frame, act on what is on screen --------------------
|
|
85
|
+
|
|
86
|
+
def type_colon(vnc):
|
|
87
|
+
"""The VNC keymap mangles ':' (shift modifier dropped), so send
|
|
88
|
+
shift+';' explicitly."""
|
|
89
|
+
vnc.keyDown('lshift')
|
|
90
|
+
time.sleep(0.15)
|
|
91
|
+
vnc.keyPress(';')
|
|
92
|
+
time.sleep(0.15)
|
|
93
|
+
vnc.keyUp('lshift')
|
|
94
|
+
time.sleep(0.2)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def type_string(vnc, text):
|
|
98
|
+
"""Type a string character by character. ':' needs the shift trick
|
|
99
|
+
(see type_colon); everything else is a plain key. Per-character
|
|
100
|
+
try/except so an unmappable key never aborts the typing."""
|
|
101
|
+
for ch in text:
|
|
102
|
+
try:
|
|
103
|
+
if ch == ':':
|
|
104
|
+
type_colon(vnc)
|
|
105
|
+
else:
|
|
106
|
+
vnc.keyPress(ch)
|
|
107
|
+
time.sleep(0.08)
|
|
108
|
+
except Exception: # noqa: BLE001 - keep typing the rest
|
|
109
|
+
log(f'WARN: could not type {ch!r}')
|
|
110
|
+
|
|
111
|
+
time.sleep(0.5)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def type_boot_command(vnc, outdir, at_shell):
|
|
115
|
+
"""Types the Ubuntu autoinstall grub command (BOOT_CMD, one grub
|
|
116
|
+
command per line). 'c' opens the grub shell from the menu; when the
|
|
117
|
+
shell prompt is already up (at_shell), the command lines are typed
|
|
118
|
+
directly."""
|
|
119
|
+
if not at_shell:
|
|
120
|
+
vnc.keyPress('c')
|
|
121
|
+
time.sleep(1.0)
|
|
122
|
+
for line in BOOT_CMD.split('\n'):
|
|
123
|
+
if not line.strip():
|
|
124
|
+
continue
|
|
125
|
+
type_string(vnc, line)
|
|
126
|
+
vnc.keyPress('return')
|
|
127
|
+
time.sleep(1.5)
|
|
128
|
+
# Snapshot the shell right after typing (before/while the kernel
|
|
129
|
+
# boots) — debugging aid: the frame shows the typed command line.
|
|
130
|
+
try:
|
|
131
|
+
vnc.captureScreen(os.path.join(outdir, 'boot-typed.png'))
|
|
132
|
+
except Exception: # noqa: BLE001 - best effort
|
|
133
|
+
pass
|
|
134
|
+
# The marker: typed once per build (the wrapper removes it when the
|
|
135
|
+
# watchdog starts). The worker is re-exec-ed per frame, so state only
|
|
136
|
+
# survives via this file.
|
|
137
|
+
with open(os.path.join(outdir, '.boot-typed'), 'w') as f:
|
|
138
|
+
f.write(time.ctime())
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def click_no(vnc, position):
|
|
142
|
+
vnc.mouseMove(*position)
|
|
143
|
+
time.sleep(0.3)
|
|
144
|
+
vnc.mousePress(1)
|
|
145
|
+
time.sleep(0.2)
|
|
146
|
+
vnc.mouseUp(1)
|
|
147
|
+
time.sleep(0.5)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def shell_rescue(vnc):
|
|
151
|
+
"""Boot the Windows ISO from the UEFI shell."""
|
|
152
|
+
for ch in 'fs0':
|
|
153
|
+
vnc.keyPress(ch)
|
|
154
|
+
time.sleep(0.25)
|
|
155
|
+
type_colon(vnc)
|
|
156
|
+
vnc.keyPress('return')
|
|
157
|
+
time.sleep(2.5)
|
|
158
|
+
for ch in r'EFI\BOOT\BOOTAA64.EFI':
|
|
159
|
+
vnc.keyPress(ch)
|
|
160
|
+
time.sleep(0.25)
|
|
161
|
+
vnc.keyPress('return')
|
|
162
|
+
time.sleep(1.5)
|
|
163
|
+
vnc.keyPress('spacebar')
|
|
164
|
+
time.sleep(0.5)
|
|
165
|
+
vnc.keyPress('spacebar')
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def find_no_button(ocr_text):
|
|
169
|
+
"""OCR lines look like 'No | center=(487,381) box=(...)' — return the
|
|
170
|
+
center of the last line whose text is exactly 'No' (buttons sit at the
|
|
171
|
+
bottom of the dialog), or None."""
|
|
172
|
+
found = None
|
|
173
|
+
for line in ocr_text.splitlines():
|
|
174
|
+
if '|' not in line:
|
|
175
|
+
continue
|
|
176
|
+
label, _, rest = line.partition('|')
|
|
177
|
+
if label.strip().lower() != 'no':
|
|
178
|
+
continue
|
|
179
|
+
m = re.search(r'center=\((\d+),(\d+)\)', rest)
|
|
180
|
+
if m:
|
|
181
|
+
found = (int(m.group(1)), int(m.group(2)))
|
|
182
|
+
return found
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def run_worker(port, outdir, ocr, frame):
|
|
186
|
+
from vncdotool import api
|
|
187
|
+
|
|
188
|
+
vnc = api.connect(f'127.0.0.1::{port}', timeout=10)
|
|
189
|
+
try:
|
|
190
|
+
vnc.captureScreen(frame)
|
|
191
|
+
text = subprocess.run([ocr, frame], capture_output=True,
|
|
192
|
+
text=True, timeout=50).stdout
|
|
193
|
+
actions = []
|
|
194
|
+
if 'Are you sure you want to quit?' in text:
|
|
195
|
+
actions.append('NO_CLICK')
|
|
196
|
+
pos = find_no_button(text) or NO_BUTTON_FALLBACK
|
|
197
|
+
click_no(vnc, pos)
|
|
198
|
+
if 'Press any key to boot from CD' in text:
|
|
199
|
+
actions.append('CD_KEY')
|
|
200
|
+
vnc.keyPress('spacebar')
|
|
201
|
+
if 'startup.nsh' in text or 'Shell>' in text or 'Shel1>' in text:
|
|
202
|
+
actions.append('SHELL_RESCUE')
|
|
203
|
+
shell_rescue(vnc)
|
|
204
|
+
if 'could not complete the installation' in text:
|
|
205
|
+
# Windows Setup (Win11, VMware Fusion): the first-boot pass
|
|
206
|
+
# failed ("Windows could not complete the installation. To
|
|
207
|
+
# install Windows on this computer, restart the
|
|
208
|
+
# installation."). Clicking OK only restarts the broken
|
|
209
|
+
# install — do not press anything; shout loudly instead, so
|
|
210
|
+
# the build log shows the real state instead of Packer
|
|
211
|
+
# spinning on "Waiting for WinRM" for 90 min. Diagnose from
|
|
212
|
+
# C:\Windows\Panther\setuperr.log / setupact.log on the disk.
|
|
213
|
+
actions.append('SETUP_FAILED')
|
|
214
|
+
log('WINDOWS SETUP FAILED: "Windows could not complete the '
|
|
215
|
+
'installation" dialog on screen — the build will hang at '
|
|
216
|
+
'WinRM. Check C:\\Windows\\Panther\\setuperr.log.')
|
|
217
|
+
if (BOOT_CMD and 'grub>' not in text
|
|
218
|
+
and 'Try or Install Ubuntu Server' not in text):
|
|
219
|
+
# keep silent: grub is not on screen yet
|
|
220
|
+
pass
|
|
221
|
+
elif BOOT_CMD and not os.path.exists(
|
|
222
|
+
os.path.join(outdir, '.boot-typed')):
|
|
223
|
+
# grub menu or shell is up: type the autoinstall command.
|
|
224
|
+
actions.append('AUTOINSTALL_BOOT')
|
|
225
|
+
type_boot_command(vnc, outdir, at_shell='grub>' in text)
|
|
226
|
+
log('actions: ' + (','.join(actions) if actions else 'none'))
|
|
227
|
+
finally:
|
|
228
|
+
vnc.disconnect()
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
# --- supervisor: run the worker per frame with a hard timeout ---------------
|
|
232
|
+
|
|
233
|
+
def main():
|
|
234
|
+
args = sys.argv[1:]
|
|
235
|
+
if args and args[0] in ('-h', '--help'):
|
|
236
|
+
print(__doc__)
|
|
237
|
+
return
|
|
238
|
+
if args and args[0] == '--worker':
|
|
239
|
+
# Re-exec'd by the supervisor: one capture cycle.
|
|
240
|
+
_, port, outdir, ocr, frame = args
|
|
241
|
+
run_worker(port, outdir, ocr, frame)
|
|
242
|
+
return
|
|
243
|
+
|
|
244
|
+
port, outdir, ocr = args
|
|
245
|
+
os.makedirs(outdir, exist_ok=True)
|
|
246
|
+
log(f'watching VNC port {port} (frames in {outdir})')
|
|
247
|
+
i = 0
|
|
248
|
+
fast_until = time.time() + FAST_POLL_MAX_SECONDS
|
|
249
|
+
try:
|
|
250
|
+
while True:
|
|
251
|
+
frame = os.path.join(outdir, f'w{i:04d}.png')
|
|
252
|
+
# Fast-poll while the Ubuntu autoinstall command still has to be
|
|
253
|
+
# typed (grub's menu countdown is only ~20 s wide); relax once
|
|
254
|
+
# typed or when a boot never reaches grub (hard cap, so a stuck
|
|
255
|
+
# interactive installer is watched at the gentle cadence).
|
|
256
|
+
boot_marker = os.path.join(outdir, '.boot-typed')
|
|
257
|
+
fast = bool(BOOT_CMD) and not os.path.exists(boot_marker) \
|
|
258
|
+
and time.time() < fast_until
|
|
259
|
+
try:
|
|
260
|
+
subprocess.run(
|
|
261
|
+
[sys.executable, os.path.abspath(__file__), '--worker',
|
|
262
|
+
port, outdir, ocr, frame],
|
|
263
|
+
timeout=SLOW_POLL_WORKER_TIMEOUT)
|
|
264
|
+
except subprocess.TimeoutExpired:
|
|
265
|
+
log('worker timed out — skipping this frame')
|
|
266
|
+
except Exception as e: # noqa: BLE001 - keep watching
|
|
267
|
+
log(f'error: {e}')
|
|
268
|
+
i += 1
|
|
269
|
+
time.sleep(FAST_POLL_INTERVAL if fast else SLOW_POLL_INTERVAL)
|
|
270
|
+
except KeyboardInterrupt:
|
|
271
|
+
log('stopping')
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
if __name__ == '__main__':
|
|
275
|
+
main()
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
//
|
|
3
|
+
// cli.ts — the agent-dev-env CLI entry point (commander).
|
|
4
|
+
//
|
|
5
|
+
// The surface is registered in commands/register.ts (small register
|
|
6
|
+
// functions, one per group): the lifecycle commands (build/deploy/tag),
|
|
7
|
+
// the diagnostic commands (list/status/doctor), and the VM commands
|
|
8
|
+
// (run/stop/delete/sync/watch-build) — all implemented for every
|
|
9
|
+
// platform; `sync` is limited to the tart/ssh2 transports (macos,
|
|
10
|
+
// ubuntu-vmware).
|
|
11
|
+
import { Command, CommanderError } from 'commander';
|
|
12
|
+
import { createRequire } from 'node:module';
|
|
13
|
+
import { registerDoctorCommands, registerLifecycleCommands, registerStatusCommands, registerVmCommands, } from './commands/register.js';
|
|
14
|
+
import { logger } from './lib/logger.js';
|
|
15
|
+
// dist/cli.js -> ../package.json is the package root; src/cli.ts ->
|
|
16
|
+
// ../package.json is the repo root. Both work because package.json and
|
|
17
|
+
// dist/ sit next to each other in the published npm package.
|
|
18
|
+
const pkg = createRequire(import.meta.url)('../package.json');
|
|
19
|
+
function buildProgram() {
|
|
20
|
+
const program = new Command();
|
|
21
|
+
program
|
|
22
|
+
.name('agent-dev-env')
|
|
23
|
+
.description('Sandbox VM runner and image lifecycle CLI. Builds, runs, and wires ' +
|
|
24
|
+
'up sandbox VMs (macOS via Tart, Windows via QEMU/VMware, Ubuntu via ' +
|
|
25
|
+
'VMware) and manages their image releases on GHCR.')
|
|
26
|
+
.version(pkg.version)
|
|
27
|
+
.showSuggestionAfterError();
|
|
28
|
+
registerVmCommands(program);
|
|
29
|
+
registerStatusCommands(program);
|
|
30
|
+
registerLifecycleCommands(program);
|
|
31
|
+
registerDoctorCommands(program);
|
|
32
|
+
// Errors: commander messages are printed by commander itself; our own
|
|
33
|
+
// throws go through die().
|
|
34
|
+
program.exitOverride();
|
|
35
|
+
return program;
|
|
36
|
+
}
|
|
37
|
+
async function main() {
|
|
38
|
+
const program = buildProgram();
|
|
39
|
+
try {
|
|
40
|
+
await program.parseAsync(process.argv);
|
|
41
|
+
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
if (err instanceof CommanderError) {
|
|
44
|
+
process.exit(err.exitCode);
|
|
45
|
+
}
|
|
46
|
+
if (err instanceof Error) {
|
|
47
|
+
logger.die(err.message);
|
|
48
|
+
}
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
void main();
|