Skip to content

feat(computer): add the computer artifact and the ubuntu-24.04 base - #354

Merged
earakely-scale merged 22 commits into
mainfrom
edgararakelyan/fd-4347-m01-computer-artifact-type-and-an-ubuntu-2404-base-container
Oct 28, 2026
Merged

earakely-scale merged 22 commits into
mainfrom
edgararakelyan/fd-4347-m01-computer-artifact-type-and-an-ubuntu-2404-base-container

Conversation

@earakely-scale

@earakely-scale earakely-scale commented Oct 27, 2026 •

Copy link
Copy Markdown
Collaborator

M0.1 of the CUA env epic (FD-4345): the computer artifact and the ubuntu-24.04 base, FD-4347. It also folds in M0.10, [artifacts] aliases (FD-4381), and closes M4.6, publishing the base from the release workflow (FD-4385); M4.6's disk form moves to M4.1 (FD-4363). It follows the reviewed design, CUA Env M0.1: Computer Artifact and ubuntu-24.04 Base, with all 16 choices confirmed.

What's in it

  • Artifact metadata. Every artifact gains a metadata dict outside its versioned fields. merge_metadata changes it by compare-and-swap on metadata.updated_at, as envs and agents do. The golden wire files gain the empty metadata each artifact now writes. An environment universe is the exception: its stored metadata key already holds its legacy metadata file ids, so it reads that key as those ids only, keeps no artifact metadata, and refuses merge_metadata.

  • The computer artifact type (ComputerArtifact, computer). It declares:

    • arch;
    • os, only linux for now: distribution, version (or rolling), display;
    • session.user;
    • config.<name> settings, each with a default, a phase and a scope; build-phase settings are task-scoped;
    • forms.container, whose image is a docker_image artifact pinned by id and version, with init = "supervisord" and input_channel = "env".

    Other OS types and forms are refused as not yet supported.

  • Inspection at put. Each form's image is read from its registry, with no Docker and no full pull:

    • the index has a linux/<arch> manifest for every declared architecture;
    • the os-release of the lowest layer holding it names the declared distribution and version.

    A mismatch refuses the put, naming the form and architecture. An image no registry serves yet, such as a build context alone, is recorded uninspected. What was read goes in metadata.validated_images. The registry API gains read_manifest and read_blob, on one request path with pin_digest. store/image_store/image_contents.py reads an image's platform manifests and its os-release from them, and each result is an ImageCheck.

  • In a bundle, forms.container.image takes what an image key takes:

    • { dockerfile = "<path>" }, built from the folder;
    • { ref = "<image>" }, pinned from a registry;
    • an image artifact's id, or { artifact = "<id>", version = <n> }.

    The resolver now writes images for image keys inside a table.

  • [artifacts] aliases (M0.10). aliases.toml gains an [artifacts] table, read and checked like [agents] and [envs].

    • A task's reference to an installed bundle's artifact alias is pointed at that artifact and pulled in, unless the store has one of that id.
    • An entry's reference to one is refused, as env aliases are.
    • The run says what each alias names, and plugin list shows them.
    • An alias may now hold ., so ubuntu-24.04 is one.
  • The computers bundle and the ubuntu-24.04 base:

    • Ubuntu 24.04, pinned by digest, for amd64 and arm64.
    • XFCE on Xvfb, with D-Bus and AT-SPI.
    • Python 3 with venv and Tk, for drivers and the probe's Fixture Desk.
    • tini starts supervisord, and every program runs as user (uid 1000), so nothing runs as root.
    • resolution and timezone arrive as AGENTENV_SETTING_<NAME> and reach every program. A value it can't use stops it at start with exit 2.
    • A driver or an app adds its services as programs in /etc/supervisor/conf.d; one that needs X runs as start-desktop with-x <command>, as the XFCE session does, which waits for X.
    • Its aliases.toml names it ubuntu-24.04.
    • It joins published_images, so a release builds it on both platforms, checks its desktop comes up, and publishes ghcr.io/scaleapi/agentenv-ubuntu-24.04:src-<hex>. A released install then writes the base over that published image, pulled by its digest, instead of building it. Dependabot keeps its pinned base current.
  • The scaffold moves onto the base (row 16). tst/integration/computer/scaffold's desktop was a hand-written image with a start script. It is now FROM the base, with the reference driver and the Fixture Desk added as supervisord programs. The tests build the base once (a session fixture in tst/integration/computer/conftest.py). The probe, recording, recording-chaos and agent tests all run on it, so M0.7 only deletes the scaffold bundle.

