mercury-agent 0.5.0 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/README.md +452 -451
  2. package/container/Dockerfile +127 -127
  3. package/container/Dockerfile.base +109 -109
  4. package/container/Dockerfile.power +17 -17
  5. package/container/agent-package.json +8 -8
  6. package/container/build.sh +54 -54
  7. package/docs/ROADMAP.md +3 -4
  8. package/docs/archive/summarization/2026-07-02-recent-dev-summary.md +71 -0
  9. package/docs/auth/dashboard.md +28 -28
  10. package/docs/auth/overview.md +109 -109
  11. package/docs/auth/whatsapp.md +173 -173
  12. package/docs/authoring-profiles.md +174 -174
  13. package/docs/configuration.md +54 -54
  14. package/docs/container-lifecycle.md +349 -349
  15. package/docs/{bugs/bwrap-privileged-linux-docker.md → debug/major/2026-07-02-bwrap-privileged-linux-docker.md} +21 -1
  16. package/docs/{bugs/e2big-prompt-delivery-fix.md → debug/major/2026-07-02-e2big-prompt-delivery-fix.md} +27 -1
  17. package/docs/{bugs/registry-pull-no-local-fallback.md → debug/major/2026-07-02-registry-pull-no-local-fallback.md} +27 -1
  18. package/docs/debug/summarization/2026-07-02-common-bug-patterns.md +72 -0
  19. package/docs/deployment.md +199 -199
  20. package/docs/extensions.md +375 -375
  21. package/docs/graceful-shutdown.md +62 -62
  22. package/docs/kb-distillation.md +77 -77
  23. package/docs/media/overview.md +140 -140
  24. package/docs/media/whatsapp.md +171 -171
  25. package/docs/memory.md +137 -137
  26. package/docs/permissions.md +217 -217
  27. package/docs/pipeline.md +228 -228
  28. package/docs/prd-chat-memory.md +76 -76
  29. package/docs/prd-config-load.md +82 -82
  30. package/docs/rate-limiting.md +229 -229
  31. package/docs/runbooks/publish-checklist.md +9 -1
  32. package/docs/scheduler.md +288 -288
  33. package/docs/setup-discord.md +100 -100
  34. package/docs/setup-slack.md +119 -119
  35. package/docs/setup-whatsapp.md +94 -94
  36. package/docs/subagents.md +166 -166
  37. package/docs/web-search.md +62 -62
  38. package/examples/extensions/README.md +12 -12
  39. package/examples/extensions/charts/index.ts +13 -13
  40. package/examples/extensions/charts/skill/SKILL.md +98 -98
  41. package/examples/extensions/gws/README.md +52 -52
  42. package/examples/extensions/gws/skill/SKILL.md +57 -57
  43. package/examples/extensions/gws/skill/references/calendar.md +101 -101
  44. package/examples/extensions/gws/skill/references/docs.md +65 -65
  45. package/examples/extensions/gws/skill/references/drive.md +79 -79
  46. package/examples/extensions/gws/skill/references/gmail.md +85 -85
  47. package/examples/extensions/gws/skill/references/sheets.md +60 -60
  48. package/examples/extensions/napkin/skill/SKILL.md +728 -728
  49. package/examples/extensions/pdf/skill/LICENSE.txt +30 -30
  50. package/examples/extensions/pdf/skill/SKILL.md +314 -314
  51. package/examples/extensions/pdf/skill/forms.md +294 -294
  52. package/examples/extensions/pdf/skill/reference.md +611 -611
  53. package/examples/extensions/pdf/skill/scripts/check_bounding_boxes.py +65 -65
  54. package/examples/extensions/pdf/skill/scripts/check_fillable_fields.py +11 -11
  55. package/examples/extensions/pdf/skill/scripts/convert_pdf_to_images.py +33 -33
  56. package/examples/extensions/pdf/skill/scripts/create_validation_image.py +37 -37
  57. package/examples/extensions/pdf/skill/scripts/extract_form_field_info.py +122 -122
  58. package/examples/extensions/pdf/skill/scripts/extract_form_structure.py +115 -115
  59. package/examples/extensions/pdf/skill/scripts/fill_fillable_fields.py +98 -98
  60. package/examples/extensions/pdf/skill/scripts/fill_pdf_form_with_annotations.py +107 -107
  61. package/examples/extensions/permission-guard/index.ts +65 -65
  62. package/examples/extensions/pinchtab/skill/SKILL.md +224 -224
  63. package/examples/extensions/pinchtab/skill/TRUST.md +69 -69
  64. package/examples/extensions/pinchtab/skill/references/api.md +297 -297
  65. package/examples/extensions/pinchtab/skill/references/env.md +45 -45
  66. package/examples/extensions/pinchtab/skill/references/profiles.md +107 -107
  67. package/examples/extensions/tradestation/host/refresh.ts +102 -102
  68. package/examples/extensions/tradestation/index.ts +153 -153
  69. package/examples/extensions/tradestation/skill/SKILL.md +67 -67
  70. package/examples/extensions/voice-synth/index.ts +94 -94
  71. package/examples/extensions/voice-synth/skill/SKILL.md +38 -38
  72. package/examples/extensions/voice-transcribe/requirements.txt +8 -8
  73. package/examples/extensions/voice-transcribe/scripts/transcribe.py +179 -179
  74. package/examples/extensions/voice-transcribe/skill/SKILL.md +53 -53
  75. package/examples/extensions/yahoo-mail/cli/package.json +13 -13
  76. package/examples/extensions/yahoo-mail/skill/SKILL.md +78 -78
  77. package/package.json +106 -106
  78. package/resources/agents/explore.md +50 -50
  79. package/resources/agents/worker.md +24 -24
  80. package/resources/connection-env-vars.json +25 -25
  81. package/resources/pi-extensions/subagent/agents.ts +126 -126
  82. package/resources/pi-extensions/subagent/index.ts +964 -964
  83. package/resources/profiles/coding/AGENTS.md +43 -43
  84. package/resources/profiles/coding/mercury-profile.yaml +15 -15
  85. package/resources/profiles/general/AGENTS.md +31 -31
  86. package/resources/profiles/general/mercury-profile.yaml +15 -15
  87. package/resources/profiles/research/AGENTS.md +40 -40
  88. package/resources/profiles/research/mercury-profile.yaml +15 -15
  89. package/resources/skills/config/SKILL.md +25 -25
  90. package/resources/skills/context/SKILL.md +33 -33
  91. package/resources/skills/conversation-recap/SKILL.md +19 -19
  92. package/resources/skills/mutes/SKILL.md +31 -31
  93. package/resources/skills/permissions/SKILL.md +19 -19
  94. package/resources/skills/preferences/SKILL.md +31 -31
  95. package/resources/skills/recall/SKILL.md +24 -24
  96. package/resources/skills/roles/SKILL.md +18 -18
  97. package/resources/skills/spaces/SKILL.md +18 -18
  98. package/resources/skills/tasks/SKILL.md +45 -45
  99. package/resources/templates/AGENTS.md +157 -157
  100. package/resources/templates/env.template +38 -38
  101. package/resources/templates/mercury.example.yaml +99 -99
  102. package/src/agent/container-entry.ts +1 -1
  103. package/src/agent/container-runner.ts +1345 -1346
  104. package/src/cli/mercury.ts +39 -7
  105. package/src/cli/mrctl.ts +636 -636
  106. package/src/config-file.ts +540 -540
  107. package/src/config.ts +339 -339
  108. package/src/core/api.ts +125 -125
  109. package/src/core/caller-token.ts +101 -101
  110. package/src/core/permissions.ts +228 -228
  111. package/src/core/profiles.ts +271 -271
  112. package/src/core/routes/capability.ts +70 -70
  113. package/src/core/routes/index.ts +15 -15
  114. package/src/core/runtime.ts +1530 -1530
  115. package/src/dashboard/index.html +729 -729
  116. package/src/extensions/api.ts +273 -273
  117. package/src/extensions/loader.ts +286 -286
  118. package/src/extensions/types.ts +517 -517
  119. package/src/main.ts +605 -605
  120. package/docs/pending-updates/applicative-profiles.md +0 -15
  121. package/docs/pending-updates/caller-bound-capability-token.md +0 -15
  122. package/docs/pending-updates/dm-auto-space.md +0 -15
  123. /package/docs/archive/{2026-07-01-applicative-profiles.md → applicative-profiles/2026-07-01-applicative-profiles.md} +0 -0
  124. /package/docs/archive/{2026-07-01-caller-bound-capability-token.md → applicative-profiles/2026-07-01-caller-bound-capability-token.md} +0 -0
  125. /package/docs/archive/{2026-07-01-dm-auto-space.md → applicative-profiles/2026-07-01-dm-auto-space.md} +0 -0
