# Deploying your agent (https://www.agentenvframework.com/docs/agents/deploying)

> Deploy your agent on any sandbox and connect with any env

1. Register the folder. The put builds the image and stores the agent `claudius`, version 1.
2. Deploy it with `--sandbox local`. The local sandbox starts an agent instance, with its own id and A2A URL.
3. The framework reads the card the instance serves: its name, its A2A endpoint and its extensions.
4. Deploy the same version with `--sandbox modal`: a new instance in a Modal sandbox, reached through a Modal tunnel.
5. Deploy it with `--sandbox <name>`, on a sandbox provider you register yourself: another instance, wherever the provider runs it.
6. Each instance has its own id and URL, and all of them serve the same card.

Parts of the scene:

- **Your folder**: `agent.py` holds `ClaudiusRLMAgent`, and the `Dockerfile` installs `agentenv-framework-protocol[agent]`, `mcp` and `anthropic` and runs it.
- **Version 1 of claudius**: What `agent-env a2a-agent put` stored: the agent, pointing at the image the put built. It runs nothing, and a later put adds version 2 without changing this one.
- **The commands**: `agent-env a2a-agent put` registers the agent, and `agent-env a2a-agent deploy` starts an instance in the sandbox its `--sandbox` names.
- **--sandbox**: Picks where the instance runs. Without it, a deploy uses `[sandbox] agent_default` in `config.toml`, else `local`. Either takes a fallback chain, such as `modal,local`.
- **local**: Runs the agent as a container on your machine’s Docker, with its A2A port published on `localhost`. It does not enforce the TTL.
- **modal**: Runs the agent’s image in a Modal sandbox, reached through an HTTPS tunnel on `modal.host`. Modal pulls the image from your image store, so it needs a registry Modal can reach, and your Modal token.
- **Your sandbox provider**: Subclass `SandboxProvider` and register it under `[sandbox.providers.<name>]` in `config.toml`, or through the `agent_env.sandbox_providers` entry point. Then deploy with `--sandbox <name>`.
- **An agent instance**: One deployment of the agent, with an id that uniquely identifies it and its own A2A URL. Each deploy passes it the model endpoint and key from `[model]`.
- **The model**: `[model] base_url` and `api_key` in `config.toml`, with the key as a `secret:` reference. `LITELLM_BASE_URL` and `LITELLM_API_KEY` win over them.
- **The framework**: Or anything else that speaks A2A. It reads the card at `/.well-known/agent-card.json`, then sends tasks to the agent’s `/a2a` endpoint.
- **The agent card**: The card the SDK built from the class: the agent’s name, its A2A endpoint and its extensions. Every instance serves the same card, from its own address.

## Package it

The framework deploys container images, so the first step is a Dockerfile next to `agent.py`:

```dockerfile title="claudius/Dockerfile"
FROM python:3.12-slim
WORKDIR /app
RUN pip install --no-cache-dir "agentenv-framework-protocol[agent]" "mcp>=1.25,<2" anthropic
COPY agent.py .
CMD ["python", "agent.py"]
```

## Register it

Registering builds the image and stores it as an artifact, a stored object that the framework
versions. It then stores the agent, a document that points at that artifact, under an id you
choose. These pages use the id `claudius`:

```bash title="Terminal"
agent-env a2a-agent put --id claudius --dockerfile claudius/Dockerfile --skip-validation
```

```text title="Output"
Building Docker image...
Creating DockerImageArtifact...
Created artifact: id=a2a-agent-claudius version=1
Registering A2A agent...
Created A2A agent: id=claudius version=1 image=a2a-agent-claudius:1
```

Each put of the same id stores a new version, and older versions never change. Without
`--skip-validation`, the put also deploys the agent to run conformance checks, which need an object
store that signs URLs, such as S3; on the local defaults they stop with `RuntimeError: A2A
validation requires a signable object store for the presigned-URI probe.`

## Point it at a model

A deploy passes the agent a model endpoint and key, as `LITELLM_BASE_URL` and `LITELLM_API_KEY`.
Neither has a default, so set them under `[model]` in `.agentenv/config.toml`, with the key as a
`secret:` reference:

```toml title=".agentenv/config.toml"
[model]
base_url = "https://llm.example.com/v1"
api_key = "secret:litellm_api_key"
```

