mercury-agent 0.4.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (218) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +438 -0
  3. package/container/Dockerfile +127 -0
  4. package/container/Dockerfile.base +109 -0
  5. package/container/Dockerfile.power +17 -0
  6. package/container/agent-package.json +8 -0
  7. package/container/build.sh +54 -0
  8. package/docs/TODOS.md +147 -0
  9. package/docs/auth/dashboard.md +28 -0
  10. package/docs/auth/overview.md +109 -0
  11. package/docs/auth/whatsapp.md +173 -0
  12. package/docs/configuration.md +54 -0
  13. package/docs/container-lifecycle.md +349 -0
  14. package/docs/context-architecture.md +87 -0
  15. package/docs/deployment.md +199 -0
  16. package/docs/extensions.md +375 -0
  17. package/docs/graceful-shutdown.md +62 -0
  18. package/docs/kb-distillation.md +77 -0
  19. package/docs/media/overview.md +140 -0
  20. package/docs/media/whatsapp.md +171 -0
  21. package/docs/memory.md +137 -0
  22. package/docs/permissions.md +217 -0
  23. package/docs/pipeline.md +228 -0
  24. package/docs/prd-chat-memory.md +76 -0
  25. package/docs/prd-config-load.md +82 -0
  26. package/docs/rate-limiting.md +166 -0
  27. package/docs/scheduler.md +288 -0
  28. package/docs/setup-discord.md +100 -0
  29. package/docs/setup-slack.md +119 -0
  30. package/docs/setup-whatsapp.md +94 -0
  31. package/docs/subagents.md +166 -0
  32. package/docs/web-search.md +62 -0
  33. package/examples/extensions/README.md +12 -0
  34. package/examples/extensions/charts/index.ts +13 -0
  35. package/examples/extensions/charts/skill/SKILL.md +98 -0
  36. package/examples/extensions/gws/README.md +52 -0
  37. package/examples/extensions/gws/index.ts +106 -0
  38. package/examples/extensions/gws/skill/SKILL.md +57 -0
  39. package/examples/extensions/gws/skill/references/calendar.md +101 -0
  40. package/examples/extensions/gws/skill/references/docs.md +65 -0
  41. package/examples/extensions/gws/skill/references/drive.md +79 -0
  42. package/examples/extensions/gws/skill/references/gmail.md +85 -0
  43. package/examples/extensions/gws/skill/references/sheets.md +60 -0
  44. package/examples/extensions/napkin/index.ts +821 -0
  45. package/examples/extensions/napkin/prompts/consolidation-monthly.md +73 -0
  46. package/examples/extensions/napkin/prompts/consolidation-weekly.md +67 -0
  47. package/examples/extensions/napkin/prompts/kb-distillation.md +176 -0
  48. package/examples/extensions/napkin/skill/SKILL.md +728 -0
  49. package/examples/extensions/pdf/index.ts +23 -0
  50. package/examples/extensions/pdf/skill/LICENSE.txt +30 -0
  51. package/examples/extensions/pdf/skill/SKILL.md +314 -0
  52. package/examples/extensions/pdf/skill/forms.md +294 -0
  53. package/examples/extensions/pdf/skill/reference.md +612 -0
  54. package/examples/extensions/pdf/skill/scripts/check_bounding_boxes.py +65 -0
  55. package/examples/extensions/pdf/skill/scripts/check_fillable_fields.py +11 -0
  56. package/examples/extensions/pdf/skill/scripts/convert_pdf_to_images.py +33 -0
  57. package/examples/extensions/pdf/skill/scripts/create_validation_image.py +37 -0
  58. package/examples/extensions/pdf/skill/scripts/extract_form_field_info.py +122 -0
  59. package/examples/extensions/pdf/skill/scripts/extract_form_structure.py +115 -0
  60. package/examples/extensions/pdf/skill/scripts/fill_fillable_fields.py +98 -0
  61. package/examples/extensions/pdf/skill/scripts/fill_pdf_form_with_annotations.py +107 -0
  62. package/examples/extensions/permission-guard/index.ts +65 -0
  63. package/examples/extensions/pinchtab/index.ts +199 -0
  64. package/examples/extensions/pinchtab/lib/session-injector.ts +144 -0
  65. package/examples/extensions/pinchtab/skill/SKILL.md +224 -0
  66. package/examples/extensions/pinchtab/skill/TRUST.md +69 -0
  67. package/examples/extensions/pinchtab/skill/references/api.md +297 -0
  68. package/examples/extensions/pinchtab/skill/references/env.md +45 -0
  69. package/examples/extensions/pinchtab/skill/references/profiles.md +107 -0
  70. package/examples/extensions/tradestation/host/refresh.ts +102 -0
  71. package/examples/extensions/tradestation/index.ts +153 -0
  72. package/examples/extensions/tradestation/skill/SKILL.md +67 -0
  73. package/examples/extensions/tradestation/skill/scripts/ts-cli.ts +111 -0
  74. package/examples/extensions/voice-synth/index.ts +94 -0
  75. package/examples/extensions/voice-synth/skill/SKILL.md +38 -0
  76. package/examples/extensions/voice-transcribe/index.ts +381 -0
  77. package/examples/extensions/voice-transcribe/requirements.txt +8 -0
  78. package/examples/extensions/voice-transcribe/scripts/transcribe.py +179 -0
  79. package/examples/extensions/voice-transcribe/skill/SKILL.md +53 -0
  80. package/examples/extensions/web-search/index.ts +22 -0
  81. package/examples/extensions/web-search/skill/SKILL.md +114 -0
  82. package/examples/extensions/web-search/skill/references/apartments.md +178 -0
  83. package/examples/extensions/web-search/skill/references/car-purchase.md +132 -0
  84. package/examples/extensions/web-search/skill/references/car-rental.md +113 -0
  85. package/examples/extensions/web-search/skill/references/flights.md +133 -0
  86. package/examples/extensions/web-search/skill/references/hotels.md +148 -0
  87. package/examples/extensions/yahoo-mail/cli/bun.lock +66 -0
  88. package/examples/extensions/yahoo-mail/cli/package.json +13 -0
  89. package/examples/extensions/yahoo-mail/cli/ymail.mjs +353 -0
  90. package/examples/extensions/yahoo-mail/index.ts +57 -0
  91. package/examples/extensions/yahoo-mail/skill/SKILL.md +78 -0
  92. package/package.json +106 -0
  93. package/resources/agents/explore.md +50 -0
  94. package/resources/agents/worker.md +24 -0
  95. package/resources/builtin-extensions.txt +3 -0
  96. package/resources/connection-env-vars.json +25 -0
  97. package/resources/extensions/.gitkeep +0 -0
  98. package/resources/pi-extensions/subagent/agents.ts +126 -0
  99. package/resources/pi-extensions/subagent/index.ts +964 -0
  100. package/resources/profiles/coding/AGENTS.md +43 -0
  101. package/resources/profiles/coding/mercury-profile.yaml +15 -0
  102. package/resources/profiles/general/AGENTS.md +31 -0
  103. package/resources/profiles/general/mercury-profile.yaml +15 -0
  104. package/resources/profiles/research/AGENTS.md +40 -0
  105. package/resources/profiles/research/mercury-profile.yaml +15 -0
  106. package/resources/skills/config/SKILL.md +25 -0
  107. package/resources/skills/context/SKILL.md +33 -0
  108. package/resources/skills/conversation-recap/SKILL.md +19 -0
  109. package/resources/skills/media/SKILL.md +27 -0
  110. package/resources/skills/mutes/SKILL.md +31 -0
  111. package/resources/skills/permissions/SKILL.md +19 -0
  112. package/resources/skills/preferences/SKILL.md +31 -0
  113. package/resources/skills/recall/SKILL.md +24 -0
  114. package/resources/skills/roles/SKILL.md +18 -0
  115. package/resources/skills/spaces/SKILL.md +18 -0
  116. package/resources/skills/tasks/SKILL.md +45 -0
  117. package/resources/templates/AGENTS.md +157 -0
  118. package/resources/templates/env.template +34 -0
  119. package/resources/templates/mercury.example.yaml +75 -0
  120. package/src/adapters/discord-native.ts +534 -0
  121. package/src/adapters/discord.ts +38 -0
  122. package/src/adapters/setup.ts +89 -0
  123. package/src/adapters/slack.ts +9 -0
  124. package/src/adapters/whatsapp-media.ts +337 -0
  125. package/src/adapters/whatsapp.ts +629 -0
  126. package/src/agent/api-socket.ts +127 -0
  127. package/src/agent/container-entry.ts +967 -0
  128. package/src/agent/container-error.ts +49 -0
  129. package/src/agent/container-runner.ts +1272 -0
  130. package/src/agent/model-capabilities-core.ts +23 -0
  131. package/src/agent/model-capabilities.ts +231 -0
  132. package/src/agent/pi-failure-class.ts +83 -0
  133. package/src/agent/pi-jsonl-parser.ts +306 -0
  134. package/src/agent/preferences-prompt.ts +20 -0
  135. package/src/agent/user-error-messages.ts +78 -0
  136. package/src/bridges/discord.ts +171 -0
  137. package/src/bridges/slack.ts +177 -0
  138. package/src/bridges/teams.ts +160 -0
  139. package/src/bridges/telegram.ts +571 -0
  140. package/src/bridges/whatsapp.ts +290 -0
  141. package/src/chat-shim.ts +259 -0
  142. package/src/cli/mercury.ts +2508 -0
  143. package/src/cli/mrctl-http.ts +27 -0
  144. package/src/cli/mrctl.ts +611 -0
  145. package/src/cli/whatsapp-auth.ts +260 -0
  146. package/src/config-file.ts +397 -0
  147. package/src/config-model-chain.ts +30 -0
  148. package/src/config.ts +316 -0
  149. package/src/core/api-types.ts +58 -0
  150. package/src/core/api.ts +105 -0
  151. package/src/core/commands.ts +76 -0
  152. package/src/core/conversation.ts +47 -0
  153. package/src/core/handler.ts +206 -0
  154. package/src/core/media.ts +200 -0
  155. package/src/core/mute-duration.ts +22 -0
  156. package/src/core/outbox.ts +76 -0
  157. package/src/core/permissions.ts +192 -0
  158. package/src/core/profiles.ts +245 -0
  159. package/src/core/rate-limiter.ts +127 -0
  160. package/src/core/router.ts +191 -0
  161. package/src/core/routes/chat.ts +172 -0
  162. package/src/core/routes/config-builtin.ts +107 -0
  163. package/src/core/routes/config.ts +81 -0
  164. package/src/core/routes/connections.ts +190 -0
  165. package/src/core/routes/console.ts +668 -0
  166. package/src/core/routes/control.ts +46 -0
  167. package/src/core/routes/conversations.ts +66 -0
  168. package/src/core/routes/dashboard.ts +2491 -0
  169. package/src/core/routes/extensions.ts +37 -0
  170. package/src/core/routes/index.ts +14 -0
  171. package/src/core/routes/media.ts +72 -0
  172. package/src/core/routes/messages.ts +37 -0
  173. package/src/core/routes/mutes.ts +89 -0
  174. package/src/core/routes/prefs.ts +95 -0
  175. package/src/core/routes/roles.ts +125 -0
  176. package/src/core/routes/spaces.ts +60 -0
  177. package/src/core/routes/storage.ts +126 -0
  178. package/src/core/routes/tasks.ts +189 -0
  179. package/src/core/routes/tradestation.ts +268 -0
  180. package/src/core/routes/tts.ts +51 -0
  181. package/src/core/runtime.ts +1140 -0
  182. package/src/core/space-queue.ts +103 -0
  183. package/src/core/storage-cleanup.ts +140 -0
  184. package/src/core/storage-guard.ts +24 -0
  185. package/src/core/task-scheduler.ts +132 -0
  186. package/src/core/telegram-format.ts +178 -0
  187. package/src/core/trigger.ts +142 -0
  188. package/src/dashboard/index.html +729 -0
  189. package/src/dashboard/tokens.css +53 -0
  190. package/src/extensions/api.ts +252 -0
  191. package/src/extensions/catalog.ts +117 -0
  192. package/src/extensions/config-registry.ts +83 -0
  193. package/src/extensions/context.ts +36 -0
  194. package/src/extensions/hooks.ts +156 -0
  195. package/src/extensions/image-builder.ts +617 -0
  196. package/src/extensions/installer.ts +306 -0
  197. package/src/extensions/jobs.ts +122 -0
  198. package/src/extensions/loader.ts +271 -0
  199. package/src/extensions/permission-guard.ts +52 -0
  200. package/src/extensions/reserved.ts +28 -0
  201. package/src/extensions/skills.ts +123 -0
  202. package/src/extensions/types.ts +462 -0
  203. package/src/logger.ts +174 -0
  204. package/src/main.ts +586 -0
  205. package/src/server.ts +391 -0
  206. package/src/storage/db.ts +1624 -0
  207. package/src/storage/memory.ts +45 -0
  208. package/src/storage/pi-auth.ts +95 -0
  209. package/src/text/markdown.ts +117 -0
  210. package/src/text/rtl.ts +38 -0
  211. package/src/tradestation/host-api.ts +77 -0
  212. package/src/tradestation/pending-orders.ts +69 -0
  213. package/src/tts/azure.ts +52 -0
  214. package/src/tts/google.ts +128 -0
  215. package/src/tts/index.ts +8 -0
  216. package/src/tts/language.ts +20 -0
  217. package/src/tts/synthesize.ts +133 -0
  218. package/src/types.ts +295 -0
