aquaman-plugin 0.9.2 → 0.11.0

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/README.md CHANGED
@@ -1,19 +1,20 @@
1
- # aquaman-plugin
1
+ # Aquaman — API Key Protection for OpenClaw
2
2
 
3
- OpenClaw Gateway plugin for [aquaman](https://github.com/tech4242/aquaman) credential isolation.
4
-
5
- ## How It Works
3
+ Your API keys and tokens stay in your vault. The agent never sees them.
4
+ Even a compromised agent can't steal credentials — they live in a
5
+ separate process.
6
6
 
7
7
  ```
8
8
  Agent / OpenClaw Gateway Aquaman Proxy
9
9
  ┌──────────────────────┐ ┌──────────────────────┐
10
10
  │ │ │ │
11
- │ ANTHROPIC_BASE_URL │══ Unix ════>│ Keychain / 1Pass / │
12
- │ = aquaman.local │ Domain │ Vault / Encrypted │
13
- │ │<═ Socket ═══│
14
- │ fetch() interceptor │══ (UDS) ══=>│ + Auth injected:
15
- │ redirects channel │ │ header / url-path
16
- │ API traffic │ │ basic / oauth
11
+ │ ANTHROPIC_BASE_URL │══ Unix ═════>│ Keychain / 1Pass / │
12
+ │ = aquaman.local │ Domain │ Vault / Encrypted │
13
+ │ │<═ Socket ════│
14
+ │ fetch() interceptor │══ (UDS) ════>│ + Policy enforced
15
+ │ redirects channel │ │ + Auth injected:
16
+ │ API traffic │ │ header / url-path
17
+ │ │ │ basic / oauth │
17
18
  │ │ │ │
18
19
  │ No credentials. │ ~/.aquaman/ │ │
19
20
  │ No open ports. │ proxy.sock │ │
@@ -30,25 +31,42 @@ Agent / OpenClaw Gateway Aquaman Proxy
30
31
  slack.com/api ...
31
32
  ```
32
33
 
33
- This plugin makes the left side work. It routes all LLM and channel API traffic through the aquaman proxy via Unix domain socket so credentials never enter the Gateway process. No TCP port is opened — traffic flows through `~/.aquaman/proxy.sock`.
34
+ ## What It Does
35
+
36
+ 1. **Secrets stay in your vault** — Keychain, 1Password, HashiCorp Vault, KeePassXC, systemd-creds, Bitwarden, or encrypted file
37
+ 2. **Agent gets a proxy URL** — requests route through a local proxy that injects auth headers on the fly
38
+ 3. **Dangerous endpoints blocked** — request policies deny admin APIs, prevent deletions, block sends — before credentials are even injected
39
+ 4. **Tamper-evident audit log** — every credential use logged with SHA-256 hash chains
34
40
 
35
41
  ## Quick Start
36
42
 
37
43
  ```bash
38
- npm install -g aquaman-proxy # install the proxy CLI
39
- aquaman setup # stores keys, installs plugin, configures OpenClaw
40
- openclaw # proxy starts automatically
44
+ openclaw plugins install aquaman-plugin # 1. install plugin + proxy
45
+ openclaw aquaman setup # 2. store your API keys
46
+ openclaw # 3. done — proxy starts automatically
41
47
  ```
42
48
 
43
- > `aquaman setup` auto-detects your credential backend. macOS defaults to Keychain,
44
- > Linux defaults to encrypted file. Override with `--backend`:
45
- > `aquaman setup --backend keepassxc`
46
- > Options: `keychain`, `encrypted-file`, `keepassxc`, `1password`, `vault`, `systemd-creds`, `bitwarden`
49
+ > **Using npm?** `npm install -g aquaman-proxy && aquaman setup` does
50
+ > the same thing. Use this if you prefer managing packages with npm.
51
+
52
+ ## Available Commands
53
+
54
+ All commands work via OpenClaw CLI or your terminal:
55
+
56
+ | OpenClaw CLI | Terminal | Description |
57
+ |---|---|---|
58
+ | `openclaw aquaman setup` | `aquaman setup` | Onboarding wizard — stores keys, configures backend |
59
+ | `openclaw aquaman doctor` | `aquaman doctor` | Diagnostic checks with actionable fixes |
60
+ | `openclaw aquaman credentials list` | `aquaman credentials list` | List stored credentials |
61
+ | `openclaw aquaman credentials add` | `aquaman credentials add` | Add a credential (interactive) |
62
+ | `openclaw aquaman policy-list` | `aquaman policy list` | Show request policy rules |
63
+ | `openclaw aquaman audit-tail` | `aquaman audit tail` | Recent audit entries |
64
+ | `openclaw aquaman services-list` | `aquaman services list` | List configured services |
65
+ | `openclaw aquaman status` | `aquaman status` | Proxy status |
47
66
 
48
- Existing plaintext credentials are migrated automatically during setup.
49
- Run again anytime to migrate new credentials: `aquaman migrate openclaw --auto`
67
+ Slash commands in chat: `/aquaman-status`, `/aquaman list`, `/aquaman doctor`
50
68
 
51
- Troubleshooting: `aquaman doctor`
69
+ Troubleshooting: `openclaw aquaman doctor` or `aquaman doctor`
52
70
 
53
71
  ## Config Options
54
72
 
@@ -59,20 +77,20 @@ Troubleshooting: `aquaman doctor`
59
77
  | `backend` | `"keychain"` \| `"1password"` \| `"vault"` \| `"encrypted-file"` \| `"keepassxc"` \| `"systemd-creds"` \| `"bitwarden"` | `"keychain"` | Credential store |
60
78
  | `services` | `string[]` | `["anthropic", "openai"]` | Services to proxy |
61
79
 
62
- > Advanced settings (audit, vault) go in `~/.aquaman/config.yaml`.
80
+ > Advanced settings (audit, vault, request policies) go in `~/.aquaman/config.yaml`. See [request policy docs](https://github.com/tech4242/aquaman#request-policies).
63
81
 
64
- ## Security Audit Note
82
+ ## Security Audit
65
83
 
66
- Running `openclaw security audit --deep` will show two expected findings:
84
+ `openclaw security audit --deep` reports two expected findings:
67
85
 
68
- - **`dangerous-exec`** on `proxy-manager.ts` — the plugin spawns the aquaman proxy as a separate process, which is the whole point of credential isolation.
69
- - **`tools_reachable_permissive_policy`** — advisory that plugin tools are reachable under the default tool policy. This is about your OpenClaw tool profile setting, not about aquaman. Set `"tools": { "profile": "coding" }` in `openclaw.json` if your agents handle untrusted input.
86
+ - **`dangerous-exec`** on `proxy-manager.ts` — the plugin spawns the proxy as a separate process. This is how credential isolation works.
87
+ - **`tools_reachable_permissive_policy`** — advisory about your tool policy, not an aquaman vulnerability. Set `"tools": { "profile": "coding" }` in `openclaw.json` if your agents handle untrusted input.
70
88
 
71
- `aquaman setup` adds the plugin to your `plugins.allow` trust list automatically.
89
+ `aquaman setup` adds the plugin to `plugins.allow` automatically.
72
90
 
73
91
  ## Documentation
74
92
 
75
- See the [main README](https://github.com/tech4242/aquaman#readme) for architecture, Docker deployment, and manual testing.
93
+ See the [main README](https://github.com/tech4242/aquaman#readme) for the full security model, architecture diagrams, request policy config, and manual testing guides.
76
94
 
77
95
  ## License
78
96
 
package/index.ts CHANGED
@@ -16,10 +16,53 @@
16
16
  * - Agent never sees the actual API keys
17
17
  */
18
18
 
19
- import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
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
+ }
20
65
 
21
- // OpenClawPluginDefinition exists in the SDK internals but isn't re-exported from "openclaw/plugin-sdk".
22
- // Mirror the type here until OpenClaw exposes it from the barrel.
23
66
  type OpenClawPluginDefinition = {
24
67
  id?: string;
25
68
  name?: string;
@@ -31,7 +74,7 @@ import * as fs from "node:fs";
31
74
  import * as path from "node:path";
32
75
  import * as os from "node:os";
33
76
  import { HttpInterceptor, createHttpInterceptor } from "./src/http-interceptor.js";
34
- import { createProxyManager, type ProxyManager } from "./src/proxy-manager.js";
77
+ import { createProxyManager, findAquamanProxyBinary, execAquamanProxyCli, execAquamanProxyInteractive, type ProxyManager } from "./src/proxy-manager.js";
35
78
  import { loadHostMap, isProxyRunning, getProxyVersion } from "./src/proxy-health.js";
36
79
 
37
80
  /**
@@ -102,10 +145,10 @@ const FALLBACK_HOST_MAP = new Map<string, string>([
102
145
  ]);
103
146
 
104
147
  /**
105
- * Check if aquaman CLI is installed (fs-based, no shell execution)
148
+ * Check if aquaman proxy binary is available (local node_modules or PATH)
106
149
  */
107
- function isAquamanInstalled(): boolean {
108
- return findInPath("aquaman") !== null;
150
+ function isAquamanProxyInstalled(): boolean {
151
+ return findAquamanProxyBinary() !== null;
109
152
  }
110
153
 
111
154
  /**
@@ -205,7 +248,34 @@ function configureEnvironment(log: OpenClawPluginApi["logger"], services: string
205
248
  }
206
249
 
207
250
  /**
208
- * Register the aquaman_status tool
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)
209
279
  */
210
280
  function registerStatusTool(api: OpenClawPluginApi, services: string[]): void {
211
281
  api.registerTool(
@@ -221,23 +291,7 @@ function registerStatusTool(api: OpenClawPluginApi, services: string[]): void {
221
291
  required: [] as string[],
222
292
  },
223
293
  async execute(_toolCallId: string, _params: unknown) {
224
- const status = {
225
- proxyRunning: proxyManager !== null,
226
- socketPath: socketPath || getDefaultSocketPath(),
227
- services,
228
- httpInterceptorActive: httpInterceptor?.isActive() ?? false,
229
- environmentVariables: Object.fromEntries(
230
- services.map((s) => {
231
- const key =
232
- s === "anthropic"
233
- ? "ANTHROPIC_BASE_URL"
234
- : s === "openai"
235
- ? "OPENAI_BASE_URL"
236
- : `${s.toUpperCase()}_BASE_URL`;
237
- return [key, process.env[key] ?? null];
238
- })
239
- ),
240
- };
294
+ const status = getStatus(services);
241
295
  return {
242
296
  content: [{ type: "text" as const, text: JSON.stringify(status, null, 2) }],
243
297
  details: status,
@@ -299,9 +353,9 @@ function ensureAuthProfiles(log: OpenClawPluginApi["logger"], services: string[]
299
353
  */
300
354
  const plugin: OpenClawPluginDefinition = {
301
355
  id: 'aquaman-plugin',
302
- name: 'Aquaman Vault',
356
+ name: 'Aquaman — API Key Protection',
303
357
  version: PLUGIN_VERSION,
304
- description: 'Credential isolation for OpenClaw — API keys never enter the agent process',
358
+ description: 'API key protection for OpenClaw — credentials stay in your vault, never in the agent\'s memory',
305
359
 
306
360
  register(api) {
307
361
  api.logger.info("Aquaman plugin loaded");
@@ -313,70 +367,76 @@ const plugin: OpenClawPluginDefinition = {
313
367
  // Auto-generate auth-profiles.json if missing
314
368
  ensureAuthProfiles(api.logger, configuredServices);
315
369
 
316
- // Local proxy mode requires aquaman CLI
317
- if (!isAquamanInstalled()) {
370
+ // Check if aquaman proxy binary is available
371
+ const proxyAvailable = isAquamanProxyInstalled();
372
+
373
+ if (!proxyAvailable) {
318
374
  api.logger.warn(
319
- "aquaman CLI not found. Install with: npm install -g aquaman-proxy"
375
+ "aquaman proxy not found. Install with: npm install -g aquaman-proxy"
320
376
  );
321
377
  api.logger.warn(
322
378
  "Then run: aquaman setup"
323
379
  );
324
- configureEnvironment(api.logger, configuredServices);
325
- return;
326
- }
327
-
328
- api.logger.info("aquaman CLI found, will start proxy on gateway start");
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");
329
384
 
330
- // Configure environment variables immediately (sentinel hostname)
331
- configureEnvironment(api.logger, configuredServices);
332
-
333
- // Register service for proxy lifecycle management
334
- api.registerService({
335
- id: 'aquaman-proxy',
336
- async start(ctx) {
337
- ctx.logger.info("Starting aquaman proxy...");
338
-
339
- const started = await startProxy(ctx.logger);
340
- if (started && socketPath) {
341
- ctx.logger.info("Aquaman proxy started successfully");
342
-
343
- // Check for version mismatch between plugin and proxy
344
- const proxyVersion = await getProxyVersion(socketPath);
345
- if (proxyVersion && proxyVersion !== PLUGIN_VERSION) {
346
- ctx.logger.warn(
347
- `Warning: plugin version ${PLUGIN_VERSION} \u2260 proxy version ${proxyVersion}. ` +
348
- `Update both: npm install -g aquaman-proxy && openclaw plugins install aquaman-plugin`
349
- );
350
- }
385
+ // Configure environment variables immediately (sentinel hostname)
386
+ configureEnvironment(api.logger, configuredServices);
351
387
 
352
- // Activate HTTP interceptor to redirect channel traffic through proxy
353
- activateHttpInterceptor(ctx.logger);
354
- } else {
355
- ctx.logger.error("Failed to start aquaman proxy");
356
- // Check if another instance is already running
357
- const defaultSock = getDefaultSocketPath();
358
- const alreadyRunning = await isProxyRunning(defaultSock);
359
- if (alreadyRunning) {
360
- socketPath = defaultSock;
361
- ctx.logger.info(
362
- "Another aquaman instance is already running using it"
363
- );
364
- // Load host map from existing proxy
365
- const map = await loadHostMap(defaultSock);
366
- dynamicHostMap = map.size > 0 ? map : FALLBACK_HOST_MAP;
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
367
408
  activateHttpInterceptor(ctx.logger);
368
409
  } else {
369
- ctx.logger.error(
370
- "No running proxy found. Check: aquaman doctor"
371
- );
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
+ }
372
428
  }
429
+ },
430
+ async stop(ctx) {
431
+ ctx.logger.info("Stopping aquaman proxy...");
432
+ stopProxy();
373
433
  }
374
- },
375
- async stop(ctx) {
376
- ctx.logger.info("Stopping aquaman proxy...");
377
- stopProxy();
378
- }
379
- });
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.
380
440
 
381
441
  // Register /aquaman-status slash command for humans
382
442
  api.registerCommand({
@@ -385,12 +445,7 @@ const plugin: OpenClawPluginDefinition = {
385
445
  acceptsArgs: false,
386
446
  requireAuth: true,
387
447
  async handler() {
388
- const status = {
389
- proxyRunning: proxyManager !== null,
390
- socketPath: socketPath || getDefaultSocketPath(),
391
- services: configuredServices,
392
- httpInterceptorActive: httpInterceptor?.isActive() ?? false,
393
- };
448
+ const status = getStatus(configuredServices);
394
449
  return { text: JSON.stringify(status, null, 2) };
395
450
  }
396
451
  });
@@ -401,40 +456,128 @@ const plugin: OpenClawPluginDefinition = {
401
456
  ({ program }) => {
402
457
  const aquamanCmd = program
403
458
  .command("aquaman")
404
- .description("Aquaman credential management");
459
+ .description("Aquaman API key protection");
405
460
 
406
461
  aquamanCmd
407
462
  .command("status")
408
463
  .description("Show aquaman proxy status")
409
464
  .action(() => {
465
+ const status = getStatus(configuredServices);
410
466
  console.log("\nAquaman Status:");
411
- console.log(` Proxy running: ${proxyManager !== null}`);
412
- console.log(` Socket path: ${socketPath || getDefaultSocketPath()}`);
467
+ console.log(` Proxy binary: ${status.cliInstalled ? "found" : "NOT FOUND"}`);
468
+ console.log(` Proxy running: ${status.proxyRunning}`);
469
+ console.log(` Socket path: ${status.socketPath}`);
413
470
  console.log(` Services: ${configuredServices.join(", ")}`);
414
- console.log("\nEnvironment Variables:");
415
- for (const service of configuredServices) {
416
- const envKey =
417
- service === "anthropic"
418
- ? "ANTHROPIC_BASE_URL"
419
- : service === "openai"
420
- ? "OPENAI_BASE_URL"
421
- : `${service.toUpperCase()}_BASE_URL`;
422
- console.log(` ${envKey}=${process.env[envKey] ?? "(not set)"}`);
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
+ }
423
485
  }
424
486
  });
425
487
 
426
488
  aquamanCmd
427
- .command("add <service> [key]")
428
- .description("Add a credential (opens secure prompt)")
429
- .action((service: string, key: string = "api_key") => {
430
- console.log(`\n Run in your terminal:\n aquaman credentials add ${service} ${key}\n`);
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
+ }
431
498
  });
432
499
 
433
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
434
520
  .command("list")
435
521
  .description("List stored credentials")
436
- .action(() => {
437
- console.log(`\n Run in your terminal:\n aquaman credentials list\n`);
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
+ }
438
581
  });
439
582
  },
440
583
  { commands: ["aquaman"] }
@@ -1,7 +1,11 @@
1
1
  {
2
2
  "id": "aquaman-plugin",
3
- "name": "Aquaman Vault",
4
- "description": "Credential isolation - API keys never touch the agent process",
3
+ "name": "Aquaman — API Key Protection",
4
+ "version": "0.11.0",
5
+ "description": "Protect your API keys, tokens, and secrets — they stay in your vault (Keychain, 1Password, HashiCorp Vault, Bitwarden, and more), never in the agent's memory. Block dangerous API endpoints before credentials are injected. Works with 25+ services across 6 auth modes.",
6
+ "author": "tech4242",
7
+ "repository": "https://github.com/tech4242/aquaman",
8
+ "license": "MIT",
5
9
  "permissions": {
6
10
  "env:write": ["*_BASE_URL", "GITHUB_API_URL"],
7
11
  "process:spawn": ["aquaman"],
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "aquaman-plugin",
3
- "version": "0.9.2",
4
- "description": "Credential isolation plugin for OpenClaw",
3
+ "version": "0.11.0",
4
+ "description": "Protect API keys and secrets for OpenClaw — credentials stay in your vault, never in the agent's memory",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "build": "echo 'OpenClaw runs TypeScript directly - no build needed'",
@@ -18,21 +18,23 @@
18
18
  "security",
19
19
  "credentials",
20
20
  "vault",
21
- "1password"
21
+ "1password",
22
+ "api-key",
23
+ "secrets",
24
+ "keychain",
25
+ "bitwarden",
26
+ "credential-isolation",
27
+ "token-security",
28
+ "api-protection"
22
29
  ],
23
30
  "author": "tech4242",
24
31
  "license": "MIT",
25
32
  "dependencies": {
26
- "undici": "^7.0.0"
33
+ "undici": "^7.0.0",
34
+ "aquaman-proxy": "0.11.0"
27
35
  },
28
36
  "peerDependencies": {
29
- "openclaw": ">=2026.1.11",
30
- "aquaman-proxy": "0.9.2"
31
- },
32
- "peerDependenciesMeta": {
33
- "aquaman-proxy": {
34
- "optional": true
35
- }
37
+ "openclaw": ">=2026.4.7"
36
38
  },
37
39
  "devDependencies": {
38
40
  "@types/node": "^20.10.0",
package/src/commands.ts CHANGED
@@ -2,9 +2,13 @@
2
2
  * Slash commands for the OpenClaw plugin
3
3
  *
4
4
  * Provides /aquaman commands for users to interact with the plugin.
5
+ * Non-interactive commands execute the aquaman proxy binary directly.
6
+ * Interactive commands (add) show instructions since slash commands
7
+ * run in the chat UI where TTY is not available.
5
8
  */
6
9
 
7
10
  import type { ProxyManager } from './proxy-manager.js';
11
+ import { execAquamanProxyCli, findAquamanProxyBinary } from './proxy-manager.js';
8
12
  import type { PluginConfig } from './config-schema.js';
9
13
 
10
14
  export interface CommandContext {
@@ -32,11 +36,13 @@ export interface PluginCommand {
32
36
  */
33
37
  export async function statusCommand(ctx: CommandContext): Promise<CommandResult> {
34
38
  const lines: string[] = [];
39
+ const cliInstalled = findAquamanProxyBinary() !== null;
35
40
 
36
41
  lines.push('aquaman plugin status');
37
42
  lines.push('');
38
43
  lines.push(`Backend: ${ctx.config.backend || 'keychain'}`);
39
44
  lines.push(`Services: ${(ctx.config.services || []).join(', ')}`);
45
+ lines.push(`Proxy binary: ${cliInstalled ? 'found' : 'NOT FOUND'}`);
40
46
 
41
47
  if (ctx.proxyManager?.isRunning()) {
42
48
  const info = ctx.proxyManager.getConnectionInfo();
@@ -46,6 +52,10 @@ export async function statusCommand(ctx: CommandContext): Promise<CommandResult>
46
52
  } else {
47
53
  lines.push('');
48
54
  lines.push('Proxy Status: Not running');
55
+ if (!cliInstalled) {
56
+ lines.push('');
57
+ lines.push('Setup: npm install -g aquaman-proxy && aquaman setup');
58
+ }
49
59
  }
50
60
 
51
61
  return {
@@ -55,7 +65,7 @@ export async function statusCommand(ctx: CommandContext): Promise<CommandResult>
55
65
  }
56
66
 
57
67
  /**
58
- * /aquaman add <service> - Add a credential (prompts for value)
68
+ * /aquaman add <service> - Add a credential (shows instructions — TTY not available in chat UI)
59
69
  */
60
70
  export async function addCommand(
61
71
  _ctx: CommandContext,
@@ -65,7 +75,8 @@ export async function addCommand(
65
75
  return {
66
76
  success: true,
67
77
  message: `To add a credential for ${service}/${key}:\n\n` +
68
- `Run: aquaman credentials add ${service} ${key}\n\n` +
78
+ `Run: openclaw aquaman credentials add ${service} ${key}\n` +
79
+ `Or in terminal: aquaman credentials add ${service} ${key}\n\n` +
69
80
  `Or configure via environment variables:\n` +
70
81
  ` export AQUAMAN_${service.toUpperCase()}_${key.toUpperCase()}=<your-key>`
71
82
  };
@@ -75,30 +86,72 @@ export async function addCommand(
75
86
  * /aquaman list - List stored credentials
76
87
  */
77
88
  export async function listCommand(_ctx: CommandContext): Promise<CommandResult> {
78
- return {
79
- success: true,
80
- message: 'Run in your terminal:\n aquaman credentials list'
81
- };
89
+ try {
90
+ const result = await execAquamanProxyCli(['credentials', 'list']);
91
+ return { success: result.exitCode === 0, message: result.stdout || result.stderr };
92
+ } catch (err: any) {
93
+ return { success: false, message: `Failed: ${err.message}\n\nRun in terminal: aquaman credentials list` };
94
+ }
95
+ }
96
+
97
+ /**
98
+ * /aquaman doctor - Run diagnostic checks
99
+ */
100
+ export async function doctorCommand(_ctx: CommandContext): Promise<CommandResult> {
101
+ try {
102
+ const result = await execAquamanProxyCli(['doctor']);
103
+ return { success: result.exitCode === 0, message: result.stdout || result.stderr };
104
+ } catch (err: any) {
105
+ return { success: false, message: `Failed: ${err.message}\n\nRun in terminal: aquaman doctor` };
106
+ }
82
107
  }
83
108
 
84
109
  /**
85
110
  * /aquaman logs - Show recent audit entries
86
111
  */
87
- export async function logsCommand(_ctx: CommandContext, _count: number = 10): Promise<CommandResult> {
88
- return {
89
- success: true,
90
- message: 'Run in your terminal:\n aquaman audit tail'
91
- };
112
+ export async function logsCommand(_ctx: CommandContext, count: number = 10): Promise<CommandResult> {
113
+ try {
114
+ const result = await execAquamanProxyCli(['audit', 'tail', '-n', String(count)]);
115
+ return { success: result.exitCode === 0, message: result.stdout || result.stderr };
116
+ } catch (err: any) {
117
+ return { success: false, message: `Failed: ${err.message}\n\nRun in terminal: aquaman audit tail` };
118
+ }
92
119
  }
93
120
 
94
121
  /**
95
122
  * /aquaman verify - Verify audit log integrity
96
123
  */
97
124
  export async function verifyCommand(_ctx: CommandContext): Promise<CommandResult> {
98
- return {
99
- success: true,
100
- message: 'Run in your terminal:\n aquaman audit verify'
101
- };
125
+ try {
126
+ const result = await execAquamanProxyCli(['audit', 'verify']);
127
+ return { success: result.exitCode === 0, message: result.stdout || result.stderr };
128
+ } catch (err: any) {
129
+ return { success: false, message: `Failed: ${err.message}\n\nRun in terminal: aquaman audit verify` };
130
+ }
131
+ }
132
+
133
+ /**
134
+ * /aquaman policy - List policy rules
135
+ */
136
+ export async function policyListCommand(_ctx: CommandContext): Promise<CommandResult> {
137
+ try {
138
+ const result = await execAquamanProxyCli(['policy', 'list']);
139
+ return { success: result.exitCode === 0, message: result.stdout || result.stderr };
140
+ } catch (err: any) {
141
+ return { success: false, message: `Failed: ${err.message}\n\nRun in terminal: aquaman policy list` };
142
+ }
143
+ }
144
+
145
+ /**
146
+ * /aquaman services - List configured services
147
+ */
148
+ export async function servicesListCommand(_ctx: CommandContext): Promise<CommandResult> {
149
+ try {
150
+ const result = await execAquamanProxyCli(['services', 'list']);
151
+ return { success: result.exitCode === 0, message: result.stdout || result.stderr };
152
+ } catch (err: any) {
153
+ return { success: false, message: `Failed: ${err.message}\n\nRun in terminal: aquaman services list` };
154
+ }
102
155
  }
103
156
 
104
157
  /**
@@ -122,6 +175,9 @@ export async function executeCommand(
122
175
  case 'list':
123
176
  return listCommand(ctx);
124
177
 
178
+ case 'doctor':
179
+ return doctorCommand(ctx);
180
+
125
181
  case 'logs': {
126
182
  const count = args[0] ? parseInt(args[0], 10) : 10;
127
183
  return logsCommand(ctx, count);
@@ -130,18 +186,37 @@ export async function executeCommand(
130
186
  case 'verify':
131
187
  return verifyCommand(ctx);
132
188
 
189
+ case 'policy':
190
+ return policyListCommand(ctx);
191
+
192
+ case 'services':
193
+ return servicesListCommand(ctx);
194
+
133
195
  case 'help':
134
196
  default:
135
197
  return {
136
198
  success: true,
137
199
  message: `aquaman plugin commands:
138
200
 
139
- /aquaman status - Show plugin status
140
- /aquaman add - Add a credential (shows instructions)
201
+ /aquaman status - Show plugin and proxy status
202
+ /aquaman doctor - Run diagnostic checks
203
+ /aquaman add - Add a credential
141
204
  /aquaman list - List stored credentials
205
+ /aquaman policy - List request policy rules
206
+ /aquaman services - List configured services
142
207
  /aquaman logs [n] - Show recent audit entries
143
208
  /aquaman verify - Verify audit log integrity
144
- /aquaman help - Show this help message`
209
+ /aquaman help - Show this help message
210
+
211
+ CLI commands (via terminal or openclaw aquaman <cmd>):
212
+
213
+ openclaw aquaman setup - Run the setup wizard
214
+ openclaw aquaman doctor - Diagnose issues
215
+ openclaw aquaman credentials list - List credentials
216
+ openclaw aquaman credentials add - Add a credential (interactive)
217
+ openclaw aquaman policy-list - Show policy rules
218
+ openclaw aquaman audit-tail - Recent audit entries
219
+ openclaw aquaman services-list - List services`
145
220
  };
146
221
  }
147
222
  }
@@ -180,6 +255,30 @@ export function getAvailableCommands(ctx: CommandContext): PluginCommand[] {
180
255
  return result.message;
181
256
  }
182
257
  },
258
+ {
259
+ name: 'doctor',
260
+ description: 'Run diagnostic checks',
261
+ execute: async () => {
262
+ const result = await doctorCommand(ctx);
263
+ return result.message;
264
+ }
265
+ },
266
+ {
267
+ name: 'policy',
268
+ description: 'List request policy rules',
269
+ execute: async () => {
270
+ const result = await policyListCommand(ctx);
271
+ return result.message;
272
+ }
273
+ },
274
+ {
275
+ name: 'services',
276
+ description: 'List configured services',
277
+ execute: async () => {
278
+ const result = await servicesListCommand(ctx);
279
+ return result.message;
280
+ }
281
+ },
183
282
  {
184
283
  name: 'logs',
185
284
  description: 'Show recent audit log entries',
@@ -9,8 +9,106 @@ import { spawn, type ChildProcess } from 'node:child_process';
9
9
  import * as path from 'node:path';
10
10
  import * as fs from 'node:fs';
11
11
  import * as os from 'node:os';
12
+ import { fileURLToPath } from 'node:url';
12
13
  import type { PluginConfig } from './config-schema.js';
13
14
 
15
+ /**
16
+ * Find the aquaman proxy binary.
17
+ *
18
+ * Search order:
19
+ * 1. Plugin's own node_modules/.bin/aquaman (bundled dep — version-matched)
20
+ * 2. PATH (global install via npm install -g aquaman-proxy)
21
+ */
22
+ export function findAquamanProxyBinary(): string | null {
23
+ // 1. Resolve from this file's location → plugin package root → node_modules/.bin/
24
+ const thisDir = path.dirname(fileURLToPath(import.meta.url));
25
+ const pluginRoot = path.resolve(thisDir, '..');
26
+ const localBin = path.join(pluginRoot, 'node_modules', '.bin', 'aquaman');
27
+ if (fs.existsSync(localBin)) {
28
+ return localBin;
29
+ }
30
+
31
+ // 2. Search PATH
32
+ const pathEnv = process.env.PATH || '';
33
+ const dirs = pathEnv.split(path.delimiter);
34
+ for (const dir of dirs) {
35
+ const candidate = path.join(dir, 'aquaman');
36
+ try {
37
+ fs.accessSync(candidate, fs.constants.X_OK);
38
+ return candidate;
39
+ } catch {
40
+ // Not found in this dir
41
+ }
42
+ }
43
+
44
+ return null;
45
+ }
46
+
47
+ /**
48
+ * Execute an aquaman proxy CLI command (non-interactive).
49
+ * Captures stdout/stderr and returns them.
50
+ */
51
+ export function execAquamanProxyCli(
52
+ args: string[],
53
+ options?: { timeoutMs?: number },
54
+ ): Promise<{ stdout: string; stderr: string; exitCode: number }> {
55
+ return new Promise((resolve, reject) => {
56
+ const binary = findAquamanProxyBinary();
57
+ if (!binary) {
58
+ reject(new Error('aquaman proxy binary not found. Install with: npm install -g aquaman-proxy'));
59
+ return;
60
+ }
61
+
62
+ const proc = spawn(binary, args, {
63
+ stdio: ['ignore', 'pipe', 'pipe'],
64
+ env: process.env,
65
+ });
66
+
67
+ let stdout = '';
68
+ let stderr = '';
69
+
70
+ proc.stdout?.on('data', (d: Buffer) => { stdout += d.toString(); });
71
+ proc.stderr?.on('data', (d: Buffer) => { stderr += d.toString(); });
72
+
73
+ proc.on('error', reject);
74
+ proc.on('close', (code) => {
75
+ resolve({ stdout, stderr, exitCode: code ?? 1 });
76
+ });
77
+
78
+ const timeout = options?.timeoutMs ?? 30_000;
79
+ const timer = setTimeout(() => {
80
+ proc.kill('SIGTERM');
81
+ reject(new Error(`aquaman CLI timed out after ${timeout}ms`));
82
+ }, timeout);
83
+
84
+ proc.on('close', () => clearTimeout(timer));
85
+ });
86
+ }
87
+
88
+ /**
89
+ * Execute an aquaman proxy CLI command interactively (stdio: inherit).
90
+ * Used for commands that need TTY input (setup, credentials add).
91
+ */
92
+ export function execAquamanProxyInteractive(
93
+ args: string[],
94
+ ): Promise<number> {
95
+ return new Promise((resolve, reject) => {
96
+ const binary = findAquamanProxyBinary();
97
+ if (!binary) {
98
+ reject(new Error('aquaman proxy binary not found. Install with: npm install -g aquaman-proxy'));
99
+ return;
100
+ }
101
+
102
+ const proc = spawn(binary, args, {
103
+ stdio: 'inherit',
104
+ env: process.env,
105
+ });
106
+
107
+ proc.on('error', reject);
108
+ proc.on('close', (code) => resolve(code ?? 1));
109
+ });
110
+ }
111
+
14
112
  export interface ProxyConnectionInfo {
15
113
  ready: boolean;
16
114
  socketPath: string;
@@ -66,7 +164,7 @@ export class ProxyManager {
66
164
  const config = this.options.config;
67
165
 
68
166
  // Find aquaman binary
69
- const binaryPath = this.findAquamanBinary();
167
+ const binaryPath = this.findBinary();
70
168
 
71
169
  if (!binaryPath) {
72
170
  const error = new Error(
@@ -199,45 +297,10 @@ export class ProxyManager {
199
297
  }
200
298
 
201
299
  /**
202
- * Find the aquaman binary
300
+ * Find the aquaman proxy binary
203
301
  */
204
- private findAquamanBinary(): string | null {
205
- // Check common locations
206
- const locations = [
207
- // In node_modules
208
- path.join(process.cwd(), 'node_modules', '.bin', 'aquaman'),
209
- path.join(process.cwd(), 'node_modules', '@aquaman', 'proxy', 'dist', 'cli', 'index.js'),
210
-
211
- // Global install
212
- '/usr/local/bin/aquaman',
213
-
214
- // In PATH (will use which in spawn)
215
- 'aquaman'
216
- ];
217
-
218
- for (const loc of locations) {
219
- if (loc === 'aquaman') {
220
- // Check if in PATH using filesystem checks (no shell execution)
221
- const pathEnv = process.env.PATH || '';
222
- const dirs = pathEnv.split(path.delimiter);
223
- for (const dir of dirs) {
224
- const candidate = path.join(dir, 'aquaman');
225
- try {
226
- fs.accessSync(candidate, fs.constants.X_OK);
227
- return 'aquaman';
228
- } catch {
229
- // Not found in this dir
230
- }
231
- }
232
- continue;
233
- }
234
-
235
- if (fs.existsSync(loc)) {
236
- return loc;
237
- }
238
- }
239
-
240
- return null;
302
+ private findBinary(): string | null {
303
+ return findAquamanProxyBinary();
241
304
  }
242
305
  }
243
306