test: cover AWS desktop targets

This commit is contained in:
Peter Steinberger 2026-05-04 02:28:48 +01:00
parent 343ca7baa9
commit 59afa7d720
No known key found for this signature in database
16 changed files with 236 additions and 34 deletions

View File

@ -7,7 +7,9 @@
- Added `--desktop`, `--browser`, and `crabbox vnc` for optional Linux UI/browser leases, including loopback-only VNC with per-lease passwords and headless browser support without a desktop.
- Added static macOS/Windows VNC endpoint discovery, including SSH-tunneled loopback VNC and trusted static direct VNC on `host:5900`.
- Added `crabbox vnc --open` to start the SSH tunnel and launch the local VNC client for managed desktop leases.
- Added `crabbox screenshot` to save a PNG from a Linux desktop lease without opening a VNC client.
- Added managed AWS Windows desktop leases with OpenSSH, Git for Windows, loopback TightVNC, per-lease VNC passwords, and `crabbox vnc`.
- Added AWS macOS desktop lease plumbing for EC2 Mac Dedicated Hosts, including Screen Sharing setup and per-lease credentials.
- Added `crabbox screenshot` to save a PNG from a desktop lease without opening a VNC client.
- Added a minimal XFCE desktop profile with panel/window manager for managed VNC leases.
- Clarified static macOS/Windows VNC as existing-host access, not Crabbox-created boxes, so `--open` no longer launches an OS credential prompt unless `--host-managed` is passed.

View File

@ -81,7 +81,7 @@ For the full mental model, see [How Crabbox Works](docs/how-it-works.md). For th
- **Trusted AWS images.** Operators can create AMIs from active brokered AWS leases and promote a known-good image as the coordinator default.
- **Cost guardrails.** Per-lease and monthly spend caps. Live pricing from EC2 Spot history or Hetzner server-type prices, with static fallbacks. `crabbox usage` summarizes spend by user, org, provider, and type.
- **GitHub Actions hydration.** `crabbox actions hydrate` registers a leased box as an ephemeral Actions runner, so the repo's own workflow installs runtimes, services, and secrets. Crabbox does not parse Actions YAML.
- **Interactive desktop and browser leases.** `--browser` provisions Chrome or Chromium for headless automation, `--desktop` provisions Xvfb/Openbox/x11vnc for visible UI and tunnel-only VNC takeover, and QA systems such as Mantis own scenario logic, screenshots, and PR evidence.
- **Interactive desktop and browser leases.** `--browser` provisions Chrome or Chromium for headless automation, `--desktop` provisions visible UI with tunnel-only VNC takeover on managed Linux, AWS Windows, and AWS EC2 Mac targets, and QA systems such as Mantis own scenario logic, screenshots, and PR evidence.
- **Hardened coordinator auth.** GitHub browser login, owner-scoped leases, admin-only routes, optional GitHub team allowlists, Cloudflare Access JWT verification, and service-token support keep normal use and operator automation separate.
- **OpenClaw plugin.** The repo root is a native OpenClaw plugin for box lifecycle operations: `crabbox_run`, `crabbox_warmup`, `crabbox_status`, `crabbox_list`, and `crabbox_stop`. Run inspection stays in the CLI and Crabbox skill.
- **Operator surface.** `doctor`, `init`, `status`, `inspect`, `list`, `usage`, `history`, `logs`, `results`, `cache`, `admin`, `cleanup`, plus `--json` output where it matters.

View File