Where this differs from the design

  1. M0.10 is folded in. A shipped bundle with nothing to run has to alias what it holds, or the checks a run makes refuse it, so the [artifacts] table ships with the base it names. FD-4381 closes with this PR.
  2. os-release is read from the bottom up: the lowest layer holding it, normally the OS's own, so only that layer is fetched (about 30 MB for Ubuntu). A later layer that rewrites it isn't seen.
  3. Artifact.get reads no alias. The planner rewrites every alias a task names to its target id, and nothing else looks one up yet.
  4. The base's Dockerfile sits at the artifact folder's root, not in container/, so the published image's build context is the same folder the bundle builds from, and a deploy finds it by its sources.
  5. Settings are declared on the base only, not on forms. No container-form setting exists yet; disk forms bring firmware.

Testing

The unit, integration and chaos runs and the probe ran on an amd64 EC2 box (c6i.4xlarge, Ubuntu 24.04, Docker 29) and on the laptop (arm64, Docker Desktop), on the branch before its review fixes. Every later change has its own unit test and touches neither the base image nor the probe path:

  • tag-only images are pinned before inspection, also checked against the local registry;
  • a released install writes the base over its published image;
  • environment universes keep their legacy metadata key;
  • the maintainability pass, in which dropping the CLI is the only change in behaviour;
  • a file error while inspecting reports a failed form;
  • rebased on main, whose new next-env-event example makes seven shipped bundles.

The unit tier and tst/integration/computer were rerun on the laptop after the rebase.

Unit: 11,094 passed locally on Python 3.14 after the rebase. The one failure was parse_test.py::test_deep_nesting[json]: it fails under xdist on 3.14 on main too, and passes alone and in CI on 3.12. Coverage, branches included: computer.py 98%, the CLI 100%, registry_api.py 94%.

Integration (tst/integration/computer, 15 tests):

  • EC2: 14 passed; 1 skipped, the agent test, which needs a model endpoint.
  • Laptop: the same, plus the agent test, which passed with a model endpoint (Claude Code driving the Fixture Desk under a recording).
  • The new base tests:
    • the desktop runs as user (uid 1000) with the resolution and time zone a deploy passes;
    • it comes back however often X dies;
    • big, 40000x800, 1280x0 and Mars/Olympus each stop it with exit 2;
    • pushed to the local registry, it registers as a computer whose image put reads back.

The release path, built as publish-pypi.yml builds it (docker build --platform … --provenance=false, no cache):

amd64 (EC2) arm64
Build 61.7 s 37.1 s native on the laptop; 256 s under QEMU on EC2 (the release uses an arm runner)
infra_build.py smoke passed, 2.5 s passed, 2.4 s on the laptop; 13.7 s under QEMU
On disk (du) 548 MB 578 MB

On EC2 both builds were pushed as -amd64 and -arm64 and joined with docker buildx imagetools create into one index holding linux/amd64 and linux/arm64.

put on EC2, run through an agent-env artifact computer CLI this PR has since dropped (bundles write computers):

Folder Result
the shipped base, image = { ref = "<the joined index>" } written; amd64 and arm64 passed, os-release ubuntu / 24.04, in 2.4 s
stock public.ecr.aws/docker/library/ubuntu:24.04 written; both passed, 3.2 s
an amd64-only push refused: forms.container (arm64): it has no linux/arm64 image, only linux/amd64
the joined index declared as 22.04 refused for both: os-release has VERSION_ID='24.04', not '22.04'
the folder as shipped, { dockerfile = "Dockerfile" } written; both uninspected (only its build context is stored, so its first boot checks it)

Chaos: 22/22 on both. The image was the base plus the reference driver and the Fixture Desk. After each of these, every program was RUNNING again and the probe passed at basic (25 checks):

  • Xvfb, xfce4-session, D-Bus, xfwm4 and the driver, each killed with SIGKILL;
  • Xvfb killed ten times in a row, each as soon as it was back;
  • docker restart, back in 3.8 s on EC2 and 3.9 s on the laptop;
  • resolutions 640x480, 1920x1080 and 3840x2160, each checked with xdpyinfo.
EC2 amd64 Laptop arm64
Boot to ready (×3) 2.1 s 1.7 s
Idle, base alone 0.01% CPU, 110 MB 0.03% CPU, 108 MB
Idle, with driver and desk 1.15% CPU, 178 MB 0.55% CPU, 176 MB

The probe at full, three runs in a row, on each: 51 passed, 0 failed, 1 not applicable, recording included. Afterwards all 22 processes ran as user, and each of the five supervisord programs was RUNNING.

Found by chaos and the e2e, and fixed here:

  • Ten quick Xvfb deaths left the XFCE session FATAL: it exits with X inside supervisord's start seconds. It now counts as started at once (startsecs=0), and with-x waits for X; the new integration test fails on the old config.
  • On the base, xfwm4 adopted the Fixture Desk after it had written its layout, so its boxes were off by the window decorations (+5+29) and one grid row of clicks missed. The protocol's fixture now asks for fullscreen, which keeps it where it put itself. Nothing changes without a window manager.

