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
9 changes: 5 additions & 4 deletions src/agent_env/cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from agent_env.plugins._cli import load_cli_plugins, load_cli_root_options
from agent_env.store.base import NotFoundError
from agent_env.store.routing import namespace_routing
from agent_env.utils.docker_build import DockerBuildError

from .a2a_agent import a2a_agent
from .artifact import artifact
Expand All @@ -17,11 +18,11 @@
from .up import up


# Errors that mean "not allowed" or "not there" rather than "agent-env is broken". Spelled out
# because there is no domain base class to catch: ConfigError subclasses ValueError (so does
# pydantic's ValidationError), while NotFoundError subclasses Exception. A TypeError or a
# Errors that mean "not allowed", "not there" or "your build failed" rather than "agent-env is broken".
# Spelled out because there is no domain base class to catch: ConfigError subclasses ValueError (so does
# pydantic's ValidationError), while NotFoundError and DockerBuildError don't. A TypeError or a
# retry-exhausted DuplicateKeyError is a defect, and a defect keeps its traceback.
_USER_FACING_ERRORS = (ValueError, NotFoundError)
_USER_FACING_ERRORS = (ValueError, NotFoundError, DockerBuildError)


class _UserErrorsAreNotCrashes(click.Group):
Expand Down
14 changes: 3 additions & 11 deletions src/agent_env/cli/a2a_agent/put.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import subprocess
import sys
from pathlib import Path

import click

from agent_env.a2a_agent import A2AAgent
from agent_env.artifact import DockerImageArtifact
from agent_env.cli.utils import build_platform_option, detect_env_metadata, docker_build_platform_args, skips_local_validation
from agent_env.cli.utils import build_platform_option, detect_env_metadata, skips_local_validation
from agent_env.utils.docker_build import build_image


