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.
- package/README.md +25 -28
- package/index.ts +135 -92
- 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
|
|
12
|
-
│ = aquaman.local │ Domain
|
|
13
|
-
│ │<═ Socket
|
|
14
|
-
│ fetch() interceptor │══ (UDS)
|
|
15
|
-
│ redirects channel │ │
|
|
16
|
-
│ API traffic │ │
|
|
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
|
|
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
|
|
39
|
-
aquaman setup
|
|
40
|
-
openclaw
|
|
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
|
|
61
|
+
## Security Audit
|
|
65
62
|
|
|
66
|
-
|
|
63
|
+
`openclaw security audit --deep` reports two expected findings:
|
|
67
64
|
|
|
68
|
-
- **`dangerous-exec`** on `proxy-manager.ts` — the plugin spawns the
|
|
69
|
-
- **`tools_reachable_permissive_policy`** — advisory
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
298
|
+
* Aquaman OpenClaw Plugin Definition
|
|
284
299
|
*/
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
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
|
-
|
|
328
|
+
api.logger.info("aquaman CLI found, will start proxy on gateway start");
|
|
304
329
|
|
|
305
|
-
|
|
306
|
-
|
|
330
|
+
// Configure environment variables immediately (sentinel hostname)
|
|
331
|
+
configureEnvironment(api.logger, configuredServices);
|
|
307
332
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
async
|
|
312
|
-
|
|
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(
|
|
339
|
+
const started = await startProxy(ctx.logger);
|
|
315
340
|
if (started && socketPath) {
|
|
316
|
-
|
|
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
|
-
|
|
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(
|
|
353
|
+
activateHttpInterceptor(ctx.logger);
|
|
329
354
|
} else {
|
|
330
|
-
|
|
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
|
-
|
|
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(
|
|
367
|
+
activateHttpInterceptor(ctx.logger);
|
|
343
368
|
} else {
|
|
344
|
-
|
|
369
|
+
ctx.logger.error(
|
|
345
370
|
"No running proxy found. Check: aquaman doctor"
|
|
346
371
|
);
|
|
347
372
|
}
|
|
348
373
|
}
|
|
349
374
|
},
|
|
350
|
-
|
|
351
|
-
|
|
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
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
30
|
-
"aquaman-proxy": "0.
|
|
29
|
+
"openclaw": ">=2026.1.11",
|
|
30
|
+
"aquaman-proxy": "0.10.0"
|
|
31
31
|
},
|
|
32
32
|
"peerDependenciesMeta": {
|
|
33
33
|
"aquaman-proxy": {
|