Maintainability pass (reviewed in the PR):

  • The planner's alias tables, uses and stored versions are keyed by EntityKind, and Plan.alias_uses replaces aliases, env_aliases and artifact_aliases.
  • pin_digest and the new reads share one request path (_Repository, now with HEAD).
  • The image reading is in image_contents, and inspection results are ImageChecks.
  • Tests serve the fake registry with Registry().serve(monkeypatch), and the integration tests share docker().
  • The CLI and put_folder are gone, and with them a second reader of the image-key syntax the bundle resolver owns.

Greptile: three findings, each fixed and answered:

  • put(inspect=False) didn't check the image artifact;
  • a numeric resolution outside what X can address went unrefused;
  • an image recorded by its tag alone failed inspection. It is now pinned to the digest its registry serves; checked against the local registry, a tag-only record of the base inspects as passed.

🤖 Generated with Claude Code

RetriggerView in GreptileConfidence Score: 5/5

The reviewed changes appear safe to merge.

Summary

Adds the computer artifact, the Ubuntu 24.04 desktop base, artifact aliases, and release-image publishing.

  • Computer artifacts describe a desktop and check its container image.
  • The Ubuntu 24.04 desktop base runs computer workloads on two platforms.
  • Bundle aliases can now name artifacts, including ubuntu-24.04.
  • Artifact versions gain mutable metadata that stays separate from their main fields.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Computer put] --> B[Load referenced image artifact]
    B --> C[Check Linux architectures]
    C --> D[Read OS release]
    D --> E{Result}
    E -->|Matches| F[Record passed]
    E -->|Unsupported compression| G[Record uninspected]
    E -->|Mismatch or read failure| H[Refuse put]
    F --> I[Store computer]
    G --> I
Loading

Reviews (9) · Last reviewed commit: "fix(computer): write an image whose zstd..." · Reviewed by Greptile

Comment thread src/agent_env/artifact/artifacts/computer.py Outdated
Comment thread src/agent_env/computers/artifacts/ubuntu-24.04/start-desktop Outdated
@earakely-scale
earakely-scale force-pushed the edgararakelyan/fd-4347-m01-computer-artifact-type-and-an-ubuntu-2404-base-container branch from d81c9cb to ac3a200 Compare October 27, 2026 19:26
Comment thread src/agent_env/artifact/artifacts/computer.py Outdated
Comment thread src/agent_env/artifact/artifacts/computer.py
earakely-scale and others added 21 commits October 27, 2026 15:26
…swap

What is learned about an artifact version after it's written, such as the
images a computer artifact inspects at put and, later, what booting them
found, has nowhere to go. Artifacts gain a metadata dict outside their
versioned fields, which merge_metadata changes by compare-and-swap on
metadata.updated_at, as envs and agents do. The golden wire files gain the
empty metadata each artifact now writes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A computer artifact is the base a computer env composes a driver and apps
onto: its architectures, its OS (linux), its session user, its settings, and
its container form, whose image is a docker_image artifact pinned by id and
version. Other OS types and forms are refused as not yet supported.

put reads each form's image from its registry before writing: the index has
a linux manifest for every architecture, and the os-release of the lowest
layer holding it names the declared distribution and version. A mismatch
refuses the put, naming its form and architecture; an image no registry
serves yet is recorded uninspected. What was read goes in
metadata.validated_images.

In a bundle, forms.container.image takes what an image key takes, a
Dockerfile in the folder, a registry ref or an image artifact, so the
resolver now writes images for image keys inside a table. The registry API
gains manifest and blob reads, with the token handshake pin_digest uses.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
aliases.toml gains an [artifacts] table, read and checked like [agents] and
[envs]: each alias names an artifact the bundle has by its folder. A task's
reference to an installed bundle's artifact alias is pointed at that
artifact and pulled in, unless the store has one of the alias's id, and an
entry's reference to one is refused, as env aliases are. The run says what
each artifact alias names, and plugin list shows them.

An alias may now hold '.', so a versioned name like ubuntu-24.04 is one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The computers bundle ships one base, ubuntu-24.04: Ubuntu 24.04 for amd64 and
arm64 with XFCE on Xvfb, D-Bus and AT-SPI, and Python 3 with venv and Tk for
drivers and the probe's Fixture Desk. tini starts supervisord, which runs
every program as the session user, user (uid 1000); nothing runs as root.
The resolution and time zone settings arrive as AGENTENV_SETTING_<NAME> and
reach every program, and a value it can't use stops it at start. A driver or
an app adds its services as programs in /etc/supervisor/conf.d.

