@intentius/chant 0.100.0 → 0.102.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 (608) hide show
  1. package/dist/agents/checks.d.ts.map +1 -1
  2. package/dist/attrref.d.ts.map +1 -1
  3. package/dist/audit/discover.d.ts.map +1 -1
  4. package/dist/audit/nginx.d.ts.map +1 -1
  5. package/dist/audit/prior-art.d.ts +48 -0
  6. package/dist/audit/prior-art.d.ts.map +1 -1
  7. package/dist/behaviour-kinds.d.ts.map +1 -1
  8. package/dist/build.d.ts +6 -0
  9. package/dist/build.d.ts.map +1 -1
  10. package/dist/builder.d.ts.map +1 -1
  11. package/dist/cli/build-options.d.ts +2 -0
  12. package/dist/cli/build-options.d.ts.map +1 -1
  13. package/dist/cli/commands/build.d.ts.map +1 -1
  14. package/dist/cli/commands/carve-emit.d.ts.map +1 -1
  15. package/dist/cli/commands/check-lexicon.d.ts +16 -0
  16. package/dist/cli/commands/check-lexicon.d.ts.map +1 -1
  17. package/dist/cli/commands/import.d.ts +8 -0
  18. package/dist/cli/commands/import.d.ts.map +1 -1
  19. package/dist/cli/commands/init-lexicon.d.ts.map +1 -1
  20. package/dist/cli/commands/init.d.ts +19 -0
  21. package/dist/cli/commands/init.d.ts.map +1 -1
  22. package/dist/cli/commands/onboard.d.ts +9 -0
  23. package/dist/cli/commands/onboard.d.ts.map +1 -1
  24. package/dist/cli/commands/pinned-upgrade.d.ts +19 -5
  25. package/dist/cli/commands/pinned-upgrade.d.ts.map +1 -1
  26. package/dist/cli/handlers/dev.d.ts.map +1 -1
  27. package/dist/cli/handlers/fan-out.d.ts.map +1 -1
  28. package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
  29. package/dist/cli/handlers/misc.d.ts.map +1 -1
  30. package/dist/cli/handlers/operator.d.ts +9 -0
  31. package/dist/cli/handlers/operator.d.ts.map +1 -1
  32. package/dist/cli/handlers/run.d.ts.map +1 -1
  33. package/dist/cli/lsp/server.d.ts.map +1 -1
  34. package/dist/cli/main.d.ts.map +1 -1
  35. package/dist/cli/mcp/server.d.ts.map +1 -1
  36. package/dist/cli/mcp/tools/build.d.ts.map +1 -1
  37. package/dist/cli/mcp/tools/composites.d.ts.map +1 -1
  38. package/dist/cli/mcp/tools/explain.d.ts.map +1 -1
  39. package/dist/cli/mcp/tools/import.d.ts.map +1 -1
  40. package/dist/cli/mcp/tools/lint.d.ts.map +1 -1
  41. package/dist/cli/mcp/tools/scaffold.d.ts.map +1 -1
  42. package/dist/cli/mcp/tools/search.d.ts.map +1 -1
  43. package/dist/cli/mcp/workspace-tools.d.ts +9 -1
  44. package/dist/cli/mcp/workspace-tools.d.ts.map +1 -1
  45. package/dist/cli/registry.d.ts +33 -2
  46. package/dist/cli/registry.d.ts.map +1 -1
  47. package/dist/codegen/fetch.d.ts.map +1 -1
  48. package/dist/codegen/naming.d.ts.map +1 -1
  49. package/dist/codegen/pinned-upgrade.d.ts +67 -3
  50. package/dist/codegen/pinned-upgrade.d.ts.map +1 -1
  51. package/dist/codegen/registry.d.ts.map +1 -1
  52. package/dist/components/capability-plugin-loader.d.ts.map +1 -1
  53. package/dist/components/capability.d.ts.map +1 -1
  54. package/dist/components/driver.d.ts.map +1 -1
  55. package/dist/components/fan-out.d.ts.map +1 -1
  56. package/dist/components/starter-plugin.d.ts.map +1 -1
  57. package/dist/components/verbs/process-runner.d.ts.map +1 -1
  58. package/dist/components/verbs/r2-sync.d.ts.map +1 -1
  59. package/dist/components/verbs/run-agent.d.ts.map +1 -1
  60. package/dist/components/verbs/sbom-generator.d.ts.map +1 -1
  61. package/dist/components/verbs/sign.d.ts.map +1 -1
  62. package/dist/components/verbs/verify.d.ts.map +1 -1
  63. package/dist/components/verbs/vuln-gate.d.ts.map +1 -1
  64. package/dist/components/verbs/vuln-scan.d.ts.map +1 -1
  65. package/dist/components/verbs/wrangler.d.ts.map +1 -1
  66. package/dist/composite.d.ts.map +1 -1
  67. package/dist/config.d.ts +30 -7
  68. package/dist/config.d.ts.map +1 -1
  69. package/dist/declarable.d.ts +16 -0
  70. package/dist/declarable.d.ts.map +1 -1
  71. package/dist/discovery/cache.d.ts.map +1 -1
  72. package/dist/discovery/fold-import.d.ts +15 -5
  73. package/dist/discovery/fold-import.d.ts.map +1 -1
  74. package/dist/discovery/param-deps.d.ts +16 -0
  75. package/dist/discovery/param-deps.d.ts.map +1 -1
  76. package/dist/discovery/sandbox/policy-wire.d.ts.map +1 -1
  77. package/dist/discovery/specifier-resolve.d.ts +37 -0
  78. package/dist/discovery/specifier-resolve.d.ts.map +1 -0
  79. package/dist/errors.d.ts.map +1 -1
  80. package/dist/fold/fold.d.ts +3 -1
  81. package/dist/fold/fold.d.ts.map +1 -1
  82. package/dist/graph-detail.d.ts.map +1 -1
  83. package/dist/graph-ir.d.ts.map +1 -1
  84. package/dist/graph-layout.d.ts.map +1 -1
  85. package/dist/import/base-parser.d.ts.map +1 -1
  86. package/dist/import/embedded.d.ts +7 -0
  87. package/dist/import/embedded.d.ts.map +1 -1
  88. package/dist/import/parser-options.d.ts +14 -0
  89. package/dist/import/parser-options.d.ts.map +1 -0
  90. package/dist/import/parser.d.ts +17 -0
  91. package/dist/import/parser.d.ts.map +1 -1
  92. package/dist/kubectl-context.d.ts.map +1 -1
  93. package/dist/lexicon-output.d.ts.map +1 -1
  94. package/dist/lexicon-schema.d.ts +3 -3
  95. package/dist/lexicon.d.ts +78 -3
  96. package/dist/lexicon.d.ts.map +1 -1
  97. package/dist/lifecycle/assert-live.d.ts.map +1 -1
  98. package/dist/lifecycle/gate-ledger.d.ts +15 -0
  99. package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
  100. package/dist/lifecycle/git.d.ts.map +1 -1
  101. package/dist/lifecycle/member-ledger.d.ts.map +1 -1
  102. package/dist/lifecycle/observe.d.ts.map +1 -1
  103. package/dist/lifecycle/plan-digest.d.ts +26 -5
  104. package/dist/lifecycle/plan-digest.d.ts.map +1 -1
  105. package/dist/lifecycle/plan-ledger.d.ts.map +1 -1
  106. package/dist/lifecycle/release-ledger.d.ts.map +1 -1
  107. package/dist/lifecycle/scenario.d.ts.map +1 -1
  108. package/dist/lifecycle/status.d.ts.map +1 -1
  109. package/dist/lifecycle/work-lease.d.ts +61 -2
  110. package/dist/lifecycle/work-lease.d.ts.map +1 -1
  111. package/dist/lint/config.d.ts +4 -4
  112. package/dist/lint/engine.d.ts +6 -0
  113. package/dist/lint/engine.d.ts.map +1 -1
  114. package/dist/lsp/lexicon-providers.d.ts.map +1 -1
  115. package/dist/op/activities/activity-contracts.d.ts +12 -11
  116. package/dist/op/activities/activity-contracts.d.ts.map +1 -1
  117. package/dist/op/activities/apply.d.ts +79 -9
  118. package/dist/op/activities/apply.d.ts.map +1 -1
  119. package/dist/op/activities/decide.d.ts.map +1 -1
  120. package/dist/op/activities/index.d.ts +1 -1
  121. package/dist/op/activities/index.d.ts.map +1 -1
  122. package/dist/op/activities/lexicon-upgrade.d.ts +44 -12
  123. package/dist/op/activities/lexicon-upgrade.d.ts.map +1 -1
  124. package/dist/op/activities/propose-upgrade.d.ts +2 -0
  125. package/dist/op/activities/propose-upgrade.d.ts.map +1 -1
  126. package/dist/op/activity-contract.d.ts +1 -1
  127. package/dist/op/activity-failure.d.ts.map +1 -1
  128. package/dist/op/activity-profiles.d.ts.map +1 -1
  129. package/dist/op/builders.d.ts.map +1 -1
  130. package/dist/op/composites/apply-op.d.ts +12 -5
  131. package/dist/op/composites/apply-op.d.ts.map +1 -1
  132. package/dist/op/composites/lexicon-upgrade-op.d.ts +22 -6
  133. package/dist/op/composites/lexicon-upgrade-op.d.ts.map +1 -1
  134. package/dist/op/decide-backend.d.ts.map +1 -1
  135. package/dist/op/decide-read-inputs.d.ts.map +1 -1
  136. package/dist/op/emulator-lifecycle.d.ts +21 -2
  137. package/dist/op/emulator-lifecycle.d.ts.map +1 -1
  138. package/dist/op/gate-approval.d.ts +5 -1
  139. package/dist/op/gate-approval.d.ts.map +1 -1
  140. package/dist/op/gate-wait.d.ts.map +1 -1
  141. package/dist/op/gate.d.ts +19 -3
  142. package/dist/op/gate.d.ts.map +1 -1
  143. package/dist/op/index.d.ts +3 -1
  144. package/dist/op/index.d.ts.map +1 -1
  145. package/dist/op/local-executor.d.ts +6 -0
  146. package/dist/op/local-executor.d.ts.map +1 -1
  147. package/dist/op/operator.d.ts.map +1 -1
  148. package/dist/op/run-context.d.ts +39 -0
  149. package/dist/op/run-context.d.ts.map +1 -0
  150. package/dist/op/run-live.d.ts.map +1 -1
  151. package/dist/op/runtimes/local.d.ts.map +1 -1
  152. package/dist/op/source-archive.d.ts.map +1 -1
  153. package/dist/op/steward-beside.d.ts.map +1 -1
  154. package/dist/op/steward-points.d.ts.map +1 -1
  155. package/dist/op/steward.d.ts.map +1 -1
  156. package/dist/op/types.d.ts +8 -3
  157. package/dist/op/types.d.ts.map +1 -1
  158. package/dist/op/work-lease-run.d.ts +24 -4
  159. package/dist/op/work-lease-run.d.ts.map +1 -1
  160. package/dist/project-root.d.ts.map +1 -1
  161. package/dist/provenance.d.ts +21 -0
  162. package/dist/provenance.d.ts.map +1 -1
  163. package/dist/pseudo-parameter.d.ts.map +1 -1
  164. package/dist/reconcile.d.ts.map +1 -1
  165. package/dist/secret-materialization.d.ts.map +1 -1
  166. package/dist/serializer.d.ts +8 -0
  167. package/dist/serializer.d.ts.map +1 -1
  168. package/dist/telemetry-attribution.d.ts +77 -0
  169. package/dist/telemetry-attribution.d.ts.map +1 -0
  170. package/dist/terraform/emit-source.d.ts.map +1 -1
  171. package/dist/terraform/parse.d.ts.map +1 -1
  172. package/dist/testing.d.ts.map +1 -1
  173. package/dist/workspace/__fixtures__/contract-repo.d.ts.map +1 -1
  174. package/dist/workspace/__fixtures__/decide-workspace.d.ts +1 -1
  175. package/dist/workspace/__fixtures__/decide-workspace.d.ts.map +1 -1
  176. package/dist/workspace/agent-cli.d.ts +88 -0
  177. package/dist/workspace/agent-cli.d.ts.map +1 -0
  178. package/dist/workspace/box-factory.d.ts +73 -0
  179. package/dist/workspace/box-factory.d.ts.map +1 -0
  180. package/dist/workspace/changes-cli.d.ts.map +1 -1
  181. package/dist/workspace/changes.d.ts +8 -1
  182. package/dist/workspace/changes.d.ts.map +1 -1
  183. package/dist/workspace/chant-migrations/chud-lexicon-exit.d.ts.map +1 -1
  184. package/dist/workspace/checks/boxes.d.ts.map +1 -1
  185. package/dist/workspace/checks/links.d.ts +1 -0
  186. package/dist/workspace/checks/links.d.ts.map +1 -1
  187. package/dist/workspace/checks/live.d.ts +40 -0
  188. package/dist/workspace/checks/live.d.ts.map +1 -0
  189. package/dist/workspace/checks.d.ts +26 -2
  190. package/dist/workspace/checks.d.ts.map +1 -1
  191. package/dist/workspace/compose-graph.d.ts +73 -0
  192. package/dist/workspace/compose-graph.d.ts.map +1 -1
  193. package/dist/workspace/conformance/index.d.ts +67 -6
  194. package/dist/workspace/conformance/index.d.ts.map +1 -1
  195. package/dist/workspace/conformance/vitest.d.ts +12 -3
  196. package/dist/workspace/conformance/vitest.d.ts.map +1 -1
  197. package/dist/workspace/conformance/writer.d.ts +368 -0
  198. package/dist/workspace/conformance/writer.d.ts.map +1 -0
  199. package/dist/workspace/decide.d.ts +1 -1
  200. package/dist/workspace/decide.d.ts.map +1 -1
  201. package/dist/workspace/declaration.d.ts +119 -0
  202. package/dist/workspace/declaration.d.ts.map +1 -1
  203. package/dist/workspace/declaration.schema.json +368 -4
  204. package/dist/workspace/export-cli.d.ts +12 -0
  205. package/dist/workspace/export-cli.d.ts.map +1 -0
  206. package/dist/workspace/export.d.ts +145 -0
  207. package/dist/workspace/export.d.ts.map +1 -0
  208. package/dist/workspace/generated-files.d.ts.map +1 -1
  209. package/dist/workspace/graph-cache.d.ts.map +1 -1
  210. package/dist/workspace/graph-cli.d.ts +2 -0
  211. package/dist/workspace/graph-cli.d.ts.map +1 -1
  212. package/dist/workspace/identity.d.ts +111 -0
  213. package/dist/workspace/identity.d.ts.map +1 -0
  214. package/dist/workspace/import.d.ts +73 -0
  215. package/dist/workspace/import.d.ts.map +1 -0
  216. package/dist/workspace/intent-cli.d.ts +7 -4
  217. package/dist/workspace/intent-cli.d.ts.map +1 -1
  218. package/dist/workspace/intent-record.d.ts +8 -1
  219. package/dist/workspace/intent-record.d.ts.map +1 -1
  220. package/dist/workspace/intent.d.ts +149 -5
  221. package/dist/workspace/intent.d.ts.map +1 -1
  222. package/dist/workspace/kinds.d.ts +24 -6
  223. package/dist/workspace/kinds.d.ts.map +1 -1
  224. package/dist/workspace/lineage-adopt-cli.d.ts +15 -0
  225. package/dist/workspace/lineage-adopt-cli.d.ts.map +1 -0
  226. package/dist/workspace/lineage-adopt.d.ts +106 -0
  227. package/dist/workspace/lineage-adopt.d.ts.map +1 -0
  228. package/dist/workspace/lineage-check.d.ts +9 -2
  229. package/dist/workspace/lineage-check.d.ts.map +1 -1
  230. package/dist/workspace/lineage-cli.d.ts +6 -1
  231. package/dist/workspace/lineage-cli.d.ts.map +1 -1
  232. package/dist/workspace/lineage-hash-index.d.ts +110 -0
  233. package/dist/workspace/lineage-hash-index.d.ts.map +1 -0
  234. package/dist/workspace/lineage-init.d.ts +10 -0
  235. package/dist/workspace/lineage-init.d.ts.map +1 -1
  236. package/dist/workspace/lineage-lock.d.ts +149 -2
  237. package/dist/workspace/lineage-lock.d.ts.map +1 -1
  238. package/dist/workspace/lineage-migrations.d.ts +15 -3
  239. package/dist/workspace/lineage-migrations.d.ts.map +1 -1
  240. package/dist/workspace/lineage-provenance.d.ts +18 -0
  241. package/dist/workspace/lineage-provenance.d.ts.map +1 -0
  242. package/dist/workspace/lineage-upgrade-cli.d.ts +2 -0
  243. package/dist/workspace/lineage-upgrade-cli.d.ts.map +1 -1
  244. package/dist/workspace/lineage-upgrade.d.ts +30 -1
  245. package/dist/workspace/lineage-upgrade.d.ts.map +1 -1
  246. package/dist/workspace/lineage-versions.d.ts +116 -0
  247. package/dist/workspace/lineage-versions.d.ts.map +1 -0
  248. package/dist/workspace/links.d.ts +31 -5
  249. package/dist/workspace/links.d.ts.map +1 -1
  250. package/dist/workspace/ls-generated.d.ts +37 -0
  251. package/dist/workspace/ls-generated.d.ts.map +1 -0
  252. package/dist/workspace/ls.d.ts +4 -0
  253. package/dist/workspace/ls.d.ts.map +1 -1
  254. package/dist/workspace/member-commands.d.ts.map +1 -1
  255. package/dist/workspace/nested-graph.d.ts +56 -0
  256. package/dist/workspace/nested-graph.d.ts.map +1 -0
  257. package/dist/workspace/nesting.d.ts +21 -0
  258. package/dist/workspace/nesting.d.ts.map +1 -0
  259. package/dist/workspace/pin-cli.d.ts +10 -0
  260. package/dist/workspace/pin-cli.d.ts.map +1 -0
  261. package/dist/workspace/pin-integrity.d.ts +51 -0
  262. package/dist/workspace/pin-integrity.d.ts.map +1 -0
  263. package/dist/workspace/points.d.ts.map +1 -1
  264. package/dist/workspace/principal-classes.d.ts +114 -0
  265. package/dist/workspace/principal-classes.d.ts.map +1 -0
  266. package/dist/workspace/reason-codes.d.ts +47 -3
  267. package/dist/workspace/reason-codes.d.ts.map +1 -1
  268. package/dist/workspace/record-assets.d.ts.map +1 -1
  269. package/dist/workspace/record-sessions.d.ts +7 -11
  270. package/dist/workspace/record-sessions.d.ts.map +1 -1
  271. package/dist/workspace/records-checkout.d.ts +54 -0
  272. package/dist/workspace/records-checkout.d.ts.map +1 -0
  273. package/dist/workspace/records-cli.d.ts +57 -2
  274. package/dist/workspace/records-cli.d.ts.map +1 -1
  275. package/dist/workspace/records-close.d.ts +5 -2
  276. package/dist/workspace/records-close.d.ts.map +1 -1
  277. package/dist/workspace/records-since.d.ts.map +1 -1
  278. package/dist/workspace/records-write.d.ts +23 -4
  279. package/dist/workspace/records-write.d.ts.map +1 -1
  280. package/dist/workspace/records.d.ts +58 -5
  281. package/dist/workspace/records.d.ts.map +1 -1
  282. package/dist/workspace/returns.d.ts +129 -0
  283. package/dist/workspace/returns.d.ts.map +1 -0
  284. package/dist/workspace/runs-cli.d.ts +164 -0
  285. package/dist/workspace/runs-cli.d.ts.map +1 -0
  286. package/dist/workspace/runs.d.ts +423 -0
  287. package/dist/workspace/runs.d.ts.map +1 -0
  288. package/dist/workspace/source-block.d.ts +1 -1
  289. package/dist/workspace/source-block.d.ts.map +1 -1
  290. package/dist/workspace/status-gates.d.ts +15 -3
  291. package/dist/workspace/status-gates.d.ts.map +1 -1
  292. package/dist/workspace/status.d.ts +10 -0
  293. package/dist/workspace/status.d.ts.map +1 -1
  294. package/dist/workspace/symbols.d.ts +52 -0
  295. package/dist/workspace/symbols.d.ts.map +1 -0
  296. package/dist/workspace/template-manifest.d.ts +11 -3
  297. package/dist/workspace/template-manifest.d.ts.map +1 -1
  298. package/dist/workspace/trailer-joins.d.ts +46 -0
  299. package/dist/workspace/trailer-joins.d.ts.map +1 -0
  300. package/dist/workspace/trailers.d.ts +89 -0
  301. package/dist/workspace/trailers.d.ts.map +1 -0
  302. package/dist/workspace/trust/attestor.d.ts +8 -0
  303. package/dist/workspace/trust/attestor.d.ts.map +1 -1
  304. package/dist/workspace/trust/dsse.d.ts +58 -0
  305. package/dist/workspace/trust/dsse.d.ts.map +1 -0
  306. package/dist/workspace/trust/evidence-cli.d.ts +66 -0
  307. package/dist/workspace/trust/evidence-cli.d.ts.map +1 -0
  308. package/dist/workspace/trust/evidence.d.ts +93 -0
  309. package/dist/workspace/trust/evidence.d.ts.map +1 -0
  310. package/dist/workspace/trust/policy.d.ts +54 -1
  311. package/dist/workspace/trust/policy.d.ts.map +1 -1
  312. package/dist/workspace/trust/provenance.d.ts +21 -1
  313. package/dist/workspace/trust/provenance.d.ts.map +1 -1
  314. package/dist/workspace/trust/rotation.d.ts +132 -0
  315. package/dist/workspace/trust/rotation.d.ts.map +1 -0
  316. package/dist/workspace/trust/seal.d.ts +33 -0
  317. package/dist/workspace/trust/seal.d.ts.map +1 -1
  318. package/dist/workspace/trust/signers-cli.d.ts +49 -0
  319. package/dist/workspace/trust/signers-cli.d.ts.map +1 -0
  320. package/dist/workspace/trust/ssh-commit.d.ts +12 -0
  321. package/dist/workspace/trust/ssh-commit.d.ts.map +1 -1
  322. package/dist/workspace/trust/test-repo.d.ts +13 -0
  323. package/dist/workspace/trust/test-repo.d.ts.map +1 -1
  324. package/dist/workspace/trust/verify.d.ts +15 -0
  325. package/dist/workspace/trust/verify.d.ts.map +1 -1
  326. package/dist/workspace/work-cli.d.ts +39 -3
  327. package/dist/workspace/work-cli.d.ts.map +1 -1
  328. package/dist/workspace/work-evidence.d.ts +1 -1
  329. package/dist/workspace/work-evidence.d.ts.map +1 -1
  330. package/dist/workspace/work.d.ts +26 -1
  331. package/dist/workspace/work.d.ts.map +1 -1
  332. package/dist/workspace/workspace-principals.schema.json +60 -0
  333. package/dist/workspace/write-scope.d.ts +259 -0
  334. package/dist/workspace/write-scope.d.ts.map +1 -0
  335. package/dist/yaml.d.ts +21 -39
  336. package/dist/yaml.d.ts.map +1 -1
  337. package/package.json +2 -2
  338. package/src/audit/prior-art.ts +15 -0
  339. package/src/build.ts +8 -0
  340. package/src/cli/build-options.ts +4 -1
  341. package/src/cli/commands/__fixtures__/init-lexicon-output/package.json +1 -1
  342. package/src/cli/commands/build.ts +2 -0
  343. package/src/cli/commands/carve-emit-state.test.ts +4 -0
  344. package/src/cli/commands/carve-emit.ts +5 -19
  345. package/src/cli/commands/check-lexicon.test.ts +62 -1
  346. package/src/cli/commands/check-lexicon.ts +36 -6
  347. package/src/cli/commands/import-live.test.ts +87 -1
  348. package/src/cli/commands/import-parser-options.test.ts +133 -0
  349. package/src/cli/commands/import.ts +69 -25
  350. package/src/cli/commands/init-lexicon/templates/project.ts +1 -1
  351. package/src/cli/commands/init.test.ts +8 -1
  352. package/src/cli/commands/init.ts +23 -8
  353. package/src/cli/commands/lint.test.ts +37 -0
  354. package/src/cli/commands/onboard.test.ts +53 -3
  355. package/src/cli/commands/onboard.ts +52 -4
  356. package/src/cli/commands/pinned-upgrade.ts +73 -33
  357. package/src/cli/handlers/__snapshots__/build-member.test.ts.snap +2 -2
  358. package/src/cli/handlers/components.ts +2 -2
  359. package/src/cli/handlers/dev.ts +8 -6
  360. package/src/cli/handlers/fan-out.test.ts +6 -6
  361. package/src/cli/handlers/fan-out.ts +2 -1
  362. package/src/cli/handlers/graph.test.ts +42 -0
  363. package/src/cli/handlers/graph.ts +22 -0
  364. package/src/cli/handlers/lifecycle.test.ts +67 -0
  365. package/src/cli/handlers/lifecycle.ts +6 -5
  366. package/src/cli/handlers/misc.ts +2 -0
  367. package/src/cli/handlers/operator.ts +88 -4
  368. package/src/cli/handlers/run.test.ts +111 -0
  369. package/src/cli/handlers/run.ts +85 -24
  370. package/src/cli/handlers/scenario.ts +2 -2
  371. package/src/cli/main.test.ts +33 -0
  372. package/src/cli/main.ts +168 -17
  373. package/src/cli/mcp/workspace-tools.test.ts +5 -2
  374. package/src/cli/mcp/workspace-tools.ts +62 -4
  375. package/src/cli/registry.ts +33 -2
  376. package/src/cli/serve-mcp-workspace.test.ts +1 -1
  377. package/src/codegen/pinned-upgrade-multi.test.ts +157 -0
  378. package/src/codegen/pinned-upgrade-tags.test.ts +112 -0
  379. package/src/codegen/pinned-upgrade.test.ts +104 -0
  380. package/src/codegen/pinned-upgrade.ts +250 -56
  381. package/src/codegen/release-wiring.test.ts +5 -1
  382. package/src/codegen/testdata/postgres-tags.json +242 -0
  383. package/src/components/fan-out-output.test.ts +1 -1
  384. package/src/components/fan-out.test.ts +1 -1
  385. package/src/components/promote.test.ts +1 -1
  386. package/src/config.test.ts +21 -0
  387. package/src/config.ts +27 -0
  388. package/src/content-digest.test.ts +2 -2
  389. package/src/declarable.test.ts +21 -0
  390. package/src/declarable.ts +19 -0
  391. package/src/discovery/fold-composite.test.ts +41 -5
  392. package/src/discovery/fold-import.test.ts +129 -0
  393. package/src/discovery/fold-import.ts +643 -131
  394. package/src/discovery/fold-package-composite.test.ts +264 -0
  395. package/src/discovery/fold-specifiers.test.ts +191 -0
  396. package/src/discovery/param-deps.test.ts +66 -1
  397. package/src/discovery/param-deps.ts +45 -0
  398. package/src/discovery/specifier-resolve.ts +344 -0
  399. package/src/fold/fold.test.ts +39 -1
  400. package/src/fold/fold.ts +83 -9
  401. package/src/fold-provenance.test.ts +140 -0
  402. package/src/graph-ir.test.ts +83 -0
  403. package/src/graph-ir.ts +29 -0
  404. package/src/import/embedded.test.ts +20 -0
  405. package/src/import/embedded.ts +13 -0
  406. package/src/import/parser-options.ts +46 -0
  407. package/src/import/parser.ts +20 -0
  408. package/src/lexicon.ts +79 -3
  409. package/src/lifecycle/gate-ledger.test.ts +14 -0
  410. package/src/lifecycle/gate-ledger.ts +18 -1
  411. package/src/lifecycle/observe.test.ts +18 -0
  412. package/src/lifecycle/observe.ts +6 -4
  413. package/src/lifecycle/plan-digest.test.ts +54 -3
  414. package/src/lifecycle/plan-digest.ts +38 -8
  415. package/src/lifecycle/snapshot.ts +2 -2
  416. package/src/lifecycle/work-lease.test.ts +1 -1
  417. package/src/lifecycle/work-lease.ts +93 -2
  418. package/src/lint/engine.test.ts +55 -0
  419. package/src/lint/engine.ts +30 -4
  420. package/src/lint/output-docs.test.ts +59 -56
  421. package/src/op/activities/activity-contracts.ts +1 -0
  422. package/src/op/activities/apply.test.ts +189 -5
  423. package/src/op/activities/apply.ts +230 -8
  424. package/src/op/activities/index.ts +3 -0
  425. package/src/op/activities/lexicon-upgrade.test.ts +160 -6
  426. package/src/op/activities/lexicon-upgrade.ts +154 -51
  427. package/src/op/activities/propose-upgrade.ts +8 -5
  428. package/src/op/composites/apply-op.ts +12 -5
  429. package/src/op/composites/composites.test.ts +30 -3
  430. package/src/op/composites/lexicon-upgrade-op.test.ts +7 -2
  431. package/src/op/composites/lexicon-upgrade-op.ts +29 -8
  432. package/src/op/emulator-lifecycle.test.ts +23 -0
  433. package/src/op/emulator-lifecycle.ts +52 -14
  434. package/src/op/gate-approval.test.ts +91 -0
  435. package/src/op/gate-approval.ts +5 -1
  436. package/src/op/gate.ts +34 -8
  437. package/src/op/index.ts +3 -1
  438. package/src/op/local-executor.test.ts +15 -0
  439. package/src/op/local-executor.ts +40 -12
  440. package/src/op/local-output.ts +1 -1
  441. package/src/op/operator.ts +4 -0
  442. package/src/op/run-context.test.ts +141 -0
  443. package/src/op/run-context.ts +51 -0
  444. package/src/op/runtimes/local.test.ts +14 -0
  445. package/src/op/runtimes/local.ts +2 -0
  446. package/src/op/steward-beside.ts +2 -0
  447. package/src/op/types.ts +8 -3
  448. package/src/op/work-lease-run.test.ts +47 -0
  449. package/src/op/work-lease-run.ts +73 -11
  450. package/src/provenance.ts +35 -0
  451. package/src/serializer.ts +9 -0
  452. package/src/telemetry-attribution.test.ts +91 -0
  453. package/src/telemetry-attribution.ts +145 -0
  454. package/src/workspace/__fixtures__/studio-template-84c8b21.json +1 -1
  455. package/src/workspace/agent-cli.ts +138 -0
  456. package/src/workspace/agent.schema.json +378 -0
  457. package/src/workspace/behold-kinds.test.ts +1 -1
  458. package/src/workspace/box-factory.test.ts +320 -0
  459. package/src/workspace/box-factory.ts +103 -0
  460. package/src/workspace/changes-cli.ts +5 -0
  461. package/src/workspace/changes.schema.json +171 -1
  462. package/src/workspace/changes.ts +61 -4
  463. package/src/workspace/check-live.test.ts +192 -0
  464. package/src/workspace/check.schema.json +64 -0
  465. package/src/workspace/checks/boxes.test.ts +2 -0
  466. package/src/workspace/checks/links.ts +23 -2
  467. package/src/workspace/checks/live.ts +113 -0
  468. package/src/workspace/checks.test.ts +2 -2
  469. package/src/workspace/checks.ts +49 -8
  470. package/src/workspace/compose-graph.test.ts +47 -0
  471. package/src/workspace/compose-graph.ts +127 -3
  472. package/src/workspace/conformance/__fixture__/decisions/decision.kind.mjs +7 -1
  473. package/src/workspace/conformance/__fixture__/decisions/decision.schema.json +5 -0
  474. package/src/workspace/conformance/__writer_fixture__/answers/answer.kind.mjs +27 -0
  475. package/src/workspace/conformance/__writer_fixture__/answers/answer.schema.json +95 -0
  476. package/src/workspace/conformance/__writer_fixture__/decisions/points.json +132 -0
  477. package/src/workspace/conformance/__writer_fixture__/sessions/session.kind.mjs +48 -0
  478. package/src/workspace/conformance/__writer_fixture__/sessions/session.schema.json +108 -0
  479. package/src/workspace/conformance/__writer_fixture__/work/W-001-the-writer-suite-works-this-item.md +25 -0
  480. package/src/workspace/conformance/__writer_fixture__/work/work.kind.mjs +60 -0
  481. package/src/workspace/conformance/__writer_fixture__/work/work.schema.json +578 -0
  482. package/src/workspace/conformance/conformance.e2e.test.ts +35 -3
  483. package/src/workspace/conformance/index.mjs +30 -3
  484. package/src/workspace/conformance/index.ts +103 -15
  485. package/src/workspace/conformance/vitest.ts +44 -2
  486. package/src/workspace/conformance/writer-conformance.e2e.test.ts +131 -0
  487. package/src/workspace/conformance/writer.test.ts +156 -0
  488. package/src/workspace/conformance/writer.ts +880 -0
  489. package/src/workspace/decide.ts +7 -1
  490. package/src/workspace/declaration.schema.json +368 -4
  491. package/src/workspace/declaration.ts +270 -1
  492. package/src/workspace/declared-kinds.test.ts +33 -0
  493. package/src/workspace/evidence.schema.json +279 -0
  494. package/src/workspace/export-cli.ts +180 -0
  495. package/src/workspace/export-import.test.ts +282 -0
  496. package/src/workspace/export.ts +486 -0
  497. package/src/workspace/graph-cli.ts +51 -2
  498. package/src/workspace/graph-contract.test.ts +89 -4
  499. package/src/workspace/graph.schema.json +453 -1
  500. package/src/workspace/identity.test.ts +196 -0
  501. package/src/workspace/identity.ts +186 -0
  502. package/src/workspace/import.ts +325 -0
  503. package/src/workspace/intent-cli.ts +64 -10
  504. package/src/workspace/intent-record.schema.json +208 -0
  505. package/src/workspace/intent-record.ts +35 -1
  506. package/src/workspace/intent-why.test.ts +274 -0
  507. package/src/workspace/intent.schema.json +718 -7
  508. package/src/workspace/intent.test.ts +11 -2
  509. package/src/workspace/intent.ts +495 -32
  510. package/src/workspace/kinds.test.ts +5 -5
  511. package/src/workspace/kinds.ts +62 -33
  512. package/src/workspace/lineage-adopt-cli.ts +103 -0
  513. package/src/workspace/lineage-adopt.test.ts +552 -0
  514. package/src/workspace/lineage-adopt.ts +452 -0
  515. package/src/workspace/lineage-check.ts +26 -5
  516. package/src/workspace/lineage-cli.ts +12 -2
  517. package/src/workspace/lineage-hash-index.ts +305 -0
  518. package/src/workspace/lineage-init.test.ts +9 -0
  519. package/src/workspace/lineage-init.ts +29 -9
  520. package/src/workspace/lineage-lock.ts +54 -0
  521. package/src/workspace/lineage-migrations.test.ts +27 -0
  522. package/src/workspace/lineage-migrations.ts +44 -14
  523. package/src/workspace/lineage-provenance.ts +40 -0
  524. package/src/workspace/lineage-upgrade-cli.ts +9 -5
  525. package/src/workspace/lineage-upgrade.test.ts +92 -1
  526. package/src/workspace/lineage-upgrade.ts +111 -16
  527. package/src/workspace/lineage-versions.ts +348 -0
  528. package/src/workspace/links.test.ts +121 -2
  529. package/src/workspace/links.ts +97 -6
  530. package/src/workspace/ls-contract.test.ts +84 -1
  531. package/src/workspace/ls-generated.ts +111 -0
  532. package/src/workspace/ls.schema.json +19 -0
  533. package/src/workspace/ls.ts +8 -1
  534. package/src/workspace/member-commands.ts +3 -1
  535. package/src/workspace/nested-graph.test.ts +176 -0
  536. package/src/workspace/nested-graph.ts +169 -0
  537. package/src/workspace/nesting.ts +37 -0
  538. package/src/workspace/pin-cli.test.ts +71 -0
  539. package/src/workspace/pin-cli.ts +57 -0
  540. package/src/workspace/pin-integrity.test.ts +121 -0
  541. package/src/workspace/pin-integrity.ts +104 -0
  542. package/src/workspace/points-write.schema.json +2 -0
  543. package/src/workspace/principal-classes.test.ts +175 -0
  544. package/src/workspace/principal-classes.ts +307 -0
  545. package/src/workspace/read-contract.test.ts +24 -0
  546. package/src/workspace/reason-codes.test.ts +20 -2
  547. package/src/workspace/reason-codes.ts +53 -3
  548. package/src/workspace/record-sessions.ts +12 -14
  549. package/src/workspace/records-amend.schema.json +6 -0
  550. package/src/workspace/records-checkout.ts +106 -0
  551. package/src/workspace/records-cli.ts +195 -22
  552. package/src/workspace/records-close.schema.json +7 -2
  553. package/src/workspace/records-close.ts +10 -2
  554. package/src/workspace/records-formats.test.ts +13 -5
  555. package/src/workspace/records-new.schema.json +6 -0
  556. package/src/workspace/records-review.schema.json +6 -0
  557. package/src/workspace/records-sessions.test.ts +23 -6
  558. package/src/workspace/records-uncommitted.test.ts +171 -0
  559. package/src/workspace/records-write.test.ts +65 -8
  560. package/src/workspace/records-write.ts +92 -6
  561. package/src/workspace/records.schema.json +127 -4
  562. package/src/workspace/records.ts +123 -5
  563. package/src/workspace/returns.ts +328 -0
  564. package/src/workspace/runs-cli.ts +381 -0
  565. package/src/workspace/runs-write.schema.json +583 -0
  566. package/src/workspace/runs.schema.json +810 -0
  567. package/src/workspace/runs.test.ts +242 -0
  568. package/src/workspace/runs.ts +709 -0
  569. package/src/workspace/signers.schema.json +206 -0
  570. package/src/workspace/status-contract.test.ts +5 -0
  571. package/src/workspace/status-gates.ts +24 -9
  572. package/src/workspace/status.schema.json +127 -2
  573. package/src/workspace/status.ts +21 -1
  574. package/src/workspace/symbols.ts +180 -0
  575. package/src/workspace/template-manifest.ts +22 -4
  576. package/src/workspace/trailer-joins.test.ts +171 -0
  577. package/src/workspace/trailer-joins.ts +82 -0
  578. package/src/workspace/trailers.ts +148 -0
  579. package/src/workspace/trust/attestor.ts +15 -0
  580. package/src/workspace/trust/dsse.ts +134 -0
  581. package/src/workspace/trust/evidence-cli.ts +195 -0
  582. package/src/workspace/trust/evidence.test.ts +241 -0
  583. package/src/workspace/trust/evidence.ts +207 -0
  584. package/src/workspace/trust/policy.ts +110 -3
  585. package/src/workspace/trust/provenance.ts +41 -4
  586. package/src/workspace/trust/record-seal.test.ts +1 -1
  587. package/src/workspace/trust/rotation.test.ts +258 -0
  588. package/src/workspace/trust/rotation.ts +336 -0
  589. package/src/workspace/trust/seal.ts +63 -1
  590. package/src/workspace/trust/signers-cli.ts +178 -0
  591. package/src/workspace/trust/ssh-commit.ts +53 -4
  592. package/src/workspace/trust/test-repo.ts +18 -0
  593. package/src/workspace/trust/trust.test.ts +8 -2
  594. package/src/workspace/trust/verify-cli.ts +1 -0
  595. package/src/workspace/trust/verify.ts +22 -0
  596. package/src/workspace/work-cli.e2e.test.ts +28 -1
  597. package/src/workspace/work-cli.ts +133 -8
  598. package/src/workspace/work-evidence.schema.json +6 -0
  599. package/src/workspace/work-history.schema.json +73 -1
  600. package/src/workspace/work-history.test.ts +48 -5
  601. package/src/workspace/work-kind-fields.test.ts +154 -0
  602. package/src/workspace/work-lease.schema.json +14 -1
  603. package/src/workspace/work.ts +82 -0
  604. package/src/workspace/workspace-principals.schema.json +60 -0
  605. package/src/workspace/write-scope.test.ts +466 -0
  606. package/src/workspace/write-scope.ts +573 -0
  607. package/src/yaml.test.ts +111 -1
  608. package/src/yaml.ts +141 -549
