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,4 +1,7 @@
1
- import { closeSync, constants as fsConstants, fchmodSync, fstatSync, lstatSync, mkdirSync, openSync, readSync } from "node:fs";
1
+ import { closeSync, constants as fsConstants, fchmodSync, fstatSync, lstatSync, mkdirSync, openSync, readSync, unlinkSync } from "node:fs";
2
+ import { filesystemIdentity, sameFilesystemIdentity } from "./filesystem-identity.mjs";
3
+
4
+ const MULTIPLE_LINK_RETRY_BUFFER = new Int32Array(new SharedArrayBuffer(4));
2
5
 
3
6
  export function openRegularFileSync(file, flags, options = {}) {
4
7
  const mode = Number.isInteger(options.mode) ? options.mode : undefined;
@@ -14,15 +17,25 @@ export function openRegularFileSync(file, flags, options = {}) {
14
17
  throw error;
15
18
  }
16
19
  try {
17
- const info = fstatSync(fd);
20
+ const inspectDescriptor = options.fstatSync || fstatSync;
21
+ const info = inspectDescriptor(fd);
18
22
  if (!info.isFile()) throw new Error(`${label} is not a regular file`);
23
+ const descriptorIdentityInfo = options.fstatSync ? info : fstatSync(fd, { bigint: true });
24
+ const identity = filesystemIdentity(descriptorIdentityInfo, label);
25
+ if (options.rejectMultipleLinks === true && Number(info.nlink) > 1) {
26
+ throw Object.assign(new Error(`${label} must not have multiple hard links`), { code: "MBM_MULTIPLE_HARD_LINKS" });
27
+ }
19
28
  if (options.verifyPathIdentity === true) {
20
- const pathInfo = lstatSync(file);
29
+ const inspectPath = options.lstatSync || lstatSync;
30
+ const pathInfo = inspectPath(file);
21
31
  if (pathInfo.isSymbolicLink() || !pathInfo.isFile()) throw new Error(`${label} must be a regular file and not a symbolic link`);
22
- if (!sameFileIdentity(info, pathInfo)) throw new Error(`${label} identity changed while opening`);
32
+ const pathIdentityInfo = options.lstatSync ? pathInfo : lstatSync(file, { bigint: true });
33
+ if (!sameFilesystemIdentity(identity, filesystemIdentity(pathIdentityInfo, label))) {
34
+ throw Object.assign(new Error(`${label} identity changed while opening`), { code: "MBM_IDENTITY_CHANGED" });
35
+ }
23
36
  }
24
37
  if (Number.isInteger(options.chmod)) setDescriptorMode(fd, options.chmod);
25
- return { fd, info };
38
+ return { fd, info, identity, identityInfo: descriptorIdentityInfo };
26
39
  } catch (error) {
27
40
  closeSync(fd);
28
41
  throw error;
@@ -32,14 +45,60 @@ export function openRegularFileSync(file, flags, options = {}) {
32
45
  function withRegularFileSync(file, flags, options, callback) {
33
46
  const opened = openRegularFileSync(file, flags, options);
34
47
  try {
35
- return callback(opened.fd, opened.info);
48
+ return callback(opened.fd, opened.info, opened.identity, opened.identityInfo);
36
49
  } finally {
37
50
  closeSync(opened.fd);
38
51
  }
39
52
  }
40
53
 
54
+ export function retryTransientMultipleLinksSync(callback) {
55
+ for (let attempt = 1; attempt <= 4; attempt += 1) {
56
+ try { return callback(); }
57
+ catch (error) {
58
+ if (error?.code !== "MBM_MULTIPLE_HARD_LINKS" || attempt === 4) throw error;
59
+ Atomics.wait(MULTIPLE_LINK_RETRY_BUFFER, 0, 0, 1);
60
+ }
61
+ }
62
+ throw new Error("transient multiple-link retry did not settle");
63
+ }
64
+
41
65
  export function chmodRegularFileSync(file, mode, label = "path") {
42
- return withRegularFileSync(file, fsConstants.O_RDONLY, { label, chmod: mode }, () => undefined);
66
+ return withRegularFileSync(file, fsConstants.O_RDONLY, { label, chmod: mode, rejectMultipleLinks: true }, () => undefined);
67
+ }
68
+
69
+ export function chmodRegularFileIfIdentitySync(file, expectedIdentity, mode, label = "path") {
70
+ return withRegularFileSync(file, fsConstants.O_RDONLY, { label, rejectMultipleLinks: true }, (fd, _info, identity) => {
71
+ if (!sameFilesystemIdentity(expectedIdentity, identity)) throw new Error(`${label} changed before permission update`);
72
+ setDescriptorMode(fd, mode);
73
+ });
74
+ }
75
+
76
+ export function ownerOnlyFile(filePath) {
77
+ return chmodRegularFileSync(filePath, 0o600, "owner-only path");
78
+ }
79
+
80
+ export function unlinkRegularFileIfIdentitySync(file, expectedIdentity, label = "file") {
81
+ let current;
82
+ try { current = lstatSync(file, { bigint: true }); } catch (error) {
83
+ if (String(/** @type {any} */ (error)?.code || "") === "ENOENT") return false;
84
+ throw error;
85
+ }
86
+ if (current.isSymbolicLink() || !current.isFile() || current.nlink !== 1n
87
+ || !sameFilesystemIdentity(expectedIdentity, filesystemIdentity(current, label))) return false;
88
+ try { unlinkSync(file); return true; } catch (error) {
89
+ if (String(/** @type {any} */ (error)?.code || "") === "ENOENT") return false;
90
+ throw error;
91
+ }
92
+ }
93
+
94
+
95
+ export function inspectPathIfPresentSync(file, label = "path", options = {}) {
96
+ const inspect = options.lstatSync || lstatSync;
97
+ try { return inspect(file); }
98
+ catch (error) {
99
+ if (error?.code === "ENOENT") return null;
100
+ throw new Error(`${label} could not be inspected`, { cause: error });
101
+ }
43
102
  }
44
103
 
45
104
  export function readBoundedRegularFileSync(file, maxBytes, label = "path", options = {}) {
@@ -52,7 +111,8 @@ export function readBoundedRegularFileWithInfoSync(file, maxBytes, label = "path
52
111
  return withRegularFileSync(file, fsConstants.O_RDONLY, {
53
112
  label,
54
113
  verifyPathIdentity: options.verifyPathIdentity === true,
55
- }, (fd, info) => {
114
+ rejectMultipleLinks: options.rejectMultipleLinks === true,
115
+ }, (fd, info, identity, identityInfo) => {
56
116
  if (info.size > limit) throw new Error(`file exceeds ${limit} bytes`);
57
117
  options.afterOpen?.({ fd, info });
58
118
  const buffer = Buffer.alloc(info.size);
@@ -62,7 +122,7 @@ export function readBoundedRegularFileWithInfoSync(file, maxBytes, label = "path
62
122
  if (!count) break;
63
123
  offset += count;
64
124
  }
65
- return { buffer: buffer.subarray(0, offset), info };
125
+ return { buffer: buffer.subarray(0, offset), info, identity, identityInfo };
66
126
  });
67
127
  }
68
128
 
@@ -108,19 +168,12 @@ export function ensureOwnerOnlyDirectorySync(dir, options = {}) {
108
168
  }
109
169
  }
110
170
 
171
+ export function ensureOwnerOnlyDir(dir, options = {}) {
172
+ return ensureOwnerOnlyDirectorySync(dir, options);
173
+ }
174
+
111
175
  function setDescriptorMode(fd, mode) {
112
176
  try { fchmodSync(fd, mode); } catch (error) {
113
177
  if (process.platform !== "win32") throw error;
114
178
  }
115
179
  }
116
-
117
- function sameFileIdentity(left, right) {
118
- const leftDevice = Number(left.dev);
119
- const rightDevice = Number(right.dev);
120
- const leftInode = Number(left.ino);
121
- const rightInode = Number(right.ino);
122
- if ([leftDevice, rightDevice, leftInode, rightInode].every((value) => Number.isSafeInteger(value) && value >= 0)) {
123
- return leftDevice === rightDevice && leftInode === rightInode;
124
- }
125
- return true;
126
- }
@@ -0,0 +1,35 @@
1
+ export function enqueueSecurityAudit(securityAudit, operation, outcome, failureReporter) {
2
+ if (!securityAudit?.record || operation.context.origin !== "relay") return;
3
+ let persistence;
4
+ try {
5
+ persistence = securityAudit.record({
6
+ ...outcome,
7
+ tool: operation.tool,
8
+ riskCategory: operation.context.operationAuthorization?.category || "ordinary operation",
9
+ targetHash: operation.context.operationAuthorization?.targetHash || "",
10
+ principal: operation.context.authority?.principal || {},
11
+ inputBytes: safeByteLength(operation.args),
12
+ });
13
+ } catch (error) {
14
+ operation.context.auditWarning = "security_audit_unavailable";
15
+ failureReporter.report("security.audit.enqueue.failed", { error_class: auditErrorClass(error) }, "Security audit event could not be queued");
16
+ return;
17
+ }
18
+ Promise.resolve(persistence).then((recorded) => {
19
+ if (recorded) return;
20
+ operation.context.auditWarning = "security_audit_unavailable";
21
+ failureReporter.report("security.audit.persist.failed", { tool: operation.tool }, "Security audit event could not be persisted");
22
+ }).catch((error) => {
23
+ operation.context.auditWarning = "security_audit_unavailable";
24
+ failureReporter.report("security.audit.persist.failed", {
25
+ error_class: auditErrorClass(error), tool: operation.tool,
26
+ }, "Security audit event could not be persisted");
27
+ });
28
+ }
29
+
30
+ function safeByteLength(value) {
31
+ try { return Buffer.byteLength(JSON.stringify(value)); } catch { return 0; }
32
+ }
33
+ function auditErrorClass(error) {
34
+ return String(error?.code || error?.name || "audit_error").replace(/[^A-Za-z0-9._-]/g, "_").slice(0, 80);
35
+ }
@@ -0,0 +1,215 @@
1
+ import { createHmac, randomBytes } from "node:crypto";
2
+ import { Worker } from "node:worker_threads";
3
+ import {
4
+ SECURITY_AUDIT_MAX_BYTES,
5
+ SECURITY_AUDIT_MAX_EVENTS,
6
+ auditFilePath,
7
+ unhealthyAuditSnapshot,
8
+ } from "./security-audit-storage.mjs";
9
+
10
+ const MAX_PENDING_RECORDS = 1024;
11
+ const CLOSE_TIMEOUT_MS = 5000;
12
+
13
+ export class SecurityAuditLog {
14
+ constructor({ root = "", now = Date.now, WorkerClass = Worker, identityKey = null } = {}) {
15
+ this.root = root ? String(root) : "";
16
+ this.file = this.root ? auditFilePath(this.root) : "";
17
+ this.now = typeof now === "function" ? now : Date.now;
18
+ this.identityKey = identityKey ? Buffer.from(identityKey) : randomBytes(32);
19
+ this.worker = null;
20
+ this.workerReady = false;
21
+ this.closed = false;
22
+ this.nextId = 1;
23
+ this.pending = new Map();
24
+ this.droppedRecords = 0;
25
+ this.cachedSnapshot = this.file ? initializingSnapshot() : disabledSnapshot();
26
+ if (!this.file) return;
27
+ try {
28
+ const worker = new WorkerClass(new URL("./security-audit-worker.mjs", import.meta.url), {
29
+ workerData: { root: this.root },
30
+ });
31
+ this.worker = worker;
32
+ worker.on("message", (message) => { if (this.worker === worker) this.handleMessage(message); });
33
+ worker.on("error", (error) => { if (this.worker === worker) this.handleWorkerFailure(error); });
34
+ worker.on("exit", (code) => {
35
+ if (this.worker === worker && !this.closed) {
36
+ this.handleWorkerFailure(Object.assign(new Error("security audit worker exited"), {
37
+ code: code === 0 ? "audit_worker_unexpected_exit" : "audit_worker_exit",
38
+ }));
39
+ }
40
+ });
41
+ } catch (error) {
42
+ this.cachedSnapshot = unhealthyAuditSnapshot(error);
43
+ }
44
+ }
45
+
46
+ record(input = {}) {
47
+ if (!this.file || this.closed || !this.worker) return Promise.resolve(false);
48
+ if (this.pending.size >= MAX_PENDING_RECORDS) {
49
+ this.droppedRecords += 1;
50
+ this.cachedSnapshot = { ...this.cachedSnapshot, healthy: false, last_error_class: "audit_queue_full" };
51
+ return Promise.resolve(false);
52
+ }
53
+ const id = this.nextId++;
54
+ return new Promise((resolvePromise) => {
55
+ this.pending.set(id, { resolve: resolvePromise, kind: "record" });
56
+ try {
57
+ this.worker.postMessage({ type: "record", id, input: projectAuditInput(input, this.identityKey), nowMs: Number(this.now()) });
58
+ } catch (error) {
59
+ this.pending.delete(id);
60
+ this.droppedRecords += 1;
61
+ this.cachedSnapshot = unhealthyAuditSnapshot(error);
62
+ resolvePromise(false);
63
+ }
64
+ });
65
+ }
66
+
67
+ flush() {
68
+ return this.barrier("flush");
69
+ }
70
+
71
+ async close({ timeoutMs = CLOSE_TIMEOUT_MS } = {}) {
72
+ if (this.closed) return true;
73
+ this.closed = true;
74
+ if (!this.worker) return false;
75
+ const worker = this.worker;
76
+ const closing = this.barrier("close", true);
77
+ let timer;
78
+ try {
79
+ return await Promise.race([
80
+ closing,
81
+ new Promise((resolvePromise) => {
82
+ timer = setTimeout(() => resolvePromise(false), normalizeCloseTimeoutMs(timeoutMs));
83
+ timer.unref?.();
84
+ }),
85
+ ]);
86
+ } finally {
87
+ clearTimeout(timer);
88
+ await worker.terminate().catch(() => { /* Audit shutdown already owns the terminal state; worker termination is best-effort cleanup. */ });
89
+ if (this.worker === worker) this.worker = null;
90
+ this.failPending(false);
91
+ }
92
+ }
93
+
94
+ snapshot() {
95
+ return {
96
+ ...this.cachedSnapshot,
97
+ persistence: this.file ? "worker-thread-batched-atomic" : "disabled",
98
+ worker_ready: this.workerReady,
99
+ queue_depth: [...this.pending.values()].filter((entry) => entry.kind === "record").length,
100
+ queue_capacity: MAX_PENDING_RECORDS,
101
+ dropped_records: this.droppedRecords,
102
+ };
103
+ }
104
+
105
+ barrier(type, allowClosed = false) {
106
+ if (!this.worker || (this.closed && !allowClosed)) return Promise.resolve(false);
107
+ const id = this.nextId++;
108
+ return new Promise((resolvePromise) => {
109
+ this.pending.set(id, { resolve: resolvePromise, kind: type });
110
+ try { this.worker.postMessage({ type, id }); }
111
+ catch {
112
+ this.pending.delete(id);
113
+ resolvePromise(false);
114
+ }
115
+ });
116
+ }
117
+
118
+ handleMessage(message) {
119
+ if (message?.snapshot) this.cachedSnapshot = message.snapshot;
120
+ if (message?.type === "ready") {
121
+ this.workerReady = message.snapshot?.healthy === true;
122
+ return;
123
+ }
124
+ if (message?.type === "record_batch_result" && Array.isArray(message.ids)) {
125
+ for (const rawId of message.ids) {
126
+ const id = Number(rawId);
127
+ const pending = this.pending.get(id);
128
+ if (!pending) continue;
129
+ this.pending.delete(id);
130
+ if (message.recorded !== true) this.droppedRecords += 1;
131
+ pending.resolve(message.recorded === true);
132
+ }
133
+ return;
134
+ }
135
+ const id = Number(message?.id);
136
+ const pending = this.pending.get(id);
137
+ if (!pending) return;
138
+ this.pending.delete(id);
139
+ if (message.type === "flushed" || message.type === "closed") pending.resolve(true);
140
+ }
141
+
142
+ handleWorkerFailure(error) {
143
+ this.workerReady = false;
144
+ this.cachedSnapshot = unhealthyAuditSnapshot(error);
145
+ this.droppedRecords += [...this.pending.values()].filter((entry) => entry.kind === "record").length;
146
+ const worker = this.worker;
147
+ this.worker = null;
148
+ this.failPending(false);
149
+ if (!this.closed) void worker?.terminate?.().catch?.(() => {
150
+ // The audit worker has already failed and pending records were settled false;
151
+ // termination is best-effort cleanup and cannot restore audit availability.
152
+ });
153
+ }
154
+
155
+ failPending(value) {
156
+ for (const pending of this.pending.values()) pending.resolve(value);
157
+ this.pending.clear();
158
+ }
159
+ }
160
+
161
+ function normalizeCloseTimeoutMs(value) {
162
+ let numeric;
163
+ try { numeric = Number(value); } catch { return CLOSE_TIMEOUT_MS; }
164
+ if (!Number.isFinite(numeric) || numeric <= 0) return CLOSE_TIMEOUT_MS;
165
+ return Math.min(CLOSE_TIMEOUT_MS, Math.max(1, numeric));
166
+ }
167
+
168
+ function initializingSnapshot() {
169
+ return {
170
+ enabled: true, healthy: false, retained: 0, maximum: SECURITY_AUDIT_MAX_EVENTS, maximum_bytes: SECURITY_AUDIT_MAX_BYTES,
171
+ last_event_at: null, last_error_class: "audit_initializing",
172
+ content_logged: false, chain_verified: false,
173
+ };
174
+ }
175
+
176
+ function disabledSnapshot() {
177
+ return {
178
+ enabled: false,
179
+ healthy: true,
180
+ retained: 0,
181
+ maximum: SECURITY_AUDIT_MAX_EVENTS,
182
+ maximum_bytes: SECURITY_AUDIT_MAX_BYTES,
183
+ last_event_at: null,
184
+ last_error_class: null,
185
+ content_logged: false,
186
+ chain_verified: true,
187
+ };
188
+ }
189
+
190
+ function projectAuditInput(input, identityKey) {
191
+ const principal = input?.principal && typeof input.principal === "object" ? input.principal : {};
192
+ return {
193
+ outcome: input?.outcome,
194
+ tool: input?.tool,
195
+ riskCategory: input?.riskCategory,
196
+ targetHash: input?.targetHash,
197
+ durationMs: input?.durationMs,
198
+ inputBytes: input?.inputBytes,
199
+ outputBytes: input?.outputBytes,
200
+ errorCode: input?.errorCode,
201
+ principal: {
202
+ kind: principal.kind,
203
+ accountId: runtimePrivateReference(identityKey, principal.accountId),
204
+ accountVersion: principal.accountVersion,
205
+ clientId: runtimePrivateReference(identityKey, principal.clientId),
206
+ familyId: runtimePrivateReference(identityKey, principal.familyId),
207
+ role: principal.role,
208
+ },
209
+ };
210
+ }
211
+
212
+ function runtimePrivateReference(key, value) {
213
+ if (!value) return null;
214
+ return createHmac("sha256", key).update(String(value)).digest("hex");
215
+ }
@@ -0,0 +1,139 @@
1
+ import { createHash, randomBytes } from "node:crypto";
2
+
3
+ const HASH_PATTERN = /^[a-f0-9]{64}$/;
4
+
5
+ export function decodeAndVerifyAuditState(buffer, schemaVersion, maximumEvents) {
6
+ let state;
7
+ try { state = JSON.parse(buffer.toString("utf8")); } catch (error) {
8
+ throw new Error("security audit state is not valid JSON", { cause: error });
9
+ }
10
+ validateState(state, schemaVersion, maximumEvents);
11
+ let previous = state.anchor;
12
+ for (const event of state.events) {
13
+ if (event.previous_hash !== previous || event.hash !== eventHash(event)) {
14
+ throw new Error("security audit hash chain verification failed");
15
+ }
16
+ previous = event.hash;
17
+ }
18
+ return state;
19
+ }
20
+
21
+ export function emptyAuditState(schemaVersion) {
22
+ return {
23
+ schemaVersion,
24
+ identity_salt: randomBytes(32).toString("hex"),
25
+ anchor: randomBytes(32).toString("hex"),
26
+ next_sequence: 1,
27
+ events: [],
28
+ };
29
+ }
30
+
31
+ export function copyAuditState(state) {
32
+ return { ...state, events: [...state.events] };
33
+ }
34
+
35
+ export function appendAuditRecords(state, records) {
36
+ for (const record of records) state.events.push(buildEvent(record?.input || {}, state, record?.nowMs));
37
+ }
38
+
39
+ export function boundedAuditStateContent(state, maximumEvents, maximumBytes) {
40
+ const events = state.events;
41
+ const eventBytes = events.map((event) => Buffer.byteLength(JSON.stringify(event)));
42
+ let start = Math.max(0, events.length - maximumEvents);
43
+ let retained = events.length - start;
44
+ let bytes = Buffer.byteLength(`${JSON.stringify({ ...state, events: [] })}\n`);
45
+ for (let index = start; index < events.length; index += 1) bytes += eventBytes[index];
46
+ if (retained > 1) bytes += retained - 1;
47
+
48
+ while (bytes > maximumBytes && start < events.length) {
49
+ bytes -= eventBytes[start];
50
+ if (retained > 1) bytes -= 1;
51
+ start += 1;
52
+ retained -= 1;
53
+ }
54
+ if (start > 0) state.anchor = events[start - 1].hash;
55
+ state.events = events.slice(start);
56
+
57
+ const content = `${JSON.stringify(state)}\n`;
58
+ if (Buffer.byteLength(content) > maximumBytes) throw new Error("security audit state exceeds its size limit");
59
+ return content;
60
+ }
61
+
62
+ function buildEvent(input, state, nowMs) {
63
+ const timestamp = new Date(Number(nowMs)).toISOString();
64
+ if (!Number.isFinite(Date.parse(timestamp))) throw new Error("security audit timestamp is invalid");
65
+ const principal = input.principal && typeof input.principal === "object" ? input.principal : {};
66
+ const event = {
67
+ sequence: state.next_sequence,
68
+ timestamp,
69
+ outcome: boundedToken(input.outcome, "unknown"),
70
+ tool: boundedToken(input.tool, "unknown"),
71
+ risk_category: boundedText(input.riskCategory, "ordinary operation", 160),
72
+ target_hash: HASH_PATTERN.test(String(input.targetHash || "")) ? String(input.targetHash) : null,
73
+ account_ref: principal.accountId ? privateReference(state.identity_salt, principal.accountId) : null,
74
+ client_ref: principal.clientId ? privateReference(state.identity_salt, principal.clientId) : null,
75
+ family_ref: principal.familyId ? privateReference(state.identity_salt, principal.familyId) : null,
76
+ account_version: Number.isSafeInteger(principal.accountVersion) ? principal.accountVersion : null,
77
+ role: boundedToken(principal.role, principal.kind === "local" ? "local" : "unknown"),
78
+ duration_ms: boundedNumber(input.durationMs),
79
+ input_bytes: boundedNumber(input.inputBytes),
80
+ output_bytes: boundedNumber(input.outputBytes),
81
+ error_code: input.errorCode ? boundedToken(input.errorCode, "unknown") : null,
82
+ previous_hash: state.events.at(-1)?.hash || state.anchor,
83
+ };
84
+ const completed = { ...event, hash: eventHash(event) };
85
+ state.next_sequence += 1;
86
+ return completed;
87
+ }
88
+
89
+ function validateState(state, schemaVersion, maximumEvents) {
90
+ if (!plainRecord(state) || state.schemaVersion !== schemaVersion) throw new Error("security audit state schema is invalid");
91
+ if (!HASH_PATTERN.test(state.identity_salt) || !HASH_PATTERN.test(state.anchor)) {
92
+ throw new Error("security audit state identity is invalid");
93
+ }
94
+ if (!Number.isSafeInteger(state.next_sequence) || state.next_sequence < 1) {
95
+ throw new Error("security audit sequence is invalid");
96
+ }
97
+ if (!Array.isArray(state.events) || state.events.length > maximumEvents || !state.events.every(validEvent)) {
98
+ throw new Error("security audit events are invalid");
99
+ }
100
+ if (state.events.length && state.next_sequence <= state.events.at(-1).sequence) {
101
+ throw new Error("security audit sequence did not advance");
102
+ }
103
+ }
104
+
105
+ function validEvent(event) {
106
+ return plainRecord(event)
107
+ && Number.isSafeInteger(event.sequence) && event.sequence > 0
108
+ && Number.isFinite(Date.parse(String(event.timestamp || "")))
109
+ && typeof event.outcome === "string" && typeof event.tool === "string" && typeof event.risk_category === "string"
110
+ && (event.target_hash === null || HASH_PATTERN.test(event.target_hash))
111
+ && ["account_ref", "client_ref", "family_ref"].every((key) => event[key] === null || HASH_PATTERN.test(event[key]))
112
+ && (event.account_version === null || Number.isSafeInteger(event.account_version))
113
+ && typeof event.role === "string"
114
+ && Number.isFinite(event.duration_ms) && Number.isFinite(event.input_bytes) && Number.isFinite(event.output_bytes)
115
+ && (event.error_code === null || typeof event.error_code === "string")
116
+ && HASH_PATTERN.test(event.previous_hash) && HASH_PATTERN.test(event.hash);
117
+ }
118
+
119
+ function eventHash(event) {
120
+ const value = { ...event };
121
+ delete value.hash;
122
+ return createHash("sha256").update(JSON.stringify(value)).digest("hex");
123
+ }
124
+
125
+ function privateReference(salt, value) {
126
+ return createHash("sha256").update(salt).update("\0").update(String(value)).digest("hex");
127
+ }
128
+
129
+ function boundedToken(value, fallback) {
130
+ return String(value || fallback).replace(/[^A-Za-z0-9._:-]/g, "_").slice(0, 128) || fallback;
131
+ }
132
+ function boundedText(value, fallback, maximum) {
133
+ return String(value || fallback).replace(/[\r\n\t\u0000-\u001f\u007f]/g, " ").trim().slice(0, maximum) || fallback;
134
+ }
135
+ function boundedNumber(value) {
136
+ const number = Number(value);
137
+ return Number.isFinite(number) && number >= 0 ? Math.min(Math.floor(number), Number.MAX_SAFE_INTEGER) : 0;
138
+ }
139
+ function plainRecord(value) { return Boolean(value) && typeof value === "object" && !Array.isArray(value); }
@@ -0,0 +1,135 @@
1
+ import { lstatSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { replaceFileAtomicallySync } from "./exclusive-file.mjs";
4
+ import { withOwnerStateLock } from "./owner-state-lock.mjs";
5
+ import { ensureOwnerOnlyDirectorySync, readBoundedRegularFileWithInfoSync } from "./secure-file.mjs";
6
+ import { exactFilesystemInteger, filesystemIdentity, filesystemTimeMs, sameFilesystemIdentity } from "./filesystem-identity.mjs";
7
+ import {
8
+ appendAuditRecords,
9
+ boundedAuditStateContent,
10
+ copyAuditState,
11
+ decodeAndVerifyAuditState,
12
+ emptyAuditState,
13
+ } from "./security-audit-state.mjs";
14
+
15
+ export const SECURITY_AUDIT_SCHEMA_VERSION = 1;
16
+ export const SECURITY_AUDIT_MAX_EVENTS = 4096;
17
+ export const SECURITY_AUDIT_MAX_BYTES = 4 * 1024 * 1024;
18
+
19
+ export function auditFilePath(root) {
20
+ return path.join(path.resolve(root), "security-audit.json");
21
+ }
22
+
23
+ export function readVerifiedAuditState(root) {
24
+ return readVerifiedAuditStateWithIdentity(root).state;
25
+ }
26
+
27
+ export function createAuditStorageSession(root) {
28
+ const directory = path.resolve(root);
29
+ const file = auditFilePath(directory);
30
+ let cachedState = null;
31
+ let cachedIdentity;
32
+
33
+ const loadVerifiedState = () => {
34
+ const observedIdentity = inspectAuditFile(file);
35
+ if (cachedState && sameFileIdentity(cachedIdentity, observedIdentity)) return cachedState;
36
+ const loaded = readVerifiedAuditStateWithIdentity(directory);
37
+ cachedState = loaded.state;
38
+ cachedIdentity = loaded.identity;
39
+ return cachedState;
40
+ };
41
+
42
+ return Object.freeze({
43
+ snapshot: () => auditSnapshotFromState(loadVerifiedState()),
44
+ async recordBatch(records) {
45
+ if (!Array.isArray(records) || records.length === 0) return auditSnapshotFromState(loadVerifiedState());
46
+ return withOwnerStateLock(directory, async () => {
47
+ const state = copyAuditState(loadVerifiedState());
48
+ appendAuditRecords(state, records);
49
+ const content = boundedAuditStateContent(state, SECURITY_AUDIT_MAX_EVENTS, SECURITY_AUDIT_MAX_BYTES);
50
+ cachedIdentity = writeState(file, content);
51
+ cachedState = state;
52
+ return auditSnapshotFromState(state);
53
+ }, {
54
+ purpose: "security-audit", fileName: "security-audit.lock", label: "security audit",
55
+ });
56
+ },
57
+ });
58
+ }
59
+
60
+ export async function recordAuditBatch(root, records) {
61
+ return createAuditStorageSession(root).recordBatch(records);
62
+ }
63
+
64
+ export function auditSnapshotFromState(state) {
65
+ return {
66
+ enabled: true, healthy: true, retained: state.events.length,
67
+ maximum: SECURITY_AUDIT_MAX_EVENTS, maximum_bytes: SECURITY_AUDIT_MAX_BYTES,
68
+ last_event_at: state.events.at(-1)?.timestamp || null,
69
+ last_error_class: null, content_logged: false, chain_verified: true,
70
+ };
71
+ }
72
+
73
+ export function unhealthyAuditSnapshot(error) {
74
+ return {
75
+ enabled: true, healthy: false, retained: 0,
76
+ maximum: SECURITY_AUDIT_MAX_EVENTS, maximum_bytes: SECURITY_AUDIT_MAX_BYTES,
77
+ last_event_at: null, last_error_class: auditErrorClass(error),
78
+ content_logged: false, chain_verified: false,
79
+ };
80
+ }
81
+
82
+ export function auditErrorClass(error) {
83
+ return String(error?.code || error?.name || "audit_error").replace(/[^A-Za-z0-9._-]/g, "_").slice(0, 80);
84
+ }
85
+
86
+ function readVerifiedAuditStateWithIdentity(root) {
87
+ const file = auditFilePath(root);
88
+ let loaded;
89
+ try {
90
+ loaded = readBoundedRegularFileWithInfoSync(file, SECURITY_AUDIT_MAX_BYTES, "security audit state", {
91
+ verifyPathIdentity: true,
92
+ rejectMultipleLinks: true,
93
+ });
94
+ } catch (error) {
95
+ if (error?.code === "ENOENT") return { state: emptyAuditState(SECURITY_AUDIT_SCHEMA_VERSION), identity: null };
96
+ throw error;
97
+ }
98
+ return {
99
+ state: decodeAndVerifyAuditState(loaded.buffer, SECURITY_AUDIT_SCHEMA_VERSION, SECURITY_AUDIT_MAX_EVENTS),
100
+ identity: fileIdentity(loaded.info, loaded.identity),
101
+ };
102
+ }
103
+
104
+ function writeState(file, content) {
105
+ ensureOwnerOnlyDirectorySync(path.dirname(file));
106
+ replaceFileAtomicallySync(file, content, { mode: 0o600 });
107
+ return inspectAuditFile(file);
108
+ }
109
+
110
+ function inspectAuditFile(file) {
111
+ let info;
112
+ try { info = lstatSync(file, { bigint: true }); } catch (error) {
113
+ if (error?.code === "ENOENT") return null;
114
+ throw error;
115
+ }
116
+ if (info.isSymbolicLink() || !info.isFile()) throw new Error("security audit state must be a regular file and not a symbolic link");
117
+ if (Number(info.nlink) > 1) throw new Error("security audit state must not have multiple hard links");
118
+ return fileIdentity(info);
119
+ }
120
+
121
+ function fileIdentity(info, exactIdentity = filesystemIdentity(info, "security audit state")) {
122
+ return {
123
+ ...exactIdentity,
124
+ size: exactFilesystemInteger(info.size, "security audit state size"),
125
+ mtime_ms: filesystemTimeMs(info.mtimeMs, "security audit modification time"),
126
+ ctime_ms: filesystemTimeMs(info.ctimeMs, "security audit change time"),
127
+ };
128
+ }
129
+
130
+ function sameFileIdentity(left, right) {
131
+ if (left === null || right === null) return left === right;
132
+ return Boolean(left && right)
133
+ && sameFilesystemIdentity(left, right) && left.size === right.size
134
+ && left.mtime_ms === right.mtime_ms && left.ctime_ms === right.ctime_ms;
135
+ }