prism-mcp-server 7.2.0 → 7.3.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.
@@ -0,0 +1,479 @@
1
+ import * as fs from "fs";
2
+ import { getQuickJS } from "quickjs-emscripten";
3
+ import { TestSuiteSchema, } from "./schema.js";
4
+ import { evaluateSeverityGates, resolveEffectiveSeverity } from "./severityPolicy.js";
5
+ // ─── Utilities ──────────────────────────────────────────────
6
+ /** Deeply match objects (expected ⊆ actual) */
7
+ function deepMatch(actual, expected) {
8
+ if (typeof expected !== 'object' || expected === null) {
9
+ return actual === expected;
10
+ }
11
+ for (const key of Object.keys(expected)) {
12
+ if (typeof actual[key] === 'object') {
13
+ if (!deepMatch(actual[key], expected[key]))
14
+ return false;
15
+ }
16
+ else if (actual[key] !== expected[key]) {
17
+ return false;
18
+ }
19
+ }
20
+ return true;
21
+ }
22
+ /** Race a promise against a timeout */
23
+ function withTimeout(promise, ms) {
24
+ return Promise.race([
25
+ promise,
26
+ new Promise((_, reject) => setTimeout(() => reject(new Error(`Assertion timed out after ${ms}ms`)), ms)),
27
+ ]);
28
+ }
29
+ /** Sleep utility for retry backoff */
30
+ function sleep(ms) {
31
+ return new Promise(resolve => setTimeout(resolve, ms));
32
+ }
33
+ const PRIVATE_IPV4_CIDRS = [
34
+ [ipToInt("10.0.0.0"), ipToInt("10.255.255.255")],
35
+ [ipToInt("127.0.0.0"), ipToInt("127.255.255.255")],
36
+ [ipToInt("169.254.0.0"), ipToInt("169.254.255.255")],
37
+ [ipToInt("172.16.0.0"), ipToInt("172.31.255.255")],
38
+ [ipToInt("192.168.0.0"), ipToInt("192.168.255.255")],
39
+ [ipToInt("100.64.0.0"), ipToInt("100.127.255.255")],
40
+ [ipToInt("0.0.0.0"), ipToInt("0.255.255.255")],
41
+ [ipToInt("224.0.0.0"), ipToInt("255.255.255.255")],
42
+ ];
43
+ function ipToInt(ip) {
44
+ return ip.split(".").reduce((acc, octet) => (acc << 8) + Number(octet), 0) >>> 0;
45
+ }
46
+ function isPrivateIpv4(hostname) {
47
+ if (!/^\d+\.\d+\.\d+\.\d+$/.test(hostname))
48
+ return false;
49
+ const value = ipToInt(hostname);
50
+ return PRIVATE_IPV4_CIDRS.some(([start, end]) => value >= start && value <= end);
51
+ }
52
+ function isDisallowedIpv6(hostname) {
53
+ const normalized = hostname.toLowerCase();
54
+ return (normalized === "::1" ||
55
+ normalized.startsWith("fc") || // fc00::/7
56
+ normalized.startsWith("fd") || // fc00::/7
57
+ normalized.startsWith("fe8") || // fe80::/10
58
+ normalized.startsWith("fe9") ||
59
+ normalized.startsWith("fea") ||
60
+ normalized.startsWith("feb") ||
61
+ normalized === "::" ||
62
+ normalized === "0:0:0:0:0:0:0:0");
63
+ }
64
+ function validateHttpTarget(target) {
65
+ let parsed;
66
+ try {
67
+ parsed = new URL(target);
68
+ }
69
+ catch {
70
+ return { ok: false, reason: `Invalid URL: ${target}` };
71
+ }
72
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
73
+ return { ok: false, reason: `Unsupported protocol: ${parsed.protocol}` };
74
+ }
75
+ const hostname = parsed.hostname.toLowerCase();
76
+ if (hostname === "localhost" || hostname.endsWith(".localhost") || hostname.endsWith(".local")) {
77
+ return { ok: false, reason: `Blocked host: ${hostname}` };
78
+ }
79
+ // Block decimal/hex IP obfuscation (e.g. http://2852039166/ → 169.254.169.254)
80
+ if (/^0x[0-9a-fA-F]+$/.test(hostname) || /^\d+$/.test(hostname)) {
81
+ return { ok: false, reason: `Blocked obfuscated IP: ${hostname}` };
82
+ }
83
+ if (isPrivateIpv4(hostname) || isDisallowedIpv6(hostname)) {
84
+ return { ok: false, reason: `Blocked internal IP: ${hostname}` };
85
+ }
86
+ return { ok: true };
87
+ }
88
+ function skipResult(test, reason) {
89
+ return {
90
+ id: test.id,
91
+ layer: test.layer,
92
+ description: test.description,
93
+ severity: test.severity,
94
+ passed: false,
95
+ skipped: true,
96
+ skip_reason: reason,
97
+ duration_ms: 0,
98
+ retries_used: 0,
99
+ };
100
+ }
101
+ function prepareAssertions(tests, filterLayers, minSeverity, config) {
102
+ const preparedById = new Map();
103
+ const orderedIds = [];
104
+ const precomputed = new Map();
105
+ for (let index = 0; index < tests.length; index++) {
106
+ const test = tests[index];
107
+ orderedIds.push(test.id);
108
+ if (preparedById.has(test.id)) {
109
+ precomputed.set(test.id, skipResult(test, `Duplicate assertion id "${test.id}"`));
110
+ continue;
111
+ }
112
+ let skipped;
113
+ if (!filterLayers.includes(test.layer)) {
114
+ skipped = skipResult(test, `Layer "${test.layer}" not in active layers [${filterLayers.join(", ")}]`);
115
+ }
116
+ if (!skipped && minSeverity) {
117
+ const effective = resolveEffectiveSeverity(test.severity, config.default_severity);
118
+ const severityOrder = { warn: 0, gate: 1, abort: 2 };
119
+ if (severityOrder[effective] < severityOrder[minSeverity]) {
120
+ skipped = skipResult(test, `Severity "${effective}" below minimum "${minSeverity}"`);
121
+ }
122
+ }
123
+ const prepared = {
124
+ test,
125
+ skipped,
126
+ dependents: [],
127
+ };
128
+ preparedById.set(test.id, prepared);
129
+ if (skipped)
130
+ precomputed.set(test.id, skipped);
131
+ }
132
+ for (const prepared of preparedById.values()) {
133
+ const dep = prepared.test.depends_on;
134
+ if (!dep)
135
+ continue;
136
+ const depAssertion = preparedById.get(dep);
137
+ if (!depAssertion) {
138
+ const depReason = `Dependency "${dep}" not found`;
139
+ prepared.dependencyReason = depReason;
140
+ precomputed.set(prepared.test.id, skipResult(prepared.test, depReason));
141
+ continue;
142
+ }
143
+ depAssertion.dependents.push(prepared.test.id);
144
+ }
145
+ const indegree = new Map();
146
+ for (const [id, prepared] of preparedById.entries()) {
147
+ indegree.set(id, prepared.test.depends_on && !prepared.dependencyReason ? 1 : 0);
148
+ }
149
+ const queue = [];
150
+ for (const [id, count] of indegree.entries()) {
151
+ if (count === 0)
152
+ queue.push(id);
153
+ }
154
+ let visited = 0;
155
+ while (queue.length > 0) {
156
+ const id = queue.shift();
157
+ visited++;
158
+ const prepared = preparedById.get(id);
159
+ if (!prepared)
160
+ continue;
161
+ for (const dependentId of prepared.dependents) {
162
+ const next = (indegree.get(dependentId) || 0) - 1;
163
+ indegree.set(dependentId, next);
164
+ if (next === 0)
165
+ queue.push(dependentId);
166
+ }
167
+ }
168
+ if (visited < preparedById.size) {
169
+ for (const [id, count] of indegree.entries()) {
170
+ if (count > 0) {
171
+ const prepared = preparedById.get(id);
172
+ if (!prepared)
173
+ continue;
174
+ if (!precomputed.has(id)) {
175
+ precomputed.set(id, skipResult(prepared.test, `Cyclic dependency involving "${id}"`));
176
+ }
177
+ }
178
+ }
179
+ }
180
+ return { preparedById, orderedIds, precomputed };
181
+ }
182
+ // ─── Default config when none provided ──────────────────────
183
+ const DEFAULT_CONFIG = {
184
+ enabled: true,
185
+ layers: ["data", "agent", "pipeline"],
186
+ default_severity: "warn",
187
+ };
188
+ // ─── v7.2.0: Enhanced Verification Runner ───────────────────
189
+ export class VerificationRunner {
190
+ /**
191
+ * v7.2.0 enhanced suite runner.
192
+ *
193
+ * - Layer/severity filtering
194
+ * - Per-assertion timeout
195
+ * - Retry logic for transient failures
196
+ * - Dependency chain resolution
197
+ * - Structured VerificationResult with per-layer breakdown
198
+ */
199
+ static async runSuite(jsonContent, options) {
200
+ const startTime = Date.now();
201
+ const config = options?.config ?? DEFAULT_CONFIG;
202
+ const filterLayers = options?.layers ?? config.layers;
203
+ const minSeverity = options?.minSeverity;
204
+ let assertionResults = [];
205
+ try {
206
+ const parsed = JSON.parse(jsonContent);
207
+ const suite = TestSuiteSchema.parse(parsed);
208
+ const { preparedById, orderedIds, precomputed } = prepareAssertions(suite.tests, filterLayers, minSeverity, config);
209
+ const outcomes = new Map();
210
+ const resultById = new Map(precomputed);
211
+ for (const [id] of preparedById.entries()) {
212
+ if (precomputed.has(id)) {
213
+ outcomes.set(id, { passed: false, skipped: true });
214
+ }
215
+ }
216
+ const pending = new Map(preparedById);
217
+ for (const id of precomputed.keys()) {
218
+ pending.delete(id);
219
+ }
220
+ while (pending.size > 0) {
221
+ const ready = [];
222
+ for (const prepared of pending.values()) {
223
+ if (prepared.test.depends_on && !outcomes.has(prepared.test.depends_on)) {
224
+ continue;
225
+ }
226
+ ready.push(prepared);
227
+ }
228
+ if (ready.length === 0) {
229
+ // Safety guard — remaining assertions are unresolved; skip them.
230
+ for (const prepared of pending.values()) {
231
+ const unresolved = skipResult(prepared.test, `Unresolved dependency state for "${prepared.test.id}"`);
232
+ resultById.set(prepared.test.id, unresolved);
233
+ outcomes.set(prepared.test.id, { passed: false, skipped: true });
234
+ }
235
+ pending.clear();
236
+ break;
237
+ }
238
+ await Promise.all(ready.map(async (prepared) => {
239
+ pending.delete(prepared.test.id);
240
+ if (prepared.skipped) {
241
+ resultById.set(prepared.test.id, prepared.skipped);
242
+ outcomes.set(prepared.test.id, { passed: false, skipped: true });
243
+ return;
244
+ }
245
+ if (prepared.dependencyReason) {
246
+ const skipped = skipResult(prepared.test, prepared.dependencyReason);
247
+ resultById.set(prepared.test.id, skipped);
248
+ outcomes.set(prepared.test.id, { passed: false, skipped: true });
249
+ return;
250
+ }
251
+ if (prepared.test.depends_on) {
252
+ const depOutcome = outcomes.get(prepared.test.depends_on);
253
+ if (!depOutcome || !depOutcome.passed) {
254
+ const depStatus = depOutcome?.skipped ? "skipped" : "failed";
255
+ const skipped = skipResult(prepared.test, `Dependency "${prepared.test.depends_on}" ${depStatus}`);
256
+ resultById.set(prepared.test.id, skipped);
257
+ outcomes.set(prepared.test.id, { passed: false, skipped: true });
258
+ return;
259
+ }
260
+ }
261
+ const assertionStart = Date.now();
262
+ const maxRetries = prepared.test.retry_count ?? 0;
263
+ let lastError = "";
264
+ let retriesUsed = 0;
265
+ let passed = false;
266
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
267
+ if (attempt > 0) {
268
+ retriesUsed++;
269
+ await sleep(Math.min(1000 * attempt, 3000));
270
+ }
271
+ try {
272
+ const runPromise = this.runAssertion(prepared.test);
273
+ const result = prepared.test.timeout_ms
274
+ ? await withTimeout(runPromise, prepared.test.timeout_ms)
275
+ : await runPromise;
276
+ if (result.passed) {
277
+ passed = true;
278
+ break;
279
+ }
280
+ else {
281
+ lastError = result.error || "Assertion returned false";
282
+ }
283
+ }
284
+ catch (e) {
285
+ lastError = e.message || String(e);
286
+ }
287
+ }
288
+ const result = {
289
+ id: prepared.test.id,
290
+ layer: prepared.test.layer,
291
+ description: prepared.test.description,
292
+ severity: prepared.test.severity,
293
+ passed,
294
+ error: passed ? undefined : lastError,
295
+ duration_ms: Date.now() - assertionStart,
296
+ retries_used: retriesUsed,
297
+ skipped: false,
298
+ };
299
+ resultById.set(prepared.test.id, result);
300
+ outcomes.set(prepared.test.id, { passed, skipped: false });
301
+ }));
302
+ }
303
+ assertionResults = orderedIds
304
+ .map((id) => resultById.get(id))
305
+ .filter((result) => Boolean(result));
306
+ }
307
+ catch (e) {
308
+ // Parse error — return a single synthetic failure
309
+ assertionResults = [{
310
+ id: "__parse_error__",
311
+ layer: "pipeline",
312
+ description: "Test suite specification parse",
313
+ severity: "abort",
314
+ passed: false,
315
+ error: `Specification Parse Error: ${e.message}`,
316
+ duration_ms: Date.now() - startTime,
317
+ retries_used: 0,
318
+ skipped: false,
319
+ }];
320
+ }
321
+ // ── Build per-layer breakdown ──
322
+ const byLayer = {};
323
+ for (const ar of assertionResults) {
324
+ if (!byLayer[ar.layer]) {
325
+ byLayer[ar.layer] = { passed: 0, failed: 0, skipped: 0, total: 0, assertions: [] };
326
+ }
327
+ const lr = byLayer[ar.layer];
328
+ lr.total++;
329
+ if (ar.skipped)
330
+ lr.skipped++;
331
+ else if (ar.passed)
332
+ lr.passed++;
333
+ else
334
+ lr.failed++;
335
+ lr.assertions.push(ar);
336
+ }
337
+ // ── Evaluate severity gates ──
338
+ const severityGate = evaluateSeverityGates(assertionResults, config);
339
+ const passedCount = assertionResults.filter(a => a.passed).length;
340
+ const failedCount = assertionResults.filter(a => !a.passed && !a.skipped).length;
341
+ const skippedCount = assertionResults.filter(a => a.skipped).length;
342
+ return {
343
+ passed: failedCount === 0,
344
+ total: assertionResults.length,
345
+ passed_count: passedCount,
346
+ failed_count: failedCount,
347
+ skipped_count: skippedCount,
348
+ by_layer: byLayer,
349
+ duration_ms: Date.now() - startTime,
350
+ severity_gate: severityGate,
351
+ assertion_results: assertionResults,
352
+ };
353
+ }
354
+ // ── Legacy API (backward compat with v5.3 callers) ────────
355
+ static async runSuiteLegacy(jsonContent) {
356
+ const result = await this.runSuite(jsonContent);
357
+ return {
358
+ passed: result.passed,
359
+ failures: result.assertion_results
360
+ .filter(a => !a.passed && !a.skipped)
361
+ .map(a => `[${a.layer}] ${a.description} failed: ${a.error}`),
362
+ };
363
+ }
364
+ static async runAssertion(test) {
365
+ const a = test.assertion;
366
+ switch (a.type) {
367
+ case "file_exists": {
368
+ const exists = fs.existsSync(a.target);
369
+ return exists === a.expected
370
+ ? { passed: true }
371
+ : { passed: false, error: `Expected file_exists=${a.expected} for ${a.target}` };
372
+ }
373
+ case "file_contains": {
374
+ if (!fs.existsSync(a.target)) {
375
+ return { passed: false, error: `File not found: ${a.target}` };
376
+ }
377
+ const content = fs.readFileSync(a.target, "utf8");
378
+ const contains = content.includes(a.expected);
379
+ return contains
380
+ ? { passed: true }
381
+ : { passed: false, error: `File ${a.target} did not contain expected string` };
382
+ }
383
+ case "http_status": {
384
+ try {
385
+ const targetCheck = validateHttpTarget(a.target);
386
+ if (!targetCheck.ok) {
387
+ return { passed: false, error: `HTTP target blocked: ${targetCheck.reason}` };
388
+ }
389
+ const res = await fetch(a.target);
390
+ return res.status === a.expected
391
+ ? { passed: true }
392
+ : { passed: false, error: `Expected status ${a.expected}, got ${res.status} for ${a.target}` };
393
+ }
394
+ catch (e) {
395
+ return { passed: false, error: `HTTP fetch failed: ${e.message}` };
396
+ }
397
+ }
398
+ case "sqlite_query": {
399
+ try {
400
+ const { execFileSync } = await import("child_process");
401
+ const dbFile = process.env.DATABASE_URL?.replace("file:", "") || ".prism-mcp/local.db";
402
+ if (!fs.existsSync(dbFile))
403
+ return { passed: false, error: `DB file not found: ${dbFile}` };
404
+ // v7.2.0 FIX: Use execFileSync with -readonly flag for cross-platform read-only.
405
+ // The URI ?mode=ro approach requires SQLITE_USE_URI=1 at compile time,
406
+ // which isn't guaranteed on all distributions. -readonly is a native CLI flag.
407
+ const rawResult = execFileSync("sqlite3", ["-readonly", dbFile, a.target, "--json"], {
408
+ encoding: "utf8",
409
+ timeout: 10_000, // 10s hard limit for any SQL query
410
+ });
411
+ const rows = rawResult.trim() ? JSON.parse(rawResult) : [];
412
+ return deepMatch(rows, a.expected)
413
+ ? { passed: true }
414
+ : { passed: false, error: `SQL expected ${JSON.stringify(a.expected)}, got ${JSON.stringify(rows)}` };
415
+ }
416
+ catch (e) {
417
+ return { passed: false, error: `SQL execution failed: ${e.message}` };
418
+ }
419
+ }
420
+ case "quickjs_eval": {
421
+ return await this.runQuickJs(a.code, a.inputs || {});
422
+ }
423
+ default:
424
+ return { passed: false, error: `Unknown assertion type` };
425
+ }
426
+ }
427
+ /**
428
+ * Execute JavaScript safely in WebAssembly sandbox via quickjs-emscripten
429
+ */
430
+ static async runQuickJs(code, inputs) {
431
+ const QuickJS = await getQuickJS();
432
+ const vm = QuickJS.newContext();
433
+ try {
434
+ vm.runtime.setMemoryLimit(10 * 1024 * 1024);
435
+ vm.runtime.setMaxStackSize(512 * 1024);
436
+ let ops = 0;
437
+ vm.runtime.setInterruptHandler(() => {
438
+ ops++;
439
+ return ops > 10000;
440
+ });
441
+ // v7.2.0 FIX: Properly inject inputs as a JSON string literal,
442
+ // then JSON.parse inside the VM. The previous approach broke on
443
+ // object/array inputs due to unquoted interpolation.
444
+ const inputsJson = JSON.stringify(inputs);
445
+ const parseResult = vm.evalCode(`JSON.parse('${inputsJson.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}')`);
446
+ if (parseResult.error) {
447
+ const err = vm.dump(parseResult.error);
448
+ parseResult.error.dispose();
449
+ return { passed: false, error: `QuickJS Input Parse Error: ${err}` };
450
+ }
451
+ if (parseResult.value) {
452
+ vm.setProp(vm.global, "inputs", parseResult.value || vm.undefined);
453
+ parseResult.value.dispose();
454
+ }
455
+ const wrappedCode = `(function() { ${code} })()`;
456
+ const result = vm.evalCode(wrappedCode);
457
+ if (result.error) {
458
+ const errorString = vm.dump(result.error);
459
+ result.error.dispose();
460
+ return { passed: false, error: `QuickJS Error: ${errorString}` };
461
+ }
462
+ const value = result.value ? vm.dump(result.value) : undefined;
463
+ if (result.value)
464
+ result.value.dispose();
465
+ if (typeof value !== "boolean") {
466
+ return { passed: false, error: `QuickJS evaluation returned ${typeof value}, expected boolean` };
467
+ }
468
+ return value
469
+ ? { passed: true }
470
+ : { passed: false, error: `Sandbox evaluation returned false` };
471
+ }
472
+ catch (e) {
473
+ return { passed: false, error: `Sandbox crashed: ${e.message}` };
474
+ }
475
+ finally {
476
+ vm.dispose();
477
+ }
478
+ }
479
+ }
@@ -0,0 +1,46 @@
1
+ import { z } from "zod";
2
+ // ─── v7.2.0: Severity Levels ────────────────────────────────
3
+ // warn → log and continue
4
+ // gate → block progression until resolved
5
+ // abort → rollback (fail the pipeline)
6
+ export const SeverityLevel = z.enum(["warn", "gate", "abort"]).default("warn");
7
+ // Base for all assertions
8
+ const BaseAssertion = z.object({
9
+ target: z.string().describe("The SQL query, URL, or file path"),
10
+ expected: z.any().describe("The expected outcome to match against"),
11
+ });
12
+ // 1. Declarative Assertions (Split for better type inference)
13
+ export const SqliteAssertionSchema = BaseAssertion.extend({ type: z.literal("sqlite_query") });
14
+ export const HttpStatusAssertionSchema = BaseAssertion.extend({ type: z.literal("http_status") });
15
+ export const FileExistsAssertionSchema = BaseAssertion.extend({ type: z.literal("file_exists") });
16
+ export const FileContainsAssertionSchema = BaseAssertion.extend({ type: z.literal("file_contains") });
17
+ // 2. Sandboxed JS Assertion
18
+ export const SandboxedJsAssertionSchema = z.object({
19
+ type: z.literal("quickjs_eval"),
20
+ code: z.string().describe("JS code to run in QuickJS. Must return a boolean."),
21
+ inputs: z.record(z.string(), z.any()).optional().describe("Data to inject as globals into the sandbox"),
22
+ });
23
+ // 3. Main Schema Wrapper (v7.2.0 enhanced)
24
+ export const TestAssertionSchema = z.object({
25
+ id: z.string(),
26
+ layer: z.enum(["data", "agent", "pipeline"]),
27
+ description: z.string(),
28
+ severity: SeverityLevel,
29
+ // v7.2.0: per-assertion timeout in ms
30
+ timeout_ms: z.number().int().min(50).max(120_000).optional().describe("Per-assertion timeout in milliseconds"),
31
+ // v7.2.0: retry on transient failures (e.g. http_status)
32
+ retry_count: z.number().int().min(0).max(5).optional().describe("Number of retries on transient failures"),
33
+ // v7.2.0: assertion dependency chain
34
+ depends_on: z.string().optional().describe("ID of assertion that must pass first"),
35
+ // Discriminated union gives pinpoint accuracy on parsing errors
36
+ assertion: z.discriminatedUnion("type", [
37
+ SqliteAssertionSchema,
38
+ HttpStatusAssertionSchema,
39
+ FileExistsAssertionSchema,
40
+ FileContainsAssertionSchema,
41
+ SandboxedJsAssertionSchema
42
+ ])
43
+ });
44
+ export const TestSuiteSchema = z.object({
45
+ tests: z.array(TestAssertionSchema)
46
+ });
@@ -0,0 +1,94 @@
1
+ // ─── v7.2.0: Severity Gate Enforcement ──────────────────────
2
+ // Separated from the runner for testability.
3
+ //
4
+ // Rules:
5
+ // - "warn" failures → logged, always continue
6
+ // - "gate" failures → block. Return "block" action with failed assertions list
7
+ // - "abort" failures → immediate abort. Return "abort" action
8
+ //
9
+ // When PRISM_VERIFICATION_DEFAULT_SEVERITY is set, it overrides
10
+ // individual assertion severity levels (acts as a floor).
11
+ /**
12
+ * Map severity string to numeric rank for comparison.
13
+ * Higher = more severe.
14
+ */
15
+ function severityRank(s) {
16
+ switch (s) {
17
+ case "warn": return 0;
18
+ case "gate": return 1;
19
+ case "abort": return 2;
20
+ default: return 0;
21
+ }
22
+ }
23
+ /**
24
+ * Resolve the effective severity for an assertion, considering
25
+ * the global default severity override (acts as a floor).
26
+ */
27
+ export function resolveEffectiveSeverity(assertionSeverity, defaultSeverity) {
28
+ const assertRank = severityRank(assertionSeverity);
29
+ const defaultRank = severityRank(defaultSeverity);
30
+ // Use whichever is more severe (floor behavior)
31
+ return assertRank >= defaultRank ? assertionSeverity : defaultSeverity;
32
+ }
33
+ /**
34
+ * Evaluate all assertion results against severity gates.
35
+ *
36
+ * Returns a SeverityGateResult indicating the overall action:
37
+ * - "continue" → all clear, or only "warn"-level failures
38
+ * - "block" → at least one "gate"-level failure (no "abort")
39
+ * - "abort" → at least one "abort"-level failure
40
+ */
41
+ export function evaluateSeverityGates(results, config) {
42
+ const failures = results.filter(r => !r.passed && !r.skipped);
43
+ if (failures.length === 0) {
44
+ return {
45
+ action: "continue",
46
+ failed_assertions: [],
47
+ summary: "All assertions passed."
48
+ };
49
+ }
50
+ // Resolve effective severities and categorize failures
51
+ const abortFailures = [];
52
+ const gateFailures = [];
53
+ const warnFailures = [];
54
+ for (const f of failures) {
55
+ const effective = resolveEffectiveSeverity(f.severity, config.default_severity);
56
+ switch (effective) {
57
+ case "abort":
58
+ abortFailures.push(f);
59
+ break;
60
+ case "gate":
61
+ gateFailures.push(f);
62
+ break;
63
+ case "warn":
64
+ default:
65
+ warnFailures.push(f);
66
+ break;
67
+ }
68
+ }
69
+ // Abort takes precedence over gate
70
+ if (abortFailures.length > 0) {
71
+ const ids = abortFailures.map(a => a.id).join(", ");
72
+ return {
73
+ action: "abort",
74
+ failed_assertions: [...abortFailures, ...gateFailures, ...warnFailures],
75
+ summary: `ABORT: ${abortFailures.length} abort-level failure(s) [${ids}]. ` +
76
+ `${gateFailures.length} gate, ${warnFailures.length} warn failures also present.`
77
+ };
78
+ }
79
+ if (gateFailures.length > 0) {
80
+ const ids = gateFailures.map(a => a.id).join(", ");
81
+ return {
82
+ action: "block",
83
+ failed_assertions: [...gateFailures, ...warnFailures],
84
+ summary: `BLOCKED: ${gateFailures.length} gate-level failure(s) [${ids}]. ` +
85
+ `${warnFailures.length} warn-level failures also present.`
86
+ };
87
+ }
88
+ // Only warn-level failures → continue
89
+ return {
90
+ action: "continue",
91
+ failed_assertions: warnFailures,
92
+ summary: `CONTINUE: ${warnFailures.length} warn-level failure(s) logged, no blocking issues.`
93
+ };
94
+ }
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "prism-mcp-server",
3
- "version": "7.2.0",
3
+ "version": "7.3.1",
4
4
  "mcpName": "io.github.dcostenco/prism-mcp",
