@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,490 @@
1
+ // Reads the service topology of a cockpit's own Compose files, as Compose itself resolves them, and
2
+ // judges it against what parallel stacks need. It owns one bounded `docker compose config` probe
3
+ // (dispatched through environment-exec/execute.ts) and the projection of its JSON; it never parses
4
+ // the YAML itself, never interpolates the environment, and never starts anything. The generated
5
+ // environment does not come through here: its topology is the one cockpit rendered.
6
+ //
7
+ // The runtime topology admitted by runtime.ts serves `up` and keeps only what readiness needs; it
8
+ // drops published ports and image references, which are exactly what these detectors judge.
9
+ import { realpath } from 'node:fs/promises';
10
+ import { basename, dirname, isAbsolute, join, resolve, sep } from 'node:path';
11
+ import { authenticatedExecutionOutcome } from '../environment-exec/execute.js';
12
+ import { readAnchoredBatches } from '../generated-environment/transaction.js';
13
+ import { snapshotExecutionStep, hasErrorCode } from './contract.js';
14
+ import { redactSensitiveText } from './sensitive.js';
15
+ import { cockpitCommand } from '../fix-catalogue.js';
16
+ /**
17
+ * The directory every relative path in a cockpit's own Compose files resolves against: the first
18
+ * file's, as `docker compose -f first.yml -f second.yml` takes it. Compose cannot default it itself
19
+ * because it reads snapshots of the files, and the cockpit root is the wrong default: a product
20
+ * repository's Compose file names `./api` relative to the repository.
21
+ */
22
+ export function composeProjectDirectory(root, references) {
23
+ return dirname(resolve(root, references[0]));
24
+ }
25
+ // The same allowlist the lifecycle owners hand Docker: no HOME, so no per-user configuration or
26
+ // credential helper can change what Compose resolves.
27
+ const DOCKER_ENVIRONMENT = new Set(['PATH', 'DOCKER_HOST', 'DOCKER_CONTEXT', 'DOCKER_TLS_VERIFY', 'DOCKER_CERT_PATH']);
28
+ const OUTPUT_LIMIT_BYTES = 1_048_576;
29
+ const STDERR_LIMIT_BYTES = 65_536;
30
+ const SERVICE_LIMIT = 4_096;
31
+ const TIMEOUT_MS = 30_000;
32
+ const SERVICE_ID = /^[a-z0-9][a-z0-9_.-]*$/;
33
+ const PINNED_IMAGE = /@sha256:[a-f0-9]{64}$/;
34
+ function unavailable(reason) {
35
+ return { kind: 'unavailable', reason };
36
+ }
37
+ function topologyStep(input, composeFiles) {
38
+ const fileArgs = composeFiles.flatMap(({ path }) => ['--file', path]);
39
+ return snapshotExecutionStep({
40
+ id: `compose-topology:${input.projectName}`,
41
+ argv: [input.docker.canonicalPath, 'compose', '--project-directory', composeProjectDirectory(input.root, input.references), ...fileArgs,
42
+ '--project-name', input.projectName, 'config', '--format', 'json', '--no-interpolate', '--no-env-resolution'],
43
+ argv0: input.docker.invocationPath,
44
+ cwd: input.root,
45
+ env: Object.fromEntries(Object.entries(input.env).filter((entry) => DOCKER_ENVIRONMENT.has(entry[0]) && typeof entry[1] === 'string')),
46
+ timeoutMs: TIMEOUT_MS,
47
+ stdoutLimitBytes: OUTPUT_LIMIT_BYTES,
48
+ stderrLimitBytes: STDERR_LIMIT_BYTES,
49
+ paths: composeFiles.map(({ path }) => path),
50
+ ports: [],
51
+ labels: { operation: 'compose-topology' },
52
+ expected: [],
53
+ consumedFiles: [
54
+ { path: input.docker.canonicalPath, sha256: input.docker.sha256, maxBytes: 536_870_912,
55
+ argvIndexes: [0], executable: true, executionMode: input.docker.executionMode },
56
+ ...composeFiles.map(({ path, sha256 }, index) => ({
57
+ path, sha256, maxBytes: OUTPUT_LIMIT_BYTES, argvIndexes: [5 + index * 2], executable: false,
58
+ })),
59
+ ],
60
+ filesystemChecks: [],
61
+ compensation: [],
62
+ });
63
+ }
64
+ function record(value) {
65
+ return value !== null && typeof value === 'object' && !Array.isArray(value) ? value : undefined;
66
+ }
67
+ /**
68
+ * Splits a Compose short-syntax entry (`[ip:]host:container[/proto]`, `source:target[:mode]`) on
69
+ * its colons. Variable references and bracketed IPv6 addresses are kept whole so their colons do
70
+ * not split the entry; an unbalanced one returns `undefined`.
71
+ */
72
+ export function splitComposeShort(value) {
73
+ const segments = [];
74
+ let current = '';
75
+ let depth = 0;
76
+ let bracket = false;
77
+ for (let index = 0; index < value.length; index += 1) {
78
+ const character = value[index];
79
+ if (character === '$' && value[index + 1] === '{')
80
+ depth += 1;
81
+ else if (character === '}' && depth > 0)
82
+ depth -= 1;
83
+ else if (character === '[' && depth === 0)
84
+ bracket = true;
85
+ else if (character === ']' && depth === 0)
86
+ bracket = false;
87
+ if (character === ':' && depth === 0 && !bracket) {
88
+ segments.push(current);
89
+ current = '';
90
+ }
91
+ else
92
+ current += character;
93
+ }
94
+ segments.push(current);
95
+ return depth !== 0 || bracket ? undefined : segments;
96
+ }
97
+ // Compose keeps a port it could not interpolate in its short form, and normalises literal ports
98
+ // to the long form. Both are read here.
99
+ function shortPort(value) {
100
+ const split = splitComposeShort(value);
101
+ if (!split)
102
+ return undefined;
103
+ const segments = [...split.slice(0, -1), split[split.length - 1].replace(/\/(?:tcp|udp|sctp)$/, '')];
104
+ if (segments.length > 3 || segments.some((segment, index) => segment === '' && index === segments.length - 1))
105
+ return undefined;
106
+ if (segments.length === 1)
107
+ return { published: null, hostIp: null, target: segments[0] };
108
+ const published = segments[segments.length - 2];
109
+ const hostIp = segments.length === 3 && segments[0] !== '' ? segments[0] : null;
110
+ return { published: published === '' ? null : published, hostIp, target: segments[segments.length - 1] };
111
+ }
112
+ function longPort(value) {
113
+ const target = value.target;
114
+ const published = value.published;
115
+ const hostIp = value.host_ip;
116
+ if (typeof target !== 'number' && typeof target !== 'string')
117
+ return undefined;
118
+ if (hostIp !== undefined && hostIp !== null && typeof hostIp !== 'string')
119
+ return undefined;
120
+ const ip = hostIp === undefined || hostIp === null || hostIp === '' ? null : hostIp;
121
+ if (published === undefined || published === null || published === '')
122
+ return { published: null, hostIp: ip, target: String(target) };
123
+ if (typeof published !== 'number' && typeof published !== 'string')
124
+ return undefined;
125
+ return { published: String(published), hostIp: ip, target: String(target) };
126
+ }
127
+ /** A service's raw `ports` entries, short or long form, or undefined when one cannot be read. */
128
+ export function composePublishedPorts(rawPorts = []) {
129
+ if (!Array.isArray(rawPorts))
130
+ return undefined;
131
+ const ports = rawPorts.map(port => typeof port === 'string' ? shortPort(port) : record(port) ? longPort(record(port)) : undefined);
132
+ return ports.every(port => port !== undefined) ? ports : undefined;
133
+ }
134
+ function projectServices(stdout) {
135
+ let parsed;
136
+ try {
137
+ parsed = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(stdout));
138
+ }
139
+ catch {
140
+ return undefined;
141
+ } // The caller refuses output that is not UTF-8 JSON; parser text would quote it.
142
+ const services = record(record(parsed)?.services);
143
+ if (!services)
144
+ return undefined;
145
+ const names = Object.keys(services).sort();
146
+ if (names.length > SERVICE_LIMIT || names.some(name => !SERVICE_ID.test(name)))
147
+ return undefined;
148
+ const projected = [];
149
+ for (const id of names) {
150
+ const service = record(services[id]);
151
+ if (!service)
152
+ return undefined;
153
+ const ports = composePublishedPorts(service.ports);
154
+ if (!ports)
155
+ return undefined;
156
+ const health = service.healthcheck === undefined ? undefined : record(service.healthcheck);
157
+ if (service.healthcheck !== undefined && !health)
158
+ return undefined;
159
+ const test = health?.test;
160
+ const healthcheck = health !== undefined && health.disable !== true
161
+ && !(Array.isArray(test) && test[0] === 'NONE');
162
+ const image = service.image;
163
+ if (image !== undefined && typeof image !== 'string')
164
+ return undefined;
165
+ const containerName = service.container_name;
166
+ if (containerName !== undefined && typeof containerName !== 'string')
167
+ return undefined;
168
+ const profiles = service.profiles ?? [];
169
+ if (!Array.isArray(profiles) || profiles.some(profile => typeof profile !== 'string'))
170
+ return undefined;
171
+ const rawVolumes = service.volumes ?? [];
172
+ if (!Array.isArray(rawVolumes))
173
+ return undefined;
174
+ const mountSources = [];
175
+ for (const volume of rawVolumes) {
176
+ const source = record(volume)?.source;
177
+ if (source === undefined)
178
+ continue;
179
+ if (typeof source !== 'string')
180
+ return undefined;
181
+ mountSources.push(source);
182
+ }
183
+ projected.push({
184
+ id, ports, healthcheck,
185
+ image: image ?? null, build: service.build !== undefined, containerName: containerName ?? null, mountSources,
186
+ ...profiles.length > 0 ? { profiles: profiles } : {},
187
+ });
188
+ }
189
+ return projected;
190
+ }
191
+ /** Runs Compose's own resolution of the declared files and projects the services it reports. */
192
+ export async function observeComposeTopology(input, execute) {
193
+ if (input.references.length === 0)
194
+ return unavailable('no Compose file is declared');
195
+ const reads = await readAnchoredBatches(input.root, input.references.map(path => ({
196
+ path, maxBytes: OUTPUT_LIMIT_BYTES, action: 'read-file',
197
+ })), input.node, { execute });
198
+ const composeFiles = reads?.kind === 'complete' && reads.results.length === input.references.length
199
+ ? reads.results.map((read, index) => read.kind === 'complete'
200
+ ? { path: resolve(input.root, input.references[index]), sha256: read.sha256 } : undefined)
201
+ : undefined;
202
+ if (!composeFiles || composeFiles.some(file => file === undefined)) {
203
+ return unavailable(`a declared Compose file (${input.references.join(', ')}) is missing, larger than 1 MiB, or crosses a symbolic link`);
204
+ }
205
+ const step = topologyStep(input, composeFiles);
206
+ if (!step)
207
+ return unavailable('the Compose topology probe could not be bounded');
208
+ const outcome = authenticatedExecutionOutcome(await execute(step), step);
209
+ if (!outcome)
210
+ return unavailable('the Compose topology probe returned unauthenticated evidence');
211
+ if (outcome.kind !== 'complete') {
212
+ const detail = redactSensitiveText(new TextDecoder().decode(outcome.stderr).trim().slice(0, 512));
213
+ return unavailable(`docker compose config failed (${outcome.code})${detail ? `: ${detail}` : ''}`);
214
+ }
215
+ const services = projectServices(outcome.stdout);
216
+ return services ? { kind: 'complete', services } : unavailable('docker compose config returned a topology this probe cannot read');
217
+ }
218
+ export function variableName(value) {
219
+ return /^\$\{([A-Za-z_][A-Za-z0-9_]*)(?:[:]?[-?+][^}]*)?\}$/.exec(value)?.[1] ?? /^\$([A-Za-z_][A-Za-z0-9_]*)$/.exec(value)?.[1];
220
+ }
221
+ function suggestedVariable(service) {
222
+ return `${service.toUpperCase().replace(/[^A-Z0-9]+/g, '_')}_HOST_PORT`;
223
+ }
224
+ const LEADING_VARIABLE = /^\$(?:\{([A-Za-z_][A-Za-z0-9_]*)(?:(:?[-?+])([^}]*))?\}|([A-Za-z_][A-Za-z0-9_]*))/;
225
+ /**
226
+ * Where a volume's host side lands, read from its source as written or as Compose reports it
227
+ * uninterpolated; a relative path resolves against `directory`, the Compose project directory.
228
+ * `variables` are the values cockpit hands Compose (the sibling directories); any other variable
229
+ * resolves to its own default, as it does in `up`, or cannot be resolved at all. Returns the
230
+ * absolute host path of a bind, `'volume'` for a named volume, and `undefined` when the host side
231
+ * cannot be known.
232
+ */
233
+ export function composeBindPath(source, directory, variables) {
234
+ // `config --no-interpolate` still resolves paths, so it joins a long-syntax source that starts
235
+ // with a variable to the project directory as if it were relative: `${API_DIR}` comes back as
236
+ // `<project>/${API_DIR}`. Interpolated, an absolute value replaces that directory and a relative
237
+ // one stays inside it, which is what resolving the variable form against `directory` does.
238
+ const written = directory !== sep && source.startsWith(`${directory}${sep}$`) ? source.slice(directory.length + 1) : source;
239
+ let text = written;
240
+ const lead = LEADING_VARIABLE.exec(written);
241
+ if (lead) {
242
+ const name = (lead[1] ?? lead[4]);
243
+ const operator = lead[2];
244
+ const base = Object.hasOwn(variables, name) && operator !== '+' && operator !== ':+' ? variables[name]
245
+ : operator === '-' || operator === ':-' ? lead[3] : undefined;
246
+ if (base === undefined)
247
+ return undefined;
248
+ text = base + written.slice(lead[0].length);
249
+ }
250
+ if (text.includes('$'))
251
+ return undefined;
252
+ if (isAbsolute(text))
253
+ return resolve(text);
254
+ if (text === '.' || text === '..' || text.startsWith('./') || text.startsWith('../'))
255
+ return resolve(directory, text);
256
+ // `~` is the invoking user's home, which cockpit never hands a container; anything else is a volume name.
257
+ return text.startsWith('~') ? undefined : 'volume';
258
+ }
259
+ function contains(parent, path) {
260
+ return path === parent || path.startsWith(parent.endsWith(sep) ? parent : `${parent}${sep}`);
261
+ }
262
+ /** The deepest declared mount source holding `bind`, or `undefined` when none does. */
263
+ export function bindHolder(bind, declared) {
264
+ return declared.filter(source => contains(source, bind)).sort((left, right) => right.length - left.length)[0];
265
+ }
266
+ /**
267
+ * Pairs each bind with the deepest declared mount source holding it. A bind of a directory inside
268
+ * a mount = "always" checkout (`./api` of the repository) mounts that checkout, so it counts for
269
+ * it; a bind inside no declared source is undeclared. A declared source is mounted when a bind
270
+ * lands in it or when an admitted bind holds it: a generated host path inside a checkout bound
271
+ * whole reaches the container through that bind, and nothing it is declared for (reset deleting
272
+ * it, the ignore block, the browser writing there) needs a mount of its own. Any other declared
273
+ * source is missing. Both `check` and `up` judge their binds with this one rule.
274
+ */
275
+ export function compareBindSources(binds, declared) {
276
+ const used = new Set();
277
+ const admitted = [];
278
+ const unexpected = [];
279
+ for (const bind of new Set(binds)) {
280
+ const owner = bindHolder(bind, declared);
281
+ if (owner === undefined)
282
+ unexpected.push(bind);
283
+ else {
284
+ used.add(owner);
285
+ admitted.push(bind);
286
+ }
287
+ }
288
+ return {
289
+ missing: [...new Set(declared)]
290
+ .filter(source => !used.has(source) && !admitted.some(bind => contains(bind, source))).sort(),
291
+ unexpected: unexpected.sort(),
292
+ };
293
+ }
294
+ async function canonicalPath(path) {
295
+ try {
296
+ return await realpath(path);
297
+ }
298
+ catch (error) {
299
+ // Compose creates a missing bind source, so the nearest existing ancestor decides where it lands.
300
+ if (!hasErrorCode(error, 'ENOENT') || dirname(path) === path)
301
+ throw error;
302
+ return join(await canonicalPath(dirname(path)), basename(path));
303
+ }
304
+ }
305
+ /**
306
+ * The binds inside a declared source whose real path leaves it. Containment above is lexical, and
307
+ * Docker follows a symbolic link in the bind source, so `./api/cache -> /` would otherwise hand
308
+ * the host root to a container under a mount = "always" checkout.
309
+ */
310
+ export async function redirectedBindSources(binds, declared, resolve = canonicalPath) {
311
+ const redirected = [];
312
+ for (const bind of new Set(binds)) {
313
+ if (declared.includes(bind))
314
+ continue;
315
+ const owner = bindHolder(bind, declared);
316
+ if (owner === undefined)
317
+ continue;
318
+ let inside;
319
+ try {
320
+ inside = contains(await resolve(owner), await resolve(bind));
321
+ }
322
+ catch {
323
+ inside = false;
324
+ } // A bind that cannot be resolved cannot be shown to sit inside its holder, so it is reported redirected and refused.
325
+ if (!inside)
326
+ redirected.push({ bind, holder: owner });
327
+ }
328
+ return redirected.sort((left, right) => left.bind.localeCompare(right.bind));
329
+ }
330
+ function profileOf(sibling) {
331
+ return sibling.mount.startsWith('profile:') ? sibling.mount.slice('profile:'.length) : undefined;
332
+ }
333
+ // Image references that differ per stack: Compose's own project name, which cockpit derives per
334
+ // checkout, and the leased ports.bases variables.
335
+ function imageVariesPerStack(image, leased) {
336
+ return [...image.matchAll(/\$\{?([A-Za-z_][A-Za-z0-9_]*)/g)].some(([, name]) => name === 'COMPOSE_PROJECT_NAME' || leased.has(name));
337
+ }
338
+ // The bind findings of the services one profile set runs, judged as `up` judges them under that set.
339
+ async function assessBinds(services, declarations, variables, active, resolve) {
340
+ const diagnostics = [];
341
+ const running = services.filter(service => !service.profiles?.length || service.profiles.some(profile => active.includes(profile)));
342
+ const mounted = declarations.siblings.filter(sibling => sibling.mount === 'always'
343
+ || profileOf(sibling) !== undefined && active.includes(profileOf(sibling)));
344
+ const declared = [...mounted.map(sibling => sibling.path), ...declarations.hostPaths.map(({ path }) => path)];
345
+ const binds = running.flatMap(service => service.mountSources.map(source => ({
346
+ service: service.id, source, path: composeBindPath(source, declarations.projectDirectory, variables)
347
+ })))
348
+ .filter(bind => bind.path !== 'volume');
349
+ const resolved = binds.flatMap(bind => bind.path === undefined ? [] : [bind.path]);
350
+ const { missing, unexpected } = compareBindSources(resolved, declared);
351
+ const redirected = (await redirectedBindSources(resolved, declared, resolve)).map(({ bind }) => bind);
352
+ const under = active.length === 0 ? '' : ` with profile ${active.join(', ')} active`;
353
+ for (const sibling of mounted) {
354
+ if (!missing.includes(sibling.path))
355
+ continue;
356
+ diagnostics.push({
357
+ severity: 'error', code: 'COMPOSE_SIBLING_NOT_MOUNTED', target: `sibling:${sibling.slug}`,
358
+ message: `sibling ${sibling.slug} is declared mount = "${sibling.mount}", but no Compose service${under} bind-mounts ${sibling.path} or a directory inside it; up admits a stack only when every such sibling is mounted, so it would refuse to start`,
359
+ fix: `bind-mount it in a service of the Compose file, whole or a directory inside it (for example "\${${sibling.var}}:/workspace/${sibling.slug}"), or set mount = "none" on sibling ${sibling.slug} in cockpit.toml, then run ${cockpitCommand('sync')}`,
360
+ });
361
+ }
362
+ for (const hostPath of declarations.hostPaths) {
363
+ if (!missing.includes(hostPath.path))
364
+ continue;
365
+ diagnostics.push({
366
+ severity: 'error', code: 'GENERATED_HOST_PATH_UNMOUNTED',
367
+ target: `environment.generated_host_paths:${hostPath.declared}`,
368
+ message: `environment.generated_host_paths lists ${hostPath.declared}, but no Compose service bind-mounts ${hostPath.path}, a path inside it or a directory that holds it; up admits a stack only when every declared host path is mounted, so it would refuse to start`,
369
+ fix: `bind-mount ${hostPath.declared} in a service of the Compose file, or remove "${hostPath.declared}" from environment.generated_host_paths in cockpit.toml; then run ${cockpitCommand('check')}`,
370
+ });
371
+ }
372
+ for (const bind of binds) {
373
+ const why = bind.path === undefined
374
+ ? 'whose host side names a variable cockpit does not supply and gives it no default'
375
+ : unexpected.includes(bind.path) ? `which lies inside no mounted sibling${under} and is no environment.generated_host_paths entry`
376
+ : redirected.includes(bind.path) ? 'which resolves through a symbolic link to a path outside the sibling that holds it' : undefined;
377
+ if (why === undefined)
378
+ continue;
379
+ diagnostics.push({
380
+ severity: 'error', code: 'COMPOSE_BIND_UNDECLARED', target: `compose:${bind.service}:${bind.source}`,
381
+ message: `service ${bind.service} bind-mounts ${bind.path ?? bind.source}, ${why}; up hands a container only the checkouts and host paths cockpit.toml declares, so it would refuse to start`,
382
+ fix: `bind a directory inside a mounted sibling (its variable, as in "\${VAR}/path", or a path relative to the first Compose file's directory), list the path under environment.generated_host_paths, or remove the bind mount from service ${bind.service}; then run ${cockpitCommand('check')}`,
383
+ });
384
+ }
385
+ return diagnostics;
386
+ }
387
+ /**
388
+ * Judges a cockpit's own Compose topology. A host port that is not drawn from a leased
389
+ * `ports.bases` variable is the same number in every worktree and parallel stack, so the second
390
+ * stack to start fails to bind it; a fixed `container_name` fails the same way, and a fixed tag on a
391
+ * built image is shared the same way. The remaining findings are warnings about what cockpit can
392
+ * then promise, not about whether the stack runs.
393
+ */
394
+ export async function assessComposeTopology(services, declarations) {
395
+ const leased = new Set(declarations.portVariables);
396
+ const diagnostics = [];
397
+ const defined = new Set(services.flatMap(service => service.profiles ?? []));
398
+ const defaults = declarations.profiles ?? [];
399
+ for (const [profile, source] of [
400
+ ...defaults.map(profile => [profile, 'environment.profiles names it']),
401
+ ...declarations.siblings.flatMap(sibling => profileOf(sibling) === undefined ? []
402
+ : [[profileOf(sibling), `sibling ${sibling.slug} is mounted under it`]]),
403
+ ]) {
404
+ if (defined.has(profile))
405
+ continue;
406
+ diagnostics.push({
407
+ severity: 'error', code: 'COMPOSE_PROFILE_UNDEFINED',
408
+ target: `profile:${profile}`,
409
+ message: `profile ${profile} is declared by no service in environment.compose_files, and ${source}; Compose accepts an unknown profile silently, so activating it would start nothing`,
410
+ fix: `add profiles: [${profile}] to the services it should start, or remove it from cockpit.toml; then run ${cockpitCommand('check')}`,
411
+ });
412
+ }
413
+ // `up` admits the topology only when every bind of an active service lands inside a mounted
414
+ // checkout or a generated host path, and every such checkout is mounted; check judges the same
415
+ // binds the same way, or a configuration check accepts refuses every runtime command. A stack
416
+ // runs the default profile set plus any profiles `up --profile` adds, so the binds are judged
417
+ // under the default set and under the default set plus each other defined profile: a finding
418
+ // under several added profiles already shows under one of them.
419
+ const usable = defaults.filter(profile => defined.has(profile));
420
+ const sets = [usable, ...[...defined].filter(profile => !usable.includes(profile)).sort().map(profile => [...usable, profile])];
421
+ const variables = Object.fromEntries(declarations.siblings.map(sibling => [sibling.var, sibling.path]));
422
+ // The same binds recur in most profile sets, so each real path is resolved once.
423
+ const resolutions = new Map();
424
+ const resolve = (path) => {
425
+ let resolution = resolutions.get(path);
426
+ if (resolution === undefined) {
427
+ resolution = canonicalPath(path);
428
+ resolutions.set(path, resolution);
429
+ }
430
+ return resolution;
431
+ };
432
+ const seen = new Set();
433
+ for (const active of sets) {
434
+ for (const diagnostic of await assessBinds(services, declarations, variables, active, resolve)) {
435
+ const key = `${diagnostic.code}\0${diagnostic.target}`;
436
+ if (!seen.has(key)) {
437
+ seen.add(key);
438
+ diagnostics.push(diagnostic);
439
+ }
440
+ }
441
+ }
442
+ for (const service of services) {
443
+ for (const port of service.ports) {
444
+ // No host interface means Docker binds every one (0.0.0.0 and ::), exposing the service
445
+ // to the network. Writing an interface, even 0.0.0.0, is the declared way to mean it.
446
+ if (port.hostIp === null)
447
+ diagnostics.push({
448
+ severity: 'warning', code: 'COMPOSE_PORT_ALL_INTERFACES', target: `compose:${service.id}:${port.target}`,
449
+ message: `service ${service.id} publishes container port ${port.target} with no host interface, so Docker binds every interface and the service is reachable from the network`,
450
+ fix: `write the host interface in front of the port, as "127.0.0.1:\${${(port.published === null ? undefined : variableName(port.published)) ?? suggestedVariable(service.id)}}:${port.target}", or write 0.0.0.0 there if you mean the wider exposure`,
451
+ });
452
+ if (port.published === null)
453
+ continue;
454
+ const variable = variableName(port.published);
455
+ if (variable !== undefined && leased.has(variable))
456
+ continue;
457
+ const name = variable ?? suggestedVariable(service.id);
458
+ diagnostics.push({
459
+ severity: 'error', code: 'COMPOSE_PORT_NOT_LEASED', target: `compose:${service.id}:${port.target}`,
460
+ message: `service ${service.id} publishes host port ${JSON.stringify(port.published)} for container port ${port.target}, which is not taken from a ports.bases variable; the same host port collides across worktrees and parallel stacks`,
461
+ fix: `publish it as "\${${name}}:${port.target}" in the Compose file and declare ${name} under [ports.bases]`,
462
+ });
463
+ }
464
+ if (service.containerName !== null)
465
+ diagnostics.push({
466
+ severity: 'error', code: 'COMPOSE_CONTAINER_NAME_FIXED', target: `compose:${service.id}`,
467
+ message: `service ${service.id} sets container_name ${JSON.stringify(service.containerName)}, and Docker allows one container of a name, so a second checkout's stack fails to start`,
468
+ fix: `remove container_name from service ${service.id} and address it by its Compose service name, or with docker compose exec ${service.id}`,
469
+ });
470
+ if (!service.healthcheck)
471
+ diagnostics.push({
472
+ severity: 'warning', code: 'COMPOSE_HEALTHCHECK_MISSING', target: `compose:${service.id}`,
473
+ message: `service ${service.id} declares no Compose healthcheck, so running is treated as ready`,
474
+ fix: `add a healthcheck to service ${service.id} that succeeds only once it serves requests`,
475
+ });
476
+ if (!service.build && service.image !== null && !PINNED_IMAGE.test(service.image))
477
+ diagnostics.push({
478
+ severity: 'warning', code: 'COMPOSE_IMAGE_UNPINNED', target: `compose:${service.id}`,
479
+ message: `service ${service.id} runs image ${JSON.stringify(service.image)} by tag, so a later pull can change what runs`,
480
+ fix: `pin it by digest, for example ${service.image.replace(/:[^/:]*$/, '')}@sha256:<digest> from docker image inspect`,
481
+ });
482
+ if (service.build && service.image !== null && !imageVariesPerStack(service.image, leased))
483
+ diagnostics.push({
484
+ severity: 'error', code: 'COMPOSE_IMAGE_TAG_SHARED', target: `compose:${service.id}`,
485
+ message: `service ${service.id} builds its image and tags it ${JSON.stringify(service.image)}, which is the same in every worktree and parallel stack, so one checkout's build replaces the image another checkout's stack runs`,
486
+ fix: `remove image from service ${service.id}, so Compose names the build after this stack's project`,
487
+ });
488
+ }
489
+ return diagnostics;
490
+ }