mercury-agent 0.4.28 → 0.5.1

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 (122) hide show
  1. package/README.md +451 -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/ARCHITECTURE.md +4 -0
  8. package/docs/ROADMAP.md +4 -5
  9. package/docs/{backlog/applicative-profiles.md → archive/applicative-profiles/2026-07-01-applicative-profiles.md} +29 -18
  10. package/docs/{backlog/caller-bound-capability-token.md → archive/applicative-profiles/2026-07-01-caller-bound-capability-token.md} +15 -11
  11. package/docs/archive/summarization/2026-07-02-recent-dev-summary.md +71 -0
  12. package/docs/auth/dashboard.md +28 -28
  13. package/docs/auth/overview.md +109 -109
  14. package/docs/auth/whatsapp.md +173 -173
  15. package/docs/authoring-profiles.md +174 -0
  16. package/docs/configuration.md +54 -54
  17. package/docs/container-lifecycle.md +349 -349
  18. package/docs/{bugs/bwrap-privileged-linux-docker.md → debug/major/2026-07-02-bwrap-privileged-linux-docker.md} +21 -1
  19. package/docs/{bugs/e2big-prompt-delivery-fix.md → debug/major/2026-07-02-e2big-prompt-delivery-fix.md} +27 -1
  20. package/docs/{bugs/registry-pull-no-local-fallback.md → debug/major/2026-07-02-registry-pull-no-local-fallback.md} +27 -1
  21. package/docs/debug/summarization/2026-07-02-common-bug-patterns.md +72 -0
  22. package/docs/deployment.md +199 -199
  23. package/docs/extensions.md +375 -375
  24. package/docs/graceful-shutdown.md +62 -62
  25. package/docs/kb-distillation.md +77 -77
  26. package/docs/media/overview.md +140 -140
  27. package/docs/media/whatsapp.md +171 -171
  28. package/docs/memory.md +137 -137
  29. package/docs/permissions.md +217 -217
  30. package/docs/pipeline.md +228 -228
  31. package/docs/prd-chat-memory.md +76 -76
  32. package/docs/prd-config-load.md +82 -82
  33. package/docs/rate-limiting.md +229 -229
  34. package/docs/scheduler.md +288 -288
  35. package/docs/setup-discord.md +100 -100
  36. package/docs/setup-slack.md +119 -119
  37. package/docs/setup-whatsapp.md +94 -94
  38. package/docs/subagents.md +166 -166
  39. package/docs/web-search.md +62 -62
  40. package/examples/extensions/README.md +12 -12
  41. package/examples/extensions/charts/index.ts +13 -13
  42. package/examples/extensions/charts/skill/SKILL.md +98 -98
  43. package/examples/extensions/gws/README.md +52 -52
  44. package/examples/extensions/gws/skill/SKILL.md +57 -57
  45. package/examples/extensions/gws/skill/references/calendar.md +101 -101
  46. package/examples/extensions/gws/skill/references/docs.md +65 -65
  47. package/examples/extensions/gws/skill/references/drive.md +79 -79
  48. package/examples/extensions/gws/skill/references/gmail.md +85 -85
  49. package/examples/extensions/gws/skill/references/sheets.md +60 -60
  50. package/examples/extensions/napkin/skill/SKILL.md +728 -728
  51. package/examples/extensions/pdf/skill/LICENSE.txt +30 -30
  52. package/examples/extensions/pdf/skill/SKILL.md +314 -314
  53. package/examples/extensions/pdf/skill/forms.md +294 -294
  54. package/examples/extensions/pdf/skill/reference.md +611 -611
  55. package/examples/extensions/pdf/skill/scripts/check_bounding_boxes.py +65 -65
  56. package/examples/extensions/pdf/skill/scripts/check_fillable_fields.py +11 -11
  57. package/examples/extensions/pdf/skill/scripts/convert_pdf_to_images.py +33 -33
  58. package/examples/extensions/pdf/skill/scripts/create_validation_image.py +37 -37
  59. package/examples/extensions/pdf/skill/scripts/extract_form_field_info.py +122 -122
  60. package/examples/extensions/pdf/skill/scripts/extract_form_structure.py +115 -115
  61. package/examples/extensions/pdf/skill/scripts/fill_fillable_fields.py +98 -98
  62. package/examples/extensions/pdf/skill/scripts/fill_pdf_form_with_annotations.py +107 -107
  63. package/examples/extensions/permission-guard/index.ts +65 -65
  64. package/examples/extensions/pinchtab/skill/SKILL.md +224 -224
  65. package/examples/extensions/pinchtab/skill/TRUST.md +69 -69
  66. package/examples/extensions/pinchtab/skill/references/api.md +297 -297
  67. package/examples/extensions/pinchtab/skill/references/env.md +45 -45
  68. package/examples/extensions/pinchtab/skill/references/profiles.md +107 -107
  69. package/examples/extensions/tradestation/host/refresh.ts +102 -102
  70. package/examples/extensions/tradestation/index.ts +153 -153
  71. package/examples/extensions/tradestation/skill/SKILL.md +67 -67
  72. package/examples/extensions/voice-synth/index.ts +94 -94
  73. package/examples/extensions/voice-synth/skill/SKILL.md +38 -38
  74. package/examples/extensions/voice-transcribe/requirements.txt +8 -8
  75. package/examples/extensions/voice-transcribe/scripts/transcribe.py +179 -179
  76. package/examples/extensions/voice-transcribe/skill/SKILL.md +53 -53
  77. package/examples/extensions/yahoo-mail/cli/package.json +13 -13
  78. package/examples/extensions/yahoo-mail/skill/SKILL.md +78 -78
  79. package/package.json +106 -106
  80. package/resources/agents/explore.md +50 -50
  81. package/resources/agents/worker.md +24 -24
  82. package/resources/connection-env-vars.json +25 -25
  83. package/resources/pi-extensions/subagent/agents.ts +126 -126
  84. package/resources/pi-extensions/subagent/index.ts +964 -964
  85. package/resources/profiles/coding/AGENTS.md +43 -43
  86. package/resources/profiles/coding/mercury-profile.yaml +15 -15
  87. package/resources/profiles/general/AGENTS.md +31 -31
  88. package/resources/profiles/general/mercury-profile.yaml +15 -15
  89. package/resources/profiles/research/AGENTS.md +40 -40
  90. package/resources/profiles/research/mercury-profile.yaml +15 -15
  91. package/resources/skills/config/SKILL.md +25 -25
  92. package/resources/skills/context/SKILL.md +33 -33
  93. package/resources/skills/conversation-recap/SKILL.md +19 -19
  94. package/resources/skills/mutes/SKILL.md +31 -31
  95. package/resources/skills/permissions/SKILL.md +19 -19
  96. package/resources/skills/preferences/SKILL.md +31 -31
  97. package/resources/skills/recall/SKILL.md +24 -24
  98. package/resources/skills/roles/SKILL.md +18 -18
  99. package/resources/skills/spaces/SKILL.md +18 -18
  100. package/resources/skills/tasks/SKILL.md +45 -45
  101. package/resources/templates/AGENTS.md +157 -157
  102. package/resources/templates/env.template +38 -38
  103. package/resources/templates/mercury.example.yaml +15 -0
  104. package/src/agent/container-runner.ts +23 -0
  105. package/src/cli/mercury.ts +38 -6
  106. package/src/cli/mrctl.ts +25 -0
  107. package/src/config-file.ts +3 -0
  108. package/src/config.ts +15 -0
  109. package/src/core/api.ts +23 -3
  110. package/src/core/caller-token.ts +101 -0
  111. package/src/core/permissions.ts +37 -5
  112. package/src/core/profiles.ts +110 -84
  113. package/src/core/routes/capability.ts +70 -0
  114. package/src/core/routes/index.ts +1 -0
  115. package/src/core/runtime.ts +14 -0
  116. package/src/dashboard/index.html +729 -729
  117. package/src/extensions/api.ts +21 -0
  118. package/src/extensions/loader.ts +15 -0
  119. package/src/extensions/types.ts +55 -0
  120. package/src/main.ts +19 -0
  121. package/docs/pending-updates/dm-auto-space.md +0 -15
  122. /package/docs/archive/{2026-07-01-dm-auto-space.md → applicative-profiles/2026-07-01-dm-auto-space.md} +0 -0
