@aikdna/kdna-core 0.17.0 → 0.19.0
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 +227 -64
- package/README.md +114 -77
- package/package.json +8 -2
- package/schema/agent-host-capabilities.schema.json +62 -0
- package/schema/agent-host-receipt.schema.json +371 -0
- package/schema/agent-host-request.schema.json +223 -0
- package/schema/{bundle-profile-v1.schema.json → bundle-profile.schema.json} +9 -5
- package/schema/checksums.schema.json +42 -3
- package/schema/consumption-plan.schema.json +428 -0
- package/schema/digest-evidence.schema.json +212 -0
- package/schema/{kdna-envelope-external-grant-v1.schema.json → external-grant-envelope.schema.json} +17 -4
- package/schema/{kdna-key-grant-v1.schema.json → external-key-grant.schema.json} +39 -7
- package/schema/judgment-trace.schema.json +1251 -0
- package/schema/load-plan.schema.json +7 -5
- package/schema/manifest.schema.json +70 -10
- package/schema/{payload-profile-v1.schema.json → payload-profile.schema.json} +30 -6
- package/schema/runtime-capsule.schema.json +116 -0
- package/src/asset-reader.js +106 -120
- package/src/{v1 → container}/index.js +566 -209
- package/src/container/index.mjs +20 -0
- package/src/container-dispatcher.js +10 -7
- package/src/crypto-profile.js +37 -107
- package/src/external-key-grant.js +41 -15
- package/src/index.js +11 -4
- package/src/index.mjs +143 -24
- package/src/loader-compatibility.js +59 -0
- package/src/loader.js +1 -1
- package/src/public-api.js +59 -163
- package/src/remote-runtime.d.ts +13 -0
- package/src/remote-runtime.js +27 -0
- package/src/remote-runtime.mjs +3 -0
- package/src/runtime-api.js +9 -10
- package/src/runtime-capsule.js +529 -0
- package/src/runtime-contract.js +1882 -0
- package/src/types.d.ts +1127 -136
- package/src/workpack-pure.js +1 -1
- package/src/lint-pure.js +0 -430
- package/src/v1/index.mjs +0 -20
|
@@ -0,0 +1,1882 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('node:crypto');
|
|
4
|
+
|
|
5
|
+
const JsonSchema2020 = require('ajv/dist/2020');
|
|
6
|
+
const addFormats = require('ajv-formats');
|
|
7
|
+
|
|
8
|
+
const { canonicalizeJcs, computeCapsuleDeliveryDigest } = require('./runtime-capsule');
|
|
9
|
+
const digestEvidenceSchema = require('../schema/digest-evidence.schema.json');
|
|
10
|
+
const runtimeCapsuleSchema = require('../schema/runtime-capsule.schema.json');
|
|
11
|
+
const planSchema = require('../schema/consumption-plan.schema.json');
|
|
12
|
+
const capabilitiesSchema = require('../schema/agent-host-capabilities.schema.json');
|
|
13
|
+
const requestSchema = require('../schema/agent-host-request.schema.json');
|
|
14
|
+
const receiptSchema = require('../schema/agent-host-receipt.schema.json');
|
|
15
|
+
const traceSchema = require('../schema/judgment-trace.schema.json');
|
|
16
|
+
|
|
17
|
+
const PLAN_DIGEST_PROFILE = 'kdna.canonicalization.consumption-plan-jcs';
|
|
18
|
+
const PLAN_DIGEST_PROFILE_VERSION = '0.1.0';
|
|
19
|
+
const CAPSULE_DELIVERY_PROFILE = 'kdna.canonicalization.runtime-capsule-jcs';
|
|
20
|
+
const CAPSULE_DELIVERY_PROFILE_VERSION = '0.1.0';
|
|
21
|
+
const CAPSULE_DIGEST_PROFILE = 'kdna.digest-evidence';
|
|
22
|
+
const HOST_PROTOCOL = 'kdna.agent-host';
|
|
23
|
+
const HOST_PROTOCOL_VERSION = '0.1.0';
|
|
24
|
+
const CAPSULE_CONTRACT_VERSION = '0.1.0';
|
|
25
|
+
const DEFAULT_CORE_CAPSULE_CONTRACT_VERSIONS = Object.freeze([
|
|
26
|
+
CAPSULE_CONTRACT_VERSION,
|
|
27
|
+
]);
|
|
28
|
+
const JSON_MAX_BYTES = 2 * 1024 * 1024;
|
|
29
|
+
const JSON_MAX_DEPTH = 64;
|
|
30
|
+
const PRE_HOST_INTERNAL_REQUEST_ID = 'host_000000000000000000000000';
|
|
31
|
+
const PRE_HOST_BUDGET_ERROR = Object.freeze({
|
|
32
|
+
code: 'KDNA_HOST_BUDGET_LIMIT_EXCEEDED',
|
|
33
|
+
message: 'Pre-Host projection or task budget exceeded.',
|
|
34
|
+
phase: 'budget',
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
let validators;
|
|
38
|
+
|
|
39
|
+
class KDNARuntimeContractError extends Error {
|
|
40
|
+
constructor(code, message, details) {
|
|
41
|
+
super(message);
|
|
42
|
+
this.name = 'KDNARuntimeContractError';
|
|
43
|
+
this.code = code;
|
|
44
|
+
if (details !== undefined) this.details = details;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function schemaValidators() {
|
|
49
|
+
if (validators) return validators;
|
|
50
|
+
const ajv = new JsonSchema2020({ allErrors: true, strict: false });
|
|
51
|
+
addFormats(ajv);
|
|
52
|
+
for (const schema of [
|
|
53
|
+
digestEvidenceSchema,
|
|
54
|
+
runtimeCapsuleSchema,
|
|
55
|
+
planSchema,
|
|
56
|
+
capabilitiesSchema,
|
|
57
|
+
requestSchema,
|
|
58
|
+
receiptSchema,
|
|
59
|
+
traceSchema,
|
|
60
|
+
]) {
|
|
61
|
+
ajv.addSchema(schema);
|
|
62
|
+
}
|
|
63
|
+
validators = {
|
|
64
|
+
plan: ajv.getSchema(planSchema.$id),
|
|
65
|
+
capabilities: ajv.getSchema(capabilitiesSchema.$id),
|
|
66
|
+
request: ajv.getSchema(requestSchema.$id),
|
|
67
|
+
receipt: ajv.getSchema(receiptSchema.$id),
|
|
68
|
+
trace: ajv.getSchema(traceSchema.$id),
|
|
69
|
+
};
|
|
70
|
+
return validators;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function ok(value) {
|
|
74
|
+
return { valid: true, code: null, errors: [], ...(value === undefined ? {} : { value }) };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function invalid(code, message, details) {
|
|
78
|
+
return {
|
|
79
|
+
valid: false,
|
|
80
|
+
code,
|
|
81
|
+
errors: [{ code, message, ...(details === undefined ? {} : { details }) }],
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function throwInvalid(result) {
|
|
86
|
+
if (result.valid) return;
|
|
87
|
+
const first = result.errors[0] || {};
|
|
88
|
+
throw new KDNARuntimeContractError(
|
|
89
|
+
result.code || 'KDNA_EXECUTION_CONTRACT_INVALID',
|
|
90
|
+
first.message || 'KDNA execution contract is invalid.',
|
|
91
|
+
first.details,
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function clone(value) {
|
|
96
|
+
return globalThis.structuredClone(value);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function equalJson(left, right) {
|
|
100
|
+
try {
|
|
101
|
+
return canonicalizeJcs(left) === canonicalizeJcs(right);
|
|
102
|
+
} catch {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function digestJcs(value) {
|
|
108
|
+
return `sha256:${crypto
|
|
109
|
+
.createHash('sha256')
|
|
110
|
+
.update(Buffer.from(canonicalizeJcs(value), 'utf8'))
|
|
111
|
+
.digest('hex')}`;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function jcsCharacterCount(value) {
|
|
115
|
+
return [...canonicalizeJcs(value)].length;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function snapshotJsonValue(value) {
|
|
119
|
+
return parseRuntimeContractJson(canonicalizeJcs(value));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function validateSchema(kind, value) {
|
|
123
|
+
const validator = schemaValidators()[kind];
|
|
124
|
+
try {
|
|
125
|
+
const snapshot = snapshotJsonValue(value);
|
|
126
|
+
if (validator(snapshot)) return ok(snapshot);
|
|
127
|
+
const details = clone(validator.errors || []);
|
|
128
|
+
if (
|
|
129
|
+
kind === 'plan' &&
|
|
130
|
+
details.some(
|
|
131
|
+
(error) =>
|
|
132
|
+
error.keyword === 'const' &&
|
|
133
|
+
error.instancePath === '/projection_request/require_packaged_asset',
|
|
134
|
+
)
|
|
135
|
+
) {
|
|
136
|
+
return invalid(
|
|
137
|
+
'KDNA_ASSET_FILE_REQUIRED',
|
|
138
|
+
'ConsumptionPlan requires a packaged .kdna Runtime asset.',
|
|
139
|
+
details,
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
if (
|
|
143
|
+
kind === 'trace' &&
|
|
144
|
+
snapshot &&
|
|
145
|
+
snapshot.overall_status === 'execution_completed' &&
|
|
146
|
+
snapshot.budget?.comparison?.overall === 'exceeded'
|
|
147
|
+
) {
|
|
148
|
+
return invalid(
|
|
149
|
+
'KDNA_TRACE_BUDGET_LIMIT_VIOLATION',
|
|
150
|
+
'A completed JudgmentTrace cannot cross an enforced budget limit.',
|
|
151
|
+
details,
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
return invalid('SCHEMA_INVALID', `${kind} does not match its packaged JSON Schema.`, details);
|
|
155
|
+
} catch (error) {
|
|
156
|
+
return invalid('KDNA_INPUT_INVALID', `${kind} is not a safe JSON value.`, {
|
|
157
|
+
cause: error && typeof error.message === 'string' ? error.message : String(error),
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function snapshotContext(context, keys) {
|
|
163
|
+
try {
|
|
164
|
+
if (
|
|
165
|
+
context === null ||
|
|
166
|
+
typeof context !== 'object' ||
|
|
167
|
+
Array.isArray(context) ||
|
|
168
|
+
Object.getPrototypeOf(context) !== Object.prototype
|
|
169
|
+
) {
|
|
170
|
+
return invalid(
|
|
171
|
+
'KDNA_VALIDATION_CONTEXT_INVALID',
|
|
172
|
+
'Validation context must be a plain object.',
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
const descriptors = Object.getOwnPropertyDescriptors(context);
|
|
176
|
+
if (
|
|
177
|
+
Reflect.ownKeys(descriptors).some(
|
|
178
|
+
(key) =>
|
|
179
|
+
typeof key !== 'string' ||
|
|
180
|
+
!descriptors[key].enumerable ||
|
|
181
|
+
!Object.prototype.hasOwnProperty.call(descriptors[key], 'value'),
|
|
182
|
+
)
|
|
183
|
+
) {
|
|
184
|
+
return invalid(
|
|
185
|
+
'KDNA_VALIDATION_CONTEXT_INVALID',
|
|
186
|
+
'Validation context must contain only enumerable data properties.',
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const snapshot = {};
|
|
191
|
+
for (const key of keys) {
|
|
192
|
+
const descriptor = descriptors[key];
|
|
193
|
+
if (!descriptor) {
|
|
194
|
+
return invalid(
|
|
195
|
+
'KDNA_VALIDATION_CONTEXT_INVALID',
|
|
196
|
+
`Validation context must explicitly provide ${key}.`,
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
snapshot[key] = snapshotJsonValue(descriptor.value);
|
|
200
|
+
}
|
|
201
|
+
return ok(snapshot);
|
|
202
|
+
} catch (error) {
|
|
203
|
+
return invalid(
|
|
204
|
+
'KDNA_VALIDATION_CONTEXT_INVALID',
|
|
205
|
+
'Validation context could not be safely snapshotted.',
|
|
206
|
+
{ cause: error && typeof error.message === 'string' ? error.message : String(error) },
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function snapshotPublicInput(input, requiredKeys, optionalKeys = []) {
|
|
212
|
+
try {
|
|
213
|
+
if (
|
|
214
|
+
input === null ||
|
|
215
|
+
typeof input !== 'object' ||
|
|
216
|
+
Array.isArray(input) ||
|
|
217
|
+
Object.getPrototypeOf(input) !== Object.prototype
|
|
218
|
+
) {
|
|
219
|
+
return invalid('KDNA_INPUT_INVALID', 'Builder input must be a plain object.');
|
|
220
|
+
}
|
|
221
|
+
const descriptors = Object.getOwnPropertyDescriptors(input);
|
|
222
|
+
const allowedKeys = new Set([...requiredKeys, ...optionalKeys]);
|
|
223
|
+
if (
|
|
224
|
+
Reflect.ownKeys(descriptors).some(
|
|
225
|
+
(key) =>
|
|
226
|
+
typeof key !== 'string' ||
|
|
227
|
+
!allowedKeys.has(key) ||
|
|
228
|
+
!descriptors[key].enumerable ||
|
|
229
|
+
!Object.prototype.hasOwnProperty.call(descriptors[key], 'value'),
|
|
230
|
+
)
|
|
231
|
+
) {
|
|
232
|
+
return invalid(
|
|
233
|
+
'KDNA_INPUT_INVALID',
|
|
234
|
+
'Builder input must contain only declared enumerable data properties.',
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
const snapshot = {};
|
|
238
|
+
for (const key of requiredKeys) {
|
|
239
|
+
if (!descriptors[key]) {
|
|
240
|
+
return invalid('KDNA_INPUT_INVALID', `Builder input must explicitly provide ${key}.`);
|
|
241
|
+
}
|
|
242
|
+
snapshot[key] = snapshotJsonValue(descriptors[key].value);
|
|
243
|
+
}
|
|
244
|
+
for (const key of optionalKeys) {
|
|
245
|
+
if (descriptors[key]) snapshot[key] = snapshotJsonValue(descriptors[key].value);
|
|
246
|
+
}
|
|
247
|
+
return ok(snapshot);
|
|
248
|
+
} catch (error) {
|
|
249
|
+
return invalid('KDNA_INPUT_INVALID', 'Builder input could not be safely snapshotted.', {
|
|
250
|
+
cause: error && typeof error.message === 'string' ? error.message : String(error),
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Parse an execution-contract JSON value without JSON.parse semantics.
|
|
257
|
+
* Duplicate member names are rejected after JSON escape decoding, so
|
|
258
|
+
* {"a": 1, "\\u0061": 2} is also rejected.
|
|
259
|
+
*/
|
|
260
|
+
function parseRuntimeContractJson(input, options = {}) {
|
|
261
|
+
if (
|
|
262
|
+
options === null ||
|
|
263
|
+
typeof options !== 'object' ||
|
|
264
|
+
Array.isArray(options) ||
|
|
265
|
+
Object.getPrototypeOf(options) !== Object.prototype
|
|
266
|
+
) {
|
|
267
|
+
throw new KDNARuntimeContractError(
|
|
268
|
+
'KDNA_JSON_OPTIONS_INVALID',
|
|
269
|
+
'Execution-contract JSON parser options must be a plain object.',
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
const descriptors = Object.getOwnPropertyDescriptors(options);
|
|
273
|
+
const optionKeys = Reflect.ownKeys(descriptors);
|
|
274
|
+
if (
|
|
275
|
+
optionKeys.some(
|
|
276
|
+
(key) =>
|
|
277
|
+
typeof key !== 'string' ||
|
|
278
|
+
!['maxBytes', 'maxDepth'].includes(key) ||
|
|
279
|
+
!Object.prototype.hasOwnProperty.call(descriptors[key], 'value'),
|
|
280
|
+
)
|
|
281
|
+
) {
|
|
282
|
+
throw new KDNARuntimeContractError(
|
|
283
|
+
'KDNA_JSON_OPTIONS_INVALID',
|
|
284
|
+
'Execution-contract JSON parser options contain an unknown or accessor property.',
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
const maxBytes = options.maxBytes ?? JSON_MAX_BYTES;
|
|
288
|
+
const maxDepth = options.maxDepth ?? JSON_MAX_DEPTH;
|
|
289
|
+
if (
|
|
290
|
+
!Number.isSafeInteger(maxBytes) ||
|
|
291
|
+
maxBytes < 1 ||
|
|
292
|
+
maxBytes > JSON_MAX_BYTES ||
|
|
293
|
+
!Number.isSafeInteger(maxDepth) ||
|
|
294
|
+
maxDepth < 1 ||
|
|
295
|
+
maxDepth > JSON_MAX_DEPTH
|
|
296
|
+
) {
|
|
297
|
+
throw new KDNARuntimeContractError(
|
|
298
|
+
'KDNA_JSON_OPTIONS_INVALID',
|
|
299
|
+
`maxBytes and maxDepth must be positive safe integers no greater than ${JSON_MAX_BYTES} and ${JSON_MAX_DEPTH}.`,
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
let text;
|
|
303
|
+
if (typeof input === 'string') {
|
|
304
|
+
text = input;
|
|
305
|
+
} else if (Buffer.isBuffer(input) || input instanceof Uint8Array) {
|
|
306
|
+
if (input.byteLength > maxBytes) {
|
|
307
|
+
throw new KDNARuntimeContractError(
|
|
308
|
+
'KDNA_JSON_LIMIT_EXCEEDED',
|
|
309
|
+
'Execution-contract JSON exceeds the configured byte limit.',
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
if (input.byteLength >= 3 && input[0] === 0xef && input[1] === 0xbb && input[2] === 0xbf) {
|
|
313
|
+
throw new KDNARuntimeContractError(
|
|
314
|
+
'KDNA_JSON_BOM_FORBIDDEN',
|
|
315
|
+
'Execution-contract JSON must not begin with a UTF-8 BOM.',
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
try {
|
|
319
|
+
text = new TextDecoder('utf-8', { fatal: true }).decode(input);
|
|
320
|
+
} catch {
|
|
321
|
+
throw new KDNARuntimeContractError(
|
|
322
|
+
'KDNA_JSON_INVALID_UNICODE',
|
|
323
|
+
'Execution-contract JSON is not valid UTF-8.',
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
} else {
|
|
327
|
+
throw new KDNARuntimeContractError(
|
|
328
|
+
'KDNA_JSON_INPUT_INVALID',
|
|
329
|
+
'Execution-contract JSON input must be a string, Buffer, or Uint8Array.',
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (text.charCodeAt(0) === 0xfeff) {
|
|
334
|
+
throw new KDNARuntimeContractError(
|
|
335
|
+
'KDNA_JSON_BOM_FORBIDDEN',
|
|
336
|
+
'Execution-contract JSON must not begin with a BOM.',
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (Buffer.byteLength(text, 'utf8') > maxBytes) {
|
|
341
|
+
throw new KDNARuntimeContractError(
|
|
342
|
+
'KDNA_JSON_LIMIT_EXCEEDED',
|
|
343
|
+
'Execution-contract JSON exceeds the configured byte limit.',
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const parser = new StrictJsonParser(text, maxDepth);
|
|
348
|
+
return parser.parse();
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
class StrictJsonParser {
|
|
352
|
+
constructor(text, maxDepth) {
|
|
353
|
+
this.text = text;
|
|
354
|
+
this.maxDepth = maxDepth;
|
|
355
|
+
this.index = 0;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
parse() {
|
|
359
|
+
this.skipWhitespace();
|
|
360
|
+
const value = this.parseValue(0);
|
|
361
|
+
this.skipWhitespace();
|
|
362
|
+
if (this.index !== this.text.length) this.syntax('Unexpected trailing JSON content.');
|
|
363
|
+
return value;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
syntax(message) {
|
|
367
|
+
throw new KDNARuntimeContractError('KDNA_JSON_SYNTAX_INVALID', message, {
|
|
368
|
+
code_unit_offset: this.index,
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
skipWhitespace() {
|
|
373
|
+
while (
|
|
374
|
+
this.index < this.text.length &&
|
|
375
|
+
(this.text[this.index] === ' ' ||
|
|
376
|
+
this.text[this.index] === '\n' ||
|
|
377
|
+
this.text[this.index] === '\r' ||
|
|
378
|
+
this.text[this.index] === '\t')
|
|
379
|
+
) {
|
|
380
|
+
this.index += 1;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
parseValue(depth) {
|
|
385
|
+
this.skipWhitespace();
|
|
386
|
+
const char = this.text[this.index];
|
|
387
|
+
if (char === '{' || char === '[') {
|
|
388
|
+
if (depth >= this.maxDepth) {
|
|
389
|
+
throw new KDNARuntimeContractError(
|
|
390
|
+
'KDNA_JSON_LIMIT_EXCEEDED',
|
|
391
|
+
'Execution-contract JSON exceeds the configured nesting limit.',
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
return char === '{' ? this.parseObject(depth + 1) : this.parseArray(depth + 1);
|
|
395
|
+
}
|
|
396
|
+
if (char === '"') return this.parseString();
|
|
397
|
+
if (char === '-' || (char >= '0' && char <= '9')) return this.parseNumber();
|
|
398
|
+
if (this.text.startsWith('true', this.index)) {
|
|
399
|
+
this.index += 4;
|
|
400
|
+
return true;
|
|
401
|
+
}
|
|
402
|
+
if (this.text.startsWith('false', this.index)) {
|
|
403
|
+
this.index += 5;
|
|
404
|
+
return false;
|
|
405
|
+
}
|
|
406
|
+
if (this.text.startsWith('null', this.index)) {
|
|
407
|
+
this.index += 4;
|
|
408
|
+
return null;
|
|
409
|
+
}
|
|
410
|
+
this.syntax('Expected a JSON value.');
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
parseObject(depth) {
|
|
414
|
+
const output = {};
|
|
415
|
+
const keys = new Set();
|
|
416
|
+
this.index += 1;
|
|
417
|
+
this.skipWhitespace();
|
|
418
|
+
if (this.text[this.index] === '}') {
|
|
419
|
+
this.index += 1;
|
|
420
|
+
return output;
|
|
421
|
+
}
|
|
422
|
+
while (this.index < this.text.length) {
|
|
423
|
+
if (this.text[this.index] !== '"') this.syntax('Expected a quoted JSON member name.');
|
|
424
|
+
const key = this.parseString();
|
|
425
|
+
if (keys.has(key)) {
|
|
426
|
+
throw new KDNARuntimeContractError(
|
|
427
|
+
'KDNA_JSON_DUPLICATE_KEY',
|
|
428
|
+
`Execution-contract JSON contains duplicate member name ${JSON.stringify(key)}.`,
|
|
429
|
+
{ key, code_unit_offset: this.index },
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
keys.add(key);
|
|
433
|
+
this.skipWhitespace();
|
|
434
|
+
if (this.text[this.index] !== ':') this.syntax('Expected a colon after a JSON member name.');
|
|
435
|
+
this.index += 1;
|
|
436
|
+
Object.defineProperty(output, key, {
|
|
437
|
+
value: this.parseValue(depth),
|
|
438
|
+
enumerable: true,
|
|
439
|
+
writable: true,
|
|
440
|
+
configurable: true,
|
|
441
|
+
});
|
|
442
|
+
this.skipWhitespace();
|
|
443
|
+
const next = this.text[this.index];
|
|
444
|
+
if (next === '}') {
|
|
445
|
+
this.index += 1;
|
|
446
|
+
return output;
|
|
447
|
+
}
|
|
448
|
+
if (next !== ',') this.syntax('Expected a comma or closing brace.');
|
|
449
|
+
this.index += 1;
|
|
450
|
+
this.skipWhitespace();
|
|
451
|
+
}
|
|
452
|
+
this.syntax('Unterminated JSON object.');
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
parseArray(depth) {
|
|
456
|
+
const output = [];
|
|
457
|
+
this.index += 1;
|
|
458
|
+
this.skipWhitespace();
|
|
459
|
+
if (this.text[this.index] === ']') {
|
|
460
|
+
this.index += 1;
|
|
461
|
+
return output;
|
|
462
|
+
}
|
|
463
|
+
while (this.index < this.text.length) {
|
|
464
|
+
output.push(this.parseValue(depth));
|
|
465
|
+
this.skipWhitespace();
|
|
466
|
+
const next = this.text[this.index];
|
|
467
|
+
if (next === ']') {
|
|
468
|
+
this.index += 1;
|
|
469
|
+
return output;
|
|
470
|
+
}
|
|
471
|
+
if (next !== ',') this.syntax('Expected a comma or closing bracket.');
|
|
472
|
+
this.index += 1;
|
|
473
|
+
this.skipWhitespace();
|
|
474
|
+
}
|
|
475
|
+
this.syntax('Unterminated JSON array.');
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
parseString() {
|
|
479
|
+
this.index += 1;
|
|
480
|
+
let output = '';
|
|
481
|
+
while (this.index < this.text.length) {
|
|
482
|
+
const char = this.text[this.index];
|
|
483
|
+
if (char === '"') {
|
|
484
|
+
this.index += 1;
|
|
485
|
+
return output;
|
|
486
|
+
}
|
|
487
|
+
if (char === '\\') {
|
|
488
|
+
this.index += 1;
|
|
489
|
+
output += this.parseEscape();
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
492
|
+
const code = this.text.charCodeAt(this.index);
|
|
493
|
+
if (code <= 0x1f) this.syntax('Unescaped control character in JSON string.');
|
|
494
|
+
if (code >= 0xd800 && code <= 0xdbff) {
|
|
495
|
+
const low = this.text.charCodeAt(this.index + 1);
|
|
496
|
+
if (low < 0xdc00 || low > 0xdfff) this.invalidUnicode();
|
|
497
|
+
output += this.text.slice(this.index, this.index + 2);
|
|
498
|
+
this.index += 2;
|
|
499
|
+
continue;
|
|
500
|
+
}
|
|
501
|
+
if (code >= 0xdc00 && code <= 0xdfff) this.invalidUnicode();
|
|
502
|
+
output += char;
|
|
503
|
+
this.index += 1;
|
|
504
|
+
}
|
|
505
|
+
this.syntax('Unterminated JSON string.');
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
parseEscape() {
|
|
509
|
+
const char = this.text[this.index];
|
|
510
|
+
const simple = { '"': '"', '\\': '\\', '/': '/', b: '\b', f: '\f', n: '\n', r: '\r', t: '\t' };
|
|
511
|
+
if (Object.prototype.hasOwnProperty.call(simple, char)) {
|
|
512
|
+
this.index += 1;
|
|
513
|
+
return simple[char];
|
|
514
|
+
}
|
|
515
|
+
if (char !== 'u') this.syntax('Invalid JSON string escape.');
|
|
516
|
+
this.index += 1;
|
|
517
|
+
const first = this.parseHexCodeUnit();
|
|
518
|
+
if (first >= 0xd800 && first <= 0xdbff) {
|
|
519
|
+
if (this.text.slice(this.index, this.index + 2) !== '\\u') this.invalidUnicode();
|
|
520
|
+
this.index += 2;
|
|
521
|
+
const second = this.parseHexCodeUnit();
|
|
522
|
+
if (second < 0xdc00 || second > 0xdfff) this.invalidUnicode();
|
|
523
|
+
return String.fromCodePoint(0x10000 + ((first - 0xd800) << 10) + (second - 0xdc00));
|
|
524
|
+
}
|
|
525
|
+
if (first >= 0xdc00 && first <= 0xdfff) this.invalidUnicode();
|
|
526
|
+
return String.fromCharCode(first);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
parseHexCodeUnit() {
|
|
530
|
+
const hex = this.text.slice(this.index, this.index + 4);
|
|
531
|
+
if (!/^[0-9a-fA-F]{4}$/.test(hex)) this.syntax('Invalid Unicode escape in JSON string.');
|
|
532
|
+
this.index += 4;
|
|
533
|
+
return Number.parseInt(hex, 16);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
invalidUnicode() {
|
|
537
|
+
throw new KDNARuntimeContractError(
|
|
538
|
+
'KDNA_JSON_INVALID_UNICODE',
|
|
539
|
+
'Execution-contract JSON contains an unpaired UTF-16 surrogate.',
|
|
540
|
+
{ code_unit_offset: this.index },
|
|
541
|
+
);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
parseNumber() {
|
|
545
|
+
const source = this.text.slice(this.index);
|
|
546
|
+
const match = /^-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?/.exec(source);
|
|
547
|
+
if (!match) this.syntax('Invalid JSON number.');
|
|
548
|
+
const token = match[0];
|
|
549
|
+
this.index += token.length;
|
|
550
|
+
const number = Number(token);
|
|
551
|
+
if (!Number.isFinite(number)) {
|
|
552
|
+
throw new KDNARuntimeContractError(
|
|
553
|
+
'KDNA_JSON_NUMBER_INVALID',
|
|
554
|
+
'Execution-contract JSON number is outside the finite IEEE-754 range.',
|
|
555
|
+
);
|
|
556
|
+
}
|
|
557
|
+
return number;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function computeConsumptionPlanDigest(plan) {
|
|
562
|
+
try {
|
|
563
|
+
if (plan === null || typeof plan !== 'object' || Array.isArray(plan)) {
|
|
564
|
+
throw new TypeError('plan must be an object');
|
|
565
|
+
}
|
|
566
|
+
const projection = clone(plan);
|
|
567
|
+
delete projection.integrity;
|
|
568
|
+
return digestJcs(projection);
|
|
569
|
+
} catch (error) {
|
|
570
|
+
if (error instanceof KDNARuntimeContractError) throw error;
|
|
571
|
+
throw new KDNARuntimeContractError(
|
|
572
|
+
'KDNA_PLAN_INPUT_INVALID',
|
|
573
|
+
'ConsumptionPlan digest input must be a finite, acyclic JSON object.',
|
|
574
|
+
{ cause: error && typeof error.message === 'string' ? error.message : String(error) },
|
|
575
|
+
);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
function validatePlanSemantics(plan, trustedPlanDigest) {
|
|
580
|
+
let computed;
|
|
581
|
+
try {
|
|
582
|
+
computed = computeConsumptionPlanDigest(plan);
|
|
583
|
+
} catch {
|
|
584
|
+
return 'KDNA_PLAN_INPUT_INVALID';
|
|
585
|
+
}
|
|
586
|
+
if (computed !== plan.integrity.plan_digest) return 'KDNA_PLAN_DIGEST_MISMATCH';
|
|
587
|
+
if (trustedPlanDigest !== null && computed !== trustedPlanDigest) {
|
|
588
|
+
return 'KDNA_TRUSTED_PLAN_DIGEST_MISMATCH';
|
|
589
|
+
}
|
|
590
|
+
return null;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
function validateConsumptionPlan(plan, context) {
|
|
594
|
+
const contextResult = snapshotContext(context, ['trustedPlanDigest']);
|
|
595
|
+
if (!contextResult.valid) return contextResult;
|
|
596
|
+
const safeContext = contextResult.value;
|
|
597
|
+
const schemaResult = validateSchema('plan', plan);
|
|
598
|
+
if (!schemaResult.valid) return schemaResult;
|
|
599
|
+
const safePlan = schemaResult.value;
|
|
600
|
+
if (
|
|
601
|
+
typeof safeContext.trustedPlanDigest !== 'string' ||
|
|
602
|
+
!/^sha256:[0-9a-f]{64}$/.test(safeContext.trustedPlanDigest)
|
|
603
|
+
) {
|
|
604
|
+
return invalid(
|
|
605
|
+
'KDNA_VALIDATION_CONTEXT_INVALID',
|
|
606
|
+
'trustedPlanDigest must be a non-null lowercase sha256 digest.',
|
|
607
|
+
);
|
|
608
|
+
}
|
|
609
|
+
const code = validatePlanSemantics(safePlan, safeContext.trustedPlanDigest);
|
|
610
|
+
return code ? invalid(code, 'ConsumptionPlan integrity correlation failed.') : ok(safePlan);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
function buildConsumptionPlan(input) {
|
|
614
|
+
let plan;
|
|
615
|
+
try {
|
|
616
|
+
plan = {
|
|
617
|
+
type: 'kdna.consumption-plan',
|
|
618
|
+
contract_version: '0.1.0',
|
|
619
|
+
plan_id: input.plan_id,
|
|
620
|
+
created_at: input.created_at,
|
|
621
|
+
mode: 'single',
|
|
622
|
+
task: clone(input.task),
|
|
623
|
+
asset_ref: clone(input.asset_ref),
|
|
624
|
+
projection_request: {
|
|
625
|
+
profile: input.projection_profile,
|
|
626
|
+
accepted_capsule_versions: [CAPSULE_CONTRACT_VERSION],
|
|
627
|
+
required_digest_profile: CAPSULE_DIGEST_PROFILE,
|
|
628
|
+
required_digest_profile_version: '0.1.0',
|
|
629
|
+
require_packaged_asset: true,
|
|
630
|
+
},
|
|
631
|
+
host_request: { accepted_protocols: [HOST_PROTOCOL] },
|
|
632
|
+
result_request: { shape: 'structured_judgment' },
|
|
633
|
+
budget: clone(input.budget),
|
|
634
|
+
trace_policy: clone(input.trace_policy),
|
|
635
|
+
integrity: {
|
|
636
|
+
profile: PLAN_DIGEST_PROFILE,
|
|
637
|
+
profile_version: PLAN_DIGEST_PROFILE_VERSION,
|
|
638
|
+
plan_digest: '',
|
|
639
|
+
},
|
|
640
|
+
constraints: clone(input.constraints),
|
|
641
|
+
};
|
|
642
|
+
plan.integrity.plan_digest = computeConsumptionPlanDigest(plan);
|
|
643
|
+
} catch (error) {
|
|
644
|
+
if (error instanceof KDNARuntimeContractError) throw error;
|
|
645
|
+
throw new KDNARuntimeContractError(
|
|
646
|
+
'KDNA_PLAN_INPUT_INVALID',
|
|
647
|
+
'Cannot build ConsumptionPlan from the supplied input.',
|
|
648
|
+
);
|
|
649
|
+
}
|
|
650
|
+
const result = validateConsumptionPlan(plan, {
|
|
651
|
+
trustedPlanDigest: plan.integrity.plan_digest,
|
|
652
|
+
});
|
|
653
|
+
throwInvalid(result);
|
|
654
|
+
return plan;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
function blocked(issueCode) {
|
|
658
|
+
return {
|
|
659
|
+
state: 'blocked',
|
|
660
|
+
capsule_version: null,
|
|
661
|
+
host_protocol: null,
|
|
662
|
+
issue_code: issueCode,
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
function negotiateRuntimePair(plan, context) {
|
|
667
|
+
const contextResult = snapshotContext(context, [
|
|
668
|
+
'trustedPlanDigest',
|
|
669
|
+
'capabilities',
|
|
670
|
+
'coreCapsuleVersions',
|
|
671
|
+
]);
|
|
672
|
+
if (!contextResult.valid) return blocked(contextResult.code);
|
|
673
|
+
const safeContext = contextResult.value;
|
|
674
|
+
const planResult = validateConsumptionPlan(plan, {
|
|
675
|
+
trustedPlanDigest: safeContext.trustedPlanDigest,
|
|
676
|
+
});
|
|
677
|
+
if (!planResult.valid) return blocked(planResult.code);
|
|
678
|
+
const safePlan = planResult.value;
|
|
679
|
+
const capabilitiesResult = validateSchema('capabilities', safeContext.capabilities);
|
|
680
|
+
if (!capabilitiesResult.valid) return blocked(capabilitiesResult.code);
|
|
681
|
+
const safeCapabilities = capabilitiesResult.value;
|
|
682
|
+
if (
|
|
683
|
+
!Array.isArray(safeContext.coreCapsuleVersions) ||
|
|
684
|
+
safeContext.coreCapsuleVersions.some((value) => typeof value !== 'string')
|
|
685
|
+
) {
|
|
686
|
+
return blocked('KDNA_VALIDATION_CONTEXT_INVALID');
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
const hasCapsule =
|
|
690
|
+
safePlan.projection_request.accepted_capsule_versions.includes(CAPSULE_CONTRACT_VERSION) &&
|
|
691
|
+
safeContext.coreCapsuleVersions.includes(CAPSULE_CONTRACT_VERSION) &&
|
|
692
|
+
safeCapabilities.capsule_versions.includes(CAPSULE_CONTRACT_VERSION);
|
|
693
|
+
if (!hasCapsule) return blocked('KDNA_CAPSULE_CONTRACT_VERSION_UNSUPPORTED');
|
|
694
|
+
|
|
695
|
+
const hasProtocol =
|
|
696
|
+
safePlan.host_request.accepted_protocols.includes(HOST_PROTOCOL) &&
|
|
697
|
+
safeCapabilities.host_protocols.includes(HOST_PROTOCOL);
|
|
698
|
+
if (!hasProtocol) return blocked('KDNA_HOST_PROTOCOL_UNSUPPORTED');
|
|
699
|
+
|
|
700
|
+
const hasVerifiablePair =
|
|
701
|
+
safeCapabilities.capability_basis === 'registered_descriptor' &&
|
|
702
|
+
safeCapabilities.capsule_digest_profiles.includes(CAPSULE_DELIVERY_PROFILE);
|
|
703
|
+
if (!hasVerifiablePair) return blocked('KDNA_HOST_CAPSULE_PAIR_UNSUPPORTED');
|
|
704
|
+
|
|
705
|
+
return {
|
|
706
|
+
state: 'selected',
|
|
707
|
+
capsule_version: CAPSULE_CONTRACT_VERSION,
|
|
708
|
+
host_protocol: HOST_PROTOCOL,
|
|
709
|
+
issue_code: null,
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
function expectedAgainst(source) {
|
|
714
|
+
if (source === 'kdna.json.content_digest') return 'manifest_declaration';
|
|
715
|
+
if (source === 'checksums.json.entry_set_digest') {
|
|
716
|
+
return 'checksum_declaration';
|
|
717
|
+
}
|
|
718
|
+
return 'external_expected';
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
function validateExpectedDigests(expectedDigests, capsuleDigests) {
|
|
722
|
+
for (const name of ['asset', 'content', 'runtime_entry_set']) {
|
|
723
|
+
const expected = expectedDigests[name];
|
|
724
|
+
if (expected === null) continue;
|
|
725
|
+
const observed = capsuleDigests[name];
|
|
726
|
+
if (observed.value !== expected.value || observed.basis !== expected.basis) {
|
|
727
|
+
return 'KDNA_HOST_DIGEST_EXPECTATION_MISMATCH';
|
|
728
|
+
}
|
|
729
|
+
if (observed.comparison.state !== expected.comparison) {
|
|
730
|
+
return 'KDNA_HOST_DIGEST_EXPECTATION_MISMATCH';
|
|
731
|
+
}
|
|
732
|
+
if (expected.comparison === 'matched') {
|
|
733
|
+
if (
|
|
734
|
+
observed.comparison.expected !== expected.value ||
|
|
735
|
+
observed.comparison.source !== expected.source ||
|
|
736
|
+
observed.comparison.against !== expectedAgainst(expected.source)
|
|
737
|
+
) {
|
|
738
|
+
return 'KDNA_HOST_DIGEST_EXPECTATION_MISMATCH';
|
|
739
|
+
}
|
|
740
|
+
} else if (
|
|
741
|
+
observed.comparison.expected !== null ||
|
|
742
|
+
observed.comparison.source !== null ||
|
|
743
|
+
observed.comparison.against !== null
|
|
744
|
+
) {
|
|
745
|
+
return 'KDNA_HOST_DIGEST_EXPECTATION_MISMATCH';
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
return null;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
function validateRequestSemantics(request, plan, enforceBudget = true) {
|
|
752
|
+
if (
|
|
753
|
+
request.plan_ref.plan_id !== plan.plan_id ||
|
|
754
|
+
request.plan_ref.plan_digest_profile !== plan.integrity.profile ||
|
|
755
|
+
request.plan_ref.plan_digest_profile_version !== plan.integrity.profile_version ||
|
|
756
|
+
request.plan_ref.plan_digest !== plan.integrity.plan_digest ||
|
|
757
|
+
computeConsumptionPlanDigest(plan) !== plan.integrity.plan_digest
|
|
758
|
+
) {
|
|
759
|
+
return 'KDNA_HOST_PLAN_REF_MISMATCH';
|
|
760
|
+
}
|
|
761
|
+
if (!equalJson(request.task, plan.task)) return 'KDNA_HOST_TASK_MISMATCH';
|
|
762
|
+
if (request.authority.asset_id !== plan.asset_ref.asset_id) {
|
|
763
|
+
return 'KDNA_HOST_ASSET_ID_MISMATCH';
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
const expectedRequestAsset = { ...plan.asset_ref, role: 'primary' };
|
|
767
|
+
if (!equalJson(request.asset, expectedRequestAsset)) return 'KDNA_HOST_ASSET_REF_MISMATCH';
|
|
768
|
+
|
|
769
|
+
const capsuleAsset = request.capsule.asset;
|
|
770
|
+
for (const field of ['asset_id', 'asset_uid', 'version', 'judgment_version']) {
|
|
771
|
+
if (capsuleAsset[field] !== plan.asset_ref[field]) return 'KDNA_HOST_ASSET_REF_MISMATCH';
|
|
772
|
+
}
|
|
773
|
+
if (request.capsule.access !== plan.asset_ref.access) return 'KDNA_HOST_ASSET_REF_MISMATCH';
|
|
774
|
+
|
|
775
|
+
if (
|
|
776
|
+
request.runtime_contract.capsule_version !== request.capsule.contract_version ||
|
|
777
|
+
request.capsule.contract_version !== CAPSULE_CONTRACT_VERSION
|
|
778
|
+
) {
|
|
779
|
+
return 'KDNA_HOST_CAPSULE_CONTRACT_VERSION_MISMATCH';
|
|
780
|
+
}
|
|
781
|
+
if (
|
|
782
|
+
request.runtime_contract.capsule_digest_profile !== CAPSULE_DELIVERY_PROFILE ||
|
|
783
|
+
request.runtime_contract.capsule_digest_profile_version !==
|
|
784
|
+
CAPSULE_DELIVERY_PROFILE_VERSION ||
|
|
785
|
+
request.capsule.digests.profile !== plan.projection_request.required_digest_profile ||
|
|
786
|
+
request.capsule.digests.profile_version !==
|
|
787
|
+
plan.projection_request.required_digest_profile_version
|
|
788
|
+
) {
|
|
789
|
+
return 'KDNA_HOST_PROJECTION_CONTRACT_MISMATCH';
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
const expectedProjectionContract = {
|
|
793
|
+
profile: plan.projection_request.profile,
|
|
794
|
+
required_digest_profile: plan.projection_request.required_digest_profile,
|
|
795
|
+
required_digest_profile_version:
|
|
796
|
+
plan.projection_request.required_digest_profile_version,
|
|
797
|
+
require_packaged_asset: plan.projection_request.require_packaged_asset,
|
|
798
|
+
};
|
|
799
|
+
if (
|
|
800
|
+
!equalJson(request.projection_contract, expectedProjectionContract) ||
|
|
801
|
+
request.capsule.profile !== request.projection_contract.profile
|
|
802
|
+
) {
|
|
803
|
+
return 'KDNA_HOST_PROJECTION_CONTRACT_MISMATCH';
|
|
804
|
+
}
|
|
805
|
+
if (!equalJson(request.result_contract, plan.result_request)) {
|
|
806
|
+
return 'KDNA_HOST_RESULT_CONTRACT_MISMATCH';
|
|
807
|
+
}
|
|
808
|
+
if (!equalJson(request.budget, plan.budget)) return 'KDNA_HOST_BUDGET_MISMATCH';
|
|
809
|
+
if (!equalJson(request.constraints, plan.constraints)) return 'KDNA_HOST_CONSTRAINTS_MISMATCH';
|
|
810
|
+
if (
|
|
811
|
+
enforceBudget &&
|
|
812
|
+
(jcsCharacterCount(request.capsule) > request.budget.max_projection_chars ||
|
|
813
|
+
jcsCharacterCount(request.task) > request.budget.max_task_chars)
|
|
814
|
+
) {
|
|
815
|
+
return 'KDNA_HOST_BUDGET_LIMIT_EXCEEDED';
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
const digestError = validateExpectedDigests(
|
|
819
|
+
plan.asset_ref.expected_digests,
|
|
820
|
+
request.capsule.digests,
|
|
821
|
+
);
|
|
822
|
+
if (digestError) return digestError;
|
|
823
|
+
|
|
824
|
+
if (
|
|
825
|
+
computeCapsuleDeliveryDigest(request.capsule) !==
|
|
826
|
+
request.runtime_contract.capsule_delivery_digest
|
|
827
|
+
) {
|
|
828
|
+
return 'KDNA_CAPSULE_DELIVERY_DIGEST_MISMATCH';
|
|
829
|
+
}
|
|
830
|
+
return null;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
function validateAgentHostRequest(request, context) {
|
|
834
|
+
const contextResult = snapshotContext(context, [
|
|
835
|
+
'plan',
|
|
836
|
+
'trustedPlanDigest',
|
|
837
|
+
'capabilities',
|
|
838
|
+
'coreCapsuleVersions',
|
|
839
|
+
]);
|
|
840
|
+
if (!contextResult.valid) return contextResult;
|
|
841
|
+
const safeContext = contextResult.value;
|
|
842
|
+
const schemaResult = validateSchema('request', request);
|
|
843
|
+
if (!schemaResult.valid) return schemaResult;
|
|
844
|
+
const safeRequest = schemaResult.value;
|
|
845
|
+
const planResult = validateConsumptionPlan(safeContext.plan, {
|
|
846
|
+
trustedPlanDigest: safeContext.trustedPlanDigest,
|
|
847
|
+
});
|
|
848
|
+
if (!planResult.valid) return planResult;
|
|
849
|
+
const safePlan = planResult.value;
|
|
850
|
+
const negotiation = negotiateRuntimePair(safePlan, safeContext);
|
|
851
|
+
if (negotiation.state !== 'selected') {
|
|
852
|
+
return invalid(negotiation.issue_code, 'No strict ConsumptionPlan / Agent Host pair was selected.');
|
|
853
|
+
}
|
|
854
|
+
try {
|
|
855
|
+
const code = validateRequestSemantics(safeRequest, safePlan, true);
|
|
856
|
+
return code ? invalid(code, 'Agent Host request correlation failed.') : ok(safeRequest);
|
|
857
|
+
} catch (error) {
|
|
858
|
+
return invalid('KDNA_INPUT_INVALID', 'Agent Host request is not a safe JSON value.', {
|
|
859
|
+
cause: error && typeof error.message === 'string' ? error.message : String(error),
|
|
860
|
+
});
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
function assembleAgentHostRequest(input, plan) {
|
|
865
|
+
return {
|
|
866
|
+
protocol: HOST_PROTOCOL,
|
|
867
|
+
protocol_version: HOST_PROTOCOL_VERSION,
|
|
868
|
+
request_id: input.request_id,
|
|
869
|
+
plan_ref: {
|
|
870
|
+
plan_id: plan.plan_id,
|
|
871
|
+
plan_digest_profile: plan.integrity.profile,
|
|
872
|
+
plan_digest_profile_version: plan.integrity.profile_version,
|
|
873
|
+
plan_digest: plan.integrity.plan_digest,
|
|
874
|
+
},
|
|
875
|
+
runtime_contract: {
|
|
876
|
+
capsule_version: CAPSULE_CONTRACT_VERSION,
|
|
877
|
+
capsule_digest_profile: CAPSULE_DELIVERY_PROFILE,
|
|
878
|
+
capsule_digest_profile_version: CAPSULE_DELIVERY_PROFILE_VERSION,
|
|
879
|
+
capsule_delivery_digest: computeCapsuleDeliveryDigest(input.capsule),
|
|
880
|
+
},
|
|
881
|
+
projection_contract: {
|
|
882
|
+
profile: plan.projection_request.profile,
|
|
883
|
+
required_digest_profile: plan.projection_request.required_digest_profile,
|
|
884
|
+
required_digest_profile_version:
|
|
885
|
+
plan.projection_request.required_digest_profile_version,
|
|
886
|
+
require_packaged_asset: plan.projection_request.require_packaged_asset,
|
|
887
|
+
},
|
|
888
|
+
result_contract: clone(plan.result_request),
|
|
889
|
+
budget: clone(plan.budget),
|
|
890
|
+
constraints: clone(plan.constraints),
|
|
891
|
+
phase: 'single_judgment',
|
|
892
|
+
task: clone(plan.task),
|
|
893
|
+
authority: { asset_id: plan.asset_ref.asset_id, role: 'primary', final_decision: true },
|
|
894
|
+
asset: { ...clone(plan.asset_ref), role: 'primary' },
|
|
895
|
+
capsule: clone(input.capsule),
|
|
896
|
+
};
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
function buildAgentHostRequest(input, context) {
|
|
900
|
+
const contextResult = snapshotContext(context, [
|
|
901
|
+
'plan',
|
|
902
|
+
'trustedPlanDigest',
|
|
903
|
+
'capabilities',
|
|
904
|
+
'coreCapsuleVersions',
|
|
905
|
+
]);
|
|
906
|
+
throwInvalid(contextResult);
|
|
907
|
+
const inputResult = snapshotPublicInput(input, ['request_id', 'capsule']);
|
|
908
|
+
throwInvalid(inputResult);
|
|
909
|
+
const safeContext = contextResult.value;
|
|
910
|
+
const planResult = validateConsumptionPlan(safeContext.plan, {
|
|
911
|
+
trustedPlanDigest: safeContext.trustedPlanDigest,
|
|
912
|
+
});
|
|
913
|
+
throwInvalid(planResult);
|
|
914
|
+
const plan = planResult.value;
|
|
915
|
+
const negotiation = negotiateRuntimePair(plan, safeContext);
|
|
916
|
+
if (negotiation.state !== 'selected') {
|
|
917
|
+
throw new KDNARuntimeContractError(
|
|
918
|
+
negotiation.issue_code,
|
|
919
|
+
'Cannot build Agent Host request without a selected strict execution pair.',
|
|
920
|
+
);
|
|
921
|
+
}
|
|
922
|
+
let request;
|
|
923
|
+
try {
|
|
924
|
+
request = assembleAgentHostRequest(inputResult.value, plan);
|
|
925
|
+
} catch {
|
|
926
|
+
throw new KDNARuntimeContractError(
|
|
927
|
+
'KDNA_HOST_REQUEST_INPUT_INVALID',
|
|
928
|
+
'Cannot build Agent Host request from the supplied input.',
|
|
929
|
+
);
|
|
930
|
+
}
|
|
931
|
+
const result = validateAgentHostRequest(request, { ...safeContext, plan });
|
|
932
|
+
throwInvalid(result);
|
|
933
|
+
return request;
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
function validateReceiptSemantics(receipt, request) {
|
|
937
|
+
if (
|
|
938
|
+
receipt.protocol !== request.protocol ||
|
|
939
|
+
receipt.protocol_version !== request.protocol_version ||
|
|
940
|
+
receipt.request_id !== request.request_id
|
|
941
|
+
) {
|
|
942
|
+
return 'KDNA_HOST_REQUEST_ID_MISMATCH';
|
|
943
|
+
}
|
|
944
|
+
const runtimeReceipt = receipt.runtime_receipt;
|
|
945
|
+
const senderP = request.runtime_contract.capsule_delivery_digest;
|
|
946
|
+
const recomputedP = computeCapsuleDeliveryDigest(request.capsule);
|
|
947
|
+
if (
|
|
948
|
+
runtimeReceipt.sender_capsule_delivery_digest !== senderP ||
|
|
949
|
+
runtimeReceipt.echoed_capsule_delivery_digest !==
|
|
950
|
+
runtimeReceipt.host_recomputed_capsule_delivery_digest
|
|
951
|
+
) {
|
|
952
|
+
return 'KDNA_HOST_CAPSULE_DELIVERY_DIGEST_MISMATCH';
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
if (runtimeReceipt.capsule_delivery_comparison === 'mismatched') {
|
|
956
|
+
if (
|
|
957
|
+
runtimeReceipt.host_recomputed_capsule_delivery_digest === senderP ||
|
|
958
|
+
runtimeReceipt.provider_execution_status !== 'not_started' ||
|
|
959
|
+
receipt.outcome !== null
|
|
960
|
+
) {
|
|
961
|
+
return 'KDNA_HOST_CAPSULE_DELIVERY_DIGEST_MISMATCH';
|
|
962
|
+
}
|
|
963
|
+
} else if (
|
|
964
|
+
runtimeReceipt.capsule_delivery_comparison !== 'matched' ||
|
|
965
|
+
runtimeReceipt.provider_execution_status === 'not_started' ||
|
|
966
|
+
senderP !== recomputedP ||
|
|
967
|
+
runtimeReceipt.host_recomputed_capsule_delivery_digest !== senderP ||
|
|
968
|
+
runtimeReceipt.host_recomputed_capsule_delivery_digest !== recomputedP
|
|
969
|
+
) {
|
|
970
|
+
return 'KDNA_HOST_CAPSULE_DELIVERY_DIGEST_MISMATCH';
|
|
971
|
+
}
|
|
972
|
+
if (
|
|
973
|
+
runtimeReceipt.capsule_version !== request.runtime_contract.capsule_version ||
|
|
974
|
+
runtimeReceipt.capsule_digest_profile !== request.runtime_contract.capsule_digest_profile
|
|
975
|
+
) {
|
|
976
|
+
return 'KDNA_HOST_CAPSULE_CONTRACT_VERSION_MISMATCH';
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
const outcomeUsage = receipt.outcome?.usage ?? null;
|
|
980
|
+
const observed = runtimeReceipt.usage;
|
|
981
|
+
if (receipt.outcome === null) return null;
|
|
982
|
+
if (outcomeUsage === null) {
|
|
983
|
+
if (
|
|
984
|
+
observed.basis !== 'not_observed' ||
|
|
985
|
+
observed.tokens_used !== null ||
|
|
986
|
+
observed.model_calls !== null
|
|
987
|
+
) {
|
|
988
|
+
return 'KDNA_HOST_USAGE_MISMATCH';
|
|
989
|
+
}
|
|
990
|
+
} else if (
|
|
991
|
+
observed.basis !== 'host_reported' ||
|
|
992
|
+
observed.tokens_used !== outcomeUsage.tokens_used ||
|
|
993
|
+
observed.model_calls !== outcomeUsage.model_calls
|
|
994
|
+
) {
|
|
995
|
+
return 'KDNA_HOST_USAGE_MISMATCH';
|
|
996
|
+
}
|
|
997
|
+
return null;
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
function validateAgentHostReceipt(receipt, context) {
|
|
1001
|
+
const contextResult = snapshotContext(context, ['request']);
|
|
1002
|
+
if (!contextResult.valid) return contextResult;
|
|
1003
|
+
const safeContext = contextResult.value;
|
|
1004
|
+
const schemaResult = validateSchema('receipt', receipt);
|
|
1005
|
+
if (!schemaResult.valid) return schemaResult;
|
|
1006
|
+
const safeReceipt = schemaResult.value;
|
|
1007
|
+
const requestSchemaResult = validateSchema('request', safeContext.request);
|
|
1008
|
+
if (!requestSchemaResult.valid) {
|
|
1009
|
+
return invalid(
|
|
1010
|
+
'KDNA_VALIDATION_CONTEXT_INVALID',
|
|
1011
|
+
'Receipt validation requires a schema-valid independent Agent Host request.',
|
|
1012
|
+
requestSchemaResult.errors,
|
|
1013
|
+
);
|
|
1014
|
+
}
|
|
1015
|
+
const safeRequest = requestSchemaResult.value;
|
|
1016
|
+
try {
|
|
1017
|
+
const code = validateReceiptSemantics(safeReceipt, safeRequest);
|
|
1018
|
+
return code ? invalid(code, 'Agent Host receipt correlation failed.') : ok(safeReceipt);
|
|
1019
|
+
} catch (error) {
|
|
1020
|
+
return invalid('KDNA_INPUT_INVALID', 'Agent Host receipt is not a safe JSON value.', {
|
|
1021
|
+
cause: error && typeof error.message === 'string' ? error.message : String(error),
|
|
1022
|
+
});
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
function compareObserved(value, limit, unobserved = 'not_observed') {
|
|
1027
|
+
if (limit === null) return 'not_limited';
|
|
1028
|
+
if (value === null) return unobserved;
|
|
1029
|
+
return value <= limit ? 'within_limit' : 'exceeded';
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
function expectedBudgetEvidence(plan, request, receipt) {
|
|
1033
|
+
const projectionChars = request === null ? null : jcsCharacterCount(request.capsule);
|
|
1034
|
+
const taskChars = jcsCharacterCount(plan.task);
|
|
1035
|
+
const usage = receipt?.runtime_receipt.usage ?? {
|
|
1036
|
+
elapsed_ms: null,
|
|
1037
|
+
elapsed_basis: 'not_observed',
|
|
1038
|
+
tokens_used: null,
|
|
1039
|
+
model_calls: null,
|
|
1040
|
+
basis: 'not_observed',
|
|
1041
|
+
};
|
|
1042
|
+
const actual = {
|
|
1043
|
+
projection_chars: projectionChars,
|
|
1044
|
+
task_chars: taskChars,
|
|
1045
|
+
elapsed_ms: usage.elapsed_ms,
|
|
1046
|
+
elapsed_basis: usage.elapsed_basis,
|
|
1047
|
+
tokens_used: usage.tokens_used,
|
|
1048
|
+
model_calls: usage.model_calls,
|
|
1049
|
+
usage_basis: usage.basis,
|
|
1050
|
+
};
|
|
1051
|
+
const comparison = {
|
|
1052
|
+
projection_chars: compareObserved(projectionChars, plan.budget.max_projection_chars),
|
|
1053
|
+
task_chars: compareObserved(taskChars, plan.budget.max_task_chars),
|
|
1054
|
+
elapsed_ms: compareObserved(actual.elapsed_ms, plan.budget.deadline_ms),
|
|
1055
|
+
tokens_used: compareObserved(usage.tokens_used, plan.budget.max_tokens),
|
|
1056
|
+
model_calls: compareObserved(usage.model_calls, plan.budget.max_model_calls),
|
|
1057
|
+
};
|
|
1058
|
+
const dimensionStates = Object.values(comparison);
|
|
1059
|
+
comparison.overall = dimensionStates.includes('exceeded')
|
|
1060
|
+
? 'exceeded'
|
|
1061
|
+
: dimensionStates.includes('not_observed')
|
|
1062
|
+
? 'not_observed'
|
|
1063
|
+
: 'within_limit';
|
|
1064
|
+
return { limits: clone(plan.budget), actual, comparison };
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
function deriveBudgetEvidence(plan, context) {
|
|
1068
|
+
const contextResult = snapshotContext(context, ['trustedPlanDigest', 'request', 'receipt']);
|
|
1069
|
+
throwInvalid(contextResult);
|
|
1070
|
+
const safeContext = contextResult.value;
|
|
1071
|
+
const planResult = validateConsumptionPlan(plan, {
|
|
1072
|
+
trustedPlanDigest: safeContext.trustedPlanDigest,
|
|
1073
|
+
});
|
|
1074
|
+
throwInvalid(planResult);
|
|
1075
|
+
const safePlan = planResult.value;
|
|
1076
|
+
if (safeContext.request !== null) {
|
|
1077
|
+
const requestResult = validateSchema('request', safeContext.request);
|
|
1078
|
+
if (!requestResult.valid) {
|
|
1079
|
+
throw new KDNARuntimeContractError(
|
|
1080
|
+
'KDNA_VALIDATION_CONTEXT_INVALID',
|
|
1081
|
+
'Budget derivation requires a schema-valid independent Agent Host request.',
|
|
1082
|
+
requestResult.errors,
|
|
1083
|
+
);
|
|
1084
|
+
}
|
|
1085
|
+
safeContext.request = requestResult.value;
|
|
1086
|
+
const requestCode = validateRequestSemantics(safeContext.request, safePlan, false);
|
|
1087
|
+
if (requestCode !== null) {
|
|
1088
|
+
throw new KDNARuntimeContractError(
|
|
1089
|
+
requestCode,
|
|
1090
|
+
'Budget derivation requires an independently correlated Agent Host request.',
|
|
1091
|
+
);
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
if (safeContext.receipt !== null) {
|
|
1095
|
+
if (safeContext.request === null) {
|
|
1096
|
+
throw new KDNARuntimeContractError(
|
|
1097
|
+
'KDNA_VALIDATION_CONTEXT_INVALID',
|
|
1098
|
+
'Budget derivation cannot use a receipt without its independent request.',
|
|
1099
|
+
);
|
|
1100
|
+
}
|
|
1101
|
+
const receiptResult = validateAgentHostReceipt(safeContext.receipt, {
|
|
1102
|
+
request: safeContext.request,
|
|
1103
|
+
});
|
|
1104
|
+
throwInvalid(receiptResult);
|
|
1105
|
+
safeContext.receipt = receiptResult.value;
|
|
1106
|
+
}
|
|
1107
|
+
return expectedBudgetEvidence(safePlan, safeContext.request, safeContext.receipt);
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
function preparePreHostBudgetBlocked(capsule, safeContext) {
|
|
1111
|
+
const planResult = validateConsumptionPlan(safeContext.plan, {
|
|
1112
|
+
trustedPlanDigest: safeContext.trustedPlanDigest,
|
|
1113
|
+
});
|
|
1114
|
+
if (!planResult.valid) return planResult;
|
|
1115
|
+
const plan = planResult.value;
|
|
1116
|
+
const negotiation = negotiateRuntimePair(plan, safeContext);
|
|
1117
|
+
if (negotiation.state !== 'selected') {
|
|
1118
|
+
return invalid(
|
|
1119
|
+
negotiation.issue_code,
|
|
1120
|
+
'Pre-Host budget evidence requires a selected strict execution pair.',
|
|
1121
|
+
);
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
let projectedCandidate;
|
|
1125
|
+
try {
|
|
1126
|
+
projectedCandidate = assembleAgentHostRequest(
|
|
1127
|
+
{ request_id: PRE_HOST_INTERNAL_REQUEST_ID, capsule },
|
|
1128
|
+
plan,
|
|
1129
|
+
);
|
|
1130
|
+
} catch (error) {
|
|
1131
|
+
return invalid('KDNA_HOST_REQUEST_INPUT_INVALID', 'Projected Capsule input is invalid.', {
|
|
1132
|
+
cause: error && typeof error.message === 'string' ? error.message : String(error),
|
|
1133
|
+
});
|
|
1134
|
+
}
|
|
1135
|
+
const schemaResult = validateSchema('request', projectedCandidate);
|
|
1136
|
+
if (!schemaResult.valid) return schemaResult;
|
|
1137
|
+
projectedCandidate = schemaResult.value;
|
|
1138
|
+
const requestCode = validateRequestSemantics(projectedCandidate, plan, false);
|
|
1139
|
+
if (requestCode !== null) {
|
|
1140
|
+
return invalid(
|
|
1141
|
+
requestCode,
|
|
1142
|
+
'Projected Capsule does not correlate with the independently validated Plan.',
|
|
1143
|
+
);
|
|
1144
|
+
}
|
|
1145
|
+
const budget = expectedBudgetEvidence(plan, projectedCandidate, null);
|
|
1146
|
+
if (
|
|
1147
|
+
budget.comparison.projection_chars !== 'exceeded' &&
|
|
1148
|
+
budget.comparison.task_chars !== 'exceeded'
|
|
1149
|
+
) {
|
|
1150
|
+
return invalid(
|
|
1151
|
+
'KDNA_PRE_HOST_BUDGET_NOT_EXCEEDED',
|
|
1152
|
+
'Pre-Host blocked Trace construction requires an exceeded projection or task limit.',
|
|
1153
|
+
);
|
|
1154
|
+
}
|
|
1155
|
+
return ok({ plan, projectedCandidate, budget });
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
/**
|
|
1159
|
+
* Build the sole supported pre-Host blocked Trace path for a successfully projected Capsule
|
|
1160
|
+
* that exceeds an enforce-before-Host projection or task limit. This function
|
|
1161
|
+
* never exposes the internal request-shaped correlation candidate and cannot
|
|
1162
|
+
* produce a deliverable Agent Host request.
|
|
1163
|
+
*/
|
|
1164
|
+
function buildPreHostBudgetBlockedTrace(input, context) {
|
|
1165
|
+
const inputResult = snapshotPublicInput(
|
|
1166
|
+
input,
|
|
1167
|
+
['trace_id', 'timestamp', 'capsule'],
|
|
1168
|
+
['parent_trace_id', 'warnings'],
|
|
1169
|
+
);
|
|
1170
|
+
throwInvalid(inputResult);
|
|
1171
|
+
const contextResult = snapshotContext(context, [
|
|
1172
|
+
'plan',
|
|
1173
|
+
'trustedPlanDigest',
|
|
1174
|
+
'capabilities',
|
|
1175
|
+
'coreCapsuleVersions',
|
|
1176
|
+
]);
|
|
1177
|
+
throwInvalid(contextResult);
|
|
1178
|
+
const safeInput = inputResult.value;
|
|
1179
|
+
const safeContext = contextResult.value;
|
|
1180
|
+
const prepared = preparePreHostBudgetBlocked(safeInput.capsule, safeContext);
|
|
1181
|
+
throwInvalid(prepared);
|
|
1182
|
+
|
|
1183
|
+
return buildJudgmentTrace(
|
|
1184
|
+
{
|
|
1185
|
+
trace_id: safeInput.trace_id,
|
|
1186
|
+
timestamp: safeInput.timestamp,
|
|
1187
|
+
parent_trace_id: safeInput.parent_trace_id ?? null,
|
|
1188
|
+
overall_status: 'blocked',
|
|
1189
|
+
errors: [clone(PRE_HOST_BUDGET_ERROR)],
|
|
1190
|
+
warnings: safeInput.warnings || [],
|
|
1191
|
+
},
|
|
1192
|
+
{
|
|
1193
|
+
...safeContext,
|
|
1194
|
+
plan: prepared.value.plan,
|
|
1195
|
+
request: prepared.value.projectedCandidate,
|
|
1196
|
+
receipt: null,
|
|
1197
|
+
trustedDeliveryObservation: 'not_delivered',
|
|
1198
|
+
},
|
|
1199
|
+
);
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
/**
|
|
1203
|
+
* Validate a pre-Host budget-blocked Trace against the original Capsule and
|
|
1204
|
+
* independent Plan/Host capability facts without requiring callers to
|
|
1205
|
+
* construct or retain an over-budget request object.
|
|
1206
|
+
*/
|
|
1207
|
+
function validatePreHostBudgetBlockedTrace(trace, context) {
|
|
1208
|
+
const contextResult = snapshotContext(context, [
|
|
1209
|
+
'plan',
|
|
1210
|
+
'trustedPlanDigest',
|
|
1211
|
+
'capabilities',
|
|
1212
|
+
'coreCapsuleVersions',
|
|
1213
|
+
'capsule',
|
|
1214
|
+
]);
|
|
1215
|
+
if (!contextResult.valid) return contextResult;
|
|
1216
|
+
const safeContext = contextResult.value;
|
|
1217
|
+
const prepared = preparePreHostBudgetBlocked(safeContext.capsule, safeContext);
|
|
1218
|
+
if (!prepared.valid) return prepared;
|
|
1219
|
+
const traceResult = validateJudgmentTrace(trace, {
|
|
1220
|
+
...safeContext,
|
|
1221
|
+
plan: prepared.value.plan,
|
|
1222
|
+
request: prepared.value.projectedCandidate,
|
|
1223
|
+
receipt: null,
|
|
1224
|
+
trustedDeliveryObservation: 'not_delivered',
|
|
1225
|
+
});
|
|
1226
|
+
if (!traceResult.valid) return traceResult;
|
|
1227
|
+
const safeTrace = traceResult.value;
|
|
1228
|
+
if (
|
|
1229
|
+
safeTrace.overall_status !== 'blocked' ||
|
|
1230
|
+
safeTrace.host_receipt !== null ||
|
|
1231
|
+
safeTrace.execution.delivery_status !== 'not_delivered' ||
|
|
1232
|
+
safeTrace.execution.execution_status !== 'not_started' ||
|
|
1233
|
+
!equalJson(safeTrace.execution.semantic_consumption, {
|
|
1234
|
+
state: 'not_observed',
|
|
1235
|
+
basis: null,
|
|
1236
|
+
}) ||
|
|
1237
|
+
!equalJson(safeTrace.execution.model_identity, {
|
|
1238
|
+
value: null,
|
|
1239
|
+
basis: 'not_observed',
|
|
1240
|
+
}) ||
|
|
1241
|
+
safeTrace.budget.comparison.overall !== 'exceeded' ||
|
|
1242
|
+
(safeTrace.budget.comparison.projection_chars !== 'exceeded' &&
|
|
1243
|
+
safeTrace.budget.comparison.task_chars !== 'exceeded') ||
|
|
1244
|
+
!equalJson(safeTrace.errors, [PRE_HOST_BUDGET_ERROR])
|
|
1245
|
+
) {
|
|
1246
|
+
return invalid(
|
|
1247
|
+
'KDNA_TRACE_PRE_HOST_BUDGET_MISMATCH',
|
|
1248
|
+
'Trace is not an authentic pre-Host budget-blocked terminal.',
|
|
1249
|
+
);
|
|
1250
|
+
}
|
|
1251
|
+
return ok(safeTrace);
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
function withoutExpectedDigests(asset) {
|
|
1255
|
+
const value = clone(asset);
|
|
1256
|
+
delete value.expected_digests;
|
|
1257
|
+
delete value.role;
|
|
1258
|
+
return value;
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
function unavailableDigestEvidence() {
|
|
1262
|
+
const item = (basis) => ({
|
|
1263
|
+
value: null,
|
|
1264
|
+
basis,
|
|
1265
|
+
comparison: { state: 'unavailable', against: null, expected: null, source: null },
|
|
1266
|
+
});
|
|
1267
|
+
return {
|
|
1268
|
+
profile: CAPSULE_DIGEST_PROFILE,
|
|
1269
|
+
profile_version: '0.1.0',
|
|
1270
|
+
asset: item('kdna.digest-basis.container-bytes'),
|
|
1271
|
+
content: item('kdna.digest-basis.content-tree'),
|
|
1272
|
+
runtime_entry_set: item('kdna.digest-basis.runtime-entry-set'),
|
|
1273
|
+
};
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
function validateTerminalState(trace, receipt) {
|
|
1277
|
+
const table = {
|
|
1278
|
+
execution_completed: ['completed', 'completed', true, 0],
|
|
1279
|
+
execution_failed: ['failed', 'failed', false, 1],
|
|
1280
|
+
cancelled: ['cancelled', 'cancelled', false, 1],
|
|
1281
|
+
timed_out: ['timed_out', 'timed_out', false, 1],
|
|
1282
|
+
};
|
|
1283
|
+
|
|
1284
|
+
if (trace.overall_status === 'blocked') {
|
|
1285
|
+
if (
|
|
1286
|
+
trace.result_ref !== null ||
|
|
1287
|
+
trace.execution.execution_status !== 'not_started' ||
|
|
1288
|
+
trace.errors.length < 1
|
|
1289
|
+
) {
|
|
1290
|
+
return 'KDNA_TRACE_TERMINAL_STATE_MISMATCH';
|
|
1291
|
+
}
|
|
1292
|
+
return null;
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
const expected = table[trace.overall_status];
|
|
1296
|
+
if (!expected || receipt === null) return 'KDNA_TRACE_TERMINAL_STATE_MISMATCH';
|
|
1297
|
+
const [providerStatus, executionStatus, hasResult, minimumErrors] = expected;
|
|
1298
|
+
if (
|
|
1299
|
+
receipt.runtime_receipt.provider_execution_status !== providerStatus ||
|
|
1300
|
+
trace.execution.execution_status !== executionStatus ||
|
|
1301
|
+
trace.execution.delivery_status !== 'correlated_response' ||
|
|
1302
|
+
(trace.result_ref !== null) !== hasResult ||
|
|
1303
|
+
trace.errors.length < minimumErrors
|
|
1304
|
+
) {
|
|
1305
|
+
return 'KDNA_TRACE_TERMINAL_STATE_MISMATCH';
|
|
1306
|
+
}
|
|
1307
|
+
if (trace.overall_status === 'execution_completed' && trace.errors.length !== 0) {
|
|
1308
|
+
return 'KDNA_TRACE_TERMINAL_STATE_MISMATCH';
|
|
1309
|
+
}
|
|
1310
|
+
return null;
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
function validateTrustedDeliveryObservation(context) {
|
|
1314
|
+
const observation = context.trustedDeliveryObservation;
|
|
1315
|
+
if (!['host_receipt', 'not_delivered', 'not_observed'].includes(observation)) {
|
|
1316
|
+
return invalid(
|
|
1317
|
+
'KDNA_VALIDATION_CONTEXT_INVALID',
|
|
1318
|
+
'trustedDeliveryObservation must be host_receipt, not_delivered, or not_observed.',
|
|
1319
|
+
);
|
|
1320
|
+
}
|
|
1321
|
+
if (context.receipt !== null && observation !== 'host_receipt') {
|
|
1322
|
+
return invalid(
|
|
1323
|
+
'KDNA_VALIDATION_CONTEXT_INVALID',
|
|
1324
|
+
'A correlated Host receipt requires trustedDeliveryObservation=host_receipt.',
|
|
1325
|
+
);
|
|
1326
|
+
}
|
|
1327
|
+
if (context.receipt === null && observation === 'host_receipt') {
|
|
1328
|
+
return invalid(
|
|
1329
|
+
'KDNA_VALIDATION_CONTEXT_INVALID',
|
|
1330
|
+
'trustedDeliveryObservation=host_receipt requires an independent receipt.',
|
|
1331
|
+
);
|
|
1332
|
+
}
|
|
1333
|
+
if (context.request === null && observation !== 'not_delivered') {
|
|
1334
|
+
return invalid(
|
|
1335
|
+
'KDNA_VALIDATION_CONTEXT_INVALID',
|
|
1336
|
+
'Without a Host request the trusted delivery observation must be not_delivered.',
|
|
1337
|
+
);
|
|
1338
|
+
}
|
|
1339
|
+
return ok(observation);
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
function validateTraceSemantics(
|
|
1343
|
+
trace,
|
|
1344
|
+
plan,
|
|
1345
|
+
request,
|
|
1346
|
+
receipt,
|
|
1347
|
+
capabilities,
|
|
1348
|
+
coreCapsuleVersions,
|
|
1349
|
+
trustedDeliveryObservation,
|
|
1350
|
+
) {
|
|
1351
|
+
if (
|
|
1352
|
+
trace.plan_ref.plan_id !== plan.plan_id ||
|
|
1353
|
+
trace.plan_ref.plan_digest_profile !== plan.integrity.profile ||
|
|
1354
|
+
trace.plan_ref.plan_digest_profile_version !== plan.integrity.profile_version ||
|
|
1355
|
+
trace.plan_ref.plan_digest !== plan.integrity.plan_digest ||
|
|
1356
|
+
trace.plan_ref.plan_digest !== computeConsumptionPlanDigest(plan)
|
|
1357
|
+
) {
|
|
1358
|
+
return 'KDNA_TRACE_PLAN_REF_MISMATCH';
|
|
1359
|
+
}
|
|
1360
|
+
const runtime = trace.runtime_contract;
|
|
1361
|
+
if (
|
|
1362
|
+
!equalJson(runtime.plan_capsule_versions, plan.projection_request.accepted_capsule_versions) ||
|
|
1363
|
+
!equalJson(runtime.plan_host_protocols, plan.host_request.accepted_protocols) ||
|
|
1364
|
+
!equalJson(runtime.core_capsule_versions, coreCapsuleVersions) ||
|
|
1365
|
+
!equalJson(runtime.host_capabilities, capabilities)
|
|
1366
|
+
) {
|
|
1367
|
+
return 'KDNA_TRACE_NEGOTIATION_EVIDENCE_MISMATCH';
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
if (!equalJson(trace.asset_identity, withoutExpectedDigests(plan.asset_ref))) {
|
|
1371
|
+
return 'KDNA_TRACE_ASSET_IDENTITY_MISMATCH';
|
|
1372
|
+
}
|
|
1373
|
+
if (!equalJson(trace.budget.limits, plan.budget)) return 'KDNA_TRACE_BUDGET_MISMATCH';
|
|
1374
|
+
|
|
1375
|
+
if (trace.overall_status === 'blocked') {
|
|
1376
|
+
if (!equalJson(trace.host_receipt, receipt)) return 'KDNA_TRACE_TERMINAL_STATE_MISMATCH';
|
|
1377
|
+
if (request !== null) {
|
|
1378
|
+
if (validateRequestSemantics(request, plan, false) !== null) {
|
|
1379
|
+
return 'KDNA_TRACE_REQUEST_MISMATCH';
|
|
1380
|
+
}
|
|
1381
|
+
const negotiation = negotiateRuntimePair(plan, {
|
|
1382
|
+
trustedPlanDigest: plan.integrity.plan_digest,
|
|
1383
|
+
capabilities,
|
|
1384
|
+
coreCapsuleVersions,
|
|
1385
|
+
});
|
|
1386
|
+
if (
|
|
1387
|
+
negotiation.state !== 'selected' ||
|
|
1388
|
+
runtime.negotiation_state !== 'selected' ||
|
|
1389
|
+
runtime.selected_capsule_version !== negotiation.capsule_version ||
|
|
1390
|
+
runtime.selected_host_protocol !== negotiation.host_protocol ||
|
|
1391
|
+
runtime.issue_code !== null
|
|
1392
|
+
) {
|
|
1393
|
+
return 'KDNA_TRACE_NEGOTIATION_EVIDENCE_MISMATCH';
|
|
1394
|
+
}
|
|
1395
|
+
if (
|
|
1396
|
+
!equalJson(trace.asset_identity, withoutExpectedDigests(request.asset)) ||
|
|
1397
|
+
!equalJson(trace.digest_evidence, request.capsule.digests)
|
|
1398
|
+
) {
|
|
1399
|
+
return 'KDNA_TRACE_DIGEST_EVIDENCE_MISMATCH';
|
|
1400
|
+
}
|
|
1401
|
+
const p = request.runtime_contract.capsule_delivery_digest;
|
|
1402
|
+
const delivery = trace.capsule_delivery_evidence;
|
|
1403
|
+
const projectionMismatch =
|
|
1404
|
+
trace.projection_actual.profile !== request.projection_contract.profile ||
|
|
1405
|
+
trace.projection_actual.capsule_delivery_digest !== p ||
|
|
1406
|
+
trace.projection_actual.profile_deviated_from_plan !== false;
|
|
1407
|
+
if (delivery.observed !== p || delivery.sender_computed !== true || projectionMismatch) {
|
|
1408
|
+
return 'KDNA_TRACE_UNDELIVERED_HOST_EVIDENCE';
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
if (delivery.host_boundary_comparison === 'mismatched') {
|
|
1412
|
+
if (
|
|
1413
|
+
trustedDeliveryObservation !== 'host_receipt' ||
|
|
1414
|
+
receipt === null ||
|
|
1415
|
+
validateReceiptSemantics(receipt, request) !== null ||
|
|
1416
|
+
receipt.runtime_receipt.capsule_delivery_comparison !== 'mismatched' ||
|
|
1417
|
+
delivery.host_recomputed !==
|
|
1418
|
+
receipt.runtime_receipt.host_recomputed_capsule_delivery_digest ||
|
|
1419
|
+
delivery.host_echoed !== receipt.runtime_receipt.echoed_capsule_delivery_digest ||
|
|
1420
|
+
delivery.host_recomputed === p ||
|
|
1421
|
+
delivery.delivered_capsule_version !== CAPSULE_CONTRACT_VERSION ||
|
|
1422
|
+
delivery.request_id !== request.request_id ||
|
|
1423
|
+
trace.execution.delivery_status !== 'rejected_before_execution'
|
|
1424
|
+
) {
|
|
1425
|
+
return 'KDNA_TRACE_CAPSULE_DELIVERY_DIGEST_MISMATCH';
|
|
1426
|
+
}
|
|
1427
|
+
} else if (delivery.host_boundary_comparison === 'not_observed') {
|
|
1428
|
+
if (
|
|
1429
|
+
trustedDeliveryObservation !== 'not_observed' ||
|
|
1430
|
+
receipt !== null ||
|
|
1431
|
+
delivery.host_recomputed !== null ||
|
|
1432
|
+
delivery.host_echoed !== null ||
|
|
1433
|
+
delivery.delivered_capsule_version !== CAPSULE_CONTRACT_VERSION ||
|
|
1434
|
+
delivery.request_id !== request.request_id ||
|
|
1435
|
+
trace.execution.delivery_status !== 'rejected_before_execution'
|
|
1436
|
+
) {
|
|
1437
|
+
return 'KDNA_TRACE_UNDELIVERED_HOST_EVIDENCE';
|
|
1438
|
+
}
|
|
1439
|
+
} else {
|
|
1440
|
+
if (trustedDeliveryObservation !== 'not_delivered') {
|
|
1441
|
+
return 'KDNA_TRACE_DELIVERY_OBSERVATION_MISMATCH';
|
|
1442
|
+
}
|
|
1443
|
+
if (
|
|
1444
|
+
receipt !== null ||
|
|
1445
|
+
delivery.host_recomputed !== null ||
|
|
1446
|
+
delivery.host_echoed !== null ||
|
|
1447
|
+
delivery.delivered_capsule_version !== null ||
|
|
1448
|
+
delivery.host_boundary_comparison !== 'not_delivered' ||
|
|
1449
|
+
delivery.request_id !== null ||
|
|
1450
|
+
trace.execution.delivery_status !== 'not_delivered'
|
|
1451
|
+
) {
|
|
1452
|
+
return 'KDNA_TRACE_UNDELIVERED_HOST_EVIDENCE';
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
const expectedBudget = expectedBudgetEvidence(plan, request, receipt);
|
|
1457
|
+
if (!equalJson(trace.budget, expectedBudget)) return 'KDNA_TRACE_BUDGET_MISMATCH';
|
|
1458
|
+
return validateTerminalState(trace, null);
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
if (
|
|
1462
|
+
trustedDeliveryObservation !== 'not_delivered' ||
|
|
1463
|
+
runtime.selected_capsule_version !== null ||
|
|
1464
|
+
runtime.selected_host_protocol !== null ||
|
|
1465
|
+
!['not_started', 'blocked'].includes(runtime.negotiation_state) ||
|
|
1466
|
+
trace.capsule_delivery_evidence.host_recomputed !== null ||
|
|
1467
|
+
trace.capsule_delivery_evidence.host_echoed !== null ||
|
|
1468
|
+
trace.capsule_delivery_evidence.host_boundary_comparison !== 'unavailable' ||
|
|
1469
|
+
trace.projection_actual.profile !== null ||
|
|
1470
|
+
trace.projection_actual.capsule_delivery_digest !== null ||
|
|
1471
|
+
trace.projection_actual.profile_deviated_from_plan !== null
|
|
1472
|
+
) {
|
|
1473
|
+
return 'KDNA_TRACE_UNDELIVERED_HOST_EVIDENCE';
|
|
1474
|
+
}
|
|
1475
|
+
for (const name of ['asset', 'content', 'runtime_entry_set']) {
|
|
1476
|
+
if (
|
|
1477
|
+
trace.digest_evidence[name].value !== null ||
|
|
1478
|
+
trace.digest_evidence[name].comparison.state !== 'unavailable'
|
|
1479
|
+
) {
|
|
1480
|
+
return 'KDNA_TRACE_UNDELIVERED_HOST_EVIDENCE';
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
if (runtime.negotiation_state === 'not_started' && runtime.issue_code !== null) {
|
|
1484
|
+
return 'KDNA_TRACE_NEGOTIATION_EVIDENCE_MISMATCH';
|
|
1485
|
+
}
|
|
1486
|
+
if (runtime.negotiation_state === 'blocked') {
|
|
1487
|
+
const negotiation = negotiateRuntimePair(plan, {
|
|
1488
|
+
trustedPlanDigest: plan.integrity.plan_digest,
|
|
1489
|
+
capabilities,
|
|
1490
|
+
coreCapsuleVersions,
|
|
1491
|
+
});
|
|
1492
|
+
if (negotiation.state !== 'blocked' || runtime.issue_code !== negotiation.issue_code) {
|
|
1493
|
+
return 'KDNA_TRACE_NEGOTIATION_EVIDENCE_MISMATCH';
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
if (receipt !== null || trace.execution.delivery_status !== 'not_delivered') {
|
|
1497
|
+
return 'KDNA_TRACE_UNDELIVERED_HOST_EVIDENCE';
|
|
1498
|
+
}
|
|
1499
|
+
const expectedBudget = expectedBudgetEvidence(plan, null, null);
|
|
1500
|
+
if (!equalJson(trace.budget, expectedBudget)) return 'KDNA_TRACE_BUDGET_MISMATCH';
|
|
1501
|
+
return validateTerminalState(trace, null);
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
if (
|
|
1505
|
+
request === null ||
|
|
1506
|
+
receipt === null ||
|
|
1507
|
+
trustedDeliveryObservation !== 'host_receipt'
|
|
1508
|
+
) {
|
|
1509
|
+
return 'KDNA_TRACE_TERMINAL_STATE_MISMATCH';
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
const requestError = validateRequestSemantics(request, plan);
|
|
1513
|
+
if (requestError === 'KDNA_HOST_BUDGET_LIMIT_EXCEEDED') {
|
|
1514
|
+
return 'KDNA_TRACE_BUDGET_LIMIT_VIOLATION';
|
|
1515
|
+
}
|
|
1516
|
+
if (requestError !== null) return 'KDNA_TRACE_REQUEST_MISMATCH';
|
|
1517
|
+
if (validateReceiptSemantics(receipt, request) !== null) {
|
|
1518
|
+
return 'KDNA_TRACE_HOST_RECEIPT_MISMATCH';
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
const negotiation = negotiateRuntimePair(plan, {
|
|
1522
|
+
trustedPlanDigest: plan.integrity.plan_digest,
|
|
1523
|
+
capabilities,
|
|
1524
|
+
coreCapsuleVersions,
|
|
1525
|
+
});
|
|
1526
|
+
if (
|
|
1527
|
+
negotiation.state !== 'selected' ||
|
|
1528
|
+
runtime.negotiation_state !== negotiation.state ||
|
|
1529
|
+
runtime.selected_capsule_version !== negotiation.capsule_version ||
|
|
1530
|
+
runtime.selected_host_protocol !== negotiation.host_protocol ||
|
|
1531
|
+
runtime.issue_code !== null
|
|
1532
|
+
) {
|
|
1533
|
+
return 'KDNA_TRACE_NEGOTIATION_EVIDENCE_MISMATCH';
|
|
1534
|
+
}
|
|
1535
|
+
if (!runtime.core_capsule_versions.includes(runtime.selected_capsule_version)) {
|
|
1536
|
+
return 'KDNA_TRACE_NEGOTIATION_EVIDENCE_MISMATCH';
|
|
1537
|
+
}
|
|
1538
|
+
if (!equalJson(trace.asset_identity, withoutExpectedDigests(request.asset))) {
|
|
1539
|
+
return 'KDNA_TRACE_ASSET_IDENTITY_MISMATCH';
|
|
1540
|
+
}
|
|
1541
|
+
if (!equalJson(trace.digest_evidence, request.capsule.digests)) {
|
|
1542
|
+
return 'KDNA_TRACE_DIGEST_EVIDENCE_MISMATCH';
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
const p = request.runtime_contract.capsule_delivery_digest;
|
|
1546
|
+
const delivery = trace.capsule_delivery_evidence;
|
|
1547
|
+
const runtimeReceipt = receipt.runtime_receipt;
|
|
1548
|
+
if (
|
|
1549
|
+
delivery.observed !== p ||
|
|
1550
|
+
delivery.host_boundary_comparison !== 'matched' ||
|
|
1551
|
+
delivery.host_recomputed !== runtimeReceipt.host_recomputed_capsule_delivery_digest ||
|
|
1552
|
+
delivery.host_recomputed !== p ||
|
|
1553
|
+
delivery.host_echoed !== runtimeReceipt.echoed_capsule_delivery_digest ||
|
|
1554
|
+
delivery.host_echoed !== p ||
|
|
1555
|
+
delivery.delivered_capsule_version !== request.runtime_contract.capsule_version ||
|
|
1556
|
+
delivery.request_id !== request.request_id ||
|
|
1557
|
+
trace.projection_actual.profile !== request.projection_contract.profile ||
|
|
1558
|
+
trace.projection_actual.capsule_delivery_digest !== p ||
|
|
1559
|
+
trace.projection_actual.profile_deviated_from_plan !== false
|
|
1560
|
+
) {
|
|
1561
|
+
return 'KDNA_TRACE_CAPSULE_DELIVERY_DIGEST_MISMATCH';
|
|
1562
|
+
}
|
|
1563
|
+
if (!equalJson(trace.host_receipt, receipt)) return 'KDNA_TRACE_HOST_RECEIPT_MISMATCH';
|
|
1564
|
+
if (
|
|
1565
|
+
!equalJson(trace.execution.semantic_consumption, runtimeReceipt.semantic_consumption) ||
|
|
1566
|
+
!equalJson(trace.execution.model_identity, runtimeReceipt.model_identity)
|
|
1567
|
+
) {
|
|
1568
|
+
return 'KDNA_TRACE_EXECUTION_EVIDENCE_MISMATCH';
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
const expectedBudget = expectedBudgetEvidence(plan, request, receipt);
|
|
1572
|
+
if (!equalJson(trace.budget, expectedBudget)) return 'KDNA_TRACE_BUDGET_MISMATCH';
|
|
1573
|
+
if (trace.overall_status !== 'timed_out' && expectedBudget.comparison.overall === 'exceeded') {
|
|
1574
|
+
return 'KDNA_TRACE_BUDGET_LIMIT_VIOLATION';
|
|
1575
|
+
}
|
|
1576
|
+
if (trace.overall_status === 'timed_out' && expectedBudget.comparison.elapsed_ms !== 'exceeded') {
|
|
1577
|
+
return 'KDNA_TRACE_BUDGET_MISMATCH';
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
if (trace.result_ref !== null) {
|
|
1581
|
+
if (
|
|
1582
|
+
trace.result_ref.shape !== request.result_contract.shape ||
|
|
1583
|
+
receipt.outcome === null ||
|
|
1584
|
+
trace.result_ref.result_digest !== digestJcs(receipt.outcome)
|
|
1585
|
+
) {
|
|
1586
|
+
return 'KDNA_TRACE_RESULT_DIGEST_MISMATCH';
|
|
1587
|
+
}
|
|
1588
|
+
} else if (receipt.outcome !== null) {
|
|
1589
|
+
return 'KDNA_TRACE_RESULT_DIGEST_MISMATCH';
|
|
1590
|
+
}
|
|
1591
|
+
return validateTerminalState(trace, receipt);
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
function validateJudgmentTrace(trace, context) {
|
|
1595
|
+
const contextResult = snapshotContext(context, [
|
|
1596
|
+
'plan',
|
|
1597
|
+
'trustedPlanDigest',
|
|
1598
|
+
'capabilities',
|
|
1599
|
+
'coreCapsuleVersions',
|
|
1600
|
+
'request',
|
|
1601
|
+
'receipt',
|
|
1602
|
+
'trustedDeliveryObservation',
|
|
1603
|
+
]);
|
|
1604
|
+
if (!contextResult.valid) return contextResult;
|
|
1605
|
+
const safeContext = contextResult.value;
|
|
1606
|
+
const observationResult = validateTrustedDeliveryObservation(safeContext);
|
|
1607
|
+
if (!observationResult.valid) return observationResult;
|
|
1608
|
+
const schemaResult = validateSchema('trace', trace);
|
|
1609
|
+
if (!schemaResult.valid) return schemaResult;
|
|
1610
|
+
const safeTrace = schemaResult.value;
|
|
1611
|
+
const planResult = validateConsumptionPlan(safeContext.plan, {
|
|
1612
|
+
trustedPlanDigest: safeContext.trustedPlanDigest,
|
|
1613
|
+
});
|
|
1614
|
+
if (!planResult.valid) return planResult;
|
|
1615
|
+
safeContext.plan = planResult.value;
|
|
1616
|
+
const capabilitiesResult = validateSchema('capabilities', safeContext.capabilities);
|
|
1617
|
+
if (!capabilitiesResult.valid) {
|
|
1618
|
+
return invalid(
|
|
1619
|
+
'KDNA_VALIDATION_CONTEXT_INVALID',
|
|
1620
|
+
'Trace validation requires schema-valid independent Host capabilities.',
|
|
1621
|
+
capabilitiesResult.errors,
|
|
1622
|
+
);
|
|
1623
|
+
}
|
|
1624
|
+
safeContext.capabilities = capabilitiesResult.value;
|
|
1625
|
+
if (safeContext.request !== null) {
|
|
1626
|
+
const requestSchemaResult = validateSchema('request', safeContext.request);
|
|
1627
|
+
if (!requestSchemaResult.valid) {
|
|
1628
|
+
return invalid(
|
|
1629
|
+
'KDNA_VALIDATION_CONTEXT_INVALID',
|
|
1630
|
+
'Trace validation requires a schema-valid independent Host request.',
|
|
1631
|
+
requestSchemaResult.errors,
|
|
1632
|
+
);
|
|
1633
|
+
}
|
|
1634
|
+
safeContext.request = requestSchemaResult.value;
|
|
1635
|
+
}
|
|
1636
|
+
if (safeContext.receipt !== null) {
|
|
1637
|
+
if (safeContext.request === null) {
|
|
1638
|
+
return invalid(
|
|
1639
|
+
'KDNA_VALIDATION_CONTEXT_INVALID',
|
|
1640
|
+
'Trace validation cannot correlate a receipt without its independent request.',
|
|
1641
|
+
);
|
|
1642
|
+
}
|
|
1643
|
+
const receiptResult = validateAgentHostReceipt(safeContext.receipt, {
|
|
1644
|
+
request: safeContext.request,
|
|
1645
|
+
});
|
|
1646
|
+
if (!receiptResult.valid) {
|
|
1647
|
+
if (receiptResult.code === 'KDNA_HOST_CAPSULE_DELIVERY_DIGEST_MISMATCH') {
|
|
1648
|
+
return invalid(
|
|
1649
|
+
'KDNA_TRACE_CAPSULE_DELIVERY_DIGEST_MISMATCH',
|
|
1650
|
+
'JudgmentTrace receipt context contains inconsistent Capsule delivery evidence.',
|
|
1651
|
+
receiptResult.errors,
|
|
1652
|
+
);
|
|
1653
|
+
}
|
|
1654
|
+
return invalid(
|
|
1655
|
+
'KDNA_VALIDATION_CONTEXT_INVALID',
|
|
1656
|
+
'Trace validation requires a valid independently correlated Host receipt.',
|
|
1657
|
+
receiptResult.errors,
|
|
1658
|
+
);
|
|
1659
|
+
}
|
|
1660
|
+
safeContext.receipt = receiptResult.value;
|
|
1661
|
+
}
|
|
1662
|
+
try {
|
|
1663
|
+
const code = validateTraceSemantics(
|
|
1664
|
+
safeTrace,
|
|
1665
|
+
safeContext.plan,
|
|
1666
|
+
safeContext.request,
|
|
1667
|
+
safeContext.receipt,
|
|
1668
|
+
safeContext.capabilities,
|
|
1669
|
+
safeContext.coreCapsuleVersions,
|
|
1670
|
+
safeContext.trustedDeliveryObservation,
|
|
1671
|
+
);
|
|
1672
|
+
return code ? invalid(code, 'JudgmentTrace correlation failed.') : ok(safeTrace);
|
|
1673
|
+
} catch (error) {
|
|
1674
|
+
return invalid('KDNA_INPUT_INVALID', 'JudgmentTrace is not a safe JSON value.', {
|
|
1675
|
+
cause: error && typeof error.message === 'string' ? error.message : String(error),
|
|
1676
|
+
});
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
function buildJudgmentTrace(input, context) {
|
|
1681
|
+
const contextResult = snapshotContext(context, [
|
|
1682
|
+
'plan',
|
|
1683
|
+
'trustedPlanDigest',
|
|
1684
|
+
'capabilities',
|
|
1685
|
+
'coreCapsuleVersions',
|
|
1686
|
+
'request',
|
|
1687
|
+
'receipt',
|
|
1688
|
+
'trustedDeliveryObservation',
|
|
1689
|
+
]);
|
|
1690
|
+
throwInvalid(contextResult);
|
|
1691
|
+
const safeContext = contextResult.value;
|
|
1692
|
+
throwInvalid(validateTrustedDeliveryObservation(safeContext));
|
|
1693
|
+
const planResult = validateConsumptionPlan(safeContext.plan, {
|
|
1694
|
+
trustedPlanDigest: safeContext.trustedPlanDigest,
|
|
1695
|
+
});
|
|
1696
|
+
throwInvalid(planResult);
|
|
1697
|
+
safeContext.plan = planResult.value;
|
|
1698
|
+
|
|
1699
|
+
const capabilitiesResult = validateSchema('capabilities', safeContext.capabilities);
|
|
1700
|
+
if (!capabilitiesResult.valid) {
|
|
1701
|
+
throw new KDNARuntimeContractError(
|
|
1702
|
+
'KDNA_VALIDATION_CONTEXT_INVALID',
|
|
1703
|
+
'Trace construction requires schema-valid independent Host capabilities.',
|
|
1704
|
+
capabilitiesResult.errors,
|
|
1705
|
+
);
|
|
1706
|
+
}
|
|
1707
|
+
safeContext.capabilities = capabilitiesResult.value;
|
|
1708
|
+
if (safeContext.request !== null) {
|
|
1709
|
+
const requestResult = validateSchema('request', safeContext.request);
|
|
1710
|
+
if (!requestResult.valid) {
|
|
1711
|
+
throw new KDNARuntimeContractError(
|
|
1712
|
+
'KDNA_VALIDATION_CONTEXT_INVALID',
|
|
1713
|
+
'Trace construction requires a schema-valid independent Host request.',
|
|
1714
|
+
requestResult.errors,
|
|
1715
|
+
);
|
|
1716
|
+
}
|
|
1717
|
+
safeContext.request = requestResult.value;
|
|
1718
|
+
const requestCode = validateRequestSemantics(
|
|
1719
|
+
safeContext.request,
|
|
1720
|
+
safeContext.plan,
|
|
1721
|
+
false,
|
|
1722
|
+
);
|
|
1723
|
+
if (requestCode !== null) {
|
|
1724
|
+
throw new KDNARuntimeContractError(
|
|
1725
|
+
requestCode,
|
|
1726
|
+
'Trace construction requires an independently correlated Host request.',
|
|
1727
|
+
);
|
|
1728
|
+
}
|
|
1729
|
+
const negotiation = negotiateRuntimePair(safeContext.plan, safeContext);
|
|
1730
|
+
if (negotiation.state !== 'selected') {
|
|
1731
|
+
throw new KDNARuntimeContractError(
|
|
1732
|
+
negotiation.issue_code,
|
|
1733
|
+
'Trace construction requires a selected strict execution pair.',
|
|
1734
|
+
);
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
if (safeContext.receipt !== null) {
|
|
1738
|
+
if (safeContext.request === null) {
|
|
1739
|
+
throw new KDNARuntimeContractError(
|
|
1740
|
+
'KDNA_VALIDATION_CONTEXT_INVALID',
|
|
1741
|
+
'Trace construction cannot use a receipt without its independent request.',
|
|
1742
|
+
);
|
|
1743
|
+
}
|
|
1744
|
+
const receiptResult = validateAgentHostReceipt(safeContext.receipt, {
|
|
1745
|
+
request: safeContext.request,
|
|
1746
|
+
});
|
|
1747
|
+
throwInvalid(receiptResult);
|
|
1748
|
+
safeContext.receipt = receiptResult.value;
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
const plan = safeContext.plan;
|
|
1752
|
+
const request = safeContext.request;
|
|
1753
|
+
const receipt = safeContext.receipt;
|
|
1754
|
+
const observation = safeContext.trustedDeliveryObservation;
|
|
1755
|
+
const negotiation = negotiateRuntimePair(plan, safeContext);
|
|
1756
|
+
const hasRequest = request !== null;
|
|
1757
|
+
const hasReceipt = receipt !== null;
|
|
1758
|
+
const comparison = hasReceipt
|
|
1759
|
+
? receipt.runtime_receipt.capsule_delivery_comparison
|
|
1760
|
+
: hasRequest
|
|
1761
|
+
? observation
|
|
1762
|
+
: 'unavailable';
|
|
1763
|
+
const p = hasRequest ? request.runtime_contract.capsule_delivery_digest : null;
|
|
1764
|
+
const matched = comparison === 'matched';
|
|
1765
|
+
|
|
1766
|
+
let trace;
|
|
1767
|
+
try {
|
|
1768
|
+
trace = {
|
|
1769
|
+
type: 'kdna.judgment-trace',
|
|
1770
|
+
contract_version: '0.1.0',
|
|
1771
|
+
trace_id: input.trace_id,
|
|
1772
|
+
plan_ref: {
|
|
1773
|
+
plan_id: plan.plan_id,
|
|
1774
|
+
plan_digest_profile: plan.integrity.profile,
|
|
1775
|
+
plan_digest_profile_version: plan.integrity.profile_version,
|
|
1776
|
+
plan_digest: plan.integrity.plan_digest,
|
|
1777
|
+
comparison: 'matched',
|
|
1778
|
+
},
|
|
1779
|
+
parent_trace_id: input.parent_trace_id ?? null,
|
|
1780
|
+
timestamp: input.timestamp,
|
|
1781
|
+
overall_status: input.overall_status,
|
|
1782
|
+
runtime_contract: {
|
|
1783
|
+
plan_capsule_versions: clone(plan.projection_request.accepted_capsule_versions),
|
|
1784
|
+
core_capsule_versions: clone(safeContext.coreCapsuleVersions),
|
|
1785
|
+
plan_host_protocols: clone(plan.host_request.accepted_protocols),
|
|
1786
|
+
host_capabilities: clone(safeContext.capabilities),
|
|
1787
|
+
negotiation_state: hasRequest
|
|
1788
|
+
? 'selected'
|
|
1789
|
+
: negotiation.state === 'selected'
|
|
1790
|
+
? 'not_started'
|
|
1791
|
+
: 'blocked',
|
|
1792
|
+
selected_capsule_version: hasRequest ? negotiation.capsule_version : null,
|
|
1793
|
+
selected_host_protocol: hasRequest ? negotiation.host_protocol : null,
|
|
1794
|
+
issue_code: hasRequest || negotiation.state === 'selected' ? null : negotiation.issue_code,
|
|
1795
|
+
},
|
|
1796
|
+
asset_identity: withoutExpectedDigests(plan.asset_ref),
|
|
1797
|
+
digest_evidence: hasRequest ? clone(request.capsule.digests) : unavailableDigestEvidence(),
|
|
1798
|
+
capsule_delivery_evidence: {
|
|
1799
|
+
basis: CAPSULE_DELIVERY_PROFILE,
|
|
1800
|
+
basis_version: CAPSULE_DELIVERY_PROFILE_VERSION,
|
|
1801
|
+
observed: p,
|
|
1802
|
+
sender_computed: hasRequest,
|
|
1803
|
+
host_recomputed: hasReceipt
|
|
1804
|
+
? receipt.runtime_receipt.host_recomputed_capsule_delivery_digest
|
|
1805
|
+
: null,
|
|
1806
|
+
host_echoed: hasReceipt
|
|
1807
|
+
? receipt.runtime_receipt.echoed_capsule_delivery_digest
|
|
1808
|
+
: null,
|
|
1809
|
+
delivered_capsule_version:
|
|
1810
|
+
hasReceipt || observation === 'not_observed' ? CAPSULE_CONTRACT_VERSION : null,
|
|
1811
|
+
host_boundary_comparison: comparison,
|
|
1812
|
+
request_id: hasReceipt || observation === 'not_observed' ? request.request_id : null,
|
|
1813
|
+
},
|
|
1814
|
+
projection_actual: {
|
|
1815
|
+
profile: hasRequest ? request.projection_contract.profile : null,
|
|
1816
|
+
capsule_delivery_digest: p,
|
|
1817
|
+
profile_deviated_from_plan: hasRequest ? false : null,
|
|
1818
|
+
},
|
|
1819
|
+
host_receipt: hasReceipt ? clone(receipt) : null,
|
|
1820
|
+
execution: {
|
|
1821
|
+
delivery_status: matched
|
|
1822
|
+
? 'correlated_response'
|
|
1823
|
+
: hasReceipt || observation === 'not_observed'
|
|
1824
|
+
? 'rejected_before_execution'
|
|
1825
|
+
: 'not_delivered',
|
|
1826
|
+
semantic_consumption: matched
|
|
1827
|
+
? clone(receipt.runtime_receipt.semantic_consumption)
|
|
1828
|
+
: { state: 'not_observed', basis: null },
|
|
1829
|
+
execution_status: matched
|
|
1830
|
+
? receipt.runtime_receipt.provider_execution_status
|
|
1831
|
+
: 'not_started',
|
|
1832
|
+
conformance_status: 'not_evaluated',
|
|
1833
|
+
model_identity: matched
|
|
1834
|
+
? clone(receipt.runtime_receipt.model_identity)
|
|
1835
|
+
: { value: null, basis: 'not_observed' },
|
|
1836
|
+
},
|
|
1837
|
+
budget: expectedBudgetEvidence(plan, request, receipt),
|
|
1838
|
+
result_ref:
|
|
1839
|
+
matched && receipt.outcome !== null
|
|
1840
|
+
? {
|
|
1841
|
+
shape: request.result_contract.shape,
|
|
1842
|
+
result_digest: digestJcs(receipt.outcome),
|
|
1843
|
+
basis: 'kdna.canonicalization.result-jcs',
|
|
1844
|
+
stored: input.result_stored ?? true,
|
|
1845
|
+
}
|
|
1846
|
+
: null,
|
|
1847
|
+
errors: clone(input.errors || []),
|
|
1848
|
+
warnings: clone(input.warnings || []),
|
|
1849
|
+
};
|
|
1850
|
+
} catch {
|
|
1851
|
+
throw new KDNARuntimeContractError(
|
|
1852
|
+
'KDNA_TRACE_INPUT_INVALID',
|
|
1853
|
+
'Cannot build JudgmentTrace from the supplied input and independent context.',
|
|
1854
|
+
);
|
|
1855
|
+
}
|
|
1856
|
+
|
|
1857
|
+
const result = validateJudgmentTrace(trace, safeContext);
|
|
1858
|
+
throwInvalid(result);
|
|
1859
|
+
return trace;
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1862
|
+
module.exports = {
|
|
1863
|
+
KDNARuntimeContractError,
|
|
1864
|
+
PLAN_DIGEST_PROFILE,
|
|
1865
|
+
PLAN_DIGEST_PROFILE_VERSION,
|
|
1866
|
+
HOST_PROTOCOL,
|
|
1867
|
+
HOST_PROTOCOL_VERSION,
|
|
1868
|
+
DEFAULT_CORE_CAPSULE_CONTRACT_VERSIONS,
|
|
1869
|
+
parseRuntimeContractJson,
|
|
1870
|
+
computeConsumptionPlanDigest,
|
|
1871
|
+
buildConsumptionPlan,
|
|
1872
|
+
validateConsumptionPlan,
|
|
1873
|
+
negotiateRuntimePair,
|
|
1874
|
+
buildAgentHostRequest,
|
|
1875
|
+
validateAgentHostRequest,
|
|
1876
|
+
validateAgentHostReceipt,
|
|
1877
|
+
deriveBudgetEvidence,
|
|
1878
|
+
buildPreHostBudgetBlockedTrace,
|
|
1879
|
+
validatePreHostBudgetBlockedTrace,
|
|
1880
|
+
buildJudgmentTrace,
|
|
1881
|
+
validateJudgmentTrace,
|
|
1882
|
+
};
|