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,6 +1,15 @@
1
1
  import { DAEMON_AUTH_CHALLENGE_TTL_SECONDS, DAEMON_AUTH_SCHEME, DAEMON_PREFLIGHT_SCHEME, DAEMON_PREFLIGHT_TTL_SECONDS, daemonAuthTranscript, daemonPreflightTranscript } from "../shared/daemon-auth.mjs";
2
2
  import { safeEqual } from "./oauth-state.ts";
3
3
  import { consumeBoundedNonce } from "./nonce-store.ts";
4
+ import {
5
+ decodeBase64Url,
6
+ parsePublicJwk,
7
+ publicKeyId,
8
+ verifyDeviceSessionCertificate,
9
+ verifyP256Signature,
10
+ } from "./device-session-verifier.ts";
11
+
12
+ const SESSION_CERTIFICATE_HEADER = "X-Bridge-Device-Certificate";
4
13
 
5
14
  export interface DaemonChallenge {
6
15
  scheme: typeof DAEMON_AUTH_SCHEME;
@@ -22,26 +31,39 @@ export function createDaemonChallenge(workerOrigin: string, now = Math.floor(Dat
22
31
  };
23
32
  }
24
33
 
25
-
26
34
  export function sanitizeDaemonChallengeAttachment(value: Record<string, unknown>): Partial<{
27
35
  authChallenge: string;
28
36
  authIssuedAt: number;
29
37
  authExpiresAt: number;
30
38
  workerOrigin: string;
39
+ authSessionPublicKeyJson: string;
40
+ authSessionKeyId: string;
41
+ authCertificateExpiresAt: number;
31
42
  }> {
32
43
  const authChallenge = typeof value.authChallenge === "string" && /^daemon_challenge_[A-Za-z0-9_-]{40,96}$/.test(value.authChallenge)
33
44
  ? value.authChallenge
34
45
  : undefined;
35
46
  const authIssuedAt = positiveSafeInteger(value.authIssuedAt);
36
47
  const authExpiresAt = positiveSafeInteger(value.authExpiresAt);
48
+ const authCertificateExpiresAt = positiveSafeInteger(value.authCertificateExpiresAt);
49
+ const authSessionKeyId = typeof value.authSessionKeyId === "string" && /^device_[A-Za-z0-9_-]{32}$/.test(value.authSessionKeyId)
50
+ ? value.authSessionKeyId
51
+ : undefined;
37
52
  let workerOrigin: string | undefined;
38
- try { workerOrigin = normalizeWorkerOrigin(String(value.workerOrigin || "")); } catch {}
39
- return { authChallenge, authIssuedAt, authExpiresAt, workerOrigin };
53
+ try { workerOrigin = normalizeWorkerOrigin(String(value.workerOrigin || "")); }
54
+ catch { /* Invalid optional persisted auth metadata remains undefined and fails the caller's completeness checks. */ }
55
+ let authSessionPublicKeyJson: string | undefined;
56
+ try { authSessionPublicKeyJson = JSON.stringify(parsePublicJwk(String(value.authSessionPublicKeyJson || ""))); }
57
+ catch { /* Invalid optional persisted auth metadata remains undefined and fails the caller's completeness checks. */ }
58
+ return { authChallenge, authIssuedAt, authExpiresAt, workerOrigin, authSessionPublicKeyJson, authSessionKeyId, authCertificateExpiresAt };
40
59
  }
41
60
 
42
61
  export interface DaemonPreflightAuthorization {
43
62
  nonce: string;
44
63
  expiresAt: number;
64
+ sessionPublicKeyJson: string;
65
+ sessionKeyId: string;
66
+ certificateExpiresAt: number;
45
67
  }
46
68
 
47
69
  export async function verifyDaemonPreflight(input: {
@@ -53,6 +75,15 @@ export async function verifyDaemonPreflight(input: {
53
75
  now?: number;
54
76
  }): Promise<DaemonPreflightAuthorization | null> {
55
77
  const now = Number.isSafeInteger(input.now) ? Number(input.now) : Math.floor(Date.now() / 1000);
78
+ const certificate = await verifyDeviceSessionCertificate({
79
+ encodedCertificate: input.headers.get(SESSION_CERTIFICATE_HEADER) || "",
80
+ rootPublicKeyJson: input.publicKeyJson,
81
+ workerOrigin: input.workerOrigin,
82
+ server: input.server,
83
+ version: input.version,
84
+ now,
85
+ });
86
+ if (!certificate) return null;
56
87
  const scheme = input.headers.get("X-Bridge-Device-Scheme") || "";
57
88
  const keyId = input.headers.get("X-Bridge-Device-Key") || "";
58
89
  const nonce = input.headers.get("X-Bridge-Device-Nonce") || "";
@@ -61,36 +92,23 @@ export async function verifyDaemonPreflight(input: {
61
92
  if (scheme !== DAEMON_PREFLIGHT_SCHEME || !signature) return null;
62
93
  if (!Number.isSafeInteger(issuedAt) || Math.abs(now - issuedAt) > DAEMON_PREFLIGHT_TTL_SECONDS) return null;
63
94
  if (!/^[A-Za-z0-9_-]{24,128}$/.test(nonce)) return null;
64
- let publicJwk: JsonWebKey;
65
- try { publicJwk = parsePublicJwk(input.publicKeyJson); } catch { return null; }
66
- if (!(await safeEqual(keyId, await publicKeyId(publicJwk)))) return null;
95
+ if (!(await safeEqual(keyId, certificate.sessionKeyId))) return null;
67
96
  let transcript: string;
68
97
  try {
69
- transcript = daemonPreflightTranscript({
70
- workerOrigin: input.workerOrigin,
71
- server: input.server,
72
- version: input.version,
73
- nonce,
74
- issuedAt,
75
- });
76
- } catch {
77
- return null;
78
- }
79
- try {
80
- const key = await crypto.subtle.importKey("jwk", publicJwk, { name: "ECDSA", namedCurve: "P-256" }, false, ["verify"]);
81
- const valid = await crypto.subtle.verify(
82
- { name: "ECDSA", hash: "SHA-256" },
83
- key,
84
- signature,
85
- new TextEncoder().encode(transcript),
86
- );
87
- return valid ? { nonce, expiresAt: issuedAt + DAEMON_PREFLIGHT_TTL_SECONDS } : null;
98
+ transcript = daemonPreflightTranscript({ workerOrigin: input.workerOrigin, server: input.server, version: input.version, nonce, issuedAt });
88
99
  } catch {
89
100
  return null;
90
101
  }
102
+ if (!(await verifyP256Signature(certificate.sessionPublicJwk, transcript, signature))) return null;
103
+ return {
104
+ nonce,
105
+ expiresAt: issuedAt + DAEMON_PREFLIGHT_TTL_SECONDS,
106
+ sessionPublicKeyJson: certificate.sessionPublicKeyJson,
107
+ sessionKeyId: certificate.sessionKeyId,
108
+ certificateExpiresAt: certificate.expiresAt,
109
+ };
91
110
  }
92
111
 
93
-
94
112
  export async function consumeDaemonPreflightNonce(
95
113
  storage: DurableObjectStorage,
96
114
  authorization: DaemonPreflightAuthorization,
@@ -103,6 +121,7 @@ export async function consumeDaemonPreflightNonce(
103
121
  now,
104
122
  noncePattern: /^[A-Za-z0-9_-]{24,128}$/,
105
123
  maximum: 128,
124
+ maxFutureSeconds: DAEMON_PREFLIGHT_TTL_SECONDS * 2,
106
125
  });
107
126
  }
108
127
 
@@ -113,10 +132,12 @@ export async function verifyDaemonAuthentication(input: {
113
132
  server: string;
114
133
  version: string;
115
134
  instanceId: string;
135
+ certificateExpiresAt?: number;
116
136
  now?: number;
117
137
  }): Promise<boolean> {
118
138
  const now = Number.isSafeInteger(input.now) ? Number(input.now) : Math.floor(Date.now() / 1000);
119
139
  if (now < input.challenge.issuedAt - 5 || now > input.challenge.expiresAt) return false;
140
+ if (input.certificateExpiresAt !== undefined && (!Number.isSafeInteger(input.certificateExpiresAt) || now > input.certificateExpiresAt)) return false;
120
141
  if (!input.authentication || typeof input.authentication !== "object" || Array.isArray(input.authentication)) return false;
121
142
  const auth = input.authentication as Record<string, unknown>;
122
143
  if (auth.scheme !== DAEMON_AUTH_SCHEME) return false;
@@ -126,8 +147,7 @@ export async function verifyDaemonAuthentication(input: {
126
147
  if (!signature) return false;
127
148
  let publicJwk: JsonWebKey;
128
149
  try { publicJwk = parsePublicJwk(input.publicKeyJson); } catch { return false; }
129
- const expectedKeyId = await publicKeyId(publicJwk);
130
- if (!(await safeEqual(String(auth.key_id || ""), expectedKeyId))) return false;
150
+ if (!(await safeEqual(String(auth.key_id || ""), await publicKeyId(publicJwk)))) return false;
131
151
  let transcript: string;
132
152
  try {
133
153
  transcript = daemonAuthTranscript({
@@ -141,45 +161,7 @@ export async function verifyDaemonAuthentication(input: {
141
161
  } catch {
142
162
  return false;
143
163
  }
144
- try {
145
- const key = await crypto.subtle.importKey("jwk", publicJwk, { name: "ECDSA", namedCurve: "P-256" }, false, ["verify"]);
146
- return crypto.subtle.verify(
147
- { name: "ECDSA", hash: "SHA-256" },
148
- key,
149
- signature,
150
- new TextEncoder().encode(transcript),
151
- );
152
- } catch {
153
- return false;
154
- }
155
- }
156
-
157
- function parsePublicJwk(value: string): JsonWebKey {
158
- const parsed = JSON.parse(value) as JsonWebKey;
159
- if (!parsed || parsed.kty !== "EC" || parsed.crv !== "P-256") throw new Error("invalid device public key");
160
- if (typeof parsed.x !== "string" || typeof parsed.y !== "string" || parsed.d !== undefined) throw new Error("invalid device public key");
161
- return { kty: "EC", crv: "P-256", x: parsed.x, y: parsed.y, ext: true, key_ops: ["verify"] };
162
- }
163
-
164
- async function publicKeyId(jwk: JsonWebKey): Promise<string> {
165
- const canonical = JSON.stringify({ crv: jwk.crv, kty: jwk.kty, x: jwk.x, y: jwk.y });
166
- const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(canonical));
167
- return `device_${base64Url(new Uint8Array(digest)).slice(0, 32)}`;
168
- }
169
-
170
- function decodeBase64Url(value: string, expectedBytes: number): Uint8Array<ArrayBuffer> | null {
171
- if (!/^[A-Za-z0-9_-]+$/.test(value)) return null;
172
- const normalized = value.replace(/-/g, "+").replace(/_/g, "/");
173
- const padding = "=".repeat((4 - (normalized.length % 4)) % 4);
174
- try {
175
- const binary = atob(normalized + padding);
176
- if (binary.length !== expectedBytes) return null;
177
- const bytes = new Uint8Array(expectedBytes);
178
- for (let index = 0; index < binary.length; index += 1) bytes[index] = binary.charCodeAt(index);
179
- return bytes;
180
- } catch {
181
- return null;
182
- }
164
+ return verifyP256Signature(publicJwk, transcript, signature);
183
165
  }
184
166
 
185
167
  function base64Url(bytes: Uint8Array): string {
@@ -188,7 +170,6 @@ function base64Url(bytes: Uint8Array): string {
188
170
  return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
189
171
  }
190
172
 
191
-
192
173
  function positiveSafeInteger(value: unknown): number | undefined {
193
174
  const number = Number(value);
194
175
  return Number.isSafeInteger(number) && number > 0 ? number : undefined;
@@ -0,0 +1,36 @@
1
+ export const DAEMON_CHANNEL_OPEN = 1;
2
+
3
+ export interface DaemonChannel {
4
+ readonly readyState: number;
5
+ send(data: string): void;
6
+ readonly daemonTransport?: "https";
7
+ }
8
+
9
+ export interface ReadyDaemonRegistry {
10
+ readyChannels?(): DaemonChannel[];
11
+ readySockets?(): DaemonChannel[];
12
+ }
13
+
14
+ export function readyDaemonChannels(registry: ReadyDaemonRegistry): DaemonChannel[] {
15
+ if (typeof registry.readyChannels === "function") return registry.readyChannels();
16
+ if (typeof registry.readySockets === "function") return registry.readySockets();
17
+ return [];
18
+ }
19
+
20
+ export function daemonChannelOpen(channel: DaemonChannel | undefined): boolean {
21
+ return Boolean(channel && channel.readyState === DAEMON_CHANNEL_OPEN);
22
+ }
23
+
24
+ export function daemonChannelTransport(channel: DaemonChannel | undefined): "https" | "websocket" {
25
+ return channel?.daemonTransport === "https" ? "https" : "websocket";
26
+ }
27
+
28
+ export function trySendDaemonChannel(channel: DaemonChannel | undefined, value: unknown): boolean {
29
+ if (!channel || channel.readyState !== DAEMON_CHANNEL_OPEN) return false;
30
+ try {
31
+ channel.send(JSON.stringify(value));
32
+ return true;
33
+ } catch {
34
+ return false;
35
+ }
36
+ }
@@ -0,0 +1,63 @@
1
+ import {
2
+ DAEMON_HTTP_RELAY_SCHEME, DAEMON_HTTP_RELAY_TTL_SECONDS, daemonHttpRelayTranscript,
3
+ } from "../shared/daemon-auth.mjs";
4
+ import { safeEqual } from "./oauth-state.ts";
5
+ import { consumeBoundedNonce } from "./nonce-store.ts";
6
+ import {
7
+ decodeBase64Url, verifyDeviceSessionCertificate, verifyP256Signature,
8
+ } from "./device-session-verifier.ts";
9
+
10
+ const SESSION_CERTIFICATE_HEADER = "X-Bridge-Device-Certificate";
11
+
12
+ export async function verifyDaemonHttpRelayRequest(input: {
13
+ storage: DurableObjectStorage;
14
+ publicKeyJson: string;
15
+ headers: Headers;
16
+ body: Uint8Array;
17
+ workerOrigin: string;
18
+ server: string;
19
+ version: string;
20
+ now?: number;
21
+ }): Promise<boolean> {
22
+ const now = Number.isSafeInteger(input.now) ? Number(input.now) : Math.floor(Date.now() / 1000);
23
+ const certificate = await verifyDeviceSessionCertificate({
24
+ encodedCertificate: input.headers.get(SESSION_CERTIFICATE_HEADER) || "",
25
+ rootPublicKeyJson: input.publicKeyJson,
26
+ workerOrigin: input.workerOrigin,
27
+ server: input.server,
28
+ version: input.version,
29
+ now,
30
+ });
31
+ if (!certificate) return false;
32
+ const scheme = input.headers.get("X-Bridge-Device-Scheme") || "";
33
+ const keyId = input.headers.get("X-Bridge-Device-Key") || "";
34
+ const nonce = input.headers.get("X-Bridge-Device-Nonce") || "";
35
+ const issuedAt = Number(input.headers.get("X-Bridge-Device-Time"));
36
+ const claimedBodyHash = input.headers.get("X-Bridge-Body-SHA256") || "";
37
+ const signature = decodeBase64Url(input.headers.get("X-Bridge-Device-Signature") || "", 64);
38
+ if (scheme !== DAEMON_HTTP_RELAY_SCHEME || !signature || !/^[A-Za-z0-9_-]{43}$/.test(claimedBodyHash)) return false;
39
+ if (!Number.isSafeInteger(issuedAt) || Math.abs(now - issuedAt) > DAEMON_HTTP_RELAY_TTL_SECONDS) return false;
40
+ if (!/^[A-Za-z0-9_-]{24,128}$/.test(nonce) || !(await safeEqual(keyId, certificate.sessionKeyId))) return false;
41
+ const digestInput = new Uint8Array(input.body.byteLength);
42
+ digestInput.set(input.body);
43
+ const actualBodyHash = base64Url(new Uint8Array(await crypto.subtle.digest("SHA-256", digestInput)));
44
+ if (!(await safeEqual(claimedBodyHash, actualBodyHash))) return false;
45
+ let transcript: string;
46
+ try {
47
+ transcript = daemonHttpRelayTranscript({
48
+ workerOrigin: input.workerOrigin, server: input.server, version: input.version,
49
+ nonce, issuedAt, bodySha256: claimedBodyHash,
50
+ });
51
+ } catch { return false; }
52
+ if (!(await verifyP256Signature(certificate.sessionPublicJwk, transcript, signature))) return false;
53
+ return consumeBoundedNonce(input.storage, {
54
+ key: "daemon-http-relay-nonces", nonce, expiresAt: issuedAt + DAEMON_HTTP_RELAY_TTL_SECONDS, now,
55
+ noncePattern: /^[A-Za-z0-9_-]{24,128}$/, maximum: 1024, maxFutureSeconds: DAEMON_HTTP_RELAY_TTL_SECONDS * 2,
56
+ });
57
+ }
58
+
59
+ function base64Url(bytes: Uint8Array): string {
60
+ let binary = "";
61
+ for (const byte of bytes) binary += String.fromCharCode(byte);
62
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
63
+ }
@@ -0,0 +1,120 @@
1
+ import relayContract from "../shared/relay-contract.json" with { type: "json" };
2
+ import { DAEMON_CHANNEL_OPEN, type DaemonChannel } from "./daemon-channel.ts";
3
+ import type { DaemonAttachment } from "./daemon-socket-attachment.ts";
4
+ import { relayDiagnosticsAfterReady } from "./daemon-relay-diagnostics.ts";
5
+
6
+ type SequencedMessage = Readonly<{ seq: number; payload: unknown; bytes: number }>;
7
+
8
+ export class DaemonHttpChannel implements DaemonChannel {
9
+ readonly daemonTransport = "https" as const;
10
+ readonly sessionId: string;
11
+ readonly activationToken: string;
12
+ readonly attachment: DaemonAttachment;
13
+ private activatedAtMs = 0;
14
+ private verifiedReadyAtMs = 0;
15
+ private lastSeenAtMs: number;
16
+ private nextWorkerSeq = 1;
17
+ private acknowledgedWorkerSeq = 0;
18
+ private acknowledgedDaemonSeq = 0;
19
+ private queuedBytes = 0;
20
+ private readonly outbound: SequencedMessage[] = [];
21
+ private closed = false;
22
+
23
+ constructor(input: {
24
+ sessionId: string;
25
+ activationToken: string;
26
+ attachment: DaemonAttachment;
27
+ now?: number;
28
+ }) {
29
+ this.sessionId = input.sessionId;
30
+ this.activationToken = input.activationToken;
31
+ this.attachment = input.attachment;
32
+ this.lastSeenAtMs = input.now ?? Date.now();
33
+ }
34
+
35
+ get readyState(): number { return !this.closed && this.verifiedReadyAtMs > 0 ? DAEMON_CHANNEL_OPEN : 0; }
36
+ get lastSeenMs(): number { return this.lastSeenAtMs; }
37
+ get activatedMs(): number { return this.activatedAtMs; }
38
+ get isActivated(): boolean { return !this.closed && this.activatedAtMs > 0; }
39
+ get daemonSequence(): number { return this.acknowledgedDaemonSeq; }
40
+
41
+ activate(now = Date.now()): void {
42
+ if (this.closed) throw new Error("HTTP daemon channel is closed");
43
+ this.activatedAtMs = now;
44
+ this.touch(now);
45
+ this.attachment.role = "probing";
46
+ }
47
+
48
+ verifyReady(now = Date.now()): void {
49
+ if (!this.isActivated) throw new Error("HTTP daemon channel was not activated");
50
+ this.verifiedReadyAtMs = now;
51
+ this.touch(now);
52
+ this.attachment.role = "daemon";
53
+ this.attachment.connectedAt = new Date(now).toISOString();
54
+ this.attachment.lastSeenAt = this.attachment.connectedAt;
55
+ this.attachment.relayDiagnostics = relayDiagnosticsAfterReady(
56
+ this.attachment.relayDiagnostics,
57
+ this.attachment.connectedAt,
58
+ );
59
+ }
60
+
61
+ touch(now = Date.now()): void {
62
+ this.lastSeenAtMs = now;
63
+ this.attachment.lastSeenAt = new Date(now).toISOString();
64
+ }
65
+
66
+ send(data: string): void {
67
+ if (!this.isActivated) throw new Error("HTTP daemon channel is not activated");
68
+ const bytes = new TextEncoder().encode(data).byteLength;
69
+ if (bytes > relayContract.httpFallbackMaximumMessageBytes
70
+ || this.outbound.length >= relayContract.httpFallbackMaximumQueuedMessages
71
+ || this.queuedBytes + bytes > relayContract.httpFallbackMaximumQueuedBytes) {
72
+ throw new Error("HTTP daemon relay queue capacity exceeded");
73
+ }
74
+ let payload: unknown;
75
+ try { payload = JSON.parse(data); } catch { throw new Error("HTTP daemon relay message is not JSON"); }
76
+ this.outbound.push(Object.freeze({ seq: this.nextWorkerSeq++, payload, bytes }));
77
+ this.queuedBytes += bytes;
78
+ }
79
+
80
+ acknowledgeWorker(sequence: number): boolean {
81
+ if (!Number.isSafeInteger(sequence) || sequence < 0 || sequence >= this.nextWorkerSeq) return false;
82
+ if (sequence <= this.acknowledgedWorkerSeq) return true;
83
+ this.acknowledgedWorkerSeq = sequence;
84
+ while (this.outbound[0]?.seq <= sequence) {
85
+ const removed = this.outbound.shift();
86
+ if (removed) this.queuedBytes = Math.max(0, this.queuedBytes - removed.bytes);
87
+ }
88
+ return true;
89
+ }
90
+
91
+ outboundMessages(): Array<{ seq: number; payload: unknown }> {
92
+ const selected: Array<{ seq: number; payload: unknown }> = [];
93
+ let bytes = 0;
94
+ for (const message of this.outbound) {
95
+ if (selected.length >= relayContract.httpFallbackMaximumQueuedMessages) break;
96
+ if (selected.length > 0 && bytes + message.bytes > relayContract.httpFallbackMaximumEnvelopeBytes) break;
97
+ selected.push({ seq: message.seq, payload: message.payload });
98
+ bytes += message.bytes;
99
+ }
100
+ return selected;
101
+ }
102
+
103
+ acceptDaemonSequence(sequence: number): "new" | "duplicate" | "gap" {
104
+ if (!Number.isSafeInteger(sequence) || sequence <= 0) return "gap";
105
+ if (sequence <= this.acknowledgedDaemonSeq) return "duplicate";
106
+ if (sequence !== this.acknowledgedDaemonSeq + 1) return "gap";
107
+ return "new";
108
+ }
109
+
110
+ commitDaemonSequence(sequence: number): void {
111
+ if (sequence !== this.acknowledgedDaemonSeq + 1) throw new Error("HTTP daemon sequence commit is not contiguous");
112
+ this.acknowledgedDaemonSeq = sequence;
113
+ }
114
+
115
+ close(): void {
116
+ this.closed = true;
117
+ this.outbound.length = 0;
118
+ this.queuedBytes = 0;
119
+ }
120
+ }
@@ -0,0 +1,163 @@
1
+ import relayContract from "../shared/relay-contract.json" with { type: "json" };
2
+ import { authorityRevocations, authorityRevocationWireMessage } from "./authority-revocations.ts";
3
+ import type { DaemonChannel } from "./daemon-channel.ts";
4
+ import { verifyDaemonHttpRelayRequest } from "./daemon-http-auth.ts";
5
+ import type { DaemonHttpChannel } from "./daemon-http-channel.ts";
6
+ import { normalizeDaemonHttpExchange } from "./daemon-http-protocol.ts";
7
+ import type { DaemonRegistry } from "./daemon-registry.ts";
8
+ import { handleReadyDaemonMessage } from "./daemon-ready-messages.ts";
9
+ import { notifyReadyDaemon } from "./daemon-ready-waiters.ts";
10
+ import { HttpError, json, readBoundedBytes } from "./http.ts";
11
+ import type { WorkerObservability } from "./observability.ts";
12
+ import type { PendingCallRegistry } from "./pending-calls.ts";
13
+ import { sanitizeDaemonPolicy, sanitizeDaemonTools } from "./policy.ts";
14
+ import { sanitizeDaemonRelayDiagnostics } from "./daemon-relay-diagnostics.ts";
15
+ import { randomToken } from "./oauth-state.ts";
16
+
17
+ export async function handleDaemonHttpRelay(input: {
18
+ request: Request;
19
+ storage: DurableObjectStorage;
20
+ registry: DaemonRegistry;
21
+ pending: PendingCallRegistry;
22
+ observability: WorkerObservability;
23
+ publicKeyJson: string;
24
+ server: string;
25
+ version: string;
26
+ scheduleAlarm: () => Promise<void>;
27
+ detachChannel: (channel: DaemonChannel, message: string) => Promise<void>;
28
+ retireWebSocket: (socket: WebSocket, message: string) => Promise<void>;
29
+ }): Promise<Response> {
30
+ const bodyBytes = await readBoundedBytes(input.request, relayContract.httpFallbackMaximumEnvelopeBytes);
31
+ const workerOrigin = new URL(input.request.url).origin;
32
+ if (!(await verifyDaemonHttpRelayRequest({
33
+ storage: input.storage, publicKeyJson: input.publicKeyJson, headers: input.request.headers,
34
+ body: bodyBytes, workerOrigin, server: input.server, version: input.version,
35
+ }))) return json({ error: "daemon_http_unauthorized" }, 401);
36
+ const exchange = normalizeDaemonHttpExchange(parseJson(bodyBytes));
37
+ if (!exchange) return json({ error: "invalid_daemon_http_exchange" }, 400);
38
+
39
+ let channel = input.registry.http.get(exchange.sessionId);
40
+ if (!channel && (exchange.activationToken || exchange.ackWorkerSeq !== 0 || exchange.messages.length > 0)) {
41
+ return json({ error: "unknown_daemon_http_session" }, 409);
42
+ }
43
+ const readyHttpChannels = input.registry.httpReadyChannels();
44
+ if (!channel && readyHttpChannels.some((candidate) => candidate.attachment.instanceId !== exchange.instanceId)) {
45
+ return relayResponse("standby", null, 0, []);
46
+ }
47
+ const readySockets = input.registry.readySockets();
48
+ if (readySockets.length > 0) {
49
+ if (channel || !exchange.takeoverWebSocket || readySockets.some((socket) =>
50
+ input.registry.readyAttachment(socket)?.instanceId !== exchange.instanceId)) {
51
+ if (channel) input.registry.http.close(channel);
52
+ return relayResponse("standby", null, 0, []);
53
+ }
54
+ for (const socket of readySockets) {
55
+ await input.retireWebSocket(socket, "authenticated HTTPS fallback replacing same-instance WebSocket");
56
+ }
57
+ }
58
+
59
+ if (!channel) {
60
+ const sameInstanceChannels = [
61
+ ...readyHttpChannels,
62
+ ...input.registry.httpCandidates().filter((candidate) => candidate.sessionId !== exchange.sessionId),
63
+ ].filter((candidate) => candidate.attachment.instanceId === exchange.instanceId);
64
+ for (const previous of sameInstanceChannels) {
65
+ await input.detachChannel(previous, "new authenticated HTTPS fallback session replaced same-instance channel");
66
+ input.registry.http.close(previous);
67
+ }
68
+ const policy = sanitizeDaemonPolicy(exchange.policy);
69
+ const now = Date.now();
70
+ channel = input.registry.http.beginCandidate(exchange.sessionId, {
71
+ role: "candidate", connectedAt: new Date(now).toISOString(), lastSeenAt: new Date(now).toISOString(),
72
+ instanceId: exchange.instanceId, connectionId: randomToken("connection"), policy,
73
+ tools: sanitizeDaemonTools(exchange.tools, policy),
74
+ relayDiagnostics: sanitizeDaemonRelayDiagnostics(exchange.relayDiagnostics),
75
+ }, now);
76
+ if (!channel) return json({ error: "daemon_http_candidate_capacity" }, 503);
77
+ const queuedRevocations = await authorityRevocations(input.storage);
78
+ input.registry.http.activate(exchange.sessionId, channel.activationToken, now);
79
+ const rebound = input.pending.rebindInstance(exchange.instanceId, channel);
80
+ channel.send(JSON.stringify({ type: "resume_calls", ids: rebound }));
81
+ for (const revocation of queuedRevocations) channel.send(JSON.stringify(authorityRevocationWireMessage(revocation)));
82
+ channel.send(JSON.stringify({ type: "ready_ack", server: input.server, version: input.version }));
83
+ await input.scheduleAlarm();
84
+ return relayResponse("probing", channel.activationToken, channel.daemonSequence, channel.outboundMessages());
85
+ }
86
+ if (channel.attachment.instanceId !== exchange.instanceId) return json({ error: "daemon_http_instance_mismatch" }, 409);
87
+
88
+ if (channel.readyState !== 1) {
89
+ if (!channel.isActivated) {
90
+ input.registry.http.close(channel);
91
+ return json({ error: "invalid_daemon_http_candidate_state" }, 409);
92
+ }
93
+ if (exchange.activationToken !== channel.activationToken || !channel.acknowledgeWorker(exchange.ackWorkerSeq)) {
94
+ return json({ error: "invalid_daemon_http_acknowledgement" }, 409);
95
+ }
96
+ channel.touch();
97
+ for (const message of exchange.messages) {
98
+ const sequence = channel.acceptDaemonSequence(message.seq);
99
+ if (sequence === "duplicate") continue;
100
+ if (sequence === "gap") return invalidate(channel, input, "daemon_http_sequence_gap");
101
+ if (message.payload.type === "https_ready") {
102
+ if (channel.outboundMessages().length > 0) {
103
+ return invalidate(channel, input, "daemon_http_ready_before_control_ack");
104
+ }
105
+ channel.verifyReady();
106
+ input.registry.rememberReady(channel);
107
+ channel.commitDaemonSequence(message.seq);
108
+ const previous = input.registry.httpReadyChannels().filter((candidate) => candidate !== channel);
109
+ for (const old of previous) {
110
+ await input.detachChannel(old, "HTTPS fallback channel replaced after verified handover");
111
+ input.registry.http.close(old);
112
+ }
113
+ input.observability.event("info", "daemon.https_fallback.ready", { daemon_owned_calls: exchange.ownedCallIds.length });
114
+ notifyReadyDaemon(input.registry);
115
+ continue;
116
+ }
117
+ const handled = await handleReadyDaemonMessage({
118
+ channel, body: message.payload, pending: input.pending, storage: input.storage, observability: input.observability,
119
+ });
120
+ if (!handled.ok) return invalidate(channel, input, handled.errorCode ?? "invalid_daemon_http_message");
121
+ channel.commitDaemonSequence(message.seq);
122
+ }
123
+ await input.scheduleAlarm();
124
+ return relayResponse(channel.readyState === 1 ? "ready" : "probing", channel.activationToken, channel.daemonSequence, channel.outboundMessages());
125
+ }
126
+
127
+ if (exchange.activationToken !== channel.activationToken || !channel.acknowledgeWorker(exchange.ackWorkerSeq)) {
128
+ return json({ error: "invalid_daemon_http_acknowledgement" }, 409);
129
+ }
130
+ channel.touch();
131
+ for (const message of exchange.messages) {
132
+ const sequence = channel.acceptDaemonSequence(message.seq);
133
+ if (sequence === "duplicate") continue;
134
+ if (sequence === "gap") return invalidate(channel, input, "daemon_http_sequence_gap");
135
+ const handled = await handleReadyDaemonMessage({
136
+ channel, body: message.payload, pending: input.pending, storage: input.storage, observability: input.observability,
137
+ });
138
+ if (!handled.ok) return invalidate(channel, input, handled.errorCode ?? "invalid_daemon_http_message");
139
+ channel.commitDaemonSequence(message.seq);
140
+ }
141
+ await input.scheduleAlarm();
142
+ return relayResponse("ready", channel.activationToken, channel.daemonSequence, channel.outboundMessages());
143
+ }
144
+
145
+ async function invalidate(channel: DaemonHttpChannel, input: Parameters<typeof handleDaemonHttpRelay>[0], code: string): Promise<Response> {
146
+ await input.detachChannel(channel, `HTTPS fallback protocol failure: ${code}`);
147
+ input.registry.http.close(channel);
148
+ input.observability.event("warn", "daemon.https_fallback.invalidated", { error_class: code });
149
+ await input.scheduleAlarm();
150
+ return json({ error: code }, 409);
151
+ }
152
+
153
+ function relayResponse(phase: "probing" | "ready" | "standby", activationToken: string | null, ackDaemonSeq: number, messages: unknown[]): Response {
154
+ return json({ protocol: 1, phase, activation_token: activationToken, ack_daemon_seq: ackDaemonSeq, messages });
155
+ }
156
+
157
+ function parseJson(bytes: Uint8Array): unknown {
158
+ let text: string;
159
+ try { text = new TextDecoder("utf-8", { fatal: true }).decode(bytes); }
160
+ catch { throw new HttpError(400, "invalid_encoding", "daemon HTTP relay body must be UTF-8"); }
161
+ try { return JSON.parse(text); }
162
+ catch { throw new HttpError(400, "invalid_json", "daemon HTTP relay body is not valid JSON"); }
163
+ }
@@ -0,0 +1,62 @@
1
+ import { sanitizeDaemonInstanceId } from "./daemon-socket-attachment.ts";
2
+
3
+ const SESSION_ID = /^relay_http_[A-Za-z0-9_-]{43}$/;
4
+ const ACTIVATION_TOKEN = /^activate_[A-Za-z0-9_-]{43}$/;
5
+ const CALL_ID = /^call_[A-Za-z0-9_-]{8,240}$/;
6
+
7
+ export interface DaemonHttpExchange {
8
+ sessionId: string;
9
+ instanceId: string;
10
+ activationToken: string;
11
+ ackWorkerSeq: number;
12
+ takeoverWebSocket: boolean;
13
+ ownedCallIds: string[];
14
+ messages: Array<{ seq: number; payload: Record<string, unknown> }>;
15
+ tools: unknown;
16
+ policy: unknown;
17
+ relayDiagnostics: unknown;
18
+ }
19
+
20
+ export function normalizeDaemonHttpExchange(value: unknown): DaemonHttpExchange | null {
21
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
22
+ const body = value as Record<string, unknown>;
23
+ if (body.protocol !== 1) return null;
24
+ const sessionId = typeof body.session_id === "string" && SESSION_ID.test(body.session_id) ? body.session_id : "";
25
+ const instanceId = sanitizeDaemonInstanceId(body.instance_id) ?? "";
26
+ const activationToken = body.activation_token === undefined ? ""
27
+ : typeof body.activation_token === "string" && ACTIVATION_TOKEN.test(body.activation_token) ? body.activation_token : "invalid";
28
+ const ackWorkerSeq = Number(body.ack_worker_seq);
29
+ const takeoverWebSocket = body.takeover_websocket === undefined ? false : body.takeover_websocket;
30
+ const ownedCallIds = normalizeCallIds(body.owned_call_ids);
31
+ const messages = normalizeMessages(body.messages);
32
+ if (!sessionId || !instanceId || activationToken === "invalid" || !Number.isSafeInteger(ackWorkerSeq) || ackWorkerSeq < 0
33
+ || typeof takeoverWebSocket !== "boolean" || !ownedCallIds || !messages) return null;
34
+ return {
35
+ sessionId, instanceId, activationToken, ackWorkerSeq, takeoverWebSocket, ownedCallIds, messages,
36
+ tools: body.tools, policy: body.policy, relayDiagnostics: body.relay_diagnostics,
37
+ };
38
+ }
39
+
40
+ function normalizeCallIds(value: unknown): string[] | null {
41
+ if (!Array.isArray(value) || value.length > 32) return null;
42
+ const result: string[] = [];
43
+ const seen = new Set<string>();
44
+ for (const item of value) {
45
+ if (typeof item !== "string" || !CALL_ID.test(item) || seen.has(item)) return null;
46
+ seen.add(item); result.push(item);
47
+ }
48
+ return result;
49
+ }
50
+
51
+ function normalizeMessages(value: unknown): Array<{ seq: number; payload: Record<string, unknown> }> | null {
52
+ if (!Array.isArray(value) || value.length > 64) return null;
53
+ const result = [];
54
+ for (const item of value) {
55
+ if (!item || typeof item !== "object" || Array.isArray(item)) return null;
56
+ const record = item as Record<string, unknown>;
57
+ if (!Number.isSafeInteger(record.seq) || Number(record.seq) <= 0
58
+ || !record.payload || typeof record.payload !== "object" || Array.isArray(record.payload)) return null;
59
+ result.push({ seq: Number(record.seq), payload: record.payload as Record<string, unknown> });
60
+ }
61
+ return result;
62
+ }