@@ -28,3 +28,7 @@
28
28
  ## Extension Points
29
29
 
30
30
  > Where the system is designed to be extended — plugin interfaces, hook points, configuration surfaces.
31
+
32
+ - **Applicative profiles** (`src/core/profiles.ts`) — a `mercury-profile.yaml` declares `capabilities`, an exhaustive `member_permissions` set, and a `system_prompt`. `applyProfile` validates capabilities and persists `.mercury/active-profile.json`; `main.ts` activates it at startup (project-wide). Member permission scoping is in `getRolePermissions` (`src/core/permissions.ts`). Authoring contract: `docs/authoring-profiles.md`.
33
+ - **Host-side capability broker** — extensions register `mercury.capability(name, handler)` (`src/extensions/types.ts`, `api.ts`); the agent invokes them from the container via `mrctl capability <name> <action>` → `POST /api/capability/:name/:action` (`src/core/routes/capability.ts`). Handlers run on the host with credentials that never enter the container; identity is the token-derived caller.
34
+ - **Caller-bound capability token** (`src/core/caller-token.ts`) — per-turn HMAC token minted at container spawn, made authoritative over spoofable identity headers in the control-plane API auth middleware (`src/core/api.ts`).
package/docs/ROADMAP.md CHANGED
@@ -10,7 +10,7 @@ Single source of truth for priority. Rule: ≤15 min/week to maintain.
10
10
 
11
11
  | Item | Doc | Notes |
12
12
  |------|-----|-------|
13
- | **business-extensions** | [doc](ideas/business-extensions.md) | Dedicated business extensions wrapping raw capabilities with deterministic logic |
13
+ | | | |
14
14
 
15
15
  ## Now
16
16
 
