@b00dea/cockpit 0.0.0-stage → 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 +22 -2
  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 +74 -4
  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,519 @@
1
+ import { randomBytes } from 'node:crypto';
2
+ import { isAbsolute, normalize } from 'node:path';
3
+ import { admitLocalDockerEndpoint } from './docker-endpoint.js';
4
+ import { composeProjectDirectory } from './compose-topology.js';
5
+ import { authenticatedExecutionOutcome } from '../environment-exec/execute.js';
6
+ import { ENVIRONMENT_CODES, MAX_STEP_TIMEOUT_MS, snapshotExecutionStep, snapshotJson, RefusalError, errorCause } from './contract.js';
7
+ import { isRuntimeEvidenceAuthentic } from './evidence.js';
8
+ import { consumeRuntimeServiceFailureForLogs, consumeRuntimeServiceObservationForFailureLogs, createRuntimeAdapter, observedVersionCore } from './runtime.js';
9
+ import { redactSensitiveMessage } from './sensitive.js';
10
+ import { isResolvedDockerExecutable } from './executable.js';
11
+ import { inspectPortLeaseProjection } from './ports.js';
12
+ import { exactKeys } from '../exact-keys.js';
13
+ const SERVICE = /^[a-z0-9][a-z0-9_.-]*$/;
14
+ const RELATIVE_SINCE = /^\d+(?:\.\d+)?[smhd]$/;
15
+ const SHA256 = /^[a-f0-9]{64}$/;
16
+ const STACK_ID = /^stack:[a-f0-9]{64}$/;
17
+ const RFC3339 = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,9}))?(Z|([+-])(\d{2}):(\d{2}))$/;
18
+ const issuedLogPlans = new WeakSet();
19
+ function validInstant(value) {
20
+ const match = RFC3339.exec(value);
21
+ if (match === null)
22
+ return false;
23
+ const [year, month, day, hour, minute, second] = match.slice(1, 7).map(Number);
24
+ const calendar = new Date(`${match[1]}-${match[2]}-${match[3]}T${match[4]}:${match[5]}:${match[6]}Z`);
25
+ if (Number.isNaN(calendar.valueOf()) || calendar.getUTCFullYear() !== year || calendar.getUTCMonth() + 1 !== month
26
+ || calendar.getUTCDate() !== day || calendar.getUTCHours() !== hour || calendar.getUTCMinutes() !== minute
27
+ || calendar.getUTCSeconds() !== second)
28
+ return false;
29
+ if (match[8] !== 'Z') {
30
+ const offsetHour = Number(match[10]);
31
+ const offsetMinute = Number(match[11]);
32
+ if (offsetHour > 14 || offsetMinute > 59 || (offsetHour === 14 && offsetMinute !== 0))
33
+ return false;
34
+ }
35
+ return Number.isFinite(Date.parse(value));
36
+ }
37
+ function captureOwnValues(value, keys) {
38
+ if (value === null || typeof value !== 'object' || Array.isArray(value) || !exactKeys(value, keys))
39
+ return undefined;
40
+ const captured = {};
41
+ for (const key of keys)
42
+ captured[key] = value[key];
43
+ return captured;
44
+ }
45
+ function stringRecord(value) {
46
+ return value !== null && typeof value === 'object' && !Array.isArray(value)
47
+ && Object.entries(value).every(([name, item]) => name.length > 0 && typeof item === 'string');
48
+ }
49
+ function logRequest(value) {
50
+ if (value === null || typeof value !== 'object' || Array.isArray(value))
51
+ return undefined;
52
+ const input = value;
53
+ const keys = ['runtime', 'executable', 'cwd', 'stackId', 'configSha256', 'batchId', 'containerId', 'runtimeVersion', 'composeVersion',
54
+ 'projectName', 'composeFiles', 'services', 'service', 'since', 'tail', 'follow', 'timeoutMs', 'outputLimitBytes', 'env'];
55
+ if (!exactKeys(value, input.restartBatches === undefined ? keys : [...keys, 'restartBatches'])
56
+ || !isResolvedDockerExecutable(input.executable) || typeof input.runtime !== 'string' || typeof input.cwd !== 'string'
57
+ || typeof input.stackId !== 'string' || typeof input.configSha256 !== 'string' || typeof input.batchId !== 'string'
58
+ || typeof input.containerId !== 'string' || typeof input.runtimeVersion !== 'string' || typeof input.composeVersion !== 'string'
59
+ || typeof input.projectName !== 'string' || typeof input.service !== 'string' || typeof input.since !== 'string'
60
+ || typeof input.tail !== 'number' || typeof input.follow !== 'boolean' || typeof input.timeoutMs !== 'number'
61
+ || typeof input.outputLimitBytes !== 'number' || !stringRecord(input.env) || !Array.isArray(input.services)
62
+ || input.services.some((service) => typeof service !== 'string') || !Array.isArray(input.composeFiles)
63
+ || input.composeFiles.some((artifact) => artifact === null || typeof artifact !== 'object' || Array.isArray(artifact)
64
+ || !exactKeys(artifact, ['reference', 'canonicalPath', 'source', 'sha256'])
65
+ || Object.values(artifact).some((part) => typeof part !== 'string'))
66
+ || (input.restartBatches !== undefined && (!Array.isArray(input.restartBatches) || input.restartBatches.length !== 2
67
+ || input.restartBatches.some((batch) => typeof batch !== 'string'))))
68
+ return undefined;
69
+ return input;
70
+ }
71
+ // Logs must read the daemon the lifecycle started, so they accept the same local selection: a named
72
+ // context or a local Unix socket. TLS and remote endpoints stay refused.
73
+ function localDockerSelection(name, value) {
74
+ return name === 'DOCKER_HOST' ? admitLocalDockerEndpoint(value) !== undefined
75
+ : name === 'DOCKER_CONTEXT' && /^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,127}$/.test(value);
76
+ }
77
+ export function planLogs(rawInput, ownComposeAuthority) {
78
+ const input = logRequest(snapshotJson(rawInput));
79
+ if (input === undefined)
80
+ return { kind: 'unavailable', code: ENVIRONMENT_CODES.EVIDENCE_UNAVAILABLE, reason: 'log request must be readable JSON data' };
81
+ if (input.runtime !== 'docker-compose')
82
+ return { kind: 'unavailable', code: ENVIRONMENT_CODES.RUNTIME_UNAVAILABLE, reason: 'runtime does not expose a measured log adapter' };
83
+ if (!STACK_ID.test(input.stackId) || !SHA256.test(input.configSha256) || !SERVICE.test(input.projectName) || !SERVICE.test(input.service) || !input.services.includes(input.service) || input.services.some((service) => !SERVICE.test(service)) || !(RELATIVE_SINCE.test(input.since) || validInstant(input.since)) || !isAbsolute(input.cwd)
84
+ || !isResolvedDockerExecutable(input.executable)
85
+ || !SHA256.test(input.batchId) || !SERVICE.test(input.containerId) || observedVersionCore(input.runtimeVersion) === undefined || observedVersionCore(input.composeVersion) === undefined
86
+ || (input.restartBatches !== undefined && (input.restartBatches.length !== 2 || input.restartBatches.some((batch) => !SHA256.test(batch)) || input.restartBatches[0] === input.restartBatches[1]))
87
+ || !Number.isInteger(input.tail) || input.tail < 1 || input.tail > 10_000
88
+ || !Number.isInteger(input.timeoutMs) || input.timeoutMs <= 0 || input.timeoutMs > 300_000
89
+ || !Number.isInteger(input.outputLimitBytes) || input.outputLimitBytes <= 0 || input.outputLimitBytes > 16_777_216
90
+ || input.composeFiles.length === 0
91
+ || Object.entries(input.env).some(([name, value]) => name.startsWith('COMPOSE_') || name.includes('\0') || value.includes('\0')
92
+ || name.startsWith('DOCKER_') && !localDockerSelection(name, value))) {
93
+ return { kind: 'unavailable', code: ENVIRONMENT_CODES.EVIDENCE_UNAVAILABLE, reason: 'log request must be resolved and bounded' };
94
+ }
95
+ const suppliedPortProjection = ownComposeAuthority?.portLeaseProjection;
96
+ const portProjection = suppliedPortProjection === undefined ? undefined : inspectPortLeaseProjection(suppliedPortProjection);
97
+ if (suppliedPortProjection !== undefined && portProjection === undefined) {
98
+ return { kind: 'unavailable', code: ENVIRONMENT_CODES.EVIDENCE_UNAVAILABLE, reason: 'compose-first log port authority is malformed' };
99
+ }
100
+ const projectedPorts = portProjection ? Object.values(portProjection.lease.ports) : [];
101
+ const composeBoundary = createRuntimeAdapter({
102
+ runtime: { name: 'docker', version: input.runtimeVersion, executableSha256: input.executable.sha256 },
103
+ composeVersion: input.composeVersion, executable: input.executable, operation: 'validate', cwd: input.cwd,
104
+ composeFiles: input.composeFiles, projectName: input.projectName, env: input.env, ports: projectedPorts,
105
+ labels: {}, timeoutMs: input.timeoutMs,
106
+ }, ownComposeAuthority);
107
+ if (composeBoundary.kind === 'refusal')
108
+ return { kind: 'unavailable', code: ENVIRONMENT_CODES.EVIDENCE_UNAVAILABLE, reason: composeBoundary.diagnostics[0].message };
109
+ const argv = [input.executable.canonicalPath, 'container', 'logs', '--timestamps', '--since', input.since, '--tail', String(input.tail), ...(input.follow ? ['--follow'] : []), input.containerId];
110
+ const step = {
111
+ id: `runtime:logs:${input.projectName}:${input.service}`, argv: argv, argv0: input.executable.invocationPath, cwd: input.cwd,
112
+ // A follow runs until it is cancelled, so the per-query budget would end it with nothing shown;
113
+ // it gets the longest step the executor accepts instead.
114
+ env: { ...input.env }, timeoutMs: input.follow ? MAX_STEP_TIMEOUT_MS : input.timeoutMs, stdoutLimitBytes: input.outputLimitBytes,
115
+ // The newest records are the ones asked for, so output past the limit drops the oldest bytes
116
+ // rather than the whole read.
117
+ stderrLimitBytes: Math.min(input.outputLimitBytes, 65_536), outputOverflow: 'retain-tail', paths: [], ports: [],
118
+ labels: { stackId: input.stackId, configSha256: input.configSha256, batchId: input.batchId, containerId: input.containerId, service: input.service, since: input.since, runtimeVersion: input.runtimeVersion, executableSha256: input.executable.sha256, ...(input.restartBatches === undefined ? {} : { restartBeforeBatch: input.restartBatches[0], restartAfterBatch: input.restartBatches[1] }) }, expected: [],
119
+ consumedFiles: [
120
+ { path: input.executable.canonicalPath, sha256: input.executable.sha256, maxBytes: 536_870_912, argvIndexes: [0], executable: true, executionMode: input.executable.executionMode },
121
+ ],
122
+ filesystemChecks: [], compensation: [],
123
+ };
124
+ const resolvedStep = snapshotExecutionStep(step);
125
+ if (resolvedStep === undefined)
126
+ return { kind: 'unavailable', code: ENVIRONMENT_CODES.EVIDENCE_UNAVAILABLE, reason: 'log request did not produce a valid execution plan' };
127
+ const plan = { kind: 'planned', step: resolvedStep };
128
+ issuedLogPlans.add(plan);
129
+ return plan;
130
+ }
131
+ const issuedFailureLogPlans = new WeakMap();
132
+ const consumedFailureLogPlans = new WeakSet();
133
+ function unavailableFailurePlan() {
134
+ return { kind: 'unavailable', reason: 'failure logs are unavailable' };
135
+ }
136
+ function composeFailurePrefix(context) {
137
+ const prefix = context.composeArgvPrefix;
138
+ if (!Array.isArray(prefix) || prefix.length < 7 || prefix[0] !== 'compose'
139
+ || prefix[1] !== '--project-directory'
140
+ // A rendered stack runs from the cockpit root, a cockpit's own files from the first one's directory.
141
+ || prefix[2] !== context.cwd && (context.paths.length === 0 || prefix[2] !== composeProjectDirectory(context.cwd, context.paths))
142
+ || prefix.at(-2) !== '--project-name' || prefix.at(-1) !== context.projectName)
143
+ return undefined;
144
+ const fileArgs = prefix.slice(3, -2);
145
+ if (fileArgs.length !== context.paths.length * 2)
146
+ return undefined;
147
+ for (let index = 0; index < context.paths.length; index++) {
148
+ if (fileArgs[index * 2] !== '--file' || fileArgs[index * 2 + 1] !== context.paths[index])
149
+ return undefined;
150
+ }
151
+ return prefix;
152
+ }
153
+ export function planFailureLogs(input) {
154
+ try {
155
+ const captured = captureOwnValues(input, ['evidence', 'tail', 'timeoutMs', 'outputLimitBytes']);
156
+ const evidenceInput = captured?.evidence;
157
+ const tail = captured?.tail;
158
+ const timeoutMs = captured?.timeoutMs;
159
+ const outputLimitBytes = captured?.outputLimitBytes;
160
+ if (!Number.isInteger(tail) || tail < 0 || tail > 100_000
161
+ || !Number.isInteger(timeoutMs) || timeoutMs <= 0 || timeoutMs > 300_000
162
+ || !Number.isInteger(outputLimitBytes) || outputLimitBytes <= 0 || outputLimitBytes > 16_777_216) {
163
+ return unavailableFailurePlan();
164
+ }
165
+ const failureContext = evidenceInput ? consumeRuntimeServiceFailureForLogs(evidenceInput) : undefined;
166
+ const observationContext = failureContext || !evidenceInput
167
+ ? undefined
168
+ : consumeRuntimeServiceObservationForFailureLogs(evidenceInput);
169
+ const context = failureContext ?? observationContext;
170
+ if (!context)
171
+ return unavailableFailurePlan();
172
+ const evidence = failureContext ?? observationContext.observation;
173
+ const containerId = evidence.containerId;
174
+ const composePrefix = failureContext && containerId === null ? composeFailurePrefix(failureContext) : undefined;
175
+ if (failureContext && containerId === null && !composePrefix)
176
+ return unavailableFailurePlan();
177
+ const argv = containerId === null ? [
178
+ context.executable.canonicalPath,
179
+ ...composePrefix,
180
+ 'logs',
181
+ '--no-color',
182
+ '--no-log-prefix',
183
+ '--timestamps',
184
+ '--since',
185
+ evidence.observationWindow.startedAt,
186
+ '--until',
187
+ evidence.observationWindow.expiresAt,
188
+ '--tail',
189
+ String(tail),
190
+ '--follow=false',
191
+ '--',
192
+ evidence.service,
193
+ ] : [
194
+ context.executable.canonicalPath,
195
+ 'container',
196
+ 'logs',
197
+ '--follow=false',
198
+ '--timestamps',
199
+ '--since',
200
+ evidence.observationWindow.startedAt,
201
+ '--until',
202
+ evidence.observationWindow.expiresAt,
203
+ '--tail',
204
+ String(tail),
205
+ evidence.containerId,
206
+ ];
207
+ const executableConsumedFile = {
208
+ path: context.executable.canonicalPath,
209
+ sha256: context.executable.sha256,
210
+ maxBytes: 536_870_912,
211
+ argvIndexes: [0],
212
+ executable: true,
213
+ executionMode: context.executable.executionMode,
214
+ };
215
+ const inheritedConsumedFiles = failureContext && containerId === null
216
+ ? failureContext.consumedFiles
217
+ : [];
218
+ const consumedFiles = inheritedConsumedFiles.some(({ path }) => path === context.executable.canonicalPath)
219
+ ? inheritedConsumedFiles
220
+ : [executableConsumedFile, ...inheritedConsumedFiles];
221
+ const step = snapshotExecutionStep({
222
+ id: `runtime:failure-logs:${containerId ?? `${evidence.projectName}:${evidence.service}`}`,
223
+ argv,
224
+ argv0: context.executable.invocationPath,
225
+ cwd: context.cwd,
226
+ env: context.env,
227
+ timeoutMs: timeoutMs,
228
+ stdoutLimitBytes: outputLimitBytes,
229
+ stderrLimitBytes: Math.min(outputLimitBytes, 65_536),
230
+ paths: failureContext && containerId === null ? failureContext.paths : [],
231
+ ports: [],
232
+ labels: {
233
+ stackId: evidence.stackId,
234
+ configSha256: evidence.configSha256,
235
+ batchId: evidence.batchId,
236
+ ...(containerId === null ? {} : { containerId }),
237
+ service: evidence.service,
238
+ runtimeVersion: evidence.runtimeVersion,
239
+ composeVersion: evidence.composeVersion,
240
+ executableSha256: evidence.executableSha256,
241
+ projectName: evidence.projectName,
242
+ since: evidence.observationWindow.startedAt,
243
+ until: evidence.observationWindow.expiresAt,
244
+ follow: 'false',
245
+ issuanceId: randomBytes(32).toString('hex'),
246
+ },
247
+ expected: [],
248
+ consumedFiles,
249
+ filesystemChecks: failureContext && containerId === null ? failureContext.filesystemChecks : [],
250
+ compensation: [],
251
+ });
252
+ if (!step)
253
+ return unavailableFailurePlan();
254
+ const plan = { kind: 'planned', step };
255
+ issuedFailureLogPlans.set(plan, Object.freeze({ evidence, step, early: failureContext !== undefined }));
256
+ return plan;
257
+ }
258
+ catch {
259
+ // Any throw while building the plan yields the unavailable plan; log content and executor errors can echo secrets.
260
+ return unavailableFailurePlan();
261
+ }
262
+ }
263
+ export function admitFailureLogs(input) {
264
+ const unavailable = () => ({ kind: 'unavailable', records: [] });
265
+ try {
266
+ const captured = captureOwnValues(input, ['plan', 'outcome']);
267
+ const plan = captured?.plan;
268
+ const suppliedOutcome = captured?.outcome;
269
+ if (!plan || plan.kind !== 'planned' || consumedFailureLogPlans.has(plan))
270
+ return unavailable();
271
+ const contract = issuedFailureLogPlans.get(plan);
272
+ const outcome = contract && suppliedOutcome ? authenticatedExecutionOutcome(suppliedOutcome, contract.step) : undefined;
273
+ if (!contract || !outcome)
274
+ return unavailable();
275
+ consumedFailureLogPlans.add(plan);
276
+ if (outcome.kind !== 'complete') {
277
+ if (contract.early)
278
+ return {
279
+ kind: 'unavailable',
280
+ code: outcome.code,
281
+ service: contract.evidence.service,
282
+ records: [],
283
+ };
284
+ return unavailable();
285
+ }
286
+ if (outcome.stdout.byteLength + outcome.stderr.byteLength > contract.step.stdoutLimitBytes)
287
+ return unavailable();
288
+ const windowStart = Date.parse(contract.evidence.observationWindow.startedAt);
289
+ const windowEnd = Date.parse(contract.evidence.observationWindow.expiresAt);
290
+ const windowStartNanoseconds = instantNanoseconds(contract.evidence.observationWindow.startedAt);
291
+ const windowEndNanoseconds = instantNanoseconds(contract.evidence.observationWindow.expiresAt);
292
+ const receiptStart = Date.parse(outcome.receipt.startedAt);
293
+ const receiptEnd = Date.parse(outcome.receipt.finishedAt);
294
+ const now = Date.now();
295
+ if (!validInstant(outcome.receipt.startedAt) || !validInstant(outcome.receipt.finishedAt)
296
+ || windowStart > receiptStart || receiptStart > receiptEnd || receiptEnd > now || receiptEnd > windowEnd
297
+ || now < windowStart || now > windowEnd)
298
+ return unavailable();
299
+ const records = [];
300
+ for (const [stream, output] of [['stdout', outcome.stdout], ['stderr', outcome.stderr]]) {
301
+ const text = new TextDecoder('utf-8', { fatal: true }).decode(output);
302
+ for (const line of text.split('\n').filter(Boolean)) {
303
+ const match = line.match(/^(\S+)\s+(.*)$/);
304
+ if (!match || !validInstant(match[1]))
305
+ return unavailable();
306
+ const timestamp = instantNanoseconds(match[1]);
307
+ if (timestamp < windowStartNanoseconds || timestamp > windowEndNanoseconds)
308
+ return unavailable();
309
+ const message = redactSensitiveMessage(match[2]);
310
+ if (message === undefined)
311
+ return unavailable();
312
+ records.push({
313
+ stackId: contract.evidence.stackId,
314
+ configSha256: contract.evidence.configSha256,
315
+ batchId: contract.evidence.batchId,
316
+ containerId: contract.evidence.containerId,
317
+ service: contract.evidence.service,
318
+ runtimeVersion: contract.evidence.runtimeVersion,
319
+ executableSha256: contract.evidence.executableSha256,
320
+ timestamp: match[1],
321
+ message,
322
+ stream: contract.evidence.containerId === null ? 'combined' : stream,
323
+ restart: false,
324
+ });
325
+ }
326
+ }
327
+ records.sort((left, right) => compareInstants(left.timestamp, right.timestamp)
328
+ || (left.stream === right.stream ? 0 : left.stream === 'stdout' ? -1 : 1));
329
+ return { kind: 'complete', records };
330
+ }
331
+ catch {
332
+ // Any throw while parsing yields unavailable; log content and parser errors can echo secrets.
333
+ return unavailable();
334
+ }
335
+ }
336
+ const FAILURE_LOG_EXCERPT_LINES = 10;
337
+ const FAILURE_LOG_LINE_CHARACTERS = 240;
338
+ /**
339
+ * The bounded message a failed service's diagnostic carries. A restart policy replays the same crash
340
+ * on every start, which filled the excerpt with one line repeated; each distinct line is shown once,
341
+ * where it last occurred, with its count. A line is labelled with its stream only when the runtime
342
+ * reported which stream it was.
343
+ */
344
+ export function failureLogExcerpt(service, records) {
345
+ const distinct = new Map();
346
+ for (const { stream, message } of records) {
347
+ const key = `${stream}\0${message}`;
348
+ const seen = distinct.get(key);
349
+ distinct.delete(key);
350
+ distinct.set(key, { stream, message, count: (seen?.count ?? 0) + 1 });
351
+ }
352
+ const lines = [...distinct.values()];
353
+ const shown = lines.slice(-FAILURE_LOG_EXCERPT_LINES).map(({ stream, message, count }) => {
354
+ const characters = [...message];
355
+ const text = `${characters.slice(0, FAILURE_LOG_LINE_CHARACTERS).join('')}${characters.length > FAILURE_LOG_LINE_CHARACTERS ? '…' : ''}`;
356
+ return `${stream === 'combined' ? '' : `${stream}: `}${text}${count > 1 ? ` (${count} times)` : ''}`;
357
+ });
358
+ const count = `${records.length} log line${records.length === 1 ? '' : 's'}`;
359
+ return `service ${service} wrote ${count} before it failed${lines.length > shown.length ? `; the last ${shown.length} distinct` : ''}: ${shown.join(' | ')}`;
360
+ }
361
+ function instantMs(value) { return Date.parse(value); }
362
+ function instantNanoseconds(value) {
363
+ const match = RFC3339.exec(value);
364
+ if (match === null)
365
+ throw new RefusalError('invalid timestamp');
366
+ const baseMilliseconds = Date.parse(`${match[1]}-${match[2]}-${match[3]}T${match[4]}:${match[5]}:${match[6]}Z`);
367
+ if (!Number.isFinite(baseMilliseconds))
368
+ throw new RefusalError('invalid timestamp');
369
+ const fraction = BigInt((match[7] ?? '').padEnd(9, '0') || '0');
370
+ const offsetMagnitude = match[8] === 'Z' ? 0n : BigInt((Number(match[10]) * 60 + Number(match[11])) * 60);
371
+ const offsetSeconds = match[9] === '-' ? -offsetMagnitude : offsetMagnitude;
372
+ return (BigInt(baseMilliseconds / 1_000) - offsetSeconds) * 1000000000n + fraction;
373
+ }
374
+ function compareInstants(left, right) {
375
+ const leftNanoseconds = instantNanoseconds(left);
376
+ const rightNanoseconds = instantNanoseconds(right);
377
+ return leftNanoseconds < rightNanoseconds ? -1 : leftNanoseconds > rightNanoseconds ? 1 : 0;
378
+ }
379
+ function ownedRestartWindow(value) {
380
+ const snapshot = snapshotJson(value);
381
+ if (snapshot === undefined || snapshot === null || typeof snapshot !== 'object' || Array.isArray(snapshot)
382
+ || !exactKeys(snapshot, ['before', 'after']))
383
+ return undefined;
384
+ const candidate = snapshot;
385
+ return candidate.before !== null && typeof candidate.before === 'object'
386
+ && candidate.after !== null && typeof candidate.after === 'object'
387
+ ? candidate : undefined;
388
+ }
389
+ function ownedLogPlan(value) {
390
+ const snapshot = snapshotJson(value);
391
+ if (snapshot === undefined || snapshot === null || typeof snapshot !== 'object' || Array.isArray(snapshot))
392
+ return undefined;
393
+ const plan = snapshot;
394
+ const labels = plan.step?.labels;
395
+ if (plan.kind !== 'planned' || labels === undefined || !STACK_ID.test(labels.stackId ?? '')
396
+ || !SHA256.test(labels.configSha256 ?? '') || !SHA256.test(labels.batchId ?? '')
397
+ || !SERVICE.test(labels.containerId ?? '') || !SERVICE.test(labels.service ?? '')
398
+ || !(RELATIVE_SINCE.test(labels.since ?? '') || validInstant(labels.since ?? '')) || observedVersionCore(labels.runtimeVersion ?? '') === undefined
399
+ || !SHA256.test(labels.executableSha256 ?? '')
400
+ || ((labels.restartBeforeBatch === undefined) !== (labels.restartAfterBatch === undefined))
401
+ || (labels.restartBeforeBatch !== undefined && (!SHA256.test(labels.restartBeforeBatch)
402
+ || !SHA256.test(labels.restartAfterBatch ?? '') || labels.restartBeforeBatch === labels.restartAfterBatch)))
403
+ return undefined;
404
+ return plan;
405
+ }
406
+ // Collection failures are about the log read itself, never the runtime measurement, so they name what
407
+ // failed instead of sending the reader to measure a runtime that is already measured.
408
+ function unreadable(reason) {
409
+ return { kind: 'unavailable', code: ENVIRONMENT_CODES.EVIDENCE_UNAVAILABLE, reason, records: [] };
410
+ }
411
+ export async function collectLogs(plan, execute, signal, restart) {
412
+ const resolvedPlan = ownedLogPlan(plan);
413
+ if (resolvedPlan === undefined || !issuedLogPlans.has(plan))
414
+ return unreadable('the log plan was not issued by cockpit');
415
+ const resolvedRestart = restart === undefined ? undefined : ownedRestartWindow(restart);
416
+ if (restart !== undefined && resolvedRestart === undefined)
417
+ return unreadable('the restart window is malformed');
418
+ let rawOutcome;
419
+ try {
420
+ rawOutcome = await execute(resolvedPlan.step, { signal });
421
+ }
422
+ catch (error) {
423
+ return unreadable(`the log read could not run: ${errorCause(error)}`);
424
+ }
425
+ const outcome = authenticatedExecutionOutcome(rawOutcome, resolvedPlan.step);
426
+ const authority = resolvedPlan.step.labels;
427
+ const restartBatches = authority.restartBeforeBatch === undefined ? undefined : [authority.restartBeforeBatch, authority.restartAfterBatch];
428
+ if (outcome === undefined)
429
+ return unreadable('the log read returned an outcome cockpit did not issue');
430
+ const cancelled = outcome.kind === 'refusal' && outcome.code === ENVIRONMENT_CODES.EXECUTION_ABORTED;
431
+ // A follow that reaches its budget has ended, as it does when the container stops: what it read is the result.
432
+ const followEnded = outcome.kind === 'refusal' && outcome.code === ENVIRONMENT_CODES.EXECUTION_TIMEOUT
433
+ && resolvedPlan.step.argv.includes('--follow');
434
+ if (outcome.kind === 'refusal' && !cancelled && !followEnded)
435
+ return { kind: 'unavailable', code: outcome.code, reason: `the log read failed (${outcome.code})`, records: [] };
436
+ if (cancelled && !outcome.execution.started)
437
+ return { kind: 'cancelled', records: [], truncated: false };
438
+ if (!outcome.receipt || outcome.receipt.stepId !== resolvedPlan.step.id
439
+ || !validInstant(outcome.receipt.startedAt) || !validInstant(outcome.receipt.finishedAt)
440
+ || instantMs(outcome.receipt.startedAt) > instantMs(outcome.receipt.finishedAt)) {
441
+ return unreadable('the log read has no valid execution receipt');
442
+ }
443
+ const records = [];
444
+ const truncatedStreamStarts = [];
445
+ for (const [stream, output, observedBytes] of [
446
+ ['stdout', outcome.stdout, outcome.execution.stdoutObservedBytes],
447
+ ['stderr', outcome.stderr, outcome.execution.stderrObservedBytes],
448
+ ]) {
449
+ const truncated = observedBytes > output.byteLength;
450
+ // A retained tail starts inside a line, possibly inside a character, so its first newline is the
451
+ // first record boundary that can be trusted.
452
+ const firstNewline = truncated ? output.indexOf(0x0a) : -1;
453
+ const whole = !truncated ? output : firstNewline === -1 ? Buffer.alloc(0) : output.subarray(firstNewline + 1);
454
+ let text;
455
+ try {
456
+ text = new TextDecoder('utf-8', { fatal: true }).decode(whole);
457
+ }
458
+ catch { /* Invalid UTF-8 is the only throw and is what the message names. */
459
+ return unreadable(`the container runtime returned ${stream} that is not UTF-8`);
460
+ }
461
+ const streamStart = records.length;
462
+ for (const line of text.split('\n').filter(Boolean)) {
463
+ const match = line.match(/^(\S+)\s+(.*)$/);
464
+ if (match === null || !validInstant(match[1]))
465
+ return unreadable(`the container runtime returned a ${stream} line without a valid timestamp`);
466
+ const message = redactSensitiveMessage(match[2]);
467
+ if (message === undefined)
468
+ return unreadable(`a ${stream} record could not be redacted, so none are shown`);
469
+ records.push({
470
+ stackId: authority.stackId, configSha256: authority.configSha256,
471
+ batchId: authority.batchId, containerId: authority.containerId,
472
+ service: authority.service, runtimeVersion: authority.runtimeVersion,
473
+ executableSha256: authority.executableSha256,
474
+ timestamp: match[1], message, restart: false, stream,
475
+ });
476
+ }
477
+ if (truncated && records.length > streamStart)
478
+ truncatedStreamStarts.push(records[streamStart].timestamp);
479
+ }
480
+ if (resolvedRestart !== undefined) {
481
+ const { before, after } = resolvedRestart;
482
+ if (restartBatches === undefined || !isRuntimeEvidenceAuthentic(before) || !isRuntimeEvidenceAuthentic(after)
483
+ || before.stackId !== authority.stackId || after.stackId !== authority.stackId
484
+ || before.configSha256 !== authority.configSha256 || after.configSha256 !== authority.configSha256
485
+ || before.command.containerId !== authority.containerId || after.command.containerId !== authority.containerId
486
+ || before.runtime.name !== 'docker' || after.runtime.name !== 'docker'
487
+ || before.runtime.version !== authority.runtimeVersion || after.runtime.version !== authority.runtimeVersion
488
+ || before.runtime.executableSha256 !== authority.executableSha256 || after.runtime.executableSha256 !== authority.executableSha256
489
+ || before.command.batchId !== restartBatches[0] || after.command.batchId !== restartBatches[1]
490
+ || before.command.batchId === after.command.batchId || instantMs(before.observedAt) > instantMs(outcome.receipt.startedAt)
491
+ || instantMs(outcome.receipt.finishedAt) > instantMs(after.captureStartedAt)) {
492
+ return unreadable('the restart evidence does not bracket the log read');
493
+ }
494
+ if (before.stability.processIdentitySha256 !== after.stability.processIdentitySha256) {
495
+ records.push({
496
+ stackId: authority.stackId, configSha256: authority.configSha256,
497
+ batchId: authority.batchId, containerId: authority.containerId,
498
+ service: authority.service, runtimeVersion: authority.runtimeVersion,
499
+ executableSha256: authority.executableSha256,
500
+ timestamp: after.observedAt, message: 'runtime restart observed', stream: 'stdout', restart: true,
501
+ });
502
+ }
503
+ }
504
+ if (resolvedRestart === undefined && restartBatches !== undefined) {
505
+ return unreadable('the log plan expects restart evidence that was not supplied');
506
+ }
507
+ records.sort((left, right) => compareInstants(left.timestamp, right.timestamp)
508
+ || (left.stream === right.stream ? 0 : left.stream === 'stdout' ? -1 : 1));
509
+ // Each stream keeps its own newest bytes. Older records from the other stream would sit beside a
510
+ // stretch where the truncated one shows nothing, so the result starts where every stream is whole.
511
+ const wholeFrom = truncatedStreamStarts.reduce((latest, start) => latest === undefined || compareInstants(start, latest) > 0 ? start : latest, undefined);
512
+ const shown = wholeFrom === undefined ? records : records.filter(({ timestamp }) => compareInstants(timestamp, wholeFrom) >= 0);
513
+ return {
514
+ kind: cancelled ? 'cancelled' : 'complete',
515
+ records: shown,
516
+ truncated: outcome.execution.stdoutObservedBytes > outcome.stdout.byteLength
517
+ || outcome.execution.stderrObservedBytes > outcome.stderr.byteLength,
518
+ };
519
+ }
@@ -0,0 +1,46 @@
1
+ import { type EnvironmentObservation, type JsonSnapshot, type ObservationDeclaration, type StackId } from './contract.js';
2
+ import { type PreflightContext } from './posture-observation.js';
3
+ export { resolveObservationPostures, refreshObservationPostures } from './posture-observation.js';
4
+ export type { PreflightContext, ResolvedPostureAuthority } from './posture-observation.js';
5
+ export interface EvaluatedObservation {
6
+ readonly code: EnvironmentObservation['code'];
7
+ readonly owner: string;
8
+ readonly stackId: StackId;
9
+ readonly source: string;
10
+ readonly verdict: 'pass' | 'fail' | 'unavailable' | 'not-applicable';
11
+ readonly reason: string;
12
+ readonly remedy: string;
13
+ readonly observedAt: string;
14
+ readonly measured?: JsonSnapshot;
15
+ readonly expected: EnvironmentObservation['expectation'];
16
+ }
17
+ export interface PreflightEvaluation {
18
+ readonly verdict: 'pass' | 'fail' | 'unavailable';
19
+ readonly revision: string;
20
+ readonly checks: readonly EvaluatedObservation[];
21
+ }
22
+ export interface DoctorCheck extends EvaluatedObservation {
23
+ readonly evidence: string;
24
+ }
25
+ export interface DoctorProjection {
26
+ readonly verdict: PreflightEvaluation['verdict'];
27
+ readonly checks: readonly DoctorCheck[];
28
+ }
29
+ export declare const allObservationDeclarations: readonly ObservationDeclaration[];
30
+ export declare function validateObservationDeclarations(declarations: readonly ObservationDeclaration[], requiredPostures?: readonly string[]): {
31
+ readonly kind: 'valid';
32
+ readonly declarations: readonly ObservationDeclaration[];
33
+ } | {
34
+ readonly kind: 'invalid';
35
+ readonly reason: string;
36
+ };
37
+ /**
38
+ * Which rows cannot be observed when no stack runs, and why. The key is a declaration id, or the
39
+ * posture-requirement owner for the posture checks.
40
+ */
41
+ export declare function stoppedStackApplicability(input: {
42
+ readonly runtimeDeclared: boolean;
43
+ }): ReadonlyMap<string, string>;
44
+ export declare function observationSatisfiesExpectation(observation: EnvironmentObservation): boolean;
45
+ export declare function projectDoctor(preflight: PreflightEvaluation): DoctorProjection;
46
+ export declare function aggregateObservations(groups: readonly (readonly EnvironmentObservation[])[], context: PreflightContext, unobservable?: ReadonlyMap<string, string>): PreflightEvaluation;