aquaman-plugin 0.11.2 → 0.11.4

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/index.ts DELETED
@@ -1,592 +0,0 @@
1
- /**
2
- * Aquaman OpenClaw Plugin
3
- *
4
- * Credential isolation for OpenClaw.
5
- * Credentials never enter the agent process - they're managed by a separate proxy.
6
- *
7
- * Usage:
8
- * 1. Install aquaman: npm install -g aquaman-proxy
9
- * 2. Store credentials: aquaman credentials add anthropic api_key
10
- * 3. Enable this plugin in openclaw.json
11
- *
12
- * The plugin will:
13
- * - Start the aquaman proxy on plugin load
14
- * - Set ANTHROPIC_BASE_URL, OPENAI_BASE_URL etc. to route through proxy via UDS
15
- * - The proxy injects credentials into requests
16
- * - Agent never sees the actual API keys
17
- */
18
-
19
- // OpenClaw plugin SDK types — defined locally to avoid import resolution failures.
20
- // The root import "openclaw/plugin-sdk" broke for user-installed plugins in OpenClaw 2026.3.23
21
- // (GitHub issue #53403: jiti resolver can't walk from ~/.openclaw/extensions/ to OpenClaw's
22
- // package tree). Since we only use these as compile-time types, local definitions are zero-risk
23
- // and make the plugin resilient to SDK path changes. Revert to SDK import if OpenClaw stabilizes
24
- // module resolution for user-installed plugins.
25
-
26
- interface OpenClawPluginLogger {
27
- info(msg: string): void;
28
- warn(msg: string): void;
29
- error(msg: string): void;
30
- }
31
-
32
- interface OpenClawPluginApi {
33
- logger: OpenClawPluginLogger;
34
- pluginConfig: unknown;
35
- registerService(def: {
36
- id: string;
37
- start(ctx: { logger: OpenClawPluginLogger }): void | Promise<void>;
38
- stop(ctx: { logger: OpenClawPluginLogger }): void | Promise<void>;
39
- }): void;
40
- registerCommand(def: {
41
- name: string;
42
- description: string;
43
- acceptsArgs: boolean;
44
- requireAuth: boolean;
45
- handler(): Promise<{ text: string }>;
46
- }): void;
47
- registerCli?(
48
- fn: (opts: { program: any }) => void,
49
- opts: { commands: string[] },
50
- ): void;
51
- registerTool(
52
- factory: () => {
53
- name: string;
54
- label: string;
55
- description: string;
56
- parameters: { type: "object"; properties: Record<string, unknown>; required: string[] };
57
- execute(toolCallId: string, params: unknown): Promise<{
58
- content: { type: "text"; text: string }[];
59
- details: unknown;
60
- }>;
61
- },
62
- opts: { names: string[] },
63
- ): void;
64
- }
65
-
66
- type OpenClawPluginDefinition = {
67
- id?: string;
68
- name?: string;
69
- description?: string;
70
- version?: string;
71
- register?: (api: OpenClawPluginApi) => void | Promise<void>;
72
- };
73
- import * as fs from "node:fs";
74
- import * as path from "node:path";
75
- import * as os from "node:os";
76
- import { HttpInterceptor, createHttpInterceptor } from "./src/http-interceptor.js";
77
- import { createProxyManager, findAquamanProxyBinary, execAquamanProxyCli, execAquamanProxyInteractive, type ProxyManager } from "./src/proxy-manager.js";
78
- import { loadHostMap, isProxyRunning, getProxyVersion } from "./src/proxy-health.js";
79
-
80
- /**
81
- * Find an executable in PATH using filesystem checks (no shell execution).
82
- * Avoids execSync("which ...") which triggers dangerous-exec security audit flags.
83
- */
84
- function findInPath(name: string): string | null {
85
- const pathEnv = process.env.PATH || "";
86
- const dirs = pathEnv.split(path.delimiter);
87
- for (const dir of dirs) {
88
- const candidate = path.join(dir, name);
89
- try {
90
- fs.accessSync(candidate, fs.constants.X_OK);
91
- return candidate;
92
- } catch {
93
- // Not found or not executable in this dir
94
- }
95
- }
96
- return null;
97
- }
98
-
99
- // Read plugin version from package.json
100
- const pluginPkgPath = path.join(path.dirname(new URL(import.meta.url).pathname), 'package.json');
101
- let PLUGIN_VERSION = 'unknown';
102
- try { PLUGIN_VERSION = JSON.parse(fs.readFileSync(pluginPkgPath, 'utf-8')).version; } catch { /* ok */ }
103
-
104
- let proxyManager: ProxyManager | null = null;
105
- let httpInterceptor: HttpInterceptor | null = null;
106
- let socketPath: string | null = null;
107
- let dynamicHostMap: Map<string, string> | null = null;
108
- let configuredServices: string[] = ["anthropic", "openai"];
109
-
110
- /** Default socket path */
111
- function getDefaultSocketPath(): string {
112
- const configDir = path.join(os.homedir(), '.aquaman');
113
- return path.join(configDir, 'proxy.sock');
114
- }
115
-
116
- /** Fallback host map used when proxy doesn't provide one */
117
- const FALLBACK_HOST_MAP = new Map<string, string>([
118
- ['api.anthropic.com', 'anthropic'],
119
- ['api.openai.com', 'openai'],
120
- ['api.github.com', 'github'],
121
- ['api.x.ai', 'xai'],
122
- ['gateway.ai.cloudflare.com', 'cloudflare-ai'],
123
- ['api.mistral.ai', 'mistral'],
124
- ['api-inference.huggingface.co', 'huggingface'],
125
- ['slack.com', 'slack'],
126
- ['*.slack.com', 'slack'],
127
- ['discord.com', 'discord'],
128
- ['*.discord.com', 'discord'],
129
- ['api.telegram.org', 'telegram'],
130
- ['matrix.org', 'matrix'],
131
- ['*.matrix.org', 'matrix'],
132
- ['api.line.me', 'line'],
133
- ['api-data.line.me', 'line'],
134
- ['api.twitch.tv', 'twitch'],
135
- ['id.twitch.tv', 'twitch'],
136
- ['api.twilio.com', 'twilio'],
137
- ['*.twilio.com', 'twilio'],
138
- ['api.telnyx.com', 'telnyx'],
139
- ['api.elevenlabs.io', 'elevenlabs'],
140
- ['openapi.zalo.me', 'zalo'],
141
- ['graph.microsoft.com', 'ms-teams'],
142
- ['open.feishu.cn', 'feishu'],
143
- ['open.larksuite.com', 'feishu'],
144
- ['chat.googleapis.com', 'google-chat'],
145
- ]);
146
-
147
- /**
148
- * Check if aquaman proxy binary is available (local node_modules or PATH)
149
- */
150
- function isAquamanProxyInstalled(): boolean {
151
- return findAquamanProxyBinary() !== null;
152
- }
153
-
154
- /**
155
- * Start the aquaman proxy daemon using ProxyManager
156
- */
157
- async function startProxy(log: OpenClawPluginApi["logger"]): Promise<boolean> {
158
- try {
159
- const mgr = createProxyManager({
160
- config: {},
161
- onReady: (info) => {
162
- socketPath = info.socketPath;
163
- if (info.hostMap && typeof info.hostMap === "object") {
164
- dynamicHostMap = new Map(Object.entries(info.hostMap));
165
- }
166
- },
167
- onError: (err) => log.error(`Proxy error: ${err.message}`),
168
- onExit: (code) => {
169
- proxyManager = null;
170
- log.warn(`Proxy exited with code ${code}`);
171
- },
172
- });
173
- await mgr.start();
174
- proxyManager = mgr;
175
- socketPath = mgr.getSocketPath();
176
- return true;
177
- } catch (err) {
178
- log.error(`Failed to start proxy: ${err}`);
179
- return false;
180
- }
181
- }
182
-
183
- /**
184
- * Stop the proxy daemon and deactivate the HTTP interceptor
185
- */
186
- function stopProxy(): void {
187
- if (httpInterceptor) {
188
- httpInterceptor.deactivate();
189
- httpInterceptor = null;
190
- }
191
- if (proxyManager) {
192
- proxyManager.stop();
193
- proxyManager = null;
194
- }
195
- socketPath = null;
196
- }
197
-
198
- /**
199
- * Activate the HTTP interceptor to redirect channel API traffic through the proxy.
200
- * This is what provides credential isolation for channels that don't support base URL overrides.
201
- */
202
- function activateHttpInterceptor(log: OpenClawPluginApi["logger"]): void {
203
- if (!socketPath) {
204
- log.error("Cannot activate HTTP interceptor: no socket path");
205
- return;
206
- }
207
-
208
- // Use dynamic host map from proxy (includes custom services from services.yaml)
209
- // Falls back to builtin map for backward compatibility
210
- const hostMap = dynamicHostMap || FALLBACK_HOST_MAP;
211
-
212
- httpInterceptor = createHttpInterceptor({
213
- socketPath,
214
- hostMap,
215
- log: (msg) => log.info(msg),
216
- });
217
-
218
- httpInterceptor.activate();
219
- log.info(`HTTP interceptor active: ${hostMap.size} host patterns redirected through proxy`);
220
- }
221
-
222
- /**
223
- * Set environment variables for SDK clients using sentinel hostname
224
- */
225
- function configureEnvironment(log: OpenClawPluginApi["logger"], services: string[]): void {
226
- for (const service of services) {
227
- const serviceUrl = `http://aquaman.local/${service}`;
228
-
229
- switch (service) {
230
- case "anthropic":
231
- process.env["ANTHROPIC_BASE_URL"] = serviceUrl;
232
- log.info(`Set ANTHROPIC_BASE_URL=${serviceUrl}`);
233
- break;
234
- case "openai":
235
- process.env["OPENAI_BASE_URL"] = serviceUrl;
236
- log.info(`Set OPENAI_BASE_URL=${serviceUrl}`);
237
- break;
238
- case "github":
239
- process.env["GITHUB_API_URL"] = serviceUrl;
240
- log.info(`Set GITHUB_API_URL=${serviceUrl}`);
241
- break;
242
- default:
243
- const envKey = `${service.toUpperCase().replace(/-/g, "_")}_BASE_URL`;
244
- process.env[envKey] = serviceUrl;
245
- log.info(`Set ${envKey}=${serviceUrl}`);
246
- }
247
- }
248
- }
249
-
250
- /**
251
- * Build status object for both the tool and slash command
252
- */
253
- function getStatus(services: string[]) {
254
- const cliInstalled = isAquamanProxyInstalled();
255
- return {
256
- cliInstalled,
257
- proxyRunning: proxyManager !== null,
258
- socketPath: socketPath || getDefaultSocketPath(),
259
- services,
260
- httpInterceptorActive: httpInterceptor?.isActive() ?? false,
261
- ...(cliInstalled ? {} : { fix: "Run: npm install -g aquaman-proxy && aquaman setup" }),
262
- ...(!cliInstalled ? {} : proxyManager === null ? { fix: "Run: aquaman setup (or: openclaw aquaman setup)" } : {}),
263
- environmentVariables: Object.fromEntries(
264
- services.map((s) => {
265
- const key =
266
- s === "anthropic"
267
- ? "ANTHROPIC_BASE_URL"
268
- : s === "openai"
269
- ? "OPENAI_BASE_URL"
270
- : `${s.toUpperCase()}_BASE_URL`;
271
- return [key, process.env[key] ?? null];
272
- })
273
- ),
274
- };
275
- }
276
-
277
- /**
278
- * Register the aquaman_status tool — always registered (works in degraded mode)
279
- */
280
- function registerStatusTool(api: OpenClawPluginApi, services: string[]): void {
281
- api.registerTool(
282
- () => {
283
- return {
284
- name: "aquaman_status",
285
- label: "Aquaman Status",
286
- description:
287
- "Check aquaman credential proxy status and configured services",
288
- parameters: {
289
- type: "object" as const,
290
- properties: {},
291
- required: [] as string[],
292
- },
293
- async execute(_toolCallId: string, _params: unknown) {
294
- const status = getStatus(services);
295
- return {
296
- content: [{ type: "text" as const, text: JSON.stringify(status, null, 2) }],
297
- details: status,
298
- };
299
- },
300
- };
301
- },
302
- { names: ["aquaman_status"] }
303
- );
304
- }
305
-
306
- /**
307
- * Auto-generate auth-profiles.json with placeholder keys for proxied services.
308
- * OpenClaw checks its auth store before making API calls — without a placeholder
309
- * key, requests are rejected before they ever reach the proxy.
310
- */
311
- function ensureAuthProfiles(log: OpenClawPluginApi["logger"], services: string[]): void {
312
- const stateDir =
313
- process.env.OPENCLAW_STATE_DIR ||
314
- path.join(os.homedir(), ".openclaw");
315
- const profilesPath = path.join(
316
- stateDir,
317
- "agents",
318
- "main",
319
- "agent",
320
- "auth-profiles.json"
321
- );
322
-
323
- if (fs.existsSync(profilesPath)) return;
324
-
325
- const profiles: Record<string, any> = {};
326
- const order: Record<string, string[]> = {};
327
-
328
- for (const service of services) {
329
- if (service === "anthropic" || service === "openai") {
330
- profiles[`${service}:default`] = {
331
- type: "api_key",
332
- provider: service,
333
- key: "aquaman-proxy-managed",
334
- };
335
- order[service] = [`${service}:default`];
336
- }
337
- }
338
-
339
- const dir = path.dirname(profilesPath);
340
- fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
341
- fs.writeFileSync(
342
- profilesPath,
343
- JSON.stringify({ version: 1, profiles, order }, null, 2),
344
- { mode: 0o600 }
345
- );
346
- log.info(
347
- `Generated auth-profiles.json with placeholder keys at ${profilesPath}`
348
- );
349
- }
350
-
351
- /**
352
- * Aquaman OpenClaw Plugin Definition
353
- */
354
- const plugin: OpenClawPluginDefinition = {
355
- id: 'aquaman-plugin',
356
- name: 'Aquaman — API Key Protection',
357
- version: PLUGIN_VERSION,
358
- description: 'API key protection for OpenClaw — credentials stay in your vault, never in the agent\'s memory',
359
-
360
- register(api) {
361
- api.logger.info("Aquaman plugin loaded");
362
-
363
- // Read services from plugin config
364
- const pluginCfg = api.pluginConfig as { backend?: string; services?: string[] } | undefined;
365
- configuredServices = pluginCfg?.services ?? ["anthropic", "openai"];
366
-
367
- // Auto-generate auth-profiles.json if missing
368
- ensureAuthProfiles(api.logger, configuredServices);
369
-
370
- // Check if aquaman proxy binary is available
371
- const proxyAvailable = isAquamanProxyInstalled();
372
-
373
- if (!proxyAvailable) {
374
- api.logger.warn(
375
- "aquaman proxy not found. Install with: npm install -g aquaman-proxy"
376
- );
377
- api.logger.warn(
378
- "Then run: aquaman setup"
379
- );
380
- // DO NOT call configureEnvironment() — sentinel URLs without a proxy
381
- // would break all API calls (connection refused to non-existent socket)
382
- } else {
383
- api.logger.info("aquaman proxy found, will start proxy on gateway start");
384
-
385
- // Configure environment variables immediately (sentinel hostname)
386
- configureEnvironment(api.logger, configuredServices);
387
-
388
- // Register service for proxy lifecycle management
389
- api.registerService({
390
- id: 'aquaman-proxy',
391
- async start(ctx) {
392
- ctx.logger.info("Starting aquaman proxy...");
393
-
394
- const started = await startProxy(ctx.logger);
395
- if (started && socketPath) {
396
- ctx.logger.info("Aquaman proxy started successfully");
397
-
398
- // Check for version mismatch between plugin and proxy
399
- const proxyVersion = await getProxyVersion(socketPath);
400
- if (proxyVersion && proxyVersion !== PLUGIN_VERSION) {
401
- ctx.logger.warn(
402
- `Warning: plugin version ${PLUGIN_VERSION} \u2260 proxy version ${proxyVersion}. ` +
403
- `Update both: npm install -g aquaman-proxy && openclaw plugins install aquaman-plugin`
404
- );
405
- }
406
-
407
- // Activate HTTP interceptor to redirect channel traffic through proxy
408
- activateHttpInterceptor(ctx.logger);
409
- } else {
410
- ctx.logger.error("Failed to start aquaman proxy");
411
- // Check if another instance is already running
412
- const defaultSock = getDefaultSocketPath();
413
- const alreadyRunning = await isProxyRunning(defaultSock);
414
- if (alreadyRunning) {
415
- socketPath = defaultSock;
416
- ctx.logger.info(
417
- "Another aquaman instance is already running — using it"
418
- );
419
- // Load host map from existing proxy
420
- const map = await loadHostMap(defaultSock);
421
- dynamicHostMap = map.size > 0 ? map : FALLBACK_HOST_MAP;
422
- activateHttpInterceptor(ctx.logger);
423
- } else {
424
- ctx.logger.error(
425
- "No running proxy found. Check: openclaw aquaman doctor"
426
- );
427
- }
428
- }
429
- },
430
- async stop(ctx) {
431
- ctx.logger.info("Stopping aquaman proxy...");
432
- stopProxy();
433
- }
434
- });
435
- }
436
-
437
- // --- Commands, tools, and CLI are ALWAYS registered (even without proxy) ---
438
- // This ensures ClawHub users who installed the plugin but haven't run setup
439
- // still get actionable commands and status information.
440
-
441
- // Register /aquaman-status slash command for humans
442
- api.registerCommand({
443
- name: 'aquaman-status',
444
- description: 'Show aquaman credential proxy status and configured services',
445
- acceptsArgs: false,
446
- requireAuth: true,
447
- async handler() {
448
- const status = getStatus(configuredServices);
449
- return { text: JSON.stringify(status, null, 2) };
450
- }
451
- });
452
-
453
- // Register CLI commands if available
454
- if (api.registerCli) {
455
- api.registerCli(
456
- ({ program }) => {
457
- const aquamanCmd = program
458
- .command("aquaman")
459
- .description("Aquaman — API key protection");
460
-
461
- aquamanCmd
462
- .command("status")
463
- .description("Show aquaman proxy status")
464
- .action(() => {
465
- const status = getStatus(configuredServices);
466
- console.log("\nAquaman Status:");
467
- console.log(` Proxy binary: ${status.cliInstalled ? "found" : "NOT FOUND"}`);
468
- console.log(` Proxy running: ${status.proxyRunning}`);
469
- console.log(` Socket path: ${status.socketPath}`);
470
- console.log(` Services: ${configuredServices.join(", ")}`);
471
- if (status.fix) {
472
- console.log(`\n Action needed: ${status.fix}`);
473
- }
474
- if (status.proxyRunning) {
475
- console.log("\nEnvironment Variables:");
476
- for (const service of configuredServices) {
477
- const envKey =
478
- service === "anthropic"
479
- ? "ANTHROPIC_BASE_URL"
480
- : service === "openai"
481
- ? "OPENAI_BASE_URL"
482
- : `${service.toUpperCase()}_BASE_URL`;
483
- console.log(` ${envKey}=${process.env[envKey] ?? "(not set)"}`);
484
- }
485
- }
486
- });
487
-
488
- aquamanCmd
489
- .command("setup")
490
- .description("Run the setup wizard (stores keys, configures backend)")
491
- .action(async () => {
492
- try {
493
- const exitCode = await execAquamanProxyInteractive(['setup']);
494
- if (exitCode !== 0) process.exitCode = exitCode;
495
- } catch {
496
- console.log("\n Run in your terminal:\n aquaman setup\n");
497
- }
498
- });
499
-
500
- aquamanCmd
501
- .command("doctor")
502
- .description("Diagnose issues with actionable fixes")
503
- .action(async () => {
504
- try {
505
- const result = await execAquamanProxyCli(['doctor']);
506
- process.stdout.write(result.stdout);
507
- if (result.stderr) process.stderr.write(result.stderr);
508
- if (result.exitCode !== 0) process.exitCode = result.exitCode;
509
- } catch (err: any) {
510
- console.error(`Failed to run aquaman doctor: ${err.message}`);
511
- process.exitCode = 1;
512
- }
513
- });
514
-
515
- const credsCmd = aquamanCmd
516
- .command("credentials")
517
- .description("Credential management");
518
-
519
- credsCmd
520
- .command("list")
521
- .description("List stored credentials")
522
- .action(async () => {
523
- try {
524
- const result = await execAquamanProxyCli(['credentials', 'list']);
525
- process.stdout.write(result.stdout);
526
- if (result.stderr) process.stderr.write(result.stderr);
527
- } catch (err: any) {
528
- console.error(`Failed: ${err.message}`);
529
- }
530
- });
531
-
532
- credsCmd
533
- .command("add <service> [key]")
534
- .description("Add a credential (secure prompt)")
535
- .action(async (service: string, key: string = "api_key") => {
536
- try {
537
- const exitCode = await execAquamanProxyInteractive(['credentials', 'add', service, key]);
538
- if (exitCode !== 0) process.exitCode = exitCode;
539
- } catch {
540
- console.log(`\n Run in your terminal:\n aquaman credentials add ${service} ${key}\n`);
541
- }
542
- });
543
-
544
- aquamanCmd
545
- .command("policy-list")
546
- .description("List configured request policy rules")
547
- .action(async () => {
548
- try {
549
- const result = await execAquamanProxyCli(['policy', 'list']);
550
- process.stdout.write(result.stdout);
551
- if (result.stderr) process.stderr.write(result.stderr);
552
- } catch (err: any) {
553
- console.error(`Failed: ${err.message}`);
554
- }
555
- });
556
-
557
- aquamanCmd
558
- .command("audit-tail")
559
- .description("Show recent audit log entries")
560
- .action(async () => {
561
- try {
562
- const result = await execAquamanProxyCli(['audit', 'tail']);
563
- process.stdout.write(result.stdout);
564
- if (result.stderr) process.stderr.write(result.stderr);
565
- } catch (err: any) {
566
- console.error(`Failed: ${err.message}`);
567
- }
568
- });
569
-
570
- aquamanCmd
571
- .command("services-list")
572
- .description("List all configured services")
573
- .action(async () => {
574
- try {
575
- const result = await execAquamanProxyCli(['services', 'list']);
576
- process.stdout.write(result.stdout);
577
- if (result.stderr) process.stderr.write(result.stderr);
578
- } catch (err: any) {
579
- console.error(`Failed: ${err.message}`);
580
- }
581
- });
582
- },
583
- { commands: ["aquaman"] }
584
- );
585
- }
586
-
587
- registerStatusTool(api, configuredServices);
588
- api.logger.info("Aquaman plugin registered successfully");
589
- }
590
- };
591
-
592
- export default plugin;