Skip to content

feat(computer): the QEMU launcher boots Windows guests and shares a thin disk's base - #524

Merged
earakely-scale merged 1 commit into
mainfrom
edgararakelyan/fd-5311-w1-windows-computers-in-the-framework
Nov 7, 2026
Merged

earakely-scale merged 1 commit into
mainfrom
edgararakelyan/fd-5311-w1-windows-computers-in-the-framework

Conversation

@earakely-scale

@earakely-scale earakely-scale commented Nov 7, 2026 •

Copy link
Copy Markdown
Collaborator

What

The QEMU launcher boots Windows guests, and a local machine image of a thin disk shares its base. First part of W1 (FD-5311) of Windows computers (FD-4538); design: Windows Computers in agent-env.

  • The guest comes from the disk. The launcher boots a disk as the guest its folder's guest file names (disk/guest beside disk/disk.img), linux when there's none, so no provider or create_vm signature changes.
  • A windows guest (amd64 only), which has no virtio drivers of its own and installs only with secure boot and a TPM 2.0, gets:
    • secure-boot UEFI with Microsoft's keys (OVMF_CODE_4M.secboot.fd, OVMF_VARS_4M.ms.fd, SMM on);
    • a fresh swtpm TPM 2.0 on every boot, so a reset leaves nothing behind;
    • its disk and the NoCloud seed on AHCI, an e1000e NIC, a 1920×1080 VGA display;
    • Hyper-V enlightenments under KVM.
  • A thin disk is a disk image of more than one layer: a base, disk/base.img, in a layer of its own below disk/disk.img (backed by it) and disk/guest. Its local machine image copies the base in a step of its own, so every machine on one base shares one copy of it, as decided for composing Windows computers (thin layers on a shared base). A one-layer disk is copied as before.
  • The launcher image adds swtpm; the release's smoke test checks for it and the secure-boot firmware.

What this doesn't do

The windows OS type stays refused: nothing composes a Windows computer yet, so no computer env can ask for one. It opens with composing (the rest of W1), together with the image check for thin disks (_read_disk reads one layer) and Modal carrying a thin disk's base (W7). Until then the Windows path is reached by a disk image that carries a guest file, as the tests below build.

Testing

On EC2 c8i.4xlarge boxes with nested virtualization (agent-environments-admin), not the laptop.

Unit. tst/unit/env/envs/test_disk_recipe.py: 33 passed, among them the new ones:

  • a windows guest's QEMU line (secure-boot firmware, SMM, TPM, AHCI, e1000e, the 1920×1080 VGA, no virtio) and a fresh swtpm on a reset;
  • a guest the launcher refuses (windows on arm64, an unknown name);
  • a machine of a one-layer disk and of a thin one.

