@intentius/chant 0.102.0 → 0.104.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/audit/core.d.ts +2 -0
- package/dist/audit/core.d.ts.map +1 -1
- package/dist/audit/discover.d.ts.map +1 -1
- package/dist/change-set.d.ts +232 -0
- package/dist/change-set.d.ts.map +1 -0
- package/dist/cli/commands/audit.d.ts +7 -0
- package/dist/cli/commands/audit.d.ts.map +1 -1
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/handlers/build.d.ts.map +1 -1
- package/dist/cli/handlers/change-set.d.ts +11 -0
- package/dist/cli/handlers/change-set.d.ts.map +1 -0
- package/dist/cli/handlers/fan-out.d.ts +33 -0
- package/dist/cli/handlers/fan-out.d.ts.map +1 -1
- package/dist/cli/handlers/operator.d.ts +7 -0
- package/dist/cli/handlers/operator.d.ts.map +1 -1
- package/dist/cli/handlers/pr.d.ts +31 -0
- package/dist/cli/handlers/pr.d.ts.map +1 -0
- package/dist/cli/handlers/run.d.ts.map +1 -1
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/mcp/op-tools.d.ts.map +1 -1
- package/dist/cli/mcp/workspace-tools.d.ts +3 -2
- package/dist/cli/mcp/workspace-tools.d.ts.map +1 -1
- package/dist/cli/registry.d.ts +63 -1
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/components/auto-release.d.ts +16 -0
- package/dist/components/auto-release.d.ts.map +1 -1
- package/dist/components/capability.d.ts +53 -0
- package/dist/components/capability.d.ts.map +1 -1
- package/dist/components/changed-files.d.ts +31 -0
- package/dist/components/changed-files.d.ts.map +1 -0
- package/dist/components/cli-support.d.ts +9 -1
- package/dist/components/cli-support.d.ts.map +1 -1
- package/dist/components/component.d.ts +24 -0
- package/dist/components/component.d.ts.map +1 -1
- package/dist/components/deploy-units.d.ts.map +1 -1
- package/dist/components/driver.d.ts +13 -0
- package/dist/components/driver.d.ts.map +1 -1
- package/dist/components/fan-out-output.d.ts +6 -0
- package/dist/components/fan-out-output.d.ts.map +1 -1
- package/dist/components/fan-out-record.d.ts +41 -0
- package/dist/components/fan-out-record.d.ts.map +1 -0
- package/dist/components/fan-out-run.d.ts +65 -0
- package/dist/components/fan-out-run.d.ts.map +1 -1
- package/dist/components/fan-out-support.d.ts +2 -0
- package/dist/components/fan-out-support.d.ts.map +1 -1
- package/dist/components/fan-out.d.ts +6 -0
- package/dist/components/fan-out.d.ts.map +1 -1
- package/dist/components/gated-wave-pipeline.d.ts +50 -0
- package/dist/components/gated-wave-pipeline.d.ts.map +1 -0
- package/dist/components/index.d.ts +1 -1
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/layers.d.ts +25 -0
- package/dist/components/layers.d.ts.map +1 -0
- package/dist/components/pr-pipeline.d.ts +61 -0
- package/dist/components/pr-pipeline.d.ts.map +1 -0
- package/dist/components/pr-run.d.ts +119 -0
- package/dist/components/pr-run.d.ts.map +1 -0
- package/dist/components/promote.d.ts.map +1 -1
- package/dist/components/verbs/shell.d.ts.map +1 -1
- package/dist/components/wave-plan.d.ts +49 -0
- package/dist/components/wave-plan.d.ts.map +1 -0
- package/dist/gated-waves.d.ts +147 -0
- package/dist/gated-waves.d.ts.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/lexicon.d.ts +48 -0
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lifecycle/gate-ledger.d.ts +7 -0
- package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
- package/dist/lifecycle/git.d.ts +46 -1
- package/dist/lifecycle/git.d.ts.map +1 -1
- package/dist/lint/post-synth.d.ts +18 -0
- package/dist/lint/post-synth.d.ts.map +1 -1
- package/dist/op/activities/activity-contracts.d.ts +126 -0
- package/dist/op/activities/activity-contracts.d.ts.map +1 -1
- package/dist/op/activities/change-set.d.ts +62 -0
- package/dist/op/activities/change-set.d.ts.map +1 -0
- package/dist/op/activities/decide.d.ts +2 -0
- package/dist/op/activities/decide.d.ts.map +1 -1
- package/dist/op/activities/factory.d.ts +137 -0
- package/dist/op/activities/factory.d.ts.map +1 -0
- package/dist/op/activities/index.d.ts +4 -0
- package/dist/op/activities/index.d.ts.map +1 -1
- package/dist/op/activity-contract.d.ts.map +1 -1
- package/dist/op/builders.d.ts +11 -0
- package/dist/op/builders.d.ts.map +1 -1
- package/dist/op/decide-backend.d.ts +5 -1
- package/dist/op/decide-backend.d.ts.map +1 -1
- package/dist/op/decide-read-inputs.d.ts +4 -2
- package/dist/op/decide-read-inputs.d.ts.map +1 -1
- package/dist/op/factory.d.ts +83 -0
- package/dist/op/factory.d.ts.map +1 -0
- package/dist/op/index.d.ts +4 -1
- package/dist/op/index.d.ts.map +1 -1
- package/dist/op/op-ir.d.ts +2 -0
- package/dist/op/op-ir.d.ts.map +1 -1
- package/dist/op/op-verb-class.d.ts.map +1 -1
- package/dist/op/types.d.ts +8 -0
- package/dist/op/types.d.ts.map +1 -1
- package/dist/op/work-lease-run.d.ts.map +1 -1
- package/dist/plan-summary.d.ts +207 -0
- package/dist/plan-summary.d.ts.map +1 -0
- package/dist/pr-forge.d.ts +105 -0
- package/dist/pr-forge.d.ts.map +1 -0
- package/dist/pr-loop.d.ts +178 -0
- package/dist/pr-loop.d.ts.map +1 -0
- package/dist/telemetry-attribution.d.ts +31 -0
- package/dist/telemetry-attribution.d.ts.map +1 -1
- package/dist/terraform/hcl2json.d.ts +45 -0
- package/dist/terraform/hcl2json.d.ts.map +1 -0
- package/dist/terraform/parse.d.ts +4 -40
- package/dist/terraform/parse.d.ts.map +1 -1
- package/dist/workspace/box-cli.d.ts +12 -0
- package/dist/workspace/box-cli.d.ts.map +1 -0
- package/dist/workspace/box-factory.d.ts +27 -3
- package/dist/workspace/box-factory.d.ts.map +1 -1
- package/dist/workspace/box-listing.d.ts +85 -0
- package/dist/workspace/box-listing.d.ts.map +1 -0
- package/dist/workspace/box-publish.d.ts +147 -0
- package/dist/workspace/box-publish.d.ts.map +1 -0
- package/dist/workspace/broker-protocol.d.ts +190 -0
- package/dist/workspace/broker-protocol.d.ts.map +1 -0
- package/dist/workspace/checks.d.ts.map +1 -1
- package/dist/workspace/composites.d.ts +2 -2
- package/dist/workspace/composites.d.ts.map +1 -1
- package/dist/workspace/conformance/__fixtures__/reference-broker.d.ts +17 -0
- package/dist/workspace/conformance/__fixtures__/reference-broker.d.ts.map +1 -0
- package/dist/workspace/conformance/broker.d.ts +202 -0
- package/dist/workspace/conformance/broker.d.ts.map +1 -0
- package/dist/workspace/conformance/index.d.ts +2 -1
- package/dist/workspace/conformance/index.d.ts.map +1 -1
- package/dist/workspace/conformance/vitest.d.ts +9 -2
- package/dist/workspace/conformance/vitest.d.ts.map +1 -1
- package/dist/workspace/conformance/writer.d.ts +108 -14
- package/dist/workspace/conformance/writer.d.ts.map +1 -1
- package/dist/workspace/decide.d.ts +126 -12
- package/dist/workspace/decide.d.ts.map +1 -1
- package/dist/workspace/decision-points.schema.json +11 -4
- package/dist/workspace/declaration.d.ts +65 -0
- package/dist/workspace/declaration.d.ts.map +1 -1
- package/dist/workspace/declaration.schema.json +153 -4
- package/dist/workspace/environments.d.ts +36 -10
- package/dist/workspace/environments.d.ts.map +1 -1
- package/dist/workspace/factory-rules.d.ts +180 -0
- package/dist/workspace/factory-rules.d.ts.map +1 -0
- package/dist/workspace/init.d.ts +21 -0
- package/dist/workspace/init.d.ts.map +1 -1
- package/dist/workspace/intent-cli.d.ts +1 -1
- package/dist/workspace/intent-cli.d.ts.map +1 -1
- package/dist/workspace/intent-record.d.ts +28 -1
- package/dist/workspace/intent-record.d.ts.map +1 -1
- package/dist/workspace/intent.d.ts +102 -5
- package/dist/workspace/intent.d.ts.map +1 -1
- package/dist/workspace/json-edit.d.ts +24 -0
- package/dist/workspace/json-edit.d.ts.map +1 -0
- package/dist/workspace/jsonc.d.ts +11 -0
- package/dist/workspace/jsonc.d.ts.map +1 -1
- package/dist/workspace/kinds.d.ts +48 -0
- package/dist/workspace/kinds.d.ts.map +1 -1
- package/dist/workspace/lexicon-symbols.d.ts +29 -0
- package/dist/workspace/lexicon-symbols.d.ts.map +1 -0
- package/dist/workspace/lineage-upgrade.d.ts +31 -2
- package/dist/workspace/lineage-upgrade.d.ts.map +1 -1
- package/dist/workspace/point-answer.schema.json +42 -2
- package/dist/workspace/points-cli.d.ts +10 -3
- package/dist/workspace/points-cli.d.ts.map +1 -1
- package/dist/workspace/points.d.ts +66 -5
- package/dist/workspace/points.d.ts.map +1 -1
- package/dist/workspace/reason-codes.d.ts +33 -4
- package/dist/workspace/reason-codes.d.ts.map +1 -1
- package/dist/workspace/record-sessions.d.ts +5 -7
- package/dist/workspace/record-sessions.d.ts.map +1 -1
- package/dist/workspace/records-cli.d.ts +7 -0
- package/dist/workspace/records-cli.d.ts.map +1 -1
- package/dist/workspace/records-close.d.ts +7 -3
- package/dist/workspace/records-close.d.ts.map +1 -1
- package/dist/workspace/records-write.d.ts +66 -6
- package/dist/workspace/records-write.d.ts.map +1 -1
- package/dist/workspace/records.d.ts +28 -9
- package/dist/workspace/records.d.ts.map +1 -1
- package/dist/workspace/runs-cli.d.ts +138 -6
- package/dist/workspace/runs-cli.d.ts.map +1 -1
- package/dist/workspace/runs.d.ts +132 -11
- package/dist/workspace/runs.d.ts.map +1 -1
- package/dist/workspace/session-kinds.d.ts +1 -1
- package/dist/workspace/session-kinds.d.ts.map +1 -1
- package/dist/workspace/squash.d.ts +80 -0
- package/dist/workspace/squash.d.ts.map +1 -0
- package/dist/workspace/status-gates.d.ts +2 -0
- package/dist/workspace/status-gates.d.ts.map +1 -1
- package/dist/workspace/status.d.ts +54 -0
- package/dist/workspace/status.d.ts.map +1 -1
- package/dist/workspace/symbols.d.ts +16 -5
- package/dist/workspace/symbols.d.ts.map +1 -1
- package/dist/workspace/trust/run-commits.d.ts +46 -0
- package/dist/workspace/trust/run-commits.d.ts.map +1 -0
- package/dist/workspace/trust/run-statement.d.ts +128 -0
- package/dist/workspace/trust/run-statement.d.ts.map +1 -0
- package/dist/workspace/trust/seal.d.ts +6 -1
- package/dist/workspace/trust/seal.d.ts.map +1 -1
- package/dist/workspace/trust/verify-cli.d.ts +1 -1
- package/dist/workspace/trust/verify.d.ts +16 -2
- package/dist/workspace/trust/verify.d.ts.map +1 -1
- package/dist/workspace/wip-cli.d.ts +21 -0
- package/dist/workspace/wip-cli.d.ts.map +1 -0
- package/dist/workspace/wip.d.ts +268 -0
- package/dist/workspace/wip.d.ts.map +1 -0
- 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-size.d.ts +55 -0
- package/dist/workspace/work-size.d.ts.map +1 -0
- package/dist/workspace/workspace-kinds.schema.json +122 -0
- package/dist/workspace/write-lock-cli.d.ts +60 -0
- package/dist/workspace/write-lock-cli.d.ts.map +1 -0
- package/dist/workspace/write-lock.d.ts +152 -0
- package/dist/workspace/write-lock.d.ts.map +1 -0
- package/dist/workspace/write-scope.d.ts +6 -2
- package/dist/workspace/write-scope.d.ts.map +1 -1
- package/package.json +6 -1
- package/src/__fixtures__/change-set/lifecycle-plan.floci.json +59 -0
- package/src/__fixtures__/change-set/lifecycle-v1.ts.txt +4 -0
- package/src/__fixtures__/change-set/lifecycle-v2.ts.txt +4 -0
- package/src/__fixtures__/change-set/lifecycle.change-set.golden.json +95 -0
- package/src/__fixtures__/change-set/warden.change-set.golden.json +106 -0
- package/src/__fixtures__/change-set/warden.plan.json +44 -0
- package/src/__fixtures__/plan-summary/normalization-vectors.json +1593 -0
- package/src/__fixtures__/pr-loop/pr-apply-applied.note.golden.md +31 -0
- package/src/__fixtures__/pr-loop/pr-apply-refused.note.golden.md +31 -0
- package/src/__fixtures__/pr-loop/pr-plan.golden.json +127 -0
- package/src/__fixtures__/pr-loop/pr-plan.note.golden.md +37 -0
- package/src/audit/core.test.ts +15 -0
- package/src/audit/core.ts +8 -2
- package/src/audit/discover.test.ts +11 -0
- package/src/audit/discover.ts +7 -2
- package/src/change-set-bundle.test.ts +57 -0
- package/src/change-set.test.ts +215 -0
- package/src/change-set.ts +445 -0
- package/src/cli/commands/audit-opt-in.test.ts +54 -0
- package/src/cli/commands/audit.ts +29 -2
- package/src/cli/commands/build.test.ts +19 -0
- package/src/cli/commands/build.ts +3 -2
- package/src/cli/handlers/build.ts +13 -1
- package/src/cli/handlers/change-set.test.ts +56 -0
- package/src/cli/handlers/change-set.ts +44 -0
- package/src/cli/handlers/fan-out.test.ts +221 -2
- package/src/cli/handlers/fan-out.ts +165 -75
- package/src/cli/handlers/graph.ts +3 -0
- package/src/cli/handlers/operator.test.ts +21 -0
- package/src/cli/handlers/operator.ts +34 -3
- package/src/cli/handlers/pr.ts +373 -0
- package/src/cli/handlers/run.ts +1 -0
- package/src/cli/main.test.ts +20 -0
- package/src/cli/main.ts +185 -23
- package/src/cli/mcp/never-over-mcp.test.ts +101 -0
- package/src/cli/mcp/op-tools.ts +24 -2
- package/src/cli/mcp/workspace-tools.test.ts +1 -1
- package/src/cli/mcp/workspace-tools.ts +34 -6
- package/src/cli/registry.ts +63 -1
- package/src/components/auto-release.ts +1 -1
- package/src/components/capability.ts +55 -0
- package/src/components/changed-files.ts +66 -0
- package/src/components/cli-support.ts +28 -5
- package/src/components/component.schema.json +17 -0
- package/src/components/component.ts +25 -0
- package/src/components/deploy-units.ts +3 -0
- package/src/components/driver.test.ts +52 -0
- package/src/components/driver.ts +31 -13
- package/src/components/fan-out-output.ts +42 -1
- package/src/components/fan-out-record.ts +69 -0
- package/src/components/fan-out-run.test.ts +17 -0
- package/src/components/fan-out-run.ts +304 -1
- package/src/components/fan-out-support.ts +3 -0
- package/src/components/fan-out-waves.test.ts +375 -0
- package/src/components/fan-out.ts +18 -17
- package/src/components/gated-wave-pipeline.ts +82 -0
- package/src/components/index.ts +1 -0
- package/src/components/layers.ts +30 -0
- package/src/components/pr-pipeline.ts +91 -0
- package/src/components/pr-run.test.ts +205 -0
- package/src/components/pr-run.ts +280 -0
- package/src/components/promote.ts +5 -0
- package/src/components/verbs/shell.test.ts +11 -0
- package/src/components/verbs/shell.ts +7 -3
- package/src/components/wave-plan.ts +124 -0
- package/src/discovery/sandbox/bundle.ts +1 -1
- package/src/gated-waves-bundle.test.ts +62 -0
- package/src/gated-waves.test.ts +118 -0
- package/src/gated-waves.ts +240 -0
- package/src/index.ts +1 -0
- package/src/lexicon.ts +51 -0
- package/src/lifecycle/gate-ledger.ts +7 -0
- package/src/lifecycle/git.ts +102 -16
- package/src/lint/post-synth.test.ts +19 -0
- package/src/lint/post-synth.ts +19 -1
- package/src/meta/declared-imports.test.ts +1 -1
- package/src/op/activities/activity-contracts.ts +84 -0
- package/src/op/activities/change-set.ts +114 -0
- package/src/op/activities/decide.ts +3 -0
- package/src/op/activities/factory.ts +675 -0
- package/src/op/activities/index.ts +6 -0
- package/src/op/activity-contract.test.ts +21 -1
- package/src/op/activity-contract.ts +19 -9
- package/src/op/builders.ts +23 -0
- package/src/op/decide-backend.ts +5 -1
- package/src/op/decide-read-inputs.ts +33 -5
- package/src/op/factory.e2e.test.ts +232 -0
- package/src/op/factory.ts +118 -0
- package/src/op/index.ts +9 -1
- package/src/op/op-ir.test.ts +9 -0
- package/src/op/op-ir.ts +4 -0
- package/src/op/op-verb-class.ts +3 -0
- package/src/op/types.ts +8 -0
- package/src/op/work-lease-run.ts +3 -0
- package/src/plan-summary.test.ts +263 -0
- package/src/plan-summary.ts +845 -0
- package/src/pr-forge.test.ts +237 -0
- package/src/pr-forge.ts +332 -0
- package/src/pr-loop-bundle.test.ts +46 -0
- package/src/pr-loop.test.ts +171 -0
- package/src/pr-loop.ts +324 -0
- package/src/telemetry-attribution.test.ts +23 -1
- package/src/telemetry-attribution.ts +51 -0
- package/src/terraform/hcl2json.test.ts +21 -0
- package/src/terraform/hcl2json.ts +81 -0
- package/src/terraform/parse.ts +11 -74
- package/src/workspace/__fixtures__/studio-template-84c8b21.json +1 -1
- package/src/workspace/app-kind.test.ts +196 -0
- package/src/workspace/box-cli.ts +113 -0
- package/src/workspace/box-factory.test.ts +4 -1
- package/src/workspace/box-factory.ts +37 -4
- package/src/workspace/box-listing-write.schema.json +204 -0
- package/src/workspace/box-listing.test.ts +234 -0
- package/src/workspace/box-listing.ts +324 -0
- package/src/workspace/box-publish.schema.json +498 -0
- package/src/workspace/box-publish.test.ts +204 -0
- package/src/workspace/box-publish.ts +420 -0
- package/src/workspace/box-ship.test.ts +96 -0
- package/src/workspace/broker-protocol.schema.json +186 -0
- package/src/workspace/broker-protocol.test.ts +122 -0
- package/src/workspace/broker-protocol.ts +317 -0
- package/src/workspace/builder-tiers.test.ts +146 -0
- package/src/workspace/change-set.schema.json +460 -0
- package/src/workspace/chant-migrations.e2e.test.ts +17 -5
- package/src/workspace/checks/boxes.test.ts +3 -0
- package/src/workspace/checks.ts +10 -3
- package/src/workspace/component-environments.test.ts +105 -0
- package/src/workspace/composites.schema.json +23 -4
- package/src/workspace/composites.test.ts +8 -8
- package/src/workspace/composites.ts +13 -2
- package/src/workspace/conformance/__fixture__/decisions/decision.schema.json +2 -2
- package/src/workspace/conformance/__fixtures__/reference-broker.ts +207 -0
- package/src/workspace/conformance/__writer_fixture__/answers/answer.kind.mjs +7 -2
- package/src/workspace/conformance/__writer_fixture__/answers/answer.schema.json +42 -2
- package/src/workspace/conformance/__writer_fixture__/contracts/contract.kind.mjs +37 -0
- package/src/workspace/conformance/__writer_fixture__/contracts/contract.schema.json +213 -0
- package/src/workspace/conformance/__writer_fixture__/decisions/points.json +96 -0
- package/src/workspace/conformance/__writer_fixture__/drivers/driver.kind.mjs +15 -0
- package/src/workspace/conformance/__writer_fixture__/drivers/driver.schema.json +38 -0
- package/src/workspace/conformance/__writer_fixture__/sessions/session.kind.mjs +14 -8
- package/src/workspace/conformance/__writer_fixture__/sessions/session.schema.json +187 -6
- package/src/workspace/conformance/__writer_fixture__/work/work.schema.json +25 -0
- package/src/workspace/conformance/broker.test.ts +122 -0
- package/src/workspace/conformance/broker.ts +792 -0
- package/src/workspace/conformance/conformance.e2e.test.ts +2 -1
- package/src/workspace/conformance/index.mjs +22 -1
- package/src/workspace/conformance/index.ts +5 -1
- package/src/workspace/conformance/vitest.ts +33 -1
- package/src/workspace/conformance/writer-conformance.e2e.test.ts +49 -3
- package/src/workspace/conformance/writer.test.ts +32 -2
- package/src/workspace/conformance/writer.ts +339 -28
- package/src/workspace/decide.test.ts +208 -4
- package/src/workspace/decide.ts +427 -73
- package/src/workspace/decision-points.schema.json +11 -4
- package/src/workspace/declaration.schema.json +153 -4
- package/src/workspace/declaration.ts +141 -0
- package/src/workspace/environments.ts +66 -14
- package/src/workspace/factory-rules.test.ts +62 -0
- package/src/workspace/factory-rules.ts +218 -0
- package/src/workspace/graph-cli.ts +2 -2
- package/src/workspace/graph.schema.json +67 -0
- package/src/workspace/identity.test.ts +23 -0
- package/src/workspace/init.ts +82 -2
- package/src/workspace/intent-cli.ts +26 -9
- package/src/workspace/intent-patch-id.test.ts +162 -0
- package/src/workspace/intent-record.schema.json +130 -3
- package/src/workspace/intent-record.ts +74 -28
- package/src/workspace/intent-reviews.test.ts +76 -0
- package/src/workspace/intent-squash.test.ts +213 -0
- package/src/workspace/intent-why.test.ts +5 -5
- package/src/workspace/intent.schema.json +234 -4
- package/src/workspace/intent.ts +260 -66
- package/src/workspace/json-edit.test.ts +121 -0
- package/src/workspace/json-edit.ts +142 -0
- package/src/workspace/jsonc.ts +20 -0
- package/src/workspace/kinds.ts +157 -2
- package/src/workspace/lexicon-symbols.test.ts +148 -0
- package/src/workspace/lexicon-symbols.ts +125 -0
- package/src/workspace/lineage-upgrade.test.ts +63 -0
- package/src/workspace/lineage-upgrade.ts +146 -5
- package/src/workspace/plan-summary.schema.json +155 -0
- package/src/workspace/point-answer.schema.json +42 -2
- package/src/workspace/points-cli.ts +42 -16
- package/src/workspace/points-write.schema.json +162 -4
- package/src/workspace/points.schema.json +157 -2
- package/src/workspace/points.test.ts +13 -1
- package/src/workspace/points.ts +118 -12
- package/src/workspace/pr-report.schema.json +95 -0
- package/src/workspace/profiles.test.ts +159 -0
- package/src/workspace/read-contract.test.ts +2 -2
- package/src/workspace/reason-codes.test.ts +13 -1
- package/src/workspace/reason-codes.ts +38 -4
- package/src/workspace/record-sessions.ts +9 -8
- package/src/workspace/records-amend.schema.json +78 -0
- package/src/workspace/records-cli.ts +20 -6
- package/src/workspace/records-close.schema.json +78 -0
- package/src/workspace/records-close.ts +31 -7
- package/src/workspace/records-concurrent.e2e.test.ts +135 -0
- package/src/workspace/records-concurrent.test.ts +145 -0
- package/src/workspace/records-formats.test.ts +46 -10
- package/src/workspace/records-new.schema.json +7 -0
- package/src/workspace/records-review.schema.json +78 -0
- package/src/workspace/records-sessions-write.test.ts +69 -0
- package/src/workspace/records-sessions.test.ts +22 -0
- package/src/workspace/records-write.ts +192 -25
- package/src/workspace/records.schema.json +16 -0
- package/src/workspace/records.ts +50 -9
- package/src/workspace/reference-kinds/app/package.json +10 -0
- package/src/workspace/reference-kinds/app/workspace-kinds.json +45 -0
- package/src/workspace/run-statement.schema.json +622 -0
- package/src/workspace/runs-cli.ts +343 -14
- package/src/workspace/runs-statement.test.ts +239 -0
- package/src/workspace/runs-write.schema.json +416 -5
- package/src/workspace/runs.schema.json +451 -4
- package/src/workspace/runs.test.ts +39 -0
- package/src/workspace/runs.ts +296 -35
- package/src/workspace/session-kinds.ts +3 -3
- package/src/workspace/squash.ts +198 -0
- package/src/workspace/status-contract.test.ts +5 -2
- package/src/workspace/status-gates.ts +3 -1
- package/src/workspace/status.schema.json +126 -3
- package/src/workspace/status.ts +90 -1
- package/src/workspace/symbols.ts +20 -9
- package/src/workspace/trust/run-commits.ts +115 -0
- package/src/workspace/trust/run-statement.ts +226 -0
- package/src/workspace/trust/seal.ts +8 -2
- package/src/workspace/trust/verify-cli.ts +6 -5
- package/src/workspace/trust/verify.ts +39 -6
- package/src/workspace/wip-cli.ts +97 -0
- package/src/workspace/wip-write.schema.json +504 -0
- package/src/workspace/wip.schema.json +334 -0
- package/src/workspace/wip.test.ts +294 -0
- package/src/workspace/wip.ts +753 -0
- package/src/workspace/work-cli.ts +3 -0
- package/src/workspace/work-evidence.schema.json +3 -0
- package/src/workspace/work-size.test.ts +77 -0
- package/src/workspace/work-size.ts +93 -0
- package/src/workspace/workspace-kinds.schema.json +122 -0
- package/src/workspace/write-lock-cli.ts +131 -0
- package/src/workspace/write-lock.schema.json +172 -0
- package/src/workspace/write-lock.test.ts +189 -0
- package/src/workspace/write-lock.ts +474 -0
- package/src/workspace/write-scope.test.ts +16 -1
- package/src/workspace/write-scope.ts +33 -11
package/src/workspace/symbols.ts
CHANGED
|
@@ -7,7 +7,10 @@
|
|
|
7
7
|
* walk follows those lines through history as it follows any line range
|
|
8
8
|
* (`git log -L`). Resolvers are chosen by file extension. Core ships one for
|
|
9
9
|
* TypeScript and JavaScript, parsed with the TypeScript compiler chant already
|
|
10
|
-
* depends on
|
|
10
|
+
* depends on. A lexicon can contribute more through its plugin's
|
|
11
|
+
* `symbolResolvers()` (#3313), which the intent walk loads for a file in a
|
|
12
|
+
* member that configures the lexicon (`lexicon-symbols.ts`); core's resolver
|
|
13
|
+
* stays the one for `.ts` and `.js`. A file with no resolver is refused with
|
|
11
14
|
* `intent-symbol-unsupported`, and a line range still works for it.
|
|
12
15
|
*
|
|
13
16
|
* A symbol is a name, or a dotted path through classes, namespaces,
|
|
@@ -143,17 +146,25 @@ const typescriptResolver: SymbolResolver = {
|
|
|
143
146
|
/** The resolvers core ships, chosen by the file's extension. */
|
|
144
147
|
export const SYMBOL_RESOLVERS: readonly SymbolResolver[] = [typescriptResolver];
|
|
145
148
|
|
|
146
|
-
/**
|
|
147
|
-
|
|
149
|
+
/**
|
|
150
|
+
* The resolver for a path, or undefined when none reads its language. Core's
|
|
151
|
+
* resolvers come first, so a lexicon's never takes `.ts` or `.js` from them;
|
|
152
|
+
* among `extra`, the first that names the extension wins.
|
|
153
|
+
*/
|
|
154
|
+
export function symbolResolverFor(path: string, extra: readonly SymbolResolver[] = []): SymbolResolver | undefined {
|
|
148
155
|
const lower = path.toLowerCase();
|
|
149
|
-
return SYMBOL_RESOLVERS.find((r) => r.extensions.some((e) => lower.endsWith(e)));
|
|
156
|
+
return [...SYMBOL_RESOLVERS, ...extra].find((r) => r.extensions.some((e) => lower.endsWith(e.toLowerCase())));
|
|
150
157
|
}
|
|
151
158
|
|
|
152
|
-
/**
|
|
153
|
-
|
|
154
|
-
|
|
159
|
+
/**
|
|
160
|
+
* Resolve `symbol` in the file `path` holding `text`: its current lines, or
|
|
161
|
+
* why it can't be. `extra` are the resolvers a lexicon contributed for the
|
|
162
|
+
* file's member (#3313).
|
|
163
|
+
*/
|
|
164
|
+
export function resolveSymbol(path: string, text: string, symbol: string, extra: readonly SymbolResolver[] = []): SymbolResolution {
|
|
165
|
+
const resolver = symbolResolverFor(path, extra);
|
|
155
166
|
if (!resolver) {
|
|
156
|
-
const known = SYMBOL_RESOLVERS.flatMap((r) => r.extensions).join(", ");
|
|
167
|
+
const known = [...new Set([...SYMBOL_RESOLVERS, ...extra].flatMap((r) => r.extensions))].join(", ");
|
|
157
168
|
return { ok: false, reason: "unsupported", message: `${path} has no symbol resolver: symbols resolve in ${known} files. Give a line range, ${path}:<start>-<end>, instead`, candidates: [] };
|
|
158
169
|
}
|
|
159
170
|
const all = resolver.declarations(path, text);
|
|
@@ -165,7 +176,7 @@ export function resolveSymbol(path: string, text: string, symbol: string): Symbo
|
|
|
165
176
|
return { ok: true, declaration: { qualified: symbol, kind: exact[0].kind, lines: { start, end } } };
|
|
166
177
|
}
|
|
167
178
|
const nested = [...new Set(all.filter((d) => d.qualified.endsWith(`.${symbol}`)).map((d) => d.qualified))];
|
|
168
|
-
if (nested.length === 1) return resolveSymbol(path, text, nested[0]);
|
|
179
|
+
if (nested.length === 1) return resolveSymbol(path, text, nested[0], extra);
|
|
169
180
|
if (nested.length > 1) {
|
|
170
181
|
return { ok: false, reason: "ambiguous", message: `${symbol} names ${nested.length} declarations in ${path}: ${nested.join(", ")}. Give the qualified name`, candidates: nested };
|
|
171
182
|
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which commits of a change agent runs made, and which of those a signed run
|
|
3
|
+
* statement covers (#3192), for `chant workspace verify`.
|
|
4
|
+
*
|
|
5
|
+
* A commit names its run by the `Chant-Run` trailer (ws-075), or a run's end
|
|
6
|
+
* lists it (ws-076). A run statement verified against the runner keys at
|
|
7
|
+
* base names the commits it covers as its subjects. Every `_agent-runs`
|
|
8
|
+
* directory on `chant/lifecycle` is read, the flat one and each member's,
|
|
9
|
+
* from the local branch or, when there is none, from `origin`'s copy. Nothing
|
|
10
|
+
* is fetched: CI fetches `chant/lifecycle` before it verifies.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { execFileSync } from "node:child_process";
|
|
14
|
+
import { AGENT_RUNS_DIR, LEDGER_BRANCH, foldRun, parseRunFile, readRunFiles, type RunView } from "../runs";
|
|
15
|
+
import { RUN_TRAILER } from "../trailers";
|
|
16
|
+
import type { RunnerKey } from "./policy";
|
|
17
|
+
import { attestRun } from "./run-statement";
|
|
18
|
+
|
|
19
|
+
function git(repo: string, args: string[]): string | undefined {
|
|
20
|
+
try {
|
|
21
|
+
return execFileSync("git", args, { cwd: repo, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"], maxBuffer: 256 * 1024 * 1024 });
|
|
22
|
+
} catch {
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Every run on the ledger, in every member's `_agent-runs`, and the ref and tip read. */
|
|
28
|
+
export function readLedgerRuns(repo: string): { ref: string | null; tip: string | null; runs: RunView[] } {
|
|
29
|
+
for (const ref of [`refs/heads/${LEDGER_BRANCH}`, `refs/remotes/origin/${LEDGER_BRANCH}`]) {
|
|
30
|
+
const tip = git(repo, ["rev-parse", "--verify", "--quiet", `${ref}^{commit}`])?.trim();
|
|
31
|
+
if (!tip) continue;
|
|
32
|
+
const dirs = new Set<string>();
|
|
33
|
+
for (const path of (git(repo, ["ls-tree", "-r", "--name-only", tip]) ?? "").split("\n")) {
|
|
34
|
+
const at = path.lastIndexOf(`${AGENT_RUNS_DIR}/`);
|
|
35
|
+
if (at >= 0 && (at === 0 || path[at - 1] === "/") && path.endsWith(".jsonl")) dirs.add(path.slice(0, at + AGENT_RUNS_DIR.length));
|
|
36
|
+
}
|
|
37
|
+
const runs: RunView[] = [];
|
|
38
|
+
for (const dir of dirs) {
|
|
39
|
+
for (const [id, f] of readRunFiles(repo, dir, ref).files) {
|
|
40
|
+
const view = foldRun(id, f.path, parseRunFile(f.content, id).lines);
|
|
41
|
+
if (view) runs.push(view);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return { ref, tip, runs };
|
|
45
|
+
}
|
|
46
|
+
return { ref: null, tip: null, runs: [] };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** One commit of a change that an agent run made, and whether a signed statement covers it. */
|
|
50
|
+
export interface RunCommitVerdict {
|
|
51
|
+
commit: string;
|
|
52
|
+
/** The runs that name the commit: by its `Chant-Run` trailer, or by their end's list. */
|
|
53
|
+
runs: string[];
|
|
54
|
+
/** Whether a statement verified against the runner keys at base names the commit. */
|
|
55
|
+
attested: boolean;
|
|
56
|
+
/** The run whose statement covers it, when attested. */
|
|
57
|
+
run?: string;
|
|
58
|
+
signer?: string;
|
|
59
|
+
class?: RunnerKey["class"];
|
|
60
|
+
reason: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface ChangeRuns {
|
|
64
|
+
/** The ledger ref read, or null when there is no `chant/lifecycle` here. */
|
|
65
|
+
ledger: string | null;
|
|
66
|
+
/** The runner and steward principals at base. */
|
|
67
|
+
runners: string[];
|
|
68
|
+
/** The change's commits that a run names or a statement covers, oldest first. */
|
|
69
|
+
commits: RunCommitVerdict[];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Judge `commits` (the change, oldest first) against the run ledger: which
|
|
74
|
+
* ones runs made, and which of those a verified statement covers.
|
|
75
|
+
*/
|
|
76
|
+
export function changeRuns(repo: string, commits: string[], runners: readonly RunnerKey[]): ChangeRuns {
|
|
77
|
+
const { ref, runs } = readLedgerRuns(repo);
|
|
78
|
+
const out: ChangeRuns = { ledger: ref, runners: runners.map((r) => r.principal), commits: [] };
|
|
79
|
+
if (commits.length === 0) return out;
|
|
80
|
+
const inChange = new Set(commits);
|
|
81
|
+
const naming = new Map<string, Set<string>>();
|
|
82
|
+
const name = (sha: string, id: string) => (naming.get(sha) ?? naming.set(sha, new Set()).get(sha)!).add(id);
|
|
83
|
+
// Trailers on the change's own commits.
|
|
84
|
+
const log = git(repo, ["log", "--no-walk=unsorted", `--format=%x00%H%x1f%(trailers:key=${RUN_TRAILER},valueonly,separator=%x1e)`, ...commits]) ?? "";
|
|
85
|
+
for (const rec of log.split("\0")) {
|
|
86
|
+
if (!rec.trim()) continue;
|
|
87
|
+
const [sha, values] = rec.split("\x1f");
|
|
88
|
+
for (const v of (values ?? "").split("\x1e").map((x) => x.trim()).filter(Boolean)) name(sha.trim(), v);
|
|
89
|
+
}
|
|
90
|
+
const covered = new Map<string, { run: string; signer: string; class: RunnerKey["class"] }>();
|
|
91
|
+
for (const run of runs) {
|
|
92
|
+
for (const c of run.commits) if (inChange.has(c.sha)) name(c.sha, run.id);
|
|
93
|
+
const a = attestRun(run, runners);
|
|
94
|
+
if (a.status !== "signed") continue;
|
|
95
|
+
for (const sha of a.commits) if (inChange.has(sha) && !covered.has(sha)) covered.set(sha, { run: run.id, signer: a.signer!, class: a.class! });
|
|
96
|
+
}
|
|
97
|
+
const byId = new Map(runs.map((r) => [r.id, r]));
|
|
98
|
+
for (const commit of commits) {
|
|
99
|
+
const ids = [...(naming.get(commit) ?? [])].sort();
|
|
100
|
+
const cover = covered.get(commit);
|
|
101
|
+
if (ids.length === 0 && !cover) continue;
|
|
102
|
+
if (cover) {
|
|
103
|
+
out.commits.push({ commit, runs: ids, attested: true, run: cover.run, signer: cover.signer, class: cover.class, reason: `agent run ${cover.run}'s statement, signed by ${cover.signer} (${cover.class}), names this commit` });
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
const why = ids.map((id) => {
|
|
107
|
+
const r = byId.get(id);
|
|
108
|
+
if (!r) return `${id} is not in the run ledger${ref ? "" : " (there is no chant/lifecycle branch here; fetch it)"}`;
|
|
109
|
+
const a = attestRun(r, runners);
|
|
110
|
+
return a.status === "signed" ? `${id}'s statement, signed by ${a.signer}, does not name this commit` : `${id}: ${a.reason}`;
|
|
111
|
+
});
|
|
112
|
+
out.commits.push({ commit, runs: ids, attested: false, reason: why.join("; ") });
|
|
113
|
+
}
|
|
114
|
+
return out;
|
|
115
|
+
}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The agent-run statement (#3192, ws-090): a runner's or steward's signed
|
|
3
|
+
* word that an agent run happened as its record says.
|
|
4
|
+
*
|
|
5
|
+
* The run record (#3033, ws-076) is written by whatever ran the agent, so on
|
|
6
|
+
* its own it is as trustworthy as that writer. A statement is an in-toto
|
|
7
|
+
* Statement v1 in a DSSE envelope (./dsse.ts), signed with an Ed25519 key the
|
|
8
|
+
* trust policy at base lists under `runners` in `.chant/trust.json`, the same
|
|
9
|
+
* keys runner evidence uses (#2553, ws-069). A person's key, one the signers
|
|
10
|
+
* file lists, is never a runner key.
|
|
11
|
+
*
|
|
12
|
+
* - `subject`: each commit the run made, by its id (`digest.gitCommit`), with
|
|
13
|
+
* its `git patch-id --stable` as an annotation when it has a patch.
|
|
14
|
+
* - `predicate`: the signer's principal, the run's id and the SHA-256 of its
|
|
15
|
+
* record (the start and end lines), the work item, the harness, the model
|
|
16
|
+
* and provider, and the principal the run worked for (studio-036 b).
|
|
17
|
+
*
|
|
18
|
+
* The statement is stored as a run-ledger fact: a `statement` line in the
|
|
19
|
+
* run's own file on `chant/lifecycle`, after its end. Verifying needs only
|
|
20
|
+
* the envelope, the run's record and the runner keys at base, so it works
|
|
21
|
+
* offline, and a verifier re-checks every stored statement rather than
|
|
22
|
+
* trusting that it was checked when written.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { createHash } from "node:crypto";
|
|
26
|
+
import { z } from "zod";
|
|
27
|
+
import { canonicalJson } from "../../effect-receipt";
|
|
28
|
+
import type { ReasonCode } from "../reason-codes";
|
|
29
|
+
import type { RunEndLine, RunStartLine, RunView } from "../runs";
|
|
30
|
+
import { IN_TOTO_PAYLOAD_TYPE, signEnvelope, verifyEnvelope, type DsseEnvelope } from "./dsse";
|
|
31
|
+
import { STATEMENT_TYPE } from "./evidence";
|
|
32
|
+
import type { KeyObject } from "node:crypto";
|
|
33
|
+
import type { RunnerKey } from "./policy";
|
|
34
|
+
|
|
35
|
+
export const AGENT_RUN_PREDICATE = "https://intentius.io/chant/agent-run/v1";
|
|
36
|
+
|
|
37
|
+
const sha256Hex = z.string().regex(/^[0-9a-f]{64}$/);
|
|
38
|
+
const fullCommit = z.string().regex(/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/);
|
|
39
|
+
const patchId = z.string().regex(/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/);
|
|
40
|
+
const text = z.string().min(1);
|
|
41
|
+
|
|
42
|
+
/** The statement a runner signs for a run. Strict, so a verifier never acts on a field it does not know. */
|
|
43
|
+
export const runStatementSchema = z
|
|
44
|
+
.object({
|
|
45
|
+
_type: z.literal(STATEMENT_TYPE),
|
|
46
|
+
subject: z.array(
|
|
47
|
+
z
|
|
48
|
+
.object({
|
|
49
|
+
name: fullCommit,
|
|
50
|
+
digest: z.object({ gitCommit: fullCommit }).strict(),
|
|
51
|
+
annotations: z.object({ patchId }).strict().optional(),
|
|
52
|
+
})
|
|
53
|
+
.strict()
|
|
54
|
+
.refine((s) => s.name === s.digest.gitCommit, "a commit subject is named by its own id"),
|
|
55
|
+
),
|
|
56
|
+
predicateType: z.literal(AGENT_RUN_PREDICATE),
|
|
57
|
+
predicate: z
|
|
58
|
+
.object({
|
|
59
|
+
signer: text,
|
|
60
|
+
run: z.object({ id: text, record: z.object({ sha256: sha256Hex }).strict() }).strict(),
|
|
61
|
+
unit: z.object({ id: text, kind: text.nullable() }).strict().nullable(),
|
|
62
|
+
harness: z.object({ name: text, version: text.nullable() }).strict(),
|
|
63
|
+
model: text.nullable(),
|
|
64
|
+
provider: text.nullable(),
|
|
65
|
+
by: text.nullable(),
|
|
66
|
+
})
|
|
67
|
+
.strict(),
|
|
68
|
+
})
|
|
69
|
+
.strict();
|
|
70
|
+
export type RunStatement = z.infer<typeof runStatementSchema>;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The SHA-256 of a run's record: the canonical JSON (keys sorted, no
|
|
74
|
+
* whitespace) of the array `[start line, end line]` as the ledger holds them.
|
|
75
|
+
* Statement lines are not part of it, so storing a statement never changes
|
|
76
|
+
* the hash it signs.
|
|
77
|
+
*/
|
|
78
|
+
export function runRecordDigest(start: RunStartLine, end: RunEndLine): string {
|
|
79
|
+
return createHash("sha256").update(canonicalJson([start, end]), "utf8").digest("hex");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** The statement for an ended run, as `signer` would sign it. The run's commits are its subjects, sorted by id. */
|
|
83
|
+
export function buildRunStatement(run: RunView, signer: string): RunStatement {
|
|
84
|
+
if (!run.record) throw new Error(`agent run ${run.id} has not ended, so it has no record to sign`);
|
|
85
|
+
const commits = [...new Map(run.commits.map((c) => [c.sha, c])).values()].sort((a, b) => (a.sha < b.sha ? -1 : a.sha > b.sha ? 1 : 0));
|
|
86
|
+
return {
|
|
87
|
+
_type: STATEMENT_TYPE,
|
|
88
|
+
subject: commits.map((c) => ({ name: c.sha, digest: { gitCommit: c.sha }, ...(c.patchId ? { annotations: { patchId: c.patchId } } : {}) })),
|
|
89
|
+
predicateType: AGENT_RUN_PREDICATE,
|
|
90
|
+
predicate: {
|
|
91
|
+
signer,
|
|
92
|
+
run: { id: run.id, record: { sha256: run.record.sha256 } },
|
|
93
|
+
unit: run.unit ? { id: run.unit.id, kind: run.unit.kind } : null,
|
|
94
|
+
harness: { name: run.harness.name, version: run.harness.version },
|
|
95
|
+
model: run.model,
|
|
96
|
+
provider: run.provider,
|
|
97
|
+
by: run.by,
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** The payload bytes a signer signs: the statement's canonical JSON. Any serialization verifies; this one is reproducible. */
|
|
103
|
+
export function runStatementPayload(statement: RunStatement): Buffer {
|
|
104
|
+
return Buffer.from(canonicalJson(statement), "utf8");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function signRunStatement(statement: RunStatement, key: KeyObject, publicKey: string): DsseEnvelope {
|
|
108
|
+
return signEnvelope(IN_TOTO_PAYLOAD_TYPE, runStatementPayload(statement), key, publicKey);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ── Verifying ────────────────────────────────────────────────────────────────
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Why a stored statement does not verify. Each is in `reason-codes.ts`.
|
|
115
|
+
* `envelope-invalid` and `envelope-untrusted` are runner evidence's codes:
|
|
116
|
+
* the envelope is the same, and so is the rule for its keys.
|
|
117
|
+
*/
|
|
118
|
+
export const RUN_STATEMENT_FAILURE_CODES = ["envelope-invalid", "envelope-untrusted", "run-statement-invalid", "run-statement-signer-mismatch", "run-statement-mismatch"] as const satisfies readonly ReasonCode[];
|
|
119
|
+
export type RunStatementFailureCode = (typeof RUN_STATEMENT_FAILURE_CODES)[number];
|
|
120
|
+
|
|
121
|
+
/** One stored statement, judged against the runner keys at base and the run's record. */
|
|
122
|
+
export type RunStatementVerdict =
|
|
123
|
+
| { status: "verified"; signer: string; class: RunnerKey["class"]; keyid: string; statement: RunStatement; reason: string }
|
|
124
|
+
| {
|
|
125
|
+
status: "mismatch" | "untrusted" | "invalid";
|
|
126
|
+
code: RunStatementFailureCode;
|
|
127
|
+
reason: string;
|
|
128
|
+
/** Set when a listed runner key signed it, so the failure is in what it says. */
|
|
129
|
+
signer?: string;
|
|
130
|
+
class?: RunnerKey["class"];
|
|
131
|
+
keyid?: string;
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const same = (a: unknown, b: unknown) => canonicalJson(a ?? null) === canonicalJson(b ?? null);
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Verify one envelope for `run`: a signature by a runner key at base, a
|
|
138
|
+
* well-formed statement whose signer is that key's principal, for this run,
|
|
139
|
+
* and agreeing with its record. Never throws.
|
|
140
|
+
*/
|
|
141
|
+
export function verifyRunStatement(envelope: unknown, runners: readonly RunnerKey[], run: RunView): RunStatementVerdict {
|
|
142
|
+
const v = verifyEnvelope(envelope, runners);
|
|
143
|
+
if (!v.ok) return { status: v.code === "envelope-untrusted" ? "untrusted" : "invalid", code: v.code, reason: v.reason };
|
|
144
|
+
const runner = runners.find((r) => r.principal === v.principal)!;
|
|
145
|
+
const signed = { signer: v.principal, class: runner.class, keyid: v.keyid };
|
|
146
|
+
if (v.payloadType !== IN_TOTO_PAYLOAD_TYPE) {
|
|
147
|
+
return { status: "invalid", code: "run-statement-invalid", reason: `payload type ${v.payloadType} is not ${IN_TOTO_PAYLOAD_TYPE}`, ...signed };
|
|
148
|
+
}
|
|
149
|
+
let raw: unknown;
|
|
150
|
+
try {
|
|
151
|
+
raw = JSON.parse(v.payload.toString("utf8"));
|
|
152
|
+
} catch {
|
|
153
|
+
return { status: "invalid", code: "run-statement-invalid", reason: "the payload is not JSON", ...signed };
|
|
154
|
+
}
|
|
155
|
+
const parsed = runStatementSchema.safeParse(raw);
|
|
156
|
+
if (!parsed.success) {
|
|
157
|
+
const detail = parsed.error.issues.map((i) => `${i.path.join(".") || "/"}: ${i.message}`).join("; ");
|
|
158
|
+
return { status: "invalid", code: "run-statement-invalid", reason: `the payload is not a ${AGENT_RUN_PREDICATE} statement: ${detail}`, ...signed };
|
|
159
|
+
}
|
|
160
|
+
const statement = parsed.data;
|
|
161
|
+
const p = statement.predicate;
|
|
162
|
+
// A runner cannot speak for another: the principal inside is the one whose key signed.
|
|
163
|
+
if (p.signer !== v.principal) {
|
|
164
|
+
return { status: "invalid", code: "run-statement-signer-mismatch", reason: `the statement names signer ${JSON.stringify(p.signer)}, but ${v.principal}'s key signed it`, ...signed };
|
|
165
|
+
}
|
|
166
|
+
const differs: string[] = [];
|
|
167
|
+
if (p.run.id !== run.id) differs.push(`it is for run ${p.run.id}`);
|
|
168
|
+
else if (!run.record) differs.push("the run has no end in the ledger");
|
|
169
|
+
else if (p.run.record.sha256 !== run.record.sha256) differs.push(`the record hashed ${p.run.record.sha256.slice(0, 12)} when signed and ${run.record.sha256.slice(0, 12)} now`);
|
|
170
|
+
if (!same(p.unit, run.unit)) differs.push(`unit ${JSON.stringify(p.unit?.id ?? null)}, the record says ${JSON.stringify(run.unit?.id ?? null)}`);
|
|
171
|
+
if (!same(p.harness, run.harness)) differs.push(`harness ${JSON.stringify(p.harness.name)}, the record says ${JSON.stringify(run.harness.name)}`);
|
|
172
|
+
if (p.model !== run.model) differs.push(`model ${JSON.stringify(p.model)}, the record says ${JSON.stringify(run.model)}`);
|
|
173
|
+
if (p.provider !== run.provider) differs.push(`provider ${JSON.stringify(p.provider)}, the record says ${JSON.stringify(run.provider)}`);
|
|
174
|
+
if (p.by !== run.by) differs.push(`by ${JSON.stringify(p.by)}, the record says ${JSON.stringify(run.by)}`);
|
|
175
|
+
if (differs.length > 0) {
|
|
176
|
+
return { status: "mismatch", code: "run-statement-mismatch", reason: `signed by ${v.principal}, but the statement does not match run ${run.id}'s record: ${differs.join("; ")}`, ...signed };
|
|
177
|
+
}
|
|
178
|
+
const n = statement.subject.length;
|
|
179
|
+
return { status: "verified", ...signed, statement, reason: `signed by ${v.principal} (${runner.class}) over the run's record and ${n} commit${n === 1 ? "" : "s"}` };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** A run's attestation: its best statement, or why it has none. */
|
|
183
|
+
export interface RunAttestation {
|
|
184
|
+
/**
|
|
185
|
+
* `signed`: a statement verifies. `unsigned`: the ledger holds none.
|
|
186
|
+
* Otherwise the best failure among the stored statements, `mismatch`
|
|
187
|
+
* before `untrusted` before `invalid`.
|
|
188
|
+
*/
|
|
189
|
+
status: "signed" | "unsigned" | "mismatch" | "untrusted" | "invalid";
|
|
190
|
+
signer: string | null;
|
|
191
|
+
class: RunnerKey["class"] | null;
|
|
192
|
+
keyid: string | null;
|
|
193
|
+
/** The code of the failure `status` reports, or null when signed or unsigned. */
|
|
194
|
+
code: RunStatementFailureCode | null;
|
|
195
|
+
reason: string;
|
|
196
|
+
/** The commits the verified statement names, sorted. Empty unless signed. */
|
|
197
|
+
commits: string[];
|
|
198
|
+
/** One verdict per stored statement, in ledger order. */
|
|
199
|
+
statements: RunStatementVerdict[];
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const RANK = { mismatch: 0, untrusted: 1, invalid: 2 } as const;
|
|
203
|
+
|
|
204
|
+
/** Judge every statement the ledger holds for `run`. */
|
|
205
|
+
export function attestRun(run: RunView, runners: readonly RunnerKey[]): RunAttestation {
|
|
206
|
+
const verdicts = run.statements.map((s) => verifyRunStatement(s.envelope, runners, run));
|
|
207
|
+
const good = verdicts.find((v): v is Extract<RunStatementVerdict, { status: "verified" }> => v.status === "verified");
|
|
208
|
+
if (good) {
|
|
209
|
+
return { status: "signed", signer: good.signer, class: good.class, keyid: good.keyid, code: null, reason: good.reason, commits: good.statement.subject.map((s) => s.name), statements: verdicts };
|
|
210
|
+
}
|
|
211
|
+
if (verdicts.length === 0) {
|
|
212
|
+
const reason = run.state === "running" ? "the run has not ended, and a statement is signed over an ended run" : "the ledger holds no statement for this run";
|
|
213
|
+
return { status: "unsigned", signer: null, class: null, keyid: null, code: null, reason, commits: [], statements: verdicts };
|
|
214
|
+
}
|
|
215
|
+
const worst = [...verdicts].sort((a, b) => RANK[a.status as keyof typeof RANK] - RANK[b.status as keyof typeof RANK])[0] as Exclude<RunStatementVerdict, { status: "verified" }>;
|
|
216
|
+
return {
|
|
217
|
+
status: worst.status,
|
|
218
|
+
signer: worst.signer ?? null,
|
|
219
|
+
class: worst.class ?? null,
|
|
220
|
+
keyid: worst.keyid ?? null,
|
|
221
|
+
code: worst.code,
|
|
222
|
+
reason: worst.reason,
|
|
223
|
+
commits: [],
|
|
224
|
+
statements: verdicts,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
@@ -94,6 +94,8 @@ export interface SealedGateApproval {
|
|
|
94
94
|
environment?: string;
|
|
95
95
|
planDigest?: string;
|
|
96
96
|
resolvedBy: string;
|
|
97
|
+
/** Who relayed the approval for `resolvedBy` (#3402), when someone did. */
|
|
98
|
+
relayedBy?: string;
|
|
97
99
|
timestamp: string;
|
|
98
100
|
seal?: unknown;
|
|
99
101
|
}
|
|
@@ -102,10 +104,14 @@ export interface SealedGateApproval {
|
|
|
102
104
|
* The bytes a gate approval's seal signs (#3163): the op (or component), the
|
|
103
105
|
* gate, the environment, the plan digest, the approver and the time, joined
|
|
104
106
|
* by LF with no final newline. An approval that binds no environment or plan
|
|
105
|
-
* signs an empty line for it.
|
|
107
|
+
* signs an empty line for it. A relayed approval (#3402) signs a seventh line,
|
|
108
|
+
* `relayed-by <principal>`, so the relay can't be added, changed or removed
|
|
109
|
+
* after sealing; an approval nobody relayed signs the six lines it always did,
|
|
110
|
+
* so seals made before #3402 still verify.
|
|
106
111
|
*/
|
|
107
112
|
export function gateSealPayload(a: SealedGateApproval): Buffer {
|
|
108
|
-
|
|
113
|
+
const relay = a.relayedBy !== undefined ? `\nrelayed-by ${a.relayedBy}` : "";
|
|
114
|
+
return Buffer.from(`${a.op}\n${a.gate}\n${a.environment ?? ""}\n${a.planDigest ?? ""}\n${a.resolvedBy}\n${a.timestamp}${relay}`, "utf-8");
|
|
109
115
|
}
|
|
110
116
|
|
|
111
117
|
/** The record an author seal covers (#2688). */
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `chant workspace verify [--base <rev>] [--head <rev>] [--require attested] [--json]`
|
|
2
|
+
* `chant workspace verify [--base <rev>] [--head <rev>] [--require attested | attested-runs] [--json]`
|
|
3
3
|
* (#2547): check a change against the trust policy at its base. See ./verify.ts.
|
|
4
4
|
*
|
|
5
5
|
* Exit 0 when the change passes, 1 when it does not or cannot be checked.
|
|
@@ -11,12 +11,12 @@ import { gitRoot } from "../record-source";
|
|
|
11
11
|
import { activeAttestors } from "./attestor";
|
|
12
12
|
import { verifyChange, type ChangeReport } from "./verify";
|
|
13
13
|
|
|
14
|
-
const USAGE = "chant workspace verify [--base <rev>] [--head <rev>] [--require attested] [--json]";
|
|
14
|
+
const USAGE = "chant workspace verify [--base <rev>] [--head <rev>] [--require attested | attested-runs] [--json]";
|
|
15
15
|
|
|
16
16
|
export async function runWorkspaceVerify(ctx: CommandContext): Promise<number> {
|
|
17
17
|
const { args } = ctx;
|
|
18
|
-
if (args.require !== undefined && args.require !== "attested") {
|
|
19
|
-
console.error(formatError({ message: `--require takes
|
|
18
|
+
if (args.require !== undefined && args.require !== "attested" && args.require !== "attested-runs") {
|
|
19
|
+
console.error(formatError({ message: `--require takes attested or attested-runs, not ${JSON.stringify(args.require)}`, hint: USAGE }));
|
|
20
20
|
return 1;
|
|
21
21
|
}
|
|
22
22
|
const repo = gitRoot(process.cwd());
|
|
@@ -28,7 +28,7 @@ export async function runWorkspaceVerify(ctx: CommandContext): Promise<number> {
|
|
|
28
28
|
repo,
|
|
29
29
|
base: args.base,
|
|
30
30
|
head: args.head,
|
|
31
|
-
require: args.require === "attested"
|
|
31
|
+
require: args.require === "attested" || args.require === "attested-runs" ? args.require : undefined,
|
|
32
32
|
attestors: await activeAttestors(),
|
|
33
33
|
});
|
|
34
34
|
if (args.json) console.log(JSON.stringify(report, null, 2));
|
|
@@ -48,6 +48,7 @@ function printReport(r: ChangeReport): void {
|
|
|
48
48
|
const note = c.skipped ? ` (${c.skipped})` : c.level === "attested" ? "" : ` (${c.reason})`;
|
|
49
49
|
lines.push(` ${c.commit.slice(0, 8)} ${c.level}${who} ${c.subject}${note}`);
|
|
50
50
|
}
|
|
51
|
+
for (const c of r.runs.commits) lines.push(` ${c.commit.slice(0, 8)} agent run ${c.runs.join(", ") || c.run}: ${c.attested ? "attested" : "not attested"}, ${c.reason}`);
|
|
51
52
|
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
53
|
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(", ")}`);
|
|
53
54
|
for (const n of r.notes) lines.push(n);
|
|
@@ -10,6 +10,15 @@
|
|
|
10
10
|
*
|
|
11
11
|
* Attestation is opt-in (#2525 rule 6). Without a signers file at base the
|
|
12
12
|
* check passes and says so, unless `--require attested` asks for more.
|
|
13
|
+
*
|
|
14
|
+
* Agent runs (#3192): a commit an agent run made, by its `Chant-Run` trailer
|
|
15
|
+
* or its run's end, is attested by a run statement that a runner or steward
|
|
16
|
+
* key at base signed and that names the commit. Such a commit counts as
|
|
17
|
+
* `attested` with the attestor `agent-run`, so `--require attested` tells a
|
|
18
|
+
* person's signed commit from a run's attested one by the attestor.
|
|
19
|
+
* `--require attested-runs` asks only that every commit a run made be
|
|
20
|
+
* covered, and needs runner keys at base but no signers file. Without
|
|
21
|
+
* `--require`, `runs` reports and nothing fails (studio-035 d).
|
|
13
22
|
*/
|
|
14
23
|
|
|
15
24
|
import { execFileSync } from "node:child_process";
|
|
@@ -17,6 +26,10 @@ import { type CommitAttestor, type ProvenanceLevel } from "./attestor";
|
|
|
17
26
|
import { policyAtBase, commitProvenance, resolveBase, type BaseSource, type RecordProvenance } from "./provenance";
|
|
18
27
|
import { policyWriters, protectedPaths, type ExcludedSigner, type TrustPolicy } from "./policy";
|
|
19
28
|
import { fileAt, nextVersion, rotationPath, signerHistory, type RotationRefusalCode } from "./rotation";
|
|
29
|
+
import { changeRuns, type ChangeRuns } from "./run-commits";
|
|
30
|
+
|
|
31
|
+
/** What `--require` takes: a provenance level, or `attested-runs` (#3192). */
|
|
32
|
+
export type VerifyRequirement = ProvenanceLevel | "attested-runs";
|
|
20
33
|
|
|
21
34
|
export interface CommitVerdict extends RecordProvenance {
|
|
22
35
|
commit: string;
|
|
@@ -46,8 +59,10 @@ export interface ChangeReport {
|
|
|
46
59
|
writers: string[];
|
|
47
60
|
problems: string[];
|
|
48
61
|
};
|
|
49
|
-
require:
|
|
62
|
+
require: VerifyRequirement | null;
|
|
50
63
|
commits: CommitVerdict[];
|
|
64
|
+
/** The change's commits that agent runs made, and which a signed run statement covers (#3192). */
|
|
65
|
+
runs: ChangeRuns;
|
|
51
66
|
protectedPaths: string[];
|
|
52
67
|
/** Protected paths whose content differs between the merge base and head. */
|
|
53
68
|
protectedChanged: string[];
|
|
@@ -67,7 +82,7 @@ export interface VerifyOptions {
|
|
|
67
82
|
repo: string;
|
|
68
83
|
base?: string;
|
|
69
84
|
head?: string;
|
|
70
|
-
require?:
|
|
85
|
+
require?: VerifyRequirement;
|
|
71
86
|
attestors: readonly CommitAttestor[];
|
|
72
87
|
}
|
|
73
88
|
|
|
@@ -115,6 +130,7 @@ export function verifyChange(opts: VerifyOptions): ChangeReport {
|
|
|
115
130
|
},
|
|
116
131
|
require: opts.require ?? null,
|
|
117
132
|
commits: [],
|
|
133
|
+
runs: { ledger: null, runners: [], commits: [] },
|
|
118
134
|
protectedPaths: protectedPaths(policy),
|
|
119
135
|
protectedChanged: [],
|
|
120
136
|
protectedWrites: [],
|
|
@@ -139,6 +155,20 @@ export function verifyChange(opts: VerifyOptions): ChangeReport {
|
|
|
139
155
|
return report;
|
|
140
156
|
}
|
|
141
157
|
|
|
158
|
+
// Every commit in the change, oldest first, with its parents.
|
|
159
|
+
const listing = git(repo, ["rev-list", "--reverse", "--parents", `${base.commit}..${head}`]).split("\n").filter(Boolean);
|
|
160
|
+
const parentsOf = new Map(listing.map((l) => { const [c, ...p] = l.split(" "); return [c, p.length] as const; }));
|
|
161
|
+
report.runs = changeRuns(repo, [...parentsOf.keys()], policy.runners);
|
|
162
|
+
if (opts.require === "attested-runs") {
|
|
163
|
+
if (policy.runners.length === 0 && report.runs.commits.length > 0) {
|
|
164
|
+
report.notes.push(`.chant/trust.json at base lists no runner keys, so no run statement can verify`);
|
|
165
|
+
}
|
|
166
|
+
for (const r of report.runs.commits) {
|
|
167
|
+
if (r.attested || r.runs.length === 0 || mergeWithoutOwnChanges(repo, r.commit, parentsOf.get(r.commit) ?? 0)) continue;
|
|
168
|
+
report.failures.push(`${r.commit.slice(0, 8)} was made by agent run ${r.runs.join(", ")}, and --require attested-runs was given: ${r.reason}`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
142
172
|
const mergeBase = git(repo, ["merge-base", base.commit, head]).trim();
|
|
143
173
|
const changedPaths = git(repo, ["--literal-pathspecs", "diff", "--name-only", "--no-renames", mergeBase, head, "--", ...report.protectedPaths])
|
|
144
174
|
.split("\n")
|
|
@@ -150,18 +180,21 @@ export function verifyChange(opts: VerifyOptions): ChangeReport {
|
|
|
150
180
|
if (changedPaths.includes(policy.signersPath)) {
|
|
151
181
|
report.notes.push(`this change adds ${policy.signersPath}; it applies to changes made after it is merged, never to this one`);
|
|
152
182
|
}
|
|
153
|
-
if (opts.require) report.failures.push(`--require ${opts.require} was given, and there is no signers file at base to attest anything`);
|
|
183
|
+
if (opts.require && opts.require !== "attested-runs") report.failures.push(`--require ${opts.require} was given, and there is no signers file at base to attest anything`);
|
|
154
184
|
report.ok = report.failures.length === 0;
|
|
155
185
|
return report;
|
|
156
186
|
}
|
|
157
187
|
|
|
158
188
|
// Every commit in the change, judged by the policy at base.
|
|
159
|
-
const
|
|
189
|
+
const runCover = new Map(report.runs.commits.filter((r) => r.attested).map((r) => [r.commit, r]));
|
|
160
190
|
const verdicts = new Map<string, CommitVerdict>();
|
|
161
191
|
for (const line of listing) {
|
|
162
192
|
const [commit, ...parents] = line.split(" ");
|
|
163
193
|
const subject = git(repo, ["log", "-1", "--format=%s", commit]).trim();
|
|
164
|
-
|
|
194
|
+
let p = commitProvenance(repo, policy, commit, opts.attestors);
|
|
195
|
+
const cover = runCover.get(commit);
|
|
196
|
+
// A commit no signer attests, made by a run a runner key at base vouches for (#3192).
|
|
197
|
+
if (p.level !== "attested" && cover) p = { level: "attested", commit, attestor: "agent-run", principal: cover.signer, reason: cover.reason };
|
|
165
198
|
const v: CommitVerdict = { ...p, commit, subject };
|
|
166
199
|
if (p.level !== "attested" && mergeWithoutOwnChanges(repo, commit, parents.length)) {
|
|
167
200
|
v.skipped = "a merge with no changes of its own; the commits it brings are checked instead";
|
|
@@ -205,7 +238,7 @@ export function verifyChange(opts: VerifyOptions): ChangeReport {
|
|
|
205
238
|
}
|
|
206
239
|
}
|
|
207
240
|
|
|
208
|
-
if (opts.require) {
|
|
241
|
+
if (opts.require && opts.require !== "attested-runs") {
|
|
209
242
|
for (const v of report.commits) {
|
|
210
243
|
if (v.skipped || v.level === opts.require) continue;
|
|
211
244
|
report.failures.push(`${v.commit.slice(0, 8)} is ${v.level}, and --require ${opts.require} was given: ${v.reason}`);
|