@click.command()
Expand Down Expand Up @@ -44,15 +44,7 @@ def put(agent_id: str, dockerfile: str, context_path: str | None, env_var_pairs:
image_tag = f"a2a-agent-{agent_id}"

click.echo(f"Building Docker image...")
result = subprocess.run(
["docker", "build", *docker_build_platform_args(build_platform),
"-f", str(dockerfile_path), "-t", image_tag, str(context)],
capture_output=True,
text=True,
)
if result.returncode != 0:
click.echo(f"Docker build failed: {result.stderr}", err=True)
sys.exit(1)
build_image(dockerfile_path, context, image_tag, platform=build_platform)

click.echo(f"Creating DockerImageArtifact...")
artifact = DockerImageArtifact.put(
Expand Down
14 changes: 3 additions & 11 deletions src/agent_env/cli/env/gateway.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import subprocess
import sys
from pathlib import Path

import click

from agent_env.artifact import DockerImageArtifact
from agent_env.cli.utils import build_platform_option, detect_env_metadata, docker_build_platform_args
from agent_env.cli.utils import build_platform_option, detect_env_metadata
from agent_env.env import GatewayEnv
from agent_env.utils.docker_build import build_image

_PACKAGE_ROOT = Path(__file__).parent.parent.parent
GATEWAY_DOCKERFILE = _PACKAGE_ROOT / "env" / "gateway" / "Dockerfile"
Expand All @@ -28,15 +28,7 @@ def put(env_id: str, metadata_pairs: tuple[str, ...], build_platform: str):
"""Build and upload a gateway environment."""

click.echo(f"Building gateway Docker image...")
result = subprocess.run(
["docker", "build", *docker_build_platform_args(build_platform),
"-f", str(GATEWAY_DOCKERFILE), "-t", GATEWAY_IMAGE_TAG, str(GATEWAY_CONTEXT)],
capture_output=True,
text=True,
)
if result.returncode != 0:
click.echo(f"Docker build failed: {result.stderr}", err=True)
sys.exit(1)
build_image(GATEWAY_DOCKERFILE, GATEWAY_CONTEXT, GATEWAY_IMAGE_TAG, platform=build_platform)

click.echo(f"Creating DockerImageArtifact...")
artifact = DockerImageArtifact.put(
Expand Down
14 changes: 2 additions & 12 deletions src/agent_env/cli/env/mcp_server.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,22 @@
import asyncio
import os
import subprocess
import sys
from pathlib import Path

import click

from agent_env.artifact import DockerImageArtifact, EnvironmentArtifact
from agent_env.cli.utils import (
DEFAULT_BUILD_PLATFORM,
deployed_env_from_instance,
build_platform_option,
detect_env_metadata,
docker_build_platform_args,
env_provider_type_option,
environment_name_options,
resolve_environment_name,
skips_local_validation,
)
from agent_env.utils.card_naming import card_name_from_github, card_name_from_source
from agent_env.utils.docker_build import DEFAULT_BUILD_PLATFORM, build_image
from agent_env.env import Env, MCPServerEnv


Expand Down Expand Up @@ -194,15 +192,7 @@ def _on_progress(step: str, message: str, percent: int) -> None:
image_tag = f"mcp-server-{env_id}"

click.echo(f"Building MCP server Docker image...")
result = subprocess.run(
["docker", "build", *docker_build_platform_args(build_platform),
"-f", str(dockerfile_path), "-t", image_tag, str(context)],
capture_output=True,
text=True,
)
if result.returncode != 0:
click.echo(f"Docker build failed: {result.stderr}", err=True)
sys.exit(1)
build_image(dockerfile_path, context, image_tag, platform=build_platform)

click.echo(f"Creating DockerImageArtifact...")
artifact = DockerImageArtifact.put(
Expand Down
40 changes: 5 additions & 35 deletions src/agent_env/cli/env/service_db.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
"""CLI commands for ServiceDBEnv."""

import subprocess
from pathlib import Path

import click

from agent_env.artifact import DockerImageArtifact
from agent_env.cli.utils import build_platform_option, detect_env_metadata, docker_build_platform_args
from agent_env.cli.utils import build_platform_option, detect_env_metadata
from agent_env.env.envs.service_db import ServiceDBEnv
from agent_env.config import get_config
from agent_env.utils.docker_build import build_image

# Path to ServiceDB Dockerfile
SERVICE_DB_DOCKERFILE = Path(__file__).parent.parent.parent / "env" / "envs" / "service_db" / "Dockerfile"
Expand Down Expand Up @@ -37,17 +37,7 @@ def put(env_id: str, metadata_pairs: tuple[str, ...], build_platform: str):
click.echo(f"Building ServiceDB image from {SERVICE_DB_DOCKERFILE}...")

# Build Docker image
result = subprocess.run(
["docker", "build", *docker_build_platform_args(build_platform),
"-f", str(SERVICE_DB_DOCKERFILE),
"-t", SERVICE_DB_IMAGE_NAME,
str(SERVICE_DB_DOCKERFILE.parent)],
capture_output=True,
text=True,
)
if result.returncode != 0:
click.echo(f"Docker build failed: {result.stderr}", err=True)
raise click.Abort()
build_image(SERVICE_DB_DOCKERFILE, SERVICE_DB_DOCKERFILE.parent, SERVICE_DB_IMAGE_NAME, platform=build_platform)
click.echo("Docker build successful")

# Create DB DockerImageArtifact
Expand All @@ -61,17 +51,7 @@ def put(env_id: str, metadata_pairs: tuple[str, ...], build_platform: str):

# Build db-web image (build instead of pull to avoid docker save manifest issues on Apple Silicon)
click.echo(f"Building db-web image from {DB_WEB_DOCKERFILE}...")
result = subprocess.run(
["docker", "build", *docker_build_platform_args(build_platform),
"-f", str(DB_WEB_DOCKERFILE),
"-t", DB_WEB_IMAGE_NAME,
str(DB_WEB_DOCKERFILE.parent)],
capture_output=True,
text=True,
)
if result.returncode != 0:
click.echo(f"db-web build failed: {result.stderr}", err=True)
raise click.Abort()
build_image(DB_WEB_DOCKERFILE, DB_WEB_DOCKERFILE.parent, DB_WEB_IMAGE_NAME, platform=build_platform)
click.echo("db-web build successful")

# Create db-web DockerImageArtifact
Expand All @@ -85,17 +65,7 @@ def put(env_id: str, metadata_pairs: tuple[str, ...], build_platform: str):

# Build db-mcp image (PostgreSQL MCP server for direct DB access)
click.echo(f"Building db-mcp image from {DB_MCP_DOCKERFILE}...")
result = subprocess.run(
["docker", "build", *docker_build_platform_args(build_platform),
"-f", str(DB_MCP_DOCKERFILE),
"-t", DB_MCP_IMAGE_NAME,
str(DB_MCP_DOCKERFILE.parent)],
capture_output=True,
text=True,
)
if result.returncode != 0:
click.echo(f"db-mcp build failed: {result.stderr}", err=True)
raise click.Abort()
build_image(DB_MCP_DOCKERFILE, DB_MCP_DOCKERFILE.parent, DB_MCP_IMAGE_NAME, platform=build_platform)
click.echo("db-mcp build successful")

# Create db-mcp DockerImageArtifact
Expand Down
24 changes: 3 additions & 21 deletions src/agent_env/cli/env/website.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,22 @@
import asyncio
import os
import subprocess
import sys
from pathlib import Path

import click

from agent_env.artifact import DockerImageArtifact, EnvironmentArtifact
from agent_env.cli.utils import (
DEFAULT_BUILD_PLATFORM,
deployed_env_from_instance,
build_platform_option,
detect_env_metadata,
docker_build_platform_args,
env_provider_type_option,
environment_name_options,
resolve_environment_name,
skips_local_validation,
)
from agent_env.utils.card_naming import card_name_from_github, card_name_from_source
from agent_env.utils.docker_build import DEFAULT_BUILD_PLATFORM, build_image
from agent_env.env import Env
from agent_env.env.envs.website import WebsiteEnv
from agent_env.providers import get_env_sandbox_provider
Expand Down Expand Up @@ -152,15 +150,7 @@ def _frontend_progress(step: str, message: str, percent: int) -> None:
click.echo(f"Derived environment_name={environment_name!r} from the environment card.")

click.echo(f"Building website backend Docker image...")
result = subprocess.run(
["docker", "build", *docker_build_platform_args(build_platform),
"-f", str(backend_dockerfile_path), "-t", backend_tag, str(backend_ctx)],
capture_output=True,
text=True,
)
if result.returncode != 0:
click.echo(f"Backend Docker build failed: {result.stderr}", err=True)
sys.exit(1)
build_image(backend_dockerfile_path, backend_ctx, backend_tag, platform=build_platform)

click.echo(f"Creating backend DockerImageArtifact...")
backend_artifact = DockerImageArtifact.put(
Expand All @@ -175,15 +165,7 @@ def _frontend_progress(step: str, message: str, percent: int) -> None:
frontend_tag = f"website-frontend-{env_id}"

click.echo(f"Building website frontend Docker image...")
result = subprocess.run(
["docker", "build", *docker_build_platform_args(build_platform),
"-f", str(frontend_dockerfile_path), "-t", frontend_tag, str(frontend_ctx)],
capture_output=True,
text=True,
)
if result.returncode != 0:
click.echo(f"Frontend Docker build failed: {result.stderr}", err=True)
sys.exit(1)
build_image(frontend_dockerfile_path, frontend_ctx, frontend_tag, platform=build_platform)

click.echo(f"Creating frontend DockerImageArtifact...")
frontend_artifact = DockerImageArtifact.put(
Expand Down
16 changes: 4 additions & 12 deletions src/agent_env/cli/env/website_browser.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import subprocess
import sys
from pathlib import Path

import click

from agent_env.artifact import DockerImageArtifact
from agent_env.cli.utils import build_platform_option, detect_env_metadata, docker_build_platform_args
from agent_env.cli.utils import build_platform_option, detect_env_metadata
from agent_env.env import MCPServerEnv
from agent_env.env.envs.website_browser import (
PLAYWRIGHT_MCP_VERSION,
WEBSITE_BROWSER_IMAGE_TAG,
WEBSITE_BROWSER_ENVIRONMENT_NAME,
)
from agent_env.utils.docker_build import build_image

_PACKAGE_ROOT = Path(__file__).parent.parent.parent
WEBSITE_BROWSER_DOCKERFILE = _PACKAGE_ROOT / "env" / "envs" / "website_browser" / "Dockerfile"
Expand All @@ -36,16 +36,8 @@ def put(env_id: str, metadata_pairs: tuple[str, ...], build_platform: str):
env_id = get_config().default_website_browser_env_id

click.echo("Building website browser Docker image...")
result = subprocess.run(
["docker", "build", *docker_build_platform_args(build_platform),
"--build-arg", f"PLAYWRIGHT_MCP_VERSION={PLAYWRIGHT_MCP_VERSION}",
"-f", str(WEBSITE_BROWSER_DOCKERFILE), "-t", WEBSITE_BROWSER_IMAGE_TAG, str(WEBSITE_BROWSER_CONTEXT)],
capture_output=True,
text=True,
)
if result.returncode != 0:
click.echo(f"Docker build failed: {result.stderr}", err=True)
sys.exit(1)
build_image(WEBSITE_BROWSER_DOCKERFILE, WEBSITE_BROWSER_CONTEXT, WEBSITE_BROWSER_IMAGE_TAG, platform=build_platform,
build_args={"PLAYWRIGHT_MCP_VERSION": PLAYWRIGHT_MCP_VERSION})

click.echo("Creating DockerImageArtifact...")
artifact = DockerImageArtifact.put(
Expand Down
8 changes: 1 addition & 7 deletions src/agent_env/cli/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@
from agent_env.providers.env_providers.env_server_provider import EnvironmentServerProvider
from agent_env.store.base import NotFoundError
from agent_env.store.ids import is_local_id

DEFAULT_BUILD_PLATFORM = "linux/amd64"
from agent_env.utils.docker_build import DEFAULT_BUILD_PLATFORM


def build_platform_option(f):
Expand All @@ -35,11 +34,6 @@ def build_platform_option(f):
)(f)


def docker_build_platform_args(platform: str | None) -> list[str]:
"""['--platform', <platform>] when a platform is set, else [] (host-native build)."""
return ["--platform", platform] if platform else []


def env_provider_type_option(help: str, env_type: str = "mcp_server"):
"""Shared `--env-provider-type` option (default `gateway`), refused at parse time unless an installed provider has that type, and
for an env other than one MCP server, unless that provider deploys more than one."""
Expand Down
29 changes: 29 additions & 0 deletions src/agent_env/utils/docker_build.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
"""Build a Docker image on this machine, the one way every put command and bundle writer does it."""

from __future__ import annotations

import subprocess
from collections.abc import Mapping
from pathlib import Path

DEFAULT_BUILD_PLATFORM = "linux/amd64" # the remote sandbox VMs


class DockerBuildError(RuntimeError):
"""A local ``docker build`` that failed or couldn't start; the message carries docker's output."""


def build_image(dockerfile: Path, context: Path, tag: str, *, platform: str | None,
build_args: Mapping[str, str] | None = None) -> None:
"""Build ``context`` with ``dockerfile`` into the local image ``tag``, for ``platform``, or for this host's
platform when it's None or empty. Raises DockerBuildError, with the build's output, when it fails."""
argv = ["docker", "build", *(["--platform", platform] if platform else [])]
for name, value in (build_args or {}).items():
argv += ["--build-arg", f"{name}={value}"]
argv += ["-f", str(dockerfile), "-t", tag, str(context)]
try:
result = subprocess.run(argv, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, errors="replace")
except FileNotFoundError as e:
raise DockerBuildError(f"docker build of {tag} failed: docker is not on PATH") from e
if result.returncode != 0:
raise DockerBuildError(f"docker build of {tag} failed (exit {result.returncode}):\n{result.stdout.rstrip()}")
Loading
Loading