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
@@ -1,51 +1,72 @@
1
1
  import {
2
- clampInt, normalizeBrowserAction, normalizeBrowserSelector, normalizeBrowserWait, normalizeFormAction,
3
- normalizeInputMode, normalizeNavigationWait, normalizeTabCommand, optionalInteger, optionalString,
2
+ browserPairingLaunchUnavailable, browserPairingLaunchUnknown, clampInt, normalizeBrowserAction, normalizeBrowserSelector, normalizeBrowserWait,
3
+ normalizeBrowserSnapshotIdentity, normalizeImageFormat, normalizeInputMode, normalizeNavigationWait, normalizeTabCommand, optionalBoolean, optionalInteger, optionalString,
4
4
  validateNavigationUrl,
5
5
  } from "./browser-command.mjs";
6
-
6
+ import { BrowserComputerObservationService } from "./browser-computer-observation-service.mjs";
7
+ import { BrowserTrustedInputHealth, TRUSTED_INPUT_QUARANTINE_FALLBACK } from "./browser-trusted-input-health.mjs";
8
+ import {
9
+ boundedBrowserValue, normalizeMimeType, normalizeUploadFilename, optionalStringArray, prepareBrowserFormField,
10
+ resolveBrowserActionValue, validateBrowserResource,
11
+ } from "./browser-resource-input.mjs";
7
12
  const MAX_SOURCE_BYTES = 4 * 1024 * 1024;
8
13
  const MAX_FORM_FIELDS = 200;
9
- const MAX_FIELD_VALUE_CHARS = 128 * 1024;
10
14
  const MAX_FORM_VALUE_BYTES = 4 * 1024 * 1024;
