@neurcode-ai/governance-runtime 0.1.2 → 0.1.4

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.
Files changed (54) hide show
  1. package/LICENSE +201 -0
  2. package/dist/admission-provenance.d.ts +111 -0
  3. package/dist/admission-provenance.d.ts.map +1 -0
  4. package/dist/admission-provenance.js +735 -0
  5. package/dist/admission-provenance.js.map +1 -0
  6. package/dist/agent-guard-posture.d.ts +40 -0
  7. package/dist/agent-guard-posture.d.ts.map +1 -0
  8. package/dist/agent-guard-posture.js +117 -0
  9. package/dist/agent-guard-posture.js.map +1 -0
  10. package/dist/agent-invocation-observability.d.ts +47 -0
  11. package/dist/agent-invocation-observability.d.ts.map +1 -0
  12. package/dist/agent-invocation-observability.js +229 -0
  13. package/dist/agent-invocation-observability.js.map +1 -0
  14. package/dist/agent-plan.d.ts +119 -0
  15. package/dist/agent-plan.d.ts.map +1 -0
  16. package/dist/agent-plan.js +565 -0
  17. package/dist/agent-plan.js.map +1 -0
  18. package/dist/agent-runtime-adapter.d.ts +69 -0
  19. package/dist/agent-runtime-adapter.d.ts.map +1 -0
  20. package/dist/agent-runtime-adapter.js +274 -0
  21. package/dist/agent-runtime-adapter.js.map +1 -0
  22. package/dist/ai-change-record.d.ts +185 -0
  23. package/dist/ai-change-record.d.ts.map +1 -0
  24. package/dist/ai-change-record.js +580 -0
  25. package/dist/ai-change-record.js.map +1 -0
  26. package/dist/architecture-graph.d.ts +153 -0
  27. package/dist/architecture-graph.d.ts.map +1 -0
  28. package/dist/architecture-graph.js +646 -0
  29. package/dist/architecture-graph.js.map +1 -0
  30. package/dist/architecture-obligations.d.ts +153 -0
  31. package/dist/architecture-obligations.d.ts.map +1 -0
  32. package/dist/architecture-obligations.js +505 -0
  33. package/dist/architecture-obligations.js.map +1 -0
  34. package/dist/constraints.d.ts +9 -1
  35. package/dist/constraints.d.ts.map +1 -1
  36. package/dist/constraints.js +285 -34
  37. package/dist/constraints.js.map +1 -1
  38. package/dist/index.d.ts +10 -0
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +188 -10
  41. package/dist/index.js.map +1 -1
  42. package/dist/profile.d.ts +159 -0
  43. package/dist/profile.d.ts.map +1 -0
  44. package/dist/profile.js +611 -0
  45. package/dist/profile.js.map +1 -0
  46. package/dist/session.d.ts +428 -0
  47. package/dist/session.d.ts.map +1 -0
  48. package/dist/session.js +2052 -0
  49. package/dist/session.js.map +1 -0
  50. package/package.json +19 -8
  51. package/src/constraints.ts +0 -432
  52. package/src/index.test.ts +0 -382
  53. package/src/index.ts +0 -373
  54. package/tsconfig.json +0 -19