@ -114,6 +114,15 @@ crabbox run --provider ssh --target windows --windows-mode normal --static-host
crabbox run --provider ssh --target windows --windows-mode wsl2 --static-host win-dev.local -- pnpm test
```
Create managed AWS desktop boxes:
```sh
crabbox warmup --provider aws --target windows --desktop --market on-demand
CRABBOX_AWS_MAC_HOST_ID=h-... crabbox warmup --provider aws --target macos --desktop --market on-demand
crabbox vnc --id blue-lobster
crabbox screenshot --id blue-lobster --output desktop.png
```
Inspect pool:
```sh
@ -333,6 +342,17 @@ static:
workRoot: C:\crabbox
```
AWS EC2 Mac target:
```yaml
provider: aws
target: macos
aws:
macHostId: h-0123456789abcdef0
capacity:
market: on-demand
```
`windows.mode: normal` runs native PowerShell over OpenSSH and syncs with a tar
archive. `windows.mode: wsl2` runs commands through `wsl.exe --exec bash -lc`
and uses rsync inside WSL2, so `static.workRoot` should be a WSL path.

View File

@ -1,7 +1,7 @@
# screenshot
`crabbox screenshot` captures a PNG from a Linux desktop lease without opening a
VNC client.
`crabbox screenshot` captures a PNG from a desktop lease without opening a VNC
client.
```sh
crabbox warmup --desktop
@ -11,7 +11,9 @@ crabbox screenshot --id blue-lobster --output desktop.png
The command resolves and touches the lease like `crabbox ssh`, verifies that the
lease has `desktop=true`, waits for the loopback desktop/VNC service, then
streams a PNG over SSH from `DISPLAY=:99`.
streams a PNG over SSH. Linux captures `DISPLAY=:99`, Windows captures the
primary desktop with PowerShell/.NET drawing APIs, and macOS uses
`screencapture`.
If `--output` is omitted, Crabbox writes:
@ -19,10 +21,10 @@ If `--output` is omitted, Crabbox writes:
crabbox-<slug-or-id>-screenshot.png
```
Screenshots are currently supported for Linux desktop leases. Static macOS and
Windows targets are existing host machines, not Crabbox-created desktops, so
`screenshot` rejects those targets instead of capturing your local or home-host
desktop by accident.
Static macOS and Windows targets are existing host machines, not Crabbox-created
desktops, so `screenshot` rejects those targets instead of capturing your local
or home-host desktop by accident. Managed AWS Windows and AWS macOS desktop
leases are Crabbox-created boxes and can be captured by lease id or slug.
Flags:

View File

@ -26,10 +26,11 @@ Keep the tunnel process running while connected.
```
Run the tunnel command in another terminal, then connect your VNC client to the
printed `localhost:<port>` endpoint. Managed Linux desktop leases use a
per-lease VNC password stored on the runner under `/var/lib/crabbox`; the
password is retrieved over SSH only when `vnc` is called. It is not stored in
provider labels or run history.
printed `localhost:<port>` endpoint. Managed desktop leases use a per-lease VNC
password stored on the runner. Linux stores it under `/var/lib/crabbox`, Windows
under `C:\ProgramData\crabbox`, and macOS under `/var/db/crabbox`; the password
is retrieved over SSH only when `vnc` is called. It is not stored in provider
labels or run history.
Use `--open` to let Crabbox start the SSH tunnel, open the local VNC URL, and
print the tunnel process ID. Keep that tunnel process alive while connected.
@ -55,14 +56,24 @@ Security boundary:
- VNC is never exposed directly to the public internet.
- Managed Linux binds x11vnc to `127.0.0.1:5900` on the runner.
- Managed Windows installs TightVNC and connects through the SSH tunnel.
- Managed macOS enables Screen Sharing and connects through the SSH tunnel.
- Crabbox does not add provider firewall or security-group ingress for VNC.
- Brokered leases use SSH tunnels only. Static hosts may also use direct
operator-managed VNC when `host:5900` is already reachable.
Provider behavior:
- Brokered and direct AWS/Hetzner leases are Linux-only in this release. They
support `vnc` only when created with `--desktop`.
- Brokered and direct Hetzner leases support Linux VNC only when created with
`--desktop`.
- Brokered and direct AWS Linux leases support VNC when created with
`--desktop`.
- Brokered and direct AWS native Windows leases support VNC when created with
`--target windows --desktop`. Crabbox installs OpenSSH, Git for Windows, and
TightVNC through EC2Launch user data.
- Brokered and direct AWS macOS leases support VNC when created with
`--target macos --desktop --market on-demand` and an EC2 Mac Dedicated Host id
from `CRABBOX_AWS_MAC_HOST_ID` or `aws.macHostId`.
- Static Linux can participate if the operator already configured Xvfb and
loopback-bound x11vnc.
- Static macOS can participate when Screen Sharing or another VNC-compatible
@ -70,8 +81,8 @@ Provider behavior:
`host:5900`. This reuses an existing Mac; it does not create a macOS Crabbox.
Credentials are host-managed.
- Static native Windows can participate when a VNC server is already available
on `127.0.0.1:5900` over SSH or directly on `host:5900`. Crabbox does not
create a Windows Crabbox, or install or configure the Windows VNC server.
on `127.0.0.1:5900` over SSH or directly on `host:5900`. Static Windows is
still host-managed; managed Windows VNC is AWS-only.
- Blacksmith Testbox does not support managed VNC in this release.
Flags:

