Skip to content

feat(images): register an image already in a registry with put_ref - #121

Merged
earakely-scale merged 5 commits into
mainfrom
edgararakelyan/image-put-ref
Oct 8, 2026
Merged

earakely-scale merged 5 commits into
mainfrom
edgararakelyan/image-put-ref

Conversation

@earakely-scale

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

Copy link
Copy Markdown
Collaborator

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 put and a2a-agent put take --image-ref as 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, read Docker-Content-Digest. A registry that omits the header gets a GET, and the manifest body is hashed.
  • Credentials: the configured image store's auth(ref), which only answers for its own registry. Anywhere else the resolver follows the 401 Bearer challenge 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.
  • A digest-only ref is kept once the registry serves it. A tag plus a digest must agree, since a pull goes by the digest and the tag would otherwise be misleading. With no tag, latest is used; docker.io maps to registry-1.docker.io with the library/ prefix.
  • A 404, a 401/403, other statuses and an unreachable registry each raise a ValueError that names the ref and the host. Nothing is written when pinning fails.

Refusals before any request

  • A ref must name its registry (alpine:3.20 → "spell it out, as in docker.io/library/alpine:3.20").
  • A registry on this machine is refused unless the image's store is the local registry's (an @local id), because no sandbox elsewhere could pull it. is_loopback_host now also counts 0.0.0.0 and host.docker.internal, case-insensitively.

CLI

  • env mcp-server put: exactly one of --dockerfile, --dockerfile-github-url, --image-ref. --image-ref needs --environment-name, since there is no environment card to read it from. --context needs --dockerfile, and --platform is 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

  • Unit tier: 6,899 passed. New tests:
    • 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) and cli/image_ref_put_test.py (9).
    • 13 more is_loopback_host cases.
  • Live, against real registries, writing only @local ids to a local store:
    • Pinned correctly:
      • a private ECR tag through the image store's credentials (the digest matches the registry API's);
      • public ECR, GHCR, and Docker Hub with no tag;
      • digest-only, and a tag with its matching digest.
    • Refused as expected:
      • a tag with a different digest;
      • a missing tag;
      • a private registry with no credentials;
      • an unqualified name;
      • a loopback registry for a shared-store id. The same ref is accepted for an @local id.
  • End to end: env mcp-server put --image-ref plus a2a-agent put --image-ref (both from private ECR), then a bundle task on beta_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

RetriggerConfidence Score: 5/5

The PR appears safe to merge; no new finding or outstanding previous finding remains.

Fix All in CursorFindings

  1. P1 Saved image fails to pull ▶
Fix with agent prompt
### Issue 1
src/agent_env/store/image_store/registry_api.py:undefined-46
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.

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.

  • Registry images are registered by digest for sandboxes to pull.
  • Both put commands can register images already in a registry.

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]
Loading

Reviews (6) · Last reviewed commit: "refactor(images): simplify put_ref's CLI..." · Reviewed by Greptile

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>
@earakely-scale
earakely-scale requested a review from a team as a code owner October 8, 2026 17:31
Comment thread src/agent_env/store/image_store/registry_api.py
Comment thread src/agent_env/store/image_store/registry_api.py
Comment thread src/agent_env/store/image_store/registry_api.py Outdated
Comment thread src/agent_env/store/image_store/registry_api.py Outdated
earakely-scale and others added 3 commits October 8, 2026 10:44
… 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>
@earakely-scale

Copy link
Copy Markdown
Collaborator Author

Chaos battery (local stores and @local ids, a throwaway registry, fake registries for the failure modes):

Scenario Result
Tag re-pushed with a different image after put_ref the first document still pulls the original image; a second put pins the new one
Pinned digest deleted from the registry put_ref of it: "has no such image"; a local deploy of an agent over it fails naming the ref, no leftover containers
Registry that accepts and never answers fails after 30 s (ReadTimeout), naming the host
429, 500, missing Docker-Content-Digest, malformed digest, redirect loop each a ValueError naming the ref and host; a missing header falls back to hashing the manifest
Token service answering 200 with HTML or a JSON array bug: a bare JSON parse error, or AttributeError. Fixed in dad960c: "the token service at … answered without a token"
SIGINT while the registry hangs nothing written
8 concurrent puts of one id versions 1–8, no collisions
a2a-agent put --image-ref with default validation (private registry image) completes; deploy, card, prompts, MCP and snapshot checks run. install/v1 reports unsupported because a registry-only image has no build context (expected; the step names the missing build context)
env mcp-server put --image-ref --validate deploys through the gateway and runs all 9 steps; the release gate blocks on mcp_tool_schema for that image's own parameter descriptions, not the registration

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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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.

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.

Fix in Cursor Fix in Claude Code Fix in Codex

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@earakely-scale

Copy link
Copy Markdown
Collaborator Author

@greptileai review

@earakely-scale
earakely-scale merged commit 4578b5f into main Oct 8, 2026
14 checks passed
@earakely-scale
earakely-scale deleted the edgararakelyan/image-put-ref branch October 8, 2026 20:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant