@alfe.ai/openclaw-remote 0.0.19 → 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/README.md +21 -0
- package/dist/index.cjs +2 -0
- package/dist/index.d.cts +36 -2
- package/dist/index.d.ts +36 -2
- package/dist/index.js +2 -2
- package/dist/runtime.cjs +119 -41
- package/dist/runtime.js +109 -43
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -40,6 +40,27 @@ Validation failures deliberately safe for the model are returned as structured
|
|
|
40
40
|
tool errors. Unexpected browser, relay, and API diagnostics are redacted.
|
|
41
41
|
Screenshots use OpenClaw image content and keep base64 out of JSON details.
|
|
42
42
|
|
|
43
|
+
## Trusted local adapters
|
|
44
|
+
|
|
45
|
+
The root library exports two helpers for plugins in the same OpenClaw process:
|
|
46
|
+
|
|
47
|
+
- `withRemoteBrowserOperation(callback, { signal? })` invokes the callback with
|
|
48
|
+
`{ browserWSEndpoint, targetId, signal }` under the existing automation queue.
|
|
49
|
+
Connect to that exact target; reuse its profile and authenticated page.
|
|
50
|
+
Disconnect the attachment and await child exit before the callback settles,
|
|
51
|
+
including when aborted. Never close the owned Chrome or expose its endpoint.
|
|
52
|
+
- `requestRemoteBrowserTakeover({ instructions, conversationId?, timeout?, signal? })`
|
|
53
|
+
uses the existing takeover session and waits for hand-back. `timeout` is in
|
|
54
|
+
milliseconds. Tell the user to open the Browser tab before invoking; provide
|
|
55
|
+
`conversationId` to deliver the chat card during the wait. Its result includes
|
|
56
|
+
`sessionId`, optional `controlUrl`, `released`, `timedOut`, `url`, and `title`.
|
|
57
|
+
|
|
58
|
+
Finish an operation before requesting takeover, then acquire a fresh operation
|
|
59
|
+
after hand-back. Nested operations/handoffs deadlock on the shared queue.
|
|
60
|
+
These helpers never launch a second runtime; an absent or outdated owner needs
|
|
61
|
+
the remote plugin started or updated/restarted. A CDP adapter is trusted code,
|
|
62
|
+
not a target-isolated sandbox. Its consuming tools must redact raw failures.
|
|
63
|
+
|
|
43
64
|
## Development
|
|
44
65
|
|
|
45
66
|
```bash
|
package/dist/index.cjs
CHANGED
|
@@ -9,3 +9,5 @@ exports.createRemotePluginRuntimeState = require_runtime.createRemotePluginRunti
|
|
|
9
9
|
exports.deriveDashboardBaseUrl = require_runtime.deriveDashboardBaseUrl;
|
|
10
10
|
exports.deriveRemoteWsUrl = require_runtime.deriveRemoteWsUrl;
|
|
11
11
|
exports.parseRemotePluginConfig = require_runtime.parseRemotePluginConfig;
|
|
12
|
+
exports.requestRemoteBrowserTakeover = require_runtime.requestRemoteBrowserTakeover;
|
|
13
|
+
exports.withRemoteBrowserOperation = require_runtime.withRemoteBrowserOperation;
|
package/dist/index.d.cts
CHANGED
|
@@ -89,6 +89,18 @@ interface Logger$2 {
|
|
|
89
89
|
//#endregion
|
|
90
90
|
//#endregion
|
|
91
91
|
//#region src/types.d.ts
|
|
92
|
+
/** Private local capability for a trusted adapter, never a tool result. */
|
|
93
|
+
interface BrowserOperationContext {
|
|
94
|
+
/** Loopback Chrome endpoint. Do not log, persist, or expose to a viewer/model. */
|
|
95
|
+
browserWSEndpoint: string;
|
|
96
|
+
/** The exact active page's CDP target; never choose the first context/page. */
|
|
97
|
+
targetId: string;
|
|
98
|
+
/** On abort, stop and await all external work before returning. */
|
|
99
|
+
signal: AbortSignal;
|
|
100
|
+
}
|
|
101
|
+
interface BrowserOperationOptions {
|
|
102
|
+
signal?: AbortSignal;
|
|
103
|
+
}
|
|
92
104
|
interface BrowserSessionOptions {
|
|
93
105
|
/** Path to the Chrome/Chromium binary (from the headless-browser integration). */
|
|
94
106
|
executablePath: string;
|
|
@@ -284,11 +296,13 @@ interface BrowserSurfaceLike {
|
|
|
284
296
|
}): Promise<void>;
|
|
285
297
|
screenshot(): Promise<string>;
|
|
286
298
|
evaluate(expression: string): Promise<unknown>;
|
|
299
|
+
/** Optional only to recognize an older process-global owner during upgrade. */
|
|
300
|
+
withCdpOperation?<T>(operation: (context: BrowserOperationContext) => Promise<T>, options?: BrowserOperationOptions): Promise<T>;
|
|
287
301
|
};
|
|
288
302
|
openSession(sessionId: number, open: SessionOpenPayload): void | Promise<void>;
|
|
289
303
|
closeSession(sessionId: number): void;
|
|
290
304
|
handleFrame(frame: RemoteFrame): void;
|
|
291
|
-
requestHandoff(timeoutMs: number): Promise<HandoffResult>;
|
|
305
|
+
requestHandoff(timeoutMs: number, signal?: AbortSignal): Promise<HandoffResult>;
|
|
292
306
|
addHold(): void;
|
|
293
307
|
removeHold(): void;
|
|
294
308
|
shutdown(): Promise<void>;
|
|
@@ -330,6 +344,26 @@ interface RemotePluginDependencies {
|
|
|
330
344
|
runtimeState?: RemotePluginRuntimeState;
|
|
331
345
|
}
|
|
332
346
|
declare function createRemotePluginRuntimeState(): RemotePluginRuntimeState;
|
|
347
|
+
interface RemoteBrowserTakeoverOptions {
|
|
348
|
+
instructions: string;
|
|
349
|
+
conversationId?: string;
|
|
350
|
+
/** Milliseconds, 1000..1800000; defaults to the remote runtime's timeout. */
|
|
351
|
+
timeout?: number;
|
|
352
|
+
signal?: AbortSignal;
|
|
353
|
+
}
|
|
354
|
+
interface RemoteBrowserTakeoverResult extends HandoffResult {
|
|
355
|
+
sessionId: string;
|
|
356
|
+
controlUrl?: string;
|
|
357
|
+
}
|
|
358
|
+
/** Trusted in-process adapter bridge, not a model-facing endpoint-discovery tool.
|
|
359
|
+
* The callback owns its CDP attachment/children and MUST await their cleanup
|
|
360
|
+
* before settling, including on signal abort. Never close Chrome, retain the
|
|
361
|
+
* endpoint, or call nested browser operations/takeover from this callback. */
|
|
362
|
+
declare function withRemoteBrowserOperation<T>(operation: (context: BrowserOperationContext) => Promise<T>, options?: BrowserOperationOptions): Promise<T>;
|
|
363
|
+
/** Create the existing takeover request and await human hand-back. Tell the
|
|
364
|
+
* user to open the agent Browser tab before invoking; the URL returns afterward.
|
|
365
|
+
* Call between operations, never while holding a withRemoteBrowserOperation. */
|
|
366
|
+
declare function requestRemoteBrowserTakeover(options: RemoteBrowserTakeoverOptions): Promise<RemoteBrowserTakeoverResult>;
|
|
333
367
|
/**
|
|
334
368
|
* Derive the relay WebSocket URL from the agent's cloud apiUrl when the
|
|
335
369
|
* manifest doesn't provide one — mirrors @alfe.ai/console-client's
|
|
@@ -397,4 +431,4 @@ declare function buildIsNavigationAllowed(policy: SsrfPolicy | undefined, log?:
|
|
|
397
431
|
warn(msg: string): void;
|
|
398
432
|
}): (url: string) => boolean;
|
|
399
433
|
//#endregion
|
|
400
|
-
export { type BrowserSurfaceLike, PLUGIN_VERSION, REMOTE_ACTIVATION_KEY, type RemoteApiClient, type RemoteClientLike, type RemotePluginConfig, type RemotePluginDependencies, type RemotePluginRuntimeState, type SsrfPolicy, type TerminalSurfaceLike, buildControlUrl, buildIsNavigationAllowed, createRemotePlugin, createRemotePluginRuntimeState, deriveDashboardBaseUrl, deriveRemoteWsUrl, parseRemotePluginConfig };
|
|
434
|
+
export { type BrowserOperationContext, type BrowserOperationOptions, type BrowserSurfaceLike, PLUGIN_VERSION, REMOTE_ACTIVATION_KEY, type RemoteApiClient, type RemoteBrowserTakeoverOptions, type RemoteBrowserTakeoverResult, type RemoteClientLike, type RemotePluginConfig, type RemotePluginDependencies, type RemotePluginRuntimeState, type SsrfPolicy, type TerminalSurfaceLike, buildControlUrl, buildIsNavigationAllowed, createRemotePlugin, createRemotePluginRuntimeState, deriveDashboardBaseUrl, deriveRemoteWsUrl, parseRemotePluginConfig, requestRemoteBrowserTakeover, withRemoteBrowserOperation };
|
package/dist/index.d.ts
CHANGED
|
@@ -89,6 +89,18 @@ interface Logger$2 {
|
|
|
89
89
|
//#endregion
|
|
90
90
|
//#endregion
|
|
91
91
|
//#region src/types.d.ts
|
|
92
|
+
/** Private local capability for a trusted adapter, never a tool result. */
|
|
93
|
+
interface BrowserOperationContext {
|
|
94
|
+
/** Loopback Chrome endpoint. Do not log, persist, or expose to a viewer/model. */
|
|
95
|
+
browserWSEndpoint: string;
|
|
96
|
+
/** The exact active page's CDP target; never choose the first context/page. */
|
|
97
|
+
targetId: string;
|
|
98
|
+
/** On abort, stop and await all external work before returning. */
|
|
99
|
+
signal: AbortSignal;
|
|
100
|
+
}
|
|
101
|
+
interface BrowserOperationOptions {
|
|
102
|
+
signal?: AbortSignal;
|
|
103
|
+
}
|
|
92
104
|
interface BrowserSessionOptions {
|
|
93
105
|
/** Path to the Chrome/Chromium binary (from the headless-browser integration). */
|
|
94
106
|
executablePath: string;
|
|
@@ -284,11 +296,13 @@ interface BrowserSurfaceLike {
|
|
|
284
296
|
}): Promise<void>;
|
|
285
297
|
screenshot(): Promise<string>;
|
|
286
298
|
evaluate(expression: string): Promise<unknown>;
|
|
299
|
+
/** Optional only to recognize an older process-global owner during upgrade. */
|
|
300
|
+
withCdpOperation?<T>(operation: (context: BrowserOperationContext) => Promise<T>, options?: BrowserOperationOptions): Promise<T>;
|
|
287
301
|
};
|
|
288
302
|
openSession(sessionId: number, open: SessionOpenPayload): void | Promise<void>;
|
|
289
303
|
closeSession(sessionId: number): void;
|
|
290
304
|
handleFrame(frame: RemoteFrame): void;
|
|
291
|
-
requestHandoff(timeoutMs: number): Promise<HandoffResult>;
|
|
305
|
+
requestHandoff(timeoutMs: number, signal?: AbortSignal): Promise<HandoffResult>;
|
|
292
306
|
addHold(): void;
|
|
293
307
|
removeHold(): void;
|
|
294
308
|
shutdown(): Promise<void>;
|
|
@@ -330,6 +344,26 @@ interface RemotePluginDependencies {
|
|
|
330
344
|
runtimeState?: RemotePluginRuntimeState;
|
|
331
345
|
}
|
|
332
346
|
declare function createRemotePluginRuntimeState(): RemotePluginRuntimeState;
|
|
347
|
+
interface RemoteBrowserTakeoverOptions {
|
|
348
|
+
instructions: string;
|
|
349
|
+
conversationId?: string;
|
|
350
|
+
/** Milliseconds, 1000..1800000; defaults to the remote runtime's timeout. */
|
|
351
|
+
timeout?: number;
|
|
352
|
+
signal?: AbortSignal;
|
|
353
|
+
}
|
|
354
|
+
interface RemoteBrowserTakeoverResult extends HandoffResult {
|
|
355
|
+
sessionId: string;
|
|
356
|
+
controlUrl?: string;
|
|
357
|
+
}
|
|
358
|
+
/** Trusted in-process adapter bridge, not a model-facing endpoint-discovery tool.
|
|
359
|
+
* The callback owns its CDP attachment/children and MUST await their cleanup
|
|
360
|
+
* before settling, including on signal abort. Never close Chrome, retain the
|
|
361
|
+
* endpoint, or call nested browser operations/takeover from this callback. */
|
|
362
|
+
declare function withRemoteBrowserOperation<T>(operation: (context: BrowserOperationContext) => Promise<T>, options?: BrowserOperationOptions): Promise<T>;
|
|
363
|
+
/** Create the existing takeover request and await human hand-back. Tell the
|
|
364
|
+
* user to open the agent Browser tab before invoking; the URL returns afterward.
|
|
365
|
+
* Call between operations, never while holding a withRemoteBrowserOperation. */
|
|
366
|
+
declare function requestRemoteBrowserTakeover(options: RemoteBrowserTakeoverOptions): Promise<RemoteBrowserTakeoverResult>;
|
|
333
367
|
/**
|
|
334
368
|
* Derive the relay WebSocket URL from the agent's cloud apiUrl when the
|
|
335
369
|
* manifest doesn't provide one — mirrors @alfe.ai/console-client's
|
|
@@ -397,4 +431,4 @@ declare function buildIsNavigationAllowed(policy: SsrfPolicy | undefined, log?:
|
|
|
397
431
|
warn(msg: string): void;
|
|
398
432
|
}): (url: string) => boolean;
|
|
399
433
|
//#endregion
|
|
400
|
-
export { type BrowserSurfaceLike, PLUGIN_VERSION, REMOTE_ACTIVATION_KEY, type RemoteApiClient, type RemoteClientLike, type RemotePluginConfig, type RemotePluginDependencies, type RemotePluginRuntimeState, type SsrfPolicy, type TerminalSurfaceLike, buildControlUrl, buildIsNavigationAllowed, createRemotePlugin, createRemotePluginRuntimeState, deriveDashboardBaseUrl, deriveRemoteWsUrl, parseRemotePluginConfig };
|
|
434
|
+
export { type BrowserOperationContext, type BrowserOperationOptions, type BrowserSurfaceLike, PLUGIN_VERSION, REMOTE_ACTIVATION_KEY, type RemoteApiClient, type RemoteBrowserTakeoverOptions, type RemoteBrowserTakeoverResult, type RemoteClientLike, type RemotePluginConfig, type RemotePluginDependencies, type RemotePluginRuntimeState, type SsrfPolicy, type TerminalSurfaceLike, buildControlUrl, buildIsNavigationAllowed, createRemotePlugin, createRemotePluginRuntimeState, deriveDashboardBaseUrl, deriveRemoteWsUrl, parseRemotePluginConfig, requestRemoteBrowserTakeover, withRemoteBrowserOperation };
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as createRemotePluginRuntimeState, c as parseRemotePluginConfig, i as createRemotePlugin, l as
|
|
2
|
-
export { PLUGIN_VERSION, REMOTE_ACTIVATION_KEY, buildControlUrl, buildIsNavigationAllowed, createRemotePlugin, createRemotePluginRuntimeState, deriveDashboardBaseUrl, deriveRemoteWsUrl, parseRemotePluginConfig };
|
|
1
|
+
import { a as createRemotePluginRuntimeState, c as parseRemotePluginConfig, d as buildIsNavigationAllowed, i as createRemotePlugin, l as requestRemoteBrowserTakeover, n as REMOTE_ACTIVATION_KEY, o as deriveDashboardBaseUrl, r as buildControlUrl, s as deriveRemoteWsUrl, t as PLUGIN_VERSION, u as withRemoteBrowserOperation } from "./runtime.js";
|
|
2
|
+
export { PLUGIN_VERSION, REMOTE_ACTIVATION_KEY, buildControlUrl, buildIsNavigationAllowed, createRemotePlugin, createRemotePluginRuntimeState, deriveDashboardBaseUrl, deriveRemoteWsUrl, parseRemotePluginConfig, requestRemoteBrowserTakeover, withRemoteBrowserOperation };
|
package/dist/runtime.cjs
CHANGED
|
@@ -148,6 +148,70 @@ function createRemotePluginRuntimeState() {
|
|
|
148
148
|
stopPromise: null
|
|
149
149
|
};
|
|
150
150
|
}
|
|
151
|
+
/** Trusted in-process adapter bridge, not a model-facing endpoint-discovery tool.
|
|
152
|
+
* The callback owns its CDP attachment/children and MUST await their cleanup
|
|
153
|
+
* before settling, including on signal abort. Never close Chrome, retain the
|
|
154
|
+
* endpoint, or call nested browser operations/takeover from this callback. */
|
|
155
|
+
async function withRemoteBrowserOperation(operation, options = {}) {
|
|
156
|
+
const state = requireGlobalRuntimeState();
|
|
157
|
+
const surface = requireBrowserSurface(state);
|
|
158
|
+
const generation = state.generation;
|
|
159
|
+
if (!surface.automation.withCdpOperation) throw (0, _alfe_ai_openclaw_plugin_kit.publicToolError)("Shared browser adapter support is unavailable; update and restart the remote plugin.");
|
|
160
|
+
const assertCurrent = () => {
|
|
161
|
+
if (state.generation !== generation || state.browserSurface !== surface || state.stopPromise !== null) throw (0, _alfe_ai_openclaw_plugin_kit.publicToolError)("Remote browser restarted during the operation; retry after it reconnects.");
|
|
162
|
+
};
|
|
163
|
+
const result = await surface.automation.withCdpOperation(async (context) => {
|
|
164
|
+
assertCurrent();
|
|
165
|
+
const value = await operation(context);
|
|
166
|
+
assertCurrent();
|
|
167
|
+
return value;
|
|
168
|
+
}, options);
|
|
169
|
+
assertCurrent();
|
|
170
|
+
return result;
|
|
171
|
+
}
|
|
172
|
+
/** Create the existing takeover request and await human hand-back. Tell the
|
|
173
|
+
* user to open the agent Browser tab before invoking; the URL returns afterward.
|
|
174
|
+
* Call between operations, never while holding a withRemoteBrowserOperation. */
|
|
175
|
+
async function requestRemoteBrowserTakeover(options) {
|
|
176
|
+
return requestBrowserTakeover(requireGlobalRuntimeState(), options);
|
|
177
|
+
}
|
|
178
|
+
function requireBrowserSurface(state) {
|
|
179
|
+
if (!state.browserSurface || state.stopPromise !== null) throw (0, _alfe_ai_openclaw_plugin_kit.publicToolError)("Browser surface is unavailable; retry after the remote service connects.");
|
|
180
|
+
return state.browserSurface;
|
|
181
|
+
}
|
|
182
|
+
async function requestBrowserTakeover(state, options) {
|
|
183
|
+
const surface = requireBrowserSurface(state);
|
|
184
|
+
const client = state.apiClient;
|
|
185
|
+
if (!client) throw (0, _alfe_ai_openclaw_plugin_kit.publicToolError)("Remote service is unavailable; retry after it connects.");
|
|
186
|
+
if (options.signal && !surface.automation.withCdpOperation) throw (0, _alfe_ai_openclaw_plugin_kit.publicToolError)("Cancellable browser takeover is unavailable; update and restart the remote plugin.");
|
|
187
|
+
options.signal?.throwIfAborted();
|
|
188
|
+
const generation = state.generation;
|
|
189
|
+
const instructions = requireString(options.instructions, "instructions", MAX_INSTRUCTIONS_CHARS);
|
|
190
|
+
const conversationId = optionalString(options.conversationId, "conversationId", MAX_CONVERSATION_ID_CHARS);
|
|
191
|
+
const url = options.url === void 0 ? void 0 : requireHttpUrl(options.url, "url", 2048);
|
|
192
|
+
const timeout = optionalInteger(options.timeout, "timeout", 1e3, 1800 * 1e3) ?? state.handoffTimeoutMs;
|
|
193
|
+
let sessionId;
|
|
194
|
+
surface.addHold();
|
|
195
|
+
try {
|
|
196
|
+
sessionId = requireTrustedString((await client.requestBrowserTakeover({
|
|
197
|
+
instructions,
|
|
198
|
+
url,
|
|
199
|
+
conversationId
|
|
200
|
+
})).sessionId, "remote session ID", MAX_SESSION_ID_CHARS);
|
|
201
|
+
options.signal?.throwIfAborted();
|
|
202
|
+
if (state.generation !== generation || state.browserSurface !== surface || state.apiClient !== client || state.stopPromise !== null) throw (0, _alfe_ai_openclaw_plugin_kit.publicToolError)("Remote browser restarted while creating the takeover; retry the request.");
|
|
203
|
+
const controlUrl = buildControlUrl(state.dashboardBaseUrl, state.selfAgentId, sessionId);
|
|
204
|
+
const result = options.signal ? await surface.requestHandoff(timeout, options.signal) : await surface.requestHandoff(timeout);
|
|
205
|
+
return {
|
|
206
|
+
sessionId,
|
|
207
|
+
controlUrl,
|
|
208
|
+
...result
|
|
209
|
+
};
|
|
210
|
+
} finally {
|
|
211
|
+
surface.removeHold();
|
|
212
|
+
if (sessionId) await client.completeRemoteSession(sessionId).catch(() => void 0);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
151
215
|
/**
|
|
152
216
|
* Derive the relay WebSocket URL from the agent's cloud apiUrl when the
|
|
153
217
|
* manifest doesn't provide one — mirrors @alfe.ai/console-client's
|
|
@@ -208,16 +272,28 @@ function dispatchFrame(state, frame, log) {
|
|
|
208
272
|
const handler = open.surface === "terminal" ? state.terminalSurface : state.browserSurface;
|
|
209
273
|
if (!handler) {
|
|
210
274
|
log.warn("Remote session surface is unavailable");
|
|
275
|
+
closeUnavailableSession(state.remoteClient, frame.sessionId, log);
|
|
211
276
|
return;
|
|
212
277
|
}
|
|
278
|
+
const client = state.remoteClient;
|
|
279
|
+
const generation = state.generation;
|
|
280
|
+
const sessions = state.sessionSurfaces;
|
|
213
281
|
state.sessionSurfaces.set(frame.sessionId, open.surface);
|
|
214
|
-
|
|
215
|
-
if (state.sessionSurfaces.get(frame.sessionId) === open.surface)
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
282
|
+
const failOpen = () => {
|
|
283
|
+
if (state.generation === generation && state.remoteClient === client && state.sessionSurfaces === sessions && state.sessionSurfaces.get(frame.sessionId) === open.surface) {
|
|
284
|
+
state.sessionSurfaces.delete(frame.sessionId);
|
|
285
|
+
try {
|
|
286
|
+
handler.closeSession(frame.sessionId);
|
|
287
|
+
} catch {}
|
|
288
|
+
closeUnavailableSession(client, frame.sessionId, log);
|
|
289
|
+
}
|
|
219
290
|
log.warn("Remote session could not be opened");
|
|
220
|
-
}
|
|
291
|
+
};
|
|
292
|
+
try {
|
|
293
|
+
Promise.resolve(handler.openSession(frame.sessionId, open)).catch(failOpen);
|
|
294
|
+
} catch {
|
|
295
|
+
failOpen();
|
|
296
|
+
}
|
|
221
297
|
return;
|
|
222
298
|
}
|
|
223
299
|
const surface = state.sessionSurfaces.get(frame.sessionId);
|
|
@@ -238,6 +314,13 @@ function dispatchFrame(state, frame, log) {
|
|
|
238
314
|
log.warn("Remote session frame handling failed");
|
|
239
315
|
}
|
|
240
316
|
}
|
|
317
|
+
function closeUnavailableSession(client, sessionId, log) {
|
|
318
|
+
try {
|
|
319
|
+
client?.sendFrame((0, _alfe_ai_remote.encodeJsonFrame)(_alfe_ai_remote.RemoteFrameType.SESSION_CLOSE, sessionId, { reason: "surface_unavailable" }));
|
|
320
|
+
} catch {
|
|
321
|
+
log.warn("Could not close unavailable remote session");
|
|
322
|
+
}
|
|
323
|
+
}
|
|
241
324
|
function startService(pluginConfig, ssrfPolicy, workspaceDir, log, dependencies, state) {
|
|
242
325
|
(0, _alfe_ai_openclaw_plugin_kit.guardedStart)(REMOTE_ACTIVATION_KEY, log, () => {
|
|
243
326
|
if (pluginConfig === null) {
|
|
@@ -377,12 +460,10 @@ function closeAllSessions(state, log) {
|
|
|
377
460
|
}
|
|
378
461
|
}
|
|
379
462
|
state.sessionSurfaces.clear();
|
|
463
|
+
state.sessionSurfaces = /* @__PURE__ */ new Map();
|
|
380
464
|
}
|
|
381
465
|
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
|
-
};
|
|
466
|
+
const needBrowser = () => requireBrowserSurface(state);
|
|
386
467
|
api.registerTool(remoteTool({
|
|
387
468
|
name: "browser_navigate",
|
|
388
469
|
description: "Navigate the shared browser to a URL. Returns the final URL and page title.",
|
|
@@ -527,7 +608,7 @@ function registerTools(api, state) {
|
|
|
527
608
|
}));
|
|
528
609
|
api.registerTool(remoteTool({
|
|
529
610
|
name: "request_browser_takeover",
|
|
530
|
-
description: "Ask a human to take over the live browser for a manual step.
|
|
611
|
+
description: "Ask a human to take over the live shared browser for a manual step. Tell the user to open this agent's Browser tab before calling: this tool waits until hand-back or timeout, so its returned URL arrives afterward. Supply the current conversationId when available to show the takeover card in chat. The optional URL is context only; navigate the browser before requesting takeover.",
|
|
531
612
|
parameters: {
|
|
532
613
|
type: "object",
|
|
533
614
|
properties: {
|
|
@@ -553,36 +634,16 @@ function registerTools(api, state) {
|
|
|
553
634
|
additionalProperties: false
|
|
554
635
|
},
|
|
555
636
|
handler: async (params) => {
|
|
556
|
-
const
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
const
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
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
|
-
}
|
|
637
|
+
const result = await requestBrowserTakeover(state, {
|
|
638
|
+
instructions: requireString(params.instructions, "instructions", MAX_INSTRUCTIONS_CHARS),
|
|
639
|
+
url: params.url === void 0 ? void 0 : requireHttpUrl(params.url, "url", 2048),
|
|
640
|
+
conversationId: optionalString(params.conversationId, "conversationId", MAX_CONVERSATION_ID_CHARS)
|
|
641
|
+
});
|
|
642
|
+
const message = result.timedOut ? "Browser takeover timed out. Request a new takeover if the user still needs to complete the task." : "Browser control returned to the agent. Continue on the existing page and check whether the user completed the task.";
|
|
643
|
+
return {
|
|
644
|
+
...result,
|
|
645
|
+
message
|
|
646
|
+
};
|
|
586
647
|
}
|
|
587
648
|
}));
|
|
588
649
|
}
|
|
@@ -748,6 +809,11 @@ function getGlobalRuntimeState() {
|
|
|
748
809
|
root[RUNTIME_STATE_KEY] = state;
|
|
749
810
|
return state;
|
|
750
811
|
}
|
|
812
|
+
function requireGlobalRuntimeState() {
|
|
813
|
+
const existing = globalThis[RUNTIME_STATE_KEY];
|
|
814
|
+
if (!isRuntimeState(existing)) throw (0, _alfe_ai_openclaw_plugin_kit.publicToolError)("Shared browser runtime is unavailable; start or update and restart the remote plugin.");
|
|
815
|
+
return existing;
|
|
816
|
+
}
|
|
751
817
|
function isRuntimeState(value) {
|
|
752
818
|
if (!isRecord(value)) return false;
|
|
753
819
|
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));
|
|
@@ -840,3 +906,15 @@ Object.defineProperty(exports, "parseRemotePluginConfig", {
|
|
|
840
906
|
return parseRemotePluginConfig;
|
|
841
907
|
}
|
|
842
908
|
});
|
|
909
|
+
Object.defineProperty(exports, "requestRemoteBrowserTakeover", {
|
|
910
|
+
enumerable: true,
|
|
911
|
+
get: function() {
|
|
912
|
+
return requestRemoteBrowserTakeover;
|
|
913
|
+
}
|
|
914
|
+
});
|
|
915
|
+
Object.defineProperty(exports, "withRemoteBrowserOperation", {
|
|
916
|
+
enumerable: true,
|
|
917
|
+
get: function() {
|
|
918
|
+
return withRemoteBrowserOperation;
|
|
919
|
+
}
|
|
920
|
+
});
|
package/dist/runtime.js
CHANGED
|
@@ -4,7 +4,7 @@ import { AgentApiClient, installToolErrorCapture } from "@alfe.ai/agent-api-clie
|
|
|
4
4
|
import { BrowserSurface } from "@alfe.ai/browser";
|
|
5
5
|
import { resolveConfig } from "@alfe.ai/config";
|
|
6
6
|
import { PublicToolError, defineTool, errResult, getActivationKey, guardedStart, publicToolError, resetActivation } from "@alfe.ai/openclaw-plugin-kit";
|
|
7
|
-
import { RemoteFrameType, RemoteServiceClient, decodeSessionOpenPayload } from "@alfe.ai/remote";
|
|
7
|
+
import { RemoteFrameType, RemoteServiceClient, decodeSessionOpenPayload, encodeJsonFrame } from "@alfe.ai/remote";
|
|
8
8
|
import { TerminalSurface } from "@alfe.ai/terminal";
|
|
9
9
|
//#region src/ssrf.ts
|
|
10
10
|
/** Hostnames blocked outright (case-insensitive, exact match). */
|
|
@@ -148,6 +148,70 @@ function createRemotePluginRuntimeState() {
|
|
|
148
148
|
stopPromise: null
|
|
149
149
|
};
|
|
150
150
|
}
|
|
151
|
+
/** Trusted in-process adapter bridge, not a model-facing endpoint-discovery tool.
|
|
152
|
+
* The callback owns its CDP attachment/children and MUST await their cleanup
|
|
153
|
+
* before settling, including on signal abort. Never close Chrome, retain the
|
|
154
|
+
* endpoint, or call nested browser operations/takeover from this callback. */
|
|
155
|
+
async function withRemoteBrowserOperation(operation, options = {}) {
|
|
156
|
+
const state = requireGlobalRuntimeState();
|
|
157
|
+
const surface = requireBrowserSurface(state);
|
|
158
|
+
const generation = state.generation;
|
|
159
|
+
if (!surface.automation.withCdpOperation) throw publicToolError("Shared browser adapter support is unavailable; update and restart the remote plugin.");
|
|
160
|
+
const assertCurrent = () => {
|
|
161
|
+
if (state.generation !== generation || state.browserSurface !== surface || state.stopPromise !== null) throw publicToolError("Remote browser restarted during the operation; retry after it reconnects.");
|
|
162
|
+
};
|
|
163
|
+
const result = await surface.automation.withCdpOperation(async (context) => {
|
|
164
|
+
assertCurrent();
|
|
165
|
+
const value = await operation(context);
|
|
166
|
+
assertCurrent();
|
|
167
|
+
return value;
|
|
168
|
+
}, options);
|
|
169
|
+
assertCurrent();
|
|
170
|
+
return result;
|
|
171
|
+
}
|
|
172
|
+
/** Create the existing takeover request and await human hand-back. Tell the
|
|
173
|
+
* user to open the agent Browser tab before invoking; the URL returns afterward.
|
|
174
|
+
* Call between operations, never while holding a withRemoteBrowserOperation. */
|
|
175
|
+
async function requestRemoteBrowserTakeover(options) {
|
|
176
|
+
return requestBrowserTakeover(requireGlobalRuntimeState(), options);
|
|
177
|
+
}
|
|
178
|
+
function requireBrowserSurface(state) {
|
|
179
|
+
if (!state.browserSurface || state.stopPromise !== null) throw publicToolError("Browser surface is unavailable; retry after the remote service connects.");
|
|
180
|
+
return state.browserSurface;
|
|
181
|
+
}
|
|
182
|
+
async function requestBrowserTakeover(state, options) {
|
|
183
|
+
const surface = requireBrowserSurface(state);
|
|
184
|
+
const client = state.apiClient;
|
|
185
|
+
if (!client) throw publicToolError("Remote service is unavailable; retry after it connects.");
|
|
186
|
+
if (options.signal && !surface.automation.withCdpOperation) throw publicToolError("Cancellable browser takeover is unavailable; update and restart the remote plugin.");
|
|
187
|
+
options.signal?.throwIfAborted();
|
|
188
|
+
const generation = state.generation;
|
|
189
|
+
const instructions = requireString(options.instructions, "instructions", MAX_INSTRUCTIONS_CHARS);
|
|
190
|
+
const conversationId = optionalString(options.conversationId, "conversationId", MAX_CONVERSATION_ID_CHARS);
|
|
191
|
+
const url = options.url === void 0 ? void 0 : requireHttpUrl(options.url, "url", 2048);
|
|
192
|
+
const timeout = optionalInteger(options.timeout, "timeout", 1e3, 1800 * 1e3) ?? state.handoffTimeoutMs;
|
|
193
|
+
let sessionId;
|
|
194
|
+
surface.addHold();
|
|
195
|
+
try {
|
|
196
|
+
sessionId = requireTrustedString((await client.requestBrowserTakeover({
|
|
197
|
+
instructions,
|
|
198
|
+
url,
|
|
199
|
+
conversationId
|
|
200
|
+
})).sessionId, "remote session ID", MAX_SESSION_ID_CHARS);
|
|
201
|
+
options.signal?.throwIfAborted();
|
|
202
|
+
if (state.generation !== generation || state.browserSurface !== surface || state.apiClient !== client || state.stopPromise !== null) throw publicToolError("Remote browser restarted while creating the takeover; retry the request.");
|
|
203
|
+
const controlUrl = buildControlUrl(state.dashboardBaseUrl, state.selfAgentId, sessionId);
|
|
204
|
+
const result = options.signal ? await surface.requestHandoff(timeout, options.signal) : await surface.requestHandoff(timeout);
|
|
205
|
+
return {
|
|
206
|
+
sessionId,
|
|
207
|
+
controlUrl,
|
|
208
|
+
...result
|
|
209
|
+
};
|
|
210
|
+
} finally {
|
|
211
|
+
surface.removeHold();
|
|
212
|
+
if (sessionId) await client.completeRemoteSession(sessionId).catch(() => void 0);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
151
215
|
/**
|
|
152
216
|
* Derive the relay WebSocket URL from the agent's cloud apiUrl when the
|
|
153
217
|
* manifest doesn't provide one — mirrors @alfe.ai/console-client's
|
|
@@ -208,16 +272,28 @@ function dispatchFrame(state, frame, log) {
|
|
|
208
272
|
const handler = open.surface === "terminal" ? state.terminalSurface : state.browserSurface;
|
|
209
273
|
if (!handler) {
|
|
210
274
|
log.warn("Remote session surface is unavailable");
|
|
275
|
+
closeUnavailableSession(state.remoteClient, frame.sessionId, log);
|
|
211
276
|
return;
|
|
212
277
|
}
|
|
278
|
+
const client = state.remoteClient;
|
|
279
|
+
const generation = state.generation;
|
|
280
|
+
const sessions = state.sessionSurfaces;
|
|
213
281
|
state.sessionSurfaces.set(frame.sessionId, open.surface);
|
|
214
|
-
|
|
215
|
-
if (state.sessionSurfaces.get(frame.sessionId) === open.surface)
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
282
|
+
const failOpen = () => {
|
|
283
|
+
if (state.generation === generation && state.remoteClient === client && state.sessionSurfaces === sessions && state.sessionSurfaces.get(frame.sessionId) === open.surface) {
|
|
284
|
+
state.sessionSurfaces.delete(frame.sessionId);
|
|
285
|
+
try {
|
|
286
|
+
handler.closeSession(frame.sessionId);
|
|
287
|
+
} catch {}
|
|
288
|
+
closeUnavailableSession(client, frame.sessionId, log);
|
|
289
|
+
}
|
|
219
290
|
log.warn("Remote session could not be opened");
|
|
220
|
-
}
|
|
291
|
+
};
|
|
292
|
+
try {
|
|
293
|
+
Promise.resolve(handler.openSession(frame.sessionId, open)).catch(failOpen);
|
|
294
|
+
} catch {
|
|
295
|
+
failOpen();
|
|
296
|
+
}
|
|
221
297
|
return;
|
|
222
298
|
}
|
|
223
299
|
const surface = state.sessionSurfaces.get(frame.sessionId);
|
|
@@ -238,6 +314,13 @@ function dispatchFrame(state, frame, log) {
|
|
|
238
314
|
log.warn("Remote session frame handling failed");
|
|
239
315
|
}
|
|
240
316
|
}
|
|
317
|
+
function closeUnavailableSession(client, sessionId, log) {
|
|
318
|
+
try {
|
|
319
|
+
client?.sendFrame(encodeJsonFrame(RemoteFrameType.SESSION_CLOSE, sessionId, { reason: "surface_unavailable" }));
|
|
320
|
+
} catch {
|
|
321
|
+
log.warn("Could not close unavailable remote session");
|
|
322
|
+
}
|
|
323
|
+
}
|
|
241
324
|
function startService(pluginConfig, ssrfPolicy, workspaceDir, log, dependencies, state) {
|
|
242
325
|
guardedStart(REMOTE_ACTIVATION_KEY, log, () => {
|
|
243
326
|
if (pluginConfig === null) {
|
|
@@ -377,12 +460,10 @@ function closeAllSessions(state, log) {
|
|
|
377
460
|
}
|
|
378
461
|
}
|
|
379
462
|
state.sessionSurfaces.clear();
|
|
463
|
+
state.sessionSurfaces = /* @__PURE__ */ new Map();
|
|
380
464
|
}
|
|
381
465
|
function registerTools(api, state) {
|
|
382
|
-
const needBrowser = () =>
|
|
383
|
-
if (!state.browserSurface) throw publicToolError("Browser surface is unavailable; retry after the remote service connects.");
|
|
384
|
-
return state.browserSurface;
|
|
385
|
-
};
|
|
466
|
+
const needBrowser = () => requireBrowserSurface(state);
|
|
386
467
|
api.registerTool(remoteTool({
|
|
387
468
|
name: "browser_navigate",
|
|
388
469
|
description: "Navigate the shared browser to a URL. Returns the final URL and page title.",
|
|
@@ -527,7 +608,7 @@ function registerTools(api, state) {
|
|
|
527
608
|
}));
|
|
528
609
|
api.registerTool(remoteTool({
|
|
529
610
|
name: "request_browser_takeover",
|
|
530
|
-
description: "Ask a human to take over the live browser for a manual step.
|
|
611
|
+
description: "Ask a human to take over the live shared browser for a manual step. Tell the user to open this agent's Browser tab before calling: this tool waits until hand-back or timeout, so its returned URL arrives afterward. Supply the current conversationId when available to show the takeover card in chat. The optional URL is context only; navigate the browser before requesting takeover.",
|
|
531
612
|
parameters: {
|
|
532
613
|
type: "object",
|
|
533
614
|
properties: {
|
|
@@ -553,36 +634,16 @@ function registerTools(api, state) {
|
|
|
553
634
|
additionalProperties: false
|
|
554
635
|
},
|
|
555
636
|
handler: async (params) => {
|
|
556
|
-
const
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
const
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
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 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
|
-
}
|
|
637
|
+
const result = await requestBrowserTakeover(state, {
|
|
638
|
+
instructions: requireString(params.instructions, "instructions", MAX_INSTRUCTIONS_CHARS),
|
|
639
|
+
url: params.url === void 0 ? void 0 : requireHttpUrl(params.url, "url", 2048),
|
|
640
|
+
conversationId: optionalString(params.conversationId, "conversationId", MAX_CONVERSATION_ID_CHARS)
|
|
641
|
+
});
|
|
642
|
+
const message = result.timedOut ? "Browser takeover timed out. Request a new takeover if the user still needs to complete the task." : "Browser control returned to the agent. Continue on the existing page and check whether the user completed the task.";
|
|
643
|
+
return {
|
|
644
|
+
...result,
|
|
645
|
+
message
|
|
646
|
+
};
|
|
586
647
|
}
|
|
587
648
|
}));
|
|
588
649
|
}
|
|
@@ -748,6 +809,11 @@ function getGlobalRuntimeState() {
|
|
|
748
809
|
root[RUNTIME_STATE_KEY] = state;
|
|
749
810
|
return state;
|
|
750
811
|
}
|
|
812
|
+
function requireGlobalRuntimeState() {
|
|
813
|
+
const existing = globalThis[RUNTIME_STATE_KEY];
|
|
814
|
+
if (!isRuntimeState(existing)) throw publicToolError("Shared browser runtime is unavailable; start or update and restart the remote plugin.");
|
|
815
|
+
return existing;
|
|
816
|
+
}
|
|
751
817
|
function isRuntimeState(value) {
|
|
752
818
|
if (!isRecord(value)) return false;
|
|
753
819
|
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));
|
|
@@ -786,4 +852,4 @@ function validatePackageVersion(value) {
|
|
|
786
852
|
return value;
|
|
787
853
|
}
|
|
788
854
|
//#endregion
|
|
789
|
-
export { createRemotePluginRuntimeState as a, parseRemotePluginConfig as c, createRemotePlugin as i,
|
|
855
|
+
export { createRemotePluginRuntimeState as a, parseRemotePluginConfig as c, buildIsNavigationAllowed as d, createRemotePlugin as i, requestRemoteBrowserTakeover as l, REMOTE_ACTIVATION_KEY as n, deriveDashboardBaseUrl as o, buildControlUrl as r, deriveRemoteWsUrl as s, PLUGIN_VERSION as t, withRemoteBrowserOperation as u };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alfe.ai/openclaw-remote",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"description": "OpenClaw plugin for Alfe's interactive remote-control relay — browser co-browse + web terminal surfaces over one outbound WS",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/plugin.js",
|
|
@@ -28,12 +28,12 @@
|
|
|
28
28
|
"README.md"
|
|
29
29
|
],
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@alfe.ai/agent-api-client": "^0.
|
|
31
|
+
"@alfe.ai/agent-api-client": "^0.19.0",
|
|
32
32
|
"@alfe.ai/openclaw-plugin-kit": "0.2.0",
|
|
33
|
-
"@alfe.ai/browser": "^0.
|
|
33
|
+
"@alfe.ai/browser": "^0.3.0",
|
|
34
34
|
"@alfe.ai/config": "0.4.1",
|
|
35
|
-
"@alfe.ai/remote": "^0.
|
|
36
|
-
"@alfe.ai/terminal": "^0.1.
|
|
35
|
+
"@alfe.ai/remote": "^0.2.0",
|
|
36
|
+
"@alfe.ai/terminal": "^0.1.4"
|
|
37
37
|
},
|
|
38
38
|
"peerDependencies": {
|
|
39
39
|
"openclaw": ">=2026.3.0"
|