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
@@ -3,17 +3,39 @@ import { lstatSync, rmSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
  import { createExclusiveFileSync, replaceFileAtomicallySync } from "./exclusive-file.mjs";
5
5
  import { currentProcessStartTimeMs, inspectProcessInstance, processStartTimeMs } from "./process-identity.mjs";
6
- import { readBoundedRegularFileSync } from "./secure-file.mjs";
7
- import { ownerOnlyFile } from "./state.mjs";
6
+ import { ownerOnlyFile, readBoundedRegularFileWithInfoSync, retryTransientMultipleLinksSync } from "./secure-file.mjs";
7
+ import { exactFilesystemInteger, filesystemIdentity, filesystemTimeMs, sameFilesystemIdentity } from "./filesystem-identity.mjs";
8
8
 
9
9
  export function acquireRecoveryLock(dir) {
10
10
  return acquirePidLock(join(dir, "recovery.lock"), { allowHandoff: true });
11
11
  }
12
12
 
13
+ export function acquireJobCapacityLock(jobRoot) {
14
+ return acquirePidLock(join(jobRoot, "capacity.lock"));
15
+ }
16
+
13
17
  export function acquireJobTransitionLock(dir) {
14
18
  return acquirePidLock(join(dir, "transition.lock"));
15
19
  }
16
20
 
21
+ export function activeManagedJobLock(file) {
22
+ let snapshot;
23
+ try { snapshot = readPidLockSnapshot(file); } catch {
24
+ return { active: true, pid: null, reason: "invalid_or_unreadable_lock" };
25
+ }
26
+ if (!snapshot) return null;
27
+ const age = Date.now() - snapshot.info.mtimeMs;
28
+ if (!snapshot.owner || !Number.isInteger(snapshot.owner.pid) || snapshot.owner.pid <= 0
29
+ || !Number.isFinite(Date.parse(String(snapshot.owner.startedAt || "")))) {
30
+ return age < 60_000 ? { active: true, pid: null, reason: "recent_malformed_lock" } : null;
31
+ }
32
+ const identity = inspectProcessInstance(snapshot.owner, { maxAgeMs: 5 * 60_000 });
33
+ if (identity.current || (identity.alive && !identity.reclaimable)) {
34
+ return { active: true, pid: snapshot.owner.pid, reason: identity.reason };
35
+ }
36
+ return null;
37
+ }
38
+
17
39
  function acquirePidLock(file, { allowHandoff = false } = {}) {
18
40
  for (let attempt = 0; attempt < 3; attempt += 1) {
19
41
  const owner = pidLockOwner(process.pid, currentProcessStartTimeMs());
@@ -54,17 +76,22 @@ function pidLockOwner(pid, startedAtMs) {
54
76
  }
55
77
 
56
78
  function readPidLockSnapshot(file) {
57
- let info;
58
- try { info = lstatSync(file); } catch (error) { if (error?.code === "ENOENT") return null; throw error; }
59
- if (info.isSymbolicLink() || !info.isFile()) throw new Error("job lock must be a regular non-symbolic-link file");
60
- let owner = null;
79
+ let opened;
61
80
  try {
62
- const parsed = JSON.parse(readBoundedRegularFileSync(file, 1024).toString("utf8").trim());
63
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) owner = parsed;
81
+ opened = retryTransientMultipleLinksSync(() => readBoundedRegularFileWithInfoSync(file, 1024, "job lock", {
82
+ verifyPathIdentity: true,
83
+ rejectMultipleLinks: true,
84
+ }));
64
85
  } catch (error) {
65
- if (error?.code === "ENOENT") return null;
86
+ if (error?.code === "ENOENT" || error?.cause?.code === "ENOENT") return null;
87
+ throw error;
66
88
  }
67
- return { owner, info: pidLockIdentity(info) };
89
+ let owner = null;
90
+ try {
91
+ const parsed = JSON.parse(opened.buffer.toString("utf8").trim());
92
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) owner = parsed;
93
+ } catch { /* Successfully read malformed JSON may be reclaimed only after the stale grace. */ }
94
+ return { owner, info: pidLockIdentity(opened.identityInfo, opened.identity) };
68
95
  }
69
96
 
70
97
  function removePidLockOwnedBy(file, token) {
@@ -75,7 +102,7 @@ function removePidLockOwnedBy(file, token) {
75
102
 
76
103
  function removePidLockSnapshot(file, snapshot) {
77
104
  let current;
78
- try { current = lstatSync(file); } catch (error) { return error?.code === "ENOENT"; }
105
+ try { current = lstatSync(file, { bigint: true }); } catch (error) { return error?.code === "ENOENT"; }
79
106
  if (current.isSymbolicLink() || !current.isFile()) return false;
80
107
  if (!samePidLockIdentity(snapshot.info, pidLockIdentity(current))) return false;
81
108
  if (snapshot.owner?.token) {
@@ -85,12 +112,17 @@ function removePidLockSnapshot(file, snapshot) {
85
112
  try { rmSync(file); return true; } catch (error) { return error?.code === "ENOENT"; }
86
113
  }
87
114
 
88
- function pidLockIdentity(info) {
89
- return { dev: Number(info.dev), ino: Number(info.ino), size: Number(info.size), mtimeMs: Number(info.mtimeMs) };
115
+ function pidLockIdentity(info, identity = filesystemIdentity(info, "managed-job lock")) {
116
+ return {
117
+ ...identity,
118
+ size: exactFilesystemInteger(info.size, "managed-job lock size"),
119
+ nlink: exactFilesystemInteger(info.nlink, "managed-job lock link count"),
120
+ mtimeMs: filesystemTimeMs(info.mtimeMs, "managed-job lock modification time"),
121
+ };
90
122
  }
91
123
 
92
124
  function samePidLockIdentity(left, right) {
93
- return left.dev === right.dev && left.ino === right.ino && left.size === right.size && left.mtimeMs === right.mtimeMs;
125
+ return sameFilesystemIdentity(left, right) && left.size === right.size && left.nlink === right.nlink && left.mtimeMs === right.mtimeMs;
94
126
  }
95
127
 
96
128
  function replacePrivateTextFile(file, content) {
@@ -2,7 +2,6 @@ import { createHash } from "node:crypto";
2
2
  import { lstatSync, realpathSync, statSync } from "node:fs";
3
3
  import { basename, isAbsolute, relative, resolve, sep } from "node:path";
4
4
  import { readBoundedRegularFileWithInfoSync } from "./secure-file.mjs";
5
- import { clampInteger } from "./numbers.mjs";
6
5
 
7
6
  const RESOURCE_NAME = /^[a-z][a-z0-9._-]{0,63}$/;
8
7
  const RESOURCE_TOKEN = /\{\{resource:([a-z][a-z0-9._-]{0,63})\}\}/g;
@@ -17,10 +16,10 @@ export function validateResourceName(value) {
17
16
  return name;
18
17
  }
19
18
 
20
- export function inspectResourceFile(inputPath, { allowInsecurePermissions = false, includeHash = false } = {}) {
19
+ export function inspectResourceFile(inputPath, { allowInsecurePermissions = false, includeHash = false, includeContent = false } = {}) {
21
20
  const path = resolve(String(inputPath || ""));
22
21
  const canonical = realpathFile(path);
23
- const { buffer: content, info } = readBoundedRegularFileWithInfoSync(canonical, MAX_RESOURCE_BYTES);
22
+ const { buffer: content, info } = readBoundedRegularFileWithInfoSync(canonical, MAX_RESOURCE_BYTES, "resource file", { verifyPathIdentity: true });
24
23
  if (process.platform !== "win32" && !allowInsecurePermissions && (info.mode & 0o077) !== 0) {
25
24
  throw new Error("resource file is readable by group or others; restrict permissions or use --allow-insecure-permissions");
26
25
  }
@@ -33,6 +32,7 @@ export function inspectResourceFile(inputPath, { allowInsecurePermissions = fals
33
32
  updatedAt: new Date().toISOString(),
34
33
  allowInsecurePermissions: allowInsecurePermissions === true,
35
34
  ...(includeHash ? { sha256: createHash("sha256").update(content).digest("hex") } : {}),
35
+ ...(includeContent ? { content } : {}),
36
36
  };
37
37
  }
38
38
 
@@ -53,10 +53,10 @@ export function validatePlan(args, context) {
53
53
  if (!args || typeof args !== "object" || Array.isArray(args)) throw new Error("job arguments must be an object");
54
54
  const allowed = new Set(["name", "steps", "finally_steps", "temporary_files"]);
55
55
  for (const key of Object.keys(args)) if (!allowed.has(key)) throw new Error(`job contains unknown field: ${key}`);
56
- const name = String(args.name || "managed job").trim().slice(0, 128) || "managed job";
56
+ const name = args.name === undefined ? "managed job" : boundedString(args.name, 128, "name").trim() || "managed job";
57
57
  const steps = validateSteps(args.steps, "steps", context);
58
- const finallySteps = validateSteps(args.finally_steps || [], "finally_steps", context, true);
59
- const temporaryFiles = validateTemporaryFiles(args.temporary_files || []);
58
+ const finallySteps = validateSteps(args.finally_steps === undefined ? [] : args.finally_steps, "finally_steps", context, true);
59
+ const temporaryFiles = validateTemporaryFiles(args.temporary_files === undefined ? [] : args.temporary_files);
60
60
  if (!steps.length) throw new Error("steps must contain at least one step");
61
61
  return {
62
62
  version: 1,
@@ -83,7 +83,7 @@ function validateTemporaryFiles(value) {
83
83
  const content = boundedString(item.content, 256 * 1024, `temporary_files[${index}].content`);
84
84
  totalBytes += Buffer.byteLength(content);
85
85
  if (totalBytes > MAX_TEMPORARY_FILE_BYTES) throw new Error(`temporary file contents exceed ${MAX_TEMPORARY_FILE_BYTES} bytes`);
86
- return { name, content, executable: item.executable === true };
86
+ return { name, content, executable: optionalBoolean(item.executable, `temporary_files[${index}].executable`) };
87
87
  });
88
88
  }
89
89
 
@@ -113,19 +113,37 @@ function validateSteps(value, label, context, allowEmpty = false) {
113
113
  env_resources: envResources,
114
114
  stdin,
115
115
  stdin_resource: stdinResource,
116
- timeout_seconds: clampInteger(input.timeout_seconds, 600, 1, 3600),
117
- allow_failure: input.allow_failure === true,
118
- capture_output: input.capture_output === "discard" ? "discard" : "redacted",
116
+ timeout_seconds: optionalInteger(input.timeout_seconds, 600, 1, 3600, `${label}[${index}].timeout_seconds`),
117
+ allow_failure: optionalBoolean(input.allow_failure, `${label}[${index}].allow_failure`),
118
+ capture_output: validateCaptureOutput(input.capture_output, `${label}[${index}].capture_output`),
119
119
  };
120
120
  });
121
121
  }
122
122
 
123
+ function optionalBoolean(value, label) {
124
+ if (value === undefined) return false;
125
+ if (typeof value !== "boolean") throw new Error(`${label} must be a boolean`);
126
+ return value;
127
+ }
128
+
129
+ function optionalInteger(value, defaultValue, min, max, label) {
130
+ if (value === undefined) return defaultValue;
131
+ if (!Number.isInteger(value) || value < min || value > max) throw new Error(`${label} must be an integer between ${min} and ${max}`);
132
+ return value;
133
+ }
134
+
135
+ function validateCaptureOutput(value, label) {
136
+ if (value === undefined || value === "redacted") return "redacted";
137
+ if (value === "discard") return "discard";
138
+ throw new Error(`${label} must be redacted or discard`);
139
+ }
140
+
123
141
  function validateEnv(value, label) {
124
142
  if (value === undefined) return {};
125
143
  if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${label} must be an object`);
126
144
  const entries = Object.entries(value);
127
145
  if (entries.length > 64) throw new Error(`${label} has too many entries`);
128
- const out = {};
146
+ const out = Object.create(null);
129
147
  for (const [key, raw] of entries) {
130
148
  if (!/^[A-Za-z_][A-Za-z0-9_]{0,127}$/.test(key)) throw new Error(`${label} contains invalid variable name: ${key}`);
131
149
  out[key] = boundedString(raw, 16 * 1024, `${label}.${key}`);
@@ -138,7 +156,7 @@ function validateEnvResources(value, label) {
138
156
  if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${label} must be an object`);
139
157
  const entries = Object.entries(value);
140
158
  if (entries.length > 32) throw new Error(`${label} has too many entries`);
141
- const out = {};
159
+ const out = Object.create(null);
142
160
  for (const [key, raw] of entries) {
143
161
  if (!/^[A-Za-z_][A-Za-z0-9_]{0,127}$/.test(key)) throw new Error(`${label} contains invalid variable name: ${key}`);
144
162
  out[key] = validateResourceName(raw);
@@ -175,8 +193,14 @@ function referencedResources(steps, registry) {
175
193
 
176
194
  export function normalizeResourceRegistry(resources) {
177
195
  const out = Object.create(null);
178
- if (!resources || typeof resources !== "object" || Array.isArray(resources)) return out;
179
- for (const [rawName, rawValue] of Object.entries(resources).slice(0, MAX_RESOURCES)) {
196
+ if (resources === undefined || resources === null) return out;
197
+ if (typeof resources !== "object" || Array.isArray(resources)) throw new Error("local resource registry must be an object");
198
+ let count = 0;
199
+ for (const rawName in resources) {
200
+ if (!Object.hasOwn(resources, rawName)) continue;
201
+ count += 1;
202
+ if (count > MAX_RESOURCES) throw new Error(`local resource registry limit exceeded (${MAX_RESOURCES})`);
203
+ const rawValue = resources[rawName];
180
204
  const name = validateResourceName(rawName);
181
205
  if (!rawValue || rawValue.kind !== "file" || typeof rawValue.path !== "string") continue;
182
206
  out[name] = {
@@ -31,23 +31,27 @@ export function reviewablePlan(plan) {
31
31
  };
32
32
  }
33
33
 
34
+ /**
35
+ * Project owner-only managed-job timing out of delegated result reads.
36
+ * @param {Record<string, unknown> | null | undefined} result
37
+ * @param {{includeResourceAdmissionTiming?: boolean}} [options]
38
+ */
39
+ export function projectManagedJobResult(result, { includeResourceAdmissionTiming = false } = {}) {
40
+ if (!result || includeResourceAdmissionTiming) return result;
41
+ const projectSteps = (/** @type {unknown} */ steps) => Array.isArray(steps) ? steps.map((/** @type {unknown} */ step) => {
42
+ if (!step || typeof step !== "object" || Array.isArray(step)) return step;
43
+ const { resource_admission_ms: _resourceAdmissionMs, ...projected } = /** @type {Record<string, unknown>} */ (step);
44
+ return projected;
45
+ }) : steps;
46
+ return { ...result, steps: projectSteps(result.steps), finally_steps: projectSteps(result.finally_steps) };
47
+ }
48
+
34
49
  /**
35
50
  * Return the stable public status shape without runner identity or internal paths.
36
- * @param {{
37
- * job_id?: unknown,
38
- * name?: unknown,
39
- * status?: unknown,
40
- * created_at?: unknown,
41
- * started_at?: unknown,
42
- * finished_at?: unknown,
43
- * current_phase?: unknown,
44
- * current_step?: unknown,
45
- * approval?: unknown,
46
- * plan_sha256?: unknown,
47
- * cleanup_guarantee?: unknown,
48
- * error_class?: unknown,
49
- * recovery_attempts?: unknown
50
- * }} status
51
+ * @param {{job_id?: unknown, name?: unknown, status?: unknown, created_at?: unknown, started_at?: unknown,
52
+ * finished_at?: unknown, current_phase?: unknown, current_step?: unknown, approval?: unknown, plan_sha256?: unknown,
53
+ * cleanup_guarantee?: unknown, error_class?: unknown, recovery_attempts?: unknown, result_persisted?: unknown,
54
+ * terminal_record_error_class?: unknown, artifact_cleanup_pending?: unknown, artifact_cleanup_error_class?: unknown}} status
51
55
  */
52
56
  export function publicStatus(status) {
53
57
  return {
@@ -64,5 +68,9 @@ export function publicStatus(status) {
64
68
  cleanup_guarantee: status.cleanup_guarantee ?? "best-effort-finally-and-recovery",
65
69
  error_class: status.error_class ?? null,
66
70
  recovery_attempts: Number(status.recovery_attempts || 0),
71
+ result_persisted: typeof status.result_persisted === "boolean" ? status.result_persisted : null,
72
+ terminal_record_error_class: status.terminal_record_error_class ?? null,
73
+ artifact_cleanup_pending: status.artifact_cleanup_pending === true,
74
+ artifact_cleanup_error_class: status.artifact_cleanup_error_class ?? null,
67
75
  };
68
76
  }
@@ -0,0 +1,128 @@
1
+ import { rmSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { MANAGED_JOB_ID } from "./managed-job-directory.mjs";
4
+ import { inspectManagedJobDirectoryGeneration, pruneRetiredManagedJobDirectories, removeManagedJobDirectoryIfCurrent } from "./managed-job-directory-generation.mjs";
5
+ import { managedJobCapacitySnapshot, MAX_JOBS } from "./managed-job-capacity.mjs";
6
+ import { runnerProcessIsCurrent } from "./managed-job-runner.mjs";
7
+ import { acquireJobTransitionLock } from "./managed-job-lock.mjs";
8
+ import { atomicWriteJson, readJson, resourceErrorClass, safeReadDir } from "./managed-job-storage.mjs";
9
+ import { ACTIVE_JOB_STATES, isTerminalManagedJobStatus, persistManagedJobTerminal } from "./managed-job-terminal.mjs";
10
+ import { scrubTerminalJobArtifacts } from "./managed-job-terminal-maintenance.mjs";
11
+ const JOB_RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
12
+ const STAGED_PLAN_RETENTION_MS = 24 * 60 * 60 * 1000;
13
+ export const PLAN_RETAINING_STATES = new Set(["staged", ...ACTIVE_JOB_STATES]);
14
+ export function pruneManagedJobs({ jobRoot, logger = console, reserveSlots = 0 }) {
15
+ const reserved = Math.max(0, Math.min(MAX_JOBS, Math.floor(Number(reserveSlots) || 0)));
16
+ const targetMaximum = MAX_JOBS - reserved;
17
+ pruneRetiredManagedJobDirectories(jobRoot, logger);
18
+ const entries = [];
19
+ for (const entry of safeReadDir(jobRoot)) {
20
+ if (!MANAGED_JOB_ID.test(entry.name)) continue;
21
+ if (!entry.isDirectory()) { logger.warn?.("managed job pruning retained wrong-type job state", { error_class: "integrity_error" }); continue; }
22
+ const dir = join(jobRoot, entry.name);
23
+ let status; let mtime;
24
+ let generation;
25
+ try {
26
+ status = readJson(join(dir, "status.json"), 256 * 1024, "job status");
27
+ const observed = inspectManagedJobDirectoryGeneration(dir);
28
+ mtime = observed.mtimeMs;
29
+ generation = observed.identity;
30
+ } catch (error) {
31
+ logger.warn?.("managed job pruning skipped unreadable state; retaining it for inspection", { error_class: resourceErrorClass(error) });
32
+ continue;
33
+ }
34
+ if (!status) {
35
+ const fallbackOwner = { startedAt: new Date(mtime).toISOString() };
36
+ let runnerAlive;
37
+ try { runnerAlive = runnerProcessIsCurrent(fallbackOwner, dir, { ownerOnly: true }); }
38
+ catch (error) {
39
+ logger.warn?.("managed job pruning skipped unreadable runner ownership; retaining it for inspection", { error_class: resourceErrorClass(error) });
40
+ entries.push({ dir, status, mtime });
41
+ continue;
42
+ }
43
+ if (!runnerAlive && Date.now() - mtime > 60_000) {
44
+ removeManagedJobDirectoryIfCurrent(dir, generation);
45
+ continue;
46
+ }
47
+ }
48
+ if (status && status.job_id !== entry.name) {
49
+ logger.warn?.("managed job pruning skipped mismatched directory identity; retaining it for inspection", { error_class: "integrity_error" });
50
+ entries.push({ dir, status: null, mtime });
51
+ continue;
52
+ }
53
+ if (status?.status === "staged" && stagedPlanExpired(status, mtime)) {
54
+ const transition = acquireJobTransitionLock(dir);
55
+ if (!transition) {
56
+ entries.push({ dir, status, mtime });
57
+ continue;
58
+ }
59
+ try {
60
+ try { status = readJson(join(dir, "status.json"), 256 * 1024, "job status"); }
61
+ catch (error) {
62
+ logger.warn?.("managed job pruning skipped unreadable staged state; retaining it for inspection", { error_class: resourceErrorClass(error) });
63
+ entries.push({ dir, status, mtime });
64
+ continue;
65
+ }
66
+ if (status?.status === "staged" && stagedPlanExpired(status, mtime)) status = expireStagedJob(dir, status);
67
+ } finally {
68
+ transition.release();
69
+ }
70
+ }
71
+ if (status && !PLAN_RETAINING_STATES.has(status.status) && !isTerminalManagedJobStatus(status.status)) {
72
+ logger.warn?.("managed job pruning skipped invalid status; retaining it for inspection", { error_class: "integrity_error" }); entries.push({ dir, status, mtime }); continue;
73
+ }
74
+ if (status && isTerminalManagedJobStatus(status.status)) {
75
+ try {
76
+ scrubTerminalJobArtifacts(dir, status);
77
+ generation = inspectManagedJobDirectoryGeneration(dir).identity;
78
+ } catch (error) {
79
+ logger.warn?.("managed job pruning retained inconsistent terminal state", { error_class: error?.code === "integrity_error" ? "integrity_error" : resourceErrorClass(error) });
80
+ entries.push({ dir, status: null, mtime }); continue;
81
+ }
82
+ }
83
+ entries.push({ dir, status, mtime, generation });
84
+ }
85
+ const finished = entries
86
+ .filter(({ status }) => status && isTerminalManagedJobStatus(status.status))
87
+ .sort((a, b) => terminalRetentionTime(b.status, b.mtime) - terminalRetentionTime(a.status, a.mtime));
88
+ const now = Date.now();
89
+ for (const item of finished) {
90
+ if (now - terminalRetentionTime(item.status, item.mtime) > JOB_RETENTION_MS) {
91
+ removeManagedJobDirectoryIfCurrent(item.dir, item.generation);
92
+ }
93
+ }
94
+ const retainedCount = () => managedJobCapacitySnapshot(jobRoot).retained_state;
95
+ if (retainedCount() <= targetMaximum) return;
96
+ const removable = entries
97
+ .filter(({ status }) => status && isTerminalManagedJobStatus(status.status))
98
+ .sort((a, b) => terminalRetentionTime(a.status, a.mtime) - terminalRetentionTime(b.status, b.mtime));
99
+ for (const item of removable) {
100
+ if (retainedCount() <= targetMaximum) break;
101
+ removeManagedJobDirectoryIfCurrent(item.dir, item.generation);
102
+ }
103
+ }
104
+ function stagedPlanExpired(status, fallbackMtime) {
105
+ const createdAt = Date.parse(String(status?.created_at || ""));
106
+ const baseline = Number.isFinite(createdAt) ? createdAt : fallbackMtime;
107
+ return Number.isFinite(baseline) && Date.now() - baseline > STAGED_PLAN_RETENTION_MS;
108
+ }
109
+ function terminalRetentionTime(status, fallbackMtime) {
110
+ const finishedAt = Date.parse(String(status?.finished_at || ""));
111
+ return Number.isFinite(finishedAt) ? finishedAt : fallbackMtime;
112
+ }
113
+ function expireStagedJob(dir, status) {
114
+ const finishedAt = new Date().toISOString();
115
+ const result = {
116
+ job_id: status.job_id, name: status.name, status: "expired_before_start",
117
+ steps: [], finally_steps: [], error_class: "expired", cleanup_error_class: null, finished_at: finishedAt,
118
+ };
119
+ const terminal = persistManagedJobTerminal({
120
+ statusFile: join(dir, "status.json"), resultFile: join(dir, "result.json"),
121
+ artifacts: [join(dir, "runtime"), join(dir, "plan.json"), join(dir, "runner.pid"), join(dir, "cancel")],
122
+ status: { ...status, cleanup_guarantee: "not-started" }, result,
123
+ writeJson: atomicWriteJson, removeFile: (file) => rmSync(file, { recursive: true, force: true }),
124
+ maxStatusBytes: 256 * 1024, maxResultBytes: 4 * 1024 * 1024, classifyPersistenceError: resourceErrorClass,
125
+ });
126
+ if (!terminal.statusPersisted) throw new Error(`managed job staged expiry status persistence failed: ${terminal.statusErrorClass}`);
127
+ return terminal.status;
128
+ }
@@ -0,0 +1,70 @@
1
+ import { join } from "node:path";
2
+ import { createMonotonicDeadline } from "./monotonic-deadline.mjs";
3
+ import { createExclusiveFileSync, replaceFileAtomicallySync } from "./exclusive-file.mjs";
4
+ import { inspectPathIfPresentSync, ownerOnlyFile, readBoundedRegularFileSync, retryTransientMultipleLinksSync } from "./secure-file.mjs";
5
+
6
+ const RUNNER_CLAIM_BYTES = 1024;
7
+ const RUNNER_CLAIM_WAIT_MS = 30_000;
8
+
9
+ export function publishProvisionalRunnerClaim(dir, pid, launchToken) {
10
+ if (!/^[a-f0-9]{32}$/.test(String(launchToken || ""))) throw new Error("runner launch token is invalid");
11
+ const file = join(dir, "runner.pid");
12
+ const claim = { pid, startedAt: new Date().toISOString(), launchToken, committed: false };
13
+ try {
14
+ createExclusiveFileSync(file, `${JSON.stringify(claim)}\n`, { mode: 0o600 });
15
+ } catch (error) {
16
+ if (error?.code !== "EEXIST") throw error;
17
+ const existing = readRunnerClaim(file, "managed job runner claim already exists but is unreadable");
18
+ if (Number(existing?.pid) !== pid || existing?.launchToken !== launchToken) {
19
+ throw new Error("managed job runner claim is owned by another process or launch");
20
+ }
21
+ if (existing.committed === true) return;
22
+ if (existing.committed !== false) throw new Error("managed job runner claim has an invalid provisional state");
23
+ claim.startedAt = typeof existing.startedAt === "string" && existing.startedAt ? existing.startedAt : claim.startedAt;
24
+ }
25
+ ownerOnlyFile(file);
26
+ replaceFileAtomicallySync(file, `${JSON.stringify({ ...claim, committed: true })}\n`, { mode: 0o600 });
27
+ }
28
+
29
+ export async function confirmRunnerClaim({
30
+ file, pid, processStartedAt, launchToken, inspectPath = inspectPathIfPresentSync,
31
+ waitMs = RUNNER_CLAIM_WAIT_MS,
32
+ }) {
33
+ const exact = { pid, startedAt: new Date().toISOString(), processStartedAt };
34
+ if (!launchToken) {
35
+ createExclusiveFileSync(file, `${JSON.stringify(exact)}\n`, { mode: 0o600 });
36
+ return;
37
+ }
38
+ if (!/^[a-f0-9]{32}$/.test(launchToken)) throw new Error("runner launch token is invalid");
39
+ const deadline = createMonotonicDeadline(waitMs);
40
+ while (!deadline.expired()) {
41
+ if (!inspectPath(file, "managed job runner claim")) {
42
+ await new Promise((resolvePromise) => { setTimeout(resolvePromise, 10); });
43
+ continue;
44
+ }
45
+ const provisional = readRunnerClaim(file, "runner ownership claim is unreadable");
46
+ if (Number(provisional?.pid) !== pid || provisional?.launchToken !== launchToken) {
47
+ throw new Error("runner ownership claim does not match the spawned process");
48
+ }
49
+ if (provisional.committed !== true) {
50
+ await new Promise((resolvePromise) => { setTimeout(resolvePromise, Math.min(10, Math.max(1, deadline.remainingMs()))); });
51
+ continue;
52
+ }
53
+ exact.startedAt = typeof provisional.startedAt === "string" && provisional.startedAt
54
+ ? provisional.startedAt
55
+ : exact.startedAt;
56
+ replaceFileAtomicallySync(file, `${JSON.stringify(exact)}\n`, { mode: 0o600 });
57
+ return;
58
+ }
59
+ throw new Error("runner ownership claim was not published before startup deadline");
60
+ }
61
+
62
+ function readRunnerClaim(file, message) {
63
+ try {
64
+ const bytes = retryTransientMultipleLinksSync(() => readBoundedRegularFileSync(file, RUNNER_CLAIM_BYTES, "managed job runner claim", {
65
+ verifyPathIdentity: true,
66
+ rejectMultipleLinks: true,
67
+ }));
68
+ return JSON.parse(bytes.toString("utf8"));
69
+ } catch (error) { throw new Error(message, { cause: error }); }
70
+ }
@@ -1,15 +1,19 @@
1
1
  import { spawn } from "node:child_process";
2
+ import { randomBytes } from "node:crypto";
2
3
  import { closeSync } from "node:fs";
3
- import { basename, join } from "node:path";
4
+ import { join } from "node:path";
4
5
  import { fileURLToPath } from "node:url";
5
6
  import { inspectProcessInstance } from "./process-identity.mjs";
6
7
  import { classifyOperationalError } from "./log.mjs";
7
- import { ownerOnlyFile } from "./state.mjs";
8
+ import { ownerOnlyFile } from "./secure-file.mjs";
8
9
  import { openPrivateAppendFile, readBoundedFile, trimDiagnosticFile } from "./managed-job-storage.mjs";
10
+ import { publishProvisionalRunnerClaim } from "./managed-job-runner-claim.mjs";
11
+ import { EXECUTION_SURFACE, withExecutionSurface } from "./execution-surface.mjs";
9
12
 
10
13
  const RUNNER_PATH = fileURLToPath(new URL("./job-runner.mjs", import.meta.url));
11
14
 
12
15
  export function launchRunner(dir, recover = false, recoveryToken = "", options = {}) {
16
+ const launchToken = randomBytes(16).toString("hex");
13
17
  const args = [RUNNER_PATH, "--job-dir", dir];
14
18
  if (recover) args.push("--recover");
15
19
  const stdoutFile = join(dir, "runner.out.log");
@@ -28,7 +32,7 @@ export function launchRunner(dir, recover = false, recoveryToken = "", options =
28
32
  stdio: ["ignore", stdoutFd, stderrFd],
29
33
  windowsHide: true,
30
34
  shell: false,
31
- env: recoveryToken ? { ...process.env, MBM_RECOVERY_LOCK_TOKEN: recoveryToken } : process.env,
35
+ env: managedRunnerEnvironment({ fullEnv: options.fullEnv === true, recoveryToken, launchToken, source: options.env || process.env }),
32
36
  });
33
37
  } finally {
34
38
  if (stdoutFd !== undefined) closeSync(stdoutFd);
@@ -39,13 +43,19 @@ export function launchRunner(dir, recover = false, recoveryToken = "", options =
39
43
  const logger = options.logger || console;
40
44
  child.once?.("error", (error) => {
41
45
  logger.error?.("managed job runner process reported an asynchronous failure", {
42
- job_id: basename(dir),
43
46
  recovery: recover,
44
47
  error_class: classifyOperationalError(error),
45
48
  });
46
49
  });
47
50
  const pid = Number(child.pid);
48
51
  if (!Number.isInteger(pid) || pid <= 0) throw new Error("managed job runner did not receive a process id");
52
+ try {
53
+ publishProvisionalRunnerClaim(dir, pid, launchToken);
54
+ } catch (error) {
55
+ try { child.kill?.("SIGKILL"); }
56
+ catch { /* An uncommitted two-phase runner claim prevents this child from executing even if local kill delivery fails. */ }
57
+ throw error;
58
+ }
49
59
  child.unref();
50
60
  return pid;
51
61
  }
@@ -63,11 +73,26 @@ export function runnerProcessIsCurrent(status, dir, { ownerOnly = false } = {})
63
73
  }
64
74
 
65
75
  function readRunnerOwner(dir, fallback = {}) {
66
- try {
67
- const parsed = JSON.parse(readBoundedFile(join(dir, "runner.pid"), 1024).toString("utf8"));
68
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return { ...fallback };
69
- return { ...fallback, ...parsed };
70
- } catch {
71
- return { ...fallback };
76
+ let text;
77
+ try { text = readBoundedFile(join(dir, "runner.pid"), 1024).toString("utf8"); }
78
+ catch (error) { if (error?.code === "ENOENT") return { ...fallback }; throw error; }
79
+ let parsed;
80
+ try { parsed = JSON.parse(text); }
81
+ catch (error) { throw new Error("managed job runner claim is invalid", { cause: error }); }
82
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("managed job runner claim is invalid");
83
+ return { ...fallback, ...parsed };
84
+ }
85
+
86
+ export function managedRunnerEnvironment({ fullEnv = false, recoveryToken = "", launchToken = "", source = process.env } = {}) {
87
+ const env = fullEnv ? { ...source } : {};
88
+ if (!fullEnv) {
89
+ for (const key of ["PATH", "HOME", "USERPROFILE", "SystemRoot", "WINDIR", "COMSPEC", "PATHEXT", "LANG", "LC_ALL", "LC_CTYPE", "TMPDIR", "TEMP", "TMP", "AGENT_RESOURCE_COORDINATOR_ROOT", "AGENT_BUILD_ROOT"]) {
90
+ if (typeof source[key] === "string" && source[key]) env[key] = source[key];
91
+ }
72
92
  }
93
+ if (recoveryToken) env.MBM_RECOVERY_LOCK_TOKEN = recoveryToken;
94
+ else delete env.MBM_RECOVERY_LOCK_TOKEN;
95
+ if (launchToken) env.MBM_RUNNER_LAUNCH_TOKEN = launchToken;
96
+ else delete env.MBM_RUNNER_LAUNCH_TOKEN;
97
+ return withExecutionSurface(env, EXECUTION_SURFACE.managedJob);
73
98
  }
@@ -1,8 +1,10 @@
1
1
  import { closeSync, constants as fsConstants, ftruncateSync, readSync, readdirSync, writeSync } from "node:fs";
2
2
  import { dirname } from "node:path";
3
+
4
+ const MANAGED_STATE_READ_ATTEMPTS = 4;
3
5
  import { replaceFileAtomicallySync } from "./exclusive-file.mjs";
4
6
  import { openRegularFileSync, readBoundedRegularFileSync } from "./secure-file.mjs";
5
- import { ensureOwnerOnlyDir, ownerOnlyFile } from "./state.mjs";
7
+ import { ensureOwnerOnlyDir, ownerOnlyFile } from "./secure-file.mjs";
6
8
 
7
9
  export function atomicWriteJson(file, value, maxBytes) {
8
10
  ensureOwnerOnlyDir(dirname(file));
@@ -12,11 +14,21 @@ export function atomicWriteJson(file, value, maxBytes) {
12
14
  ownerOnlyFile(file);
13
15
  }
14
16
 
15
- export function readJson(file, maxBytes, label = "JSON") {
17
+ export function readJson(file, maxBytes, label = "JSON", options = {}) {
18
+ const readFile = typeof options.readFile === "function" ? options.readFile : readBoundedRegularFileSync;
16
19
  let buffer;
17
- try { buffer = readBoundedFile(file, maxBytes); } catch (error) {
18
- if (error?.code === "ENOENT") return null;
19
- throw new Error(`${label} is unavailable (${resourceErrorClass(error)})`);
20
+ for (let attempt = 1; attempt <= MANAGED_STATE_READ_ATTEMPTS; attempt += 1) {
21
+ try {
22
+ buffer = readFile(file, maxBytes, "managed job state", {
23
+ verifyPathIdentity: true,
24
+ rejectMultipleLinks: true,
25
+ });
26
+ break;
27
+ } catch (error) {
28
+ if (error?.code === "ENOENT") return null;
29
+ if (error?.code === "MBM_IDENTITY_CHANGED" && attempt < MANAGED_STATE_READ_ATTEMPTS) continue;
30
+ throw new Error(`${label} is unavailable (${resourceErrorClass(error)})`, { cause: error });
31
+ }
20
32
  }
21
33
  let text;
22
34
  try { text = new TextDecoder("utf-8", { fatal: true }).decode(buffer); } catch {
@@ -41,7 +53,7 @@ export function openPrivateAppendFile(file) {
41
53
  return openRegularFileSync(
42
54
  file,
43
55
  Number(fsConstants.O_WRONLY) | Number(fsConstants.O_CREAT) | Number(fsConstants.O_APPEND),
44
- { label: "runner diagnostic path", mode: 0o600, chmod: 0o600 },
56
+ { label: "runner diagnostic path", mode: 0o600, chmod: 0o600, rejectMultipleLinks: true },
45
57
  ).fd;
46
58
  }
47
59
 
@@ -51,6 +63,7 @@ export function trimDiagnosticFile(file, maxBytes = 64 * 1024, keepBytes = 32 *
51
63
  const opened = openRegularFileSync(file, fsConstants.O_RDWR, {
52
64
  label: "runner diagnostic path",
53
65
  chmod: 0o600,
66
+ rejectMultipleLinks: true,
54
67
  });
55
68
  fd = opened.fd;
56
69
  if (opened.info.size <= maxBytes) return;
@@ -79,10 +92,13 @@ export function trimDiagnosticFile(file, maxBytes = 64 * 1024, keepBytes = 32 *
79
92
  }
80
93
 
81
94
  export function resourceErrorClass(error) {
95
+ if (error?.code === "integrity_error") return "integrity_error";
96
+ if (error?.code === "MBM_IDENTITY_CHANGED" || error?.cause?.code === "MBM_IDENTITY_CHANGED") return "identity_changed";
82
97
  const message = String(error?.message || error || "");
83
98
  if (/permission|EACCES|EPERM/i.test(message)) return "permission_denied";
84
99
  if (/not found|ENOENT/i.test(message)) return "not_found";
85
100
  if (/symbolic link/i.test(message)) return "symbolic_link_denied";
101
+ if (/multiple hard links/i.test(message)) return "insecure_links";
86
102
  if (/readable by group|permissions/i.test(message)) return "insecure_permissions";
87
103
  if (/exceeds/i.test(message)) return "size_limit";
88
104
  return "resource_unavailable";