View File

@ -7,6 +7,8 @@ crabbox warmup --class beast
crabbox warmup --provider aws --class beast --market on-demand
crabbox warmup --browser
crabbox warmup --desktop --browser
crabbox warmup --provider aws --target windows --desktop --market on-demand
crabbox warmup --provider aws --target macos --desktop --market on-demand --type mac2.metal
crabbox warmup --actions-runner
crabbox warmup --provider blacksmith-testbox --blacksmith-workflow .github/workflows/ci-check-testbox.yml --blacksmith-job test
crabbox warmup --provider ssh --target macos --static-host mac-studio.local
@ -25,6 +27,18 @@ OpenSSH Server reachable, PowerShell, Git, `tar`, and a writable
`static.workRoot`. Restart `sshd` after installing Git so new SSH sessions see
the updated PATH.
With `--provider aws --target windows --desktop`, Crabbox creates a real AWS
Windows Server lease. EC2Launch user data installs OpenSSH Server, Git for
Windows, TightVNC Server, a per-lease local administrator named `crabbox`, and a
loopback VNC password retrievable through `crabbox vnc --id <lease>`.
With `--provider aws --target macos --desktop`, Crabbox launches an EC2 Mac
instance on an already allocated Dedicated Host. Set `CRABBOX_AWS_MAC_HOST_ID`
or `aws.macHostId`, use `--market on-demand`, and expect EC2 Mac host lifecycle
rules to dominate cleanup and cost. The default SSH user is `ec2-user`; the VNC
password printed by `crabbox vnc` is the per-lease macOS account password set by
bootstrap.
On success, `warmup` prints a concise total duration line. Add `--timing-json` to emit a final JSON timing record with provider, lease ID, slug, total duration, and exit code.
Flags:

View File

@ -14,8 +14,8 @@ inside that lease.
The intended contract is:
- `crabbox warmup --desktop` leases or reuses a Linux machine with the normal
Crabbox SSH contract plus a desktop profile;
- `crabbox warmup --desktop` leases or reuses a machine with the normal Crabbox
SSH contract plus a desktop profile;
- `crabbox warmup --browser` leases or reuses a Linux machine with a known
browser binary for headless automation;
- `crabbox warmup --desktop --browser` combines a visible session with a browser
@ -69,16 +69,22 @@ Security rules:
- never expose VNC directly to the public internet;
- prefer SSH local forwarding such as `localhost:5901 -> 127.0.0.1:5900`;
- generate per-lease VNC passwords for managed Linux desktop leases;
- generate per-lease VNC passwords for managed desktop leases;
- redact passwords from logs and run records;
- stop desktop services when the lease stops;
- keep the normal TTL and idle-timeout lifecycle in force.
Provider notes:
- Hetzner and AWS brokered Linux leases are the primary target because Crabbox
controls cloud-init and firewall shape there. Brokered macOS and Windows
desktop leases do not exist in this release.
- Hetzner and AWS brokered Linux leases use cloud-init to install Xvfb, XFCE,
x11vnc, and optional Chrome/Chromium.
- AWS brokered Windows desktop leases use EC2Launch PowerShell user data to
install OpenSSH, Git for Windows, TightVNC, and a local `crabbox`
administrator. VNC is reached through the SSH tunnel; the security group only
needs SSH.
- AWS brokered macOS desktop leases require an allocated EC2 Mac Dedicated Host
and On-Demand capacity. Bootstrap enables Screen Sharing for `ec2-user` and
stores the generated password on the instance for `crabbox vnc`.
- Static SSH Linux hosts can participate when the operator accepts responsibility
for packages and display services.
- Static macOS hosts are existing Macs, not Crabbox-created boxes. They can
@ -94,8 +100,8 @@ Provider notes:
- Blacksmith Testbox can run headless browser automation today, but VNC takeover
needs a Blacksmith-supported SSH tunnel or connection-info API before Crabbox
can offer the same `vnc` command there.
- Crabbox-managed macOS and Windows VNC installers are still out of scope for
this release.
- EC2 Mac host allocation, host scrubbing, and the AWS 24-hour host lifecycle
remain operator concerns; Crabbox only launches onto a host id it is given.
For Mantis, the first consumer should be a Discord QA lane:

View File

@ -13,8 +13,9 @@ hetzner Hetzner Cloud servers
aws AWS EC2 one-time Spot instances
```
Brokered Hetzner and AWS leases are Linux targets. macOS and Windows targets use
the direct static SSH provider:
Brokered Hetzner leases are Linux targets. Brokered AWS supports Linux, native
Windows Server, and EC2 Mac when a Dedicated Host is configured. Static SSH
still exists for reusing existing macOS and Windows machines:
```text
ssh Existing SSH host selected by static.host
@ -34,6 +35,10 @@ AWS behavior:
- imports or reuses an EC2 key pair;
- creates or reuses the `crabbox-runners` security group with SSH ingress limited to configured CIDRs or the request source IP;
- launches one-time Spot instances;
- launches AWS Windows Server desktop leases with EC2Launch PowerShell user
data, OpenSSH, Git for Windows, and TightVNC when `target=windows`;
- launches EC2 Mac leases only with an explicit Dedicated Host id
(`CRABBOX_AWS_MAC_HOST_ID` or `aws.macHostId`) and On-Demand capacity;
- tags instances, volumes, and Spot requests;
- falls back across broad C/M/R instance families for class requests, including account policy and capacity rejections;
- can fall back to a small burstable type when account policy rejects the high-core class candidates;
@ -64,6 +69,15 @@ standard c7a.8xlarge, c7i.8xlarge, m7a.8xlarge, m7i.8xlarge, c7a.4xlarge
fast c7a.16xlarge, c7i.16xlarge, m7a.16xlarge, m7i.16xlarge, c7a.12xlarge, c7a.8xlarge
large c7a.24xlarge, c7i.24xlarge, m7a.24xlarge, m7i.24xlarge, r7a.24xlarge, c7a.16xlarge, c7a.12xlarge
beast c7a.48xlarge, c7i.48xlarge, m7a.48xlarge, m7i.48xlarge, r7a.48xlarge, c7a.32xlarge, c7i.32xlarge, m7a.32xlarge, c7a.24xlarge, c7a.16xlarge
AWS Windows
standard m7i.large, m7a.large, t3.large
fast m7i.xlarge, m7a.xlarge, t3.xlarge
large m7i.2xlarge, m7a.2xlarge, t3.2xlarge
beast m7i.4xlarge, m7a.4xlarge, m7i.2xlarge
AWS macOS
all mac2.metal unless `--type` is set
```
Direct provider mode still exists when no coordinator is configured. It uses local AWS credentials or `HCLOUD_TOKEN`/`HETZNER_TOKEN` and should stay secondary to the brokered path.

View File

@ -81,3 +81,45 @@ func TestCloudInitBrowserProfile(t *testing.T) {
}
}
}
func TestAWSUserDataWindowsProfile(t *testing.T) {
cfg := baseConfig()
cfg.Provider = "aws"
cfg.TargetOS = targetWindows
cfg.WindowsMode = windowsModeNormal
cfg.WorkRoot = `C:\crabbox`
got := awsUserData(cfg, "ssh-ed25519 test")
for _, want := range []string{
"<powershell>",
"OpenSSH.Server~~~~0.0.1.0",
"administrators_authorized_keys",
"Git-2.52.0-64-bit.exe",
"tightvnc-2.8.85-gpl-setup-64bit.msi",
"VALUE_OF_PASSWORD=$vncPassword",
"VALUE_OF_ALLOWLOOPBACK=1",
`C:\ProgramData\crabbox\vnc.password`,
} {
if !strings.Contains(got, want) {
t.Fatalf("windows user data missing %q", want)
}
}
}
func TestAWSUserDataMacOSProfile(t *testing.T) {
cfg := baseConfig()
cfg.Provider = "aws"
cfg.TargetOS = targetMacOS
cfg.SSHUser = "ec2-user"
got := awsUserData(cfg, "ssh-ed25519 test")
for _, want := range []string{
"#!/bin/bash",
"/var/db/crabbox/vnc.password",
"com.apple.screensharing",
"/usr/local/bin/crabbox-ready",
"nc -z 127.0.0.1 5900",
} {
if !strings.Contains(got, want) {
t.Fatalf("macOS user data missing %q", want)
}
}
}

View File

@ -28,3 +28,14 @@ func TestScreenshotRemoteCommandUsesDesktopDisplayAndPNG(t *testing.T) {
}
}
}
func TestScreenshotRemoteCommandSupportsWindowsAndMacOS(t *testing.T) {
windows := screenshotRemoteCommand(SSHTarget{TargetOS: targetWindows, WindowsMode: windowsModeNormal})
if !strings.Contains(windows, "System.Windows.Forms") || !strings.Contains(windows, "ImageFormat]::Png") {
t.Fatalf("windows screenshot command=%s", windows)
}
mac := screenshotRemoteCommand(SSHTarget{TargetOS: targetMacOS})
if !strings.Contains(mac, "screencapture -x -t png -") {
t.Fatalf("mac screenshot command=%s", mac)
}
}

View File

@ -89,7 +89,7 @@ func validateProviderTarget(cfg Config) error {
return nil
}
if cfg.Provider == "aws" && cfg.TargetOS == targetMacOS {
if cfg.AWSMacHostID == "" {
if cfg.AWSMacHostID == "" && cfg.Coordinator == "" {
return exit(2, "provider=aws target=macos requires CRABBOX_AWS_MAC_HOST_ID or aws.macHostId for an allocated EC2 Mac Dedicated Host")
}
if cfg.Capacity.Market != "on-demand" {

View File

@ -30,6 +30,16 @@ func TestVNCLoopbackCheckCommandSupportsWindows(t *testing.T) {
}
}
func TestVNCPasswordCommandSupportsManagedTargets(t *testing.T) {
windows := vncPasswordCommand(SSHTarget{TargetOS: targetWindows, WindowsMode: windowsModeNormal})
if !strings.Contains(windows, "EncodedCommand") {
t.Fatalf("windows password command should be encoded PowerShell: %q", windows)
}
if got := vncPasswordCommand(SSHTarget{TargetOS: targetMacOS}); got != "cat '/var/db/crabbox/vnc.password'" {
t.Fatalf("mac password command=%q", got)
}
}
func TestOpenURLCommandIncludesURL(t *testing.T) {
name, args := openURLCommand("vnc://localhost:5901")
if name == "" {

View File

@ -99,7 +99,9 @@ export function leaseConfig(input: LeaseRequest): LeaseConfig {
sshPort: input.sshPort ?? "2222",
sshFallbackPorts: validPorts(input.sshFallbackPorts ?? ["22"]),
providerKey: input.providerKey ?? "crabbox-steipete",
workRoot: input.workRoot ?? (target === "windows" && windowsMode === "normal" ? "C:\\crabbox" : "/work/crabbox"),
workRoot:
input.workRoot ??
(target === "windows" && windowsMode === "normal" ? "C:\\crabbox" : "/work/crabbox"),
ttlSeconds,
idleTimeoutSeconds,
keep: input.keep ?? false,

View File

@ -60,6 +60,7 @@ describe("aws provider", () => {
expect(
awsLaunchCandidates({
class: "beast",
target: "linux",
serverType: "c7a.48xlarge",
serverTypeExplicit: false,
}),
@ -67,6 +68,7 @@ describe("aws provider", () => {
expect(
awsLaunchCandidates({
class: "beast",
target: "linux",
serverType: "t3.small",
serverTypeExplicit: true,
}),

View File

@ -1,6 +1,6 @@
import { describe, expect, it } from "vitest";
import { cloudInit } from "../src/bootstrap";
import { awsUserData, cloudInit } from "../src/bootstrap";
import type { LeaseConfig } from "../src/config";
const config: LeaseConfig = {
@ -95,4 +95,30 @@ describe("cloud-init bootstrap", () => {
expect(got).toContain('test -x "$BROWSER"');
expect(got).toContain('"$BROWSER" --version >/dev/null');
});
it("builds Windows EC2Launch user data for managed VNC", () => {
const got = awsUserData({
...config,
target: "windows",
workRoot: "C:\\crabbox",
});
expect(got).toContain("<powershell>");
expect(got).toContain("OpenSSH.Server~~~~0.0.1.0");
expect(got).toContain("administrators_authorized_keys");
expect(got).toContain("tightvnc-2.8.85-gpl-setup-64bit.msi");
expect(got).toContain("VALUE_OF_PASSWORD=$vncPassword");
expect(got).toContain("VALUE_OF_ALLOWLOOPBACK=1");
});
it("builds macOS user data for managed screen sharing", () => {
const got = awsUserData({
...config,
target: "macos",
sshUser: "ec2-user",
});
expect(got).toContain("#!/bin/bash");
expect(got).toContain("/var/db/crabbox/vnc.password");
expect(got).toContain("com.apple.screensharing");
expect(got).toContain("/usr/local/bin/crabbox-ready");
});
});

View File

@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
import {
awsInstanceTypeCandidatesForClass,
awsInstanceTypeCandidatesForTargetClass,
leaseConfig,
serverTypeCandidatesForClass,
serverTypeForClass,
@ -40,6 +41,15 @@ describe("machine class config", () => {
"c7a.16xlarge",
]);
});
it("maps AWS Windows and macOS classes to compatible families", () => {
expect(awsInstanceTypeCandidatesForTargetClass("windows", "standard")).toEqual([
"m7i.large",
"m7a.large",
"t3.large",
]);
expect(awsInstanceTypeCandidatesForTargetClass("macos", "standard")).toEqual(["mac2.metal"]);
});
});
describe("lease config", () => {
@ -77,13 +87,43 @@ describe("lease config", () => {
expect(config.awsRegion).toBe("eu-west-1");
});
it("records linux target defaults and rejects brokered non-linux targets", () => {
it("records linux target defaults and rejects unsupported brokered non-linux targets", () => {
const config = leaseConfig({ sshPublicKey: "ssh-ed25519 test" });
expect(config.target).toBe("linux");
expect(config.windowsMode).toBe("normal");
expect(() => leaseConfig({ target: "macos", sshPublicKey: "ssh-ed25519 test" })).toThrow(
"unsupported target",
);
expect(() =>
leaseConfig({ provider: "hetzner", target: "windows", sshPublicKey: "ssh-ed25519 test" }),
).toThrow("unsupported target");
});
it("allows AWS native Windows leases", () => {
const config = leaseConfig({
provider: "aws",
target: "windows",
desktop: true,
sshPublicKey: "ssh-ed25519 test",
});
expect(config.serverType).toBe("m7i.4xlarge");
expect(config.workRoot).toBe("C:\\crabbox");
expect(config.desktop).toBe(true);
});
it("allows AWS macOS leases only with on-demand capacity", () => {
expect(() =>
leaseConfig({
provider: "aws",
target: "macos",
sshPublicKey: "ssh-ed25519 test",
}),
).toThrow("capacity.market=on-demand");
const config = leaseConfig({
provider: "aws",
target: "macos",
capacity: { market: "on-demand" },
sshPublicKey: "ssh-ed25519 test",
});
expect(config.serverType).toBe("mac2.metal");
expect(config.sshUser).toBe("ec2-user");
});
it("preserves exact server type requests", () => {