Repository navigation
feat(images): register an image already in a registry with put_ref - #121
Conversation
DockerImageArtifact.put_ref writes an image document with no tar.gz: only a registry reference, its tag pinned at put time to the digest the registry serves (repo:tag@sha256:...), so every later pull gets the same image. Sandboxes already pull such a document. - The digest comes from a manifest HEAD over the OCI distribution API, with the configured image store's credentials for its own registry and an anonymous bearer token anywhere else. A digest-only ref is kept once the registry serves it; a tag and a digest together must agree. - A ref must name its registry, and a registry on this machine (loopback, 0.0.0.0, host.docker.internal) is refused unless the image's store is the local registry's, since no sandbox elsewhere could pull it. - `env mcp-server put` and `a2a-agent put` take --image-ref as the alternative to a Dockerfile. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… check the digest it serves A registry that takes basic credentials and leaves out Docker-Content-Digest got the fallback GET without them, so a private image it served couldn't be registered. A digest header that isn't one is refused rather than written. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The digest check took the reference grammar's form (any algorithm, 32 or more hex digits of either case), which Docker narrows to sha256, sha384 or sha512 at their full length in lowercase. A digest outside that, given or served, would be written and then fail at pull time; it is now refused at put time. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A token service answering 200 with no token in a JSON object, as a sign-in page or a proxy does, surfaced as a bare JSON parse error, or crashed on a JSON array. It is now a ValueError naming the image and the token service; one that refuses still leaves the registry's 401. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
Chaos battery (local stores and
Unit tier: 6,910 passed. 🤖 Generated with Claude Code |
- `env mcp-server put --image-ref` shares the local build's tail (write the env, gate the release) instead of repeating it. - A ref giving a tag and a digest takes one registry lookup: the tag must resolve to the digest, which proves the digest exists. - The registry tests' fixture is `fake_registry`, not `requests`. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
| if image.digest is None: | ||
| tag = image.tag or "latest" | ||
| return f"{image.name}:{tag}@{_check(image, tag, auth)}" | ||
| served = _check(image, image.tag or image.digest, auth) |
There was a problem hiding this comment.
When someone registers name:tag@digest, pin_digest now checks only the tag. If the registry serves that tag with the matching digest but refuses a request for the digest itself, registration succeeds. The sandbox then pulls by digest and fails. Check that the supplied digest can be pulled before saving the image.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agent_env/store/image_store/registry_api.py
Line: 46
Comment:
**Saved image fails to pull**
When someone registers `name:tag@digest`, `pin_digest` now checks only the tag. If the registry serves that tag with the matching digest but refuses a request for the digest itself, registration succeeds. The sandbox then pulls by digest and fails. Check that the supplied digest can be pulled before saving the image.
---
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.
Keeping this as is. Fetching a manifest by its digest is required by the OCI distribution spec, and Docker-Content-Digest names the content the registry served, so a registry that resolves the tag to D but refuses D is broken in a way no client works around. More to the point, the common case, a ref with only a tag, never fetches by digest either: it resolves the tag and saves the digest the registry reported. A separate digest lookup only for the rarer tag-plus-digest form would make the two paths inconsistent without making either safer. The pinned refs this writes have been pulled by digest on beta_scale and Modal in the e2e runs.
There was a problem hiding this comment.
That’s a fair distinction. I agree this does not warrant a change: the OCI distribution contract requires a manifest addressed by its digest to be retrievable, so a registry that serves a tag as D but rejects the equivalent @D request is non-conforming. Adding a digest-only probe only for tag-plus-digest references would also make that path stricter than the ordinary tag-only path without providing a meaningful guarantee. The beta_scale and Modal end-to-end pulls further validate the intended behavior. I’m closing this finding.
|
@greptileai review |
What
DockerImageArtifact.put_ref(id, *, description, image_name)registers an image that already lives in a registry. The document it writes has no tar.gz, only the reference, with the tag pinned to the digest the registry serves at put time (repo:tag@sha256:…), so every later pull gets the same image. Sandboxes have pulled tarball-less image documents since #113; this adds the writer for them.env mcp-server putanda2a-agent puttake--image-refas the alternative to a Dockerfile.Behavior
Pinning (
store/image_store/registry_api.py, httpx over the OCI distribution API)HEAD /v2/<repo>/manifests/<tag>with the index/list/manifest Accept types, readDocker-Content-Digest. A registry that omits the header gets a GET, and the manifest body is hashed.auth(ref), which only answers for its own registry. Anywhere else the resolver follows the 401Bearerchallenge for an anonymous pull token. agent-env reads no other credentials, so a private registry that isn't the store's is refused with a message saying so.latestis used;docker.iomaps toregistry-1.docker.iowith thelibrary/prefix.ValueErrorthat names the ref and the host. Nothing is written when pinning fails.Refusals before any request
alpine:3.20→ "spell it out, as in docker.io/library/alpine:3.20").@localid), because no sandbox elsewhere could pull it.is_loopback_hostnow also counts0.0.0.0andhost.docker.internal, case-insensitively.CLI
env mcp-server put: exactly one of--dockerfile,--dockerfile-github-url,--image-ref.--image-refneeds--environment-name, since there is no environment card to read it from.--contextneeds--dockerfile, and--platformis ignored with a warning.a2a-agent put: exactly one of--dockerfile,--image-ref. With--image-ref, metadata starts empty (no env detection from a Dockerfile).Testing
registry_api_test.py(12): the bearer flow, basic auth, latest and the Docker Hub library prefix, digest-only, tag/digest mismatch, loopback over http, missing digest header, the 404/401/500 messages, connection errors, malformed digests.test_docker_image_put_ref.py(9) andcli/image_ref_put_test.py(9).is_loopback_hostcases.@localids to a local store:@localid.env mcp-server put --image-refplusa2a-agent put --image-ref(both from private ECR), then a bundle task onbeta_scale(deploy env → load → run agent → verifier) passed: env 93s, load 4s, agent 52s, check 1.0, both sandboxes torn down. The same agent document also deployed on Modal containers in 32s.🤖 Generated with Claude Code

Confidence Score: 5/5
The PR appears safe to merge; no new finding or outstanding previous finding remains.Fix with agent prompt
Summary
The PR lets MCP server and A2A agent commands register images that already live in a registry, without building or uploading them. It pins image tags to registry digests so sandboxes can pull the same image later.
Diagram
%%{init: {'theme': 'neutral'}}%% flowchart TD A[MCP put] --> B{Image source} B -->|GitHub Dockerfile| C[Build from GitHub] B -->|Image reference| D[Pin registry digest] B -->|Local Dockerfile| E[Build local image] D --> F[Save image and environment] E --> F C --> G[Save environment] F --> H{Validation requested?} G --> H H -->|Yes| I[Run release gate]Reviews (6) · Last reviewed commit: "refactor(images): simplify put_ref's CLI..." · Reviewed by Greptile