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
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ and deserialized through a registry.
| `task/`, `task_step/` | A `Task` holds its `TaskStep`s inline; `Task.run()` executes them as a DAG. `depends_on` (None means all prior steps) gates a step, independent steps run concurrently, `fail_task_on_error` makes a failure fatal or tolerated, `retry_config` rolls a failed span back through the step journal and re-dispatches it. Built-in steps live in `task_step/task_steps/` (`deploy_env`, `deploy_agent`, `prompt_agent`, the verifiers under `verifiers/`, and more). |
| `store/` | Four store ABCs with local and cloud implementations: `DocumentStore` (SQLite, MongoDB), `ObjectStore` (filesystem, S3, Cloud Storage), `ImageStore` (local OCI registry, ECR), `SecretStore` (env vars or file, AWS Secrets Manager, Google Cloud Secret Manager). `VersionedEntityStore` implements the shared versioned get/put logic, `QueryBuilder` is the immutable chained query API, `store/base.py` holds the error types. A new backend must pass the conformance kits in `tst/store/`. |
| `config/` | The `Config` singleton (`get_config`, `configure`, `reset_config`) in `config/runtime.py`, file discovery in `config/loader.py`, and `load_impl`, which resolves `module:Class` pointers. `agent_env.store` re-exports the config names for compatibility. |
| `providers/` | `providers/sandbox_providers/` holds the sandbox providers `local`, `modal`, `modal_vm`, `e2b`, `sail_vm` (the `sail` extra); `[sandbox] default` and `agent_default` accept a comma-separated fallback chain, whose deploys skip a provider that can't run them. A sandbox provider declares what it runs (`ON_THIS_MACHINE`, `SANDBOX_ACCEPTS`, `CONTAINER_ACCEPTS`, `PRIVATE_NETWORK`, and `SUPPORTED_SANDBOX_MODES`, derived from whether it implements `create_sandbox` and `create_vm`), each sandbox carries the `platform` of the images it runs (linux/amd64 unless its provider sets another when it creates it; None, any, unchecked, for a local sandbox), and code outside `providers/sandbox_providers/` reads those, never which provider it has. `providers/env_providers/` holds the environment providers: `EnvironmentProvider` (an env's containers and state store) and `EnvironmentGatewayProvider`, which renders a docker-compose for the gateway and its MCP servers inside the sandbox; `providers/env_state/` holds env-state providers (`local_postgres` built in). |
| `providers/` | `providers/sandbox_providers/` holds the sandbox providers `local`, `modal`, `modal_vm`, `e2b`, `sail_vm` (the `sail` extra); `[sandbox] default` and `agent_default` accept a comma-separated fallback chain, whose deploys skip a provider that can't run them. A sandbox provider declares what it runs (`ON_THIS_MACHINE`, `CONTAINER_ACCEPTS`, `PRIVATE_NETWORK`; `SUPPORTED_SANDBOX_MODES`, derived from whether it implements `create_sandbox` and `create_vm`; and `SANDBOX_ACCEPTS`, the images an agent's sandbox runs, which defaults to those a VM loads when it implements `create_vm` and to `CONTAINER_ACCEPTS` otherwise, so only a provider that implements `create_vm` yet gives an agent a container, like the local one, declares it), each sandbox carries the `platform` of the images it runs (linux/amd64 unless its provider sets another when it creates it; None, any, unchecked, for a local sandbox), and code outside `providers/sandbox_providers/` reads those, never which provider it has. `providers/env_providers/` holds the environment providers: `EnvironmentProvider` (an env's containers and state store) and `EnvironmentGatewayProvider`, which renders a docker-compose for the gateway and its MCP servers inside the sandbox; `providers/env_state/` holds env-state providers (`local_postgres` built in). |
| `a2a_agent/` | The `A2AAgent` entity (`a2a_agent`), its stores and the validator steps. The protocol package provides the agent-side framework. |
| `runner/` | The `[runner]` seam: `Runner.submit()` returns `(run_id, instance_id)`; `LocalRunner` is built in. |
| `explorer/` | Optional local web UI: `agent-env up`, needs the `explorer` extra, binds loopback `:8234`. |
Expand Down
2 changes: 2 additions & 0 deletions src/agent_env/a2a_agent/a2a_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -462,6 +462,8 @@ async def deploy(
)
self._owns_sandbox = True
logger.info(f"Sandbox created: {self._sandbox.sandbox_id} (mode={self._sandbox.mode})")
if problem := provider.sandbox_problem(self._sandbox, image):
raise ValueError(problem)
else:
if network_policy is not None and network_policy.restricts_egress:
raise NetworkPolicyUnsupportedError(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
from typing import TYPE_CHECKING

from agent_env.attribution import Attribution
from agent_env.providers.sandbox_providers.sandbox import NetworkPolicy, NetworkPolicyUnsupportedError, Sandbox, VmSandbox
from agent_env.providers.sandbox_providers.sandbox_provider import SANDBOX_MODE_VM, SandboxProvider
from agent_env.providers.sandbox_providers.sandbox import NetworkPolicy, NetworkPolicyUnsupportedError, Sandbox
from agent_env.providers.sandbox_providers.sandbox_provider import SandboxProvider

if TYPE_CHECKING:
from agent_env.artifact.artifacts.docker_image import DockerImageArtifact
Expand Down Expand Up @@ -48,6 +48,11 @@ def filter_sandbox_providers(
f"(providers: {[type(p).__name__ for p in providers]})"),
)

def sandbox_problem(self, sandbox: Sandbox, image: DockerImageArtifact) -> str | None:
"""None: create_sandbox checks each provider's sandbox as it tries it, and gives only one that runs
the image."""
return None

def supports_network_policy(self, policy: NetworkPolicy) -> bool:
return any(p.supports_network_policy(policy) for p in self._providers)

Expand Down Expand Up @@ -76,11 +81,10 @@ async def create_sandbox(
logger.warning(f"{name}.create_sandbox failed: {e}; trying next provider")
errors.append((name, e))
continue
# The deploy loads the image into a VM, which refuses one it can't get or run; the next provider's may not.
if (image is not None and sandbox.mode == SANDBOX_MODE_VM and isinstance(sandbox, VmSandbox)
and (problems := sandbox.load_problems([image]))):
problem = "; ".join(problems)
logger.warning(f"{name}.create_sandbox gave a VM that can't load the image: {problem}; trying next provider")
# A VM refuses an image it can't get or run, and a container runs only what its provider's containers do;
# the next provider's sandbox may run it.
if image is not None and (problem := p.sandbox_problem(sandbox, image)):
logger.warning(f"{problem}; trying next provider")
errors.append((name, ValueError(problem)))
try:
await sandbox.terminate()
Expand Down
2 changes: 0 additions & 2 deletions src/agent_env/providers/sandbox_providers/e2b/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
from agent_env.providers.sandbox_providers.sandbox import NetworkMode, NetworkPolicy
from agent_env.providers.sandbox_providers.sandbox_provider import (
SANDBOX_MODE_VM,
Accepts,
SandboxProvider,
apply_default_attribution,
)
Expand All @@ -38,7 +37,6 @@ class E2BSandboxProvider(SandboxProvider):
interpolation in ``[sandbox.providers.e2b.config]``.
"""

SANDBOX_ACCEPTS = Accepts.LOADABLE # a sandbox is a VM, which loads the image; a container pulls it by name

# AsyncSandbox.get_host(port) returns ``<port>-<sandbox-id>.e2b.app``.
# Workloads receive those public URLs, so an allowlisted sandbox must be
Expand Down
3 changes: 3 additions & 0 deletions src/agent_env/providers/sandbox_providers/local_sandbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
from agent_env.providers.sandbox_providers.sandbox_provider import (
SANDBOX_MODE_CONTAINER,
SANDBOX_MODE_VM,
Accepts,
SandboxProvider,
refuse_unenforceable_policy,
)
Expand Down Expand Up @@ -538,6 +539,8 @@ class LocalSandboxProvider(SandboxProvider):

EXTRA_CONTAINER_RUN_ARGS = " ".join(f"--add-host {entry}" for entry in _EXTRA_HOSTS)
ON_THIS_MACHINE = True
# An agent's sandbox is a container from its image's name, though a VM it creates (create_vm) loads images.
SANDBOX_ACCEPTS = Accepts.NAME

def url_from_sandbox(self, url: str) -> str:
return self.get_external_url(url)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ class ModalSandboxProvider(SandboxProvider):

# Each image in a container of its own, built first when it is only a build context (prepare_image); its
# containers share i6pn, Modal's private network.
SANDBOX_ACCEPTS = CONTAINER_ACCEPTS = Accepts.NAME_OR_CONTEXT
CONTAINER_ACCEPTS = Accepts.NAME_OR_CONTEXT
PRIVATE_NETWORK = True

@classmethod
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
from agent_env.attribution import Attribution
from agent_env.config import get_config
from agent_env.providers.sandbox_providers.sandbox import NetworkPolicy, VmSandbox, push_object_over_stdin
from agent_env.providers.sandbox_providers.sandbox_provider import SANDBOX_MODE_VM, Accepts, SandboxProvider
from agent_env.providers.sandbox_providers.sandbox_provider import SANDBOX_MODE_VM, SandboxProvider

if TYPE_CHECKING:
from agent_env.store.object_store import ObjectStore
Expand Down Expand Up @@ -274,7 +274,6 @@ async def _download_ranged(self, url_factory: Callable[[int], Awaitable[str]], v

class ModalVmSandboxProvider(SandboxProvider):
EGRESS_HOSTS: ClassVar[tuple[str, ...]] = ("*.modal.host", "*.w.modal.host")
SANDBOX_ACCEPTS = Accepts.LOADABLE # a sandbox is a VM, which loads the image; a container pulls it by name

@classmethod
def supports_network_policy(cls, policy: NetworkPolicy) -> bool:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@
from agent_env.providers.sandbox_providers.sandbox import NetworkPolicy, NetworkPolicyUnsupportedError
from agent_env.providers.sandbox_providers.sandbox_provider import (
SANDBOX_MODE_VM,
Accepts,
SandboxProvider,
apply_default_attribution,
)
Expand Down Expand Up @@ -134,7 +133,6 @@ class SailVmSandboxProvider(SandboxProvider):
Sail adds it to the agent's requests to the model endpoint (see ``model_key``)."""

EGRESS_HOSTS: ClassVar[tuple[str, ...]] = ("*.sail.box",)
SANDBOX_ACCEPTS = Accepts.LOADABLE # a sandbox is a VM, which loads the image; a container pulls it by name

def __init__(
self,
Expand Down
36 changes: 31 additions & 5 deletions src/agent_env/providers/sandbox_providers/sandbox_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,19 +166,24 @@ def from_config(cls, **config: Any) -> Self:
# Internal→external host rewrites for URLs issued on this platform ({} = none).
URL_REWRITES: ClassVar[dict[str, str]] = {}

# What it declares, which nothing outside the providers reads any other way. A provider that declares nothing runs
# each image from its name, in a sandbox that isn't on this machine, on linux/amd64, and creates no VM and no
# private network.
# What it declares, which nothing outside the providers reads any other way. A provider that declares nothing has
# its sandboxes off this machine, runs its containers' images by name on no private network, and gives an agent's
# sandbox the images that follow from what it implements (SANDBOX_ACCEPTS).
#
# Whether its sandboxes run on this machine: an image only this machine holds (in a registry or object store on
# it) reaches them, its containers need this machine's Docker, and a gateway's infra envs are built here for it.
ON_THIS_MACHINE: ClassVar[bool] = False
# The sandbox modes it creates, derived for each provider class from what it implements (``__init_subclass__``):
# SANDBOX_MODE_CONTAINER when it implements create_sandbox, SANDBOX_MODE_VM when it implements create_vm.
SUPPORTED_SANDBOX_MODES: ClassVar[frozenset[str]] = frozenset()
# The images create_sandbox and create_container run.
SANDBOX_ACCEPTS: ClassVar[Accepts] = Accepts.NAME
# The images create_container runs.
CONTAINER_ACCEPTS: ClassVar[Accepts] = Accepts.NAME
# The images create_sandbox, an agent's sandbox, runs; derived for a provider class that doesn't declare them
# (``__init_subclass__``): those a VM loads when it implements create_vm, since its create_sandbox gives a bare VM
# the deploy loads the image into, and else CONTAINER_ACCEPTS, since its create_sandbox is one of its containers.
# One whose create_sandbox differs declares them, as the local provider does, and its subclasses keep them.
SANDBOX_ACCEPTS: ClassVar[Accepts] = Accepts.NAME
_DECLARES_SANDBOX_ACCEPTS: ClassVar[bool] = False
# Whether create_container(private_network=True) puts containers on a network they share.
PRIVATE_NETWORK: ClassVar[bool] = False

Expand All @@ -187,12 +192,33 @@ def __init_subclass__(cls, **kwargs):
cls.SUPPORTED_SANDBOX_MODES = frozenset(
mode for mode, method in ((SANDBOX_MODE_CONTAINER, "create_sandbox"), (SANDBOX_MODE_VM, "create_vm"))
if getattr(cls, method) is not getattr(SandboxProvider, method))
if "SANDBOX_ACCEPTS" in cls.__dict__:
cls._DECLARES_SANDBOX_ACCEPTS = True
elif not cls._DECLARES_SANDBOX_ACCEPTS:
cls.SANDBOX_ACCEPTS = (Accepts.LOADABLE if SANDBOX_MODE_VM in cls.SUPPORTED_SANDBOX_MODES
else cls.CONTAINER_ACCEPTS)

@property
def links(self) -> tuple[SandboxProvider, ...]:
"""The providers a deploy on this one may run on, in the order it tries them: itself, or a chain's."""
return (self,)

def sandbox_problem(self, sandbox: Sandbox, image: DockerImageArtifact) -> str | None:
"""Why ``sandbox``, which create_sandbox gave to run ``image``, can't run it, or None when it can: a VM that
can't load it, or a container from a provider whose SANDBOX_ACCEPTS takes images as a VM loads them, which runs
only the images its containers do (CONTAINER_ACCEPTS). One that declares other images is taken at its word."""
name = type(self).__name__
if sandbox.mode == SANDBOX_MODE_VM:
problems = sandbox.load_problems([image]) if isinstance(sandbox, VmSandbox) else []
return f"{name} gave a VM that can't load the image: {'; '.join(problems)}" if problems else None
if self.SANDBOX_ACCEPTS is not Accepts.LOADABLE:
return None
if (problem := self.CONTAINER_ACCEPTS.problem(image, on_this_machine=sandbox.ON_THIS_MACHINE)) is None:
return None
Comment thread
greptile-apps[bot] marked this conversation as resolved.
return (f"{name} gave a container, which can't run the image: {problem}; its SANDBOX_ACCEPTS is LOADABLE, the "
"default for a provider that implements create_vm, so declare the images its containers run instead: "
f"SANDBOX_ACCEPTS = Accepts.{self.CONTAINER_ACCEPTS.name}")

def url_from_sandbox(self, url: str) -> str:
"""``url``, of a service this process reaches, as its sandboxes reach it."""
return url
Expand Down
Loading
Loading