@@ -25,9 +25,6 @@ Single source of truth for priority. Rule: ≤15 min/week to maintain.
25
25
  | # | Item | Doc | Depends on | Why now |
26
26
  |---|------|-----|-----------|---------|
27
27
  | 1 | **autonomous-planning-tier** | [doc](backlog/autonomous-planning-tier.md) | — | Let agents decompose ideas, keep human gate |
28
- | 2 | **dm-auto-space** | [doc](backlog/dm-auto-space.md) | — | Auto-create space per customer DM |
29
- | 3 | **applicative-profiles** | [doc](backlog/applicative-profiles.md) | caller-bound-capability-token | Generic profile layer; host-side broker keeps secrets off customer containers |
30
- | 4 | **caller-bound-capability-token** | [doc](backlog/caller-bound-capability-token.md) | — | Per-turn token so brokers can trust callerId; also hardens TradeStation |
31
28
 
32
29
  ## Later
33
30
 
@@ -41,4 +38,6 @@ Single source of truth for priority. Rule: ≤15 min/week to maintain.
41
38
 
42
39
  | Item | Shipped | What |
43
40
  |------|---------|------|
44
- | | | |
41
+ | **applicative-profiles** | 2026-07-01 | Generic profile layer: manifest schema, exhaustive member scoping, host-side capability broker |
42
+ | **caller-bound-capability-token** | 2026-07-01 | Per-turn unspoofable caller token for host API auth; also hardens TradeStation |
43
+ | **dm-auto-space** | 2026-07-01 | Auto-create space per customer DM with rate limits and dashboard controls |
@@ -1,6 +1,6 @@
1
1
  # Applicative Profiles
2
2
 
3
- **Status**: Backlog
3
+ **Status**: Done
4
4
  **Slug**: applicative-profiles
5
5
  **Created**: 2026-07-01
6
6
  **Last updated**: 2026-07-01
@@ -70,7 +70,7 @@ Therefore **any credential injected into a customer's container is readable by t
70
70
  | 1. Profile schema + permission scoping | `mercury-public` | New `mercury-profile.yaml` fields; profile-aware member permissions |
71
71
  | 2. Host-side capability broker | `mercury-public` | Profile capabilities run host-side via `mrctl`→API, à la TradeStation; secrets stay on host |
72
72
  | 3. Per-turn caller token | `mercury-public` | [[caller-bound-capability-token]] — trustworthy `callerId` on the host (dependency) |
73
- | 4. Barber business logic | **private profiles repo** | Availability, 2 barbers, per-number limits, ownership rules — the moat |
73
+ | 4. Barber business logic | **private repo** `tagula-agents/tagula-agent-profiles` | Availability, 2 barbers, per-number limits, ownership rules — the moat |
74
74
 
75
75
  ### Part 1 — Schema + permission scoping
76
76
 
@@ -115,7 +115,7 @@ The broker's ownership checks require a `callerId` the customer cannot forge. To
115
115
 
116
116
  ### Part 4 — Barber business logic (private repo)
117
117
 
118
- Lives in `tagula-agent-profiles` (or similar). Contains the `mercury-profile.yaml`, the agent persona, and the host-side route implementations' business rules (availability windows, 2-barber scheduling, per-number booking limits, double-booking prevention). Consumes Parts 1-3 as the public contract; never touches `mercury-public`.
118
+ Lives in the private monorepo `Avishai-tsabari/tagula-agents` (`c:/code/agentic/tagula-agents`), under `tagula-agent-profiles/` (the repo also holds the cloud console). Checked out as a sibling of `mercury-public`. Contains the `mercury-profile.yaml`, the agent persona, and the host-side route implementations' business rules (availability windows, 2-barber scheduling, per-number booking limits, double-booking prevention). Consumes Parts 1-3 as the public contract; never touches `mercury-public`.
119
119
 
120
120
  ### Example manifest (in the private repo)
121
121
 
@@ -217,14 +217,15 @@ defaults:
217
217
  - [x] Context for Claude pointers filled in
218
218
 
219
219
  ### Implementation