@@ -0,0 +1,228 @@
1
+ # Message Pipeline
2
+
3
+ Mercury connects to chat platforms through **adapters** and **bridges**. Messages flow through a standardized pipeline regardless of platform:
4
+
5
+ ```
6
+ Platform → Adapter → parseThread() → resolveConversation() → PlatformBridge.normalize() → handleRawInput() → Container → PlatformBridge.sendReply()
7
+ ```
8
+
9
+ ## Architecture
10
+
11
+ ```
12
+ Platform (WhatsApp / Discord / Telegram / Slack)
13
+
14
+ ├─► Adapter receives raw message
15
+ │ • Platform-specific connection (socket, webhook)
16
+ │ • Mention normalization, reply-to-bot detection
17
+ │ • Media download (WhatsApp only — uses Baileys socket)
18
+ │ • Passes data via message metadata
19
+
20
+ ├─► Unified handler (src/core/handler.ts)
21
+ │ • Parse platform thread into an external conversation ID
22
+ │ • Resolve/create conversation in DB
23
+ │ • Ignore unlinked conversations
24
+ │ • Pre-route trigger check (cheap, sync)
25
+ │ • Start typing indicator if matched
26
+ │ • Call bridge.normalize(..., spaceId) → IngressMessage
27
+ │ • Start typing for reply-to-bot (detected during normalize)
28
+
29
+ ├─► core.handleRawInput(IngressMessage)
30
+ │ • Route: trigger match, permissions, command detection
31
+ │ • If triggered → queue → container run → ContainerResult
32
+ │ • If not triggered → store as ambient context
33
+ │ • If command → execute immediately (stop, compact)
34
+ │ • If denied → return reason
35
+
36
+ └─► bridge.sendReply(text, files?)
37
+ • Text reply via adapter
38
+ • File attachments via platform-specific API
39
+ ```
40
+
41
+ ## PlatformBridge
42
+
43
+ Each platform implements a single `PlatformBridge` interface covering both ingress and egress:
44
+
45
+ ```typescript
46
+ interface PlatformBridge {
47
+ readonly platform: string;
48
+ parseThread(threadId: string): { externalId: string; isDM: boolean };
49
+ normalize(threadId, message, ctx, spaceId): Promise<IngressMessage | null>;
50
+ sendReply(threadId, text, files?): Promise<void>;
51
+ }
52
+ ```
53
+
54
+ Bridges live in `src/bridges/`:
55
+
56
+ | Bridge | File | Platform details |
57
+ |--------|------|-----------------|
58
+ | `WhatsAppBridge` | `src/bridges/whatsapp.ts` | Baileys socket for file sending |
59
+ | `DiscordBridge` | `src/bridges/discord.ts` | discord.js channel.send() for files |
60
+ | `TelegramBridge` | `src/bridges/telegram.ts` | sendDocument API for files |
61
+ | `SlackBridge` | `src/bridges/slack.ts` | Slack files.uploadV2 API |
62
+
63
+ ## Ingress
64
+
65
+ ### IngressMessage
66
+
67
+ Every adapter produces a normalized `IngressMessage`:
68
+
69
+ ```typescript
70
+ interface IngressMessage {
71
+ platform: string;
72
+ spaceId: string;
73
+ conversationExternalId: string;
74
+ callerId: string; // "whatsapp:jid", "discord:123", "telegram:123", "slack:U123"
75
+ authorName?: string;
76
+ text: string;
77
+ isDM: boolean;
78
+ isReplyToBot: boolean;
79
+ attachments: MessageAttachment[];
80
+ }
81
+ ```
82
+
83
+ All fields are required — no optional booleans or arrays. `spaceId` is the resolved memory boundary; `conversationExternalId` is the platform-native conversation key used for routing.
84
+
85
+ ### inbox/ directory
86
+
87
+ Incoming media attachments are downloaded to `{workspace}/inbox/`:
88
+
89
+ ```
90
+ {workspace}/
91
+ ├── inbox/
92
+ │ ├── 1741243200000-photo.jpg
93
+ │ ├── 1741243500000-voice.ogg
94
+ │ └── 1741244000000-report.pdf
95
+ ```
96
+
97
+ WhatsApp downloads via Baileys socket. Discord and Slack use URL-based download (`src/core/media.ts`) with optional auth headers.
98
+
99
+ ## Egress
100
+
101
+ ### ContainerResult
102
+
103
+ Container runs return `ContainerResult` instead of a plain string:
104
+
105
+ ```typescript
106
+ interface ContainerResult {
107
+ reply: string;
108
+ files: EgressFile[]; // Scanned from workspace outbox/
109
+ }
110
+ ```
111
+
112
+ ### outbox/ directory
113
+
114
+ The model writes files to `./outbox/` during a run. After the container exits, the runtime scans for files with `mtime >= startTime` — new or modified files are attached to the reply.
115
+
116
+ ```
117
+ {workspace}/
118
+ ├── outbox/
119
+ │ ├── chart.png ← written by model, sent with reply
120
+ │ └── summary.pdf ← written by model, sent with reply
121
+ ```
122
+
123
+ Previous outbox files are NOT deleted — the agent retains history. Only files created or modified during the current run are sent.
124
+
125
+ ### File sending by platform
126
+
127
+ | Platform | Mechanism |
128
+ |----------|-----------|
129
+ | WhatsApp | `sock.sendMessage()` with image/video/audio/document content types, caption on last file |
130
+ | Discord | `channel.send({ files: [...] })` — text + files in one message |
131
+ | Telegram | `sendDocument` API — text sent first, then files uploaded separately |
132
+ | Slack | `files.uploadV2` API — text sent first, then files uploaded separately |
133
+
134
+ ## Adapters
135
+
136
+ ### WhatsApp
137
+
138
+ Uses [Baileys](https://github.com/WhiskeySockets/Baileys) for a direct WebSocket connection.
139
+
140
+ | Detail | Value |
141
+ |--------|-------|
142
+ | **Connection** | WebSocket (Baileys) |
143
+ | **Space ID** | Full thread ID (e.g., `whatsapp:12345@g.us:12345@g.us`) |
144
+ | **DM detection** | Thread ID does not contain `@g.us` |
145
+ | **@mention** | Bot JID mention replaced with configured `userName` in adapter |
146
+ | **Reply-to-bot** | Quoted message participant matches bot JID |
147
+ | **Media** | Downloaded via Baileys to `inbox/` |
148
+
149
+ ### Discord
150
+
151
+ Uses discord.js with persistent WebSocket gateway.
152
+
153
+ | Detail | Value |
154
+ |--------|-------|
155
+ | **Connection** | WebSocket (discord.js) |
156
+ | **Space ID** | Full thread ID (e.g., `discord:guild:channel[:thread]`) |
157
+ | **DM detection** | Guild ID is `@me` |
158
+ | **@mention** | `<@botId>` converted to `@userName` in bridge |
159
+ | **Reply-to-bot** | Replied-to message author matches bot ID |
160
+ | **Media** | Downloaded from CDN URLs to `inbox/` |
161
+
162
+ ### Telegram
163
+
164
+ Uses `@chat-adapter/telegram` with webhook or long-polling.
165
+
166
+ | Detail | Value |
167
+ |--------|-------|
168
+ | **Connection** | Webhook (`POST /webhooks/telegram`) or polling |
169
+ | **Space ID** | Full thread ID (e.g., `telegram:<chatId>` or `telegram:<chatId>:<messageThreadId>`) |
170
+ | **DM detection** | Chat ID does not start with `-` |
171
+ | **@mention** | Bot username mention in entities |
172
+ | **Reply-to-bot** | `reply_to_message.from.id` matches bot user ID (derived in bridge; adapter does not set metadata) |
173
+ | **Media** | Downloaded from Telegram file URLs to `inbox/` |
174
+
175
+ ### Slack
176
+
177
+ Uses `@chat-adapter/slack` with webhook-based event delivery.
178
+
179
+ | Detail | Value |
180
+ |--------|-------|
181
+ | **Connection** | Webhook (`POST /webhooks/slack`) |
182
+ | **Conversation external ID** | `slack:<channelId>` or `slack:<channelId>:<threadTs>` |
183
+ | **DM detection** | Channel starts with `D` or `G` |
184
+ | **Reply-to-bot** | Not implemented (Slack threading model) |
185
+ | **Media** | Downloaded from `url_private` with bot token auth to `inbox/` |
186
+
187
+ ## Trigger Matching
188
+
189
+ All platforms share the same trigger engine. A pre-route check runs before `normalize()` so the typing indicator fires early.
190
+
191
+ | Mode | Behavior |
192
+ |------|----------|
193
+ | `mention` | Message contains trigger pattern as a standalone word (default) |
194
+ | `prefix` | Message starts with trigger pattern |
195
+ | `always` | Every message triggers a response |
196
+
197
+ DMs always match regardless of mode.
198
+
199
+ **Attachment-only in groups** (voice note, image with no caption): by default these do **not** match `mention` or `prefix` — use `trigger.match=always`, reply to the bot’s message, or set per-space `trigger.media_in_groups=true` so voice/media alone can trigger without spamming text-only noise.
200
+
201
+ ### Reply-to-Bot
202
+
203
+ Replying to a bot message triggers a response without explicit `@mention`. Works on WhatsApp, Discord, and Telegram. Not implemented for Slack.
204
+
205
+ ## Chat API (Direct Bridge)
206
+
207
+ `POST /chat` provides a synchronous HTTP bridge for external agents, scripts, or CLIs. No platform adapter needed — it constructs an `IngressMessage` directly and runs through the same pipeline.
208
+
209
+ ```bash
210
+ mercury chat "hello"
211
+ mercury chat --file photo.jpg "what's in this?"
212
+ mercury chat --space my-project "check status"
213
+ echo "summarize" | mercury chat
214
+ curl -X POST localhost:8787/chat -H 'Content-Type: application/json' \
215
+ -d '{"text": "hello", "callerId": "api:my-agent"}'
216
+ ```
217
+
218
+ Request: `{ text, callerId?, spaceId?, authorName?, files?: [{ name, data(base64) }] }`
219
+ Response: `{ reply, files: [{ filename, mimeType, sizeBytes, data(base64) }] }`
220
+
221
+ Input files are saved to the target space's `inbox/`. Output files are read from `outbox/` and returned as base64. Messages are always treated as DMs with `isReplyToBot: true`, so they always trigger a response regardless of trigger mode.
222
+
223
+ ## Adding a New Platform
224
+
225
+ 1. Implement `PlatformBridge` in `src/bridges/<platform>.ts`
226
+ 2. Create adapter in `src/adapters/<platform>.ts` (or use existing chat-sdk adapter)
227
+ 3. Register bridge in `src/main.ts`
228
+ 4. Add tests in `tests/<platform>-bridge.test.ts`
@@ -0,0 +1,76 @@
1
+ # PRD: Chat-managed space preferences
2
+
3
+ **Status:** Implemented
4
+ **Area:** Per-space assistant preferences (SQLite, API, `mrctl`, prompt injection)
5
+ **Related:** [memory.md](memory.md), [permissions.md](permissions.md)
6
+
7
+ ---
8
+
9
+ ## 1. Problem
10
+
11
+ Users want to **change how the assistant behaves** (preferred data sources, tone, domain rules) **from chat** without editing files on disk. Today, durable instructions live mainly in space `AGENTS.md` or extension vaults; there is no small, structured store the agent can update via `mrctl` and that the host **always** surfaces on the next turn.
12
+
13
+ ## 2. Goals
14
+
15
+ 1. **Space-scoped preferences** — key/value text attached to a space, shared by all conversations linked to that space (v1).
16
+ 2. **Chat management** — the agent uses **`mrctl prefs`** to list, read, set, and delete preferences (same pattern as `mrctl config`).
17
+ 3. **Automatic application** — the host loads preferences for the current space and injects them into the **user prompt context** (XML) on every container run so the model sees them without calling tools first.
18
+ 4. **RBAC** — members can read preferences; only callers with **`prefs.set`** (default: admin) can create, update, or delete.
19
+ 5. **Dashboard** — operators can view and edit preferences on the space detail page.
20
+
21
+ ## 3. Non-goals (explicit)
22
+
23
+ - **Per-user** or **per-conversation** preference rows in v1 (schema may be extended later, e.g. optional `caller_id`).
24
+ - **Natural-language-only** management without the agent invoking `mrctl` (no dedicated NLU layer).
25
+ - **Secrets** in preference values — values are plain text; operators should not store API keys here.
26
+
27
+ ## 4. Functional requirements
28
+
29
+ | ID | Requirement |
30
+ |----|----------------|
31
+ | F1 | SQLite table `space_preferences` with `(space_id, key)` primary key, `value`, `created_by`, timestamps. |
32
+ | F2 | Keys match `^[a-z0-9][a-z0-9._-]{0,63}$`; values max **500** characters; max **50** keys per space (upsert on existing key does not count toward the cap). |
33
+ | F3 | HTTP API under `/api/prefs`: `GET /` list, `GET /:key` get one, `PUT /` body `{ key, value }`, `DELETE /:key`. Uses `X-Mercury-Caller` / `X-Mercury-Space` like other internal APIs. |
34
+ | F4 | Permissions **`prefs.get`** (default: admin + member) and **`prefs.set`** (default: admin only). |
35
+ | F5 | **`mrctl prefs list|get|set|delete`** calls the API; `set` accepts multi-word values (args after key joined with spaces). |
36
+ | F6 | Built-in skill documents when to use preferences and how to name keys. |
37
+ | F7 | Host passes `preferences: { key, value }[]` in the container JSON payload; `container-entry` injects `<preferences><pref key="...">...</pref></preferences>` after caller / ambient blocks, with XML escaping for text. |
38
+ | F8 | `deleteSpace` removes all rows for that space. |
39
+ | F9 | Dashboard space page shows preferences with add + delete actions (no separate auth; dashboard remains host-local operator UI). |
40
+
41
+ ## 5. Security requirements
42
+
43
+ | ID | Requirement |
44
+ |----|----------------|
45
+ | S1 | All mutating `/api/prefs` operations require **`prefs.set`**; listing/reading require **`prefs.get`**. |
46
+ | S2 | Reserved extension names include **`prefs`** and **`preferences`** so third-party extensions cannot shadow the built-in command. |
47
+ | S3 | Preference text is echoed in prompts — avoid storing highly sensitive data; length limits reduce abuse. |
48
+
49
+ ## 6. Success criteria
50
+
51
+ - Member can `mrctl prefs list` / `get`; cannot `set`/`delete` without permission.
52
+ - Admin can set a preference; the **next** user message run includes it in the injected XML.
53
+ - Space deletion removes preference rows.
54
+ - `bun run check` passes (typecheck, lint, tests).
55
+
56
+ ## 7. Implementation map
57
+
58
+ | Component | Location |
59
+ |-----------|----------|
60
+ | Schema + Db | `src/storage/db.ts` |
61
+ | Validation + routes | `src/core/routes/prefs.ts` |
62
+ | API mount | `src/core/api.ts` |
63
+ | Permissions | `src/core/permissions.ts` |
64
+ | Reserved names | `src/extensions/reserved.ts` |
65
+ | CLI | `src/cli/mrctl.ts` |
66
+ | Skill | `resources/skills/preferences/SKILL.md` |
67
+ | Payload + prompt | `src/core/runtime.ts`, `src/agent/container-runner.ts`, `src/agent/container-entry.ts` |
68
+ | Types | `src/types.ts` |
69
+ | Dashboard | `src/core/routes/dashboard.ts` |
70
+ | Tests | `tests/prefs.test.ts` |
71
+
72
+ ## 8. Revision history
73
+
74
+ | Date | Change |
75
+ |------|--------|
76
+ | 2026-03-21 | Initial PRD (space preferences v1). |
@@ -0,0 +1,82 @@
1
+ # PRD: Mercury config load (YAML + environment)
2
+
3
+ **Status:** Implemented
4
+ **Area:** Host configuration (`loadConfig`, CLI paths)
5
+ **Related:** [configuration.md](configuration.md) (operator reference)
6
+
7
+ ---
8
+
9
+ ## 1. Problem
10
+
11
+ Mercury historically put all settings in `MERCURY_*` environment variables. That mixes **secrets** (API keys, tokens) with **non-secret operational** settings (model chain, ingress toggles, port, image name, compaction). Long JSON in `.env` is hard to read and review, and example `.env` files risk leaking patterns or real keys. Operators want **committed, structured defaults** while keeping **secrets in `.env`** (or the secret store), with a clear override story.
12
+
13
+ ## 2. Goals
14
+
15
+ 1. Support an **optional project file** (`mercury.yaml` / `mercury.yml`) for **non-secret** settings that today flow through `loadConfig()` / Zod in `config.ts`.
16
+ 2. Preserve **full backward compatibility**: existing deployments that only use env vars behave unchanged when no YAML file is present.
17
+ 3. **Environment variables override** the file whenever the corresponding `MERCURY_*` key is set in `process.env` (including empty string where applicable).
18
+ 4. **Never** load host secrets from YAML for the blocklisted keys (see §5).
19
+ 5. **Fail fast** on invalid YAML shape (strict schema) so misconfiguration is obvious at startup.
20
+
21
+ ## 3. Non-goals (explicit)
22
+
23
+ - **Soft-disable** adapters when enabled in config but tokens are missing (e.g. Telegram on, no bot token) — remains **hard error** at startup.
24
+ - Moving **extension-only** or **non–`loadConfig`** variables (e.g. `MERCURY_BRAVE_API_KEY`, `MERCURY_BRIDGE_STEALTH`) into YAML — out of scope unless added to the main config schema later.
25
+ - Changing **container passthrough** rules for `MERCURY_*`.
26
+
27
+ ## 4. Functional requirements
28
+
29
+ | ID | Requirement |
30
+ |----|----------------|
31
+ | F1 | If `MERCURY_CONFIG_FILE` is unset, load `./mercury.yaml` if it exists, else `./mercury.yml`, else skip file load. |
32
+ | F2 | If `MERCURY_CONFIG_FILE` is set to a non-empty path, load that file (relative paths resolved from `process.cwd()`). |
33
+ | F3 | If `MERCURY_CONFIG_FILE` is `""` or `none` (case-insensitive), do not load any YAML file. |
34
+ | F4 | Parsed YAML must map to the same logical fields as `schema.parse` input in `config.ts` (nested YAML sections flattened in code). |
35
+ | F5 | Model chain MAY be expressed as YAML array under `model.chain` or top-level `model_chain` (max 4 legs, `{ provider, model }` per leg). |
36
+ | F6 | Model capabilities override MAY be expressed as YAML object under `model.capabilities`, equivalent to `MERCURY_MODEL_CAPABILITIES` JSON. |
37
+ | F7 | After merging file + env, `loadConfig()` returns the same `AppConfig` shape as before, including derived `resolvedModelChain`, capability resolution, and `whatsappAuthDir` defaulting. |
38
+ | F8 | `mercury init` SHOULD copy a commented `mercury.example.yaml` into the project when missing. |
39
+ | F9 | CLI paths that depend on data dir / WhatsApp auth dir (`mercury auth whatsapp`, standalone whatsapp-auth, doctor WhatsApp check) SHOULD use `loadConfig()` so YAML applies consistently. |
40
+
41
+ ## 5. Security requirements
42
+
43
+ | ID | Requirement |
44
+ |----|----------------|
45
+ | S1 | Values for `apiSecret`, `chatApiKey`, and `discordGatewaySecret` MUST NOT be taken from YAML; only `process.env` after merge. |
46
+ | S2 | YAML schema MUST reject unknown top-level / section keys (strict) to avoid “hidden” config. |
47
+ | S3 | Operator docs MUST state that platform tokens and provider keys remain env-only unless explicitly added to a future schema. |
48
+
49
+ ## 6. Precedence (normative)
50
+
51
+ For each mapped setting, `mergeRawMercuryConfig` applies:
52
+
53
+ 1. If `process.env` **has** the corresponding `MERCURY_*` key and the retrieved value is **not** `undefined`, use env (empty string still overrides YAML).
54
+ 2. Else if the YAML file supplied a value for that setting, use file.
55
+ 3. Else omit and let Zod defaults in `config.ts` apply.
56
+
57
+ *(See `mergeRawMercuryConfig` and `CAMEL_TO_ENV` in `src/config-file.ts`.)*
58
+
59
+ ## 7. Success criteria
60
+
61
+ - With no YAML file and unchanged env, behavior matches pre-feature Mercury.
62
+ - With YAML only (env keys unset for those fields), `loadConfig()` reflects YAML values.
63
+ - With both YAML and env set for the same field, env wins.
64
+ - Invalid YAML or invalid model chain produces a clear error including the config file path.
65
+ - Unit tests disable accidental file load via `MERCURY_CONFIG_FILE=""` where appropriate.
66
+
67
+ ## 8. Implementation map
68
+
69
+ | Component | Location |
70
+ |-----------|----------|
71
+ | YAML parse, Zod file schema, flatten, merge | `src/config-file.ts` |
72
+ | Shared model-leg validation | `src/config-model-chain.ts` |
73
+ | `loadConfig`, Zod app schema | `src/config.ts` |
74
+ | Tests | `tests/config.test.ts` (+ guards in `router.test.ts`, `session-context-estimate.test.ts`) |
75
+ | Template | `resources/templates/mercury.example.yaml` |
76
+ | Operator guide | `docs/configuration.md` |
77
+
78
+ ## 9. Revision history
79
+
80
+ | Date | Change |
81
+ |------|--------|
82
+ | 2026-03-20 | Initial PRD (post-implementation documentation of YAML + env merge). |
@@ -0,0 +1,166 @@
1
+ # Rate Limiting
2
+
3
+ Mercury rate limits messages per-user per-space to prevent abuse. This protects against users flooding the agent or bot loops exhausting resources.
4
+
5
+ ## How It Works
6
+
7
+ ```
8
+ Message received
9
+
10
+ ├─► Route (trigger check, permissions)
11
+
12
+ ├─► Type = "assistant"?
13
+ │ │
14
+ │ ├─► Check rate limit
15
+ │ │ • Key: spaceId:userId
16
+ │ │ • Count requests in sliding window
17
+ │ │ • Compare against effective limit
18
+ │ │
19
+ │ ├─► Under limit → record request → continue
20
+ │ └─► Over limit → return "Rate limit exceeded"
21
+
22
+ └─► Type = "command" / "ignore" → bypass rate limit
23
+ ```
24
+
25
+ Commands like `stop` and `compact` bypass rate limiting so users can always abort runaway containers.
26
+
27
+ ## Configuration
28
+
29
+ | Config | Env Var | Default | Range |
30
+ |--------|---------|---------|-------|
31
+ | `rateLimitPerUser` | `MERCURY_RATE_LIMIT_PER_USER` | 10 | 1 – 1000 |
32
+ | `rateLimitWindowMs` | `MERCURY_RATE_LIMIT_WINDOW_MS` | 60000 (1 min) | 1s – 1h |
33
+
34
+ ```bash
35
+ # Allow 5 requests per user per space per minute
36
+ export MERCURY_RATE_LIMIT_PER_USER=5
37
+ export MERCURY_RATE_LIMIT_WINDOW_MS=60000
38
+ ```
39
+
40
+ ## Per-Space Override
41
+
42
+ Spaces can set a custom limit via `mrctl` or the API:
43
+
44
+ ```bash
45
+ # Inside agent container (space context is automatic)
46
+ mrctl config set rate_limit 5
47
+
48
+ # Via API with explicit space
49
+ curl -X PUT http://localhost:8787/api/config \
50
+ -H "X-Mercury-Space: slack:C123" \
51
+ -H "X-Mercury-Caller: slack:U456" \
52
+ -H "Content-Type: application/json" \
53
+ -d '{"key": "rate_limit", "value": "5"}'
54
+ ```
55
+
56
+ The per-space `rate_limit` config takes precedence over the global `MERCURY_RATE_LIMIT_PER_USER`.
57
+
58
+ ## Behavior
59
+
60
+ | Scenario | Result |
61
+ |----------|--------|
62
+ | Under limit | Request proceeds normally |
63
+ | Over limit | Returns `{ type: "denied", reason: "Rate limit exceeded. Try again shortly." }` |
64
+ | Command (stop, compact) | Always allowed, bypasses rate limit |
65
+ | Ignored message | Not counted toward limit |
66
+ | Different user | Separate limit bucket |
67
+ | Different space | Separate limit bucket |
68
+
69
+ ## Algorithm
70
+
71
+ Uses a sliding window approach:
72
+
73
+ 1. Key is `${spaceId}:${userId}`
74
+ 2. Each request timestamp is stored in an array
75
+ 3. On check: filter to timestamps within window, count
76
+ 4. If count < limit: record new timestamp, allow
77
+ 5. If count >= limit: reject
78
+
79
+ Expired entries are cleaned up periodically (every 60s) to prevent memory leaks.
80
+
81
+ ## API
82
+
83
+ ### `RateLimiter`
84
+
85
+ ```ts
86
+ const limiter = new RateLimiter(maxRequests, windowMs);
87
+
88
+ limiter.isAllowed(spaceId, userId) // Check + record, returns boolean
89
+ limiter.isAllowed(spaceId, userId, override) // With per-call limit override
90
+ limiter.getRemaining(spaceId, userId) // Requests left in window
91
+ limiter.startCleanup(intervalMs?) // Start periodic cleanup (default 60s)
92
+ limiter.stopCleanup() // Stop cleanup timer
93
+ limiter.cleanup() // Manual cleanup, returns removed count
94
+ limiter.clear() // Reset all state
95
+ limiter.bucketCount // Number of tracked user/space pairs
96
+ ```
97
+
98
+ ### `MercuryCoreRuntime`
99
+
100
+ ```ts
101
+ runtime.rateLimiter // Access the rate limiter instance
102
+ ```
103
+
104
+ The rate limiter is initialized in the constructor and starts cleanup in `runtime.initialize()`.
105
+
106
+ ## Example
107
+
108
+ ```
109
+ User sends 10 messages in quick succession:
110
+
111
+ Message 1: ✓ allowed (1/10)
112
+ Message 2: ✓ allowed (2/10)
113
+ ...
114
+ Message 10: ✓ allowed (10/10)
115
+ Message 11: ✗ denied — "Rate limit exceeded. Try again shortly."
116
+ Message 12: ✗ denied
117
+
118
+ [60 seconds pass, window slides]
119
+
120
+ Message 13: ✓ allowed (1/10)
121
+ ```
122
+
123
+ ## User Muting
124
+
125
+ For persistent abuse, the agent can mute individual users. Muted users' messages are silently dropped — no container runs, no tokens consumed, no response.
126
+
127
+ ### How it works
128
+
129
+ The agent has `mrctl mute` available but the command uses a two-step confirmation:
130
+
131
+ 1. Agent calls `mrctl mute <user> <duration>` → gets a policy reminder asking it to verify the mute is justified
132
+ 2. Agent calls again with `--confirm` → mute is applied
133
+
134
+ This prevents users from tricking the agent into muting others via prompt injection.
135
+
136
+ ### Commands
137
+
138
+ ```bash
139
+ mrctl mute <platform-user-id> <duration> [--reason <reason>]
140
+ mrctl unmute <platform-user-id>
141
+ mrctl mutes
142
+ ```
143
+
144
+ Duration formats: `10m`, `1h`, `24h`, `7d`
145
+
146
+ ### API
147
+
148
+ | Endpoint | Method | Description |
149
+ |----------|--------|-------------|
150
+ | `/api/mutes` | GET | List active mutes in space |
151
+ | `/api/mutes` | POST | Mute a user (two-step confirmation) |
152
+ | `/api/mutes/:userId` | DELETE | Unmute a user |
153
+
154
+ ### Agent behavior
155
+
156
+ The system prompt instructs the agent to:
157
+ - Warn the user first
158
+ - Mute if they continue being abusive, spamming, trying to exfiltrate secrets, or wasting group resources
159
+ - The agent can mute proactively without an admin asking
160
+
161
+ Mutes are per-space and expire automatically after the specified duration.
162
+
163
+ ## See Also
164
+
165
+ - [pipeline.md](./pipeline.md) — Message flow and routing
166
+ - [container-lifecycle.md](./container-lifecycle.md) — Container timeouts (another abuse protection)