@preapexis/piexis 0.2.2 → 0.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +85 -131
- package/apps/server/src/index.ts +97 -0
- package/apps/server/src/session-manager.ts +95 -0
- package/apps/server/src/workspace.ts +39 -0
- package/{tsconfig.json → apps/server/tsconfig.json} +3 -3
- package/apps/web/dist/assets/index-D4n3_lAw.js +10 -0
- package/apps/web/dist/assets/index-igiK0FPY.css +1 -0
- package/apps/web/dist/index.html +3 -0
- package/bin/piexis-web.mjs +24 -0
- package/extensions/agents.ts +205 -0
- package/extensions/ask-question.ts +54 -95
- package/extensions/recommended-tools.ts +62 -0
- package/extensions/ui.ts +204 -58
- package/extensions/webui.ts +45 -0
- package/lib/agent-config.ts +55 -0
- package/lib/agents.ts +208 -0
- package/lib/question-model.ts +98 -0
- package/lib/question-ui.ts +214 -0
- package/lib/recommended-tools.ts +119 -0
- package/lib/subagent-runner.ts +146 -0
- package/lib/ui-tips.json +41 -0
- package/package.json +41 -14
- package/packages/pi-integration/src/rpc-client.ts +99 -0
- package/packages/shared/src/index.ts +27 -0
- package/prompts/init.md +10 -2
- package/prompts/summary.md +7 -6
- package/schemas/agent.schema.json +48 -0
- package/skills/{claude-api/LICENSE.txt → caveman/LICENSE} +2 -2
- package/skills/caveman/LICENSE-MIT +21 -0
- package/skills/caveman/NOTICE +10 -0
- package/skills/caveman/SKILL.md +89 -0
- package/skills/find-skills/SKILL.md +141 -0
- package/skills/ponytail/LICENSE +21 -0
- package/skills/ponytail/NOTICE +9 -0
- package/skills/ponytail/SKILL.md +52 -0
- package/themes/midnight.json +81 -0
- package/.github/workflows/publish.yml +0 -35
- package/AGENTS.md +0 -58
- package/eval/README.md +0 -72
- package/eval/permission-cases.json +0 -28
- package/eval/permissions.mjs +0 -103
- package/extensions/background-tasks.ts +0 -433
- package/extensions/background-worker.ts +0 -50
- package/extensions/glob.ts +0 -32
- package/extensions/mode.ts +0 -397
- package/lib/bash-approvals.ts +0 -113
- package/lib/bash-policy.ts +0 -164
- package/lib/mode-policy.ts +0 -174
- package/lib/path-policy.ts +0 -182
- package/lib/permission-classifier.ts +0 -128
- package/lib/permission-config.ts +0 -91
- package/lib/permission-denials.ts +0 -32
- package/lib/permission-intent.ts +0 -128
- package/lib/permission-taxonomy.ts +0 -15
- package/lib/task-broker.ts +0 -65
- package/lib/task-checkout.ts +0 -39
- package/lib/task-git.ts +0 -44
- package/lib/task-patch.ts +0 -106
- package/lib/task-permissions.ts +0 -51
- package/lib/task-report.ts +0 -62
- package/lib/task-worktree-policy.ts +0 -56
- package/scripts/release.mjs +0 -32
- package/skills/claude-api/SKILL.md +0 -572
- package/skills/claude-api/csharp/claude-api/README.md +0 -362
- package/skills/claude-api/csharp/claude-api/batches.md +0 -14
- package/skills/claude-api/csharp/claude-api/files-api.md +0 -25
- package/skills/claude-api/csharp/claude-api/streaming.md +0 -28
- package/skills/claude-api/csharp/claude-api/tool-use.md +0 -164
- package/skills/claude-api/curl/examples.md +0 -256
- package/skills/claude-api/curl/managed-agents.md +0 -371
- package/skills/claude-api/go/claude-api/README.md +0 -188
- package/skills/claude-api/go/claude-api/files-api.md +0 -23
- package/skills/claude-api/go/claude-api/streaming.md +0 -43
- package/skills/claude-api/go/claude-api/tool-use.md +0 -220
- package/skills/claude-api/go/managed-agents/README.md +0 -564
- package/skills/claude-api/java/claude-api/README.md +0 -238
- package/skills/claude-api/java/claude-api/files-api.md +0 -27
- package/skills/claude-api/java/claude-api/streaming.md +0 -24
- package/skills/claude-api/java/claude-api/tool-use.md +0 -228
- package/skills/claude-api/java/managed-agents/README.md +0 -443
- package/skills/claude-api/php/claude-api/README.md +0 -174
- package/skills/claude-api/php/claude-api/batches.md +0 -15
- package/skills/claude-api/php/claude-api/files-api.md +0 -13
- package/skills/claude-api/php/claude-api/streaming.md +0 -27
- package/skills/claude-api/php/claude-api/tool-use.md +0 -253
- package/skills/claude-api/php/managed-agents/README.md +0 -443
- package/skills/claude-api/python/claude-api/README.md +0 -565
- package/skills/claude-api/python/claude-api/batches.md +0 -198
- package/skills/claude-api/python/claude-api/files-api.md +0 -170
- package/skills/claude-api/python/claude-api/sdk-upgrade.md +0 -286
- package/skills/claude-api/python/claude-api/streaming.md +0 -179
- package/skills/claude-api/python/claude-api/tool-use.md +0 -627
- package/skills/claude-api/python/managed-agents/README.md +0 -335
- package/skills/claude-api/ruby/claude-api/README.md +0 -134
- package/skills/claude-api/ruby/claude-api/streaming.md +0 -16
- package/skills/claude-api/ruby/claude-api/tool-use.md +0 -41
- package/skills/claude-api/ruby/managed-agents/README.md +0 -394
- package/skills/claude-api/shared/admin-api.md +0 -179
- package/skills/claude-api/shared/agent-design.md +0 -101
- package/skills/claude-api/shared/anthropic-cli.md +0 -269
- package/skills/claude-api/shared/claude-platform-on-aws.md +0 -59
- package/skills/claude-api/shared/cost-optimization.md +0 -233
- package/skills/claude-api/shared/error-codes.md +0 -262
- package/skills/claude-api/shared/live-sources.md +0 -166
- package/skills/claude-api/shared/managed-agents-api-reference.md +0 -455
- package/skills/claude-api/shared/managed-agents-client-patterns.md +0 -220
- package/skills/claude-api/shared/managed-agents-core.md +0 -359
- package/skills/claude-api/shared/managed-agents-environments.md +0 -226
- package/skills/claude-api/shared/managed-agents-events.md +0 -290
- package/skills/claude-api/shared/managed-agents-memory.md +0 -201
- package/skills/claude-api/shared/managed-agents-multiagent.md +0 -260
- package/skills/claude-api/shared/managed-agents-onboarding.md +0 -82
- package/skills/claude-api/shared/managed-agents-outcomes.md +0 -108
- package/skills/claude-api/shared/managed-agents-overview.md +0 -77
- package/skills/claude-api/shared/managed-agents-scheduled-deployments.md +0 -156
- package/skills/claude-api/shared/managed-agents-self-hosted-sandboxes.md +0 -282
- package/skills/claude-api/shared/managed-agents-tools.md +0 -504
- package/skills/claude-api/shared/managed-agents-webhooks.md +0 -143
- package/skills/claude-api/shared/model-migration.md +0 -1872
- package/skills/claude-api/shared/models.md +0 -142
- package/skills/claude-api/shared/platform-availability.md +0 -53
- package/skills/claude-api/shared/prompt-audit.md +0 -223
- package/skills/claude-api/shared/prompt-caching.md +0 -292
- package/skills/claude-api/shared/token-counting.md +0 -56
- package/skills/claude-api/shared/tool-use-concepts.md +0 -526
- package/skills/claude-api/typescript/claude-api/README.md +0 -405
- package/skills/claude-api/typescript/claude-api/batches.md +0 -106
- package/skills/claude-api/typescript/claude-api/files-api.md +0 -98
- package/skills/claude-api/typescript/claude-api/streaming.md +0 -178
- package/skills/claude-api/typescript/claude-api/tool-use.md +0 -586
- package/skills/claude-api/typescript/managed-agents/README.md +0 -359
- package/skills-lock.json +0 -359
- package/tests/auto-permissions.test.mjs +0 -315
- package/tests/background-tasks.test.mjs +0 -27
- package/tests/background-worker.test.mjs +0 -60
- package/tests/bash-permissions.test.mjs +0 -276
- package/tests/glob.test.mjs +0 -31
- package/tests/mode.test.mjs +0 -62
- package/tests/permission-evaluation.test.mjs +0 -70
- package/tests/permission-foundation.test.mjs +0 -194
- package/tests/permission-policy.test.mjs +0 -158
- package/tests/runtime/README.md +0 -51
- package/tests/runtime/harness.mjs +0 -190
- package/tests/runtime/permissions.test.mjs +0 -192
- package/tests/runtime/tui.test.mjs +0 -60
- package/tests/skills-routing.test.mjs +0 -51
- package/tests/task-integration.test.mjs +0 -324
- package/tests/task-permissions.test.mjs +0 -178
- package/tests/task-worktree-policy.test.mjs +0 -53
|
@@ -1,282 +0,0 @@
|
|
|
1
|
-
# Managed Agents - Self-Hosted Sandboxes
|
|
2
|
-
|
|
3
|
-
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.
|
|
4
|
-
|
|
5
|
-
## Flow
|
|
6
|
-
|
|
7
|
-
```
|
|
8
|
-
1. Create environment: config: {type: "self_hosted"} -> env_...
|
|
9
|
-
2. Generate environment key (Console, on the environment page) -> sk-ant-oat01-... as ANTHROPIC_ENVIRONMENT_KEY
|
|
10
|
-
3. Run a worker: EnvironmentWorker.run() or ant beta:worker poll
|
|
11
|
-
4. Sessions reference environment_id=env_... exactly as for cloud
|
|
12
|
-
```
|
|
13
|
-
|
|
14
|
-
## Create the environment
|
|
15
|
-
|
|
16
|
-
```python
|
|
17
|
-
client = anthropic.Anthropic()
|
|
18
|
-
|
|
19
|
-
environment = client.beta.environments.create(
|
|
20
|
-
name="self-hosted", config={"type": "self_hosted"}
|
|
21
|
-
)
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
`{"type": "self_hosted"}` is the entire config - there are no pool, capacity, or networking sub-fields; you control those on your side.
|
|
25
|
-
|
|
26
|
-
## Run a worker - SDK (primary path)
|
|
27
|
-
|
|
28
|
-
`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).
|
|
29
|
-
|
|
30
|
-
**Python - always-on:**
|
|
31
|
-
|
|
32
|
-
```python
|
|
33
|
-
import asyncio
|
|
34
|
-
import contextlib
|
|
35
|
-
import os
|
|
36
|
-
import signal
|
|
37
|
-
from anthropic import AsyncAnthropic
|
|
38
|
-
from anthropic.lib.environments import EnvironmentWorker
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
async def main() -> None:
|
|
42
|
-
environment_key = os.environ["ANTHROPIC_ENVIRONMENT_KEY"]
|
|
43
|
-
environment_id = os.environ["ANTHROPIC_ENVIRONMENT_ID"]
|
|
44
|
-
async with AsyncAnthropic(auth_token=environment_key) as client:
|
|
45
|
-
worker = EnvironmentWorker(
|
|
46
|
-
client,
|
|
47
|
-
environment_id=environment_id,
|
|
48
|
-
environment_key=environment_key,
|
|
49
|
-
workdir="/workspace",
|
|
50
|
-
)
|
|
51
|
-
task = asyncio.create_task(worker.run())
|
|
52
|
-
# Cancel the task (don't kill the process): the worker stops its in-flight
|
|
53
|
-
# work item and uploads changed memory files before exiting.
|
|
54
|
-
loop = asyncio.get_running_loop()
|
|
55
|
-
for signum in (signal.SIGINT, signal.SIGTERM):
|
|
56
|
-
loop.add_signal_handler(signum, task.cancel)
|
|
57
|
-
with contextlib.suppress(asyncio.CancelledError):
|
|
58
|
-
await task
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
asyncio.run(main())
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
**TypeScript - always-on:**
|
|
65
|
-
|
|
66
|
-
```typescript
|
|
67
|
-
import Anthropic from "@anthropic-ai/sdk";
|
|
68
|
-
import { EnvironmentWorker } from "@anthropic-ai/sdk/helpers/beta/environments";
|
|
69
|
-
|
|
70
|
-
const environmentKey = process.env.ANTHROPIC_ENVIRONMENT_KEY!;
|
|
71
|
-
const environmentId = process.env.ANTHROPIC_ENVIRONMENT_ID!;
|
|
72
|
-
const client = new Anthropic({ authToken: environmentKey });
|
|
73
|
-
const ctrl = new AbortController();
|
|
74
|
-
process.once("SIGTERM", () => ctrl.abort());
|
|
75
|
-
process.once("SIGINT", () => ctrl.abort());
|
|
76
|
-
|
|
77
|
-
await new EnvironmentWorker({
|
|
78
|
-
client,
|
|
79
|
-
environmentId,
|
|
80
|
-
environmentKey,
|
|
81
|
-
workdir: "/workspace",
|
|
82
|
-
signal: ctrl.signal
|
|
83
|
-
}).run();
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
**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/<name>/` 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.
|
|
87
|
-
|
|
88
|
-
> **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.
|
|
89
|
-
|
|
90
|
-
**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.
|
|
91
|
-
|
|
92
|
-
## Run a worker - `ant` CLI (fixed tools)
|
|
93
|
-
|
|
94
|
-
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:
|
|
95
|
-
|
|
96
|
-
```sh
|
|
97
|
-
export ANTHROPIC_ENVIRONMENT_KEY=sk-ant-oat01-...
|
|
98
|
-
ant beta:worker poll --environment-id env_... --workdir /workspace
|
|
99
|
-
```
|
|
100
|
-
|
|
101
|
-
- `--workdir` is the directory tools operate in (default `.`); tool calls are sandboxed to it.
|
|
102
|
-
- `--environment-key` overrides the env var.
|
|
103
|
-
- `--on-work <script>` runs your script per work item (e.g. to spin a fresh container per session - see Container orchestration below).
|
|
104
|
-
- `--unrestricted-paths`, `--max-idle` (default `60s`), `--log-format` - see `ant beta:worker poll --help`.
|
|
105
|
-
- Flags fall back to env vars (`ANTHROPIC_ENVIRONMENT_ID`, `ANTHROPIC_ENVIRONMENT_KEY`).
|
|
106
|
-
- Exits cleanly on SIGTERM/SIGINT after draining in-flight work.
|
|
107
|
-
- **Fixed toolset** - for custom tools, use the SDK worker above.
|
|
108
|
-
- **Does not mount memory stores.** A session that attaches one still runs, but the agent finds nothing at the store's `/mnt/memory/<store-name>/` directory and nothing syncs back. To combine the CLI poller with memory stores, keep `ant beta:worker poll --on-work` on the host and run the **SDK** worker (`EnvironmentWorker.handle_item()`) inside the per-session sandbox - see § Memory stores -> Sandbox-per-session.
|
|
109
|
-
|
|
110
|
-
Inside an `--on-work` container, run `ant beta:worker run --workdir <dir>` as the entrypoint (or the SDK worker, if the session needs memory stores).
|
|
111
|
-
|
|
112
|
-
## Webhook-driven wake (instead of always-on)
|
|
113
|
-
|
|
114
|
-
Register a webhook for `session.status_run_started` (see `shared/managed-agents-webhooks.md`), verify the delivery, then **drain** the queue with the poller (`drain=True` stops when it's empty; `block_ms=None` is non-blocking; `auto_stop=False` because `handle_item` force-stops the item itself) and hand each claimed item to `handle_item()`. **Don't `await` the drain inside the HTTP handler** - a session run outlives the webhook delivery timeout, so acknowledge the delivery and run the drain as a background task (`asyncio.create_task` / a detached promise / a goroutine off `context.Background()`), keeping the process alive until it finishes:
|
|
115
|
-
|
|
116
|
-
```python
|
|
117
|
-
import asyncio
|
|
118
|
-
import os
|
|
119
|
-
import anthropic
|
|
120
|
-
|
|
121
|
-
environment_key = os.environ["ANTHROPIC_ENVIRONMENT_KEY"]
|
|
122
|
-
environment_id = os.environ["ANTHROPIC_ENVIRONMENT_ID"]
|
|
123
|
-
client = anthropic.AsyncAnthropic(
|
|
124
|
-
auth_token=environment_key,
|
|
125
|
-
) # reads ANTHROPIC_WEBHOOK_SIGNING_KEY from env for webhooks.unwrap()
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
async def handle(raw: bytes, headers: dict[str, str]) -> dict:
|
|
129
|
-
event = client.beta.webhooks.unwrap(raw.decode(), headers=headers)
|
|
130
|
-
if event.data.type != "session.status_run_started":
|
|
131
|
-
return {"status": "ignored"}
|
|
132
|
-
asyncio.create_task(drain()) # keep a reference if your framework may GC it
|
|
133
|
-
return {"status": "accepted"}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
async def drain() -> None:
|
|
137
|
-
async for work in client.beta.environments.work.poller(
|
|
138
|
-
environment_id=environment_id,
|
|
139
|
-
environment_key=environment_key,
|
|
140
|
-
block_ms=None,
|
|
141
|
-
reclaim_older_than_ms=2000,
|
|
142
|
-
drain=True,
|
|
143
|
-
auto_stop=False,
|
|
144
|
-
):
|
|
145
|
-
await client.beta.environments.work.worker(workdir="/workspace").handle_item(
|
|
146
|
-
work_id=work.id,
|
|
147
|
-
environment_id=environment_id,
|
|
148
|
-
session_id=work.data.id,
|
|
149
|
-
environment_key=environment_key,
|
|
150
|
-
work_secret=work.secret, # lets the worker mount the session's memory stores
|
|
151
|
-
)
|
|
152
|
-
```
|
|
153
|
-
|
|
154
|
-
TypeScript: same shape with `client.beta.webhooks.unwrap(body, {headers})`, `client.beta.environments.work.poller({environmentId, environmentKey, blockMs: null, reclaimOlderThanMs: 2000, drain: true, autoStop: false})`, and `client.beta.environments.work.worker({workdir}).handleItem({workId, environmentId, sessionId, environmentKey, workSecret: work.secret})`. Go: no `RunOne` convenience either - `environments.NewWorkPoller(ctx, client, environments.WorkPollerOptions{EnvironmentID, EnvironmentKey, BlockMs: param.Null[int64](), ReclaimOlderThanMs: param.NewOpt[int64](2000), Drain: true, AutoStop: param.NewOpt(false)})`, then `worker.HandleItem(ctx, environments.HandleItemOptions{WorkID: item.ID, EnvironmentID: item.EnvironmentID, SessionID: item.Data.ID, EnvironmentKey, WorkSecret: item.Secret})` per `poller.Next()` item, in a goroutine off `context.Background()`. Always pass the work item's `secret` through, or sessions with memory stores fail at claim time. `handle_item` skips non-session work items itself, so the drain loop needs no `work.data.type` check.
|
|
155
|
-
|
|
156
|
-
## Container orchestration (mid-level)
|
|
157
|
-
|
|
158
|
-
`EnvironmentWorker.run()` polls and executes tools in the same process. To run each session in its **own** container, use the mid-level poller in a thin orchestrator - Python `client.beta.environments.work.poller(environment_id=, environment_key=, drain=, block_ms=, reclaim_older_than_ms=, auto_stop=)`; TypeScript `new WorkPoller({client, environmentId, environmentKey, autoStop})` from `@anthropic-ai/sdk/helpers/beta/environments` - and, for each yielded `work` item, start a fresh container with these env vars injected, whose entrypoint runs `ant beta:worker run` or an `EnvironmentWorker(...).handle_item()` (required if the session attaches memory stores). `block_ms` is 1-999 (or `None` for non-blocking); `reclaim_older_than_ms` re-claims items leased to a dead worker; `drain` stops once the queue is empty; `auto_stop` posts a stop signal after the iterator exits (set `False` when the launched container owns the stop call). Go: `environments.NewWorkPoller(ctx, client, environments.WorkPollerOptions{EnvironmentID, EnvironmentKey, BlockMs, ReclaimOlderThanMs, Drain, AutoStop: param.NewOpt(false)})` with `poller.Next()` / `poller.Current()` / `poller.Err()`.
|
|
159
|
-
|
|
160
|
-
| Env var | Value |
|
|
161
|
-
|---|---|
|
|
162
|
-
| `ANTHROPIC_SESSION_ID` | `work.data.id` |
|
|
163
|
-
| `ANTHROPIC_WORK_ID` | `work.id` |
|
|
164
|
-
| `ANTHROPIC_ENVIRONMENT_ID` | `work.environment_id` |
|
|
165
|
-
| `ANTHROPIC_ENVIRONMENT_KEY` | pass through |
|
|
166
|
-
| `ANTHROPIC_BASE_URL` | pass through |
|
|
167
|
-
| `ANTHROPIC_WORK_SECRET` | `work.secret` - the per-session credential the worker inside needs to mount memory stores. `ant beta:worker poll --on-work` does **not** set it for the spawned script; read it from the work-item JSON on stdin (`jq -r '.secret // empty'`) and pass it in. Only into the sandbox serving that session; never log it. |
|
|
168
|
-
|
|
169
|
-
Skip items where `work.data.type != "session"` when you dispatch containers yourself (`handle_item` does this check for you).
|
|
170
|
-
|
|
171
|
-
## Memory stores
|
|
172
|
-
|
|
173
|
-
Sessions on a self-hosted environment attach memory stores exactly like cloud sessions - `resources=[{"type": "memory_store", "memory_store_id": ..., "access": ...}]` at session create, up to 8 per session (see `shared/managed-agents-memory.md`). The difference is *who materializes them*: on cloud, Anthropic mounts a live FUSE filesystem; on self-hosted, the **SDK worker** (`EnvironmentWorker`, or its `handle_item()` / `handleItem()` / `HandleItem()`) downloads a working copy and syncs it. Requires the Python, TypeScript, or Go SDK; the `ant` CLI worker and the C#/Java/PHP/Ruby SDKs don't mount stores. Not available on Claude Platform on AWS.
|
|
174
|
-
|
|
175
|
-
**What the worker does** when it claims a work item whose session has stores attached:
|
|
176
|
-
|
|
177
|
-
1. Downloads each store to its mount path under `/mnt/memory/` - derived from the store's name, not a settable field (e.g. `/mnt/memory/user-preferences/` for a store named "User Preferences"); the same path cloud sessions use, and the session's system prompt describes it to the agent. Authenticates with the work item's per-session `secret`.
|
|
178
|
-
2. Adds those directories to the file tools' `allowed_roots`, and `access: "read_only"` stores to `read_only_roots`, so the agent uses the ordinary `read`/`write`/`edit`/`glob`/`grep` tools on memories.
|
|
179
|
-
3. Reconciles after tool calls, at most once per sync interval (default 15 s): remote changes are written to disk, files the agent changed are uploaded.
|
|
180
|
-
4. On session end: final sync, flushes pending uploads for up to 30 s, removes the directories. A worker that is *cancelled* mid-session skips the final sync but still uploads changed files and removes the directories; a worker that is *killed* runs no teardown at all.
|
|
181
|
-
|
|
182
|
-
The store on Anthropic's side remains the source of truth - memory versions, redaction, and Console viewing/editing work as for cloud sessions, and the agent's memory reads/writes appear in the event stream as ordinary tool events. Because sync is interval-based, a change written by one self-hosted session is visible to another running session only after both have synced (typically well under a minute); cloud sessions see each other's changes almost immediately. Each store directory holds a marker file `.anthropic-memory-store` - leave it alone; the worker won't sync a directory whose marker is missing or altered.
|
|
183
|
-
|
|
184
|
-
**Prepare the host.** POSIX (Linux/macOS) only; a case-sensitive filesystem is recommended. Before starting the worker:
|
|
185
|
-
|
|
186
|
-
```bash
|
|
187
|
-
sudo mkdir -p /mnt/memory && sudo chown "$USER" /mnt/memory
|
|
188
|
-
```
|
|
189
|
-
|
|
190
|
-
Do **not** create the per-store directories yourself - the worker creates each store's directory when a session starts, **refuses the work item if something already exists at that path**, and removes it at session end. Two rules follow: (a) two sessions can't mount the same store on one host simultaneously (they need the same path) - give each session its own sandbox; (b) stop workers gracefully. `EnvironmentWorker` installs no signal handlers: wire SIGTERM/SIGINT to cancellation yourself (abort the `signal` in TypeScript, cancel the context in Go, cancel the task running `run()` / `handle_item()` in Python), send SIGTERM, and allow >= 30 s before any hard kill. If a worker is killed before teardown, remove the leftover directory under `/mnt/memory/` before the next session that attaches that store - unsynced edits in it are lost.
|
|
191
|
-
|
|
192
|
-
**Sandbox-per-session** (the pattern from § Container orchestration) satisfies rule (a) automatically. Keep `ant beta:worker poll --on-work` (or the SDK poller) on the host; build the per-session image around the SDK worker instead of `ant beta:worker run` - its entrypoint constructs `EnvironmentWorker` and calls `handle_item()`, which reads the session/work/environment IDs from the `ANTHROPIC_*` vars and the per-session secret from `ANTHROPIC_WORK_SECRET` (or pass `work_secret=` / `workSecret` / `WorkSecret` explicitly). `--on-work` does not set `ANTHROPIC_WORK_SECRET` for the spawn script, so read it from the work-item JSON on stdin:
|
|
193
|
-
|
|
194
|
-
```bash
|
|
195
|
-
#!/bin/bash
|
|
196
|
-
# spawn.sh - called once per claimed work item; the work item arrives as JSON on stdin
|
|
197
|
-
ANTHROPIC_WORK_SECRET="$(jq -r '.secret // empty')"
|
|
198
|
-
export ANTHROPIC_WORK_SECRET
|
|
199
|
-
exec docker run --rm \
|
|
200
|
-
-e ANTHROPIC_SESSION_ID -e ANTHROPIC_WORK_ID -e ANTHROPIC_ENVIRONMENT_ID \
|
|
201
|
-
-e ANTHROPIC_ENVIRONMENT_KEY -e ANTHROPIC_BASE_URL -e ANTHROPIC_WORK_SECRET \
|
|
202
|
-
my-sdk-worker-image
|
|
203
|
-
```
|
|
204
|
-
|
|
205
|
-
The per-session entrypoint is a few lines - no arguments needed, `handle_item()` reads the forwarded `ANTHROPIC_*` vars including `ANTHROPIC_WORK_SECRET`; wire signals to cancellation so a stopped container still uploads:
|
|
206
|
-
|
|
207
|
-
```python
|
|
208
|
-
import asyncio, contextlib, os, signal
|
|
209
|
-
from anthropic import AsyncAnthropic
|
|
210
|
-
from anthropic.lib.environments import EnvironmentWorker
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
async def main() -> None:
|
|
214
|
-
async with AsyncAnthropic(auth_token=os.environ["ANTHROPIC_ENVIRONMENT_KEY"]) as client:
|
|
215
|
-
task = asyncio.create_task(EnvironmentWorker(client, workdir="/workspace").handle_item())
|
|
216
|
-
loop = asyncio.get_running_loop()
|
|
217
|
-
for signum in (signal.SIGINT, signal.SIGTERM):
|
|
218
|
-
loop.add_signal_handler(signum, task.cancel)
|
|
219
|
-
with contextlib.suppress(asyncio.CancelledError):
|
|
220
|
-
await task
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
asyncio.run(main())
|
|
224
|
-
```
|
|
225
|
-
|
|
226
|
-
TypeScript: `new EnvironmentWorker({ client, workdir: "/workspace", signal: controller.signal }).handleItem()` with `process.once("SIGTERM"/"SIGINT", () => controller.abort())`. Go: `signal.NotifyContext(ctx, os.Interrupt, syscall.SIGTERM)` then `environments.NewEnvironmentWorker(client, environments.EnvironmentWorkerOptions{Workdir: "/workspace"}).HandleItem(ctx, environments.HandleItemOptions{})`.
|
|
227
|
-
|
|
228
|
-
The image needs a writable `/mnt/memory`; the memory directories need **not** be bind-mounted to the host - the worker uploads before the sandbox exits, and a discarded sandbox leaves nothing to clean up. Stop a container early with a signal the entrypoint turns into cancellation, not a kill, so that upload still runs.
|
|
229
|
-
|
|
230
|
-
**Configure sync** - two `EnvironmentWorker` options (constructor or `client.beta.environments.work.worker()` factory in Python; the options object in TypeScript; `environments.EnvironmentWorkerOptions` in Go):
|
|
231
|
-
|
|
232
|
-
| Option | Python / TypeScript / Go | Behavior |
|
|
233
|
-
|---|---|---|
|
|
234
|
-
| Sync interval | `memory_sync_interval` (seconds) / `memorySyncIntervalMs` (ms) / `MemorySyncInterval` (duration) | Default 15 s, minimum 5 s. Shorter narrows the stale window at the cost of more memory-store requests. `None` / `null` / negative duration **disables memory support entirely** - stores are neither downloaded nor synced, and a session with stores attached runs without them even though its system prompt still describes them. Only disable on workers whose sessions never attach stores. While enabled, a work item that arrives without a `secret` for a session with stores **fails** rather than running memory-less. |
|
|
235
|
-
| Delete propagation | `memory_sync_deletes` / `memorySyncDeletes` / `MemorySyncDeletes` | `"enabled"` (default - deletes from the store once a later sync confirms the file is still gone), `"log_only"` (same checks, only logs what it would delete - use to audit before trusting `enabled`), `"disabled"` (never deletes from the store). Go: `environments.MemorySyncDeletesEnabled` (zero value) / `LogOnly` / `Disabled`. Uploads/downloads are unaffected. |
|
|
236
|
-
|
|
237
|
-
For example, sync every 10 s and only *log* would-be deletes: Python `EnvironmentWorker(client, environment_id=..., environment_key=..., workdir="/workspace", memory_sync_interval=10, memory_sync_deletes="log_only")`; TypeScript `new EnvironmentWorker({ client, environmentId, environmentKey, workdir: "/workspace", memorySyncIntervalMs: 10_000, memorySyncDeletes: "log_only" })`; Go `environments.EnvironmentWorkerOptions{..., MemorySyncInterval: 10 * time.Second, MemorySyncDeletes: environments.MemorySyncDeletesLogOnly}`.
|
|
238
|
-
|
|
239
|
-
**Read-only stores and conflicts.** For `access: "read_only"`, `write`/`edit` refuse changes under the directory (the only memory errors that reach the agent, as tool errors) and nothing uploads; the memory-store endpoints also reject writes made with the session's `secret`. `bash` edits aren't blocked locally - they never sync and the next remote change overwrites them. Conflicts resolve **in favor of the store**: if the agent changes a file that also changed remotely since the last sync, the worker keeps the store's version at the next sync, overwrites the local file, and logs a warning - `write`/`edit` still succeed and no error reaches the agent; it can re-read and re-apply.
|
|
240
|
-
|
|
241
|
-
**Troubleshooting.** Mount and background-sync failures are *logged*, not reported to the session. If a store can't be mounted at claim time the worker fails the work item - the session emits no error event and sits `idle` (`requires_action` stop reason).
|
|
242
|
-
|
|
243
|
-
| Log line / symptom | Cause | Fix |
|
|
244
|
-
|---|---|---|
|
|
245
|
-
| `the work item carried no sessions token` (Go: `ErrSessionMemoryNoToken`), work item fails | The per-session `secret` didn't reach the worker - memory on self-hosted isn't enabled for your org, or your spawn script didn't forward it | Forward `ANTHROPIC_WORK_SECRET` into the sandbox. If the in-process worker (poll + run in one process) still logs this, contact support |
|
|
246
|
-
| `something already exists at the memory store's path` | Leftover directory from a killed worker | Remove the named directory (unsynced edits are lost) |
|
|
247
|
-
| `cannot create the memory store's folder` + `the worker host must make this mount path writable` | Worker user can't create dirs under `/mnt/memory` | `mkdir -p /mnt/memory && chown <worker-user> /mnt/memory` |
|
|
248
|
-
| Session `idle` with `requires_action`, no error event, shortly after a claim | Worker failed the work item on a mount error above | Fix the host, then send `user.interrupt` - the work is re-queued and the next claim retries the mount |
|
|
249
|
-
|
|
250
|
-
## Monitoring & control
|
|
251
|
-
|
|
252
|
-
These are **control-plane** calls - authenticate with `x-api-key` (not the environment key); `managed-agents-2026-04-01` beta header. **Call them from outside the worker host** - setting `ANTHROPIC_API_KEY` on the worker host exposes an organization-scoped credential to agent tool calls.
|
|
253
|
-
|
|
254
|
-
| SDK (`client.beta.environments.work.*`) | REST | CLI | Returns |
|
|
255
|
-
|---|---|---|---|
|
|
256
|
-
| `stats(environment_id)` | `GET /v1/environments/{id}/work/stats` | `ant beta:environments:work stats` | `{type:"work_queue_stats", depth, pending, oldest_queued_at, workers_polling}` |
|
|
257
|
-
| `stop(work_id, environment_id=)` | `POST /v1/environments/{id}/work/{work_id}/stop` | `ant beta:environments:work stop` | `work.state` |
|
|
258
|
-
|
|
259
|
-
## What changes vs `cloud`
|
|
260
|
-
|
|
261
|
-
| Concern | `cloud` | `self_hosted` |
|
|
262
|
-
|---|---|---|
|
|
263
|
-
| Container lifecycle, hardening, networking | Anthropic | **You** - run non-root, read-only rootfs, drop caps; egress is whatever your VPC/firewall allows - except `web_search` / `web_fetch`, which run on Anthropic's servers either way (restrict them per tool with `allowed_domains` / `blocked_domains`) |
|
|
264
|
-
| `file` / `github_repository` resource mounting | Anthropic mounts into the container | **You** - pass pointers via `sessions.create(metadata={...})` and have your orchestrator fetch/clone before dispatch |
|
|
265
|
-
| `memory_store` resources | Mounted by Anthropic at `/mnt/memory/<name>/` (live FUSE mount) | **Supported via the SDK worker** (Python / TypeScript / Go `EnvironmentWorker`), which downloads each store to `/mnt/memory/<store-name>/` and syncs on an interval - see § Memory stores. Not mounted by the `ant` CLI worker; not available in the C#, Java, PHP, or Ruby SDKs. `memory_store` is the **only** resource type self-hosted environments accept - `file` / `github_repository` are still rejected with the 400 message "Environment env_... is a self-hosted environment. `resources` are not supported with self-hosted environments." (deployments targeting a self-hosted environment follow the same rule; the Console deployment form doesn't offer memory stores for them - use the API/SDK). |
|
|
266
|
-
| Vault `environment_variable` credentials | Supported (substituted at Anthropic-managed egress) | **Not yet supported** - egress is yours, so there's nowhere to substitute the secret. Use MCP credentials or a host-side custom tool (`shared/managed-agents-client-patterns.md` Pattern 9) |
|
|
267
|
-
| Built-in tools | Via `agent_toolset_20260401` | Supplied by your worker (`EnvironmentWorker` default / `beta_agent_toolset(env)` / `ant` CLI fixed set) |
|
|
268
|
-
| Skills download | Automatic | `EnvironmentWorker` / `AgentToolContext` fetch into `{workdir}/skills/` (needs `client` + `session_id`) |
|
|
269
|
-
| Claude Platform on AWS | Supported | Supported - the worker authenticates with AWS IAM (SigV4) or an AWS-Console-generated API key (Console-generated environment keys don't work against the AWS endpoint); attach the `AnthropicSelfHostedEnvironmentAccess` managed policy to the worker's principal. **Memory stores cannot be attached** to sessions on self-hosted environments there (rejected at session create); cloud environments attach them as usual. |
|
|
270
|
-
| SDK worker helpers | All SDKs | **Python, TypeScript, Go only** (`EnvironmentWorker` / poller not in Java, Ruby, PHP, or C#) - use one of those three or the `ant` CLI |
|
|
271
|
-
|
|
272
|
-
## Credentials
|
|
273
|
-
|
|
274
|
-
| Credential | Format | Scope |
|
|
275
|
-
|---|---|---|
|
|
276
|
-
| `ANTHROPIC_ENVIRONMENT_KEY` | `sk-ant-oat01-...` | One environment's work queue. Generate in Console ("Generate environment key"). Pass as `auth_token=` / `authToken` on the client **and** as `environment_key=` / `environmentKey` on `EnvironmentWorker`. Store in a secrets manager; rotate on exposure. |
|
|
277
|
-
| `ANTHROPIC_WEBHOOK_SIGNING_KEY` | `whsec_...` | Webhook signature verification (if using webhook-driven wake). The SDK reads this env var automatically for `client.beta.webhooks.unwrap()`. |
|
|
278
|
-
| Work-item `secret` (`ANTHROPIC_WORK_SECRET`) | per-session, issued by Anthropic on the claimed work item | Posts that session's events and reads/writes the memory stores attached to it. You don't generate it; the in-process worker picks it up from the work item, and in the sandbox-per-session pattern you forward it into the sandbox yourself (or pass `work_secret=` / `workSecret` / `WorkSecret` explicitly). Treat like the environment key: only into the sandbox serving that session, never in images, shared volumes, or logs. |
|
|
279
|
-
|
|
280
|
-
## Security - what you own
|
|
281
|
-
|
|
282
|
-
Container hardening; egress restriction for the sandbox (there is no default; the server-side `web_search` / `web_fetch` are governed only by their `allowed_domains` / `blocked_domains`); `ANTHROPIC_ENVIRONMENT_KEY` custody and rotation; one workspace + environment per trust boundary when running untrusted code; least-privilege for the tool process; log retention and redaction. **Anthropic cannot**: fast-revoke a leaked environment key, verify your image or supply chain, sandbox tool execution inside your container, or enforce retention after tool output reaches your infrastructure. **Memory stores** stay hosted by Anthropic (with version history), but the working copy under `/mnt/memory/` is yours for the session's duration: the worker deletes it on teardown, a killed worker leaves it behind, and permissions/isolation between sessions sharing a filesystem are your responsibility. A `read_only` store is protected from *upload*, not from local modification - `bash` can still change the local copy (later tool calls in that session read the changed copy until the store next changes that memory); disable `bash` or mount the path read-only if the agent must not alter even its local view. See the Self-Hosted Sandboxes Security page in `shared/live-sources.md` for the full checklist.
|