machine-bridge-mcp 2.0.0 → 3.0.0-beta.102

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 (473) hide show
  1. package/CHANGELOG.md +1142 -0
  2. package/CONTRIBUTING.md +23 -25
  3. package/GOVERNANCE.md +2 -2
  4. package/README.md +58 -20
  5. package/SECURITY.md +161 -120
  6. package/SUPPORT.md +1 -1
  7. package/browser-extension/broker-auth.js +83 -0
  8. package/browser-extension/broker-liveness.js +35 -0
  9. package/browser-extension/browser-error-boundary.js +96 -0
  10. package/browser-extension/browser-operations.js +1456 -101
  11. package/browser-extension/devtools-input.js +195 -34
  12. package/browser-extension/devtools-observation.js +429 -0
  13. package/browser-extension/devtools-session.js +48 -0
  14. package/browser-extension/manifest.json +4 -3
  15. package/browser-extension/page-automation.js +578 -118
  16. package/browser-extension/pairing-bootstrap.js +42 -0
  17. package/browser-extension/pairing.js +28 -12
  18. package/browser-extension/service-worker.js +126 -106
  19. package/docs/AGENT_CONTEXT.md +13 -10
  20. package/docs/ARCHITECTURE.md +159 -67
  21. package/docs/AUDIT.md +1302 -4
  22. package/docs/CLIENTS.md +19 -6
  23. package/docs/COMPUTER_USE.md +297 -0
  24. package/docs/ENGINEERING.md +72 -13
  25. package/docs/GETTING_STARTED.md +18 -20
  26. package/docs/LOCAL_AUTHORIZATION.md +115 -72
  27. package/docs/LOCAL_AUTOMATION.md +27 -21
  28. package/docs/LOGGING.md +40 -19
  29. package/docs/MANAGED_JOBS.md +37 -34
  30. package/docs/MULTI_ACCOUNT.md +98 -37
  31. package/docs/OPERATIONS.md +145 -51
  32. package/docs/OVERVIEW.md +14 -11
  33. package/docs/POLICY_REFERENCE.md +71 -3
  34. package/docs/PRIVACY.md +41 -2
  35. package/docs/PROJECT_STANDARDS.md +29 -6
  36. package/docs/RELEASING.md +238 -64
  37. package/docs/TESTING.md +150 -39
  38. package/docs/THREAT_MODEL.md +164 -67
  39. package/docs/TOOL_REFERENCE.md +653 -60
  40. package/docs/UPGRADING.md +78 -18
  41. package/native/macos/MachineBridgeBackgroundInput.swift +1051 -0
  42. package/native/macos/MachineBridgeBackgroundInputSmokeFixture.swift +295 -0
  43. package/native/macos/MachineBridgeTrustBroker.swift +233 -0
  44. package/package.json +94 -16
  45. package/scripts/accepted-candidate-tarball.mjs +77 -0
  46. package/scripts/candidate-runtime-store.mjs +93 -0
  47. package/scripts/check-plan.mjs +73 -3
  48. package/scripts/check-runner.mjs +129 -17
  49. package/scripts/commit-message-check.mjs +5 -2
  50. package/scripts/consumer-package-security.mjs +293 -0
  51. package/scripts/coverage-check.mjs +251 -15
  52. package/scripts/coverage-generation.mjs +41 -0
  53. package/scripts/coverage-range-merge.mjs +41 -0
  54. package/scripts/foreground-daemon-recovery.mjs +88 -0
  55. package/scripts/generate-policy-reference.mjs +28 -5
  56. package/scripts/generate-tool-reference.mjs +1 -1
  57. package/scripts/generate-worker-types.mjs +21 -11
  58. package/scripts/github-backlog.mjs +11 -10
  59. package/scripts/github-push.mjs +62 -16
  60. package/scripts/github-release-asset.mjs +58 -0
  61. package/scripts/github-release.mjs +241 -132
  62. package/scripts/global-package-installation.mjs +68 -0
  63. package/scripts/hardened-npm-session.mjs +42 -0
  64. package/scripts/install-published-prerelease.mjs +198 -0
  65. package/scripts/local-release-acceptance.mjs +103 -45
  66. package/scripts/macos-background-input-smoke.mjs +515 -0
  67. package/scripts/network-retry.mjs +8 -1
  68. package/scripts/npm-global-prefix.mjs +27 -0
  69. package/scripts/npm-publication-policy.mjs +17 -0
  70. package/scripts/official-mcp-conformance.mjs +267 -0
  71. package/scripts/persistent-activation-process.mjs +101 -0
  72. package/scripts/prepare-pinned-npm.mjs +12 -69
  73. package/scripts/prerelease-activation.mjs +160 -0
  74. package/scripts/privacy-check.mjs +14 -6
  75. package/scripts/promotion-digest.mjs +128 -0
  76. package/scripts/publish-npm.mjs +260 -0
  77. package/scripts/published-release.mjs +132 -0
  78. package/scripts/release-acceptance.mjs +62 -54
  79. package/scripts/release-browser-extension-store.mjs +172 -0
  80. package/scripts/release-candidate-manifest.mjs +52 -0
  81. package/scripts/release-channel.mjs +106 -0
  82. package/scripts/release-diagnostic.mjs +41 -0
  83. package/scripts/release-impact-check.mjs +12 -22
  84. package/scripts/release-oauth-canary-core.mjs +325 -0
  85. package/scripts/release-oauth-canary-evidence.mjs +100 -0
  86. package/scripts/release-oauth-canary.mjs +127 -0
  87. package/scripts/release-publication-guard.mjs +67 -0
  88. package/scripts/release-soak.mjs +284 -0
  89. package/scripts/release-state.mjs +1 -1
  90. package/scripts/run-checks.mjs +44 -7
  91. package/scripts/run-worker-dry-run.mjs +21 -0
  92. package/scripts/sarif-security-gate.mjs +2 -1
  93. package/scripts/sbom-check.mjs +106 -0
  94. package/scripts/start-release-candidate.mjs +267 -44
  95. package/scripts/syntax-check.mjs +36 -14
  96. package/scripts/verification-generation-guard.mjs +15 -0
  97. package/scripts/verification-idle-sleep-guard.mjs +37 -0
  98. package/scripts/verification-state.mjs +108 -0
  99. package/scripts/wrangler-command-lifecycle.mjs +115 -0
  100. package/src/local/account-access.mjs +13 -1
  101. package/src/local/account-admin-response.mjs +92 -0
  102. package/src/local/account-admin.mjs +52 -19
  103. package/src/local/agent-context-projection.mjs +26 -7
  104. package/src/local/agent-context.mjs +88 -49
  105. package/src/local/agent-skill-discovery.mjs +16 -5
  106. package/src/local/app-automation-macos-jxa.mjs +350 -0
  107. package/src/local/app-automation.mjs +873 -163
  108. package/src/local/application-capability-projection.mjs +24 -0
  109. package/src/local/authority-context.mjs +106 -0
  110. package/src/local/autostart-log-maintenance.mjs +36 -0
  111. package/src/local/browser-bridge-http.mjs +5 -3
  112. package/src/local/browser-bridge.mjs +109 -31
  113. package/src/local/browser-broker-auth-http.mjs +67 -0
  114. package/src/local/browser-broker-auth.mjs +98 -0
  115. package/src/local/browser-broker-routes.mjs +63 -20
  116. package/src/local/browser-broker-server.mjs +17 -7
  117. package/src/local/browser-command.mjs +79 -30
  118. package/src/local/browser-computer-observation-service.mjs +266 -0
  119. package/src/local/browser-extension-identity.mjs +50 -0
  120. package/src/local/browser-extension-path.mjs +22 -0
  121. package/src/local/browser-extension-protocol.mjs +50 -11
  122. package/src/local/browser-operation-service.mjs +147 -117
  123. package/src/local/browser-pairing-grant.mjs +109 -0
  124. package/src/local/browser-pairing-http.mjs +36 -0
  125. package/src/local/browser-pairing-launch.mjs +58 -0
  126. package/src/local/browser-pairing-store.mjs +85 -48
  127. package/src/local/browser-request-registry.mjs +40 -14
  128. package/src/local/browser-request-settlement.mjs +78 -0
  129. package/src/local/browser-resource-input.mjs +74 -0
  130. package/src/local/browser-trusted-input-health.mjs +63 -0
  131. package/src/local/call-authority.mjs +21 -0
  132. package/src/local/call-capacity.mjs +45 -0
  133. package/src/local/call-registry-drain.mjs +54 -0
  134. package/src/local/call-registry.mjs +30 -15
  135. package/src/local/capability-observer.mjs +5 -0
  136. package/src/local/capability-ranking.mjs +3 -2
  137. package/src/local/child-process-settlement.mjs +132 -0
  138. package/src/local/cli-account-admin.mjs +40 -6
  139. package/src/local/cli-activate.mjs +162 -0
  140. package/src/local/cli-local-admin.mjs +20 -45
  141. package/src/local/cli-options.mjs +12 -13
  142. package/src/local/cli-ready-output.mjs +52 -0
  143. package/src/local/cli-service.mjs +91 -11
  144. package/src/local/cli.mjs +232 -150
  145. package/src/local/computer-use-application-observation.mjs +224 -0
  146. package/src/local/computer-use-arguments.mjs +270 -0
  147. package/src/local/computer-use-deadline.mjs +50 -0
  148. package/src/local/computer-use-dispatch-settlement.mjs +40 -0
  149. package/src/local/computer-use-expectation.mjs +171 -0
  150. package/src/local/computer-use-observation.mjs +769 -0
  151. package/src/local/computer-use-recovery.mjs +82 -0
  152. package/src/local/computer-use-result-budget.mjs +145 -0
  153. package/src/local/computer-use-snapshot-store.mjs +67 -0
  154. package/src/local/computer-use.mjs +2255 -0
  155. package/src/local/daemon-http-relay-auth.mjs +26 -0
  156. package/src/local/daemon-http-relay-connection.mjs +298 -0
  157. package/src/local/daemon-http-relay-request.mjs +64 -0
  158. package/src/local/daemon-http-relay-sequence.mjs +49 -0
  159. package/src/local/daemon-process.mjs +40 -7
  160. package/src/local/delegated-process-sandbox.mjs +148 -0
  161. package/src/local/device-identity.mjs +156 -49
  162. package/src/local/device-root-provider.mjs +81 -0
  163. package/src/local/directory-metadata.mjs +47 -0
  164. package/src/local/doctor-reporting.mjs +23 -0
  165. package/src/local/durable-process-spec.mjs +80 -0
  166. package/src/local/errors.mjs +25 -15
  167. package/src/local/exclusive-file.mjs +74 -26
  168. package/src/local/execution-limits.mjs +9 -0
  169. package/src/local/execution-routing.mjs +276 -0
  170. package/src/local/execution-surface.mjs +23 -0
  171. package/src/local/file-mutation-coordinator.mjs +47 -0
  172. package/src/local/file-snapshot-preservation.mjs +24 -0
  173. package/src/local/filesystem-identity.mjs +54 -0
  174. package/src/local/fixed-process-environment.mjs +21 -0
  175. package/src/local/full-access-test.mjs +31 -13
  176. package/src/local/git-commit.mjs +69 -0
  177. package/src/local/git-config-safety.mjs +55 -0
  178. package/src/local/git-log-parser.mjs +41 -0
  179. package/src/local/git-metadata-boundary.mjs +77 -0
  180. package/src/local/git-metadata-tree-safety.mjs +59 -0
  181. package/src/local/git-operation-state.mjs +19 -0
  182. package/src/local/git-service.mjs +165 -30
  183. package/src/local/hardened-npm-download-timeout.mjs +46 -0
  184. package/src/local/hardened-npm-download.mjs +109 -0
  185. package/src/local/hardened-npm-extract.mjs +20 -0
  186. package/src/local/hardened-npm-verification.mjs +104 -0
  187. package/src/local/hardened-npm.mjs +199 -0
  188. package/src/local/job-runner.mjs +244 -89
  189. package/src/local/lifecycle.mjs +6 -0
  190. package/src/local/log.mjs +19 -45
  191. package/src/local/macos-background-input.mjs +398 -0
  192. package/src/local/macos-idle-sleep-assertion.mjs +65 -0
  193. package/src/local/macos-trust-broker.mjs +404 -0
  194. package/src/local/managed-job-cancellation.mjs +42 -0
  195. package/src/local/managed-job-capacity.mjs +16 -0
  196. package/src/local/managed-job-directory-generation.mjs +60 -0
  197. package/src/local/managed-job-directory.mjs +75 -0
  198. package/src/local/managed-job-durable-process.mjs +49 -0
  199. package/src/local/managed-job-lock.mjs +46 -14
  200. package/src/local/managed-job-plan.mjs +38 -14
  201. package/src/local/managed-job-projection.mjs +23 -15
  202. package/src/local/managed-job-retention.mjs +128 -0
  203. package/src/local/managed-job-runner-claim.mjs +70 -0
  204. package/src/local/managed-job-runner.mjs +35 -10
  205. package/src/local/managed-job-storage.mjs +22 -6
  206. package/src/local/managed-job-terminal-maintenance.mjs +40 -0
  207. package/src/local/managed-job-terminal.mjs +150 -0
  208. package/src/local/managed-jobs.mjs +424 -287
  209. package/src/local/npm-cli.mjs +42 -0
  210. package/src/local/npm-environment.mjs +37 -0
  211. package/src/local/operation-authorization.mjs +68 -324
  212. package/src/local/operation-risk.mjs +51 -4
  213. package/src/local/owner-state-lock.mjs +118 -0
  214. package/src/local/package-identity.mjs +9 -0
  215. package/src/local/patch.mjs +20 -18
  216. package/src/local/path-inspection.mjs +23 -0
  217. package/src/local/policy.mjs +19 -0
  218. package/src/local/private-toolchain-integrity.mjs +54 -0
  219. package/src/local/process-execution.mjs +87 -75
  220. package/src/local/process-foreground-timeout.mjs +50 -0
  221. package/src/local/process-identity.mjs +41 -7
  222. package/src/local/process-nonreplayable-settlement.mjs +50 -0
  223. package/src/local/process-result-projection.mjs +22 -0
  224. package/src/local/process-session-events.mjs +47 -0
  225. package/src/local/process-session-remote-activity.mjs +10 -0
  226. package/src/local/process-session-termination.mjs +73 -0
  227. package/src/local/process-sessions.mjs +144 -112
  228. package/src/local/process-tracker.mjs +140 -13
  229. package/src/local/process-tree-ownership-types.d.ts +37 -0
  230. package/src/local/process-tree-ownership.mjs +57 -0
  231. package/src/local/process-tree-signal.mjs +34 -0
  232. package/src/local/process-tree-snapshot.mjs +77 -0
  233. package/src/local/process-tree-supervisor.mjs +69 -0
  234. package/src/local/process-tree.mjs +11 -53
  235. package/src/local/project-metadata.mjs +19 -6
  236. package/src/local/relay-call-recovery.mjs +58 -38
  237. package/src/local/relay-connection-classification.mjs +131 -0
  238. package/src/local/relay-connection-support.mjs +91 -0
  239. package/src/local/relay-connection.mjs +179 -272
  240. package/src/local/relay-diagnostics.mjs +91 -0
  241. package/src/local/relay-heartbeat.mjs +119 -0
  242. package/src/local/relay-liveness.mjs +109 -0
  243. package/src/local/relay-peer-diagnostics.mjs +29 -0
  244. package/src/local/release-runtime-lock.mjs +18 -0
  245. package/src/local/remote-activity-idle-sleep-guard.mjs +77 -0
  246. package/src/local/remote-configuration.mjs +48 -0
  247. package/src/local/resilient-relay-connection.mjs +178 -0
  248. package/src/local/resource-admission-diagnostics.mjs +52 -0
  249. package/src/local/resource-admission-policy.mjs +115 -0
  250. package/src/local/resource-admission.mjs +339 -0
  251. package/src/local/resource-build-root.mjs +83 -0
  252. package/src/local/resource-cargo-concurrency.mjs +38 -0
  253. package/src/local/resource-cmake-concurrency.mjs +42 -0
  254. package/src/local/resource-command-concurrency.mjs +37 -0
  255. package/src/local/resource-command-profile.mjs +176 -0
  256. package/src/local/resource-coordinator-accounting.mjs +21 -0
  257. package/src/local/resource-cpu-window.mjs +20 -0
  258. package/src/local/resource-disk-headroom.mjs +13 -0
  259. package/src/local/resource-elastic-memory.mjs +19 -0
  260. package/src/local/resource-elastic-request.mjs +36 -0
  261. package/src/local/resource-foreground-wait.mjs +29 -0
  262. package/src/local/resource-go-concurrency.mjs +43 -0
  263. package/src/local/resource-gradle-concurrency.mjs +57 -0
  264. package/src/local/resource-host-cache.mjs +39 -0
  265. package/src/local/resource-host-darwin.mjs +54 -0
  266. package/src/local/resource-host-linux.mjs +35 -0
  267. package/src/local/resource-host-sample-file.mjs +23 -0
  268. package/src/local/resource-host-snapshot.mjs +99 -0
  269. package/src/local/resource-lease-accounting.mjs +119 -0
  270. package/src/local/resource-light-command.mjs +6 -0
  271. package/src/local/resource-make-concurrency.mjs +42 -0
  272. package/src/local/resource-maven-concurrency.mjs +38 -0
  273. package/src/local/resource-ninja-command-concurrency.mjs +20 -0
  274. package/src/local/resource-ninja-concurrency.mjs +28 -0
  275. package/src/local/resource-operations.mjs +26 -10
  276. package/src/local/resource-pressure.mjs +67 -0
  277. package/src/local/resource-probe-command.mjs +35 -0
  278. package/src/local/resource-process-admission.mjs +59 -0
  279. package/src/local/resource-process-ancestry-cache.mjs +43 -0
  280. package/src/local/resource-process-ancestry.mjs +32 -0
  281. package/src/local/resource-process-priority.mjs +15 -0
  282. package/src/local/resource-project-key.mjs +53 -0
  283. package/src/local/resource-pytest-concurrency.mjs +33 -0
  284. package/src/local/resource-release-control-classification.mjs +14 -0
  285. package/src/local/resource-release-control-executable.mjs +70 -0
  286. package/src/local/resource-release-control-workspace.mjs +41 -0
  287. package/src/local/resource-request-contract.mjs +30 -0
  288. package/src/local/resource-script-classification.mjs +44 -0
  289. package/src/local/resource-shell-analysis.mjs +58 -0
  290. package/src/local/resource-staging-recovery.mjs +97 -0
  291. package/src/local/resource-swift-concurrency.mjs +31 -0
  292. package/src/local/resource-transaction-lock.mjs +189 -0
  293. package/src/local/resource-wait.mjs +48 -0
  294. package/src/local/resource-waiters.mjs +139 -0
  295. package/src/local/resource-xcode-command.mjs +7 -0
  296. package/src/local/resource-xcode-concurrency.mjs +34 -0
  297. package/src/local/resource-xcode-non-build.mjs +19 -0
  298. package/src/local/runtime-activation.mjs +700 -0
  299. package/src/local/runtime-activity-projection.mjs +48 -0
  300. package/src/local/runtime-capabilities.mjs +48 -15
  301. package/src/local/runtime-diagnostic-state.mjs +14 -0
  302. package/src/local/runtime-diagnostics.mjs +40 -13
  303. package/src/local/runtime-info-projection.mjs +60 -0
  304. package/src/local/runtime-info-relay-projection.mjs +21 -0
  305. package/src/local/runtime-paths.mjs +2 -1
  306. package/src/local/runtime-process-routing.mjs +33 -0
  307. package/src/local/runtime-relay-connection-options.mjs +44 -0
  308. package/src/local/runtime-relay-control.mjs +98 -0
  309. package/src/local/runtime-relay.mjs +26 -50
  310. package/src/local/runtime-reporting.mjs +37 -21
  311. package/src/local/runtime-resource-service.mjs +77 -0
  312. package/src/local/runtime-tool-handlers.mjs +13 -10
  313. package/src/local/runtime.mjs +244 -208
  314. package/src/local/secure-file.mjs +73 -20
  315. package/src/local/security-audit-dispatch.mjs +35 -0
  316. package/src/local/security-audit-log.mjs +215 -0
  317. package/src/local/security-audit-state.mjs +139 -0
  318. package/src/local/security-audit-storage.mjs +135 -0
  319. package/src/local/security-audit-warning.mjs +31 -0
  320. package/src/local/security-audit-worker.mjs +87 -0
  321. package/src/local/service-convergence.mjs +42 -1
  322. package/src/local/service-definition.mjs +24 -0
  323. package/src/local/service-environment.mjs +18 -10
  324. package/src/local/service-owner.mjs +146 -0
  325. package/src/local/service-ownership.mjs +17 -0
  326. package/src/local/service-restart-handoff.mjs +64 -0
  327. package/src/local/service-restart-scheduler.mjs +49 -0
  328. package/src/local/service-runtime-convergence.mjs +52 -0
  329. package/src/local/service-runtime.mjs +105 -0
  330. package/src/local/service-status.mjs +60 -0
  331. package/src/local/service.mjs +385 -92
  332. package/src/local/shell.mjs +39 -19
  333. package/src/local/ssh-key.mjs +158 -42
  334. package/src/local/state-inventory.mjs +50 -24
  335. package/src/local/state-owner-lock-inventory.mjs +23 -0
  336. package/src/local/state-root-owned-namespaces.mjs +83 -0
  337. package/src/local/state-root-retirement.mjs +89 -0
  338. package/src/local/state.mjs +386 -96
  339. package/src/local/stdio.mjs +96 -66
  340. package/src/local/support-state-projection.mjs +30 -0
  341. package/src/local/system-network-route.mjs +76 -0
  342. package/src/local/systemd-removal.mjs +20 -0
  343. package/src/local/tool-executor.mjs +85 -20
  344. package/src/local/tool-result-boundary.mjs +51 -0
  345. package/src/local/toolchain-operation-lock.mjs +41 -0
  346. package/src/local/tools.mjs +7 -5
  347. package/src/local/trusted-executable.mjs +53 -0
  348. package/src/local/trusted-git-executable.mjs +34 -0
  349. package/src/local/trusted-github-cli.mjs +24 -0
  350. package/src/local/windows-service-convergence.mjs +49 -0
  351. package/src/local/windows-service.mjs +52 -45
  352. package/src/local/worker-deployment-fingerprint.mjs +102 -0
  353. package/src/local/worker-deployment.mjs +48 -60
  354. package/src/local/worker-health.mjs +12 -4
  355. package/src/local/worker-secret-file.mjs +37 -17
  356. package/src/local/workspace-file-service.mjs +156 -239
  357. package/src/local/workspace-file-transaction.mjs +189 -0
  358. package/src/local/workspace-search.mjs +55 -0
  359. package/src/local/wrangler-toolchain/package-lock.json +1505 -0
  360. package/src/local/wrangler-toolchain/package.json +23 -0
  361. package/src/local/wrangler-toolchain-verification.mjs +135 -0
  362. package/src/local/wrangler-toolchain.mjs +188 -0
  363. package/src/shared/access-contract.json +7 -1
  364. package/src/shared/activation-recovery.mjs +49 -0
  365. package/src/shared/admin-auth.d.mts +2 -1
  366. package/src/shared/admin-auth.mjs +3 -3
  367. package/src/shared/authority-revocation.d.mts +9 -0
  368. package/src/shared/authority-revocation.mjs +28 -0
  369. package/src/shared/daemon-auth.d.mts +11 -0
  370. package/src/shared/daemon-auth.mjs +17 -0
  371. package/src/shared/device-session-auth.d.mts +20 -0
  372. package/src/shared/device-session-auth.mjs +51 -0
  373. package/src/shared/foreground-timeout.d.mts +15 -0
  374. package/src/shared/foreground-timeout.mjs +70 -0
  375. package/src/shared/log-redaction.d.mts +9 -0
  376. package/src/shared/log-redaction.mjs +51 -0
  377. package/src/shared/mcp-protocol.d.mts +27 -0
  378. package/src/shared/mcp-protocol.mjs +275 -0
  379. package/src/shared/project-overview-projection.d.mts +4 -0
  380. package/src/shared/project-overview-projection.mjs +96 -0
  381. package/src/shared/relay-contract.json +28 -0
  382. package/src/shared/result-projection.d.mts +2 -1
  383. package/src/shared/result-projection.mjs +13 -2
  384. package/src/shared/server-metadata.json +15 -9
  385. package/src/shared/tool-argument-validation.d.mts +17 -0
  386. package/src/shared/tool-argument-validation.mjs +325 -0
  387. package/src/shared/tool-call-capacity.d.mts +41 -0
  388. package/src/shared/tool-call-capacity.mjs +59 -0
  389. package/src/shared/tool-catalog.json +626 -57
  390. package/src/worker/access.ts +12 -0
  391. package/src/worker/account-admin.ts +55 -33
  392. package/src/worker/authority-revocations.ts +174 -0
  393. package/src/worker/authority.ts +52 -6
  394. package/src/worker/daemon-auth.ts +49 -68
  395. package/src/worker/daemon-channel.ts +36 -0
  396. package/src/worker/daemon-http-auth.ts +63 -0
  397. package/src/worker/daemon-http-channel.ts +120 -0
  398. package/src/worker/daemon-http-controller.ts +163 -0
  399. package/src/worker/daemon-http-protocol.ts +62 -0
  400. package/src/worker/daemon-http-registry.ts +82 -0
  401. package/src/worker/daemon-last-observation.ts +30 -0
  402. package/src/worker/daemon-liveness.ts +8 -6
  403. package/src/worker/daemon-ready-dispatch.ts +17 -0
  404. package/src/worker/daemon-ready-messages.ts +73 -0
  405. package/src/worker/daemon-ready-waiters.ts +79 -0
  406. package/src/worker/daemon-recovery-budget.ts +13 -0
  407. package/src/worker/daemon-registry.ts +58 -0
  408. package/src/worker/daemon-relay-diagnostics.ts +127 -0
  409. package/src/worker/daemon-socket-attachment.ts +55 -0
  410. package/src/worker/daemon-sockets.ts +72 -62
  411. package/src/worker/daemon-status.ts +38 -0
  412. package/src/worker/device-session-verifier.ts +129 -0
  413. package/src/worker/dpop.ts +157 -0
  414. package/src/worker/durable-process-timeout.ts +29 -0
  415. package/src/worker/errors.ts +57 -0
  416. package/src/worker/http.ts +112 -26
  417. package/src/worker/index.ts +446 -440
  418. package/src/worker/mcp-access.ts +57 -0
  419. package/src/worker/mcp-controller.ts +188 -0
  420. package/src/worker/mcp-http-accept.ts +11 -0
  421. package/src/worker/mcp-http-contract.ts +322 -0
  422. package/src/worker/mcp-initialization-compat.ts +141 -0
  423. package/src/worker/mcp-jsonrpc.ts +11 -29
  424. package/src/worker/mcp-removed-protocol.ts +17 -0
  425. package/src/worker/mcp-response-proxy.ts +135 -0
  426. package/src/worker/mcp-response-stream.ts +65 -0
  427. package/src/worker/mcp-stale-schema-compat.ts +57 -0
  428. package/src/worker/mcp-stream-proxy-contract.ts +48 -0
  429. package/src/worker/mcp-tool-call-input.ts +23 -0
  430. package/src/worker/nonce-store.ts +33 -35
  431. package/src/worker/oauth-authorization-page.ts +21 -1
  432. package/src/worker/oauth-client-admin.ts +52 -0
  433. package/src/worker/oauth-client-contract.ts +38 -0
  434. package/src/worker/oauth-controller.ts +107 -47
  435. package/src/worker/oauth-field-contract.ts +31 -0
  436. package/src/worker/oauth-record-contract.ts +7 -0
  437. package/src/worker/oauth-refresh-authority.ts +27 -0
  438. package/src/worker/oauth-refresh-exchange.ts +162 -0
  439. package/src/worker/oauth-refresh-families.ts +130 -27
  440. package/src/worker/oauth-refresh-persistence.ts +116 -0
  441. package/src/worker/oauth-state.ts +33 -18
  442. package/src/worker/oauth-store-validation.ts +131 -0
  443. package/src/worker/oauth-token-derivation.ts +33 -0
  444. package/src/worker/oauth-token-issuance.ts +119 -0
  445. package/src/worker/oauth-tokens.ts +56 -190
  446. package/src/worker/observability.ts +38 -5
  447. package/src/worker/pending-admission.ts +15 -0
  448. package/src/worker/pending-call-capacity.ts +108 -0
  449. package/src/worker/pending-call-contract.ts +42 -5
  450. package/src/worker/pending-call-deadlines.ts +75 -0
  451. package/src/worker/pending-calls.ts +131 -125
  452. package/src/worker/runtime-alarm-storage.ts +37 -0
  453. package/src/worker/runtime-alarm.ts +136 -0
  454. package/src/worker/server-info-activity.ts +36 -0
  455. package/src/worker/server-info-tool-delivery.ts +15 -0
  456. package/src/worker/server-info.ts +168 -0
  457. package/src/worker/tool-catalog.ts +47 -1
  458. package/src/worker/tool-timeout.ts +75 -14
  459. package/src/worker/websocket-protocol.ts +26 -2
  460. package/src/worker/worker-edge-guard.ts +90 -0
  461. package/src/worker/worker-edge-log.ts +65 -0
  462. package/src/worker/worker-entry.ts +52 -0
  463. package/src/worker/worker-mcp-config.ts +23 -0
  464. package/src/worker/worker-metadata.ts +48 -0
  465. package/src/worker/worker-rate-limit-key.ts +30 -0
  466. package/src/worker/worker-runtime-config.ts +20 -0
  467. package/src/worker/worker-static-routes.ts +54 -0
  468. package/src/worker/worker-task-lifetime.ts +18 -0
  469. package/tsconfig.local.json +17 -1
  470. package/wrangler.jsonc +13 -0
  471. package/src/local/cli-approval.mjs +0 -117
  472. package/src/local/operation-state-lock.mjs +0 -92
  473. package/src/worker/mcp-session.ts +0 -72
