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.
| 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:
[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:
[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:
[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:
[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:
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:
"""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 = TrueIt 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:
[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:
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