Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 31 additions & 14 deletions src/agent_env/env/bootstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from dataclasses import dataclass, field, replace
from enum import Enum
from pathlib import Path
from typing import Any

from agent_env.artifact import DockerImageArtifact
from agent_env.config import get_config
Expand Down Expand Up @@ -54,22 +55,29 @@ def _quiet(line: str) -> None:
pass


def put_gateway_env(env_id: str, *, platform: str | None, metadata: Mapping[str, str] | None = None,
say: Callable[[str], None] = _quiet, published: Mapping[Path, str] | None = None) -> GatewayEnv:
def put_gateway_env(env_id: str, *, platform: str | None, metadata: Mapping[str, Any] | None = None,
say: Callable[[str], None] = _quiet, published: Mapping[Path, str] | None = None,
version: int | None = None) -> GatewayEnv:
"""Write the gateway env ``env_id``, its image registered from ``published``, each Dockerfile's published digest,
when given, else built for ``platform`` (this host's when None)."""
when given, else built for ``platform`` (this host's when None). It's stored as ``version`` when given, which must
be the env's next: ConcurrentModificationError otherwise."""
_check_version(version)
artifact = _image("gateway", derive_id(env_id, "env_image"), "Created from agent-env CLI", GATEWAY_DOCKERFILE,
GATEWAY_IMAGE_TAG, platform=platform, say=say, published=published)
say("Creating GatewayEnv...")
env = GatewayEnv.put(id=env_id, docker_image_artifact=artifact, metadata=_metadata(GATEWAY, metadata))
env = GatewayEnv.put(id=env_id, docker_image_artifact=artifact, metadata=_metadata(GATEWAY, metadata),
version=version)
Comment thread
greptile-apps[bot] marked this conversation as resolved.
say(f"Created GatewayEnv: id={env.id} version={env.version}")
return env


def put_service_db_env(env_id: str, *, platform: str | None, metadata: Mapping[str, str] | None = None,
say: Callable[[str], None] = _quiet, published: Mapping[Path, str] | None = None) -> ServiceDBEnv:
def put_service_db_env(env_id: str, *, platform: str | None, metadata: Mapping[str, Any] | None = None,
say: Callable[[str], None] = _quiet, published: Mapping[Path, str] | None = None,
version: int | None = None) -> ServiceDBEnv:
"""Write the service-db env ``env_id``, its service-db, db-web and db-mcp images registered from ``published``, each
Dockerfile's published digest, when given, else built for ``platform`` (this host's when None)."""
Dockerfile's published digest, when given, else built for ``platform`` (this host's when None). It's stored as
``version`` when given, which must be the env's next: ConcurrentModificationError otherwise."""
_check_version(version)
db = _image("ServiceDB", derive_id(env_id, "db_image"), f"ServiceDB PostgreSQL image for {env_id}",
SERVICE_DB_DOCKERFILE, SERVICE_DB_IMAGE_NAME, platform=platform, say=say, published=published)
db_web = _image("db-web", derive_id(env_id, "db_web_image"), "db-web lightweight web UI for database inspection",
Expand All @@ -78,26 +86,35 @@ def put_service_db_env(env_id: str, *, platform: str | None, metadata: Mapping[s
DB_MCP_DOCKERFILE, DB_MCP_IMAGE_NAME, platform=platform, say=say, published=published)
say("Creating ServiceDBEnv...")
env = ServiceDBEnv.put(id=env_id, db_docker_image_artifact=db, db_web_docker_image_artifact=db_web,
db_mcp_docker_image_artifact=db_mcp, metadata=_metadata(SERVICE_DB, metadata))
db_mcp_docker_image_artifact=db_mcp, metadata=_metadata(SERVICE_DB, metadata),
version=version)
say(f"Created ServiceDBEnv: id={env.id} version={env.version}")
return env


def put_website_browser_env(env_id: str, *, platform: str | None, metadata: Mapping[str, str] | None = None,
say: Callable[[str], None] = _quiet,
published: Mapping[Path, str] | None = None) -> MCPServerEnv:
def put_website_browser_env(env_id: str, *, platform: str | None, metadata: Mapping[str, Any] | None = None,
say: Callable[[str], None] = _quiet, published: Mapping[Path, str] | None = None,
version: int | None = None) -> MCPServerEnv:
"""Write the website browser as the MCP server env ``env_id``, its image registered from ``published``, each
Dockerfile's published digest, when given, else built for ``platform`` (this host's when None)."""
Dockerfile's published digest, when given, else built for ``platform`` (this host's when None). It's stored as
``version`` when given, which must be the env's next: ConcurrentModificationError otherwise."""
_check_version(version)
artifact = _image("website browser", derive_id(env_id, "env_image"), "Website browser MCP server",
WEBSITE_BROWSER_DOCKERFILE, WEBSITE_BROWSER_IMAGE_TAG, platform=platform, say=say,
published=published)
say("Creating MCPServerEnv...")
env = MCPServerEnv.put(id=env_id, docker_image_artifact=artifact, environment_name=WEBSITE_BROWSER_ENVIRONMENT_NAME,
metadata=_metadata(WEBSITE_BROWSER, metadata))
metadata=_metadata(WEBSITE_BROWSER, metadata), version=version)
say(f"Created MCPServerEnv: id={env.id} version={env.version} environment_name={env.environment_name}")
return env