220
- - [ ] Step 1 — Extend profile schema
221
- - [ ] Step 2 — Capability validation
222
- - [ ] Step 3 — Activation + config
223
- - [ ] Step 4 — Profile-aware permissions
224
- - [ ] Step 5 — Caller token dependency ([[caller-bound-capability-token]])
225
- - [ ] Step 6Broker route pattern (mirror `tradestation.ts`)
226
- - [ ] Step 7Tests
227
- - [ ] Step 8Example config
220
+ - [x] Step 1 — Extend profile schema (`capabilities`, `member_permissions`, `system_prompt`); swapped hand-rolled `parseSimpleYaml` for the `yaml` lib (multiline `system_prompt` support, less fragility)
221
+ - [x] Step 2 — Capability validation (`validateProfileCapabilities`, called in `applyProfile`); tests in `tests/applicative-profiles.test.ts`
222
+ - [x] Step 3 — Activation + config: `profile` config key; `applyProfile` persists `.mercury/active-profile.json`; `main.ts` reads it at startup (after extensions load) and calls `setActiveProfileMemberPermissions`
223
+ - [x] Step 4 — Profile-aware permissions: `getRolePermissions` uses the active profile's member set as exhaustive; precedence = explicit per-space override > profile > defaults; admin/system unaffected
224
+ - [x] Step 5 — Caller token dependency ([[caller-bound-capability-token]]) — implemented and committed
225
+ - [x] Step 5bInject `system_prompt` at container spawn (appended to `MERCURY_EXT_SYSTEM_PROMPT` in `runtime.ts`, ahead of per-space prompt)
226
+ - [x] Step 6Host-side capability broker: `mercury.capability(name, handler)` extension API; `POST /api/capability/:name/:action` route (auth = token-derived caller, authz = permission named after the capability); `registry.getCapability`; `mrctl capability <name> <action> [json]`
227
+ - [x] Step 7Tests: `tests/api.test.ts` capability broker (dispatch with token-derived caller; permission-denied → 403) + schema/scoping tests in `tests/applicative-profiles.test.ts`
228
+ - [x] Step 8 — Example config: profile + `mrctl capability` + `MERCURY_CALLER_TOKEN_KEY` notes in `mercury.example.yaml`
228
229
  - [ ] `bun run check` passes
229
230
  - [ ] No secrets or `.env` files committed
230
231
  - [ ] No unrelated files modified
@@ -243,17 +244,27 @@ defaults:
243
244
  ## Retrospective
244
245
 
245
246
  **Residual risks / follow-ups:**
