feat(smolmachines): build agent image from repo Dockerfile (PRD 0023 chunk 4c)
test / unit (pull_request) Successful in 21s
test / unit (push) Successful in 21s
test / integration (push) Successful in 42s
test / integration (pull_request) Successful in 41s

Replaces the alpine:latest placeholder with a real claude-bottle
agent image, converted into a .smolmachine artifact via an
ephemeral local OCI registry.

Why the registry hop: smolvm pack create only accepts OCI registry
refs. Empirically it rejects docker-daemon://, oci-layout://,
docker-archive: tarballs, and every other transport tested — the
crane backend treats anything with a scheme prefix as a registry
hostname. To convert a locally-built docker image into a
.smolmachine we have to push it somewhere smolvm can pull from.
Smallest path: bring up registry:2.8.3 bound to 127.0.0.1:<random>,
docker tag + docker push into it, smolvm pack create --image
localhost:<port>/claude-bottle:<id>, tear down the registry.

The .smolmachine is cached under
~/.cache/claude-bottle/smolmachines/ keyed by the docker image ID
(first 16 hex chars of the sha256), so a Dockerfile change picks
up a new image ID and invalidates the cache. Unchanged rebuilds
skip the whole build → registry → pack pipeline.

This puts `docker build` in smolmachines prepare (the docker
backend defers it to launch). Necessary because pack_create needs
the image ID to derive the cache key, and prepare is the only
hook ahead of launch that runs once per slug.

Adds:
- claude_bottle/backend/docker/util.py: image_id / tag / push
  helpers (thin docker CLI wrappers).
- claude_bottle/backend/smolmachines/local_registry.py:
  ephemeral_registry() context manager; pins registry:2.8.3 by
  digest, binds 127.0.0.1::5000 (loopback-only), force-removes on
  exit.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit was merged in pull request #71.
This commit is contained in:
2026-05-27 13:51:02 -04:00
parent 4ac61a563b
commit 1fa17d1822
7 changed files with 567 additions and 28 deletions
+13 -4
View File
@@ -393,10 +393,19 @@ Three changes vs. the Docker backend:
2. Derive a per-bottle docker subnet from `sha256(slug) % 254`
(skipping the docker-default 17): `192.168.X.0/24`. The bundle
IP is always `192.168.X.2` (gateway is `.1`).
3. Resolve the agent guest image: convert the existing
`Dockerfile` into a `.smolmachine` artifact via
`smolvm pack create --image <name> -o <stage>/agent.smolmachine`
(idempotent, layer-cached).
3. Resolve the agent guest image: `docker build` the existing
`Dockerfile`, then convert the resulting image into a
`.smolmachine` artifact. Empirically `smolvm pack create` only
reads OCI registry refs — it rejects `docker-daemon://`,
`oci-layout://`, `docker-archive:` tarballs, and every other
transport tested. The conversion path is a registry hop: bring
up an ephemeral `registry:2.8.3` container bound to
`127.0.0.1:<random>`, `docker tag` + `docker push` into it,
`smolvm pack create --image localhost:<port>/claude-bottle:<id>`,
tear down the registry. The `.smolmachine` is cached under
`~/.cache/claude-bottle/smolmachines/` keyed by the docker
image ID, so Dockerfile changes invalidate the cache and
unchanged rebuilds skip the whole pipeline.
4. Render the per-bottle Smolfile to `stage_dir/smolfile.toml`
using smolvm 0.8.0's schema:
- `image` / `entrypoint` / `cmd` — bundled into the