Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion images/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ image-build wrapper.
| `router/distrobuilder.yaml` | The recipe. Installs only the pinned APKs from an offline seed (`--no-network`, empty `/etc/apk/repositories`), enables OpenRC `lxc` mode, and emits a unified tarball. |
| `runner/distrobuilder.yaml` | Split VM recipe with `runner` and `publisher` variants, signed shim/GRUB, growroot, incus-agent generator, and `ttyS0` output. |
| `ubuntu-24.04-desktop/distrobuilder.yaml` | Split desktop VM recipe with Xorg, GDM automatic login, AT-SPI, Cua Driver, X0tigervnc, systemd-networkd, growroot, and signed shim/GRUB. |
| `ubuntu-24.04-desktop/smoke.py` | Candidate qualification for X11, the Driver session socket and user unit, native `list_apps`, disabled nesting, and owned-resource cleanup. |
| `ubuntu-24.04-desktop/smoke.py` | Candidate qualification for X11, the Driver session socket and user unit, native `list_apps`, live whole-desktop capture after launching Text Editor, disabled nesting, and owned-resource cleanup. |
| `build.py` | `validate` (schema and pin checks, no credentials) and `build` (download-verify, compile distrobuilder from vendored source, assemble). PEP 723 script with `build.py.lock`. |
| `catalog.yaml` | Startup catalog: name → digest-pinned GHCR reference, upstream Incus `remote:alias`, or cluster-local `alias` in `image-build`, plus kind, OS, and defaults. |
| `smoke.sh` | Router boot qualification, including the four `/opt/router/{nat,route,dhcp,wg}` helpers. |
Expand Down Expand Up @@ -96,6 +96,15 @@ and JSON arguments pass through unchanged; the host does not add typed
wrappers for individual Driver tools. Screenshots travel through the binary
guest-file API and return as URLs rather than embedded image data.

The daemon serves with `--no-overlay`: the synthetic agent-cursor overlay
freezes X root-window reads on this GNOME/Xorg session. Starting before the
first composite freezes a black frame; both Driver whole-desktop capture and
VNC are affected. Reproduced on Driver 0.28.1 and 0.28.2. Window capture and
input remain functional. Disabling the cosmetic overlay restores live capture
but removes its session-coloured pointer; re-enabling it reintroduces the defect.
The desktop smoke compares image data before and after launching Text Editor,
so it rejects a frozen frame even when that frame is not black.

See the [Phase 6 desktop spike report](../spikes/desktop/README.md) for direct
Driver results, timing, screenshot scaling, VNC, and reboot evidence.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ export DBUS_SESSION_BUS_ADDRESS="${DBUS_SESSION_BUS_ADDRESS:-unix:path=${XDG_RUN

case "${1:-}" in
cua-driver)
exec /usr/local/bin/cua-driver serve --socket "${XDG_RUNTIME_DIR}/cua-driver.sock"
# The cursor overlay freezes X root reads on GNOME/Xorg, including VNC.
# Disable that cosmetic overlay so whole-desktop capture stays live.
exec /usr/local/bin/cua-driver serve --no-overlay --socket "${XDG_RUNTIME_DIR}/cua-driver.sock"
;;
x0vnc)
# Only private sandbox networks and explicitly created human-viewer forwards
Expand Down
67 changes: 66 additions & 1 deletion images/ubuntu-24.04-desktop/smoke.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
"""Boot-test a split desktop VM for X11, cua-driver, and one list_apps call.
"""Boot-test a split desktop VM for X11, cua-driver, list_apps, and live capture.

The instance is created with security.nesting=false. This script imports the
candidate only when the fingerprint is absent, never touches shared aliases,
Expand Down Expand Up @@ -32,6 +32,7 @@
GUEST_GID = "1000"
X11_SOCKET = "/tmp/.X11-unix/X0"
EDITOR_LAUNCH = "gnome-text-editor"
GUEST_DESKTOP_PNG = "/tmp/agentcompute-desktop-smoke.png"


class Error(RuntimeError):
Expand Down Expand Up @@ -119,6 +120,35 @@ def guest_exec(
return incus(project, args, capture_output=True)


def guest_pull(remote: str, project: str, name: str, guest_path: str, target: Path) -> None:
pulled = incus(
project,
["file", "pull", f"{remote}:{name}{guest_path}", str(target)],
capture_output=True,
)
if pulled.returncode != 0:
die(f"pulling {guest_path} failed: {(pulled.stderr or pulled.stdout or '').strip()}")


def screenshot_pixels(raw: bytes) -> bytes:
"""Compare PNG image data without timestamps or other ancillary metadata."""
if not raw.startswith(b"\x89PNG\r\n\x1a\n"):
die("screenshot is not a PNG")
offset = 8
pixels = bytearray()
while offset + 12 <= len(raw):
length = int.from_bytes(raw[offset : offset + 4], "big")
end = offset + 12 + length
if end > len(raw):
die("screenshot PNG contains a truncated chunk")
if raw[offset + 4 : offset + 8] == b"IDAT":
pixels.extend(raw[offset + 8 : end - 4])
offset = end
if not pixels:
die("screenshot PNG contains no image data")
return bytes(pixels)


def main(argv: list[str]) -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--metadata", type=Path, required=True)
Expand Down Expand Up @@ -297,6 +327,40 @@ def unit_active() -> bool:
die("list_apps did not include gnome-text-editor")
note(f"list_apps returned {len(apps)} apps including {EDITOR_LAUNCH}", log)

def capture(filename: str) -> bytes:
captured = guest_exec(
remote, project, name,
[
DRIVER_BIN, "call", "--socket", DRIVER_SOCKET,
"--screenshot-out-file", GUEST_DESKTOP_PNG,
"get_desktop_state", "{}",
],
user=True,
)
if captured.returncode != 0:
die(f"get_desktop_state exited {captured.returncode}: {captured.stderr}")
target = evidence / filename
guest_pull(remote, project, name, GUEST_DESKTOP_PNG, target)
return screenshot_pixels(target.read_bytes())

before = capture("desktop-before.png")
launched = guest_exec(
remote, project, name,
[
"systemd-run", "--user", "--collect",
"--unit=agentcompute-capture-smoke", EDITOR_LAUNCH,
],
user=True,
)
if launched.returncode != 0:
die(f"launching {EDITOR_LAUNCH} failed: {launched.stderr}")
wait_until(
min(deadline, time.time() + 30),
"whole-desktop pixels to change after launching Text Editor",
lambda: capture("desktop.png") != before,
)
note("whole-desktop pixels changed after launching Text Editor", log)

result = {
"fingerprint": fingerprint,
"metadata": str(metadata),
Expand All @@ -308,6 +372,7 @@ def unit_active() -> bool:
"x11": True,
"cua_driver_unit": "active",
"list_apps": len(apps),
"desktop_capture_live": True,
"security_nesting": "false",
}
(evidence / "result.json").write_text(json.dumps(result, indent=2) + "\n", encoding="utf-8")
Expand Down