aquaman-plugin 0.6.0 → 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/README.md +10 -11
- package/index.ts +41 -82
- package/openclaw.plugin.json +0 -11
- package/package.json +5 -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/src/config-schema.ts
CHANGED
|
@@ -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
|
*/
|
|
@@ -38,19 +28,12 @@ export const ProxiedServices = Type.Array(Type.String(), {
|
|
|
38
28
|
* Complete plugin configuration schema
|
|
39
29
|
*/
|
|
40
30
|
export const ConfigSchema = Type.Object({
|
|
41
|
-
// Mode selection
|
|
42
|
-
mode: Type.Optional(PluginMode),
|
|
43
|
-
|
|
44
31
|
// Credential backend
|
|
45
32
|
backend: Type.Optional(CredentialBackend),
|
|
46
33
|
|
|
47
34
|
// Services to proxy
|
|
48
35
|
services: Type.Optional(ProxiedServices),
|
|
49
36
|
|
|
50
|
-
// Proxy mode options
|
|
51
|
-
proxyPort: Type.Optional(Type.Number({ default: 8081, minimum: 1024, maximum: 65535 })),
|
|
52
|
-
proxyAutoStart: Type.Optional(Type.Boolean({ default: true })),
|
|
53
|
-
|
|
54
37
|
// 1Password options
|
|
55
38
|
onePasswordVault: Type.Optional(Type.String()),
|
|
56
39
|
onePasswordAccount: Type.Optional(Type.String()),
|
|
@@ -60,15 +43,6 @@ export const ConfigSchema = Type.Object({
|
|
|
60
43
|
vaultToken: Type.Optional(Type.String()),
|
|
61
44
|
vaultNamespace: Type.Optional(Type.String()),
|
|
62
45
|
vaultMountPath: Type.Optional(Type.String({ default: 'secret' })),
|
|
63
|
-
|
|
64
|
-
// TLS options
|
|
65
|
-
tlsEnabled: Type.Optional(Type.Boolean({ default: true })),
|
|
66
|
-
tlsCertPath: Type.Optional(Type.String()),
|
|
67
|
-
tlsKeyPath: Type.Optional(Type.String()),
|
|
68
|
-
|
|
69
|
-
// Audit options
|
|
70
|
-
auditEnabled: Type.Optional(Type.Boolean({ default: true })),
|
|
71
|
-
auditLogDir: Type.Optional(Type.String())
|
|
72
46
|
});
|
|
73
47
|
|
|
74
48
|
export type PluginConfig = Static<typeof ConfigSchema>;
|
|
@@ -77,14 +51,9 @@ export type PluginConfig = Static<typeof ConfigSchema>;
|
|
|
77
51
|
* Default configuration values
|
|
78
52
|
*/
|
|
79
53
|
export const defaultConfig: PluginConfig = {
|
|
80
|
-
mode: 'embedded',
|
|
81
54
|
backend: 'keychain',
|
|
82
55
|
services: ['anthropic', 'openai'],
|
|
83
|
-
proxyPort: 8081,
|
|
84
|
-
proxyAutoStart: true,
|
|
85
56
|
vaultMountPath: 'secret',
|
|
86
|
-
tlsEnabled: true,
|
|
87
|
-
auditEnabled: true
|
|
88
57
|
};
|
|
89
58
|
|
|
90
59
|
/**
|
package/src/http-interceptor.ts
CHANGED
|
@@ -1,47 +1,44 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* HTTP
|
|
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
|
|
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
|
-
/**
|
|
15
|
-
|
|
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
|
|
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.
|
|
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
|
|
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
|
-
//
|
|
75
|
-
if (url.hostname ===
|
|
76
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
* -
|
|
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
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
|
136
|
-
this.configureEnvironmentForProxy(
|
|
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(
|
|
156
|
-
this.activateHttpInterceptor(info.
|
|
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(
|
|
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
|
-
|
|
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
|
|
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(
|
|
169
|
+
private configureEnvironmentForProxy(): void {
|
|
222
170
|
const services = this.config.services || defaultConfig.services;
|
|
223
|
-
|
|
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
|
-
|
|
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
|
*/
|
package/src/proxy-health.ts
CHANGED
|
@@ -3,60 +3,65 @@
|
|
|
3
3
|
*
|
|
4
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.
|
|
6
8
|
*/
|
|
7
9
|
|
|
10
|
+
import * as http from 'node:http';
|
|
11
|
+
|
|
8
12
|
/**
|
|
9
|
-
*
|
|
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.
|
|
10
39
|
* Returns an empty map if the endpoint is unavailable (caller handles fallback).
|
|
11
40
|
*/
|
|
12
|
-
export async function loadHostMap(
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
|
50
|
+
* Check if a proxy is running on the given socket path.
|
|
35
51
|
*/
|
|
36
|
-
export async function isProxyRunning(
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
return resp.ok;
|
|
40
|
-
} catch {
|
|
41
|
-
return false;
|
|
42
|
-
}
|
|
52
|
+
export async function isProxyRunning(socketPath: string): Promise<boolean> {
|
|
53
|
+
const result = await udsRequest(socketPath, '/_health');
|
|
54
|
+
return result.ok;
|
|
43
55
|
}
|
|
44
56
|
|
|
45
57
|
/**
|
|
46
|
-
* Get the version of a running proxy from its /_health endpoint.
|
|
58
|
+
* Get the version of a running proxy from its /_health endpoint via UDS.
|
|
47
59
|
* Returns null if the proxy is not running or doesn't report version.
|
|
48
60
|
*/
|
|
49
|
-
export async function getProxyVersion(
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
});
|
|
54
|
-
if (resp.ok) {
|
|
55
|
-
const data = (await resp.json()) as { version?: string };
|
|
56
|
-
return data.version || null;
|
|
57
|
-
}
|
|
58
|
-
} catch {
|
|
59
|
-
// Proxy not reachable
|
|
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;
|
|
60
65
|
}
|
|
61
66
|
return null;
|
|
62
67
|
}
|