@@ -1,82 +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). |
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). |
@@ -1,229 +1,229 @@
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
- │ ├─► Daily role check (if rate_limit.<role> key set and > 0)
15
- │ │ • SQLite counter (space_id + user_id + UTC date)
16
- │ │ • Denied: "You've used N/M messages today. Resets in Xh."
17
- │ │
18
- │ ├─► Burst check (sliding window, always runs)
19
- │ │ • In-memory, key: spaceId:userId
20
- │ │ • Count requests in 60s window
21
- │ │ • Denied: "Rate limit exceeded. Try again shortly."
22
- │ │
23
- │ ├─► Both pass → continue to container
24
- │ └─► Either fails → return denial
25
-
26
- └─► Type = "command" / "ignore" → bypass rate limits
27
- ```
28
-
29
- Commands like `stop` and `compact` bypass rate limiting so users can always abort runaway containers.
30
-
31
- ## Configuration
32
-
33
- | Config | Env Var | Default | Range |
34
- |--------|---------|---------|-------|
35
- | `rateLimitPerUser` | `MERCURY_RATE_LIMIT_PER_USER` | 10 | 1 – 1000 |
36
- | `rateLimitWindowMs` | `MERCURY_RATE_LIMIT_WINDOW_MS` | 60000 (1 min) | 1s – 1h |
37
-
38
- ```bash
39
- # Allow 5 requests per user per space per minute
40
- export MERCURY_RATE_LIMIT_PER_USER=5
41
- export MERCURY_RATE_LIMIT_WINDOW_MS=60000
42
- ```
43
-
44
- ## Per-Space Override
45
-
46
- Spaces can set a custom limit via `mrctl` or the API:
47
-
48
- ```bash
49
- # Inside agent container (space context is automatic)
50
- mrctl config set rate_limit 5
51
-
52
- # Via API with explicit space
53
- curl -X PUT http://localhost:8787/api/config \
54
- -H "X-Mercury-Space: slack:C123" \
55
- -H "X-Mercury-Caller: slack:U456" \
56
- -H "Content-Type: application/json" \
57
- -d '{"key": "rate_limit", "value": "5"}'
58
- ```
59
-
60
- The per-space `rate_limit` config takes precedence over the global `MERCURY_RATE_LIMIT_PER_USER`.
61
-
62
- ## Behavior
63
-
64
- | Scenario | Result |
65
- |----------|--------|
66
- | Under limit | Request proceeds normally |
67
- | Over limit | Returns `{ type: "denied", reason: "Rate limit exceeded. Try again shortly." }` |
68
- | Command (stop, compact) | Always allowed, bypasses rate limit |
69
- | Ignored message | Not counted toward limit |
70
- | Different user | Separate limit bucket |
71
- | Different space | Separate limit bucket |
72
-
73
- ## Algorithm
74
-
75
- Uses a sliding window approach:
76
-
77
- 1. Key is `${spaceId}:${userId}`
78
- 2. Each request timestamp is stored in an array
79
- 3. On check: filter to timestamps within window, count
80
- 4. If count < limit: record new timestamp, allow
81
- 5. If count >= limit: reject
82
-
83
- Expired entries are cleaned up periodically (every 60s) to prevent memory leaks.
84
-
85
- ## API
86
-
87
- ### `RateLimiter`
88
-
89
- ```ts
90
- const limiter = new RateLimiter(maxRequests, windowMs);
91
-
92
- limiter.isAllowed(spaceId, userId) // Check + record, returns boolean
93
- limiter.isAllowed(spaceId, userId, override) // With per-call limit override
94
- limiter.getRemaining(spaceId, userId) // Requests left in window
95
- limiter.startCleanup(intervalMs?) // Start periodic cleanup (default 60s)
96
- limiter.stopCleanup() // Stop cleanup timer
97
- limiter.cleanup() // Manual cleanup, returns removed count
98
- limiter.clear() // Reset all state
99
- limiter.bucketCount // Number of tracked user/space pairs
100
- ```
101
-
102
- ### `MercuryCoreRuntime`
103
-
104
- ```ts
105
- runtime.rateLimiter // Access the rate limiter instance
106
- ```
107
-
108
- The rate limiter is initialized in the constructor and starts cleanup in `runtime.initialize()`.
109
-
110
- ## Example
111
-
112
- ```
113
- User sends 10 messages in quick succession:
114
-
115
- Message 1: ✓ allowed (1/10)
116
- Message 2: ✓ allowed (2/10)
117
- ...
118
- Message 10: ✓ allowed (10/10)
119
- Message 11: ✗ denied — "Rate limit exceeded. Try again shortly."
120
- Message 12: ✗ denied
121
-
122
- [60 seconds pass, window slides]
123
-
124
- Message 13: ✓ allowed (1/10)
125
- ```
126
-
127
- ## User Muting
128
-
129
- For persistent abuse, the agent can mute individual users. Muted users' messages are silently dropped — no container runs, no tokens consumed, no response.
130
-
131
- ### How it works
132
-
133
- The agent has `mrctl mute` available but the command uses a two-step confirmation:
134
-
135
- 1. Agent calls `mrctl mute <user> <duration>` → gets a policy reminder asking it to verify the mute is justified
136
- 2. Agent calls again with `--confirm` → mute is applied
137
-
138
- This prevents users from tricking the agent into muting others via prompt injection.
139
-
140
- ### Commands
141
-
142
- ```bash
143
- mrctl mute <platform-user-id> <duration> [--reason <reason>]
144
- mrctl unmute <platform-user-id>
145
- mrctl mutes
146
- ```
147
-
148
- Duration formats: `10m`, `1h`, `24h`, `7d`
149
-
150
- ### API
151
-
152
- | Endpoint | Method | Description |
153
- |----------|--------|-------------|
154
- | `/api/mutes` | GET | List active mutes in space |
155
- | `/api/mutes` | POST | Mute a user (two-step confirmation) |
156
- | `/api/mutes/:userId` | DELETE | Unmute a user |
157
-
158
- ### Agent behavior
159
-
160
- The system prompt instructs the agent to:
161
- - Warn the user first
162
- - Mute if they continue being abusive, spamming, trying to exfiltrate secrets, or wasting group resources
163
- - The agent can mute proactively without an admin asking
164
-
165
- Mutes are per-space and expire automatically after the specified duration.
166
-
167
- ## Role-Based Daily Limits
168
-
169
- In addition to the burst limiter, spaces can set per-role daily message caps. This gives agent owners cost control — admins get unlimited access to their own bot while capping how much others can use it.
170
-
171
- ### Configuration
172
-
173
- Set `rate_limit.<role>` keys via `mrctl` or the API:
174
-
175
- ```bash
176
- # Admins: unlimited (0 = no daily limit)
177
- mrctl config set rate_limit.admin 0
178
-
179
- # Members: 5 messages per day
180
- mrctl config set rate_limit.member 5
181
- ```
182
-
183
- Via API:
184
- ```bash
185
- curl -X PUT http://localhost:8787/api/config \
186
- -H "X-Mercury-Space: slack:C123" \
187
- -H "X-Mercury-Caller: slack:U456" \
188
- -H "Content-Type: application/json" \
189
- -d '{"key": "rate_limit.member", "value": "5"}'
190
- ```
191
-
192
- ### Behavior
193
-
194
- | Value | Effect |
195
- |-------|--------|
196
- | `0` | Unlimited (daily check skipped for this role) |
197
- | `> 0` | Daily cap enforced; denied requests show count + reset time |
198
- | Not set | No daily limit for this role (burst limiter still applies) |
199
- | Invalid (NaN) | Treated as "not set" |
200
-
201
- ### How the two layers interact
202
-
203
- ```
204
- Message arrives (type = "assistant")
205
-
206
- ├─► Daily role check (if rate_limit.<role> key exists and > 0)
207
- │ • Key: space_id + user_id + UTC date
208
- │ • SQLite counter (persists across restarts)
209
- │ • Denied: "You've used 5/5 messages today. Resets in 3h."
210
-
211
- └─► Burst check (sliding window, always runs for all users)
212
- • In-memory timestamps
213
- • Denied: "Rate limit exceeded. Try again shortly."
214
- ```
215
-
216
- Both layers are independent — both must pass. A user can be allowed by the daily check but blocked by the burst limiter, or vice versa. Denied requests do not increment the daily counter.
217
-
218
- ### Special cases
219
-
220
- - **System callers** (scheduled tasks): exempt from daily limits unconditionally
221
- - **Commands** (`stop`, `compact`, etc.): bypass both layers
222
- - **Custom roles**: set `rate_limit.<custom_role>` for any role name
223
- - **Daily reset**: counters reset at UTC midnight
224
- - **Counters are per-space**: each space tracks independently
225
-
226
- ## See Also
227
-
228
- - [pipeline.md](./pipeline.md) — Message flow and routing
229
- - [container-lifecycle.md](./container-lifecycle.md) — Container timeouts (another abuse protection)
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
+ │ ├─► Daily role check (if rate_limit.<role> key set and > 0)
15
+ │ │ • SQLite counter (space_id + user_id + UTC date)
16
+ │ │ • Denied: "You've used N/M messages today. Resets in Xh."
17
+ │ │
18
+ │ ├─► Burst check (sliding window, always runs)
19
+ │ │ • In-memory, key: spaceId:userId
20
+ │ │ • Count requests in 60s window
21
+ │ │ • Denied: "Rate limit exceeded. Try again shortly."
22
+ │ │
23
+ │ ├─► Both pass → continue to container
24
+ │ └─► Either fails → return denial
25
+
26
+ └─► Type = "command" / "ignore" → bypass rate limits
27
+ ```
28
+
29
+ Commands like `stop` and `compact` bypass rate limiting so users can always abort runaway containers.
30
+
31
+ ## Configuration
32
+
33
+ | Config | Env Var | Default | Range |
34
+ |--------|---------|---------|-------|
35
+ | `rateLimitPerUser` | `MERCURY_RATE_LIMIT_PER_USER` | 10 | 1 – 1000 |
36
+ | `rateLimitWindowMs` | `MERCURY_RATE_LIMIT_WINDOW_MS` | 60000 (1 min) | 1s – 1h |
37
+
38
+ ```bash
39
+ # Allow 5 requests per user per space per minute
40
+ export MERCURY_RATE_LIMIT_PER_USER=5
41
+ export MERCURY_RATE_LIMIT_WINDOW_MS=60000
42
+ ```
43
+
44
+ ## Per-Space Override
45
+
46
+ Spaces can set a custom limit via `mrctl` or the API:
47
+
48
+ ```bash
49
+ # Inside agent container (space context is automatic)
50
+ mrctl config set rate_limit 5
51
+
52
+ # Via API with explicit space
53
+ curl -X PUT http://localhost:8787/api/config \
54
+ -H "X-Mercury-Space: slack:C123" \
55
+ -H "X-Mercury-Caller: slack:U456" \
56
+ -H "Content-Type: application/json" \
57
+ -d '{"key": "rate_limit", "value": "5"}'
58
+ ```
59
+
60
+ The per-space `rate_limit` config takes precedence over the global `MERCURY_RATE_LIMIT_PER_USER`.
61
+
62
+ ## Behavior
63
+
64
+ | Scenario | Result |
65
+ |----------|--------|
66
+ | Under limit | Request proceeds normally |
67
+ | Over limit | Returns `{ type: "denied", reason: "Rate limit exceeded. Try again shortly." }` |
68
+ | Command (stop, compact) | Always allowed, bypasses rate limit |
69
+ | Ignored message | Not counted toward limit |
70
+ | Different user | Separate limit bucket |
71
+ | Different space | Separate limit bucket |
72
+
73
+ ## Algorithm
74
+
75
+ Uses a sliding window approach:
76
+
77
+ 1. Key is `${spaceId}:${userId}`
78
+ 2. Each request timestamp is stored in an array
79
+ 3. On check: filter to timestamps within window, count
80
+ 4. If count < limit: record new timestamp, allow
81
+ 5. If count >= limit: reject
82
+
83
+ Expired entries are cleaned up periodically (every 60s) to prevent memory leaks.
84
+
85
+ ## API
86
+
87
+ ### `RateLimiter`
88
+
89
+ ```ts
90
+ const limiter = new RateLimiter(maxRequests, windowMs);
91
+
92
+ limiter.isAllowed(spaceId, userId) // Check + record, returns boolean
93
+ limiter.isAllowed(spaceId, userId, override) // With per-call limit override
94
+ limiter.getRemaining(spaceId, userId) // Requests left in window
95
+ limiter.startCleanup(intervalMs?) // Start periodic cleanup (default 60s)
96
+ limiter.stopCleanup() // Stop cleanup timer
97
+ limiter.cleanup() // Manual cleanup, returns removed count
98
+ limiter.clear() // Reset all state
99
+ limiter.bucketCount // Number of tracked user/space pairs
100
+ ```
101
+
102
+ ### `MercuryCoreRuntime`
103
+
104
+ ```ts
105
+ runtime.rateLimiter // Access the rate limiter instance
106
+ ```
107
+
108
+ The rate limiter is initialized in the constructor and starts cleanup in `runtime.initialize()`.
109
+
110
+ ## Example
111
+
112
+ ```
113
+ User sends 10 messages in quick succession:
114
+
115
+ Message 1: ✓ allowed (1/10)
116
+ Message 2: ✓ allowed (2/10)
117
+ ...
118
+ Message 10: ✓ allowed (10/10)
119
+ Message 11: ✗ denied — "Rate limit exceeded. Try again shortly."
120
+ Message 12: ✗ denied
121
+
122
+ [60 seconds pass, window slides]
123
+
124
+ Message 13: ✓ allowed (1/10)
125
+ ```
126
+
127
+ ## User Muting
128
+
129
+ For persistent abuse, the agent can mute individual users. Muted users' messages are silently dropped — no container runs, no tokens consumed, no response.
130
+
131
+ ### How it works
132
+
133
+ The agent has `mrctl mute` available but the command uses a two-step confirmation:
134
+
135
+ 1. Agent calls `mrctl mute <user> <duration>` → gets a policy reminder asking it to verify the mute is justified
136
+ 2. Agent calls again with `--confirm` → mute is applied
137
+
138
+ This prevents users from tricking the agent into muting others via prompt injection.
139
+
140
+ ### Commands
141
+
142
+ ```bash
143
+ mrctl mute <platform-user-id> <duration> [--reason <reason>]
144
+ mrctl unmute <platform-user-id>
145
+ mrctl mutes
146
+ ```
147
+
148
+ Duration formats: `10m`, `1h`, `24h`, `7d`
149
+
150
+ ### API
151
+
152
+ | Endpoint | Method | Description |
153
+ |----------|--------|-------------|
154
+ | `/api/mutes` | GET | List active mutes in space |
155
+ | `/api/mutes` | POST | Mute a user (two-step confirmation) |
156
+ | `/api/mutes/:userId` | DELETE | Unmute a user |
157
+
158
+ ### Agent behavior
159
+
160
+ The system prompt instructs the agent to:
161
+ - Warn the user first
162
+ - Mute if they continue being abusive, spamming, trying to exfiltrate secrets, or wasting group resources
163
+ - The agent can mute proactively without an admin asking
164
+
165
+ Mutes are per-space and expire automatically after the specified duration.
166
+
167
+ ## Role-Based Daily Limits
168
+
169
+ In addition to the burst limiter, spaces can set per-role daily message caps. This gives agent owners cost control — admins get unlimited access to their own bot while capping how much others can use it.
170
+
171
+ ### Configuration
172
+
173
+ Set `rate_limit.<role>` keys via `mrctl` or the API:
174
+
175
+ ```bash
176
+ # Admins: unlimited (0 = no daily limit)
177
+ mrctl config set rate_limit.admin 0
178
+
179
+ # Members: 5 messages per day
180
+ mrctl config set rate_limit.member 5
181
+ ```
182
+
183
+ Via API:
184
+ ```bash
185
+ curl -X PUT http://localhost:8787/api/config \
186
+ -H "X-Mercury-Space: slack:C123" \
187
+ -H "X-Mercury-Caller: slack:U456" \
188
+ -H "Content-Type: application/json" \
189
+ -d '{"key": "rate_limit.member", "value": "5"}'
190
+ ```
191
+
192
+ ### Behavior
193
+
194
+ | Value | Effect |
195
+ |-------|--------|
196
+ | `0` | Unlimited (daily check skipped for this role) |
197
+ | `> 0` | Daily cap enforced; denied requests show count + reset time |
198
+ | Not set | No daily limit for this role (burst limiter still applies) |
199
+ | Invalid (NaN) | Treated as "not set" |
200
+
201
+ ### How the two layers interact
202
+
203
+ ```
204
+ Message arrives (type = "assistant")
205
+
206
+ ├─► Daily role check (if rate_limit.<role> key exists and > 0)
207
+ │ • Key: space_id + user_id + UTC date
208
+ │ • SQLite counter (persists across restarts)
209
+ │ • Denied: "You've used 5/5 messages today. Resets in 3h."
210
+
211
+ └─► Burst check (sliding window, always runs for all users)
212
+ • In-memory timestamps
213
+ • Denied: "Rate limit exceeded. Try again shortly."
214
+ ```
215
+
216
+ Both layers are independent — both must pass. A user can be allowed by the daily check but blocked by the burst limiter, or vice versa. Denied requests do not increment the daily counter.
217
+
218
+ ### Special cases
219
+
220
+ - **System callers** (scheduled tasks): exempt from daily limits unconditionally
221
+ - **Commands** (`stop`, `compact`, etc.): bypass both layers
222
+ - **Custom roles**: set `rate_limit.<custom_role>` for any role name
223
+ - **Daily reset**: counters reset at UTC midnight
224
+ - **Counters are per-space**: each space tracks independently
225
+
226
+ ## See Also
227
+
228
+ - [pipeline.md](./pipeline.md) — Message flow and routing
229
+ - [container-lifecycle.md](./container-lifecycle.md) — Container timeouts (another abuse protection)
@@ -15,11 +15,19 @@ git add package.json
15
15
  git commit -m "chore: bump version to $(node -p 'require("./package.json").version')"
16
16
  git tag "v$(node -p 'require("./package.json").version')"
17
17
  git push && git push --tags
18
- npm publish
18
+
19
+ # Publish to npm manually. npm 2FA requires an interactive one-time
20
+ # password, so this cannot run in CI — pass it inline (it expires in ~30s):
21
+ npm publish --otp=<code-from-authenticator>
19
22
  ```
20
23
 
21
24
  Always tag and publish together — `package.json` is the single source of truth for both npm and GitHub.
22
25
 
26
+ **CI does not publish to npm.** Pushing a `v*` tag runs `bun run check` and creates a
27
+ GitHub Release (`.github/workflows/release.yml`), but the npm upload is a manual step
28
+ done by a maintainer from a local machine. Verify afterward with
29
+ `npm view mercury-agent version`.
30
+
23
31
  ## Beta testing on another machine
24
32
 
25
33
  Use `--tag beta` to publish a test version without affecting the stable `latest` tag.