@aiwg/cli 2026.9.20 → 2026.9.21
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/agentic/code/providers/capability-matrix.yaml +58 -6
- package/agentic/code/providers/model-capabilities.v1.json +34 -0
- package/agentic/code/providers/model-catalog.v1.json +55 -13
- package/bin/aiwg.mjs +2 -0
- package/dist/src/api/index.d.ts +1 -0
- package/dist/src/api/index.js +1 -0
- package/dist/src/artifacts/enrichment/cli.js +1 -1
- package/dist/src/artifacts/views/cli.js +1 -1
- package/dist/src/audit/operator-decision.js +10 -0
- package/dist/src/auth/credential-store.js +127 -27
- package/dist/src/catalog/builtin-models.json +272 -40
- package/dist/src/catalog/cli.js +1 -1
- package/dist/src/catalog/cli.mjs +1 -1
- package/dist/src/cli/handlers/effect.js +872 -0
- package/dist/src/cli/handlers/help.js +2 -0
- package/dist/src/cli/handlers/index.js +4 -2
- package/dist/src/cli/handlers/models.js +41 -10
- package/dist/src/cli/handlers/ralph.js +1 -1
- package/dist/src/cli/handlers/sessions.js +113 -37
- package/dist/src/cli/handlers/subcommands.js +31 -0
- package/dist/src/cli/handlers/use.js +46 -9
- package/dist/src/cli/handlers/utilities.js +10 -0
- package/dist/src/cli/handlers/workspace.js +3 -1
- package/dist/src/cli/scope-resolver.js +112 -9
- package/dist/src/cli/services/deployment-verification.js +5 -0
- package/dist/src/config/aiwg-config.js +3 -0
- package/dist/src/decision/acceptance-rollout.js +93 -0
- package/dist/src/decision/acceptance.js +175 -0
- package/dist/src/decision/adapters/jev.js +234 -13
- package/dist/src/decision/adapters/llm-subagent.js +60 -12
- package/dist/src/decision/admission.js +550 -0
- package/dist/src/decision/batch-benchmark.js +47 -0
- package/dist/src/decision/batch-receipts/accounting.js +75 -0
- package/dist/src/decision/batch-receipts/export.js +40 -0
- package/dist/src/decision/batch-receipts/index.js +9 -0
- package/dist/src/decision/batch-receipts/protection.js +40 -0
- package/dist/src/decision/batch-receipts/receipt.js +44 -0
- package/dist/src/decision/batch-receipts/result-store.js +341 -0
- package/dist/src/decision/batch-receipts/store.js +362 -0
- package/dist/src/decision/batch-receipts/types.js +2 -0
- package/dist/src/decision/batch-receipts/validate.js +127 -0
- package/dist/src/decision/batch.js +68 -0
- package/dist/src/decision/calibration/index.js +4 -0
- package/dist/src/decision/calibration/receipts.js +130 -0
- package/dist/src/decision/calibration/registry.js +258 -0
- package/dist/src/decision/calibration/types.js +2 -0
- package/dist/src/decision/compile-cache/benchmark.js +62 -0
- package/dist/src/decision/compile-cache/compatibility.js +65 -0
- package/dist/src/decision/compile-cache/file-store.js +280 -0
- package/dist/src/decision/compile-cache/identity.js +26 -0
- package/dist/src/decision/compile-cache/index.js +9 -0
- package/dist/src/decision/compile-cache/prefix.js +21 -0
- package/dist/src/decision/compile-cache/records.js +86 -0
- package/dist/src/decision/compile-cache/runtime.js +68 -0
- package/dist/src/decision/compile-cache/store.js +108 -0
- package/dist/src/decision/compile-cache/types.js +2 -0
- package/dist/src/decision/context-plan.js +234 -0
- package/dist/src/decision/context-qualification.js +60 -0
- package/dist/src/decision/ensemble/aggregate.js +215 -0
- package/dist/src/decision/ensemble/contract.js +316 -0
- package/dist/src/decision/ensemble/drift.js +51 -0
- package/dist/src/decision/ensemble/index.js +6 -0
- package/dist/src/decision/ensemble/report.js +71 -0
- package/dist/src/decision/ensemble/types.js +2 -0
- package/dist/src/decision/evaluate.js +1024 -78
- package/dist/src/decision/file-lifecycle-store.js +168 -0
- package/dist/src/decision/graph-beam.js +40 -0
- package/dist/src/decision/graph-budget.js +110 -0
- package/dist/src/decision/graph-decision-bridge.js +93 -0
- package/dist/src/decision/graph-evidence.js +123 -0
- package/dist/src/decision/graph-flow-adapter.js +56 -0
- package/dist/src/decision/graph-flow.js +105 -0
- package/dist/src/decision/graph-parallel.js +79 -0
- package/dist/src/decision/graph-patterns.js +20 -0
- package/dist/src/decision/graph-run-store.js +66 -0
- package/dist/src/decision/graph-run.js +166 -0
- package/dist/src/decision/graph-skill-bridge.js +91 -0
- package/dist/src/decision/graph-templates.js +56 -0
- package/dist/src/decision/graph.js +115 -0
- package/dist/src/decision/index.js +35 -0
- package/dist/src/decision/job-accounting.js +93 -0
- package/dist/src/decision/job-contract.js +186 -0
- package/dist/src/decision/job-effects.js +95 -0
- package/dist/src/decision/job-evaluate.js +80 -0
- package/dist/src/decision/job-gateway.js +121 -0
- package/dist/src/decision/job-http.js +194 -0
- package/dist/src/decision/job-payload-store.js +258 -0
- package/dist/src/decision/job-poll-limiter.js +92 -0
- package/dist/src/decision/job-quota.js +158 -0
- package/dist/src/decision/job-runtime.js +240 -0
- package/dist/src/decision/job-scheduler.js +26 -0
- package/dist/src/decision/job-service.js +117 -0
- package/dist/src/decision/job-store.js +264 -0
- package/dist/src/decision/job-worker.js +141 -0
- package/dist/src/decision/lifecycle.js +148 -0
- package/dist/src/decision/patterns/artifacts.js +173 -0
- package/dist/src/decision/patterns/catalog.js +108 -0
- package/dist/src/decision/patterns/durable-review.js +237 -0
- package/dist/src/decision/patterns/index.js +8 -0
- package/dist/src/decision/patterns/playground.js +401 -0
- package/dist/src/decision/patterns/recorded.js +43 -0
- package/dist/src/decision/patterns/specs.js +174 -0
- package/dist/src/decision/patterns/types.js +2 -0
- package/dist/src/decision/portable-secrets.js +53 -0
- package/dist/src/decision/projection.js +210 -0
- package/dist/src/decision/qualification/capture.js +67 -0
- package/dist/src/decision/qualification/drift.js +68 -0
- package/dist/src/decision/qualification/gate-evidence.js +172 -0
- package/dist/src/decision/qualification/gates.js +83 -0
- package/dist/src/decision/qualification/index.js +12 -0
- package/dist/src/decision/qualification/load.js +275 -0
- package/dist/src/decision/qualification/manifest.js +37 -0
- package/dist/src/decision/qualification/privacy.js +41 -0
- package/dist/src/decision/qualification/quality.js +213 -0
- package/dist/src/decision/qualification/release.js +116 -0
- package/dist/src/decision/qualification/runner.js +408 -0
- package/dist/src/decision/qualification/types.js +2 -0
- package/dist/src/decision/receipts.js +30 -7
- package/dist/src/decision/result-cache/index.js +6 -0
- package/dist/src/decision/result-cache/integrity.js +28 -0
- package/dist/src/decision/result-cache/key.js +28 -0
- package/dist/src/decision/result-cache/service.js +92 -0
- package/dist/src/decision/result-cache/store.js +327 -0
- package/dist/src/decision/result-cache/types.js +3 -0
- package/dist/src/decision/review/authorization.js +110 -0
- package/dist/src/decision/review/effect-ledger-adapter.js +321 -0
- package/dist/src/decision/review/index.js +12 -0
- package/dist/src/decision/review/migrate.js +34 -0
- package/dist/src/decision/review/operator-audit.js +59 -0
- package/dist/src/decision/review/recovery-adapters.js +252 -0
- package/dist/src/decision/review/recovery.js +33 -0
- package/dist/src/decision/review/service.js +498 -0
- package/dist/src/decision/review/store.js +219 -0
- package/dist/src/decision/review/types.js +2 -0
- package/dist/src/decision/review/validate.js +205 -0
- package/dist/src/decision/scheduler.js +115 -0
- package/dist/src/decision/telemetry/context.js +42 -0
- package/dist/src/decision/telemetry/debug-sidecar.js +152 -0
- package/dist/src/decision/telemetry/exporter.js +77 -0
- package/dist/src/decision/telemetry/file-debug-backend.js +112 -0
- package/dist/src/decision/telemetry/index.js +13 -0
- package/dist/src/decision/telemetry/mapping.js +146 -0
- package/dist/src/decision/telemetry/metrics.js +164 -0
- package/dist/src/decision/telemetry/otlp-http.js +183 -0
- package/dist/src/decision/telemetry/redaction.js +93 -0
- package/dist/src/decision/telemetry/retention.js +109 -0
- package/dist/src/decision/telemetry/runtime.js +310 -0
- package/dist/src/decision/telemetry/trace.js +158 -0
- package/dist/src/decision/telemetry/types.js +3 -0
- package/dist/src/decision/validate.js +135 -5
- package/dist/src/effects/checkpoint-sinks.js +94 -0
- package/dist/src/effects/checkpoint.js +76 -0
- package/dist/src/effects/decision-links.js +54 -0
- package/dist/src/effects/errors.js +48 -0
- package/dist/src/effects/identity.js +178 -0
- package/dist/src/effects/index.js +25 -0
- package/dist/src/effects/keyring.js +160 -0
- package/dist/src/effects/keys.js +130 -0
- package/dist/src/effects/ledger.js +435 -0
- package/dist/src/effects/lifecycle.js +110 -0
- package/dist/src/effects/lock-recovery.js +269 -0
- package/dist/src/effects/reader.js +103 -0
- package/dist/src/effects/records.js +114 -0
- package/dist/src/effects/redaction.js +51 -0
- package/dist/src/effects/schema.js +73 -0
- package/dist/src/effects/store.js +239 -0
- package/dist/src/effects/types.js +49 -0
- package/dist/src/effects/verifiers/decision.js +168 -0
- package/dist/src/effects/verifiers/file.js +106 -0
- package/dist/src/effects/verifiers/git.js +206 -0
- package/dist/src/effects/verifiers/index.js +90 -0
- package/dist/src/effects/verifiers/registry.js +148 -0
- package/dist/src/effects/verifiers/review.js +123 -0
- package/dist/src/effects/verifiers/tracker.js +299 -0
- package/dist/src/effects/verifiers/types.js +21 -0
- package/dist/src/effects/verify.js +295 -0
- package/dist/src/evidence/bundle.js +2 -0
- package/dist/src/extensions/commands/definitions.js +28 -0
- package/dist/src/extensions/manifest.js +1 -0
- package/dist/src/marketplace/provenance.js +25 -27
- package/dist/src/mcp/cli.mjs +11 -1
- package/dist/src/mcp/grok-build-config.mjs +189 -0
- package/dist/src/mcp/registry.js +21 -1
- package/dist/src/mcp/registry.mjs +20 -1
- package/dist/src/models/grok-build-models.js +356 -0
- package/dist/src/models/index.js +1 -0
- package/dist/src/models/model-capabilities.v1.json +34 -0
- package/dist/src/models/model-catalog.v1.json +55 -13
- package/dist/src/models/model-discovery.js +41 -5
- package/dist/src/models/provider-policy.js +3 -2
- package/dist/src/plugin/skill-command-translator.js +1 -0
- package/dist/src/providers/capability-matrix.yaml +58 -6
- package/dist/src/providers/grok-build-acp.js +192 -0
- package/dist/src/providers/grok-build-dispatch.js +251 -0
- package/dist/src/providers/grok-build-headless.js +170 -0
- package/dist/src/providers/muse-paths.js +105 -0
- package/dist/src/providers/provider-definitions.js +93 -7
- package/dist/src/providers/provider-definitions.mjs +21 -1
- package/dist/src/providers/provider-inventory.js +3 -0
- package/dist/src/security/artifact-attestation.js +4 -8
- package/dist/src/security/signing.js +119 -0
- package/dist/src/sessions/adapters/grok-build.js +187 -0
- package/dist/src/sessions/adapters/muse.js +412 -0
- package/dist/src/sessions/contracts.js +1 -1
- package/dist/src/sessions/grok-build-cli.js +196 -0
- package/dist/src/sessions/index.js +3 -0
- package/dist/src/sessions/workspace-discovery.js +9 -5
- package/dist/src/skills/deployer.js +33 -5
- package/dist/src/smiths/context-pipeline/claude-hook.js +2 -0
- package/dist/src/smiths/context-pipeline/context-file-safety.js +18 -0
- package/dist/src/smiths/context-pipeline/finalization.js +1 -32
- package/dist/src/smiths/context-pipeline/generator.js +12 -0
- package/dist/src/smiths/context-pipeline/index.js +1 -1
- package/dist/src/smiths/context-pipeline/managed-hook.js +2 -0
- package/dist/src/smiths/context-pipeline/muse-bridge.js +50 -0
- package/dist/src/smiths/context-pipeline/provider-policy.js +5 -0
- package/dist/src/smiths/context-pipeline/workspace-context.js +22 -16
- package/dist/src/smiths/hook-bridge/grok-build-translator.js +64 -0
- package/dist/src/smiths/hook-bridge/index.js +3 -0
- package/dist/src/smiths/hook-bridge/shim.js +2 -0
- package/dist/src/smiths/hook-bridge/types.js +5 -0
- package/dist/src/smiths/skillsmith/collision-detector.js +1 -0
- package/dist/src/storage/protected-files.js +102 -0
- package/dist/src/tracker/forge-http.js +371 -0
- package/dist/src/tracker/project-inputs.js +41 -0
- package/package.json +2 -1
- package/schemas/decision/BatchReceipt.v1.schema.json +50 -0
- package/schemas/decision/CalibrationArtifact.v1.schema.json +76 -0
- package/schemas/decision/CalibrationCompatibility.v1.schema.json +18 -0
- package/schemas/decision/CalibrationGovernanceReceipt.v1.schema.json +44 -0
- package/schemas/decision/DecisionAdmissionEvidence.v1.schema.json +43 -0
- package/schemas/decision/DecisionBinding.schema.json +309 -0
- package/schemas/decision/DecisionBinding.v1alpha2.schema.json +369 -0
- package/schemas/decision/DecisionCacheTelemetry.v1.schema.json +20 -0
- package/schemas/decision/DecisionChampionChallenger.v1.schema.json +100 -0
- package/schemas/decision/DecisionCompileCacheEntry.v1.schema.json +35 -0
- package/schemas/decision/DecisionCompileCacheIdentity.v1.schema.json +26 -0
- package/schemas/decision/DecisionCompileCacheTombstone.v1.schema.json +31 -0
- package/schemas/decision/DecisionContextEvidence.v1.schema.json +136 -0
- package/schemas/decision/DecisionDefinition.schema.json +159 -0
- package/schemas/decision/DecisionDefinition.v1alpha2.schema.json +203 -0
- package/schemas/decision/DecisionDriftResponse.v1.schema.json +55 -0
- package/schemas/decision/DecisionEnsembleAggregate.v1.schema.json +86 -0
- package/schemas/decision/DecisionEnsembleIntegrityReport.v1.schema.json +64 -0
- package/schemas/decision/DecisionEnsemblePolicy.v1.schema.json +130 -0
- package/schemas/decision/DecisionGraph.v1.schema.json +77 -0
- package/schemas/decision/DecisionJob.v1.schema.json +71 -0
- package/schemas/decision/DecisionLifecyclePolicy.v1.schema.json +52 -0
- package/schemas/decision/DecisionProjectionPolicy.v1.schema.json +65 -0
- package/schemas/decision/DecisionProviderPrefixCompatibility.v1.schema.json +45 -0
- package/schemas/decision/DecisionProviderPrefixEvidence.v1.schema.json +17 -0
- package/schemas/decision/DecisionProviderPrefixIdentity.v1.schema.json +36 -0
- package/schemas/decision/DecisionResult.schema.json +524 -0
- package/schemas/decision/DecisionResult.v1alpha2.schema.json +619 -0
- package/schemas/decision/DecisionResultCacheEntry.v1.schema.json +53 -0
- package/schemas/decision/DecisionReview.schema.json +29 -0
- package/schemas/decision/DecisionRuleset.schema.json +313 -0
- package/schemas/decision/DecisionRuleset.v1alpha2.schema.json +313 -0
- package/schemas/decision/DecisionTelemetryTrace.v1.schema.json +95 -0
- package/schemas/decision/RulesetResult.schema.json +721 -0
- package/schemas/decision/RulesetResult.v1alpha2.schema.json +891 -0
- package/schemas/security/pr-evidence-receipt.v1.schema.json +77 -0
- package/schemas/security/threat-assessment-report.v1.schema.json +71 -1
- package/tools/agents/deploy-agents.mjs +21 -6
- package/tools/agents/providers/base.mjs +7 -4
- package/tools/agents/providers/codex.mjs +1 -1
- package/tools/agents/providers/grok-build.d.mts +7 -0
- package/tools/agents/providers/grok-build.mjs +249 -9
- package/tools/agents/providers/model-role.mjs +6 -3
- package/tools/agents/providers/muse-hooks.mjs +785 -0
- package/tools/agents/providers/muse-paths.mjs +112 -0
- package/tools/agents/providers/muse.mjs +336 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Independent checkpoint sinks. A local chain alone cannot detect deletion of
|
|
3
|
+
* a whole segment or of its tail, so every checkpoint is also published to a
|
|
4
|
+
* sink the ledger directory does not control.
|
|
5
|
+
*
|
|
6
|
+
* - `gitRefCheckpointSink` (the default) stores the canonical checkpoint as a
|
|
7
|
+
* git blob and moves `refs/aiwg/effects/<subsystem>/checkpoint` to it with a
|
|
8
|
+
* compare-and-swap `update-ref`.
|
|
9
|
+
* - `memoryCheckpointSink` is for tests and embedding hosts.
|
|
10
|
+
* - Other sinks (for example the #1567 operator-decision audit) implement the
|
|
11
|
+
* same `CheckpointSink` port.
|
|
12
|
+
*/
|
|
13
|
+
import { spawn } from 'node:child_process';
|
|
14
|
+
import { canonicalJson } from '../security/artifact-trust.js';
|
|
15
|
+
import { EffectLedgerError } from './errors.js';
|
|
16
|
+
const sinkError = (message) => new EffectLedgerError('internal', message, 'checkpoint-sink-unavailable');
|
|
17
|
+
function defaultGitRunner(repoDir) {
|
|
18
|
+
return (args, stdin = '') => new Promise((resolve, reject) => {
|
|
19
|
+
const child = spawn('git', ['-C', repoDir, ...args], { stdio: ['pipe', 'pipe', 'ignore'], windowsHide: true, env: { ...process.env, GIT_TERMINAL_PROMPT: '0' } });
|
|
20
|
+
const chunks = [];
|
|
21
|
+
child.stdout.on('data', (chunk) => chunks.push(chunk));
|
|
22
|
+
child.once('error', reject);
|
|
23
|
+
child.stdin.once('error', () => { });
|
|
24
|
+
child.once('close', code => resolve({ stdout: Buffer.concat(chunks).toString('utf8'), exitCode: code ?? 1 }));
|
|
25
|
+
child.stdin.end(stdin);
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
export function gitRefCheckpointSink(options) {
|
|
29
|
+
const ref = options.ref ?? `refs/aiwg/effects/${options.subsystem}/checkpoint`;
|
|
30
|
+
if (!/^refs\/[A-Za-z0-9._/-]+$/.test(ref) || ref.includes('..'))
|
|
31
|
+
throw new EffectLedgerError('usage', 'Invalid checkpoint ref', 'invalid-ref');
|
|
32
|
+
const git = options.runner ?? defaultGitRunner(options.repoDir);
|
|
33
|
+
const current = async () => {
|
|
34
|
+
const result = await git(['rev-parse', '-q', '--verify', ref]);
|
|
35
|
+
return result.exitCode === 0 ? result.stdout.trim() : null;
|
|
36
|
+
};
|
|
37
|
+
return {
|
|
38
|
+
name: 'git-ref',
|
|
39
|
+
async publish(checkpoint) {
|
|
40
|
+
let previous;
|
|
41
|
+
try {
|
|
42
|
+
previous = await current();
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
throw sinkError('Git checkpoint sink is unavailable');
|
|
46
|
+
}
|
|
47
|
+
const stored = await git(['hash-object', '-w', '--stdin'], `${canonicalJson(checkpoint)}\n`).catch(() => null);
|
|
48
|
+
const oid = stored?.stdout.trim();
|
|
49
|
+
if (!stored || stored.exitCode !== 0 || !oid || !/^[a-f0-9]{40,64}$/.test(oid))
|
|
50
|
+
throw sinkError('Git checkpoint sink could not store the checkpoint');
|
|
51
|
+
const moved = await git(['update-ref', '-m', `aiwg effect checkpoint ${checkpoint.sequence}`, ref, oid, previous ?? '']).catch(() => null);
|
|
52
|
+
if (!moved || moved.exitCode !== 0)
|
|
53
|
+
throw sinkError('Git checkpoint sink ref moved concurrently or could not be updated');
|
|
54
|
+
return { sink: 'git-ref', reference: `${ref}@${oid}` };
|
|
55
|
+
},
|
|
56
|
+
async latest() {
|
|
57
|
+
let head;
|
|
58
|
+
try {
|
|
59
|
+
head = await current();
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
throw sinkError('Git checkpoint sink is unavailable');
|
|
63
|
+
}
|
|
64
|
+
if (!head)
|
|
65
|
+
return null;
|
|
66
|
+
const blob = await git(['cat-file', 'blob', head]).catch(() => null);
|
|
67
|
+
if (!blob || blob.exitCode !== 0)
|
|
68
|
+
throw sinkError('Git checkpoint sink ref is unreadable');
|
|
69
|
+
try {
|
|
70
|
+
return JSON.parse(blob.stdout);
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
throw sinkError('Git checkpoint sink holds a malformed checkpoint');
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
/** In-memory sink. `published` holds every checkpoint in publication order. */
|
|
79
|
+
export function memoryCheckpointSink() {
|
|
80
|
+
const published = [];
|
|
81
|
+
return {
|
|
82
|
+
name: 'memory',
|
|
83
|
+
published,
|
|
84
|
+
async publish(checkpoint) {
|
|
85
|
+
published.push(structuredClone(checkpoint));
|
|
86
|
+
return { sink: 'memory', reference: `memory#${published.length - 1}` };
|
|
87
|
+
},
|
|
88
|
+
async latest(scope) {
|
|
89
|
+
const match = [...published].reverse().find(entry => canonicalJson(entry.scope) === canonicalJson(scope));
|
|
90
|
+
return match ? structuredClone(match) : null;
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
//# sourceMappingURL=checkpoint-sinks.js.map
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Signed checkpoints over every writer's segment head, published first to the
|
|
3
|
+
* independent sink and then written locally.
|
|
4
|
+
*
|
|
5
|
+
* @see docs/contracts/effect-ledger.v1.md "Storage layout"
|
|
6
|
+
*/
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import { canonicalJson } from '../security/artifact-trust.js';
|
|
9
|
+
import { publishExclusive } from '../storage/protected-files.js';
|
|
10
|
+
import { EffectLedgerError, integrityError, usageError } from './errors.js';
|
|
11
|
+
import { sha256Digest } from './identity.js';
|
|
12
|
+
import { activeKey, assertKeyring, keyringDigest, keyValidAt } from './keyring.js';
|
|
13
|
+
import { readAllSegments, readKeyring } from './reader.js';
|
|
14
|
+
import { assertEffectSchema } from './schema.js';
|
|
15
|
+
import { ensureLedgerDirectories, withLedgerLock } from './store.js';
|
|
16
|
+
import { CHECKPOINT_PAYLOAD_TYPE, CHECKPOINT_SCHEMA_VERSION } from './types.js';
|
|
17
|
+
import { checkpointDigest, readLocalCheckpoints, verifyLedger } from './verify.js';
|
|
18
|
+
/**
|
|
19
|
+
* Checkpoint the ledger. The ledger must verify first (against the previous
|
|
20
|
+
* checkpoint), so a checkpoint never anchors a truncated or edited ledger.
|
|
21
|
+
*/
|
|
22
|
+
export async function writeCheckpoint(ledger, options = {}) {
|
|
23
|
+
const sink = options.sink ?? ledger.sink;
|
|
24
|
+
const paths = ledger.paths();
|
|
25
|
+
await ensureLedgerDirectories(paths);
|
|
26
|
+
const key = await ledger.signingKey();
|
|
27
|
+
return withLedgerLock(paths, 'checkpoint', ledger.lockTimeoutMs, async () => {
|
|
28
|
+
const verification = await verifyLedger(ledger, { sink });
|
|
29
|
+
if (!verification.ok)
|
|
30
|
+
throw integrityError(verification.failures[0].reason, 'Effect ledger failed verification; refusing to checkpoint');
|
|
31
|
+
if (verification.warnings.includes('checkpoint-sink-unreadable')) {
|
|
32
|
+
throw new EffectLedgerError('internal', 'Checkpoint sink is unavailable; no checkpoint was written', 'checkpoint-sink-unavailable');
|
|
33
|
+
}
|
|
34
|
+
const keyring = await readKeyring(paths);
|
|
35
|
+
assertKeyring(keyring, ledger.scope);
|
|
36
|
+
const createdAt = ledger.now();
|
|
37
|
+
const active = activeKey(keyring);
|
|
38
|
+
if (active.keyid !== key.keyid || !keyValidAt(active, createdAt)) {
|
|
39
|
+
throw new EffectLedgerError('key-unavailable', 'The loaded signing key is not the active ledger key at this time', 'key-not-active');
|
|
40
|
+
}
|
|
41
|
+
const segments = await readAllSegments(paths);
|
|
42
|
+
const writers = segments.filter(segment => segment.lines.length > 0).map(segment => ({
|
|
43
|
+
writer: segment.writer,
|
|
44
|
+
segment: `segments/${segment.writer}.jsonl`,
|
|
45
|
+
count: segment.lines.length,
|
|
46
|
+
headHash: segment.lines.at(-1).decoded.line.recordHash,
|
|
47
|
+
}));
|
|
48
|
+
if (!writers.length)
|
|
49
|
+
throw usageError('The effect ledger has no records to checkpoint', 'nothing-to-checkpoint');
|
|
50
|
+
const local = (await readLocalCheckpoints(paths)).at(-1)?.value;
|
|
51
|
+
const remote = await sink.latest(ledger.scope);
|
|
52
|
+
const previous = [local, remote].filter((value) => !!value)
|
|
53
|
+
.sort((a, b) => b.sequence - a.sequence)[0];
|
|
54
|
+
const body = {
|
|
55
|
+
schemaVersion: CHECKPOINT_SCHEMA_VERSION,
|
|
56
|
+
scope: structuredClone(ledger.scope),
|
|
57
|
+
sequence: previous ? previous.sequence + 1 : 0,
|
|
58
|
+
createdAt,
|
|
59
|
+
writers,
|
|
60
|
+
root: sha256Digest(canonicalJson(writers)),
|
|
61
|
+
keyringDigest: keyringDigest(keyring),
|
|
62
|
+
previousCheckpoint: previous ? checkpointDigest(previous) : null,
|
|
63
|
+
};
|
|
64
|
+
const checkpoint = {
|
|
65
|
+
...body,
|
|
66
|
+
signatures: [{ keyid: key.keyid, sig: key.signPae(CHECKPOINT_PAYLOAD_TYPE, Buffer.from(canonicalJson(body), 'utf8')) }],
|
|
67
|
+
};
|
|
68
|
+
assertEffectSchema('checkpoint', checkpoint);
|
|
69
|
+
const receipt = await sink.publish(checkpoint);
|
|
70
|
+
const written = await publishExclusive(paths.checkpoints, join(paths.checkpoints, `${checkpoint.sequence}.json`), `${JSON.stringify(checkpoint, null, 2)}\n`, 'checkpoint');
|
|
71
|
+
if (!written)
|
|
72
|
+
throw integrityError('checkpoint-sequence-exists');
|
|
73
|
+
return { checkpoint, digest: checkpointDigest(checkpoint), sink: receipt };
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
//# sourceMappingURL=checkpoint.js.map
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #1567 composition: effect records link the operator decisions that
|
|
3
|
+
* authorized them through `links.operatorDecisionEventId` and
|
|
4
|
+
* `links.operatorDecisionRecordHash`. This module checks those links against
|
|
5
|
+
* an operator-decision audit chain. It is read-only on both sides: there is
|
|
6
|
+
* no write coupling between the ledger and the decision store.
|
|
7
|
+
*
|
|
8
|
+
* @see docs/contracts/effect-ledger.v1.md "Composition"
|
|
9
|
+
* @see docs/contracts/operator-decision-audit.v1.md
|
|
10
|
+
*/
|
|
11
|
+
import { readAllSegments } from './reader.js';
|
|
12
|
+
/** The operator-decision links a ledger record carries, or null. */
|
|
13
|
+
export function decisionLinkOf(links) {
|
|
14
|
+
if (!links || (!links.operatorDecisionEventId && !links.operatorDecisionRecordHash))
|
|
15
|
+
return null;
|
|
16
|
+
return {
|
|
17
|
+
...(links.operatorDecisionEventId ? { eventId: links.operatorDecisionEventId } : {}),
|
|
18
|
+
...(links.operatorDecisionRecordHash ? { recordHash: links.operatorDecisionRecordHash } : {}),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Check every operator-decision link in the ledger against `decisions`. A
|
|
23
|
+
* linked event ID must exist; a linked record hash must equal that event's
|
|
24
|
+
* `record_hash`; a record hash without an event ID is refused. The caller
|
|
25
|
+
* verifies the decision chain itself (`verifyDecisionChain`).
|
|
26
|
+
*/
|
|
27
|
+
export async function verifyDecisionLinks(ledger, decisions) {
|
|
28
|
+
const byId = new Map(decisions.map(record => [record.event_id, record.record_hash]));
|
|
29
|
+
const failures = [];
|
|
30
|
+
const events = new Set();
|
|
31
|
+
let linked = 0;
|
|
32
|
+
for (const segment of await readAllSegments(ledger.paths())) {
|
|
33
|
+
for (const line of segment.lines) {
|
|
34
|
+
const predicate = line.decoded?.statement.predicate;
|
|
35
|
+
const link = decisionLinkOf(predicate?.links);
|
|
36
|
+
if (!predicate || !link)
|
|
37
|
+
continue;
|
|
38
|
+
linked += 1;
|
|
39
|
+
const at = { writer: segment.writer, seq: line.index, effectId: predicate.effectId };
|
|
40
|
+
if (!link.eventId) {
|
|
41
|
+
failures.push({ ...at, reason: 'decision-record-hash-orphan' });
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
events.add(link.eventId);
|
|
45
|
+
const recordHash = byId.get(link.eventId);
|
|
46
|
+
if (recordHash === undefined)
|
|
47
|
+
failures.push({ ...at, reason: 'decision-event-missing', operatorDecisionEventId: link.eventId });
|
|
48
|
+
else if (link.recordHash && link.recordHash !== recordHash)
|
|
49
|
+
failures.push({ ...at, reason: 'decision-record-hash-mismatch', operatorDecisionEventId: link.eventId });
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return { ok: failures.length === 0, linked, events: events.size, failures };
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=decision-links.js.map
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Effect ledger error taxonomy, bound to the v1 exit codes.
|
|
3
|
+
*
|
|
4
|
+
* Messages are fixed, reference-only text: they never echo payloads, keys,
|
|
5
|
+
* vault locators or rejected material.
|
|
6
|
+
*
|
|
7
|
+
* @see docs/contracts/effect-ledger.v1.md "CLI and exit codes"
|
|
8
|
+
*/
|
|
9
|
+
/** Exit codes pinned by the effect ledger v1 contract. */
|
|
10
|
+
export const EFFECT_EXIT_CODES = Object.freeze({
|
|
11
|
+
ok: 0,
|
|
12
|
+
internal: 1,
|
|
13
|
+
usage: 2,
|
|
14
|
+
absent: 3,
|
|
15
|
+
unknown: 4,
|
|
16
|
+
conflict: 5,
|
|
17
|
+
integrity: 6,
|
|
18
|
+
artifactRootUnavailable: 7,
|
|
19
|
+
});
|
|
20
|
+
const EXIT_BY_CODE = {
|
|
21
|
+
internal: EFFECT_EXIT_CODES.internal,
|
|
22
|
+
usage: EFFECT_EXIT_CODES.usage,
|
|
23
|
+
conflict: EFFECT_EXIT_CODES.conflict,
|
|
24
|
+
integrity: EFFECT_EXIT_CODES.integrity,
|
|
25
|
+
'artifact-root-unavailable': EFFECT_EXIT_CODES.artifactRootUnavailable,
|
|
26
|
+
'key-unavailable': EFFECT_EXIT_CODES.internal,
|
|
27
|
+
'lock-timeout': EFFECT_EXIT_CODES.internal,
|
|
28
|
+
'purge-refused': EFFECT_EXIT_CODES.usage,
|
|
29
|
+
};
|
|
30
|
+
export class EffectLedgerError extends Error {
|
|
31
|
+
code;
|
|
32
|
+
reason;
|
|
33
|
+
exitCode;
|
|
34
|
+
constructor(code, message, reason) {
|
|
35
|
+
super(message);
|
|
36
|
+
this.code = code;
|
|
37
|
+
this.reason = reason;
|
|
38
|
+
this.name = 'EffectLedgerError';
|
|
39
|
+
this.exitCode = EXIT_BY_CODE[code];
|
|
40
|
+
}
|
|
41
|
+
toJSON() {
|
|
42
|
+
return { code: this.code, ...(this.reason ? { reason: this.reason } : {}), message: this.message, exitCode: this.exitCode };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
export const usageError = (message, reason) => new EffectLedgerError('usage', message, reason);
|
|
46
|
+
export const integrityError = (reason, message = 'Effect ledger integrity check failed') => new EffectLedgerError('integrity', message, reason);
|
|
47
|
+
export const conflictError = (message = 'Effect ID already recorded with a different payload digest') => new EffectLedgerError('conflict', message, 'payload-digest-conflict');
|
|
48
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Effect identity: the `aiwg.effect/v1` derivation, the contract-registered
|
|
3
|
+
* adapter derivations (`d13.review/v1`), payload digests and input grammar.
|
|
4
|
+
*
|
|
5
|
+
* Every canonical form is RFC 8785 JSON from `artifact-trust.canonicalJson`.
|
|
6
|
+
*
|
|
7
|
+
* @see docs/contracts/effect-ledger.v1.md "Effect identity"
|
|
8
|
+
*/
|
|
9
|
+
import { createHash } from 'node:crypto';
|
|
10
|
+
import { canonicalJson } from '../security/artifact-trust.js';
|
|
11
|
+
import { usageError } from './errors.js';
|
|
12
|
+
import { CORE_EFFECT_KINDS, EFFECT_ID_DERIVATION_NAMES, EFFECT_SUBSYSTEMS, } from './types.js';
|
|
13
|
+
const BASE32_ALPHABET = 'abcdefghijklmnopqrstuvwxyz234567';
|
|
14
|
+
export const EFFECT_ID_V1_PATTERN = /^eff1_[a-z2-7]{51}[aq]$/;
|
|
15
|
+
export const D13_REVIEW_ID_PATTERN = /^sha256:[a-f0-9]{64}$/;
|
|
16
|
+
export const SHA256_DIGEST_PATTERN = /^sha256:[a-f0-9]{64}$/;
|
|
17
|
+
export const WRITER_ID_PATTERN = /^[a-z0-9][a-z0-9-]{0,63}$/;
|
|
18
|
+
const IDENTIFIER_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:/@-]{0,255}$/;
|
|
19
|
+
const EXTENSION_KIND_PATTERN = /^x\.[a-z0-9][a-z0-9-]*(?:\.[a-z0-9][a-z0-9-]*)+$/;
|
|
20
|
+
const TARGET_PATTERN = /^(?:gitea|github|git|git-tag|file|release|decision|review|x-[a-z0-9-]+):[^\s]+$/;
|
|
21
|
+
const CONTEXT_KEY_PATTERN = /^[A-Za-z][A-Za-z0-9_]{0,63}$/;
|
|
22
|
+
const D13_CONTEXT_KEYS = ['continuationId', 'proposalVersion', 'reviewId'];
|
|
23
|
+
const sha256Hex = (bytes) => createHash('sha256').update(bytes).digest('hex');
|
|
24
|
+
/** `"sha256:"` plus lowercase hex SHA-256 of the bytes (strings are UTF-8). */
|
|
25
|
+
export function sha256Digest(bytes) {
|
|
26
|
+
return `sha256:${sha256Hex(bytes)}`;
|
|
27
|
+
}
|
|
28
|
+
/** Unpadded lowercase RFC 4648 section 6 base32, most significant bit first. */
|
|
29
|
+
export function base32Lower(bytes) {
|
|
30
|
+
let bits = 0;
|
|
31
|
+
let value = 0;
|
|
32
|
+
let out = '';
|
|
33
|
+
for (const byte of bytes) {
|
|
34
|
+
value = ((value << 8) | byte) & 0xffff;
|
|
35
|
+
bits += 8;
|
|
36
|
+
while (bits >= 5) {
|
|
37
|
+
out += BASE32_ALPHABET[(value >>> (bits - 5)) & 31];
|
|
38
|
+
bits -= 5;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (bits > 0)
|
|
42
|
+
out += BASE32_ALPHABET[(value << (5 - bits)) & 31];
|
|
43
|
+
return out;
|
|
44
|
+
}
|
|
45
|
+
export function isCoreEffectKind(kind) {
|
|
46
|
+
return CORE_EFFECT_KINDS.includes(kind);
|
|
47
|
+
}
|
|
48
|
+
export function isEffectKind(kind) {
|
|
49
|
+
return typeof kind === 'string' && (isCoreEffectKind(kind) || (kind.length <= 128 && EXTENSION_KIND_PATTERN.test(kind)));
|
|
50
|
+
}
|
|
51
|
+
export function assertEffectScope(scope) {
|
|
52
|
+
const value = scope;
|
|
53
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)
|
|
54
|
+
|| Object.keys(value).sort().join(',') !== 'project,subsystem,tenant'
|
|
55
|
+
|| typeof value.tenant !== 'string' || !IDENTIFIER_PATTERN.test(value.tenant)
|
|
56
|
+
|| typeof value.project !== 'string' || !IDENTIFIER_PATTERN.test(value.project)
|
|
57
|
+
|| !EFFECT_SUBSYSTEMS.includes(value.subsystem)) {
|
|
58
|
+
throw usageError('Effect scope must be {tenant, project, subsystem} with a known subsystem', 'invalid-scope');
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
export function assertWriterId(writer) {
|
|
62
|
+
if (typeof writer !== 'string' || !WRITER_ID_PATTERN.test(writer))
|
|
63
|
+
throw usageError('Effect writer ID is invalid', 'invalid-writer');
|
|
64
|
+
}
|
|
65
|
+
export function assertEffectKind(kind) {
|
|
66
|
+
if (!isEffectKind(kind))
|
|
67
|
+
throw usageError('Unknown effect kind', 'unknown-kind');
|
|
68
|
+
}
|
|
69
|
+
export function assertEffectTarget(target) {
|
|
70
|
+
if (typeof target !== 'string' || target.length > 1024 || !TARGET_PATTERN.test(target)) {
|
|
71
|
+
throw usageError('Effect target must be a <scheme>:<ref> reference', 'invalid-target');
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
export function assertEffectContext(context) {
|
|
75
|
+
if (!context || typeof context !== 'object' || Array.isArray(context))
|
|
76
|
+
throw usageError('Effect context must be a flat object', 'invalid-context');
|
|
77
|
+
const entries = Object.entries(context);
|
|
78
|
+
if (entries.length > 16)
|
|
79
|
+
throw usageError('Effect context has more than 16 members', 'invalid-context');
|
|
80
|
+
for (const [key, value] of entries) {
|
|
81
|
+
const scalar = (typeof value === 'string' && value.length <= 256)
|
|
82
|
+
|| (typeof value === 'number' && Number.isSafeInteger(value))
|
|
83
|
+
|| typeof value === 'boolean';
|
|
84
|
+
if (!CONTEXT_KEY_PATTERN.test(key) || !scalar)
|
|
85
|
+
throw usageError('Effect context members must be named scalars', 'invalid-context');
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
export function assertPayloadDigest(value) {
|
|
89
|
+
if (typeof value !== 'string' || !SHA256_DIGEST_PATTERN.test(value))
|
|
90
|
+
throw usageError('Payload digest must be sha256:<hex>', 'invalid-payload-digest');
|
|
91
|
+
}
|
|
92
|
+
/** Digest the exact bytes an effect sends, or `canonicalJson` of a structured request. */
|
|
93
|
+
export function payloadDigest(payload) {
|
|
94
|
+
if (typeof payload === 'string' || payload instanceof Uint8Array)
|
|
95
|
+
return sha256Digest(payload);
|
|
96
|
+
return sha256Digest(canonicalJson(payload));
|
|
97
|
+
}
|
|
98
|
+
function assertIdentityInput(input) {
|
|
99
|
+
if (!input || typeof input !== 'object')
|
|
100
|
+
throw usageError('Effect identity input is required', 'invalid-identity');
|
|
101
|
+
assertEffectScope(input.scope);
|
|
102
|
+
assertEffectKind(input.kind);
|
|
103
|
+
assertEffectTarget(input.target);
|
|
104
|
+
assertEffectContext(input.context);
|
|
105
|
+
}
|
|
106
|
+
const aiwgEffectV1 = Object.freeze({
|
|
107
|
+
name: 'aiwg.effect/v1',
|
|
108
|
+
pattern: EFFECT_ID_V1_PATTERN,
|
|
109
|
+
permitsKind: (kind) => kind !== 'decision.review.continuation',
|
|
110
|
+
derive(input) {
|
|
111
|
+
assertIdentityInput(input);
|
|
112
|
+
if (input.kind === 'decision.review.continuation')
|
|
113
|
+
throw usageError('decision.review.continuation uses the d13.review/v1 derivation', 'derivation-kind-mismatch');
|
|
114
|
+
const canonical = canonicalJson({ v: 1, scope: input.scope, kind: input.kind, target: input.target, context: input.context });
|
|
115
|
+
return `eff1_${base32Lower(createHash('sha256').update(canonical, 'utf8').digest())}`;
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
/**
|
|
119
|
+
* `d13.review/v1`: exactly D13's `reviewDigest({reviewId, continuationId,
|
|
120
|
+
* proposalVersion})` (`src/decision/review/validate.ts`), so persisted reviews
|
|
121
|
+
* keep their identity.
|
|
122
|
+
*/
|
|
123
|
+
const d13ReviewV1 = Object.freeze({
|
|
124
|
+
name: 'd13.review/v1',
|
|
125
|
+
pattern: D13_REVIEW_ID_PATTERN,
|
|
126
|
+
permitsKind: (kind) => kind === 'decision.review.continuation',
|
|
127
|
+
derive(input) {
|
|
128
|
+
assertIdentityInput(input);
|
|
129
|
+
const context = input.context;
|
|
130
|
+
if (input.kind !== 'decision.review.continuation' || input.scope.subsystem !== 'review'
|
|
131
|
+
|| Object.keys(context).sort().join(',') !== D13_CONTEXT_KEYS.join(',')
|
|
132
|
+
|| typeof context.reviewId !== 'string' || !context.reviewId
|
|
133
|
+
|| typeof context.continuationId !== 'string' || !context.continuationId
|
|
134
|
+
|| typeof context.proposalVersion !== 'number' || !Number.isSafeInteger(context.proposalVersion) || context.proposalVersion < 1) {
|
|
135
|
+
throw usageError('d13.review/v1 needs kind decision.review.continuation, subsystem review and context {reviewId, continuationId, proposalVersion}', 'derivation-input-invalid');
|
|
136
|
+
}
|
|
137
|
+
const identity = { reviewId: context.reviewId, continuationId: context.continuationId, proposalVersion: context.proposalVersion };
|
|
138
|
+
return sha256Digest(canonicalJson(identity));
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
/**
|
|
142
|
+
* Contract-registered derivations. Registration is closed: a new derivation
|
|
143
|
+
* needs a contract revision (and a schema `idDerivation` value), so this map is
|
|
144
|
+
* frozen rather than extensible at runtime.
|
|
145
|
+
*/
|
|
146
|
+
export const EFFECT_ID_DERIVATIONS = Object.freeze({
|
|
147
|
+
'aiwg.effect/v1': aiwgEffectV1,
|
|
148
|
+
'd13.review/v1': d13ReviewV1,
|
|
149
|
+
});
|
|
150
|
+
export function effectIdDerivation(name) {
|
|
151
|
+
if (!EFFECT_ID_DERIVATION_NAMES.includes(name))
|
|
152
|
+
throw usageError('Unknown effect ID derivation', 'unknown-derivation');
|
|
153
|
+
return EFFECT_ID_DERIVATIONS[name];
|
|
154
|
+
}
|
|
155
|
+
/** The derivation a kind uses by default. */
|
|
156
|
+
export function defaultDerivationFor(kind) {
|
|
157
|
+
return kind === 'decision.review.continuation' ? 'd13.review/v1' : 'aiwg.effect/v1';
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Derive an effect ID. Attempt, time, actor, host and payload are not inputs.
|
|
161
|
+
* `derivation` defaults to the kind's registered derivation.
|
|
162
|
+
*/
|
|
163
|
+
export function effectId(input, derivation = defaultDerivationFor(input?.kind)) {
|
|
164
|
+
return effectIdDerivation(derivation).derive(input);
|
|
165
|
+
}
|
|
166
|
+
/** Strict reader-side check. Uppercase, padding, other lengths and non-zero trailing bits are rejected, never normalized. */
|
|
167
|
+
export function isValidEffectId(id, derivation) {
|
|
168
|
+
if (typeof id !== 'string')
|
|
169
|
+
return false;
|
|
170
|
+
if (derivation)
|
|
171
|
+
return effectIdDerivation(derivation).pattern.test(id);
|
|
172
|
+
return EFFECT_ID_V1_PATTERN.test(id) || D13_REVIEW_ID_PATTERN.test(id);
|
|
173
|
+
}
|
|
174
|
+
export function assertEffectId(id, derivation) {
|
|
175
|
+
if (!isValidEffectId(id, derivation))
|
|
176
|
+
throw usageError('Malformed effect ID', 'malformed-effect-id');
|
|
177
|
+
}
|
|
178
|
+
//# sourceMappingURL=identity.js.map
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AIWG effect ledger core library (#2717, epic #2714).
|
|
3
|
+
*
|
|
4
|
+
* A signed proof and deduplication index for side effects. It is not an
|
|
5
|
+
* exactly-once mechanism and it never authorizes an effect.
|
|
6
|
+
*
|
|
7
|
+
* @see docs/contracts/effect-ledger.v1.md
|
|
8
|
+
* @see docs/architecture/adr-effect-ledger.md
|
|
9
|
+
*/
|
|
10
|
+
export * from './types.js';
|
|
11
|
+
export { EFFECT_EXIT_CODES, EffectLedgerError, } from './errors.js';
|
|
12
|
+
export { D13_REVIEW_ID_PATTERN, EFFECT_ID_DERIVATIONS, EFFECT_ID_V1_PATTERN, assertEffectId, base32Lower, defaultDerivationFor, effectId, effectIdDerivation, isEffectKind, isValidEffectId, payloadDigest, sha256Digest as effectSha256Digest, } from './identity.js';
|
|
13
|
+
export { DEFAULT_LEDGER_KEY_SERVICE, LEDGER_TEST_KEY_ENV, LedgerSigningKey, credentialStoreKeyProvider, environmentTestKeyProvider, generateLedgerKeySecret, staticKeyProvider, } from './keys.js';
|
|
14
|
+
export { activeKey, keyValidAt, keyringDigest, keyringFailure } from './keyring.js';
|
|
15
|
+
export { EffectLedger, effectOutputJson, initLedgerKeyring, lookupEffect, readLedgerKeyring, openEffectLedger, reconcileEffect, recordIntent, recordOutcome, recordReconciled, rotateKey, } from './ledger.js';
|
|
16
|
+
export * from './verifiers/index.js';
|
|
17
|
+
export { gitRefCheckpointSink, memoryCheckpointSink, } from './checkpoint-sinks.js';
|
|
18
|
+
export { writeCheckpoint } from './checkpoint.js';
|
|
19
|
+
export { checkpointDigest, verifyLedger, } from './verify.js';
|
|
20
|
+
export { EFFECT_RETENTION_POLICY, purgeEffect } from './lifecycle.js';
|
|
21
|
+
export { LEDGER_LOCK_NAME_PATTERN, LOCK_RECOVERY_KIND, LOCK_RECOVERY_VERIFIER_VERSION, inspectLedgerLock, inspectLedgerLocks, ledgerLockRecoveryVerifier, lockRecoveryTarget, recoverStaleLedgerLock, } from './lock-recovery.js';
|
|
22
|
+
export { assertDigestOnly, containsRestrictedMaterial } from './redaction.js';
|
|
23
|
+
export { EFFECT_SCHEMA_IDS, effectSchemaErrors, isEffectSchemaValid } from './schema.js';
|
|
24
|
+
export { decisionLinkOf, verifyDecisionLinks, } from './decision-links.js';
|
|
25
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Effect ledger keyring: public Ed25519 keys with validity windows, extended
|
|
3
|
+
* only by rotations signed by both the prior and the successor key.
|
|
4
|
+
*
|
|
5
|
+
* @see docs/contracts/effect-ledger.v1.md "Signing and keyring"
|
|
6
|
+
*/
|
|
7
|
+
import { canonicalJson, decodeBase64, dssePae, publicKeyFingerprint, verifyBytes } from '../security/artifact-trust.js';
|
|
8
|
+
import { integrityError, usageError } from './errors.js';
|
|
9
|
+
import { sha256Digest } from './identity.js';
|
|
10
|
+
import { isEffectSchemaValid } from './schema.js';
|
|
11
|
+
import { KEYRING_SCHEMA_VERSION, ROTATION_PAYLOAD_TYPE, ROTATION_SCHEMA_VERSION, } from './types.js';
|
|
12
|
+
const time = (value) => Date.parse(value);
|
|
13
|
+
/** `"sha256:"` plus hex SHA-256 of `canonicalJson(keyring)`, as recorded in checkpoints. */
|
|
14
|
+
export function keyringDigest(keyring) {
|
|
15
|
+
return sha256Digest(canonicalJson(keyring));
|
|
16
|
+
}
|
|
17
|
+
/** `validFrom ≤ t < validUntil`, and before `revokedAt` for a revoked key. */
|
|
18
|
+
export function keyValidAt(key, at) {
|
|
19
|
+
const t = time(at);
|
|
20
|
+
if (!Number.isFinite(t) || t < time(key.validFrom))
|
|
21
|
+
return false;
|
|
22
|
+
if (key.validUntil !== undefined && t >= time(key.validUntil))
|
|
23
|
+
return false;
|
|
24
|
+
if (key.status === 'revoked' && (key.revokedAt === undefined || t >= time(key.revokedAt)))
|
|
25
|
+
return false;
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
function signatureValid(publicKey, payloadType, payload, sig) {
|
|
29
|
+
try {
|
|
30
|
+
return verifyBytes('ed25519', publicKey, dssePae(payloadType, payload), decodeBase64(sig, 'signature'));
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/** Check one DSSE signature against the keyring at signing time `at`. */
|
|
37
|
+
export function checkSignature(keyring, keyid, sig, payloadType, payload, at) {
|
|
38
|
+
const key = keyring.keys.find(entry => entry.keyid === keyid);
|
|
39
|
+
if (!key)
|
|
40
|
+
return 'unknown-keyid';
|
|
41
|
+
if (!signatureValid(key.publicKey, payloadType, payload, sig))
|
|
42
|
+
return 'signature-invalid';
|
|
43
|
+
if (!keyValidAt(key, at))
|
|
44
|
+
return 'key-window';
|
|
45
|
+
return 'ok';
|
|
46
|
+
}
|
|
47
|
+
function rotationBody(rotation) {
|
|
48
|
+
const { signatures: _signatures, ...body } = rotation;
|
|
49
|
+
return body;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Structural and cryptographic keyring check. Returns a failure reason, or
|
|
53
|
+
* null when the keyring is a genesis key extended only by valid rotations.
|
|
54
|
+
*/
|
|
55
|
+
export function keyringFailure(keyring, scope) {
|
|
56
|
+
if (!isEffectSchemaValid('keyring', keyring))
|
|
57
|
+
return 'keyring-schema-invalid';
|
|
58
|
+
const ring = keyring;
|
|
59
|
+
if (scope && canonicalJson(ring.scope) !== canonicalJson(scope))
|
|
60
|
+
return 'keyring-scope-mismatch';
|
|
61
|
+
const byId = new Map();
|
|
62
|
+
for (const key of ring.keys) {
|
|
63
|
+
if (byId.has(key.keyid))
|
|
64
|
+
return 'keyring-duplicate-keyid';
|
|
65
|
+
try {
|
|
66
|
+
if (`sha256:${publicKeyFingerprint(key.publicKey)}` !== key.keyid)
|
|
67
|
+
return 'keyring-keyid-mismatch';
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return 'keyring-keyid-mismatch';
|
|
71
|
+
}
|
|
72
|
+
byId.set(key.keyid, key);
|
|
73
|
+
}
|
|
74
|
+
const introduced = new Set(ring.rotations.map(rotation => rotation.to));
|
|
75
|
+
const genesis = ring.keys.filter(key => !introduced.has(key.keyid));
|
|
76
|
+
if (genesis.length !== 1 || ring.keys.length !== ring.rotations.length + 1)
|
|
77
|
+
return 'keyring-unrotated-key';
|
|
78
|
+
let current = genesis[0];
|
|
79
|
+
let lastEffective = -Infinity;
|
|
80
|
+
for (const [index, rotation] of ring.rotations.entries()) {
|
|
81
|
+
if (rotation.sequence !== index + 1)
|
|
82
|
+
return 'keyring-rotation-sequence';
|
|
83
|
+
const successor = byId.get(rotation.to);
|
|
84
|
+
const effective = time(rotation.effectiveAt);
|
|
85
|
+
if (rotation.from !== current.keyid || !successor || successor.keyid === current.keyid)
|
|
86
|
+
return 'keyring-rotation-chain';
|
|
87
|
+
if (!(effective > lastEffective) || current.validUntil === undefined || time(current.validUntil) !== effective
|
|
88
|
+
|| time(successor.validFrom) !== effective)
|
|
89
|
+
return 'keyring-rotation-window';
|
|
90
|
+
const payload = Buffer.from(canonicalJson(rotationBody(rotation)), 'utf8');
|
|
91
|
+
const prior = rotation.signatures.find(entry => entry.role === 'prior');
|
|
92
|
+
const next = rotation.signatures.find(entry => entry.role === 'successor');
|
|
93
|
+
if (!prior || prior.keyid !== current.keyid || !signatureValid(current.publicKey, ROTATION_PAYLOAD_TYPE, payload, prior.sig))
|
|
94
|
+
return 'keyring-rotation-prior-signature';
|
|
95
|
+
if (!next || next.keyid !== successor.keyid || !signatureValid(successor.publicKey, ROTATION_PAYLOAD_TYPE, payload, next.sig))
|
|
96
|
+
return 'keyring-rotation-successor-signature';
|
|
97
|
+
current = successor;
|
|
98
|
+
lastEffective = effective;
|
|
99
|
+
}
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
export function assertKeyring(keyring, scope) {
|
|
103
|
+
const failure = keyringFailure(keyring, scope);
|
|
104
|
+
if (failure)
|
|
105
|
+
throw integrityError(failure, 'Effect ledger keyring failed verification');
|
|
106
|
+
}
|
|
107
|
+
/** The key currently allowed to sign: the last one in the rotation chain. */
|
|
108
|
+
export function activeKey(keyring) {
|
|
109
|
+
const last = keyring.rotations.at(-1);
|
|
110
|
+
const keyid = last ? last.to : keyring.keys[0].keyid;
|
|
111
|
+
return keyring.keys.find(key => key.keyid === keyid);
|
|
112
|
+
}
|
|
113
|
+
export function createGenesisKeyring(scope, key, validFrom) {
|
|
114
|
+
return {
|
|
115
|
+
schemaVersion: KEYRING_SCHEMA_VERSION,
|
|
116
|
+
scope: structuredClone(scope),
|
|
117
|
+
keys: [{ keyid: key.keyid, algorithm: 'ed25519', publicKey: key.publicKey, validFrom, status: 'active' }],
|
|
118
|
+
rotations: [],
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Extend `keyring` with a rotation from `prior` (which must be the active key)
|
|
123
|
+
* to `successor`, signed by both. The prior key's window closes at
|
|
124
|
+
* `effectiveAt`; records it signed earlier stay verifiable.
|
|
125
|
+
*/
|
|
126
|
+
export function rotateKeyring(keyring, prior, successor, effectiveAt, reason = 'scheduled') {
|
|
127
|
+
assertKeyring(keyring);
|
|
128
|
+
const current = activeKey(keyring);
|
|
129
|
+
if (current.keyid !== prior.keyid)
|
|
130
|
+
throw usageError('Only the active ledger key can sign a rotation', 'rotation-prior-not-active');
|
|
131
|
+
if (keyring.keys.some(key => key.keyid === successor.keyid))
|
|
132
|
+
throw usageError('The successor key is already in the keyring', 'rotation-key-reused');
|
|
133
|
+
if (!(time(effectiveAt) > time(current.validFrom)))
|
|
134
|
+
throw usageError('A rotation must take effect after the active key became valid', 'rotation-effective-at');
|
|
135
|
+
const body = {
|
|
136
|
+
schemaVersion: ROTATION_SCHEMA_VERSION,
|
|
137
|
+
sequence: keyring.rotations.length + 1,
|
|
138
|
+
from: prior.keyid,
|
|
139
|
+
to: successor.keyid,
|
|
140
|
+
effectiveAt,
|
|
141
|
+
reason,
|
|
142
|
+
};
|
|
143
|
+
const payload = Buffer.from(canonicalJson(body), 'utf8');
|
|
144
|
+
const next = structuredClone(keyring);
|
|
145
|
+
const retired = next.keys.find(key => key.keyid === prior.keyid);
|
|
146
|
+
retired.validUntil = effectiveAt;
|
|
147
|
+
if (retired.status === 'active')
|
|
148
|
+
retired.status = 'retired';
|
|
149
|
+
next.keys.push({ keyid: successor.keyid, algorithm: 'ed25519', publicKey: successor.publicKey, validFrom: effectiveAt, status: 'active' });
|
|
150
|
+
next.rotations.push({
|
|
151
|
+
...body,
|
|
152
|
+
signatures: [
|
|
153
|
+
{ role: 'prior', keyid: prior.keyid, sig: prior.signPae(ROTATION_PAYLOAD_TYPE, payload) },
|
|
154
|
+
{ role: 'successor', keyid: successor.keyid, sig: successor.signPae(ROTATION_PAYLOAD_TYPE, payload) },
|
|
155
|
+
],
|
|
156
|
+
});
|
|
157
|
+
assertKeyring(next);
|
|
158
|
+
return next;
|
|
159
|
+
}
|
|
160
|
+
//# sourceMappingURL=keyring.js.map
|