@@ -2,17 +2,124 @@
2
2
  if (globalThis.__machineBridgeBrowserOperations) return;
3
3
 
4
4
  const MAX_ACCESSIBLE_FRAMES = 64;
5
+ const PAGE_AUTOMATION_VERSION = 4;
6
+ const MUTATING_METHODS = new Set([
7
+ "manage_tabs", "point_action", "backend_node_action", "action", "fill_form", "upload_files", "screenshot",
8
+ ]);
9
+ const MUTATION_RESULT_SETTLEMENT_UNKNOWN = "browser mutation may have completed; the action outcome is unknown because its result could not be delivered. Inspect browser state before retrying.";
10
+
11
+ function exactFiniteNumber(value) {
12
+ return typeof value === "number" && Number.isFinite(value) ? (Object.is(value, -0) ? 0 : value) : null;
13
+ }
14
+
15
+ function exactPositiveFiniteNumber(value) {
16
+ const number = exactFiniteNumber(value);
17
+ return number !== null && number > 0 ? number : null;
18
+ }
19
+
20
+ function exactNormalizedCoordinate(value) {
21
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 && value < 1
22
+ ? (Object.is(value, -0) ? 0 : value)
23
+ : null;
24
+ }
25
+
26
+ function exactPositiveInteger(value) {
27
+ return Number.isSafeInteger(value) && value > 0 ? value : null;
28
+ }
29
+
30
+ function exactNonNegativeInteger(value) {
31
+ return Number.isSafeInteger(value) && value >= 0 ? value : null;
32
+ }
33
+
34
+ function exactBoolean(value, label, fallback) {
35
+ if (value === undefined) return fallback;
36
+ if (typeof value !== "boolean") throw new Error(`${label} must be boolean before dispatch`);
37
+ return value;
38
+ }
39
+
40
+ function exactBoundedInteger(value, label, fallback, min, max) {
41
+ if (value === undefined) return fallback;
42
+ if (!Number.isSafeInteger(value) || value < min || value > max) throw new Error(`${label} is invalid before dispatch`);
43
+ return value;
44
+ }
45
+
46
+ function exactImageFormat(value, label, fallback = "png") {
47
+ if (value === undefined) return fallback;
48
+ if (typeof value !== "string" || !["png", "jpeg"].includes(value)) throw new Error(`${label} is invalid before dispatch`);
49
+ return value;
50
+ }
51
+
52
+ function exactOptionalSha256(value, label) {
53
+ if (value === undefined || value === "") return "";
54
+ if (typeof value !== "string" || !/^[A-Fa-f0-9]{64}$/.test(value)) throw new Error(`${label} is invalid before dispatch`);
55
+ return value.toLowerCase();
56
+ }
57
+
58
+ function exactOptionalText(value, label, maxLength) {
59
+ if (value === undefined || value === "") return "";
60
+ if (typeof value !== "string" || value.includes("\0") || value.length > maxLength) throw new Error(`${label} is invalid before dispatch`);
61
+ return value;
62
+ }
63
+
64
+ function requiredSnapshotAuthorityString(value, label, maxLength) {
65
+ const text = exactOptionalAuthorityString(value, maxLength);
66
+ if (!text) throw new Error(`${label} is required before dispatch`);
67
+ return text;
68
+ }
69
+
70
+ function requiredSnapshotViewport(value) {
71
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("visual snapshot viewport is required before dispatch");
72
+ const output = {};
73
+ for (const key of ["width", "height", "scale"]) {
74
+ const number = exactPositiveFiniteNumber(value[key]);
75
+ if (number === null) throw new Error(`visual snapshot viewport ${key} is invalid before dispatch`);
76
+ output[key] = number;
77
+ }
78
+ return output;
79
+ }
80
+
81
+ function exactOptionalAuthorityString(value, maxLength) {
82
+ if (value === undefined || value === "") return "";
83
+ if (typeof value !== "string" || value.length > maxLength || value.includes("\0")) {
84
+ throw new Error("snapshot authority string is invalid before dispatch");
85
+ }
86
+ return value;
87
+ }
88
+
89
+ function methodMayMutate(method) {
90
+ return typeof method === "string" && MUTATING_METHODS.has(method);
91
+ }
92
+
93
+ function responsePayload({ id, ok, result, error = "", method = "", maxBytes }) {
94
+ const mutatingSuccess = ok === true && methodMayMutate(method);
95
+ let payload;
96
+ try { payload = JSON.stringify({ type: "response", id, ok, ...(ok ? { result } : { error }) }); }
97
+ catch {
98
+ payload = JSON.stringify({ type: "response", id, ok: false,
99
+ error: mutatingSuccess ? MUTATION_RESULT_SETTLEMENT_UNKNOWN : "browser result could not be serialized" });
100
+ }
101
+ const resultLimit = Number.isSafeInteger(maxBytes) && maxBytes > 0 ? maxBytes : 0;
102
+ if (resultLimit === 0 || new TextEncoder().encode(payload).byteLength > resultLimit) {
103
+ payload = JSON.stringify({ type: "response", id, ok: false,
104
+ error: mutatingSuccess ? MUTATION_RESULT_SETTLEMENT_UNKNOWN : "browser result exceeds maximum size" });
105
+ }
106
+ return payload;
107
+ }
5
108
 
