prism-mcp-server 20.0.1 → 20.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -8
- package/dist/boundaries/__tests__/boundaries.test.js +18 -30
- package/dist/boundaries/boundaries.js +11 -36
- package/dist/server.js +1 -1
- package/dist/session/__tests__/sessionContext.test.js +11 -10
- package/dist/storage/sqlite.js +13 -0
- package/dist/tools/__tests__/layer1Integration.test.js +11 -3
- package/dist/tools/__tests__/ledgerHandlers.test.js +24 -2
- package/dist/tools/ledgerHandlers.js +7 -13
- package/dist/tools/prismInferHandler.js +24 -22
- package/dist/utils/entitlements.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -382,9 +382,9 @@ All on-device models are free to run locally via Ollama on every tier. A subscri
|
|
|
382
382
|
|---|---|---|---|---|
|
|
383
383
|
| Seats | 1 | 1 | up to 5 | up to 25 |
|
|
384
384
|
| Local model ceiling | up to 4b | up to 9b | up to 27b | up to 27b |
|
|
385
|
-
|
|
|
386
|
-
| Cloud Coder (Web IDE) | -- |
|
|
387
|
-
| Cloud search | -- |
|
|
385
|
+
| Cloud inference | -- | ✅ | ✅ | ✅ (priority) |
|
|
386
|
+
| Cloud Coder (Web IDE) | -- | ✅ | ✅ | ✅ (priority) |
|
|
387
|
+
| Cloud search | -- | ✅ | ✅ | ✅ |
|
|
388
388
|
| Max output tokens | 512 | 1,024 | 2,048 | 4,096 |
|
|
389
389
|
| Cloud fallback | -- | Claude Opus 4.7 | Claude Opus 4.7 | Priority + Opus 4.7 |
|
|
390
390
|
| Grounding verifier (fact-check AI output) | -- | ✅ | ✅ | ✅ |
|
|
@@ -644,11 +644,32 @@ It reads `~/.prism-mcp/data.db` and POSTs entries to the portal. Ledger entries
|
|
|
644
644
|
|
|
645
645
|
## License & Tiers
|
|
646
646
|
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
647
|
+
**This repository (the Prism MCP client)** is licensed under [Apache-2.0](./LICENSE).
|
|
648
|
+
|
|
649
|
+
### Free (no account)
|
|
650
|
+
|
|
651
|
+
| Feature | Details |
|
|
652
|
+
|---------|---------|
|
|
653
|
+
| Local inference | Ollama via `prism_infer`, capped at the 4B model tier |
|
|
654
|
+
| Session memory | Persistent sessions, handoffs, ledger — all local SQLite |
|
|
655
|
+
| Knowledge search | Semantic search across session history |
|
|
656
|
+
| Skills | All skills available locally (run `sync-skills.sh` to populate) |
|
|
657
|
+
| Drift detection | Server-side GATE 5 reminders |
|
|
658
|
+
|
|
659
|
+
### Paid (Synalux subscription)
|
|
660
|
+
|
|
661
|
+
Everything in Free, plus:
|
|
662
|
+
|
|
663
|
+
| Feature | Details |
|
|
664
|
+
|---------|---------|
|
|
665
|
+
| Model ceiling | Up to 27B locally + cloud cascade (9B → 27B → Claude) when local is unavailable |
|
|
666
|
+
| Skill routing | Portal resolves which skills to load based on your project and prompt |
|
|
667
|
+
| Cross-device memory | Supabase cloud sync — sessions survive across machines |
|
|
668
|
+
| Grounding verifier | L3 NLI verification on model outputs |
|
|
669
|
+
| Team features | Multi-agent Hivemind, workspace collaboration |
|
|
670
|
+
|
|
671
|
+
The paid tier adds **intelligent routing** — the Synalux portal determines which skills are relevant to your current project and prompt, so your agent gets domain expertise (stripe patterns, training protocols, clinical standards) instead of loading everything. Free users with the repo can run `sync-skills.sh` to populate all skills locally; paid routing adds project-aware and prompt-aware selection.
|
|
672
|
+
|
|
652
673
|
- Contributions require signing the [CLA](./CLA.md).
|
|
653
674
|
- "Prism" and "Synalux" are trade names of Synalux LLC; the Apache license does
|
|
654
675
|
not grant trademark rights (see §6 of the license).
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* boundaries.ts unit tests
|
|
3
3
|
*
|
|
4
|
-
* Verifies structural invariants of the
|
|
4
|
+
* Verifies structural invariants of the safety declaration:
|
|
5
5
|
* - BOUNDARIES_VERSION is present and non-empty
|
|
6
|
-
* - BOUNDARIES_TEXT covers
|
|
7
|
-
* -
|
|
8
|
-
* remove them without also bumping the version and updating tests)
|
|
6
|
+
* - BOUNDARIES_TEXT covers safety-critical concepts
|
|
7
|
+
* - Architecture/routing docs moved to server instructions (not per-call)
|
|
9
8
|
*/
|
|
10
9
|
import { describe, it, expect } from "vitest";
|
|
11
10
|
import { BOUNDARIES_VERSION, BOUNDARIES_TEXT } from "../../boundaries/boundaries.js";
|
|
@@ -16,43 +15,32 @@ describe("BOUNDARIES_VERSION", () => {
|
|
|
16
15
|
});
|
|
17
16
|
});
|
|
18
17
|
describe("BOUNDARIES_TEXT structure", () => {
|
|
19
|
-
it("is a non-empty string", () => {
|
|
18
|
+
it("is a non-empty string under 500 chars (compressed from ~2000)", () => {
|
|
20
19
|
expect(typeof BOUNDARIES_TEXT).toBe("string");
|
|
21
|
-
expect(BOUNDARIES_TEXT.length).toBeGreaterThan(
|
|
20
|
+
expect(BOUNDARIES_TEXT.length).toBeGreaterThan(50);
|
|
21
|
+
expect(BOUNDARIES_TEXT.length).toBeLessThan(600);
|
|
22
22
|
});
|
|
23
23
|
it("has no leading or trailing whitespace (trim() was applied)", () => {
|
|
24
24
|
expect(BOUNDARIES_TEXT).toBe(BOUNDARIES_TEXT.trim());
|
|
25
25
|
});
|
|
26
|
-
it("
|
|
27
|
-
expect(BOUNDARIES_TEXT).toMatch(/
|
|
28
|
-
});
|
|
29
|
-
it("contains a BCBA clinical standards section", () => {
|
|
30
|
-
expect(BOUNDARIES_TEXT).toMatch(/bcba/i);
|
|
31
|
-
});
|
|
32
|
-
it("contains a correctness gates section", () => {
|
|
33
|
-
expect(BOUNDARIES_TEXT).toMatch(/correctness gates/i);
|
|
34
|
-
});
|
|
35
|
-
it("contains an inference routing section", () => {
|
|
36
|
-
expect(BOUNDARIES_TEXT).toMatch(/inference routing/i);
|
|
26
|
+
it("references BCBA reserved categories", () => {
|
|
27
|
+
expect(BOUNDARIES_TEXT).toMatch(/BCBA reserved/i);
|
|
37
28
|
});
|
|
38
|
-
it("
|
|
39
|
-
expect(BOUNDARIES_TEXT).toMatch(/
|
|
40
|
-
});
|
|
41
|
-
it("asserts that enforcement is server-side (not instruction-based)", () => {
|
|
42
|
-
expect(BOUNDARIES_TEXT).toMatch(/server.*enforc|enforc.*server/i);
|
|
43
|
-
});
|
|
44
|
-
it("references the fail-closed rule for reserved + no-cloud", () => {
|
|
45
|
-
expect(BOUNDARIES_TEXT).toMatch(/fail.?closed|refused/i);
|
|
29
|
+
it("references fail-closed refusal for reserved content", () => {
|
|
30
|
+
expect(BOUNDARIES_TEXT).toMatch(/refused/i);
|
|
46
31
|
});
|
|
47
32
|
it("states AAC access is never restricted as a consequence", () => {
|
|
48
33
|
expect(BOUNDARIES_TEXT).toMatch(/AAC access is never restricted/i);
|
|
49
34
|
});
|
|
50
|
-
it("references restraint as
|
|
35
|
+
it("references restraint as reserved", () => {
|
|
51
36
|
expect(BOUNDARIES_TEXT).toMatch(/restraint/i);
|
|
52
|
-
expect(BOUNDARIES_TEXT).toMatch(/RESERVED/i);
|
|
53
37
|
});
|
|
54
|
-
it("
|
|
55
|
-
expect(BOUNDARIES_TEXT).
|
|
56
|
-
|
|
38
|
+
it("references crisis/self-harm interception", () => {
|
|
39
|
+
expect(BOUNDARIES_TEXT).toMatch(/crisis|self-harm/i);
|
|
40
|
+
});
|
|
41
|
+
it("does NOT contain architecture/routing docs (those moved to server instructions)", () => {
|
|
42
|
+
expect(BOUNDARIES_TEXT).not.toContain("session_save_ledger");
|
|
43
|
+
expect(BOUNDARIES_TEXT).not.toContain("inference routing");
|
|
44
|
+
expect(BOUNDARIES_TEXT).not.toMatch(/host note/i);
|
|
57
45
|
});
|
|
58
46
|
});
|
|
@@ -1,48 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* src/boundaries/boundaries.ts
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Safety declaration delivered in every session_load_context result.
|
|
5
|
+
* Architecture/routing documentation lives in the MCP server description
|
|
6
|
+
* (loaded once at connection, not per-call).
|
|
5
7
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
8
|
+
* The safety gates (Layer 1 classifier, cloud routing, fail-closed refusal)
|
|
9
|
+
* are enforced in code — this text is defense-in-depth so cooperative hosts
|
|
10
|
+
* avoid drafting reserved content that would be refused at the gate.
|
|
9
11
|
*
|
|
10
12
|
* Update BOUNDARIES_VERSION any time the text changes so session drift
|
|
11
13
|
* detection can flag stale sessions.
|
|
12
14
|
*/
|
|
13
|
-
export const BOUNDARIES_VERSION = "
|
|
15
|
+
export const BOUNDARIES_VERSION = "2";
|
|
14
16
|
export const BOUNDARIES_TEXT = `
|
|
15
|
-
|
|
17
|
+
Safety boundaries are enforced in code — shown so hosts avoid wasted round-trips.
|
|
16
18
|
|
|
17
|
-
|
|
18
|
-
-
|
|
19
|
-
-
|
|
20
|
-
to cloud or refuse; they NEVER generate locally. Fail-closed: if cloud is unavailable
|
|
21
|
-
and the prompt is reserved, the request is refused — never downgraded to local.
|
|
22
|
-
- Dangerous output (restraint instructions, overdose methods, self-harm guidance)
|
|
23
|
-
is blocked regardless of which host requested it.
|
|
24
|
-
|
|
25
|
-
### 2. BCBA clinical standards
|
|
26
|
-
- Apply ABA principles grounded in the current BACB Ethics Code and Task List (5th Ed).
|
|
27
|
-
- Use evidence-based interventions: FCT, DRA, DRO, NCR, antecedent modifications.
|
|
28
|
-
- Least restrictive, dignity-preserving, trauma-informed procedures always.
|
|
19
|
+
- **Crisis/self-harm** inputs are intercepted before reaching any model.
|
|
20
|
+
- **BCBA reserved categories** (restraint, seclusion, physical management, dosing) route to cloud or are refused. They are NEVER generated locally. If cloud is unavailable and the prompt is reserved, the request is refused — never downgraded to local.
|
|
21
|
+
- **Dangerous output** (restraint instructions, overdose methods, self-harm guidance) is blocked regardless of host.
|
|
29
22
|
- AAC access is never restricted as a consequence.
|
|
30
|
-
- Physical management / restraint / seclusion are RESERVED — cloud only, with audit.
|
|
31
|
-
|
|
32
|
-
### 3. Correctness gates (project-scoped write tools)
|
|
33
|
-
- session_save_ledger and session_save_handoff require a loaded project context
|
|
34
|
-
(conversation_id that called session_load_context successfully).
|
|
35
|
-
- This prevents a non-Claude host from writing state it hasn't confirmed.
|
|
36
|
-
|
|
37
|
-
### 4. Inference routing
|
|
38
|
-
- Local inference (Ollama) runs ONLY for OBVIOUS_NOT_RESERVED prompts (Layer 1 verdict).
|
|
39
|
-
- RESERVED / UNCERTAIN / classifier errors escalate to cloud. Never downgrade reserved
|
|
40
|
-
prompts to local — that is exactly what Layer 1 flagged.
|
|
41
|
-
- Cloud must never be reached directly by a host; all cloud inference routes through
|
|
42
|
-
the Synalux portal for billing, tier-gating, and HIPAA audit.
|
|
43
|
-
|
|
44
|
-
### 5. Host note
|
|
45
|
-
These boundaries are enforced by the server. They apply identically whether the host
|
|
46
|
-
is Claude Code, Gemini, an autonomous script, or a cron job. A host that does not read
|
|
47
|
-
this text still cannot bypass the enforcement — it is in code, not instructions.
|
|
48
23
|
`.trim();
|
package/dist/server.js
CHANGED
|
@@ -358,7 +358,7 @@ export function createServer() {
|
|
|
358
358
|
},
|
|
359
359
|
// Supplementary signal — not all clients support this field.
|
|
360
360
|
// Primary mechanism is the dynamic tool description above.
|
|
361
|
-
instructions: `Prism MCP — The Mind Palace for AI Agents. This server provides persistent session memory, knowledge search, and context management tools. Use session_load_context to recover previous work state, session_save_ledger to log completed work, and session_save_handoff to preserve state for the next session.`,
|
|
361
|
+
instructions: `Prism MCP — The Mind Palace for AI Agents. This server provides persistent session memory, knowledge search, and context management tools. Use session_load_context to recover previous work state, session_save_ledger to log completed work, and session_save_handoff to preserve state for the next session.\n\nArchitecture: session_save_ledger and session_save_handoff require a loaded project context (conversation_id that called session_load_context). Local inference (prism_infer) routes through Layer 1 classification — RESERVED prompts escalate to cloud or are refused, never served locally. All cloud inference routes through the Synalux portal for billing, tier-gating, and audit.`,
|
|
362
362
|
});
|
|
363
363
|
// ── Handler: Initialize ──
|
|
364
364
|
// NOTE: The SDK's built-in _oninitialize() handles the Initialize request.
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* an exported reset function.
|
|
10
10
|
*/
|
|
11
11
|
import { describe, it, expect, beforeEach, vi } from "vitest";
|
|
12
|
+
import { BOUNDARIES_VERSION } from "../../boundaries/boundaries.js";
|
|
12
13
|
// Reset module registry before each test so the in-memory Map starts empty.
|
|
13
14
|
let markContextLoaded;
|
|
14
15
|
let requireContextLoaded;
|
|
@@ -56,7 +57,7 @@ describe("requireContextLoaded — fail-closed defaults", () => {
|
|
|
56
57
|
});
|
|
57
58
|
describe("markContextLoaded → requireContextLoaded lifecycle", () => {
|
|
58
59
|
it("returns null (pass) after markContextLoaded is called", () => {
|
|
59
|
-
markContextLoaded("conv-abc", "project-x",
|
|
60
|
+
markContextLoaded("conv-abc", "project-x", BOUNDARIES_VERSION);
|
|
60
61
|
expect(requireContextLoaded("conv-abc")).toBeNull();
|
|
61
62
|
});
|
|
62
63
|
it("records project and boundariesVersion on the session", () => {
|
|
@@ -68,30 +69,30 @@ describe("markContextLoaded → requireContextLoaded lifecycle", () => {
|
|
|
68
69
|
expect(state.contextLoaded).toBe(true);
|
|
69
70
|
});
|
|
70
71
|
it("is idempotent — calling twice does not break state", () => {
|
|
71
|
-
// Use the actual BOUNDARIES_VERSION
|
|
72
|
-
markContextLoaded("conv-idem", "proj",
|
|
73
|
-
markContextLoaded("conv-idem", "proj-updated",
|
|
72
|
+
// Use the actual BOUNDARIES_VERSION so no drift warning fires.
|
|
73
|
+
markContextLoaded("conv-idem", "proj", BOUNDARIES_VERSION);
|
|
74
|
+
markContextLoaded("conv-idem", "proj-updated", BOUNDARIES_VERSION);
|
|
74
75
|
const state = getSessionState("conv-idem");
|
|
75
76
|
expect(state.project).toBe("proj-updated");
|
|
76
|
-
expect(state.boundariesVersion).toBe(
|
|
77
|
+
expect(state.boundariesVersion).toBe(BOUNDARIES_VERSION);
|
|
77
78
|
expect(requireContextLoaded("conv-idem")).toBeNull();
|
|
78
79
|
});
|
|
79
80
|
it("isolates sessions — loading one does not unblock another", () => {
|
|
80
|
-
markContextLoaded("conv-A", "proj",
|
|
81
|
+
markContextLoaded("conv-A", "proj", BOUNDARIES_VERSION);
|
|
81
82
|
expect(requireContextLoaded("conv-A")).toBeNull();
|
|
82
83
|
expect(requireContextLoaded("conv-B")).not.toBeNull();
|
|
83
84
|
});
|
|
84
85
|
});
|
|
85
86
|
describe("noteInferenceForSession", () => {
|
|
86
87
|
it("increments inferenceCalls on every call", () => {
|
|
87
|
-
markContextLoaded("conv-inf", "proj",
|
|
88
|
+
markContextLoaded("conv-inf", "proj", BOUNDARIES_VERSION);
|
|
88
89
|
noteInferenceForSession("conv-inf", { backend: "local", usedCloud: false });
|
|
89
90
|
noteInferenceForSession("conv-inf", { backend: "local", usedCloud: false });
|
|
90
91
|
const state = getSessionState("conv-inf");
|
|
91
92
|
expect(state.inferenceCalls).toBe(2);
|
|
92
93
|
});
|
|
93
94
|
it("increments usedCloudCalls only for cloud calls", () => {
|
|
94
|
-
markContextLoaded("conv-cloud", "proj",
|
|
95
|
+
markContextLoaded("conv-cloud", "proj", BOUNDARIES_VERSION);
|
|
95
96
|
noteInferenceForSession("conv-cloud", { backend: "cloud", usedCloud: true });
|
|
96
97
|
noteInferenceForSession("conv-cloud", { backend: "local", usedCloud: false });
|
|
97
98
|
const state = getSessionState("conv-cloud");
|
|
@@ -112,7 +113,7 @@ describe("getSessionState", () => {
|
|
|
112
113
|
expect(getSessionState("does-not-exist")).toBeNull();
|
|
113
114
|
});
|
|
114
115
|
it("returns the current state object for a known session", () => {
|
|
115
|
-
markContextLoaded("conv-get", "proj",
|
|
116
|
+
markContextLoaded("conv-get", "proj", BOUNDARIES_VERSION);
|
|
116
117
|
const state = getSessionState("conv-get");
|
|
117
118
|
expect(state).not.toBeNull();
|
|
118
119
|
expect(state.contextLoaded).toBe(true);
|
|
@@ -120,7 +121,7 @@ describe("getSessionState", () => {
|
|
|
120
121
|
});
|
|
121
122
|
describe("lastSeen update", () => {
|
|
122
123
|
it("updates lastSeen on every requireContextLoaded call", async () => {
|
|
123
|
-
markContextLoaded("conv-ts", "proj",
|
|
124
|
+
markContextLoaded("conv-ts", "proj", BOUNDARIES_VERSION);
|
|
124
125
|
const before = getSessionState("conv-ts").lastSeen;
|
|
125
126
|
// Advance time by mocking Date.now via vi.useFakeTimers
|
|
126
127
|
vi.useFakeTimers();
|
package/dist/storage/sqlite.js
CHANGED
|
@@ -909,6 +909,19 @@ export class SqliteStorage {
|
|
|
909
909
|
async saveLedger(entry) {
|
|
910
910
|
const id = entry.id || randomUUID();
|
|
911
911
|
const now = new Date().toISOString();
|
|
912
|
+
// Dedup: skip if an identical (project, conversation_id, summary) exists within 5 minutes
|
|
913
|
+
if (entry.project && entry.conversation_id && entry.summary) {
|
|
914
|
+
const dup = await this.db.execute({
|
|
915
|
+
sql: `SELECT id FROM session_ledger
|
|
916
|
+
WHERE project = ? AND conversation_id = ? AND summary = ?
|
|
917
|
+
AND created_at > datetime('now', '-5 minutes')
|
|
918
|
+
LIMIT 1`,
|
|
919
|
+
args: [entry.project, entry.conversation_id, entry.summary],
|
|
920
|
+
});
|
|
921
|
+
if (dup.rows.length > 0) {
|
|
922
|
+
return { id: dup.rows[0].id, deduplicated: true };
|
|
923
|
+
}
|
|
924
|
+
}
|
|
912
925
|
await this.db.execute({
|
|
913
926
|
sql: `INSERT INTO session_ledger
|
|
914
927
|
(id, project, conversation_id, user_id, role, summary, todos, files_changed,
|
|
@@ -75,6 +75,7 @@ function makeBaseDeps(overrides = {}) {
|
|
|
75
75
|
listLoaded: async () => new Set(),
|
|
76
76
|
callLocal: async () => ({ ok: true, text: "local response", doneReason: "stop" }),
|
|
77
77
|
callCloud: async () => ({ ok: true, output: "cloud response", backend: "synalux" }),
|
|
78
|
+
callLayer1: async () => "OBVIOUS_NOT_RESERVED",
|
|
78
79
|
ollamaUrl: "http://localhost:11434",
|
|
79
80
|
entitlements: {
|
|
80
81
|
plan: "pro",
|
|
@@ -138,11 +139,18 @@ describe("Layer 1 handler integration", () => {
|
|
|
138
139
|
expect(callLocal).toHaveBeenCalled();
|
|
139
140
|
expect(result.used_cloud).toBe(false);
|
|
140
141
|
});
|
|
141
|
-
it("cloud_fallback=false →
|
|
142
|
+
it("cloud_fallback=false + RESERVED → refuses (fail-closed, no local fallback)", async () => {
|
|
142
143
|
const callLayer1Mock = vi.fn().mockResolvedValue("OBVIOUS_RESERVED");
|
|
143
144
|
const callLocal = vi.fn().mockResolvedValue({ ok: true, text: "local response", doneReason: "stop" });
|
|
144
|
-
|
|
145
|
-
expect(callLayer1Mock).
|
|
145
|
+
await expect(runInfer({ prompt: "write something", mode: "code", cloud_fallback: false, max_tokens: 512 }, makeBaseDeps({ callLayer1: callLayer1Mock, callLocal }))).rejects.toThrow(/reserved content refused/i);
|
|
146
|
+
expect(callLayer1Mock).toHaveBeenCalled();
|
|
147
|
+
expect(callLocal).not.toHaveBeenCalled();
|
|
148
|
+
});
|
|
149
|
+
it("cloud_fallback=false + NOT_RESERVED → proceeds to local", async () => {
|
|
150
|
+
const callLayer1Mock = vi.fn().mockResolvedValue("OBVIOUS_NOT_RESERVED");
|
|
151
|
+
const callLocal = vi.fn().mockResolvedValue({ ok: true, text: "local response", doneReason: "stop" });
|
|
152
|
+
const result = await runInfer({ prompt: "what is 2+2?", mode: "code", cloud_fallback: false, max_tokens: 512 }, makeBaseDeps({ callLayer1: callLayer1Mock, callLocal }));
|
|
153
|
+
expect(callLayer1Mock).toHaveBeenCalled();
|
|
146
154
|
expect(result.used_cloud).toBe(false);
|
|
147
155
|
});
|
|
148
156
|
it("recursion guard: mode=route + max_tokens<=16 skips Layer 1", async () => {
|
|
@@ -531,11 +531,33 @@ describe("ledgerHandlers", () => {
|
|
|
531
531
|
await sessionLoadContextHandler({ project: "test-project" });
|
|
532
532
|
expect(vi.mocked(markContextLoaded)).not.toHaveBeenCalled();
|
|
533
533
|
});
|
|
534
|
-
it("prepends
|
|
534
|
+
it("prepends safety header on no-history path", async () => {
|
|
535
535
|
storage.loadContext.mockResolvedValue(null);
|
|
536
536
|
const result = await sessionLoadContextHandler(validArgs);
|
|
537
537
|
const text = result.content[0].text;
|
|
538
|
-
expect(text).toContain("
|
|
538
|
+
expect(text).toContain("Safety Boundaries");
|
|
539
|
+
expect(text).toContain("BOUNDARIES STUB");
|
|
540
|
+
expect(text).toContain("no previous session history");
|
|
541
|
+
});
|
|
542
|
+
it("prepends safety header on history-found path (standard/deep)", async () => {
|
|
543
|
+
storage.loadContext.mockResolvedValue({
|
|
544
|
+
last_summary: "Did some work",
|
|
545
|
+
version: 1,
|
|
546
|
+
});
|
|
547
|
+
const result = await sessionLoadContextHandler({ ...validArgs, level: "standard" });
|
|
548
|
+
const text = result.content[0].text;
|
|
549
|
+
expect(text).toContain("Safety Boundaries");
|
|
550
|
+
expect(text).toContain("BOUNDARIES STUB");
|
|
551
|
+
expect(text).toContain("Did some work");
|
|
552
|
+
});
|
|
553
|
+
it("prepends safety header on quick path", async () => {
|
|
554
|
+
storage.loadContext.mockResolvedValue({
|
|
555
|
+
last_summary: "Quick summary",
|
|
556
|
+
version: 2,
|
|
557
|
+
});
|
|
558
|
+
const result = await sessionLoadContextHandler({ ...validArgs, level: "quick" });
|
|
559
|
+
const text = result.content[0].text;
|
|
560
|
+
expect(text).toContain("Safety Boundaries");
|
|
539
561
|
expect(text).toContain("BOUNDARIES STUB");
|
|
540
562
|
});
|
|
541
563
|
});
|
|
@@ -670,10 +670,7 @@ export async function sessionLoadContextHandler(args) {
|
|
|
670
670
|
noteDriftSessionStart(convId);
|
|
671
671
|
}
|
|
672
672
|
const { BOUNDARIES_TEXT: BT0, BOUNDARIES_VERSION: BV0 } = await import("../boundaries/boundaries.js");
|
|
673
|
-
const boundariesHeader0 = `#
|
|
674
|
-
BT0 + "\n\n" +
|
|
675
|
-
`# NOTE FOR NON-CLAUDE HOSTS: these boundaries run in code on every prism_infer call.\n` +
|
|
676
|
-
`# Reserved-category requests are routed to cloud or refused — not by instruction.\n\n---\n\n`;
|
|
673
|
+
const boundariesHeader0 = `# Safety Boundaries (v${BV0})\n${BT0}\n\n---\n\n`;
|
|
677
674
|
return {
|
|
678
675
|
content: [{
|
|
679
676
|
type: "text",
|
|
@@ -944,7 +941,10 @@ export async function sessionLoadContextHandler(args) {
|
|
|
944
941
|
skillLoaded = true;
|
|
945
942
|
}
|
|
946
943
|
}
|
|
947
|
-
// Offline fallback: load
|
|
944
|
+
// Offline fallback: load ALL local skill: content (no tier gating).
|
|
945
|
+
// Deliberate: offline = degraded = best-effort. A repo-holder with
|
|
946
|
+
// sync-skills.sh has the full library locally regardless of tier.
|
|
947
|
+
// Tier gating is portal-side (name resolution); offline bypasses it.
|
|
948
948
|
if (skillResolution.isOffline) {
|
|
949
949
|
const allSettings = await storage.getAllSettings?.() || {};
|
|
950
950
|
for (const [k, v] of Object.entries(allSettings)) {
|
|
@@ -1074,10 +1074,7 @@ export async function sessionLoadContextHandler(args) {
|
|
|
1074
1074
|
noteDriftSessionStart(convId);
|
|
1075
1075
|
}
|
|
1076
1076
|
const { BOUNDARIES_TEXT, BOUNDARIES_VERSION: BV } = await import("../boundaries/boundaries.js");
|
|
1077
|
-
const boundariesHeader = `#
|
|
1078
|
-
BOUNDARIES_TEXT + "\n\n" +
|
|
1079
|
-
`# NOTE FOR NON-CLAUDE HOSTS: these boundaries run in code on every prism_infer call.\n` +
|
|
1080
|
-
`# Reserved-category requests are routed to cloud or refused — not by instruction.\n\n---\n\n`;
|
|
1077
|
+
const boundariesHeader = `# Safety Boundaries (v${BV})\n${BOUNDARIES_TEXT}\n\n---\n\n`;
|
|
1081
1078
|
return {
|
|
1082
1079
|
content: [{ type: "text", text: boundariesHeader + responseText + MEMORY_BOUNDARY_SUFFIX }],
|
|
1083
1080
|
isError: false,
|
|
@@ -1091,10 +1088,7 @@ export async function sessionLoadContextHandler(args) {
|
|
|
1091
1088
|
noteDriftSessionStart(convId);
|
|
1092
1089
|
}
|
|
1093
1090
|
const { BOUNDARIES_TEXT, BOUNDARIES_VERSION: BV2 } = await import("../boundaries/boundaries.js");
|
|
1094
|
-
const boundariesHeader2 = `#
|
|
1095
|
-
BOUNDARIES_TEXT + "\n\n" +
|
|
1096
|
-
`# NOTE FOR NON-CLAUDE HOSTS: these boundaries run in code on every prism_infer call.\n` +
|
|
1097
|
-
`# Reserved-category requests are routed to cloud or refused — not by instruction.\n\n---\n\n`;
|
|
1091
|
+
const boundariesHeader2 = `# Safety Boundaries (v${BV2})\n${BOUNDARIES_TEXT}\n\n---\n\n`;
|
|
1098
1092
|
return {
|
|
1099
1093
|
content: [{ type: "text", text: boundariesHeader2 + responseText + MEMORY_BOUNDARY_SUFFIX }],
|
|
1100
1094
|
isError: false,
|
|
@@ -412,36 +412,38 @@ export async function runInfer(args, deps) {
|
|
|
412
412
|
attempts.push({ tier: "ollama_probe", reason: "unreachable" });
|
|
413
413
|
}
|
|
414
414
|
// ── §E Layer 1 semantic pre-classifier ──────────────────────────────────
|
|
415
|
-
//
|
|
416
|
-
//
|
|
417
|
-
//
|
|
415
|
+
// Runs for ALL tiers when Ollama is reachable. RESERVED prompts escalate
|
|
416
|
+
// to cloud if available; otherwise refuse (fail-closed). Free-tier users
|
|
417
|
+
// without cloud still get classified — a RESERVED verdict refuses the
|
|
418
|
+
// request rather than silently routing to local.
|
|
418
419
|
// Recursion guard: skip when this call IS the Layer 1 classification
|
|
419
420
|
// (mode="route" + max_tokens<=16 is the Layer 1 call signature).
|
|
420
421
|
const layer1RecursionGuard = mode === "route" && maxTokens <= 16;
|
|
421
|
-
if (
|
|
422
|
+
if (installed && !layer1RecursionGuard) {
|
|
422
423
|
const l1fn = deps.callLayer1 ?? defaultCallLayer1;
|
|
423
|
-
const l1Model = resolveOllamaName("prism-coder:4b", installed
|
|
424
|
+
const l1Model = resolveOllamaName("prism-coder:4b", installed);
|
|
424
425
|
const l1 = await l1fn(args.prompt, deps.ollamaUrl, l1Model);
|
|
425
426
|
if (l1 !== "OBVIOUS_NOT_RESERVED") {
|
|
426
|
-
debugLog(`[prism_infer] Layer 1 verdict=${l1} —
|
|
427
|
+
debugLog(`[prism_infer] Layer 1 verdict=${l1} — reserved content detected`);
|
|
427
428
|
attempts.push({ tier: "layer1", reason: `layer1_${l1.toLowerCase()}` });
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
429
|
+
if (allowCloud) {
|
|
430
|
+
const cloudTimeout = args.timeout_ms ?? 90_000;
|
|
431
|
+
const cloud = await deps.callCloud(args.prompt, maxTokens, cloudTimeout);
|
|
432
|
+
if (cloud.ok && cloud.output) {
|
|
433
|
+
return await applyVerification(cloud.output, gatedArgs, deps, {
|
|
434
|
+
backend: cloud.backend ?? "synalux",
|
|
435
|
+
model_picked: null,
|
|
436
|
+
ram_free_mb: ramFreeMb,
|
|
437
|
+
latency_ms: Date.now() - t0,
|
|
438
|
+
used_cloud: true,
|
|
439
|
+
attempts,
|
|
440
|
+
plan: ent.plan,
|
|
441
|
+
completion_tokens: Math.ceil(cloud.output.length / 4),
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
attempts.push({ tier: "synalux", reason: cloud.reason ?? "unknown" });
|
|
441
445
|
}
|
|
442
|
-
|
|
443
|
-
// Layer 1 flagged RESERVED but cloud unavailable — fail closed, never fall through to local.
|
|
444
|
-
throw new Error(`prism_infer: Layer 1 verdict=${l1} but cloud unavailable. attempts=${JSON.stringify(attempts)}`);
|
|
446
|
+
throw new Error(`prism_infer: Layer 1 verdict=${l1}, reserved content refused. attempts=${JSON.stringify(attempts)}`);
|
|
445
447
|
}
|
|
446
448
|
debugLog(`[prism_infer] Layer 1 verdict=OBVIOUS_NOT_RESERVED — proceeding local`);
|
|
447
449
|
}
|
|
@@ -15,7 +15,7 @@ import { debugLog } from "./logger.js";
|
|
|
15
15
|
export const FREE_ENTITLEMENTS = {
|
|
16
16
|
plan: "free",
|
|
17
17
|
model_ceiling: "4b",
|
|
18
|
-
daily_infer_limit: 50,
|
|
18
|
+
daily_infer_limit: 50, // reserved, not enforced; cloud limits are portal-side
|
|
19
19
|
max_tokens: 512,
|
|
20
20
|
max_seats: 1,
|
|
21
21
|
features: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "prism-mcp-server",
|
|
3
|
-
"version": "20.0.
|
|
3
|
+
"version": "20.0.2",
|
|
4
4
|
"mcpName": "io.github.dcostenco/prism-coder",
|
|
5
5
|
"description": "Prism Coder — Cognitive memory + tool-calling intelligence for AI agents. Mind Palace persistent memory (BFCL Gold Certified, 100% Tool-Call Accuracy, 114 Agent Skills, PHI Guard, Tier Enforcement, Prompt-Based Skill Routing, Zero-Search HDC/HRR retrieval, HRR Semantic Drift Detection across BCBA/Coding/AAC domains, HIPAA-hardened local-first storage, SLERP-optimized GRPO alignment) plus the prism-coder 1.7B–32B open-weights LLM fleet.",
|
|
6
6
|
"module": "index.ts",
|