@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,270 @@
1
+ 'use strict'
2
+
3
+ // Drives one browser operation for packages/cockpit/src/browser/surfaces.ts through the project's own
4
+ // Playwright module: navigate to a surface, then report it or save a screenshot into the artifact
5
+ // root. Runs from a staged snapshot, so it must stay self-contained.
6
+ //
7
+ // argv: named flags (--operation, --engine, --connection, --url, --origin, --timeout, --module,
8
+ // --module-proof, --control-authorities, --control-ports, --screenshot-limit, and the screenshot
9
+ // artifact flags); see operationStep in surfaces.ts for the full set.
10
+ // stdout on success: one JSON 'cockpit-browser-observation/v1' object. A drive observation also carries
11
+ // per-route diagnostics: counts of console errors, uncaught page errors and failed or >= 400 requests to
12
+ // the application origin, a count of warnings, aborted requests and third-party traffic, and a capped
13
+ // list of entries. Entries hold only identifiers: a request's method, URL path (no query) and status or
14
+ // network error code; a console error's script path and line; an uncaught error's class name. Bodies,
15
+ // headers and cookies are never read. Message text is read only when cockpit passes --messages, which it
16
+ // does only for a data posture that is isolated, not restored and not reaching production; the drive
17
+ // observation then carries a capped `messages` list of console error and uncaught error text.
18
+ // stderr on failure: one JSON 'cockpit-browser-failure/v1' object naming the phase and its category,
19
+ // and, when one is known, a fixed-vocabulary reason (a browser network error code such as
20
+ // ERR_CONNECTION_REFUSED, HTTP_<status> or TIMEOUT), exit code 1. The failure deliberately carries no
21
+ // error text: page content, URLs and storage state must not leak into cockpit's output.
22
+ //
23
+ // Requests to cockpit's own browser-control authorities and nested top-level navigations are blocked
24
+ // so the page under test cannot reach the control plane that drives it. Requests to a loopback or
25
+ // resolver-mapped host outside the application origin are blocked too: such an address is the host
26
+ // (through the remote runner's network exposure, or directly for a local browser), and the operation
27
+ // was given only the application. Playwright does not route a request the browser follows as a
28
+ // redirect, so a redirect to another loopback port still reaches it; this blocks direct requests only.
29
+ const fs = require('node:fs')
30
+ const crypto = require('node:crypto')
31
+ const path = require('node:path')
32
+ const args = process.argv.slice(2)
33
+ const one = name => { const index = args.indexOf(name); if (index < 0 || index + 1 >= args.length) throw new Error('missing argument'); return args[index + 1] }
34
+ const digest = bytes => crypto.createHash('sha256').update(bytes).digest('hex')
35
+ const MAX_ENTRIES = 20
36
+ const MAX_PATH = 200
37
+ const MAX_MESSAGES = 5
38
+ const MAX_MESSAGE_BYTES = 240
39
+ const SETTLE_MS = 1000
40
+ // Records what the page did wrong; what it said only with withMessages: see the stdout contract above.
41
+ function collectDiagnostics(page, origin, withMessages) {
42
+ const counts = { consoleErrors: 0, consoleWarnings: 0, pageErrors: 0, failedRequests: 0, abortedRequests: 0 }
43
+ const thirdParty = { requests: 0, failed: 0 }
44
+ const entries = []
45
+ let omitted = 0
46
+ const messages = []
47
+ // Control characters are replaced so message text stays one line of plain text; the cap is in bytes.
48
+ const excerpt = value => {
49
+ // A character is at least one byte, so the first MAX_MESSAGE_BYTES characters hold the longest prefix within the cap.
50
+ let text = String(value).replace(/[\x00-\x1f\x7f]+/g, ' ').trim().slice(0, MAX_MESSAGE_BYTES)
51
+ while (Buffer.byteLength(text) > MAX_MESSAGE_BYTES) text = text.slice(0, -1)
52
+ return text
53
+ }
54
+ const keep = (kind, value) => { if (withMessages && messages.length < MAX_MESSAGES) { const text = excerpt(value); if (text) messages.push({ kind, text }) } }
55
+ const add = entry => { if (entries.length < MAX_ENTRIES) entries.push(entry); else omitted += 1 }
56
+ // `new URL` throws TypeError for an address it cannot parse; anything else is a real fault.
57
+ const parse = value => {
58
+ let parsed
59
+ try { parsed = new URL(value) } catch (error) { if (error instanceof TypeError) return undefined; throw error }
60
+ return ['http:', 'https:'].includes(parsed.protocol) ? parsed : undefined
61
+ }
62
+ const pathOf = parsed => parsed.pathname.slice(0, MAX_PATH)
63
+ page.on('console', message => {
64
+ if (message.type() === 'warning') counts.consoleWarnings += 1
65
+ if (message.type() !== 'error') return
66
+ counts.consoleErrors += 1
67
+ keep('console', message.text())
68
+ const location = message.location()
69
+ const parsed = location && typeof location.url === 'string' ? parse(location.url) : undefined
70
+ const own = parsed && parsed.origin === origin
71
+ add({ kind: 'console', ...(own ? { path: pathOf(parsed) } : {}),
72
+ ...(own && Number.isInteger(location.lineNumber) && location.lineNumber >= 0 ? { line: location.lineNumber } : {}) })
73
+ })
74
+ page.on('pageerror', error => {
75
+ counts.pageErrors += 1
76
+ keep('pageerror', error && typeof error.message === 'string' ? error.message : 'uncaught error')
77
+ add({ kind: 'pageerror', name: error && typeof error.name === 'string' && /^[A-Za-z0-9_$.]{1,64}$/.test(error.name) ? error.name : 'Error' })
78
+ })
79
+ page.on('request', request => { const parsed = parse(request.url()); if (parsed && parsed.origin !== origin) thirdParty.requests += 1 })
80
+ page.on('requestfailed', request => {
81
+ const parsed = parse(request.url())
82
+ if (!parsed) return
83
+ const failure = request.failure()
84
+ const code = failure && typeof failure.errorText === 'string' ? /net::(ERR_[A-Z0-9_]{1,64})\b/.exec(failure.errorText) : null
85
+ // A request the browser cancelled itself, such as one left behind by a navigation, is not a failure.
86
+ if (code && code[1] === 'ERR_ABORTED') { counts.abortedRequests += 1; return }
87
+ if (parsed.origin !== origin) { thirdParty.failed += 1; return }
88
+ counts.failedRequests += 1
89
+ add({ kind: 'request', method: request.method(), path: pathOf(parsed), failure: code ? code[1] : 'ERR_UNKNOWN' })
90
+ })
91
+ page.on('response', response => {
92
+ const status = response.status()
93
+ if (status < 400) return
94
+ const request = response.request()
95
+ // A main-document failure already refuses the drive, so it is not reported twice.
96
+ if (request.isNavigationRequest() && request.frame() === page.mainFrame()) return
97
+ const parsed = parse(response.url())
98
+ if (!parsed) return
99
+ if (parsed.origin !== origin) { thirdParty.failed += 1; return }
100
+ counts.failedRequests += 1
101
+ add({ kind: 'request', method: request.method(), path: pathOf(parsed), status })
102
+ })
103
+ return () => ({ ...counts, thirdParty, entries, omitted, ...(withMessages ? { messages } : {}) })
104
+ }
105
+ async function main() {
106
+ const moduleProof = fs.readFileSync(one('--module-proof'))
107
+ const modulePath = one('--module')
108
+ const moduleBytes = fs.readFileSync(modulePath)
109
+ if (digest(moduleProof) !== digest(moduleBytes)) throw new Error('module changed')
110
+ const api = require(modulePath)
111
+ const engine = one('--engine')
112
+ const browserType = api && api[engine]
113
+ const connection = one('--connection')
114
+ if (!['chromium', 'firefox', 'webkit'].includes(engine) || !browserType
115
+ || connection === 'remote' && typeof browserType.connect !== 'function'
116
+ || connection === 'local' && typeof browserType.launch !== 'function') throw new Error('browser module lacks selected engine operation')
117
+ const operation = one('--operation')
118
+ const url = one('--url')
119
+ const timeout = Number(one('--timeout'))
120
+ const deadline = Date.now() + timeout
121
+ const remaining = () => { const value = deadline - Date.now(); if (value <= 0) throw new Error('browser operation deadline expired'); return value }
122
+ const resolverRule = args.includes('--resolver-rule') ? one('--resolver-rule') : undefined
123
+ const categories = { connect: 'availability', context: 'setup', navigation: 'navigation',
124
+ screenshot: 'observation', artifact: 'artifact', cleanup: 'cleanup' }
125
+ let browser, context, page, observation, failurePhase, failureReason, httpStatus, cleanupFailed = false, phase = 'connect'
126
+ try {
127
+ browser = connection === 'remote'
128
+ ? await browserType.connect(one('--endpoint'), { timeout: remaining(), exposeNetwork: one('--expose-network') })
129
+ : await browserType.launch({ headless: true, timeout: remaining(),
130
+ ...(resolverRule ? { args: ['--host-resolver-rules=' + resolverRule] } : {}) })
131
+ phase = 'context'
132
+ const state = process.env.COCKPIT_BROWSER_STORAGE_STATE
133
+ const stateProof = state ? fs.readFileSync(one('--state-proof')) : undefined
134
+ if (state && digest(fs.readFileSync(state)) !== digest(stateProof)) throw new Error('storage state changed')
135
+ const storageState = stateProof ? JSON.parse(stateProof.toString('utf8')) : undefined
136
+ context = await browser.newContext({ ...(storageState ? { storageState } : {}), serviceWorkers: 'block' })
137
+ const controlAuthorities = new Set(JSON.parse(one('--control-authorities')))
138
+ const controlPorts = new Set(JSON.parse(one('--control-ports')).map(String))
139
+ // An address that does not parse cannot be shown to be safe, so both guards treat it as blocked
140
+ // rather than naming the error, which would quote the address.
141
+ const isControlAuthority = value => { try {
142
+ const parsed = new URL(value)
143
+ const port = parsed.port || (parsed.protocol === 'http:' || parsed.protocol === 'ws:' ? '80'
144
+ : parsed.protocol === 'https:' || parsed.protocol === 'wss:' ? '443' : '')
145
+ return controlAuthorities.has(parsed.hostname + ':' + port) || controlPorts.has(port)
146
+ } catch { return true } }
147
+ const origin = one('--origin')
148
+ const mappedHosts = new Set((resolverRule || '').split(',').map(rule => rule.trim().split(/\s+/))
149
+ .filter(parts => parts[0] === 'MAP' && parts[1]).map(parts => parts[1].toLowerCase()))
150
+ const isForeignHost = value => { try {
151
+ const parsed = new URL(value)
152
+ const scheme = { 'ws:': 'http:', 'wss:': 'https:' }[parsed.protocol] || parsed.protocol
153
+ if (!['http:', 'https:'].includes(scheme) || new URL(scheme + '//' + parsed.host).origin === origin) return false
154
+ const hostname = parsed.hostname
155
+ return hostname === 'localhost' || hostname.endsWith('.localhost') || /^127\./.test(hostname) || hostname === '0.0.0.0'
156
+ || hostname === '[::1]' || hostname.startsWith('[::ffff:7f') || mappedHosts.has(hostname)
157
+ } catch { return true } }
158
+ let mainNavigationAttempts = 0
159
+ await context.route('**/*', async (route, request) => {
160
+ if (isControlAuthority(request.url()) || isForeignHost(request.url())) return route.abort('blockedbyclient')
161
+ const frame = request.frame()
162
+ const mainNavigation = request.isNavigationRequest() && page && frame === page.mainFrame()
163
+ if (request.isNavigationRequest() && !mainNavigation
164
+ && (!frame || typeof frame.parentFrame !== 'function' || frame.parentFrame() === null)) return route.abort('blockedbyclient')
165
+ if (mainNavigation) mainNavigationAttempts += 1
166
+ await route.continue()
167
+ })
168
+ if (typeof context.routeWebSocket !== 'function') throw new Error('browser module lacks WebSocket routing')
169
+ await context.routeWebSocket(() => true, webSocket => {
170
+ if (isControlAuthority(webSocket.url()) || isForeignHost(webSocket.url())) return webSocket.close({ code: 1008, reason: 'blocked by the cockpit browser operation' })
171
+ webSocket.connectToServer()
172
+ })
173
+ phase = 'navigation'
174
+ page = await context.newPage()
175
+ const diagnostics = operation === 'drive' ? collectDiagnostics(page, origin, args.includes('--messages')) : undefined
176
+ let documentEpoch = 0
177
+ let documentResponse
178
+ page.on('framenavigated', frame => { if (frame === page.mainFrame()) documentEpoch += 1 })
179
+ page.on('response', response => {
180
+ const request = response.request()
181
+ if (request.isNavigationRequest() && request.frame() === page.mainFrame()) documentResponse = response
182
+ })
183
+ const currentDocument = () => {
184
+ const displayed = new URL(page.url())
185
+ if (displayed.origin !== origin || displayed.username || displayed.password || displayed.hash) throw new Error('navigation left application origin')
186
+ if (!documentResponse || typeof documentResponse.ok !== 'function' || !documentResponse.ok()
187
+ || typeof documentResponse.url !== 'function' || documentResponse.url() !== displayed.href) {
188
+ throw new Error('displayed document lacks a successful terminal HTTP response')
189
+ }
190
+ return { url: displayed.href, epoch: documentEpoch, attempts: mainNavigationAttempts }
191
+ }
192
+ const response = await page.goto(url, { timeout: remaining() })
193
+ if (response && typeof response.status === 'function') httpStatus = response.status()
194
+ if (!response || typeof response.ok !== 'function' || !response.ok()) throw new Error('navigation did not return a successful HTTP response')
195
+ documentResponse = response
196
+ const initialDocument = currentDocument()
197
+ if (operation === 'drive') {
198
+ // Errors raised just after load are findings too. An application that polls never goes idle,
199
+ // so running out the short wait is not a failure; any other error still propagates.
200
+ try { await page.waitForLoadState('networkidle', { timeout: Math.min(remaining(), SETTLE_MS) }) }
201
+ catch (error) { if (!error || error.name !== 'TimeoutError') throw error }
202
+ observation = { schema: 'cockpit-browser-observation/v1', operation, finalUrl: initialDocument.url, diagnostics: diagnostics() }
203
+ }
204
+ else if (operation === 'screenshot') {
205
+ phase = 'screenshot'
206
+ const displayedBefore = currentDocument()
207
+ const bytes = await page.screenshot()
208
+ if (!Buffer.isBuffer(bytes) || bytes.byteLength > Number(one('--screenshot-limit'))) throw new Error('screenshot observation exceeds bound')
209
+ const displayedAfter = currentDocument()
210
+ if (displayedAfter.url !== displayedBefore.url || displayedAfter.epoch !== displayedBefore.epoch
211
+ || displayedAfter.attempts !== displayedBefore.attempts) throw new Error('navigation changed during screenshot observation')
212
+ const output = one('--output')
213
+ const root = one('--artifact-root')
214
+ const expectedDev = Number(one('--artifact-dev'))
215
+ const expectedIno = Number(one('--artifact-ino'))
216
+ const expectedBirthtimeNs = one('--artifact-birthtime-ns')
217
+ phase = 'artifact'
218
+ if (path.dirname(output) !== root || path.basename(output) !== one('--output-name')) throw new Error('screenshot output escaped root')
219
+ let rootDescriptor, dotDescriptor, descriptor
220
+ try {
221
+ rootDescriptor = fs.openSync(root, fs.constants.O_RDONLY | fs.constants.O_DIRECTORY | fs.constants.O_NOFOLLOW)
222
+ const held = fs.fstatSync(rootDescriptor)
223
+ const named = fs.lstatSync(root)
224
+ // ext4 reuses a freed inode for the next directory created, so a root recreated since
225
+ // planning can carry the planned device and inode; birth time tells the two apart.
226
+ if (!held.isDirectory() || named.isSymbolicLink() || held.dev !== expectedDev || held.ino !== expectedIno
227
+ || String(fs.fstatSync(rootDescriptor, { bigint: true }).birthtimeNs) !== expectedBirthtimeNs
228
+ || named.dev !== held.dev || named.ino !== held.ino) throw new Error('artifact root changed')
229
+ process.chdir(root)
230
+ dotDescriptor = fs.openSync('.', fs.constants.O_RDONLY | fs.constants.O_DIRECTORY | fs.constants.O_NOFOLLOW)
231
+ const anchored = fs.fstatSync(dotDescriptor)
232
+ if (anchored.dev !== held.dev || anchored.ino !== held.ino) throw new Error('artifact root changed')
233
+ descriptor = fs.openSync(one('--output-name'), fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_NOFOLLOW, 0o600)
234
+ fs.fchmodSync(descriptor, 0o600); fs.writeFileSync(descriptor, bytes); fs.fsyncSync(descriptor)
235
+ const after = fs.fstatSync(dotDescriptor)
236
+ if (after.dev !== held.dev || after.ino !== held.ino) throw new Error('artifact root changed')
237
+ } finally {
238
+ if (descriptor !== undefined) fs.closeSync(descriptor)
239
+ if (dotDescriptor !== undefined) fs.closeSync(dotDescriptor)
240
+ if (rootDescriptor !== undefined) fs.closeSync(rootDescriptor)
241
+ }
242
+ observation = { schema: 'cockpit-browser-observation/v1', operation, finalUrl: displayedAfter.url, evidence: { sha256: digest(bytes), byteLength: bytes.byteLength } }
243
+ } else throw new Error('unsupported operation')
244
+ } catch (error) {
245
+ failurePhase = phase
246
+ // Only identifiers leave this process: Chromium's net error code, the HTTP status or a timeout.
247
+ const message = error && typeof error.message === 'string' ? error.message : ''
248
+ const network = /net::(ERR_[A-Z0-9_]{1,64})\b/.exec(message)
249
+ failureReason = network ? network[1]
250
+ : error && error.name === 'TimeoutError' ? 'TIMEOUT'
251
+ : phase === 'navigation' && Number.isInteger(httpStatus) && httpStatus >= 100 && httpStatus <= 599
252
+ && (httpStatus < 200 || httpStatus > 299) ? 'HTTP_' + httpStatus
253
+ : undefined
254
+ }
255
+ finally {
256
+ for (const value of [page, context, browser]) if (value && typeof value.close === 'function') {
257
+ // The failure record carries a cleanup flag and never a message, so the error is not kept.
258
+ try { await value.close() } catch { cleanupFailed = true }
259
+ }
260
+ }
261
+ if (failurePhase || cleanupFailed || !observation) {
262
+ const failedAt = failurePhase || 'cleanup'
263
+ process.stderr.write(JSON.stringify({ schema: 'cockpit-browser-failure/v1', phase: failedAt,
264
+ category: categories[failedAt], cleanup: cleanupFailed, ...(failurePhase && failureReason ? { reason: failureReason } : {}) }))
265
+ process.exitCode = 1
266
+ return
267
+ }
268
+ process.stdout.write(JSON.stringify(observation))
269
+ }
270
+ main().catch(() => { process.stderr.write('{"schema":"cockpit-browser-failure/v1","phase":"context","category":"setup","cleanup":false}'); process.exitCode = 1 })
@@ -0,0 +1,101 @@
1
+ 'use strict'
2
+
3
+ // Reads and replaces the executor's fence marker: the small JSON file that records whether a fenced
4
+ // attempt is active, terminating, terminated or cancelled. The executor
5
+ // (packages/cockpit/src/environment-exec/execute.ts), libexec/fenced-supervisor.cjs and
6
+ // libexec/fenced-group-runner.cjs all read it here so they agree on what an authentic marker is.
7
+ //
8
+ // A marker is accepted only when the bytes came from the file that is named now: it is opened without
9
+ // following links, read whole within a small bound, and the descriptor, the named entry and the parent
10
+ // directory must still agree afterwards. Each marker records its own device and inode, so a copy or a
11
+ // marker renamed into place from elsewhere does not match the file it claims to be. This module
12
+ // decides nothing about status; callers check the fields they need.
13
+ const fs = require('node:fs')
14
+ const path = require('node:path')
15
+
16
+ const FENCE_MARKER_MAX_BYTES = 1024n
17
+
18
+ function readFenceMarkerFile(markerPath) {
19
+ let fd
20
+ try {
21
+ fd = fs.openSync(markerPath, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW | fs.constants.O_NONBLOCK)
22
+ const before = fs.fstatSync(fd, { bigint: true })
23
+ if (!before.isFile() || before.size > FENCE_MARKER_MAX_BYTES) return undefined
24
+ const bytes = Buffer.alloc(Number(before.size) + 1)
25
+ const count = fs.readSync(fd, bytes, 0, bytes.length, 0)
26
+ const after = fs.fstatSync(fd, { bigint: true })
27
+ const named = fs.lstatSync(markerPath, { bigint: true })
28
+ const parent = fs.lstatSync(path.dirname(markerPath), { bigint: true })
29
+ if (count !== Number(before.size) || before.dev !== after.dev || before.ino !== after.ino || before.size !== after.size
30
+ || !named.isFile() || named.dev !== before.dev || named.ino !== before.ino
31
+ || !parent.isDirectory() || parent.isSymbolicLink()) return undefined
32
+ return {
33
+ value: JSON.parse(bytes.subarray(0, count).toString('utf8')),
34
+ identity: {
35
+ markerDev: String(before.dev), markerIno: String(before.ino),
36
+ parentDev: String(parent.dev), parentIno: String(parent.ino),
37
+ },
38
+ }
39
+ } catch {
40
+ // Absent, unreadable and malformed markers all mean "no authentic marker"; every caller refuses
41
+ // on that answer, and the fence protocol has no channel that could carry the cause further.
42
+ return undefined
43
+ } finally {
44
+ if (fd !== undefined) try { fs.closeSync(fd) } catch { /* the answer above does not depend on the descriptor */ }
45
+ }
46
+ }
47
+
48
+ // The marker the executor published for this supervisor, unchanged since the executor named it in
49
+ // its release line. `approved` carries supervisorPid plus the four identity fields from that line.
50
+ function readApprovedActiveMarker(fence, approved) {
51
+ const read = readFenceMarkerFile(fence.markerPath)
52
+ if (!read) return undefined
53
+ const { value: marker, identity } = read
54
+ if (marker === null || typeof marker !== 'object'
55
+ || marker.format !== 'cockpit-executor/v1' || marker.attemptId !== fence.attemptId
56
+ || marker.supervisorPid !== approved.supervisorPid || marker.status !== 'active'
57
+ || marker.markerDev !== approved.markerDev || marker.markerIno !== approved.markerIno
58
+ || marker.parentDev !== approved.parentDev || marker.parentIno !== approved.parentIno
59
+ || marker.markerDev !== identity.markerDev || marker.markerIno !== identity.markerIno
60
+ || marker.parentDev !== identity.parentDev || marker.parentIno !== identity.parentIno) return undefined
61
+ return marker
62
+ }
63
+
64
+ function syncDirectory(directory) {
65
+ const fd = fs.openSync(directory, fs.constants.O_RDONLY | fs.constants.O_DIRECTORY | fs.constants.O_NOFOLLOW)
66
+ try { fs.fsyncSync(fd) } finally { fs.closeSync(fd) }
67
+ }
68
+
69
+ // Replaces the approved active marker with one carrying `fields`, durably. The replacement is written
70
+ // beside the marker under a name owned by `writer` and this process, and renamed over the marker only
71
+ // if the active marker is still the approved one, so a concurrent repair or cancel is never overwritten.
72
+ function replaceApprovedActiveMarker(fence, approved, writer, fields) {
73
+ const current = readApprovedActiveMarker(fence, approved)
74
+ if (!current) return false
75
+ const temporary = `${fence.markerPath}.${writer}-${process.pid}`
76
+ let fd
77
+ try {
78
+ fd = fs.openSync(temporary, fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY | fs.constants.O_NOFOLLOW, 0o600)
79
+ const identity = fs.fstatSync(fd, { bigint: true })
80
+ fs.writeFileSync(fd, `${JSON.stringify({ ...current, markerDev: String(identity.dev), markerIno: String(identity.ino), ...fields })}\n`)
81
+ fs.fsyncSync(fd)
82
+ fs.closeSync(fd)
83
+ fd = undefined
84
+ if (!readApprovedActiveMarker(fence, approved)) return false
85
+ fs.renameSync(temporary, fence.markerPath)
86
+ syncDirectory(path.dirname(fence.markerPath))
87
+ return true
88
+ } catch {
89
+ // Before the rename, nothing landed: the active marker stays in place, which repair reads as
90
+ // "attempt may still run". If only the directory sync failed, the rename has happened and the
91
+ // replacement is what a reader now sees, but a crash may still bring the old active marker
92
+ // back. Either way the new status is not known durable, so the caller gets false and must not
93
+ // report it published; repair then reads whichever marker survives and both are safe to act on.
94
+ return false
95
+ } finally {
96
+ if (fd !== undefined) try { fs.closeSync(fd) } catch { /* the replacement already failed */ }
97
+ try { fs.unlinkSync(temporary) } catch { /* renamed into place, or never created */ }
98
+ }
99
+ }
100
+
101
+ module.exports = { readFenceMarkerFile, replaceApprovedActiveMarker, syncDirectory }
@@ -0,0 +1,24 @@
1
+ export interface FenceMarkerIdentity {
2
+ readonly markerDev: string
3
+ readonly markerIno: string
4
+ readonly parentDev: string
5
+ readonly parentIno: string
6
+ }
7
+
8
+ export interface FenceMarkerLocation {
9
+ readonly attemptId: string
10
+ readonly markerPath: string
11
+ }
12
+
13
+ export interface ApprovedFenceMarker extends FenceMarkerIdentity {
14
+ readonly supervisorPid: number
15
+ }
16
+
17
+ export function readFenceMarkerFile(markerPath: string): { readonly value: unknown; readonly identity: FenceMarkerIdentity } | undefined
18
+ export function replaceApprovedActiveMarker(
19
+ fence: FenceMarkerLocation,
20
+ approved: ApprovedFenceMarker,
21
+ writer: string,
22
+ fields: Readonly<Record<string, unknown>>,
23
+ ): boolean
24
+ export function syncDirectory(directory: string): void
@@ -0,0 +1,77 @@
1
+ 'use strict'
2
+
3
+ // Runs one fenced action as the leader of its own process group, under libexec/fenced-supervisor.cjs.
4
+ //
5
+ // argv: <config>, base64url JSON { command, args, argv0, cwd, spawnNotAfter, fence, approved }, where
6
+ // `approved` is the active marker the executor published ({ supervisorPid, markerDev, markerIno,
7
+ // parentDev, parentIno }).
8
+ // fd 3 to the supervisor: 'EXPIRED\n' when the spawn deadline had passed (the action never starts),
9
+ // otherwise exactly one 'DONE <exit code>\n' when the action closes.
10
+ // stdin from the supervisor: 'STOP\n' stops the group. Any other input, or stdin closing without it,
11
+ // means the supervisor is gone: the runner then records a 'terminating' marker naming its group, so
12
+ // a later repair can prove the group ended, before it stops the group itself.
13
+ const { spawn } = require('node:child_process')
14
+ const fs = require('node:fs')
15
+ const path = require('node:path')
16
+ const { replaceApprovedActiveMarker, syncDirectory } = require('./fence-marker.cjs')
17
+
18
+ const STOP_KILL_DELAY_MS = 50
19
+
20
+ const config = JSON.parse(Buffer.from(process.argv[2], 'base64url').toString('utf8'))
21
+ const { fence, approved } = config
22
+
23
+ // The group ends only through SIGKILL below, after the action had its SIGTERM; a stray terminal
24
+ // signal must not take the runner down and orphan the action outside its fence.
25
+ process.on('SIGTERM', () => {})
26
+ process.on('SIGINT', () => {})
27
+
28
+ const expired = config.spawnNotAfter !== undefined && Date.now() > Date.parse(config.spawnNotAfter)
29
+ const child = expired ? undefined : spawn(config.command, config.args, {
30
+ argv0: config.argv0, cwd: config.cwd, env: process.env, stdio: ['ignore', 'inherit', 'inherit'],
31
+ })
32
+ let reported = false
33
+ const report = code => {
34
+ if (reported) return
35
+ reported = true
36
+ fs.writeSync(3, `DONE ${Number.isInteger(code) ? code : 1}\n`)
37
+ }
38
+ if (expired) fs.writeSync(3, 'EXPIRED\n')
39
+ else {
40
+ child.once('error', () => report(1))
41
+ child.once('close', code => report(code))
42
+ }
43
+
44
+ function publishTerminating() {
45
+ if (!replaceApprovedActiveMarker(fence, approved, 'runner', { status: 'terminating', groupPid: process.pid })) return false
46
+ try {
47
+ try { fs.unlinkSync(fence.socketPath) } catch (error) { if (error.code !== 'ENOENT') return false }
48
+ syncDirectory(path.dirname(fence.socketPath))
49
+ return true
50
+ } catch {
51
+ // The 'terminating' marker is already durable; repair still waits for the socket to go.
52
+ return false
53
+ }
54
+ }
55
+
56
+ let stopping = false
57
+ let controlled = false
58
+ function stop(supervisorLost) {
59
+ if (stopping) return
60
+ if (supervisorLost) publishTerminating()
61
+ stopping = true
62
+ process.kill(-process.pid, 'SIGTERM')
63
+ setTimeout(() => process.kill(-process.pid, 'SIGKILL'), STOP_KILL_DELAY_MS)
64
+ }
65
+
66
+ let input = ''
67
+ process.stdin.setEncoding('utf8')
68
+ process.stdin.on('data', chunk => {
69
+ input += chunk
70
+ if (stopping || !input.includes('\n')) return
71
+ if (input !== 'STOP\n') return stop(true)
72
+ controlled = true
73
+ stop(false)
74
+ })
75
+ process.stdin.on('end', () => { if (!controlled) stop(true) })
76
+ // Keeps the runner alive after the action closes, until the supervisor sends STOP.
77
+ setInterval(() => {}, 1000)
@@ -0,0 +1,137 @@
1
+ 'use strict'
2
+
3
+ // Supervises one fenced action for the executor (packages/cockpit/src/environment-exec/execute.ts) so
4
+ // that an attempt whose executor died can still be ended and proven gone by `repairFencedExecution`.
5
+ //
6
+ // argv: <config>, base64url JSON { command, args, argv0, cwd, spawnNotAfter, fence }.
7
+ // fd 3 to the executor: 'READY\n' once the repair socket listens; 'EXPIRED\n' before exiting when the
8
+ // action was never released because its spawn deadline passed or the executor cancelled it.
9
+ // stdin from the executor: one release line, 'GO' or 'CANCEL', then the attempt id and the device and
10
+ // inode of the active marker and its parent directory that the executor published. Anything else exits.
11
+ // repair socket: a client that writes the attempt id is answered { attemptId, supervisorPid } and the
12
+ // action is stopped.
13
+ // exit code: the action's exit code, or 125 when the fence could not be kept (protocol violation, the
14
+ // runner lost, or no durable 'terminated' marker). The supervisor exits only after the action's process
15
+ // group is gone and the marker reads 'terminated', so that marker proves nothing of the attempt runs.
16
+ const { spawn } = require('node:child_process')
17
+ const { createServer } = require('node:net')
18
+ const fs = require('node:fs')
19
+ const path = require('node:path')
20
+ const { replaceApprovedActiveMarker } = require('./fence-marker.cjs')
21
+
22
+ const FENCE_LOST = 125
23
+ const GROUP_EXIT_WAIT_MS = 2000
24
+ const RUNNER = path.join(__dirname, 'fenced-group-runner.cjs')
25
+
26
+ const config = JSON.parse(Buffer.from(process.argv[2], 'base64url').toString('utf8'))
27
+ const { fence } = config
28
+
29
+ let action
30
+ let approved
31
+ let started = false
32
+ let finished = false
33
+ let terminating = false
34
+ let expired = false
35
+ let actionCode = 1
36
+
37
+ const groupGone = pid => {
38
+ try { process.kill(-pid, 0); return false } catch (error) { return error.code === 'ESRCH' }
39
+ }
40
+ const removeSocket = () => {
41
+ try { fs.unlinkSync(fence.socketPath) } catch { /* already absent; repair checks absence itself */ }
42
+ }
43
+ const abandon = () => {
44
+ removeSocket()
45
+ process.exit(FENCE_LOST)
46
+ }
47
+ const publishTerminated = () => approved !== undefined && replaceApprovedActiveMarker(
48
+ fence, { ...approved, supervisorPid: process.pid }, 'supervisor',
49
+ { status: 'terminated', finishedAt: new Date().toISOString() },
50
+ )
51
+
52
+ const server = createServer(socket => {
53
+ let request = ''
54
+ socket.setEncoding('utf8')
55
+ socket.on('data', chunk => {
56
+ request += chunk
57
+ if (request.length > 256) return socket.destroy()
58
+ if (!request.includes('\n')) return
59
+ if (request.trim() !== fence.attemptId) return socket.destroy()
60
+ socket.end(`${JSON.stringify({ attemptId: fence.attemptId, supervisorPid: process.pid })}\n`)
61
+ terminate()
62
+ })
63
+ })
64
+
65
+ function finish(deadline) {
66
+ if (finished) return
67
+ if (action && !groupGone(action.pid)) {
68
+ if (Date.now() >= deadline) return abandon()
69
+ return setTimeout(() => finish(deadline), 10)
70
+ }
71
+ finished = true
72
+ server.close()
73
+ removeSocket()
74
+ if (!publishTerminated()) process.exit(FENCE_LOST)
75
+ if (expired) fs.writeSync(3, 'EXPIRED\n')
76
+ process.exit(actionCode)
77
+ }
78
+
79
+ function terminate(code) {
80
+ if (!action || terminating) return
81
+ terminating = true
82
+ actionCode = Number.isInteger(code) ? code : 1
83
+ // A stop line that cannot be written means the action is unreachable; abandon exits with the fence-lost status.
84
+ try { action.stdin.end('STOP\n') } catch { abandon() }
85
+ }
86
+
87
+ function startAction() {
88
+ const runnerConfig = Buffer.from(JSON.stringify({
89
+ command: config.command, args: config.args, argv0: config.argv0, cwd: config.cwd,
90
+ spawnNotAfter: config.spawnNotAfter, fence, approved: { ...approved, supervisorPid: process.pid },
91
+ })).toString('base64url')
92
+ action = spawn(process.execPath, ['--', RUNNER, runnerConfig], {
93
+ cwd: config.cwd, env: process.env, stdio: ['pipe', 'inherit', 'inherit', 'pipe'], detached: true,
94
+ })
95
+ let control = ''
96
+ action.stdio[3].setEncoding('utf8')
97
+ action.stdio[3].on('data', chunk => {
98
+ control += chunk
99
+ if (control.length > 64) return abandon()
100
+ if (!control.includes('\n')) return
101
+ if (control === 'EXPIRED\n') {
102
+ expired = true
103
+ return terminate(1)
104
+ }
105
+ const done = /^DONE (-?[0-9]+)\n$/.exec(control)
106
+ if (!done) return abandon()
107
+ terminate(Number(done[1]))
108
+ })
109
+ action.once('error', abandon)
110
+ action.once('close', () => {
111
+ if (!terminating) return abandon()
112
+ finish(Date.now() + GROUP_EXIT_WAIT_MS)
113
+ })
114
+ }
115
+
116
+ try { fs.unlinkSync(fence.socketPath) } catch (error) { if (error.code !== 'ENOENT') process.exit(FENCE_LOST) }
117
+ server.listen(fence.socketPath, () => fs.writeSync(3, 'READY\n'))
118
+
119
+ let release = ''
120
+ process.stdin.setEncoding('utf8')
121
+ process.stdin.on('data', chunk => {
122
+ release += chunk
123
+ if (started || !release.includes('\n')) return
124
+ const fields = release.trim().split(' ')
125
+ if (fields.length !== 6 || (fields[0] !== 'GO' && fields[0] !== 'CANCEL') || fields[1] !== fence.attemptId
126
+ || fields.slice(2).some(value => !/^[0-9]+$/.test(value))) return abandon()
127
+ approved = { markerDev: fields[2], markerIno: fields[3], parentDev: fields[4], parentIno: fields[5] }
128
+ started = true
129
+ if (fields[0] === 'CANCEL' || config.spawnNotAfter !== undefined && Date.now() > Date.parse(config.spawnNotAfter)) {
130
+ expired = true
131
+ return finish(Date.now() + GROUP_EXIT_WAIT_MS)
132
+ }
133
+ startAction()
134
+ })
135
+ process.stdin.on('end', () => { if (!started) abandon() })
136
+ process.on('SIGTERM', () => terminate(1))
137
+ process.on('SIGINT', () => terminate(1))