@@ -36,6 +36,12 @@ export const REVIEW_SEAL_NAMESPACE = "chant-review";
36
36
  /** The ssh signature namespace of a record's author seal (#2688). */
37
37
  export const RECORD_SEAL_NAMESPACE = "chant-record";
38
38
 
39
+ /**
40
+ * The ssh signature namespace of a gate approval's seal (#3163, ws-080), so a
41
+ * verdict, record or commit signature can't stand in for one.
42
+ */
43
+ export const GATE_SEAL_NAMESPACE = "chant-gate";
44
+
39
45
  /** A seal as a review entry holds it. */
40
46
  export interface VerdictSeal {
41
47
  /** The principal who signed: the reviewer, as the signers file names them. */
@@ -81,6 +87,27 @@ export function recordSealPayload(record: string, digest: string, author: string
81
87
  return Buffer.from(`${record}\n${digest}\n${author}\n${state ?? ""}`, "utf-8");
82
88
  }
83
89
 
90
+ /** The gate approval a seal covers (#3163): the fields of a gate resolution line that say what was approved, by whom and when. */
91
+ export interface SealedGateApproval {
92
+ op: string;
93
+ gate: string;
94
+ environment?: string;
95
+ planDigest?: string;
96
+ resolvedBy: string;
97
+ timestamp: string;
98
+ seal?: unknown;
99
+ }
100
+
101
+ /**
102
+ * The bytes a gate approval's seal signs (#3163): the op (or component), the
103
+ * gate, the environment, the plan digest, the approver and the time, joined
104
+ * by LF with no final newline. An approval that binds no environment or plan
105
+ * signs an empty line for it.
106
+ */
107
+ export function gateSealPayload(a: SealedGateApproval): Buffer {
108
+ return Buffer.from(`${a.op}\n${a.gate}\n${a.environment ?? ""}\n${a.planDigest ?? ""}\n${a.resolvedBy}\n${a.timestamp}`, "utf-8");
109
+ }
110
+
84
111
  /** The record an author seal covers (#2688). */
85
112
  export interface SealedRecord {
86
113
  record: string | null;
@@ -155,6 +182,22 @@ export function checkRecordSeal(policy: TrustPolicy, r: SealedRecord): SealCheck
155
182
  });