11
- const RESOURCE_NAME = /^[a-z][a-z0-9._-]{0,63}$/;
12
-
13
15
  export class BrowserOperationService {
14
16
  constructor({
15
17
  authorizeTool,
16
18
  ensureStarted,
17
19
  request,
18
20
  bridgeStatus,
21
+ createPairingLaunch,
19
22
  extensionPath,
20
23
  expectedExtensionVersion,
24
+ expectedExtensionId,
21
25
  runProcess,
22
26
  readResourceText,
23
27
  readResourceBinary,
28
+ throwIfCancelled = () => {},
29
+ logger = null,
24
30
  }) {
25
31
  this.authorizeTool = authorizeTool;
26
32
  this.ensureStarted = ensureStarted;
27
33
  this.request = request;
28
34
  this.bridgeStatus = bridgeStatus;
35
+ this.createPairingLaunch = createPairingLaunch;
29
36
  this.extensionPath = extensionPath;
30
37
  this.expectedExtensionVersion = expectedExtensionVersion;
38
+ this.expectedExtensionId = expectedExtensionId;
31
39
  this.runProcess = runProcess;
32
40
  this.readResourceText = readResourceText;
33
41
  this.readResourceBinary = readResourceBinary;
42
+ this.throwIfCancelled = throwIfCancelled;
43
+ this.trustedInputHealth = new BrowserTrustedInputHealth({ logger });
44
+ this.computerObservation = new BrowserComputerObservationService({
45
+ authorizeTool: (tool) => this.authorizeTool(tool),
46
+ request: (...args) => this.requestComputerObservation(...args),
47
+ bridgeStatus: () => this.bridgeStatus(),
48
+ inspectPage: (args, context) => this.inspectPage(args, context),
49
+ screenshot: (args, context) => this.screenshot(args, context),
50
+ });
34
51
  }
35
-
36
52
  async status(context = {}) {
37
53
  this.authorizeTool("browser_status");
38
54
  await this.ensureStarted(context);
39
55
  const bridge = this.bridgeStatus();
40
56
  const extension = bridge.extensionInfo;
57
+ const trustedHealth = this.trustedInputHealth.status(bridge);
41
58
  return {
42
59
  available: true,
43
60
  connected: bridge.extensionConnected,
44
61
  broker_role: bridge.brokerRole,
62
+ runtime_clients: Number(bridge.runtime_clients) || 0,
63
+ routed_requests: Number(bridge.routed_requests) || 0,
45
64
  endpoint: `ws://127.0.0.1:${bridge.port}/extension`,
46
65
  pairing_url: `http://127.0.0.1:${bridge.port}/pair`,
47
66
  extension_path: this.extensionPath,
48
67
  expected_extension_version: this.expectedExtensionVersion,
68
+ expected_extension_id: this.expectedExtensionId,
69
+ extension_id: extension?.extension_id || "",
49
70
  extension_protocol: extension?.protocol || null,
50
71
  extension_version: extension?.version || "",
51
72
  extension_capabilities: extension?.capabilities || [],
@@ -60,53 +81,71 @@ export class BrowserOperationService {
60
81
  source_access: true,
61
82
  semantic_snapshot_refs: true,
62
83
  actionability_waits: true,
63
- trusted_input: true,
84
+ trusted_input: trustedHealth.available,
85
+ trusted_input_quarantined: trustedHealth.quarantined,
86
+ trusted_input_health: !bridge.extensionConnected ? "disconnected" : trustedHealth.quarantined ? "quarantined" : trustedHealth.supported ? "ready" : "unsupported",
64
87
  input_modes: ["auto", "trusted", "dom"],
65
88
  complex_form_fill: true,
66
89
  tab_management: true,
67
90
  explicit_waits: true,
68
91
  screenshots: true,
92
+ computer_observation_v1: extension?.capabilities?.includes("computer_observation_v1") === true,
93
+ cdp_accessibility_snapshot: extension?.capabilities?.includes("cdp_accessibility_snapshot") === true,
94
+ cdp_surface_screenshot: extension?.capabilities?.includes("cdp_surface_screenshot") === true,
95
+ backend_node_trusted_input: extension?.capabilities?.includes("backend_node_trusted_input") === true && !trustedHealth.quarantined,
69
96
  restricted_pages: ["browser-internal pages", "extension stores", "some PDF/plugin viewers", "pages blocked by enterprise policy"],
70
97
  security: {
71
98
  loopback_only: true,
72
- bearer_pairing_token: true,
99
+ bearer_pairing_token: false,
100
+ short_lived_pairing_grant: true,
101
+ ephemeral_pairing_listener: true,
102
+ one_time_hmac_socket_auth: true,
103
+ long_lived_tokens_on_wire: false,
104
+ pinned_extension_identity: true,
73
105
  arbitrary_extension_code_from_mcp: false,
74
106
  resource_values_returned_to_model: false,
75
107
  },
76
108
  };
77
109
  }
78
-
79
110
  async pair(args = {}, context = {}) {
80
111
  this.authorizeTool("pair_browser_extension");
81
112
  const status = await this.status(context);
82
- if (args.open !== false) {
113
+ const open = optionalBoolean(args.open, "open", true);
114
+ if (open) {
115
+ const launch = await this.createPairingLaunch(this.bridgeStatus().port);
83
116
  const command = process.platform === "darwin"
84
- ? { cmd: "open", argv: [status.pairing_url] }
117
+ ? { cmd: "open", argv: [launch.url] }
85
118
  : process.platform === "win32"
86
- ? { cmd: "cmd.exe", argv: ["/d", "/s", "/c", "start", "", status.pairing_url] }
87
- : { cmd: "xdg-open", argv: [status.pairing_url] };
88
- await this.runProcess(command.cmd, command.argv, 30_000, false, 128 * 1024, context);
119
+ ? { cmd: "cmd.exe", argv: ["/d", "/s", "/c", "start", "", launch.url] }
120
+ : { cmd: "xdg-open", argv: [launch.url] };
121
+ this.throwIfCancelled(context);
122
+ try {
123
+ await this.runProcess(command.cmd, command.argv, 30_000, false, 128 * 1024, context, undefined, null, { nonReplayableMutation: true });
124
+ } catch (error) {
125
+ launch.close();
126
+ if (error?.details?.reason === "process_outcome_unknown_after_spawn") throw browserPairingLaunchUnknown();
127
+ if (error?.details?.reason === "process_failed_before_spawn") throw browserPairingLaunchUnavailable();
128
+ throw error;
129
+ }
89
130
  }
90
131
  return {
91
132
  ...status,
92
- opened_pairing_page: args.open !== false,
133
+ opened_pairing_page: open,
93
134
  setup_steps: [
94
135
  "Open the browser extensions page and enable developer mode.",
95
136
  "Load the unpacked extension from extension_path once.",
96
- "Open pairing_url; the installed extension stores the loopback endpoint and token locally.",
97
- "After upgrades, reload the unpacked extension and accept any newly requested browser permission.",
137
+ "Run pair_browser_extension with opening enabled; the sanitized pairing_url alone contains no pairing grant.",
138
+ "After upgrades, reload the same unpacked extension path and accept any newly requested browser permission; older local-candidate installs may need one Load unpacked migration to the current stable extension_path.",
98
139
  ],
99
140
  };
100
141
  }
101
-
102
142
  async listTabs(args = {}, context = {}) {
103
143
  this.authorizeTool("browser_list_tabs");
104
144
  return this.request("list_tabs", {
105
- currentWindow: args.current_window === true,
106
- includePinned: args.include_pinned !== false,
145
+ currentWindow: optionalBoolean(args.current_window, "current_window", false),
146
+ includePinned: optionalBoolean(args.include_pinned, "include_pinned", true),
107
147
  }, clampInt(args.timeout_seconds, 30, 1, 120), context);
108
148
  }
109
-
110
149
  async manageTabs(args = {}, context = {}) {
111
150
  this.authorizeTool("browser_manage_tabs");
112
151
  return this.request("manage_tabs", normalizeTabCommand(args), clampInt(args.timeout_seconds, 30, 1, 120), context);
@@ -124,7 +163,7 @@ export class BrowserOperationService {
124
163
  return this.request("get_source", {
125
164
  tabId: optionalInteger(args.tab_id, "tab_id", 1, Number.MAX_SAFE_INTEGER),
126
165
  frameId: optionalInteger(args.frame_id, "frame_id", 0, Number.MAX_SAFE_INTEGER),
127
- allFrames: args.all_frames === true,
166
+ allFrames: optionalBoolean(args.all_frames, "all_frames", false),
128
167
  maxBytes: clampInt(args.max_bytes, 1024 * 1024, 1, MAX_SOURCE_BYTES),
129
168
  }, clampInt(args.timeout_seconds, 30, 1, 120), context);
130
169
  }
@@ -134,15 +173,36 @@ export class BrowserOperationService {
134
173
  return this.request("inspect_page", {
135
174
  tabId: optionalInteger(args.tab_id, "tab_id", 1, Number.MAX_SAFE_INTEGER),
136
175
  frameId: optionalInteger(args.frame_id, "frame_id", 0, Number.MAX_SAFE_INTEGER),
137
- allFrames: args.all_frames !== false,
176
+ allFrames: optionalBoolean(args.all_frames, "all_frames", true),
138
177
  maxElements: clampInt(args.max_elements, 300, 1, 1000),
139
- includeValues: args.include_values === true,
178
+ includeValues: optionalBoolean(args.include_values, "include_values", false),
179
+ focusQuery: optionalString(args.focus_query, "focus_query", 1000),
140
180
  }, clampInt(args.timeout_seconds, 30, 1, 120), context);
141
181
  }
142
182
 
183
+ documentState(args = {}, context = {}) { return this.computerObservation.documentState(args, context); }
184
+ observeComputer(args = {}, context = {}) { return this.computerObservation.observe(args, context); }
185
+ pointAction(args = {}, context = {}) { return this.computerObservation.pointAction(args, context); }
186
+ async backendNodeAction(args = {}, context = {}) {
187
+ const action = args.action;
188
+ if (typeof action !== "string" || !["click", "double_click", "hover", "drag", "scroll", "press", "type_text", "fill", "check", "uncheck", "submit"].includes(action)) throw new Error("snapshot backend action must be click, double_click, hover, drag, scroll, press, type_text, fill, check, uncheck, or submit");
189
+ if (!["fill", "type_text"].includes(action) && (args.value !== undefined || args.value_resource !== undefined)) throw new Error(`value and value_resource are not valid for snapshot backend ${action}`);
190
+ if (action !== "press" && args.key !== undefined) throw new Error(`key is not valid for snapshot backend ${action}`);
191
+ this.computerObservation.preflightBackendNodeAction(args);
192
+ this.trustedInputHealth.assertTrustedAvailable(this.bridgeStatus());
193
+ const resolved = await resolveBrowserActionValue(args, this.readResourceText);
194
+ return this.computerObservation.backendNodeAction({ ...args, value: resolved.value ?? undefined }, context);
195
+ }
143
196
  async act(args = {}, context = {}) {
144
197
  this.authorizeTool("browser_action");
145
198
  const action = normalizeBrowserAction(args.action);
199
+ const valueActions = new Set(["fill", "type_text", "select"]);
200
+ if (!valueActions.has(action) && (args.value !== undefined || args.value_resource !== undefined)) {
201
+ throw new Error(`value and value_resource are not valid for browser action ${action}`);
202
+ }
203
+ if (valueActions.has(action) && args.value === undefined && args.value_resource === undefined) {
204
+ throw new Error(`browser action ${action} requires value or value_resource`);
205
+ }
146
206
  const rawUrl = optionalString(args.url, "url", 32768);
147
207
  const payload = {
148
208
  tabId: optionalInteger(args.tab_id, "tab_id", 1, Number.MAX_SAFE_INTEGER),
@@ -155,70 +215,83 @@ export class BrowserOperationService {
155
215
  waitFor: normalizeNavigationWait(args.wait_for),
156
216
  inputMode: normalizeInputMode(args.input_mode),
157
217
  elementTimeoutMs: clampInt(args.element_timeout_seconds, 10, 1, 60) * 1000,
218
+ expectedIdentity: normalizeBrowserSnapshotIdentity(args.expected_ref_identity),
219
+ expectedTabUrl: optionalString(args.expected_tab_url, "expected_tab_url", 32768),
220
+ expectedDocumentEpoch: optionalString(args.expected_document_epoch, "expected_document_epoch", 9000), expectedHistoryEntryKey: optionalString(args.expected_history_entry_key, "expected_history_entry_key", 512),
158
221
  };
222
+ if (!["navigate", "reload", "back", "forward"].includes(action) && payload.expectedTabUrl) throw new Error("expected_tab_url is only valid for snapshot-bound navigation actions");
223
+ if (!["reload", "back", "forward"].includes(action) && payload.expectedDocumentEpoch) throw new Error("expected_document_epoch is only valid for snapshot-bound reload, back, or forward");
224
+ if (!["reload", "back", "forward"].includes(action) && payload.expectedHistoryEntryKey) throw new Error("expected_history_entry_key is only valid for snapshot-bound reload, back, or forward");
159
225
  if (action !== "navigate" && rawUrl) throw new Error("url is only valid for navigate");
160
226
  if (action !== "press" && payload.key) throw new Error("key is only valid for press");
161
227
  if (payload.inputMode === "trusted" && !["click", "double_click", "hover", "press", "type_text"].includes(action)) {
162
228
  throw new Error("input_mode=trusted supports click, double_click, hover, press, and type_text only");
163
229
  }
164
- if (args.value !== undefined) payload.value = boundedValue(args.value, "value");
165
- if (args.value_resource !== undefined) {
166
- if (payload.value !== null) throw new Error("value and value_resource are mutually exclusive");
167
- payload.value = boundedValue(await this.readResourceText(validateResource(args.value_resource)), "value_resource");
168
- }
230
+ const trustedDecision = this.trustedInputHealth.pageInputMode({ inputMode: payload.inputMode, action, bridge: this.bridgeStatus() });
231
+ payload.inputMode = trustedDecision.inputMode;
232
+ const resolvedValue = await resolveBrowserActionValue(args, this.readResourceText);
233
+ payload.value = resolvedValue.value;
169
234
  if (payload.value !== null && !["fill", "select", "press", "type_text"].includes(action)) {
170
235
  throw new Error(`value is not valid for browser action '${action}'`);
171
236
  }
172
237
  const pageAction = !["navigate", "reload", "back", "forward"].includes(action);
173
238
  const defaultTimeout = pageAction ? Math.min(120, Math.max(30, payload.elementTimeoutMs / 1000 + 5)) : 30;
174
- const response = await this.request("action", payload, clampInt(args.timeout_seconds, defaultTimeout, 1, 120), context);
239
+ let response;
240
+ try {
241
+ response = await this.request("action", payload, clampInt(args.timeout_seconds, defaultTimeout, 1, 120), context);
242
+ } catch (error) {
243
+ this.trustedInputHealth.noteAmbiguousFailure(error, this.bridgeStatus());
244
+ throw error;
245
+ }
175
246
  return {
176
247
  ...response,
177
- value_source: args.value_resource !== undefined ? "local-resource" : payload.value === null ? "none" : "mcp-argument",
248
+ ...(trustedDecision.fallback ? {
249
+ input_mode: "dom",
250
+ trusted_input_fallback: true,
251
+ fallback_reason: TRUSTED_INPUT_QUARANTINE_FALLBACK,
252
+ } : {}),
253
+ value_source: resolvedValue.source,
178
254
  value_exposed: false,
179
255
  };
180
256
  }
181
257
 
258
+ async requestComputerObservation(method, params, timeoutSeconds, context) {
259
+ const trustedMethod = method === "point_action" || method === "backend_node_action";
260
+ if (trustedMethod) this.trustedInputHealth.assertTrustedAvailable(this.bridgeStatus());
261
+ try {
262
+ return await this.request(method, params, timeoutSeconds, context);
263
+ } catch (error) {
264
+ if (trustedMethod) this.trustedInputHealth.noteAmbiguousFailure(error, this.bridgeStatus());
265
+ throw error;
266
+ }
267
+ }
268
+
182
269
  async fillForm(args = {}, context = {}) {
183
270
  this.authorizeTool("browser_fill_form");
184
271
  if (!Array.isArray(args.fields) || !args.fields.length) throw new Error("fields must be a non-empty array");
185
272
  if (args.fields.length > MAX_FORM_FIELDS) throw new Error(`fields contains more than ${MAX_FORM_FIELDS} entries`);
273
+ const tabId = optionalInteger(args.tab_id, "tab_id", 1, Number.MAX_SAFE_INTEGER);
274
+ const frameId = optionalInteger(args.frame_id, "frame_id", 0, Number.MAX_SAFE_INTEGER);
275
+ const submit = optionalBoolean(args.submit, "submit", false);
276
+ const submitSelector = args.submit_selector === undefined ? null : normalizeBrowserSelector(args.submit_selector, "click");
277
+ const waitFor = normalizeNavigationWait(args.wait_for);
278
+ const elementTimeoutSeconds = clampInt(args.element_timeout_seconds, 10, 1, 60);
279
+ const timeoutSeconds = clampInt(args.timeout_seconds, Math.max(60, elementTimeoutSeconds + 5), 1, 180);
280
+ const prepared = args.fields.map((input, index) => prepareBrowserFormField(input, index));
186
281
  const fields = [];
187
282
  let totalValueBytes = 0;
188
- for (let index = 0; index < args.fields.length; index += 1) {
189
- const input = args.fields[index];
190
- if (!input || typeof input !== "object" || Array.isArray(input)) throw new Error(`fields[${index}] must be an object`);
191
- const allowed = new Set(["selector", "value", "value_resource", "action", "sensitive"]);
192
- for (const key of Object.keys(input)) if (!allowed.has(key)) throw new Error(`unknown fields[${index}] property: ${key}`);
193
- let value = input.value === undefined ? null : boundedValue(input.value, `fields[${index}].value`);
194
- if (input.value_resource !== undefined) {
195
- if (value !== null) throw new Error(`fields[${index}] value and value_resource are mutually exclusive`);
196
- value = boundedValue(await this.readResourceText(validateResource(input.value_resource)), `fields[${index}].value_resource`);
197
- }
283
+ for (const field of prepared) {
284
+ let value = field.value;
285
+ if (field.resourceName) value = boundedBrowserValue(await this.readResourceText(field.resourceName), `fields[${field.index}].value_resource`);
198
286
  if (value !== null) {
199
287
  totalValueBytes += Buffer.byteLength(value);
200
288
  if (totalValueBytes > MAX_FORM_VALUE_BYTES) throw new Error("form field values exceed 4 MiB total");
201
289
  }
202
- const action = input.action === undefined ? "fill" : normalizeFormAction(input.action);
203
- if (value === null && !["check", "uncheck", "click"].includes(action)) throw new Error(`fields[${index}] requires value or value_resource`);
204
- if (value !== null && !["fill", "select"].includes(action)) throw new Error(`fields[${index}] value is not valid for action '${action}'`);
205
- fields.push({
206
- selector: normalizeBrowserSelector(input.selector, action),
207
- value,
208
- action,
209
- sensitive: input.sensitive === true || input.value_resource !== undefined,
210
- });
290
+ fields.push({ selector: field.selector, value, action: field.action, sensitive: field.sensitive || Boolean(field.resourceName) });
211
291
  }
212
- const elementTimeoutSeconds = clampInt(args.element_timeout_seconds, 10, 1, 60);
213
292
  return this.request("fill_form", {
214
- tabId: optionalInteger(args.tab_id, "tab_id", 1, Number.MAX_SAFE_INTEGER),
215
- frameId: optionalInteger(args.frame_id, "frame_id", 0, Number.MAX_SAFE_INTEGER),
216
- fields,
217
- submit: args.submit === true,
218
- submitSelector: args.submit_selector ? normalizeBrowserSelector(args.submit_selector, "click") : null,
219
- waitFor: normalizeNavigationWait(args.wait_for),
220
- elementTimeoutMs: elementTimeoutSeconds * 1000,
221
- }, clampInt(args.timeout_seconds, Math.max(60, elementTimeoutSeconds + 5), 1, 180), context);
293
+ tabId, frameId, fields, submit, submitSelector, waitFor, elementTimeoutMs: elementTimeoutSeconds * 1000,
294
+ }, timeoutSeconds, context);
222
295
  }
223
296
 
224
297
  async uploadFiles(args = {}, context = {}) {
@@ -226,15 +299,20 @@ export class BrowserOperationService {
226
299
  if (!Array.isArray(args.resources) || !args.resources.length || args.resources.length > 8) {
227
300
  throw new Error("resources must contain 1 to 8 registered resource names");
228
301
  }
302
+ const tabId = optionalInteger(args.tab_id, "tab_id", 1, Number.MAX_SAFE_INTEGER);
303
+ const frameId = optionalInteger(args.frame_id, "frame_id", 0, Number.MAX_SAFE_INTEGER);
304
+ const selector = normalizeBrowserSelector(args.selector, "fill");
305
+ const elementTimeoutSeconds = clampInt(args.element_timeout_seconds, 10, 1, 60);
306
+ const timeoutSeconds = clampInt(args.timeout_seconds, Math.max(60, elementTimeoutSeconds + 5), 1, 180);
307
+ const resourceNames = args.resources.map(validateBrowserResource);
229
308
  const filenames = optionalStringArray(args.filenames, "filenames", 8, 255);
230
309
  const mimeTypes = optionalStringArray(args.mime_types, "mime_types", 8, 200);
231
- if (filenames.length > args.resources.length || mimeTypes.length > args.resources.length) {
310
+ if (filenames.length > resourceNames.length || mimeTypes.length > resourceNames.length) {
232
311
  throw new Error("filenames and mime_types cannot contain more entries than resources");
233
312
  }
234
313
  const files = [];
235
314
  let total = 0;
236
- for (const raw of args.resources) {
237
- const name = validateResource(raw);
315
+ for (const name of resourceNames) {
238
316
  const resource = this.readResourceBinary(name);
239
317
  total += resource.buffer.length;
240
318
  if (total > 5 * 1024 * 1024) throw new Error("browser upload resources exceed 5 MiB total");
@@ -246,25 +324,20 @@ export class BrowserOperationService {
246
324
  data: resource.buffer.toString("base64"),
247
325
  });
248
326
  }
249
- const elementTimeoutSeconds = clampInt(args.element_timeout_seconds, 10, 1, 60);
250
327
  const result = await this.request("upload_files", {
251
- tabId: optionalInteger(args.tab_id, "tab_id", 1, Number.MAX_SAFE_INTEGER),
252
- frameId: optionalInteger(args.frame_id, "frame_id", 0, Number.MAX_SAFE_INTEGER),
253
- selector: normalizeBrowserSelector(args.selector, "fill"),
254
- files,
255
- elementTimeoutMs: elementTimeoutSeconds * 1000,
256
- }, clampInt(args.timeout_seconds, Math.max(60, elementTimeoutSeconds + 5), 1, 180), context);
257
- return { ...result, resource_names: args.resources.map(String), resource_contents_exposed: false };
328
+ tabId, frameId, selector, files, elementTimeoutMs: elementTimeoutSeconds * 1000,
329
+ }, timeoutSeconds, context);
330
+ return { ...result, resource_names: resourceNames, resource_contents_exposed: false };
258
331
  }
259
332
 
260
333
  async screenshot(args = {}, context = {}) {
261
334
  this.authorizeTool("browser_screenshot");
262
335
  const result = await this.request("screenshot", {
263
336
  tabId: optionalInteger(args.tab_id, "tab_id", 1, Number.MAX_SAFE_INTEGER),
264
- format: args.format === "jpeg" ? "jpeg" : "png",
337
+ format: normalizeImageFormat(args.format, "format"),
265
338
  quality: clampInt(args.quality, 90, 1, 100),
266
339
  }, clampInt(args.timeout_seconds, 30, 1, 120), context);
267
- const data = String(result.data || "");
340
+ const data = typeof result.data === "string" ? result.data : "";
268
341
  const match = /^data:(image\/(?:png|jpeg));base64,([A-Za-z0-9+/=]+)$/.exec(data);
269
342
  if (!match) throw new Error("browser extension returned an invalid screenshot");
270
343
  return {
@@ -273,53 +346,10 @@ export class BrowserOperationService {
273
346
  structuredContent: {
274
347
  tab_id: result.tab_id,
275
348
  url: result.url,
276
- title: result.title,
349
+ title: result.title, tab_metadata_verified: result.tab_metadata_verified === true,
277
350
  mime_type: match[1],
278
351
  },
279
352
  },
280
353
  };
281
354
  }
282
355
  }
283
-
284
- function normalizeUploadFilename(value, { derived = false } = {}) {
285
- let name = String(value || "");
286
- if (derived) name = name.replace(/[\u0000-\u001f\u007f/\\]+/g, "_").trim();
287
- if (!name || name === "." || name === ".." || name.length > 255 || /[\u0000-\u001f\u007f/\\]/.test(name)) {
288
- if (derived) return "upload.bin";
289
- throw new Error("filenames entries must be safe single-component filenames of at most 255 characters");
290
- }
291
- return name;
292
- }
293
-
294
- function normalizeMimeType(value) {
295
- const mime = String(value || "").trim().toLowerCase();
296
- if (!/^[a-z0-9!#$&^_.+-]+\/[a-z0-9!#$&^_.+-]+$/.test(mime) || mime.length > 200) {
297
- throw new Error("mime_types entries must be valid media types");
298
- }
299
- return mime;
300
- }
301
-
302
- function optionalStringArray(value, label, maxItems, maxLength) {
303
- if (value === undefined || value === null) return [];
304
- if (!Array.isArray(value) || value.length > maxItems) throw new Error(`${label} must be an array with at most ${maxItems} entries`);
305
- return value.map((item, index) => {
306
- if (typeof item !== "string" || item.includes("\0") || !item.length || item.length > maxLength) {
307
- throw new Error(`${label}[${index}] must be a non-empty string of at most ${maxLength} characters`);
308
- }
309
- return item;
310
- });
311
- }
312
-
313
- function boundedValue(value, label) {
314
- const string = String(value);
315
- if (string.includes("\0") || string.length > MAX_FIELD_VALUE_CHARS) {
316
- throw new Error(`${label} exceeds the maximum length or contains a NUL byte`);
317
- }
318
- return string;
319
- }
320
-
321
- function validateResource(value) {
322
- const name = String(value || "").trim();
323
- if (!RESOURCE_NAME.test(name)) throw new Error("value_resource is invalid");
324
- return name;
325
- }
@@ -0,0 +1,109 @@
1
+ import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
2
+ import { createMonotonicDeadline } from "./monotonic-deadline.mjs";
3
+ import { createBrokerAuthChallenge } from "./browser-broker-auth.mjs";
4
+
5
+ const GRANT = /^(\d{13})\.([A-Za-z0-9_-]{22})\.([A-Za-z0-9_-]{43})$/;
6
+ const GRANT_ID = /^(\d{13})\.([A-Za-z0-9_-]{22})$/;
7
+ const CHALLENGE = /^[A-Za-z0-9_-]{32}$/;
8
+ const PROOF = /^[A-Za-z0-9_-]{43}$/;
9
+ const TOKEN = /^[A-Za-z0-9_-]{32,100}$/;
10
+ const PAIRING_GRANT_TTL_MS = 30_000;
11
+ const EXCHANGE_TTL_MS = 5_000;
12
+ const MAX_PENDING = 32;
13
+
14
+ export function createBrowserPairingGrant(extensionToken, port, now = Date.now()) {
15
+ assertToken(extensionToken);
16
+ const expiresAt = Number(now) + PAIRING_GRANT_TTL_MS;
17
+ if (!Number.isSafeInteger(expiresAt) || expiresAt <= 0) throw new Error("browser pairing grant time is invalid");
18
+ const nonce = randomBytes(16).toString("base64url");
19
+ const proof = grantSecret(extensionToken, port, expiresAt, nonce);
20
+ return `${expiresAt}.${nonce}.${proof}`;
21
+ }
22
+
23
+ export function parseBrowserPairingGrant(grant, now = Date.now()) {
24
+ const match = GRANT.exec(String(grant || ""));
25
+ if (!match) return null;
26
+ const expiresAt = Number(match[1]);
27
+ const current = Number(now);
28
+ if (!Number.isFinite(current) || expiresAt < current || expiresAt - current > PAIRING_GRANT_TTL_MS) return null;
29
+ return { id: `${match[1]}.${match[2]}`, secret: match[3], expiresAt };
30
+ }
31
+
32
+ export function createPairingBootstrapRegistry(extensionToken, port, options = {}) {
33
+ assertToken(extensionToken);
34
+ const wallNow = typeof options.wallNow === "function" ? options.wallNow : Date.now;
35
+ const monotonicNow = typeof options.monotonicNow === "function" ? options.monotonicNow : undefined;
36
+ const pending = new Map();
37
+ const used = new Map();
38
+ return {
39
+ issue(grantId, clientChallenge, initProof) {
40
+ prune(used, pending, wallNow());
41
+ const grant = grantFromId(extensionToken, port, grantId, wallNow());
42
+ if (!grant || used.has(grant.id) || !CHALLENGE.test(String(clientChallenge || "")) || !PROOF.test(String(initProof || ""))) return null;
43
+ if (!safeEqual(bootstrapInitProof(grant.secret, grant.id, clientChallenge), initProof)) return null;
44
+ const existing = pending.get(grant.id);
45
+ if (existing && !existing.deadline.expired()) return existing.clientChallenge === clientChallenge ? { serverNonce: existing.serverNonce, serverProof: bootstrapProof(grant.secret, "server", grant.id, clientChallenge, existing.serverNonce) } : null;
46
+ if (used.size + pending.size >= MAX_PENDING) return null;
47
+ const serverNonce = createBrokerAuthChallenge();
48
+ pending.set(grant.id, { clientChallenge, serverNonce, deadline: createMonotonicDeadline(EXCHANGE_TTL_MS, monotonicNow) });
49
+ return { serverNonce, serverProof: bootstrapProof(grant.secret, "server", grant.id, clientChallenge, serverNonce) };
50
+ },
51
+ consume(grantId, clientChallenge, serverNonce, clientProof) {
52
+ prune(used, pending, wallNow());
53
+ const grant = grantFromId(extensionToken, port, grantId, wallNow());
54
+ if (!grant || used.has(grant.id) || !PROOF.test(String(clientProof || ""))) return false;
55
+ const current = pending.get(grant.id);
56
+ pending.delete(grant.id);
57
+ if (!current || current.deadline.expired()
58
+ || current.clientChallenge !== clientChallenge || current.serverNonce !== serverNonce) return false;
59
+ const expected = bootstrapProof(grant.secret, "client", grant.id, clientChallenge, serverNonce);
60
+ if (!safeEqual(expected, clientProof)) return false;
61
+ used.set(grant.id, grant.expiresAt);
62
+ return true;
63
+ },
64
+ };
65
+ }
66
+
67
+ export function createPairingBootstrapInitProof(grantSecret, grantId, clientChallenge) {
68
+ if (!PROOF.test(String(grantSecret || "")) || !GRANT_ID.test(String(grantId || "")) || !CHALLENGE.test(String(clientChallenge || ""))) throw new Error("browser pairing bootstrap init proof input is invalid");
69
+ return bootstrapInitProof(grantSecret, grantId, clientChallenge);
70
+ }
71
+
72
+ export function createPairingBootstrapProof(grantSecret, direction, grantId, clientChallenge, serverNonce) {
73
+ if (!PROOF.test(String(grantSecret || "")) || (direction !== "server" && direction !== "client")
74
+ || !GRANT_ID.test(String(grantId || "")) || !CHALLENGE.test(String(clientChallenge || ""))
75
+ || !CHALLENGE.test(String(serverNonce || ""))) {
76
+ throw new Error("browser pairing bootstrap proof input is invalid");
77
+ }
78
+ return bootstrapProof(grantSecret, direction, grantId, clientChallenge, serverNonce);
79
+ }
80
+
81
+ function grantFromId(token, port, grantId, now) {
82
+ const match = GRANT_ID.exec(String(grantId || ""));
83
+ if (!match) return null;
84
+ const expiresAt = Number(match[1]);
85
+ if (!Number.isFinite(now) || expiresAt < now || expiresAt - now > PAIRING_GRANT_TTL_MS) return null;
86
+ return { id: grantId, secret: grantSecret(token, port, expiresAt, match[2]), expiresAt };
87
+ }
88
+
89
+ function grantSecret(token, port, expiresAt, nonce) {
90
+ const normalizedPort = Number(port);
91
+ if (!Number.isInteger(normalizedPort) || normalizedPort < 1024 || normalizedPort > 65535) throw new Error("browser pairing grant port is invalid");
92
+ return hmac(token, `machine-bridge-browser-pair-v2\0${normalizedPort}\0${expiresAt}\0${nonce}`);
93
+ }
94
+ function bootstrapInitProof(secret, grantId, clientChallenge) {
95
+ return hmac(secret, `machine-bridge-browser-pair-init-v2\0${grantId}\0${clientChallenge}`);
96
+ }
97
+ function bootstrapProof(secret, direction, grantId, clientChallenge, serverNonce) {
98
+ return hmac(secret, `machine-bridge-browser-pair-${direction}-v2\0${grantId}\0${clientChallenge}\0${serverNonce}`);
99
+ }
100
+ function hmac(key, message) { return createHmac("sha256", key).update(message).digest("base64url"); }
101
+ function assertToken(value) { if (!TOKEN.test(String(value || ""))) throw new Error("browser broker credential is invalid"); }
102
+ function safeEqual(left, right) {
103
+ const a = Buffer.from(String(left)); const b = Buffer.from(String(right));
104
+ return a.length === b.length && timingSafeEqual(a, b);
105
+ }
106
+ function prune(used, pending, now) {
107
+ for (const [id, expiresAt] of used) if (expiresAt < now) used.delete(id);
108
+ for (const [id, entry] of pending) if (entry.deadline.expired()) pending.delete(id);
109
+ }
@@ -0,0 +1,36 @@
1
+ import { EXPECTED_EXTENSION_ID, normalizeExtensionId } from "./browser-extension-identity.mjs";
2
+ import { EXPECTED_EXTENSION_VERSION } from "./browser-extension-protocol.mjs";
3
+
4
+ export function pairingHtml(port) {
5
+ return `<!doctype html><html><head><meta charset="utf-8"><meta name="machine-bridge-browser-pair" content="1"><meta name="machine-bridge-browser-port" content="${port}"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Machine Bridge browser pairing</title></head><body><h1>Machine Bridge browser pairing</h1><p>Expected extension build: <strong>${EXPECTED_EXTENSION_VERSION}</strong>.</p><p>Expected extension ID: <code>${EXPECTED_EXTENSION_ID}</code>.</p><p>Reload the unpacked extension after every Machine Bridge upgrade.</p><p>Pairing credentials are never embedded in this HTTP page. Run pair_browser_extension with opening enabled to create a short-lived extension bootstrap.</p><p id="status">Waiting for the Machine Bridge extension bootstrap.</p></body></html>`;
6
+ }
7
+
8
+ export function isAllowedExtensionOrigin(origin, expectedExtensionId = EXPECTED_EXTENSION_ID) {
9
+ const expected = normalizeExtensionId(expectedExtensionId);
10
+ if (!expected) return false;
11
+ let parsed;
12
+ try { parsed = new URL(String(origin || "")); } catch { return false; }
13
+ return parsed.protocol === "chrome-extension:" && parsed.hostname === expected
14
+ && !parsed.username && !parsed.password && !parsed.port
15
+ && (parsed.pathname === "" || parsed.pathname === "/") && !parsed.search && !parsed.hash;
16
+ }
17
+
18
+ export function isAllowedLoopbackHost(host, port) {
19
+ const normalized = String(host || "").toLowerCase();
20
+ return normalized === `127.0.0.1:${port}` || normalized === `localhost:${port}` || normalized === `[::1]:${port}`;
21
+ }
22
+
23
+ export function securityHeaders(contentType) {
24
+ return {
25
+ "content-type": contentType,
26
+ "cache-control": "no-store",
27
+ "content-security-policy": "default-src 'none'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'",
28
+ "x-content-type-options": "nosniff",
29
+ "referrer-policy": "no-referrer",
30
+ };
31
+ }
32
+
33
+ export function sendJson(response, value) {
34
+ response.writeHead(200, securityHeaders("application/json; charset=utf-8"));
35
+ response.end(`${JSON.stringify(value)}\n`);
36
+ }
@@ -0,0 +1,58 @@
1
+ import { createServer } from "node:http";
2
+ import { pairingHtml, securityHeaders } from "./browser-pairing-http.mjs";
3
+ import { createBrowserPairingGrant } from "./browser-pairing-grant.mjs";
4
+
5
+ const LAUNCH_TTL_MS = 30_000;
6
+
7
+ export async function startBrowserPairingLaunch({ brokerPort, extensionToken, timeoutMs = LAUNCH_TTL_MS } = {}) {
8
+ const targetPort = Number(brokerPort);
9
+ if (!Number.isInteger(targetPort) || targetPort < 1024 || targetPort > 65535) throw new Error("browser pairing broker port is invalid");
10
+ const grant = createBrowserPairingGrant(extensionToken, targetPort);
11
+ const ttl = Number(timeoutMs);
12
+ if (!Number.isInteger(ttl) || ttl < 1 || ttl > LAUNCH_TTL_MS) throw new Error("browser pairing launch timeout is invalid");
13
+
14
+ let listenerPort = 0;
15
+ let closed = false;
16
+ let timer;
17
+ let resolveClosed;
18
+ const closedPromise = new Promise((resolvePromise) => { resolveClosed = resolvePromise; });
19
+ const server = createServer((request, response) => {
20
+ const host = String(request.headers.host || "");
21
+ if (!listenerPort || host.toLowerCase() !== `127.0.0.1:${listenerPort}`) {
22
+ response.writeHead(403, securityHeaders("text/plain; charset=utf-8")).end("forbidden\n");
23
+ return;
24
+ }
25
+ let url;
26
+ try { url = new URL(request.url || "/", `http://${host}`); }
27
+ catch { response.writeHead(400, securityHeaders("text/plain; charset=utf-8")).end("bad request\n"); return; }
28
+ if (request.method !== "GET" || url.pathname !== "/pair" || url.search) {
29
+ response.writeHead(404, securityHeaders("text/plain; charset=utf-8")).end("not found\n");
30
+ return;
31
+ }
32
+ const html = pairingHtml(targetPort);
33
+ response.writeHead(200, { ...securityHeaders("text/html; charset=utf-8"), "content-length": Buffer.byteLength(html) });
34
+ response.end(html, () => close());
35
+ });
36
+
37
+ const close = () => {
38
+ if (closed) return;
39
+ closed = true;
40
+ if (timer) clearTimeout(timer);
41
+ try { server.close(() => resolveClosed()); } catch { resolveClosed(); }
42
+ };
43
+ await new Promise((resolvePromise, rejectPromise) => {
44
+ const onError = (error) => { server.off("listening", onListening); rejectPromise(error); };
45
+ const onListening = () => { server.off("error", onError); resolvePromise(); };
46
+ server.once("error", onError);
47
+ server.once("listening", onListening);
48
+ server.listen(0, "127.0.0.1");
49
+ });
50
+ server.on("error", close);
51
+ const address = server.address();
52
+ if (!address || typeof address === "string") { close(); throw new Error("browser pairing launch listener address is unavailable"); }
53
+ listenerPort = address.port;
54
+ timer = setTimeout(close, ttl);
55
+ timer.unref?.();
56
+ const fragment = new URLSearchParams({ broker_port: String(targetPort), grant: String(grant) }).toString();
57
+ return { url: `http://127.0.0.1:${listenerPort}/pair#${fragment}`, close, closed: closedPromise, listenerPort, brokerPort: targetPort };
58
+ }