@mono-agent/agent-app 0.11.2 → 0.11.4

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 (374) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +222 -26
  3. package/dist/adapter-send-tools-main.js +7 -1
  4. package/dist/adapter-send-tools-main.js.map +1 -1
  5. package/dist/adapter-send-tools.d.ts +19 -1
  6. package/dist/adapter-send-tools.d.ts.map +1 -1
  7. package/dist/adapter-send-tools.js +127 -13
  8. package/dist/adapter-send-tools.js.map +1 -1
  9. package/dist/app-config.d.ts.map +1 -1
  10. package/dist/app-config.js +15 -2
  11. package/dist/app-config.js.map +1 -1
  12. package/dist/app-controller-channels.d.ts +8 -0
  13. package/dist/app-controller-channels.d.ts.map +1 -0
  14. package/dist/app-controller-channels.js +207 -0
  15. package/dist/app-controller-channels.js.map +1 -0
  16. package/dist/app-controller-continuation.d.ts +26 -0
  17. package/dist/app-controller-continuation.d.ts.map +1 -0
  18. package/dist/app-controller-continuation.js +198 -0
  19. package/dist/app-controller-continuation.js.map +1 -0
  20. package/dist/app-controller-lifecycle.d.ts +6 -0
  21. package/dist/app-controller-lifecycle.d.ts.map +1 -0
  22. package/dist/app-controller-lifecycle.js +81 -0
  23. package/dist/app-controller-lifecycle.js.map +1 -0
  24. package/dist/app-controller-maintenance.d.ts +13 -0
  25. package/dist/app-controller-maintenance.d.ts.map +1 -0
  26. package/dist/app-controller-maintenance.js +180 -0
  27. package/dist/app-controller-maintenance.js.map +1 -0
  28. package/dist/app-controller-memory-health.d.ts +12 -0
  29. package/dist/app-controller-memory-health.d.ts.map +1 -0
  30. package/dist/app-controller-memory-health.js +160 -0
  31. package/dist/app-controller-memory-health.js.map +1 -0
  32. package/dist/app-controller-memory.d.ts +9 -0
  33. package/dist/app-controller-memory.d.ts.map +1 -0
  34. package/dist/app-controller-memory.js +83 -0
  35. package/dist/app-controller-memory.js.map +1 -0
  36. package/dist/app-controller-responder.d.ts +20 -0
  37. package/dist/app-controller-responder.d.ts.map +1 -0
  38. package/dist/app-controller-responder.js +246 -0
  39. package/dist/app-controller-responder.js.map +1 -0
  40. package/dist/app-controller-traceability.d.ts +25 -0
  41. package/dist/app-controller-traceability.d.ts.map +1 -0
  42. package/dist/app-controller-traceability.js +418 -0
  43. package/dist/app-controller-traceability.js.map +1 -0
  44. package/dist/app-controller-utils.d.ts +38 -0
  45. package/dist/app-controller-utils.d.ts.map +1 -0
  46. package/dist/app-controller-utils.js +185 -0
  47. package/dist/app-controller-utils.js.map +1 -0
  48. package/dist/app-controller.d.ts +400 -0
  49. package/dist/app-controller.d.ts.map +1 -0
  50. package/dist/app-controller.js +336 -0
  51. package/dist/app-controller.js.map +1 -0
  52. package/dist/app.d.ts +7 -115
  53. package/dist/app.d.ts.map +1 -1
  54. package/dist/app.js +6 -1912
  55. package/dist/app.js.map +1 -1
  56. package/dist/artifact-retention.d.ts +4 -0
  57. package/dist/artifact-retention.d.ts.map +1 -1
  58. package/dist/artifact-retention.js +38 -0
  59. package/dist/artifact-retention.js.map +1 -1
  60. package/dist/backfill.d.ts +3 -1
  61. package/dist/backfill.d.ts.map +1 -1
  62. package/dist/backfill.js +5 -1
  63. package/dist/backfill.js.map +1 -1
  64. package/dist/background-runtime.d.ts +27 -0
  65. package/dist/background-runtime.d.ts.map +1 -1
  66. package/dist/background-runtime.js +383 -205
  67. package/dist/background-runtime.js.map +1 -1
  68. package/dist/background-worker-lease.d.ts.map +1 -1
  69. package/dist/background-worker-lease.js +41 -241
  70. package/dist/background-worker-lease.js.map +1 -1
  71. package/dist/background.d.ts +31 -9
  72. package/dist/background.d.ts.map +1 -1
  73. package/dist/background.js +543 -272
  74. package/dist/background.js.map +1 -1
  75. package/dist/channel-drivers/cron.d.ts +11 -0
  76. package/dist/channel-drivers/cron.d.ts.map +1 -0
  77. package/dist/channel-drivers/cron.js +168 -0
  78. package/dist/channel-drivers/cron.js.map +1 -0
  79. package/dist/channel-drivers/live.d.ts +11 -0
  80. package/dist/channel-drivers/live.d.ts.map +1 -0
  81. package/dist/channel-drivers/live.js +47 -0
  82. package/dist/channel-drivers/live.js.map +1 -0
  83. package/dist/channel-drivers/native-notify.d.ts +27 -0
  84. package/dist/channel-drivers/native-notify.d.ts.map +1 -0
  85. package/dist/channel-drivers/native-notify.js +94 -0
  86. package/dist/channel-drivers/native-notify.js.map +1 -0
  87. package/dist/channel-drivers/openai-api.d.ts +7 -0
  88. package/dist/channel-drivers/openai-api.d.ts.map +1 -0
  89. package/dist/channel-drivers/openai-api.js +63 -0
  90. package/dist/channel-drivers/openai-api.js.map +1 -0
  91. package/dist/channel-drivers/shared.d.ts +4 -0
  92. package/dist/channel-drivers/shared.d.ts.map +1 -0
  93. package/dist/channel-drivers/shared.js +5 -0
  94. package/dist/channel-drivers/shared.js.map +1 -0
  95. package/dist/channel-drivers/slack.d.ts +14 -0
  96. package/dist/channel-drivers/slack.d.ts.map +1 -0
  97. package/dist/channel-drivers/slack.js +227 -0
  98. package/dist/channel-drivers/slack.js.map +1 -0
  99. package/dist/channel-drivers/telegram.d.ts +11 -0
  100. package/dist/channel-drivers/telegram.d.ts.map +1 -0
  101. package/dist/channel-drivers/telegram.js +244 -0
  102. package/dist/channel-drivers/telegram.js.map +1 -0
  103. package/dist/channel-drivers/tui.d.ts +15 -0
  104. package/dist/channel-drivers/tui.d.ts.map +1 -0
  105. package/dist/channel-drivers/tui.js +108 -0
  106. package/dist/channel-drivers/tui.js.map +1 -0
  107. package/dist/channel-drivers/webhook.d.ts +7 -0
  108. package/dist/channel-drivers/webhook.d.ts.map +1 -0
  109. package/dist/channel-drivers/webhook.js +115 -0
  110. package/dist/channel-drivers/webhook.js.map +1 -0
  111. package/dist/channels.d.ts +21 -81
  112. package/dist/channels.d.ts.map +1 -1
  113. package/dist/channels.js +11 -1174
  114. package/dist/channels.js.map +1 -1
  115. package/dist/cli-args.d.ts +121 -0
  116. package/dist/cli-args.d.ts.map +1 -0
  117. package/dist/cli-args.js +532 -0
  118. package/dist/cli-args.js.map +1 -0
  119. package/dist/cli-background-command.d.ts +59 -0
  120. package/dist/cli-background-command.d.ts.map +1 -0
  121. package/dist/cli-background-command.js +475 -0
  122. package/dist/cli-background-command.js.map +1 -0
  123. package/dist/cli-cancellation.d.ts +6 -0
  124. package/dist/cli-cancellation.d.ts.map +1 -0
  125. package/dist/cli-cancellation.js +23 -0
  126. package/dist/cli-cancellation.js.map +1 -0
  127. package/dist/cli-commands.d.ts +12 -0
  128. package/dist/cli-commands.d.ts.map +1 -0
  129. package/dist/cli-commands.js +306 -0
  130. package/dist/cli-commands.js.map +1 -0
  131. package/dist/cli-help.d.ts +11 -0
  132. package/dist/cli-help.d.ts.map +1 -0
  133. package/dist/cli-help.js +262 -0
  134. package/dist/cli-help.js.map +1 -0
  135. package/dist/cli-init-command.d.ts +60 -0
  136. package/dist/cli-init-command.d.ts.map +1 -0
  137. package/dist/cli-init-command.js +1910 -0
  138. package/dist/cli-init-command.js.map +1 -0
  139. package/dist/cli-install-skill-command.d.ts +3 -0
  140. package/dist/cli-install-skill-command.d.ts.map +1 -0
  141. package/dist/cli-install-skill-command.js +51 -0
  142. package/dist/cli-install-skill-command.js.map +1 -0
  143. package/dist/cli-sandbox-command.d.ts +10 -0
  144. package/dist/cli-sandbox-command.d.ts.map +1 -0
  145. package/dist/cli-sandbox-command.js +100 -0
  146. package/dist/cli-sandbox-command.js.map +1 -0
  147. package/dist/cli-validate-config-command.d.ts +36 -0
  148. package/dist/cli-validate-config-command.d.ts.map +1 -0
  149. package/dist/cli-validate-config-command.js +317 -0
  150. package/dist/cli-validate-config-command.js.map +1 -0
  151. package/dist/cli.d.ts +6 -248
  152. package/dist/cli.d.ts.map +1 -1
  153. package/dist/cli.js +6 -3792
  154. package/dist/cli.js.map +1 -1
  155. package/dist/config-reference.d.ts +2 -1
  156. package/dist/config-reference.d.ts.map +1 -1
  157. package/dist/config-reference.js +34 -8
  158. package/dist/config-reference.js.map +1 -1
  159. package/dist/configured-agent.d.ts +23 -0
  160. package/dist/configured-agent.d.ts.map +1 -1
  161. package/dist/configured-agent.js +92 -13
  162. package/dist/configured-agent.js.map +1 -1
  163. package/dist/continuation-dispatch-worker.d.ts +21 -0
  164. package/dist/continuation-dispatch-worker.d.ts.map +1 -0
  165. package/dist/continuation-dispatch-worker.js +425 -0
  166. package/dist/continuation-dispatch-worker.js.map +1 -0
  167. package/dist/continuation-http-routes.d.ts +15 -0
  168. package/dist/continuation-http-routes.d.ts.map +1 -0
  169. package/dist/continuation-http-routes.js +265 -0
  170. package/dist/continuation-http-routes.js.map +1 -0
  171. package/dist/continuation-operator-api.d.ts +18 -0
  172. package/dist/continuation-operator-api.d.ts.map +1 -0
  173. package/dist/continuation-operator-api.js +164 -0
  174. package/dist/continuation-operator-api.js.map +1 -0
  175. package/dist/continuation-origin-store.d.ts +19 -0
  176. package/dist/continuation-origin-store.d.ts.map +1 -0
  177. package/dist/continuation-origin-store.js +273 -0
  178. package/dist/continuation-origin-store.js.map +1 -0
  179. package/dist/continuation-service-errors.d.ts +18 -0
  180. package/dist/continuation-service-errors.d.ts.map +1 -0
  181. package/dist/continuation-service-errors.js +35 -0
  182. package/dist/continuation-service-errors.js.map +1 -0
  183. package/dist/continuation-service-helpers.d.ts +39 -0
  184. package/dist/continuation-service-helpers.d.ts.map +1 -0
  185. package/dist/continuation-service-helpers.js +308 -0
  186. package/dist/continuation-service-helpers.js.map +1 -0
  187. package/dist/continuation-service-origin.d.ts +55 -0
  188. package/dist/continuation-service-origin.d.ts.map +1 -0
  189. package/dist/continuation-service-origin.js +325 -0
  190. package/dist/continuation-service-origin.js.map +1 -0
  191. package/dist/continuation-service-types.d.ts +110 -0
  192. package/dist/continuation-service-types.d.ts.map +1 -0
  193. package/dist/continuation-service-types.js +11 -0
  194. package/dist/continuation-service-types.js.map +1 -0
  195. package/dist/continuation-service.d.ts +4 -97
  196. package/dist/continuation-service.d.ts.map +1 -1
  197. package/dist/continuation-service.js +9 -1525
  198. package/dist/continuation-service.js.map +1 -1
  199. package/dist/continuation-store-fs.d.ts +15 -0
  200. package/dist/continuation-store-fs.d.ts.map +1 -0
  201. package/dist/continuation-store-fs.js +247 -0
  202. package/dist/continuation-store-fs.js.map +1 -0
  203. package/dist/continuation-store-policy.d.ts +21 -0
  204. package/dist/continuation-store-policy.d.ts.map +1 -0
  205. package/dist/continuation-store-policy.js +278 -0
  206. package/dist/continuation-store-policy.js.map +1 -0
  207. package/dist/continuation-store-records.d.ts +9 -0
  208. package/dist/continuation-store-records.d.ts.map +1 -0
  209. package/dist/continuation-store-records.js +221 -0
  210. package/dist/continuation-store-records.js.map +1 -0
  211. package/dist/continuation-store-types.d.ts +173 -0
  212. package/dist/continuation-store-types.d.ts.map +1 -0
  213. package/dist/continuation-store-types.js +26 -0
  214. package/dist/continuation-store-types.js.map +1 -0
  215. package/dist/continuation-store.d.ts +4 -138
  216. package/dist/continuation-store.d.ts.map +1 -1
  217. package/dist/continuation-store.js +145 -1049
  218. package/dist/continuation-store.js.map +1 -1
  219. package/dist/doctor.d.ts +10 -5
  220. package/dist/doctor.d.ts.map +1 -1
  221. package/dist/doctor.js +629 -58
  222. package/dist/doctor.js.map +1 -1
  223. package/dist/init.d.ts +2 -0
  224. package/dist/init.d.ts.map +1 -1
  225. package/dist/init.js +92 -239
  226. package/dist/init.js.map +1 -1
  227. package/dist/interaction-bridge.d.ts +19 -0
  228. package/dist/interaction-bridge.d.ts.map +1 -1
  229. package/dist/interaction-bridge.js +130 -0
  230. package/dist/interaction-bridge.js.map +1 -1
  231. package/dist/launchd-logs.d.ts +91 -0
  232. package/dist/launchd-logs.d.ts.map +1 -0
  233. package/dist/launchd-logs.js +1499 -0
  234. package/dist/launchd-logs.js.map +1 -0
  235. package/dist/launchd.d.ts +27 -0
  236. package/dist/launchd.d.ts.map +1 -1
  237. package/dist/launchd.js +89 -10
  238. package/dist/launchd.js.map +1 -1
  239. package/dist/memory-command.d.ts +2 -0
  240. package/dist/memory-command.d.ts.map +1 -1
  241. package/dist/memory-command.js +161 -48
  242. package/dist/memory-command.js.map +1 -1
  243. package/dist/memory-recall-settings.d.ts +64 -0
  244. package/dist/memory-recall-settings.d.ts.map +1 -0
  245. package/dist/memory-recall-settings.js +51 -0
  246. package/dist/memory-recall-settings.js.map +1 -0
  247. package/dist/memory-recall.d.ts +7 -95
  248. package/dist/memory-recall.d.ts.map +1 -1
  249. package/dist/memory-recall.js +8 -143
  250. package/dist/memory-recall.js.map +1 -1
  251. package/dist/memory-rituals.d.ts +3 -3
  252. package/dist/memory-rituals.d.ts.map +1 -1
  253. package/dist/memory-rituals.js +16 -111
  254. package/dist/memory-rituals.js.map +1 -1
  255. package/dist/modules/catalog.js +2 -2
  256. package/dist/modules/catalog.js.map +1 -1
  257. package/dist/modules/known-tools.d.ts +8 -6
  258. package/dist/modules/known-tools.d.ts.map +1 -1
  259. package/dist/modules/known-tools.js +8 -6
  260. package/dist/modules/known-tools.js.map +1 -1
  261. package/dist/notify-destinations.d.ts +5 -0
  262. package/dist/notify-destinations.d.ts.map +1 -1
  263. package/dist/notify-destinations.js +7 -1
  264. package/dist/notify-destinations.js.map +1 -1
  265. package/dist/owner-private-lock.d.ts +95 -0
  266. package/dist/owner-private-lock.d.ts.map +1 -0
  267. package/dist/owner-private-lock.js +566 -0
  268. package/dist/owner-private-lock.js.map +1 -0
  269. package/dist/pi-oauth-login.d.ts +2 -0
  270. package/dist/pi-oauth-login.d.ts.map +1 -1
  271. package/dist/pi-oauth-login.js +39 -5
  272. package/dist/pi-oauth-login.js.map +1 -1
  273. package/dist/posted-message-index.d.ts +35 -16
  274. package/dist/posted-message-index.d.ts.map +1 -1
  275. package/dist/posted-message-index.js +1068 -52
  276. package/dist/posted-message-index.js.map +1 -1
  277. package/dist/posted-reply-history.d.ts +17 -0
  278. package/dist/posted-reply-history.d.ts.map +1 -0
  279. package/dist/posted-reply-history.js +169 -0
  280. package/dist/posted-reply-history.js.map +1 -0
  281. package/dist/proactive-notify.d.ts.map +1 -1
  282. package/dist/proactive-notify.js +6 -4
  283. package/dist/proactive-notify.js.map +1 -1
  284. package/dist/project-skills.d.ts +4 -0
  285. package/dist/project-skills.d.ts.map +1 -1
  286. package/dist/project-skills.js +99 -130
  287. package/dist/project-skills.js.map +1 -1
  288. package/dist/provider-setup.d.ts +0 -1
  289. package/dist/provider-setup.d.ts.map +1 -1
  290. package/dist/provider-setup.js +0 -3
  291. package/dist/provider-setup.js.map +1 -1
  292. package/dist/readiness-probe-worker.d.ts +61 -1
  293. package/dist/readiness-probe-worker.d.ts.map +1 -1
  294. package/dist/readiness-probe-worker.js +54 -58
  295. package/dist/readiness-probe-worker.js.map +1 -1
  296. package/dist/readiness-probe.d.ts +6 -9
  297. package/dist/readiness-probe.d.ts.map +1 -1
  298. package/dist/readiness-probe.js +30 -56
  299. package/dist/readiness-probe.js.map +1 -1
  300. package/dist/redact-secrets.d.ts +15 -0
  301. package/dist/redact-secrets.d.ts.map +1 -0
  302. package/dist/redact-secrets.js +65 -0
  303. package/dist/redact-secrets.js.map +1 -0
  304. package/dist/run-history.js +34 -24
  305. package/dist/run-history.js.map +1 -1
  306. package/dist/runtime-provenance.d.ts +8 -0
  307. package/dist/runtime-provenance.d.ts.map +1 -0
  308. package/dist/runtime-provenance.js +225 -0
  309. package/dist/runtime-provenance.js.map +1 -0
  310. package/dist/sandbox-manager.d.ts +4 -1
  311. package/dist/sandbox-manager.d.ts.map +1 -1
  312. package/dist/sandbox-manager.js +114 -273
  313. package/dist/sandbox-manager.js.map +1 -1
  314. package/dist/secure-file-replace.d.ts +49 -0
  315. package/dist/secure-file-replace.d.ts.map +1 -0
  316. package/dist/secure-file-replace.js +270 -0
  317. package/dist/secure-file-replace.js.map +1 -0
  318. package/dist/seen-conversations.d.ts +32 -5
  319. package/dist/seen-conversations.d.ts.map +1 -1
  320. package/dist/seen-conversations.js +145 -15
  321. package/dist/seen-conversations.js.map +1 -1
  322. package/dist/sessions.d.ts +20 -0
  323. package/dist/sessions.d.ts.map +1 -1
  324. package/dist/sessions.js +41 -6
  325. package/dist/sessions.js.map +1 -1
  326. package/dist/wizard/index.d.ts +2 -2
  327. package/dist/wizard/index.d.ts.map +1 -1
  328. package/dist/wizard/index.js +1 -1
  329. package/dist/wizard/index.js.map +1 -1
  330. package/dist/wizard/presets.d.ts +2 -1
  331. package/dist/wizard/presets.d.ts.map +1 -1
  332. package/dist/wizard/presets.js +2 -1
  333. package/dist/wizard/presets.js.map +1 -1
  334. package/dist/wizard/run.d.ts +0 -26
  335. package/dist/wizard/run.d.ts.map +1 -1
  336. package/dist/wizard/run.js +0 -53
  337. package/dist/wizard/run.js.map +1 -1
  338. package/package.json +22 -17
  339. package/schema/mono-agent.config.schema.json +45 -10
  340. package/skills/mono-agent-composer/SKILL.md +3 -3
  341. package/skills/mono-agent-composer/references/config-blueprint.md +123 -29
  342. package/skills/mono-agent-composer/references/discovery-questions.md +10 -8
  343. package/skills/mono-agent-composer/references/feature-coverage.md +90 -76
  344. package/skills/mono-agent-composer/references/package-map.md +6 -4
  345. package/skills/mono-agent-composer/references/playbooks.md +48 -27
  346. package/skills/mono-agent-composer/references/validation.md +16 -3
  347. package/dist/notify-runtime.d.ts +0 -26
  348. package/dist/notify-runtime.d.ts.map +0 -1
  349. package/dist/notify-runtime.js +0 -27
  350. package/dist/notify-runtime.js.map +0 -1
  351. package/dist/notify-tool.d.ts +0 -51
  352. package/dist/notify-tool.d.ts.map +0 -1
  353. package/dist/notify-tool.js +0 -182
  354. package/dist/notify-tool.js.map +0 -1
  355. package/dist/recipes/base.d.ts +0 -15
  356. package/dist/recipes/base.d.ts.map +0 -1
  357. package/dist/recipes/base.js +0 -51
  358. package/dist/recipes/base.js.map +0 -1
  359. package/dist/recipes/catalog.d.ts +0 -4
  360. package/dist/recipes/catalog.d.ts.map +0 -1
  361. package/dist/recipes/catalog.js +0 -525
  362. package/dist/recipes/catalog.js.map +0 -1
  363. package/dist/recipes/index.d.ts +0 -11
  364. package/dist/recipes/index.d.ts.map +0 -1
  365. package/dist/recipes/index.js +0 -14
  366. package/dist/recipes/index.js.map +0 -1
  367. package/dist/recipes/types.d.ts +0 -70
  368. package/dist/recipes/types.d.ts.map +0 -1
  369. package/dist/recipes/types.js +0 -15
  370. package/dist/recipes/types.js.map +0 -1
  371. package/dist/setup.d.ts +0 -29
  372. package/dist/setup.d.ts.map +0 -1
  373. package/dist/setup.js +0 -97
  374. package/dist/setup.js.map +0 -1
@@ -1,17 +1,19 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
2
  import { constants as fsConstants } from "node:fs";
3
- import { chmod, cp, lstat, mkdir, open, readFile, readlink, readdir, realpath, rename, rm, stat, symlink, writeFile, } from "node:fs/promises";
3
+ import { chmod, cp, lstat, mkdir, open, readFile, readlink, readdir, realpath, rename, rm, symlink, writeFile, } from "node:fs/promises";
4
4
  import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
5
5
  import process from "node:process";
6
6
  import { accountHomeDirectory } from "./account-home.js";
7
7
  import { isBackgroundOperationalEnvName } from "./background-environment.js";
8
8
  import { currentProcessIncarnation, isSameProcessIncarnation as matchesProcessIncarnation, processIncarnationFromJson, } from "./process-incarnation.js";
9
+ import { acquireOwnerPrivateLock } from "./owner-private-lock.js";
9
10
  const PACKAGE_NAME = "@mono-agent/agent-app";
10
11
  export const MANAGED_BACKGROUND_WORKER_ENV = "MONO_AGENT_MANAGED_WORKER";
11
12
  const LOCK_WAIT_TIMEOUT_MS = 30_000;
12
13
  const LOCK_STALE_AFTER_MS = 5 * 60_000;
13
14
  const LOCK_POLL_INTERVAL_MS = 200;
14
15
  const PROVISIONAL_RUNTIME_INSTALLED_AT = "1970-01-01T00:00:00.000Z";
16
+ const MAX_RUNTIME_MARKER_BYTES = 16 * 1024;
15
17
  export function defaultManagedBackgroundRuntimeDeps() {
16
18
  return {
17
19
  now: () => Date.now(),
@@ -173,7 +175,86 @@ export async function ensureManagedBackgroundRuntime(input, deps = defaultManage
173
175
  return runtimeResult(layout, identity, input.nodePath);
174
176
  }
175
177
  finally {
176
- await removeSameRuntimeLockDirectory(layout.lockDir, acquired).catch(() => undefined);
178
+ await acquired.release().catch(() => undefined);
179
+ }
180
+ }
181
+ /**
182
+ * Canonically verify the installed package, freshly recomputed execution
183
+ * closure/filesystem proof, and coherent current manifest behind a provenance
184
+ * marker. This intentionally does not install, repair, or compare against a
185
+ * deploy checkout; it only validates the managed snapshot executing the caller.
186
+ */
187
+ export async function verifyManagedRuntimeClosureForProvenance(input, deps = {}) {
188
+ try {
189
+ if (!isExactVersion(input.packageVersion)
190
+ || !/^[0-9a-f]{64}$/u.test(input.cliSha256)
191
+ || !/^[0-9a-f]{64}$/u.test(input.sourceClosureSha256)
192
+ || !/^[0-9a-f]{64}$/u.test(input.closureManifestSha256)
193
+ || !/^[0-9a-f]{64}$/u.test(input.executionProofSha256)) {
194
+ return false;
195
+ }
196
+ const packageRoot = resolve(input.packageRoot);
197
+ const installRoot = dirname(dirname(dirname(packageRoot)));
198
+ if (join(installRoot, "node_modules", "@mono-agent", "agent-app") !== packageRoot)
199
+ return false;
200
+ const identity = {
201
+ packageVersion: input.packageVersion,
202
+ cliSha256: input.cliSha256,
203
+ sourceClosureSha256: input.sourceClosureSha256,
204
+ nodeAbi: input.nodeAbi,
205
+ platform: input.platform,
206
+ arch: input.arch,
207
+ };
208
+ const appRuntimeRoot = dirname(dirname(dirname(installRoot)));
209
+ const home = dirname(dirname(dirname(appRuntimeRoot)));
210
+ const layout = runtimeLayout(home, identity, "provenance", 0);
211
+ if (layout.installRoot !== installRoot || layout.cliPath !== join(packageRoot, "dist", "cli.js"))
212
+ return false;
213
+ if (!(await verifyPrivateRuntimeAncestors(home, layout.versionAbiDir)))
214
+ return false;
215
+ const expectedMarker = {
216
+ schema: "mono-agent.managed-runtime.v4",
217
+ packageName: PACKAGE_NAME,
218
+ closureManifestSha256: input.closureManifestSha256,
219
+ executionProofSha256: input.executionProofSha256,
220
+ ...identity,
221
+ installedAt: input.installedAt,
222
+ };
223
+ const markerInitial = await verifiedRuntimeMarker(layout, identity);
224
+ if (markerInitial === undefined || !sameRuntimeMarker(markerInitial, expectedMarker))
225
+ return false;
226
+ const additionalPackages = await managedRuntimeAdditionalPackages(layout);
227
+ if (additionalPackages === undefined)
228
+ return false;
229
+ const closureInitial = await matchingRuntimeClosureCapture(layout, identity, markerInitial, additionalPackages);
230
+ if (closureInitial === undefined)
231
+ return false;
232
+ await deps.afterInitialClosureCapture?.();
233
+ const closureFinal = await matchingRuntimeClosureCapture(layout, identity, markerInitial, additionalPackages, deps.afterFinalRootCapture === undefined
234
+ ? {}
235
+ : { afterRootCapture: deps.afterFinalRootCapture });
236
+ if (closureFinal === undefined
237
+ || closureFinal.sourceClosureSha256 !== closureInitial.sourceClosureSha256
238
+ || closureFinal.sourceProofSha256 !== closureInitial.sourceProofSha256
239
+ || closureFinal.filesystemProofSha256 !== closureInitial.filesystemProofSha256) {
240
+ return false;
241
+ }
242
+ const markerFinal = await readRuntimeMarker(layout.markerPath, identity);
243
+ if (markerFinal === undefined
244
+ || !sameRuntimeMarker(markerFinal, markerInitial)
245
+ || !sameRuntimeMarker(markerFinal, expectedMarker)
246
+ || !(await verifyClosureManifest(layout, markerFinal.closureManifestSha256, deps.beforeFinalManifestProof === undefined
247
+ ? {}
248
+ : { beforeFinalProof: deps.beforeFinalManifestProof }))) {
249
+ return false;
250
+ }
251
+ const markerAfterManifest = await readRuntimeMarker(layout.markerPath, identity);
252
+ return markerAfterManifest !== undefined
253
+ && sameRuntimeMarker(markerAfterManifest, markerInitial)
254
+ && sameRuntimeMarker(markerAfterManifest, expectedMarker);
255
+ }
256
+ catch {
257
+ return false;
177
258
  }
178
259
  }
179
260
  /**
@@ -353,11 +434,12 @@ async function materializeExactExecutionClosure(input) {
353
434
  throw new Error("The managed runtime package closure changed while it was staged.");
354
435
  }
355
436
  }
356
- async function captureExecutionClosure(packageSource, additionalPackages, filesystemProofRoot) {
437
+ async function captureExecutionClosure(packageSource, additionalPackages, filesystemProofRoot, deps = {}) {
357
438
  const nodes = [];
358
439
  const bySourceRoot = new Map();
359
440
  const proofs = new Map();
360
441
  const dependencyProofs = [];
442
+ const dependencyProofPaths = [];
361
443
  const resolutionPathProof = filesystemProofRoot === undefined
362
444
  ? undefined
363
445
  : await createResolutionPathProof(filesystemProofRoot);
@@ -400,13 +482,20 @@ async function captureExecutionClosure(packageSource, additionalPackages, filesy
400
482
  throw new Error(`Installed dependency ${dependencyName} changed while its execution closure was inspected.`);
401
483
  }
402
484
  dependencyProofs.push({ ownerId: id, dependencyName, proof: dependencyProofAfter });
485
+ dependencyProofPaths.push({
486
+ path: dependency.candidatePath,
487
+ logicalPath: dependencyName,
488
+ proof: dependencyProofAfter,
489
+ });
403
490
  node.dependencies.set(dependencyName, dependencyNodeId);
404
491
  }
405
492
  return id;
406
493
  };
407
494
  await visit(packageSource);
495
+ await deps.afterRootCapture?.();
408
496
  const additionalRoots = [];
409
497
  const additionalProofs = [];
498
+ const additionalProofPaths = [];
410
499
  for (const additional of additionalPackages) {
411
500
  const additionalProof = await captureFilesystemPathProof(additional.packageSource, additional.packageName);
412
501
  const nodeId = await visit(additional.packageSource);
@@ -423,9 +512,18 @@ async function captureExecutionClosure(packageSource, additionalPackages, filesy
423
512
  throw new Error(`Additional managed runtime package ${additional.packageName} changed while its execution closure was inspected.`);
424
513
  }
425
514
  additionalProofs.push({ packageName: additional.packageName, proof: additionalProofAfter });
515
+ additionalProofPaths.push({
516
+ path: additional.packageSource,
517
+ logicalPath: additional.packageName,
518
+ proof: additionalProofAfter,
519
+ });
426
520
  additionalRoots.push({ packageName: additional.packageName, nodeId });
427
521
  }
428
522
  const closure = nodes;
523
+ await revalidateExecutionClosureProofs(closure, proofs, [
524
+ ...dependencyProofPaths,
525
+ ...additionalProofPaths,
526
+ ]);
429
527
  const resolutionPaths = await resolutionPathProof?.finalize() ?? [];
430
528
  const stable = {
431
529
  schema: "mono-agent.source-execution-closure.v1",
@@ -473,6 +571,30 @@ async function captureExecutionClosure(packageSource, additionalPackages, filesy
473
571
  filesystemProofSha256: sha256(Buffer.from(JSON.stringify(filesystemProof), "utf8")),
474
572
  };
475
573
  }
574
+ /**
575
+ * Final identity pass for every package entry and link captured above. Per-file
576
+ * reads are descriptor-stable, but without this pass an early file could be
577
+ * overwritten while a later large file was still being hashed.
578
+ */
579
+ async function revalidateExecutionClosureProofs(closure, proofs, links) {
580
+ for (const node of closure) {
581
+ for (const expected of requiredMapValue(proofs, node.id, "source package proof")) {
582
+ const path = expected.path === "."
583
+ ? node.sourceRoot
584
+ : join(node.sourceRoot, ...expected.path.split("/"));
585
+ const current = await captureFilesystemPathProof(path, expected.path);
586
+ if (!sameSourceProofEntry(expected, current)) {
587
+ throw new Error(`Managed runtime source package entry ${expected.path} changed during closure capture.`);
588
+ }
589
+ }
590
+ }
591
+ for (const link of links) {
592
+ const current = await captureFilesystemPathProof(link.path, link.logicalPath);
593
+ if (!sameSourceProofEntry(link.proof, current)) {
594
+ throw new Error(`Managed runtime package link ${link.logicalPath} changed during closure capture.`);
595
+ }
596
+ }
597
+ }
476
598
  async function canonicalAdditionalPackages(packages) {
477
599
  const byName = new Map();
478
600
  for (const entry of packages) {
@@ -660,6 +782,7 @@ function sourceProofEntry(path, type, details, target) {
660
782
  type,
661
783
  dev: details.dev.toString(),
662
784
  ino: details.ino.toString(),
785
+ nlink: details.nlink.toString(),
663
786
  mode: details.mode.toString(),
664
787
  size: details.size.toString(),
665
788
  mtimeNs: details.mtimeNs.toString(),
@@ -672,6 +795,7 @@ function sameSourceIdentity(left, right) {
672
795
  }
673
796
  function sameSourceStats(left, right) {
674
797
  return sameSourceIdentity(left, right)
798
+ && left.nlink === right.nlink
675
799
  && left.mode === right.mode
676
800
  && left.size === right.size
677
801
  && left.mtimeNs === right.mtimeNs
@@ -960,127 +1084,40 @@ function runtimeLayoutForRoot(layout, root) {
960
1084
  };
961
1085
  }
962
1086
  async function acquireRuntimeLock(layout, identity, additionalPackages, deps) {
963
- const deadline = deps.now() + LOCK_WAIT_TIMEOUT_MS;
964
1087
  const incarnation = await (deps.currentProcessIncarnation ?? currentProcessIncarnation)();
965
- const isSameProcess = deps.isSameProcessIncarnation ?? matchesProcessIncarnation;
966
- for (;;) {
967
- let createdIdentity;
968
- try {
969
- await mkdir(layout.lockDir, { mode: 0o700 });
970
- createdIdentity = runtimeLockIdentity(await assertPrivateRuntimeLockDirectory(layout.lockDir));
971
- await writePrivateJson(join(layout.lockDir, "owner.json"), {
972
- pid: process.pid,
973
- createdAt: new Date(deps.now()).toISOString(),
974
- incarnation,
975
- ...identity,
976
- });
977
- if (!(await sameRuntimeLockDirectory(layout.lockDir, createdIdentity))) {
978
- throw new Error(`Managed runtime lock ${layout.lockDir} changed while its owner record was written.`);
979
- }
980
- return createdIdentity;
981
- }
982
- catch (error) {
983
- if (createdIdentity !== undefined) {
984
- await removeSameRuntimeLockDirectory(layout.lockDir, createdIdentity).catch(() => undefined);
985
- }
986
- if (!isErrno(error, "EEXIST"))
987
- throw error;
988
- }
989
- if (await verifyRuntime(layout, identity, additionalPackages))
990
- return undefined;
991
- let stale = false;
992
- try {
993
- const lockDetails = await assertPrivateRuntimeLockDirectory(layout.lockDir);
994
- const observedIdentity = runtimeLockIdentity(lockDetails);
995
- const owner = await runtimeLockOwner(join(layout.lockDir, "owner.json"));
996
- stale = owner === undefined
997
- ? deps.now() - (await stat(layout.lockDir)).mtimeMs >= LOCK_STALE_AFTER_MS
998
- : !(await isSameProcess(owner.pid, owner.incarnation));
999
- if (!(await sameRuntimeLockDirectory(layout.lockDir, observedIdentity)))
1000
- continue;
1001
- if (stale) {
1002
- const staleLock = `${layout.quarantineDir}-stale-lock`;
1003
- await mkdir(dirname(staleLock), { recursive: true, mode: 0o700 });
1004
- try {
1005
- await rename(layout.lockDir, staleLock);
1006
- }
1007
- catch (error) {
1008
- if (isErrno(error, "ENOENT") || isErrno(error, "EEXIST"))
1009
- continue;
1010
- throw error;
1011
- }
1012
- const moved = await lstat(staleLock);
1013
- if (!sameRuntimeLockIdentity(moved, observedIdentity)) {
1014
- await rename(staleLock, layout.lockDir).catch(() => undefined);
1015
- return undefined;
1016
- }
1017
- continue;
1018
- }
1019
- }
1020
- catch (error) {
1021
- if (isErrno(error, "ENOENT"))
1022
- continue;
1023
- throw error;
1024
- }
1025
- if (deps.now() >= deadline)
1026
- return undefined;
1027
- await deps.sleep(LOCK_POLL_INTERVAL_MS);
1028
- }
1029
- }
1030
- async function runtimeLockOwner(path) {
1031
- try {
1032
- const owner = JSON.parse(await readFile(path, "utf8"));
1033
- const incarnation = processIncarnationFromJson(owner.incarnation);
1034
- return typeof owner.pid === "number"
1035
- && Number.isSafeInteger(owner.pid)
1036
- && owner.pid > 0
1037
- && incarnation !== undefined
1038
- ? { pid: owner.pid, incarnation }
1039
- : undefined;
1040
- }
1041
- catch {
1042
- return undefined;
1043
- }
1044
- }
1045
- async function assertPrivateRuntimeLockDirectory(path) {
1046
- const details = await lstat(path);
1047
- if (!details.isDirectory() || details.isSymbolicLink() || !ownedPrivately(details)) {
1048
- throw new Error(`Managed runtime lock ${path} is not a private current-user directory.`);
1049
- }
1050
- return details;
1051
- }
1052
- function runtimeLockIdentity(details) {
1053
- return { dev: details.dev, ino: details.ino };
1054
- }
1055
- function sameRuntimeLockIdentity(details, identity) {
1056
- return details.dev === identity.dev && details.ino === identity.ino;
1057
- }
1058
- async function sameRuntimeLockDirectory(path, identity) {
1059
- try {
1060
- return sameRuntimeLockIdentity(await assertPrivateRuntimeLockDirectory(path), identity);
1061
- }
1062
- catch {
1063
- return false;
1064
- }
1065
- }
1066
- async function removeSameRuntimeLockDirectory(path, identity) {
1067
- if (!(await sameRuntimeLockDirectory(path, identity)))
1068
- return;
1069
- const released = `${path}.released-${process.pid}-${randomUUID()}`;
1070
- try {
1071
- await rename(path, released);
1072
- }
1073
- catch (error) {
1074
- if (isErrno(error, "ENOENT"))
1075
- return;
1076
- throw error;
1077
- }
1078
- const moved = await lstat(released);
1079
- if (!sameRuntimeLockIdentity(moved, identity)) {
1080
- await rename(released, path).catch(() => undefined);
1081
- throw new Error(`Managed runtime lock ${path} changed while it was released.`);
1082
- }
1083
- await rm(released, { recursive: true, force: true });
1088
+ return acquireOwnerPrivateLock({
1089
+ path: layout.lockDir,
1090
+ label: "Managed runtime lock",
1091
+ schemaTag: "mono-agent.managed-runtime-lock.v1",
1092
+ ownerlessGraceMs: LOCK_STALE_AFTER_MS,
1093
+ waitTimeoutMs: LOCK_WAIT_TIMEOUT_MS,
1094
+ pollIntervalMs: LOCK_POLL_INTERVAL_MS,
1095
+ now: deps.now,
1096
+ processIncarnation: incarnation,
1097
+ ...(deps.isSameProcessIncarnation === undefined
1098
+ ? {}
1099
+ : { isSameProcessIncarnation: deps.isSameProcessIncarnation }),
1100
+ ownerFields: () => ({ ...identity }),
1101
+ validateOwnerFields: (record) => Object.entries(identity)
1102
+ .every(([key, value]) => record[key] === value),
1103
+ parseLegacyOwner: (record) => {
1104
+ const legacyIncarnation = processIncarnationFromJson(record.incarnation);
1105
+ return typeof record.pid === "number"
1106
+ && Number.isSafeInteger(record.pid)
1107
+ && record.pid > 0
1108
+ && legacyIncarnation !== undefined
1109
+ ? { pid: record.pid, incarnation: legacyIncarnation }
1110
+ : undefined;
1111
+ },
1112
+ invalidOwner: "ownerless",
1113
+ isSatisfied: () => verifyRuntime(layout, identity, additionalPackages),
1114
+ sleep: deps.sleep,
1115
+ staleRace: "return",
1116
+ beforeStaleRename: async () => {
1117
+ await mkdir(dirname(layout.quarantineDir), { recursive: true, mode: 0o700 });
1118
+ },
1119
+ stalePath: () => `${layout.quarantineDir}-stale-lock`,
1120
+ });
1084
1121
  }
1085
1122
  async function quarantineInvalidRuntime(layout, identity, additionalPackages, deps) {
1086
1123
  try {
@@ -1114,24 +1151,88 @@ async function promoteStaging(layout, staged, identity, additionalPackages) {
1114
1151
  return false;
1115
1152
  }
1116
1153
  }
1154
+ async function managedRuntimeAdditionalPackages(layout) {
1155
+ try {
1156
+ const parsed = JSON.parse(await readFile(layout.packageLockPath, "utf8"));
1157
+ if (!isRecord(parsed) || parsed.lockfileVersion !== 3 || !isRecord(parsed.packages))
1158
+ return undefined;
1159
+ const byName = new Map();
1160
+ for (const [path, value] of Object.entries(parsed.packages)) {
1161
+ if (!isRecord(value))
1162
+ return undefined;
1163
+ if (value.link !== true)
1164
+ continue;
1165
+ const segments = path.split("/");
1166
+ if (segments[0] !== "node_modules")
1167
+ return undefined;
1168
+ const nameSegments = segments.slice(1);
1169
+ const packageName = nameSegments.join("/");
1170
+ packageNameSegments(packageName);
1171
+ if (packageName === PACKAGE_NAME
1172
+ || join("node_modules", ...nameSegments).split(sep).join("/") !== path
1173
+ || typeof value.resolved !== "string") {
1174
+ return undefined;
1175
+ }
1176
+ const packageSource = join(layout.installRoot, "node_modules", ...nameSegments);
1177
+ const linkDetails = await lstat(packageSource);
1178
+ if (!linkDetails.isSymbolicLink())
1179
+ return undefined;
1180
+ const target = await readlink(packageSource);
1181
+ if (isAbsolute(target))
1182
+ return undefined;
1183
+ const canonical = await realpath(packageSource);
1184
+ assertPathInsideRoot(layout.installRoot, canonical, `additional package ${packageName}`);
1185
+ if (portableRelativePath(layout.installRoot, canonical) !== value.resolved)
1186
+ return undefined;
1187
+ const existing = byName.get(packageName);
1188
+ if (existing !== undefined && existing !== packageSource)
1189
+ return undefined;
1190
+ byName.set(packageName, packageSource);
1191
+ }
1192
+ return [...byName]
1193
+ .sort(([left], [right]) => compareCodeUnits(left, right))
1194
+ .map(([packageName, packageSource]) => ({ packageName, packageSource }));
1195
+ }
1196
+ catch {
1197
+ return undefined;
1198
+ }
1199
+ }
1200
+ async function matchingRuntimeClosureCapture(layout, identity, marker, additionalPackages, deps = {}) {
1201
+ const packageSource = dirname(dirname(layout.cliPath));
1202
+ const runtimeAdditionalPackages = additionalPackages.map(({ packageName }) => ({
1203
+ packageName,
1204
+ packageSource: join(layout.installRoot, "node_modules", ...packageNameSegments(packageName)),
1205
+ }));
1206
+ const closure = await captureExecutionClosure(packageSource, runtimeAdditionalPackages, layout.installRoot, deps);
1207
+ return closure.sourceClosureSha256 === identity.sourceClosureSha256
1208
+ && closure.filesystemProofSha256 === marker.executionProofSha256
1209
+ ? closure
1210
+ : undefined;
1211
+ }
1117
1212
  async function verifyRuntime(layout, identity, additionalPackages) {
1118
1213
  const marker = await verifiedRuntimeMarker(layout, identity);
1119
1214
  if (marker === undefined)
1120
1215
  return false;
1121
1216
  try {
1122
- const packageSource = dirname(dirname(layout.cliPath));
1123
- const runtimeAdditionalPackages = additionalPackages.map(({ packageName }) => ({
1124
- packageName,
1125
- packageSource: join(layout.installRoot, "node_modules", ...packageNameSegments(packageName)),
1126
- }));
1127
- const closure = await captureExecutionClosure(packageSource, runtimeAdditionalPackages, layout.installRoot);
1128
- return closure.sourceClosureSha256 === identity.sourceClosureSha256
1129
- && closure.filesystemProofSha256 === marker.executionProofSha256;
1217
+ return await matchingRuntimeClosureCapture(layout, identity, marker, additionalPackages) !== undefined;
1130
1218
  }
1131
1219
  catch {
1132
1220
  return false;
1133
1221
  }
1134
1222
  }
1223
+ function sameRuntimeMarker(left, right) {
1224
+ return left.schema === right.schema
1225
+ && left.packageName === right.packageName
1226
+ && left.closureManifestSha256 === right.closureManifestSha256
1227
+ && left.executionProofSha256 === right.executionProofSha256
1228
+ && left.packageVersion === right.packageVersion
1229
+ && left.cliSha256 === right.cliSha256
1230
+ && left.sourceClosureSha256 === right.sourceClosureSha256
1231
+ && left.nodeAbi === right.nodeAbi
1232
+ && left.platform === right.platform
1233
+ && left.arch === right.arch
1234
+ && left.installedAt === right.installedAt;
1235
+ }
1135
1236
  async function verifiedRuntimeMarker(layout, identity) {
1136
1237
  try {
1137
1238
  const root = await lstat(layout.installRoot);
@@ -1139,49 +1240,90 @@ async function verifiedRuntimeMarker(layout, identity) {
1139
1240
  return undefined;
1140
1241
  if (!(await verifyInstalledPackage(layout, identity)))
1141
1242
  return undefined;
1142
- await assertRegularFile(layout.markerPath, "managed runtime marker");
1143
- const marker = JSON.parse(await readFile(layout.markerPath, "utf8"));
1144
- const exactKeys = [
1145
- "schema",
1146
- "packageName",
1147
- "closureManifestSha256",
1148
- "executionProofSha256",
1149
- "packageVersion",
1150
- "cliSha256",
1151
- "sourceClosureSha256",
1152
- "nodeAbi",
1153
- "platform",
1154
- "arch",
1155
- "installedAt",
1156
- ];
1157
- const installedAtMs = typeof marker.installedAt === "string" ? Date.parse(marker.installedAt) : Number.NaN;
1158
- const valid = Object.keys(marker).length === exactKeys.length
1159
- && exactKeys.every((key) => Object.hasOwn(marker, key))
1160
- && marker.schema === "mono-agent.managed-runtime.v4"
1161
- && marker.packageName === PACKAGE_NAME
1162
- && marker.packageVersion === identity.packageVersion
1163
- && marker.cliSha256 === identity.cliSha256
1164
- && marker.sourceClosureSha256 === identity.sourceClosureSha256
1165
- && marker.nodeAbi === identity.nodeAbi
1166
- && marker.platform === identity.platform
1167
- && marker.arch === identity.arch
1168
- && typeof marker.closureManifestSha256 === "string"
1169
- && /^[0-9a-f]{64}$/u.test(marker.closureManifestSha256)
1170
- && typeof marker.executionProofSha256 === "string"
1171
- && /^[0-9a-f]{64}$/u.test(marker.executionProofSha256)
1172
- && typeof marker.installedAt === "string"
1173
- && Number.isFinite(installedAtMs)
1174
- && new Date(installedAtMs).toISOString() === marker.installedAt
1175
- // Promotion writes this sentinel before the final filesystem proof is
1176
- // rebound. It must remain unverifiable even when the filesystem records
1177
- // both writes with indistinguishable timestamp precision.
1178
- && marker.installedAt !== PROVISIONAL_RUNTIME_INSTALLED_AT
1179
- && await verifyClosureManifest(layout, marker.closureManifestSha256);
1180
- return valid ? marker : undefined;
1243
+ const marker = await readRuntimeMarker(layout.markerPath, identity);
1244
+ return marker !== undefined && await verifyClosureManifest(layout, marker.closureManifestSha256)
1245
+ ? marker
1246
+ : undefined;
1247
+ }
1248
+ catch {
1249
+ return undefined;
1250
+ }
1251
+ }
1252
+ async function readRuntimeMarker(path, identity) {
1253
+ let handle;
1254
+ try {
1255
+ handle = await open(path, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW | fsConstants.O_NONBLOCK);
1256
+ const before = await handle.stat({ bigint: true });
1257
+ if (!privateRuntimeMarkerFile(before))
1258
+ return undefined;
1259
+ const bytes = await handle.readFile();
1260
+ const [after, named] = await Promise.all([
1261
+ handle.stat({ bigint: true }),
1262
+ lstat(path, { bigint: true }),
1263
+ ]);
1264
+ if (!privateRuntimeMarkerFile(after)
1265
+ || !privateRuntimeMarkerFile(named)
1266
+ || !sameSourceStats(before, after)
1267
+ || !sameSourceStats(before, named)) {
1268
+ return undefined;
1269
+ }
1270
+ return runtimeMarkerFromJson(JSON.parse(bytes.toString("utf8")), identity);
1181
1271
  }
1182
1272
  catch {
1183
1273
  return undefined;
1184
1274
  }
1275
+ finally {
1276
+ await handle?.close();
1277
+ }
1278
+ }
1279
+ function privateRuntimeMarkerFile(details) {
1280
+ const owned = typeof process.getuid !== "function" || details.uid === BigInt(process.getuid());
1281
+ return details.isFile()
1282
+ && !details.isSymbolicLink()
1283
+ && details.nlink === 1n
1284
+ && details.size <= BigInt(MAX_RUNTIME_MARKER_BYTES)
1285
+ && (details.mode & 63n) === 0n
1286
+ && owned;
1287
+ }
1288
+ function runtimeMarkerFromJson(value, identity) {
1289
+ if (!isRecord(value))
1290
+ return undefined;
1291
+ const exactKeys = [
1292
+ "schema",
1293
+ "packageName",
1294
+ "closureManifestSha256",
1295
+ "executionProofSha256",
1296
+ "packageVersion",
1297
+ "cliSha256",
1298
+ "sourceClosureSha256",
1299
+ "nodeAbi",
1300
+ "platform",
1301
+ "arch",
1302
+ "installedAt",
1303
+ ];
1304
+ const installedAtMs = typeof value.installedAt === "string" ? Date.parse(value.installedAt) : Number.NaN;
1305
+ const valid = Object.keys(value).length === exactKeys.length
1306
+ && exactKeys.every((key) => Object.hasOwn(value, key))
1307
+ && value.schema === "mono-agent.managed-runtime.v4"
1308
+ && value.packageName === PACKAGE_NAME
1309
+ && value.packageVersion === identity.packageVersion
1310
+ && value.cliSha256 === identity.cliSha256
1311
+ && value.sourceClosureSha256 === identity.sourceClosureSha256
1312
+ && value.nodeAbi === identity.nodeAbi
1313
+ && value.platform === identity.platform
1314
+ && value.arch === identity.arch
1315
+ && typeof value.closureManifestSha256 === "string"
1316
+ && /^[0-9a-f]{64}$/u.test(value.closureManifestSha256)
1317
+ && typeof value.executionProofSha256 === "string"
1318
+ && /^[0-9a-f]{64}$/u.test(value.executionProofSha256)
1319
+ && typeof value.installedAt === "string"
1320
+ && Number.isFinite(installedAtMs)
1321
+ && new Date(installedAtMs).toISOString() === value.installedAt
1322
+ // Promotion writes this sentinel before the final filesystem proof is
1323
+ // rebound. It must remain unverifiable even when the filesystem records
1324
+ // both writes with indistinguishable timestamp precision.
1325
+ && value.installedAt !== PROVISIONAL_RUNTIME_INSTALLED_AT;
1326
+ return valid ? value : undefined;
1185
1327
  }
1186
1328
  async function verifyInstalledPackage(layout, identity) {
1187
1329
  try {
@@ -1226,37 +1368,45 @@ async function writeClosureManifest(layout) {
1226
1368
  await chmod(layout.closureManifestPath, 0o600);
1227
1369
  return sha256(Buffer.from(contents, "utf8"));
1228
1370
  }
1229
- async function verifyClosureManifest(layout, expectedSha256) {
1371
+ async function verifyClosureManifest(layout, expectedSha256, deps = {}) {
1230
1372
  try {
1231
1373
  await assertRegularFile(layout.closureManifestPath, "managed runtime closure manifest");
1232
- const contents = await readFile(layout.closureManifestPath);
1233
- if (sha256(contents) !== expectedSha256)
1374
+ const stored = await fingerprintClosureFile(layout.closureManifestPath, ".mono-agent-closure.json");
1375
+ if (stored.sha256 !== expectedSha256)
1234
1376
  return false;
1235
- const parsed = JSON.parse(contents.toString("utf8"));
1377
+ const parsed = JSON.parse(stored.bytes.toString("utf8"));
1236
1378
  if (parsed.schema !== "mono-agent.execution-closure.v1" || !Array.isArray(parsed.entries))
1237
1379
  return false;
1238
- const current = await captureRuntimeClosureManifest(layout);
1239
- return JSON.stringify(parsed) === JSON.stringify(current);
1380
+ const current = await captureRuntimeClosureManifest(layout, deps);
1381
+ const storedAfter = await captureFilesystemPathProof(layout.closureManifestPath, ".mono-agent-closure.json");
1382
+ return sameSourceProofEntry(stored.proof, storedAfter)
1383
+ && JSON.stringify(parsed) === JSON.stringify(current);
1240
1384
  }
1241
1385
  catch {
1242
1386
  return false;
1243
1387
  }
1244
1388
  }
1245
- async function captureRuntimeClosureManifest(layout) {
1389
+ async function captureRuntimeClosureManifest(layout, deps = {}) {
1246
1390
  const entries = [];
1391
+ const proofs = [];
1247
1392
  const excluded = new Set([resolve(layout.closureManifestPath), resolve(layout.markerPath)]);
1248
- const visit = async (directory) => {
1249
- const children = await readdir(directory, { withFileTypes: true });
1250
- children.sort((left, right) => compareCodeUnits(left.name, right.name));
1251
- for (const child of children) {
1252
- const path = join(directory, child.name);
1393
+ const visit = async (directory, logicalPath) => {
1394
+ const before = await captureFilesystemPathProof(directory, logicalPath);
1395
+ if (before.type !== "directory") {
1396
+ throw new Error(`Managed runtime closure directory ${logicalPath} changed type.`);
1397
+ }
1398
+ const childNames = (await readdir(directory)).sort(compareCodeUnits);
1399
+ for (const name of childNames) {
1400
+ const path = join(directory, name);
1253
1401
  if (excluded.has(resolve(path)))
1254
1402
  continue;
1255
- const details = await lstat(path);
1256
1403
  const pathRelative = portableRelativePath(layout.installRoot, path);
1257
- const mode = fileMode(details.mode);
1258
- if (details.isSymbolicLink()) {
1259
- const target = await readlink(path);
1404
+ const initial = await captureFilesystemPathProof(path, pathRelative);
1405
+ if (initial.type === "symlink") {
1406
+ const target = initial.target;
1407
+ if (target === undefined) {
1408
+ throw new Error(`Managed runtime symlink ${pathRelative} has no target proof.`);
1409
+ }
1260
1410
  if (isAbsolute(target)) {
1261
1411
  throw new Error(`Managed runtime symlink ${pathRelative} has an absolute target.`);
1262
1412
  }
@@ -1264,29 +1414,50 @@ async function captureRuntimeClosureManifest(layout) {
1264
1414
  entries.push({
1265
1415
  path: pathRelative,
1266
1416
  type: "symlink",
1267
- mode,
1268
- target: target.split(sep).join("/"),
1417
+ mode: fileMode(BigInt(initial.mode)),
1418
+ target,
1269
1419
  });
1420
+ proofs.push({ path, logicalPath: pathRelative, proof: initial });
1270
1421
  continue;
1271
1422
  }
1272
- if (details.isDirectory()) {
1273
- entries.push({ path: pathRelative, type: "directory", mode });
1274
- await visit(path);
1423
+ if (initial.type === "directory") {
1424
+ const proof = await visit(path, pathRelative);
1425
+ entries.push({ path: pathRelative, type: "directory", mode: fileMode(BigInt(proof.mode)) });
1275
1426
  continue;
1276
1427
  }
1277
- if (details.isFile()) {
1428
+ if (initial.type === "file") {
1429
+ const captured = await fingerprintClosureFile(path, pathRelative);
1278
1430
  entries.push({
1279
1431
  path: pathRelative,
1280
1432
  type: "file",
1281
- mode,
1282
- sha256: await fingerprintClosureFile(path, pathRelative),
1433
+ mode: fileMode(BigInt(captured.proof.mode)),
1434
+ sha256: captured.sha256,
1283
1435
  });
1436
+ proofs.push({ path, logicalPath: pathRelative, proof: captured.proof });
1284
1437
  continue;
1285
1438
  }
1286
1439
  throw new Error(`Managed runtime closure contains unsupported entry ${pathRelative}.`);
1287
1440
  }
1441
+ const [after, childNamesAfter] = await Promise.all([
1442
+ captureFilesystemPathProof(directory, logicalPath),
1443
+ readdir(directory).then((names) => names.sort(compareCodeUnits)),
1444
+ ]);
1445
+ if (after.type !== "directory"
1446
+ || !sameSourceProofEntry(before, after)
1447
+ || JSON.stringify(childNames) !== JSON.stringify(childNamesAfter)) {
1448
+ throw new Error(`Managed runtime closure directory ${logicalPath} changed while it was inspected.`);
1449
+ }
1450
+ proofs.push({ path: directory, logicalPath, proof: after });
1451
+ return after;
1288
1452
  };
1289
- await visit(layout.installRoot);
1453
+ await visit(layout.installRoot, ".");
1454
+ await deps.beforeFinalProof?.();
1455
+ for (const expected of proofs) {
1456
+ const current = await captureFilesystemPathProof(expected.path, expected.logicalPath);
1457
+ if (!sameSourceProofEntry(expected.proof, current)) {
1458
+ throw new Error(`Managed runtime closure entry ${expected.logicalPath} changed during closure capture.`);
1459
+ }
1460
+ }
1290
1461
  entries.sort((left, right) => compareCodeUnits(left.path, right.path));
1291
1462
  return { schema: "mono-agent.execution-closure.v1", entries };
1292
1463
  }
@@ -1302,24 +1473,31 @@ async function fingerprintClosureFile(path, pathRelative) {
1302
1473
  throw error;
1303
1474
  }
1304
1475
  try {
1305
- const before = await handle.stat();
1476
+ const before = await handle.stat({ bigint: true });
1306
1477
  if (!before.isFile())
1307
1478
  throw new Error(`Managed runtime entry ${pathRelative} is not a regular file.`);
1479
+ if (before.nlink !== 1n) {
1480
+ throw new Error(`Managed runtime file ${pathRelative} must not have additional hard links.`);
1481
+ }
1308
1482
  const bytes = await handle.readFile();
1309
- const [after, pathAfter] = await Promise.all([handle.stat(), lstat(path)]);
1483
+ const [after, pathAfter] = await Promise.all([
1484
+ handle.stat({ bigint: true }),
1485
+ lstat(path, { bigint: true }),
1486
+ ]);
1310
1487
  if (!after.isFile()
1311
1488
  || !pathAfter.isFile()
1312
1489
  || pathAfter.isSymbolicLink()
1313
- || before.dev !== after.dev
1314
- || before.ino !== after.ino
1315
- || before.size !== after.size
1316
- || before.mtimeMs !== after.mtimeMs
1317
- || before.ctimeMs !== after.ctimeMs
1318
- || after.dev !== pathAfter.dev
1319
- || after.ino !== pathAfter.ino) {
1490
+ || after.nlink !== 1n
1491
+ || pathAfter.nlink !== 1n
1492
+ || !sameSourceStats(before, after)
1493
+ || !sameSourceStats(after, pathAfter)) {
1320
1494
  throw new Error(`Managed runtime file ${pathRelative} changed while it was fingerprinted.`);
1321
1495
  }
1322
- return sha256(bytes);
1496
+ return {
1497
+ sha256: sha256(bytes),
1498
+ proof: sourceProofEntry(pathRelative, "file", after),
1499
+ bytes,
1500
+ };
1323
1501
  }
1324
1502
  finally {
1325
1503
  await handle.close();