@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,622 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { lstatSync, realpathSync } from 'node:fs';
3
+ import { isDeepStrictEqual } from 'node:util';
4
+ import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { executeProbe } from '../../environment-exec/execute.js';
7
+ import { hasErrorCode, RefusalError, ENVIRONMENT_CODES, errorCause, snapshotJson, } from '../contract.js';
8
+ import { GUARD_EXECUTABLE_SEARCH_PATH } from '../executable.js';
9
+ import { CockpitConfig } from '../../schema.js';
10
+ import { pinnedPolicyDenial } from '../../guards/policy-pin.js';
11
+ import { evaluateGuard, foldGuardSpelling, GUARD_LOCATION_LIMITS, GUARD_ROOT_PROTECTIONS, guardIdentity, } from '../../guards/evaluator.js';
12
+ import { ordinarySessionCommand, resolveOrdinaryInvocation } from '../../command-table.js';
13
+ import { effectivePolicy } from '../../autonomy.js';
14
+ import { GUARD_INPUT_LIMITS, parseGuardInput } from '../../guards/invocation-parser.js';
15
+ import { readAnchoredFile, readAnchoredBatches, nodeAuthority } from '../../generated-environment/transaction.js';
16
+ import { parseOwnershipManifest } from '../../surfaces/ownership.js';
17
+ import { installedPackageMetadata } from '../../package-metadata.js';
18
+ import { GUARD_LOCAL_PATHS, parseGuardLocalIdentity } from '../../guards/local-identity.js';
19
+ import { guardInstalledDirectories, guardLauncherEvidence, guardSearchDirectories, guardSearchPathRelativeEntry, guardSecretFiles, guardSensitivePathsBelow, guardTrackedSensitivePaths, guardUnignoredSensitivePaths, } from '../../guards/protected-locations.js';
20
+ import { classifyReadOnlyInvocation, cockpitLauncherPrefix } from '../../guards/read-only.js';
21
+ import { fileFingerprint, recordVerifiedDigest } from '../../environment-exec/verified-digest.js';
22
+ import { isGuardSurfaceTarget } from '../../surfaces/harness-registry.js';
23
+ import { cockpitCommand, formatShellCommand } from '../../fix-catalogue.js';
24
+ import { observeComposeTopology } from '../compose-topology.js';
25
+ import { declaredRuntime } from '../declaration.js';
26
+ import { observeInstallPolicyLockfiles, withInstallPolicyProjection } from '../install-policy-observation.js';
27
+ import { resolveInstallPolicies, renderGuardProjectCommand } from '../install-policy.js';
28
+ import { checkoutId as deriveCheckoutId, publicationCockpitId } from '../identity.js';
29
+ import { consumeSetupCheckoutProjection, observeSetupCheckout, withSetupCheckoutProjection, } from '../setup-observation.js';
30
+ import { loadCockpitConfig, consumeLoadedConfigProjection } from '../../config/load.js';
31
+ import { deriveRegistry } from '../../registry-derivation.js';
32
+ import { confinedPath, lifecycleRefusal, ownDataValues, sameSource } from './shared.js';
33
+ // `npx --no cockpit logs --service <id>` is an ordinary invocation only for a declared service. A
34
+ // cockpit's services are whatever its own Compose files resolve to, so they are read from Compose
35
+ // rather than guessed, and an unreadable topology refuses.
36
+ async function guardServiceIds(config, root, observe) {
37
+ const runtime = declaredRuntime(config.environment);
38
+ if (runtime === 'none')
39
+ return [];
40
+ const docker = observe.resolveDocker ? await observe.resolveDocker() : undefined;
41
+ if (docker?.state !== 'available')
42
+ return docker?.reason ?? 'no Docker executable resolver is bound';
43
+ const topology = await observeComposeTopology({ root, references: config.environment.compose_files,
44
+ projectName: config.cockpit.name, docker: docker.value, node: observe.node, env: { PATH: GUARD_EXECUTABLE_SEARCH_PATH } }, observe.execute);
45
+ return topology.kind === 'complete' ? topology.services.map(service => service.id) : topology.reason;
46
+ }
47
+ function guardHeld(identity, what, why, fix) {
48
+ const sync = `\`${cockpitCommand('sync', { config: identity.configPath })}\` in a terminal, outside the agent`;
49
+ const action = fix.kind === 'sync' ? `a human must run ${sync}, then commit any regenerated files`
50
+ : `a human must restore it with \`${formatShellCommand(['git', '-C', identity.root, 'restore', '--', fix.path])}\` (it is committed), or run ${sync}`;
51
+ return `${what}; ${why}, so every tool call is held, including a sync run through this agent. Fix: ${action}.`;
52
+ }
53
+ const GUARD_RESTORABLE = new Set(['.cockpit/generated-environment-ownership.json', '.cockpit/guards/bash-guard.sh',
54
+ '.claude/settings.json', '.codex/hooks.json', '.opencode/plugin/cockpit-guards.js']);
55
+ /** Installed bytes are authority only while their source, roots and publication still match. */
56
+ export async function runGuardEvaluationApplication(input, adapters) {
57
+ const unavailable = (reason) => ({
58
+ decision: { decision: 'unavailable', reason },
59
+ diagnostics: [{ severity: 'error', code: 'GUARD_AUTHORITY_UNAVAILABLE', message: reason, target: 'guard' }],
60
+ });
61
+ try {
62
+ const data = ownDataValues(snapshotJson(input), ['cwd', 'guardRoot', 'harness', 'identity', 'payload'], ['askIsDeny', 'ripgrepConfigActive', 'searchPath']);
63
+ if (!data
64
+ || data.searchPath !== undefined && (typeof data.searchPath !== 'string' || /[\x00-\x1f\x7f]/.test(data.searchPath) || Buffer.byteLength(data.searchPath) > GUARD_INPUT_LIMITS.inputBytes)
65
+ || data.askIsDeny !== undefined && typeof data.askIsDeny !== 'boolean'
66
+ || data.ripgrepConfigActive !== undefined && typeof data.ripgrepConfigActive !== 'boolean'
67
+ || !isGuardSurfaceTarget(data.harness))
68
+ return unavailable('Guard request is malformed.');
69
+ const harness = data.harness;
70
+ const captured = ownDataValues(snapshotJson(data.identity), ['root', 'rootDevice', 'rootInode', 'configPath', 'configSha256', 'configDevice', 'configInode', 'node', 'nodeSha256', 'nodeFingerprint', 'module', 'moduleSha256']);
71
+ if (!captured
72
+ || !Object.values(captured).every(value => typeof value === 'string')
73
+ || !['configSha256', 'nodeSha256', 'moduleSha256'].every(key => /^[a-f0-9]{64}$/.test(String(captured[key]))))
74
+ return unavailable('Installed guard identity is missing or malformed.');
75
+ const identity = captured;
76
+ if ([identity.root, identity.configPath, identity.node, identity.module].some(path => !isAbsolute(path) || resolve(path) !== path || /[\x00-\x1f\x7f-\x9f]/.test(path))
77
+ || dirname(identity.configPath) !== identity.root || typeof data.cwd !== 'string' || realpathSync(data.cwd) !== data.cwd
78
+ // A local identity copied from another checkout would evaluate this one under that one's policy.
79
+ || data.guardRoot !== identity.root)
80
+ return unavailable('Installed guard location does not match this application.');
81
+ const configFile = relative(identity.root, identity.configPath);
82
+ const changed = (what) => unavailable(guardHeld(identity, `${what} since the guards were generated`, 'the guard trusts only the cockpit, Node and configuration it was generated for', { kind: 'sync' }));
83
+ if (realpathSync(fileURLToPath(new URL('../../../dist/cli.js', import.meta.url))) !== identity.module)
84
+ return changed('a different cockpit installation is running');
85
+ const roots = new Map();
86
+ for (const [path, directory] of [[identity.root, true], [identity.configPath, false]]) {
87
+ let status;
88
+ try {
89
+ status = lstatSync(path);
90
+ }
91
+ catch (error) {
92
+ if (hasErrorCode(error, 'ENOENT') && !directory)
93
+ return unavailable(guardHeld(identity, `${identity.configPath} is missing`, 'guard policy comes from it', { kind: 'restore', path: configFile }));
94
+ throw error;
95
+ }
96
+ // The configuration is authenticated by its content digest below. Holding on its inode
97
+ // as well refused the identical bytes that `git restore`, `sed -i` and editors that save
98
+ // by rename leave in a new file, until a human ran sync for no change.
99
+ if (status.isSymbolicLink() || (directory ? !status.isDirectory() : !status.isFile())
100
+ || directory && (String(status.dev) !== identity.rootDevice || String(status.ino) !== identity.rootInode)
101
+ || realpathSync(path) !== path) {
102
+ return changed(directory ? `the cockpit checkout ${path} was moved or replaced` : `${path} was replaced by a link or a non-file`);
103
+ }
104
+ roots.set(path, status);
105
+ }
106
+ // Re-hashing a 100 MB Node binary in every guard process dominated each call. The local record
107
+ // carries the fingerprint sync hashed; an identical device, inode, size, mode and timestamps
108
+ // stand in for the re-hash, and the local record is checked against this identity below.
109
+ const nodePath = realpathSync(process.execPath);
110
+ if (nodePath === identity.node && fileFingerprint(nodePath, lstatSync(nodePath)) === identity.nodeFingerprint) {
111
+ recordVerifiedDigest(identity.nodeFingerprint, identity.nodeSha256);
112
+ }
113
+ const node = await nodeAuthority();
114
+ if (!node || node.canonicalPath !== identity.node || node.sha256 !== identity.nodeSha256)
115
+ return changed(`the Node executable changed (now ${node?.canonicalPath ?? 'unreadable'})`);
116
+ const read = async (root, path, maxBytes) => {
117
+ const result = await readAnchoredFile(root, path, maxBytes, node, { execute: adapters.execute ?? executeProbe });
118
+ if (result.kind !== 'complete')
119
+ throw new RefusalError('Guard authority file is missing, changed, oversized or crosses a symlink boundary.');
120
+ return result;
121
+ };
122
+ const manifestPath = '.cockpit/generated-environment-ownership.json';
123
+ const registration = { claudecode: '.claude/settings.json', codexcli: '.codex/hooks.json', opencode: '.opencode/plugin/cockpit-guards.js' }[harness];
124
+ // The configuration rides in the same batch: the policy pin applies an edit to exactly these bytes.
125
+ const configName = basename(identity.configPath);
126
+ const publicationRequests = [manifestPath, '.cockpit/guards/bash-guard.sh', registration, GUARD_LOCAL_PATHS.identity, configName].map(path => ({
127
+ path, action: 'read-file', maxBytes: path === manifestPath || path === configName ? 1_048_576 : 65_536,
128
+ }));
129
+ const readPublication = async () => {
130
+ const batch = await readAnchoredBatches(identity.root, publicationRequests, node, { execute: adapters.execute ?? executeProbe });
131
+ if (batch?.kind !== 'complete' || batch.results.length !== publicationRequests.length)
132
+ throw new RefusalError('Guard publication read could not be authenticated.');
133
+ return new Map(publicationRequests.map((request, index) => {
134
+ const result = batch.results[index];
135
+ if (result?.kind === 'missing')
136
+ throw new GuardFileMissing(request.path);
137
+ if (result?.kind !== 'complete')
138
+ throw new RefusalError('Guard authority file is missing, changed, oversized or crosses a symlink boundary.');
139
+ return [request.path, result];
140
+ }));
141
+ };
142
+ // The module, configuration and publication reads are independent and each costs a child
143
+ // process, so they run together. Results are consumed in the order the checks below need, so
144
+ // the first failing check is the one reported.
145
+ const settle = (work) => work.then(value => ({ value }), (error) => ({ error }));
146
+ const unwrap = (settled) => { if ('error' in settled)
147
+ throw settled.error; return settled.value; };
148
+ const modulePending = settle(read(dirname(identity.module), basename(identity.module), 16_777_216));
149
+ const configPending = settle(adapters.loadConfig(identity.configPath));
150
+ const publicationPending = settle(readPublication());
151
+ const module = unwrap(await modulePending);
152
+ if (module.sha256 !== identity.moduleSha256)
153
+ return changed('the installed cockpit version changed (its handler bytes differ)');
154
+ const loaded = unwrap(await configPending);
155
+ if (loaded.kind !== 'complete' || loaded.result.source.path !== identity.configPath || loaded.result.source.sha256 !== identity.configSha256) {
156
+ return unavailable(guardHeld(identity, `${identity.configPath} changed since the guards were generated`, 'guard policy comes from it and an edit takes effect only once a human approves it', { kind: 'sync' }));
157
+ }
158
+ const config = loaded.result.config;
159
+ let publication;
160
+ try {
161
+ publication = unwrap(await publicationPending);
162
+ }
163
+ catch (error) {
164
+ if (!(error instanceof GuardFileMissing))
165
+ throw error;
166
+ return unavailable(GUARD_RESTORABLE.has(error.path)
167
+ ? guardHeld(identity, `${error.path} is missing`, 'it authenticates the guard and the hook that launches it', { kind: 'restore', path: error.path })
168
+ : guardHeld(identity, `${error.path} is missing`, 'it records the Node executable and cockpit handler on this machine, which committed files cannot carry', { kind: 'sync' }));
169
+ }
170
+ const local = parseGuardLocalIdentity(publication.get(GUARD_LOCAL_PATHS.identity).bytes.toString('utf8'));
171
+ if (!local || !isDeepStrictEqual(local, identity))
172
+ return unavailable('Installed guard identity does not match its machine-local record.');
173
+ const manifestRead = publication.get(manifestPath);
174
+ const ownership = parseOwnershipManifest(manifestRead.bytes.toString('utf8'), {
175
+ root: identity.root, configPath: identity.configPath,
176
+ cockpitId: publicationCockpitId({ name: config.cockpit.name }),
177
+ });
178
+ if (ownership.kind !== 'complete' || ownership.manifest.source.sha256 !== identity.configSha256)
179
+ return unavailable('Installed guard publication is stale or unauthenticated.');
180
+ const producer = ownership.manifest.producer;
181
+ if (producer.package !== installedPackageMetadata.name
182
+ || producer.version !== installedPackageMetadata.version
183
+ || producer.sourceRevision !== installedPackageMetadata.cockpitSourceRevision)
184
+ return changed(`the installed cockpit version changed (the guards were generated by ${producer.package} ${producer.version}, ${installedPackageMetadata.name} ${installedPackageMetadata.version} is installed)`);
185
+ const published = new Map();
186
+ for (const path of ['.cockpit/guards/bash-guard.sh', registration]) {
187
+ const entry = ownership.manifest.entries.find(entry => entry.path === path);
188
+ const actual = publication.get(path);
189
+ if (!entry || entry.kind !== 'file' || entry.sha256 !== actual.sha256)
190
+ return unavailable('Installed guard registration or launcher bytes drifted.');
191
+ published.set(path, actual.sha256);
192
+ }
193
+ const git = await adapters.resolveGit();
194
+ if (git.state !== 'available')
195
+ return unavailable('Guard checkout observation requires an authenticated Git executable.');
196
+ const checkout = await adapters.observeCheckout({ source: loaded.result.source, cockpitName: config.cockpit.name,
197
+ observedAt: new Date().toISOString(), env: {}, gitExecutable: git.value });
198
+ const parsed = parseGuardInput(data.payload);
199
+ if (parsed.status !== 'parsed')
200
+ return unavailable(parsed.reason);
201
+ if (parsed.cwd !== data.cwd)
202
+ return unavailable('Guard request cwd does not match its invocation authority.');
203
+ const prepared = await prepareGuardEvaluationPolicy({ configuration: loaded.result, checkout });
204
+ if (prepared.kind !== 'complete')
205
+ return unavailable('Guard policy authority could not be authenticated.');
206
+ // The inspector answers synchronously, so what only Git can say is asked first: which protected
207
+ // paths a default ripgrep search reads, and which a repository's index holds for a Git command
208
+ // that prints committed content. A request not asked here gets the walk, or no answer.
209
+ const policy = prepared.result.policy;
210
+ const instancePaths = policy.instances.map(item => item.path);
211
+ const present = (path) => { try {
212
+ lstatSync(path);
213
+ return true;
214
+ }
215
+ catch (error) {
216
+ if (error.code === 'ENOENT')
217
+ return false;
218
+ throw error;
219
+ } };
220
+ const allowed = (path) => policy.allowedRoots.some(root => confinedPath(root, path));
221
+ // Each entry starts as the answer for Git not answering and is replaced by Git's.
222
+ const unignored = new Map();
223
+ const tracked = new Map();
224
+ const queries = [];
225
+ for (const invocation of parsed.invocations) {
226
+ const access = classifyReadOnlyInvocation(invocation, { ripgrepConfigActive: data.ripgrepConfigActive === true });
227
+ if (access?.trackedContent && allowed(access.cwd) && !tracked.has(access.cwd)) {
228
+ tracked.set(access.cwd, null);
229
+ queries.push(async () => { tracked.set(access.cwd, await guardTrackedSensitivePaths(access.cwd, instancePaths, git.value, adapters.execute ?? executeProbe)); });
230
+ }
231
+ for (const tree of access?.skipsIgnored ? access.trees : []) {
232
+ const path = resolve(access.cwd, tree);
233
+ if (!allowed(path) || unignored.has(path) || !present(path))
234
+ continue;
235
+ unignored.set(path, undefined);
236
+ queries.push(async () => { unignored.set(path, await guardUnignoredSensitivePaths(path, instancePaths, git.value, adapters.execute ?? executeProbe)); });
237
+ }
238
+ }
239
+ // A query starts up to four Git processes, so a command naming many trees is asked about a few
240
+ // at a time, and a failed one stops the rest from starting.
241
+ const worker = async () => {
242
+ for (let query = queries.shift(); query; query = queries.shift())
243
+ await query().catch((error) => { queries.length = 0; throw error; });
244
+ };
245
+ await Promise.all([worker(), worker(), worker(), worker()]);
246
+ const walks = new Map();
247
+ const searched = (path, unignoredOnly) => {
248
+ const listed = unignoredOnly ? unignored.get(path) : undefined;
249
+ if (listed !== undefined)
250
+ return listed;
251
+ let walked = walks.get(path);
252
+ if (walked === undefined)
253
+ walks.set(path, walked = guardSensitivePathsBelow(path));
254
+ // The walk names environment and secrets names; an ignore-aware search also needs the instances it reads.
255
+ if (!unignoredOnly || walked === null)
256
+ return walked;
257
+ const found = [...new Set([...walked, ...instancePaths.filter(item => confinedPath(path, item) && present(item))])].sort();
258
+ return found.length > GUARD_LOCATION_LIMITS.sensitivePaths ? null : found;
259
+ };
260
+ const inspected = new Map();
261
+ // The system resolves `..` from where a link leads, so `link/../x` names a sibling of the link's
262
+ // target, not of the link: a spelling that leaves a link through `..` is not its lexical path.
263
+ const leavesLink = (cwd, spelled) => {
264
+ let cursor = isAbsolute(spelled) ? '/' : cwd;
265
+ for (const component of spelled.split('/')) {
266
+ if (component === '..') {
267
+ try {
268
+ const status = lstatSync(cursor);
269
+ if (status.isSymbolicLink())
270
+ return true;
271
+ inspected.set(cursor, status);
272
+ }
273
+ catch (error) {
274
+ // A missing or non-directory component fails the system's lookup before `..` applies.
275
+ if (!['ENOENT', 'ENOTDIR'].includes(error.code ?? ''))
276
+ throw error;
277
+ }
278
+ cursor = dirname(cursor);
279
+ }
280
+ else if (component && component !== '.')
281
+ cursor = join(cursor, component);
282
+ }
283
+ return false;
284
+ };
285
+ const inspect = request => {
286
+ const path = resolve(request.cwd, request.path);
287
+ const root = prepared.result.policy.allowedRoots.filter(root => confinedPath(root, path)).sort((a, b) => b.length - a.length)[0];
288
+ if (!root)
289
+ return { status: 'unavailable', path, symlink: false };
290
+ if (leavesLink(request.cwd, request.path))
291
+ return { status: 'resolved', path, symlink: true };
292
+ let cursor = root;
293
+ const rootStatus = lstatSync(root);
294
+ inspected.set(root, rootStatus);
295
+ // Identities let the evaluator recognise a protected entry under any spelling the volume
296
+ // resolves to it; a missing leaf reports its nearest existing ancestors only.
297
+ const chain = [guardIdentity(rootStatus)];
298
+ let exists = true;
299
+ for (const component of relative(root, path).split(sep).filter(Boolean)) {
300
+ cursor = join(cursor, component);
301
+ let status;
302
+ try {
303
+ status = lstatSync(cursor);
304
+ }
305
+ catch (error) {
306
+ if (error.code === 'ENOENT') {
307
+ exists = false;
308
+ break;
309
+ }
310
+ throw error;
311
+ }
312
+ if (status.isSymbolicLink())
313
+ return { status: 'resolved', path, symlink: true };
314
+ inspected.set(cursor, status);
315
+ chain.push(guardIdentity(status));
316
+ }
317
+ const search = request.operation === 'search' || request.operation === 'unignored-search';
318
+ return { status: 'resolved', path, symlink: false, identity: exists ? chain.at(-1) : null, ancestors: exists ? chain.slice(0, -1) : chain,
319
+ ...(search ? { sensitive: exists ? searched(path, request.operation === 'unignored-search') : [] } : {}),
320
+ ...(request.operation === 'repository' ? { tracked: tracked.get(path) ?? null } : {}) };
321
+ };
322
+ const cockpitInvocations = [];
323
+ const launcher = resolve(dirname(identity.module), '../bin/cockpit.mjs');
324
+ const searchPath = data.searchPath ?? '';
325
+ const launched = [];
326
+ const prove = (executable, cwd, argv, session) => {
327
+ const prefix = executable === null ? undefined : cockpitLauncherPrefix(executable, argv);
328
+ const evidence = prefix === undefined ? undefined : guardLauncherEvidence(executable, argv.slice(0, prefix), cwd, identity.root, launcher, searchPath);
329
+ if (evidence === undefined)
330
+ return undefined;
331
+ inspected.set(evidence, lstatSync(evidence));
332
+ const proven = { executable: executable, cwd, argv, prefix: prefix, session };
333
+ launched.push(proven);
334
+ return proven;
335
+ };
336
+ const configured = { canonicalRoot: identity.root, configPath: identity.configPath };
337
+ for (const { executable, cwd, argv } of parsed.invocations) {
338
+ const proven = prove(executable, cwd, argv, true);
339
+ // The evaluator judges a session's command as one more invocation, so its spelling needs proof too.
340
+ const inner = proven && ordinarySessionCommand(argv.slice(proven.prefix), cwd, configured);
341
+ if (inner)
342
+ prove(inner[0], cwd, inner.slice(1), false);
343
+ }
344
+ let launcherSha256;
345
+ if (launched.length) {
346
+ const status = lstatSync(launcher);
347
+ if (!status.isFile() || status.isSymbolicLink() || !(status.mode & 0o111) || realpathSync(launcher) !== launcher) {
348
+ return unavailable('Guard ordinary command executable is unavailable or ambiguous.');
349
+ }
350
+ launcherSha256 = (await read(dirname(launcher), basename(launcher), 65_536)).sha256;
351
+ inspected.set(launcher, status);
352
+ const services = await guardServiceIds(config, identity.root, { node, resolveDocker: adapters.resolveDocker, execute: adapters.execute ?? executeProbe });
353
+ if (typeof services === 'string')
354
+ return unavailable(`Guard could not read the Compose service list: ${services}`);
355
+ const authority = { executable: launcher, ...configured, allowedRoots: prepared.result.policy.allowedRoots, services,
356
+ gates: config.testing?.gates.map(gate => gate.id) ?? [], grants: ordinaryGrants(config) };
357
+ for (const { executable, cwd, argv, prefix, session } of launched) {
358
+ const args = argv.slice(prefix);
359
+ // A session inside a session would run a command the evaluator never sees.
360
+ const admitted = session && ordinarySessionCommand(args, cwd, configured) ? {}
361
+ : resolveOrdinaryInvocation({ executable: launcher, cwd, argv: args }, authority, path => {
362
+ const fact = inspect({ path, cwd, operation: 'write' });
363
+ return !!fact && typeof fact === 'object' && 'status' in fact && fact.status === 'resolved'
364
+ && 'symlink' in fact && fact.symlink === false;
365
+ });
366
+ // The evaluator matches the invocation as the agent spelled it, and checks what it reads.
367
+ if (admitted)
368
+ cockpitInvocations.push({ executable, cwd, argv, ...(admitted.reads ? { reads: admitted.reads } : {}) });
369
+ }
370
+ }
371
+ if (cockpitInvocations.length + prepared.result.policy.projectCommands.length > GUARD_INPUT_LIMITS.invocations)
372
+ return unavailable('Guard candidates exceed the invocation bound.');
373
+ // The policy pin needs the text an edit applies to, which must be the text the guard was generated for.
374
+ const configRead = publication.get(configName);
375
+ if (configRead.sha256 !== identity.configSha256)
376
+ return unavailable('Guard configuration changed during evaluation.');
377
+ const configStatus = lstatSync(identity.configPath);
378
+ // A hard link or another spelling of the configuration is the configuration.
379
+ const isConfig = (path) => {
380
+ if (foldGuardSpelling(path) === foldGuardSpelling(identity.configPath))
381
+ return true;
382
+ try {
383
+ const status = lstatSync(path);
384
+ return status.dev === configStatus.dev && status.ino === configStatus.ino;
385
+ }
386
+ catch (error) {
387
+ if (hasErrorCode(error, 'ENOENT', 'ENOTDIR'))
388
+ return false;
389
+ throw error;
390
+ }
391
+ };
392
+ const pinned = pinnedPolicyDenial(parsed, configRead.bytes.toString('utf8'), isConfig);
393
+ if (pinned)
394
+ return { decision: pinned, diagnostics: [{ severity: 'error', code: 'GUARD_REFUSED', message: pinned.reason, target: 'guard' }] };
395
+ const decision = evaluateGuard(parsed, { ...prepared.result.policy, cockpitInvocations,
396
+ cockpitLaunchers: launched.map(({ executable, cwd, argv, prefix }) => ({ executable, cwd, argv: argv.slice(0, prefix) })),
397
+ searchDirectories: guardSearchDirectories(searchPath, prepared.result.policy.allowedRoots),
398
+ searchPathRelativeEntry: guardSearchPathRelativeEntry(searchPath),
399
+ ripgrepConfigActive: data.ripgrepConfigActive === true }, inspect);
400
+ const current = await adapters.loadConfig(identity.configPath);
401
+ if (current.kind !== 'complete' || current.result.source.sha256 !== identity.configSha256)
402
+ return unavailable('Guard configuration changed during evaluation.');
403
+ for (const [path, before] of [...roots, ...inspected]) {
404
+ const after = lstatSync(path);
405
+ // Directories may receive unrelated leaves, but replacement and file changes invalidate evidence.
406
+ if (before.dev !== after.dev || before.ino !== after.ino || before.mode !== after.mode
407
+ || before.isFile() && (before.mtimeMs !== after.mtimeMs || before.ctimeMs !== after.ctimeMs || before.size !== after.size))
408
+ return unavailable('Guard filesystem authority changed during evaluation.');
409
+ }
410
+ const currentPublication = await readPublication();
411
+ for (const [path, hash] of [[manifestPath, manifestRead.sha256], [GUARD_LOCAL_PATHS.identity, publication.get(GUARD_LOCAL_PATHS.identity).sha256], ...published]) {
412
+ if (currentPublication.get(path).sha256 !== hash)
413
+ return unavailable('Guard publication changed during evaluation.');
414
+ }
415
+ if ((await read(dirname(identity.module), basename(identity.module), 16_777_216)).sha256 !== identity.moduleSha256)
416
+ return unavailable('Guard handler changed during evaluation.');
417
+ if (launcherSha256 && (await read(dirname(launcher), basename(launcher), 65_536)).sha256 !== launcherSha256)
418
+ return unavailable('Guard ordinary command executable changed during evaluation.');
419
+ return { decision, diagnostics: decision.decision === 'allow' ? [] : [{ severity: 'error', code: 'GUARD_REFUSED', message: decision.reason, target: 'guard' }] };
420
+ }
421
+ catch (error) {
422
+ return unavailable(`Guard authority could not be read or revalidated: ${errorCause(error)}`);
423
+ }
424
+ }
425
+ // The guard runs only against the configuration sync generated it from, so the policy read here is the
426
+ // one the last sync accepted.
427
+ function ordinaryGrants(config) {
428
+ const policy = effectivePolicy(config);
429
+ return [...config.browser?.request_preapproval === true ? ['browser-preapproval'] : [],
430
+ ...policy.autonomy.sync === 'agent' ? ['autonomy-sync'] : [], ...policy.autonomy.reset === 'agent' ? ['autonomy-reset'] : []];
431
+ }
432
+ class GuardFileMissing extends RefusalError {
433
+ path;
434
+ constructor(path) {
435
+ super(`${path} is missing`);
436
+ this.path = path;
437
+ }
438
+ }
439
+ function guardPolicyRefusal(message) {
440
+ return lifecycleRefusal(ENVIRONMENT_CODES.LIFECYCLE_EVIDENCE_INVALID, message, 'guard-policy');
441
+ }
442
+ /** Source projections share authenticated authority; installed consumption is admitted separately. */
443
+ export async function prepareGuardEvaluationPolicy(input) {
444
+ const inputData = ownDataValues(input, ['configuration', 'checkout']);
445
+ const data = inputData && Object.hasOwn(inputData, 'configuration') && Object.hasOwn(inputData, 'checkout') ? inputData : undefined;
446
+ if (!data)
447
+ return guardPolicyRefusal('guard policy requires own-data configuration and checkout observations');
448
+ const configuration = data.configuration;
449
+ const exposed = snapshotJson(configuration);
450
+ const loaded = consumeLoadedConfigProjection(configuration);
451
+ if (!loaded || !exposed || !isDeepStrictEqual(exposed, snapshotJson(loaded))) {
452
+ return guardPolicyRefusal('guard policy configuration authority is copied, changed or already consumed');
453
+ }
454
+ const derived = deriveRegistry(loaded.config);
455
+ if (derived.kind !== 'complete')
456
+ return guardPolicyRefusal(`guard policy registry refused (${derived.code}): ${derived.message}`);
457
+ const guardConfig = CockpitConfig.parse(derived.result.config);
458
+ const observed = snapshotJson(data.checkout);
459
+ if (observed?.state !== 'available')
460
+ return guardPolicyRefusal('guard policy checkout observation is unavailable');
461
+ try {
462
+ const identities = new Map();
463
+ let siblingsRoot;
464
+ const result = await withSetupCheckoutProjection(data.checkout, {
465
+ source: loaded.source, cockpitName: loaded.config.cockpit.name, evaluatedAt: Date.parse(observed.observedAt),
466
+ }, (projection) => {
467
+ const checkout = consumeSetupCheckoutProjection(projection);
468
+ if (!checkout || !sameSource(checkout.source, loaded.source)) {
469
+ return guardPolicyRefusal('guard policy checkout does not match configuration authority');
470
+ }
471
+ const config = guardConfig;
472
+ const protectedIdentities = new Map();
473
+ // Records the identity of every existing component so the evaluator can match a protected
474
+ // entry by inode rather than by spelling.
475
+ const protect = (root, path, leaf, label) => {
476
+ const segments = relative(root, path).split(sep);
477
+ const chain = [guardIdentity(identities.get(root))];
478
+ let cursor = root;
479
+ let exists = true;
480
+ for (const [index, segment] of segments.entries()) {
481
+ cursor = join(cursor, segment);
482
+ let status;
483
+ try {
484
+ status = lstatSync(cursor);
485
+ }
486
+ catch (error) {
487
+ if (hasErrorCode(error, 'ENOENT')) {
488
+ exists = false;
489
+ break;
490
+ }
491
+ throw error;
492
+ }
493
+ if (status.isSymbolicLink() || (index < segments.length - 1 || leaf === 'directory' ? !status.isDirectory() : !status.isFile())) {
494
+ throw new RefusalError(`guard policy ${label} path has a symlink or unexpected file type`);
495
+ }
496
+ identities.set(cursor, status);
497
+ chain.push(guardIdentity(status));
498
+ }
499
+ protectedIdentities.set(path, { path, identity: exists ? chain.at(-1) : null,
500
+ ancestors: exists ? chain.slice(0, -1) : chain });
501
+ };
502
+ const directory = (path) => {
503
+ const before = lstatSync(path);
504
+ if (!before.isDirectory() || before.isSymbolicLink() || realpathSync(path) !== path
505
+ || !isAbsolute(path) || path === '/' || /[\x00-\x1f\x7f]/.test(path)) {
506
+ throw new RefusalError('guard policy requires canonical directory authority');
507
+ }
508
+ identities.set(path, before);
509
+ return path;
510
+ };
511
+ const canonicalRoot = directory(dirname(loaded.source.path));
512
+ siblingsRoot = directory(resolve(canonicalRoot, config.cockpit.siblings_root));
513
+ const allowedRoots = new Set([canonicalRoot]);
514
+ const instances = [];
515
+ const templates = [];
516
+ for (const sibling of config.sibling) {
517
+ const root = resolve(siblingsRoot, sibling.dir);
518
+ if (!confinedPath(siblingsRoot, root))
519
+ throw new RefusalError('guard policy sibling escapes its declared root');
520
+ allowedRoots.add(directory(root));
521
+ for (const file of derived.result.secretPaths.filter(row => row.slug === sibling.slug)) {
522
+ const path = resolve(root, file.path);
523
+ if (!confinedPath(root, path) || path === root)
524
+ throw new RefusalError('guard policy instance escapes its sibling root');
525
+ protect(root, path, 'file', 'instance');
526
+ instances.push({ name: `${sibling.slug}-${createHash('sha256').update(file.path).digest('hex').slice(0, 12)}`, path });
527
+ }
528
+ for (const file of sibling.environment_files ?? []) {
529
+ const path = resolve(root, file.template);
530
+ if (!confinedPath(root, path) || path === root)
531
+ throw new RefusalError('guard policy environment template escapes its sibling root');
532
+ templates.push(path);
533
+ }
534
+ }
535
+ if (allowedRoots.size > 256 || instances.length > 256)
536
+ throw new RefusalError('guard policy exceeds restriction root or instance bounds');
537
+ for (const { path, type } of GUARD_ROOT_PROTECTIONS)
538
+ protect(canonicalRoot, join(canonicalRoot, path), type, 'guard control or secrets');
539
+ // The configuration's own identity lets the evaluator recognise its deletion under any spelling.
540
+ const configPath = loaded.source.path;
541
+ protect(canonicalRoot, configPath, 'file', 'configuration');
542
+ // This module runs from dist/environment, so two levels up is the running cockpit package.
543
+ const executionDirectories = guardInstalledDirectories(resolve(fileURLToPath(new URL('../../..', import.meta.url))), canonicalRoot)
544
+ .filter(path => [...allowedRoots].some(root => confinedPath(root, path)));
545
+ const secretFiles = guardSecretFiles(canonicalRoot);
546
+ const protectedFilesLinked = secretFiles.linked || instances.some(item => (identities.get(item.path)?.nlink ?? 1) > 1);
547
+ for (const [path, before] of identities) {
548
+ const after = lstatSync(path);
549
+ if (before.dev !== after.dev || before.ino !== after.ino || before.mode !== after.mode
550
+ || before.mtimeMs !== after.mtimeMs || before.ctimeMs !== after.ctimeMs || before.size !== after.size) {
551
+ throw new RefusalError('guard policy root or instance identity changed during construction');
552
+ }
553
+ }
554
+ const policy = { canonicalRoot, configPath,
555
+ allowedRoots: [...allowedRoots], instances, templates,
556
+ protectedIdentities: [...protectedIdentities.values()],
557
+ protectedBranches: [...config.guards.protected_branches],
558
+ hostedDatabaseHosts: [...config.guards.hosted_db_hosts],
559
+ vendorHosts: [...config.guards.vendor_endpoint_hosts],
560
+ declaredAllows: config.guards.allow.map(rule => ({ ...rule,
561
+ ...(rule.cwd === undefined ? {} : { cwd: resolve(canonicalRoot, rule.cwd) }), argv: [...rule.argv] })),
562
+ declaredDenies: config.guards.deny.map(rule => ({
563
+ match: 'prefix' in rule ? 'prefix' : 'contains',
564
+ argv: [...('prefix' in rule ? rule.prefix : rule.contains)], reason: rule.reason,
565
+ ...(rule.fix === undefined ? {} : { fix: rule.fix }),
566
+ })),
567
+ ripgrepConfigActive: false,
568
+ executionDirectories, searchDirectories: [], searchPathRelativeEntry: null,
569
+ secretFileIdentities: secretFiles.identities && [...secretFiles.identities], protectedFilesLinked,
570
+ cockpitInvocations: [], cockpitLaunchers: [], projectCommands: [], };
571
+ return { kind: 'complete', status: 0, diagnostics: [],
572
+ result: { policy } };
573
+ });
574
+ if (!result || result.kind !== 'complete' || !siblingsRoot) {
575
+ return result ?? guardPolicyRefusal('guard policy checkout authority is copied, changed, expired or already consumed');
576
+ }
577
+ const boundaries = result.result.policy;
578
+ const projectCommands = [];
579
+ for (const [siblingIndex, sibling] of guardConfig.sibling.entries()) {
580
+ const policies = guardConfig.environment.install_policies;
581
+ if (!policies.some(policy => policy.toolchain === sibling.toolchain && (policy.build || policy.test)))
582
+ continue;
583
+ const checkoutRoot = resolve(siblingsRoot, sibling.dir);
584
+ const checkoutId = deriveCheckoutId({ checkoutPath: checkoutRoot });
585
+ const evaluatedAt = Date.now();
586
+ const observation = await observeInstallPolicyLockfiles({ source: loaded.source, siblingIndex, siblingSlug: sibling.slug,
587
+ checkoutRoot, checkoutId, policies, observedAt: new Date(evaluatedAt).toISOString() });
588
+ const resolution = await withInstallPolicyProjection(observation, {
589
+ source: loaded.source, siblingIndex, siblingSlug: sibling.slug, checkoutRoot, checkoutId, policies, evaluatedAt,
590
+ }, projection => resolveInstallPolicies({ source: loaded.source, siblingIndex, sibling, checkoutRoot, checkoutId, policies, projection }));
591
+ if (resolution?.kind !== 'complete' || resolution.bindings.length !== 1) {
592
+ return guardPolicyRefusal(`guard project command policy is unavailable for ${sibling.slug}`);
593
+ }
594
+ for (const kind of ['build', 'test']) {
595
+ const argv = resolution.bindings[0].policy[kind];
596
+ if (!argv)
597
+ continue;
598
+ if (renderGuardProjectCommand(argv).kind !== 'complete')
599
+ return guardPolicyRefusal(`guard ${kind} command exceeds its rendering bound`);
600
+ projectCommands.push({ executable: argv[0], checkoutRoot, kind, argv: argv.slice(1) });
601
+ }
602
+ }
603
+ if (projectCommands.length > GUARD_INPUT_LIMITS.invocations)
604
+ return guardPolicyRefusal('guard candidates exceed the invocation bound');
605
+ const current = await loadCockpitConfig(loaded.source.path);
606
+ const revalidated = current.kind === 'complete' ? consumeLoadedConfigProjection(current.result) : undefined;
607
+ if (!revalidated || !sameSource(revalidated.source, loaded.source))
608
+ return guardPolicyRefusal('guard configuration source changed during command construction');
609
+ for (const [path, before] of identities) {
610
+ const after = lstatSync(path);
611
+ if (before.dev !== after.dev || before.ino !== after.ino || before.mode !== after.mode
612
+ || before.mtimeMs !== after.mtimeMs || before.ctimeMs !== after.ctimeMs || before.size !== after.size) {
613
+ return guardPolicyRefusal('guard root or instance identity changed during command construction');
614
+ }
615
+ }
616
+ const policy = { ...boundaries, projectCommands };
617
+ return { kind: 'complete', status: 0, diagnostics: [], result: { policy } };
618
+ }
619
+ catch (error) {
620
+ return guardPolicyRefusal(`guard policy construction failed: ${errorCause(error)}`);
621
+ }
622
+ }