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/src/plugin.ts DELETED
@@ -1,281 +0,0 @@
1
- /**
2
- * OpenClaw Plugin Entry Point
3
- *
4
- * This is the main plugin that implements the OpenClaw plugin interface.
5
- * It provides credential isolation through proxy mode:
6
- * credentials are held in a separate process and never enter the Gateway.
7
- */
8
-
9
- import { type PluginConfig, mergeConfig, defaultConfig } from './config-schema.js';
10
- import { createProxyManager, type ProxyManager, type ProxyConnectionInfo } from './proxy-manager.js';
11
- import { executeCommand, type CommandContext, type CommandResult, getAvailableCommands, type PluginCommand } from './commands.js';
12
- import { HttpInterceptor, createHttpInterceptor } from './http-interceptor.js';
13
-
14
- /**
15
- * OpenClaw Plugin Interface (simplified for standalone use)
16
- *
17
- * When used with actual OpenClaw, this would implement their ToolPlugin interface.
18
- */
19
- export interface AquamanPluginOptions {
20
- config?: Partial<PluginConfig>;
21
- }
22
-
23
- export class AquamanPlugin {
24
- /**
25
- * Plugin name - required by OpenClaw ToolPlugin interface
26
- */
27
- readonly name = 'aquaman-plugin';
28
-
29
- private config: PluginConfig;
30
- private proxyManager: ProxyManager | null = null;
31
- private httpInterceptor: HttpInterceptor | null = null;
32
- private initialized = false;
33
- private environmentVariables: Record<string, string> = {};
34
-
35
- constructor(options: AquamanPluginOptions = {}) {
36
- this.config = mergeConfig(options.config || {});
37
- }
38
-
39
- /**
40
- * Plugin lifecycle: onLoad
41
- * Called when the plugin is loaded by OpenClaw
42
- *
43
- * @param config - Configuration passed from openclaw.json
44
- */
45
- async onLoad(config?: Partial<PluginConfig>): Promise<void> {
46
- if (this.initialized) {
47
- return;
48
- }
49
-
50
- // Merge any runtime config with defaults
51
- if (config) {
52
- this.config = mergeConfig({ ...this.config, ...config });
53
- }
54
-
55
- // Validate config
56
- this.validateConfig();
57
-
58
- console.log('[aquaman] Initializing plugin...');
59
-
60
- await this.initProxyMode();
61
-
62
- this.initialized = true;
63
- console.log('[aquaman] Plugin initialized in proxy mode');
64
- }
65
-
66
- /**
67
- * Validate configuration
68
- */
69
- private validateConfig(): void {
70
- // Vault backend requires address
71
- if (this.config.backend === 'vault' && !this.config.vaultAddress) {
72
- throw new Error('Vault backend requires vaultAddress configuration');
73
- }
74
- }
75
-
76
- /**
77
- * Plugin lifecycle: onUnload
78
- * Called when the plugin is unloaded
79
- */
80
- async onUnload(): Promise<void> {
81
- console.log('[aquaman] Unloading plugin...');
82
-
83
- if (this.httpInterceptor) {
84
- this.httpInterceptor.deactivate();
85
- this.httpInterceptor = null;
86
- }
87
-
88
- if (this.proxyManager) {
89
- await this.proxyManager.stop();
90
- this.proxyManager = null;
91
- }
92
-
93
- this.initialized = false;
94
-
95
- console.log('[aquaman] Plugin unloaded');
96
- }
97
-
98
- /**
99
- * Initialize proxy mode
100
- */
101
- private async initProxyMode(): Promise<void> {
102
- this.proxyManager = createProxyManager({
103
- config: this.config,
104
- onReady: (info) => {
105
- console.log(`[aquaman] Proxy ready on ${info.socketPath}`);
106
- this.configureEnvironmentForProxy();
107
- },
108
- onError: (error) => {
109
- console.error('[aquaman] Proxy error:', error);
110
- },
111
- onExit: (code) => {
112
- console.log(`[aquaman] Proxy exited with code ${code}`);
113
- }
114
- });
115
-
116
- // Start proxy
117
- try {
118
- const info = await this.proxyManager.start();
119
- this.configureEnvironmentForProxy();
120
- this.activateHttpInterceptor(info.socketPath);
121
- } catch (error) {
122
- console.error('[aquaman] Failed to start proxy:', error);
123
- }
124
- }
125
-
126
- /**
127
- * Activate HTTP interceptor for channel credential isolation.
128
- */
129
- private activateHttpInterceptor(proxySocketPath: string): void {
130
- // Build host map from the service registry's host patterns
131
- const hostMap = new Map<string, string>([
132
- ['api.anthropic.com', 'anthropic'],
133
- ['api.openai.com', 'openai'],
134
- ['api.github.com', 'github'],
135
- ['slack.com', 'slack'],
136
- ['*.slack.com', 'slack'],
137
- ['discord.com', 'discord'],
138
- ['*.discord.com', 'discord'],
139
- ['api.telegram.org', 'telegram'],
140
- ['matrix.org', 'matrix'],
141
- ['*.matrix.org', 'matrix'],
142
- ['api.line.me', 'line'],
143
- ['api-data.line.me', 'line'],
144
- ['api.twitch.tv', 'twitch'],
145
- ['id.twitch.tv', 'twitch'],
146
- ['api.twilio.com', 'twilio'],
147
- ['*.twilio.com', 'twilio'],
148
- ['api.telnyx.com', 'telnyx'],
149
- ['api.elevenlabs.io', 'elevenlabs'],
150
- ['openapi.zalo.me', 'zalo'],
151
- ['graph.microsoft.com', 'ms-teams'],
152
- ['open.feishu.cn', 'feishu'],
153
- ['open.larksuite.com', 'feishu'],
154
- ['chat.googleapis.com', 'google-chat'],
155
- ]);
156
-
157
- this.httpInterceptor = createHttpInterceptor({
158
- socketPath: proxySocketPath,
159
- hostMap,
160
- log: (msg) => console.log(msg),
161
- });
162
-
163
- this.httpInterceptor.activate();
164
- }
165
-
166
- /**
167
- * Configure environment variables using sentinel hostname
168
- */
169
- private configureEnvironmentForProxy(): void {
170
- const services = this.config.services || defaultConfig.services;
171
- for (const service of services!) {
172
- const serviceUrl = `http://aquaman.local/${service}`;
173
-
174
- switch (service) {
175
- case 'anthropic':
176
- this.setEnvVar('ANTHROPIC_BASE_URL', serviceUrl);
177
- break;
178
- case 'openai':
179
- this.setEnvVar('OPENAI_BASE_URL', serviceUrl);
180
- break;
181
- case 'github':
182
- this.setEnvVar('GITHUB_API_URL', serviceUrl);
183
- break;
184
- default: {
185
- const envKey = `${service.toUpperCase().replace(/-/g, '_')}_BASE_URL`;
186
- this.setEnvVar(envKey, serviceUrl);
187
- }
188
- }
189
- }
190
- }
191
-
192
- /**
193
- * Set an environment variable and track it
194
- */
195
- private setEnvVar(key: string, value: string): void {
196
- process.env[key] = value;
197
- this.environmentVariables[key] = value;
198
- console.log(`[aquaman] Set ${key}=${value}`);
199
- }
200
-
201
- /**
202
- * Execute a slash command
203
- */
204
- async executeCommand(command: string, args: string[] = []): Promise<CommandResult> {
205
- const ctx: CommandContext = {
206
- config: this.config,
207
- proxyManager: this.proxyManager || undefined
208
- };
209
-
210
- return executeCommand(ctx, command, args);
211
- }
212
-
213
- /**
214
- * Get plugin status
215
- */
216
- getStatus(): {
217
- initialized: boolean;
218
- backend: string;
219
- proxyRunning: boolean;
220
- services: string[];
221
- } {
222
- return {
223
- initialized: this.initialized,
224
- backend: this.config.backend || 'keychain',
225
- proxyRunning: this.proxyManager?.isRunning() || false,
226
- services: this.config.services || []
227
- };
228
- }
229
-
230
- /**
231
- * Get configured backend
232
- */
233
- getBackend(): string {
234
- return this.config.backend || 'keychain';
235
- }
236
-
237
- /**
238
- * Check if plugin is ready
239
- */
240
- isReady(): boolean {
241
- return this.initialized;
242
- }
243
-
244
- /**
245
- * Get environment variables set by the plugin
246
- */
247
- getEnvironmentVariables(): Record<string, string> {
248
- return { ...this.environmentVariables };
249
- }
250
-
251
- /**
252
- * Get available slash commands
253
- */
254
- getCommands(): PluginCommand[] {
255
- const ctx: CommandContext = {
256
- config: this.config,
257
- proxyManager: this.proxyManager || undefined
258
- };
259
-
260
- return getAvailableCommands(ctx);
261
- }
262
-
263
- /**
264
- * Check if proxy is healthy
265
- */
266
- async isProxyHealthy(): Promise<boolean> {
267
- return this.proxyManager?.isRunning() || false;
268
- }
269
- }
270
-
271
- /**
272
- * Create plugin instance
273
- */
274
- export function createAquamanPlugin(options?: AquamanPluginOptions): AquamanPlugin {
275
- return new AquamanPlugin(options);
276
- }
277
-
278
- /**
279
- * Default export for OpenClaw plugin loading
280
- */
281
- export default AquamanPlugin;
@@ -1,67 +0,0 @@
1
- /**
2
- * Proxy health and discovery utilities.
3
- *
4
- * Separated from index.ts to avoid co-locating network calls with env reads
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.
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
- }
36
-
37
- /**
38
- * Request host map from proxy's /_hostmap endpoint via UDS.
39
- * Returns an empty map if the endpoint is unavailable (caller handles fallback).
40
- */
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>));
45
- }
46
- return new Map();
47
- }
48
-
49
- /**
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.
60
- */
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;
65
- }
66
- return null;
67
- }
@@ -1,309 +0,0 @@
1
- /**
2
- * Proxy process lifecycle manager
3
- *
4
- * Spawns and manages the aquaman proxy daemon as a separate process
5
- * for maximum credential isolation. Communicates via Unix domain socket.
6
- */
7
-
8
- import { spawn, type ChildProcess } from 'node:child_process';
9
- import * as path from 'node:path';
10
- import * as fs from 'node:fs';
11
- import * as os from 'node:os';
12
- import { fileURLToPath } from 'node:url';
13
- import type { PluginConfig } from './config-schema.js';
14
-
15
- /**
16
- * Find the aquaman proxy binary.
17
- *
18
- * Search order:
19
- * 1. Plugin's own node_modules/.bin/aquaman (bundled dep — version-matched)
20
- * 2. PATH (global install via npm install -g aquaman-proxy)
21
- */
22
- export function findAquamanProxyBinary(): string | null {
23
- // 1. Resolve from this file's location → plugin package root → node_modules/.bin/
24
- const thisDir = path.dirname(fileURLToPath(import.meta.url));
25
- const pluginRoot = path.resolve(thisDir, '..');
26
- const localBin = path.join(pluginRoot, 'node_modules', '.bin', 'aquaman');
27
- if (fs.existsSync(localBin)) {
28
- return localBin;
29
- }
30
-
31
- // 2. Search PATH
32
- const pathEnv = process.env.PATH || '';
33
- const dirs = pathEnv.split(path.delimiter);
34
- for (const dir of dirs) {
35
- const candidate = path.join(dir, 'aquaman');
36
- try {
37
- fs.accessSync(candidate, fs.constants.X_OK);
38
- return candidate;
39
- } catch {
40
- // Not found in this dir
41
- }
42
- }
43
-
44
- return null;
45
- }
46
-
47
- /**
48
- * Execute an aquaman proxy CLI command (non-interactive).
49
- * Captures stdout/stderr and returns them.
50
- */
51
- export function execAquamanProxyCli(
52
- args: string[],
53
- options?: { timeoutMs?: number },
54
- ): Promise<{ stdout: string; stderr: string; exitCode: number }> {
55
- return new Promise((resolve, reject) => {
56
- const binary = findAquamanProxyBinary();
57
- if (!binary) {
58
- reject(new Error('aquaman proxy binary not found. Install with: npm install -g aquaman-proxy'));
59
- return;
60
- }
61
-
62
- const proc = spawn(binary, args, {
63
- stdio: ['ignore', 'pipe', 'pipe'],
64
- env: process.env,
65
- });
66
-
67
- let stdout = '';
68
- let stderr = '';
69
-
70
- proc.stdout?.on('data', (d: Buffer) => { stdout += d.toString(); });
71
- proc.stderr?.on('data', (d: Buffer) => { stderr += d.toString(); });
72
-
73
- proc.on('error', reject);
74
- proc.on('close', (code) => {
75
- resolve({ stdout, stderr, exitCode: code ?? 1 });
76
- });
77
-
78
- const timeout = options?.timeoutMs ?? 30_000;
79
- const timer = setTimeout(() => {
80
- proc.kill('SIGTERM');
81
- reject(new Error(`aquaman CLI timed out after ${timeout}ms`));
82
- }, timeout);
83
-
84
- proc.on('close', () => clearTimeout(timer));
85
- });
86
- }
87
-
88
- /**
89
- * Execute an aquaman proxy CLI command interactively (stdio: inherit).
90
- * Used for commands that need TTY input (setup, credentials add).
91
- */
92
- export function execAquamanProxyInteractive(
93
- args: string[],
94
- ): Promise<number> {
95
- return new Promise((resolve, reject) => {
96
- const binary = findAquamanProxyBinary();
97
- if (!binary) {
98
- reject(new Error('aquaman proxy binary not found. Install with: npm install -g aquaman-proxy'));
99
- return;
100
- }
101
-
102
- const proc = spawn(binary, args, {
103
- stdio: 'inherit',
104
- env: process.env,
105
- });
106
-
107
- proc.on('error', reject);
108
- proc.on('close', (code) => resolve(code ?? 1));
109
- });
110
- }
111
-
112
- export interface ProxyConnectionInfo {
113
- ready: boolean;
114
- socketPath: string;
115
- services: string[];
116
- backend: string;
117
- hostMap?: Record<string, string>;
118
- }
119
-
120
- export interface ProxyManagerOptions {
121
- config: PluginConfig;
122
- onReady?: (info: ProxyConnectionInfo) => void;
123
- onError?: (error: Error) => void;
124
- onExit?: (code: number | null) => void;
125
- }
126
-
127
- export class ProxyManager {
128
- private process: ChildProcess | null = null;
129
- private options: ProxyManagerOptions;
130
- private connectionInfo: ProxyConnectionInfo | null = null;
131
- private starting = false;
132
- private startPromise: Promise<ProxyConnectionInfo> | null = null;
133
-
134
- constructor(options: ProxyManagerOptions) {
135
- this.options = options;
136
- }
137
-
138
- /**
139
- * Start the proxy process
140
- */
141
- async start(): Promise<ProxyConnectionInfo> {
142
- if (this.process && this.connectionInfo) {
143
- return this.connectionInfo;
144
- }
145
-
146
- if (this.starting && this.startPromise) {
147
- return this.startPromise;
148
- }
149
-
150
- this.starting = true;
151
- this.startPromise = this.doStart();
152
-
153
- try {
154
- const result = await this.startPromise;
155
- return result;
156
- } finally {
157
- this.starting = false;
158
- this.startPromise = null;
159
- }
160
- }
161
-
162
- private async doStart(): Promise<ProxyConnectionInfo> {
163
- return new Promise((resolve, reject) => {
164
- const config = this.options.config;
165
-
166
- // Find aquaman binary
167
- const binaryPath = this.findBinary();
168
-
169
- if (!binaryPath) {
170
- const error = new Error(
171
- 'aquaman proxy binary not found. Install with: npm install -g aquaman-proxy'
172
- );
173
- this.options.onError?.(error);
174
- reject(error);
175
- return;
176
- }
177
-
178
- // Build arguments — UDS is the default, no --port needed
179
- const args = ['plugin-mode'];
180
-
181
- // Spawn proxy process
182
- this.process = spawn(binaryPath, args, {
183
- stdio: ['ignore', 'pipe', 'pipe'],
184
- env: {
185
- ...process.env,
186
- // Pass config through environment
187
- AQUAMAN_BACKEND: config.backend,
188
- AQUAMAN_VAULT_ADDRESS: config.vaultAddress,
189
- AQUAMAN_VAULT_TOKEN: config.vaultToken,
190
- AQUAMAN_VAULT_NAMESPACE: config.vaultNamespace,
191
- AQUAMAN_VAULT_MOUNT_PATH: config.vaultMountPath,
192
- AQUAMAN_1PASSWORD_VAULT: config.onePasswordVault,
193
- AQUAMAN_1PASSWORD_ACCOUNT: config.onePasswordAccount
194
- }
195
- });
196
-
197
- let stdout = '';
198
- let stderr = '';
199
-
200
- this.process.stdout?.on('data', (data) => {
201
- stdout += data.toString();
202
-
203
- // Try to parse connection info from first line
204
- const firstLine = stdout.split('\n')[0];
205
- if (firstLine && !this.connectionInfo) {
206
- try {
207
- const info = JSON.parse(firstLine) as ProxyConnectionInfo;
208
- if (info.ready) {
209
- this.connectionInfo = info;
210
- this.options.onReady?.(info);
211
- resolve(info);
212
- }
213
- } catch {
214
- // Not JSON yet, keep buffering
215
- }
216
- }
217
- });
218
-
219
- this.process.stderr?.on('data', (data) => {
220
- stderr += data.toString();
221
- console.error('[aquaman-proxy]', data.toString().trim());
222
- });
223
-
224
- this.process.on('error', (error) => {
225
- this.options.onError?.(error);
226
- reject(error);
227
- });
228
-
229
- this.process.on('exit', (code) => {
230
- this.process = null;
231
- this.connectionInfo = null;
232
- this.options.onExit?.(code);
233
-
234
- if (!this.connectionInfo) {
235
- const stderrText = stderr || stdout;
236
- const error = new Error(`Proxy exited with code ${code}: ${stderrText}`);
237
- reject(error);
238
- }
239
- });
240
-
241
- // Timeout after 10 seconds
242
- setTimeout(() => {
243
- if (!this.connectionInfo) {
244
- const error = new Error('Proxy startup timeout');
245
- this.stop();
246
- reject(error);
247
- }
248
- }, 10000);
249
- });
250
- }
251
-
252
- /**
253
- * Stop the proxy process
254
- */
255
- async stop(): Promise<void> {
256
- if (!this.process) {
257
- return;
258
- }
259
-
260
- return new Promise((resolve) => {
261
- const proc = this.process!;
262
-
263
- const timeout = setTimeout(() => {
264
- proc.kill('SIGKILL');
265
- }, 5000);
266
-
267
- proc.on('exit', () => {
268
- clearTimeout(timeout);
269
- this.process = null;
270
- this.connectionInfo = null;
271
- resolve();
272
- });
273
-
274
- proc.kill('SIGTERM');
275
- });
276
- }
277
-
278
- /**
279
- * Check if proxy is running
280
- */
281
- isRunning(): boolean {
282
- return this.process !== null && this.connectionInfo !== null;
283
- }
284
-
285
- /**
286
- * Get connection info
287
- */
288
- getConnectionInfo(): ProxyConnectionInfo | null {
289
- return this.connectionInfo;
290
- }
291
-
292
- /**
293
- * Get socket path
294
- */
295
- getSocketPath(): string | null {
296
- return this.connectionInfo?.socketPath || null;
297
- }
298
-
299
- /**
300
- * Find the aquaman proxy binary
301
- */
302
- private findBinary(): string | null {
303
- return findAquamanProxyBinary();
304
- }
305
- }
306
-
307
- export function createProxyManager(options: ProxyManagerOptions): ProxyManager {
308
- return new ProxyManager(options);
309
- }