156
183
  }
157
184
 
185
+ /**
186
+ * Check a gate approval's seal (#3163), as a verdict's is checked, over
187
+ * {@link gateSealPayload} in the `chant-gate` namespace: it verifies only
188
+ * when the signers file at base lists a key for the approver.
189
+ */
190
+ export function checkGateSeal(policy: TrustPolicy, a: SealedGateApproval): SealCheck {
191
+ return checkSeal(policy, {
192
+ principal: a.resolvedBy,
193
+ what: `the approval of gate ${a.gate} by ${a.resolvedBy}`,
194
+ role: "approver",
195
+ namespace: GATE_SEAL_NAMESPACE,
196
+ seal: a.seal,
197
+ payload: gateSealPayload(a),
198
+ });
199
+ }
200
+
158
201
  function checkSeal(policy: TrustPolicy, s: SealSubject): SealCheck {
159
202
  const where = `${policy.signersPath} at base`;
160
203
  if (s.seal === undefined || s.seal === null) {
@@ -176,6 +219,17 @@ function checkSeal(policy: TrustPolicy, s: SealSubject): SealCheck {
176
219
  if (typeof s.payload === "string") return { attested: false, code: "seal-signature-invalid", message: s.payload };
177
220
  if (!policy.active) return checkIntegrity(s.principal, s.payload, seal.signature, s.namespace);
178
221
  const listed = policy.signers.filter((x) => normalise(x.principal) === normalise(s.principal));
222
+ if (listed.length === 0 && policy.returnedFrom !== undefined) {
223
+ // Returned work (#2552, ws-004): a signer the service never saw is unverifiable until an admin admits them.
224
+ const intact = checkIntegrity(s.principal, s.payload, seal.signature, s.namespace);
225
+ if (intact.attested === false) return intact;
226
+ return {
227
+ attested: null,
228
+ code: "seal-unverifiable",
229
+ message: `${s.what} came back in return ${policy.returnedFrom}, sealed by ${s.principal}${intact.key ? ` with ${intact.key}` : ""}, whom neither ${where} nor an admission for that return lists; it counts once an admin admits the signer (chant workspace admit ${policy.returnedFrom})`,
230
+ ...(intact.key ? { key: intact.key } : {}),
231
+ };
232
+ }
179
233
  if (listed.length === 0) {
180
234
  return { attested: false, code: "seal-signer-unlisted", message: `${s.principal} has no key in ${where}, so the seal can't count` };
181
235
  }
@@ -194,7 +248,7 @@ function checkIntegrity(principal: string, payload: Buffer, signature: string, n
194
248
  const r = sshKeygen(["-Y", "check-novalidate", "-n", namespace, "-s", sigFile], payload);
195
249
  if (r.missing) return { attested: null, code: "seal-unverifiable", message: `the seal by ${principal} can't be checked here: ssh-keygen is not installed` };
196
250
  if (r.status !== 0) {
197
- return { attested: false, code: "seal-signature-invalid", message: `the seal by ${principal} does not verify over ${namespace === RECORD_SEAL_NAMESPACE ? "this record" : "this verdict"}, even without a signers file` };
251
+ return { attested: false, code: "seal-signature-invalid", message: `the seal by ${principal} does not verify over ${namespace === RECORD_SEAL_NAMESPACE ? "this record" : namespace === GATE_SEAL_NAMESPACE ? "this approval" : "this verdict"}, even without a signers file` };
198
252
  }
199
253
  const key = FINGERPRINT.exec(r.stdout + r.stderr)?.[1];
200
254
  return {
@@ -268,6 +322,14 @@ export function sealRecord(keyFile: string, r: { record: string; digest: string;
268
322
  return sign(keyFile, recordSealPayload(r.record, r.digest, r.author, r.state), RECORD_SEAL_NAMESPACE, r.author);
269
323
  }
270
324
 
325
+ /**
326
+ * Seal a gate approval (#3163) with the key in `keyFile`, over
327
+ * {@link gateSealPayload}. Throws a {@link SealError}.
328
+ */
329
+ export function sealGateApproval(keyFile: string, a: SealedGateApproval): VerdictSeal {
330
+ return sign(keyFile, gateSealPayload(a), GATE_SEAL_NAMESPACE, a.resolvedBy);
331
+ }
332
+
271
333
  function sign(keyFile: string, payload: Buffer, namespace: string, signer: string): VerdictSeal {
272
334
  const signed = sshKeygen(["-q", "-Y", "sign", "-n", namespace, "-f", keyFile], payload);
273
335
  if (signed.missing) throw new SealError("--sign needs ssh-keygen, and it is not installed here");
@@ -0,0 +1,178 @@
1
+ /**
2
+ * `chant workspace signers [rotate [--threshold <n>] | sign --key <file>]`
3
+ * (#2553): show the signer history, and propose a new signer set.
4
+ *
5
+ * Rotating is three steps: edit the signers file, run `rotate` to write the
6
+ * rotation file for the next version, and have a threshold of the current
7
+ * signers each run `sign` with their own key. `chant workspace verify` then
8
+ * checks the result against the set at base.
9
+ */
10
+
11
+ import { execFileSync } from "node:child_process";
12
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
13
+ import { join } from "node:path";
14
+ import { formatError, formatSuccess } from "../../cli/format";
15
+ import type { CommandContext } from "../../cli/registry";
16
+ import { readerVersion } from "../declaration";
17
+ import type { ReasonCode } from "../reason-codes";
18
+ import { gitRoot } from "../record-source";
19
+ import { parseAllowedSigners, readTrustPolicy } from "./policy";
20
+ import { resolveBase } from "./provenance";
21
+ import { gitRevisionSource } from "../record-source";
22
+ import {
23
+ distinctHolders,
24
+ rotationFileSchema,
25
+ rotationPath,
26
+ rotationStatement,
27
+ ROTATION_NAMESPACE,
28
+ signerHistory,
29
+ signersDigest,
30
+ type RotationFile,
31
+ type RotationRefusalCode,
32
+ type SignerHistory,
33
+ } from "./rotation";
34
+ import { verifySshSignature } from "./ssh-commit";
35
+
36
+ const USAGE = "chant workspace signers [--base <rev>] [--json] | signers rotate [--threshold <n>] | signers sign --key <ssh private key>";
37
+
38
+ export const SIGNERS_CONTRACT_VERSION = 1;
39
+ export const SIGNERS_OUTPUT_SCHEMA_ID = "https://intentius.io/chant/schemas/workspace/signers/v1/signers.schema.json";
40
+
41
+ /** Why `chant workspace signers` prints no history. Each is in `reason-codes.ts` and `signers.schema.json`. */
42
+ export const SIGNERS_ERROR_CODES = ["not-a-git-repository", "revision-unknown", "signers-file-missing"] as const satisfies readonly ReasonCode[];
43
+ export type SignersErrorCode = (typeof SIGNERS_ERROR_CODES)[number];
44
+
45
+ /** What `chant workspace signers --json` prints. */
46
+ export type SignersDocument =
47
+ | {
48
+ $schema: string;
49
+ contract: number;
50
+ chant: string;
51
+ base: string;
52
+ signersPath: string;
53
+ versions: Array<{ version: number; commit: string | null; digest: string | null; threshold: number; principals: string[]; signedBy: string[] }>;
54
+ broken: { commit: string | null; code: RotationRefusalCode; message: string } | null;
55
+ }
56
+ | { $schema: string; contract: number; chant: string; error: { code: SignersErrorCode; message: string } };
57
+
58
+ function head() {
59
+ return { $schema: SIGNERS_OUTPUT_SCHEMA_ID, contract: SIGNERS_CONTRACT_VERSION, chant: readerVersion() };
60
+ }
61
+
62
+ /** The signer history at `base` as the `--json` document. */
63
+ export function signersDocument(repo: string, base: string, signersPath: string, history: SignerHistory): SignersDocument {
64
+ return {
65
+ ...head(),
66
+ base,
67
+ signersPath,
68
+ versions: history.versions.map((v) => ({
69
+ version: v.version,
70
+ commit: v.commit,
71
+ digest: v.version === 0 ? null : v.digest,
72
+ threshold: v.threshold,
73
+ principals: [...new Set(v.signers.map((s) => s.principal))].sort(),
74
+ signedBy: v.signedBy,
75
+ })),
76
+ broken: history.broken ? { commit: history.broken.commit, code: history.broken.code, message: history.broken.reason } : null,
77
+ };
78
+ }
79
+
80
+ function fail(message: string): number {
81
+ console.error(formatError({ message, hint: USAGE }));
82
+ return 1;
83
+ }
84
+
85
+ export async function runWorkspaceSigners(ctx: CommandContext): Promise<number> {
86
+ const { args } = ctx;
87
+ const sub = args.extraPositional;
88
+ const refuse = (code: SignersErrorCode, message: string): number => {
89
+ if (args.json && !sub) console.log(JSON.stringify({ ...head(), error: { code, message } }, null, 2));
90
+ else console.error(formatError({ message, hint: USAGE }));
91
+ return 1;
92
+ };
93
+ const repo = gitRoot(process.cwd());
94
+ if (!repo) return refuse("not-a-git-repository", "signer history is read from git, and this directory is not in a git repository");
95
+ const base = resolveBase(repo, args.base);
96
+ if (!base.commit) return refuse("revision-unknown", base.problem ?? "no base revision");
97
+ // The policy as written at base; its history is checked below, not taken on trust.
98
+ const policy = readTrustPolicy(gitRevisionSource(repo, base.commit), base.commit);
99
+ if (!policy.active) return refuse("signers-file-missing", `there is no signers file (${policy.signersPath}) at base ${base.commit.slice(0, 8)}`);
100
+ const history = signerHistory(repo, base.commit, policy.signersPath);
101
+ const latest = history.versions.at(-1);
102
+
103
+ if (!sub) {
104
+ if (args.json) {
105
+ console.log(JSON.stringify(signersDocument(repo, base.commit, policy.signersPath, history), null, 2));
106
+ return history.broken ? 1 : 0;
107
+ }
108
+ for (const v of history.versions) {
109
+ if (v.version === 0) {
110
+ console.log(` ${v.commit?.slice(0, 8)} signers file removed`);
111
+ continue;
112
+ }
113
+ const by = v.signedBy.length ? `, signed by ${v.signedBy.join(", ")}` : ", the first version";
114
+ console.log(` ${v.commit?.slice(0, 8)} version ${v.version}: ${new Set(v.signers.map((s) => s.principal)).size} signers, threshold ${v.threshold}${by}`);
115
+ }
116
+ if (history.broken) return fail(`the history breaks at ${history.broken.commit?.slice(0, 8)} (${history.broken.code}): ${history.broken.reason}`);
117
+ return 0;
118
+ }
119
+ if (history.broken || !latest || latest.version === 0) return fail("the signer history at base is not valid, so there is no version to rotate from");
120
+
121
+ const signersFile = join(repo, policy.signersPath);
122
+ const rotationFile = join(repo, rotationPath(policy.signersPath));
123
+ const text = readFileSync(signersFile, "utf-8");
124
+
125
+ if (sub === "rotate") {
126
+ const threshold = args.threshold !== undefined ? Number(args.threshold) : latest.threshold;
127
+ if (!Number.isInteger(threshold) || threshold < 1) return fail("--threshold takes a whole number of at least 1");
128
+ const holders = distinctHolders(parseAllowedSigners(text).signers);
129
+ if (threshold > holders) return fail(`--threshold ${threshold} is more than the ${holders} distinct signers in the new set`);
130
+ const rotation: RotationFile = { schema: 1, version: latest.version + 1, previous: latest.digest, threshold, signatures: [] };
131
+ writeFileSync(rotationFile, JSON.stringify(rotation, null, 2) + "\n");
132
+ console.log(
133
+ formatSuccess(
134
+ `wrote ${rotationPath(policy.signersPath)} for version ${rotation.version}; ${latest.threshold} of the version ${latest.version} signers must now run chant workspace signers sign --key <their key>`,
135
+ ),
136
+ );
137
+ return 0;
138
+ }
139
+
140
+ if (sub === "sign") {
141
+ if (!args.key) return fail("--key <ssh private key> is required");
142
+ if (!existsSync(rotationFile)) return fail(`there is no ${rotationPath(policy.signersPath)}; run chant workspace signers rotate first`);
143
+ let raw: unknown;
144
+ try {
145
+ raw = JSON.parse(readFileSync(rotationFile, "utf-8"));
146
+ } catch {
147
+ return fail(`${rotationPath(policy.signersPath)} is not JSON`);
148
+ }
149
+ const parsed = rotationFileSchema.safeParse(raw);
150
+ if (!parsed.success) return fail(`${rotationPath(policy.signersPath)} is invalid`);
151
+ if (parsed.data.version !== latest.version + 1 || parsed.data.previous !== latest.digest) {
152
+ return fail(`${rotationPath(policy.signersPath)} is not the next version after version ${latest.version} at base; run chant workspace signers rotate again`);
153
+ }
154
+ const rotation = parsed.data;
155
+ const statement = rotationStatement(rotation, signersDigest(text));
156
+ let pub: string;
157
+ try {
158
+ pub = execFileSync("ssh-keygen", ["-y", "-f", args.key], { encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] }).trim().split(/\s+/).slice(0, 2).join(" ");
159
+ } catch (err) {
160
+ return fail(`ssh-keygen could not read ${args.key}: ${err instanceof Error ? err.message.split("\n")[0] : String(err)}`);
161
+ }
162
+ const me = latest.signers.find((s) => s.key === pub);
163
+ if (!me) return fail(`this key is not in version ${latest.version} of the signer set, so its signature would not count`);
164
+ let sig: string;
165
+ try {
166
+ sig = execFileSync("ssh-keygen", ["-q", "-Y", "sign", "-n", ROTATION_NAMESPACE, "-f", args.key], { input: statement, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
167
+ } catch (err) {
168
+ return fail(`ssh-keygen could not sign with ${args.key}: ${err instanceof Error ? err.message.split("\n")[0] : String(err)}`);
169
+ }
170
+ const check = verifySshSignature([me], statement, sig, ROTATION_NAMESPACE);
171
+ if (!check.ok) return fail(`the signature did not verify: ${check.reason}`);
172
+ rotation.signatures = [...rotation.signatures.filter((s) => s.principal !== me.principal), { principal: me.principal, signature: sig }];
173
+ writeFileSync(rotationFile, JSON.stringify(rotation, null, 2) + "\n");
174
+ console.log(formatSuccess(`signed version ${rotation.version} as ${me.principal} (${rotation.signatures.length} of ${latest.threshold} needed)`));
175
+ return 0;
176
+ }
177
+ return fail(`Unknown signers subcommand: ${sub}`);
178
+ }
@@ -116,15 +116,19 @@ export const sshCommitAttestor: CommitAttestor = {
116
116
  const memo = `${ctx.repo}\0${commit}\0${renderAllowedSigners(signers)}`;
117
117
  const hit = cache.get(memo);
118
118
  if (hit) return hit;
119
- const answer = attest(ctx.repo, commit, signers);
119
+ const raw = execFileSync("git", ["cat-file", "commit", commit], { cwd: ctx.repo, maxBuffer: 64 * 1024 * 1024 });
120
+ const answer = judgeRaw(raw, objectFormat(ctx.repo) === "sha256" ? "gpgsig-sha256" : "gpgsig", signers);
120
121
  cache.set(memo, answer);
121
122
  return answer;
122
123
  },
124
+ attestRaw(ctx: AttestorContext, raw: Buffer): CommitAttestation {
125
+ // A commit from another repository: its object format shows in its tree line.
126
+ const tree = /^tree ([0-9a-f]+)$/m.exec(raw.subarray(0, 200).toString("latin1"))?.[1];
127
+ return judgeRaw(raw, tree?.length === 64 ? "gpgsig-sha256" : "gpgsig", ctx.policy.signers);
128
+ },
123
129
  };
124
130
 
125
- function attest(repo: string, commit: string, signers: Signer[]): CommitAttestation {
126
- const raw = execFileSync("git", ["cat-file", "commit", commit], { cwd: repo, maxBuffer: 64 * 1024 * 1024 });
127
- const header = objectFormat(repo) === "sha256" ? "gpgsig-sha256" : "gpgsig";
131
+ function judgeRaw(raw: Buffer, header: string, signers: Signer[]): CommitAttestation {
128
132
  const { payload, signature } = splitSignedCommit(raw, header);
129
133
  if (!signature) return { level: "unattested", attestor: NAME, reason: "the commit is not signed" };
130
134
  if (!signature.startsWith("-----BEGIN SSH SIGNATURE-----")) {
@@ -136,3 +140,48 @@ function attest(repo: string, commit: string, signers: Signer[]): CommitAttestat
136
140
  if (r.missing) return { level: "attested-unverifiable-here", attestor: NAME, reason: r.reason };
137
141
  return { level: "unattested", attestor: NAME, reason: r.reason };
138
142
  }
143
+
144
+ /**
145
+ * The public key inside an armored ssh signature (the SSHSIG format), as
146
+ * `<type> <base64>`, or undefined when the text is not one. The key is what
147
+ * made the signature; whether anyone trusts it is a separate question.
148
+ */
149
+ export function sshSignatureKey(signature: string): string | undefined {
150
+ const body = signature
151
+ .replace(/-----BEGIN SSH SIGNATURE-----/, "")
152
+ .replace(/-----END SSH SIGNATURE-----/, "")
153
+ .replace(/\s+/g, "");
154
+ let buf: Buffer;
155
+ try {
156
+ buf = Buffer.from(body, "base64");
157
+ } catch {
158
+ return undefined;
159
+ }
160
+ if (buf.length < 14 || buf.subarray(0, 6).toString("latin1") !== "SSHSIG") return undefined;
161
+ const len = buf.readUInt32BE(10);
162
+ const blob = buf.subarray(14, 14 + len);
163
+ if (blob.length !== len || len < 4) return undefined;
164
+ const typeLen = blob.readUInt32BE(0);
165
+ const type = blob.subarray(4, 4 + typeLen).toString("latin1");
166
+ if (!/^[a-z0-9@.-]+$/.test(type)) return undefined;
167
+ return `${type} ${blob.toString("base64")}`;
168
+ }
169
+
170
+ /**
171
+ * Whether an ssh signature over `payload` in `namespace` is intact, whoever
172
+ * made it (`ssh-keygen -Y check-novalidate`). Returns the key's fingerprint,
173
+ * false when it does not check, or null when ssh-keygen is not installed.
174
+ */
175
+ export function sshSignatureIntact(payload: Buffer, signature: string, namespace: string): string | false | null {
176
+ const dir = mkdtempSync(join(tmpdir(), "chant-intact-"));
177
+ try {
178
+ const sigFile = join(dir, "signature");
179
+ writeFileSync(sigFile, signature);
180
+ const r = sshKeygen(["-Y", "check-novalidate", "-n", namespace, "-s", sigFile], payload);
181
+ if (r.missing) return null;
182
+ if (r.status !== 0) return false;
183
+ return /key (SHA256:[A-Za-z0-9+/=]+)/.exec(r.stdout + r.stderr)?.[1] ?? "";
184
+ } finally {
185
+ rmSync(dir, { recursive: true, force: true });
186
+ }
187
+ }
@@ -8,6 +8,7 @@ import { execFileSync, spawnSync } from "node:child_process";
8
8
  import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
9
9
  import { tmpdir } from "node:os";
10
10
  import { dirname, join } from "node:path";
11
+ import { ROTATION_NAMESPACE, rotationStatement, signersDigest } from "./rotation";
11
12
 
12
13
  export const hasSshKeygen = spawnSync("ssh-keygen", ["-Y"], { stdio: "ignore" }).error === undefined;
13
14
 
@@ -109,6 +110,23 @@ export class TestRepo {
109
110
  }
110
111
  }
111
112
 
113
+ /**
114
+ * Write `<signers>.rotation.json` for the signers file now in the working
115
+ * tree, as the next version after `previousText`, signed by `by` in the
116
+ * rotation namespace (#2553).
117
+ */
118
+ export function writeRotation(
119
+ repo: TestRepo,
120
+ opts: { previousText: string; version: number; threshold?: number; by: Array<[string, Key]>; namespace?: string; signersPath?: string },
121
+ ): void {
122
+ const path = opts.signersPath ?? ".chant/allowed_signers";
123
+ const text = readFileSync(join(repo.dir, path), "utf-8");
124
+ const rotation = { version: opts.version, previous: signersDigest(opts.previousText), threshold: opts.threshold ?? 1 };
125
+ const statement = rotationStatement(rotation, signersDigest(text));
126
+ const signatures = opts.by.map(([principal, key]) => ({ principal, signature: repo.sshSign(key, statement, opts.namespace ?? ROTATION_NAMESPACE) }));
127
+ repo.write(`${path}.rotation.json`, JSON.stringify({ schema: 1, ...rotation, signatures }, null, 2));
128
+ }
129
+
112
130
  /** A record kind reading `records/*.md`, and its schema, written into `repo`. */
113
131
  export function writeRecordKind(repo: TestRepo): string {
114
132
  repo.write(
@@ -15,7 +15,7 @@ import { queryRecords, type RecordsDocument, type RecordView } from "../records-
15
15
  import { activeAttestors } from "./attestor";
16
16
  import { parseAllowedSigners } from "./policy";
17
17
  import { verifyChange } from "./verify";
18
- import { hasSshKeygen, note, TestRepo, writeRecordKind, type Key } from "./test-repo";
18
+ import { hasSshKeygen, note, TestRepo, writeRecordKind, writeRotation, type Key } from "./test-repo";
19
19
 
20
20
  const repos: TestRepo[] = [];
21
21
  function repo(label: string): TestRepo {
@@ -90,8 +90,11 @@ describe.skipIf(!hasSshKeygen)("the ssh-commit attestor, against the policy at b
90
90
  const kind = writeRecordKind(r);
91
91
  r.write(".chant/allowed_signers", signers(["alice@example.test", alice], ["bob@example.test", bob]));
92
92
  if (opts.admin) r.write(".chant/trust.json", JSON.stringify({ schema: 1, roles: { admin: ["alice@example.test"] } }));
93
+ r.commit("policy", alice);
94
+ // A separate commit: the one that adds the signers file is judged by the
95
+ // set before it, which is none (#2553), so it attests nothing itself.
93
96
  r.write("records/n1.md", note("n1"));
94
- r.commit("policy and first record", alice);
97
+ r.commit("first record", alice);
95
98
  r.git(["checkout", "-q", "-b", "change"]);
96
99
  return { r, alice, bob, mallory, kind };
97
100
  }
@@ -181,7 +184,9 @@ describe.skipIf(!hasSshKeygen)("the ssh-commit attestor, against the policy at b
181
184
  test("attack: a backdated commit by a key removed from the signers before base", async () => {
182
185
  const { r, alice, bob, kind } = baseline("backdated-revoked");
183
186
  r.git(["checkout", "-q", "main"]);
187
+ const v1 = r.git(["show", "main:.chant/allowed_signers"]);
184
188
  r.write(".chant/allowed_signers", signers(["alice@example.test", alice]));
189
+ writeRotation(r, { previousText: v1, version: 2, by: [["alice@example.test", alice]] });
185
190
  r.commit("remove bob", alice);
186
191
  r.git(["checkout", "-q", "-b", "late"]);
187
192
  r.write("records/n2.md", note("n2"));
@@ -299,6 +304,7 @@ describe.skipIf(!hasSshKeygen)("the ssh-commit attestor, against the policy at b
299
304
 
300
305
  r.git(["reset", "-q", "--hard", "main"]);
301
306
  r.write(".chant/allowed_signers", signers(["alice@example.test", alice], ["bob@example.test", bob], ["carol@example.test", r.key("carol2")]));
307
+ writeRotation(r, { previousText: r.git(["show", "main:.chant/allowed_signers"]), version: 2, by: [["alice@example.test", alice]] });
302
308
  r.commit("add carol", alice);
303
309
  report = await verify(r);
304
310
  expect(report.ok).toBe(true);
@@ -49,6 +49,7 @@ function printReport(r: ChangeReport): void {
49
49
  lines.push(` ${c.commit.slice(0, 8)} ${c.level}${who} ${c.subject}${note}`);
50
50
  }
51
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
+ if (r.rotation && "to" in r.rotation) lines.push(` signer set: version ${r.rotation.from} to ${r.rotation.to}, signed by ${r.rotation.signedBy.join(", ")}`);
52
53
  for (const n of r.notes) lines.push(n);
53
54
  console.log(lines.join("\n"));
54
55
  if (r.ok) console.log(formatSuccess(`verified ${r.commits.length} commits against the policy at base`));
@@ -16,6 +16,7 @@ import { execFileSync } from "node:child_process";
16
16
  import { type CommitAttestor, type ProvenanceLevel } from "./attestor";
17
17
  import { policyAtBase, commitProvenance, resolveBase, type BaseSource, type RecordProvenance } from "./provenance";
18
18
  import { policyWriters, protectedPaths, type ExcludedSigner, type TrustPolicy } from "./policy";
19
+ import { fileAt, nextVersion, rotationPath, signerHistory, type RotationRefusalCode } from "./rotation";
19
20
 
20
21
  export interface CommitVerdict extends RecordProvenance {
21
22
  commit: string;
@@ -51,6 +52,12 @@ export interface ChangeReport {
51
52
  /** Protected paths whose content differs between the merge base and head. */
52
53
  protectedChanged: string[];
53
54
  protectedWrites: ProtectedWrite[];
55
+ /**
56
+ * When the change edits the signer set (#2553): the version it proposes and
57
+ * who signed it, or, when the new set is refused, the version it had to
58
+ * follow and why.
59
+ */
60
+ rotation: { from: number; to: number; signedBy: string[] } | { from: number; code: RotationRefusalCode; reason: string } | null;
54
61
  notes: string[];
55
62
  failures: string[];
56
63
  ok: boolean;
@@ -111,6 +118,7 @@ export function verifyChange(opts: VerifyOptions): ChangeReport {
111
118
  protectedPaths: protectedPaths(policy),
112
119
  protectedChanged: [],
113
120
  protectedWrites: [],
121
+ rotation: null,
114
122
  notes: [],
115
123
  failures: [],
116
124
  ok: false,
@@ -183,6 +191,20 @@ export function verifyChange(opts: VerifyOptions): ChangeReport {
183
191
  }
184
192
  }
185
193
 
194
+ // A new signer set must be signed by a threshold of the set at base (#2553).
195
+ const rotation = rotationPath(policy.signersPath);
196
+ if (changedPaths.includes(policy.signersPath) || changedPaths.includes(rotation)) {
197
+ const next = { signers: fileAt(repo, head, policy.signersPath), rotation: fileAt(repo, head, rotation) };
198
+ const latest = signerHistory(repo, base.commit, policy.signersPath).versions.at(-1);
199
+ if (next.signers !== undefined && latest && latest.version > 0) {
200
+ const v = nextVersion(latest, next, null);
201
+ if ("code" in v) {
202
+ report.rotation = { from: latest.version, code: v.code, reason: v.reason };
203
+ report.failures.push(`the signer set at head is not a valid rotation of the set at base (${v.code}): ${v.reason}`);
204
+ } else if (v !== latest) report.rotation = { from: latest.version, to: v.version, signedBy: v.signedBy };
205
+ }
206
+ }
207
+
186
208
  if (opts.require) {
187
209
  for (const v of report.commits) {
188
210
  if (v.skipped || v.level === opts.require) continue;
@@ -158,12 +158,39 @@ describe("chant workspace work (#2732)", () => {
158
158
  expect(json(closed).error.code).toBe("work-item-closed");
159
159
  const verb = await chant(root, ["workspace", "work", "grab", "W-001", "--holder", "a"]);
160
160
  expect(verb.code).toBe(1);
161
- expect(verb.stderr).toContain("claim, renew, release or history");
161
+ expect(verb.stderr).toContain("claim, renew, release, history or evidence");
162
162
  const noHolder = await chant(root, ["workspace", "work", "claim", "W-001"]);
163
163
  expect(noHolder.code).toBe(1);
164
164
  expect(noHolder.stderr).toContain("--holder");
165
165
  }, 120_000);
166
166
 
167
+ test("evidence (#3159): needs the holder, the token and the entry, runs only under the live lease, and names only a criterion the item lists", async () => {
168
+ const entry = JSON.stringify({ criterion: "AC-1", result: "pass", title: "The test", url: "https://example.com/ci/1" });
169
+ const usage = await chant(root, ["workspace", "work", "evidence", "W-002", "--holder", "a", "--from", "-"]);
170
+ expect(usage.code).toBe(1);
171
+ expect(JSON.parse(usage.stdout).error).toMatchObject({ code: "write-usage-invalid", message: expect.stringContaining("--token") });
172
+ const evidence = (holder: string, token: string) =>
173
+ new Promise<Run>((resolve) => {
174
+ const child = spawn(CHANT[0], [...CHANT.slice(1), "workspace", "work", "evidence", "W-002", "--holder", holder, "--token", token, "--from", "-"], { cwd: root, env: { ...process.env, NO_COLOR: "1" } });
175
+ let stdout = "";
176
+ let stderr = "";
177
+ child.stdout.on("data", (d) => (stdout += d));
178
+ child.stderr.on("data", (d) => (stderr += d));
179
+ child.on("close", (code) => resolve({ code, stdout, stderr }));
180
+ child.stdin.end(entry);
181
+ });
182
+ // worker-d holds W-002 since the records test above: anyone else is refused.
183
+ const held = json(await chant(root, ["workspace", "work", "claim", "W-002", "--holder", "ev", "--json"])).refused.heldBy as { holder: string; token: string };
184
+ const other = await evidence("ev", "none");
185
+ expect(other.code).toBe(1);
186
+ expect(JSON.parse(other.stdout).error.code).toBe("lease-held");
187
+ const stale = await evidence(held.holder, "not-the-token");
188
+ expect(JSON.parse(stale.stdout).error.code).toBe("lease-token-mismatch");
189
+ const unlisted = await evidence(held.holder, held.token);
190
+ expect(unlisted.code).toBe(1);
191
+ expect(JSON.parse(unlisted.stdout).error.code).toBe("work-criterion-unknown");
192
+ }, 120_000);
193
+
167
194
  test("the lease fields reach an MCP client through workspace-records and workspace-status", async () => {
168
195
  const claim = await chant(root, ["workspace", "work", "claim", "W-001", "--holder", "mcp-worker", "--json"]);
169
196
  expect(claim.code, claim.stderr).toBe(0);