@intentius/chant 0.101.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/builder.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/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 +1 -1
- package/dist/cli/mcp/workspace-tools.d.ts.map +1 -1
- package/dist/cli/registry.d.ts +11 -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 +19 -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 +55 -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-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 +8 -8
- 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 +11 -11
- 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/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 +2 -0
- 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/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 +5 -0
- package/dist/workspace/agent-cli.d.ts.map +1 -1
- package/dist/workspace/box-factory.d.ts +73 -0
- package/dist/workspace/box-factory.d.ts.map +1 -0
- 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.d.ts +5 -0
- package/dist/workspace/checks.d.ts.map +1 -1
- package/dist/workspace/compose-graph.d.ts +10 -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 +91 -4
- package/dist/workspace/declaration.d.ts.map +1 -1
- package/dist/workspace/declaration.schema.json +234 -5
- 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/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 +18 -4
- package/dist/workspace/kinds.d.ts.map +1 -1
- package/dist/workspace/lineage-hash-index.d.ts.map +1 -1
- package/dist/workspace/lineage-lock.d.ts +2 -2
- package/dist/workspace/lineage-lock.d.ts.map +1 -1
- package/dist/workspace/links.d.ts.map +1 -1
- 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 +21 -1
- package/dist/workspace/reason-codes.d.ts.map +1 -1
- package/dist/workspace/record-assets.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 +27 -1
- package/dist/workspace/records-cli.d.ts.map +1 -1
- package/dist/workspace/records-close.d.ts +1 -1
- 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 +5 -4
- package/dist/workspace/records-write.d.ts.map +1 -1
- package/dist/workspace/records.d.ts +15 -0
- package/dist/workspace/records.d.ts.map +1 -1
- 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/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.map +1 -1
- package/dist/workspace/trust/rotation.d.ts.map +1 -1
- package/dist/workspace/trust/seal.d.ts +33 -0
- package/dist/workspace/trust/seal.d.ts.map +1 -1
- package/dist/workspace/trust/test-repo.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 +79 -19
- package/dist/workspace/write-scope.d.ts.map +1 -1
- 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/cli/commands/__fixtures__/init-lexicon-output/package.json +1 -1
- 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 +18 -0
- package/src/cli/commands/import-parser-options.test.ts +133 -0
- package/src/cli/commands/import.ts +21 -3
- 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/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 +87 -3
- 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 +1 -0
- package/src/cli/main.ts +65 -11
- package/src/cli/mcp/workspace-tools.test.ts +5 -2
- package/src/cli/mcp/workspace-tools.ts +29 -2
- package/src/cli/registry.ts +11 -2
- 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/testdata/postgres-tags.json +242 -0
- package/src/config.test.ts +21 -0
- package/src/config.ts +15 -0
- 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 +55 -3
- package/src/lifecycle/gate-ledger.ts +16 -0
- package/src/lifecycle/observe.test.ts +18 -0
- package/src/lifecycle/observe.ts +6 -4
- 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/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/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 +74 -0
- package/src/op/gate-approval.ts +5 -1
- package/src/op/gate.ts +31 -5
- package/src/op/index.ts +2 -0
- 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/workspace/__fixtures__/studio-template-84c8b21.json +1 -1
- package/src/workspace/agent-cli.ts +7 -3
- package/src/workspace/agent.schema.json +22 -0
- package/src/workspace/box-factory.test.ts +320 -0
- package/src/workspace/box-factory.ts +103 -0
- package/src/workspace/changes.schema.json +12 -20
- package/src/workspace/changes.ts +4 -1
- package/src/workspace/checks/boxes.test.ts +2 -0
- package/src/workspace/checks.ts +29 -5
- package/src/workspace/compose-graph.ts +7 -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 +234 -5
- package/src/workspace/declaration.ts +174 -10
- package/src/workspace/graph-cli.ts +5 -2
- package/src/workspace/graph.schema.json +247 -0
- package/src/workspace/identity.test.ts +196 -0
- package/src/workspace/identity.ts +186 -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.ts +44 -37
- package/src/workspace/points-write.schema.json +1 -0
- package/src/workspace/principal-classes.test.ts +175 -0
- package/src/workspace/principal-classes.ts +307 -0
- package/src/workspace/reason-codes.test.ts +11 -3
- package/src/workspace/reason-codes.ts +24 -1
- package/src/workspace/records-amend.schema.json +2 -0
- package/src/workspace/records-checkout.ts +106 -0
- package/src/workspace/records-cli.ts +103 -11
- package/src/workspace/records-close.schema.json +1 -0
- package/src/workspace/records-new.schema.json +2 -0
- package/src/workspace/records-review.schema.json +2 -0
- package/src/workspace/records-uncommitted.test.ts +171 -0
- package/src/workspace/records-write.ts +28 -6
- package/src/workspace/records.schema.json +72 -1
- package/src/workspace/records.ts +27 -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/status-contract.test.ts +5 -0
- package/src/workspace/status-gates.ts +22 -8
- 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/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/seal.ts +52 -1
- 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 +2 -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 +128 -2
- package/src/workspace/write-scope.ts +170 -45
- package/src/yaml.test.ts +111 -1
- package/src/yaml.ts +141 -549
|
@@ -237,6 +237,10 @@
|
|
|
237
237
|
"items": {
|
|
238
238
|
"$ref": "#/$defs/record"
|
|
239
239
|
}
|
|
240
|
+
},
|
|
241
|
+
"plantable": {
|
|
242
|
+
"description": "Added in contract 1 by #3146. Whether a host such as a studio's planter can plant the workspace as one box: exactly one member's box block declares services, read from the declaration at the revision read. Not a check: a records-only or infra workspace is valid and simply not plantable.",
|
|
243
|
+
"$ref": "#/$defs/plantable"
|
|
240
244
|
}
|
|
241
245
|
}
|
|
242
246
|
},
|
|
@@ -372,6 +376,43 @@
|
|
|
372
376
|
"description": "The nested workspace's links, naming its members as the nested side does."
|
|
373
377
|
}
|
|
374
378
|
}
|
|
379
|
+
},
|
|
380
|
+
"box": {
|
|
381
|
+
"description": "Added in contract 1 by #3146. The factory and listing the member's box block declares (ws-077), read from the declaration at the revision read, so --at gives them as they were. null when the box declares neither, or the member has no box block; the rest of the box block is in chant workspace status --json.",
|
|
382
|
+
"oneOf": [
|
|
383
|
+
{
|
|
384
|
+
"type": "null"
|
|
385
|
+
},
|
|
386
|
+
{
|
|
387
|
+
"type": "object",
|
|
388
|
+
"required": [
|
|
389
|
+
"factory",
|
|
390
|
+
"listing"
|
|
391
|
+
],
|
|
392
|
+
"properties": {
|
|
393
|
+
"factory": {
|
|
394
|
+
"oneOf": [
|
|
395
|
+
{
|
|
396
|
+
"type": "null"
|
|
397
|
+
},
|
|
398
|
+
{
|
|
399
|
+
"$ref": "#/$defs/factory"
|
|
400
|
+
}
|
|
401
|
+
]
|
|
402
|
+
},
|
|
403
|
+
"listing": {
|
|
404
|
+
"oneOf": [
|
|
405
|
+
{
|
|
406
|
+
"type": "null"
|
|
407
|
+
},
|
|
408
|
+
{
|
|
409
|
+
"$ref": "#/$defs/listing"
|
|
410
|
+
}
|
|
411
|
+
]
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
]
|
|
375
416
|
}
|
|
376
417
|
},
|
|
377
418
|
"if": {
|
|
@@ -1019,6 +1060,212 @@
|
|
|
1019
1060
|
}
|
|
1020
1061
|
}
|
|
1021
1062
|
}
|
|
1063
|
+
},
|
|
1064
|
+
"plantable": {
|
|
1065
|
+
"type": "object",
|
|
1066
|
+
"required": [
|
|
1067
|
+
"plantable",
|
|
1068
|
+
"box",
|
|
1069
|
+
"reason"
|
|
1070
|
+
],
|
|
1071
|
+
"properties": {
|
|
1072
|
+
"plantable": {
|
|
1073
|
+
"type": "boolean"
|
|
1074
|
+
},
|
|
1075
|
+
"box": {
|
|
1076
|
+
"type": [
|
|
1077
|
+
"string",
|
|
1078
|
+
"null"
|
|
1079
|
+
],
|
|
1080
|
+
"description": "The member whose box block declares services, or null when the workspace isn't plantable."
|
|
1081
|
+
},
|
|
1082
|
+
"reason": {
|
|
1083
|
+
"description": "Why the workspace isn't plantable, or null when it is.",
|
|
1084
|
+
"oneOf": [
|
|
1085
|
+
{
|
|
1086
|
+
"type": "null"
|
|
1087
|
+
},
|
|
1088
|
+
{
|
|
1089
|
+
"type": "object",
|
|
1090
|
+
"required": [
|
|
1091
|
+
"code",
|
|
1092
|
+
"message"
|
|
1093
|
+
],
|
|
1094
|
+
"properties": {
|
|
1095
|
+
"code": {
|
|
1096
|
+
"enum": [
|
|
1097
|
+
"box-none",
|
|
1098
|
+
"box-several"
|
|
1099
|
+
]
|
|
1100
|
+
},
|
|
1101
|
+
"message": {
|
|
1102
|
+
"type": "string"
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
]
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
},
|
|
1110
|
+
"factory": {
|
|
1111
|
+
"type": "object",
|
|
1112
|
+
"required": [
|
|
1113
|
+
"builds",
|
|
1114
|
+
"check",
|
|
1115
|
+
"checks",
|
|
1116
|
+
"builders",
|
|
1117
|
+
"publish"
|
|
1118
|
+
],
|
|
1119
|
+
"properties": {
|
|
1120
|
+
"builds": {
|
|
1121
|
+
"type": "array",
|
|
1122
|
+
"minItems": 1,
|
|
1123
|
+
"items": {
|
|
1124
|
+
"type": "string"
|
|
1125
|
+
},
|
|
1126
|
+
"description": "The members the factory builds, in declaration order. A work item a person asks for constrains member:<the first>."
|
|
1127
|
+
},
|
|
1128
|
+
"check": {
|
|
1129
|
+
"description": "The verdict command, run from the workspace root, and what it is; null when none is declared.",
|
|
1130
|
+
"oneOf": [
|
|
1131
|
+
{
|
|
1132
|
+
"type": "null"
|
|
1133
|
+
},
|
|
1134
|
+
{
|
|
1135
|
+
"type": "object",
|
|
1136
|
+
"required": [
|
|
1137
|
+
"run",
|
|
1138
|
+
"kind"
|
|
1139
|
+
],
|
|
1140
|
+
"properties": {
|
|
1141
|
+
"run": {
|
|
1142
|
+
"type": "string"
|
|
1143
|
+
},
|
|
1144
|
+
"kind": {
|
|
1145
|
+
"enum": [
|
|
1146
|
+
"test",
|
|
1147
|
+
"build",
|
|
1148
|
+
"lint",
|
|
1149
|
+
"plan",
|
|
1150
|
+
"conformance"
|
|
1151
|
+
]
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
]
|
|
1156
|
+
},
|
|
1157
|
+
"checks": {
|
|
1158
|
+
"type": [
|
|
1159
|
+
"string",
|
|
1160
|
+
"null"
|
|
1161
|
+
],
|
|
1162
|
+
"description": "Where a work item's builder writes its check, from the workspace root, or null for the orchestrator's choice."
|
|
1163
|
+
},
|
|
1164
|
+
"builders": {
|
|
1165
|
+
"type": [
|
|
1166
|
+
"string",
|
|
1167
|
+
"null"
|
|
1168
|
+
],
|
|
1169
|
+
"description": "The member that declares the builder agents, or null."
|
|
1170
|
+
},
|
|
1171
|
+
"publish": {
|
|
1172
|
+
"description": "Where a finished build is published, or null when the factory doesn't publish past the box.",
|
|
1173
|
+
"oneOf": [
|
|
1174
|
+
{
|
|
1175
|
+
"type": "null"
|
|
1176
|
+
},
|
|
1177
|
+
{
|
|
1178
|
+
"type": "object",
|
|
1179
|
+
"required": [
|
|
1180
|
+
"forge",
|
|
1181
|
+
"repo",
|
|
1182
|
+
"base",
|
|
1183
|
+
"branchPrefix",
|
|
1184
|
+
"head"
|
|
1185
|
+
],
|
|
1186
|
+
"properties": {
|
|
1187
|
+
"forge": {
|
|
1188
|
+
"const": "github"
|
|
1189
|
+
},
|
|
1190
|
+
"repo": {
|
|
1191
|
+
"type": "string",
|
|
1192
|
+
"description": "owner/name: where the pull request is opened."
|
|
1193
|
+
},
|
|
1194
|
+
"base": {
|
|
1195
|
+
"type": [
|
|
1196
|
+
"string",
|
|
1197
|
+
"null"
|
|
1198
|
+
],
|
|
1199
|
+
"description": "The branch the pull request targets, or null for the repository's default branch."
|
|
1200
|
+
},
|
|
1201
|
+
"branchPrefix": {
|
|
1202
|
+
"type": "string",
|
|
1203
|
+
"description": "What the published branch's name starts with, before the work item's id. chant/work/ unless declared."
|
|
1204
|
+
},
|
|
1205
|
+
"head": {
|
|
1206
|
+
"type": [
|
|
1207
|
+
"string",
|
|
1208
|
+
"null"
|
|
1209
|
+
],
|
|
1210
|
+
"description": "owner/name of the fork the branch is pushed to, or null to push to repo."
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
]
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
},
|
|
1218
|
+
"listing": {
|
|
1219
|
+
"type": "object",
|
|
1220
|
+
"required": [
|
|
1221
|
+
"published",
|
|
1222
|
+
"title",
|
|
1223
|
+
"line",
|
|
1224
|
+
"cover"
|
|
1225
|
+
],
|
|
1226
|
+
"properties": {
|
|
1227
|
+
"published": {
|
|
1228
|
+
"type": "boolean",
|
|
1229
|
+
"description": "Whether the box is listed; true unless declared otherwise."
|
|
1230
|
+
},
|
|
1231
|
+
"title": {
|
|
1232
|
+
"type": "string",
|
|
1233
|
+
"description": "Empty when not declared: a reader shows a name of its own."
|
|
1234
|
+
},
|
|
1235
|
+
"line": {
|
|
1236
|
+
"type": "string",
|
|
1237
|
+
"description": "Empty when not declared."
|
|
1238
|
+
},
|
|
1239
|
+
"cover": {
|
|
1240
|
+
"description": "The cover image, or null when none is declared.",
|
|
1241
|
+
"oneOf": [
|
|
1242
|
+
{
|
|
1243
|
+
"type": "null"
|
|
1244
|
+
},
|
|
1245
|
+
{
|
|
1246
|
+
"type": "object",
|
|
1247
|
+
"required": [
|
|
1248
|
+
"path",
|
|
1249
|
+
"sha256"
|
|
1250
|
+
],
|
|
1251
|
+
"properties": {
|
|
1252
|
+
"path": {
|
|
1253
|
+
"type": "string",
|
|
1254
|
+
"description": "From the workspace root."
|
|
1255
|
+
},
|
|
1256
|
+
"sha256": {
|
|
1257
|
+
"type": [
|
|
1258
|
+
"string",
|
|
1259
|
+
"null"
|
|
1260
|
+
],
|
|
1261
|
+
"pattern": "^[0-9a-f]{64}$",
|
|
1262
|
+
"description": "The sha256 of the file's bytes at the revision read, or null when it can't be read."
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
]
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1022
1269
|
}
|
|
1023
1270
|
}
|
|
1024
1271
|
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #3163, ws-080: person-attributed records and gate approvals rest on a forge
|
|
3
|
+
* identity or a signer key, and a gate the declaration names passes only on a
|
|
4
|
+
* signed approval from its class.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { readFileSync } from "node:fs";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import { afterEach, describe, expect, test } from "vitest";
|
|
10
|
+
import { parseDeclaration, type Declaration } from "./declaration";
|
|
11
|
+
import { forgePrincipal, gateAdmissionFrom, identify, IdentityError, parseForgeIdentity, refuseUnidentified } from "./identity";
|
|
12
|
+
import { createClassRegistry, coreClassRegistry } from "./principal-classes";
|
|
13
|
+
import { reviewRecord } from "./records-write";
|
|
14
|
+
import { answerPoint } from "./decide";
|
|
15
|
+
import { gatesInLedger } from "./status-gates";
|
|
16
|
+
import { emptyPolicy, type TrustPolicy } from "./trust/policy";
|
|
17
|
+
import { sealGateApproval } from "./trust/seal";
|
|
18
|
+
import { hasSshKeygen, TestRepo, type Key } from "./trust/test-repo";
|
|
19
|
+
import { evaluateGate, memoryGateLedgerPort } from "../op/gate";
|
|
20
|
+
import type { GateResolutionRecord, PendingGateRecord } from "../lifecycle/gate-ledger";
|
|
21
|
+
|
|
22
|
+
const decl = (identity: unknown): Declaration =>
|
|
23
|
+
parseDeclaration(JSON.stringify({ name: "acme", schema: 1, members: [], identity }), "chant.workspace.json", "0.102.0");
|
|
24
|
+
|
|
25
|
+
const policyWith = (signers: { principal: string; key: string }[], roles: Record<string, string[]> = {}): TrustPolicy => ({
|
|
26
|
+
...emptyPolicy("0".repeat(40)),
|
|
27
|
+
active: true,
|
|
28
|
+
signers: signers.map((s, i) => ({ ...s, line: i + 1 })),
|
|
29
|
+
roles,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const repos: TestRepo[] = [];
|
|
33
|
+
afterEach(() => {
|
|
34
|
+
while (repos.length) repos.pop()!.cleanup();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe("forge identities", () => {
|
|
38
|
+
test("github and gitlab name their own host; forgejo and gitea name one", () => {
|
|
39
|
+
expect(parseForgeIdentity("github:Alice")).toEqual({ forge: "github", host: "github.com", login: "alice" });
|
|
40
|
+
expect(parseForgeIdentity("gitlab:a.b_c")).toEqual({ forge: "gitlab", host: "gitlab.com", login: "a.b_c" });
|
|
41
|
+
expect(parseForgeIdentity("forgejo@codeberg.org:bob")).toEqual({ forge: "forgejo", host: "codeberg.org", login: "bob" });
|
|
42
|
+
expect(parseForgeIdentity("github@ghe.example.com:carol")).toEqual({ forge: "github", host: "ghe.example.com", login: "carol" });
|
|
43
|
+
expect(parseForgeIdentity("forgejo:bob")).toBeNull();
|
|
44
|
+
expect(parseForgeIdentity("alice")).toBeNull();
|
|
45
|
+
expect(parseForgeIdentity("alice@example.com")).toBeNull();
|
|
46
|
+
expect(parseForgeIdentity("github:")).toBeNull();
|
|
47
|
+
expect(parseForgeIdentity("bitbucket:alice")).toBeNull();
|
|
48
|
+
expect(forgePrincipal({ forge: "github", host: "github.com", login: "alice" })).toBe("github:alice");
|
|
49
|
+
expect(forgePrincipal({ forge: "forgejo", host: "codeberg.org", login: "bob" })).toBe("forgejo@codeberg.org:bob");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("a principal is a signer, a forge identity, a role holder or a name", () => {
|
|
53
|
+
const policy = policyWith([{ principal: "github:alice", key: "ssh-ed25519 AAAA" }, { principal: "dana@example.test", key: "ssh-ed25519 BBBB" }], { runner: ["ci-bot"] });
|
|
54
|
+
expect(identify("GitHub:Alice", policy).form).toBe("signer");
|
|
55
|
+
expect(identify("dana@example.test", policy).form).toBe("signer");
|
|
56
|
+
expect(identify("github:bob", policy).form).toBe("forge");
|
|
57
|
+
expect(identify("ci-bot", policy).form).toBe("role");
|
|
58
|
+
expect(identify("alex", policy).form).toBe("name");
|
|
59
|
+
const d = parseDeclaration(
|
|
60
|
+
JSON.stringify({ name: "acme", schema: 1, members: [{ name: "app", dir: "app", kind: "chant" }], agents: [{ name: "builder", member: "app", principals: ["builder-bot"] }] }),
|
|
61
|
+
"chant.workspace.json",
|
|
62
|
+
"0.102.0",
|
|
63
|
+
);
|
|
64
|
+
expect(identify("builder-bot", emptyPolicy(null), d).form).toBe("role");
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe("identity.attribution", () => {
|
|
69
|
+
test("parses, and defaults to any", () => {
|
|
70
|
+
expect(decl({}).identity).toEqual({ attribution: "any", gates: {} });
|
|
71
|
+
expect(decl({ attribution: "identified", gates: { ship: { class: "operator" }, rollback: {} } }).identity).toEqual({
|
|
72
|
+
attribution: "identified",
|
|
73
|
+
gates: {
|
|
74
|
+
ship: { gate: "ship", class: "operator", pointer: "/identity/gates/ship" },
|
|
75
|
+
rollback: { gate: "rollback", class: null, pointer: "/identity/gates/rollback" },
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
expect(() => decl({ attribution: "verified" })).toThrow();
|
|
79
|
+
expect(() => decl({ gates: { ship: { by: "x" } } })).toThrow();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("identified refuses a bare name and takes a forge identity or a signer", () => {
|
|
83
|
+
const source = { declaration: decl({ attribution: "identified" }), policy: policyWith([{ principal: "dana@example.test", key: "ssh-ed25519 AAAA" }]) };
|
|
84
|
+
expect(() => refuseUnidentified(source, ["github:alice", "dana@example.test"], "--by")).not.toThrow();
|
|
85
|
+
expect(() => refuseUnidentified(source, ["github:alice", "alex"], "--by")).toThrow(IdentityError);
|
|
86
|
+
// any, or no identity block, keeps every name: hud's roster names still work there.
|
|
87
|
+
expect(() => refuseUnidentified({ ...source, declaration: decl({}) }, ["alex"], "--by")).not.toThrow();
|
|
88
|
+
expect(() => refuseUnidentified({ ...source, declaration: null }, ["alex"], "--by")).not.toThrow();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test.skipIf(!hasSshKeygen)("records review and points answer refuse a roster name under identified at base", async () => {
|
|
92
|
+
const r = new TestRepo("identity-review");
|
|
93
|
+
repos.push(r);
|
|
94
|
+
const DECISIONS = join(import.meta.dirname, "..", "..", "..", "..", "docs", "design", "decisions");
|
|
95
|
+
r.write("decisions/decision.kind.mjs", readFileSync(join(DECISIONS, "decision.kind.mjs"), "utf-8"));
|
|
96
|
+
r.write("decisions/decision.schema.json", readFileSync(join(DECISIONS, "decision.schema.json"), "utf-8"));
|
|
97
|
+
r.write("decisions/ws-003-seal-scope.md", readFileSync(join(DECISIONS, "ws-003-seal-scope.md"), "utf-8"));
|
|
98
|
+
r.write("chant.workspace.json", JSON.stringify({ name: "acme", schema: 1, members: [], identity: { attribution: "identified" } }));
|
|
99
|
+
r.commit("base");
|
|
100
|
+
r.git(["checkout", "-q", "-b", "change"]);
|
|
101
|
+
const kind = "decisions/decision.kind.mjs";
|
|
102
|
+
const refused = await reviewRecord({ kind, id: "ws-003", verdict: "agree", by: "alex", cwd: r.dir, on: "2026-10-03", dryRun: true });
|
|
103
|
+
expect("error" in refused && refused.error.code).toBe("principal-unidentified");
|
|
104
|
+
const taken = await reviewRecord({ kind, id: "ws-003", verdict: "agree", by: "github:alex", cwd: r.dir, on: "2026-10-03", dryRun: true });
|
|
105
|
+
expect("error" in taken ? taken.error : null).toBeNull();
|
|
106
|
+
const answer = await answerPoint({ id: "P-1", answer: "yes", by: ["alex"], cwd: r.dir });
|
|
107
|
+
expect("error" in answer && answer.error.code).toBe("principal-unidentified");
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe.skipIf(!hasSshKeygen)("identity.gates", () => {
|
|
112
|
+
function keys(): { r: TestRepo; alice: Key; bob: Key; mallory: Key } {
|
|
113
|
+
const r = new TestRepo("identity-gates");
|
|
114
|
+
repos.push(r);
|
|
115
|
+
return { r, alice: r.key("alice"), bob: r.key("bob"), mallory: r.key("mallory") };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const approval = (resolvedBy: string, extra: Partial<GateResolutionRecord> = {}): GateResolutionRecord => ({
|
|
119
|
+
version: 1,
|
|
120
|
+
op: "web",
|
|
121
|
+
gate: "ship",
|
|
122
|
+
environment: "prod",
|
|
123
|
+
planDigest: "sha256:" + "a".repeat(64),
|
|
124
|
+
resolvedBy,
|
|
125
|
+
timestamp: "2026-10-03T12:00:00.000Z",
|
|
126
|
+
...extra,
|
|
127
|
+
});
|
|
128
|
+
const signed = (key: Key, a: GateResolutionRecord): GateResolutionRecord => ({ ...a, seal: sealGateApproval(key.file, a) });
|
|
129
|
+
|
|
130
|
+
test("counts only an approval sealed by a listed signer in the class", () => {
|
|
131
|
+
const { alice, bob, mallory } = keys();
|
|
132
|
+
const policy = policyWith(
|
|
133
|
+
[
|
|
134
|
+
{ principal: "github:alice", key: alice.pub },
|
|
135
|
+
{ principal: "github:bob", key: bob.pub },
|
|
136
|
+
],
|
|
137
|
+
{ operator: ["github:alice"] },
|
|
138
|
+
);
|
|
139
|
+
const classes = createClassRegistry([{ name: "operator", description: "operators", role: "operator", source: "plugins/ops" }]);
|
|
140
|
+
const rule = gateAdmissionFrom({ declaration: decl({ gates: { ship: { class: "operator" } } }), policy, classes }, "ship")!;
|
|
141
|
+
expect(rule.requirement.class).toBe("operator");
|
|
142
|
+
expect(gateAdmissionFrom({ declaration: decl({ gates: { ship: {} } }), policy, classes }, "rollout")).toBeNull();
|
|
143
|
+
|
|
144
|
+
expect(rule.refuses(signed(alice, approval("github:alice")))).toBeNull();
|
|
145
|
+
// A hud roster name or an unsigned forge identity can't clear it.
|
|
146
|
+
expect(rule.refuses(approval("alex"))).toMatch(/not signed/);
|
|
147
|
+
expect(rule.refuses(approval("github:alice"))).toMatch(/not signed/);
|
|
148
|
+
// Signed by a listed signer outside the class.
|
|
149
|
+
expect(rule.refuses(signed(bob, approval("github:bob")))).toMatch(/not in the class operator/);
|
|
150
|
+
// Someone else's key under alice's name.
|
|
151
|
+
expect(rule.refuses(signed(mallory, approval("github:alice")))).toMatch(/does not verify/);
|
|
152
|
+
// A seal over another plan doesn't carry over.
|
|
153
|
+
const moved = { ...signed(alice, approval("github:alice")), planDigest: "sha256:" + "b".repeat(64) };
|
|
154
|
+
expect(rule.refuses(moved)).toMatch(/does not verify/);
|
|
155
|
+
|
|
156
|
+
// Any listed signer, when the rule names no class.
|
|
157
|
+
const anySigner = gateAdmissionFrom({ declaration: decl({ gates: { ship: {} } }), policy, classes }, "ship")!;
|
|
158
|
+
expect(anySigner.refuses(signed(bob, approval("github:bob")))).toBeNull();
|
|
159
|
+
// A class no pinned package supplies lets nothing through.
|
|
160
|
+
const unknown = gateAdmissionFrom({ declaration: decl({ gates: { ship: { class: "operator" } } }), policy, classes: coreClassRegistry() }, "ship")!;
|
|
161
|
+
expect(unknown.refuses(signed(alice, approval("github:alice")))).toMatch(/no pinned package supplies/);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test("a run and workspace status leave out approvals the rule refuses", async () => {
|
|
165
|
+
const { alice } = keys();
|
|
166
|
+
const policy = policyWith([{ principal: "github:alice", key: alice.pub }]);
|
|
167
|
+
const rule = gateAdmissionFrom({ declaration: decl({ gates: { ship: {} } }), policy, classes: coreClassRegistry() }, "ship")!;
|
|
168
|
+
const pending: PendingGateRecord = {
|
|
169
|
+
version: 1,
|
|
170
|
+
kind: "pending",
|
|
171
|
+
op: "web",
|
|
172
|
+
gate: "ship",
|
|
173
|
+
environment: "prod",
|
|
174
|
+
planDigest: "sha256:" + "a".repeat(64),
|
|
175
|
+
timestamp: "2026-10-03T11:00:00.000Z",
|
|
176
|
+
expiresAt: "2026-10-10T11:00:00.000Z",
|
|
177
|
+
};
|
|
178
|
+
const input = { op: "web", gate: "ship", environment: "prod", planDigest: pending.planDigest, now: "2026-10-03T13:00:00.000Z" };
|
|
179
|
+
|
|
180
|
+
const roster = memoryGateLedgerPort({ pending: [pending], resolutions: [approval("alex")] });
|
|
181
|
+
expect((await evaluateGate(roster, input)).satisfied).toBe(true);
|
|
182
|
+
expect((await evaluateGate({ ...roster, approvalRule: async () => rule }, input)).satisfied).toBe(false);
|
|
183
|
+
const sealed = memoryGateLedgerPort({ pending: [pending], resolutions: [approval("alex"), signed(alice, approval("github:alice"))] });
|
|
184
|
+
const check = await evaluateGate({ ...sealed, approvalRule: async () => rule }, input);
|
|
185
|
+
expect(check.satisfied && check.resolution.resolvedBy).toBe("github:alice");
|
|
186
|
+
|
|
187
|
+
const ledger = [pending, approval("alex")].map((l) => JSON.stringify(l)).join("\n");
|
|
188
|
+
const open = gatesInLedger("web", ledger, ["prod"], "2026-10-03T13:00:00.000Z", (g) => (g === "ship" ? rule : null)).gates[0];
|
|
189
|
+
expect(open.state).toBe("pending");
|
|
190
|
+
expect(open.signed).toEqual({ class: null });
|
|
191
|
+
expect(open.approve).toMatch(/--sign$/);
|
|
192
|
+
const plain = gatesInLedger("web", ledger, ["prod"], "2026-10-03T13:00:00.000Z").gates[0];
|
|
193
|
+
expect(plain.state).toBe("approved");
|
|
194
|
+
expect(plain.signed).toBeNull();
|
|
195
|
+
});
|
|
196
|
+
});
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Who a person-attributed record or gate approval names (#3163, ws-080).
|
|
3
|
+
*
|
|
4
|
+
* chant never authenticates a person (ws-052): hud, studio or behold signs a
|
|
5
|
+
* person in and maps the session to one of the identities below, then passes
|
|
6
|
+
* it to chant's write commands (`--by`, `--actor`). chant records what it is
|
|
7
|
+
* given and says which form it is:
|
|
8
|
+
*
|
|
9
|
+
* - `signer`: a principal `.chant/allowed_signers` lists at base. A seal made
|
|
10
|
+
* with that principal's key (`--sign`) attests it.
|
|
11
|
+
* - `forge`: a forge identity, `github:<login>`, `gitlab:<login>`, or
|
|
12
|
+
* `<forge>@<host>:<login>` (forgejo, gitea, or a self-hosted github or
|
|
13
|
+
* gitlab). The surface that signed the person in vouches for it; nothing
|
|
14
|
+
* here checks it, and only a seal verified against the signers at base
|
|
15
|
+
* attests it. The signers file and the role grants name people this way, so
|
|
16
|
+
* one string is the person in the record, the key list and the classes.
|
|
17
|
+
* - `role`: a principal holding the agent, runner or service role at base, or
|
|
18
|
+
* listed by a declared agent session: not a person, and named by a grant an
|
|
19
|
+
* admin controls.
|
|
20
|
+
* - `name`: anything else, such as a hud roster name or `$USER`.
|
|
21
|
+
*
|
|
22
|
+
* The declaration's `identity` block, read at base, turns this into rules:
|
|
23
|
+
* `attribution: "identified"` refuses a `name` on every person-attributed
|
|
24
|
+
* write, and `gates.<gate>` lets a gate of that name pass only on an approval
|
|
25
|
+
* sealed by a signer, from a class when it names one.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import type { Declaration, SignedGate } from "./declaration";
|
|
29
|
+
import { classesOf, coreClassRegistry, principalClass, type ClassRegistry } from "./principal-classes";
|
|
30
|
+
import type { ReasonCode } from "./reason-codes";
|
|
31
|
+
import { normalisePrincipal } from "./records";
|
|
32
|
+
import type { TrustPolicy } from "./trust/policy";
|
|
33
|
+
import { checkGateSeal, type SealedGateApproval } from "./trust/seal";
|
|
34
|
+
import { scopeSource } from "./write-scope";
|
|
35
|
+
|
|
36
|
+
/** The forges a forge identity can name. */
|
|
37
|
+
export const FORGES = ["github", "gitlab", "forgejo", "gitea"] as const;
|
|
38
|
+
export type Forge = (typeof FORGES)[number];
|
|
39
|
+
|
|
40
|
+
/** The host a forge identity means when it names none. Forgejo and gitea have no default: name the host. */
|
|
41
|
+
const DEFAULT_HOST: Record<Forge, string | null> = { github: "github.com", gitlab: "gitlab.com", forgejo: null, gitea: null };
|
|
42
|
+
|
|
43
|
+
/** A person's account on a forge. */
|
|
44
|
+
export interface ForgeIdentity {
|
|
45
|
+
forge: Forge;
|
|
46
|
+
/** Lower-cased, such as `github.com` or `codeberg.org`. */
|
|
47
|
+
host: string;
|
|
48
|
+
/** Lower-cased: the forges compare logins without case. */
|
|
49
|
+
login: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const LABEL = "[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?";
|
|
53
|
+
const FORGE_PRINCIPAL = new RegExp(`^(${FORGES.join("|")})(?:@(${LABEL}(?:\\.${LABEL})+))?:([a-z0-9](?:[a-z0-9._-]{0,98}[a-z0-9_])?)$`);
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Read `principal` as a forge identity, or null when it is not one. Case and
|
|
57
|
+
* surrounding space are folded as {@link normalisePrincipal} folds them.
|
|
58
|
+
*/
|
|
59
|
+
export function parseForgeIdentity(principal: string): ForgeIdentity | null {
|
|
60
|
+
const m = FORGE_PRINCIPAL.exec(normalisePrincipal(principal));
|
|
61
|
+
if (!m) return null;
|
|
62
|
+
const forge = m[1] as Forge;
|
|
63
|
+
const host = m[2] ?? DEFAULT_HOST[forge];
|
|
64
|
+
if (host === null) return null;
|
|
65
|
+
return { forge, host, login: m[3] };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** The principal string of a forge identity: `<forge>:<login>` on the forge's default host, else `<forge>@<host>:<login>`. */
|
|
69
|
+
export function forgePrincipal(id: ForgeIdentity): string {
|
|
70
|
+
return id.host === DEFAULT_HOST[id.forge] ? `${id.forge}:${id.login}` : `${id.forge}@${id.host}:${id.login}`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export type IdentityForm = "signer" | "forge" | "role" | "name";
|
|
74
|
+
|
|
75
|
+
export interface Identified {
|
|
76
|
+
principal: string;
|
|
77
|
+
form: IdentityForm;
|
|
78
|
+
/** Set whenever the principal reads as a forge identity, whatever its form. */
|
|
79
|
+
forge: ForgeIdentity | null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const NON_PERSON_CLASSES = new Set(["agent", "runner", "service"]);
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* What `principal` is, under the policy and declaration read at base. A
|
|
86
|
+
* principal the signers file lists is a `signer` even when it is also a
|
|
87
|
+
* forge identity, since a seal can attest it.
|
|
88
|
+
*/
|
|
89
|
+
export function identify(principal: string, policy: TrustPolicy, declaration: Declaration | null = null): Identified {
|
|
90
|
+
const name = normalisePrincipal(principal);
|
|
91
|
+
const forge = parseForgeIdentity(principal);
|
|
92
|
+
if (policy.active && policy.signers.some((s) => normalisePrincipal(s.principal) === name)) return { principal, form: "signer", forge };
|
|
93
|
+
if (forge) return { principal, form: "forge", forge };
|
|
94
|
+
const session = (declaration?.agents ?? []).some((a) => a.principals.some((p) => normalisePrincipal(p) === name));
|
|
95
|
+
if (session || classesOf(coreClassRegistry(), policy, principal).some((c) => NON_PERSON_CLASSES.has(c))) return { principal, form: "role", forge: null };
|
|
96
|
+
return { principal, form: "name", forge: null };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Why a write naming a person was refused. Closed. */
|
|
100
|
+
export const IDENTITY_CODES = ["principal-unidentified"] as const satisfies readonly ReasonCode[];
|
|
101
|
+
export type IdentityCode = (typeof IDENTITY_CODES)[number];
|
|
102
|
+
|
|
103
|
+
export class IdentityError extends Error {
|
|
104
|
+
readonly code: IdentityCode = "principal-unidentified";
|
|
105
|
+
constructor(message: string) {
|
|
106
|
+
super(message);
|
|
107
|
+
this.name = "IdentityError";
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** What {@link refuseUnidentified} reads: the declaration and policy at base, as `scopeSource` gives them. */
|
|
112
|
+
export interface IdentitySource {
|
|
113
|
+
declaration: Declaration | null;
|
|
114
|
+
policy: TrustPolicy;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Refuse a person-attributed write that names someone by a bare name, when
|
|
119
|
+
* the declaration at base sets `identity.attribution` to `identified`.
|
|
120
|
+
* `flag` names where the name came from (`--by`, `--actor`). A write in a
|
|
121
|
+
* declared agent session (`CHANT_AGENT`) names the session, not a person,
|
|
122
|
+
* and is not judged here. Throws an {@link IdentityError}.
|
|
123
|
+
*/
|
|
124
|
+
export function refuseUnidentified(source: IdentitySource, names: readonly string[], flag: string, opts: { agent?: string | null } = {}): void {
|
|
125
|
+
if (source.declaration?.identity?.attribution !== "identified") return;
|
|
126
|
+
if (opts.agent !== undefined && opts.agent !== null && source.declaration.agents.some((a) => a.name === opts.agent)) return;
|
|
127
|
+
for (const name of names) {
|
|
128
|
+
if (identify(name, source.policy, source.declaration).form !== "name") continue;
|
|
129
|
+
throw new IdentityError(
|
|
130
|
+
`${flag} ${JSON.stringify(name)} names no identity the workspace can check: chant.workspace.json at base sets identity.attribution to identified, so a person is named by a forge identity (github:<login>, gitlab:<login>, or <forge>@<host>:<login>) or by a principal ${source.policy.signersPath} lists at base`,
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** A gate approval as the rule judges it: the resolution line's own fields. */
|
|
136
|
+
export type GateApprovalLike = SealedGateApproval;
|
|
137
|
+
|
|
138
|
+
/** A gate the declaration names in `identity.gates`, and the judge for its approvals. */
|
|
139
|
+
export interface GateAdmission {
|
|
140
|
+
requirement: SignedGate;
|
|
141
|
+
/** Why `approval` doesn't count toward the gate, or null when it does. */
|
|
142
|
+
refuses(approval: GateApprovalLike): string | null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** What {@link gateAdmissionFrom} reads: the declaration, policy and classes at base, as `scopeSource` gives them. */
|
|
146
|
+
export interface GateRuleSource extends IdentitySource {
|
|
147
|
+
classes: ClassRegistry;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* The rule for `gate`, or null when the declaration at base names none. An
|
|
152
|
+
* approval counts only when its seal verifies for its approver against the
|
|
153
|
+
* signers at base and, when the rule names a class, the approver's role
|
|
154
|
+
* grants at base put it in that class. A class no pinned package supplies
|
|
155
|
+
* lets nothing through.
|
|
156
|
+
*/
|
|
157
|
+
export function gateAdmissionFrom(source: GateRuleSource, gate: string): GateAdmission | null {
|
|
158
|
+
const requirement = source.declaration?.identity?.gates[gate];
|
|
159
|
+
if (!requirement) return null;
|
|
160
|
+
const { policy, classes } = source;
|
|
161
|
+
return {
|
|
162
|
+
requirement,
|
|
163
|
+
refuses(a) {
|
|
164
|
+
const want = requirement.class;
|
|
165
|
+
if (want !== null && classes.get(want) === undefined) {
|
|
166
|
+
return `identity.gates.${gate} names the class ${want}, which no pinned package supplies, so no approval can show it is in that class (known classes: ${classes.names().join(", ")})`;
|
|
167
|
+
}
|
|
168
|
+
if (!policy.active) return `gate ${gate} needs a signed approval, and there is no signers file (${policy.signersPath}) at base to check one against`;
|
|
169
|
+
const seal = checkGateSeal(policy, a);
|
|
170
|
+
if (seal.attested !== true) return a.seal === undefined || a.seal === null ? `the approval by ${a.resolvedBy} is not signed; gate ${gate} needs chant approve --sign` : seal.message;
|
|
171
|
+
if (want === null) return null;
|
|
172
|
+
const inClass = want === "human" ? principalClass(policy, a.resolvedBy, classes) === "human" : classesOf(classes, policy, a.resolvedBy).includes(want);
|
|
173
|
+
return inClass ? null : `${a.resolvedBy} is not in the class ${want} that gate ${gate} needs: no role grant at base puts them there`;
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* The rule for `gate` in the workspace holding `cwd`, read at base (the
|
|
180
|
+
* target branch tip), or null when there is no declaration or it names none.
|
|
181
|
+
* What a run, `chant approve` and `workspace status` ask before counting an
|
|
182
|
+
* approval.
|
|
183
|
+
*/
|
|
184
|
+
export function gateAdmission(cwd: string, gate: string): GateAdmission | null {
|
|
185
|
+
return gateAdmissionFrom(scopeSource(cwd), gate);
|
|
186
|
+
}
|