Skip to content

feat(bundle): images a bundle carries as docker save archives, loaded rather than built or pulled - #401

Closed
earakely-scale wants to merge 4 commits into
mainfrom
feat/bundle-image-archives
Closed

earakely-scale wants to merge 4 commits into
mainfrom
feat/bundle-image-archives

Conversation

@earakely-scale

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

Copy link
Copy Markdown
Collaborator

What

A bundle can carry an image as a docker save archive in its folder, which a run loads instead of building or pulling: image = { archive = "image.tar.gz", dockerfile = "Dockerfile", platform = "linux/amd64" } (the last two optional), or archive = in an image folder's artifact.toml. It's the first step toward a bundle that runs away from the store it came from.

  • Storage: put_archive stores the archive as a tar.gz naming no image (RepoTags null, no index.json name annotations) under local/<slug>-<hash>:<archive sha256>. Writing it needs no Docker. When the image names a dockerfile, the folder is kept beside the archive as its build context.
  • Folder listings: no listing of a folder holds an archive the folder names, neither a build context (a sibling image's included) nor a folder-written entity's files.
  • Local Docker now loads tar.gz images. Each is untagged on its way to docker load, so no name it embeds replaces an image the machine holds, then tagged by the ID Docker reports.
  • VMs tag a loaded image with its stored name when that name isn't there after the load.
  • A sandbox that only pulls (modal containers) refuses it in the dry run, as it refuses any tar.gz under a bare tag.

Testing

  • Unit: tests cover resolve, materialize and ledger reuse, untagging (legacy and OCI layouts), host loads, the VM load script and the dry run. tst/unit/bundle, artifact, providers and utils pass (3346).
  • Real Docker 29 (the containerd image store, as on Modal VMs): the local provider and the VM load shell each load the archive's image under its stored name, and an image the machine already held under the archive's embedded tag keeps that tag.
  • Not yet run on a Modal VM.

🤖 Generated with Claude Code

RetriggerView in GreptileConfidence Score: 5/5

The PR appears safe to merge; the previous file-exclusion bug is fixed.

Summary

Adds images carried inside bundles as Docker archives, with storage, reuse checks, and loading on local Docker and VMs.

  • Bundles can carry Docker images as saved archives.
  • Local Docker and VM sandboxes load bundle image archives.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  A[Bundle archive] --> B[Remove embedded image names]
  B --> C[Store compressed archive]
  C --> D[Local Docker or VM loads image]
  D --> E[Assign stored image name]
  A --> F[Exclude archive from build context]
Loading

Reviews (3) · Last reviewed commit: "fix(bundle): an entry's archives are onl..." · Reviewed by Greptile

earakely-scale and others added 2 commits October 29, 2026 17:44
… rather than built or pulled

An env, agent or image folder can name an image saved in its folder:
`image = { archive = "image.tar", dockerfile = "Dockerfile", platform = "linux/amd64" }`, or
`archive =` in an image folder's artifact.toml. A run stores it as its tar.gz under
`local/<slug>-<hash>:<archive digest>`, never builds it, and keeps the Dockerfile it names
as its build context beside it. Local Docker loads it (`host_images.load`), and a VM tags
what it loaded with that name when the archive saved it under another one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comment thread src/agent_env/bundle/authoring.py Resolved
…chives out of their folder's listings

docker load applies every name an archive embeds, so a bundle's archive could replace an image
the loading machine holds under that name. put_archive now stores the archive naming no image
(manifest.json RepoTags null, no index.json name annotations, no legacy repositories file), and
host_images.load untags every tar.gz on its way to docker load, then tags the image by the
immutable ID docker load reports, so concurrent loads can't race on a shared name either.

entry_files leaves out every archive an entry's toml names, normalized, so no build context of
the folder (a sibling built image's included) and no folder-written file holds one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comment thread src/agent_env/bundle/authoring.py Resolved
entry_archives read every nested `archive` string as an image archive, so a key like an agent's
`default_env_vars = { archive = "assets.zip" }` dropped that file from its build context. It now
reads `archive` only at the docker_image keys the entry's type declares, as the resolver does,
and at an image folder's top level. entity_class moves to authoring.py so both can use it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@earakely-scale

Copy link
Copy Markdown
Collaborator Author

Closing: the bundle export now names each image by its delivery ECR digest, as Harbor does, so bundles don't carry archives. The branch is kept.

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