`secret:litellm_api_key` names a key in your secret store, so the file never holds the key itself.
The default store reads it from a `litellm_api_key` environment variable or a YAML file, and
[Secret store](https://www.agentenvframework.com/docs/registry/secret-store.md) covers the others.

The `LITELLM_BASE_URL` and `LITELLM_API_KEY` environment variables win over the file. When the
secret store has no value for the key, a deploy stops before it starts a container, with `Error:
Unresolved secret:litellm_api_key reference (no value and no default)`.

`ClaudiusRLMAgent` sends Anthropic Messages API requests to that endpoint, for `claude-opus-5-5` by
default. A LiteLLM proxy with that model serves them at `/v1/messages`, next to its
OpenAI-compatible routes.

## Deploy your agent

With the agent registered and pointed at a model, one command deploys it:

```bash title="Terminal"
agent-env a2a-agent deploy --id claudius
```

```text title="Output"
Sandbox backend: config default
Fetching A2A agent: id=claudius version=latest...
Found: id=claudius version=1 image=a2a-agent-claudius
Deploying (ttl=7200s)...
Deployed!
Instance ID: claudius-4msj7n4d
A2A URL: http://localhost:42031
Sandbox ID: local-531d8cae
Agent Card: claudius
Expires At (UTC): 2026-09-29 21:44 UTC
```

`A2A URL` is how the framework, or you, reach your agent: it serves its card there and takes tasks
at `/a2a`. The instance id uniquely identifies this deployment of the agent.

Where the instance runs depends on the sandbox. The default is `local`, where the agent is a
container on your machine's Docker. `--sandbox` picks another built-in, such as `modal`, or a
provider you register under `[sandbox.providers.<name>]` in `config.toml`.

The framework records every instance. `agent-env a2a-agent get-instance --id claudius-4msj7n4d`
reads the record back: the agent id and the exact version it deployed, the instance's A2A URL, the
sandbox it runs in and when it expires. The record also stores the card the instance served.

## Use it

The framework uses your agent through what the instance's card declares. The card's `url`, `/a2a`,
takes tasks as A2A messages, and its extensions list what else the agent supports: mcp-config takes
environments, agent-config takes settings such as the model, and trajectory hands back what the
agent did. In a [task](https://www.agentenvframework.com/docs/tasks.md), a `deploy_agent` step deploys the agent and posts it each
env's MCP URL, and a `prompt_agent` step sets its config, sends the prompt and reads the trajectory.

1. The instance serves its card at `/.well-known/agent-card.json`: its name, its A2A endpoint and its extensions.
2. The card lists mcp-config, so `deploy_agent` posts it the `email` instance’s MCP URL. It reaches `run()` in `request.mcp_servers`.
3. The card lists agent-config, so `prompt_agent` sends the settings its step sets, such as `model`, before the prompt.
4. `prompt_agent` sends the prompt to the card’s `url`, `/a2a`. `run()` calls the env’s tools and answers, and the task completes.
5. The card lists trajectory, so `prompt_agent` reads back every tool call the agent made.
6. Other extensions are declared on the card the same way: an agent that implements `peer-agents/v1` can message the other agents in a task. `claudius` does not.

Parts of the scene:

- **The instance’s card**: Served at `/.well-known/agent-card.json` and stored on the instance’s record. Every client finds how to use the agent here: its name, its A2A endpoint and its extensions.
- **Extensions**: Each extension is a URN and the endpoints it serves. The framework uses one only when the card lists it, so the same task drives agents that support different things.
- **peer-agents**: An agent that implements `urn:agentenv:peer-agents/v1` takes the other agents in a task from a `peer_agents` step, and can message them. `ClaudiusRLMAgent` does not, so its card does not list it.
- **deploy_agent**: A task step that deploys the agent the way `a2a-agent deploy` does, then posts it the MCP URL of each env it names, under the name on the env’s card.
- **prompt_agent**: A task step that sends the settings it sets to agent-config, the prompt to `/a2a` as an A2A message, and reads the trajectory once the agent replies. The reply is stored under its `prompt_id`.
- **Another agent**: Another agent in the task, which messages this one over A2A, the same way `prompt_agent` does. It learns where to from a `peer_agents` step.
- **The agent instance**: `claudius-4msj7n4d`, from Deploy your agent, running in the local sandbox.
- **The SDK’s server**: `serve()` answers the card, `/a2a` and each extension’s `/ext/…` endpoint, and turns each A2A task into one call to `run()`. You write none of it.
- **Your agent**: `ClaudiusRLMAgent`, running from the image. `run()` gets the envs, the config and the prompt in its `TaskRequest`, calls the env’s tools with Claude, and returns the answer and a trajectory.

Other extensions are declared the same way, and the framework uses one only when the card lists it:
an agent that implements `peer-agents/v1` can message the other agents in a task. [Useful A2A
extensions](https://www.agentenvframework.com/docs/agents/extensions.md) covers every one.

## Tear it down

Deploying again starts another instance with another id, and the agent itself never changes. An
instance runs until its TTL runs out, which you set with `--ttl-seconds`, or until you close it
from Python by its id:

```python title="teardown_agent.py"
import asyncio

from agent_env.a2a_agent.store import get_a2a_agent_instance_store
from agent_env.providers.sandbox_providers.sandbox_provider import build_sandbox_provider


async def main() -> None:
    deployed = get_a2a_agent_instance_store().get("claudius-4msj7n4d")
    sandbox = await build_sandbox_provider(deployed.sandbox_type).get_sandbox(deployed.sandbox_id)
    await sandbox.terminate()


asyncio.run(main())
```

In a [task](https://www.agentenvframework.com/docs/tasks.md), a `teardown_sandboxes` step closes the instances of the agents it names.