machine-bridge-mcp 0.14.0 → 0.16.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.
Files changed (50) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.md +18 -8
  3. package/SECURITY.md +10 -3
  4. package/browser-extension/browser-operations.js +515 -0
  5. package/browser-extension/devtools-input.js +17 -2
  6. package/browser-extension/manifest.json +2 -2
  7. package/browser-extension/page-automation.js +245 -60
  8. package/browser-extension/pairing.js +1 -1
  9. package/browser-extension/service-worker.js +151 -354
  10. package/docs/ARCHITECTURE.md +6 -4
  11. package/docs/AUDIT.md +24 -1
  12. package/docs/LOCAL_AUTOMATION.md +10 -10
  13. package/docs/LOGGING.md +7 -1
  14. package/docs/OPERATIONS.md +13 -5
  15. package/docs/POLICY_REFERENCE.md +88 -0
  16. package/docs/TESTING.md +9 -2
  17. package/package.json +16 -3
  18. package/scripts/coverage-check.mjs +108 -0
  19. package/scripts/generate-policy-reference.mjs +95 -0
  20. package/src/local/agent-context.mjs +1 -103
  21. package/src/local/app-automation.mjs +7 -9
  22. package/src/local/browser-bridge.mjs +69 -143
  23. package/src/local/browser-extension-protocol.mjs +75 -0
  24. package/src/local/browser-pairing-store.mjs +83 -0
  25. package/src/local/call-registry.mjs +113 -0
  26. package/src/local/capability-ranking.mjs +103 -0
  27. package/src/local/cli-local-admin.mjs +308 -0
  28. package/src/local/cli-options.mjs +151 -0
  29. package/src/local/cli-policy.mjs +77 -0
  30. package/src/local/cli.mjs +16 -507
  31. package/src/local/errors.mjs +122 -0
  32. package/src/local/git-service.mjs +88 -0
  33. package/src/local/lifecycle.mjs +64 -0
  34. package/src/local/log.mjs +50 -19
  35. package/src/local/managed-job-plan.mjs +235 -0
  36. package/src/local/managed-jobs.mjs +16 -220
  37. package/src/local/observability.mjs +83 -0
  38. package/src/local/policy.mjs +148 -0
  39. package/src/local/process-execution.mjs +153 -0
  40. package/src/local/process-sessions.mjs +10 -20
  41. package/src/local/process-tracker.mjs +55 -0
  42. package/src/local/runtime.mjs +154 -672
  43. package/src/local/service.mjs +8 -3
  44. package/src/local/stdio.mjs +3 -11
  45. package/src/local/tool-executor.mjs +102 -0
  46. package/src/local/tools.mjs +21 -104
  47. package/src/local/workspace-file-service.mjs +451 -0
  48. package/src/shared/policy-contract.json +54 -0
  49. package/src/shared/tool-catalog.json +11 -11
  50. package/src/worker/index.ts +69 -524