246
- - Per-turn caller token is a hard dependency for safe ownership backlog/caller-bound-capability-token.md
247
- - Barber business logic tracked in the private profiles repo (out of this repo's scope) — accepted
247
+ - Barber business logic (availability, 2-barber, per-number limits) lives in the private `tagula-agents` repo out of this repo's scope — accepted
248
+ - Ephemeral caller-token key breaks `mrctl` if the API and container-runner ever run in separate processes without `MERCURY_CALLER_TOKEN_KEY` set; documented in config + `docs/authoring-profiles.md` — accepted
249
+ - `mrctl capability <name>/<action>` are not URL-encoded (path traversal only reaches other RBAC-guarded routes, no escalation) — accepted
250
+ - Duplicate capability-name registration across two extensions silently first-wins in `registry.getCapability` — accepted
251
+ - Empty `member_permissions: []` in a manifest gives members zero permissions (author's explicit exhaustive choice) — accepted
248
252
 
249
253
  **What changed from the plan:**
250
- - ...
254
+ - Signing key is NOT derived from `API_SECRET` (the plan's fallback) — `API_SECRET` is readable in the container, so an ephemeral host-only random key is used instead (config `callerTokenKey` for multi-process).
255
+ - Caller token split into its own delivered dependency (`caller-bound-capability-token`) — also hardened TradeStation, which reads identity from the same auth context (no code change needed there).
256
+ - Replaced the hand-rolled `parseSimpleYaml` in `profiles.ts` with the `yaml` lib (multiline `system_prompt` support + less fragility); built-in profiles still parse.
257
+ - Profile activation is project-wide via a module-level registry + persisted `.mercury/active-profile.json`, not per-space `space_config` writes (simpler, works for later-created dm-auto-space spaces).
258
+ - Broker registration model chosen with the user: generic `mercury.capability(name, handler)` + `POST /api/capability/:name/:action` + `mrctl capability`, authorized by the permission named after the capability.
259
+ - Added `docs/authoring-profiles.md` as the contract for the external profiles repo.
251
260
 
252
261
  **Key decisions made during implementation:**
253
- - ...
262
+ - Token = HMAC-SHA256 over `base64url(payload).base64url(sig)`, constant-time compare with length guard; TTL = container timeout + 60s.
263
+ - `getRolePermissions` uses `Array.isArray` (not `!== null`) on the active-profile member set so a malformed `active-profile.json` falls back to defaults instead of throwing in the hot path (found in session review).
264
+ - Profile schema uses plain `z.object` (no `.strict()`), so unknown manifest keys are stripped, honoring the `2026-07-01-strict-yaml-schema-crash` post-mortem (warn/ignore unknown keys, fail only on invalid values); startup `loadActiveProfile` is try/catch → null.
254
265
 
255
266
  **Architecture impact** (update `docs/ARCHITECTURE.md` if any of these apply):
256
- - [ ] New package/module added
257
- - [ ] New data model or schema change
258
- - [ ] New inter-package interaction
267
+ - [x] New package/module added — `src/core/caller-token.ts`, `src/core/routes/capability.ts`
268
+ - [ ] New data model or schema change (uses existing `space_config`; adds a local `.mercury/active-profile.json` file, no DB schema change)
269
+ - [x] New inter-package interaction — extension host-side capability handlers invoked via the control-plane API
259
270
  - [ ] Deployment topology changed
@@ -1,6 +1,6 @@
1
1
  # Caller-Bound Capability Token
2
2
 
3
- **Status**: Backlog
3
+ **Status**: Done
4
4
  **Slug**: caller-bound-capability-token
5
5
  **Created**: 2026-07-01
6
6
  **Last updated**: 2026-07-01
@@ -144,14 +144,14 @@ The signing key never enters the container. The container holds only its **own**
144
144
  - [x] Context for Claude pointers filled in
145
145
 
146
146
  ### Implementation
147
- - [ ] Step 1 — Token helpers
148
- - [ ] Step 2 — Mint at spawn
149
- - [ ] Step 3 — Forward in mrctl
150
- - [ ] Step 4 — Verify host-side
151
- - [ ] Step 5 — Migrate TradeStation guard
152
- - [ ] Step 6 — Tests
153
- - [ ] `bun run check` passes
154
- - [ ] No secrets committed
147
+ - [x] Step 1 — Token helpers (`src/core/caller-token.ts`)
148
+ - [x] Step 2 — Mint at spawn (`container-runner.ts`; key added to `BLOCKED_ENV_VARS`)
149
+ - [x] Step 3 — Forward in mrctl (`x-mercury-token` header)
150
+ - [x] Step 4 — Verify host-side (`api.ts` middleware; token identity wins)
151
+ - [x] Step 5 — TradeStation guard: **no change needed** — it reads `c.get("auth")`, which now derives from the token
152
+ - [x] Step 6 — Tests (`tests/caller-token.test.ts` unit; `tests/api.test.ts` integration: token overrides spoofed header, invalid token → 401)
153
+ - [x] `bun run check` passes (one unrelated discord-adapter timeout flake under full parallel run; passes in isolation)
154
+ - [x] No secrets committed
155
155
 
156
156
  ---
157
157
 
@@ -168,10 +168,14 @@ The signing key never enters the container. The container holds only its **own**
168
168
  - none
169
169
 
170
170
  **What changed from the plan:**
171
- - ...
171
+ - **Signing key is NOT derived from `API_SECRET`.** The plan suggested deriving it when unset, but `API_SECRET` is injected into containers — the agent could recompute the key and forge tokens. Instead: an optional host-only `callerTokenKey` (env `MERCURY_CALLER_TOKEN_KEY`, in `BLOCKED_ENV_VARS`), falling back to an **ephemeral random key** generated once per host process. Zero required config; nothing forgeable from inside the container.
172
+ - **TradeStation guard needed no code change** — it already reads identity from `c.get("auth")`, which the middleware now sets from the token.
173
+ - Token injected as container env `CALLER_TOKEN` (unprefixed, like `CALLER_ID`/`API_SECRET`), forwarded as `x-mercury-token`.
172
174
 
173
175
  **Key decisions made during implementation:**
174
- - ...
176
+ - HMAC-SHA256 over `base64url(payload).base64url(sig)`, payload `{c,s,exp}`; constant-time compare; no new dependency (`node:crypto`).
177
+ - TTL = container timeout + 60s buffer, so a token never expires mid-turn but stays short-lived vs the permanent shared secret.
178
+ - Header path retained as fallback → fully backward compatible; existing header-based tests and old container images keep working.
175
179
 
176
180
  **Architecture impact:**
177
181
  - [ ] New package/module added
@@ -0,0 +1,71 @@
1
+ # Recent Dev Summary
2
+
3
+ **Date:** 2026-07-02
4
+ **Covers:** 3 shipped features · 2026-06-30 → 2026-07-01
5
+
6
+ Three tightly-related features shipped together to lay the groundwork for customer-facing (member) profiles: a per-DM auto-space so each customer gets isolated persistent context, a caller-bound capability token so the host can trust *who* is calling, and applicative profiles that scope member capabilities and route privileged work through a host-side broker. The unifying thread is a **security pivot**: moving from "trust whoever holds a tool" to "keep secrets on the host and bind identity cryptographically" — a prerequisite for exposing capabilities to untrusted members.
7
+
8
+ ## Shipped
9
+
10
+ | Feature | Slug | Date shipped | Key takeaway |
11
+ |---------|------|-------------|--------------|
12
+ | Auto-Create Space per DM | dm-auto-space | 2026-07-01 | Config-driven per-customer spaces; default-off, idempotent seeding via read-before-write |
13
+ | Caller-Bound Capability Token | caller-bound-capability-token | 2026-07-01 | Per-turn HMAC token binds `{callerId, spaceId}`; host trusts the token, not spoofable headers |
14
+ | Applicative Profiles | applicative-profiles | 2026-07-01 | Profiles scope member permissions + run privileged work host-side; secrets never enter member containers |
15
+
16
+ ## Build patterns
17
+
18
+ ### 1. Security by architecture, not by guardrail
19
+
20
+ Rather than hardening a bypassable boundary, these features removed the sensitive material from the untrusted zone entirely.
21
+
22
+ | Feature | Observation |
23
+ |---------|-------------|
24
+ | applicative-profiles | Credentials stay in host storage; member containers get only a self-scoped per-turn token — so `printenv` leaks nothing. Explicitly does *not* rely on the bypassable permission guard (TODO-2). |
25
+ | caller-bound-capability-token | Signing key never enters the container; the container holds only its own turn's token and cannot forge another caller. |
26
+
27
+ **Rule:** When an untrusted actor shares an execution environment, don't guard the secret — remove it from that environment. Security should come from where the secret lives, not from blocking the paths to it.
28
+
29
+ ### 2. Reuse the proven pattern over inventing one
30
+
31
+ New capabilities deliberately mirrored an existing production mechanism instead of designing fresh machinery.
32
+
33
+ | Feature | Observation |
34
+ |---------|-------------|
35
+ | applicative-profiles | The host-side capability broker is the TradeStation pattern generalized (`mrctl`→control-plane API, creds host-side), not a new transport. |
36
+ | caller-bound-capability-token | TradeStation's confirm guard needed *no code change* — it already read identity from `c.get("auth")`, which the new middleware now populates from the token. |
37
+
38
+ **Rule:** Before building new infrastructure, find the closest production-proven pattern and generalize it. Consumers that read from the right abstraction inherit the upgrade for free.
39
+
40
+ ### 3. Backward-compatible, additive rollout
41
+
42
+ Every feature shipped default-off or fallback-preserving so existing deployments were unaffected.
43
+
44
+ | Feature | Observation |
45
+ |---------|-------------|
46
+ | dm-auto-space | `enabled` defaults to `false`; global daily rate limits default to `0` (unlimited). |
47
+ | caller-bound-capability-token | Absent a token, the old shared-secret + header path is unchanged, so old container images keep working during rollout. |
48
+ | applicative-profiles | All new profile schema fields optional; brokers additive; existing profiles still parse. |
49
+
50
+ **Rule:** Ship member-facing and security-sensitive changes as additive with a safe default; make the new path authoritative only when explicitly present, so rollout never breaks in-flight deployments.
51
+
52
+ ### 4. Plan-vs-reality divergence on the crypto detail
53
+
54
+ Each retrospective flagged where the plan's shortcut was unsafe and was corrected during implementation.
55
+
56
+ | Feature | Observation |
57
+ |---------|-------------|
58
+ | caller-bound-capability-token | Plan proposed deriving the signing key from `API_SECRET`; rejected because `API_SECRET` is readable in the container — switched to an ephemeral host-only key. |
59
+ | applicative-profiles | Profile activation moved from per-space `space_config` writes to a module-level registry + persisted `.mercury/active-profile.json`; schema dropped `.strict()` to honor the strict-yaml crash post-mortem. |
60
+
61
+ **Rule:** Re-examine any "derive the key from the existing secret" shortcut against the threat model — if the base secret is reachable by the attacker, the derived key is too.
62
+
63
+ ## Comparison with previous dev summary
64
+
65
+ None — this is the first dev summary. Future summaries should watch whether the "security by architecture" and "reuse the proven pattern" themes persist as more member-facing profiles land in the private repo.
66
+
67
+ ## Action items
68
+
69
+ 1. **Land the private barber-appointments profile** against the now-public contract (schema + broker + caller token) to validate the design end-to-end.
70
+ 2. **Audit remaining header-trusting routes** to migrate onto token-derived identity now that the mechanism exists.
71
+ 3. **Document the host-side broker contract** (`docs/authoring-profiles.md`) as the canonical reference for external profile authors, and keep it in sync as capabilities grow.
@@ -1,28 +1,28 @@
1
- # Dashboard Authentication
2
-
3
- The Mercury dashboard (`/dashboard/*`) is protected by `MERCURY_API_SECRET`. All htmx partial requests and the SSE events stream require a valid `mercury_token` session cookie.
4
-
5
- ## How it works
6
-
7
- ### Login route
8
-
9
- `GET /dashboard/login?token=<secret>` — the entry point for browser sessions.
10
-
11
- - Validates `token` against `MERCURY_API_SECRET` using `timingSafeEqual`
12
- - On success: sets `mercury_token` HttpOnly cookie and redirects to `/dashboard`
13
- - On failure: returns `401 Invalid or missing token`
14
- - Not protected by the dashboard auth middleware (it's the login endpoint itself)
15
-
16
- ## Auth flow
17
-
18
- ```
19
- User navigates to https://<host>:<port>/dashboard/login?token=<MERCURY_API_SECRET>
20
- → agent sets mercury_token HttpOnly cookie
21
- → 302 /dashboard
22
- → dashboard loads, htmx requests include cookie ✅
23
- ```
24
-
25
- ## Key constraints
26
-
27
- - `MERCURY_API_SECRET` should be set to a long random string. `mercury setup` auto-generates one.
28
- - The cookie is HttpOnly — not accessible from JavaScript.
1
+ # Dashboard Authentication
2
+
3
+ The Mercury dashboard (`/dashboard/*`) is protected by `MERCURY_API_SECRET`. All htmx partial requests and the SSE events stream require a valid `mercury_token` session cookie.
4
+
5
+ ## How it works
6
+
7
+ ### Login route
8
+
9
+ `GET /dashboard/login?token=<secret>` — the entry point for browser sessions.
10
+
11
+ - Validates `token` against `MERCURY_API_SECRET` using `timingSafeEqual`
12
+ - On success: sets `mercury_token` HttpOnly cookie and redirects to `/dashboard`
13
+ - On failure: returns `401 Invalid or missing token`
14
+ - Not protected by the dashboard auth middleware (it's the login endpoint itself)
15
+
16
+ ## Auth flow
17
+
18
+ ```
19
+ User navigates to https://<host>:<port>/dashboard/login?token=<MERCURY_API_SECRET>
20
+ → agent sets mercury_token HttpOnly cookie
21
+ → 302 /dashboard
22
+ → dashboard loads, htmx requests include cookie ✅
23
+ ```
24
+
25
+ ## Key constraints
26
+
27
+ - `MERCURY_API_SECRET` should be set to a long random string. `mercury setup` auto-generates one.
28
+ - The cookie is HttpOnly — not accessible from JavaScript.
@@ -1,109 +1,109 @@
1
- # Authentication
2
-
3
- Mercury needs credentials for two things:
4
-
5
- 1. **AI model provider** — to call the LLM (Anthropic, OpenAI, etc.)
6
- 2. **Chat platforms** — to connect to WhatsApp, Discord, Slack, Teams
7
-
8
- ## Model Provider Auth
9
-
10
- ### OAuth Login (Recommended)
11
-
12
- Mercury reuses [pi](https://github.com/badlogic/pi)'s OAuth providers. No API key needed — just login:
13
-
14
- ```bash
15
- mercury auth login # Interactive provider picker
16
- mercury auth login anthropic # Or specify directly
17
- ```
18
-
19
- Supported providers:
20
-
21
- | Provider | ID | What it gives you |
22
- |----------|----|-------------------|
23
- | Anthropic (Claude Pro/Max) | `anthropic` | Claude access via your subscription |
24
- | GitHub Copilot | `github-copilot` | Models via Copilot subscription |
25
- | Google Gemini CLI | `google-gemini-cli` | Gemini via Google Cloud |
26
- | Antigravity | `antigravity` | Gemini 3, Claude, GPT-OSS via Google Cloud |
27
- | ChatGPT Plus/Pro | `openai-codex` | OpenAI models via ChatGPT subscription |
28
-
29
- Credentials are saved to `.mercury/global/auth.json` and auto-refreshed when expired.
30
-
31
- ### API Key
32
-
33
- Alternatively, set an API key in `.env`:
34
-
35
- ```bash
36
- MERCURY_ANTHROPIC_API_KEY=sk-ant-...
37
- # or
38
- MERCURY_ANTHROPIC_OAUTH_TOKEN=sk-ant-oat01-...
39
- ```
40
-
41
- ### Resolution Order
42
-
43
- Mercury resolves credentials in this order:
44
-
45
- 1. OAuth credentials from `auth.json` (via `mercury auth login`)
46
- 2. `MERCURY_ANTHROPIC_OAUTH_TOKEN` from `.env`
47
- 3. `MERCURY_ANTHROPIC_API_KEY` from `.env`
48
-
49
- > **OAuth always takes precedence over API keys.** If a provider has both an OAuth connection (in `auth.json` or `MERCURY_*_OAUTH_TOKEN`) and an API key (`MERCURY_*_API_KEY`), the OAuth token is used and the API key is ignored. This applies both to the CLI and to agents provisioned via the cloud console.
50
-
51
- ### Managing Credentials
52
-
53
- ```bash
54
- mercury auth status # Show what's configured
55
- mercury auth logout anthropic # Remove saved credentials
56
- mercury auth logout # List what's logged in
57
- ```
58
-
59
- ## Chat Platform Auth
60
-
61
- ### WhatsApp
62
-
63
- ```bash
64
- mercury auth whatsapp # QR code (recommended)
65
- mercury auth whatsapp --pairing-code --phone 1234 # Pairing code (headless)
66
- ```
67
-
68
- See [whatsapp.md](whatsapp.md) for details.
69
-
70
- ### Discord
71
-
72
- Set in `.env`:
73
-
74
- ```bash
75
- MERCURY_ENABLE_DISCORD=true
76
- MERCURY_DISCORD_BOT_TOKEN=your-bot-token
77
- ```
78
-
79
- ### Slack
80
-
81
- Set in `.env`:
82
-
83
- ```bash
84
- MERCURY_ENABLE_SLACK=true
85
- MERCURY_SLACK_BOT_TOKEN=xoxb-...
86
- MERCURY_SLACK_SIGNING_SECRET=...
87
- ```
88
-
89
- ### Teams
90
-
91
- Set in `.env`:
92
-
93
- ```bash
94
- MERCURY_ENABLE_TEAMS=true
95
- MERCURY_TEAMS_APP_ID=...
96
- MERCURY_TEAMS_APP_PASSWORD=...
97
- MERCURY_TEAMS_APP_TENANT_ID=...
98
- ```
99
-
100
- ## Dashboard Auth
101
-
102
- The web dashboard is protected by `MERCURY_API_SECRET`. Browser sessions are established via a login handshake managed by the cloud console — see [dashboard.md](dashboard.md).
103
-
104
- ## Security
105
-
106
- - `auth.json` has `0600` permissions (owner read/write only)
107
- - WhatsApp credentials in `.mercury/whatsapp-auth/` are sensitive — treat like passwords
108
- - All `MERCURY_*` env vars are passed into containers with the prefix stripped
109
- - Never commit `.env` or `auth.json` to version control
1
+ # Authentication
2
+
3
+ Mercury needs credentials for two things:
4
+
5
+ 1. **AI model provider** — to call the LLM (Anthropic, OpenAI, etc.)
6
+ 2. **Chat platforms** — to connect to WhatsApp, Discord, Slack, Teams
7
+
8
+ ## Model Provider Auth
9
+
10
+ ### OAuth Login (Recommended)
11
+
12
+ Mercury reuses [pi](https://github.com/badlogic/pi)'s OAuth providers. No API key needed — just login:
13
+
14
+ ```bash
15
+ mercury auth login # Interactive provider picker
16
+ mercury auth login anthropic # Or specify directly
17
+ ```
18
+
19
+ Supported providers:
20
+
21
+ | Provider | ID | What it gives you |
22
+ |----------|----|-------------------|
23
+ | Anthropic (Claude Pro/Max) | `anthropic` | Claude access via your subscription |
24
+ | GitHub Copilot | `github-copilot` | Models via Copilot subscription |
25
+ | Google Gemini CLI | `google-gemini-cli` | Gemini via Google Cloud |
26
+ | Antigravity | `antigravity` | Gemini 3, Claude, GPT-OSS via Google Cloud |
27
+ | ChatGPT Plus/Pro | `openai-codex` | OpenAI models via ChatGPT subscription |
28
+
29
+ Credentials are saved to `.mercury/global/auth.json` and auto-refreshed when expired.
30
+
31
+ ### API Key
32
+
33
+ Alternatively, set an API key in `.env`:
34
+
35
+ ```bash
36
+ MERCURY_ANTHROPIC_API_KEY=sk-ant-...
37
+ # or
38
+ MERCURY_ANTHROPIC_OAUTH_TOKEN=sk-ant-oat01-...
39
+ ```
40
+
41
+ ### Resolution Order
42
+
43
+ Mercury resolves credentials in this order:
44
+
45
+ 1. OAuth credentials from `auth.json` (via `mercury auth login`)
46
+ 2. `MERCURY_ANTHROPIC_OAUTH_TOKEN` from `.env`
47
+ 3. `MERCURY_ANTHROPIC_API_KEY` from `.env`
48
+
49
+ > **OAuth always takes precedence over API keys.** If a provider has both an OAuth connection (in `auth.json` or `MERCURY_*_OAUTH_TOKEN`) and an API key (`MERCURY_*_API_KEY`), the OAuth token is used and the API key is ignored. This applies both to the CLI and to agents provisioned via the cloud console.
50
+
51
+ ### Managing Credentials
52
+
53
+ ```bash
54
+ mercury auth status # Show what's configured
55
+ mercury auth logout anthropic # Remove saved credentials
56
+ mercury auth logout # List what's logged in
57
+ ```
58
+
59
+ ## Chat Platform Auth
60
+
61
+ ### WhatsApp
62
+
63
+ ```bash
64
+ mercury auth whatsapp # QR code (recommended)
65
+ mercury auth whatsapp --pairing-code --phone 1234 # Pairing code (headless)
66
+ ```
67
+
68
+ See [whatsapp.md](whatsapp.md) for details.
69
+
70
+ ### Discord
71
+
72
+ Set in `.env`:
73
+
74
+ ```bash
75
+ MERCURY_ENABLE_DISCORD=true
76
+ MERCURY_DISCORD_BOT_TOKEN=your-bot-token
77
+ ```
78
+
79
+ ### Slack
80
+
81
+ Set in `.env`:
82
+
83
+ ```bash
84
+ MERCURY_ENABLE_SLACK=true
85
+ MERCURY_SLACK_BOT_TOKEN=xoxb-...
86
+ MERCURY_SLACK_SIGNING_SECRET=...
87
+ ```
88
+
89
+ ### Teams
90
+
91
+ Set in `.env`:
92
+
93
+ ```bash
94
+ MERCURY_ENABLE_TEAMS=true
95
+ MERCURY_TEAMS_APP_ID=...
96
+ MERCURY_TEAMS_APP_PASSWORD=...
97
+ MERCURY_TEAMS_APP_TENANT_ID=...
98
+ ```
99
+
100
+ ## Dashboard Auth
101
+
102
+ The web dashboard is protected by `MERCURY_API_SECRET`. Browser sessions are established via a login handshake managed by the cloud console — see [dashboard.md](dashboard.md).
103
+
104
+ ## Security
105
+
106
+ - `auth.json` has `0600` permissions (owner read/write only)
107
+ - WhatsApp credentials in `.mercury/whatsapp-auth/` are sensitive — treat like passwords
108
+ - All `MERCURY_*` env vars are passed into containers with the prefix stripped
109
+ - Never commit `.env` or `auth.json` to version control