@emilia-protocol/gate 0.16.1 → 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.
- package/CHANGELOG.md +65 -0
- package/README.md +65 -0
- package/admission-store-postgres.js +4 -0
- package/admission-store.js +4 -0
- package/autonomy-control-plane-profile.js +2 -0
- package/dist/admission-store-postgres.d.ts +66 -0
- package/dist/admission-store-postgres.d.ts.map +1 -0
- package/dist/admission-store-postgres.js +537 -0
- package/dist/admission-store-postgres.js.map +1 -0
- package/dist/admission-store.d.ts +292 -0
- package/dist/admission-store.d.ts.map +1 -0
- package/dist/admission-store.js +777 -0
- package/dist/admission-store.js.map +1 -0
- package/dist/autonomy-control-plane-profile.d.ts +18 -0
- package/dist/autonomy-control-plane-profile.d.ts.map +1 -0
- package/dist/autonomy-control-plane-profile.js +352 -0
- package/dist/autonomy-control-plane-profile.js.map +1 -0
- package/dist/gate-qualification-v2.d.ts +225 -0
- package/dist/gate-qualification-v2.d.ts.map +1 -0
- package/dist/gate-qualification-v2.js +1011 -0
- package/dist/gate-qualification-v2.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/gate-qualification-v2.js +4 -0
- package/package.json +24 -3
- package/src/admission-store-postgres.ts +695 -0
- package/src/admission-store.ts +1065 -0
- package/src/autonomy-control-plane-profile.ts +371 -0
- package/src/gate-qualification-v2.ts +1534 -0
- package/src/index.ts +4 -0
|
@@ -0,0 +1,1011 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
/**
|
|
4
|
+
* Gate Qualification v2 orchestration over the canonical admission-custody
|
|
5
|
+
* contract. Qualification is evidence, not authorization: only a complete
|
|
6
|
+
* immutable AdmissionSnapshot may cross the one-time invocation boundary.
|
|
7
|
+
*/
|
|
8
|
+
import { createAdmissionSnapshot } from './admission-store.js';
|
|
9
|
+
export const GATE_QUALIFICATION_V2_VERSION = 'EP-GATE-QUALIFICATION-v2';
|
|
10
|
+
const DIGEST = /^sha256:[0-9a-f]{64}$/;
|
|
11
|
+
const REQUIRED_QUALIFICATION_CHECKS = Object.freeze([
|
|
12
|
+
'schemas',
|
|
13
|
+
'payload_signatures',
|
|
14
|
+
'trust_accepted',
|
|
15
|
+
'campaign_lineage',
|
|
16
|
+
'terminal_outcomes_complete',
|
|
17
|
+
'hidden_challenge_commitments',
|
|
18
|
+
'qualification_statement_binding',
|
|
19
|
+
'status_chain',
|
|
20
|
+
'status_current_as_observed',
|
|
21
|
+
'runtime_candidate_exact_match',
|
|
22
|
+
'assignment_in_scope',
|
|
23
|
+
'protected_request_bound',
|
|
24
|
+
]);
|
|
25
|
+
const DEFAULT_ADAPTER_TIMEOUT_MS = 30_000;
|
|
26
|
+
const MAX_ADAPTER_TIMEOUT_MS = 300_000;
|
|
27
|
+
/** Explicitly test-only custody. Production callers must supply durable KMS-backed custody. */
|
|
28
|
+
export function createMemoryInvocationAuthorityCustodyV2() {
|
|
29
|
+
const values = new Map();
|
|
30
|
+
return {
|
|
31
|
+
custody: 'protected',
|
|
32
|
+
durable: false,
|
|
33
|
+
testOnly: true,
|
|
34
|
+
async put(input) {
|
|
35
|
+
values.set(authorityKey(input.tenantId, input.admissionId), frozenCopy(input.authority));
|
|
36
|
+
},
|
|
37
|
+
async get(input) {
|
|
38
|
+
return values.get(authorityKey(input.tenantId, input.admissionId)) ?? null;
|
|
39
|
+
},
|
|
40
|
+
async delete(input) {
|
|
41
|
+
values.delete(authorityKey(input.tenantId, input.admissionId));
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function isRecord(value) {
|
|
46
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
const prototype = Object.getPrototypeOf(value);
|
|
50
|
+
return prototype === Object.prototype || prototype === null;
|
|
51
|
+
}
|
|
52
|
+
function validString(value) {
|
|
53
|
+
return typeof value === 'string' && value.length > 0 && value.length <= 512;
|
|
54
|
+
}
|
|
55
|
+
function validDigest(value) {
|
|
56
|
+
return typeof value === 'string' && DIGEST.test(value);
|
|
57
|
+
}
|
|
58
|
+
function validInstant(value) {
|
|
59
|
+
return typeof value === 'string' && Number.isFinite(Date.parse(value));
|
|
60
|
+
}
|
|
61
|
+
function clone(value) {
|
|
62
|
+
return structuredClone(value);
|
|
63
|
+
}
|
|
64
|
+
function deepFreeze(value) {
|
|
65
|
+
if (value !== null && typeof value === 'object' && !Object.isFrozen(value)) {
|
|
66
|
+
for (const child of Object.values(value))
|
|
67
|
+
deepFreeze(child);
|
|
68
|
+
Object.freeze(value);
|
|
69
|
+
}
|
|
70
|
+
return value;
|
|
71
|
+
}
|
|
72
|
+
function frozenCopy(value) {
|
|
73
|
+
return deepFreeze(clone(value));
|
|
74
|
+
}
|
|
75
|
+
function deeplyFrozen(value, seen = new WeakSet()) {
|
|
76
|
+
if (value === null || typeof value !== 'object')
|
|
77
|
+
return true;
|
|
78
|
+
if (seen.has(value))
|
|
79
|
+
return true;
|
|
80
|
+
seen.add(value);
|
|
81
|
+
if (!Object.isFrozen(value))
|
|
82
|
+
return false;
|
|
83
|
+
return Object.values(value).every((child) => deeplyFrozen(child, seen));
|
|
84
|
+
}
|
|
85
|
+
function addReason(reasons, reason) {
|
|
86
|
+
if (!reasons.includes(reason))
|
|
87
|
+
reasons.push(reason);
|
|
88
|
+
}
|
|
89
|
+
function canonicalSnapshot(value) {
|
|
90
|
+
if (!isRecord(value) || !isRecord(value.body)
|
|
91
|
+
|| !validDigest(value.snapshot_digest))
|
|
92
|
+
return { ok: false };
|
|
93
|
+
try {
|
|
94
|
+
const normalized = createAdmissionSnapshot(value.body);
|
|
95
|
+
if (normalized.snapshot_digest !== value.snapshot_digest) {
|
|
96
|
+
return { ok: false };
|
|
97
|
+
}
|
|
98
|
+
return { ok: true, snapshot: normalized };
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return { ok: false };
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
function actuationKey(snapshot) {
|
|
105
|
+
return JSON.stringify([
|
|
106
|
+
snapshot.body.tenant_id,
|
|
107
|
+
snapshot.body.operation_id,
|
|
108
|
+
]);
|
|
109
|
+
}
|
|
110
|
+
function rolePayloads(snapshot, role) {
|
|
111
|
+
return snapshot.body.inputs
|
|
112
|
+
.filter((entry) => entry.role === role)
|
|
113
|
+
.map((entry) => entry.payload_digest);
|
|
114
|
+
}
|
|
115
|
+
function sameDigests(left, right) {
|
|
116
|
+
if (left.length !== right.length)
|
|
117
|
+
return false;
|
|
118
|
+
const a = [...left].sort();
|
|
119
|
+
const b = [...right].sort();
|
|
120
|
+
return a.every((value, index) => value === b[index]);
|
|
121
|
+
}
|
|
122
|
+
function boundedReason(value) {
|
|
123
|
+
return validString(value) ? value : 'unspecified';
|
|
124
|
+
}
|
|
125
|
+
function validateQualification(qualification, snapshot, reasons) {
|
|
126
|
+
if (!isRecord(qualification)) {
|
|
127
|
+
addReason(reasons, 'qualification_result_invalid');
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (qualification.decision !== 'QUALIFIED') {
|
|
131
|
+
addReason(reasons, `qualification_not_qualified:${boundedReason(qualification.reason)}`);
|
|
132
|
+
}
|
|
133
|
+
if (qualification.verification !== 'VERIFIED') {
|
|
134
|
+
addReason(reasons, 'qualification_not_verified');
|
|
135
|
+
}
|
|
136
|
+
if (qualification.acceptance !== 'ACCEPTED') {
|
|
137
|
+
addReason(reasons, 'qualification_not_accepted');
|
|
138
|
+
}
|
|
139
|
+
if (qualification.candidate_match !== 'EXACT_MATCH') {
|
|
140
|
+
addReason(reasons, 'qualification_candidate_mismatch');
|
|
141
|
+
}
|
|
142
|
+
if (qualification.assignment_scope !== 'IN_SCOPE') {
|
|
143
|
+
addReason(reasons, 'qualification_assignment_out_of_scope');
|
|
144
|
+
}
|
|
145
|
+
if (qualification.currentness !== 'CURRENT_AS_OBSERVED') {
|
|
146
|
+
addReason(reasons, 'qualification_not_current');
|
|
147
|
+
}
|
|
148
|
+
if (qualification.campaign_graph !== 'COMPLETE') {
|
|
149
|
+
addReason(reasons, 'qualification_campaign_incomplete');
|
|
150
|
+
}
|
|
151
|
+
if (typeof qualification.remeasure_at_begin_invocation !== 'boolean') {
|
|
152
|
+
addReason(reasons, 'qualification_remeasurement_invalid');
|
|
153
|
+
}
|
|
154
|
+
const qualificationChecks = qualification.checks;
|
|
155
|
+
if (!isRecord(qualificationChecks)
|
|
156
|
+
|| REQUIRED_QUALIFICATION_CHECKS.some((check) => qualificationChecks[check] !== true)) {
|
|
157
|
+
addReason(reasons, 'qualification_checks_incomplete');
|
|
158
|
+
}
|
|
159
|
+
const payloads = qualification.payload_digests;
|
|
160
|
+
if (!isRecord(payloads)) {
|
|
161
|
+
addReason(reasons, 'qualification_payload_digests_invalid');
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if (payloads.candidate_manifest !== snapshot.body.candidate_manifest_digest) {
|
|
165
|
+
addReason(reasons, 'qualification_candidate_manifest_binding_mismatch');
|
|
166
|
+
}
|
|
167
|
+
if (payloads.runtime_measurement !== snapshot.body.runtime_measurement_digest) {
|
|
168
|
+
addReason(reasons, 'qualification_runtime_measurement_binding_mismatch');
|
|
169
|
+
}
|
|
170
|
+
if (payloads.protected_request_digest
|
|
171
|
+
!== snapshot.body.effect_request_digest) {
|
|
172
|
+
addReason(reasons, 'qualification_protected_request_binding_mismatch');
|
|
173
|
+
}
|
|
174
|
+
if (payloads.qualification_statement
|
|
175
|
+
!== snapshot.body.qualification_statement_payload_digest) {
|
|
176
|
+
addReason(reasons, 'qualification_statement_binding_mismatch');
|
|
177
|
+
}
|
|
178
|
+
if (payloads.qualification_status_head
|
|
179
|
+
!== snapshot.body.qualification_status.head_payload_digest) {
|
|
180
|
+
addReason(reasons, 'qualification_status_binding_mismatch');
|
|
181
|
+
}
|
|
182
|
+
if (!validDigest(payloads.campaign_head)
|
|
183
|
+
|| !validDigest(payloads.qualification_graph)) {
|
|
184
|
+
addReason(reasons, 'qualification_graph_binding_invalid');
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
function validateRequirement(name, leg, snapshot, reasons) {
|
|
188
|
+
if (!isRecord(leg)) {
|
|
189
|
+
addReason(reasons, `${name}_missing`);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
if (leg.decision !== 'allow')
|
|
193
|
+
addReason(reasons, `${name}_denied`);
|
|
194
|
+
if (!validString(leg.requirementId)) {
|
|
195
|
+
addReason(reasons, `${name}_requirement_missing`);
|
|
196
|
+
}
|
|
197
|
+
if (leg.caid !== snapshot.body.caid
|
|
198
|
+
|| leg.actionDigest !== snapshot.body.action_digest) {
|
|
199
|
+
addReason(reasons, `${name}_binding_mismatch`);
|
|
200
|
+
}
|
|
201
|
+
if (!validDigest(leg.evidenceDigest)) {
|
|
202
|
+
addReason(reasons, `${name}_evidence_invalid`);
|
|
203
|
+
}
|
|
204
|
+
else if (!sameDigests(rolePayloads(snapshot, name), [leg.evidenceDigest])) {
|
|
205
|
+
addReason(reasons, `${name}_snapshot_binding_mismatch`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
function validateLocalPolicy(leg, snapshot, reasons) {
|
|
209
|
+
if (!isRecord(leg)) {
|
|
210
|
+
addReason(reasons, 'localPolicy_missing');
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
if (leg.decision !== 'allow')
|
|
214
|
+
addReason(reasons, 'localPolicy_denied');
|
|
215
|
+
if (!validString(leg.policyId)) {
|
|
216
|
+
addReason(reasons, 'localPolicy_policy_missing');
|
|
217
|
+
}
|
|
218
|
+
if (leg.caid !== snapshot.body.caid
|
|
219
|
+
|| leg.actionDigest !== snapshot.body.action_digest) {
|
|
220
|
+
addReason(reasons, 'localPolicy_binding_mismatch');
|
|
221
|
+
}
|
|
222
|
+
if (!validDigest(leg.evidenceDigest)) {
|
|
223
|
+
addReason(reasons, 'localPolicy_evidence_invalid');
|
|
224
|
+
}
|
|
225
|
+
else if (!sameDigests(rolePayloads(snapshot, 'local_policy'), [leg.evidenceDigest])) {
|
|
226
|
+
addReason(reasons, 'localPolicy_snapshot_binding_mismatch');
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
function validateSnapshotBindings(snapshot, reasons) {
|
|
230
|
+
const body = snapshot.body;
|
|
231
|
+
const singletonBindings = [
|
|
232
|
+
['candidate_manifest', body.candidate_manifest_digest],
|
|
233
|
+
['runtime_measurement', body.runtime_measurement_digest],
|
|
234
|
+
['qualification_statement', body.qualification_statement_payload_digest],
|
|
235
|
+
['qualification_status', body.qualification_status.head_payload_digest],
|
|
236
|
+
];
|
|
237
|
+
for (const [role, expected] of singletonBindings) {
|
|
238
|
+
if (!sameDigests(rolePayloads(snapshot, role), [expected])) {
|
|
239
|
+
addReason(reasons, `snapshot_${role}_binding_mismatch`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
if (!sameDigests(rolePayloads(snapshot, 'test_result'), body.test_result_payload_digests))
|
|
243
|
+
addReason(reasons, 'snapshot_test_result_binding_mismatch');
|
|
244
|
+
if (!sameDigests(rolePayloads(snapshot, 'agent_evaluation_evidence'), body.agent_evaluation_evidence_payload_digests))
|
|
245
|
+
addReason(reasons, 'snapshot_agent_evidence_binding_mismatch');
|
|
246
|
+
if (body.candidate_custody.request_construction !== 'EXECUTOR_ADAPTER'
|
|
247
|
+
|| body.candidate_custody.mutation_credential_custody
|
|
248
|
+
!== 'EXECUTOR_ADAPTER') {
|
|
249
|
+
addReason(reasons, 'snapshot_adapter_custody_mismatch');
|
|
250
|
+
}
|
|
251
|
+
const providerOperations = body.resource_reservations.filter((resource) => resource.kind === 'provider_operation'
|
|
252
|
+
&& resource.resource_id === body.operation_id);
|
|
253
|
+
if (providerOperations.length !== 1) {
|
|
254
|
+
addReason(reasons, 'snapshot_provider_operation_binding_mismatch');
|
|
255
|
+
}
|
|
256
|
+
if (body.supersedes_admission_id !== null) {
|
|
257
|
+
addReason(reasons, 'supersession_requires_atomic_store_transition');
|
|
258
|
+
}
|
|
259
|
+
if (body.remedy_for !== null) {
|
|
260
|
+
if (body.remedy_for.admission_id === body.admission_id) {
|
|
261
|
+
addReason(reasons, 'remedy_requires_new_admission');
|
|
262
|
+
}
|
|
263
|
+
if (body.remedy_for.operation_id === body.operation_id) {
|
|
264
|
+
addReason(reasons, 'remedy_requires_new_operation');
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
function emptyDecision(reasons) {
|
|
269
|
+
return deepFreeze({
|
|
270
|
+
version: GATE_QUALIFICATION_V2_VERSION,
|
|
271
|
+
allow: false,
|
|
272
|
+
reasons: deepFreeze([...reasons]),
|
|
273
|
+
tenantId: '',
|
|
274
|
+
admissionId: '',
|
|
275
|
+
operationId: '',
|
|
276
|
+
caid: '',
|
|
277
|
+
actionDigest: '',
|
|
278
|
+
snapshotDigest: '',
|
|
279
|
+
effectKey: '',
|
|
280
|
+
requirements: deepFreeze({
|
|
281
|
+
qualificationEvidenceDigest: '',
|
|
282
|
+
aebRequirementId: '',
|
|
283
|
+
aebEvidenceDigest: '',
|
|
284
|
+
aecRequirementId: '',
|
|
285
|
+
aecEvidenceDigest: '',
|
|
286
|
+
localPolicyId: '',
|
|
287
|
+
localPolicyEvidenceDigest: '',
|
|
288
|
+
}),
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
function composeCanonical(snapshot, qualification) {
|
|
292
|
+
const reasons = [];
|
|
293
|
+
validateSnapshotBindings(snapshot, reasons);
|
|
294
|
+
validateQualification(qualification?.qualification, snapshot, reasons);
|
|
295
|
+
validateRequirement('aeb', qualification?.aeb, snapshot, reasons);
|
|
296
|
+
validateRequirement('aec', qualification?.aec, snapshot, reasons);
|
|
297
|
+
validateLocalPolicy(qualification?.localPolicy, snapshot, reasons);
|
|
298
|
+
return deepFreeze({
|
|
299
|
+
version: GATE_QUALIFICATION_V2_VERSION,
|
|
300
|
+
allow: reasons.length === 0,
|
|
301
|
+
reasons: deepFreeze([...reasons]),
|
|
302
|
+
tenantId: snapshot.body.tenant_id,
|
|
303
|
+
admissionId: snapshot.body.admission_id,
|
|
304
|
+
operationId: snapshot.body.operation_id,
|
|
305
|
+
caid: snapshot.body.caid,
|
|
306
|
+
actionDigest: snapshot.body.action_digest,
|
|
307
|
+
snapshotDigest: snapshot.snapshot_digest,
|
|
308
|
+
effectKey: actuationKey(snapshot),
|
|
309
|
+
requirements: deepFreeze({
|
|
310
|
+
qualificationEvidenceDigest: snapshot.body.qualification_statement_payload_digest,
|
|
311
|
+
aebRequirementId: qualification?.aeb?.requirementId ?? '',
|
|
312
|
+
aebEvidenceDigest: qualification?.aeb?.evidenceDigest ?? '',
|
|
313
|
+
aecRequirementId: qualification?.aec?.requirementId ?? '',
|
|
314
|
+
aecEvidenceDigest: qualification?.aec?.evidenceDigest ?? '',
|
|
315
|
+
localPolicyId: qualification?.localPolicy?.policyId ?? '',
|
|
316
|
+
localPolicyEvidenceDigest: qualification?.localPolicy?.evidenceDigest ?? '',
|
|
317
|
+
}),
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
/** Pure deterministic composition; it performs no store or adapter access. */
|
|
321
|
+
export function composeQualificationDecisionV2(input) {
|
|
322
|
+
const checked = canonicalSnapshot(input?.snapshot);
|
|
323
|
+
if (!checked.ok)
|
|
324
|
+
return emptyDecision(['admission_snapshot_invalid']);
|
|
325
|
+
return composeCanonical(checked.snapshot, input?.qualification);
|
|
326
|
+
}
|
|
327
|
+
function verifyStoreCapabilities(store, testOnly) {
|
|
328
|
+
if (!store || store.atomic !== true || store.compareAndSwap !== true
|
|
329
|
+
|| store.appendOnlyJournal !== true
|
|
330
|
+
|| store.exclusiveActuation !== true) {
|
|
331
|
+
throw new TypeError('Gate Qualification v2 requires an atomic, compare-and-swap, '
|
|
332
|
+
+ 'append-only, exclusive AdmissionStore');
|
|
333
|
+
}
|
|
334
|
+
if (store.transactionalCurrentness !== true) {
|
|
335
|
+
throw new TypeError('Gate Qualification v2 requires a transactional-currentness AdmissionStore');
|
|
336
|
+
}
|
|
337
|
+
const candidate = store;
|
|
338
|
+
for (const method of [
|
|
339
|
+
'reserve',
|
|
340
|
+
'release',
|
|
341
|
+
'beginInvocation',
|
|
342
|
+
'recoverIndeterminate',
|
|
343
|
+
'recordProviderOutcome',
|
|
344
|
+
'recordEffectRelation',
|
|
345
|
+
'read',
|
|
346
|
+
'readByOperation',
|
|
347
|
+
'readSnapshot',
|
|
348
|
+
]) {
|
|
349
|
+
if (typeof candidate[method] !== 'function') {
|
|
350
|
+
throw new TypeError(`Gate Qualification v2 AdmissionStore requires ${method}()`);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
if (testOnly) {
|
|
354
|
+
if (store.testOnly !== true) {
|
|
355
|
+
throw new TypeError('Gate Qualification v2 testOnly requires an explicit test store');
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
else if (store.durable !== true || store.testOnly === true) {
|
|
359
|
+
throw new TypeError('Gate Qualification v2 production mode requires a durable AdmissionStore');
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
function protectedInvocation(snapshot, invocationToken) {
|
|
363
|
+
return deepFreeze({ snapshot, invocationToken });
|
|
364
|
+
}
|
|
365
|
+
function providerEvidenceBindingValid(evidence, snapshot) {
|
|
366
|
+
const body = snapshot.body;
|
|
367
|
+
return isRecord(evidence)
|
|
368
|
+
&& validString(evidence.evidenceId)
|
|
369
|
+
&& validDigest(evidence.evidenceDigest)
|
|
370
|
+
&& evidence.tenantId === body.tenant_id
|
|
371
|
+
&& evidence.admissionId === body.admission_id
|
|
372
|
+
&& evidence.operationId === body.operation_id
|
|
373
|
+
&& evidence.snapshotDigest === snapshot.snapshot_digest
|
|
374
|
+
&& evidence.caid === body.caid
|
|
375
|
+
&& evidence.actionDigest === body.action_digest
|
|
376
|
+
&& evidence.effectRequestDigest === body.effect_request_digest
|
|
377
|
+
&& isRecord(evidence.provider)
|
|
378
|
+
&& evidence.provider.provider_id === body.provider.provider_id
|
|
379
|
+
&& evidence.provider.account_id === body.provider.account_id
|
|
380
|
+
&& evidence.provider.environment === body.provider.environment
|
|
381
|
+
&& evidence.executorAdapterDigest === body.executor_adapter_digest
|
|
382
|
+
&& evidence.idempotencyKey === body.idempotency_key
|
|
383
|
+
&& ['COMMITTED', 'PROVEN_NOT_COMMITTED', 'INDETERMINATE'].includes(evidence.outcome)
|
|
384
|
+
&& validInstant(evidence.observedAt);
|
|
385
|
+
}
|
|
386
|
+
function observedRelationBindingValid(relation, evidence, snapshot) {
|
|
387
|
+
const body = snapshot.body;
|
|
388
|
+
if (!isRecord(relation)
|
|
389
|
+
|| !['OBSERVED_AS_REQUESTED', 'DIVERGED', 'INDETERMINATE'].includes(relation.relation)
|
|
390
|
+
|| relation.tenantId !== body.tenant_id
|
|
391
|
+
|| relation.admissionId !== body.admission_id
|
|
392
|
+
|| relation.operationId !== body.operation_id
|
|
393
|
+
|| relation.snapshotDigest !== snapshot.snapshot_digest
|
|
394
|
+
|| relation.caid !== body.caid
|
|
395
|
+
|| relation.actionDigest !== body.action_digest
|
|
396
|
+
|| relation.providerEvidenceDigest !== evidence.evidenceDigest
|
|
397
|
+
|| !validInstant(relation.observedAt)
|
|
398
|
+
|| (relation.evidenceDigest !== null
|
|
399
|
+
&& !validDigest(relation.evidenceDigest))
|
|
400
|
+
|| (relation.observedEffectDigest !== null
|
|
401
|
+
&& !validDigest(relation.observedEffectDigest)))
|
|
402
|
+
return false;
|
|
403
|
+
if (relation.relation === 'INDETERMINATE')
|
|
404
|
+
return true;
|
|
405
|
+
return validDigest(relation.evidenceDigest)
|
|
406
|
+
&& validDigest(relation.observedEffectDigest);
|
|
407
|
+
}
|
|
408
|
+
class AdapterTimeoutError extends Error {
|
|
409
|
+
constructor() {
|
|
410
|
+
super('protected adapter timed out');
|
|
411
|
+
this.name = 'AdapterTimeoutError';
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
async function withinTimeout(operation, timeoutMs) {
|
|
415
|
+
let timer;
|
|
416
|
+
try {
|
|
417
|
+
return await Promise.race([
|
|
418
|
+
Promise.resolve().then(operation),
|
|
419
|
+
new Promise((_resolve, reject) => {
|
|
420
|
+
timer = setTimeout(() => reject(new AdapterTimeoutError()), timeoutMs);
|
|
421
|
+
}),
|
|
422
|
+
]);
|
|
423
|
+
}
|
|
424
|
+
finally {
|
|
425
|
+
if (timer !== undefined)
|
|
426
|
+
clearTimeout(timer);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
function reference(snapshot) {
|
|
430
|
+
return {
|
|
431
|
+
tenant_id: snapshot.body.tenant_id,
|
|
432
|
+
admission_id: snapshot.body.admission_id,
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
function authorityKey(tenantId, admissionId) {
|
|
436
|
+
return JSON.stringify([tenantId, admissionId]);
|
|
437
|
+
}
|
|
438
|
+
function reconciliationRequired(admissionId, reason) {
|
|
439
|
+
return deepFreeze({ status: 'reconciliation_required', reason, admissionId });
|
|
440
|
+
}
|
|
441
|
+
export class GateQualificationV2 {
|
|
442
|
+
mode;
|
|
443
|
+
#store;
|
|
444
|
+
#adapter;
|
|
445
|
+
#evidenceVerifier;
|
|
446
|
+
#effectRelator;
|
|
447
|
+
#remeasurer;
|
|
448
|
+
#authorityCustody;
|
|
449
|
+
#legacy;
|
|
450
|
+
#adapterTimeoutMs;
|
|
451
|
+
constructor(options) {
|
|
452
|
+
if (!options || !['enforce', 'shadow'].includes(options.mode)) {
|
|
453
|
+
throw new TypeError('Gate Qualification v2 mode must be enforce or shadow');
|
|
454
|
+
}
|
|
455
|
+
if (options.mode === 'shadow') {
|
|
456
|
+
if ('admissionStore' in options || 'protectedAdapter' in options
|
|
457
|
+
|| 'providerEvidenceVerifier' in options
|
|
458
|
+
|| 'observedEffectRelator' in options
|
|
459
|
+
|| 'invocationRemeasurer' in options
|
|
460
|
+
|| 'authorityCustody' in options) {
|
|
461
|
+
throw new TypeError('Gate Qualification v2 shadow mode accepts no AdmissionStore or protected adapter');
|
|
462
|
+
}
|
|
463
|
+
if (options.legacyQualification
|
|
464
|
+
&& typeof options.legacyQualification.qualify !== 'function') {
|
|
465
|
+
throw new TypeError('Gate Qualification v2 legacy qualification must expose qualify()');
|
|
466
|
+
}
|
|
467
|
+
this.mode = 'shadow';
|
|
468
|
+
this.#legacy = options.legacyQualification;
|
|
469
|
+
this.#adapterTimeoutMs = DEFAULT_ADAPTER_TIMEOUT_MS;
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
verifyStoreCapabilities(options.admissionStore, options.testOnly === true);
|
|
473
|
+
if (!options.protectedAdapter
|
|
474
|
+
|| options.protectedAdapter.custody !== 'protected'
|
|
475
|
+
|| options.protectedAdapter.credentialsExposed !== false
|
|
476
|
+
|| typeof options.protectedAdapter.invoke !== 'function'
|
|
477
|
+
|| typeof options.protectedAdapter.reconcile !== 'function') {
|
|
478
|
+
throw new TypeError('Gate Qualification v2 requires protected adapter custody');
|
|
479
|
+
}
|
|
480
|
+
if (!options.providerEvidenceVerifier
|
|
481
|
+
|| typeof options.providerEvidenceVerifier.verify !== 'function') {
|
|
482
|
+
throw new TypeError('Gate Qualification v2 requires a provider evidence verifier');
|
|
483
|
+
}
|
|
484
|
+
if (!options.observedEffectRelator
|
|
485
|
+
|| typeof options.observedEffectRelator.relate !== 'function') {
|
|
486
|
+
throw new TypeError('Gate Qualification v2 requires an observed-effect relator');
|
|
487
|
+
}
|
|
488
|
+
if (!options.invocationRemeasurer
|
|
489
|
+
|| options.invocationRemeasurer.source !== 'authoritative'
|
|
490
|
+
|| typeof options.invocationRemeasurer.remeasure !== 'function') {
|
|
491
|
+
throw new TypeError('Gate Qualification v2 requires authoritative invocation remeasurement');
|
|
492
|
+
}
|
|
493
|
+
if (!options.authorityCustody
|
|
494
|
+
|| options.authorityCustody.custody !== 'protected'
|
|
495
|
+
|| typeof options.authorityCustody.put !== 'function'
|
|
496
|
+
|| typeof options.authorityCustody.get !== 'function'
|
|
497
|
+
|| typeof options.authorityCustody.delete !== 'function') {
|
|
498
|
+
throw new TypeError('Gate Qualification v2 requires protected invocation-authority custody');
|
|
499
|
+
}
|
|
500
|
+
if (options.testOnly === true) {
|
|
501
|
+
if (options.authorityCustody.testOnly !== true) {
|
|
502
|
+
throw new TypeError('Gate Qualification v2 testOnly requires explicit test authority custody');
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
else if (options.authorityCustody.durable !== true
|
|
506
|
+
|| options.authorityCustody.testOnly === true) {
|
|
507
|
+
throw new TypeError('Gate Qualification v2 production mode requires durable authority custody');
|
|
508
|
+
}
|
|
509
|
+
const timeout = options.adapterTimeoutMs ?? DEFAULT_ADAPTER_TIMEOUT_MS;
|
|
510
|
+
if (!Number.isSafeInteger(timeout) || timeout < 1
|
|
511
|
+
|| timeout > MAX_ADAPTER_TIMEOUT_MS) {
|
|
512
|
+
throw new TypeError('Gate Qualification v2 adapterTimeoutMs is invalid');
|
|
513
|
+
}
|
|
514
|
+
this.mode = 'enforce';
|
|
515
|
+
this.#store = options.admissionStore;
|
|
516
|
+
this.#adapter = options.protectedAdapter;
|
|
517
|
+
this.#evidenceVerifier = options.providerEvidenceVerifier;
|
|
518
|
+
this.#effectRelator = options.observedEffectRelator;
|
|
519
|
+
this.#remeasurer = options.invocationRemeasurer;
|
|
520
|
+
this.#authorityCustody = options.authorityCustody;
|
|
521
|
+
this.#adapterTimeoutMs = timeout;
|
|
522
|
+
}
|
|
523
|
+
async #shadow(input, decision) {
|
|
524
|
+
let legacyAllowed = null;
|
|
525
|
+
let legacyReasons = [];
|
|
526
|
+
if (this.#legacy) {
|
|
527
|
+
try {
|
|
528
|
+
const legacy = await this.#legacy.qualify(frozenCopy(input));
|
|
529
|
+
legacyAllowed = legacy.allow === true;
|
|
530
|
+
legacyReasons = deepFreeze([...legacy.reasons]);
|
|
531
|
+
}
|
|
532
|
+
catch {
|
|
533
|
+
legacyAllowed = false;
|
|
534
|
+
legacyReasons = deepFreeze(['legacy_qualification_failed']);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
return deepFreeze({
|
|
538
|
+
status: 'shadow',
|
|
539
|
+
decision,
|
|
540
|
+
comparison: {
|
|
541
|
+
legacyAllowed,
|
|
542
|
+
v2Allowed: decision.allow,
|
|
543
|
+
match: legacyAllowed === null
|
|
544
|
+
? null
|
|
545
|
+
: legacyAllowed === decision.allow,
|
|
546
|
+
legacyReasons,
|
|
547
|
+
v2Reasons: decision.reasons,
|
|
548
|
+
},
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
async #saveAuthority(snapshot, authority) {
|
|
552
|
+
await this.#authorityCustody.put({
|
|
553
|
+
tenantId: snapshot.body.tenant_id,
|
|
554
|
+
admissionId: snapshot.body.admission_id,
|
|
555
|
+
authority: frozenCopy(authority),
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
async #getAuthority(tenantId, admissionId) {
|
|
559
|
+
return this.#authorityCustody.get({ tenantId, admissionId });
|
|
560
|
+
}
|
|
561
|
+
async #recordIndeterminateEffect(snapshot, authority, expectedRevision) {
|
|
562
|
+
try {
|
|
563
|
+
const recorded = await this.#store.recordEffectRelation({
|
|
564
|
+
...reference(snapshot),
|
|
565
|
+
expected_revision: expectedRevision,
|
|
566
|
+
owner_token: authority.ownerToken,
|
|
567
|
+
invocation_token: authority.invocationToken,
|
|
568
|
+
value: 'INDETERMINATE',
|
|
569
|
+
evidence_digest: null,
|
|
570
|
+
observed_at: new Date().toISOString(),
|
|
571
|
+
});
|
|
572
|
+
return recorded.ok ? recorded.record : null;
|
|
573
|
+
}
|
|
574
|
+
catch {
|
|
575
|
+
return null;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
async #recoverInvoking(snapshot, ownerToken) {
|
|
579
|
+
try {
|
|
580
|
+
const recovered = await this.#store.recoverIndeterminate({
|
|
581
|
+
...reference(snapshot),
|
|
582
|
+
owner_token: ownerToken,
|
|
583
|
+
});
|
|
584
|
+
if (!recovered.ok)
|
|
585
|
+
return null;
|
|
586
|
+
const authority = {
|
|
587
|
+
ownerToken,
|
|
588
|
+
invocationToken: recovered.reconciliation_token,
|
|
589
|
+
snapshotDigest: snapshot.snapshot_digest,
|
|
590
|
+
};
|
|
591
|
+
await this.#saveAuthority(snapshot, authority);
|
|
592
|
+
await this.#recordIndeterminateEffect(snapshot, authority, recovered.record.revision);
|
|
593
|
+
return authority;
|
|
594
|
+
}
|
|
595
|
+
catch {
|
|
596
|
+
return null;
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
async #beginAmbiguity(snapshot, ownerToken) {
|
|
600
|
+
let record;
|
|
601
|
+
try {
|
|
602
|
+
record = await this.#store.read(reference(snapshot));
|
|
603
|
+
}
|
|
604
|
+
catch {
|
|
605
|
+
return reconciliationRequired(snapshot.body.admission_id, 'begin_invocation_read_ambiguous');
|
|
606
|
+
}
|
|
607
|
+
if (!record) {
|
|
608
|
+
return reconciliationRequired(snapshot.body.admission_id, 'begin_invocation_read_ambiguous');
|
|
609
|
+
}
|
|
610
|
+
if (record.state === 'INVOKING') {
|
|
611
|
+
await this.#recoverInvoking(snapshot, ownerToken);
|
|
612
|
+
}
|
|
613
|
+
return reconciliationRequired(snapshot.body.admission_id, 'begin_invocation_unconfirmed');
|
|
614
|
+
}
|
|
615
|
+
async #existingAdmissionResult(snapshot, storeReason, decision) {
|
|
616
|
+
if (storeReason !== 'admission_exists') {
|
|
617
|
+
return deepFreeze({ status: 'refused', reason: storeReason, decision });
|
|
618
|
+
}
|
|
619
|
+
try {
|
|
620
|
+
const record = await this.#store.read(reference(snapshot));
|
|
621
|
+
if (record && (record.state === 'RESERVED'
|
|
622
|
+
|| record.state === 'INVOKING'
|
|
623
|
+
|| record.state === 'INDETERMINATE')) {
|
|
624
|
+
return reconciliationRequired(record.admission_id, storeReason);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
catch {
|
|
628
|
+
return reconciliationRequired(snapshot.body.admission_id, 'admission_read_ambiguous');
|
|
629
|
+
}
|
|
630
|
+
return deepFreeze({ status: 'refused', reason: storeReason, decision });
|
|
631
|
+
}
|
|
632
|
+
async #verifyProviderEvidence(rawEvidence, snapshot) {
|
|
633
|
+
let verification;
|
|
634
|
+
try {
|
|
635
|
+
verification = await withinTimeout(() => this.#evidenceVerifier.verify(rawEvidence, snapshot), this.#adapterTimeoutMs);
|
|
636
|
+
}
|
|
637
|
+
catch {
|
|
638
|
+
return { ok: false, reason: 'provider_evidence_verification_failed' };
|
|
639
|
+
}
|
|
640
|
+
if (!verification.ok) {
|
|
641
|
+
return {
|
|
642
|
+
ok: false,
|
|
643
|
+
reason: `provider_evidence_verification_failed:${verification.reason}`,
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
const evidence = frozenCopy(verification.evidence);
|
|
647
|
+
if (!providerEvidenceBindingValid(evidence, snapshot)) {
|
|
648
|
+
return { ok: false, reason: 'provider_evidence_binding_mismatch' };
|
|
649
|
+
}
|
|
650
|
+
return { ok: true, evidence };
|
|
651
|
+
}
|
|
652
|
+
async #relateEffect(evidence, snapshot) {
|
|
653
|
+
let relation;
|
|
654
|
+
try {
|
|
655
|
+
relation = frozenCopy(await withinTimeout(() => this.#effectRelator.relate(evidence, snapshot), this.#adapterTimeoutMs));
|
|
656
|
+
}
|
|
657
|
+
catch {
|
|
658
|
+
return { ok: false, reason: 'observed_effect_relation_failed' };
|
|
659
|
+
}
|
|
660
|
+
if (!observedRelationBindingValid(relation, evidence, snapshot)) {
|
|
661
|
+
return { ok: false, reason: 'observed_effect_relation_mismatch' };
|
|
662
|
+
}
|
|
663
|
+
return { ok: true, relation };
|
|
664
|
+
}
|
|
665
|
+
async #processEvidence(rawEvidence, snapshot, authority, record) {
|
|
666
|
+
const verified = await this.#verifyProviderEvidence(rawEvidence, snapshot);
|
|
667
|
+
if (!verified.ok) {
|
|
668
|
+
if (record.state === 'INVOKING') {
|
|
669
|
+
await this.#recoverInvoking(snapshot, authority.ownerToken);
|
|
670
|
+
}
|
|
671
|
+
return reconciliationRequired(snapshot.body.admission_id, verified.reason);
|
|
672
|
+
}
|
|
673
|
+
let providerRecord;
|
|
674
|
+
const currentProvider = record.provider_outcome;
|
|
675
|
+
if (currentProvider && currentProvider.value !== 'INDETERMINATE') {
|
|
676
|
+
if (currentProvider.value !== verified.evidence.outcome
|
|
677
|
+
|| currentProvider.evidence_digest
|
|
678
|
+
!== verified.evidence.evidenceDigest) {
|
|
679
|
+
return reconciliationRequired(snapshot.body.admission_id, 'provider_outcome_conflict');
|
|
680
|
+
}
|
|
681
|
+
providerRecord = record;
|
|
682
|
+
}
|
|
683
|
+
else if (currentProvider?.value === 'INDETERMINATE'
|
|
684
|
+
&& verified.evidence.outcome === 'INDETERMINATE') {
|
|
685
|
+
providerRecord = record;
|
|
686
|
+
}
|
|
687
|
+
else {
|
|
688
|
+
try {
|
|
689
|
+
const recorded = await this.#store.recordProviderOutcome({
|
|
690
|
+
...reference(snapshot),
|
|
691
|
+
expected_revision: record.revision,
|
|
692
|
+
owner_token: authority.ownerToken,
|
|
693
|
+
invocation_token: authority.invocationToken,
|
|
694
|
+
value: verified.evidence.outcome,
|
|
695
|
+
evidence_digest: verified.evidence.evidenceDigest,
|
|
696
|
+
observed_at: verified.evidence.observedAt,
|
|
697
|
+
});
|
|
698
|
+
if (!recorded.ok) {
|
|
699
|
+
return reconciliationRequired(snapshot.body.admission_id, `provider_outcome_unconfirmed:${recorded.reason}`);
|
|
700
|
+
}
|
|
701
|
+
providerRecord = recorded.record;
|
|
702
|
+
}
|
|
703
|
+
catch {
|
|
704
|
+
return reconciliationRequired(snapshot.body.admission_id, 'provider_outcome_unconfirmed');
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
const related = await this.#relateEffect(verified.evidence, snapshot);
|
|
708
|
+
if (!related.ok) {
|
|
709
|
+
await this.#recordIndeterminateEffect(snapshot, authority, providerRecord.revision);
|
|
710
|
+
return reconciliationRequired(snapshot.body.admission_id, related.reason);
|
|
711
|
+
}
|
|
712
|
+
let effectRecord;
|
|
713
|
+
const currentEffect = providerRecord.effect_relation;
|
|
714
|
+
if (currentEffect && currentEffect.value !== 'INDETERMINATE') {
|
|
715
|
+
if (currentEffect.value !== related.relation.relation
|
|
716
|
+
|| currentEffect.evidence_digest !== related.relation.evidenceDigest) {
|
|
717
|
+
return reconciliationRequired(snapshot.body.admission_id, 'effect_relation_conflict');
|
|
718
|
+
}
|
|
719
|
+
effectRecord = providerRecord;
|
|
720
|
+
}
|
|
721
|
+
else if (currentEffect?.value === 'INDETERMINATE'
|
|
722
|
+
&& related.relation.relation === 'INDETERMINATE') {
|
|
723
|
+
effectRecord = providerRecord;
|
|
724
|
+
}
|
|
725
|
+
else {
|
|
726
|
+
try {
|
|
727
|
+
const recorded = await this.#store.recordEffectRelation({
|
|
728
|
+
...reference(snapshot),
|
|
729
|
+
expected_revision: providerRecord.revision,
|
|
730
|
+
owner_token: authority.ownerToken,
|
|
731
|
+
invocation_token: authority.invocationToken,
|
|
732
|
+
value: related.relation.relation,
|
|
733
|
+
evidence_digest: related.relation.evidenceDigest,
|
|
734
|
+
observed_at: related.relation.observedAt,
|
|
735
|
+
});
|
|
736
|
+
if (!recorded.ok) {
|
|
737
|
+
return reconciliationRequired(snapshot.body.admission_id, `effect_relation_unconfirmed:${recorded.reason}`);
|
|
738
|
+
}
|
|
739
|
+
effectRecord = recorded.record;
|
|
740
|
+
}
|
|
741
|
+
catch {
|
|
742
|
+
return reconciliationRequired(snapshot.body.admission_id, 'effect_relation_unconfirmed');
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
if (verified.evidence.outcome === 'INDETERMINATE'
|
|
746
|
+
|| related.relation.relation === 'INDETERMINATE'
|
|
747
|
+
|| effectRecord.provider_outcome?.value === 'INDETERMINATE'
|
|
748
|
+
|| effectRecord.effect_relation?.value === 'INDETERMINATE') {
|
|
749
|
+
return reconciliationRequired(snapshot.body.admission_id, 'provider_or_effect_indeterminate');
|
|
750
|
+
}
|
|
751
|
+
await this.#authorityCustody.delete({
|
|
752
|
+
tenantId: snapshot.body.tenant_id,
|
|
753
|
+
admissionId: snapshot.body.admission_id,
|
|
754
|
+
});
|
|
755
|
+
return deepFreeze({
|
|
756
|
+
status: verified.evidence.outcome === 'COMMITTED'
|
|
757
|
+
? 'committed'
|
|
758
|
+
: 'not_committed',
|
|
759
|
+
admissionId: snapshot.body.admission_id,
|
|
760
|
+
evidence: verified.evidence,
|
|
761
|
+
relation: related.relation,
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
async execute(input) {
|
|
765
|
+
const checked = canonicalSnapshot(input?.snapshot);
|
|
766
|
+
const decision = checked.ok
|
|
767
|
+
? composeCanonical(checked.snapshot, input?.qualification)
|
|
768
|
+
: emptyDecision(['admission_snapshot_invalid']);
|
|
769
|
+
if (this.mode === 'shadow')
|
|
770
|
+
return this.#shadow(input, decision);
|
|
771
|
+
if (!checked.ok || !decision.allow) {
|
|
772
|
+
return deepFreeze({
|
|
773
|
+
status: 'refused',
|
|
774
|
+
reason: decision.reasons[0] ?? 'qualification_refused',
|
|
775
|
+
decision,
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
const snapshot = checked.snapshot;
|
|
779
|
+
let reserved;
|
|
780
|
+
try {
|
|
781
|
+
reserved = await this.#store.reserve(snapshot);
|
|
782
|
+
}
|
|
783
|
+
catch {
|
|
784
|
+
return deepFreeze({
|
|
785
|
+
status: 'refused',
|
|
786
|
+
reason: 'admission_reserve_failed',
|
|
787
|
+
decision,
|
|
788
|
+
});
|
|
789
|
+
}
|
|
790
|
+
if (!reserved.ok) {
|
|
791
|
+
return this.#existingAdmissionResult(snapshot, reserved.reason, decision);
|
|
792
|
+
}
|
|
793
|
+
const reservedSnapshot = canonicalSnapshot(reserved.snapshot);
|
|
794
|
+
if (!reservedSnapshot.ok || !deeplyFrozen(reserved.snapshot)
|
|
795
|
+
|| reserved.snapshot.snapshot_digest !== snapshot.snapshot_digest
|
|
796
|
+
|| reserved.record.snapshot_digest !== snapshot.snapshot_digest) {
|
|
797
|
+
try {
|
|
798
|
+
await this.#store.release({
|
|
799
|
+
...reference(snapshot),
|
|
800
|
+
expected_revision: reserved.record.revision,
|
|
801
|
+
owner_token: reserved.owner_token,
|
|
802
|
+
}, 'reserve_snapshot_mismatch');
|
|
803
|
+
}
|
|
804
|
+
catch {
|
|
805
|
+
// No provider entry occurred; the reservation remains closed.
|
|
806
|
+
}
|
|
807
|
+
return deepFreeze({
|
|
808
|
+
status: 'refused',
|
|
809
|
+
reason: 'reserve_snapshot_mismatch',
|
|
810
|
+
decision,
|
|
811
|
+
});
|
|
812
|
+
}
|
|
813
|
+
// Reread the candidate, qualification status, AEB, AEC, local policy and
|
|
814
|
+
// protected-request binding from authoritative sources immediately before
|
|
815
|
+
// the store's transactional currentness check consumes execution rights.
|
|
816
|
+
// The caller-supplied decision is never reused across this boundary.
|
|
817
|
+
let refreshedBundle;
|
|
818
|
+
try {
|
|
819
|
+
refreshedBundle = await withinTimeout(() => this.#remeasurer.remeasure(reserved.snapshot), this.#adapterTimeoutMs);
|
|
820
|
+
}
|
|
821
|
+
catch {
|
|
822
|
+
try {
|
|
823
|
+
await this.#store.release({
|
|
824
|
+
...reference(reserved.snapshot),
|
|
825
|
+
expected_revision: reserved.record.revision,
|
|
826
|
+
owner_token: reserved.owner_token,
|
|
827
|
+
}, 'invocation_remeasurement_failed');
|
|
828
|
+
}
|
|
829
|
+
catch {
|
|
830
|
+
// No execution right was consumed and no provider entry occurred.
|
|
831
|
+
}
|
|
832
|
+
return deepFreeze({
|
|
833
|
+
status: 'refused',
|
|
834
|
+
reason: 'invocation_remeasurement_failed',
|
|
835
|
+
decision,
|
|
836
|
+
});
|
|
837
|
+
}
|
|
838
|
+
const refreshedDecision = composeCanonical(reserved.snapshot, frozenCopy(refreshedBundle));
|
|
839
|
+
if (!refreshedDecision.allow) {
|
|
840
|
+
try {
|
|
841
|
+
await this.#store.release({
|
|
842
|
+
...reference(reserved.snapshot),
|
|
843
|
+
expected_revision: reserved.record.revision,
|
|
844
|
+
owner_token: reserved.owner_token,
|
|
845
|
+
}, 'invocation_remeasurement_refused');
|
|
846
|
+
}
|
|
847
|
+
catch {
|
|
848
|
+
// No execution right was consumed and no provider entry occurred.
|
|
849
|
+
}
|
|
850
|
+
return deepFreeze({
|
|
851
|
+
status: 'refused',
|
|
852
|
+
reason: refreshedDecision.reasons[0]
|
|
853
|
+
?? 'invocation_remeasurement_refused',
|
|
854
|
+
decision: refreshedDecision,
|
|
855
|
+
});
|
|
856
|
+
}
|
|
857
|
+
let begun;
|
|
858
|
+
try {
|
|
859
|
+
begun = await this.#store.beginInvocation({
|
|
860
|
+
...reference(reserved.snapshot),
|
|
861
|
+
expected_revision: reserved.record.revision,
|
|
862
|
+
owner_token: reserved.owner_token,
|
|
863
|
+
});
|
|
864
|
+
}
|
|
865
|
+
catch {
|
|
866
|
+
return this.#beginAmbiguity(reserved.snapshot, reserved.owner_token);
|
|
867
|
+
}
|
|
868
|
+
if (!begun.ok) {
|
|
869
|
+
if (begun.reason === 'state_conflict'
|
|
870
|
+
|| begun.reason === 'revision_conflict') {
|
|
871
|
+
return this.#beginAmbiguity(reserved.snapshot, reserved.owner_token);
|
|
872
|
+
}
|
|
873
|
+
return deepFreeze({
|
|
874
|
+
status: 'refused',
|
|
875
|
+
reason: begun.reason,
|
|
876
|
+
decision,
|
|
877
|
+
});
|
|
878
|
+
}
|
|
879
|
+
const begunSnapshot = canonicalSnapshot(begun.snapshot);
|
|
880
|
+
if (!begunSnapshot.ok || !deeplyFrozen(begun.snapshot)
|
|
881
|
+
|| begun.snapshot.snapshot_digest !== reserved.snapshot.snapshot_digest
|
|
882
|
+
|| begun.record.snapshot_digest !== begun.snapshot.snapshot_digest
|
|
883
|
+
|| begun.record.state !== 'INVOKING'
|
|
884
|
+
|| begun.record.execution_right !== 'CONSUMED'
|
|
885
|
+
|| begun.record.resources.some((resource) => resource.state !== 'CONSUMED')) {
|
|
886
|
+
await this.#recoverInvoking(begun.snapshot, reserved.owner_token);
|
|
887
|
+
return reconciliationRequired(reserved.snapshot.body.admission_id, 'begin_invocation_snapshot_mismatch');
|
|
888
|
+
}
|
|
889
|
+
const authority = {
|
|
890
|
+
ownerToken: reserved.owner_token,
|
|
891
|
+
invocationToken: begun.invocation_token,
|
|
892
|
+
snapshotDigest: begun.snapshot.snapshot_digest,
|
|
893
|
+
};
|
|
894
|
+
try {
|
|
895
|
+
await this.#saveAuthority(begun.snapshot, authority);
|
|
896
|
+
}
|
|
897
|
+
catch {
|
|
898
|
+
await this.#recoverInvoking(begun.snapshot, reserved.owner_token);
|
|
899
|
+
return reconciliationRequired(begun.snapshot.body.admission_id, 'invocation_authority_custody_failed');
|
|
900
|
+
}
|
|
901
|
+
const adapterInput = protectedInvocation(begun.snapshot, begun.invocation_token);
|
|
902
|
+
let rawEvidence;
|
|
903
|
+
try {
|
|
904
|
+
rawEvidence = await withinTimeout(() => this.#adapter.invoke(adapterInput), this.#adapterTimeoutMs);
|
|
905
|
+
}
|
|
906
|
+
catch (error) {
|
|
907
|
+
await this.#recoverInvoking(begun.snapshot, authority.ownerToken);
|
|
908
|
+
return reconciliationRequired(begun.snapshot.body.admission_id, error instanceof AdapterTimeoutError
|
|
909
|
+
? 'provider_invocation_timed_out'
|
|
910
|
+
: 'provider_invocation_outcome_unknown');
|
|
911
|
+
}
|
|
912
|
+
return this.#processEvidence(rawEvidence, begun.snapshot, authority, begun.record);
|
|
913
|
+
}
|
|
914
|
+
/** Evidence-only reconciliation. This method has no mutation-adapter path. */
|
|
915
|
+
async reconcile(input) {
|
|
916
|
+
if (this.mode !== 'enforce') {
|
|
917
|
+
return deepFreeze({
|
|
918
|
+
status: 'refused',
|
|
919
|
+
reason: 'reconciliation_unavailable_in_shadow',
|
|
920
|
+
});
|
|
921
|
+
}
|
|
922
|
+
if (!input || !validString(input.tenant_id)
|
|
923
|
+
|| !validString(input.admission_id)) {
|
|
924
|
+
return deepFreeze({ status: 'refused', reason: 'admission_reference_invalid' });
|
|
925
|
+
}
|
|
926
|
+
let record;
|
|
927
|
+
try {
|
|
928
|
+
record = await this.#store.read(input);
|
|
929
|
+
}
|
|
930
|
+
catch {
|
|
931
|
+
return reconciliationRequired(input.admission_id, 'admission_read_ambiguous');
|
|
932
|
+
}
|
|
933
|
+
if (!record) {
|
|
934
|
+
return deepFreeze({ status: 'refused', reason: 'admission_not_found' });
|
|
935
|
+
}
|
|
936
|
+
if (record.execution_right !== 'CONSUMED') {
|
|
937
|
+
return deepFreeze({
|
|
938
|
+
status: 'refused',
|
|
939
|
+
reason: 'reconciliation_not_required',
|
|
940
|
+
});
|
|
941
|
+
}
|
|
942
|
+
let snapshot;
|
|
943
|
+
try {
|
|
944
|
+
snapshot = await this.#store.readSnapshot(record.snapshot_digest);
|
|
945
|
+
}
|
|
946
|
+
catch {
|
|
947
|
+
return reconciliationRequired(input.admission_id, 'admission_snapshot_read_ambiguous');
|
|
948
|
+
}
|
|
949
|
+
const checked = canonicalSnapshot(snapshot);
|
|
950
|
+
if (!snapshot || !checked.ok || !deeplyFrozen(snapshot)
|
|
951
|
+
|| snapshot.snapshot_digest !== record.snapshot_digest) {
|
|
952
|
+
return reconciliationRequired(input.admission_id, 'admission_snapshot_read_ambiguous');
|
|
953
|
+
}
|
|
954
|
+
let authority;
|
|
955
|
+
try {
|
|
956
|
+
authority = await this.#getAuthority(input.tenant_id, input.admission_id);
|
|
957
|
+
}
|
|
958
|
+
catch {
|
|
959
|
+
return reconciliationRequired(input.admission_id, 'reconciliation_authority_unavailable');
|
|
960
|
+
}
|
|
961
|
+
if (!authority || authority.snapshotDigest !== snapshot.snapshot_digest) {
|
|
962
|
+
return reconciliationRequired(input.admission_id, 'reconciliation_authority_unavailable');
|
|
963
|
+
}
|
|
964
|
+
if (record.state === 'INVOKING') {
|
|
965
|
+
const recovered = await this.#recoverInvoking(snapshot, authority.ownerToken);
|
|
966
|
+
if (!recovered) {
|
|
967
|
+
return reconciliationRequired(input.admission_id, 'recovery_unconfirmed');
|
|
968
|
+
}
|
|
969
|
+
authority = recovered;
|
|
970
|
+
try {
|
|
971
|
+
record = await this.#store.read(input);
|
|
972
|
+
}
|
|
973
|
+
catch {
|
|
974
|
+
return reconciliationRequired(input.admission_id, 'admission_read_ambiguous');
|
|
975
|
+
}
|
|
976
|
+
if (!record) {
|
|
977
|
+
return reconciliationRequired(input.admission_id, 'admission_read_ambiguous');
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
const providerUnresolved = !record.provider_outcome
|
|
981
|
+
|| record.provider_outcome.value === 'INDETERMINATE';
|
|
982
|
+
const effectUnresolved = !record.effect_relation
|
|
983
|
+
|| record.effect_relation.value === 'INDETERMINATE';
|
|
984
|
+
if (!providerUnresolved && !effectUnresolved) {
|
|
985
|
+
return deepFreeze({
|
|
986
|
+
status: 'refused',
|
|
987
|
+
reason: 'reconciliation_not_required',
|
|
988
|
+
});
|
|
989
|
+
}
|
|
990
|
+
const base = protectedInvocation(snapshot, authority.invocationToken);
|
|
991
|
+
let rawEvidence;
|
|
992
|
+
try {
|
|
993
|
+
rawEvidence = await withinTimeout(() => this.#adapter.reconcile(deepFreeze({
|
|
994
|
+
...base,
|
|
995
|
+
reconciliationOnly: true,
|
|
996
|
+
})), this.#adapterTimeoutMs);
|
|
997
|
+
}
|
|
998
|
+
catch (error) {
|
|
999
|
+
return reconciliationRequired(input.admission_id, error instanceof AdapterTimeoutError
|
|
1000
|
+
? 'provider_reconciliation_timed_out'
|
|
1001
|
+
: 'provider_reconciliation_failed');
|
|
1002
|
+
}
|
|
1003
|
+
return this.#processEvidence(rawEvidence, snapshot, authority, record);
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
export default {
|
|
1007
|
+
GATE_QUALIFICATION_V2_VERSION,
|
|
1008
|
+
composeQualificationDecisionV2,
|
|
1009
|
+
GateQualificationV2,
|
|
1010
|
+
};
|
|
1011
|
+
//# sourceMappingURL=gate-qualification-v2.js.map
|