fix(smolmachines): use containerized crane to push, bypassing docker daemon's HTTPS preference
test / unit (pull_request) Successful in 27s
test / integration (pull_request) Successful in 42s

The previous fix (`host.docker.internal:<port>` for daemon-side
push) still failed:

  Get "https://host.docker.internal:53958/v2/":
    http: server gave HTTP response to HTTPS client

`host.docker.internal` is reachable from Docker Desktop's daemon
VM but isn't in the daemon's default insecure-registries CIDRs
(only `::1/128` and `127.0.0.0/8` are), so docker push tries
HTTPS, hits a plain-HTTP registry, and refuses. The daemon.json
fix (`"insecure-registries": ["host.docker.internal"]`) works
but is a one-time manual step in Docker Desktop's UI — not
something we can do for the user.

Sidestep the daemon push entirely:

  1. docker build (as before) — local layer cache makes
     no-change rebuilds cheap.
  2. docker save the image to a per-digest tarball alongside the
     cached `.smolmachine`.
  3. Start an ephemeral registry container on a per-session
     docker network, with `-p :5000` so the host can also reach
     it for the pack step.
  4. docker run a one-shot crane container on the SAME network,
     mount the tarball, `crane push --insecure /img.tar
     <registry-container>:5000/...`. Container DNS resolves the
     registry on the network; `--insecure` forces plain HTTP.
  5. `smolvm pack create --image localhost:<host port>/...` from
     the host. smolvm's bundled crane auto-falls-back to HTTP
     for localhost addresses, so no insecure-registries config
     is needed on that side.
  6. Tear down everything; reap the tarball (registries hold the
     same bytes, no need to keep both around).

Net effect: the docker daemon never does an HTTP/HTTPS-policy
decision on our behalf. `docker push` is gone from the prepare
path; `docker save`, `docker network create`, `docker run` (for
registry + crane) replace it.

Tested end-to-end on Docker Desktop / macOS: `_ensure_smolmachine
("claude-bottle:latest")` produces a 204MB
`.smolmachine.smolmachine` artifact.

Adds:
- backend/docker/util.py:save() — thin docker save wrapper.
- local_registry.crane_push_tarball() — one-shot crane run on
  the registry's network.
- CRANE_IMAGE constant pinned by digest
  (gcr.io/go-containerregistry/crane@sha256:0ae17ecb...).

Removes:
- backend/docker/util.py:tag() / push() — unused without daemon
  push.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-27 14:52:40 -04:00
parent f4026ea3ae
commit 47eb56bd10
6 changed files with 347 additions and 258 deletions
+4 -12
View File
@@ -54,26 +54,18 @@ class TestImageId(unittest.TestCase):
self.assertIn("missing:tag", die.call_args.args[0])
class TestTagPush(unittest.TestCase):
def test_tag_runs_docker_tag(self):
class TestSave(unittest.TestCase):
def test_save_runs_docker_save(self):
with patch.object(
docker_mod.subprocess, "run", return_value=_ok(),
) as run:
docker_mod.tag("claude-bottle:latest", "localhost:5000/cb:abc")
docker_mod.save("claude-bottle:latest", "/tmp/img.tar")
argv = run.call_args.args[0]
self.assertEqual(
["docker", "tag", "claude-bottle:latest", "localhost:5000/cb:abc"],
["docker", "save", "claude-bottle:latest", "-o", "/tmp/img.tar"],
argv,
)
def test_push_runs_docker_push(self):
with patch.object(
docker_mod.subprocess, "run", return_value=_ok(),
) as run:
docker_mod.push("localhost:5000/cb:abc")
argv = run.call_args.args[0]
self.assertEqual(["docker", "push", "localhost:5000/cb:abc"], argv)
if __name__ == "__main__":
unittest.main()