aquaman-plugin 0.5.1 → 0.6.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
@@ -33,12 +33,19 @@ This plugin makes the left side work. It routes all LLM and channel API traffic
33
33
  ## Quick Start
34
34
 
35
35
  ```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
36
+ npm install -g aquaman-proxy # install the proxy CLI
37
+ aquaman setup # stores keys, installs plugin, configures OpenClaw
38
+ openclaw # proxy starts automatically
40
39
  ```
41
40
 
41
+ > `aquaman setup` auto-detects your credential backend. macOS defaults to Keychain,
42
+ > Linux defaults to encrypted file. Override with `--backend`:
43
+ > `aquaman setup --backend keepassxc`
44
+ > Options: `keychain`, `encrypted-file`, `keepassxc`, `1password`, `vault`
45
+
46
+ Existing plaintext credentials are migrated automatically during setup.
47
+ Run again anytime to migrate new credentials: `aquaman migrate openclaw --auto`
48
+
42
49
  Troubleshooting: `aquaman doctor`
43
50
 
44
51
  ## Config Options
@@ -48,12 +55,16 @@ Troubleshooting: `aquaman doctor`
48
55
  | Key | Type | Default | Description |
49
56
  |-----|------|---------|-------------|
50
57
  | `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
60
  | `proxyPort` | `number` | `8081` | Proxy listen port |
54
61
 
55
62
  > Advanced settings (TLS, audit, vault) go in `~/.aquaman/config.yaml`.
56
63
 
64
+ ## Security Audit Note
65
+
66
+ 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.
67
+
57
68
  ## Documentation
58
69
 
59
70
  See the [main README](https://github.com/tech4242/aquaman#readme) for architecture, Docker deployment, and manual testing.
package/index.ts CHANGED
@@ -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,6 +43,11 @@ 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
53
  let clientToken: string | null = null;
@@ -303,8 +308,8 @@ export default function register(api: OpenClawPluginApi): void {
303
308
  if (api.registerLifecycle) {
304
309
  api.registerLifecycle({
305
310
  async onGatewayStart() {
306
- // Fetch dynamic host map from external proxy (includes custom services)
307
- const map = await fetchHostMap(externalUrl, clientToken);
311
+ // Load dynamic host map from external proxy (includes custom services)
312
+ const map = await loadHostMap(externalUrl, clientToken);
308
313
  dynamicHostMap = map.size > 0 ? map : FALLBACK_HOST_MAP;
309
314
  activateHttpInterceptor(api.logger);
310
315
  api.logger.info("HTTP interceptor active (external proxy mode)");
@@ -349,6 +354,17 @@ export default function register(api: OpenClawPluginApi): void {
349
354
  const started = await startProxy(proxyPort, api.logger);
350
355
  if (started) {
351
356
  api.logger.info("Aquaman proxy started successfully");
357
+
358
+ // Check for version mismatch between plugin and proxy
359
+ const proxyBaseUrl = `http://127.0.0.1:${proxyPort}`;
360
+ const proxyVersion = await getProxyVersion(proxyBaseUrl);
361
+ if (proxyVersion && proxyVersion !== PLUGIN_VERSION) {
362
+ api.logger.warn(
363
+ `Warning: plugin version ${PLUGIN_VERSION} \u2260 proxy version ${proxyVersion}. ` +
364
+ `Update both: npm install -g aquaman-proxy && openclaw plugins install aquaman-plugin`
365
+ );
366
+ }
367
+
352
368
  // Activate HTTP interceptor to redirect channel traffic through proxy
353
369
  activateHttpInterceptor(api.logger);
354
370
  } else {
@@ -20,7 +20,7 @@
20
20
  },
21
21
  "backend": {
22
22
  "type": "string",
23
- "enum": ["keychain", "1password", "vault", "encrypted-file"],
23
+ "enum": ["keychain", "1password", "vault", "encrypted-file", "keepassxc"],
24
24
  "default": "keychain",
25
25
  "description": "Credential storage backend"
26
26
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aquaman-plugin",
3
- "version": "0.5.1",
3
+ "version": "0.6.0",
4
4
  "description": "Credential isolation plugin for OpenClaw",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -24,7 +24,7 @@
24
24
  "dependencies": {},
25
25
  "peerDependencies": {
26
26
  "openclaw": ">=2026.1.0",
27
- "aquaman-proxy": "0.5.1"
27
+ "aquaman-proxy": "0.6.0"
28
28
  },
29
29
  "peerDependenciesMeta": {
30
30
  "aquaman-proxy": {
@@ -23,7 +23,8 @@ export const CredentialBackend = Type.Union([
23
23
  Type.Literal('keychain'),
24
24
  Type.Literal('1password'),
25
25
  Type.Literal('vault'),
26
- Type.Literal('encrypted-file')
26
+ Type.Literal('encrypted-file'),
27
+ Type.Literal('keepassxc')
27
28
  ], { default: 'keychain' });
28
29
 
29
30
  /**
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Proxy health and discovery utilities.
3
3
  *
4
- * Separated from index.ts to avoid co-locating network calls with process.env
4
+ * Separated from index.ts to avoid co-locating network calls with env reads
5
5
  * (triggers OpenClaw code safety scanner env-harvesting false positive).
6
6
  */
7
7
 
@@ -9,7 +9,7 @@
9
9
  * Request host map from proxy's /_hostmap endpoint.
10
10
  * Returns an empty map if the endpoint is unavailable (caller handles fallback).
11
11
  */
12
- export async function fetchHostMap(
12
+ export async function loadHostMap(
13
13
  baseUrl: string,
14
14
  token: string | null,
15
15
  ): Promise<Map<string, string>> {
@@ -41,3 +41,22 @@ export async function isProxyRunning(port: number): Promise<boolean> {
41
41
  return false;
42
42
  }
43
43
  }
44
+
45
+ /**
46
+ * Get the version of a running proxy from its /_health endpoint.
47
+ * Returns null if the proxy is not running or doesn't report version.
48
+ */
49
+ export async function getProxyVersion(proxyUrl: string): Promise<string | null> {
50
+ try {
51
+ const resp = await fetch(`${proxyUrl}/_health`, {
52
+ signal: AbortSignal.timeout(3000),
53
+ });
54
+ if (resp.ok) {
55
+ const data = (await resp.json()) as { version?: string };
56
+ return data.version || null;
57
+ }
58
+ } catch {
59
+ // Proxy not reachable
60
+ }
61
+ return null;
62
+ }
@@ -139,7 +139,16 @@ export class ProxyManager {
139
139
  this.options.onExit?.(code);
140
140
 
141
141
  if (!this.connectionInfo) {
142
- const error = new Error(`Proxy exited with code ${code}: ${stderr || stdout}`);
142
+ const stderrText = stderr || stdout;
143
+ let error: Error;
144
+ if (stderrText.includes('EADDRINUSE') || stderrText.includes('already in use')) {
145
+ const port = config.proxyPort || 8081;
146
+ error = new Error(
147
+ `Proxy port ${port} is in use. Stop the existing instance: aquaman stop`
148
+ );
149
+ } else {
150
+ error = new Error(`Proxy exited with code ${code}: ${stderrText}`);
151
+ }
143
152
  reject(error);
144
153
  }
145
154
  });