aquaman-plugin 0.11.2 → 0.11.4

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
@@ -51,6 +51,42 @@ The `aquaman` proxy binary is bundled as an npm dependency — no separate downl
51
51
  > **Using npm?** `npm install -g aquaman-proxy && aquaman setup` does
52
52
  > the same thing. Use this if you prefer managing packages with npm.
53
53
 
54
+ ## Security model
55
+
56
+ Aquaman keeps API credentials out of the agent process by running them in a separate proxy process. The agent never sees the secret — only a sentinel base URL that the proxy intercepts, authenticates, and forwards. See the [architecture diagram in the main README](https://github.com/tech4242/aquaman#architecture-decision-isolation-vs-detection).
57
+
58
+ **Proxy process**
59
+
60
+ - The plugin spawns the `aquaman` binary from the `aquaman-proxy` npm package, which is declared as an exact-pinned dependency (no semver range) in the plugin's `package.json` and published by the same author. After spawn the plugin checks the running proxy's reported version against the plugin's own and warns if they disagree.
61
+ - The spawn is what triggers the `dangerous-exec` finding in OpenClaw's static scanner — it's intentional and is the whole point of the plugin.
62
+
63
+ **HTTP interceptor**
64
+
65
+ - Only services listed in the plugin's `services` config get their traffic redirected to the local proxy. As of v0.11.4, the interceptor filters its known-host map by your `services` list — channels you didn't opt into keep talking to the upstream directly.
66
+ - The interceptor uses a Unix Domain Socket (no TCP, no network exposure).
67
+
68
+ **Auth profiles**
69
+
70
+ - On load the plugin writes `~/.openclaw/agents/<id>/agent/auth-profiles.json` with placeholder API-key entries for `anthropic` and `openai` so OpenClaw doesn't reject requests before they reach the proxy. The proxy strips the placeholder and injects the real credential.
71
+ - The plugin never overwrites an existing `auth-profiles.json`. To suppress the generation entirely, set `autoGenerateAuthProfiles: false` in the plugin config (v0.11.4+).
72
+
73
+ **Audit log**
74
+
75
+ - Every credential use is recorded in `~/.aquaman/audit/current.jsonl` with a SHA-256 hash chain so tampering is detectable. The log stays local — no telemetry.
76
+ - `aquaman doctor` surfaces audit log issues; `aquaman audit tail` shows recent entries.
77
+ - Operators can constrain which upstream endpoints get proxied (and therefore credentialed) via the `policy` config in `~/.aquaman/config.yaml`. Denied requests return 403 before any credential is injected.
78
+
79
+ ### Scanner findings
80
+
81
+ `openclaw security audit --deep` reports two expected findings:
82
+
83
+ - **`dangerous-exec`** on the proxy-manager module — the plugin spawns the proxy as a separate process. This is how credential isolation works.
84
+ - **`tools_reachable_permissive_policy`** — advisory about your tool policy, not an aquaman vulnerability. Set `"tools": { "profile": "coding" }` in `openclaw.json` if your agents handle untrusted input.
85
+
86
+ ClawHub's ClawScan additionally produces a higher-level review of plugin behavior. The current scan acknowledges credential isolation, proxy spawn, the host map, the auth-profiles generation, and the audit log — see the publisher note on the package page for context on each item.
87
+
88
+ `aquaman setup` adds the plugin to `plugins.allow` automatically.
89
+
54
90
  ## Available Commands
55
91
 
56
92
  All commands work via OpenClaw CLI or your terminal:
@@ -77,19 +113,11 @@ Troubleshooting: `openclaw aquaman doctor` or `aquaman doctor`
77
113
  | Key | Type | Default | Description |
78
114
  |-----|------|---------|-------------|
79
115
  | `backend` | `"keychain"` \| `"1password"` \| `"vault"` \| `"encrypted-file"` \| `"keepassxc"` \| `"systemd-creds"` \| `"bitwarden"` | `"keychain"` | Credential store |
80
- | `services` | `string[]` | `["anthropic", "openai"]` | Services to proxy |
116
+ | `services` | `string[]` | `["anthropic", "openai"]` | Services to proxy (also gates which hostnames the interceptor redirects, v0.11.4+) |
117
+ | `autoGenerateAuthProfiles` | `boolean` | `true` | Auto-generate `auth-profiles.json` with placeholder anthropic/openai entries when the file is absent. Set `false` to manage your own (v0.11.4+) |
81
118
 
82
119
  > Advanced settings (audit, vault, request policies) go in `~/.aquaman/config.yaml`. See [request policy docs](https://github.com/tech4242/aquaman#request-policies).
83
120
 
84
- ## Security Audit
85
-
86
- `openclaw security audit --deep` reports two expected findings:
87
-
88
- - **`dangerous-exec`** on `proxy-manager.ts` — the plugin spawns the proxy as a separate process. This is how credential isolation works.
89
- - **`tools_reachable_permissive_policy`** — advisory about your tool policy, not an aquaman vulnerability. Set `"tools": { "profile": "coding" }` in `openclaw.json` if your agents handle untrusted input.
90
-
91
- `aquaman setup` adds the plugin to `plugins.allow` automatically.
92
-
93
121
  ## Documentation
94
122
 
95
123
  See the [main README](https://github.com/tech4242/aquaman#readme) for the full security model, architecture diagrams, request policy config, and manual testing guides.
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Aquaman OpenClaw Plugin
3
+ *
4
+ * Credential isolation for OpenClaw.
5
+ * Credentials never enter the agent process - they're managed by a separate proxy.
6
+ *
7
+ * Usage:
8
+ * 1. Install aquaman: npm install -g aquaman-proxy
9
+ * 2. Store credentials: aquaman credentials add anthropic api_key
10
+ * 3. Enable this plugin in openclaw.json
11
+ *
12
+ * The plugin will:
13
+ * - Start the aquaman proxy on plugin load
14
+ * - Set ANTHROPIC_BASE_URL, OPENAI_BASE_URL etc. to route through proxy via UDS
15
+ * - The proxy injects credentials into requests
16
+ * - Agent never sees the actual API keys
17
+ */
18
+ interface OpenClawPluginLogger {
19
+ info(msg: string): void;
20
+ warn(msg: string): void;
21
+ error(msg: string): void;
22
+ }
23
+ interface OpenClawPluginApi {
24
+ logger: OpenClawPluginLogger;
25
+ pluginConfig: unknown;
26
+ registerService(def: {
27
+ id: string;
28
+ start(ctx: {
29
+ logger: OpenClawPluginLogger;
30
+ }): void | Promise<void>;
31
+ stop(ctx: {
32
+ logger: OpenClawPluginLogger;
33
+ }): void | Promise<void>;
34
+ }): void;
35
+ registerCommand(def: {
36
+ name: string;
37
+ description: string;
38
+ acceptsArgs: boolean;
39
+ requireAuth: boolean;
40
+ handler(): Promise<{
41
+ text: string;
42
+ }>;
43
+ }): void;
44
+ registerCli?(fn: (opts: {
45
+ program: any;
46
+ }) => void, opts: {
47
+ commands: string[];
48
+ }): void;
49
+ registerTool(factory: () => {
50
+ name: string;
51
+ label: string;
52
+ description: string;
53
+ parameters: {
54
+ type: "object";
55
+ properties: Record<string, unknown>;
56
+ required: string[];
57
+ };
58
+ execute(toolCallId: string, params: unknown): Promise<{
59
+ content: {
60
+ type: "text";
61
+ text: string;
62
+ }[];
63
+ details: unknown;
64
+ }>;
65
+ }, opts: {
66
+ names: string[];
67
+ }): void;
68
+ }
69
+ type OpenClawPluginDefinition = {
70
+ id?: string;
71
+ name?: string;
72
+ description?: string;
73
+ version?: string;
74
+ register?: (api: OpenClawPluginApi) => void | Promise<void>;
75
+ };
76
+ /**
77
+ * Aquaman OpenClaw Plugin Definition
78
+ */
79
+ declare const plugin: OpenClawPluginDefinition;
80
+ export default plugin;
81
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AASH,UAAU,oBAAoB;IAC5B,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAED,UAAU,iBAAiB;IACzB,MAAM,EAAE,oBAAoB,CAAC;IAC7B,YAAY,EAAE,OAAO,CAAC;IACtB,eAAe,CAAC,GAAG,EAAE;QACnB,EAAE,EAAE,MAAM,CAAC;QACX,KAAK,CAAC,GAAG,EAAE;YAAE,MAAM,EAAE,oBAAoB,CAAA;SAAE,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;QACnE,IAAI,CAAC,GAAG,EAAE;YAAE,MAAM,EAAE,oBAAoB,CAAA;SAAE,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;KACnE,GAAG,IAAI,CAAC;IACT,eAAe,CAAC,GAAG,EAAE;QACnB,IAAI,EAAE,MAAM,CAAC;QACb,WAAW,EAAE,MAAM,CAAC;QACpB,WAAW,EAAE,OAAO,CAAC;QACrB,WAAW,EAAE,OAAO,CAAC;QACrB,OAAO,IAAI,OAAO,CAAC;YAAE,IAAI,EAAE,MAAM,CAAA;SAAE,CAAC,CAAC;KACtC,GAAG,IAAI,CAAC;IACT,WAAW,CAAC,CACV,EAAE,EAAE,CAAC,IAAI,EAAE;QAAE,OAAO,EAAE,GAAG,CAAA;KAAE,KAAK,IAAI,EACpC,IAAI,EAAE;QAAE,QAAQ,EAAE,MAAM,EAAE,CAAA;KAAE,GAC3B,IAAI,CAAC;IACR,YAAY,CACV,OAAO,EAAE,MAAM;QACb,IAAI,EAAE,MAAM,CAAC;QACb,KAAK,EAAE,MAAM,CAAC;QACd,WAAW,EAAE,MAAM,CAAC;QACpB,UAAU,EAAE;YAAE,IAAI,EAAE,QAAQ,CAAC;YAAC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;YAAC,QAAQ,EAAE,MAAM,EAAE,CAAA;SAAE,CAAC;QACxF,OAAO,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,GAAG,OAAO,CAAC;YACpD,OAAO,EAAE;gBAAE,IAAI,EAAE,MAAM,CAAC;gBAAC,IAAI,EAAE,MAAM,CAAA;aAAE,EAAE,CAAC;YAC1C,OAAO,EAAE,OAAO,CAAC;SAClB,CAAC,CAAC;KACJ,EACD,IAAI,EAAE;QAAE,KAAK,EAAE,MAAM,EAAE,CAAA;KAAE,GACxB,IAAI,CAAC;CACT;AAED,KAAK,wBAAwB,GAAG;IAC9B,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,CAAC,GAAG,EAAE,iBAAiB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC7D,CAAC;AAqSF;;GAEG;AACH,QAAA,MAAM,MAAM,EAAE,wBAqPb,CAAC;AAEF,eAAe,MAAM,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,502 @@
1
+ /**
2
+ * Aquaman OpenClaw Plugin
3
+ *
4
+ * Credential isolation for OpenClaw.
5
+ * Credentials never enter the agent process - they're managed by a separate proxy.
6
+ *
7
+ * Usage:
8
+ * 1. Install aquaman: npm install -g aquaman-proxy
9
+ * 2. Store credentials: aquaman credentials add anthropic api_key
10
+ * 3. Enable this plugin in openclaw.json
11
+ *
12
+ * The plugin will:
13
+ * - Start the aquaman proxy on plugin load
14
+ * - Set ANTHROPIC_BASE_URL, OPENAI_BASE_URL etc. to route through proxy via UDS
15
+ * - The proxy injects credentials into requests
16
+ * - Agent never sees the actual API keys
17
+ */
18
+ import * as fs from "node:fs";
19
+ import * as path from "node:path";
20
+ import * as os from "node:os";
21
+ import { createHttpInterceptor } from "./src/http-interceptor.js";
22
+ import { createProxyManager, findAquamanProxyBinary, execAquamanProxyCli, execAquamanProxyInteractive } from "./src/proxy-manager.js";
23
+ import { loadHostMap, isProxyRunning, getProxyVersion } from "./src/proxy-health.js";
24
+ /**
25
+ * Find an executable in PATH using filesystem checks (no shell execution).
26
+ * Avoids execSync("which ...") which triggers dangerous-exec security audit flags.
27
+ */
28
+ function findInPath(name) {
29
+ const pathEnv = process.env.PATH || "";
30
+ const dirs = pathEnv.split(path.delimiter);
31
+ for (const dir of dirs) {
32
+ const candidate = path.join(dir, name);
33
+ try {
34
+ fs.accessSync(candidate, fs.constants.X_OK);
35
+ return candidate;
36
+ }
37
+ catch {
38
+ // Not found or not executable in this dir
39
+ }
40
+ }
41
+ return null;
42
+ }
43
+ // Read plugin version from package.json
44
+ const pluginPkgPath = path.join(path.dirname(new URL(import.meta.url).pathname), 'package.json');
45
+ let PLUGIN_VERSION = 'unknown';
46
+ try {
47
+ PLUGIN_VERSION = JSON.parse(fs.readFileSync(pluginPkgPath, 'utf-8')).version;
48
+ }
49
+ catch { /* ok */ }
50
+ let proxyManager = null;
51
+ let httpInterceptor = null;
52
+ let socketPath = null;
53
+ let dynamicHostMap = null;
54
+ let configuredServices = ["anthropic", "openai"];
55
+ /** Default socket path */
56
+ function getDefaultSocketPath() {
57
+ const configDir = path.join(os.homedir(), '.aquaman');
58
+ return path.join(configDir, 'proxy.sock');
59
+ }
60
+ /** Fallback host map used when proxy doesn't provide one */
61
+ const FALLBACK_HOST_MAP = new Map([
62
+ ['api.anthropic.com', 'anthropic'],
63
+ ['api.openai.com', 'openai'],
64
+ ['api.github.com', 'github'],
65
+ ['api.x.ai', 'xai'],
66
+ ['gateway.ai.cloudflare.com', 'cloudflare-ai'],
67
+ ['api.mistral.ai', 'mistral'],
68
+ ['api-inference.huggingface.co', 'huggingface'],
69
+ ['slack.com', 'slack'],
70
+ ['*.slack.com', 'slack'],
71
+ ['discord.com', 'discord'],
72
+ ['*.discord.com', 'discord'],
73
+ ['api.telegram.org', 'telegram'],
74
+ ['matrix.org', 'matrix'],
75
+ ['*.matrix.org', 'matrix'],
76
+ ['api.line.me', 'line'],
77
+ ['api-data.line.me', 'line'],
78
+ ['api.twitch.tv', 'twitch'],
79
+ ['id.twitch.tv', 'twitch'],
80
+ ['api.twilio.com', 'twilio'],
81
+ ['*.twilio.com', 'twilio'],
82
+ ['api.telnyx.com', 'telnyx'],
83
+ ['api.elevenlabs.io', 'elevenlabs'],
84
+ ['openapi.zalo.me', 'zalo'],
85
+ ['graph.microsoft.com', 'ms-teams'],
86
+ ['open.feishu.cn', 'feishu'],
87
+ ['open.larksuite.com', 'feishu'],
88
+ ['chat.googleapis.com', 'google-chat'],
89
+ ]);
90
+ /**
91
+ * Check if aquaman proxy binary is available (local node_modules or PATH)
92
+ */
93
+ function isAquamanProxyInstalled() {
94
+ return findAquamanProxyBinary() !== null;
95
+ }
96
+ /**
97
+ * Start the aquaman proxy daemon using ProxyManager
98
+ */
99
+ async function startProxy(log) {
100
+ try {
101
+ const mgr = createProxyManager({
102
+ config: {},
103
+ onReady: (info) => {
104
+ socketPath = info.socketPath;
105
+ if (info.hostMap && typeof info.hostMap === "object") {
106
+ dynamicHostMap = new Map(Object.entries(info.hostMap));
107
+ }
108
+ },
109
+ onError: (err) => log.error(`Proxy error: ${err.message}`),
110
+ onExit: (code) => {
111
+ proxyManager = null;
112
+ log.warn(`Proxy exited with code ${code}`);
113
+ },
114
+ });
115
+ await mgr.start();
116
+ proxyManager = mgr;
117
+ socketPath = mgr.getSocketPath();
118
+ return true;
119
+ }
120
+ catch (err) {
121
+ log.error(`Failed to start proxy: ${err}`);
122
+ return false;
123
+ }
124
+ }
125
+ /**
126
+ * Stop the proxy daemon and deactivate the HTTP interceptor
127
+ */
128
+ function stopProxy() {
129
+ if (httpInterceptor) {
130
+ httpInterceptor.deactivate();
131
+ httpInterceptor = null;
132
+ }
133
+ if (proxyManager) {
134
+ proxyManager.stop();
135
+ proxyManager = null;
136
+ }
137
+ socketPath = null;
138
+ }
139
+ /**
140
+ * Activate the HTTP interceptor to redirect channel API traffic through the proxy.
141
+ * This is what provides credential isolation for channels that don't support base URL overrides.
142
+ */
143
+ function activateHttpInterceptor(log) {
144
+ if (!socketPath) {
145
+ log.error("Cannot activate HTTP interceptor: no socket path");
146
+ return;
147
+ }
148
+ // Use dynamic host map from proxy (includes custom services from services.yaml)
149
+ // Falls back to builtin map for backward compatibility.
150
+ const sourceMap = dynamicHostMap || FALLBACK_HOST_MAP;
151
+ // Restrict interception to services the operator opted into. The interceptor
152
+ // only redirects traffic to hosts whose service value appears in the plugin's
153
+ // `services` config. Channels not in `services` keep their normal direct-to-
154
+ // upstream behavior. (Closes ClawScan ASI02.)
155
+ const allowed = new Set(configuredServices);
156
+ const effectiveHostMap = new Map();
157
+ for (const [host, service] of sourceMap) {
158
+ if (allowed.has(service))
159
+ effectiveHostMap.set(host, service);
160
+ }
161
+ httpInterceptor = createHttpInterceptor({
162
+ socketPath,
163
+ hostMap: effectiveHostMap,
164
+ log: (msg) => log.info(msg),
165
+ });
166
+ httpInterceptor.activate();
167
+ const skipped = sourceMap.size - effectiveHostMap.size;
168
+ log.info(`HTTP interceptor active: ${effectiveHostMap.size} host patterns redirected through proxy` +
169
+ (skipped > 0 ? ` (${skipped} known patterns skipped — not in plugin services config)` : ""));
170
+ }
171
+ /**
172
+ * Set environment variables for SDK clients using sentinel hostname
173
+ */
174
+ function configureEnvironment(log, services) {
175
+ for (const service of services) {
176
+ const serviceUrl = `http://aquaman.local/${service}`;
177
+ switch (service) {
178
+ case "anthropic":
179
+ process.env["ANTHROPIC_BASE_URL"] = serviceUrl;
180
+ log.info(`Set ANTHROPIC_BASE_URL=${serviceUrl}`);
181
+ break;
182
+ case "openai":
183
+ process.env["OPENAI_BASE_URL"] = serviceUrl;
184
+ log.info(`Set OPENAI_BASE_URL=${serviceUrl}`);
185
+ break;
186
+ case "github":
187
+ process.env["GITHUB_API_URL"] = serviceUrl;
188
+ log.info(`Set GITHUB_API_URL=${serviceUrl}`);
189
+ break;
190
+ default:
191
+ const envKey = `${service.toUpperCase().replace(/-/g, "_")}_BASE_URL`;
192
+ process.env[envKey] = serviceUrl;
193
+ log.info(`Set ${envKey}=${serviceUrl}`);
194
+ }
195
+ }
196
+ }
197
+ /**
198
+ * Build status object for both the tool and slash command
199
+ */
200
+ function getStatus(services) {
201
+ const cliInstalled = isAquamanProxyInstalled();
202
+ return {
203
+ cliInstalled,
204
+ proxyRunning: proxyManager !== null,
205
+ socketPath: socketPath || getDefaultSocketPath(),
206
+ services,
207
+ httpInterceptorActive: httpInterceptor?.isActive() ?? false,
208
+ ...(cliInstalled ? {} : { fix: "Run: npm install -g aquaman-proxy && aquaman setup" }),
209
+ ...(!cliInstalled ? {} : proxyManager === null ? { fix: "Run: aquaman setup (or: openclaw aquaman setup)" } : {}),
210
+ environmentVariables: Object.fromEntries(services.map((s) => {
211
+ const key = s === "anthropic"
212
+ ? "ANTHROPIC_BASE_URL"
213
+ : s === "openai"
214
+ ? "OPENAI_BASE_URL"
215
+ : `${s.toUpperCase()}_BASE_URL`;
216
+ return [key, process.env[key] ?? null];
217
+ })),
218
+ };
219
+ }
220
+ /**
221
+ * Register the aquaman_status tool — always registered (works in degraded mode)
222
+ */
223
+ function registerStatusTool(api, services) {
224
+ api.registerTool(() => {
225
+ return {
226
+ name: "aquaman_status",
227
+ label: "Aquaman Status",
228
+ description: "Check aquaman credential proxy status and configured services",
229
+ parameters: {
230
+ type: "object",
231
+ properties: {},
232
+ required: [],
233
+ },
234
+ async execute(_toolCallId, _params) {
235
+ const status = getStatus(services);
236
+ return {
237
+ content: [{ type: "text", text: JSON.stringify(status, null, 2) }],
238
+ details: status,
239
+ };
240
+ },
241
+ };
242
+ }, { names: ["aquaman_status"] });
243
+ }
244
+ /**
245
+ * Auto-generate auth-profiles.json with placeholder keys for proxied services.
246
+ * OpenClaw checks its auth store before making API calls — without a placeholder
247
+ * key, requests are rejected before they ever reach the proxy.
248
+ */
249
+ function ensureAuthProfiles(log, services) {
250
+ const stateDir = process.env.OPENCLAW_STATE_DIR ||
251
+ path.join(os.homedir(), ".openclaw");
252
+ const profilesPath = path.join(stateDir, "agents", "main", "agent", "auth-profiles.json");
253
+ if (fs.existsSync(profilesPath))
254
+ return;
255
+ const profiles = {};
256
+ const order = {};
257
+ for (const service of services) {
258
+ if (service === "anthropic" || service === "openai") {
259
+ profiles[`${service}:default`] = {
260
+ type: "api_key",
261
+ provider: service,
262
+ key: "aquaman-proxy-managed",
263
+ };
264
+ order[service] = [`${service}:default`];
265
+ }
266
+ }
267
+ const dir = path.dirname(profilesPath);
268
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
269
+ fs.writeFileSync(profilesPath, JSON.stringify({ version: 1, profiles, order }, null, 2), { mode: 0o600 });
270
+ log.info(`Generated auth-profiles.json with placeholder keys at ${profilesPath}`);
271
+ }
272
+ /**
273
+ * Aquaman OpenClaw Plugin Definition
274
+ */
275
+ const plugin = {
276
+ id: 'aquaman-plugin',
277
+ name: 'Aquaman — API Key Protection',
278
+ version: PLUGIN_VERSION,
279
+ description: 'API key protection for OpenClaw — credentials stay in your vault, never in the agent\'s memory',
280
+ register(api) {
281
+ api.logger.info("Aquaman plugin loaded");
282
+ // Read services from plugin config
283
+ const pluginCfg = api.pluginConfig;
284
+ configuredServices = pluginCfg?.services ?? ["anthropic", "openai"];
285
+ // Auto-generate auth-profiles.json if missing. Opt-out via plugin config
286
+ // `autoGenerateAuthProfiles: false` for operators managing their own auth
287
+ // profiles. (Closes ClawScan ASI03.)
288
+ const autoGenerateAuthProfiles = pluginCfg?.autoGenerateAuthProfiles ?? true;
289
+ if (autoGenerateAuthProfiles) {
290
+ ensureAuthProfiles(api.logger, configuredServices);
291
+ }
292
+ else {
293
+ api.logger.info("auto-generation of auth-profiles.json disabled by plugin config");
294
+ }
295
+ // Check if aquaman proxy binary is available
296
+ const proxyAvailable = isAquamanProxyInstalled();
297
+ if (!proxyAvailable) {
298
+ api.logger.warn("aquaman proxy not found. Install with: npm install -g aquaman-proxy");
299
+ api.logger.warn("Then run: aquaman setup");
300
+ // DO NOT call configureEnvironment() — sentinel URLs without a proxy
301
+ // would break all API calls (connection refused to non-existent socket)
302
+ }
303
+ else {
304
+ api.logger.info("aquaman proxy found, will start proxy on gateway start");
305
+ // Configure environment variables immediately (sentinel hostname)
306
+ configureEnvironment(api.logger, configuredServices);
307
+ // Register service for proxy lifecycle management
308
+ api.registerService({
309
+ id: 'aquaman-proxy',
310
+ async start(ctx) {
311
+ ctx.logger.info("Starting aquaman proxy...");
312
+ const started = await startProxy(ctx.logger);
313
+ if (started && socketPath) {
314
+ ctx.logger.info("Aquaman proxy started successfully");
315
+ // Check for version mismatch between plugin and proxy
316
+ const proxyVersion = await getProxyVersion(socketPath);
317
+ if (proxyVersion && proxyVersion !== PLUGIN_VERSION) {
318
+ ctx.logger.warn(`Warning: plugin version ${PLUGIN_VERSION} \u2260 proxy version ${proxyVersion}. ` +
319
+ `Update both: npm install -g aquaman-proxy && openclaw plugins install aquaman-plugin`);
320
+ }
321
+ // Activate HTTP interceptor to redirect channel traffic through proxy
322
+ activateHttpInterceptor(ctx.logger);
323
+ }
324
+ else {
325
+ ctx.logger.error("Failed to start aquaman proxy");
326
+ // Check if another instance is already running
327
+ const defaultSock = getDefaultSocketPath();
328
+ const alreadyRunning = await isProxyRunning(defaultSock);
329
+ if (alreadyRunning) {
330
+ socketPath = defaultSock;
331
+ ctx.logger.info("Another aquaman instance is already running — using it");
332
+ // Load host map from existing proxy
333
+ const map = await loadHostMap(defaultSock);
334
+ dynamicHostMap = map.size > 0 ? map : FALLBACK_HOST_MAP;
335
+ activateHttpInterceptor(ctx.logger);
336
+ }
337
+ else {
338
+ ctx.logger.error("No running proxy found. Check: openclaw aquaman doctor");
339
+ }
340
+ }
341
+ },
342
+ async stop(ctx) {
343
+ ctx.logger.info("Stopping aquaman proxy...");
344
+ stopProxy();
345
+ }
346
+ });
347
+ }
348
+ // --- Commands, tools, and CLI are ALWAYS registered (even without proxy) ---
349
+ // This ensures ClawHub users who installed the plugin but haven't run setup
350
+ // still get actionable commands and status information.
351
+ // Register /aquaman-status slash command for humans
352
+ api.registerCommand({
353
+ name: 'aquaman-status',
354
+ description: 'Show aquaman credential proxy status and configured services',
355
+ acceptsArgs: false,
356
+ requireAuth: true,
357
+ async handler() {
358
+ const status = getStatus(configuredServices);
359
+ return { text: JSON.stringify(status, null, 2) };
360
+ }
361
+ });
362
+ // Register CLI commands if available
363
+ if (api.registerCli) {
364
+ api.registerCli(({ program }) => {
365
+ const aquamanCmd = program
366
+ .command("aquaman")
367
+ .description("Aquaman — API key protection");
368
+ aquamanCmd
369
+ .command("status")
370
+ .description("Show aquaman proxy status")
371
+ .action(() => {
372
+ const status = getStatus(configuredServices);
373
+ console.log("\nAquaman Status:");
374
+ console.log(` Proxy binary: ${status.cliInstalled ? "found" : "NOT FOUND"}`);
375
+ console.log(` Proxy running: ${status.proxyRunning}`);
376
+ console.log(` Socket path: ${status.socketPath}`);
377
+ console.log(` Services: ${configuredServices.join(", ")}`);
378
+ if (status.fix) {
379
+ console.log(`\n Action needed: ${status.fix}`);
380
+ }
381
+ if (status.proxyRunning) {
382
+ console.log("\nEnvironment Variables:");
383
+ for (const service of configuredServices) {
384
+ const envKey = service === "anthropic"
385
+ ? "ANTHROPIC_BASE_URL"
386
+ : service === "openai"
387
+ ? "OPENAI_BASE_URL"
388
+ : `${service.toUpperCase()}_BASE_URL`;
389
+ console.log(` ${envKey}=${process.env[envKey] ?? "(not set)"}`);
390
+ }
391
+ }
392
+ });
393
+ aquamanCmd
394
+ .command("setup")
395
+ .description("Run the setup wizard (stores keys, configures backend)")
396
+ .action(async () => {
397
+ try {
398
+ const exitCode = await execAquamanProxyInteractive(['setup']);
399
+ if (exitCode !== 0)
400
+ process.exitCode = exitCode;
401
+ }
402
+ catch {
403
+ console.log("\n Run in your terminal:\n aquaman setup\n");
404
+ }
405
+ });
406
+ aquamanCmd
407
+ .command("doctor")
408
+ .description("Diagnose issues with actionable fixes")
409
+ .action(async () => {
410
+ try {
411
+ const result = await execAquamanProxyCli(['doctor']);
412
+ process.stdout.write(result.stdout);
413
+ if (result.stderr)
414
+ process.stderr.write(result.stderr);
415
+ if (result.exitCode !== 0)
416
+ process.exitCode = result.exitCode;
417
+ }
418
+ catch (err) {
419
+ console.error(`Failed to run aquaman doctor: ${err.message}`);
420
+ process.exitCode = 1;
421
+ }
422
+ });
423
+ const credsCmd = aquamanCmd
424
+ .command("credentials")
425
+ .description("Credential management");
426
+ credsCmd
427
+ .command("list")
428
+ .description("List stored credentials")
429
+ .action(async () => {
430
+ try {
431
+ const result = await execAquamanProxyCli(['credentials', 'list']);
432
+ process.stdout.write(result.stdout);
433
+ if (result.stderr)
434
+ process.stderr.write(result.stderr);
435
+ }
436
+ catch (err) {
437
+ console.error(`Failed: ${err.message}`);
438
+ }
439
+ });
440
+ credsCmd
441
+ .command("add <service> [key]")
442
+ .description("Add a credential (secure prompt)")
443
+ .action(async (service, key = "api_key") => {
444
+ try {
445
+ const exitCode = await execAquamanProxyInteractive(['credentials', 'add', service, key]);
446
+ if (exitCode !== 0)
447
+ process.exitCode = exitCode;
448
+ }
449
+ catch {
450
+ console.log(`\n Run in your terminal:\n aquaman credentials add ${service} ${key}\n`);
451
+ }
452
+ });
453
+ aquamanCmd
454
+ .command("policy-list")
455
+ .description("List configured request policy rules")
456
+ .action(async () => {
457
+ try {
458
+ const result = await execAquamanProxyCli(['policy', 'list']);
459
+ process.stdout.write(result.stdout);
460
+ if (result.stderr)
461
+ process.stderr.write(result.stderr);
462
+ }
463
+ catch (err) {
464
+ console.error(`Failed: ${err.message}`);
465
+ }
466
+ });
467
+ aquamanCmd
468
+ .command("audit-tail")
469
+ .description("Show recent audit log entries")
470
+ .action(async () => {
471
+ try {
472
+ const result = await execAquamanProxyCli(['audit', 'tail']);
473
+ process.stdout.write(result.stdout);
474
+ if (result.stderr)
475
+ process.stderr.write(result.stderr);
476
+ }
477
+ catch (err) {
478
+ console.error(`Failed: ${err.message}`);
479
+ }
480
+ });
481
+ aquamanCmd
482
+ .command("services-list")
483
+ .description("List all configured services")
484
+ .action(async () => {
485
+ try {
486
+ const result = await execAquamanProxyCli(['services', 'list']);
487
+ process.stdout.write(result.stdout);
488
+ if (result.stderr)
489
+ process.stderr.write(result.stderr);
490
+ }
491
+ catch (err) {
492
+ console.error(`Failed: ${err.message}`);
493
+ }
494
+ });
495
+ }, { commands: ["aquaman"] });
496
+ }
497
+ registerStatusTool(api, configuredServices);
498
+ api.logger.info("Aquaman plugin registered successfully");
499
+ }
500
+ };
501
+ export default plugin;
502
+ //# sourceMappingURL=index.js.map