@b00dea/cockpit 0.0.0-bootstrap.0 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (363) hide show
  1. package/CHANGELOG.md +375 -0
  2. package/LICENSE +21 -0
  3. package/README.md +21 -1
  4. package/bin/cockpit.mjs +33 -0
  5. package/cockpit.schema.json +2574 -0
  6. package/dist/autonomy.d.ts +44 -0
  7. package/dist/autonomy.js +156 -0
  8. package/dist/bounded-read.d.ts +14 -0
  9. package/dist/bounded-read.js +42 -0
  10. package/dist/browser/commands.d.ts +62 -0
  11. package/dist/browser/commands.js +349 -0
  12. package/dist/browser/package-paths.d.ts +1 -0
  13. package/dist/browser/package-paths.js +6 -0
  14. package/dist/browser/proxy.d.ts +22 -0
  15. package/dist/browser/proxy.js +37 -0
  16. package/dist/browser/runtime.d.ts +117 -0
  17. package/dist/browser/runtime.js +183 -0
  18. package/dist/browser/surfaces.d.ts +141 -0
  19. package/dist/browser/surfaces.js +556 -0
  20. package/dist/browser/ui-gate.d.ts +15 -0
  21. package/dist/browser/ui-gate.js +18 -0
  22. package/dist/cli-guard.d.ts +5 -0
  23. package/dist/cli-guard.js +71 -0
  24. package/dist/cli-main.d.ts +139 -0
  25. package/dist/cli-main.js +1405 -0
  26. package/dist/cli.d.ts +3 -0
  27. package/dist/cli.js +16 -0
  28. package/dist/command-table.d.ts +113 -0
  29. package/dist/command-table.js +512 -0
  30. package/dist/config/load.d.ts +171 -0
  31. package/dist/config/load.js +345 -0
  32. package/dist/config/sibling-toml.d.ts +48 -0
  33. package/dist/config/sibling-toml.js +160 -0
  34. package/dist/config/surface-tools-toml.d.ts +9 -0
  35. package/dist/config/surface-tools-toml.js +37 -0
  36. package/dist/config/verified-write.d.ts +43 -0
  37. package/dist/config/verified-write.js +89 -0
  38. package/dist/diagnostics.d.ts +26 -0
  39. package/dist/diagnostics.js +335 -0
  40. package/dist/emit.d.ts +22 -0
  41. package/dist/emit.js +43 -0
  42. package/dist/entrypoint-kinds.d.ts +3 -0
  43. package/dist/entrypoint-kinds.js +11 -0
  44. package/dist/environment/application/destructive.d.ts +6 -0
  45. package/dist/environment/application/destructive.js +425 -0
  46. package/dist/environment/application/doctor.d.ts +89 -0
  47. package/dist/environment/application/doctor.js +235 -0
  48. package/dist/environment/application/guard.d.ts +43 -0
  49. package/dist/environment/application/guard.js +622 -0
  50. package/dist/environment/application/health.d.ts +134 -0
  51. package/dist/environment/application/health.js +379 -0
  52. package/dist/environment/application/lifecycle-context.d.ts +228 -0
  53. package/dist/environment/application/lifecycle-context.js +154 -0
  54. package/dist/environment/application/lifecycle-state.d.ts +25 -0
  55. package/dist/environment/application/lifecycle-state.js +201 -0
  56. package/dist/environment/application/lifecycle.d.ts +17 -0
  57. package/dist/environment/application/lifecycle.js +729 -0
  58. package/dist/environment/application/logs.d.ts +45 -0
  59. package/dist/environment/application/logs.js +127 -0
  60. package/dist/environment/application/setup.d.ts +129 -0
  61. package/dist/environment/application/setup.js +741 -0
  62. package/dist/environment/application/shared.d.ts +11 -0
  63. package/dist/environment/application/shared.js +61 -0
  64. package/dist/environment/claims.d.ts +389 -0
  65. package/dist/environment/claims.js +1709 -0
  66. package/dist/environment/codex-hook-trust.d.ts +11 -0
  67. package/dist/environment/codex-hook-trust.js +185 -0
  68. package/dist/environment/compose-environment.d.ts +1 -0
  69. package/dist/environment/compose-environment.js +6 -0
  70. package/dist/environment/compose-ownership.d.ts +21 -0
  71. package/dist/environment/compose-ownership.js +117 -0
  72. package/dist/environment/compose-topology.d.ts +117 -0
  73. package/dist/environment/compose-topology.js +413 -0
  74. package/dist/environment/contract.d.ts +489 -0
  75. package/dist/environment/contract.js +761 -0
  76. package/dist/environment/declaration.d.ts +14 -0
  77. package/dist/environment/declaration.js +39 -0
  78. package/dist/environment/destructive-composition.d.ts +29 -0
  79. package/dist/environment/destructive-composition.js +333 -0
  80. package/dist/environment/docker-daemon.d.ts +30 -0
  81. package/dist/environment/docker-daemon.js +78 -0
  82. package/dist/environment/docker-endpoint.d.ts +10 -0
  83. package/dist/environment/docker-endpoint.js +19 -0
  84. package/dist/environment/env-schema-observation.d.ts +46 -0
  85. package/dist/environment/env-schema-observation.js +241 -0
  86. package/dist/environment/evidence.d.ts +136 -0
  87. package/dist/environment/evidence.js +437 -0
  88. package/dist/environment/executable.d.ts +26 -0
  89. package/dist/environment/executable.js +167 -0
  90. package/dist/environment/generated-application/authorities.d.ts +5 -0
  91. package/dist/environment/generated-application/authorities.js +61 -0
  92. package/dist/environment/generated-application/bootstrap-skill.d.ts +16 -0
  93. package/dist/environment/generated-application/bootstrap-skill.js +328 -0
  94. package/dist/environment/generated-application/check.d.ts +13 -0
  95. package/dist/environment/generated-application/check.js +189 -0
  96. package/dist/environment/generated-application/custom-surface.d.ts +26 -0
  97. package/dist/environment/generated-application/custom-surface.js +289 -0
  98. package/dist/environment/generated-application/prepare.d.ts +29 -0
  99. package/dist/environment/generated-application/prepare.js +276 -0
  100. package/dist/environment/generated-application/publication.d.ts +33 -0
  101. package/dist/environment/generated-application/publication.js +547 -0
  102. package/dist/environment/generated-application/shared.d.ts +484 -0
  103. package/dist/environment/generated-application/shared.js +349 -0
  104. package/dist/environment/generated-application/surface-observation.d.ts +4 -0
  105. package/dist/environment/generated-application/surface-observation.js +69 -0
  106. package/dist/environment/generated-application/surface-outputs.d.ts +71 -0
  107. package/dist/environment/generated-application/surface-outputs.js +389 -0
  108. package/dist/environment/generated-application/surfaces.d.ts +3 -0
  109. package/dist/environment/generated-application/surfaces.js +699 -0
  110. package/dist/environment/generated-application/sync.d.ts +6 -0
  111. package/dist/environment/generated-application/sync.js +109 -0
  112. package/dist/environment/git-observation.d.ts +183 -0
  113. package/dist/environment/git-observation.js +544 -0
  114. package/dist/environment/health-composition.d.ts +11 -0
  115. package/dist/environment/health-composition.js +628 -0
  116. package/dist/environment/host-observation.d.ts +56 -0
  117. package/dist/environment/host-observation.js +52 -0
  118. package/dist/environment/identity.d.ts +48 -0
  119. package/dist/environment/identity.js +85 -0
  120. package/dist/environment/init-application.d.ts +308 -0
  121. package/dist/environment/init-application.js +697 -0
  122. package/dist/environment/install-policy-observation.d.ts +71 -0
  123. package/dist/environment/install-policy-observation.js +284 -0
  124. package/dist/environment/install-policy.d.ts +48 -0
  125. package/dist/environment/install-policy.js +96 -0
  126. package/dist/environment/instance.d.ts +33 -0
  127. package/dist/environment/instance.js +122 -0
  128. package/dist/environment/instruction-pointer.d.ts +19 -0
  129. package/dist/environment/instruction-pointer.js +144 -0
  130. package/dist/environment/lifecycle-composition.d.ts +29 -0
  131. package/dist/environment/lifecycle-composition.js +675 -0
  132. package/dist/environment/lifecycle-stage.d.ts +115 -0
  133. package/dist/environment/lifecycle-stage.js +398 -0
  134. package/dist/environment/lifecycle.d.ts +75 -0
  135. package/dist/environment/lifecycle.js +354 -0
  136. package/dist/environment/logs.d.ts +132 -0
  137. package/dist/environment/logs.js +519 -0
  138. package/dist/environment/observations.d.ts +46 -0
  139. package/dist/environment/observations.js +344 -0
  140. package/dist/environment/operator-application.d.ts +50 -0
  141. package/dist/environment/operator-application.js +155 -0
  142. package/dist/environment/ports.d.ts +182 -0
  143. package/dist/environment/ports.js +1205 -0
  144. package/dist/environment/posture-observation.d.ts +68 -0
  145. package/dist/environment/posture-observation.js +253 -0
  146. package/dist/environment/read-composition.d.ts +27 -0
  147. package/dist/environment/read-composition.js +352 -0
  148. package/dist/environment/readiness-composition.d.ts +38 -0
  149. package/dist/environment/readiness-composition.js +130 -0
  150. package/dist/environment/reclaim.d.ts +333 -0
  151. package/dist/environment/reclaim.js +1835 -0
  152. package/dist/environment/repository-identity.d.ts +13 -0
  153. package/dist/environment/repository-identity.js +108 -0
  154. package/dist/environment/runtime-measurement-record.d.ts +38 -0
  155. package/dist/environment/runtime-measurement-record.js +111 -0
  156. package/dist/environment/runtime-measurement.d.ts +27 -0
  157. package/dist/environment/runtime-measurement.js +224 -0
  158. package/dist/environment/runtime-probe-residue.d.ts +43 -0
  159. package/dist/environment/runtime-probe-residue.js +148 -0
  160. package/dist/environment/runtime-probe.d.ts +72 -0
  161. package/dist/environment/runtime-probe.js +725 -0
  162. package/dist/environment/runtime.d.ts +505 -0
  163. package/dist/environment/runtime.js +2335 -0
  164. package/dist/environment/seed.d.ts +16 -0
  165. package/dist/environment/seed.js +43 -0
  166. package/dist/environment/selftest-application.d.ts +86 -0
  167. package/dist/environment/selftest-application.js +713 -0
  168. package/dist/environment/sensitive.d.ts +3 -0
  169. package/dist/environment/sensitive.js +48 -0
  170. package/dist/environment/setup-observation.d.ts +62 -0
  171. package/dist/environment/setup-observation.js +1473 -0
  172. package/dist/environment/setup.d.ts +43 -0
  173. package/dist/environment/setup.js +176 -0
  174. package/dist/environment/sibling-instructions.d.ts +33 -0
  175. package/dist/environment/sibling-instructions.js +224 -0
  176. package/dist/environment/sibling-registration.d.ts +45 -0
  177. package/dist/environment/sibling-registration.js +538 -0
  178. package/dist/environment/state.d.ts +371 -0
  179. package/dist/environment/state.js +2013 -0
  180. package/dist/environment/templates.d.ts +140 -0
  181. package/dist/environment/templates.js +685 -0
  182. package/dist/environment/tool-registration.d.ts +9 -0
  183. package/dist/environment/tool-registration.js +96 -0
  184. package/dist/environment/upgrade-application.d.ts +19 -0
  185. package/dist/environment/upgrade-application.js +175 -0
  186. package/dist/environment-exec/execute.d.ts +102 -0
  187. package/dist/environment-exec/execute.js +1175 -0
  188. package/dist/environment-exec/libexec-program.d.ts +6 -0
  189. package/dist/environment-exec/libexec-program.js +22 -0
  190. package/dist/environment-exec/verified-digest.d.ts +4 -0
  191. package/dist/environment-exec/verified-digest.js +16 -0
  192. package/dist/exact-keys.d.ts +7 -0
  193. package/dist/exact-keys.js +15 -0
  194. package/dist/fix-catalogue.d.ts +32 -0
  195. package/dist/fix-catalogue.js +60 -0
  196. package/dist/freshness.d.ts +29 -0
  197. package/dist/freshness.js +75 -0
  198. package/dist/generated-environment/adopt.d.ts +16 -0
  199. package/dist/generated-environment/adopt.js +275 -0
  200. package/dist/generated-environment/command.d.ts +47 -0
  201. package/dist/generated-environment/command.js +75 -0
  202. package/dist/generated-environment/transaction.d.ts +186 -0
  203. package/dist/generated-environment/transaction.js +1770 -0
  204. package/dist/grading/evaluate.d.ts +71 -0
  205. package/dist/grading/evaluate.js +403 -0
  206. package/dist/grading/grade.d.ts +33 -0
  207. package/dist/grading/grade.js +57 -0
  208. package/dist/guards/evaluator.d.ts +113 -0
  209. package/dist/guards/evaluator.js +1226 -0
  210. package/dist/guards/harness-adapter.d.ts +17 -0
  211. package/dist/guards/harness-adapter.js +57 -0
  212. package/dist/guards/host.d.ts +8 -0
  213. package/dist/guards/host.js +10 -0
  214. package/dist/guards/invocation-parser.d.ts +66 -0
  215. package/dist/guards/invocation-parser.js +595 -0
  216. package/dist/guards/local-identity.d.ts +40 -0
  217. package/dist/guards/local-identity.js +123 -0
  218. package/dist/guards/policy-pin.d.ts +13 -0
  219. package/dist/guards/policy-pin.js +175 -0
  220. package/dist/guards/protected-locations.d.ts +62 -0
  221. package/dist/guards/protected-locations.js +461 -0
  222. package/dist/guards/read-only.d.ts +37 -0
  223. package/dist/guards/read-only.js +315 -0
  224. package/dist/init/command.d.ts +40 -0
  225. package/dist/init/command.js +310 -0
  226. package/dist/init/derive.d.ts +51 -0
  227. package/dist/init/derive.js +715 -0
  228. package/dist/init/draft.d.ts +138 -0
  229. package/dist/init/draft.js +1205 -0
  230. package/dist/init/render.d.ts +35 -0
  231. package/dist/init/render.js +82 -0
  232. package/dist/init/scaffold.d.ts +53 -0
  233. package/dist/init/scaffold.js +121 -0
  234. package/dist/invocation.d.ts +13 -0
  235. package/dist/invocation.js +20 -0
  236. package/dist/package-metadata.d.ts +9 -0
  237. package/dist/package-metadata.js +19 -0
  238. package/dist/readiness.d.ts +53 -0
  239. package/dist/readiness.js +88 -0
  240. package/dist/references/named.d.ts +24 -0
  241. package/dist/references/named.js +27 -0
  242. package/dist/refusal.d.ts +15 -0
  243. package/dist/refusal.js +7 -0
  244. package/dist/registry-derivation.d.ts +31 -0
  245. package/dist/registry-derivation.js +32 -0
  246. package/dist/registry-resolve/command.d.ts +47 -0
  247. package/dist/registry-resolve/command.js +226 -0
  248. package/dist/registry-resolve/registry.d.ts +68 -0
  249. package/dist/registry-resolve/registry.js +292 -0
  250. package/dist/registry-resolve/render-shell.d.ts +18 -0
  251. package/dist/registry-resolve/render-shell.js +36 -0
  252. package/dist/repair.d.ts +29 -0
  253. package/dist/repair.js +8 -0
  254. package/dist/schema-json.d.ts +1 -0
  255. package/dist/schema-json.js +177 -0
  256. package/dist/schema.d.ts +1322 -0
  257. package/dist/schema.js +934 -0
  258. package/dist/setup/command.d.ts +4 -0
  259. package/dist/setup/command.js +4 -0
  260. package/dist/sha256.d.ts +1 -0
  261. package/dist/sha256.js +4 -0
  262. package/dist/surfaces/adoption.d.ts +41 -0
  263. package/dist/surfaces/adoption.js +217 -0
  264. package/dist/surfaces/consumer-instructions.d.ts +20 -0
  265. package/dist/surfaces/consumer-instructions.js +43 -0
  266. package/dist/surfaces/contract.d.ts +42 -0
  267. package/dist/surfaces/contract.js +169 -0
  268. package/dist/surfaces/core-declarations.d.ts +24 -0
  269. package/dist/surfaces/core-declarations.js +79 -0
  270. package/dist/surfaces/development-markers.d.ts +1 -0
  271. package/dist/surfaces/development-markers.js +18 -0
  272. package/dist/surfaces/generator-adapter.d.ts +71 -0
  273. package/dist/surfaces/generator-adapter.js +809 -0
  274. package/dist/surfaces/harness-registry.d.ts +180 -0
  275. package/dist/surfaces/harness-registry.js +117 -0
  276. package/dist/surfaces/mcp.d.ts +95 -0
  277. package/dist/surfaces/mcp.js +186 -0
  278. package/dist/surfaces/merge.d.ts +30 -0
  279. package/dist/surfaces/merge.js +105 -0
  280. package/dist/surfaces/ownership.d.ts +190 -0
  281. package/dist/surfaces/ownership.js +795 -0
  282. package/dist/surfaces/projection.d.ts +136 -0
  283. package/dist/surfaces/projection.js +681 -0
  284. package/dist/surfaces/render.d.ts +118 -0
  285. package/dist/surfaces/render.js +633 -0
  286. package/dist/surfaces/restrictions.d.ts +130 -0
  287. package/dist/surfaces/restrictions.js +447 -0
  288. package/dist/surfaces/selftest.d.ts +162 -0
  289. package/dist/surfaces/selftest.js +393 -0
  290. package/dist/surfaces/skill-contract.d.ts +85 -0
  291. package/dist/surfaces/skill-contract.js +449 -0
  292. package/dist/surfaces/sync.d.ts +57 -0
  293. package/dist/surfaces/sync.js +164 -0
  294. package/dist/team-metadata.d.ts +41 -0
  295. package/dist/team-metadata.js +512 -0
  296. package/dist/testing/gates.d.ts +91 -0
  297. package/dist/testing/gates.js +182 -0
  298. package/dist/testing/hooks.d.ts +59 -0
  299. package/dist/testing/hooks.js +139 -0
  300. package/dist/testing/verdict.d.ts +29 -0
  301. package/dist/testing/verdict.js +62 -0
  302. package/dist/work-orders/application.d.ts +99 -0
  303. package/dist/work-orders/application.js +383 -0
  304. package/dist/work-orders/composition.d.ts +124 -0
  305. package/dist/work-orders/composition.js +1010 -0
  306. package/dist/work-orders/contract.d.ts +61 -0
  307. package/dist/work-orders/contract.js +122 -0
  308. package/dist/work-orders/store.d.ts +158 -0
  309. package/dist/work-orders/store.js +197 -0
  310. package/docs/README.md +65 -0
  311. package/docs/architecture.md +136 -0
  312. package/docs/capability-matrix.md +50 -0
  313. package/docs/cli-reference.md +599 -0
  314. package/docs/command-list.md +51 -0
  315. package/docs/compose-patterns.md +99 -0
  316. package/docs/concepts.md +171 -0
  317. package/docs/configuration-essentials.md +172 -0
  318. package/docs/configuration-reference.md +621 -0
  319. package/docs/environment.md +760 -0
  320. package/docs/extending.md +43 -0
  321. package/docs/faq.md +111 -0
  322. package/docs/getting-started.md +608 -0
  323. package/docs/harness-surfaces.md +433 -0
  324. package/docs/joining-a-team.md +76 -0
  325. package/docs/release-policy.md +49 -0
  326. package/docs/safety-and-autonomy.md +419 -0
  327. package/docs/troubleshooting.md +361 -0
  328. package/docs/tutorial.md +170 -0
  329. package/docs/work-and-browser.md +254 -0
  330. package/libexec/browser-operation.cjs +186 -0
  331. package/libexec/fence-marker.cjs +101 -0
  332. package/libexec/fence-marker.d.cts +24 -0
  333. package/libexec/fenced-group-runner.cjs +77 -0
  334. package/libexec/fenced-supervisor.cjs +137 -0
  335. package/libexec/generated-filesystem.cjs +731 -0
  336. package/libexec/install-filesystem.cjs +225 -0
  337. package/libexec/rulesync-generator.NOTICE.md +1112 -0
  338. package/libexec/rulesync-generator.cjs +743 -0
  339. package/libexec/surface-generator-worker.cjs +266 -0
  340. package/libexec/surface-output-reader.cjs +99 -0
  341. package/libexec/surface-scratch-cleanup.cjs +14 -0
  342. package/libexec/validate-harness-evidence.d.mts +183 -0
  343. package/libexec/validate-harness-evidence.mjs +1312 -0
  344. package/package.json +76 -1
  345. package/surfaces/README.md +33 -0
  346. package/surfaces/agents/evaluator.agent.md +76 -0
  347. package/surfaces/guards/bash-guard.sh +48 -0
  348. package/surfaces/guards/claude.adapter.json +25 -0
  349. package/surfaces/guards/codex.adapter.json +24 -0
  350. package/surfaces/guards/opencode.adapter.json +25 -0
  351. package/surfaces/references/browser.md +63 -0
  352. package/surfaces/references/test-data-and-personas.md +84 -0
  353. package/surfaces/references/testing-tiers.md +11 -0
  354. package/surfaces/skills/cockpit-browser.skill.md +54 -0
  355. package/surfaces/skills/cockpit-explain.skill.md +32 -0
  356. package/surfaces/skills/cockpit-fix.skill.md +37 -0
  357. package/surfaces/skills/cockpit-setup.skill.md +39 -0
  358. package/surfaces/skills/cockpit-work.skill.md +34 -0
  359. package/surfaces/skills/collection.json +5 -0
  360. package/surfaces/skills/procedures/explain.md +61 -0
  361. package/surfaces/skills/procedures/fix.md +81 -0
  362. package/surfaces/skills/procedures/setup.md +379 -0
  363. package/surfaces/skills/procedures/work.md +105 -0
