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,517 +1,517 @@
1
- /**
2
- * Mercury Extension System — Type Definitions
3
- *
4
- * All types for the extension API, events, metadata, and supporting structures.
5
- * No runtime code — types only.
6
- */
7
-
8
- import type { ContainerError } from "../agent/container-error.js";
9
- import type { ModelCapabilityKey } from "../agent/model-capabilities.js";
10
- import type { AppConfig } from "../config.js";
11
- import type { Logger } from "../logger.js";
12
- import type { Db } from "../storage/db.js";
13
- import type { EgressFile, MessageAttachment } from "../types.js";
14
-
15
- // ---------------------------------------------------------------------------
16
- // Extension context — passed to event handlers and job runners
17
- // ---------------------------------------------------------------------------
18
-
19
- /** Context available to extension hooks and jobs at runtime. */
20
- export interface MercuryExtensionContext {
21
- /** Database access. */
22
- readonly db: Db;
23
- /** Mercury configuration. */
24
- readonly config: AppConfig;
25
- /** Logger scoped to the extension. */
26
- readonly log: Logger;
27
- /**
28
- * True if the caller has the permission in this space (built-in or extension-registered).
29
- * Used by extensions in hooks to mirror container RBAC.
30
- */
31
- hasCallerPermission(
32
- spaceId: string,
33
- callerId: string,
34
- permission: string,
35
- ): boolean;
36
- }
37
-
38
- // ---------------------------------------------------------------------------
39
- // Events
40
- // ---------------------------------------------------------------------------
41
-
42
- /** All lifecycle events an extension can subscribe to. */
43
- export interface MercuryEvents {
44
- /** Fired after all extensions are loaded and the runtime is ready. */
45
- startup: StartupEvent;
46
- /** Fired when Mercury is shutting down. */
47
- shutdown: ShutdownEvent;
48
- /** Fired when a space workspace directory is created or ensured. */
49
- workspace_init: WorkspaceInitEvent;
50
- /** Fired just before a container is spawned for a message. */
51
- before_container: BeforeContainerEvent;
52
- /** Fired after a container finishes (success or error). */
53
- after_container: AfterContainerEvent;
54
- }
55
-
56
- export type StartupEvent = Record<string, never>;
57
-
58
- export type ShutdownEvent = Record<string, never>;
59
-
60
- export interface WorkspaceInitEvent {
61
- /** The space this workspace belongs to. */
62
- spaceId: string;
63
- /** Absolute path to the workspace directory. */
64
- workspace: string;
65
- /** Container-relative path to the workspace (e.g. /spaces/main). */
66
- containerWorkspace: string;
67
- }
68
-
69
- export interface BeforeContainerEvent {
70
- /** The space the message belongs to. */
71
- spaceId: string;
72
- /** The user's prompt. */
73
- prompt: string;
74
- /** Platform-specific caller identifier. */
75
- callerId: string;
76
- /** Absolute path to the space workspace. */
77
- workspace: string;
78
- /** Container-relative path to the workspace (e.g. /spaces/main). */
79
- containerWorkspace: string;
80
- /** Incoming attachments (e.g. voice, images), if any. */
81
- attachments?: MessageAttachment[];
82
- }
83
-
84
- export interface AfterContainerEvent {
85
- /** The space the message belongs to. */
86
- spaceId: string;
87
- /** Absolute path to the space workspace on the host. */
88
- workspace: string;
89
- /** Platform user id for this turn (same as container `CALLER_ID`). */
90
- callerId: string;
91
- /** User prompt for this turn (includes any `promptAppend` from `before_container`). */
92
- prompt: string;
93
- /** The agent's reply (empty string on error). */
94
- reply: string;
95
- /** How long the container ran, in milliseconds. */
96
- durationMs: number;
97
- /** Present if the container failed. */
98
- error?: ContainerError;
99
- }
100
-
101
- // ---------------------------------------------------------------------------
102
- // Event return types — mutations hooks can apply
103
- // ---------------------------------------------------------------------------
104
-
105
- /**
106
- * Return value from a `before_container` handler.
107
- * All fields are optional — return only what you want to mutate.
108
- */
109
- export interface BeforeContainerResult {
110
- /** Extra text appended to the system prompt inside the container. */
111
- systemPrompt?: string;
112
- /** Text appended to the user prompt (newline-joined across handlers). */
113
- promptAppend?: string;
114
- /** Extra environment variables passed to the container. */
115
- env?: Record<string, string>;
116
- /** If set, blocks the container from running entirely. */
117
- block?: { reason: string };
118
- }
119
-
120
- /**
121
- * Return value from an `after_container` handler.
122
- * All fields are optional — return only what you want to mutate.
123
- */
124
- export interface AfterContainerResult {
125
- /** Replace the agent's reply. */
126
- reply?: string;
127
- /** If true, suppress the reply (don't send it to the chat). */
128
- suppress?: boolean;
129
- /**
130
- * Extra egress files to attach (e.g. host-generated audio). Appended after
131
- * container outbox files; order is concatenation of handlers in registration order.
132
- */
133
- files?: EgressFile[];
134
- }
135
-
136
- /** Maps event names to their allowed return types. */
137
- export type EventResult<E extends keyof MercuryEvents> =
138
- E extends "before_container"
139
- ? BeforeContainerResult | undefined
140
- : E extends "after_container"
141
- ? AfterContainerResult | undefined
142
- : undefined;
143
-
144
- /** A typed event handler for a specific event. */
145
- export type EventHandler<E extends keyof MercuryEvents> = (
146
- event: MercuryEvents[E],
147
- ctx: MercuryExtensionContext,
148
- ) => Promise<EventResult<E>>;
149
-
150
- // ---------------------------------------------------------------------------
151
- // Jobs
152
- // ---------------------------------------------------------------------------
153
-
154
- /** Definition for a background job registered by an extension. */
155
- export interface JobDef {
156
- /** Run on a fixed interval (milliseconds). Mutually exclusive with `cron`. */
157
- interval?: number;
158
- /** Run on a cron schedule (5-field expression). Mutually exclusive with `interval`. */
159
- cron?: string;
160
- /** The function to execute on each tick. */
161
- run: (ctx: MercuryExtensionContext) => Promise<void>;
162
- }
163
-
164
- // ---------------------------------------------------------------------------
165
- // Config
166
- // ---------------------------------------------------------------------------
167
-
168
- /** Definition for a per-space config key registered by an extension. */
169
- export interface ConfigDef {
170
- /** Human-readable description shown in `mrctl config get`. */
171
- description: string;
172
- /** Default value when not explicitly set. */
173
- default: string;
174
- /** Optional validator — return true if value is acceptable. */
175
- validate?: (value: string) => boolean;
176
- }
177
-
178
- // ---------------------------------------------------------------------------
179
- // Widgets
180
- // ---------------------------------------------------------------------------
181
-
182
- /** Definition for a dashboard widget registered by an extension. */
183
- export interface WidgetDef {
184
- /** Display label shown in the dashboard. */
185
- label: string;
186
- /** Render function returning an HTML fragment. */
187
- render: (ctx: MercuryExtensionContext) => string;
188
- }
189
-
190
- // ---------------------------------------------------------------------------
191
- // Capabilities — host-side broker actions invoked from the container
192
- // ---------------------------------------------------------------------------
193
-
194
- /** A single broker request, dispatched from `POST /api/capability/:name/:action`. */
195
- export interface CapabilityRequest {
196
- /** Capability name (matches the required permission). */
197
- name: string;
198
- /** Sub-action within the capability (e.g. "book", "cancel"). */
199
- action: string;
200
- /**
201
- * The authoritative, token-derived caller id. Trustworthy for ownership
202
- * checks — NOT a container-supplied argument.
203
- */
204
- callerId: string;
205
- /** The space the call was made from. */
206
- spaceId: string;
207
- /** Parsed JSON request body (or null). */
208
- body: unknown;
209
- }
210
-
211
- /** Result returned by a capability handler. */
212
- export interface CapabilityResult {
213
- /** HTTP status to return (default 200). */
214
- status?: number;
215
- /** JSON-serializable response payload. */
216
- data: unknown;
217
- }
218
-
219
- /**
220
- * Host-side handler for a capability. Runs in the Mercury host process with the
221
- * full extension context (credentials in host storage stay on the host). The
222
- * container never sees secrets — only the returned `data`.
223
- */
224
- export type CapabilityHandler = (
225
- req: CapabilityRequest,
226
- ctx: MercuryExtensionContext,
227
- ) => Promise<CapabilityResult>;
228
-
229
- // ---------------------------------------------------------------------------
230
- // Store
231
- // ---------------------------------------------------------------------------
232
-
233
- /** Scoped key-value store for extension-private persistent state. */
234
- export interface ExtensionStore {
235
- /** Get a value by key, or null if not set. */
236
- get(key: string): string | null;
237
- /** Set a key-value pair (upsert). */
238
- set(key: string, value: string): void;
239
- /** Delete a key. Returns true if the key existed. */
240
- delete(key: string): boolean;
241
- /** List all key-value pairs for this extension. */
242
- list(): Array<{ key: string; value: string }>;
243
- }
244
-
245
- // ---------------------------------------------------------------------------
246
- // Extension API — the object passed to each extension's setup function
247
- // ---------------------------------------------------------------------------
248
-
249
- /** The API surface available to extensions during setup. */
250
- export interface MercuryExtensionAPI {
251
- /** The extension's name (directory name). */
252
- readonly name: string;
253
-
254
- /**
255
- * Declare a CLI tool to install in the derived container image.
256
- * Can only be called once per extension.
257
- *
258
- * @example
259
- * mercury.cli({ name: "napkin", install: "bun add -g napkin-ai" });
260
- */
261
- cli(opts: CliDef): void;
262
-
263
- /**
264
- * Register this extension's permission and set which roles get it by default.
265
- * The permission name is the extension name. Can only be called once.
266
- *
267
- * @example
268
- * mercury.permission({ defaultRoles: ["admin", "member"] });
269
- */
270
- permission(opts: PermissionDef): void;
271
-
272
- /**
273
- * Declare an environment variable this extension needs.
274
- * Only injected into containers when the caller has permission for this extension.
275
- * Can be called multiple times for multiple env vars.
276
- *
277
- * @example
278
- * mercury.env({ from: "MERCURY_GH_TOKEN" }); // injected as GH_TOKEN
279
- * mercury.env({ from: "MERCURY_GH_TOKEN", as: "GITHUB_TOKEN" }); // custom name
280
- */
281
- env(def: EnvDef): void;
282
-
283
- /**
284
- * Register a skill directory containing a SKILL.md for agent discovery.
285
- * Path is relative to the extension directory.
286
- *
287
- * @example
288
- * mercury.skill("./skill");
289
- */
290
- skill(relativePath: string): void;
291
-
292
- /**
293
- * Declare capability requirements for this extension's skill / CLI workflows.
294
- * If no model leg in the chain satisfies all listed capabilities, the extension
295
- * skill is not installed and a startup warning is logged.
296
- *
297
- * @example
298
- * mercury.requires(["tools"]);
299
- */
300
- requires(capabilities: ModelCapabilityKey[]): void;
301
-
302
- /**
303
- * Subscribe to a lifecycle event.
304
- *
305
- * @example
306
- * mercury.on("workspace_init", async (event, ctx) => {
307
- * mkdirSync(join(event.workspace, "my-dir"), { recursive: true });
308
- * });
309
- */
310
- on<E extends keyof MercuryEvents>(event: E, handler: EventHandler<E>): void;
311
-
312
- /**
313
- * Register a background job that runs on the host.
314
- *
315
- * @example
316
- * mercury.job("cleanup", { interval: 3600_000, run: async (ctx) => { ... } });
317
- */
318
- job(name: string, def: JobDef): void;
319
-
320
- /**
321
- * Register a per-space config key. Namespaced to the extension automatically.
322
- *
323
- * @example
324
- * mercury.config("enabled", { description: "Enable for this group", default: "true" });
325
- * // Registers as "napkin.enabled" in the DB
326
- */
327
- config(key: string, def: ConfigDef): void;
328
-
329
- /**
330
- * Register a dashboard widget.
331
- *
332
- * @example
333
- * mercury.widget({ label: "Status", render: (ctx) => "<p>OK</p>" });
334
- */
335
- widget(def: WidgetDef): void;
336
-
337
- /**
338
- * Declare this extension as a personal service connection. Additive on top
339
- * of cli/env/skill; extensions that never call this are unaffected. Can
340
- * only be called once per extension.
341
- *
342
- * @example
343
- * mercury.connection({
344
- * displayName: "Google Workspace",
345
- * category: "workspace",
346
- * authType: "credentials-file",
347
- * credentialEnvVar: "MERCURY_GOOGLE_WORKSPACE_CLI_CREDENTIALS_FILE",
348
- * });
349
- */
350
- connection(def: ConnectionDef): void;
351
-
352
- /**
353
- * Register a host-side capability handler, invoked from the container via
354
- * `mrctl capability <name> <action> <json>` → `POST /api/capability/:name/:action`.
355
- * The caller must hold the `<name>` permission. Credentials used by the
356
- * handler stay on the host and never enter the agent container.
357
- *
358
- * @example
359
- * mercury.capability("barber", async (req, ctx) => {
360
- * if (req.action === "book") return { data: await book(req.callerId, req.body) };
361
- * return { status: 400, data: { error: "unknown action" } };
362
- * });
363
- */
364
- capability(name: string, handler: CapabilityHandler): void;
365
-
366
- /** Scoped key-value store for persistent extension state. */
367
- readonly store: ExtensionStore;
368
- }
369
-
370
- // ---------------------------------------------------------------------------
371
- // CLI + Permission definitions
372
- // ---------------------------------------------------------------------------
373
-
374
- /** Declaration for a CLI tool to install in the container image. */
375
- export interface CliDef {
376
- /** CLI binary name (should match the extension name). */
377
- name: string;
378
- /** Shell command to install the CLI (runs as a Dockerfile RUN step). */
379
- install: string;
380
- /**
381
- * Absolute path to a local script to copy into `/usr/local/bin/{name}`.
382
- * Set by the extension loader from the extension's directory.
383
- */
384
- bin?: string;
385
- }
386
-
387
- /** Permission configuration for an extension. */
388
- export interface PermissionDef {
389
- /** Roles that should have this permission by default. */
390
- defaultRoles: string[];
391
- }
392
-
393
- /** Environment variable declaration for an extension. */
394
- export interface EnvDef {
395
- /** Env var name as it appears in .env (e.g. "MERCURY_GH_TOKEN"). */
396
- from: string;
397
- /** Env var name inside the container (e.g. "GH_TOKEN"). Defaults to `from` with MERCURY_ prefix stripped. */
398
- as?: string;
399
- }
400
-
401
- // ---------------------------------------------------------------------------
402
- // Connection metadata — first-class personal service connections
403
- // ---------------------------------------------------------------------------
404
-
405
- /** Runtime status of a personal service connection. */
406
- export type ConnectionStatus =
407
- | "connected"
408
- | "needs-reauth"
409
- | "broken"
410
- | "unknown";
411
-
412
- /** Closed category taxonomy for connections (v1 — tags deferred). */
413
- export type ConnectionCategory =
414
- | "email"
415
- | "drive"
416
- | "calendar"
417
- | "finance"
418
- | "messaging"
419
- | "docs"
420
- | "workspace"
421
- | "other";
422
-
423
- /** How the connection authenticates. */
424
- export type ConnectionAuthType =
425
- | "oauth2"
426
- | "apikey"
427
- | "app-password"
428
- | "credentials-file"
429
- | "form"
430
- | "custom";
431
-
432
- /** Result of an on-request `statusCheck` probe. */
433
- export interface ConnectionStatusResult {
434
- status: ConnectionStatus;
435
- /** Optional one-line explanation surfaced to the UI (e.g. "token expired 2h ago"). */
436
- detail?: string;
437
- }
438
-
439
- /**
440
- * Declaration for a personal service connection registered via
441
- * `mercury.connection()`. At least one of `credentialEnvVar` or `statusCheck`
442
- * must be set — enforced at load.
443
- */
444
- export interface ConnectionDef {
445
- /** User-facing name (e.g. "Google Workspace"). */
446
- displayName: string;
447
- /** Optional icon URL surfaced to the console. */
448
- iconUrl?: string;
449
- /** Category used for grouping in the UI. */
450
- category: ConnectionCategory;
451
- /** Auth mechanism used by the upstream service. */
452
- authType: ConnectionAuthType;
453
- /**
454
- * Optional. If set, MUST match one of the env var names declared via
455
- * `mercury.env({ from })`. Validated at load. Used for the default
456
- * presence-check status when `statusCheck` is absent. Extensions that store
457
- * credentials in `extension_state` (e.g. tradestation OAuth) omit this and
458
- * rely on `statusCheck`.
459
- */
460
- credentialEnvVar?: string;
461
- /** Optional OAuth-style scope list (informational, surfaced to the UI). */
462
- scopes?: string[];
463
- /**
464
- * Optional probe. Runs on the host with the full extension context. The
465
- * caller enforces a 5-second timeout. Must be side-effect free.
466
- */
467
- statusCheck?: (
468
- ctx: MercuryExtensionContext,
469
- ) => Promise<ConnectionStatusResult>;
470
- /**
471
- * If true, this connection accesses personal/sensitive data (email, finance,
472
- * authenticated browser). In group-linked spaces, the runtime guard requires
473
- * explicit admin enablement and per-request confirmation before proceeding.
474
- */
475
- sensitive?: boolean;
476
- }
477
-
478
- // ---------------------------------------------------------------------------
479
- // Extension metadata — collected after running the setup function
480
- // ---------------------------------------------------------------------------
481
-
482
- /** Fully resolved metadata for a loaded extension. */
483
- export interface ExtensionMeta {
484
- /** Extension name (directory name). */
485
- name: string;
486
- /** Absolute path to the extension directory. */
487
- dir: string;
488
- /** CLI declarations (may be empty). */
489
- clis: CliDef[];
490
- /** Permission configuration, if any. */
491
- permission?: PermissionDef;
492
- /** If set, skill install requires these capabilities on at least one model chain leg. */
493
- requires?: ModelCapabilityKey[];
494
- /** Absolute path to the skill directory, if declared. */
495
- skillDir?: string;
496
- /** Event handlers keyed by event name. */
497
- hooks: Map<keyof MercuryEvents, EventHandler<keyof MercuryEvents>[]>;
498
- /** Background jobs keyed by job name. */
499
- jobs: Map<string, JobDef>;
500
- /** Config key definitions keyed by local key (not namespaced). */
501
- configs: Map<string, ConfigDef>;
502
- /** Dashboard widgets. */
503
- widgets: WidgetDef[];
504
- /** Host-side capability handlers, keyed by capability name. */
505
- capabilities: Map<string, CapabilityHandler>;
506
- /** Declared environment variables. */
507
- envVars: EnvDef[];
508
- /** Personal service connection metadata, if declared via `mercury.connection()`. */
509
- connection?: ConnectionDef;
510
- }
511
-
512
- // ---------------------------------------------------------------------------
513
- // Extension setup function signature
514
- // ---------------------------------------------------------------------------
515
-
516
- /** The default export every extension must provide. */
517
- export type ExtensionSetupFn = (api: MercuryExtensionAPI) => void;
1
+ /**
2
+ * Mercury Extension System — Type Definitions
3
+ *
4
+ * All types for the extension API, events, metadata, and supporting structures.
5
+ * No runtime code — types only.
6
+ */
7
+
8
+ import type { ContainerError } from "../agent/container-error.js";
9
+ import type { ModelCapabilityKey } from "../agent/model-capabilities.js";
10
+ import type { AppConfig } from "../config.js";
11
+ import type { Logger } from "../logger.js";
12
+ import type { Db } from "../storage/db.js";
13
+ import type { EgressFile, MessageAttachment } from "../types.js";
14
+
15
+ // ---------------------------------------------------------------------------
16
+ // Extension context — passed to event handlers and job runners
17
+ // ---------------------------------------------------------------------------
18
+
19
+ /** Context available to extension hooks and jobs at runtime. */
20
+ export interface MercuryExtensionContext {
21
+ /** Database access. */
22
+ readonly db: Db;
23
+ /** Mercury configuration. */
24
+ readonly config: AppConfig;
25
+ /** Logger scoped to the extension. */
26
+ readonly log: Logger;
27
+ /**
28
+ * True if the caller has the permission in this space (built-in or extension-registered).
29
+ * Used by extensions in hooks to mirror container RBAC.
30
+ */
31
+ hasCallerPermission(
32
+ spaceId: string,
33
+ callerId: string,
34
+ permission: string,
35
+ ): boolean;
36
+ }
37
+
38
+ // ---------------------------------------------------------------------------
39
+ // Events
40
+ // ---------------------------------------------------------------------------
41
+
42
+ /** All lifecycle events an extension can subscribe to. */
43
+ export interface MercuryEvents {
44
+ /** Fired after all extensions are loaded and the runtime is ready. */
45
+ startup: StartupEvent;
46
+ /** Fired when Mercury is shutting down. */
47
+ shutdown: ShutdownEvent;
48
+ /** Fired when a space workspace directory is created or ensured. */
49
+ workspace_init: WorkspaceInitEvent;
50
+ /** Fired just before a container is spawned for a message. */
51
+ before_container: BeforeContainerEvent;
52
+ /** Fired after a container finishes (success or error). */
53
+ after_container: AfterContainerEvent;
54
+ }
55
+
56
+ export type StartupEvent = Record<string, never>;
57
+
58
+ export type ShutdownEvent = Record<string, never>;
59
+
60
+ export interface WorkspaceInitEvent {
61
+ /** The space this workspace belongs to. */
62
+ spaceId: string;
63
+ /** Absolute path to the workspace directory. */
64
+ workspace: string;
65
+ /** Container-relative path to the workspace (e.g. /spaces/main). */
66
+ containerWorkspace: string;
67
+ }
68
+
69
+ export interface BeforeContainerEvent {
70
+ /** The space the message belongs to. */
71
+ spaceId: string;
72
+ /** The user's prompt. */
73
+ prompt: string;
74
+ /** Platform-specific caller identifier. */
75
+ callerId: string;
76
+ /** Absolute path to the space workspace. */
77
+ workspace: string;
78
+ /** Container-relative path to the workspace (e.g. /spaces/main). */
79
+ containerWorkspace: string;
80
+ /** Incoming attachments (e.g. voice, images), if any. */
81
+ attachments?: MessageAttachment[];
82
+ }
83
+
84
+ export interface AfterContainerEvent {
85
+ /** The space the message belongs to. */
86
+ spaceId: string;
87
+ /** Absolute path to the space workspace on the host. */
88
+ workspace: string;
89
+ /** Platform user id for this turn (same as container `CALLER_ID`). */
90
+ callerId: string;
91
+ /** User prompt for this turn (includes any `promptAppend` from `before_container`). */
92
+ prompt: string;
93
+ /** The agent's reply (empty string on error). */
94
+ reply: string;
95
+ /** How long the container ran, in milliseconds. */
96
+ durationMs: number;
97
+ /** Present if the container failed. */
98
+ error?: ContainerError;
99
+ }
100
+
101
+ // ---------------------------------------------------------------------------
102
+ // Event return types — mutations hooks can apply
103
+ // ---------------------------------------------------------------------------
104
+
105
+ /**
106
+ * Return value from a `before_container` handler.
107
+ * All fields are optional — return only what you want to mutate.
108
+ */
109
+ export interface BeforeContainerResult {
110
+ /** Extra text appended to the system prompt inside the container. */
111
+ systemPrompt?: string;
112
+ /** Text appended to the user prompt (newline-joined across handlers). */
113
+ promptAppend?: string;
114
+ /** Extra environment variables passed to the container. */
115
+ env?: Record<string, string>;
116
+ /** If set, blocks the container from running entirely. */
117
+ block?: { reason: string };
118
+ }
119
+
120
+ /**
121
+ * Return value from an `after_container` handler.
122
+ * All fields are optional — return only what you want to mutate.
123
+ */
124
+ export interface AfterContainerResult {
125
+ /** Replace the agent's reply. */
126
+ reply?: string;
127
+ /** If true, suppress the reply (don't send it to the chat). */
128
+ suppress?: boolean;
129
+ /**
130
+ * Extra egress files to attach (e.g. host-generated audio). Appended after
131
+ * container outbox files; order is concatenation of handlers in registration order.
132
+ */
133
+ files?: EgressFile[];
134
+ }
135
+
136
+ /** Maps event names to their allowed return types. */
137
+ export type EventResult<E extends keyof MercuryEvents> =
138
+ E extends "before_container"
139
+ ? BeforeContainerResult | undefined
140
+ : E extends "after_container"
141
+ ? AfterContainerResult | undefined
142
+ : undefined;
143
+
144
+ /** A typed event handler for a specific event. */
145
+ export type EventHandler<E extends keyof MercuryEvents> = (
146
+ event: MercuryEvents[E],
147
+ ctx: MercuryExtensionContext,
148
+ ) => Promise<EventResult<E>>;
149
+
150
+ // ---------------------------------------------------------------------------
151
+ // Jobs
152
+ // ---------------------------------------------------------------------------
153
+
154
+ /** Definition for a background job registered by an extension. */
155
+ export interface JobDef {
156
+ /** Run on a fixed interval (milliseconds). Mutually exclusive with `cron`. */
157
+ interval?: number;
158
+ /** Run on a cron schedule (5-field expression). Mutually exclusive with `interval`. */
159
+ cron?: string;
160
+ /** The function to execute on each tick. */
161
+ run: (ctx: MercuryExtensionContext) => Promise<void>;
162
+ }
163
+
164
+ // ---------------------------------------------------------------------------
165
+ // Config
166
+ // ---------------------------------------------------------------------------
167
+
168
+ /** Definition for a per-space config key registered by an extension. */
169
+ export interface ConfigDef {
170
+ /** Human-readable description shown in `mrctl config get`. */
171
+ description: string;
172
+ /** Default value when not explicitly set. */
173
+ default: string;
174
+ /** Optional validator — return true if value is acceptable. */
175
+ validate?: (value: string) => boolean;
176
+ }
177
+
178
+ // ---------------------------------------------------------------------------
179
+ // Widgets
180
+ // ---------------------------------------------------------------------------
181
+
182
+ /** Definition for a dashboard widget registered by an extension. */
183
+ export interface WidgetDef {
184
+ /** Display label shown in the dashboard. */
185
+ label: string;
186
+ /** Render function returning an HTML fragment. */
187
+ render: (ctx: MercuryExtensionContext) => string;
188
+ }
189
+
190
+ // ---------------------------------------------------------------------------
191
+ // Capabilities — host-side broker actions invoked from the container
192
+ // ---------------------------------------------------------------------------
193
+
194
+ /** A single broker request, dispatched from `POST /api/capability/:name/:action`. */
195
+ export interface CapabilityRequest {
196
+ /** Capability name (matches the required permission). */
197
+ name: string;
198
+ /** Sub-action within the capability (e.g. "book", "cancel"). */
199
+ action: string;
200
+ /**
201
+ * The authoritative, token-derived caller id. Trustworthy for ownership
202
+ * checks — NOT a container-supplied argument.
203
+ */
204
+ callerId: string;
205
+ /** The space the call was made from. */
206
+ spaceId: string;
207
+ /** Parsed JSON request body (or null). */
208
+ body: unknown;
209
+ }
210
+
211
+ /** Result returned by a capability handler. */
212
+ export interface CapabilityResult {
213
+ /** HTTP status to return (default 200). */
214
+ status?: number;
215
+ /** JSON-serializable response payload. */
216
+ data: unknown;
217
+ }
218
+
219
+ /**
220
+ * Host-side handler for a capability. Runs in the Mercury host process with the
221
+ * full extension context (credentials in host storage stay on the host). The
222
+ * container never sees secrets — only the returned `data`.
223
+ */
224
+ export type CapabilityHandler = (
225
+ req: CapabilityRequest,
226
+ ctx: MercuryExtensionContext,
227
+ ) => Promise<CapabilityResult>;
228
+
229
+ // ---------------------------------------------------------------------------
230
+ // Store
231
+ // ---------------------------------------------------------------------------
232
+
233
+ /** Scoped key-value store for extension-private persistent state. */
234
+ export interface ExtensionStore {
235
+ /** Get a value by key, or null if not set. */
236
+ get(key: string): string | null;
237
+ /** Set a key-value pair (upsert). */
238
+ set(key: string, value: string): void;
239
+ /** Delete a key. Returns true if the key existed. */
240
+ delete(key: string): boolean;
241
+ /** List all key-value pairs for this extension. */
242
+ list(): Array<{ key: string; value: string }>;
243
+ }
244
+
245
+ // ---------------------------------------------------------------------------
246
+ // Extension API — the object passed to each extension's setup function
247
+ // ---------------------------------------------------------------------------
248
+
249
+ /** The API surface available to extensions during setup. */
250
+ export interface MercuryExtensionAPI {
251
+ /** The extension's name (directory name). */
252
+ readonly name: string;
253
+
254
+ /**
255
+ * Declare a CLI tool to install in the derived container image.
256
+ * Can only be called once per extension.
257
+ *
258
+ * @example
259
+ * mercury.cli({ name: "napkin", install: "bun add -g napkin-ai" });
260
+ */
261
+ cli(opts: CliDef): void;
262
+
263
+ /**
264
+ * Register this extension's permission and set which roles get it by default.
265
+ * The permission name is the extension name. Can only be called once.
266
+ *
267
+ * @example
268
+ * mercury.permission({ defaultRoles: ["admin", "member"] });
269
+ */
270
+ permission(opts: PermissionDef): void;
271
+
272
+ /**
273
+ * Declare an environment variable this extension needs.
274
+ * Only injected into containers when the caller has permission for this extension.
275
+ * Can be called multiple times for multiple env vars.
276
+ *
277
+ * @example
278
+ * mercury.env({ from: "MERCURY_GH_TOKEN" }); // injected as GH_TOKEN
279
+ * mercury.env({ from: "MERCURY_GH_TOKEN", as: "GITHUB_TOKEN" }); // custom name
280
+ */
281
+ env(def: EnvDef): void;
282
+
283
+ /**
284
+ * Register a skill directory containing a SKILL.md for agent discovery.
285
+ * Path is relative to the extension directory.
286
+ *
287
+ * @example
288
+ * mercury.skill("./skill");
289
+ */
290
+ skill(relativePath: string): void;
291
+
292
+ /**
293
+ * Declare capability requirements for this extension's skill / CLI workflows.
294
+ * If no model leg in the chain satisfies all listed capabilities, the extension
295
+ * skill is not installed and a startup warning is logged.
296
+ *
297
+ * @example
298
+ * mercury.requires(["tools"]);
299
+ */
300
+ requires(capabilities: ModelCapabilityKey[]): void;
301
+
302
+ /**
303
+ * Subscribe to a lifecycle event.
304
+ *
305
+ * @example
306
+ * mercury.on("workspace_init", async (event, ctx) => {
307
+ * mkdirSync(join(event.workspace, "my-dir"), { recursive: true });
308
+ * });
309
+ */
310
+ on<E extends keyof MercuryEvents>(event: E, handler: EventHandler<E>): void;
311
+
312
+ /**
313
+ * Register a background job that runs on the host.
314
+ *
315
+ * @example
316
+ * mercury.job("cleanup", { interval: 3600_000, run: async (ctx) => { ... } });
317
+ */
318
+ job(name: string, def: JobDef): void;
319
+
320
+ /**
321
+ * Register a per-space config key. Namespaced to the extension automatically.
322
+ *
323
+ * @example
324
+ * mercury.config("enabled", { description: "Enable for this group", default: "true" });
325
+ * // Registers as "napkin.enabled" in the DB
326
+ */
327
+ config(key: string, def: ConfigDef): void;
328
+
329
+ /**
330
+ * Register a dashboard widget.
331
+ *
332
+ * @example
333
+ * mercury.widget({ label: "Status", render: (ctx) => "<p>OK</p>" });
334
+ */
335
+ widget(def: WidgetDef): void;
336
+
337
+ /**
338
+ * Declare this extension as a personal service connection. Additive on top
339
+ * of cli/env/skill; extensions that never call this are unaffected. Can
340
+ * only be called once per extension.
341
+ *
342
+ * @example
343
+ * mercury.connection({
344
+ * displayName: "Google Workspace",
345
+ * category: "workspace",
346
+ * authType: "credentials-file",
347
+ * credentialEnvVar: "MERCURY_GOOGLE_WORKSPACE_CLI_CREDENTIALS_FILE",
348
+ * });
349
+ */
350
+ connection(def: ConnectionDef): void;
351
+
352
+ /**
353
+ * Register a host-side capability handler, invoked from the container via
354
+ * `mrctl capability <name> <action> <json>` → `POST /api/capability/:name/:action`.
355
+ * The caller must hold the `<name>` permission. Credentials used by the
356
+ * handler stay on the host and never enter the agent container.
357
+ *
358
+ * @example
359
+ * mercury.capability("barber", async (req, ctx) => {
360
+ * if (req.action === "book") return { data: await book(req.callerId, req.body) };
361
+ * return { status: 400, data: { error: "unknown action" } };
362
+ * });
363
+ */
364
+ capability(name: string, handler: CapabilityHandler): void;
365
+
366
+ /** Scoped key-value store for persistent extension state. */
367
+ readonly store: ExtensionStore;
368
+ }
369
+
370
+ // ---------------------------------------------------------------------------
371
+ // CLI + Permission definitions
372
+ // ---------------------------------------------------------------------------
373
+
374
+ /** Declaration for a CLI tool to install in the container image. */
375
+ export interface CliDef {
376
+ /** CLI binary name (should match the extension name). */
377
+ name: string;
378
+ /** Shell command to install the CLI (runs as a Dockerfile RUN step). */
379
+ install: string;
380
+ /**
381
+ * Absolute path to a local script to copy into `/usr/local/bin/{name}`.
382
+ * Set by the extension loader from the extension's directory.
383
+ */
384
+ bin?: string;
385
+ }
386
+
387
+ /** Permission configuration for an extension. */
388
+ export interface PermissionDef {
389
+ /** Roles that should have this permission by default. */
390
+ defaultRoles: string[];
391
+ }
392
+
393
+ /** Environment variable declaration for an extension. */
394
+ export interface EnvDef {
395
+ /** Env var name as it appears in .env (e.g. "MERCURY_GH_TOKEN"). */
396
+ from: string;
397
+ /** Env var name inside the container (e.g. "GH_TOKEN"). Defaults to `from` with MERCURY_ prefix stripped. */
398
+ as?: string;
399
+ }
400
+
401
+ // ---------------------------------------------------------------------------
402
+ // Connection metadata — first-class personal service connections
403
+ // ---------------------------------------------------------------------------
404
+
405
+ /** Runtime status of a personal service connection. */
406
+ export type ConnectionStatus =
407
+ | "connected"
408
+ | "needs-reauth"
409
+ | "broken"
410
+ | "unknown";
411
+
412
+ /** Closed category taxonomy for connections (v1 — tags deferred). */
413
+ export type ConnectionCategory =
414
+ | "email"
415
+ | "drive"
416
+ | "calendar"
417
+ | "finance"
418
+ | "messaging"
419
+ | "docs"
420
+ | "workspace"
421
+ | "other";
422
+
423
+ /** How the connection authenticates. */
424
+ export type ConnectionAuthType =
425
+ | "oauth2"
426
+ | "apikey"
427
+ | "app-password"
428
+ | "credentials-file"
429
+ | "form"
430
+ | "custom";
431
+
432
+ /** Result of an on-request `statusCheck` probe. */
433
+ export interface ConnectionStatusResult {
434
+ status: ConnectionStatus;
435
+ /** Optional one-line explanation surfaced to the UI (e.g. "token expired 2h ago"). */
436
+ detail?: string;
437
+ }
438
+
439
+ /**
440
+ * Declaration for a personal service connection registered via
441
+ * `mercury.connection()`. At least one of `credentialEnvVar` or `statusCheck`
442
+ * must be set — enforced at load.
443
+ */
444
+ export interface ConnectionDef {
445
+ /** User-facing name (e.g. "Google Workspace"). */
446
+ displayName: string;
447
+ /** Optional icon URL surfaced to the console. */
448
+ iconUrl?: string;
449
+ /** Category used for grouping in the UI. */
450
+ category: ConnectionCategory;
451
+ /** Auth mechanism used by the upstream service. */
452
+ authType: ConnectionAuthType;
453
+ /**
454
+ * Optional. If set, MUST match one of the env var names declared via
455
+ * `mercury.env({ from })`. Validated at load. Used for the default
456
+ * presence-check status when `statusCheck` is absent. Extensions that store
457
+ * credentials in `extension_state` (e.g. tradestation OAuth) omit this and
458
+ * rely on `statusCheck`.
459
+ */
460
+ credentialEnvVar?: string;
461
+ /** Optional OAuth-style scope list (informational, surfaced to the UI). */
462
+ scopes?: string[];
463
+ /**
464
+ * Optional probe. Runs on the host with the full extension context. The
465
+ * caller enforces a 5-second timeout. Must be side-effect free.
466
+ */
467
+ statusCheck?: (
468
+ ctx: MercuryExtensionContext,
469
+ ) => Promise<ConnectionStatusResult>;
470
+ /**
471
+ * If true, this connection accesses personal/sensitive data (email, finance,
472
+ * authenticated browser). In group-linked spaces, the runtime guard requires
473
+ * explicit admin enablement and per-request confirmation before proceeding.
474
+ */
475
+ sensitive?: boolean;
476
+ }
477
+
478
+ // ---------------------------------------------------------------------------
479
+ // Extension metadata — collected after running the setup function
480
+ // ---------------------------------------------------------------------------
481
+
482
+ /** Fully resolved metadata for a loaded extension. */
483
+ export interface ExtensionMeta {
484
+ /** Extension name (directory name). */
485
+ name: string;
486
+ /** Absolute path to the extension directory. */
487
+ dir: string;
488
+ /** CLI declarations (may be empty). */
489
+ clis: CliDef[];
490
+ /** Permission configuration, if any. */
491
+ permission?: PermissionDef;
492
+ /** If set, skill install requires these capabilities on at least one model chain leg. */
493
+ requires?: ModelCapabilityKey[];
494
+ /** Absolute path to the skill directory, if declared. */
495
+ skillDir?: string;
496
+ /** Event handlers keyed by event name. */
497
+ hooks: Map<keyof MercuryEvents, EventHandler<keyof MercuryEvents>[]>;
498
+ /** Background jobs keyed by job name. */
499
+ jobs: Map<string, JobDef>;
500
+ /** Config key definitions keyed by local key (not namespaced). */
501
+ configs: Map<string, ConfigDef>;
502
+ /** Dashboard widgets. */
503
+ widgets: WidgetDef[];
504
+ /** Host-side capability handlers, keyed by capability name. */
505
+ capabilities: Map<string, CapabilityHandler>;
506
+ /** Declared environment variables. */
507
+ envVars: EnvDef[];
508
+ /** Personal service connection metadata, if declared via `mercury.connection()`. */
509
+ connection?: ConnectionDef;
510
+ }
511
+
512
+ // ---------------------------------------------------------------------------
513
+ // Extension setup function signature
514
+ // ---------------------------------------------------------------------------
515
+
516
+ /** The default export every extension must provide. */
517
+ export type ExtensionSetupFn = (api: MercuryExtensionAPI) => void;