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 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 │──request────>│ Keychain / 1Pass / │
12
- │ = localhost:8081 Vault / Encrypted │
13
- │<─response────│
14
- │ fetch() interceptor │──channel────>│ + Auth injected: │
15
- │ redirects channel │ traffic │ header / url-path │
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
- Nothing to steal. │ │
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 # 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
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
- | `mode` | `"embedded"` \| `"proxy"` | `"embedded"` | Isolation mode |
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 (TLS, audit, vault) go in `~/.aquaman/config.yaml`.
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 { fetchHostMap, isProxyRunning } from "./src/proxy-health.js";
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 clientToken: string | null = null;
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
- /** Fallback host map used when proxy doesn't provide one (backward compat) */
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(port: number, log: OpenClawPluginApi["logger"]): Promise<boolean> {
102
+ async function startProxy(log: OpenClawPluginApi["logger"]): Promise<boolean> {
108
103
  try {
109
104
  const mgr = createProxyManager({
110
- config: { proxyPort: port },
105
+ config: {},
111
106
  onReady: (info) => {
112
- clientToken = info.token || null;
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
- clientToken = null;
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
- proxyBaseUrl: baseUrl,
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 = `${baseUrl}/${service}`;
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 (shared between local and external proxy modes)
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
- externalProxy: externalUrl !== null,
218
- proxyUrl: externalUrl || `http://127.0.0.1:${proxyPort}`,
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(`Starting aquaman proxy on port ${proxyPort}...`);
309
+ api.logger.info("Starting aquaman proxy...");
348
310
 
349
- const started = await startProxy(proxyPort, api.logger);
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
- `Failed to start aquaman proxy on port ${proxyPort}`
357
- );
358
- // Check if another instance is already running on the port
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
- `Another aquaman instance is already running on port ${proxyPort} — using it`
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
- `Port ${proxyPort} may be in use. Check with: lsof -i :${proxyPort}`
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(` Proxy port: ${proxyPort}`);
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) {
@@ -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.5.1",
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.5.1"
29
+ "aquaman-proxy": "0.7.0"
28
30
  },
29
31
  "peerDependenciesMeta": {
30
32
  "aquaman-proxy": {