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.
- package/LICENSE +22 -0
- package/README.md +438 -0
- package/container/Dockerfile +127 -0
- package/container/Dockerfile.base +109 -0
- package/container/Dockerfile.power +17 -0
- package/container/agent-package.json +8 -0
- package/container/build.sh +54 -0
- package/docs/TODOS.md +147 -0
- package/docs/auth/dashboard.md +28 -0
- package/docs/auth/overview.md +109 -0
- package/docs/auth/whatsapp.md +173 -0
- package/docs/configuration.md +54 -0
- package/docs/container-lifecycle.md +349 -0
- package/docs/context-architecture.md +87 -0
- package/docs/deployment.md +199 -0
- package/docs/extensions.md +375 -0
- package/docs/graceful-shutdown.md +62 -0
- package/docs/kb-distillation.md +77 -0
- package/docs/media/overview.md +140 -0
- package/docs/media/whatsapp.md +171 -0
- package/docs/memory.md +137 -0
- package/docs/permissions.md +217 -0
- package/docs/pipeline.md +228 -0
- package/docs/prd-chat-memory.md +76 -0
- package/docs/prd-config-load.md +82 -0
- package/docs/rate-limiting.md +166 -0
- package/docs/scheduler.md +288 -0
- package/docs/setup-discord.md +100 -0
- package/docs/setup-slack.md +119 -0
- package/docs/setup-whatsapp.md +94 -0
- package/docs/subagents.md +166 -0
- package/docs/web-search.md +62 -0
- package/examples/extensions/README.md +12 -0
- package/examples/extensions/charts/index.ts +13 -0
- package/examples/extensions/charts/skill/SKILL.md +98 -0
- package/examples/extensions/gws/README.md +52 -0
- package/examples/extensions/gws/index.ts +106 -0
- package/examples/extensions/gws/skill/SKILL.md +57 -0
- package/examples/extensions/gws/skill/references/calendar.md +101 -0
- package/examples/extensions/gws/skill/references/docs.md +65 -0
- package/examples/extensions/gws/skill/references/drive.md +79 -0
- package/examples/extensions/gws/skill/references/gmail.md +85 -0
- package/examples/extensions/gws/skill/references/sheets.md +60 -0
- package/examples/extensions/napkin/index.ts +821 -0
- package/examples/extensions/napkin/prompts/consolidation-monthly.md +73 -0
- package/examples/extensions/napkin/prompts/consolidation-weekly.md +67 -0
- package/examples/extensions/napkin/prompts/kb-distillation.md +176 -0
- package/examples/extensions/napkin/skill/SKILL.md +728 -0
- package/examples/extensions/pdf/index.ts +23 -0
- package/examples/extensions/pdf/skill/LICENSE.txt +30 -0
- package/examples/extensions/pdf/skill/SKILL.md +314 -0
- package/examples/extensions/pdf/skill/forms.md +294 -0
- package/examples/extensions/pdf/skill/reference.md +612 -0
- package/examples/extensions/pdf/skill/scripts/check_bounding_boxes.py +65 -0
- package/examples/extensions/pdf/skill/scripts/check_fillable_fields.py +11 -0
- package/examples/extensions/pdf/skill/scripts/convert_pdf_to_images.py +33 -0
- package/examples/extensions/pdf/skill/scripts/create_validation_image.py +37 -0
- package/examples/extensions/pdf/skill/scripts/extract_form_field_info.py +122 -0
- package/examples/extensions/pdf/skill/scripts/extract_form_structure.py +115 -0
- package/examples/extensions/pdf/skill/scripts/fill_fillable_fields.py +98 -0
- package/examples/extensions/pdf/skill/scripts/fill_pdf_form_with_annotations.py +107 -0
- package/examples/extensions/permission-guard/index.ts +65 -0
- package/examples/extensions/pinchtab/index.ts +199 -0
- package/examples/extensions/pinchtab/lib/session-injector.ts +144 -0
- package/examples/extensions/pinchtab/skill/SKILL.md +224 -0
- package/examples/extensions/pinchtab/skill/TRUST.md +69 -0
- package/examples/extensions/pinchtab/skill/references/api.md +297 -0
- package/examples/extensions/pinchtab/skill/references/env.md +45 -0
- package/examples/extensions/pinchtab/skill/references/profiles.md +107 -0
- package/examples/extensions/tradestation/host/refresh.ts +102 -0
- package/examples/extensions/tradestation/index.ts +153 -0
- package/examples/extensions/tradestation/skill/SKILL.md +67 -0
- package/examples/extensions/tradestation/skill/scripts/ts-cli.ts +111 -0
- package/examples/extensions/voice-synth/index.ts +94 -0
- package/examples/extensions/voice-synth/skill/SKILL.md +38 -0
- package/examples/extensions/voice-transcribe/index.ts +381 -0
- package/examples/extensions/voice-transcribe/requirements.txt +8 -0
- package/examples/extensions/voice-transcribe/scripts/transcribe.py +179 -0
- package/examples/extensions/voice-transcribe/skill/SKILL.md +53 -0
- package/examples/extensions/web-search/index.ts +22 -0
- package/examples/extensions/web-search/skill/SKILL.md +114 -0
- package/examples/extensions/web-search/skill/references/apartments.md +178 -0
- package/examples/extensions/web-search/skill/references/car-purchase.md +132 -0
- package/examples/extensions/web-search/skill/references/car-rental.md +113 -0
- package/examples/extensions/web-search/skill/references/flights.md +133 -0
- package/examples/extensions/web-search/skill/references/hotels.md +148 -0
- package/examples/extensions/yahoo-mail/cli/bun.lock +66 -0
- package/examples/extensions/yahoo-mail/cli/package.json +13 -0
- package/examples/extensions/yahoo-mail/cli/ymail.mjs +353 -0
- package/examples/extensions/yahoo-mail/index.ts +57 -0
- package/examples/extensions/yahoo-mail/skill/SKILL.md +78 -0
- package/package.json +106 -0
- package/resources/agents/explore.md +50 -0
- package/resources/agents/worker.md +24 -0
- package/resources/builtin-extensions.txt +3 -0
- package/resources/connection-env-vars.json +25 -0
- package/resources/extensions/.gitkeep +0 -0
- package/resources/pi-extensions/subagent/agents.ts +126 -0
- package/resources/pi-extensions/subagent/index.ts +964 -0
- package/resources/profiles/coding/AGENTS.md +43 -0
- package/resources/profiles/coding/mercury-profile.yaml +15 -0
- package/resources/profiles/general/AGENTS.md +31 -0
- package/resources/profiles/general/mercury-profile.yaml +15 -0
- package/resources/profiles/research/AGENTS.md +40 -0
- package/resources/profiles/research/mercury-profile.yaml +15 -0
- package/resources/skills/config/SKILL.md +25 -0
- package/resources/skills/context/SKILL.md +33 -0
- package/resources/skills/conversation-recap/SKILL.md +19 -0
- package/resources/skills/media/SKILL.md +27 -0
- package/resources/skills/mutes/SKILL.md +31 -0
- package/resources/skills/permissions/SKILL.md +19 -0
- package/resources/skills/preferences/SKILL.md +31 -0
- package/resources/skills/recall/SKILL.md +24 -0
- package/resources/skills/roles/SKILL.md +18 -0
- package/resources/skills/spaces/SKILL.md +18 -0
- package/resources/skills/tasks/SKILL.md +45 -0
- package/resources/templates/AGENTS.md +157 -0
- package/resources/templates/env.template +34 -0
- package/resources/templates/mercury.example.yaml +75 -0
- package/src/adapters/discord-native.ts +534 -0
- package/src/adapters/discord.ts +38 -0
- package/src/adapters/setup.ts +89 -0
- package/src/adapters/slack.ts +9 -0
- package/src/adapters/whatsapp-media.ts +337 -0
- package/src/adapters/whatsapp.ts +629 -0
- package/src/agent/api-socket.ts +127 -0
- package/src/agent/container-entry.ts +967 -0
- package/src/agent/container-error.ts +49 -0
- package/src/agent/container-runner.ts +1272 -0
- package/src/agent/model-capabilities-core.ts +23 -0
- package/src/agent/model-capabilities.ts +231 -0
- package/src/agent/pi-failure-class.ts +83 -0
- package/src/agent/pi-jsonl-parser.ts +306 -0
- package/src/agent/preferences-prompt.ts +20 -0
- package/src/agent/user-error-messages.ts +78 -0
- package/src/bridges/discord.ts +171 -0
- package/src/bridges/slack.ts +177 -0
- package/src/bridges/teams.ts +160 -0
- package/src/bridges/telegram.ts +571 -0
- package/src/bridges/whatsapp.ts +290 -0
- package/src/chat-shim.ts +259 -0
- package/src/cli/mercury.ts +2508 -0
- package/src/cli/mrctl-http.ts +27 -0
- package/src/cli/mrctl.ts +611 -0
- package/src/cli/whatsapp-auth.ts +260 -0
- package/src/config-file.ts +397 -0
- package/src/config-model-chain.ts +30 -0
- package/src/config.ts +316 -0
- package/src/core/api-types.ts +58 -0
- package/src/core/api.ts +105 -0
- package/src/core/commands.ts +76 -0
- package/src/core/conversation.ts +47 -0
- package/src/core/handler.ts +206 -0
- package/src/core/media.ts +200 -0
- package/src/core/mute-duration.ts +22 -0
- package/src/core/outbox.ts +76 -0
- package/src/core/permissions.ts +192 -0
- package/src/core/profiles.ts +245 -0
- package/src/core/rate-limiter.ts +127 -0
- package/src/core/router.ts +191 -0
- package/src/core/routes/chat.ts +172 -0
- package/src/core/routes/config-builtin.ts +107 -0
- package/src/core/routes/config.ts +81 -0
- package/src/core/routes/connections.ts +190 -0
- package/src/core/routes/console.ts +668 -0
- package/src/core/routes/control.ts +46 -0
- package/src/core/routes/conversations.ts +66 -0
- package/src/core/routes/dashboard.ts +2491 -0
- package/src/core/routes/extensions.ts +37 -0
- package/src/core/routes/index.ts +14 -0
- package/src/core/routes/media.ts +72 -0
- package/src/core/routes/messages.ts +37 -0
- package/src/core/routes/mutes.ts +89 -0
- package/src/core/routes/prefs.ts +95 -0
- package/src/core/routes/roles.ts +125 -0
- package/src/core/routes/spaces.ts +60 -0
- package/src/core/routes/storage.ts +126 -0
- package/src/core/routes/tasks.ts +189 -0
- package/src/core/routes/tradestation.ts +268 -0
- package/src/core/routes/tts.ts +51 -0
- package/src/core/runtime.ts +1140 -0
- package/src/core/space-queue.ts +103 -0
- package/src/core/storage-cleanup.ts +140 -0
- package/src/core/storage-guard.ts +24 -0
- package/src/core/task-scheduler.ts +132 -0
- package/src/core/telegram-format.ts +178 -0
- package/src/core/trigger.ts +142 -0
- package/src/dashboard/index.html +729 -0
- package/src/dashboard/tokens.css +53 -0
- package/src/extensions/api.ts +252 -0
- package/src/extensions/catalog.ts +117 -0
- package/src/extensions/config-registry.ts +83 -0
- package/src/extensions/context.ts +36 -0
- package/src/extensions/hooks.ts +156 -0
- package/src/extensions/image-builder.ts +617 -0
- package/src/extensions/installer.ts +306 -0
- package/src/extensions/jobs.ts +122 -0
- package/src/extensions/loader.ts +271 -0
- package/src/extensions/permission-guard.ts +52 -0
- package/src/extensions/reserved.ts +28 -0
- package/src/extensions/skills.ts +123 -0
- package/src/extensions/types.ts +462 -0
- package/src/logger.ts +174 -0
- package/src/main.ts +586 -0
- package/src/server.ts +391 -0
- package/src/storage/db.ts +1624 -0
- package/src/storage/memory.ts +45 -0
- package/src/storage/pi-auth.ts +95 -0
- package/src/text/markdown.ts +117 -0
- package/src/text/rtl.ts +38 -0
- package/src/tradestation/host-api.ts +77 -0
- package/src/tradestation/pending-orders.ts +69 -0
- package/src/tts/azure.ts +52 -0
- package/src/tts/google.ts +128 -0
- package/src/tts/index.ts +8 -0
- package/src/tts/language.ts +20 -0
- package/src/tts/synthesize.ts +133 -0
- package/src/types.ts +295 -0
package/src/server.ts
ADDED
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
import { timingSafeEqual } from "node:crypto";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import type { Adapter } from "chat";
|
|
6
|
+
import { Hono } from "hono";
|
|
7
|
+
import type { WhatsAppBaileysAdapter } from "./adapters/whatsapp.js";
|
|
8
|
+
import type { AppConfig } from "./config.js";
|
|
9
|
+
import { resolveProjectPath } from "./config.js";
|
|
10
|
+
import { createApiApp } from "./core/api.js";
|
|
11
|
+
import { createChatRoute } from "./core/routes/chat.js";
|
|
12
|
+
import { createConsoleApp } from "./core/routes/console.js";
|
|
13
|
+
import { createDashboardRoutes } from "./core/routes/dashboard.js";
|
|
14
|
+
import type { MercuryCoreRuntime } from "./core/runtime.js";
|
|
15
|
+
import type { ConfigRegistry } from "./extensions/config-registry.js";
|
|
16
|
+
import { createMercuryExtensionContext } from "./extensions/context.js";
|
|
17
|
+
import { ensureDerivedImage } from "./extensions/image-builder.js";
|
|
18
|
+
import { ExtensionRegistry } from "./extensions/loader.js";
|
|
19
|
+
import { logger } from "./logger.js";
|
|
20
|
+
|
|
21
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
22
|
+
|
|
23
|
+
const MAX_BODY_SIZE = 50 * 1024 * 1024; // 50 MB
|
|
24
|
+
|
|
25
|
+
type WaitUntil = (task: Promise<unknown>) => void;
|
|
26
|
+
|
|
27
|
+
type WebhookHandler = (
|
|
28
|
+
request: Request,
|
|
29
|
+
options?: { waitUntil?: WaitUntil },
|
|
30
|
+
) => Promise<Response>;
|
|
31
|
+
|
|
32
|
+
export interface ServerContext {
|
|
33
|
+
core: MercuryCoreRuntime;
|
|
34
|
+
config: AppConfig;
|
|
35
|
+
adapters: Record<string, Adapter>;
|
|
36
|
+
webhooks: Record<string, WebhookHandler>;
|
|
37
|
+
startTime: number;
|
|
38
|
+
registry: ExtensionRegistry;
|
|
39
|
+
configRegistry: ConfigRegistry;
|
|
40
|
+
/** Current Mercury project directory (usually `process.cwd()`). */
|
|
41
|
+
projectRoot: string;
|
|
42
|
+
/** Root of the mercury-agent package (for bundled `examples/extensions`). */
|
|
43
|
+
packageRoot: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Authorize an infra request against the `MERCURY_API_SECRET` Bearer token —
|
|
48
|
+
* the same secret enforced by `/api/*` and `/api/console/*`. Returns 503 when no
|
|
49
|
+
* secret is configured (a side-effecting endpoint must never silently run
|
|
50
|
+
* unauthenticated), 401 on a missing or mismatched token, `ok` when valid.
|
|
51
|
+
* Length is checked before `timingSafeEqual` (which throws on unequal buffer
|
|
52
|
+
* lengths); the comparison itself is constant-time. Exported for testing.
|
|
53
|
+
*/
|
|
54
|
+
export function authorizeApiSecret(
|
|
55
|
+
authHeader: string | undefined,
|
|
56
|
+
secret: string | undefined,
|
|
57
|
+
): { ok: true } | { ok: false; status: 401 | 503 } {
|
|
58
|
+
if (!secret) return { ok: false, status: 503 };
|
|
59
|
+
const token = authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : "";
|
|
60
|
+
const tokenBuf = Buffer.from(token);
|
|
61
|
+
const secretBuf = Buffer.from(secret);
|
|
62
|
+
if (
|
|
63
|
+
tokenBuf.length !== secretBuf.length ||
|
|
64
|
+
!timingSafeEqual(tokenBuf, secretBuf)
|
|
65
|
+
) {
|
|
66
|
+
return { ok: false, status: 401 };
|
|
67
|
+
}
|
|
68
|
+
return { ok: true };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function createApp(ctx: ServerContext): Hono {
|
|
72
|
+
const {
|
|
73
|
+
core,
|
|
74
|
+
config,
|
|
75
|
+
adapters,
|
|
76
|
+
webhooks,
|
|
77
|
+
startTime,
|
|
78
|
+
projectRoot,
|
|
79
|
+
packageRoot,
|
|
80
|
+
} = ctx;
|
|
81
|
+
|
|
82
|
+
const waitUntil: WaitUntil = (task) => {
|
|
83
|
+
void task.catch((error) => {
|
|
84
|
+
logger.error(
|
|
85
|
+
"Background task failed",
|
|
86
|
+
error instanceof Error ? error : undefined,
|
|
87
|
+
);
|
|
88
|
+
});
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const app = new Hono();
|
|
92
|
+
|
|
93
|
+
// ─── Body Size Limit ──────────────────────────────────────────────────
|
|
94
|
+
app.use("*", async (c, next) => {
|
|
95
|
+
const contentLength = c.req.header("content-length");
|
|
96
|
+
if (contentLength && Number.parseInt(contentLength, 10) > MAX_BODY_SIZE) {
|
|
97
|
+
return c.json({ error: "Request body too large" }, 413);
|
|
98
|
+
}
|
|
99
|
+
await next();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// ─── Dashboard ──────────────────────────────────────────────────────────
|
|
103
|
+
|
|
104
|
+
// Cache dashboard HTML at startup
|
|
105
|
+
let dashboardHtml: string | null = null;
|
|
106
|
+
try {
|
|
107
|
+
const html = readFileSync(join(__dirname, "dashboard/index.html"), "utf8");
|
|
108
|
+
let version = "unknown";
|
|
109
|
+
try {
|
|
110
|
+
const pkg = JSON.parse(
|
|
111
|
+
readFileSync(join(packageRoot, "package.json"), "utf8"),
|
|
112
|
+
);
|
|
113
|
+
version = `v${pkg.version}`;
|
|
114
|
+
} catch {
|
|
115
|
+
// ignore — version stays "unknown"
|
|
116
|
+
}
|
|
117
|
+
dashboardHtml = html.replace("{{VERSION}}", version);
|
|
118
|
+
} catch {
|
|
119
|
+
// Dashboard not found — will return 404
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Cache tokens.css at startup
|
|
123
|
+
let tokensCss: string | null = null;
|
|
124
|
+
try {
|
|
125
|
+
tokensCss = readFileSync(join(__dirname, "dashboard/tokens.css"), "utf8");
|
|
126
|
+
} catch {
|
|
127
|
+
// tokens.css not found — will return 404
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
app.get("/", (c) => {
|
|
131
|
+
if (!dashboardHtml) return c.text("Dashboard not found", 404);
|
|
132
|
+
return c.html(dashboardHtml);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
app.get("/dashboard", (c) => {
|
|
136
|
+
if (!dashboardHtml) return c.text("Dashboard not found", 404);
|
|
137
|
+
return c.html(dashboardHtml);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
app.get("/dashboard/tokens.css", (c) => {
|
|
141
|
+
if (!tokensCss) return c.text("tokens.css not found", 404);
|
|
142
|
+
c.header("Content-Type", "text/css; charset=utf-8");
|
|
143
|
+
c.header("Cache-Control", "public, max-age=300");
|
|
144
|
+
return c.body(tokensCss);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// Dashboard partials (htmx)
|
|
148
|
+
const adapterStatus: Record<string, boolean> = {};
|
|
149
|
+
for (const name of Object.keys(adapters)) {
|
|
150
|
+
adapterStatus[name] = true;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const dashboardRoutes = createDashboardRoutes({
|
|
154
|
+
core,
|
|
155
|
+
adapters: adapterStatus,
|
|
156
|
+
startTime,
|
|
157
|
+
registry: ctx.registry,
|
|
158
|
+
configRegistry: ctx.configRegistry,
|
|
159
|
+
extensionCtx: createMercuryExtensionContext({
|
|
160
|
+
db: core.db,
|
|
161
|
+
config,
|
|
162
|
+
log: logger,
|
|
163
|
+
}),
|
|
164
|
+
projectRoot,
|
|
165
|
+
packageRoot,
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// Login route — validates token, sets session cookie, redirects to dashboard
|
|
169
|
+
app.get("/dashboard/login", (c) => {
|
|
170
|
+
const secret = config.apiSecret;
|
|
171
|
+
if (!secret) {
|
|
172
|
+
return c.redirect("/dashboard");
|
|
173
|
+
}
|
|
174
|
+
const token = c.req.query("token");
|
|
175
|
+
if (
|
|
176
|
+
!token ||
|
|
177
|
+
token.length !== secret.length ||
|
|
178
|
+
!timingSafeEqual(Buffer.from(token), Buffer.from(secret))
|
|
179
|
+
) {
|
|
180
|
+
return c.text("Invalid or missing token", 401);
|
|
181
|
+
}
|
|
182
|
+
c.header(
|
|
183
|
+
"Set-Cookie",
|
|
184
|
+
`mercury_token=${token}; Path=/; HttpOnly; SameSite=Strict`,
|
|
185
|
+
);
|
|
186
|
+
return c.redirect("/dashboard");
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
app.use("/dashboard/*", async (c, next) => {
|
|
190
|
+
// Login route handled above — skip auth
|
|
191
|
+
if (c.req.path === "/dashboard/login") return next();
|
|
192
|
+
|
|
193
|
+
const secret = config.apiSecret;
|
|
194
|
+
if (secret) {
|
|
195
|
+
const authHeader = c.req.header("authorization");
|
|
196
|
+
const token = authHeader?.startsWith("Bearer ")
|
|
197
|
+
? authHeader.slice(7)
|
|
198
|
+
: undefined;
|
|
199
|
+
const cookie = c.req.header("cookie");
|
|
200
|
+
const cookieToken = cookie
|
|
201
|
+
?.split(";")
|
|
202
|
+
.map((s) => s.trim())
|
|
203
|
+
.find((s) => s.startsWith("mercury_token="))
|
|
204
|
+
?.split("=")[1];
|
|
205
|
+
|
|
206
|
+
const provided = token || cookieToken;
|
|
207
|
+
if (
|
|
208
|
+
!provided ||
|
|
209
|
+
provided.length !== secret.length ||
|
|
210
|
+
!timingSafeEqual(Buffer.from(provided), Buffer.from(secret))
|
|
211
|
+
) {
|
|
212
|
+
return c.json({ error: "Unauthorized" }, 401);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
await next();
|
|
216
|
+
});
|
|
217
|
+
app.route("/dashboard", dashboardRoutes);
|
|
218
|
+
|
|
219
|
+
// ─── Health & Auth ──────────────────────────────────────────────────────
|
|
220
|
+
|
|
221
|
+
app.get("/health", (c) => {
|
|
222
|
+
const uptimeSeconds = Math.floor((Date.now() - startTime) / 1000);
|
|
223
|
+
const adapterStatus: Record<string, boolean> = {};
|
|
224
|
+
for (const name of Object.keys(adapters)) {
|
|
225
|
+
adapterStatus[name] = true;
|
|
226
|
+
}
|
|
227
|
+
const currentImage = core.containerRunner.image;
|
|
228
|
+
const extImage = currentImage.startsWith("mercury-agent-ext-")
|
|
229
|
+
? currentImage
|
|
230
|
+
: null;
|
|
231
|
+
return c.json({
|
|
232
|
+
status: "ok",
|
|
233
|
+
version:
|
|
234
|
+
process.env.MERCURY_VERSION ??
|
|
235
|
+
process.env.npm_package_version ??
|
|
236
|
+
"unknown",
|
|
237
|
+
uptime: uptimeSeconds,
|
|
238
|
+
queue: {
|
|
239
|
+
active: core.queue.activeCount,
|
|
240
|
+
pending: core.queue.pendingCount,
|
|
241
|
+
},
|
|
242
|
+
containers: {
|
|
243
|
+
active: core.containerRunner.activeCount,
|
|
244
|
+
},
|
|
245
|
+
adapters: adapterStatus,
|
|
246
|
+
extImage,
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
// ─── Pre-build ext image (called by the orchestrator during Phase A of rolling deploy) ──
|
|
251
|
+
// Builds the derived ext image for the given base image tag against this agent's
|
|
252
|
+
// current extension set. After this completes the image is cached — the swap
|
|
253
|
+
// container finds it immediately and skips the build, exiting warming in ~1s.
|
|
254
|
+
//
|
|
255
|
+
// Auth: requires the MERCURY_API_SECRET Bearer token (same as /api/* and
|
|
256
|
+
// /api/console/*). Unlike /health this endpoint has side effects — it loads
|
|
257
|
+
// extensions with caller-supplied env and spawns a Docker build — so it must
|
|
258
|
+
// not rely on mercury-net topology alone (the process binds 0.0.0.0:8787).
|
|
259
|
+
// When no secret is configured it refuses with 503 rather than running open,
|
|
260
|
+
// so it can never silently serve unauthenticated.
|
|
261
|
+
app.post("/pre-build-ext-image", async (c) => {
|
|
262
|
+
const auth = authorizeApiSecret(
|
|
263
|
+
c.req.header("authorization"),
|
|
264
|
+
config.apiSecret,
|
|
265
|
+
);
|
|
266
|
+
if (!auth.ok) {
|
|
267
|
+
return c.json(
|
|
268
|
+
auth.status === 503
|
|
269
|
+
? { error: "MERCURY_API_SECRET must be set for /pre-build-ext-image" }
|
|
270
|
+
: { error: "Unauthorized" },
|
|
271
|
+
auth.status,
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
const raw = await c.req.json().catch(() => null);
|
|
275
|
+
const body =
|
|
276
|
+
raw && typeof raw === "object" ? (raw as Record<string, unknown>) : {};
|
|
277
|
+
const targetAgentImage = body.targetAgentImage;
|
|
278
|
+
if (typeof targetAgentImage !== "string" || !targetAgentImage) {
|
|
279
|
+
return c.json({ error: "targetAgentImage is required" }, 400);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// When targetEnv is provided, simulate extension loading with the target
|
|
283
|
+
// container's env so the pre-built image hash matches the -next container.
|
|
284
|
+
let extensions = ctx.registry.list();
|
|
285
|
+
const targetEnv = body.targetEnv;
|
|
286
|
+
if (
|
|
287
|
+
targetEnv &&
|
|
288
|
+
typeof targetEnv === "object" &&
|
|
289
|
+
!Array.isArray(targetEnv)
|
|
290
|
+
) {
|
|
291
|
+
// Coerce all values to strings to avoid null/number values slipping past
|
|
292
|
+
// the credential gate check (!envToCheck[credVar]).
|
|
293
|
+
const envOverride = Object.fromEntries(
|
|
294
|
+
Object.entries(targetEnv as Record<string, unknown>)
|
|
295
|
+
.filter(([, v]) => typeof v === "string")
|
|
296
|
+
.map(([k, v]) => [k, v as string]),
|
|
297
|
+
);
|
|
298
|
+
const tempRegistry = new ExtensionRegistry();
|
|
299
|
+
const extensionsDir = resolveProjectPath(`${config.dataDir}/extensions`);
|
|
300
|
+
const builtinExtDir = join(packageRoot, "resources/extensions");
|
|
301
|
+
try {
|
|
302
|
+
await tempRegistry.loadAll(
|
|
303
|
+
extensionsDir,
|
|
304
|
+
core.db,
|
|
305
|
+
logger,
|
|
306
|
+
null,
|
|
307
|
+
[builtinExtDir],
|
|
308
|
+
envOverride,
|
|
309
|
+
);
|
|
310
|
+
extensions = tempRegistry.list();
|
|
311
|
+
} catch (err) {
|
|
312
|
+
logger.warn(
|
|
313
|
+
"pre-build: failed to load extensions with targetEnv, falling back to current registry",
|
|
314
|
+
{
|
|
315
|
+
error: err instanceof Error ? err.message : String(err),
|
|
316
|
+
},
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const derivedImage = await ensureDerivedImage(
|
|
322
|
+
targetAgentImage,
|
|
323
|
+
extensions,
|
|
324
|
+
logger,
|
|
325
|
+
process.env.MERCURY_AGENT_ID,
|
|
326
|
+
);
|
|
327
|
+
return c.json({ status: "ok", derivedImage });
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
app.get("/auth/whatsapp", (c) => {
|
|
331
|
+
const whatsappAdapter = adapters.whatsapp as
|
|
332
|
+
| WhatsAppBaileysAdapter
|
|
333
|
+
| undefined;
|
|
334
|
+
if (!whatsappAdapter) {
|
|
335
|
+
return c.json({ error: "WhatsApp adapter not enabled" }, 400);
|
|
336
|
+
}
|
|
337
|
+
const status = whatsappAdapter.getQrStatus();
|
|
338
|
+
return c.json(status);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
// ─── Control plane JSON API (Bearer MERCURY_API_SECRET) ─────────────────
|
|
342
|
+
const consoleApp = createConsoleApp({
|
|
343
|
+
projectRoot,
|
|
344
|
+
packageRoot,
|
|
345
|
+
apiSecret: config.apiSecret,
|
|
346
|
+
db: core.db,
|
|
347
|
+
spacesDir: config.spacesDir,
|
|
348
|
+
dbPath: config.dbPath,
|
|
349
|
+
whatsappAuthDir: config.whatsappAuthDir,
|
|
350
|
+
registry: ctx.registry,
|
|
351
|
+
config,
|
|
352
|
+
});
|
|
353
|
+
app.route("/api/console", consoleApp);
|
|
354
|
+
|
|
355
|
+
// ─── Internal API ───────────────────────────────────────────────────────
|
|
356
|
+
|
|
357
|
+
const apiApp = createApiApp({
|
|
358
|
+
db: core.db,
|
|
359
|
+
config,
|
|
360
|
+
containerRunner: core.containerRunner,
|
|
361
|
+
queue: core.queue,
|
|
362
|
+
scheduler: core.scheduler,
|
|
363
|
+
registry: ctx.registry,
|
|
364
|
+
configRegistry: ctx.configRegistry,
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
app.route("/api", apiApp);
|
|
368
|
+
app.route("/chat", createChatRoute(core));
|
|
369
|
+
|
|
370
|
+
// ─── Webhooks ───────────────────────────────────────────────────────────
|
|
371
|
+
|
|
372
|
+
app.all("/webhooks/:platform", async (c) => {
|
|
373
|
+
const platform = c.req.param("platform");
|
|
374
|
+
logger.info("Webhook dispatch", { platform });
|
|
375
|
+
|
|
376
|
+
const handler = webhooks[platform];
|
|
377
|
+
if (!handler) {
|
|
378
|
+
return c.text(`Unknown platform: ${platform}`, 404);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
return handler(c.req.raw, { waitUntil });
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
// ─── Fallback ───────────────────────────────────────────────────────────
|
|
385
|
+
|
|
386
|
+
app.all("*", (c) => {
|
|
387
|
+
return c.text("Not found", 404);
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
return app;
|
|
391
|
+
}
|