Its aliases.toml names it ubuntu-24.04. Its image joins the published images,
so a release builds it on both platforms, checks its desktop comes up, and
publishes ghcr.io/scaleapi/agentenv-ubuntu-24.04:src-<hex>; Dependabot keeps
its pinned Ubuntu base current.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ocal registry

Built from its folder, the base's desktop runs as user (uid 1000) with every
program under supervisord, at the resolution and in the time zone the deploy
passes, with a window manager and AT-SPI up; a setting it can't use stops it
with exit 2. Pushed to the local registry, it registers as a computer whose
image put reads back as Ubuntu 24.04.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
agent-env artifact computer put --id <id> <folder> writes the computer a
folder's artifact.toml declares, outside a bundle: its form's image key
written as a bundle writes one, from a Dockerfile in the folder as its build
context alone, a registry ref pinned now, or the image artifact it names.
Its images are read from their registries first, and --no-inspect skips
that, leaving each form uninspected. get prints a computer as JSON, with
what put read.

The fake OCI registry the computer tests share moves to tst/util.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The XFCE session exits whenever X does, within supervisord's three start
seconds, so ten quick Xvfb deaths left the desktop program FATAL. It now
counts as started at once; start-desktop already waits for X.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ndow manager

The desk sizes itself to the screen at its origin and writes its widgets'
screen boxes once it starts. A window manager that adopts it after that
moves it under its decorations and panel, so the boxes it wrote were off by
the move. Asking for fullscreen keeps it where it put itself.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…4 base

The scaffold's desktop was a hand-written image with a start script. It's
now the base with the reference driver and the Fixture Desk added as
supervisord programs, built from a base the tests build once. The base's
start-desktop gains with-x, which runs any program once X answers, as it
already ran the XFCE session.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ifact, inspected or not

put read the form's image artifact only to inspect it, so put(inspect=False)
wrote a computer naming a missing artifact, a missing version or a file. It
now loads the image on every put and refuses one the store doesn't hold.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t the base's start

Any digits passed the check, so a width Xvfb or X can't use left a container
whose desktop never came up, not the exit 2 a setting it can't use gives.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…its registry serves

DockerImageArtifact.put records the image store's tag with no digest, so a
single-platform image's manifest digest came out as the whole tag and its
layers couldn't be read. Inspection now pins such a ref first, as put_ref
does, and records what it read by that digest.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ublished image

The planner already registers a shipped image published for a bundle image's
sources, the base's among them; this shows it does for ubuntu-24.04, with
nothing built and both architectures read back from the registry.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… of artifact metadata

A stored universe's metadata key holds its legacy metadata file ids, the key
artifact metadata now uses, so reading one filled its artifact metadata with
them, and merging into it would have left a document no version can load.
A universe reads the key as its legacy ids only and keeps no artifact
metadata.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… they name

The planner kept a table, uses and stored versions apiece for agent, env and
artifact aliases, and the plan three fields; one mapping by kind holds them
now, so a kind of alias adds one entry rather than a set of fields.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Bundles write computers, and the CLI's folder put kept a second reader of
the image key the bundle resolver owns. It can come back as a one-entry
bundle run if a caller needs it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d type what put records

The platform manifests and os-release reading move out of the computer
artifact into image_store/image_contents, which a disk form's inspection can
reuse; the artifact keeps the check of what it declares against what was
read, each result an ImageCheck. put_folder goes with the CLI. Tests serve
their fake registry with Registry().serve(monkeypatch) in place of patching
three functions apiece.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
pin_digest's check and the manifest and blob reads each chose the scheme,
traded Basic credentials for a Bearer token and mapped a registry's answers
to errors. The check now runs on the repository reader they share, with HEAD
added, so a refusal from any read gives the same advice.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s base arg, read an unreadable layer

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…a failed check

Moving the os-release reading out of the computer artifact left the layer's
download and removal outside the error the caller turns into a failed form,
so a full disk printed a traceback. Both reads now raise ValueError for a
file error, naming the blob.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… ships

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@earakely-scale
earakely-scale force-pushed the edgararakelyan/fd-4347-m01-computer-artifact-type-and-an-ubuntu-2404-base-container branch from 77a90dc to 3e2870d Compare October 27, 2026 22:31
Comment thread src/agent_env/store/image_store/image_contents.py
… uninspected, not refused

tarfile reads zstd from Python 3.14; before it, a layer of that media type is
recorded as uninspected with the reason, as a context-only image is.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@earakely-scale
earakely-scale merged commit ca6d8f9 into main Oct 28, 2026
13 of 14 checks passed
@earakely-scale
earakely-scale deleted the edgararakelyan/fd-4347-m01-computer-artifact-type-and-an-ubuntu-2404-base-container branch October 28, 2026 00:44
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