@alfe.ai/openclaw-remote 0.0.15 → 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 -8
- package/dist/plugin2.cjs +0 -485
- package/dist/plugin2.d.cts +0 -2
- package/dist/plugin2.d.ts +0 -2
- package/dist/plugin2.js +0 -462
package/dist/types.d.cts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
//#region src/types.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Types for the Alfe remote plugin.
|
|
4
|
+
*/
|
|
5
|
+
interface Logger {
|
|
6
|
+
info(msg: string, ...args: unknown[]): void;
|
|
7
|
+
warn(msg: string, ...args: unknown[]): void;
|
|
8
|
+
error(msg: string, ...args: unknown[]): void;
|
|
9
|
+
debug(msg: string, ...args: unknown[]): void;
|
|
10
|
+
}
|
|
11
|
+
interface RemotePluginConfig {
|
|
12
|
+
/** Relay WebSocket URL (e.g. wss://remote.dev.alfe.ai/ws). */
|
|
13
|
+
remoteWsUrl?: string;
|
|
14
|
+
/** Chrome binary path (from the headless-browser integration). */
|
|
15
|
+
browserExecutablePath?: string;
|
|
16
|
+
browserHeadless?: boolean;
|
|
17
|
+
browserNoSandbox?: boolean;
|
|
18
|
+
/** Default handoff timeout in ms (agent parks this long awaiting the human). */
|
|
19
|
+
handoffTimeoutMs?: number;
|
|
20
|
+
}
|
|
21
|
+
interface ToolDef {
|
|
22
|
+
name: string;
|
|
23
|
+
label: string;
|
|
24
|
+
description: string;
|
|
25
|
+
parameters: Record<string, unknown>;
|
|
26
|
+
execute: (toolCallId: string, params: Record<string, unknown>) => Promise<unknown>;
|
|
27
|
+
}
|
|
28
|
+
interface OpenClawConfig {
|
|
29
|
+
plugins?: {
|
|
30
|
+
entries?: Record<string, {
|
|
31
|
+
config?: RemotePluginConfig;
|
|
32
|
+
[key: string]: unknown;
|
|
33
|
+
}>;
|
|
34
|
+
[key: string]: unknown;
|
|
35
|
+
};
|
|
36
|
+
browser?: {
|
|
37
|
+
executablePath?: string;
|
|
38
|
+
headless?: boolean;
|
|
39
|
+
noSandbox?: boolean;
|
|
40
|
+
/** SSRF policy the `alfe` integration writes; consumed by the nav guard. */
|
|
41
|
+
ssrfPolicy?: {
|
|
42
|
+
dangerouslyAllowPrivateNetwork?: boolean;
|
|
43
|
+
};
|
|
44
|
+
[key: string]: unknown;
|
|
45
|
+
};
|
|
46
|
+
[key: string]: unknown;
|
|
47
|
+
}
|
|
48
|
+
interface PluginServiceContext {
|
|
49
|
+
config: Record<string, unknown>;
|
|
50
|
+
workspaceDir?: string;
|
|
51
|
+
stateDir: string;
|
|
52
|
+
logger: Logger;
|
|
53
|
+
}
|
|
54
|
+
interface OpenClawPluginApi {
|
|
55
|
+
logger: Logger;
|
|
56
|
+
registrationMode?: "full" | "setup-only" | "setup-runtime" | "cli-metadata";
|
|
57
|
+
config?: OpenClawConfig;
|
|
58
|
+
registerTool(tool: ToolDef): void;
|
|
59
|
+
registerService(service: {
|
|
60
|
+
id: string;
|
|
61
|
+
start: (ctx: PluginServiceContext) => void | Promise<void>;
|
|
62
|
+
stop?: (ctx: PluginServiceContext) => void | Promise<void>;
|
|
63
|
+
}): void;
|
|
64
|
+
}
|
|
65
|
+
//#endregion
|
|
66
|
+
export { RemotePluginConfig as n, OpenClawPluginApi as t };
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
//#region src/types.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Types for the Alfe remote plugin.
|
|
4
|
+
*/
|
|
5
|
+
interface Logger {
|
|
6
|
+
info(msg: string, ...args: unknown[]): void;
|
|
7
|
+
warn(msg: string, ...args: unknown[]): void;
|
|
8
|
+
error(msg: string, ...args: unknown[]): void;
|
|
9
|
+
debug(msg: string, ...args: unknown[]): void;
|
|
10
|
+
}
|
|
11
|
+
interface RemotePluginConfig {
|
|
12
|
+
/** Relay WebSocket URL (e.g. wss://remote.dev.alfe.ai/ws). */
|
|
13
|
+
remoteWsUrl?: string;
|
|
14
|
+
/** Chrome binary path (from the headless-browser integration). */
|
|
15
|
+
browserExecutablePath?: string;
|
|
16
|
+
browserHeadless?: boolean;
|
|
17
|
+
browserNoSandbox?: boolean;
|
|
18
|
+
/** Default handoff timeout in ms (agent parks this long awaiting the human). */
|
|
19
|
+
handoffTimeoutMs?: number;
|
|
20
|
+
}
|
|
21
|
+
interface ToolDef {
|
|
22
|
+
name: string;
|
|
23
|
+
label: string;
|
|
24
|
+
description: string;
|
|
25
|
+
parameters: Record<string, unknown>;
|
|
26
|
+
execute: (toolCallId: string, params: Record<string, unknown>) => Promise<unknown>;
|
|
27
|
+
}
|
|
28
|
+
interface OpenClawConfig {
|
|
29
|
+
plugins?: {
|
|
30
|
+
entries?: Record<string, {
|
|
31
|
+
config?: RemotePluginConfig;
|
|
32
|
+
[key: string]: unknown;
|
|
33
|
+
}>;
|
|
34
|
+
[key: string]: unknown;
|
|
35
|
+
};
|
|
36
|
+
browser?: {
|
|
37
|
+
executablePath?: string;
|
|
38
|
+
headless?: boolean;
|
|
39
|
+
noSandbox?: boolean;
|
|
40
|
+
/** SSRF policy the `alfe` integration writes; consumed by the nav guard. */
|
|
41
|
+
ssrfPolicy?: {
|
|
42
|
+
dangerouslyAllowPrivateNetwork?: boolean;
|
|
43
|
+
};
|
|
44
|
+
[key: string]: unknown;
|
|
45
|
+
};
|
|
46
|
+
[key: string]: unknown;
|
|
47
|
+
}
|
|
48
|
+
interface PluginServiceContext {
|
|
49
|
+
config: Record<string, unknown>;
|
|
50
|
+
workspaceDir?: string;
|
|
51
|
+
stateDir: string;
|
|
52
|
+
logger: Logger;
|
|
53
|
+
}
|
|
54
|
+
interface OpenClawPluginApi {
|
|
55
|
+
logger: Logger;
|
|
56
|
+
registrationMode?: "full" | "setup-only" | "setup-runtime" | "cli-metadata";
|
|
57
|
+
config?: OpenClawConfig;
|
|
58
|
+
registerTool(tool: ToolDef): void;
|
|
59
|
+
registerService(service: {
|
|
60
|
+
id: string;
|
|
61
|
+
start: (ctx: PluginServiceContext) => void | Promise<void>;
|
|
62
|
+
stop?: (ctx: PluginServiceContext) => void | Promise<void>;
|
|
63
|
+
}): void;
|
|
64
|
+
}
|
|
65
|
+
//#endregion
|
|
66
|
+
export { RemotePluginConfig as n, OpenClawPluginApi as t };
|
package/openclaw.plugin.json
CHANGED
|
@@ -17,12 +17,17 @@
|
|
|
17
17
|
},
|
|
18
18
|
"configSchema": {
|
|
19
19
|
"type": "object",
|
|
20
|
-
"additionalProperties":
|
|
20
|
+
"additionalProperties": false,
|
|
21
21
|
"properties": {
|
|
22
|
-
"remoteWsUrl": { "type": "string" },
|
|
23
|
-
"browserExecutablePath": { "type": "string" },
|
|
22
|
+
"remoteWsUrl": { "type": "string", "minLength": 1, "maxLength": 2048 },
|
|
23
|
+
"browserExecutablePath": { "type": "string", "minLength": 1, "maxLength": 4096 },
|
|
24
24
|
"browserHeadless": { "type": "boolean" },
|
|
25
|
-
"browserNoSandbox": { "type": "boolean" }
|
|
25
|
+
"browserNoSandbox": { "type": "boolean" },
|
|
26
|
+
"handoffTimeoutMs": {
|
|
27
|
+
"type": "integer",
|
|
28
|
+
"minimum": 1000,
|
|
29
|
+
"maximum": 1800000
|
|
30
|
+
}
|
|
26
31
|
}
|
|
27
32
|
}
|
|
28
33
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alfe.ai/openclaw-remote",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.16",
|
|
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",
|
|
@@ -24,15 +24,16 @@
|
|
|
24
24
|
},
|
|
25
25
|
"files": [
|
|
26
26
|
"dist",
|
|
27
|
-
"openclaw.plugin.json"
|
|
27
|
+
"openclaw.plugin.json",
|
|
28
|
+
"README.md"
|
|
28
29
|
],
|
|
29
30
|
"dependencies": {
|
|
30
|
-
"@alfe.ai/agent-api-client": "^0.
|
|
31
|
-
"@alfe.ai/openclaw-plugin-kit": "0.
|
|
32
|
-
"@alfe.ai/browser": "^0.2.
|
|
33
|
-
"@alfe.ai/config": "0.4.
|
|
34
|
-
"@alfe.ai/remote": "^0.1.
|
|
35
|
-
"@alfe.ai/terminal": "^0.1.
|
|
31
|
+
"@alfe.ai/agent-api-client": "^0.15.0",
|
|
32
|
+
"@alfe.ai/openclaw-plugin-kit": "0.2.0",
|
|
33
|
+
"@alfe.ai/browser": "^0.2.1",
|
|
34
|
+
"@alfe.ai/config": "0.4.1",
|
|
35
|
+
"@alfe.ai/remote": "^0.1.1",
|
|
36
|
+
"@alfe.ai/terminal": "^0.1.3"
|
|
36
37
|
},
|
|
37
38
|
"peerDependencies": {
|
|
38
39
|
"openclaw": ">=2026.3.0"
|
package/dist/plugin2.cjs
DELETED
|
@@ -1,485 +0,0 @@
|
|
|
1
|
-
let _alfe_ai_config = require("@alfe.ai/config");
|
|
2
|
-
let _alfe_ai_agent_api_client = require("@alfe.ai/agent-api-client");
|
|
3
|
-
let _alfe_ai_remote = require("@alfe.ai/remote");
|
|
4
|
-
let _alfe_ai_browser = require("@alfe.ai/browser");
|
|
5
|
-
let _alfe_ai_terminal = require("@alfe.ai/terminal");
|
|
6
|
-
let _alfe_ai_openclaw_plugin_kit = require("@alfe.ai/openclaw-plugin-kit");
|
|
7
|
-
let node_module = require("node:module");
|
|
8
|
-
//#region src/ssrf.ts
|
|
9
|
-
/** Hostnames blocked outright (case-insensitive, exact match). */
|
|
10
|
-
const BLOCKED_HOSTNAMES = new Set(["localhost"]);
|
|
11
|
-
/** Hostname suffixes blocked outright (internal service discovery names). */
|
|
12
|
-
const BLOCKED_HOST_SUFFIXES = [".internal", ".local"];
|
|
13
|
-
/** Parse a dotted-quad IPv4 literal into its four octets, or null. */
|
|
14
|
-
function parseIpv4(host) {
|
|
15
|
-
const parts = host.split(".");
|
|
16
|
-
if (parts.length !== 4) return null;
|
|
17
|
-
const octets = [];
|
|
18
|
-
for (const part of parts) {
|
|
19
|
-
if (!/^\d{1,3}$/.test(part)) return null;
|
|
20
|
-
const n = Number(part);
|
|
21
|
-
if (n > 255) return null;
|
|
22
|
-
octets.push(n);
|
|
23
|
-
}
|
|
24
|
-
return octets;
|
|
25
|
-
}
|
|
26
|
-
/** Is this IPv4 literal private, loopback, link-local, or otherwise reserved? */
|
|
27
|
-
function isPrivateOrReservedIpv4(host) {
|
|
28
|
-
const octets = parseIpv4(host);
|
|
29
|
-
if (!octets) return false;
|
|
30
|
-
const [a, b] = octets;
|
|
31
|
-
if (a === 0) return true;
|
|
32
|
-
if (a === 10) return true;
|
|
33
|
-
if (a === 127) return true;
|
|
34
|
-
if (a === 169 && b === 254) return true;
|
|
35
|
-
if (a === 172 && b >= 16 && b <= 31) return true;
|
|
36
|
-
if (a === 192 && b === 168) return true;
|
|
37
|
-
if (a === 100 && b >= 64 && b <= 127) return true;
|
|
38
|
-
return false;
|
|
39
|
-
}
|
|
40
|
-
/** Is this IPv6 literal (brackets already stripped) loopback/private/link-local? */
|
|
41
|
-
function isPrivateOrReservedIpv6(host) {
|
|
42
|
-
const h = host.toLowerCase();
|
|
43
|
-
if (h === "::1" || h === "::") return true;
|
|
44
|
-
const mapped = /^::ffff:(.+)$/.exec(h);
|
|
45
|
-
if (mapped) {
|
|
46
|
-
const rest = mapped[1];
|
|
47
|
-
if (rest.includes(".")) return isPrivateOrReservedIpv4(rest);
|
|
48
|
-
const groups = rest.split(":");
|
|
49
|
-
if (groups.length === 2 && groups.every((g) => /^[0-9a-f]{1,4}$/.test(g))) {
|
|
50
|
-
const g1 = parseInt(groups[0], 16);
|
|
51
|
-
const g2 = parseInt(groups[1], 16);
|
|
52
|
-
return isPrivateOrReservedIpv4([
|
|
53
|
-
g1 >> 8 & 255,
|
|
54
|
-
g1 & 255,
|
|
55
|
-
g2 >> 8 & 255,
|
|
56
|
-
g2 & 255
|
|
57
|
-
].join("."));
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
if (/^f[cd][0-9a-f]*:/.test(h)) return true;
|
|
61
|
-
if (/^fe[89ab][0-9a-f]*:/.test(h)) return true;
|
|
62
|
-
return false;
|
|
63
|
-
}
|
|
64
|
-
/**
|
|
65
|
-
* Build the navigation predicate for a given SSRF policy. When
|
|
66
|
-
* `dangerouslyAllowPrivateNetwork` is true the predicate is allow-all
|
|
67
|
-
* (matching the `alfe` integration's current default); otherwise it blocks
|
|
68
|
-
* the reserved ranges above and any non-http(s) scheme.
|
|
69
|
-
*/
|
|
70
|
-
function buildIsNavigationAllowed(policy, log) {
|
|
71
|
-
const allowPrivate = policy?.dangerouslyAllowPrivateNetwork === true;
|
|
72
|
-
const block = (rawUrl) => {
|
|
73
|
-
log?.warn(`SSRF policy blocked navigation to ${rawUrl}`);
|
|
74
|
-
return false;
|
|
75
|
-
};
|
|
76
|
-
return (rawUrl) => {
|
|
77
|
-
let url;
|
|
78
|
-
try {
|
|
79
|
-
url = new URL(rawUrl);
|
|
80
|
-
} catch {
|
|
81
|
-
return block(rawUrl);
|
|
82
|
-
}
|
|
83
|
-
if (url.protocol !== "http:" && url.protocol !== "https:") return block(rawUrl);
|
|
84
|
-
if (allowPrivate) return true;
|
|
85
|
-
const host = url.hostname.toLowerCase().replace(/^\[|\]$/g, "");
|
|
86
|
-
if (BLOCKED_HOSTNAMES.has(host)) return block(rawUrl);
|
|
87
|
-
if (BLOCKED_HOST_SUFFIXES.some((s) => host.endsWith(s))) return block(rawUrl);
|
|
88
|
-
if (parseIpv4(host)) {
|
|
89
|
-
if (isPrivateOrReservedIpv4(host)) return block(rawUrl);
|
|
90
|
-
} else if (host.includes(":")) {
|
|
91
|
-
if (isPrivateOrReservedIpv6(host)) return block(rawUrl);
|
|
92
|
-
}
|
|
93
|
-
return true;
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
//#endregion
|
|
97
|
-
//#region src/plugin.ts
|
|
98
|
-
/**
|
|
99
|
-
* @alfe.ai/openclaw-remote — OpenClaw plugin for the interactive remote-control
|
|
100
|
-
* relay. Owns one outbound WS to the relay and routes per-session frames to the
|
|
101
|
-
* browser co-browse surface (@alfe.ai/browser) or the web terminal surface
|
|
102
|
-
* (@alfe.ai/terminal). Registers the agent-callable browser tools, including
|
|
103
|
-
* `request_browser_takeover` ("help me complete these").
|
|
104
|
-
*
|
|
105
|
-
* Follows the established Alfe plugin shape (see @alfe.ai/openclaw-webhooks):
|
|
106
|
-
* tools register on every activate(); the long-lived connection is guarded and
|
|
107
|
-
* created inside registerService.start.
|
|
108
|
-
*/
|
|
109
|
-
const pkg = (0, node_module.createRequire)(require("url").pathToFileURL(__filename).href)("../package.json");
|
|
110
|
-
const ACTIVATED_KEY = (0, _alfe_ai_openclaw_plugin_kit.getActivationKey)("remote");
|
|
111
|
-
const DEFAULT_HANDOFF_TIMEOUT_MS = 600 * 1e3;
|
|
112
|
-
const DEFAULT_CHROME_PATH = "/usr/bin/google-chrome-stable";
|
|
113
|
-
/**
|
|
114
|
-
* Derive the relay WebSocket URL from the agent's cloud apiUrl when the
|
|
115
|
-
* manifest doesn't provide one — mirrors @alfe.ai/console-client's
|
|
116
|
-
* `deriveConsoleWsUrl` so the URL is per-stage automatically:
|
|
117
|
-
* https://api.dev.alfe.ai → wss://remote.dev.alfe.ai/ws
|
|
118
|
-
* (matches config.flyDomains.remote per stage). This is why the
|
|
119
|
-
* headless-browser integration manifest carries no hardcoded, prod-pinned
|
|
120
|
-
* `remoteWsUrl`: the plugin resolves it from the agent's own endpoint.
|
|
121
|
-
*/
|
|
122
|
-
function deriveRemoteWsUrl(apiUrl) {
|
|
123
|
-
try {
|
|
124
|
-
return `wss://${new URL(apiUrl).hostname.replace("api.", "remote.")}/ws`;
|
|
125
|
-
} catch {
|
|
126
|
-
return;
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
/**
|
|
130
|
-
* Derive the dashboard (control-plane) base URL from the agent's cloud apiUrl,
|
|
131
|
-
* mirroring `deriveRemoteWsUrl` so it's per-stage automatically:
|
|
132
|
-
* https://api.dev.alfe.ai → https://app.dev.alfe.ai
|
|
133
|
-
* https://api.alfe.ai → https://app.alfe.ai (prod)
|
|
134
|
-
* The `api.` → `app.` swap matches config.*.ts `dashboardDomain` for every
|
|
135
|
-
* stage. Returns undefined for a non-`api.` host (e.g. localhost/dev override)
|
|
136
|
-
* so the caller can fall back to a session-only link.
|
|
137
|
-
*/
|
|
138
|
-
function deriveDashboardBaseUrl(apiUrl) {
|
|
139
|
-
try {
|
|
140
|
-
const url = new URL(apiUrl);
|
|
141
|
-
if (!url.hostname.startsWith("api.")) return void 0;
|
|
142
|
-
return `https://${url.hostname.replace("api.", "app.")}`;
|
|
143
|
-
} catch {
|
|
144
|
-
return;
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
/**
|
|
148
|
-
* Build the dashboard deep-link ("control URL") the human opens to take over.
|
|
149
|
-
* Prefers the full agent-scoped path; if the dashboard host or agentId is
|
|
150
|
-
* unavailable, degrades gracefully (session-only path, then null).
|
|
151
|
-
*/
|
|
152
|
-
function buildControlUrl(dashboardBaseUrl, agentId, sessionId) {
|
|
153
|
-
if (!dashboardBaseUrl) return void 0;
|
|
154
|
-
const session = encodeURIComponent(sessionId);
|
|
155
|
-
if (agentId) return `${dashboardBaseUrl}/agents/${encodeURIComponent(agentId)}?tab=browser&session=${session}`;
|
|
156
|
-
return `${dashboardBaseUrl}/agents?tab=browser&session=${session}`;
|
|
157
|
-
}
|
|
158
|
-
let remoteClient = null;
|
|
159
|
-
let browserSurface = null;
|
|
160
|
-
let terminalSurface = null;
|
|
161
|
-
let apiClient = null;
|
|
162
|
-
let handoffTimeoutMs = DEFAULT_HANDOFF_TIMEOUT_MS;
|
|
163
|
-
let dashboardBaseUrl;
|
|
164
|
-
let selfAgentId;
|
|
165
|
-
const sessionSurfaces = /* @__PURE__ */ new Map();
|
|
166
|
-
/** Coerce an unknown tool param to a string (empty if not a string). */
|
|
167
|
-
function asStr(v) {
|
|
168
|
-
return typeof v === "string" ? v : "";
|
|
169
|
-
}
|
|
170
|
-
/** Coerce an unknown tool param to a string or undefined. */
|
|
171
|
-
function optStr(v) {
|
|
172
|
-
return typeof v === "string" ? v : void 0;
|
|
173
|
-
}
|
|
174
|
-
/** Route an inbound relay frame to the owning surface by session. */
|
|
175
|
-
function dispatchFrame(frame, log) {
|
|
176
|
-
if (frame.type === _alfe_ai_remote.RemoteFrameType.SESSION_OPEN) {
|
|
177
|
-
const open = (0, _alfe_ai_remote.decodeJson)(frame.payload);
|
|
178
|
-
if (!open) return;
|
|
179
|
-
const handler = open.surface === "terminal" ? terminalSurface : browserSurface;
|
|
180
|
-
if (!handler) {
|
|
181
|
-
log.warn(`No handler for surface ${open.surface}`);
|
|
182
|
-
return;
|
|
183
|
-
}
|
|
184
|
-
sessionSurfaces.set(frame.sessionId, open.surface);
|
|
185
|
-
Promise.resolve(handler.openSession(frame.sessionId, open)).catch((err) => {
|
|
186
|
-
log.warn(`openSession failed: ${err.message}`);
|
|
187
|
-
});
|
|
188
|
-
return;
|
|
189
|
-
}
|
|
190
|
-
const surface = sessionSurfaces.get(frame.sessionId);
|
|
191
|
-
const handler = surface === "terminal" ? terminalSurface : surface === "browser" ? browserSurface : null;
|
|
192
|
-
if (!handler) return;
|
|
193
|
-
if (frame.type === _alfe_ai_remote.RemoteFrameType.SESSION_CLOSE) {
|
|
194
|
-
handler.closeSession(frame.sessionId);
|
|
195
|
-
sessionSurfaces.delete(frame.sessionId);
|
|
196
|
-
return;
|
|
197
|
-
}
|
|
198
|
-
handler.handleFrame(frame);
|
|
199
|
-
}
|
|
200
|
-
function startService(pluginConfig, ssrfPolicy, workspaceDir, log) {
|
|
201
|
-
(0, _alfe_ai_openclaw_plugin_kit.guardedStart)(ACTIVATED_KEY, log, () => {
|
|
202
|
-
startServiceInner(pluginConfig, ssrfPolicy, workspaceDir, log);
|
|
203
|
-
});
|
|
204
|
-
}
|
|
205
|
-
function startServiceInner(pluginConfig, ssrfPolicy, workspaceDir, log) {
|
|
206
|
-
let alfeConfig = null;
|
|
207
|
-
try {
|
|
208
|
-
alfeConfig = (0, _alfe_ai_config.resolveConfig)();
|
|
209
|
-
} catch {
|
|
210
|
-
log.info("Could not resolve Alfe config — remote plugin idle");
|
|
211
|
-
}
|
|
212
|
-
const apiKey = alfeConfig?.apiKey;
|
|
213
|
-
const apiUrl = alfeConfig?.apiUrl;
|
|
214
|
-
const wsUrl = pluginConfig.remoteWsUrl ?? (apiUrl ? deriveRemoteWsUrl(apiUrl) : void 0);
|
|
215
|
-
if (!wsUrl || !apiKey || !apiUrl) {
|
|
216
|
-
log.info("Remote relay URL or credentials not configured — plugin running without relay");
|
|
217
|
-
(0, _alfe_ai_openclaw_plugin_kit.resetActivation)(ACTIVATED_KEY);
|
|
218
|
-
return;
|
|
219
|
-
}
|
|
220
|
-
handoffTimeoutMs = pluginConfig.handoffTimeoutMs ?? DEFAULT_HANDOFF_TIMEOUT_MS;
|
|
221
|
-
apiClient = new _alfe_ai_agent_api_client.AgentApiClient({
|
|
222
|
-
apiKey,
|
|
223
|
-
apiUrl
|
|
224
|
-
});
|
|
225
|
-
dashboardBaseUrl = deriveDashboardBaseUrl(apiUrl);
|
|
226
|
-
apiClient.whoami().then(({ agentId }) => {
|
|
227
|
-
selfAgentId = agentId;
|
|
228
|
-
}).catch((err) => {
|
|
229
|
-
log.debug(`whoami failed — takeover links will omit agentId: ${err.message}`);
|
|
230
|
-
});
|
|
231
|
-
const sendFrame = (buf) => {
|
|
232
|
-
remoteClient?.sendFrame(buf);
|
|
233
|
-
};
|
|
234
|
-
browserSurface = new _alfe_ai_browser.BrowserSurface({
|
|
235
|
-
executablePath: pluginConfig.browserExecutablePath ?? DEFAULT_CHROME_PATH,
|
|
236
|
-
headless: pluginConfig.browserHeadless ?? true,
|
|
237
|
-
noSandbox: pluginConfig.browserNoSandbox ?? true,
|
|
238
|
-
userDataDir: `${workspaceDir ?? alfeConfig?.workspacePath ?? "."}/.alfe-browser-profile`,
|
|
239
|
-
isNavigationAllowed: buildIsNavigationAllowed(ssrfPolicy, log),
|
|
240
|
-
logger: log
|
|
241
|
-
}, sendFrame);
|
|
242
|
-
terminalSurface = new _alfe_ai_terminal.TerminalSurface({
|
|
243
|
-
cwd: workspaceDir ?? alfeConfig?.workspacePath,
|
|
244
|
-
logger: log
|
|
245
|
-
}, sendFrame);
|
|
246
|
-
remoteClient = new _alfe_ai_remote.RemoteServiceClient({
|
|
247
|
-
wsUrl,
|
|
248
|
-
apiKey,
|
|
249
|
-
onFrame: (frame) => {
|
|
250
|
-
dispatchFrame(frame, log);
|
|
251
|
-
},
|
|
252
|
-
onConnectionChange: (connected) => {
|
|
253
|
-
log.info(`Remote relay connection: ${connected ? "connected" : "disconnected"}`);
|
|
254
|
-
},
|
|
255
|
-
logger: log
|
|
256
|
-
});
|
|
257
|
-
remoteClient.start();
|
|
258
|
-
log.info(`Remote plugin started — relay ${wsUrl}`);
|
|
259
|
-
}
|
|
260
|
-
function stopService(log) {
|
|
261
|
-
remoteClient?.stop();
|
|
262
|
-
remoteClient = null;
|
|
263
|
-
browserSurface?.shutdown();
|
|
264
|
-
browserSurface = null;
|
|
265
|
-
terminalSurface?.shutdown();
|
|
266
|
-
terminalSurface = null;
|
|
267
|
-
apiClient = null;
|
|
268
|
-
dashboardBaseUrl = void 0;
|
|
269
|
-
selfAgentId = void 0;
|
|
270
|
-
sessionSurfaces.clear();
|
|
271
|
-
(0, _alfe_ai_openclaw_plugin_kit.resetActivation)(ACTIVATED_KEY);
|
|
272
|
-
log.info("Remote plugin stopped");
|
|
273
|
-
}
|
|
274
|
-
function registerTools(api) {
|
|
275
|
-
const needBrowser = () => {
|
|
276
|
-
if (!browserSurface) throw new Error("Browser surface not available (remote relay not connected)");
|
|
277
|
-
return browserSurface;
|
|
278
|
-
};
|
|
279
|
-
api.registerTool({
|
|
280
|
-
name: "browser_navigate",
|
|
281
|
-
label: "browser_navigate",
|
|
282
|
-
description: "Navigate the shared browser to a URL. Returns the final URL and page title.",
|
|
283
|
-
parameters: {
|
|
284
|
-
type: "object",
|
|
285
|
-
properties: { url: {
|
|
286
|
-
type: "string",
|
|
287
|
-
description: "The URL to open"
|
|
288
|
-
} },
|
|
289
|
-
required: ["url"]
|
|
290
|
-
},
|
|
291
|
-
execute: async (_id, params) => needBrowser().automation.navigate(asStr(params.url))
|
|
292
|
-
});
|
|
293
|
-
api.registerTool({
|
|
294
|
-
name: "browser_click",
|
|
295
|
-
label: "browser_click",
|
|
296
|
-
description: "Click an element in the shared browser by CSS selector.",
|
|
297
|
-
parameters: {
|
|
298
|
-
type: "object",
|
|
299
|
-
properties: { selector: {
|
|
300
|
-
type: "string",
|
|
301
|
-
description: "CSS selector to click"
|
|
302
|
-
} },
|
|
303
|
-
required: ["selector"]
|
|
304
|
-
},
|
|
305
|
-
execute: async (_id, params) => {
|
|
306
|
-
await needBrowser().automation.click(asStr(params.selector));
|
|
307
|
-
return { ok: true };
|
|
308
|
-
}
|
|
309
|
-
});
|
|
310
|
-
api.registerTool({
|
|
311
|
-
name: "browser_type",
|
|
312
|
-
label: "browser_type",
|
|
313
|
-
description: "Type text into an element in the shared browser by CSS selector.",
|
|
314
|
-
parameters: {
|
|
315
|
-
type: "object",
|
|
316
|
-
properties: {
|
|
317
|
-
selector: {
|
|
318
|
-
type: "string",
|
|
319
|
-
description: "CSS selector of the input"
|
|
320
|
-
},
|
|
321
|
-
text: {
|
|
322
|
-
type: "string",
|
|
323
|
-
description: "Text to type"
|
|
324
|
-
}
|
|
325
|
-
},
|
|
326
|
-
required: ["selector", "text"]
|
|
327
|
-
},
|
|
328
|
-
execute: async (_id, params) => {
|
|
329
|
-
await needBrowser().automation.type(asStr(params.selector), asStr(params.text));
|
|
330
|
-
return { ok: true };
|
|
331
|
-
}
|
|
332
|
-
});
|
|
333
|
-
api.registerTool({
|
|
334
|
-
name: "browser_wait_for",
|
|
335
|
-
label: "browser_wait_for",
|
|
336
|
-
description: "Wait for a selector to appear, a URL substring to match, or a fixed delay.",
|
|
337
|
-
parameters: {
|
|
338
|
-
type: "object",
|
|
339
|
-
properties: {
|
|
340
|
-
selector: { type: "string" },
|
|
341
|
-
urlPattern: { type: "string" },
|
|
342
|
-
ms: { type: "number" }
|
|
343
|
-
}
|
|
344
|
-
},
|
|
345
|
-
execute: async (_id, params) => {
|
|
346
|
-
await needBrowser().automation.waitFor({
|
|
347
|
-
selector: optStr(params.selector),
|
|
348
|
-
urlPattern: optStr(params.urlPattern),
|
|
349
|
-
ms: typeof params.ms === "number" ? params.ms : void 0
|
|
350
|
-
});
|
|
351
|
-
return { ok: true };
|
|
352
|
-
}
|
|
353
|
-
});
|
|
354
|
-
api.registerTool({
|
|
355
|
-
name: "browser_screenshot",
|
|
356
|
-
label: "browser_screenshot",
|
|
357
|
-
description: "Capture a JPEG screenshot of the shared browser's current page (base64).",
|
|
358
|
-
parameters: {
|
|
359
|
-
type: "object",
|
|
360
|
-
properties: {}
|
|
361
|
-
},
|
|
362
|
-
execute: async () => ({ imageBase64: await needBrowser().automation.screenshot() })
|
|
363
|
-
});
|
|
364
|
-
api.registerTool({
|
|
365
|
-
name: "browser_evaluate",
|
|
366
|
-
label: "browser_evaluate",
|
|
367
|
-
description: "Evaluate a JavaScript expression in the shared browser page and return the result.",
|
|
368
|
-
parameters: {
|
|
369
|
-
type: "object",
|
|
370
|
-
properties: { expression: {
|
|
371
|
-
type: "string",
|
|
372
|
-
description: "JS expression to evaluate"
|
|
373
|
-
} },
|
|
374
|
-
required: ["expression"]
|
|
375
|
-
},
|
|
376
|
-
execute: async (_id, params) => ({ result: await needBrowser().automation.evaluate(asStr(params.expression)) })
|
|
377
|
-
});
|
|
378
|
-
api.registerTool({
|
|
379
|
-
name: "request_browser_takeover",
|
|
380
|
-
label: "request_browser_takeover",
|
|
381
|
-
description: "Ask a human to take over the browser you're currently looking at and complete a step you can't do yourself — logging in, solving a captcha, or clicking through a manual flow. The human sees your current live page, completes the instructions, and hands control back; you then resume on the same authenticated page. Returns a control URL — SHARE IT WITH THE USER so they can open the takeover. Blocks until the human is done or the request times out.",
|
|
382
|
-
parameters: {
|
|
383
|
-
type: "object",
|
|
384
|
-
properties: {
|
|
385
|
-
instructions: {
|
|
386
|
-
type: "string",
|
|
387
|
-
description: "What you need the human to do, e.g. 'Log in with the saved credentials and complete the 2FA prompt, then click Continue.'"
|
|
388
|
-
},
|
|
389
|
-
url: {
|
|
390
|
-
type: "string",
|
|
391
|
-
description: "Optional: the page you're stuck on (display only — the human sees your live page)."
|
|
392
|
-
},
|
|
393
|
-
conversationId: {
|
|
394
|
-
type: "string",
|
|
395
|
-
description: "Optional: the chat conversation to surface the request in."
|
|
396
|
-
}
|
|
397
|
-
},
|
|
398
|
-
required: ["instructions"]
|
|
399
|
-
},
|
|
400
|
-
execute: async (_id, params) => {
|
|
401
|
-
const surface = needBrowser();
|
|
402
|
-
if (!apiClient) throw new Error("Remote plugin not connected");
|
|
403
|
-
const { sessionId } = await apiClient.requestBrowserTakeover({
|
|
404
|
-
instructions: asStr(params.instructions),
|
|
405
|
-
url: optStr(params.url),
|
|
406
|
-
conversationId: optStr(params.conversationId)
|
|
407
|
-
});
|
|
408
|
-
const controlUrl = buildControlUrl(dashboardBaseUrl, selfAgentId, sessionId);
|
|
409
|
-
const message = controlUrl ? `Browser takeover requested (session ${sessionId}). Ask the user to open this link to take control and complete: ${controlUrl} — then wait; I'll resume automatically once they hand back.` : `Browser takeover requested (session ${sessionId}). Ask the user to open the Alfe dashboard, go to this agent's Browser tab, and take control — then wait; I'll resume automatically once they hand back.`;
|
|
410
|
-
surface.addHold();
|
|
411
|
-
let holdReleased = false;
|
|
412
|
-
const releaseHold = () => {
|
|
413
|
-
if (holdReleased) return;
|
|
414
|
-
holdReleased = true;
|
|
415
|
-
surface.removeHold();
|
|
416
|
-
};
|
|
417
|
-
try {
|
|
418
|
-
return {
|
|
419
|
-
sessionId,
|
|
420
|
-
controlUrl,
|
|
421
|
-
message,
|
|
422
|
-
...await surface.requestHandoff(handoffTimeoutMs)
|
|
423
|
-
};
|
|
424
|
-
} finally {
|
|
425
|
-
releaseHold();
|
|
426
|
-
await apiClient.completeRemoteSession(sessionId).catch(() => {});
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
});
|
|
430
|
-
}
|
|
431
|
-
const plugin = {
|
|
432
|
-
id: "@alfe.ai/openclaw-remote",
|
|
433
|
-
name: "Remote",
|
|
434
|
-
description: "Interactive remote control — browser co-browse takeover and web terminal",
|
|
435
|
-
version: pkg.version,
|
|
436
|
-
activate(api) {
|
|
437
|
-
(0, _alfe_ai_agent_api_client.installToolErrorCapture)(api, { plugin: "openclaw-remote" });
|
|
438
|
-
const log = api.logger;
|
|
439
|
-
const pluginConfig = api.config?.plugins?.entries?.["@alfe.ai/openclaw-remote"]?.config ?? {};
|
|
440
|
-
pluginConfig.browserExecutablePath ??= api.config?.browser?.executablePath;
|
|
441
|
-
pluginConfig.browserHeadless ??= api.config?.browser?.headless;
|
|
442
|
-
pluginConfig.browserNoSandbox ??= api.config?.browser?.noSandbox;
|
|
443
|
-
const ssrfPolicy = api.config?.browser?.ssrfPolicy;
|
|
444
|
-
registerTools(api);
|
|
445
|
-
api.registerService({
|
|
446
|
-
id: "alfe-remote",
|
|
447
|
-
start: (ctx) => {
|
|
448
|
-
startService(pluginConfig, ssrfPolicy, ctx.workspaceDir, log);
|
|
449
|
-
},
|
|
450
|
-
stop: () => {
|
|
451
|
-
stopService(log);
|
|
452
|
-
}
|
|
453
|
-
});
|
|
454
|
-
log.info("Alfe Remote plugin activated");
|
|
455
|
-
},
|
|
456
|
-
deactivate(api) {
|
|
457
|
-
stopService(api.logger);
|
|
458
|
-
api.logger.info("Alfe Remote plugin deactivated");
|
|
459
|
-
}
|
|
460
|
-
};
|
|
461
|
-
//#endregion
|
|
462
|
-
Object.defineProperty(exports, "buildControlUrl", {
|
|
463
|
-
enumerable: true,
|
|
464
|
-
get: function() {
|
|
465
|
-
return buildControlUrl;
|
|
466
|
-
}
|
|
467
|
-
});
|
|
468
|
-
Object.defineProperty(exports, "buildIsNavigationAllowed", {
|
|
469
|
-
enumerable: true,
|
|
470
|
-
get: function() {
|
|
471
|
-
return buildIsNavigationAllowed;
|
|
472
|
-
}
|
|
473
|
-
});
|
|
474
|
-
Object.defineProperty(exports, "deriveDashboardBaseUrl", {
|
|
475
|
-
enumerable: true,
|
|
476
|
-
get: function() {
|
|
477
|
-
return deriveDashboardBaseUrl;
|
|
478
|
-
}
|
|
479
|
-
});
|
|
480
|
-
Object.defineProperty(exports, "plugin", {
|
|
481
|
-
enumerable: true,
|
|
482
|
-
get: function() {
|
|
483
|
-
return plugin;
|
|
484
|
-
}
|
|
485
|
-
});
|
package/dist/plugin2.d.cts
DELETED
package/dist/plugin2.d.ts
DELETED