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 +16 -5
- package/index.ts +19 -3
- package/openclaw.plugin.json +1 -1
- package/package.json +2 -2
- package/src/config-schema.ts +2 -1
- package/src/proxy-health.ts +21 -2
- package/src/proxy-manager.ts +10 -1
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 #
|
|
37
|
-
aquaman setup #
|
|
38
|
-
|
|
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 {
|
|
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
|
-
//
|
|
307
|
-
const map = await
|
|
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 {
|
package/openclaw.plugin.json
CHANGED
|
@@ -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.
|
|
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.
|
|
27
|
+
"aquaman-proxy": "0.6.0"
|
|
28
28
|
},
|
|
29
29
|
"peerDependenciesMeta": {
|
|
30
30
|
"aquaman-proxy": {
|
package/src/config-schema.ts
CHANGED
|
@@ -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
|
/**
|
package/src/proxy-health.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
+
}
|
package/src/proxy-manager.ts
CHANGED
|
@@ -139,7 +139,16 @@ export class ProxyManager {
|
|
|
139
139
|
this.options.onExit?.(code);
|
|
140
140
|
|
|
141
141
|
if (!this.connectionInfo) {
|
|
142
|
-
const
|
|
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
|
});
|