@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.
Files changed (149) hide show
  1. package/README.md +85 -131
  2. package/apps/server/src/index.ts +97 -0
  3. package/apps/server/src/session-manager.ts +95 -0
  4. package/apps/server/src/workspace.ts +39 -0
  5. package/{tsconfig.json → apps/server/tsconfig.json} +3 -3
  6. package/apps/web/dist/assets/index-D4n3_lAw.js +10 -0
  7. package/apps/web/dist/assets/index-igiK0FPY.css +1 -0
  8. package/apps/web/dist/index.html +3 -0
  9. package/bin/piexis-web.mjs +24 -0
  10. package/extensions/agents.ts +205 -0
  11. package/extensions/ask-question.ts +54 -95
  12. package/extensions/recommended-tools.ts +62 -0
  13. package/extensions/ui.ts +204 -58
  14. package/extensions/webui.ts +45 -0
  15. package/lib/agent-config.ts +55 -0
  16. package/lib/agents.ts +208 -0
  17. package/lib/question-model.ts +98 -0
  18. package/lib/question-ui.ts +214 -0
  19. package/lib/recommended-tools.ts +119 -0
  20. package/lib/subagent-runner.ts +146 -0
  21. package/lib/ui-tips.json +41 -0
  22. package/package.json +41 -14
  23. package/packages/pi-integration/src/rpc-client.ts +99 -0
  24. package/packages/shared/src/index.ts +27 -0
  25. package/prompts/init.md +10 -2
  26. package/prompts/summary.md +7 -6
  27. package/schemas/agent.schema.json +48 -0
  28. package/skills/{claude-api/LICENSE.txt → caveman/LICENSE} +2 -2
  29. package/skills/caveman/LICENSE-MIT +21 -0
  30. package/skills/caveman/NOTICE +10 -0
  31. package/skills/caveman/SKILL.md +89 -0
  32. package/skills/find-skills/SKILL.md +141 -0
  33. package/skills/ponytail/LICENSE +21 -0
  34. package/skills/ponytail/NOTICE +9 -0
  35. package/skills/ponytail/SKILL.md +52 -0
  36. package/themes/midnight.json +81 -0
  37. package/.github/workflows/publish.yml +0 -35
  38. package/AGENTS.md +0 -58
  39. package/eval/README.md +0 -72
  40. package/eval/permission-cases.json +0 -28
  41. package/eval/permissions.mjs +0 -103
  42. package/extensions/background-tasks.ts +0 -433
  43. package/extensions/background-worker.ts +0 -50
  44. package/extensions/glob.ts +0 -32
  45. package/extensions/mode.ts +0 -397
  46. package/lib/bash-approvals.ts +0 -113
  47. package/lib/bash-policy.ts +0 -164
  48. package/lib/mode-policy.ts +0 -174
  49. package/lib/path-policy.ts +0 -182
  50. package/lib/permission-classifier.ts +0 -128
  51. package/lib/permission-config.ts +0 -91
  52. package/lib/permission-denials.ts +0 -32
  53. package/lib/permission-intent.ts +0 -128
  54. package/lib/permission-taxonomy.ts +0 -15
  55. package/lib/task-broker.ts +0 -65
  56. package/lib/task-checkout.ts +0 -39
  57. package/lib/task-git.ts +0 -44
  58. package/lib/task-patch.ts +0 -106
  59. package/lib/task-permissions.ts +0 -51
  60. package/lib/task-report.ts +0 -62
  61. package/lib/task-worktree-policy.ts +0 -56
  62. package/scripts/release.mjs +0 -32
  63. package/skills/claude-api/SKILL.md +0 -572
  64. package/skills/claude-api/csharp/claude-api/README.md +0 -362
  65. package/skills/claude-api/csharp/claude-api/batches.md +0 -14
  66. package/skills/claude-api/csharp/claude-api/files-api.md +0 -25
  67. package/skills/claude-api/csharp/claude-api/streaming.md +0 -28
  68. package/skills/claude-api/csharp/claude-api/tool-use.md +0 -164
  69. package/skills/claude-api/curl/examples.md +0 -256
  70. package/skills/claude-api/curl/managed-agents.md +0 -371
  71. package/skills/claude-api/go/claude-api/README.md +0 -188
  72. package/skills/claude-api/go/claude-api/files-api.md +0 -23
  73. package/skills/claude-api/go/claude-api/streaming.md +0 -43
  74. package/skills/claude-api/go/claude-api/tool-use.md +0 -220
  75. package/skills/claude-api/go/managed-agents/README.md +0 -564
  76. package/skills/claude-api/java/claude-api/README.md +0 -238
  77. package/skills/claude-api/java/claude-api/files-api.md +0 -27
  78. package/skills/claude-api/java/claude-api/streaming.md +0 -24
  79. package/skills/claude-api/java/claude-api/tool-use.md +0 -228
  80. package/skills/claude-api/java/managed-agents/README.md +0 -443
  81. package/skills/claude-api/php/claude-api/README.md +0 -174
  82. package/skills/claude-api/php/claude-api/batches.md +0 -15
  83. package/skills/claude-api/php/claude-api/files-api.md +0 -13
  84. package/skills/claude-api/php/claude-api/streaming.md +0 -27
  85. package/skills/claude-api/php/claude-api/tool-use.md +0 -253
  86. package/skills/claude-api/php/managed-agents/README.md +0 -443
  87. package/skills/claude-api/python/claude-api/README.md +0 -565
  88. package/skills/claude-api/python/claude-api/batches.md +0 -198
  89. package/skills/claude-api/python/claude-api/files-api.md +0 -170
  90. package/skills/claude-api/python/claude-api/sdk-upgrade.md +0 -286
  91. package/skills/claude-api/python/claude-api/streaming.md +0 -179
  92. package/skills/claude-api/python/claude-api/tool-use.md +0 -627
  93. package/skills/claude-api/python/managed-agents/README.md +0 -335
  94. package/skills/claude-api/ruby/claude-api/README.md +0 -134
  95. package/skills/claude-api/ruby/claude-api/streaming.md +0 -16
  96. package/skills/claude-api/ruby/claude-api/tool-use.md +0 -41
  97. package/skills/claude-api/ruby/managed-agents/README.md +0 -394
  98. package/skills/claude-api/shared/admin-api.md +0 -179
  99. package/skills/claude-api/shared/agent-design.md +0 -101
  100. package/skills/claude-api/shared/anthropic-cli.md +0 -269
  101. package/skills/claude-api/shared/claude-platform-on-aws.md +0 -59
  102. package/skills/claude-api/shared/cost-optimization.md +0 -233
  103. package/skills/claude-api/shared/error-codes.md +0 -262
  104. package/skills/claude-api/shared/live-sources.md +0 -166
  105. package/skills/claude-api/shared/managed-agents-api-reference.md +0 -455
  106. package/skills/claude-api/shared/managed-agents-client-patterns.md +0 -220
  107. package/skills/claude-api/shared/managed-agents-core.md +0 -359
  108. package/skills/claude-api/shared/managed-agents-environments.md +0 -226
  109. package/skills/claude-api/shared/managed-agents-events.md +0 -290
  110. package/skills/claude-api/shared/managed-agents-memory.md +0 -201
  111. package/skills/claude-api/shared/managed-agents-multiagent.md +0 -260
  112. package/skills/claude-api/shared/managed-agents-onboarding.md +0 -82
  113. package/skills/claude-api/shared/managed-agents-outcomes.md +0 -108
  114. package/skills/claude-api/shared/managed-agents-overview.md +0 -77
  115. package/skills/claude-api/shared/managed-agents-scheduled-deployments.md +0 -156
  116. package/skills/claude-api/shared/managed-agents-self-hosted-sandboxes.md +0 -282
  117. package/skills/claude-api/shared/managed-agents-tools.md +0 -504
  118. package/skills/claude-api/shared/managed-agents-webhooks.md +0 -143
  119. package/skills/claude-api/shared/model-migration.md +0 -1872
  120. package/skills/claude-api/shared/models.md +0 -142
  121. package/skills/claude-api/shared/platform-availability.md +0 -53
  122. package/skills/claude-api/shared/prompt-audit.md +0 -223
  123. package/skills/claude-api/shared/prompt-caching.md +0 -292
  124. package/skills/claude-api/shared/token-counting.md +0 -56
  125. package/skills/claude-api/shared/tool-use-concepts.md +0 -526
  126. package/skills/claude-api/typescript/claude-api/README.md +0 -405
  127. package/skills/claude-api/typescript/claude-api/batches.md +0 -106
  128. package/skills/claude-api/typescript/claude-api/files-api.md +0 -98
  129. package/skills/claude-api/typescript/claude-api/streaming.md +0 -178
  130. package/skills/claude-api/typescript/claude-api/tool-use.md +0 -586
  131. package/skills/claude-api/typescript/managed-agents/README.md +0 -359
  132. package/skills-lock.json +0 -359
  133. package/tests/auto-permissions.test.mjs +0 -315
  134. package/tests/background-tasks.test.mjs +0 -27
  135. package/tests/background-worker.test.mjs +0 -60
  136. package/tests/bash-permissions.test.mjs +0 -276
  137. package/tests/glob.test.mjs +0 -31
  138. package/tests/mode.test.mjs +0 -62
  139. package/tests/permission-evaluation.test.mjs +0 -70
  140. package/tests/permission-foundation.test.mjs +0 -194
  141. package/tests/permission-policy.test.mjs +0 -158
  142. package/tests/runtime/README.md +0 -51
  143. package/tests/runtime/harness.mjs +0 -190
  144. package/tests/runtime/permissions.test.mjs +0 -192
  145. package/tests/runtime/tui.test.mjs +0 -60
  146. package/tests/skills-routing.test.mjs +0 -51
  147. package/tests/task-integration.test.mjs +0 -324
  148. package/tests/task-permissions.test.mjs +0 -178
  149. 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.