From a155daf2f683e721dac31919f343c728398a9bb0 Mon Sep 17 00:00:00 2001 From: Edgar Arakelyan Date: Fri, 30 Oct 2026 11:28:33 -0700 Subject: [PATCH] fix(artifacts): a digest ref its registry refuses without credentials is kept, for Docker's own login to pull A bundle whose images are in a private registry couldn't run with core's default stores: put_ref checked each digest-pinned ref against a registry the local store holds no credentials for, which answered 401, though Docker on the machine was logged in to it. Such a ref is now recorded as given; a tag still has to be resolved, and a refusal of the store's own credentials still fails. The README bundle init writes says a digest-pinned image needs only docker login. Co-Authored-By: Claude Opus 5.5 --- .../artifact/artifacts/docker_image.py | 14 +++++++-- src/agent_env/bundle/_import_report.py | 6 ++-- .../artifact/test_docker_image_put_ref.py | 29 +++++++++++++++++++ tst/unit/bundle/importer_entities_test.py | 5 ++-- 4 files changed, 47 insertions(+), 7 deletions(-) diff --git a/src/agent_env/artifact/artifacts/docker_image.py b/src/agent_env/artifact/artifacts/docker_image.py index 0f921d59..e3c6c8a6 100644 --- a/src/agent_env/artifact/artifacts/docker_image.py +++ b/src/agent_env/artifact/artifacts/docker_image.py @@ -22,7 +22,7 @@ from agent_env.store.ids import fs_safe, image_repository, is_local_id, local_image_repository from agent_env.store.image_store.local_registry_image_store import LocalRegistryImageStore from agent_env.store.image_store.oci_registry_credentials import image_reference_problem, is_loopback_host, names_registry, registry_host_from_ref -from agent_env.store.image_store.registry_api import NoSuchImageError, pin_digest, pinned_from +from agent_env.store.image_store.registry_api import NoSuchImageError, RegistryRefusedError, pin_digest, pinned_from from agent_env.utils.docker.context import BuildContext from agent_env.utils.docker.build import DEFAULT_BUILD_PLATFORM, build_image, normalize_platform @@ -222,7 +222,9 @@ def put_ref(cls, id: str, *, description: str, image_name: str, earlier_pin: str sandbox that can't pull the image to build it. ``image_name`` must name its registry, read with the configured image store's credentials where it holds them; - one on this machine is refused unless ``id``'s image store is the local registry.""" + one on this machine is refused unless ``id``'s image store is the local registry. One already pinned to a + digest, in a registry that refuses to serve it to a store holding no credentials for it, is recorded as given, + for a sandbox whose Docker is logged in to that registry to pull.""" from agent_env.artifact.store import get_artifact_store from agent_env.config import get_config @@ -240,7 +242,13 @@ def put_ref(cls, id: str, *, description: str, image_name: str, earlier_pin: str if earlier_pin is not None and pinned_from(earlier_pin, image_name): pinned = earlier_pin else: - pinned = pin_digest(image_name, config.get_image_store().auth(image_name)) + auth = config.get_image_store().auth(image_name) + try: + pinned = pin_digest(image_name, auth) + except RegistryRefusedError: + if auth is not None or "@" not in image_name: + raise + pinned = image_name if context_path is None: return get_artifact_store().put_document(cls(id=id, description=description, image_name=pinned)) image_name, _, image_digest = pinned.rpartition("@") diff --git a/src/agent_env/bundle/_import_report.py b/src/agent_env/bundle/_import_report.py index c1468766..01818f70 100644 --- a/src/agent_env/bundle/_import_report.py +++ b/src/agent_env/bundle/_import_report.py @@ -260,8 +260,10 @@ def _needs_lines(imported: BundleImport) -> list[str]: refs = [pulled.image for pulled in imported.pulled] + [image.ref for image in imported.images if image.ref] registries = sorted({host for ref in refs if (host := registry_host_from_ref(ref))}) if registries: - lines.append(f"- Access to the registries its pulled images are in: `{'`, `'.join(registries)}`. agent-env reads " - "a private one only through its configured image store, such as an ECR store for an ECR registry.") + lines.append(f"- Access to the registries its pulled images are in: `{'`, `'.join(registries)}`. An image pinned " + "to a digest needs only this machine's Docker logged in to its registry (`docker login`); agent-env " + "reads any other private one only through its configured image store, such as an ECR store for an " + "ECR registry.") return lines diff --git a/tst/unit/artifact/test_docker_image_put_ref.py b/tst/unit/artifact/test_docker_image_put_ref.py index e56f4878..ea2f40d8 100644 --- a/tst/unit/artifact/test_docker_image_put_ref.py +++ b/tst/unit/artifact/test_docker_image_put_ref.py @@ -114,6 +114,35 @@ def unresolvable(ref, auth): assert get_artifact_store().next_version("tool-image") == 1 +@pytest.mark.parametrize("ref, credentials, kept", [ + (f"registry.example/team/app:v1@{DIGEST}", None, True), + ("registry.example/team/app:v1", None, False), + (f"registry.example/team/app:v1@{DIGEST}", RegistryAuth("registry.example", "user", "pw"), False), +], ids=["digest", "tag", "the store's own registry"]) +def test_a_digest_ref_its_registry_refuses_a_store_without_its_credentials_is_kept_as_given( + local_stores, monkeypatch, ref, credentials, kept): + """For a sandbox whose Docker is logged in to that registry to pull; a tag, or the store's own refusal, still fails.""" + class _Registry(ImageStore): + def image_ref(self, repository, tag): + return f"registry.example/{repository}:{tag}" + + def auth(self, ref): + return credentials + + def refuse(ref, auth): + raise docker_image.RegistryRefusedError(f"{ref}: registry.example refused to serve it (HTTP 401)") + + local_stores.set_image_store(_Registry()) + monkeypatch.setattr(docker_image, "pin_digest", refuse) + + if kept: + assert DockerImageArtifact.put_ref("app-image", description="d", image_name=ref).image_name == ref + else: + with pytest.raises(ValueError, match="refused to serve it"): + DockerImageArtifact.put_ref("app-image", description="d", image_name=ref) + assert get_artifact_store().next_version("app-image") == 1 + + def test_a_ref_given_its_build_context_runs_under_its_tag_pulled_by_the_digest_given(local_stores, resolved, tmp_path): """So a sandbox that can't pull it builds it from the context, under the name its containers start from.""" (tmp_path / "Dockerfile").write_text("FROM scratch\n") diff --git a/tst/unit/bundle/importer_entities_test.py b/tst/unit/bundle/importer_entities_test.py index a5729120..e79dce6a 100644 --- a/tst/unit/bundle/importer_entities_test.py +++ b/tst/unit/bundle/importer_entities_test.py @@ -106,8 +106,9 @@ def test_an_image_its_registry_names_is_written_as_its_ref_and_reported_as_pulle assert _toml(imported.root, "envs/crm/env.toml")["image"] == {"ref": "registry.example.com/crm:v1"} assert [(pull.folder, pull.image) for pull in imported.pulled] == [("envs/crm", "registry.example.com/crm:v1")] assert imported.pinned == () and imported.images == () - assert ("- Access to the registries its pulled images are in: `registry.example.com`. agent-env reads a private one " - "only through its configured image store, such as an ECR store for an ECR registry.") in ( + assert ("- Access to the registries its pulled images are in: `registry.example.com`. An image pinned to a digest " + "needs only this machine's Docker logged in to its registry (`docker login`); agent-env reads any other " + "private one only through its configured image store, such as an ECR store for an ECR registry.") in ( imported.root / "README.md").read_text().splitlines()