mercury-agent 0.22.0 → 0.23.0-beta.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.
- package/README.md +2 -2
- package/container/Dockerfile +1 -1
- package/container/Dockerfile.base +15 -1
- package/container/agent-package.json +1 -1
- package/container/build.sh +6 -3
- package/docs/configuration.md +207 -1
- package/docs/container-lifecycle.md +24 -8
- package/docs/deployment.md +24 -4
- package/docs/extensions.md +11 -5
- package/docs/goals/feed-watch-social-sources/decisions.md +390 -1
- package/docs/goals/feed-watch-social-sources/roadmap.md +15 -5
- package/docs/goals/feed-watch-social-sources/source-review-task-draft.md +80 -0
- package/docs/goals/football-match-day-mode/goal.md +2 -2
- package/docs/goals/football-match-day-mode/roadmap.md +2 -2
- package/docs/goals/football-reporter-profile/decisions.md +143 -0
- package/docs/goals/football-reporter-profile/roadmap.md +10 -7
- package/docs/goals/rehearsal-bench/decisions.md +76 -0
- package/docs/goals/rehearsal-bench/roadmap.md +2 -1
- package/docs/goals/release-gate/decisions.md +118 -0
- package/docs/goals/release-gate/goal.md +1 -1
- package/docs/goals/release-gate/roadmap.md +6 -6
- package/docs/goals/whatsapp-bot-hardening/decisions.md +53 -0
- package/docs/goals/whatsapp-bot-hardening/roadmap.md +15 -1
- package/docs/live-testing.md +57 -110
- package/docs/pending-verification.md +315 -16
- package/docs/pipeline.md +4 -1
- package/docs/profile-guide.md +6 -2
- package/docs/release-gate.md +276 -0
- package/docs/smoke-fixtures.md +245 -0
- package/examples/extensions/feed-watch/config.ts +103 -4
- package/examples/extensions/feed-watch/digest.ts +465 -19
- package/examples/extensions/feed-watch/feeds.ts +124 -10
- package/examples/extensions/feed-watch/index.ts +111 -1
- package/examples/extensions/feed-watch/items.ts +147 -10
- package/examples/extensions/feed-watch/match.ts +77 -15
- package/examples/extensions/feed-watch/posted.ts +275 -0
- package/examples/extensions/feed-watch/prompts/verify.md +9 -0
- package/examples/extensions/feed-watch/scorecard.ts +232 -0
- package/examples/extensions/feed-watch/skill/SKILL.md +80 -10
- package/examples/extensions/feed-watch/watch.ts +531 -38
- package/examples/extensions/feed-watch/yield.ts +221 -0
- package/examples/extensions/gws/index.ts +150 -14
- package/examples/extensions/longview/hook.ts +63 -4
- package/examples/extensions/longview/index.ts +9 -0
- package/examples/extensions/longview/render/telegraph-nodes.ts +56 -10
- package/examples/extensions/morning/index.ts +1 -1
- package/examples/extensions/napkin/index.ts +167 -64
- package/examples/extensions/napkin/pi-spawn.ts +157 -2
- package/examples/extensions/poster/index.ts +13 -3
- package/examples/extensions/tradestation/host/refresh.ts +20 -0
- package/examples/extensions/tradestation/index.ts +81 -1
- package/examples/extensions/voice-transcribe/index.ts +1 -1
- package/examples/extensions/yahoo-mail/index.ts +23 -2
- package/examples/extensions/yahoo-mail/lib/ymail.ts +147 -0
- package/examples/extensions/yahoo-mail/package.json +1 -1
- package/examples/profiles/_template/config.yaml +11 -1
- package/examples/profiles/football-reporter/AGENTS.md +94 -21
- package/examples/profiles/football-reporter/README.md +47 -0
- package/examples/profiles/football-reporter/config.yaml +246 -60
- package/examples/profiles/football-reporter/seed/MEMORY.md +1 -1
- package/examples/profiles/football-reporter/seed/episodes/man-united-2026-27.md +1 -1
- package/examples/profiles/football-reporter/seed/names.md +56 -0
- package/examples/profiles/football-reporter/standard.json +46 -6
- package/examples/profiles/football-reporter/tasks/daily-article.md +102 -30
- package/examples/profiles/football-reporter/tasks/memes.md +76 -0
- package/examples/profiles/football-reporter/tasks/roundup.md +8 -1
- package/examples/smoke/club-newsroom/smoke.yaml +46 -0
- package/examples/smoke/proposal-desk/brief.md +8 -0
- package/examples/smoke/proposal-desk/smoke.yaml +26 -0
- package/package.json +15 -11
- package/src/adapters/whatsapp-media.ts +46 -33
- package/src/agent/container-runner.ts +4 -2
- package/src/agent/image-refresh.ts +27 -10
- package/src/cli/build-context.ts +119 -0
- package/src/cli/mercury.ts +660 -331
- package/src/cli/upgrade.ts +392 -2
- package/src/config-file.ts +6 -0
- package/src/config.ts +73 -3
- package/src/core/commands.ts +17 -1
- package/src/core/connection-health.ts +278 -151
- package/src/core/heartbeat.ts +204 -0
- package/src/core/inbox-write.ts +148 -0
- package/src/core/operator-alerts.ts +210 -0
- package/src/core/reporter-lint-on-send.ts +294 -0
- package/src/core/routes/chat.ts +44 -6
- package/src/core/routes/config-builtin.ts +27 -0
- package/src/core/routes/dashboard.ts +22 -0
- package/src/core/routes/shadow.ts +470 -0
- package/src/core/runtime.ts +431 -16
- package/src/core/shadow-bridge.ts +64 -9
- package/src/core/shadow-outbox.ts +70 -8
- package/src/core/simulate-ingress.ts +226 -69
- package/src/core/system-messages.ts +10 -0
- package/src/core/task-scheduler.ts +78 -0
- package/src/core/task-sender.ts +164 -0
- package/src/env-dump/manifest.ts +6 -2
- package/src/extensions/loader.ts +13 -0
- package/src/extensions/types.ts +109 -7
- package/src/logger.ts +51 -2
- package/src/main.ts +34 -61
- package/src/ops/shadow-service.ts +386 -10
- package/src/ops/shadow-snapshot.ts +124 -3
- package/src/preflight/checks/docker.ts +23 -1
- package/src/preflight/checks/host-deps.ts +130 -0
- package/src/preflight/report.ts +3 -0
- package/src/preflight/run.ts +2 -0
- package/src/profile/readonly-store.ts +118 -0
- package/src/profile/space-profile.ts +170 -7
- package/src/server.ts +32 -0
- package/src/smoke/boot.ts +303 -0
- package/src/smoke/cli.ts +256 -0
- package/src/smoke/evaluate.ts +401 -0
- package/src/smoke/fixture.ts +387 -0
- package/src/smoke/refusal.ts +96 -0
- package/src/smoke/run.ts +355 -0
- package/src/storage/db.ts +200 -10
- package/src/storage/pi-auth.ts +42 -10
- package/src/text/reporter-lint.ts +501 -74
- package/src/types.ts +15 -0
package/README.md
CHANGED
|
@@ -292,7 +292,7 @@ mercury service uninstall
|
|
|
292
292
|
mercury service status
|
|
293
293
|
mercury service logs [-f]
|
|
294
294
|
|
|
295
|
-
# upgrade (stops the service, installs globally, restarts)
|
|
295
|
+
# upgrade (pulls the matching agent image first, then stops the service, installs globally, restarts; --dry-run prints the plan)
|
|
296
296
|
mercury upgrade # latest
|
|
297
297
|
mercury upgrade 0.14.0 # a specific version
|
|
298
298
|
```
|
|
@@ -420,7 +420,7 @@ Supported OAuth providers: Anthropic, GitHub Copilot, Google Gemini CLI, Antigra
|
|
|
420
420
|
|
|
421
421
|
| Variable | Default | Description |
|
|
422
422
|
|----------|---------|-------------|
|
|
423
|
-
| `MERCURY_AGENT_IMAGE` | `ghcr.io/avishai-tsabari/mercury-agent
|
|
423
|
+
| `MERCURY_AGENT_IMAGE` | `ghcr.io/avishai-tsabari/mercury-agent:<version>` | Container image; pinned to the installed version, see `docs/configuration.md` § Agent image |
|
|
424
424
|
| `MERCURY_CONTAINER_TIMEOUT_MS` | `300000` | Container timeout (5 min) |
|
|
425
425
|
| `MERCURY_CONTAINER_RUNTIME` | `runc` | `runc` (default) or `runsc` ([gVisor](https://gvisor.dev)) |
|
|
426
426
|
| `MERCURY_CONTAINER_BWRAP_DOCKER_COMPAT` | `false` | Set `true` on Linux Docker Engine (see note below) |
|
package/container/Dockerfile
CHANGED
|
@@ -60,7 +60,7 @@ RUN echo '{"args":["--no-sandbox"]}' > /home/mercury/.puppeteerrc.json
|
|
|
60
60
|
ENV CHROMIUM_FLAGS="--no-sandbox"
|
|
61
61
|
|
|
62
62
|
# Install CLIs
|
|
63
|
-
RUN bun add -g @earendil-works/pi-coding-agent@~0.
|
|
63
|
+
RUN bun add -g @earendil-works/pi-coding-agent@~0.85.1
|
|
64
64
|
|
|
65
65
|
WORKDIR /app
|
|
66
66
|
|
|
@@ -42,7 +42,7 @@ RUN echo '{"args":["--no-sandbox"]}' > /home/mercury/.puppeteerrc.json
|
|
|
42
42
|
ENV CHROMIUM_FLAGS="--no-sandbox"
|
|
43
43
|
|
|
44
44
|
# Install CLIs
|
|
45
|
-
RUN bun add -g @earendil-works/pi-coding-agent@~0.
|
|
45
|
+
RUN bun add -g @earendil-works/pi-coding-agent@~0.85.1
|
|
46
46
|
|
|
47
47
|
WORKDIR /app
|
|
48
48
|
|
|
@@ -95,6 +95,20 @@ RUN while IFS= read -r ext || [ -n "$ext" ]; do \
|
|
|
95
95
|
RUN echo '#!/bin/sh\nbun run /app/src/cli/mrctl.ts "$@"' > /usr/local/bin/mrctl && \
|
|
96
96
|
chmod +x /usr/local/bin/mrctl
|
|
97
97
|
|
|
98
|
+
# Same stamp as `container/Dockerfile`, generated from THIS file so the
|
|
99
|
+
# manifest cannot drift from the tier that was actually built. The host's
|
|
100
|
+
# drift check hashes its own `container/Dockerfile`; an image cut from this
|
|
101
|
+
# tier will report that as a difference, which is true and is the point.
|
|
102
|
+
ARG MERCURY_VERSION=unknown
|
|
103
|
+
COPY container/Dockerfile.base /app/container/Dockerfile.base
|
|
104
|
+
COPY src/agent/image-manifest.ts /app/src/agent/image-manifest.ts
|
|
105
|
+
RUN bun run /app/src/agent/image-manifest.ts \
|
|
106
|
+
/app/container/Dockerfile.base "$MERCURY_VERSION" > /app/image-manifest.json && \
|
|
107
|
+
grep -q 'src/agent/container-entry.ts' /app/image-manifest.json
|
|
108
|
+
LABEL org.opencontainers.image.version="${MERCURY_VERSION}" \
|
|
109
|
+
org.opencontainers.image.title="mercury-agent" \
|
|
110
|
+
com.mercury.version="${MERCURY_VERSION}"
|
|
111
|
+
|
|
98
112
|
# Fix ownership of all mercury home dir artifacts before switching user
|
|
99
113
|
# `mkdir` here rather than leaving it to Docker: the host mounts the global dir
|
|
100
114
|
# into PI_CODING_AGENT_DIR entry by entry, so Docker would create this dir as
|
package/container/build.sh
CHANGED
|
@@ -31,9 +31,12 @@ docker build -f container/Dockerfile \
|
|
|
31
31
|
-t "${IMAGE_NAME}:${VERSION}" \
|
|
32
32
|
-t "${IMAGE_NAME}:latest" .
|
|
33
33
|
|
|
34
|
-
# Both tags, on purpose.
|
|
35
|
-
#
|
|
36
|
-
#
|
|
34
|
+
# Both tags, on purpose. `mercury-agent:latest` is the explicit local override
|
|
35
|
+
# a source-run box sets (`agent.image` / MERCURY_AGENT_IMAGE), so dropping it
|
|
36
|
+
# would break every such host; `mercury-agent:<version>` mirrors the registry
|
|
37
|
+
# tag the default now pins to (release-gate R0.6 — `src/config.ts` defaults to
|
|
38
|
+
# `ghcr.io/…/mercury-agent:<version>`, which `mercury build` tags as well).
|
|
39
|
+
# Neither local name has a registry host, so neither is ever pulled.
|
|
37
40
|
echo "✓ Built ${IMAGE_NAME}:${VERSION} and ${IMAGE_NAME}:latest"
|
|
38
41
|
echo ""
|
|
39
42
|
echo "Version label:"
|
package/docs/configuration.md
CHANGED
|
@@ -20,7 +20,9 @@ These must be supplied via environment variables only; they are **not** read fro
|
|
|
20
20
|
|
|
21
21
|
- `MERCURY_API_SECRET`
|
|
22
22
|
- `MERCURY_CHAT_API_KEY`
|
|
23
|
+
- `MERCURY_SHADOW_API_SECRET` (a shadow's own; see [Shadow mode](#shadow-mode-mercury_shadow))
|
|
23
24
|
- `MERCURY_DISCORD_GATEWAY_SECRET`
|
|
25
|
+
- `MERCURY_HEARTBEAT_URL` (see [Heartbeat](#heartbeat-mercury_heartbeat_url-mercury_heartbeat_tasks))
|
|
24
26
|
|
|
25
27
|
Platform tokens, provider API keys, and extension keys (e.g. `MERCURY_TELEGRAM_BOT_TOKEN`, `MERCURY_BRAVE_API_KEY`) are also **env-only** today—they are not part of the YAML schema.
|
|
26
28
|
|
|
@@ -103,6 +105,107 @@ Restarting *before* the make-up run lands drops it, the same way a restart drops
|
|
|
103
105
|
a pending retry: the boot pass leaves `next_run_at` off the cron grid, and the
|
|
104
106
|
next boot realigns normally.
|
|
105
107
|
|
|
108
|
+
## Heartbeat (`MERCURY_HEARTBEAT_URL`, `MERCURY_HEARTBEAT_TASKS`)
|
|
109
|
+
|
|
110
|
+
Nothing on the box can alert while the box is off. Twice in six days the box
|
|
111
|
+
was down overnight (2026-09-05, 2026-09-10) and the 09:00 article with it, and
|
|
112
|
+
the missed-run report above could only speak once the box was back. The
|
|
113
|
+
heartbeat is the reader that lives somewhere else: after a listed task
|
|
114
|
+
**delivers**, the host GETs a URL, and the *service* behind that URL alerts the
|
|
115
|
+
owner's phone when the ping is late. A dead box, a dead WSL distro and a dead
|
|
116
|
+
WhatsApp socket are all heard the same way, because nothing here has to be
|
|
117
|
+
alive to raise the alarm.
|
|
118
|
+
|
|
119
|
+
Both keys are **env-only** (the URL is a credential — anyone holding it can mark
|
|
120
|
+
the check up — and is listed under Secrets above; `mercury env dump` reports
|
|
121
|
+
both as present or absent and prints neither value, so a swapped pair cannot
|
|
122
|
+
leak the URL through the task list):
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
# .env
|
|
126
|
+
MERCURY_HEARTBEAT_URL=https://hc-ping.com/<ping-key>/mercury-{task}
|
|
127
|
+
MERCURY_HEARTBEAT_TASKS=24,621
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
- **`{task}`** in the URL is replaced by the task id, so one credential serves
|
|
131
|
+
one check per task — with healthchecks.io's slug pings the checks above are
|
|
132
|
+
`mercury-24` and `mercury-621` under one project ping key. A URL without the
|
|
133
|
+
placeholder is fine for a single listed task; with two or more, every ping
|
|
134
|
+
lands on one check and a delivered roundup would silence the alarm for a
|
|
135
|
+
missing article — `mercury preflight` warns on that shape.
|
|
136
|
+
- **When a ping is sent — one rule.** A run of a listed task that recorded
|
|
137
|
+
`status: ok` with `outcome: posted` (the ledger's own words, see
|
|
138
|
+
`mercury env dump` / `task_runs`). **Not** on `no_update` — the article that
|
|
139
|
+
found nothing to say is exactly the case to alert on. Not on an error, not on
|
|
140
|
+
a delivery failure, not for an unlisted task (extension one-shots, chat turns).
|
|
141
|
+
A manual **Run now** that delivers counts; a `silent` task that delivers
|
|
142
|
+
counts (the run ran and produced; whether the group heard it is `silent`'s
|
|
143
|
+
question).
|
|
144
|
+
- **Bounded and never fatal.** One GET with a 5-second bound, after the ledger
|
|
145
|
+
row and the schedule advance, so a slow or dead ping host holds neither
|
|
146
|
+
hostage. Success is `Heartbeat pinged taskId=24 status=200` at info; failure
|
|
147
|
+
is `Heartbeat ping failed taskId=24 reason=ENOTFOUND` at **warn**, never an
|
|
148
|
+
error and never a failed run — a missing ping is what the service alerts on.
|
|
149
|
+
The log line carries the task id and the status; never the URL.
|
|
150
|
+
- **A shadow never pings.** `MERCURY_SHADOW=1` (below) disables the heartbeat
|
|
151
|
+
whatever the copied `.env` says, so a rehearsal cannot mark the live check up.
|
|
152
|
+
- **`mercury preflight` / `mercury doctor` — `host.heartbeat`.** `skipped` on a
|
|
153
|
+
box with no active cron task; `warn` (never `blocked`) when there are cron
|
|
154
|
+
tasks and no URL, a URL and no listed task, a listed id that is not an active
|
|
155
|
+
cron task, a malformed URL, or the shared-check shape above; `pass` otherwise.
|
|
156
|
+
The evidence names task ids, never the URL.
|
|
157
|
+
|
|
158
|
+
**Setting up the service (recommended: healthchecks.io, free tier).** One check
|
|
159
|
+
per listed task, shaped by how often the task *delivers*, not by its cron: only
|
|
160
|
+
a `posted` run pings, a `no_update` slot does not. A task that posts on every
|
|
161
|
+
slot gets a cron-shaped check — the article as `0 9 * * *` Asia/Jerusalem with a
|
|
162
|
+
30-minute grace (alert at 09:30). A task that is allowed to stay silent on a
|
|
163
|
+
slot gets a *simple* check whose period covers its longest expected silence —
|
|
164
|
+
the roundup, where 3 of 9 slots in one week were `no_update`, as period 1 day
|
|
165
|
+
with a 6-hour grace; a cron-shaped check there alerts on every quiet slot.
|
|
166
|
+
Alert channels: email is enough — it reached the owner's phone within the
|
|
167
|
+
grace period the first time the box died (2026-09-12); a push channel
|
|
168
|
+
(Telegram, Signal) is optional. Never the bot's own WhatsApp, which is the
|
|
169
|
+
thing that is down.
|
|
170
|
+
|
|
171
|
+
**One healthchecks.io project per box, named after the box.** One account
|
|
172
|
+
will end up holding checks for several bots — a client's, a second project's,
|
|
173
|
+
this one's. The alert email leads with the *project* name, then the check
|
|
174
|
+
name, and `mercury-24` says nothing about which box missed. A project named
|
|
175
|
+
after the box (`Mercury — home PC (WSL)`) is the cheapest fix and the safest
|
|
176
|
+
shape: the project is the credential boundary — it owns the ping key, so a
|
|
177
|
+
key that leaks from a client's box cannot mark this bot's checks up — and it
|
|
178
|
+
owns its own alert channels and members, so a colleague added to a work
|
|
179
|
+
project never sees a private bot. Renaming the project touches no slug and
|
|
180
|
+
nothing on the box. Rename the checks inside it only if the task is not
|
|
181
|
+
obvious from the name (`article 09:00`, `roundup`, `WSL anchor`), and then
|
|
182
|
+
mind that the name and the **slug** are separate fields: the URL template
|
|
183
|
+
above pings by slug, so a rename must leave the slug (`mercury-24`) as it is
|
|
184
|
+
— read the Slug field in the same dialog before saving, and put it back if
|
|
185
|
+
the new name re-derived it — or the ping starts returning 404 and every
|
|
186
|
+
listed task reads as down. A changed slug is a box-side change: new
|
|
187
|
+
`MERCURY_HEARTBEAT_URL` template in `.env`, a restart in a gap, and the
|
|
188
|
+
anchor loop's own URL.
|
|
189
|
+
|
|
190
|
+
**Optional second signal, not built here.** A box that pings every few minutes
|
|
191
|
+
from outside Mercury gives the same service a "the machine is up" check that is
|
|
192
|
+
independent of any task — and it is the one that speaks first: a dead distro
|
|
193
|
+
is heard within its grace period instead of at 09:30. On the WSL box that is a
|
|
194
|
+
loop started by the owner's anchor script, against its own check
|
|
195
|
+
(`mercury-anchor`, a *simple* check — period 10 min, grace 10 min, pinged every
|
|
196
|
+
5 min):
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
# ~/mercury-anchor.sh — alongside the sleep that holds the distro open
|
|
200
|
+
while true; do curl -fsS -m 10 --retry 2 -o /dev/null "https://hc-ping.com/<ping-key>/mercury-anchor" || true; sleep 300; done &
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
The loop dies with the distro, and the logon task that starts the anchor fires
|
|
204
|
+
at Windows logon only — after a distro restart with no logon (a WSL crash, a
|
|
205
|
+
`wsl --shutdown`) the anchor and the loop are both gone until
|
|
206
|
+
`Start-ScheduledTask 'Start Mercury WSL'` is run again; the check going late a
|
|
207
|
+
second time is how that shows.
|
|
208
|
+
|
|
106
209
|
## Operator alerts (`alerts:`)
|
|
107
210
|
|
|
108
211
|
When a provider OAuth credential can no longer be refreshed, every space stops
|
|
@@ -138,6 +241,49 @@ Env equivalents: **`MERCURY_ALERTS_ENABLED`**, **`MERCURY_ALERTS_COOLDOWN_MS`**.
|
|
|
138
241
|
- **Transient failures** (HTTP 408/429/5xx from the token endpoint) alert only
|
|
139
242
|
after three in a row; a single blip heals itself. A rejected refresh token, or
|
|
140
243
|
a cause that could not be classified, alerts on the first occurrence.
|
|
244
|
+
- **Scheduled-task failures go two ways.** A task a person created reports its
|
|
245
|
+
final failure into its own space, as before (`scheduling.notify_on_failure`).
|
|
246
|
+
A task an **extension** opened — feed-watch's verify one-shots; any task named
|
|
247
|
+
after a loaded extension, or whose `created_by` is not a platform user id
|
|
248
|
+
(`system`, the console, `space-profile`) — reports to the operators above
|
|
249
|
+
instead, and its space hears nothing, even when no operator is reachable
|
|
250
|
+
(then the only record is the ERROR log line). No cooldown: a final failure is
|
|
251
|
+
already once per due cycle.
|
|
252
|
+
- **Partial deliveries.** A scheduled post that reached some of a space's linked
|
|
253
|
+
conversations and not others is **not** retried — a retry would post a second
|
|
254
|
+
copy where the first landed — and the operators are told which conversations
|
|
255
|
+
missed it.
|
|
256
|
+
|
|
257
|
+
## History per conversation, and the reporter lint (per-space)
|
|
258
|
+
|
|
259
|
+
Two behaviours that only matter to a space with **more than one linked
|
|
260
|
+
conversation** or with a **reporter standard** — every other space is
|
|
261
|
+
byte-identical.
|
|
262
|
+
|
|
263
|
+
**History per conversation.** Chat and ambient rows record the conversation
|
|
264
|
+
they were said in (`messages.conversation`, a thread id such as
|
|
265
|
+
`whatsapp:<group-jid>`). When a space has two or more linked conversations, a
|
|
266
|
+
run answering one of them reads only that conversation's rows plus the space's
|
|
267
|
+
own rows — scheduled posts, and rows written before the column existed — and
|
|
268
|
+
never the other conversation's chatter. A scheduled run in such a space reads
|
|
269
|
+
only the space's own rows. A space with one conversation reads exactly what it
|
|
270
|
+
always read. Nothing to configure.
|
|
271
|
+
|
|
272
|
+
**`reporter.standard`** (per space, unset by default) — the path, relative to
|
|
273
|
+
the space directory, of a reporter standard JSON file (the shape of
|
|
274
|
+
`examples/profiles/football-reporter/standard.json`; the football profile
|
|
275
|
+
seeds it there and sets the key). When set, every scheduled reply the space
|
|
276
|
+
sends is linted on the host **after** delivery and the findings are written
|
|
277
|
+
to the journal — one `reporter-lint: finding` line per finding with the rule
|
|
278
|
+
id and the task id, and one `reporter-lint: <n> findings` line per run. The
|
|
279
|
+
reply is never altered or held; an unreadable file is one warn line. A reply
|
|
280
|
+
that carries a `[longview:summary]` block is linted as the article, anything
|
|
281
|
+
else as a scan. The path must stay inside the space directory and end in
|
|
282
|
+
`.json`.
|
|
283
|
+
|
|
284
|
+
```bash
|
|
285
|
+
mrctl config set reporter.standard standard.json
|
|
286
|
+
```
|
|
141
287
|
|
|
142
288
|
## Ambient group context
|
|
143
289
|
|
|
@@ -218,6 +364,28 @@ Snapshot directories are built by `mercury shadow snapshot`, which generates
|
|
|
218
364
|
the shadow's `.env` and `mercury.yaml` — nothing here needs to be set by
|
|
219
365
|
hand. See [live-testing.md](live-testing.md) §2 and §7.
|
|
220
366
|
|
|
367
|
+
**A shadow answers to no live credential.** Every snapshot gets a fresh
|
|
368
|
+
**`MERCURY_SHADOW_API_SECRET`** (64 hex characters), and the generated `.env`
|
|
369
|
+
also overrides `MERCURY_API_SECRET` and `MERCURY_CHAT_API_KEY` with that same
|
|
370
|
+
value — so the live bot's secret opens nothing on the shadow: not `/api/*`,
|
|
371
|
+
not `/chat`, not the dashboard, and not the route below. The value lives
|
|
372
|
+
only in the snapshot's `.env`; `shadow snapshot`, `shadow up`, `shadow
|
|
373
|
+
status`, `/health` and the logs print key names, never the value. Rotation
|
|
374
|
+
is a new snapshot.
|
|
375
|
+
|
|
376
|
+
**`POST /api/shadow/simulate`** (goal `rehearsal-bench` M2.2b) exists only on
|
|
377
|
+
a shadow — on a live process the path is whatever `/api`'s guard says (401
|
|
378
|
+
without the live secret, 404 with it). It is authenticated by
|
|
379
|
+
`MERCURY_SHADOW_API_SECRET` **alone**: unset ⇒ 503 (a snapshot taken before
|
|
380
|
+
this key existed — take a fresh one), mismatch ⇒ 401. The body is a
|
|
381
|
+
`SyntheticWhatsAppEvent` by field name (`application/json`, or
|
|
382
|
+
`multipart/form-data` with an `event` part and `file` parts for attachments);
|
|
383
|
+
`isBotMentioned` / `isReplyToBot` are derived from the stanza and a body
|
|
384
|
+
carrying either is a 400. The response is `text/event-stream`: one `entry`
|
|
385
|
+
event per outbox line as it is journaled, then a `result` event with the
|
|
386
|
+
`SimulateResult`. `/health` reports `simulateRoute: true|false` and
|
|
387
|
+
`mercury shadow status` prints it.
|
|
388
|
+
|
|
221
389
|
## System prompt authorship (`agent.override_pi_system_prompt`)
|
|
222
390
|
|
|
223
391
|
Chooses whether Mercury replaces pi's own system prompt or appends to it:
|
|
@@ -300,7 +468,7 @@ and under an explicit override, the vars reaching every container:
|
|
|
300
468
|
Container env passthrough: explicit `all` override — these vars reach every space's container and are scoped to nothing. […] vars=MERCURY_SCRAPER_API_KEY, MERCURY_BILLING_API_KEY
|
|
301
469
|
```
|
|
302
470
|
|
|
303
|
-
For secrets that only host-side hooks and jobs need, prefer `mercury.env({ from: "…", hostOnly: true })`, which keeps them out of containers in either mode. For credentials the agent should never hold at all, use a host-side capability handler (`mercury.capability()`), which runs the privileged call on the host and returns only the result.
|
|
471
|
+
For secrets that only host-side hooks and jobs need, prefer `mercury.env({ from: "…", hostOnly: true })`, which keeps them out of containers in either mode. For credentials the agent should never hold at all, use a host-side capability handler (`mercury.capability()`), which runs the privileged call on the host and returns only the result. `hostOnly` is a required field: under `claimed` a claim is the only way a `MERCURY_*` var reaches a container, so a claim has to say whether it means to — `hostOnly: false` is a decision that something inside the container reads the var, and the repo gate (`tests/env-claims-declare-hostonly.test.ts`) holds those to an allowlist naming the reader. The loader warns at startup about any installed extension whose claim does not decide.
|
|
304
472
|
|
|
305
473
|
## Container shared memory (`agent.container_shm_size`)
|
|
306
474
|
|
|
@@ -422,6 +590,7 @@ a detail on the page can be answered.
|
|
|
422
590
|
| `longview.enabled` | `false` | Publish long replies from this space. |
|
|
423
591
|
| `longview.threshold_chars` | `2000` | Publish when a reply exceeds this many characters. Minimum 500. |
|
|
424
592
|
| `longview.target` | `telegraph` | Where pages are published. |
|
|
593
|
+
| `longview.sources` | `both` | Which replies are published: `both` (any reply over the threshold, whoever asked), `scheduled` (task runs only — a member's long answer stays in chat), or `chat` (the reverse). A turn whose origin the host did not report is never published under `scheduled` or `chat`. |
|
|
425
594
|
| `longview.summary_model` | *(empty)* | Model that writes the chat summary. Empty uses the space's configured model. |
|
|
426
595
|
|
|
427
596
|
`summary_model` must name a model on the space's **existing provider**. The
|
|
@@ -577,3 +746,40 @@ lands back in `default`.
|
|
|
577
746
|
The **Cursor Agent CLI** integration has been removed. All model legs use **pi** with standard providers (`anthropic`, `openai`, `google`, `mistral`, `groq`, `openrouter`, etc.).
|
|
578
747
|
|
|
579
748
|
If your chain still has `provider: cursor`, the agent run **fails fast** with an error that points here. Switch to the **native provider** for the model you want (for example `anthropic` for Claude, `openai` for GPT) and set the matching **`MERCURY_*_API_KEY`**.
|
|
749
|
+
|
|
750
|
+
## Agent image (`agent.image`)
|
|
751
|
+
|
|
752
|
+
Which container image every agent run spawns:
|
|
753
|
+
|
|
754
|
+
```yaml
|
|
755
|
+
agent:
|
|
756
|
+
image: mercury-agent:latest # a locally built image (container/build.sh or `mercury build`)
|
|
757
|
+
```
|
|
758
|
+
|
|
759
|
+
Env: `MERCURY_AGENT_IMAGE`.
|
|
760
|
+
|
|
761
|
+
**The default is the registry image at this host's own version** —
|
|
762
|
+
`ghcr.io/avishai-tsabari/mercury-agent:<version>`, where `<version>` is the
|
|
763
|
+
installed package's `package.json` version (release-gate R0.6, since 0.22.0;
|
|
764
|
+
before that the default floated on `:latest`). Nothing needs to be set for it,
|
|
765
|
+
and a host upgrade changes the reference by itself, so the box asks for the
|
|
766
|
+
new tag on its next boot. The tag is what `release.yml` pushes on every
|
|
767
|
+
release; the package is private, so a box on the default needs a registry
|
|
768
|
+
token that can read it, and a box that has none should set an explicit image
|
|
769
|
+
instead. `mercury preflight`'s `docker.image` check names the pinned tag, the
|
|
770
|
+
host version and both ways out when the tag is absent — that is the check to
|
|
771
|
+
read, not the first failed spawn.
|
|
772
|
+
|
|
773
|
+
**An explicit value is kept exactly as written.** Nothing normalises it, and
|
|
774
|
+
the pin never rewrites it: a locally built `mercury-agent:latest`, another
|
|
775
|
+
registry, a digest-pinned reference. Two of those shapes are never pulled or
|
|
776
|
+
re-pulled — a name with no registry host (`mercury-agent:latest`,
|
|
777
|
+
`mercury-agent-ext:<hash>`) is a local build, and a digest cannot drift; see
|
|
778
|
+
[container-lifecycle.md](container-lifecycle.md) § Staying Current. An
|
|
779
|
+
explicit *floating* tag with a registry host is re-pulled when the host
|
|
780
|
+
version changes, as before.
|
|
781
|
+
|
|
782
|
+
`mercury env dump --json` reports the key with its source
|
|
783
|
+
(`config.keys[].key == "agentContainerImage"`, `source` one of `default`,
|
|
784
|
+
`file`, `env`), which is how a deploy tells a box on the pinned default from
|
|
785
|
+
one with an override before restarting it.
|
|
@@ -182,8 +182,9 @@ Message received
|
|
|
182
182
|
# Set container timeout to 10 minutes
|
|
183
183
|
export MERCURY_CONTAINER_TIMEOUT_MS=600000
|
|
184
184
|
|
|
185
|
-
#
|
|
186
|
-
|
|
185
|
+
# Override the agent image. The default needs no setting: it is the registry
|
|
186
|
+
# image at this host's own version, ghcr.io/avishai-tsabari/mercury-agent:<version>
|
|
187
|
+
export MERCURY_AGENT_IMAGE=mercury-agent:latest # a local `mercury build` / container/build.sh
|
|
187
188
|
```
|
|
188
189
|
|
|
189
190
|
## Sandboxing (Bubblewrap)
|
|
@@ -336,12 +337,24 @@ Mercury publishes an image preset to GitHub Container Registry:
|
|
|
336
337
|
|
|
337
338
|
| Preset | Size | Contents |
|
|
338
339
|
|--------|------|----------|
|
|
339
|
-
| `ghcr.io/avishai-tsabari/mercury-agent
|
|
340
|
+
| `ghcr.io/avishai-tsabari/mercury-agent:<version>` | ~2.8GB | Full devcontainer: Bun, Node.js, Python, Go, git, build tools |
|
|
340
341
|
|
|
341
342
|
Images are published on each release by `release.yml`'s `publish-image` job,
|
|
342
343
|
which builds from the published npm tarball and pushes `:<version>` alongside
|
|
343
344
|
`:latest`. A prerelease publishes its own tag only and leaves `:latest` alone.
|
|
344
345
|
|
|
346
|
+
**The default is pinned to the host's version** (release-gate R0.6, since
|
|
347
|
+
0.22.0): a host running mercury `X.Y.Z` with no `agent.image` override asks
|
|
348
|
+
for `ghcr.io/avishai-tsabari/mercury-agent:X.Y.Z` — never `:latest`. So a
|
|
349
|
+
host upgrade changes the reference, the new tag is absent locally, and boot
|
|
350
|
+
pulls it (or fails loudly if it cannot); `mercury preflight`'s `docker.image`
|
|
351
|
+
check names the pinned tag, the host version and the two ways out when the
|
|
352
|
+
tag is missing. An explicit override (`agent.image` in `mercury.yaml`,
|
|
353
|
+
`MERCURY_AGENT_IMAGE`) is kept byte-for-byte: a source-run box that builds
|
|
354
|
+
its own image sets `mercury-agent:latest` and is untouched by the pin. The
|
|
355
|
+
package is private (G-007), so a box on the default needs a registry token
|
|
356
|
+
that can read it; the boot line and preflight are where its absence shows.
|
|
357
|
+
|
|
345
358
|
> Before 0.19.0 this sentence was aspirational: nothing in CI built the image,
|
|
346
359
|
> the registry copy was hand-pushed and stale, and an install outside the
|
|
347
360
|
> tagula fleet had no image to pull. See `docs/goals/release-gate/decisions.md`
|
|
@@ -354,8 +367,11 @@ A floating tag used to mean "whatever this box pulled the first time":
|
|
|
354
367
|
upgraded through npm kept running the old container against new host code with
|
|
355
368
|
`/health` at 200 and nothing in the log.
|
|
356
369
|
|
|
357
|
-
|
|
358
|
-
|
|
370
|
+
Two things close that. The default is now pinned to the host's version (above),
|
|
371
|
+
so on a default box the question is simply whether `:<version>` is present.
|
|
372
|
+
And for any **floating** reference an operator sets explicitly, startup
|
|
373
|
+
re-pulls the image whenever the host's Mercury version is not the one that
|
|
374
|
+
last pulled it, recording the pair in `<dataDir>/image-pull.json`.
|
|
359
375
|
Two references are never re-pulled: a **digest-pinned** one (`…@sha256:…`),
|
|
360
376
|
which cannot drift, and a **locally built** one, which exists in no registry —
|
|
361
377
|
so a pull would fail on every boot. "Locally built" means any name with no
|
|
@@ -371,9 +387,9 @@ image id, repo digest, version label and whether a refresh happened, so drift
|
|
|
371
387
|
is visible without being asked for:
|
|
372
388
|
|
|
373
389
|
```
|
|
374
|
-
Agent image resolved image=ghcr.io/avishai-tsabari/mercury-agent:
|
|
375
|
-
imageId=sha256:… digest=…@sha256:… imageVersion=
|
|
376
|
-
refresh=
|
|
390
|
+
Agent image resolved image=ghcr.io/avishai-tsabari/mercury-agent:0.22.0
|
|
391
|
+
imageId=sha256:… digest=…@sha256:… imageVersion=0.22.0 hostVersion=0.22.0
|
|
392
|
+
refresh=pulled reason=not-present
|
|
377
393
|
```
|
|
378
394
|
|
|
379
395
|
`imageVersion` is the image's `com.mercury.version` label (release-gate R0.1
|
package/docs/deployment.md
CHANGED
|
@@ -102,12 +102,32 @@ Not currently supported via `mercury service`. Options:
|
|
|
102
102
|
## Upgrading
|
|
103
103
|
|
|
104
104
|
```bash
|
|
105
|
-
mercury upgrade
|
|
106
|
-
mercury upgrade 0.14.0
|
|
105
|
+
mercury upgrade # to the latest published version
|
|
106
|
+
mercury upgrade 0.14.0 # to a specific version
|
|
107
|
+
mercury upgrade --dry-run # print the plan, do nothing
|
|
108
|
+
mercury upgrade --skip-image-pull # host only — for a box on a locally built image
|
|
107
109
|
```
|
|
108
110
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
+
**Image first, host second** (release-gate R0.5). The command resolves what
|
|
112
|
+
you asked for to one exact version (`latest`, a dist-tag or a range go
|
|
113
|
+
through `npm view`), then **pulls the agent image that version's host will
|
|
114
|
+
ask for** — the default is pinned to the version, so that is
|
|
115
|
+
`ghcr.io/avishai-tsabari/mercury-agent:<version>` — and refuses when the
|
|
116
|
+
registry has no such tag or refuses the credential, **leaving the host
|
|
117
|
+
untouched**. Only then, on macOS and Linux, does it stop the managed service,
|
|
118
|
+
install the new version globally and restart the service. It installs the
|
|
119
|
+
exact version it pulled the image for, not the spec, so the two cannot drift
|
|
120
|
+
apart between the pull and the install. Every step prints a line; the last
|
|
121
|
+
one names `mercury preflight` as what to run next from the project
|
|
122
|
+
directory (the derived image rebuilds on the first run, as before).
|
|
123
|
+
|
|
124
|
+
An explicit `agent.image` is respected: a registry reference is pulled as
|
|
125
|
+
configured (if it pins another version, preflight's `image.contract` says so
|
|
126
|
+
after the restart); a **local build** (`mercury-agent:latest`) cannot be
|
|
127
|
+
fetched, so the upgrade refuses unless you pass `--skip-image-pull` — then
|
|
128
|
+
run `mercury build` before the first run on the new version. The rules are
|
|
129
|
+
`docs/release-gate.md` Gate 4; `upgrade` does not run preflight itself
|
|
130
|
+
because it runs outside any project directory.
|
|
111
131
|
|
|
112
132
|
### Windows: stop Mercury before upgrading
|
|
113
133
|
|
package/docs/extensions.md
CHANGED
|
@@ -95,17 +95,23 @@ Can only be called once per extension.
|
|
|
95
95
|
|
|
96
96
|
### `mercury.env(def)`
|
|
97
97
|
|
|
98
|
-
Declare an environment variable this extension needs
|
|
98
|
+
Declare an environment variable this extension needs, and decide whether it enters the container. Claimed vars are excluded from the blind `MERCURY_*` passthrough; a claim with `hostOnly: false` is injected into containers only when the caller has permission for this extension.
|
|
99
99
|
|
|
100
100
|
```typescript
|
|
101
|
-
mercury.env({ from: "
|
|
102
|
-
mercury.env({ from: "MERCURY_GH_TOKEN",
|
|
103
|
-
mercury.env({ from: "
|
|
101
|
+
mercury.env({ from: "MERCURY_STT_API_KEY", hostOnly: true }); // host-side only — never enters a container
|
|
102
|
+
mercury.env({ from: "MERCURY_GH_TOKEN", hostOnly: false }); // injected as GH_TOKEN
|
|
103
|
+
mercury.env({ from: "MERCURY_GH_TOKEN", as: "GITHUB_TOKEN", hostOnly: false }); // custom container name
|
|
104
104
|
```
|
|
105
105
|
|
|
106
106
|
- `from` — env var name as set in `.env` (e.g. `MERCURY_GH_TOKEN`)
|
|
107
107
|
- `as` — (optional) name inside the container. Defaults to `from` with `MERCURY_` prefix stripped
|
|
108
|
-
- `hostOnly` —
|
|
108
|
+
- `hostOnly` — **required**, no default. `true`: claim the var (kept out of the blind `MERCURY_*` passthrough) but **never inject it into any container** — secrets consumed only by host-side hooks/jobs (an STT API key used in `before_container`, an OAuth refresh token the host exchanges, a credential a `mercury.capability()` handler uses). `false`: something inside the container reads it — name that reader, because a reviewer will ask.
|
|
109
|
+
|
|
110
|
+
**Why it is required.** The claim loop in `src/core/runtime.ts` skips a claim only when `hostOnly` is `true`; a claim that did not decide was injected. Under the default `claimed` passthrough (`docs/configuration.md`, *Container env passthrough*) a claim is the *only* way a `MERCURY_*` var reaches a container, so a bare claim was strictly wider than not claiming at all — the one call an author made in order to be careful about a secret was the call that exposed it. It shipped three times (`docs/debug/moderate/2026-09-01-tradestation-credentials-enter-the-container.md`, `docs/debug/moderate/2026-09-12-gws-env-claim-bypasses-the-credential-shape-guard.md`) before the field became required.
|
|
111
|
+
|
|
112
|
+
**The gate.** `tests/env-claims-declare-hostonly.test.ts` walks every `mercury.env()` call in `examples/**/index.ts` and `src/**`, loads every example extension through the real API, and asserts that the set of container-visible claims **equals** `ENV_CLAIM_ALLOWLIST` — a row per `hostOnly: false` claim naming the file inside the container that reads it. A new container-visible claim without a row, a bare claim, or a row whose reader no longer mentions the key is a red `bun run check`. The three rows today: `poster`'s two provider keys (the poster CLI runs inside the container) and `gws`'s legacy credentials-file path (a path, not a secret). Everything else is `hostOnly: true`.
|
|
113
|
+
|
|
114
|
+
**On a live box.** The type cannot reach an extension written outside this repo, so `ExtensionLoader` logs `Extension "<name>": env claim "<VAR>" does not state hostOnly; it is container-visible …` for each bare claim it loads. The extension still loads and behaves as before; the line is the reader.
|
|
109
115
|
|
|
110
116
|
Can be called multiple times for multiple env vars.
|
|
111
117
|
|