@neuroverseos/governance 0.1.5 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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 +301 -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 +267 -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 +267 -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 +267 -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-FYPYZFV5.js → chunk-2JQJ5U5X.js} +1 -1
- package/dist/chunk-37JG24WH.js +161 -0
- package/dist/chunk-5EDDNJU6.js +321 -0
- package/dist/{chunk-O5OMJMIE.js → chunk-7P3S7MAY.js} +502 -2
- package/dist/chunk-A5W4GNQO.js +130 -0
- package/dist/{chunk-ITJ3LCPG.js → chunk-ADV7Q2LJ.js} +1 -1
- package/dist/chunk-AKW5YVCE.js +96 -0
- package/dist/{chunk-EIUHJXBB.js → chunk-GR6DGCZ2.js} +1 -1
- package/dist/{chunk-EQXFOKH2.js → chunk-IVPKFJX3.js} +24 -3
- package/dist/{chunk-D7BGWV2J.js → chunk-NF5POFCI.js} +5 -3
- package/dist/chunk-OT6PXH54.js +61 -0
- package/dist/chunk-P74Y66ZV.js +205 -0
- package/dist/chunk-PAX2P6ZP.js +601 -0
- package/dist/{chunk-B4NF3OLW.js → chunk-PQBJBVSW.js} +56 -2
- package/dist/{chunk-T4X42QXC.js → chunk-Q6O7ZLO2.js} +0 -59
- package/dist/{chunk-FZQCRGUU.js → chunk-TINSRYXQ.js} +24 -3
- package/dist/{chunk-CROPZ75A.js → chunk-UPJNTSVM.js} +24 -3
- package/dist/chunk-YZFATT7X.js +9 -0
- package/dist/{chunk-Z2S2HIV5.js → chunk-ZL4AHY4X.js} +2 -2
- package/dist/cli/neuroverse.cjs +5287 -740
- package/dist/cli/neuroverse.js +69 -13
- package/dist/cli/plan.cjs +1554 -0
- package/dist/cli/plan.d.cts +20 -0
- package/dist/cli/plan.d.ts +20 -0
- package/dist/cli/plan.js +346 -0
- package/dist/cli/run.cjs +1716 -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-V72UM2TC.js +170 -0
- package/dist/{explain-3B3VB6TL.js → explain-IDCRWMPX.js} +2 -1
- package/dist/{guard-67Y66P3I.js → guard-WA3FCCIO.js} +20 -6
- package/dist/{guard-contract-D_RQz9kt.d.ts → guard-contract-D-2LQInm.d.cts} +144 -2
- package/dist/{guard-contract-D_RQz9kt.d.cts → guard-contract-D-2LQInm.d.ts} +144 -2
- package/dist/guard-engine-D7X4CVAE.js +10 -0
- package/dist/{impact-CHERK3O6.js → impact-BWULZ5RP.js} +5 -3
- package/dist/{improve-YG6I6ERG.js → improve-GPUBKTEA.js} +4 -3
- package/dist/index.cjs +2095 -89
- package/dist/index.d.cts +466 -12
- package/dist/index.d.ts +466 -12
- package/dist/index.js +70 -20
- package/dist/{init-Z66T6TDI.js → init-PKPIYHYE.js} +2 -0
- package/dist/mcp-server-YUOQP4M5.js +13 -0
- package/dist/model-adapter-BB7G4MFI.js +11 -0
- package/dist/playground-CBXMAW2B.js +550 -0
- package/dist/redteam-SSNABQ7W.js +357 -0
- package/dist/session-MWRBTCYX.js +14 -0
- package/dist/{simulate-ETHHINZ4.js → simulate-VDOYQFRO.js} +2 -1
- package/dist/test-3GZSG5FR.js +217 -0
- package/dist/{trace-3YODSSIP.js → trace-TM4Z7G73.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
package/dist/index.cjs
CHANGED
|
@@ -37,12 +37,21 @@ __export(index_exports, {
|
|
|
37
37
|
DERIVE_EXIT_CODES: () => DERIVE_EXIT_CODES,
|
|
38
38
|
FileAuditLogger: () => FileAuditLogger,
|
|
39
39
|
GUARD_EXIT_CODES: () => GUARD_EXIT_CODES,
|
|
40
|
+
McpGovernanceServer: () => McpGovernanceServer,
|
|
41
|
+
ModelAdapter: () => ModelAdapter,
|
|
42
|
+
PLAN_EXIT_CODES: () => PLAN_EXIT_CODES,
|
|
43
|
+
PROVIDERS: () => PROVIDERS,
|
|
44
|
+
SessionManager: () => SessionManager,
|
|
40
45
|
VALIDATE_EXIT_CODES: () => VALIDATE_EXIT_CODES,
|
|
46
|
+
advancePlan: () => advancePlan,
|
|
47
|
+
buildPlanCheck: () => buildPlanCheck,
|
|
41
48
|
createGovernanceEngine: () => createGovernanceEngine,
|
|
42
49
|
deriveWorld: () => deriveWorld,
|
|
50
|
+
describeActiveWorld: () => describeActiveWorld,
|
|
43
51
|
emitWorldDefinition: () => emitWorldDefinition,
|
|
44
52
|
evaluateCondition: () => evaluateCondition,
|
|
45
53
|
evaluateGuard: () => evaluateGuard,
|
|
54
|
+
evaluatePlan: () => evaluatePlan,
|
|
46
55
|
eventToAllowlistKey: () => eventToAllowlistKey,
|
|
47
56
|
explainWorld: () => explainWorld,
|
|
48
57
|
extractWorldMarkdown: () => extractWorldMarkdown,
|
|
@@ -50,16 +59,25 @@ __export(index_exports, {
|
|
|
50
59
|
formatVerdictOneLine: () => formatVerdictOneLine,
|
|
51
60
|
generateImpactReport: () => generateImpactReport,
|
|
52
61
|
generateImpactReportFromFile: () => generateImpactReportFromFile,
|
|
62
|
+
getActiveWorldName: () => getActiveWorldName,
|
|
63
|
+
getPlanProgress: () => getPlanProgress,
|
|
53
64
|
improveWorld: () => improveWorld,
|
|
65
|
+
listWorlds: () => listWorlds,
|
|
54
66
|
loadWorld: () => loadWorld,
|
|
55
67
|
loadWorldFromDirectory: () => loadWorldFromDirectory,
|
|
56
68
|
normalizeWorldMarkdown: () => normalizeWorldMarkdown,
|
|
69
|
+
parsePlanMarkdown: () => parsePlanMarkdown,
|
|
57
70
|
parseWorldMarkdown: () => parseWorldMarkdown,
|
|
58
71
|
readAuditLog: () => readAuditLog,
|
|
59
72
|
renderExplainText: () => renderExplainText,
|
|
60
73
|
renderImpactReport: () => renderImpactReport,
|
|
61
74
|
renderImproveText: () => renderImproveText,
|
|
62
75
|
renderSimulateText: () => renderSimulateText,
|
|
76
|
+
resolveProvider: () => resolveProvider,
|
|
77
|
+
resolveWorldPath: () => resolveWorldPath,
|
|
78
|
+
runInteractiveMode: () => runInteractiveMode,
|
|
79
|
+
runPipeMode: () => runPipeMode,
|
|
80
|
+
setActiveWorld: () => setActiveWorld,
|
|
63
81
|
simulateWorld: () => simulateWorld,
|
|
64
82
|
summarizeAuditEvents: () => summarizeAuditEvents,
|
|
65
83
|
validateWorld: () => validateWorld,
|
|
@@ -67,6 +85,205 @@ __export(index_exports, {
|
|
|
67
85
|
});
|
|
68
86
|
module.exports = __toCommonJS(index_exports);
|
|
69
87
|
|
|
88
|
+
// src/engine/plan-engine.ts
|
|
89
|
+
function keywordMatch(eventText, step) {
|
|
90
|
+
const stepText = [
|
|
91
|
+
step.label,
|
|
92
|
+
step.description ?? "",
|
|
93
|
+
...step.tags ?? []
|
|
94
|
+
].join(" ").toLowerCase();
|
|
95
|
+
const keywords = stepText.split(/\s+/).filter((w) => w.length > 3);
|
|
96
|
+
if (keywords.length === 0) return false;
|
|
97
|
+
const matched = keywords.filter((kw) => eventText.includes(kw));
|
|
98
|
+
return matched.length >= Math.ceil(keywords.length * 0.5);
|
|
99
|
+
}
|
|
100
|
+
function tokenSimilarity(a, b) {
|
|
101
|
+
const tokensA = new Set(a.toLowerCase().split(/\s+/).filter((w) => w.length > 2));
|
|
102
|
+
const tokensB = new Set(b.toLowerCase().split(/\s+/).filter((w) => w.length > 2));
|
|
103
|
+
if (tokensA.size === 0 || tokensB.size === 0) return 0;
|
|
104
|
+
let intersection = 0;
|
|
105
|
+
for (const t of tokensA) {
|
|
106
|
+
if (tokensB.has(t)) intersection++;
|
|
107
|
+
}
|
|
108
|
+
const union = (/* @__PURE__ */ new Set([...tokensA, ...tokensB])).size;
|
|
109
|
+
return union > 0 ? intersection / union : 0;
|
|
110
|
+
}
|
|
111
|
+
function findMatchingStep(eventText, event, steps) {
|
|
112
|
+
const pendingOrActive = steps.filter((s) => s.status === "pending" || s.status === "active");
|
|
113
|
+
if (pendingOrActive.length === 0) {
|
|
114
|
+
return { matched: null, closest: null, closestScore: 0 };
|
|
115
|
+
}
|
|
116
|
+
for (const step of pendingOrActive) {
|
|
117
|
+
if (keywordMatch(eventText, step)) {
|
|
118
|
+
if (step.tools && event.tool && !step.tools.includes(event.tool)) {
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
return { matched: step, closest: step, closestScore: 1 };
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
const intentText = [event.intent, event.tool ?? "", event.scope ?? ""].join(" ");
|
|
125
|
+
let bestStep = null;
|
|
126
|
+
let bestScore = 0;
|
|
127
|
+
for (const step of pendingOrActive) {
|
|
128
|
+
const stepText = [step.label, step.description ?? "", ...step.tags ?? []].join(" ");
|
|
129
|
+
const score = tokenSimilarity(intentText, stepText);
|
|
130
|
+
if (score > bestScore) {
|
|
131
|
+
bestScore = score;
|
|
132
|
+
bestStep = step;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
const SIMILARITY_THRESHOLD = 0.35;
|
|
136
|
+
if (bestScore >= SIMILARITY_THRESHOLD && bestStep) {
|
|
137
|
+
if (bestStep.tools && event.tool && !bestStep.tools.includes(event.tool)) {
|
|
138
|
+
return { matched: null, closest: bestStep, closestScore: bestScore };
|
|
139
|
+
}
|
|
140
|
+
return { matched: bestStep, closest: bestStep, closestScore: bestScore };
|
|
141
|
+
}
|
|
142
|
+
return { matched: null, closest: bestStep, closestScore: bestScore };
|
|
143
|
+
}
|
|
144
|
+
function isSequenceValid(step, plan) {
|
|
145
|
+
if (!plan.sequential) return true;
|
|
146
|
+
if (!step.requires || step.requires.length === 0) return true;
|
|
147
|
+
return step.requires.every((reqId) => {
|
|
148
|
+
const reqStep = plan.steps.find((s) => s.id === reqId);
|
|
149
|
+
return reqStep?.status === "completed";
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
function checkConstraints(event, eventText, constraints) {
|
|
153
|
+
const checks = [];
|
|
154
|
+
for (const constraint of constraints) {
|
|
155
|
+
if (constraint.type === "approval") {
|
|
156
|
+
if (constraint.trigger && eventText.includes(constraint.trigger.substring(0, 10).toLowerCase())) {
|
|
157
|
+
checks.push({ constraintId: constraint.id, passed: false, reason: constraint.description });
|
|
158
|
+
return { violated: constraint, checks };
|
|
159
|
+
}
|
|
160
|
+
const keywords = constraint.description.toLowerCase().split(/\s+/).filter((w) => w.length > 3);
|
|
161
|
+
const relevant = keywords.some((kw) => eventText.includes(kw));
|
|
162
|
+
if (relevant) {
|
|
163
|
+
checks.push({ constraintId: constraint.id, passed: false, reason: constraint.description });
|
|
164
|
+
return { violated: constraint, checks };
|
|
165
|
+
}
|
|
166
|
+
checks.push({ constraintId: constraint.id, passed: true });
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
if (constraint.type === "scope" && constraint.trigger) {
|
|
170
|
+
const keywords = constraint.trigger.split(/\s+/).filter((w) => w.length > 3);
|
|
171
|
+
const violated = keywords.length > 0 && keywords.every((kw) => eventText.includes(kw));
|
|
172
|
+
checks.push({
|
|
173
|
+
constraintId: constraint.id,
|
|
174
|
+
passed: !violated,
|
|
175
|
+
reason: violated ? constraint.description : void 0
|
|
176
|
+
});
|
|
177
|
+
if (violated) {
|
|
178
|
+
return { violated: constraint, checks };
|
|
179
|
+
}
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
checks.push({ constraintId: constraint.id, passed: true });
|
|
183
|
+
}
|
|
184
|
+
return { violated: null, checks };
|
|
185
|
+
}
|
|
186
|
+
function getPlanProgress(plan) {
|
|
187
|
+
const completed = plan.steps.filter((s) => s.status === "completed").length;
|
|
188
|
+
const total = plan.steps.length;
|
|
189
|
+
return {
|
|
190
|
+
completed,
|
|
191
|
+
total,
|
|
192
|
+
percentage: total > 0 ? Math.round(completed / total * 100) : 0
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
function advancePlan(plan, stepId) {
|
|
196
|
+
return {
|
|
197
|
+
...plan,
|
|
198
|
+
steps: plan.steps.map(
|
|
199
|
+
(s) => s.id === stepId ? { ...s, status: "completed" } : s
|
|
200
|
+
)
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
function evaluatePlan(event, plan) {
|
|
204
|
+
const progress = getPlanProgress(plan);
|
|
205
|
+
if (plan.expires_at) {
|
|
206
|
+
const expiresAt = new Date(plan.expires_at).getTime();
|
|
207
|
+
if (Date.now() > expiresAt) {
|
|
208
|
+
return {
|
|
209
|
+
allowed: true,
|
|
210
|
+
status: "PLAN_COMPLETE",
|
|
211
|
+
reason: "Plan has expired.",
|
|
212
|
+
progress
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
if (progress.completed === progress.total) {
|
|
217
|
+
return {
|
|
218
|
+
allowed: true,
|
|
219
|
+
status: "PLAN_COMPLETE",
|
|
220
|
+
reason: "All plan steps are completed.",
|
|
221
|
+
progress
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
const eventText = [
|
|
225
|
+
event.intent,
|
|
226
|
+
event.tool ?? "",
|
|
227
|
+
event.scope ?? ""
|
|
228
|
+
].join(" ").toLowerCase();
|
|
229
|
+
const { matched, closest, closestScore } = findMatchingStep(eventText, event, plan.steps);
|
|
230
|
+
if (!matched) {
|
|
231
|
+
return {
|
|
232
|
+
allowed: false,
|
|
233
|
+
status: "OFF_PLAN",
|
|
234
|
+
reason: "Action does not match any plan step.",
|
|
235
|
+
closestStep: closest?.label,
|
|
236
|
+
similarityScore: closestScore,
|
|
237
|
+
progress
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
if (!isSequenceValid(matched, plan)) {
|
|
241
|
+
const pendingDeps = (matched.requires ?? []).filter((reqId) => plan.steps.find((s) => s.id === reqId)?.status !== "completed").join(", ");
|
|
242
|
+
return {
|
|
243
|
+
allowed: false,
|
|
244
|
+
status: "OFF_PLAN",
|
|
245
|
+
reason: `Step "${matched.label}" requires completion of: ${pendingDeps}`,
|
|
246
|
+
matchedStep: matched.id,
|
|
247
|
+
progress
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
const { violated } = checkConstraints(event, eventText, plan.constraints);
|
|
251
|
+
if (violated) {
|
|
252
|
+
return {
|
|
253
|
+
allowed: false,
|
|
254
|
+
status: "CONSTRAINT_VIOLATED",
|
|
255
|
+
reason: violated.description,
|
|
256
|
+
matchedStep: matched.id,
|
|
257
|
+
progress
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
return {
|
|
261
|
+
allowed: true,
|
|
262
|
+
status: "ON_PLAN",
|
|
263
|
+
reason: `Matches step: ${matched.label}`,
|
|
264
|
+
matchedStep: matched.id,
|
|
265
|
+
progress
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
function buildPlanCheck(event, plan, verdict) {
|
|
269
|
+
const eventText = [event.intent, event.tool ?? "", event.scope ?? ""].join(" ").toLowerCase();
|
|
270
|
+
const { matched, closest, closestScore } = findMatchingStep(eventText, event, plan.steps);
|
|
271
|
+
const { checks: constraintChecks } = checkConstraints(event, eventText, plan.constraints);
|
|
272
|
+
const progress = getPlanProgress(plan);
|
|
273
|
+
return {
|
|
274
|
+
planId: plan.plan_id,
|
|
275
|
+
matched: !!matched,
|
|
276
|
+
matchedStepId: matched?.id,
|
|
277
|
+
matchedStepLabel: matched?.label,
|
|
278
|
+
closestStepId: !matched ? closest?.id : void 0,
|
|
279
|
+
closestStepLabel: !matched ? closest?.label : void 0,
|
|
280
|
+
similarityScore: !matched ? closestScore : void 0,
|
|
281
|
+
sequenceValid: matched ? isSequenceValid(matched, plan) : void 0,
|
|
282
|
+
constraintsChecked: constraintChecks,
|
|
283
|
+
progress: { completed: progress.completed, total: progress.total }
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
70
287
|
// src/engine/guard-engine.ts
|
|
71
288
|
var PROMPT_INJECTION_PATTERNS = [
|
|
72
289
|
// Instruction override
|
|
@@ -158,6 +375,7 @@ function evaluateGuard(event, world, options = {}) {
|
|
|
158
375
|
const eventText = (event.intent + " " + (event.tool ?? "") + " " + (event.scope ?? "")).toLowerCase();
|
|
159
376
|
const invariantChecks = [];
|
|
160
377
|
const safetyChecks = [];
|
|
378
|
+
let planCheckResult;
|
|
161
379
|
const roleChecks = [];
|
|
162
380
|
const guardChecks = [];
|
|
163
381
|
const kernelRuleChecks = [];
|
|
@@ -185,6 +403,7 @@ function evaluateGuard(event, world, options = {}) {
|
|
|
185
403
|
includeTrace ? buildTrace(
|
|
186
404
|
invariantChecks,
|
|
187
405
|
safetyChecks,
|
|
406
|
+
planCheckResult,
|
|
188
407
|
roleChecks,
|
|
189
408
|
guardChecks,
|
|
190
409
|
kernelRuleChecks,
|
|
@@ -213,6 +432,7 @@ function evaluateGuard(event, world, options = {}) {
|
|
|
213
432
|
includeTrace ? buildTrace(
|
|
214
433
|
invariantChecks,
|
|
215
434
|
safetyChecks,
|
|
435
|
+
planCheckResult,
|
|
216
436
|
roleChecks,
|
|
217
437
|
guardChecks,
|
|
218
438
|
kernelRuleChecks,
|
|
@@ -223,6 +443,42 @@ function evaluateGuard(event, world, options = {}) {
|
|
|
223
443
|
) : void 0
|
|
224
444
|
);
|
|
225
445
|
}
|
|
446
|
+
if (options.plan) {
|
|
447
|
+
const planVerdict = evaluatePlan(event, options.plan);
|
|
448
|
+
planCheckResult = buildPlanCheck(event, options.plan, planVerdict);
|
|
449
|
+
if (!planVerdict.allowed && planVerdict.status !== "PLAN_COMPLETE") {
|
|
450
|
+
decidingLayer = "plan-enforcement";
|
|
451
|
+
decidingId = `plan-${options.plan.plan_id}`;
|
|
452
|
+
const planStatus = planVerdict.status === "CONSTRAINT_VIOLATED" ? "PAUSE" : "BLOCK";
|
|
453
|
+
let reason = planVerdict.reason ?? "Action blocked by plan.";
|
|
454
|
+
if (planVerdict.status === "OFF_PLAN" && planVerdict.closestStep) {
|
|
455
|
+
reason += ` Closest step: "${planVerdict.closestStep}" (similarity: ${(planVerdict.similarityScore ?? 0).toFixed(2)})`;
|
|
456
|
+
}
|
|
457
|
+
return buildVerdict(
|
|
458
|
+
planStatus,
|
|
459
|
+
reason,
|
|
460
|
+
`plan-${options.plan.plan_id}`,
|
|
461
|
+
void 0,
|
|
462
|
+
world,
|
|
463
|
+
level,
|
|
464
|
+
invariantChecks,
|
|
465
|
+
guardsMatched,
|
|
466
|
+
rulesMatched,
|
|
467
|
+
includeTrace ? buildTrace(
|
|
468
|
+
invariantChecks,
|
|
469
|
+
safetyChecks,
|
|
470
|
+
planCheckResult,
|
|
471
|
+
roleChecks,
|
|
472
|
+
guardChecks,
|
|
473
|
+
kernelRuleChecks,
|
|
474
|
+
levelChecks,
|
|
475
|
+
decidingLayer,
|
|
476
|
+
decidingId,
|
|
477
|
+
startTime
|
|
478
|
+
) : void 0
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
226
482
|
const roleVerdict = checkRoleRules(event, eventText, world, roleChecks);
|
|
227
483
|
if (roleVerdict) {
|
|
228
484
|
decidingLayer = "role";
|
|
@@ -240,6 +496,7 @@ function evaluateGuard(event, world, options = {}) {
|
|
|
240
496
|
includeTrace ? buildTrace(
|
|
241
497
|
invariantChecks,
|
|
242
498
|
safetyChecks,
|
|
499
|
+
planCheckResult,
|
|
243
500
|
roleChecks,
|
|
244
501
|
guardChecks,
|
|
245
502
|
kernelRuleChecks,
|
|
@@ -268,6 +525,7 @@ function evaluateGuard(event, world, options = {}) {
|
|
|
268
525
|
includeTrace ? buildTrace(
|
|
269
526
|
invariantChecks,
|
|
270
527
|
safetyChecks,
|
|
528
|
+
planCheckResult,
|
|
271
529
|
roleChecks,
|
|
272
530
|
guardChecks,
|
|
273
531
|
kernelRuleChecks,
|
|
@@ -296,6 +554,7 @@ function evaluateGuard(event, world, options = {}) {
|
|
|
296
554
|
includeTrace ? buildTrace(
|
|
297
555
|
invariantChecks,
|
|
298
556
|
safetyChecks,
|
|
557
|
+
planCheckResult,
|
|
299
558
|
roleChecks,
|
|
300
559
|
guardChecks,
|
|
301
560
|
kernelRuleChecks,
|
|
@@ -323,6 +582,7 @@ function evaluateGuard(event, world, options = {}) {
|
|
|
323
582
|
includeTrace ? buildTrace(
|
|
324
583
|
invariantChecks,
|
|
325
584
|
safetyChecks,
|
|
585
|
+
planCheckResult,
|
|
326
586
|
roleChecks,
|
|
327
587
|
guardChecks,
|
|
328
588
|
kernelRuleChecks,
|
|
@@ -347,6 +607,7 @@ function evaluateGuard(event, world, options = {}) {
|
|
|
347
607
|
includeTrace ? buildTrace(
|
|
348
608
|
invariantChecks,
|
|
349
609
|
safetyChecks,
|
|
610
|
+
planCheckResult,
|
|
350
611
|
roleChecks,
|
|
351
612
|
guardChecks,
|
|
352
613
|
kernelRuleChecks,
|
|
@@ -684,8 +945,8 @@ function matchesKeywords(eventText, ruleText) {
|
|
|
684
945
|
function eventToAllowlistKey(event) {
|
|
685
946
|
return `${(event.tool ?? "*").toLowerCase()}::${event.intent.toLowerCase().trim()}`;
|
|
686
947
|
}
|
|
687
|
-
function buildTrace(invariantChecks, safetyChecks, roleChecks, guardChecks, kernelRuleChecks, levelChecks, decidingLayer, decidingId, startTime) {
|
|
688
|
-
|
|
948
|
+
function buildTrace(invariantChecks, safetyChecks, planCheck, roleChecks, guardChecks, kernelRuleChecks, levelChecks, decidingLayer, decidingId, startTime) {
|
|
949
|
+
const trace = {
|
|
689
950
|
invariantChecks,
|
|
690
951
|
safetyChecks,
|
|
691
952
|
roleChecks,
|
|
@@ -703,6 +964,7 @@ function buildTrace(invariantChecks, safetyChecks, roleChecks, guardChecks, kern
|
|
|
703
964
|
"safety-scope-escape",
|
|
704
965
|
"safety-execution-claim",
|
|
705
966
|
"safety-execution-intent",
|
|
967
|
+
"plan-enforcement",
|
|
706
968
|
"role-rules",
|
|
707
969
|
"declarative-guards",
|
|
708
970
|
"kernel-rules",
|
|
@@ -712,6 +974,10 @@ function buildTrace(invariantChecks, safetyChecks, roleChecks, guardChecks, kern
|
|
|
712
974
|
},
|
|
713
975
|
durationMs: performance.now() - startTime
|
|
714
976
|
};
|
|
977
|
+
if (planCheck) {
|
|
978
|
+
trace.planCheck = planCheck;
|
|
979
|
+
}
|
|
980
|
+
return trace;
|
|
715
981
|
}
|
|
716
982
|
function buildVerdict(status, reason, ruleId, warning, world, level, invariantChecks, guardsMatched, rulesMatched, trace) {
|
|
717
983
|
const evidence = {
|
|
@@ -744,6 +1010,1303 @@ var GUARD_EXIT_CODES = {
|
|
|
744
1010
|
ERROR: 3
|
|
745
1011
|
};
|
|
746
1012
|
|
|
1013
|
+
// src/engine/plan-parser.ts
|
|
1014
|
+
function slugify(text) {
|
|
1015
|
+
return text.toLowerCase().replace(/[^a-z0-9\s]/g, "").trim().replace(/\s+/g, "_");
|
|
1016
|
+
}
|
|
1017
|
+
function extractBracketAnnotation(line, key) {
|
|
1018
|
+
const regex = new RegExp(`\\[${key}:\\s*([^\\]]+)\\]`, "i");
|
|
1019
|
+
const match = line.match(regex);
|
|
1020
|
+
if (!match) return null;
|
|
1021
|
+
return match[1].split(",").map((s) => s.trim()).filter(Boolean);
|
|
1022
|
+
}
|
|
1023
|
+
function extractParenAnnotation(line, key) {
|
|
1024
|
+
const regex = new RegExp(`\\(${key}:\\s*([^)]+)\\)`, "i");
|
|
1025
|
+
const match = line.match(regex);
|
|
1026
|
+
if (!match) return null;
|
|
1027
|
+
return match[1].split(",").map((s) => s.trim()).filter(Boolean);
|
|
1028
|
+
}
|
|
1029
|
+
function stripAnnotations(line) {
|
|
1030
|
+
return line.replace(/\[(?:tools|tag|verify|type):\s*[^\]]+\]/gi, "").replace(/\((?:after):\s*[^)]+\)/gi, "").trim();
|
|
1031
|
+
}
|
|
1032
|
+
function parseFrontmatter(content) {
|
|
1033
|
+
const frontmatter = {};
|
|
1034
|
+
const fmMatch = content.match(/^---\s*\n([\s\S]*?)\n---\s*\n/);
|
|
1035
|
+
if (!fmMatch) {
|
|
1036
|
+
return { frontmatter, body: content };
|
|
1037
|
+
}
|
|
1038
|
+
const fmBody = fmMatch[1];
|
|
1039
|
+
for (const line of fmBody.split("\n")) {
|
|
1040
|
+
const colonIndex = line.indexOf(":");
|
|
1041
|
+
if (colonIndex > 0) {
|
|
1042
|
+
const key = line.slice(0, colonIndex).trim();
|
|
1043
|
+
const value = line.slice(colonIndex + 1).trim();
|
|
1044
|
+
frontmatter[key] = value;
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
return { frontmatter, body: content.slice(fmMatch[0].length) };
|
|
1048
|
+
}
|
|
1049
|
+
function parseSections(body) {
|
|
1050
|
+
const steps = [];
|
|
1051
|
+
const constraints = [];
|
|
1052
|
+
let currentSection = "none";
|
|
1053
|
+
for (const line of body.split("\n")) {
|
|
1054
|
+
const trimmed = line.trim();
|
|
1055
|
+
if (/^#+\s*Steps/i.test(trimmed)) {
|
|
1056
|
+
currentSection = "steps";
|
|
1057
|
+
continue;
|
|
1058
|
+
}
|
|
1059
|
+
if (/^#+\s*Constraints/i.test(trimmed)) {
|
|
1060
|
+
currentSection = "constraints";
|
|
1061
|
+
continue;
|
|
1062
|
+
}
|
|
1063
|
+
if (/^#+\s/.test(trimmed) && currentSection !== "none") {
|
|
1064
|
+
currentSection = "none";
|
|
1065
|
+
continue;
|
|
1066
|
+
}
|
|
1067
|
+
if (trimmed.startsWith("- ")) {
|
|
1068
|
+
const item = trimmed.slice(2).trim();
|
|
1069
|
+
if (currentSection === "steps") {
|
|
1070
|
+
steps.push(item);
|
|
1071
|
+
} else if (currentSection === "constraints") {
|
|
1072
|
+
constraints.push(item);
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
return { steps, constraints };
|
|
1077
|
+
}
|
|
1078
|
+
function parseStep(raw) {
|
|
1079
|
+
const label = stripAnnotations(raw);
|
|
1080
|
+
const id = slugify(label);
|
|
1081
|
+
const tools = extractBracketAnnotation(raw, "tools");
|
|
1082
|
+
const tags = extractBracketAnnotation(raw, "tag");
|
|
1083
|
+
const verifyArr = extractBracketAnnotation(raw, "verify");
|
|
1084
|
+
const requires = extractParenAnnotation(raw, "after");
|
|
1085
|
+
return {
|
|
1086
|
+
id,
|
|
1087
|
+
label,
|
|
1088
|
+
tools: tools ?? void 0,
|
|
1089
|
+
tags: tags ?? void 0,
|
|
1090
|
+
verify: verifyArr?.[0] ?? void 0,
|
|
1091
|
+
requires: requires ?? void 0,
|
|
1092
|
+
status: "pending"
|
|
1093
|
+
};
|
|
1094
|
+
}
|
|
1095
|
+
function parseConstraint(raw, index) {
|
|
1096
|
+
const typeAnnotation = extractBracketAnnotation(raw, "type");
|
|
1097
|
+
const description = stripAnnotations(raw);
|
|
1098
|
+
const id = `constraint_${index}`;
|
|
1099
|
+
let type = "custom";
|
|
1100
|
+
let enforcement = "block";
|
|
1101
|
+
let limit;
|
|
1102
|
+
let unit;
|
|
1103
|
+
if (typeAnnotation?.[0] === "approval") {
|
|
1104
|
+
type = "approval";
|
|
1105
|
+
enforcement = "pause";
|
|
1106
|
+
} else if (/budget|\$|spending|cost/i.test(description)) {
|
|
1107
|
+
type = "budget";
|
|
1108
|
+
const amountMatch = description.match(/\$?([\d,]+)/);
|
|
1109
|
+
if (amountMatch) {
|
|
1110
|
+
limit = parseInt(amountMatch[1].replace(/,/g, ""), 10);
|
|
1111
|
+
unit = "USD";
|
|
1112
|
+
}
|
|
1113
|
+
} else if (/time|hour|minute|day|deadline/i.test(description)) {
|
|
1114
|
+
type = "time";
|
|
1115
|
+
} else if (/scope|access|database|production/i.test(description)) {
|
|
1116
|
+
type = "scope";
|
|
1117
|
+
}
|
|
1118
|
+
const trigger = description.toLowerCase();
|
|
1119
|
+
return {
|
|
1120
|
+
id,
|
|
1121
|
+
type,
|
|
1122
|
+
description,
|
|
1123
|
+
enforcement,
|
|
1124
|
+
limit,
|
|
1125
|
+
unit,
|
|
1126
|
+
trigger
|
|
1127
|
+
};
|
|
1128
|
+
}
|
|
1129
|
+
function parsePlanMarkdown(markdown) {
|
|
1130
|
+
const errors = [];
|
|
1131
|
+
const { frontmatter, body } = parseFrontmatter(markdown.trim());
|
|
1132
|
+
const { steps: stepLines, constraints: constraintLines } = parseSections(body);
|
|
1133
|
+
if (!frontmatter.plan_id) {
|
|
1134
|
+
errors.push("Missing required field: plan_id");
|
|
1135
|
+
}
|
|
1136
|
+
if (stepLines.length === 0) {
|
|
1137
|
+
errors.push("Plan must have at least one step");
|
|
1138
|
+
}
|
|
1139
|
+
if (errors.length > 0) {
|
|
1140
|
+
return { success: false, errors };
|
|
1141
|
+
}
|
|
1142
|
+
const steps = stepLines.map((line) => parseStep(line));
|
|
1143
|
+
const constraints = constraintLines.map((line, i) => parseConstraint(line, i));
|
|
1144
|
+
let expires_at;
|
|
1145
|
+
if (frontmatter.expires) {
|
|
1146
|
+
expires_at = new Date(frontmatter.expires).toISOString();
|
|
1147
|
+
}
|
|
1148
|
+
const plan = {
|
|
1149
|
+
plan_id: frontmatter.plan_id,
|
|
1150
|
+
objective: frontmatter.objective ?? "",
|
|
1151
|
+
sequential: frontmatter.sequential === "true",
|
|
1152
|
+
steps,
|
|
1153
|
+
constraints,
|
|
1154
|
+
world_id: frontmatter.world ?? void 0,
|
|
1155
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1156
|
+
expires_at
|
|
1157
|
+
};
|
|
1158
|
+
return { success: true, plan, errors: [] };
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
// src/contracts/plan-contract.ts
|
|
1162
|
+
var PLAN_EXIT_CODES = {
|
|
1163
|
+
ON_PLAN: 0,
|
|
1164
|
+
OFF_PLAN: 1,
|
|
1165
|
+
CONSTRAINT_VIOLATED: 2,
|
|
1166
|
+
ERROR: 3,
|
|
1167
|
+
PLAN_COMPLETE: 4
|
|
1168
|
+
};
|
|
1169
|
+
|
|
1170
|
+
// src/loader/world-loader.ts
|
|
1171
|
+
async function loadWorldFromDirectory(dirPath) {
|
|
1172
|
+
const { readFile: readFile3 } = await import("fs/promises");
|
|
1173
|
+
const { join: join5 } = await import("path");
|
|
1174
|
+
const { readdirSync: readdirSync3 } = await import("fs");
|
|
1175
|
+
async function readJson(filename) {
|
|
1176
|
+
try {
|
|
1177
|
+
const content = await readFile3(join5(dirPath, filename), "utf-8");
|
|
1178
|
+
return JSON.parse(content);
|
|
1179
|
+
} catch {
|
|
1180
|
+
return void 0;
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
const worldJson = await readJson("world.json");
|
|
1184
|
+
if (!worldJson) {
|
|
1185
|
+
throw new Error(`Cannot read world.json in ${dirPath}`);
|
|
1186
|
+
}
|
|
1187
|
+
const invariantsJson = await readJson("invariants.json");
|
|
1188
|
+
const assumptionsJson = await readJson("assumptions.json");
|
|
1189
|
+
const stateSchemaJson = await readJson("state-schema.json");
|
|
1190
|
+
const gatesJson = await readJson("gates.json");
|
|
1191
|
+
const outcomesJson = await readJson("outcomes.json");
|
|
1192
|
+
const guardsJson = await readJson("guards.json");
|
|
1193
|
+
const rolesJson = await readJson("roles.json");
|
|
1194
|
+
const kernelJson = await readJson("kernel.json");
|
|
1195
|
+
const metadataJson = await readJson("metadata.json");
|
|
1196
|
+
const rules = [];
|
|
1197
|
+
try {
|
|
1198
|
+
const rulesDir = join5(dirPath, "rules");
|
|
1199
|
+
const ruleFiles = readdirSync3(rulesDir).filter((f) => f.endsWith(".json")).sort();
|
|
1200
|
+
for (const file of ruleFiles) {
|
|
1201
|
+
const content = await readFile3(join5(rulesDir, file), "utf-8");
|
|
1202
|
+
rules.push(JSON.parse(content));
|
|
1203
|
+
}
|
|
1204
|
+
} catch {
|
|
1205
|
+
}
|
|
1206
|
+
return {
|
|
1207
|
+
world: worldJson,
|
|
1208
|
+
invariants: invariantsJson?.invariants ?? [],
|
|
1209
|
+
assumptions: assumptionsJson ?? { profiles: {}, parameter_definitions: {} },
|
|
1210
|
+
stateSchema: stateSchemaJson ?? { variables: {}, presets: {} },
|
|
1211
|
+
rules,
|
|
1212
|
+
gates: gatesJson ?? {
|
|
1213
|
+
viability_classification: [],
|
|
1214
|
+
structural_override: { description: "", enforcement: "mandatory" },
|
|
1215
|
+
sustainability_threshold: 0,
|
|
1216
|
+
collapse_visual: { background: "", text: "", border: "", label: "" }
|
|
1217
|
+
},
|
|
1218
|
+
outcomes: outcomesJson ?? {
|
|
1219
|
+
computed_outcomes: [],
|
|
1220
|
+
comparison_layout: { primary_card: "", status_badge: "", structural_indicators: [] }
|
|
1221
|
+
},
|
|
1222
|
+
guards: guardsJson,
|
|
1223
|
+
roles: rolesJson,
|
|
1224
|
+
kernel: kernelJson,
|
|
1225
|
+
metadata: metadataJson ?? {
|
|
1226
|
+
format_version: "1.0.0",
|
|
1227
|
+
created_at: "",
|
|
1228
|
+
last_modified: "",
|
|
1229
|
+
authoring_method: "manual-authoring"
|
|
1230
|
+
}
|
|
1231
|
+
};
|
|
1232
|
+
}
|
|
1233
|
+
async function loadWorld(worldPath) {
|
|
1234
|
+
const { stat } = await import("fs/promises");
|
|
1235
|
+
const info = await stat(worldPath);
|
|
1236
|
+
if (info.isDirectory()) {
|
|
1237
|
+
return loadWorldFromDirectory(worldPath);
|
|
1238
|
+
}
|
|
1239
|
+
if (worldPath.endsWith(".nv-world.zip")) {
|
|
1240
|
+
throw new Error(".nv-world.zip loading not yet implemented \u2014 use a world directory");
|
|
1241
|
+
}
|
|
1242
|
+
throw new Error(`Cannot load world from: ${worldPath} \u2014 expected a directory or .nv-world.zip`);
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
// src/runtime/session.ts
|
|
1246
|
+
async function defaultToolExecutor(name, args) {
|
|
1247
|
+
return `Tool "${name}" executed successfully with args: ${JSON.stringify(args)}`;
|
|
1248
|
+
}
|
|
1249
|
+
var SessionManager = class {
|
|
1250
|
+
config;
|
|
1251
|
+
state;
|
|
1252
|
+
engineOptions;
|
|
1253
|
+
executor;
|
|
1254
|
+
constructor(config) {
|
|
1255
|
+
this.config = config;
|
|
1256
|
+
this.executor = config.toolExecutor ?? defaultToolExecutor;
|
|
1257
|
+
this.engineOptions = {
|
|
1258
|
+
trace: config.trace ?? false,
|
|
1259
|
+
level: config.level,
|
|
1260
|
+
plan: config.plan
|
|
1261
|
+
};
|
|
1262
|
+
this.state = {
|
|
1263
|
+
active: false,
|
|
1264
|
+
world: config.world,
|
|
1265
|
+
plan: config.plan,
|
|
1266
|
+
progress: config.plan ? getPlanProgress(config.plan) : void 0,
|
|
1267
|
+
actionsEvaluated: 0,
|
|
1268
|
+
actionsAllowed: 0,
|
|
1269
|
+
actionsBlocked: 0,
|
|
1270
|
+
actionsPaused: 0
|
|
1271
|
+
};
|
|
1272
|
+
}
|
|
1273
|
+
/**
|
|
1274
|
+
* Initialize the session — load world from disk if needed.
|
|
1275
|
+
*/
|
|
1276
|
+
async start() {
|
|
1277
|
+
if (this.config.worldPath && !this.config.world) {
|
|
1278
|
+
this.state.world = await loadWorld(this.config.worldPath);
|
|
1279
|
+
}
|
|
1280
|
+
if (!this.state.world) {
|
|
1281
|
+
throw new Error("No world provided. Use --world or pass a world definition.");
|
|
1282
|
+
}
|
|
1283
|
+
this.state.active = true;
|
|
1284
|
+
return this.getState();
|
|
1285
|
+
}
|
|
1286
|
+
/**
|
|
1287
|
+
* Evaluate a single event against governance.
|
|
1288
|
+
* Returns the verdict without executing anything.
|
|
1289
|
+
*/
|
|
1290
|
+
evaluate(event) {
|
|
1291
|
+
this.engineOptions.plan = this.state.plan;
|
|
1292
|
+
const verdict = evaluateGuard(event, this.state.world, this.engineOptions);
|
|
1293
|
+
this.state.actionsEvaluated++;
|
|
1294
|
+
if (verdict.status === "ALLOW") this.state.actionsAllowed++;
|
|
1295
|
+
if (verdict.status === "BLOCK") this.state.actionsBlocked++;
|
|
1296
|
+
if (verdict.status === "PAUSE") this.state.actionsPaused++;
|
|
1297
|
+
this.config.onVerdict?.(verdict, event);
|
|
1298
|
+
return verdict;
|
|
1299
|
+
}
|
|
1300
|
+
/**
|
|
1301
|
+
* Evaluate and execute a tool call.
|
|
1302
|
+
* Returns the execution result or block reason.
|
|
1303
|
+
*/
|
|
1304
|
+
async executeToolCall(toolCall) {
|
|
1305
|
+
let args;
|
|
1306
|
+
try {
|
|
1307
|
+
args = JSON.parse(toolCall.function.arguments);
|
|
1308
|
+
} catch {
|
|
1309
|
+
args = { raw: toolCall.function.arguments };
|
|
1310
|
+
}
|
|
1311
|
+
const event = {
|
|
1312
|
+
intent: toolCall.function.name,
|
|
1313
|
+
tool: toolCall.function.name,
|
|
1314
|
+
args,
|
|
1315
|
+
direction: "input"
|
|
1316
|
+
};
|
|
1317
|
+
const verdict = this.evaluate(event);
|
|
1318
|
+
if (verdict.status === "BLOCK") {
|
|
1319
|
+
return { allowed: false, verdict };
|
|
1320
|
+
}
|
|
1321
|
+
if (verdict.status === "PAUSE") {
|
|
1322
|
+
return { allowed: false, verdict };
|
|
1323
|
+
}
|
|
1324
|
+
const result = await this.executor(toolCall.function.name, args);
|
|
1325
|
+
this.config.onToolResult?.(toolCall.function.name, result);
|
|
1326
|
+
if (this.state.plan) {
|
|
1327
|
+
const planVerdict = evaluatePlan(event, this.state.plan);
|
|
1328
|
+
if (planVerdict.matchedStep) {
|
|
1329
|
+
this.state.plan = advancePlan(this.state.plan, planVerdict.matchedStep);
|
|
1330
|
+
this.engineOptions.plan = this.state.plan;
|
|
1331
|
+
this.state.progress = getPlanProgress(this.state.plan);
|
|
1332
|
+
this.config.onPlanProgress?.(this.state.progress);
|
|
1333
|
+
if (this.state.progress.completed === this.state.progress.total) {
|
|
1334
|
+
this.config.onPlanComplete?.();
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
return { allowed: true, verdict, result };
|
|
1339
|
+
}
|
|
1340
|
+
/**
|
|
1341
|
+
* Process a model response — evaluate and execute all tool calls.
|
|
1342
|
+
* Returns results for each tool call.
|
|
1343
|
+
*/
|
|
1344
|
+
async processModelResponse(response, model) {
|
|
1345
|
+
if (response.toolCalls.length === 0) {
|
|
1346
|
+
return response;
|
|
1347
|
+
}
|
|
1348
|
+
for (const toolCall of response.toolCalls) {
|
|
1349
|
+
const { allowed, verdict, result } = await this.executeToolCall(toolCall);
|
|
1350
|
+
if (allowed && result) {
|
|
1351
|
+
const nextResponse = await model.sendToolResult(toolCall.id, result);
|
|
1352
|
+
if (nextResponse.toolCalls.length > 0) {
|
|
1353
|
+
return this.processModelResponse(nextResponse, model);
|
|
1354
|
+
}
|
|
1355
|
+
return nextResponse;
|
|
1356
|
+
} else {
|
|
1357
|
+
const reason = verdict.reason ?? "Action blocked by governance.";
|
|
1358
|
+
const nextResponse = await model.sendBlockedResult(toolCall.id, reason);
|
|
1359
|
+
if (nextResponse.toolCalls.length > 0) {
|
|
1360
|
+
return this.processModelResponse(nextResponse, model);
|
|
1361
|
+
}
|
|
1362
|
+
return nextResponse;
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
return response;
|
|
1366
|
+
}
|
|
1367
|
+
/** Get current session state. */
|
|
1368
|
+
getState() {
|
|
1369
|
+
return { ...this.state };
|
|
1370
|
+
}
|
|
1371
|
+
/** Stop the session. */
|
|
1372
|
+
stop() {
|
|
1373
|
+
this.state.active = false;
|
|
1374
|
+
return this.getState();
|
|
1375
|
+
}
|
|
1376
|
+
};
|
|
1377
|
+
async function runPipeMode(config) {
|
|
1378
|
+
const session = new SessionManager(config);
|
|
1379
|
+
await session.start();
|
|
1380
|
+
const state = session.getState();
|
|
1381
|
+
process.stderr.write(`[neuroverse] Pipe mode active
|
|
1382
|
+
`);
|
|
1383
|
+
process.stderr.write(`[neuroverse] World: ${state.world.world.name}
|
|
1384
|
+
`);
|
|
1385
|
+
if (state.plan) {
|
|
1386
|
+
process.stderr.write(`[neuroverse] Plan: ${state.plan.plan_id} (${state.plan.objective})
|
|
1387
|
+
`);
|
|
1388
|
+
}
|
|
1389
|
+
return new Promise((resolve3, reject) => {
|
|
1390
|
+
let buffer = "";
|
|
1391
|
+
process.stdin.setEncoding("utf-8");
|
|
1392
|
+
process.stdin.on("data", (chunk) => {
|
|
1393
|
+
buffer += chunk;
|
|
1394
|
+
const lines = buffer.split("\n");
|
|
1395
|
+
buffer = lines.pop() ?? "";
|
|
1396
|
+
for (const line of lines) {
|
|
1397
|
+
const trimmed = line.trim();
|
|
1398
|
+
if (!trimmed) continue;
|
|
1399
|
+
try {
|
|
1400
|
+
const event = JSON.parse(trimmed);
|
|
1401
|
+
if (!event.intent) {
|
|
1402
|
+
process.stderr.write(`[neuroverse] Warning: event missing "intent" field
|
|
1403
|
+
`);
|
|
1404
|
+
continue;
|
|
1405
|
+
}
|
|
1406
|
+
const verdict = session.evaluate(event);
|
|
1407
|
+
process.stdout.write(JSON.stringify(verdict) + "\n");
|
|
1408
|
+
} catch (err) {
|
|
1409
|
+
process.stderr.write(`[neuroverse] Error parsing line: ${err}
|
|
1410
|
+
`);
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
});
|
|
1414
|
+
process.stdin.on("end", () => {
|
|
1415
|
+
if (buffer.trim()) {
|
|
1416
|
+
try {
|
|
1417
|
+
const event = JSON.parse(buffer.trim());
|
|
1418
|
+
if (event.intent) {
|
|
1419
|
+
const verdict = session.evaluate(event);
|
|
1420
|
+
process.stdout.write(JSON.stringify(verdict) + "\n");
|
|
1421
|
+
}
|
|
1422
|
+
} catch {
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
const finalState = session.stop();
|
|
1426
|
+
process.stderr.write(
|
|
1427
|
+
`[neuroverse] Session complete: ${finalState.actionsEvaluated} evaluated, ${finalState.actionsAllowed} allowed, ${finalState.actionsBlocked} blocked, ${finalState.actionsPaused} paused
|
|
1428
|
+
`
|
|
1429
|
+
);
|
|
1430
|
+
resolve3();
|
|
1431
|
+
});
|
|
1432
|
+
process.stdin.on("error", reject);
|
|
1433
|
+
});
|
|
1434
|
+
}
|
|
1435
|
+
async function runInteractiveMode(config, model) {
|
|
1436
|
+
const session = new SessionManager(config);
|
|
1437
|
+
await session.start();
|
|
1438
|
+
const state = session.getState();
|
|
1439
|
+
process.stdout.write("\n");
|
|
1440
|
+
process.stdout.write(` World: ${state.world.world.name}
|
|
1441
|
+
`);
|
|
1442
|
+
if (state.plan) {
|
|
1443
|
+
process.stdout.write(` Plan: ${state.plan.plan_id}
|
|
1444
|
+
`);
|
|
1445
|
+
process.stdout.write(` Goal: ${state.plan.objective}
|
|
1446
|
+
`);
|
|
1447
|
+
process.stdout.write(` Steps: ${state.progress?.total ?? 0}
|
|
1448
|
+
`);
|
|
1449
|
+
}
|
|
1450
|
+
process.stdout.write(` Type "exit" to end session.
|
|
1451
|
+
`);
|
|
1452
|
+
process.stdout.write("\n");
|
|
1453
|
+
const readline = await import("readline");
|
|
1454
|
+
const rl = readline.createInterface({
|
|
1455
|
+
input: process.stdin,
|
|
1456
|
+
output: process.stdout,
|
|
1457
|
+
prompt: "> "
|
|
1458
|
+
});
|
|
1459
|
+
const printProgress = () => {
|
|
1460
|
+
const s = session.getState();
|
|
1461
|
+
if (s.progress) {
|
|
1462
|
+
process.stdout.write(
|
|
1463
|
+
` [plan: ${s.progress.completed}/${s.progress.total} (${s.progress.percentage}%)]
|
|
1464
|
+
`
|
|
1465
|
+
);
|
|
1466
|
+
}
|
|
1467
|
+
};
|
|
1468
|
+
rl.prompt();
|
|
1469
|
+
rl.on("line", async (input) => {
|
|
1470
|
+
const trimmed = input.trim();
|
|
1471
|
+
if (!trimmed) {
|
|
1472
|
+
rl.prompt();
|
|
1473
|
+
return;
|
|
1474
|
+
}
|
|
1475
|
+
if (trimmed === "exit" || trimmed === "quit") {
|
|
1476
|
+
const finalState = session.stop();
|
|
1477
|
+
process.stdout.write("\n");
|
|
1478
|
+
process.stdout.write(` Session complete.
|
|
1479
|
+
`);
|
|
1480
|
+
process.stdout.write(` Actions: ${finalState.actionsEvaluated} evaluated`);
|
|
1481
|
+
process.stdout.write(`, ${finalState.actionsAllowed} allowed`);
|
|
1482
|
+
process.stdout.write(`, ${finalState.actionsBlocked} blocked
|
|
1483
|
+
`);
|
|
1484
|
+
if (finalState.progress) {
|
|
1485
|
+
process.stdout.write(
|
|
1486
|
+
` Plan: ${finalState.progress.completed}/${finalState.progress.total} steps completed
|
|
1487
|
+
`
|
|
1488
|
+
);
|
|
1489
|
+
}
|
|
1490
|
+
process.stdout.write("\n");
|
|
1491
|
+
rl.close();
|
|
1492
|
+
return;
|
|
1493
|
+
}
|
|
1494
|
+
if (trimmed === "status") {
|
|
1495
|
+
const s = session.getState();
|
|
1496
|
+
process.stdout.write(`
|
|
1497
|
+
World: ${s.world.world.name}
|
|
1498
|
+
`);
|
|
1499
|
+
process.stdout.write(` Actions: ${s.actionsEvaluated} evaluated
|
|
1500
|
+
`);
|
|
1501
|
+
process.stdout.write(` Allowed: ${s.actionsAllowed} | Blocked: ${s.actionsBlocked} | Paused: ${s.actionsPaused}
|
|
1502
|
+
`);
|
|
1503
|
+
if (s.progress && s.plan) {
|
|
1504
|
+
process.stdout.write(` Plan: ${s.plan.plan_id} \u2014 ${s.progress.completed}/${s.progress.total} (${s.progress.percentage}%)
|
|
1505
|
+
`);
|
|
1506
|
+
for (const step of s.plan.steps) {
|
|
1507
|
+
const icon = step.status === "completed" ? "[x]" : "[ ]";
|
|
1508
|
+
process.stdout.write(` ${icon} ${step.label}
|
|
1509
|
+
`);
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
process.stdout.write("\n");
|
|
1513
|
+
rl.prompt();
|
|
1514
|
+
return;
|
|
1515
|
+
}
|
|
1516
|
+
try {
|
|
1517
|
+
const response = await model.chat(trimmed);
|
|
1518
|
+
if (response.toolCalls.length > 0) {
|
|
1519
|
+
const finalResponse = await session.processModelResponse(response, model);
|
|
1520
|
+
if (finalResponse.content) {
|
|
1521
|
+
process.stdout.write(`
|
|
1522
|
+
${finalResponse.content}
|
|
1523
|
+
|
|
1524
|
+
`);
|
|
1525
|
+
}
|
|
1526
|
+
printProgress();
|
|
1527
|
+
} else if (response.content) {
|
|
1528
|
+
process.stdout.write(`
|
|
1529
|
+
${response.content}
|
|
1530
|
+
|
|
1531
|
+
`);
|
|
1532
|
+
}
|
|
1533
|
+
} catch (err) {
|
|
1534
|
+
process.stderr.write(`
|
|
1535
|
+
Error: ${err}
|
|
1536
|
+
|
|
1537
|
+
`);
|
|
1538
|
+
}
|
|
1539
|
+
rl.prompt();
|
|
1540
|
+
});
|
|
1541
|
+
rl.on("close", () => {
|
|
1542
|
+
session.stop();
|
|
1543
|
+
});
|
|
1544
|
+
return new Promise((resolve3) => {
|
|
1545
|
+
rl.on("close", resolve3);
|
|
1546
|
+
});
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
// src/runtime/model-adapter.ts
|
|
1550
|
+
var DEFAULT_SYSTEM_PROMPT = `You are an AI assistant operating under NeuroVerse governance.
|
|
1551
|
+
All your tool calls are evaluated against governance rules before execution.
|
|
1552
|
+
If an action is blocked, you will be told why. Adjust your approach accordingly.
|
|
1553
|
+
Do not attempt to bypass governance rules.`;
|
|
1554
|
+
var ModelAdapter = class {
|
|
1555
|
+
config;
|
|
1556
|
+
messages;
|
|
1557
|
+
tools;
|
|
1558
|
+
constructor(config, tools = []) {
|
|
1559
|
+
this.config = config;
|
|
1560
|
+
this.tools = tools;
|
|
1561
|
+
this.messages = [];
|
|
1562
|
+
const systemPrompt = config.systemPrompt ?? DEFAULT_SYSTEM_PROMPT;
|
|
1563
|
+
this.messages.push({ role: "system", content: systemPrompt });
|
|
1564
|
+
}
|
|
1565
|
+
/**
|
|
1566
|
+
* Send a user message and get the model's response.
|
|
1567
|
+
*/
|
|
1568
|
+
async chat(userMessage) {
|
|
1569
|
+
this.messages.push({ role: "user", content: userMessage });
|
|
1570
|
+
return this.complete();
|
|
1571
|
+
}
|
|
1572
|
+
/**
|
|
1573
|
+
* Send a tool result back to the model and get the next response.
|
|
1574
|
+
*/
|
|
1575
|
+
async sendToolResult(toolCallId, result) {
|
|
1576
|
+
this.messages.push({
|
|
1577
|
+
role: "tool",
|
|
1578
|
+
content: result,
|
|
1579
|
+
tool_call_id: toolCallId
|
|
1580
|
+
});
|
|
1581
|
+
return this.complete();
|
|
1582
|
+
}
|
|
1583
|
+
/**
|
|
1584
|
+
* Send a governance block message as a tool result.
|
|
1585
|
+
*/
|
|
1586
|
+
async sendBlockedResult(toolCallId, reason) {
|
|
1587
|
+
return this.sendToolResult(
|
|
1588
|
+
toolCallId,
|
|
1589
|
+
`[GOVERNANCE BLOCKED] ${reason}. Please adjust your approach.`
|
|
1590
|
+
);
|
|
1591
|
+
}
|
|
1592
|
+
/**
|
|
1593
|
+
* Call the model API and parse the response.
|
|
1594
|
+
*/
|
|
1595
|
+
async complete() {
|
|
1596
|
+
const url = `${this.config.baseUrl}/chat/completions`;
|
|
1597
|
+
const body = {
|
|
1598
|
+
model: this.config.model,
|
|
1599
|
+
messages: this.messages,
|
|
1600
|
+
max_tokens: this.config.maxTokens ?? 4096
|
|
1601
|
+
};
|
|
1602
|
+
if (this.tools.length > 0) {
|
|
1603
|
+
body.tools = this.tools;
|
|
1604
|
+
}
|
|
1605
|
+
const response = await fetch(url, {
|
|
1606
|
+
method: "POST",
|
|
1607
|
+
headers: {
|
|
1608
|
+
"Content-Type": "application/json",
|
|
1609
|
+
"Authorization": `Bearer ${this.config.apiKey}`
|
|
1610
|
+
},
|
|
1611
|
+
body: JSON.stringify(body)
|
|
1612
|
+
});
|
|
1613
|
+
if (!response.ok) {
|
|
1614
|
+
const text = await response.text();
|
|
1615
|
+
throw new Error(`Model API error ${response.status}: ${text}`);
|
|
1616
|
+
}
|
|
1617
|
+
const data = await response.json();
|
|
1618
|
+
const choice = data.choices?.[0];
|
|
1619
|
+
if (!choice) {
|
|
1620
|
+
throw new Error("Model returned no choices");
|
|
1621
|
+
}
|
|
1622
|
+
const message = choice.message;
|
|
1623
|
+
this.messages.push(message);
|
|
1624
|
+
return {
|
|
1625
|
+
content: message.content ?? null,
|
|
1626
|
+
toolCalls: message.tool_calls ?? [],
|
|
1627
|
+
finishReason: choice.finish_reason ?? "stop"
|
|
1628
|
+
};
|
|
1629
|
+
}
|
|
1630
|
+
/** Get current message count (for context tracking). */
|
|
1631
|
+
get messageCount() {
|
|
1632
|
+
return this.messages.length;
|
|
1633
|
+
}
|
|
1634
|
+
};
|
|
1635
|
+
var PROVIDERS = {
|
|
1636
|
+
openai: {
|
|
1637
|
+
baseUrl: "https://api.openai.com/v1",
|
|
1638
|
+
defaultModel: "gpt-4o",
|
|
1639
|
+
envVar: "OPENAI_API_KEY"
|
|
1640
|
+
},
|
|
1641
|
+
anthropic: {
|
|
1642
|
+
baseUrl: "https://api.anthropic.com/v1",
|
|
1643
|
+
defaultModel: "claude-sonnet-4-20250514",
|
|
1644
|
+
envVar: "ANTHROPIC_API_KEY"
|
|
1645
|
+
},
|
|
1646
|
+
ollama: {
|
|
1647
|
+
baseUrl: "http://localhost:11434/v1",
|
|
1648
|
+
defaultModel: "llama3",
|
|
1649
|
+
envVar: ""
|
|
1650
|
+
}
|
|
1651
|
+
};
|
|
1652
|
+
function resolveProvider(provider, overrides) {
|
|
1653
|
+
const preset = PROVIDERS[provider];
|
|
1654
|
+
if (!preset) {
|
|
1655
|
+
throw new Error(
|
|
1656
|
+
`Unknown provider: "${provider}". Available: ${Object.keys(PROVIDERS).join(", ")}`
|
|
1657
|
+
);
|
|
1658
|
+
}
|
|
1659
|
+
const apiKey = overrides?.apiKey ?? (preset.envVar ? process.env[preset.envVar] : "") ?? "";
|
|
1660
|
+
if (!apiKey && preset.envVar) {
|
|
1661
|
+
throw new Error(
|
|
1662
|
+
`Missing API key. Set ${preset.envVar} or pass --api-key.`
|
|
1663
|
+
);
|
|
1664
|
+
}
|
|
1665
|
+
return {
|
|
1666
|
+
baseUrl: overrides?.baseUrl ?? preset.baseUrl,
|
|
1667
|
+
apiKey,
|
|
1668
|
+
model: overrides?.model ?? preset.defaultModel,
|
|
1669
|
+
systemPrompt: overrides?.systemPrompt,
|
|
1670
|
+
maxTokens: overrides?.maxTokens
|
|
1671
|
+
};
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
// src/loader/world-resolver.ts
|
|
1675
|
+
var import_fs = require("fs");
|
|
1676
|
+
var import_path = require("path");
|
|
1677
|
+
var WORLDS_DIR = ".neuroverse/worlds";
|
|
1678
|
+
var ACTIVE_WORLD_FILE = ".neuroverse/active_world";
|
|
1679
|
+
function listWorlds(cwd = process.cwd()) {
|
|
1680
|
+
const worldsDir = (0, import_path.join)(cwd, WORLDS_DIR);
|
|
1681
|
+
if (!(0, import_fs.existsSync)(worldsDir)) return [];
|
|
1682
|
+
const activeName = getActiveWorldName(cwd);
|
|
1683
|
+
const entries = (0, import_fs.readdirSync)(worldsDir);
|
|
1684
|
+
return entries.filter((name) => {
|
|
1685
|
+
const worldJson = (0, import_path.join)(worldsDir, name, "world.json");
|
|
1686
|
+
return (0, import_fs.existsSync)(worldJson);
|
|
1687
|
+
}).map((name) => ({
|
|
1688
|
+
name,
|
|
1689
|
+
path: (0, import_path.join)(worldsDir, name),
|
|
1690
|
+
active: name === activeName
|
|
1691
|
+
})).sort((a, b) => a.name.localeCompare(b.name));
|
|
1692
|
+
}
|
|
1693
|
+
function getActiveWorldName(cwd = process.cwd()) {
|
|
1694
|
+
const filePath = (0, import_path.join)(cwd, ACTIVE_WORLD_FILE);
|
|
1695
|
+
try {
|
|
1696
|
+
return (0, import_fs.readFileSync)(filePath, "utf-8").trim() || void 0;
|
|
1697
|
+
} catch {
|
|
1698
|
+
return void 0;
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
function setActiveWorld(name, cwd = process.cwd()) {
|
|
1702
|
+
const worldsDir = (0, import_path.join)(cwd, WORLDS_DIR);
|
|
1703
|
+
const worldPath = (0, import_path.join)(worldsDir, name, "world.json");
|
|
1704
|
+
if (!(0, import_fs.existsSync)(worldPath)) {
|
|
1705
|
+
const available = listWorlds(cwd);
|
|
1706
|
+
const names = available.map((w) => w.name).join(", ");
|
|
1707
|
+
throw new Error(
|
|
1708
|
+
`World "${name}" not found in ${WORLDS_DIR}/
|
|
1709
|
+
` + (names ? `Available: ${names}` : "No worlds found. Run `neuroverse build` first.")
|
|
1710
|
+
);
|
|
1711
|
+
}
|
|
1712
|
+
const dir = (0, import_path.join)(cwd, ".neuroverse");
|
|
1713
|
+
if (!(0, import_fs.existsSync)(dir)) (0, import_fs.mkdirSync)(dir, { recursive: true });
|
|
1714
|
+
(0, import_fs.writeFileSync)((0, import_path.join)(cwd, ACTIVE_WORLD_FILE), name + "\n", "utf-8");
|
|
1715
|
+
}
|
|
1716
|
+
function resolveWorldPath(explicit, cwd = process.cwd()) {
|
|
1717
|
+
if (explicit) {
|
|
1718
|
+
return resolveNameOrPath(explicit, cwd);
|
|
1719
|
+
}
|
|
1720
|
+
const envWorld = process.env.NEUROVERSE_WORLD;
|
|
1721
|
+
if (envWorld) {
|
|
1722
|
+
return resolveNameOrPath(envWorld, cwd);
|
|
1723
|
+
}
|
|
1724
|
+
const activeName = getActiveWorldName(cwd);
|
|
1725
|
+
if (activeName) {
|
|
1726
|
+
return resolveNameOrPath(activeName, cwd);
|
|
1727
|
+
}
|
|
1728
|
+
const worlds = listWorlds(cwd);
|
|
1729
|
+
if (worlds.length === 1) {
|
|
1730
|
+
return (0, import_path.resolve)(worlds[0].path);
|
|
1731
|
+
}
|
|
1732
|
+
return void 0;
|
|
1733
|
+
}
|
|
1734
|
+
function describeActiveWorld(explicit, cwd = process.cwd()) {
|
|
1735
|
+
if (explicit) {
|
|
1736
|
+
return { name: explicit, source: "--world flag" };
|
|
1737
|
+
}
|
|
1738
|
+
const envWorld = process.env.NEUROVERSE_WORLD;
|
|
1739
|
+
if (envWorld) {
|
|
1740
|
+
return { name: envWorld, source: "NEUROVERSE_WORLD env var" };
|
|
1741
|
+
}
|
|
1742
|
+
const activeName = getActiveWorldName(cwd);
|
|
1743
|
+
if (activeName) {
|
|
1744
|
+
return { name: activeName, source: ".neuroverse/active_world" };
|
|
1745
|
+
}
|
|
1746
|
+
const worlds = listWorlds(cwd);
|
|
1747
|
+
if (worlds.length === 1) {
|
|
1748
|
+
return { name: worlds[0].name, source: "auto-detected (only world)" };
|
|
1749
|
+
}
|
|
1750
|
+
return void 0;
|
|
1751
|
+
}
|
|
1752
|
+
function resolveNameOrPath(ref, cwd) {
|
|
1753
|
+
if (ref.includes("/") || ref.includes("\\") || ref.startsWith(".") || (0, import_path.isAbsolute)(ref)) {
|
|
1754
|
+
return (0, import_path.resolve)(cwd, ref);
|
|
1755
|
+
}
|
|
1756
|
+
const namedPath = (0, import_path.join)(cwd, WORLDS_DIR, ref);
|
|
1757
|
+
if ((0, import_fs.existsSync)((0, import_path.join)(namedPath, "world.json"))) {
|
|
1758
|
+
return (0, import_path.resolve)(namedPath);
|
|
1759
|
+
}
|
|
1760
|
+
return (0, import_path.resolve)(cwd, ref);
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1763
|
+
// src/runtime/mcp-server.ts
|
|
1764
|
+
var import_child_process = require("child_process");
|
|
1765
|
+
var import_fs2 = require("fs");
|
|
1766
|
+
var import_path2 = require("path");
|
|
1767
|
+
var GOVERNED_TOOLS = [
|
|
1768
|
+
{
|
|
1769
|
+
name: "governed_shell",
|
|
1770
|
+
description: "Execute a shell command. This command is evaluated against governance rules before execution.",
|
|
1771
|
+
inputSchema: {
|
|
1772
|
+
type: "object",
|
|
1773
|
+
properties: {
|
|
1774
|
+
command: { type: "string", description: "The shell command to execute" }
|
|
1775
|
+
},
|
|
1776
|
+
required: ["command"]
|
|
1777
|
+
}
|
|
1778
|
+
},
|
|
1779
|
+
{
|
|
1780
|
+
name: "governed_read_file",
|
|
1781
|
+
description: "Read a file. This action is evaluated against governance rules before execution.",
|
|
1782
|
+
inputSchema: {
|
|
1783
|
+
type: "object",
|
|
1784
|
+
properties: {
|
|
1785
|
+
path: { type: "string", description: "File path to read" }
|
|
1786
|
+
},
|
|
1787
|
+
required: ["path"]
|
|
1788
|
+
}
|
|
1789
|
+
},
|
|
1790
|
+
{
|
|
1791
|
+
name: "governed_write_file",
|
|
1792
|
+
description: "Write content to a file. This action is evaluated against governance rules before execution.",
|
|
1793
|
+
inputSchema: {
|
|
1794
|
+
type: "object",
|
|
1795
|
+
properties: {
|
|
1796
|
+
path: { type: "string", description: "File path to write" },
|
|
1797
|
+
content: { type: "string", description: "Content to write" }
|
|
1798
|
+
},
|
|
1799
|
+
required: ["path", "content"]
|
|
1800
|
+
}
|
|
1801
|
+
},
|
|
1802
|
+
{
|
|
1803
|
+
name: "governed_list_directory",
|
|
1804
|
+
description: "List files in a directory. This action is evaluated against governance rules.",
|
|
1805
|
+
inputSchema: {
|
|
1806
|
+
type: "object",
|
|
1807
|
+
properties: {
|
|
1808
|
+
path: { type: "string", description: "Directory path to list" }
|
|
1809
|
+
},
|
|
1810
|
+
required: ["path"]
|
|
1811
|
+
}
|
|
1812
|
+
},
|
|
1813
|
+
{
|
|
1814
|
+
name: "governed_http_request",
|
|
1815
|
+
description: "Make an HTTP request. This action is evaluated against governance rules before execution.",
|
|
1816
|
+
inputSchema: {
|
|
1817
|
+
type: "object",
|
|
1818
|
+
properties: {
|
|
1819
|
+
url: { type: "string", description: "URL to request" },
|
|
1820
|
+
method: { type: "string", description: "HTTP method (GET, POST, PUT, DELETE)", default: "GET" },
|
|
1821
|
+
body: { type: "string", description: "Request body (for POST/PUT)" },
|
|
1822
|
+
headers: { type: "object", description: "Request headers" }
|
|
1823
|
+
},
|
|
1824
|
+
required: ["url"]
|
|
1825
|
+
}
|
|
1826
|
+
},
|
|
1827
|
+
// Governance introspection tools — always available
|
|
1828
|
+
{
|
|
1829
|
+
name: "governance_check",
|
|
1830
|
+
description: "Check if an action would be allowed by governance rules without executing it.",
|
|
1831
|
+
inputSchema: {
|
|
1832
|
+
type: "object",
|
|
1833
|
+
properties: {
|
|
1834
|
+
intent: { type: "string", description: "What the action intends to do" },
|
|
1835
|
+
tool: { type: "string", description: "Tool name (shell, http, file, etc.)" },
|
|
1836
|
+
scope: { type: "string", description: "Scope (file path, URL, etc.)" }
|
|
1837
|
+
},
|
|
1838
|
+
required: ["intent"]
|
|
1839
|
+
}
|
|
1840
|
+
},
|
|
1841
|
+
{
|
|
1842
|
+
name: "governance_plan_status",
|
|
1843
|
+
description: "Show current plan progress and remaining steps.",
|
|
1844
|
+
inputSchema: {
|
|
1845
|
+
type: "object",
|
|
1846
|
+
properties: {}
|
|
1847
|
+
}
|
|
1848
|
+
},
|
|
1849
|
+
{
|
|
1850
|
+
name: "governance_plan_advance",
|
|
1851
|
+
description: "Mark a plan step as completed.",
|
|
1852
|
+
inputSchema: {
|
|
1853
|
+
type: "object",
|
|
1854
|
+
properties: {
|
|
1855
|
+
step_id: { type: "string", description: "ID of the step to mark as completed" }
|
|
1856
|
+
},
|
|
1857
|
+
required: ["step_id"]
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1860
|
+
];
|
|
1861
|
+
function executeShell(command, workingDir) {
|
|
1862
|
+
try {
|
|
1863
|
+
const result = (0, import_child_process.execSync)(command, {
|
|
1864
|
+
cwd: workingDir,
|
|
1865
|
+
encoding: "utf-8",
|
|
1866
|
+
timeout: 3e4,
|
|
1867
|
+
maxBuffer: 1024 * 1024
|
|
1868
|
+
});
|
|
1869
|
+
return result;
|
|
1870
|
+
} catch (err) {
|
|
1871
|
+
return `Error: ${err.message}
|
|
1872
|
+
${err.stderr ?? ""}`;
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
function executeReadFile(path, workingDir) {
|
|
1876
|
+
const fullPath = (0, import_path2.resolve)(workingDir ?? ".", path);
|
|
1877
|
+
return (0, import_fs2.readFileSync)(fullPath, "utf-8");
|
|
1878
|
+
}
|
|
1879
|
+
function executeWriteFile(path, content, workingDir) {
|
|
1880
|
+
const fullPath = (0, import_path2.resolve)(workingDir ?? ".", path);
|
|
1881
|
+
(0, import_fs2.writeFileSync)(fullPath, content);
|
|
1882
|
+
return `File written: ${fullPath}`;
|
|
1883
|
+
}
|
|
1884
|
+
function executeListDir(path, workingDir) {
|
|
1885
|
+
const fullPath = (0, import_path2.resolve)(workingDir ?? ".", path);
|
|
1886
|
+
const entries = (0, import_fs2.readdirSync)(fullPath);
|
|
1887
|
+
return entries.map((e) => {
|
|
1888
|
+
try {
|
|
1889
|
+
const stat = (0, import_fs2.statSync)((0, import_path2.join)(fullPath, e));
|
|
1890
|
+
return `${stat.isDirectory() ? "d" : "-"} ${e}`;
|
|
1891
|
+
} catch {
|
|
1892
|
+
return `? ${e}`;
|
|
1893
|
+
}
|
|
1894
|
+
}).join("\n");
|
|
1895
|
+
}
|
|
1896
|
+
async function executeHttpRequest(url, method, body, headers) {
|
|
1897
|
+
const response = await fetch(url, {
|
|
1898
|
+
method: method || "GET",
|
|
1899
|
+
body: body || void 0,
|
|
1900
|
+
headers: { "Content-Type": "application/json", ...headers }
|
|
1901
|
+
});
|
|
1902
|
+
const text = await response.text();
|
|
1903
|
+
return `HTTP ${response.status}
|
|
1904
|
+
${text}`;
|
|
1905
|
+
}
|
|
1906
|
+
var McpGovernanceServer = class {
|
|
1907
|
+
world;
|
|
1908
|
+
plan;
|
|
1909
|
+
config;
|
|
1910
|
+
engineOptions;
|
|
1911
|
+
initialized = false;
|
|
1912
|
+
// Stats
|
|
1913
|
+
actionsEvaluated = 0;
|
|
1914
|
+
actionsAllowed = 0;
|
|
1915
|
+
actionsBlocked = 0;
|
|
1916
|
+
constructor(config) {
|
|
1917
|
+
this.config = config;
|
|
1918
|
+
this.plan = config.plan;
|
|
1919
|
+
this.engineOptions = {
|
|
1920
|
+
trace: config.trace ?? false,
|
|
1921
|
+
level: config.level,
|
|
1922
|
+
plan: this.plan
|
|
1923
|
+
};
|
|
1924
|
+
}
|
|
1925
|
+
/**
|
|
1926
|
+
* Start the MCP server — reads JSON-RPC from stdin, writes to stdout.
|
|
1927
|
+
*/
|
|
1928
|
+
async start() {
|
|
1929
|
+
if (this.config.worldPath) {
|
|
1930
|
+
this.world = await loadWorld(this.config.worldPath);
|
|
1931
|
+
} else if (this.config.world) {
|
|
1932
|
+
this.world = this.config.world;
|
|
1933
|
+
} else {
|
|
1934
|
+
throw new Error("No world provided");
|
|
1935
|
+
}
|
|
1936
|
+
if (this.config.planPath && !this.plan) {
|
|
1937
|
+
this.plan = JSON.parse((0, import_fs2.readFileSync)(this.config.planPath, "utf-8"));
|
|
1938
|
+
this.engineOptions.plan = this.plan;
|
|
1939
|
+
}
|
|
1940
|
+
process.stderr.write(`[neuroverse-mcp] Server starting
|
|
1941
|
+
`);
|
|
1942
|
+
process.stderr.write(`[neuroverse-mcp] World: ${this.world.world.name}
|
|
1943
|
+
`);
|
|
1944
|
+
if (this.plan) {
|
|
1945
|
+
process.stderr.write(`[neuroverse-mcp] Plan: ${this.plan.plan_id}
|
|
1946
|
+
`);
|
|
1947
|
+
}
|
|
1948
|
+
let buffer = "";
|
|
1949
|
+
process.stdin.setEncoding("utf-8");
|
|
1950
|
+
process.stdin.on("data", (chunk) => {
|
|
1951
|
+
buffer += chunk;
|
|
1952
|
+
while (buffer.length > 0) {
|
|
1953
|
+
const headerEnd = buffer.indexOf("\r\n\r\n");
|
|
1954
|
+
if (headerEnd === -1) break;
|
|
1955
|
+
const header = buffer.slice(0, headerEnd);
|
|
1956
|
+
const contentLengthMatch = header.match(/Content-Length:\s*(\d+)/i);
|
|
1957
|
+
if (!contentLengthMatch) {
|
|
1958
|
+
const newlineIdx = buffer.indexOf("\n");
|
|
1959
|
+
if (newlineIdx === -1) break;
|
|
1960
|
+
const line = buffer.slice(0, newlineIdx).trim();
|
|
1961
|
+
buffer = buffer.slice(newlineIdx + 1);
|
|
1962
|
+
if (line) this.handleRawLine(line);
|
|
1963
|
+
continue;
|
|
1964
|
+
}
|
|
1965
|
+
const contentLength = parseInt(contentLengthMatch[1], 10);
|
|
1966
|
+
const bodyStart = headerEnd + 4;
|
|
1967
|
+
const bodyEnd = bodyStart + contentLength;
|
|
1968
|
+
if (buffer.length < bodyEnd) break;
|
|
1969
|
+
const body = buffer.slice(bodyStart, bodyEnd);
|
|
1970
|
+
buffer = buffer.slice(bodyEnd);
|
|
1971
|
+
this.handleRawLine(body);
|
|
1972
|
+
}
|
|
1973
|
+
});
|
|
1974
|
+
process.stdin.on("end", () => {
|
|
1975
|
+
process.stderr.write(
|
|
1976
|
+
`[neuroverse-mcp] Server stopped. Evaluated: ${this.actionsEvaluated}, Allowed: ${this.actionsAllowed}, Blocked: ${this.actionsBlocked}
|
|
1977
|
+
`
|
|
1978
|
+
);
|
|
1979
|
+
});
|
|
1980
|
+
await new Promise(() => {
|
|
1981
|
+
});
|
|
1982
|
+
}
|
|
1983
|
+
handleRawLine(line) {
|
|
1984
|
+
try {
|
|
1985
|
+
const msg = JSON.parse(line);
|
|
1986
|
+
if (msg.method) {
|
|
1987
|
+
if (msg.id !== void 0) {
|
|
1988
|
+
this.handleRequest(msg);
|
|
1989
|
+
} else {
|
|
1990
|
+
this.handleNotification(msg);
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1993
|
+
} catch (err) {
|
|
1994
|
+
process.stderr.write(`[neuroverse-mcp] Parse error: ${err}
|
|
1995
|
+
`);
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
send(msg) {
|
|
1999
|
+
const json = JSON.stringify(msg);
|
|
2000
|
+
const header = `Content-Length: ${Buffer.byteLength(json)}\r
|
|
2001
|
+
\r
|
|
2002
|
+
`;
|
|
2003
|
+
process.stdout.write(header + json);
|
|
2004
|
+
}
|
|
2005
|
+
sendResult(id, result) {
|
|
2006
|
+
this.send({ jsonrpc: "2.0", id, result });
|
|
2007
|
+
}
|
|
2008
|
+
sendError(id, code, message) {
|
|
2009
|
+
this.send({ jsonrpc: "2.0", id, error: { code, message } });
|
|
2010
|
+
}
|
|
2011
|
+
// ─── Request Handlers ───────────────────────────────────────────────────
|
|
2012
|
+
handleRequest(request) {
|
|
2013
|
+
switch (request.method) {
|
|
2014
|
+
case "initialize":
|
|
2015
|
+
this.handleInitialize(request);
|
|
2016
|
+
break;
|
|
2017
|
+
case "tools/list":
|
|
2018
|
+
this.handleToolsList(request);
|
|
2019
|
+
break;
|
|
2020
|
+
case "tools/call":
|
|
2021
|
+
this.handleToolsCall(request);
|
|
2022
|
+
break;
|
|
2023
|
+
case "ping":
|
|
2024
|
+
this.sendResult(request.id, {});
|
|
2025
|
+
break;
|
|
2026
|
+
default:
|
|
2027
|
+
this.sendError(request.id, -32601, `Method not found: ${request.method}`);
|
|
2028
|
+
}
|
|
2029
|
+
}
|
|
2030
|
+
handleNotification(notification) {
|
|
2031
|
+
switch (notification.method) {
|
|
2032
|
+
case "notifications/initialized":
|
|
2033
|
+
this.initialized = true;
|
|
2034
|
+
process.stderr.write(`[neuroverse-mcp] Client initialized
|
|
2035
|
+
`);
|
|
2036
|
+
break;
|
|
2037
|
+
case "notifications/cancelled":
|
|
2038
|
+
break;
|
|
2039
|
+
default:
|
|
2040
|
+
process.stderr.write(`[neuroverse-mcp] Unknown notification: ${notification.method}
|
|
2041
|
+
`);
|
|
2042
|
+
}
|
|
2043
|
+
}
|
|
2044
|
+
handleInitialize(request) {
|
|
2045
|
+
this.sendResult(request.id, {
|
|
2046
|
+
protocolVersion: "2024-11-05",
|
|
2047
|
+
capabilities: {
|
|
2048
|
+
tools: {}
|
|
2049
|
+
},
|
|
2050
|
+
serverInfo: {
|
|
2051
|
+
name: "neuroverse-governance",
|
|
2052
|
+
version: "0.2.0"
|
|
2053
|
+
}
|
|
2054
|
+
});
|
|
2055
|
+
}
|
|
2056
|
+
handleToolsList(request) {
|
|
2057
|
+
const tools = [];
|
|
2058
|
+
if (this.config.enableShell !== false) {
|
|
2059
|
+
tools.push(GOVERNED_TOOLS.find((t) => t.name === "governed_shell"));
|
|
2060
|
+
}
|
|
2061
|
+
if (this.config.enableFiles !== false) {
|
|
2062
|
+
tools.push(GOVERNED_TOOLS.find((t) => t.name === "governed_read_file"));
|
|
2063
|
+
tools.push(GOVERNED_TOOLS.find((t) => t.name === "governed_write_file"));
|
|
2064
|
+
tools.push(GOVERNED_TOOLS.find((t) => t.name === "governed_list_directory"));
|
|
2065
|
+
}
|
|
2066
|
+
if (this.config.enableHttp !== false) {
|
|
2067
|
+
tools.push(GOVERNED_TOOLS.find((t) => t.name === "governed_http_request"));
|
|
2068
|
+
}
|
|
2069
|
+
tools.push(GOVERNED_TOOLS.find((t) => t.name === "governance_check"));
|
|
2070
|
+
tools.push(GOVERNED_TOOLS.find((t) => t.name === "governance_plan_status"));
|
|
2071
|
+
if (this.plan) {
|
|
2072
|
+
tools.push(GOVERNED_TOOLS.find((t) => t.name === "governance_plan_advance"));
|
|
2073
|
+
}
|
|
2074
|
+
this.sendResult(request.id, { tools });
|
|
2075
|
+
}
|
|
2076
|
+
async handleToolsCall(request) {
|
|
2077
|
+
const params = request.params;
|
|
2078
|
+
const toolName = params.name;
|
|
2079
|
+
const args = params.arguments ?? {};
|
|
2080
|
+
try {
|
|
2081
|
+
const result = await this.executeTool(toolName, args);
|
|
2082
|
+
this.sendResult(request.id, result);
|
|
2083
|
+
} catch (err) {
|
|
2084
|
+
this.sendResult(request.id, {
|
|
2085
|
+
content: [{ type: "text", text: `Error: ${err.message}` }],
|
|
2086
|
+
isError: true
|
|
2087
|
+
});
|
|
2088
|
+
}
|
|
2089
|
+
}
|
|
2090
|
+
// ─── Tool Execution with Governance ─────────────────────────────────────
|
|
2091
|
+
async executeTool(name, args) {
|
|
2092
|
+
if (name === "governance_check") {
|
|
2093
|
+
return this.toolGovernanceCheck(args);
|
|
2094
|
+
}
|
|
2095
|
+
if (name === "governance_plan_status") {
|
|
2096
|
+
return this.toolPlanStatus();
|
|
2097
|
+
}
|
|
2098
|
+
if (name === "governance_plan_advance") {
|
|
2099
|
+
return this.toolPlanAdvance(args);
|
|
2100
|
+
}
|
|
2101
|
+
const event = this.buildEvent(name, args);
|
|
2102
|
+
this.engineOptions.plan = this.plan;
|
|
2103
|
+
const verdict = evaluateGuard(event, this.world, this.engineOptions);
|
|
2104
|
+
this.actionsEvaluated++;
|
|
2105
|
+
if (verdict.status === "BLOCK") {
|
|
2106
|
+
this.actionsBlocked++;
|
|
2107
|
+
let reason = `[GOVERNANCE BLOCKED] ${verdict.reason ?? "Action blocked by governance rules."}`;
|
|
2108
|
+
if (verdict.ruleId) reason += ` (Rule: ${verdict.ruleId})`;
|
|
2109
|
+
if (verdict.trace?.planCheck && !verdict.trace.planCheck.matched) {
|
|
2110
|
+
const pc = verdict.trace.planCheck;
|
|
2111
|
+
if (pc.closestStepLabel) {
|
|
2112
|
+
reason += `
|
|
2113
|
+
Closest plan step: "${pc.closestStepLabel}"`;
|
|
2114
|
+
}
|
|
2115
|
+
}
|
|
2116
|
+
process.stderr.write(`[neuroverse-mcp] BLOCKED: ${event.intent}
|
|
2117
|
+
`);
|
|
2118
|
+
return { content: [{ type: "text", text: reason }], isError: true };
|
|
2119
|
+
}
|
|
2120
|
+
if (verdict.status === "PAUSE") {
|
|
2121
|
+
this.actionsBlocked++;
|
|
2122
|
+
const reason = `[GOVERNANCE PAUSED] ${verdict.reason ?? "Action requires human approval."}`;
|
|
2123
|
+
process.stderr.write(`[neuroverse-mcp] PAUSED: ${event.intent}
|
|
2124
|
+
`);
|
|
2125
|
+
return { content: [{ type: "text", text: reason }], isError: true };
|
|
2126
|
+
}
|
|
2127
|
+
this.actionsAllowed++;
|
|
2128
|
+
process.stderr.write(`[neuroverse-mcp] ALLOWED: ${event.intent}
|
|
2129
|
+
`);
|
|
2130
|
+
const result = await this.executeActualTool(name, args);
|
|
2131
|
+
if (this.plan) {
|
|
2132
|
+
const planVerdict = evaluatePlan(event, this.plan);
|
|
2133
|
+
if (planVerdict.matchedStep) {
|
|
2134
|
+
this.plan = advancePlan(this.plan, planVerdict.matchedStep);
|
|
2135
|
+
this.engineOptions.plan = this.plan;
|
|
2136
|
+
const progress = getPlanProgress(this.plan);
|
|
2137
|
+
process.stderr.write(
|
|
2138
|
+
`[neuroverse-mcp] Plan: ${progress.completed}/${progress.total} (${progress.percentage}%)
|
|
2139
|
+
`
|
|
2140
|
+
);
|
|
2141
|
+
if (progress.completed === progress.total) {
|
|
2142
|
+
process.stderr.write(`[neuroverse-mcp] Plan complete!
|
|
2143
|
+
`);
|
|
2144
|
+
}
|
|
2145
|
+
}
|
|
2146
|
+
}
|
|
2147
|
+
return result;
|
|
2148
|
+
}
|
|
2149
|
+
buildEvent(toolName, args) {
|
|
2150
|
+
switch (toolName) {
|
|
2151
|
+
case "governed_shell":
|
|
2152
|
+
return {
|
|
2153
|
+
intent: `execute shell command: ${args.command}`,
|
|
2154
|
+
tool: "shell",
|
|
2155
|
+
scope: String(args.command ?? ""),
|
|
2156
|
+
actionCategory: "shell",
|
|
2157
|
+
args,
|
|
2158
|
+
direction: "input"
|
|
2159
|
+
};
|
|
2160
|
+
case "governed_read_file":
|
|
2161
|
+
return {
|
|
2162
|
+
intent: `read file: ${args.path}`,
|
|
2163
|
+
tool: "fs",
|
|
2164
|
+
scope: String(args.path ?? ""),
|
|
2165
|
+
actionCategory: "read",
|
|
2166
|
+
args,
|
|
2167
|
+
direction: "input"
|
|
2168
|
+
};
|
|
2169
|
+
case "governed_write_file":
|
|
2170
|
+
return {
|
|
2171
|
+
intent: `write file: ${args.path}`,
|
|
2172
|
+
tool: "fs",
|
|
2173
|
+
scope: String(args.path ?? ""),
|
|
2174
|
+
actionCategory: "write",
|
|
2175
|
+
args,
|
|
2176
|
+
direction: "input"
|
|
2177
|
+
};
|
|
2178
|
+
case "governed_list_directory":
|
|
2179
|
+
return {
|
|
2180
|
+
intent: `list directory: ${args.path}`,
|
|
2181
|
+
tool: "fs",
|
|
2182
|
+
scope: String(args.path ?? ""),
|
|
2183
|
+
actionCategory: "read",
|
|
2184
|
+
args,
|
|
2185
|
+
direction: "input"
|
|
2186
|
+
};
|
|
2187
|
+
case "governed_http_request":
|
|
2188
|
+
return {
|
|
2189
|
+
intent: `http ${args.method ?? "GET"} ${args.url}`,
|
|
2190
|
+
tool: "http",
|
|
2191
|
+
scope: String(args.url ?? ""),
|
|
2192
|
+
actionCategory: "network",
|
|
2193
|
+
args,
|
|
2194
|
+
direction: "input"
|
|
2195
|
+
};
|
|
2196
|
+
default:
|
|
2197
|
+
return {
|
|
2198
|
+
intent: `${toolName}: ${JSON.stringify(args)}`,
|
|
2199
|
+
tool: toolName,
|
|
2200
|
+
args,
|
|
2201
|
+
direction: "input"
|
|
2202
|
+
};
|
|
2203
|
+
}
|
|
2204
|
+
}
|
|
2205
|
+
async executeActualTool(name, args) {
|
|
2206
|
+
const workingDir = this.config.workingDir;
|
|
2207
|
+
switch (name) {
|
|
2208
|
+
case "governed_shell": {
|
|
2209
|
+
const output = executeShell(String(args.command), workingDir);
|
|
2210
|
+
return { content: [{ type: "text", text: output }] };
|
|
2211
|
+
}
|
|
2212
|
+
case "governed_read_file": {
|
|
2213
|
+
const content = executeReadFile(String(args.path), workingDir);
|
|
2214
|
+
return { content: [{ type: "text", text: content }] };
|
|
2215
|
+
}
|
|
2216
|
+
case "governed_write_file": {
|
|
2217
|
+
const result = executeWriteFile(String(args.path), String(args.content), workingDir);
|
|
2218
|
+
return { content: [{ type: "text", text: result }] };
|
|
2219
|
+
}
|
|
2220
|
+
case "governed_list_directory": {
|
|
2221
|
+
const listing = executeListDir(String(args.path), workingDir);
|
|
2222
|
+
return { content: [{ type: "text", text: listing }] };
|
|
2223
|
+
}
|
|
2224
|
+
case "governed_http_request": {
|
|
2225
|
+
const result = await executeHttpRequest(
|
|
2226
|
+
String(args.url),
|
|
2227
|
+
String(args.method ?? "GET"),
|
|
2228
|
+
args.body,
|
|
2229
|
+
args.headers
|
|
2230
|
+
);
|
|
2231
|
+
return { content: [{ type: "text", text: result }] };
|
|
2232
|
+
}
|
|
2233
|
+
default:
|
|
2234
|
+
return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true };
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
2237
|
+
// ─── Governance Introspection Tools ─────────────────────────────────────
|
|
2238
|
+
toolGovernanceCheck(args) {
|
|
2239
|
+
const event = {
|
|
2240
|
+
intent: String(args.intent ?? ""),
|
|
2241
|
+
tool: args.tool,
|
|
2242
|
+
scope: args.scope,
|
|
2243
|
+
direction: "input"
|
|
2244
|
+
};
|
|
2245
|
+
this.engineOptions.plan = this.plan;
|
|
2246
|
+
const verdict = evaluateGuard(event, this.world, this.engineOptions);
|
|
2247
|
+
const lines = [
|
|
2248
|
+
`Verdict: ${verdict.status}`,
|
|
2249
|
+
verdict.reason ? `Reason: ${verdict.reason}` : null,
|
|
2250
|
+
verdict.ruleId ? `Rule: ${verdict.ruleId}` : null,
|
|
2251
|
+
verdict.warning ? `Warning: ${verdict.warning}` : null
|
|
2252
|
+
].filter(Boolean).join("\n");
|
|
2253
|
+
return { content: [{ type: "text", text: lines }] };
|
|
2254
|
+
}
|
|
2255
|
+
toolPlanStatus() {
|
|
2256
|
+
if (!this.plan) {
|
|
2257
|
+
return { content: [{ type: "text", text: "No active plan." }] };
|
|
2258
|
+
}
|
|
2259
|
+
const progress = getPlanProgress(this.plan);
|
|
2260
|
+
const lines = [
|
|
2261
|
+
`Plan: ${this.plan.plan_id}`,
|
|
2262
|
+
`Objective: ${this.plan.objective}`,
|
|
2263
|
+
`Progress: ${progress.completed}/${progress.total} (${progress.percentage}%)`,
|
|
2264
|
+
"",
|
|
2265
|
+
"Steps:",
|
|
2266
|
+
...this.plan.steps.map((s) => {
|
|
2267
|
+
const icon = s.status === "completed" ? "[x]" : s.status === "active" ? "[>]" : "[ ]";
|
|
2268
|
+
return ` ${icon} ${s.label} (${s.id})`;
|
|
2269
|
+
})
|
|
2270
|
+
];
|
|
2271
|
+
if (this.plan.constraints.length > 0) {
|
|
2272
|
+
lines.push("", "Constraints:");
|
|
2273
|
+
for (const c2 of this.plan.constraints) {
|
|
2274
|
+
lines.push(` - ${c2.description} [${c2.type}]`);
|
|
2275
|
+
}
|
|
2276
|
+
}
|
|
2277
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
2278
|
+
}
|
|
2279
|
+
toolPlanAdvance(args) {
|
|
2280
|
+
if (!this.plan) {
|
|
2281
|
+
return { content: [{ type: "text", text: "No active plan." }], isError: true };
|
|
2282
|
+
}
|
|
2283
|
+
const stepId = String(args.step_id ?? "");
|
|
2284
|
+
const step = this.plan.steps.find((s) => s.id === stepId);
|
|
2285
|
+
if (!step) {
|
|
2286
|
+
const ids = this.plan.steps.map((s) => s.id).join(", ");
|
|
2287
|
+
return {
|
|
2288
|
+
content: [{ type: "text", text: `Step "${stepId}" not found. Available: ${ids}` }],
|
|
2289
|
+
isError: true
|
|
2290
|
+
};
|
|
2291
|
+
}
|
|
2292
|
+
if (step.status === "completed") {
|
|
2293
|
+
return { content: [{ type: "text", text: `Step "${stepId}" is already completed.` }] };
|
|
2294
|
+
}
|
|
2295
|
+
this.plan = advancePlan(this.plan, stepId);
|
|
2296
|
+
this.engineOptions.plan = this.plan;
|
|
2297
|
+
const progress = getPlanProgress(this.plan);
|
|
2298
|
+
let text = `Step completed: ${step.label}
|
|
2299
|
+
Progress: ${progress.completed}/${progress.total} (${progress.percentage}%)`;
|
|
2300
|
+
if (progress.completed === progress.total) {
|
|
2301
|
+
text += "\n\nPlan complete!";
|
|
2302
|
+
}
|
|
2303
|
+
if (this.config.planPath) {
|
|
2304
|
+
(0, import_fs2.writeFileSync)(this.config.planPath, JSON.stringify(this.plan, null, 2) + "\n");
|
|
2305
|
+
}
|
|
2306
|
+
return { content: [{ type: "text", text }] };
|
|
2307
|
+
}
|
|
2308
|
+
};
|
|
2309
|
+
|
|
747
2310
|
// src/engine/audit-logger.ts
|
|
748
2311
|
var FileAuditLogger = class {
|
|
749
2312
|
logPath;
|
|
@@ -1223,81 +2786,6 @@ function emptyReport() {
|
|
|
1223
2786
|
};
|
|
1224
2787
|
}
|
|
1225
2788
|
|
|
1226
|
-
// src/loader/world-loader.ts
|
|
1227
|
-
async function loadWorldFromDirectory(dirPath) {
|
|
1228
|
-
const { readFile: readFile3 } = await import("fs/promises");
|
|
1229
|
-
const { join: join3 } = await import("path");
|
|
1230
|
-
const { readdirSync } = await import("fs");
|
|
1231
|
-
async function readJson(filename) {
|
|
1232
|
-
try {
|
|
1233
|
-
const content = await readFile3(join3(dirPath, filename), "utf-8");
|
|
1234
|
-
return JSON.parse(content);
|
|
1235
|
-
} catch {
|
|
1236
|
-
return void 0;
|
|
1237
|
-
}
|
|
1238
|
-
}
|
|
1239
|
-
const worldJson = await readJson("world.json");
|
|
1240
|
-
if (!worldJson) {
|
|
1241
|
-
throw new Error(`Cannot read world.json in ${dirPath}`);
|
|
1242
|
-
}
|
|
1243
|
-
const invariantsJson = await readJson("invariants.json");
|
|
1244
|
-
const assumptionsJson = await readJson("assumptions.json");
|
|
1245
|
-
const stateSchemaJson = await readJson("state-schema.json");
|
|
1246
|
-
const gatesJson = await readJson("gates.json");
|
|
1247
|
-
const outcomesJson = await readJson("outcomes.json");
|
|
1248
|
-
const guardsJson = await readJson("guards.json");
|
|
1249
|
-
const rolesJson = await readJson("roles.json");
|
|
1250
|
-
const kernelJson = await readJson("kernel.json");
|
|
1251
|
-
const metadataJson = await readJson("metadata.json");
|
|
1252
|
-
const rules = [];
|
|
1253
|
-
try {
|
|
1254
|
-
const rulesDir = join3(dirPath, "rules");
|
|
1255
|
-
const ruleFiles = readdirSync(rulesDir).filter((f) => f.endsWith(".json")).sort();
|
|
1256
|
-
for (const file of ruleFiles) {
|
|
1257
|
-
const content = await readFile3(join3(rulesDir, file), "utf-8");
|
|
1258
|
-
rules.push(JSON.parse(content));
|
|
1259
|
-
}
|
|
1260
|
-
} catch {
|
|
1261
|
-
}
|
|
1262
|
-
return {
|
|
1263
|
-
world: worldJson,
|
|
1264
|
-
invariants: invariantsJson?.invariants ?? [],
|
|
1265
|
-
assumptions: assumptionsJson ?? { profiles: {}, parameter_definitions: {} },
|
|
1266
|
-
stateSchema: stateSchemaJson ?? { variables: {}, presets: {} },
|
|
1267
|
-
rules,
|
|
1268
|
-
gates: gatesJson ?? {
|
|
1269
|
-
viability_classification: [],
|
|
1270
|
-
structural_override: { description: "", enforcement: "mandatory" },
|
|
1271
|
-
sustainability_threshold: 0,
|
|
1272
|
-
collapse_visual: { background: "", text: "", border: "", label: "" }
|
|
1273
|
-
},
|
|
1274
|
-
outcomes: outcomesJson ?? {
|
|
1275
|
-
computed_outcomes: [],
|
|
1276
|
-
comparison_layout: { primary_card: "", status_badge: "", structural_indicators: [] }
|
|
1277
|
-
},
|
|
1278
|
-
guards: guardsJson,
|
|
1279
|
-
roles: rolesJson,
|
|
1280
|
-
kernel: kernelJson,
|
|
1281
|
-
metadata: metadataJson ?? {
|
|
1282
|
-
format_version: "1.0.0",
|
|
1283
|
-
created_at: "",
|
|
1284
|
-
last_modified: "",
|
|
1285
|
-
authoring_method: "manual-authoring"
|
|
1286
|
-
}
|
|
1287
|
-
};
|
|
1288
|
-
}
|
|
1289
|
-
async function loadWorld(worldPath) {
|
|
1290
|
-
const { stat } = await import("fs/promises");
|
|
1291
|
-
const info = await stat(worldPath);
|
|
1292
|
-
if (info.isDirectory()) {
|
|
1293
|
-
return loadWorldFromDirectory(worldPath);
|
|
1294
|
-
}
|
|
1295
|
-
if (worldPath.endsWith(".nv-world.zip")) {
|
|
1296
|
-
throw new Error(".nv-world.zip loading not yet implemented \u2014 use a world directory");
|
|
1297
|
-
}
|
|
1298
|
-
throw new Error(`Cannot load world from: ${worldPath} \u2014 expected a directory or .nv-world.zip`);
|
|
1299
|
-
}
|
|
1300
|
-
|
|
1301
2789
|
// src/engine/condition-engine.ts
|
|
1302
2790
|
function getFieldValue(event, field) {
|
|
1303
2791
|
if (field.startsWith("args.")) {
|
|
@@ -1444,15 +2932,39 @@ function evaluateEndsWith(fieldValue, conditionValue) {
|
|
|
1444
2932
|
}
|
|
1445
2933
|
|
|
1446
2934
|
// src/engine/validate-engine.ts
|
|
1447
|
-
function validateWorld(world) {
|
|
2935
|
+
function validateWorld(world, mode = "standard") {
|
|
1448
2936
|
const startTime = performance.now();
|
|
1449
2937
|
const findings = [];
|
|
1450
2938
|
checkCompleteness(world, findings);
|
|
1451
2939
|
checkReferentialIntegrity(world, findings);
|
|
1452
2940
|
checkGuardCoverage(world, findings);
|
|
2941
|
+
checkSemanticCoverage(world, findings);
|
|
1453
2942
|
checkContradictions(world, findings);
|
|
2943
|
+
checkGuardShadows(world, findings);
|
|
2944
|
+
checkFailClosedSurfaces(world, findings);
|
|
2945
|
+
checkReachability(world, findings);
|
|
2946
|
+
checkStateCoverage(world, findings);
|
|
1454
2947
|
checkOrphans(world, findings);
|
|
1455
2948
|
checkSchemaViolations(world, findings);
|
|
2949
|
+
const governanceCategories = /* @__PURE__ */ new Set([
|
|
2950
|
+
"guard-coverage",
|
|
2951
|
+
"contradiction",
|
|
2952
|
+
"semantic-tension",
|
|
2953
|
+
"orphan"
|
|
2954
|
+
]);
|
|
2955
|
+
if (mode === "dev") {
|
|
2956
|
+
for (const f of findings) {
|
|
2957
|
+
if (governanceCategories.has(f.category) && f.severity === "warning") {
|
|
2958
|
+
f.severity = "info";
|
|
2959
|
+
}
|
|
2960
|
+
}
|
|
2961
|
+
} else if (mode === "strict") {
|
|
2962
|
+
for (const f of findings) {
|
|
2963
|
+
if (governanceCategories.has(f.category) && f.severity === "info") {
|
|
2964
|
+
f.severity = "warning";
|
|
2965
|
+
}
|
|
2966
|
+
}
|
|
2967
|
+
}
|
|
1456
2968
|
const severityOrder = { error: 0, warning: 1, info: 2 };
|
|
1457
2969
|
findings.sort((a, b) => severityOrder[a.severity] - severityOrder[b.severity]);
|
|
1458
2970
|
const errors = findings.filter((f) => f.severity === "error").length;
|
|
@@ -1460,6 +2972,7 @@ function validateWorld(world) {
|
|
|
1460
2972
|
const info = findings.filter((f) => f.severity === "info").length;
|
|
1461
2973
|
const completenessScore = computeCompletenessScore(world);
|
|
1462
2974
|
const invariantCoverage = computeInvariantCoverage(world);
|
|
2975
|
+
const governanceHealth = computeGovernanceHealth(world, findings);
|
|
1463
2976
|
const summary = {
|
|
1464
2977
|
errors,
|
|
1465
2978
|
warnings,
|
|
@@ -1467,7 +2980,8 @@ function validateWorld(world) {
|
|
|
1467
2980
|
completenessScore,
|
|
1468
2981
|
invariantCoverage,
|
|
1469
2982
|
canRun: errors === 0,
|
|
1470
|
-
isHealthy: errors === 0 && warnings === 0
|
|
2983
|
+
isHealthy: errors === 0 && warnings === 0,
|
|
2984
|
+
governanceHealth
|
|
1471
2985
|
};
|
|
1472
2986
|
return {
|
|
1473
2987
|
worldId: world.world.world_id,
|
|
@@ -1475,6 +2989,7 @@ function validateWorld(world) {
|
|
|
1475
2989
|
worldVersion: world.world.version,
|
|
1476
2990
|
validatedAt: Date.now(),
|
|
1477
2991
|
durationMs: performance.now() - startTime,
|
|
2992
|
+
validationMode: mode,
|
|
1478
2993
|
summary,
|
|
1479
2994
|
findings
|
|
1480
2995
|
};
|
|
@@ -1657,6 +3172,183 @@ function checkGuardCoverage(world, findings) {
|
|
|
1657
3172
|
}
|
|
1658
3173
|
}
|
|
1659
3174
|
}
|
|
3175
|
+
function checkSemanticCoverage(world, findings) {
|
|
3176
|
+
if (!world.invariants || world.invariants.length === 0) return;
|
|
3177
|
+
const hasGuards = (world.guards?.guards?.length ?? 0) > 0;
|
|
3178
|
+
const hasKernel = (world.kernel?.input_boundaries?.forbidden_patterns?.length ?? 0) > 0 || (world.kernel?.output_boundaries?.forbidden_patterns?.length ?? 0) > 0;
|
|
3179
|
+
if (!hasGuards && !hasKernel) return;
|
|
3180
|
+
const guards = world.guards?.guards ?? [];
|
|
3181
|
+
const vocabEntries = world.guards?.intent_vocabulary ?? {};
|
|
3182
|
+
const kernelInput = world.kernel?.input_boundaries?.forbidden_patterns ?? [];
|
|
3183
|
+
const kernelOutput = world.kernel?.output_boundaries?.forbidden_patterns ?? [];
|
|
3184
|
+
const allKernelRules = [...kernelInput, ...kernelOutput];
|
|
3185
|
+
const guardSearchTexts = guards.map((g) => {
|
|
3186
|
+
const parts = [];
|
|
3187
|
+
for (const patternKey of g.intent_patterns) {
|
|
3188
|
+
parts.push(patternKey.toLowerCase());
|
|
3189
|
+
const vocab = vocabEntries[patternKey];
|
|
3190
|
+
if (vocab) {
|
|
3191
|
+
parts.push(vocab.label.toLowerCase());
|
|
3192
|
+
parts.push(vocab.pattern.toLowerCase());
|
|
3193
|
+
}
|
|
3194
|
+
}
|
|
3195
|
+
parts.push(g.description.toLowerCase());
|
|
3196
|
+
return { guard: g, text: parts.join(" ") };
|
|
3197
|
+
});
|
|
3198
|
+
const kernelSearchTexts = allKernelRules.map((k) => ({
|
|
3199
|
+
rule: k,
|
|
3200
|
+
text: `${k.id} ${k.reason} ${k.pattern ?? ""}`.toLowerCase()
|
|
3201
|
+
}));
|
|
3202
|
+
for (const invariant of world.invariants) {
|
|
3203
|
+
if (invariant.enforcement === "prompt") continue;
|
|
3204
|
+
const tokens = extractActionTokens(invariant.id, invariant.label);
|
|
3205
|
+
if (tokens.length === 0) continue;
|
|
3206
|
+
const coveringGuards = guardSearchTexts.filter((gs) => {
|
|
3207
|
+
const enabled = gs.guard.immutable || gs.guard.default_enabled !== false;
|
|
3208
|
+
if (!enabled) return false;
|
|
3209
|
+
return tokens.some((token) => gs.text.includes(token));
|
|
3210
|
+
});
|
|
3211
|
+
const coveringKernel = kernelSearchTexts.filter(
|
|
3212
|
+
(ks) => tokens.some((token) => ks.text.includes(token))
|
|
3213
|
+
);
|
|
3214
|
+
const hasStructuralGuard = guards.some(
|
|
3215
|
+
(g) => g.invariant_ref === invariant.id && g.immutable
|
|
3216
|
+
);
|
|
3217
|
+
if (coveringGuards.length === 0 && coveringKernel.length === 0) {
|
|
3218
|
+
if (hasStructuralGuard) {
|
|
3219
|
+
findings.push(finding(
|
|
3220
|
+
`weak-coverage-${invariant.id}`,
|
|
3221
|
+
`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`,
|
|
3222
|
+
"warning",
|
|
3223
|
+
"guard-coverage",
|
|
3224
|
+
["invariants.json", "guards.json"],
|
|
3225
|
+
invariant.id,
|
|
3226
|
+
`Ensure the backing guard's intent_patterns include patterns that can detect "${invariant.label}"`
|
|
3227
|
+
));
|
|
3228
|
+
} else {
|
|
3229
|
+
findings.push(finding(
|
|
3230
|
+
`unenforced-invariant-${invariant.id}`,
|
|
3231
|
+
`Invariant "${invariant.id}" has no guard or kernel rule capable of enforcing it \u2014 no interceptor matches action class [${tokens.join(", ")}]`,
|
|
3232
|
+
"warning",
|
|
3233
|
+
"guard-coverage",
|
|
3234
|
+
["invariants.json", "guards.json"],
|
|
3235
|
+
invariant.id,
|
|
3236
|
+
`Add a guard with intent_patterns that can intercept "${invariant.label}", or add a kernel forbidden_pattern`
|
|
3237
|
+
));
|
|
3238
|
+
}
|
|
3239
|
+
}
|
|
3240
|
+
}
|
|
3241
|
+
}
|
|
3242
|
+
function extractActionTokens(id, label) {
|
|
3243
|
+
const stopWords = /* @__PURE__ */ new Set([
|
|
3244
|
+
"a",
|
|
3245
|
+
"an",
|
|
3246
|
+
"the",
|
|
3247
|
+
"is",
|
|
3248
|
+
"are",
|
|
3249
|
+
"was",
|
|
3250
|
+
"were",
|
|
3251
|
+
"be",
|
|
3252
|
+
"been",
|
|
3253
|
+
"being",
|
|
3254
|
+
"have",
|
|
3255
|
+
"has",
|
|
3256
|
+
"had",
|
|
3257
|
+
"do",
|
|
3258
|
+
"does",
|
|
3259
|
+
"did",
|
|
3260
|
+
"will",
|
|
3261
|
+
"would",
|
|
3262
|
+
"could",
|
|
3263
|
+
"should",
|
|
3264
|
+
"may",
|
|
3265
|
+
"might",
|
|
3266
|
+
"must",
|
|
3267
|
+
"shall",
|
|
3268
|
+
"can",
|
|
3269
|
+
"need",
|
|
3270
|
+
"dare",
|
|
3271
|
+
"to",
|
|
3272
|
+
"of",
|
|
3273
|
+
"in",
|
|
3274
|
+
"for",
|
|
3275
|
+
"on",
|
|
3276
|
+
"with",
|
|
3277
|
+
"at",
|
|
3278
|
+
"by",
|
|
3279
|
+
"from",
|
|
3280
|
+
"as",
|
|
3281
|
+
"into",
|
|
3282
|
+
"through",
|
|
3283
|
+
"during",
|
|
3284
|
+
"before",
|
|
3285
|
+
"after",
|
|
3286
|
+
"above",
|
|
3287
|
+
"below",
|
|
3288
|
+
"between",
|
|
3289
|
+
"out",
|
|
3290
|
+
"off",
|
|
3291
|
+
"over",
|
|
3292
|
+
"under",
|
|
3293
|
+
"again",
|
|
3294
|
+
"further",
|
|
3295
|
+
"then",
|
|
3296
|
+
"once",
|
|
3297
|
+
"that",
|
|
3298
|
+
"than",
|
|
3299
|
+
"too",
|
|
3300
|
+
"very",
|
|
3301
|
+
"just",
|
|
3302
|
+
"only",
|
|
3303
|
+
"not",
|
|
3304
|
+
"no",
|
|
3305
|
+
"all",
|
|
3306
|
+
"any",
|
|
3307
|
+
"both",
|
|
3308
|
+
"each",
|
|
3309
|
+
"every",
|
|
3310
|
+
"few",
|
|
3311
|
+
"more",
|
|
3312
|
+
"most",
|
|
3313
|
+
"other",
|
|
3314
|
+
"some",
|
|
3315
|
+
"such",
|
|
3316
|
+
"and",
|
|
3317
|
+
"but",
|
|
3318
|
+
"or",
|
|
3319
|
+
"nor",
|
|
3320
|
+
"so",
|
|
3321
|
+
"yet",
|
|
3322
|
+
"if",
|
|
3323
|
+
"it",
|
|
3324
|
+
"its",
|
|
3325
|
+
"they",
|
|
3326
|
+
"them",
|
|
3327
|
+
"their",
|
|
3328
|
+
"this",
|
|
3329
|
+
"these",
|
|
3330
|
+
"those",
|
|
3331
|
+
"which",
|
|
3332
|
+
"who",
|
|
3333
|
+
"whom",
|
|
3334
|
+
"what",
|
|
3335
|
+
"where",
|
|
3336
|
+
"when",
|
|
3337
|
+
"how",
|
|
3338
|
+
"why"
|
|
3339
|
+
]);
|
|
3340
|
+
const idTokens = id.toLowerCase().split(/[_\-]+/);
|
|
3341
|
+
const labelTokens = label.toLowerCase().split(/[\s\-—:,;.!?()[\]{}]+/);
|
|
3342
|
+
const allTokens = [...idTokens, ...labelTokens];
|
|
3343
|
+
const unique = /* @__PURE__ */ new Set();
|
|
3344
|
+
for (const token of allTokens) {
|
|
3345
|
+
const clean = token.replace(/[^a-z0-9]/g, "");
|
|
3346
|
+
if (clean.length >= 3 && !stopWords.has(clean)) {
|
|
3347
|
+
unique.add(clean);
|
|
3348
|
+
}
|
|
3349
|
+
}
|
|
3350
|
+
return [...unique];
|
|
3351
|
+
}
|
|
1660
3352
|
function checkContradictions(world, findings) {
|
|
1661
3353
|
if (!world.rules || world.rules.length < 2) return;
|
|
1662
3354
|
checkCircularExclusiveWith(world.rules, findings);
|
|
@@ -1862,6 +3554,251 @@ function describeEffect(effect) {
|
|
|
1862
3554
|
return `${effect.operation} ${effect.value}`;
|
|
1863
3555
|
}
|
|
1864
3556
|
}
|
|
3557
|
+
function checkGuardShadows(world, findings) {
|
|
3558
|
+
if (!world.guards?.guards || world.guards.guards.length < 2) return;
|
|
3559
|
+
const guards = world.guards.guards;
|
|
3560
|
+
for (let i = 0; i < guards.length; i++) {
|
|
3561
|
+
const guardA = guards[i];
|
|
3562
|
+
const enabledA = guardA.immutable || guardA.default_enabled !== false;
|
|
3563
|
+
if (!enabledA) continue;
|
|
3564
|
+
if (guardA.enforcement !== "block" && guardA.enforcement !== "pause") continue;
|
|
3565
|
+
for (let j = i + 1; j < guards.length; j++) {
|
|
3566
|
+
const guardB = guards[j];
|
|
3567
|
+
const enabledB = guardB.immutable || guardB.default_enabled !== false;
|
|
3568
|
+
if (!enabledB) continue;
|
|
3569
|
+
const overlap = guardA.intent_patterns.filter(
|
|
3570
|
+
(p) => guardB.intent_patterns.includes(p)
|
|
3571
|
+
);
|
|
3572
|
+
if (overlap.length === 0) continue;
|
|
3573
|
+
if (guardA.appliesTo?.length && guardB.appliesTo?.length) {
|
|
3574
|
+
const toolsA = new Set(guardA.appliesTo.map((t) => t.toLowerCase()));
|
|
3575
|
+
const toolsB = new Set(guardB.appliesTo.map((t) => t.toLowerCase()));
|
|
3576
|
+
const toolOverlap = [...toolsA].some((t) => toolsB.has(t));
|
|
3577
|
+
if (!toolOverlap) continue;
|
|
3578
|
+
}
|
|
3579
|
+
if (guardA.required_roles?.length && guardB.required_roles?.length) {
|
|
3580
|
+
const rolesA = new Set(guardA.required_roles);
|
|
3581
|
+
const rolesB = new Set(guardB.required_roles);
|
|
3582
|
+
const roleOverlap = [...rolesA].some((r) => rolesB.has(r));
|
|
3583
|
+
if (!roleOverlap) continue;
|
|
3584
|
+
}
|
|
3585
|
+
const patternsStr = overlap.join(", ");
|
|
3586
|
+
if (guardB.enforcement === guardA.enforcement) {
|
|
3587
|
+
findings.push(finding(
|
|
3588
|
+
`guard-shadow-${guardA.id}-${guardB.id}`,
|
|
3589
|
+
`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`,
|
|
3590
|
+
"warning",
|
|
3591
|
+
"contradiction",
|
|
3592
|
+
["guards/"],
|
|
3593
|
+
`${guardA.id}, ${guardB.id}`,
|
|
3594
|
+
`Remove "${guardB.label}", merge its patterns into "${guardA.label}", or reorder guards`
|
|
3595
|
+
));
|
|
3596
|
+
} else {
|
|
3597
|
+
findings.push(finding(
|
|
3598
|
+
`guard-conflict-${guardA.id}-${guardB.id}`,
|
|
3599
|
+
`Guards "${guardA.label}" (${guardA.enforcement.toUpperCase()}) and "${guardB.label}" (${guardB.enforcement.toUpperCase()}) share patterns [${patternsStr}] \u2014 "${guardA.label}" always wins because it appears first`,
|
|
3600
|
+
"warning",
|
|
3601
|
+
"contradiction",
|
|
3602
|
+
["guards/"],
|
|
3603
|
+
`${guardA.id}, ${guardB.id}`,
|
|
3604
|
+
`If "${guardB.label}" should take precedence, move it before "${guardA.label}" in guards.json`
|
|
3605
|
+
));
|
|
3606
|
+
}
|
|
3607
|
+
}
|
|
3608
|
+
}
|
|
3609
|
+
}
|
|
3610
|
+
function checkFailClosedSurfaces(world, findings) {
|
|
3611
|
+
const declaredSurfaces = world.guards?.tool_surfaces;
|
|
3612
|
+
if (!declaredSurfaces || declaredSurfaces.length === 0) return;
|
|
3613
|
+
const guards = world.guards?.guards ?? [];
|
|
3614
|
+
const guardedSurfaces = /* @__PURE__ */ new Set();
|
|
3615
|
+
let hasCatchAllGuard = false;
|
|
3616
|
+
for (const guard of guards) {
|
|
3617
|
+
const enabled = guard.immutable || guard.default_enabled !== false;
|
|
3618
|
+
if (!enabled) continue;
|
|
3619
|
+
if (!guard.appliesTo || guard.appliesTo.length === 0) {
|
|
3620
|
+
hasCatchAllGuard = true;
|
|
3621
|
+
} else {
|
|
3622
|
+
for (const tool of guard.appliesTo) {
|
|
3623
|
+
guardedSurfaces.add(tool.toLowerCase());
|
|
3624
|
+
}
|
|
3625
|
+
}
|
|
3626
|
+
}
|
|
3627
|
+
if (hasCatchAllGuard) return;
|
|
3628
|
+
for (const surface of declaredSurfaces) {
|
|
3629
|
+
if (!guardedSurfaces.has(surface.toLowerCase())) {
|
|
3630
|
+
findings.push(finding(
|
|
3631
|
+
`fail-open-surface-${surface.toLowerCase()}`,
|
|
3632
|
+
`Action surface "${surface}" has no governing guard \u2014 actions on this surface bypass governance entirely`,
|
|
3633
|
+
"warning",
|
|
3634
|
+
"guard-coverage",
|
|
3635
|
+
["guards.json"],
|
|
3636
|
+
void 0,
|
|
3637
|
+
`Add a guard with appliesTo including "${surface}", or add a catch-all guard (no appliesTo) to cover all surfaces`
|
|
3638
|
+
));
|
|
3639
|
+
}
|
|
3640
|
+
}
|
|
3641
|
+
}
|
|
3642
|
+
function checkReachability(world, findings) {
|
|
3643
|
+
if (!world.stateSchema?.variables) return;
|
|
3644
|
+
const vars = world.stateSchema.variables;
|
|
3645
|
+
for (const rule of world.rules ?? []) {
|
|
3646
|
+
for (const trigger of rule.triggers) {
|
|
3647
|
+
if (trigger.source !== "state") continue;
|
|
3648
|
+
const unreachable = isTriggerUnreachable(trigger, vars);
|
|
3649
|
+
if (unreachable) {
|
|
3650
|
+
findings.push(finding(
|
|
3651
|
+
`unreachable-rule-${rule.id}-${trigger.field}`,
|
|
3652
|
+
`Rule "${rule.id}" has unreachable trigger: ${trigger.field} ${trigger.operator} ${JSON.stringify(trigger.value)} \u2014 ${unreachable}`,
|
|
3653
|
+
"warning",
|
|
3654
|
+
"contradiction",
|
|
3655
|
+
["rules/", "state-schema.json"],
|
|
3656
|
+
rule.id,
|
|
3657
|
+
`Remove this rule or adjust the trigger condition to match the schema constraints for "${trigger.field}"`
|
|
3658
|
+
));
|
|
3659
|
+
}
|
|
3660
|
+
}
|
|
3661
|
+
if (rule.collapse_check) {
|
|
3662
|
+
const cc = rule.collapse_check;
|
|
3663
|
+
const unreachable = isTriggerUnreachable(
|
|
3664
|
+
{ field: cc.field, operator: cc.operator, value: cc.value },
|
|
3665
|
+
vars
|
|
3666
|
+
);
|
|
3667
|
+
if (unreachable) {
|
|
3668
|
+
findings.push(finding(
|
|
3669
|
+
`unreachable-collapse-${rule.id}`,
|
|
3670
|
+
`Rule "${rule.id}" has unreachable collapse_check: ${cc.field} ${cc.operator} ${cc.value} \u2014 ${unreachable}`,
|
|
3671
|
+
"warning",
|
|
3672
|
+
"contradiction",
|
|
3673
|
+
["rules/", "state-schema.json"],
|
|
3674
|
+
rule.id
|
|
3675
|
+
));
|
|
3676
|
+
}
|
|
3677
|
+
}
|
|
3678
|
+
}
|
|
3679
|
+
for (const gate of world.gates?.viability_classification ?? []) {
|
|
3680
|
+
const unreachable = isTriggerUnreachable(
|
|
3681
|
+
{ field: gate.field, operator: gate.operator, value: gate.value },
|
|
3682
|
+
vars
|
|
3683
|
+
);
|
|
3684
|
+
if (unreachable) {
|
|
3685
|
+
findings.push(finding(
|
|
3686
|
+
`unreachable-gate-${gate.status}`,
|
|
3687
|
+
`Viability gate "${gate.status}" has unreachable condition: ${gate.field} ${gate.operator} ${gate.value} \u2014 ${unreachable}`,
|
|
3688
|
+
"warning",
|
|
3689
|
+
"contradiction",
|
|
3690
|
+
["gates.json", "state-schema.json"],
|
|
3691
|
+
`gate-${gate.status}`
|
|
3692
|
+
));
|
|
3693
|
+
}
|
|
3694
|
+
}
|
|
3695
|
+
}
|
|
3696
|
+
function isTriggerUnreachable(trigger, vars) {
|
|
3697
|
+
const variable = vars[trigger.field];
|
|
3698
|
+
if (!variable) return null;
|
|
3699
|
+
const { operator, value } = trigger;
|
|
3700
|
+
if (variable.type === "number") {
|
|
3701
|
+
const numVal = typeof value === "number" ? value : Number(value);
|
|
3702
|
+
if (isNaN(numVal)) return null;
|
|
3703
|
+
const min = variable.min;
|
|
3704
|
+
const max = variable.max;
|
|
3705
|
+
if (operator === ">" || operator === ">=") {
|
|
3706
|
+
if (max !== void 0 && numVal >= max && operator === ">") {
|
|
3707
|
+
return `schema declares max=${max}, so ${trigger.field} can never exceed ${max}`;
|
|
3708
|
+
}
|
|
3709
|
+
if (max !== void 0 && numVal > max && operator === ">=") {
|
|
3710
|
+
return `schema declares max=${max}, so ${trigger.field} can never reach ${numVal}`;
|
|
3711
|
+
}
|
|
3712
|
+
}
|
|
3713
|
+
if (operator === "<" || operator === "<=") {
|
|
3714
|
+
if (min !== void 0 && numVal <= min && operator === "<") {
|
|
3715
|
+
return `schema declares min=${min}, so ${trigger.field} can never go below ${min}`;
|
|
3716
|
+
}
|
|
3717
|
+
if (min !== void 0 && numVal < min && operator === "<=") {
|
|
3718
|
+
return `schema declares min=${min}, so ${trigger.field} can never reach ${numVal}`;
|
|
3719
|
+
}
|
|
3720
|
+
}
|
|
3721
|
+
if (operator === "==") {
|
|
3722
|
+
if (min !== void 0 && numVal < min) {
|
|
3723
|
+
return `schema declares min=${min}, so ${trigger.field} can never equal ${numVal}`;
|
|
3724
|
+
}
|
|
3725
|
+
if (max !== void 0 && numVal > max) {
|
|
3726
|
+
return `schema declares max=${max}, so ${trigger.field} can never equal ${numVal}`;
|
|
3727
|
+
}
|
|
3728
|
+
}
|
|
3729
|
+
}
|
|
3730
|
+
if (variable.type === "enum" && variable.options) {
|
|
3731
|
+
if (operator === "==" && typeof value === "string") {
|
|
3732
|
+
if (!variable.options.includes(value)) {
|
|
3733
|
+
return `"${value}" is not in enum options [${variable.options.join(", ")}]`;
|
|
3734
|
+
}
|
|
3735
|
+
}
|
|
3736
|
+
if (operator === "!=" && typeof value === "string") {
|
|
3737
|
+
if (variable.options.length === 1 && variable.options[0] === value) {
|
|
3738
|
+
return `enum has only option "${value}", so != "${value}" can never be true`;
|
|
3739
|
+
}
|
|
3740
|
+
}
|
|
3741
|
+
if (operator === "in" && Array.isArray(value)) {
|
|
3742
|
+
const validValues = value.filter((v) => variable.options.includes(v));
|
|
3743
|
+
if (validValues.length === 0) {
|
|
3744
|
+
return `none of [${value.join(", ")}] are in enum options [${variable.options.join(", ")}]`;
|
|
3745
|
+
}
|
|
3746
|
+
}
|
|
3747
|
+
}
|
|
3748
|
+
if (variable.type === "boolean") {
|
|
3749
|
+
if (operator === "==" && typeof value !== "boolean" && value !== "true" && value !== "false") {
|
|
3750
|
+
return `boolean variable compared to non-boolean value "${value}"`;
|
|
3751
|
+
}
|
|
3752
|
+
}
|
|
3753
|
+
return null;
|
|
3754
|
+
}
|
|
3755
|
+
function checkStateCoverage(world, findings) {
|
|
3756
|
+
if (!world.stateSchema?.variables) return;
|
|
3757
|
+
const vars = world.stateSchema.variables;
|
|
3758
|
+
for (const [varId, variable] of Object.entries(vars)) {
|
|
3759
|
+
if (variable.type !== "enum" || !variable.options || variable.options.length <= 1) continue;
|
|
3760
|
+
const allOptions = new Set(variable.options);
|
|
3761
|
+
const coveredOptions = /* @__PURE__ */ new Set();
|
|
3762
|
+
for (const rule of world.rules ?? []) {
|
|
3763
|
+
for (const trigger of rule.triggers) {
|
|
3764
|
+
if (trigger.field !== varId || trigger.source !== "state") continue;
|
|
3765
|
+
if (trigger.operator === "==" && typeof trigger.value === "string") {
|
|
3766
|
+
coveredOptions.add(trigger.value);
|
|
3767
|
+
}
|
|
3768
|
+
if (trigger.operator === "in" && Array.isArray(trigger.value)) {
|
|
3769
|
+
for (const v of trigger.value) coveredOptions.add(v);
|
|
3770
|
+
}
|
|
3771
|
+
if (trigger.operator === "!=") {
|
|
3772
|
+
for (const opt of allOptions) {
|
|
3773
|
+
if (opt !== trigger.value) coveredOptions.add(opt);
|
|
3774
|
+
}
|
|
3775
|
+
}
|
|
3776
|
+
}
|
|
3777
|
+
}
|
|
3778
|
+
for (const gate of world.gates?.viability_classification ?? []) {
|
|
3779
|
+
if (gate.field !== varId) continue;
|
|
3780
|
+
if (gate.operator === "==" && typeof gate.value === "string") {
|
|
3781
|
+
coveredOptions.add(gate.value);
|
|
3782
|
+
}
|
|
3783
|
+
if (gate.operator === "in" && Array.isArray(gate.value)) {
|
|
3784
|
+
for (const v of gate.value) coveredOptions.add(v);
|
|
3785
|
+
}
|
|
3786
|
+
}
|
|
3787
|
+
if (coveredOptions.size === 0) continue;
|
|
3788
|
+
const uncovered = [...allOptions].filter((opt) => !coveredOptions.has(opt));
|
|
3789
|
+
if (uncovered.length > 0 && uncovered.length < allOptions.size) {
|
|
3790
|
+
findings.push(finding(
|
|
3791
|
+
`incomplete-state-coverage-${varId}`,
|
|
3792
|
+
`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`,
|
|
3793
|
+
"warning",
|
|
3794
|
+
"guard-coverage",
|
|
3795
|
+
["state-schema.json", "rules/", "gates.json"],
|
|
3796
|
+
varId,
|
|
3797
|
+
`Add rules or gates that handle ${uncovered.map((u) => `"${u}"`).join(", ")} for variable "${varId}"`
|
|
3798
|
+
));
|
|
3799
|
+
}
|
|
3800
|
+
}
|
|
3801
|
+
}
|
|
1865
3802
|
function checkOrphans(world, findings) {
|
|
1866
3803
|
if (!world.stateSchema?.variables || !world.rules) return;
|
|
1867
3804
|
const referencedVars = /* @__PURE__ */ new Set();
|
|
@@ -2023,6 +3960,57 @@ function computeInvariantCoverage(world) {
|
|
|
2023
3960
|
}
|
|
2024
3961
|
return Math.round(covered / world.invariants.length * 100);
|
|
2025
3962
|
}
|
|
3963
|
+
function computeGovernanceHealth(world, findings) {
|
|
3964
|
+
const guards = world.guards?.guards ?? [];
|
|
3965
|
+
if (guards.length === 0 && !world.kernel) return void 0;
|
|
3966
|
+
const declaredSurfaces = world.guards?.tool_surfaces ?? [];
|
|
3967
|
+
const guardedSurfaces = /* @__PURE__ */ new Set();
|
|
3968
|
+
let hasCatchAll = false;
|
|
3969
|
+
for (const guard of guards) {
|
|
3970
|
+
const enabled = guard.immutable || guard.default_enabled !== false;
|
|
3971
|
+
if (!enabled) continue;
|
|
3972
|
+
if (!guard.appliesTo || guard.appliesTo.length === 0) {
|
|
3973
|
+
hasCatchAll = true;
|
|
3974
|
+
} else {
|
|
3975
|
+
for (const t of guard.appliesTo) guardedSurfaces.add(t.toLowerCase());
|
|
3976
|
+
}
|
|
3977
|
+
}
|
|
3978
|
+
const allSurfaces = /* @__PURE__ */ new Set();
|
|
3979
|
+
for (const s of declaredSurfaces) allSurfaces.add(s.toLowerCase());
|
|
3980
|
+
for (const s of guardedSurfaces) allSurfaces.add(s);
|
|
3981
|
+
const surfaces = [...allSurfaces].map((name) => ({
|
|
3982
|
+
name,
|
|
3983
|
+
governed: hasCatchAll || guardedSurfaces.has(name)
|
|
3984
|
+
}));
|
|
3985
|
+
const surfacesCovered = hasCatchAll ? allSurfaces.size : guardedSurfaces.size;
|
|
3986
|
+
const structuralInvariants = (world.invariants ?? []).filter((i) => i.enforcement === "structural");
|
|
3987
|
+
let invariantsEnforced = 0;
|
|
3988
|
+
for (const inv of structuralInvariants) {
|
|
3989
|
+
const hasGuard = guards.some((g) => g.invariant_ref === inv.id && g.immutable);
|
|
3990
|
+
if (hasGuard) invariantsEnforced++;
|
|
3991
|
+
}
|
|
3992
|
+
const shadowedGuards = findings.filter((f) => f.id.startsWith("guard-shadow-")).length;
|
|
3993
|
+
const unenforcedInvariants = findings.filter((f) => f.id.startsWith("unenforced-invariant-")).length;
|
|
3994
|
+
const unreachableRules = findings.filter((f) => f.id.startsWith("unreachable-")).length;
|
|
3995
|
+
const incompleteStateCoverage = findings.filter((f) => f.id.startsWith("incomplete-state-coverage-")).length;
|
|
3996
|
+
const failOpenCount = findings.filter((f) => f.id.startsWith("fail-open-surface-")).length;
|
|
3997
|
+
let riskLevel = "low";
|
|
3998
|
+
const totalIssues = unenforcedInvariants + failOpenCount + incompleteStateCoverage;
|
|
3999
|
+
if (totalIssues > 0 || unreachableRules > 0) riskLevel = "moderate";
|
|
4000
|
+
if (totalIssues > 2 || unenforcedInvariants > 0 && failOpenCount > 0 || incompleteStateCoverage > 2) riskLevel = "high";
|
|
4001
|
+
return {
|
|
4002
|
+
surfacesCovered,
|
|
4003
|
+
surfacesTotal: allSurfaces.size,
|
|
4004
|
+
surfaces,
|
|
4005
|
+
invariantsEnforced,
|
|
4006
|
+
invariantsTotal: structuralInvariants.length,
|
|
4007
|
+
shadowedGuards,
|
|
4008
|
+
unenforcedInvariants,
|
|
4009
|
+
unreachableRules,
|
|
4010
|
+
incompleteStateCoverage,
|
|
4011
|
+
riskLevel
|
|
4012
|
+
};
|
|
4013
|
+
}
|
|
2026
4014
|
function finding(id, message, severity, category, affectedBlocks, source, suggestion) {
|
|
2027
4015
|
const f = { id, message, severity, category, affectedBlocks };
|
|
2028
4016
|
if (source) f.source = source;
|
|
@@ -2077,7 +4065,7 @@ function splitSections(markdown) {
|
|
|
2077
4065
|
}
|
|
2078
4066
|
return { frontmatter, sections };
|
|
2079
4067
|
}
|
|
2080
|
-
function
|
|
4068
|
+
function parseFrontmatter2(yaml, issues) {
|
|
2081
4069
|
const result = {};
|
|
2082
4070
|
for (const line of yaml.split("\n")) {
|
|
2083
4071
|
const trimmed = line.trim();
|
|
@@ -2426,7 +4414,7 @@ function parseValueLiteral(raw) {
|
|
|
2426
4414
|
function parseWorldMarkdown(markdown) {
|
|
2427
4415
|
const issues = [];
|
|
2428
4416
|
const { frontmatter: fmRaw, sections } = splitSections(markdown);
|
|
2429
|
-
const frontmatter =
|
|
4417
|
+
const frontmatter = parseFrontmatter2(fmRaw, issues);
|
|
2430
4418
|
const findSection = (name) => sections.find((s) => s.name.toLowerCase() === name.toLowerCase());
|
|
2431
4419
|
const thesisSection = findSection("Thesis");
|
|
2432
4420
|
const thesis = thesisSection ? parseThesis(thesisSection.content, thesisSection.startLine, issues) : "";
|
|
@@ -2831,12 +4819,12 @@ function normalizeWorldMarkdown(markdown) {
|
|
|
2831
4819
|
|
|
2832
4820
|
// src/engine/derive-prompt.ts
|
|
2833
4821
|
var import_promises = require("fs/promises");
|
|
2834
|
-
var
|
|
4822
|
+
var import_path3 = require("path");
|
|
2835
4823
|
var import_meta = {};
|
|
2836
4824
|
var WORLD_FILENAME = "derivation-world.nv-world.md";
|
|
2837
4825
|
function getModuleDir() {
|
|
2838
4826
|
try {
|
|
2839
|
-
return (0,
|
|
4827
|
+
return (0, import_path3.dirname)(new URL(import_meta.url).pathname);
|
|
2840
4828
|
} catch {
|
|
2841
4829
|
return __dirname;
|
|
2842
4830
|
}
|
|
@@ -2844,8 +4832,8 @@ function getModuleDir() {
|
|
|
2844
4832
|
async function loadDerivationWorld() {
|
|
2845
4833
|
const moduleDir = getModuleDir();
|
|
2846
4834
|
const candidates = [
|
|
2847
|
-
(0,
|
|
2848
|
-
(0,
|
|
4835
|
+
(0, import_path3.join)(moduleDir, "..", "worlds", WORLD_FILENAME),
|
|
4836
|
+
(0, import_path3.join)(moduleDir, "worlds", WORLD_FILENAME)
|
|
2849
4837
|
];
|
|
2850
4838
|
for (const candidate of candidates) {
|
|
2851
4839
|
try {
|
|
@@ -3120,15 +5108,15 @@ function createProvider(config) {
|
|
|
3120
5108
|
|
|
3121
5109
|
// src/providers/config-manager.ts
|
|
3122
5110
|
var import_promises2 = require("fs/promises");
|
|
3123
|
-
var
|
|
5111
|
+
var import_path4 = require("path");
|
|
3124
5112
|
var import_os = require("os");
|
|
3125
5113
|
function getConfigDir() {
|
|
3126
5114
|
const xdg = process.env.XDG_CONFIG_HOME;
|
|
3127
|
-
if (xdg) return (0,
|
|
3128
|
-
return (0,
|
|
5115
|
+
if (xdg) return (0, import_path4.join)(xdg, "neuroverse");
|
|
5116
|
+
return (0, import_path4.join)((0, import_os.homedir)(), ".neuroverse");
|
|
3129
5117
|
}
|
|
3130
5118
|
function getConfigPath() {
|
|
3131
|
-
return (0,
|
|
5119
|
+
return (0, import_path4.join)(getConfigDir(), "config.json");
|
|
3132
5120
|
}
|
|
3133
5121
|
async function loadConfig() {
|
|
3134
5122
|
try {
|
|
@@ -4197,12 +6185,21 @@ var CONFIGURE_AI_EXIT_CODES = {
|
|
|
4197
6185
|
DERIVE_EXIT_CODES,
|
|
4198
6186
|
FileAuditLogger,
|
|
4199
6187
|
GUARD_EXIT_CODES,
|
|
6188
|
+
McpGovernanceServer,
|
|
6189
|
+
ModelAdapter,
|
|
6190
|
+
PLAN_EXIT_CODES,
|
|
6191
|
+
PROVIDERS,
|
|
6192
|
+
SessionManager,
|
|
4200
6193
|
VALIDATE_EXIT_CODES,
|
|
6194
|
+
advancePlan,
|
|
6195
|
+
buildPlanCheck,
|
|
4201
6196
|
createGovernanceEngine,
|
|
4202
6197
|
deriveWorld,
|
|
6198
|
+
describeActiveWorld,
|
|
4203
6199
|
emitWorldDefinition,
|
|
4204
6200
|
evaluateCondition,
|
|
4205
6201
|
evaluateGuard,
|
|
6202
|
+
evaluatePlan,
|
|
4206
6203
|
eventToAllowlistKey,
|
|
4207
6204
|
explainWorld,
|
|
4208
6205
|
extractWorldMarkdown,
|
|
@@ -4210,16 +6207,25 @@ var CONFIGURE_AI_EXIT_CODES = {
|
|
|
4210
6207
|
formatVerdictOneLine,
|
|
4211
6208
|
generateImpactReport,
|
|
4212
6209
|
generateImpactReportFromFile,
|
|
6210
|
+
getActiveWorldName,
|
|
6211
|
+
getPlanProgress,
|
|
4213
6212
|
improveWorld,
|
|
6213
|
+
listWorlds,
|
|
4214
6214
|
loadWorld,
|
|
4215
6215
|
loadWorldFromDirectory,
|
|
4216
6216
|
normalizeWorldMarkdown,
|
|
6217
|
+
parsePlanMarkdown,
|
|
4217
6218
|
parseWorldMarkdown,
|
|
4218
6219
|
readAuditLog,
|
|
4219
6220
|
renderExplainText,
|
|
4220
6221
|
renderImpactReport,
|
|
4221
6222
|
renderImproveText,
|
|
4222
6223
|
renderSimulateText,
|
|
6224
|
+
resolveProvider,
|
|
6225
|
+
resolveWorldPath,
|
|
6226
|
+
runInteractiveMode,
|
|
6227
|
+
runPipeMode,
|
|
6228
|
+
setActiveWorld,
|
|
4223
6229
|
simulateWorld,
|
|
4224
6230
|
summarizeAuditEvents,
|
|
4225
6231
|
validateWorld,
|