@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.
- package/dist/agents/checks.d.ts.map +1 -1
- package/dist/attrref.d.ts.map +1 -1
- package/dist/audit/discover.d.ts.map +1 -1
- package/dist/audit/nginx.d.ts.map +1 -1
- package/dist/audit/prior-art.d.ts +48 -0
- package/dist/audit/prior-art.d.ts.map +1 -1
- package/dist/behaviour-kinds.d.ts.map +1 -1
- package/dist/build.d.ts +6 -0
- package/dist/build.d.ts.map +1 -1
- package/dist/builder.d.ts.map +1 -1
- package/dist/cli/build-options.d.ts +2 -0
- package/dist/cli/build-options.d.ts.map +1 -1
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/carve-emit.d.ts.map +1 -1
- package/dist/cli/commands/check-lexicon.d.ts +16 -0
- package/dist/cli/commands/check-lexicon.d.ts.map +1 -1
- package/dist/cli/commands/import.d.ts +8 -0
- package/dist/cli/commands/import.d.ts.map +1 -1
- package/dist/cli/commands/init-lexicon.d.ts.map +1 -1
- package/dist/cli/commands/init.d.ts +19 -0
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/onboard.d.ts +9 -0
- package/dist/cli/commands/onboard.d.ts.map +1 -1
- package/dist/cli/commands/pinned-upgrade.d.ts +19 -5
- package/dist/cli/commands/pinned-upgrade.d.ts.map +1 -1
- package/dist/cli/handlers/dev.d.ts.map +1 -1
- package/dist/cli/handlers/fan-out.d.ts.map +1 -1
- package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
- package/dist/cli/handlers/misc.d.ts.map +1 -1
- package/dist/cli/handlers/operator.d.ts +9 -0
- package/dist/cli/handlers/operator.d.ts.map +1 -1
- package/dist/cli/handlers/run.d.ts.map +1 -1
- package/dist/cli/lsp/server.d.ts.map +1 -1
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/mcp/server.d.ts.map +1 -1
- package/dist/cli/mcp/tools/build.d.ts.map +1 -1
- package/dist/cli/mcp/tools/composites.d.ts.map +1 -1
- package/dist/cli/mcp/tools/explain.d.ts.map +1 -1
- package/dist/cli/mcp/tools/import.d.ts.map +1 -1
- package/dist/cli/mcp/tools/lint.d.ts.map +1 -1
- package/dist/cli/mcp/tools/scaffold.d.ts.map +1 -1
- package/dist/cli/mcp/tools/search.d.ts.map +1 -1
- package/dist/cli/mcp/workspace-tools.d.ts +9 -1
- package/dist/cli/mcp/workspace-tools.d.ts.map +1 -1
- package/dist/cli/registry.d.ts +33 -2
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/codegen/fetch.d.ts.map +1 -1
- package/dist/codegen/naming.d.ts.map +1 -1
- package/dist/codegen/pinned-upgrade.d.ts +67 -3
- package/dist/codegen/pinned-upgrade.d.ts.map +1 -1
- package/dist/codegen/registry.d.ts.map +1 -1
- package/dist/components/capability-plugin-loader.d.ts.map +1 -1
- package/dist/components/capability.d.ts.map +1 -1
- package/dist/components/driver.d.ts.map +1 -1
- package/dist/components/fan-out.d.ts.map +1 -1
- package/dist/components/starter-plugin.d.ts.map +1 -1
- package/dist/components/verbs/process-runner.d.ts.map +1 -1
- package/dist/components/verbs/r2-sync.d.ts.map +1 -1
- package/dist/components/verbs/run-agent.d.ts.map +1 -1
- package/dist/components/verbs/sbom-generator.d.ts.map +1 -1
- package/dist/components/verbs/sign.d.ts.map +1 -1
- package/dist/components/verbs/verify.d.ts.map +1 -1
- package/dist/components/verbs/vuln-gate.d.ts.map +1 -1
- package/dist/components/verbs/vuln-scan.d.ts.map +1 -1
- package/dist/components/verbs/wrangler.d.ts.map +1 -1
- package/dist/composite.d.ts.map +1 -1
- package/dist/config.d.ts +30 -7
- package/dist/config.d.ts.map +1 -1
- package/dist/declarable.d.ts +16 -0
- package/dist/declarable.d.ts.map +1 -1
- package/dist/discovery/cache.d.ts.map +1 -1
- package/dist/discovery/fold-import.d.ts +15 -5
- package/dist/discovery/fold-import.d.ts.map +1 -1
- package/dist/discovery/param-deps.d.ts +16 -0
- package/dist/discovery/param-deps.d.ts.map +1 -1
- package/dist/discovery/sandbox/policy-wire.d.ts.map +1 -1
- package/dist/discovery/specifier-resolve.d.ts +37 -0
- package/dist/discovery/specifier-resolve.d.ts.map +1 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/fold/fold.d.ts +3 -1
- package/dist/fold/fold.d.ts.map +1 -1
- package/dist/graph-detail.d.ts.map +1 -1
- package/dist/graph-ir.d.ts.map +1 -1
- package/dist/graph-layout.d.ts.map +1 -1
- package/dist/import/base-parser.d.ts.map +1 -1
- package/dist/import/embedded.d.ts +7 -0
- package/dist/import/embedded.d.ts.map +1 -1
- package/dist/import/parser-options.d.ts +14 -0
- package/dist/import/parser-options.d.ts.map +1 -0
- package/dist/import/parser.d.ts +17 -0
- package/dist/import/parser.d.ts.map +1 -1
- package/dist/kubectl-context.d.ts.map +1 -1
- package/dist/lexicon-output.d.ts.map +1 -1
- package/dist/lexicon-schema.d.ts +3 -3
- package/dist/lexicon.d.ts +78 -3
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lifecycle/assert-live.d.ts.map +1 -1
- package/dist/lifecycle/gate-ledger.d.ts +15 -0
- package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
- package/dist/lifecycle/git.d.ts.map +1 -1
- package/dist/lifecycle/member-ledger.d.ts.map +1 -1
- package/dist/lifecycle/observe.d.ts.map +1 -1
- package/dist/lifecycle/plan-digest.d.ts +26 -5
- package/dist/lifecycle/plan-digest.d.ts.map +1 -1
- package/dist/lifecycle/plan-ledger.d.ts.map +1 -1
- package/dist/lifecycle/release-ledger.d.ts.map +1 -1
- package/dist/lifecycle/scenario.d.ts.map +1 -1
- package/dist/lifecycle/status.d.ts.map +1 -1
- package/dist/lifecycle/work-lease.d.ts +61 -2
- package/dist/lifecycle/work-lease.d.ts.map +1 -1
- package/dist/lint/config.d.ts +4 -4
- package/dist/lint/engine.d.ts +6 -0
- package/dist/lint/engine.d.ts.map +1 -1
- package/dist/lsp/lexicon-providers.d.ts.map +1 -1
- package/dist/op/activities/activity-contracts.d.ts +12 -11
- package/dist/op/activities/activity-contracts.d.ts.map +1 -1
- package/dist/op/activities/apply.d.ts +79 -9
- package/dist/op/activities/apply.d.ts.map +1 -1
- package/dist/op/activities/decide.d.ts.map +1 -1
- package/dist/op/activities/index.d.ts +1 -1
- package/dist/op/activities/index.d.ts.map +1 -1
- package/dist/op/activities/lexicon-upgrade.d.ts +44 -12
- package/dist/op/activities/lexicon-upgrade.d.ts.map +1 -1
- package/dist/op/activities/propose-upgrade.d.ts +2 -0
- package/dist/op/activities/propose-upgrade.d.ts.map +1 -1
- package/dist/op/activity-contract.d.ts +1 -1
- package/dist/op/activity-failure.d.ts.map +1 -1
- package/dist/op/activity-profiles.d.ts.map +1 -1
- package/dist/op/builders.d.ts.map +1 -1
- package/dist/op/composites/apply-op.d.ts +12 -5
- package/dist/op/composites/apply-op.d.ts.map +1 -1
- package/dist/op/composites/lexicon-upgrade-op.d.ts +22 -6
- package/dist/op/composites/lexicon-upgrade-op.d.ts.map +1 -1
- package/dist/op/decide-backend.d.ts.map +1 -1
- package/dist/op/decide-read-inputs.d.ts.map +1 -1
- package/dist/op/emulator-lifecycle.d.ts +21 -2
- package/dist/op/emulator-lifecycle.d.ts.map +1 -1
- package/dist/op/gate-approval.d.ts +5 -1
- package/dist/op/gate-approval.d.ts.map +1 -1
- package/dist/op/gate-wait.d.ts.map +1 -1
- package/dist/op/gate.d.ts +19 -3
- package/dist/op/gate.d.ts.map +1 -1
- package/dist/op/index.d.ts +3 -1
- package/dist/op/index.d.ts.map +1 -1
- package/dist/op/local-executor.d.ts +6 -0
- package/dist/op/local-executor.d.ts.map +1 -1
- package/dist/op/operator.d.ts.map +1 -1
- package/dist/op/run-context.d.ts +39 -0
- package/dist/op/run-context.d.ts.map +1 -0
- package/dist/op/run-live.d.ts.map +1 -1
- package/dist/op/runtimes/local.d.ts.map +1 -1
- package/dist/op/source-archive.d.ts.map +1 -1
- package/dist/op/steward-beside.d.ts.map +1 -1
- package/dist/op/steward-points.d.ts.map +1 -1
- package/dist/op/steward.d.ts.map +1 -1
- package/dist/op/types.d.ts +8 -3
- package/dist/op/types.d.ts.map +1 -1
- package/dist/op/work-lease-run.d.ts +24 -4
- package/dist/op/work-lease-run.d.ts.map +1 -1
- package/dist/project-root.d.ts.map +1 -1
- package/dist/provenance.d.ts +21 -0
- package/dist/provenance.d.ts.map +1 -1
- package/dist/pseudo-parameter.d.ts.map +1 -1
- package/dist/reconcile.d.ts.map +1 -1
- package/dist/secret-materialization.d.ts.map +1 -1
- package/dist/serializer.d.ts +8 -0
- package/dist/serializer.d.ts.map +1 -1
- package/dist/telemetry-attribution.d.ts +77 -0
- package/dist/telemetry-attribution.d.ts.map +1 -0
- package/dist/terraform/emit-source.d.ts.map +1 -1
- package/dist/terraform/parse.d.ts.map +1 -1
- package/dist/testing.d.ts.map +1 -1
- package/dist/workspace/__fixtures__/contract-repo.d.ts.map +1 -1
- package/dist/workspace/__fixtures__/decide-workspace.d.ts +1 -1
- package/dist/workspace/__fixtures__/decide-workspace.d.ts.map +1 -1
- package/dist/workspace/agent-cli.d.ts +88 -0
- package/dist/workspace/agent-cli.d.ts.map +1 -0
- package/dist/workspace/box-factory.d.ts +73 -0
- package/dist/workspace/box-factory.d.ts.map +1 -0
- package/dist/workspace/changes-cli.d.ts.map +1 -1
- package/dist/workspace/changes.d.ts +8 -1
- package/dist/workspace/changes.d.ts.map +1 -1
- package/dist/workspace/chant-migrations/chud-lexicon-exit.d.ts.map +1 -1
- package/dist/workspace/checks/boxes.d.ts.map +1 -1
- package/dist/workspace/checks/links.d.ts +1 -0
- package/dist/workspace/checks/links.d.ts.map +1 -1
- package/dist/workspace/checks/live.d.ts +40 -0
- package/dist/workspace/checks/live.d.ts.map +1 -0
- package/dist/workspace/checks.d.ts +26 -2
- package/dist/workspace/checks.d.ts.map +1 -1
- package/dist/workspace/compose-graph.d.ts +73 -0
- package/dist/workspace/compose-graph.d.ts.map +1 -1
- package/dist/workspace/conformance/index.d.ts +67 -6
- package/dist/workspace/conformance/index.d.ts.map +1 -1
- package/dist/workspace/conformance/vitest.d.ts +12 -3
- package/dist/workspace/conformance/vitest.d.ts.map +1 -1
- package/dist/workspace/conformance/writer.d.ts +368 -0
- package/dist/workspace/conformance/writer.d.ts.map +1 -0
- package/dist/workspace/decide.d.ts +1 -1
- package/dist/workspace/decide.d.ts.map +1 -1
- package/dist/workspace/declaration.d.ts +119 -0
- package/dist/workspace/declaration.d.ts.map +1 -1
- package/dist/workspace/declaration.schema.json +368 -4
- package/dist/workspace/export-cli.d.ts +12 -0
- package/dist/workspace/export-cli.d.ts.map +1 -0
- package/dist/workspace/export.d.ts +145 -0
- package/dist/workspace/export.d.ts.map +1 -0
- package/dist/workspace/generated-files.d.ts.map +1 -1
- package/dist/workspace/graph-cache.d.ts.map +1 -1
- package/dist/workspace/graph-cli.d.ts +2 -0
- package/dist/workspace/graph-cli.d.ts.map +1 -1
- package/dist/workspace/identity.d.ts +111 -0
- package/dist/workspace/identity.d.ts.map +1 -0
- package/dist/workspace/import.d.ts +73 -0
- package/dist/workspace/import.d.ts.map +1 -0
- package/dist/workspace/intent-cli.d.ts +7 -4
- package/dist/workspace/intent-cli.d.ts.map +1 -1
- package/dist/workspace/intent-record.d.ts +8 -1
- package/dist/workspace/intent-record.d.ts.map +1 -1
- package/dist/workspace/intent.d.ts +149 -5
- package/dist/workspace/intent.d.ts.map +1 -1
- package/dist/workspace/kinds.d.ts +24 -6
- package/dist/workspace/kinds.d.ts.map +1 -1
- package/dist/workspace/lineage-adopt-cli.d.ts +15 -0
- package/dist/workspace/lineage-adopt-cli.d.ts.map +1 -0
- package/dist/workspace/lineage-adopt.d.ts +106 -0
- package/dist/workspace/lineage-adopt.d.ts.map +1 -0
- package/dist/workspace/lineage-check.d.ts +9 -2
- package/dist/workspace/lineage-check.d.ts.map +1 -1
- package/dist/workspace/lineage-cli.d.ts +6 -1
- package/dist/workspace/lineage-cli.d.ts.map +1 -1
- package/dist/workspace/lineage-hash-index.d.ts +110 -0
- package/dist/workspace/lineage-hash-index.d.ts.map +1 -0
- package/dist/workspace/lineage-init.d.ts +10 -0
- package/dist/workspace/lineage-init.d.ts.map +1 -1
- package/dist/workspace/lineage-lock.d.ts +149 -2
- package/dist/workspace/lineage-lock.d.ts.map +1 -1
- package/dist/workspace/lineage-migrations.d.ts +15 -3
- package/dist/workspace/lineage-migrations.d.ts.map +1 -1
- package/dist/workspace/lineage-provenance.d.ts +18 -0
- package/dist/workspace/lineage-provenance.d.ts.map +1 -0
- package/dist/workspace/lineage-upgrade-cli.d.ts +2 -0
- package/dist/workspace/lineage-upgrade-cli.d.ts.map +1 -1
- package/dist/workspace/lineage-upgrade.d.ts +30 -1
- package/dist/workspace/lineage-upgrade.d.ts.map +1 -1
- package/dist/workspace/lineage-versions.d.ts +116 -0
- package/dist/workspace/lineage-versions.d.ts.map +1 -0
- package/dist/workspace/links.d.ts +31 -5
- package/dist/workspace/links.d.ts.map +1 -1
- package/dist/workspace/ls-generated.d.ts +37 -0
- package/dist/workspace/ls-generated.d.ts.map +1 -0
- package/dist/workspace/ls.d.ts +4 -0
- package/dist/workspace/ls.d.ts.map +1 -1
- package/dist/workspace/member-commands.d.ts.map +1 -1
- package/dist/workspace/nested-graph.d.ts +56 -0
- package/dist/workspace/nested-graph.d.ts.map +1 -0
- package/dist/workspace/nesting.d.ts +21 -0
- package/dist/workspace/nesting.d.ts.map +1 -0
- package/dist/workspace/pin-cli.d.ts +10 -0
- package/dist/workspace/pin-cli.d.ts.map +1 -0
- package/dist/workspace/pin-integrity.d.ts +51 -0
- package/dist/workspace/pin-integrity.d.ts.map +1 -0
- package/dist/workspace/points.d.ts.map +1 -1
- package/dist/workspace/principal-classes.d.ts +114 -0
- package/dist/workspace/principal-classes.d.ts.map +1 -0
- package/dist/workspace/reason-codes.d.ts +47 -3
- package/dist/workspace/reason-codes.d.ts.map +1 -1
- package/dist/workspace/record-assets.d.ts.map +1 -1
- package/dist/workspace/record-sessions.d.ts +7 -11
- package/dist/workspace/record-sessions.d.ts.map +1 -1
- package/dist/workspace/records-checkout.d.ts +54 -0
- package/dist/workspace/records-checkout.d.ts.map +1 -0
- package/dist/workspace/records-cli.d.ts +57 -2
- package/dist/workspace/records-cli.d.ts.map +1 -1
- package/dist/workspace/records-close.d.ts +5 -2
- package/dist/workspace/records-close.d.ts.map +1 -1
- package/dist/workspace/records-since.d.ts.map +1 -1
- package/dist/workspace/records-write.d.ts +23 -4
- package/dist/workspace/records-write.d.ts.map +1 -1
- package/dist/workspace/records.d.ts +58 -5
- package/dist/workspace/records.d.ts.map +1 -1
- package/dist/workspace/returns.d.ts +129 -0
- package/dist/workspace/returns.d.ts.map +1 -0
- package/dist/workspace/runs-cli.d.ts +164 -0
- package/dist/workspace/runs-cli.d.ts.map +1 -0
- package/dist/workspace/runs.d.ts +423 -0
- package/dist/workspace/runs.d.ts.map +1 -0
- package/dist/workspace/source-block.d.ts +1 -1
- package/dist/workspace/source-block.d.ts.map +1 -1
- package/dist/workspace/status-gates.d.ts +15 -3
- package/dist/workspace/status-gates.d.ts.map +1 -1
- package/dist/workspace/status.d.ts +10 -0
- package/dist/workspace/status.d.ts.map +1 -1
- package/dist/workspace/symbols.d.ts +52 -0
- package/dist/workspace/symbols.d.ts.map +1 -0
- package/dist/workspace/template-manifest.d.ts +11 -3
- package/dist/workspace/template-manifest.d.ts.map +1 -1
- package/dist/workspace/trailer-joins.d.ts +46 -0
- package/dist/workspace/trailer-joins.d.ts.map +1 -0
- package/dist/workspace/trailers.d.ts +89 -0
- package/dist/workspace/trailers.d.ts.map +1 -0
- package/dist/workspace/trust/attestor.d.ts +8 -0
- package/dist/workspace/trust/attestor.d.ts.map +1 -1
- package/dist/workspace/trust/dsse.d.ts +58 -0
- package/dist/workspace/trust/dsse.d.ts.map +1 -0
- package/dist/workspace/trust/evidence-cli.d.ts +66 -0
- package/dist/workspace/trust/evidence-cli.d.ts.map +1 -0
- package/dist/workspace/trust/evidence.d.ts +93 -0
- package/dist/workspace/trust/evidence.d.ts.map +1 -0
- package/dist/workspace/trust/policy.d.ts +54 -1
- package/dist/workspace/trust/policy.d.ts.map +1 -1
- package/dist/workspace/trust/provenance.d.ts +21 -1
- package/dist/workspace/trust/provenance.d.ts.map +1 -1
- package/dist/workspace/trust/rotation.d.ts +132 -0
- package/dist/workspace/trust/rotation.d.ts.map +1 -0
- package/dist/workspace/trust/seal.d.ts +33 -0
- package/dist/workspace/trust/seal.d.ts.map +1 -1
- package/dist/workspace/trust/signers-cli.d.ts +49 -0
- package/dist/workspace/trust/signers-cli.d.ts.map +1 -0
- package/dist/workspace/trust/ssh-commit.d.ts +12 -0
- package/dist/workspace/trust/ssh-commit.d.ts.map +1 -1
- package/dist/workspace/trust/test-repo.d.ts +13 -0
- package/dist/workspace/trust/test-repo.d.ts.map +1 -1
- package/dist/workspace/trust/verify.d.ts +15 -0
- package/dist/workspace/trust/verify.d.ts.map +1 -1
- package/dist/workspace/work-cli.d.ts +39 -3
- package/dist/workspace/work-cli.d.ts.map +1 -1
- package/dist/workspace/work-evidence.d.ts +1 -1
- package/dist/workspace/work-evidence.d.ts.map +1 -1
- package/dist/workspace/work.d.ts +26 -1
- package/dist/workspace/work.d.ts.map +1 -1
- package/dist/workspace/workspace-principals.schema.json +60 -0
- package/dist/workspace/write-scope.d.ts +259 -0
- package/dist/workspace/write-scope.d.ts.map +1 -0
- package/dist/yaml.d.ts +21 -39
- package/dist/yaml.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/audit/prior-art.ts +15 -0
- package/src/build.ts +8 -0
- package/src/cli/build-options.ts +4 -1
- package/src/cli/commands/__fixtures__/init-lexicon-output/package.json +1 -1
- package/src/cli/commands/build.ts +2 -0
- package/src/cli/commands/carve-emit-state.test.ts +4 -0
- package/src/cli/commands/carve-emit.ts +5 -19
- package/src/cli/commands/check-lexicon.test.ts +62 -1
- package/src/cli/commands/check-lexicon.ts +36 -6
- package/src/cli/commands/import-live.test.ts +87 -1
- package/src/cli/commands/import-parser-options.test.ts +133 -0
- package/src/cli/commands/import.ts +69 -25
- package/src/cli/commands/init-lexicon/templates/project.ts +1 -1
- package/src/cli/commands/init.test.ts +8 -1
- package/src/cli/commands/init.ts +23 -8
- package/src/cli/commands/lint.test.ts +37 -0
- package/src/cli/commands/onboard.test.ts +53 -3
- package/src/cli/commands/onboard.ts +52 -4
- package/src/cli/commands/pinned-upgrade.ts +73 -33
- package/src/cli/handlers/__snapshots__/build-member.test.ts.snap +2 -2
- package/src/cli/handlers/components.ts +2 -2
- package/src/cli/handlers/dev.ts +8 -6
- package/src/cli/handlers/fan-out.test.ts +6 -6
- package/src/cli/handlers/fan-out.ts +2 -1
- package/src/cli/handlers/graph.test.ts +42 -0
- package/src/cli/handlers/graph.ts +22 -0
- package/src/cli/handlers/lifecycle.test.ts +67 -0
- package/src/cli/handlers/lifecycle.ts +6 -5
- package/src/cli/handlers/misc.ts +2 -0
- package/src/cli/handlers/operator.ts +88 -4
- package/src/cli/handlers/run.test.ts +111 -0
- package/src/cli/handlers/run.ts +85 -24
- package/src/cli/handlers/scenario.ts +2 -2
- package/src/cli/main.test.ts +33 -0
- package/src/cli/main.ts +168 -17
- package/src/cli/mcp/workspace-tools.test.ts +5 -2
- package/src/cli/mcp/workspace-tools.ts +62 -4
- package/src/cli/registry.ts +33 -2
- package/src/cli/serve-mcp-workspace.test.ts +1 -1
- package/src/codegen/pinned-upgrade-multi.test.ts +157 -0
- package/src/codegen/pinned-upgrade-tags.test.ts +112 -0
- package/src/codegen/pinned-upgrade.test.ts +104 -0
- package/src/codegen/pinned-upgrade.ts +250 -56
- package/src/codegen/release-wiring.test.ts +5 -1
- package/src/codegen/testdata/postgres-tags.json +242 -0
- package/src/components/fan-out-output.test.ts +1 -1
- package/src/components/fan-out.test.ts +1 -1
- package/src/components/promote.test.ts +1 -1
- package/src/config.test.ts +21 -0
- package/src/config.ts +27 -0
- package/src/content-digest.test.ts +2 -2
- package/src/declarable.test.ts +21 -0
- package/src/declarable.ts +19 -0
- package/src/discovery/fold-composite.test.ts +41 -5
- package/src/discovery/fold-import.test.ts +129 -0
- package/src/discovery/fold-import.ts +643 -131
- package/src/discovery/fold-package-composite.test.ts +264 -0
- package/src/discovery/fold-specifiers.test.ts +191 -0
- package/src/discovery/param-deps.test.ts +66 -1
- package/src/discovery/param-deps.ts +45 -0
- package/src/discovery/specifier-resolve.ts +344 -0
- package/src/fold/fold.test.ts +39 -1
- package/src/fold/fold.ts +83 -9
- package/src/fold-provenance.test.ts +140 -0
- package/src/graph-ir.test.ts +83 -0
- package/src/graph-ir.ts +29 -0
- package/src/import/embedded.test.ts +20 -0
- package/src/import/embedded.ts +13 -0
- package/src/import/parser-options.ts +46 -0
- package/src/import/parser.ts +20 -0
- package/src/lexicon.ts +79 -3
- package/src/lifecycle/gate-ledger.test.ts +14 -0
- package/src/lifecycle/gate-ledger.ts +18 -1
- package/src/lifecycle/observe.test.ts +18 -0
- package/src/lifecycle/observe.ts +6 -4
- package/src/lifecycle/plan-digest.test.ts +54 -3
- package/src/lifecycle/plan-digest.ts +38 -8
- package/src/lifecycle/snapshot.ts +2 -2
- package/src/lifecycle/work-lease.test.ts +1 -1
- package/src/lifecycle/work-lease.ts +93 -2
- package/src/lint/engine.test.ts +55 -0
- package/src/lint/engine.ts +30 -4
- package/src/lint/output-docs.test.ts +59 -56
- package/src/op/activities/activity-contracts.ts +1 -0
- package/src/op/activities/apply.test.ts +189 -5
- package/src/op/activities/apply.ts +230 -8
- package/src/op/activities/index.ts +3 -0
- package/src/op/activities/lexicon-upgrade.test.ts +160 -6
- package/src/op/activities/lexicon-upgrade.ts +154 -51
- package/src/op/activities/propose-upgrade.ts +8 -5
- package/src/op/composites/apply-op.ts +12 -5
- package/src/op/composites/composites.test.ts +30 -3
- package/src/op/composites/lexicon-upgrade-op.test.ts +7 -2
- package/src/op/composites/lexicon-upgrade-op.ts +29 -8
- package/src/op/emulator-lifecycle.test.ts +23 -0
- package/src/op/emulator-lifecycle.ts +52 -14
- package/src/op/gate-approval.test.ts +91 -0
- package/src/op/gate-approval.ts +5 -1
- package/src/op/gate.ts +34 -8
- package/src/op/index.ts +3 -1
- package/src/op/local-executor.test.ts +15 -0
- package/src/op/local-executor.ts +40 -12
- package/src/op/local-output.ts +1 -1
- package/src/op/operator.ts +4 -0
- package/src/op/run-context.test.ts +141 -0
- package/src/op/run-context.ts +51 -0
- package/src/op/runtimes/local.test.ts +14 -0
- package/src/op/runtimes/local.ts +2 -0
- package/src/op/steward-beside.ts +2 -0
- package/src/op/types.ts +8 -3
- package/src/op/work-lease-run.test.ts +47 -0
- package/src/op/work-lease-run.ts +73 -11
- package/src/provenance.ts +35 -0
- package/src/serializer.ts +9 -0
- package/src/telemetry-attribution.test.ts +91 -0
- package/src/telemetry-attribution.ts +145 -0
- package/src/workspace/__fixtures__/studio-template-84c8b21.json +1 -1
- package/src/workspace/agent-cli.ts +138 -0
- package/src/workspace/agent.schema.json +378 -0
- package/src/workspace/behold-kinds.test.ts +1 -1
- package/src/workspace/box-factory.test.ts +320 -0
- package/src/workspace/box-factory.ts +103 -0
- package/src/workspace/changes-cli.ts +5 -0
- package/src/workspace/changes.schema.json +171 -1
- package/src/workspace/changes.ts +61 -4
- package/src/workspace/check-live.test.ts +192 -0
- package/src/workspace/check.schema.json +64 -0
- package/src/workspace/checks/boxes.test.ts +2 -0
- package/src/workspace/checks/links.ts +23 -2
- package/src/workspace/checks/live.ts +113 -0
- package/src/workspace/checks.test.ts +2 -2
- package/src/workspace/checks.ts +49 -8
- package/src/workspace/compose-graph.test.ts +47 -0
- package/src/workspace/compose-graph.ts +127 -3
- package/src/workspace/conformance/__fixture__/decisions/decision.kind.mjs +7 -1
- package/src/workspace/conformance/__fixture__/decisions/decision.schema.json +5 -0
- package/src/workspace/conformance/__writer_fixture__/answers/answer.kind.mjs +27 -0
- package/src/workspace/conformance/__writer_fixture__/answers/answer.schema.json +95 -0
- package/src/workspace/conformance/__writer_fixture__/decisions/points.json +132 -0
- package/src/workspace/conformance/__writer_fixture__/sessions/session.kind.mjs +48 -0
- package/src/workspace/conformance/__writer_fixture__/sessions/session.schema.json +108 -0
- package/src/workspace/conformance/__writer_fixture__/work/W-001-the-writer-suite-works-this-item.md +25 -0
- package/src/workspace/conformance/__writer_fixture__/work/work.kind.mjs +60 -0
- package/src/workspace/conformance/__writer_fixture__/work/work.schema.json +578 -0
- package/src/workspace/conformance/conformance.e2e.test.ts +35 -3
- package/src/workspace/conformance/index.mjs +30 -3
- package/src/workspace/conformance/index.ts +103 -15
- package/src/workspace/conformance/vitest.ts +44 -2
- package/src/workspace/conformance/writer-conformance.e2e.test.ts +131 -0
- package/src/workspace/conformance/writer.test.ts +156 -0
- package/src/workspace/conformance/writer.ts +880 -0
- package/src/workspace/decide.ts +7 -1
- package/src/workspace/declaration.schema.json +368 -4
- package/src/workspace/declaration.ts +270 -1
- package/src/workspace/declared-kinds.test.ts +33 -0
- package/src/workspace/evidence.schema.json +279 -0
- package/src/workspace/export-cli.ts +180 -0
- package/src/workspace/export-import.test.ts +282 -0
- package/src/workspace/export.ts +486 -0
- package/src/workspace/graph-cli.ts +51 -2
- package/src/workspace/graph-contract.test.ts +89 -4
- package/src/workspace/graph.schema.json +453 -1
- package/src/workspace/identity.test.ts +196 -0
- package/src/workspace/identity.ts +186 -0
- package/src/workspace/import.ts +325 -0
- package/src/workspace/intent-cli.ts +64 -10
- package/src/workspace/intent-record.schema.json +208 -0
- package/src/workspace/intent-record.ts +35 -1
- package/src/workspace/intent-why.test.ts +274 -0
- package/src/workspace/intent.schema.json +718 -7
- package/src/workspace/intent.test.ts +11 -2
- package/src/workspace/intent.ts +495 -32
- package/src/workspace/kinds.test.ts +5 -5
- package/src/workspace/kinds.ts +62 -33
- package/src/workspace/lineage-adopt-cli.ts +103 -0
- package/src/workspace/lineage-adopt.test.ts +552 -0
- package/src/workspace/lineage-adopt.ts +452 -0
- package/src/workspace/lineage-check.ts +26 -5
- package/src/workspace/lineage-cli.ts +12 -2
- package/src/workspace/lineage-hash-index.ts +305 -0
- package/src/workspace/lineage-init.test.ts +9 -0
- package/src/workspace/lineage-init.ts +29 -9
- package/src/workspace/lineage-lock.ts +54 -0
- package/src/workspace/lineage-migrations.test.ts +27 -0
- package/src/workspace/lineage-migrations.ts +44 -14
- package/src/workspace/lineage-provenance.ts +40 -0
- package/src/workspace/lineage-upgrade-cli.ts +9 -5
- package/src/workspace/lineage-upgrade.test.ts +92 -1
- package/src/workspace/lineage-upgrade.ts +111 -16
- package/src/workspace/lineage-versions.ts +348 -0
- package/src/workspace/links.test.ts +121 -2
- package/src/workspace/links.ts +97 -6
- package/src/workspace/ls-contract.test.ts +84 -1
- package/src/workspace/ls-generated.ts +111 -0
- package/src/workspace/ls.schema.json +19 -0
- package/src/workspace/ls.ts +8 -1
- package/src/workspace/member-commands.ts +3 -1
- package/src/workspace/nested-graph.test.ts +176 -0
- package/src/workspace/nested-graph.ts +169 -0
- package/src/workspace/nesting.ts +37 -0
- package/src/workspace/pin-cli.test.ts +71 -0
- package/src/workspace/pin-cli.ts +57 -0
- package/src/workspace/pin-integrity.test.ts +121 -0
- package/src/workspace/pin-integrity.ts +104 -0
- package/src/workspace/points-write.schema.json +2 -0
- package/src/workspace/principal-classes.test.ts +175 -0
- package/src/workspace/principal-classes.ts +307 -0
- package/src/workspace/read-contract.test.ts +24 -0
- package/src/workspace/reason-codes.test.ts +20 -2
- package/src/workspace/reason-codes.ts +53 -3
- package/src/workspace/record-sessions.ts +12 -14
- package/src/workspace/records-amend.schema.json +6 -0
- package/src/workspace/records-checkout.ts +106 -0
- package/src/workspace/records-cli.ts +195 -22
- package/src/workspace/records-close.schema.json +7 -2
- package/src/workspace/records-close.ts +10 -2
- package/src/workspace/records-formats.test.ts +13 -5
- package/src/workspace/records-new.schema.json +6 -0
- package/src/workspace/records-review.schema.json +6 -0
- package/src/workspace/records-sessions.test.ts +23 -6
- package/src/workspace/records-uncommitted.test.ts +171 -0
- package/src/workspace/records-write.test.ts +65 -8
- package/src/workspace/records-write.ts +92 -6
- package/src/workspace/records.schema.json +127 -4
- package/src/workspace/records.ts +123 -5
- package/src/workspace/returns.ts +328 -0
- package/src/workspace/runs-cli.ts +381 -0
- package/src/workspace/runs-write.schema.json +583 -0
- package/src/workspace/runs.schema.json +810 -0
- package/src/workspace/runs.test.ts +242 -0
- package/src/workspace/runs.ts +709 -0
- package/src/workspace/signers.schema.json +206 -0
- package/src/workspace/status-contract.test.ts +5 -0
- package/src/workspace/status-gates.ts +24 -9
- package/src/workspace/status.schema.json +127 -2
- package/src/workspace/status.ts +21 -1
- package/src/workspace/symbols.ts +180 -0
- package/src/workspace/template-manifest.ts +22 -4
- package/src/workspace/trailer-joins.test.ts +171 -0
- package/src/workspace/trailer-joins.ts +82 -0
- package/src/workspace/trailers.ts +148 -0
- package/src/workspace/trust/attestor.ts +15 -0
- package/src/workspace/trust/dsse.ts +134 -0
- package/src/workspace/trust/evidence-cli.ts +195 -0
- package/src/workspace/trust/evidence.test.ts +241 -0
- package/src/workspace/trust/evidence.ts +207 -0
- package/src/workspace/trust/policy.ts +110 -3
- package/src/workspace/trust/provenance.ts +41 -4
- package/src/workspace/trust/record-seal.test.ts +1 -1
- package/src/workspace/trust/rotation.test.ts +258 -0
- package/src/workspace/trust/rotation.ts +336 -0
- package/src/workspace/trust/seal.ts +63 -1
- package/src/workspace/trust/signers-cli.ts +178 -0
- package/src/workspace/trust/ssh-commit.ts +53 -4
- package/src/workspace/trust/test-repo.ts +18 -0
- package/src/workspace/trust/trust.test.ts +8 -2
- package/src/workspace/trust/verify-cli.ts +1 -0
- package/src/workspace/trust/verify.ts +22 -0
- package/src/workspace/work-cli.e2e.test.ts +28 -1
- package/src/workspace/work-cli.ts +133 -8
- package/src/workspace/work-evidence.schema.json +6 -0
- package/src/workspace/work-history.schema.json +73 -1
- package/src/workspace/work-history.test.ts +48 -5
- package/src/workspace/work-kind-fields.test.ts +154 -0
- package/src/workspace/work-lease.schema.json +14 -1
- package/src/workspace/work.ts +82 -0
- package/src/workspace/workspace-principals.schema.json +60 -0
- package/src/workspace/write-scope.test.ts +466 -0
- package/src/workspace/write-scope.ts +573 -0
- package/src/yaml.test.ts +111 -1
- 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
|
|
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
|
|
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("
|
|
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
|
|
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);
|