aquaman-plugin 0.1.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/LICENSE +21 -0
- package/README.md +95 -0
- package/index.ts +328 -0
- package/openclaw.plugin.json +34 -0
- package/package.json +47 -0
- package/src/commands.ts +381 -0
- package/src/config-schema.ts +101 -0
- package/src/embedded.ts +182 -0
- package/src/http-interceptor.ts +168 -0
- package/src/index.ts +74 -0
- package/src/plugin.ts +408 -0
- package/src/proxy-manager.ts +265 -0
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Proxy process lifecycle manager
|
|
3
|
+
*
|
|
4
|
+
* Spawns and manages the aquaman proxy daemon as a separate process
|
|
5
|
+
* for maximum credential isolation.
|
|
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 type { PluginConfig } from './config-schema.js';
|
|
12
|
+
|
|
13
|
+
export interface ProxyConnectionInfo {
|
|
14
|
+
ready: boolean;
|
|
15
|
+
port: number;
|
|
16
|
+
protocol: 'http' | 'https';
|
|
17
|
+
baseUrl: string;
|
|
18
|
+
services: string[];
|
|
19
|
+
backend: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ProxyManagerOptions {
|
|
23
|
+
config: PluginConfig;
|
|
24
|
+
onReady?: (info: ProxyConnectionInfo) => void;
|
|
25
|
+
onError?: (error: Error) => void;
|
|
26
|
+
onExit?: (code: number | null) => void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class ProxyManager {
|
|
30
|
+
private process: ChildProcess | null = null;
|
|
31
|
+
private options: ProxyManagerOptions;
|
|
32
|
+
private connectionInfo: ProxyConnectionInfo | null = null;
|
|
33
|
+
private starting = false;
|
|
34
|
+
private startPromise: Promise<ProxyConnectionInfo> | null = null;
|
|
35
|
+
|
|
36
|
+
constructor(options: ProxyManagerOptions) {
|
|
37
|
+
this.options = options;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Start the proxy process
|
|
42
|
+
*/
|
|
43
|
+
async start(): Promise<ProxyConnectionInfo> {
|
|
44
|
+
if (this.process && this.connectionInfo) {
|
|
45
|
+
return this.connectionInfo;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (this.starting && this.startPromise) {
|
|
49
|
+
return this.startPromise;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
this.starting = true;
|
|
53
|
+
this.startPromise = this.doStart();
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
const result = await this.startPromise;
|
|
57
|
+
return result;
|
|
58
|
+
} finally {
|
|
59
|
+
this.starting = false;
|
|
60
|
+
this.startPromise = null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
private async doStart(): Promise<ProxyConnectionInfo> {
|
|
65
|
+
return new Promise((resolve, reject) => {
|
|
66
|
+
const config = this.options.config;
|
|
67
|
+
|
|
68
|
+
// Find aquaman binary
|
|
69
|
+
const binaryPath = this.findAquamanBinary();
|
|
70
|
+
|
|
71
|
+
if (!binaryPath) {
|
|
72
|
+
const error = new Error(
|
|
73
|
+
'aquaman proxy binary not found. Install with: npm install -g aquaman-proxy'
|
|
74
|
+
);
|
|
75
|
+
this.options.onError?.(error);
|
|
76
|
+
reject(error);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Build arguments
|
|
81
|
+
const args = [
|
|
82
|
+
'plugin-mode',
|
|
83
|
+
'--port', String(config.proxyPort || 8081)
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
// Spawn proxy process
|
|
87
|
+
this.process = spawn(binaryPath, args, {
|
|
88
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
89
|
+
env: {
|
|
90
|
+
...process.env,
|
|
91
|
+
// Pass config through environment
|
|
92
|
+
AQUAMAN_BACKEND: config.backend,
|
|
93
|
+
AQUAMAN_VAULT_ADDRESS: config.vaultAddress,
|
|
94
|
+
AQUAMAN_VAULT_TOKEN: config.vaultToken,
|
|
95
|
+
AQUAMAN_VAULT_NAMESPACE: config.vaultNamespace,
|
|
96
|
+
AQUAMAN_VAULT_MOUNT_PATH: config.vaultMountPath,
|
|
97
|
+
AQUAMAN_1PASSWORD_VAULT: config.onePasswordVault,
|
|
98
|
+
AQUAMAN_1PASSWORD_ACCOUNT: config.onePasswordAccount
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
let stdout = '';
|
|
103
|
+
let stderr = '';
|
|
104
|
+
|
|
105
|
+
this.process.stdout?.on('data', (data) => {
|
|
106
|
+
stdout += data.toString();
|
|
107
|
+
|
|
108
|
+
// Try to parse connection info from first line
|
|
109
|
+
const firstLine = stdout.split('\n')[0];
|
|
110
|
+
if (firstLine && !this.connectionInfo) {
|
|
111
|
+
try {
|
|
112
|
+
const info = JSON.parse(firstLine) as ProxyConnectionInfo;
|
|
113
|
+
if (info.ready) {
|
|
114
|
+
this.connectionInfo = info;
|
|
115
|
+
this.options.onReady?.(info);
|
|
116
|
+
resolve(info);
|
|
117
|
+
}
|
|
118
|
+
} catch {
|
|
119
|
+
// Not JSON yet, keep buffering
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
this.process.stderr?.on('data', (data) => {
|
|
125
|
+
stderr += data.toString();
|
|
126
|
+
console.error('[aquaman-proxy]', data.toString().trim());
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
this.process.on('error', (error) => {
|
|
130
|
+
this.options.onError?.(error);
|
|
131
|
+
reject(error);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
this.process.on('exit', (code) => {
|
|
135
|
+
this.process = null;
|
|
136
|
+
this.connectionInfo = null;
|
|
137
|
+
this.options.onExit?.(code);
|
|
138
|
+
|
|
139
|
+
if (!this.connectionInfo) {
|
|
140
|
+
const error = new Error(`Proxy exited with code ${code}: ${stderr || stdout}`);
|
|
141
|
+
reject(error);
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// Timeout after 10 seconds
|
|
146
|
+
setTimeout(() => {
|
|
147
|
+
if (!this.connectionInfo) {
|
|
148
|
+
const error = new Error('Proxy startup timeout');
|
|
149
|
+
this.stop();
|
|
150
|
+
reject(error);
|
|
151
|
+
}
|
|
152
|
+
}, 10000);
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Stop the proxy process
|
|
158
|
+
*/
|
|
159
|
+
async stop(): Promise<void> {
|
|
160
|
+
if (!this.process) {
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return new Promise((resolve) => {
|
|
165
|
+
const proc = this.process!;
|
|
166
|
+
|
|
167
|
+
const timeout = setTimeout(() => {
|
|
168
|
+
proc.kill('SIGKILL');
|
|
169
|
+
}, 5000);
|
|
170
|
+
|
|
171
|
+
proc.on('exit', () => {
|
|
172
|
+
clearTimeout(timeout);
|
|
173
|
+
this.process = null;
|
|
174
|
+
this.connectionInfo = null;
|
|
175
|
+
resolve();
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
proc.kill('SIGTERM');
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Check if proxy is running
|
|
184
|
+
*/
|
|
185
|
+
isRunning(): boolean {
|
|
186
|
+
return this.process !== null && this.connectionInfo !== null;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Get connection info
|
|
191
|
+
*/
|
|
192
|
+
getConnectionInfo(): ProxyConnectionInfo | null {
|
|
193
|
+
return this.connectionInfo;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Get base URL for a service
|
|
198
|
+
*/
|
|
199
|
+
getServiceUrl(service: string): string | null {
|
|
200
|
+
if (!this.connectionInfo) {
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
return `${this.connectionInfo.baseUrl}/${service}`;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Health check
|
|
208
|
+
*/
|
|
209
|
+
async healthCheck(): Promise<boolean> {
|
|
210
|
+
if (!this.connectionInfo) {
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
try {
|
|
215
|
+
const response = await fetch(`${this.connectionInfo.baseUrl}/health`, {
|
|
216
|
+
method: 'GET',
|
|
217
|
+
signal: AbortSignal.timeout(5000)
|
|
218
|
+
});
|
|
219
|
+
return response.ok;
|
|
220
|
+
} catch {
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Find the aquaman binary
|
|
227
|
+
*/
|
|
228
|
+
private findAquamanBinary(): string | null {
|
|
229
|
+
// Check common locations
|
|
230
|
+
const locations = [
|
|
231
|
+
// In node_modules
|
|
232
|
+
path.join(process.cwd(), 'node_modules', '.bin', 'aquaman'),
|
|
233
|
+
path.join(process.cwd(), 'node_modules', '@aquaman', 'proxy', 'dist', 'cli', 'index.js'),
|
|
234
|
+
|
|
235
|
+
// Global install
|
|
236
|
+
'/usr/local/bin/aquaman',
|
|
237
|
+
|
|
238
|
+
// In PATH (will use which in spawn)
|
|
239
|
+
'aquaman'
|
|
240
|
+
];
|
|
241
|
+
|
|
242
|
+
for (const loc of locations) {
|
|
243
|
+
if (loc === 'aquaman') {
|
|
244
|
+
// Check if in PATH
|
|
245
|
+
try {
|
|
246
|
+
const { execSync } = require('child_process');
|
|
247
|
+
execSync('which aquaman', { stdio: 'ignore' });
|
|
248
|
+
return 'aquaman';
|
|
249
|
+
} catch {
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (fs.existsSync(loc)) {
|
|
255
|
+
return loc;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export function createProxyManager(options: ProxyManagerOptions): ProxyManager {
|
|
264
|
+
return new ProxyManager(options);
|
|
265
|
+
}
|