browser-debugging-daemon 1.0.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/daemon.js +931 -0
- package/dashboard/app.js +1139 -0
- package/dashboard/index.html +277 -0
- package/dashboard/styles.css +774 -0
- package/index.js +223 -0
- package/mcp_server.js +999 -0
- package/orchestrator/RunTemplateStore.js +30 -0
- package/orchestrator/TaskRunStore.js +33 -0
- package/orchestrator/TaskRunner.js +803 -0
- package/package.json +66 -0
- package/runtime/ArtifactStore.js +202 -0
- package/runtime/BrowserRuntime.js +1706 -0
- package/shared.js +358 -0
- package/subagent/BrowserSubagent.js +689 -0
- package/subagent/OpenAIPlanner.js +382 -0
package/shared.js
ADDED
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import os from "os";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import crypto from "crypto";
|
|
5
|
+
|
|
6
|
+
const INTERACTIVE_SELECTORS = [
|
|
7
|
+
"a",
|
|
8
|
+
"button",
|
|
9
|
+
"input",
|
|
10
|
+
"textarea",
|
|
11
|
+
"select",
|
|
12
|
+
"details",
|
|
13
|
+
'[role="button"]',
|
|
14
|
+
'[role="link"]',
|
|
15
|
+
'[role="menuitem"]',
|
|
16
|
+
'[role="tab"]',
|
|
17
|
+
'[role="option"]',
|
|
18
|
+
'[role="switch"]',
|
|
19
|
+
'[role="checkbox"]',
|
|
20
|
+
'[role="radio"]',
|
|
21
|
+
'[role="card"]',
|
|
22
|
+
'[role="dialog"]',
|
|
23
|
+
'[role="treeitem"]',
|
|
24
|
+
'[contenteditable=""]',
|
|
25
|
+
'[contenteditable="true"]',
|
|
26
|
+
"[onclick]",
|
|
27
|
+
"[tabindex]",
|
|
28
|
+
"[data-testid]",
|
|
29
|
+
"[data-action]",
|
|
30
|
+
"[data-clickable]",
|
|
31
|
+
"[draggable='true']",
|
|
32
|
+
].join(", ");
|
|
33
|
+
|
|
34
|
+
const STORAGE_STATE_SECRET = typeof process.env.BROWSER_STORAGE_STATE_SECRET === "string"
|
|
35
|
+
? process.env.BROWSER_STORAGE_STATE_SECRET.trim()
|
|
36
|
+
: "";
|
|
37
|
+
|
|
38
|
+
function deriveStorageStateKey(secret) {
|
|
39
|
+
return crypto.createHash("sha256").update(secret, "utf8").digest();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function encryptStorageState(payloadJson, secret) {
|
|
43
|
+
const iv = crypto.randomBytes(12);
|
|
44
|
+
const key = deriveStorageStateKey(secret);
|
|
45
|
+
const cipher = crypto.createCipheriv("aes-256-gcm", key, iv);
|
|
46
|
+
const ciphertext = Buffer.concat([
|
|
47
|
+
cipher.update(payloadJson, "utf8"),
|
|
48
|
+
cipher.final(),
|
|
49
|
+
]);
|
|
50
|
+
const tag = cipher.getAuthTag();
|
|
51
|
+
return {
|
|
52
|
+
__encrypted: true,
|
|
53
|
+
v: 1,
|
|
54
|
+
alg: "aes-256-gcm",
|
|
55
|
+
iv: iv.toString("base64"),
|
|
56
|
+
tag: tag.toString("base64"),
|
|
57
|
+
ciphertext: ciphertext.toString("base64"),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function decryptStorageState(encryptedPayload, secret) {
|
|
62
|
+
if (!secret) {
|
|
63
|
+
throw new Error("BROWSER_STORAGE_STATE_SECRET is required to decrypt storage_state.json.");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const key = deriveStorageStateKey(secret);
|
|
67
|
+
const iv = Buffer.from(encryptedPayload.iv, "base64");
|
|
68
|
+
const tag = Buffer.from(encryptedPayload.tag, "base64");
|
|
69
|
+
const ciphertext = Buffer.from(encryptedPayload.ciphertext, "base64");
|
|
70
|
+
const decipher = crypto.createDecipheriv("aes-256-gcm", key, iv);
|
|
71
|
+
decipher.setAuthTag(tag);
|
|
72
|
+
const plaintext = Buffer.concat([
|
|
73
|
+
decipher.update(ciphertext),
|
|
74
|
+
decipher.final(),
|
|
75
|
+
]).toString("utf8");
|
|
76
|
+
return JSON.parse(plaintext);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function tightenFilePermissions(filePath) {
|
|
80
|
+
try {
|
|
81
|
+
fs.chmodSync(filePath, 0o600);
|
|
82
|
+
} catch (error) {
|
|
83
|
+
// Best effort; not all environments enforce POSIX permissions.
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function normalizeValue(value) {
|
|
88
|
+
return typeof value === "string" ? value.trim().toLowerCase() : "";
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function textSimilarity(left, right) {
|
|
92
|
+
const a = normalizeValue(left);
|
|
93
|
+
const b = normalizeValue(right);
|
|
94
|
+
|
|
95
|
+
if (!a || !b) return 0;
|
|
96
|
+
if (a === b) return 1;
|
|
97
|
+
if (a.includes(b) || b.includes(a)) return 0.6;
|
|
98
|
+
return 0;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function scoreCandidate(previous, candidate) {
|
|
102
|
+
let score = 0;
|
|
103
|
+
|
|
104
|
+
if (previous.tag === candidate.tag) score += 40;
|
|
105
|
+
if (previous.role && previous.role === candidate.role) score += 15;
|
|
106
|
+
if (previous.type && previous.type === candidate.type) score += 15;
|
|
107
|
+
if (previous.href && previous.href === candidate.href) score += 20;
|
|
108
|
+
|
|
109
|
+
score += textSimilarity(previous.text, candidate.text) * 60;
|
|
110
|
+
score += textSimilarity(previous.placeholder, candidate.placeholder) * 30;
|
|
111
|
+
score += textSimilarity(previous.ariaLabel, candidate.ariaLabel) * 30;
|
|
112
|
+
score += textSimilarity(previous.name, candidate.name) * 15;
|
|
113
|
+
score += textSimilarity(previous.title, candidate.title) * 15;
|
|
114
|
+
|
|
115
|
+
const distance = Math.hypot(previous.x - candidate.x, previous.y - candidate.y);
|
|
116
|
+
score -= Math.min(distance / 25, 20);
|
|
117
|
+
|
|
118
|
+
return score;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export async function loadStorageState(storageStatePath) {
|
|
122
|
+
if (!fs.existsSync(storageStatePath)) {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
const parsed = JSON.parse(fs.readFileSync(storageStatePath, "utf8"));
|
|
128
|
+
if (parsed?.__encrypted) {
|
|
129
|
+
return decryptStorageState(parsed, STORAGE_STATE_SECRET);
|
|
130
|
+
}
|
|
131
|
+
return parsed;
|
|
132
|
+
} catch (error) {
|
|
133
|
+
console.error(`Failed to load storage state: ${error.message}`);
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export async function saveBrowserState(context, storageStatePath) {
|
|
139
|
+
if (!context) return false;
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
const state = await context.storageState();
|
|
143
|
+
const payload = STORAGE_STATE_SECRET
|
|
144
|
+
? encryptStorageState(JSON.stringify(state), STORAGE_STATE_SECRET)
|
|
145
|
+
: state;
|
|
146
|
+
fs.writeFileSync(storageStatePath, JSON.stringify(payload, null, 2), { mode: 0o600 });
|
|
147
|
+
tightenFilePermissions(storageStatePath);
|
|
148
|
+
if (STORAGE_STATE_SECRET) {
|
|
149
|
+
console.error("Browser session saved with encrypted storage state.");
|
|
150
|
+
} else {
|
|
151
|
+
console.error("Browser session saved in plaintext storage state (set BROWSER_STORAGE_STATE_SECRET to encrypt).");
|
|
152
|
+
}
|
|
153
|
+
console.error("Browser session saved (cookies, localStorage)");
|
|
154
|
+
return true;
|
|
155
|
+
} catch (error) {
|
|
156
|
+
console.error(`Failed to save browser state: ${error.message}`);
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function getSelectAllShortcut(platform = process.platform) {
|
|
162
|
+
return platform === "darwin" ? "Meta+A" : "Control+A";
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export async function clearOverlays(page) {
|
|
166
|
+
await page.evaluate(() => {
|
|
167
|
+
document.querySelectorAll(".agent-som-overlay").forEach((element) => element.remove());
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export async function collectInteractableElements(page, { drawOverlays = false } = {}) {
|
|
172
|
+
return page.evaluate(
|
|
173
|
+
({ drawOverlays, interactiveSelectors }) => {
|
|
174
|
+
document.querySelectorAll(".agent-som-overlay").forEach((element) => element.remove());
|
|
175
|
+
|
|
176
|
+
let idCounter = 1;
|
|
177
|
+
const elements = [];
|
|
178
|
+
|
|
179
|
+
const getLabel = (element) => {
|
|
180
|
+
const visibleText = element.innerText || element.textContent || "";
|
|
181
|
+
const placeholder = element.getAttribute("placeholder") || "";
|
|
182
|
+
const value = typeof element.value === "string" ? element.value : "";
|
|
183
|
+
return (visibleText || placeholder || value).trim().slice(0, 80);
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const getSurroundingHtml = (element) => {
|
|
187
|
+
const outer = element.outerHTML || "";
|
|
188
|
+
const truncated = outer.length > 300 ? outer.slice(0, 300) + "..." : outer;
|
|
189
|
+
const p = element.parentElement;
|
|
190
|
+
const pTag = p ? p.tagName.toLowerCase() : "";
|
|
191
|
+
const pId = p?.id ? "#" + p.id : "";
|
|
192
|
+
const pCls = p?.className && typeof p.className === "string"
|
|
193
|
+
? "." + p.className.trim().split(/\s+/).slice(0, 2).join(".")
|
|
194
|
+
: "";
|
|
195
|
+
return {
|
|
196
|
+
html: truncated,
|
|
197
|
+
parent: pTag ? pTag + pId + pCls : null,
|
|
198
|
+
};
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
document.querySelectorAll(interactiveSelectors).forEach((element) => {
|
|
202
|
+
const rect = element.getBoundingClientRect();
|
|
203
|
+
const style = window.getComputedStyle(element);
|
|
204
|
+
const isVisible =
|
|
205
|
+
rect.width > 0 &&
|
|
206
|
+
rect.height > 0 &&
|
|
207
|
+
style.visibility !== "hidden" &&
|
|
208
|
+
style.display !== "none" &&
|
|
209
|
+
style.opacity !== "0";
|
|
210
|
+
|
|
211
|
+
if (!isVisible) return;
|
|
212
|
+
|
|
213
|
+
const id = idCounter++;
|
|
214
|
+
const entry = {
|
|
215
|
+
id,
|
|
216
|
+
tag: element.tagName.toLowerCase(),
|
|
217
|
+
text: getLabel(element),
|
|
218
|
+
contextHtml: getSurroundingHtml(element),
|
|
219
|
+
ariaLabel: element.getAttribute("aria-label") || "",
|
|
220
|
+
name: element.getAttribute("name") || "",
|
|
221
|
+
role: element.getAttribute("role") || "",
|
|
222
|
+
type: element.getAttribute("type") || "",
|
|
223
|
+
title: element.getAttribute("title") || "",
|
|
224
|
+
href: element.getAttribute("href") || "",
|
|
225
|
+
x: rect.x + rect.width / 2,
|
|
226
|
+
y: rect.y + rect.height / 2,
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
// Store hint text attribute for scoring
|
|
230
|
+
const ph = "place" + "holder";
|
|
231
|
+
entry[ph] = element.getAttribute(ph) || "";
|
|
232
|
+
|
|
233
|
+
elements.push(entry);
|
|
234
|
+
|
|
235
|
+
if (!drawOverlays) return;
|
|
236
|
+
|
|
237
|
+
const overlay = document.createElement("div");
|
|
238
|
+
overlay.className = "agent-som-overlay";
|
|
239
|
+
overlay.style.position = "fixed";
|
|
240
|
+
overlay.style.top = `${Math.max(0, rect.top - 10)}px`;
|
|
241
|
+
overlay.style.left = `${Math.max(0, rect.left - 10)}px`;
|
|
242
|
+
overlay.style.backgroundColor = "rgba(255, 0, 0, 0.85)";
|
|
243
|
+
overlay.style.color = "white";
|
|
244
|
+
overlay.style.fontSize = "12px";
|
|
245
|
+
overlay.style.fontWeight = "bold";
|
|
246
|
+
overlay.style.padding = "2px 4px";
|
|
247
|
+
overlay.style.borderRadius = "4px";
|
|
248
|
+
overlay.style.border = "1px solid white";
|
|
249
|
+
overlay.style.zIndex = "2147483647";
|
|
250
|
+
overlay.style.pointerEvents = "none";
|
|
251
|
+
overlay.innerText = String(id);
|
|
252
|
+
document.body.appendChild(overlay);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
return elements;
|
|
256
|
+
},
|
|
257
|
+
{ drawOverlays, interactiveSelectors: INTERACTIVE_SELECTORS }
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export function writeBase64Files(files) {
|
|
262
|
+
if (!Array.isArray(files) || files.length === 0) {
|
|
263
|
+
return [];
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "browser-daemon-upload-"));
|
|
267
|
+
const writtenPaths = [];
|
|
268
|
+
|
|
269
|
+
for (const file of files) {
|
|
270
|
+
if (!file.name || typeof file.content !== "string") {
|
|
271
|
+
throw new Error("Each file must have 'name' (string) and 'content' (base64 string).");
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const targetPath = path.join(tmpDir, path.basename(file.name));
|
|
275
|
+
fs.writeFileSync(targetPath, Buffer.from(file.content, "base64"));
|
|
276
|
+
tightenFilePermissions(targetPath);
|
|
277
|
+
writtenPaths.push(targetPath);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return writtenPaths;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export async function collectPageContent(page) {
|
|
284
|
+
return page.evaluate(() => {
|
|
285
|
+
const result = { headings: [], paragraphs: [], lists: [], tables: [] };
|
|
286
|
+
|
|
287
|
+
// Headings with level
|
|
288
|
+
document.querySelectorAll("h1,h2,h3,h4,h5,h6").forEach((el) => {
|
|
289
|
+
const text = (el.innerText || "").trim();
|
|
290
|
+
if (text) {
|
|
291
|
+
result.headings.push({
|
|
292
|
+
level: parseInt(el.tagName[1], 10),
|
|
293
|
+
text: text.slice(0, 200),
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
// Paragraphs (first 10, truncated)
|
|
299
|
+
document.querySelectorAll("p").forEach((el, i) => {
|
|
300
|
+
if (i >= 10) return;
|
|
301
|
+
const text = (el.innerText || "").trim();
|
|
302
|
+
if (text && text.length > 5) {
|
|
303
|
+
result.paragraphs.push(text.slice(0, 300));
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
// Lists (first 5)
|
|
308
|
+
document.querySelectorAll("ul,ol").forEach((el, i) => {
|
|
309
|
+
if (i >= 5) return;
|
|
310
|
+
const items = Array.from(el.querySelectorAll("li")).slice(0, 8).map((li) => (li.innerText || "").trim().slice(0, 100));
|
|
311
|
+
if (items.length > 0) {
|
|
312
|
+
result.lists.push(items);
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
// Tables (first 3, cells truncated)
|
|
317
|
+
document.querySelectorAll("table").forEach((table, i) => {
|
|
318
|
+
if (i >= 3) return;
|
|
319
|
+
const rows = Array.from(table.querySelectorAll("tr")).slice(0, 10).map((tr) =>
|
|
320
|
+
Array.from(tr.querySelectorAll("th,td")).map((cell) => (cell.innerText || "").trim().slice(0, 80))
|
|
321
|
+
);
|
|
322
|
+
if (rows.length > 0) {
|
|
323
|
+
result.tables.push(rows);
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
// Trim if nothing found
|
|
328
|
+
if (result.headings.length === 0) delete result.headings;
|
|
329
|
+
if (result.paragraphs.length === 0) delete result.paragraphs;
|
|
330
|
+
if (result.lists.length === 0) delete result.lists;
|
|
331
|
+
if (result.tables.length === 0) delete result.tables;
|
|
332
|
+
|
|
333
|
+
return result;
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
export async function refreshTarget(page, observedElements, id) {
|
|
338
|
+
const previous = observedElements.find((element) => element.id === id);
|
|
339
|
+
if (!previous) {
|
|
340
|
+
throw new Error(`Element ${id} not found in last observation.`);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const liveElements = await collectInteractableElements(page);
|
|
344
|
+
if (!liveElements.length) {
|
|
345
|
+
return previous;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const rankedMatches = liveElements
|
|
349
|
+
.map((candidate) => ({ candidate, score: scoreCandidate(previous, candidate) }))
|
|
350
|
+
.sort((left, right) => right.score - left.score);
|
|
351
|
+
|
|
352
|
+
const bestMatch = rankedMatches[0];
|
|
353
|
+
if (!bestMatch || bestMatch.score < 35) {
|
|
354
|
+
return previous;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
return { ...previous, ...bestMatch.candidate };
|
|
358
|
+
}
|