@b00dea/cockpit 0.0.0-bootstrap.0 → 0.1.0

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 (363) hide show
  1. package/CHANGELOG.md +375 -0
  2. package/LICENSE +21 -0
  3. package/README.md +21 -1
  4. package/bin/cockpit.mjs +33 -0
  5. package/cockpit.schema.json +2574 -0
  6. package/dist/autonomy.d.ts +44 -0
  7. package/dist/autonomy.js +156 -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 +62 -0
  11. package/dist/browser/commands.js +349 -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 +117 -0
  17. package/dist/browser/runtime.js +183 -0
  18. package/dist/browser/surfaces.d.ts +141 -0
  19. package/dist/browser/surfaces.js +556 -0
  20. package/dist/browser/ui-gate.d.ts +15 -0
  21. package/dist/browser/ui-gate.js +18 -0
  22. package/dist/cli-guard.d.ts +5 -0
  23. package/dist/cli-guard.js +71 -0
  24. package/dist/cli-main.d.ts +139 -0
  25. package/dist/cli-main.js +1405 -0
  26. package/dist/cli.d.ts +3 -0
  27. package/dist/cli.js +16 -0
  28. package/dist/command-table.d.ts +113 -0
  29. package/dist/command-table.js +512 -0
  30. package/dist/config/load.d.ts +171 -0
  31. package/dist/config/load.js +345 -0
  32. package/dist/config/sibling-toml.d.ts +48 -0
  33. package/dist/config/sibling-toml.js +160 -0
  34. package/dist/config/surface-tools-toml.d.ts +9 -0
  35. package/dist/config/surface-tools-toml.js +37 -0
  36. package/dist/config/verified-write.d.ts +43 -0
  37. package/dist/config/verified-write.js +89 -0
  38. package/dist/diagnostics.d.ts +26 -0
  39. package/dist/diagnostics.js +335 -0
  40. package/dist/emit.d.ts +22 -0
  41. package/dist/emit.js +43 -0
  42. package/dist/entrypoint-kinds.d.ts +3 -0
  43. package/dist/entrypoint-kinds.js +11 -0
  44. package/dist/environment/application/destructive.d.ts +6 -0
  45. package/dist/environment/application/destructive.js +425 -0
  46. package/dist/environment/application/doctor.d.ts +89 -0
  47. package/dist/environment/application/doctor.js +235 -0
  48. package/dist/environment/application/guard.d.ts +43 -0
  49. package/dist/environment/application/guard.js +622 -0
  50. package/dist/environment/application/health.d.ts +134 -0
  51. package/dist/environment/application/health.js +379 -0
  52. package/dist/environment/application/lifecycle-context.d.ts +228 -0
  53. package/dist/environment/application/lifecycle-context.js +154 -0
  54. package/dist/environment/application/lifecycle-state.d.ts +25 -0
  55. package/dist/environment/application/lifecycle-state.js +201 -0
  56. package/dist/environment/application/lifecycle.d.ts +17 -0
  57. package/dist/environment/application/lifecycle.js +729 -0
  58. package/dist/environment/application/logs.d.ts +45 -0
  59. package/dist/environment/application/logs.js +127 -0
  60. package/dist/environment/application/setup.d.ts +129 -0
  61. package/dist/environment/application/setup.js +741 -0
  62. package/dist/environment/application/shared.d.ts +11 -0
  63. package/dist/environment/application/shared.js +61 -0
  64. package/dist/environment/claims.d.ts +389 -0
  65. package/dist/environment/claims.js +1709 -0
  66. package/dist/environment/codex-hook-trust.d.ts +11 -0
  67. package/dist/environment/codex-hook-trust.js +185 -0
  68. package/dist/environment/compose-environment.d.ts +1 -0
  69. package/dist/environment/compose-environment.js +6 -0
  70. package/dist/environment/compose-ownership.d.ts +21 -0
  71. package/dist/environment/compose-ownership.js +117 -0
  72. package/dist/environment/compose-topology.d.ts +117 -0
  73. package/dist/environment/compose-topology.js +413 -0
  74. package/dist/environment/contract.d.ts +489 -0
  75. package/dist/environment/contract.js +761 -0
  76. package/dist/environment/declaration.d.ts +14 -0
  77. package/dist/environment/declaration.js +39 -0
  78. package/dist/environment/destructive-composition.d.ts +29 -0
  79. package/dist/environment/destructive-composition.js +333 -0
  80. package/dist/environment/docker-daemon.d.ts +30 -0
  81. package/dist/environment/docker-daemon.js +78 -0
  82. package/dist/environment/docker-endpoint.d.ts +10 -0
  83. package/dist/environment/docker-endpoint.js +19 -0
  84. package/dist/environment/env-schema-observation.d.ts +46 -0
  85. package/dist/environment/env-schema-observation.js +241 -0
  86. package/dist/environment/evidence.d.ts +136 -0
  87. package/dist/environment/evidence.js +437 -0
  88. package/dist/environment/executable.d.ts +26 -0
  89. package/dist/environment/executable.js +167 -0
  90. package/dist/environment/generated-application/authorities.d.ts +5 -0
  91. package/dist/environment/generated-application/authorities.js +61 -0
  92. package/dist/environment/generated-application/bootstrap-skill.d.ts +16 -0
  93. package/dist/environment/generated-application/bootstrap-skill.js +328 -0
  94. package/dist/environment/generated-application/check.d.ts +13 -0
  95. package/dist/environment/generated-application/check.js +189 -0
  96. package/dist/environment/generated-application/custom-surface.d.ts +26 -0
  97. package/dist/environment/generated-application/custom-surface.js +289 -0
  98. package/dist/environment/generated-application/prepare.d.ts +29 -0
  99. package/dist/environment/generated-application/prepare.js +276 -0
  100. package/dist/environment/generated-application/publication.d.ts +33 -0
  101. package/dist/environment/generated-application/publication.js +547 -0
  102. package/dist/environment/generated-application/shared.d.ts +484 -0
  103. package/dist/environment/generated-application/shared.js +349 -0
  104. package/dist/environment/generated-application/surface-observation.d.ts +4 -0
  105. package/dist/environment/generated-application/surface-observation.js +69 -0
  106. package/dist/environment/generated-application/surface-outputs.d.ts +71 -0
  107. package/dist/environment/generated-application/surface-outputs.js +389 -0
  108. package/dist/environment/generated-application/surfaces.d.ts +3 -0
  109. package/dist/environment/generated-application/surfaces.js +699 -0
  110. package/dist/environment/generated-application/sync.d.ts +6 -0
  111. package/dist/environment/generated-application/sync.js +109 -0
  112. package/dist/environment/git-observation.d.ts +183 -0
  113. package/dist/environment/git-observation.js +544 -0
  114. package/dist/environment/health-composition.d.ts +11 -0
  115. package/dist/environment/health-composition.js +628 -0
  116. package/dist/environment/host-observation.d.ts +56 -0
  117. package/dist/environment/host-observation.js +52 -0
  118. package/dist/environment/identity.d.ts +48 -0
  119. package/dist/environment/identity.js +85 -0
  120. package/dist/environment/init-application.d.ts +308 -0
  121. package/dist/environment/init-application.js +697 -0
  122. package/dist/environment/install-policy-observation.d.ts +71 -0
  123. package/dist/environment/install-policy-observation.js +284 -0
  124. package/dist/environment/install-policy.d.ts +48 -0
  125. package/dist/environment/install-policy.js +96 -0
  126. package/dist/environment/instance.d.ts +33 -0
  127. package/dist/environment/instance.js +122 -0
  128. package/dist/environment/instruction-pointer.d.ts +19 -0
  129. package/dist/environment/instruction-pointer.js +144 -0
  130. package/dist/environment/lifecycle-composition.d.ts +29 -0
  131. package/dist/environment/lifecycle-composition.js +675 -0
  132. package/dist/environment/lifecycle-stage.d.ts +115 -0
  133. package/dist/environment/lifecycle-stage.js +398 -0
  134. package/dist/environment/lifecycle.d.ts +75 -0
  135. package/dist/environment/lifecycle.js +354 -0
  136. package/dist/environment/logs.d.ts +132 -0
  137. package/dist/environment/logs.js +519 -0
  138. package/dist/environment/observations.d.ts +46 -0
  139. package/dist/environment/observations.js +344 -0
  140. package/dist/environment/operator-application.d.ts +50 -0
  141. package/dist/environment/operator-application.js +155 -0
  142. package/dist/environment/ports.d.ts +182 -0
  143. package/dist/environment/ports.js +1205 -0
  144. package/dist/environment/posture-observation.d.ts +68 -0
  145. package/dist/environment/posture-observation.js +253 -0
  146. package/dist/environment/read-composition.d.ts +27 -0
  147. package/dist/environment/read-composition.js +352 -0
  148. package/dist/environment/readiness-composition.d.ts +38 -0
  149. package/dist/environment/readiness-composition.js +130 -0
  150. package/dist/environment/reclaim.d.ts +333 -0
  151. package/dist/environment/reclaim.js +1835 -0
  152. package/dist/environment/repository-identity.d.ts +13 -0
  153. package/dist/environment/repository-identity.js +108 -0
  154. package/dist/environment/runtime-measurement-record.d.ts +38 -0
  155. package/dist/environment/runtime-measurement-record.js +111 -0
  156. package/dist/environment/runtime-measurement.d.ts +27 -0
  157. package/dist/environment/runtime-measurement.js +224 -0
  158. package/dist/environment/runtime-probe-residue.d.ts +43 -0
  159. package/dist/environment/runtime-probe-residue.js +148 -0
  160. package/dist/environment/runtime-probe.d.ts +72 -0
  161. package/dist/environment/runtime-probe.js +725 -0
  162. package/dist/environment/runtime.d.ts +505 -0
  163. package/dist/environment/runtime.js +2335 -0
  164. package/dist/environment/seed.d.ts +16 -0
  165. package/dist/environment/seed.js +43 -0
  166. package/dist/environment/selftest-application.d.ts +86 -0
  167. package/dist/environment/selftest-application.js +713 -0
  168. package/dist/environment/sensitive.d.ts +3 -0
  169. package/dist/environment/sensitive.js +48 -0
  170. package/dist/environment/setup-observation.d.ts +62 -0
  171. package/dist/environment/setup-observation.js +1473 -0
  172. package/dist/environment/setup.d.ts +43 -0
  173. package/dist/environment/setup.js +176 -0
  174. package/dist/environment/sibling-instructions.d.ts +33 -0
  175. package/dist/environment/sibling-instructions.js +224 -0
  176. package/dist/environment/sibling-registration.d.ts +45 -0
  177. package/dist/environment/sibling-registration.js +538 -0
  178. package/dist/environment/state.d.ts +371 -0
  179. package/dist/environment/state.js +2013 -0
  180. package/dist/environment/templates.d.ts +140 -0
  181. package/dist/environment/templates.js +685 -0
  182. package/dist/environment/tool-registration.d.ts +9 -0
  183. package/dist/environment/tool-registration.js +96 -0
  184. package/dist/environment/upgrade-application.d.ts +19 -0
  185. package/dist/environment/upgrade-application.js +175 -0
  186. package/dist/environment-exec/execute.d.ts +102 -0
  187. package/dist/environment-exec/execute.js +1175 -0
  188. package/dist/environment-exec/libexec-program.d.ts +6 -0
  189. package/dist/environment-exec/libexec-program.js +22 -0
  190. package/dist/environment-exec/verified-digest.d.ts +4 -0
  191. package/dist/environment-exec/verified-digest.js +16 -0
  192. package/dist/exact-keys.d.ts +7 -0
  193. package/dist/exact-keys.js +15 -0
  194. package/dist/fix-catalogue.d.ts +32 -0
  195. package/dist/fix-catalogue.js +60 -0
  196. package/dist/freshness.d.ts +29 -0
  197. package/dist/freshness.js +75 -0
  198. package/dist/generated-environment/adopt.d.ts +16 -0
  199. package/dist/generated-environment/adopt.js +275 -0
  200. package/dist/generated-environment/command.d.ts +47 -0
  201. package/dist/generated-environment/command.js +75 -0
  202. package/dist/generated-environment/transaction.d.ts +186 -0
  203. package/dist/generated-environment/transaction.js +1770 -0
  204. package/dist/grading/evaluate.d.ts +71 -0
  205. package/dist/grading/evaluate.js +403 -0
  206. package/dist/grading/grade.d.ts +33 -0
  207. package/dist/grading/grade.js +57 -0
  208. package/dist/guards/evaluator.d.ts +113 -0
  209. package/dist/guards/evaluator.js +1226 -0
  210. package/dist/guards/harness-adapter.d.ts +17 -0
  211. package/dist/guards/harness-adapter.js +57 -0
  212. package/dist/guards/host.d.ts +8 -0
  213. package/dist/guards/host.js +10 -0
  214. package/dist/guards/invocation-parser.d.ts +66 -0
  215. package/dist/guards/invocation-parser.js +595 -0
  216. package/dist/guards/local-identity.d.ts +40 -0
  217. package/dist/guards/local-identity.js +123 -0
  218. package/dist/guards/policy-pin.d.ts +13 -0
  219. package/dist/guards/policy-pin.js +175 -0
  220. package/dist/guards/protected-locations.d.ts +62 -0
  221. package/dist/guards/protected-locations.js +461 -0
  222. package/dist/guards/read-only.d.ts +37 -0
  223. package/dist/guards/read-only.js +315 -0
  224. package/dist/init/command.d.ts +40 -0
  225. package/dist/init/command.js +310 -0
  226. package/dist/init/derive.d.ts +51 -0
  227. package/dist/init/derive.js +715 -0
  228. package/dist/init/draft.d.ts +138 -0
  229. package/dist/init/draft.js +1205 -0
  230. package/dist/init/render.d.ts +35 -0
  231. package/dist/init/render.js +82 -0
  232. package/dist/init/scaffold.d.ts +53 -0
  233. package/dist/init/scaffold.js +121 -0
  234. package/dist/invocation.d.ts +13 -0
  235. package/dist/invocation.js +20 -0
  236. package/dist/package-metadata.d.ts +9 -0
  237. package/dist/package-metadata.js +19 -0
  238. package/dist/readiness.d.ts +53 -0
  239. package/dist/readiness.js +88 -0
  240. package/dist/references/named.d.ts +24 -0
  241. package/dist/references/named.js +27 -0
  242. package/dist/refusal.d.ts +15 -0
  243. package/dist/refusal.js +7 -0
  244. package/dist/registry-derivation.d.ts +31 -0
  245. package/dist/registry-derivation.js +32 -0
  246. package/dist/registry-resolve/command.d.ts +47 -0
  247. package/dist/registry-resolve/command.js +226 -0
  248. package/dist/registry-resolve/registry.d.ts +68 -0
  249. package/dist/registry-resolve/registry.js +292 -0
  250. package/dist/registry-resolve/render-shell.d.ts +18 -0
  251. package/dist/registry-resolve/render-shell.js +36 -0
  252. package/dist/repair.d.ts +29 -0
  253. package/dist/repair.js +8 -0
  254. package/dist/schema-json.d.ts +1 -0
  255. package/dist/schema-json.js +177 -0
  256. package/dist/schema.d.ts +1322 -0
  257. package/dist/schema.js +934 -0
  258. package/dist/setup/command.d.ts +4 -0
  259. package/dist/setup/command.js +4 -0
  260. package/dist/sha256.d.ts +1 -0
  261. package/dist/sha256.js +4 -0
  262. package/dist/surfaces/adoption.d.ts +41 -0
  263. package/dist/surfaces/adoption.js +217 -0
  264. package/dist/surfaces/consumer-instructions.d.ts +20 -0
  265. package/dist/surfaces/consumer-instructions.js +43 -0
  266. package/dist/surfaces/contract.d.ts +42 -0
  267. package/dist/surfaces/contract.js +169 -0
  268. package/dist/surfaces/core-declarations.d.ts +24 -0
  269. package/dist/surfaces/core-declarations.js +79 -0
  270. package/dist/surfaces/development-markers.d.ts +1 -0
  271. package/dist/surfaces/development-markers.js +18 -0
  272. package/dist/surfaces/generator-adapter.d.ts +71 -0
  273. package/dist/surfaces/generator-adapter.js +809 -0
  274. package/dist/surfaces/harness-registry.d.ts +180 -0
  275. package/dist/surfaces/harness-registry.js +117 -0
  276. package/dist/surfaces/mcp.d.ts +95 -0
  277. package/dist/surfaces/mcp.js +186 -0
  278. package/dist/surfaces/merge.d.ts +30 -0
  279. package/dist/surfaces/merge.js +105 -0
  280. package/dist/surfaces/ownership.d.ts +190 -0
  281. package/dist/surfaces/ownership.js +795 -0
  282. package/dist/surfaces/projection.d.ts +136 -0
  283. package/dist/surfaces/projection.js +681 -0
  284. package/dist/surfaces/render.d.ts +118 -0
  285. package/dist/surfaces/render.js +633 -0
  286. package/dist/surfaces/restrictions.d.ts +130 -0
  287. package/dist/surfaces/restrictions.js +447 -0
  288. package/dist/surfaces/selftest.d.ts +162 -0
  289. package/dist/surfaces/selftest.js +393 -0
  290. package/dist/surfaces/skill-contract.d.ts +85 -0
  291. package/dist/surfaces/skill-contract.js +449 -0
  292. package/dist/surfaces/sync.d.ts +57 -0
  293. package/dist/surfaces/sync.js +164 -0
  294. package/dist/team-metadata.d.ts +41 -0
  295. package/dist/team-metadata.js +512 -0
  296. package/dist/testing/gates.d.ts +91 -0
  297. package/dist/testing/gates.js +182 -0
  298. package/dist/testing/hooks.d.ts +59 -0
  299. package/dist/testing/hooks.js +139 -0
  300. package/dist/testing/verdict.d.ts +29 -0
  301. package/dist/testing/verdict.js +62 -0
  302. package/dist/work-orders/application.d.ts +99 -0
  303. package/dist/work-orders/application.js +383 -0
  304. package/dist/work-orders/composition.d.ts +124 -0
  305. package/dist/work-orders/composition.js +1010 -0
  306. package/dist/work-orders/contract.d.ts +61 -0
  307. package/dist/work-orders/contract.js +122 -0
  308. package/dist/work-orders/store.d.ts +158 -0
  309. package/dist/work-orders/store.js +197 -0
  310. package/docs/README.md +65 -0
  311. package/docs/architecture.md +136 -0
  312. package/docs/capability-matrix.md +50 -0
  313. package/docs/cli-reference.md +599 -0
  314. package/docs/command-list.md +51 -0
  315. package/docs/compose-patterns.md +99 -0
  316. package/docs/concepts.md +171 -0
  317. package/docs/configuration-essentials.md +172 -0
  318. package/docs/configuration-reference.md +621 -0
  319. package/docs/environment.md +760 -0
  320. package/docs/extending.md +43 -0
  321. package/docs/faq.md +111 -0
  322. package/docs/getting-started.md +608 -0
  323. package/docs/harness-surfaces.md +433 -0
  324. package/docs/joining-a-team.md +76 -0
  325. package/docs/release-policy.md +49 -0
  326. package/docs/safety-and-autonomy.md +419 -0
  327. package/docs/troubleshooting.md +361 -0
  328. package/docs/tutorial.md +170 -0
  329. package/docs/work-and-browser.md +254 -0
  330. package/libexec/browser-operation.cjs +186 -0
  331. package/libexec/fence-marker.cjs +101 -0
  332. package/libexec/fence-marker.d.cts +24 -0
  333. package/libexec/fenced-group-runner.cjs +77 -0
  334. package/libexec/fenced-supervisor.cjs +137 -0
  335. package/libexec/generated-filesystem.cjs +731 -0
  336. package/libexec/install-filesystem.cjs +225 -0
  337. package/libexec/rulesync-generator.NOTICE.md +1112 -0
  338. package/libexec/rulesync-generator.cjs +743 -0
  339. package/libexec/surface-generator-worker.cjs +266 -0
  340. package/libexec/surface-output-reader.cjs +99 -0
  341. package/libexec/surface-scratch-cleanup.cjs +14 -0
  342. package/libexec/validate-harness-evidence.d.mts +183 -0
  343. package/libexec/validate-harness-evidence.mjs +1312 -0
  344. package/package.json +76 -1
  345. package/surfaces/README.md +33 -0
  346. package/surfaces/agents/evaluator.agent.md +76 -0
  347. package/surfaces/guards/bash-guard.sh +48 -0
  348. package/surfaces/guards/claude.adapter.json +25 -0
  349. package/surfaces/guards/codex.adapter.json +24 -0
  350. package/surfaces/guards/opencode.adapter.json +25 -0
  351. package/surfaces/references/browser.md +63 -0
  352. package/surfaces/references/test-data-and-personas.md +84 -0
  353. package/surfaces/references/testing-tiers.md +11 -0
  354. package/surfaces/skills/cockpit-browser.skill.md +54 -0
  355. package/surfaces/skills/cockpit-explain.skill.md +32 -0
  356. package/surfaces/skills/cockpit-fix.skill.md +37 -0
  357. package/surfaces/skills/cockpit-setup.skill.md +39 -0
  358. package/surfaces/skills/cockpit-work.skill.md +34 -0
  359. package/surfaces/skills/collection.json +5 -0
  360. package/surfaces/skills/procedures/explain.md +61 -0
  361. package/surfaces/skills/procedures/fix.md +81 -0
  362. package/surfaces/skills/procedures/setup.md +379 -0
  363. package/surfaces/skills/procedures/work.md +105 -0