6
109
  async function dispatch(method, params, state) {
7
110
  if (method === "list_tabs") return listTabs(params);
8
- if (method === "manage_tabs") return manageTabs(params);
111
+ if (method === "manage_tabs") return manageTabs(params, state);
9
112
  if (method === "wait") return browserWait(params, state);
10
113
  if (method === "get_source") return getSource(params, state);
11
114
  if (method === "inspect_page") return inspectPage(params, state);
115
+ if (method === "observe_computer") return observeComputer(params, state);
116
+ if (method === "document_state") return documentState(params);
117
+ if (method === "point_action") return pointAction(params, state);
118
+ if (method === "backend_node_action") return backendNodeAction(params, state);
12
119
  if (method === "action") return browserAction(params, state);
13
120
  if (method === "fill_form") return fillForm(params, state);
14
- if (method === "upload_files") return uploadFiles(params);
15
- if (method === "screenshot") return screenshot(params);
121
+ if (method === "upload_files") return uploadFiles(params, state);
122
+ if (method === "screenshot") return screenshot(params, state);
16
123
  throw new Error(`unknown browser method: ${method}`);
17
124
  }
18
125
 
@@ -37,40 +144,103 @@
37
144
  }
38
145
 
39
146
 
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 });
147
+ async function manageTabs(params, state) {
148
+ const action = exactOptionalText(params?.action, "browser tab action", 32);
149
+ if (!["new", "activate", "close"].includes(action)) throw new Error("unsupported browser tab action");
150
+ let newTabUrl = "";
151
+ let newTabActive = true;
152
+ if (action === "new") {
153
+ newTabUrl = exactOptionalText(params.url, "browser tab url", 32768);
154
+ if (newTabUrl && !/^(https?|file):/i.test(newTabUrl)) throw new Error("browser tab url is invalid before dispatch");
155
+ newTabActive = exactBoolean(params.active, "browser tab active", true);
156
+ } else if (!Number.isSafeInteger(params.tabId) || params.tabId < 1) {
157
+ throw new Error("browser tab id is invalid");
158
+ }
159
+ throwIfCancelled(state);
160
+ if (action === "new") {
161
+ const tab = await invokeBrowserTabMutation(
162
+ () => chrome.tabs.create({ url: newTabUrl || "about:blank", active: newTabActive }),
163
+ "create",
164
+ );
43
165
  return { action: "new", ...publicTab(tab) };
44
166
  }
45
167
  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)) };
168
+ throwIfCancelled(state);
169
+ if (action === "activate") {
170
+ const activated = await invokeBrowserTabMutation(() => chrome.tabs.update(tab.id, { active: true }), "activate_tab");
171
+ throwIfCancelled(state);
172
+ if (!Number.isInteger(activated?.windowId) || activated.windowId < 1) {
173
+ throw new Error("browser tab activation completed but its current window is unavailable before focus; inspect tabs before retrying");
174
+ }
175
+ let focusTarget;
176
+ try { focusTarget = await chrome.tabs.get(tab.id); }
177
+ catch { throw new Error("browser tab activation completed but the target tab could not be verified before focus; inspect tabs before retrying"); }
178
+ throwIfCancelled(state);
179
+ if (!Number.isInteger(focusTarget?.windowId) || focusTarget.windowId < 1) {
180
+ throw new Error("browser tab activation completed but its current window is unavailable before focus; inspect tabs before retrying");
181
+ }
182
+ if (focusTarget.active !== true) {
183
+ throw new Error("browser tab activation completed but the target tab was no longer active before focus; inspect tabs before retrying");
184
+ }
185
+ const focusWindowId = focusTarget.windowId;
186
+ await invokeBrowserTabMutation(() => chrome.windows.update(focusWindowId, { focused: true }), "focus_window");
187
+ let settled;
188
+ try { settled = await chrome.tabs.get(tab.id); }
189
+ catch { throw new Error("browser tab activation and window focus completed but the target tab could not be verified; inspect tabs before retrying"); }
190
+ if (settled?.windowId !== focusWindowId) {
191
+ throw new Error("browser tab activation and window focus completed but the target tab moved windows; inspect tabs before retrying");
192
+ }
193
+ if (settled.active !== true) {
194
+ throw new Error("browser tab activation and window focus completed but the target tab was no longer active; inspect tabs before retrying");
195
+ }
196
+ return { action: "activate", ...publicTab(settled) };
50
197
  }
51
- if (params.action === "close") {
52
- const result = { action: "close", closed: true, ...publicTab(tab) };
53
- await chrome.tabs.remove(tab.id);
54
- return result;
198
+ const result = { action: "close", closed: true, ...publicTab(tab) };
199
+ await invokeBrowserTabMutation(() => chrome.tabs.remove(tab.id), "close");
200
+ return result;
201
+ }
202
+
203
+ function normalizeBrowserWaitParams(params) {
204
+ if (!params || typeof params !== "object" || Array.isArray(params)) throw new Error("browser wait parameters are invalid");
205
+ const timeoutMs = exactBoundedInteger(params.timeoutMs, "browser wait timeoutMs", 30000, 1, 120000);
206
+ let frameId = params.frameId;
207
+ if (frameId === undefined || frameId === null) frameId = null;
208
+ else if (!Number.isSafeInteger(frameId) || frameId < 0) throw new Error("browser wait frame id is invalid");
209
+ const selector = params.selector === undefined || params.selector === null ? null : params.selector;
210
+ const waitState = exactOptionalText(params.state, "browser wait state", 32);
211
+ if (waitState && !["attached", "detached", "visible", "hidden", "enabled", "editable", "checked", "unchecked"].includes(waitState)) {
212
+ throw new Error("browser wait state is invalid");
55
213
  }
56
- throw new Error("unsupported browser tab action");
214
+ if (waitState && !selector) throw new Error("browser wait state requires selector");
215
+ const text = exactOptionalText(params.text, "browser wait text", 4000);
216
+ const urlContains = exactOptionalText(params.urlContains, "browser wait urlContains", 32768);
217
+ const loadState = exactOptionalText(params.loadState, "browser wait loadState", 32);
218
+ if (loadState && !["domcontentloaded", "complete"].includes(loadState)) throw new Error("browser wait loadState is invalid");
219
+ if (!selector && !text && !urlContains && !loadState) throw new Error("browser wait requires a condition");
220
+ return { tabId: params.tabId, frameId, selector, state: waitState, text, urlContains, loadState, timeoutMs };
57
221
  }
