# Managed Agents - Self-Hosted Sandboxes With `config.type: "self_hosted"`, the **agent loop stays on Anthropic's orchestration layer** but **tool execution moves to infrastructure you control** - bash, file ops, and code run inside your container, so filesystem contents and the sandbox's network egress never leave your environment. (`web_search` / `web_fetch` are the exception: they run on Anthropic's servers in both environment types - restrict them with `allowed_domains` / `blocked_domains` in the agent toolset, `shared/managed-agents-tools.md` § Web search & web fetch settings.) Tool inputs/outputs still flow to Anthropic's control plane so the model can see results; the agent's skills and the contents of any attached memory stores are stored by Anthropic and copied into your sandbox for the session (memory changes sync back - see § Memory stores). Contrast with `config.type: "cloud"`, where Anthropic runs the container. Connectivity is **outbound-only**: your worker long-polls Anthropic's work queue; Anthropic never dials into your network. ## Flow ``` 1. Create environment: config: {type: "self_hosted"} -> env_... 2. Generate environment key (Console, on the environment page) -> sk-ant-oat01-... as ANTHROPIC_ENVIRONMENT_KEY 3. Run a worker: EnvironmentWorker.run() or ant beta:worker poll 4. Sessions reference environment_id=env_... exactly as for cloud ``` ## Create the environment ```python client = anthropic.Anthropic() environment = client.beta.environments.create( name="self-hosted", config={"type": "self_hosted"} ) ``` `{"type": "self_hosted"}` is the entire config - there are no pool, capacity, or networking sub-fields; you control those on your side. ## Run a worker - SDK (primary path) `EnvironmentWorker` wraps the poll -> dispatch -> tool-execute loop. `.run()` is the always-on loop (loops until cancelled). `.handle_item()` / `.handleItem()` / `.HandleItem()` services **one already-claimed** work item without polling - IDs fall back to `ANTHROPIC_WORK_ID` / `ANTHROPIC_ENVIRONMENT_ID` / `ANTHROPIC_SESSION_ID`, the key to the worker's own `environment_key` and then `ANTHROPIC_ENVIRONMENT_KEY`, and the per-session secret to `ANTHROPIC_WORK_SECRET`, so inside an `ant beta:worker poll --on-work` container it needs no arguments. It ignores (and force-stops) non-session work items itself. There is no `run_one()`; claiming is done by `.run()` or by the mid-level poller (below). **Python - always-on:** ```python import asyncio import contextlib import os import signal from anthropic import AsyncAnthropic from anthropic.lib.environments import EnvironmentWorker async def main() -> None: environment_key = os.environ["ANTHROPIC_ENVIRONMENT_KEY"] environment_id = os.environ["ANTHROPIC_ENVIRONMENT_ID"] async with AsyncAnthropic(auth_token=environment_key) as client: worker = EnvironmentWorker( client, environment_id=environment_id, environment_key=environment_key, workdir="/workspace", ) task = asyncio.create_task(worker.run()) # Cancel the task (don't kill the process): the worker stops its in-flight # work item and uploads changed memory files before exiting. loop = asyncio.get_running_loop() for signum in (signal.SIGINT, signal.SIGTERM): loop.add_signal_handler(signum, task.cancel) with contextlib.suppress(asyncio.CancelledError): await task asyncio.run(main()) ``` **TypeScript - always-on:** ```typescript import Anthropic from "@anthropic-ai/sdk"; import { EnvironmentWorker } from "@anthropic-ai/sdk/helpers/beta/environments"; const environmentKey = process.env.ANTHROPIC_ENVIRONMENT_KEY!; const environmentId = process.env.ANTHROPIC_ENVIRONMENT_ID!; const client = new Anthropic({ authToken: environmentKey }); const ctrl = new AbortController(); process.once("SIGTERM", () => ctrl.abort()); process.once("SIGINT", () => ctrl.abort()); await new EnvironmentWorker({ client, environmentId, environmentKey, workdir: "/workspace", signal: ctrl.signal }).run(); ``` **Customizing tools.** `EnvironmentWorker` runs the built-in toolset by default. To add or replace tools, use `AgentToolContext(workdir=, client=, session_id=)` with `beta_agent_toolset(env)` / `betaAgentToolset(env)` and pass the resulting tools to the lower-level `tool_runner()`. Skills attached to the agent are downloaded into `{workdir}/skills//` before tool calls begin (`AgentToolContext` handles this when given `client` and `session_id`). Downloaded skill files are marked executable automatically by the CLI and SDK; if you implement skills download yourself, you set permissions. > **Runtime deps:** the SDK helpers require `/bin/bash` at that exact path (not consulted via `PATH`). The TypeScript SDK additionally requires `unzip` and `tar` on `PATH` and Node.js 22+; Python and Go use their standard libraries for archive extraction. Memory stores additionally need a POSIX host (Linux or macOS - not Windows, the worker opens memory files with `O_NOFOLLOW`) with a writable `/mnt/memory` - see § Memory stores. **File-tool confinement.** `AgentToolContext` confines `read`/`write`/`edit`/`glob`/`grep` to the working directory plus `allowed_roots` (`allowedRoots` / `AllowedRoots`); `write` and `edit` also refuse paths under `read_only_roots` (`readOnlyRoots` / `ReadOnlyRoots`). `EnvironmentWorker` adds the session's memory store directories to these lists itself. This is a guardrail for the file tools only - it does **not** constrain `bash`. The old `unrestricted_paths` option is no longer accepted (passing it raises); add directories to `allowed_roots` instead. ## Run a worker - `ant` CLI (fixed tools) The `ant` CLI ships a worker with the fixed built-in toolset (`bash`, `read`, `write`, `edit`, `glob`, `grep`). Install per `shared/anthropic-cli.md`, then: ```sh export ANTHROPIC_ENVIRONMENT_KEY=sk-ant-oat01-... ant beta:worker poll --environment-id env_... --workdir /workspace ``` - `--workdir` is the directory tools operate in (default `.`); tool calls are sandboxed to it. - `--environment-key` overrides the env var. - `--on-work