Skip to content
AgentEnv Framework
RegistryImage Store

Image Store

Where the container images that envs and agents run are pushed, tagged by version

The image store holds the container images that envs and agents run, one tag per version. It is one of the registry's four stores, and every image pushed to it is also saved as a tarball in the object store.

Names and tags

Putting an env or an agent pushes its image as a docker_image artifact, at <registry_host>/<repository_prefix>/<artifact id>:v<version>. With the defaults, which leave the prefix empty, the env email from Deploying your environment becomes localhost:5000/mcp-server-email:v1, and the agent claudius from Deploying your agent becomes localhost:5000/a2a-agent-claudius:v1.

How an image reaches a sandbox

A sandbox in VM mode loads each image from its tarball in the object store. One in container mode pulls the image from the registry, so only those deploys need the tag to still be there.

SandboxEnvsAgents
localload the tarballpull
modalpullpull
modal_vm, e2bload the tarballload the tarball

Credentials

Before a push or a pull, the framework asks the store for a login with auth(ref), and gets one only for refs on the store's own host. The store's credentials mint it each time: SecretStoreCredentials reads a Docker config.json from the secret store, EcrCredentials gets a short-lived ECR token, and GoogleAccessTokenCredentials an Artifact Registry access token.

Use a Supported Image Store

Three image stores are built in, and Artifact Registry works through the OCI one. A [stores.image] table selects one by its impl and passes its settings under config.

Local Registry

The default, which needs no table: an anonymous registry on localhost:5000. If nothing answers there, the first push starts one, a registry:2 container named agentenv-registry, bound to 127.0.0.1 and restarted unless you stop it. Name it only to use another port:

.agentenv/config.toml
[stores.image]
impl = "agent_env.store.image_store:LocalRegistryImageStore"
config = { registry_host = "localhost:5001" }

OCI Registry

OciRegistryImageStore works with any OCI registry, such as GitHub Container Registry or Docker Hub. SecretStoreCredentials logs in with the registry's entry in a Docker config.json held in the secret store:

.agentenv/config.toml
[stores.image]
impl = "agent_env.store.image_store:OciRegistryImageStore"
[stores.image.config]
registry_host     = "ghcr.io"
repository_prefix = "my-org/agent-env"
[stores.image.config.credentials]
impl       = "agent_env.store.image_store:SecretStoreCredentials"
secret_key = "registry_auths"

Amazon ECR

EcrImageStore creates each repository before its first push. EcrCredentials mints a short-lived ECR token for every login, from keys you keep in the secret store:

.agentenv/config.toml
[stores.image]
impl = "agent_env.store.image_store:EcrImageStore"
[stores.image.config]
registry_host     = "<account>.dkr.ecr.<region>.amazonaws.com"
repository_prefix = "agent-env"
[stores.image.config.credentials]
impl       = "agent_env.store.image_store:EcrCredentials"
region     = "<region>"
access_key = "secret:aws_access_key_id"
secret_key = "secret:aws_secret_access_key"

Artifact Registry

From the gcp extra. GoogleAccessTokenCredentials logs in with access tokens of a service account that the Application Default Credentials impersonate. The store doesn't create repositories, so create the Artifact Registry repository beforehand:

.agentenv/config.toml
[stores.image]
impl = "agent_env.store.image_store:OciRegistryImageStore"
[stores.image.config]
registry_host     = "<region>-docker.pkg.dev"
repository_prefix = "<project>/<repository>"
[stores.image.config.credentials]
impl            = "agent_env.store.image_store.google_credentials:GoogleAccessTokenCredentials"
service_account = "<name>@<project>.iam.gserviceaccount.com"

agent-env config explain image prints the store in effect without building it, and masks everything under credentials:

Output
image
  OciRegistryImageStore  registry_host=ghcr.io  repository_prefix=my-org/agent-env
  credentials:
    impl=***
    secret_key=***
  from [stores.image]

Write Your Own Image Store

A store of your own subclasses ImageStore and implements image_ref(repository, tag) and auth(ref). It never runs Docker: the framework tags, logs in and pushes with what they return. Override ensure_repository for a registry that needs something created first, and owns to say which refs your login covers.

This one pushes to Harbor, which refuses a push to a project that doesn't exist. Like EcrImageStore with ECR repositories, it creates its project in ensure_repository, and one robot account calls Harbor's API and logs in:

mycorp/harbor_store.py
"""An ImageStore on Harbor: every image in one Harbor project, created on first use."""

from __future__ import annotations

import logging
from urllib.parse import urlsplit

import httpx

from agent_env.store.image_store import (
    ImageStore,
    RegistryAuth,
    normalize_registry_host,
    registry_host_from_ref,
)

logger = logging.getLogger(__name__)


class HarborImageStore(ImageStore):
    """Refs are `<host>/<project>/<repository>:<tag>`. Harbor refuses a push to a
    project that does not exist, so `ensure_repository` creates the project, as
    `EcrImageStore` creates each ECR repository. One robot account calls the API and
    logs in to push and pull."""

    def __init__(self, url: str, project: str, username: str, password: str) -> None:
        self._host = normalize_registry_host(urlsplit(url).netloc)
        self._project = project
        self._login = RegistryAuth(self._host, username, password)
        api = f"{url.rstrip('/')}/api/v2.0"
        self._api = httpx.Client(base_url=api, auth=(username, password), timeout=30)
        self._project_exists = False

    def image_ref(self, repository: str, tag: str) -> str:
        return f"{self._host}/{self._project}/{repository}:{tag}"

    def owns(self, ref: str) -> bool:
        return registry_host_from_ref(ref) == self._host

    def auth(self, ref: str) -> RegistryAuth | None:
        return self._login if self.owns(ref) else None

    def ensure_repository(self, repository: str) -> None:
        """Creates the project once per process; Harbor creates each repository in it
        on its first push."""
        if self._project_exists:
            return
        project = {"project_name": self._project, "metadata": {"public": "false"}}
        response = self._api.post("/projects", json=project)
        if response.status_code == 201:
            logger.info("Created Harbor project %s", self._project)
        elif response.status_code == 403:
            logger.info("Can't create project %s; assuming it exists", self._project)
        elif response.status_code != 409:
            response.raise_for_status()
        self._project_exists = True

It needs a Harbor robot account that can push, pull and create projects. You select it like a built-in, with the robot's secret behind a secret: reference:

.agentenv/config.toml
[stores.image]
impl = "mycorp.harbor_store:HarborImageStore"
[stores.image.config]
url      = "https://harbor.example.com"
project  = "agent-env"
username = "robot$agent-env"
password = "secret:harbor_robot_secret"

It passes all 5 ImageStore conformance cases against Harbor 2.13, including the one that builds, pushes and pulls an image through Docker. The suite ships in the agentenv-framework repository's tst/, not in the wheel, so run it from a clone:

tests/test_harbor_store.py
import os
import uuid

import pytest

from mycorp.harbor_store import HarborImageStore
from tst.store import image_conformance

URL = "http://harbor.local:8080"


@pytest.fixture(scope="module")
def store():
    project = f"conformance-{uuid.uuid4().hex[:12]}"
    return HarborImageStore(
        URL, project, "robot$agent-env", os.environ["HARBOR_ROBOT_SECRET"]
    )


@pytest.mark.parametrize("case", image_conformance.CASES, ids=lambda c: c.__name__)
def test_conformance(case, store):
    case(store, f"repo-{uuid.uuid4().hex[:12]}")

Every login hands the robot's secret to the sandbox that pulls, so give the robot account nothing beyond pushing, pulling and creating projects.

Last updated on

Ask a question · Report an issue

On this page