@@ -0,0 +1,795 @@
1
+ import { installedPackageMetadata } from "../package-metadata.js";
2
+ import { basename, dirname, isAbsolute, relative, resolve, sep } from "node:path";
3
+ import { isProxy, isUint8Array } from "node:util/types";
4
+ import { sha256Hex } from '../sha256.js';
5
+ import { snapshotGeneratedJson, RefusalError, tryParseJson } from '../environment/contract.js';
6
+ import { publicationCockpitId } from '../environment/identity.js';
7
+ import { safeSurfacePath as safePath } from './contract.js';
8
+ import { SURFACE_TARGETS } from './harness-registry.js';
9
+ import { exactKeys } from '../exact-keys.js';
10
+ // An agent once read "delete the file" as permission and deleted a file without asking. A hand
11
+ // edit is the person's work: say where it belongs, and that discarding it is their call.
12
+ export const SURFACE_HAND_EDIT_FIX = 'keep the edit by moving it into cockpit.toml or a surface overlay, then ask the person who owns the cockpit before you discard the hand edit (undo it or delete the file), and rerun npx --no cockpit sync --config <your cockpit.toml>';
13
+ export const SURFACE_UNRECORDED_FIX = 'ask the person who owns the cockpit whether this file is theirs; move anything they want kept into cockpit.toml or a surface overlay, and only with their approval move the file away or delete it, then rerun npx --no cockpit sync --config <your cockpit.toml>';
14
+ const SHA256 = /^[a-f0-9]{64}$/;
15
+ const HARNESS = /^[a-z0-9][a-z0-9-]{0,127}$/;
16
+ const MAX_ENTRIES = 256;
17
+ const MAX_FILE_BYTES = 65_536;
18
+ const MAX_TOTAL_BYTES = 4_194_304;
19
+ const SHARED_FILE_MAX_BYTES = 16_777_216;
20
+ const MARKDOWN = Object.freeze({
21
+ 'AGENTS.md': { regionId: 'cockpit-instructions', begin: '<!-- cockpit:instructions:start -->', end: '<!-- cockpit:instructions:end -->' },
22
+ 'CLAUDE.md': { regionId: 'cockpit-instructions', begin: '<!-- cockpit:instructions:start -->', end: '<!-- cockpit:instructions:end -->' },
23
+ });
24
+ const OPENCODE_PATH = 'opencode.jsonc';
25
+ const OPENCODE_REGION = 'cockpit-opencode-root-properties';
26
+ const OPENCODE_KEYS = new Set(['$schema', 'instructions', 'mcp', 'permission']);
27
+ const OWNER_ID = /^cockpit:[a-z0-9][a-z0-9-]*:[a-f0-9]{64}$/;
28
+ const OWNER_PREFIX = 'cockpit:owner:';
29
+ function ownerMarkers(id, format) {
30
+ return format === 'markdown'
31
+ ? [`<!-- ${OWNER_PREFIX}${id}:start -->`, `<!-- ${OWNER_PREFIX}${id}:end -->`]
32
+ : [`/* ${OWNER_PREFIX}${id}:start */`, `/* ${OWNER_PREFIX}${id}:end */`];
33
+ }
34
+ function openCodeOwner(regionId) {
35
+ const id = regionId.slice(OPENCODE_REGION.length + 1);
36
+ return regionId.startsWith(`${OPENCODE_REGION}:`) && OWNER_ID.test(id) ? id : undefined;
37
+ }
38
+ function markdownOwner(bytes) {
39
+ const match = /^<!-- cockpit:owner:(cockpit:[a-z0-9][a-z0-9-]*:[a-f0-9]{64}):start -->\n/.exec(bytes);
40
+ if (!match)
41
+ return undefined;
42
+ const [begin, end] = ownerMarkers(match[1], 'markdown');
43
+ return bytes.endsWith(`${end}\n`) && bytes.split(OWNER_PREFIX).length === 3
44
+ && bytes.slice(begin.length + 1, -end.length - 1).endsWith('\n') ? match[1] : undefined;
45
+ }
46
+ export function bindSurfaceOwnershipIdentity(entries, cockpitId) {
47
+ if (!OWNER_ID.test(cockpitId))
48
+ return refusal('SURFACE_OWNERSHIP_INVALID', 'shared ownership requires an exact cockpit identity');
49
+ const bound = [];
50
+ for (const entry of entries) {
51
+ if (entry.kind === 'shared-region' && entry.region.format === 'opencode-json-root-properties') {
52
+ bound.push({ ...entry, regionId: `${OPENCODE_REGION}:${cockpitId}` });
53
+ continue;
54
+ }
55
+ if (entry.kind !== 'shared-region' || entry.region.format !== 'markdown-delimited') {
56
+ bound.push(entry);
57
+ continue;
58
+ }
59
+ if (entry.region.bytes.includes(OWNER_PREFIX))
60
+ return refusal('SURFACE_OWNERSHIP_INVALID', 'generated Markdown body contains a reserved owner marker', entry.path);
61
+ const [begin, end] = ownerMarkers(cockpitId, 'markdown');
62
+ const bytes = `${begin}\n${entry.region.bytes}${end}\n`;
63
+ bound.push({ ...entry, region: { ...entry.region, bytes, sha256: sha256Hex(bytes) } });
64
+ }
65
+ return classifySurfaceOwnership({ configuredHarnesses: [...new Set(entries.flatMap(entry => entry.harnesses))].sort(), entries: bound });
66
+ }
67
+ function object(value) {
68
+ return value !== undefined && value !== null && typeof value === 'object' && !Array.isArray(value);
69
+ }
70
+ function refusal(code, message, target, fix) {
71
+ return { kind: 'refusal', diagnostic: { code, message, ...(target === undefined ? {} : { target }), ...(fix === undefined ? {} : { fix }) } };
72
+ }
73
+ function sortedUniqueStrings(value, allowed) {
74
+ return Array.isArray(value) && value.length > 0 && value.every((item, index) => typeof item === 'string'
75
+ && HARNESS.test(item) && (allowed === undefined || allowed.has(item)) && (index === 0 || value[index - 1] < item));
76
+ }
77
+ function entryBytes(entry) {
78
+ if (entry.kind === 'file')
79
+ return Buffer.byteLength(entry.bytes);
80
+ return entry.region.format === 'markdown-delimited' ? Buffer.byteLength(entry.region.bytes)
81
+ : entry.region.members.reduce((total, member) => total + Buffer.byteLength(member.key) + Buffer.byteLength(member.valueBytes), 0);
82
+ }
83
+ function captureEntry(value, configured) {
84
+ if (!object(value) || typeof value.kind !== 'string' || typeof value.path !== 'string' || !safePath(value.path)
85
+ || !sortedUniqueStrings(value.harnesses, configured) || value.writer !== 'cockpit' || value.schemaVersion !== 1
86
+ || value.scope !== 'always' && value.scope !== 'scoped' || value.capability !== 'enforced' && value.capability !== 'instructed')
87
+ return undefined;
88
+ if (value.kind === 'file') {
89
+ if (!exactKeys(value, ['kind', 'path', 'harnesses', 'scope', 'capability', 'writer', 'schemaVersion', 'ownership', 'bytes', 'sha256'])
90
+ || value.ownership !== 'whole-file' || typeof value.bytes !== 'string' || Buffer.byteLength(value.bytes) > MAX_FILE_BYTES
91
+ || typeof value.sha256 !== 'string' || !SHA256.test(value.sha256) || sha256Hex(value.bytes) !== value.sha256
92
+ || Object.hasOwn(MARKDOWN, value.path) || value.path === OPENCODE_PATH)
93
+ return undefined;
94
+ }
95
+ else if (value.kind === 'shared-region') {
96
+ if (!exactKeys(value, ['kind', 'path', 'harnesses', 'scope', 'capability', 'writer', 'schemaVersion', 'ownership', 'regionId', 'region'])
97
+ || value.ownership !== 'shared-region' || typeof value.regionId !== 'string' || !object(value.region))
98
+ return undefined;
99
+ const markdown = MARKDOWN[value.path];
100
+ if (markdown) {
101
+ if (value.regionId !== markdown.regionId || !exactKeys(value.region, ['format', 'begin', 'end', 'bytes', 'sha256'])
102
+ || value.region.format !== 'markdown-delimited' || value.region.begin !== markdown.begin || value.region.end !== markdown.end
103
+ || typeof value.region.bytes !== 'string' || !value.region.bytes.endsWith('\n') || Buffer.byteLength(value.region.bytes) > MAX_FILE_BYTES
104
+ || value.region.bytes.includes(markdown.begin) || value.region.bytes.includes(markdown.end)
105
+ || value.region.bytes.includes(OWNER_PREFIX) && markdownOwner(value.region.bytes) === undefined
106
+ || typeof value.region.sha256 !== 'string' || sha256Hex(value.region.bytes) !== value.region.sha256)
107
+ return undefined;
108
+ }
109
+ else if (value.path === OPENCODE_PATH) {
110
+ if (value.regionId !== OPENCODE_REGION && openCodeOwner(value.regionId) === undefined || !exactKeys(value.region, ['format', 'members', 'sha256'])
111
+ || value.region.format !== 'opencode-json-root-properties' || !Array.isArray(value.region.members)
112
+ || value.region.members.length === 0 || value.region.members.length > OPENCODE_KEYS.size)
113
+ return undefined;
114
+ let priorKey = '';
115
+ for (const member of value.region.members) {
116
+ if (!object(member) || !exactKeys(member, ['key', 'valueBytes']) || typeof member.key !== 'string'
117
+ || !OPENCODE_KEYS.has(member.key) || member.key <= priorKey || typeof member.valueBytes !== 'string'
118
+ || Buffer.byteLength(member.valueBytes) > MAX_FILE_BYTES || tryParseJson(member.valueBytes) === undefined)
119
+ return undefined;
120
+ priorKey = member.key;
121
+ }
122
+ if (typeof value.region.sha256 !== 'string' || sha256Hex(JSON.stringify(value.region.members)) !== value.region.sha256)
123
+ return undefined;
124
+ }
125
+ else
126
+ return undefined;
127
+ }
128
+ else
129
+ return undefined;
130
+ return value;
131
+ }
132
+ export function classifySurfaceOwnership(input) {
133
+ const captured = snapshotGeneratedJson(input);
134
+ if (!object(captured) || !exactKeys(captured, ['configuredHarnesses', 'entries']) || !sortedUniqueStrings(captured.configuredHarnesses)
135
+ || captured.configuredHarnesses.length > SURFACE_TARGETS.length + 64
136
+ || !Array.isArray(captured.entries) || captured.entries.length > MAX_ENTRIES) {
137
+ return refusal('SURFACE_INPUT_INVALID', 'surface ownership input must be bounded canonical data');
138
+ }
139
+ const configured = new Set(captured.configuredHarnesses);
140
+ const entries = [];
141
+ let priorPath = '';
142
+ let total = 0;
143
+ for (const candidate of captured.entries) {
144
+ const entry = captureEntry(candidate, configured);
145
+ if (!entry)
146
+ return refusal('SURFACE_OWNERSHIP_INVALID', 'surface entry has an invalid path, hash, target, harness set, or ownership shape');
147
+ if (entry.path <= priorPath)
148
+ return refusal('SURFACE_PATH_CONFLICT', 'surface paths must be unique and canonically sorted', entry.path);
149
+ priorPath = entry.path;
150
+ total += entryBytes(entry);
151
+ if (total > MAX_TOTAL_BYTES)
152
+ return refusal('SURFACE_BOUNDS', 'surface output exceeds the total byte bound');
153
+ entries.push(entry);
154
+ }
155
+ const immutable = snapshotGeneratedJson({ kind: 'complete', entries });
156
+ return immutable ? immutable : refusal('SURFACE_BOUNDS', 'surface output cannot be captured');
157
+ }
158
+ function maskOwnerComments(bytes) {
159
+ if (!bytes.includes(OWNER_PREFIX))
160
+ return { bytes };
161
+ const pattern = /\/\* cockpit:owner:(cockpit:[a-z0-9][a-z0-9-]*:[a-f0-9]{64}):(start|end) \*\//g;
162
+ const matches = [...bytes.matchAll(pattern)];
163
+ if (matches.length !== 2 || bytes.split(OWNER_PREFIX).length !== 3 || matches[0][1] !== matches[1][1]
164
+ || matches[0][2] !== 'start' || matches[1][2] !== 'end')
165
+ return undefined;
166
+ const first = matches[0], last = matches[1];
167
+ const owner = { id: first[1], start: first.index, beginEnd: first.index + first[0].length,
168
+ endStart: last.index, finish: last.index + last[0].length };
169
+ return { bytes: `${bytes.slice(0, owner.start)}${' '.repeat(first[0].length)}${bytes.slice(owner.beginEnd, owner.endStart)}${' '.repeat(last[0].length)}${bytes.slice(owner.finish)}`, owner };
170
+ }
171
+ function stringEnd(bytes, start) {
172
+ if (bytes[start] !== '"')
173
+ return undefined;
174
+ for (let index = start + 1; index < bytes.length; index += 1) {
175
+ const code = bytes.charCodeAt(index);
176
+ if (code < 0x20)
177
+ return undefined;
178
+ if (bytes[index] === '"')
179
+ return index + 1;
180
+ if (bytes[index] === '\\') {
181
+ index += 1;
182
+ if (index >= bytes.length)
183
+ return undefined;
184
+ if (bytes[index] === 'u') {
185
+ if (!/^[0-9a-fA-F]{4}$/.test(bytes.slice(index + 1, index + 5)))
186
+ return undefined;
187
+ index += 4;
188
+ }
189
+ }
190
+ }
191
+ return undefined;
192
+ }
193
+ function uniqueJsonKeys(bytes) {
194
+ const containers = [];
195
+ for (let index = 0; index < bytes.length; index += 1) {
196
+ const char = bytes[index];
197
+ const container = containers.at(-1);
198
+ if (char === '"') {
199
+ const end = stringEnd(bytes, index);
200
+ if (end === undefined)
201
+ return false;
202
+ if (container?.keys && container.expectsKey) {
203
+ const key = tryParseJson(bytes.slice(index, end));
204
+ if (typeof key !== 'string' || container.keys.has(key))
205
+ return false;
206
+ container.keys.add(key);
207
+ container.expectsKey = false;
208
+ }
209
+ index = end - 1;
210
+ }
211
+ else if (char === '{')
212
+ containers.push({ keys: new Set(), expectsKey: true });
213
+ else if (char === '[')
214
+ containers.push({ expectsKey: false });
215
+ else if (char === '}' || char === ']')
216
+ containers.pop();
217
+ else if (char === ',' && container?.keys)
218
+ container.expectsKey = true;
219
+ }
220
+ return true;
221
+ }
222
+ function valueEnd(bytes, start) {
223
+ let index = start;
224
+ let containers = 0;
225
+ while (index < bytes.length) {
226
+ const char = bytes[index];
227
+ if (char === '"') {
228
+ const end = stringEnd(bytes, index);
229
+ if (end === undefined)
230
+ return undefined;
231
+ index = end;
232
+ continue;
233
+ }
234
+ if (containers === 0 && /\s/.test(char))
235
+ return index;
236
+ if (char === '{' || char === '[')
237
+ containers += 1;
238
+ else if (char === '}' || char === ']') {
239
+ if (containers === 0)
240
+ return index;
241
+ containers -= 1;
242
+ }
243
+ else if (char === ',' && containers === 0)
244
+ return index;
245
+ index += 1;
246
+ }
247
+ return undefined;
248
+ }
249
+ function rootObject(original) {
250
+ if (Buffer.byteLength(original) > SHARED_FILE_MAX_BYTES)
251
+ return undefined;
252
+ const masked = maskOwnerComments(original);
253
+ if (!masked)
254
+ return undefined;
255
+ const { bytes, owner } = masked;
256
+ if (!object(snapshotGeneratedJson(tryParseJson(bytes))) || !uniqueJsonKeys(bytes))
257
+ return undefined;
258
+ let index = 0;
259
+ while (/\s/.test(bytes[index] ?? ''))
260
+ index += 1;
261
+ if (bytes[index] !== '{')
262
+ return undefined;
263
+ const open = index;
264
+ index += 1;
265
+ const members = [];
266
+ const keys = new Set();
267
+ while (true) {
268
+ while (/\s/.test(bytes[index] ?? ''))
269
+ index += 1;
270
+ if (bytes[index] === '}') {
271
+ if (owner) {
272
+ const owned = members.filter(member => member.end <= owner.endStart);
273
+ const last = owned.at(-1);
274
+ if (!last || owner.start < open + 1 || bytes.slice(open + 1, owner.start).trim() !== ''
275
+ || bytes.slice(owner.beginEnd, members[0].start).trim() !== ''
276
+ || bytes.slice(last.end, owner.endStart).trim() !== '')
277
+ return undefined;
278
+ }
279
+ return { open, members, ...(owner ? { owner } : {}) };
280
+ }
281
+ const start = index;
282
+ const keyEnd = stringEnd(bytes, index);
283
+ if (keyEnd === undefined)
284
+ return undefined;
285
+ const key = tryParseJson(bytes.slice(index, keyEnd));
286
+ if (typeof key !== 'string' || keys.has(key))
287
+ return undefined;
288
+ keys.add(key);
289
+ index = keyEnd;
290
+ while (/\s/.test(bytes[index] ?? ''))
291
+ index += 1;
292
+ if (bytes[index] !== ':')
293
+ return undefined;
294
+ index += 1;
295
+ while (/\s/.test(bytes[index] ?? ''))
296
+ index += 1;
297
+ const end = valueEnd(bytes, index);
298
+ if (end === undefined)
299
+ return undefined;
300
+ members.push({ key, start, end });
301
+ index = end;
302
+ while (/\s/.test(bytes[index] ?? ''))
303
+ index += 1;
304
+ if (bytes[index] === ',') {
305
+ index += 1;
306
+ continue;
307
+ }
308
+ if (bytes[index] !== '}')
309
+ return undefined;
310
+ }
311
+ }
312
+ function rootMemberValues(bytes) {
313
+ const lexed = rootObject(bytes);
314
+ const masked = maskOwnerComments(bytes);
315
+ const parsed = masked ? tryParseJson(masked.bytes) : undefined;
316
+ if (!lexed || parsed === undefined || parsed === null || typeof parsed !== 'object' || Array.isArray(parsed))
317
+ return undefined;
318
+ const record = parsed;
319
+ return lexed.members.map(member => ({ key: member.key, valueBytes: JSON.stringify(record[member.key]) }));
320
+ }
321
+ export function captureOpenCodeRootMembers(bytes) {
322
+ if (bytes.includes(OWNER_PREFIX))
323
+ return undefined;
324
+ const members = rootMemberValues(bytes);
325
+ return members && members.every(member => OPENCODE_KEYS.has(member.key))
326
+ ? [...members].sort((left, right) => left.key < right.key ? -1 : left.key > right.key ? 1 : 0) : undefined;
327
+ }
328
+ function markdownPostimage(existing, entry) {
329
+ if (entry.region.format !== 'markdown-delimited')
330
+ return refusal('SURFACE_STATE_CONFLICT', 'Markdown path has a non-Markdown region', entry.path);
331
+ const { begin, end, bytes } = entry.region;
332
+ const starts = [];
333
+ const finishes = [];
334
+ for (let index = existing.indexOf(begin); index >= 0; index = existing.indexOf(begin, index + begin.length))
335
+ starts.push(index);
336
+ for (let index = existing.indexOf(end); index >= 0; index = existing.indexOf(end, index + end.length))
337
+ finishes.push(index);
338
+ if (starts.length > 1 || finishes.length > 1 || starts.length !== finishes.length)
339
+ return refusal('SURFACE_STATE_CONFLICT', 'Markdown ownership markers are partial or duplicated', entry.path);
340
+ const block = `${begin}\n${bytes}${bytes.endsWith('\n') ? '' : '\n'}${end}`;
341
+ if (starts.length === 0)
342
+ return { kind: 'complete', bytes: existing.length === 0 ? `${block}\n` : `${existing}${existing.endsWith('\n') ? '' : '\n'}${block}\n` };
343
+ if (starts[0] >= finishes[0])
344
+ return refusal('SURFACE_STATE_CONFLICT', 'Markdown ownership markers are out of order', entry.path);
345
+ return { kind: 'complete', bytes: `${existing.slice(0, starts[0])}${block}${existing.slice(finishes[0] + end.length)}` };
346
+ }
347
+ function markdownOwnedBytes(existing, begin, end) {
348
+ const start = existing.indexOf(begin);
349
+ const finish = existing.indexOf(end);
350
+ if (start < 0 || finish <= start || existing.indexOf(begin, start + begin.length) >= 0 || existing.indexOf(end, finish + end.length) >= 0)
351
+ return undefined;
352
+ const content = existing.slice(start + begin.length, finish);
353
+ if (!content.startsWith('\n'))
354
+ return undefined;
355
+ return { start, finish: finish + end.length, bytes: content.slice(1) };
356
+ }
357
+ function openCodeOwnedGroup(existing, keys) {
358
+ const parsed = rootObject(existing);
359
+ if (!parsed || keys.length === 0 || parsed.members.length < keys.length
360
+ || parsed.members.slice(0, keys.length).some((member, index) => member.key !== keys[index]))
361
+ return undefined;
362
+ const values = rootMemberValues(existing);
363
+ if (!values)
364
+ return undefined;
365
+ const group = values.slice(0, keys.length);
366
+ const last = parsed.members[keys.length - 1];
367
+ if (parsed.owner && (last.end > parsed.owner.endStart
368
+ || parsed.members[keys.length]?.start !== undefined && parsed.members[keys.length].start < parsed.owner.finish))
369
+ return undefined;
370
+ const groupEnd = parsed.owner?.finish ?? last.end;
371
+ let separator = groupEnd;
372
+ while (/\s/.test(existing[separator] ?? ''))
373
+ separator += 1;
374
+ let finish = groupEnd;
375
+ let separatorPrefix = '';
376
+ if (parsed.members.length > keys.length) {
377
+ if (existing[separator] !== ',')
378
+ return undefined;
379
+ separatorPrefix = existing.slice(groupEnd, separator);
380
+ finish = separator + 1;
381
+ }
382
+ return { finish, separatorPrefix, sha256: sha256Hex(JSON.stringify(group)) };
383
+ }
384
+ export function renderSharedPostimage(existingBytes, entry, priorOwned, manifest) {
385
+ const authority = manifest && issuedManifests.get(manifest);
386
+ if (manifest && (!authority || priorOwned && !manifest.entries.some(row => sameData(row, priorOwned)))) {
387
+ return refusal('SURFACE_OWNERSHIP_INVALID', 'shared region lacks authenticated enclosing ownership', entry.path);
388
+ }
389
+ if (Buffer.byteLength(existingBytes) > SHARED_FILE_MAX_BYTES || entry.kind !== 'shared-region'
390
+ || priorOwned !== undefined && (priorOwned.kind !== 'shared-region' || priorOwned.path !== entry.path
391
+ || priorOwned.regionId !== entry.regionId && !((priorOwned.regionId === OPENCODE_REGION || openCodeOwner(priorOwned.regionId)) && openCodeOwner(entry.regionId)))) {
392
+ return refusal('SURFACE_STATE_CONFLICT', 'shared surface input is invalid', entry.path);
393
+ }
394
+ if (entry.path !== OPENCODE_PATH) {
395
+ if (entry.region.format !== 'markdown-delimited')
396
+ return refusal('SURFACE_STATE_CONFLICT', 'Markdown path has a non-Markdown region', entry.path);
397
+ const current = markdownOwnedBytes(existingBytes, entry.region.begin, entry.region.end);
398
+ // Bytes that match the prior record are cockpit's own under whatever id their markers carry,
399
+ // which differs from the desired one only after `cockpit.name` was renamed.
400
+ const recorded = current !== undefined && priorOwned?.kind === 'shared-region' && priorOwned.region.format === 'markdown-delimited'
401
+ && sha256Hex(current.bytes) === priorOwned.region.sha256;
402
+ const expectedOwner = recorded ? markdownOwner(current.bytes) : authority?.cockpitId ?? markdownOwner(entry.region.bytes);
403
+ if (existingBytes.includes(OWNER_PREFIX) && (!current || !markdownOwner(current.bytes)
404
+ || existingBytes.split(OWNER_PREFIX).length !== 3 || expectedOwner !== undefined && markdownOwner(current.bytes) !== expectedOwner)) {
405
+ return refusal('SURFACE_STATE_CONFLICT', 'Markdown owner markers are foreign or ambiguous', entry.path);
406
+ }
407
+ if (authority && markdownOwner(entry.region.bytes) !== authority.cockpitId)
408
+ return refusal('SURFACE_OWNERSHIP_INVALID', 'desired Markdown region belongs to another cockpit', entry.path);
409
+ if (current && (!priorOwned || priorOwned.kind !== 'shared-region' || priorOwned.region.format !== 'markdown-delimited')) {
410
+ return refusal('SURFACE_STATE_CONFLICT', 'cockpit markers are present but the ownership record has no region for them', entry.path);
411
+ }
412
+ if (current && priorOwned?.kind === 'shared-region' && sha256Hex(current.bytes) !== priorOwned.region.sha256) {
413
+ return refusal('SURFACE_STATE_CONFLICT', "the text between cockpit's markers was edited by hand since cockpit last wrote it, and sync does not overwrite hand edits", entry.path, SURFACE_HAND_EDIT_FIX);
414
+ }
415
+ if (!current && priorOwned)
416
+ return refusal('SURFACE_STATE_CONFLICT', 'prior Markdown region is missing', entry.path);
417
+ return markdownPostimage(existingBytes, entry);
418
+ }
419
+ if (entry.region.format !== 'opencode-json-root-properties')
420
+ return refusal('SURFACE_STATE_CONFLICT', 'OpenCode path has a non-OpenCode region', entry.path);
421
+ const parsed = rootObject(existingBytes);
422
+ if (!parsed)
423
+ return refusal('SURFACE_STATE_CONFLICT', 'OpenCode surface must be strict duplicate-free JSON with a root object', entry.path);
424
+ const desiredKeys = entry.region.members.map(member => member.key);
425
+ const desiredOwner = openCodeOwner(entry.regionId);
426
+ // The prior record names the owner on disk; after a rename it differs from the desired one, and
427
+ // the group digest below still has to match what cockpit last wrote.
428
+ const priorOwner = priorOwned?.kind === 'shared-region' ? openCodeOwner(priorOwned.regionId) : undefined;
429
+ if (authority && desiredOwner !== authority.cockpitId
430
+ || parsed.owner && (authority && parsed.owner.id !== authority.cockpitId || desiredOwner && parsed.owner.id !== (priorOwner ?? desiredOwner))
431
+ || priorOwner && parsed.owner?.id !== priorOwner) {
432
+ return refusal('SURFACE_STATE_CONFLICT', 'OpenCode owner markers are missing or belong to another cockpit', entry.path);
433
+ }
434
+ const owner = authority?.cockpitId ?? desiredOwner ?? parsed.owner?.id;
435
+ const markers = owner ? ownerMarkers(owner, 'json') : ['', ''];
436
+ const group = `${markers[0]}${entry.region.members.map(member => `${JSON.stringify(member.key)}:${member.valueBytes}`).join(',')}${markers[1]}`;
437
+ if (priorOwned === undefined) {
438
+ if (parsed.members.some(member => desiredKeys.includes(member.key)))
439
+ return refusal('SURFACE_STATE_CONFLICT', 'OpenCode root property exists without prior ownership', entry.path);
440
+ return { kind: 'complete', bytes: `${existingBytes.slice(0, parsed.open + 1)}${group}${parsed.members.length === 0 ? '' : ','}${existingBytes.slice(parsed.open + 1)}` };
441
+ }
442
+ if (priorOwned.kind !== 'shared-region' || priorOwned.region.format !== 'opencode-json-root-properties')
443
+ return refusal('SURFACE_STATE_CONFLICT', 'prior OpenCode ownership is invalid', entry.path);
444
+ const prior = openCodeOwnedGroup(existingBytes, priorOwned.region.keys);
445
+ if (!prior || prior.sha256 !== priorOwned.region.sha256)
446
+ return refusal('SURFACE_STATE_CONFLICT', 'owned OpenCode root group is missing or changed', entry.path, SURFACE_HAND_EDIT_FIX);
447
+ if (parsed.members.slice(priorOwned.region.keys.length).some(member => desiredKeys.includes(member.key))) {
448
+ return refusal('SURFACE_STATE_CONFLICT', 'OpenCode root property exists outside prior ownership', entry.path);
449
+ }
450
+ const separator = parsed.members.length > priorOwned.region.keys.length ? `${prior.separatorPrefix},` : '';
451
+ return { kind: 'complete', bytes: `${existingBytes.slice(0, parsed.open + 1)}${group}${separator}${existingBytes.slice(prior.finish)}` };
452
+ }
453
+ export function removeSharedOwnership(existingBytes, priorOwned, manifest) {
454
+ const authority = manifest && issuedManifests.get(manifest);
455
+ if (manifest && (!authority || !manifest.entries.some(row => sameData(row, priorOwned))))
456
+ return refusal('SURFACE_OWNERSHIP_INVALID', 'shared removal lacks authenticated enclosing ownership', priorOwned.path);
457
+ if (priorOwned.kind !== 'shared-region' || Buffer.byteLength(existingBytes) > SHARED_FILE_MAX_BYTES)
458
+ return refusal('SURFACE_STATE_CONFLICT', 'stale shared ownership input is invalid', priorOwned.path);
459
+ if (priorOwned.region.format === 'markdown-delimited') {
460
+ const current = markdownOwnedBytes(existingBytes, priorOwned.region.begin, priorOwned.region.end);
461
+ if (existingBytes.includes(OWNER_PREFIX) && (!current || !markdownOwner(current.bytes)
462
+ || existingBytes.split(OWNER_PREFIX).length !== 3 || authority && markdownOwner(current.bytes) !== manifest?.cockpitId)) {
463
+ return refusal('SURFACE_STATE_CONFLICT', 'stale Markdown owner markers are foreign or ambiguous', priorOwned.path);
464
+ }
465
+ if (!current || sha256Hex(current.bytes) !== priorOwned.region.sha256)
466
+ return refusal('SURFACE_STATE_CONFLICT', 'stale Markdown region changed', priorOwned.path, SURFACE_HAND_EDIT_FIX);
467
+ return { kind: 'complete', bytes: `${existingBytes.slice(0, current.start)}${existingBytes.slice(current.finish)}` };
468
+ }
469
+ const parsed = rootObject(existingBytes);
470
+ if (authority && parsed?.owner && parsed.owner.id !== manifest?.cockpitId
471
+ || openCodeOwner(priorOwned.regionId) && parsed?.owner?.id !== openCodeOwner(priorOwned.regionId))
472
+ return refusal('SURFACE_STATE_CONFLICT', 'stale OpenCode owner markers are missing or foreign', priorOwned.path);
473
+ const current = openCodeOwnedGroup(existingBytes, priorOwned.region.keys);
474
+ if (!parsed || !current || current.sha256 !== priorOwned.region.sha256)
475
+ return refusal('SURFACE_STATE_CONFLICT', 'stale OpenCode group changed', priorOwned.path, SURFACE_HAND_EDIT_FIX);
476
+ return { kind: 'complete', bytes: `${existingBytes.slice(0, parsed.open + 1)}${current.separatorPrefix}${existingBytes.slice(current.finish)}` };
477
+ }
478
+ const issuedManifests = new WeakMap();
479
+ const OWNERSHIP_PATH = ".cockpit/generated-environment-ownership.json";
480
+ const MAX_OWNERSHIP_BYTES = 1_048_576;
481
+ const MAX_OWNED_FILES = 256;
482
+ const sha256 = sha256Hex;
483
+ const compareCodeUnits = (left, right) => left < right ? -1 : left > right ? 1 : 0;
484
+ function confinedOwnershipPath(root, path) {
485
+ const offset = relative(root, resolve(root, path));
486
+ return offset !== "" && offset !== ".." && !offset.startsWith(`..${sep}`) && !isAbsolute(offset);
487
+ }
488
+ export function parseOwnershipManifest(bytes, authority) {
489
+ const fields = ownRecord(authority);
490
+ const captured = fields && exactKeys(fields, ['root', 'cockpitId', 'configPath']) && snapshotGeneratedJson(fields);
491
+ if (!object(captured) || typeof captured.root !== "string" || !isAbsolute(captured.root)
492
+ || typeof captured.configPath !== "string" || dirname(captured.configPath) !== captured.root
493
+ || typeof captured.cockpitId !== "string" || captured.cockpitId.length === 0
494
+ || typeof bytes !== "string" || Buffer.byteLength(bytes) > MAX_OWNERSHIP_BYTES)
495
+ return { kind: "invalid" };
496
+ const record = ownRecord(tryParseJson(bytes));
497
+ if (typeof record?.format === "string" && /^cockpit-generated-ownership\/v[0-9]+$/.test(record.format)
498
+ && Number(record.format.slice(record.format.lastIndexOf("v") + 1)) > 3) {
499
+ return { kind: "unsupported", format: record.format };
500
+ }
501
+ const manifest = parseOwnership(bytes, captured);
502
+ return manifest ? { kind: "complete", manifest } : { kind: "invalid" };
503
+ }
504
+ function ownershipPayload(prepared) {
505
+ const entries = [
506
+ ...prepared.owned.map(({ path, sha256: digest }) => ({ kind: 'file', path, ownership: 'whole-file', sha256: digest })),
507
+ ...prepared.surfaces.map((entry) => {
508
+ if (entry.kind === 'file') {
509
+ const { bytes: _bytes, ...prior } = entry;
510
+ return prior;
511
+ }
512
+ if (entry.region.format === 'markdown-delimited') {
513
+ const { bytes: _bytes, ...region } = entry.region;
514
+ return { ...entry, region };
515
+ }
516
+ return { ...entry, region: {
517
+ format: entry.region.format, keys: entry.region.members.map(member => member.key), sha256: entry.region.sha256,
518
+ } };
519
+ }),
520
+ ].sort((left, right) => compareCodeUnits(left.path, right.path));
521
+ return {
522
+ format: 'cockpit-generated-ownership/v3',
523
+ cockpitId: prepared.cockpitId,
524
+ generation: prepared.generation,
525
+ source: { path: basename(prepared.configPath), sha256: prepared.sourceSha256 },
526
+ producer: { package: installedPackageMetadata.name, version: installedPackageMetadata.version, sourceRevision: installedPackageMetadata.cockpitSourceRevision },
527
+ entries,
528
+ };
529
+ }
530
+ export function encodeOwnershipManifest(prepared) {
531
+ const payload = ownershipPayload(prepared);
532
+ const bytes = `${JSON.stringify({ ...payload, integrity: sha256(JSON.stringify(payload)) }, null, 2)}\n`;
533
+ const parsed = parseOwnershipManifest(bytes, { root: prepared.root, cockpitId: prepared.cockpitId, configPath: prepared.configPath });
534
+ if (parsed.kind !== "complete")
535
+ throw new RefusalError("generated ownership encoder received invalid authority or entries");
536
+ return { bytes, manifest: parsed.manifest };
537
+ }
538
+ function ownRecord(value) {
539
+ return value !== null && typeof value === 'object' && !Array.isArray(value)
540
+ && [Object.prototype, null].includes(Object.getPrototypeOf(value)) ? value : undefined;
541
+ }
542
+ // Renaming `cockpit.name` changes the publication identity the ledger and every owner marker carry.
543
+ // The ledger sits at a fixed path under the configuration's own directory and names that file, so
544
+ // one recorded under another well-formed name is this cockpit's before the rename: refusing it left
545
+ // the operator nothing to do but delete the ledger and lose track of every file cockpit wrote.
546
+ // Its entries keep the id they were written under, which classification compares with the markers
547
+ // on disk; the next sync rewrites them under the new id.
548
+ function renamedPublicationId(value) {
549
+ const name = typeof value === 'string' ? /^cockpit:([a-z0-9][a-z0-9-]*):[a-f0-9]{64}$/.exec(value)?.[1] : undefined;
550
+ return name !== undefined && publicationCockpitId({ name }) === value;
551
+ }
552
+ function parseOwnership(bytes, prepared) {
553
+ if (Buffer.byteLength(bytes, 'utf8') > MAX_OWNERSHIP_BYTES)
554
+ return undefined;
555
+ try {
556
+ const record = ownRecord(JSON.parse(bytes));
557
+ if (!uniqueJsonKeys(bytes))
558
+ return undefined;
559
+ if (!record || record.format !== 'cockpit-generated-ownership/v3'
560
+ || !exactKeys(record, ['format', 'producer', 'cockpitId', 'generation', 'source', 'entries', 'integrity'])
561
+ || record.cockpitId !== prepared.cockpitId && !renamedPublicationId(record.cockpitId)
562
+ || typeof record.generation !== 'string' || !SHA256.test(record.generation)
563
+ || typeof record.integrity !== 'string' || !SHA256.test(record.integrity)
564
+ || !Array.isArray(record.entries) || record.entries.length > MAX_OWNED_FILES)
565
+ return undefined;
566
+ const source = ownRecord(record.source);
567
+ if (!source || !exactKeys(source, ['path', 'sha256'])
568
+ || source.path !== basename(prepared.configPath) || typeof source.sha256 !== 'string' || !SHA256.test(source.sha256))
569
+ return undefined;
570
+ const producer = ownRecord(record.producer);
571
+ if (!producer || !exactKeys(producer, ['package', 'version', 'sourceRevision'])
572
+ || [producer.package, producer.version, producer.sourceRevision].some(value => typeof value !== 'string' || value.length === 0 || value.length > 256 || /[\u0000-\u0020\u007f-\u009f]/.test(value))
573
+ || typeof producer.package !== 'string' || !/^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/.test(producer.package)
574
+ || typeof producer.version !== 'string' || !/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/.test(producer.version))
575
+ return undefined;
576
+ const entries = [];
577
+ for (const item of record.entries) {
578
+ const row = ownRecord(item);
579
+ if (!row || typeof row.path !== 'string' || !safePath(row.path) || !confinedOwnershipPath(prepared.root, row.path) || row.path === OWNERSHIP_PATH)
580
+ return undefined;
581
+ if (exactKeys(row, ['kind', 'path', 'ownership', 'sha256'])) {
582
+ if (row.kind !== 'file' || row.ownership !== 'whole-file'
583
+ || typeof row.sha256 !== 'string' || !SHA256.test(row.sha256))
584
+ return undefined;
585
+ entries.push({ kind: 'file', path: row.path, ownership: 'whole-file', sha256: row.sha256 });
586
+ continue;
587
+ }
588
+ const producerKeys = ['kind', 'path', 'harnesses', 'scope', 'capability', 'writer', 'schemaVersion', 'ownership'];
589
+ const harnesses = row.harnesses;
590
+ if (!Array.isArray(harnesses) || harnesses.length === 0 || harnesses.some((value, index) => typeof value !== 'string'
591
+ || index > 0 && String(harnesses[index - 1]) >= value) || row.scope !== 'always' && row.scope !== 'scoped'
592
+ || row.capability !== 'enforced' && row.capability !== 'instructed' || row.writer !== 'cockpit' || row.schemaVersion !== 1)
593
+ return undefined;
594
+ if (row.kind === 'file') {
595
+ if (!exactKeys(row, [...producerKeys, 'sha256']) || row.ownership !== 'whole-file'
596
+ || typeof row.sha256 !== 'string' || !SHA256.test(row.sha256))
597
+ return undefined;
598
+ }
599
+ else if (row.kind === 'shared-region') {
600
+ const region = ownRecord(row.region);
601
+ if (!exactKeys(row, [...producerKeys, 'regionId', 'region']) || row.ownership !== 'shared-region'
602
+ || typeof row.regionId !== 'string' || !region || typeof region.sha256 !== 'string' || !SHA256.test(region.sha256))
603
+ return undefined;
604
+ if (region.format === 'markdown-delimited') {
605
+ if (!exactKeys(region, ['format', 'begin', 'end', 'sha256']) || typeof region.begin !== 'string' || typeof region.end !== 'string')
606
+ return undefined;
607
+ }
608
+ else if (region.format === 'opencode-json-root-properties') {
609
+ const keys = region.keys;
610
+ if (!exactKeys(region, ['format', 'keys', 'sha256']) || !Array.isArray(keys) || keys.length === 0
611
+ || keys.some((key, index) => typeof key !== 'string' || index > 0 && String(keys[index - 1]) >= key))
612
+ return undefined;
613
+ }
614
+ else
615
+ return undefined;
616
+ }
617
+ else
618
+ return undefined;
619
+ entries.push(row);
620
+ }
621
+ if (new Set(entries.map(({ path }) => path)).size !== entries.length
622
+ || entries.some((file, index) => index > 0
623
+ && compareCodeUnits(entries[index - 1].path, file.path) >= 0))
624
+ return undefined;
625
+ const rawPayload = {
626
+ format: record.format, cockpitId: record.cockpitId,
627
+ generation: record.generation, source: source, producer, entries,
628
+ };
629
+ if (record.integrity !== sha256(JSON.stringify(rawPayload)))
630
+ return undefined;
631
+ const payload = {
632
+ format: record.format, producer: producer, cockpitId: record.cockpitId,
633
+ generation: record.generation, source: source, entries,
634
+ };
635
+ const manifest = snapshotGeneratedJson({
636
+ ...payload,
637
+ entries: entries.map((entry) => entry),
638
+ integrity: record.integrity,
639
+ });
640
+ if (!manifest)
641
+ return undefined;
642
+ issuedManifests.set(manifest, Object.freeze({ root: prepared.root, cockpitId: prepared.cockpitId, configPath: prepared.configPath }));
643
+ return manifest;
644
+ }
645
+ catch {
646
+ // an unauthenticatable manifest is refused by the caller.
647
+ return undefined;
648
+ }
649
+ }
650
+ const issuedFacts = new WeakSet();
651
+ function sameData(left, right) {
652
+ if (left === right)
653
+ return true;
654
+ if (left === null || right === null || typeof left !== 'object' || typeof right !== 'object')
655
+ return false;
656
+ const a = Object.keys(left).sort();
657
+ const b = Object.keys(right).sort();
658
+ return a.length === b.length && a.every((key, index) => key === b[index]
659
+ && sameData(left[key], right[key]));
660
+ }
661
+ export function projectGeneratedPathFacts(manifest, authority, declarations) {
662
+ const issued = issuedManifests.get(manifest);
663
+ const fields = ownRecord(authority);
664
+ const expected = fields && exactKeys(fields, ['root', 'cockpitId', 'configPath']) && snapshotGeneratedJson(fields);
665
+ if (!issued || !object(expected) || issued.root !== expected.root || issued.cockpitId !== expected.cockpitId
666
+ || issued.configPath !== expected.configPath)
667
+ return refusal('SURFACE_OWNERSHIP_INVALID', 'ownership authority was not issued for this cockpit and config');
668
+ const captured = snapshotGeneratedJson(declarations);
669
+ if (!Array.isArray(captured) || captured.length > MAX_ENTRIES)
670
+ return refusal('SURFACE_OWNERSHIP_INVALID', 'producer declarations must be bounded canonical data');
671
+ const byPath = new Map();
672
+ for (const row of captured) {
673
+ if (!object(row) || !['generated-environment', 'environment-data-plane', 'surface-render'].includes(String(row.producer))) {
674
+ return refusal('SURFACE_OWNERSHIP_INVALID', 'producer declaration has an unknown producer');
675
+ }
676
+ const unavailable = row.applicability === 'sibling-unavailable';
677
+ if (unavailable ? !exactKeys(row, ['producer', 'applicability', 'path'])
678
+ : !exactKeys(row, ['producer', 'applicability', 'entry']) || !['declared-before-sync', 'required-after-sync'].includes(String(row.applicability))) {
679
+ return refusal('SURFACE_OWNERSHIP_INVALID', 'producer declaration has an invalid applicability or shape');
680
+ }
681
+ const path = unavailable ? row.path : object(row.entry) ? row.entry.path : undefined;
682
+ if (typeof path !== 'string' || !safePath(path) || byPath.has(path))
683
+ return refusal('SURFACE_PATH_CONFLICT', 'producer declarations must have unique exact paths');
684
+ const entry = manifest.entries.find(entry => entry.path === path);
685
+ if (!entry || !unavailable && (!sameData(row.entry, entry)
686
+ || (row.producer === 'surface-render') !== ('harnesses' in entry))) {
687
+ return refusal('SURFACE_OWNERSHIP_INVALID', 'producer declaration does not match the exact manifest entry', path);
688
+ }
689
+ byPath.set(path, row);
690
+ }
691
+ // Classification compares the owner markers on disk with the id the ledger recorded them under.
692
+ const enclosing = { ...issued, cockpitId: manifest.cockpitId, generation: manifest.generation, source: manifest.source, integrity: manifest.integrity };
693
+ const facts = manifest.entries.map(entry => {
694
+ const declaration = byPath.get(entry.path) ?? null;
695
+ const fact = { path: entry.path, authority: enclosing, entry, declaration,
696
+ allowsMissing: declaration?.applicability === 'declared-before-sync',
697
+ harnesses: 'harnesses' in entry ? entry.harnesses : [],
698
+ scope: 'scope' in entry ? entry.scope : 'always', ownership: entry.ownership };
699
+ issuedFacts.add(fact);
700
+ return fact;
701
+ });
702
+ return { kind: 'complete', facts };
703
+ }
704
+ function captureNativeBytes(input) {
705
+ if (isProxy(input) || !isUint8Array(input))
706
+ return undefined;
707
+ try {
708
+ const prototype = Object.getPrototypeOf(input);
709
+ if (prototype !== Uint8Array.prototype && prototype !== Buffer.prototype)
710
+ return undefined;
711
+ const length = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(Uint8Array.prototype), 'byteLength').get.call(input);
712
+ if (length > SHARED_FILE_MAX_BYTES)
713
+ return undefined;
714
+ const descriptors = Object.getOwnPropertyDescriptors(input);
715
+ const keys = Reflect.ownKeys(descriptors);
716
+ if (keys.length !== length || keys.some(key => typeof key !== 'string'
717
+ || !/^(?:0|[1-9]\d*)$/.test(key) || Number(key) >= length || !('value' in descriptors[key])))
718
+ return undefined;
719
+ const bytes = new Uint8Array(length);
720
+ Uint8Array.prototype.set.call(bytes, input);
721
+ return bytes;
722
+ }
723
+ catch {
724
+ // a guard over caller-supplied values; a throwing accessor or proxy trap is malformed input the caller refuses.
725
+ return undefined;
726
+ }
727
+ }
728
+ export function classifyGeneratedPaths(facts, observations) {
729
+ if (!Array.isArray(facts) || facts.length > MAX_ENTRIES || facts.some(fact => !issuedFacts.has(fact))
730
+ || new Set(facts.map(fact => fact.path)).size !== facts.length)
731
+ return refusal('SURFACE_OWNERSHIP_INVALID', 'classification requires unique issued ownership facts');
732
+ if (!Array.isArray(observations) || isProxy(observations) || observations.length > MAX_ENTRIES + 1)
733
+ return refusal('SURFACE_INPUT_INVALID', 'observations must be a bounded array');
734
+ const rows = new Map();
735
+ for (let index = 0; index < observations.length; index += 1) {
736
+ const descriptor = Object.getOwnPropertyDescriptor(observations, String(index));
737
+ const row = descriptor && !descriptor.get && !descriptor.set && ownRecord(descriptor.value);
738
+ if (!row || typeof row.path !== 'string' || !safePath(row.path) || rows.has(row.path))
739
+ return refusal('SURFACE_INPUT_INVALID', 'observations require unique confined path names');
740
+ rows.set(row.path, row);
741
+ }
742
+ const paths = facts.map((fact) => {
743
+ const row = rows.get(fact.path);
744
+ let state = 'unknown';
745
+ let rawSha256;
746
+ if (row?.confinement === 'confined') {
747
+ const entry = fact.entry;
748
+ const bytes = row.kind === 'file' ? captureNativeBytes(row.bytes) : undefined;
749
+ if (row.kind === 'missing')
750
+ state = 'missing';
751
+ else if (row.kind === 'file' && typeof row.identity === 'string' && row.identity.length > 0
752
+ && bytes !== undefined) {
753
+ rawSha256 = sha256Hex(bytes);
754
+ if (entry.kind === 'file')
755
+ state = rawSha256 === entry.sha256 ? 'unchanged' : 'modified';
756
+ else if (entry.kind === 'shared-region') {
757
+ let text;
758
+ try {
759
+ text = new TextDecoder('utf-8', { fatal: true }).decode(bytes);
760
+ }
761
+ catch { /* undecodable bytes are malformed input the caller refuses. */
762
+ state = 'unknown';
763
+ }
764
+ if (text !== undefined && entry.region.format === 'markdown-delimited') {
765
+ const region = markdownOwnedBytes(text, entry.region.begin, entry.region.end);
766
+ const owner = region && markdownOwner(region.bytes);
767
+ if (region && (!text.includes(OWNER_PREFIX) || owner === fact.authority.cockpitId && text.split(OWNER_PREFIX).length === 3)) {
768
+ state = sha256Hex(region.bytes) === entry.region.sha256 ? 'unchanged' : 'modified';
769
+ }
770
+ else if (!text.includes(OWNER_PREFIX) && !text.includes(entry.region.begin) && !text.includes(entry.region.end))
771
+ state = 'missing';
772
+ }
773
+ else if (text !== undefined && entry.region.format === 'opencode-json-root-properties') {
774
+ const parsed = rootObject(text);
775
+ const group = openCodeOwnedGroup(text, entry.region.keys);
776
+ const expectedOwner = openCodeOwner(entry.regionId);
777
+ if (group && (!parsed?.owner || parsed.owner.id === fact.authority.cockpitId)
778
+ && (!expectedOwner || parsed?.owner?.id === expectedOwner))
779
+ state = group.sha256 === entry.region.sha256 ? 'unchanged' : 'modified';
780
+ else if (parsed && !parsed.owner && !parsed.members.some(member => entry.region.format === 'opencode-json-root-properties' && entry.region.keys.includes(member.key)))
781
+ state = 'missing';
782
+ }
783
+ }
784
+ }
785
+ }
786
+ const diagnostics = state === 'unchanged' || state === 'missing' && fact.allowsMissing ? []
787
+ : [{ code: 'SURFACE_STATE_CONFLICT', target: fact.path,
788
+ message: state === 'unknown' ? 'cockpit could not confirm this generated path is still the plain file or link inside the cockpit root that it last wrote'
789
+ : state === 'missing' ? 'this generated path is missing and nothing in this sync recreates it'
790
+ : 'this generated file was edited by hand since cockpit last wrote it, and sync does not overwrite hand edits',
791
+ ...(state === 'modified' ? { fix: SURFACE_HAND_EDIT_FIX } : {}) }];
792
+ return { fact, state, ...(rawSha256 ? { rawSha256 } : {}), diagnostics };
793
+ });
794
+ return { kind: 'complete', paths };
795
+ }