@@ -0,0 +1,735 @@
1
+ "use strict";
2
+ /**
3
+ * Runtime Admission — pure provenance core (Phase A).
4
+ *
5
+ * Deterministic, source-free. No filesystem, no shell, no network. Consumes a
6
+ * raw git tree-delta (captured elsewhere) plus a governance classification map,
7
+ * and produces the normalized delta, the governed coverage manifest, and the
8
+ * two distinct hashes:
9
+ *
10
+ * - deltaHash — exact, base-specific normalized tree-delta fingerprint.
11
+ * - coverageSetHash — squash/rebase-survivable governed-effect SET fingerprint.
12
+ *
13
+ * Coverage matching is per-entry subset membership, never global-hash equality:
14
+ * a squash/rebase that preserves file content preserves coverage identities, so
15
+ * a previously governed PR stays matchable even though its base (and therefore
16
+ * its deltaHash) changed.
17
+ *
18
+ * Eligibility is strict by default (pre-write governance only); see
19
+ * `validateSelfAttestedRecordConsistency`.
20
+ */
21
+ Object.defineProperty(exports, "__esModule", { value: true });
22
+ exports.MAX_ADMISSION_ID_LENGTH = exports.MAX_ADMISSION_PATH_LENGTH = exports.MAX_ADMISSION_SESSIONS_PER_ENTRY = exports.MAX_ADMISSION_SESSION_REFS = exports.MAX_ADMISSION_COVERAGE_ENTRIES = exports.MAX_ADMISSION_DELTA_ENTRIES = exports.MAX_ADMISSION_JSON_BYTES = void 0;
23
+ exports.sortDeltaEntries = sortDeltaEntries;
24
+ exports.sortCoverageEntries = sortCoverageEntries;
25
+ exports.normalizeDeltaEntries = normalizeDeltaEntries;
26
+ exports.deriveCoverageEntries = deriveCoverageEntries;
27
+ exports.computeDeltaHash = computeDeltaHash;
28
+ exports.computeCoverageSetHash = computeCoverageSetHash;
29
+ exports.buildCoverageManifest = buildCoverageManifest;
30
+ exports.unionCoverageEntries = unionCoverageEntries;
31
+ exports.unionCoverageManifests = unionCoverageManifests;
32
+ exports.validateSelfAttestedRecordConsistency = validateSelfAttestedRecordConsistency;
33
+ exports.readSelfAttestedAdmissionRecord = readSelfAttestedAdmissionRecord;
34
+ exports.readSelfAttestedAdmissionRecordFromText = readSelfAttestedAdmissionRecordFromText;
35
+ const node_crypto_1 = require("node:crypto");
36
+ const contracts_1 = require("@neurcode-ai/contracts");
37
+ // ── untrusted-input limits (Fix 4) ───────────────────────────────────────────
38
+ exports.MAX_ADMISSION_JSON_BYTES = 8 * 1024 * 1024;
39
+ exports.MAX_ADMISSION_DELTA_ENTRIES = 100_000;
40
+ exports.MAX_ADMISSION_COVERAGE_ENTRIES = 100_000;
41
+ exports.MAX_ADMISSION_SESSION_REFS = 4_096;
42
+ exports.MAX_ADMISSION_SESSIONS_PER_ENTRY = 4_096;
43
+ exports.MAX_ADMISSION_PATH_LENGTH = 4_096;
44
+ exports.MAX_ADMISSION_ID_LENGTH = 256;
45
+ const CHANGE_TYPES = new Set(['added', 'modified', 'deleted', 'typechanged']);
46
+ const OBJECT_TYPES = new Set(['blob', 'symlink', 'submodule', 'absent']);
47
+ const CLASSIFICATIONS = new Set([
48
+ 'governed_prewrite',
49
+ 'governed_delete',
50
+ 'observed_postwrite',
51
+ 'generated',
52
+ 'ungoverned',
53
+ ]);
54
+ const HEX_ID = /^[0-9a-f]{40}$|^[0-9a-f]{64}$/;
55
+ const HEX_32 = /^[0-9a-f]{32}$/;
56
+ const HEX_64 = /^[0-9a-f]{64}$/;
57
+ // ── hashing ─────────────────────────────────────────────────────────────────
58
+ function sha256Hex(bytes) {
59
+ return (0, node_crypto_1.createHash)('sha256').update(bytes).digest('hex');
60
+ }
61
+ // ── deterministic ordering ────────────────────────────────────────────────
62
+ // NUL separator: git paths never contain NUL, so this can never collide across
63
+ // fields. Escaped here so the source file stays plain text; the runtime value
64
+ // (and therefore every hash) is byte-identical to a literal NUL.
65
+ const SORT_SEP = '\0';
66
+ function deltaSortKey(entry) {
67
+ return (0, contracts_1.deltaEntryCanonicalFields)(entry).join(SORT_SEP);
68
+ }
69
+ function coverageSortKey(entry) {
70
+ return (0, contracts_1.coverageEntryCanonicalFields)(entry).join(SORT_SEP);
71
+ }
72
+ /** Deterministic, locale-independent string order (UTF-16 code-unit, identical everywhere). */
73
+ function byKey(a, b) {
74
+ if (a < b)
75
+ return -1;
76
+ if (a > b)
77
+ return 1;
78
+ return 0;
79
+ }
80
+ function sortDeltaEntries(entries) {
81
+ return [...entries].sort((a, b) => byKey(deltaSortKey(a), deltaSortKey(b)));
82
+ }
83
+ function sortCoverageEntries(entries) {
84
+ return [...entries].sort((a, b) => byKey(coverageSortKey(a), coverageSortKey(b)));
85
+ }
86
+ // ── normalization ───────────────────────────────────────────────────────────
87
+ function normalizeMode(mode) {
88
+ const value = (mode ?? '').trim();
89
+ if (!value)
90
+ return contracts_1.GIT_MODE_ABSENT;
91
+ if (!(0, contracts_1.isKnownGitMode)(value)) {
92
+ throw new Error(`admission: unsupported git mode "${value}"`);
93
+ }
94
+ return value;
95
+ }
96
+ function normalizeObjectId(objectId, mode, format) {
97
+ if (mode === contracts_1.GIT_MODE_ABSENT)
98
+ return (0, contracts_1.zeroObjectId)(format);
99
+ const value = (objectId ?? '').trim().toLowerCase();
100
+ if (!value || (0, contracts_1.isZeroObjectId)(value)) {
101
+ throw new Error(`admission: missing object id for present mode ${mode}`);
102
+ }
103
+ // Submodule gitlinks carry a commit id from another repo whose object format
104
+ // may differ from the super-repo, so accept either width for any present id.
105
+ if (!HEX_ID.test(value)) {
106
+ throw new Error(`admission: invalid object id "${value}"`);
107
+ }
108
+ return value;
109
+ }
110
+ function buildEntry(path, rawOldMode, rawOldId, rawNewMode, rawNewId, format) {
111
+ const cleanPath = path.replace(/^\.\//, '').trim();
112
+ if (!cleanPath)
113
+ return null;
114
+ const oldMode = normalizeMode(rawOldMode);
115
+ const newMode = normalizeMode(rawNewMode);
116
+ const oldPresent = oldMode !== contracts_1.GIT_MODE_ABSENT;
117
+ const newPresent = newMode !== contracts_1.GIT_MODE_ABSENT;
118
+ if (!oldPresent && !newPresent)
119
+ return null;
120
+ const oldObjectId = normalizeObjectId(rawOldId, oldMode, format);
121
+ const newObjectId = normalizeObjectId(rawNewId, newMode, format);
122
+ let changeType;
123
+ let objectType;
124
+ if (!oldPresent && newPresent) {
125
+ changeType = 'added';
126
+ objectType = (0, contracts_1.objectTypeForMode)(newMode);
127
+ }
128
+ else if (oldPresent && !newPresent) {
129
+ changeType = 'deleted';
130
+ objectType = (0, contracts_1.objectTypeForMode)(oldMode);
131
+ }
132
+ else {
133
+ const oldType = (0, contracts_1.objectTypeForMode)(oldMode);
134
+ const newType = (0, contracts_1.objectTypeForMode)(newMode);
135
+ changeType = oldType !== newType ? 'typechanged' : 'modified';
136
+ objectType = newType;
137
+ }
138
+ return { path: cleanPath, changeType, objectType, oldMode, newMode, oldObjectId, newObjectId };
139
+ }
140
+ function isRenameOrCopy(status) {
141
+ const value = (status ?? '').trim().toUpperCase();
142
+ if (value.startsWith('R'))
143
+ return 'rename';
144
+ if (value.startsWith('C'))
145
+ return 'copy';
146
+ return null;
147
+ }
148
+ /**
149
+ * Normalize raw capture entries into the canonical delta. Renames become a
150
+ * delete (old path) + add (new path); copies become an add only (the source is
151
+ * unchanged and not part of the tree delta). Deterministically sorted, deduped.
152
+ */
153
+ function normalizeDeltaEntries(raw, objectFormat) {
154
+ const out = [];
155
+ for (const entry of raw) {
156
+ const renameKind = isRenameOrCopy(entry.status);
157
+ const hasDistinctSource = typeof entry.oldPath === 'string' && entry.oldPath.trim() && entry.oldPath.trim() !== entry.path.trim();
158
+ if ((renameKind === 'rename' || (!entry.status && hasDistinctSource)) && entry.oldPath) {
159
+ // delete(oldPath) + add(newPath)
160
+ const del = buildEntry(entry.oldPath, entry.oldMode, entry.oldObjectId, null, null, objectFormat);
161
+ if (del)
162
+ out.push(del);
163
+ const add = buildEntry(entry.path, null, null, entry.newMode, entry.newObjectId, objectFormat);
164
+ if (add)
165
+ out.push(add);
166
+ continue;
167
+ }
168
+ if (renameKind === 'copy') {
169
+ const add = buildEntry(entry.path, null, null, entry.newMode, entry.newObjectId, objectFormat);
170
+ if (add)
171
+ out.push(add);
172
+ continue;
173
+ }
174
+ const built = buildEntry(entry.path, entry.oldMode, entry.oldObjectId, entry.newMode, entry.newObjectId, objectFormat);
175
+ if (built)
176
+ out.push(built);
177
+ }
178
+ // Dedup by full canonical fields, then sort.
179
+ const seen = new Map();
180
+ for (const entry of out) {
181
+ seen.set(deltaSortKey(entry), entry);
182
+ }
183
+ return sortDeltaEntries([...seen.values()]);
184
+ }
185
+ // ── coverage derivation ───────────────────────────────────────────────────
186
+ function coverageIdentityFields(entry) {
187
+ if (entry.changeType === 'deleted') {
188
+ return { mode: entry.oldMode, objectId: entry.oldObjectId };
189
+ }
190
+ return { mode: entry.newMode, objectId: entry.newObjectId };
191
+ }
192
+ function sortedUniqueSessions(sessions) {
193
+ return Array.from(new Set((sessions ?? []).map((s) => s.trim()).filter(Boolean))).sort(byKey);
194
+ }
195
+ /**
196
+ * Derive governed coverage entries from a normalized delta plus a per-path
197
+ * classification map. Paths with no governance evidence are 'ungoverned'.
198
+ */
199
+ function deriveCoverageEntries(delta, governance = {}) {
200
+ const entries = delta.map((entry) => {
201
+ const { mode, objectId } = coverageIdentityFields(entry);
202
+ const governedInfo = governance[entry.path];
203
+ const classification = governedInfo?.classification ?? 'ungoverned';
204
+ return {
205
+ path: entry.path,
206
+ changeType: entry.changeType,
207
+ objectType: entry.objectType,
208
+ mode,
209
+ objectId,
210
+ classification,
211
+ sessions: sortedUniqueSessions(governedInfo?.sessions),
212
+ };
213
+ });
214
+ return sortCoverageEntries(entries);
215
+ }
216
+ // ── hashes ─────────────────────────────────────────────────────────────────
217
+ function computeDeltaHash(delta, objectFormat) {
218
+ const sorted = sortDeltaEntries(delta);
219
+ const records = sorted.map((entry) => (0, contracts_1.deltaEntryCanonicalFields)(entry));
220
+ const header = [
221
+ contracts_1.ADMISSION_DELTA_HASH_DOMAIN,
222
+ contracts_1.ADMISSION_FRAMING_VERSION,
223
+ objectFormat,
224
+ String(records.length),
225
+ ];
226
+ return sha256Hex((0, contracts_1.frameRecordSet)(header, records));
227
+ }
228
+ /**
229
+ * Hash of the governed-effect identity SET. Deduped by identity (classification
230
+ * and sessions are excluded), sorted, framed. Stable across squash/rebase that
231
+ * preserve file content.
232
+ */
233
+ function computeCoverageSetHash(coverage, objectFormat) {
234
+ const byIdentity = new Map();
235
+ for (const entry of coverage) {
236
+ byIdentity.set((0, contracts_1.coverageEntryIdentityKey)(entry), entry);
237
+ }
238
+ const sorted = sortCoverageEntries([...byIdentity.values()]);
239
+ const records = sorted.map((entry) => (0, contracts_1.coverageEntryCanonicalFields)(entry));
240
+ const header = [
241
+ contracts_1.ADMISSION_COVERAGE_SET_HASH_DOMAIN,
242
+ contracts_1.ADMISSION_FRAMING_VERSION,
243
+ objectFormat,
244
+ String(records.length),
245
+ ];
246
+ return sha256Hex((0, contracts_1.frameRecordSet)(header, records));
247
+ }
248
+ function buildCoverageManifest(input) {
249
+ const delta = normalizeDeltaEntries(input.rawDelta, input.objectFormat);
250
+ const coverage = deriveCoverageEntries(delta, input.governance ?? {});
251
+ return {
252
+ schemaVersion: contracts_1.ADMISSION_COVERAGE_MANIFEST_SCHEMA_VERSION,
253
+ objectFormat: input.objectFormat,
254
+ framingVersion: contracts_1.ADMISSION_FRAMING_VERSION,
255
+ entryCount: delta.length,
256
+ deltaHash: computeDeltaHash(delta, input.objectFormat),
257
+ coverageSetHash: computeCoverageSetHash(coverage, input.objectFormat),
258
+ delta,
259
+ coverage,
260
+ };
261
+ }
262
+ // ── multi-session deterministic union ───────────────────────────────────────
263
+ const CLASSIFICATION_RANK = {
264
+ governed_prewrite: 5,
265
+ governed_delete: 4,
266
+ observed_postwrite: 3,
267
+ generated: 2,
268
+ ungoverned: 1,
269
+ };
270
+ function strongerClassification(a, b) {
271
+ return CLASSIFICATION_RANK[a] >= CLASSIFICATION_RANK[b] ? a : b;
272
+ }
273
+ /**
274
+ * Deterministically union coverage entries from multiple sessions/manifests.
275
+ * Entries sharing an identity (path + mode + objectId) merge: classification
276
+ * becomes the strongest, sessions union and sort. Distinct identities are kept
277
+ * (e.g. the same path edited to different final objects by different sessions).
278
+ */
279
+ function unionCoverageEntries(groups) {
280
+ const merged = new Map();
281
+ for (const group of groups) {
282
+ for (const entry of group) {
283
+ const key = (0, contracts_1.coverageEntryIdentityKey)(entry);
284
+ const existing = merged.get(key);
285
+ if (!existing) {
286
+ merged.set(key, {
287
+ ...entry,
288
+ sessions: sortedUniqueSessions(entry.sessions),
289
+ });
290
+ continue;
291
+ }
292
+ merged.set(key, {
293
+ ...existing,
294
+ classification: strongerClassification(existing.classification, entry.classification),
295
+ sessions: sortedUniqueSessions([...existing.sessions, ...entry.sessions]),
296
+ });
297
+ }
298
+ }
299
+ return sortCoverageEntries([...merged.values()]);
300
+ }
301
+ function unionCoverageManifests(manifests, objectFormat) {
302
+ const coverage = unionCoverageEntries(manifests.map((m) => m.coverage));
303
+ return { coverage, coverageSetHash: computeCoverageSetHash(coverage, objectFormat) };
304
+ }
305
+ // ── self-attested record consistency ────────────────────────────────────────
306
+ function consistencyBase() {
307
+ return {
308
+ schemaVersion: contracts_1.ADMISSION_CONSISTENCY_DECISION_SCHEMA_VERSION,
309
+ trustLevel: 'self-attested',
310
+ notProof: true,
311
+ };
312
+ }
313
+ function inconsistentDecision(reason) {
314
+ return {
315
+ ...consistencyBase(),
316
+ verdict: 'self_attested_inconsistent',
317
+ deltaHashMatches: false,
318
+ coveredPaths: [],
319
+ uncoveredPaths: [],
320
+ unexpectedCoverage: [],
321
+ reasons: [reason],
322
+ };
323
+ }
324
+ /**
325
+ * Validate a self-attested record against a recomputed ground-truth delta.
326
+ *
327
+ * The coverage verdict is decided by per-entry subset matching of the
328
+ * ground-truth identities against the record's ADMISSIBLE coverage entries —
329
+ * NOT by deltaHash equality (which is base-specific and breaks under
330
+ * squash/rebase). `deltaHashMatches` is a diagnostic only.
331
+ *
332
+ * Eligibility defaults to STRICT (pre-write governance only): `observed_postwrite`
333
+ * and `generated` do not satisfy admission unless `options` opts in
334
+ * (`mode: 'descriptive'` or `allowGenerated: true`).
335
+ *
336
+ * 'self_attested_inconsistent' is reserved for a record whose own claimed hashes
337
+ * do not match its own contents (corrupted/tampered artifact). This function
338
+ * never throws: malformed input yields 'self_attested_inconsistent'.
339
+ */
340
+ function validateSelfAttestedRecordConsistency(record, groundTruthDelta, objectFormat, options = {}) {
341
+ if (!record) {
342
+ return {
343
+ ...consistencyBase(),
344
+ verdict: 'no_record',
345
+ deltaHashMatches: false,
346
+ coveredPaths: [],
347
+ uncoveredPaths: [],
348
+ unexpectedCoverage: [],
349
+ reasons: ['No self-attested admission record present for this change.'],
350
+ };
351
+ }
352
+ try {
353
+ const validatedRecord = readSelfAttestedAdmissionRecord(record);
354
+ if (!validatedRecord) {
355
+ return inconsistentDecision('Admission record failed bounded structural validation.');
356
+ }
357
+ const manifest = validatedRecord.manifest;
358
+ if (!manifest || typeof manifest !== 'object') {
359
+ return inconsistentDecision('Admission record has no manifest.');
360
+ }
361
+ const recomputedDeltaHash = computeDeltaHash(manifest.delta, manifest.objectFormat);
362
+ const recomputedCoverageSetHash = computeCoverageSetHash(manifest.coverage, manifest.objectFormat);
363
+ if (manifest.deltaHash !== recomputedDeltaHash ||
364
+ manifest.coverageSetHash !== recomputedCoverageSetHash) {
365
+ return inconsistentDecision('Admission record hashes do not match its own contents (corrupted or tampered artifact).');
366
+ }
367
+ // Ground-truth coverage identities (no governance — just what changed).
368
+ const groundTruthCoverage = deriveCoverageEntries(sortDeltaEntries(groundTruthDelta), {});
369
+ const recordByIdentity = new Map();
370
+ for (const entry of manifest.coverage) {
371
+ recordByIdentity.set((0, contracts_1.coverageEntryIdentityKey)(entry), entry);
372
+ }
373
+ const groundTruthKeys = new Set();
374
+ const coveredPaths = [];
375
+ const uncoveredPaths = [];
376
+ for (const gt of groundTruthCoverage) {
377
+ const key = (0, contracts_1.coverageEntryIdentityKey)(gt);
378
+ groundTruthKeys.add(key);
379
+ const match = recordByIdentity.get(key);
380
+ if (match && (0, contracts_1.isAdmissibleClassification)(match.classification, options)) {
381
+ coveredPaths.push(gt.path);
382
+ }
383
+ else {
384
+ uncoveredPaths.push(gt.path);
385
+ }
386
+ }
387
+ const unexpectedCoverage = [];
388
+ for (const entry of manifest.coverage) {
389
+ if (!groundTruthKeys.has((0, contracts_1.coverageEntryIdentityKey)(entry))) {
390
+ unexpectedCoverage.push(entry.path);
391
+ }
392
+ }
393
+ const deltaHashMatches = computeDeltaHash(sortDeltaEntries(groundTruthDelta), objectFormat) === manifest.deltaHash;
394
+ const verdict = uncoveredPaths.length === 0 ? 'self_attested_complete' : 'self_attested_incomplete';
395
+ const eligibility = (options.mode ?? 'strict') === 'descriptive' ? 'descriptive' : 'strict';
396
+ const reasons = [];
397
+ reasons.push(verdict === 'self_attested_complete'
398
+ ? `All ${coveredPaths.length} changed file(s) have ${eligibility}-admissible governance (self-attested).`
399
+ : `${uncoveredPaths.length} changed file(s) are not ${eligibility}-admissible (drift, post-write-only, or post-session edits).`);
400
+ if (!deltaHashMatches) {
401
+ reasons.push('Delta differs from capture base (squash/rebase/new base); matched by coverage identity.');
402
+ }
403
+ if (unexpectedCoverage.length > 0) {
404
+ reasons.push(`${unexpectedCoverage.length} governed coverage entry(ies) are not present in this change.`);
405
+ }
406
+ reasons.push('Self-attested: a claim, not proof that governance ran.');
407
+ return {
408
+ ...consistencyBase(),
409
+ verdict,
410
+ deltaHashMatches,
411
+ coveredPaths: coveredPaths.sort(byKey),
412
+ uncoveredPaths: uncoveredPaths.sort(byKey),
413
+ unexpectedCoverage: unexpectedCoverage.sort(byKey),
414
+ reasons,
415
+ };
416
+ }
417
+ catch (error) {
418
+ return inconsistentDecision(`Admission record could not be evaluated: ${error instanceof Error ? error.message : String(error)}`);
419
+ }
420
+ }
421
+ // ── hardened, bounded reading of untrusted artifacts (Fix 4) ─────────────────
422
+ function isBoundedString(value, max) {
423
+ return typeof value === 'string' && value.length <= max;
424
+ }
425
+ function isObjectIdForMode(value, mode, format) {
426
+ if (typeof value !== 'string')
427
+ return false;
428
+ if (mode === contracts_1.GIT_MODE_ABSENT)
429
+ return value === (0, contracts_1.zeroObjectId)(format);
430
+ if ((0, contracts_1.isZeroObjectId)(value))
431
+ return false;
432
+ // A submodule gitlink may point into a repo using the other object format.
433
+ return mode === '160000' ? HEX_ID.test(value) : (0, contracts_1.isValidObjectId)(value, format);
434
+ }
435
+ function validateDeltaEntry(value, format) {
436
+ if (!value || typeof value !== 'object' || Array.isArray(value))
437
+ return false;
438
+ const e = value;
439
+ if (!(isBoundedString(e.path, exports.MAX_ADMISSION_PATH_LENGTH) && e.path.length > 0 &&
440
+ typeof e.changeType === 'string' && CHANGE_TYPES.has(e.changeType) &&
441
+ typeof e.objectType === 'string' && OBJECT_TYPES.has(e.objectType) &&
442
+ typeof e.oldMode === 'string' && (0, contracts_1.isKnownGitMode)(e.oldMode) &&
443
+ typeof e.newMode === 'string' && (0, contracts_1.isKnownGitMode)(e.newMode) &&
444
+ isObjectIdForMode(e.oldObjectId, e.oldMode, format) &&
445
+ isObjectIdForMode(e.newObjectId, e.newMode, format)))
446
+ return false;
447
+ const oldPresent = e.oldMode !== contracts_1.GIT_MODE_ABSENT;
448
+ const newPresent = e.newMode !== contracts_1.GIT_MODE_ABSENT;
449
+ const expectedChangeType = !oldPresent
450
+ ? 'added'
451
+ : !newPresent
452
+ ? 'deleted'
453
+ : e.oldMode !== e.newMode
454
+ ? 'typechanged'
455
+ : 'modified';
456
+ const identityMode = newPresent ? e.newMode : e.oldMode;
457
+ return e.changeType === expectedChangeType && e.objectType === (0, contracts_1.objectTypeForMode)(identityMode);
458
+ }
459
+ function validateCoverageEntry(value, format) {
460
+ if (!value || typeof value !== 'object' || Array.isArray(value))
461
+ return false;
462
+ const e = value;
463
+ if (!(isBoundedString(e.path, exports.MAX_ADMISSION_PATH_LENGTH) && e.path.length > 0))
464
+ return false;
465
+ if (!(typeof e.changeType === 'string' && CHANGE_TYPES.has(e.changeType)))
466
+ return false;
467
+ if (!(typeof e.objectType === 'string' && OBJECT_TYPES.has(e.objectType)))
468
+ return false;
469
+ if (!(typeof e.mode === 'string' && (0, contracts_1.isKnownGitMode)(e.mode)))
470
+ return false;
471
+ if (e.mode === contracts_1.GIT_MODE_ABSENT || !isObjectIdForMode(e.objectId, e.mode, format))
472
+ return false;
473
+ if (e.objectType !== (0, contracts_1.objectTypeForMode)(e.mode))
474
+ return false;
475
+ if (!(typeof e.classification === 'string' && CLASSIFICATIONS.has(e.classification)))
476
+ return false;
477
+ if (!Array.isArray(e.sessions) || e.sessions.length > exports.MAX_ADMISSION_SESSIONS_PER_ENTRY)
478
+ return false;
479
+ if (!e.sessions.every((s) => isBoundedString(s, exports.MAX_ADMISSION_ID_LENGTH) && s.length > 0))
480
+ return false;
481
+ return e.classification === 'ungoverned' || e.sessions.length > 0;
482
+ }
483
+ function validateSessionRef(value) {
484
+ if (!value || typeof value !== 'object' || Array.isArray(value))
485
+ return false;
486
+ const r = value;
487
+ if (!(isBoundedString(r.sessionId, exports.MAX_ADMISSION_ID_LENGTH) && r.sessionId.length > 0))
488
+ return false;
489
+ if (r.replayHash !== undefined && !isBoundedString(r.replayHash, exports.MAX_ADMISSION_ID_LENGTH))
490
+ return false;
491
+ if (r.profileHash !== undefined && !isBoundedString(r.profileHash, exports.MAX_ADMISSION_ID_LENGTH))
492
+ return false;
493
+ return true;
494
+ }
495
+ function validateBoundedStringArray(value, maxItems, maxLength) {
496
+ return Array.isArray(value)
497
+ && value.length <= maxItems
498
+ && value.every((entry) => isBoundedString(entry, maxLength));
499
+ }
500
+ function validateNonNegativeInteger(value) {
501
+ return typeof value === 'number' && Number.isInteger(value) && value >= 0;
502
+ }
503
+ function validateRuntimeAdmissionTrustLevel(value) {
504
+ return value === 'unsigned_local' || value === 'self_attested' || value === 'backend_signed';
505
+ }
506
+ function validateRuntimeAdmissionReceiptSummary(value) {
507
+ if (!value || typeof value !== 'object' || Array.isArray(value))
508
+ return false;
509
+ const r = value;
510
+ if (typeof r.present !== 'boolean')
511
+ return false;
512
+ if (!validateRuntimeAdmissionTrustLevel(r.trustLevel))
513
+ return false;
514
+ if (r.receiptId !== undefined && !isBoundedString(r.receiptId, exports.MAX_ADMISSION_ID_LENGTH))
515
+ return false;
516
+ if (r.keyId !== undefined && r.keyId !== null && !isBoundedString(r.keyId, exports.MAX_ADMISSION_ID_LENGTH))
517
+ return false;
518
+ if (r.replayHash !== undefined && r.replayHash !== null && !isBoundedString(r.replayHash, exports.MAX_ADMISSION_ID_LENGTH))
519
+ return false;
520
+ if (r.signatureStatus !== undefined && r.signatureStatus !== null && !isBoundedString(r.signatureStatus, exports.MAX_ADMISSION_ID_LENGTH))
521
+ return false;
522
+ if (r.verificationStatus !== undefined && r.verificationStatus !== null && !isBoundedString(r.verificationStatus, exports.MAX_ADMISSION_ID_LENGTH))
523
+ return false;
524
+ if (r.signedAt !== undefined && r.signedAt !== null && !(isBoundedString(r.signedAt, 64) && Number.isFinite(Date.parse(r.signedAt))))
525
+ return false;
526
+ if (r.verifier !== undefined && r.verifier !== null && !isBoundedString(r.verifier, 512))
527
+ return false;
528
+ return true;
529
+ }
530
+ function validateRuntimeAdmissionContext(value) {
531
+ if (!value || typeof value !== 'object' || Array.isArray(value))
532
+ return false;
533
+ const ctx = value;
534
+ if (ctx.schemaVersion !== 'neurcode.runtime-admission-context.v1')
535
+ return false;
536
+ if (!validateRuntimeAdmissionTrustLevel(ctx.trustLevel))
537
+ return false;
538
+ if (!(isBoundedString(ctx.createdAt, 64) && Number.isFinite(Date.parse(ctx.createdAt))))
539
+ return false;
540
+ if (!(isBoundedString(ctx.sessionId, exports.MAX_ADMISSION_ID_LENGTH) && ctx.sessionId.length > 0))
541
+ return false;
542
+ if (!(isBoundedString(ctx.sessionStatus, exports.MAX_ADMISSION_ID_LENGTH) && ctx.sessionStatus.length > 0))
543
+ return false;
544
+ if (ctx.intentSummary !== null && ctx.intentSummary !== undefined && !isBoundedString(ctx.intentSummary, 512))
545
+ return false;
546
+ if (ctx.scopeMode !== null && ctx.scopeMode !== undefined && !isBoundedString(ctx.scopeMode, exports.MAX_ADMISSION_ID_LENGTH))
547
+ return false;
548
+ const agentHost = ctx.agentHost;
549
+ if (!agentHost || typeof agentHost !== 'object' || Array.isArray(agentHost))
550
+ return false;
551
+ const host = agentHost;
552
+ if (host.adapter !== null && host.adapter !== undefined && !isBoundedString(host.adapter, exports.MAX_ADMISSION_ID_LENGTH))
553
+ return false;
554
+ if (host.enforcementLevel !== null && host.enforcementLevel !== undefined && !isBoundedString(host.enforcementLevel, exports.MAX_ADMISSION_ID_LENGTH))
555
+ return false;
556
+ if (host.controlLevel !== null && host.controlLevel !== undefined && !isBoundedString(host.controlLevel, exports.MAX_ADMISSION_ID_LENGTH))
557
+ return false;
558
+ if (host.automatic !== undefined && typeof host.automatic !== 'boolean')
559
+ return false;
560
+ const counts = ctx.counts;
561
+ if (!counts || typeof counts !== 'object' || Array.isArray(counts))
562
+ return false;
563
+ for (const key of [
564
+ 'changedPaths',
565
+ 'blockedPaths',
566
+ 'suggestedApprovalPaths',
567
+ 'approvedExactPaths',
568
+ 'deniedPaths',
569
+ 'approvalRequiredSurfaces',
570
+ 'owners',
571
+ 'preWriteChecks',
572
+ 'allowedChecks',
573
+ 'warningChecks',
574
+ ]) {
575
+ if (!validateNonNegativeInteger(counts[key]))
576
+ return false;
577
+ }
578
+ const paths = ctx.paths;
579
+ if (!paths || typeof paths !== 'object' || Array.isArray(paths))
580
+ return false;
581
+ for (const key of ['changed', 'blocked', 'suggestedApproval', 'approvedExact', 'denied', 'approvalRequiredSurfaces']) {
582
+ if (!validateBoundedStringArray(paths[key], exports.MAX_ADMISSION_COVERAGE_ENTRIES, exports.MAX_ADMISSION_PATH_LENGTH))
583
+ return false;
584
+ }
585
+ if (!Array.isArray(ctx.owners) || ctx.owners.length > exports.MAX_ADMISSION_SESSION_REFS)
586
+ return false;
587
+ for (const owner of ctx.owners) {
588
+ if (!owner || typeof owner !== 'object' || Array.isArray(owner))
589
+ return false;
590
+ const item = owner;
591
+ if (!(isBoundedString(item.owner, exports.MAX_ADMISSION_ID_LENGTH) && item.owner.length > 0))
592
+ return false;
593
+ if (!validateNonNegativeInteger(item.count))
594
+ return false;
595
+ }
596
+ const guard = ctx.guard;
597
+ if (!guard || typeof guard !== 'object' || Array.isArray(guard))
598
+ return false;
599
+ const g = guard;
600
+ if (!(isBoundedString(g.status, exports.MAX_ADMISSION_ID_LENGTH) && g.status.length > 0))
601
+ return false;
602
+ for (const key of ['verifiedPrewrite', 'deniedButChanged', 'unverifiedWrites', 'observedAfterOnly']) {
603
+ if (!validateNonNegativeInteger(g[key]))
604
+ return false;
605
+ }
606
+ const integrity = ctx.integrity;
607
+ if (!integrity || typeof integrity !== 'object' || Array.isArray(integrity))
608
+ return false;
609
+ const i = integrity;
610
+ if (i.sourceFree !== true)
611
+ return false;
612
+ if (i.replayHash !== null && i.replayHash !== undefined && !isBoundedString(i.replayHash, exports.MAX_ADMISSION_ID_LENGTH))
613
+ return false;
614
+ if (i.replayHashStatus !== 'present' && i.replayHashStatus !== 'missing')
615
+ return false;
616
+ if (!(typeof i.deltaHash === 'string' && HEX_64.test(i.deltaHash)))
617
+ return false;
618
+ if (!(typeof i.coverageSetHash === 'string' && HEX_64.test(i.coverageSetHash)))
619
+ return false;
620
+ if (i.evidenceIntegrityStatus !== 'local_self_attested' &&
621
+ i.evidenceIntegrityStatus !== 'backend_signed' &&
622
+ i.evidenceIntegrityStatus !== 'unsigned_local')
623
+ return false;
624
+ if (!validateRuntimeAdmissionReceiptSummary(i.receipt))
625
+ return false;
626
+ return true;
627
+ }
628
+ /**
629
+ * Strict, bounded structural validation of an untrusted, already-parsed value.
630
+ * Returns a typed record only when every field, enum, hash, mode, array, and
631
+ * limit checks out; otherwise null. Never throws.
632
+ */
633
+ function readSelfAttestedAdmissionRecord(value) {
634
+ if (!value || typeof value !== 'object' || Array.isArray(value))
635
+ return null;
636
+ const record = value;
637
+ try {
638
+ (0, contracts_1.assertSourceFreeAdmissionValue)(value);
639
+ }
640
+ catch {
641
+ return null;
642
+ }
643
+ if (record.attestationKind !== 'self-attested')
644
+ return null;
645
+ if (record.schemaVersion !== contracts_1.SELF_ATTESTED_ADMISSION_RECORD_SCHEMA_VERSION)
646
+ return null;
647
+ if (!isBoundedString(record.admissionContractVersion, exports.MAX_ADMISSION_ID_LENGTH))
648
+ return null;
649
+ if (!isBoundedString(record.disclaimer, 4096))
650
+ return null;
651
+ if (!(isBoundedString(record.sessionId, exports.MAX_ADMISSION_ID_LENGTH) && record.sessionId.length > 0))
652
+ return null;
653
+ if (!Array.isArray(record.sessionRefs) || record.sessionRefs.length > exports.MAX_ADMISSION_SESSION_REFS)
654
+ return null;
655
+ if (!record.sessionRefs.every(validateSessionRef))
656
+ return null;
657
+ const repo = record.repo;
658
+ if (!repo || typeof repo !== 'object' || Array.isArray(repo))
659
+ return null;
660
+ const r = repo;
661
+ if (r.name !== undefined && !isBoundedString(r.name, exports.MAX_ADMISSION_PATH_LENGTH))
662
+ return null;
663
+ if (r.rootHash !== undefined && !(typeof r.rootHash === 'string' && HEX_32.test(r.rootHash)))
664
+ return null;
665
+ if (r.remoteHash !== undefined && !(typeof r.remoteHash === 'string' && HEX_32.test(r.remoteHash)))
666
+ return null;
667
+ const capture = record.capture;
668
+ if (!capture || typeof capture !== 'object' || Array.isArray(capture))
669
+ return null;
670
+ const c = capture;
671
+ if (c.mode !== 'worktree' && c.mode !== 'committed')
672
+ return null;
673
+ if (!isBoundedString(c.capturedAt, 64) || !Number.isFinite(Date.parse(c.capturedAt)))
674
+ return null;
675
+ if (c.baseRef !== undefined && !isBoundedString(c.baseRef, exports.MAX_ADMISSION_PATH_LENGTH))
676
+ return null;
677
+ if (c.headRef !== undefined && !isBoundedString(c.headRef, exports.MAX_ADMISSION_PATH_LENGTH))
678
+ return null;
679
+ const manifest = record.manifest;
680
+ if (!manifest || typeof manifest !== 'object' || Array.isArray(manifest))
681
+ return null;
682
+ const m = manifest;
683
+ if (m.schemaVersion !== contracts_1.ADMISSION_COVERAGE_MANIFEST_SCHEMA_VERSION)
684
+ return null;
685
+ if (m.objectFormat !== 'sha1' && m.objectFormat !== 'sha256')
686
+ return null;
687
+ if (!isBoundedString(m.framingVersion, exports.MAX_ADMISSION_ID_LENGTH))
688
+ return null;
689
+ if (typeof m.entryCount !== 'number' || !Number.isInteger(m.entryCount) || m.entryCount < 0)
690
+ return null;
691
+ if (!(typeof m.deltaHash === 'string' && HEX_64.test(m.deltaHash)))
692
+ return null;
693
+ if (!(typeof m.coverageSetHash === 'string' && HEX_64.test(m.coverageSetHash)))
694
+ return null;
695
+ if (!Array.isArray(m.delta) || m.delta.length > exports.MAX_ADMISSION_DELTA_ENTRIES)
696
+ return null;
697
+ if (!Array.isArray(m.coverage) || m.coverage.length > exports.MAX_ADMISSION_COVERAGE_ENTRIES)
698
+ return null;
699
+ if (m.entryCount !== m.delta.length || m.entryCount !== m.coverage.length)
700
+ return null;
701
+ if (!m.delta.every((entry) => validateDeltaEntry(entry, m.objectFormat)))
702
+ return null;
703
+ if (!m.coverage.every((entry) => validateCoverageEntry(entry, m.objectFormat)))
704
+ return null;
705
+ if (record.runtimeContext !== undefined && !validateRuntimeAdmissionContext(record.runtimeContext))
706
+ return null;
707
+ return value;
708
+ }
709
+ /**
710
+ * Parse + validate untrusted artifact JSON text. Enforces a byte ceiling before
711
+ * JSON.parse, never throws, and returns null on any violation.
712
+ */
713
+ function readSelfAttestedAdmissionRecordFromText(text) {
714
+ if (typeof text !== 'string')
715
+ return null;
716
+ // Byte length, not code units, so multibyte payloads cannot exceed the cap.
717
+ let byteLength;
718
+ try {
719
+ byteLength = new TextEncoder().encode(text).length;
720
+ }
721
+ catch {
722
+ return null;
723
+ }
724
+ if (byteLength > exports.MAX_ADMISSION_JSON_BYTES)
725
+ return null;
726
+ let parsed;
727
+ try {
728
+ parsed = JSON.parse(text);
729
+ }
730
+ catch {
731
+ return null;
732
+ }
733
+ return readSelfAttestedAdmissionRecord(parsed);
734
+ }
735
+ //# sourceMappingURL=admission-provenance.js.map