58
-
222
+
223
+ function pageWaitPayload(params) {
224
+ return { selector: params.selector, state: params.state, text: params.text, loadState: params.loadState };
225
+ }
226
+
59
227
  async function browserWait(params, state) {
228
+ params = normalizeBrowserWaitParams(params);
60
229
  const tab = await resolveTab(params.tabId);
61
- const timeoutMs = Math.max(1, Number(params.timeoutMs) || 30000);
230
+ const timeoutMs = params.timeoutMs;
62
231
  const startedAt = performance.now();
63
232
  let last = null;
64
233
  while (performance.now() - startedAt <= timeoutMs) {
65
234
  throwIfCancelled(state);
66
235
  const current = await chrome.tabs.get(tab.id);
67
- const urlMatched = !params.urlContains || String(current.url || "").includes(params.urlContains);
236
+ const currentUrl = exactOptionalAuthorityString(current?.url, 32768);
237
+ const urlMatched = !params.urlContains || currentUrl.includes(params.urlContains);
68
238
  const needsPage = Boolean(params.selector || params.text || params.loadState);
69
239
  let page = { matched: true, ready_state: current.status || "" };
70
240
  if (needsPage) {
71
241
  try {
72
242
  assertPageTab(current);
73
- const [execution] = await executePageAutomation(scriptTarget(current.id, params.frameId, false), "checkWait", params);
243
+ const [execution] = await executePageAutomation(scriptTarget(current.id, params.frameId, false), "checkWait", pageWaitPayload(params));
74
244
  page = execution?.result || { matched: false };
75
245
  } catch (error) {
76
246
  const message = String(error?.message || error).replace(/\s+/g, " ").slice(0, 500);
@@ -87,23 +257,32 @@
87
257
  throw new Error(`browser wait timed out; last condition: ${JSON.stringify(last || {})}`);
88
258
  }
89
259
  function scriptTarget(tabId, frameId, allFrames) {
90
- if (Number.isInteger(frameId)) return { tabId, frameIds: [frameId] };
91
- if (allFrames) return { tabId, allFrames: true };
260
+ if (!Number.isSafeInteger(tabId) || tabId < 1) throw new Error("browser script target tab is invalid");
261
+ if (frameId !== undefined && frameId !== null) {
262
+ if (!Number.isSafeInteger(frameId) || frameId < 0) throw new Error("browser script target frame is invalid");
263
+ return { tabId, frameIds: [frameId] };
264
+ }
265
+ if (allFrames === true) return { tabId, allFrames: true };
266
+ if (allFrames !== undefined && allFrames !== false) throw new Error("browser script allFrames flag is invalid");
92
267
  return { tabId };
93
268
  }
94
269
 
95
270
  async function resolveTab(tabId) {
96
- if (Number.isInteger(tabId)) return chrome.tabs.get(tabId);
271
+ if (tabId !== undefined && tabId !== null) {
272
+ if (!Number.isSafeInteger(tabId) || tabId < 1) throw new Error("browser tab id is invalid");
273
+ return chrome.tabs.get(tabId);
274
+ }
97
275
  const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
98
276
  if (!tab?.id) throw new Error("no active browser tab");
99
277
  return tab;
100
278
  }
101
279
 
102
280
  async function getSource(params, state) {
281
+ const maxBytes = exactBoundedInteger(params.maxBytes, "browser source maxBytes", 1024 * 1024, 1, 4 * 1024 * 1024);
282
+ const allFrames = exactBoolean(params.allFrames, "browser source allFrames", false);
103
283
  const tab = await resolveTab(params.tabId);
104
284
  assertPageTab(tab);
105
- const maxBytes = Math.max(1, Number(params.maxBytes) || 1024 * 1024);
106
- const selection = await discoverPageFrames(tab.id, params.frameId, params.allFrames === true);
285
+ const selection = await discoverPageFrames(tab.id, params.frameId, allFrames);
107
286
  let remainingBytes = maxBytes;
108
287
  const frames = [];
109
288
  for (let index = 0; index < selection.frames.length && remainingBytes > 0; index += 1) {
@@ -116,7 +295,10 @@
116
295
  args: [frameBudget],
117
296
  });
118
297
  const result = execution?.result || { source: "", bytes: 0, returned_bytes: 0, truncated: true, url: selection.frames[index].url };
119
- const returnedBytes = Math.max(0, Math.min(frameBudget, Number(result.returned_bytes ?? result.bytes) || 0));
298
+ const rawReturnedBytes = result.returned_bytes ?? result.bytes;
299
+ const returnedBytes = Number.isSafeInteger(rawReturnedBytes) && rawReturnedBytes >= 0
300
+ ? Math.min(frameBudget, rawReturnedBytes)
301
+ : frameBudget;
120
302
  remainingBytes -= returnedBytes;
121
303
  frames.push({ frame_id: execution?.frameId ?? selection.frames[index].frameId, ...result, returned_bytes: returnedBytes, bytes: returnedBytes });
122
304
  }
@@ -132,37 +314,833 @@
132
314
  }
133
315
 
134
316
  async function inspectPage(params, state) {
317
+ const maxElements = exactBoundedInteger(params.maxElements, "browser inspection maxElements", 300, 1, 1000);
318
+ const allFrames = exactBoolean(params.allFrames, "browser inspection allFrames", true);
319
+ const includeValues = exactBoolean(params.includeValues, "browser inspection includeValues", false);
320
+ const includePrivateHistory = exactBoolean(params.includePrivateHistory, "browser inspection includePrivateHistory", false);
321
+ const focusQueryInput = exactOptionalText(params.focusQuery, "browser inspection focusQuery", 1000);
135
322
  const tab = await resolveTab(params.tabId);
136
323
  assertPageTab(tab);
137
- const maxElements = Math.max(1, Number(params.maxElements) || 300);
138
- const selection = await discoverPageFrames(tab.id, params.frameId, params.allFrames === true, maxElements);
139
- let remainingElements = maxElements;
140
- const frames = [];
141
- for (let index = 0; index < selection.frames.length && remainingElements > 0; index += 1) {
324
+ const selection = await discoverPageFrames(tab.id, params.frameId, allFrames, MAX_ACCESSIBLE_FRAMES);
325
+ const frameCount = Math.max(1, selection.frames.length);
326
+ const probeBudget = Math.max(1, Math.min(maxElements, Math.ceil((maxElements * 2) / frameCount)));
327
+ const probedFrames = [];
328
+ const candidates = [];
329
+ for (let frameIndex = 0; frameIndex < selection.frames.length; frameIndex += 1) {
142
330
  throwIfCancelled(state);
143
- const remainingFrames = selection.frames.length - index;
144
- const frameBudget = Math.max(1, Math.floor(remainingElements / remainingFrames));
331
+ const frame = selection.frames[frameIndex];
145
332
  const [execution] = await executePageAutomation(
146
- { tabId: tab.id, frameIds: [selection.frames[index].frameId] },
333
+ { tabId: tab.id, frameIds: [frame.frameId] },
147
334
  "inspect",
148
- { maxElements: frameBudget, includeValues: params.includeValues === true },
335
+ {
336
+ maxElements: probeBudget,
337
+ includeValues,
338
+ focusQuery: focusQueryInput,
339
+ includePrivateHistory,
340
+ },
149
341
  );
150
342
  const result = execution?.result || { elements: [], truncated: true };
151
- const used = Math.min(frameBudget, Array.isArray(result.elements) ? result.elements.length : 0);
152
- remainingElements -= used;
153
- frames.push({ frame_id: execution?.frameId ?? selection.frames[index].frameId, ...result });
343
+ const actualFrameId = execution?.frameId ?? frame.frameId;
344
+ const elements = Array.isArray(result.elements) ? result.elements : [];
345
+ probedFrames.push({ frame_id: actualFrameId, ...result, elements: [] });
346
+ for (let elementIndex = 0; elementIndex < elements.length; elementIndex += 1) {
347
+ const element = elements[elementIndex];
348
+ candidates.push({
349
+ frameIndex,
350
+ frameId: actualFrameId,
351
+ element,
352
+ elementIndex,
353
+ score: typeof element?.salience_score === "number" && Number.isFinite(element.salience_score) ? element.salience_score : 0,
354
+ topFrame: actualFrameId === 0,
355
+ });
356
+ }
154
357
  }
358
+ candidates.sort((left, right) =>
359
+ right.score - left.score
360
+ || (right.topFrame ? 1 : 0) - (left.topFrame ? 1 : 0)
361
+ || left.frameId - right.frameId
362
+ || left.elementIndex - right.elementIndex);
363
+ const selected = candidates.slice(0, maxElements);
364
+ for (const candidate of selected) probedFrames[candidate.frameIndex].elements.push(candidate.element);
365
+ const probeTruncated = probedFrames.some((frame) => frame.truncated === true);
366
+ const focusQuery = focusQueryInput.trim().replace(/\s+/g, " ").toLowerCase();
367
+ const queryMatchCount = focusQuery
368
+ ? probedFrames.reduce((sum, frame) => {
369
+ const count = exactNonNegativeInteger(frame.document?.focus_query_match_count);
370
+ return sum + (count ?? 0);
371
+ }, 0)
372
+ : null;
373
+ const querySearchExhaustive = focusQuery
374
+ ? selection.truncated !== true && probedFrames.every((frame) => frame.document?.focus_query_search_exhaustive === true)
375
+ : null;
155
376
  return {
156
377
  tab_id: tab.id,
157
378
  title: tab.title || "",
158
379
  url: tab.url || "",
159
- frames,
160
- total_elements: maxElements - remainingElements,
380
+ frames: probedFrames,
381
+ total_elements: selected.length,
161
382
  max_elements: maxElements,
162
383
  frames_truncated: selection.truncated,
384
+ selection: {
385
+ strategy: "global_salience",
386
+ frames_scanned: probedFrames.length,
387
+ probed_elements: candidates.length,
388
+ per_frame_probe_budget: probeBudget,
389
+ candidate_truncated: probeTruncated || candidates.length > selected.length,
390
+ focus_query: focusQuery,
391
+ query_matched: focusQuery ? queryMatchCount > 0 : null,
392
+ query_match_count: queryMatchCount,
393
+ query_search_exhaustive: querySearchExhaustive,
394
+ },
163
395
  };
164
396
  }
165
-
397
+
398
+ async function documentState(params) {
399
+ const tab = await resolveTab(params.tabId);
400
+ assertPageTab(tab);
401
+ const [execution] = await executePageAutomation({ tabId: tab.id, frameIds: [0] }, "documentState", {});
402
+ const state = execution?.result || {};
403
+ return {
404
+ tab_id: tab.id,
405
+ title: tab.title || "",
406
+ url: tab.url || "",
407
+ document_epoch: exactOptionalAuthorityString(state.epoch, 9000),
408
+ document_url: exactOptionalAuthorityString(state.url, 8192),
409
+ _machine_history_entry_key: exactOptionalAuthorityString(state._machine_history_entry_key, 512),
410
+ ready_state: String(state.ready_state || ""),
411
+ viewport: state.viewport && typeof state.viewport === "object" ? state.viewport : null,
412
+ };
413
+ }
414
+
415
+ async function pointAction(params, state) {
416
+ const tabId = exactPositiveInteger(params.tabId);
417
+ if (tabId === null) throw new Error("visual snapshot tab id is required before dispatch");
418
+ const expectedDocumentEpoch = requiredSnapshotAuthorityString(params.expectedDocumentEpoch, "visual snapshot document epoch", 9000);
419
+ const expectedViewport = requiredSnapshotViewport(params.expectedViewport);
420
+ const expectedScreenshotSha256 = exactOptionalSha256(params.expectedScreenshotSha256, "visual snapshot digest");
421
+ if (!expectedScreenshotSha256) throw new Error("visual snapshot digest is required before dispatch");
422
+ const visualSnapshot = {
423
+ expectedScreenshotSha256,
424
+ screenshotFormat: exactImageFormat(params.screenshotFormat, "visual snapshot format"),
425
+ screenshotQuality: exactBoundedInteger(params.screenshotQuality, "visual snapshot quality", 90, 1, 100),
426
+ };
427
+ throwIfCancelled(state);
428
+ const tab = await resolveTab(tabId);
429
+ assertPageTab(tab);
430
+ const action = exactOptionalAuthorityString(params.action, 32);
431
+ if (!["click", "double_click", "hover", "drag", "scroll"].includes(action)) throw new Error("visual point action must be click, double_click, hover, drag, or scroll");
432
+ const scrollDeltas = action === "scroll" ? normalizeScrollDeltas(params) : null;
433
+ const normalizedX = exactNormalizedCoordinate(params.normalizedX);
434
+ const normalizedY = exactNormalizedCoordinate(params.normalizedY);
435
+ if (normalizedX === null || normalizedY === null) {
436
+ throw new Error("visual point must use normalized viewport coordinates from 0 (inclusive) to 1 (exclusive)");
437
+ }
438
+ const currentState = await documentState({ tabId: tab.id });
439
+ if (currentState.document_epoch !== expectedDocumentEpoch) {
440
+ throw new Error("computer visual snapshot is stale because the document was replaced");
441
+ }
442
+ const viewport = currentState.viewport || {};
443
+ const width = exactPositiveFiniteNumber(viewport.width);
444
+ const height = exactPositiveFiniteNumber(viewport.height);
445
+ const scale = exactPositiveFiniteNumber(viewport.scale);
446
+ const offsetLeft = exactFiniteNumber(viewport.offset_left);
447
+ const offsetTop = exactFiniteNumber(viewport.offset_top);
448
+ if (width === null || height === null || scale === null || offsetLeft === null || offsetTop === null) {
449
+ throw new Error("browser viewport dimensions are unavailable for visual point action");
450
+ }
451
+ if (Math.abs(scale - 1) > 0.001 || Math.abs(offsetLeft) > 0.5 || Math.abs(offsetTop) > 0.5) {
452
+ throw new Error("visual point action is unavailable while the visual viewport is zoomed or offset");
453
+ }
454
+ if (expectedViewport && !sameViewport(expectedViewport, viewport)) {
455
+ throw new Error("computer visual snapshot is stale because the viewport changed");
456
+ }
457
+ const point = { x: normalizedX * width, y: normalizedY * height };
458
+ const [probe] = await executePageAutomation({ tabId: tab.id, frameIds: [0] }, "pointProbe", point);
459
+ let destinationPoint = null;
460
+ let destinationProbe = null;
461
+ if (action === "drag") {
462
+ const destinationNormalizedX = exactNormalizedCoordinate(params.destinationNormalizedX);
463
+ const destinationNormalizedY = exactNormalizedCoordinate(params.destinationNormalizedY);
464
+ if (destinationNormalizedX === null || destinationNormalizedY === null) {
465
+ throw new Error("visual drag destination must use normalized viewport coordinates from 0 (inclusive) to 1 (exclusive)");
466
+ }
467
+ destinationPoint = { x: destinationNormalizedX * width, y: destinationNormalizedY * height };
468
+ [destinationProbe] = await executePageAutomation({ tabId: tab.id, frameIds: [0] }, "pointProbe", destinationPoint);
469
+ }
470
+ throwIfCancelled(state);
471
+ try {
472
+ const api = globalThis.__machineBridgeDevtoolsInput;
473
+ if (!api?.perform) throw trustedUnavailableError();
474
+ await api.perform(tab.id, action, {
475
+ point,
476
+ ...(destinationPoint ? { destinationPoint } : {}),
477
+ ...(scrollDeltas || {}),
478
+ ...(visualSnapshot || {}),
479
+ beforeDispatch: () => throwIfCancelledBeforeTrustedInput(state),
480
+ });
481
+ } catch (error) {
482
+ if (error?.machineBridgeBeforeDispatchAbort === true) throw error;
483
+ if (error?.safeToFallback === true) throw error;
484
+ const detail = String(error?.message || error).replace(/\s+/g, " ").slice(0, 500);
485
+ throw new Error(`trusted browser input may have been partially dispatched; the action outcome is unknown. Inspect the page before retrying. (${detail})`);
486
+ }
487
+ const current = await postActionTabMetadata(tab.id);
488
+ return {
489
+ ok: true,
490
+ input_mode: "trusted",
491
+ trusted_input_fallback: false,
492
+ ...current,
493
+ point: { normalized_x: normalizedX, normalized_y: normalizedY, css_x: point.x, css_y: point.y },
494
+ ...(scrollDeltas ? { scroll_delta: { delta_x: scrollDeltas.deltaX, delta_y: scrollDeltas.deltaY } } : {}),
495
+ ...(destinationPoint ? { destination_point: {
496
+ normalized_x: params.destinationNormalizedX, normalized_y: params.destinationNormalizedY,
497
+ css_x: destinationPoint.x, css_y: destinationPoint.y,
498
+ } } : {}),
499
+ hit: probe?.result?.hit || null,
500
+ ...(destinationPoint ? { destination_hit: destinationProbe?.result?.hit || null } : {}),
501
+ };
502
+ }
503
+
504
+ async function backendNodeAction(params, state) {
505
+ const tabId = exactPositiveInteger(params.tabId);
506
+ if (tabId === null) throw new Error("snapshot backend tab id is required before dispatch");
507
+ const expectedDocumentEpoch = requiredSnapshotAuthorityString(params.expectedDocumentEpoch, "snapshot backend document epoch", 9000);
508
+ throwIfCancelled(state);
509
+ const tab = await resolveTab(tabId);
510
+ assertPageTab(tab);
511
+ const action = exactOptionalAuthorityString(params.action, 32);
512
+ const pointerActions = new Set(["click", "double_click", "hover", "drag", "scroll"]);
513
+ const focusActions = new Set(["press", "type_text", "fill", "check", "uncheck", "submit"]);
514
+ if (!pointerActions.has(action) && !focusActions.has(action)) {
515
+ throw new Error("snapshot backend action must be click, double_click, hover, drag, scroll, press, type_text, fill, check, uncheck, or submit");
516
+ }
517
+ const backendNodeId = exactPositiveInteger(params.backendNodeId);
518
+ if (backendNodeId === null) throw new Error("snapshot backend action requires a valid backend node");
519
+ const destinationBackendNodeId = action === "drag" ? exactPositiveInteger(params.destinationBackendNodeId) : null;
520
+ if (action === "drag" && destinationBackendNodeId === null) {
521
+ throw new Error("snapshot backend drag requires a valid destination backend node");
522
+ }
523
+ const scrollDeltas = action === "scroll" ? normalizeScrollDeltas(params) : null;
524
+ const currentState = await documentState({ tabId: tab.id });
525
+ if (currentState.document_epoch !== expectedDocumentEpoch) {
526
+ throw new Error("snapshot_backend_target_changed_before_dispatch");
527
+ }
528
+ if (params.expectedFrameDocumentEpoch) {
529
+ const extensionFrameId = exactNonNegativeInteger(params.extensionFrameId);
530
+ let frameState = null;
531
+ if (extensionFrameId === 0) {
532
+ frameState = { epoch: currentState.document_epoch, url: currentState.document_url };
533
+ } else if (Number.isInteger(extensionFrameId) && extensionFrameId > 0) {
534
+ try {
535
+ const [execution] = await executePageAutomation({ tabId: tab.id, frameIds: [extensionFrameId] }, "documentState", {});
536
+ frameState = execution?.result || null;
537
+ } catch { /* Missing frame state fails closed below as a stale snapshot target. */ }
538
+ }
539
+ if (!frameState
540
+ || exactOptionalAuthorityString(frameState.epoch, 9000) !== exactOptionalAuthorityString(params.expectedFrameDocumentEpoch, 9000)
541
+ || (params.expectedFrameUrl && exactOptionalAuthorityString(frameState.url, 8192) !== exactOptionalAuthorityString(params.expectedFrameUrl, 8192))) {
542
+ throw new Error("snapshot_backend_target_changed_before_dispatch");
543
+ }
544
+ }
545
+ if (params.extensionRef && params.expectedIdentity) {
546
+ const extensionFrameId = exactNonNegativeInteger(params.extensionFrameId);
547
+ let identityMatched = false;
548
+ if (Number.isInteger(extensionFrameId) && extensionFrameId >= 0) {
549
+ try {
550
+ const [execution] = await executePageAutomation({ tabId: tab.id, frameIds: [extensionFrameId] }, "refIdentity", {
551
+ ref: params.extensionRef,
552
+ expectedIdentity: params.expectedIdentity,
553
+ });
554
+ identityMatched = execution?.result?.attached === true && execution?.result?.matched === true;
555
+ } catch { /* Failed identity read leaves identityMatched=false and fails closed below. */ }
556
+ }
557
+ if (!identityMatched) throw new Error("snapshot_backend_target_changed_before_dispatch");
558
+ }
559
+ if (action === "drag") {
560
+ if (params.destinationExpectedFrameDocumentEpoch) {
561
+ const destinationExtensionFrameId = exactNonNegativeInteger(params.destinationExtensionFrameId);
562
+ let frameState = null;
563
+ if (destinationExtensionFrameId === 0) {
564
+ frameState = { epoch: currentState.document_epoch, url: currentState.document_url };
565
+ } else if (Number.isInteger(destinationExtensionFrameId) && destinationExtensionFrameId > 0) {
566
+ try {
567
+ const [execution] = await executePageAutomation({ tabId: tab.id, frameIds: [destinationExtensionFrameId] }, "documentState", {});
568
+ frameState = execution?.result || null;
569
+ } catch { /* Missing destination frame state fails closed below as a stale snapshot target. */ }
570
+ }
571
+ if (!frameState
572
+ || exactOptionalAuthorityString(frameState.epoch, 9000) !== exactOptionalAuthorityString(params.destinationExpectedFrameDocumentEpoch, 9000)
573
+ || (params.destinationExpectedFrameUrl && exactOptionalAuthorityString(frameState.url, 8192) !== exactOptionalAuthorityString(params.destinationExpectedFrameUrl, 8192))) {
574
+ throw new Error("snapshot_backend_target_changed_before_dispatch");
575
+ }
576
+ }
577
+ if (params.destinationExtensionRef && params.destinationExpectedIdentity) {
578
+ const destinationExtensionFrameId = exactNonNegativeInteger(params.destinationExtensionFrameId);
579
+ let identityMatched = false;
580
+ if (Number.isInteger(destinationExtensionFrameId) && destinationExtensionFrameId >= 0) {
581
+ try {
582
+ const [execution] = await executePageAutomation({ tabId: tab.id, frameIds: [destinationExtensionFrameId] }, "refIdentity", {
583
+ ref: params.destinationExtensionRef,
584
+ expectedIdentity: params.destinationExpectedIdentity,
585
+ });
586
+ identityMatched = execution?.result?.attached === true && execution?.result?.matched === true;
587
+ } catch { /* Failed destination identity read fails closed below before dispatch. */ }
588
+ }
589
+ if (!identityMatched) throw new Error("snapshot_backend_target_changed_before_dispatch");
590
+ }
591
+ }
592
+ const session = globalThis.__machineBridgeDevtoolsSession;
593
+ const input = globalThis.__machineBridgeDevtoolsInput;
594
+ if (!session?.run || !input?.performWithSend) throw new Error("snapshot_backend_trusted_input_unavailable_before_dispatch");
595
+ let focusApplied = false;
596
+ let scrollApplied = false;
597
+ try {
598
+ const result = await session.run(tab.id, async ({ send }) => {
599
+ throwIfCancelled(state);
600
+ if (action === "check" || action === "uncheck") {
601
+ const desired = action === "check";
602
+ const checked = await backendNodeCheckedState(send, backendNodeId);
603
+ throwIfCancelled(state);
604
+ if (checked === desired) {
605
+ return { point: null, coordinateSource: "cdp_ax_state_noop", noInputRequired: true };
606
+ }
607
+ }
608
+ if (focusActions.has(action)) {
609
+ await focusBackendNode(send, backendNodeId);
610
+ focusApplied = true;
611
+ throwIfCancelled(state);
612
+ const focusResult = await performBackendFocusAction(input, send, action, params, backendNodeId, state);
613
+ return { point: null, coordinateSource: "cdp_dom_focus", ...focusResult };
614
+ }
615
+ if (action === "drag") {
616
+ const sourceGeometry = await backendNodeViewportGeometry(send, backendNodeId);
617
+ throwIfCancelled(state);
618
+ const destinationGeometry = await backendNodeViewportGeometry(send, destinationBackendNodeId);
619
+ throwIfCancelled(state);
620
+ if (!sourceGeometry.insideViewport || !destinationGeometry.insideViewport) {
621
+ throw new Error("snapshot_backend_drag_geometry_unavailable_before_dispatch");
622
+ }
623
+ await input.performWithSend(send, "drag", {
624
+ point: sourceGeometry.point,
625
+ destinationPoint: destinationGeometry.point,
626
+ });
627
+ return {
628
+ point: sourceGeometry.point,
629
+ destinationPoint: destinationGeometry.point,
630
+ coordinateSource: "cdp_content_quad",
631
+ };
632
+ }
633
+ if (action === "scroll") {
634
+ const geometry = await backendNodeViewportGeometry(send, backendNodeId);
635
+ throwIfCancelled(state);
636
+ if (!geometry.insideViewport) throw new Error("snapshot_backend_scroll_geometry_unavailable_before_dispatch");
637
+ await input.performWithSend(send, "scroll", {
638
+ point: geometry.point,
639
+ deltaX: scrollDeltas.deltaX,
640
+ deltaY: scrollDeltas.deltaY,
641
+ });
642
+ return {
643
+ point: geometry.point,
644
+ scrollDelta: scrollDeltas,
645
+ coordinateSource: "cdp_content_quad",
646
+ };
647
+ }
648
+ let geometry = await backendNodeViewportGeometry(send, backendNodeId);
649
+ throwIfCancelled(state);
650
+ if (!geometry.insideViewport) {
651
+ await scrollBackendNodeIntoView(send, backendNodeId);
652
+ scrollApplied = true;
653
+ throwIfCancelled(state);
654
+ geometry = await backendNodeViewportGeometry(send, backendNodeId);
655
+ if (!geometry.insideViewport) throw new Error("snapshot_backend_geometry_unavailable_before_dispatch");
656
+ }
657
+ const point = geometry.point;
658
+ throwIfCancelled(state);
659
+ await input.performWithSend(send, action, { point });
660
+ return { point, coordinateSource: "cdp_content_quad" };
661
+ }, { beforeAttach: () => throwIfCancelledBeforeTrustedInput(state) });
662
+ const current = await postActionTabMetadata(tab.id);
663
+ return {
664
+ ok: true,
665
+ input_mode: "trusted",
666
+ trusted_input_fallback: false,
667
+ coordinate_source: result.coordinateSource,
668
+ cross_frame_trusted: exactNonNegativeInteger(params.extensionFrameId) > 0,
669
+ ...current,
670
+ no_input_required: result.noInputRequired === true,
671
+ ...(result.point ? { point: result.point } : {}),
672
+ ...(result.destinationPoint ? { destination_point: result.destinationPoint } : {}),
673
+ ...(result.scrollDelta ? { scroll_delta: {
674
+ delta_x: result.scrollDelta.deltaX, delta_y: result.scrollDelta.deltaY,
675
+ } } : {}),
676
+ };
677
+ } catch (error) {
678
+ if (error?.machineBridgeFocusOutcomeUnknown === true
679
+ || error?.machineBridgeScrollOutcomeUnknown === true
680
+ || focusApplied
681
+ || scrollApplied) {
682
+ const detail = String(error?.message || error).replace(/\s+/g, " ").slice(0, 500);
683
+ throw new Error(`trusted browser input may have been partially dispatched; the action outcome is unknown. Inspect the page before retrying. (${detail})`);
684
+ }
685
+ if (error?.machineBridgeTrustedInput === true) {
686
+ if (error.safeToFallback === true) throw new Error("snapshot_backend_trusted_input_unavailable_before_dispatch");
687
+ const detail = String(error?.message || error).replace(/\s+/g, " ").slice(0, 500);
688
+ throw new Error(`trusted browser input may have been partially dispatched; the action outcome is unknown. Inspect the page before retrying. (${detail})`);
689
+ }
690
+ throw error;
691
+ }
692
+ }
693
+
694
+ async function scrollBackendNodeIntoView(send, backendNodeId) {
695
+ try { await send("DOM.scrollIntoViewIfNeeded", { backendNodeId }); } catch (error) {
696
+ if (backendTargetChanged(error)) throw new Error("snapshot_backend_target_changed_before_dispatch");
697
+ const failure = new Error("snapshot_backend_scroll_outcome_unknown");
698
+ Object.defineProperty(failure, "machineBridgeScrollOutcomeUnknown", { value: true });
699
+ throw failure;
700
+ }
701
+ }
702
+
703
+ async function focusBackendNode(send, backendNodeId) {
704
+ try { await send("DOM.focus", { backendNodeId }); } catch (error) {
705
+ if (backendTargetChanged(error)) throw new Error("snapshot_backend_target_changed_before_dispatch");
706
+ const failure = new Error("snapshot_backend_focus_outcome_unknown");
707
+ Object.defineProperty(failure, "machineBridgeFocusOutcomeUnknown", { value: true });
708
+ throw failure;
709
+ }
710
+ }
711
+
712
+ async function backendNodeViewportGeometry(send, backendNodeId) {
713
+ let quads;
714
+ try {
715
+ quads = (await send("DOM.getContentQuads", { backendNodeId }))?.quads;
716
+ } catch (error) {
717
+ if (backendTargetChanged(error)) throw new Error("snapshot_backend_target_changed_before_dispatch");
718
+ throw new Error("snapshot_backend_geometry_unavailable_before_dispatch");
719
+ }
720
+ const point = pointFromContentQuads(quads);
721
+ if (!point) throw new Error("snapshot_backend_geometry_unavailable_before_dispatch");
722
+ let metrics;
723
+ try { metrics = await send("Page.getLayoutMetrics"); }
724
+ catch { throw new Error("snapshot_backend_geometry_unavailable_before_dispatch"); }
725
+ const insideViewport = pointInsideViewport(point, metrics);
726
+ if (insideViewport === null) throw new Error("snapshot_backend_geometry_unavailable_before_dispatch");
727
+ return { point, insideViewport };
728
+ }
729
+
730
+ async function performBackendFocusAction(input, send, action, params, backendNodeId, state) {
731
+ if (action === "press") {
732
+ await input.performWithSend(send, "press", { key: boundedBackendKey(params.key) || "Enter" });
733
+ return {};
734
+ }
735
+ if (action === "check" || action === "uncheck") {
736
+ const desired = action === "check";
737
+ const checked = await backendNodeCheckedState(send, backendNodeId);
738
+ throwIfCancelled(state);
739
+ if (checked === desired) return { noInputRequired: true };
740
+ await input.performWithSend(send, "press", { key: "Space" });
741
+ return { noInputRequired: false };
742
+ }
743
+ if (action === "submit") {
744
+ await input.performWithSend(send, "press", { key: "Enter" });
745
+ return {};
746
+ }
747
+ const text = boundedBackendText(params.value);
748
+ if (action === "type_text") {
749
+ await input.performWithSend(send, "type_text", { text });
750
+ return {};
751
+ }
752
+ if (action === "fill") {
753
+ await input.performWithSend(send, "fill_text", { text, selectAllKey: selectAllShortcut() });
754
+ return {};
755
+ }
756
+ throw new Error("unsupported snapshot backend focus action");
757
+ }
758
+
759
+ async function backendNodeCheckedState(send, backendNodeId) {
760
+ let nodes;
761
+ try {
762
+ nodes = (await send("Accessibility.getPartialAXTree", {
763
+ backendNodeId,
764
+ fetchRelatives: false,
765
+ }))?.nodes;
766
+ } catch (error) {
767
+ if (backendTargetChanged(error)) throw new Error("snapshot_backend_target_changed_before_dispatch");
768
+ throw new Error("snapshot_backend_checked_state_unavailable_before_dispatch");
769
+ }
770
+ const candidates = Array.isArray(nodes) ? nodes : [];
771
+ const node = candidates.find((item) => Number.isSafeInteger(item?.backendDOMNodeId) && item.backendDOMNodeId === backendNodeId) || null;
772
+ const property = Array.isArray(node?.properties)
773
+ ? node.properties.find((item) => item?.name === "checked")
774
+ : null;
775
+ const value = property?.value?.value;
776
+ if (value === true || value === 1 || value === "true") return true;
777
+ if (value === false || value === 0 || value === "false") return false;
778
+ throw new Error("snapshot_backend_checked_state_unavailable_before_dispatch");
779
+ }
780
+
781
+ function boundedBackendText(value) {
782
+ if (typeof value !== "string") throw new Error("snapshot backend text action requires value");
783
+ if (value.includes("\0") || value.length > 128 * 1024) throw new Error("snapshot backend text value exceeds the maximum length or contains a NUL byte");
784
+ return value;
785
+ }
786
+
787
+ function boundedBackendKey(value) {
788
+ if (value === undefined || value === "") return "";
789
+ if (typeof value !== "string" || value.includes("\0") || value.length > 100) throw new Error("snapshot backend key is invalid");
790
+ return value;
791
+ }
792
+
793
+ function selectAllShortcut() {
794
+ const platform = String(globalThis.navigator?.userAgentData?.platform || globalThis.navigator?.platform || "").toLowerCase();
795
+ return platform.includes("mac") ? "Meta+A" : "Control+A";
796
+ }
797
+
798
+ function pointFromContentQuads(quads) {
799
+ let best = null;
800
+ let bestArea = 0;
801
+ for (const quad of Array.isArray(quads) ? quads : []) {
802
+ if (!Array.isArray(quad) || quad.length !== 8 || !quad.every((value) => typeof value === "number" && Number.isFinite(value))) continue;
803
+ const points = [];
804
+ for (let index = 0; index < 8; index += 2) points.push({ x: quad[index], y: quad[index + 1] });
805
+ let twiceArea = 0;
806
+ for (let index = 0; index < points.length; index += 1) {
807
+ const next = points[(index + 1) % points.length];
808
+ twiceArea += points[index].x * next.y - next.x * points[index].y;
809
+ }
810
+ const area = Math.abs(twiceArea) / 2;
811
+ if (area <= bestArea) continue;
812
+ bestArea = area;
813
+ best = {
814
+ x: points.reduce((sum, point) => sum + point.x, 0) / points.length,
815
+ y: points.reduce((sum, point) => sum + point.y, 0) / points.length,
816
+ };
817
+ }
818
+ return bestArea > 0 ? best : null;
819
+ }
820
+
821
+ function pointInsideViewport(point, metrics) {
822
+ const viewport = metrics?.cssVisualViewport || metrics?.visualViewport || {};
823
+ const width = exactPositiveFiniteNumber(viewport.clientWidth ?? viewport.width);
824
+ const height = exactPositiveFiniteNumber(viewport.clientHeight ?? viewport.height);
825
+ if (width === null || height === null) return null;
826
+ return point.x >= 0 && point.y >= 0 && point.x < width && point.y < height;
827
+ }
828
+
829
+ function backendTargetChanged(error) {
830
+ const message = String(error?.message || error || "").toLowerCase();
831
+ return /(?:node|backend).*(?:not found|does not exist|detached)/.test(message)
832
+ || /(?:not found|does not exist|detached|could not find|no such|no)\s+.*(?:node|backend)/.test(message)
833
+ || /could not find.*(?:node|backend)/.test(message);
834
+ }
835
+
836
+ function normalizeScrollDeltas(params) {
837
+ const deltaX = normalizeScrollDelta(params.deltaX, "deltaX");
838
+ const deltaY = normalizeScrollDelta(params.deltaY, "deltaY");
839
+ if (deltaX === 0 && deltaY === 0) throw new Error("scroll requires a non-zero delta");
840
+ return { deltaX, deltaY };
841
+ }
842
+
843
+ function normalizeScrollDelta(value, label) {
844
+ if (typeof value !== "number" || !Number.isFinite(value) || Math.abs(value) > 10_000) {
845
+ throw new Error(`${label} must be a finite number from -10000 to 10000`);
846
+ }
847
+ return Object.is(value, -0) ? 0 : value;
848
+ }
849
+
850
+ function sameViewport(expected, current) {
851
+ for (const key of ["width", "height", "scale"]) {
852
+ const left = expected?.[key];
853
+ const right = current?.[key];
854
+ if (typeof left !== "number" || typeof right !== "number"
855
+ || !Number.isFinite(left) || !Number.isFinite(right) || Math.abs(left - right) > 0.5) return false;
856
+ }
857
+ return true;
858
+ }
859
+
860
+ function trustedUnavailableError() {
861
+ const error = new Error("trusted input module is unavailable");
862
+ Object.defineProperty(error, "safeToFallback", { value: true });
863
+ return error;
864
+ }
865
+
866
+ async function observeComputer(params, state) {
867
+ const normalized = normalizeComputerObservationParams(params);
868
+ let last = null;
869
+ for (let attempt = 0; attempt < 2; attempt += 1) {
870
+ throwIfCancelled(state);
871
+ last = await captureComputerAttempt(normalized, state);
872
+ if (last.capture.navigation_coherent === true) return last;
873
+ }
874
+ throw new Error("page changed during computer observation; observe again");
875
+ }
876
+
877
+ function normalizeComputerObservationParams(params) {
878
+ if (!params || typeof params !== "object" || Array.isArray(params)) throw new Error("computer observation parameters are invalid");
879
+ return {
880
+ tabId: params.tabId,
881
+ allFrames: exactBoolean(params.allFrames, "computer observation allFrames", true),
882
+ maxElements: exactBoundedInteger(params.maxElements, "computer observation maxElements", 300, 1, 1000),
883
+ maxAxNodes: exactBoundedInteger(params.maxAxNodes, "computer observation maxAxNodes", 600, 1, 2000),
884
+ maxFrames: exactBoundedInteger(params.maxFrames, "computer observation maxFrames", 32, 1, MAX_ACCESSIBLE_FRAMES),
885
+ axDepth: exactBoundedInteger(params.axDepth, "computer observation axDepth", 12, 1, 16),
886
+ includeValues: exactBoolean(params.includeValues, "computer observation includeValues", false),
887
+ includeScreenshot: exactBoolean(params.includeScreenshot, "computer observation includeScreenshot", true),
888
+ format: exactImageFormat(params.format, "computer observation format"),
889
+ quality: exactBoundedInteger(params.quality, "computer observation quality", 90, 1, 100),
890
+ focusQuery: exactOptionalText(params.focusQuery, "computer observation focusQuery", 1000),
891
+ };
892
+ }
893
+
894
+ async function captureComputerAttempt(params, state) {
895
+ const tab = await resolveTab(params.tabId);
896
+ assertPageTab(tab);
897
+ const inspected = await inspectPage({
898
+ tabId: tab.id,
899
+ allFrames: params.allFrames,
900
+ maxElements: params.maxElements,
901
+ includeValues: params.includeValues === true,
902
+ focusQuery: params.focusQuery || "",
903
+ includePrivateHistory: true,
904
+ }, state);
905
+ const topFrame = inspected.frames.find((frame) => frame.frame_id === 0) || inspected.frames[0] || null;
906
+ const semanticEpoch = exactOptionalAuthorityString(topFrame?.document?.epoch, 9000);
907
+ let cdp = null;
908
+ let fallbackReason = "";
909
+ try {
910
+ const api = globalThis.__machineBridgeDevtoolsObservation;
911
+ if (!api?.capture) throw new Error("CDP observation module unavailable");
912
+ cdp = await api.capture(tab.id, {
913
+ maxNodes: params.maxAxNodes,
914
+ maxFrames: params.maxFrames,
915
+ depth: params.axDepth,
916
+ includeValues: params.includeValues === true,
917
+ includeScreenshot: params.includeScreenshot,
918
+ format: params.format,
919
+ quality: params.quality,
920
+ focusQuery: params.focusQuery || "",
921
+ }, state);
922
+ } catch (error) {
923
+ if (requestCancelled(error)) throw error;
924
+ fallbackReason = "cdp_observation_unavailable";
925
+ }
926
+
927
+ let fallbackScreenshot = null;
928
+ let screenshotFallbackReason = "";
929
+ if (params.includeScreenshot && !cdp?.screenshot?.data) {
930
+ const [activeTab] = await chrome.tabs.query({ active: true, windowId: tab.windowId }).catch(() => []);
931
+ if (activeTab?.id === tab.id) {
932
+ try {
933
+ fallbackScreenshot = await screenshot({
934
+ tabId: tab.id,
935
+ format: params.format,
936
+ quality: params.quality,
937
+ allowTabSwitch: false,
938
+ });
939
+ } catch (error) {
940
+ if (requestCancelled(error)) throw error;
941
+ screenshotFallbackReason = "visible_tab_fallback_unavailable";
942
+ }
943
+ } else {
944
+ screenshotFallbackReason = activeTab?.id
945
+ ? "visible_tab_fallback_skipped_inactive_target"
946
+ : "visible_tab_fallback_skipped_active_tab_unknown";
947
+ }
948
+ }
949
+
950
+ throwIfCancelled(state);
951
+ const postFrameStates = await browserFrameDocumentStates(tab.id, inspected.frames).catch(() => new Map());
952
+ throwIfCancelled(state);
953
+ const postState = postFrameStates.get(0) || null;
954
+ let current;
955
+ try { current = await chrome.tabs.get(tab.id); }
956
+ catch { throw new Error("browser tab became unavailable during computer observation"); }
957
+ throwIfCancelled(state);
958
+ const semanticFramesCoherent = browserSemanticFramesCoherent(inspected.frames, postFrameStates);
959
+ const semanticCoherent = Boolean(semanticEpoch && semanticFramesCoherent);
960
+ const cdpRootUrl = exactOptionalAuthorityString(cdp?.frame_tree?.[0]?.url, 8192);
961
+ const postUrl = exactOptionalAuthorityString(postState?.url, 8192);
962
+ const currentUrl = exactOptionalAuthorityString(current?.url, 32768);
963
+ const cdpCoherent = !cdp || (cdp.navigation_coherent === true && (!cdpRootUrl || (postUrl && cdpRootUrl === postUrl)));
964
+ const navigationCoherent = Boolean(semanticCoherent && cdpCoherent && currentUrl && postUrl && currentUrl === postUrl);
965
+
966
+ if (cdp?.accessibility?.nodes) fuseAccessibilityRefs(inspected.frames, cdp);
967
+ const screenshotResult = typeof cdp?.screenshot?.data === "string" && cdp.screenshot.data
968
+ ? {
969
+ mime_type: cdp.screenshot.mime_type,
970
+ data: `data:${cdp.screenshot.mime_type};base64,${cdp.screenshot.data}`,
971
+ source: "cdp_surface",
972
+ }
973
+ : typeof fallbackScreenshot?.data === "string" && fallbackScreenshot.data
974
+ ? {
975
+ mime_type: fallbackScreenshot.data.startsWith("data:image/jpeg") ? "image/jpeg" : "image/png",
976
+ data: fallbackScreenshot.data,
977
+ source: "capture_visible_tab_fallback",
978
+ }
979
+ : null;
980
+ return {
981
+ tab_id: current.id,
982
+ title: current.title || "",
983
+ url: current.url || "",
984
+ semantic: inspected,
985
+ accessibility: cdp?.accessibility || null,
986
+ viewport: cdp?.viewport || null,
987
+ frame_tree: cdp?.frame_tree || [],
988
+ document_epoch: exactOptionalAuthorityString(cdp?.document_epoch, 9000) || semanticEpoch,
989
+ _machine_history_entry_key: exactOptionalAuthorityString(postState?._machine_history_entry_key, 512),
990
+ capture: {
991
+ atomic: false,
992
+ navigation_coherent: navigationCoherent,
993
+ frame_epochs_coherent: semanticFramesCoherent,
994
+ semantic_epoch: semanticEpoch,
995
+ cdp_epoch: exactOptionalAuthorityString(cdp?.document_epoch, 9000),
996
+ cdp: Boolean(cdp),
997
+ cdp_components: cdp?.components || null,
998
+ screenshot_source: screenshotResult?.source || "none",
999
+ screenshot_format: screenshotResult?.mime_type === "image/jpeg" ? "jpeg" : screenshotResult ? "png" : "",
1000
+ screenshot_quality: params.format === "jpeg" ? params.quality : null,
1001
+ coherence: cdp
1002
+ ? "single_cdp_session_plus_stable_extension_document_epoch"
1003
+ : fallbackScreenshot?.data
1004
+ ? "stable_extension_document_epoch_with_visible_tab_screenshot_fallback"
1005
+ : "stable_extension_document_epoch_without_screenshot",
1006
+ ...(fallbackReason ? { fallback_reason: fallbackReason } : {}),
1007
+ ...(screenshotFallbackReason ? { screenshot_fallback_reason: screenshotFallbackReason } : {}),
1008
+ },
1009
+ screenshot: screenshotResult,
1010
+ };
1011
+ }
1012
+
1013
+ async function browserFrameDocumentStates(tabId, frames) {
1014
+ const frameIds = [...new Set((Array.isArray(frames) ? frames : [])
1015
+ .map((frame) => exactNonNegativeInteger(frame?.frame_id))
1016
+ .filter((frameId) => frameId !== null))].slice(0, MAX_ACCESSIBLE_FRAMES);
1017
+ if (!frameIds.length) return new Map();
1018
+ const executions = await executePageAutomation({ tabId, frameIds }, "documentState", {});
1019
+ const states = new Map();
1020
+ for (const execution of executions || []) {
1021
+ if (!Number.isInteger(execution?.frameId)) continue;
1022
+ const state = execution?.result;
1023
+ if (!state || typeof state !== "object") continue;
1024
+ states.set(execution.frameId, state);
1025
+ }
1026
+ return states;
1027
+ }
1028
+
1029
+ function browserSemanticFramesCoherent(frames, postStates) {
1030
+ const observed = Array.isArray(frames) ? frames : [];
1031
+ if (!observed.length || !(postStates instanceof Map)) return false;
1032
+ return observed.every((frame) => {
1033
+ const frameId = exactNonNegativeInteger(frame?.frame_id);
1034
+ const before = frame?.document || {};
1035
+ const after = frameId === null ? null : postStates.get(frameId);
1036
+ const epoch = exactOptionalAuthorityString(before.epoch, 9000);
1037
+ const url = exactOptionalAuthorityString(before.url, 8192);
1038
+ const historyEntryKey = exactOptionalAuthorityString(before._machine_history_entry_key, 512);
1039
+ return frameId !== null
1040
+ && Boolean(epoch)
1041
+ && Boolean(after)
1042
+ && exactOptionalAuthorityString(after.epoch, 9000) === epoch
1043
+ && exactOptionalAuthorityString(after.url, 8192) === url
1044
+ && exactOptionalAuthorityString(after._machine_history_entry_key, 512) === historyEntryKey;
1045
+ });
1046
+ }
1047
+
1048
+ function fuseAccessibilityRefs(frames, cdp) {
1049
+ const cdpFrameUrls = new Map();
1050
+ for (const frame of Array.isArray(cdp.frame_tree) ? cdp.frame_tree : []) {
1051
+ const frameId = exactOptionalAuthorityString(frame?.id, 32768);
1052
+ const frameUrl = exactOptionalAuthorityString(frame?.url, 8192);
1053
+ if (frameId && frameUrl) cdpFrameUrls.set(frameId, frameUrl);
1054
+ }
1055
+ const frameCandidates = new Map();
1056
+ for (const frame of frames || []) {
1057
+ const url = exactOptionalAuthorityString(frame?.document?.url, 8192);
1058
+ if (!url) continue;
1059
+ const list = frameCandidates.get(url) || [];
1060
+ list.push(frame);
1061
+ frameCandidates.set(url, list);
1062
+ }
1063
+ const usedRefs = new Set();
1064
+ for (const node of cdp.accessibility?.nodes || []) {
1065
+ if (!Number.isSafeInteger(node?.backend_dom_node_id) || node.backend_dom_node_id <= 0) continue;
1066
+ const nodeFrameId = exactOptionalAuthorityString(node.frame_id, 32768);
1067
+ if (!nodeFrameId) continue;
1068
+ const frameUrl = cdpFrameUrls.get(nodeFrameId) || "";
1069
+ const matchingFrames = frameCandidates.get(frameUrl) || [];
1070
+ if (matchingFrames.length !== 1) continue;
1071
+ const candidates = [];
1072
+ for (const element of Array.isArray(matchingFrames[0].elements) ? matchingFrames[0].elements : []) {
1073
+ if (typeof element?.ref !== "string" || !element.ref || element.ref.length > 100 || usedRefs.has(element.ref)) continue;
1074
+ const evidence = accessibilityMatchEvidence(node, element);
1075
+ if (evidence.score > 0) candidates.push({ element, ...evidence });
1076
+ }
1077
+ candidates.sort((left, right) => right.score - left.score);
1078
+ if (!candidates.length || candidates[0].score < 100) continue;
1079
+ if (candidates[1] && candidates[0].score - candidates[1].score < 15) continue;
1080
+ node.action_ref = candidates[0].element.ref;
1081
+ const confidence = candidates[0].score >= 180 && candidates[0].executable === true ? "high" : "medium";
1082
+ node.action_ref_confidence = confidence;
1083
+ if (confidence === "high" && Number.isSafeInteger(node.backend_dom_node_id) && node.backend_dom_node_id > 0) {
1084
+ candidates[0].element._machine_backend_node_id = node.backend_dom_node_id;
1085
+ }
1086
+ usedRefs.add(candidates[0].element.ref);
1087
+ }
1088
+ }
1089
+
1090
+ function accessibilityMatchEvidence(node, element) {
1091
+ let score = 0;
1092
+ const roleA = normalizedIdentity(node.role);
1093
+ const roleB = normalizedIdentity(element.role);
1094
+ const nameA = normalizedIdentity(node.name);
1095
+ const nameB = normalizedIdentity(element.name) || normalizedIdentity(element.label) || normalizedIdentity(element.placeholder);
1096
+ const roleExact = Boolean(roleA && roleB && roleA === roleB);
1097
+ const roleConflict = Boolean(roleA && roleB && roleA !== roleB);
1098
+ const nameExact = Boolean(nameA && nameB && nameA === nameB);
1099
+ const nameConflict = Boolean(nameA && nameB && nameA !== nameB);
1100
+ if (roleExact) score += 50;
1101
+ else if (roleConflict) score -= 30;
1102
+ if (nameExact) score += 60;
1103
+ else if (nameConflict) score -= 40;
1104
+ const geometry = boundingSimilarity(node.bounding_box, element.bounding_box);
1105
+ score += geometry.score;
1106
+ if (node.clickable === true && element.visible === true && element.enabled === true) score += 15;
1107
+ return {
1108
+ score,
1109
+ executable: geometry.strong === true
1110
+ && !roleConflict
1111
+ && !nameConflict
1112
+ && (roleExact || nameExact),
1113
+ };
1114
+ }
1115
+
1116
+ function boundingSimilarity(left, right) {
1117
+ if (!left || typeof left !== "object" || Array.isArray(left)
1118
+ || !right || typeof right !== "object" || Array.isArray(right)) return { score: 0, strong: false };
1119
+ const values = [left.x, left.y, left.width, left.height, right.x, right.y, right.width, right.height];
1120
+ if (!values.every((value) => typeof value === "number" && Number.isFinite(value))) return { score: 0, strong: false };
1121
+ const [ax1, ay1, aw, ah, bx1, by1, bw, bh] = values;
1122
+ if (aw <= 0 || ah <= 0 || bw <= 0 || bh <= 0) {
1123
+ return { score: 0, strong: false };
1124
+ }
1125
+ const ax2 = ax1 + aw; const ay2 = ay1 + ah; const bx2 = bx1 + bw; const by2 = by1 + bh;
1126
+ const intersection = Math.max(0, Math.min(ax2, bx2) - Math.max(ax1, bx1)) * Math.max(0, Math.min(ay2, by2) - Math.max(ay1, by1));
1127
+ const union = aw * ah + bw * bh - intersection;
1128
+ const iou = union > 0 ? intersection / union : 0;
1129
+ if (iou >= 0.9) return { score: 120, strong: true };
1130
+ if (iou >= 0.7) return { score: 100, strong: true };
1131
+ if (iou >= 0.5) return { score: 75, strong: false };
1132
+ const acx = ax1 + aw / 2; const acy = ay1 + ah / 2; const bcx = bx1 + bw / 2; const bcy = by1 + bh / 2;
1133
+ const distance = Math.hypot(acx - bcx, acy - bcy);
1134
+ if (distance <= 2) return { score: 55, strong: false };
1135
+ if (distance <= 5) return { score: 35, strong: false };
1136
+ return { score: 0, strong: false };
1137
+ }
1138
+
1139
+ function normalizedIdentity(value) {
1140
+ if (typeof value !== "string" || value.length > 4000 || value.includes("\0")) return "";
1141
+ return value.trim().replace(/\s+/g, " ").toLowerCase();
1142
+ }
1143
+
166
1144
  async function discoverPageFrames(tabId, frameId, allFrames, resultBudget = MAX_ACCESSIBLE_FRAMES) {
167
1145
  if (Number.isInteger(frameId)) return { frames: [{ frameId, url: "" }], truncated: false };
168
1146
  if (!allFrames) return { frames: [{ frameId: 0, url: "" }], truncated: false };
@@ -174,12 +1152,14 @@
174
1152
  .filter((item) => Number.isInteger(item.frameId))
175
1153
  .map((item) => ({ frameId: item.frameId, url: item.result?.url || "" }))
176
1154
  .sort((left, right) => (left.frameId === 0 ? -1 : right.frameId === 0 ? 1 : left.frameId - right.frameId));
177
- const limit = Math.max(1, Math.min(MAX_ACCESSIBLE_FRAMES, Number(resultBudget) || 1));
1155
+ const limit = Number.isSafeInteger(resultBudget) && resultBudget > 0
1156
+ ? Math.min(MAX_ACCESSIBLE_FRAMES, resultBudget)
1157
+ : 1;
178
1158
  return { frames: ordered.slice(0, limit), truncated: ordered.length > limit };
179
1159
  }
180
1160
 
181
1161
  function boundedDocumentSource(limit) {
182
- const maxBytes = Math.max(1, Number(limit) || 1);
1162
+ const maxBytes = Number.isSafeInteger(limit) && limit > 0 ? limit : 1;
183
1163
  const encoder = new TextEncoder();
184
1164
  const decoder = new TextDecoder("utf-8", { fatal: true });
185
1165
  const chunks = [];
@@ -281,15 +1261,108 @@
281
1261
  };
282
1262
  }
283
1263
 
1264
+ async function assertExpectedNavigationTab(tabId, expectedTabUrl) {
1265
+ const expected = exactOptionalAuthorityString(expectedTabUrl, 32768);
1266
+ if (!expected) return;
1267
+ let current;
1268
+ try { current = await chrome.tabs.get(tabId); }
1269
+ catch { throw new Error("snapshot browser tab could not be verified before navigation dispatch; observe again"); }
1270
+ if (exactOptionalAuthorityString(current?.url, 32768) !== expected) {
1271
+ throw new Error("snapshot browser tab changed before navigation dispatch; observe again");
1272
+ }
1273
+ }
1274
+
1275
+ async function assertExpectedHistoryDocument(tabId, expectedTabUrl, expectedDocumentEpoch, expectedHistoryEntryKey) {
1276
+ const expectedDocument = exactOptionalAuthorityString(expectedDocumentEpoch, 9000);
1277
+ const expectedHistoryEntry = exactOptionalAuthorityString(expectedHistoryEntryKey, 512);
1278
+ if (!expectedDocument && !expectedHistoryEntry) {
1279
+ await assertExpectedNavigationTab(tabId, expectedTabUrl);
1280
+ return;
1281
+ }
1282
+ let current;
1283
+ try { current = await documentState({ tabId }); }
1284
+ catch { throw new Error("snapshot history document could not be verified before dispatch; observe again"); }
1285
+ const expectedUrl = exactOptionalAuthorityString(expectedTabUrl, 32768);
1286
+ if (expectedUrl && (exactOptionalAuthorityString(current?.url, 32768) !== expectedUrl
1287
+ || exactOptionalAuthorityString(current?.document_url, 8192) !== expectedUrl)) {
1288
+ throw new Error("snapshot browser tab changed before navigation dispatch; observe again");
1289
+ }
1290
+ if (expectedDocument && exactOptionalAuthorityString(current?.document_epoch, 9000) !== expectedDocument) {
1291
+ throw new Error("snapshot history document changed before dispatch; observe again");
1292
+ }
1293
+ if (expectedHistoryEntry) {
1294
+ const currentHistoryEntry = exactOptionalAuthorityString(current?._machine_history_entry_key, 512);
1295
+ if (!currentHistoryEntry) throw new Error("snapshot history entry could not be verified before dispatch; observe again");
1296
+ if (currentHistoryEntry !== expectedHistoryEntry) throw new Error("snapshot history entry changed before dispatch; observe again");
1297
+ }
1298
+ }
1299
+
1300
+ function snapshotBoundHistoryAction(params) {
1301
+ return Boolean(exactOptionalAuthorityString(params?.expectedDocumentEpoch, 9000)
1302
+ || exactOptionalAuthorityString(params?.expectedHistoryEntryKey, 512));
1303
+ }
1304
+
1305
+ async function invokeSnapshotHistoryMutation(tab, params, state) {
1306
+ assertPageTab(tab);
1307
+ await executePageMutation({ tabId: tab.id, frameIds: [0] }, "historyAction", {
1308
+ action: params.action,
1309
+ expectedTabUrl: params.expectedTabUrl,
1310
+ expectedDocumentEpoch: params.expectedDocumentEpoch,
1311
+ expectedHistoryEntryKey: params.expectedHistoryEntryKey,
1312
+ }, state);
1313
+ }
1314
+
1315
+ function normalizeBrowserActionParams(params) {
1316
+ if (!params || typeof params !== "object" || Array.isArray(params)) throw new Error("browser action parameters are invalid before dispatch");
1317
+ const action = exactOptionalText(params.action, "browser action", 32);
1318
+ const allowed = new Set([
1319
+ "navigate", "reload", "back", "forward", "click", "double_click", "hover", "fill", "type_text", "select",
1320
+ "check", "uncheck", "focus", "press", "submit", "scroll_into_view",
1321
+ ]);
1322
+ if (!allowed.has(action)) throw new Error("unsupported browser action");
1323
+ let frameId = params.frameId;
1324
+ if (frameId === undefined || frameId === null) frameId = null;
1325
+ else if (!Number.isSafeInteger(frameId) || frameId < 0) throw new Error("browser action frame id is invalid before dispatch");
1326
+ const waitFor = params.waitFor === undefined ? "none" : exactOptionalText(params.waitFor, "browser action waitFor", 32);
1327
+ if (!["none", "domcontentloaded", "complete"].includes(waitFor)) throw new Error("browser action waitFor is invalid before dispatch");
1328
+ const inputMode = params.inputMode === undefined ? "auto" : exactOptionalText(params.inputMode, "browser action inputMode", 16);
1329
+ if (!["auto", "trusted", "dom"].includes(inputMode)) throw new Error("browser action inputMode is invalid before dispatch");
1330
+ const elementTimeoutMs = exactBoundedInteger(params.elementTimeoutMs, "browser action elementTimeoutMs", 10000, 1, 60000);
1331
+ const expectedTabUrl = exactOptionalAuthorityString(params.expectedTabUrl, 32768);
1332
+ const expectedDocumentEpoch = exactOptionalAuthorityString(params.expectedDocumentEpoch, 9000);
1333
+ const expectedHistoryEntryKey = exactOptionalAuthorityString(params.expectedHistoryEntryKey, 512);
1334
+ let value = params.value;
1335
+ if (value === undefined || value === null) value = null;
1336
+ else value = exactOptionalText(value, "browser action value", 131072);
1337
+ let key = params.key;
1338
+ if (key === undefined) key = "";
1339
+ else key = exactOptionalText(key, "browser action key", 100);
1340
+ let url = params.url;
1341
+ if (action === "navigate") {
1342
+ if (url === undefined || url === null || url === "") throw new Error("navigate requires url");
1343
+ if (typeof url !== "string" || !/^(https?|file):/i.test(url) || url.includes("\0") || url.length > 32768) {
1344
+ throw new Error("navigate requires a valid absolute url before dispatch");
1345
+ }
1346
+ } else if (url !== undefined && url !== null && url !== "") throw new Error("url is only valid for navigate before dispatch");
1347
+ else url = "";
1348
+ return {
1349
+ ...params, action, frameId, waitFor, inputMode, elementTimeoutMs, expectedTabUrl, expectedDocumentEpoch, expectedHistoryEntryKey, value, key, url,
1350
+ };
1351
+ }
1352
+
284
1353
  async function browserAction(params, state) {
1354
+ params = normalizeBrowserActionParams(params);
1355
+ throwIfCancelled(state);
285
1356
  const tab = await resolveTab(params.tabId);
1357
+ throwIfCancelled(state);
286
1358
  if (params.action === "navigate") {
287
- if (!params.url) throw new Error("navigate requires url");
288
1359
  const waiter = beginTabWait(tab.id, params.waitFor, state?.timeoutMs, state);
289
1360
  try {
290
- const updated = await chrome.tabs.update(tab.id, { url: params.url });
291
- await waiter.promise;
292
- return publicTab(await chrome.tabs.get(updated.id));
1361
+ await assertExpectedNavigationTab(tab.id, params.expectedTabUrl);
1362
+ throwIfCancelled(state);
1363
+ const updated = await invokeBrowserNavigationMutation(() => chrome.tabs.update(tab.id, { url: params.url }));
1364
+ await awaitPostDispatchWait(waiter);
1365
+ return postActionTabMetadata(updated.id);
293
1366
  } catch (error) {
294
1367
  waiter.cancel();
295
1368
  throw error;
@@ -298,9 +1371,17 @@
298
1371
  if (params.action === "reload") {
299
1372
  const waiter = beginTabWait(tab.id, params.waitFor, state?.timeoutMs, state);
300
1373
  try {
301
- await chrome.tabs.reload(tab.id);
302
- await waiter.promise;
303
- return publicTab(await chrome.tabs.get(tab.id));
1374
+ if (snapshotBoundHistoryAction(params)) {
1375
+ await invokeSnapshotHistoryMutation(tab, params, state);
1376
+ } else {
1377
+ await assertExpectedHistoryDocument(
1378
+ tab.id, params.expectedTabUrl, params.expectedDocumentEpoch, params.expectedHistoryEntryKey,
1379
+ );
1380
+ throwIfCancelled(state);
1381
+ await invokeBrowserNavigationMutation(() => chrome.tabs.reload(tab.id));
1382
+ }
1383
+ await awaitPostDispatchWait(waiter);
1384
+ return postActionTabMetadata(tab.id);
304
1385
  } catch (error) {
305
1386
  waiter.cancel();
306
1387
  throw error;
@@ -309,10 +1390,18 @@
309
1390
  if (params.action === "back" || params.action === "forward") {
310
1391
  const waiter = beginTabWait(tab.id, params.waitFor, state?.timeoutMs, state);
311
1392
  try {
312
- if (params.action === "back") await chrome.tabs.goBack(tab.id);
313
- else await chrome.tabs.goForward(tab.id);
314
- await waiter.promise;
315
- return publicTab(await chrome.tabs.get(tab.id));
1393
+ if (snapshotBoundHistoryAction(params)) {
1394
+ await invokeSnapshotHistoryMutation(tab, params, state);
1395
+ } else {
1396
+ await assertExpectedHistoryDocument(
1397
+ tab.id, params.expectedTabUrl, params.expectedDocumentEpoch, params.expectedHistoryEntryKey,
1398
+ );
1399
+ throwIfCancelled(state);
1400
+ if (params.action === "back") await invokeBrowserNavigationMutation(() => chrome.tabs.goBack(tab.id));
1401
+ else await invokeBrowserNavigationMutation(() => chrome.tabs.goForward(tab.id));
1402
+ }
1403
+ await awaitPostDispatchWait(waiter);
1404
+ return postActionTabMetadata(tab.id);
316
1405
  } catch (error) {
317
1406
  waiter.cancel();
318
1407
  throw error;
@@ -322,17 +1411,26 @@
322
1411
  const waiter = beginTabWait(tab.id, params.waitFor, state?.timeoutMs, state);
323
1412
  let result;
324
1413
  try {
325
- result = await performPageAction(tab, params);
326
- await waiter.promise;
1414
+ result = await performPageAction(tab, params, state);
1415
+ await awaitPostDispatchWait(waiter);
327
1416
  } catch (error) {
328
1417
  waiter.cancel();
329
1418
  throw error;
330
1419
  }
331
- const current = await chrome.tabs.get(tab.id).catch(() => tab);
332
- return { tab_id: current.id, title: current.title || "", url: current.url || "", ...result };
1420
+ const current = await postActionTabMetadata(tab.id);
1421
+ return { ...result, ...current };
333
1422
  }
334
1423
 
335
- async function performPageAction(tab, params) {
1424
+ function pageActionPayload(params) {
1425
+ return {
1426
+ action: params.action, selector: params.selector, value: params.value, key: params.key,
1427
+ elementTimeoutMs: params.elementTimeoutMs, expectedIdentity: params.expectedIdentity,
1428
+ };
1429
+ }
1430
+
1431
+ async function performPageAction(tab, params, state) {
1432
+ throwIfCancelled(state);
1433
+ const rendererParams = pageActionPayload(params);
336
1434
  const trustedActions = new Set(["click", "double_click", "hover", "press", "type_text"]);
337
1435
  if (params.inputMode === "trusted" && !trustedActions.has(params.action)) {
338
1436
  throw new Error("trusted input is unavailable for this action");
@@ -343,86 +1441,260 @@
343
1441
  throw new Error("trusted input currently requires the top frame; use input_mode=dom for a subframe");
344
1442
  }
345
1443
  if (wantsTrusted && topFrame) {
346
- const [prepared] = await executePageAutomation(scriptTarget(tab.id, params.frameId, false), "prepareAction", params);
1444
+ const api = globalThis.__machineBridgeDevtoolsInput;
1445
+ if (!api?.perform) {
1446
+ if (params.inputMode === "trusted") throw new Error("trusted input module is unavailable");
1447
+ const [fallback] = await executePageMutation(scriptTarget(tab.id, params.frameId, false), "action", rendererParams, state);
1448
+ return {
1449
+ ...fallback.result,
1450
+ input_mode: "dom",
1451
+ trusted_input_fallback: true,
1452
+ fallback_reason: "trusted_input_unavailable_before_dispatch",
1453
+ };
1454
+ }
1455
+ const [prepared] = await executePageMutation(scriptTarget(tab.id, params.frameId, false), "prepareAction", rendererParams, state);
1456
+ throwIfCancelled(state);
347
1457
  try {
348
- const api = globalThis.__machineBridgeDevtoolsInput;
349
- if (!api?.perform) {
350
- const unavailable = new Error("trusted input module is unavailable");
351
- Object.defineProperty(unavailable, "safeToFallback", { value: true });
352
- throw unavailable;
353
- }
354
1458
  await api.perform(tab.id, params.action, {
355
1459
  point: prepared.result?.point,
356
1460
  key: params.key || params.value || "Enter",
357
1461
  text: params.value || "",
1462
+ beforeDispatch: () => throwIfCancelledBeforeTrustedInput(state),
358
1463
  });
359
1464
  return { ...prepared.result, input_mode: "trusted", trusted_input_fallback: false };
360
1465
  } catch (error) {
361
- if (params.inputMode === "trusted") throw error;
362
- if (error?.safeToFallback !== true) {
363
- const detail = String(error?.message || error).replace(/\s+/g, " ").slice(0, 500);
364
- throw new Error(`trusted browser input may have been partially dispatched; the action outcome is unknown. Inspect the page before retrying. (${detail})`);
365
- }
366
- const [fallback] = await executePageAutomation(scriptTarget(tab.id, params.frameId, false), "action", params);
367
- return {
368
- ...fallback.result,
369
- input_mode: "dom",
370
- trusted_input_fallback: true,
371
- fallback_reason: String(error?.message || error).slice(0, 500),
372
- };
1466
+ const detail = String(error?.message || error).replace(/\s+/g, " ").slice(0, 500);
1467
+ throw new Error(`trusted browser input may have been partially dispatched; the action outcome is unknown. Inspect the page before retrying. (${detail})`);
373
1468
  }
374
1469
  }
375
- const [execution] = await executePageAutomation(scriptTarget(tab.id, params.frameId, false), "action", params);
1470
+ throwIfCancelled(state);
1471
+ const [execution] = await executePageMutation(scriptTarget(tab.id, params.frameId, false), "action", rendererParams, state);
376
1472
  return { ...execution.result, input_mode: "dom", trusted_input_fallback: false };
377
1473
  }
1474
+ function utf8ByteLength(value) {
1475
+ let bytes = 0;
1476
+ for (let index = 0; index < value.length; index += 1) {
1477
+ const code = value.charCodeAt(index);
1478
+ if (code < 0x80) bytes += 1;
1479
+ else if (code < 0x800) bytes += 2;
1480
+ else if (code >= 0xd800 && code <= 0xdbff && index + 1 < value.length) {
1481
+ const next = value.charCodeAt(index + 1);
1482
+ if (next >= 0xdc00 && next <= 0xdfff) { bytes += 4; index += 1; } else bytes += 3;
1483
+ } else bytes += 3;
1484
+ }
1485
+ return bytes;
1486
+ }
1487
+
1488
+ function exactBrokerFrameId(value, label) {
1489
+ if (value === undefined || value === null) return null;
1490
+ if (!Number.isSafeInteger(value) || value < 0) throw new Error(`${label} is invalid before dispatch`);
1491
+ return value;
1492
+ }
1493
+
1494
+ function exactBrokerSelector(value, label) {
1495
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${label} must be an object before dispatch`);
1496
+ return value;
1497
+ }
1498
+
1499
+ function normalizeBrokerFillFormParams(params) {
1500
+ if (!params || typeof params !== "object" || Array.isArray(params)) throw new Error("browser fill form parameters are invalid before dispatch");
1501
+ const allowed = new Set(["tabId", "frameId", "fields", "submit", "submitSelector", "waitFor", "elementTimeoutMs"]);
1502
+ for (const key of Object.keys(params)) if (!allowed.has(key)) throw new Error(`unknown browser fill form property before dispatch: ${key}`);
1503
+ if (!Array.isArray(params.fields) || params.fields.length < 1 || params.fields.length > 200) throw new Error("browser fill form fields are invalid before dispatch");
1504
+ const frameId = exactBrokerFrameId(params.frameId, "browser fill form frame id");
1505
+ const submit = exactBoolean(params.submit, "browser fill form submit", false);
1506
+ const submitSelector = params.submitSelector === undefined || params.submitSelector === null ? null : exactBrokerSelector(params.submitSelector, "browser fill form submit selector");
1507
+ const waitFor = params.waitFor === undefined ? "none" : exactOptionalText(params.waitFor, "browser fill form waitFor", 32);
1508
+ if (!["none", "domcontentloaded", "complete"].includes(waitFor)) throw new Error("browser fill form waitFor is invalid before dispatch");
1509
+ const elementTimeoutMs = exactBoundedInteger(params.elementTimeoutMs, "browser fill form elementTimeoutMs", 10000, 1, 60000);
1510
+ let totalValueBytes = 0;
1511
+ const fields = params.fields.map((field, index) => {
1512
+ if (!field || typeof field !== "object" || Array.isArray(field)) throw new Error(`browser fill form field ${index} is invalid before dispatch`);
1513
+ const allowedField = new Set(["selector", "value", "action", "sensitive"]);
1514
+ for (const key of Object.keys(field)) if (!allowedField.has(key)) throw new Error(`unknown browser fill form field ${index} property before dispatch: ${key}`);
1515
+ const selector = exactBrokerSelector(field.selector, `browser fill form field ${index} selector`);
1516
+ if (typeof field.action !== "string" || !["fill", "select", "check", "uncheck", "click"].includes(field.action)) throw new Error(`browser fill form field ${index} action is invalid before dispatch`);
1517
+ if (typeof field.sensitive !== "boolean") throw new Error(`browser fill form field ${index} sensitive flag must be boolean before dispatch`);
1518
+ let value = field.value;
1519
+ if (["fill", "select"].includes(field.action)) {
1520
+ if (typeof value !== "string" || value.includes("\0") || value.length > 128 * 1024) throw new Error(`browser fill form field ${index} value is invalid before dispatch`);
1521
+ totalValueBytes += utf8ByteLength(value);
1522
+ if (totalValueBytes > 4 * 1024 * 1024) throw new Error("browser fill form values exceed the 4 MiB aggregate budget before dispatch");
1523
+ } else if (value !== undefined && value !== null) throw new Error(`browser fill form field ${index} value is not valid before dispatch`);
1524
+ else value = null;
1525
+ return { selector, value, action: field.action, sensitive: field.sensitive };
1526
+ });
1527
+ return { tabId: params.tabId, frameId, fields, submit, submitSelector, waitFor, elementTimeoutMs };
1528
+ }
1529
+
1530
+ function fillFormRendererPayload(params) {
1531
+ return { fields: params.fields, submit: params.submit, submitSelector: params.submitSelector, elementTimeoutMs: params.elementTimeoutMs };
1532
+ }
1533
+
1534
+ function decodedBase64Bytes(value) {
1535
+ if (typeof value !== "string" || value.length % 4 !== 0 || !/^[A-Za-z0-9+/]*={0,2}$/.test(value)) return null;
1536
+ const padding = value.endsWith("==") ? 2 : value.endsWith("=") ? 1 : 0;
1537
+ return (value.length / 4) * 3 - padding;
1538
+ }
1539
+
1540
+ function normalizeBrokerUploadParams(params) {
1541
+ if (!params || typeof params !== "object" || Array.isArray(params)) throw new Error("browser upload parameters are invalid before dispatch");
1542
+ const allowed = new Set(["tabId", "frameId", "selector", "files", "elementTimeoutMs"]);
1543
+ for (const key of Object.keys(params)) if (!allowed.has(key)) throw new Error(`unknown browser upload property before dispatch: ${key}`);
1544
+ const frameId = exactBrokerFrameId(params.frameId, "browser upload frame id");
1545
+ const selector = exactBrokerSelector(params.selector, "browser upload selector");
1546
+ const elementTimeoutMs = exactBoundedInteger(params.elementTimeoutMs, "browser upload elementTimeoutMs", 10000, 1, 60000);
1547
+ if (!Array.isArray(params.files) || params.files.length < 1 || params.files.length > 8) throw new Error("browser upload files are invalid before dispatch");
1548
+ let totalBytes = 0;
1549
+ const files = params.files.map((item, index) => {
1550
+ if (!item || typeof item !== "object" || Array.isArray(item)) throw new Error(`browser upload file ${index} is invalid before dispatch`);
1551
+ for (const key of Object.keys(item)) if (!["filename", "mime", "data"].includes(key)) throw new Error(`unknown browser upload file ${index} property before dispatch: ${key}`);
1552
+ if (typeof item.filename !== "string" || !item.filename || item.filename === "." || item.filename === ".." || item.filename.length > 255 || /[\/\\\u0000-\u001f\u007f]/.test(item.filename)) throw new Error(`browser upload file ${index} filename is invalid before dispatch`);
1553
+ if (typeof item.mime !== "string" || item.mime.length > 200 || !/^[a-z0-9!#$&^_.+-]+\/[a-z0-9!#$&^_.+-]+$/.test(item.mime)) throw new Error(`browser upload file ${index} mime is invalid before dispatch`);
1554
+ const bytes = decodedBase64Bytes(item.data);
1555
+ if (bytes === null) throw new Error(`browser upload file ${index} data is invalid before dispatch`);
1556
+ totalBytes += bytes;
1557
+ if (totalBytes > 5 * 1024 * 1024) throw new Error("browser upload files exceed the 5 MiB aggregate budget before dispatch");
1558
+ return { filename: item.filename, mime: item.mime, data: item.data };
1559
+ });
1560
+ return { tabId: params.tabId, frameId, selector, files, elementTimeoutMs };
1561
+ }
1562
+
1563
+ function uploadRendererPayload(params) {
1564
+ return { selector: params.selector, files: params.files, elementTimeoutMs: params.elementTimeoutMs };
1565
+ }
1566
+
378
1567
  async function fillForm(params, state) {
1568
+ params = normalizeBrokerFillFormParams(params);
1569
+ throwIfCancelled(state);
379
1570
  const tab = await resolveTab(params.tabId);
1571
+ throwIfCancelled(state);
380
1572
  assertPageTab(tab);
381
1573
  const waiter = beginTabWait(tab.id, params.waitFor, state?.timeoutMs, state);
382
1574
  let execution;
383
1575
  try {
384
- [execution] = await executePageAutomation(scriptTarget(tab.id, params.frameId, false), "fillForm", params);
385
- await waiter.promise;
1576
+ [execution] = await executePageMutation(scriptTarget(tab.id, params.frameId, false), "fillForm", fillFormRendererPayload(params), state);
1577
+ await awaitPostDispatchWait(waiter);
386
1578
  } catch (error) {
387
1579
  waiter.cancel();
388
1580
  throw error;
389
1581
  }
390
- return { tab_id: tab.id, title: tab.title || "", url: tab.url || "", ...execution.result };
1582
+ const current = await postActionTabMetadata(tab.id);
1583
+ return { ...execution.result, ...current };
391
1584
  }
392
1585
 
393
- async function uploadFiles(params) {
1586
+ async function uploadFiles(params, state) {
1587
+ params = normalizeBrokerUploadParams(params);
1588
+ throwIfCancelled(state);
394
1589
  const tab = await resolveTab(params.tabId);
1590
+ throwIfCancelled(state);
395
1591
  assertPageTab(tab);
396
- const [execution] = await executePageAutomation(scriptTarget(tab.id, params.frameId, false), "uploadFiles", params);
397
- return { tab_id: tab.id, title: tab.title || "", url: tab.url || "", ...execution.result };
1592
+ const [execution] = await executePageMutation(scriptTarget(tab.id, params.frameId, false), "uploadFiles", uploadRendererPayload(params), state);
1593
+ const current = await postActionTabMetadata(tab.id);
1594
+ return { ...execution.result, ...current };
398
1595
  }
399
1596
 
400
- async function screenshot(params) {
1597
+ async function screenshot(params, state) {
1598
+ const format = exactImageFormat(params.format, "browser screenshot format");
1599
+ const quality = exactBoundedInteger(params.quality, "browser screenshot quality", 90, 1, 100);
1600
+ const allowTabSwitch = exactBoolean(params.allowTabSwitch, "browser screenshot allowTabSwitch", true);
1601
+ throwIfCancelled(state);
401
1602
  const tab = await resolveTab(params.tabId);
402
1603
  const [previousActive] = await chrome.tabs.query({ active: true, windowId: tab.windowId });
1604
+ throwIfCancelled(state);
403
1605
  const changedActiveTab = previousActive?.id !== tab.id;
404
- if (changedActiveTab) await chrome.tabs.update(tab.id, { active: true });
1606
+ if (changedActiveTab && !allowTabSwitch) {
1607
+ throw new Error("browser screenshot target tab is not active for non-disruptive capture");
1608
+ }
1609
+ if (changedActiveTab && !previousActive?.id) throw new Error("browser screenshot cannot safely switch tabs without a restore baseline");
1610
+ let data = null;
1611
+ let failure = null;
1612
+ let captureActive = null;
1613
+ let activationAttempted = false;
405
1614
  try {
406
- const data = await chrome.tabs.captureVisibleTab(tab.windowId, {
407
- format: params.format === "jpeg" ? "jpeg" : "png",
408
- quality: Number(params.quality) || 90,
409
- });
410
- return { tab_id: tab.id, title: tab.title || "", url: tab.url || "", data };
411
- } finally {
412
- if (changedActiveTab && previousActive?.id) {
413
- const [currentActive] = await chrome.tabs.query({ active: true, windowId: tab.windowId }).catch(() => []);
414
- if (currentActive?.id === tab.id) await chrome.tabs.update(previousActive.id, { active: true }).catch(() => {});
1615
+ if (changedActiveTab) {
1616
+ let activationBaseline = null;
1617
+ try { [activationBaseline] = await chrome.tabs.query({ active: true, windowId: tab.windowId }); }
1618
+ catch { throw new Error("browser screenshot could not revalidate the active tab before temporary activation"); }
1619
+ if (activationBaseline?.id !== previousActive.id) {
1620
+ throw new Error("browser screenshot active tab changed before temporary activation");
1621
+ }
1622
+ throwIfCancelled(state);
1623
+ activationAttempted = true;
1624
+ let activated;
1625
+ try { activated = await chrome.tabs.update(tab.id, { active: true }); }
1626
+ catch (error) {
1627
+ const detail = String(error?.message || error).replace(/\s+/g, " ").slice(0, 500);
1628
+ throw new Error(`browser screenshot temporary tab activation may have been dispatched; the outcome is unknown. Inspect tabs before retrying. (${detail})`);
1629
+ }
1630
+ if (activated?.id !== tab.id || activated.windowId !== tab.windowId) {
1631
+ throw new Error("browser screenshot temporary tab activation may have been dispatched; the outcome is unknown because target window provenance changed. Inspect tabs before retrying.");
1632
+ }
415
1633
  }
1634
+ throwIfCancelled(state);
1635
+ try { [captureActive] = await chrome.tabs.query({ active: true, windowId: tab.windowId }); }
1636
+ catch { failure = new Error("browser screenshot could not verify target tab at capture boundary"); }
1637
+ if (!failure && captureActive?.id !== tab.id) failure = new Error("browser screenshot target tab was not active at capture boundary");
1638
+ if (!failure) {
1639
+ throwIfCancelled(state);
1640
+ try {
1641
+ data = await chrome.tabs.captureVisibleTab(tab.windowId, { format, quality });
1642
+ throwIfCancelled(state);
1643
+ } catch (error) {
1644
+ failure = error;
1645
+ }
1646
+ }
1647
+ if (!failure) {
1648
+ try { [captureActive] = await chrome.tabs.query({ active: true, windowId: tab.windowId }); }
1649
+ catch { failure = new Error("browser screenshot could not verify the active tab after capture"); }
1650
+ if (!failure && captureActive?.id !== tab.id) failure = new Error("browser screenshot active tab changed during capture");
1651
+ }
1652
+ } finally {
1653
+ if (activationAttempted && previousActive?.id) await restoreScreenshotActiveTab(tab, previousActive);
1654
+ }
1655
+ if (failure) throw failure;
1656
+ if (typeof data !== "string" || !/^data:image\/(?:png|jpeg);base64,[A-Za-z0-9+/=]+$/.test(data)) {
1657
+ throw new Error("browser screenshot returned invalid image data");
1658
+ }
1659
+ return { ...publicTab(captureActive), tab_metadata_verified: true, data };
1660
+ }
1661
+
1662
+ async function restoreScreenshotActiveTab(tab, previousActive) {
1663
+ let currentActive = null;
1664
+ try { [currentActive] = await chrome.tabs.query({ active: true, windowId: tab.windowId }); }
1665
+ catch { throw new Error("browser screenshot could not verify active-tab restoration"); }
1666
+ if (!currentActive?.id) throw new Error("browser screenshot could not verify active-tab restoration");
1667
+ if (currentActive.id !== tab.id) return;
1668
+ let restoreBaseline = null;
1669
+ try { restoreBaseline = await chrome.tabs.get(previousActive.id); }
1670
+ catch { throw new Error("browser screenshot could not restore the previous active tab"); }
1671
+ if (restoreBaseline?.windowId !== tab.windowId) throw new Error("browser screenshot could not restore the previous active tab");
1672
+ try {
1673
+ const restoredTab = await chrome.tabs.update(previousActive.id, { active: true });
1674
+ if (restoredTab?.id !== previousActive.id || restoredTab.windowId !== tab.windowId) throw new Error("restore window changed");
1675
+ const [restored] = await chrome.tabs.query({ active: true, windowId: tab.windowId });
1676
+ if (restored?.id !== previousActive.id) throw new Error("restore verification mismatch");
1677
+ } catch (error) {
1678
+ const detail = String(error?.message || error).replace(/\s+/g, " ").slice(0, 500);
1679
+ throw new Error(`browser screenshot restoration may have been dispatched; the active-tab outcome is unknown. Inspect tabs before retrying. (${detail})`);
416
1680
  }
417
1681
  }
418
1682
 
1683
+ async function postActionTabMetadata(tabId) {
1684
+ try {
1685
+ return { ...publicTab(await chrome.tabs.get(tabId)), tab_metadata_verified: true };
1686
+ } catch {
1687
+ return { tab_id: tabId, tab_metadata_verified: false };
1688
+ }
1689
+ }
1690
+
419
1691
  function publicTab(tab) {
420
1692
  return { tab_id: tab.id, window_id: tab.windowId, title: tab.title || "", url: tab.url || "", status: tab.status || "" };
421
1693
  }
422
1694
 
423
1695
  function assertPageTab(tab) {
424
- const url = String(tab.url || "");
425
- if (!/^(https?|file):/i.test(url)) throw new Error("this page cannot be scripted by a browser extension");
1696
+ const url = exactOptionalAuthorityString(tab?.url, 32768);
1697
+ if (!url || !/^(https?|file):/i.test(url)) throw new Error("this page cannot be scripted by a browser extension");
426
1698
  }
427
1699
 
428
1700
  function beginTabWait(tabId, mode, requestTimeoutMs = 30000, state = null) {
@@ -487,7 +1759,80 @@
487
1759
  timeout = setTimeout(() => settle(new Error(`browser navigation wait timed out${lastPollError ? `: ${lastPollError}` : ""}`)), waitTimeoutMs);
488
1760
  return { promise, cancel: () => settle() };
489
1761
  }
1762
+
1763
+ async function awaitPostDispatchWait(waiter) {
1764
+ try {
1765
+ await waiter.promise;
1766
+ } catch (error) {
1767
+ const detail = String(error?.message || error).replace(/\s+/g, " ").slice(0, 500);
1768
+ throw new Error(`browser action may have been dispatched; the action outcome is unknown because post-dispatch wait failed. Inspect the page before retrying. (${detail})`);
1769
+ }
1770
+ }
1771
+
1772
+ async function invokeBrowserNavigationMutation(operation) {
1773
+ try {
1774
+ return await operation();
1775
+ } catch (error) {
1776
+ const detail = String(error?.message || error).replace(/\s+/g, " ").slice(0, 500);
1777
+ throw new Error(`browser action may have been dispatched; the action outcome is unknown because the navigation mutation API failed. Inspect the page before retrying. (${detail})`);
1778
+ }
1779
+ }
1780
+
1781
+ async function invokeBrowserTabMutation(operation, phase) {
1782
+ try {
1783
+ return await operation();
1784
+ } catch (error) {
1785
+ const detail = String(error?.message || error).replace(/\s+/g, " ").slice(0, 500);
1786
+ throw new Error(`browser tab mutation may have been dispatched; the outcome is unknown during ${phase}. Inspect tabs before retrying. (${detail})`);
1787
+ }
1788
+ }
490
1789
 
1790
+ async function executePageMutation(target, method, params, state) {
1791
+ await chrome.scripting.executeScript({ target, files: ["page-automation.js"] });
1792
+ throwIfCancelled(state);
1793
+ let executions;
1794
+ try {
1795
+ executions = await chrome.scripting.executeScript({
1796
+ target,
1797
+ func: async (operation, payload, expectedVersion) => {
1798
+ const protocol = "machine_bridge_page_mutation_v1";
1799
+ try {
1800
+ const api = globalThis.__machineBridgePageAutomation;
1801
+ if (!api || api.version !== expectedVersion) throw new Error("page automation module version mismatch");
1802
+ if (typeof api[operation] !== "function") throw new Error("page automation module is unavailable");
1803
+ return { protocol, ok: true, result: await api[operation](payload) };
1804
+ } catch (error) {
1805
+ return { protocol, ok: false, error: String(error?.message || error).replace(/\s+/g, " ").slice(0, 1000) };
1806
+ }
1807
+ },
1808
+ args: [method, params, PAGE_AUTOMATION_VERSION],
1809
+ });
1810
+ } catch (error) {
1811
+ throw pageMutationDispatchUnknown(error);
1812
+ }
1813
+ const settlement = Array.isArray(executions) && executions.length === 1 ? executions[0]?.result : null;
1814
+ if (settlement?.protocol !== "machine_bridge_page_mutation_v1") {
1815
+ throw pageMutationDispatchUnknown(new Error("page mutation settlement response was unavailable"));
1816
+ }
1817
+ if (settlement.ok === false) {
1818
+ if (typeof settlement.error !== "string" || !settlement.error || settlement.error.includes("\0") || settlement.error.length > 1000) {
1819
+ throw pageMutationSettlementUnknown();
1820
+ }
1821
+ throw new Error(settlement.error);
1822
+ }
1823
+ if (settlement.ok !== true) throw pageMutationSettlementUnknown();
1824
+ return [{ ...executions[0], result: settlement.result }];
1825
+ }
1826
+
1827
+ function pageMutationSettlementUnknown() {
1828
+ return new Error("browser action may have been dispatched; the action outcome is unknown because the page mutation settlement was malformed. Inspect the page before retrying.");
1829
+ }
1830
+
1831
+ function pageMutationDispatchUnknown(error) {
1832
+ const detail = String(error?.message || error).replace(/\s+/g, " ").slice(0, 500);
1833
+ return new Error(`browser action may have been dispatched; the action outcome is unknown because the page mutation scripting call did not settle. Inspect the page before retrying. (${detail})`);
1834
+ }
1835
+
491
1836
  function executePageAutomation(target, method, params) {
492
1837
  return chrome.scripting.executeScript({ target, files: ["page-automation.js"] })
493
1838
  .then(() => chrome.scripting.executeScript({
@@ -506,17 +1851,27 @@
506
1851
  }
507
1852
 
508
1853
  function boundedRequestTimeout(value) {
509
- const timeout = Number(value);
510
- if (!Number.isFinite(timeout)) return 30000;
511
- return Math.max(1000, Math.min(185000, Math.floor(timeout)));
1854
+ if (!Number.isSafeInteger(value)) return 30000;
1855
+ return Math.max(1000, Math.min(185000, value));
1856
+ }
1857
+
1858
+ function requestCancelled(error) {
1859
+ return String(error?.message || error) === "browser request cancelled";
512
1860
  }
513
1861
 
514
1862
  function throwIfCancelled(state) {
515
1863
  if (state?.cancelled) throw new Error("browser request cancelled");
516
1864
  }
517
1865
 
1866
+ function throwIfCancelledBeforeTrustedInput(state) {
1867
+ if (!state?.cancelled) return;
1868
+ const error = new Error("browser request cancelled");
1869
+ Object.defineProperty(error, "machineBridgeBeforeDispatchAbort", { value: true });
1870
+ throw error;
1871
+ }
1872
+
518
1873
  Object.defineProperty(globalThis, "__machineBridgeBrowserOperations", {
519
- value: Object.freeze({ dispatch, boundedRequestTimeout, boundedDocumentSource }),
1874
+ value: Object.freeze({ dispatch, methodMayMutate, responsePayload, boundedRequestTimeout, boundedDocumentSource }),
520
1875
  configurable: false,
521
1876
  });
522
1877
  })();