prism-mcp-server 20.13.0 → 20.13.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/dist/storage/synalux.js
CHANGED
|
@@ -29,6 +29,8 @@
|
|
|
29
29
|
* - getHistory → POST /api/v1/prism/memory action=memory_history
|
|
30
30
|
* - patchLedger → POST /api/v1/prism/memory action=save_embedding
|
|
31
31
|
* - getEntriesMissingEmbeddings → POST /api/v1/prism/memory action=list_missing_embeddings
|
|
32
|
+
* - listProjects → POST /api/v1/prism/memory action=list_projects
|
|
33
|
+
* - exportLedger → POST /api/v1/prism/memory action=export_memory (paginated)
|
|
32
34
|
*
|
|
33
35
|
* Methods still falling through to SupabaseStorage (Phase 3 Tier B+):
|
|
34
36
|
* save_experience direct entrypoint, compactLedger, image ops,
|
|
@@ -449,6 +451,58 @@ export class SynaluxStorage extends SupabaseStorage {
|
|
|
449
451
|
const entries = Array.isArray(result.entries) ? result.entries : [];
|
|
450
452
|
return entries;
|
|
451
453
|
}
|
|
454
|
+
// ─── Project inventory + export ──────────────────────────────
|
|
455
|
+
// Both portal actions shipped in Phase 3 but the client was never
|
|
456
|
+
// wired: listProjects and the export path fell through to
|
|
457
|
+
// SupabaseStorage and threw "Supabase not configured" on every
|
|
458
|
+
// paid-tier install (2026-08-18 audit).
|
|
459
|
+
async listProjects() {
|
|
460
|
+
const result = await this.portalPost("/api/v1/prism/memory", {
|
|
461
|
+
action: "list_projects",
|
|
462
|
+
});
|
|
463
|
+
// Strict on drift: a 200 without a projects ARRAY is a contract change,
|
|
464
|
+
// not "no projects" — the portal returns projects:[] for genuinely none.
|
|
465
|
+
// Coercing drift to [] would make callers report empty inventories while
|
|
466
|
+
// claiming success. (R1 adversarial review 2026-08-18.)
|
|
467
|
+
if (!Array.isArray(result.projects)) {
|
|
468
|
+
throw new Error("[SynaluxStorage] list_projects: portal response missing projects[] — contract drift");
|
|
469
|
+
}
|
|
470
|
+
return result.projects
|
|
471
|
+
.map((p) => (typeof p === "string" ? p : p?.project))
|
|
472
|
+
.filter((name) => typeof name === "string" && name.length > 0);
|
|
473
|
+
}
|
|
474
|
+
async exportLedger(project) {
|
|
475
|
+
// action=export_memory is paginated (EXPORT_PAGE_SIZE=1000). Follow
|
|
476
|
+
// next_offset until has_more is false, capped at 10 pages to match the
|
|
477
|
+
// local path's 10k-row OOM guard.
|
|
478
|
+
//
|
|
479
|
+
// Strict on drift (R1 adversarial review 2026-08-18): this feeds a
|
|
480
|
+
// BACKUP. A 200 missing ledger[] or page{} must throw, not degrade —
|
|
481
|
+
// coercing either would write an empty or silently-truncated export
|
|
482
|
+
// file that reports ✅ success, which is worse than any error.
|
|
483
|
+
const rows = [];
|
|
484
|
+
let offset = 0;
|
|
485
|
+
for (let page = 0; page < 10; page++) {
|
|
486
|
+
const result = await this.portalPost("/api/v1/prism/memory", {
|
|
487
|
+
action: "export_memory",
|
|
488
|
+
project,
|
|
489
|
+
offset,
|
|
490
|
+
limit: 1000,
|
|
491
|
+
});
|
|
492
|
+
if (!Array.isArray(result.ledger)) {
|
|
493
|
+
throw new Error("[SynaluxStorage] export_memory: portal response missing ledger[] — contract drift");
|
|
494
|
+
}
|
|
495
|
+
const pageInfo = result.page;
|
|
496
|
+
if (typeof pageInfo?.has_more !== "boolean") {
|
|
497
|
+
throw new Error("[SynaluxStorage] export_memory: portal response missing page.has_more — refusing a possibly-truncated export");
|
|
498
|
+
}
|
|
499
|
+
rows.push(...result.ledger);
|
|
500
|
+
if (!pageInfo.has_more || typeof pageInfo.next_offset !== "number")
|
|
501
|
+
break;
|
|
502
|
+
offset = pageInfo.next_offset;
|
|
503
|
+
}
|
|
504
|
+
return rows;
|
|
505
|
+
}
|
|
452
506
|
// ─── Time Travel ─────────────────────────────────────────────
|
|
453
507
|
// Phase 3 Tier B: route memory_history through portal instead of
|
|
454
508
|
// falling through to SupabaseStorage (which requires a direct
|
|
@@ -2719,12 +2719,24 @@ export async function sessionExportMemoryHandler(args) {
|
|
|
2719
2719
|
debugLog(`[session_export_memory] Exporting project "${project}" as ${format}`);
|
|
2720
2720
|
// Fetch handoff (live context)
|
|
2721
2721
|
const ctx = await storage.loadContext(project, "deep", PRISM_USER_ID);
|
|
2722
|
-
// Fetch full ledger (all non-deleted entries, capped at 10k as OOM guard)
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2722
|
+
// Fetch full ledger (all non-deleted entries, capped at 10k as OOM guard).
|
|
2723
|
+
// Portal-backed installs export via action=export_memory (exportLedger);
|
|
2724
|
+
// local/direct backends keep the getLedgerEntries assembly. Without this
|
|
2725
|
+
// branch, paid thin-client installs threw "Supabase not configured" here.
|
|
2726
|
+
const ledger = (typeof storage.exportLedger === "function"
|
|
2727
|
+
? await storage.exportLedger(project)
|
|
2728
|
+
: await storage.getLedgerEntries({
|
|
2729
|
+
project: `eq.${project}`,
|
|
2730
|
+
// R1 adversarial review 2026-08-18: without this filter the
|
|
2731
|
+
// direct/local paths exported TOMBSTONED rows — content the user
|
|
2732
|
+
// had asked session_forget_memory to erase shipped in every
|
|
2733
|
+
// backup (GDPR Art. 17 leak; the portal export always excluded
|
|
2734
|
+
// them, so the two paths also disagreed). Both PostgREST and the
|
|
2735
|
+
// sqlite filter parser support is.null.
|
|
2736
|
+
deleted_at: "is.null",
|
|
2737
|
+
order: "created_at.asc",
|
|
2738
|
+
limit: "10000",
|
|
2739
|
+
}));
|
|
2728
2740
|
// Strip raw embedding vectors from the export (large binary / not human-useful)
|
|
2729
2741
|
// embedding: raw float32 JSON array (~12KB/entry)
|
|
2730
2742
|
// embedding_compressed: TurboQuant binary blob (~400B/entry, base64 in JSON)
|
|
@@ -1090,7 +1090,9 @@ export async function runInfer(args, deps) {
|
|
|
1090
1090
|
// describe the request rather than a specific backend.
|
|
1091
1091
|
const maxTokens = cloudMaxTokens;
|
|
1092
1092
|
// Cloud fallback only for paid plans
|
|
1093
|
-
const
|
|
1093
|
+
// let, not const: the reserved-image branch pins this off mid-call so no
|
|
1094
|
+
// later escalation path can carry even the prompt text off-device.
|
|
1095
|
+
let allowCloud = args.cloud_fallback === true && ent.features.cloud_fallback;
|
|
1094
1096
|
// Verification only for paid plans (free users skip L3 grounding)
|
|
1095
1097
|
const canVerify = ent.features.grounding_verifier;
|
|
1096
1098
|
// The portal entitlement is authoritative. A paid plan alone must not
|
|
@@ -1106,7 +1108,10 @@ export async function runInfer(args, deps) {
|
|
|
1106
1108
|
const verificationGatedArgs = canVerify
|
|
1107
1109
|
? args
|
|
1108
1110
|
: { ...args, verify: false, evidence: undefined };
|
|
1109
|
-
const
|
|
1111
|
+
// let, not const: re-pinned below once images are resolved — an image
|
|
1112
|
+
// request must not leave the device through ANY channel, including the
|
|
1113
|
+
// paid ones this gate would otherwise leave enabled.
|
|
1114
|
+
let gatedArgs = canUsePrivateRouteGuard
|
|
1110
1115
|
? verificationGatedArgs
|
|
1111
1116
|
: { ...verificationGatedArgs, route_guard: "local" };
|
|
1112
1117
|
// §5.2 failure contract: under escalation:"report", safety refusals return
|
|
@@ -1190,6 +1195,21 @@ export async function runInfer(args, deps) {
|
|
|
1190
1195
|
// fail open: the original images are still in resolvedImages
|
|
1191
1196
|
}
|
|
1192
1197
|
}
|
|
1198
|
+
if (resolvedImages?.length) {
|
|
1199
|
+
// Adversarial review R1 (2026-08-18): serving image requests locally is
|
|
1200
|
+
// not enough — two paid side doors still carried content DERIVED from
|
|
1201
|
+
// the pixels off-device. The Synalux route guard POSTs the prompt and
|
|
1202
|
+
// the draft; the Synalux grounding verifier POSTs the draft and the
|
|
1203
|
+
// evidence. A draft written by a model that just read a clinical
|
|
1204
|
+
// screenshot can quote it. Pin both local for EVERY image request, not
|
|
1205
|
+
// just reserved-flagged ones: the content screen is FN-porous by
|
|
1206
|
+
// design, so a clean screen is not a leak clearance. Text-only
|
|
1207
|
+
// requests keep both features.
|
|
1208
|
+
const wouldVerify = gatedArgs.verify ?? ((gatedArgs.evidence?.length ?? 0) > 0);
|
|
1209
|
+
if (wouldVerify)
|
|
1210
|
+
attempts.push({ tier: "verifier", reason: "verifier_skipped_images_stay_local" });
|
|
1211
|
+
gatedArgs = { ...gatedArgs, route_guard: "local", verify: false };
|
|
1212
|
+
}
|
|
1193
1213
|
if (installed && !layer1RecursionGuard) {
|
|
1194
1214
|
const l1fn = deps.callLayer1 ?? defaultCallLayer1;
|
|
1195
1215
|
const l1Model = resolveOllamaName("prism-coder:4b", installed);
|
|
@@ -1222,7 +1242,26 @@ export async function runInfer(args, deps) {
|
|
|
1222
1242
|
// Null when the deterministic floor did not fire — the verdict then came
|
|
1223
1243
|
// from the semantic classifier, which has no per-rule attribution.
|
|
1224
1244
|
const reservedCat = reservedCategory(args.prompt);
|
|
1225
|
-
if (l1 === "OBVIOUS_RESERVED" || l1 === "UNCERTAIN")
|
|
1245
|
+
if ((l1 === "OBVIOUS_RESERVED" || l1 === "UNCERTAIN")
|
|
1246
|
+
&& (resolvedImages?.length ?? 0) > 0) {
|
|
1247
|
+
// Clinical images are PROCESSED, never refused (ruling 2026-08-18:
|
|
1248
|
+
// the standard BCBA role works from scanned assessments and
|
|
1249
|
+
// screenshots — locally, or via the sanctioned prism cloud once it
|
|
1250
|
+
// has an image channel). Local inference is exactly where that
|
|
1251
|
+
// content is SAFE: nothing leaves the device. Refusing here broke
|
|
1252
|
+
// screenshot verification and assessment work for the clinical
|
|
1253
|
+
// enterprise tiers that need it most, while the actual no-leak
|
|
1254
|
+
// property — images never reach unsanctioned cloud — is enforced
|
|
1255
|
+
// architecturally either way. Serve locally with cloud pinned off
|
|
1256
|
+
// for the rest of the call. No text-policy bypass results: an
|
|
1257
|
+
// image-carrying request is STRICTER than the same words without
|
|
1258
|
+
// one (local-only), so attaching an image can only reduce
|
|
1259
|
+
// exposure. The verdict stays in attempts for the audit trail.
|
|
1260
|
+
debugLog(`[prism_infer] Layer 1 verdict=${l1} with images — serving locally, cloud disabled for this call`);
|
|
1261
|
+
attempts.push({ tier: "layer1", reason: `layer1_${l1.toLowerCase()}_image_local_only` });
|
|
1262
|
+
allowCloud = false;
|
|
1263
|
+
}
|
|
1264
|
+
else if (l1 === "OBVIOUS_RESERVED" || l1 === "UNCERTAIN") {
|
|
1226
1265
|
debugLog(`[prism_infer] Layer 1 verdict=${l1} — reserved content detected`);
|
|
1227
1266
|
attempts.push({ tier: "layer1", reason: `layer1_${l1.toLowerCase()}` });
|
|
1228
1267
|
// Images never leave the device, and callCloud has no image channel
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "prism-mcp-server",
|
|
3
|
-
"version": "20.13.
|
|
3
|
+
"version": "20.13.1",
|
|
4
4
|
"mcpName": "io.github.dcostenco/prism-coder",
|
|
5
5
|
"description": "Persistent session memory for AI coding agents that never leaves your machine — including the on-device model that reasons over it. Restores your prior decisions, open TODOs, and changed files across sessions; adds associative recall of related past work, semantic drift detection, and local inference. Local-first by default. Works with Claude Code, Cursor, and Codex.",
|
|
6
6
|
"module": "index.ts",
|