Repository navigation
feat(computer): a computer env composes the apps it lists into its image - #420
Merged
earakely-scale merged 15 commits intoOct 31, 2026
Merged
earakely-scale merged 15 commits into
earakely-scale merged 15 commits into
Conversation
…at build A computer env takes apps, computer app envs built into its image after the driver in the order listed. In env.toml each is a ref, or a table with install = "build" beside it; "boot" is refused as not supported yet. Gate 1 checks each app against the base and driver, now including a port the driver listens on, and the apps against each other: one listed twice, two sharing a folder, an environment name or a port. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two parts of a composed computer (its driver and apps) can write the same path only from their run scripts and install steps, the steps that write outside their own folders; the later part's copy would hide the earlier's. When two or more parts have such steps, the builder saves the built image and lists what each step's layer holds, found by its command in the image's history; a path two parts wrote with different content fails the pair at its build stage, before the push, naming the path and the parts. Temporary files, logs, caches and the files installers edit in place (account databases, the linker's cache, /etc/environment, shell startup files) are left out. A builder VM reads the layers in a container of the image, on its own Python. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ild compares their files Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ps given as an iterator, match each run's layer exactly - Gate 1 refuses apps that each share an arch with the base and driver but share none all together, and two apps whose supervisord programs would take one name (an app mail with a service sync, and an app mail-sync). gate.programs names an app's programs, and the composer writes them by it. - ComputerEnv.put lists its apps once, so an iterator is checked and stored alike. - The layer reader finds each RUN's layer by the exact command a shell ran, the next one after the last found, so setup.sh no longer matches setup.sh.extra and a command run twice maps to two layers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s layer of the same command never stands in Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…iles two parts wrote differently The layer reader lists each entry's kind: a file, a program (a file any can run), a link, a folder, a deletion or a folder made opaque. A build fails only when a later part certainly undoes what an earlier one installed: it deletes it, points its link elsewhere, makes it another kind, or replaces its program. Any other file two parts wrote differently may be an edit that kept the earlier part's change, such as two apps adding users or a cache rebuilt from every app's entries, so a progress line names it and the parts, and the build goes on. Only the scratch locations of the filesystem hierarchy standard are never compared; the list of files edited in place is gone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ser's source An image's key covered composer.py's source, so any edit to it, a progress line or a check, built every computer env again. The key now covers the base, the Dockerfile, the build files with their executable bits (the composer alone sets them), the lock, the form and the arch, and composer.FORMAT, which replaces builder.FORMAT and is raised only for a change that alters what an image holds some other way, its labels or how it's built. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nment name An app was installed in a folder named for its id's last part, so two apps whose ids end alike (@a/mail, @b/mail) couldn't share a computer, and a bundle prefix made a folder's name differ from the name everything else uses. Its folder under /opt/agentenv/apps, its supervisord programs and its conf file now take its environment name, which a computer already requires to be unique; a driver keeps its id's. Gate 1 names each pair of clashing apps once, for the first thing they'd share. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… of an image it emulates Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…write; skip files already deleted under an opaque folder A path's owner passed to whichever part wrote it last, so a part that first copied identical bytes over an earlier part's program could then delete or replace it unchecked. Every part that wrote a path is now kept, and each later command is checked against the last other part to write it. An opaque folder no longer counts files its earlier part had already deleted, and marks what it hides as deleted. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ter a deletion starts the path afresh - utils/clashes.py holds what the composer refuses or says about two parts' writes: the scratch paths, each layer entry parsed once into an Entry, and compare() over (part, what one command wrote) in build order. The composer's _compare reads the layers and words the result, so _built reads build, compare, push, probe. - A deleted path, an opaque folder's hidden one, or one a loss was recorded on forgets its earlier writers, so a part that recreates and then deletes its own file is no longer blamed for an earlier part's. - _Layer.of is env_id. ComputerEnv.from_toml reads its apps in _listed_apps, which shares from_dict's type check. - Several apps' tests move to test_computer_apps.py, the clash rules as direct tables of compare(); the shared mail app helper and its constants move to _computer.py. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…an image of the same key an earlier run pushed Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…eted folder's contents, leave unread hard links uncertain - A driver's programs and their file are named under driver- (agentenv-driver-<name>.conf, driver-<name>-<service>), as an app's are under app-, so a driver whose id ends app-notes no longer shares an app notes' conf or programs. - Deleting a folder deletes what was under it for the comparison, as an opaque folder does, so a part recreating a file there and deleting its own copy isn't blamed for an earlier part's. - A hard link to a file of an earlier layer, whose content the reader doesn't read, never counts as a replaced program; it's said as a file two parts may have written differently. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…his machine's arch only CI's amd64 runner has no arm64 emulation, so its arm64 pair fails at resolve and compares nothing; the test asserted a compare or a reuse for both arches. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
earakely-scale
deleted the
edgararakelyan/fd-4367-m13-apps-in-composition-build-mode
branch
October 31, 2026 19:13
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
A computer env now composes the computer app envs it lists. In env.toml:
Each app is built into the computer's image after the driver, in the order listed, each under its own folder (
/opt/agentenv/apps/<environment name>) with its own venv, programs and server, as an app deployed alone already is.install = "build"is the default and the only value for now;"boot"is refused as not supported yet.Design: the M1.3 sections of "Computer Envs and Computer App Envs" ("Including an app", "Composition, end to end", "Several apps"). Issue: FD-4367.
How
ComputerEnv.apps.put(apps=[...]),from_tomland the wire form carry them;composed_apps()returns them, so the composer, boot readiness and app settings, which already work over that list, take every app. The wire form writesappsonly when there are some, so existing computer envs keep their stored form and bundles still reuse them.A toml ref with a key beside it.
EntityRef(beside=("install",))lets anappsentry be{ env = "clock", install = "build" }; the resolver acceptsinstallthere and still refuses any other key.Gate 1 over several apps. Each app is checked against the base and the driver as before, now including a port the driver listens on (
check_app, so an app deployed alone gets it too). The apps are checked against each other: no arch they all run, one listed twice, or two sharing an environment name, a folder, a supervisord program name or a port, each pair named once, for the first thing they'd share. Every problem of one app is named with its index,apps[1]: ....Folders by environment name. An app's folder, its supervisord programs and its conf file take its environment name, which a computer already requires to be unique, instead of its id's last part. Apps whose ids end alike (
@a/mail,@b/mail) can share a computer, and a bundle prefix no longer makes a folder's name differ from the app's name. A driver keeps its id's, and its programs and their file are named underdriver-(agentenv-driver-<name>.conf,driver-<name>-<service>) as an app's are underapp-, so a driver and an app never share a conf file or a program, whatever their names.A part undoing what an earlier one installed. An app's files, venv and programs go under its own folder, so the only steps that can write elsewhere are run scripts (as root) and install steps (as the session user). When two or more parts (the driver or an app) have such steps, the builder saves the built image and lists what each step's layer holds, found from the newest layer down as the layer a shell ran exactly that command for. Only certain losses are refused, failing the pair at its build stage, before the push:
Any other file two parts wrote differently may be an edit that kept the earlier part's change (two apps adding users to
/etc/passwd, a cache rebuilt from every app's entries), so it's said on a progress line, naming the parts, and not refused. Scratch locations (/tmp,/run,/var/tmp,/var/cache,/var/log,.cache,__pycache__) are never compared. apt packages install in one shared layer that's no part's, so files the package manager owns never count.utils/clashes.py(compareover each part's id and what one of its commands wrote, in build order). A path is compared with the last other part to write it, whoever wrote it since, until it's deleted or a loss is recorded on it.Builder.written(tag, commands). This machine's Docker saves the image and reads it in-process (utils/layers_in_image.py, standard library only). A builder VM runs the same script in a container of the image, on the image's own Python, so the VM needs none.Image keys cover what an image is made of. The key covered
composer.py's source, so every edit to it, a progress line or a check, built every computer env again. It now covers the base, the Dockerfile, the build files with their executable bits, the lock, the form and the arch, andcomposer.FORMAT(replacingbuilder.FORMAT), raised only for a change that alters an image some other way, its labels or how it's built. Changing the key builds every computer env once more after this lands, as this PR's changes to the composer would have anyway; later composer edits won't.Not in this PR
/svc/<app>/agentenvthrough the driver, and the probe running each app's export: M1.4 (FD-4369). Its design doc, CUA Env M1.4: App Data Planes Through the Driver, has seven choices to review.install = "boot").Testing
Unit: 12,257 passed; the one failure is the macOS-only Hermes pipe test, as on main. Several apps' tests are in a file of their own,
test_computer_apps.py; the clash rules are direct tables ofclashes.compare. New tests:composer.FORMAT.mailin a folder named for its environment name,notes, each with its venv layer and programs; the env stores and reads them back; listing them in the other order makes another key.install; refused:install = "boot", an app listed twice, another key beside the ref.setup.shbesidesetup.sh.extra, one command run twice, a base layer of the same command), in-process and as the script a VM runs; a command missing or out of order is refused.Integration, laptop (local Docker,
localhost:5000):computer_app_testgains a computer envdeskwith two apps,notes(PPA package, download, run script, install step, launched window) andclock(tzdata, a run script, a service on its own port). Throughagent-env run, 1 passed in 6 min 20 s: each app runs on its own settings from its own folder, clock answers on its port with tzdata's zones, the card is the desk's, both apps' readiness passes, only notes launches a window, and both arches compared what the two run scripts wrote before the push. The comparison took about 3.5 s on that image (docker save2.9 s, reading 0.6 s), and the real run-script layers held only each app's own output, so nothing needed leaving out.Modal VM, emulated:
builder_modal_vm_test(slow,remote_sandbox-gated) now also reads what each RUN of its arm64 image wrote, in a container of that image under QEMU in an amd64 Modal VM:jqfrom the apt layer listed as a program, the driver's venv andinstalled.jsonfrom the next. 1 passed in 7 min 6 s.Modal VM: the same bundle through
bundle register --sandbox modal_vm,local, on S3 and ECR. The desk's amd64 built in a Modal VM, ran its comparison in a container of the image there, and was ready in 2 min 23 s; arm64 built natively on the laptop in 56 s.task run --env-sandbox modal_vmthen deployed it on Modal VM, and the check returned the same as on the laptop.🤖 Generated with Claude Code


Confidence Score: 5/5
The PR appears safe to merge; no new blocking issues were found.Summary
Computer environments now build their listed apps into one image, in order.
The earlier findings were rechecked against the current code. Their fixes remain in place. No new actionable issues were found.
Diagram
%%{init: {'theme': 'neutral'}}%% flowchart LR Base[Base computer] --> Driver[Install driver] Driver --> Apps[Install apps in listed order] Apps --> Compare[Compare files written by each part] Compare -->|Certain loss| Fail[Fail build] Compare -->|No certain loss| Push[Push image] Push --> Ready[Check readiness]Reviews (11) · Last reviewed commit: "test(computer): the two-app desk checks ..." · Reviewed by Greptile