Repository navigation
feat(bundle): build an agent's image from its folder's Dockerfile - #10
Merged
Merged
Conversation
The six put commands ran nine copies of the same `docker build` (a2a-agent, gateway, mcp-server, service-db x3, website x2, website-browser). They now call agent_env.utils.docker_build.build_image(), which library code can import too: stdlib only, no click, no providers. - build_image(dockerfile, context, tag, *, platform, build_args=None) builds only; each caller still puts the image. platform has no default, so a caller chooses; None or "" builds host-native. - A failed build raises DockerBuildError with docker's whole output (stderr merged into stdout, bytes that don't decode replaced). The CLI renders it as one user-facing error and exits 1, replacing the five per-site prefixes and service-db's "Aborted!". A missing docker binary is the same one line instead of a traceback. - DEFAULT_BUILD_PLATFORM moves to the new module; docker_build_platform_args goes. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…age captures output - The put test stopped each command at its first build, so service-db's db-web and db-mcp builds and website's frontend build were never checked. A test with the builds succeeding now asserts all of them; website's backend and frontend get separate folders so a swapped context shows. - A fake docker on PATH checks that both streams, and a byte that isn't UTF-8, reach DockerBuildError. - Drop two mock names the lift left unused. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
An agent folder with a Dockerfile and no `image` was parsed and planned as a built image, then refused by materialize. It is now built with `build_image()` on this machine, for its own platform, and written as the @Local docker_image `<agent id>__agent_image` (pushed to the local registry, saved as a tarball in the local object store, its build context kept for install/v1), just before the agent written over it. - The ledger tracks built images: an image is made from every file of its folder, the entry's toml too, since a Dockerfile can copy it. An unchanged folder reuses the image; a changed file rebuilds it and rewrites the agent. What the build fetches (base image, packages) isn't an input. - Without docker on PATH, a built image is refused before any write. A failed build is a BundleError naming the agent's folder, with the end of docker's output. - `materialize(on_build=...)` is called before each build; `agent-env run` prints it and labels image writes apart from their agent. - `COPY .` (or `ADD .`) keeps the whole build context in an image's saved context; the COPY-source parser dropped `.`, which left install/v1 with only the Dockerfile. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
earakely-scale
requested review from
a team and
polakamtejas
as code owners
September 30, 2026 17:13
Re-applies only this branch's own change on top of main; the build_image() lift it was stacked on landed separately. The README hunks are dropped: the bundle docs moved to the docs site. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… hashes The build ran over the agent's folder while the ledger hashed the folder less what the OS or Python leaves behind (`__pycache__`, `.DS_Store`, ...), and docker copied links as links. A change there could reuse an image built from other files. The image is now built from a temporary copy of `build_context_files` (the folder's files and its toml) and that copy is saved as its build context, so the hash, the build and the saved context hold the same files. The folder's `.dockerignore` still applies to the build. This also drops the change to the shared COPY-source parser, so `a2a-agent put`, `env mcp-server put` and GitHub builds behave as on main. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The docker check refused every built image when docker wasn't on PATH, including one the ledger would reuse, and it ran in the dry run too. It now runs after the ledger is opened and refuses only the images it would build, still before anything is written, in the run and the dry run alike. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A RuntimeError from the image store (the local registry failing to start), docker push or docker save reached the user as a traceback. It is now a problem naming the agent's folder, like a failed build. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
DockerImageArtifact.put uploaded its tarball and build context to write-once keys named by version, then wrote the document. A put that stopped in between, say on Ctrl-C during a long upload, left those keys behind, and every later put of the id picked the same version and failed on them. The objects now go under `ArtifactStore.attempt_prefix`, as file artifacts' already do. Only new objects' keys change; readers follow the URLs in the document. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…it with derive_id The image id a bundle builds is now spelled by `derive_id`, as every other derived id is (the same bytes as before). New tests: the dry run lists an image it would build, predicts a rebuild and the agent written over it, and builds nothing; a run says before it builds and names the image apart from its agent. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ock is held Another run writing the same ids may be building the image; once it releases the lock, the ledger can reuse what it built, so a run without docker waits for it instead of refusing. The check still comes before any write. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…e, for real A slow-tier test runs a bundle of four agents built by docker and deployed on the local sandbox, each with a Dockerfile of another shape: - at the folder's root, with no agent.toml; - `COPY .`, with a .dockerignore, a link and what the OS or Python leaves behind; - in a subfolder named by agent.toml, whose env vars reach the container; - multi-stage under another name. Each agent replies with what its build put in it, and its task checks the reply. A rerun builds nothing, an edited file rebuilds only its own agent, and a Dockerfile that fails is one problem with docker's output. HOME stays put: docker's credential helper can hang a build when it moves. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The test's agents move their trajectories through this process's local grant server, which serves a certificate from the test's state root. A later test in the same process gave its agent another root's CA, so its upload failed with transfer_unavailable. The test now closes the server, and the next one to issue a grant starts it afresh. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The locals in DockerImageArtifact.put and put_from_github held object URLs from whichever object store is configured (file://, s3://, gs://), but were still named tar_gz_s3_url and build_context_s3_url. They now match the fields they fill, tar_gz_object_url and build_context_object_url. put_tar's keyword arguments and the stored documents' keys keep their names. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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.
An agent folder with a
Dockerfileand noimagewas already parsed and planned as a built image, but materialize refused to write it. A bundle run now builds it, so a bundle can carry an agent's source instead of naming an image someone registered first:What it does
build_image()over it on this machine for its own platform, and writes the result as the@localdocker_image<agent id>__agent_image. The copy is what the ledger hashes (build_context_files): every file of the folder,agent.tomlincluded, less what the OS or Python leaves behind (__pycache__,.DS_Store, ...). Links inside the bundle are copied as their targets. The folder's.dockerignorestill applies to the build.FROMtag names, isn't an input.agent-env run --dry-runlists the image apart from its agent, asagents/solver (Dockerfile image): v1 (new), and predicts a rebuild and the agent rewritten over it. It builds nothing.materialize(on_build=...)is called before each build.agent-env runprintsagents/solver (Dockerfile image): building with docker, which can take minutesand labels the image's write apart from its agent's.DockerImageArtifact.putnow writes its tarball and build context under an attempt prefix, as file artifacts already do. A put that stopped before writing its document, say on Ctrl-C during a long upload, no longer blocks every later put of that id. Only new objects' keys change; readers follow the URLs in the document.Limits
registry:2from Docker Hub.build_context_files.Tests
materialize:
agent.tomledit rebuilds the image and rewrites the agent;run: the progress lines, and the CLI dry run's line for a built image.
DockerImageArtifact.put: a put that stops before its document doesn't block the next.ledger: what a built image is made from.
Unit tier: 5,750 passed.
Integration (slow tier, real docker builds, agents deployed on the local sandbox):
tst/integration/cli/run_bundle_built_agents_test.py. One bundle holds four agents, each built from a different shape of Dockerfile, and each replies with what its build put in it:plainagent.tomlwholeCOPY ., with a.dockerignore, a link,__pycache__and.DS_Storenotes.md: the ignored file and the leftovers stayed out, and the link was copied as its targetsubdirdocker/Dockerfile, named byagent.tomlagent.toml's env vars setstagedDockerfile.agent__pycache__or.DS_Storedoesn't.End to end (local sandbox, default local stores)
I ran a bundle whose
agents/echo/folder holds the integration suites' echo A2A agent and its Dockerfile. Its task deploys the agent, prompts it once and checks the reply withresponse_contains. Command:agent-env run <bundle> --sandbox local.v1 (new), nothing built or writtenv1 (new)agent.pyand adding__pycache__/and.DS_Storefiles changed: agent.py)v1 → v2)__pycache__/The saved build context of v2 holds
Dockerfile,agent.pyandagent.toml, and its tarball sits under its attempt prefix (.../2-<id>/...).🤖 Generated with Claude Code
The changes since the previous review appear safe to merge; no new issue was found.
Summary
Bundle runs can now build an agent’s Docker image from the files in its folder and write the agent to use it. The image is tracked for reuse, and interrupted image uploads get their own object keys.
Diagram
%%{init: {'theme': 'neutral'}}%% flowchart LR A[Agent folder] --> B[Stage build files] B --> C[Build image] C --> D[Push and save image] D --> E[Write image artifact] E --> F[Write agent]Reviews (6) · Last reviewed commit: "refactor(artifact): name a docker image ..."