@emilia-protocol/gate 0.17.0 → 0.18.1

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.
@@ -0,0 +1,1065 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ /**
3
+ * Gate Qualification v2 admission custody.
4
+ *
5
+ * The immutable snapshot is the complete input to one consequential operation.
6
+ * Mutable lifecycle state lives in a separately CAS-owned record and every
7
+ * accepted transition is chained into an append-only journal. The in-memory
8
+ * implementation is a linearizable reference for conformance and crash-model
9
+ * tests; it is deliberately not a durability claim.
10
+ */
11
+
12
+ import crypto from 'node:crypto';
13
+
14
+ export const ADMISSION_SNAPSHOT_VERSION = 'EP-GATE-ADMISSION-SNAPSHOT-v2';
15
+ export const ADMISSION_RECORD_VERSION = 'EP-GATE-ADMISSION-RECORD-v2';
16
+ export const ADMISSION_JOURNAL_VERSION = 'EP-GATE-ADMISSION-JOURNAL-v2';
17
+ export const ADMISSION_CURRENTNESS_VERSION = 'EP-GATE-ADMISSION-CURRENTNESS-v2';
18
+
19
+ export const ADMISSION_LIMITS = Object.freeze({
20
+ inputs: 128,
21
+ resources: 64,
22
+ testResults: 64,
23
+ agentEvidence: 32,
24
+ identifierBytes: 512,
25
+ currentnessMaxAgeMs: 5_000,
26
+ });
27
+
28
+ const SHA256 = /^sha256:[0-9a-f]{64}$/;
29
+ const CAID = /^caid:1:[a-z][a-z0-9-]*(?:\.[a-z][a-z0-9-]*)*\.[1-9][0-9]*:jcs-sha256:[A-Za-z0-9_-]{43}$/;
30
+ const IDENTIFIER = /^[A-Za-z0-9][A-Za-z0-9:_.@/-]{0,511}$/;
31
+ const RFC3339 = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.\d{1,9})?(?:Z|[+-]\d{2}:\d{2})$/;
32
+ const OWNER_TOKEN = /^admission-owner:v2:[A-Za-z0-9_-]{32,128}$/;
33
+
34
+ type Json = null | boolean | number | string | Json[] | { [key: string]: Json };
35
+ export type AdmissionDigest = `sha256:${string}`;
36
+
37
+ export type AdmissionInputRole =
38
+ | 'candidate_manifest'
39
+ | 'runtime_measurement'
40
+ | 'test_result'
41
+ | 'agent_evaluation_evidence'
42
+ | 'qualification_statement'
43
+ | 'qualification_status'
44
+ | 'aeb'
45
+ | 'aec'
46
+ | 'local_policy'
47
+ | 'authorization';
48
+
49
+ const REQUIRED_SINGLETON_ROLES = Object.freeze([
50
+ 'candidate_manifest',
51
+ 'runtime_measurement',
52
+ 'qualification_statement',
53
+ 'qualification_status',
54
+ 'aeb',
55
+ 'aec',
56
+ 'local_policy',
57
+ 'authorization',
58
+ ] as const satisfies readonly AdmissionInputRole[]);
59
+ const REPEATABLE_ROLES = new Set<AdmissionInputRole>([
60
+ 'test_result',
61
+ 'agent_evaluation_evidence',
62
+ ]);
63
+ const ROLE_ORDER = new Map<AdmissionInputRole, number>([
64
+ 'candidate_manifest', 'runtime_measurement', 'test_result',
65
+ 'agent_evaluation_evidence', 'qualification_statement',
66
+ 'qualification_status', 'aeb', 'aec', 'local_policy', 'authorization',
67
+ ].map((role, index) => [role as AdmissionInputRole, index]));
68
+
69
+ export interface AdmissionInput {
70
+ role: AdmissionInputRole;
71
+ artifact_type: string;
72
+ subject: string;
73
+ payload_digest: AdmissionDigest;
74
+ profile_digest: AdmissionDigest;
75
+ verifier_id: string;
76
+ trust_configuration_digest: AdmissionDigest;
77
+ valid_until: string;
78
+ }
79
+
80
+ export type AdmissionResourceKind =
81
+ | 'replay'
82
+ | 'capability'
83
+ | 'budget'
84
+ | 'qualification_use'
85
+ | 'provider_operation'
86
+ | 'external_lease';
87
+
88
+ export interface AdmissionResourceReservationInput {
89
+ kind: AdmissionResourceKind;
90
+ resource_id: string;
91
+ reservation_id: string;
92
+ digest: AdmissionDigest;
93
+ expires_at: string;
94
+ }
95
+
96
+ export interface CandidateCustodyBinding {
97
+ request_construction: 'GATE' | 'EXECUTOR_ADAPTER' | 'EXTERNAL';
98
+ mutation_credential_custody: 'GATE' | 'EXECUTOR_ADAPTER' | 'EXTERNAL';
99
+ enforcement_placement: 'SYSTEM_OF_RECORD' | 'ACTUATOR' | 'MIDDLEWARE';
100
+ evidence_digest: AdmissionDigest;
101
+ }
102
+
103
+ export interface QualificationStatusBinding {
104
+ authority_id: string;
105
+ sequence: number;
106
+ head_payload_digest: AdmissionDigest;
107
+ observed_at: string;
108
+ expires_at: string;
109
+ }
110
+
111
+ export interface ProviderBinding {
112
+ provider_id: string;
113
+ account_id: string;
114
+ environment: string;
115
+ }
116
+
117
+ export interface AdmissionRelation {
118
+ tenant_id: string;
119
+ admission_id: string;
120
+ operation_id: string;
121
+ snapshot_digest: AdmissionDigest;
122
+ }
123
+
124
+ export interface AdmissionSnapshotInput {
125
+ tenant_id: string;
126
+ admission_id: string;
127
+ operation_id: string;
128
+ candidate_manifest_digest: AdmissionDigest;
129
+ runtime_measurement_digest: AdmissionDigest;
130
+ candidate_custody: CandidateCustodyBinding;
131
+ assignment_digest: AdmissionDigest;
132
+ qualification_policy_digest: AdmissionDigest;
133
+ test_result_payload_digests: AdmissionDigest[];
134
+ agent_evaluation_evidence_payload_digests: AdmissionDigest[];
135
+ qualification_statement_payload_digest: AdmissionDigest;
136
+ qualification_status: QualificationStatusBinding;
137
+ caid: string;
138
+ action_digest: AdmissionDigest;
139
+ effect_request_digest: AdmissionDigest;
140
+ provider: ProviderBinding;
141
+ executor_adapter_digest: AdmissionDigest;
142
+ idempotency_key: string;
143
+ authorization_policy_digest: AdmissionDigest;
144
+ trust_epoch: number;
145
+ trust_configuration_digest: AdmissionDigest;
146
+ configuration_epoch: number;
147
+ configuration_digest: AdmissionDigest;
148
+ inputs: AdmissionInput[];
149
+ resource_reservations: AdmissionResourceReservationInput[];
150
+ admitted_at: string;
151
+ expires_at: string;
152
+ supersedes_admission_id?: string | null;
153
+ remedy_for?: AdmissionRelation | null;
154
+ }
155
+
156
+ export interface AdmissionSnapshotBody extends Omit<AdmissionSnapshotInput,
157
+ 'supersedes_admission_id' | 'remedy_for'> {
158
+ '@version': typeof ADMISSION_SNAPSHOT_VERSION;
159
+ supersedes_admission_id: string | null;
160
+ remedy_for: Readonly<AdmissionRelation> | null;
161
+ }
162
+
163
+ export interface AdmissionSnapshot {
164
+ body: Readonly<AdmissionSnapshotBody>;
165
+ snapshot_digest: AdmissionDigest;
166
+ }
167
+
168
+ export type AdmissionState =
169
+ | 'RESERVED'
170
+ | 'RELEASED'
171
+ | 'EXPIRED'
172
+ | 'SUPERSEDED'
173
+ | 'INVOKING'
174
+ | 'INDETERMINATE'
175
+ | 'COMMITTED'
176
+ | 'PROVEN_NOT_COMMITTED';
177
+ export type AdmissionExecutionRight = 'RESERVED' | 'RELEASED' | 'CONSUMED';
178
+ export type AdmissionProviderAttempt =
179
+ | 'NOT_ENTERED'
180
+ | 'INVOKING'
181
+ | 'INDETERMINATE'
182
+ | 'COMMITTED'
183
+ | 'PROVEN_NOT_COMMITTED';
184
+ export type AdmissionProviderOutcome = 'COMMITTED' | 'PROVEN_NOT_COMMITTED' | 'INDETERMINATE';
185
+ export type AdmissionEffectRelation = 'OBSERVED_AS_REQUESTED' | 'DIVERGED' | 'INDETERMINATE';
186
+ export type AdmissionResourceState = 'RESERVED' | 'RELEASED' | 'CONSUMED';
187
+
188
+ export interface AdmissionEvidenceOutcome<T extends string> {
189
+ value: T;
190
+ evidence_digest: AdmissionDigest | null;
191
+ observed_at: string;
192
+ }
193
+
194
+ export interface AdmissionResourceReservation extends AdmissionResourceReservationInput {
195
+ state: AdmissionResourceState;
196
+ }
197
+
198
+ export interface AdmissionRecord {
199
+ '@version': typeof ADMISSION_RECORD_VERSION;
200
+ tenant_id: string;
201
+ admission_id: string;
202
+ operation_id: string;
203
+ snapshot_digest: AdmissionDigest;
204
+ revision: number;
205
+ state: AdmissionState;
206
+ execution_right: AdmissionExecutionRight;
207
+ provider_attempt: AdmissionProviderAttempt;
208
+ owner_digest: AdmissionDigest;
209
+ invocation_token_digest: AdmissionDigest | null;
210
+ provider_outcome: Readonly<AdmissionEvidenceOutcome<AdmissionProviderOutcome>> | null;
211
+ effect_relation: Readonly<AdmissionEvidenceOutcome<AdmissionEffectRelation>> | null;
212
+ resources: readonly Readonly<AdmissionResourceReservation>[];
213
+ superseded_by_admission_id: string | null;
214
+ refusal_reason: string | null;
215
+ invocation_started_at: string | null;
216
+ created_at: string;
217
+ updated_at: string;
218
+ predecessor_record_digest: AdmissionDigest | null;
219
+ record_digest: AdmissionDigest;
220
+ }
221
+
222
+ export type AdmissionJournalEvent =
223
+ | 'RESERVED'
224
+ | 'RELEASED'
225
+ | 'EXPIRED'
226
+ | 'SUPERSEDED'
227
+ | 'INVOKING'
228
+ | 'RECOVERED_INDETERMINATE'
229
+ | 'PROVIDER_OUTCOME'
230
+ | 'EFFECT_RELATION';
231
+
232
+ export interface AdmissionJournalEntry {
233
+ '@version': typeof ADMISSION_JOURNAL_VERSION;
234
+ tenant_id: string;
235
+ admission_id: string;
236
+ operation_id: string;
237
+ sequence: number;
238
+ event: AdmissionJournalEvent;
239
+ snapshot_digest: AdmissionDigest;
240
+ record_digest: AdmissionDigest;
241
+ predecessor_digest: AdmissionDigest | null;
242
+ recorded_at: string;
243
+ entry_digest: AdmissionDigest;
244
+ }
245
+
246
+ export interface AdmissionCurrentnessObservation {
247
+ '@version': typeof ADMISSION_CURRENTNESS_VERSION;
248
+ observed_at: string;
249
+ qualification_status_authority_id: string;
250
+ qualification_status_sequence: number;
251
+ qualification_status_head_digest: AdmissionDigest;
252
+ qualification_status_expires_at: string;
253
+ trust_epoch: number;
254
+ trust_configuration_digest: AdmissionDigest;
255
+ configuration_epoch: number;
256
+ configuration_digest: AdmissionDigest;
257
+ runtime_measurement_digest: AdmissionDigest;
258
+ candidate_match: 'EXACT_MATCH' | 'MISMATCH' | 'STALE' | 'UNPINNABLE';
259
+ external_leases: Array<{
260
+ resource_id: string;
261
+ digest: AdmissionDigest;
262
+ expires_at: string;
263
+ }>;
264
+ }
265
+
266
+ export interface AdmissionCurrentnessOracle {
267
+ read(snapshot: Readonly<AdmissionSnapshot>): Promise<AdmissionCurrentnessObservation>;
268
+ }
269
+
270
+ export type AdmissionRefusalReason =
271
+ | 'admission_exists'
272
+ | 'admission_not_found'
273
+ | 'operation_exists'
274
+ | 'operation_conflict'
275
+ | 'revision_conflict'
276
+ | 'owner_conflict'
277
+ | 'resource_conflict'
278
+ | 'admission_expired'
279
+ | 'currentness_refused'
280
+ | 'execution_right_consumed'
281
+ | 'state_conflict'
282
+ | 'relation_not_found'
283
+ | 'relation_conflict'
284
+ | 'outcome_conflict'
285
+ | 'evidence_required'
286
+ | 'invocation_token_conflict';
287
+ export type AdmissionRefusal = { ok: false; reason: AdmissionRefusalReason };
288
+ export type AdmissionTransitionResult = { ok: true; record: Readonly<AdmissionRecord> } | AdmissionRefusal;
289
+ export type AdmissionReserveResult = {
290
+ ok: true;
291
+ snapshot: Readonly<AdmissionSnapshot>;
292
+ record: Readonly<AdmissionRecord>;
293
+ owner_token: string;
294
+ } | AdmissionRefusal;
295
+ export type AdmissionBeginResult = {
296
+ ok: true;
297
+ snapshot: Readonly<AdmissionSnapshot>;
298
+ record: Readonly<AdmissionRecord>;
299
+ invocation_token: string;
300
+ } | AdmissionRefusal;
301
+ export type AdmissionSupersedeResult = {
302
+ ok: true;
303
+ predecessor_record: Readonly<AdmissionRecord>;
304
+ successor_snapshot: Readonly<AdmissionSnapshot>;
305
+ successor_record: Readonly<AdmissionRecord>;
306
+ successor_owner_token: string;
307
+ } | AdmissionRefusal;
308
+ export type AdmissionRecoveryResult = {
309
+ ok: true;
310
+ record: Readonly<AdmissionRecord>;
311
+ reconciliation_token: string;
312
+ } | AdmissionRefusal;
313
+
314
+ export interface AdmissionReference {
315
+ tenant_id: string;
316
+ admission_id: string;
317
+ }
318
+ export interface AdmissionOperationReference {
319
+ tenant_id: string;
320
+ operation_id: string;
321
+ }
322
+ export interface AdmissionCas extends AdmissionReference {
323
+ expected_revision: number;
324
+ owner_token: string;
325
+ }
326
+ export interface AdmissionRecoveryInput extends AdmissionReference {
327
+ owner_token: string;
328
+ }
329
+ export interface AdmissionProviderOutcomeInput extends AdmissionCas {
330
+ invocation_token: string;
331
+ value: AdmissionProviderOutcome;
332
+ evidence_digest: AdmissionDigest | null;
333
+ observed_at: string;
334
+ }
335
+ export interface AdmissionEffectRelationInput extends AdmissionCas {
336
+ invocation_token: string;
337
+ value: AdmissionEffectRelation;
338
+ evidence_digest: AdmissionDigest | null;
339
+ observed_at: string;
340
+ }
341
+ export interface AdmissionSupersedeInput extends AdmissionCas {
342
+ successor: AdmissionSnapshotInput;
343
+ }
344
+
345
+ export interface AdmissionInvariantCheck { ok: boolean; violations: readonly string[] }
346
+
347
+ export interface AdmissionStore {
348
+ readonly durable: boolean;
349
+ readonly atomic: true;
350
+ readonly compareAndSwap: true;
351
+ readonly appendOnlyJournal: true;
352
+ readonly exclusiveActuation: true;
353
+ readonly transactionalCurrentness: true;
354
+ readonly testOnly?: true;
355
+ reserve(input: AdmissionSnapshotInput | AdmissionSnapshot): Promise<AdmissionReserveResult>;
356
+ release(input: AdmissionCas, reason?: string): Promise<AdmissionTransitionResult>;
357
+ expire(input: AdmissionCas): Promise<AdmissionTransitionResult>;
358
+ supersede(input: AdmissionSupersedeInput): Promise<AdmissionSupersedeResult>;
359
+ beginInvocation(input: AdmissionCas): Promise<AdmissionBeginResult>;
360
+ recoverIndeterminate(input: AdmissionRecoveryInput): Promise<AdmissionRecoveryResult>;
361
+ recordProviderOutcome(input: AdmissionProviderOutcomeInput): Promise<AdmissionTransitionResult>;
362
+ recordEffectRelation(input: AdmissionEffectRelationInput): Promise<AdmissionTransitionResult>;
363
+ read(input: AdmissionReference): Promise<Readonly<AdmissionRecord> | null>;
364
+ readByOperation(input: AdmissionOperationReference): Promise<Readonly<AdmissionRecord> | null>;
365
+ readSnapshot(digest: AdmissionDigest): Promise<Readonly<AdmissionSnapshot> | null>;
366
+ journal(input: AdmissionReference): Promise<readonly Readonly<AdmissionJournalEntry>[]>;
367
+ checkInvariants(): Promise<AdmissionInvariantCheck>;
368
+ }
369
+
370
+ export interface CreateMemoryAdmissionStoreOptions {
371
+ now?: number | string | Date | (() => number | string | Date);
372
+ currentnessOracle?: AdmissionCurrentnessOracle;
373
+ maxCurrentnessAgeMs?: number;
374
+ ownerTokenFactory?: () => string;
375
+ invocationTokenFactory?: () => string;
376
+ }
377
+
378
+ export class AdmissionStoreValidationError extends TypeError {
379
+ readonly code: string;
380
+ constructor(code: string, message: string) {
381
+ super(message);
382
+ this.name = 'AdmissionStoreValidationError';
383
+ this.code = code;
384
+ }
385
+ }
386
+
387
+ function fail(code: string, message: string): never {
388
+ throw new AdmissionStoreValidationError(code, message);
389
+ }
390
+
391
+ function plain(value: unknown): value is Record<string, unknown> {
392
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) return false;
393
+ const proto = Object.getPrototypeOf(value);
394
+ return proto === Object.prototype || proto === null;
395
+ }
396
+
397
+ function canonical(value: Json): string {
398
+ if (value === null || typeof value === 'boolean' || typeof value === 'string') return JSON.stringify(value);
399
+ if (typeof value === 'number') {
400
+ if (!Number.isFinite(value)) fail('invalid_json', 'non-finite JSON number');
401
+ return JSON.stringify(value);
402
+ }
403
+ if (Array.isArray(value)) return `[${value.map((item) => canonical(item)).join(',')}]`;
404
+ if (!plain(value)) fail('invalid_json', 'non-plain JSON object');
405
+ return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonical(value[key] as Json)}`).join(',')}}`;
406
+ }
407
+
408
+ function hash(domain: string, value: unknown): AdmissionDigest {
409
+ return `sha256:${crypto.createHash('sha256').update(domain).update('\0').update(canonical(value as Json)).digest('hex')}`;
410
+ }
411
+
412
+ function deepFreeze<T>(value: T): Readonly<T> {
413
+ if (value !== null && typeof value === 'object' && !Object.isFrozen(value)) {
414
+ for (const child of Object.values(value)) deepFreeze(child);
415
+ Object.freeze(value);
416
+ }
417
+ return value;
418
+ }
419
+
420
+ function frozenCopy<T>(value: T): Readonly<T> {
421
+ return deepFreeze(structuredClone(value));
422
+ }
423
+
424
+ function identifier(value: unknown, field: string): string {
425
+ if (typeof value !== 'string' || !IDENTIFIER.test(value)) fail('invalid_identifier', `${field} is invalid`);
426
+ return value;
427
+ }
428
+
429
+ function digest(value: unknown, field: string): AdmissionDigest {
430
+ if (typeof value !== 'string' || !SHA256.test(value)) fail('invalid_digest', `${field} is invalid`);
431
+ return value as AdmissionDigest;
432
+ }
433
+
434
+ function instant(value: unknown, field: string): { iso: string; ms: number } {
435
+ if (typeof value !== 'string' || !RFC3339.test(value)) fail('invalid_instant', `${field} must be RFC 3339`);
436
+ const ms = Date.parse(value);
437
+ if (!Number.isFinite(ms)) fail('invalid_instant', `${field} must identify an instant`);
438
+ return { iso: new Date(ms).toISOString(), ms };
439
+ }
440
+
441
+ function nonNegativeInteger(value: unknown, field: string): number {
442
+ if (!Number.isSafeInteger(value) || (value as number) < 0) fail('invalid_integer', `${field} is invalid`);
443
+ return value as number;
444
+ }
445
+
446
+ function stringBytes(value: string): number { return Buffer.byteLength(value, 'utf8'); }
447
+
448
+ function currentMs(source: CreateMemoryAdmissionStoreOptions['now']): number {
449
+ const raw = typeof source === 'function' ? source() : source;
450
+ if (raw === undefined) return Date.now();
451
+ if (raw instanceof Date) return raw.getTime();
452
+ if (typeof raw === 'string') return instant(raw, 'now').ms;
453
+ if (!Number.isFinite(raw)) fail('invalid_time', 'now is invalid');
454
+ return raw as number;
455
+ }
456
+
457
+ function normalizeInputs(raw: unknown, admittedAt: number): AdmissionInput[] {
458
+ if (!Array.isArray(raw) || raw.length > ADMISSION_LIMITS.inputs) fail('invalid_inputs', 'inputs are invalid');
459
+ const counts = new Map<AdmissionInputRole, number>();
460
+ const seen = new Set<string>();
461
+ const inputs = raw.map((entry, index) => {
462
+ if (!plain(entry)) fail('invalid_input', `inputs[${index}] is invalid`);
463
+ const role = entry.role as AdmissionInputRole;
464
+ if (!ROLE_ORDER.has(role)) fail('invalid_input_role', `inputs[${index}].role is invalid`);
465
+ const normalized: AdmissionInput = {
466
+ role,
467
+ artifact_type: identifier(entry.artifact_type, `inputs[${index}].artifact_type`),
468
+ subject: identifier(entry.subject, `inputs[${index}].subject`),
469
+ payload_digest: digest(entry.payload_digest, `inputs[${index}].payload_digest`),
470
+ profile_digest: digest(entry.profile_digest, `inputs[${index}].profile_digest`),
471
+ verifier_id: identifier(entry.verifier_id, `inputs[${index}].verifier_id`),
472
+ trust_configuration_digest: digest(entry.trust_configuration_digest, `inputs[${index}].trust_configuration_digest`),
473
+ valid_until: instant(entry.valid_until, `inputs[${index}].valid_until`).iso,
474
+ };
475
+ if (Date.parse(normalized.valid_until) <= admittedAt) fail('expired_input', `inputs[${index}] is expired`);
476
+ const key = canonical(normalized as unknown as Json);
477
+ if (seen.has(key)) fail('duplicate_input', `inputs[${index}] is duplicated`);
478
+ seen.add(key);
479
+ counts.set(role, (counts.get(role) ?? 0) + 1);
480
+ if (!REPEATABLE_ROLES.has(role) && (counts.get(role) ?? 0) > 1) fail('duplicate_input_role', `${role} is singleton`);
481
+ return normalized;
482
+ });
483
+ for (const role of REQUIRED_SINGLETON_ROLES) if (counts.get(role) !== 1) fail('missing_input_role', `${role} is required exactly once`);
484
+ for (const role of REPEATABLE_ROLES) if ((counts.get(role) ?? 0) < 1) fail('missing_input_role', `${role} is required`);
485
+ inputs.sort((left, right) => {
486
+ const role = (ROLE_ORDER.get(left.role) ?? 99) - (ROLE_ORDER.get(right.role) ?? 99);
487
+ if (role !== 0) return role;
488
+ return Buffer.from(canonical(left as unknown as Json)).compare(Buffer.from(canonical(right as unknown as Json)));
489
+ });
490
+ return inputs;
491
+ }
492
+
493
+ function normalizeDigests(raw: unknown, field: string, limit: number): AdmissionDigest[] {
494
+ if (!Array.isArray(raw) || raw.length < 1 || raw.length > limit) fail('invalid_digest_list', `${field} is invalid`);
495
+ const values = raw.map((value, index) => digest(value, `${field}[${index}]`)).sort();
496
+ if (new Set(values).size !== values.length) fail('duplicate_digest', `${field} contains duplicates`);
497
+ return values;
498
+ }
499
+
500
+ function normalizeResources(raw: unknown, admittedAt: number): AdmissionResourceReservationInput[] {
501
+ if (!Array.isArray(raw) || raw.length < 1 || raw.length > ADMISSION_LIMITS.resources) fail('invalid_resources', 'resource reservations are invalid');
502
+ const allowed = new Set<AdmissionResourceKind>(['replay', 'capability', 'budget', 'qualification_use', 'provider_operation', 'external_lease']);
503
+ const seen = new Set<string>();
504
+ const values = raw.map((entry, index) => {
505
+ if (!plain(entry) || !allowed.has(entry.kind as AdmissionResourceKind)) fail('invalid_resource', `resource[${index}] is invalid`);
506
+ const value: AdmissionResourceReservationInput = {
507
+ kind: entry.kind as AdmissionResourceKind,
508
+ resource_id: identifier(entry.resource_id, `resource[${index}].resource_id`),
509
+ reservation_id: identifier(entry.reservation_id, `resource[${index}].reservation_id`),
510
+ digest: digest(entry.digest, `resource[${index}].digest`),
511
+ expires_at: instant(entry.expires_at, `resource[${index}].expires_at`).iso,
512
+ };
513
+ if (Date.parse(value.expires_at) <= admittedAt) fail('expired_resource', `resource[${index}] is expired`);
514
+ const key = `${value.kind}\0${value.resource_id}`;
515
+ if (seen.has(key)) fail('duplicate_resource', `resource[${index}] is duplicated`);
516
+ seen.add(key);
517
+ return value;
518
+ });
519
+ values.sort((left, right) => Buffer.from(`${left.kind}\0${left.resource_id}`).compare(Buffer.from(`${right.kind}\0${right.resource_id}`)));
520
+ return values;
521
+ }
522
+
523
+ function normalizeRelation(raw: unknown): AdmissionRelation | null {
524
+ if (raw === undefined || raw === null) return null;
525
+ if (!plain(raw)) fail('invalid_relation', 'relation is invalid');
526
+ return {
527
+ tenant_id: identifier(raw.tenant_id, 'relation.tenant_id'),
528
+ admission_id: identifier(raw.admission_id, 'relation.admission_id'),
529
+ operation_id: identifier(raw.operation_id, 'relation.operation_id'),
530
+ snapshot_digest: digest(raw.snapshot_digest, 'relation.snapshot_digest'),
531
+ };
532
+ }
533
+
534
+ export function createAdmissionSnapshot(raw: AdmissionSnapshotInput): Readonly<AdmissionSnapshot> {
535
+ if (!plain(raw)) fail('invalid_snapshot', 'snapshot input is invalid');
536
+ const admitted = instant(raw.admitted_at, 'admitted_at');
537
+ const expires = instant(raw.expires_at, 'expires_at');
538
+ if (expires.ms <= admitted.ms) fail('invalid_expiration', 'expires_at must follow admitted_at');
539
+ const inputs = normalizeInputs(raw.inputs, admitted.ms);
540
+ const resources = normalizeResources(raw.resource_reservations, admitted.ms);
541
+ const status = raw.qualification_status;
542
+ if (!plain(status)) fail('invalid_status_binding', 'qualification_status is invalid');
543
+ const statusBinding: QualificationStatusBinding = {
544
+ authority_id: identifier(status.authority_id, 'qualification_status.authority_id'),
545
+ sequence: nonNegativeInteger(status.sequence, 'qualification_status.sequence'),
546
+ head_payload_digest: digest(status.head_payload_digest, 'qualification_status.head_payload_digest'),
547
+ observed_at: instant(status.observed_at, 'qualification_status.observed_at').iso,
548
+ expires_at: instant(status.expires_at, 'qualification_status.expires_at').iso,
549
+ };
550
+ const custody = raw.candidate_custody;
551
+ if (!plain(custody)
552
+ || !['GATE', 'EXECUTOR_ADAPTER', 'EXTERNAL'].includes(custody.request_construction)
553
+ || !['GATE', 'EXECUTOR_ADAPTER', 'EXTERNAL'].includes(custody.mutation_credential_custody)
554
+ || !['SYSTEM_OF_RECORD', 'ACTUATOR', 'MIDDLEWARE'].includes(custody.enforcement_placement)) {
555
+ fail('invalid_custody', 'candidate_custody is invalid');
556
+ }
557
+ const provider = raw.provider;
558
+ if (!plain(provider)) fail('invalid_provider', 'provider is invalid');
559
+ const body: AdmissionSnapshotBody = {
560
+ '@version': ADMISSION_SNAPSHOT_VERSION,
561
+ tenant_id: identifier(raw.tenant_id, 'tenant_id'),
562
+ admission_id: identifier(raw.admission_id, 'admission_id'),
563
+ operation_id: identifier(raw.operation_id, 'operation_id'),
564
+ candidate_manifest_digest: digest(raw.candidate_manifest_digest, 'candidate_manifest_digest'),
565
+ runtime_measurement_digest: digest(raw.runtime_measurement_digest, 'runtime_measurement_digest'),
566
+ candidate_custody: {
567
+ request_construction: custody.request_construction as CandidateCustodyBinding['request_construction'],
568
+ mutation_credential_custody: custody.mutation_credential_custody as CandidateCustodyBinding['mutation_credential_custody'],
569
+ enforcement_placement: custody.enforcement_placement as CandidateCustodyBinding['enforcement_placement'],
570
+ evidence_digest: digest(custody.evidence_digest, 'candidate_custody.evidence_digest'),
571
+ },
572
+ assignment_digest: digest(raw.assignment_digest, 'assignment_digest'),
573
+ qualification_policy_digest: digest(raw.qualification_policy_digest, 'qualification_policy_digest'),
574
+ test_result_payload_digests: normalizeDigests(raw.test_result_payload_digests, 'test_result_payload_digests', ADMISSION_LIMITS.testResults),
575
+ agent_evaluation_evidence_payload_digests: normalizeDigests(raw.agent_evaluation_evidence_payload_digests, 'agent_evaluation_evidence_payload_digests', ADMISSION_LIMITS.agentEvidence),
576
+ qualification_statement_payload_digest: digest(raw.qualification_statement_payload_digest, 'qualification_statement_payload_digest'),
577
+ qualification_status: statusBinding,
578
+ caid: typeof raw.caid === 'string' && CAID.test(raw.caid) ? raw.caid : fail('invalid_caid', 'caid is invalid'),
579
+ action_digest: digest(raw.action_digest, 'action_digest'),
580
+ effect_request_digest: digest(raw.effect_request_digest, 'effect_request_digest'),
581
+ provider: {
582
+ provider_id: identifier(provider.provider_id, 'provider.provider_id'),
583
+ account_id: identifier(provider.account_id, 'provider.account_id'),
584
+ environment: identifier(provider.environment, 'provider.environment'),
585
+ },
586
+ executor_adapter_digest: digest(raw.executor_adapter_digest, 'executor_adapter_digest'),
587
+ idempotency_key: identifier(raw.idempotency_key, 'idempotency_key'),
588
+ authorization_policy_digest: digest(raw.authorization_policy_digest, 'authorization_policy_digest'),
589
+ trust_epoch: nonNegativeInteger(raw.trust_epoch, 'trust_epoch'),
590
+ trust_configuration_digest: digest(raw.trust_configuration_digest, 'trust_configuration_digest'),
591
+ configuration_epoch: nonNegativeInteger(raw.configuration_epoch, 'configuration_epoch'),
592
+ configuration_digest: digest(raw.configuration_digest, 'configuration_digest'),
593
+ inputs,
594
+ resource_reservations: resources,
595
+ admitted_at: admitted.iso,
596
+ expires_at: expires.iso,
597
+ supersedes_admission_id: raw.supersedes_admission_id === undefined || raw.supersedes_admission_id === null
598
+ ? null : identifier(raw.supersedes_admission_id, 'supersedes_admission_id'),
599
+ remedy_for: normalizeRelation(raw.remedy_for),
600
+ };
601
+ const ceilings = [statusBinding.expires_at, ...inputs.map((input) => input.valid_until), ...resources.map((resource) => resource.expires_at)];
602
+ if (ceilings.some((ceiling) => expires.ms > Date.parse(ceiling))) fail('expiration_exceeds_evidence', 'expires_at exceeds an input deadline');
603
+ const snapshot: AdmissionSnapshot = {
604
+ body: frozenCopy(body),
605
+ snapshot_digest: hash(`${ADMISSION_SNAPSHOT_VERSION}:DIGEST`, body),
606
+ };
607
+ return frozenCopy(snapshot);
608
+ }
609
+
610
+ function validateSnapshot(raw: AdmissionSnapshot): Readonly<AdmissionSnapshot> {
611
+ if (!plain(raw) || !plain(raw.body)) fail('invalid_snapshot', 'snapshot is invalid');
612
+ const recreated = createAdmissionSnapshot(raw.body as unknown as AdmissionSnapshotInput);
613
+ if (raw.snapshot_digest !== recreated.snapshot_digest) fail('snapshot_digest_mismatch', 'snapshot digest does not match body');
614
+ return recreated;
615
+ }
616
+
617
+ function operationKey(tenant: string, operation: string): string { return JSON.stringify([tenant, operation]); }
618
+ function admissionKey(tenant: string, admission: string): string { return JSON.stringify([tenant, admission]); }
619
+ function resourceKey(tenant: string, resource: AdmissionResourceReservationInput): string { return JSON.stringify([tenant, resource.kind, resource.resource_id]); }
620
+ function tokenDigest(token: string): AdmissionDigest { return hash(`${ADMISSION_RECORD_VERSION}:TOKEN`, token); }
621
+
622
+ function defaultOwnerToken(): string { return `admission-owner:v2:${crypto.randomBytes(32).toString('base64url')}`; }
623
+ function defaultInvocationToken(): string { return `admission-invocation:v2:${crypto.randomBytes(32).toString('base64url')}`; }
624
+ function validateOwner(value: unknown): string {
625
+ if (typeof value !== 'string' || !OWNER_TOKEN.test(value)) fail('invalid_owner_token', 'owner token is invalid');
626
+ return value;
627
+ }
628
+
629
+ type MutableRecord = Omit<AdmissionRecord, 'record_digest' | 'resources'> & {
630
+ resources: AdmissionResourceReservation[];
631
+ record_digest?: AdmissionDigest;
632
+ };
633
+ interface Stored { record: Readonly<AdmissionRecord>; ownerToken: string }
634
+
635
+ function finalizeRecord(raw: MutableRecord): Readonly<AdmissionRecord> {
636
+ const { record_digest: _ignored, ...body } = raw;
637
+ return frozenCopy({ ...body, record_digest: hash(`${ADMISSION_RECORD_VERSION}:DIGEST`, body) } as AdmissionRecord);
638
+ }
639
+
640
+ function finalizeJournal(raw: Omit<AdmissionJournalEntry, 'entry_digest'>): Readonly<AdmissionJournalEntry> {
641
+ return frozenCopy({ ...raw, entry_digest: hash(`${ADMISSION_JOURNAL_VERSION}:DIGEST`, raw) });
642
+ }
643
+
644
+ export function verifyAdmissionJournal(entries: readonly AdmissionJournalEntry[]): { ok: true } | { ok: false; at: number; reason: string } {
645
+ let predecessor: string | null = null;
646
+ for (let index = 0; index < entries.length; index += 1) {
647
+ const entry = entries[index];
648
+ if (entry.sequence !== index) return { ok: false, at: index, reason: 'sequence_mismatch' };
649
+ if (entry.predecessor_digest !== predecessor) return { ok: false, at: index, reason: 'predecessor_mismatch' };
650
+ const { entry_digest, ...body } = entry;
651
+ if (hash(`${ADMISSION_JOURNAL_VERSION}:DIGEST`, body) !== entry_digest) return { ok: false, at: index, reason: 'digest_mismatch' };
652
+ predecessor = entry_digest;
653
+ }
654
+ return { ok: true };
655
+ }
656
+
657
+ function exactOperationIdentity(left: AdmissionSnapshotBody, right: AdmissionSnapshotBody): boolean {
658
+ return left.tenant_id === right.tenant_id
659
+ && left.operation_id === right.operation_id
660
+ && left.caid === right.caid
661
+ && left.action_digest === right.action_digest
662
+ && left.effect_request_digest === right.effect_request_digest
663
+ && canonical(left.provider as unknown as Json) === canonical(right.provider as unknown as Json)
664
+ && left.executor_adapter_digest === right.executor_adapter_digest
665
+ && left.idempotency_key === right.idempotency_key;
666
+ }
667
+
668
+ function currentnessMatches(
669
+ snapshot: Readonly<AdmissionSnapshot>,
670
+ observation: AdmissionCurrentnessObservation,
671
+ now: number,
672
+ maxAgeMs: number,
673
+ ): boolean {
674
+ const body = snapshot.body;
675
+ const observedAt = Date.parse(observation.observed_at);
676
+ if (observation['@version'] !== ADMISSION_CURRENTNESS_VERSION
677
+ || observation.candidate_match !== 'EXACT_MATCH'
678
+ || !Number.isFinite(observedAt)
679
+ || observedAt > now
680
+ || now - observedAt > maxAgeMs
681
+ || Date.parse(observation.qualification_status_expires_at) <= now
682
+ || observation.qualification_status_authority_id !== body.qualification_status.authority_id
683
+ || observation.qualification_status_sequence !== body.qualification_status.sequence
684
+ || observation.qualification_status_head_digest !== body.qualification_status.head_payload_digest
685
+ || observation.trust_epoch !== body.trust_epoch
686
+ || observation.trust_configuration_digest !== body.trust_configuration_digest
687
+ || observation.configuration_epoch !== body.configuration_epoch
688
+ || observation.configuration_digest !== body.configuration_digest
689
+ || observation.runtime_measurement_digest !== body.runtime_measurement_digest) return false;
690
+ const expected = body.resource_reservations.filter((resource) => resource.kind === 'external_lease');
691
+ if (observation.external_leases.length !== expected.length) return false;
692
+ const byId = new Map(observation.external_leases.map((lease) => [lease.resource_id, lease]));
693
+ return expected.every((lease) => {
694
+ const current = byId.get(lease.resource_id);
695
+ return current?.digest === lease.digest && Date.parse(current.expires_at) > now;
696
+ });
697
+ }
698
+
699
+ /** Linearizable, explicitly test-only reference implementation. */
700
+ export function createMemoryAdmissionStore(options: CreateMemoryAdmissionStoreOptions = {}): AdmissionStore & { readonly testOnly: true } {
701
+ const snapshots = new Map<string, Readonly<AdmissionSnapshot>>();
702
+ const records = new Map<string, Stored>();
703
+ const operationHeads = new Map<string, string>();
704
+ const resourceOwners = new Map<string, string>();
705
+ const journals = new Map<string, readonly Readonly<AdmissionJournalEntry>[]>();
706
+ const ownerFactory = options.ownerTokenFactory ?? defaultOwnerToken;
707
+ const invocationFactory = options.invocationTokenFactory ?? defaultInvocationToken;
708
+ const maxCurrentnessAgeMs = options.maxCurrentnessAgeMs
709
+ ?? ADMISSION_LIMITS.currentnessMaxAgeMs;
710
+ if (!Number.isSafeInteger(maxCurrentnessAgeMs)
711
+ || maxCurrentnessAgeMs < 1
712
+ || maxCurrentnessAgeMs > 300_000) {
713
+ fail('invalid_currentness_age', 'max currentness age is invalid');
714
+ }
715
+ let queue: Promise<void> = Promise.resolve();
716
+
717
+ function atomic<T>(fn: () => T | Promise<T>): Promise<T> {
718
+ const next = queue.then(fn, fn);
719
+ queue = next.then(() => undefined, () => undefined);
720
+ return next;
721
+ }
722
+
723
+ function append(key: string, ownerToken: string, draft: MutableRecord, event: AdmissionJournalEvent, at: string): Readonly<AdmissionRecord> {
724
+ const history = journals.get(key) ?? [];
725
+ if (draft.revision !== history.length) throw new Error('admission invariant: revision/journal mismatch');
726
+ const record = finalizeRecord(draft);
727
+ const previous = history.at(-1);
728
+ const entry = finalizeJournal({
729
+ '@version': ADMISSION_JOURNAL_VERSION,
730
+ tenant_id: record.tenant_id,
731
+ admission_id: record.admission_id,
732
+ operation_id: record.operation_id,
733
+ sequence: history.length,
734
+ event,
735
+ snapshot_digest: record.snapshot_digest,
736
+ record_digest: record.record_digest,
737
+ predecessor_digest: previous?.entry_digest ?? null,
738
+ recorded_at: at,
739
+ });
740
+ journals.set(key, [...history, entry]);
741
+ records.set(key, { record, ownerToken });
742
+ return record;
743
+ }
744
+
745
+ function next(current: Readonly<AdmissionRecord>, at: string, changes: Partial<MutableRecord>): MutableRecord {
746
+ return {
747
+ ...current,
748
+ ...changes,
749
+ revision: current.revision + 1,
750
+ updated_at: at,
751
+ predecessor_record_digest: current.record_digest,
752
+ resources: changes.resources ?? current.resources.map((resource) => ({ ...resource })),
753
+ record_digest: undefined,
754
+ };
755
+ }
756
+
757
+ function selectCas(input: AdmissionCas): { key: string; stored: Stored } | AdmissionRefusal {
758
+ const key = admissionKey(identifier(input.tenant_id, 'tenant_id'), identifier(input.admission_id, 'admission_id'));
759
+ const stored = records.get(key);
760
+ if (!stored) return { ok: false, reason: 'admission_not_found' };
761
+ if (stored.record.revision !== nonNegativeInteger(input.expected_revision, 'expected_revision')) return { ok: false, reason: 'revision_conflict' };
762
+ if (stored.ownerToken !== validateOwner(input.owner_token)) return { ok: false, reason: 'owner_conflict' };
763
+ return { key, stored };
764
+ }
765
+
766
+ function resourcesAvailable(snapshot: Readonly<AdmissionSnapshot>, ignored?: string): boolean {
767
+ return snapshot.body.resource_reservations.every((resource) => {
768
+ const owner = resourceOwners.get(resourceKey(snapshot.body.tenant_id, resource));
769
+ return owner === undefined || owner === ignored;
770
+ });
771
+ }
772
+ function claimResources(snapshot: Readonly<AdmissionSnapshot>, key: string): void {
773
+ for (const resource of snapshot.body.resource_reservations) resourceOwners.set(resourceKey(snapshot.body.tenant_id, resource), key);
774
+ }
775
+ function freeResources(record: Readonly<AdmissionRecord>, key: string): void {
776
+ for (const resource of record.resources) {
777
+ const rKey = resourceKey(record.tenant_id, resource);
778
+ if (resourceOwners.get(rKey) === key) resourceOwners.delete(rKey);
779
+ }
780
+ }
781
+
782
+ function invariantViolations(): string[] {
783
+ const violations: string[] = [];
784
+ for (const [key, stored] of records) {
785
+ const record = stored.record;
786
+ const history = journals.get(key) ?? [];
787
+ if (!snapshots.has(record.snapshot_digest)) violations.push(`${key}:snapshot_missing`);
788
+ if (!verifyAdmissionJournal(history).ok) violations.push(`${key}:journal_invalid`);
789
+ if (history.length !== record.revision + 1 || history.at(-1)?.record_digest !== record.record_digest) violations.push(`${key}:head_mismatch`);
790
+ if (record.state === 'RESERVED' && (record.execution_right !== 'RESERVED' || record.provider_attempt !== 'NOT_ENTERED')) violations.push(`${key}:reserved_invalid`);
791
+ if (['INVOKING', 'INDETERMINATE', 'COMMITTED', 'PROVEN_NOT_COMMITTED'].includes(record.state)
792
+ && (record.execution_right !== 'CONSUMED' || record.resources.some((resource) => resource.state !== 'CONSUMED'))) violations.push(`${key}:consumption_invalid`);
793
+ if (record.state === 'SUPERSEDED' && record.superseded_by_admission_id === null) violations.push(`${key}:supersession_missing`);
794
+ }
795
+ for (const [op, admission] of operationHeads) {
796
+ const stored = records.get(admission);
797
+ if (!stored || operationKey(stored.record.tenant_id, stored.record.operation_id) !== op) violations.push(`${op}:operation_head_invalid`);
798
+ }
799
+ return violations;
800
+ }
801
+
802
+ const store: AdmissionStore & { readonly testOnly: true } = {
803
+ testOnly: true,
804
+ durable: false,
805
+ atomic: true,
806
+ compareAndSwap: true,
807
+ appendOnlyJournal: true,
808
+ exclusiveActuation: true,
809
+ transactionalCurrentness: true,
810
+
811
+ reserve(raw) {
812
+ return atomic(() => {
813
+ const snapshot = plain(raw) && Object.hasOwn(raw, 'snapshot_digest')
814
+ ? validateSnapshot(raw as AdmissionSnapshot)
815
+ : createAdmissionSnapshot(raw as AdmissionSnapshotInput);
816
+ const body = snapshot.body;
817
+ if (body.supersedes_admission_id !== null) return { ok: false, reason: 'relation_conflict' };
818
+ const key = admissionKey(body.tenant_id, body.admission_id);
819
+ const opKey = operationKey(body.tenant_id, body.operation_id);
820
+ if (records.has(key)) return { ok: false, reason: 'admission_exists' };
821
+ if (operationHeads.has(opKey)) return { ok: false, reason: 'operation_exists' };
822
+ const now = currentMs(options.now);
823
+ if (Date.parse(body.expires_at) <= now) return { ok: false, reason: 'admission_expired' };
824
+ if (body.remedy_for !== null) {
825
+ const target = records.get(admissionKey(body.remedy_for.tenant_id, body.remedy_for.admission_id));
826
+ if (!target || target.record.snapshot_digest !== body.remedy_for.snapshot_digest) return { ok: false, reason: 'relation_not_found' };
827
+ if (!['INVOKING', 'INDETERMINATE', 'COMMITTED', 'PROVEN_NOT_COMMITTED'].includes(target.record.state)) return { ok: false, reason: 'relation_conflict' };
828
+ const targetSnapshot = snapshots.get(target.record.snapshot_digest);
829
+ if (!targetSnapshot || targetSnapshot.body.operation_id === body.operation_id || targetSnapshot.body.caid === body.caid) return { ok: false, reason: 'relation_conflict' };
830
+ }
831
+ if (!resourcesAvailable(snapshot)) return { ok: false, reason: 'resource_conflict' };
832
+ const owner = validateOwner(ownerFactory());
833
+ const at = new Date(now).toISOString();
834
+ snapshots.set(snapshot.snapshot_digest, snapshot);
835
+ operationHeads.set(opKey, key);
836
+ claimResources(snapshot, key);
837
+ const record = append(key, owner, {
838
+ '@version': ADMISSION_RECORD_VERSION,
839
+ tenant_id: body.tenant_id,
840
+ admission_id: body.admission_id,
841
+ operation_id: body.operation_id,
842
+ snapshot_digest: snapshot.snapshot_digest,
843
+ revision: 0,
844
+ state: 'RESERVED',
845
+ execution_right: 'RESERVED',
846
+ provider_attempt: 'NOT_ENTERED',
847
+ owner_digest: tokenDigest(owner),
848
+ invocation_token_digest: null,
849
+ provider_outcome: null,
850
+ effect_relation: null,
851
+ resources: body.resource_reservations.map((resource) => ({ ...resource, state: 'RESERVED' })),
852
+ superseded_by_admission_id: null,
853
+ refusal_reason: null,
854
+ invocation_started_at: null,
855
+ created_at: at,
856
+ updated_at: at,
857
+ predecessor_record_digest: null,
858
+ }, 'RESERVED', at);
859
+ return { ok: true, snapshot, record, owner_token: owner };
860
+ });
861
+ },
862
+
863
+ release(input, reason = 'released_before_invocation') {
864
+ return atomic(() => {
865
+ const selected = selectCas(input);
866
+ if ('ok' in selected) return selected;
867
+ const { key, stored } = selected;
868
+ if (stored.record.execution_right === 'CONSUMED') return { ok: false, reason: 'execution_right_consumed' };
869
+ if (stored.record.state !== 'RESERVED') return { ok: false, reason: 'state_conflict' };
870
+ const at = new Date(currentMs(options.now)).toISOString();
871
+ freeResources(stored.record, key);
872
+ const record = append(key, stored.ownerToken, next(stored.record, at, {
873
+ state: 'RELEASED', execution_right: 'RELEASED', refusal_reason: reason,
874
+ resources: stored.record.resources.map((resource) => ({ ...resource, state: 'RELEASED' })),
875
+ }), 'RELEASED', at);
876
+ return { ok: true, record };
877
+ });
878
+ },
879
+
880
+ expire(input) {
881
+ return atomic(() => {
882
+ const selected = selectCas(input);
883
+ if ('ok' in selected) return selected;
884
+ const { key, stored } = selected;
885
+ if (stored.record.state !== 'RESERVED') return { ok: false, reason: 'state_conflict' };
886
+ const snapshot = snapshots.get(stored.record.snapshot_digest)!;
887
+ const now = currentMs(options.now);
888
+ if (Date.parse(snapshot.body.expires_at) > now) return { ok: false, reason: 'state_conflict' };
889
+ const at = new Date(now).toISOString();
890
+ freeResources(stored.record, key);
891
+ const record = append(key, stored.ownerToken, next(stored.record, at, {
892
+ state: 'EXPIRED', execution_right: 'RELEASED', refusal_reason: 'admission_expired',
893
+ resources: stored.record.resources.map((resource) => ({ ...resource, state: 'RELEASED' })),
894
+ }), 'EXPIRED', at);
895
+ return { ok: true, record };
896
+ });
897
+ },
898
+
899
+ supersede(input) {
900
+ return atomic(() => {
901
+ const selected = selectCas(input);
902
+ if ('ok' in selected) return selected;
903
+ const { key, stored } = selected;
904
+ if (stored.record.state !== 'RESERVED' || stored.record.execution_right !== 'RESERVED') return { ok: false, reason: 'state_conflict' };
905
+ const predecessor = snapshots.get(stored.record.snapshot_digest)!;
906
+ const successor = createAdmissionSnapshot({ ...input.successor, supersedes_admission_id: stored.record.admission_id, remedy_for: null });
907
+ if (successor.body.admission_id === stored.record.admission_id || !exactOperationIdentity(predecessor.body, successor.body)) return { ok: false, reason: 'operation_conflict' };
908
+ const successorKey = admissionKey(successor.body.tenant_id, successor.body.admission_id);
909
+ if (records.has(successorKey)) return { ok: false, reason: 'admission_exists' };
910
+ const now = currentMs(options.now);
911
+ if (Date.parse(successor.body.expires_at) <= now) return { ok: false, reason: 'admission_expired' };
912
+ if (!resourcesAvailable(successor, key)) return { ok: false, reason: 'resource_conflict' };
913
+ const at = new Date(now).toISOString();
914
+ freeResources(stored.record, key);
915
+ const predecessorRecord = append(key, stored.ownerToken, next(stored.record, at, {
916
+ state: 'SUPERSEDED', execution_right: 'RELEASED', superseded_by_admission_id: successor.body.admission_id,
917
+ resources: stored.record.resources.map((resource) => ({ ...resource, state: 'RELEASED' })),
918
+ }), 'SUPERSEDED', at);
919
+ const owner = validateOwner(ownerFactory());
920
+ snapshots.set(successor.snapshot_digest, successor);
921
+ claimResources(successor, successorKey);
922
+ operationHeads.set(operationKey(successor.body.tenant_id, successor.body.operation_id), successorKey);
923
+ const successorRecord = append(successorKey, owner, {
924
+ '@version': ADMISSION_RECORD_VERSION,
925
+ tenant_id: successor.body.tenant_id,
926
+ admission_id: successor.body.admission_id,
927
+ operation_id: successor.body.operation_id,
928
+ snapshot_digest: successor.snapshot_digest,
929
+ revision: 0,
930
+ state: 'RESERVED', execution_right: 'RESERVED', provider_attempt: 'NOT_ENTERED',
931
+ owner_digest: tokenDigest(owner), invocation_token_digest: null,
932
+ provider_outcome: null, effect_relation: null,
933
+ resources: successor.body.resource_reservations.map((resource) => ({ ...resource, state: 'RESERVED' })),
934
+ superseded_by_admission_id: null, refusal_reason: null, invocation_started_at: null,
935
+ created_at: at, updated_at: at, predecessor_record_digest: null,
936
+ }, 'RESERVED', at);
937
+ return { ok: true, predecessor_record: predecessorRecord, successor_snapshot: successor, successor_record: successorRecord, successor_owner_token: owner };
938
+ });
939
+ },
940
+
941
+ beginInvocation(input) {
942
+ return atomic(async () => {
943
+ const selected = selectCas(input);
944
+ if ('ok' in selected) return selected;
945
+ const { key, stored } = selected;
946
+ if (stored.record.state !== 'RESERVED' || stored.record.execution_right !== 'RESERVED' || stored.record.provider_attempt !== 'NOT_ENTERED') return { ok: false, reason: 'state_conflict' };
947
+ const snapshot = snapshots.get(stored.record.snapshot_digest)!;
948
+ const now = currentMs(options.now);
949
+ if (Date.parse(snapshot.body.expires_at) <= now) return { ok: false, reason: 'admission_expired' };
950
+ const observation = options.currentnessOracle
951
+ ? await options.currentnessOracle.read(snapshot)
952
+ : null;
953
+ if (!observation || !currentnessMatches(
954
+ snapshot,
955
+ observation,
956
+ now,
957
+ maxCurrentnessAgeMs,
958
+ )) {
959
+ const at = new Date(now).toISOString();
960
+ freeResources(stored.record, key);
961
+ append(key, stored.ownerToken, next(stored.record, at, {
962
+ state: 'RELEASED', execution_right: 'RELEASED', refusal_reason: 'currentness_refused',
963
+ resources: stored.record.resources.map((resource) => ({ ...resource, state: 'RELEASED' })),
964
+ }), 'RELEASED', at);
965
+ return { ok: false, reason: 'currentness_refused' };
966
+ }
967
+ const invocationToken = invocationFactory();
968
+ if (typeof invocationToken !== 'string' || invocationToken.length < 48) fail('invalid_invocation_token', 'invocation token is invalid');
969
+ const at = new Date(now).toISOString();
970
+ const record = append(key, stored.ownerToken, next(stored.record, at, {
971
+ state: 'INVOKING', execution_right: 'CONSUMED', provider_attempt: 'INVOKING',
972
+ invocation_token_digest: tokenDigest(invocationToken), invocation_started_at: at,
973
+ resources: stored.record.resources.map((resource) => ({ ...resource, state: 'CONSUMED' })),
974
+ }), 'INVOKING', at);
975
+ return { ok: true, snapshot, record, invocation_token: invocationToken };
976
+ });
977
+ },
978
+
979
+ recoverIndeterminate(input) {
980
+ return atomic(() => {
981
+ const key = admissionKey(identifier(input.tenant_id, 'tenant_id'), identifier(input.admission_id, 'admission_id'));
982
+ const stored = records.get(key);
983
+ if (!stored) return { ok: false, reason: 'admission_not_found' };
984
+ if (stored.ownerToken !== validateOwner(input.owner_token)) return { ok: false, reason: 'owner_conflict' };
985
+ if (stored.record.state !== 'INVOKING') return { ok: false, reason: 'state_conflict' };
986
+ const reconciliationToken = invocationFactory();
987
+ if (typeof reconciliationToken !== 'string' || reconciliationToken.length < 48) fail('invalid_invocation_token', 'reconciliation token is invalid');
988
+ const at = new Date(currentMs(options.now)).toISOString();
989
+ const record = append(key, stored.ownerToken, next(stored.record, at, {
990
+ state: 'INDETERMINATE', provider_attempt: 'INDETERMINATE',
991
+ invocation_token_digest: tokenDigest(reconciliationToken),
992
+ provider_outcome: { value: 'INDETERMINATE', evidence_digest: null, observed_at: at },
993
+ refusal_reason: 'ambiguous_provider_entry',
994
+ }), 'RECOVERED_INDETERMINATE', at);
995
+ return { ok: true, record, reconciliation_token: reconciliationToken };
996
+ });
997
+ },
998
+
999
+ recordProviderOutcome(input) {
1000
+ return atomic(() => {
1001
+ const selected = selectCas(input);
1002
+ if ('ok' in selected) return selected;
1003
+ const { key, stored } = selected;
1004
+ if (stored.record.execution_right !== 'CONSUMED' || stored.record.invocation_token_digest !== tokenDigest(input.invocation_token)) return { ok: false, reason: 'invocation_token_conflict' };
1005
+ if (!['INVOKING', 'INDETERMINATE', 'COMMITTED', 'PROVEN_NOT_COMMITTED'].includes(stored.record.state)) return { ok: false, reason: 'state_conflict' };
1006
+ if (!['COMMITTED', 'PROVEN_NOT_COMMITTED', 'INDETERMINATE'].includes(input.value)) fail('invalid_provider_outcome', 'provider outcome is invalid');
1007
+ const observed = instant(input.observed_at, 'observed_at').iso;
1008
+ const evidence = input.evidence_digest === null ? null : digest(input.evidence_digest, 'evidence_digest');
1009
+ if (input.value !== 'INDETERMINATE' && evidence === null) return { ok: false, reason: 'evidence_required' };
1010
+ const current = stored.record.provider_outcome;
1011
+ if (current && current.value !== 'INDETERMINATE' && (current.value !== input.value || current.evidence_digest !== evidence)) return { ok: false, reason: 'outcome_conflict' };
1012
+ const at = new Date(currentMs(options.now)).toISOString();
1013
+ const record = append(key, stored.ownerToken, next(stored.record, at, {
1014
+ state: input.value,
1015
+ provider_attempt: input.value,
1016
+ provider_outcome: { value: input.value, evidence_digest: evidence, observed_at: observed },
1017
+ }), 'PROVIDER_OUTCOME', at);
1018
+ return { ok: true, record };
1019
+ });
1020
+ },
1021
+
1022
+ recordEffectRelation(input) {
1023
+ return atomic(() => {
1024
+ const selected = selectCas(input);
1025
+ if ('ok' in selected) return selected;
1026
+ const { key, stored } = selected;
1027
+ if (stored.record.execution_right !== 'CONSUMED' || stored.record.invocation_token_digest !== tokenDigest(input.invocation_token)) return { ok: false, reason: 'invocation_token_conflict' };
1028
+ if (!['OBSERVED_AS_REQUESTED', 'DIVERGED', 'INDETERMINATE'].includes(input.value)) fail('invalid_effect_relation', 'effect relation is invalid');
1029
+ const observed = instant(input.observed_at, 'observed_at').iso;
1030
+ const evidence = input.evidence_digest === null ? null : digest(input.evidence_digest, 'evidence_digest');
1031
+ if (input.value !== 'INDETERMINATE' && evidence === null) return { ok: false, reason: 'evidence_required' };
1032
+ const current = stored.record.effect_relation;
1033
+ if (current && current.value !== 'INDETERMINATE' && (current.value !== input.value || current.evidence_digest !== evidence)) return { ok: false, reason: 'outcome_conflict' };
1034
+ const at = new Date(currentMs(options.now)).toISOString();
1035
+ const record = append(key, stored.ownerToken, next(stored.record, at, {
1036
+ effect_relation: { value: input.value, evidence_digest: evidence, observed_at: observed },
1037
+ }), 'EFFECT_RELATION', at);
1038
+ return { ok: true, record };
1039
+ });
1040
+ },
1041
+
1042
+ read(input) {
1043
+ return atomic(() => records.get(admissionKey(identifier(input.tenant_id, 'tenant_id'), identifier(input.admission_id, 'admission_id')))?.record ?? null);
1044
+ },
1045
+ readByOperation(input) {
1046
+ return atomic(() => {
1047
+ const head = operationHeads.get(operationKey(identifier(input.tenant_id, 'tenant_id'), identifier(input.operation_id, 'operation_id')));
1048
+ return head ? records.get(head)?.record ?? null : null;
1049
+ });
1050
+ },
1051
+ readSnapshot(raw) { return atomic(() => snapshots.get(digest(raw, 'snapshot_digest')) ?? null); },
1052
+ journal(input) {
1053
+ return atomic(() => journals.get(admissionKey(identifier(input.tenant_id, 'tenant_id'), identifier(input.admission_id, 'admission_id'))) ?? []);
1054
+ },
1055
+ checkInvariants() {
1056
+ return atomic(() => {
1057
+ const violations = invariantViolations();
1058
+ return { ok: violations.length === 0, violations };
1059
+ });
1060
+ },
1061
+ };
1062
+ return store;
1063
+ }
1064
+
1065
+ export default createMemoryAdmissionStore;