@b00dea/cockpit 0.0.0-stage → 0.1.1

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 (426) hide show
  1. package/CHANGELOG.md +474 -0
  2. package/LICENSE +21 -0
  3. package/README.md +25 -2
  4. package/bin/cockpit.mjs +33 -0
  5. package/cockpit.schema.json +2644 -0
  6. package/dist/autonomy.d.ts +45 -0
  7. package/dist/autonomy.js +173 -0
  8. package/dist/bounded-read.d.ts +14 -0
  9. package/dist/bounded-read.js +42 -0
  10. package/dist/browser/commands.d.ts +117 -0
  11. package/dist/browser/commands.js +453 -0
  12. package/dist/browser/package-paths.d.ts +1 -0
  13. package/dist/browser/package-paths.js +6 -0
  14. package/dist/browser/proxy.d.ts +22 -0
  15. package/dist/browser/proxy.js +37 -0
  16. package/dist/browser/runtime.d.ts +118 -0
  17. package/dist/browser/runtime.js +186 -0
  18. package/dist/browser/surfaces.d.ts +141 -0
  19. package/dist/browser/surfaces.js +561 -0
  20. package/dist/browser/ui-gate.d.ts +15 -0
  21. package/dist/browser/ui-gate.js +18 -0
  22. package/dist/canonical-json.d.ts +2 -0
  23. package/dist/canonical-json.js +5 -0
  24. package/dist/cli-guard.d.ts +5 -0
  25. package/dist/cli-guard.js +71 -0
  26. package/dist/cli-main.d.ts +176 -0
  27. package/dist/cli-main.js +1584 -0
  28. package/dist/cli.d.ts +3 -0
  29. package/dist/cli.js +16 -0
  30. package/dist/command-table.d.ts +121 -0
  31. package/dist/command-table.js +664 -0
  32. package/dist/config/load.d.ts +183 -0
  33. package/dist/config/load.js +367 -0
  34. package/dist/config/sibling-toml.d.ts +48 -0
  35. package/dist/config/sibling-toml.js +160 -0
  36. package/dist/config/surface-tools-toml.d.ts +9 -0
  37. package/dist/config/surface-tools-toml.js +37 -0
  38. package/dist/config/unconfirmed-toml.d.ts +9 -0
  39. package/dist/config/unconfirmed-toml.js +29 -0
  40. package/dist/config/verified-write.d.ts +43 -0
  41. package/dist/config/verified-write.js +89 -0
  42. package/dist/diagnostics.d.ts +26 -0
  43. package/dist/diagnostics.js +344 -0
  44. package/dist/docs/check.d.ts +16 -0
  45. package/dist/docs/check.js +197 -0
  46. package/dist/docs/decision-record.d.ts +34 -0
  47. package/dist/docs/decision-record.js +135 -0
  48. package/dist/docs/decisions.d.ts +16 -0
  49. package/dist/docs/decisions.js +103 -0
  50. package/dist/docs/markdown.d.ts +36 -0
  51. package/dist/docs/markdown.js +88 -0
  52. package/dist/docs/references.d.ts +32 -0
  53. package/dist/docs/references.js +148 -0
  54. package/dist/emit.d.ts +22 -0
  55. package/dist/emit.js +43 -0
  56. package/dist/entrypoint-kinds.d.ts +3 -0
  57. package/dist/entrypoint-kinds.js +11 -0
  58. package/dist/environment/application/confirm.d.ts +35 -0
  59. package/dist/environment/application/confirm.js +122 -0
  60. package/dist/environment/application/destructive.d.ts +6 -0
  61. package/dist/environment/application/destructive.js +429 -0
  62. package/dist/environment/application/doctor.d.ts +94 -0
  63. package/dist/environment/application/doctor.js +247 -0
  64. package/dist/environment/application/guard.d.ts +49 -0
  65. package/dist/environment/application/guard.js +677 -0
  66. package/dist/environment/application/health.d.ts +143 -0
  67. package/dist/environment/application/health.js +385 -0
  68. package/dist/environment/application/lifecycle-context.d.ts +235 -0
  69. package/dist/environment/application/lifecycle-context.js +154 -0
  70. package/dist/environment/application/lifecycle-state.d.ts +25 -0
  71. package/dist/environment/application/lifecycle-state.js +202 -0
  72. package/dist/environment/application/lifecycle.d.ts +18 -0
  73. package/dist/environment/application/lifecycle.js +737 -0
  74. package/dist/environment/application/logs.d.ts +45 -0
  75. package/dist/environment/application/logs.js +127 -0
  76. package/dist/environment/application/services.d.ts +60 -0
  77. package/dist/environment/application/services.js +89 -0
  78. package/dist/environment/application/setup.d.ts +129 -0
  79. package/dist/environment/application/setup.js +741 -0
  80. package/dist/environment/application/shared.d.ts +11 -0
  81. package/dist/environment/application/shared.js +61 -0
  82. package/dist/environment/application/siblings.d.ts +43 -0
  83. package/dist/environment/application/siblings.js +269 -0
  84. package/dist/environment/claims.d.ts +389 -0
  85. package/dist/environment/claims.js +1701 -0
  86. package/dist/environment/codex-hook-trust.d.ts +11 -0
  87. package/dist/environment/codex-hook-trust.js +185 -0
  88. package/dist/environment/compose-environment.d.ts +1 -0
  89. package/dist/environment/compose-environment.js +6 -0
  90. package/dist/environment/compose-ownership.d.ts +21 -0
  91. package/dist/environment/compose-ownership.js +117 -0
  92. package/dist/environment/compose-topology.d.ts +125 -0
  93. package/dist/environment/compose-topology.js +490 -0
  94. package/dist/environment/contract.d.ts +522 -0
  95. package/dist/environment/contract.js +794 -0
  96. package/dist/environment/declaration.d.ts +14 -0
  97. package/dist/environment/declaration.js +39 -0
  98. package/dist/environment/destructive-composition.d.ts +29 -0
  99. package/dist/environment/destructive-composition.js +333 -0
  100. package/dist/environment/docker-daemon.d.ts +30 -0
  101. package/dist/environment/docker-daemon.js +78 -0
  102. package/dist/environment/docker-endpoint.d.ts +10 -0
  103. package/dist/environment/docker-endpoint.js +19 -0
  104. package/dist/environment/env-schema-observation.d.ts +46 -0
  105. package/dist/environment/env-schema-observation.js +241 -0
  106. package/dist/environment/evidence.d.ts +136 -0
  107. package/dist/environment/evidence.js +437 -0
  108. package/dist/environment/executable.d.ts +38 -0
  109. package/dist/environment/executable.js +190 -0
  110. package/dist/environment/generated-application/authorities.d.ts +5 -0
  111. package/dist/environment/generated-application/authorities.js +61 -0
  112. package/dist/environment/generated-application/bootstrap-skill.d.ts +16 -0
  113. package/dist/environment/generated-application/bootstrap-skill.js +328 -0
  114. package/dist/environment/generated-application/check.d.ts +13 -0
  115. package/dist/environment/generated-application/check.js +191 -0
  116. package/dist/environment/generated-application/custom-surface.d.ts +26 -0
  117. package/dist/environment/generated-application/custom-surface.js +289 -0
  118. package/dist/environment/generated-application/prepare.d.ts +29 -0
  119. package/dist/environment/generated-application/prepare.js +278 -0
  120. package/dist/environment/generated-application/publication.d.ts +33 -0
  121. package/dist/environment/generated-application/publication.js +547 -0
  122. package/dist/environment/generated-application/shared.d.ts +492 -0
  123. package/dist/environment/generated-application/shared.js +354 -0
  124. package/dist/environment/generated-application/surface-observation.d.ts +4 -0
  125. package/dist/environment/generated-application/surface-observation.js +69 -0
  126. package/dist/environment/generated-application/surface-outputs.d.ts +71 -0
  127. package/dist/environment/generated-application/surface-outputs.js +400 -0
  128. package/dist/environment/generated-application/surfaces.d.ts +3 -0
  129. package/dist/environment/generated-application/surfaces.js +704 -0
  130. package/dist/environment/generated-application/sync.d.ts +6 -0
  131. package/dist/environment/generated-application/sync.js +124 -0
  132. package/dist/environment/git-observation.d.ts +202 -0
  133. package/dist/environment/git-observation.js +553 -0
  134. package/dist/environment/health-composition.d.ts +12 -0
  135. package/dist/environment/health-composition.js +645 -0
  136. package/dist/environment/host-observation.d.ts +56 -0
  137. package/dist/environment/host-observation.js +52 -0
  138. package/dist/environment/identity.d.ts +48 -0
  139. package/dist/environment/identity.js +85 -0
  140. package/dist/environment/init-application.d.ts +333 -0
  141. package/dist/environment/init-application.js +726 -0
  142. package/dist/environment/install-policy-observation.d.ts +71 -0
  143. package/dist/environment/install-policy-observation.js +284 -0
  144. package/dist/environment/install-policy.d.ts +48 -0
  145. package/dist/environment/install-policy.js +96 -0
  146. package/dist/environment/instance.d.ts +33 -0
  147. package/dist/environment/instance.js +122 -0
  148. package/dist/environment/instruction-pointer.d.ts +19 -0
  149. package/dist/environment/instruction-pointer.js +144 -0
  150. package/dist/environment/lifecycle-composition.d.ts +31 -0
  151. package/dist/environment/lifecycle-composition.js +742 -0
  152. package/dist/environment/lifecycle-stage.d.ts +120 -0
  153. package/dist/environment/lifecycle-stage.js +408 -0
  154. package/dist/environment/lifecycle.d.ts +75 -0
  155. package/dist/environment/lifecycle.js +354 -0
  156. package/dist/environment/logs.d.ts +132 -0
  157. package/dist/environment/logs.js +519 -0
  158. package/dist/environment/map-limit.d.ts +2 -0
  159. package/dist/environment/map-limit.js +13 -0
  160. package/dist/environment/mcp-observation.d.ts +9 -0
  161. package/dist/environment/mcp-observation.js +46 -0
  162. package/dist/environment/observations.d.ts +46 -0
  163. package/dist/environment/observations.js +346 -0
  164. package/dist/environment/operator-application.d.ts +79 -0
  165. package/dist/environment/operator-application.js +197 -0
  166. package/dist/environment/ports.d.ts +182 -0
  167. package/dist/environment/ports.js +1205 -0
  168. package/dist/environment/posture-observation.d.ts +68 -0
  169. package/dist/environment/posture-observation.js +253 -0
  170. package/dist/environment/presence.d.ts +4 -0
  171. package/dist/environment/presence.js +26 -0
  172. package/dist/environment/read-composition.d.ts +32 -0
  173. package/dist/environment/read-composition.js +397 -0
  174. package/dist/environment/readiness-composition.d.ts +57 -0
  175. package/dist/environment/readiness-composition.js +151 -0
  176. package/dist/environment/reclaim.d.ts +370 -0
  177. package/dist/environment/reclaim.js +1940 -0
  178. package/dist/environment/repository-identity.d.ts +13 -0
  179. package/dist/environment/repository-identity.js +108 -0
  180. package/dist/environment/runtime-measurement-record.d.ts +38 -0
  181. package/dist/environment/runtime-measurement-record.js +111 -0
  182. package/dist/environment/runtime-measurement.d.ts +27 -0
  183. package/dist/environment/runtime-measurement.js +224 -0
  184. package/dist/environment/runtime-probe-residue.d.ts +43 -0
  185. package/dist/environment/runtime-probe-residue.js +148 -0
  186. package/dist/environment/runtime-probe.d.ts +72 -0
  187. package/dist/environment/runtime-probe.js +725 -0
  188. package/dist/environment/runtime.d.ts +551 -0
  189. package/dist/environment/runtime.js +2405 -0
  190. package/dist/environment/seed.d.ts +16 -0
  191. package/dist/environment/seed.js +43 -0
  192. package/dist/environment/selftest-application.d.ts +86 -0
  193. package/dist/environment/selftest-application.js +713 -0
  194. package/dist/environment/sensitive.d.ts +3 -0
  195. package/dist/environment/sensitive.js +48 -0
  196. package/dist/environment/served-checkout-grants.d.ts +53 -0
  197. package/dist/environment/served-checkout-grants.js +182 -0
  198. package/dist/environment/setup-observation.d.ts +62 -0
  199. package/dist/environment/setup-observation.js +1473 -0
  200. package/dist/environment/setup.d.ts +43 -0
  201. package/dist/environment/setup.js +176 -0
  202. package/dist/environment/sibling-checkouts.d.ts +119 -0
  203. package/dist/environment/sibling-checkouts.js +254 -0
  204. package/dist/environment/sibling-instructions.d.ts +33 -0
  205. package/dist/environment/sibling-instructions.js +224 -0
  206. package/dist/environment/sibling-observation.d.ts +27 -0
  207. package/dist/environment/sibling-observation.js +45 -0
  208. package/dist/environment/sibling-pull.d.ts +23 -0
  209. package/dist/environment/sibling-pull.js +95 -0
  210. package/dist/environment/sibling-registration.d.ts +45 -0
  211. package/dist/environment/sibling-registration.js +538 -0
  212. package/dist/environment/stack-inputs.d.ts +18 -0
  213. package/dist/environment/stack-inputs.js +210 -0
  214. package/dist/environment/stack-inventory-prune.d.ts +31 -0
  215. package/dist/environment/stack-inventory-prune.js +129 -0
  216. package/dist/environment/stack-inventory-report.d.ts +44 -0
  217. package/dist/environment/stack-inventory-report.js +78 -0
  218. package/dist/environment/stack-inventory.d.ts +94 -0
  219. package/dist/environment/stack-inventory.js +418 -0
  220. package/dist/environment/state.d.ts +410 -0
  221. package/dist/environment/state.js +2153 -0
  222. package/dist/environment/templates.d.ts +140 -0
  223. package/dist/environment/templates.js +685 -0
  224. package/dist/environment/tool-registration.d.ts +9 -0
  225. package/dist/environment/tool-registration.js +92 -0
  226. package/dist/environment/upgrade-application.d.ts +19 -0
  227. package/dist/environment/upgrade-application.js +175 -0
  228. package/dist/environment-exec/execute.d.ts +105 -0
  229. package/dist/environment-exec/execute.js +1179 -0
  230. package/dist/environment-exec/libexec-program.d.ts +6 -0
  231. package/dist/environment-exec/libexec-program.js +22 -0
  232. package/dist/environment-exec/verified-digest.d.ts +4 -0
  233. package/dist/environment-exec/verified-digest.js +16 -0
  234. package/dist/exact-keys.d.ts +7 -0
  235. package/dist/exact-keys.js +15 -0
  236. package/dist/fix-catalogue.d.ts +32 -0
  237. package/dist/fix-catalogue.js +60 -0
  238. package/dist/freshness.d.ts +29 -0
  239. package/dist/freshness.js +75 -0
  240. package/dist/generated-environment/adopt.d.ts +16 -0
  241. package/dist/generated-environment/adopt.js +271 -0
  242. package/dist/generated-environment/command.d.ts +47 -0
  243. package/dist/generated-environment/command.js +75 -0
  244. package/dist/generated-environment/transaction.d.ts +186 -0
  245. package/dist/generated-environment/transaction.js +1770 -0
  246. package/dist/grading/evaluate.d.ts +71 -0
  247. package/dist/grading/evaluate.js +403 -0
  248. package/dist/grading/grade.d.ts +33 -0
  249. package/dist/grading/grade.js +57 -0
  250. package/dist/guards/answer-confirmation.d.ts +41 -0
  251. package/dist/guards/answer-confirmation.js +109 -0
  252. package/dist/guards/evaluator.d.ts +130 -0
  253. package/dist/guards/evaluator.js +1258 -0
  254. package/dist/guards/harness-adapter.d.ts +17 -0
  255. package/dist/guards/harness-adapter.js +57 -0
  256. package/dist/guards/host.d.ts +8 -0
  257. package/dist/guards/host.js +10 -0
  258. package/dist/guards/invocation-parser.d.ts +66 -0
  259. package/dist/guards/invocation-parser.js +595 -0
  260. package/dist/guards/local-identity.d.ts +44 -0
  261. package/dist/guards/local-identity.js +129 -0
  262. package/dist/guards/policy-change.d.ts +43 -0
  263. package/dist/guards/policy-change.js +159 -0
  264. package/dist/guards/policy-pin.d.ts +13 -0
  265. package/dist/guards/policy-pin.js +177 -0
  266. package/dist/guards/protected-locations.d.ts +62 -0
  267. package/dist/guards/protected-locations.js +461 -0
  268. package/dist/guards/read-only.d.ts +37 -0
  269. package/dist/guards/read-only.js +315 -0
  270. package/dist/init/command.d.ts +41 -0
  271. package/dist/init/command.js +330 -0
  272. package/dist/init/derive.d.ts +53 -0
  273. package/dist/init/derive.js +819 -0
  274. package/dist/init/draft.d.ts +140 -0
  275. package/dist/init/draft.js +1228 -0
  276. package/dist/init/render.d.ts +47 -0
  277. package/dist/init/render.js +104 -0
  278. package/dist/init/scaffold.d.ts +53 -0
  279. package/dist/init/scaffold.js +121 -0
  280. package/dist/invocation.d.ts +13 -0
  281. package/dist/invocation.js +20 -0
  282. package/dist/package-metadata.d.ts +9 -0
  283. package/dist/package-metadata.js +19 -0
  284. package/dist/readiness.d.ts +53 -0
  285. package/dist/readiness.js +88 -0
  286. package/dist/references/named.d.ts +24 -0
  287. package/dist/references/named.js +27 -0
  288. package/dist/refusal.d.ts +15 -0
  289. package/dist/refusal.js +7 -0
  290. package/dist/registry-derivation.d.ts +31 -0
  291. package/dist/registry-derivation.js +32 -0
  292. package/dist/registry-resolve/command.d.ts +47 -0
  293. package/dist/registry-resolve/command.js +226 -0
  294. package/dist/registry-resolve/registry.d.ts +68 -0
  295. package/dist/registry-resolve/registry.js +292 -0
  296. package/dist/registry-resolve/render-shell.d.ts +18 -0
  297. package/dist/registry-resolve/render-shell.js +36 -0
  298. package/dist/repair.d.ts +29 -0
  299. package/dist/repair.js +8 -0
  300. package/dist/repository-path.d.ts +9 -0
  301. package/dist/repository-path.js +16 -0
  302. package/dist/schema-json.d.ts +1 -0
  303. package/dist/schema-json.js +177 -0
  304. package/dist/schema.d.ts +1289 -0
  305. package/dist/schema.js +985 -0
  306. package/dist/setup/command.d.ts +4 -0
  307. package/dist/setup/command.js +4 -0
  308. package/dist/sha256.d.ts +1 -0
  309. package/dist/sha256.js +4 -0
  310. package/dist/surfaces/adoption.d.ts +41 -0
  311. package/dist/surfaces/adoption.js +217 -0
  312. package/dist/surfaces/consumer-instructions.d.ts +20 -0
  313. package/dist/surfaces/consumer-instructions.js +43 -0
  314. package/dist/surfaces/contract.d.ts +42 -0
  315. package/dist/surfaces/contract.js +169 -0
  316. package/dist/surfaces/core-declarations.d.ts +24 -0
  317. package/dist/surfaces/core-declarations.js +79 -0
  318. package/dist/surfaces/development-markers.d.ts +1 -0
  319. package/dist/surfaces/development-markers.js +18 -0
  320. package/dist/surfaces/generator-adapter.d.ts +71 -0
  321. package/dist/surfaces/generator-adapter.js +809 -0
  322. package/dist/surfaces/harness-registry.d.ts +260 -0
  323. package/dist/surfaces/harness-registry.js +146 -0
  324. package/dist/surfaces/mcp.d.ts +145 -0
  325. package/dist/surfaces/mcp.js +349 -0
  326. package/dist/surfaces/merge.d.ts +30 -0
  327. package/dist/surfaces/merge.js +105 -0
  328. package/dist/surfaces/ownership.d.ts +190 -0
  329. package/dist/surfaces/ownership.js +795 -0
  330. package/dist/surfaces/projection.d.ts +136 -0
  331. package/dist/surfaces/projection.js +716 -0
  332. package/dist/surfaces/render.d.ts +79 -0
  333. package/dist/surfaces/render.js +636 -0
  334. package/dist/surfaces/restrictions.d.ts +130 -0
  335. package/dist/surfaces/restrictions.js +447 -0
  336. package/dist/surfaces/selftest.d.ts +162 -0
  337. package/dist/surfaces/selftest.js +393 -0
  338. package/dist/surfaces/skill-contract.d.ts +85 -0
  339. package/dist/surfaces/skill-contract.js +449 -0
  340. package/dist/surfaces/sync.d.ts +57 -0
  341. package/dist/surfaces/sync.js +164 -0
  342. package/dist/team-metadata.d.ts +41 -0
  343. package/dist/team-metadata.js +513 -0
  344. package/dist/testing/gates.d.ts +91 -0
  345. package/dist/testing/gates.js +182 -0
  346. package/dist/testing/hooks.d.ts +59 -0
  347. package/dist/testing/hooks.js +139 -0
  348. package/dist/testing/test-protection.d.ts +34 -0
  349. package/dist/testing/test-protection.js +137 -0
  350. package/dist/testing/verdict.d.ts +29 -0
  351. package/dist/testing/verdict.js +62 -0
  352. package/dist/testing/weakening.d.ts +15 -0
  353. package/dist/testing/weakening.js +30 -0
  354. package/dist/work-orders/application-closure.d.ts +7 -0
  355. package/dist/work-orders/application-closure.js +76 -0
  356. package/dist/work-orders/application-context.d.ts +162 -0
  357. package/dist/work-orders/application-context.js +137 -0
  358. package/dist/work-orders/application-decisions.d.ts +27 -0
  359. package/dist/work-orders/application-decisions.js +121 -0
  360. package/dist/work-orders/application-grading.d.ts +11 -0
  361. package/dist/work-orders/application-grading.js +212 -0
  362. package/dist/work-orders/application.d.ts +6 -0
  363. package/dist/work-orders/application.js +223 -0
  364. package/dist/work-orders/composition.d.ts +101 -0
  365. package/dist/work-orders/composition.js +1202 -0
  366. package/dist/work-orders/contract.d.ts +77 -0
  367. package/dist/work-orders/contract.js +140 -0
  368. package/dist/work-orders/decisions.d.ts +23 -0
  369. package/dist/work-orders/decisions.js +55 -0
  370. package/dist/work-orders/store.d.ts +207 -0
  371. package/dist/work-orders/store.js +291 -0
  372. package/docs/README.md +68 -0
  373. package/docs/architecture.md +136 -0
  374. package/docs/capability-matrix.md +58 -0
  375. package/docs/cli-reference.md +807 -0
  376. package/docs/command-list.md +62 -0
  377. package/docs/compose-patterns.md +99 -0
  378. package/docs/concepts.md +173 -0
  379. package/docs/configuration-essentials.md +177 -0
  380. package/docs/configuration-reference.md +636 -0
  381. package/docs/docs-check.md +76 -0
  382. package/docs/environment.md +948 -0
  383. package/docs/extending.md +43 -0
  384. package/docs/faq.md +113 -0
  385. package/docs/getting-started.md +692 -0
  386. package/docs/harness-surfaces.md +500 -0
  387. package/docs/joining-a-team.md +76 -0
  388. package/docs/release-policy.md +49 -0
  389. package/docs/safety-and-autonomy.md +463 -0
  390. package/docs/troubleshooting.md +382 -0
  391. package/docs/tutorial.md +171 -0
  392. package/docs/work-and-browser.md +420 -0
  393. package/libexec/browser-operation.cjs +270 -0
  394. package/libexec/fence-marker.cjs +101 -0
  395. package/libexec/fence-marker.d.cts +24 -0
  396. package/libexec/fenced-group-runner.cjs +77 -0
  397. package/libexec/fenced-supervisor.cjs +137 -0
  398. package/libexec/generated-filesystem.cjs +731 -0
  399. package/libexec/install-filesystem.cjs +225 -0
  400. package/libexec/rulesync-generator.NOTICE.md +1112 -0
  401. package/libexec/rulesync-generator.cjs +743 -0
  402. package/libexec/surface-generator-worker.cjs +266 -0
  403. package/libexec/surface-output-reader.cjs +99 -0
  404. package/libexec/surface-scratch-cleanup.cjs +14 -0
  405. package/libexec/validate-harness-evidence.d.mts +183 -0
  406. package/libexec/validate-harness-evidence.mjs +1312 -0
  407. package/package.json +74 -4
  408. package/surfaces/README.md +33 -0
  409. package/surfaces/agents/evaluator.agent.md +76 -0
  410. package/surfaces/guards/bash-guard.sh +48 -0
  411. package/surfaces/guards/claude.adapter.json +25 -0
  412. package/surfaces/guards/codex.adapter.json +24 -0
  413. package/surfaces/guards/opencode.adapter.json +25 -0
  414. package/surfaces/references/browser.md +63 -0
  415. package/surfaces/references/test-data-and-personas.md +84 -0
  416. package/surfaces/references/testing-tiers.md +11 -0
  417. package/surfaces/skills/cockpit-browser.skill.md +59 -0
  418. package/surfaces/skills/cockpit-explain.skill.md +32 -0
  419. package/surfaces/skills/cockpit-fix.skill.md +37 -0
  420. package/surfaces/skills/cockpit-setup.skill.md +39 -0
  421. package/surfaces/skills/cockpit-work.skill.md +34 -0
  422. package/surfaces/skills/collection.json +5 -0
  423. package/surfaces/skills/procedures/explain.md +66 -0
  424. package/surfaces/skills/procedures/fix.md +95 -0
  425. package/surfaces/skills/procedures/setup.md +452 -0
  426. package/surfaces/skills/procedures/work.md +134 -0
@@ -0,0 +1,1701 @@
1
+ import { z } from 'zod';
2
+ import { lstat, opendir, readFile } from 'node:fs/promises';
3
+ import { dirname, isAbsolute, join, normalize } from 'node:path';
4
+ import { createHash, randomBytes } from 'node:crypto';
5
+ import { platform } from 'node:os';
6
+ import { STRUCTURED_INPUT_ENTRY_LIMIT } from '../schema.js';
7
+ import { canonicalJson } from '../canonical-json.js';
8
+ import { hasErrorCode, RefusalError, ENVIRONMENT_CODES, errorCause, isCanonicalTimestamp, snapshotJson, snapshotExecutionPlan, snapshotEnvironmentObservation, } from './contract.js';
9
+ import { authenticatedExecutionOutcome, consumeFencedExecutionTermination } from '../environment-exec/execute.js';
10
+ import { resolveExecutablePath } from './executable.js';
11
+ import { abortStateTransaction, authenticateStateNamespace, inspectStateLock, parseCoordinationStateEntry, readState, SERIALIZED_RECORD_BYTE_LIMIT, snapshotStateNamespace, transactState, withStateNamespaceInventory, } from './state.js';
12
+ const OWNER = 'environment-claims';
13
+ export const observationDeclarations = Object.freeze([
14
+ { id: 'claim-state', owner: OWNER, postures: ['environment'], stages: [{ stage: 'bootstrap', disposition: 'not-applicable', reason: 'claims do not exist before the first seed' }, { stage: 'steady', disposition: 'required' }], code: ENVIRONMENT_CODES.CLAIM_STATE_UNHEALTHY, isIssued: isClaimObservation, issuedAt: observationIssuedAt },
15
+ ]);
16
+ const issuedObservations = new WeakMap();
17
+ function issueObservation(value) {
18
+ const snapshot = snapshotEnvironmentObservation(value);
19
+ if (!snapshot || snapshot.owner !== OWNER)
20
+ throw new RefusalError('claim observation is malformed');
21
+ issuedObservations.set(snapshot, Date.now());
22
+ return snapshot;
23
+ }
24
+ function isClaimObservation(value) {
25
+ return value !== null && typeof value === 'object' && issuedObservations.has(value);
26
+ }
27
+ function observationIssuedAt(value) {
28
+ return value !== null && typeof value === 'object' ? issuedObservations.get(value) : undefined;
29
+ }
30
+ export async function observeClaimState(input) {
31
+ const namespace = snapshotStateNamespace(input.namespace);
32
+ if (!namespace || !/^stack:[a-f0-9]{64}$/.test(input.stackId) || !isCanonicalTimestamp(input.observedAt)) {
33
+ throw new RefusalError('claim observation input is malformed');
34
+ }
35
+ const inventory = await readActiveStackInventory(namespace);
36
+ const refusal = inventory.kind === 'refusal' ? inventory.diagnostics[0] : undefined;
37
+ return issueObservation({
38
+ code: ENVIRONMENT_CODES.CLAIM_STATE_UNHEALTHY,
39
+ owner: OWNER,
40
+ stackId: input.stackId,
41
+ source: namespace.paths.coordination(input.stackId).path,
42
+ observed: refusal
43
+ ? { state: 'failed', reason: refusal.message, observedAt: input.observedAt }
44
+ : { state: 'available', value: true, observedAt: input.observedAt },
45
+ expectation: { kind: 'equal', value: true },
46
+ applicability: { state: 'required', posture: 'environment' },
47
+ freshness: { state: 'current' },
48
+ remedy: 'repair or release malformed coordination state',
49
+ });
50
+ }
51
+ const holderIdentity = /^holder:[a-f0-9]{64}:session$/;
52
+ const checkoutIdentity = /^checkout:[a-f0-9]{64}$/;
53
+ const stackIdentity = /^stack:[a-f0-9]{64}$/;
54
+ const sha256Identity = /^[a-f0-9]{64}$/;
55
+ const ClaimHolderSchema = z.object({
56
+ holderId: z.string().regex(holderIdentity),
57
+ checkoutId: z.string().regex(checkoutIdentity),
58
+ claimedAt: z.string().refine(isCanonicalTimestamp),
59
+ pid: z.number().int().positive().max(2_147_483_647).optional(),
60
+ processStart: z.string().refine(isProcessStart).optional(),
61
+ }).strict().refine((holder) => holder.processStart === undefined || holder.pid !== undefined);
62
+ const PortLeaseRecordSchema = z.object({
63
+ offset: z.number().int().nonnegative().safe(),
64
+ ports: z.record(z.string().regex(/^[A-Z][A-Z0-9_]*$/), z.number().int().min(1).max(65535))
65
+ .refine((ports) => Object.keys(ports).length > 0)
66
+ .refine((ports) => new Set(Object.values(ports)).size === Object.keys(ports).length),
67
+ released: z.boolean(),
68
+ declaration: z.object({
69
+ step: z.number().int().positive().safe(),
70
+ count: z.number().int().positive().safe(),
71
+ bases: z.record(z.string().regex(/^[A-Z][A-Z0-9_]*$/), z.number().int().min(1).max(65535))
72
+ .refine((bases) => Object.keys(bases).length > 0)
73
+ .refine((bases) => new Set(Object.values(bases)).size === Object.keys(bases).length),
74
+ }).strict(),
75
+ }).strict().superRefine((lease, context) => {
76
+ const portKeys = Object.keys(lease.ports).sort();
77
+ const baseKeys = Object.keys(lease.declaration.bases).sort();
78
+ const candidate = lease.offset % lease.declaration.step === 0
79
+ && lease.offset / lease.declaration.step < lease.declaration.count;
80
+ const derived = baseKeys.every((key) => lease.ports[key] === lease.declaration.bases[key] + lease.offset);
81
+ if (!candidate || portKeys.join('\0') !== baseKeys.join('\0') || !derived) {
82
+ context.addIssue({ code: 'custom', message: 'lease does not match its declaration' });
83
+ }
84
+ });
85
+ const DrainAttemptSchema = z.object({
86
+ attemptId: z.string().regex(sha256Identity),
87
+ reservationId: z.string().regex(sha256Identity),
88
+ purpose: z.enum(['sleep', 'down', 'reset']),
89
+ targetIds: z.array(z.string().min(1)).min(1).max(STRUCTURED_INPUT_ENTRY_LIMIT),
90
+ generation: z.string().regex(sha256Identity),
91
+ actionIndex: z.number().int().nonnegative().safe(),
92
+ currentTarget: z.string().min(1),
93
+ currentStage: z.enum(['runtime-stop', 'reclaim']),
94
+ committedRevision: z.number().int().positive().safe(),
95
+ committedAt: z.string().refine(isCanonicalTimestamp),
96
+ validThrough: z.string().refine(isCanonicalTimestamp),
97
+ status: z.enum(['active', 'failed']),
98
+ failedRevision: z.number().int().positive().safe().nullable(),
99
+ failedAt: z.string().refine(isCanonicalTimestamp).nullable(),
100
+ }).strict().superRefine((attempt, context) => {
101
+ if (new Set(attempt.targetIds).size !== attempt.targetIds.length
102
+ || attempt.targetIds.some((target, index) => Buffer.byteLength(target, 'utf8') > 512
103
+ || target.includes('\0') || index > 0 && attempt.targetIds[index - 1] >= target)
104
+ || Buffer.byteLength(attempt.currentTarget, 'utf8') > 512 || attempt.currentTarget.includes('\0')
105
+ || Date.parse(attempt.validThrough) <= Date.parse(attempt.committedAt)
106
+ || attempt.status === 'active' && (attempt.failedRevision !== null || attempt.failedAt !== null)
107
+ || attempt.status === 'failed' && (attempt.failedRevision !== attempt.committedRevision + 1
108
+ || attempt.failedAt === null || Date.parse(attempt.failedAt) < Date.parse(attempt.committedAt))) {
109
+ context.addIssue({ code: 'custom', message: 'drain attempt targets are not canonical' });
110
+ }
111
+ });
112
+ function isCanonicalJson(body) {
113
+ try {
114
+ return canonicalJson(JSON.parse(body)) === body;
115
+ }
116
+ catch {
117
+ // Unparseable JSON is simply not canonical.
118
+ return false;
119
+ }
120
+ }
121
+ const DrainTargetCommitmentSchema = z.object({
122
+ format: z.literal('cockpit-reclaim-target/v1'),
123
+ sha256: z.string().regex(sha256Identity),
124
+ body: z.string().min(2).max(32_768),
125
+ }).strict().superRefine((commitment, context) => {
126
+ if (Buffer.byteLength(commitment.body, 'utf8') > 32_768
127
+ || createHash('sha256').update(commitment.body).digest('hex') !== commitment.sha256
128
+ || !isCanonicalJson(commitment.body)) {
129
+ context.addIssue({ code: 'custom', message: 'drain target commitment is malformed' });
130
+ }
131
+ });
132
+ const DrainActionSchema = z.discriminatedUnion('stage', [
133
+ z.object({
134
+ stage: z.literal('runtime-stop'),
135
+ targetId: z.string().min(1),
136
+ validForMs: z.number().int().min(1).max(300_000).safe(),
137
+ }).strict(),
138
+ z.object({
139
+ stage: z.literal('reclaim'),
140
+ targetId: z.string().min(1),
141
+ validForMs: z.number().int().min(1).max(300_000).safe(),
142
+ targetCommitment: DrainTargetCommitmentSchema,
143
+ }).strict(),
144
+ ]);
145
+ const DrainReservationSchema = z.object({
146
+ purpose: z.enum(['sleep', 'down', 'reset']),
147
+ stackId: z.string().regex(stackIdentity),
148
+ holderId: z.string().regex(holderIdentity),
149
+ requesterId: z.string().regex(holderIdentity),
150
+ configSha256: z.string().regex(sha256Identity),
151
+ planDigest: z.string().regex(sha256Identity),
152
+ operationBasis: z.string().regex(sha256Identity),
153
+ generation: z.string().regex(sha256Identity),
154
+ targetIds: z.array(z.string().min(1)).min(1).max(STRUCTURED_INPUT_ENTRY_LIMIT),
155
+ actions: z.array(DrainActionSchema).min(1).max(STRUCTURED_INPUT_ENTRY_LIMIT),
156
+ expectedCoordinationRevision: z.number().int().nonnegative().safe(),
157
+ coordinationRevision: z.number().int().positive().safe(),
158
+ completedActionCount: z.number().int().nonnegative().safe(),
159
+ progressRevision: z.number().int().positive().safe(),
160
+ reservedAt: z.string().refine(isCanonicalTimestamp),
161
+ reservationId: z.string().regex(sha256Identity),
162
+ attempt: DrainAttemptSchema.nullable().optional().default(null),
163
+ }).strict().superRefine((value, context) => {
164
+ if (value.coordinationRevision !== value.expectedCoordinationRevision + 1
165
+ || value.attempt !== null && (value.attempt.reservationId !== value.reservationId
166
+ || value.attempt.purpose !== value.purpose
167
+ || value.attempt.generation !== value.generation
168
+ || !sameTargets(value.attempt.targetIds, value.targetIds)
169
+ || value.attempt.committedRevision <= value.progressRevision
170
+ || (value.attempt.committedRevision - value.progressRevision) % 2 !== 1
171
+ || Date.parse(value.attempt.committedAt) < Date.parse(value.reservedAt)
172
+ || value.attempt.actionIndex !== value.completedActionCount
173
+ || value.actions[value.completedActionCount]?.stage !== value.attempt.currentStage
174
+ || value.actions[value.completedActionCount]?.targetId !== value.attempt.currentTarget
175
+ || Date.parse(value.attempt.validThrough) - Date.parse(value.attempt.committedAt) !== value.actions[value.completedActionCount]?.validForMs) || value.generation === value.reservationId
176
+ || value.completedActionCount >= value.actions.length
177
+ || value.progressRevision < value.coordinationRevision
178
+ || new Set(value.targetIds).size !== value.targetIds.length
179
+ || value.targetIds.some((target, index) => Buffer.byteLength(target, 'utf8') > 512
180
+ || target.includes('\0') || index > 0 && value.targetIds[index - 1] >= target)
181
+ || value.actions[0]?.stage !== 'runtime-stop' || value.actions[0]?.targetId !== value.stackId
182
+ || value.actions.slice(1).some((action, index) => action.stage !== 'reclaim'
183
+ || action.targetId !== value.targetIds.filter((target) => target !== value.stackId)[index])
184
+ || value.actions.length !== value.targetIds.length
185
+ || !sameTargets([...value.actions.map(({ targetId }) => targetId)].sort(), value.targetIds)
186
+ || value.purpose === 'sleep' && value.actions.length !== 1) {
187
+ context.addIssue({ code: 'custom', message: 'drain reservation chronology is inconsistent' });
188
+ }
189
+ });
190
+ const CoordinationValueSchema = z.object({
191
+ holders: z.array(ClaimHolderSchema).max(STRUCTURED_INPUT_ENTRY_LIMIT),
192
+ lease: PortLeaseRecordSchema.nullable(),
193
+ lastReleasedHolder: z.string().regex(holderIdentity).nullable().optional().default(null),
194
+ drain: DrainReservationSchema.nullable().optional().default(null),
195
+ }).strict().superRefine((value, context) => {
196
+ const holderIds = value.holders.map(({ holderId }) => holderId);
197
+ if (new Set(holderIds).size !== holderIds.length
198
+ || value.lastReleasedHolder !== null && holderIds.includes(value.lastReleasedHolder)
199
+ || value.holders.length > 0 && (value.lease === null || value.lease.released)
200
+ || value.holders.length === 0 && value.lease !== null && !value.lease.released && value.lastReleasedHolder === null
201
+ || value.drain !== null && (value.holders.length !== 1
202
+ || value.lease === null
203
+ || value.lease.released
204
+ || value.drain.holderId !== value.drain.requesterId
205
+ || value.holders[0]?.holderId !== value.drain.holderId
206
+ || !holderIds.includes(value.drain.holderId)
207
+ || !holderIds.includes(value.drain.requesterId))) {
208
+ context.addIssue({ code: 'custom', message: 'coordination invariants are inconsistent' });
209
+ }
210
+ });
211
+ const PS_START = /^(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun) (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) +(\d{1,2}) (\d{2}):(\d{2}):(\d{2}) (\d{4})$/;
212
+ const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
213
+ const BOOT_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
214
+ const LSTART_START = /^lstart:(.+)$/;
215
+ const PROC_START = /^proc:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}:[0-9]{1,20}$/;
216
+ /** A start identity as `observeProcessStart` reports it: `lstart:<timestamp>` or `proc:<boot id>:<ticks>`. */
217
+ export function isProcessStart(value) {
218
+ return typeof value === 'string' && (PROC_START.test(value) || isCanonicalTimestamp(LSTART_START.exec(value)?.[1]));
219
+ }
220
+ const startKind = (start) => start.slice(0, start.indexOf(':'));
221
+ /**
222
+ * Whether an observation proves gone the process recorded with `processStart`. A gone pid proves it
223
+ * whether or not a start was recorded, since reuse can only make a dead process look alive; a start
224
+ * identity decides only against one read the same way.
225
+ */
226
+ export function isProcessGone(processStart, observed) {
227
+ return observed.state === 'absent' || observed.state === 'present' && processStart !== undefined
228
+ && startKind(observed.start) === startKind(processStart) && observed.start !== processStart;
229
+ }
230
+ // A pid alone does not name a holder: the pid of a killed session is reused, and a reused pid read
231
+ // as the session would wedge its stack for as long as the stranger runs. So a session records its
232
+ // process's start identity, and a holder is dead when its pid is gone or now belongs to a process
233
+ // with another identity. On Linux the identity is the start in clock ticks since boot (field 22 of
234
+ // /proc/<pid>/stat) with the boot's ID: ps derives a wall-clock start from the boot time, which moves
235
+ // whenever the wall clock is stepped (NTP, a resumed VM), so a running session would read as a
236
+ // stranger and lose its hold. macOS records the wall-clock start once, and ps prints it to the
237
+ // second under a fixed locale and zone. Each identity names how it was read; anything that cannot
238
+ // be confirmed reads as unknown, and unknown is never dead.
239
+ export async function observeProcessStart(pid, host) {
240
+ if (!Number.isSafeInteger(pid) || pid < 1 || pid > 2_147_483_647)
241
+ return { state: 'unknown', reason: `process id ${pid} is invalid` };
242
+ const presence = await host.presence(pid);
243
+ if (presence === 'absent')
244
+ return { state: 'absent' };
245
+ if (presence !== 'live')
246
+ return { state: 'unknown', reason: `the presence of process ${pid} could not be confirmed` };
247
+ return host.platform === 'linux' ? observeProcStart(pid, host) : observeLstart(pid, host);
248
+ }
249
+ async function observeProcStart(pid, host) {
250
+ let stat;
251
+ try {
252
+ stat = await host.readFile(`/proc/${pid}/stat`);
253
+ }
254
+ catch (error) {
255
+ // Signal 0 just found it, so a missing entry means it exited since, unless this /proc is not
256
+ // this pid namespace's (unmounted in a chroot or sandbox); only a second signal decides.
257
+ if (hasErrorCode(error, 'ENOENT') && await host.presence(pid) === 'absent')
258
+ return { state: 'absent' };
259
+ return { state: 'unknown', reason: `/proc/${pid}/stat could not be read (${errorCause(error)})` };
260
+ }
261
+ let bootId;
262
+ try {
263
+ bootId = (await host.readFile('/proc/sys/kernel/random/boot_id')).trim();
264
+ }
265
+ catch (error) {
266
+ return { state: 'unknown', reason: `the boot ID could not be read (${errorCause(error)})` };
267
+ }
268
+ // The command name in parentheses may itself hold spaces and parentheses; the fields after the
269
+ // last `)` start at field 3, so field 22 is the twentieth.
270
+ const ticks = stat.slice(stat.lastIndexOf(')') + 2).split(' ')[19];
271
+ if (stat.lastIndexOf(')') === -1 || ticks === undefined || !/^[0-9]{1,20}$/.test(ticks) || !BOOT_ID.test(bootId)) {
272
+ return { state: 'unknown', reason: `/proc reported an unrecognised start for process ${pid}` };
273
+ }
274
+ return { state: 'present', start: `proc:${bootId}:${ticks}` };
275
+ }
276
+ async function observeLstart(pid, host) {
277
+ const started = await host.psStart(pid);
278
+ if (started.kind === 'failed')
279
+ return { state: 'unknown', reason: started.reason };
280
+ const match = PS_START.exec(started.text.trim());
281
+ if (!match)
282
+ return { state: 'unknown', reason: `ps reported an unrecognised start time for process ${pid}` };
283
+ const [, month, day, hours, minutes, seconds, year] = match;
284
+ const startedAt = new Date(Date.UTC(Number(year), MONTHS.indexOf(month), Number(day), Number(hours), Number(minutes), Number(seconds)));
285
+ return Number.isNaN(startedAt.getTime()) ? { state: 'unknown', reason: `ps reported an invalid start time for process ${pid}` }
286
+ : { state: 'present', start: `lstart:${startedAt.toISOString()}` };
287
+ }
288
+ /** The production `ProcessHost`: signal 0 and ps cross the executor, and /proc is read directly. */
289
+ export function processHost(execute) {
290
+ return {
291
+ platform: process.platform,
292
+ presence: async (pid) => {
293
+ const node = await resolveExecutablePath(process.execPath);
294
+ if (node.state !== 'available')
295
+ return 'unavailable';
296
+ const answer = await runBoundedProbe(node.value, `process:presence:${pid}`, ['-e', PROCESS_PRESENCE_PROBE, String(pid)], {}, execute);
297
+ return answer === 'live' || answer === 'absent' ? answer : 'unavailable';
298
+ },
299
+ psStart: async (pid) => {
300
+ let ps;
301
+ for (const candidate of ['/bin/ps', '/usr/bin/ps']) {
302
+ const resolved = await resolveExecutablePath(candidate);
303
+ if (resolved.state === 'available') {
304
+ ps = resolved.value;
305
+ break;
306
+ }
307
+ }
308
+ if (!ps)
309
+ return { kind: 'failed', reason: 'ps is not installed at /bin/ps or /usr/bin/ps' };
310
+ const text = await runBoundedProbe(ps, `process:start:${pid}`, ['-o', 'lstart=', '-p', String(pid)], { LC_ALL: 'C', TZ: 'UTC0' }, execute);
311
+ return text === undefined ? { kind: 'failed', reason: `ps did not report when process ${pid} started` } : { kind: 'complete', text };
312
+ },
313
+ readFile: (path) => readFile(path, 'utf8'),
314
+ };
315
+ }
316
+ // Reports `live` or `absent` for a pid; twice, so a transient answer reads as `unavailable`.
317
+ const PROCESS_PRESENCE_PROBE = "const p=Number(process.argv[1]);const c=()=>{try{process.kill(p,0);return'live'}catch(e){return e&&e.code==='ESRCH'?'absent':'unavailable'}};const a=c(),b=c();process.stdout.write(a===b?a:'unavailable')";
318
+ // One short read-only step through the executor; its stdout when it exits 0, otherwise undefined.
319
+ async function runBoundedProbe(executable, id, args, env, execute) {
320
+ const step = snapshotExecutionPlan({ steps: [{
321
+ id, argv: [executable.canonicalPath, ...args],
322
+ argv0: executable.invocationPath, cwd: '/', env: { ...env }, timeoutMs: 5_000,
323
+ stdoutLimitBytes: 256, stderrLimitBytes: 1_024, paths: [], ports: [],
324
+ labels: { probe: id }, expected: [],
325
+ consumedFiles: [{ path: executable.canonicalPath, sha256: executable.sha256, maxBytes: 536_870_912,
326
+ argvIndexes: [0], executable: true, executionMode: executable.executionMode }],
327
+ filesystemChecks: [], compensation: [],
328
+ }] })?.steps[0];
329
+ if (!step)
330
+ return undefined;
331
+ const outcome = authenticatedExecutionOutcome(await execute(step), step);
332
+ return outcome?.kind === 'complete' && outcome.execution.exitCode === 0 ? Buffer.from(outcome.stdout).toString('utf8') : undefined;
333
+ }
334
+ function describeHolder(holder) {
335
+ return holder.pid === undefined ? holder.holderId : `${holder.holderId} (process ${holder.pid})`;
336
+ }
337
+ // Names each other holder and how to end it. A dead session's hold is released before this is
338
+ // reached, so every holder named here is running, or could not be confirmed gone.
339
+ export function liveHolderMessage(holders) {
340
+ const pids = holders.flatMap(({ pid }) => pid === undefined ? [] : [pid]);
341
+ return `another cockpit session holds this stack (${holders.map(describeHolder).join(', ')}), and down and reset never displace a running session; `
342
+ + `let its command finish${pids.length > 0 ? ` or stop it with \`kill ${pids.join(' ')}\`` : ''}, then retry`;
343
+ }
344
+ // Every probe is a child process; a record may name thousands of holders.
345
+ const MAX_CONCURRENT_HOLDER_PROBES = 4;
346
+ /**
347
+ * Remove the holds of `cockpit session` wrappers whose process is gone. A session killed with
348
+ * SIGKILL, a crash or a closed terminal never runs its closing `sleep`, and its hold then made
349
+ * `down`, `reset` and `sleep` refuse forever while naming a session nobody could end. When every
350
+ * holder is dead, the requesting holder takes the stack over in the same transaction: the stack is
351
+ * still running, and a lease no holder retains reads as a stopped stack whose ports are free.
352
+ */
353
+ export async function releaseDeadSessionHolders(input) {
354
+ const stored = await readState(input.namespace.paths.coordination(input.stackId));
355
+ if (stored.kind === 'missing')
356
+ return { kind: 'complete', released: [] };
357
+ if (stored.kind !== 'complete')
358
+ return stateRefusal(stored);
359
+ const value = decodeCoordinationRecord(stored.value, input.stackId);
360
+ if (!value)
361
+ return refuse(ENVIRONMENT_CODES.STATE_RECORD_MALFORMED, 'coordination record is malformed', input.stackId);
362
+ if (isDrainActive(value))
363
+ return { kind: 'complete', released: [] };
364
+ const candidates = value.holders.filter((holder) => holder.holderId !== input.successor.holderId && holder.pid !== undefined);
365
+ // Indexed slots keep the verdicts in holder order whatever order the probes finish in.
366
+ const observed = new Array(candidates.length);
367
+ let next = 0;
368
+ await Promise.all(Array.from({ length: Math.min(MAX_CONCURRENT_HOLDER_PROBES, candidates.length) }, async () => {
369
+ while (next < candidates.length) {
370
+ const index = next++;
371
+ observed[index] = await input.observe(candidates[index].pid);
372
+ }
373
+ }));
374
+ const dead = candidates.filter((holder, index) => isProcessGone(holder.processStart, observed[index]));
375
+ if (dead.length === 0)
376
+ return { kind: 'complete', released: [] };
377
+ let committed = [];
378
+ const result = await transactClaim(input.namespace, input.stackId, input.successor.holderId, (current) => {
379
+ const latest = decodeCoordinationRecord(current, input.stackId);
380
+ if (!latest)
381
+ return abortStateTransaction(refuse(ENVIRONMENT_CODES.STATE_RECORD_MALFORMED, 'coordination record is malformed', input.stackId));
382
+ if (isDrainActive(latest) || latest.lease === null || latest.lease.released) {
383
+ return abortStateTransaction({ kind: 'complete', released: [] });
384
+ }
385
+ // Only a holder still recorded exactly as it was observed is released; one that changed
386
+ // since is someone else's to judge.
387
+ const released = dead.filter((holder) => latest.holders.some((candidate) => candidate.holderId === holder.holderId
388
+ && candidate.claimedAt === holder.claimedAt && candidate.pid === holder.pid
389
+ && candidate.processStart === holder.processStart));
390
+ if (released.length === 0)
391
+ return abortStateTransaction({ kind: 'complete', released: [] });
392
+ committed = released;
393
+ const remaining = latest.holders.filter((holder) => !released.some(({ holderId }) => holderId === holder.holderId));
394
+ const reduced = { ...latest, holders: remaining, lastReleasedHolder: released.at(-1).holderId };
395
+ const next = remaining.length > 0 ? reduced : addCoordinationHolder(reduced, input.successor, input.now)?.value;
396
+ if (!next)
397
+ return abortStateTransaction(refuse(ENVIRONMENT_CODES.STATE_RECORD_MALFORMED, 'coordination holder set cannot accept the successor holder', input.successor.holderId));
398
+ return coordinationStateRecord(input.namespace, input.stackId, current, input.now, next);
399
+ });
400
+ if ('diagnostics' in result || 'released' in result)
401
+ return result;
402
+ if (result.kind !== 'complete')
403
+ return result.kind === 'missing'
404
+ ? refuse(ENVIRONMENT_CODES.STATE_RECORD_MALFORMED, 'claim transaction returned no record', result.path) : stateRefusal(result);
405
+ return { kind: 'complete', released: committed };
406
+ }
407
+ function diagnostic(code, message, target, fix) {
408
+ return { severity: 'error', code, message, target, ...(fix ? { fix } : {}) };
409
+ }
410
+ function refuse(code, message, target, fix) {
411
+ return { kind: 'refusal', diagnostics: [diagnostic(code, message, target, fix)] };
412
+ }
413
+ export async function readActiveStackInventory(namespace, options = {}) {
414
+ return readCoordinationInventory(namespace, options);
415
+ }
416
+ async function readCoordinationInventory(namespace, options, budget) {
417
+ const ownedNamespace = snapshotStateNamespace(namespace);
418
+ if (!ownedNamespace)
419
+ return refuse(ENVIRONMENT_CODES.STATE_ROOT_UNAVAILABLE, 'state namespace is malformed', 'state namespace');
420
+ namespace = ownedNamespace;
421
+ const directory = dirname(namespace.paths.coordination(`stack:${'0'.repeat(64)}`).path);
422
+ let inspected = false;
423
+ try {
424
+ const before = await lstat(directory, { bigint: true });
425
+ inspected = true;
426
+ if (!before.isDirectory() || before.isSymbolicLink()) {
427
+ return refuse(ENVIRONMENT_CODES.STATE_RECORD_MALFORMED, 'coordination store is not a safe directory', directory);
428
+ }
429
+ const names = [];
430
+ const handle = await opendir(directory);
431
+ try {
432
+ for await (const entry of handle) {
433
+ if (names.length >= STRUCTURED_INPUT_ENTRY_LIMIT) {
434
+ return refuse(ENVIRONMENT_CODES.STATE_RECORD_MALFORMED, 'coordination store exceeds its entry boundary', directory);
435
+ }
436
+ if (budget && ++budget.entries > STRUCTURED_INPUT_ENTRY_LIMIT) {
437
+ return refuse(ENVIRONMENT_CODES.STATE_RECORD_MALFORMED, 'coordination inventory exceeds its aggregate entry boundary', directory);
438
+ }
439
+ names.push(entry.name);
440
+ }
441
+ }
442
+ finally {
443
+ await handle.close().catch(() => undefined);
444
+ }
445
+ const records = [];
446
+ const observedRecords = new Map();
447
+ for (const name of names.sort()) {
448
+ if (budget) {
449
+ const path = join(directory, name);
450
+ const status = await lstat(path);
451
+ if (!status.isFile() || status.isSymbolicLink() || !Number.isSafeInteger(status.size)
452
+ || (budget.bytes += status.size) > SERIALIZED_RECORD_BYTE_LIMIT) {
453
+ return refuse(ENVIRONMENT_CODES.STATE_RECORD_MALFORMED, 'coordination inventory exceeds its aggregate byte boundary or contains an unsafe entry', path);
454
+ }
455
+ }
456
+ const entry = parseCoordinationStateEntry(name);
457
+ if (entry?.kind === 'temporary') {
458
+ const lock = await inspectStateLock(namespace, options.observeHolder);
459
+ const residue = join(directory, name);
460
+ const [currentResidue, afterLockInspection] = await Promise.all([
461
+ lstat(residue),
462
+ lstat(directory, { bigint: true }),
463
+ ]);
464
+ if (!currentResidue.isFile() || currentResidue.isSymbolicLink()
465
+ || !afterLockInspection.isDirectory() || afterLockInspection.isSymbolicLink()
466
+ || afterLockInspection.dev !== before.dev || afterLockInspection.ino !== before.ino
467
+ || afterLockInspection.mtimeNs !== before.mtimeNs || afterLockInspection.ctimeNs !== before.ctimeNs) {
468
+ return refuse(ENVIRONMENT_CODES.STATE_RECORD_MALFORMED, 'coordination store changed during observation', directory);
469
+ }
470
+ if (lock.holderState === 'live') {
471
+ return refuse(ENVIRONMENT_CODES.STATE_LOCKED, 'coordination publication is in progress', residue, 'retry after the coordination transaction completes');
472
+ }
473
+ if (lock.holderState === 'unknown') {
474
+ return refuse(ENVIRONMENT_CODES.STATE_LOCKED, 'coordination publication ownership is unverified', residue, lock.recovery);
475
+ }
476
+ return refuse(ENVIRONMENT_CODES.STATE_RECORD_MALFORMED, 'coordination store contains an orphaned unverified publication residue', residue, `${lock.description}; ${lock.recovery}; remove the residue only after proving that no writer owns it`);
477
+ }
478
+ if (entry?.kind === 'executor') {
479
+ const path = join(directory, name);
480
+ const marker = await lstat(path, { bigint: true });
481
+ const stored = await readState(namespace.paths.coordination(entry.stackId));
482
+ const record = stored.kind === 'complete' ? decodeCoordinationRecord(stored.value, entry.stackId) : undefined;
483
+ const after = await lstat(path, { bigint: true });
484
+ // The executor owns marker contents and repair. Inventory only admits a safe
485
+ // auxiliary file for the exact independently recorded drain attempt.
486
+ if (!marker.isFile() || marker.isSymbolicLink() || marker.nlink !== 1n
487
+ || marker.uid !== BigInt(process.getuid?.() ?? -1) || (marker.mode & 18n) !== 0n
488
+ || marker.size > BigInt(SERIALIZED_RECORD_BYTE_LIMIT)
489
+ || !after.isFile() || after.dev !== marker.dev || after.ino !== marker.ino
490
+ || after.size !== marker.size || after.mtimeNs !== marker.mtimeNs || after.ctimeNs !== marker.ctimeNs
491
+ || after.uid !== marker.uid || after.mode !== marker.mode || after.nlink !== marker.nlink
492
+ || !record?.drain?.attempt || record.drain.attempt.attemptId !== entry.attemptId
493
+ || stored.kind !== 'complete'
494
+ || exactRecordFingerprint(stored.value) !== exactRecordFingerprint(observedRecords.get(entry.stackId))) {
495
+ return refuse(ENVIRONMENT_CODES.STATE_RECORD_MALFORMED, 'coordination executor marker is unsafe or does not match its current drain attempt', path);
496
+ }
497
+ continue;
498
+ }
499
+ if (!entry || entry.kind !== 'record') {
500
+ return refuse(ENVIRONMENT_CODES.STATE_RECORD_MALFORMED, 'coordination store contains an unexpected entry', join(directory, name));
501
+ }
502
+ const stackId = entry.stackId;
503
+ const result = await readState(namespace.paths.coordination(stackId));
504
+ if (result.kind !== 'complete') {
505
+ return refuse(result.kind === 'missing' ? ENVIRONMENT_CODES.STATE_RECORD_MALFORMED : result.code, result.kind === 'missing' ? 'coordination record disappeared during observation' : result.message, result.kind === 'missing' ? result.path : result.target);
506
+ }
507
+ const record = decodeCoordinationRecord(result.value, stackId);
508
+ if (!record)
509
+ return refuse(ENVIRONMENT_CODES.STATE_RECORD_MALFORMED, 'coordination record is malformed', result.value.subjectId);
510
+ observedRecords.set(stackId, result.value);
511
+ if (record.holders.length > 0 || record.lease !== null && !record.lease.released)
512
+ records.push({ stackId, record });
513
+ }
514
+ const after = await lstat(directory, { bigint: true });
515
+ if (!after.isDirectory() || after.isSymbolicLink() || after.dev !== before.dev || after.ino !== before.ino
516
+ || after.mtimeNs !== before.mtimeNs || after.ctimeNs !== before.ctimeNs) {
517
+ return refuse(ENVIRONMENT_CODES.STATE_RECORD_MALFORMED, 'coordination store changed during observation', directory);
518
+ }
519
+ return { kind: 'complete', value: records };
520
+ }
521
+ catch (error) {
522
+ if (hasErrorCode(error, 'ENOENT')) {
523
+ return inspected
524
+ ? refuse(ENVIRONMENT_CODES.STATE_RECORD_MALFORMED, 'coordination store changed during observation', directory)
525
+ : { kind: 'complete', value: [] };
526
+ }
527
+ return refuse(ENVIRONMENT_CODES.STATE_ROOT_UNAVAILABLE, 'coordination store could not be inspected', directory);
528
+ }
529
+ }
530
+ // Port leases are machine-wide, so this reads every cockpit's coordination records. `skipped` names
531
+ // each other cockpit's namespace this version cannot read; its leases are not seen, and the caller
532
+ // reports it so a person can remove it. The caller's own namespace, a contended lock and the
533
+ // shared read bounds still refuse.
534
+ export async function readRootPortInventory(namespace, options = {}) {
535
+ const result = await withStateNamespaceInventory(namespace, async (namespaces, unreadable) => {
536
+ const records = [];
537
+ const skipped = [...unreadable];
538
+ const budget = { entries: namespaces.length, bytes: 0 };
539
+ for (const candidate of namespaces) {
540
+ const inventory = await readCoordinationInventory(candidate, options, budget);
541
+ if (inventory.kind !== 'complete') {
542
+ const diagnostic = inventory.diagnostics[0];
543
+ if (candidate.cockpitRoot === namespace.cockpitRoot || diagnostic?.code === ENVIRONMENT_CODES.STATE_LOCKED
544
+ || budget.entries > STRUCTURED_INPUT_ENTRY_LIMIT || budget.bytes > SERIALIZED_RECORD_BYTE_LIMIT)
545
+ return inventory;
546
+ skipped.push({ path: candidate.cockpitRoot, reason: diagnostic?.message ?? 'coordination records are unreadable' });
547
+ continue;
548
+ }
549
+ const cockpitId = candidate.paths.coordination(`stack:${'0'.repeat(64)}`).cockpitId;
550
+ records.push(...inventory.value.map((record) => ({ cockpitId, ...record })));
551
+ }
552
+ return { kind: 'complete', value: records, skipped };
553
+ });
554
+ if (result.kind === 'complete')
555
+ return result.value;
556
+ if (result.kind === 'missing')
557
+ return refuse(ENVIRONMENT_CODES.STATE_RECORD_MALFORMED, 'state namespace inventory disappeared during observation', result.path);
558
+ return refuse(result.code, result.message, result.target, result.recovery);
559
+ }
560
+ function validIdentity(value, pattern) {
561
+ return typeof value === 'string' && pattern.test(value);
562
+ }
563
+ function hasExactKeys(value, keys) {
564
+ try {
565
+ const actual = Reflect.ownKeys(Object.getOwnPropertyDescriptors(value));
566
+ return actual.length === keys.length && actual.every((key) => typeof key === 'string' && keys.includes(key));
567
+ }
568
+ catch {
569
+ // A throwing accessor or proxy trap makes the input malformed, which the caller refuses.
570
+ return false;
571
+ }
572
+ }
573
+ export function decodeCoordinationRecord(record, stackId) {
574
+ if (record === null)
575
+ return { holders: [], lease: null, lastReleasedHolder: null, drain: null };
576
+ if (record.kind !== 'coordination' || record.subjectId !== stackId)
577
+ return undefined;
578
+ const parsed = CoordinationValueSchema.safeParse(record.value);
579
+ if (!parsed.success)
580
+ return undefined;
581
+ if (parsed.data.drain !== null && parsed.data.drain.stackId !== stackId)
582
+ return undefined;
583
+ if (parsed.data.drain !== null) {
584
+ const boundaryRevision = parsed.data.drain.attempt?.status === 'failed'
585
+ ? parsed.data.drain.attempt.failedRevision
586
+ : parsed.data.drain.attempt?.committedRevision ?? parsed.data.drain.progressRevision;
587
+ if (record.revision !== boundaryRevision)
588
+ return undefined;
589
+ }
590
+ return {
591
+ holders: parsed.data.holders
592
+ .map((holder) => ({
593
+ holderId: holder.holderId,
594
+ checkoutId: holder.checkoutId,
595
+ claimedAt: holder.claimedAt,
596
+ ...(holder.pid === undefined ? {} : { pid: holder.pid }),
597
+ ...(holder.processStart === undefined ? {} : { processStart: holder.processStart }),
598
+ }))
599
+ .sort((left, right) => left.holderId < right.holderId ? -1 : left.holderId > right.holderId ? 1 : 0),
600
+ lease: parsed.data.lease === null ? null : {
601
+ offset: parsed.data.lease.offset,
602
+ ports: { ...parsed.data.lease.ports },
603
+ released: parsed.data.lease.released,
604
+ declaration: {
605
+ step: parsed.data.lease.declaration.step,
606
+ count: parsed.data.lease.declaration.count,
607
+ bases: { ...parsed.data.lease.declaration.bases },
608
+ },
609
+ },
610
+ lastReleasedHolder: parsed.data.lastReleasedHolder,
611
+ drain: parsed.data.drain === null ? null : snapshotDrainReservation({
612
+ ...parsed.data.drain,
613
+ stackId: parsed.data.drain.stackId,
614
+ holderId: parsed.data.drain.holderId,
615
+ requesterId: parsed.data.drain.requesterId,
616
+ }),
617
+ };
618
+ }
619
+ export function addCoordinationHolder(value, holder, now) {
620
+ const existing = value.holders.find((candidate) => candidate.holderId === holder.holderId);
621
+ if (existing && existing.checkoutId !== holder.checkoutId)
622
+ return undefined;
623
+ if (existing)
624
+ return { value, change: 'unchanged' };
625
+ return {
626
+ value: {
627
+ ...value,
628
+ holders: [...value.holders, { ...holder, claimedAt: now }]
629
+ .sort((left, right) => left.holderId < right.holderId ? -1 : left.holderId > right.holderId ? 1 : 0),
630
+ lastReleasedHolder: value.lastReleasedHolder === holder.holderId ? null : value.lastReleasedHolder,
631
+ },
632
+ change: 'acquired',
633
+ };
634
+ }
635
+ export function removeExactCoordinationHolder(value, holderId) {
636
+ try {
637
+ if (!holderIdentity.test(holderId) || value.lease === null || value.lease.released
638
+ || value.drain != null)
639
+ return undefined;
640
+ const holders = value.holders.filter((holder) => holder.holderId !== holderId);
641
+ if (holders.length !== value.holders.length - 1)
642
+ return undefined;
643
+ return {
644
+ ...value,
645
+ holders,
646
+ lastReleasedHolder: holderId,
647
+ };
648
+ }
649
+ catch {
650
+ // A throwing accessor or proxy trap makes the input malformed, which the caller refuses.
651
+ return undefined;
652
+ }
653
+ }
654
+ export function coordinationStateRecord(namespace, stackId, current, now, value) {
655
+ return {
656
+ format: 'cockpit-state/v1',
657
+ kind: 'coordination',
658
+ cockpitId: namespace.paths.coordination(stackId).cockpitId,
659
+ subjectId: stackId,
660
+ revision: current === null ? 0 : current.revision + 1,
661
+ updatedAt: now,
662
+ value: {
663
+ holders: value.holders.map((holder) => ({ ...holder })),
664
+ lease: value.lease === null ? null : {
665
+ ...value.lease,
666
+ ports: { ...value.lease.ports },
667
+ declaration: { ...value.lease.declaration, bases: { ...value.lease.declaration.bases } },
668
+ },
669
+ lastReleasedHolder: value.lastReleasedHolder,
670
+ drain: value.drain == null ? null : {
671
+ ...value.drain,
672
+ targetIds: [...value.drain.targetIds],
673
+ actions: value.drain.actions.map((action) => ({
674
+ stage: action.stage,
675
+ targetId: action.targetId,
676
+ validForMs: action.validForMs,
677
+ ...(action.stage === 'runtime-stop' ? {} : {
678
+ targetCommitment: { ...action.targetCommitment },
679
+ }),
680
+ })),
681
+ attempt: value.drain.attempt === null ? null : {
682
+ ...value.drain.attempt,
683
+ targetIds: [...value.drain.attempt.targetIds],
684
+ },
685
+ },
686
+ },
687
+ };
688
+ }
689
+ function snapshotDrainRequest(value) {
690
+ try {
691
+ if (!value || typeof value !== 'object' || Array.isArray(value))
692
+ return undefined;
693
+ const keys = ['purpose', 'stackId', 'holderId', 'requesterId', 'configSha256', 'planDigest', 'operationBasis', 'generation', 'targetIds', 'actions', 'expectedCoordinationRevision'];
694
+ const descriptors = Object.getOwnPropertyDescriptors(value);
695
+ const supplied = Reflect.ownKeys(descriptors);
696
+ if (supplied.length !== keys.length || supplied.some((key) => typeof key !== 'string')
697
+ || keys.some((key) => !supplied.includes(key) || !('value' in descriptors[key])))
698
+ return undefined;
699
+ const request = Object.fromEntries(keys.map((key) => [key, descriptors[key].value]));
700
+ const targetIds = snapshotDrainTargets(request.targetIds);
701
+ const actions = snapshotDrainActions(request.actions);
702
+ if ((request.purpose !== 'sleep' && request.purpose !== 'down' && request.purpose !== 'reset')
703
+ || !validIdentity(request.stackId, stackIdentity)
704
+ || !validIdentity(request.holderId, holderIdentity)
705
+ || !validIdentity(request.requesterId, holderIdentity)
706
+ || !sha256Identity.test(request.configSha256)
707
+ || !sha256Identity.test(request.planDigest)
708
+ || !sha256Identity.test(request.operationBasis)
709
+ || !sha256Identity.test(request.generation)
710
+ || !targetIds || !actions || !validDrainActions(request.purpose, request.stackId, targetIds, actions)
711
+ || !Number.isSafeInteger(request.expectedCoordinationRevision)
712
+ || request.expectedCoordinationRevision < 0
713
+ || request.expectedCoordinationRevision >= Number.MAX_SAFE_INTEGER)
714
+ return undefined;
715
+ return {
716
+ purpose: request.purpose, stackId: request.stackId, holderId: request.holderId,
717
+ requesterId: request.requesterId, configSha256: request.configSha256,
718
+ planDigest: request.planDigest, operationBasis: request.operationBasis,
719
+ generation: request.generation, targetIds, actions,
720
+ expectedCoordinationRevision: request.expectedCoordinationRevision,
721
+ };
722
+ }
723
+ catch {
724
+ // A throwing accessor or proxy trap makes the input malformed, which the caller refuses.
725
+ return undefined;
726
+ }
727
+ }
728
+ function sameDrainRequest(left, right) {
729
+ return left.purpose === right.purpose && left.stackId === right.stackId
730
+ && left.holderId === right.holderId && left.requesterId === right.requesterId
731
+ && left.configSha256 === right.configSha256 && left.planDigest === right.planDigest
732
+ && left.operationBasis === right.operationBasis
733
+ && left.generation === right.generation && sameTargets(left.targetIds, right.targetIds)
734
+ && sameDrainActions(left.actions, right.actions)
735
+ && left.expectedCoordinationRevision === right.expectedCoordinationRevision;
736
+ }
737
+ function sameDrainAttemptBinding(left, right) {
738
+ return left.attemptId === right.attemptId && left.reservationId === right.reservationId
739
+ && left.purpose === right.purpose && sameTargets(left.targetIds, right.targetIds)
740
+ && left.generation === right.generation && left.actionIndex === right.actionIndex && left.currentTarget === right.currentTarget
741
+ && left.currentStage === right.currentStage && left.committedRevision === right.committedRevision
742
+ && left.committedAt === right.committedAt && left.validThrough === right.validThrough;
743
+ }
744
+ function sameDrainAttempt(left, right) {
745
+ return sameDrainAttemptBinding(left, right) && left.status === right.status
746
+ && left.failedRevision === right.failedRevision && left.failedAt === right.failedAt;
747
+ }
748
+ function snapshotDrainReservation(reservation) {
749
+ return {
750
+ ...reservation,
751
+ targetIds: [...reservation.targetIds],
752
+ actions: reservation.actions.map(snapshotDrainAction),
753
+ attempt: reservation.attempt === null ? null : {
754
+ ...reservation.attempt,
755
+ targetIds: [...reservation.attempt.targetIds],
756
+ },
757
+ };
758
+ }
759
+ function snapshotDrainAction(action) {
760
+ return action.stage === 'runtime-stop'
761
+ ? { ...action }
762
+ : { ...action, targetCommitment: { ...action.targetCommitment } };
763
+ }
764
+ function snapshotDrainTargets(value) {
765
+ try {
766
+ if (!Array.isArray(value))
767
+ return undefined;
768
+ const descriptors = Object.getOwnPropertyDescriptors(value);
769
+ const lengthValue = descriptors.length?.value;
770
+ if (typeof lengthValue !== 'number' || !Number.isSafeInteger(lengthValue)
771
+ || lengthValue === 0 || lengthValue > STRUCTURED_INPUT_ENTRY_LIMIT)
772
+ return undefined;
773
+ if (Reflect.ownKeys(descriptors).some((key) => typeof key === 'symbol' || key !== 'length' && !/^(?:0|[1-9]\d*)$/.test(key)))
774
+ return undefined;
775
+ const targets = Array.from({ length: lengthValue }, (_, index) => {
776
+ const property = descriptors[String(index)];
777
+ if (!property || !('value' in property) || typeof property.value !== 'string')
778
+ throw new RefusalError('invalid target');
779
+ return property.value;
780
+ });
781
+ if (new Set(targets).size !== targets.length
782
+ || targets.some((target, index) => target.length === 0 || Buffer.byteLength(target, 'utf8') > 512
783
+ || target.includes('\0') || index > 0 && targets[index - 1] >= target))
784
+ return undefined;
785
+ return targets;
786
+ }
787
+ catch {
788
+ // A throwing accessor or proxy trap makes the input malformed, which the caller refuses.
789
+ return undefined;
790
+ }
791
+ }
792
+ function snapshotDataOnly(value, seen = new WeakSet(), depth = 0) {
793
+ if (value === null || typeof value === 'string' || typeof value === 'boolean'
794
+ || typeof value === 'number' && Number.isFinite(value))
795
+ return value;
796
+ if (typeof value !== 'object' || depth > 16 || seen.has(value))
797
+ return undefined;
798
+ try {
799
+ const prototype = Object.getPrototypeOf(value);
800
+ if (prototype !== Object.prototype && prototype !== null && prototype !== Array.prototype)
801
+ return undefined;
802
+ seen.add(value);
803
+ const descriptors = Object.getOwnPropertyDescriptors(value);
804
+ if (Reflect.ownKeys(descriptors).some((key) => typeof key === 'symbol'))
805
+ return undefined;
806
+ if (Array.isArray(value)) {
807
+ const lengthValue = descriptors.length?.value;
808
+ if (typeof lengthValue !== 'number' || !Number.isSafeInteger(lengthValue)
809
+ || lengthValue < 0 || lengthValue > STRUCTURED_INPUT_ENTRY_LIMIT)
810
+ return undefined;
811
+ const keys = Object.keys(descriptors).filter((key) => key !== 'length');
812
+ if (keys.length !== lengthValue)
813
+ return undefined;
814
+ const result = [];
815
+ for (let index = 0; index < lengthValue; index += 1) {
816
+ const descriptor = descriptors[String(index)];
817
+ if (!descriptor || !('value' in descriptor))
818
+ return undefined;
819
+ const item = snapshotDataOnly(descriptor.value, seen, depth + 1);
820
+ if (item === undefined && descriptor.value !== undefined)
821
+ return undefined;
822
+ result.push(item);
823
+ }
824
+ return result;
825
+ }
826
+ const result = {};
827
+ for (const [key, descriptor] of Object.entries(descriptors)) {
828
+ if (!('value' in descriptor))
829
+ return undefined;
830
+ const item = snapshotDataOnly(descriptor.value, seen, depth + 1);
831
+ if (item === undefined && descriptor.value !== undefined)
832
+ return undefined;
833
+ result[key] = item;
834
+ }
835
+ return result;
836
+ }
837
+ catch {
838
+ // A throwing accessor or proxy trap makes the input malformed, which the caller refuses.
839
+ return undefined;
840
+ }
841
+ }
842
+ function snapshotDrainActions(value) {
843
+ try {
844
+ if (!Array.isArray(value))
845
+ return undefined;
846
+ const descriptors = Object.getOwnPropertyDescriptors(value);
847
+ const lengthValue = descriptors.length?.value;
848
+ if (typeof lengthValue !== 'number' || !Number.isSafeInteger(lengthValue)
849
+ || lengthValue === 0 || lengthValue > STRUCTURED_INPUT_ENTRY_LIMIT)
850
+ return undefined;
851
+ if (Reflect.ownKeys(descriptors).some((key) => typeof key === 'symbol' || key !== 'length' && !/^(?:0|[1-9]\d*)$/.test(key)))
852
+ return undefined;
853
+ const actions = [];
854
+ for (let index = 0; index < lengthValue; index += 1) {
855
+ const item = descriptors[String(index)];
856
+ if (!item || !('value' in item) || !item.value || typeof item.value !== 'object' || Array.isArray(item.value))
857
+ return undefined;
858
+ const snapshot = snapshotDataOnly(item.value);
859
+ const parsed = DrainActionSchema.safeParse(snapshot);
860
+ if (!parsed.success)
861
+ return undefined;
862
+ actions.push(snapshotDrainAction(parsed.data));
863
+ }
864
+ return actions;
865
+ }
866
+ catch {
867
+ // A throwing accessor or proxy trap makes the input malformed, which the caller refuses.
868
+ return undefined;
869
+ }
870
+ }
871
+ function sameDrainActions(left, right) {
872
+ return left.length === right.length && left.every((action, index) => action.stage === right[index]?.stage
873
+ && action.targetId === right[index]?.targetId && action.validForMs === right[index]?.validForMs
874
+ && (action.stage === 'runtime-stop' || right[index]?.stage === 'reclaim'
875
+ && JSON.stringify(action.targetCommitment) === JSON.stringify(right[index].targetCommitment)));
876
+ }
877
+ // Persisting the order prevents callers and executors from choosing destructive work.
878
+ function validDrainActions(purpose, stackId, targetIds, actions) {
879
+ const reclaimTargets = targetIds.filter((target) => target !== stackId);
880
+ return actions.length === targetIds.length
881
+ && actions[0]?.stage === 'runtime-stop' && actions[0].targetId === stackId
882
+ && actions.slice(1).every((action, index) => action.stage === 'reclaim' && action.targetId === reclaimTargets[index])
883
+ && sameTargets([...actions.map(({ targetId }) => targetId)].sort(), targetIds)
884
+ && (purpose !== 'sleep' || actions.length === 1);
885
+ }
886
+ function sameTargets(left, right) {
887
+ return left.length === right.length && left.every((target, index) => target === right[index]);
888
+ }
889
+ function exactRecordFingerprint(record) {
890
+ const snapshot = snapshotDataOnly(record);
891
+ if (!snapshot || typeof snapshot !== 'object' || Array.isArray(snapshot)
892
+ || !hasExactKeys(snapshot, ['format', 'kind', 'cockpitId', 'subjectId', 'revision', 'updatedAt', 'value']))
893
+ return undefined;
894
+ const candidate = snapshot;
895
+ if (candidate.format !== 'cockpit-state/v1' || candidate.kind !== 'coordination'
896
+ || typeof candidate.cockpitId !== 'string' || candidate.cockpitId.length === 0
897
+ || typeof candidate.subjectId !== 'string' || candidate.subjectId.length === 0
898
+ || typeof candidate.revision !== 'number' || !Number.isInteger(candidate.revision) || candidate.revision < 0
899
+ || typeof candidate.updatedAt !== 'string' || !isCanonicalTimestamp(candidate.updatedAt)
900
+ || !z.json().safeParse(candidate.value).success)
901
+ return undefined;
902
+ return canonicalJson(snapshot);
903
+ }
904
+ export function isDrainActive(value) {
905
+ return value.drain != null;
906
+ }
907
+ export async function reserveDrain(input) {
908
+ let namespace;
909
+ let request;
910
+ try {
911
+ if (!input || typeof input !== 'object' || Array.isArray(input) || !hasExactKeys(input, ['namespace', 'request']))
912
+ throw new RefusalError('invalid');
913
+ const descriptors = Object.getOwnPropertyDescriptors(input);
914
+ if (!('value' in descriptors.namespace) || !('value' in descriptors.request))
915
+ throw new RefusalError('invalid');
916
+ namespace = snapshotStateNamespace(descriptors.namespace.value);
917
+ request = snapshotDrainRequest(descriptors.request.value);
918
+ }
919
+ catch {
920
+ // A throwing accessor or proxy trap makes the input malformed, which the caller refuses.
921
+ namespace = undefined;
922
+ request = undefined;
923
+ }
924
+ if (!namespace || !request)
925
+ return refuse(ENVIRONMENT_CODES.STATE_RECORD_MALFORMED, 'drain reservation input is malformed', 'drain reservation');
926
+ const reservedAt = new Date().toISOString();
927
+ const reservationId = randomBytes(32).toString('hex');
928
+ const reservation = {
929
+ ...request,
930
+ coordinationRevision: request.expectedCoordinationRevision + 1,
931
+ completedActionCount: 0,
932
+ progressRevision: request.expectedCoordinationRevision + 1,
933
+ reservedAt,
934
+ reservationId,
935
+ attempt: null,
936
+ };
937
+ const result = await transactClaim(namespace, request.stackId, request.requesterId, (current) => {
938
+ const value = decodeCoordinationRecord(current, request.stackId);
939
+ if (!value || current === null || current.revision !== request.expectedCoordinationRevision) {
940
+ return abortStateTransaction(refuse(ENVIRONMENT_CODES.CLAIM_CO_TENANT, 'drain reservation requires the exact current coordination revision', request.stackId));
941
+ }
942
+ if (value.drain !== null)
943
+ return abortStateTransaction(refuse(ENVIRONMENT_CODES.CLAIM_CO_TENANT, 'the stack already has an exclusive mutation', request.stackId));
944
+ if (request.holderId !== request.requesterId
945
+ || value.holders.length !== 1
946
+ || value.holders[0]?.holderId !== request.holderId
947
+ || value.lease === null || value.lease.released) {
948
+ return abortStateTransaction(refuse(ENVIRONMENT_CODES.CLAIM_CO_TENANT, 'drain reservation requires retained holder and lease ownership', request.stackId));
949
+ }
950
+ return coordinationStateRecord(namespace, request.stackId, current, reservedAt, { ...value, drain: reservation });
951
+ });
952
+ if ('diagnostics' in result || 'reservation' in result)
953
+ return result;
954
+ if (result.kind !== 'complete') {
955
+ return result.kind === 'missing'
956
+ ? refuse(ENVIRONMENT_CODES.STATE_RECORD_MALFORMED, 'drain reservation returned no record', result.path)
957
+ : stateRefusal(result);
958
+ }
959
+ const value = decodeCoordinationRecord(result.value, request.stackId);
960
+ if (!value?.drain || !sameDrainRequest(value.drain, request) || value.drain.reservationId !== reservationId) {
961
+ return refuse(ENVIRONMENT_CODES.STATE_RECORD_MALFORMED, 'published drain reservation is malformed', request.stackId);
962
+ }
963
+ return { kind: 'complete', reservation: value.drain };
964
+ }
965
+ function reduceDrainAttemptState(record, reservation, attempt, retry) {
966
+ const value = decodeCoordinationRecord(record, reservation.stackId);
967
+ const currentAction = reservation.actions[reservation.completedActionCount];
968
+ const prior = reservation.attempt;
969
+ const expectedRevision = retry ? prior?.failedRevision : reservation.progressRevision;
970
+ if (!value?.drain || !currentAction || expectedRevision === null || expectedRevision === undefined
971
+ || record.revision !== expectedRevision || attempt.status !== 'active' || attempt.failedRevision !== null || attempt.failedAt !== null
972
+ || attempt.reservationId !== reservation.reservationId || attempt.purpose !== reservation.purpose
973
+ || !sameTargets(attempt.targetIds, reservation.targetIds) || attempt.generation !== reservation.generation
974
+ || attempt.actionIndex !== reservation.completedActionCount || attempt.currentStage !== currentAction.stage
975
+ || attempt.currentTarget !== currentAction.targetId || attempt.committedRevision !== expectedRevision + 1
976
+ || Date.parse(attempt.validThrough) - Date.parse(attempt.committedAt) !== currentAction.validForMs
977
+ || !sameDrainRequest(value.drain, reservation) || value.drain.reservationId !== reservation.reservationId
978
+ || value.drain.reservedAt !== reservation.reservedAt)
979
+ return undefined;
980
+ if (retry) {
981
+ if (!prior || prior.status !== 'failed' || value.drain.attempt?.status !== 'failed'
982
+ || !sameDrainAttempt(value.drain.attempt, prior)
983
+ || prior.failedAt === null || Date.parse(attempt.committedAt) < Date.parse(prior.failedAt))
984
+ return undefined;
985
+ }
986
+ else if (prior !== null || value.drain.attempt !== null)
987
+ return undefined;
988
+ const proposed = { ...value, drain: { ...value.drain, attempt } };
989
+ return CoordinationValueSchema.safeParse(proposed).success ? proposed : undefined;
990
+ }
991
+ /**
992
+ * Commits the next attempt of a reservation: its first try, or a retry of the action whose try was
993
+ * published as failed. The commit admits only the exact persisted reservation at the revision it
994
+ * names, so two holders of one reservation cannot both start an attempt.
995
+ */
996
+ export async function reserveDrainAttempt(input) {
997
+ const namespace = snapshotStateNamespace(input.namespace);
998
+ const { reservation } = input;
999
+ const currentAction = reservation.actions[reservation.completedActionCount];
1000
+ if (!namespace || !currentAction || reservation.attempt?.status === 'active'
1001
+ || reservation.attempt?.status === 'failed' && (currentAction.targetId !== reservation.attempt.currentTarget
1002
+ || currentAction.stage !== reservation.attempt.currentStage
1003
+ || reservation.completedActionCount !== reservation.attempt.actionIndex)) {
1004
+ return refuse(ENVIRONMENT_CODES.STATE_RECORD_MALFORMED, 'drain attempt input is malformed', 'drain attempt');
1005
+ }
1006
+ const committedAt = new Date().toISOString();
1007
+ const priorRevision = reservation.attempt?.status === 'failed'
1008
+ ? reservation.attempt.failedRevision
1009
+ : reservation.progressRevision;
1010
+ const validThrough = new Date(Date.parse(committedAt) + currentAction.validForMs).toISOString();
1011
+ const attempt = {
1012
+ attemptId: randomBytes(32).toString('hex'), reservationId: reservation.reservationId,
1013
+ purpose: reservation.purpose, targetIds: reservation.targetIds, generation: reservation.generation,
1014
+ actionIndex: reservation.completedActionCount, currentTarget: currentAction.targetId, currentStage: currentAction.stage,
1015
+ committedRevision: priorRevision + 1, committedAt, validThrough,
1016
+ status: 'active', failedRevision: null, failedAt: null,
1017
+ };
1018
+ const fence = drainExecutionFence(attempt, namespace.paths.coordination(reservation.stackId));
1019
+ if (!fence)
1020
+ return refuse(ENVIRONMENT_CODES.STATE_RECORD_MALFORMED, 'drain attempt execution fence is malformed', 'drain attempt');
1021
+ const transaction = await transactClaim(namespace, reservation.stackId, reservation.requesterId, (current) => {
1022
+ const reduced = current === null ? undefined : reduceDrainAttemptState(current, reservation, attempt, reservation.attempt !== null);
1023
+ if (!reduced) {
1024
+ return abortStateTransaction(refuse(ENVIRONMENT_CODES.CLAIM_CO_TENANT, 'drain attempt requires an exact unstarted or failed reservation', reservation.stackId));
1025
+ }
1026
+ return coordinationStateRecord(namespace, reservation.stackId, current, committedAt, reduced);
1027
+ });
1028
+ if ('diagnostics' in transaction || 'attempt' in transaction)
1029
+ return transaction;
1030
+ if (transaction.kind !== 'complete') {
1031
+ return transaction.kind === 'missing'
1032
+ ? refuse(ENVIRONMENT_CODES.STATE_RECORD_MALFORMED, 'drain attempt returned no record', transaction.path)
1033
+ : stateRefusal(transaction);
1034
+ }
1035
+ const value = decodeCoordinationRecord(transaction.value, reservation.stackId);
1036
+ if (!value?.drain?.attempt || value.drain.attempt.attemptId !== attempt.attemptId
1037
+ || transaction.value.revision !== attempt.committedRevision) {
1038
+ return refuse(ENVIRONMENT_CODES.STATE_RECORD_MALFORMED, 'published drain attempt is malformed', reservation.stackId);
1039
+ }
1040
+ return { kind: 'complete', reservation: value.drain, attempt: value.drain.attempt, fence };
1041
+ }
1042
+ function sameSnapshot(left, right) {
1043
+ const leftSnapshot = snapshotJson(left);
1044
+ const rightSnapshot = snapshotJson(right);
1045
+ return leftSnapshot !== undefined && rightSnapshot !== undefined
1046
+ && JSON.stringify(leftSnapshot) === JSON.stringify(rightSnapshot);
1047
+ }
1048
+ function planningRecordPrecedesAttempt(record, reservation) {
1049
+ const snapshot = snapshotDataOnly(record);
1050
+ if (!snapshot || reservation.attempt === null || snapshot.subjectId !== reservation.stackId)
1051
+ return undefined;
1052
+ const value = decodeCoordinationRecord(snapshot, reservation.stackId);
1053
+ if (value === undefined)
1054
+ return undefined;
1055
+ if (value.drain == null) {
1056
+ // A record from before the reservation plans only the first try of an action: once a try
1057
+ // failed, a retry must be planned from the published failure, never from the original plan.
1058
+ return reservation.expectedCoordinationRevision === snapshot.revision
1059
+ && reservation.coordinationRevision === snapshot.revision + 1
1060
+ && reservation.attempt.committedRevision === reservation.progressRevision + 1 ? snapshot : undefined;
1061
+ }
1062
+ if (reservation.attempt.committedRevision !== snapshot.revision + 1)
1063
+ return undefined;
1064
+ if (value.drain.attempt === null) {
1065
+ if (snapshot.revision !== value.drain.progressRevision
1066
+ || !sameSnapshot({ ...reservation, attempt: null }, value.drain))
1067
+ return undefined;
1068
+ return snapshot;
1069
+ }
1070
+ const failed = value.drain.attempt;
1071
+ if (failed.status !== 'failed' || failed.failedRevision !== snapshot.revision
1072
+ || failed.actionIndex !== reservation.attempt.actionIndex
1073
+ || failed.currentStage !== reservation.attempt.currentStage
1074
+ || failed.currentTarget !== reservation.attempt.currentTarget
1075
+ || failed.reservationId !== reservation.attempt.reservationId
1076
+ || failed.purpose !== reservation.attempt.purpose
1077
+ || failed.generation !== reservation.attempt.generation
1078
+ || !sameTargets(failed.targetIds, reservation.attempt.targetIds)
1079
+ || !sameSnapshot({ ...reservation, attempt: failed }, value.drain))
1080
+ return undefined;
1081
+ return snapshot;
1082
+ }
1083
+ /**
1084
+ * Binds an active reclaim attempt to the coordination record its removal was planned from. Undefined
1085
+ * when that record does not directly precede the attempt, so a plan never runs under another try.
1086
+ */
1087
+ export function reclaimDrainStep(input) {
1088
+ const { reservation } = input;
1089
+ const attempt = reservation.attempt;
1090
+ const action = attempt === null ? undefined : reservation.actions[attempt.actionIndex];
1091
+ if (!attempt || attempt.status !== 'active' || attempt.currentStage !== 'reclaim'
1092
+ || action?.stage !== 'reclaim' || action.targetId !== attempt.currentTarget)
1093
+ return undefined;
1094
+ const planningRecord = planningRecordPrecedesAttempt(input.planningRecord, reservation);
1095
+ return planningRecord === undefined ? undefined : { planningRecord, reservation, failedAt: null };
1096
+ }
1097
+ function drainExecutionFence(attempt, address) {
1098
+ const attemptId = attempt.attemptId;
1099
+ const markerPath = `${address.path}.${attemptId}.executor.json`;
1100
+ const socketRoot = join(platform() === 'darwin' ? '/private/tmp' : '/tmp', `cockpit-exec-${process.getuid?.() ?? 0}`);
1101
+ const socketPath = join(socketRoot, `${attemptId.slice(0, 24)}.sock`);
1102
+ return /^[a-f0-9]{64}$/.test(attemptId) && isAbsolute(markerPath) && normalize(markerPath) === markerPath
1103
+ && isAbsolute(socketPath) && normalize(socketPath) === socketPath && Buffer.byteLength(socketPath) <= 103
1104
+ ? { attemptId, markerPath, socketPath } : undefined;
1105
+ }
1106
+ /** The coordination value the outcome publishes over `record`, or undefined when it no longer applies. */
1107
+ export function reduceDrainOutcome(input) {
1108
+ const { outcome, record } = input;
1109
+ if (outcome.kind === 'failure')
1110
+ return reduceDrainAttemptFailure(record, outcome.reservation, outcome.failedAt);
1111
+ const attempt = outcome.reservation.attempt;
1112
+ return attempt !== null && attempt.actionIndex === outcome.reservation.actions.length - 1
1113
+ ? reduceDrain(record, outcome.reservation, outcome.fact)
1114
+ : reduceDrainAttemptSuccess(record, outcome.reservation, outcome.fact);
1115
+ }
1116
+ function outcomeObservedAt(outcome) {
1117
+ return outcome.kind === 'failure' ? outcome.failedAt : outcome.fact.observedAt;
1118
+ }
1119
+ /**
1120
+ * Publishes a settled drain outcome to the coordination record. The publication admits it only while
1121
+ * the record holds the outcome's exact active attempt, so an outcome publishes at most once.
1122
+ */
1123
+ export async function commitDrainOutcome(input) {
1124
+ const { outcome, stackId, beforePublish } = input;
1125
+ const namespace = snapshotStateNamespace(input.namespace);
1126
+ const reservation = outcome.reservation;
1127
+ const notBefore = outcomeObservedAt(outcome);
1128
+ // Ownership and the observation time come from the reservation and this clock, never the caller.
1129
+ if (!namespace || stackId !== reservation.stackId || reservation.requesterId !== reservation.holderId
1130
+ || !holderIdentity.test(reservation.requesterId) || !isCanonicalTimestamp(notBefore)) {
1131
+ return refuse(ENVIRONMENT_CODES.EXECUTION_PRECONDITION_CHANGED, 'drain outcome publication requires exact coordination authority and ownership', stackId);
1132
+ }
1133
+ const address = namespace.paths.coordination(stackId);
1134
+ let preparedAt;
1135
+ const publication = await transactState(namespace, address, {
1136
+ ownerId: reservation.requesterId,
1137
+ contentionTimeoutMs: 2_000,
1138
+ ...(beforePublish ? { beforePublish } : {}),
1139
+ beforeCommit: () => {
1140
+ const now = Date.now();
1141
+ return !preparedAt || now < Date.parse(notBefore) || now < Date.parse(preparedAt)
1142
+ ? abortStateTransaction(refuse(ENVIRONMENT_CODES.EXECUTION_PRECONDITION_CHANGED, 'drain outcome publication clock changed before commit', stackId))
1143
+ : undefined;
1144
+ },
1145
+ }, (current) => {
1146
+ const updatedAt = new Date().toISOString();
1147
+ if (!current || Date.parse(updatedAt) < Date.parse(notBefore)
1148
+ || current.kind !== 'coordination' || current.cockpitId !== address.cockpitId || current.subjectId !== stackId) {
1149
+ return abortStateTransaction(refuse(ENVIRONMENT_CODES.EXECUTION_PRECONDITION_CHANGED, 'drain outcome publication requires an exact current record and a non-regressive observation time', stackId));
1150
+ }
1151
+ const value = reduceDrainOutcome({ outcome, record: current });
1152
+ if (!value) {
1153
+ return abortStateTransaction(refuse(ENVIRONMENT_CODES.EXECUTION_PRECONDITION_CHANGED, 'drain outcome publication no longer matches the current coordination record', stackId));
1154
+ }
1155
+ preparedAt = updatedAt;
1156
+ return coordinationStateRecord(namespace, stackId, current, updatedAt, value);
1157
+ });
1158
+ return 'aborted' in publication ? publication.value : publication;
1159
+ }
1160
+ /**
1161
+ * Whether `record` is the one the step's outcome published: its removal progressed, or (for a step
1162
+ * that never started) its failure was recorded. Anything else is unmatched.
1163
+ */
1164
+ export function matchReclaimDrainContinuation(input) {
1165
+ const step = input.continuation;
1166
+ if (step === null)
1167
+ return 'unmatched';
1168
+ if (step.failedAt === null)
1169
+ return matchesDrainProgress(step, input.record) ? 'progressed' : 'unmatched';
1170
+ return matchesDrainFailure(step, step.failedAt, input.record) ? 'failed' : 'unmatched';
1171
+ }
1172
+ function followsPlanningRecord(step, record, notBefore) {
1173
+ const attempt = step.reservation.attempt;
1174
+ const snapshot = snapshotDataOnly(record);
1175
+ if (!attempt || !snapshot || snapshot.format !== step.planningRecord.format
1176
+ || snapshot.kind !== step.planningRecord.kind || snapshot.cockpitId !== step.planningRecord.cockpitId
1177
+ || snapshot.subjectId !== step.planningRecord.subjectId
1178
+ || snapshot.revision !== attempt.committedRevision + 1
1179
+ || !isCanonicalTimestamp(snapshot.updatedAt)
1180
+ || Date.parse(snapshot.updatedAt) < Date.parse(notBefore)
1181
+ || Date.parse(snapshot.updatedAt) > Date.now())
1182
+ return undefined;
1183
+ const predecessor = decodeCoordinationRecord(step.planningRecord, step.reservation.stackId);
1184
+ const value = decodeCoordinationRecord(snapshot, step.reservation.stackId);
1185
+ if (!predecessor || !value?.drain
1186
+ || !sameSnapshot(value.holders, predecessor.holders)
1187
+ || !sameSnapshot(value.lease, predecessor.lease)
1188
+ || value.lastReleasedHolder !== predecessor.lastReleasedHolder)
1189
+ return undefined;
1190
+ return value;
1191
+ }
1192
+ function matchesDrainProgress(step, record) {
1193
+ const attempt = step.reservation.attempt;
1194
+ const value = followsPlanningRecord(step, record, attempt.committedAt);
1195
+ return value !== undefined && sameSnapshot(value.drain, {
1196
+ ...step.reservation,
1197
+ completedActionCount: attempt.actionIndex + 1,
1198
+ progressRevision: attempt.committedRevision + 1,
1199
+ attempt: null,
1200
+ });
1201
+ }
1202
+ function matchesDrainFailure(step, failedAt, record) {
1203
+ const attempt = step.reservation.attempt;
1204
+ const value = followsPlanningRecord(step, record, failedAt);
1205
+ return value !== undefined && sameSnapshot(value.drain, {
1206
+ ...step.reservation,
1207
+ attempt: { ...attempt, status: 'failed', failedRevision: attempt.committedRevision + 1, failedAt },
1208
+ });
1209
+ }
1210
+ function reduceDrainAttemptFailure(record, reservation, failedAt) {
1211
+ if (!reservation.attempt || reservation.attempt.status !== 'active'
1212
+ || record.revision !== reservation.attempt.committedRevision
1213
+ || !isCanonicalTimestamp(failedAt)
1214
+ || Date.parse(failedAt) < Date.parse(reservation.attempt.committedAt)
1215
+ || Date.parse(failedAt) > Date.now())
1216
+ return undefined;
1217
+ const value = decodeCoordinationRecord(record, reservation.stackId);
1218
+ const attempt = value?.drain?.attempt;
1219
+ if (!value?.drain || !attempt || attempt.status !== 'active'
1220
+ || value.drain.reservationId !== reservation.reservationId
1221
+ || !sameDrainAttempt(attempt, reservation.attempt))
1222
+ return undefined;
1223
+ return {
1224
+ ...value,
1225
+ drain: {
1226
+ ...value.drain,
1227
+ attempt: { ...attempt, status: 'failed',
1228
+ failedRevision: attempt.committedRevision + 1, failedAt },
1229
+ },
1230
+ };
1231
+ }
1232
+ function drainRepair(address, record, reservation) {
1233
+ if (!reservation.attempt || reservation.attempt.status !== 'active')
1234
+ return undefined;
1235
+ const fence = drainExecutionFence(reservation.attempt, address);
1236
+ return fence && exactRecordFingerprint(record) ? { kind: 'repairable', fence, record } : undefined;
1237
+ }
1238
+ /**
1239
+ * Records an interrupted attempt as failed once the executor's termination is proven for its fence.
1240
+ * The record must still be exactly the one inspected, so a concurrent change is never overwritten.
1241
+ */
1242
+ export async function settleInterruptedDrainRepair(input) {
1243
+ const namespace = snapshotStateNamespace(input.namespace);
1244
+ const stackId = input.record.subjectId;
1245
+ const fingerprint = exactRecordFingerprint(input.record);
1246
+ const reservation = validIdentity(stackId, stackIdentity) ? decodeCoordinationRecord(input.record, stackId)?.drain : undefined;
1247
+ const fence = namespace && reservation?.attempt?.status === 'active'
1248
+ ? drainExecutionFence(reservation.attempt, namespace.paths.coordination(stackId)) : undefined;
1249
+ if (!namespace || !fingerprint || !reservation?.attempt || !fence || !consumeFencedExecutionTermination(input.evidence, fence)) {
1250
+ return refuse(ENVIRONMENT_CODES.DRAIN_ATTEMPT_REPAIR_REQUIRED, 'interrupted drain termination evidence is unavailable', 'drain repair');
1251
+ }
1252
+ const failedAt = new Date().toISOString();
1253
+ return transactClaim(namespace, stackId, reservation.holderId, current => {
1254
+ const value = decodeCoordinationRecord(current, stackId);
1255
+ const attempt = value?.drain?.attempt;
1256
+ if (!current || exactRecordFingerprint(current) !== fingerprint || !value?.drain || !attempt
1257
+ || attempt.status !== 'active' || !sameDrainAttempt(attempt, reservation.attempt)) {
1258
+ return abortStateTransaction(refuse(ENVIRONMENT_CODES.DRAIN_ATTEMPT_REPAIR_REQUIRED, 'interrupted drain changed before executor termination could be recorded', stackId));
1259
+ }
1260
+ return coordinationStateRecord(namespace, stackId, current, failedAt, {
1261
+ ...value,
1262
+ drain: { ...value.drain, attempt: { ...attempt, status: 'failed',
1263
+ failedRevision: current.revision + 1, failedAt } },
1264
+ });
1265
+ });
1266
+ }
1267
+ export async function inspectDestructiveDrainRecovery(input) {
1268
+ let namespace;
1269
+ let stackId;
1270
+ let holderId;
1271
+ let purpose;
1272
+ try {
1273
+ if (!input || typeof input !== 'object' || Array.isArray(input)
1274
+ || !hasExactKeys(input, ['namespace', 'stackId', 'holderId', 'purpose']))
1275
+ throw new RefusalError('invalid');
1276
+ const descriptors = Object.getOwnPropertyDescriptors(input);
1277
+ if (!('value' in descriptors.namespace) || !('value' in descriptors.stackId)
1278
+ || !('value' in descriptors.holderId) || !('value' in descriptors.purpose))
1279
+ throw new RefusalError('invalid');
1280
+ namespace = authenticateStateNamespace(descriptors.namespace.value);
1281
+ const suppliedStackId = descriptors.stackId.value;
1282
+ const suppliedHolderId = descriptors.holderId.value;
1283
+ const suppliedPurpose = descriptors.purpose.value;
1284
+ if (typeof suppliedStackId !== 'string' || typeof suppliedHolderId !== 'string'
1285
+ || !validIdentity(suppliedStackId, stackIdentity) || !validIdentity(suppliedHolderId, holderIdentity)
1286
+ || suppliedPurpose !== 'down' && suppliedPurpose !== 'reset')
1287
+ throw new RefusalError('invalid');
1288
+ stackId = suppliedStackId;
1289
+ holderId = suppliedHolderId;
1290
+ purpose = suppliedPurpose;
1291
+ }
1292
+ catch {
1293
+ // A throwing accessor or proxy trap makes the input malformed, which the caller refuses.
1294
+ namespace = undefined;
1295
+ }
1296
+ if (!namespace || !stackId || !holderId || !purpose) {
1297
+ return refuse(ENVIRONMENT_CODES.STATE_RECORD_MALFORMED, 'destructive drain recovery inspection input is malformed', 'destructive drain recovery');
1298
+ }
1299
+ const address = namespace.paths.coordination(stackId);
1300
+ const stored = await readState(address);
1301
+ if (stored.kind === 'missing') {
1302
+ return refuse(ENVIRONMENT_CODES.STATE_RECORD_MALFORMED, 'destructive recovery requires current coordination state', address.path);
1303
+ }
1304
+ if (stored.kind !== 'complete')
1305
+ return stateRefusal(stored);
1306
+ const record = stored.value;
1307
+ const value = decodeCoordinationRecord(record, stackId);
1308
+ if (!value)
1309
+ return refuse(ENVIRONMENT_CODES.STATE_RECORD_MALFORMED, 'coordination record is malformed', address.path);
1310
+ if (value.drain == null) {
1311
+ const others = value.holders.filter((holder) => holder.holderId !== holderId);
1312
+ if (others.length > 0 && value.holders.some((holder) => holder.holderId === holderId)) {
1313
+ return refuse(ENVIRONMENT_CODES.CLAIM_CO_TENANT, liveHolderMessage(others), stackId);
1314
+ }
1315
+ if (value.holders.length !== 1 || value.holders[0]?.holderId !== holderId
1316
+ || value.lease === null || value.lease.released) {
1317
+ return refuse(ENVIRONMENT_CODES.CLAIM_CO_TENANT, 'destructive recovery requires exact retained holder and lease ownership', stackId);
1318
+ }
1319
+ return { kind: 'none' };
1320
+ }
1321
+ const reservation = value.drain;
1322
+ if (reservation.holderId !== holderId || reservation.requesterId !== holderId) {
1323
+ return refuse(ENVIRONMENT_CODES.CLAIM_CO_TENANT, 'persisted drain belongs to another holder', stackId);
1324
+ }
1325
+ if (reservation.purpose !== purpose) {
1326
+ return refuse(ENVIRONMENT_CODES.DRAIN_ATTEMPT_REPAIR_REQUIRED, 'persisted drain requires explicit repair', stackId);
1327
+ }
1328
+ if (reservation.attempt?.status === 'active') {
1329
+ return drainRepair(address, record, reservation)
1330
+ ?? refuse(ENVIRONMENT_CODES.DRAIN_ATTEMPT_REPAIR_REQUIRED, 'the interrupted destructive executor has no repairable execution fence', stackId);
1331
+ }
1332
+ const request = snapshotDrainRequest({
1333
+ purpose: reservation.purpose,
1334
+ stackId: reservation.stackId,
1335
+ holderId: reservation.holderId,
1336
+ requesterId: reservation.requesterId,
1337
+ configSha256: reservation.configSha256,
1338
+ planDigest: reservation.planDigest,
1339
+ operationBasis: reservation.operationBasis,
1340
+ generation: reservation.generation,
1341
+ targetIds: reservation.targetIds,
1342
+ actions: reservation.actions,
1343
+ expectedCoordinationRevision: reservation.expectedCoordinationRevision,
1344
+ });
1345
+ return request ? { kind: 'recoverable', request }
1346
+ : refuse(ENVIRONMENT_CODES.STATE_RECORD_MALFORMED, 'persisted destructive drain is malformed', address.path);
1347
+ }
1348
+ export async function inspectSleepDrainRecovery(input) {
1349
+ let namespace;
1350
+ let stackId;
1351
+ let holderId;
1352
+ try {
1353
+ if (!input || typeof input !== 'object' || Array.isArray(input)
1354
+ || !hasExactKeys(input, ['namespace', 'stackId', 'holderId']))
1355
+ throw new RefusalError('invalid');
1356
+ const descriptors = Object.getOwnPropertyDescriptors(input);
1357
+ if (!('value' in descriptors.namespace) || !('value' in descriptors.stackId)
1358
+ || !('value' in descriptors.holderId))
1359
+ throw new RefusalError('invalid');
1360
+ namespace = authenticateStateNamespace(descriptors.namespace.value);
1361
+ const suppliedStackId = descriptors.stackId.value;
1362
+ const suppliedHolderId = descriptors.holderId.value;
1363
+ if (typeof suppliedStackId !== 'string' || typeof suppliedHolderId !== 'string'
1364
+ || !validIdentity(suppliedStackId, stackIdentity) || !validIdentity(suppliedHolderId, holderIdentity))
1365
+ throw new RefusalError('invalid');
1366
+ stackId = suppliedStackId;
1367
+ holderId = suppliedHolderId;
1368
+ }
1369
+ catch {
1370
+ // A throwing accessor or proxy trap makes the input malformed, which the caller refuses.
1371
+ namespace = undefined;
1372
+ }
1373
+ if (!namespace || !stackId || !holderId) {
1374
+ return refuse(ENVIRONMENT_CODES.STATE_RECORD_MALFORMED, 'sleep drain recovery inspection input is malformed', 'sleep drain recovery');
1375
+ }
1376
+ const address = namespace.paths.coordination(stackId);
1377
+ const stored = await readState(address);
1378
+ if (stored.kind === 'missing')
1379
+ return { kind: 'none' };
1380
+ if (stored.kind !== 'complete')
1381
+ return stateRefusal(stored);
1382
+ const record = stored.value;
1383
+ const value = decodeCoordinationRecord(record, stackId);
1384
+ if (!value)
1385
+ return refuse(ENVIRONMENT_CODES.STATE_RECORD_MALFORMED, 'coordination record is malformed', address.path);
1386
+ if (value.drain == null)
1387
+ return { kind: 'none' };
1388
+ const reservation = value.drain;
1389
+ if (reservation.holderId !== holderId || reservation.requesterId !== holderId) {
1390
+ return refuse(ENVIRONMENT_CODES.CLAIM_CO_TENANT, 'persisted drain belongs to another holder', stackId);
1391
+ }
1392
+ if (reservation.purpose !== 'sleep') {
1393
+ return refuse(ENVIRONMENT_CODES.DRAIN_ATTEMPT_REPAIR_REQUIRED, 'persisted drain requires explicit repair', stackId);
1394
+ }
1395
+ if (reservation.attempt?.status === 'active') {
1396
+ return drainRepair(address, record, reservation)
1397
+ ?? refuse(ENVIRONMENT_CODES.DRAIN_ATTEMPT_REPAIR_REQUIRED, 'the interrupted sleep executor has no repairable execution fence', stackId);
1398
+ }
1399
+ const runtimeStop = reservation.actions[0];
1400
+ if (!runtimeStop || runtimeStop.stage !== 'runtime-stop' || runtimeStop.targetId !== stackId || exactRecordFingerprint(record) === undefined) {
1401
+ return refuse(ENVIRONMENT_CODES.STATE_RECORD_MALFORMED, 'persisted sleep drain is malformed', address.path);
1402
+ }
1403
+ return {
1404
+ kind: 'recoverable',
1405
+ generation: reservation.generation,
1406
+ runtimeStopValidForMs: runtimeStop.validForMs,
1407
+ record,
1408
+ };
1409
+ }
1410
+ /**
1411
+ * Resumes a persisted sleep drain from fresh planning evidence. The stored record must still be the
1412
+ * one inspected and the fresh request must name its revision and the same work.
1413
+ */
1414
+ export async function recoverSleepDrainReservation(input) {
1415
+ let namespace;
1416
+ let request;
1417
+ try {
1418
+ namespace = authenticateStateNamespace(input.namespace);
1419
+ request = snapshotDrainRequest(input.request);
1420
+ }
1421
+ catch {
1422
+ // A throwing accessor or proxy trap makes the input malformed, which the caller refuses.
1423
+ namespace = undefined;
1424
+ }
1425
+ const fingerprint = exactRecordFingerprint(input.record);
1426
+ const inspected = fingerprint === undefined ? undefined : decodeCoordinationRecord(input.record, input.record.subjectId)?.drain;
1427
+ if (!namespace || !request || !inspected) {
1428
+ return refuse(ENVIRONMENT_CODES.STATE_RECORD_MALFORMED, 'sleep drain recovery input is malformed', 'sleep drain recovery');
1429
+ }
1430
+ const address = namespace.paths.coordination(inspected.stackId);
1431
+ const stored = await readState(address);
1432
+ if (stored.kind !== 'complete') {
1433
+ return stored.kind === 'missing'
1434
+ ? refuse(ENVIRONMENT_CODES.EXECUTION_PRECONDITION_CHANGED, 'persisted sleep drain disappeared', stored.path)
1435
+ : stateRefusal(stored);
1436
+ }
1437
+ if (exactRecordFingerprint(stored.value) !== fingerprint) {
1438
+ return refuse(ENVIRONMENT_CODES.EXECUTION_PRECONDITION_CHANGED, 'persisted sleep drain changed before recovery', address.path);
1439
+ }
1440
+ if (request.expectedCoordinationRevision !== stored.value.revision) {
1441
+ return refuse(ENVIRONMENT_CODES.EXECUTION_PRECONDITION_CHANGED, 'fresh sleep drain planning evidence does not name the current coordination revision', address.path);
1442
+ }
1443
+ const persistedRequest = {
1444
+ ...request,
1445
+ expectedCoordinationRevision: inspected.expectedCoordinationRevision,
1446
+ };
1447
+ if (request.purpose !== 'sleep' || inspected.purpose !== 'sleep' || inspected.attempt?.status === 'active'
1448
+ || !sameDrainRequest(inspected, persistedRequest)) {
1449
+ return refuse(ENVIRONMENT_CODES.CLAIM_CO_TENANT, 'persisted sleep drain does not match fresh planning evidence', inspected.stackId);
1450
+ }
1451
+ return { kind: 'complete', reservation: inspected };
1452
+ }
1453
+ export async function recoverDrainReservation(input) {
1454
+ let namespace;
1455
+ let request;
1456
+ try {
1457
+ if (!input || typeof input !== 'object' || Array.isArray(input) || !hasExactKeys(input, ['namespace', 'request']))
1458
+ throw new RefusalError('invalid');
1459
+ const descriptors = Object.getOwnPropertyDescriptors(input);
1460
+ if (!('value' in descriptors.namespace) || !('value' in descriptors.request))
1461
+ throw new RefusalError('invalid');
1462
+ namespace = snapshotStateNamespace(descriptors.namespace.value);
1463
+ request = snapshotDrainRequest(descriptors.request.value);
1464
+ }
1465
+ catch {
1466
+ // A throwing accessor or proxy trap makes the input malformed, which the caller refuses.
1467
+ namespace = undefined;
1468
+ request = undefined;
1469
+ }
1470
+ if (!namespace || !request)
1471
+ return refuse(ENVIRONMENT_CODES.STATE_RECORD_MALFORMED, 'drain recovery input is malformed', 'drain recovery');
1472
+ const stored = await readState(namespace.paths.coordination(request.stackId));
1473
+ if (stored.kind !== 'complete') {
1474
+ return stored.kind === 'missing'
1475
+ ? refuse(ENVIRONMENT_CODES.STATE_RECORD_MALFORMED, 'drain recovery found no coordination record', stored.path)
1476
+ : stateRefusal(stored);
1477
+ }
1478
+ const value = decodeCoordinationRecord(stored.value, request.stackId);
1479
+ if (!value?.drain || !sameDrainRequest(value.drain, request)) {
1480
+ return refuse(ENVIRONMENT_CODES.CLAIM_CO_TENANT, 'persisted drain does not match the recovery request', request.stackId);
1481
+ }
1482
+ if (value.drain.attempt?.status === 'active') {
1483
+ return refuse(ENVIRONMENT_CODES.DRAIN_ATTEMPT_REPAIR_REQUIRED, 'the active destructive attempt requires explicit repair', request.stackId);
1484
+ }
1485
+ if (stored.value.revision !== (value.drain.attempt?.failedRevision ?? value.drain.progressRevision)) {
1486
+ return refuse(ENVIRONMENT_CODES.CLAIM_CO_TENANT, 'persisted drain does not match its coordination revision', request.stackId);
1487
+ }
1488
+ return { kind: 'complete', reservation: value.drain };
1489
+ }
1490
+ function failedReclaimRecoveryTarget(action) {
1491
+ return {
1492
+ targetId: action.targetId,
1493
+ targetCommitment: { ...action.targetCommitment },
1494
+ };
1495
+ }
1496
+ /**
1497
+ * The committed targets of a recovered reservation whose reclaim try failed, read from the record
1498
+ * that published that failure. Undefined for any other reservation or record.
1499
+ */
1500
+ export function inspectFailedReclaimRecovery(input) {
1501
+ const { reservation, record } = input;
1502
+ const value = exactRecordFingerprint(record) === undefined ? undefined : decodeCoordinationRecord(record, reservation.stackId);
1503
+ if (!value?.drain || !sameSnapshot(value.drain, reservation))
1504
+ return undefined;
1505
+ const attempt = reservation.attempt;
1506
+ const action = attempt && reservation.actions[attempt.actionIndex];
1507
+ if (!attempt || attempt.status !== 'failed' || attempt.currentStage !== 'reclaim'
1508
+ || reservation.purpose !== 'down' && reservation.purpose !== 'reset'
1509
+ || record.revision !== attempt.failedRevision || !isCanonicalTimestamp(record.updatedAt)
1510
+ || Date.parse(record.updatedAt) < Date.parse(attempt.failedAt) || Date.parse(record.updatedAt) > Date.now()
1511
+ || Date.parse(reservation.reservedAt) > Date.parse(attempt.committedAt)
1512
+ || reservation.progressRevision !== attempt.committedRevision - 1
1513
+ || attempt.failedRevision !== attempt.committedRevision + 1
1514
+ || reservation.completedActionCount !== attempt.actionIndex
1515
+ || !action || action.stage !== 'reclaim' || action.targetId !== attempt.currentTarget
1516
+ || !action.targetCommitment
1517
+ || !validDrainActions(reservation.purpose, reservation.stackId, reservation.targetIds, reservation.actions))
1518
+ return undefined;
1519
+ const reclaimActions = reservation.actions.filter((candidate) => candidate.stage === 'reclaim');
1520
+ const currentIndex = attempt.actionIndex - 1;
1521
+ if (currentIndex < 0 || reclaimActions[currentIndex] !== action)
1522
+ return undefined;
1523
+ return {
1524
+ purpose: reservation.purpose,
1525
+ stackId: reservation.stackId,
1526
+ configSha256: reservation.configSha256,
1527
+ planDigest: reservation.planDigest,
1528
+ freshAfter: record.updatedAt,
1529
+ completed: reclaimActions.slice(0, currentIndex).map(failedReclaimRecoveryTarget),
1530
+ current: failedReclaimRecoveryTarget(action),
1531
+ pending: reclaimActions.slice(currentIndex + 1).map(failedReclaimRecoveryTarget),
1532
+ };
1533
+ }
1534
+ function sameDrainFact(reservation, fact) {
1535
+ return reservation.attempt !== null && fact.purpose === reservation.purpose && fact.stackId === reservation.stackId
1536
+ && fact.configSha256 === reservation.configSha256 && fact.planDigest === reservation.planDigest
1537
+ && fact.operationBasis === reservation.operationBasis
1538
+ && fact.coordinationRevision === reservation.coordinationRevision
1539
+ && fact.reservationId === reservation.reservationId && fact.reservedAt === reservation.reservedAt
1540
+ && fact.generation === reservation.attempt.generation && isCanonicalTimestamp(fact.observedAt)
1541
+ && fact.attemptId === reservation.attempt.attemptId
1542
+ && fact.attemptRevision === reservation.attempt.committedRevision
1543
+ && fact.attemptCommittedAt === reservation.attempt.committedAt
1544
+ && fact.validThrough === reservation.attempt.validThrough
1545
+ && fact.actionIndex === reservation.attempt.actionIndex
1546
+ && fact.currentTarget === reservation.attempt.currentTarget
1547
+ && fact.currentStage === reservation.attempt.currentStage
1548
+ && Date.parse(fact.observedAt) >= Date.parse(reservation.attempt.committedAt)
1549
+ && Date.parse(fact.observedAt) <= Date.parse(reservation.attempt.validThrough)
1550
+ && Date.parse(fact.observedAt) <= Date.now();
1551
+ }
1552
+ const runtimeStoppedKeys = [
1553
+ 'purpose', 'stackId', 'configSha256', 'planDigest', 'operationBasis', 'coordinationRevision',
1554
+ 'reservationId', 'reservedAt', 'generation', 'attemptId', 'attemptRevision', 'attemptCommittedAt',
1555
+ 'validThrough', 'actionIndex', 'currentTarget', 'currentStage', 'observedAt',
1556
+ ];
1557
+ function isExactDrainSuccess(reservation, fact) {
1558
+ if (!reservation.attempt)
1559
+ return false;
1560
+ if (reservation.attempt.currentStage === 'runtime-stop') {
1561
+ return hasExactKeys(fact, runtimeStoppedKeys) && sameDrainFact(reservation, fact);
1562
+ }
1563
+ if (!hasExactKeys(fact, [...runtimeStoppedKeys, 'remainingTargetIds']))
1564
+ return false;
1565
+ const reclaim = fact;
1566
+ const remaining = snapshotDrainTargetsAllowEmpty(reclaim.remainingTargetIds);
1567
+ const expected = reservation.actions.slice(reservation.attempt.actionIndex + 1).map(({ targetId }) => targetId);
1568
+ return sameDrainFact(reservation, reclaim) && remaining !== undefined && sameTargets(remaining, expected);
1569
+ }
1570
+ function snapshotDrainTargetsAllowEmpty(value) {
1571
+ if (Array.isArray(value) && value.length === 0)
1572
+ return [];
1573
+ return snapshotDrainTargets(value);
1574
+ }
1575
+ function reduceDrainAttemptSuccess(record, reservation, success) {
1576
+ if (!reservation.attempt || reservation.attempt.status !== 'active'
1577
+ || reservation.attempt.actionIndex >= reservation.actions.length - 1
1578
+ || record.revision !== reservation.attempt.committedRevision
1579
+ || !isExactDrainSuccess(reservation, success))
1580
+ return undefined;
1581
+ const value = decodeCoordinationRecord(record, reservation.stackId);
1582
+ if (!value?.drain?.attempt || !sameDrainRequest(value.drain, reservation)
1583
+ || value.drain.reservationId !== reservation.reservationId
1584
+ || !sameDrainAttempt(value.drain.attempt, reservation.attempt))
1585
+ return undefined;
1586
+ return {
1587
+ ...value,
1588
+ drain: {
1589
+ ...value.drain,
1590
+ completedActionCount: value.drain.completedActionCount + 1,
1591
+ progressRevision: value.drain.attempt.committedRevision + 1,
1592
+ attempt: null,
1593
+ },
1594
+ };
1595
+ }
1596
+ function reduceDrain(record, reservation, success) {
1597
+ if (!reservation.attempt
1598
+ || reservation.attempt.actionIndex !== reservation.actions.length - 1
1599
+ || reservation.completedActionCount !== reservation.actions.length - 1
1600
+ || !Number.isSafeInteger(record.revision) || record.revision !== reservation.attempt.committedRevision
1601
+ || !isExactDrainSuccess(reservation, success))
1602
+ return undefined;
1603
+ const value = decodeCoordinationRecord(record, reservation.stackId);
1604
+ if (!value?.drain || !sameDrainRequest(value.drain, reservation)
1605
+ || value.drain.coordinationRevision !== reservation.coordinationRevision
1606
+ || value.drain.reservationId !== reservation.reservationId
1607
+ || value.drain.reservedAt !== reservation.reservedAt
1608
+ || !value.drain.attempt || !sameDrainAttempt(value.drain.attempt, reservation.attempt)
1609
+ || value.drain.attempt.status !== 'active')
1610
+ return undefined;
1611
+ return {
1612
+ holders: [],
1613
+ lease: value.lease === null ? null : { ...value.lease, released: true },
1614
+ lastReleasedHolder: value.drain.holderId,
1615
+ drain: null,
1616
+ };
1617
+ }
1618
+ function stateRefusal(result) {
1619
+ return refuse(result.code, result.message, result.target, 'recovery' in result ? result.recovery : undefined);
1620
+ }
1621
+ async function transactClaim(namespace, stackId, ownerId, update, beforeCommit) {
1622
+ const result = await transactState(namespace, namespace.paths.coordination(stackId), { ownerId, contentionTimeoutMs: 2_000, beforeCommit }, update);
1623
+ const aborted = result;
1624
+ return aborted.aborted === true ? aborted.value : result;
1625
+ }
1626
+ export async function releaseClaim(input) {
1627
+ let request;
1628
+ try {
1629
+ const namespaceProperty = Object.getOwnPropertyDescriptor(input, 'namespace');
1630
+ if (!namespaceProperty || !('value' in namespaceProperty))
1631
+ throw new RefusalError('invalid namespace property');
1632
+ const stableNamespace = snapshotStateNamespace(namespaceProperty.value);
1633
+ if (!stableNamespace)
1634
+ throw new RefusalError('invalid namespace');
1635
+ request = {
1636
+ namespace: stableNamespace,
1637
+ stackId: input.stackId,
1638
+ requesterId: input.requesterId,
1639
+ holderId: input.holderId,
1640
+ now: input.now,
1641
+ };
1642
+ }
1643
+ catch (error) {
1644
+ return refuse(ENVIRONMENT_CODES.STATE_RECORD_MALFORMED, `claim release input could not be read safely: ${errorCause(error)}`, 'claim release input');
1645
+ }
1646
+ if (!validIdentity(request.stackId, stackIdentity)
1647
+ || !validIdentity(request.requesterId, holderIdentity)
1648
+ || !validIdentity(request.holderId, holderIdentity)
1649
+ || !isCanonicalTimestamp(request.now)) {
1650
+ return refuse(ENVIRONMENT_CODES.STATE_RECORD_MALFORMED, 'claim release requires canonical stack, holder, and timestamp values', String(request.stackId ?? ''));
1651
+ }
1652
+ // A holder whose session is gone is released by `releaseDeadSessionHolders`, which proves the
1653
+ // death itself; no other holder's claim is this function's to drop.
1654
+ if (request.requesterId !== request.holderId) {
1655
+ return refuse(ENVIRONMENT_CODES.CLAIM_CO_TENANT, 'a holder can release only its own claim', request.holderId);
1656
+ }
1657
+ const result = await transactClaim(request.namespace, request.stackId, request.requesterId, (current) => {
1658
+ const value = decodeCoordinationRecord(current, request.stackId);
1659
+ if (!value)
1660
+ return abortStateTransaction(refuse(ENVIRONMENT_CODES.STATE_RECORD_MALFORMED, 'coordination record is malformed', request.stackId));
1661
+ if (isDrainActive(value))
1662
+ return abortStateTransaction(refuse(ENVIRONMENT_CODES.CLAIM_CO_TENANT, 'claim release is blocked while the stack has an exclusive mutation', request.stackId));
1663
+ const holders = value.holders.filter((holder) => holder.holderId !== request.holderId);
1664
+ if (holders.length === value.holders.length) {
1665
+ if (holders.length === 0) {
1666
+ return abortStateTransaction(refuse(ENVIRONMENT_CODES.CLAIM_CO_TENANT, 'claim release requires an active non-final holder', request.stackId));
1667
+ }
1668
+ return abortStateTransaction({
1669
+ kind: 'complete', change: 'unchanged', decision: 'retain',
1670
+ remaining: holders.map((holder) => holder.holderId),
1671
+ });
1672
+ }
1673
+ if (holders.length === 0) {
1674
+ return abortStateTransaction(refuse(ENVIRONMENT_CODES.CLAIM_CO_TENANT, 'the final holder must reserve and complete a drain before releasing the stack', request.stackId));
1675
+ }
1676
+ return coordinationStateRecord(request.namespace, request.stackId, current, new Date().toISOString(), {
1677
+ holders,
1678
+ lease: value.lease,
1679
+ lastReleasedHolder: request.holderId,
1680
+ drain: value.drain,
1681
+ });
1682
+ });
1683
+ if ('diagnostics' in result || 'remaining' in result)
1684
+ return result;
1685
+ if (result.kind === 'missing') {
1686
+ return refuse(ENVIRONMENT_CODES.STATE_RECORD_MALFORMED, 'claim transaction returned no record', result.path);
1687
+ }
1688
+ if (result.kind !== 'complete')
1689
+ return stateRefusal(result);
1690
+ const value = decodeCoordinationRecord(result.value, request.stackId);
1691
+ if (!value) {
1692
+ return refuse(ENVIRONMENT_CODES.STATE_RECORD_MALFORMED, 'published claim record is malformed', result.value.subjectId);
1693
+ }
1694
+ const remaining = value.holders.map((holder) => holder.holderId);
1695
+ return {
1696
+ kind: 'complete',
1697
+ change: 'released',
1698
+ decision: remaining.length === 0 ? 'stop' : 'retain',
1699
+ remaining,
1700
+ };
1701
+ }