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 +46 -28
- package/index.ts +247 -104
- package/openclaw.plugin.json +6 -2
- package/package.json +13 -11
- package/src/commands.ts +117 -18
- package/src/proxy-manager.ts +102 -39
package/README.md
CHANGED
|
@@ -1,19 +1,20 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Aquaman — API Key Protection for OpenClaw
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
|
12
|
-
│ = aquaman.local │ Domain
|
|
13
|
-
│ │<═ Socket
|
|
14
|
-
│ fetch() interceptor │══ (UDS)
|
|
15
|
-
│ redirects channel │ │
|
|
16
|
-
│ API traffic │ │
|
|
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
|
-
|
|
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
|
-
|
|
39
|
-
aquaman setup
|
|
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
|
-
>
|
|
44
|
-
>
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
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
|
|
82
|
+
## Security Audit
|
|
65
83
|
|
|
66
|
-
|
|
84
|
+
`openclaw security audit --deep` reports two expected findings:
|
|
67
85
|
|
|
68
|
-
- **`dangerous-exec`** on `proxy-manager.ts` — the plugin spawns the
|
|
69
|
-
- **`tools_reachable_permissive_policy`** — advisory
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
|
148
|
+
* Check if aquaman proxy binary is available (local node_modules or PATH)
|
|
106
149
|
*/
|
|
107
|
-
function
|
|
108
|
-
return
|
|
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
|
-
*
|
|
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
|
|
356
|
+
name: 'Aquaman — API Key Protection',
|
|
303
357
|
version: PLUGIN_VERSION,
|
|
304
|
-
description: '
|
|
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
|
-
//
|
|
317
|
-
|
|
370
|
+
// Check if aquaman proxy binary is available
|
|
371
|
+
const proxyAvailable = isAquamanProxyInstalled();
|
|
372
|
+
|
|
373
|
+
if (!proxyAvailable) {
|
|
318
374
|
api.logger.warn(
|
|
319
|
-
"aquaman
|
|
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(
|
|
325
|
-
|
|
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
|
-
|
|
331
|
-
|
|
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
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
const
|
|
359
|
-
if (
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
);
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
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
|
-
|
|
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
|
-
|
|
376
|
-
|
|
377
|
-
|
|
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
|
|
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
|
|
412
|
-
console.log(`
|
|
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
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
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("
|
|
428
|
-
.description("
|
|
429
|
-
.action((
|
|
430
|
-
|
|
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
|
-
|
|
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"] }
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "aquaman-plugin",
|
|
3
|
-
"name": "Aquaman
|
|
4
|
-
"
|
|
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.
|
|
4
|
-
"description": "
|
|
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.
|
|
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 (
|
|
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
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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,
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
|
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',
|
package/src/proxy-manager.ts
CHANGED
|
@@ -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.
|
|
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
|
|
205
|
-
|
|
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
|
|