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/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.config.mode === 'proxy') {
45
- if (ctx.proxyManager?.isRunning()) {
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('Embedded Mode Status:');
60
- lines.push(` Initialized: ${status.initialized}`);
61
- lines.push(` Audit Enabled: ${status.auditEnabled}`);
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
- ctx: CommandContext,
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(ctx: CommandContext): Promise<CommandResult> {
115
- if (!ctx.embeddedMode) {
116
- return {
117
- success: false,
118
- message: 'Embedded mode not available.'
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(ctx: CommandContext, count: number = 10): Promise<CommandResult> {
154
- if (!ctx.embeddedMode) {
155
- return {
156
- success: false,
157
- message: 'Embedded mode not available.'
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(ctx: CommandContext): Promise<CommandResult> {
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: `To switch to ${mode} mode, update your openclaw.json:\n\n` +
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 and stored credentials
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 (default: 10)
142
+ /aquaman logs [n] - Show recent audit entries
298
143
  /aquaman verify - Verify audit log integrity
299
- /aquaman mode - Switch between embedded and proxy mode
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 and stored credentials',
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',
@@ -6,16 +6,6 @@
6
6
 
7
7
  import { Type, type Static } from '@sinclair/typebox';
8
8
 
9
- /**
10
- * Plugin operation mode
11
- * - embedded: Direct vault access within OpenClaw process (simpler, less isolation)
12
- * - proxy: Separate proxy process (stronger isolation, credentials never in Gateway)
13
- */
14
- export const PluginMode = Type.Union([
15
- Type.Literal('embedded'),
16
- Type.Literal('proxy')
17
- ], { default: 'embedded' });
18
-
19
9
  /**
20
10
  * Credential backend type
21
11
  */
@@ -23,7 +13,8 @@ export const CredentialBackend = Type.Union([
23
13
  Type.Literal('keychain'),
24
14
  Type.Literal('1password'),
25
15
  Type.Literal('vault'),
26
- Type.Literal('encrypted-file')
16
+ Type.Literal('encrypted-file'),
17
+ Type.Literal('keepassxc')
27
18
  ], { default: 'keychain' });
28
19
 
29
20
  /**
@@ -37,19 +28,12 @@ export const ProxiedServices = Type.Array(Type.String(), {
37
28
  * Complete plugin configuration schema
38
29
  */
39
30
  export const ConfigSchema = Type.Object({
40
- // Mode selection
41
- mode: Type.Optional(PluginMode),
42
-
43
31
  // Credential backend
44
32
  backend: Type.Optional(CredentialBackend),
45
33
 
46
34
  // Services to proxy
47
35
  services: Type.Optional(ProxiedServices),
48
36
 
49
- // Proxy mode options
50
- proxyPort: Type.Optional(Type.Number({ default: 8081, minimum: 1024, maximum: 65535 })),
51
- proxyAutoStart: Type.Optional(Type.Boolean({ default: true })),
52
-
53
37
  // 1Password options
54
38
  onePasswordVault: Type.Optional(Type.String()),
55
39
  onePasswordAccount: Type.Optional(Type.String()),
@@ -59,15 +43,6 @@ export const ConfigSchema = Type.Object({
59
43
  vaultToken: Type.Optional(Type.String()),
60
44
  vaultNamespace: Type.Optional(Type.String()),
61
45
  vaultMountPath: Type.Optional(Type.String({ default: 'secret' })),
62
-
63
- // TLS options
64
- tlsEnabled: Type.Optional(Type.Boolean({ default: true })),
65
- tlsCertPath: Type.Optional(Type.String()),
66
- tlsKeyPath: Type.Optional(Type.String()),
67
-
68
- // Audit options
69
- auditEnabled: Type.Optional(Type.Boolean({ default: true })),
70
- auditLogDir: Type.Optional(Type.String())
71
46
  });
72
47
 
73
48
  export type PluginConfig = Static<typeof ConfigSchema>;
@@ -76,14 +51,9 @@ export type PluginConfig = Static<typeof ConfigSchema>;
76
51
  * Default configuration values
77
52
  */
78
53
  export const defaultConfig: PluginConfig = {
79
- mode: 'embedded',
80
54
  backend: 'keychain',
81
55
  services: ['anthropic', 'openai'],
82
- proxyPort: 8081,
83
- proxyAutoStart: true,
84
56
  vaultMountPath: 'secret',
85
- tlsEnabled: true,
86
- auditEnabled: true
87
57
  };
88
58
 
89
59
  /**
@@ -1,47 +1,44 @@
1
1
  /**
2
- * HTTP fetch interceptor for channel credential isolation.
2
+ * HTTP interceptor for channel credential isolation.
3
3
  *
4
4
  * Overrides globalThis.fetch to redirect requests targeting known channel API
5
- * hosts through the aquaman credential proxy. The proxy injects the real
6
- * credentials, so the Gateway process never sees them.
5
+ * hosts through the aquaman credential proxy via Unix domain socket.
6
+ * The proxy injects the real credentials, so the Gateway process never sees them.
7
7
  *
8
8
  * OpenClaw channels use globalThis.fetch (backed by undici) for all HTTP calls.
9
9
  * Many channel monitor functions also accept a `proxyFetch` parameter that
10
10
  * falls back to globalThis.fetch, so overriding it covers both paths.
11
+ *
12
+ * Uses sentinel hostname `aquaman.local` — SDK base URLs are set to
13
+ * `http://aquaman.local/<service>` and the interceptor routes them through UDS.
11
14
  */
12
15
 
16
+ import { Agent } from 'undici';
17
+
18
+ /** Sentinel hostname used in SDK base URLs to route through UDS */
19
+ const PROXY_HOST = 'aquaman.local';
20
+
13
21
  export interface HttpInterceptorOptions {
14
- /** Base URL of the aquaman proxy, e.g. "http://127.0.0.1:8081" */
15
- proxyBaseUrl: string;
22
+ /** Unix domain socket path for the proxy */
23
+ socketPath: string;
16
24
  /** Map of hostname (or *.domain wildcard) → service name */
17
25
  hostMap: Map<string, string>;
18
- /** Client authentication token for the proxy */
19
- clientToken?: string;
20
26
  /** Optional logger */
21
27
  log?: (msg: string) => void;
22
28
  }
23
29
 
24
30
  export class HttpInterceptor {
25
- private proxyBaseUrl: string;
26
- private proxyHost: string;
31
+ private socketPath: string;
27
32
  private hostMap: Map<string, string>;
28
- private clientToken: string | null;
29
33
  private originalFetch: typeof globalThis.fetch | null = null;
30
34
  private active = false;
31
35
  private log: (msg: string) => void;
36
+ private socketAgent: Agent | null = null;
32
37
 
33
38
  constructor(options: HttpInterceptorOptions) {
34
- this.proxyBaseUrl = options.proxyBaseUrl.replace(/\/$/, '');
39
+ this.socketPath = options.socketPath;
35
40
  this.hostMap = options.hostMap;
36
- this.clientToken = options.clientToken || null;
37
41
  this.log = options.log || (() => {});
38
-
39
- // Extract proxy hostname to avoid intercepting requests to the proxy itself
40
- try {
41
- this.proxyHost = new URL(this.proxyBaseUrl).hostname;
42
- } catch {
43
- this.proxyHost = '127.0.0.1';
44
- }
45
42
  }
46
43
 
47
44
  /**
@@ -51,15 +48,13 @@ export class HttpInterceptor {
51
48
  if (this.active) return;
52
49
 
53
50
  this.originalFetch = globalThis.fetch;
51
+ this.socketAgent = new Agent({ connect: { socketPath: this.socketPath } });
54
52
 
55
53
  const origFetch = this.originalFetch;
56
- const proxyBase = this.proxyBaseUrl;
57
- const proxyHostname = this.proxyHost;
58
- const token = this.clientToken;
54
+ const socketAgent = this.socketAgent;
59
55
  const matchHost = this.matchHost.bind(this);
60
56
  const extractUrl = this.extractUrl.bind(this);
61
57
  const stripAuthHeaders = this.stripAuthHeaders.bind(this);
62
- const injectToken = this.injectTokenHeader.bind(this);
63
58
  const logFn = this.log;
64
59
 
65
60
  (globalThis as any).fetch = (
@@ -71,22 +66,19 @@ export class HttpInterceptor {
71
66
  return origFetch.call(globalThis, input, init);
72
67
  }
73
68
 
74
- // Requests to the proxy itself (SDK traffic via env vars) — inject token, pass through
75
- if (url.hostname === proxyHostname || url.hostname === 'localhost') {
76
- if (token) {
77
- const tokenInit = injectToken(init, token);
78
- return origFetch.call(globalThis, input, tokenInit);
79
- }
80
- return origFetch.call(globalThis, input, init);
69
+ // SDK traffic via env vars (hostname = aquaman.local) — route through UDS
70
+ if (url.hostname === PROXY_HOST) {
71
+ return origFetch.call(globalThis, input, { ...init, dispatcher: socketAgent } as any);
81
72
  }
82
73
 
74
+ // Channel traffic — match against host map
83
75
  const service = matchHost(url.hostname);
84
76
  if (!service) {
85
77
  return origFetch.call(globalThis, input, init);
86
78
  }
87
79
 
88
80
  // Rewrite the URL to go through the proxy
89
- const proxyUrl = `${proxyBase}/${service}${url.pathname}${url.search}`;
81
+ const proxyUrl = `http://${PROXY_HOST}/${service}${url.pathname}${url.search}`;
90
82
  logFn(`[aquaman] Intercepted ${url.hostname}${url.pathname} → ${service}`);
91
83
 
92
84
  // Strip any existing authorization headers — the proxy will inject the real ones
@@ -96,12 +88,7 @@ export class HttpInterceptor {
96
88
  newInit = { ...init, headers: stripped };
97
89
  }
98
90
 
99
- // Inject client token for proxy authentication
100
- if (token) {
101
- newInit = injectToken(newInit, token);
102
- }
103
-
104
- return origFetch.call(globalThis, proxyUrl, { ...newInit, redirect: 'manual' });
91
+ return origFetch.call(globalThis, proxyUrl, { ...newInit, redirect: 'manual', dispatcher: socketAgent } as any);
105
92
  };
106
93
 
107
94
  this.active = true;
@@ -117,6 +104,12 @@ export class HttpInterceptor {
117
104
  globalThis.fetch = this.originalFetch;
118
105
  this.originalFetch = null;
119
106
  this.active = false;
107
+
108
+ if (this.socketAgent) {
109
+ this.socketAgent.close();
110
+ this.socketAgent = null;
111
+ }
112
+
120
113
  this.log('[aquaman] HTTP interceptor deactivated');
121
114
  }
122
115
 
@@ -153,28 +146,6 @@ export class HttpInterceptor {
153
146
  return null;
154
147
  }
155
148
 
156
- private injectTokenHeader(init: RequestInit | undefined, token: string): RequestInit {
157
- const base = init || {};
158
- const headers = base.headers;
159
-
160
- if (!headers) {
161
- return { ...base, headers: { 'x-aquaman-token': token } };
162
- }
163
-
164
- if (headers instanceof Headers) {
165
- const h = new Headers(headers);
166
- h.set('x-aquaman-token', token);
167
- return { ...base, headers: h };
168
- }
169
-
170
- if (Array.isArray(headers)) {
171
- return { ...base, headers: [...headers, ['x-aquaman-token', token]] };
172
- }
173
-
174
- // Plain object
175
- return { ...base, headers: { ...headers, 'x-aquaman-token': token } };
176
- }
177
-
178
149
  private stripAuthHeaders(headers: HeadersInit): HeadersInit {
179
150
  if (headers instanceof Headers) {
180
151
  const h = new Headers(headers);
package/src/index.ts CHANGED
@@ -3,9 +3,7 @@
3
3
  *
4
4
  * This package provides an OpenClaw plugin that enables:
5
5
  * - Secure credential storage using enterprise backends (Keychain, 1Password, Vault)
6
- * - Two operation modes:
7
- * - Embedded: Direct vault access (simpler, credentials in Gateway memory)
8
- * - Proxy: Separate process (stronger isolation, credentials never in Gateway)
6
+ * - Proxy mode: Separate process, credentials never in Gateway memory
9
7
  * - Hash-chained tamper-evident audit logs
10
8
  * - Slash commands for credential management
11
9
  *
@@ -16,7 +14,6 @@
16
14
  * {
17
15
  * "plugins": {
18
16
  * "aquaman-plugin": {
19
- * "mode": "embedded",
20
17
  * "backend": "keychain",
21
18
  * "services": ["anthropic", "openai"]
22
19
  * }
@@ -34,7 +31,6 @@ export {
34
31
  // Config Schema
35
32
  export {
36
33
  ConfigSchema,
37
- PluginMode,
38
34
  CredentialBackend,
39
35
  ProxiedServices,
40
36
  type PluginConfig,
@@ -42,13 +38,6 @@ export {
42
38
  mergeConfig
43
39
  } from './config-schema.js';
44
40
 
45
- // Embedded Mode
46
- export {
47
- type EmbeddedModeOptions,
48
- EmbeddedMode,
49
- createEmbeddedMode
50
- } from './embedded.js';
51
-
52
41
  // Proxy Manager
53
42
  export {
54
43
  type ProxyConnectionInfo,
@@ -66,7 +55,6 @@ export {
66
55
  listCommand,
67
56
  logsCommand,
68
57
  verifyCommand,
69
- modeCommand,
70
58
  executeCommand
71
59
  } from './commands.js';
72
60