This is the implementation checklist for wiring Anthropic Managed Agents self-hosted environments
to E2B in Python. The local example implements these functions under
anthropic_managed_agents_e2b/.
Implement load_settings().
It should load local environment variables and return a typed settings object with:
| Setting | Required for |
|---|---|
ANTHROPIC_API_KEY |
Creating agents, creating environments, sending sessions, updating environment metadata. |
ANTHROPIC_AGENT_ID |
Sending a session message. |
ANTHROPIC_ENVIRONMENT_ID |
Starting workers, sending sessions, metadata lookup. |
ANTHROPIC_ENVIRONMENT_KEY |
Running the self-hosted environment worker. |
ANTHROPIC_WEBHOOK_SIGNING_KEY |
Verifying real webhook deliveries. |
The example reads the repository root .env first, then the example-local .env.
Implement create_self_hosted_environment(api_key, name).
It should call:
client.beta.environments.create(
name=name,
config={"type": "self_hosted"},
)Print the returned env.id as ANTHROPIC_ENVIRONMENT_ID, then send the user to
Anthropic Environments to generate
ANTHROPIC_ENVIRONMENT_KEY.
Implement create_agent(api_key, name, model).
It should create a Managed Agent with:
- the target model
- a system prompt that says
/mnt/sessionis the sandbox workdir - Anthropic's
agent_toolset_20260401 - enabled tools:
bash,read,write,edit,glob,grep,web_fetch,web_search
For this cookbook example, the tool permission policy is always_allow so the smoke flow can run
without an approval UI.
Implement worker_template() and build_template(template_name).
The template should:
- start from Python 3.12
- install shell utilities used by the Anthropic toolset
- install
anthropic[webhooks],fastapi, anduvicorn - copy the worker and webhook runtime modules into
/opt/anthropic-managed-agents - create writable
/mnt/session - set
/mnt/sessionas the default workdir
build_template(template_name) should call E2B's Template.build(...) with that template.
Implement start_worker_sandbox(settings, template_name, timeout_seconds, worker_max_idle_seconds, log_level, sandbox_id).
It should:
- Require
ANTHROPIC_ENVIRONMENT_IDandANTHROPIC_ENVIRONMENT_KEY. - Connect to
sandbox_idif provided, otherwise create a new E2B sandbox fromtemplate_name. - Upload or refresh the worker runtime files.
- Start the worker process in the background inside
/mnt/session. - Write the worker pid to
/opt/anthropic-managed-agents/worker.pid. - Write logs to
/opt/anthropic-managed-agents/worker.log. - Update Anthropic environment metadata:
e2b_worker_sandbox_id=<sandbox id>
e2b_worker_sandbox_ids=["<sandbox id>", ...]
The process environment passed to the worker must include:
ANTHROPIC_ENVIRONMENT_ID
ANTHROPIC_ENVIRONMENT_KEY
WORKER_MAX_IDLE_SECONDS
LOG_LEVEL
Implement run_worker().
It should run Anthropic's SDK worker:
async with AsyncAnthropic(auth_token=environment_key) as client:
worker = client.beta.environments.work.worker(
environment_id=environment_id,
environment_key=environment_key,
workdir="/mnt/session",
max_idle=max_idle_seconds(),
)
await worker.run()This is the core handoff. Anthropic's SDK owns polling, claiming work, heartbeating, dispatching
tool calls, and sending tool results back to the session.
Leaving unrestricted_paths unset keeps file tools constrained to the worker workdir.
Implement start_webhook_server_sandbox(settings, template_name, timeout_seconds, worker_max_idle_seconds, log_level, port, sandbox_id).
It should:
- Require
ANTHROPIC_ENVIRONMENT_IDandANTHROPIC_ENVIRONMENT_KEY. - Connect to
sandbox_idif provided, otherwise create an E2B sandbox with:
lifecycle={"on_timeout": "pause", "auto_resume": True}- Upload or refresh the worker and webhook runtime files.
- Start the webhook server in the background.
- Print
https://<sandbox-host>/webhook. - Update Anthropic environment metadata:
e2b_webhook_sandbox_id=<sandbox id>
e2b_webhook_sandbox_ids=["<sandbox id>", ...]
Implement webhook(request).
It should:
- Return
503whenANTHROPIC_WEBHOOK_SIGNING_KEYis not configured. - Read the raw request body with a fixed size limit.
- Verify the payload with:
event = client.beta.webhooks.unwrap(
payload,
headers=dict(request.headers),
key=signing_key,
)- If
event.data.type == "session.status_run_started", start a worker process when capacity is available. - Return
204for accepted webhook deliveries.
Also implement health() so setup can confirm the webhook sandbox is serving HTTP and see the
current worker count. The webhook sandbox should support file-backed config in /mnt/session so a
resumed sandbox can receive updated environment keys, signing keys, idle timeout, and log level
without rebuilding the template.
Implement app_webhook_server.py when webhooks should land on your application instead of inside
an E2B sandbox.
It should:
- Receive
POST /webhookin the app process. - Verify the raw Anthropic webhook payload with
client.beta.webhooks.unwrap(...). - Wake an app-side drain of the self-hosted environment work queue.
- For each claimed session work item, compute the sandbox routing key from
APP_SANDBOX_ROUTING_SCOPE. - Reconnect to that key's sandbox and start
worker.handle_item()with the claimed work id, or create a fresh worker sandbox when the assignment is missing or stale.
This keeps webhook policy, routing, observability, and sandbox replacement under app control while
still using the same E2B worker runtime. Add GET /sandboxes so operators can inspect the current
session-to-sandbox assignments behind an app-owned bearer token. Do not start a normal
environment-polling worker inside the session sandbox; it can claim a different queued session.
Implement stream_message(api_key, agent_id, environment_id, message).
It should:
- Create a session with
agentandenvironment_id. - Open a session event stream.
- Send a
user.message. - Print streamed events.
- Stop when the stream reaches
session.status_idlewithstop_reason.type == "end_turn".
Anthropic session resources are not available for self-hosted environments. For this E2B pattern,
upload files through E2B before sending the session message:
from pathlib import Path
from e2b import Sandbox
def upload_file_to_sandbox(sandbox_id: str, local_path: Path, remote_path: str):
sandbox = Sandbox.connect(sandbox_id)
sandbox.files.write(remote_path, local_path.read_bytes())
return remote_pathThen ask the agent to read the remote path, for example
/mnt/session/uploads/example-input.txt.
Implement:
retrieve_environment(api_key, environment_id)update_environment_metadata(api_key, environment_id, metadata)clear_matching_sandbox_metadata(api_key, environment_id, sandbox_id)upload_file_to_sandbox(sandbox_id, local_path, remote_path)show_environment_main()
show_environment_main() should print:
ANTHROPIC_ENVIRONMENT_ID=...
name=...
e2b_worker_sandbox_id=...
e2b_worker_sandbox_ids=...
e2b_webhook_sandbox_id=...
e2b_webhook_sandbox_ids=...
stop_worker_sandbox(settings, sandbox_id) should kill the E2B sandbox and clear
e2b_worker_sandbox_id or e2b_webhook_sandbox_id only when the stored value matches the sandbox
being stopped. It should also remove the sandbox id from the matching JSON metadata list.
That gives another process a simple lookup path:
ANTHROPIC_ENVIRONMENT_ID -> environment.metadata.e2b_*_sandbox_ids -> E2B sandbox ids