@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,75 @@
|
|
|
1
|
+
/** Adds every dispatched attempt once, including failed, retry, and fallback consumption. */
|
|
2
|
+
export function batchAccountingTotals(receipt) {
|
|
3
|
+
let input = 0;
|
|
4
|
+
let output = 0;
|
|
5
|
+
let known = 0;
|
|
6
|
+
let unknown = 0;
|
|
7
|
+
let bound = 0;
|
|
8
|
+
let hasBound = false;
|
|
9
|
+
for (const attempt of receipt.attempts) {
|
|
10
|
+
input = addKnown(input, attempt.usage.inputTokens);
|
|
11
|
+
output = addKnown(output, attempt.usage.outputTokens);
|
|
12
|
+
if (attempt.cost.kind === 'provider-authoritative' || attempt.cost.kind === 'client-derived')
|
|
13
|
+
known += attempt.cost.amountMicros;
|
|
14
|
+
else if (attempt.cost.kind === 'bounded-unknown') {
|
|
15
|
+
unknown++;
|
|
16
|
+
bound += attempt.cost.upperBoundMicros;
|
|
17
|
+
hasBound = true;
|
|
18
|
+
}
|
|
19
|
+
else
|
|
20
|
+
unknown++;
|
|
21
|
+
}
|
|
22
|
+
return { usage: { inputTokens: input, outputTokens: output },
|
|
23
|
+
cost: { knownAmountMicros: known, unknownAttemptCount: unknown,
|
|
24
|
+
conservativeUpperBoundMicros: unknown === 0 ? known : hasBound && receipt.attempts.every(a => a.cost.kind !== 'unknown') ? known + bound : null } };
|
|
25
|
+
}
|
|
26
|
+
/** Admission consumes receipt-level evidence only; per-answer estimates are deliberately ignored. */
|
|
27
|
+
export function batchEnforcementCostMicros(receipt) {
|
|
28
|
+
const total = batchAccountingTotals(receipt).cost;
|
|
29
|
+
return total.unknownAttemptCount === 0 ? total.knownAmountMicros : total.conservativeUpperBoundMicros;
|
|
30
|
+
}
|
|
31
|
+
/** Exact integer arithmetic; USD 0.042/million is 42,000 micros/million. */
|
|
32
|
+
export function deriveCost(usage, catalog) {
|
|
33
|
+
if (usage.inputTokens === null || usage.outputTokens === null)
|
|
34
|
+
return { kind: 'unknown' };
|
|
35
|
+
validateRate(catalog.inputMicrosPerMillionTokens);
|
|
36
|
+
validateRate(catalog.outputMicrosPerMillionTokens);
|
|
37
|
+
const numerator = BigInt(usage.inputTokens) * BigInt(catalog.inputMicrosPerMillionTokens)
|
|
38
|
+
+ BigInt(usage.outputTokens) * BigInt(catalog.outputMicrosPerMillionTokens);
|
|
39
|
+
const micros = Number((numerator + 500000n) / 1000000n);
|
|
40
|
+
if (!Number.isSafeInteger(micros))
|
|
41
|
+
throw new RangeError('Derived batch cost exceeds safe integer range');
|
|
42
|
+
return { kind: 'client-derived', currency: 'USD', amountMicros: micros,
|
|
43
|
+
priceCatalogId: catalog.id, priceCatalogVersion: catalog.version, effectiveAt: catalog.effectiveAt };
|
|
44
|
+
}
|
|
45
|
+
/** Largest remainder makes integer allocations deterministic and exactly reconciling. */
|
|
46
|
+
export function allocateEstimatedUsage(questionIds, total, weights = {}) {
|
|
47
|
+
if (!questionIds.length || new Set(questionIds).size !== questionIds.length)
|
|
48
|
+
throw new RangeError('Unique question IDs are required');
|
|
49
|
+
const normalized = questionIds.map(id => weights[id] ?? 1);
|
|
50
|
+
if (normalized.some(weight => !Number.isSafeInteger(weight) || weight <= 0))
|
|
51
|
+
throw new RangeError('Allocation weights must be positive integers');
|
|
52
|
+
const input = allocate(total.inputTokens, normalized, questionIds);
|
|
53
|
+
const output = allocate(total.outputTokens, normalized, questionIds);
|
|
54
|
+
return questionIds.map((questionId, index) => ({ questionId, kind: 'estimated',
|
|
55
|
+
algorithm: 'largest-remainder-weighted', algorithmVersion: '1',
|
|
56
|
+
inputTokens: input?.[index] ?? null, outputTokens: output?.[index] ?? null }));
|
|
57
|
+
}
|
|
58
|
+
function allocate(total, weights, ids) {
|
|
59
|
+
if (total === null)
|
|
60
|
+
return null;
|
|
61
|
+
if (!Number.isSafeInteger(total) || total < 0)
|
|
62
|
+
throw new RangeError('Usage must be a non-negative safe integer or null');
|
|
63
|
+
const denominator = weights.reduce((sum, value) => sum + BigInt(value), 0n);
|
|
64
|
+
const base = weights.map(weight => Number(BigInt(total) * BigInt(weight) / denominator));
|
|
65
|
+
let remaining = total - base.reduce((sum, value) => sum + value, 0);
|
|
66
|
+
const order = weights.map((weight, index) => ({ index, remainder: (BigInt(total) * BigInt(weight)) % denominator, id: ids[index] }))
|
|
67
|
+
.sort((a, b) => a.remainder === b.remainder ? a.id.localeCompare(b.id) : a.remainder > b.remainder ? -1 : 1);
|
|
68
|
+
for (let index = 0; index < remaining; index++)
|
|
69
|
+
base[order[index].index]++;
|
|
70
|
+
return base;
|
|
71
|
+
}
|
|
72
|
+
function addKnown(sum, value) { return sum === null || value === null ? null : sum + value; }
|
|
73
|
+
function validateRate(value) { if (!Number.isSafeInteger(value) || value < 0)
|
|
74
|
+
throw new RangeError('Catalog rates must be non-negative integer micros'); }
|
|
75
|
+
//# sourceMappingURL=accounting.js.map
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
/** Raw request IDs, state, response bodies, credentials, and reasoning cannot enter this shape. */
|
|
3
|
+
export function sanitizedBatchReceiptExport(receipt, requestIdHashSalt) {
|
|
4
|
+
if (requestIdHashSalt.length < 16)
|
|
5
|
+
throw new Error('A deployment-scoped request-ID hash salt of at least 16 characters is required');
|
|
6
|
+
return {
|
|
7
|
+
schemaVersion: 'decision-batch-receipt-export/v1', batchId: receipt.batchId,
|
|
8
|
+
invocationId: receipt.invocationId, runId: receipt.runId,
|
|
9
|
+
plan: { planDigest: receipt.plan.planDigest, partitionId: receipt.plan.partitionId,
|
|
10
|
+
nativeBatchGroupId: receipt.plan.nativeBatchGroupId },
|
|
11
|
+
subjectHash: receipt.subjectHash, stateHash: receipt.stateHash, questionIds: [...receipt.questionIds],
|
|
12
|
+
status: receipt.status, createdAtEpochMs: receipt.createdAtEpochMs, updatedAtEpochMs: receipt.updatedAtEpochMs,
|
|
13
|
+
terminalAtEpochMs: receipt.terminalAtEpochMs,
|
|
14
|
+
attempts: receipt.attempts.map(attempt => ({
|
|
15
|
+
ordinal: attempt.ordinal, adapterId: attempt.adapterId, adapterVersion: attempt.adapterVersion,
|
|
16
|
+
requestedModel: attempt.requestedModel, actualModel: attempt.actualModel, status: attempt.status,
|
|
17
|
+
dispatchedAtEpochMs: attempt.dispatchedAtEpochMs, completedAtEpochMs: attempt.completedAtEpochMs,
|
|
18
|
+
usage: { inputTokens: attempt.usage.inputTokens, outputTokens: attempt.usage.outputTokens },
|
|
19
|
+
cost: exportCost(attempt.cost),
|
|
20
|
+
fallbackFromAttemptOrdinal: attempt.fallbackFromAttemptOrdinal,
|
|
21
|
+
providerRequestIdHash: attempt.providerRequestId === null ? null : `sha256:${createHash('sha256').update(requestIdHashSalt).update('\0').update(attempt.providerRequestId).digest('hex')}`
|
|
22
|
+
})),
|
|
23
|
+
answerReferences: receipt.answerReferences.map(ref => ({ questionId: ref.questionId, answerId: ref.answerId,
|
|
24
|
+
resultId: ref.resultId })),
|
|
25
|
+
allocations: receipt.allocations.map(value => ({ questionId: value.questionId, kind: value.kind,
|
|
26
|
+
algorithm: value.algorithm, algorithmVersion: value.algorithmVersion,
|
|
27
|
+
inputTokens: value.inputTokens, outputTokens: value.outputTokens })),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function exportCost(cost) {
|
|
31
|
+
switch (cost.kind) {
|
|
32
|
+
case 'provider-authoritative': return { kind: cost.kind, currency: cost.currency, amountMicros: cost.amountMicros };
|
|
33
|
+
case 'client-derived': return { kind: cost.kind, currency: cost.currency, amountMicros: cost.amountMicros,
|
|
34
|
+
priceCatalogId: cost.priceCatalogId, priceCatalogVersion: cost.priceCatalogVersion, effectiveAt: cost.effectiveAt };
|
|
35
|
+
case 'bounded-unknown': return { kind: cost.kind, currency: cost.currency, upperBoundMicros: cost.upperBoundMicros,
|
|
36
|
+
boundPolicyId: cost.boundPolicyId, boundPolicyVersion: cost.boundPolicyVersion };
|
|
37
|
+
case 'unknown': return { kind: 'unknown' };
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=export.js.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export * from './types.js';
|
|
2
|
+
export * from './validate.js';
|
|
3
|
+
export * from './accounting.js';
|
|
4
|
+
export * from './receipt.js';
|
|
5
|
+
export * from './store.js';
|
|
6
|
+
export * from './result-store.js';
|
|
7
|
+
export * from './export.js';
|
|
8
|
+
export * from './protection.js';
|
|
9
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { ensurePrivateDirectory as ensureProtectedDirectory, parseCanonical as parseProtectedCanonical, publishTombstone, } from '../../storage/protected-files.js';
|
|
2
|
+
import { validateDecisionLifecyclePolicy } from '../lifecycle.js';
|
|
3
|
+
import { BatchReceiptValidationError } from './validate.js';
|
|
4
|
+
/** Opaque by design: callers learn only that stored bytes failed verification. */
|
|
5
|
+
export class BatchStoreIntegrityError extends Error {
|
|
6
|
+
constructor() { super('Batch store integrity check failed'); this.name = 'BatchStoreIntegrityError'; }
|
|
7
|
+
}
|
|
8
|
+
/** Erased, expired, or tombstoned durable state. Replay must neither re-dispatch nor serve it. */
|
|
9
|
+
export class BatchRecordUnavailableError extends Error {
|
|
10
|
+
code = 'batch-record-unavailable';
|
|
11
|
+
constructor() { super('Batch record unavailable'); this.name = 'BatchRecordUnavailableError'; }
|
|
12
|
+
}
|
|
13
|
+
/** Unkeyed records from before integrity protection. They are read-only until explicitly migrated. */
|
|
14
|
+
export class BatchStoreMigrationRequiredError extends Error {
|
|
15
|
+
code = 'batch-store-migration-required';
|
|
16
|
+
constructor() { super('Batch store requires explicit migration'); this.name = 'BatchStoreMigrationRequiredError'; }
|
|
17
|
+
}
|
|
18
|
+
// Generic protected-file primitives live in src/storage; they are re-exported
|
|
19
|
+
// here so existing batch-store imports keep their names and behavior.
|
|
20
|
+
export { exists, keyedName, macFor, macMatches, publishExclusive, requireIntegrityKey, serialized, syncDirectory, } from '../../storage/protected-files.js';
|
|
21
|
+
export function receiptLifecycleRule(binding) {
|
|
22
|
+
validateDecisionLifecyclePolicy(binding?.lifecycle);
|
|
23
|
+
return binding.lifecycle.surfaces.receipt;
|
|
24
|
+
}
|
|
25
|
+
/** Stored bytes must be exactly the canonical serialization, so no byte can change unnoticed. */
|
|
26
|
+
export function parseCanonical(raw) {
|
|
27
|
+
return parseProtectedCanonical(raw, () => new BatchStoreIntegrityError());
|
|
28
|
+
}
|
|
29
|
+
export async function ensurePrivateDirectory(directory, message) {
|
|
30
|
+
await ensureProtectedDirectory(directory, message, (text) => new BatchReceiptValidationError(text ?? message));
|
|
31
|
+
}
|
|
32
|
+
/** Body-free local tombstone. Its presence alone blocks reads, writes, acquisition and restore. */
|
|
33
|
+
export async function writeTombstone(directory, path, reference, deletedAt) {
|
|
34
|
+
// An existing tombstone already has the same effect; erasure is idempotent.
|
|
35
|
+
await publishTombstone(directory, path, { version: 'decision-batch-tombstone/v1', reference, deletedAtEpochMs: deletedAt }, 'batch-tombstone');
|
|
36
|
+
}
|
|
37
|
+
export function expired(createdAtEpochMs, now, rule) {
|
|
38
|
+
return !Number.isSafeInteger(now) || now < createdAtEpochMs || now - createdAtEpochMs >= rule.retentionMs;
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=protection.js.map
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { assertNoPortableSecretMaterial } from '../portable-secrets.js';
|
|
2
|
+
import { BatchReceiptValidationError, validateBatchReceipt, validateBatchReceiptTransition } from './validate.js';
|
|
3
|
+
/** Integration hook for D04 native groups and D06 deterministic context partitions. */
|
|
4
|
+
export function newBatchReceipt(input) {
|
|
5
|
+
if (input.partition.subject !== input.contextPlan.subject
|
|
6
|
+
|| !input.contextPlan.partitions.some(candidate => candidate.id === input.partition.id)) {
|
|
7
|
+
throw new Error('Batch partition does not belong to the supplied context plan');
|
|
8
|
+
}
|
|
9
|
+
const receipt = {
|
|
10
|
+
schemaVersion: 'decision-batch-receipt/v1', revision: 1,
|
|
11
|
+
tenantId: input.tenantId, projectId: input.projectId, batchId: input.batchId,
|
|
12
|
+
invocationId: input.invocationId, runId: input.runId,
|
|
13
|
+
plan: { planDigest: input.contextPlan.planDigest, partitionId: input.partition.id,
|
|
14
|
+
nativeBatchGroupId: input.nativeBatchGroupId ?? null },
|
|
15
|
+
subjectHash: input.subjectHash, stateHash: input.partition.stateDigest,
|
|
16
|
+
executionEnvelope: input.executionEnvelope, questionIds: [...input.partition.questionIds],
|
|
17
|
+
attempts: [], answerReferences: [], allocations: [], status: 'acquired',
|
|
18
|
+
createdAtEpochMs: input.nowEpochMs, updatedAtEpochMs: input.nowEpochMs, terminalAtEpochMs: null,
|
|
19
|
+
...(input.traceParent !== undefined ? { traceParent: input.traceParent } : {}),
|
|
20
|
+
};
|
|
21
|
+
validateBatchReceipt(receipt);
|
|
22
|
+
assertBatchReceiptPortable(receipt);
|
|
23
|
+
return receipt;
|
|
24
|
+
}
|
|
25
|
+
export function nextBatchReceipt(previous, update) {
|
|
26
|
+
const terminal = update.status === 'completed' || update.status === 'failed' || update.status === 'execution-uncertain';
|
|
27
|
+
const next = { ...structuredClone(previous), ...structuredClone(update),
|
|
28
|
+
revision: previous.revision + 1, terminalAtEpochMs: terminal ? update.updatedAtEpochMs : null };
|
|
29
|
+
validateBatchReceiptTransition(previous, next);
|
|
30
|
+
assertBatchReceiptPortable(next);
|
|
31
|
+
return next;
|
|
32
|
+
}
|
|
33
|
+
/** Batch receipts are portable artifacts: reject embedded keys, bearer values, private locators, and secret hashes. */
|
|
34
|
+
export function assertBatchReceiptPortable(receipt) {
|
|
35
|
+
assertNoPortableSecretMaterial(receipt, 'Batch receipt', message => new BatchReceiptValidationError(message));
|
|
36
|
+
}
|
|
37
|
+
export function batchResultReference(receipt, questionId) {
|
|
38
|
+
const answer = receipt.answerReferences.find(candidate => candidate.questionId === questionId);
|
|
39
|
+
if (!answer)
|
|
40
|
+
throw new Error(`No answer reference for question '${questionId}'`);
|
|
41
|
+
return { schemaVersion: 'decision-batch-result-ref/v1', batchId: receipt.batchId,
|
|
42
|
+
receiptRevision: receipt.revision, questionId, answerId: answer.answerId };
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=receipt.js.map
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
import { createCipheriv, createDecipheriv, createHash, randomBytes } from 'node:crypto';
|
|
2
|
+
import { lstat, readFile, readdir, rm } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { canonicalJson } from '../../security/artifact-trust.js';
|
|
5
|
+
import { BatchRecordUnavailableError, BatchStoreIntegrityError, BatchStoreMigrationRequiredError, ensurePrivateDirectory, exists, expired, keyedName, macFor, macMatches, parseCanonical, publishExclusive, receiptLifecycleRule, requireIntegrityKey, serialized, syncDirectory, writeTombstone } from './protection.js';
|
|
6
|
+
import { BatchReceiptValidationError } from './validate.js';
|
|
7
|
+
export class MemoryBatchResultStore {
|
|
8
|
+
records = new Map();
|
|
9
|
+
async writeMany(receipt, observations) {
|
|
10
|
+
const snapshots = snapshotsFor(receipt, observations);
|
|
11
|
+
for (const snapshot of snapshots) {
|
|
12
|
+
const existing = this.records.get(snapshotKey(snapshot));
|
|
13
|
+
if (existing && canonicalJson(existing) !== canonicalJson(snapshot)) {
|
|
14
|
+
throw new BatchReceiptValidationError('Conflicting batch result publication');
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
for (const snapshot of snapshots)
|
|
18
|
+
this.records.set(snapshotKey(snapshot), structuredClone(snapshot));
|
|
19
|
+
}
|
|
20
|
+
async readMany(receipt) {
|
|
21
|
+
const found = new Map();
|
|
22
|
+
for (const reference of receipt.answerReferences) {
|
|
23
|
+
const snapshot = this.records.get(snapshotKey({ ...scope(receipt), questionId: reference.questionId, answerId: reference.answerId }));
|
|
24
|
+
if (!snapshot)
|
|
25
|
+
return new Map();
|
|
26
|
+
validateSnapshotForReceipt(snapshot, receipt);
|
|
27
|
+
found.set(reference.questionId, structuredClone(snapshot.observation));
|
|
28
|
+
}
|
|
29
|
+
return found;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
const ENVELOPE = 'decision-batch-result-envelope/v1';
|
|
33
|
+
const MAC_DOMAIN = 'decision-batch-result/v1';
|
|
34
|
+
const SEALED = /^([a-f0-9]{64})\.([a-f0-9]{64})\.sealed\.json$/;
|
|
35
|
+
const LEGACY = /^([a-f0-9]{64})\.json$/;
|
|
36
|
+
/**
|
|
37
|
+
* Snapshot values are encrypted with AES-256-GCM. The AAD binds tenant, project, batch, question,
|
|
38
|
+
* answer and receipt revision, so a snapshot moved to any other scope fails authentication.
|
|
39
|
+
*/
|
|
40
|
+
export class FileBatchResultStore {
|
|
41
|
+
directory;
|
|
42
|
+
options;
|
|
43
|
+
key;
|
|
44
|
+
rule;
|
|
45
|
+
clock;
|
|
46
|
+
constructor(directory, options) {
|
|
47
|
+
this.directory = directory;
|
|
48
|
+
this.options = options;
|
|
49
|
+
this.key = requireIntegrityKey(options?.integrityKey, 'Batch result');
|
|
50
|
+
if (typeof options.resolveEncryptionKey !== 'function' || typeof options.encryptionKeyReference !== 'string'
|
|
51
|
+
|| !options.encryptionKeyReference)
|
|
52
|
+
throw new Error('Batch result encryption key resolver required');
|
|
53
|
+
this.rule = receiptLifecycleRule(options);
|
|
54
|
+
this.clock = options.clock ?? Date.now;
|
|
55
|
+
}
|
|
56
|
+
async writeMany(receipt, observations) {
|
|
57
|
+
await this.ensurePrivateDirectory();
|
|
58
|
+
const snapshots = snapshotsFor(receipt, observations);
|
|
59
|
+
if (await exists(this.tombstonePath(this.batchPrefix(receipt))))
|
|
60
|
+
throw new BatchRecordUnavailableError();
|
|
61
|
+
for (const snapshot of snapshots) {
|
|
62
|
+
if (await exists(join(this.directory, `${legacyName(snapshot)}.json`)))
|
|
63
|
+
throw new BatchStoreMigrationRequiredError();
|
|
64
|
+
}
|
|
65
|
+
for (const snapshot of snapshots)
|
|
66
|
+
await this.publish(snapshot);
|
|
67
|
+
}
|
|
68
|
+
/** Erased, expired and missing snapshots all return an empty map. A verification failure throws. */
|
|
69
|
+
async readMany(receipt) {
|
|
70
|
+
await this.ensurePrivateDirectory();
|
|
71
|
+
if (await exists(this.tombstonePath(this.batchPrefix(receipt))))
|
|
72
|
+
return new Map();
|
|
73
|
+
// Values expire with their primary receipt as well as on their own creation time.
|
|
74
|
+
if (expired(receipt.createdAtEpochMs, this.clock(), this.rule))
|
|
75
|
+
return new Map();
|
|
76
|
+
const found = new Map();
|
|
77
|
+
for (const reference of receipt.answerReferences) {
|
|
78
|
+
const identity = { ...scope(receipt), receiptRevision: receipt.revision,
|
|
79
|
+
questionId: reference.questionId, answerId: reference.answerId };
|
|
80
|
+
const envelope = await this.envelope(this.directory, this.sealedName(identity));
|
|
81
|
+
if (!envelope) {
|
|
82
|
+
if (await exists(join(this.directory, `${legacyName(identity)}.json`)))
|
|
83
|
+
throw new BatchStoreMigrationRequiredError();
|
|
84
|
+
return new Map();
|
|
85
|
+
}
|
|
86
|
+
if (expired(envelope.createdAtEpochMs, this.clock(), this.rule))
|
|
87
|
+
return new Map();
|
|
88
|
+
const snapshot = await this.decrypt(envelope, identity);
|
|
89
|
+
validateSnapshotForReceipt(snapshot, receipt);
|
|
90
|
+
found.set(reference.questionId, structuredClone(snapshot.observation));
|
|
91
|
+
}
|
|
92
|
+
return found;
|
|
93
|
+
}
|
|
94
|
+
/** Cascade target of batch receipt erasure: a body-free tombstone, then every snapshot body is removed. */
|
|
95
|
+
async eraseBatch(batchId, tenantId, projectId) {
|
|
96
|
+
await this.ensurePrivateDirectory();
|
|
97
|
+
const prefix = this.batchPrefix({ batchId, tenantId, projectId });
|
|
98
|
+
await writeTombstone(this.directory, this.tombstonePath(prefix), { surface: 'receipt', opaqueId: `batch-result-${prefix}` }, this.clock());
|
|
99
|
+
for (const name of await readdir(this.directory)) {
|
|
100
|
+
if (SEALED.exec(name)?.[1] === prefix)
|
|
101
|
+
await rm(join(this.directory, name), { force: true });
|
|
102
|
+
}
|
|
103
|
+
await syncDirectory(this.directory);
|
|
104
|
+
}
|
|
105
|
+
/** Restore sealed snapshots from a backup copy, refusing erased batches, expired values and unverifiable files. */
|
|
106
|
+
async restoreFrom(backupDirectory) {
|
|
107
|
+
if (this.rule.backup === 'not-persisted')
|
|
108
|
+
throw new Error('Batch result backup restore denied by lifecycle policy');
|
|
109
|
+
await this.ensurePrivateDirectory();
|
|
110
|
+
const report = { restored: 0, refused: 0 };
|
|
111
|
+
for (const name of await readdir(backupDirectory)) {
|
|
112
|
+
const match = SEALED.exec(name);
|
|
113
|
+
if (!match)
|
|
114
|
+
continue;
|
|
115
|
+
try {
|
|
116
|
+
const envelope = await this.envelope(backupDirectory, name);
|
|
117
|
+
if (!envelope || await exists(this.tombstonePath(match[1]))
|
|
118
|
+
|| expired(envelope.createdAtEpochMs, this.clock(), this.rule)) {
|
|
119
|
+
report.refused++;
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
const contents = serialized(envelope);
|
|
123
|
+
if (!await publishExclusive(this.directory, join(this.directory, name), contents, 'batch-result')
|
|
124
|
+
&& await readFile(join(this.directory, name), 'utf8') !== contents)
|
|
125
|
+
throw new BatchStoreIntegrityError();
|
|
126
|
+
report.restored++;
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
report.refused++;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return report;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Explicit, authorized upgrade of plaintext snapshots written before encryption. Every legacy
|
|
136
|
+
* file is validated before authorization; each is sealed and encrypted, then the plaintext is removed.
|
|
137
|
+
*/
|
|
138
|
+
async migrateLegacy(authorize) {
|
|
139
|
+
await this.ensurePrivateDirectory();
|
|
140
|
+
const legacy = [];
|
|
141
|
+
for (const name of await readdir(this.directory)) {
|
|
142
|
+
const match = LEGACY.exec(name);
|
|
143
|
+
if (!match)
|
|
144
|
+
continue;
|
|
145
|
+
const path = join(this.directory, name);
|
|
146
|
+
await assertPrivateFile(path);
|
|
147
|
+
let snapshot;
|
|
148
|
+
try {
|
|
149
|
+
snapshot = JSON.parse(await readFile(path, 'utf8'));
|
|
150
|
+
validateSnapshotShape(snapshot);
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
throw new BatchStoreIntegrityError();
|
|
154
|
+
}
|
|
155
|
+
if (legacyName(snapshot) !== match[1])
|
|
156
|
+
throw new BatchStoreIntegrityError();
|
|
157
|
+
legacy.push({ name, snapshot });
|
|
158
|
+
}
|
|
159
|
+
if (!legacy.length)
|
|
160
|
+
return { migrated: 0 };
|
|
161
|
+
let approved = false;
|
|
162
|
+
try {
|
|
163
|
+
approved = await authorize({ results: legacy.length }) === true;
|
|
164
|
+
}
|
|
165
|
+
catch { /* authorization fails closed */ }
|
|
166
|
+
if (!approved)
|
|
167
|
+
throw new Error('Batch store migration denied');
|
|
168
|
+
for (const { name, snapshot } of legacy) {
|
|
169
|
+
if (await exists(this.tombstonePath(this.batchPrefix(snapshot))))
|
|
170
|
+
throw new BatchRecordUnavailableError();
|
|
171
|
+
await this.publish(snapshot);
|
|
172
|
+
await rm(join(this.directory, name));
|
|
173
|
+
}
|
|
174
|
+
await syncDirectory(this.directory);
|
|
175
|
+
return { migrated: legacy.length };
|
|
176
|
+
}
|
|
177
|
+
batchPrefix(input) {
|
|
178
|
+
return keyedName(this.key, 'decision-batch-result-batch/v1', [input.tenantId, input.projectId, input.batchId]);
|
|
179
|
+
}
|
|
180
|
+
sealedName(input) {
|
|
181
|
+
return `${this.batchPrefix(input)}.${keyedName(this.key, 'decision-batch-result-answer/v1', [input.questionId, input.answerId])}.sealed.json`;
|
|
182
|
+
}
|
|
183
|
+
tombstonePath(prefix) { return join(this.directory, `${prefix}.tombstone`); }
|
|
184
|
+
async ensurePrivateDirectory() {
|
|
185
|
+
await ensurePrivateDirectory(this.directory, 'Insecure batch result directory');
|
|
186
|
+
}
|
|
187
|
+
async resolveKey(reference) {
|
|
188
|
+
let resolved;
|
|
189
|
+
try {
|
|
190
|
+
resolved = await this.options.resolveEncryptionKey(reference);
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
throw new Error('Batch result encryption key unavailable');
|
|
194
|
+
}
|
|
195
|
+
if (!(resolved instanceof Uint8Array) || resolved.length !== 32)
|
|
196
|
+
throw new Error('Batch result encryption key invalid');
|
|
197
|
+
return Buffer.from(resolved);
|
|
198
|
+
}
|
|
199
|
+
async publish(snapshot) {
|
|
200
|
+
const keyReference = this.options.encryptionKeyReference;
|
|
201
|
+
const nonce = randomBytes(12);
|
|
202
|
+
const key = await this.resolveKey(keyReference);
|
|
203
|
+
let ciphertext;
|
|
204
|
+
let tag;
|
|
205
|
+
try {
|
|
206
|
+
const cipher = createCipheriv('aes-256-gcm', key, nonce);
|
|
207
|
+
cipher.setAAD(aad(snapshot, keyReference, snapshot.createdAtEpochMs));
|
|
208
|
+
ciphertext = Buffer.concat([cipher.update(canonicalJson(snapshot), 'utf8'), cipher.final()]);
|
|
209
|
+
tag = cipher.getAuthTag();
|
|
210
|
+
}
|
|
211
|
+
finally {
|
|
212
|
+
key.fill(0);
|
|
213
|
+
}
|
|
214
|
+
const fields = { version: ENVELOPE, keyReference, createdAtEpochMs: snapshot.createdAtEpochMs,
|
|
215
|
+
nonce: nonce.toString('base64url'), tag: tag.toString('base64url'), ciphertext: ciphertext.toString('base64url') };
|
|
216
|
+
const envelope = { ...fields, mac: macFor(this.key, MAC_DOMAIN, fields) };
|
|
217
|
+
const name = this.sealedName(snapshot);
|
|
218
|
+
if (await publishExclusive(this.directory, join(this.directory, name), serialized(envelope), 'batch-result'))
|
|
219
|
+
return;
|
|
220
|
+
const existing = await this.envelope(this.directory, name);
|
|
221
|
+
if (!existing || canonicalJson(await this.decrypt(existing, snapshot)) !== canonicalJson(snapshot)) {
|
|
222
|
+
throw new BatchReceiptValidationError('Conflicting batch result publication');
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
async envelope(directory, name) {
|
|
226
|
+
const path = join(directory, name);
|
|
227
|
+
if (directory === this.directory) {
|
|
228
|
+
try {
|
|
229
|
+
await assertPrivateFile(path);
|
|
230
|
+
}
|
|
231
|
+
catch (error) {
|
|
232
|
+
if (error.code === 'ENOENT')
|
|
233
|
+
return null;
|
|
234
|
+
throw error;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
try {
|
|
238
|
+
const envelope = parseCanonical(await readFile(path, 'utf8'));
|
|
239
|
+
const { mac, ...fields } = envelope ?? {};
|
|
240
|
+
if (envelope?.version !== ENVELOPE || Object.keys(envelope).length !== 7
|
|
241
|
+
|| typeof envelope.keyReference !== 'string' || !envelope.keyReference
|
|
242
|
+
|| !Number.isSafeInteger(envelope.createdAtEpochMs) || envelope.createdAtEpochMs < 0
|
|
243
|
+
|| ![envelope.nonce, envelope.tag, envelope.ciphertext].every(part => typeof part === 'string' && /^[A-Za-z0-9_-]+$/.test(part))
|
|
244
|
+
|| !macMatches(this.key, MAC_DOMAIN, fields, mac))
|
|
245
|
+
throw new BatchStoreIntegrityError();
|
|
246
|
+
return envelope;
|
|
247
|
+
}
|
|
248
|
+
catch {
|
|
249
|
+
throw new BatchStoreIntegrityError();
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
async decrypt(envelope, identity) {
|
|
253
|
+
const key = await this.resolveKey(envelope.keyReference);
|
|
254
|
+
try {
|
|
255
|
+
const decipher = createDecipheriv('aes-256-gcm', key, Buffer.from(envelope.nonce, 'base64url'));
|
|
256
|
+
decipher.setAAD(aad(identity, envelope.keyReference, envelope.createdAtEpochMs));
|
|
257
|
+
decipher.setAuthTag(Buffer.from(envelope.tag, 'base64url'));
|
|
258
|
+
const plain = Buffer.concat([decipher.update(Buffer.from(envelope.ciphertext, 'base64url')), decipher.final()]);
|
|
259
|
+
const snapshot = JSON.parse(plain.toString('utf8'));
|
|
260
|
+
validateSnapshotShape(snapshot);
|
|
261
|
+
if (snapshot.createdAtEpochMs !== envelope.createdAtEpochMs)
|
|
262
|
+
throw new BatchStoreIntegrityError();
|
|
263
|
+
return snapshot;
|
|
264
|
+
}
|
|
265
|
+
catch {
|
|
266
|
+
throw new BatchStoreIntegrityError();
|
|
267
|
+
}
|
|
268
|
+
finally {
|
|
269
|
+
key.fill(0);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
function aad(identity, keyReference, createdAtEpochMs) {
|
|
274
|
+
return Buffer.from(canonicalJson({ domain: MAC_DOMAIN, tenantId: identity.tenantId, projectId: identity.projectId,
|
|
275
|
+
batchId: identity.batchId, receiptRevision: identity.receiptRevision, questionId: identity.questionId,
|
|
276
|
+
answerId: identity.answerId, keyReference, createdAtEpochMs }), 'utf8');
|
|
277
|
+
}
|
|
278
|
+
async function assertPrivateFile(path) {
|
|
279
|
+
const stat = await lstat(path);
|
|
280
|
+
if (!stat.isFile() || (stat.mode & 0o077) !== 0)
|
|
281
|
+
throw new BatchReceiptValidationError('Insecure batch result file');
|
|
282
|
+
}
|
|
283
|
+
function snapshotsFor(receipt, observations) {
|
|
284
|
+
if (receipt.status !== 'completed' || receipt.terminalAtEpochMs === null) {
|
|
285
|
+
throw new BatchReceiptValidationError('Batch results require a completed receipt');
|
|
286
|
+
}
|
|
287
|
+
if (observations.size !== receipt.answerReferences.length) {
|
|
288
|
+
throw new BatchReceiptValidationError('Batch result set does not match receipt references');
|
|
289
|
+
}
|
|
290
|
+
return receipt.answerReferences.map(reference => {
|
|
291
|
+
const observation = observations.get(reference.questionId);
|
|
292
|
+
if (!observation || observation.status !== 'success') {
|
|
293
|
+
throw new BatchReceiptValidationError('Batch result store only accepts successful completed observations');
|
|
294
|
+
}
|
|
295
|
+
const snapshot = {
|
|
296
|
+
schemaVersion: 'decision-batch-result/v1', ...scope(receipt), receiptRevision: receipt.revision,
|
|
297
|
+
questionId: reference.questionId, answerId: reference.answerId, resultId: reference.resultId,
|
|
298
|
+
observation: structuredClone(observation), createdAtEpochMs: receipt.terminalAtEpochMs,
|
|
299
|
+
};
|
|
300
|
+
validateSnapshotForReceipt(snapshot, receipt);
|
|
301
|
+
return snapshot;
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
function validateSnapshotShape(snapshot) {
|
|
305
|
+
if (!snapshot || snapshot.schemaVersion !== 'decision-batch-result/v1'
|
|
306
|
+
|| [snapshot.tenantId, snapshot.projectId, snapshot.batchId, snapshot.questionId, snapshot.answerId, snapshot.resultId]
|
|
307
|
+
.some(value => typeof value !== 'string' || !value)
|
|
308
|
+
|| !Number.isSafeInteger(snapshot.receiptRevision) || snapshot.receiptRevision < 1
|
|
309
|
+
|| !Number.isSafeInteger(snapshot.createdAtEpochMs) || snapshot.createdAtEpochMs < 0
|
|
310
|
+
|| !snapshot.observation || snapshot.observation.status !== 'success') {
|
|
311
|
+
throw new BatchReceiptValidationError('Invalid batch result snapshot');
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
function validateSnapshotForReceipt(snapshot, receipt) {
|
|
315
|
+
if (snapshot.schemaVersion !== 'decision-batch-result/v1'
|
|
316
|
+
|| snapshot.tenantId !== receipt.tenantId || snapshot.projectId !== receipt.projectId
|
|
317
|
+
|| snapshot.batchId !== receipt.batchId || snapshot.receiptRevision !== receipt.revision) {
|
|
318
|
+
throw new BatchReceiptValidationError('Batch result scope substitution');
|
|
319
|
+
}
|
|
320
|
+
const reference = receipt.answerReferences.find(candidate => candidate.questionId === snapshot.questionId);
|
|
321
|
+
if (!reference || reference.answerId !== snapshot.answerId || reference.resultId !== snapshot.resultId) {
|
|
322
|
+
throw new BatchReceiptValidationError('Batch result reference mismatch');
|
|
323
|
+
}
|
|
324
|
+
if (snapshot.createdAtEpochMs !== receipt.terminalAtEpochMs || snapshot.observation.status !== 'success'
|
|
325
|
+
|| snapshot.observation.requestId !== null || snapshot.observation.requestIdSource !== undefined
|
|
326
|
+
|| snapshot.observation.usage?.inputTokens !== null || snapshot.observation.usage?.outputTokens !== null
|
|
327
|
+
|| snapshot.observation.usage?.costUsd !== null) {
|
|
328
|
+
throw new BatchReceiptValidationError('Batch result must contain only successful value evidence, not shared accounting');
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
function scope(receipt) {
|
|
332
|
+
return { tenantId: receipt.tenantId, projectId: receipt.projectId, batchId: receipt.batchId };
|
|
333
|
+
}
|
|
334
|
+
function snapshotKey(input) {
|
|
335
|
+
return `${input.tenantId}\u0000${input.projectId}\u0000${input.batchId}\u0000${input.questionId}\u0000${input.answerId}`;
|
|
336
|
+
}
|
|
337
|
+
/** Unkeyed name used by stores written before encryption; only migration and refusal checks read it. */
|
|
338
|
+
function legacyName(input) {
|
|
339
|
+
return createHash('sha256').update(snapshotKey(input)).digest('hex');
|
|
340
|
+
}
|
|
341
|
+
//# sourceMappingURL=result-store.js.map
|