@agenticmail/enterprise 0.5.78 → 0.5.80
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/dist/chunk-7MILGDAA.js +2191 -0
- package/dist/chunk-7RNT4O5T.js +15198 -0
- package/dist/chunk-AGFOJCSB.js +2191 -0
- package/dist/chunk-F4GSFCM3.js +898 -0
- package/dist/chunk-GWUIYH7I.js +15035 -0
- package/dist/chunk-PZA7YOJE.js +898 -0
- package/dist/chunk-Q3V7VZFQ.js +2191 -0
- package/dist/chunk-RRFB6G6M.js +15198 -0
- package/dist/chunk-VX3VFMVB.js +409 -0
- package/dist/chunk-WRPZCOWC.js +898 -0
- package/dist/cli.js +1 -1
- package/dist/dashboard/pages/agent-detail.js +313 -1
- package/dist/index.js +3 -3
- package/dist/pw-ai-KPETTB25.js +2212 -0
- package/dist/routes-PDHMCIXU.js +6676 -0
- package/dist/runtime-7HW4GX5L.js +48 -0
- package/dist/runtime-GYVO3NF3.js +47 -0
- package/dist/runtime-XXDCZZIK.js +48 -0
- package/dist/server-FMP4BFGW.js +12 -0
- package/dist/server-JRHDUNII.js +12 -0
- package/dist/server-VNW6G4GB.js +12 -0
- package/dist/setup-AANLREEL.js +20 -0
- package/dist/setup-O5FPRLK4.js +20 -0
- package/dist/setup-S4Z4PPIJ.js +20 -0
- package/package.json +15 -2
- package/src/agent-tools/common.ts +25 -0
- package/src/agent-tools/index.ts +4 -0
- package/src/agent-tools/schema/typebox.ts +25 -0
- package/src/agent-tools/tools/browser-tool.schema.ts +112 -0
- package/src/agent-tools/tools/browser-tool.ts +388 -0
- package/src/agent-tools/tools/gateway.ts +126 -0
- package/src/agent-tools/tools/nodes-utils.ts +80 -0
- package/src/browser/bridge-auth-registry.ts +34 -0
- package/src/browser/bridge-server.ts +93 -0
- package/src/browser/cdp.helpers.ts +180 -0
- package/src/browser/cdp.ts +466 -0
- package/src/browser/chrome.executables.ts +625 -0
- package/src/browser/chrome.profile-decoration.ts +198 -0
- package/src/browser/chrome.ts +349 -0
- package/src/browser/client-actions-core.ts +259 -0
- package/src/browser/client-actions-observe.ts +184 -0
- package/src/browser/client-actions-state.ts +284 -0
- package/src/browser/client-actions-types.ts +16 -0
- package/src/browser/client-actions-url.ts +11 -0
- package/src/browser/client-actions.ts +4 -0
- package/src/browser/client-fetch.ts +253 -0
- package/src/browser/client.ts +337 -0
- package/src/browser/config.ts +296 -0
- package/src/browser/constants.ts +8 -0
- package/src/browser/control-auth.ts +94 -0
- package/src/browser/control-service.ts +81 -0
- package/src/browser/csrf.ts +87 -0
- package/src/browser/enterprise-compat.ts +518 -0
- package/src/browser/extension-relay.ts +834 -0
- package/src/browser/http-auth.ts +63 -0
- package/src/browser/navigation-guard.ts +50 -0
- package/src/browser/paths.ts +49 -0
- package/src/browser/profiles-service.ts +187 -0
- package/src/browser/profiles.ts +113 -0
- package/src/browser/proxy-files.ts +41 -0
- package/src/browser/pw-ai-module.ts +52 -0
- package/src/browser/pw-ai-state.ts +9 -0
- package/src/browser/pw-ai.ts +65 -0
- package/src/browser/pw-role-snapshot.ts +434 -0
- package/src/browser/pw-session.ts +810 -0
- package/src/browser/pw-tools-core.activity.ts +68 -0
- package/src/browser/pw-tools-core.downloads.ts +281 -0
- package/src/browser/pw-tools-core.interactions.ts +646 -0
- package/src/browser/pw-tools-core.responses.ts +124 -0
- package/src/browser/pw-tools-core.shared.ts +70 -0
- package/src/browser/pw-tools-core.snapshot.ts +213 -0
- package/src/browser/pw-tools-core.state.ts +209 -0
- package/src/browser/pw-tools-core.storage.ts +128 -0
- package/src/browser/pw-tools-core.trace.ts +37 -0
- package/src/browser/pw-tools-core.ts +8 -0
- package/src/browser/resolved-config-refresh.ts +59 -0
- package/src/browser/routes/agent.act.shared.ts +52 -0
- package/src/browser/routes/agent.act.ts +575 -0
- package/src/browser/routes/agent.debug.ts +149 -0
- package/src/browser/routes/agent.shared.ts +143 -0
- package/src/browser/routes/agent.snapshot.ts +333 -0
- package/src/browser/routes/agent.storage.ts +451 -0
- package/src/browser/routes/agent.ts +13 -0
- package/src/browser/routes/basic.ts +202 -0
- package/src/browser/routes/dispatcher.ts +126 -0
- package/src/browser/routes/index.ts +11 -0
- package/src/browser/routes/path-output.ts +1 -0
- package/src/browser/routes/tabs.ts +217 -0
- package/src/browser/routes/types.ts +26 -0
- package/src/browser/routes/utils.ts +73 -0
- package/src/browser/screenshot.ts +54 -0
- package/src/browser/server-context.ts +688 -0
- package/src/browser/server-context.types.ts +65 -0
- package/src/browser/server-lifecycle.ts +48 -0
- package/src/browser/server-middleware.ts +37 -0
- package/src/browser/server.ts +110 -0
- package/src/browser/target-id.ts +30 -0
- package/src/browser/trash.ts +21 -0
- package/src/dashboard/pages/agent-detail.js +313 -1
- package/src/engine/agent-routes.ts +46 -0
- package/src/security/external-content.ts +299 -0
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import type { BrowserActionOk, BrowserActionTargetOk } from "./client-actions-types.js";
|
|
2
|
+
import { buildProfileQuery, withBaseUrl } from "./client-actions-url.js";
|
|
3
|
+
import { fetchBrowserJson } from "./client-fetch.js";
|
|
4
|
+
|
|
5
|
+
export async function browserCookies(
|
|
6
|
+
baseUrl: string | undefined,
|
|
7
|
+
opts: { targetId?: string; profile?: string } = {},
|
|
8
|
+
): Promise<{ ok: true; targetId: string; cookies: unknown[] }> {
|
|
9
|
+
const q = new URLSearchParams();
|
|
10
|
+
if (opts.targetId) {
|
|
11
|
+
q.set("targetId", opts.targetId);
|
|
12
|
+
}
|
|
13
|
+
if (opts.profile) {
|
|
14
|
+
q.set("profile", opts.profile);
|
|
15
|
+
}
|
|
16
|
+
const suffix = q.toString() ? `?${q.toString()}` : "";
|
|
17
|
+
return await fetchBrowserJson<{
|
|
18
|
+
ok: true;
|
|
19
|
+
targetId: string;
|
|
20
|
+
cookies: unknown[];
|
|
21
|
+
}>(withBaseUrl(baseUrl, `/cookies${suffix}`), { timeoutMs: 20000 });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function browserCookiesSet(
|
|
25
|
+
baseUrl: string | undefined,
|
|
26
|
+
opts: {
|
|
27
|
+
cookie: Record<string, unknown>;
|
|
28
|
+
targetId?: string;
|
|
29
|
+
profile?: string;
|
|
30
|
+
},
|
|
31
|
+
): Promise<BrowserActionTargetOk> {
|
|
32
|
+
const q = buildProfileQuery(opts.profile);
|
|
33
|
+
return await fetchBrowserJson<BrowserActionTargetOk>(withBaseUrl(baseUrl, `/cookies/set${q}`), {
|
|
34
|
+
method: "POST",
|
|
35
|
+
headers: { "Content-Type": "application/json" },
|
|
36
|
+
body: JSON.stringify({ targetId: opts.targetId, cookie: opts.cookie }),
|
|
37
|
+
timeoutMs: 20000,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function browserCookiesClear(
|
|
42
|
+
baseUrl: string | undefined,
|
|
43
|
+
opts: { targetId?: string; profile?: string } = {},
|
|
44
|
+
): Promise<BrowserActionTargetOk> {
|
|
45
|
+
const q = buildProfileQuery(opts.profile);
|
|
46
|
+
return await fetchBrowserJson<BrowserActionTargetOk>(withBaseUrl(baseUrl, `/cookies/clear${q}`), {
|
|
47
|
+
method: "POST",
|
|
48
|
+
headers: { "Content-Type": "application/json" },
|
|
49
|
+
body: JSON.stringify({ targetId: opts.targetId }),
|
|
50
|
+
timeoutMs: 20000,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function browserStorageGet(
|
|
55
|
+
baseUrl: string | undefined,
|
|
56
|
+
opts: {
|
|
57
|
+
kind: "local" | "session";
|
|
58
|
+
key?: string;
|
|
59
|
+
targetId?: string;
|
|
60
|
+
profile?: string;
|
|
61
|
+
},
|
|
62
|
+
): Promise<{ ok: true; targetId: string; values: Record<string, string> }> {
|
|
63
|
+
const q = new URLSearchParams();
|
|
64
|
+
if (opts.targetId) {
|
|
65
|
+
q.set("targetId", opts.targetId);
|
|
66
|
+
}
|
|
67
|
+
if (opts.key) {
|
|
68
|
+
q.set("key", opts.key);
|
|
69
|
+
}
|
|
70
|
+
if (opts.profile) {
|
|
71
|
+
q.set("profile", opts.profile);
|
|
72
|
+
}
|
|
73
|
+
const suffix = q.toString() ? `?${q.toString()}` : "";
|
|
74
|
+
return await fetchBrowserJson<{
|
|
75
|
+
ok: true;
|
|
76
|
+
targetId: string;
|
|
77
|
+
values: Record<string, string>;
|
|
78
|
+
}>(withBaseUrl(baseUrl, `/storage/${opts.kind}${suffix}`), { timeoutMs: 20000 });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function browserStorageSet(
|
|
82
|
+
baseUrl: string | undefined,
|
|
83
|
+
opts: {
|
|
84
|
+
kind: "local" | "session";
|
|
85
|
+
key: string;
|
|
86
|
+
value: string;
|
|
87
|
+
targetId?: string;
|
|
88
|
+
profile?: string;
|
|
89
|
+
},
|
|
90
|
+
): Promise<BrowserActionTargetOk> {
|
|
91
|
+
const q = buildProfileQuery(opts.profile);
|
|
92
|
+
return await fetchBrowserJson<BrowserActionTargetOk>(
|
|
93
|
+
withBaseUrl(baseUrl, `/storage/${opts.kind}/set${q}`),
|
|
94
|
+
{
|
|
95
|
+
method: "POST",
|
|
96
|
+
headers: { "Content-Type": "application/json" },
|
|
97
|
+
body: JSON.stringify({
|
|
98
|
+
targetId: opts.targetId,
|
|
99
|
+
key: opts.key,
|
|
100
|
+
value: opts.value,
|
|
101
|
+
}),
|
|
102
|
+
timeoutMs: 20000,
|
|
103
|
+
},
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export async function browserStorageClear(
|
|
108
|
+
baseUrl: string | undefined,
|
|
109
|
+
opts: { kind: "local" | "session"; targetId?: string; profile?: string },
|
|
110
|
+
): Promise<BrowserActionTargetOk> {
|
|
111
|
+
const q = buildProfileQuery(opts.profile);
|
|
112
|
+
return await fetchBrowserJson<BrowserActionTargetOk>(
|
|
113
|
+
withBaseUrl(baseUrl, `/storage/${opts.kind}/clear${q}`),
|
|
114
|
+
{
|
|
115
|
+
method: "POST",
|
|
116
|
+
headers: { "Content-Type": "application/json" },
|
|
117
|
+
body: JSON.stringify({ targetId: opts.targetId }),
|
|
118
|
+
timeoutMs: 20000,
|
|
119
|
+
},
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export async function browserSetOffline(
|
|
124
|
+
baseUrl: string | undefined,
|
|
125
|
+
opts: { offline: boolean; targetId?: string; profile?: string },
|
|
126
|
+
): Promise<BrowserActionTargetOk> {
|
|
127
|
+
const q = buildProfileQuery(opts.profile);
|
|
128
|
+
return await fetchBrowserJson<BrowserActionTargetOk>(withBaseUrl(baseUrl, `/set/offline${q}`), {
|
|
129
|
+
method: "POST",
|
|
130
|
+
headers: { "Content-Type": "application/json" },
|
|
131
|
+
body: JSON.stringify({ targetId: opts.targetId, offline: opts.offline }),
|
|
132
|
+
timeoutMs: 20000,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export async function browserSetHeaders(
|
|
137
|
+
baseUrl: string | undefined,
|
|
138
|
+
opts: {
|
|
139
|
+
headers: Record<string, string>;
|
|
140
|
+
targetId?: string;
|
|
141
|
+
profile?: string;
|
|
142
|
+
},
|
|
143
|
+
): Promise<BrowserActionTargetOk> {
|
|
144
|
+
const q = buildProfileQuery(opts.profile);
|
|
145
|
+
return await fetchBrowserJson<BrowserActionTargetOk>(withBaseUrl(baseUrl, `/set/headers${q}`), {
|
|
146
|
+
method: "POST",
|
|
147
|
+
headers: { "Content-Type": "application/json" },
|
|
148
|
+
body: JSON.stringify({ targetId: opts.targetId, headers: opts.headers }),
|
|
149
|
+
timeoutMs: 20000,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export async function browserSetHttpCredentials(
|
|
154
|
+
baseUrl: string | undefined,
|
|
155
|
+
opts: {
|
|
156
|
+
username?: string;
|
|
157
|
+
password?: string;
|
|
158
|
+
clear?: boolean;
|
|
159
|
+
targetId?: string;
|
|
160
|
+
profile?: string;
|
|
161
|
+
} = {},
|
|
162
|
+
): Promise<BrowserActionTargetOk> {
|
|
163
|
+
const q = buildProfileQuery(opts.profile);
|
|
164
|
+
return await fetchBrowserJson<BrowserActionTargetOk>(
|
|
165
|
+
withBaseUrl(baseUrl, `/set/credentials${q}`),
|
|
166
|
+
{
|
|
167
|
+
method: "POST",
|
|
168
|
+
headers: { "Content-Type": "application/json" },
|
|
169
|
+
body: JSON.stringify({
|
|
170
|
+
targetId: opts.targetId,
|
|
171
|
+
username: opts.username,
|
|
172
|
+
password: opts.password,
|
|
173
|
+
clear: opts.clear,
|
|
174
|
+
}),
|
|
175
|
+
timeoutMs: 20000,
|
|
176
|
+
},
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export async function browserSetGeolocation(
|
|
181
|
+
baseUrl: string | undefined,
|
|
182
|
+
opts: {
|
|
183
|
+
latitude?: number;
|
|
184
|
+
longitude?: number;
|
|
185
|
+
accuracy?: number;
|
|
186
|
+
origin?: string;
|
|
187
|
+
clear?: boolean;
|
|
188
|
+
targetId?: string;
|
|
189
|
+
profile?: string;
|
|
190
|
+
} = {},
|
|
191
|
+
): Promise<BrowserActionTargetOk> {
|
|
192
|
+
const q = buildProfileQuery(opts.profile);
|
|
193
|
+
return await fetchBrowserJson<BrowserActionTargetOk>(
|
|
194
|
+
withBaseUrl(baseUrl, `/set/geolocation${q}`),
|
|
195
|
+
{
|
|
196
|
+
method: "POST",
|
|
197
|
+
headers: { "Content-Type": "application/json" },
|
|
198
|
+
body: JSON.stringify({
|
|
199
|
+
targetId: opts.targetId,
|
|
200
|
+
latitude: opts.latitude,
|
|
201
|
+
longitude: opts.longitude,
|
|
202
|
+
accuracy: opts.accuracy,
|
|
203
|
+
origin: opts.origin,
|
|
204
|
+
clear: opts.clear,
|
|
205
|
+
}),
|
|
206
|
+
timeoutMs: 20000,
|
|
207
|
+
},
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export async function browserSetMedia(
|
|
212
|
+
baseUrl: string | undefined,
|
|
213
|
+
opts: {
|
|
214
|
+
colorScheme: "dark" | "light" | "no-preference" | "none";
|
|
215
|
+
targetId?: string;
|
|
216
|
+
profile?: string;
|
|
217
|
+
},
|
|
218
|
+
): Promise<BrowserActionTargetOk> {
|
|
219
|
+
const q = buildProfileQuery(opts.profile);
|
|
220
|
+
return await fetchBrowserJson<BrowserActionTargetOk>(withBaseUrl(baseUrl, `/set/media${q}`), {
|
|
221
|
+
method: "POST",
|
|
222
|
+
headers: { "Content-Type": "application/json" },
|
|
223
|
+
body: JSON.stringify({
|
|
224
|
+
targetId: opts.targetId,
|
|
225
|
+
colorScheme: opts.colorScheme,
|
|
226
|
+
}),
|
|
227
|
+
timeoutMs: 20000,
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export async function browserSetTimezone(
|
|
232
|
+
baseUrl: string | undefined,
|
|
233
|
+
opts: { timezoneId: string; targetId?: string; profile?: string },
|
|
234
|
+
): Promise<BrowserActionTargetOk> {
|
|
235
|
+
const q = buildProfileQuery(opts.profile);
|
|
236
|
+
return await fetchBrowserJson<BrowserActionTargetOk>(withBaseUrl(baseUrl, `/set/timezone${q}`), {
|
|
237
|
+
method: "POST",
|
|
238
|
+
headers: { "Content-Type": "application/json" },
|
|
239
|
+
body: JSON.stringify({
|
|
240
|
+
targetId: opts.targetId,
|
|
241
|
+
timezoneId: opts.timezoneId,
|
|
242
|
+
}),
|
|
243
|
+
timeoutMs: 20000,
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export async function browserSetLocale(
|
|
248
|
+
baseUrl: string | undefined,
|
|
249
|
+
opts: { locale: string; targetId?: string; profile?: string },
|
|
250
|
+
): Promise<BrowserActionTargetOk> {
|
|
251
|
+
const q = buildProfileQuery(opts.profile);
|
|
252
|
+
return await fetchBrowserJson<BrowserActionTargetOk>(withBaseUrl(baseUrl, `/set/locale${q}`), {
|
|
253
|
+
method: "POST",
|
|
254
|
+
headers: { "Content-Type": "application/json" },
|
|
255
|
+
body: JSON.stringify({ targetId: opts.targetId, locale: opts.locale }),
|
|
256
|
+
timeoutMs: 20000,
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export async function browserSetDevice(
|
|
261
|
+
baseUrl: string | undefined,
|
|
262
|
+
opts: { name: string; targetId?: string; profile?: string },
|
|
263
|
+
): Promise<BrowserActionTargetOk> {
|
|
264
|
+
const q = buildProfileQuery(opts.profile);
|
|
265
|
+
return await fetchBrowserJson<BrowserActionTargetOk>(withBaseUrl(baseUrl, `/set/device${q}`), {
|
|
266
|
+
method: "POST",
|
|
267
|
+
headers: { "Content-Type": "application/json" },
|
|
268
|
+
body: JSON.stringify({ targetId: opts.targetId, name: opts.name }),
|
|
269
|
+
timeoutMs: 20000,
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export async function browserClearPermissions(
|
|
274
|
+
baseUrl: string | undefined,
|
|
275
|
+
opts: { targetId?: string; profile?: string } = {},
|
|
276
|
+
): Promise<BrowserActionOk> {
|
|
277
|
+
const q = buildProfileQuery(opts.profile);
|
|
278
|
+
return await fetchBrowserJson<BrowserActionOk>(withBaseUrl(baseUrl, `/set/geolocation${q}`), {
|
|
279
|
+
method: "POST",
|
|
280
|
+
headers: { "Content-Type": "application/json" },
|
|
281
|
+
body: JSON.stringify({ targetId: opts.targetId, clear: true }),
|
|
282
|
+
timeoutMs: 20000,
|
|
283
|
+
});
|
|
284
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type BrowserActionOk = { ok: true };
|
|
2
|
+
|
|
3
|
+
export type BrowserActionTabResult = {
|
|
4
|
+
ok: true;
|
|
5
|
+
targetId: string;
|
|
6
|
+
url?: string;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export type BrowserActionPathResult = {
|
|
10
|
+
ok: true;
|
|
11
|
+
path: string;
|
|
12
|
+
targetId: string;
|
|
13
|
+
url?: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type BrowserActionTargetOk = { ok: true; targetId: string };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export function buildProfileQuery(profile?: string): string {
|
|
2
|
+
return profile ? `?profile=${encodeURIComponent(profile)}` : "";
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function withBaseUrl(baseUrl: string | undefined, path: string): string {
|
|
6
|
+
const trimmed = baseUrl?.trim();
|
|
7
|
+
if (!trimmed) {
|
|
8
|
+
return path;
|
|
9
|
+
}
|
|
10
|
+
return `${trimmed.replace(/\/$/, "")}${path}`;
|
|
11
|
+
}
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
import { getBridgeAuthForPort } from "./bridge-auth-registry.js";
|
|
4
|
+
import { resolveBrowserControlAuth } from "./control-auth.js";
|
|
5
|
+
import {
|
|
6
|
+
createBrowserControlContext,
|
|
7
|
+
startBrowserControlServiceFromConfig,
|
|
8
|
+
} from "./control-service.js";
|
|
9
|
+
import { createBrowserRouteDispatcher } from "./routes/dispatcher.js";
|
|
10
|
+
import { formatCliCommand, loadConfig } from "./enterprise-compat.js";
|
|
11
|
+
|
|
12
|
+
type LoopbackBrowserAuthDeps = {
|
|
13
|
+
loadConfig: typeof loadConfig;
|
|
14
|
+
resolveBrowserControlAuth: typeof resolveBrowserControlAuth;
|
|
15
|
+
getBridgeAuthForPort: typeof getBridgeAuthForPort;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
function isAbsoluteHttp(url: string): boolean {
|
|
19
|
+
return /^https?:\/\//i.test(url.trim());
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function isLoopbackHttpUrl(url: string): boolean {
|
|
23
|
+
try {
|
|
24
|
+
const host = new URL(url).hostname.trim().toLowerCase();
|
|
25
|
+
// URL hostnames may keep IPv6 brackets (for example "[::1]"); normalize before checks.
|
|
26
|
+
const normalizedHost = host.startsWith("[") && host.endsWith("]") ? host.slice(1, -1) : host;
|
|
27
|
+
return (
|
|
28
|
+
normalizedHost === "127.0.0.1" || normalizedHost === "localhost" || normalizedHost === "::1"
|
|
29
|
+
);
|
|
30
|
+
} catch {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function withLoopbackBrowserAuthImpl(
|
|
36
|
+
url: string,
|
|
37
|
+
init: (RequestInit & { timeoutMs?: number }) | undefined,
|
|
38
|
+
deps: LoopbackBrowserAuthDeps,
|
|
39
|
+
): RequestInit & { timeoutMs?: number } {
|
|
40
|
+
const headers = new Headers(init?.headers ?? {});
|
|
41
|
+
if (headers.has("authorization") || headers.has("x-openclaw-password")) {
|
|
42
|
+
return { ...init, headers };
|
|
43
|
+
}
|
|
44
|
+
if (!isLoopbackHttpUrl(url)) {
|
|
45
|
+
return { ...init, headers };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
const cfg = deps.loadConfig();
|
|
50
|
+
const auth = deps.resolveBrowserControlAuth(cfg);
|
|
51
|
+
if (auth.token) {
|
|
52
|
+
headers.set("Authorization", `Bearer ${auth.token}`);
|
|
53
|
+
return { ...init, headers };
|
|
54
|
+
}
|
|
55
|
+
if (auth.password) {
|
|
56
|
+
headers.set("x-openclaw-password", auth.password);
|
|
57
|
+
return { ...init, headers };
|
|
58
|
+
}
|
|
59
|
+
} catch {
|
|
60
|
+
// ignore config/auth lookup failures and continue without auth headers
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Sandbox bridge servers can run with per-process ephemeral auth on dynamic ports.
|
|
64
|
+
// Fall back to the in-memory registry if config auth is not available.
|
|
65
|
+
try {
|
|
66
|
+
const parsed = new URL(url);
|
|
67
|
+
const port =
|
|
68
|
+
parsed.port && Number.parseInt(parsed.port, 10) > 0
|
|
69
|
+
? Number.parseInt(parsed.port, 10)
|
|
70
|
+
: parsed.protocol === "https:"
|
|
71
|
+
? 443
|
|
72
|
+
: 80;
|
|
73
|
+
const bridgeAuth = deps.getBridgeAuthForPort(port);
|
|
74
|
+
if (bridgeAuth?.token) {
|
|
75
|
+
headers.set("Authorization", `Bearer ${bridgeAuth.token}`);
|
|
76
|
+
} else if (bridgeAuth?.password) {
|
|
77
|
+
headers.set("x-openclaw-password", bridgeAuth.password);
|
|
78
|
+
}
|
|
79
|
+
} catch {
|
|
80
|
+
// ignore
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return { ...init, headers };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function withLoopbackBrowserAuth(
|
|
87
|
+
url: string,
|
|
88
|
+
init: (RequestInit & { timeoutMs?: number }) | undefined,
|
|
89
|
+
): RequestInit & { timeoutMs?: number } {
|
|
90
|
+
return withLoopbackBrowserAuthImpl(url, init, {
|
|
91
|
+
loadConfig,
|
|
92
|
+
resolveBrowserControlAuth,
|
|
93
|
+
getBridgeAuthForPort,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function enhanceBrowserFetchError(url: string, err: unknown, timeoutMs: number): Error {
|
|
98
|
+
const isLocal = !isAbsoluteHttp(url);
|
|
99
|
+
// Human-facing hint for logs/diagnostics.
|
|
100
|
+
const operatorHint = isLocal
|
|
101
|
+
? `Restart the OpenClaw gateway (OpenClaw.app menubar, or \`${formatCliCommand("openclaw gateway")}\`).`
|
|
102
|
+
: "If this is a sandboxed session, ensure the sandbox browser is running.";
|
|
103
|
+
// Model-facing suffix: explicitly tell the LLM NOT to retry.
|
|
104
|
+
// Without this, models see "try again" and enter an infinite tool-call loop.
|
|
105
|
+
const modelHint =
|
|
106
|
+
"Do NOT retry the browser tool — it will keep failing. " +
|
|
107
|
+
"Use an alternative approach or inform the user that the browser is currently unavailable.";
|
|
108
|
+
const msg = String(err);
|
|
109
|
+
const msgLower = msg.toLowerCase();
|
|
110
|
+
const looksLikeTimeout =
|
|
111
|
+
msgLower.includes("timed out") ||
|
|
112
|
+
msgLower.includes("timeout") ||
|
|
113
|
+
msgLower.includes("aborted") ||
|
|
114
|
+
msgLower.includes("abort") ||
|
|
115
|
+
msgLower.includes("aborterror");
|
|
116
|
+
if (looksLikeTimeout) {
|
|
117
|
+
return new Error(
|
|
118
|
+
`Can't reach the OpenClaw browser control service (timed out after ${timeoutMs}ms). ${operatorHint} ${modelHint}`,
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
return new Error(
|
|
122
|
+
`Can't reach the OpenClaw browser control service. ${operatorHint} ${modelHint} (${msg})`,
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function fetchHttpJson<T>(
|
|
127
|
+
url: string,
|
|
128
|
+
init: RequestInit & { timeoutMs?: number },
|
|
129
|
+
): Promise<T> {
|
|
130
|
+
const timeoutMs = init.timeoutMs ?? 5000;
|
|
131
|
+
const ctrl = new AbortController();
|
|
132
|
+
const upstreamSignal = init.signal;
|
|
133
|
+
let upstreamAbortListener: (() => void) | undefined;
|
|
134
|
+
if (upstreamSignal) {
|
|
135
|
+
if (upstreamSignal.aborted) {
|
|
136
|
+
ctrl.abort(upstreamSignal.reason);
|
|
137
|
+
} else {
|
|
138
|
+
upstreamAbortListener = () => ctrl.abort(upstreamSignal.reason);
|
|
139
|
+
upstreamSignal.addEventListener("abort", upstreamAbortListener, { once: true });
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const t = setTimeout(() => ctrl.abort(new Error("timed out")), timeoutMs);
|
|
144
|
+
try {
|
|
145
|
+
const res = await fetch(url, { ...init, signal: ctrl.signal });
|
|
146
|
+
if (!res.ok) {
|
|
147
|
+
const text = await res.text().catch(() => "");
|
|
148
|
+
throw new Error(text || `HTTP ${res.status}`);
|
|
149
|
+
}
|
|
150
|
+
return (await res.json()) as T;
|
|
151
|
+
} finally {
|
|
152
|
+
clearTimeout(t);
|
|
153
|
+
if (upstreamSignal && upstreamAbortListener) {
|
|
154
|
+
upstreamSignal.removeEventListener("abort", upstreamAbortListener);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export async function fetchBrowserJson<T>(
|
|
160
|
+
url: string,
|
|
161
|
+
init?: RequestInit & { timeoutMs?: number },
|
|
162
|
+
): Promise<T> {
|
|
163
|
+
const timeoutMs = init?.timeoutMs ?? 5000;
|
|
164
|
+
try {
|
|
165
|
+
if (isAbsoluteHttp(url)) {
|
|
166
|
+
const httpInit = withLoopbackBrowserAuth(url, init);
|
|
167
|
+
return await fetchHttpJson<T>(url, { ...httpInit, timeoutMs });
|
|
168
|
+
}
|
|
169
|
+
const started = await startBrowserControlServiceFromConfig();
|
|
170
|
+
if (!started) {
|
|
171
|
+
throw new Error("browser control disabled");
|
|
172
|
+
}
|
|
173
|
+
const dispatcher = createBrowserRouteDispatcher(createBrowserControlContext());
|
|
174
|
+
const parsed = new URL(url, "http://localhost");
|
|
175
|
+
const query: Record<string, unknown> = {};
|
|
176
|
+
for (const [key, value] of parsed.searchParams.entries()) {
|
|
177
|
+
query[key] = value;
|
|
178
|
+
}
|
|
179
|
+
let body = init?.body;
|
|
180
|
+
if (typeof body === "string") {
|
|
181
|
+
try {
|
|
182
|
+
body = JSON.parse(body);
|
|
183
|
+
} catch {
|
|
184
|
+
// keep as string
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const abortCtrl = new AbortController();
|
|
189
|
+
const upstreamSignal = init?.signal;
|
|
190
|
+
let upstreamAbortListener: (() => void) | undefined;
|
|
191
|
+
if (upstreamSignal) {
|
|
192
|
+
if (upstreamSignal.aborted) {
|
|
193
|
+
abortCtrl.abort(upstreamSignal.reason);
|
|
194
|
+
} else {
|
|
195
|
+
upstreamAbortListener = () => abortCtrl.abort(upstreamSignal.reason);
|
|
196
|
+
upstreamSignal.addEventListener("abort", upstreamAbortListener, { once: true });
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
let abortListener: (() => void) | undefined;
|
|
201
|
+
const abortPromise: Promise<never> = abortCtrl.signal.aborted
|
|
202
|
+
? Promise.reject(abortCtrl.signal.reason ?? new Error("aborted"))
|
|
203
|
+
: new Promise((_, reject) => {
|
|
204
|
+
abortListener = () => reject(abortCtrl.signal.reason ?? new Error("aborted"));
|
|
205
|
+
abortCtrl.signal.addEventListener("abort", abortListener, { once: true });
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
209
|
+
if (timeoutMs) {
|
|
210
|
+
timer = setTimeout(() => abortCtrl.abort(new Error("timed out")), timeoutMs);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const dispatchPromise = dispatcher.dispatch({
|
|
214
|
+
method:
|
|
215
|
+
init?.method?.toUpperCase() === "DELETE"
|
|
216
|
+
? "DELETE"
|
|
217
|
+
: init?.method?.toUpperCase() === "POST"
|
|
218
|
+
? "POST"
|
|
219
|
+
: "GET",
|
|
220
|
+
path: parsed.pathname,
|
|
221
|
+
query,
|
|
222
|
+
body,
|
|
223
|
+
signal: abortCtrl.signal,
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
const result = await Promise.race([dispatchPromise, abortPromise]).finally(() => {
|
|
227
|
+
if (timer) {
|
|
228
|
+
clearTimeout(timer);
|
|
229
|
+
}
|
|
230
|
+
if (abortListener) {
|
|
231
|
+
abortCtrl.signal.removeEventListener("abort", abortListener);
|
|
232
|
+
}
|
|
233
|
+
if (upstreamSignal && upstreamAbortListener) {
|
|
234
|
+
upstreamSignal.removeEventListener("abort", upstreamAbortListener);
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
if (result.status >= 400) {
|
|
239
|
+
const message =
|
|
240
|
+
result.body && typeof result.body === "object" && "error" in result.body
|
|
241
|
+
? String((result.body as { error?: unknown }).error)
|
|
242
|
+
: `HTTP ${result.status}`;
|
|
243
|
+
throw new Error(message);
|
|
244
|
+
}
|
|
245
|
+
return result.body as T;
|
|
246
|
+
} catch (err) {
|
|
247
|
+
throw enhanceBrowserFetchError(url, err, timeoutMs);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export const __test = {
|
|
252
|
+
withLoopbackBrowserAuth: withLoopbackBrowserAuthImpl,
|
|
253
|
+
};
|