@aiwg/cli 2026.9.19 → 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,269 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Operator-authorized recovery of a stale effect ledger lock.
|
|
3
|
+
*
|
|
4
|
+
* Ledger locks (`locks/<name>.lock`) are same-host directory locks whose
|
|
5
|
+
* `owner` file holds `<pid>:<uuid>`. A crashed writer leaves its lock behind,
|
|
6
|
+
* and every later writer then times out rather than steal it. This module is
|
|
7
|
+
* the only supported way to clear such a lock. It is modelled on the D16 quota
|
|
8
|
+
* lock recovery (`recoverStaleJobQuotaLock`, `src/decision/job-quota.ts`):
|
|
9
|
+
*
|
|
10
|
+
* - it refuses a live owner, a PID that has been reused by a newer process,
|
|
11
|
+
* and any owner it cannot verify (malformed owner file, permission denied);
|
|
12
|
+
* - it needs an explicit authorization callback, which must approve the exact
|
|
13
|
+
* inspected owner;
|
|
14
|
+
* - it re-checks the owner after authorization and removes the lock only when
|
|
15
|
+
* the same dead owner still holds it;
|
|
16
|
+
* - it records the recovery in the ledger as a signed, verified effect of kind
|
|
17
|
+
* `x.aiwg.ledger-lock-recovery` (intent before the removal, completed after
|
|
18
|
+
* the lock-state verifier confirms it).
|
|
19
|
+
*
|
|
20
|
+
* @see docs/contracts/effect-ledger.v1.md "Stale lock recovery"
|
|
21
|
+
*/
|
|
22
|
+
import { lstat, mkdir, readdir, readFile, rm } from 'node:fs/promises';
|
|
23
|
+
import { join } from 'node:path';
|
|
24
|
+
import { EFFECT_EXIT_CODES, EffectLedgerError, usageError } from './errors.js';
|
|
25
|
+
import { payloadDigest, sha256Digest } from './identity.js';
|
|
26
|
+
import { EffectLedger, lookupEffect, recordIntent, recordOutcome, recordReconciled } from './ledger.js';
|
|
27
|
+
import { resolveLedgerPaths } from './store.js';
|
|
28
|
+
import { createVerifierRegistry, runVerifier } from './verifiers/registry.js';
|
|
29
|
+
import { EffectVerifierError } from './verifiers/types.js';
|
|
30
|
+
export const LOCK_RECOVERY_KIND = 'x.aiwg.ledger-lock-recovery';
|
|
31
|
+
export const LOCK_RECOVERY_VERIFIER_VERSION = '1.0.0';
|
|
32
|
+
/** Lock names the ledger creates: one per writer, plus the checkpoint and keyring locks. */
|
|
33
|
+
export const LEDGER_LOCK_NAME_PATTERN = /^(?:writer-[a-z0-9][a-z0-9-]{0,63}|checkpoint|keyring)$/;
|
|
34
|
+
const OWNER_PATTERN = /^([1-9][0-9]{0,9}):([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/;
|
|
35
|
+
const RECOVERY_GUARD = '.recover.guard';
|
|
36
|
+
/** A process that started this long after the owner file was written cannot be its owner. */
|
|
37
|
+
const PID_REUSE_TOLERANCE_MS = 2_000;
|
|
38
|
+
export function lockRecoveryTarget(scope, lock) {
|
|
39
|
+
return `x-aiwg:effects/${scope.subsystem}/locks/${lock}`;
|
|
40
|
+
}
|
|
41
|
+
function assertLockName(lock) {
|
|
42
|
+
if (typeof lock !== 'string' || !LEDGER_LOCK_NAME_PATTERN.test(lock))
|
|
43
|
+
throw usageError('Unknown effect ledger lock name', 'invalid-lock-name');
|
|
44
|
+
}
|
|
45
|
+
const lockDir = (paths, lock) => join(paths.locks, `${lock}.lock`);
|
|
46
|
+
/** Linux only: the process start time in epoch ms, or null when it cannot be read. */
|
|
47
|
+
async function processStartMs(pid) {
|
|
48
|
+
if (process.platform !== 'linux')
|
|
49
|
+
return null;
|
|
50
|
+
try {
|
|
51
|
+
const stat = await readFile(`/proc/${pid}/stat`, 'utf8');
|
|
52
|
+
const fields = stat.slice(stat.lastIndexOf(')') + 2).split(' ');
|
|
53
|
+
const startTicks = Number(fields[19]);
|
|
54
|
+
const bootLine = (await readFile('/proc/stat', 'utf8')).split('\n').find(line => line.startsWith('btime '));
|
|
55
|
+
const bootSeconds = Number(bootLine?.split(/\s+/)[1]);
|
|
56
|
+
if (!Number.isFinite(startTicks) || !Number.isFinite(bootSeconds))
|
|
57
|
+
return null;
|
|
58
|
+
// /proc reports start time in USER_HZ, which is 100 on Linux.
|
|
59
|
+
return bootSeconds * 1000 + (startTicks / 100) * 1000;
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
async function ownerState(pid, lockedAtMs) {
|
|
66
|
+
try {
|
|
67
|
+
process.kill(pid, 0);
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
const code = error.code;
|
|
71
|
+
if (code === 'ESRCH')
|
|
72
|
+
return { state: 'dead', reason: 'owner-dead' };
|
|
73
|
+
return { state: 'unverifiable', reason: code === 'EPERM' ? 'owner-permission-denied' : 'owner-unreadable' };
|
|
74
|
+
}
|
|
75
|
+
const started = await processStartMs(pid);
|
|
76
|
+
if (started !== null && started > lockedAtMs + PID_REUSE_TOLERANCE_MS)
|
|
77
|
+
return { state: 'reused', reason: 'owner-pid-reused' };
|
|
78
|
+
return { state: 'live', reason: 'owner-live' };
|
|
79
|
+
}
|
|
80
|
+
async function inspectAt(paths, lock) {
|
|
81
|
+
const dir = lockDir(paths, lock);
|
|
82
|
+
let info;
|
|
83
|
+
try {
|
|
84
|
+
info = await lstat(dir);
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
if (error.code === 'ENOENT')
|
|
88
|
+
return { lock, held: false, reason: 'not-held' };
|
|
89
|
+
return { lock, held: true, state: 'unverifiable', reason: 'owner-unreadable' };
|
|
90
|
+
}
|
|
91
|
+
if (!info.isDirectory() || info.isSymbolicLink())
|
|
92
|
+
return { lock, held: true, state: 'unverifiable', reason: 'owner-malformed' };
|
|
93
|
+
const ownerFile = join(dir, 'owner');
|
|
94
|
+
let token;
|
|
95
|
+
let ownerInfo;
|
|
96
|
+
try {
|
|
97
|
+
ownerInfo = await lstat(ownerFile);
|
|
98
|
+
if (!ownerInfo.isFile() || ownerInfo.isSymbolicLink() || (ownerInfo.mode & 0o077) !== 0) {
|
|
99
|
+
return { lock, held: true, state: 'unverifiable', reason: 'owner-malformed' };
|
|
100
|
+
}
|
|
101
|
+
token = (await readFile(ownerFile, 'utf8')).trim();
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return { lock, held: true, state: 'unverifiable', reason: 'owner-unreadable' };
|
|
105
|
+
}
|
|
106
|
+
const match = OWNER_PATTERN.exec(token);
|
|
107
|
+
if (!match || !Number.isSafeInteger(Number(match[1]))) {
|
|
108
|
+
return { lock, held: true, state: 'unverifiable', reason: 'owner-malformed', lockedAt: ownerInfo.mtime.toISOString() };
|
|
109
|
+
}
|
|
110
|
+
const pid = Number(match[1]);
|
|
111
|
+
const lockedAtMs = ownerInfo.mtimeMs;
|
|
112
|
+
const state = pid === process.pid ? { state: 'live', reason: 'owner-live' } : await ownerState(pid, lockedAtMs);
|
|
113
|
+
return { lock, held: true, ...state, pid, ownerDigest: sha256Digest(token), lockedAt: new Date(lockedAtMs).toISOString(), token };
|
|
114
|
+
}
|
|
115
|
+
function publicInspection(inspection) {
|
|
116
|
+
const { token: _token, ...rest } = inspection;
|
|
117
|
+
return rest;
|
|
118
|
+
}
|
|
119
|
+
/** Inspect one ledger lock. Read-only. */
|
|
120
|
+
export async function inspectLedgerLock(ledger, lock) {
|
|
121
|
+
assertLockName(lock);
|
|
122
|
+
return publicInspection(await inspectAt(ledger.paths(), lock));
|
|
123
|
+
}
|
|
124
|
+
/** Inspect every lock currently present in the ledger. Read-only. */
|
|
125
|
+
export async function inspectLedgerLocks(ledger) {
|
|
126
|
+
const paths = ledger.paths();
|
|
127
|
+
let names;
|
|
128
|
+
try {
|
|
129
|
+
names = await readdir(paths.locks);
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
if (error.code === 'ENOENT')
|
|
133
|
+
return [];
|
|
134
|
+
throw error;
|
|
135
|
+
}
|
|
136
|
+
const locks = names.filter(name => name.endsWith('.lock')).map(name => name.slice(0, -'.lock'.length))
|
|
137
|
+
.filter(name => LEDGER_LOCK_NAME_PATTERN.test(name)).sort();
|
|
138
|
+
const out = [];
|
|
139
|
+
for (const lock of locks)
|
|
140
|
+
out.push(publicInspection(await inspectAt(paths, lock)));
|
|
141
|
+
return out;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* The lock-state verifier for recovery effects. `present` when the recovered
|
|
145
|
+
* owner no longer holds the lock, `absent` when it still does.
|
|
146
|
+
*/
|
|
147
|
+
export function ledgerLockRecoveryVerifier(options) {
|
|
148
|
+
return {
|
|
149
|
+
kind: LOCK_RECOVERY_KIND,
|
|
150
|
+
version: LOCK_RECOVERY_VERIFIER_VERSION,
|
|
151
|
+
canReportAbsent: true,
|
|
152
|
+
async verify(request) {
|
|
153
|
+
const lock = request.context.lock;
|
|
154
|
+
const ownerDigest = request.context.ownerDigest;
|
|
155
|
+
if (typeof lock !== 'string' || !LEDGER_LOCK_NAME_PATTERN.test(lock) || typeof ownerDigest !== 'string'
|
|
156
|
+
|| request.target !== lockRecoveryTarget(request.scope, lock)) {
|
|
157
|
+
throw new EffectVerifierError('malformed-response', 'Lock recovery effect has an unexpected target or context');
|
|
158
|
+
}
|
|
159
|
+
let paths;
|
|
160
|
+
try {
|
|
161
|
+
paths = resolveLedgerPaths(options.projectDir, request.scope.subsystem);
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
throw new EffectVerifierError('container-unreadable', 'Effect ledger root is unavailable');
|
|
165
|
+
}
|
|
166
|
+
const current = await inspectAt(paths, lock);
|
|
167
|
+
if (current.held && current.reason === 'owner-unreadable')
|
|
168
|
+
throw new EffectVerifierError('container-unreadable', 'Lock owner is unreadable');
|
|
169
|
+
if (current.held && current.ownerDigest === ownerDigest) {
|
|
170
|
+
return { result: 'absent', reason: 'complete-query-no-match', complete: true, evidence: { lock, held: true } };
|
|
171
|
+
}
|
|
172
|
+
return { result: 'present', reason: 'state-match', complete: true, evidence: { lock, held: current.held } };
|
|
173
|
+
},
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
/** A handle on the same ledger that appends under another writer, so recovering our own writer lock never deadlocks. */
|
|
177
|
+
function recoveryWriter(ledger, lock) {
|
|
178
|
+
if (lock !== `writer-${ledger.writer}`)
|
|
179
|
+
return ledger;
|
|
180
|
+
const writer = ledger.writer === 'lock-recovery' ? 'lock-recovery-alt' : 'lock-recovery';
|
|
181
|
+
return new EffectLedger({
|
|
182
|
+
projectDir: ledger.projectDir, scope: ledger.scope, writer,
|
|
183
|
+
keyProvider: { name: 'delegated', load: () => ledger.signingKey() },
|
|
184
|
+
clock: () => Date.parse(ledger.now()), sink: ledger.sink, verifiers: ledger.verifiers, lockTimeoutMs: ledger.lockTimeoutMs,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Remove a stale ledger lock after explicit authorization and record the
|
|
189
|
+
* recovery in the ledger. A live, reused or unverifiable owner is refused and
|
|
190
|
+
* nothing is removed or recorded.
|
|
191
|
+
*/
|
|
192
|
+
export async function recoverStaleLedgerLock(ledger, lock, options) {
|
|
193
|
+
assertLockName(lock);
|
|
194
|
+
if (!options || typeof options.authorize !== 'function')
|
|
195
|
+
throw usageError('Lock recovery requires an explicit authorization', 'authorization-required');
|
|
196
|
+
const paths = ledger.paths();
|
|
197
|
+
const refuse = (outcome, inspection, exitCode) => ({
|
|
198
|
+
outcome, lock, inspection: publicInspection(inspection), exitCode,
|
|
199
|
+
});
|
|
200
|
+
const first = await inspectAt(paths, lock);
|
|
201
|
+
if (!first.held)
|
|
202
|
+
return refuse('not-held', first, EFFECT_EXIT_CODES.absent);
|
|
203
|
+
const guard = join(paths.locks, RECOVERY_GUARD);
|
|
204
|
+
try {
|
|
205
|
+
await mkdir(guard, { mode: 0o700 });
|
|
206
|
+
}
|
|
207
|
+
catch (error) {
|
|
208
|
+
if (error.code === 'EEXIST')
|
|
209
|
+
return refuse('recovery-in-progress', first, EFFECT_EXIT_CODES.conflict);
|
|
210
|
+
throw error;
|
|
211
|
+
}
|
|
212
|
+
try {
|
|
213
|
+
const inspection = await inspectAt(paths, lock);
|
|
214
|
+
if (!inspection.held)
|
|
215
|
+
return refuse('not-held', inspection, EFFECT_EXIT_CODES.absent);
|
|
216
|
+
if (inspection.state !== 'dead') {
|
|
217
|
+
return refuse('refused', inspection, inspection.state === 'unverifiable' ? EFFECT_EXIT_CODES.unknown : EFFECT_EXIT_CODES.conflict);
|
|
218
|
+
}
|
|
219
|
+
let approved = false;
|
|
220
|
+
try {
|
|
221
|
+
approved = (await options.authorize(publicInspection(inspection))) === true;
|
|
222
|
+
}
|
|
223
|
+
catch {
|
|
224
|
+
approved = false;
|
|
225
|
+
}
|
|
226
|
+
if (!approved)
|
|
227
|
+
return refuse('not-authorized', inspection, EFFECT_EXIT_CODES.usage);
|
|
228
|
+
const recorder = recoveryWriter(ledger, lock);
|
|
229
|
+
await recorder.signingKey();
|
|
230
|
+
const context = { lock, ownerPid: inspection.pid, ownerDigest: inspection.ownerDigest };
|
|
231
|
+
const digest = payloadDigest({ schemaVersion: 'aiwg.effect.lock-recovery.v1', ...context, authorizedBy: 'operator' });
|
|
232
|
+
const identity = { kind: LOCK_RECOVERY_KIND, target: lockRecoveryTarget(ledger.scope, lock), context };
|
|
233
|
+
const intent = await recordIntent(recorder, { ...identity, payloadDigest: digest, links: options.links });
|
|
234
|
+
const recheck = await inspectAt(paths, lock);
|
|
235
|
+
if (recheck.held && (recheck.state !== 'dead' || recheck.ownerDigest !== inspection.ownerDigest)) {
|
|
236
|
+
return { ...refuse('refused', recheck, EFFECT_EXIT_CODES.conflict), effectId: intent.effectId, intent };
|
|
237
|
+
}
|
|
238
|
+
if (recheck.held)
|
|
239
|
+
await rm(lockDir(paths, lock), { recursive: true });
|
|
240
|
+
const found = await lookupEffect(recorder, intent.effectId);
|
|
241
|
+
const intentAt = found.records.find(record => record.phase === 'intent')?.recordedAt ?? recorder.now();
|
|
242
|
+
const run = await runVerifier(createVerifierRegistry([ledgerLockRecoveryVerifier({ projectDir: ledger.projectDir })]).get(LOCK_RECOVERY_KIND), {
|
|
243
|
+
effectId: intent.effectId, scope: structuredClone(ledger.scope), kind: LOCK_RECOVERY_KIND, target: identity.target,
|
|
244
|
+
context, payloadDigest: digest, intentRecordedAt: intentAt, expected: {},
|
|
245
|
+
});
|
|
246
|
+
const verification = { verifier: run.verifier, ...run.observation, checkedAt: recorder.now() };
|
|
247
|
+
let completed = null;
|
|
248
|
+
if (verification.result === 'present') {
|
|
249
|
+
completed = await recordOutcome(recorder, intent.effectId, { phase: 'completed', payloadDigest: digest, verification, links: options.links });
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
await recordReconciled(recorder, intent.effectId, verification, options.links);
|
|
253
|
+
}
|
|
254
|
+
const exitCode = verification.result === 'present' ? EFFECT_EXIT_CODES.ok : verification.result === 'absent' ? EFFECT_EXIT_CODES.absent : EFFECT_EXIT_CODES.unknown;
|
|
255
|
+
return {
|
|
256
|
+
outcome: verification.result === 'present' ? 'recovered' : 'refused', lock, inspection: publicInspection(inspection),
|
|
257
|
+
effectId: intent.effectId, intent, completed, verification, exitCode,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
catch (error) {
|
|
261
|
+
if (error instanceof EffectLedgerError)
|
|
262
|
+
throw error;
|
|
263
|
+
throw new EffectLedgerError('internal', 'Effect ledger lock recovery failed', 'lock-recovery-failed');
|
|
264
|
+
}
|
|
265
|
+
finally {
|
|
266
|
+
await rm(guard, { recursive: true, force: true });
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
//# sourceMappingURL=lock-recovery.js.map
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read-side helpers shared by lookup, verification and purge: the keyring,
|
|
3
|
+
* decoded segments, and the per-line checks that bind a signed record to its
|
|
4
|
+
* segment position, chain, scope, identity and key window.
|
|
5
|
+
*/
|
|
6
|
+
import { readFile } from 'node:fs/promises';
|
|
7
|
+
import { canonicalJson } from '../security/artifact-trust.js';
|
|
8
|
+
import { integrityError } from './errors.js';
|
|
9
|
+
import { checkSignature } from './keyring.js';
|
|
10
|
+
import { decodeSegmentLine, effectIdBindingHolds, recordHashFor, signedAt, subjectBindingHolds, } from './records.js';
|
|
11
|
+
import { listWriters, readSegment } from './store.js';
|
|
12
|
+
import { STATEMENT_PAYLOAD_TYPE } from './types.js';
|
|
13
|
+
/** The stored keyring, or null before the first write. Not verified here. */
|
|
14
|
+
export async function readKeyring(paths) {
|
|
15
|
+
let raw;
|
|
16
|
+
try {
|
|
17
|
+
raw = await readFile(paths.keyring, 'utf8');
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
if (error.code === 'ENOENT')
|
|
21
|
+
return null;
|
|
22
|
+
throw error;
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
return JSON.parse(raw);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
throw integrityError('keyring-malformed');
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export async function readWriterSegment(paths, writer) {
|
|
32
|
+
const segment = await readSegment(paths, writer);
|
|
33
|
+
if (!segment)
|
|
34
|
+
return null;
|
|
35
|
+
return {
|
|
36
|
+
writer,
|
|
37
|
+
torn: segment.torn,
|
|
38
|
+
lines: segment.lines.map((text, index) => {
|
|
39
|
+
const decoded = decodeSegmentLine(text);
|
|
40
|
+
return 'failure' in decoded ? { writer, index, text, failure: decoded.failure } : { writer, index, text, decoded };
|
|
41
|
+
}),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
export async function readAllSegments(paths) {
|
|
45
|
+
const out = [];
|
|
46
|
+
for (const writer of await listWriters(paths)) {
|
|
47
|
+
const segment = await readWriterSegment(paths, writer);
|
|
48
|
+
if (segment)
|
|
49
|
+
out.push(segment);
|
|
50
|
+
}
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Every check that binds one decoded line to its place. Returns the failure
|
|
55
|
+
* reasons (empty when the line verifies).
|
|
56
|
+
*/
|
|
57
|
+
export function lineFailures(keyring, scope, writer, index, decoded, previousHash) {
|
|
58
|
+
const failures = [];
|
|
59
|
+
const { line, payload, statement } = decoded;
|
|
60
|
+
const predicate = statement.predicate;
|
|
61
|
+
if (line.writer !== writer || predicate.writer !== writer)
|
|
62
|
+
failures.push('writer-mismatch');
|
|
63
|
+
if (line.seq !== index || predicate.seq !== index)
|
|
64
|
+
failures.push('seq-mismatch');
|
|
65
|
+
if (predicate.prev !== previousHash)
|
|
66
|
+
failures.push('chain-broken');
|
|
67
|
+
if (canonicalJson(predicate.scope) !== canonicalJson(scope))
|
|
68
|
+
failures.push('scope-mismatch');
|
|
69
|
+
if (!subjectBindingHolds(statement))
|
|
70
|
+
failures.push('subject-mismatch');
|
|
71
|
+
if (!effectIdBindingHolds(predicate))
|
|
72
|
+
failures.push('effect-id-mismatch');
|
|
73
|
+
if (predicate.phase === 'tombstone') {
|
|
74
|
+
if (predicate.tombstone.originalRecordHash !== line.recordHash)
|
|
75
|
+
failures.push('record-hash-mismatch');
|
|
76
|
+
if (!keyring.keys.some(key => key.keyid === predicate.tombstone.originalKeyid))
|
|
77
|
+
failures.push('unknown-keyid');
|
|
78
|
+
}
|
|
79
|
+
else if (recordHashFor(payload) !== line.recordHash) {
|
|
80
|
+
failures.push('record-hash-mismatch');
|
|
81
|
+
}
|
|
82
|
+
const at = signedAt(predicate);
|
|
83
|
+
for (const signature of line.envelope.signatures) {
|
|
84
|
+
const check = checkSignature(keyring, signature.keyid, signature.sig, STATEMENT_PAYLOAD_TYPE, payload, at);
|
|
85
|
+
if (check !== 'ok') {
|
|
86
|
+
failures.push(check);
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return [...new Set(failures)];
|
|
91
|
+
}
|
|
92
|
+
/** Merge order for readers: `(recordedAt, writer, seq)`. */
|
|
93
|
+
export function mergeOrder(a, b) {
|
|
94
|
+
const left = a.statement.predicate;
|
|
95
|
+
const right = b.statement.predicate;
|
|
96
|
+
const time = Date.parse(left.recordedAt) - Date.parse(right.recordedAt);
|
|
97
|
+
if (time !== 0)
|
|
98
|
+
return time;
|
|
99
|
+
if (left.writer !== right.writer)
|
|
100
|
+
return left.writer < right.writer ? -1 : 1;
|
|
101
|
+
return left.seq - right.seq;
|
|
102
|
+
}
|
|
103
|
+
//# sourceMappingURL=reader.js.map
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Effect record construction and decoding: in-toto Statement v1 predicates,
|
|
3
|
+
* DSSE envelopes, segment lines and record hashes.
|
|
4
|
+
*
|
|
5
|
+
* Verifiers check the exact decoded payload bytes; they never reserialize a
|
|
6
|
+
* statement before signature verification.
|
|
7
|
+
*
|
|
8
|
+
* @see docs/contracts/effect-ledger.v1.md "Envelope and record"
|
|
9
|
+
*/
|
|
10
|
+
import { canonicalJson, decodeBase64, dssePae } from '../security/artifact-trust.js';
|
|
11
|
+
import { integrityError } from './errors.js';
|
|
12
|
+
import { effectIdDerivation, sha256Digest } from './identity.js';
|
|
13
|
+
import { assertDigestOnly } from './redaction.js';
|
|
14
|
+
import { assertEffectSchema, isEffectSchemaValid } from './schema.js';
|
|
15
|
+
import { EFFECT_PREDICATE_TYPE, IN_TOTO_STATEMENT_TYPE, SEGMENT_LINE_SCHEMA_VERSION, STATEMENT_PAYLOAD_TYPE, } from './types.js';
|
|
16
|
+
const sha256Hex = (value) => sha256Digest(value).slice('sha256:'.length);
|
|
17
|
+
/** Wrap a predicate in a Statement whose single subject is the target reference and its digest. */
|
|
18
|
+
export function statementFor(predicate) {
|
|
19
|
+
const target = predicate.target;
|
|
20
|
+
if (typeof target !== 'string')
|
|
21
|
+
throw integrityError('statement-target-missing');
|
|
22
|
+
return {
|
|
23
|
+
_type: IN_TOTO_STATEMENT_TYPE,
|
|
24
|
+
subject: [{ name: target, digest: { sha256: sha256Hex(target) } }],
|
|
25
|
+
predicateType: EFFECT_PREDICATE_TYPE,
|
|
26
|
+
predicate,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
/** `"sha256:"` plus hex SHA-256 of the DSSE PAE of exact payload bytes. */
|
|
30
|
+
export function recordHashFor(payload) {
|
|
31
|
+
return sha256Digest(dssePae(STATEMENT_PAYLOAD_TYPE, payload));
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Validate, scan and sign a statement into a segment line. `recordHash` is the
|
|
35
|
+
* PAE hash of the payload, except for a tombstone, which keeps the original.
|
|
36
|
+
*/
|
|
37
|
+
export function signSegmentLine(statement, key, recordHash) {
|
|
38
|
+
assertDigestOnly(statement);
|
|
39
|
+
assertEffectSchema('record', statement);
|
|
40
|
+
const payload = Buffer.from(canonicalJson(statement), 'utf8');
|
|
41
|
+
const line = {
|
|
42
|
+
schemaVersion: SEGMENT_LINE_SCHEMA_VERSION,
|
|
43
|
+
writer: statement.predicate.writer,
|
|
44
|
+
seq: statement.predicate.seq,
|
|
45
|
+
recordHash: recordHash ?? recordHashFor(payload),
|
|
46
|
+
envelope: {
|
|
47
|
+
payloadType: STATEMENT_PAYLOAD_TYPE,
|
|
48
|
+
payload: payload.toString('base64'),
|
|
49
|
+
signatures: [{ keyid: key.keyid, sig: key.signPae(STATEMENT_PAYLOAD_TYPE, payload) }],
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
assertEffectSchema('record', line);
|
|
53
|
+
return line;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Parse one segment line and its statement. Returns a failure reason instead of
|
|
57
|
+
* throwing so the verifier can report every broken line.
|
|
58
|
+
*/
|
|
59
|
+
export function decodeSegmentLine(text) {
|
|
60
|
+
let line;
|
|
61
|
+
try {
|
|
62
|
+
line = JSON.parse(text);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return { failure: 'segment-line-malformed' };
|
|
66
|
+
}
|
|
67
|
+
if (!isEffectSchemaValid('record', line) || line.envelope === undefined)
|
|
68
|
+
return { failure: 'segment-line-schema-invalid' };
|
|
69
|
+
const typed = line;
|
|
70
|
+
let payload;
|
|
71
|
+
try {
|
|
72
|
+
payload = decodeBase64(typed.envelope.payload, 'payload');
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return { failure: 'segment-line-malformed' };
|
|
76
|
+
}
|
|
77
|
+
let statement;
|
|
78
|
+
try {
|
|
79
|
+
statement = JSON.parse(payload.toString('utf8'));
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return { failure: 'statement-malformed' };
|
|
83
|
+
}
|
|
84
|
+
if (canonicalJson(statement) !== payload.toString('utf8'))
|
|
85
|
+
return { failure: 'statement-not-canonical' };
|
|
86
|
+
if (!isEffectSchemaValid('record', statement) || statement.predicate === undefined)
|
|
87
|
+
return { failure: 'statement-schema-invalid' };
|
|
88
|
+
return { line: typed, payload, statement: statement };
|
|
89
|
+
}
|
|
90
|
+
/** Recompute a non-tombstone record's ID from its signed identity fields. */
|
|
91
|
+
export function effectIdBindingHolds(predicate) {
|
|
92
|
+
if (predicate.phase === 'tombstone')
|
|
93
|
+
return true;
|
|
94
|
+
try {
|
|
95
|
+
return effectIdDerivation(predicate.idDerivation).derive({
|
|
96
|
+
scope: predicate.scope, kind: predicate.kind, target: predicate.target, context: predicate.context,
|
|
97
|
+
}) === predicate.effectId;
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/** The subject binds the target (or, for a tombstone, the effect ID) to its digest. */
|
|
104
|
+
export function subjectBindingHolds(statement) {
|
|
105
|
+
const [subject] = statement.subject;
|
|
106
|
+
if (statement.predicate.phase === 'tombstone')
|
|
107
|
+
return subject.name === `aiwg-effect:${statement.predicate.effectId}`;
|
|
108
|
+
return subject.name === statement.predicate.target && subject.digest.sha256 === sha256Hex(subject.name);
|
|
109
|
+
}
|
|
110
|
+
/** The time a record's signature must fall inside its key window. */
|
|
111
|
+
export function signedAt(predicate) {
|
|
112
|
+
return predicate.phase === 'tombstone' ? predicate.tombstone.purgedAt : predicate.recordedAt;
|
|
113
|
+
}
|
|
114
|
+
//# sourceMappingURL=records.js.map
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Digest-only guard applied to every effect ledger document before it is
|
|
3
|
+
* signed or written.
|
|
4
|
+
*
|
|
5
|
+
* It composes the D13 restricted-material scan (`assertReviewProjection`,
|
|
6
|
+
* `src/decision/review/validate.ts`) with the evidence-bundle restricted member
|
|
7
|
+
* names (`src/evidence/bundle.ts`), plus private-key and token material and the
|
|
8
|
+
* raw-payload member names the contract forbids. Rejected material is never
|
|
9
|
+
* echoed.
|
|
10
|
+
*/
|
|
11
|
+
import { assertReviewProjection } from '../decision/review/validate.js';
|
|
12
|
+
import { EVIDENCE_RESTRICTED_KEY } from '../evidence/bundle.js';
|
|
13
|
+
import { EffectLedgerError } from './errors.js';
|
|
14
|
+
/** Raw effect payload members. `payload` is allowed only as the DSSE envelope member. */
|
|
15
|
+
const RAW_PAYLOAD_KEY = /^(?:body|rawBody|raw_body|content|text|request|response|payload)$/;
|
|
16
|
+
/** PEM blocks, base64 PKCS#8 Ed25519 private keys, and common token shapes. */
|
|
17
|
+
const RESTRICTED_MATERIAL = /(?:-----BEGIN [A-Z ]*PRIVATE KEY-----|MC4CAQAwBQYDK2Vw|\b(?:aiwg_(?:at|rt)_[A-Za-z0-9_-]{6,}|gh[opsu]_[A-Za-z0-9]{12,}|github_pat_[A-Za-z0-9_]{12,}|xox[abprs]-[A-Za-z0-9-]{10,}|AKIA[0-9A-Z]{16})\b|\bBearer\s+[A-Za-z0-9._~+/-]{12,})/;
|
|
18
|
+
const restricted = () => new EffectLedgerError('usage', 'Effect ledger documents are digest-only; restricted material was refused', 'restricted-material');
|
|
19
|
+
function visit(value, parentKey) {
|
|
20
|
+
if (typeof value === 'string') {
|
|
21
|
+
if (RESTRICTED_MATERIAL.test(value))
|
|
22
|
+
throw restricted();
|
|
23
|
+
}
|
|
24
|
+
else if (Array.isArray(value)) {
|
|
25
|
+
for (const item of value)
|
|
26
|
+
visit(item, parentKey);
|
|
27
|
+
}
|
|
28
|
+
else if (value && typeof value === 'object') {
|
|
29
|
+
for (const [key, child] of Object.entries(value)) {
|
|
30
|
+
const envelopePayload = key === 'payload' && parentKey === 'envelope';
|
|
31
|
+
if (EVIDENCE_RESTRICTED_KEY.test(key) || (!envelopePayload && RAW_PAYLOAD_KEY.test(key)))
|
|
32
|
+
throw restricted();
|
|
33
|
+
visit(child, key);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/** Throw a usage error (exit 2) when `value` carries restricted material or raw payload members. */
|
|
38
|
+
export function assertDigestOnly(value) {
|
|
39
|
+
try {
|
|
40
|
+
assertReviewProjection(value);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
throw restricted();
|
|
44
|
+
}
|
|
45
|
+
visit(value, undefined);
|
|
46
|
+
}
|
|
47
|
+
/** True when `text` contains private-key or token-shaped material. For output canary scans. */
|
|
48
|
+
export function containsRestrictedMaterial(text) {
|
|
49
|
+
return RESTRICTED_MATERIAL.test(text) || /vault:\/\//i.test(text);
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=redaction.js.map
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime validation of every effect ledger document against the committed
|
|
3
|
+
* v1 schemas (`schemas/effects/*`). Compiled lazily, once per process.
|
|
4
|
+
*/
|
|
5
|
+
import { readFileSync } from 'node:fs';
|
|
6
|
+
import { dirname, join, resolve } from 'node:path';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import Ajv2020 from 'ajv/dist/2020.js';
|
|
9
|
+
import addFormats from 'ajv-formats';
|
|
10
|
+
import { integrityError } from './errors.js';
|
|
11
|
+
export const EFFECT_SCHEMA_IDS = Object.freeze({
|
|
12
|
+
record: 'https://aiwg.io/schemas/effects/EffectRecord.v1.schema.json',
|
|
13
|
+
checkpoint: 'https://aiwg.io/schemas/effects/EffectCheckpoint.v1.schema.json',
|
|
14
|
+
keyring: 'https://aiwg.io/schemas/effects/EffectKeyring.v1.schema.json',
|
|
15
|
+
verifierResult: 'https://aiwg.io/schemas/effects/EffectVerifierResult.v1.schema.json',
|
|
16
|
+
});
|
|
17
|
+
const SCHEMA_FILES = {
|
|
18
|
+
record: 'EffectRecord.v1.schema.json',
|
|
19
|
+
checkpoint: 'EffectCheckpoint.v1.schema.json',
|
|
20
|
+
keyring: 'EffectKeyring.v1.schema.json',
|
|
21
|
+
verifierResult: 'EffectVerifierResult.v1.schema.json',
|
|
22
|
+
};
|
|
23
|
+
function packageRoot(start) {
|
|
24
|
+
let current = resolve(start);
|
|
25
|
+
for (;;) {
|
|
26
|
+
try {
|
|
27
|
+
const pkg = JSON.parse(readFileSync(join(current, 'package.json'), 'utf8'));
|
|
28
|
+
// The release packager renames the installed package to `@aiwg/cli`.
|
|
29
|
+
if (pkg.name === 'aiwg' || pkg.name === '@aiwg/cli')
|
|
30
|
+
return current;
|
|
31
|
+
}
|
|
32
|
+
catch { /* keep walking */ }
|
|
33
|
+
const parent = dirname(current);
|
|
34
|
+
if (parent === current)
|
|
35
|
+
throw new Error('effect ledger: could not locate the package root for schemas/effects');
|
|
36
|
+
current = parent;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
let validators;
|
|
40
|
+
function compiled() {
|
|
41
|
+
if (validators)
|
|
42
|
+
return validators;
|
|
43
|
+
const directory = join(packageRoot(dirname(fileURLToPath(import.meta.url))), 'schemas', 'effects');
|
|
44
|
+
const ajv = new Ajv2020({ strict: true, allErrors: true });
|
|
45
|
+
addFormats(ajv);
|
|
46
|
+
for (const file of Object.values(SCHEMA_FILES))
|
|
47
|
+
ajv.addSchema(JSON.parse(readFileSync(join(directory, file), 'utf8')));
|
|
48
|
+
const entries = Object.keys(EFFECT_SCHEMA_IDS).map((name) => {
|
|
49
|
+
const validate = ajv.getSchema(EFFECT_SCHEMA_IDS[name]);
|
|
50
|
+
if (!validate)
|
|
51
|
+
throw new Error(`effect ledger: schema ${name} did not compile`);
|
|
52
|
+
return [name, validate];
|
|
53
|
+
});
|
|
54
|
+
validators = Object.fromEntries(entries);
|
|
55
|
+
return validators;
|
|
56
|
+
}
|
|
57
|
+
/** True when `value` conforms to the named v1 schema. */
|
|
58
|
+
export function isEffectSchemaValid(name, value) {
|
|
59
|
+
return compiled()[name](value) === true;
|
|
60
|
+
}
|
|
61
|
+
/** Instance paths of schema violations, without echoing instance values. */
|
|
62
|
+
export function effectSchemaErrors(name, value) {
|
|
63
|
+
const validate = compiled()[name];
|
|
64
|
+
if (validate(value))
|
|
65
|
+
return [];
|
|
66
|
+
return (validate.errors ?? []).map(error => `${error.instancePath || '/'} ${error.keyword}`);
|
|
67
|
+
}
|
|
68
|
+
/** Refuse a document that does not conform; the reason names the schema only. */
|
|
69
|
+
export function assertEffectSchema(name, value) {
|
|
70
|
+
if (!isEffectSchemaValid(name, value))
|
|
71
|
+
throw integrityError(`schema-invalid:${name}`, `Effect ledger ${name} document does not conform to its v1 schema`);
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=schema.js.map
|