@intentius/chant 0.79.0 → 0.81.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 (462) hide show
  1. package/dist/audit/core.d.ts.map +1 -1
  2. package/dist/audit/discover.d.ts +30 -0
  3. package/dist/audit/discover.d.ts.map +1 -1
  4. package/dist/audit/report-html.d.ts +5 -0
  5. package/dist/audit/report-html.d.ts.map +1 -1
  6. package/dist/audit/report-model.d.ts +13 -0
  7. package/dist/audit/report-model.d.ts.map +1 -1
  8. package/dist/audit/terraform-state.d.ts +13 -0
  9. package/dist/audit/terraform-state.d.ts.map +1 -1
  10. package/dist/cli/command-group.d.ts +6 -0
  11. package/dist/cli/command-group.d.ts.map +1 -1
  12. package/dist/cli/commands/audit.d.ts +44 -3
  13. package/dist/cli/commands/audit.d.ts.map +1 -1
  14. package/dist/cli/commands/check-lexicon-examples.d.ts.map +1 -1
  15. package/dist/cli/commands/check-lexicon.d.ts +8 -0
  16. package/dist/cli/commands/check-lexicon.d.ts.map +1 -1
  17. package/dist/cli/commands/doctor.d.ts.map +1 -1
  18. package/dist/cli/commands/import-agents.d.ts.map +1 -1
  19. package/dist/cli/commands/init.d.ts +6 -0
  20. package/dist/cli/commands/init.d.ts.map +1 -1
  21. package/dist/cli/commands/lint.d.ts.map +1 -1
  22. package/dist/cli/commands/onboard.d.ts.map +1 -1
  23. package/dist/cli/commands/update.d.ts +6 -0
  24. package/dist/cli/commands/update.d.ts.map +1 -1
  25. package/dist/cli/commands/vendor.d.ts +5 -0
  26. package/dist/cli/commands/vendor.d.ts.map +1 -1
  27. package/dist/cli/conflict-check.d.ts +6 -0
  28. package/dist/cli/conflict-check.d.ts.map +1 -1
  29. package/dist/cli/handlers/build.d.ts.map +1 -1
  30. package/dist/cli/handlers/components.d.ts.map +1 -1
  31. package/dist/cli/handlers/dev.d.ts.map +1 -1
  32. package/dist/cli/handlers/fan-out.d.ts.map +1 -1
  33. package/dist/cli/handlers/init.d.ts.map +1 -1
  34. package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
  35. package/dist/cli/handlers/misc.d.ts.map +1 -1
  36. package/dist/cli/handlers/operator.d.ts +7 -0
  37. package/dist/cli/handlers/operator.d.ts.map +1 -1
  38. package/dist/cli/handlers/promote.d.ts +20 -0
  39. package/dist/cli/handlers/promote.d.ts.map +1 -0
  40. package/dist/cli/handlers/run-generate.d.ts +39 -0
  41. package/dist/cli/handlers/run-generate.d.ts.map +1 -0
  42. package/dist/cli/handlers/run.d.ts +4 -1
  43. package/dist/cli/handlers/run.d.ts.map +1 -1
  44. package/dist/cli/handlers/vendor.d.ts +8 -3
  45. package/dist/cli/handlers/vendor.d.ts.map +1 -1
  46. package/dist/cli/main.d.ts +12 -0
  47. package/dist/cli/main.d.ts.map +1 -1
  48. package/dist/cli/mcp/op-tools.d.ts.map +1 -1
  49. package/dist/cli/plugins.d.ts +14 -0
  50. package/dist/cli/plugins.d.ts.map +1 -1
  51. package/dist/cli/registry.d.ts +46 -1
  52. package/dist/cli/registry.d.ts.map +1 -1
  53. package/dist/codegen/fetch.d.ts +10 -0
  54. package/dist/codegen/fetch.d.ts.map +1 -1
  55. package/dist/components/capability-plugin-loader.d.ts.map +1 -1
  56. package/dist/components/cli-support.d.ts +3 -1
  57. package/dist/components/cli-support.d.ts.map +1 -1
  58. package/dist/components/discover.d.ts.map +1 -1
  59. package/dist/components/driver.d.ts +16 -2
  60. package/dist/components/driver.d.ts.map +1 -1
  61. package/dist/components/fan-out-support.d.ts.map +1 -1
  62. package/dist/components/gate-plan.d.ts +40 -0
  63. package/dist/components/gate-plan.d.ts.map +1 -0
  64. package/dist/components/pilots/alb-ecs.pilot.d.ts +7 -1
  65. package/dist/components/pilots/alb-ecs.pilot.d.ts.map +1 -1
  66. package/dist/components/presets/ecs-fargate.d.ts +10 -4
  67. package/dist/components/presets/ecs-fargate.d.ts.map +1 -1
  68. package/dist/components/promote.d.ts +264 -0
  69. package/dist/components/promote.d.ts.map +1 -0
  70. package/dist/components/sandbox/driver.d.ts.map +1 -1
  71. package/dist/components/sandbox/run.d.ts.map +1 -1
  72. package/dist/components/verbs/build-archive.d.ts +22 -11
  73. package/dist/components/verbs/build-archive.d.ts.map +1 -1
  74. package/dist/components/verbs/run-agent.d.ts.map +1 -1
  75. package/dist/config-static.d.ts +59 -0
  76. package/dist/config-static.d.ts.map +1 -0
  77. package/dist/config.d.ts +104 -5
  78. package/dist/config.d.ts.map +1 -1
  79. package/dist/content-digest.d.ts +17 -0
  80. package/dist/content-digest.d.ts.map +1 -0
  81. package/dist/discovery/files.d.ts +48 -4
  82. package/dist/discovery/files.d.ts.map +1 -1
  83. package/dist/discovery/fold-import.d.ts +32 -1
  84. package/dist/discovery/fold-import.d.ts.map +1 -1
  85. package/dist/discovery/index.d.ts.map +1 -1
  86. package/dist/discovery/sandbox/driver.d.ts +20 -0
  87. package/dist/discovery/sandbox/driver.d.ts.map +1 -1
  88. package/dist/discovery/sandbox/fork.d.ts +20 -0
  89. package/dist/discovery/sandbox/fork.d.ts.map +1 -1
  90. package/dist/discovery/walk.d.ts +119 -0
  91. package/dist/discovery/walk.d.ts.map +1 -0
  92. package/dist/graph-ir.d.ts +17 -1
  93. package/dist/graph-ir.d.ts.map +1 -1
  94. package/dist/join-key.d.ts +31 -0
  95. package/dist/join-key.d.ts.map +1 -0
  96. package/dist/lexicon-module.d.ts +110 -0
  97. package/dist/lexicon-module.d.ts.map +1 -0
  98. package/dist/lexicon.d.ts +82 -1
  99. package/dist/lexicon.d.ts.map +1 -1
  100. package/dist/lifecycle/build-ledger-store.d.ts.map +1 -1
  101. package/dist/lifecycle/digest.d.ts +5 -1
  102. package/dist/lifecycle/digest.d.ts.map +1 -1
  103. package/dist/lifecycle/gate-ledger.d.ts +20 -5
  104. package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
  105. package/dist/lifecycle/git.d.ts +64 -0
  106. package/dist/lifecycle/git.d.ts.map +1 -1
  107. package/dist/lifecycle/lease.d.ts +8 -1
  108. package/dist/lifecycle/lease.d.ts.map +1 -1
  109. package/dist/lifecycle/legacy-digest.d.ts +59 -0
  110. package/dist/lifecycle/legacy-digest.d.ts.map +1 -0
  111. package/dist/lifecycle/member-ledger.d.ts +67 -0
  112. package/dist/lifecycle/member-ledger.d.ts.map +1 -0
  113. package/dist/lifecycle/plan-digest.d.ts +4 -3
  114. package/dist/lifecycle/plan-digest.d.ts.map +1 -1
  115. package/dist/lifecycle/release-ledger.d.ts +42 -1
  116. package/dist/lifecycle/release-ledger.d.ts.map +1 -1
  117. package/dist/lifecycle/status.d.ts +90 -5
  118. package/dist/lifecycle/status.d.ts.map +1 -1
  119. package/dist/lint/rules/comp/comp003-mutating-no-rollback.d.ts +2 -3
  120. package/dist/lint/rules/comp/comp003-mutating-no-rollback.d.ts.map +1 -1
  121. package/dist/op/activities/activity-contracts.d.ts +32 -0
  122. package/dist/op/activities/activity-contracts.d.ts.map +1 -1
  123. package/dist/op/activities/index.d.ts +2 -0
  124. package/dist/op/activities/index.d.ts.map +1 -1
  125. package/dist/op/activities/propose-upgrade.d.ts +75 -0
  126. package/dist/op/activities/propose-upgrade.d.ts.map +1 -0
  127. package/dist/op/activity-contract-registry.d.ts.map +1 -1
  128. package/dist/op/activity-registry.d.ts.map +1 -1
  129. package/dist/op/discover.d.ts.map +1 -1
  130. package/dist/op/gate-approval.d.ts.map +1 -1
  131. package/dist/op/gate-name.d.ts +7 -0
  132. package/dist/op/gate-name.d.ts.map +1 -1
  133. package/dist/op/gate-summary.d.ts +16 -0
  134. package/dist/op/gate-summary.d.ts.map +1 -1
  135. package/dist/op/gate.d.ts +16 -2
  136. package/dist/op/gate.d.ts.map +1 -1
  137. package/dist/op/generate-pipeline.d.ts.map +1 -1
  138. package/dist/op/runtimes/local.d.ts.map +1 -1
  139. package/dist/project-root.d.ts +26 -0
  140. package/dist/project-root.d.ts.map +1 -1
  141. package/dist/workspace/__fixtures__/contract-repo.d.ts +37 -0
  142. package/dist/workspace/__fixtures__/contract-repo.d.ts.map +1 -0
  143. package/dist/workspace/__fixtures__/two-member-workspace.d.ts +11 -0
  144. package/dist/workspace/__fixtures__/two-member-workspace.d.ts.map +1 -0
  145. package/dist/workspace/checks/generated.d.ts +156 -0
  146. package/dist/workspace/checks/generated.d.ts.map +1 -0
  147. package/dist/workspace/checks/ledgers.d.ts +78 -0
  148. package/dist/workspace/checks/ledgers.d.ts.map +1 -0
  149. package/dist/workspace/checks/links.d.ts +25 -0
  150. package/dist/workspace/checks/links.d.ts.map +1 -0
  151. package/dist/workspace/checks/pipelines.d.ts +76 -0
  152. package/dist/workspace/checks/pipelines.d.ts.map +1 -0
  153. package/dist/workspace/checks.d.ts +161 -0
  154. package/dist/workspace/checks.d.ts.map +1 -0
  155. package/dist/workspace/compose-graph.d.ts +115 -0
  156. package/dist/workspace/compose-graph.d.ts.map +1 -0
  157. package/dist/workspace/compose-reports.d.ts +68 -0
  158. package/dist/workspace/compose-reports.d.ts.map +1 -0
  159. package/dist/workspace/declaration.d.ts +220 -0
  160. package/dist/workspace/declaration.d.ts.map +1 -0
  161. package/dist/workspace/declaration.schema.json +434 -0
  162. package/dist/workspace/generated-files.d.ts +65 -0
  163. package/dist/workspace/generated-files.d.ts.map +1 -0
  164. package/dist/workspace/glob.d.ts +13 -0
  165. package/dist/workspace/glob.d.ts.map +1 -0
  166. package/dist/workspace/graph-cli.d.ts +75 -0
  167. package/dist/workspace/graph-cli.d.ts.map +1 -0
  168. package/dist/workspace/init.d.ts +96 -0
  169. package/dist/workspace/init.d.ts.map +1 -0
  170. package/dist/workspace/jsonc.d.ts +38 -0
  171. package/dist/workspace/jsonc.d.ts.map +1 -0
  172. package/dist/workspace/kinds.d.ts +192 -0
  173. package/dist/workspace/kinds.d.ts.map +1 -0
  174. package/dist/workspace/lineage-check.d.ts +84 -0
  175. package/dist/workspace/lineage-check.d.ts.map +1 -0
  176. package/dist/workspace/lineage-cli.d.ts +31 -0
  177. package/dist/workspace/lineage-cli.d.ts.map +1 -0
  178. package/dist/workspace/lineage-init.d.ts +101 -0
  179. package/dist/workspace/lineage-init.d.ts.map +1 -0
  180. package/dist/workspace/lineage-lock.d.ts +258 -0
  181. package/dist/workspace/lineage-lock.d.ts.map +1 -0
  182. package/dist/workspace/lineage-merge.d.ts +14 -0
  183. package/dist/workspace/lineage-merge.d.ts.map +1 -0
  184. package/dist/workspace/lineage-migrations.d.ts +178 -0
  185. package/dist/workspace/lineage-migrations.d.ts.map +1 -0
  186. package/dist/workspace/lineage-update.d.ts +59 -0
  187. package/dist/workspace/lineage-update.d.ts.map +1 -0
  188. package/dist/workspace/lineage-upgrade-cli.d.ts +40 -0
  189. package/dist/workspace/lineage-upgrade-cli.d.ts.map +1 -0
  190. package/dist/workspace/lineage-upgrade.d.ts +146 -0
  191. package/dist/workspace/lineage-upgrade.d.ts.map +1 -0
  192. package/dist/workspace/lineage-vendor.d.ts +63 -0
  193. package/dist/workspace/lineage-vendor.d.ts.map +1 -0
  194. package/dist/workspace/lineage-version.d.ts +32 -0
  195. package/dist/workspace/lineage-version.d.ts.map +1 -0
  196. package/dist/workspace/links.d.ts +150 -0
  197. package/dist/workspace/links.d.ts.map +1 -0
  198. package/dist/workspace/ls.d.ts +99 -0
  199. package/dist/workspace/ls.d.ts.map +1 -0
  200. package/dist/workspace/member-commands.d.ts +139 -0
  201. package/dist/workspace/member-commands.d.ts.map +1 -0
  202. package/dist/workspace/member-handles.d.ts +54 -0
  203. package/dist/workspace/member-handles.d.ts.map +1 -0
  204. package/dist/workspace/member-pipeline.d.ts +143 -0
  205. package/dist/workspace/member-pipeline.d.ts.map +1 -0
  206. package/dist/workspace/member-run.d.ts +67 -0
  207. package/dist/workspace/member-run.d.ts.map +1 -0
  208. package/dist/workspace/reason-codes.d.ts +65 -0
  209. package/dist/workspace/reason-codes.d.ts.map +1 -0
  210. package/dist/workspace/record-source.d.ts +22 -0
  211. package/dist/workspace/record-source.d.ts.map +1 -0
  212. package/dist/workspace/records-cli.d.ts +81 -0
  213. package/dist/workspace/records-cli.d.ts.map +1 -0
  214. package/dist/workspace/records.d.ts +118 -0
  215. package/dist/workspace/records.d.ts.map +1 -0
  216. package/dist/workspace/root-refusal.d.ts +53 -0
  217. package/dist/workspace/root-refusal.d.ts.map +1 -0
  218. package/dist/workspace/status.d.ts +150 -0
  219. package/dist/workspace/status.d.ts.map +1 -0
  220. package/dist/workspace/template-manifest.d.ts +90 -0
  221. package/dist/workspace/template-manifest.d.ts.map +1 -0
  222. package/dist/workspace/tree.d.ts +36 -0
  223. package/dist/workspace/tree.d.ts.map +1 -0
  224. package/dist/workspace/trust/attestor.d.ts +67 -0
  225. package/dist/workspace/trust/attestor.d.ts.map +1 -0
  226. package/dist/workspace/trust/policy.d.ts +111 -0
  227. package/dist/workspace/trust/policy.d.ts.map +1 -0
  228. package/dist/workspace/trust/provenance.d.ts +67 -0
  229. package/dist/workspace/trust/provenance.d.ts.map +1 -0
  230. package/dist/workspace/trust/ssh-commit.d.ts +43 -0
  231. package/dist/workspace/trust/ssh-commit.d.ts.map +1 -0
  232. package/dist/workspace/trust/test-repo.d.ts +42 -0
  233. package/dist/workspace/trust/test-repo.d.ts.map +1 -0
  234. package/dist/workspace/trust/verify-cli.d.ts +9 -0
  235. package/dist/workspace/trust/verify-cli.d.ts.map +1 -0
  236. package/dist/workspace/trust/verify.d.ts +66 -0
  237. package/dist/workspace/trust/verify.d.ts.map +1 -0
  238. package/dist/workspace/which-chant.d.ts +58 -0
  239. package/dist/workspace/which-chant.d.ts.map +1 -0
  240. package/dist/workspace/workspace-kinds.schema.json +133 -0
  241. package/package.json +4 -3
  242. package/src/audit/core.ts +13 -2
  243. package/src/audit/discover.ts +102 -36
  244. package/src/audit/report-html.ts +3 -1
  245. package/src/audit/report-model.ts +14 -1
  246. package/src/audit/terraform-state.test.ts +81 -1
  247. package/src/audit/terraform-state.ts +25 -0
  248. package/src/cli/command-group.ts +7 -0
  249. package/src/cli/commands/audit-truncation.test.ts +167 -0
  250. package/src/cli/commands/audit.ts +132 -29
  251. package/src/cli/commands/check-lexicon-examples.ts +2 -1
  252. package/src/cli/commands/check-lexicon.ts +24 -0
  253. package/src/cli/commands/doctor.test.ts +30 -0
  254. package/src/cli/commands/doctor.ts +21 -0
  255. package/src/cli/commands/import-agents.ts +15 -4
  256. package/src/cli/commands/init.ts +76 -25
  257. package/src/cli/commands/lint.test.ts +52 -1
  258. package/src/cli/commands/lint.ts +41 -71
  259. package/src/cli/commands/onboard.ts +15 -0
  260. package/src/cli/commands/update.ts +18 -5
  261. package/src/cli/commands/vendor.ts +11 -6
  262. package/src/cli/conflict-check.test.ts +18 -1
  263. package/src/cli/conflict-check.ts +13 -0
  264. package/src/cli/discovery-skip.test.ts +168 -0
  265. package/src/cli/handlers/__snapshots__/build-member.test.ts.snap +38 -0
  266. package/src/cli/handlers/build-member.test.ts +127 -0
  267. package/src/cli/handlers/build.test.ts +11 -0
  268. package/src/cli/handlers/build.ts +35 -1
  269. package/src/cli/handlers/components.test.ts +127 -0
  270. package/src/cli/handlers/components.ts +70 -8
  271. package/src/cli/handlers/dev.ts +6 -1
  272. package/src/cli/handlers/fan-out.ts +9 -2
  273. package/src/cli/handlers/graph.test.ts +20 -0
  274. package/src/cli/handlers/graph.ts +4 -2
  275. package/src/cli/handlers/init.ts +54 -1
  276. package/src/cli/handlers/lifecycle.ts +2 -1
  277. package/src/cli/handlers/misc.ts +16 -0
  278. package/src/cli/handlers/operator.test.ts +68 -0
  279. package/src/cli/handlers/operator.ts +39 -10
  280. package/src/cli/handlers/promote.test.ts +377 -0
  281. package/src/cli/handlers/promote.ts +391 -0
  282. package/src/cli/handlers/run-generate-member.test.ts +83 -0
  283. package/src/cli/handlers/run-generate.test.ts +158 -0
  284. package/src/cli/handlers/run-generate.ts +168 -0
  285. package/src/cli/handlers/run.test.ts +47 -0
  286. package/src/cli/handlers/run.ts +41 -6
  287. package/src/cli/handlers/vendor.ts +147 -43
  288. package/src/cli/main.test.ts +137 -1
  289. package/src/cli/main.ts +290 -24
  290. package/src/cli/mcp/op-tools.ts +2 -1
  291. package/src/cli/path-lexicon-messages.test.ts +152 -0
  292. package/src/cli/plugins.ts +43 -5
  293. package/src/cli/registry.ts +46 -1
  294. package/src/cli/static-config-read.test.ts +208 -0
  295. package/src/codegen/fetch.test.ts +63 -1
  296. package/src/codegen/fetch.ts +30 -1
  297. package/src/components/__fixtures__/alb-ecs-service.json +0 -8
  298. package/src/components/capability-plugin-loader.ts +3 -1
  299. package/src/components/cli-support.ts +9 -3
  300. package/src/components/discover.test.ts +27 -57
  301. package/src/components/discover.ts +20 -51
  302. package/src/components/driver.test.ts +90 -12
  303. package/src/components/driver.ts +43 -5
  304. package/src/components/fan-out-support.ts +2 -1
  305. package/src/components/gate-plan.ts +53 -0
  306. package/src/components/pilots/README.md +1 -1
  307. package/src/components/pilots/alb-ecs.pilot.ts +7 -7
  308. package/src/components/presets/ecs-fargate.ts +10 -5
  309. package/src/components/promote.test.ts +645 -0
  310. package/src/components/promote.ts +641 -0
  311. package/src/components/sandbox/driver.ts +8 -8
  312. package/src/components/sandbox/run.ts +4 -1
  313. package/src/components/verbs/build-archive.ts +27 -21
  314. package/src/components/verbs/run-agent.ts +3 -5
  315. package/src/config-static.test.ts +136 -0
  316. package/src/config-static.ts +232 -0
  317. package/src/config.test.ts +40 -0
  318. package/src/config.ts +161 -11
  319. package/src/content-digest.test.ts +120 -0
  320. package/src/content-digest.ts +21 -0
  321. package/src/discovery/files.test.ts +127 -1
  322. package/src/discovery/files.ts +91 -59
  323. package/src/discovery/fold-import.ts +127 -9
  324. package/src/discovery/index.ts +1 -0
  325. package/src/discovery/sandbox/driver.ts +73 -35
  326. package/src/discovery/sandbox/fork-diagnostic.test.ts +136 -13
  327. package/src/discovery/sandbox/fork.ts +83 -43
  328. package/src/discovery/sandbox/path-lexicon-multifile.test.ts +285 -0
  329. package/src/discovery/sandbox/path-lexicon-parity.test.ts +162 -0
  330. package/src/discovery/walk.test.ts +263 -0
  331. package/src/discovery/walk.ts +334 -0
  332. package/src/effect-receipt.ts +2 -2
  333. package/src/graph-ir.ts +18 -1
  334. package/src/join-key.test.ts +25 -0
  335. package/src/join-key.ts +39 -0
  336. package/src/lexicon-module.test.ts +203 -0
  337. package/src/lexicon-module.ts +215 -0
  338. package/src/lexicon.ts +105 -1
  339. package/src/lifecycle/build-ledger-store.ts +13 -2
  340. package/src/lifecycle/digest.ts +9 -5
  341. package/src/lifecycle/gate-ledger.test.ts +20 -0
  342. package/src/lifecycle/gate-ledger.ts +28 -6
  343. package/src/lifecycle/git.test.ts +13 -1
  344. package/src/lifecycle/git.ts +221 -176
  345. package/src/lifecycle/lease.ts +25 -10
  346. package/src/lifecycle/legacy-digest.test.ts +206 -0
  347. package/src/lifecycle/legacy-digest.ts +103 -0
  348. package/src/lifecycle/member-ledger.test.ts +319 -0
  349. package/src/lifecycle/member-ledger.ts +145 -0
  350. package/src/lifecycle/plan-digest.ts +7 -7
  351. package/src/lifecycle/release-ledger.test.ts +20 -0
  352. package/src/lifecycle/release-ledger.ts +53 -3
  353. package/src/lifecycle/status.test.ts +151 -1
  354. package/src/lifecycle/status.ts +228 -7
  355. package/src/lint/rules/comp/comp003-mutating-no-rollback.ts +2 -3
  356. package/src/op/activities/activity-contracts.ts +37 -0
  357. package/src/op/activities/index.ts +10 -0
  358. package/src/op/activities/propose-upgrade.ts +206 -0
  359. package/src/op/activity-contract-registry.ts +18 -4
  360. package/src/op/activity-registry.ts +26 -0
  361. package/src/op/discover.test.ts +14 -4
  362. package/src/op/discover.ts +19 -23
  363. package/src/op/gate-approval.ts +3 -1
  364. package/src/op/gate-name.ts +8 -0
  365. package/src/op/gate-summary.test.ts +14 -0
  366. package/src/op/gate-summary.ts +25 -2
  367. package/src/op/gate.ts +36 -7
  368. package/src/op/generate-pipeline.test.ts +14 -5
  369. package/src/op/generate-pipeline.ts +2 -1
  370. package/src/op/runtimes/local.ts +2 -1
  371. package/src/project-root.test.ts +66 -1
  372. package/src/project-root.ts +48 -0
  373. package/src/workspace/__fixtures__/contract-repo.ts +100 -0
  374. package/src/workspace/__fixtures__/two-member-workspace.ts +35 -0
  375. package/src/workspace/behold-kinds.test.ts +162 -0
  376. package/src/workspace/check-contract.test.ts +127 -0
  377. package/src/workspace/check.schema.json +347 -0
  378. package/src/workspace/checks/generated.test.ts +287 -0
  379. package/src/workspace/checks/generated.ts +475 -0
  380. package/src/workspace/checks/ledgers.test.ts +139 -0
  381. package/src/workspace/checks/ledgers.ts +246 -0
  382. package/src/workspace/checks/links.ts +183 -0
  383. package/src/workspace/checks/pipelines.test.ts +133 -0
  384. package/src/workspace/checks/pipelines.ts +225 -0
  385. package/src/workspace/checks-repo.test.ts +71 -0
  386. package/src/workspace/checks.test.ts +450 -0
  387. package/src/workspace/checks.ts +461 -0
  388. package/src/workspace/compose-graph.test.ts +146 -0
  389. package/src/workspace/compose-graph.ts +228 -0
  390. package/src/workspace/compose-reports.test.ts +109 -0
  391. package/src/workspace/compose-reports.ts +186 -0
  392. package/src/workspace/declaration.schema.json +434 -0
  393. package/src/workspace/declaration.test.ts +347 -0
  394. package/src/workspace/declaration.ts +623 -0
  395. package/src/workspace/generated-files.test.ts +114 -0
  396. package/src/workspace/generated-files.ts +94 -0
  397. package/src/workspace/glob.ts +49 -0
  398. package/src/workspace/graph-cli.ts +220 -0
  399. package/src/workspace/graph-contract.test.ts +204 -0
  400. package/src/workspace/graph.schema.json +662 -0
  401. package/src/workspace/init.test.ts +223 -0
  402. package/src/workspace/init.ts +585 -0
  403. package/src/workspace/jsonc.test.ts +65 -0
  404. package/src/workspace/jsonc.ts +231 -0
  405. package/src/workspace/kinds.test.ts +288 -0
  406. package/src/workspace/kinds.ts +506 -0
  407. package/src/workspace/lineage-check.ts +264 -0
  408. package/src/workspace/lineage-cli.ts +114 -0
  409. package/src/workspace/lineage-init.test.ts +231 -0
  410. package/src/workspace/lineage-init.ts +365 -0
  411. package/src/workspace/lineage-lock.test.ts +224 -0
  412. package/src/workspace/lineage-lock.ts +357 -0
  413. package/src/workspace/lineage-merge.ts +44 -0
  414. package/src/workspace/lineage-migrations.test.ts +178 -0
  415. package/src/workspace/lineage-migrations.ts +343 -0
  416. package/src/workspace/lineage-update.ts +160 -0
  417. package/src/workspace/lineage-upgrade-cli.ts +151 -0
  418. package/src/workspace/lineage-upgrade.test.ts +474 -0
  419. package/src/workspace/lineage-upgrade.ts +624 -0
  420. package/src/workspace/lineage-vendor.test.ts +126 -0
  421. package/src/workspace/lineage-vendor.ts +215 -0
  422. package/src/workspace/lineage-version.ts +127 -0
  423. package/src/workspace/links.test.ts +344 -0
  424. package/src/workspace/links.ts +331 -0
  425. package/src/workspace/ls-contract.test.ts +200 -0
  426. package/src/workspace/ls.schema.json +190 -0
  427. package/src/workspace/ls.ts +254 -0
  428. package/src/workspace/member-commands.e2e.test.ts +244 -0
  429. package/src/workspace/member-commands.test.ts +217 -0
  430. package/src/workspace/member-commands.ts +540 -0
  431. package/src/workspace/member-handles.test.ts +76 -0
  432. package/src/workspace/member-handles.ts +228 -0
  433. package/src/workspace/member-pipeline.test.ts +135 -0
  434. package/src/workspace/member-pipeline.ts +317 -0
  435. package/src/workspace/member-run.ts +146 -0
  436. package/src/workspace/read-contract.test.ts +129 -0
  437. package/src/workspace/reason-codes.test.ts +113 -0
  438. package/src/workspace/reason-codes.ts +81 -0
  439. package/src/workspace/record-source.ts +117 -0
  440. package/src/workspace/records-cli.ts +198 -0
  441. package/src/workspace/records-contract.test.ts +93 -0
  442. package/src/workspace/records.schema.json +150 -0
  443. package/src/workspace/records.test.ts +243 -0
  444. package/src/workspace/records.ts +384 -0
  445. package/src/workspace/root-refusal.ts +97 -0
  446. package/src/workspace/status-contract.test.ts +308 -0
  447. package/src/workspace/status.schema.json +210 -0
  448. package/src/workspace/status.ts +412 -0
  449. package/src/workspace/template-manifest.test.ts +79 -0
  450. package/src/workspace/template-manifest.ts +221 -0
  451. package/src/workspace/tree.ts +132 -0
  452. package/src/workspace/trust/attestor.ts +93 -0
  453. package/src/workspace/trust/policy.ts +306 -0
  454. package/src/workspace/trust/provenance.ts +181 -0
  455. package/src/workspace/trust/ssh-commit.ts +138 -0
  456. package/src/workspace/trust/test-repo.ts +138 -0
  457. package/src/workspace/trust/trust.test.ts +397 -0
  458. package/src/workspace/trust/verify-cli.ts +56 -0
  459. package/src/workspace/trust/verify.ts +194 -0
  460. package/src/workspace/which-chant.test.ts +117 -0
  461. package/src/workspace/which-chant.ts +151 -0
  462. package/src/workspace/workspace-kinds.schema.json +133 -0
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Test support for the trust modules: throwaway git repositories whose commits
3
+ * are signed with throwaway ssh keys. Isolated from the user's git config, so a
4
+ * developer's own signing setup never leaks into a test.
5
+ */
6
+
7
+ import { execFileSync, spawnSync } from "node:child_process";
8
+ import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
9
+ import { tmpdir } from "node:os";
10
+ import { dirname, join } from "node:path";
11
+
12
+ export const hasSshKeygen = spawnSync("ssh-keygen", ["-Y"], { stdio: "ignore" }).error === undefined;
13
+
14
+ export interface Key {
15
+ name: string;
16
+ /** Private key file. */
17
+ file: string;
18
+ /** `ssh-ed25519 AAAA...`, no comment. */
19
+ pub: string;
20
+ }
21
+
22
+ export class TestRepo {
23
+ readonly dir: string;
24
+ private readonly home: string;
25
+ private readonly scratch: string;
26
+
27
+ constructor(label: string) {
28
+ this.scratch = mkdtempSync(join(tmpdir(), `chant-2547-${label}-`));
29
+ this.dir = join(this.scratch, "repo");
30
+ this.home = join(this.scratch, "home");
31
+ mkdirSync(this.dir);
32
+ mkdirSync(this.home);
33
+ this.git(["init", "-q", "-b", "main"]);
34
+ }
35
+
36
+ env(extra: Record<string, string> = {}): NodeJS.ProcessEnv {
37
+ return {
38
+ PATH: process.env.PATH ?? "",
39
+ HOME: this.home,
40
+ GIT_CONFIG_GLOBAL: join(this.home, "gitconfig"),
41
+ GIT_CONFIG_NOSYSTEM: "1",
42
+ GIT_AUTHOR_NAME: "t",
43
+ GIT_AUTHOR_EMAIL: "t@example.test",
44
+ GIT_COMMITTER_NAME: "t",
45
+ GIT_COMMITTER_EMAIL: "t@example.test",
46
+ ...extra,
47
+ };
48
+ }
49
+
50
+ git(args: string[], opts: { env?: Record<string, string>; input?: string | Buffer } = {}): string {
51
+ return execFileSync("git", args, { cwd: this.dir, env: this.env(opts.env), input: opts.input, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
52
+ }
53
+
54
+ key(name: string): Key {
55
+ const file = join(this.scratch, `key-${name}`);
56
+ execFileSync("ssh-keygen", ["-q", "-t", "ed25519", "-N", "", "-C", name, "-f", file]);
57
+ const pub = readFileSync(`${file}.pub`, "utf-8").trim().split(" ").slice(0, 2).join(" ");
58
+ return { name, file, pub };
59
+ }
60
+
61
+ write(path: string, text: string): void {
62
+ mkdirSync(dirname(join(this.dir, path)), { recursive: true });
63
+ writeFileSync(join(this.dir, path), text);
64
+ }
65
+
66
+ /** Stage everything and commit, signed with `key` when given. Returns the commit id. */
67
+ commit(message: string, key?: Key, env: Record<string, string> = {}): string {
68
+ this.git(["add", "-A"]);
69
+ const sign = key ? ["-c", "gpg.format=ssh", "-c", `user.signingkey=${key.file}`, "commit", "-S"] : ["commit", "--no-gpg-sign"];
70
+ this.git([...sign, "-q", "--allow-empty", "-m", message], { env });
71
+ return this.head();
72
+ }
73
+
74
+ head(): string {
75
+ return this.git(["rev-parse", "HEAD"]).trim();
76
+ }
77
+
78
+ /**
79
+ * Write a commit object by hand: the current index's tree, `HEAD` as parent,
80
+ * and `signature` spliced in as the gpgsig header. Returns the new commit,
81
+ * which HEAD then points at.
82
+ */
83
+ forgeCommit(message: string, sign: (payload: Buffer) => string): string {
84
+ this.git(["add", "-A"]);
85
+ const tree = this.git(["write-tree"]).trim();
86
+ const parent = this.head();
87
+ const payload = Buffer.from(
88
+ `tree ${tree}\nparent ${parent}\nauthor t <t@example.test> 1700000000 +0000\ncommitter t <t@example.test> 1700000000 +0000\n\n${message}\n`,
89
+ );
90
+ const sig = sign(payload).trimEnd().split("\n");
91
+ const head = payload.toString("latin1").split("\n\n")[0];
92
+ const body = payload.toString("latin1").slice(head.length);
93
+ const raw = `${head}\ngpgsig ${sig[0]}\n${sig.slice(1).map((l) => ` ${l}`).join("\n")}${body}`;
94
+ const commit = this.git(["hash-object", "-t", "commit", "-w", "--stdin"], { input: Buffer.from(raw, "latin1") }).trim();
95
+ this.git(["update-ref", "HEAD", commit]);
96
+ return commit;
97
+ }
98
+
99
+ /** An ssh signature over `payload` in `namespace`, by `key`. */
100
+ sshSign(key: Key, payload: Buffer, namespace: string): string {
101
+ const f = join(this.scratch, `payload-${Math.random().toString(36).slice(2)}`);
102
+ writeFileSync(f, payload);
103
+ execFileSync("ssh-keygen", ["-q", "-Y", "sign", "-n", namespace, "-f", key.file, f], { stdio: "ignore" });
104
+ return readFileSync(`${f}.sig`, "utf-8");
105
+ }
106
+
107
+ cleanup(): void {
108
+ rmSync(this.scratch, { recursive: true, force: true });
109
+ }
110
+ }
111
+
112
+ /** A record kind reading `records/*.md`, and its schema, written into `repo`. */
113
+ export function writeRecordKind(repo: TestRepo): string {
114
+ repo.write(
115
+ "kinds/note.kind.mjs",
116
+ `export const recordKind = {
117
+ name: "note",
118
+ location: { dir: "../records", match: "\\\\.md$" },
119
+ format: "markdown-front-matter",
120
+ schema: { id: "urn:test:note:1", path: "note.schema.json" },
121
+ idField: "id",
122
+ stateField: "state",
123
+ states: ["open", "closed"],
124
+ closedStates: ["closed"],
125
+ supersedes: { field: "supersedes", key: "note" },
126
+ };
127
+ `,
128
+ );
129
+ repo.write(
130
+ "kinds/note.schema.json",
131
+ JSON.stringify({ $id: "urn:test:note:1", type: "object", required: ["id", "state"], properties: { id: { type: "string" }, state: { type: "string" } } }),
132
+ );
133
+ return "kinds/note.kind.mjs";
134
+ }
135
+
136
+ export function note(id: string, body = ""): string {
137
+ return `---\nid: ${id}\nstate: open\n---\n${body}\n`;
138
+ }
@@ -0,0 +1,397 @@
1
+ /**
2
+ * #2547: attestors and policy read from the base revision.
3
+ *
4
+ * Every verification path is tested with the attacker controlling the working
5
+ * tree and the change under review: an edited signers file, a signature by a
6
+ * key the same change adds, a backdated commit, a trailer, a signature in the
7
+ * wrong namespace, and a forged merge.
8
+ */
9
+
10
+ import { execFileSync } from "node:child_process";
11
+ import Ajv2020 from "ajv/dist/2020";
12
+ import schema from "../records.schema.json";
13
+ import { afterEach, describe, expect, test } from "vitest";
14
+ import { queryRecords, type RecordsDocument, type RecordView } from "../records-cli";
15
+ import { activeAttestors } from "./attestor";
16
+ import { parseAllowedSigners } from "./policy";
17
+ import { verifyChange } from "./verify";
18
+ import { hasSshKeygen, note, TestRepo, writeRecordKind, type Key } from "./test-repo";
19
+
20
+ const repos: TestRepo[] = [];
21
+ function repo(label: string): TestRepo {
22
+ const r = new TestRepo(label);
23
+ repos.push(r);
24
+ return r;
25
+ }
26
+ afterEach(() => {
27
+ while (repos.length) repos.pop()!.cleanup();
28
+ });
29
+
30
+ function signers(...entries: Array<[string, Key] | string>): string {
31
+ return entries.map((e) => (typeof e === "string" ? e : `${e[0]} ${e[1].pub}`)).join("\n") + "\n";
32
+ }
33
+
34
+ async function verify(r: TestRepo, opts: { base?: string; head?: string; require?: "attested" } = {}) {
35
+ return verifyChange({ repo: r.dir, base: opts.base ?? "main", head: opts.head, require: opts.require, attestors: await activeAttestors() });
36
+ }
37
+
38
+ const validate = new Ajv2020({ strict: true, allErrors: true }).compile(schema);
39
+
40
+ /** Records through the real query; every document is checked against the published output schema. */
41
+ async function records(r: TestRepo, kind: string, extra: { base?: string; at?: string } = {}): Promise<RecordView[]> {
42
+ const doc: RecordsDocument = await queryRecords({ kind, cwd: r.dir, base: extra.base ?? "main", at: extra.at });
43
+ expect(validate(doc), JSON.stringify(validate.errors)).toBe(true);
44
+ if ("error" in doc) throw new Error(doc.error.message);
45
+ return doc.records;
46
+ }
47
+
48
+ function level(rs: RecordView[], id: string): string {
49
+ return rs.find((r) => r.id === id)!.provenance.level;
50
+ }
51
+
52
+ describe("parseAllowedSigners", () => {
53
+ const k = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOrbXxpbHmPGSDRaxyUkF4H1j3VsnNwhk4tEsZ7Cw6Tt";
54
+
55
+ test("reads principals, namespaces and comments", () => {
56
+ const set = parseAllowedSigners(`# team\nalice@example.test ${k} laptop\n"bob@example.test,carol@example.test" namespaces="git,file" ${k}\n\n`);
57
+ expect(set.excluded).toEqual([]);
58
+ expect(set.signers.map((s) => [s.principal, s.namespaces, s.line])).toEqual([
59
+ ["alice@example.test", undefined, 2],
60
+ ["bob@example.test", "git,file", 3],
61
+ ["carol@example.test", "git,file", 3],
62
+ ]);
63
+ expect(set.signers[0].key).toBe(k);
64
+ });
65
+
66
+ test("refuses entries whose meaning depends on a date, a CA or a pattern", () => {
67
+ const set = parseAllowedSigners(
68
+ [
69
+ `old@example.test valid-before="20200101" ${k}`,
70
+ `new@example.test valid-after="20990101" ${k}`,
71
+ `ca@example.test cert-authority ${k}`,
72
+ `*@example.test ${k}`,
73
+ `x@example.test no-touch-required ${k}`,
74
+ `broken`,
75
+ ].join("\n"),
76
+ );
77
+ expect(set.signers).toEqual([]);
78
+ expect(set.excluded.map((e) => e.line)).toEqual([1, 2, 3, 4, 5, 6]);
79
+ expect(set.excluded[0].reason).toMatch(/commit's own date/);
80
+ });
81
+ });
82
+
83
+ describe.skipIf(!hasSshKeygen)("the ssh-commit attestor, against the policy at base", () => {
84
+ /** main: alice (admin) and bob are signers; one note record. Returns keys and the kind path. */
85
+ function baseline(label: string, opts: { admin?: boolean } = {}) {
86
+ const r = repo(label);
87
+ const alice = r.key("alice");
88
+ const bob = r.key("bob");
89
+ const mallory = r.key("mallory");
90
+ const kind = writeRecordKind(r);
91
+ r.write(".chant/allowed_signers", signers(["alice@example.test", alice], ["bob@example.test", bob]));
92
+ if (opts.admin) r.write(".chant/trust.json", JSON.stringify({ schema: 1, roles: { admin: ["alice@example.test"] } }));
93
+ r.write("records/n1.md", note("n1"));
94
+ r.commit("policy and first record", alice);
95
+ r.git(["checkout", "-q", "-b", "change"]);
96
+ return { r, alice, bob, mallory, kind };
97
+ }
98
+
99
+ test("a commit signed by a signer at base is attested, and so is its record", async () => {
100
+ const { r, bob, kind } = baseline("good");
101
+ r.write("records/n2.md", note("n2"));
102
+ r.commit("add n2", bob);
103
+ const report = await verify(r, { require: "attested" });
104
+ expect(report.failures).toEqual([]);
105
+ expect(report.commits.map((c) => [c.level, c.principal])).toEqual([["attested", "bob@example.test"]]);
106
+ const rs = await records(r, kind);
107
+ expect(level(rs, "n1")).toBe("attested");
108
+ expect(level(rs, "n2")).toBe("attested");
109
+ expect(rs.find((x) => x.id === "n2")!.provenance.principal).toBe("bob@example.test");
110
+ });
111
+
112
+ test("attack: the change edits the signers file to add its own key, and signs with it", async () => {
113
+ const { r, mallory, kind } = baseline("edited-signers");
114
+ r.write(".chant/allowed_signers", signers(["mallory@example.test", mallory]));
115
+ r.write("records/n1.md", note("n1", "rewritten"));
116
+ r.commit("trust me", mallory);
117
+ const report = await verify(r);
118
+ expect(report.ok).toBe(false);
119
+ expect(report.commits[0].level).toBe("unattested");
120
+ expect(report.protectedWrites).toHaveLength(1);
121
+ expect(report.protectedWrites[0].allowed).toBe(false);
122
+ expect(report.failures.join("\n")).toMatch(/\.chant\/allowed_signers is protected/);
123
+ // The record is judged by main's signers, where mallory is nobody.
124
+ expect(level(await records(r, kind), "n1")).toBe("unattested");
125
+ });
126
+
127
+ test("attack: the signers file is edited in the working tree only", async () => {
128
+ const { r, mallory, kind } = baseline("worktree-signers");
129
+ r.write("records/n3.md", note("n3"));
130
+ r.commit("n3", mallory);
131
+ r.write(".chant/allowed_signers", signers(["mallory@example.test", mallory]));
132
+ expect(level(await records(r, kind), "n3")).toBe("unattested");
133
+ // Even reading at the base itself: the working-tree policy is never read.
134
+ expect(level(await records(r, kind, { base: "HEAD" }), "n3")).toBe("unattested");
135
+ });
136
+
137
+ test("attack: a signer at base adds a key, and the change's next commit is signed by that key", async () => {
138
+ const { r, alice, mallory, kind } = baseline("key-added-in-change");
139
+ r.write(".chant/allowed_signers", signers(["alice@example.test", alice], ["mallory@example.test", mallory]));
140
+ r.commit("add mallory", alice);
141
+ r.write("records/n2.md", note("n2"));
142
+ r.commit("n2", mallory);
143
+ const report = await verify(r, { require: "attested" });
144
+ // The policy edit itself is signed by alice, a signer at base: allowed.
145
+ expect(report.protectedWrites.map((w) => w.allowed)).toEqual([true]);
146
+ // mallory's key is not in the policy at base, so her commit is not attested.
147
+ expect(report.commits.map((c) => c.level)).toEqual(["attested", "unattested"]);
148
+ expect(report.ok).toBe(false);
149
+ expect(level(await records(r, kind), "n2")).toBe("unattested");
150
+ });
151
+
152
+ test("attack: a backdated commit signed by a key whose valid-before window has closed", async () => {
153
+ const r = repo("backdated");
154
+ const alice = r.key("alice");
155
+ const old = r.key("old");
156
+ const kind = writeRecordKind(r);
157
+ r.write(".chant/allowed_signers", signers(["alice@example.test", alice], `old@example.test valid-before="20200101" ${old.pub}`));
158
+ r.commit("policy", alice);
159
+ r.git(["checkout", "-q", "-b", "change"]);
160
+ r.write("records/n1.md", note("n1"));
161
+ const date = "2019-06-01T00:00:00Z";
162
+ const c = r.commit("backdated", old, { GIT_AUTHOR_DATE: date, GIT_COMMITTER_DATE: date });
163
+
164
+ // git's own check trusts the commit's date, so it accepts this signature.
165
+ r.write("allowed-for-git", signers(`old@example.test valid-before="20200101" ${old.pub}`));
166
+ const gitSays = execFileSync("git", ["-c", `gpg.ssh.allowedSignersFile=${r.dir}/allowed-for-git`, "verify-commit", c], {
167
+ cwd: r.dir,
168
+ env: r.env(),
169
+ encoding: "utf-8",
170
+ stdio: ["ignore", "pipe", "pipe"],
171
+ });
172
+ expect(gitSays).toBeDefined();
173
+
174
+ const report = await verify(r, { require: "attested" });
175
+ expect(report.policy.excluded.map((e) => e.line)).toEqual([2]);
176
+ expect(report.commits[0].level).toBe("unattested");
177
+ expect(report.ok).toBe(false);
178
+ expect(level(await records(r, kind), "n1")).toBe("unattested");
179
+ });
180
+
181
+ test("attack: a backdated commit by a key removed from the signers before base", async () => {
182
+ const { r, alice, bob, kind } = baseline("backdated-revoked");
183
+ r.git(["checkout", "-q", "main"]);
184
+ r.write(".chant/allowed_signers", signers(["alice@example.test", alice]));
185
+ r.commit("remove bob", alice);
186
+ r.git(["checkout", "-q", "-b", "late"]);
187
+ r.write("records/n2.md", note("n2"));
188
+ const date = "2001-01-01T00:00:00Z";
189
+ r.commit("dated long ago", bob, { GIT_AUTHOR_DATE: date, GIT_COMMITTER_DATE: date });
190
+ const report = await verify(r, { require: "attested" });
191
+ expect(report.commits[0].level).toBe("unattested");
192
+ expect(report.ok).toBe(false);
193
+ expect(level(await records(r, kind), "n2")).toBe("unattested");
194
+ });
195
+
196
+ test("attack: a trailer or an author name is not an attestation", async () => {
197
+ const { r, kind } = baseline("trailer");
198
+ r.write("records/n2.md", note("n2"));
199
+ r.commit("n2\n\nSigned-off-by: alice@example.test\nApproved-by: alice@example.test", undefined, {
200
+ GIT_AUTHOR_NAME: "alice",
201
+ GIT_AUTHOR_EMAIL: "alice@example.test",
202
+ GIT_COMMITTER_EMAIL: "alice@example.test",
203
+ });
204
+ const report = await verify(r, { require: "attested" });
205
+ expect(report.commits[0].level).toBe("unattested");
206
+ expect(report.commits[0].reason).toMatch(/not signed/);
207
+ expect(level(await records(r, kind), "n2")).toBe("unattested");
208
+ });
209
+
210
+ test("attack: a valid signature by a base signer, made in another namespace, is spliced into a commit", async () => {
211
+ const { r, bob } = baseline("namespace");
212
+ r.write("records/n2.md", note("n2"));
213
+ r.forgeCommit("n2", (payload) => r.sshSign(bob, payload, "file"));
214
+ const report = await verify(r, { require: "attested" });
215
+ expect(report.commits[0].level).toBe("unattested");
216
+ expect(report.commits[0].reason).toMatch(/git namespace/);
217
+ });
218
+
219
+ test("attack: a signature lifted from another commit does not cover this one", async () => {
220
+ const { r, bob } = baseline("lifted");
221
+ const signed = r.git(["cat-file", "commit", "main"]);
222
+ const sig = signed.split("\n").reduce<string[]>((acc, l) => {
223
+ if (l.startsWith("gpgsig ")) acc.push(l.slice(7));
224
+ else if (acc.length && l.startsWith(" ")) acc.push(l.slice(1));
225
+ return acc;
226
+ }, []);
227
+ void bob;
228
+ r.write("records/n2.md", note("n2"));
229
+ r.forgeCommit("n2", () => sig.join("\n") + "\n");
230
+ const report = await verify(r);
231
+ expect(report.commits[0].level).toBe("unattested");
232
+ });
233
+
234
+ test("an OpenPGP signature is reported as present but unverifiable here", async () => {
235
+ const { r, kind } = baseline("pgp");
236
+ r.write("records/n2.md", note("n2"));
237
+ r.forgeCommit("n2", () => "-----BEGIN PGP SIGNATURE-----\n\nwsBcBAABCAAQBQJ\n-----END PGP SIGNATURE-----\n");
238
+ const report = await verify(r, { require: "attested" });
239
+ expect(report.commits[0].level).toBe("attested-unverifiable-here");
240
+ expect(report.ok).toBe(false);
241
+ expect(level(await records(r, kind), "n2")).toBe("attested-unverifiable-here");
242
+ });
243
+
244
+ test("an uncommitted edit to a signed record is unattested", async () => {
245
+ const { r, kind } = baseline("dirty");
246
+ r.write("records/n1.md", note("n1", "edited"));
247
+ const rs = await records(r, kind);
248
+ expect(level(rs, "n1")).toBe("unattested");
249
+ expect(rs[0].provenance.reason).toMatch(/uncommitted/);
250
+ // The committed version, read with --at, is still attested.
251
+ expect(level(await records(r, kind, { at: "HEAD" }), "n1")).toBe("attested");
252
+ });
253
+
254
+ test("attack: an evil merge changes a record beyond either parent, and is judged as its own commit", async () => {
255
+ const { r, bob, kind } = baseline("evil-merge");
256
+ r.write("records/n2.md", note("n2"));
257
+ r.commit("side", bob);
258
+ r.git(["checkout", "-q", "main"]);
259
+ r.git(["checkout", "-q", "-b", "merge"]);
260
+ r.write("other.txt", "x");
261
+ r.commit("other", bob);
262
+ r.git(["merge", "-q", "--no-ff", "--no-commit", "change"]);
263
+ r.write("records/n2.md", note("n2", "slipped in during the merge"));
264
+ r.commit("merge");
265
+ const rs = await records(r, kind);
266
+ expect(level(rs, "n2")).toBe("unattested");
267
+ const report = await verify(r, { require: "attested" });
268
+ expect(report.ok).toBe(false);
269
+ });
270
+
271
+ test("a merge with no changes of its own is passed through to the commits it brings", async () => {
272
+ const { r, bob, kind } = baseline("clean-merge");
273
+ r.write("records/n2.md", note("n2"));
274
+ r.commit("side", bob);
275
+ r.git(["checkout", "-q", "main"]);
276
+ r.git(["checkout", "-q", "-b", "merge"]);
277
+ r.write("other.txt", "x");
278
+ r.commit("other", bob);
279
+ r.git(["merge", "-q", "--no-ff", "--no-gpg-sign", "-m", "merge", "change"]);
280
+ expect(level(await records(r, kind), "n2")).toBe("attested");
281
+ const report = await verify(r, { require: "attested" });
282
+ expect(report.failures).toEqual([]);
283
+ expect(report.commits.find((c) => c.subject === "merge")!.skipped).toBeDefined();
284
+ });
285
+
286
+ test("with an admin role granted, a policy edit by a non-admin signer is refused", async () => {
287
+ const { r, alice, bob } = baseline("admin", { admin: true });
288
+ r.write(".chant/allowed_signers", signers(["alice@example.test", alice], ["bob@example.test", bob], ["carol@example.test", r.key("carol")]));
289
+ r.commit("add carol", bob);
290
+ let report = await verify(r);
291
+ expect(report.protectedWrites[0]).toMatchObject({ allowed: false, principal: "bob@example.test" });
292
+ expect(report.ok).toBe(false);
293
+
294
+ r.git(["reset", "-q", "--hard", "main"]);
295
+ r.write(".chant/trust.json", JSON.stringify({ schema: 1, roles: { admin: ["bob@example.test"] } }));
296
+ r.commit("make bob admin", bob);
297
+ report = await verify(r);
298
+ expect(report.protectedWrites[0]).toMatchObject({ allowed: false, paths: [".chant/trust.json"] });
299
+
300
+ r.git(["reset", "-q", "--hard", "main"]);
301
+ r.write(".chant/allowed_signers", signers(["alice@example.test", alice], ["bob@example.test", bob], ["carol@example.test", r.key("carol2")]));
302
+ r.commit("add carol", alice);
303
+ report = await verify(r);
304
+ expect(report.ok).toBe(true);
305
+ });
306
+
307
+ test("attack: pointing trust.json at another signers file is itself a protected write", async () => {
308
+ const { r, mallory } = baseline("redirect");
309
+ r.write("mine", signers(["mallory@example.test", mallory]));
310
+ r.write(".chant/trust.json", JSON.stringify({ schema: 1, signers: "mine" }));
311
+ r.commit("redirect", mallory);
312
+ const report = await verify(r);
313
+ expect(report.protectedWrites[0]).toMatchObject({ allowed: false, paths: [".chant/trust.json"] });
314
+ expect(report.ok).toBe(false);
315
+ });
316
+
317
+ test("deleting the signers file is a protected write", async () => {
318
+ const { r } = baseline("delete");
319
+ r.git(["rm", "-q", ".chant/allowed_signers"]);
320
+ r.commit("turn it off");
321
+ const report = await verify(r);
322
+ expect(report.protectedWrites[0].allowed).toBe(false);
323
+ expect(report.ok).toBe(false);
324
+ });
325
+
326
+ test("an unreadable trust.json at base fails closed", async () => {
327
+ const { r, alice, kind } = baseline("bad-trust");
328
+ r.git(["checkout", "-q", "main"]);
329
+ r.write(".chant/trust.json", "{ not json");
330
+ r.commit("oops", alice);
331
+ r.git(["checkout", "-q", "-b", "next"]);
332
+ r.write("records/n2.md", note("n2"));
333
+ r.commit("n2", alice);
334
+ const report = await verify(r);
335
+ expect(report.ok).toBe(false);
336
+ expect(report.failures[0]).toMatch(/cannot be read/);
337
+ expect(level(await records(r, kind), "n2")).toBe("unattested");
338
+ });
339
+
340
+ test("a commit range the base policy adopts is adopted", async () => {
341
+ const r = repo("adopted");
342
+ const alice = r.key("alice");
343
+ const kind = writeRecordKind(r);
344
+ r.write("records/n1.md", note("n1"));
345
+ const old = r.commit("history before signing");
346
+ r.write(".chant/allowed_signers", signers(["alice@example.test", alice]));
347
+ r.write(".chant/trust.json", JSON.stringify({ schema: 1, adopted: [{ to: old, note: "before signing" }] }));
348
+ r.commit("start signing", alice);
349
+ const rs = await records(r, kind);
350
+ expect(level(rs, "n1")).toBe("adopted");
351
+ });
352
+ });
353
+
354
+ describe("attestation is opt-in", () => {
355
+ test("without a signers file at base nothing is verified, and every record is unattested", async () => {
356
+ const r = repo("off");
357
+ const kind = writeRecordKind(r);
358
+ r.write("records/n1.md", note("n1"));
359
+ r.commit("n1");
360
+ r.git(["checkout", "-q", "-b", "change"]);
361
+ r.write("records/n2.md", note("n2"));
362
+ r.commit("n2");
363
+ const report = await verify(r);
364
+ expect(report.ok).toBe(true);
365
+ expect(report.commits).toEqual([]);
366
+ expect(report.notes[0]).toMatch(/attestation is off/);
367
+ const doc = await queryRecords({ kind, cwd: r.dir });
368
+ if ("error" in doc) throw new Error(doc.error.message);
369
+ expect(doc.trust).toMatchObject({ active: false, baseFrom: "main" });
370
+ expect(doc.records.every((x) => x.provenance.level === "unattested")).toBe(true);
371
+ // A gate that asks for attested still fails.
372
+ expect((await verify(r, { require: "attested" })).ok).toBe(false);
373
+ });
374
+
375
+ test.skipIf(!hasSshKeygen)("a change that adds the first signers file applies only after it merges", async () => {
376
+ const r = repo("first");
377
+ r.write("a.txt", "a");
378
+ r.commit("a");
379
+ r.git(["checkout", "-q", "-b", "change"]);
380
+ const alice = r.key("alice");
381
+ r.write(".chant/allowed_signers", signers(["alice@example.test", alice]));
382
+ r.commit("start signing", alice);
383
+ const report = await verify(r);
384
+ expect(report.ok).toBe(true);
385
+ expect(report.notes.join("\n")).toMatch(/applies to changes made after it is merged/);
386
+ });
387
+
388
+ test("outside git every record is unattested", async () => {
389
+ const r = repo("nogit");
390
+ const kind = writeRecordKind(r);
391
+ r.write("records/n1.md", note("n1"));
392
+ execFileSync("rm", ["-rf", `${r.dir}/.git`]);
393
+ const doc = await queryRecords({ kind, cwd: r.dir });
394
+ if ("error" in doc) throw new Error(doc.error.message);
395
+ expect(doc.records[0].provenance).toMatchObject({ level: "unattested", reason: "not in a git repository" });
396
+ });
397
+ });
@@ -0,0 +1,56 @@
1
+ /**
2
+ * `chant workspace verify [--base <rev>] [--head <rev>] [--require attested] [--json]`
3
+ * (#2547): check a change against the trust policy at its base. See ./verify.ts.
4
+ *
5
+ * Exit 0 when the change passes, 1 when it does not or cannot be checked.
6
+ */
7
+
8
+ import { formatError, formatSuccess } from "../../cli/format";
9
+ import type { CommandContext } from "../../cli/registry";
10
+ import { gitRoot } from "../record-source";
11
+ import { activeAttestors } from "./attestor";
12
+ import { verifyChange, type ChangeReport } from "./verify";
13
+
14
+ const USAGE = "chant workspace verify [--base <rev>] [--head <rev>] [--require attested] [--json]";
15
+
16
+ export async function runWorkspaceVerify(ctx: CommandContext): Promise<number> {
17
+ const { args } = ctx;
18
+ if (args.require !== undefined && args.require !== "attested") {
19
+ console.error(formatError({ message: `--require takes one level, attested, not ${JSON.stringify(args.require)}`, hint: USAGE }));
20
+ return 1;
21
+ }
22
+ const repo = gitRoot(process.cwd());
23
+ if (!repo) {
24
+ console.error(formatError({ message: "chant workspace verify checks git commits, and this directory is not in a git repository", hint: USAGE }));
25
+ return 1;
26
+ }
27
+ const report = verifyChange({
28
+ repo,
29
+ base: args.base,
30
+ head: args.head,
31
+ require: args.require === "attested" ? "attested" : undefined,
32
+ attestors: await activeAttestors(),
33
+ });
34
+ if (args.json) console.log(JSON.stringify(report, null, 2));
35
+ else printReport(report);
36
+ return report.ok ? 0 : 1;
37
+ }
38
+
39
+ function printReport(r: ChangeReport): void {
40
+ const lines: string[] = [];
41
+ if (r.base) lines.push(`base ${r.base.slice(0, 8)} (${r.baseFrom})${r.head ? `, head ${r.head.slice(0, 8)}` : ""}`);
42
+ if (r.policy.active) {
43
+ lines.push(`policy at base: ${r.policy.principals.length} signers in ${r.policy.signersPath}; policy writers: ${r.policy.writers.join(", ") || "none"}`);
44
+ for (const e of r.policy.excluded) lines.push(` ${r.policy.signersPath}:${e.line} not used: ${e.reason}`);
45
+ }
46
+ for (const c of r.commits) {
47
+ const who = c.principal ? ` by ${c.principal}` : "";
48
+ const note = c.skipped ? ` (${c.skipped})` : c.level === "attested" ? "" : ` (${c.reason})`;
49
+ lines.push(` ${c.commit.slice(0, 8)} ${c.level}${who} ${c.subject}${note}`);
50
+ }
51
+ for (const w of r.protectedWrites) lines.push(` protected write ${w.commit.slice(0, 8)} ${w.paths.join(", ")}: ${w.allowed ? "allowed" : "refused"}, ${w.reason}`);
52
+ for (const n of r.notes) lines.push(n);
53
+ console.log(lines.join("\n"));
54
+ if (r.ok) console.log(formatSuccess(`verified ${r.commits.length} commits against the policy at base`));
55
+ else console.error(formatError({ message: `verification failed:\n${r.failures.map((f) => ` ${f}`).join("\n")}`, hint: USAGE }));
56
+ }