5
- "description": "The Mind Palace for AI Agents — persistent memory (SQLite/Supabase), behavioral learning & IDE rules sync, multimodal VLM image captioning, pluggable LLM providers (OpenAI/Anthropic/Gemini/Ollama), OpenTelemetry distributed tracing, GDPR export, multi-agent Hivemind sync, time travel, visual Mind Palace dashboard. Zero-config local mode.",
5
+ "description": "The Mind Palace for AI Agents — fail-closed Dark Factory autonomous pipelines (3-gate parse→type→scope validation), persistent memory (SQLite/Supabase), ACT-R cognitive retrieval, behavioral learning & IDE rules sync, multi-agent Hivemind, time travel, visual dashboard. Zero-config local mode.",
6
6
  "module": "index.ts",
7
7
  "type": "module",
8
8
  "main": "dist/server.js",
@@ -68,7 +68,11 @@
68
68
  "dashboard",
69
69
  "actr",
70
70
  "cognitive-memory",
71
- "activation-memory"
71
+ "activation-memory",
72
+ "dark-factory",
73
+ "autonomous-pipeline",
74
+ "fail-closed",
75
+ "path-traversal-prevention"
72
76
  ],
73
77
  "homepage": "https://github.com/dcostenco/prism-mcp",
74
78
  "repository": {
@@ -90,7 +94,6 @@
90
94
  },
91
95
  "dependencies": {
92
96
  "@anthropic-ai/sdk": "^0.80.0",
93
- "@tavily/core": "^0.6.0",
94
97
  "@google-cloud/discoveryengine": "^2.5.3",
95
98
  "@google/generative-ai": "^0.24.1",
96
99
  "@libsql/client": "^0.17.2",
@@ -102,6 +105,7 @@
102
105
  "@opentelemetry/sdk-trace-node": "^2.6.1",
103
106
  "@opentelemetry/semantic-conventions": "^1.40.0",
104
107
  "@supabase/supabase-js": "^2.99.3",
108
+ "@tavily/core": "^0.6.0",
105
109
  "cheerio": "^1.2.0",
106
110
  "dotenv": "^16.5.0",
107
111
  "fflate": "^0.8.2",
@@ -110,6 +114,7 @@
110
114
  "p-limit": "^7.3.0",
111
115
  "quickjs-emscripten": "^0.32.0",
112
116
  "stream-json": "^2.0.0",
113
- "turndown": "^7.2.2"
117
+ "turndown": "^7.2.2",
118
+ "zod": "^4.3.6"
114
119
  }
115
120
  }