aquaman-plugin 0.5.1 → 0.7.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 +26 -16
- package/index.ts +56 -81
- package/openclaw.plugin.json +1 -12
- package/package.json +5 -3
- package/src/commands.ts +26 -198
- package/src/config-schema.ts +2 -32
- package/src/http-interceptor.ts +30 -59
- package/src/index.ts +1 -13
- package/src/plugin.ts +16 -143
- package/src/proxy-health.ts +50 -26
- package/src/proxy-manager.ts +10 -24
- package/src/embedded.ts +0 -182
package/README.md
CHANGED
|
@@ -8,15 +8,16 @@ OpenClaw Gateway plugin for [aquaman](https://github.com/tech4242/aquaman) crede
|
|
|
8
8
|
Agent / OpenClaw Gateway Aquaman Proxy
|
|
9
9
|
┌──────────────────────┐ ┌──────────────────────┐
|
|
10
10
|
│ │ │ │
|
|
11
|
-
│ ANTHROPIC_BASE_URL
|
|
12
|
-
│ =
|
|
13
|
-
│
|
|
14
|
-
│ fetch() interceptor
|
|
15
|
-
│ redirects channel │
|
|
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
16
|
│ API traffic │ │ basic / oauth │
|
|
17
17
|
│ │ │ │
|
|
18
|
-
│ No credentials. │
|
|
19
|
-
│
|
|
18
|
+
│ No credentials. │ ~/.aquaman/ │ │
|
|
19
|
+
│ No open ports. │ proxy.sock │ │
|
|
20
|
+
│ Nothing to steal. │ (chmod 600) │ │
|
|
20
21
|
└──────────────────────┘ └───┬──────────┬───────┘
|
|
21
22
|
│ │
|
|
22
23
|
│ ▼
|
|
@@ -28,17 +29,24 @@ Agent / OpenClaw Gateway Aquaman Proxy
|
|
|
28
29
|
slack.com/api ...
|
|
29
30
|
```
|
|
30
31
|
|
|
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.
|
|
32
|
+
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
33
|
|
|
33
34
|
## Quick Start
|
|
34
35
|
|
|
35
36
|
```bash
|
|
36
|
-
npm install -g aquaman-proxy #
|
|
37
|
-
aquaman setup #
|
|
38
|
-
|
|
39
|
-
openclaw # 4. Proxy starts automatically
|
|
37
|
+
npm install -g aquaman-proxy # install the proxy CLI
|
|
38
|
+
aquaman setup # stores keys, installs plugin, configures OpenClaw
|
|
39
|
+
openclaw # proxy starts automatically
|
|
40
40
|
```
|
|
41
41
|
|
|
42
|
+
> `aquaman setup` auto-detects your credential backend. macOS defaults to Keychain,
|
|
43
|
+
> Linux defaults to encrypted file. Override with `--backend`:
|
|
44
|
+
> `aquaman setup --backend keepassxc`
|
|
45
|
+
> Options: `keychain`, `encrypted-file`, `keepassxc`, `1password`, `vault`
|
|
46
|
+
|
|
47
|
+
Existing plaintext credentials are migrated automatically during setup.
|
|
48
|
+
Run again anytime to migrate new credentials: `aquaman migrate openclaw --auto`
|
|
49
|
+
|
|
42
50
|
Troubleshooting: `aquaman doctor`
|
|
43
51
|
|
|
44
52
|
## Config Options
|
|
@@ -47,12 +55,14 @@ Troubleshooting: `aquaman doctor`
|
|
|
47
55
|
|
|
48
56
|
| Key | Type | Default | Description |
|
|
49
57
|
|-----|------|---------|-------------|
|
|
50
|
-
| `
|
|
51
|
-
| `backend` | `"keychain"` \| `"1password"` \| `"vault"` \| `"encrypted-file"` | `"keychain"` | Credential store |
|
|
58
|
+
| `backend` | `"keychain"` \| `"1password"` \| `"vault"` \| `"encrypted-file"` \| `"keepassxc"` | `"keychain"` | Credential store |
|
|
52
59
|
| `services` | `string[]` | `["anthropic", "openai"]` | Services to proxy |
|
|
53
|
-
| `proxyPort` | `number` | `8081` | Proxy listen port |
|
|
54
60
|
|
|
55
|
-
> Advanced settings (
|
|
61
|
+
> Advanced settings (audit, vault) go in `~/.aquaman/config.yaml`.
|
|
62
|
+
|
|
63
|
+
## Security Audit Note
|
|
64
|
+
|
|
65
|
+
Running `openclaw security audit --deep` will show a `dangerous-exec` finding for this plugin. That's expected — the plugin spawns the aquaman proxy as a separate process, which is the whole point of credential isolation. `aquaman setup` adds the plugin to your `plugins.allow` trust list automatically.
|
|
56
66
|
|
|
57
67
|
## Documentation
|
|
58
68
|
|
package/index.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*
|
|
12
12
|
* The plugin will:
|
|
13
13
|
* - Start the aquaman proxy on plugin load
|
|
14
|
-
* - Set ANTHROPIC_BASE_URL, OPENAI_BASE_URL etc. to route through proxy
|
|
14
|
+
* - Set ANTHROPIC_BASE_URL, OPENAI_BASE_URL etc. to route through proxy via UDS
|
|
15
15
|
* - The proxy injects credentials into requests
|
|
16
16
|
* - Agent never sees the actual API keys
|
|
17
17
|
*/
|
|
@@ -22,7 +22,7 @@ import * as path from "node:path";
|
|
|
22
22
|
import * as os from "node:os";
|
|
23
23
|
import { HttpInterceptor, createHttpInterceptor } from "./src/http-interceptor.js";
|
|
24
24
|
import { createProxyManager, type ProxyManager } from "./src/proxy-manager.js";
|
|
25
|
-
import {
|
|
25
|
+
import { loadHostMap, isProxyRunning, getProxyVersion } from "./src/proxy-health.js";
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
28
|
* Find an executable in PATH using filesystem checks (no shell execution).
|
|
@@ -43,14 +43,24 @@ function findInPath(name: string): string | null {
|
|
|
43
43
|
return null;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
// Read plugin version from package.json
|
|
47
|
+
const pluginPkgPath = path.join(path.dirname(new URL(import.meta.url).pathname), 'package.json');
|
|
48
|
+
let PLUGIN_VERSION = 'unknown';
|
|
49
|
+
try { PLUGIN_VERSION = JSON.parse(fs.readFileSync(pluginPkgPath, 'utf-8')).version; } catch { /* ok */ }
|
|
50
|
+
|
|
46
51
|
let proxyManager: ProxyManager | null = null;
|
|
47
52
|
let httpInterceptor: HttpInterceptor | null = null;
|
|
48
|
-
let
|
|
53
|
+
let socketPath: string | null = null;
|
|
49
54
|
let dynamicHostMap: Map<string, string> | null = null;
|
|
50
|
-
const proxyPort = 8081;
|
|
51
55
|
const services = ["anthropic", "openai"];
|
|
52
56
|
|
|
53
|
-
/**
|
|
57
|
+
/** Default socket path */
|
|
58
|
+
function getDefaultSocketPath(): string {
|
|
59
|
+
const configDir = path.join(os.homedir(), '.aquaman');
|
|
60
|
+
return path.join(configDir, 'proxy.sock');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Fallback host map used when proxy doesn't provide one */
|
|
54
64
|
const FALLBACK_HOST_MAP = new Map<string, string>([
|
|
55
65
|
['api.anthropic.com', 'anthropic'],
|
|
56
66
|
['api.openai.com', 'openai'],
|
|
@@ -79,21 +89,6 @@ const FALLBACK_HOST_MAP = new Map<string, string>([
|
|
|
79
89
|
['chat.googleapis.com', 'google-chat'],
|
|
80
90
|
]);
|
|
81
91
|
|
|
82
|
-
/**
|
|
83
|
-
* Get external proxy URL from environment (for Docker two-container mode).
|
|
84
|
-
* When set, the plugin skips spawning a local proxy and routes traffic to the external URL.
|
|
85
|
-
*/
|
|
86
|
-
function getExternalProxyUrl(): string | null {
|
|
87
|
-
return process.env.AQUAMAN_PROXY_URL || null;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Get external client token from environment (for Docker two-container mode).
|
|
92
|
-
*/
|
|
93
|
-
function getExternalClientToken(): string | null {
|
|
94
|
-
return process.env.AQUAMAN_CLIENT_TOKEN || null;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
92
|
/**
|
|
98
93
|
* Check if aquaman CLI is installed (fs-based, no shell execution)
|
|
99
94
|
*/
|
|
@@ -104,12 +99,12 @@ function isAquamanInstalled(): boolean {
|
|
|
104
99
|
/**
|
|
105
100
|
* Start the aquaman proxy daemon using ProxyManager
|
|
106
101
|
*/
|
|
107
|
-
async function startProxy(
|
|
102
|
+
async function startProxy(log: OpenClawPluginApi["logger"]): Promise<boolean> {
|
|
108
103
|
try {
|
|
109
104
|
const mgr = createProxyManager({
|
|
110
|
-
config: {
|
|
105
|
+
config: {},
|
|
111
106
|
onReady: (info) => {
|
|
112
|
-
|
|
107
|
+
socketPath = info.socketPath;
|
|
113
108
|
if (info.hostMap && typeof info.hostMap === "object") {
|
|
114
109
|
dynamicHostMap = new Map(Object.entries(info.hostMap));
|
|
115
110
|
}
|
|
@@ -122,6 +117,7 @@ async function startProxy(port: number, log: OpenClawPluginApi["logger"]): Promi
|
|
|
122
117
|
});
|
|
123
118
|
await mgr.start();
|
|
124
119
|
proxyManager = mgr;
|
|
120
|
+
socketPath = mgr.getSocketPath();
|
|
125
121
|
return true;
|
|
126
122
|
} catch (err) {
|
|
127
123
|
log.error(`Failed to start proxy: ${err}`);
|
|
@@ -141,7 +137,7 @@ function stopProxy(): void {
|
|
|
141
137
|
proxyManager.stop();
|
|
142
138
|
proxyManager = null;
|
|
143
139
|
}
|
|
144
|
-
|
|
140
|
+
socketPath = null;
|
|
145
141
|
}
|
|
146
142
|
|
|
147
143
|
/**
|
|
@@ -149,16 +145,18 @@ function stopProxy(): void {
|
|
|
149
145
|
* This is what provides credential isolation for channels that don't support base URL overrides.
|
|
150
146
|
*/
|
|
151
147
|
function activateHttpInterceptor(log: OpenClawPluginApi["logger"]): void {
|
|
148
|
+
if (!socketPath) {
|
|
149
|
+
log.error("Cannot activate HTTP interceptor: no socket path");
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
152
153
|
// Use dynamic host map from proxy (includes custom services from services.yaml)
|
|
153
154
|
// Falls back to builtin map for backward compatibility
|
|
154
155
|
const hostMap = dynamicHostMap || FALLBACK_HOST_MAP;
|
|
155
156
|
|
|
156
|
-
const baseUrl = getExternalProxyUrl() || `http://127.0.0.1:${proxyPort}`;
|
|
157
|
-
|
|
158
157
|
httpInterceptor = createHttpInterceptor({
|
|
159
|
-
|
|
158
|
+
socketPath,
|
|
160
159
|
hostMap,
|
|
161
|
-
clientToken: clientToken || undefined,
|
|
162
160
|
log: (msg) => log.info(msg),
|
|
163
161
|
});
|
|
164
162
|
|
|
@@ -167,13 +165,11 @@ function activateHttpInterceptor(log: OpenClawPluginApi["logger"]): void {
|
|
|
167
165
|
}
|
|
168
166
|
|
|
169
167
|
/**
|
|
170
|
-
* Set environment variables for SDK clients
|
|
168
|
+
* Set environment variables for SDK clients using sentinel hostname
|
|
171
169
|
*/
|
|
172
170
|
function configureEnvironment(log: OpenClawPluginApi["logger"]): void {
|
|
173
|
-
const baseUrl = getExternalProxyUrl() || `http://127.0.0.1:${proxyPort}`;
|
|
174
|
-
|
|
175
171
|
for (const service of services) {
|
|
176
|
-
const serviceUrl =
|
|
172
|
+
const serviceUrl = `http://aquaman.local/${service}`;
|
|
177
173
|
|
|
178
174
|
switch (service) {
|
|
179
175
|
case "anthropic":
|
|
@@ -197,10 +193,9 @@ function configureEnvironment(log: OpenClawPluginApi["logger"]): void {
|
|
|
197
193
|
}
|
|
198
194
|
|
|
199
195
|
/**
|
|
200
|
-
* Register the aquaman_status tool
|
|
196
|
+
* Register the aquaman_status tool
|
|
201
197
|
*/
|
|
202
198
|
function registerStatusTool(api: OpenClawPluginApi): void {
|
|
203
|
-
const externalUrl = getExternalProxyUrl();
|
|
204
199
|
api.registerTool(
|
|
205
200
|
() => {
|
|
206
201
|
return {
|
|
@@ -214,10 +209,8 @@ function registerStatusTool(api: OpenClawPluginApi): void {
|
|
|
214
209
|
},
|
|
215
210
|
async execute() {
|
|
216
211
|
return {
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
proxyRunning: externalUrl !== null || proxyManager !== null,
|
|
220
|
-
proxyPort,
|
|
212
|
+
proxyRunning: proxyManager !== null,
|
|
213
|
+
socketPath: socketPath || getDefaultSocketPath(),
|
|
221
214
|
services,
|
|
222
215
|
httpInterceptorActive: httpInterceptor?.isActive() ?? false,
|
|
223
216
|
environmentVariables: Object.fromEntries(
|
|
@@ -292,37 +285,6 @@ export default function register(api: OpenClawPluginApi): void {
|
|
|
292
285
|
// Auto-generate auth-profiles.json if missing
|
|
293
286
|
ensureAuthProfiles(api.logger);
|
|
294
287
|
|
|
295
|
-
const externalUrl = getExternalProxyUrl();
|
|
296
|
-
|
|
297
|
-
// External proxy mode (Docker two-container architecture)
|
|
298
|
-
if (externalUrl) {
|
|
299
|
-
api.logger.info(`External proxy mode: ${externalUrl}`);
|
|
300
|
-
clientToken = getExternalClientToken();
|
|
301
|
-
configureEnvironment(api.logger);
|
|
302
|
-
|
|
303
|
-
if (api.registerLifecycle) {
|
|
304
|
-
api.registerLifecycle({
|
|
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;
|
|
309
|
-
activateHttpInterceptor(api.logger);
|
|
310
|
-
api.logger.info("HTTP interceptor active (external proxy mode)");
|
|
311
|
-
},
|
|
312
|
-
async onGatewayStop() {
|
|
313
|
-
if (httpInterceptor) {
|
|
314
|
-
httpInterceptor.deactivate();
|
|
315
|
-
httpInterceptor = null;
|
|
316
|
-
}
|
|
317
|
-
},
|
|
318
|
-
});
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
registerStatusTool(api);
|
|
322
|
-
api.logger.info("Aquaman plugin registered successfully");
|
|
323
|
-
return;
|
|
324
|
-
}
|
|
325
|
-
|
|
326
288
|
// Local proxy mode — requires aquaman CLI
|
|
327
289
|
if (!isAquamanInstalled()) {
|
|
328
290
|
api.logger.warn(
|
|
@@ -337,34 +299,47 @@ export default function register(api: OpenClawPluginApi): void {
|
|
|
337
299
|
|
|
338
300
|
api.logger.info("aquaman CLI found, will start proxy on gateway start");
|
|
339
301
|
|
|
340
|
-
// Configure environment variables immediately
|
|
302
|
+
// Configure environment variables immediately (sentinel hostname)
|
|
341
303
|
configureEnvironment(api.logger);
|
|
342
304
|
|
|
343
305
|
// Register lifecycle hooks if available
|
|
344
306
|
if (api.registerLifecycle) {
|
|
345
307
|
api.registerLifecycle({
|
|
346
308
|
async onGatewayStart() {
|
|
347
|
-
api.logger.info(
|
|
309
|
+
api.logger.info("Starting aquaman proxy...");
|
|
348
310
|
|
|
349
|
-
const started = await startProxy(
|
|
350
|
-
if (started) {
|
|
311
|
+
const started = await startProxy(api.logger);
|
|
312
|
+
if (started && socketPath) {
|
|
351
313
|
api.logger.info("Aquaman proxy started successfully");
|
|
314
|
+
|
|
315
|
+
// Check for version mismatch between plugin and proxy
|
|
316
|
+
const proxyVersion = await getProxyVersion(socketPath);
|
|
317
|
+
if (proxyVersion && proxyVersion !== PLUGIN_VERSION) {
|
|
318
|
+
api.logger.warn(
|
|
319
|
+
`Warning: plugin version ${PLUGIN_VERSION} \u2260 proxy version ${proxyVersion}. ` +
|
|
320
|
+
`Update both: npm install -g aquaman-proxy && openclaw plugins install aquaman-plugin`
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
|
|
352
324
|
// Activate HTTP interceptor to redirect channel traffic through proxy
|
|
353
325
|
activateHttpInterceptor(api.logger);
|
|
354
326
|
} else {
|
|
355
|
-
api.logger.error(
|
|
356
|
-
|
|
357
|
-
);
|
|
358
|
-
|
|
359
|
-
const alreadyRunning = await isProxyRunning(proxyPort);
|
|
327
|
+
api.logger.error("Failed to start aquaman proxy");
|
|
328
|
+
// Check if another instance is already running
|
|
329
|
+
const defaultSock = getDefaultSocketPath();
|
|
330
|
+
const alreadyRunning = await isProxyRunning(defaultSock);
|
|
360
331
|
if (alreadyRunning) {
|
|
332
|
+
socketPath = defaultSock;
|
|
361
333
|
api.logger.info(
|
|
362
|
-
|
|
334
|
+
"Another aquaman instance is already running — using it"
|
|
363
335
|
);
|
|
336
|
+
// Load host map from existing proxy
|
|
337
|
+
const map = await loadHostMap(defaultSock);
|
|
338
|
+
dynamicHostMap = map.size > 0 ? map : FALLBACK_HOST_MAP;
|
|
364
339
|
activateHttpInterceptor(api.logger);
|
|
365
340
|
} else {
|
|
366
341
|
api.logger.error(
|
|
367
|
-
|
|
342
|
+
"No running proxy found. Check: aquaman doctor"
|
|
368
343
|
);
|
|
369
344
|
}
|
|
370
345
|
}
|
|
@@ -391,7 +366,7 @@ export default function register(api: OpenClawPluginApi): void {
|
|
|
391
366
|
.action(() => {
|
|
392
367
|
console.log("\nAquaman Status:");
|
|
393
368
|
console.log(` Proxy running: ${proxyManager !== null}`);
|
|
394
|
-
console.log(`
|
|
369
|
+
console.log(` Socket path: ${socketPath || getDefaultSocketPath()}`);
|
|
395
370
|
console.log(` Services: ${services.join(", ")}`);
|
|
396
371
|
console.log("\nEnvironment Variables:");
|
|
397
372
|
for (const service of services) {
|
package/openclaw.plugin.json
CHANGED
|
@@ -12,15 +12,9 @@
|
|
|
12
12
|
"type": "object",
|
|
13
13
|
"additionalProperties": false,
|
|
14
14
|
"properties": {
|
|
15
|
-
"mode": {
|
|
16
|
-
"type": "string",
|
|
17
|
-
"enum": ["embedded", "proxy"],
|
|
18
|
-
"default": "embedded",
|
|
19
|
-
"description": "embedded: credentials in gateway memory, proxy: separate process"
|
|
20
|
-
},
|
|
21
15
|
"backend": {
|
|
22
16
|
"type": "string",
|
|
23
|
-
"enum": ["keychain", "1password", "vault", "encrypted-file"],
|
|
17
|
+
"enum": ["keychain", "1password", "vault", "encrypted-file", "keepassxc"],
|
|
24
18
|
"default": "keychain",
|
|
25
19
|
"description": "Credential storage backend"
|
|
26
20
|
},
|
|
@@ -29,11 +23,6 @@
|
|
|
29
23
|
"items": { "type": "string" },
|
|
30
24
|
"default": ["anthropic", "openai"],
|
|
31
25
|
"description": "Services to proxy credentials for"
|
|
32
|
-
},
|
|
33
|
-
"proxyPort": {
|
|
34
|
-
"type": "number",
|
|
35
|
-
"default": 8081,
|
|
36
|
-
"description": "Port for credential proxy (proxy mode only)"
|
|
37
26
|
}
|
|
38
27
|
}
|
|
39
28
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aquaman-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Credential isolation plugin for OpenClaw",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -21,10 +21,12 @@
|
|
|
21
21
|
],
|
|
22
22
|
"author": "tech4242",
|
|
23
23
|
"license": "MIT",
|
|
24
|
-
"dependencies": {
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"undici": "^7.0.0"
|
|
26
|
+
},
|
|
25
27
|
"peerDependencies": {
|
|
26
28
|
"openclaw": ">=2026.1.0",
|
|
27
|
-
"aquaman-proxy": "0.
|
|
29
|
+
"aquaman-proxy": "0.7.0"
|
|
28
30
|
},
|
|
29
31
|
"peerDependenciesMeta": {
|
|
30
32
|
"aquaman-proxy": {
|