aquaman-plugin 0.6.0 → 0.7.1
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 -12
- package/index.ts +44 -84
- package/openclaw.plugin.json +0 -11
- package/package.json +6 -3
- package/src/commands.ts +26 -198
- package/src/config-schema.ts +0 -31
- package/src/http-interceptor.ts +30 -59
- package/src/index.ts +1 -13
- package/src/plugin.ts +16 -143
- package/src/proxy-health.ts +43 -38
- package/src/proxy-manager.ts +9 -32
- 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,7 +29,7 @@ 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
|
|
|
@@ -54,16 +55,19 @@ Troubleshooting: `aquaman doctor`
|
|
|
54
55
|
|
|
55
56
|
| Key | Type | Default | Description |
|
|
56
57
|
|-----|------|---------|-------------|
|
|
57
|
-
| `mode` | `"embedded"` \| `"proxy"` | `"embedded"` | Isolation mode |
|
|
58
58
|
| `backend` | `"keychain"` \| `"1password"` \| `"vault"` \| `"encrypted-file"` \| `"keepassxc"` | `"keychain"` | Credential store |
|
|
59
59
|
| `services` | `string[]` | `["anthropic", "openai"]` | Services to proxy |
|
|
60
|
-
| `proxyPort` | `number` | `8081` | Proxy listen port |
|
|
61
60
|
|
|
62
|
-
> Advanced settings (
|
|
61
|
+
> Advanced settings (audit, vault) go in `~/.aquaman/config.yaml`.
|
|
63
62
|
|
|
64
63
|
## Security Audit Note
|
|
65
64
|
|
|
66
|
-
Running `openclaw security audit --deep` will show
|
|
65
|
+
Running `openclaw security audit --deep` will show two expected findings:
|
|
66
|
+
|
|
67
|
+
- **`dangerous-exec`** on `proxy-manager.ts` — the plugin spawns the aquaman proxy as a separate process, which is the whole point of credential isolation.
|
|
68
|
+
- **`tools_reachable_permissive_policy`** — advisory that plugin tools are reachable under the default tool policy. This is about your OpenClaw tool profile setting, not about aquaman. Set `"tools": { "profile": "coding" }` in `openclaw.json` if your agents handle untrusted input.
|
|
69
|
+
|
|
70
|
+
`aquaman setup` adds the plugin to your `plugins.allow` trust list automatically.
|
|
67
71
|
|
|
68
72
|
## Documentation
|
|
69
73
|
|
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
|
*/
|
|
@@ -50,12 +50,17 @@ try { PLUGIN_VERSION = JSON.parse(fs.readFileSync(pluginPkgPath, 'utf-8')).versi
|
|
|
50
50
|
|
|
51
51
|
let proxyManager: ProxyManager | null = null;
|
|
52
52
|
let httpInterceptor: HttpInterceptor | null = null;
|
|
53
|
-
let
|
|
53
|
+
let socketPath: string | null = null;
|
|
54
54
|
let dynamicHostMap: Map<string, string> | null = null;
|
|
55
|
-
const proxyPort = 8081;
|
|
56
55
|
const services = ["anthropic", "openai"];
|
|
57
56
|
|
|
58
|
-
/**
|
|
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 */
|
|
59
64
|
const FALLBACK_HOST_MAP = new Map<string, string>([
|
|
60
65
|
['api.anthropic.com', 'anthropic'],
|
|
61
66
|
['api.openai.com', 'openai'],
|
|
@@ -84,21 +89,6 @@ const FALLBACK_HOST_MAP = new Map<string, string>([
|
|
|
84
89
|
['chat.googleapis.com', 'google-chat'],
|
|
85
90
|
]);
|
|
86
91
|
|
|
87
|
-
/**
|
|
88
|
-
* Get external proxy URL from environment (for Docker two-container mode).
|
|
89
|
-
* When set, the plugin skips spawning a local proxy and routes traffic to the external URL.
|
|
90
|
-
*/
|
|
91
|
-
function getExternalProxyUrl(): string | null {
|
|
92
|
-
return process.env.AQUAMAN_PROXY_URL || null;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Get external client token from environment (for Docker two-container mode).
|
|
97
|
-
*/
|
|
98
|
-
function getExternalClientToken(): string | null {
|
|
99
|
-
return process.env.AQUAMAN_CLIENT_TOKEN || null;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
92
|
/**
|
|
103
93
|
* Check if aquaman CLI is installed (fs-based, no shell execution)
|
|
104
94
|
*/
|
|
@@ -109,12 +99,12 @@ function isAquamanInstalled(): boolean {
|
|
|
109
99
|
/**
|
|
110
100
|
* Start the aquaman proxy daemon using ProxyManager
|
|
111
101
|
*/
|
|
112
|
-
async function startProxy(
|
|
102
|
+
async function startProxy(log: OpenClawPluginApi["logger"]): Promise<boolean> {
|
|
113
103
|
try {
|
|
114
104
|
const mgr = createProxyManager({
|
|
115
|
-
config: {
|
|
105
|
+
config: {},
|
|
116
106
|
onReady: (info) => {
|
|
117
|
-
|
|
107
|
+
socketPath = info.socketPath;
|
|
118
108
|
if (info.hostMap && typeof info.hostMap === "object") {
|
|
119
109
|
dynamicHostMap = new Map(Object.entries(info.hostMap));
|
|
120
110
|
}
|
|
@@ -127,6 +117,7 @@ async function startProxy(port: number, log: OpenClawPluginApi["logger"]): Promi
|
|
|
127
117
|
});
|
|
128
118
|
await mgr.start();
|
|
129
119
|
proxyManager = mgr;
|
|
120
|
+
socketPath = mgr.getSocketPath();
|
|
130
121
|
return true;
|
|
131
122
|
} catch (err) {
|
|
132
123
|
log.error(`Failed to start proxy: ${err}`);
|
|
@@ -146,7 +137,7 @@ function stopProxy(): void {
|
|
|
146
137
|
proxyManager.stop();
|
|
147
138
|
proxyManager = null;
|
|
148
139
|
}
|
|
149
|
-
|
|
140
|
+
socketPath = null;
|
|
150
141
|
}
|
|
151
142
|
|
|
152
143
|
/**
|
|
@@ -154,16 +145,18 @@ function stopProxy(): void {
|
|
|
154
145
|
* This is what provides credential isolation for channels that don't support base URL overrides.
|
|
155
146
|
*/
|
|
156
147
|
function activateHttpInterceptor(log: OpenClawPluginApi["logger"]): void {
|
|
148
|
+
if (!socketPath) {
|
|
149
|
+
log.error("Cannot activate HTTP interceptor: no socket path");
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
157
153
|
// Use dynamic host map from proxy (includes custom services from services.yaml)
|
|
158
154
|
// Falls back to builtin map for backward compatibility
|
|
159
155
|
const hostMap = dynamicHostMap || FALLBACK_HOST_MAP;
|
|
160
156
|
|
|
161
|
-
const baseUrl = getExternalProxyUrl() || `http://127.0.0.1:${proxyPort}`;
|
|
162
|
-
|
|
163
157
|
httpInterceptor = createHttpInterceptor({
|
|
164
|
-
|
|
158
|
+
socketPath,
|
|
165
159
|
hostMap,
|
|
166
|
-
clientToken: clientToken || undefined,
|
|
167
160
|
log: (msg) => log.info(msg),
|
|
168
161
|
});
|
|
169
162
|
|
|
@@ -172,13 +165,11 @@ function activateHttpInterceptor(log: OpenClawPluginApi["logger"]): void {
|
|
|
172
165
|
}
|
|
173
166
|
|
|
174
167
|
/**
|
|
175
|
-
* Set environment variables for SDK clients
|
|
168
|
+
* Set environment variables for SDK clients using sentinel hostname
|
|
176
169
|
*/
|
|
177
170
|
function configureEnvironment(log: OpenClawPluginApi["logger"]): void {
|
|
178
|
-
const baseUrl = getExternalProxyUrl() || `http://127.0.0.1:${proxyPort}`;
|
|
179
|
-
|
|
180
171
|
for (const service of services) {
|
|
181
|
-
const serviceUrl =
|
|
172
|
+
const serviceUrl = `http://aquaman.local/${service}`;
|
|
182
173
|
|
|
183
174
|
switch (service) {
|
|
184
175
|
case "anthropic":
|
|
@@ -202,10 +193,9 @@ function configureEnvironment(log: OpenClawPluginApi["logger"]): void {
|
|
|
202
193
|
}
|
|
203
194
|
|
|
204
195
|
/**
|
|
205
|
-
* Register the aquaman_status tool
|
|
196
|
+
* Register the aquaman_status tool
|
|
206
197
|
*/
|
|
207
198
|
function registerStatusTool(api: OpenClawPluginApi): void {
|
|
208
|
-
const externalUrl = getExternalProxyUrl();
|
|
209
199
|
api.registerTool(
|
|
210
200
|
() => {
|
|
211
201
|
return {
|
|
@@ -219,10 +209,8 @@ function registerStatusTool(api: OpenClawPluginApi): void {
|
|
|
219
209
|
},
|
|
220
210
|
async execute() {
|
|
221
211
|
return {
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
proxyRunning: externalUrl !== null || proxyManager !== null,
|
|
225
|
-
proxyPort,
|
|
212
|
+
proxyRunning: proxyManager !== null,
|
|
213
|
+
socketPath: socketPath || getDefaultSocketPath(),
|
|
226
214
|
services,
|
|
227
215
|
httpInterceptorActive: httpInterceptor?.isActive() ?? false,
|
|
228
216
|
environmentVariables: Object.fromEntries(
|
|
@@ -278,10 +266,11 @@ function ensureAuthProfiles(log: OpenClawPluginApi["logger"]): void {
|
|
|
278
266
|
}
|
|
279
267
|
|
|
280
268
|
const dir = path.dirname(profilesPath);
|
|
281
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
269
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
282
270
|
fs.writeFileSync(
|
|
283
271
|
profilesPath,
|
|
284
|
-
JSON.stringify({ version: 1, profiles, order }, null, 2)
|
|
272
|
+
JSON.stringify({ version: 1, profiles, order }, null, 2),
|
|
273
|
+
{ mode: 0o600 }
|
|
285
274
|
);
|
|
286
275
|
log.info(
|
|
287
276
|
`Generated auth-profiles.json with placeholder keys at ${profilesPath}`
|
|
@@ -297,37 +286,6 @@ export default function register(api: OpenClawPluginApi): void {
|
|
|
297
286
|
// Auto-generate auth-profiles.json if missing
|
|
298
287
|
ensureAuthProfiles(api.logger);
|
|
299
288
|
|
|
300
|
-
const externalUrl = getExternalProxyUrl();
|
|
301
|
-
|
|
302
|
-
// External proxy mode (Docker two-container architecture)
|
|
303
|
-
if (externalUrl) {
|
|
304
|
-
api.logger.info(`External proxy mode: ${externalUrl}`);
|
|
305
|
-
clientToken = getExternalClientToken();
|
|
306
|
-
configureEnvironment(api.logger);
|
|
307
|
-
|
|
308
|
-
if (api.registerLifecycle) {
|
|
309
|
-
api.registerLifecycle({
|
|
310
|
-
async onGatewayStart() {
|
|
311
|
-
// Load dynamic host map from external proxy (includes custom services)
|
|
312
|
-
const map = await loadHostMap(externalUrl, clientToken);
|
|
313
|
-
dynamicHostMap = map.size > 0 ? map : FALLBACK_HOST_MAP;
|
|
314
|
-
activateHttpInterceptor(api.logger);
|
|
315
|
-
api.logger.info("HTTP interceptor active (external proxy mode)");
|
|
316
|
-
},
|
|
317
|
-
async onGatewayStop() {
|
|
318
|
-
if (httpInterceptor) {
|
|
319
|
-
httpInterceptor.deactivate();
|
|
320
|
-
httpInterceptor = null;
|
|
321
|
-
}
|
|
322
|
-
},
|
|
323
|
-
});
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
registerStatusTool(api);
|
|
327
|
-
api.logger.info("Aquaman plugin registered successfully");
|
|
328
|
-
return;
|
|
329
|
-
}
|
|
330
|
-
|
|
331
289
|
// Local proxy mode — requires aquaman CLI
|
|
332
290
|
if (!isAquamanInstalled()) {
|
|
333
291
|
api.logger.warn(
|
|
@@ -342,22 +300,21 @@ export default function register(api: OpenClawPluginApi): void {
|
|
|
342
300
|
|
|
343
301
|
api.logger.info("aquaman CLI found, will start proxy on gateway start");
|
|
344
302
|
|
|
345
|
-
// Configure environment variables immediately
|
|
303
|
+
// Configure environment variables immediately (sentinel hostname)
|
|
346
304
|
configureEnvironment(api.logger);
|
|
347
305
|
|
|
348
306
|
// Register lifecycle hooks if available
|
|
349
307
|
if (api.registerLifecycle) {
|
|
350
308
|
api.registerLifecycle({
|
|
351
309
|
async onGatewayStart() {
|
|
352
|
-
api.logger.info(
|
|
310
|
+
api.logger.info("Starting aquaman proxy...");
|
|
353
311
|
|
|
354
|
-
const started = await startProxy(
|
|
355
|
-
if (started) {
|
|
312
|
+
const started = await startProxy(api.logger);
|
|
313
|
+
if (started && socketPath) {
|
|
356
314
|
api.logger.info("Aquaman proxy started successfully");
|
|
357
315
|
|
|
358
316
|
// Check for version mismatch between plugin and proxy
|
|
359
|
-
const
|
|
360
|
-
const proxyVersion = await getProxyVersion(proxyBaseUrl);
|
|
317
|
+
const proxyVersion = await getProxyVersion(socketPath);
|
|
361
318
|
if (proxyVersion && proxyVersion !== PLUGIN_VERSION) {
|
|
362
319
|
api.logger.warn(
|
|
363
320
|
`Warning: plugin version ${PLUGIN_VERSION} \u2260 proxy version ${proxyVersion}. ` +
|
|
@@ -368,19 +325,22 @@ export default function register(api: OpenClawPluginApi): void {
|
|
|
368
325
|
// Activate HTTP interceptor to redirect channel traffic through proxy
|
|
369
326
|
activateHttpInterceptor(api.logger);
|
|
370
327
|
} else {
|
|
371
|
-
api.logger.error(
|
|
372
|
-
|
|
373
|
-
);
|
|
374
|
-
|
|
375
|
-
const alreadyRunning = await isProxyRunning(proxyPort);
|
|
328
|
+
api.logger.error("Failed to start aquaman proxy");
|
|
329
|
+
// Check if another instance is already running
|
|
330
|
+
const defaultSock = getDefaultSocketPath();
|
|
331
|
+
const alreadyRunning = await isProxyRunning(defaultSock);
|
|
376
332
|
if (alreadyRunning) {
|
|
333
|
+
socketPath = defaultSock;
|
|
377
334
|
api.logger.info(
|
|
378
|
-
|
|
335
|
+
"Another aquaman instance is already running — using it"
|
|
379
336
|
);
|
|
337
|
+
// Load host map from existing proxy
|
|
338
|
+
const map = await loadHostMap(defaultSock);
|
|
339
|
+
dynamicHostMap = map.size > 0 ? map : FALLBACK_HOST_MAP;
|
|
380
340
|
activateHttpInterceptor(api.logger);
|
|
381
341
|
} else {
|
|
382
342
|
api.logger.error(
|
|
383
|
-
|
|
343
|
+
"No running proxy found. Check: aquaman doctor"
|
|
384
344
|
);
|
|
385
345
|
}
|
|
386
346
|
}
|
|
@@ -407,7 +367,7 @@ export default function register(api: OpenClawPluginApi): void {
|
|
|
407
367
|
.action(() => {
|
|
408
368
|
console.log("\nAquaman Status:");
|
|
409
369
|
console.log(` Proxy running: ${proxyManager !== null}`);
|
|
410
|
-
console.log(`
|
|
370
|
+
console.log(` Socket path: ${socketPath || getDefaultSocketPath()}`);
|
|
411
371
|
console.log(` Services: ${services.join(", ")}`);
|
|
412
372
|
console.log("\nEnvironment Variables:");
|
|
413
373
|
for (const service of services) {
|
package/openclaw.plugin.json
CHANGED
|
@@ -12,12 +12,6 @@
|
|
|
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
17
|
"enum": ["keychain", "1password", "vault", "encrypted-file", "keepassxc"],
|
|
@@ -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.1",
|
|
4
4
|
"description": "Credential isolation plugin for OpenClaw",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
"keywords": [
|
|
14
14
|
"aquaman",
|
|
15
15
|
"openclaw",
|
|
16
|
+
"openclaw-plugin",
|
|
16
17
|
"plugin",
|
|
17
18
|
"security",
|
|
18
19
|
"credentials",
|
|
@@ -21,10 +22,12 @@
|
|
|
21
22
|
],
|
|
22
23
|
"author": "tech4242",
|
|
23
24
|
"license": "MIT",
|
|
24
|
-
"dependencies": {
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"undici": "^7.0.0"
|
|
27
|
+
},
|
|
25
28
|
"peerDependencies": {
|
|
26
29
|
"openclaw": ">=2026.1.0",
|
|
27
|
-
"aquaman-proxy": "0.
|
|
30
|
+
"aquaman-proxy": "0.7.1"
|
|
28
31
|
},
|
|
29
32
|
"peerDependenciesMeta": {
|
|
30
33
|
"aquaman-proxy": {
|
package/src/commands.ts
CHANGED
|
@@ -4,13 +4,11 @@
|
|
|
4
4
|
* Provides /aquaman commands for users to interact with the plugin.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import type { EmbeddedMode } from './embedded.js';
|
|
8
7
|
import type { ProxyManager } from './proxy-manager.js';
|
|
9
8
|
import type { PluginConfig } from './config-schema.js';
|
|
10
9
|
|
|
11
10
|
export interface CommandContext {
|
|
12
11
|
config: PluginConfig;
|
|
13
|
-
embeddedMode?: EmbeddedMode;
|
|
14
12
|
proxyManager?: ProxyManager;
|
|
15
13
|
}
|
|
16
14
|
|
|
@@ -37,43 +35,17 @@ export async function statusCommand(ctx: CommandContext): Promise<CommandResult>
|
|
|
37
35
|
|
|
38
36
|
lines.push('aquaman plugin status');
|
|
39
37
|
lines.push('');
|
|
40
|
-
lines.push(`Mode: ${ctx.config.mode || 'embedded'}`);
|
|
41
38
|
lines.push(`Backend: ${ctx.config.backend || 'keychain'}`);
|
|
42
39
|
lines.push(`Services: ${(ctx.config.services || []).join(', ')}`);
|
|
43
40
|
|
|
44
|
-
if (ctx.
|
|
45
|
-
|
|
46
|
-
const info = ctx.proxyManager.getConnectionInfo();
|
|
47
|
-
lines.push('');
|
|
48
|
-
lines.push('Proxy Status: Running');
|
|
49
|
-
lines.push(` URL: ${info?.baseUrl}`);
|
|
50
|
-
lines.push(` Port: ${info?.port}`);
|
|
51
|
-
lines.push(` Protocol: ${info?.protocol}`);
|
|
52
|
-
} else {
|
|
53
|
-
lines.push('');
|
|
54
|
-
lines.push('Proxy Status: Not running');
|
|
55
|
-
}
|
|
56
|
-
} else if (ctx.embeddedMode) {
|
|
57
|
-
const status = ctx.embeddedMode.getStatus();
|
|
41
|
+
if (ctx.proxyManager?.isRunning()) {
|
|
42
|
+
const info = ctx.proxyManager.getConnectionInfo();
|
|
58
43
|
lines.push('');
|
|
59
|
-
lines.push('
|
|
60
|
-
lines.push(`
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
// List credentials (without values)
|
|
65
|
-
if (ctx.embeddedMode) {
|
|
66
|
-
try {
|
|
67
|
-
const creds = await ctx.embeddedMode.listCredentials();
|
|
68
|
-
lines.push('');
|
|
69
|
-
lines.push(`Stored Credentials: ${creds.length}`);
|
|
70
|
-
for (const cred of creds) {
|
|
71
|
-
lines.push(` - ${cred.service}/${cred.key}`);
|
|
72
|
-
}
|
|
73
|
-
} catch (error) {
|
|
74
|
-
lines.push('');
|
|
75
|
-
lines.push('Credentials: (unavailable)');
|
|
76
|
-
}
|
|
44
|
+
lines.push('Proxy Status: Running');
|
|
45
|
+
lines.push(` Socket: ${info?.socketPath}`);
|
|
46
|
+
} else {
|
|
47
|
+
lines.push('');
|
|
48
|
+
lines.push('Proxy Status: Not running');
|
|
77
49
|
}
|
|
78
50
|
|
|
79
51
|
return {
|
|
@@ -86,19 +58,10 @@ export async function statusCommand(ctx: CommandContext): Promise<CommandResult>
|
|
|
86
58
|
* /aquaman add <service> - Add a credential (prompts for value)
|
|
87
59
|
*/
|
|
88
60
|
export async function addCommand(
|
|
89
|
-
|
|
61
|
+
_ctx: CommandContext,
|
|
90
62
|
service: string,
|
|
91
63
|
key: string = 'api_key'
|
|
92
64
|
): Promise<CommandResult> {
|
|
93
|
-
if (!ctx.embeddedMode) {
|
|
94
|
-
return {
|
|
95
|
-
success: false,
|
|
96
|
-
message: 'Embedded mode not available. Use proxy mode for credential management.'
|
|
97
|
-
};
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// Note: In a real OpenClaw plugin, this would trigger a secure input prompt
|
|
101
|
-
// For now, we return instructions
|
|
102
65
|
return {
|
|
103
66
|
success: true,
|
|
104
67
|
message: `To add a credential for ${service}/${key}:\n\n` +
|
|
@@ -111,143 +74,30 @@ export async function addCommand(
|
|
|
111
74
|
/**
|
|
112
75
|
* /aquaman list - List stored credentials
|
|
113
76
|
*/
|
|
114
|
-
export async function listCommand(
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
};
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
try {
|
|
123
|
-
const creds = await ctx.embeddedMode.listCredentials();
|
|
124
|
-
|
|
125
|
-
if (creds.length === 0) {
|
|
126
|
-
return {
|
|
127
|
-
success: true,
|
|
128
|
-
message: 'No credentials stored.\n\nUse `aquaman credentials add <service> <key>` to add one.'
|
|
129
|
-
};
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
const lines = ['Stored credentials:', ''];
|
|
133
|
-
for (const cred of creds) {
|
|
134
|
-
lines.push(` ${cred.service}/${cred.key}`);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
return {
|
|
138
|
-
success: true,
|
|
139
|
-
message: lines.join('\n'),
|
|
140
|
-
data: creds
|
|
141
|
-
};
|
|
142
|
-
} catch (error) {
|
|
143
|
-
return {
|
|
144
|
-
success: false,
|
|
145
|
-
message: `Failed to list credentials: ${error}`
|
|
146
|
-
};
|
|
147
|
-
}
|
|
77
|
+
export async function listCommand(_ctx: CommandContext): Promise<CommandResult> {
|
|
78
|
+
return {
|
|
79
|
+
success: true,
|
|
80
|
+
message: 'Run in your terminal:\n aquaman credentials list'
|
|
81
|
+
};
|
|
148
82
|
}
|
|
149
83
|
|
|
150
84
|
/**
|
|
151
85
|
* /aquaman logs - Show recent audit entries
|
|
152
86
|
*/
|
|
153
|
-
export async function logsCommand(
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
};
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
try {
|
|
162
|
-
const entries = await ctx.embeddedMode.getRecentAuditEntries(count);
|
|
163
|
-
|
|
164
|
-
if (entries.length === 0) {
|
|
165
|
-
return {
|
|
166
|
-
success: true,
|
|
167
|
-
message: 'No audit entries found.'
|
|
168
|
-
};
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
const lines = [`Last ${entries.length} audit entries:`, ''];
|
|
172
|
-
for (const entry of entries) {
|
|
173
|
-
const time = new Date(entry.timestamp).toISOString();
|
|
174
|
-
const type = entry.type.toUpperCase().padEnd(16);
|
|
175
|
-
let details = '';
|
|
176
|
-
|
|
177
|
-
if (entry.type === 'credential_access') {
|
|
178
|
-
details = `${entry.data.service} ${entry.data.operation} ${entry.data.success ? 'OK' : 'FAIL'}`;
|
|
179
|
-
} else {
|
|
180
|
-
details = JSON.stringify(entry.data).slice(0, 60);
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
lines.push(`${time} [${type}] ${details}`);
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
return {
|
|
187
|
-
success: true,
|
|
188
|
-
message: lines.join('\n'),
|
|
189
|
-
data: entries
|
|
190
|
-
};
|
|
191
|
-
} catch (error) {
|
|
192
|
-
return {
|
|
193
|
-
success: false,
|
|
194
|
-
message: `Failed to get audit logs: ${error}`
|
|
195
|
-
};
|
|
196
|
-
}
|
|
87
|
+
export async function logsCommand(_ctx: CommandContext, _count: number = 10): Promise<CommandResult> {
|
|
88
|
+
return {
|
|
89
|
+
success: true,
|
|
90
|
+
message: 'Run in your terminal:\n aquaman audit tail'
|
|
91
|
+
};
|
|
197
92
|
}
|
|
198
93
|
|
|
199
94
|
/**
|
|
200
95
|
* /aquaman verify - Verify audit log integrity
|
|
201
96
|
*/
|
|
202
|
-
export async function verifyCommand(
|
|
203
|
-
if (!ctx.embeddedMode) {
|
|
204
|
-
return {
|
|
205
|
-
success: false,
|
|
206
|
-
message: 'Embedded mode not available.'
|
|
207
|
-
};
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
try {
|
|
211
|
-
const result = await ctx.embeddedMode.verifyAuditIntegrity();
|
|
212
|
-
|
|
213
|
-
if (result.valid) {
|
|
214
|
-
return {
|
|
215
|
-
success: true,
|
|
216
|
-
message: 'Audit log integrity verified. No tampering detected.'
|
|
217
|
-
};
|
|
218
|
-
} else {
|
|
219
|
-
return {
|
|
220
|
-
success: false,
|
|
221
|
-
message: 'Audit log integrity FAILED!\n\n' +
|
|
222
|
-
'Errors:\n' +
|
|
223
|
-
result.errors.map(e => ` - ${e}`).join('\n')
|
|
224
|
-
};
|
|
225
|
-
}
|
|
226
|
-
} catch (error) {
|
|
227
|
-
return {
|
|
228
|
-
success: false,
|
|
229
|
-
message: `Failed to verify audit log: ${error}`
|
|
230
|
-
};
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
/**
|
|
235
|
-
* /aquaman mode <embedded|proxy> - Switch mode
|
|
236
|
-
*/
|
|
237
|
-
export async function modeCommand(ctx: CommandContext, mode: 'embedded' | 'proxy'): Promise<CommandResult> {
|
|
238
|
-
if (mode !== 'embedded' && mode !== 'proxy') {
|
|
239
|
-
return {
|
|
240
|
-
success: false,
|
|
241
|
-
message: 'Invalid mode. Use "embedded" or "proxy".'
|
|
242
|
-
};
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
// Mode switching requires configuration change
|
|
97
|
+
export async function verifyCommand(_ctx: CommandContext): Promise<CommandResult> {
|
|
246
98
|
return {
|
|
247
99
|
success: true,
|
|
248
|
-
message:
|
|
249
|
-
`{\n "plugins": {\n "aquaman": {\n "mode": "${mode}"\n }\n }\n}\n\n` +
|
|
250
|
-
`Then restart OpenClaw.`
|
|
100
|
+
message: 'Run in your terminal:\n aquaman audit verify'
|
|
251
101
|
};
|
|
252
102
|
}
|
|
253
103
|
|
|
@@ -272,36 +122,26 @@ export async function executeCommand(
|
|
|
272
122
|
case 'list':
|
|
273
123
|
return listCommand(ctx);
|
|
274
124
|
|
|
275
|
-
case 'logs':
|
|
125
|
+
case 'logs': {
|
|
276
126
|
const count = args[0] ? parseInt(args[0], 10) : 10;
|
|
277
127
|
return logsCommand(ctx, count);
|
|
128
|
+
}
|
|
278
129
|
|
|
279
130
|
case 'verify':
|
|
280
131
|
return verifyCommand(ctx);
|
|
281
132
|
|
|
282
|
-
case 'mode':
|
|
283
|
-
if (args.length < 1) {
|
|
284
|
-
return { success: false, message: 'Usage: /aquaman mode <embedded|proxy>' };
|
|
285
|
-
}
|
|
286
|
-
return modeCommand(ctx, args[0] as 'embedded' | 'proxy');
|
|
287
|
-
|
|
288
133
|
case 'help':
|
|
289
134
|
default:
|
|
290
135
|
return {
|
|
291
136
|
success: true,
|
|
292
137
|
message: `aquaman plugin commands:
|
|
293
138
|
|
|
294
|
-
/aquaman status - Show plugin status
|
|
139
|
+
/aquaman status - Show plugin status
|
|
295
140
|
/aquaman add - Add a credential (shows instructions)
|
|
296
141
|
/aquaman list - List stored credentials
|
|
297
|
-
/aquaman logs [n] - Show recent audit entries
|
|
142
|
+
/aquaman logs [n] - Show recent audit entries
|
|
298
143
|
/aquaman verify - Verify audit log integrity
|
|
299
|
-
/aquaman
|
|
300
|
-
/aquaman help - Show this help message
|
|
301
|
-
|
|
302
|
-
Mode comparison:
|
|
303
|
-
embedded: Simpler setup, credentials in Gateway memory
|
|
304
|
-
proxy: Stronger isolation, credentials in separate process`
|
|
144
|
+
/aquaman help - Show this help message`
|
|
305
145
|
};
|
|
306
146
|
}
|
|
307
147
|
}
|
|
@@ -313,7 +153,7 @@ export function getAvailableCommands(ctx: CommandContext): PluginCommand[] {
|
|
|
313
153
|
return [
|
|
314
154
|
{
|
|
315
155
|
name: 'status',
|
|
316
|
-
description: 'Show aquaman plugin status
|
|
156
|
+
description: 'Show aquaman plugin status',
|
|
317
157
|
execute: async () => {
|
|
318
158
|
const result = await statusCommand(ctx);
|
|
319
159
|
return result.message;
|
|
@@ -357,18 +197,6 @@ export function getAvailableCommands(ctx: CommandContext): PluginCommand[] {
|
|
|
357
197
|
return result.message;
|
|
358
198
|
}
|
|
359
199
|
},
|
|
360
|
-
{
|
|
361
|
-
name: 'mode',
|
|
362
|
-
description: 'Switch between embedded and proxy mode',
|
|
363
|
-
execute: async (args) => {
|
|
364
|
-
const mode = args.mode || args._?.[0];
|
|
365
|
-
if (!mode) {
|
|
366
|
-
return 'Usage: /aquaman mode <embedded|proxy>';
|
|
367
|
-
}
|
|
368
|
-
const result = await modeCommand(ctx, mode as 'embedded' | 'proxy');
|
|
369
|
-
return result.message;
|
|
370
|
-
}
|
|
371
|
-
},
|
|
372
200
|
{
|
|
373
201
|
name: 'help',
|
|
374
202
|
description: 'Show help for aquaman commands',
|