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/plugin.ts CHANGED
@@ -2,14 +2,11 @@
2
2
  * OpenClaw Plugin Entry Point
3
3
  *
4
4
  * This is the main plugin that implements the OpenClaw plugin interface.
5
- * It provides credential isolation through two modes:
6
- *
7
- * 1. Embedded Mode (default): Direct vault access, simpler setup
8
- * 2. Proxy Mode: Separate process, stronger credential isolation
5
+ * It provides credential isolation through proxy mode:
6
+ * credentials are held in a separate process and never enter the Gateway.
9
7
  */
10
8
 
11
9
  import { type PluginConfig, mergeConfig, defaultConfig } from './config-schema.js';
12
- import { createEmbeddedMode, type EmbeddedMode } from './embedded.js';
13
10
  import { createProxyManager, type ProxyManager, type ProxyConnectionInfo } from './proxy-manager.js';
14
11
  import { executeCommand, type CommandContext, type CommandResult, getAvailableCommands, type PluginCommand } from './commands.js';
15
12
  import { HttpInterceptor, createHttpInterceptor } from './http-interceptor.js';
@@ -30,7 +27,6 @@ export class AquamanPlugin {
30
27
  readonly name = 'aquaman-plugin';
31
28
 
32
29
  private config: PluginConfig;
33
- private embeddedMode: EmbeddedMode | null = null;
34
30
  private proxyManager: ProxyManager | null = null;
35
31
  private httpInterceptor: HttpInterceptor | null = null;
36
32
  private initialized = false;
@@ -61,16 +57,10 @@ export class AquamanPlugin {
61
57
 
62
58
  console.log('[aquaman] Initializing plugin...');
63
59
 
64
- if (this.config.mode === 'proxy') {
65
- // Proxy mode: Start separate process
66
- await this.initProxyMode();
67
- } else {
68
- // Embedded mode: Direct vault access
69
- await this.initEmbeddedMode();
70
- }
60
+ await this.initProxyMode();
71
61
 
72
62
  this.initialized = true;
73
- console.log(`[aquaman] Plugin initialized in ${this.config.mode || 'embedded'} mode`);
63
+ console.log('[aquaman] Plugin initialized in proxy mode');
74
64
  }
75
65
 
76
66
  /**
@@ -100,40 +90,20 @@ export class AquamanPlugin {
100
90
  this.proxyManager = null;
101
91
  }
102
92
 
103
- this.embeddedMode = null;
104
93
  this.initialized = false;
105
94
 
106
95
  console.log('[aquaman] Plugin unloaded');
107
96
  }
108
97
 
109
- /**
110
- * Initialize embedded mode
111
- */
112
- private async initEmbeddedMode(): Promise<void> {
113
- this.embeddedMode = createEmbeddedMode({
114
- config: this.config
115
- });
116
-
117
- await this.embeddedMode.initialize();
118
-
119
- // Set environment variables for OpenClaw
120
- this.configureEnvironment();
121
- }
122
-
123
98
  /**
124
99
  * Initialize proxy mode
125
100
  */
126
101
  private async initProxyMode(): Promise<void> {
127
- if (!this.config.proxyAutoStart) {
128
- console.log('[aquaman] Proxy auto-start disabled');
129
- return;
130
- }
131
-
132
102
  this.proxyManager = createProxyManager({
133
103
  config: this.config,
134
104
  onReady: (info) => {
135
- console.log(`[aquaman] Proxy ready at ${info.baseUrl}`);
136
- this.configureEnvironmentForProxy(info);
105
+ console.log(`[aquaman] Proxy ready on ${info.socketPath}`);
106
+ this.configureEnvironmentForProxy();
137
107
  },
138
108
  onError: (error) => {
139
109
  console.error('[aquaman] Proxy error:', error);
@@ -143,28 +113,20 @@ export class AquamanPlugin {
143
113
  }
144
114
  });
145
115
 
146
- // Also initialize embedded mode for credential management
147
- this.embeddedMode = createEmbeddedMode({
148
- config: this.config
149
- });
150
- await this.embeddedMode.initialize();
151
-
152
116
  // Start proxy
153
117
  try {
154
118
  const info = await this.proxyManager.start();
155
- this.configureEnvironmentForProxy(info);
156
- this.activateHttpInterceptor(info.baseUrl);
119
+ this.configureEnvironmentForProxy();
120
+ this.activateHttpInterceptor(info.socketPath);
157
121
  } catch (error) {
158
122
  console.error('[aquaman] Failed to start proxy:', error);
159
- console.log('[aquaman] Falling back to embedded mode');
160
- this.configureEnvironment();
161
123
  }
162
124
  }
163
125
 
164
126
  /**
165
127
  * Activate HTTP interceptor for channel credential isolation.
166
128
  */
167
- private activateHttpInterceptor(proxyBaseUrl: string): void {
129
+ private activateHttpInterceptor(proxySocketPath: string): void {
168
130
  // Build host map from the service registry's host patterns
169
131
  const hostMap = new Map<string, string>([
170
132
  ['api.anthropic.com', 'anthropic'],
@@ -193,7 +155,7 @@ export class AquamanPlugin {
193
155
  ]);
194
156
 
195
157
  this.httpInterceptor = createHttpInterceptor({
196
- proxyBaseUrl,
158
+ socketPath: proxySocketPath,
197
159
  hostMap,
198
160
  log: (msg) => console.log(msg),
199
161
  });
@@ -202,68 +164,27 @@ export class AquamanPlugin {
202
164
  }
203
165
 
204
166
  /**
205
- * Configure environment variables for embedded mode
206
- * In embedded mode, we still set base URLs pointing to a local proxy
207
- * so credential injection works consistently.
208
- */
209
- private configureEnvironment(): void {
210
- const services = this.config.services || defaultConfig.services;
211
- const port = this.config.proxyPort || 8081;
212
- const baseUrl = `http://127.0.0.1:${port}`;
213
-
214
- this.setServiceEnvironmentVariables(services!, baseUrl);
215
- console.log('[aquaman] Embedded mode active - credentials available via plugin');
216
- }
217
-
218
- /**
219
- * Configure environment variables to route through proxy
167
+ * Configure environment variables using sentinel hostname
220
168
  */
221
- private configureEnvironmentForProxy(info: ProxyConnectionInfo): void {
169
+ private configureEnvironmentForProxy(): void {
222
170
  const services = this.config.services || defaultConfig.services;
223
- this.setServiceEnvironmentVariables(services!, info.baseUrl);
224
-
225
- // Handle TLS
226
- if (info.protocol === 'https') {
227
- if (this.config.tlsCertPath) {
228
- this.setEnvVar('NODE_EXTRA_CA_CERTS', this.config.tlsCertPath);
229
- } else {
230
- // Development: disable TLS verification for self-signed certs
231
- this.setEnvVar('NODE_TLS_REJECT_UNAUTHORIZED', '0');
232
- }
233
- }
234
- }
235
-
236
- /**
237
- * Set environment variables for configured services
238
- */
239
- private setServiceEnvironmentVariables(services: string[], baseUrl: string): void {
240
- for (const service of services) {
241
- const serviceUrl = `${baseUrl}/${service}`;
171
+ for (const service of services!) {
172
+ const serviceUrl = `http://aquaman.local/${service}`;
242
173
 
243
174
  switch (service) {
244
175
  case 'anthropic':
245
176
  this.setEnvVar('ANTHROPIC_BASE_URL', serviceUrl);
246
- this.setEnvVar('ANTHROPIC_API_KEY', 'aquaman-proxy-managed');
247
177
  break;
248
178
  case 'openai':
249
179
  this.setEnvVar('OPENAI_BASE_URL', serviceUrl);
250
- this.setEnvVar('OPENAI_API_KEY', 'aquaman-proxy-managed');
251
180
  break;
252
181
  case 'github':
253
182
  this.setEnvVar('GITHUB_API_URL', serviceUrl);
254
- this.setEnvVar('GITHUB_TOKEN', 'aquaman-proxy-managed');
255
183
  break;
256
- case 'slack':
257
- this.setEnvVar('SLACK_API_URL', serviceUrl);
258
- this.setEnvVar('SLACK_BOT_TOKEN', 'aquaman-proxy-managed');
259
- break;
260
- case 'discord':
261
- this.setEnvVar('DISCORD_API_URL', serviceUrl);
262
- this.setEnvVar('DISCORD_BOT_TOKEN', 'aquaman-proxy-managed');
263
- break;
264
- default:
184
+ default: {
265
185
  const envKey = `${service.toUpperCase().replace(/-/g, '_')}_BASE_URL`;
266
186
  this.setEnvVar(envKey, serviceUrl);
187
+ }
267
188
  }
268
189
  }
269
190
  }
@@ -283,69 +204,29 @@ export class AquamanPlugin {
283
204
  async executeCommand(command: string, args: string[] = []): Promise<CommandResult> {
284
205
  const ctx: CommandContext = {
285
206
  config: this.config,
286
- embeddedMode: this.embeddedMode || undefined,
287
207
  proxyManager: this.proxyManager || undefined
288
208
  };
289
209
 
290
210
  return executeCommand(ctx, command, args);
291
211
  }
292
212
 
293
- /**
294
- * Get credential (for embedded mode)
295
- */
296
- async getCredential(service: string, key: string): Promise<string | null> {
297
- if (!this.embeddedMode) {
298
- throw new Error('Plugin not initialized');
299
- }
300
- return this.embeddedMode.getCredential(service, key);
301
- }
302
-
303
- /**
304
- * Set credential (for embedded mode)
305
- */
306
- async setCredential(service: string, key: string, value: string): Promise<void> {
307
- if (!this.embeddedMode) {
308
- throw new Error('Plugin not initialized');
309
- }
310
- return this.embeddedMode.setCredential(service, key, value);
311
- }
312
-
313
- /**
314
- * List credentials
315
- */
316
- async listCredentials(service?: string): Promise<Array<{ service: string; key: string }>> {
317
- if (!this.embeddedMode) {
318
- throw new Error('Plugin not initialized');
319
- }
320
- return this.embeddedMode.listCredentials(service);
321
- }
322
-
323
213
  /**
324
214
  * Get plugin status
325
215
  */
326
216
  getStatus(): {
327
217
  initialized: boolean;
328
- mode: string;
329
218
  backend: string;
330
219
  proxyRunning: boolean;
331
220
  services: string[];
332
221
  } {
333
222
  return {
334
223
  initialized: this.initialized,
335
- mode: this.config.mode || 'embedded',
336
224
  backend: this.config.backend || 'keychain',
337
225
  proxyRunning: this.proxyManager?.isRunning() || false,
338
226
  services: this.config.services || []
339
227
  };
340
228
  }
341
229
 
342
- /**
343
- * Get current operating mode
344
- */
345
- getMode(): 'embedded' | 'proxy' {
346
- return (this.config.mode as 'embedded' | 'proxy') || 'embedded';
347
- }
348
-
349
230
  /**
350
231
  * Get configured backend
351
232
  */
@@ -373,20 +254,12 @@ export class AquamanPlugin {
373
254
  getCommands(): PluginCommand[] {
374
255
  const ctx: CommandContext = {
375
256
  config: this.config,
376
- embeddedMode: this.embeddedMode || undefined,
377
257
  proxyManager: this.proxyManager || undefined
378
258
  };
379
259
 
380
260
  return getAvailableCommands(ctx);
381
261
  }
382
262
 
383
- /**
384
- * Get proxy URL for a service (proxy mode only)
385
- */
386
- getProxyUrl(service: string): string | null {
387
- return this.proxyManager?.getServiceUrl(service) || null;
388
- }
389
-
390
263
  /**
391
264
  * Check if proxy is healthy
392
265
  */
@@ -1,43 +1,67 @@
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
+ *
7
+ * Uses http.request with socketPath for UDS communication.
8
+ */
9
+
10
+ import * as http from 'node:http';
11
+
12
+ /**
13
+ * Make an HTTP request over a Unix domain socket.
6
14
  */
15
+ function udsRequest(socketPath: string, urlPath: string, timeoutMs: number = 3000): Promise<{ ok: boolean; data: any }> {
16
+ return new Promise((resolve) => {
17
+ const req = http.request(
18
+ { socketPath, path: urlPath, method: 'GET' },
19
+ (res) => {
20
+ let body = '';
21
+ res.on('data', (chunk) => { body += chunk; });
22
+ res.on('end', () => {
23
+ try {
24
+ resolve({ ok: res.statusCode === 200, data: JSON.parse(body) });
25
+ } catch {
26
+ resolve({ ok: false, data: null });
27
+ }
28
+ });
29
+ }
30
+ );
31
+ req.on('error', () => resolve({ ok: false, data: null }));
32
+ req.setTimeout(timeoutMs, () => { req.destroy(); resolve({ ok: false, data: null }); });
33
+ req.end();
34
+ });
35
+ }
7
36
 
8
37
  /**
9
- * Request host map from proxy's /_hostmap endpoint.
38
+ * Request host map from proxy's /_hostmap endpoint via UDS.
10
39
  * Returns an empty map if the endpoint is unavailable (caller handles fallback).
11
40
  */
12
- export async function fetchHostMap(
13
- baseUrl: string,
14
- token: string | null,
15
- ): Promise<Map<string, string>> {
16
- try {
17
- const headers: Record<string, string> = {};
18
- if (token) headers['X-Aquaman-Token'] = token;
19
- const resp = await fetch(`${baseUrl}/_hostmap`, {
20
- headers,
21
- signal: AbortSignal.timeout(3000),
22
- });
23
- if (resp.ok) {
24
- const obj = (await resp.json()) as Record<string, string>;
25
- return new Map(Object.entries(obj));
26
- }
27
- } catch {
28
- // Proxy may be older version without /_hostmap — caller uses fallback
41
+ export async function loadHostMap(socketPath: string): Promise<Map<string, string>> {
42
+ const result = await udsRequest(socketPath, '/_hostmap');
43
+ if (result.ok && result.data) {
44
+ return new Map(Object.entries(result.data as Record<string, string>));
29
45
  }
30
46
  return new Map();
31
47
  }
32
48
 
33
49
  /**
34
- * Check if a proxy is already running on the given port.
50
+ * Check if a proxy is running on the given socket path.
51
+ */
52
+ export async function isProxyRunning(socketPath: string): Promise<boolean> {
53
+ const result = await udsRequest(socketPath, '/_health');
54
+ return result.ok;
55
+ }
56
+
57
+ /**
58
+ * Get the version of a running proxy from its /_health endpoint via UDS.
59
+ * Returns null if the proxy is not running or doesn't report version.
35
60
  */
36
- export async function isProxyRunning(port: number): Promise<boolean> {
37
- try {
38
- const resp = await fetch(`http://127.0.0.1:${port}/_health`);
39
- return resp.ok;
40
- } catch {
41
- return false;
61
+ export async function getProxyVersion(socketPath: string): Promise<string | null> {
62
+ const result = await udsRequest(socketPath, '/_health');
63
+ if (result.ok && result.data?.version) {
64
+ return result.data.version;
42
65
  }
66
+ return null;
43
67
  }
@@ -2,22 +2,20 @@
2
2
  * Proxy process lifecycle manager
3
3
  *
4
4
  * Spawns and manages the aquaman proxy daemon as a separate process
5
- * for maximum credential isolation.
5
+ * for maximum credential isolation. Communicates via Unix domain socket.
6
6
  */
7
7
 
8
8
  import { spawn, type ChildProcess } from 'node:child_process';
9
9
  import * as path from 'node:path';
10
10
  import * as fs from 'node:fs';
11
+ import * as os from 'node:os';
11
12
  import type { PluginConfig } from './config-schema.js';
12
13
 
13
14
  export interface ProxyConnectionInfo {
14
15
  ready: boolean;
15
- port: number;
16
- protocol: 'http' | 'https';
17
- baseUrl: string;
16
+ socketPath: string;
18
17
  services: string[];
19
18
  backend: string;
20
- token?: string;
21
19
  hostMap?: Record<string, string>;
22
20
  }
23
21
 
@@ -79,11 +77,8 @@ export class ProxyManager {
79
77
  return;
80
78
  }
81
79
 
82
- // Build arguments
83
- const args = [
84
- 'plugin-mode',
85
- '--port', String(config.proxyPort || 8081)
86
- ];
80
+ // Build arguments — UDS is the default, no --port needed
81
+ const args = ['plugin-mode'];
87
82
 
88
83
  // Spawn proxy process
89
84
  this.process = spawn(binaryPath, args, {
@@ -139,7 +134,8 @@ export class ProxyManager {
139
134
  this.options.onExit?.(code);
140
135
 
141
136
  if (!this.connectionInfo) {
142
- const error = new Error(`Proxy exited with code ${code}: ${stderr || stdout}`);
137
+ const stderrText = stderr || stdout;
138
+ const error = new Error(`Proxy exited with code ${code}: ${stderrText}`);
143
139
  reject(error);
144
140
  }
145
141
  });
@@ -196,20 +192,10 @@ export class ProxyManager {
196
192
  }
197
193
 
198
194
  /**
199
- * Get client authentication token
195
+ * Get socket path
200
196
  */
201
- getClientToken(): string | null {
202
- return this.connectionInfo?.token || null;
203
- }
204
-
205
- /**
206
- * Get base URL for a service
207
- */
208
- getServiceUrl(service: string): string | null {
209
- if (!this.connectionInfo) {
210
- return null;
211
- }
212
- return `${this.connectionInfo.baseUrl}/${service}`;
197
+ getSocketPath(): string | null {
198
+ return this.connectionInfo?.socketPath || null;
213
199
  }
214
200
 
215
201
  /**
package/src/embedded.ts DELETED
@@ -1,182 +0,0 @@
1
- /**
2
- * Embedded mode - Direct vault access within OpenClaw process
3
- *
4
- * This mode provides simpler setup but credentials DO enter the Gateway process memory.
5
- * Use proxy mode for maximum isolation.
6
- */
7
-
8
- import {
9
- createCredentialStore,
10
- createAuditLogger,
11
- type CredentialStore,
12
- type AuditLogger,
13
- type CredentialBackend
14
- } from 'aquaman-core';
15
- import type { PluginConfig } from './config-schema.js';
16
-
17
- export interface EmbeddedModeOptions {
18
- config: PluginConfig;
19
- }
20
-
21
- export class EmbeddedMode {
22
- private config: PluginConfig;
23
- private store: CredentialStore | null = null;
24
- private auditLogger: AuditLogger | null = null;
25
- private initialized = false;
26
-
27
- constructor(options: EmbeddedModeOptions) {
28
- this.config = options.config;
29
- }
30
-
31
- /**
32
- * Initialize the embedded mode
33
- */
34
- async initialize(): Promise<void> {
35
- if (this.initialized) {
36
- return;
37
- }
38
-
39
- // Initialize credential store
40
- this.store = createCredentialStore({
41
- backend: (this.config.backend || 'keychain') as CredentialBackend,
42
- vaultAddress: this.config.vaultAddress,
43
- vaultToken: this.config.vaultToken,
44
- vaultNamespace: this.config.vaultNamespace,
45
- vaultMountPath: this.config.vaultMountPath,
46
- onePasswordVault: this.config.onePasswordVault,
47
- onePasswordAccount: this.config.onePasswordAccount,
48
- encryptionPassword: process.env['AQUAMAN_ENCRYPTION_PASSWORD']
49
- });
50
-
51
- // Initialize audit logger
52
- if (this.config.auditEnabled !== false) {
53
- this.auditLogger = createAuditLogger({
54
- logDir: this.config.auditLogDir || '~/.aquaman/audit',
55
- enabled: true
56
- });
57
- await this.auditLogger.initialize();
58
- }
59
-
60
- this.initialized = true;
61
- }
62
-
63
- /**
64
- * Get a credential for a service
65
- * WARNING: This retrieves the credential into process memory
66
- */
67
- async getCredential(service: string, key: string): Promise<string | null> {
68
- if (!this.store) {
69
- throw new Error('Embedded mode not initialized');
70
- }
71
-
72
- const credential = await this.store.get(service, key);
73
-
74
- // Log access
75
- if (this.auditLogger) {
76
- await this.auditLogger.logCredentialAccess('embedded', 'openclaw', {
77
- service,
78
- operation: 'read',
79
- success: credential !== null
80
- });
81
- }
82
-
83
- return credential;
84
- }
85
-
86
- /**
87
- * Set a credential
88
- */
89
- async setCredential(service: string, key: string, value: string): Promise<void> {
90
- if (!this.store) {
91
- throw new Error('Embedded mode not initialized');
92
- }
93
-
94
- await this.store.set(service, key, value);
95
-
96
- // Log access
97
- if (this.auditLogger) {
98
- await this.auditLogger.logCredentialAccess('embedded', 'openclaw', {
99
- service,
100
- operation: 'use',
101
- success: true
102
- });
103
- }
104
- }
105
-
106
- /**
107
- * Delete a credential
108
- */
109
- async deleteCredential(service: string, key: string): Promise<boolean> {
110
- if (!this.store) {
111
- throw new Error('Embedded mode not initialized');
112
- }
113
-
114
- return this.store.delete(service, key);
115
- }
116
-
117
- /**
118
- * List credentials
119
- */
120
- async listCredentials(service?: string): Promise<Array<{ service: string; key: string }>> {
121
- if (!this.store) {
122
- throw new Error('Embedded mode not initialized');
123
- }
124
-
125
- return this.store.list(service);
126
- }
127
-
128
- /**
129
- * Check if a credential exists
130
- */
131
- async hasCredential(service: string, key: string): Promise<boolean> {
132
- if (!this.store) {
133
- throw new Error('Embedded mode not initialized');
134
- }
135
-
136
- return this.store.exists(service, key);
137
- }
138
-
139
- /**
140
- * Get the audit logger
141
- */
142
- getAuditLogger(): AuditLogger | null {
143
- return this.auditLogger;
144
- }
145
-
146
- /**
147
- * Get status info
148
- */
149
- getStatus(): { initialized: boolean; backend: string; auditEnabled: boolean } {
150
- return {
151
- initialized: this.initialized,
152
- backend: this.config.backend || 'keychain',
153
- auditEnabled: this.config.auditEnabled !== false
154
- };
155
- }
156
-
157
- /**
158
- * Verify audit log integrity
159
- */
160
- async verifyAuditIntegrity(): Promise<{ valid: boolean; errors: string[] }> {
161
- if (!this.auditLogger) {
162
- return { valid: true, errors: [] };
163
- }
164
-
165
- return this.auditLogger.verifyIntegrity();
166
- }
167
-
168
- /**
169
- * Get recent audit entries
170
- */
171
- async getRecentAuditEntries(count: number = 10): Promise<any[]> {
172
- if (!this.auditLogger) {
173
- return [];
174
- }
175
-
176
- return this.auditLogger.tail(count);
177
- }
178
- }
179
-
180
- export function createEmbeddedMode(options: EmbeddedModeOptions): EmbeddedMode {
181
- return new EmbeddedMode(options);
182
- }