@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,362 @@
|
|
|
1
|
+
import { createHash } 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 { mayRestoreDecisionReference } from '../lifecycle.js';
|
|
6
|
+
import { BatchRecordUnavailableError, BatchStoreIntegrityError, BatchStoreMigrationRequiredError, ensurePrivateDirectory, exists, expired, keyedName, macFor, macMatches, parseCanonical, publishExclusive, receiptLifecycleRule, requireIntegrityKey, serialized, syncDirectory, writeTombstone } from './protection.js';
|
|
7
|
+
import { BatchReceiptValidationError, validateBatchReceipt, validateBatchReceiptTransition } from './validate.js';
|
|
8
|
+
export class BatchReceiptConflictError extends Error {
|
|
9
|
+
}
|
|
10
|
+
export class MemoryBatchReceiptStore {
|
|
11
|
+
records = new Map();
|
|
12
|
+
async acquire(initial) {
|
|
13
|
+
validateBatchReceipt(initial);
|
|
14
|
+
if (initial.revision !== 1 || initial.status !== 'acquired')
|
|
15
|
+
throw new BatchReceiptValidationError('Initial receipt must be acquired revision 1');
|
|
16
|
+
const key = scopeKey(initial.batchId, initial.tenantId, initial.projectId);
|
|
17
|
+
const existing = this.records.get(key);
|
|
18
|
+
if (existing) {
|
|
19
|
+
assertSameIdentity(initial, existing);
|
|
20
|
+
return { owner: false, receipt: structuredClone(existing) };
|
|
21
|
+
}
|
|
22
|
+
this.records.set(key, structuredClone(initial));
|
|
23
|
+
return { owner: true, receipt: structuredClone(initial) };
|
|
24
|
+
}
|
|
25
|
+
async read(batchId, tenantId, projectId) {
|
|
26
|
+
return structuredClone(this.records.get(scopeKey(batchId, tenantId, projectId)) ?? null);
|
|
27
|
+
}
|
|
28
|
+
async compareAndSwap(previous, next) {
|
|
29
|
+
validateBatchReceiptTransition(previous, next);
|
|
30
|
+
const key = scopeKey(previous.batchId, previous.tenantId, previous.projectId);
|
|
31
|
+
const current = this.records.get(key);
|
|
32
|
+
if (!current || canonicalJson(current) !== canonicalJson(previous))
|
|
33
|
+
return false;
|
|
34
|
+
this.records.set(key, structuredClone(next));
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const ENVELOPE = 'decision-batch-receipt-envelope/v1';
|
|
39
|
+
const MAC_DOMAIN = 'decision-batch-receipt/v1';
|
|
40
|
+
const REFERENCE = /^batch-receipt-([a-f0-9]{64})$/;
|
|
41
|
+
const SEALED = /^([a-f0-9]{64})\.r([0-9]+)\.sealed\.json$/;
|
|
42
|
+
const LEGACY = /^([a-f0-9]{64})\.r([0-9]+)\.json$/;
|
|
43
|
+
/**
|
|
44
|
+
* Every revision is immutable and sealed with a keyed MAC; hard-link publication supplies
|
|
45
|
+
* filesystem CAS. File names are keyed, so a directory listing does not reveal scope.
|
|
46
|
+
*/
|
|
47
|
+
export class FileBatchReceiptStore {
|
|
48
|
+
directory;
|
|
49
|
+
options;
|
|
50
|
+
key;
|
|
51
|
+
rule;
|
|
52
|
+
clock;
|
|
53
|
+
constructor(directory, options) {
|
|
54
|
+
this.directory = directory;
|
|
55
|
+
this.options = options;
|
|
56
|
+
this.key = requireIntegrityKey(options?.integrityKey, 'Batch receipt');
|
|
57
|
+
this.rule = receiptLifecycleRule(options);
|
|
58
|
+
this.clock = options.clock ?? Date.now;
|
|
59
|
+
}
|
|
60
|
+
/** Opaque D10 reference for host link registration. It never contains scope identifiers. */
|
|
61
|
+
lifecycleReference(batchId, tenantId, projectId) {
|
|
62
|
+
return { surface: 'receipt', opaqueId: `batch-receipt-${this.prefix(batchId, tenantId, projectId)}` };
|
|
63
|
+
}
|
|
64
|
+
async acquire(initial) {
|
|
65
|
+
validateBatchReceipt(initial);
|
|
66
|
+
if (initial.revision !== 1 || initial.status !== 'acquired')
|
|
67
|
+
throw new BatchReceiptValidationError('Initial receipt must be acquired revision 1');
|
|
68
|
+
await this.ensurePrivateDirectory();
|
|
69
|
+
const prefix = this.prefix(initial.batchId, initial.tenantId, initial.projectId);
|
|
70
|
+
// An erased batch must never be re-owned: that would re-dispatch a deleted request.
|
|
71
|
+
if (await this.tombstoned(prefix))
|
|
72
|
+
throw new BatchRecordUnavailableError();
|
|
73
|
+
await this.assertNoLegacy(initial.batchId, initial.tenantId, initial.projectId);
|
|
74
|
+
if (await this.publish(initial)) {
|
|
75
|
+
if (await this.tombstoned(prefix)) {
|
|
76
|
+
await this.removeRevisions(prefix);
|
|
77
|
+
throw new BatchRecordUnavailableError();
|
|
78
|
+
}
|
|
79
|
+
return { owner: true, receipt: structuredClone(initial) };
|
|
80
|
+
}
|
|
81
|
+
const existing = await this.read(initial.batchId, initial.tenantId, initial.projectId);
|
|
82
|
+
if (!existing)
|
|
83
|
+
throw new BatchRecordUnavailableError();
|
|
84
|
+
assertSameIdentity(initial, existing);
|
|
85
|
+
return { owner: false, receipt: existing };
|
|
86
|
+
}
|
|
87
|
+
/** Erased, expired and missing receipts all return null. */
|
|
88
|
+
async read(batchId, tenantId, projectId) {
|
|
89
|
+
await this.ensurePrivateDirectory();
|
|
90
|
+
const prefix = this.prefix(batchId, tenantId, projectId);
|
|
91
|
+
if (await this.tombstoned(prefix))
|
|
92
|
+
return null;
|
|
93
|
+
const revisions = revisionsIn(await readdir(this.directory), prefix, SEALED);
|
|
94
|
+
if (!revisions.length) {
|
|
95
|
+
await this.assertNoLegacy(batchId, tenantId, projectId);
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
const chain = await this.chain(this.directory, prefix, revisions);
|
|
99
|
+
if (expired(chain[0].createdAtEpochMs, this.clock(), this.rule))
|
|
100
|
+
return null;
|
|
101
|
+
return structuredClone(chain.at(-1));
|
|
102
|
+
}
|
|
103
|
+
async compareAndSwap(previous, next) {
|
|
104
|
+
validateBatchReceiptTransition(previous, next);
|
|
105
|
+
const current = await this.read(previous.batchId, previous.tenantId, previous.projectId);
|
|
106
|
+
if (!current || canonicalJson(current) !== canonicalJson(previous))
|
|
107
|
+
return false;
|
|
108
|
+
const prefix = this.prefix(next.batchId, next.tenantId, next.projectId);
|
|
109
|
+
const published = await this.publish(next);
|
|
110
|
+
if (published && await this.tombstoned(prefix)) {
|
|
111
|
+
await this.removeRevisions(prefix);
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
return published;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* D10 content eraser for the `receipt` surface. Call it through `eraseDecisionSubject`, which
|
|
118
|
+
* enforces legal holds and records the journal tombstone first. Publishes a body-free local
|
|
119
|
+
* tombstone, cascades to the bound result store, then removes every revision body.
|
|
120
|
+
*/
|
|
121
|
+
async erase(opaqueId) {
|
|
122
|
+
const match = REFERENCE.exec(opaqueId);
|
|
123
|
+
if (!match)
|
|
124
|
+
throw new Error('Batch receipt lifecycle reference invalid');
|
|
125
|
+
if (!this.options.results)
|
|
126
|
+
throw new Error('Batch receipt erase requires a bound result store');
|
|
127
|
+
await this.ensurePrivateDirectory();
|
|
128
|
+
const prefix = match[1];
|
|
129
|
+
await writeTombstone(this.directory, this.tombstonePath(prefix), { surface: 'receipt', opaqueId }, this.clock());
|
|
130
|
+
const revisions = revisionsIn(await readdir(this.directory), prefix, SEALED);
|
|
131
|
+
let identity = null;
|
|
132
|
+
for (const revision of revisions) {
|
|
133
|
+
try {
|
|
134
|
+
identity = await this.revision(this.directory, prefix, revision);
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
catch { /* try the next sealed revision */ }
|
|
138
|
+
}
|
|
139
|
+
if (identity)
|
|
140
|
+
await this.options.results.eraseBatch(identity.batchId, identity.tenantId, identity.projectId);
|
|
141
|
+
await this.removeRevisions(prefix);
|
|
142
|
+
// The bodies are gone, but an unverifiable receipt left its result cascade unresolved.
|
|
143
|
+
if (revisions.length && !identity)
|
|
144
|
+
throw new BatchStoreIntegrityError();
|
|
145
|
+
}
|
|
146
|
+
/** Erase receipts whose retention elapsed, skipping any the host reports as held. Returns the count erased. */
|
|
147
|
+
async sweepExpired(isHeld) {
|
|
148
|
+
if (typeof isHeld !== 'function')
|
|
149
|
+
throw new Error('Batch receipt sweep requires a hold authority');
|
|
150
|
+
await this.ensurePrivateDirectory();
|
|
151
|
+
let erased = 0;
|
|
152
|
+
for (const prefix of prefixesIn(await readdir(this.directory))) {
|
|
153
|
+
if (await this.tombstoned(prefix))
|
|
154
|
+
continue;
|
|
155
|
+
let first;
|
|
156
|
+
try {
|
|
157
|
+
first = await this.revision(this.directory, prefix, 1);
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
if (!expired(first.createdAtEpochMs, this.clock(), this.rule))
|
|
163
|
+
continue;
|
|
164
|
+
const reference = { surface: 'receipt', opaqueId: `batch-receipt-${prefix}` };
|
|
165
|
+
let held = true;
|
|
166
|
+
try {
|
|
167
|
+
held = await isHeld(reference) !== false;
|
|
168
|
+
}
|
|
169
|
+
catch { /* an unavailable hold authority keeps the record */ }
|
|
170
|
+
if (held)
|
|
171
|
+
continue;
|
|
172
|
+
await this.erase(reference.opaqueId);
|
|
173
|
+
erased++;
|
|
174
|
+
}
|
|
175
|
+
return erased;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Restore sealed revisions from a backup copy of this store. A batch is refused when it is
|
|
179
|
+
* tombstoned locally or by the host, is past retention, or fails verification.
|
|
180
|
+
*/
|
|
181
|
+
async restoreFrom(backupDirectory, tombstones = []) {
|
|
182
|
+
if (this.rule.backup === 'not-persisted')
|
|
183
|
+
throw new Error('Batch receipt backup restore denied by lifecycle policy');
|
|
184
|
+
await this.ensurePrivateDirectory();
|
|
185
|
+
const names = await readdir(backupDirectory);
|
|
186
|
+
const report = { restored: 0, refused: 0 };
|
|
187
|
+
for (const prefix of prefixesIn(names)) {
|
|
188
|
+
const reference = { surface: 'receipt', opaqueId: `batch-receipt-${prefix}` };
|
|
189
|
+
try {
|
|
190
|
+
const revisions = revisionsIn(names, prefix, SEALED);
|
|
191
|
+
const chain = await this.chain(backupDirectory, prefix, revisions);
|
|
192
|
+
if (await this.tombstoned(prefix)
|
|
193
|
+
|| !mayRestoreDecisionReference(reference, chain[0].createdAtEpochMs, this.clock(), this.options.lifecycle, tombstones)) {
|
|
194
|
+
report.refused++;
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
for (const revision of revisions) {
|
|
198
|
+
const name = sealedName(prefix, revision);
|
|
199
|
+
const contents = await readFile(join(backupDirectory, name), 'utf8');
|
|
200
|
+
if (!await publishExclusive(this.directory, join(this.directory, name), contents, 'batch-receipt')
|
|
201
|
+
&& await readFile(join(this.directory, name), 'utf8') !== contents)
|
|
202
|
+
throw new BatchStoreIntegrityError();
|
|
203
|
+
}
|
|
204
|
+
report.restored++;
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
report.refused++;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return report;
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Explicit, authorized upgrade of unkeyed revisions written before integrity protection.
|
|
214
|
+
* Every legacy chain is validated before authorization; nothing is written unless it is granted.
|
|
215
|
+
*/
|
|
216
|
+
async migrateLegacy(authorize) {
|
|
217
|
+
await this.ensurePrivateDirectory();
|
|
218
|
+
const names = await readdir(this.directory);
|
|
219
|
+
const chains = [];
|
|
220
|
+
for (const legacyPrefix of new Set(names.flatMap(name => LEGACY.exec(name)?.[1] ?? []))) {
|
|
221
|
+
const chain = [];
|
|
222
|
+
for (const [index, revision] of revisionsIn(names, legacyPrefix, LEGACY).entries()) {
|
|
223
|
+
const path = join(this.directory, `${legacyPrefix}.r${revision}.json`);
|
|
224
|
+
await assertPrivateFile(path, 'Insecure batch receipt file');
|
|
225
|
+
let receipt;
|
|
226
|
+
try {
|
|
227
|
+
receipt = JSON.parse(await readFile(path, 'utf8'));
|
|
228
|
+
validateBatchReceipt(receipt);
|
|
229
|
+
}
|
|
230
|
+
catch {
|
|
231
|
+
throw new BatchStoreIntegrityError();
|
|
232
|
+
}
|
|
233
|
+
if (revision !== index + 1 || receipt.revision !== revision
|
|
234
|
+
|| filePrefix(receipt.batchId, receipt.tenantId, receipt.projectId) !== legacyPrefix)
|
|
235
|
+
throw new BatchStoreIntegrityError();
|
|
236
|
+
if (chain.length)
|
|
237
|
+
validateBatchReceiptTransition(chain.at(-1), receipt);
|
|
238
|
+
chain.push(receipt);
|
|
239
|
+
}
|
|
240
|
+
chains.push(chain);
|
|
241
|
+
}
|
|
242
|
+
if (!chains.length)
|
|
243
|
+
return { migrated: 0 };
|
|
244
|
+
let approved = false;
|
|
245
|
+
try {
|
|
246
|
+
approved = await authorize({ receipts: chains.length, revisions: chains.reduce((sum, chain) => sum + chain.length, 0) }) === true;
|
|
247
|
+
}
|
|
248
|
+
catch { /* authorization fails closed */ }
|
|
249
|
+
if (!approved)
|
|
250
|
+
throw new Error('Batch store migration denied');
|
|
251
|
+
for (const chain of chains) {
|
|
252
|
+
const first = chain[0];
|
|
253
|
+
const prefix = this.prefix(first.batchId, first.tenantId, first.projectId);
|
|
254
|
+
if (await this.tombstoned(prefix))
|
|
255
|
+
throw new BatchRecordUnavailableError();
|
|
256
|
+
for (const receipt of chain) {
|
|
257
|
+
if (!await this.publish(receipt)
|
|
258
|
+
&& canonicalJson(await this.revision(this.directory, prefix, receipt.revision)) !== canonicalJson(receipt)) {
|
|
259
|
+
throw new BatchReceiptConflictError('Legacy batch receipt conflicts with a sealed revision');
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
const legacyPrefix = filePrefix(first.batchId, first.tenantId, first.projectId);
|
|
263
|
+
for (const receipt of chain)
|
|
264
|
+
await rm(join(this.directory, `${legacyPrefix}.r${receipt.revision}.json`));
|
|
265
|
+
}
|
|
266
|
+
await syncDirectory(this.directory);
|
|
267
|
+
return { migrated: chains.length };
|
|
268
|
+
}
|
|
269
|
+
prefix(batchId, tenantId, projectId) {
|
|
270
|
+
return keyedName(this.key, 'decision-batch-receipt-name/v1', [tenantId, projectId, batchId]);
|
|
271
|
+
}
|
|
272
|
+
tombstonePath(prefix) { return join(this.directory, `${prefix}.tombstone`); }
|
|
273
|
+
async tombstoned(prefix) {
|
|
274
|
+
if (await exists(this.tombstonePath(prefix)))
|
|
275
|
+
return true;
|
|
276
|
+
if (!this.options.isTombstoned)
|
|
277
|
+
return false;
|
|
278
|
+
try {
|
|
279
|
+
return await this.options.isTombstoned({ surface: 'receipt', opaqueId: `batch-receipt-${prefix}` }) !== false;
|
|
280
|
+
}
|
|
281
|
+
catch {
|
|
282
|
+
return true;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
async assertNoLegacy(batchId, tenantId, projectId) {
|
|
286
|
+
if (revisionsIn(await readdir(this.directory), filePrefix(batchId, tenantId, projectId), LEGACY).length) {
|
|
287
|
+
throw new BatchStoreMigrationRequiredError();
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
async chain(directory, prefix, revisions) {
|
|
291
|
+
revisions.forEach((revision, index) => { if (revision !== index + 1)
|
|
292
|
+
throw new BatchReceiptValidationError('Batch receipt revision gap'); });
|
|
293
|
+
const chain = [];
|
|
294
|
+
for (const revision of revisions) {
|
|
295
|
+
const receipt = await this.revision(directory, prefix, revision);
|
|
296
|
+
if (chain.length)
|
|
297
|
+
validateBatchReceiptTransition(chain.at(-1), receipt);
|
|
298
|
+
chain.push(receipt);
|
|
299
|
+
}
|
|
300
|
+
return chain;
|
|
301
|
+
}
|
|
302
|
+
async revision(directory, prefix, revision) {
|
|
303
|
+
const path = join(directory, sealedName(prefix, revision));
|
|
304
|
+
if (directory === this.directory)
|
|
305
|
+
await assertPrivateFile(path, 'Insecure batch receipt file');
|
|
306
|
+
try {
|
|
307
|
+
const envelope = parseCanonical(await readFile(path, 'utf8'));
|
|
308
|
+
if (!envelope || envelope.version !== ENVELOPE || Object.keys(envelope).length !== 3
|
|
309
|
+
|| !macMatches(this.key, MAC_DOMAIN, envelope.receipt, envelope.mac))
|
|
310
|
+
throw new BatchStoreIntegrityError();
|
|
311
|
+
validateBatchReceipt(envelope.receipt);
|
|
312
|
+
if (envelope.receipt.revision !== revision
|
|
313
|
+
|| this.prefix(envelope.receipt.batchId, envelope.receipt.tenantId, envelope.receipt.projectId) !== prefix) {
|
|
314
|
+
throw new BatchStoreIntegrityError();
|
|
315
|
+
}
|
|
316
|
+
return envelope.receipt;
|
|
317
|
+
}
|
|
318
|
+
catch {
|
|
319
|
+
throw new BatchStoreIntegrityError();
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
async removeRevisions(prefix) {
|
|
323
|
+
for (const revision of revisionsIn(await readdir(this.directory), prefix, SEALED)) {
|
|
324
|
+
await rm(join(this.directory, sealedName(prefix, revision)), { force: true });
|
|
325
|
+
}
|
|
326
|
+
await syncDirectory(this.directory);
|
|
327
|
+
}
|
|
328
|
+
async ensurePrivateDirectory() {
|
|
329
|
+
await ensurePrivateDirectory(this.directory, 'Insecure batch receipt directory');
|
|
330
|
+
}
|
|
331
|
+
async publish(receipt) {
|
|
332
|
+
const prefix = this.prefix(receipt.batchId, receipt.tenantId, receipt.projectId);
|
|
333
|
+
const envelope = { version: ENVELOPE, receipt, mac: macFor(this.key, MAC_DOMAIN, receipt) };
|
|
334
|
+
return publishExclusive(this.directory, join(this.directory, sealedName(prefix, receipt.revision)), serialized(envelope), 'batch-receipt');
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
function sealedName(prefix, revision) { return `${prefix}.r${revision}.sealed.json`; }
|
|
338
|
+
function prefixesIn(names) { return new Set(names.flatMap(name => SEALED.exec(name)?.[1] ?? [])); }
|
|
339
|
+
function revisionsIn(names, prefix, pattern) {
|
|
340
|
+
return names.flatMap(name => { const match = pattern.exec(name); return match && match[1] === prefix ? [Number(match[2])] : []; })
|
|
341
|
+
.sort((a, b) => a - b);
|
|
342
|
+
}
|
|
343
|
+
async function assertPrivateFile(path, message) {
|
|
344
|
+
const stat = await lstat(path);
|
|
345
|
+
if (!stat.isFile() || (stat.mode & 0o077) !== 0)
|
|
346
|
+
throw new BatchReceiptValidationError(message);
|
|
347
|
+
}
|
|
348
|
+
function scopeKey(batchId, tenantId, projectId) { return `${tenantId}\u0000${projectId}\u0000${batchId}`; }
|
|
349
|
+
/** Unkeyed name used by stores written before integrity protection; only migration reads it. */
|
|
350
|
+
function filePrefix(batchId, tenantId, projectId) {
|
|
351
|
+
return createHash('sha256').update(scopeKey(batchId, tenantId, projectId)).digest('hex');
|
|
352
|
+
}
|
|
353
|
+
function assertSameIdentity(requested, existing) {
|
|
354
|
+
const identity = (receipt) => ({ tenantId: receipt.tenantId, projectId: receipt.projectId,
|
|
355
|
+
batchId: receipt.batchId, invocationId: receipt.invocationId, runId: receipt.runId, plan: receipt.plan,
|
|
356
|
+
subjectHash: receipt.subjectHash, stateHash: receipt.stateHash, executionEnvelope: receipt.executionEnvelope,
|
|
357
|
+
questionIds: receipt.questionIds });
|
|
358
|
+
if (canonicalJson(identity(requested)) !== canonicalJson(identity(existing))) {
|
|
359
|
+
throw new BatchReceiptConflictError('Batch ID already belongs to another immutable receipt identity');
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
//# sourceMappingURL=store.js.map
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { canonicalJson } from '../../security/artifact-trust.js';
|
|
2
|
+
import { isTraceparent } from '../telemetry/context.js';
|
|
3
|
+
const HASH = /^sha256:[0-9a-f]{64}$/;
|
|
4
|
+
const TERMINAL = new Set(['completed', 'failed', 'execution-uncertain']);
|
|
5
|
+
const TRANSITIONS = {
|
|
6
|
+
acquired: ['running', 'failed'],
|
|
7
|
+
running: ['running', 'completed', 'failed', 'execution-uncertain'],
|
|
8
|
+
completed: [], failed: [], 'execution-uncertain': [],
|
|
9
|
+
};
|
|
10
|
+
export class BatchReceiptValidationError extends Error {
|
|
11
|
+
constructor(message) { super(message); this.name = 'BatchReceiptValidationError'; }
|
|
12
|
+
}
|
|
13
|
+
export function validateBatchReceipt(receipt) {
|
|
14
|
+
const ids = new Set(receipt.questionIds);
|
|
15
|
+
if (receipt.schemaVersion !== 'decision-batch-receipt/v1'
|
|
16
|
+
|| !positive(receipt.revision) || !text(receipt.tenantId) || !text(receipt.projectId)
|
|
17
|
+
|| !text(receipt.batchId) || !text(receipt.invocationId) || !text(receipt.runId)
|
|
18
|
+
|| !HASH.test(receipt.plan.planDigest) || !text(receipt.plan.partitionId)
|
|
19
|
+
|| !HASH.test(receipt.subjectHash) || !HASH.test(receipt.stateHash)
|
|
20
|
+
|| !text(receipt.executionEnvelope) || !receipt.questionIds.length || ids.size !== receipt.questionIds.length
|
|
21
|
+
|| receipt.questionIds.some(id => !text(id))
|
|
22
|
+
|| !Number.isSafeInteger(receipt.createdAtEpochMs) || receipt.createdAtEpochMs < 0
|
|
23
|
+
|| !Number.isSafeInteger(receipt.updatedAtEpochMs) || receipt.updatedAtEpochMs < receipt.createdAtEpochMs
|
|
24
|
+
|| (TERMINAL.has(receipt.status) !== (receipt.terminalAtEpochMs !== null))
|
|
25
|
+
|| (receipt.terminalAtEpochMs !== null && (!Number.isSafeInteger(receipt.terminalAtEpochMs)
|
|
26
|
+
|| receipt.terminalAtEpochMs < receipt.createdAtEpochMs))
|
|
27
|
+
|| (receipt.traceParent !== undefined && !isTraceparent(receipt.traceParent)))
|
|
28
|
+
fail();
|
|
29
|
+
receipt.attempts.forEach((attempt, index) => validateAttempt(attempt, index + 1));
|
|
30
|
+
const answers = new Set();
|
|
31
|
+
const referencedQuestions = new Set();
|
|
32
|
+
const results = new Set();
|
|
33
|
+
for (const ref of receipt.answerReferences) {
|
|
34
|
+
if (!ids.has(ref.questionId) || !text(ref.answerId) || !text(ref.resultId) || answers.has(ref.answerId)
|
|
35
|
+
|| referencedQuestions.has(ref.questionId) || results.has(ref.resultId))
|
|
36
|
+
fail();
|
|
37
|
+
answers.add(ref.answerId);
|
|
38
|
+
referencedQuestions.add(ref.questionId);
|
|
39
|
+
results.add(ref.resultId);
|
|
40
|
+
}
|
|
41
|
+
if (receipt.allocations.length && receipt.allocations.length !== receipt.questionIds.length)
|
|
42
|
+
fail();
|
|
43
|
+
const allocationIds = new Set();
|
|
44
|
+
for (const allocation of receipt.allocations) {
|
|
45
|
+
if (!ids.has(allocation.questionId) || allocationIds.has(allocation.questionId)
|
|
46
|
+
|| allocation.kind !== 'estimated' || allocation.algorithm !== 'largest-remainder-weighted'
|
|
47
|
+
|| allocation.algorithmVersion !== '1' || !nullableCount(allocation.inputTokens)
|
|
48
|
+
|| !nullableCount(allocation.outputTokens))
|
|
49
|
+
fail();
|
|
50
|
+
allocationIds.add(allocation.questionId);
|
|
51
|
+
}
|
|
52
|
+
if (receipt.status === 'completed' && (receipt.questionIds.some(questionId => !receipt.answerReferences.some(reference => reference.questionId === questionId))
|
|
53
|
+
|| !receipt.attempts.some(attempt => attempt.status === 'succeeded')))
|
|
54
|
+
fail();
|
|
55
|
+
if (receipt.allocations.length) {
|
|
56
|
+
const inputTotal = knownAttemptTotal(receipt, 'inputTokens');
|
|
57
|
+
const outputTotal = knownAttemptTotal(receipt, 'outputTokens');
|
|
58
|
+
if (inputTotal === null ? receipt.allocations.some(item => item.inputTokens !== null)
|
|
59
|
+
: receipt.allocations.some(item => item.inputTokens === null)
|
|
60
|
+
|| receipt.allocations.reduce((sum, item) => sum + item.inputTokens, 0) !== inputTotal)
|
|
61
|
+
fail();
|
|
62
|
+
if (outputTotal === null ? receipt.allocations.some(item => item.outputTokens !== null)
|
|
63
|
+
: receipt.allocations.some(item => item.outputTokens === null)
|
|
64
|
+
|| receipt.allocations.reduce((sum, item) => sum + item.outputTokens, 0) !== outputTotal)
|
|
65
|
+
fail();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
export function validateBatchReceiptTransition(previous, next) {
|
|
69
|
+
validateBatchReceipt(previous);
|
|
70
|
+
validateBatchReceipt(next);
|
|
71
|
+
if (next.revision !== previous.revision + 1 || next.updatedAtEpochMs < previous.updatedAtEpochMs
|
|
72
|
+
|| !TRANSITIONS[previous.status].includes(next.status))
|
|
73
|
+
throw new BatchReceiptValidationError('Illegal batch receipt transition');
|
|
74
|
+
const immutable = ['schemaVersion', 'tenantId', 'projectId', 'batchId', 'invocationId', 'runId', 'plan',
|
|
75
|
+
'subjectHash', 'stateHash', 'executionEnvelope', 'questionIds', 'createdAtEpochMs'];
|
|
76
|
+
for (const key of immutable)
|
|
77
|
+
if (canonicalJson(previous[key]) !== canonicalJson(next[key])) {
|
|
78
|
+
throw new BatchReceiptValidationError(`Immutable batch receipt field changed: ${key}`);
|
|
79
|
+
}
|
|
80
|
+
if (previous.traceParent !== next.traceParent)
|
|
81
|
+
throw new BatchReceiptValidationError('Immutable batch receipt field changed: traceParent');
|
|
82
|
+
if (next.attempts.length < previous.attempts.length
|
|
83
|
+
|| previous.attempts.some((attempt, index) => canonicalJson(attempt) !== canonicalJson(next.attempts[index]))) {
|
|
84
|
+
throw new BatchReceiptValidationError('Batch attempt chronology is append-only');
|
|
85
|
+
}
|
|
86
|
+
if (previous.answerReferences.some(ref => !next.answerReferences.some(candidate => canonicalJson(candidate) === canonicalJson(ref)))) {
|
|
87
|
+
throw new BatchReceiptValidationError('Batch answer references are append-only');
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
function validateAttempt(attempt, ordinal) {
|
|
91
|
+
if (attempt.ordinal !== ordinal || !text(attempt.adapterId) || !text(attempt.adapterVersion)
|
|
92
|
+
|| !text(attempt.requestedModel) || (attempt.actualModel !== null && !text(attempt.actualModel))
|
|
93
|
+
|| (attempt.providerRequestId !== null && !validOpaqueRequestId(attempt.providerRequestId))
|
|
94
|
+
|| !nullableTime(attempt.dispatchedAtEpochMs) || !nullableTime(attempt.completedAtEpochMs)
|
|
95
|
+
|| (attempt.dispatchedAtEpochMs !== null && attempt.completedAtEpochMs !== null
|
|
96
|
+
&& attempt.completedAtEpochMs < attempt.dispatchedAtEpochMs)
|
|
97
|
+
|| !nullableCount(attempt.usage.inputTokens) || !nullableCount(attempt.usage.outputTokens)
|
|
98
|
+
|| (attempt.status === 'not-sent' && (attempt.providerRequestId !== null || attempt.dispatchedAtEpochMs !== null))
|
|
99
|
+
|| (attempt.fallbackFromAttemptOrdinal !== null && (!positive(attempt.fallbackFromAttemptOrdinal)
|
|
100
|
+
|| attempt.fallbackFromAttemptOrdinal >= ordinal)))
|
|
101
|
+
fail();
|
|
102
|
+
const cost = attempt.cost;
|
|
103
|
+
if (cost.kind === 'provider-authoritative' && !count(cost.amountMicros))
|
|
104
|
+
fail();
|
|
105
|
+
if (cost.kind === 'client-derived' && (!count(cost.amountMicros) || !text(cost.priceCatalogId)
|
|
106
|
+
|| !text(cost.priceCatalogVersion) || !validIso(cost.effectiveAt)))
|
|
107
|
+
fail();
|
|
108
|
+
if (cost.kind === 'bounded-unknown' && (!count(cost.upperBoundMicros) || !text(cost.boundPolicyId)
|
|
109
|
+
|| !text(cost.boundPolicyVersion)))
|
|
110
|
+
fail();
|
|
111
|
+
}
|
|
112
|
+
/** Provider IDs are opaque internal data: bounded printable ASCII, never an authorization key. */
|
|
113
|
+
export function validOpaqueRequestId(value) {
|
|
114
|
+
return value.length >= 1 && value.length <= 256 && /^[\x21-\x7e]+$/.test(value);
|
|
115
|
+
}
|
|
116
|
+
function validIso(value) { return !Number.isNaN(Date.parse(value)); }
|
|
117
|
+
function text(value) { return typeof value === 'string' && value.length > 0; }
|
|
118
|
+
function count(value) { return Number.isSafeInteger(value) && value >= 0; }
|
|
119
|
+
function positive(value) { return Number.isSafeInteger(value) && value > 0; }
|
|
120
|
+
function nullableCount(value) { return value === null || count(value); }
|
|
121
|
+
function nullableTime(value) { return value === null || count(value); }
|
|
122
|
+
function knownAttemptTotal(receipt, key) {
|
|
123
|
+
const values = receipt.attempts.map(attempt => attempt.usage[key]);
|
|
124
|
+
return values.some(value => value === null) ? null : values.reduce((sum, value) => sum + value, 0);
|
|
125
|
+
}
|
|
126
|
+
function fail() { throw new BatchReceiptValidationError('Invalid batch receipt'); }
|
|
127
|
+
//# sourceMappingURL=validate.js.map
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { canonicalJson } from '../security/artifact-trust.js';
|
|
3
|
+
/** Stable, opaque, declaration-order-independent provider question identity. */
|
|
4
|
+
export function decisionBatchQuestionId(alias) {
|
|
5
|
+
return `q_${createHash('sha256').update(alias, 'utf8').digest('hex').slice(0, 24)}`;
|
|
6
|
+
}
|
|
7
|
+
export function planNativeDecisionBatches(candidates, policy) {
|
|
8
|
+
if (!policy?.enabled)
|
|
9
|
+
return [];
|
|
10
|
+
const groups = new Map();
|
|
11
|
+
for (const candidate of candidates) {
|
|
12
|
+
const rule = policy.evaluations[candidate.alias];
|
|
13
|
+
const batch = candidate.capabilities.batch;
|
|
14
|
+
if (!rule?.independent || !rule.decisionSubject || !rule.egressPolicy || !rule.hostPolicy || !batch?.native
|
|
15
|
+
|| !candidate.adapter.evaluateMany)
|
|
16
|
+
continue;
|
|
17
|
+
const stage = rule.stage ?? 0;
|
|
18
|
+
const key = canonicalJson({
|
|
19
|
+
decisionSubject: rule.decisionSubject,
|
|
20
|
+
state: candidate.input,
|
|
21
|
+
stage,
|
|
22
|
+
egressPolicy: rule.egressPolicy,
|
|
23
|
+
hostPolicy: rule.hostPolicy,
|
|
24
|
+
adapter: candidate.adapter.id,
|
|
25
|
+
adapterVersion: candidate.adapter.version,
|
|
26
|
+
executionEnvelope: batch.executionEnvelope,
|
|
27
|
+
target: {
|
|
28
|
+
adapter: candidate.target.adapter,
|
|
29
|
+
adapterVersion: candidate.target.adapterVersion,
|
|
30
|
+
model: candidate.target.model,
|
|
31
|
+
credentialRef: candidate.target.credentialRef ?? null,
|
|
32
|
+
subagent: candidate.target.subagent ?? null,
|
|
33
|
+
timeoutMs: candidate.target.timeoutMs,
|
|
34
|
+
retry: candidate.target.retry,
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
const values = groups.get(key) ?? [];
|
|
38
|
+
values.push(candidate);
|
|
39
|
+
groups.set(key, values);
|
|
40
|
+
}
|
|
41
|
+
return [...groups.entries()]
|
|
42
|
+
.filter(([, values]) => values.length > 1)
|
|
43
|
+
.map(([key, values]) => ({
|
|
44
|
+
groupId: `batch_${createHash('sha256').update(key, 'utf8').digest('hex').slice(0, 24)}`,
|
|
45
|
+
decisionSubject: policy.evaluations[values[0].alias].decisionSubject,
|
|
46
|
+
stage: policy.evaluations[values[0].alias].stage ?? 0,
|
|
47
|
+
// Candidate iteration follows ruleset declaration order; response order never does.
|
|
48
|
+
candidates: [...values],
|
|
49
|
+
}))
|
|
50
|
+
.sort((left, right) => left.stage - right.stage || left.groupId.localeCompare(right.groupId));
|
|
51
|
+
}
|
|
52
|
+
export function correlateAtomicBatch(requestedIds, returned) {
|
|
53
|
+
const requested = new Set(requestedIds);
|
|
54
|
+
if (requested.size !== requestedIds.length || returned.length !== requestedIds.length) {
|
|
55
|
+
throw new Error('batch response cardinality mismatch');
|
|
56
|
+
}
|
|
57
|
+
const correlated = new Map();
|
|
58
|
+
for (const answer of returned) {
|
|
59
|
+
if (!requested.has(answer.questionId) || correlated.has(answer.questionId)) {
|
|
60
|
+
throw new Error('batch response identity mismatch');
|
|
61
|
+
}
|
|
62
|
+
correlated.set(answer.questionId, answer.value);
|
|
63
|
+
}
|
|
64
|
+
if (correlated.size !== requested.size)
|
|
65
|
+
throw new Error('batch response identity mismatch');
|
|
66
|
+
return correlated;
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=batch.js.map
|