@b00dea/cockpit 0.0.0-bootstrap.0 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (363) hide show
  1. package/CHANGELOG.md +375 -0
  2. package/LICENSE +21 -0
  3. package/README.md +21 -1
  4. package/bin/cockpit.mjs +33 -0
  5. package/cockpit.schema.json +2574 -0
  6. package/dist/autonomy.d.ts +44 -0
  7. package/dist/autonomy.js +156 -0
  8. package/dist/bounded-read.d.ts +14 -0
  9. package/dist/bounded-read.js +42 -0
  10. package/dist/browser/commands.d.ts +62 -0
  11. package/dist/browser/commands.js +349 -0
  12. package/dist/browser/package-paths.d.ts +1 -0
  13. package/dist/browser/package-paths.js +6 -0
  14. package/dist/browser/proxy.d.ts +22 -0
  15. package/dist/browser/proxy.js +37 -0
  16. package/dist/browser/runtime.d.ts +117 -0
  17. package/dist/browser/runtime.js +183 -0
  18. package/dist/browser/surfaces.d.ts +141 -0
  19. package/dist/browser/surfaces.js +556 -0
  20. package/dist/browser/ui-gate.d.ts +15 -0
  21. package/dist/browser/ui-gate.js +18 -0
  22. package/dist/cli-guard.d.ts +5 -0
  23. package/dist/cli-guard.js +71 -0
  24. package/dist/cli-main.d.ts +139 -0
  25. package/dist/cli-main.js +1405 -0
  26. package/dist/cli.d.ts +3 -0
  27. package/dist/cli.js +16 -0
  28. package/dist/command-table.d.ts +113 -0
  29. package/dist/command-table.js +512 -0
  30. package/dist/config/load.d.ts +171 -0
  31. package/dist/config/load.js +345 -0
  32. package/dist/config/sibling-toml.d.ts +48 -0
  33. package/dist/config/sibling-toml.js +160 -0
  34. package/dist/config/surface-tools-toml.d.ts +9 -0
  35. package/dist/config/surface-tools-toml.js +37 -0
  36. package/dist/config/verified-write.d.ts +43 -0
  37. package/dist/config/verified-write.js +89 -0
  38. package/dist/diagnostics.d.ts +26 -0
  39. package/dist/diagnostics.js +335 -0
  40. package/dist/emit.d.ts +22 -0
  41. package/dist/emit.js +43 -0
  42. package/dist/entrypoint-kinds.d.ts +3 -0
  43. package/dist/entrypoint-kinds.js +11 -0
  44. package/dist/environment/application/destructive.d.ts +6 -0
  45. package/dist/environment/application/destructive.js +425 -0
  46. package/dist/environment/application/doctor.d.ts +89 -0
  47. package/dist/environment/application/doctor.js +235 -0
  48. package/dist/environment/application/guard.d.ts +43 -0
  49. package/dist/environment/application/guard.js +622 -0
  50. package/dist/environment/application/health.d.ts +134 -0
  51. package/dist/environment/application/health.js +379 -0
  52. package/dist/environment/application/lifecycle-context.d.ts +228 -0
  53. package/dist/environment/application/lifecycle-context.js +154 -0
  54. package/dist/environment/application/lifecycle-state.d.ts +25 -0
  55. package/dist/environment/application/lifecycle-state.js +201 -0
  56. package/dist/environment/application/lifecycle.d.ts +17 -0
  57. package/dist/environment/application/lifecycle.js +729 -0
  58. package/dist/environment/application/logs.d.ts +45 -0
  59. package/dist/environment/application/logs.js +127 -0
  60. package/dist/environment/application/setup.d.ts +129 -0
  61. package/dist/environment/application/setup.js +741 -0
  62. package/dist/environment/application/shared.d.ts +11 -0
  63. package/dist/environment/application/shared.js +61 -0
  64. package/dist/environment/claims.d.ts +389 -0
  65. package/dist/environment/claims.js +1709 -0
  66. package/dist/environment/codex-hook-trust.d.ts +11 -0
  67. package/dist/environment/codex-hook-trust.js +185 -0
  68. package/dist/environment/compose-environment.d.ts +1 -0
  69. package/dist/environment/compose-environment.js +6 -0
  70. package/dist/environment/compose-ownership.d.ts +21 -0
  71. package/dist/environment/compose-ownership.js +117 -0
  72. package/dist/environment/compose-topology.d.ts +117 -0
  73. package/dist/environment/compose-topology.js +413 -0
  74. package/dist/environment/contract.d.ts +489 -0
  75. package/dist/environment/contract.js +761 -0
  76. package/dist/environment/declaration.d.ts +14 -0
  77. package/dist/environment/declaration.js +39 -0
  78. package/dist/environment/destructive-composition.d.ts +29 -0
  79. package/dist/environment/destructive-composition.js +333 -0
  80. package/dist/environment/docker-daemon.d.ts +30 -0
  81. package/dist/environment/docker-daemon.js +78 -0
  82. package/dist/environment/docker-endpoint.d.ts +10 -0
  83. package/dist/environment/docker-endpoint.js +19 -0
  84. package/dist/environment/env-schema-observation.d.ts +46 -0
  85. package/dist/environment/env-schema-observation.js +241 -0
  86. package/dist/environment/evidence.d.ts +136 -0
  87. package/dist/environment/evidence.js +437 -0
  88. package/dist/environment/executable.d.ts +26 -0
  89. package/dist/environment/executable.js +167 -0
  90. package/dist/environment/generated-application/authorities.d.ts +5 -0
  91. package/dist/environment/generated-application/authorities.js +61 -0
  92. package/dist/environment/generated-application/bootstrap-skill.d.ts +16 -0
  93. package/dist/environment/generated-application/bootstrap-skill.js +328 -0
  94. package/dist/environment/generated-application/check.d.ts +13 -0
  95. package/dist/environment/generated-application/check.js +189 -0
  96. package/dist/environment/generated-application/custom-surface.d.ts +26 -0
  97. package/dist/environment/generated-application/custom-surface.js +289 -0
  98. package/dist/environment/generated-application/prepare.d.ts +29 -0
  99. package/dist/environment/generated-application/prepare.js +276 -0
  100. package/dist/environment/generated-application/publication.d.ts +33 -0
  101. package/dist/environment/generated-application/publication.js +547 -0
  102. package/dist/environment/generated-application/shared.d.ts +484 -0
  103. package/dist/environment/generated-application/shared.js +349 -0
  104. package/dist/environment/generated-application/surface-observation.d.ts +4 -0
  105. package/dist/environment/generated-application/surface-observation.js +69 -0
  106. package/dist/environment/generated-application/surface-outputs.d.ts +71 -0
  107. package/dist/environment/generated-application/surface-outputs.js +389 -0
  108. package/dist/environment/generated-application/surfaces.d.ts +3 -0
  109. package/dist/environment/generated-application/surfaces.js +699 -0
  110. package/dist/environment/generated-application/sync.d.ts +6 -0
  111. package/dist/environment/generated-application/sync.js +109 -0
  112. package/dist/environment/git-observation.d.ts +183 -0
  113. package/dist/environment/git-observation.js +544 -0
  114. package/dist/environment/health-composition.d.ts +11 -0
  115. package/dist/environment/health-composition.js +628 -0
  116. package/dist/environment/host-observation.d.ts +56 -0
  117. package/dist/environment/host-observation.js +52 -0
  118. package/dist/environment/identity.d.ts +48 -0
  119. package/dist/environment/identity.js +85 -0
  120. package/dist/environment/init-application.d.ts +308 -0
  121. package/dist/environment/init-application.js +697 -0
  122. package/dist/environment/install-policy-observation.d.ts +71 -0
  123. package/dist/environment/install-policy-observation.js +284 -0
  124. package/dist/environment/install-policy.d.ts +48 -0
  125. package/dist/environment/install-policy.js +96 -0
  126. package/dist/environment/instance.d.ts +33 -0
  127. package/dist/environment/instance.js +122 -0
  128. package/dist/environment/instruction-pointer.d.ts +19 -0
  129. package/dist/environment/instruction-pointer.js +144 -0
  130. package/dist/environment/lifecycle-composition.d.ts +29 -0
  131. package/dist/environment/lifecycle-composition.js +675 -0
  132. package/dist/environment/lifecycle-stage.d.ts +115 -0
  133. package/dist/environment/lifecycle-stage.js +398 -0
  134. package/dist/environment/lifecycle.d.ts +75 -0
  135. package/dist/environment/lifecycle.js +354 -0
  136. package/dist/environment/logs.d.ts +132 -0
  137. package/dist/environment/logs.js +519 -0
  138. package/dist/environment/observations.d.ts +46 -0
  139. package/dist/environment/observations.js +344 -0
  140. package/dist/environment/operator-application.d.ts +50 -0
  141. package/dist/environment/operator-application.js +155 -0
  142. package/dist/environment/ports.d.ts +182 -0
  143. package/dist/environment/ports.js +1205 -0
  144. package/dist/environment/posture-observation.d.ts +68 -0
  145. package/dist/environment/posture-observation.js +253 -0
  146. package/dist/environment/read-composition.d.ts +27 -0
  147. package/dist/environment/read-composition.js +352 -0
  148. package/dist/environment/readiness-composition.d.ts +38 -0
  149. package/dist/environment/readiness-composition.js +130 -0
  150. package/dist/environment/reclaim.d.ts +333 -0
  151. package/dist/environment/reclaim.js +1835 -0
  152. package/dist/environment/repository-identity.d.ts +13 -0
  153. package/dist/environment/repository-identity.js +108 -0
  154. package/dist/environment/runtime-measurement-record.d.ts +38 -0
  155. package/dist/environment/runtime-measurement-record.js +111 -0
  156. package/dist/environment/runtime-measurement.d.ts +27 -0
  157. package/dist/environment/runtime-measurement.js +224 -0
  158. package/dist/environment/runtime-probe-residue.d.ts +43 -0
  159. package/dist/environment/runtime-probe-residue.js +148 -0
  160. package/dist/environment/runtime-probe.d.ts +72 -0
  161. package/dist/environment/runtime-probe.js +725 -0
  162. package/dist/environment/runtime.d.ts +505 -0
  163. package/dist/environment/runtime.js +2335 -0
  164. package/dist/environment/seed.d.ts +16 -0
  165. package/dist/environment/seed.js +43 -0
  166. package/dist/environment/selftest-application.d.ts +86 -0
  167. package/dist/environment/selftest-application.js +713 -0
  168. package/dist/environment/sensitive.d.ts +3 -0
  169. package/dist/environment/sensitive.js +48 -0
  170. package/dist/environment/setup-observation.d.ts +62 -0
  171. package/dist/environment/setup-observation.js +1473 -0
  172. package/dist/environment/setup.d.ts +43 -0
  173. package/dist/environment/setup.js +176 -0
  174. package/dist/environment/sibling-instructions.d.ts +33 -0
  175. package/dist/environment/sibling-instructions.js +224 -0
  176. package/dist/environment/sibling-registration.d.ts +45 -0
  177. package/dist/environment/sibling-registration.js +538 -0
  178. package/dist/environment/state.d.ts +371 -0
  179. package/dist/environment/state.js +2013 -0
  180. package/dist/environment/templates.d.ts +140 -0
  181. package/dist/environment/templates.js +685 -0
  182. package/dist/environment/tool-registration.d.ts +9 -0
  183. package/dist/environment/tool-registration.js +96 -0
  184. package/dist/environment/upgrade-application.d.ts +19 -0
  185. package/dist/environment/upgrade-application.js +175 -0
  186. package/dist/environment-exec/execute.d.ts +102 -0
  187. package/dist/environment-exec/execute.js +1175 -0
  188. package/dist/environment-exec/libexec-program.d.ts +6 -0
  189. package/dist/environment-exec/libexec-program.js +22 -0
  190. package/dist/environment-exec/verified-digest.d.ts +4 -0
  191. package/dist/environment-exec/verified-digest.js +16 -0
  192. package/dist/exact-keys.d.ts +7 -0
  193. package/dist/exact-keys.js +15 -0
  194. package/dist/fix-catalogue.d.ts +32 -0
  195. package/dist/fix-catalogue.js +60 -0
  196. package/dist/freshness.d.ts +29 -0
  197. package/dist/freshness.js +75 -0
  198. package/dist/generated-environment/adopt.d.ts +16 -0
  199. package/dist/generated-environment/adopt.js +275 -0
  200. package/dist/generated-environment/command.d.ts +47 -0
  201. package/dist/generated-environment/command.js +75 -0
  202. package/dist/generated-environment/transaction.d.ts +186 -0
  203. package/dist/generated-environment/transaction.js +1770 -0
  204. package/dist/grading/evaluate.d.ts +71 -0
  205. package/dist/grading/evaluate.js +403 -0
  206. package/dist/grading/grade.d.ts +33 -0
  207. package/dist/grading/grade.js +57 -0
  208. package/dist/guards/evaluator.d.ts +113 -0
  209. package/dist/guards/evaluator.js +1226 -0
  210. package/dist/guards/harness-adapter.d.ts +17 -0
  211. package/dist/guards/harness-adapter.js +57 -0
  212. package/dist/guards/host.d.ts +8 -0
  213. package/dist/guards/host.js +10 -0
  214. package/dist/guards/invocation-parser.d.ts +66 -0
  215. package/dist/guards/invocation-parser.js +595 -0
  216. package/dist/guards/local-identity.d.ts +40 -0
  217. package/dist/guards/local-identity.js +123 -0
  218. package/dist/guards/policy-pin.d.ts +13 -0
  219. package/dist/guards/policy-pin.js +175 -0
  220. package/dist/guards/protected-locations.d.ts +62 -0
  221. package/dist/guards/protected-locations.js +461 -0
  222. package/dist/guards/read-only.d.ts +37 -0
  223. package/dist/guards/read-only.js +315 -0
  224. package/dist/init/command.d.ts +40 -0
  225. package/dist/init/command.js +310 -0
  226. package/dist/init/derive.d.ts +51 -0
  227. package/dist/init/derive.js +715 -0
  228. package/dist/init/draft.d.ts +138 -0
  229. package/dist/init/draft.js +1205 -0
  230. package/dist/init/render.d.ts +35 -0
  231. package/dist/init/render.js +82 -0
  232. package/dist/init/scaffold.d.ts +53 -0
  233. package/dist/init/scaffold.js +121 -0
  234. package/dist/invocation.d.ts +13 -0
  235. package/dist/invocation.js +20 -0
  236. package/dist/package-metadata.d.ts +9 -0
  237. package/dist/package-metadata.js +19 -0
  238. package/dist/readiness.d.ts +53 -0
  239. package/dist/readiness.js +88 -0
  240. package/dist/references/named.d.ts +24 -0
  241. package/dist/references/named.js +27 -0
  242. package/dist/refusal.d.ts +15 -0
  243. package/dist/refusal.js +7 -0
  244. package/dist/registry-derivation.d.ts +31 -0
  245. package/dist/registry-derivation.js +32 -0
  246. package/dist/registry-resolve/command.d.ts +47 -0
  247. package/dist/registry-resolve/command.js +226 -0
  248. package/dist/registry-resolve/registry.d.ts +68 -0
  249. package/dist/registry-resolve/registry.js +292 -0
  250. package/dist/registry-resolve/render-shell.d.ts +18 -0
  251. package/dist/registry-resolve/render-shell.js +36 -0
  252. package/dist/repair.d.ts +29 -0
  253. package/dist/repair.js +8 -0
  254. package/dist/schema-json.d.ts +1 -0
  255. package/dist/schema-json.js +177 -0
  256. package/dist/schema.d.ts +1322 -0
  257. package/dist/schema.js +934 -0
  258. package/dist/setup/command.d.ts +4 -0
  259. package/dist/setup/command.js +4 -0
  260. package/dist/sha256.d.ts +1 -0
  261. package/dist/sha256.js +4 -0
  262. package/dist/surfaces/adoption.d.ts +41 -0
  263. package/dist/surfaces/adoption.js +217 -0
  264. package/dist/surfaces/consumer-instructions.d.ts +20 -0
  265. package/dist/surfaces/consumer-instructions.js +43 -0
  266. package/dist/surfaces/contract.d.ts +42 -0
  267. package/dist/surfaces/contract.js +169 -0
  268. package/dist/surfaces/core-declarations.d.ts +24 -0
  269. package/dist/surfaces/core-declarations.js +79 -0
  270. package/dist/surfaces/development-markers.d.ts +1 -0
  271. package/dist/surfaces/development-markers.js +18 -0
  272. package/dist/surfaces/generator-adapter.d.ts +71 -0
  273. package/dist/surfaces/generator-adapter.js +809 -0
  274. package/dist/surfaces/harness-registry.d.ts +180 -0
  275. package/dist/surfaces/harness-registry.js +117 -0
  276. package/dist/surfaces/mcp.d.ts +95 -0
  277. package/dist/surfaces/mcp.js +186 -0
  278. package/dist/surfaces/merge.d.ts +30 -0
  279. package/dist/surfaces/merge.js +105 -0
  280. package/dist/surfaces/ownership.d.ts +190 -0
  281. package/dist/surfaces/ownership.js +795 -0
  282. package/dist/surfaces/projection.d.ts +136 -0
  283. package/dist/surfaces/projection.js +681 -0
  284. package/dist/surfaces/render.d.ts +118 -0
  285. package/dist/surfaces/render.js +633 -0
  286. package/dist/surfaces/restrictions.d.ts +130 -0
  287. package/dist/surfaces/restrictions.js +447 -0
  288. package/dist/surfaces/selftest.d.ts +162 -0
  289. package/dist/surfaces/selftest.js +393 -0
  290. package/dist/surfaces/skill-contract.d.ts +85 -0
  291. package/dist/surfaces/skill-contract.js +449 -0
  292. package/dist/surfaces/sync.d.ts +57 -0
  293. package/dist/surfaces/sync.js +164 -0
  294. package/dist/team-metadata.d.ts +41 -0
  295. package/dist/team-metadata.js +512 -0
  296. package/dist/testing/gates.d.ts +91 -0
  297. package/dist/testing/gates.js +182 -0
  298. package/dist/testing/hooks.d.ts +59 -0
  299. package/dist/testing/hooks.js +139 -0
  300. package/dist/testing/verdict.d.ts +29 -0
  301. package/dist/testing/verdict.js +62 -0
  302. package/dist/work-orders/application.d.ts +99 -0
  303. package/dist/work-orders/application.js +383 -0
  304. package/dist/work-orders/composition.d.ts +124 -0
  305. package/dist/work-orders/composition.js +1010 -0
  306. package/dist/work-orders/contract.d.ts +61 -0
  307. package/dist/work-orders/contract.js +122 -0
  308. package/dist/work-orders/store.d.ts +158 -0
  309. package/dist/work-orders/store.js +197 -0
  310. package/docs/README.md +65 -0
  311. package/docs/architecture.md +136 -0
  312. package/docs/capability-matrix.md +50 -0
  313. package/docs/cli-reference.md +599 -0
  314. package/docs/command-list.md +51 -0
  315. package/docs/compose-patterns.md +99 -0
  316. package/docs/concepts.md +171 -0
  317. package/docs/configuration-essentials.md +172 -0
  318. package/docs/configuration-reference.md +621 -0
  319. package/docs/environment.md +760 -0
  320. package/docs/extending.md +43 -0
  321. package/docs/faq.md +111 -0
  322. package/docs/getting-started.md +608 -0
  323. package/docs/harness-surfaces.md +433 -0
  324. package/docs/joining-a-team.md +76 -0
  325. package/docs/release-policy.md +49 -0
  326. package/docs/safety-and-autonomy.md +419 -0
  327. package/docs/troubleshooting.md +361 -0
  328. package/docs/tutorial.md +170 -0
  329. package/docs/work-and-browser.md +254 -0
  330. package/libexec/browser-operation.cjs +186 -0
  331. package/libexec/fence-marker.cjs +101 -0
  332. package/libexec/fence-marker.d.cts +24 -0
  333. package/libexec/fenced-group-runner.cjs +77 -0
  334. package/libexec/fenced-supervisor.cjs +137 -0
  335. package/libexec/generated-filesystem.cjs +731 -0
  336. package/libexec/install-filesystem.cjs +225 -0
  337. package/libexec/rulesync-generator.NOTICE.md +1112 -0
  338. package/libexec/rulesync-generator.cjs +743 -0
  339. package/libexec/surface-generator-worker.cjs +266 -0
  340. package/libexec/surface-output-reader.cjs +99 -0
  341. package/libexec/surface-scratch-cleanup.cjs +14 -0
  342. package/libexec/validate-harness-evidence.d.mts +183 -0
  343. package/libexec/validate-harness-evidence.mjs +1312 -0
  344. package/package.json +76 -1
  345. package/surfaces/README.md +33 -0
  346. package/surfaces/agents/evaluator.agent.md +76 -0
  347. package/surfaces/guards/bash-guard.sh +48 -0
  348. package/surfaces/guards/claude.adapter.json +25 -0
  349. package/surfaces/guards/codex.adapter.json +24 -0
  350. package/surfaces/guards/opencode.adapter.json +25 -0
  351. package/surfaces/references/browser.md +63 -0
  352. package/surfaces/references/test-data-and-personas.md +84 -0
  353. package/surfaces/references/testing-tiers.md +11 -0
  354. package/surfaces/skills/cockpit-browser.skill.md +54 -0
  355. package/surfaces/skills/cockpit-explain.skill.md +32 -0
  356. package/surfaces/skills/cockpit-fix.skill.md +37 -0
  357. package/surfaces/skills/cockpit-setup.skill.md +39 -0
  358. package/surfaces/skills/cockpit-work.skill.md +34 -0
  359. package/surfaces/skills/collection.json +5 -0
  360. package/surfaces/skills/procedures/explain.md +61 -0
  361. package/surfaces/skills/procedures/fix.md +81 -0
  362. package/surfaces/skills/procedures/setup.md +379 -0
  363. package/surfaces/skills/procedures/work.md +105 -0
