Repository navigation
feat(computer): a computer env composes the apps it lists into its image - #420
earakely-scale wants to merge 12 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>
| entries[path] = entry | ||
| writers.setdefault(path, []) | ||
| if of not in writers[path]: | ||
| writers[path].append(of) |
There was a problem hiding this comment.
Deleted files block valid builds
writers[path] keeps earlier apps even after their file is deleted. If app A creates and deletes a file, app B can recreate it successfully. But when B later deletes or replaces its own program, _other still returns A. _lost then reports that B broke A's file and rejects a valid build.
Clear the old writers when a path is deleted, including when an opaque folder hides it, while preserving checks against losses already recorded.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agent_env/env/envs/computer/utils/composer.py
Line: 515-518
Comment:
**Deleted files block valid builds**
`writers[path]` keeps earlier apps even after their file is deleted. If app A creates and deletes a file, app B can recreate it successfully. But when B later deletes or replaces its own program, `_other` still returns A. `_lost` then reports that B broke A's file and rejects a valid build.
Clear the old writers when a path is deleted, including when an opaque folder hides it, while preserving checks against losses already recorded.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.There was a problem hiding this comment.
Fixed in 4a100a3: a path's earlier writers are forgotten once it's deleted (an opaque folder's hidden paths too) or once a loss is recorded on it, so the next write starts it afresh. The rules moved to utils/clashes.py in the same commit. Test: test_what_isnt_certainly_lost_isnt_a_loss_and_only_a_file_two_parts_wrote_differently_is_said [recreated]: a creates and deletes /t, b recreates and deletes its own /t, no loss; [recreated-after-opaque] the same through an opaque folder. [after-a-loss] in the loss table checks a third part is then compared with the part whose write was recorded.
…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>
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.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,254 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