Chapter 27
Modal demo — Self-Hosted Sandboxes
Modal demo — Self-Hosted Sandboxes
Reference implementation of the usage guide's webhook flow on Modal. Two files:
modal_sandbox_webhook.py— Modal app that receives thesession.status_run_startedwebhook, verifies it withclient.beta.webhooks.unwrap(), drains the environment work queue withclient.beta.environments.work.poller(drain=True, auto_stop=False), and spins up a per-session Modal Sandbox per item. A per-sessionmodal.Volumeis mounted at/workspaceso the agent's working tree and downloaded skills persist across sandbox restarts for the same session. The sandbox env vars use the sameANTHROPIC_*contract asant beta:worker poll --on-work.sandbox_runner.py— runs inside that Sandbox:client.beta.environments.work.worker(environment_key=..., workdir="/workspace", unrestricted_paths=True).handle_item(). It reads theANTHROPIC_*env vars, builds the per-sessionAgentToolContextand downloads the agent's skills into/workspace/skills/<name>/, runs aSessionToolRunner(heartbeat + reconcile + event stream +bash/read/write/edit/glob/grepdispatch + result posting), and force-stops the work item on exit. Idle policy is the SDK default: it exits 60s aftersession.status_idlewithstop_reason: end_turn; any other event resets the clock.
No org API key anywhere: the webhook polls with the environment key, and the runner authenticates with that same environment key — the single credential for both the control plane and the per-session calls.
Prerequisites
pip install modal
modal setup # auth to your Modal workspaceConfigure
modal secret create cma-self-hosted-sandboxes-secrets \
ANTHROPIC_WEBHOOK_SECRET=placeholder \
ANTHROPIC_ENVIRONMENT_ID='env_...' \
ANTHROPIC_ENVIRONMENT_KEY='sk-ant-oat...'Deploy
modal deploy modal_sandbox_webhook.pyThis prints a *.modal.run URL. Register that URL as a webhook for session.status_run_started in Console (or via the API), copy the issued secret, then update:
modal secret create cma-self-hosted-sandboxes-secrets \
ANTHROPIC_WEBHOOK_SECRET='whsec_...' \
ANTHROPIC_ENVIRONMENT_ID='env_...' \
ANTHROPIC_ENVIRONMENT_KEY='sk-ant-oat...' \
--force(no redeploy needed — secrets are read at container start.)
Test
Create a session pointing at your environment id and send it a message:
session = client.beta.sessions.create(agent=agent_id, environment_id=ENVIRONMENT_ID)
client.beta.sessions.events.send(session.id, events=[{"type": "user.message", "content": "ls -la"}])You should see, in order:
modal app logs cma-self-hosted-sandboxes
# [webhook] event=session.status_run_started session_id=...
# [webhook] acked work=... session=... sandbox=sb-... (created)Sandbox stdout (the [runner] lines) shows in the Modal dashboard under
Apps → cma-self-hosted-sandboxes → Sandboxes.
Iterating
Editing either Python file requires a redeploy (modal deploy ...). Editing only secrets does not. To force a clean slate while iterating, modal app stop cma-self-hosted-sandboxes before redeploying.
