@alfe.ai/openclaw-remote 0.0.14 → 0.0.16
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 +52 -0
- package/dist/index.cjs +11 -7
- package/dist/index.d.cts +376 -21
- package/dist/index.d.ts +376 -21
- package/dist/index.js +2 -2
- package/dist/plugin.cjs +4 -8
- package/dist/plugin.d.cts +5 -84
- package/dist/plugin.d.ts +5 -84
- package/dist/plugin.js +5 -2
- package/dist/runtime.cjs +842 -0
- package/dist/runtime.js +789 -0
- package/dist/types.d.cts +66 -0
- package/dist/types.d.ts +66 -0
- package/openclaw.plugin.json +9 -4
- package/package.json +9 -7
- package/dist/plugin2.cjs +0 -487
- package/dist/plugin2.d.cts +0 -2
- package/dist/plugin2.d.ts +0 -2
- package/dist/plugin2.js +0 -464
package/dist/runtime.cjs
ADDED
|
@@ -0,0 +1,842 @@
|
|
|
1
|
+
let node_module = require("node:module");
|
|
2
|
+
let node_path = require("node:path");
|
|
3
|
+
let _alfe_ai_agent_api_client = require("@alfe.ai/agent-api-client");
|
|
4
|
+
let _alfe_ai_browser = require("@alfe.ai/browser");
|
|
5
|
+
let _alfe_ai_config = require("@alfe.ai/config");
|
|
6
|
+
let _alfe_ai_openclaw_plugin_kit = require("@alfe.ai/openclaw-plugin-kit");
|
|
7
|
+
let _alfe_ai_remote = require("@alfe.ai/remote");
|
|
8
|
+
let _alfe_ai_terminal = require("@alfe.ai/terminal");
|
|
9
|
+
//#region src/ssrf.ts
|
|
10
|
+
/** Hostnames blocked outright (case-insensitive, exact match). */
|
|
11
|
+
const BLOCKED_HOSTNAMES = new Set(["localhost"]);
|
|
12
|
+
/** Hostname suffixes blocked outright (internal service discovery names). */
|
|
13
|
+
const BLOCKED_HOST_SUFFIXES = [".internal", ".local"];
|
|
14
|
+
/** Parse a dotted-quad IPv4 literal into its four octets, or null. */
|
|
15
|
+
function parseIpv4(host) {
|
|
16
|
+
const parts = host.split(".");
|
|
17
|
+
if (parts.length !== 4) return null;
|
|
18
|
+
const octets = [];
|
|
19
|
+
for (const part of parts) {
|
|
20
|
+
if (!/^\d{1,3}$/.test(part)) return null;
|
|
21
|
+
const n = Number(part);
|
|
22
|
+
if (n > 255) return null;
|
|
23
|
+
octets.push(n);
|
|
24
|
+
}
|
|
25
|
+
return octets;
|
|
26
|
+
}
|
|
27
|
+
/** Is this IPv4 literal private, loopback, link-local, or otherwise reserved? */
|
|
28
|
+
function isPrivateOrReservedIpv4(host) {
|
|
29
|
+
const octets = parseIpv4(host);
|
|
30
|
+
if (!octets) return false;
|
|
31
|
+
const [a, b] = octets;
|
|
32
|
+
if (a === 0) return true;
|
|
33
|
+
if (a === 10) return true;
|
|
34
|
+
if (a === 127) return true;
|
|
35
|
+
if (a === 169 && b === 254) return true;
|
|
36
|
+
if (a === 172 && b >= 16 && b <= 31) return true;
|
|
37
|
+
if (a === 192 && b === 0 && octets[2] === 0) return true;
|
|
38
|
+
if (a === 192 && b === 0 && octets[2] === 2) return true;
|
|
39
|
+
if (a === 192 && b === 168) return true;
|
|
40
|
+
if (a === 192 && b === 88 && octets[2] === 99) return true;
|
|
41
|
+
if (a === 100 && b >= 64 && b <= 127) return true;
|
|
42
|
+
if (a === 198 && (b === 18 || b === 19)) return true;
|
|
43
|
+
if (a === 198 && b === 51 && octets[2] === 100) return true;
|
|
44
|
+
if (a === 203 && b === 0 && octets[2] === 113) return true;
|
|
45
|
+
if (a >= 224) return true;
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
/** Is this IPv6 literal (brackets already stripped) loopback/private/link-local? */
|
|
49
|
+
function isPrivateOrReservedIpv6(host) {
|
|
50
|
+
const h = host.toLowerCase();
|
|
51
|
+
if (h === "::1" || h === "::") return true;
|
|
52
|
+
const mapped = /^::ffff:(.+)$/.exec(h);
|
|
53
|
+
if (mapped) {
|
|
54
|
+
const rest = mapped[1];
|
|
55
|
+
if (rest.includes(".")) return isPrivateOrReservedIpv4(rest);
|
|
56
|
+
const groups = rest.split(":");
|
|
57
|
+
if (groups.length === 2 && groups.every((g) => /^[0-9a-f]{1,4}$/.test(g))) {
|
|
58
|
+
const g1 = parseInt(groups[0], 16);
|
|
59
|
+
const g2 = parseInt(groups[1], 16);
|
|
60
|
+
return isPrivateOrReservedIpv4([
|
|
61
|
+
g1 >> 8 & 255,
|
|
62
|
+
g1 & 255,
|
|
63
|
+
g2 >> 8 & 255,
|
|
64
|
+
g2 & 255
|
|
65
|
+
].join("."));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (/^f[cd][0-9a-f]*:/.test(h)) return true;
|
|
69
|
+
if (/^fe[89ab][0-9a-f]*:/.test(h)) return true;
|
|
70
|
+
if (/^ff[0-9a-f]{2}:/u.test(h)) return true;
|
|
71
|
+
if (h.startsWith("100::") || h.startsWith("100:0:0:0:")) return true;
|
|
72
|
+
if (h === "2001:db8::" || h.startsWith("2001:db8:")) return true;
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Build the navigation predicate for a given SSRF policy. When
|
|
77
|
+
* `dangerouslyAllowPrivateNetwork` is true the predicate is allow-all
|
|
78
|
+
* otherwise it blocks the reserved ranges above and any non-http(s) scheme.
|
|
79
|
+
*/
|
|
80
|
+
function buildIsNavigationAllowed(policy, log) {
|
|
81
|
+
const allowPrivate = policy?.dangerouslyAllowPrivateNetwork === true;
|
|
82
|
+
const block = () => {
|
|
83
|
+
log?.warn("SSRF policy blocked browser navigation");
|
|
84
|
+
return false;
|
|
85
|
+
};
|
|
86
|
+
return (rawUrl) => {
|
|
87
|
+
let url;
|
|
88
|
+
try {
|
|
89
|
+
url = new URL(rawUrl);
|
|
90
|
+
} catch {
|
|
91
|
+
return block();
|
|
92
|
+
}
|
|
93
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") return block();
|
|
94
|
+
if (url.username !== "" || url.password !== "") return block();
|
|
95
|
+
if (allowPrivate) return true;
|
|
96
|
+
const host = url.hostname.toLowerCase().replace(/^\[|\]$/g, "");
|
|
97
|
+
if (BLOCKED_HOSTNAMES.has(host)) return block();
|
|
98
|
+
if (BLOCKED_HOST_SUFFIXES.some((s) => host.endsWith(s))) return block();
|
|
99
|
+
if (parseIpv4(host)) {
|
|
100
|
+
if (isPrivateOrReservedIpv4(host)) return block();
|
|
101
|
+
} else if (host.includes(":")) {
|
|
102
|
+
if (isPrivateOrReservedIpv6(host)) return block();
|
|
103
|
+
}
|
|
104
|
+
return true;
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
//#endregion
|
|
108
|
+
//#region src/runtime.ts
|
|
109
|
+
/**
|
|
110
|
+
* @alfe.ai/openclaw-remote — testable runtime for the interactive remote-control
|
|
111
|
+
* relay. Owns one outbound WS to the relay and routes per-session frames to the
|
|
112
|
+
* browser co-browse surface (@alfe.ai/browser) or the web terminal surface
|
|
113
|
+
* (@alfe.ai/terminal). Registers the agent-callable browser tools, including
|
|
114
|
+
* `request_browser_takeover` ("help me complete these").
|
|
115
|
+
*
|
|
116
|
+
* Follows the established Alfe plugin shape (see @alfe.ai/openclaw-webhooks):
|
|
117
|
+
* tools register on every activate(); the long-lived connection is guarded and
|
|
118
|
+
* created inside registerService.start.
|
|
119
|
+
*/
|
|
120
|
+
const pkg = (0, node_module.createRequire)(require("url").pathToFileURL(__filename).href)("../package.json");
|
|
121
|
+
const REMOTE_ACTIVATION_KEY = (0, _alfe_ai_openclaw_plugin_kit.getActivationKey)("remote");
|
|
122
|
+
const DEFAULT_HANDOFF_TIMEOUT_MS = 600 * 1e3;
|
|
123
|
+
const DEFAULT_CHROME_PATH = "/usr/bin/google-chrome-stable";
|
|
124
|
+
const RUNTIME_STATE_KEY = "__alfeOpenClawRemoteRuntimeState";
|
|
125
|
+
const MAX_REMOTE_URL_CHARS = 2048;
|
|
126
|
+
const MAX_PATH_CHARS = 4096;
|
|
127
|
+
const MAX_AGENT_ID_CHARS = 256;
|
|
128
|
+
const MAX_SESSION_ID_CHARS = 256;
|
|
129
|
+
const MAX_SELECTOR_CHARS = 4096;
|
|
130
|
+
const MAX_TYPE_TEXT_CHARS = 32 * 1024;
|
|
131
|
+
const MAX_EXPRESSION_CHARS = 128 * 1024;
|
|
132
|
+
const MAX_NAVIGATION_URL_CHARS = 8192;
|
|
133
|
+
const MAX_INSTRUCTIONS_CHARS = 2e3;
|
|
134
|
+
const MAX_CONVERSATION_ID_CHARS = 200;
|
|
135
|
+
const MAX_SCREENSHOT_BYTES = 10 * 1024 * 1024;
|
|
136
|
+
const MAX_TERMINAL_RELAY_BUFFER_BYTES = 4 * 1024 * 1024;
|
|
137
|
+
const GENERIC_TOOL_ERROR = "Tool execution failed. Retry, and inspect agent diagnostics if the failure persists.";
|
|
138
|
+
const PLUGIN_VERSION = validatePackageVersion(pkg.version);
|
|
139
|
+
function createRemotePluginRuntimeState() {
|
|
140
|
+
return {
|
|
141
|
+
generation: 0,
|
|
142
|
+
remoteClient: null,
|
|
143
|
+
browserSurface: null,
|
|
144
|
+
terminalSurface: null,
|
|
145
|
+
apiClient: null,
|
|
146
|
+
handoffTimeoutMs: DEFAULT_HANDOFF_TIMEOUT_MS,
|
|
147
|
+
sessionSurfaces: /* @__PURE__ */ new Map(),
|
|
148
|
+
stopPromise: null
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Derive the relay WebSocket URL from the agent's cloud apiUrl when the
|
|
153
|
+
* manifest doesn't provide one — mirrors @alfe.ai/console-client's
|
|
154
|
+
* `deriveConsoleWsUrl` so the URL is per-stage automatically:
|
|
155
|
+
* https://api.dev.alfe.ai → wss://remote.dev.alfe.ai/ws
|
|
156
|
+
* (matches config.flyDomains.remote per stage). This is why the
|
|
157
|
+
* headless-browser integration manifest carries no hardcoded, prod-pinned
|
|
158
|
+
* `remoteWsUrl`: the plugin resolves it from the agent's own endpoint.
|
|
159
|
+
*/
|
|
160
|
+
function deriveRemoteWsUrl(apiUrl) {
|
|
161
|
+
try {
|
|
162
|
+
const url = new URL(apiUrl);
|
|
163
|
+
if (url.protocol !== "https:" || url.username !== "" || url.password !== "" || !url.hostname.startsWith("api.")) return void 0;
|
|
164
|
+
return `wss://remote.${url.hostname.slice(4)}/ws`;
|
|
165
|
+
} catch {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Derive the dashboard (control-plane) base URL from the agent's cloud apiUrl,
|
|
171
|
+
* mirroring `deriveRemoteWsUrl` so it's per-stage automatically:
|
|
172
|
+
* https://api.dev.alfe.ai → https://app.dev.alfe.ai
|
|
173
|
+
* https://api.alfe.ai → https://app.alfe.ai (prod)
|
|
174
|
+
* The `api.` → `app.` swap matches config.*.ts `dashboardDomain` for every
|
|
175
|
+
* stage. Returns undefined for a non-`api.` host (e.g. localhost/dev override)
|
|
176
|
+
* so the caller can fall back to a session-only link.
|
|
177
|
+
*/
|
|
178
|
+
function deriveDashboardBaseUrl(apiUrl) {
|
|
179
|
+
try {
|
|
180
|
+
const url = new URL(apiUrl);
|
|
181
|
+
if (url.protocol !== "https:" || url.username !== "" || url.password !== "" || !url.hostname.startsWith("api.")) return void 0;
|
|
182
|
+
return `https://app.${url.hostname.slice(4)}`;
|
|
183
|
+
} catch {
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Build the dashboard deep-link ("control URL") the human opens to take over.
|
|
189
|
+
* Prefers the full agent-scoped path; if the dashboard host or agentId is
|
|
190
|
+
* unavailable, degrades gracefully (session-only path, then null).
|
|
191
|
+
*/
|
|
192
|
+
function buildControlUrl(dashboardBaseUrl, agentId, sessionId) {
|
|
193
|
+
if (!isDashboardBaseUrl(dashboardBaseUrl) || !isBoundedString(sessionId, MAX_SESSION_ID_CHARS)) return void 0;
|
|
194
|
+
const session = encodeURIComponent(sessionId);
|
|
195
|
+
if (agentId && isBoundedString(agentId, MAX_AGENT_ID_CHARS)) return `${dashboardBaseUrl}/agents/${encodeURIComponent(agentId)}?tab=browser&session=${session}`;
|
|
196
|
+
return `${dashboardBaseUrl}/agents?tab=browser&session=${session}`;
|
|
197
|
+
}
|
|
198
|
+
/** Route an inbound relay frame to the owning surface by session. */
|
|
199
|
+
function dispatchFrame(state, frame, log) {
|
|
200
|
+
if (frame.type === _alfe_ai_remote.RemoteFrameType.SESSION_OPEN) {
|
|
201
|
+
const open = (0, _alfe_ai_remote.decodeSessionOpenPayload)(frame.payload);
|
|
202
|
+
if (!open) return;
|
|
203
|
+
const existingSurface = state.sessionSurfaces.get(frame.sessionId);
|
|
204
|
+
if (existingSurface !== void 0 && existingSurface !== open.surface) {
|
|
205
|
+
log.warn("Remote session attempted to change surface type");
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
const handler = open.surface === "terminal" ? state.terminalSurface : state.browserSurface;
|
|
209
|
+
if (!handler) {
|
|
210
|
+
log.warn("Remote session surface is unavailable");
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
state.sessionSurfaces.set(frame.sessionId, open.surface);
|
|
214
|
+
Promise.resolve(handler.openSession(frame.sessionId, open)).catch(() => {
|
|
215
|
+
if (state.sessionSurfaces.get(frame.sessionId) === open.surface) state.sessionSurfaces.delete(frame.sessionId);
|
|
216
|
+
try {
|
|
217
|
+
handler.closeSession(frame.sessionId);
|
|
218
|
+
} catch {}
|
|
219
|
+
log.warn("Remote session could not be opened");
|
|
220
|
+
});
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
const surface = state.sessionSurfaces.get(frame.sessionId);
|
|
224
|
+
const handler = surface === "terminal" ? state.terminalSurface : surface === "browser" ? state.browserSurface : null;
|
|
225
|
+
if (!handler) return;
|
|
226
|
+
if (frame.type === _alfe_ai_remote.RemoteFrameType.SESSION_CLOSE) {
|
|
227
|
+
state.sessionSurfaces.delete(frame.sessionId);
|
|
228
|
+
try {
|
|
229
|
+
handler.closeSession(frame.sessionId);
|
|
230
|
+
} catch {
|
|
231
|
+
log.warn("Remote session close failed");
|
|
232
|
+
}
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
try {
|
|
236
|
+
handler.handleFrame(frame);
|
|
237
|
+
} catch {
|
|
238
|
+
log.warn("Remote session frame handling failed");
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
function startService(pluginConfig, ssrfPolicy, workspaceDir, log, dependencies, state) {
|
|
242
|
+
(0, _alfe_ai_openclaw_plugin_kit.guardedStart)(REMOTE_ACTIVATION_KEY, log, () => {
|
|
243
|
+
if (pluginConfig === null) {
|
|
244
|
+
log.error("Remote plugin configuration is invalid");
|
|
245
|
+
(0, _alfe_ai_openclaw_plugin_kit.resetActivation)(REMOTE_ACTIVATION_KEY);
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
return startServiceInner(pluginConfig, ssrfPolicy, workspaceDir, log, dependencies, state);
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
async function startServiceInner(pluginConfig, ssrfPolicy, workspaceDir, log, dependencies, state) {
|
|
252
|
+
if (state.remoteClient !== null || state.browserSurface !== null || state.terminalSurface !== null || state.apiClient !== null) {
|
|
253
|
+
log.warn("Remote runtime already owns active resources");
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
let alfeConfig = null;
|
|
257
|
+
try {
|
|
258
|
+
alfeConfig = dependencies.resolveRuntimeConfig();
|
|
259
|
+
} catch {
|
|
260
|
+
log.info("Could not resolve Alfe config — remote plugin idle");
|
|
261
|
+
}
|
|
262
|
+
const apiKey = alfeConfig?.apiKey;
|
|
263
|
+
const apiUrl = alfeConfig?.apiUrl;
|
|
264
|
+
const wsUrl = pluginConfig.remoteWsUrl ?? (apiUrl ? deriveRemoteWsUrl(apiUrl) : void 0);
|
|
265
|
+
if (!wsUrl || !apiKey || !apiUrl) {
|
|
266
|
+
log.info("Remote relay URL or credentials not configured — plugin running without relay");
|
|
267
|
+
(0, _alfe_ai_openclaw_plugin_kit.resetActivation)(REMOTE_ACTIVATION_KEY);
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
const generation = state.generation + 1;
|
|
271
|
+
const nextApiClient = dependencies.createApiClient({
|
|
272
|
+
apiKey,
|
|
273
|
+
apiUrl
|
|
274
|
+
});
|
|
275
|
+
const resolvedWorkspace = selectWorkspacePath(workspaceDir, alfeConfig?.workspacePath);
|
|
276
|
+
let nextBrowserSurface = null;
|
|
277
|
+
let nextTerminalSurface = null;
|
|
278
|
+
let nextRemoteClient = null;
|
|
279
|
+
const sendFrame = (frame) => {
|
|
280
|
+
if (state.generation === generation && state.remoteClient === nextRemoteClient) nextRemoteClient?.sendFrame(frame);
|
|
281
|
+
};
|
|
282
|
+
const sendTerminalFrame = (frame) => {
|
|
283
|
+
if (state.generation !== generation || state.remoteClient !== nextRemoteClient || nextRemoteClient === null) return;
|
|
284
|
+
if ((nextRemoteClient.bufferedAmount ?? 0) >= MAX_TERMINAL_RELAY_BUFFER_BYTES) throw new Error("Remote terminal relay buffer limit reached");
|
|
285
|
+
nextRemoteClient.sendFrame(frame);
|
|
286
|
+
};
|
|
287
|
+
try {
|
|
288
|
+
nextBrowserSurface = dependencies.createBrowserSurface({
|
|
289
|
+
executablePath: pluginConfig.browserExecutablePath ?? DEFAULT_CHROME_PATH,
|
|
290
|
+
headless: pluginConfig.browserHeadless ?? true,
|
|
291
|
+
noSandbox: pluginConfig.browserNoSandbox ?? true,
|
|
292
|
+
userDataDir: (0, node_path.join)(resolvedWorkspace, ".alfe-browser-profile"),
|
|
293
|
+
isNavigationAllowed: buildIsNavigationAllowed(ssrfPolicy, log),
|
|
294
|
+
logger: log
|
|
295
|
+
}, sendFrame);
|
|
296
|
+
nextTerminalSurface = dependencies.createTerminalSurface({
|
|
297
|
+
cwd: resolvedWorkspace,
|
|
298
|
+
logger: log
|
|
299
|
+
}, sendTerminalFrame);
|
|
300
|
+
nextRemoteClient = dependencies.createRemoteClient({
|
|
301
|
+
wsUrl,
|
|
302
|
+
apiKey,
|
|
303
|
+
onFrame: (frame) => {
|
|
304
|
+
if (state.generation === generation && state.remoteClient === nextRemoteClient) dispatchFrame(state, frame, log);
|
|
305
|
+
},
|
|
306
|
+
onConnectionChange: (connected) => {
|
|
307
|
+
if (state.generation !== generation || state.remoteClient !== nextRemoteClient) return;
|
|
308
|
+
log.info(`Remote relay connection: ${connected ? "connected" : "disconnected"}`);
|
|
309
|
+
if (!connected) closeAllSessions(state, log);
|
|
310
|
+
},
|
|
311
|
+
logger: log
|
|
312
|
+
});
|
|
313
|
+
} catch (error) {
|
|
314
|
+
nextRemoteClient?.stop();
|
|
315
|
+
nextTerminalSurface?.shutdown();
|
|
316
|
+
await nextBrowserSurface?.shutdown().catch(() => void 0);
|
|
317
|
+
throw error;
|
|
318
|
+
}
|
|
319
|
+
state.generation = generation;
|
|
320
|
+
state.handoffTimeoutMs = pluginConfig.handoffTimeoutMs ?? DEFAULT_HANDOFF_TIMEOUT_MS;
|
|
321
|
+
state.apiClient = nextApiClient;
|
|
322
|
+
state.browserSurface = nextBrowserSurface;
|
|
323
|
+
state.terminalSurface = nextTerminalSurface;
|
|
324
|
+
state.remoteClient = nextRemoteClient;
|
|
325
|
+
state.dashboardBaseUrl = deriveDashboardBaseUrl(apiUrl);
|
|
326
|
+
state.selfAgentId = void 0;
|
|
327
|
+
try {
|
|
328
|
+
nextRemoteClient.start();
|
|
329
|
+
} catch (error) {
|
|
330
|
+
await stopService(state, log);
|
|
331
|
+
throw error;
|
|
332
|
+
}
|
|
333
|
+
nextApiClient.whoami().then(({ agentId }) => {
|
|
334
|
+
if (state.generation === generation && state.apiClient === nextApiClient && isBoundedString(agentId, MAX_AGENT_ID_CHARS)) state.selfAgentId = agentId;
|
|
335
|
+
}).catch(() => {
|
|
336
|
+
log.debug("whoami failed; takeover links will omit agentId");
|
|
337
|
+
});
|
|
338
|
+
log.info("Remote plugin started");
|
|
339
|
+
}
|
|
340
|
+
function stopService(state, log) {
|
|
341
|
+
if (state.stopPromise !== null) return state.stopPromise;
|
|
342
|
+
const generation = state.generation + 1;
|
|
343
|
+
state.generation = generation;
|
|
344
|
+
const remoteClient = state.remoteClient;
|
|
345
|
+
const browserSurface = state.browserSurface;
|
|
346
|
+
const terminalSurface = state.terminalSurface;
|
|
347
|
+
const stopping = (async () => {
|
|
348
|
+
remoteClient?.stop();
|
|
349
|
+
closeAllSessions(state, log);
|
|
350
|
+
terminalSurface?.shutdown();
|
|
351
|
+
await browserSurface?.shutdown().catch(() => void 0);
|
|
352
|
+
if (state.generation === generation) {
|
|
353
|
+
state.remoteClient = null;
|
|
354
|
+
state.browserSurface = null;
|
|
355
|
+
state.terminalSurface = null;
|
|
356
|
+
state.apiClient = null;
|
|
357
|
+
state.dashboardBaseUrl = void 0;
|
|
358
|
+
state.selfAgentId = void 0;
|
|
359
|
+
state.handoffTimeoutMs = DEFAULT_HANDOFF_TIMEOUT_MS;
|
|
360
|
+
state.sessionSurfaces.clear();
|
|
361
|
+
}
|
|
362
|
+
})().finally(() => {
|
|
363
|
+
if (state.stopPromise === stopping) state.stopPromise = null;
|
|
364
|
+
(0, _alfe_ai_openclaw_plugin_kit.resetActivation)(REMOTE_ACTIVATION_KEY);
|
|
365
|
+
log.info("Remote plugin stopped");
|
|
366
|
+
});
|
|
367
|
+
state.stopPromise = stopping;
|
|
368
|
+
return stopping;
|
|
369
|
+
}
|
|
370
|
+
function closeAllSessions(state, log) {
|
|
371
|
+
for (const [sessionId, surface] of state.sessionSurfaces) {
|
|
372
|
+
const handler = surface === "terminal" ? state.terminalSurface : state.browserSurface;
|
|
373
|
+
try {
|
|
374
|
+
handler?.closeSession(sessionId);
|
|
375
|
+
} catch {
|
|
376
|
+
log.warn("Remote session cleanup failed");
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
state.sessionSurfaces.clear();
|
|
380
|
+
}
|
|
381
|
+
function registerTools(api, state) {
|
|
382
|
+
const needBrowser = () => {
|
|
383
|
+
if (!state.browserSurface) throw (0, _alfe_ai_openclaw_plugin_kit.publicToolError)("Browser surface is unavailable; retry after the remote service connects.");
|
|
384
|
+
return state.browserSurface;
|
|
385
|
+
};
|
|
386
|
+
api.registerTool(remoteTool({
|
|
387
|
+
name: "browser_navigate",
|
|
388
|
+
description: "Navigate the shared browser to a URL. Returns the final URL and page title.",
|
|
389
|
+
parameters: {
|
|
390
|
+
type: "object",
|
|
391
|
+
properties: { url: {
|
|
392
|
+
type: "string",
|
|
393
|
+
minLength: 1,
|
|
394
|
+
maxLength: MAX_NAVIGATION_URL_CHARS,
|
|
395
|
+
description: "Absolute credential-free HTTP(S) URL to open"
|
|
396
|
+
} },
|
|
397
|
+
required: ["url"],
|
|
398
|
+
additionalProperties: false
|
|
399
|
+
},
|
|
400
|
+
handler: async (params) => {
|
|
401
|
+
const url = requireHttpUrl(params.url, "url", MAX_NAVIGATION_URL_CHARS);
|
|
402
|
+
try {
|
|
403
|
+
return await needBrowser().automation.navigate(url);
|
|
404
|
+
} catch (error) {
|
|
405
|
+
if (error instanceof Error && error.message === "Navigation blocked by browser policy") throw (0, _alfe_ai_openclaw_plugin_kit.publicToolError)(error.message);
|
|
406
|
+
throw error;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}));
|
|
410
|
+
api.registerTool(remoteTool({
|
|
411
|
+
name: "browser_click",
|
|
412
|
+
description: "Click an element in the shared browser by CSS selector.",
|
|
413
|
+
parameters: {
|
|
414
|
+
type: "object",
|
|
415
|
+
properties: { selector: {
|
|
416
|
+
type: "string",
|
|
417
|
+
minLength: 1,
|
|
418
|
+
maxLength: MAX_SELECTOR_CHARS
|
|
419
|
+
} },
|
|
420
|
+
required: ["selector"],
|
|
421
|
+
additionalProperties: false
|
|
422
|
+
},
|
|
423
|
+
handler: async (params) => {
|
|
424
|
+
await needBrowser().automation.click(requireString(params.selector, "selector", MAX_SELECTOR_CHARS));
|
|
425
|
+
return { ok: true };
|
|
426
|
+
}
|
|
427
|
+
}));
|
|
428
|
+
api.registerTool(remoteTool({
|
|
429
|
+
name: "browser_type",
|
|
430
|
+
description: "Type text into an element in the shared browser by CSS selector.",
|
|
431
|
+
parameters: {
|
|
432
|
+
type: "object",
|
|
433
|
+
properties: {
|
|
434
|
+
selector: {
|
|
435
|
+
type: "string",
|
|
436
|
+
minLength: 1,
|
|
437
|
+
maxLength: MAX_SELECTOR_CHARS
|
|
438
|
+
},
|
|
439
|
+
text: {
|
|
440
|
+
type: "string",
|
|
441
|
+
maxLength: MAX_TYPE_TEXT_CHARS
|
|
442
|
+
}
|
|
443
|
+
},
|
|
444
|
+
required: ["selector", "text"],
|
|
445
|
+
additionalProperties: false
|
|
446
|
+
},
|
|
447
|
+
handler: async (params) => {
|
|
448
|
+
await needBrowser().automation.type(requireString(params.selector, "selector", MAX_SELECTOR_CHARS), requireString(params.text, "text", MAX_TYPE_TEXT_CHARS, true));
|
|
449
|
+
return { ok: true };
|
|
450
|
+
}
|
|
451
|
+
}));
|
|
452
|
+
api.registerTool(remoteTool({
|
|
453
|
+
name: "browser_wait_for",
|
|
454
|
+
description: "Wait for exactly one selector, URL substring, or fixed delay.",
|
|
455
|
+
parameters: {
|
|
456
|
+
type: "object",
|
|
457
|
+
properties: {
|
|
458
|
+
selector: {
|
|
459
|
+
type: "string",
|
|
460
|
+
minLength: 1,
|
|
461
|
+
maxLength: MAX_SELECTOR_CHARS
|
|
462
|
+
},
|
|
463
|
+
urlPattern: {
|
|
464
|
+
type: "string",
|
|
465
|
+
minLength: 1,
|
|
466
|
+
maxLength: MAX_NAVIGATION_URL_CHARS
|
|
467
|
+
},
|
|
468
|
+
ms: {
|
|
469
|
+
type: "integer",
|
|
470
|
+
minimum: 0,
|
|
471
|
+
maximum: 12e4
|
|
472
|
+
}
|
|
473
|
+
},
|
|
474
|
+
additionalProperties: false
|
|
475
|
+
},
|
|
476
|
+
handler: async (params) => {
|
|
477
|
+
const options = parseWaitOptions(params);
|
|
478
|
+
await needBrowser().automation.waitFor(options);
|
|
479
|
+
return { ok: true };
|
|
480
|
+
}
|
|
481
|
+
}));
|
|
482
|
+
api.registerTool({
|
|
483
|
+
name: "browser_screenshot",
|
|
484
|
+
label: "browser_screenshot",
|
|
485
|
+
description: "Capture a JPEG screenshot of the shared browser's current page.",
|
|
486
|
+
parameters: {
|
|
487
|
+
type: "object",
|
|
488
|
+
properties: {},
|
|
489
|
+
additionalProperties: false
|
|
490
|
+
},
|
|
491
|
+
execute: async (_toolCallId, params) => {
|
|
492
|
+
try {
|
|
493
|
+
if (!isRecord(params) || Object.keys(params).length > 0) throw (0, _alfe_ai_openclaw_plugin_kit.publicToolError)("browser_screenshot accepts no parameters");
|
|
494
|
+
const data = await needBrowser().automation.screenshot();
|
|
495
|
+
const bytes = decodedBase64Bytes(data);
|
|
496
|
+
if (bytes < 1 || bytes > MAX_SCREENSHOT_BYTES) throw new Error("Browser screenshot exceeded its media boundary");
|
|
497
|
+
return {
|
|
498
|
+
content: [{
|
|
499
|
+
type: "image",
|
|
500
|
+
data,
|
|
501
|
+
mimeType: "image/jpeg"
|
|
502
|
+
}],
|
|
503
|
+
details: {
|
|
504
|
+
mimeType: "image/jpeg",
|
|
505
|
+
bytes
|
|
506
|
+
}
|
|
507
|
+
};
|
|
508
|
+
} catch (error) {
|
|
509
|
+
return (0, _alfe_ai_openclaw_plugin_kit.errResult)(error instanceof _alfe_ai_openclaw_plugin_kit.PublicToolError ? error.message : GENERIC_TOOL_ERROR);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
});
|
|
513
|
+
api.registerTool(remoteTool({
|
|
514
|
+
name: "browser_evaluate",
|
|
515
|
+
description: "Evaluate a JavaScript expression in the shared browser page and return bounded JSON.",
|
|
516
|
+
parameters: {
|
|
517
|
+
type: "object",
|
|
518
|
+
properties: { expression: {
|
|
519
|
+
type: "string",
|
|
520
|
+
minLength: 1,
|
|
521
|
+
maxLength: MAX_EXPRESSION_CHARS
|
|
522
|
+
} },
|
|
523
|
+
required: ["expression"],
|
|
524
|
+
additionalProperties: false
|
|
525
|
+
},
|
|
526
|
+
handler: async (params) => ({ result: await needBrowser().automation.evaluate(requireString(params.expression, "expression", MAX_EXPRESSION_CHARS)) })
|
|
527
|
+
}));
|
|
528
|
+
api.registerTool(remoteTool({
|
|
529
|
+
name: "request_browser_takeover",
|
|
530
|
+
description: "Ask a human to take over the live browser for a manual step. Share the returned control URL with the user, then wait for the hand-back.",
|
|
531
|
+
parameters: {
|
|
532
|
+
type: "object",
|
|
533
|
+
properties: {
|
|
534
|
+
instructions: {
|
|
535
|
+
type: "string",
|
|
536
|
+
minLength: 1,
|
|
537
|
+
maxLength: MAX_INSTRUCTIONS_CHARS,
|
|
538
|
+
description: "Exact task for the human to complete"
|
|
539
|
+
},
|
|
540
|
+
url: {
|
|
541
|
+
type: "string",
|
|
542
|
+
minLength: 1,
|
|
543
|
+
maxLength: 2048,
|
|
544
|
+
description: "Optional credential-free HTTP(S) page shown for context"
|
|
545
|
+
},
|
|
546
|
+
conversationId: {
|
|
547
|
+
type: "string",
|
|
548
|
+
minLength: 1,
|
|
549
|
+
maxLength: MAX_CONVERSATION_ID_CHARS
|
|
550
|
+
}
|
|
551
|
+
},
|
|
552
|
+
required: ["instructions"],
|
|
553
|
+
additionalProperties: false
|
|
554
|
+
},
|
|
555
|
+
handler: async (params) => {
|
|
556
|
+
const surface = needBrowser();
|
|
557
|
+
const client = state.apiClient;
|
|
558
|
+
if (!client) throw (0, _alfe_ai_openclaw_plugin_kit.publicToolError)("Remote service is unavailable; retry after it connects.");
|
|
559
|
+
const generation = state.generation;
|
|
560
|
+
const instructions = requireString(params.instructions, "instructions", MAX_INSTRUCTIONS_CHARS);
|
|
561
|
+
const url = params.url === void 0 ? void 0 : requireHttpUrl(params.url, "url", 2048);
|
|
562
|
+
const conversationId = optionalString(params.conversationId, "conversationId", MAX_CONVERSATION_ID_CHARS);
|
|
563
|
+
const sessionId = requireTrustedString((await client.requestBrowserTakeover({
|
|
564
|
+
instructions,
|
|
565
|
+
url,
|
|
566
|
+
conversationId
|
|
567
|
+
})).sessionId, "remote session ID", MAX_SESSION_ID_CHARS);
|
|
568
|
+
if (state.generation !== generation || state.browserSurface !== surface || state.apiClient !== client) {
|
|
569
|
+
await client.completeRemoteSession(sessionId).catch(() => void 0);
|
|
570
|
+
throw (0, _alfe_ai_openclaw_plugin_kit.publicToolError)("Remote browser restarted while creating the takeover; retry the request.");
|
|
571
|
+
}
|
|
572
|
+
const controlUrl = buildControlUrl(state.dashboardBaseUrl, state.selfAgentId, sessionId);
|
|
573
|
+
const message = controlUrl ? `Browser takeover requested (session ${sessionId}). Ask the user to open ${controlUrl}, complete the task, and hand control back.` : `Browser takeover requested (session ${sessionId}). Ask the user to open this agent's Browser tab in the Alfe dashboard, complete the task, and hand control back.`;
|
|
574
|
+
surface.addHold();
|
|
575
|
+
try {
|
|
576
|
+
return {
|
|
577
|
+
sessionId,
|
|
578
|
+
controlUrl,
|
|
579
|
+
message,
|
|
580
|
+
...await surface.requestHandoff(state.handoffTimeoutMs)
|
|
581
|
+
};
|
|
582
|
+
} finally {
|
|
583
|
+
surface.removeHold();
|
|
584
|
+
await client.completeRemoteSession(sessionId).catch(() => void 0);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
}));
|
|
588
|
+
}
|
|
589
|
+
function createRemotePlugin(dependencies = {}) {
|
|
590
|
+
const state = dependencies.runtimeState ?? getGlobalRuntimeState();
|
|
591
|
+
const runtimeDependencies = {
|
|
592
|
+
resolveRuntimeConfig: dependencies.resolveConfig ?? _alfe_ai_config.resolveConfig,
|
|
593
|
+
createApiClient: dependencies.createApiClient ?? ((config) => new _alfe_ai_agent_api_client.AgentApiClient(config)),
|
|
594
|
+
createRemoteClient: dependencies.createRemoteClient ?? ((options) => new _alfe_ai_remote.RemoteServiceClient(options)),
|
|
595
|
+
createBrowserSurface: dependencies.createBrowserSurface ?? ((options, sendFrame) => new _alfe_ai_browser.BrowserSurface(options, sendFrame)),
|
|
596
|
+
createTerminalSurface: dependencies.createTerminalSurface ?? ((options, sendFrame) => new _alfe_ai_terminal.TerminalSurface(options, sendFrame))
|
|
597
|
+
};
|
|
598
|
+
const installErrorCapture = dependencies.installErrorCapture ?? _alfe_ai_agent_api_client.installToolErrorCapture;
|
|
599
|
+
return {
|
|
600
|
+
id: "@alfe.ai/openclaw-remote",
|
|
601
|
+
name: "Remote",
|
|
602
|
+
description: "Interactive remote control — browser co-browse takeover and web terminal",
|
|
603
|
+
version: PLUGIN_VERSION,
|
|
604
|
+
activate(api) {
|
|
605
|
+
installErrorCapture(api, { plugin: "openclaw-remote" });
|
|
606
|
+
const log = api.logger;
|
|
607
|
+
let pluginConfig = null;
|
|
608
|
+
let ssrfPolicy;
|
|
609
|
+
try {
|
|
610
|
+
const parsedPluginConfig = parseRemotePluginConfig(api.config?.plugins?.entries?.["@alfe.ai/openclaw-remote"]?.config, api.config?.browser);
|
|
611
|
+
const parsedSsrfPolicy = parseSsrfPolicy(api.config?.browser?.ssrfPolicy);
|
|
612
|
+
pluginConfig = parsedPluginConfig;
|
|
613
|
+
ssrfPolicy = parsedSsrfPolicy;
|
|
614
|
+
} catch {
|
|
615
|
+
log.error("Remote plugin configuration is invalid");
|
|
616
|
+
}
|
|
617
|
+
registerTools(api, state);
|
|
618
|
+
api.registerService({
|
|
619
|
+
id: "alfe-remote",
|
|
620
|
+
start: (context) => {
|
|
621
|
+
startService(pluginConfig, ssrfPolicy, context.workspaceDir, log, runtimeDependencies, state);
|
|
622
|
+
},
|
|
623
|
+
stop: () => stopService(state, log)
|
|
624
|
+
});
|
|
625
|
+
log.info("Alfe Remote plugin activated");
|
|
626
|
+
},
|
|
627
|
+
async deactivate(api) {
|
|
628
|
+
await stopService(state, api.logger);
|
|
629
|
+
api.logger.info("Alfe Remote plugin deactivated");
|
|
630
|
+
}
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
function parseRemotePluginConfig(value, browserFallback) {
|
|
634
|
+
const config = value === void 0 ? {} : requireRecord(value, "remote plugin config");
|
|
635
|
+
const allowedKeys = new Set([
|
|
636
|
+
"remoteWsUrl",
|
|
637
|
+
"browserExecutablePath",
|
|
638
|
+
"browserHeadless",
|
|
639
|
+
"browserNoSandbox",
|
|
640
|
+
"handoffTimeoutMs"
|
|
641
|
+
]);
|
|
642
|
+
if (Object.keys(config).some((key) => !allowedKeys.has(key))) throw new Error("Remote plugin config contains unsupported fields");
|
|
643
|
+
const browser = isRecord(browserFallback) ? browserFallback : {};
|
|
644
|
+
return {
|
|
645
|
+
remoteWsUrl: optionalRelayUrl(config.remoteWsUrl),
|
|
646
|
+
browserExecutablePath: optionalAbsolutePath(config.browserExecutablePath ?? browser.executablePath),
|
|
647
|
+
browserHeadless: optionalBoolean(config.browserHeadless ?? browser.headless, "browserHeadless"),
|
|
648
|
+
browserNoSandbox: optionalBoolean(config.browserNoSandbox ?? browser.noSandbox, "browserNoSandbox"),
|
|
649
|
+
handoffTimeoutMs: optionalInteger(config.handoffTimeoutMs, "handoffTimeoutMs", 1e3, 1800 * 1e3)
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
function parseSsrfPolicy(value) {
|
|
653
|
+
if (value === void 0) return void 0;
|
|
654
|
+
const policy = requireRecord(value, "browser SSRF policy");
|
|
655
|
+
if (Object.keys(policy).some((key) => key !== "dangerouslyAllowPrivateNetwork")) throw new Error("Browser SSRF policy contains unsupported fields");
|
|
656
|
+
return { dangerouslyAllowPrivateNetwork: optionalBoolean(policy.dangerouslyAllowPrivateNetwork, "dangerouslyAllowPrivateNetwork") };
|
|
657
|
+
}
|
|
658
|
+
function remoteTool(definition) {
|
|
659
|
+
return (0, _alfe_ai_openclaw_plugin_kit.defineTool)({
|
|
660
|
+
...definition,
|
|
661
|
+
handler: (params) => Promise.resolve(definition.handler(params))
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
function requireString(value, label, maxChars, allowEmpty = false) {
|
|
665
|
+
if (typeof value !== "string" || !allowEmpty && value.length < 1 || value.length > maxChars || hasControlCharacter(value)) throw (0, _alfe_ai_openclaw_plugin_kit.publicToolError)(`${label} must contain ${allowEmpty ? "at most" : "1 to"} ${String(maxChars)} non-control characters`);
|
|
666
|
+
return value;
|
|
667
|
+
}
|
|
668
|
+
function optionalString(value, label, maxChars) {
|
|
669
|
+
return value === void 0 ? void 0 : requireString(value, label, maxChars);
|
|
670
|
+
}
|
|
671
|
+
function requireHttpUrl(value, label, maxChars) {
|
|
672
|
+
const raw = requireString(value, label, maxChars);
|
|
673
|
+
let url;
|
|
674
|
+
try {
|
|
675
|
+
url = new URL(raw);
|
|
676
|
+
} catch {
|
|
677
|
+
throw (0, _alfe_ai_openclaw_plugin_kit.publicToolError)(`${label} must be an absolute HTTP(S) URL`);
|
|
678
|
+
}
|
|
679
|
+
if (url.protocol !== "http:" && url.protocol !== "https:" || url.username !== "" || url.password !== "") throw (0, _alfe_ai_openclaw_plugin_kit.publicToolError)(`${label} must be an absolute HTTP(S) URL without credentials`);
|
|
680
|
+
return url.href;
|
|
681
|
+
}
|
|
682
|
+
function parseWaitOptions(params) {
|
|
683
|
+
if ([
|
|
684
|
+
params.selector,
|
|
685
|
+
params.urlPattern,
|
|
686
|
+
params.ms
|
|
687
|
+
].filter((value) => value !== void 0).length !== 1) throw (0, _alfe_ai_openclaw_plugin_kit.publicToolError)("Provide exactly one of selector, urlPattern, or ms");
|
|
688
|
+
if (params.selector !== void 0) return { selector: requireString(params.selector, "selector", MAX_SELECTOR_CHARS) };
|
|
689
|
+
if (params.urlPattern !== void 0) return { urlPattern: requireString(params.urlPattern, "urlPattern", MAX_NAVIGATION_URL_CHARS) };
|
|
690
|
+
if (!Number.isInteger(params.ms) || params.ms < 0 || params.ms > 12e4) throw (0, _alfe_ai_openclaw_plugin_kit.publicToolError)("ms must be an integer from 0 to 120000");
|
|
691
|
+
return { ms: params.ms };
|
|
692
|
+
}
|
|
693
|
+
function requireTrustedString(value, label, maxChars) {
|
|
694
|
+
if (!isBoundedString(value, maxChars)) throw new Error(`${label} is invalid`);
|
|
695
|
+
return value;
|
|
696
|
+
}
|
|
697
|
+
function decodedBase64Bytes(value) {
|
|
698
|
+
if (!/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/u.test(value)) throw new Error("Browser screenshot was not valid base64");
|
|
699
|
+
return Buffer.from(value, "base64").byteLength;
|
|
700
|
+
}
|
|
701
|
+
function selectWorkspacePath(contextPath, configuredPath) {
|
|
702
|
+
for (const candidate of [contextPath, configuredPath]) if (typeof candidate === "string" && candidate.length > 0 && candidate.length <= MAX_PATH_CHARS && !hasControlCharacter(candidate) && (0, node_path.isAbsolute)(candidate)) return candidate;
|
|
703
|
+
throw new Error("Remote workspace path is invalid");
|
|
704
|
+
}
|
|
705
|
+
function optionalRelayUrl(value) {
|
|
706
|
+
if (value === void 0) return void 0;
|
|
707
|
+
if (!isBoundedString(value, MAX_REMOTE_URL_CHARS)) throw new Error("remoteWsUrl is invalid");
|
|
708
|
+
let url;
|
|
709
|
+
try {
|
|
710
|
+
url = new URL(value);
|
|
711
|
+
} catch {
|
|
712
|
+
throw new Error("remoteWsUrl is invalid");
|
|
713
|
+
}
|
|
714
|
+
const host = url.hostname.toLowerCase().replace(/^\[|\]$/gu, "");
|
|
715
|
+
const loopback = host === "localhost" || host === "::1" || /^127(?:\.\d{1,3}){3}$/u.test(host);
|
|
716
|
+
if (url.protocol !== "wss:" && !(url.protocol === "ws:" && loopback) || url.username !== "" || url.password !== "" || url.hash !== "") throw new Error("remoteWsUrl is invalid");
|
|
717
|
+
return url.href;
|
|
718
|
+
}
|
|
719
|
+
function isDashboardBaseUrl(value) {
|
|
720
|
+
if (!isBoundedString(value, MAX_REMOTE_URL_CHARS)) return false;
|
|
721
|
+
try {
|
|
722
|
+
const url = new URL(value);
|
|
723
|
+
return url.protocol === "https:" && url.username === "" && url.password === "" && url.hostname.startsWith("app.") && url.pathname === "/" && url.search === "" && url.hash === "";
|
|
724
|
+
} catch {
|
|
725
|
+
return false;
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
function optionalAbsolutePath(value) {
|
|
729
|
+
if (value === void 0) return void 0;
|
|
730
|
+
if (!isBoundedString(value, MAX_PATH_CHARS) || !(0, node_path.isAbsolute)(value)) throw new Error("browserExecutablePath is invalid");
|
|
731
|
+
return value;
|
|
732
|
+
}
|
|
733
|
+
function optionalBoolean(value, label) {
|
|
734
|
+
if (value === void 0) return void 0;
|
|
735
|
+
if (typeof value !== "boolean") throw new Error(`${label} must be a boolean`);
|
|
736
|
+
return value;
|
|
737
|
+
}
|
|
738
|
+
function optionalInteger(value, label, minimum, maximum) {
|
|
739
|
+
if (value === void 0) return void 0;
|
|
740
|
+
if (!Number.isInteger(value) || value < minimum || value > maximum) throw new Error(`${label} must be an integer from ${String(minimum)} to ${String(maximum)}`);
|
|
741
|
+
return value;
|
|
742
|
+
}
|
|
743
|
+
function getGlobalRuntimeState() {
|
|
744
|
+
const root = globalThis;
|
|
745
|
+
const existing = root[RUNTIME_STATE_KEY];
|
|
746
|
+
if (isRuntimeState(existing)) return existing;
|
|
747
|
+
const state = createRemotePluginRuntimeState();
|
|
748
|
+
root[RUNTIME_STATE_KEY] = state;
|
|
749
|
+
return state;
|
|
750
|
+
}
|
|
751
|
+
function isRuntimeState(value) {
|
|
752
|
+
if (!isRecord(value)) return false;
|
|
753
|
+
return typeof value.generation === "number" && Number.isSafeInteger(value.generation) && value.generation >= 0 && typeof value.handoffTimeoutMs === "number" && Number.isInteger(value.handoffTimeoutMs) && value.handoffTimeoutMs >= 1e3 && value.handoffTimeoutMs <= 1800 * 1e3 && value.sessionSurfaces instanceof Map && (value.stopPromise === null || value.stopPromise instanceof Promise) && (value.remoteClient === null || isRemoteClient(value.remoteClient)) && (value.browserSurface === null || isBrowserSurface(value.browserSurface)) && (value.terminalSurface === null || isTerminalSurface(value.terminalSurface)) && (value.apiClient === null || isRemoteApiClient(value.apiClient));
|
|
754
|
+
}
|
|
755
|
+
function isRemoteClient(value) {
|
|
756
|
+
return isRecord(value) && typeof value.start === "function" && typeof value.stop === "function" && typeof value.sendFrame === "function";
|
|
757
|
+
}
|
|
758
|
+
function isBrowserSurface(value) {
|
|
759
|
+
return isRecord(value) && isRecord(value.automation) && typeof value.automation.navigate === "function" && typeof value.automation.click === "function" && typeof value.automation.type === "function" && typeof value.automation.waitFor === "function" && typeof value.automation.screenshot === "function" && typeof value.automation.evaluate === "function" && typeof value.openSession === "function" && typeof value.closeSession === "function" && typeof value.handleFrame === "function" && typeof value.requestHandoff === "function" && typeof value.addHold === "function" && typeof value.removeHold === "function" && typeof value.shutdown === "function";
|
|
760
|
+
}
|
|
761
|
+
function isTerminalSurface(value) {
|
|
762
|
+
return isRecord(value) && typeof value.openSession === "function" && typeof value.closeSession === "function" && typeof value.handleFrame === "function" && typeof value.shutdown === "function";
|
|
763
|
+
}
|
|
764
|
+
function isRemoteApiClient(value) {
|
|
765
|
+
return isRecord(value) && typeof value.whoami === "function" && typeof value.requestBrowserTakeover === "function" && typeof value.completeRemoteSession === "function";
|
|
766
|
+
}
|
|
767
|
+
function requireRecord(value, label) {
|
|
768
|
+
if (!isRecord(value)) throw new Error(`${label} must be an object`);
|
|
769
|
+
return value;
|
|
770
|
+
}
|
|
771
|
+
function isRecord(value) {
|
|
772
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
773
|
+
}
|
|
774
|
+
function isBoundedString(value, maxChars) {
|
|
775
|
+
return typeof value === "string" && value.length > 0 && value.length <= maxChars && !hasControlCharacter(value);
|
|
776
|
+
}
|
|
777
|
+
function hasControlCharacter(value) {
|
|
778
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
779
|
+
const code = value.charCodeAt(index);
|
|
780
|
+
if (code < 32 || code === 127) return true;
|
|
781
|
+
}
|
|
782
|
+
return false;
|
|
783
|
+
}
|
|
784
|
+
function validatePackageVersion(value) {
|
|
785
|
+
if (typeof value !== "string" || !/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/u.test(value)) throw new Error("openclaw-remote package version is invalid");
|
|
786
|
+
return value;
|
|
787
|
+
}
|
|
788
|
+
//#endregion
|
|
789
|
+
Object.defineProperty(exports, "PLUGIN_VERSION", {
|
|
790
|
+
enumerable: true,
|
|
791
|
+
get: function() {
|
|
792
|
+
return PLUGIN_VERSION;
|
|
793
|
+
}
|
|
794
|
+
});
|
|
795
|
+
Object.defineProperty(exports, "REMOTE_ACTIVATION_KEY", {
|
|
796
|
+
enumerable: true,
|
|
797
|
+
get: function() {
|
|
798
|
+
return REMOTE_ACTIVATION_KEY;
|
|
799
|
+
}
|
|
800
|
+
});
|
|
801
|
+
Object.defineProperty(exports, "buildControlUrl", {
|
|
802
|
+
enumerable: true,
|
|
803
|
+
get: function() {
|
|
804
|
+
return buildControlUrl;
|
|
805
|
+
}
|
|
806
|
+
});
|
|
807
|
+
Object.defineProperty(exports, "buildIsNavigationAllowed", {
|
|
808
|
+
enumerable: true,
|
|
809
|
+
get: function() {
|
|
810
|
+
return buildIsNavigationAllowed;
|
|
811
|
+
}
|
|
812
|
+
});
|
|
813
|
+
Object.defineProperty(exports, "createRemotePlugin", {
|
|
814
|
+
enumerable: true,
|
|
815
|
+
get: function() {
|
|
816
|
+
return createRemotePlugin;
|
|
817
|
+
}
|
|
818
|
+
});
|
|
819
|
+
Object.defineProperty(exports, "createRemotePluginRuntimeState", {
|
|
820
|
+
enumerable: true,
|
|
821
|
+
get: function() {
|
|
822
|
+
return createRemotePluginRuntimeState;
|
|
823
|
+
}
|
|
824
|
+
});
|
|
825
|
+
Object.defineProperty(exports, "deriveDashboardBaseUrl", {
|
|
826
|
+
enumerable: true,
|
|
827
|
+
get: function() {
|
|
828
|
+
return deriveDashboardBaseUrl;
|
|
829
|
+
}
|
|
830
|
+
});
|
|
831
|
+
Object.defineProperty(exports, "deriveRemoteWsUrl", {
|
|
832
|
+
enumerable: true,
|
|
833
|
+
get: function() {
|
|
834
|
+
return deriveRemoteWsUrl;
|
|
835
|
+
}
|
|
836
|
+
});
|
|
837
|
+
Object.defineProperty(exports, "parseRemotePluginConfig", {
|
|
838
|
+
enumerable: true,
|
|
839
|
+
get: function() {
|
|
840
|
+
return parseRemotePluginConfig;
|
|
841
|
+
}
|
|
842
|
+
});
|