aquaman-plugin 0.4.0 → 0.5.1
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 +36 -68
- package/index.ts +96 -141
- package/openclaw.plugin.json +6 -0
- package/package.json +9 -5
- package/src/http-interceptor.ts +1 -1
- package/src/plugin.ts +2 -2
- package/src/proxy-health.ts +43 -0
- package/src/proxy-manager.ts +13 -26
package/README.md
CHANGED
|
@@ -2,40 +2,48 @@
|
|
|
2
2
|
|
|
3
3
|
OpenClaw Gateway plugin for [aquaman](https://github.com/tech4242/aquaman) credential isolation.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## How It Works
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
Agent / OpenClaw Gateway Aquaman Proxy
|
|
9
|
+
┌──────────────────────┐ ┌──────────────────────┐
|
|
10
|
+
│ │ │ │
|
|
11
|
+
│ ANTHROPIC_BASE_URL │──request────>│ Keychain / 1Pass / │
|
|
12
|
+
│ = localhost:8081 │ │ Vault / Encrypted │
|
|
13
|
+
│ │<─response────│ │
|
|
14
|
+
│ fetch() interceptor │──channel────>│ + Auth injected: │
|
|
15
|
+
│ redirects channel │ traffic │ header / url-path │
|
|
16
|
+
│ API traffic │ │ basic / oauth │
|
|
17
|
+
│ │ │ │
|
|
18
|
+
│ No credentials. │ │ │
|
|
19
|
+
│ Nothing to steal. │ │ │
|
|
20
|
+
└──────────────────────┘ └───┬──────────┬───────┘
|
|
21
|
+
│ │
|
|
22
|
+
│ ▼
|
|
23
|
+
│ ~/.aquaman/audit/
|
|
24
|
+
│ (hash-chained log)
|
|
25
|
+
▼
|
|
26
|
+
api.anthropic.com
|
|
27
|
+
api.telegram.org
|
|
28
|
+
slack.com/api ...
|
|
29
|
+
```
|
|
6
30
|
|
|
7
|
-
|
|
31
|
+
This plugin makes the left side work. It routes all LLM and channel API traffic through the aquaman proxy so credentials never enter the Gateway process.
|
|
8
32
|
|
|
9
|
-
##
|
|
33
|
+
## Quick Start
|
|
10
34
|
|
|
11
35
|
```bash
|
|
12
|
-
|
|
13
|
-
|
|
36
|
+
npm install -g aquaman-proxy # 1. Install the proxy CLI
|
|
37
|
+
aquaman setup # 2. Store keys, install plugin, configure OpenClaw
|
|
38
|
+
aquaman migrate openclaw --auto # 3. Move existing channel creds to secure store
|
|
39
|
+
openclaw # 4. Proxy starts automatically
|
|
14
40
|
```
|
|
15
41
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
Add to `~/.openclaw/openclaw.json`:
|
|
42
|
+
Troubleshooting: `aquaman doctor`
|
|
19
43
|
|
|
20
|
-
|
|
21
|
-
{
|
|
22
|
-
"plugins": {
|
|
23
|
-
"entries": {
|
|
24
|
-
"aquaman-plugin": {
|
|
25
|
-
"enabled": true,
|
|
26
|
-
"config": {
|
|
27
|
-
"mode": "proxy",
|
|
28
|
-
"backend": "keychain",
|
|
29
|
-
"services": ["anthropic", "openai"],
|
|
30
|
-
"proxyPort": 8081
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
```
|
|
44
|
+
## Config Options
|
|
37
45
|
|
|
38
|
-
|
|
46
|
+
`aquaman setup` writes these to `~/.openclaw/openclaw.json` automatically:
|
|
39
47
|
|
|
40
48
|
| Key | Type | Default | Description |
|
|
41
49
|
|-----|------|---------|-------------|
|
|
@@ -44,51 +52,11 @@ Add to `~/.openclaw/openclaw.json`:
|
|
|
44
52
|
| `services` | `string[]` | `["anthropic", "openai"]` | Services to proxy |
|
|
45
53
|
| `proxyPort` | `number` | `8081` | Proxy listen port |
|
|
46
54
|
|
|
47
|
-
> Advanced settings (TLS, audit, vault)
|
|
48
|
-
|
|
49
|
-
## Setup
|
|
50
|
-
|
|
51
|
-
**1. Add credentials:**
|
|
52
|
-
|
|
53
|
-
```bash
|
|
54
|
-
aquaman credentials add anthropic api_key
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
**2. Register a placeholder key with OpenClaw:**
|
|
58
|
-
|
|
59
|
-
```bash
|
|
60
|
-
mkdir -p ~/.openclaw/agents/main/agent
|
|
61
|
-
cat > ~/.openclaw/agents/main/agent/auth-profiles.json << 'EOF'
|
|
62
|
-
{
|
|
63
|
-
"version": 1,
|
|
64
|
-
"profiles": {
|
|
65
|
-
"anthropic:default": {
|
|
66
|
-
"type": "api_key",
|
|
67
|
-
"provider": "anthropic",
|
|
68
|
-
"key": "aquaman-proxy-managed"
|
|
69
|
-
}
|
|
70
|
-
},
|
|
71
|
-
"order": { "anthropic": ["anthropic:default"] }
|
|
72
|
-
}
|
|
73
|
-
EOF
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
**3. Launch OpenClaw:**
|
|
77
|
-
|
|
78
|
-
```bash
|
|
79
|
-
openclaw
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
The plugin auto-starts the proxy, sets `ANTHROPIC_BASE_URL` to route through it, and intercepts channel API traffic via `globalThis.fetch`.
|
|
83
|
-
|
|
84
|
-
## How It Works
|
|
85
|
-
|
|
86
|
-
- **Proxy mode** — Spawns aquaman as a child process. Credentials live in a separate OS process. Even if the agent is compromised, it cannot access keys.
|
|
87
|
-
- **Embedded mode** — Credentials loaded in-process. Simpler setup, less isolation. Good for local development.
|
|
55
|
+
> Advanced settings (TLS, audit, vault) go in `~/.aquaman/config.yaml`.
|
|
88
56
|
|
|
89
57
|
## Documentation
|
|
90
58
|
|
|
91
|
-
See the [main README](https://github.com/tech4242/aquaman#readme) for
|
|
59
|
+
See the [main README](https://github.com/tech4242/aquaman#readme) for architecture, Docker deployment, and manual testing.
|
|
92
60
|
|
|
93
61
|
## License
|
|
94
62
|
|
package/index.ts
CHANGED
|
@@ -17,18 +17,68 @@
|
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
19
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
20
|
-
import { spawn, execSync, type ChildProcess } from "node:child_process";
|
|
21
20
|
import * as fs from "node:fs";
|
|
22
21
|
import * as path from "node:path";
|
|
23
22
|
import * as os from "node:os";
|
|
24
23
|
import { HttpInterceptor, createHttpInterceptor } from "./src/http-interceptor.js";
|
|
24
|
+
import { createProxyManager, type ProxyManager } from "./src/proxy-manager.js";
|
|
25
|
+
import { fetchHostMap, isProxyRunning } from "./src/proxy-health.js";
|
|
25
26
|
|
|
26
|
-
|
|
27
|
+
/**
|
|
28
|
+
* Find an executable in PATH using filesystem checks (no shell execution).
|
|
29
|
+
* Avoids execSync("which ...") which triggers dangerous-exec security audit flags.
|
|
30
|
+
*/
|
|
31
|
+
function findInPath(name: string): string | null {
|
|
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, name);
|
|
36
|
+
try {
|
|
37
|
+
fs.accessSync(candidate, fs.constants.X_OK);
|
|
38
|
+
return candidate;
|
|
39
|
+
} catch {
|
|
40
|
+
// Not found or not executable in this dir
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
let proxyManager: ProxyManager | null = null;
|
|
27
47
|
let httpInterceptor: HttpInterceptor | null = null;
|
|
28
48
|
let clientToken: string | null = null;
|
|
49
|
+
let dynamicHostMap: Map<string, string> | null = null;
|
|
29
50
|
const proxyPort = 8081;
|
|
30
51
|
const services = ["anthropic", "openai"];
|
|
31
52
|
|
|
53
|
+
/** Fallback host map used when proxy doesn't provide one (backward compat) */
|
|
54
|
+
const FALLBACK_HOST_MAP = new Map<string, string>([
|
|
55
|
+
['api.anthropic.com', 'anthropic'],
|
|
56
|
+
['api.openai.com', 'openai'],
|
|
57
|
+
['api.github.com', 'github'],
|
|
58
|
+
['api.x.ai', 'xai'],
|
|
59
|
+
['gateway.ai.cloudflare.com', 'cloudflare-ai'],
|
|
60
|
+
['slack.com', 'slack'],
|
|
61
|
+
['*.slack.com', 'slack'],
|
|
62
|
+
['discord.com', 'discord'],
|
|
63
|
+
['*.discord.com', 'discord'],
|
|
64
|
+
['api.telegram.org', 'telegram'],
|
|
65
|
+
['matrix.org', 'matrix'],
|
|
66
|
+
['*.matrix.org', 'matrix'],
|
|
67
|
+
['api.line.me', 'line'],
|
|
68
|
+
['api-data.line.me', 'line'],
|
|
69
|
+
['api.twitch.tv', 'twitch'],
|
|
70
|
+
['id.twitch.tv', 'twitch'],
|
|
71
|
+
['api.twilio.com', 'twilio'],
|
|
72
|
+
['*.twilio.com', 'twilio'],
|
|
73
|
+
['api.telnyx.com', 'telnyx'],
|
|
74
|
+
['api.elevenlabs.io', 'elevenlabs'],
|
|
75
|
+
['openapi.zalo.me', 'zalo'],
|
|
76
|
+
['graph.microsoft.com', 'ms-teams'],
|
|
77
|
+
['open.feishu.cn', 'feishu'],
|
|
78
|
+
['open.larksuite.com', 'feishu'],
|
|
79
|
+
['chat.googleapis.com', 'google-chat'],
|
|
80
|
+
]);
|
|
81
|
+
|
|
32
82
|
/**
|
|
33
83
|
* Get external proxy URL from environment (for Docker two-container mode).
|
|
34
84
|
* When set, the plugin skips spawning a local proxy and routes traffic to the external URL.
|
|
@@ -45,91 +95,38 @@ function getExternalClientToken(): string | null {
|
|
|
45
95
|
}
|
|
46
96
|
|
|
47
97
|
/**
|
|
48
|
-
* Check if aquaman CLI is installed
|
|
98
|
+
* Check if aquaman CLI is installed (fs-based, no shell execution)
|
|
49
99
|
*/
|
|
50
100
|
function isAquamanInstalled(): boolean {
|
|
51
|
-
|
|
52
|
-
execSync("which aquaman", { stdio: "pipe" });
|
|
53
|
-
return true;
|
|
54
|
-
} catch {
|
|
55
|
-
return false;
|
|
56
|
-
}
|
|
101
|
+
return findInPath("aquaman") !== null;
|
|
57
102
|
}
|
|
58
103
|
|
|
59
104
|
/**
|
|
60
|
-
* Start the aquaman proxy daemon
|
|
105
|
+
* Start the aquaman proxy daemon using ProxyManager
|
|
61
106
|
*/
|
|
62
107
|
async function startProxy(port: number, log: OpenClawPluginApi["logger"]): Promise<boolean> {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
log.error(`Failed to spawn aquaman: ${err}`);
|
|
71
|
-
resolve(false);
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
let started = false;
|
|
76
|
-
let stdoutBuffer = "";
|
|
77
|
-
|
|
78
|
-
proxyProcess.stdout?.on("data", (data: Buffer) => {
|
|
79
|
-
stdoutBuffer += data.toString();
|
|
80
|
-
log.debug(`[aquaman] ${data.toString().trim()}`);
|
|
81
|
-
if (started) return;
|
|
82
|
-
|
|
83
|
-
// Try to parse JSON connection info from stdout
|
|
84
|
-
const lines = stdoutBuffer.split("\n");
|
|
85
|
-
for (const line of lines) {
|
|
86
|
-
const trimmed = line.trim();
|
|
87
|
-
if (!trimmed.startsWith("{")) continue;
|
|
88
|
-
try {
|
|
89
|
-
const info = JSON.parse(trimmed);
|
|
90
|
-
if (info.ready === true) {
|
|
91
|
-
started = true;
|
|
92
|
-
clientToken = info.token || null;
|
|
93
|
-
resolve(true);
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
96
|
-
} catch {
|
|
97
|
-
// Not valid JSON yet, keep buffering
|
|
108
|
+
try {
|
|
109
|
+
const mgr = createProxyManager({
|
|
110
|
+
config: { proxyPort: port },
|
|
111
|
+
onReady: (info) => {
|
|
112
|
+
clientToken = info.token || null;
|
|
113
|
+
if (info.hostMap && typeof info.hostMap === "object") {
|
|
114
|
+
dynamicHostMap = new Map(Object.entries(info.hostMap));
|
|
98
115
|
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
}
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
proxyProcess.stderr?.on("data", (data: Buffer) => {
|
|
109
|
-
log.warn(`[aquaman] ${data.toString().trim()}`);
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
proxyProcess.on("error", (err) => {
|
|
113
|
-
log.error(`Failed to start proxy: ${err.message}`);
|
|
114
|
-
resolve(false);
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
proxyProcess.on("exit", (code) => {
|
|
118
|
-
if (!started) {
|
|
119
|
-
log.warn(`Proxy exited with code ${code} before starting`);
|
|
120
|
-
resolve(false);
|
|
121
|
-
}
|
|
122
|
-
proxyProcess = null;
|
|
116
|
+
},
|
|
117
|
+
onError: (err) => log.error(`Proxy error: ${err.message}`),
|
|
118
|
+
onExit: (code) => {
|
|
119
|
+
proxyManager = null;
|
|
120
|
+
log.warn(`Proxy exited with code ${code}`);
|
|
121
|
+
},
|
|
123
122
|
});
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
}, 5000);
|
|
132
|
-
});
|
|
123
|
+
await mgr.start();
|
|
124
|
+
proxyManager = mgr;
|
|
125
|
+
return true;
|
|
126
|
+
} catch (err) {
|
|
127
|
+
log.error(`Failed to start proxy: ${err}`);
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
133
130
|
}
|
|
134
131
|
|
|
135
132
|
/**
|
|
@@ -140,48 +137,21 @@ function stopProxy(): void {
|
|
|
140
137
|
httpInterceptor.deactivate();
|
|
141
138
|
httpInterceptor = null;
|
|
142
139
|
}
|
|
143
|
-
if (
|
|
144
|
-
|
|
145
|
-
|
|
140
|
+
if (proxyManager) {
|
|
141
|
+
proxyManager.stop();
|
|
142
|
+
proxyManager = null;
|
|
146
143
|
}
|
|
147
144
|
clientToken = null;
|
|
148
145
|
}
|
|
149
146
|
|
|
150
147
|
/**
|
|
151
|
-
* Activate the HTTP
|
|
148
|
+
* Activate the HTTP interceptor to redirect channel API traffic through the proxy.
|
|
152
149
|
* This is what provides credential isolation for channels that don't support base URL overrides.
|
|
153
150
|
*/
|
|
154
151
|
function activateHttpInterceptor(log: OpenClawPluginApi["logger"]): void {
|
|
155
|
-
//
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
['api.anthropic.com', 'anthropic'],
|
|
159
|
-
['api.openai.com', 'openai'],
|
|
160
|
-
['api.github.com', 'github'],
|
|
161
|
-
['api.x.ai', 'xai'],
|
|
162
|
-
['gateway.ai.cloudflare.com', 'cloudflare-ai'],
|
|
163
|
-
// Channel APIs
|
|
164
|
-
['slack.com', 'slack'],
|
|
165
|
-
['*.slack.com', 'slack'],
|
|
166
|
-
['discord.com', 'discord'],
|
|
167
|
-
['*.discord.com', 'discord'],
|
|
168
|
-
['api.telegram.org', 'telegram'],
|
|
169
|
-
['matrix.org', 'matrix'],
|
|
170
|
-
['*.matrix.org', 'matrix'],
|
|
171
|
-
['api.line.me', 'line'],
|
|
172
|
-
['api-data.line.me', 'line'],
|
|
173
|
-
['api.twitch.tv', 'twitch'],
|
|
174
|
-
['id.twitch.tv', 'twitch'],
|
|
175
|
-
['api.twilio.com', 'twilio'],
|
|
176
|
-
['*.twilio.com', 'twilio'],
|
|
177
|
-
['api.telnyx.com', 'telnyx'],
|
|
178
|
-
['api.elevenlabs.io', 'elevenlabs'],
|
|
179
|
-
['openapi.zalo.me', 'zalo'],
|
|
180
|
-
['graph.microsoft.com', 'ms-teams'],
|
|
181
|
-
['open.feishu.cn', 'feishu'],
|
|
182
|
-
['open.larksuite.com', 'feishu'],
|
|
183
|
-
['chat.googleapis.com', 'google-chat'],
|
|
184
|
-
]);
|
|
152
|
+
// Use dynamic host map from proxy (includes custom services from services.yaml)
|
|
153
|
+
// Falls back to builtin map for backward compatibility
|
|
154
|
+
const hostMap = dynamicHostMap || FALLBACK_HOST_MAP;
|
|
185
155
|
|
|
186
156
|
const baseUrl = getExternalProxyUrl() || `http://127.0.0.1:${proxyPort}`;
|
|
187
157
|
|
|
@@ -246,7 +216,7 @@ function registerStatusTool(api: OpenClawPluginApi): void {
|
|
|
246
216
|
return {
|
|
247
217
|
externalProxy: externalUrl !== null,
|
|
248
218
|
proxyUrl: externalUrl || `http://127.0.0.1:${proxyPort}`,
|
|
249
|
-
proxyRunning: externalUrl !== null ||
|
|
219
|
+
proxyRunning: externalUrl !== null || proxyManager !== null,
|
|
250
220
|
proxyPort,
|
|
251
221
|
services,
|
|
252
222
|
httpInterceptorActive: httpInterceptor?.isActive() ?? false,
|
|
@@ -333,6 +303,9 @@ export default function register(api: OpenClawPluginApi): void {
|
|
|
333
303
|
if (api.registerLifecycle) {
|
|
334
304
|
api.registerLifecycle({
|
|
335
305
|
async onGatewayStart() {
|
|
306
|
+
// Fetch dynamic host map from external proxy (includes custom services)
|
|
307
|
+
const map = await fetchHostMap(externalUrl, clientToken);
|
|
308
|
+
dynamicHostMap = map.size > 0 ? map : FALLBACK_HOST_MAP;
|
|
336
309
|
activateHttpInterceptor(api.logger);
|
|
337
310
|
api.logger.info("HTTP interceptor active (external proxy mode)");
|
|
338
311
|
},
|
|
@@ -376,24 +349,20 @@ export default function register(api: OpenClawPluginApi): void {
|
|
|
376
349
|
const started = await startProxy(proxyPort, api.logger);
|
|
377
350
|
if (started) {
|
|
378
351
|
api.logger.info("Aquaman proxy started successfully");
|
|
379
|
-
// Activate
|
|
352
|
+
// Activate HTTP interceptor to redirect channel traffic through proxy
|
|
380
353
|
activateHttpInterceptor(api.logger);
|
|
381
354
|
} else {
|
|
382
355
|
api.logger.error(
|
|
383
356
|
`Failed to start aquaman proxy on port ${proxyPort}`
|
|
384
357
|
);
|
|
385
358
|
// Check if another instance is already running on the port
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
359
|
+
const alreadyRunning = await isProxyRunning(proxyPort);
|
|
360
|
+
if (alreadyRunning) {
|
|
361
|
+
api.logger.info(
|
|
362
|
+
`Another aquaman instance is already running on port ${proxyPort} — using it`
|
|
389
363
|
);
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
`Another aquaman instance is already running on port ${proxyPort} — using it`
|
|
393
|
-
);
|
|
394
|
-
activateHttpInterceptor(api.logger);
|
|
395
|
-
}
|
|
396
|
-
} catch {
|
|
364
|
+
activateHttpInterceptor(api.logger);
|
|
365
|
+
} else {
|
|
397
366
|
api.logger.error(
|
|
398
367
|
`Port ${proxyPort} may be in use. Check with: lsof -i :${proxyPort}`
|
|
399
368
|
);
|
|
@@ -421,7 +390,7 @@ export default function register(api: OpenClawPluginApi): void {
|
|
|
421
390
|
.description("Show aquaman proxy status")
|
|
422
391
|
.action(() => {
|
|
423
392
|
console.log("\nAquaman Status:");
|
|
424
|
-
console.log(` Proxy running: ${
|
|
393
|
+
console.log(` Proxy running: ${proxyManager !== null}`);
|
|
425
394
|
console.log(` Proxy port: ${proxyPort}`);
|
|
426
395
|
console.log(` Services: ${services.join(", ")}`);
|
|
427
396
|
console.log("\nEnvironment Variables:");
|
|
@@ -440,28 +409,14 @@ export default function register(api: OpenClawPluginApi): void {
|
|
|
440
409
|
.command("add <service> [key]")
|
|
441
410
|
.description("Add a credential (opens secure prompt)")
|
|
442
411
|
.action((service: string, key: string = "api_key") => {
|
|
443
|
-
|
|
444
|
-
execSync(`aquaman credentials add ${service} ${key}`, {
|
|
445
|
-
stdio: "inherit",
|
|
446
|
-
});
|
|
447
|
-
} catch {
|
|
448
|
-
console.error(
|
|
449
|
-
"Failed to add credential. Is aquaman installed? npm install -g aquaman-proxy"
|
|
450
|
-
);
|
|
451
|
-
}
|
|
412
|
+
console.log(`\n Run in your terminal:\n aquaman credentials add ${service} ${key}\n`);
|
|
452
413
|
});
|
|
453
414
|
|
|
454
415
|
aquamanCmd
|
|
455
416
|
.command("list")
|
|
456
417
|
.description("List stored credentials")
|
|
457
418
|
.action(() => {
|
|
458
|
-
|
|
459
|
-
execSync("aquaman credentials list", { stdio: "inherit" });
|
|
460
|
-
} catch {
|
|
461
|
-
console.error(
|
|
462
|
-
"Failed to list credentials. Is aquaman installed?"
|
|
463
|
-
);
|
|
464
|
-
}
|
|
419
|
+
console.log(`\n Run in your terminal:\n aquaman credentials list\n`);
|
|
465
420
|
});
|
|
466
421
|
},
|
|
467
422
|
{ commands: ["aquaman"] }
|
package/openclaw.plugin.json
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
"id": "aquaman-plugin",
|
|
3
3
|
"name": "Aquaman Vault",
|
|
4
4
|
"description": "Credential isolation - API keys never touch the agent process",
|
|
5
|
+
"permissions": {
|
|
6
|
+
"env:write": ["*_BASE_URL", "GITHUB_API_URL"],
|
|
7
|
+
"process:spawn": ["aquaman"],
|
|
8
|
+
"global:override": ["fetch"],
|
|
9
|
+
"fs:write": ["~/.openclaw/agents/*/agent/auth-profiles.json"]
|
|
10
|
+
},
|
|
5
11
|
"configSchema": {
|
|
6
12
|
"type": "object",
|
|
7
13
|
"additionalProperties": false,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aquaman-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "Credential isolation plugin for OpenClaw",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -21,11 +21,15 @@
|
|
|
21
21
|
],
|
|
22
22
|
"author": "tech4242",
|
|
23
23
|
"license": "MIT",
|
|
24
|
-
"dependencies": {
|
|
25
|
-
"aquaman-core": "^0.2.0"
|
|
26
|
-
},
|
|
24
|
+
"dependencies": {},
|
|
27
25
|
"peerDependencies": {
|
|
28
|
-
"openclaw": ">=2026.1.0"
|
|
26
|
+
"openclaw": ">=2026.1.0",
|
|
27
|
+
"aquaman-proxy": "0.5.1"
|
|
28
|
+
},
|
|
29
|
+
"peerDependenciesMeta": {
|
|
30
|
+
"aquaman-proxy": {
|
|
31
|
+
"optional": true
|
|
32
|
+
}
|
|
29
33
|
},
|
|
30
34
|
"devDependencies": {
|
|
31
35
|
"@types/node": "^20.10.0",
|
package/src/http-interceptor.ts
CHANGED
package/src/plugin.ts
CHANGED
|
@@ -162,7 +162,7 @@ export class AquamanPlugin {
|
|
|
162
162
|
}
|
|
163
163
|
|
|
164
164
|
/**
|
|
165
|
-
* Activate HTTP
|
|
165
|
+
* Activate HTTP interceptor for channel credential isolation.
|
|
166
166
|
*/
|
|
167
167
|
private activateHttpInterceptor(proxyBaseUrl: string): void {
|
|
168
168
|
// Build host map from the service registry's host patterns
|
|
@@ -391,7 +391,7 @@ export class AquamanPlugin {
|
|
|
391
391
|
* Check if proxy is healthy
|
|
392
392
|
*/
|
|
393
393
|
async isProxyHealthy(): Promise<boolean> {
|
|
394
|
-
return this.proxyManager?.
|
|
394
|
+
return this.proxyManager?.isRunning() || false;
|
|
395
395
|
}
|
|
396
396
|
}
|
|
397
397
|
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Proxy health and discovery utilities.
|
|
3
|
+
*
|
|
4
|
+
* Separated from index.ts to avoid co-locating network calls with process.env
|
|
5
|
+
* (triggers OpenClaw code safety scanner env-harvesting false positive).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Request host map from proxy's /_hostmap endpoint.
|
|
10
|
+
* Returns an empty map if the endpoint is unavailable (caller handles fallback).
|
|
11
|
+
*/
|
|
12
|
+
export async function fetchHostMap(
|
|
13
|
+
baseUrl: string,
|
|
14
|
+
token: string | null,
|
|
15
|
+
): Promise<Map<string, string>> {
|
|
16
|
+
try {
|
|
17
|
+
const headers: Record<string, string> = {};
|
|
18
|
+
if (token) headers['X-Aquaman-Token'] = token;
|
|
19
|
+
const resp = await fetch(`${baseUrl}/_hostmap`, {
|
|
20
|
+
headers,
|
|
21
|
+
signal: AbortSignal.timeout(3000),
|
|
22
|
+
});
|
|
23
|
+
if (resp.ok) {
|
|
24
|
+
const obj = (await resp.json()) as Record<string, string>;
|
|
25
|
+
return new Map(Object.entries(obj));
|
|
26
|
+
}
|
|
27
|
+
} catch {
|
|
28
|
+
// Proxy may be older version without /_hostmap — caller uses fallback
|
|
29
|
+
}
|
|
30
|
+
return new Map();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Check if a proxy is already running on the given port.
|
|
35
|
+
*/
|
|
36
|
+
export async function isProxyRunning(port: number): Promise<boolean> {
|
|
37
|
+
try {
|
|
38
|
+
const resp = await fetch(`http://127.0.0.1:${port}/_health`);
|
|
39
|
+
return resp.ok;
|
|
40
|
+
} catch {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
}
|
package/src/proxy-manager.ts
CHANGED
|
@@ -18,6 +18,7 @@ export interface ProxyConnectionInfo {
|
|
|
18
18
|
services: string[];
|
|
19
19
|
backend: string;
|
|
20
20
|
token?: string;
|
|
21
|
+
hostMap?: Record<string, string>;
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
export interface ProxyManagerOptions {
|
|
@@ -211,25 +212,6 @@ export class ProxyManager {
|
|
|
211
212
|
return `${this.connectionInfo.baseUrl}/${service}`;
|
|
212
213
|
}
|
|
213
214
|
|
|
214
|
-
/**
|
|
215
|
-
* Health check
|
|
216
|
-
*/
|
|
217
|
-
async healthCheck(): Promise<boolean> {
|
|
218
|
-
if (!this.connectionInfo) {
|
|
219
|
-
return false;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
try {
|
|
223
|
-
const response = await fetch(`${this.connectionInfo.baseUrl}/health`, {
|
|
224
|
-
method: 'GET',
|
|
225
|
-
signal: AbortSignal.timeout(5000)
|
|
226
|
-
});
|
|
227
|
-
return response.ok;
|
|
228
|
-
} catch {
|
|
229
|
-
return false;
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
|
|
233
215
|
/**
|
|
234
216
|
* Find the aquaman binary
|
|
235
217
|
*/
|
|
@@ -249,14 +231,19 @@ export class ProxyManager {
|
|
|
249
231
|
|
|
250
232
|
for (const loc of locations) {
|
|
251
233
|
if (loc === 'aquaman') {
|
|
252
|
-
// Check if in PATH
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
234
|
+
// Check if in PATH using filesystem checks (no shell execution)
|
|
235
|
+
const pathEnv = process.env.PATH || '';
|
|
236
|
+
const dirs = pathEnv.split(path.delimiter);
|
|
237
|
+
for (const dir of dirs) {
|
|
238
|
+
const candidate = path.join(dir, 'aquaman');
|
|
239
|
+
try {
|
|
240
|
+
fs.accessSync(candidate, fs.constants.X_OK);
|
|
241
|
+
return 'aquaman';
|
|
242
|
+
} catch {
|
|
243
|
+
// Not found in this dir
|
|
244
|
+
}
|
|
259
245
|
}
|
|
246
|
+
continue;
|
|
260
247
|
}
|
|
261
248
|
|
|
262
249
|
if (fs.existsSync(loc)) {
|