The full suite (tst/unit and the protocol's tests, -n auto): 13,784 passed, 18 skipped. Four failed at first because the fresh box lacked unzip and httpx for the system Python; they passed once those were installed, and none touch this change.

Linux, unchanged. tst/integration/computer/launcher_test.py and disk_boot_test.py (int_test_slow, KVM): 2 passed. The shipped desktop's disk boots under the changed launcher, resets, takes a burst, and the bundle's disk task and probe pass.

Windows e2e and chaos, through LocalSandboxProvider.create_vm / reset_vm and the launcher built from this branch.

  • The disk: Windows 11 Enterprise Evaluation 25H2 (build 26200.6584), installed unattended from Microsoft's ISO with its readiness task answering on port 9999 from the signed-in user's session.
  • Packed as thin disk images in a local registry: a base image holding the 16 GB disk/base.img, and thin images adding disk/disk.img (backed by it) and disk/guest.
  • Ubuntu's swtpm AppArmor profile was loaded, so swtpm ran in the container as on a stock host.
Scenario Result
First boot, building the launcher and the machine ready in 600 s, the boot itself about 32 s; 1920×1080, secure boot on, TPM present and ready, standard user agent on session 1, the thin layer's files present
5 resets reset_vm returned in 0.3–0.4 s; ready again in 24.7–30.4 s
A reset while the guest boots from a reset ready in 30.7 s
A second thin disk on the same base, and a second VM of the first, while the first runs the second machine built in 70 s with the base step taken from the cache; ready in 120 s and 44 s with three VMs up
The two machines' layers 5 each, 4 shared (the launcher's and the base's), the tops differ
QEMU killed in a container the container stops (status 0), nothing hangs
A thin disk whose image holds no base the container exits 1: Could not open '/disk/base.img'
A guest file naming plan9 exits 2: launch: no guest plan9
The windows machine run without /dev/kvm exits 3 with the existing refusal
Teardown no container left

Not addressed here: a host keeps the base twice, in the pulled disk image and in the machine image (about 57 GB under containerd's image store). That's a follow-up for composing.

The e2e and chaos harness
"""W1 PR e2e and chaos on the spike box: W0's Windows thin disks booted by the PR's LocalSandboxProvider and launcher.
Each scenario appends a JSON line to /var/tmp/w0/w1-results.jsonl. Run with /opt/fw's venv."""

import asyncio
import json
import socket
import subprocess
import sys
import time
from pathlib import Path

from agent_env.providers.sandbox_providers.local_docker.provider import LocalSandboxProvider

W = Path("/var/tmp/w0")
IMAGES = dict(line.split("=", 1) for line in (W / "w1-images.txt").read_text().split())
OUT = W / "w1-results.jsonl"
PORTS = [9999, 22]
PROVIDER = LocalSandboxProvider()


def record(**fields):
    fields["at"] = time.strftime("%H:%M:%SZ", time.gmtime())
    with OUT.open("a") as out:
        out.write(json.dumps(fields) + "\n")
    print(json.dumps(fields), flush=True)


def ready_line(port, timeout=900):
    end = time.monotonic() + timeout
    while time.monotonic() < end:
        try:
            with socket.create_connection(("127.0.0.1", port), timeout=3) as conn:
                conn.settimeout(5)
                line = conn.recv(300).decode().strip()
                if line.startswith("ready"):
                    return line
        except OSError:
            pass
        time.sleep(1)
    return None


def sh(*args, check=False):
    return subprocess.run(args, capture_output=True, text=True, check=check)


def facts(port):
    done = sh("ssh", "-p", str(port), "-i", str(W / "secret/id"), "-o", "StrictHostKeyChecking=no", "-o",
              "UserKnownHostsFile=/dev/null", "-o", "ConnectTimeout=10", "-o", "LogLevel=ERROR", "agentadmin@127.0.0.1",
              r"powershell -NoProfile -ExecutionPolicy Bypass -File C:\agentenv\facts.ps1 & "
              r"if exist C:\opt\agentenv\cua-driver (echo cua_driver=yes) else (echo cua_driver=no)")
    return dict(line.strip().split("=", 1) for line in done.stdout.splitlines() if "=" in line)


def container_state(sandbox):
    done = sh("docker", "inspect", "-f", "{{.State.Status}} {{.State.ExitCode}}", sandbox.container_name)
    return done.stdout.strip()


async def boot(name, env=None):
    started = time.monotonic()
    sandbox = await PROVIDER.create_vm(image=IMAGES[name], exposed_ports=PORTS, env=env or {"BOOT": name}, cpu=4,
                                       memory=8192)
    created = time.monotonic() - started
    line = await asyncio.to_thread(ready_line, sandbox.host_port(9999))
    return sandbox, created, time.monotonic() - started, line


async def machines():
    tags = sh("docker", "images", "--format", "{{.Repository}}:{{.Tag}}", "agentenv-vm").stdout.split()
    return {tag: json.loads(sh("docker", "image", "inspect", "-f", "{{json .RootFS.Layers}}", tag).stdout)
            for tag in tags}


async def main():
    sandboxes = []
    used_before = sh("du", "-sb", "/var/lib/containerd").stdout.split()[0]

    a, created, total, line = await boot("thin-a")
    sandboxes.append(a)
    record(scenario="first boot of thin-a (machine image built)", create_s=round(created, 1),
           to_ready_s=round(total, 1), ready=line, facts=facts(a.host_port(22)))

    for i in range(1, 6):
        started = time.monotonic()
        await PROVIDER.reset_vm(a, image=IMAGES["thin-a"], env={"RESET": str(i)})
        returned = time.monotonic() - started
        line = await asyncio.to_thread(ready_line, a.host_port(9999))
        record(scenario=f"reset {i} of thin-a", reset_returned_s=round(returned, 1),
               to_ready_s=round(time.monotonic() - started, 1), ready=line)

    started = time.monotonic()
    await PROVIDER.reset_vm(a, image=IMAGES["thin-a"], env={"RESET": "twice-1"})
    await asyncio.sleep(5)
    await PROVIDER.reset_vm(a, image=IMAGES["thin-a"], env={"RESET": "twice-2"})
    line = await asyncio.to_thread(ready_line, a.host_port(9999))
    record(scenario="reset while booting from a reset", to_ready_s=round(time.monotonic() - started, 1), ready=line)

    results = await asyncio.gather(boot("thin-b"), boot("thin-a", {"BOOT": "second-a"}))
    for (sandbox, created, total, line), name in zip(results, ["thin-b", "second thin-a"]):
        sandboxes.append(sandbox)
        record(scenario=f"concurrent boot of {name} beside the first", create_s=round(created, 1),
               to_ready_s=round(total, 1), ready=line)
    used_after = sh("du", "-sb", "/var/lib/containerd").stdout.split()[0]
    layers = await machines()
    shared = set.intersection(*(set(v) for v in layers.values())) if len(layers) > 1 else set()
    record(scenario="machines of thin-a and thin-b", machines=len(layers),
           layer_counts={k: len(v) for k, v in layers.items()}, shared_layers=len(shared),
           tops_differ=len({v[-1] for v in layers.values()}) == len(layers),
           containerd_growth_gb=round((int(used_after) - int(used_before)) / 1e9, 1))

    victim = sandboxes[-1]
    sh("docker", "exec", victim.container_name, "pkill", "-f", "qemu-system")
    await asyncio.sleep(10)
    record(scenario="QEMU killed inside the second thin-a's container", container=container_state(victim))

    for name, why in (("no-base", "a thin disk whose image holds no base"), ("no-guest", "a guest file naming plan9")):
        try:
            sandbox = await PROVIDER.create_vm(image=IMAGES[name], exposed_ports=PORTS, env={}, cpu=4, memory=8192)
        except Exception as e:
            record(scenario=why, create_raised=f"{type(e).__name__}: {e}"[:500])
            continue
        sandboxes.append(sandbox)
        await asyncio.sleep(20)
        logs = sh("docker", "logs", "--tail", "5", sandbox.container_name)
        record(scenario=why, container=container_state(sandbox), logs=(logs.stdout + logs.stderr)[-600:])

    machine = next(iter(layers))
    done = sh("docker", "run", "--rm", "-e", "VM_REQUIRE_KVM=1", machine)
    record(scenario="the windows machine run without /dev/kvm", exit=done.returncode, stderr=done.stderr[-300:])

    for sandbox in sandboxes:
        await sandbox.terminate()
    left = sh("docker", "ps", "-aq", "--filter", "label=agentenv.sandbox").stdout.split()
    record(scenario="teardown", containers_left=len(left))


if __name__ == "__main__":
    asyncio.run(main())
    sys.exit(0)

🤖 Generated with Claude Code

RetriggerView in GreptileConfidence Score: 5/5

The PR appears safe to merge within its stated local-launcher scope.

Summary

Adds Windows boot support to the QEMU launcher and copies a thin disk's base separately so local machine images can share it.

  • A disk marker lets the QEMU launcher boot Windows guests.
  • Machine images reuse the base layer of thin disks.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Pull disk image] --> B{Image layers}
  B -->|One| C[Copy disk folder]
  B -->|Multiple| D[Copy base separately]
  D --> E[Copy disk and guest file]
  C --> F{Guest file}
  E --> F
  F -->|Absent or linux| G[Existing Linux boot setup]
  F -->|windows on amd64| H[Secure boot and fresh TPM]
  F -->|Unsupported guest or Windows arch| I[Exit with error]
Loading

Reviews (1) · Last reviewed commit: "feat(computer): the QEMU launcher boots ..." · Reviewed by Greptile

…hin disk's base

The launcher boots a disk as the guest its folder's guest file names, linux when there's none. A windows guest,
which has no virtio drivers, boots from secure-boot UEFI with Microsoft's keys and a fresh swtpm TPM 2.0 on every
boot, its disk and seed on AHCI, an e1000e NIC, a 1920x1080 display and, under KVM, Hyper-V enlightenments; it boots
on amd64 alone. The launcher image adds swtpm, and the release's smoke test checks for it and the secure-boot
firmware.

A local machine image of a thin disk (more than one layer: a base in disk/base.img below disk/disk.img and its guest
file) copies the base in a step of its own, so every machine on one base shares a single copy of it.

Part of FD-5311 (W1). The windows OS type stays refused until composing a Windows computer lands with it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@earakely-scale
earakely-scale requested a review from a team as a code owner November 7, 2026 03:27
@earakely-scale
earakely-scale merged commit 0c6a433 into main Nov 7, 2026
34 checks passed
@earakely-scale
earakely-scale deleted the edgararakelyan/fd-5311-w1-windows-computers-in-the-framework branch November 7, 2026 06:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant