# Image Store (https://www.agentenvframework.com/docs/registry/image-store)

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

1. `env mcp-server put` builds the env’s image and pushes it to the image store as `mcp-server-email:v1`.
2. The same `put` saves the image as a tarball in the object store, under the artifact’s id and version.
3. Deploying on `local` runs the env in VM mode: the sandbox loads its image from the tarball, not the registry.
4. An agent on `local` runs as a container, so the sandbox pulls `a2a-agent-claudius:v1` from the registry.
5. On a remote registry, `auth(ref)` mints a login from the store’s credentials, and `modal` pulls with it.

Parts of the scene:

- **The framework**: Pushes an image when you put an env or an agent, then saves it to the object store. Before a push or a pull it asks the image store for a login with `auth(ref)`.
- **The image store**: Holds the container images that envs and agents run, one tag per artifact version. `[stores.image]` selects it; the default is a local OCI registry.
- **The registry host**: Every ref starts with the store’s `registry_host`, `localhost:5000` by default. Pointing `[stores.image]` at another registry changes the host of every image pushed after it.
- **Local registry**: `LocalRegistryImageStore`, the default: an anonymous registry on `localhost:5000`. The first push starts it as the `registry:2` container `agentenv-registry`, unless a registry already answers there.
- **OCI registry**: `OciRegistryImageStore`, for any registry named in `registry_host`, with an optional `repository_prefix` and `credentials`.
- **ECR**: `EcrImageStore` creates each repository before its first push, and `EcrCredentials` mints a short-lived ECR token for every login.
- **Artifact Registry**: `OciRegistryImageStore` with `GoogleAccessTokenCredentials`, from the `gcp` extra: each login is an access token of a service account. Create the repository beforehand.
- **Your store**: Any `ImageStore` subclass named in `[stores.image]`. It implements `image_ref` and `auth`, and overrides `ensure_repository` if its registry needs repositories created.
- **mcp-server-email:v1**: The image of the env `email`, pushed by `env mcp-server put` as the `docker_image` artifact `mcp-server-email`. Each put pushes a new tag, so version 2 is `:v2`.
- **a2a-agent-claudius:v1**: The image of the agent `claudius`, pushed by `a2a-agent put` as the `docker_image` artifact `a2a-agent-claudius`.
- **The login**: What `auth(ref)` returns: nothing for the local registry, or a username and password the store’s `credentials` mint. They mint it at each login, so a rotated secret needs no restart.
- **The object store**: Keeps each image’s tarball under `artifacts/docker_image/<id>/<version>/`, which VM-mode sandboxes load instead of pulling.
- **mcp-server-email-v1.tar.gz**: The same image, from `docker save` and gzipped, written by the same put. The files its build copied in sit beside it, in `build-context.tar.gz`.
- **a2a-agent-claudius-v1.tar.gz**: The agent’s tarball, from its own put. Agents on `modal_vm` and `e2b` load it; agents on `local` and `modal` pull the image instead.
- **The sandbox**: Where envs and agents run. A VM-mode sandbox loads each image from its tarball; a container-mode one pulls it from the registry.
- **email-mmtflq6t**: The env on `local`, in VM mode: its gateway, server and database images are loaded from their tarballs, as on `modal_vm` and `e2b`.
- **claudius-4msj7n4d**: The agent on `local`, started as a single container: `docker pull` on its ref, after a `docker login` when `auth(ref)` returns one.
- **email-k3v9x2ra**: The env on `modal`, which runs each of its images as a separate container. Modal pulls each one from the registry, with the login passed as a registry secret.

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](https://www.agentenvframework.com/docs/core-concepts.md#the-registry), and every image pushed to it
is also saved as a tarball in the [object store](https://www.agentenvframework.com/docs/registry/object-store.md#keys).

## 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](https://www.agentenvframework.com/docs/environments/deploying.md#register-it) becomes
`localhost:5000/mcp-server-email:v1`, and the agent `claudius` from
[Deploying your agent](https://www.agentenvframework.com/docs/agents/deploying.md#register-it) 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.

| Sandbox           | Envs             | Agents           |
| ----------------- | ---------------- | ---------------- |
| `local`           | load the tarball | pull             |
| `modal`           | pull             | pull             |
| `modal_vm`, `e2b` | load the tarball | load 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:

```toml title=".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:

```toml title=".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:

```toml title=".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:

```toml title=".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`:

```text title="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:

```python title="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:

```toml title=".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](https://github.com/scaleapi/agentenv-framework/tree/main/tst/store)
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:

```python title="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.