def _check_version(version: int | None) -> None:
"""Refuse a version below 1 before anything is written: the store reads 0 as "the next free version"."""
if version is not None and version < 1:
raise ValueError(f"an env's versions start at 1, so it can't be stored as version {version}")


def _image(label: str, artifact_id: str, description: str, dockerfile: Path, image_name: str, *, platform: str | None,
say: Callable[[str], None], published: Mapping[Path, str] | None) -> DockerImageArtifact:
"""``dockerfile``'s image, written as ``artifact_id`` with the folder it's in as its build context: registered as
Expand All @@ -124,7 +141,7 @@ def _build_and_put(label: str, artifact_id: str, description: str, dockerfile: P
return artifact


def _metadata(kind: str, extra: Mapping[str, str] | None) -> dict[str, str]:
def _metadata(kind: str, extra: Mapping[str, Any] | None) -> dict[str, Any]:
dockerfile = _DOCKERFILES[kind][0]
return {**detect_env_metadata(dockerfile, dockerfile.parent), **(extra or {}),
BUILD_INPUTS_KEY: build_inputs_digest(kind)}
Expand Down
43 changes: 39 additions & 4 deletions tst/unit/env/bootstrap_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,15 @@
from agent_env.env import GatewayEnv
from agent_env.env import bootstrap
from agent_env.env.bootstrap import GATEWAY, SERVICE_DB, WEBSITE_BROWSER, InfraError, ensure_default_envs
from agent_env.env.env import Env
from agent_env.store.base import ConcurrentModificationError


_DIGEST = "sha256:" + "5" * 64
_INFRA_PUTS = pytest.mark.parametrize("kind, put", [
(GATEWAY, bootstrap.put_gateway_env), (SERVICE_DB, bootstrap.put_service_db_env),
(WEBSITE_BROWSER, bootstrap.put_website_browser_env),
], ids=[GATEWAY, SERVICE_DB, WEBSITE_BROWSER])


@pytest.fixture
Expand All @@ -42,10 +48,7 @@ def write(env_id, *, platform, metadata=None, published=None):
return calls


@pytest.mark.parametrize("kind, put", [
(GATEWAY, bootstrap.put_gateway_env), (SERVICE_DB, bootstrap.put_service_db_env),
(WEBSITE_BROWSER, bootstrap.put_website_browser_env),
], ids=[GATEWAY, SERVICE_DB, WEBSITE_BROWSER])
@_INFRA_PUTS
def test_each_infra_image_keeps_the_build_context_it_is_built_from(local_stores, monkeypatch, kind, put):
"""So a VM or Modal running another platform than this machine builds it again from that; and those contexts are
what its recorded build inputs cover. Each Dockerfile is named by its whole path: a bare name is looked up from
Expand Down Expand Up @@ -257,6 +260,38 @@ def test_a_published_image_is_registered_by_its_tag_and_digest_with_the_context_
assert bootstrap._built_from_stock(env, GATEWAY)


@_INFRA_PUTS
def test_a_put_that_names_a_version_writes_only_that_next_one(local_stores, kind, put):
"""So a promotion that read the env at one version writes nothing over a version someone put meanwhile."""
published = {dockerfile: _DIGEST for dockerfile in bootstrap._DOCKERFILES[kind]}
put("x", platform=None, published=published)

assert put("x", platform=None, published=published, version=2).version == 2
with pytest.raises(ConcurrentModificationError):
put("x", platform=None, published=published, version=2)
assert Env.get("x").version == 2


@_INFRA_PUTS
@pytest.mark.parametrize("version", [0, -1])
def test_a_put_refuses_a_version_below_1_before_writing_anything(local_stores, monkeypatch, kind, put, version):
"""The store would read 0 as "the next free version", and write one the caller didn't name."""
monkeypatch.setattr(bootstrap, "_image", lambda *args, **kwargs: pytest.fail("wrote an image"))

with pytest.raises(ValueError, match=f"can't be stored as version {version}"):
put("x", platform=None, published={}, version=version)


def test_a_put_keeps_metadata_that_isnt_a_string(local_stores):
"""Such as the record a promotion keeps on the version it wrote."""
stamp = {"stage": "dev", "passed": True, "previous": 1}

env = bootstrap.put_gateway_env("default", platform=None, published={bootstrap.GATEWAY_DOCKERFILE: _DIGEST},
metadata={"promotion": stamp})

assert Env.get("default", env.version).metadata["promotion"] == stamp


def test_a_published_image_is_registered_from_its_own_dockerfile_whatever_folder_the_run_is_in(local_stores, tmp_path,
monkeypatch):
"""A run started from a folder holding a Dockerfile of its own registers the gateway's, with its sources."""
Expand Down
Loading