@intentius/chant 0.46.0 → 0.50.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/audit/catalog.d.ts +13 -3
- package/dist/audit/catalog.d.ts.map +1 -1
- package/dist/audit/core.d.ts +30 -3
- package/dist/audit/core.d.ts.map +1 -1
- package/dist/audit/discover.d.ts +9 -2
- package/dist/audit/discover.d.ts.map +1 -1
- package/dist/audit/fetch.d.ts.map +1 -1
- package/dist/audit/report-html.d.ts.map +1 -1
- package/dist/audit/report-model.d.ts +6 -0
- package/dist/audit/report-model.d.ts.map +1 -1
- package/dist/audit/report.d.ts.map +1 -1
- package/dist/audit/rules-doc.d.ts.map +1 -1
- package/dist/audit/secrets.d.ts +95 -0
- package/dist/audit/secrets.d.ts.map +1 -0
- package/dist/audit/wrangler.d.ts +33 -0
- package/dist/audit/wrangler.d.ts.map +1 -0
- package/dist/build.d.ts +3 -3
- package/dist/build.d.ts.map +1 -1
- package/dist/cli/commands/audit.d.ts +7 -0
- package/dist/cli/commands/audit.d.ts.map +1 -1
- package/dist/cli/commands/build.d.ts +23 -0
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/lint.d.ts.map +1 -1
- package/dist/cli/handlers/build.d.ts.map +1 -1
- package/dist/cli/handlers/components.d.ts +31 -0
- package/dist/cli/handlers/components.d.ts.map +1 -1
- package/dist/cli/handlers/lifecycle.d.ts +12 -1
- package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
- package/dist/cli/handlers/operator.d.ts +32 -0
- package/dist/cli/handlers/operator.d.ts.map +1 -0
- package/dist/cli/handlers/scenario.d.ts +39 -0
- package/dist/cli/handlers/scenario.d.ts.map +1 -0
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/mcp/resource-handlers.d.ts.map +1 -1
- package/dist/cli/mcp/server.d.ts +35 -2
- package/dist/cli/mcp/server.d.ts.map +1 -1
- package/dist/cli/mcp/tools/explain.d.ts +6 -0
- package/dist/cli/mcp/tools/explain.d.ts.map +1 -1
- package/dist/cli/mcp/types.d.ts +29 -1
- package/dist/cli/mcp/types.d.ts.map +1 -1
- package/dist/cli/plugins.d.ts +1 -1
- package/dist/cli/plugins.d.ts.map +1 -1
- package/dist/cli/registry.d.ts +14 -2
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/cli/reporters/stylish.d.ts +15 -1
- package/dist/cli/reporters/stylish.d.ts.map +1 -1
- package/dist/codegen/docs-rule-scanning.d.ts.map +1 -1
- package/dist/components/auto-release.d.ts +4 -0
- package/dist/components/auto-release.d.ts.map +1 -1
- package/dist/components/capability.d.ts +17 -2
- package/dist/components/capability.d.ts.map +1 -1
- package/dist/components/cli-support.d.ts +7 -0
- package/dist/components/cli-support.d.ts.map +1 -1
- package/dist/components/component.d.ts +15 -0
- package/dist/components/component.d.ts.map +1 -1
- package/dist/components/driver.d.ts.map +1 -1
- package/dist/components/starter-plugin.d.ts +2 -0
- package/dist/components/starter-plugin.d.ts.map +1 -1
- package/dist/components/verbs/ensure-secret.d.ts +50 -0
- package/dist/components/verbs/ensure-secret.d.ts.map +1 -0
- package/dist/components/verbs/index.d.ts +13 -0
- package/dist/components/verbs/index.d.ts.map +1 -1
- package/dist/components/verbs/r2-sync.d.ts +76 -0
- package/dist/components/verbs/r2-sync.d.ts.map +1 -0
- package/dist/components/verbs/run-agent.d.ts +499 -0
- package/dist/components/verbs/run-agent.d.ts.map +1 -0
- package/dist/components/verbs/sign.d.ts +30 -0
- package/dist/components/verbs/sign.d.ts.map +1 -1
- package/dist/components/verbs/wrangler.d.ts +108 -0
- package/dist/components/verbs/wrangler.d.ts.map +1 -0
- package/dist/composite.d.ts +6 -1
- package/dist/composite.d.ts.map +1 -1
- package/dist/config.d.ts +26 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/deep-observation.d.ts +14 -0
- package/dist/deep-observation.d.ts.map +1 -1
- package/dist/discovery/collect.d.ts.map +1 -1
- package/dist/discovery/fold-import.d.ts +15 -1
- package/dist/discovery/fold-import.d.ts.map +1 -1
- package/dist/discovery/fold-rank.d.ts +66 -0
- package/dist/discovery/fold-rank.d.ts.map +1 -0
- package/dist/discovery/index.d.ts +15 -0
- package/dist/discovery/index.d.ts.map +1 -1
- package/dist/discovery/param-deps.d.ts +17 -0
- package/dist/discovery/param-deps.d.ts.map +1 -0
- package/dist/effect-receipt.d.ts +177 -0
- package/dist/effect-receipt.d.ts.map +1 -0
- package/dist/fold/fold.d.ts +55 -2
- package/dist/fold/fold.d.ts.map +1 -1
- package/dist/fold/subset.d.ts +20 -0
- package/dist/fold/subset.d.ts.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/lexicon-schema.d.ts +2 -0
- package/dist/lexicon-schema.d.ts.map +1 -1
- package/dist/lexicon.d.ts +137 -3
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lifecycle/change-set.d.ts +33 -5
- package/dist/lifecycle/change-set.d.ts.map +1 -1
- package/dist/lifecycle/converge-ledger.d.ts +90 -0
- package/dist/lifecycle/converge-ledger.d.ts.map +1 -0
- package/dist/lifecycle/deep-diff.d.ts +18 -0
- package/dist/lifecycle/deep-diff.d.ts.map +1 -1
- package/dist/lifecycle/deep-observe.d.ts +9 -1
- package/dist/lifecycle/deep-observe.d.ts.map +1 -1
- package/dist/lifecycle/gate-ledger.d.ts +33 -0
- package/dist/lifecycle/gate-ledger.d.ts.map +1 -0
- package/dist/lifecycle/git.d.ts +145 -21
- package/dist/lifecycle/git.d.ts.map +1 -1
- package/dist/lifecycle/index.d.ts +6 -0
- package/dist/lifecycle/index.d.ts.map +1 -1
- package/dist/lifecycle/lease.d.ts +113 -0
- package/dist/lifecycle/lease.d.ts.map +1 -0
- package/dist/lifecycle/observation-baseline.d.ts +21 -3
- package/dist/lifecycle/observation-baseline.d.ts.map +1 -1
- package/dist/lifecycle/receipt-plan.d.ts +62 -0
- package/dist/lifecycle/receipt-plan.d.ts.map +1 -0
- package/dist/lifecycle/release-ledger.d.ts +20 -0
- package/dist/lifecycle/release-ledger.d.ts.map +1 -1
- package/dist/lifecycle/scenario-eval.d.ts +42 -0
- package/dist/lifecycle/scenario-eval.d.ts.map +1 -0
- package/dist/lifecycle/scenario.d.ts +163 -0
- package/dist/lifecycle/scenario.d.ts.map +1 -0
- package/dist/lifecycle/symptoms.d.ts +63 -0
- package/dist/lifecycle/symptoms.d.ts.map +1 -0
- package/dist/lifecycle/teardown.d.ts +6 -4
- package/dist/lifecycle/teardown.d.ts.map +1 -1
- package/dist/lifecycle/unobserved-gate.d.ts +67 -0
- package/dist/lifecycle/unobserved-gate.d.ts.map +1 -0
- package/dist/lint/knowledge-checks.d.ts +48 -0
- package/dist/lint/knowledge-checks.d.ts.map +1 -0
- package/dist/lint/output-checks.d.ts +5 -0
- package/dist/lint/output-checks.d.ts.map +1 -0
- package/dist/lint/output-docs.d.ts +94 -0
- package/dist/lint/output-docs.d.ts.map +1 -0
- package/dist/lint/pipeline-change-gate.d.ts +101 -0
- package/dist/lint/pipeline-change-gate.d.ts.map +1 -0
- package/dist/lint/post-synth.d.ts +41 -0
- package/dist/lint/post-synth.d.ts.map +1 -1
- package/dist/lint/receipt-checks.d.ts +9 -0
- package/dist/lint/receipt-checks.d.ts.map +1 -0
- package/dist/lint/rules/__fixtures__/comp/comp003/pass/agent-turn.component.d.ts +11 -0
- package/dist/lint/rules/__fixtures__/comp/comp003/pass/agent-turn.component.d.ts.map +1 -0
- package/dist/lint/rules/cor022-receipt-leaf.d.ts +13 -0
- package/dist/lint/rules/cor022-receipt-leaf.d.ts.map +1 -0
- package/dist/lint/rules/cor024-receipt-secret-pointer.d.ts +3 -0
- package/dist/lint/rules/cor024-receipt-secret-pointer.d.ts.map +1 -0
- package/dist/lint/rules/evl001-non-literal-expression.d.ts.map +1 -1
- package/dist/lint/rules/index.d.ts +3 -1
- package/dist/lint/rules/index.d.ts.map +1 -1
- package/dist/lsp/lexicon-providers.d.ts +7 -0
- package/dist/lsp/lexicon-providers.d.ts.map +1 -1
- package/dist/okf-read.d.ts +78 -0
- package/dist/okf-read.d.ts.map +1 -0
- package/dist/op/activity-contract.d.ts +139 -0
- package/dist/op/activity-contract.d.ts.map +1 -0
- package/dist/op/builders.d.ts +140 -3
- package/dist/op/builders.d.ts.map +1 -1
- package/dist/op/converge-rule.d.ts +161 -0
- package/dist/op/converge-rule.d.ts.map +1 -0
- package/dist/op/generate-pipeline.d.ts +39 -0
- package/dist/op/generate-pipeline.d.ts.map +1 -0
- package/dist/op/index.d.ts +18 -2
- package/dist/op/index.d.ts.map +1 -1
- package/dist/op/local-executor.d.ts +2 -1
- package/dist/op/local-executor.d.ts.map +1 -1
- package/dist/op/op-verb-class.d.ts +42 -0
- package/dist/op/op-verb-class.d.ts.map +1 -0
- package/dist/op/operator.d.ts +128 -0
- package/dist/op/operator.d.ts.map +1 -0
- package/dist/op/receipt-store.d.ts +138 -0
- package/dist/op/receipt-store.d.ts.map +1 -0
- package/dist/op/step-output-ref.d.ts +187 -0
- package/dist/op/step-output-ref.d.ts.map +1 -0
- package/dist/op/types.d.ts +49 -2
- package/dist/op/types.d.ts.map +1 -1
- package/dist/provenance.d.ts +73 -3
- package/dist/provenance.d.ts.map +1 -1
- package/dist/runtime-adapter.d.ts +7 -1
- package/dist/runtime-adapter.d.ts.map +1 -1
- package/dist/secret-materialization.d.ts +138 -0
- package/dist/secret-materialization.d.ts.map +1 -0
- package/dist/secret-provenance.d.ts +218 -0
- package/dist/secret-provenance.d.ts.map +1 -0
- package/dist/serializer.d.ts +29 -0
- package/dist/serializer.d.ts.map +1 -1
- package/dist/toml.d.ts +40 -5
- package/dist/toml.d.ts.map +1 -1
- package/dist/yaml.d.ts.map +1 -1
- package/package.json +4 -1
- package/src/audit/catalog.test.ts +1 -1
- package/src/audit/catalog.ts +75 -3
- package/src/audit/core.test.ts +57 -0
- package/src/audit/core.ts +0 -0
- package/src/audit/detect-bundle.test.ts +1 -1
- package/src/audit/discover.test.ts +24 -0
- package/src/audit/discover.ts +40 -4
- package/src/audit/fetch.test.ts +216 -3
- package/src/audit/fetch.ts +270 -59
- package/src/audit/report-html.ts +5 -2
- package/src/audit/report-model.ts +9 -0
- package/src/audit/report.test.ts +22 -0
- package/src/audit/report.ts +3 -2
- package/src/audit/rules-doc.ts +13 -1
- package/src/audit/secrets.test.ts +303 -0
- package/src/audit/secrets.ts +406 -0
- package/src/audit/wrangler.test.ts +230 -0
- package/src/audit/wrangler.ts +290 -0
- package/src/build.test.ts +41 -0
- package/src/build.ts +39 -6
- package/src/cli/command-group.ts +1 -1
- package/src/cli/commands/__fixtures__/audit-fountain/agents/fleet.yaml +27 -0
- package/src/cli/commands/__fixtures__/audit-fountain/k8s/deploy.yaml +16 -0
- package/src/cli/commands/__fixtures__/audit-fountain-clean/fleet.yaml +20 -0
- package/src/cli/commands/__fixtures__/schemas/sarif-2.1.0.schema.json +2882 -0
- package/src/cli/commands/audit.test.ts +268 -1
- package/src/cli/commands/audit.ts +87 -18
- package/src/cli/commands/build.test.ts +245 -0
- package/src/cli/commands/build.ts +210 -21
- package/src/cli/commands/lint.ts +15 -3
- package/src/cli/handlers/build.ts +2 -0
- package/src/cli/handlers/components.test.ts +199 -1
- package/src/cli/handlers/components.ts +160 -3
- package/src/cli/handlers/explain.test.ts +70 -1
- package/src/cli/handlers/graph.test.ts +20 -0
- package/src/cli/handlers/graph.ts +12 -3
- package/src/cli/handlers/lifecycle.test.ts +115 -1
- package/src/cli/handlers/lifecycle.ts +96 -15
- package/src/cli/handlers/operator.test.ts +255 -0
- package/src/cli/handlers/operator.ts +240 -0
- package/src/cli/handlers/scenario.test.ts +456 -0
- package/src/cli/handlers/scenario.ts +330 -0
- package/src/cli/main.test.ts +23 -0
- package/src/cli/main.ts +72 -1
- package/src/cli/mcp/resource-handlers.ts +38 -1
- package/src/cli/mcp/server.test.ts +323 -3
- package/src/cli/mcp/server.ts +84 -7
- package/src/cli/mcp/tools/explain.ts +51 -2
- package/src/cli/mcp/types.ts +27 -1
- package/src/cli/plugins.ts +4 -2
- package/src/cli/registry.ts +14 -2
- package/src/cli/reporters/stylish.test.ts +154 -0
- package/src/cli/reporters/stylish.ts +154 -33
- package/src/codegen/docs-rule-scanning.test.ts +42 -0
- package/src/codegen/docs-rule-scanning.ts +25 -2
- package/src/components/README.md +7 -0
- package/src/components/auto-release.ts +6 -0
- package/src/components/capability.ts +17 -2
- package/src/components/cli-support.test.ts +17 -0
- package/src/components/cli-support.ts +13 -1
- package/src/components/component-schema.test.ts +32 -0
- package/src/components/component.schema.json +6 -0
- package/src/components/component.test.ts +21 -0
- package/src/components/component.ts +15 -0
- package/src/components/driver.ts +12 -4
- package/src/components/registry.test.ts +7 -2
- package/src/components/starter-plugin.ts +17 -0
- package/src/components/verbs/ensure-secret.test.ts +130 -0
- package/src/components/verbs/ensure-secret.ts +79 -0
- package/src/components/verbs/index.ts +13 -0
- package/src/components/verbs/r2-sync.test.ts +107 -0
- package/src/components/verbs/r2-sync.ts +124 -0
- package/src/components/verbs/run-agent.test.ts +683 -0
- package/src/components/verbs/run-agent.ts +786 -0
- package/src/components/verbs/sign.test.ts +19 -0
- package/src/components/verbs/sign.ts +34 -2
- package/src/components/verbs/wrangler.test.ts +170 -0
- package/src/components/verbs/wrangler.ts +241 -0
- package/src/composite.ts +31 -2
- package/src/config.test.ts +15 -0
- package/src/config.ts +30 -0
- package/src/deep-observation.test.ts +19 -0
- package/src/deep-observation.ts +17 -0
- package/src/discovery/collect.ts +11 -2
- package/src/discovery/fold-import.test.ts +54 -0
- package/src/discovery/fold-import.ts +178 -38
- package/src/discovery/fold-rank.test.ts +197 -0
- package/src/discovery/fold-rank.ts +346 -0
- package/src/discovery/index.ts +16 -1
- package/src/discovery/param-deps.test.ts +118 -0
- package/src/discovery/param-deps.ts +170 -0
- package/src/effect-receipt-exclusion.test.ts +190 -0
- package/src/effect-receipt.test.ts +419 -0
- package/src/effect-receipt.ts +412 -0
- package/src/fold/fold.test.ts +6 -2
- package/src/fold/fold.ts +184 -3
- package/src/fold/subset.test.ts +95 -6
- package/src/fold/subset.ts +66 -2
- package/src/index.ts +4 -0
- package/src/lexicon-schema.ts +3 -0
- package/src/lexicon.ts +151 -5
- package/src/lifecycle/change-set.ts +46 -7
- package/src/lifecycle/converge-ledger.test.ts +199 -0
- package/src/lifecycle/converge-ledger.ts +179 -0
- package/src/lifecycle/deep-diff.test.ts +79 -1
- package/src/lifecycle/deep-diff.ts +23 -0
- package/src/lifecycle/deep-observe.ts +13 -2
- package/src/lifecycle/gate-ledger.test.ts +103 -0
- package/src/lifecycle/gate-ledger.ts +140 -0
- package/src/lifecycle/git.test.ts +430 -0
- package/src/lifecycle/git.ts +446 -84
- package/src/lifecycle/index.ts +6 -0
- package/src/lifecycle/lease.test.ts +343 -0
- package/src/lifecycle/lease.ts +270 -0
- package/src/lifecycle/observation-baseline.test.ts +46 -0
- package/src/lifecycle/observation-baseline.ts +33 -1
- package/src/lifecycle/receipt-plan.test.ts +250 -0
- package/src/lifecycle/receipt-plan.ts +249 -0
- package/src/lifecycle/release-ledger.ts +20 -0
- package/src/lifecycle/scenario-eval.test.ts +199 -0
- package/src/lifecycle/scenario-eval.ts +158 -0
- package/src/lifecycle/scenario.test.ts +195 -0
- package/src/lifecycle/scenario.ts +321 -0
- package/src/lifecycle/symptoms.test.ts +116 -0
- package/src/lifecycle/symptoms.ts +126 -0
- package/src/lifecycle/teardown.test.ts +31 -0
- package/src/lifecycle/teardown.ts +6 -4
- package/src/lifecycle/unobserved-gate.test.ts +109 -0
- package/src/lifecycle/unobserved-gate.ts +102 -0
- package/src/lint/knowledge-checks.test.ts +80 -0
- package/src/lint/knowledge-checks.ts +74 -0
- package/src/lint/output-checks.test.ts +85 -0
- package/src/lint/output-checks.ts +99 -0
- package/src/lint/output-docs.test.ts +220 -0
- package/src/lint/output-docs.ts +204 -0
- package/src/lint/pipeline-change-gate.test.ts +144 -0
- package/src/lint/pipeline-change-gate.ts +153 -0
- package/src/lint/post-synth.test.ts +97 -0
- package/src/lint/post-synth.ts +60 -0
- package/src/lint/receipt-checks.test.ts +101 -0
- package/src/lint/receipt-checks.ts +93 -0
- package/src/lint/rules/__fixtures__/comp/comp003/pass/agent-turn.component.ts +26 -0
- package/src/lint/rules/comp/comp.test.ts +49 -1
- package/src/lint/rules/cor022-receipt-leaf.test.ts +116 -0
- package/src/lint/rules/cor022-receipt-leaf.ts +130 -0
- package/src/lint/rules/cor024-receipt-secret-pointer.test.ts +121 -0
- package/src/lint/rules/cor024-receipt-secret-pointer.ts +218 -0
- package/src/lint/rules/evl001-non-literal-expression.test.ts +35 -3
- package/src/lint/rules/evl001-non-literal-expression.ts +7 -0
- package/src/lint/rules/index.ts +7 -1
- package/src/lsp/lexicon-providers.test.ts +44 -0
- package/src/lsp/lexicon-providers.ts +11 -1
- package/src/okf-read.test.ts +149 -0
- package/src/okf-read.ts +197 -0
- package/src/op/activity-contract.test.ts +180 -0
- package/src/op/activity-contract.ts +278 -0
- package/src/op/builders-exports.test.ts +17 -1
- package/src/op/builders.ts +198 -6
- package/src/op/converge-rule.test.ts +179 -0
- package/src/op/converge-rule.ts +311 -0
- package/src/op/effect-step.test.ts +311 -0
- package/src/op/generate-pipeline.test.ts +53 -0
- package/src/op/generate-pipeline.ts +99 -0
- package/src/op/index.ts +40 -3
- package/src/op/local-executor.test.ts +92 -0
- package/src/op/local-executor.ts +212 -29
- package/src/op/op-verb-class.test.ts +126 -0
- package/src/op/op-verb-class.ts +115 -0
- package/src/op/op.test.ts +25 -2
- package/src/op/operator.test.ts +346 -0
- package/src/op/operator.ts +213 -0
- package/src/op/receipt-store.ts +211 -0
- package/src/op/step-output-ref.test.ts +334 -0
- package/src/op/step-output-ref.ts +453 -0
- package/src/op/types.ts +51 -2
- package/src/provenance.test.ts +151 -4
- package/src/provenance.ts +118 -4
- package/src/runtime-adapter.ts +31 -10
- package/src/secret-materialization.test.ts +199 -0
- package/src/secret-materialization.ts +235 -0
- package/src/secret-provenance.test.ts +388 -0
- package/src/secret-provenance.ts +475 -0
- package/src/serializer.ts +30 -0
- package/src/toml.test.ts +157 -384
- package/src/toml.ts +371 -5
- package/src/yaml.test.ts +88 -0
- package/src/yaml.ts +76 -6
|
@@ -143,6 +143,38 @@ describe("Component JSON Schema", () => {
|
|
|
143
143
|
expect(validate(invalid)).toBe(false);
|
|
144
144
|
});
|
|
145
145
|
|
|
146
|
+
it("accepts an optional composites list (#1492)", () => {
|
|
147
|
+
const withComposites = {
|
|
148
|
+
name: "aws-plane",
|
|
149
|
+
archetype: "infra",
|
|
150
|
+
dependsOn: [],
|
|
151
|
+
composites: ["ArtifactBucket", "OperatorRole"],
|
|
152
|
+
deploy: [{ phase: "Apply", steps: [{ kind: "cfn-deploy", template: "t.json" }] }],
|
|
153
|
+
};
|
|
154
|
+
expect(validate(withComposites)).toBe(true);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("omitting composites still validates (backward-compatible default)", () => {
|
|
158
|
+
const noComposites = {
|
|
159
|
+
name: "table",
|
|
160
|
+
archetype: "infra",
|
|
161
|
+
dependsOn: [],
|
|
162
|
+
deploy: [{ phase: "Apply", steps: [{ kind: "cfn-deploy", template: "t.json" }] }],
|
|
163
|
+
};
|
|
164
|
+
expect(validate(noComposites)).toBe(true);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("rejects an empty composites array (minItems: 1)", () => {
|
|
168
|
+
const invalid = {
|
|
169
|
+
name: "table",
|
|
170
|
+
archetype: "infra",
|
|
171
|
+
dependsOn: [],
|
|
172
|
+
composites: [],
|
|
173
|
+
deploy: [{ phase: "Apply", steps: [{ kind: "cfn-deploy", template: "t.json" }] }],
|
|
174
|
+
};
|
|
175
|
+
expect(validate(invalid)).toBe(false);
|
|
176
|
+
});
|
|
177
|
+
|
|
146
178
|
it("rejects an invalid archetype value", () => {
|
|
147
179
|
const invalid = {
|
|
148
180
|
name: "x",
|
|
@@ -54,6 +54,12 @@
|
|
|
54
54
|
"description": "Optional: the live lexicon entity/resource name(s) this component owns, when they differ from `name` (#598). `chant components status --live` reconciles the release ledger against live evidence by joining on this name; omitted entirely (the default), the join falls back to `name` itself with no behavior change. A component that owns several live entities (e.g. a fan-out cluster with one stack per node) may list them all.",
|
|
55
55
|
"items": { "type": "string", "minLength": 1 },
|
|
56
56
|
"minItems": 1
|
|
57
|
+
},
|
|
58
|
+
"composites": {
|
|
59
|
+
"type": "array",
|
|
60
|
+
"description": "Optional: the composite kind name(s) this component's deployed resources come from, when a consumer cannot infer it from `name` (#1492). Without this, a consumer joining a composite graph to the component DAG guesses by kebab-casing each composite's kind and matching it against this component's `name`, which only holds when a component maps 1:1 to a composite named for it. Omitted entirely (the default), the naming convention stays in effect with no behavior change. A component built from several composites may list them all.",
|
|
61
|
+
"items": { "type": "string", "minLength": 1 },
|
|
62
|
+
"minItems": 1
|
|
57
63
|
}
|
|
58
64
|
},
|
|
59
65
|
"$defs": {
|
|
@@ -202,6 +202,27 @@ describe("projectToJson()", () => {
|
|
|
202
202
|
expect("liveNames" in projected).toBe(false);
|
|
203
203
|
});
|
|
204
204
|
|
|
205
|
+
it("projects an explicit composites list through unchanged (#1492)", () => {
|
|
206
|
+
const component: Component = {
|
|
207
|
+
name: "aws-plane",
|
|
208
|
+
dependsOn: [],
|
|
209
|
+
composites: ["ArtifactBucket", "OperatorRole"],
|
|
210
|
+
deploy: [phase("Apply", [{ kind: "cfn-deploy" }])],
|
|
211
|
+
};
|
|
212
|
+
const projected = projectToJson(component) as { composites?: string[] };
|
|
213
|
+
expect(projected.composites).toEqual(["ArtifactBucket", "OperatorRole"]);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it("omits composites from the projection when not authored (#1492)", () => {
|
|
217
|
+
const component: Component = {
|
|
218
|
+
name: "table",
|
|
219
|
+
dependsOn: [],
|
|
220
|
+
deploy: [phase("Apply", [{ kind: "cfn-deploy" }])],
|
|
221
|
+
};
|
|
222
|
+
const projected = projectToJson(component) as Record<string, unknown>;
|
|
223
|
+
expect("composites" in projected).toBe(false);
|
|
224
|
+
});
|
|
225
|
+
|
|
205
226
|
it("projects a stackOutput() wiring reference to its plain-JSON form", () => {
|
|
206
227
|
const component: Component = {
|
|
207
228
|
name: "svc",
|
|
@@ -148,6 +148,21 @@ export interface Component {
|
|
|
148
148
|
* name == entity join with no behavior change.
|
|
149
149
|
*/
|
|
150
150
|
liveNames?: string[];
|
|
151
|
+
/**
|
|
152
|
+
* Optional: the composite kind name(s) this component's deployed resources
|
|
153
|
+
* come from, when a consumer joining a composite graph to the component DAG
|
|
154
|
+
* cannot infer it from `name` (#1492). Components and composites are
|
|
155
|
+
* different namespaces, the same class of mismatch `liveNames` covers for
|
|
156
|
+
* components and lexicon entities: a consumer without this field guesses by
|
|
157
|
+
* kebab-casing each composite's kind and matching it against this
|
|
158
|
+
* component's `name` (`LoomBackend` -> `loom-backend`), which only holds
|
|
159
|
+
* when a component maps 1:1 to a composite named for it. A component built
|
|
160
|
+
* from several composites (e.g. one stack containing both an ArtifactBucket
|
|
161
|
+
* and an OperatorRole composite, plus loose resources) lists them all.
|
|
162
|
+
* Omitted entirely (the common case), the naming convention stays the
|
|
163
|
+
* default and nothing changes.
|
|
164
|
+
*/
|
|
165
|
+
composites?: string[];
|
|
151
166
|
}
|
|
152
167
|
|
|
153
168
|
/** Author a named phase. `steps` may mix capability `Step`s, `Gate`s, and nested `Phase`s (fan-out). */
|
package/src/components/driver.ts
CHANGED
|
@@ -313,11 +313,19 @@ export function resolveStepInput(
|
|
|
313
313
|
|
|
314
314
|
// ── Step + phase execution ───────────────────────────────────────────────
|
|
315
315
|
|
|
316
|
-
/**
|
|
316
|
+
/**
|
|
317
|
+
* A step executed and its resolved input, kept so saga rollback can call the
|
|
318
|
+
* same capability's `rollback` with the same input. `output` is the step's
|
|
319
|
+
* own `run()` result — threaded through to `rollback` (#1944) as the
|
|
320
|
+
* serializable identity channel `Capability.rollback`'s doc comment
|
|
321
|
+
* describes, alongside object identity (which already works here, since this
|
|
322
|
+
* driver runs entirely in-process — see this module's own doc comment).
|
|
323
|
+
*/
|
|
317
324
|
interface ExecutedStep {
|
|
318
325
|
step: DriverStep;
|
|
319
326
|
resolvedInput: Record<string, unknown>;
|
|
320
327
|
phaseName: string;
|
|
328
|
+
output: unknown;
|
|
321
329
|
}
|
|
322
330
|
|
|
323
331
|
class StepFailure extends Error {
|
|
@@ -435,7 +443,7 @@ async function runPhase(
|
|
|
435
443
|
}
|
|
436
444
|
return {
|
|
437
445
|
records: [record],
|
|
438
|
-
executed: record.status === "ok" ? [{ step: entry, resolvedInput, phaseName: phaseDef.phase }] : [],
|
|
446
|
+
executed: record.status === "ok" ? [{ step: entry, resolvedInput, phaseName: phaseDef.phase, output }] : [],
|
|
439
447
|
failed: record.status === "fail",
|
|
440
448
|
};
|
|
441
449
|
};
|
|
@@ -505,7 +513,7 @@ async function rollbackExecuted(
|
|
|
505
513
|
registry: CapabilityRegistry,
|
|
506
514
|
): Promise<DriverStepRecord[]> {
|
|
507
515
|
const records: DriverStepRecord[] = [];
|
|
508
|
-
for (const { step, resolvedInput, phaseName } of [...executed].reverse()) {
|
|
516
|
+
for (const { step, resolvedInput, phaseName, output } of [...executed].reverse()) {
|
|
509
517
|
const start = Date.now();
|
|
510
518
|
try {
|
|
511
519
|
const capability = registry.resolve(step.kind);
|
|
@@ -521,7 +529,7 @@ async function rollbackExecuted(
|
|
|
521
529
|
});
|
|
522
530
|
continue;
|
|
523
531
|
}
|
|
524
|
-
await capability.rollback(ctx, resolvedInput as never);
|
|
532
|
+
await capability.rollback(ctx, resolvedInput as never, output as never);
|
|
525
533
|
records.push({
|
|
526
534
|
component: ctx.component,
|
|
527
535
|
phase: phaseName,
|
|
@@ -92,9 +92,14 @@ describe("createCapabilityRegistry", () => {
|
|
|
92
92
|
}
|
|
93
93
|
});
|
|
94
94
|
|
|
95
|
-
|
|
95
|
+
// Every AWS/k8s cloud leaf moved out to its lexicon — but the three
|
|
96
|
+
// Cloudflare Workers apply verbs (#1293, epic #1296) stay in core by
|
|
97
|
+
// deliberate decision (see ./verbs/wrangler.ts's module doc): the epic
|
|
98
|
+
// cedes the Workers plane to `wrangler` and there is no cloudflare lexicon
|
|
99
|
+
// for them to live in instead.
|
|
100
|
+
test("family table lists exactly the agnostic starter families plus the Cloudflare apply leaves (#1293)", () => {
|
|
96
101
|
expect(Object.keys(STARTER_VERB_FAMILIES).sort()).toEqual(
|
|
97
|
-
["build", "escapeHatch", "sbom", "supplyChainPolicy", "supplyChainSecurity", "waitVerify"].sort(),
|
|
102
|
+
["build", "cloudflare", "escapeHatch", "sbom", "secrets", "supplyChainPolicy", "supplyChainSecurity", "waitVerify"].sort(),
|
|
98
103
|
);
|
|
99
104
|
});
|
|
100
105
|
|
|
@@ -41,6 +41,10 @@ import {
|
|
|
41
41
|
waitEndpointCapability,
|
|
42
42
|
healthGateCapability,
|
|
43
43
|
shellCapability,
|
|
44
|
+
ensureSecretCapability,
|
|
45
|
+
wranglerDeployCapability,
|
|
46
|
+
wranglerVersionsPromoteCapability,
|
|
47
|
+
r2SyncCapability,
|
|
44
48
|
} from "./verbs/index";
|
|
45
49
|
import { ownPackageVersion, type CapabilityPlugin } from "./capability-plugin";
|
|
46
50
|
|
|
@@ -64,7 +68,16 @@ export const STARTER_VERB_FAMILIES = {
|
|
|
64
68
|
// cloud-specific waits (`wait-for-stack`/`wait-steady-state`/`wait-job`) are
|
|
65
69
|
// contributed by the relevant lexicon (aws) — see docs/components/cloud-boundary.
|
|
66
70
|
waitVerify: ["wait-cluster-healthy", "wait-endpoint", "health-gate"],
|
|
71
|
+
// generated-once secret materialization (#1829, epic #1365): read-then-write,
|
|
72
|
+
// present means done; the k8s store adapter is #1830.
|
|
73
|
+
secrets: ["ensure-secret"],
|
|
67
74
|
escapeHatch: ["shell"],
|
|
75
|
+
// The Cloudflare Workers apply leaves (#1293, epic #1296) — placed in core
|
|
76
|
+
// rather than a cloudflare lexicon (decided explicitly, see ./verbs/wrangler.ts's
|
|
77
|
+
// module doc): the epic cedes the Workers plane to `wrangler` entirely, and
|
|
78
|
+
// there is no cloudflare lexicon for these three verbs to live in. Revisit
|
|
79
|
+
// if/when the zone-plane lexicon (#1294) lands.
|
|
80
|
+
cloudflare: ["wrangler-deploy", "wrangler-versions-promote", "r2-sync"],
|
|
68
81
|
} as const;
|
|
69
82
|
|
|
70
83
|
/**
|
|
@@ -88,7 +101,11 @@ function starterCapabilities(): Array<Capability<never, unknown>> {
|
|
|
88
101
|
waitClusterHealthyCapability,
|
|
89
102
|
waitEndpointCapability,
|
|
90
103
|
healthGateCapability,
|
|
104
|
+
ensureSecretCapability,
|
|
91
105
|
shellCapability,
|
|
106
|
+
wranglerDeployCapability,
|
|
107
|
+
wranglerVersionsPromoteCapability,
|
|
108
|
+
r2SyncCapability,
|
|
92
109
|
] as unknown as Array<Capability<never, unknown>>;
|
|
93
110
|
}
|
|
94
111
|
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ensure-secret` capability surface tests (#1829, epic #1365 decision 3):
|
|
3
|
+
* the verb is the same engine as the `ensureSecret(...)` op builder, its
|
|
4
|
+
* rollback disposition is `needs-opt-out`, and COMP003 fires on a component
|
|
5
|
+
* that uses it without acknowledging the compensation gap — through the same
|
|
6
|
+
* registry-derived policy map `chant lint` builds, not a hard-coded list.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { describe, expect, it, vi } from "vitest";
|
|
10
|
+
import type { DeployContext } from "../capability";
|
|
11
|
+
import { CapabilityNotImplementedError, type RollbackPolicy } from "../capability";
|
|
12
|
+
import { createCapabilityRegistry } from "../registry";
|
|
13
|
+
import { createEnsureSecretCapability, ensureSecretCapability } from "./ensure-secret";
|
|
14
|
+
import {
|
|
15
|
+
SecretMaterial,
|
|
16
|
+
SecretContractMismatchError,
|
|
17
|
+
consumeSecretMaterial,
|
|
18
|
+
type SecretMaterialGenerator,
|
|
19
|
+
type SecretStoreAdapter,
|
|
20
|
+
} from "../../secret-materialization";
|
|
21
|
+
import { loadComponentChecks } from "../../lint/rules/comp";
|
|
22
|
+
|
|
23
|
+
const ctx: DeployContext = { env: "dev", component: "fountain" };
|
|
24
|
+
|
|
25
|
+
function fakeStore(seed?: Record<string, Record<string, string>>) {
|
|
26
|
+
const secrets = new Map<string, Record<string, string>>(Object.entries(seed ?? {}));
|
|
27
|
+
const adapter: SecretStoreAdapter = {
|
|
28
|
+
exists: vi.fn(async (name: string) => secrets.has(name)),
|
|
29
|
+
describe: vi.fn(async (name: string) => ({ keys: Object.keys(secrets.get(name) ?? {}) })),
|
|
30
|
+
create: vi.fn(async (name: string, keys: readonly string[], generate: SecretMaterialGenerator) => {
|
|
31
|
+
const data: Record<string, string> = {};
|
|
32
|
+
for (const key of keys) data[key] = consumeSecretMaterial(await generate(key));
|
|
33
|
+
secrets.set(name, data);
|
|
34
|
+
}),
|
|
35
|
+
};
|
|
36
|
+
return { adapter, secrets };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
describe("ensure-secret capability", () => {
|
|
40
|
+
it("creates an absent secret, then treats present-and-matching as done (create fires once across reruns)", async () => {
|
|
41
|
+
const { adapter } = fakeStore();
|
|
42
|
+
const capability = createEnsureSecretCapability({
|
|
43
|
+
store: adapter,
|
|
44
|
+
generator: (key) => SecretMaterial.mint(`v:${key}`),
|
|
45
|
+
});
|
|
46
|
+
const first = await capability.run(ctx, { name: "app-secret", keys: ["token"] });
|
|
47
|
+
const second = await capability.run(ctx, { name: "app-secret", keys: ["token"] });
|
|
48
|
+
expect(first.outcome).toBe("created");
|
|
49
|
+
expect(second.outcome).toBe("present");
|
|
50
|
+
expect(adapter.create).toHaveBeenCalledTimes(1);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("propagates the loud mismatch from the shared engine", async () => {
|
|
54
|
+
const { adapter } = fakeStore({ "app-secret": { other: "x" } });
|
|
55
|
+
const capability = createEnsureSecretCapability({ store: adapter });
|
|
56
|
+
await expect(capability.run(ctx, { name: "app-secret", keys: ["token"] })).rejects.toThrowError(
|
|
57
|
+
SecretContractMismatchError,
|
|
58
|
+
);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("output carries names only — no field of the outcome can hold material", async () => {
|
|
62
|
+
const { adapter } = fakeStore();
|
|
63
|
+
const capability = createEnsureSecretCapability({
|
|
64
|
+
store: adapter,
|
|
65
|
+
generator: (key) => SecretMaterial.mint(`v:${key}`),
|
|
66
|
+
});
|
|
67
|
+
const outcome = await capability.run(ctx, { name: "app-secret", keys: ["token"] });
|
|
68
|
+
expect(outcome).toEqual({ outcome: "created", name: "app-secret", keys: ["token"] });
|
|
69
|
+
expect(JSON.stringify(outcome)).not.toContain("v:token");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("is registered in the starter set as a typed stub until a provider row (#1830) wires an adapter", async () => {
|
|
73
|
+
const registry = createCapabilityRegistry();
|
|
74
|
+
const starter = registry.resolve("ensure-secret");
|
|
75
|
+
expect(starter.rollbackPolicy).toBe("needs-opt-out");
|
|
76
|
+
await expect(starter.run(ctx as never, {} as never)).rejects.toThrowError(CapabilityNotImplementedError);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe("COMP003 over ensure-secret", () => {
|
|
81
|
+
/** The same derivation chant lint's resolveRegistryContext performs. */
|
|
82
|
+
function registryRollbackPolicies(): Map<string, RollbackPolicy> {
|
|
83
|
+
const registry = createCapabilityRegistry();
|
|
84
|
+
const policies = new Map<string, RollbackPolicy>();
|
|
85
|
+
for (const kind of registry.kinds()) {
|
|
86
|
+
const capability = registry.resolve(kind);
|
|
87
|
+
policies.set(kind, capability.rollbackPolicy ?? (capability.rollback ? "native" : "none-by-design"));
|
|
88
|
+
}
|
|
89
|
+
return policies;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const [comp003] = loadComponentChecks().filter((c) => c.id === "COMP003");
|
|
93
|
+
|
|
94
|
+
function checkComponent(step: Record<string, unknown>) {
|
|
95
|
+
const checkCtx = {
|
|
96
|
+
rollbackPolicies: registryRollbackPolicies(),
|
|
97
|
+
components: new Map([
|
|
98
|
+
[
|
|
99
|
+
"fountain",
|
|
100
|
+
{
|
|
101
|
+
component: {
|
|
102
|
+
name: "fountain",
|
|
103
|
+
dependsOn: [],
|
|
104
|
+
deploy: [{ phase: "Secrets", steps: [step] }],
|
|
105
|
+
},
|
|
106
|
+
filePath: "fountain.component.ts",
|
|
107
|
+
},
|
|
108
|
+
],
|
|
109
|
+
]),
|
|
110
|
+
};
|
|
111
|
+
return comp003.check(checkCtx as never);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
it("flags a bare ensure-secret step — minting has no safe undo, so the gap needs an explicit opt-out", () => {
|
|
115
|
+
const diagnostics = checkComponent({ kind: "ensure-secret", name: "master-key", keys: ["MASTER_SECRETS_KEY"] });
|
|
116
|
+
expect(diagnostics).toHaveLength(1);
|
|
117
|
+
expect(diagnostics[0].checkId).toBe("COMP003");
|
|
118
|
+
expect(diagnostics[0].message).toContain("ensure-secret");
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("accepts the step once the component acknowledges the gap via noRollback", () => {
|
|
122
|
+
const diagnostics = checkComponent({
|
|
123
|
+
kind: "ensure-secret",
|
|
124
|
+
name: "master-key",
|
|
125
|
+
keys: ["MASTER_SECRETS_KEY"],
|
|
126
|
+
noRollback: "generated-once: un-creating would destroy the only copy of the material",
|
|
127
|
+
});
|
|
128
|
+
expect(diagnostics).toHaveLength(0);
|
|
129
|
+
});
|
|
130
|
+
});
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* secrets family — `ensure-secret` (#1829, epic #1365 decision 3). The
|
|
3
|
+
* capability surface over the one generated-once materialization engine
|
|
4
|
+
* (../../secret-materialization.ts); the op step builder `ensureSecret(...)`
|
|
5
|
+
* (../../op/builders.ts) is the other surface over the same engine.
|
|
6
|
+
*
|
|
7
|
+
* Contract (read-then-write): present means done. The verb never mints over
|
|
8
|
+
* an existing value and never rotates implicitly; an existing secret that
|
|
9
|
+
* does not match the declared key-set/metadata stops the apply with a
|
|
10
|
+
* `SecretContractMismatchError` naming key names and metadata keys — never
|
|
11
|
+
* values. There is no safe undo for minting a secret others may already have
|
|
12
|
+
* consumed, and un-creating it would destroy the only copy of the material —
|
|
13
|
+
* so `rollbackPolicy: "needs-opt-out"`: COMP003 requires a component using
|
|
14
|
+
* this verb to acknowledge the compensation gap explicitly.
|
|
15
|
+
*
|
|
16
|
+
* The store adapter is per-provider (#1830 is the k8s row). Core registers a
|
|
17
|
+
* typed stub; a provider builds the real capability with
|
|
18
|
+
* {@link createEnsureSecretCapability}.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import type { Capability } from "../capability";
|
|
22
|
+
import { CapabilityNotImplementedError } from "../capability";
|
|
23
|
+
import {
|
|
24
|
+
ensureSecretMaterialization,
|
|
25
|
+
defaultSecretMaterialGenerator,
|
|
26
|
+
type EnsureSecretOutcome,
|
|
27
|
+
type SecretMaterialGenerator,
|
|
28
|
+
type SecretStoreAdapter,
|
|
29
|
+
} from "../../secret-materialization";
|
|
30
|
+
|
|
31
|
+
export interface EnsureSecretInput {
|
|
32
|
+
/** The secret's name as the store knows it. */
|
|
33
|
+
name: string;
|
|
34
|
+
/** The declared key-set. Creation mints one value per key; verification compares names only. */
|
|
35
|
+
keys: string[];
|
|
36
|
+
/** Declared metadata an existing secret must carry. Mismatches are reported by KEY. */
|
|
37
|
+
metadata?: Record<string, string>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Names only — no field of the output can carry material. */
|
|
41
|
+
export type EnsureSecretOutput = EnsureSecretOutcome;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Build the real `ensure-secret` capability over a provider's
|
|
45
|
+
* {@link SecretStoreAdapter}. The generator seam defaults to 32 CSPRNG bytes
|
|
46
|
+
* per key; the material it mints flows straight through the adapter to the
|
|
47
|
+
* store and is never returned, logged, or retained (see
|
|
48
|
+
* ../../secret-materialization.ts).
|
|
49
|
+
*/
|
|
50
|
+
export function createEnsureSecretCapability(deps: {
|
|
51
|
+
store: SecretStoreAdapter;
|
|
52
|
+
generator?: SecretMaterialGenerator;
|
|
53
|
+
}): Capability<EnsureSecretInput, EnsureSecretOutput> {
|
|
54
|
+
return {
|
|
55
|
+
kind: "ensure-secret",
|
|
56
|
+
rollbackPolicy: "needs-opt-out",
|
|
57
|
+
async run(_ctx, input) {
|
|
58
|
+
return ensureSecretMaterialization(
|
|
59
|
+
deps.store,
|
|
60
|
+
{ name: input.name, keys: input.keys, metadata: input.metadata },
|
|
61
|
+
deps.generator ?? defaultSecretMaterialGenerator,
|
|
62
|
+
);
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The starter-set registration: fully typed, correct `rollbackPolicy` for
|
|
69
|
+
* COMP003, but no store adapter — running it throws
|
|
70
|
+
* `CapabilityNotImplementedError` until a provider row (#1830, k8s) wires a
|
|
71
|
+
* real adapter via {@link createEnsureSecretCapability}.
|
|
72
|
+
*/
|
|
73
|
+
export const ensureSecretCapability: Capability<EnsureSecretInput, EnsureSecretOutput> = {
|
|
74
|
+
kind: "ensure-secret",
|
|
75
|
+
rollbackPolicy: "needs-opt-out",
|
|
76
|
+
async run(): Promise<EnsureSecretOutput> {
|
|
77
|
+
throw new CapabilityNotImplementedError("ensure-secret");
|
|
78
|
+
},
|
|
79
|
+
};
|
|
@@ -26,6 +26,15 @@
|
|
|
26
26
|
* `ProvenanceLink` material. `verify` (#622, ./verify.ts) is the matching
|
|
27
27
|
* deploy-time gate — `cosign verify`/`verify-attestation` against a
|
|
28
28
|
* configured identity policy, mirroring ../../lint/policy.ts's `policyGate`.
|
|
29
|
+
* `wrangler-deploy`/`wrangler-versions-promote` (#1293, epic #1296,
|
|
30
|
+
* ./wrangler.ts) and `r2-sync` (#1293, ./r2-sync.ts) are the Cloudflare
|
|
31
|
+
* Workers apply leaves — real implementations over the same injectable
|
|
32
|
+
* `ProcessRunner` `sign` uses, shelling to `wrangler`/`rclone` rather than an
|
|
33
|
+
* AWS-shaped SDK client. `run-agent` (#1941, epic #1564 phase 1, ./run-agent.ts)
|
|
34
|
+
* is the typed schema + registry entry for a bounded, attested agent-turn
|
|
35
|
+
* capability over a fly Sprite — a typed stub for now (`run`/`rollback`
|
|
36
|
+
* reject with `CapabilityNotImplementedError`), `rollbackPolicy: "native"`
|
|
37
|
+
* declared up front; the real sprite-lifecycle implementation is #1942.
|
|
29
38
|
*/
|
|
30
39
|
|
|
31
40
|
export * from "./build-archive";
|
|
@@ -39,11 +48,15 @@ export * from "./component-bom";
|
|
|
39
48
|
export * from "./reproducibility";
|
|
40
49
|
export * from "./sign";
|
|
41
50
|
export * from "./verify";
|
|
51
|
+
export * from "./run-agent";
|
|
52
|
+
export * from "./wrangler";
|
|
53
|
+
export * from "./r2-sync";
|
|
42
54
|
export * from "./vuln-scan";
|
|
43
55
|
export * from "./vex";
|
|
44
56
|
export * from "./license-policy";
|
|
45
57
|
export * from "./vuln-gate";
|
|
46
58
|
export * from "./wait-verify";
|
|
47
59
|
export * from "./shell";
|
|
60
|
+
export * from "./ensure-secret";
|
|
48
61
|
export * from "./cloud-executor";
|
|
49
62
|
export * from "./process-runner";
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests `r2-sync` (#1293, epic #1296, ./r2-sync.ts) against a
|
|
3
|
+
* `MockProcessRunner` — no live `rclone`, no network call, ever. Asserts the
|
|
4
|
+
* `r2://` destination shape mirrors `s3-sync`'s `s3://` shape, the
|
|
5
|
+
* copy-vs-sync dispatch on `delete`, the uploaded/deleted count parsing, and
|
|
6
|
+
* the `needs-opt-out` rollback disposition `s3-sync` also carries.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { describe, expect, it } from "vitest";
|
|
10
|
+
import {
|
|
11
|
+
buildR2SyncArgs,
|
|
12
|
+
createR2SyncCapability,
|
|
13
|
+
parseRcloneStats,
|
|
14
|
+
R2SyncInvalidDestinationError,
|
|
15
|
+
toRcloneDest,
|
|
16
|
+
} from "./r2-sync";
|
|
17
|
+
import { ToolNotAvailableError } from "./process-runner";
|
|
18
|
+
import { createMockProcessRunner } from "./__tests__/mock-process-runner";
|
|
19
|
+
|
|
20
|
+
const ctx = { env: "prod", component: "assets" };
|
|
21
|
+
|
|
22
|
+
describe("toRcloneDest", () => {
|
|
23
|
+
it("translates r2://bucket/prefix to the default 'r2' rclone remote", () => {
|
|
24
|
+
expect(toRcloneDest("r2://my-bucket/assets")).toBe("r2:my-bucket/assets");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("translates a bare bucket with no prefix", () => {
|
|
28
|
+
expect(toRcloneDest("r2://my-bucket")).toBe("r2:my-bucket");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("honours an explicit remote name override", () => {
|
|
32
|
+
expect(toRcloneDest("r2://my-bucket/assets", "cf-r2")).toBe("cf-r2:my-bucket/assets");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("throws R2SyncInvalidDestinationError for a non-r2:// URI (e.g. an s3:// one passed by mistake)", () => {
|
|
36
|
+
expect(() => toRcloneDest("s3://my-bucket/assets")).toThrow(R2SyncInvalidDestinationError);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe("buildR2SyncArgs", () => {
|
|
41
|
+
it("uses 'rclone copy' by default — never deletes destination-only keys, matching s3-sync's default", () => {
|
|
42
|
+
const cmd = buildR2SyncArgs({ from: "dist/assets", to: "r2://my-bucket/assets" });
|
|
43
|
+
expect(cmd).toBe(`rclone copy 'dist/assets' 'r2:my-bucket/assets' -v`);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("uses 'rclone sync' when delete: true — matching s3-sync's opt-in delete", () => {
|
|
47
|
+
const cmd = buildR2SyncArgs({ from: "dist/assets", to: "r2://my-bucket/assets", delete: true });
|
|
48
|
+
expect(cmd).toBe(`rclone sync 'dist/assets' 'r2:my-bucket/assets' -v`);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe("parseRcloneStats", () => {
|
|
53
|
+
it("counts new and replaced uploads together as 'uploaded'", () => {
|
|
54
|
+
const stdout = [
|
|
55
|
+
"2026/08/24 10:00:00 INFO : a.txt: Copied (new)",
|
|
56
|
+
"2026/08/24 10:00:00 INFO : b.txt: Copied (replaced existing)",
|
|
57
|
+
"2026/08/24 10:00:00 INFO : c.txt: Copied (new)",
|
|
58
|
+
].join("\n");
|
|
59
|
+
expect(parseRcloneStats(stdout)).toEqual({ uploaded: 3, deleted: 0 });
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("counts deletions separately", () => {
|
|
63
|
+
const stdout = [
|
|
64
|
+
"2026/08/24 10:00:00 INFO : a.txt: Copied (new)",
|
|
65
|
+
"2026/08/24 10:00:00 INFO : stale.txt: Deleted",
|
|
66
|
+
].join("\n");
|
|
67
|
+
expect(parseRcloneStats(stdout)).toEqual({ uploaded: 1, deleted: 1 });
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("returns zeros for a no-op sync", () => {
|
|
71
|
+
expect(parseRcloneStats("2026/08/24 10:00:00 INFO : There was nothing to transfer\n")).toEqual({
|
|
72
|
+
uploaded: 0,
|
|
73
|
+
deleted: 0,
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe("r2-sync capability", () => {
|
|
79
|
+
it("round-trips a directory upload and reports the object count", async () => {
|
|
80
|
+
const mock = createMockProcessRunner({
|
|
81
|
+
responses: {
|
|
82
|
+
"rclone copy": "2026/08/24 10:00:00 INFO : a.txt: Copied (new)\n2026/08/24 10:00:00 INFO : b.txt: Copied (new)\n",
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
const capability = createR2SyncCapability(mock.runner);
|
|
86
|
+
|
|
87
|
+
const output = await capability.run(ctx, { from: "dist/assets", to: "r2://my-bucket/assets" });
|
|
88
|
+
|
|
89
|
+
expect(output).toEqual({ uploaded: 2, deleted: 0 });
|
|
90
|
+
expect(mock.calls.some((c) => c.command === `rclone copy 'dist/assets' 'r2:my-bucket/assets' -v`)).toBe(true);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("throws ToolNotAvailableError when rclone is absent, rather than silently skipping the sync", async () => {
|
|
94
|
+
const mock = createMockProcessRunner({ tools: { rclone: false } });
|
|
95
|
+
const capability = createR2SyncCapability(mock.runner);
|
|
96
|
+
|
|
97
|
+
await expect(capability.run(ctx, { from: "dist/assets", to: "r2://my-bucket/assets" })).rejects.toBeInstanceOf(
|
|
98
|
+
ToolNotAvailableError,
|
|
99
|
+
);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("declares rollbackPolicy: needs-opt-out — a mutating, destructive-capable sync with no native undo, same as s3-sync", () => {
|
|
103
|
+
const capability = createR2SyncCapability(createMockProcessRunner().runner);
|
|
104
|
+
expect(capability.rollbackPolicy).toBe("needs-opt-out");
|
|
105
|
+
expect(capability.rollback).toBeUndefined();
|
|
106
|
+
});
|
|
107
|
+
});
|