@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,699 @@
1
+ import { lstat, realpath } from 'node:fs/promises';
2
+ import { basename, dirname, join, relative, resolve } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { renderBrowserSurfaces } from '../../browser/surfaces.js';
5
+ import { resolveBrowserRuntime } from '../../browser/runtime.js';
6
+ import { resolveObservationPostures } from '../posture-observation.js';
7
+ import { SURFACE_FEATURES, SURFACE_TARGETS } from '../../surfaces/contract.js';
8
+ import { GUARD_SURFACE_TARGETS, harnessFact, isGuardSurfaceTarget, listedSurfaceTargets } from '../../surfaces/harness-registry.js';
9
+ import { mergeAdoptedSurface } from '../../surfaces/adoption.js';
10
+ import { generateSurfaceEstate } from '../../surfaces/generator-adapter.js';
11
+ import { mergeSurfaceDeclarations } from '../../surfaces/merge.js';
12
+ import { CORE_EVALUATOR_ASSET as EVALUATOR_ASSET, CORE_PROCEDURE_ASSETS as PROCEDURE_ASSETS, CORE_SKILL_ASSETS as SKILL_ASSETS, coreSurfaceDeclarations, } from '../../surfaces/core-declarations.js';
13
+ import { effectivePolicy } from '../../autonomy.js';
14
+ import { collectMcpDeclarations, mcpApprovalDigest, parseMcpDeclarationFile } from '../../surfaces/mcp.js';
15
+ import { bindSurfaceOwnershipIdentity, } from '../../surfaces/ownership.js';
16
+ import { observeSiblingGuides, planSiblingProjection, } from '../../surfaces/projection.js';
17
+ import { renderSurfaceSet } from '../../surfaces/render.js';
18
+ import { stripDevelopmentMarkers } from '../../surfaces/development-markers.js';
19
+ import { renderConsumerInstructions } from '../../surfaces/consumer-instructions.js';
20
+ import { expandRestrictions } from '../../surfaces/restrictions.js';
21
+ import { fileFingerprint, verifiedDigest } from '../../environment-exec/verified-digest.js';
22
+ import { SHIPPED_SETUP_SKILL, extendInspectedSkillStandingCosts, inspectRenderedSkillSet } from '../../surfaces/skill-contract.js';
23
+ import { readAnchoredFile, readAnchoredEntry, readAnchoredBatches } from '../../generated-environment/transaction.js';
24
+ import { ENVIRONMENT_CODES, errorCause, snapshotGeneratedJson, RefusalError } from '../contract.js';
25
+ import { SIBLING_DEFAULT_GUIDES } from '../../schema.js';
26
+ import { checkoutId, cockpitId, stackId } from '../identity.js';
27
+ import { resolveLifecycleStage } from '../lifecycle-stage.js';
28
+ import { observeConfiguration, selectStateRoot } from '../state.js';
29
+ import { TESTING_GUIDE_REGISTRY, createGateRegistry, renderTierGuide } from '../../testing/gates.js';
30
+ import { projectNativeHooks } from '../../testing/hooks.js';
31
+ import { refusalOutcome as refuse } from '../../refusal.js';
32
+ import { CODEX_PROJECT_DOC_BYTES, MAX_GENERATED_BYTES, MAX_OWNED_FILES, MAX_SURFACE_FILE_BYTES, REFERENCE_ASSETS, RESTRICTION_ASSETS, SURFACE_ROOT, WORK_ASSETS, activeClaudeImports, compareCodeUnits, guideBytesExceeded, guideCapExceeded, guideLines, readSurfaceAsset, sha256, siblingGuideMissing, siblingRootsCurrent, skillDescription, sourceIdentity, } from './shared.js';
33
+ import { GUIDE_LOAD_CHARACTERIZATIONS, alwaysLoadedRule, bindNativeGrantsToMcp, claudePluginManifest, emptyGeneration, expectedClaudePluginOutputs, expectedSurfaceOutputs, nativeFragmentSet, nativeGrantFragmentSet, nativeGrantPaths, nativeMcpPaths, nativeSources, openCodeReadDenials, } from './surface-outputs.js';
34
+ import { executeCustomCapability, projectExplicitNativeHooks } from './custom-surface.js';
35
+ export async function prepareSurfaces(prepared, priorOwned, adapters, manifest) {
36
+ const config = prepared.registry.config;
37
+ // Only the listed tools are rendered and drift-checked; an unlisted tool's files become stale.
38
+ const tools = listedSurfaceTargets(config.surfaces.tools);
39
+ const guardedTools = GUARD_SURFACE_TARGETS.filter(target => tools.includes(target));
40
+ const selected = config.sibling.filter(row => row.mount === 'always');
41
+ const requiredSlugs = new Set([...selected.map(row => row.slug), ...prepared.registry.mcpSources.map(row => row.slug)]);
42
+ const siblingRoots = new Map();
43
+ const sourceRoots = [];
44
+ if (!await siblingRootsCurrent(prepared.siblingRoots))
45
+ return refuse('GENERATED_SIBLING_ROOT_INVALID', 'a sibling directory changed after its root authority was authenticated', 'sibling');
46
+ try {
47
+ for (const sibling of config.sibling.filter(row => requiredSlugs.has(row.slug))) {
48
+ const authority = prepared.siblingRoots.siblings.get(sibling.slug);
49
+ if (!authority)
50
+ throw new RefusalError('root');
51
+ const root = authority.root;
52
+ const status = await lstat(root);
53
+ if (!status.isDirectory() || status.isSymbolicLink())
54
+ throw new RefusalError('root');
55
+ siblingRoots.set(sibling.slug, root);
56
+ sourceRoots.push({ slug: sibling.slug, root, identity: authority.identity });
57
+ }
58
+ }
59
+ catch (error) {
60
+ return refuse('SURFACE_SOURCE_UNAVAILABLE', `an active sibling checkout could not be authenticated: ${errorCause(error)}`, 'surfaces');
61
+ }
62
+ // A root guide that projection also writes would become both an authenticated input and an
63
+ // owned output. Native harness loading already reaches these files, so a copy adds no rule.
64
+ const rootInstructionOutputs = new Set(['AGENTS.md', 'CLAUDE.md', 'opencode.jsonc']);
65
+ const projectionGuides = (sibling) => siblingRoots.get(sibling.slug) === prepared.root
66
+ ? sibling.guides.filter(guide => !rootInstructionOutputs.has(guide))
67
+ : sibling.guides;
68
+ const guideRequests = selected.flatMap(sibling => projectionGuides(sibling).map(guide => ({
69
+ slug: sibling.slug, root: siblingRoots.get(sibling.slug), sourcePath: resolve(siblingRoots.get(sibling.slug), guide),
70
+ }))).sort((left, right) => compareCodeUnits(left.sourcePath, right.sourcePath));
71
+ const observedGuides = guideRequests.length === 0
72
+ ? { kind: 'complete', observations: [], missing: [] }
73
+ : await observeSiblingGuides({ allowedRoots: selected.map(row => siblingRoots.get(row.slug)), requests: guideRequests });
74
+ if (observedGuides.kind !== 'complete')
75
+ return refuse(observedGuides.diagnostic.code, observedGuides.diagnostic.message, observedGuides.diagnostic.target);
76
+ for (const sibling of selected)
77
+ for (const guide of projectionGuides(sibling)) {
78
+ const sourcePath = resolve(siblingRoots.get(sibling.slug), guide);
79
+ if (!SIBLING_DEFAULT_GUIDES.includes(guide) && observedGuides.missing.includes(sourcePath))
80
+ return siblingGuideMissing(sibling.slug, guide, sourcePath);
81
+ }
82
+ const projectionInput = {
83
+ canonicalMainCheckout: prepared.root,
84
+ siblingsRoot: config.cockpit.siblings_root,
85
+ activeProfiles: [],
86
+ guideCapLines: config.cockpit.guide_cap,
87
+ siblings: config.sibling.map(row => ({ slug: row.slug, mount: row.mount, directoryAuthority: row.dir, guides: [...projectionGuides(row)] })),
88
+ mountedPaths: selected.map(row => ({ slug: row.slug, path: siblingRoots.get(row.slug) })),
89
+ directoryGrants: selected.flatMap(row => tools.map(target => ({ slug: row.slug, target, path: siblingRoots.get(row.slug) }))),
90
+ guideObservations: observedGuides.observations,
91
+ standingSources: [],
92
+ loadCharacterizations: GUIDE_LOAD_CHARACTERIZATIONS,
93
+ targets: tools,
94
+ };
95
+ let projection = planSiblingProjection(projectionInput);
96
+ if (projection.kind !== 'complete')
97
+ return refuse(projection.diagnostic.code, projection.diagnostic.message, projection.diagnostic.target);
98
+ const evidence = observedGuides.observations.map(row => ({
99
+ kind: 'guide', root: siblingRoots.get(row.slug), path: row.sourcePath,
100
+ sha256: row.contentSha256, identity: row.contentIdentity,
101
+ }));
102
+ const declarations = [];
103
+ for (const source of prepared.registry.mcpSources) {
104
+ const root = siblingRoots.get(source.slug);
105
+ if (!root)
106
+ continue;
107
+ const read = await readAnchoredFile(root, source.path, MAX_GENERATED_BYTES, prepared.node, adapters);
108
+ if (read.kind !== 'complete')
109
+ return refuse('MCP_DECLARATION_INVALID', 'an MCP declaration source is unavailable', source.path);
110
+ let bytes;
111
+ try {
112
+ bytes = new TextDecoder('utf-8', { fatal: true }).decode(read.bytes);
113
+ }
114
+ catch {
115
+ // undecodable bytes are malformed input the caller refuses.
116
+ return refuse('MCP_DECLARATION_INVALID', 'an MCP declaration source is not UTF-8', source.path);
117
+ }
118
+ const parsed = parseMcpDeclarationFile({ bytes, source: { kind: 'sibling', slug: source.slug, path: source.path, sha256: read.sha256 }, workingDirectory: root });
119
+ if (parsed.kind !== 'complete')
120
+ return refuse(parsed.diagnostic.code, parsed.diagnostic.message, source.path);
121
+ declarations.push(...parsed.declarations);
122
+ evidence.push({ kind: 'mcp', root, path: source.path, sha256: read.sha256, identity: sourceIdentity(read.identity) });
123
+ }
124
+ // Under [autonomy] mcp_approve = "agent" every declared server is approved as declared now; under
125
+ // "person" only the digests a person recorded in [mcp.approved] are, and the guard pins that table.
126
+ const approved = effectivePolicy(config).autonomy.mcp_approve === 'agent'
127
+ ? { ...config.mcp.approved, ...Object.fromEntries(declarations.map(row => [row.name, mcpApprovalDigest(row, prepared.root)])) }
128
+ : config.mcp.approved;
129
+ let mcp = collectMcpDeclarations({ cockpitDirectory: prepared.root, declarations, approved, targets: tools });
130
+ if (mcp.kind !== 'complete')
131
+ return refuse(mcp.diagnostic.code, mcp.diagnostic.message, mcp.diagnostic.server);
132
+ const unapprovedMcpServers = mcp.diagnostics.filter(row => row.code === 'MCP_NOT_APPROVED' && row.server !== undefined)
133
+ .map(row => ({ name: row.server, sha256: row.sha256, sources: (row.sources ?? []).map(source => source.kind === 'sibling' ? `${source.slug}:${source.path}` : source.path) }));
134
+ if (mcp.diagnostics.some(row => row.code === 'MCP_APPROVAL_UNKNOWN')) {
135
+ const row = mcp.diagnostics.find(candidate => candidate.code === 'MCP_APPROVAL_UNKNOWN');
136
+ return refuse(row.code, row.message, row.server);
137
+ }
138
+ const assetPaths = [EVALUATOR_ASSET, ...SKILL_ASSETS, ...REFERENCE_ASSETS, ...RESTRICTION_ASSETS, ...WORK_ASSETS];
139
+ const assetReads = await readAnchoredBatches(SURFACE_ROOT, assetPaths.map(path => ({
140
+ path, maxBytes: MAX_SURFACE_FILE_BYTES, action: 'read-file',
141
+ })), prepared.node, adapters);
142
+ if (!assetReads || assetReads.kind !== 'complete' || assetReads.results.length !== assetPaths.length) {
143
+ return refuse('SURFACE_INPUT_INVALID', 'a shipped surface asset could not be authenticated', 'surfaces');
144
+ }
145
+ const assetRows = assetPaths.map((path, index) => readSurfaceAsset(path, assetReads.results[index]));
146
+ if (assetRows.some(row => row === undefined))
147
+ return refuse('SURFACE_INPUT_INVALID', 'a shipped surface asset could not be authenticated', 'surfaces');
148
+ const assets = assetRows;
149
+ const asset = (path) => assets.find(row => row.path === path);
150
+ let browser = null;
151
+ let browserAuthority;
152
+ if (prepared.workEnvironment.browser) {
153
+ const checkout = prepared.runtimeCheckout;
154
+ if (!checkout)
155
+ return refuse('SURFACE_INPUT_INVALID', 'browser checkout identity is unavailable', prepared.configPath);
156
+ const browserConfiguration = prepared.workEnvironment.browser;
157
+ const playwright = declarations.filter(row => row.name === 'playwright');
158
+ if (playwright.length !== 1) {
159
+ return refuse('MCP_DECLARATION_INVALID', `[browser] requires exactly one MCP server named playwright, and the declared MCP sources hold ${playwright.length}`, 'playwright', 'declare one playwright server in a JSON file with an mcpServers object, listed in a sibling\'s derives.mcp_sources, then run npx --no cockpit sync --config <your cockpit.toml>');
160
+ }
161
+ const playwrightDigest = mcpApprovalDigest(playwright[0], prepared.root);
162
+ if (config.mcp.approved.playwright !== playwrightDigest) {
163
+ return refuse('MCP_DECLARATION_INVALID', `[browser] requires the playwright MCP server to be approved at its current declaration (sha256 ${playwrightDigest})`, 'playwright', 'review the playwright declaration, set playwright to the sha256 in this message under [mcp.approved], then run npx --no cockpit sync --config <your cockpit.toml>');
164
+ }
165
+ if (process.platform !== 'darwin' && process.platform !== 'linux') {
166
+ return refuse('SURFACE_INPUT_INVALID', 'browser posture state is unavailable on this platform', process.platform);
167
+ }
168
+ const observedAt = new Date().toISOString();
169
+ const currentStack = stackId({ cockpitId: checkout.cockpitId, checkoutId: checkout.checkoutId, variant: { kind: 'shared' } });
170
+ const configuration = (await observeConfiguration({ path: prepared.configPath, stackId: currentStack, observedAt }))
171
+ .find(({ code }) => code === ENVIRONMENT_CODES.CONFIG_STALE);
172
+ const namespace = await selectStateRoot({
173
+ platform: process.platform,
174
+ home: process.env.HOME,
175
+ xdgStateHome: process.env.XDG_STATE_HOME,
176
+ checkoutRoots: [checkout.currentCheckoutPath, checkout.mainCheckoutPath],
177
+ cockpitId: checkout.cockpitId,
178
+ });
179
+ if (!configuration) {
180
+ return refuse('SURFACE_INPUT_INVALID', 'browser posture authority could not be observed', prepared.configPath);
181
+ }
182
+ if (namespace.kind !== 'complete')
183
+ return namespace.kind === 'refusal'
184
+ ? refuse('SURFACE_INPUT_INVALID', namespace.message, namespace.target)
185
+ : refuse('SURFACE_INPUT_INVALID', `browser posture state is ${namespace.kind}`, prepared.root);
186
+ try {
187
+ const authority = await resolveObservationPostures({
188
+ configuration,
189
+ cockpitId: checkout.cockpitId,
190
+ checkoutPath: checkout.currentCheckoutPath,
191
+ variant: { kind: 'shared' },
192
+ });
193
+ const stage = await resolveLifecycleStage({ namespace: namespace.value, stackId: currentStack, observedAt });
194
+ if (stage.kind !== 'complete')
195
+ return refuse('SURFACE_INPUT_INVALID', 'browser lifecycle authority could not be observed', prepared.root);
196
+ const configuredRuntime = resolveBrowserRuntime({
197
+ ...browserConfiguration,
198
+ postures: { authorities: [authority], stages: [stage.authority] },
199
+ }, namespace.value);
200
+ const runtime = configuredRuntime;
201
+ const browserMcp = playwright[0];
202
+ const browserSkill = asset('skills/cockpit-browser.skill.md');
203
+ browser = renderBrowserSurfaces({
204
+ runtime,
205
+ cockpitDirectory: prepared.root,
206
+ harnesses: browserConfiguration.harnesses.flatMap(row => isGuardSurfaceTarget(row.target)
207
+ ? [{ target: row.target, version: row.version }] : []),
208
+ mcp: browserMcp,
209
+ approved: true,
210
+ testPackage: null,
211
+ skillTemplate: browserSkill.bytes,
212
+ });
213
+ const browserTargets = new Set(browser.mcp.result.harnesses
214
+ .filter(row => row.servers.some(server => server.name === 'playwright'))
215
+ .map(row => row.target));
216
+ const boundMcp = collectMcpDeclarations({
217
+ cockpitDirectory: prepared.root,
218
+ declarations: declarations.flatMap(row => row.name !== 'playwright' ? [row]
219
+ : browserTargets.size ? [browserMcp] : []),
220
+ // The leased endpoint rewrites an approved argument, so the bound server carries its own digest.
221
+ approved: { ...Object.fromEntries(Object.entries(approved).filter(([name]) => name !== 'playwright')),
222
+ ...(browserTargets.size > 0 ? { playwright: mcpApprovalDigest(browserMcp, prepared.root) } : {}) },
223
+ targets: tools,
224
+ });
225
+ if (boundMcp.kind !== 'complete')
226
+ throw new RefusalError(`bound browser MCP declaration was refused: ${boundMcp.diagnostic.code}`);
227
+ mcp = { ...boundMcp, result: { ...boundMcp.result,
228
+ harnesses: boundMcp.result.harnesses.map(row => ({ ...row,
229
+ servers: row.servers.filter(server => server.name !== 'playwright' || browserTargets.has(row.target)),
230
+ })),
231
+ } };
232
+ browserAuthority = {
233
+ runtime,
234
+ namespace: namespace.value,
235
+ mcp: browserMcp,
236
+ skillTemplate: browserSkill,
237
+ };
238
+ }
239
+ catch (error) {
240
+ return refuse('SURFACE_INPUT_INVALID', `browser surface projection was refused: ${errorCause(error)}`, 'browser');
241
+ }
242
+ }
243
+ const testingRegistry = prepared.workEnvironment.testing
244
+ ? createGateRegistry(prepared.workEnvironment.testing.declarations)
245
+ : TESTING_GUIDE_REGISTRY;
246
+ const configuredHarnesses = [...tools, ...config.surfaces.harnesses.map(row => row.name)].sort();
247
+ const testingConfiguration = prepared.workEnvironment.testing;
248
+ const explicitNativeHooks = testingConfiguration?.nativeHookInstall === 'explicit-native'
249
+ ? testingConfiguration.nativeHookPolicy
250
+ ? await projectExplicitNativeHooks(prepared, manifest, adapters, testingConfiguration.nativeHookPolicy, configuredHarnesses)
251
+ : undefined
252
+ : undefined;
253
+ const nativeHooks = testingConfiguration?.nativeHookInstall === 'explicit-native'
254
+ ? explicitNativeHooks?.projection
255
+ : await projectNativeHooks({
256
+ cockpitId: prepared.cockpitId,
257
+ harnesses: configuredHarnesses,
258
+ install: 'inspect',
259
+ });
260
+ if (!nativeHooks)
261
+ return refuse('SURFACE_INPUT_INVALID', 'explicit native hook policy or measurement is unavailable', '.githooks/pre-push');
262
+ if (testingConfiguration?.nativeHookInstall === 'explicit-native' && nativeHooks.kind !== 'complete') {
263
+ return refuse('SURFACE_INPUT_INVALID', nativeHooks.reason, '.githooks/pre-push');
264
+ }
265
+ const coreAssets = new Map(assets.map(row => [row.path, row]));
266
+ const coreDeclarations = coreSurfaceDeclarations(coreAssets, tools);
267
+ if (!coreDeclarations)
268
+ return refuse('SURFACE_INPUT_INVALID', 'the shipped core surface declarations could not be read', 'skills/collection.json');
269
+ const shippedSkills = coreDeclarations.filter(declaration => declaration.feature === 'skills').map(declaration => declaration.name);
270
+ const merge = mergeSurfaceDeclarations(coreDeclarations, config.surfaces.overlays);
271
+ if (merge.kind !== 'complete')
272
+ return refuse(merge.code, merge.message, 'surfaces');
273
+ const mergedSources = [];
274
+ for (const source of merge.surfaces.flatMap(surface => surface.sources)) {
275
+ if (!source.path.startsWith('rules/')) {
276
+ mergedSources.push({ path: source.path, bytes: source.bytes });
277
+ continue;
278
+ }
279
+ const rule = alwaysLoadedRule(source.bytes);
280
+ if (rule.kind === 'refusal')
281
+ return refuse('SURFACE_RULE_SCOPE_UNSUPPORTED', `rules overlay ${source.path} ${rule.reason}`, source.path, 'remove `globs` from the rule frontmatter, or narrow the rule text itself to the files it is about, then run npx --no cockpit sync --config <your cockpit.toml>');
282
+ mergedSources.push({ path: source.path, bytes: rule.bytes });
283
+ }
284
+ // Every sibling directory relative to the cockpit root, whether or not a grant names it.
285
+ const siblingDirectories = [...prepared.siblingRoots.siblings].map(([slug, sibling]) => ({ slug, path: relative(prepared.root, sibling.root) }));
286
+ const generatedNativeSources = nativeSources(mcp, projection.grants, siblingDirectories, prepared.registry.secretPaths);
287
+ const generatedSources = [...mergedSources, ...generatedNativeSources]
288
+ .sort((left, right) => compareCodeUnits(left.path, right.path));
289
+ if (new Set(generatedSources.map(source => source.path)).size !== generatedSources.length) {
290
+ return refuse('SURFACE_PATH_CONFLICT', 'surface source paths collide with generated native sources', 'surfaces');
291
+ }
292
+ const features = SURFACE_FEATURES.filter(feature => generatedSources.some(source => source.path === `${feature}.jsonc`
293
+ || source.path.startsWith(`${feature}/`) || feature === 'rules' && source.path.startsWith('rules/')
294
+ || feature === 'subagents' && source.path.startsWith('subagents/') || feature === 'skills' && source.path.startsWith('skills/')));
295
+ const bundleEntries = [];
296
+ const expected = expectedSurfaceOutputs(tools, features, generatedSources);
297
+ const bundleRequest = config.surfaces.bundles.includes('claudecode-plugin') ? (() => {
298
+ const bundleFeatures = features.filter(feature => feature === 'mcp' || feature === 'subagents'
299
+ || feature === 'skills' || feature === 'hooks');
300
+ const bundleSources = generatedSources.filter(source => source.path === 'mcp.jsonc' || source.path === 'hooks.jsonc'
301
+ || source.path.startsWith('subagents/') || source.path.startsWith('skills/'));
302
+ return {
303
+ format: 'claudecode-plugin',
304
+ features: bundleFeatures,
305
+ sources: bundleSources,
306
+ expectedOutputs: expectedClaudePluginOutputs(bundleFeatures, bundleSources),
307
+ };
308
+ })() : undefined;
309
+ const estate = generatedSources.length === 0 ? undefined : await generateSurfaceEstate({
310
+ batch: { targets: tools, features, sources: generatedSources, expectedOutputs: expected },
311
+ ...(bundleRequest ? { bundle: bundleRequest } : {}),
312
+ });
313
+ if (estate && estate.kind !== 'complete') {
314
+ // The generator prints its warnings before the error that stopped it, then a stack; lead with
315
+ // that error and drop the stack, which names only the worker's scratch files.
316
+ const failure = estate.diagnostics[0];
317
+ const said = failure?.stderr?.split('\n').map(line => line.trim().replace(/^Error: /, ''))
318
+ .filter(line => line.length > 0 && !/^at /.test(line)) ?? [];
319
+ const cause = said.at(-1);
320
+ return refuse('SURFACE_GENERATION_FAILED', [cause, failure?.message ?? 'built-in surface generation was refused',
321
+ ...said.length > 1 ? [`generator output: ${said.slice(0, -1).join(' ')}`] : []]
322
+ .filter((part) => part !== undefined).join('; '), failure?.target);
323
+ }
324
+ const generated = estate?.kind === 'complete' && estate.batch ? estate.batch : emptyGeneration();
325
+ if (config.surfaces.bundles.includes('claudecode-plugin')) {
326
+ const bundle = estate?.kind === 'complete' ? estate.bundle : undefined;
327
+ if (!bundle)
328
+ return refuse('SURFACE_GENERATION_FAILED', 'Claude plugin bundle generation was refused', 'claudecode-plugin');
329
+ const manifest = claudePluginManifest();
330
+ // A bundled skill must carry the references its SKILL.md tells the agent to read.
331
+ const references = bundle.files.some(file => file.path === `skills/${SHIPPED_SETUP_SKILL}/SKILL.md`)
332
+ ? REFERENCE_ASSETS.map(path => {
333
+ const bytes = stripDevelopmentMarkers(asset(path).bytes);
334
+ return { path: `skills/${SHIPPED_SETUP_SKILL}/${path}`, bytes, sha256: sha256(bytes) };
335
+ }) : [];
336
+ const files = [
337
+ { path: '.claude-plugin/plugin.json', ...manifest },
338
+ ...bundle.files,
339
+ ...references,
340
+ ];
341
+ for (const file of files)
342
+ bundleEntries.push({
343
+ kind: 'file',
344
+ ownership: 'whole-file',
345
+ path: `.cockpit/bundles/claudecode-plugin/${file.path}`,
346
+ bytes: file.bytes,
347
+ sha256: file.sha256,
348
+ harnesses: ['claudecode'],
349
+ scope: 'always',
350
+ capability: 'instructed',
351
+ writer: 'cockpit',
352
+ schemaVersion: 1,
353
+ });
354
+ }
355
+ const skillSources = generatedSources.filter(source => source.path.startsWith('skills/'));
356
+ const skillAsset = (path) => {
357
+ const { bytes, sha256 } = asset(path);
358
+ return { path, bytes, sha256 };
359
+ };
360
+ const skillInspection = await inspectRenderedSkillSet({ sources: skillSources,
361
+ declaredExtensions: config.surfaces.overlays.filter(overlay => overlay.feature === 'skills')
362
+ .flatMap(overlay => overlay.sources.flatMap(source => /^skills\/([^/]+)\/SKILL\.md$/.exec(source.path)?.[1] ?? []))
363
+ .filter(name => !shippedSkills.some(shipped => shipped === name))
364
+ .sort(compareCodeUnits),
365
+ collection: skillAsset('skills/collection.json'), procedures: PROCEDURE_ASSETS.map(skillAsset), generated, targets: tools });
366
+ if (skillInspection.kind !== 'complete') {
367
+ const diagnostic = skillInspection.diagnostics[0];
368
+ return refuse(diagnostic?.code ?? 'SKILL_RENDER_INVALID', diagnostic?.message ?? 'rendered skill set was refused', diagnostic?.target);
369
+ }
370
+ let modulePath;
371
+ try {
372
+ modulePath = await realpath(fileURLToPath(new URL('../../../dist/cli.js', import.meta.url)));
373
+ }
374
+ catch (error) {
375
+ return refuse('GENERATED_EXECUTABLE_UNAVAILABLE', `the installed guard handler is unavailable: ${errorCause(error)}`, 'guard');
376
+ }
377
+ const moduleRead = await readAnchoredFile(dirname(modulePath), basename(modulePath), MAX_GENERATED_BYTES, prepared.node, adapters);
378
+ if (moduleRead.kind !== 'complete')
379
+ return refuse('GENERATED_EXECUTABLE_UNAVAILABLE', 'the installed guard handler is unavailable', 'guard');
380
+ const rootStat = await lstat(prepared.root);
381
+ const configStat = await lstat(prepared.configPath);
382
+ // Only a fingerprint this process hashed to the authenticated digest may stand in for a re-hash.
383
+ const nodeFingerprint = fileFingerprint(prepared.node.canonicalPath, await lstat(prepared.node.canonicalPath));
384
+ if (verifiedDigest(nodeFingerprint) !== prepared.node.sha256)
385
+ return refuse('GENERATED_EXECUTABLE_UNAVAILABLE', 'the Node executable changed while sync was running', 'guard', 'run `npx --no cockpit sync --config <your cockpit.toml>` again in a terminal');
386
+ const guardHandler = { root: prepared.root,
387
+ rootDevice: String(rootStat.dev), rootInode: String(rootStat.ino),
388
+ configPath: prepared.configPath, configSha256: prepared.sourceSha256,
389
+ configDevice: String(configStat.dev), configInode: String(configStat.ino),
390
+ node: prepared.node.canonicalPath, nodeSha256: prepared.node.sha256, nodeFingerprint,
391
+ module: modulePath, moduleSha256: moduleRead.sha256 };
392
+ const restrictions = expandRestrictions({
393
+ canonicalProjectRoot: prepared.root,
394
+ allowedProjectRoots: [...new Set([prepared.root, ...selected.map(row => siblingRoots.get(row.slug))])].sort(compareCodeUnits),
395
+ guardTemplate: asset(RESTRICTION_ASSETS[0]).bytes, guardPath: '.cockpit/guards/bash-guard.sh',
396
+ instances: prepared.registry.secretPaths.filter(row => siblingRoots.has(row.slug)).map(row => ({
397
+ kind: 'instance', name: `${row.slug}-${sha256(row.path).slice(0, 12)}`, root: siblingRoots.get(row.slug), path: row.path,
398
+ })),
399
+ policy: { hosted_db_hosts: config.guards.hosted_db_hosts, protected_branches: config.guards.protected_branches,
400
+ readonly: { kind: 'agent', name: 'evaluator' } },
401
+ adapters: RESTRICTION_ASSETS.slice(1).map(path => JSON.parse(asset(path).bytes)),
402
+ configFile: relative(prepared.root, prepared.configPath),
403
+ });
404
+ if (restrictions.kind !== 'complete')
405
+ return refuse(restrictions.code, restrictions.message, 'surfaces');
406
+ const listedRestrictions = { ...restrictions,
407
+ targets: restrictions.targets.filter(row => tools.includes(row.target)) };
408
+ const customReceipts = [];
409
+ const customSources = [];
410
+ const customSelftest = [];
411
+ for (const harness of config.surfaces.harnesses)
412
+ for (const capability of harness.capabilities) {
413
+ if (capability.state === 'unavailable')
414
+ continue;
415
+ const sources = merge.surfaces.filter(surface => surface.feature === capability.feature).flatMap(surface => surface.sources)
416
+ .map(source => ({ path: source.path, bytes: source.bytes })).sort((left, right) => compareCodeUnits(left.path, right.path));
417
+ const result = await executeCustomCapability(prepared, adapters, harness.name, capability, sources);
418
+ if (!result)
419
+ return refuse('SURFACE_CUSTOM_REFUSED', 'a custom harness capability did not return an authenticated complete file set', `${harness.name}:${capability.feature}`);
420
+ customReceipts.push(result.receipt);
421
+ customSources.push(...result.sources);
422
+ const { harness: name, feature, state, measuredOn, module, export: exportName, sourceSha256, resultSha256 } = result.receipt;
423
+ customSelftest.push({ harness: name, feature, state, measuredOn, module, export: exportName, sourceSha256, resultSha256, sources,
424
+ ...(result.evidence ? { evidence: result.evidence } : {}) });
425
+ }
426
+ const mcpTargets = mcp.result.harnesses.filter(row => row.servers.length > 0).map(row => row.target);
427
+ const grantTargets = [...new Set(projection.grants.map(row => row.target))];
428
+ const generatedNativeMcp = nativeFragmentSet(generated, mcp.result, nativeMcpPaths(mcpTargets));
429
+ const nativeGrants = nativeGrantFragmentSet(generated, projection.grants, nativeGrantPaths(grantTargets), openCodeReadDenials(siblingDirectories, prepared.registry.secretPaths));
430
+ const nativeMcp = generatedNativeMcp && nativeGrants ? bindNativeGrantsToMcp(generatedNativeMcp, nativeGrants) : undefined;
431
+ if (!nativeMcp || !nativeGrants)
432
+ return refuse('SURFACE_INPUT_INVALID', 'native surface fragments could not be bound', 'surfaces');
433
+ const browserSkillRendered = browser?.files.some(file => file.path.endsWith('/skills/cockpit-browser/SKILL.md')) === true;
434
+ const instructions = renderConsumerInstructions({
435
+ name: config.cockpit.name,
436
+ configFile: relative(prepared.root, prepared.configPath),
437
+ siblings: config.sibling.map(row => ({ dir: row.dir, purpose: row.purpose, acquire: row.acquire })),
438
+ hasStack: config.environment.compose_files.length > 0,
439
+ guards: { declaredDatabaseHosts: config.guards.hosted_db_hosts, declaredVendorHosts: config.guards.vendor_endpoint_hosts,
440
+ pushRefusedBranches: config.guards.protected_branches },
441
+ skills: [
442
+ ...merge.surfaces.filter(surface => surface.feature === 'skills').flatMap(surface => surface.sources
443
+ .filter(source => /^skills\/[^/]+\/SKILL\.md$/.test(source.path))
444
+ .map(source => ({ name: source.path.split('/')[1], description: skillDescription(source.bytes) }))),
445
+ ...(browserSkillRendered ? [{ name: 'cockpit-browser', description: skillDescription(asset('skills/cockpit-browser.skill.md').bytes) }] : []),
446
+ ],
447
+ });
448
+ const generatedRootInstructions = generated.files.filter(file => file.path === 'AGENTS.md');
449
+ const distinctGeneratedRoots = [...new Set(generatedRootInstructions.map(file => file.bytes))];
450
+ if (distinctGeneratedRoots.length > 1)
451
+ return refuse('SURFACE_PATH_CONFLICT', 'generated root instructions diverge across always-loaded harnesses', 'AGENTS.md');
452
+ const rootInstructions = distinctGeneratedRoots[0];
453
+ const commonRootInstructions = [rootInstructions?.replace(/\n+$/u, ''), instructions.replace(/\n+$/u, '')]
454
+ .filter((value) => value !== undefined && value.length > 0).join('\n') + '\n';
455
+ projectionInput.standingSources = guardedTools.map(target => ({
456
+ target,
457
+ path: 'AGENTS.md',
458
+ content: commonRootInstructions,
459
+ scope: 'always',
460
+ }));
461
+ projection = planSiblingProjection(projectionInput);
462
+ if (projection.kind !== 'complete')
463
+ return refuse(projection.diagnostic.code, projection.diagnostic.message, projection.diagnostic.target);
464
+ const tierGuide = renderTierGuide(testingRegistry, projectionInput);
465
+ if (tierGuide.kind !== 'complete')
466
+ return tierGuide.exceeded ? guideCapExceeded(tierGuide.exceeded) : refuse('SURFACE_INPUT_INVALID', tierGuide.reason, 'testing-tiers');
467
+ if (!prepared.workEnvironment.testing && tierGuide.bytes !== stripDevelopmentMarkers(asset('references/testing-tiers.md').bytes)) {
468
+ return refuse('SURFACE_INPUT_INVALID', 'the shipped testing tier guide differs from its canonical registry projection', 'references/testing-tiers.md');
469
+ }
470
+ const rendered = renderSurfaceSet({ configuredHarnesses, merge, generated, projection, mcp, restrictions: listedRestrictions, instructions,
471
+ nativeMcp, nativeGrants, browser, browserReference: browser ? asset('references/browser.md').bytes : null,
472
+ skillReferences: REFERENCE_ASSETS.map(path => ({ skill: SHIPPED_SETUP_SKILL, path, bytes: asset(path).bytes })),
473
+ testing: { tierGuide, nativeHooks },
474
+ customHarnesses: config.surfaces.harnesses, customReceipts, sourceEvidence: evidence });
475
+ if (rendered.kind !== 'complete')
476
+ return refuse(rendered.diagnostic.code, rendered.diagnostic.message, rendered.diagnostic.target);
477
+ const adopted = await mergeAdoptedSurfaces(prepared, rendered.entries, configuredHarnesses, adapters);
478
+ if ('status' in adopted)
479
+ return adopted;
480
+ let adoptedEntries = adopted.entries;
481
+ if (adopted.entries.some(entry => entry.path === 'CLAUDE.md')) {
482
+ const existingClaude = await readAnchoredEntry(prepared.root, 'CLAUDE.md', MAX_SURFACE_FILE_BYTES, prepared.node, adapters);
483
+ if (existingClaude.kind === 'invalid')
484
+ return refuse('SURFACE_STATE_CONFLICT', 'the existing Claude guide could not be observed safely', 'CLAUDE.md');
485
+ if (existingClaude.kind === 'file') {
486
+ let bytes;
487
+ try {
488
+ bytes = new TextDecoder('utf-8', { fatal: true }).decode(existingClaude.bytes);
489
+ }
490
+ catch {
491
+ // undecodable bytes are malformed input the caller refuses.
492
+ return refuse('SURFACE_STATE_CONFLICT', 'the existing Claude guide is not UTF-8', 'CLAUDE.md');
493
+ }
494
+ const priorClaude = priorOwned.find(entry => entry.path === 'CLAUDE.md');
495
+ let unmanaged = bytes;
496
+ if (priorClaude?.kind === 'shared-region' && priorClaude.region.format === 'markdown-delimited') {
497
+ const start = bytes.indexOf(priorClaude.region.begin);
498
+ const end = bytes.indexOf(priorClaude.region.end, start + priorClaude.region.begin.length);
499
+ if (start >= 0 && end > start)
500
+ unmanaged = `${bytes.slice(0, start)}${bytes.slice(end + priorClaude.region.end.length)}`;
501
+ }
502
+ const imports = activeClaudeImports(unmanaged);
503
+ if (imports.length > 1) {
504
+ return refuse('SURFACE_STATE_CONFLICT', 'the existing Claude guide contains ambiguous AGENTS.md imports', 'CLAUDE.md');
505
+ }
506
+ if (priorClaude === undefined && bytes.includes('<!-- cockpit:')) {
507
+ return refuse('SURFACE_STATE_CONFLICT', 'the existing Claude guide contains an unowned cockpit marker', 'CLAUDE.md');
508
+ }
509
+ if (imports.length === 1) {
510
+ adoptedEntries = adopted.entries.filter(entry => entry.path !== 'CLAUDE.md');
511
+ }
512
+ }
513
+ }
514
+ const bound = bindSurfaceOwnershipIdentity(adoptedEntries, prepared.cockpitId);
515
+ if (bound.kind !== 'complete')
516
+ return refuse(bound.diagnostic.code, bound.diagnostic.message, bound.diagnostic.target);
517
+ const surfaceEntries = [...bound.entries, ...bundleEntries]
518
+ .sort((left, right) => compareCodeUnits(left.path, right.path));
519
+ const browserSkillFiles = [];
520
+ const renderedBrowserSkills = browser?.files.filter(file => file.path.endsWith('/skills/cockpit-browser/SKILL.md')) ?? [];
521
+ const publishedBrowserSkills = surfaceEntries.filter(entry => entry.path.endsWith('/skills/cockpit-browser/SKILL.md'));
522
+ for (const entry of publishedBrowserSkills) {
523
+ if (entry.kind !== 'file')
524
+ return refuse('SURFACE_INPUT_INVALID', 'the browser skill must be a whole owned file', entry.path);
525
+ for (const target of entry.harnesses) {
526
+ const rendered = renderedBrowserSkills.find(file => file.path === entry.path && file.target === target);
527
+ const bytes = rendered && stripDevelopmentMarkers(rendered.bytes);
528
+ if (!rendered || bytes !== entry.bytes || sha256(bytes) !== entry.sha256) {
529
+ return refuse('SURFACE_INPUT_INVALID', 'the published browser skill differs from its authenticated renderer', entry.path);
530
+ }
531
+ browserSkillFiles.push({ target: rendered.target, path: entry.path, bytes, sha256: entry.sha256 });
532
+ }
533
+ }
534
+ if (renderedBrowserSkills.length !== browserSkillFiles.length
535
+ || renderedBrowserSkills.some(file => !browserSkillFiles.some(published => published.path === file.path && published.target === file.target))) {
536
+ return refuse('SURFACE_INPUT_INVALID', 'the published browser skill inventory is incomplete', 'surfaces');
537
+ }
538
+ const installedSkillInspection = extendInspectedSkillStandingCosts(skillInspection, browserSkillFiles);
539
+ if (installedSkillInspection.kind !== 'complete') {
540
+ const diagnostic = installedSkillInspection.diagnostics[0];
541
+ return refuse(diagnostic.code, diagnostic.message, diagnostic.target ?? 'surfaces');
542
+ }
543
+ const rootGuide = surfaceEntries.find(entry => entry.path === 'AGENTS.md');
544
+ if (!rootGuide || rootGuide.kind !== 'shared-region' || rootGuide.region.format !== 'markdown-delimited') {
545
+ return refuse('SURFACE_INPUT_INVALID', 'the published always-loaded root guide is unavailable', 'AGENTS.md');
546
+ }
547
+ const characterizedGuideCosts = tierGuide.costs.filter((cost) => cost.status !== 'unavailable');
548
+ if (characterizedGuideCosts.length !== tierGuide.costs.length) {
549
+ return refuse('SURFACE_INPUT_INVALID', 'published instruction cost is not characterized', 'AGENTS.md');
550
+ }
551
+ // The root region carries the copied sibling guides, so every tool's start-up cost is that region.
552
+ const rootGuideLines = guideLines(rootGuide.region.bytes);
553
+ const actualGuideCosts = characterizedGuideCosts.map(cost => ({ ...cost, value: rootGuideLines, status: rootGuideLines > cost.cap ? 'exceeded' : 'within_cap' }));
554
+ const rootGuideBytes = Buffer.byteLength(rootGuide.region.bytes);
555
+ if (rootGuideBytes > CODEX_PROJECT_DOC_BYTES)
556
+ return guideBytesExceeded(rootGuideBytes);
557
+ const overCap = actualGuideCosts.filter(cost => cost.status === 'exceeded').sort((left, right) => right.value - left.value)[0];
558
+ if (overCap)
559
+ return guideCapExceeded(overCap);
560
+ if (explicitNativeHooks && !surfaceEntries.some(entry => entry.kind === 'file'
561
+ && entry.path === explicitNativeHooks.source.path && entry.sha256 === explicitNativeHooks.source.sha256)) {
562
+ return refuse('SURFACE_INPUT_INVALID', 'native hook policy must remain an exact desired owned surface', explicitNativeHooks.source.path);
563
+ }
564
+ if (new Set(surfaceEntries.map(entry => entry.path)).size !== surfaceEntries.length) {
565
+ return refuse('SURFACE_PATH_CONFLICT', 'generated surface and bundle paths collide', 'surfaces');
566
+ }
567
+ if (new Set([...prepared.owned, ...surfaceEntries].map(entry => entry.path)).size !== prepared.owned.length + surfaceEntries.length) {
568
+ return refuse('SURFACE_PATH_CONFLICT', 'generated environment and surface paths collide', 'surfaces');
569
+ }
570
+ if (prepared.owned.length + surfaceEntries.length > MAX_OWNED_FILES) {
571
+ return refuse('SURFACE_BOUNDS', 'combined generated ownership exceeds the entry bound', 'surfaces');
572
+ }
573
+ const selftestPayload = {
574
+ schema: 'cockpit-generated-surface-selftest/v1',
575
+ configuredHarnesses,
576
+ declarations: config.surfaces.harnesses,
577
+ entries: surfaceEntries,
578
+ custom: customSelftest.sort((left, right) => compareCodeUnits(`${left.harness}\0${left.feature}`, `${right.harness}\0${right.feature}`)),
579
+ };
580
+ const captured = snapshotGeneratedJson(selftestPayload);
581
+ if (!captured)
582
+ return refuse('SURFACE_BOUNDS', 'authenticated selftest inputs exceed the captured bounds', 'surfaces');
583
+ const payload = { schema: captured.schema, configuredHarnesses: selftestPayload.configuredHarnesses,
584
+ declarations: captured.declarations, entries: surfaceEntries, custom: captured.custom };
585
+ const surfaceSelftest = { ...payload, manifestSha256: sha256(JSON.stringify(payload)) };
586
+ const skillInventory = surfaceEntries.filter((entry) => entry.kind === 'file').flatMap(entry => entry.harnesses.flatMap(target => {
587
+ if (!SURFACE_TARGETS.includes(target))
588
+ return [];
589
+ const prefix = `${harnessFact(target).outputs.skills}/`;
590
+ if (!entry.path.startsWith(prefix) || !/^[^/]+\/SKILL\.md$/.test(entry.path.slice(prefix.length)))
591
+ return [];
592
+ return [{ target, path: entry.path, sha256: entry.sha256 }];
593
+ })).sort((left, right) => compareCodeUnits(`${left.target}\0${left.path}`, `${right.target}\0${right.path}`));
594
+ const installedSkillTargets = new Set(skillInventory.map(({ target }) => target));
595
+ const installedSkillCosts = installedSkillInspection.costs.filter(({ target }) => installedSkillTargets.has(target));
596
+ if (installedSkillCosts.length !== installedSkillTargets.size) {
597
+ return refuse('SURFACE_INPUT_INVALID', 'a published skill target has no admitted standing-cost measurement', 'surfaces');
598
+ }
599
+ const surfaceHealth = {
600
+ observedAt: prepared.producedAt,
601
+ grants: projection.grants.map(grant => ({ ...grant })),
602
+ nativeGrants,
603
+ guideCosts: actualGuideCosts,
604
+ skillCosts: installedSkillCosts,
605
+ skillInventorySha256: sha256(JSON.stringify(skillInventory)),
606
+ skillInventory,
607
+ skills: installedSkillInspection.skills,
608
+ skillCollectionSha256: skillInspection.collection.source.sha256,
609
+ };
610
+ const customSurfacePaths = new Set(customReceipts.flatMap(receipt => receipt.files.map(file => file.path)));
611
+ const compactSurfacePaths = new Set(generated.files.map(file => file.path).filter(path => !customSurfacePaths.has(path)));
612
+ const evaluator = asset(EVALUATOR_ASSET);
613
+ const workApplication = {
614
+ workEnvironment: prepared.workEnvironment,
615
+ evaluator: {
616
+ path: resolve(SURFACE_ROOT, evaluator.path),
617
+ bytes: evaluator.bytes,
618
+ sha256: evaluator.sha256,
619
+ identity: evaluator.identity,
620
+ restriction: 'instructed',
621
+ },
622
+ installedSkills: surfaceEntries.filter((entry) => entry.kind === 'file' && /^(?:\.agents|\.claude|\.opencode)\/skills\/[^/]+\/SKILL\.md$/.test(entry.path))
623
+ .map(({ path, sha256: digest }) => ({ path, sha256: digest }))
624
+ .sort((left, right) => compareCodeUnits(left.path, right.path)),
625
+ ...(browserAuthority ? { browser: browserAuthority } : {}),
626
+ testing: { registry: testingRegistry },
627
+ };
628
+ const openCodeGrantRoots = projection.grants.filter(grant => grant.target === 'opencode')
629
+ .map(grant => ({ slug: grant.slug, root: siblingRoots.get(grant.slug) }));
630
+ // The file's bytes become part of an OpenCode pattern: a wildcard would widen the grant and a
631
+ // quote or backslash would change the JSON around it.
632
+ const unsafeGrant = openCodeGrantRoots.find(row => row.root === undefined || /[*?"\\\x00-\x1f\x7f]/.test(row.root));
633
+ if (unsafeGrant)
634
+ return refuse('SURFACE_INPUT_INVALID', `the directory of sibling ${unsafeGrant.slug} contains *, ?, a quote, a backslash or a control character, which OpenCode would read as part of its access pattern; move the checkout to a plain path`, unsafeGrant.slug);
635
+ return { ...prepared, guardHandler, openCodeGrantRoots, unapprovedMcpServers,
636
+ surfaces: surfaceEntries, compactSurfacePaths, configuredHarnesses,
637
+ activeSurfaceOverrides: merge.overrides, changedOverrideOriginals: merge.findings,
638
+ surfaceSelftest,
639
+ surfaceHealth,
640
+ guideSources: {
641
+ requests: guideRequests,
642
+ observations: observedGuides.observations.map(row => ({ ...row }))
643
+ .sort((left, right) => compareCodeUnits(left.sourcePath, right.sourcePath)),
644
+ missing: [...observedGuides.missing].sort(compareCodeUnits),
645
+ },
646
+ sourceEvidence: evidence, sourceRoots, customSources,
647
+ adoptedSources: adopted.sources,
648
+ ...(explicitNativeHooks ? { nativeHookPolicySource: explicitNativeHooks.source } : {}),
649
+ surfaceAssets: assets.map(({ path, sha256: digest, identity }) => ({ path, sha256: digest, identity })),
650
+ workApplication,
651
+ warnings: [...rendered.warnings] };
652
+ }
653
+ const ADOPTED_HARNESS = Object.freeze({ '.claude/settings.json': 'claudecode', '.codex/config.toml': 'codexcli', '.codex/hooks.json': 'codexcli' });
654
+ /**
655
+ * Replace each adopted harness settings file in the rendered set with the merge of the team's
656
+ * adopted copy and cockpit's rendering. The copy is read through the same anchored reader as every
657
+ * other input and recorded, so publication refuses if it changes before the write.
658
+ */
659
+ async function mergeAdoptedSurfaces(prepared, entries, configuredHarnesses, adapters) {
660
+ const rows = prepared.registry.config.surfaces.adopted;
661
+ if (rows.length === 0)
662
+ return { entries, sources: [] };
663
+ const merged = [...entries];
664
+ const sources = [];
665
+ for (const row of rows) {
666
+ const read = await readAnchoredFile(prepared.root, row.source, MAX_SURFACE_FILE_BYTES, prepared.node, adapters);
667
+ if (read.kind !== 'complete')
668
+ return refuse('SURFACE_ADOPTION_REFUSED', `the adopted copy of ${row.path} is ${read.kind === 'missing' ? 'missing' : 'not a bounded regular file'}; restore ${row.source} or remove its [[surfaces.adopted]] row`, row.source);
669
+ let bytes;
670
+ try {
671
+ bytes = new TextDecoder('utf-8', { fatal: true }).decode(read.bytes);
672
+ }
673
+ catch {
674
+ // undecodable bytes are malformed input the caller refuses.
675
+ return refuse('SURFACE_ADOPTION_REFUSED', `the adopted copy ${row.source} is not UTF-8 text`, row.source);
676
+ }
677
+ const index = merged.findIndex(entry => entry.path === row.path);
678
+ const current = index < 0 ? undefined : merged[index];
679
+ if (current && current.kind !== 'file')
680
+ return refuse('SURFACE_ADOPTION_REFUSED', `cockpit does not render ${row.path} as a whole file`, row.path);
681
+ const result = mergeAdoptedSurface(row.path, bytes, current?.bytes);
682
+ if (result.kind !== 'complete')
683
+ return refuse(result.diagnostic.code, result.diagnostic.message, result.diagnostic.target);
684
+ const harness = ADOPTED_HARNESS[row.path];
685
+ if (!configuredHarnesses.includes(harness))
686
+ return refuse('SURFACE_ADOPTION_REFUSED', `${row.path} belongs to ${harness}, which this cockpit does not configure`, row.path);
687
+ const entry = current
688
+ ? { ...current, bytes: result.bytes, sha256: sha256(result.bytes) }
689
+ : { kind: 'file', path: row.path, harnesses: [harness], scope: 'always', capability: 'instructed', writer: 'cockpit',
690
+ schemaVersion: 1, ownership: 'whole-file', bytes: result.bytes, sha256: sha256(result.bytes) };
691
+ if (index < 0)
692
+ merged.push(entry);
693
+ else
694
+ merged[index] = entry;
695
+ sources.push({ path: row.source, sha256: read.sha256, identity: read.identity });
696
+ }
697
+ merged.sort((left, right) => compareCodeUnits(left.path, right.path));
698
+ return { entries: merged, sources };
699
+ }