@neuroverseos/governance 0.1.6 → 0.2.2
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/README.md +279 -423
- package/dist/adapters/express.cjs +242 -2
- package/dist/adapters/express.d.cts +1 -1
- package/dist/adapters/express.d.ts +1 -1
- package/dist/adapters/express.js +5 -3
- package/dist/adapters/index.cjs +337 -5
- package/dist/adapters/index.d.cts +1 -1
- package/dist/adapters/index.d.ts +1 -1
- package/dist/adapters/index.js +8 -6
- package/dist/adapters/langchain.cjs +297 -3
- package/dist/adapters/langchain.d.cts +8 -1
- package/dist/adapters/langchain.d.ts +8 -1
- package/dist/adapters/langchain.js +5 -3
- package/dist/adapters/openai.cjs +297 -3
- package/dist/adapters/openai.d.cts +8 -1
- package/dist/adapters/openai.d.ts +8 -1
- package/dist/adapters/openai.js +5 -3
- package/dist/adapters/openclaw.cjs +297 -3
- package/dist/adapters/openclaw.d.cts +8 -1
- package/dist/adapters/openclaw.d.ts +8 -1
- package/dist/adapters/openclaw.js +5 -3
- package/dist/{bootstrap-H4HHKQ5G.js → bootstrap-GXVDZNF7.js} +2 -1
- package/dist/{build-73KAVHEY.js → build-P42YFKQV.js} +34 -3
- package/dist/{chunk-Z2S2HIV5.js → chunk-2NICNKOM.js} +2 -2
- package/dist/{chunk-B4NF3OLW.js → chunk-4JRYGIO7.js} +56 -2
- package/dist/chunk-4QXB6PEO.js +232 -0
- package/dist/chunk-6CZSKEY5.js +164 -0
- package/dist/{chunk-O5OMJMIE.js → chunk-7P3S7MAY.js} +502 -2
- package/dist/chunk-A5W4GNQO.js +130 -0
- package/dist/chunk-AKW5YVCE.js +96 -0
- package/dist/chunk-DPVS43ZT.js +608 -0
- package/dist/{chunk-EIUHJXBB.js → chunk-GR6DGCZ2.js} +1 -1
- package/dist/chunk-KEST3MWO.js +324 -0
- package/dist/{chunk-D7BGWV2J.js → chunk-NF5POFCI.js} +5 -3
- package/dist/{chunk-FZQCRGUU.js → chunk-OHAC6HJE.js} +27 -3
- package/dist/chunk-OT6PXH54.js +61 -0
- package/dist/{chunk-ITJ3LCPG.js → chunk-PDOZHZWL.js} +1 -1
- package/dist/{chunk-T4X42QXC.js → chunk-Q6O7ZLO2.js} +0 -59
- package/dist/{chunk-FYPYZFV5.js → chunk-QPASI2BR.js} +1 -1
- package/dist/{chunk-EQXFOKH2.js → chunk-RWXVAH6P.js} +27 -3
- package/dist/{chunk-CROPZ75A.js → chunk-SKU3GAPD.js} +27 -3
- package/dist/chunk-YZFATT7X.js +9 -0
- package/dist/cli/neuroverse.cjs +5343 -732
- package/dist/cli/neuroverse.js +69 -13
- package/dist/cli/plan.cjs +1599 -0
- package/dist/cli/plan.d.cts +20 -0
- package/dist/cli/plan.d.ts +20 -0
- package/dist/cli/plan.js +361 -0
- package/dist/cli/run.cjs +1746 -0
- package/dist/cli/run.d.cts +20 -0
- package/dist/cli/run.d.ts +20 -0
- package/dist/cli/run.js +143 -0
- package/dist/{configure-ai-46JVG56I.js → configure-ai-TK67ZWZL.js} +5 -2
- package/dist/{derive-6NAEWLM5.js → derive-TLIV4OOU.js} +6 -4
- package/dist/doctor-QV6HELS5.js +170 -0
- package/dist/{explain-3B3VB6TL.js → explain-IDCRWMPX.js} +2 -1
- package/dist/{guard-67Y66P3I.js → guard-GFLQZY6U.js} +20 -6
- package/dist/{guard-contract-D_RQz9kt.d.ts → guard-contract-Cm91Kp4j.d.cts} +182 -2
- package/dist/{guard-contract-D_RQz9kt.d.cts → guard-contract-Cm91Kp4j.d.ts} +182 -2
- package/dist/guard-engine-JLTUARGU.js +10 -0
- package/dist/{impact-CHERK3O6.js → impact-XPECYRLH.js} +5 -3
- package/dist/{improve-YG6I6ERG.js → improve-GPUBKTEA.js} +4 -3
- package/dist/index.cjs +2135 -89
- package/dist/index.d.cts +481 -12
- package/dist/index.d.ts +481 -12
- package/dist/index.js +70 -20
- package/dist/{init-Z66T6TDI.js → init-PKPIYHYE.js} +2 -0
- package/dist/mcp-server-LZVJHBT5.js +13 -0
- package/dist/model-adapter-BB7G4MFI.js +11 -0
- package/dist/playground-FGOMASHN.js +550 -0
- package/dist/redteam-SK7AMIG3.js +357 -0
- package/dist/session-VISISNWJ.js +14 -0
- package/dist/{simulate-ETHHINZ4.js → simulate-VDOYQFRO.js} +2 -1
- package/dist/test-75AVHC3R.js +217 -0
- package/dist/{trace-3YODSSIP.js → trace-JVF67VR3.js} +4 -2
- package/dist/{validate-UVE6GKQU.js → validate-LLBWVPGV.js} +15 -6
- package/dist/validate-engine-UIABSIHD.js +7 -0
- package/dist/{world-WLNHL5XC.js → world-LAXO6DOX.js} +87 -7
- package/dist/world-loader-HMPTOEA2.js +9 -0
- package/package.json +19 -5
- package/dist/validate-engine-657D75OG.js +0 -6
- /package/dist/{chunk-M3TZFGHO.js → chunk-JZPQGIKR.js} +0 -0
|
@@ -1,13 +1,37 @@
|
|
|
1
1
|
// src/engine/validate-engine.ts
|
|
2
|
-
function validateWorld(world) {
|
|
2
|
+
function validateWorld(world, mode = "standard") {
|
|
3
3
|
const startTime = performance.now();
|
|
4
4
|
const findings = [];
|
|
5
5
|
checkCompleteness(world, findings);
|
|
6
6
|
checkReferentialIntegrity(world, findings);
|
|
7
7
|
checkGuardCoverage(world, findings);
|
|
8
|
+
checkSemanticCoverage(world, findings);
|
|
8
9
|
checkContradictions(world, findings);
|
|
10
|
+
checkGuardShadows(world, findings);
|
|
11
|
+
checkFailClosedSurfaces(world, findings);
|
|
12
|
+
checkReachability(world, findings);
|
|
13
|
+
checkStateCoverage(world, findings);
|
|
9
14
|
checkOrphans(world, findings);
|
|
10
15
|
checkSchemaViolations(world, findings);
|
|
16
|
+
const governanceCategories = /* @__PURE__ */ new Set([
|
|
17
|
+
"guard-coverage",
|
|
18
|
+
"contradiction",
|
|
19
|
+
"semantic-tension",
|
|
20
|
+
"orphan"
|
|
21
|
+
]);
|
|
22
|
+
if (mode === "dev") {
|
|
23
|
+
for (const f of findings) {
|
|
24
|
+
if (governanceCategories.has(f.category) && f.severity === "warning") {
|
|
25
|
+
f.severity = "info";
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
} else if (mode === "strict") {
|
|
29
|
+
for (const f of findings) {
|
|
30
|
+
if (governanceCategories.has(f.category) && f.severity === "info") {
|
|
31
|
+
f.severity = "warning";
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
11
35
|
const severityOrder = { error: 0, warning: 1, info: 2 };
|
|
12
36
|
findings.sort((a, b) => severityOrder[a.severity] - severityOrder[b.severity]);
|
|
13
37
|
const errors = findings.filter((f) => f.severity === "error").length;
|
|
@@ -15,6 +39,7 @@ function validateWorld(world) {
|
|
|
15
39
|
const info = findings.filter((f) => f.severity === "info").length;
|
|
16
40
|
const completenessScore = computeCompletenessScore(world);
|
|
17
41
|
const invariantCoverage = computeInvariantCoverage(world);
|
|
42
|
+
const governanceHealth = computeGovernanceHealth(world, findings);
|
|
18
43
|
const summary = {
|
|
19
44
|
errors,
|
|
20
45
|
warnings,
|
|
@@ -22,7 +47,8 @@ function validateWorld(world) {
|
|
|
22
47
|
completenessScore,
|
|
23
48
|
invariantCoverage,
|
|
24
49
|
canRun: errors === 0,
|
|
25
|
-
isHealthy: errors === 0 && warnings === 0
|
|
50
|
+
isHealthy: errors === 0 && warnings === 0,
|
|
51
|
+
governanceHealth
|
|
26
52
|
};
|
|
27
53
|
return {
|
|
28
54
|
worldId: world.world.world_id,
|
|
@@ -30,6 +56,7 @@ function validateWorld(world) {
|
|
|
30
56
|
worldVersion: world.world.version,
|
|
31
57
|
validatedAt: Date.now(),
|
|
32
58
|
durationMs: performance.now() - startTime,
|
|
59
|
+
validationMode: mode,
|
|
33
60
|
summary,
|
|
34
61
|
findings
|
|
35
62
|
};
|
|
@@ -212,6 +239,183 @@ function checkGuardCoverage(world, findings) {
|
|
|
212
239
|
}
|
|
213
240
|
}
|
|
214
241
|
}
|
|
242
|
+
function checkSemanticCoverage(world, findings) {
|
|
243
|
+
if (!world.invariants || world.invariants.length === 0) return;
|
|
244
|
+
const hasGuards = (world.guards?.guards?.length ?? 0) > 0;
|
|
245
|
+
const hasKernel = (world.kernel?.input_boundaries?.forbidden_patterns?.length ?? 0) > 0 || (world.kernel?.output_boundaries?.forbidden_patterns?.length ?? 0) > 0;
|
|
246
|
+
if (!hasGuards && !hasKernel) return;
|
|
247
|
+
const guards = world.guards?.guards ?? [];
|
|
248
|
+
const vocabEntries = world.guards?.intent_vocabulary ?? {};
|
|
249
|
+
const kernelInput = world.kernel?.input_boundaries?.forbidden_patterns ?? [];
|
|
250
|
+
const kernelOutput = world.kernel?.output_boundaries?.forbidden_patterns ?? [];
|
|
251
|
+
const allKernelRules = [...kernelInput, ...kernelOutput];
|
|
252
|
+
const guardSearchTexts = guards.map((g) => {
|
|
253
|
+
const parts = [];
|
|
254
|
+
for (const patternKey of g.intent_patterns) {
|
|
255
|
+
parts.push(patternKey.toLowerCase());
|
|
256
|
+
const vocab = vocabEntries[patternKey];
|
|
257
|
+
if (vocab) {
|
|
258
|
+
parts.push(vocab.label.toLowerCase());
|
|
259
|
+
parts.push(vocab.pattern.toLowerCase());
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
parts.push(g.description.toLowerCase());
|
|
263
|
+
return { guard: g, text: parts.join(" ") };
|
|
264
|
+
});
|
|
265
|
+
const kernelSearchTexts = allKernelRules.map((k) => ({
|
|
266
|
+
rule: k,
|
|
267
|
+
text: `${k.id} ${k.reason} ${k.pattern ?? ""}`.toLowerCase()
|
|
268
|
+
}));
|
|
269
|
+
for (const invariant of world.invariants) {
|
|
270
|
+
if (invariant.enforcement === "prompt") continue;
|
|
271
|
+
const tokens = extractActionTokens(invariant.id, invariant.label);
|
|
272
|
+
if (tokens.length === 0) continue;
|
|
273
|
+
const coveringGuards = guardSearchTexts.filter((gs) => {
|
|
274
|
+
const enabled = gs.guard.immutable || gs.guard.default_enabled !== false;
|
|
275
|
+
if (!enabled) return false;
|
|
276
|
+
return tokens.some((token) => gs.text.includes(token));
|
|
277
|
+
});
|
|
278
|
+
const coveringKernel = kernelSearchTexts.filter(
|
|
279
|
+
(ks) => tokens.some((token) => ks.text.includes(token))
|
|
280
|
+
);
|
|
281
|
+
const hasStructuralGuard = guards.some(
|
|
282
|
+
(g) => g.invariant_ref === invariant.id && g.immutable
|
|
283
|
+
);
|
|
284
|
+
if (coveringGuards.length === 0 && coveringKernel.length === 0) {
|
|
285
|
+
if (hasStructuralGuard) {
|
|
286
|
+
findings.push(finding(
|
|
287
|
+
`weak-coverage-${invariant.id}`,
|
|
288
|
+
`Invariant "${invariant.id}" has a structural guard but no guard's intent patterns match its action class [${tokens.join(", ")}] \u2014 the guard may not intercept violations`,
|
|
289
|
+
"warning",
|
|
290
|
+
"guard-coverage",
|
|
291
|
+
["invariants.json", "guards.json"],
|
|
292
|
+
invariant.id,
|
|
293
|
+
`Ensure the backing guard's intent_patterns include patterns that can detect "${invariant.label}"`
|
|
294
|
+
));
|
|
295
|
+
} else {
|
|
296
|
+
findings.push(finding(
|
|
297
|
+
`unenforced-invariant-${invariant.id}`,
|
|
298
|
+
`Invariant "${invariant.id}" has no guard or kernel rule capable of enforcing it \u2014 no interceptor matches action class [${tokens.join(", ")}]`,
|
|
299
|
+
"warning",
|
|
300
|
+
"guard-coverage",
|
|
301
|
+
["invariants.json", "guards.json"],
|
|
302
|
+
invariant.id,
|
|
303
|
+
`Add a guard with intent_patterns that can intercept "${invariant.label}", or add a kernel forbidden_pattern`
|
|
304
|
+
));
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
function extractActionTokens(id, label) {
|
|
310
|
+
const stopWords = /* @__PURE__ */ new Set([
|
|
311
|
+
"a",
|
|
312
|
+
"an",
|
|
313
|
+
"the",
|
|
314
|
+
"is",
|
|
315
|
+
"are",
|
|
316
|
+
"was",
|
|
317
|
+
"were",
|
|
318
|
+
"be",
|
|
319
|
+
"been",
|
|
320
|
+
"being",
|
|
321
|
+
"have",
|
|
322
|
+
"has",
|
|
323
|
+
"had",
|
|
324
|
+
"do",
|
|
325
|
+
"does",
|
|
326
|
+
"did",
|
|
327
|
+
"will",
|
|
328
|
+
"would",
|
|
329
|
+
"could",
|
|
330
|
+
"should",
|
|
331
|
+
"may",
|
|
332
|
+
"might",
|
|
333
|
+
"must",
|
|
334
|
+
"shall",
|
|
335
|
+
"can",
|
|
336
|
+
"need",
|
|
337
|
+
"dare",
|
|
338
|
+
"to",
|
|
339
|
+
"of",
|
|
340
|
+
"in",
|
|
341
|
+
"for",
|
|
342
|
+
"on",
|
|
343
|
+
"with",
|
|
344
|
+
"at",
|
|
345
|
+
"by",
|
|
346
|
+
"from",
|
|
347
|
+
"as",
|
|
348
|
+
"into",
|
|
349
|
+
"through",
|
|
350
|
+
"during",
|
|
351
|
+
"before",
|
|
352
|
+
"after",
|
|
353
|
+
"above",
|
|
354
|
+
"below",
|
|
355
|
+
"between",
|
|
356
|
+
"out",
|
|
357
|
+
"off",
|
|
358
|
+
"over",
|
|
359
|
+
"under",
|
|
360
|
+
"again",
|
|
361
|
+
"further",
|
|
362
|
+
"then",
|
|
363
|
+
"once",
|
|
364
|
+
"that",
|
|
365
|
+
"than",
|
|
366
|
+
"too",
|
|
367
|
+
"very",
|
|
368
|
+
"just",
|
|
369
|
+
"only",
|
|
370
|
+
"not",
|
|
371
|
+
"no",
|
|
372
|
+
"all",
|
|
373
|
+
"any",
|
|
374
|
+
"both",
|
|
375
|
+
"each",
|
|
376
|
+
"every",
|
|
377
|
+
"few",
|
|
378
|
+
"more",
|
|
379
|
+
"most",
|
|
380
|
+
"other",
|
|
381
|
+
"some",
|
|
382
|
+
"such",
|
|
383
|
+
"and",
|
|
384
|
+
"but",
|
|
385
|
+
"or",
|
|
386
|
+
"nor",
|
|
387
|
+
"so",
|
|
388
|
+
"yet",
|
|
389
|
+
"if",
|
|
390
|
+
"it",
|
|
391
|
+
"its",
|
|
392
|
+
"they",
|
|
393
|
+
"them",
|
|
394
|
+
"their",
|
|
395
|
+
"this",
|
|
396
|
+
"these",
|
|
397
|
+
"those",
|
|
398
|
+
"which",
|
|
399
|
+
"who",
|
|
400
|
+
"whom",
|
|
401
|
+
"what",
|
|
402
|
+
"where",
|
|
403
|
+
"when",
|
|
404
|
+
"how",
|
|
405
|
+
"why"
|
|
406
|
+
]);
|
|
407
|
+
const idTokens = id.toLowerCase().split(/[_\-]+/);
|
|
408
|
+
const labelTokens = label.toLowerCase().split(/[\s\-—:,;.!?()[\]{}]+/);
|
|
409
|
+
const allTokens = [...idTokens, ...labelTokens];
|
|
410
|
+
const unique = /* @__PURE__ */ new Set();
|
|
411
|
+
for (const token of allTokens) {
|
|
412
|
+
const clean = token.replace(/[^a-z0-9]/g, "");
|
|
413
|
+
if (clean.length >= 3 && !stopWords.has(clean)) {
|
|
414
|
+
unique.add(clean);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
return [...unique];
|
|
418
|
+
}
|
|
215
419
|
function checkContradictions(world, findings) {
|
|
216
420
|
if (!world.rules || world.rules.length < 2) return;
|
|
217
421
|
checkCircularExclusiveWith(world.rules, findings);
|
|
@@ -417,6 +621,251 @@ function describeEffect(effect) {
|
|
|
417
621
|
return `${effect.operation} ${effect.value}`;
|
|
418
622
|
}
|
|
419
623
|
}
|
|
624
|
+
function checkGuardShadows(world, findings) {
|
|
625
|
+
if (!world.guards?.guards || world.guards.guards.length < 2) return;
|
|
626
|
+
const guards = world.guards.guards;
|
|
627
|
+
for (let i = 0; i < guards.length; i++) {
|
|
628
|
+
const guardA = guards[i];
|
|
629
|
+
const enabledA = guardA.immutable || guardA.default_enabled !== false;
|
|
630
|
+
if (!enabledA) continue;
|
|
631
|
+
if (guardA.enforcement !== "block" && guardA.enforcement !== "pause") continue;
|
|
632
|
+
for (let j = i + 1; j < guards.length; j++) {
|
|
633
|
+
const guardB = guards[j];
|
|
634
|
+
const enabledB = guardB.immutable || guardB.default_enabled !== false;
|
|
635
|
+
if (!enabledB) continue;
|
|
636
|
+
const overlap = guardA.intent_patterns.filter(
|
|
637
|
+
(p) => guardB.intent_patterns.includes(p)
|
|
638
|
+
);
|
|
639
|
+
if (overlap.length === 0) continue;
|
|
640
|
+
if (guardA.appliesTo?.length && guardB.appliesTo?.length) {
|
|
641
|
+
const toolsA = new Set(guardA.appliesTo.map((t) => t.toLowerCase()));
|
|
642
|
+
const toolsB = new Set(guardB.appliesTo.map((t) => t.toLowerCase()));
|
|
643
|
+
const toolOverlap = [...toolsA].some((t) => toolsB.has(t));
|
|
644
|
+
if (!toolOverlap) continue;
|
|
645
|
+
}
|
|
646
|
+
if (guardA.required_roles?.length && guardB.required_roles?.length) {
|
|
647
|
+
const rolesA = new Set(guardA.required_roles);
|
|
648
|
+
const rolesB = new Set(guardB.required_roles);
|
|
649
|
+
const roleOverlap = [...rolesA].some((r) => rolesB.has(r));
|
|
650
|
+
if (!roleOverlap) continue;
|
|
651
|
+
}
|
|
652
|
+
const patternsStr = overlap.join(", ");
|
|
653
|
+
if (guardB.enforcement === guardA.enforcement) {
|
|
654
|
+
findings.push(finding(
|
|
655
|
+
`guard-shadow-${guardA.id}-${guardB.id}`,
|
|
656
|
+
`Guard "${guardB.label}" (${guardB.id}) is shadowed by "${guardA.label}" (${guardA.id}) \u2014 both ${guardA.enforcement.toUpperCase()} on patterns [${patternsStr}] but "${guardA.label}" appears first and will always win`,
|
|
657
|
+
"warning",
|
|
658
|
+
"contradiction",
|
|
659
|
+
["guards/"],
|
|
660
|
+
`${guardA.id}, ${guardB.id}`,
|
|
661
|
+
`Remove "${guardB.label}", merge its patterns into "${guardA.label}", or reorder guards`
|
|
662
|
+
));
|
|
663
|
+
} else {
|
|
664
|
+
findings.push(finding(
|
|
665
|
+
`guard-conflict-${guardA.id}-${guardB.id}`,
|
|
666
|
+
`Guards "${guardA.label}" (${guardA.enforcement.toUpperCase()}) and "${guardB.label}" (${guardB.enforcement.toUpperCase()}) share patterns [${patternsStr}] \u2014 "${guardA.label}" always wins because it appears first`,
|
|
667
|
+
"warning",
|
|
668
|
+
"contradiction",
|
|
669
|
+
["guards/"],
|
|
670
|
+
`${guardA.id}, ${guardB.id}`,
|
|
671
|
+
`If "${guardB.label}" should take precedence, move it before "${guardA.label}" in guards.json`
|
|
672
|
+
));
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
function checkFailClosedSurfaces(world, findings) {
|
|
678
|
+
const declaredSurfaces = world.guards?.tool_surfaces;
|
|
679
|
+
if (!declaredSurfaces || declaredSurfaces.length === 0) return;
|
|
680
|
+
const guards = world.guards?.guards ?? [];
|
|
681
|
+
const guardedSurfaces = /* @__PURE__ */ new Set();
|
|
682
|
+
let hasCatchAllGuard = false;
|
|
683
|
+
for (const guard of guards) {
|
|
684
|
+
const enabled = guard.immutable || guard.default_enabled !== false;
|
|
685
|
+
if (!enabled) continue;
|
|
686
|
+
if (!guard.appliesTo || guard.appliesTo.length === 0) {
|
|
687
|
+
hasCatchAllGuard = true;
|
|
688
|
+
} else {
|
|
689
|
+
for (const tool of guard.appliesTo) {
|
|
690
|
+
guardedSurfaces.add(tool.toLowerCase());
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
if (hasCatchAllGuard) return;
|
|
695
|
+
for (const surface of declaredSurfaces) {
|
|
696
|
+
if (!guardedSurfaces.has(surface.toLowerCase())) {
|
|
697
|
+
findings.push(finding(
|
|
698
|
+
`fail-open-surface-${surface.toLowerCase()}`,
|
|
699
|
+
`Action surface "${surface}" has no governing guard \u2014 actions on this surface bypass governance entirely`,
|
|
700
|
+
"warning",
|
|
701
|
+
"guard-coverage",
|
|
702
|
+
["guards.json"],
|
|
703
|
+
void 0,
|
|
704
|
+
`Add a guard with appliesTo including "${surface}", or add a catch-all guard (no appliesTo) to cover all surfaces`
|
|
705
|
+
));
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
function checkReachability(world, findings) {
|
|
710
|
+
if (!world.stateSchema?.variables) return;
|
|
711
|
+
const vars = world.stateSchema.variables;
|
|
712
|
+
for (const rule of world.rules ?? []) {
|
|
713
|
+
for (const trigger of rule.triggers) {
|
|
714
|
+
if (trigger.source !== "state") continue;
|
|
715
|
+
const unreachable = isTriggerUnreachable(trigger, vars);
|
|
716
|
+
if (unreachable) {
|
|
717
|
+
findings.push(finding(
|
|
718
|
+
`unreachable-rule-${rule.id}-${trigger.field}`,
|
|
719
|
+
`Rule "${rule.id}" has unreachable trigger: ${trigger.field} ${trigger.operator} ${JSON.stringify(trigger.value)} \u2014 ${unreachable}`,
|
|
720
|
+
"warning",
|
|
721
|
+
"contradiction",
|
|
722
|
+
["rules/", "state-schema.json"],
|
|
723
|
+
rule.id,
|
|
724
|
+
`Remove this rule or adjust the trigger condition to match the schema constraints for "${trigger.field}"`
|
|
725
|
+
));
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
if (rule.collapse_check) {
|
|
729
|
+
const cc = rule.collapse_check;
|
|
730
|
+
const unreachable = isTriggerUnreachable(
|
|
731
|
+
{ field: cc.field, operator: cc.operator, value: cc.value },
|
|
732
|
+
vars
|
|
733
|
+
);
|
|
734
|
+
if (unreachable) {
|
|
735
|
+
findings.push(finding(
|
|
736
|
+
`unreachable-collapse-${rule.id}`,
|
|
737
|
+
`Rule "${rule.id}" has unreachable collapse_check: ${cc.field} ${cc.operator} ${cc.value} \u2014 ${unreachable}`,
|
|
738
|
+
"warning",
|
|
739
|
+
"contradiction",
|
|
740
|
+
["rules/", "state-schema.json"],
|
|
741
|
+
rule.id
|
|
742
|
+
));
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
for (const gate of world.gates?.viability_classification ?? []) {
|
|
747
|
+
const unreachable = isTriggerUnreachable(
|
|
748
|
+
{ field: gate.field, operator: gate.operator, value: gate.value },
|
|
749
|
+
vars
|
|
750
|
+
);
|
|
751
|
+
if (unreachable) {
|
|
752
|
+
findings.push(finding(
|
|
753
|
+
`unreachable-gate-${gate.status}`,
|
|
754
|
+
`Viability gate "${gate.status}" has unreachable condition: ${gate.field} ${gate.operator} ${gate.value} \u2014 ${unreachable}`,
|
|
755
|
+
"warning",
|
|
756
|
+
"contradiction",
|
|
757
|
+
["gates.json", "state-schema.json"],
|
|
758
|
+
`gate-${gate.status}`
|
|
759
|
+
));
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
function isTriggerUnreachable(trigger, vars) {
|
|
764
|
+
const variable = vars[trigger.field];
|
|
765
|
+
if (!variable) return null;
|
|
766
|
+
const { operator, value } = trigger;
|
|
767
|
+
if (variable.type === "number") {
|
|
768
|
+
const numVal = typeof value === "number" ? value : Number(value);
|
|
769
|
+
if (isNaN(numVal)) return null;
|
|
770
|
+
const min = variable.min;
|
|
771
|
+
const max = variable.max;
|
|
772
|
+
if (operator === ">" || operator === ">=") {
|
|
773
|
+
if (max !== void 0 && numVal >= max && operator === ">") {
|
|
774
|
+
return `schema declares max=${max}, so ${trigger.field} can never exceed ${max}`;
|
|
775
|
+
}
|
|
776
|
+
if (max !== void 0 && numVal > max && operator === ">=") {
|
|
777
|
+
return `schema declares max=${max}, so ${trigger.field} can never reach ${numVal}`;
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
if (operator === "<" || operator === "<=") {
|
|
781
|
+
if (min !== void 0 && numVal <= min && operator === "<") {
|
|
782
|
+
return `schema declares min=${min}, so ${trigger.field} can never go below ${min}`;
|
|
783
|
+
}
|
|
784
|
+
if (min !== void 0 && numVal < min && operator === "<=") {
|
|
785
|
+
return `schema declares min=${min}, so ${trigger.field} can never reach ${numVal}`;
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
if (operator === "==") {
|
|
789
|
+
if (min !== void 0 && numVal < min) {
|
|
790
|
+
return `schema declares min=${min}, so ${trigger.field} can never equal ${numVal}`;
|
|
791
|
+
}
|
|
792
|
+
if (max !== void 0 && numVal > max) {
|
|
793
|
+
return `schema declares max=${max}, so ${trigger.field} can never equal ${numVal}`;
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
if (variable.type === "enum" && variable.options) {
|
|
798
|
+
if (operator === "==" && typeof value === "string") {
|
|
799
|
+
if (!variable.options.includes(value)) {
|
|
800
|
+
return `"${value}" is not in enum options [${variable.options.join(", ")}]`;
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
if (operator === "!=" && typeof value === "string") {
|
|
804
|
+
if (variable.options.length === 1 && variable.options[0] === value) {
|
|
805
|
+
return `enum has only option "${value}", so != "${value}" can never be true`;
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
if (operator === "in" && Array.isArray(value)) {
|
|
809
|
+
const validValues = value.filter((v) => variable.options.includes(v));
|
|
810
|
+
if (validValues.length === 0) {
|
|
811
|
+
return `none of [${value.join(", ")}] are in enum options [${variable.options.join(", ")}]`;
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
if (variable.type === "boolean") {
|
|
816
|
+
if (operator === "==" && typeof value !== "boolean" && value !== "true" && value !== "false") {
|
|
817
|
+
return `boolean variable compared to non-boolean value "${value}"`;
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
return null;
|
|
821
|
+
}
|
|
822
|
+
function checkStateCoverage(world, findings) {
|
|
823
|
+
if (!world.stateSchema?.variables) return;
|
|
824
|
+
const vars = world.stateSchema.variables;
|
|
825
|
+
for (const [varId, variable] of Object.entries(vars)) {
|
|
826
|
+
if (variable.type !== "enum" || !variable.options || variable.options.length <= 1) continue;
|
|
827
|
+
const allOptions = new Set(variable.options);
|
|
828
|
+
const coveredOptions = /* @__PURE__ */ new Set();
|
|
829
|
+
for (const rule of world.rules ?? []) {
|
|
830
|
+
for (const trigger of rule.triggers) {
|
|
831
|
+
if (trigger.field !== varId || trigger.source !== "state") continue;
|
|
832
|
+
if (trigger.operator === "==" && typeof trigger.value === "string") {
|
|
833
|
+
coveredOptions.add(trigger.value);
|
|
834
|
+
}
|
|
835
|
+
if (trigger.operator === "in" && Array.isArray(trigger.value)) {
|
|
836
|
+
for (const v of trigger.value) coveredOptions.add(v);
|
|
837
|
+
}
|
|
838
|
+
if (trigger.operator === "!=") {
|
|
839
|
+
for (const opt of allOptions) {
|
|
840
|
+
if (opt !== trigger.value) coveredOptions.add(opt);
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
for (const gate of world.gates?.viability_classification ?? []) {
|
|
846
|
+
if (gate.field !== varId) continue;
|
|
847
|
+
if (gate.operator === "==" && typeof gate.value === "string") {
|
|
848
|
+
coveredOptions.add(gate.value);
|
|
849
|
+
}
|
|
850
|
+
if (gate.operator === "in" && Array.isArray(gate.value)) {
|
|
851
|
+
for (const v of gate.value) coveredOptions.add(v);
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
if (coveredOptions.size === 0) continue;
|
|
855
|
+
const uncovered = [...allOptions].filter((opt) => !coveredOptions.has(opt));
|
|
856
|
+
if (uncovered.length > 0 && uncovered.length < allOptions.size) {
|
|
857
|
+
findings.push(finding(
|
|
858
|
+
`incomplete-state-coverage-${varId}`,
|
|
859
|
+
`Enum variable "${varId}" has ${uncovered.length} uncovered state${uncovered.length > 1 ? "s" : ""}: [${uncovered.join(", ")}] \u2014 rules/gates handle [${[...coveredOptions].join(", ")}] but not all ${allOptions.size} declared options`,
|
|
860
|
+
"warning",
|
|
861
|
+
"guard-coverage",
|
|
862
|
+
["state-schema.json", "rules/", "gates.json"],
|
|
863
|
+
varId,
|
|
864
|
+
`Add rules or gates that handle ${uncovered.map((u) => `"${u}"`).join(", ")} for variable "${varId}"`
|
|
865
|
+
));
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
}
|
|
420
869
|
function checkOrphans(world, findings) {
|
|
421
870
|
if (!world.stateSchema?.variables || !world.rules) return;
|
|
422
871
|
const referencedVars = /* @__PURE__ */ new Set();
|
|
@@ -578,6 +1027,57 @@ function computeInvariantCoverage(world) {
|
|
|
578
1027
|
}
|
|
579
1028
|
return Math.round(covered / world.invariants.length * 100);
|
|
580
1029
|
}
|
|
1030
|
+
function computeGovernanceHealth(world, findings) {
|
|
1031
|
+
const guards = world.guards?.guards ?? [];
|
|
1032
|
+
if (guards.length === 0 && !world.kernel) return void 0;
|
|
1033
|
+
const declaredSurfaces = world.guards?.tool_surfaces ?? [];
|
|
1034
|
+
const guardedSurfaces = /* @__PURE__ */ new Set();
|
|
1035
|
+
let hasCatchAll = false;
|
|
1036
|
+
for (const guard of guards) {
|
|
1037
|
+
const enabled = guard.immutable || guard.default_enabled !== false;
|
|
1038
|
+
if (!enabled) continue;
|
|
1039
|
+
if (!guard.appliesTo || guard.appliesTo.length === 0) {
|
|
1040
|
+
hasCatchAll = true;
|
|
1041
|
+
} else {
|
|
1042
|
+
for (const t of guard.appliesTo) guardedSurfaces.add(t.toLowerCase());
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
const allSurfaces = /* @__PURE__ */ new Set();
|
|
1046
|
+
for (const s of declaredSurfaces) allSurfaces.add(s.toLowerCase());
|
|
1047
|
+
for (const s of guardedSurfaces) allSurfaces.add(s);
|
|
1048
|
+
const surfaces = [...allSurfaces].map((name) => ({
|
|
1049
|
+
name,
|
|
1050
|
+
governed: hasCatchAll || guardedSurfaces.has(name)
|
|
1051
|
+
}));
|
|
1052
|
+
const surfacesCovered = hasCatchAll ? allSurfaces.size : guardedSurfaces.size;
|
|
1053
|
+
const structuralInvariants = (world.invariants ?? []).filter((i) => i.enforcement === "structural");
|
|
1054
|
+
let invariantsEnforced = 0;
|
|
1055
|
+
for (const inv of structuralInvariants) {
|
|
1056
|
+
const hasGuard = guards.some((g) => g.invariant_ref === inv.id && g.immutable);
|
|
1057
|
+
if (hasGuard) invariantsEnforced++;
|
|
1058
|
+
}
|
|
1059
|
+
const shadowedGuards = findings.filter((f) => f.id.startsWith("guard-shadow-")).length;
|
|
1060
|
+
const unenforcedInvariants = findings.filter((f) => f.id.startsWith("unenforced-invariant-")).length;
|
|
1061
|
+
const unreachableRules = findings.filter((f) => f.id.startsWith("unreachable-")).length;
|
|
1062
|
+
const incompleteStateCoverage = findings.filter((f) => f.id.startsWith("incomplete-state-coverage-")).length;
|
|
1063
|
+
const failOpenCount = findings.filter((f) => f.id.startsWith("fail-open-surface-")).length;
|
|
1064
|
+
let riskLevel = "low";
|
|
1065
|
+
const totalIssues = unenforcedInvariants + failOpenCount + incompleteStateCoverage;
|
|
1066
|
+
if (totalIssues > 0 || unreachableRules > 0) riskLevel = "moderate";
|
|
1067
|
+
if (totalIssues > 2 || unenforcedInvariants > 0 && failOpenCount > 0 || incompleteStateCoverage > 2) riskLevel = "high";
|
|
1068
|
+
return {
|
|
1069
|
+
surfacesCovered,
|
|
1070
|
+
surfacesTotal: allSurfaces.size,
|
|
1071
|
+
surfaces,
|
|
1072
|
+
invariantsEnforced,
|
|
1073
|
+
invariantsTotal: structuralInvariants.length,
|
|
1074
|
+
shadowedGuards,
|
|
1075
|
+
unenforcedInvariants,
|
|
1076
|
+
unreachableRules,
|
|
1077
|
+
incompleteStateCoverage,
|
|
1078
|
+
riskLevel
|
|
1079
|
+
};
|
|
1080
|
+
}
|
|
581
1081
|
function finding(id, message, severity, category, affectedBlocks, source, suggestion) {
|
|
582
1082
|
const f = { id, message, severity, category, affectedBlocks };
|
|
583
1083
|
if (source) f.source = source;
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// src/runtime/model-adapter.ts
|
|
2
|
+
var DEFAULT_SYSTEM_PROMPT = `You are an AI assistant operating under NeuroVerse governance.
|
|
3
|
+
All your tool calls are evaluated against governance rules before execution.
|
|
4
|
+
If an action is blocked, you will be told why. Adjust your approach accordingly.
|
|
5
|
+
Do not attempt to bypass governance rules.`;
|
|
6
|
+
var ModelAdapter = class {
|
|
7
|
+
config;
|
|
8
|
+
messages;
|
|
9
|
+
tools;
|
|
10
|
+
constructor(config, tools = []) {
|
|
11
|
+
this.config = config;
|
|
12
|
+
this.tools = tools;
|
|
13
|
+
this.messages = [];
|
|
14
|
+
const systemPrompt = config.systemPrompt ?? DEFAULT_SYSTEM_PROMPT;
|
|
15
|
+
this.messages.push({ role: "system", content: systemPrompt });
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Send a user message and get the model's response.
|
|
19
|
+
*/
|
|
20
|
+
async chat(userMessage) {
|
|
21
|
+
this.messages.push({ role: "user", content: userMessage });
|
|
22
|
+
return this.complete();
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Send a tool result back to the model and get the next response.
|
|
26
|
+
*/
|
|
27
|
+
async sendToolResult(toolCallId, result) {
|
|
28
|
+
this.messages.push({
|
|
29
|
+
role: "tool",
|
|
30
|
+
content: result,
|
|
31
|
+
tool_call_id: toolCallId
|
|
32
|
+
});
|
|
33
|
+
return this.complete();
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Send a governance block message as a tool result.
|
|
37
|
+
*/
|
|
38
|
+
async sendBlockedResult(toolCallId, reason) {
|
|
39
|
+
return this.sendToolResult(
|
|
40
|
+
toolCallId,
|
|
41
|
+
`[GOVERNANCE BLOCKED] ${reason}. Please adjust your approach.`
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Call the model API and parse the response.
|
|
46
|
+
*/
|
|
47
|
+
async complete() {
|
|
48
|
+
const url = `${this.config.baseUrl}/chat/completions`;
|
|
49
|
+
const body = {
|
|
50
|
+
model: this.config.model,
|
|
51
|
+
messages: this.messages,
|
|
52
|
+
max_tokens: this.config.maxTokens ?? 4096
|
|
53
|
+
};
|
|
54
|
+
if (this.tools.length > 0) {
|
|
55
|
+
body.tools = this.tools;
|
|
56
|
+
}
|
|
57
|
+
const response = await fetch(url, {
|
|
58
|
+
method: "POST",
|
|
59
|
+
headers: {
|
|
60
|
+
"Content-Type": "application/json",
|
|
61
|
+
"Authorization": `Bearer ${this.config.apiKey}`
|
|
62
|
+
},
|
|
63
|
+
body: JSON.stringify(body)
|
|
64
|
+
});
|
|
65
|
+
if (!response.ok) {
|
|
66
|
+
const text = await response.text();
|
|
67
|
+
throw new Error(`Model API error ${response.status}: ${text}`);
|
|
68
|
+
}
|
|
69
|
+
const data = await response.json();
|
|
70
|
+
const choice = data.choices?.[0];
|
|
71
|
+
if (!choice) {
|
|
72
|
+
throw new Error("Model returned no choices");
|
|
73
|
+
}
|
|
74
|
+
const message = choice.message;
|
|
75
|
+
this.messages.push(message);
|
|
76
|
+
return {
|
|
77
|
+
content: message.content ?? null,
|
|
78
|
+
toolCalls: message.tool_calls ?? [],
|
|
79
|
+
finishReason: choice.finish_reason ?? "stop"
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
/** Get current message count (for context tracking). */
|
|
83
|
+
get messageCount() {
|
|
84
|
+
return this.messages.length;
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
var PROVIDERS = {
|
|
88
|
+
openai: {
|
|
89
|
+
baseUrl: "https://api.openai.com/v1",
|
|
90
|
+
defaultModel: "gpt-4o",
|
|
91
|
+
envVar: "OPENAI_API_KEY"
|
|
92
|
+
},
|
|
93
|
+
anthropic: {
|
|
94
|
+
baseUrl: "https://api.anthropic.com/v1",
|
|
95
|
+
defaultModel: "claude-sonnet-4-20250514",
|
|
96
|
+
envVar: "ANTHROPIC_API_KEY"
|
|
97
|
+
},
|
|
98
|
+
ollama: {
|
|
99
|
+
baseUrl: "http://localhost:11434/v1",
|
|
100
|
+
defaultModel: "llama3",
|
|
101
|
+
envVar: ""
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
function resolveProvider(provider, overrides) {
|
|
105
|
+
const preset = PROVIDERS[provider];
|
|
106
|
+
if (!preset) {
|
|
107
|
+
throw new Error(
|
|
108
|
+
`Unknown provider: "${provider}". Available: ${Object.keys(PROVIDERS).join(", ")}`
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
const apiKey = overrides?.apiKey ?? (preset.envVar ? process.env[preset.envVar] : "") ?? "";
|
|
112
|
+
if (!apiKey && preset.envVar) {
|
|
113
|
+
throw new Error(
|
|
114
|
+
`Missing API key. Set ${preset.envVar} or pass --api-key.`
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
baseUrl: overrides?.baseUrl ?? preset.baseUrl,
|
|
119
|
+
apiKey,
|
|
120
|
+
model: overrides?.model ?? preset.defaultModel,
|
|
121
|
+
systemPrompt: overrides?.systemPrompt,
|
|
122
|
+
maxTokens: overrides?.maxTokens
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export {
|
|
127
|
+
ModelAdapter,
|
|
128
|
+
PROVIDERS,
|
|
129
|
+
resolveProvider
|
|
130
|
+
};
|