diff --git a/AGENTS.md b/AGENTS.md index 2be17214..013980b3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -311,3 +311,6 @@ guide (configuration, environments, steps, artifacts, providers, the CLI) is www.agentenvframework.com/docs. A new feature, provider or option is documented there, not in the README: say in the pull request what the docs should cover, and a maintainer updates the site. `packages/agentenv-protocol/README.md` documents the protocol and the A2A agent framework. + +Guides for coding agents sit next to the code they cover: `src/agent_env/agents/AGENTS.md` for writing an A2A agent and +`src/agent_env/providers/sandbox_providers/AGENTS.md` for writing a sandbox provider. diff --git a/src/agent_env/providers/sandbox_providers/AGENTS.md b/src/agent_env/providers/sandbox_providers/AGENTS.md new file mode 100644 index 00000000..0564336d --- /dev/null +++ b/src/agent_env/providers/sandbox_providers/AGENTS.md @@ -0,0 +1,205 @@ +# AGENTS.md: writing a sandbox provider + +How to add a compute backend that agent-env runs its envs, agents and verifiers on. The repository-wide rules are in +the root `AGENTS.md`. + +A sandbox provider hands agent-env a machine; agent-env then loads Docker images, starts containers, copies files in +and runs verifiers over that machine's shell. A provider is two classes: + +- a `SandboxProvider` (`sandbox_provider.py`) that creates sandboxes and reconnects to them by id; +- a `Sandbox`, almost always a `VmSandbox` (`sandbox.py`), the handle to one machine: `exec`, `terminate`, + `tunnel_urls`. + +Read `boxd/` (the smallest complete VM provider) and `e2b/` before writing anything. `tensorlake/`, `vercel/` and `tenki/` +are further VM backends; `modal_sandbox.py` is a container backend; `local_sandbox.py` runs on this machine. + +## Workflow + +1. Ask which platform it is for. Pick a registry name: lowercase letters, digits and underscores (`acme_cloud`). It is the + entry-point name, the `[sandbox.providers.]` table and `Sandbox.type`. +2. Decide whether it ships in its own package or in this repository (below). For this repository, open an issue first, as + `CONTRIBUTING.md` says. +3. Implement a VM backend with the platform's Python SDK. +4. Write unit tests with the SDK injected and no network. +5. The user puts credentials in the environment or the secrets file. Never ask for one in the conversation and never + print one. +6. Run `agent-env plugin check` (out of tree), then `agent-env run hello --sandbox `. If either fails and you + cannot fix it, stop and show the output. +7. Report each item of the checklist as passed or failed, with command output. + +## Pick a shape + +| Shape | The platform gives you | You implement | You inherit | +|---|---|---|---| +| VM backend (recommended) | a Linux machine you can `exec` into, with Docker | `create_vm`, `create_sandbox` (a wrapper over `create_vm`), `get_sandbox`, a `VmSandbox` | `create_container` (create a VM, log in, pull, run), image loading and building, file staging | +| Container backend | an image booted directly, with a port exposed | `create_sandbox`, `get_sandbox`, a `Sandbox` with `exec` and the `write_file_*` methods | nothing | + +Nothing on `SandboxProvider` is abstract, but registration still refuses a class that overrides neither `create_sandbox` +(container) nor `create_vm` (vm) with `must implement create_sandbox or create_vm`, through an entry point and through +config alike; a provider missing `get_sandbox` registers and fails on its first reconnect. `SUPPORTED_SANDBOX_MODES` is +derived from which of the two you override. A container backend cannot run envs that deploy onto a VM or +`deploy_sandbox` steps with `sandbox_mode: vm`. + +A VM backend must override `create_sandbox` too, though registration does not ask for it: an agent's sandbox is made by +`create_sandbox`, whose base raises `NotImplementedError`, so a provider with only `create_vm` fails the first time an agent +is deployed on it. Most VM providers here do the same: `create_sandbox` ignores `image_name` and `env`, calls `create_vm` +with `exposed_ports=[port]` and returns the VM, and the deploy then loads and starts the image on it. See +`boxd/provider.py`. The rest of this file assumes a VM backend. + +## Provider contract + +| Method | Required | Called by | +|---|---|---| +| `create_vm(*, image, boot_mode, cpu, memory, disk_size_gb, timeout, exposed_ports, setup_for_gateway, attribution, network_policy) -> VmSandbox` | VM backend | env deploys, `deploy_sandbox` in VM mode, `create_container`, and your `create_sandbox` | +| `create_sandbox(*, image_name, port, env, cpu, memory, disk_size_gb, timeout, attribution, network_policy) -> Sandbox` | yes (a VM backend wraps `create_vm`) | an agent's sandbox (`provider.create_sandbox`); `create_container` of a container-only backend | +| `get_sandbox(sandbox_id) -> Sandbox` | yes | every step after the one that created the sandbox, and teardown | +| `terminate_sandbox(sandbox_id)` | no | teardown. The default calls `get_sandbox` then `terminate()`, and treats `SandboxNotFoundError` as done; override when you can terminate without a live handle | +| `create_container(...)` | inherited | containers that run an env or agent image. Override only when the platform boots images natively | +| `prepare_image(image, *, attribution)` | no | before a sandbox is asked for an image, outside any creation deadline; build a context-only image here | +| `from_config(**config)` | no | receives `[sandbox.providers..config]` with `secret:` references resolved. Override to fail early with a `ConfigError` that names the missing key (`boxd` also rejects unknown keys) | +| `close()` | no | release SDK clients | + +- Units: `cpu` in cores, `memory` in MiB, `disk_size_gb` in GB, `timeout` is the sandbox lifetime in seconds. +- `attribution` is an open `dict[str, str]` of cost-attribution tags. Pass it through `apply_default_attribution` and carry + every key onto the sandbox as tags or metadata. +- A parameter the platform cannot honour is logged as ignored, never approximated. A request it cannot satisfy (an image it + cannot boot, a size above its largest) raises `ValueError`; do not boot something else. +- `get_sandbox` may run in another process, on a provider built fresh from config. Whatever the handle needs that the + platform cannot report back (exposed ports, applied network policy) must be stored with the sandbox at creation, in + platform metadata or tags, and read back there. `e2b` keeps its exposed ports in sandbox metadata for this reason. +- A sandbox that is gone raises `SandboxNotFoundError` from `get_sandbox`. + +### Declarations + +Class variables the rest of agent-env reads instead of asking which provider it has. A provider that declares nothing has its +sandboxes off this machine, runs containers' images by name, and has no private network. + +| Variable | Set it when | +|---|---| +| `ON_THIS_MACHINE`, `REACHES_HOST_AT` | the sandboxes run on this machine (local Docker, a local VM) | +| `CONTAINER_ACCEPTS` | `create_container` runs images other than by name: `Accepts.LOADABLE` (tar.gz, registry name or build context), `Accepts.NAME_OR_CONTEXT` | +| `SANDBOX_ACCEPTS` | an agent's sandbox runs different images than the default (a provider that implements `create_vm` defaults to `LOADABLE`) | +| `PRIVATE_NETWORK` | `create_container(private_network=True)` puts containers on a network they share | + +## Sandbox handle contract + +| Member | Contract | +|---|---| +| `type` | must equal the registry name. agent-env terminates a sandbox whose type differs and raises `SandboxProviderTypeError` | +| `sandbox_id` | the platform's id; `get_sandbox` must accept it | +| `mode` | `"vm"` for a VM sandbox | +| `tunnel_urls` | `{port: url}` for every exposed port, filled in by the time `create_*` returns. Each URL must be reachable from wherever agent-env and the agent run; `https://` for a remote backend | +| `vnc_url` | `None` unless the platform serves one | +| `network_policy` | the policy actually applied, or `None` when you cannot tell | +| `platform` | the image platform it runs; `linux/amd64` by default, set another when the provider creates it | +| `exec(*argv)` | returns an object with async `stdout.read()` and `stderr.read()` (bytes) and async `wait()` (exit code). If the platform takes one command string, build it with `shlex.join(argv)` | +| `terminate()` | abstract; idempotent: terminating a sandbox that is already gone is not an error | + +Everything else is built on `exec`. `exec_script` runs `sudo bash -c