mercury-agent 0.5.0 → 0.5.2

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 (125) hide show
  1. package/README.md +452 -451
  2. package/container/Dockerfile +127 -127
  3. package/container/Dockerfile.base +109 -109
  4. package/container/Dockerfile.power +17 -17
  5. package/container/agent-package.json +8 -8
  6. package/container/build.sh +54 -54
  7. package/docs/ROADMAP.md +3 -4
  8. package/docs/archive/summarization/2026-07-02-recent-dev-summary.md +71 -0
  9. package/docs/auth/dashboard.md +28 -28
  10. package/docs/auth/overview.md +109 -109
  11. package/docs/auth/whatsapp.md +173 -173
  12. package/docs/authoring-profiles.md +174 -174
  13. package/docs/configuration.md +54 -54
  14. package/docs/container-lifecycle.md +349 -349
  15. package/docs/{bugs/bwrap-privileged-linux-docker.md → debug/major/2026-07-02-bwrap-privileged-linux-docker.md} +21 -1
  16. package/docs/{bugs/e2big-prompt-delivery-fix.md → debug/major/2026-07-02-e2big-prompt-delivery-fix.md} +27 -1
  17. package/docs/{bugs/registry-pull-no-local-fallback.md → debug/major/2026-07-02-registry-pull-no-local-fallback.md} +27 -1
  18. package/docs/debug/summarization/2026-07-02-common-bug-patterns.md +72 -0
  19. package/docs/deployment.md +199 -199
  20. package/docs/extensions.md +375 -375
  21. package/docs/graceful-shutdown.md +62 -62
  22. package/docs/kb-distillation.md +77 -77
  23. package/docs/media/overview.md +140 -140
  24. package/docs/media/whatsapp.md +171 -171
  25. package/docs/memory.md +137 -137
  26. package/docs/permissions.md +217 -217
  27. package/docs/pipeline.md +228 -228
  28. package/docs/prd-chat-memory.md +76 -76
  29. package/docs/prd-config-load.md +82 -82
  30. package/docs/rate-limiting.md +229 -229
  31. package/docs/runbooks/publish-checklist.md +9 -1
  32. package/docs/scheduler.md +288 -288
  33. package/docs/setup-discord.md +100 -100
  34. package/docs/setup-slack.md +119 -119
  35. package/docs/setup-whatsapp.md +94 -94
  36. package/docs/subagents.md +166 -166
  37. package/docs/web-search.md +62 -62
  38. package/examples/extensions/README.md +12 -12
  39. package/examples/extensions/charts/index.ts +13 -13
  40. package/examples/extensions/charts/skill/SKILL.md +98 -98
  41. package/examples/extensions/gws/README.md +52 -52
  42. package/examples/extensions/gws/skill/SKILL.md +57 -57
  43. package/examples/extensions/gws/skill/references/calendar.md +101 -101
  44. package/examples/extensions/gws/skill/references/docs.md +65 -65
  45. package/examples/extensions/gws/skill/references/drive.md +79 -79
  46. package/examples/extensions/gws/skill/references/gmail.md +85 -85
  47. package/examples/extensions/gws/skill/references/sheets.md +60 -60
  48. package/examples/extensions/napkin/skill/SKILL.md +728 -728
  49. package/examples/extensions/pdf/skill/LICENSE.txt +30 -30
  50. package/examples/extensions/pdf/skill/SKILL.md +314 -314
  51. package/examples/extensions/pdf/skill/forms.md +294 -294
  52. package/examples/extensions/pdf/skill/reference.md +611 -611
  53. package/examples/extensions/pdf/skill/scripts/check_bounding_boxes.py +65 -65
  54. package/examples/extensions/pdf/skill/scripts/check_fillable_fields.py +11 -11
  55. package/examples/extensions/pdf/skill/scripts/convert_pdf_to_images.py +33 -33
  56. package/examples/extensions/pdf/skill/scripts/create_validation_image.py +37 -37
  57. package/examples/extensions/pdf/skill/scripts/extract_form_field_info.py +122 -122
  58. package/examples/extensions/pdf/skill/scripts/extract_form_structure.py +115 -115
  59. package/examples/extensions/pdf/skill/scripts/fill_fillable_fields.py +98 -98
  60. package/examples/extensions/pdf/skill/scripts/fill_pdf_form_with_annotations.py +107 -107
  61. package/examples/extensions/permission-guard/index.ts +65 -65
  62. package/examples/extensions/pinchtab/skill/SKILL.md +224 -224
  63. package/examples/extensions/pinchtab/skill/TRUST.md +69 -69
  64. package/examples/extensions/pinchtab/skill/references/api.md +297 -297
  65. package/examples/extensions/pinchtab/skill/references/env.md +45 -45
  66. package/examples/extensions/pinchtab/skill/references/profiles.md +107 -107
  67. package/examples/extensions/tradestation/host/refresh.ts +102 -102
  68. package/examples/extensions/tradestation/index.ts +153 -153
  69. package/examples/extensions/tradestation/skill/SKILL.md +67 -67
  70. package/examples/extensions/voice-synth/index.ts +94 -94
  71. package/examples/extensions/voice-synth/skill/SKILL.md +38 -38
  72. package/examples/extensions/voice-transcribe/requirements.txt +8 -8
  73. package/examples/extensions/voice-transcribe/scripts/transcribe.py +179 -179
  74. package/examples/extensions/voice-transcribe/skill/SKILL.md +53 -53
  75. package/examples/extensions/yahoo-mail/cli/package.json +13 -13
  76. package/examples/extensions/yahoo-mail/skill/SKILL.md +78 -78
  77. package/package.json +106 -106
  78. package/resources/agents/explore.md +50 -50
  79. package/resources/agents/worker.md +24 -24
  80. package/resources/connection-env-vars.json +25 -25
  81. package/resources/pi-extensions/subagent/agents.ts +126 -126
  82. package/resources/pi-extensions/subagent/index.ts +964 -964
  83. package/resources/profiles/coding/AGENTS.md +43 -43
  84. package/resources/profiles/coding/mercury-profile.yaml +15 -15
  85. package/resources/profiles/general/AGENTS.md +31 -31
  86. package/resources/profiles/general/mercury-profile.yaml +15 -15
  87. package/resources/profiles/research/AGENTS.md +40 -40
  88. package/resources/profiles/research/mercury-profile.yaml +15 -15
  89. package/resources/skills/config/SKILL.md +25 -25
  90. package/resources/skills/context/SKILL.md +33 -33
  91. package/resources/skills/conversation-recap/SKILL.md +19 -19
  92. package/resources/skills/mutes/SKILL.md +31 -31
  93. package/resources/skills/permissions/SKILL.md +19 -19
  94. package/resources/skills/preferences/SKILL.md +31 -31
  95. package/resources/skills/recall/SKILL.md +24 -24
  96. package/resources/skills/roles/SKILL.md +18 -18
  97. package/resources/skills/spaces/SKILL.md +18 -18
  98. package/resources/skills/tasks/SKILL.md +45 -45
  99. package/resources/templates/AGENTS.md +157 -157
  100. package/resources/templates/env.template +38 -38
  101. package/resources/templates/mercury.example.yaml +99 -99
  102. package/src/agent/container-entry.ts +1 -1
  103. package/src/agent/container-runner.ts +1345 -1346
  104. package/src/cli/mercury.ts +39 -7
  105. package/src/cli/mrctl.ts +636 -636
  106. package/src/config-file.ts +540 -540
  107. package/src/config.ts +339 -339
  108. package/src/core/api.ts +125 -125
  109. package/src/core/caller-token.ts +101 -101
  110. package/src/core/permissions.ts +228 -228
  111. package/src/core/profiles.ts +271 -271
  112. package/src/core/routes/capability.ts +70 -70
  113. package/src/core/routes/index.ts +15 -15
  114. package/src/core/runtime.ts +1530 -1530
  115. package/src/dashboard/index.html +729 -729
  116. package/src/extensions/api.ts +273 -273
  117. package/src/extensions/loader.ts +286 -286
  118. package/src/extensions/types.ts +517 -517
  119. package/src/main.ts +605 -605
  120. package/docs/pending-updates/applicative-profiles.md +0 -15
  121. package/docs/pending-updates/caller-bound-capability-token.md +0 -15
  122. package/docs/pending-updates/dm-auto-space.md +0 -15
  123. /package/docs/archive/{2026-07-01-applicative-profiles.md → applicative-profiles/2026-07-01-applicative-profiles.md} +0 -0
  124. /package/docs/archive/{2026-07-01-caller-bound-capability-token.md → applicative-profiles/2026-07-01-caller-bound-capability-token.md} +0 -0
  125. /package/docs/archive/{2026-07-01-dm-auto-space.md → applicative-profiles/2026-07-01-dm-auto-space.md} +0 -0
@@ -1,349 +1,349 @@
1
- # Container Lifecycle
2
-
3
- Mercury runs agent code inside Docker containers. This document covers how containers are managed, what happens when they fail, and how the system recovers.
4
-
5
- ## Deployment Topology
6
-
7
- Mercury uses a two-layer container model. The layers differ between local and production (Hetzner) deployments.
8
-
9
- ### Local (`mercury run`)
10
-
11
- ```
12
- Local machine
13
- └── mercury run (host process)
14
- └── mercury-<ts>-<id> (inner container, ephemeral --rm, one per message)
15
- ```
16
-
17
- Mercury runs directly on the host. Each incoming message spawns a short-lived inner container to run the Claude agent, which is deleted automatically on exit (`--rm`).
18
-
19
- ### Production node
20
-
21
- ```
22
- Production node
23
- ├── orchestrator (manages the node — start/stop/update agents)
24
- ├── traefik (routes *.baseDomain → agent containers)
25
- ├── mercury-agent-<user1> (outer container, persistent, one per tenant)
26
- │ └── mercury-<ts>-<id> (inner container, ephemeral --rm, one per message)
27
- ├── mercury-agent-<user2> (outer container, persistent)
28
- │ └── mercury-<ts>-<id>
29
- └── ...
30
- ```
31
-
32
- A single node hosts many tenants. Each user's Mercury process runs inside its own persistent outer container (`--restart=unless-stopped`). Inside that, per-message inner containers work exactly as they do locally.
33
-
34
- ### Why outer containers in production?
35
-
36
- | Concern | How outer containers solve it |
37
- |---|---|
38
- | Tenant isolation | Each agent runs in its own container — can't interfere with others |
39
- | Resource limits | `--memory` and `--cpus` enforced per-agent by the orchestrator |
40
- | Routing | Traefik labels assign each container its own subdomain (`agentId.baseDomain`) |
41
- | Independent lifecycle | orchestrator can start/stop/restart/update one agent without touching others |
42
- | Persistent state | Named Docker volume per agent (`mercury-<agentId>-data`) holds SQLite DB, WhatsApp auth, and spaces |
43
-
44
- ### Comparison
45
-
46
- | | Local (`mercury run`) | Production node |
47
- |---|---|---|
48
- | Mercury process | host process | `mercury-agent-<id>` container (`-d --restart=unless-stopped`) |
49
- | Per-message agent | ephemeral container (`--rm`) | ephemeral container (`--rm`) inside the outer container |
50
- | Logs | lost on exit | retained — `--log-opt max-size=20m --log-opt max-file=3` |
51
- | State | host filesystem | named Docker volume |
52
-
53
- ### Debugging inner container logs
54
-
55
- Inner containers are `--rm` and their logs are gone once they exit. To capture them you must stream live while the container runs:
56
-
57
- ```bash
58
- # Watch for the container to appear
59
- docker ps --filter "label=mercury.managed=true"
60
-
61
- # Tail its logs while it runs
62
- docker logs -f mercury-<ts>-<id>
63
- ```
64
-
65
- On a production node, the outer container logs are always available via SSH:
66
- ```bash
67
- docker logs mercury-agent-<agentId> -f
68
- ```
69
-
70
- ---
71
-
72
- ## Container Identity
73
-
74
- Each container is tagged for tracking and cleanup:
75
-
76
- | Property | Format | Purpose |
77
- |----------|--------|---------|
78
- | **Name** | `mercury-<timestamp>-<id>` | Unique identifier for logging/debugging |
79
- | **Label** | `mercury.managed=true` | Identifies mercury-owned containers for cleanup |
80
-
81
- Example:
82
- ```
83
- docker ps --filter "label=mercury.managed=true"
84
- CONTAINER ID IMAGE NAMES
85
- a1b2c3d4e5f6 mercury-agent mercury-1709312456789-1
86
- ```
87
-
88
- ## Timeout
89
-
90
- Containers have a maximum runtime to prevent runaway processes.
91
-
92
- | Config | Env Var | Default | Range |
93
- |--------|---------|---------|-------|
94
- | `containerTimeoutMs` | `MERCURY_CONTAINER_TIMEOUT_MS` | 5 minutes | 10s – 1h |
95
-
96
- When a container exceeds the timeout:
97
- 1. Container is killed via `docker kill`
98
- 2. `ContainerError` thrown with `reason: "timeout"`
99
- 3. User sees: "Container timed out."
100
- 4. Queue unblocks, next message can proceed
101
-
102
- The host always injects a resolved **model chain** into the container (after `MERCURY_*` passthrough) so retries and fallbacks use the same policy Mercury loaded at startup:
103
-
104
- | In-container env | Source (host) | Purpose |
105
- |------------------|---------------|---------|
106
- | `MODEL_CHAIN` | `resolvedModelChain` (from `MERCURY_MODEL_CHAIN` or primary+fallback) | Ordered `{ provider, model }` legs (max 4) |
107
- | `MODEL_RETRY_MAX_PER_LEG` | `MERCURY_MODEL_MAX_RETRIES_PER_LEG` | Extra attempts per leg for transient errors |
108
- | `MODEL_CHAIN_BUDGET_MS` | `effectiveModelChainBudgetMs` | Wall-clock budget for the whole chain (clamped below container timeout) |
109
-
110
- ## Error Types
111
-
112
- Container failures are classified by `ContainerError`:
113
-
114
- | Reason | Exit Code | Cause | User Message |
115
- |--------|-----------|-------|--------------|
116
- | `timeout` | — | Exceeded `containerTimeoutMs` | "Container timed out." |
117
- | `oom` | 137 | SIGKILL (OOM, resource limits, or manual kill) | "Container was killed (possibly out of memory)." |
118
- | `aborted` | — | User sent `stop` command | "Stopped current run." |
119
- | `error` | non-zero | Agent crashed or failed | *(error thrown, logged)* |
120
-
121
- Exit code 137 = 128 + 9 (SIGKILL), typically from Docker's OOM killer.
122
-
123
- ## Orphan Cleanup
124
-
125
- If the host process crashes or restarts while containers are running, those containers become orphans. On startup, mercury cleans them up:
126
-
127
- ```
128
- Startup
129
-
130
- └─► runtime.initialize()
131
-
132
- └─► containerRunner.cleanupOrphans()
133
-
134
- ├─► docker ps -a --filter "label=mercury.managed=true"
135
- ├─► docker rm -f <container-ids>
136
- └─► Log: "Cleaned up N orphaned container(s)"
137
- ```
138
-
139
- This ensures:
140
- - No zombie containers consuming resources
141
- - No blocked space queues from previous runs
142
- - Clean state before accepting new work
143
-
144
- ## Lifecycle Diagram
145
-
146
- ```
147
- Message received
148
-
149
- ├─► Queue (one per space)
150
-
151
- ├─► Spawn container
152
- │ • --name mercury-<ts>-<id>
153
- │ • --label mercury.managed=true
154
- │ • --rm (auto-remove on exit)
155
-
156
- ├─► Start timeout timer
157
-
158
- ├─► Wait for completion
159
- │ │
160
- │ ├─► Success (exit 0) → parse reply + scan outbox/ → respond
161
- │ ├─► Timeout → kill container → ContainerError(timeout)
162
- │ ├─► OOM (exit 137) → ContainerError(oom)
163
- │ ├─► Aborted → ContainerError(aborted)
164
- │ └─► Other failure → ContainerError(error)
165
-
166
- └─► Cleanup
167
- • Clear timeout timer
168
- • Remove from tracking map
169
- • Queue unblocks (finally block)
170
- ```
171
-
172
- ## Configuration
173
-
174
- ```bash
175
- # Set container timeout to 10 minutes
176
- export MERCURY_CONTAINER_TIMEOUT_MS=600000
177
-
178
- # Use a preset image from GitHub Container Registry
179
- export MERCURY_AGENT_IMAGE=ghcr.io/avishai-tsabari/mercury-agent:latest # Full (default)
180
- export MERCURY_AGENT_IMAGE=ghcr.io/avishai-tsabari/mercury-agent:minimal # Lightweight
181
- ```
182
-
183
- ## Sandboxing (Bubblewrap)
184
-
185
- Mercury uses a two-layer isolation model:
186
-
187
- 1. **Docker** — isolates the agent from the host
188
- 2. **Bubblewrap** — restricts the pi process within the container (defense-in-depth)
189
-
190
- The pi agent runs inside `bwrap`, which creates a minimal mount namespace with only the paths needed for the agent: workspace (`/spaces`), app code (`/app`), docs (`/docs`), and runtime dirs (`/root`, `/usr`, `/etc`, `/proc`, `/dev`, `/tmp`). This limits blast radius if the agent is compromised.
191
-
192
- | Env Var | Purpose |
193
- |---------|---------|
194
- | `MERCURY_CONTAINER_BWRAP_DOCKER_COMPAT=1` | **Host only.** Adds `docker run --security-opt seccomp=unconfined --cap-add SYS_ADMIN` so `bwrap` can nest inside the agent container (e.g. Docker Desktop). Keeps bubblewrap on. |
195
- | `MERCURY_DISABLE_BUBBLEWRAP=1` | Disable bubblewrap; run pi directly (last resort / debugging) |
196
-
197
- If you see `bwrap: Creating new namespace failed: Operation not permitted`, try **`MERCURY_CONTAINER_BWRAP_DOCKER_COMPAT=1`** first so you keep defense-in-depth. Only use `MERCURY_DISABLE_BUBBLEWRAP=1` if compat mode is not enough.
198
-
199
- Custom images must install `bubblewrap` for sandboxing to work.
200
-
201
- ## Agent Image Presets
202
-
203
- Mercury publishes two image presets to GitHub Container Registry:
204
-
205
- | Preset | Size | Contents |
206
- |--------|------|----------|
207
- | `ghcr.io/avishai-tsabari/mercury-agent:latest` | ~2.8GB | Full devcontainer: Bun, Node.js, Python, Go, git, build tools |
208
- | `ghcr.io/avishai-tsabari/mercury-agent:minimal` | ~1.9GB | Lightweight runtime: Bun + pi + Chromium deps |
209
-
210
- Images are published on each release. Version-specific tags are also available (e.g., `:0.2.0`, `:0.2.0-minimal`).
211
-
212
- ### Building Locally
213
-
214
- To build images locally instead of pulling from the registry:
215
- ```bash
216
- ./container/build.sh all # Both presets
217
- ./container/build.sh latest # Full image only (default)
218
- ./container/build.sh minimal # Lightweight image only
219
- ```
220
-
221
- Then use `mercury-agent:latest` or `mercury-agent:minimal` (without the ghcr.io prefix).
222
-
223
- ## Custom Agent Images
224
-
225
- You can use custom Docker images via `MERCURY_AGENT_IMAGE`.
226
-
227
- ### Requirements
228
-
229
- Your image **must** have:
230
- - `bun` runtime
231
- - `pi` CLI (`@earendil-works/pi-coding-agent`)
232
- - `bubblewrap` (for agent sandboxing)
233
- - `mrctl` wrapper (copied during build)
234
- Extension CLIs (e.g. `pinchtab`, `napkin`, `gws`) are installed in derived images at runtime based on `.mercury/extensions/*` declarations.
235
-
236
- ### Entry Point
237
-
238
- The image must use this entrypoint:
239
- ```dockerfile
240
- ENTRYPOINT ["bun", "run", "/app/src/agent/container-entry.ts"]
241
- ```
242
-
243
- ### Required Files
244
-
245
- Copy these files into your image at `/app/`:
246
- ```dockerfile
247
- COPY src/agent/container-entry.ts /app/src/agent/container-entry.ts
248
- COPY src/agent/pi-failure-class.ts /app/src/agent/pi-failure-class.ts
249
- COPY src/agent/pi-jsonl-parser.ts /app/src/agent/pi-jsonl-parser.ts
250
- COPY src/agent/preferences-prompt.ts /app/src/agent/preferences-prompt.ts
251
- COPY src/cli/mrctl.ts /app/src/cli/mrctl.ts
252
- COPY src/cli/mrctl-http.ts /app/src/cli/mrctl-http.ts
253
- COPY src/extensions/reserved.ts /app/src/extensions/reserved.ts
254
- COPY src/types.ts /app/src/types.ts
255
- ```
256
-
257
- ### mrctl Setup
258
-
259
- Create the mrctl wrapper:
260
- ```dockerfile
261
- RUN echo '#!/bin/sh\nbun run /app/src/cli/mrctl.ts "$@"' > /usr/local/bin/mrctl && \
262
- chmod +x /usr/local/bin/mrctl
263
- ```
264
-
265
- ### Volume Mounts
266
-
267
- Mercury mounts these paths into containers:
268
- - `/spaces` — Space workspaces (read/write)
269
- - `/home/mercury/.pi/agent` — Global agent config, skills, auth (read/write)
270
- - `/docs/mercury/` — Self-documentation (read-only)
271
-
272
- ### Example Custom Dockerfile
273
-
274
- ```dockerfile
275
- FROM your-base-image:tag
276
-
277
- # Install Bun
278
- RUN curl -fsSL https://bun.sh/install | bash
279
- ENV PATH="/home/mercury/.bun/bin:$PATH"
280
-
281
- # Install required CLIs
282
- RUN bun add -g @earendil-works/pi-coding-agent
283
-
284
- # Optional: install Playwright/Chromium if your extensions need browser automation
285
- RUN bunx playwright install chromium
286
-
287
- WORKDIR /app
288
-
289
- # Copy Mercury agent files
290
- COPY src/agent/container-entry.ts /app/src/agent/container-entry.ts
291
- COPY src/agent/pi-failure-class.ts /app/src/agent/pi-failure-class.ts
292
- COPY src/agent/pi-jsonl-parser.ts /app/src/agent/pi-jsonl-parser.ts
293
- COPY src/agent/preferences-prompt.ts /app/src/agent/preferences-prompt.ts
294
- COPY src/cli/mrctl.ts /app/src/cli/mrctl.ts
295
- COPY src/cli/mrctl-http.ts /app/src/cli/mrctl-http.ts
296
- COPY src/extensions/reserved.ts /app/src/extensions/reserved.ts
297
- COPY src/types.ts /app/src/types.ts
298
-
299
- # Setup mrctl
300
- RUN echo '#!/bin/sh\nbun run /app/src/cli/mrctl.ts "$@"' > /usr/local/bin/mrctl && \
301
- chmod +x /usr/local/bin/mrctl
302
-
303
- ENTRYPOINT ["bun", "run", "/app/src/agent/container-entry.ts"]
304
- ```
305
-
306
- ### Validation
307
-
308
- When using a custom image (not `mercury-agent:*`), Mercury logs a warning at startup:
309
- ```
310
- WARN Using custom agent image
311
- image: your-image:tag
312
- note: Ensure image has: bun, pi, bubblewrap, mrctl
313
- ```
314
-
315
- ## API
316
-
317
- ### `AgentContainerRunner`
318
-
319
- ```ts
320
- runner.cleanupOrphans() // Remove orphaned containers (called on startup)
321
- runner.reply(input) // Run container, returns ContainerResult (reply + outbox files)
322
- runner.abort(spaceId) // Kill container for a space
323
- runner.killAll() // Kill all running containers (shutdown)
324
- runner.isRunning(spaceId) // Check if container is active
325
- runner.activeCount // Number of running containers
326
- ```
327
-
328
- ### `MercuryCoreRuntime`
329
-
330
- ```ts
331
- await runtime.initialize() // Call before accepting work (runs orphan cleanup)
332
- ```
333
-
334
- ### `ContainerError`
335
-
336
- ```ts
337
- import { ContainerError } from "./agent/container-error.js";
338
-
339
- // Properties
340
- error.reason // "timeout" | "oom" | "aborted" | "error"
341
- error.exitCode // number | null
342
- error.message // Human-readable description
343
-
344
- // Factory methods
345
- ContainerError.timeout(spaceId)
346
- ContainerError.oom(spaceId, exitCode)
347
- ContainerError.aborted(spaceId)
348
- ContainerError.error(exitCode, output)
349
- ```
1
+ # Container Lifecycle
2
+
3
+ Mercury runs agent code inside Docker containers. This document covers how containers are managed, what happens when they fail, and how the system recovers.
4
+
5
+ ## Deployment Topology
6
+
7
+ Mercury uses a two-layer container model. The layers differ between local and production (Hetzner) deployments.
8
+
9
+ ### Local (`mercury run`)
10
+
11
+ ```
12
+ Local machine
13
+ └── mercury run (host process)
14
+ └── mercury-<ts>-<id> (inner container, ephemeral --rm, one per message)
15
+ ```
16
+
17
+ Mercury runs directly on the host. Each incoming message spawns a short-lived inner container to run the Claude agent, which is deleted automatically on exit (`--rm`).
18
+
19
+ ### Production node
20
+
21
+ ```
22
+ Production node
23
+ ├── orchestrator (manages the node — start/stop/update agents)
24
+ ├── traefik (routes *.baseDomain → agent containers)
25
+ ├── mercury-agent-<user1> (outer container, persistent, one per tenant)
26
+ │ └── mercury-<ts>-<id> (inner container, ephemeral --rm, one per message)
27
+ ├── mercury-agent-<user2> (outer container, persistent)
28
+ │ └── mercury-<ts>-<id>
29
+ └── ...
30
+ ```
31
+
32
+ A single node hosts many tenants. Each user's Mercury process runs inside its own persistent outer container (`--restart=unless-stopped`). Inside that, per-message inner containers work exactly as they do locally.
33
+
34
+ ### Why outer containers in production?
35
+
36
+ | Concern | How outer containers solve it |
37
+ |---|---|
38
+ | Tenant isolation | Each agent runs in its own container — can't interfere with others |
39
+ | Resource limits | `--memory` and `--cpus` enforced per-agent by the orchestrator |
40
+ | Routing | Traefik labels assign each container its own subdomain (`agentId.baseDomain`) |
41
+ | Independent lifecycle | orchestrator can start/stop/restart/update one agent without touching others |
42
+ | Persistent state | Named Docker volume per agent (`mercury-<agentId>-data`) holds SQLite DB, WhatsApp auth, and spaces |
43
+
44
+ ### Comparison
45
+
46
+ | | Local (`mercury run`) | Production node |
47
+ |---|---|---|
48
+ | Mercury process | host process | `mercury-agent-<id>` container (`-d --restart=unless-stopped`) |
49
+ | Per-message agent | ephemeral container (`--rm`) | ephemeral container (`--rm`) inside the outer container |
50
+ | Logs | lost on exit | retained — `--log-opt max-size=20m --log-opt max-file=3` |
51
+ | State | host filesystem | named Docker volume |
52
+
53
+ ### Debugging inner container logs
54
+
55
+ Inner containers are `--rm` and their logs are gone once they exit. To capture them you must stream live while the container runs:
56
+
57
+ ```bash
58
+ # Watch for the container to appear
59
+ docker ps --filter "label=mercury.managed=true"
60
+
61
+ # Tail its logs while it runs
62
+ docker logs -f mercury-<ts>-<id>
63
+ ```
64
+
65
+ On a production node, the outer container logs are always available via SSH:
66
+ ```bash
67
+ docker logs mercury-agent-<agentId> -f
68
+ ```
69
+
70
+ ---
71
+
72
+ ## Container Identity
73
+
74
+ Each container is tagged for tracking and cleanup:
75
+
76
+ | Property | Format | Purpose |
77
+ |----------|--------|---------|
78
+ | **Name** | `mercury-<timestamp>-<id>` | Unique identifier for logging/debugging |
79
+ | **Label** | `mercury.managed=true` | Identifies mercury-owned containers for cleanup |
80
+
81
+ Example:
82
+ ```
83
+ docker ps --filter "label=mercury.managed=true"
84
+ CONTAINER ID IMAGE NAMES
85
+ a1b2c3d4e5f6 mercury-agent mercury-1709312456789-1
86
+ ```
87
+
88
+ ## Timeout
89
+
90
+ Containers have a maximum runtime to prevent runaway processes.
91
+
92
+ | Config | Env Var | Default | Range |
93
+ |--------|---------|---------|-------|
94
+ | `containerTimeoutMs` | `MERCURY_CONTAINER_TIMEOUT_MS` | 5 minutes | 10s – 1h |
95
+
96
+ When a container exceeds the timeout:
97
+ 1. Container is killed via `docker kill`
98
+ 2. `ContainerError` thrown with `reason: "timeout"`
99
+ 3. User sees: "Container timed out."
100
+ 4. Queue unblocks, next message can proceed
101
+
102
+ The host always injects a resolved **model chain** into the container (after `MERCURY_*` passthrough) so retries and fallbacks use the same policy Mercury loaded at startup:
103
+
104
+ | In-container env | Source (host) | Purpose |
105
+ |------------------|---------------|---------|
106
+ | `MODEL_CHAIN` | `resolvedModelChain` (from `MERCURY_MODEL_CHAIN` or primary+fallback) | Ordered `{ provider, model }` legs (max 4) |
107
+ | `MODEL_RETRY_MAX_PER_LEG` | `MERCURY_MODEL_MAX_RETRIES_PER_LEG` | Extra attempts per leg for transient errors |
108
+ | `MODEL_CHAIN_BUDGET_MS` | `effectiveModelChainBudgetMs` | Wall-clock budget for the whole chain (clamped below container timeout) |
109
+
110
+ ## Error Types
111
+
112
+ Container failures are classified by `ContainerError`:
113
+
114
+ | Reason | Exit Code | Cause | User Message |
115
+ |--------|-----------|-------|--------------|
116
+ | `timeout` | — | Exceeded `containerTimeoutMs` | "Container timed out." |
117
+ | `oom` | 137 | SIGKILL (OOM, resource limits, or manual kill) | "Container was killed (possibly out of memory)." |
118
+ | `aborted` | — | User sent `stop` command | "Stopped current run." |
119
+ | `error` | non-zero | Agent crashed or failed | *(error thrown, logged)* |
120
+
121
+ Exit code 137 = 128 + 9 (SIGKILL), typically from Docker's OOM killer.
122
+
123
+ ## Orphan Cleanup
124
+
125
+ If the host process crashes or restarts while containers are running, those containers become orphans. On startup, mercury cleans them up:
126
+
127
+ ```
128
+ Startup
129
+
130
+ └─► runtime.initialize()
131
+
132
+ └─► containerRunner.cleanupOrphans()
133
+
134
+ ├─► docker ps -a --filter "label=mercury.managed=true"
135
+ ├─► docker rm -f <container-ids>
136
+ └─► Log: "Cleaned up N orphaned container(s)"
137
+ ```
138
+
139
+ This ensures:
140
+ - No zombie containers consuming resources
141
+ - No blocked space queues from previous runs
142
+ - Clean state before accepting new work
143
+
144
+ ## Lifecycle Diagram
145
+
146
+ ```
147
+ Message received
148
+
149
+ ├─► Queue (one per space)
150
+
151
+ ├─► Spawn container
152
+ │ • --name mercury-<ts>-<id>
153
+ │ • --label mercury.managed=true
154
+ │ • --rm (auto-remove on exit)
155
+
156
+ ├─► Start timeout timer
157
+
158
+ ├─► Wait for completion
159
+ │ │
160
+ │ ├─► Success (exit 0) → parse reply + scan outbox/ → respond
161
+ │ ├─► Timeout → kill container → ContainerError(timeout)
162
+ │ ├─► OOM (exit 137) → ContainerError(oom)
163
+ │ ├─► Aborted → ContainerError(aborted)
164
+ │ └─► Other failure → ContainerError(error)
165
+
166
+ └─► Cleanup
167
+ • Clear timeout timer
168
+ • Remove from tracking map
169
+ • Queue unblocks (finally block)
170
+ ```
171
+
172
+ ## Configuration
173
+
174
+ ```bash
175
+ # Set container timeout to 10 minutes
176
+ export MERCURY_CONTAINER_TIMEOUT_MS=600000
177
+
178
+ # Use a preset image from GitHub Container Registry
179
+ export MERCURY_AGENT_IMAGE=ghcr.io/avishai-tsabari/mercury-agent:latest # Full (default)
180
+ export MERCURY_AGENT_IMAGE=ghcr.io/avishai-tsabari/mercury-agent:minimal # Lightweight
181
+ ```
182
+
183
+ ## Sandboxing (Bubblewrap)
184
+
185
+ Mercury uses a two-layer isolation model:
186
+
187
+ 1. **Docker** — isolates the agent from the host
188
+ 2. **Bubblewrap** — restricts the pi process within the container (defense-in-depth)
189
+
190
+ The pi agent runs inside `bwrap`, which creates a minimal mount namespace with only the paths needed for the agent: workspace (`/spaces`), app code (`/app`), docs (`/docs`), and runtime dirs (`/root`, `/usr`, `/etc`, `/proc`, `/dev`, `/tmp`). This limits blast radius if the agent is compromised.
191
+
192
+ | Env Var | Purpose |
193
+ |---------|---------|
194
+ | `MERCURY_CONTAINER_BWRAP_DOCKER_COMPAT=1` | **Host only.** Adds `docker run --security-opt seccomp=unconfined --cap-add SYS_ADMIN` so `bwrap` can nest inside the agent container (e.g. Docker Desktop). Keeps bubblewrap on. |
195
+ | `MERCURY_DISABLE_BUBBLEWRAP=1` | Disable bubblewrap; run pi directly (last resort / debugging) |
196
+
197
+ If you see `bwrap: Creating new namespace failed: Operation not permitted`, try **`MERCURY_CONTAINER_BWRAP_DOCKER_COMPAT=1`** first so you keep defense-in-depth. Only use `MERCURY_DISABLE_BUBBLEWRAP=1` if compat mode is not enough.
198
+
199
+ Custom images must install `bubblewrap` for sandboxing to work.
200
+
201
+ ## Agent Image Presets
202
+
203
+ Mercury publishes two image presets to GitHub Container Registry:
204
+
205
+ | Preset | Size | Contents |
206
+ |--------|------|----------|
207
+ | `ghcr.io/avishai-tsabari/mercury-agent:latest` | ~2.8GB | Full devcontainer: Bun, Node.js, Python, Go, git, build tools |
208
+ | `ghcr.io/avishai-tsabari/mercury-agent:minimal` | ~1.9GB | Lightweight runtime: Bun + pi + Chromium deps |
209
+
210
+ Images are published on each release. Version-specific tags are also available (e.g., `:0.2.0`, `:0.2.0-minimal`).
211
+
212
+ ### Building Locally
213
+
214
+ To build images locally instead of pulling from the registry:
215
+ ```bash
216
+ ./container/build.sh all # Both presets
217
+ ./container/build.sh latest # Full image only (default)
218
+ ./container/build.sh minimal # Lightweight image only
219
+ ```
220
+
221
+ Then use `mercury-agent:latest` or `mercury-agent:minimal` (without the ghcr.io prefix).
222
+
223
+ ## Custom Agent Images
224
+
225
+ You can use custom Docker images via `MERCURY_AGENT_IMAGE`.
226
+
227
+ ### Requirements
228
+
229
+ Your image **must** have:
230
+ - `bun` runtime
231
+ - `pi` CLI (`@earendil-works/pi-coding-agent`)
232
+ - `bubblewrap` (for agent sandboxing)
233
+ - `mrctl` wrapper (copied during build)
234
+ Extension CLIs (e.g. `pinchtab`, `napkin`, `gws`) are installed in derived images at runtime based on `.mercury/extensions/*` declarations.
235
+
236
+ ### Entry Point
237
+
238
+ The image must use this entrypoint:
239
+ ```dockerfile
240
+ ENTRYPOINT ["bun", "run", "/app/src/agent/container-entry.ts"]
241
+ ```
242
+
243
+ ### Required Files
244
+
245
+ Copy these files into your image at `/app/`:
246
+ ```dockerfile
247
+ COPY src/agent/container-entry.ts /app/src/agent/container-entry.ts
248
+ COPY src/agent/pi-failure-class.ts /app/src/agent/pi-failure-class.ts
249
+ COPY src/agent/pi-jsonl-parser.ts /app/src/agent/pi-jsonl-parser.ts
250
+ COPY src/agent/preferences-prompt.ts /app/src/agent/preferences-prompt.ts
251
+ COPY src/cli/mrctl.ts /app/src/cli/mrctl.ts
252
+ COPY src/cli/mrctl-http.ts /app/src/cli/mrctl-http.ts
253
+ COPY src/extensions/reserved.ts /app/src/extensions/reserved.ts
254
+ COPY src/types.ts /app/src/types.ts
255
+ ```
256
+
257
+ ### mrctl Setup
258
+
259
+ Create the mrctl wrapper:
260
+ ```dockerfile
261
+ RUN echo '#!/bin/sh\nbun run /app/src/cli/mrctl.ts "$@"' > /usr/local/bin/mrctl && \
262
+ chmod +x /usr/local/bin/mrctl
263
+ ```
264
+
265
+ ### Volume Mounts
266
+
267
+ Mercury mounts these paths into containers:
268
+ - `/spaces` — Space workspaces (read/write)
269
+ - `/home/mercury/.pi/agent` — Global agent config, skills, auth (read/write)
270
+ - `/docs/mercury/` — Self-documentation (read-only)
271
+
272
+ ### Example Custom Dockerfile
273
+
274
+ ```dockerfile
275
+ FROM your-base-image:tag
276
+
277
+ # Install Bun
278
+ RUN curl -fsSL https://bun.sh/install | bash
279
+ ENV PATH="/home/mercury/.bun/bin:$PATH"
280
+
281
+ # Install required CLIs
282
+ RUN bun add -g @earendil-works/pi-coding-agent
283
+
284
+ # Optional: install Playwright/Chromium if your extensions need browser automation
285
+ RUN bunx playwright install chromium
286
+
287
+ WORKDIR /app
288
+
289
+ # Copy Mercury agent files
290
+ COPY src/agent/container-entry.ts /app/src/agent/container-entry.ts
291
+ COPY src/agent/pi-failure-class.ts /app/src/agent/pi-failure-class.ts
292
+ COPY src/agent/pi-jsonl-parser.ts /app/src/agent/pi-jsonl-parser.ts
293
+ COPY src/agent/preferences-prompt.ts /app/src/agent/preferences-prompt.ts
294
+ COPY src/cli/mrctl.ts /app/src/cli/mrctl.ts
295
+ COPY src/cli/mrctl-http.ts /app/src/cli/mrctl-http.ts
296
+ COPY src/extensions/reserved.ts /app/src/extensions/reserved.ts
297
+ COPY src/types.ts /app/src/types.ts
298
+
299
+ # Setup mrctl
300
+ RUN echo '#!/bin/sh\nbun run /app/src/cli/mrctl.ts "$@"' > /usr/local/bin/mrctl && \
301
+ chmod +x /usr/local/bin/mrctl
302
+
303
+ ENTRYPOINT ["bun", "run", "/app/src/agent/container-entry.ts"]
304
+ ```
305
+
306
+ ### Validation
307
+
308
+ When using a custom image (not `mercury-agent:*`), Mercury logs a warning at startup:
309
+ ```
310
+ WARN Using custom agent image
311
+ image: your-image:tag
312
+ note: Ensure image has: bun, pi, bubblewrap, mrctl
313
+ ```
314
+
315
+ ## API
316
+
317
+ ### `AgentContainerRunner`
318
+
319
+ ```ts
320
+ runner.cleanupOrphans() // Remove orphaned containers (called on startup)
321
+ runner.reply(input) // Run container, returns ContainerResult (reply + outbox files)
322
+ runner.abort(spaceId) // Kill container for a space
323
+ runner.killAll() // Kill all running containers (shutdown)
324
+ runner.isRunning(spaceId) // Check if container is active
325
+ runner.activeCount // Number of running containers
326
+ ```
327
+
328
+ ### `MercuryCoreRuntime`
329
+
330
+ ```ts
331
+ await runtime.initialize() // Call before accepting work (runs orphan cleanup)
332
+ ```
333
+
334
+ ### `ContainerError`
335
+
336
+ ```ts
337
+ import { ContainerError } from "./agent/container-error.js";
338
+
339
+ // Properties
340
+ error.reason // "timeout" | "oom" | "aborted" | "error"
341
+ error.exitCode // number | null
342
+ error.message // Human-readable description
343
+
344
+ // Factory methods
345
+ ContainerError.timeout(spaceId)
346
+ ContainerError.oom(spaceId, exitCode)
347
+ ContainerError.aborted(spaceId)
348
+ ContainerError.error(exitCode, output)
349
+ ```