@@ -0,0 +1,11 @@
1
+ import type { EnvironmentDiagnostic } from './contract.js';
2
+ export interface CodexHookTrustInput {
3
+ readonly root: string;
4
+ readonly env: Readonly<Record<string, string | undefined>>;
5
+ }
6
+ /**
7
+ * A warning when Codex will skip the generated guard hook on this machine, or when that cannot be
8
+ * verified; nothing when every hook is trusted, no Codex hook is published, or Codex has never run
9
+ * as this user.
10
+ */
11
+ export declare function inspectCodexHookTrust(input: CodexHookTrustInput): Promise<readonly EnvironmentDiagnostic[]>;
@@ -0,0 +1,185 @@
1
+ // Owns the read-only check of whether Codex will run cockpit's generated guard hook on this machine.
2
+ // Codex 0.165.0 skips a project hook until a person trusts it, and says nothing about the skip: a
3
+ // measured `codex exec` ran a push the guard refuses once the hook was trusted. Codex records trust
4
+ // per user as `[hooks.state."<hooks.json path>:<event>:<group>:<handler>"] trusted_hash`, the SHA-256
5
+ // of a normalized hook identity, so this module recomputes that identity from the published
6
+ // hooks.json and compares. It never writes Codex's configuration and never reports a value from it;
7
+ // anything it cannot read or reproduce is reported as unverified, not as trusted.
8
+ import { createHash } from 'node:crypto';
9
+ import { readFile, realpath, stat } from 'node:fs/promises';
10
+ import { join, resolve } from 'node:path';
11
+ import { parse as parseToml, TomlError } from 'smol-toml';
12
+ import { RefusalError, tryParseJson, errorCause, hasErrorCode } from './contract.js';
13
+ const HOOKS_PATH = '.codex/hooks.json';
14
+ const MAX_FILE_BYTES = 4 * 1024 * 1024;
15
+ // Codex's event keys. Events it runs without a matcher drop the matcher from the identity; the
16
+ // session-end and interrupt events normalize their timeout differently and are left unverified.
17
+ const EVENT_KEYS = Object.freeze({
18
+ PreToolUse: 'pre_tool_use', PermissionRequest: 'permission_request', PostToolUse: 'post_tool_use',
19
+ PreCompact: 'pre_compact', PostCompact: 'post_compact', SessionStart: 'session_start',
20
+ SubagentStart: 'subagent_start', SubagentStop: 'subagent_stop', UserPromptSubmit: 'user_prompt_submit', Stop: 'stop',
21
+ });
22
+ const MATCHERLESS = new Set(['user_prompt_submit', 'stop']);
23
+ const HANDLER_KEYS = new Set(['type', 'command', 'commandWindows', 'timeout', 'async', 'statusMessage']);
24
+ function record(value) {
25
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
26
+ }
27
+ function sorted(value) {
28
+ if (Array.isArray(value))
29
+ return value.map(sorted);
30
+ if (!record(value))
31
+ return value;
32
+ return Object.fromEntries(Object.keys(value).sort().map(key => [key, sorted(value[key])]));
33
+ }
34
+ /** The hooks Codex discovers in a hooks.json, keyed and hashed as Codex 0.165.0 does, or why not. */
35
+ function codexHooks(bytes) {
36
+ const parsed = tryParseJson(bytes);
37
+ if (parsed === undefined)
38
+ return 'its hooks.json is not JSON';
39
+ if (!record(parsed) || !record(parsed.hooks))
40
+ return 'its hooks.json has no hooks object';
41
+ const hooks = [];
42
+ for (const [event, groups] of Object.entries(parsed.hooks)) {
43
+ const key = EVENT_KEYS[event];
44
+ if (!key)
45
+ return `its hooks.json declares the ${event} event, whose trust identity cockpit does not reproduce`;
46
+ if (!Array.isArray(groups))
47
+ return `its ${event} entry is not a list`;
48
+ for (const [groupIndex, group] of groups.entries()) {
49
+ if (!record(group) || !Array.isArray(group.hooks) || group.matcher !== undefined && typeof group.matcher !== 'string') {
50
+ return `its ${event} group ${groupIndex} is malformed`;
51
+ }
52
+ for (const [handlerIndex, handler] of group.hooks.entries()) {
53
+ if (!record(handler) || handler.type !== 'command' || typeof handler.command !== 'string'
54
+ || Object.keys(handler).some(name => !HANDLER_KEYS.has(name))
55
+ || handler.timeout !== undefined && !Number.isSafeInteger(handler.timeout)
56
+ || handler.async !== undefined && typeof handler.async !== 'boolean'
57
+ || handler.statusMessage !== undefined && typeof handler.statusMessage !== 'string') {
58
+ return `its ${event} hook ${groupIndex}:${handlerIndex} has a shape whose trust identity cockpit does not reproduce`;
59
+ }
60
+ // Codex hashes the handler after normalizing it: the timeout defaults to ten minutes and
61
+ // the Windows command is dropped on other systems.
62
+ const normalized = {
63
+ type: 'command', command: handler.command, timeout: Math.max(1, handler.timeout ?? 600),
64
+ async: handler.async ?? false, ...(handler.statusMessage === undefined ? {} : { statusMessage: handler.statusMessage }),
65
+ };
66
+ const identity = { event_name: key, hooks: [normalized],
67
+ ...(group.matcher === undefined || MATCHERLESS.has(key) ? {} : { matcher: group.matcher }) };
68
+ hooks.push({ suffix: `${key}:${groupIndex}:${handlerIndex}`,
69
+ hash: `sha256:${createHash('sha256').update(JSON.stringify(sorted(identity))).digest('hex')}` });
70
+ }
71
+ }
72
+ }
73
+ return hooks;
74
+ }
75
+ async function bounded(path) {
76
+ try {
77
+ const bytes = await readFile(path);
78
+ if (bytes.byteLength > MAX_FILE_BYTES)
79
+ throw new RefusalError(`${path} exceeds ${MAX_FILE_BYTES} bytes`);
80
+ return bytes.toString('utf8');
81
+ }
82
+ catch (error) {
83
+ if (hasErrorCode(error, 'ENOENT'))
84
+ return 'missing';
85
+ throw error;
86
+ }
87
+ }
88
+ function trustFor(states, source, hooks) {
89
+ let trust = 'trusted';
90
+ for (const hook of hooks) {
91
+ const state = states[`${source}:${hook.suffix}`];
92
+ if (!record(state) || state.trusted_hash === undefined)
93
+ return 'untrusted';
94
+ if (state.enabled === false)
95
+ trust = 'disabled';
96
+ else if (state.trusted_hash !== hook.hash && trust === 'trusted')
97
+ trust = 'modified';
98
+ }
99
+ return trust;
100
+ }
101
+ function warning(message) {
102
+ return {
103
+ severity: 'warning',
104
+ // Code and fix are literals, so the diagnostic inventory captures the fix beside the code and
105
+ // the troubleshooting index repeats it. The message names the hook's path, and the printed
106
+ // fix binds the configuration placeholder to the resolved path.
107
+ code: 'CODEX_HOOK_UNTRUSTED',
108
+ message,
109
+ target: HOOKS_PATH,
110
+ fix: 'if you use Codex in this cockpit, a person runs codex -C <cockpit root> in a terminal (not codex exec), trusts the folder if asked, and chooses "Trust all and continue" when Codex shows "Hooks need review" (or enables and trusts the hook in its /hooks view); then run npx --no cockpit doctor --config <your cockpit.toml>',
111
+ };
112
+ }
113
+ /**
114
+ * A warning when Codex will skip the generated guard hook on this machine, or when that cannot be
115
+ * verified; nothing when every hook is trusted, no Codex hook is published, or Codex has never run
116
+ * as this user.
117
+ */
118
+ export async function inspectCodexHookTrust(input) {
119
+ const root = resolve(input.root);
120
+ const hooksPath = join(root, HOOKS_PATH);
121
+ const unverified = (reason) => [warning(`cockpit could not verify whether Codex trusts the guard hook in ${hooksPath} (${reason}); Codex skips a project hook until a person trusts it, and then runs tool calls without the guard`)];
122
+ let hooksBytes;
123
+ try {
124
+ hooksBytes = await bounded(hooksPath);
125
+ }
126
+ catch (error) {
127
+ return unverified(`reading it failed: ${errorCause(error)}`);
128
+ }
129
+ if (hooksBytes === 'missing')
130
+ return [];
131
+ const hooks = codexHooks(hooksBytes);
132
+ if (typeof hooks === 'string')
133
+ return unverified(hooks);
134
+ if (hooks.length === 0)
135
+ return [];
136
+ const home = input.env.CODEX_HOME || (input.env.HOME ? join(input.env.HOME, '.codex') : undefined);
137
+ if (!home)
138
+ return unverified('neither CODEX_HOME nor HOME is set, so Codex\'s configuration cannot be found');
139
+ // Every cockpit publishes the Codex hook, so a person who never runs Codex here saw this warning on
140
+ // every check. Codex creates its home on first run; without one no Codex session can skip the hook.
141
+ try {
142
+ await stat(home);
143
+ }
144
+ catch (error) {
145
+ if (hasErrorCode(error, 'ENOENT'))
146
+ return [];
147
+ return unverified(`reading ${home} failed: ${errorCause(error)}`);
148
+ }
149
+ const codexConfig = join(home, 'config.toml');
150
+ let states = {};
151
+ try {
152
+ const bytes = await bounded(codexConfig);
153
+ if (bytes !== 'missing') {
154
+ const parsed = parseToml(bytes);
155
+ const recorded = record(parsed.hooks) ? parsed.hooks.state : undefined;
156
+ if (recorded !== undefined && !record(recorded))
157
+ return unverified(`hooks.state in ${codexConfig} is not a table`);
158
+ states = recorded ?? {};
159
+ }
160
+ }
161
+ catch (error) {
162
+ // smol-toml quotes the lines around a syntax error, and Codex's configuration can hold
163
+ // credentials, so only the position is reported.
164
+ if (error instanceof TomlError)
165
+ return unverified(`${codexConfig} is not valid TOML at line ${error.line}, column ${error.column}`);
166
+ return unverified(`reading ${codexConfig} failed: ${errorCause(error)}`);
167
+ }
168
+ // Codex keys trust by the hooks.json path as the session directory spells it, which may or may
169
+ // not resolve symbolic links, so trust recorded under either spelling counts.
170
+ let canonical = root;
171
+ try {
172
+ canonical = await realpath(root);
173
+ }
174
+ catch (error) {
175
+ return unverified(`resolving ${root} failed: ${errorCause(error)}`);
176
+ }
177
+ const results = [...new Set([hooksPath, join(canonical, HOOKS_PATH)])].map(source => trustFor(states, source, hooks));
178
+ if (results.includes('trusted'))
179
+ return [];
180
+ const consequence = 'a Codex session started there skips it and runs tool calls without the guard';
181
+ const message = results.includes('disabled') ? `the guard hook in ${hooksPath} is disabled in ${codexConfig}, so ${consequence}`
182
+ : results.includes('modified') ? `Codex trusted an earlier version of the guard hook in ${hooksPath}; until a person trusts the current one, ${consequence}`
183
+ : `Codex has not trusted the guard hook in ${hooksPath} on this machine, so ${consequence}`;
184
+ return [warning(message)];
185
+ }
@@ -0,0 +1 @@
1
+ export declare function composeProcessEnvironment(env: Readonly<Record<string, string>>): Readonly<Record<string, string>>;
@@ -0,0 +1,6 @@
1
+ // Compose process environments are completed here after their owning boundary has rejected
2
+ // caller-supplied COMPOSE_* values. The fixed value prevents unauthenticated project .env bytes
3
+ // from changing an authenticated plan while leaving service env_file loading available at runtime.
4
+ export function composeProcessEnvironment(env) {
5
+ return { ...env, COMPOSE_DISABLE_ENV_FILE: '1' };
6
+ }
@@ -0,0 +1,21 @@
1
+ /** Published beside cockpit.toml; the cockpit's own Compose files are passed first. */
2
+ export declare const COMPOSE_OWNERSHIP_FILE = "compose.cockpit.yml";
3
+ export type ComposeOwnershipRender = Readonly<{
4
+ kind: 'complete';
5
+ bytes: string;
6
+ }> | Readonly<{
7
+ kind: 'refusal';
8
+ reason: string;
9
+ target: string;
10
+ }>;
11
+ export declare function renderComposeOwnership(input: Readonly<{
12
+ sources: readonly Readonly<{
13
+ reference: string;
14
+ source: string;
15
+ }>[];
16
+ stackId: string;
17
+ cockpitId: string;
18
+ configSha256: string;
19
+ /** `environment.resettable_volumes`: each must name a volume these files create. */
20
+ resettableVolumes: readonly string[];
21
+ }>): ComposeOwnershipRender;
@@ -0,0 +1,117 @@
1
+ // Renders the Compose override that gives a compose-first cockpit's own services, the images they
2
+ // build, default network and named volumes the ownership labels runtime admission and reclaim read.
3
+ // The cockpit's Compose files stay untouched: `sync` publishes this override as an owned file and
4
+ // `check` drift-checks it like any other, and runtime readiness passes it as the last `--file` only
5
+ // when its bytes equal a fresh render of the current Compose files and identity. It parses YAML only to list service and
6
+ // volume names; what Compose resolves from the files belongs to compose-topology.ts.
7
+ import { createHash } from 'node:crypto';
8
+ import { parseDocument, stringify } from 'yaml';
9
+ /** Published beside cockpit.toml; the cockpit's own Compose files are passed first. */
10
+ export const COMPOSE_OWNERSHIP_FILE = 'compose.cockpit.yml';
11
+ const SERVICE_ID = /^[a-z0-9][a-z0-9_.-]*$/;
12
+ const VOLUME_ID = /^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/;
13
+ const SOURCE_LIMIT_BYTES = 1_048_576;
14
+ export function renderComposeOwnership(input) {
15
+ const services = new Set();
16
+ const built = new Set();
17
+ // Each owned volume and the labels its own files already declare, as KEY -> value.
18
+ const volumes = new Map();
19
+ const external = new Set();
20
+ for (const { reference, source } of input.sources) {
21
+ const refuse = (reason) => ({ kind: 'refusal', reason, target: reference });
22
+ if (Buffer.byteLength(source) > SOURCE_LIMIT_BYTES)
23
+ return refuse('the Compose file exceeds 1 MiB');
24
+ const document = parseDocument(source, { merge: true });
25
+ if (document.errors.length > 0)
26
+ return refuse(`the Compose file is not valid YAML: ${document.errors[0].message.split('\n', 1)[0]}`);
27
+ const root = document.toJS({ maxAliasCount: 100 });
28
+ if (root === null || typeof root !== 'object' || Array.isArray(root))
29
+ return refuse('the Compose file is not a mapping');
30
+ const record = root;
31
+ // An included file declares services this file cannot name, so they would run unlabelled, and
32
+ // Compose reads it from disk rather than from the snapshot cockpit verified.
33
+ if (record.include !== undefined)
34
+ return refuse('`include` is not supported, because Compose reads an included file outside the bytes cockpit verifies and labels; list the file in environment.compose_files instead, where relative paths in every listed file resolve against the first file\'s directory');
35
+ if (record.volumes !== undefined) {
36
+ if (record.volumes === null || typeof record.volumes !== 'object' || Array.isArray(record.volumes))
37
+ return refuse('`volumes` is not a mapping');
38
+ for (const [id, declaration] of Object.entries(record.volumes)) {
39
+ if (!VOLUME_ID.test(id))
40
+ return refuse(`volume name ${JSON.stringify(id)} is not a Compose volume identifier`);
41
+ // An external volume is created outside this stack, and Compose refuses labels on one.
42
+ const record = declaration !== null && typeof declaration === 'object' && !Array.isArray(declaration)
43
+ ? declaration : undefined;
44
+ if (record?.external) {
45
+ external.add(id);
46
+ continue;
47
+ }
48
+ const declared = volumes.get(id) ?? new Map();
49
+ const labels = record?.labels;
50
+ if (Array.isArray(labels)) {
51
+ for (const entry of labels)
52
+ if (typeof entry === 'string' && entry.includes('='))
53
+ declared.set(entry.slice(0, entry.indexOf('=')), entry.slice(entry.indexOf('=') + 1));
54
+ }
55
+ else if (labels !== null && typeof labels === 'object') {
56
+ for (const [key, value] of Object.entries(labels))
57
+ declared.set(key, String(value));
58
+ }
59
+ volumes.set(id, declared);
60
+ }
61
+ }
62
+ if (record.services === undefined)
63
+ continue;
64
+ if (record.services === null || typeof record.services !== 'object' || Array.isArray(record.services))
65
+ return refuse('`services` is not a mapping');
66
+ for (const [id, declaration] of Object.entries(record.services)) {
67
+ if (!SERVICE_ID.test(id))
68
+ return refuse(`service name ${JSON.stringify(id)} is not a Compose service identifier`);
69
+ services.add(id);
70
+ if (declaration !== null && typeof declaration === 'object' && !Array.isArray(declaration)
71
+ && declaration.build !== undefined)
72
+ built.add(id);
73
+ }
74
+ }
75
+ // Reset matches a declared name against the Compose volume key, so a name these files do not
76
+ // create would reset nothing while reading as a control.
77
+ for (const name of input.resettableVolumes) {
78
+ if (volumes.has(name))
79
+ continue;
80
+ const declared = [...volumes.keys()].sort();
81
+ return {
82
+ kind: 'refusal',
83
+ target: 'environment.resettable_volumes',
84
+ reason: external.has(name)
85
+ ? `resettable volume ${JSON.stringify(name)} is external: Compose does not create it, so cockpit cannot label it as this stack's own and reset could never remove it`
86
+ : `resettable volume ${JSON.stringify(name)} is not a top-level volume in environment.compose_files${declared.length === 0 ? '' : ` (declared: ${declared.join(', ')})`}`,
87
+ };
88
+ }
89
+ const labels = {
90
+ 'dev.b0dea.cockpit.config-sha256': input.configSha256,
91
+ 'dev.b0dea.cockpit.owner-id': '${COCKPIT_OWNER_ID:?cockpit runtime owner identity is unavailable}',
92
+ 'dev.b0dea.cockpit.stack-id': '${COCKPIT_STACK_ID:?cockpit runtime stack identity is unavailable}',
93
+ };
94
+ const generation = createHash('sha256').update(`compose-volume-generation\0${input.configSha256}`).digest('hex');
95
+ // Compose 5.5 refuses to merge a volume label the earlier file already sets to the same value
96
+ // ("labels must be a mapping"), so a label a volume already carries is not repeated, and a
97
+ // volume that carries them all needs no entry. Services and networks merge them fine.
98
+ const volumeLabels = [...volumes].sort(([left], [right]) => left.localeCompare(right)).flatMap(([id, declared]) => {
99
+ const missing = Object.entries({ ...labels, 'dev.b0dea.cockpit.generation-id': generation })
100
+ .filter(([key, value]) => declared.get(key) !== value);
101
+ return missing.length === 0 ? [] : [[id, { labels: Object.fromEntries(missing) }]];
102
+ });
103
+ const body = stringify({
104
+ // A built image carries the set as well, or `down` left every image the stack built. Only a
105
+ // service that declares `build` gets one: a build section on a pulled image makes Compose build it.
106
+ services: Object.fromEntries([...services].sort().map((id) => [id, built.has(id) ? { labels, build: { labels } } : { labels }])),
107
+ // The default network carries the same set, so `down` can remove the network it created.
108
+ networks: { default: { labels } },
109
+ // Reset admits a data-bearing volume only with exact ownership and a generation;
110
+ // unlabelled, a declared resettable volume was unreachable.
111
+ ...(volumeLabels.length === 0 ? {} : { volumes: Object.fromEntries(volumeLabels) }),
112
+ }, { aliasDuplicateObjects: false });
113
+ return {
114
+ kind: 'complete',
115
+ bytes: `# Generated by cockpit sync from environment.compose_files: ownership labels only. Do not edit.\n${body}`,
116
+ };
117
+ }
@@ -0,0 +1,117 @@
1
+ import { type ExecuteProbeOutcome } from '../environment-exec/execute.js';
2
+ import { type Diagnostic, type ExecutionStep } from './contract.js';
3
+ import type { ResolvedExecutable } from './executable.js';
4
+ /**
5
+ * The directory every relative path in a cockpit's own Compose files resolves against: the first
6
+ * file's, as `docker compose -f first.yml -f second.yml` takes it. Compose cannot default it itself
7
+ * because it reads snapshots of the files, and the cockpit root is the wrong default: a product
8
+ * repository's Compose file names `./api` relative to the repository.
9
+ */
10
+ export declare function composeProjectDirectory(root: string, references: readonly string[]): string;
11
+ export interface ComposeTopologyInput {
12
+ /** Canonical absolute cockpit root. */
13
+ readonly root: string;
14
+ /** The declared `environment.compose_files`, relative to `root`. */
15
+ readonly references: readonly string[];
16
+ readonly projectName: string;
17
+ readonly docker: ResolvedExecutable;
18
+ /** Reads the declared files through the anchored reader, so the probe consumes authenticated bytes. */
19
+ readonly node: ResolvedExecutable;
20
+ /** Filtered to the Docker connection variables before it reaches the probe. */
21
+ readonly env: Readonly<Record<string, string | undefined>>;
22
+ }
23
+ export interface ComposePublishedPort {
24
+ /** The host side exactly as written, `null` when Compose picks an ephemeral host port. */
25
+ readonly published: string | null;
26
+ /** The host interface exactly as written, `null` when none is given, so Docker binds every interface. */
27
+ readonly hostIp: string | null;
28
+ readonly target: string;
29
+ }
30
+ export interface ComposeServiceTopology {
31
+ readonly id: string;
32
+ readonly ports: readonly ComposePublishedPort[];
33
+ readonly healthcheck: boolean;
34
+ readonly image: string | null;
35
+ readonly build: boolean;
36
+ /**
37
+ * Each volume source as Compose reports it without interpolation: the absolute host path of a
38
+ * bind, or the raw `${VAR}` text of a source still waiting for its variable.
39
+ */
40
+ readonly mountSources: readonly string[];
41
+ }
42
+ /** A `[[sibling]]` row as the mount detector needs it: its absolute checkout path and variable. */
43
+ export interface ComposeSiblingMount {
44
+ readonly slug: string;
45
+ readonly var: string;
46
+ readonly mount: string;
47
+ readonly path: string;
48
+ }
49
+ export type ComposeTopologyObservation = Readonly<{
50
+ kind: 'complete';
51
+ services: readonly ComposeServiceTopology[];
52
+ }> | Readonly<{
53
+ kind: 'unavailable';
54
+ reason: string;
55
+ }>;
56
+ /**
57
+ * Splits a Compose short-syntax entry (`[ip:]host:container[/proto]`, `source:target[:mode]`) on
58
+ * its colons. Variable references and bracketed IPv6 addresses are kept whole so their colons do
59
+ * not split the entry; an unbalanced one returns `undefined`.
60
+ */
61
+ export declare function splitComposeShort(value: string): readonly string[] | undefined;
62
+ /** A service's raw `ports` entries, short or long form, or undefined when one cannot be read. */
63
+ export declare function composePublishedPorts(rawPorts?: unknown): readonly ComposePublishedPort[] | undefined;
64
+ /** Runs Compose's own resolution of the declared files and projects the services it reports. */
65
+ export declare function observeComposeTopology(input: ComposeTopologyInput, execute: (step: ExecutionStep) => Promise<ExecuteProbeOutcome>): Promise<ComposeTopologyObservation>;
66
+ export declare function variableName(value: string): string | undefined;
67
+ /**
68
+ * Where a volume's host side lands, read from its source as written or as Compose reports it
69
+ * uninterpolated; a relative path resolves against `directory`, the Compose project directory.
70
+ * `variables` are the values cockpit hands Compose (the sibling directories); any other variable
71
+ * resolves to its own default, as it does in `up`, or cannot be resolved at all. Returns the
72
+ * absolute host path of a bind, `'volume'` for a named volume, and `undefined` when the host side
73
+ * cannot be known.
74
+ */
75
+ export declare function composeBindPath(source: string, directory: string, variables: Readonly<Record<string, string>>): string | 'volume' | undefined;
76
+ /** The deepest declared mount source holding `bind`, or `undefined` when none does. */
77
+ export declare function bindHolder(bind: string, declared: readonly string[]): string | undefined;
78
+ /**
79
+ * Pairs each bind with the deepest declared mount source holding it. A bind of a directory inside
80
+ * a mount = "always" checkout (`./api` of the repository) mounts that checkout, so it counts for
81
+ * it; a bind inside no declared source is undeclared. A declared source is mounted when a bind
82
+ * lands in it or when an admitted bind holds it: a generated host path inside a checkout bound
83
+ * whole reaches the container through that bind, and nothing it is declared for (reset deleting
84
+ * it, the ignore block, the browser writing there) needs a mount of its own. Any other declared
85
+ * source is missing. Both `check` and `up` judge their binds with this one rule.
86
+ */
87
+ export declare function compareBindSources(binds: readonly string[], declared: readonly string[]): Readonly<{
88
+ missing: readonly string[];
89
+ unexpected: readonly string[];
90
+ }>;
91
+ /**
92
+ * The binds inside a declared source whose real path leaves it. Containment above is lexical, and
93
+ * Docker follows a symbolic link in the bind source, so `./api/cache -> /` would otherwise hand
94
+ * the host root to a container under a mount = "always" checkout.
95
+ */
96
+ export declare function redirectedBindSources(binds: readonly string[], declared: readonly string[]): Promise<readonly Readonly<{
97
+ bind: string;
98
+ holder: string;
99
+ }>[]>;
100
+ export interface ComposeTopologyDeclarations {
101
+ /** From composeProjectDirectory; a relative bind source, or a variable's relative default, resolves against it. */
102
+ readonly projectDirectory: string;
103
+ readonly portVariables: readonly string[];
104
+ readonly siblings: readonly ComposeSiblingMount[];
105
+ /** The `environment.generated_host_paths`, as declared and absolute, which `up` hands to containers beside the checkouts. */
106
+ readonly hostPaths: readonly Readonly<{
107
+ declared: string;
108
+ path: string;
109
+ }>[];
110
+ }
111
+ /**
112
+ * Judges a cockpit's own Compose topology. A host port that is not drawn from a leased
113
+ * `ports.bases` variable is the same number in every worktree and parallel stack, so the second
114
+ * stack to start fails to bind it; the remaining findings are warnings about what cockpit can
115
+ * then promise, not about whether the stack runs.
116
+ */
117
+ export declare function assessComposeTopology(services: readonly ComposeServiceTopology[], declarations: ComposeTopologyDeclarations): Promise<readonly Diagnostic[]>;