Skip to content

Commit 6784ce3

Browse files
feat(computer): keep Windows disks and their bases in the image store (#531)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 120021a commit 6784ce3

8 files changed

Lines changed: 294 additions & 121 deletions

File tree

‎src/agent_env/computers/README.md‎

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -153,19 +153,20 @@ the desktop signs in as at every boot; `setup.ps1` runs once at `agentadmin`'s f
153153
setup that fails is read back over ssh at once (`setup-failed`), then turns off what would get in an agent's way
154154
(updates, the lock screen, notifications, Widgets, Copilot, first-run pages, the page file and the firewall, behind
155155
QEMU's network, which reaches the guest at the ports the launcher forwards alone), installs the pinned Python with Tk
156-
and `agentenv-session`, and switches the sign-in to `user`. Setup takes about 13 minutes. The installed disk is kept
157-
in this machine's Docker as the base's image, `disk/base.img` in a layer of its own under an empty overlay of it, with
158-
the date its 90-day evaluation expires; a compose installs a base afresh once fewer than 7 days are left, and every
159-
input of an install (the ISO's sha256 and the recipe) is part of its key, so a newer ISO installs a new base. Name it
160-
as `windows-11`, or by its id, `@local/agentenv-framework/computers/windows-11`.
156+
and `agentenv-session`, and switches the sign-in to `user`. Setup takes about 13 minutes. The installed disk, trimmed,
157+
becomes the base's image, `disk/base.img` in a layer of its own under an empty overlay of it, with the date its 90-day
158+
evaluation expires and the key to `agentadmin`; it's pushed to the env's image store, as Linux disks
159+
are, under a tag of its recipe, so another machine composing on it pulls it rather than installing Windows, and fetches
160+
no ISO. A compose installs a base afresh once fewer than 7 days of its evaluation are left, from the ISO Microsoft's
161+
link serves then. Name it as `windows-11`, or by its id, `@local/agentenv-framework/computers/windows-11`.
161162

162163
A Windows computer env is composed on the same machine (`env/envs/computer/utils/windows_disk.py`): the base booted on
163164
an overlay kept in a folder of its own, each layer installed over ssh as `agentadmin` by a PowerShell script composing
164165
writes, as its `[install.windows]` says (its files under `C:\agentenv\drivers\<name>`, its downloads checked by
165166
sha256, its venv on the pinned Python, its PowerShell run scripts, elevated, and its services), then shut down and
166-
packed onto the base's image, so every computer composed on a base shares one copy of it. A composed disk stays in this
167-
machine's Docker, as a VM bundle stays on its Mac, under a tag of its key, which a compose of the same inputs reuses
168-
while it's there; gate 3 probes it at full, and one that fails is removed. At the session user's sign-in,
167+
packed onto the base's image, so every computer composed on a base shares one copy of it, and pushed to the env's image
168+
store under its key, pinned, as a Linux disk is; a compose of the same inputs on any machine reuses it, and a deploy
169+
pulls it. Gate 3 probes it at full. At the session user's sign-in,
169170
`agentenv-session`, run windowless by `pythonw`, reads the deploy's environment from the NoCloud seed, which the
170171
launcher attaches as a removable USB drive, since Windows mounts a FAT filesystem with no partition table on removable
171172
media alone, and starts each layer's services in that session, again whenever one exits. A Windows computer boots

‎src/agent_env/env/envs/computer/utils/composer.py‎

Lines changed: 34 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,8 @@
5454
from agent_env.store.ids import derive_id
5555
from agent_env.store.image_store.image_contents import image_config, labels
5656
from agent_env.store.image_store.oci_registry_credentials import names_registry
57-
from agent_env.store.image_store.registry_api import NoSuchImageError, pin_digest, write_index
57+
from agent_env.store.image_store.image_store import ImageStore
58+
from agent_env.store.image_store.registry_api import pin_digest, pin_if_served, write_index
5859
from agent_env.store.local_state import holding_locks
5960
from agent_env.utils.docker.build import DockerBuildError
6061

@@ -176,7 +177,7 @@ def left(form: str, arch: str, record: dict | None) -> bool:
176177
if status == "unverified":
177178
return bool(boot.native_links(provider, arch, form, env.stored_id, env.computer.os.type))
178179
if form == "disk" and env.computer.os.type == "windows":
179-
return not windows_disk.held(record["image"]) or windows_disk.expiring(record.get("expires"))
180+
return windows_disk.expiring(record.get("expires"))
180181
if form != "vm_bundle":
181182
return False
182183
links = boot.native_links(provider, arch, form, env.stored_id)
@@ -354,11 +355,11 @@ def _composed_vm_bundle(env: ComputerEnv, base: ComputerArtifact, driver: Comput
354355

355356
def _composed_windows_disk(env: ComputerEnv, base: ComputerArtifact, driver: ComputerDriverEnv, arch: str,
356357
provider: SandboxProvider, progress: _Progress) -> dict:
357-
"""The Windows disk of ``arch`` composed with this machine's Docker and KVM (``windows_disk``), which keeps it: its
358-
base installed from Microsoft's media first unless held, then the driver installed over ssh as a layer on it and
359-
packed under a tag of its key; or one of that key reused, held here already. Then probed at full (gate 3), and
360-
removed when that fails, so no deploy here boots it. All of it holds a lock of its tag, which keeps two composes of
361-
one key from breaking each other: the other waits, then reuses it."""
358+
"""The Windows disk of ``arch``: one of its key the env's image store holds, reused; else composed with this
359+
machine's Docker and KVM (``windows_disk``), on its base, this machine's, the store's or installed from Microsoft's
360+
media, the driver installed over ssh as a layer on it, packed and pushed to the store under its key, as Linux disks
361+
are. Then probed at full (gate 3). All of it holds a lock of its key, which keeps two composes of one key on this
362+
machine from breaking each other: the other waits, then reuses it."""
362363
pair = f"disk/{arch}"
363364
with (_attempting(env, "disk", arch) as attempt,
364365
tempfile.TemporaryDirectory(prefix="agentenv-compose-") as work):
@@ -387,33 +388,29 @@ def say(line: str) -> None:
387388
progress.say(f"{pair}: {line}")
388389

389390
attempt.stage = "base"
390-
iso = windows_disk.media(say)
391+
registry = windows_disk.Registry(_image_store(env), REPOSITORY)
391392
disk_gb, user = base.forms.disk.min_disk_gb, base.session.user
392-
made = windows_disk.base(_key(iso=windows_disk.sha256(iso), recipe=_digest(windows_disk.RECIPE),
393-
disk_gb=disk_gb, user=user, form="windows-base", format=FORMAT),
394-
iso, disk_gb, user, say)
393+
made = windows_disk.base(_key(recipe=_digest(windows_disk.RECIPE), disk_gb=disk_gb, user=user,
394+
form="windows-base", format=FORMAT),
395+
lambda: windows_disk.media(say), disk_gb, user, say, registry)
395396
script = windows_disk.write(build, [part], settings, {"base": made.image, "layers": [part.folder]})
396-
key = attempt.key = _key(base=windows_disk.image_id(made.image), provision=script, context=_digest(context),
397+
expires = made.expires.isoformat(timespec="seconds")
398+
key = attempt.key = _key(base=made.image, base_expires=expires, provision=script, context=_digest(context),
397399
build_settings=settings, form="disk", arch=arch, format=FORMAT)
398-
image = f"agentenv-windows:{key[7:23]}"
399400
attempt.stage = "build"
400-
with holding_locks([f"windows-disk:{image}"], lambda: progress.at(
401-
pair, "waiting", f"waiting for the compose or probe of {image} under way on this machine")):
402-
if windows_disk.held(image):
403-
progress.at(pair, "reusing", "reusing the disk of the same key, composed on this machine earlier")
404-
result = {"status": "unverified", "key": key, "image": image, "reused": True,
405-
"expires": made.expires.isoformat(timespec="seconds"), "built_at": now()}
401+
with holding_locks([f"windows-disk:{key}"], lambda: progress.at(
402+
pair, "waiting", "waiting for the compose of the same disk under way on this machine")):
403+
if (pinned := registry.served(windows_disk.disk_names(key).tag)) is not None:
404+
progress.at(pair, "reusing", "reusing the disk of the same key, composed earlier")
405+
result = {"status": "unverified", "key": key, "image": pinned, "reused": True, "expires": expires,
406+
"built_at": now()}
406407
else:
407408
progress.at(pair, "composing", "composing on this machine's Docker")
408-
windows_disk.compose(made, build, image, {KEY_LABEL: key, windows_disk.EXPIRES_LABEL:
409-
made.expires.isoformat(timespec="seconds")}, say)
410-
result = {"status": "unverified", "key": key, "image": image, "reused": False,
411-
"builder": "LocalSandboxProvider", "expires": made.expires.isoformat(timespec="seconds"),
412-
"built_at": now()}
413-
probed = _probed(env, driver, "disk", arch, result, provider, progress)
414-
if probed["status"] == "failed":
415-
windows_disk.remove(image)
416-
return probed
409+
pinned = windows_disk.compose(made, build, key,
410+
{KEY_LABEL: key, windows_disk.EXPIRES_LABEL: expires}, say, registry)
411+
result = {"status": "unverified", "key": key, "image": pinned, "reused": False,
412+
"builder": "LocalSandboxProvider", "expires": expires, "built_at": now()}
413+
return _probed(env, driver, "disk", arch, result, provider, progress)
417414
return attempt.result
418415

419416

@@ -672,13 +669,19 @@ def _key(**inputs: Any) -> str:
672669
return "sha256:" + hashlib.sha256(json.dumps(inputs, sort_keys=True).encode()).hexdigest()
673670

674671

675-
def _stored(env: ComputerEnv, key: str) -> tuple[Any, str, str | None]:
672+
def _stored(env: ComputerEnv, key: str) -> tuple[ImageStore, str, str | None]:
676673
"""The env's image store, the ref there of the image of ``key``, and that ref pinned when the store serves one
677674
already, for any env that built it."""
675+
store = _image_store(env)
676+
ref = store.image_ref(REPOSITORY, key.removeprefix("sha256:"))
677+
return store, ref, pin_if_served(ref, store.auth(ref))
678+
679+
680+
def _image_store(env: ComputerEnv) -> ImageStore:
681+
"""The env's image store, with the repository its computers' images are kept in."""
678682
store = get_config().get_image_store_for(env.stored_id)
679683
store.ensure_repository(REPOSITORY)
680-
ref = store.image_ref(REPOSITORY, key.removeprefix("sha256:"))
681-
return store, ref, _served(ref, store)
684+
return store
682685

683686

684687
def _base_disk(base: ComputerArtifact, arch: str) -> tuple[str | None, str]:
@@ -936,14 +939,6 @@ def _wants(layers: list[_Layer], protocol: bool) -> list[packages.Want]:
936939
return wants
937940

938941

939-
def _served(ref: str, store) -> str | None:
940-
"""``ref`` pinned to the digest the image store serves for it, or None when it serves none."""
941-
try:
942-
return pin_digest(ref, store.auth(ref))
943-
except NoSuchImageError:
944-
return None
945-
946-
947942
def _computer(env: ComputerEnv, base: ComputerArtifact, built: dict[str, str], disks: dict[str, str],
948943
bundles: dict[str, str]) -> dict | None:
949944
"""Write the env's computer over the images ``built``, by arch, joined into one index in the env's image store, signed

0 commit comments

Comments
 (0)