@@ -0,0 +1,731 @@
1
+ 'use strict'
2
+
3
+ // Anchored filesystem operations for packages/cockpit/src/generated-environment/transaction.ts:
4
+ // authenticated reads, adoption relocation, and the staged publication transaction (prepare,
5
+ // validate, publish, verify, commit, rollback). Runs from a staged snapshot, so it must stay
6
+ // self-contained.
7
+ //
8
+ // It is a separate process because Node has no openat(): the only way to pin a directory while
9
+ // operating inside it is to make it the working directory. Every walk moves the cwd one segment at
10
+ // a time and re-checks the entered directory's dev/ino, so a directory swapped for a symlink or
11
+ // another inode mid-walk is refused instead of followed, and every later operation is relative to
12
+ // the pinned directory. Changing the cwd of the cockpit process itself would redirect every
13
+ // concurrent relative path in it, and worker threads cannot change it at all.
14
+ //
15
+ // argv: <request JSON> [staged input paths...]. The request carries `version`, `nonce`, `action`,
16
+ // the expected `identity` of the starting cwd, the bounds in `limits`, and the action's fields.
17
+ // stdout: one JSON line { version: 1, nonce, action, kind: 'complete' | 'missing' | ..., ... }
18
+ // or { ..., kind: 'refusal', reason }. Any operation that cannot be proved exact is a refusal.
19
+
20
+ const fs = require('node:fs')
21
+ const crypto = require('node:crypto')
22
+ const path = require('node:path')
23
+
24
+ const c = fs.constants
25
+ const request = JSON.parse(process.argv[2])
26
+ const INPUT_ARGUMENT_OFFSET = 3
27
+ let limits
28
+
29
+ const parseLimits = value => {
30
+ const keys = ['fileBytes', 'frameBytes', 'readBatchEntries', 'readBatchBytes', 'readBatchReceiptBytes', 'publicationGroups']
31
+ if (!value || typeof value !== 'object' || Object.keys(value).length !== keys.length
32
+ || !keys.every(key => Number.isSafeInteger(value[key]) && value[key] > 0)) throw new Error('limits')
33
+ return value
34
+ }
35
+
36
+ const anchoredName = value => {
37
+ if (typeof value !== 'string' || value.length === 0 || value === '.' || value === '..' || value.includes('/')
38
+ || value.includes('\\') || value.includes('\0')) throw new Error('name')
39
+ return value
40
+ }
41
+ const name = value => {
42
+ if (typeof value !== 'string' || value.length === 0 || value.length > 255 || Buffer.byteLength(value, 'utf8') > 255
43
+ || /[<>:"/\\|?*\x00-\x1f\x7f]/.test(value) || /[. ]$/.test(value)
44
+ || /^(?:con|prn|aux|nul|com[1-9¹²³]|lpt[1-9¹²³])(?:\.|$)/i.test(value)) throw new Error('nonportable-name')
45
+ return value
46
+ }
47
+ const safeSegments = (value, validate = name) => {
48
+ if (!Array.isArray(value)) throw new Error('segments')
49
+ return value.map(validate)
50
+ }
51
+
52
+ const same = (actual, expected) => actual.isDirectory() && !actual.isSymbolicLink() && actual.dev === expected.dev && actual.ino === expected.ino
53
+ const identity = () => {
54
+ const value = fs.lstatSync('.')
55
+ if (!same(value, request.identity)) throw new Error('identity')
56
+ return value
57
+ }
58
+ const currentDirectory = () => {
59
+ const value = fs.lstatSync('.')
60
+ if (!value.isDirectory() || value.isSymbolicLink()) throw new Error('directory')
61
+ return value
62
+ }
63
+ const syncDirectory = () => {
64
+ const fd = fs.openSync('.', c.O_RDONLY)
65
+ try { fs.fsyncSync(fd) } finally { fs.closeSync(fd) }
66
+ }
67
+ const digest = value => crypto.createHash('sha256').update(value).digest('hex')
68
+ const readLimit = value => {
69
+ if (!Number.isSafeInteger(value) || value < 1 || value > limits.fileBytes) throw new Error('limit')
70
+ return value
71
+ }
72
+ const writeExclusive = (target, bytes) => {
73
+ const fd = fs.openSync(target, c.O_WRONLY | c.O_CREAT | c.O_EXCL | c.O_NOFOLLOW, 0o600)
74
+ try { fs.writeFileSync(fd, bytes) } finally { fs.closeSync(fd) }
75
+ }
76
+
77
+ const read = (target, maxBytes = limits.fileBytes, expected) => {
78
+ maxBytes = readLimit(maxBytes)
79
+ if (c.O_NOFOLLOW === undefined) throw new Error('no-follow')
80
+ const fd = fs.openSync(target, c.O_RDONLY | c.O_NOFOLLOW | c.O_NONBLOCK)
81
+ try {
82
+ const status = fs.fstatSync(fd)
83
+ if (!status.isFile() || status.size > maxBytes || expected && (status.dev !== expected.dev || status.ino !== expected.ino)) throw new Error('file')
84
+ const chunks = []
85
+ let total = 0
86
+ const chunk = Buffer.allocUnsafe(65536)
87
+ while (true) {
88
+ const length = fs.readSync(fd, chunk, 0, Math.min(chunk.length, maxBytes - total + 1), null)
89
+ if (length === 0) break
90
+ total += length
91
+ if (total > maxBytes) throw new Error('limit')
92
+ chunks.push(Buffer.from(chunk.subarray(0, length)))
93
+ }
94
+ const after = fs.fstatSync(fd)
95
+ if (total !== status.size || after.dev !== status.dev || after.ino !== status.ino || after.size !== status.size
96
+ || after.mtimeMs !== status.mtimeMs || after.ctimeMs !== status.ctimeMs) throw new Error('changed')
97
+ return { bytes: Buffer.concat(chunks, total), status }
98
+ } finally { fs.closeSync(fd) }
99
+ }
100
+ // Published files are committed, so they get the ordinary mode (after umask) a new file gets; 0600
101
+ // made every generated file unreadable to anything but its owner, containers included.
102
+ const copy = (source, target, expected) => {
103
+ const value = read(source)
104
+ if (digest(value.bytes) !== expected) throw new Error('digest')
105
+ const fd = fs.openSync(name(target), c.O_WRONLY | c.O_CREAT | c.O_EXCL | (c.O_NOFOLLOW || 0), 0o644)
106
+ try {
107
+ fs.writeFileSync(fd, value.bytes)
108
+ fs.fsyncSync(fd)
109
+ } finally { fs.closeSync(fd) }
110
+ }
111
+ const hash = (target, validate = name) => digest(read(validate(target)).bytes)
112
+
113
+ // Walks that change the cwd. Each pushes { name, dev, ino, created } rows describing the chain it
114
+ // entered, so a later operation can walk the same chain and prove it reached the same inodes.
115
+ const walk = (segments, chain = [], validate = name) => {
116
+ for (const raw of segments) {
117
+ const segment = validate(raw)
118
+ let created = false
119
+ let before
120
+ try { before = fs.lstatSync(segment) } catch (error) {
121
+ if (error.code !== 'ENOENT') throw error
122
+ fs.mkdirSync(segment, { mode: 0o700 })
123
+ syncDirectory()
124
+ created = true
125
+ before = fs.lstatSync(segment)
126
+ }
127
+ if (!before.isDirectory() || before.isSymbolicLink()) throw new Error('directory')
128
+ process.chdir(segment)
129
+ const after = fs.lstatSync('.')
130
+ if (after.dev !== before.dev || after.ino !== before.ino || !after.isDirectory() || after.isSymbolicLink()) throw new Error('walk')
131
+ chain.push({ name: segment, dev: after.dev, ino: after.ino, created })
132
+ }
133
+ return chain
134
+ }
135
+ // Creates each missing segment, or enters one this batch already created (keyed by its path from
136
+ // the batch root in `created`). Any other existing directory is refused: it appeared after the
137
+ // caller observed the path as missing.
138
+ const walkCreating = (segments, chain, created, prefix) => {
139
+ for (const raw of segments) {
140
+ const segment = name(raw)
141
+ const key = [...prefix, ...chain.map(entry => entry.name), segment].join('/')
142
+ let before
143
+ try { before = fs.lstatSync(segment) } catch (error) {
144
+ if (error.code !== 'ENOENT') throw error
145
+ }
146
+ if (before) {
147
+ const issued = created.get(key)
148
+ if (!issued || !same(before, issued)) throw new Error('unexpected-directory')
149
+ chain.push({ name: segment, dev: before.dev, ino: before.ino, created: false })
150
+ } else {
151
+ fs.mkdirSync(segment, { mode: 0o700 })
152
+ before = fs.lstatSync(segment)
153
+ if (!before.isDirectory() || before.isSymbolicLink()) throw new Error('created-directory')
154
+ created.set(key, { dev: before.dev, ino: before.ino })
155
+ chain.push({ name: segment, dev: before.dev, ino: before.ino, created: true })
156
+ syncDirectory()
157
+ }
158
+ process.chdir(segment)
159
+ if (!same(fs.lstatSync('.'), before)) throw new Error('created-walk')
160
+ }
161
+ return chain
162
+ }
163
+ const walkSource = (segments, chain = []) => {
164
+ for (const raw of segments) {
165
+ const segment = anchoredName(raw)
166
+ const before = fs.lstatSync(segment)
167
+ if (!before.isDirectory() || before.isSymbolicLink()) throw new Error('directory')
168
+ process.chdir(segment)
169
+ const after = fs.lstatSync('.')
170
+ if (!same(after, before)) throw new Error('walk')
171
+ chain.push({ name: segment, dev: after.dev, ino: after.ino })
172
+ }
173
+ return chain
174
+ }
175
+ const walkExpected = (chain, validate = name) => {
176
+ const walked = []
177
+ for (const expected of chain) {
178
+ const segment = validate(expected.name)
179
+ const before = fs.lstatSync(segment)
180
+ if (!same(before, expected)) throw new Error('directory-identity')
181
+ process.chdir(segment)
182
+ const after = fs.lstatSync('.')
183
+ if (!same(after, expected)) throw new Error('walk-identity')
184
+ walked.push({ name: segment, dev: after.dev, ino: after.ino, created: false })
185
+ }
186
+ return walked
187
+ }
188
+ // Walks back up a chain to `start`, proving each parent and child are still the inodes walked down.
189
+ const verifySourceChain = (chain, start = request.identity) => {
190
+ for (let index = chain.length - 1; index >= 0; index--) {
191
+ process.chdir('..')
192
+ const parent = fs.lstatSync('.')
193
+ const expected = index === 0 ? start : chain[index - 1]
194
+ if (!same(parent, expected)) throw new Error('parent-identity')
195
+ const child = fs.lstatSync(anchoredName(chain[index].name))
196
+ if (!same(child, chain[index])) throw new Error('child-identity')
197
+ }
198
+ if (!same(fs.lstatSync('.'), start)) throw new Error('start-identity')
199
+ }
200
+ const removeCreatedChain = (chain, start, validate = name) => {
201
+ for (let index = chain.length - 1; index >= 0; index--) {
202
+ process.chdir('..')
203
+ const parent = fs.lstatSync('.')
204
+ const expected = index === 0 ? start : chain[index - 1]
205
+ if (!same(parent, expected)) throw new Error('parent-identity')
206
+ const child = fs.lstatSync(validate(chain[index].name))
207
+ if (!same(child, chain[index])) throw new Error('child-identity')
208
+ if (chain[index].created) {
209
+ try {
210
+ fs.rmdirSync(validate(chain[index].name))
211
+ syncDirectory()
212
+ } catch (error) { if (error.code !== 'ENOTEMPTY') throw error }
213
+ }
214
+ }
215
+ if (!same(fs.lstatSync('.'), start)) throw new Error('start-identity')
216
+ }
217
+ const removeCreatedDirectories = (entries, start) => {
218
+ const base = process.cwd()
219
+ const created = new Map()
220
+ for (const entry of entries) {
221
+ const names = []
222
+ for (const [index, directory] of entry.chain.entries()) {
223
+ names.push(directory.name)
224
+ if (directory.created) created.set(names.join('/'), entry.chain.slice(0, index + 1))
225
+ }
226
+ }
227
+ const chains = [...created.values()].sort((left, right) => right.length - left.length)
228
+ for (const chain of chains) {
229
+ process.chdir(base)
230
+ const parents = walkExpected(chain.slice(0, -1))
231
+ const child = chain[chain.length - 1]
232
+ if (!same(fs.lstatSync(name(child.name)), child)) throw new Error('created-identity')
233
+ try {
234
+ fs.rmdirSync(name(child.name))
235
+ syncDirectory()
236
+ } catch (error) { if (error.code !== 'ENOTEMPTY') throw error }
237
+ verifySourceChain(parents, start)
238
+ }
239
+ process.chdir(base)
240
+ if (!same(fs.lstatSync('.'), start)) throw new Error('start-identity')
241
+ }
242
+
243
+ const state = target => {
244
+ try {
245
+ const status = fs.lstatSync(target)
246
+ if (status.isSymbolicLink()) return { kind: 'symlink', target: fs.readlinkSync(target), identity: { dev: status.dev, ino: status.ino } }
247
+ if (status.isFile()) return { kind: 'file', sha256: hash(target), mode: status.mode & 511, identity: { dev: status.dev, ino: status.ino } }
248
+ return { kind: 'other' }
249
+ } catch (error) {
250
+ if (error.code === 'ENOENT') return { kind: 'missing' }
251
+ throw error
252
+ }
253
+ }
254
+ const matches = (actual, expected) => actual.kind === expected.kind
255
+ && (actual.kind !== 'file' || actual.sha256 === expected.sha256)
256
+ && (actual.kind !== 'file' || expected.mode === undefined || actual.mode === expected.mode)
257
+ && (actual.kind !== 'symlink' || actual.target === expected.target)
258
+ && (actual.kind === 'missing' || !expected.identity || actual.identity.dev === expected.identity.dev && actual.identity.ino === expected.identity.ino)
259
+ const priorState = entry => ({ kind: entry.priorKind, sha256: entry.priorSha256, target: entry.priorTarget, mode: entry.priorMode, identity: entry.priorIdentity })
260
+ const postState = entry => entry.operation === 'write-file' ? { kind: 'file', sha256: entry.sha256 }
261
+ : entry.operation === 'write-symlink' ? { kind: 'symlink', target: entry.target } : { kind: 'missing' }
262
+
263
+ const publishEntries = (entries, start = currentDirectory()) => {
264
+ for (const entry of entries) {
265
+ const chain = walkExpected(entry.chain)
266
+ name(entry.name)
267
+ if (!matches(state(entry.name), priorState(entry))) throw new Error('prior-state')
268
+ if (entry.backup !== null && hash(entry.backup) !== entry.priorSha256) throw new Error('backup-digest')
269
+ if (entry.operation === 'write-file') {
270
+ name(entry.stage)
271
+ if (hash(entry.stage) !== entry.sha256) throw new Error('stage-digest')
272
+ fs.renameSync(entry.stage, entry.name)
273
+ } else if (entry.operation === 'write-symlink') {
274
+ name(entry.stage)
275
+ if (fs.readlinkSync(entry.stage) !== entry.target) throw new Error('stage-target')
276
+ fs.renameSync(entry.stage, entry.name)
277
+ } else fs.unlinkSync(entry.name)
278
+ syncDirectory()
279
+ verifySourceChain(chain, start)
280
+ }
281
+ }
282
+ const validateEntries = (entries, start = currentDirectory()) => {
283
+ for (const entry of entries) {
284
+ const chain = walkExpected(entry.chain)
285
+ name(entry.name)
286
+ if (!matches(state(entry.name), priorState(entry))) throw new Error('prior-state')
287
+ if (entry.backup !== null && hash(entry.backup) !== entry.priorSha256) throw new Error('backup-digest')
288
+ if (entry.operation === 'write-file' && hash(name(entry.stage)) !== entry.sha256) throw new Error('stage-digest')
289
+ if (entry.operation === 'write-symlink' && fs.readlinkSync(name(entry.stage)) !== entry.target) throw new Error('stage-target')
290
+ verifySourceChain(chain, start)
291
+ }
292
+ }
293
+ const verifyEntries = (entries, start = currentDirectory()) => {
294
+ for (const entry of entries) {
295
+ const chain = walkExpected(entry.chain)
296
+ name(entry.name)
297
+ if (!matches(state(entry.name), postState(entry))) throw new Error('target-state')
298
+ if (entry.backup !== null && hash(entry.backup) !== entry.priorSha256) throw new Error('backup-digest')
299
+ verifySourceChain(chain, start)
300
+ }
301
+ }
302
+
303
+ const respond = value => process.stdout.write(JSON.stringify({ version: 1, nonce: request.nonce, action: request.action, ...value }) + '\n')
304
+ const refuse = value => {
305
+ respond({ kind: 'refusal', ...value })
306
+ process.exit(0)
307
+ }
308
+
309
+ const readSource = entry => {
310
+ const chain = []
311
+ let before
312
+ const filename = anchoredName(entry.name)
313
+ try {
314
+ walkSource(entry.segments, chain)
315
+ before = fs.lstatSync(filename)
316
+ } catch (error) {
317
+ if (error.code === 'ENOENT') {
318
+ verifySourceChain(chain)
319
+ return { kind: 'missing' }
320
+ }
321
+ throw error
322
+ }
323
+ if (entry.action === 'read-entry' && before.isSymbolicLink()) {
324
+ const target = fs.readlinkSync(filename)
325
+ const current = fs.lstatSync(filename)
326
+ if (!current.isSymbolicLink() || current.dev !== before.dev || current.ino !== before.ino) throw new Error('source-changed')
327
+ verifySourceChain(chain)
328
+ return { kind: 'symlink', target, identity: { dev: before.dev, ino: before.ino } }
329
+ }
330
+ if (entry.action === 'read-entry' && !before.isFile()) {
331
+ verifySourceChain(chain)
332
+ return { kind: 'other' }
333
+ }
334
+ if (!before.isFile() || before.isSymbolicLink()) throw new Error('source')
335
+ const value = read(filename, entry.maxBytes)
336
+ if (value.status.dev !== before.dev || value.status.ino !== before.ino) throw new Error('source-identity')
337
+ const current = fs.lstatSync(filename)
338
+ if (!current.isFile() || current.isSymbolicLink() || current.dev !== before.dev || current.ino !== before.ino) throw new Error('source-changed')
339
+ verifySourceChain(chain)
340
+ writeExclusive(entry.output, value.bytes)
341
+ return {
342
+ kind: entry.action === 'read-entry' ? 'file' : 'complete',
343
+ identity: { root: request.identity, chain: chain.map(({ dev, ino }) => ({ dev, ino })), file: { dev: before.dev, ino: before.ino } },
344
+ ...(entry.action === 'read-entry' ? { mode: before.mode & 511 } : {}),
345
+ sha256: digest(value.bytes),
346
+ }
347
+ }
348
+
349
+ const readBatch = () => {
350
+ if (!Array.isArray(request.entries) || request.entries.length < 1 || request.entries.length > limits.readBatchEntries) throw new Error('batch')
351
+ const requestedBytes = request.entries.reduce((total, entry) => total + readLimit(entry.maxBytes), 0)
352
+ if (!Number.isSafeInteger(requestedBytes) || requestedBytes > limits.readBatchBytes) throw new Error('batch-limit')
353
+ const entries = []
354
+ for (const entry of request.entries) {
355
+ if (!entry || typeof entry !== 'object' || typeof entry.nonce !== 'string'
356
+ || !['read-file', 'read-entry'].includes(entry.action)) throw new Error('batch-entry')
357
+ try {
358
+ const result = readSource(entry)
359
+ if (Buffer.byteLength(JSON.stringify({ version: 1, nonce: entry.nonce, action: entry.action, ...result }) + '\n') > limits.frameBytes) throw new Error('entry-frame')
360
+ entries.push({ nonce: entry.nonce, action: entry.action, segments: entry.segments, name: entry.name, maxBytes: entry.maxBytes, ...result })
361
+ } catch {
362
+ // The parent accepts only this exact refusal frame, so the cause has no channel; it names the failed entry instead.
363
+ refuse({ failedNonce: entry.nonce })
364
+ }
365
+ }
366
+ const receipt = Buffer.from(JSON.stringify({ version: 1, nonce: request.nonce, action: request.action, kind: 'complete', entries }) + '\n')
367
+ if (receipt.length > limits.readBatchReceiptBytes) throw new Error('receipt-limit')
368
+ writeExclusive(request.receiptOutput, receipt)
369
+ respond({ kind: 'complete', receiptBytes: receipt.length, receiptSha256: digest(receipt) })
370
+ }
371
+
372
+ const relocate = () => {
373
+ const base = process.cwd()
374
+ const destinationChain = []
375
+ const sourceCapture = name('.cockpit-adopt-source-' + request.nonce)
376
+ const destinationCapture = name('.cockpit-adopt-destination-' + request.nonce)
377
+ const destinationPolicy = request.action === 'restore-file' ? anchoredName : name
378
+ let sourceCapturePath
379
+ let sourceMode
380
+ let sourceCaptured = false
381
+ let sourceRestored = false
382
+ let originalRetired = false
383
+ let destinationCreated = false
384
+ let destinationQuarantined = false
385
+ let destinationIdentity
386
+ const restoreSource = () => {
387
+ process.chdir(base)
388
+ const chain = walkExpected(request.source.chain, anchoredName)
389
+ const sourceName = anchoredName(request.source.name)
390
+ if (state(sourceName).kind !== 'missing') throw new Error('source-recovery-occupied')
391
+ fs.linkSync(sourceCapture, sourceName)
392
+ const restored = fs.lstatSync(sourceName)
393
+ const captured = fs.lstatSync(sourceCapture)
394
+ if (!restored.isFile() || restored.isSymbolicLink() || restored.dev !== captured.dev || restored.ino !== captured.ino) throw new Error('source-recovery-identity')
395
+ sourceRestored = true
396
+ syncDirectory()
397
+ verifySourceChain(chain)
398
+ }
399
+ const retireSourceCapture = (requireRestored, retired = () => {}) => {
400
+ process.chdir(base)
401
+ const chain = walkExpected(request.source.chain, anchoredName)
402
+ const sourceName = anchoredName(request.source.name)
403
+ const captured = fs.lstatSync(sourceCapture)
404
+ if (!captured.isFile() || captured.isSymbolicLink() || captured.dev !== request.source.file.dev || captured.ino !== request.source.file.ino) throw new Error('source-retirement-identity')
405
+ if (requireRestored) {
406
+ const restored = fs.lstatSync(sourceName)
407
+ if (!restored.isFile() || restored.isSymbolicLink() || restored.dev !== captured.dev || restored.ino !== captured.ino) throw new Error('source-retirement-public-identity')
408
+ }
409
+ fs.unlinkSync(sourceCapture)
410
+ sourceCaptured = false
411
+ retired()
412
+ syncDirectory()
413
+ verifySourceChain(chain)
414
+ }
415
+ const removeDestination = () => {
416
+ process.chdir(base)
417
+ const chain = walkExpected(destinationChain, destinationPolicy)
418
+ const destinationName = destinationPolicy(request.destination.name)
419
+ if (state(destinationCapture).kind !== 'missing') throw new Error('destination-recovery-capture')
420
+ fs.renameSync(destinationName, destinationCapture)
421
+ destinationQuarantined = true
422
+ const captured = fs.lstatSync(destinationCapture)
423
+ if (!captured.isFile() || captured.isSymbolicLink() || captured.dev !== destinationIdentity.dev || captured.ino !== destinationIdentity.ino) {
424
+ fs.linkSync(destinationCapture, destinationName)
425
+ fs.unlinkSync(destinationCapture)
426
+ destinationQuarantined = false
427
+ syncDirectory()
428
+ throw new Error('destination-recovery-identity')
429
+ }
430
+ fs.unlinkSync(destinationCapture)
431
+ destinationQuarantined = false
432
+ destinationCreated = false
433
+ syncDirectory()
434
+ verifySourceChain(chain)
435
+ }
436
+ const regularWithIdentity = (status, expected) => status.isFile() && !status.isSymbolicLink() && status.dev === expected.dev && status.ino === expected.ino
437
+ try {
438
+ const sourceChain = walkExpected(request.source.chain, anchoredName)
439
+ const sourceName = anchoredName(request.source.name)
440
+ if (!regularWithIdentity(fs.lstatSync(sourceName), request.source.file)) throw new Error('source-identity')
441
+ if (state(sourceCapture).kind !== 'missing') throw new Error('source-capture')
442
+ sourceCapturePath = path.join(process.cwd(), sourceCapture)
443
+ fs.renameSync(sourceName, sourceCapture)
444
+ sourceCaptured = true
445
+ syncDirectory()
446
+ const captured = fs.lstatSync(sourceCapture)
447
+ if (!regularWithIdentity(captured, request.source.file)) throw new Error('source-captured-identity')
448
+ const source = read(sourceCapture, request.source.maxBytes, request.source.file)
449
+ if (source.status.dev !== captured.dev || source.status.ino !== captured.ino || digest(source.bytes) !== request.source.sha256) throw new Error('source-digest')
450
+ sourceMode = source.status.mode & 511
451
+ verifySourceChain(sourceChain)
452
+
453
+ process.chdir(base)
454
+ walk(safeSegments(request.destination.segments, destinationPolicy), destinationChain, destinationPolicy)
455
+ const destinationName = destinationPolicy(request.destination.name)
456
+ if (state(destinationName).kind !== 'missing') throw new Error('destination-state')
457
+ process.chdir(base)
458
+ const linkSourceChain = walkExpected(request.source.chain, anchoredName)
459
+ const currentCapture = read(sourceCapturePath, request.source.maxBytes, request.source.file)
460
+ if (currentCapture.status.dev !== request.source.file.dev || currentCapture.status.ino !== request.source.file.ino
461
+ || digest(currentCapture.bytes) !== request.source.sha256 || (currentCapture.status.mode & 511) !== sourceMode) throw new Error('source-capture-changed')
462
+ verifySourceChain(linkSourceChain)
463
+ process.chdir(base)
464
+ walkExpected(destinationChain, destinationPolicy)
465
+ fs.linkSync(sourceCapturePath, destinationName)
466
+ destinationCreated = true
467
+ destinationIdentity = { ...request.source.file }
468
+ const destination = fs.lstatSync(destinationName)
469
+ if (!regularWithIdentity(destination, destinationIdentity)) throw new Error('destination-linked-identity')
470
+ process.chdir(base)
471
+ const linkedSourceChain = walkExpected(request.source.chain, anchoredName)
472
+ if (!regularWithIdentity(fs.lstatSync(sourceCapture), destinationIdentity)) throw new Error('source-linked-identity')
473
+ verifySourceChain(linkedSourceChain)
474
+ process.chdir(base)
475
+ walkExpected(destinationChain, destinationPolicy)
476
+ if (hash(destinationName, destinationPolicy) !== request.source.sha256 || (destination.mode & 511) !== sourceMode) throw new Error('destination-postimage')
477
+ syncDirectory()
478
+ verifySourceChain(destinationChain)
479
+
480
+ process.chdir(base)
481
+ const currentSourceChain = walkExpected(request.source.chain, anchoredName)
482
+ if (state(sourceName).kind !== 'missing') throw new Error('source-postimage')
483
+ if (!regularWithIdentity(fs.lstatSync(sourceCapture), destinationIdentity)) throw new Error('source-capture-identity')
484
+ verifySourceChain(currentSourceChain)
485
+
486
+ process.chdir(base)
487
+ const currentDestinationChain = walkExpected(destinationChain, destinationPolicy)
488
+ const currentDestination = fs.lstatSync(destinationName)
489
+ if (!regularWithIdentity(currentDestination, destinationIdentity) || hash(destinationName, destinationPolicy) !== request.source.sha256
490
+ || (currentDestination.mode & 511) !== sourceMode) throw new Error('destination-changed')
491
+ verifySourceChain(currentDestinationChain)
492
+ process.chdir(base)
493
+ identity()
494
+ retireSourceCapture(false, () => { originalRetired = true })
495
+ } catch (error) {
496
+ const describe = value => value instanceof Error ? value.message : String(value)
497
+ const failure = describe(error)
498
+ const recoveryFailures = []
499
+ if (sourceCaptured) {
500
+ try { restoreSource() } catch (recoveryError) { recoveryFailures.push('source:' + describe(recoveryError)) }
501
+ }
502
+ if (destinationCreated && !originalRetired) {
503
+ try { removeDestination() } catch (recoveryError) { recoveryFailures.push('destination:' + describe(recoveryError)) }
504
+ }
505
+ if (sourceCaptured && sourceRestored && !destinationCreated) {
506
+ try { retireSourceCapture(true) } catch (recoveryError) { recoveryFailures.push('source-retirement:' + describe(recoveryError)) }
507
+ }
508
+ if (originalRetired) recoveryFailures.push('source:original-retired')
509
+ if (destinationChain.length > 0) {
510
+ try {
511
+ process.chdir(base)
512
+ walkExpected(destinationChain, destinationPolicy)
513
+ removeCreatedChain(destinationChain, request.identity, destinationPolicy)
514
+ } catch (recoveryError) { recoveryFailures.push('directories:' + describe(recoveryError)) }
515
+ }
516
+ try {
517
+ process.chdir(base)
518
+ identity()
519
+ } catch (recoveryError) { recoveryFailures.push('root:' + describe(recoveryError)) }
520
+ if (recoveryFailures.length > 0) {
521
+ const retained = []
522
+ if (sourceCaptured) retained.push([...request.source.chain.map(entry => entry.name), sourceCapture].join('/'))
523
+ if (destinationQuarantined) retained.push([...request.destination.segments, destinationCapture].join('/'))
524
+ else if (destinationCreated) retained.push([...request.destination.segments, request.destination.name].join('/'))
525
+ refuse({ reason: 'relocate-recovery-failed', failure, recoveryFailures, retained })
526
+ }
527
+ refuse({ reason: 'relocate-failed', failure })
528
+ }
529
+ respond({ kind: 'complete', sha256: request.source.sha256 })
530
+ }
531
+
532
+ // Stages one group: creates its missing parents, copies each staged input beside its target, and
533
+ // copies each prior file to a backup. Targets are never changed here, so an interrupted prepare
534
+ // leaves only stage and backup files. On failure it removes what this group created.
535
+ const prepareGroup = (group, base, created) => {
536
+ const chain = []
537
+ const prepared = []
538
+ const attempted = []
539
+ let start
540
+ let final
541
+ let groupPath
542
+ try {
543
+ process.chdir(base)
544
+ if (!group || typeof group !== 'object' || typeof group.nonce !== 'string' || !Array.isArray(group.anchor) || !Array.isArray(group.entries)) throw new Error('batch-group')
545
+ walkExpected(group.anchor)
546
+ start = fs.lstatSync('.')
547
+ if (!same(start, group.identity)) throw new Error('anchor-identity')
548
+ walkCreating(safeSegments(group.segments), chain, created, group.anchor.map(entry => entry.name))
549
+ final = fs.lstatSync('.')
550
+ groupPath = process.cwd()
551
+ const groupPrefix = [...group.anchor, ...chain].map(entry => entry.name)
552
+ for (const [index, entry] of group.entries.entries()) {
553
+ process.chdir(groupPath)
554
+ const nested = []
555
+ attempted.push({ index, chain: nested })
556
+ nested.push(...walkExpected(entry.chain))
557
+ walkCreating(safeSegments(entry.segments), nested, created, groupPrefix)
558
+ const target = name(entry.name)
559
+ const stage = name('.cockpit-stage-' + group.nonce + '-' + index)
560
+ const backup = name('.cockpit-backup-' + group.nonce + '-' + index)
561
+ const prior = state(target)
562
+ if (!matches(prior, entry.expected)) throw new Error('prestate')
563
+ let stageName = null
564
+ if (entry.operation === 'write-file') {
565
+ copy(process.argv[INPUT_ARGUMENT_OFFSET + entry.input], stage, entry.sha256)
566
+ stageName = stage
567
+ } else if (entry.operation === 'write-symlink') {
568
+ fs.symlinkSync(entry.target, stage)
569
+ stageName = stage
570
+ } else if (entry.operation !== 'remove-file' && entry.operation !== 'remove-symlink') throw new Error('operation')
571
+ let backupName = null
572
+ if (prior.kind === 'file') {
573
+ copy(target, backup, prior.sha256)
574
+ backupName = backup
575
+ }
576
+ prepared.push({
577
+ chain: nested, name: target, operation: entry.operation, sha256: entry.sha256 ?? null, target: entry.target ?? null,
578
+ stage: stageName, backup: backupName, priorKind: prior.kind, priorSha256: prior.sha256 ?? null,
579
+ priorTarget: prior.target ?? null, priorMode: prior.mode ?? null, priorIdentity: prior.identity ?? null,
580
+ })
581
+ verifySourceChain(nested, final)
582
+ }
583
+ process.chdir(groupPath)
584
+ syncDirectory()
585
+ verifySourceChain(chain, group.identity)
586
+ } catch {
587
+ if (!start) return { reason: 'prepare-failed' }
588
+ try {
589
+ if (groupPath && final) {
590
+ for (const item of [...attempted].reverse()) {
591
+ process.chdir(groupPath)
592
+ walkExpected(item.chain)
593
+ fs.rmSync(name('.cockpit-stage-' + group.nonce + '-' + item.index), { force: true })
594
+ fs.rmSync(name('.cockpit-backup-' + group.nonce + '-' + item.index), { force: true })
595
+ syncDirectory()
596
+ removeCreatedChain(item.chain, final)
597
+ }
598
+ }
599
+ process.chdir(base)
600
+ walkExpected(group.anchor)
601
+ if (chain.length > 0) {
602
+ walkExpected(chain)
603
+ removeCreatedChain(chain, group.identity)
604
+ }
605
+ if (!same(fs.lstatSync('.'), group.identity)) throw new Error('anchor-identity')
606
+ } catch {
607
+ // The receipt carries a fixed reason code that the parent matches exactly; cleanup failing is that code.
608
+ return { reason: 'prepare-cleanup-failed' }
609
+ }
610
+ return { reason: 'prepare-failed' }
611
+ }
612
+ return { receipt: { nonce: group.nonce, identity: { dev: final.dev, ino: final.ino }, chain, entries: prepared } }
613
+ }
614
+
615
+ // Prepares every group in order from the transaction root. A later group may enter a directory an
616
+ // earlier group created. On a refusal the receipt still lists the groups already prepared, so the
617
+ // caller can roll them back.
618
+ const prepareBatch = () => {
619
+ if (!Array.isArray(request.groups) || request.groups.length < 1 || request.groups.length > limits.publicationGroups) throw new Error('batch')
620
+ const base = process.cwd()
621
+ const created = new Map()
622
+ const groups = []
623
+ for (const group of request.groups) {
624
+ const outcome = prepareGroup(group, base, created)
625
+ // No process.exit here: this receipt can exceed a pipe buffer, and exiting would truncate it.
626
+ if (!outcome.receipt) return respond({ kind: 'refusal', reason: outcome.reason, groups })
627
+ groups.push(outcome.receipt)
628
+ }
629
+ respond({ kind: 'complete', groups })
630
+ }
631
+
632
+ // validate-batch and verify-batch only check. publish-batch validates every group before it
633
+ // renames any staged entry; commit-batch verifies every group before it retires any backup and
634
+ // reports a verification failure as `verify-failed`, so the caller knows nothing was retired.
635
+ const publicationBatch = () => {
636
+ if (!Array.isArray(request.groups) || request.groups.length < 1 || request.groups.length > limits.publicationGroups) throw new Error('batch')
637
+ const base = process.cwd()
638
+ const eachGroup = operation => {
639
+ for (const group of request.groups) {
640
+ process.chdir(base)
641
+ if (!group || typeof group !== 'object' || typeof group.nonce !== 'string' || typeof group.parent !== 'string'
642
+ || !Array.isArray(group.chain) || !Array.isArray(group.entries)) throw new Error('batch-group')
643
+ const chain = walkExpected(group.chain)
644
+ if (!same(fs.lstatSync('.'), group.identity)) throw new Error('group-identity')
645
+ operation(group)
646
+ verifySourceChain(chain)
647
+ }
648
+ }
649
+ if (request.action === 'validate-batch' || request.action === 'publish-batch') eachGroup(group => validateEntries(group.entries))
650
+ else {
651
+ try { eachGroup(group => verifyEntries(group.entries)) } catch (error) {
652
+ if (request.action === 'commit-batch') refuse({ reason: 'verify-failed' })
653
+ throw error
654
+ }
655
+ }
656
+ if (request.action === 'publish-batch') eachGroup(group => publishEntries(group.entries))
657
+ if (request.action === 'commit-batch') {
658
+ eachGroup(group => {
659
+ for (const entry of group.entries) {
660
+ verifyEntries([entry], group.identity)
661
+ const nested = walkExpected(entry.chain)
662
+ if (entry.backup !== null) fs.unlinkSync(name(entry.backup))
663
+ syncDirectory()
664
+ verifySourceChain(nested, group.identity)
665
+ }
666
+ })
667
+ }
668
+ respond({ kind: 'complete', groups: request.groups.map(group => ({ nonce: group.nonce, parent: group.parent, kind: 'complete' })) })
669
+ }
670
+
671
+ // Restores one prepared or published group from its backups, then removes the directories it created.
672
+ const rollback = () => {
673
+ const groupPath = process.cwd()
674
+ for (const entry of request.entries) {
675
+ process.chdir(groupPath)
676
+ const nested = walkExpected(entry.chain)
677
+ name(entry.name)
678
+ if (entry.stage !== null) name(entry.stage)
679
+ const current = state(entry.name)
680
+ if (!matches(current, postState(entry)) && !matches(current, priorState(entry))
681
+ || entry.backup !== null && hash(entry.backup) !== entry.priorSha256) throw new Error('recovery-state')
682
+ verifySourceChain(nested, request.identity)
683
+ }
684
+ for (const entry of [...request.entries].reverse()) {
685
+ process.chdir(groupPath)
686
+ const nested = walkExpected(entry.chain)
687
+ const current = state(entry.name)
688
+ if (matches(current, postState(entry))) {
689
+ if (current.kind !== 'missing') fs.unlinkSync(entry.name)
690
+ if (entry.priorKind === 'file') {
691
+ fs.renameSync(entry.backup, entry.name)
692
+ fs.chmodSync(entry.name, entry.priorMode)
693
+ } else if (entry.priorKind === 'symlink') fs.symlinkSync(entry.priorTarget, entry.name)
694
+ } else if (entry.backup !== null) fs.unlinkSync(entry.backup)
695
+ if (entry.stage !== null) fs.rmSync(entry.stage, { force: true })
696
+ syncDirectory()
697
+ verifySourceChain(nested, request.identity)
698
+ }
699
+ process.chdir(groupPath)
700
+ removeCreatedDirectories(request.entries, request.identity)
701
+ for (let index = request.chain.length - 1; index >= 0; index--) {
702
+ const child = request.chain[index]
703
+ process.chdir('..')
704
+ const parent = fs.lstatSync('.')
705
+ const expected = index === 0 ? request.start : request.chain[index - 1]
706
+ if (!same(parent, expected)) throw new Error('parent-identity')
707
+ if (!same(fs.lstatSync(name(child.name)), child)) throw new Error('child-identity')
708
+ if (child.created) {
709
+ try {
710
+ fs.rmdirSync(name(child.name))
711
+ syncDirectory()
712
+ } catch (error) { if (error.code !== 'ENOTEMPTY') throw error }
713
+ }
714
+ }
715
+ respond({ kind: 'complete' })
716
+ }
717
+
718
+ try {
719
+ limits = parseLimits(request.limits)
720
+ identity()
721
+ if (request.action === 'read-batch') readBatch()
722
+ else if (request.action === 'read-file' || request.action === 'read-entry') respond(readSource(request))
723
+ else if (request.action === 'relocate-file' || request.action === 'restore-file') relocate()
724
+ else if (request.action === 'prepare-batch') prepareBatch()
725
+ else if (['validate-batch', 'publish-batch', 'verify-batch', 'commit-batch'].includes(request.action)) publicationBatch()
726
+ else if (request.action === 'rollback') rollback()
727
+ else throw new Error('action')
728
+ } catch {
729
+ // The parent accepts only this fixed refusal frame, so the cause has no channel.
730
+ respond({ kind: 'refusal', reason: 'operation-refused' })
731
+ }