@intentius/chant 0.124.30 → 0.124.32
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/bin/chant +7 -2
- package/dist/cli/commands/check-lexicon-plugin.d.ts +7 -4
- package/dist/cli/commands/check-lexicon-plugin.d.ts.map +1 -1
- package/dist/cli/commands/check-lexicon-plugin.js +10 -7
- package/dist/cli/commands/check-lexicon.d.ts.map +1 -1
- package/dist/cli/commands/check-lexicon.js +18 -12
- package/dist/cli/commands/import.d.ts +10 -0
- package/dist/cli/commands/import.d.ts.map +1 -1
- package/dist/cli/commands/import.js +37 -0
- package/dist/cli/commands/init-lexicon/templates/project.d.ts.map +1 -1
- package/dist/cli/commands/init-lexicon/templates/project.js +4 -3
- package/dist/cli/commands/onboard.d.ts.map +1 -1
- package/dist/cli/commands/onboard.js +8 -4
- package/dist/cli/commands/update.d.ts.map +1 -1
- package/dist/cli/commands/update.js +2 -1
- package/dist/cli/dependencies.d.ts +18 -0
- package/dist/cli/dependencies.d.ts.map +1 -0
- package/dist/cli/dependencies.js +120 -0
- package/dist/cli/handlers/build.d.ts.map +1 -1
- package/dist/cli/handlers/build.js +23 -4
- package/dist/cli/handlers/fan-out.d.ts.map +1 -1
- package/dist/cli/handlers/fan-out.js +12 -4
- package/dist/cli/handlers/graph.d.ts.map +1 -1
- package/dist/cli/handlers/graph.js +16 -6
- package/dist/cli/handlers/lint.d.ts.map +1 -1
- package/dist/cli/handlers/lint.js +6 -0
- package/dist/cli/handlers/operator.d.ts +2 -2
- package/dist/cli/handlers/operator.js +3 -3
- package/dist/cli/handlers/pr.d.ts +14 -0
- package/dist/cli/handlers/pr.d.ts.map +1 -1
- package/dist/cli/handlers/pr.js +488 -32
- package/dist/cli/handlers/promote.d.ts.map +1 -1
- package/dist/cli/handlers/promote.js +3 -1
- package/dist/cli/handlers/run.d.ts +13 -1
- package/dist/cli/handlers/run.d.ts.map +1 -1
- package/dist/cli/handlers/run.js +38 -15
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/main.js +74 -12
- package/dist/cli/parse-args.d.ts.map +1 -1
- package/dist/cli/parse-args.js +65 -0
- package/dist/cli/registry.d.ts +29 -2
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/cli/registry.js +6 -2
- package/dist/codegen/generate-post-synth-barrel.js +2 -2
- package/dist/codegen/lexicon-regen.d.ts +4 -0
- package/dist/codegen/lexicon-regen.d.ts.map +1 -1
- package/dist/codegen/lexicon-regen.js +1 -1
- package/dist/codegen/pinned-upgrade.js +2 -2
- package/dist/components/capability.d.ts +44 -3
- package/dist/components/capability.d.ts.map +1 -1
- package/dist/components/capability.js +21 -0
- package/dist/components/changed-files.d.ts +7 -1
- package/dist/components/changed-files.d.ts.map +1 -1
- package/dist/components/changed-files.js +9 -1
- package/dist/components/driver.d.ts +38 -0
- package/dist/components/driver.d.ts.map +1 -1
- package/dist/components/driver.js +47 -6
- package/dist/components/fan-out-record.d.ts +6 -1
- package/dist/components/fan-out-record.d.ts.map +1 -1
- package/dist/components/fan-out-record.js +24 -0
- package/dist/components/fan-out-run.d.ts +20 -0
- package/dist/components/fan-out-run.d.ts.map +1 -1
- package/dist/components/fan-out-run.js +21 -1
- package/dist/components/pr-pipeline.d.ts +65 -2
- package/dist/components/pr-pipeline.d.ts.map +1 -1
- package/dist/components/pr-pipeline.js +107 -15
- package/dist/components/pr-run.d.ts +127 -29
- package/dist/components/pr-run.d.ts.map +1 -1
- package/dist/components/pr-run.js +290 -70
- package/dist/components/wave-plan.d.ts +5 -2
- package/dist/components/wave-plan.d.ts.map +1 -1
- package/dist/components/wave-plan.js +12 -1
- package/dist/discovery/fold-import.d.ts.map +1 -1
- package/dist/discovery/fold-import.js +16 -6
- package/dist/discovery/sandbox/bundle.d.ts.map +1 -1
- package/dist/discovery/sandbox/bundle.js +6 -0
- package/dist/discovery/specifier-resolve.d.ts.map +1 -1
- package/dist/discovery/specifier-resolve.js +8 -2
- package/dist/fold/fold.d.ts.map +1 -1
- package/dist/fold/fold.js +68 -1
- package/dist/fold/subset.d.ts +37 -0
- package/dist/fold/subset.d.ts.map +1 -1
- package/dist/fold/subset.js +88 -0
- package/dist/lexicon-module.d.ts +10 -0
- package/dist/lexicon-module.d.ts.map +1 -1
- package/dist/lexicon-module.js +53 -8
- package/dist/lexicon-plugin-helpers.d.ts.map +1 -1
- package/dist/lexicon-plugin-helpers.js +6 -4
- package/dist/lexicon.d.ts +2 -0
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lifecycle/affected.d.ts.map +1 -1
- package/dist/lifecycle/affected.js +7 -1
- package/dist/lifecycle/release-ledger.d.ts +21 -0
- package/dist/lifecycle/release-ledger.d.ts.map +1 -1
- package/dist/lifecycle/release-ledger.js +24 -0
- package/dist/lifecycle/work-dir.d.ts +45 -0
- package/dist/lifecycle/work-dir.d.ts.map +1 -0
- package/dist/lifecycle/work-dir.js +78 -0
- package/dist/lint/discover.d.ts +17 -9
- package/dist/lint/discover.d.ts.map +1 -1
- package/dist/lint/discover.js +62 -54
- package/dist/module-source.d.ts +57 -0
- package/dist/module-source.d.ts.map +1 -0
- package/dist/module-source.js +104 -0
- package/dist/op/activities/factory.js +1 -1
- package/dist/op/discover.d.ts.map +1 -1
- package/dist/op/discover.js +13 -3
- package/dist/op/gate.d.ts.map +1 -1
- package/dist/op/gate.js +3 -0
- package/dist/op/local-executor.d.ts +12 -0
- package/dist/op/local-executor.d.ts.map +1 -1
- package/dist/op/local-executor.js +22 -3
- package/dist/op/op-waves-run.d.ts.map +1 -1
- package/dist/op/op-waves-run.js +20 -5
- package/dist/op/operator.d.ts +2 -2
- package/dist/op/operator.js +2 -2
- package/dist/op/redact.d.ts +34 -0
- package/dist/op/redact.d.ts.map +1 -0
- package/dist/op/redact.js +94 -0
- package/dist/op/step-console.d.ts +2 -0
- package/dist/op/step-console.d.ts.map +1 -1
- package/dist/op/step-console.js +5 -2
- package/dist/op/steward.d.ts +6 -4
- package/dist/op/steward.d.ts.map +1 -1
- package/dist/op/steward.js +5 -3
- package/dist/op/work-lease-run.d.ts +20 -7
- package/dist/op/work-lease-run.d.ts.map +1 -1
- package/dist/op/work-lease-run.js +56 -12
- package/dist/pr-forge.d.ts +27 -0
- package/dist/pr-forge.d.ts.map +1 -1
- package/dist/pr-forge.js +51 -6
- package/dist/pr-loop.d.ts +141 -4
- package/dist/pr-loop.d.ts.map +1 -1
- package/dist/pr-loop.js +122 -6
- package/dist/pr-report-url.d.ts +10 -2
- package/dist/pr-report-url.d.ts.map +1 -1
- package/dist/pr-report-url.js +52 -6
- package/dist/ts-loader.d.ts +38 -4
- package/dist/ts-loader.d.ts.map +1 -1
- package/dist/ts-loader.js +190 -16
- package/dist/wave-runtime.mjs +208 -208
- package/dist/workspace/chant-migrations/fountain-retired-kinds.d.ts +34 -0
- package/dist/workspace/chant-migrations/fountain-retired-kinds.d.ts.map +1 -0
- package/dist/workspace/chant-migrations/fountain-retired-kinds.js +176 -0
- package/dist/workspace/chant-migrations.d.ts.map +1 -1
- package/dist/workspace/chant-migrations.js +2 -0
- package/dist/workspace/checks/boxes.d.ts +6 -6
- package/dist/workspace/checks/boxes.js +9 -9
- package/dist/workspace/ci-green-forge.d.ts +6 -4
- package/dist/workspace/ci-green-forge.d.ts.map +1 -1
- package/dist/workspace/ci-green.d.ts +28 -12
- package/dist/workspace/ci-green.d.ts.map +1 -1
- package/dist/workspace/ci-green.js +49 -21
- package/dist/workspace/ci-list.schema.json +12 -6
- package/dist/workspace/ci-report-url.d.ts +18 -0
- package/dist/workspace/ci-report-url.d.ts.map +1 -0
- package/dist/workspace/ci-report-url.js +33 -0
- package/dist/workspace/ci-run-index.d.ts +108 -0
- package/dist/workspace/ci-run-index.d.ts.map +1 -0
- package/dist/workspace/ci-run-index.js +164 -0
- package/dist/workspace/ci-run-index.schema.json +52 -0
- package/dist/workspace/ci-run.d.ts +159 -9
- package/dist/workspace/ci-run.d.ts.map +1 -1
- package/dist/workspace/ci-run.js +419 -50
- package/dist/workspace/ci-runner-key.d.ts +133 -0
- package/dist/workspace/ci-runner-key.d.ts.map +1 -0
- package/dist/workspace/ci-runner-key.js +237 -0
- package/dist/workspace/ci-secrets.d.ts +65 -0
- package/dist/workspace/ci-secrets.d.ts.map +1 -0
- package/dist/workspace/ci-secrets.js +211 -0
- package/dist/workspace/ci-serve.d.ts +67 -4
- package/dist/workspace/ci-serve.d.ts.map +1 -1
- package/dist/workspace/ci-serve.js +240 -35
- package/dist/workspace/declaration.d.ts +8 -0
- package/dist/workspace/declaration.d.ts.map +1 -1
- package/dist/workspace/declaration.js +11 -2
- package/dist/workspace/declaration.schema.json +7 -2
- package/dist/workspace/export.d.ts +1 -1
- package/dist/workspace/export.d.ts.map +1 -1
- package/dist/workspace/export.js +7 -3
- package/dist/workspace/ls.d.ts +6 -1
- package/dist/workspace/ls.d.ts.map +1 -1
- package/dist/workspace/ls.js +1 -0
- package/dist/workspace/ls.schema.json +7 -1
- package/dist/workspace/pin-integrity.d.ts.map +1 -1
- package/dist/workspace/pin-integrity.js +3 -2
- package/dist/workspace/pr-report.schema.json +74 -7
- package/dist/workspace/reason-codes.d.ts +7 -3
- package/dist/workspace/reason-codes.d.ts.map +1 -1
- package/dist/workspace/reason-codes.js +9 -4
- package/dist/workspace/run-event.schema.json +3 -1
- package/dist/workspace/status.schema.json +3 -1
- package/dist/workspace/trust/trust-write.d.ts +109 -0
- package/dist/workspace/trust/trust-write.d.ts.map +1 -0
- package/dist/workspace/trust/trust-write.js +320 -0
- package/dist/workspace/trust-write.schema.json +265 -0
- package/dist/workspace/write-scope.d.ts +9 -0
- package/dist/workspace/write-scope.d.ts.map +1 -1
- package/dist/workspace/write-scope.js +11 -1
- package/package.json +1 -1
- package/src/__fixtures__/pr-loop/pr-apply-wave-waiting.note.golden.md +48 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/package.json +3 -3
- package/src/cli/commands/audit.test.ts +2 -2
- package/src/cli/commands/check-lexicon-plugin.test.ts +9 -0
- package/src/cli/commands/check-lexicon-plugin.ts +11 -8
- package/src/cli/commands/check-lexicon.test.ts +1 -1
- package/src/cli/commands/check-lexicon.ts +16 -11
- package/src/cli/commands/import.ts +40 -0
- package/src/cli/commands/init-lexicon/templates/project.ts +4 -3
- package/src/cli/commands/init-lexicon.test.ts +8 -5
- package/src/cli/commands/onboard.test.ts +45 -0
- package/src/cli/commands/onboard.ts +8 -4
- package/src/cli/commands/update.test.ts +6 -0
- package/src/cli/commands/update.ts +4 -1
- package/src/cli/compiled-cli.e2e.test.ts +76 -11
- package/src/cli/dependencies.e2e.test.ts +101 -0
- package/src/cli/dependencies.test.ts +93 -0
- package/src/cli/dependencies.ts +122 -0
- package/src/cli/handlers/build.test.ts +69 -0
- package/src/cli/handlers/build.ts +20 -4
- package/src/cli/handlers/fan-out.test.ts +73 -0
- package/src/cli/handlers/fan-out.ts +11 -4
- package/src/cli/handlers/graph.test.ts +76 -0
- package/src/cli/handlers/graph.ts +19 -12
- package/src/cli/handlers/lint.test.ts +42 -0
- package/src/cli/handlers/lint.ts +6 -0
- package/src/cli/handlers/operator.ts +3 -3
- package/src/cli/handlers/pr-waves.test.ts +449 -0
- package/src/cli/handlers/pr.test.ts +134 -0
- package/src/cli/handlers/pr.ts +523 -31
- package/src/cli/handlers/promote.test.ts +26 -1
- package/src/cli/handlers/promote.ts +3 -2
- package/src/cli/handlers/run-components-exit.test.ts +99 -0
- package/src/cli/handlers/run-signer.test.ts +79 -0
- package/src/cli/handlers/run.ts +34 -16
- package/src/cli/main.test.ts +19 -0
- package/src/cli/main.ts +73 -12
- package/src/cli/parse-args.ts +48 -0
- package/src/cli/registry.ts +35 -4
- package/src/codegen/generate-post-synth-barrel.ts +2 -2
- package/src/codegen/lexicon-regen.ts +1 -1
- package/src/codegen/pinned-upgrade.ts +2 -2
- package/src/codegen/post-synth-barrel-guards.test.ts +6 -11
- package/src/components/capability.ts +51 -3
- package/src/components/changed-files.test.ts +16 -1
- package/src/components/changed-files.ts +9 -1
- package/src/components/driver.test.ts +36 -1
- package/src/components/driver.ts +76 -4
- package/src/components/fan-out-record.ts +29 -1
- package/src/components/fan-out-run.test.ts +35 -0
- package/src/components/fan-out-run.ts +40 -1
- package/src/components/pr-pipeline.test.ts +100 -0
- package/src/components/pr-pipeline.ts +132 -17
- package/src/components/pr-run.test.ts +323 -18
- package/src/components/pr-run.ts +354 -75
- package/src/components/wave-plan.ts +16 -4
- package/src/discovery/fold-host-method.test.ts +87 -0
- package/src/discovery/fold-import.ts +15 -5
- package/src/discovery/fold-package-composite.test.ts +27 -0
- package/src/discovery/fold-specifiers.test.ts +8 -0
- package/src/discovery/sandbox/bundle.ts +6 -0
- package/src/discovery/specifier-resolve.ts +8 -2
- package/src/fold/fold.test.ts +72 -0
- package/src/fold/fold.ts +71 -0
- package/src/fold/subset.test.ts +31 -0
- package/src/fold/subset.ts +99 -0
- package/src/lexicon-dist.e2e.test.ts +254 -0
- package/src/lexicon-module.test.ts +47 -0
- package/src/lexicon-module.ts +49 -7
- package/src/lexicon-plugin-helpers.ts +6 -4
- package/src/lexicon.ts +2 -0
- package/src/lifecycle/affected.test.ts +7 -0
- package/src/lifecycle/affected.ts +6 -1
- package/src/lifecycle/release-ledger.ts +34 -0
- package/src/lifecycle/work-dir.test.ts +77 -0
- package/src/lifecycle/work-dir.ts +82 -0
- package/src/lint/discover.test.ts +46 -2
- package/src/lint/discover.ts +55 -49
- package/src/module-source.test.ts +85 -0
- package/src/module-source.ts +112 -0
- package/src/op/activities/factory.ts +1 -1
- package/src/op/discover-root.test.ts +31 -2
- package/src/op/discover.ts +14 -4
- package/src/op/gate.test.ts +30 -0
- package/src/op/gate.ts +3 -0
- package/src/op/local-executor.ts +31 -3
- package/src/op/op-waves-run.ts +20 -5
- package/src/op/op-waves.test.ts +49 -0
- package/src/op/operator.ts +2 -2
- package/src/op/redact.ts +93 -0
- package/src/op/step-console.ts +7 -2
- package/src/op/steward.ts +6 -4
- package/src/op/work-lease-run.test.ts +88 -2
- package/src/op/work-lease-run.ts +52 -12
- package/src/pr-forge-bundle.test.ts +2 -2
- package/src/pr-forge.test.ts +71 -0
- package/src/pr-forge.ts +70 -6
- package/src/pr-loop-bundle.test.ts +2 -1
- package/src/pr-loop.test.ts +117 -1
- package/src/pr-loop.ts +252 -10
- package/src/pr-report-url.ts +58 -6
- package/src/terraform/__fixtures__/advise.test.ts +21 -2
- package/src/ts-loader.test.ts +89 -1
- package/src/ts-loader.ts +180 -13
- package/src/workspace/changes.test.ts +1 -1
- package/src/workspace/chant-migrations/fountain-retired-kinds.test.ts +33 -0
- package/src/workspace/chant-migrations/fountain-retired-kinds.ts +190 -0
- package/src/workspace/chant-migrations.e2e.test.ts +5 -0
- package/src/workspace/chant-migrations.ts +2 -0
- package/src/workspace/checks/boxes.ts +9 -9
- package/src/workspace/ci-green-forge.ts +6 -4
- package/src/workspace/ci-green-runs.test.ts +53 -11
- package/src/workspace/ci-green.test.ts +1 -1
- package/src/workspace/ci-green.ts +50 -22
- package/src/workspace/ci-list.schema.json +12 -6
- package/src/workspace/ci-report-url.test.ts +220 -0
- package/src/workspace/ci-report-url.ts +33 -0
- package/src/workspace/ci-run-index.schema.json +52 -0
- package/src/workspace/ci-run-index.test.ts +392 -0
- package/src/workspace/ci-run-index.ts +206 -0
- package/src/workspace/ci-run.test.ts +520 -42
- package/src/workspace/ci-run.ts +483 -51
- package/src/workspace/ci-runner-key.test.ts +204 -0
- package/src/workspace/ci-runner-key.ts +275 -0
- package/src/workspace/ci-secrets.test.ts +106 -0
- package/src/workspace/ci-secrets.ts +213 -0
- package/src/workspace/ci-serve.test.ts +282 -39
- package/src/workspace/ci-serve.ts +231 -34
- package/src/workspace/declaration.schema.json +7 -2
- package/src/workspace/declaration.test.ts +23 -0
- package/src/workspace/declaration.ts +19 -3
- package/src/workspace/export.ts +6 -3
- package/src/workspace/ls.schema.json +7 -1
- package/src/workspace/ls.ts +7 -2
- package/src/workspace/pin-integrity.test.ts +5 -0
- package/src/workspace/pin-integrity.ts +3 -2
- package/src/workspace/pr-report.schema.json +74 -7
- package/src/workspace/reason-codes.test.ts +2 -0
- package/src/workspace/reason-codes.ts +9 -4
- package/src/workspace/run-event.schema.json +3 -1
- package/src/workspace/status.schema.json +3 -1
- package/src/workspace/trust/trust-write.test.ts +179 -0
- package/src/workspace/trust/trust-write.ts +377 -0
- package/src/workspace/trust-write.schema.json +265 -0
- package/src/workspace/write-scope.test.ts +29 -1
- package/src/workspace/write-scope.ts +12 -1
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `chant workspace trust runner add <principal> --key <key>` and `chant
|
|
3
|
+
* workspace trust runner remove <principal>` (#3510): a runner entry of
|
|
4
|
+
* `.chant/trust.json`, written through chant.
|
|
5
|
+
*
|
|
6
|
+
* ws-074 lets a tool write the repo only through chant. A lobby that signs
|
|
7
|
+
* agent-run statements, a CI job that signs Op runs and ci tags, and a host
|
|
8
|
+
* running `chant ci serve` each need their key under `runners`, and before
|
|
9
|
+
* this write a box repo added that entry by hand. `add` lists the key for the
|
|
10
|
+
* principal and, for class `runner`, grants the principal the `runner` role
|
|
11
|
+
* that `ci tick`, `ci run` and `chant run --key` require. `remove` takes both
|
|
12
|
+
* away. The file is edited in place, so its formatting and key order stay as
|
|
13
|
+
* they were.
|
|
14
|
+
*
|
|
15
|
+
* The write is a working-tree write like `member add`: it holds the write
|
|
16
|
+
* lock (#3173, ws-089), is judged by the write scope at base, notes who wrote
|
|
17
|
+
* the file in the write journal, and never commits. Changing the trust
|
|
18
|
+
* config is a protected write, so the commit that carries it is checked by
|
|
19
|
+
* `chant workspace verify` against the policy at base. `--expect <digest>`
|
|
20
|
+
* refuses the write when the file has changed since the caller read it: the
|
|
21
|
+
* digest is the SHA-256 of the file's bytes, and of empty text when there is
|
|
22
|
+
* no file. Each write prints one JSON document (`trust-write.schema.json`)
|
|
23
|
+
* with the digest after the write, to pass as `--expect` to the next one.
|
|
24
|
+
*/
|
|
25
|
+
import type { CommandContext } from "../../cli/registry.js";
|
|
26
|
+
import { type LastWrite } from "../write-lock.js";
|
|
27
|
+
import { type RunnerKey } from "./policy.js";
|
|
28
|
+
/** The version of the document these writes print. */
|
|
29
|
+
export declare const TRUST_WRITE_CONTRACT_VERSION = 1;
|
|
30
|
+
/** `$id` of the JSON Schema for what the trust writes print, shipped in `workspace/`. */
|
|
31
|
+
export declare const TRUST_WRITE_SCHEMA_ID = "https://intentius.io/chant/schemas/workspace/trust-write/v1/trust-write.schema.json";
|
|
32
|
+
export declare const TRUST_WRITE_ACTIONS: readonly ["runner add", "runner remove"];
|
|
33
|
+
export type TrustWriteAction = (typeof TRUST_WRITE_ACTIONS)[number];
|
|
34
|
+
/** Why a trust write wrote nothing. Closed. */
|
|
35
|
+
export declare const TRUST_WRITE_ERROR_CODES: readonly ["not-a-git-repository", "write-usage-invalid", "trust-config-invalid", "trust-conflict", "runner-key-invalid", "runner-key-is-signer", "runner-exists", "runner-unknown", "write-scope-member", "write-scope-protected", "write-scope-class-unknown", "agent-unknown", "principal-unidentified", "write-lock-timeout", "write-lock-not-held"];
|
|
36
|
+
export type TrustWriteErrorCode = (typeof TRUST_WRITE_ERROR_CODES)[number];
|
|
37
|
+
type Head = {
|
|
38
|
+
$schema: string;
|
|
39
|
+
contract: number;
|
|
40
|
+
chant: string;
|
|
41
|
+
action: TrustWriteAction;
|
|
42
|
+
name: string | null;
|
|
43
|
+
};
|
|
44
|
+
/** What `--expect` disagreed with: the digest the file has now, and its last chant write when the journal knows it. */
|
|
45
|
+
export interface TrustConflict {
|
|
46
|
+
path: string;
|
|
47
|
+
expected: string;
|
|
48
|
+
digest: string;
|
|
49
|
+
lastWrite: LastWrite | null;
|
|
50
|
+
}
|
|
51
|
+
/** What `trust runner add` and `trust runner remove` print. */
|
|
52
|
+
export type TrustWriteDocument = (Head & {
|
|
53
|
+
name: string;
|
|
54
|
+
/** `.chant/trust.json`, from the repository root. */
|
|
55
|
+
path: string;
|
|
56
|
+
/** The SHA-256 of the file's bytes after the write (or as they would be, with dryRun): the next write's `--expect`. */
|
|
57
|
+
digest: string;
|
|
58
|
+
/** The SHA-256 of the file's bytes before the write; of empty text when there was no file. */
|
|
59
|
+
previousDigest: string;
|
|
60
|
+
/** The file, when the write changed it; else empty. */
|
|
61
|
+
paths: string[];
|
|
62
|
+
changed: boolean;
|
|
63
|
+
dryRun: boolean;
|
|
64
|
+
/** The runner entry before the write, or null when there was none. */
|
|
65
|
+
previous: RunnerKey | null;
|
|
66
|
+
/** The runner entry after the write, or null after a remove. */
|
|
67
|
+
entry: RunnerKey | null;
|
|
68
|
+
/** Whether the principal holds the `runner` role after the write. */
|
|
69
|
+
runnerRole: boolean;
|
|
70
|
+
}) | (Head & {
|
|
71
|
+
error: {
|
|
72
|
+
code: TrustWriteErrorCode;
|
|
73
|
+
message: string;
|
|
74
|
+
};
|
|
75
|
+
conflict?: TrustConflict;
|
|
76
|
+
});
|
|
77
|
+
export interface TrustWriteRequest {
|
|
78
|
+
cwd: string;
|
|
79
|
+
action: TrustWriteAction;
|
|
80
|
+
/** The runner's principal. */
|
|
81
|
+
name: string;
|
|
82
|
+
/** For `runner add`: the public key, as an `ssh-ed25519` line or a file holding one or an Ed25519 PEM. */
|
|
83
|
+
key?: string;
|
|
84
|
+
/** For `runner add`: `runner` (the default) or `service`. */
|
|
85
|
+
runnerClass?: string;
|
|
86
|
+
/** Who is writing (`--by`). */
|
|
87
|
+
by?: string;
|
|
88
|
+
/** The agent session (`CHANT_AGENT`). */
|
|
89
|
+
agent?: string;
|
|
90
|
+
/** The file's digest the caller last read (`--expect`). */
|
|
91
|
+
expect?: string;
|
|
92
|
+
dryRun?: boolean;
|
|
93
|
+
}
|
|
94
|
+
/** The digest `--expect` compares: the SHA-256 of the file's text, of empty text when there is none. */
|
|
95
|
+
export declare function trustDigest(text: string | null): string;
|
|
96
|
+
/**
|
|
97
|
+
* The `ssh-ed25519 <base64>` line, with no comment, for what `--key` gives:
|
|
98
|
+
* the line itself (a comment after it is dropped), or a file holding such a
|
|
99
|
+
* line or an Ed25519 key in PEM, public or private, whose public half is
|
|
100
|
+
* used. Throws `runner-key-invalid` for anything else, so only a key the
|
|
101
|
+
* readers can verify is ever listed.
|
|
102
|
+
*/
|
|
103
|
+
export declare function runnerPublicKey(given: string, cwd: string): string;
|
|
104
|
+
/** Run the write and build the document it prints. Never throws for a refusal. */
|
|
105
|
+
export declare function trustWrite(req: TrustWriteRequest): TrustWriteDocument;
|
|
106
|
+
/** `chant workspace trust runner add|remove <principal>`. */
|
|
107
|
+
export declare function runWorkspaceTrust(ctx: CommandContext): number;
|
|
108
|
+
export {};
|
|
109
|
+
//# sourceMappingURL=trust-write.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"trust-write.d.ts","sourceRoot":"","sources":["../../../src/workspace/trust/trust-write.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `chant workspace trust runner add <principal> --key <key>` and `chant
|
|
3
|
+
* workspace trust runner remove <principal>` (#3510): a runner entry of
|
|
4
|
+
* `.chant/trust.json`, written through chant.
|
|
5
|
+
*
|
|
6
|
+
* ws-074 lets a tool write the repo only through chant. A lobby that signs
|
|
7
|
+
* agent-run statements, a CI job that signs Op runs and ci tags, and a host
|
|
8
|
+
* running `chant ci serve` each need their key under `runners`, and before
|
|
9
|
+
* this write a box repo added that entry by hand. `add` lists the key for the
|
|
10
|
+
* principal and, for class `runner`, grants the principal the `runner` role
|
|
11
|
+
* that `ci tick`, `ci run` and `chant run --key` require. `remove` takes both
|
|
12
|
+
* away. The file is edited in place, so its formatting and key order stay as
|
|
13
|
+
* they were.
|
|
14
|
+
*
|
|
15
|
+
* The write is a working-tree write like `member add`: it holds the write
|
|
16
|
+
* lock (#3173, ws-089), is judged by the write scope at base, notes who wrote
|
|
17
|
+
* the file in the write journal, and never commits. Changing the trust
|
|
18
|
+
* config is a protected write, so the commit that carries it is checked by
|
|
19
|
+
* `chant workspace verify` against the policy at base. `--expect <digest>`
|
|
20
|
+
* refuses the write when the file has changed since the caller read it: the
|
|
21
|
+
* digest is the SHA-256 of the file's bytes, and of empty text when there is
|
|
22
|
+
* no file. Each write prints one JSON document (`trust-write.schema.json`)
|
|
23
|
+
* with the digest after the write, to pass as `--expect` to the next one.
|
|
24
|
+
*/
|
|
25
|
+
import { createPublicKey } from "node:crypto";
|
|
26
|
+
import { existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
27
|
+
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
28
|
+
import { isDeepStrictEqual } from "node:util";
|
|
29
|
+
import { readerVersion } from "../declaration.js";
|
|
30
|
+
import { IDENTITY_CODES, IdentityError, refuseUnidentified } from "../identity.js";
|
|
31
|
+
import { appendElement, removeElement, setProperty } from "../json-edit.js";
|
|
32
|
+
import { parseJsonText } from "../jsonc.js";
|
|
33
|
+
import { gitRoot } from "../record-source.js";
|
|
34
|
+
import { AGENT_ENV, judgePath, resolveWriter, scopeSource, unknownClassVerdict, WriteScopeError } from "../write-scope.js";
|
|
35
|
+
import { lastWriteReader, noteWrites, sha256Text, withWriteLockSync, writeFileAtomic, WriteLockError, WRITE_LOCK_CODES } from "../write-lock.js";
|
|
36
|
+
import { CI_TAG_SIGNER_ROLE } from "./ci-tag-statement.js";
|
|
37
|
+
import { ed25519FromSsh, sshFromEd25519 } from "./dsse.js";
|
|
38
|
+
import { DEFAULT_SIGNERS_PATH, TRUST_CONFIG_PATH, parseAllowedSigners, trustConfigSchema } from "./policy.js";
|
|
39
|
+
/** The version of the document these writes print. */
|
|
40
|
+
export const TRUST_WRITE_CONTRACT_VERSION = 1;
|
|
41
|
+
/** `$id` of the JSON Schema for what the trust writes print, shipped in `workspace/`. */
|
|
42
|
+
export const TRUST_WRITE_SCHEMA_ID = "https://intentius.io/chant/schemas/workspace/trust-write/v1/trust-write.schema.json";
|
|
43
|
+
export const TRUST_WRITE_ACTIONS = ["runner add", "runner remove"];
|
|
44
|
+
/** Why a trust write wrote nothing. Closed. */
|
|
45
|
+
export const TRUST_WRITE_ERROR_CODES = [
|
|
46
|
+
"not-a-git-repository",
|
|
47
|
+
"write-usage-invalid",
|
|
48
|
+
"trust-config-invalid",
|
|
49
|
+
"trust-conflict",
|
|
50
|
+
"runner-key-invalid",
|
|
51
|
+
"runner-key-is-signer",
|
|
52
|
+
"runner-exists",
|
|
53
|
+
"runner-unknown",
|
|
54
|
+
"write-scope-member",
|
|
55
|
+
"write-scope-protected",
|
|
56
|
+
"write-scope-class-unknown",
|
|
57
|
+
"agent-unknown",
|
|
58
|
+
...IDENTITY_CODES,
|
|
59
|
+
...WRITE_LOCK_CODES,
|
|
60
|
+
];
|
|
61
|
+
class TrustWriteError extends Error {
|
|
62
|
+
constructor(code, message, conflict) {
|
|
63
|
+
super(message);
|
|
64
|
+
this.code = code;
|
|
65
|
+
this.conflict = conflict;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const toPosix = (p) => (sep === "/" ? p : p.split(sep).join("/"));
|
|
69
|
+
const isObject = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
|
|
70
|
+
const same = (a, b) => a.normalize("NFKC").trim().toLowerCase() === b.normalize("NFKC").trim().toLowerCase();
|
|
71
|
+
/** The digest `--expect` compares: the SHA-256 of the file's text, of empty text when there is none. */
|
|
72
|
+
export function trustDigest(text) {
|
|
73
|
+
return sha256Text(text ?? "");
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* The `ssh-ed25519 <base64>` line, with no comment, for what `--key` gives:
|
|
77
|
+
* the line itself (a comment after it is dropped), or a file holding such a
|
|
78
|
+
* line or an Ed25519 key in PEM, public or private, whose public half is
|
|
79
|
+
* used. Throws `runner-key-invalid` for anything else, so only a key the
|
|
80
|
+
* readers can verify is ever listed.
|
|
81
|
+
*/
|
|
82
|
+
export function runnerPublicKey(given, cwd) {
|
|
83
|
+
const fromLine = (line, where) => {
|
|
84
|
+
const [type, blob] = line.trim().split(/\s+/);
|
|
85
|
+
if (type !== "ssh-ed25519") {
|
|
86
|
+
throw new TrustWriteError("runner-key-invalid", `${where} is a ${type ?? "blank"} key; runner keys are ssh-ed25519, since runners sign with Ed25519`);
|
|
87
|
+
}
|
|
88
|
+
try {
|
|
89
|
+
ed25519FromSsh(`${type} ${blob ?? ""}`);
|
|
90
|
+
}
|
|
91
|
+
catch (err) {
|
|
92
|
+
throw new TrustWriteError("runner-key-invalid", `${where} is not an ssh-ed25519 public key: ${err instanceof Error ? err.message : String(err)}`);
|
|
93
|
+
}
|
|
94
|
+
return `${type} ${blob}`;
|
|
95
|
+
};
|
|
96
|
+
const trimmed = given.trim();
|
|
97
|
+
if (/^ssh-[a-z0-9-]+\s/.test(trimmed) || /^(ecdsa|sk-)/.test(trimmed))
|
|
98
|
+
return fromLine(trimmed, "--key");
|
|
99
|
+
const file = resolve(cwd, given);
|
|
100
|
+
if (!existsSync(file)) {
|
|
101
|
+
throw new TrustWriteError("runner-key-invalid", `--key takes an ssh-ed25519 public key line, or a file holding one or an Ed25519 key in PEM, and ${given} is neither`);
|
|
102
|
+
}
|
|
103
|
+
const text = readFileSync(file, "utf-8");
|
|
104
|
+
if (text.includes("-----BEGIN")) {
|
|
105
|
+
if (text.includes("OPENSSH PRIVATE KEY")) {
|
|
106
|
+
throw new TrustWriteError("runner-key-invalid", `${given} is an OpenSSH private key; give its public half (the .pub file), or a key in PEM as runners use`);
|
|
107
|
+
}
|
|
108
|
+
try {
|
|
109
|
+
const key = createPublicKey(text);
|
|
110
|
+
if (key.asymmetricKeyType !== "ed25519")
|
|
111
|
+
throw new Error(`it is ${key.asymmetricKeyType}, and runner keys are Ed25519`);
|
|
112
|
+
const jwk = key.export({ format: "jwk" });
|
|
113
|
+
return sshFromEd25519(Buffer.from(jwk.x, "base64url"));
|
|
114
|
+
}
|
|
115
|
+
catch (err) {
|
|
116
|
+
throw new TrustWriteError("runner-key-invalid", `${given} is not an Ed25519 key in PEM: ${err instanceof Error ? err.message : String(err)}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
const line = text.split(/\r?\n/).find((l) => l.trim() !== "" && !l.trim().startsWith("#"));
|
|
120
|
+
return fromLine(line ?? "", given);
|
|
121
|
+
}
|
|
122
|
+
/** Run the write and build the document it prints. Never throws for a refusal. */
|
|
123
|
+
export function trustWrite(req) {
|
|
124
|
+
const head = { $schema: TRUST_WRITE_SCHEMA_ID, contract: TRUST_WRITE_CONTRACT_VERSION, chant: readerVersion(), action: req.action, name: req.name || null };
|
|
125
|
+
try {
|
|
126
|
+
return withWriteLockSync(req.cwd, { verb: `trust ${req.action}`, by: req.by ?? null, agent: req.agent || null }, req.dryRun, () => write(req, head));
|
|
127
|
+
}
|
|
128
|
+
catch (err) {
|
|
129
|
+
const fail = (code, message, conflict) => ({ ...head, error: { code, message }, ...(conflict ? { conflict } : {}) });
|
|
130
|
+
if (err instanceof TrustWriteError)
|
|
131
|
+
return fail(err.code, err.message, err.conflict);
|
|
132
|
+
if (err instanceof WriteScopeError)
|
|
133
|
+
return fail(err.code, err.message);
|
|
134
|
+
if (err instanceof IdentityError)
|
|
135
|
+
return fail(err.code, err.message);
|
|
136
|
+
if (err instanceof WriteLockError)
|
|
137
|
+
return fail(err.code, err.message);
|
|
138
|
+
throw err;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
function write(req, head) {
|
|
142
|
+
if (!req.name || /\s/.test(req.name))
|
|
143
|
+
throw new TrustWriteError("write-usage-invalid", `${req.action} needs the runner's principal, one word such as ci:acme/app`);
|
|
144
|
+
if (req.expect !== undefined && !/^[0-9a-f]{64}$/.test(req.expect)) {
|
|
145
|
+
throw new TrustWriteError("write-usage-invalid", `--expect takes the trust file's digest, 64 lowercase hex digits as the last trust write printed it, not ${JSON.stringify(req.expect)}`);
|
|
146
|
+
}
|
|
147
|
+
if (req.action === "runner remove" && (req.key !== undefined || req.runnerClass !== undefined)) {
|
|
148
|
+
throw new TrustWriteError("write-usage-invalid", "runner remove takes no --key or --class: it removes the principal's whole entry");
|
|
149
|
+
}
|
|
150
|
+
const runnerClass = req.runnerClass ?? "runner";
|
|
151
|
+
if (runnerClass !== "runner" && runnerClass !== "service")
|
|
152
|
+
throw new TrustWriteError("write-usage-invalid", `--class is runner or service, not ${runnerClass}`);
|
|
153
|
+
if (req.action === "runner add" && req.key === undefined)
|
|
154
|
+
throw new TrustWriteError("write-usage-invalid", "runner add needs the runner's public key: --key <ssh-ed25519 line or file>");
|
|
155
|
+
const top = gitRoot(req.cwd);
|
|
156
|
+
if (!top)
|
|
157
|
+
throw new TrustWriteError("not-a-git-repository", "the trust policy is read from git, and this directory is not in a git repository");
|
|
158
|
+
const file = join(top, TRUST_CONFIG_PATH);
|
|
159
|
+
const before = existsSync(file) ? readFileSync(file, "utf-8") : null;
|
|
160
|
+
const previousDigest = trustDigest(before);
|
|
161
|
+
if (req.expect !== undefined && req.expect !== previousDigest) {
|
|
162
|
+
const lastWrite = before === null ? null : lastWriteReader(top)(TRUST_CONFIG_PATH, before);
|
|
163
|
+
const who = lastWrite ? ` It was last written by chant workspace ${lastWrite.verb}${lastWrite.by ? ` for ${lastWrite.by}` : ""}${lastWrite.agent ? ` in agent session ${lastWrite.agent}` : ""} at ${lastWrite.at}.` : "";
|
|
164
|
+
throw new TrustWriteError("trust-conflict", `${TRUST_CONFIG_PATH} has changed since the digest given with --expect (${req.expect}): its digest is now ${previousDigest}.${who} Read it again, and write again with --expect ${previousDigest} if the change still stands`, { path: TRUST_CONFIG_PATH, expected: req.expect, digest: previousDigest, lastWrite });
|
|
165
|
+
}
|
|
166
|
+
let config = { schema: 1 };
|
|
167
|
+
if (before !== null) {
|
|
168
|
+
const parsed = parseJsonText(before, { jsonc: false });
|
|
169
|
+
if (!parsed.ok)
|
|
170
|
+
throw new TrustWriteError("trust-config-invalid", `${TRUST_CONFIG_PATH} is not JSON: ${parsed.message}`);
|
|
171
|
+
const valid = trustConfigSchema.safeParse(parsed.value);
|
|
172
|
+
if (!valid.success) {
|
|
173
|
+
throw new TrustWriteError("trust-config-invalid", `${TRUST_CONFIG_PATH} is invalid, so chant reads no trust from it: ${valid.error.issues.map((i) => `${i.path.join(".") || "/"}: ${i.message}`).join("; ")}`);
|
|
174
|
+
}
|
|
175
|
+
config = valid.data;
|
|
176
|
+
}
|
|
177
|
+
const runners = config.runners ?? [];
|
|
178
|
+
const index = runners.findIndex((r) => r.principal === req.name);
|
|
179
|
+
const previous = index >= 0 ? runners[index] : null;
|
|
180
|
+
const roleHolders = config.roles?.[CI_TAG_SIGNER_ROLE] ?? [];
|
|
181
|
+
const holds = (list) => list.some((p) => same(p, req.name));
|
|
182
|
+
// The edit, made twice: in place in the text, and on the value it must read back as.
|
|
183
|
+
let after = before ?? "";
|
|
184
|
+
let expected = structuredClone(config);
|
|
185
|
+
let entry = null;
|
|
186
|
+
if (req.action === "runner add") {
|
|
187
|
+
const key = runnerPublicKey(req.key, req.cwd);
|
|
188
|
+
entry = { principal: req.name, class: runnerClass, key };
|
|
189
|
+
const signersPath = config.signers ?? DEFAULT_SIGNERS_PATH;
|
|
190
|
+
const signersFile = join(top, signersPath);
|
|
191
|
+
if (existsSync(signersFile)) {
|
|
192
|
+
const signers = parseAllowedSigners(readFileSync(signersFile, "utf-8")).signers;
|
|
193
|
+
if (signers.some((s) => s.key === key))
|
|
194
|
+
throw new TrustWriteError("runner-key-is-signer", `${signersPath} lists this key for a person; a runner key belongs to a service or CI identity, never to a signer`);
|
|
195
|
+
if (signers.some((s) => s.principal === req.name))
|
|
196
|
+
throw new TrustWriteError("runner-key-is-signer", `${signersPath} lists ${req.name} as a signer; a runner is a service or CI identity, never a signer`);
|
|
197
|
+
}
|
|
198
|
+
const other = runners.find((r) => r.key === key && r.principal !== req.name);
|
|
199
|
+
if (other)
|
|
200
|
+
throw new TrustWriteError("runner-exists", `${TRUST_CONFIG_PATH} already lists this key for ${other.principal}; each runner is one principal with one key`);
|
|
201
|
+
if (previous && !isDeepStrictEqual(previous, entry)) {
|
|
202
|
+
throw new TrustWriteError("runner-exists", `${TRUST_CONFIG_PATH} already lists ${req.name} with ${previous.key === key ? `class ${previous.class}` : "another key"}; remove it first with chant workspace trust runner remove ${req.name}, then add it again`);
|
|
203
|
+
}
|
|
204
|
+
if (before === null) {
|
|
205
|
+
const fresh = { schema: 1, runners: [entry], ...(runnerClass === "runner" ? { roles: { [CI_TAG_SIGNER_ROLE]: [req.name] } } : {}) };
|
|
206
|
+
after = `${JSON.stringify(fresh, null, 2)}\n`;
|
|
207
|
+
expected = fresh;
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
if (!previous) {
|
|
211
|
+
if (Array.isArray(config.runners))
|
|
212
|
+
after = appendElement(after, false, "/runners", entry);
|
|
213
|
+
else
|
|
214
|
+
after = setProperty(after, false, "", "runners", [entry]);
|
|
215
|
+
expected.runners = [...runners, entry];
|
|
216
|
+
}
|
|
217
|
+
if (runnerClass === "runner" && !holds(roleHolders)) {
|
|
218
|
+
if (!config.roles)
|
|
219
|
+
after = setProperty(after, false, "", "roles", { [CI_TAG_SIGNER_ROLE]: [req.name] });
|
|
220
|
+
else if (!config.roles[CI_TAG_SIGNER_ROLE])
|
|
221
|
+
after = setProperty(after, false, "/roles", CI_TAG_SIGNER_ROLE, [req.name]);
|
|
222
|
+
else
|
|
223
|
+
after = appendElement(after, false, `/roles/${CI_TAG_SIGNER_ROLE}`, req.name);
|
|
224
|
+
expected.roles = { ...(config.roles ?? {}), [CI_TAG_SIGNER_ROLE]: [...roleHolders, req.name] };
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
if (!previous) {
|
|
230
|
+
throw new TrustWriteError("runner-unknown", `${TRUST_CONFIG_PATH} lists no runner ${req.name}; its runners are ${runners.map((r) => r.principal).join(", ") || "none"}`);
|
|
231
|
+
}
|
|
232
|
+
after = removeElement(after, false, "/runners", index);
|
|
233
|
+
expected.runners = runners.filter((_, i) => i !== index);
|
|
234
|
+
// The runner role means nothing without the key, so it goes with the entry.
|
|
235
|
+
for (let i = roleHolders.length - 1; i >= 0; i--) {
|
|
236
|
+
if (same(roleHolders[i], req.name))
|
|
237
|
+
after = removeElement(after, false, `/roles/${CI_TAG_SIGNER_ROLE}`, i);
|
|
238
|
+
}
|
|
239
|
+
if (holds(roleHolders))
|
|
240
|
+
expected.roles = { ...(config.roles ?? {}), [CI_TAG_SIGNER_ROLE]: roleHolders.filter((p) => !same(p, req.name)) };
|
|
241
|
+
}
|
|
242
|
+
const reread = parseJsonText(after, { jsonc: false });
|
|
243
|
+
if (!reread.ok || !isDeepStrictEqual(reread.value, expected)) {
|
|
244
|
+
// An edit that does not read back as the value meant is never written.
|
|
245
|
+
throw new Error(`trust ${req.action}: editing ${TRUST_CONFIG_PATH} in place did not give the config intended; nothing was written`);
|
|
246
|
+
}
|
|
247
|
+
if (!trustConfigSchema.safeParse(reread.value).success) {
|
|
248
|
+
throw new TrustWriteError("trust-config-invalid", `the ${req.action} would make ${TRUST_CONFIG_PATH} invalid; nothing was written`);
|
|
249
|
+
}
|
|
250
|
+
const runnerRole = req.action === "runner add" ? holds((isObject(expected.roles) ? expected.roles[CI_TAG_SIGNER_ROLE] : undefined) ?? []) : false;
|
|
251
|
+
// The write scope at base, and who --by names.
|
|
252
|
+
const changed = after !== (before ?? "");
|
|
253
|
+
const source = scopeSource(req.cwd);
|
|
254
|
+
if (req.by !== undefined)
|
|
255
|
+
refuseUnidentified(source, [req.by], "--by", { agent: req.agent });
|
|
256
|
+
if (changed && (source.declaration !== null || (req.agent !== undefined && req.agent !== ""))) {
|
|
257
|
+
const writer = resolveWriter(source.declaration, source.policy, { agent: req.agent || null, principal: req.by ?? null }, source.classes);
|
|
258
|
+
const unknown = unknownClassVerdict(source.declaration, writer, source.classes);
|
|
259
|
+
if (!unknown.ok)
|
|
260
|
+
throw new WriteScopeError(unknown.code, unknown.message);
|
|
261
|
+
const path = source.rootOnDisk ? toPosix(relative(source.rootOnDisk, file)) : TRUST_CONFIG_PATH;
|
|
262
|
+
const verdict = judgePath(source.declaration, writer, path, { before: () => before ?? "", after: () => after });
|
|
263
|
+
if (!verdict.ok)
|
|
264
|
+
throw new TrustWriteError(verdict.code, verdict.message);
|
|
265
|
+
}
|
|
266
|
+
if (changed && !req.dryRun) {
|
|
267
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
268
|
+
writeFileAtomic(file, after);
|
|
269
|
+
noteWrites(top, [{ path: TRUST_CONFIG_PATH, text: after }], { verb: `trust ${req.action}`, by: req.by ?? null, agent: req.agent || null });
|
|
270
|
+
}
|
|
271
|
+
return {
|
|
272
|
+
...head,
|
|
273
|
+
name: req.name,
|
|
274
|
+
path: TRUST_CONFIG_PATH,
|
|
275
|
+
digest: trustDigest(changed ? after : before),
|
|
276
|
+
previousDigest,
|
|
277
|
+
paths: changed ? [TRUST_CONFIG_PATH] : [],
|
|
278
|
+
changed,
|
|
279
|
+
dryRun: req.dryRun === true,
|
|
280
|
+
previous,
|
|
281
|
+
entry,
|
|
282
|
+
runnerRole,
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
// ── The command line ─────────────────────────────────────────────────────────
|
|
286
|
+
const USAGE = {
|
|
287
|
+
"runner add": "chant workspace trust runner add <principal> --key <ssh-ed25519 line or file> [--class runner|service] [--by <principal>] [--expect <digest>] [--dry-run]",
|
|
288
|
+
"runner remove": "chant workspace trust runner remove <principal> [--by <principal>] [--expect <digest>] [--dry-run]",
|
|
289
|
+
};
|
|
290
|
+
const print = (doc) => {
|
|
291
|
+
console.log(JSON.stringify(doc, null, 2));
|
|
292
|
+
return "error" in doc ? 1 : 0;
|
|
293
|
+
};
|
|
294
|
+
/** `chant workspace trust runner add|remove <principal>`. */
|
|
295
|
+
export function runWorkspaceTrust(ctx) {
|
|
296
|
+
const [, noun, verb, ...rest] = ctx.args.positionals ?? [];
|
|
297
|
+
const action = noun === "runner" && verb === "add" ? "runner add" : noun === "runner" && verb === "remove" ? "runner remove" : null;
|
|
298
|
+
const usage = (a, name, message) => print({ $schema: TRUST_WRITE_SCHEMA_ID, contract: TRUST_WRITE_CONTRACT_VERSION, chant: readerVersion(), action: a, name: name ?? null, error: { code: "write-usage-invalid", message: `${message}; ${USAGE[a]}` } });
|
|
299
|
+
if (action === null) {
|
|
300
|
+
const given = [noun, verb].filter(Boolean).join(" ");
|
|
301
|
+
return usage("runner add", rest[0], `chant workspace trust takes runner add <principal> or runner remove <principal>${given ? `, not ${given}` : ""}`);
|
|
302
|
+
}
|
|
303
|
+
const [name, ...extra] = rest;
|
|
304
|
+
if (name === undefined)
|
|
305
|
+
return usage(action, undefined, `trust ${action} needs the runner's principal`);
|
|
306
|
+
if (extra.length > 0)
|
|
307
|
+
return usage(action, name, `trust ${action} takes one principal, and was also given ${extra.join(" ")}`);
|
|
308
|
+
const { args } = ctx;
|
|
309
|
+
return print(trustWrite({
|
|
310
|
+
cwd: process.cwd(),
|
|
311
|
+
action,
|
|
312
|
+
name,
|
|
313
|
+
...(args.key !== undefined ? { key: args.key } : {}),
|
|
314
|
+
...(args.runnerClass !== undefined ? { runnerClass: args.runnerClass } : {}),
|
|
315
|
+
...(args.by !== undefined ? { by: args.by } : {}),
|
|
316
|
+
...(args.expect !== undefined ? { expect: args.expect } : {}),
|
|
317
|
+
agent: process.env[AGENT_ENV] || undefined,
|
|
318
|
+
dryRun: args.dryRun === true,
|
|
319
|
+
}));
|
|
320
|
+
}
|