@@ -0,0 +1,515 @@
1
+ (() => {
2
+ if (globalThis.__machineBridgeBrowserOperations) return;
3
+
4
+ const MAX_ACCESSIBLE_FRAMES = 64;
5
+
6
+ async function dispatch(method, params, state) {
7
+ if (method === "list_tabs") return listTabs(params);
8
+ if (method === "manage_tabs") return manageTabs(params);
9
+ if (method === "wait") return browserWait(params, state);
10
+ if (method === "get_source") return getSource(params, state);
11
+ if (method === "inspect_page") return inspectPage(params, state);
12
+ if (method === "action") return browserAction(params, state);
13
+ if (method === "fill_form") return fillForm(params, state);
14
+ if (method === "upload_files") return uploadFiles(params);
15
+ if (method === "screenshot") return screenshot(params);
16
+ throw new Error(`unknown browser method: ${method}`);
17
+ }
18
+
19
+ async function listTabs(params) {
20
+ const query = params.currentWindow ? { currentWindow: true } : {};
21
+ const tabs = await chrome.tabs.query(query);
22
+ return {
23
+ tabs: tabs
24
+ .filter((tab) => params.includePinned !== false || !tab.pinned)
25
+ .map((tab) => ({
26
+ id: tab.id,
27
+ window_id: tab.windowId,
28
+ active: tab.active,
29
+ pinned: tab.pinned,
30
+ audible: tab.audible,
31
+ discarded: tab.discarded,
32
+ status: tab.status,
33
+ title: tab.title || "",
34
+ url: tab.url || "",
35
+ })),
36
+ };
37
+ }
38
+
39
+
40
+ async function manageTabs(params) {
41
+ if (params.action === "new") {
42
+ const tab = await chrome.tabs.create({ url: params.url || "about:blank", active: params.active !== false });
43
+ return { action: "new", ...publicTab(tab) };
44
+ }
45
+ const tab = await chrome.tabs.get(params.tabId);
46
+ if (params.action === "activate") {
47
+ await chrome.tabs.update(tab.id, { active: true });
48
+ await chrome.windows.update(tab.windowId, { focused: true });
49
+ return { action: "activate", ...publicTab(await chrome.tabs.get(tab.id)) };
50
+ }
51
+ if (params.action === "close") {
52
+ const result = { action: "close", closed: true, ...publicTab(tab) };
53
+ await chrome.tabs.remove(tab.id);
54
+ return result;
55
+ }
56
+ throw new Error("unsupported browser tab action");
57
+ }
58
+
59
+ async function browserWait(params, state) {
60
+ const tab = await resolveTab(params.tabId);
61
+ const deadline = Date.now() + Math.max(1, Number(params.timeoutMs) || 30000);
62
+ let last = null;
63
+ while (Date.now() <= deadline) {
64
+ throwIfCancelled(state);
65
+ const current = await chrome.tabs.get(tab.id);
66
+ const urlMatched = !params.urlContains || String(current.url || "").includes(params.urlContains);
67
+ const needsPage = Boolean(params.selector || params.text || params.loadState);
68
+ let page = { matched: true, ready_state: current.status || "" };
69
+ if (needsPage) {
70
+ try {
71
+ assertPageTab(current);
72
+ const [execution] = await executePageAutomation(scriptTarget(current.id, params.frameId, false), "checkWait", params);
73
+ page = execution?.result || { matched: false };
74
+ } catch (error) {
75
+ const message = String(error?.message || error).replace(/\s+/g, " ").slice(0, 500);
76
+ if (message.includes("invalid CSS selector") || message.includes("selector matched")) throw new Error(message);
77
+ page = { matched: false, error: message };
78
+ }
79
+ }
80
+ last = { url_matched: urlMatched, ...page };
81
+ if (urlMatched && page.matched === true) {
82
+ return { ok: true, tab_id: current.id, title: current.title || "", url: current.url || "", condition: last };
83
+ }
84
+ await delay(200);
85
+ }
86
+ throw new Error(`browser wait timed out; last condition: ${JSON.stringify(last || {})}`);
87
+ }
88
+ function scriptTarget(tabId, frameId, allFrames) {
89
+ if (Number.isInteger(frameId)) return { tabId, frameIds: [frameId] };
90
+ if (allFrames) return { tabId, allFrames: true };
91
+ return { tabId };
92
+ }
93
+
94
+ async function resolveTab(tabId) {
95
+ if (Number.isInteger(tabId)) return chrome.tabs.get(tabId);
96
+ const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
97
+ if (!tab?.id) throw new Error("no active browser tab");
98
+ return tab;
99
+ }
100
+
101
+ async function getSource(params, state) {
102
+ const tab = await resolveTab(params.tabId);
103
+ assertPageTab(tab);
104
+ const maxBytes = Math.max(1, Number(params.maxBytes) || 1024 * 1024);
105
+ const selection = await discoverPageFrames(tab.id, params.frameId, params.allFrames === true);
106
+ let remainingBytes = maxBytes;
107
+ const frames = [];
108
+ for (let index = 0; index < selection.frames.length && remainingBytes > 0; index += 1) {
109
+ throwIfCancelled(state);
110
+ const remainingFrames = selection.frames.length - index;
111
+ const frameBudget = Math.max(1, Math.floor(remainingBytes / remainingFrames));
112
+ const [execution] = await chrome.scripting.executeScript({
113
+ target: { tabId: tab.id, frameIds: [selection.frames[index].frameId] },
114
+ func: boundedDocumentSource,
115
+ args: [frameBudget],
116
+ });
117
+ const result = execution?.result || { source: "", bytes: 0, returned_bytes: 0, truncated: true, url: selection.frames[index].url };
118
+ const returnedBytes = Math.max(0, Math.min(frameBudget, Number(result.returned_bytes ?? result.bytes) || 0));
119
+ remainingBytes -= returnedBytes;
120
+ frames.push({ frame_id: execution?.frameId ?? selection.frames[index].frameId, ...result, returned_bytes: returnedBytes, bytes: returnedBytes });
121
+ }
122
+ return {
123
+ tab_id: tab.id,
124
+ title: tab.title || "",
125
+ url: tab.url || "",
126
+ frames,
127
+ returned_bytes: maxBytes - remainingBytes,
128
+ max_bytes: maxBytes,
129
+ frames_truncated: selection.truncated || frames.length < selection.frames.length,
130
+ };
131
+ }
132
+
133
+ async function inspectPage(params, state) {
134
+ const tab = await resolveTab(params.tabId);
135
+ assertPageTab(tab);
136
+ const maxElements = Math.max(1, Number(params.maxElements) || 300);
137
+ const selection = await discoverPageFrames(tab.id, params.frameId, params.allFrames === true, maxElements);
138
+ let remainingElements = maxElements;
139
+ const frames = [];
140
+ for (let index = 0; index < selection.frames.length && remainingElements > 0; index += 1) {
141
+ throwIfCancelled(state);
142
+ const remainingFrames = selection.frames.length - index;
143
+ const frameBudget = Math.max(1, Math.floor(remainingElements / remainingFrames));
144
+ const [execution] = await executePageAutomation(
145
+ { tabId: tab.id, frameIds: [selection.frames[index].frameId] },
146
+ "inspect",
147
+ { maxElements: frameBudget, includeValues: params.includeValues === true },
148
+ );
149
+ const result = execution?.result || { elements: [], truncated: true };
150
+ const used = Math.min(frameBudget, Array.isArray(result.elements) ? result.elements.length : 0);
151
+ remainingElements -= used;
152
+ frames.push({ frame_id: execution?.frameId ?? selection.frames[index].frameId, ...result });
153
+ }
154
+ return {
155
+ tab_id: tab.id,
156
+ title: tab.title || "",
157
+ url: tab.url || "",
158
+ frames,
159
+ total_elements: maxElements - remainingElements,
160
+ max_elements: maxElements,
161
+ frames_truncated: selection.truncated,
162
+ };
163
+ }
164
+
165
+ async function discoverPageFrames(tabId, frameId, allFrames, resultBudget = MAX_ACCESSIBLE_FRAMES) {
166
+ if (Number.isInteger(frameId)) return { frames: [{ frameId, url: "" }], truncated: false };
167
+ if (!allFrames) return { frames: [{ frameId: 0, url: "" }], truncated: false };
168
+ const executions = await chrome.scripting.executeScript({
169
+ target: { tabId, allFrames: true },
170
+ func: () => ({ url: String(location.href || "").slice(0, 8192) }),
171
+ });
172
+ const ordered = executions
173
+ .filter((item) => Number.isInteger(item.frameId))
174
+ .map((item) => ({ frameId: item.frameId, url: item.result?.url || "" }))
175
+ .sort((left, right) => (left.frameId === 0 ? -1 : right.frameId === 0 ? 1 : left.frameId - right.frameId));
176
+ const limit = Math.max(1, Math.min(MAX_ACCESSIBLE_FRAMES, Number(resultBudget) || 1));
177
+ return { frames: ordered.slice(0, limit), truncated: ordered.length > limit };
178
+ }
179
+
180
+ function boundedDocumentSource(limit) {
181
+ const maxBytes = Math.max(1, Number(limit) || 1);
182
+ const encoder = new TextEncoder();
183
+ const decoder = new TextDecoder();
184
+ const chunks = [];
185
+ const VOID_ELEMENTS = new Set(["area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source", "track", "wbr"]);
186
+ const MAX_NODES = 100000;
187
+ const MAX_ATTRIBUTES = 256;
188
+ const MAX_ATTRIBUTE_CHARS = 4096;
189
+ const MAX_TEXT_CHARS = 32768;
190
+ let returnedBytes = 0;
191
+ let budgetExhausted = false;
192
+ let safetyTruncated = false;
193
+ let visitedNodes = 0;
194
+ let openShadowRoots = 0;
195
+
196
+ const append = (raw) => {
197
+ if (budgetExhausted) return false;
198
+ let text = String(raw || "");
199
+ const remaining = maxBytes - returnedBytes;
200
+ if (remaining <= 0) { budgetExhausted = true; return false; }
201
+ const maxChars = Math.max(1024, remaining);
202
+ if (text.length > maxChars) { text = text.slice(0, maxChars); safetyTruncated = true; }
203
+ const bytes = encoder.encode(text);
204
+ if (bytes.byteLength <= remaining) {
205
+ chunks.push(text);
206
+ returnedBytes += bytes.byteLength;
207
+ return true;
208
+ }
209
+ chunks.push(decoder.decode(bytes.slice(0, remaining)));
210
+ returnedBytes = maxBytes;
211
+ budgetExhausted = true;
212
+ return false;
213
+ };
214
+ const bounded = (value, maxChars) => {
215
+ const text = String(value || "");
216
+ if (text.length > maxChars) safetyTruncated = true;
217
+ return text.slice(0, maxChars);
218
+ };
219
+ const escapeText = (value) => bounded(value, MAX_TEXT_CHARS).replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
220
+ const escapeAttribute = (value) => bounded(value, MAX_ATTRIBUTE_CHARS).replaceAll("&", "&amp;").replaceAll('"', "&quot;");
221
+
222
+ if (document.doctype) append(`<!DOCTYPE ${bounded(document.doctype.name || "html", 100)}>\n`);
223
+ const stack = [...document.childNodes].reverse().map((node) => ({ node }));
224
+ while (stack.length && !budgetExhausted) {
225
+ if (visitedNodes >= MAX_NODES) { safetyTruncated = true; break; }
226
+ const item = stack.pop();
227
+ if (item.raw) { append(item.raw); continue; }
228
+ if (item.close) { append(item.close); continue; }
229
+ const node = item.node;
230
+ if (!node || node.nodeType === 10) continue;
231
+ visitedNodes += 1;
232
+ if (node.nodeType === 1) {
233
+ const tag = bounded(node.tagName || "div", 100).toLowerCase();
234
+ if (!append(`<${tag}`)) break;
235
+ const attributes = node.attributes || [];
236
+ const attributeCount = Math.min(Number(attributes.length) || 0, MAX_ATTRIBUTES);
237
+ for (let attributeIndex = 0; attributeIndex < attributeCount; attributeIndex += 1) {
238
+ const attribute = attributes[attributeIndex] || attributes.item?.(attributeIndex);
239
+ if (!attribute) continue;
240
+ if (!append(` ${bounded(attribute.name, 200)}="${escapeAttribute(attribute.value)}"`)) break;
241
+ }
242
+ if ((Number(attributes.length) || 0) > MAX_ATTRIBUTES) safetyTruncated = true;
243
+ if (!append(">")) break;
244
+ if (VOID_ELEMENTS.has(tag)) continue;
245
+ stack.push({ close: `</${tag}>` });
246
+ const shadowChildren = node.shadowRoot?.childNodes || [];
247
+ if ((Number(shadowChildren.length) || 0) > 0) {
248
+ openShadowRoots += 1;
249
+ stack.push({ raw: "</template>" });
250
+ for (let index = (Number(shadowChildren.length) || 0) - 1; index >= 0; index -= 1) stack.push({ node: shadowChildren[index] || shadowChildren.item?.(index) });
251
+ stack.push({ raw: '<template data-machine-bridge-shadow-root="open">' });
252
+ }
253
+ const children = node.content?.childNodes || node.childNodes || [];
254
+ for (let index = (Number(children.length) || 0) - 1; index >= 0; index -= 1) stack.push({ node: children[index] || children.item?.(index) });
255
+ continue;
256
+ }
257
+ if (node.nodeType === 3) {
258
+ const parentTag = String(node.parentElement?.tagName || "").toLowerCase();
259
+ append(parentTag === "script" || parentTag === "style" ? bounded(node.data, MAX_TEXT_CHARS) : escapeText(node.data));
260
+ continue;
261
+ }
262
+ if (node.nodeType === 8) append(`<!--${bounded(node.data, MAX_TEXT_CHARS).replaceAll("--", "- -")}-->`);
263
+ }
264
+ if (safetyTruncated && !budgetExhausted) append("<!-- machine-bridge source truncated by safety limit -->");
265
+ return {
266
+ source: chunks.join(""),
267
+ bytes: returnedBytes,
268
+ returned_bytes: returnedBytes,
269
+ truncated: budgetExhausted || safetyTruncated || stack.length > 0,
270
+ visited_nodes: visitedNodes,
271
+ node_limit: MAX_NODES,
272
+ open_shadow_roots: openShadowRoots,
273
+ url: String(location.href || "").slice(0, 8192),
274
+ };
275
+ }
276
+
277
+ async function browserAction(params, state) {
278
+ const tab = await resolveTab(params.tabId);
279
+ if (params.action === "navigate") {
280
+ if (!params.url) throw new Error("navigate requires url");
281
+ const waiter = beginTabWait(tab.id, params.waitFor, state?.timeoutMs, state);
282
+ try {
283
+ const updated = await chrome.tabs.update(tab.id, { url: params.url });
284
+ await waiter.promise;
285
+ return publicTab(await chrome.tabs.get(updated.id));
286
+ } catch (error) {
287
+ waiter.cancel();
288
+ throw error;
289
+ }
290
+ }
291
+ if (params.action === "reload") {
292
+ const waiter = beginTabWait(tab.id, params.waitFor, state?.timeoutMs, state);
293
+ try {
294
+ await chrome.tabs.reload(tab.id);
295
+ await waiter.promise;
296
+ return publicTab(await chrome.tabs.get(tab.id));
297
+ } catch (error) {
298
+ waiter.cancel();
299
+ throw error;
300
+ }
301
+ }
302
+ if (params.action === "back" || params.action === "forward") {
303
+ const waiter = beginTabWait(tab.id, params.waitFor, state?.timeoutMs, state);
304
+ try {
305
+ if (params.action === "back") await chrome.tabs.goBack(tab.id);
306
+ else await chrome.tabs.goForward(tab.id);
307
+ await waiter.promise;
308
+ return publicTab(await chrome.tabs.get(tab.id));
309
+ } catch (error) {
310
+ waiter.cancel();
311
+ throw error;
312
+ }
313
+ }
314
+ assertPageTab(tab);
315
+ const waiter = beginTabWait(tab.id, params.waitFor, state?.timeoutMs, state);
316
+ let result;
317
+ try {
318
+ result = await performPageAction(tab, params);
319
+ await waiter.promise;
320
+ } catch (error) {
321
+ waiter.cancel();
322
+ throw error;
323
+ }
324
+ const current = await chrome.tabs.get(tab.id).catch(() => tab);
325
+ return { tab_id: current.id, title: current.title || "", url: current.url || "", ...result };
326
+ }
327
+
328
+ async function performPageAction(tab, params) {
329
+ const trustedActions = new Set(["click", "double_click", "hover", "press", "type_text"]);
330
+ if (params.inputMode === "trusted" && !trustedActions.has(params.action)) {
331
+ throw new Error("trusted input is unavailable for this action");
332
+ }
333
+ const wantsTrusted = params.inputMode !== "dom" && trustedActions.has(params.action);
334
+ const topFrame = !Number.isInteger(params.frameId) || params.frameId === 0;
335
+ if (wantsTrusted && !topFrame && params.inputMode === "trusted") {
336
+ throw new Error("trusted input currently requires the top frame; use input_mode=dom for a subframe");
337
+ }
338
+ if (wantsTrusted && topFrame) {
339
+ const [prepared] = await executePageAutomation(scriptTarget(tab.id, params.frameId, false), "prepareAction", params);
340
+ try {
341
+ const api = globalThis.__machineBridgeDevtoolsInput;
342
+ if (!api?.perform) {
343
+ const unavailable = new Error("trusted input module is unavailable");
344
+ Object.defineProperty(unavailable, "safeToFallback", { value: true });
345
+ throw unavailable;
346
+ }
347
+ await api.perform(tab.id, params.action, {
348
+ point: prepared.result?.point,
349
+ key: params.key || params.value || "Enter",
350
+ text: params.value || "",
351
+ });
352
+ return { ...prepared.result, input_mode: "trusted", trusted_input_fallback: false };
353
+ } catch (error) {
354
+ if (params.inputMode === "trusted") throw error;
355
+ if (error?.safeToFallback !== true) {
356
+ const detail = String(error?.message || error).replace(/\s+/g, " ").slice(0, 500);
357
+ throw new Error(`trusted browser input may have been partially dispatched; the action outcome is unknown. Inspect the page before retrying. (${detail})`);
358
+ }
359
+ const [fallback] = await executePageAutomation(scriptTarget(tab.id, params.frameId, false), "action", params);
360
+ return {
361
+ ...fallback.result,
362
+ input_mode: "dom",
363
+ trusted_input_fallback: true,
364
+ fallback_reason: String(error?.message || error).slice(0, 500),
365
+ };
366
+ }
367
+ }
368
+ const [execution] = await executePageAutomation(scriptTarget(tab.id, params.frameId, false), "action", params);
369
+ return { ...execution.result, input_mode: "dom", trusted_input_fallback: false };
370
+ }
371
+ async function fillForm(params, state) {
372
+ const tab = await resolveTab(params.tabId);
373
+ assertPageTab(tab);
374
+ const waiter = beginTabWait(tab.id, params.waitFor, state?.timeoutMs, state);
375
+ let execution;
376
+ try {
377
+ [execution] = await executePageAutomation(scriptTarget(tab.id, params.frameId, false), "fillForm", params);
378
+ await waiter.promise;
379
+ } catch (error) {
380
+ waiter.cancel();
381
+ throw error;
382
+ }
383
+ return { tab_id: tab.id, title: tab.title || "", url: tab.url || "", ...execution.result };
384
+ }
385
+
386
+ async function uploadFiles(params) {
387
+ const tab = await resolveTab(params.tabId);
388
+ assertPageTab(tab);
389
+ const [execution] = await executePageAutomation(scriptTarget(tab.id, params.frameId, false), "uploadFiles", params);
390
+ return { tab_id: tab.id, title: tab.title || "", url: tab.url || "", ...execution.result };
391
+ }
392
+
393
+ async function screenshot(params) {
394
+ const tab = await resolveTab(params.tabId);
395
+ const [previousActive] = await chrome.tabs.query({ active: true, windowId: tab.windowId });
396
+ const changedActiveTab = previousActive?.id !== tab.id;
397
+ if (changedActiveTab) await chrome.tabs.update(tab.id, { active: true });
398
+ try {
399
+ const data = await chrome.tabs.captureVisibleTab(tab.windowId, {
400
+ format: params.format === "jpeg" ? "jpeg" : "png",
401
+ quality: Number(params.quality) || 90,
402
+ });
403
+ return { tab_id: tab.id, title: tab.title || "", url: tab.url || "", data };
404
+ } finally {
405
+ if (changedActiveTab && previousActive?.id) {
406
+ const [currentActive] = await chrome.tabs.query({ active: true, windowId: tab.windowId }).catch(() => []);
407
+ if (currentActive?.id === tab.id) await chrome.tabs.update(previousActive.id, { active: true }).catch(() => {});
408
+ }
409
+ }
410
+ }
411
+
412
+ function publicTab(tab) {
413
+ return { tab_id: tab.id, window_id: tab.windowId, title: tab.title || "", url: tab.url || "", status: tab.status || "" };
414
+ }
415
+
416
+ function assertPageTab(tab) {
417
+ const url = String(tab.url || "");
418
+ if (!/^(https?|file):/i.test(url)) throw new Error("this page cannot be scripted by a browser extension");
419
+ }
420
+
421
+ function beginTabWait(tabId, mode, requestTimeoutMs = 30000, state = null) {
422
+ if (!mode || mode === "none") return { promise: Promise.resolve(), cancel() {} };
423
+ const waitTimeoutMs = Math.max(1000, boundedRequestTimeout(requestTimeoutMs) - 1000);
424
+ let settled = false;
425
+ let navigationStarted = false;
426
+ let pollTimer = null;
427
+ let cancellationTimer = null;
428
+ let timeout = null;
429
+ let lastPollError = "";
430
+ let resolveWait;
431
+ let rejectWait;
432
+ const promise = new Promise((resolvePromise, rejectPromise) => {
433
+ resolveWait = resolvePromise;
434
+ rejectWait = rejectPromise;
435
+ });
436
+ const cleanup = () => {
437
+ clearTimeout(timeout);
438
+ clearTimeout(pollTimer);
439
+ clearTimeout(cancellationTimer);
440
+ chrome.tabs.onUpdated.removeListener(listener);
441
+ chrome.tabs.onRemoved?.removeListener(removedListener);
442
+ };
443
+ const settle = (error = null) => {
444
+ if (settled) return;
445
+ settled = true;
446
+ cleanup();
447
+ if (error) rejectWait(error); else resolveWait();
448
+ };
449
+ const pollReadyState = async () => {
450
+ if (settled || !navigationStarted || mode !== "domcontentloaded") return;
451
+ try {
452
+ const [execution] = await chrome.scripting.executeScript({ target: { tabId }, func: () => document.readyState });
453
+ if (["interactive", "complete"].includes(execution?.result)) {
454
+ settle();
455
+ return;
456
+ }
457
+ } catch (error) {
458
+ lastPollError = String(error?.message || error).replace(/\s+/g, " ").slice(0, 300);
459
+ }
460
+ pollTimer = setTimeout(pollReadyState, 100);
461
+ };
462
+ const pollCancellation = () => {
463
+ if (settled) return;
464
+ if (state?.cancelled) { settle(new Error("browser request cancelled")); return; }
465
+ cancellationTimer = setTimeout(pollCancellation, 100);
466
+ };
467
+ const removedListener = (removedId) => {
468
+ if (removedId === tabId) settle(new Error("browser tab closed during navigation wait"));
469
+ };
470
+ const listener = (updatedId, changeInfo, tab) => {
471
+ if (updatedId !== tabId || settled) return;
472
+ if (changeInfo.status === "loading" || typeof changeInfo.url === "string") navigationStarted = true;
473
+ if (!navigationStarted) return;
474
+ if (mode === "complete" && (changeInfo.status === "complete" || (typeof changeInfo.url === "string" && tab.status === "complete"))) settle();
475
+ if (mode === "domcontentloaded") void pollReadyState();
476
+ };
477
+ chrome.tabs.onUpdated.addListener(listener);
478
+ chrome.tabs.onRemoved?.addListener(removedListener);
479
+ pollCancellation();
480
+ timeout = setTimeout(() => settle(new Error(`browser navigation wait timed out${lastPollError ? `: ${lastPollError}` : ""}`)), waitTimeoutMs);
481
+ return { promise, cancel: () => settle() };
482
+ }
483
+
484
+ function executePageAutomation(target, method, params) {
485
+ return chrome.scripting.executeScript({ target, files: ["page-automation.js"] })
486
+ .then(() => chrome.scripting.executeScript({
487
+ target,
488
+ func: (operation, payload) => {
489
+ const api = globalThis.__machineBridgePageAutomation;
490
+ if (!api || typeof api[operation] !== "function") throw new Error("page automation module is unavailable");
491
+ return api[operation](payload);
492
+ },
493
+ args: [method, params],
494
+ }));
495
+ }
496
+
497
+ function delay(ms) {
498
+ return new Promise((resolve) => setTimeout(resolve, ms));
499
+ }
500
+
501
+ function boundedRequestTimeout(value) {
502
+ const timeout = Number(value);
503
+ if (!Number.isFinite(timeout)) return 30000;
504
+ return Math.max(1000, Math.min(185000, Math.floor(timeout)));
505
+ }
506
+
507
+ function throwIfCancelled(state) {
508
+ if (state?.cancelled) throw new Error("browser request cancelled");
509
+ }
510
+
511
+ Object.defineProperty(globalThis, "__machineBridgeBrowserOperations", {
512
+ value: Object.freeze({ dispatch, boundedRequestTimeout, boundedDocumentSource }),
513
+ configurable: false,
514
+ });
515
+ })();
@@ -39,13 +39,18 @@
39
39
  async function withDebugger(tabId, operation) {
40
40
  const target = { tabId };
41
41
  let attached = false;
42
+ let dispatchStarted = false;
42
43
  try {
43
44
  await chrome.debugger.attach(target, "1.3");
44
45
  attached = true;
45
- const send = (method, params = {}) => chrome.debugger.sendCommand(target, method, params);
46
+ const send = (method, params = {}) => {
47
+ dispatchStarted = true;
48
+ return chrome.debugger.sendCommand(target, method, params);
49
+ };
46
50
  return await operation(send);
47
51
  } catch (error) {
48
- throw new Error(`trusted browser input unavailable: ${cleanError(error)}`);
52
+ if (error?.machineBridgeTrustedInput === true) throw error;
53
+ throw trustedInputError(error, { safeToFallback: !dispatchStarted, dispatchStarted });
49
54
  } finally {
50
55
  if (attached) {
51
56
  try { await chrome.debugger.detach(target); } catch {}
@@ -125,6 +130,16 @@
125
130
  return { x, y };
126
131
  }
127
132
 
133
+ function trustedInputError(error, { safeToFallback, dispatchStarted }) {
134
+ const wrapped = new Error(`trusted browser input unavailable: ${cleanError(error)}`);
135
+ Object.defineProperties(wrapped, {
136
+ machineBridgeTrustedInput: { value: true },
137
+ safeToFallback: { value: safeToFallback === true },
138
+ dispatchStarted: { value: dispatchStarted === true },
139
+ });
140
+ return wrapped;
141
+ }
142
+
128
143
  function cleanError(error) {
129
144
  return String(error?.message || error || "unknown error").replace(/\s+/g, " ").slice(0, 500);
130
145
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "manifest_version": 3,
3
3
  "name": "Machine Bridge Browser",
4
- "version": "0.14.0",
4
+ "version": "0.16.0",
5
5
  "description": "Connects the current Chromium browser profile to the local Machine Bridge runtime for user-authorized automation.",
6
6
  "permissions": [
7
7
  "tabs",
@@ -30,5 +30,5 @@
30
30
  "action": {
31
31
  "default_title": "Machine Bridge Browser"
32
32
  },
33
- "version_name": "0.14.0"
33
+ "version_name": "0.16.0"
34
34
  }