aquaman-plugin 0.9.1 → 0.10.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.
Files changed (3) hide show
  1. package/README.md +25 -28
  2. package/index.ts +135 -92
  3. package/package.json +3 -3
package/README.md CHANGED
@@ -1,19 +1,18 @@
1
1
  # aquaman-plugin
2
2
 
3
- OpenClaw Gateway plugin for [aquaman](https://github.com/tech4242/aquaman) credential isolation.
4
-
5
- ## How It Works
3
+ OpenClaw Gateway plugin for [aquaman](https://github.com/tech4242/aquaman) credential isolation for OpenClaw.
6
4
 
7
5
  ```
8
6
  Agent / OpenClaw Gateway Aquaman Proxy
9
7
  ┌──────────────────────┐ ┌──────────────────────┐
10
8
  │ │ │ │
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
9
+ │ ANTHROPIC_BASE_URL │══ Unix ═════>│ Keychain / 1Pass / │
10
+ │ = aquaman.local │ Domain │ Vault / Encrypted │
11
+ │ │<═ Socket ════│
12
+ │ fetch() interceptor │══ (UDS) ════>│ + Policy enforced
13
+ │ redirects channel │ │ + Auth injected:
14
+ │ API traffic │ │ header / url-path
15
+ │ │ │ basic / oauth │
17
16
  │ │ │ │
18
17
  │ No credentials. │ ~/.aquaman/ │ │
19
18
  │ No open ports. │ proxy.sock │ │
@@ -30,24 +29,22 @@ Agent / OpenClaw Gateway Aquaman Proxy
30
29
  slack.com/api ...
31
30
  ```
32
31
 
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`.
32
+ This plugin is the left side it runs inside the Gateway process and routes all LLM and channel API traffic through the aquaman proxy via Unix domain socket. Credentials never enter the agent's address space.
33
+
34
+ **What it does on load:**
35
+ 1. Sets `ANTHROPIC_BASE_URL` / `OPENAI_BASE_URL` to `http://aquaman.local/<service>` (routed to UDS)
36
+ 2. Spawns the proxy daemon via `ProxyManager`
37
+ 3. Activates a `globalThis.fetch` interceptor to redirect channel API traffic through the proxy
38
+ 4. Registers `/aquaman-status` command and `aquaman_status` tool
34
39
 
35
40
  ## Quick Start
36
41
 
37
42
  ```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
43
+ npm install -g aquaman-proxy
44
+ aquaman setup # stores keys, installs this plugin, applies policy defaults
45
+ openclaw # proxy starts automatically
41
46
  ```
42
47
 
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`
47
-
48
- Existing plaintext credentials are migrated automatically during setup.
49
- Run again anytime to migrate new credentials: `aquaman migrate openclaw --auto`
50
-
51
48
  Troubleshooting: `aquaman doctor`
52
49
 
53
50
  ## Config Options
@@ -59,20 +56,20 @@ Troubleshooting: `aquaman doctor`
59
56
  | `backend` | `"keychain"` \| `"1password"` \| `"vault"` \| `"encrypted-file"` \| `"keepassxc"` \| `"systemd-creds"` \| `"bitwarden"` | `"keychain"` | Credential store |
60
57
  | `services` | `string[]` | `["anthropic", "openai"]` | Services to proxy |
61
58
 
62
- > Advanced settings (audit, vault) go in `~/.aquaman/config.yaml`.
59
+ > Advanced settings (audit, vault, request policies) go in `~/.aquaman/config.yaml`. See [request policy docs](https://github.com/tech4242/aquaman#request-policies).
63
60
 
64
- ## Security Audit Note
61
+ ## Security Audit
65
62
 
66
- Running `openclaw security audit --deep` will show two expected findings:
63
+ `openclaw security audit --deep` reports two expected findings:
67
64
 
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.
65
+ - **`dangerous-exec`** on `proxy-manager.ts` — the plugin spawns the proxy as a separate process. This is how credential isolation works.
66
+ - **`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
67
 
71
- `aquaman setup` adds the plugin to your `plugins.allow` trust list automatically.
68
+ `aquaman setup` adds the plugin to `plugins.allow` automatically.
72
69
 
73
70
  ## Documentation
74
71
 
75
- See the [main README](https://github.com/tech4242/aquaman#readme) for architecture, Docker deployment, and manual testing.
72
+ See the [main README](https://github.com/tech4242/aquaman#readme) for the full security model, architecture diagrams, and manual testing guides.
76
73
 
77
74
  ## License
78
75
 
package/index.ts CHANGED
@@ -17,6 +17,16 @@
17
17
  */
18
18
 
19
19
  import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
20
+
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
+ type OpenClawPluginDefinition = {
24
+ id?: string;
25
+ name?: string;
26
+ description?: string;
27
+ version?: string;
28
+ register?: (api: OpenClawPluginApi) => void | Promise<void>;
29
+ };
20
30
  import * as fs from "node:fs";
21
31
  import * as path from "node:path";
22
32
  import * as os from "node:os";
@@ -52,7 +62,7 @@ let proxyManager: ProxyManager | null = null;
52
62
  let httpInterceptor: HttpInterceptor | null = null;
53
63
  let socketPath: string | null = null;
54
64
  let dynamicHostMap: Map<string, string> | null = null;
55
- const services = ["anthropic", "openai"];
65
+ let configuredServices: string[] = ["anthropic", "openai"];
56
66
 
57
67
  /** Default socket path */
58
68
  function getDefaultSocketPath(): string {
@@ -169,7 +179,7 @@ function activateHttpInterceptor(log: OpenClawPluginApi["logger"]): void {
169
179
  /**
170
180
  * Set environment variables for SDK clients using sentinel hostname
171
181
  */
172
- function configureEnvironment(log: OpenClawPluginApi["logger"]): void {
182
+ function configureEnvironment(log: OpenClawPluginApi["logger"], services: string[]): void {
173
183
  for (const service of services) {
174
184
  const serviceUrl = `http://aquaman.local/${service}`;
175
185
 
@@ -197,11 +207,12 @@ function configureEnvironment(log: OpenClawPluginApi["logger"]): void {
197
207
  /**
198
208
  * Register the aquaman_status tool
199
209
  */
200
- function registerStatusTool(api: OpenClawPluginApi): void {
210
+ function registerStatusTool(api: OpenClawPluginApi, services: string[]): void {
201
211
  api.registerTool(
202
212
  () => {
203
213
  return {
204
214
  name: "aquaman_status",
215
+ label: "Aquaman Status",
205
216
  description:
206
217
  "Check aquaman credential proxy status and configured services",
207
218
  parameters: {
@@ -209,8 +220,8 @@ function registerStatusTool(api: OpenClawPluginApi): void {
209
220
  properties: {},
210
221
  required: [] as string[],
211
222
  },
212
- async execute() {
213
- return {
223
+ async execute(_toolCallId: string, _params: unknown) {
224
+ const status = {
214
225
  proxyRunning: proxyManager !== null,
215
226
  socketPath: socketPath || getDefaultSocketPath(),
216
227
  services,
@@ -227,6 +238,10 @@ function registerStatusTool(api: OpenClawPluginApi): void {
227
238
  })
228
239
  ),
229
240
  };
241
+ return {
242
+ content: [{ type: "text" as const, text: JSON.stringify(status, null, 2) }],
243
+ details: status,
244
+ };
230
245
  },
231
246
  };
232
247
  },
@@ -239,7 +254,7 @@ function registerStatusTool(api: OpenClawPluginApi): void {
239
254
  * OpenClaw checks its auth store before making API calls — without a placeholder
240
255
  * key, requests are rejected before they ever reach the proxy.
241
256
  */
242
- function ensureAuthProfiles(log: OpenClawPluginApi["logger"]): void {
257
+ function ensureAuthProfiles(log: OpenClawPluginApi["logger"], services: string[]): void {
243
258
  const stateDir =
244
259
  process.env.OPENCLAW_STATE_DIR ||
245
260
  path.join(os.homedir(), ".openclaw");
@@ -280,127 +295,155 @@ function ensureAuthProfiles(log: OpenClawPluginApi["logger"]): void {
280
295
  }
281
296
 
282
297
  /**
283
- * OpenClaw plugin register function
298
+ * Aquaman OpenClaw Plugin Definition
284
299
  */
285
- export default function register(api: OpenClawPluginApi): void {
286
- api.logger.info("Aquaman plugin loaded");
287
-
288
- // Auto-generate auth-profiles.json if missing
289
- ensureAuthProfiles(api.logger);
290
-
291
- // Local proxy mode — requires aquaman CLI
292
- if (!isAquamanInstalled()) {
293
- api.logger.warn(
294
- "aquaman CLI not found. Install with: npm install -g aquaman-proxy"
295
- );
296
- api.logger.warn(
297
- "Then run: aquaman setup"
298
- );
299
- configureEnvironment(api.logger);
300
- return;
301
- }
300
+ const plugin: OpenClawPluginDefinition = {
301
+ id: 'aquaman-plugin',
302
+ name: 'Aquaman Vault',
303
+ version: PLUGIN_VERSION,
304
+ description: 'Credential isolation for OpenClaw — API keys never enter the agent process',
305
+
306
+ register(api) {
307
+ api.logger.info("Aquaman plugin loaded");
308
+
309
+ // Read services from plugin config
310
+ const pluginCfg = api.pluginConfig as { backend?: string; services?: string[] } | undefined;
311
+ configuredServices = pluginCfg?.services ?? ["anthropic", "openai"];
312
+
313
+ // Auto-generate auth-profiles.json if missing
314
+ ensureAuthProfiles(api.logger, configuredServices);
315
+
316
+ // Local proxy mode — requires aquaman CLI
317
+ if (!isAquamanInstalled()) {
318
+ api.logger.warn(
319
+ "aquaman CLI not found. Install with: npm install -g aquaman-proxy"
320
+ );
321
+ api.logger.warn(
322
+ "Then run: aquaman setup"
323
+ );
324
+ configureEnvironment(api.logger, configuredServices);
325
+ return;
326
+ }
302
327
 
303
- api.logger.info("aquaman CLI found, will start proxy on gateway start");
328
+ api.logger.info("aquaman CLI found, will start proxy on gateway start");
304
329
 
305
- // Configure environment variables immediately (sentinel hostname)
306
- configureEnvironment(api.logger);
330
+ // Configure environment variables immediately (sentinel hostname)
331
+ configureEnvironment(api.logger, configuredServices);
307
332
 
308
- // Register lifecycle hooks if available
309
- if (api.registerLifecycle) {
310
- api.registerLifecycle({
311
- async onGatewayStart() {
312
- api.logger.info("Starting aquaman proxy...");
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...");
313
338
 
314
- const started = await startProxy(api.logger);
339
+ const started = await startProxy(ctx.logger);
315
340
  if (started && socketPath) {
316
- api.logger.info("Aquaman proxy started successfully");
341
+ ctx.logger.info("Aquaman proxy started successfully");
317
342
 
318
343
  // Check for version mismatch between plugin and proxy
319
344
  const proxyVersion = await getProxyVersion(socketPath);
320
345
  if (proxyVersion && proxyVersion !== PLUGIN_VERSION) {
321
- api.logger.warn(
346
+ ctx.logger.warn(
322
347
  `Warning: plugin version ${PLUGIN_VERSION} \u2260 proxy version ${proxyVersion}. ` +
323
348
  `Update both: npm install -g aquaman-proxy && openclaw plugins install aquaman-plugin`
324
349
  );
325
350
  }
326
351
 
327
352
  // Activate HTTP interceptor to redirect channel traffic through proxy
328
- activateHttpInterceptor(api.logger);
353
+ activateHttpInterceptor(ctx.logger);
329
354
  } else {
330
- api.logger.error("Failed to start aquaman proxy");
355
+ ctx.logger.error("Failed to start aquaman proxy");
331
356
  // Check if another instance is already running
332
357
  const defaultSock = getDefaultSocketPath();
333
358
  const alreadyRunning = await isProxyRunning(defaultSock);
334
359
  if (alreadyRunning) {
335
360
  socketPath = defaultSock;
336
- api.logger.info(
361
+ ctx.logger.info(
337
362
  "Another aquaman instance is already running — using it"
338
363
  );
339
364
  // Load host map from existing proxy
340
365
  const map = await loadHostMap(defaultSock);
341
366
  dynamicHostMap = map.size > 0 ? map : FALLBACK_HOST_MAP;
342
- activateHttpInterceptor(api.logger);
367
+ activateHttpInterceptor(ctx.logger);
343
368
  } else {
344
- api.logger.error(
369
+ ctx.logger.error(
345
370
  "No running proxy found. Check: aquaman doctor"
346
371
  );
347
372
  }
348
373
  }
349
374
  },
350
-
351
- async onGatewayStop() {
352
- api.logger.info("Stopping aquaman proxy...");
375
+ async stop(ctx) {
376
+ ctx.logger.info("Stopping aquaman proxy...");
353
377
  stopProxy();
354
- },
378
+ }
355
379
  });
356
- }
357
380
 
358
- // Register CLI commands if available
359
- if (api.registerCli) {
360
- api.registerCli(
361
- ({ program }) => {
362
- const aquamanCmd = program
363
- .command("aquaman")
364
- .description("Aquaman credential management");
365
-
366
- aquamanCmd
367
- .command("status")
368
- .description("Show aquaman proxy status")
369
- .action(() => {
370
- console.log("\nAquaman Status:");
371
- console.log(` Proxy running: ${proxyManager !== null}`);
372
- console.log(` Socket path: ${socketPath || getDefaultSocketPath()}`);
373
- console.log(` Services: ${services.join(", ")}`);
374
- console.log("\nEnvironment Variables:");
375
- for (const service of services) {
376
- const envKey =
377
- service === "anthropic"
378
- ? "ANTHROPIC_BASE_URL"
379
- : service === "openai"
380
- ? "OPENAI_BASE_URL"
381
- : `${service.toUpperCase()}_BASE_URL`;
382
- console.log(` ${envKey}=${process.env[envKey] ?? "(not set)"}`);
383
- }
384
- });
385
-
386
- aquamanCmd
387
- .command("add <service> [key]")
388
- .description("Add a credential (opens secure prompt)")
389
- .action((service: string, key: string = "api_key") => {
390
- console.log(`\n Run in your terminal:\n aquaman credentials add ${service} ${key}\n`);
391
- });
392
-
393
- aquamanCmd
394
- .command("list")
395
- .description("List stored credentials")
396
- .action(() => {
397
- console.log(`\n Run in your terminal:\n aquaman credentials list\n`);
398
- });
399
- },
400
- { commands: ["aquaman"] }
401
- );
381
+ // Register /aquaman-status slash command for humans
382
+ api.registerCommand({
383
+ name: 'aquaman-status',
384
+ description: 'Show aquaman credential proxy status and configured services',
385
+ acceptsArgs: false,
386
+ requireAuth: true,
387
+ async handler() {
388
+ const status = {
389
+ proxyRunning: proxyManager !== null,
390
+ socketPath: socketPath || getDefaultSocketPath(),
391
+ services: configuredServices,
392
+ httpInterceptorActive: httpInterceptor?.isActive() ?? false,
393
+ };
394
+ return { text: JSON.stringify(status, null, 2) };
395
+ }
396
+ });
397
+
398
+ // Register CLI commands if available
399
+ if (api.registerCli) {
400
+ api.registerCli(
401
+ ({ program }) => {
402
+ const aquamanCmd = program
403
+ .command("aquaman")
404
+ .description("Aquaman credential management");
405
+
406
+ aquamanCmd
407
+ .command("status")
408
+ .description("Show aquaman proxy status")
409
+ .action(() => {
410
+ console.log("\nAquaman Status:");
411
+ console.log(` Proxy running: ${proxyManager !== null}`);
412
+ console.log(` Socket path: ${socketPath || getDefaultSocketPath()}`);
413
+ 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)"}`);
423
+ }
424
+ });
425
+
426
+ 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`);
431
+ });
432
+
433
+ aquamanCmd
434
+ .command("list")
435
+ .description("List stored credentials")
436
+ .action(() => {
437
+ console.log(`\n Run in your terminal:\n aquaman credentials list\n`);
438
+ });
439
+ },
440
+ { commands: ["aquaman"] }
441
+ );
442
+ }
443
+
444
+ registerStatusTool(api, configuredServices);
445
+ api.logger.info("Aquaman plugin registered successfully");
402
446
  }
447
+ };
403
448
 
404
- registerStatusTool(api);
405
- api.logger.info("Aquaman plugin registered successfully");
406
- }
449
+ export default plugin;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aquaman-plugin",
3
- "version": "0.9.1",
3
+ "version": "0.10.0",
4
4
  "description": "Credential isolation plugin for OpenClaw",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -26,8 +26,8 @@
26
26
  "undici": "^7.0.0"
27
27
  },
28
28
  "peerDependencies": {
29
- "openclaw": ">=2026.1.0",
30
- "aquaman-proxy": "0.9.1"
29
+ "openclaw": ">=2026.1.11",
30
+ "aquaman-proxy": "0.10.0"
31
31
  },
32
32
  "peerDependenciesMeta": {
33
33
  "aquaman-proxy": {