prism-mcp-server 19.2.8 → 19.2.9
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 +3 -1
- package/dist/scm/types.js +1 -4
- package/dist/storage/synalux.js +1 -1
- package/dist/tools/__tests__/ingestHandler.test.js +19 -13
- package/dist/tools/__tests__/layer1Integration.test.js +357 -0
- package/dist/tools/__tests__/ledgerHandlers.test.js +31 -28
- package/dist/tools/ledgerHandlers.js +82 -15
- package/dist/tools/prismInferHandler.js +164 -22
- package/dist/tools/sessionDriftHandler.js +1 -1
- package/dist/tools/skillRouting.js +22 -9
- package/dist/utils/entitlements.js +9 -0
- package/dist/utils/inferenceMetrics.js +89 -21
- package/dist/utils/layer1.js +110 -0
- package/dist/utils/projectResolver.js +2 -4
- package/dist/utils/qualityGate.js +19 -3
- package/dist/utils/safetyGate.js +22 -0
- package/dist/vm/competitorImport.js +1 -1
- package/dist/vm/componentMarketplace.js +1 -1
- package/dist/vm/creativeStudio.js +1 -1
- package/dist/vm/ethicsEnforcement.js +2 -2
- package/dist/vm/gameEngine.js +1 -1
- package/dist/vm/projectTemplates.js +1 -1
- package/dist/vm/types.js +1 -2
- package/dist/vm/vmManager.js +1 -1
- package/dist/vm/workspaceLicensing.js +1 -1
- package/package.json +1 -1
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* (POST /api/v1/prism/memory action=detect_drift) which owns the embedding
|
|
6
6
|
* + detection logic, and returns the structured result.
|
|
7
7
|
*
|
|
8
|
-
* Prism-mcp never does NLP or embedding here — that
|
|
8
|
+
* Prism-mcp never does NLP or embedding here — that is portal-side.
|
|
9
9
|
*/
|
|
10
10
|
import { isSessionDetectDriftArgs } from "./sessionMemoryDefinitions.js";
|
|
11
11
|
import { getStorage } from "../storage/index.js";
|
|
@@ -9,9 +9,8 @@
|
|
|
9
9
|
* free-tier / disconnected installations still get the BCBA universal
|
|
10
10
|
* skill loaded.
|
|
11
11
|
*
|
|
12
|
-
* To change the routing for production, edit
|
|
13
|
-
*
|
|
14
|
-
* and deploy synalux. prism-mcp picks up the new config within 5 minutes.
|
|
12
|
+
* To change the routing for production, edit the portal routing endpoint
|
|
13
|
+
* and deploy. prism-mcp picks up the new config within 5 minutes.
|
|
15
14
|
*
|
|
16
15
|
* Do NOT add hardcoded skill names here outside the OFFLINE_FALLBACK block
|
|
17
16
|
* — that defeats the single-source-of-truth design.
|
|
@@ -28,7 +27,10 @@ const OFFLINE_FALLBACK = {
|
|
|
28
27
|
user_local: { enabled: false, key_prefix: 'user_skill:' },
|
|
29
28
|
};
|
|
30
29
|
const SYNALUX_BASE = process.env.SYNALUX_BASE_URL || 'https://synalux.ai';
|
|
31
|
-
const
|
|
30
|
+
const LIVE_CACHE_TTL_MS = 5 * 60 * 1000; // 5min for successful fetches
|
|
31
|
+
const FAILURE_BACKOFF_MS = 30_000; // F5 fix: 30s retry after failure (not 5min)
|
|
32
|
+
// F5 fix: track live vs fallback separately so a transient failure doesn't
|
|
33
|
+
// negative-cache the full routing table for 5 minutes, dropping 19 skills.
|
|
32
34
|
let cached = null;
|
|
33
35
|
let inflight = null;
|
|
34
36
|
async function fetchOnce() {
|
|
@@ -51,7 +53,9 @@ async function fetchOnce() {
|
|
|
51
53
|
return body;
|
|
52
54
|
}
|
|
53
55
|
catch {
|
|
54
|
-
return OFFLINE_FALLBACK
|
|
56
|
+
// On failure: return the last live table (stale-while-revalidate) or OFFLINE_FALLBACK.
|
|
57
|
+
// The caller marks this as isLive=false so it retries after FAILURE_BACKOFF_MS, not 5min.
|
|
58
|
+
return cached?.table ?? OFFLINE_FALLBACK;
|
|
55
59
|
}
|
|
56
60
|
}
|
|
57
61
|
/**
|
|
@@ -68,15 +72,21 @@ function normalizeEntry(entry, defaultPriority) {
|
|
|
68
72
|
}
|
|
69
73
|
export async function resolveSkillsForProject(project) {
|
|
70
74
|
const now = Date.now();
|
|
71
|
-
|
|
75
|
+
// F5 fix: use shorter backoff TTL after failures so a 30s synalux hiccup doesn't
|
|
76
|
+
// lock the session into OFFLINE_FALLBACK for 5 minutes.
|
|
77
|
+
const ttl = (cached?.isLive ?? true) ? LIVE_CACHE_TTL_MS : FAILURE_BACKOFF_MS;
|
|
78
|
+
if (!cached || now - cached.fetchedAt > ttl) {
|
|
72
79
|
if (!inflight) {
|
|
73
80
|
inflight = fetchOnce().then((table) => {
|
|
74
|
-
|
|
81
|
+
// isLive = true only when we got a live response (not stale cache / OFFLINE_FALLBACK)
|
|
82
|
+
const isLive = table !== OFFLINE_FALLBACK && table !== cached?.table;
|
|
83
|
+
cached = { table, fetchedAt: Date.now(), isLive };
|
|
75
84
|
return table;
|
|
76
85
|
}).finally(() => { inflight = null; });
|
|
77
86
|
}
|
|
78
87
|
await inflight;
|
|
79
88
|
}
|
|
89
|
+
const isOffline = !(cached?.isLive ?? false);
|
|
80
90
|
const table = cached.table;
|
|
81
91
|
const seen = new Set();
|
|
82
92
|
const skills = [];
|
|
@@ -104,6 +114,7 @@ export async function resolveSkillsForProject(project) {
|
|
|
104
114
|
names: skills.map(s => s.name),
|
|
105
115
|
skills,
|
|
106
116
|
user_local: table.user_local ?? OFFLINE_FALLBACK.user_local,
|
|
117
|
+
isOffline,
|
|
107
118
|
};
|
|
108
119
|
}
|
|
109
120
|
/**
|
|
@@ -113,10 +124,12 @@ export async function resolveSkillsForProject(project) {
|
|
|
113
124
|
*/
|
|
114
125
|
export async function resolveSkillsForPrompt(prompt, baseSkills = []) {
|
|
115
126
|
const now = Date.now();
|
|
116
|
-
|
|
127
|
+
const ttl = (cached?.isLive ?? true) ? LIVE_CACHE_TTL_MS : FAILURE_BACKOFF_MS;
|
|
128
|
+
if (!cached || now - cached.fetchedAt > ttl) {
|
|
117
129
|
if (!inflight) {
|
|
118
130
|
inflight = fetchOnce().then((table) => {
|
|
119
|
-
|
|
131
|
+
const isLive = table !== OFFLINE_FALLBACK && table !== cached?.table;
|
|
132
|
+
cached = { table, fetchedAt: Date.now(), isLive };
|
|
120
133
|
return table;
|
|
121
134
|
}).finally(() => { inflight = null; });
|
|
122
135
|
}
|
|
@@ -93,6 +93,15 @@ async function fetchEntitlements() {
|
|
|
93
93
|
return cache.entitlements;
|
|
94
94
|
return FREE_ENTITLEMENTS;
|
|
95
95
|
}
|
|
96
|
+
// Normalize legacy ceiling values to the current fleet.
|
|
97
|
+
if (data.model_ceiling === "14b") {
|
|
98
|
+
debugLog("[entitlements] grandfathered 14b ceiling → 9b");
|
|
99
|
+
data.model_ceiling = "9b";
|
|
100
|
+
}
|
|
101
|
+
if (data.model_ceiling === "32b") {
|
|
102
|
+
debugLog("[entitlements] grandfathered 32b ceiling → 27b");
|
|
103
|
+
data.model_ceiling = "27b";
|
|
104
|
+
}
|
|
96
105
|
debugLog(`[entitlements] plan=${data.plan} ceiling=${data.model_ceiling} ` +
|
|
97
106
|
`daily=${data.daily_infer_limit} max_tokens=${data.max_tokens}`);
|
|
98
107
|
return data;
|
|
@@ -1,20 +1,40 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Inference metrics — local accumulator for user-facing display.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* did THIS session — prism is the natural and only complete source for
|
|
7
|
-
* this data (the portal only sees what prism forwards).
|
|
4
|
+
* Tracks what THIS prism process did THIS session. Portal forwarding
|
|
5
|
+
* (ddLog) is a separate best-effort stream — display never depends on it.
|
|
8
6
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
7
|
+
* T1 fix: content-aware chars/token estimator (was flat /4, biased for
|
|
8
|
+
* emoji-dense/code/CJK payloads by 15–40%).
|
|
9
|
+
* T2 fix: dual-column prompt tokens — `evaluated` (Ollama actual) vs
|
|
10
|
+
* `submittedEst` (estimated submitted, including KV-cached prefixes).
|
|
11
|
+
* Ollama returns prompt_eval_count=0 for cached prompts, so "evaluated"
|
|
12
|
+
* undercounts on repeated system-prompt calls; submittedEst shows actual load.
|
|
12
13
|
*/
|
|
13
14
|
import { debugLog } from "./logger.js";
|
|
15
|
+
// T1 fix: content-aware token estimator. Replaces flat text.length / 4 which
|
|
16
|
+
// underestimates emoji (~2 UTF-16 units but 1.5-2.5 BPE tokens) and CJK
|
|
17
|
+
// (~1 char ≈ 1 token) by 15-40%, and overestimates dense code (~3.3 chars/token).
|
|
18
|
+
export function estimateTokens(text) {
|
|
19
|
+
if (!text)
|
|
20
|
+
return 0;
|
|
21
|
+
const cjkCount = (text.match(/[ -鿿豈-]/g) ?? []).length;
|
|
22
|
+
const emojiCount = (text.match(/[\u{1F000}-\u{1FFFF}]/gu) ?? []).length;
|
|
23
|
+
// Code density check: >2% of chars are code punctuation → use code divisor
|
|
24
|
+
const codePunct = (text.match(/[`{};\[\]=>|#@$%^&*\\]/g) ?? []).length;
|
|
25
|
+
const isCode = text.length > 0 && codePunct / text.length > 0.02;
|
|
26
|
+
// UTF-16 length minus CJK and emoji codepoints (emoji are 2 units each)
|
|
27
|
+
const latinLen = text.length - cjkCount - emojiCount * 2;
|
|
28
|
+
const latinTokens = latinLen / (isCode ? 3.3 : 4.0);
|
|
29
|
+
const cjkTokens = cjkCount; // ~1 token per CJK char
|
|
30
|
+
const emojiTokens = emojiCount * 1.5; // ~1.5 BPE tokens per emoji
|
|
31
|
+
return Math.ceil(Math.max(0, latinTokens) + cjkTokens + emojiTokens);
|
|
32
|
+
}
|
|
14
33
|
const byModel = {};
|
|
15
34
|
let localCalls = 0;
|
|
16
35
|
let cloudCalls = 0;
|
|
17
|
-
let
|
|
36
|
+
let promptTokensEvaluated = 0;
|
|
37
|
+
let promptTokensSubmittedEst = 0;
|
|
18
38
|
let totalCompletionTokens = 0;
|
|
19
39
|
let totalLatencyMs = 0;
|
|
20
40
|
export function recordInference(result) {
|
|
@@ -27,16 +47,35 @@ export function recordInference(result) {
|
|
|
27
47
|
else {
|
|
28
48
|
localCalls++;
|
|
29
49
|
}
|
|
30
|
-
const
|
|
50
|
+
const evaluated = result.prompt_tokens ?? 0;
|
|
31
51
|
const ct = result.completion_tokens ?? 0;
|
|
32
|
-
|
|
52
|
+
// T2: when Ollama returns 0 evaluated (KV-cache hit), estimate submitted tokens
|
|
53
|
+
// from the prompt text/length so submittedEst reflects actual context load.
|
|
54
|
+
let submittedEst = evaluated; // default: evaluated is the best estimate
|
|
55
|
+
if (evaluated === 0 && !result.used_cloud) {
|
|
56
|
+
if (result.prompt_text) {
|
|
57
|
+
submittedEst = estimateTokens(result.prompt_text);
|
|
58
|
+
}
|
|
59
|
+
else if (result.prompt_length && result.prompt_length > 0) {
|
|
60
|
+
submittedEst = Math.ceil(result.prompt_length / 4); // flat fallback without text
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
promptTokensEvaluated += evaluated;
|
|
64
|
+
promptTokensSubmittedEst += submittedEst;
|
|
33
65
|
totalCompletionTokens += ct;
|
|
34
66
|
totalLatencyMs += result.latency_ms;
|
|
35
67
|
if (!byModel[key]) {
|
|
36
|
-
byModel[key] = {
|
|
68
|
+
byModel[key] = {
|
|
69
|
+
calls: 0,
|
|
70
|
+
promptTokensEvaluated: 0,
|
|
71
|
+
promptTokensSubmittedEst: 0,
|
|
72
|
+
completionTokens: 0,
|
|
73
|
+
totalLatencyMs: 0,
|
|
74
|
+
};
|
|
37
75
|
}
|
|
38
76
|
byModel[key].calls++;
|
|
39
|
-
byModel[key].
|
|
77
|
+
byModel[key].promptTokensEvaluated += evaluated;
|
|
78
|
+
byModel[key].promptTokensSubmittedEst += submittedEst;
|
|
40
79
|
byModel[key].completionTokens += ct;
|
|
41
80
|
byModel[key].totalLatencyMs += result.latency_ms;
|
|
42
81
|
}
|
|
@@ -52,9 +91,10 @@ export function getInferenceSnapshot() {
|
|
|
52
91
|
totalCalls: total,
|
|
53
92
|
localPct: total > 0 ? Math.round((localCalls / total) * 100) : 0,
|
|
54
93
|
cloudPct: total > 0 ? 100 - Math.round((localCalls / total) * 100) : 0,
|
|
55
|
-
|
|
94
|
+
promptTokensEvaluated,
|
|
95
|
+
promptTokensSubmittedEst,
|
|
56
96
|
totalCompletionTokens,
|
|
57
|
-
totalTokens:
|
|
97
|
+
totalTokens: promptTokensSubmittedEst + totalCompletionTokens,
|
|
58
98
|
avgLatencyMs: total > 0 ? Math.round(totalLatencyMs / total) : 0,
|
|
59
99
|
byModel: modelCopy,
|
|
60
100
|
};
|
|
@@ -62,7 +102,8 @@ export function getInferenceSnapshot() {
|
|
|
62
102
|
export function resetInferenceMetrics() {
|
|
63
103
|
localCalls = 0;
|
|
64
104
|
cloudCalls = 0;
|
|
65
|
-
|
|
105
|
+
promptTokensEvaluated = 0;
|
|
106
|
+
promptTokensSubmittedEst = 0;
|
|
66
107
|
totalCompletionTokens = 0;
|
|
67
108
|
totalLatencyMs = 0;
|
|
68
109
|
for (const key of Object.keys(byModel)) {
|
|
@@ -75,27 +116,54 @@ export async function inferenceMetricsHandler() {
|
|
|
75
116
|
return {
|
|
76
117
|
content: [{
|
|
77
118
|
type: "text",
|
|
78
|
-
text: block || "No prism_infer calls this session
|
|
119
|
+
text: block || "No prism_infer calls this session.\n" +
|
|
120
|
+
"📊 Delegation Metrics track local-model delegation — not the host model's (Claude's) spend.",
|
|
79
121
|
}],
|
|
80
122
|
};
|
|
81
123
|
}
|
|
82
|
-
|
|
124
|
+
/**
|
|
125
|
+
* Format inference metrics.
|
|
126
|
+
*
|
|
127
|
+
* @param compact - When true, returns a single-line footer for appending to
|
|
128
|
+
* prism_infer responses. Output is threshold-gated: only emits every
|
|
129
|
+
* PRISM_METRICS_EVERY calls (default 5) so it doesn't drown per-response output.
|
|
130
|
+
* When false (default), returns the full multi-line block used by the explicit
|
|
131
|
+
* inference_metrics tool.
|
|
132
|
+
*/
|
|
133
|
+
export function formatInferenceMetrics(compact = false) {
|
|
83
134
|
const snap = getInferenceSnapshot();
|
|
84
135
|
if (snap.totalCalls === 0)
|
|
85
136
|
return "";
|
|
137
|
+
if (compact) {
|
|
138
|
+
// Threshold gate: only emit every N calls so the footer is periodic, not per-call noise.
|
|
139
|
+
// The per-call header already shows backend/model/latency; this is the session rollup.
|
|
140
|
+
const every = parseInt(process.env["PRISM_METRICS_EVERY"] ?? "5", 10);
|
|
141
|
+
// Always emit on the first call (totalCalls===1) so short sessions (1–4 calls)
|
|
142
|
+
// see at least one rollup. Otherwise emit every N calls as the rolling summary.
|
|
143
|
+
if (snap.totalCalls !== 1 && snap.totalCalls % every !== 0)
|
|
144
|
+
return "";
|
|
145
|
+
return `📊 local ${snap.localCalls} (${snap.localPct}%) · cloud ${snap.cloudCalls} (${snap.cloudPct}%) · ~${snap.totalTokens.toLocaleString()} tok · avg ${snap.avgLatencyMs}ms`;
|
|
146
|
+
}
|
|
147
|
+
// Full multi-line block (explicit inference_metrics tool call).
|
|
148
|
+
// T2: show both evaluated (Ollama actual) and submitted estimate.
|
|
149
|
+
// When they differ, the gap is KV-cached prompt tokens (real load, not counted by Ollama).
|
|
150
|
+
const promptLine = snap.promptTokensEvaluated !== snap.promptTokensSubmittedEst
|
|
151
|
+
? ` Prompt tokens: ${snap.promptTokensEvaluated.toLocaleString()} evaluated / ${snap.promptTokensSubmittedEst.toLocaleString()} submitted est.`
|
|
152
|
+
: ` Prompt tokens: ${snap.promptTokensEvaluated.toLocaleString()}`;
|
|
86
153
|
const lines = [
|
|
87
|
-
`\n📊
|
|
154
|
+
`\n📊 Delegation Metrics — local-model calls this session (not host model spend):`,
|
|
88
155
|
` Total calls: ${snap.totalCalls} — Local: ${snap.localCalls} (${snap.localPct}%) | Cloud: ${snap.cloudCalls} (${snap.cloudPct}%)`,
|
|
89
|
-
|
|
156
|
+
promptLine,
|
|
157
|
+
` Completion tokens: ${snap.totalCompletionTokens.toLocaleString()}`,
|
|
90
158
|
` Avg latency: ${snap.avgLatencyMs}ms`,
|
|
91
159
|
];
|
|
92
160
|
const models = Object.entries(snap.byModel).sort((a, b) => b[1].calls - a[1].calls);
|
|
93
161
|
if (models.length > 1) {
|
|
94
162
|
lines.push(` By model:`);
|
|
95
163
|
for (const [name, stats] of models) {
|
|
96
|
-
const tokens = stats.
|
|
164
|
+
const tokens = stats.promptTokensSubmittedEst + stats.completionTokens;
|
|
97
165
|
const avgMs = stats.calls > 0 ? Math.round(stats.totalLatencyMs / stats.calls) : 0;
|
|
98
|
-
lines.push(` ${name}: ${stats.calls} calls, ${tokens.toLocaleString()} tokens
|
|
166
|
+
lines.push(` ${name}: ${stats.calls} calls, ${tokens.toLocaleString()} tokens est., avg ${avgMs}ms`);
|
|
99
167
|
}
|
|
100
168
|
}
|
|
101
169
|
return lines.join("\n");
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Layer 1 semantic pre-classifier for prism_infer.
|
|
3
|
+
* ─────────────────────────────────────────────────────────────
|
|
4
|
+
* Calls dcostenco/prism-coder:4b via Ollama to classify whether
|
|
5
|
+
* a prompt is OBVIOUS_RESERVED, OBVIOUS_NOT_RESERVED, or UNCERTAIN.
|
|
6
|
+
*
|
|
7
|
+
* Fail-closed contract:
|
|
8
|
+
* OBVIOUS_NOT_RESERVED → the ONLY verdict that permits local routing
|
|
9
|
+
* OBVIOUS_RESERVED → escalate to cloud
|
|
10
|
+
* UNCERTAIN → escalate to cloud (conservative)
|
|
11
|
+
* ERROR → escalate to cloud (never fail-open)
|
|
12
|
+
*
|
|
13
|
+
* The prompt below is VERBATIM from §E of prism-infer-boundaries/SKILL.md.
|
|
14
|
+
* It is duplicated here (not imported) because prism is a thin client with no
|
|
15
|
+
* access to the skills tree at runtime. A drift test asserts byte-for-byte match.
|
|
16
|
+
*
|
|
17
|
+
* Must not run when mode="route" and max_tokens<=16 — that IS a Layer 1 call.
|
|
18
|
+
*/
|
|
19
|
+
// VERBATIM — §E prism-infer-boundaries/SKILL.md. Do not edit without re-running eval-layer1.mjs.
|
|
20
|
+
export const LAYER1_PROMPT = `Classify this request with exactly one word: OBVIOUS_RESERVED, OBVIOUS_NOT_RESERVED, or UNCERTAIN.
|
|
21
|
+
|
|
22
|
+
OBVIOUS_RESERVED — clearly involves:
|
|
23
|
+
- Behavioral crisis: de-escalation, meltdown management, rage episode, violent behavior response
|
|
24
|
+
- Physical restraint, holds, containment, physical management of clients
|
|
25
|
+
- Self-harm: client bites/hits/scratches/bangs self, draws blood, injures self
|
|
26
|
+
- Suicide, homicide, harm to self or others, safety planning
|
|
27
|
+
- Client physical elopement incidents — client ran away, bolted, or escaped (with prior documented incidents)
|
|
28
|
+
- Writing auth/login/JWT/token/session validation code
|
|
29
|
+
- Determining if code bypasses auth or allows unauthenticated access
|
|
30
|
+
- Assessing whether code actually exposes PHI or constitutes a HIPAA violation
|
|
31
|
+
- Whether to push, ship, deploy, or block a production release
|
|
32
|
+
- Diagnosis code assignment
|
|
33
|
+
|
|
34
|
+
OBVIOUS_NOT_RESERVED — general coding, code review, analysis, documentation, test generation, data processing.
|
|
35
|
+
|
|
36
|
+
UNCERTAIN — if the request touches reserved vocabulary but the task is non-security, non-clinical operational work (e.g., deleting or renaming a file, adding a non-auth data field to a form, reviewing existing code for dead exports or hook order) rather than writing clinical safety protocols, implementing auth or security code, making push or deploy decisions, or determining compliance.
|
|
37
|
+
|
|
38
|
+
Request: "{prompt}"
|
|
39
|
+
|
|
40
|
+
Answer (one word):`;
|
|
41
|
+
const VALID = new Set([
|
|
42
|
+
"OBVIOUS_RESERVED",
|
|
43
|
+
"OBVIOUS_NOT_RESERVED",
|
|
44
|
+
"UNCERTAIN",
|
|
45
|
+
]);
|
|
46
|
+
/**
|
|
47
|
+
* Parse the model's raw text into a verdict. Extracts the first token of
|
|
48
|
+
* letters/underscores (ignoring leading punctuation, quotes, whitespace).
|
|
49
|
+
* Anything not in the valid set → ERROR, which the caller escalates.
|
|
50
|
+
* Note: OBVIOUS_NOT_RESERVED contains "RESERVED" as a substring — whole-token
|
|
51
|
+
* matching via Set prevents the substring trap from inverting the gate.
|
|
52
|
+
*/
|
|
53
|
+
export function parseLayer1(raw) {
|
|
54
|
+
if (!raw)
|
|
55
|
+
return "ERROR";
|
|
56
|
+
const m = raw.trim().toUpperCase().match(/[A-Z_]+/);
|
|
57
|
+
if (!m)
|
|
58
|
+
return "ERROR";
|
|
59
|
+
const token = m[0];
|
|
60
|
+
return VALID.has(token) ? token : "ERROR";
|
|
61
|
+
}
|
|
62
|
+
// p95 measured 484–511ms on warm 4b; 1500ms is a generous ceiling that still
|
|
63
|
+
// fails closed on model stall without blocking the handler for too long.
|
|
64
|
+
const LAYER1_TIMEOUT_MS = 1_500;
|
|
65
|
+
/**
|
|
66
|
+
* Run the Layer 1 classifier. Returns a verdict; never throws.
|
|
67
|
+
* On ANY failure path returns "ERROR" so the caller escalates to cloud.
|
|
68
|
+
*
|
|
69
|
+
* @param userPrompt the end-user prompt being classified (NOT the LAYER1_PROMPT)
|
|
70
|
+
* @param ollamaUrl Ollama base URL
|
|
71
|
+
* @param model classifier model tag (caller-resolved via resolveOllamaName)
|
|
72
|
+
* @param fetchImpl injected for tests; defaults to global fetch
|
|
73
|
+
*/
|
|
74
|
+
export async function callLayer1(userPrompt, ollamaUrl, model, fetchImpl = fetch) {
|
|
75
|
+
if (!userPrompt || !userPrompt.trim())
|
|
76
|
+
return "ERROR";
|
|
77
|
+
let res;
|
|
78
|
+
try {
|
|
79
|
+
res = await fetchImpl(`${ollamaUrl}/api/chat`, {
|
|
80
|
+
method: "POST",
|
|
81
|
+
headers: { "Content-Type": "application/json" },
|
|
82
|
+
body: JSON.stringify({
|
|
83
|
+
model,
|
|
84
|
+
messages: [
|
|
85
|
+
{ role: "user", content: LAYER1_PROMPT.replace("{prompt}", userPrompt) },
|
|
86
|
+
],
|
|
87
|
+
stream: false,
|
|
88
|
+
think: false,
|
|
89
|
+
options: { num_predict: 16, temperature: 0 },
|
|
90
|
+
}),
|
|
91
|
+
signal: AbortSignal.timeout(LAYER1_TIMEOUT_MS),
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return "ERROR";
|
|
96
|
+
}
|
|
97
|
+
if (!res.ok)
|
|
98
|
+
return "ERROR";
|
|
99
|
+
let data;
|
|
100
|
+
try {
|
|
101
|
+
data = await res.json();
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return "ERROR";
|
|
105
|
+
}
|
|
106
|
+
if (data?.error)
|
|
107
|
+
return "ERROR";
|
|
108
|
+
const text = data?.message?.content;
|
|
109
|
+
return parseLayer1(text);
|
|
110
|
+
}
|
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
* Prism Project Resolver — Local Storage Variant
|
|
3
3
|
* ================================================
|
|
4
4
|
*
|
|
5
|
-
* Same contract as the
|
|
6
|
-
* (synalux-private/portal/src/lib/prism-project-resolver.ts) but
|
|
5
|
+
* Same contract as the Synalux portal resolver but
|
|
7
6
|
* sources the project registry from local prism-config.db settings
|
|
8
7
|
* (`repo_path:<project>` keys) instead of the `prism_projects`
|
|
9
8
|
* Supabase table.
|
|
@@ -13,8 +12,7 @@
|
|
|
13
12
|
* portal resolver becomes authoritative and this one becomes a noop
|
|
14
13
|
* for the same write.
|
|
15
14
|
*
|
|
16
|
-
* Background:
|
|
17
|
-
* 2026-04-30-thin-client-architecture-directive — the prism-aac
|
|
15
|
+
* Background: thin-client architecture directive 2026-04-30 — the prism-aac
|
|
18
16
|
* Azure-leak memory-loss bug was caused by the absence of this
|
|
19
17
|
* validation.
|
|
20
18
|
*/
|
|
@@ -7,19 +7,29 @@
|
|
|
7
7
|
*
|
|
8
8
|
* Returns: { pass: boolean, reason?: string }
|
|
9
9
|
*/
|
|
10
|
+
/**
|
|
11
|
+
* Signal 5 — Tool-call bleed: pipe-delimited format leaking into non-tool turns.
|
|
12
|
+
* Matches <|tool_call|> and <|tool_call_end|> only — NOT angle-bracket <tool_call> variants
|
|
13
|
+
* (those are normalized by normalizeToolCallFormat, not gated as failures).
|
|
14
|
+
*/
|
|
15
|
+
export const TOOL_CALL_BLEED_RE = /<\|tool_call\|>|<\|tool_call_end\|>/;
|
|
10
16
|
/**
|
|
11
17
|
* Check if a model response passes the quality gate.
|
|
12
18
|
* @param stripped Response AFTER think-stripping (use stripThink first)
|
|
13
19
|
* @param thinkOnly True if the response was only <think> blocks with no answer
|
|
14
20
|
* @param finishReason Ollama's finish_reason if available (e.g. "length" = truncated)
|
|
21
|
+
* @param mode Inference mode — "route" uses length===0 floor; "code"/"chat" keep <5
|
|
15
22
|
*/
|
|
16
|
-
export function passesQualityGate(stripped, thinkOnly, finishReason) {
|
|
23
|
+
export function passesQualityGate(stripped, thinkOnly, finishReason, mode) {
|
|
17
24
|
// Signal 1: Think-only — model reasoned but produced no answer (check before empty)
|
|
18
25
|
if (thinkOnly) {
|
|
19
26
|
return { pass: false, reason: "think_only" };
|
|
20
27
|
}
|
|
21
|
-
// Signal 2:
|
|
22
|
-
|
|
28
|
+
// Signal 2: Mode-aware empty floor.
|
|
29
|
+
// Route legitimately returns 1–4 char labels ("P1", "YES", "CO4", "FIXED").
|
|
30
|
+
// Use length===0 for route; keep <5 for code/chat where single-word answers are invalid.
|
|
31
|
+
const emptyFloor = mode === "route" ? 0 : 4;
|
|
32
|
+
if (stripped.trim().length <= emptyFloor) {
|
|
23
33
|
return { pass: false, reason: "empty_response" };
|
|
24
34
|
}
|
|
25
35
|
// Signal 3: Hard truncation — Ollama reports finish_reason="length"
|
|
@@ -27,6 +37,12 @@ export function passesQualityGate(stripped, thinkOnly, finishReason) {
|
|
|
27
37
|
if (finishReason === "length") {
|
|
28
38
|
return { pass: false, reason: "hard_truncation" };
|
|
29
39
|
}
|
|
40
|
+
// Signal 5: Tool-call bleed — fine-tuned 4b emits <|tool_call|> format in non-tool turns.
|
|
41
|
+
// Pipe-delimited format only; angle-bracket variants are handled by normalizeToolCallFormat.
|
|
42
|
+
// False-positive guard: requires the literal pipe tokens, not the words "tool call".
|
|
43
|
+
if (TOOL_CALL_BLEED_RE.test(stripped)) {
|
|
44
|
+
return { pass: false, reason: "tool_call_bleed" };
|
|
45
|
+
}
|
|
30
46
|
// Signal 4: Exact-loop detection (two passes).
|
|
31
47
|
//
|
|
32
48
|
// Pass A (prose-only, threshold ≥3): strip structural markdown that
|
package/dist/utils/safetyGate.js
CHANGED
|
@@ -56,6 +56,26 @@ const MEDICAL_INPUT_RE = [
|
|
|
56
56
|
/(?:how\s+much|what\s+dose)\s+.*(?:should\s+I|do\s+I|can\s+I)\s+(?:inject|take|give)/i,
|
|
57
57
|
/(?:dose|dosage)\s+(?:should\s+I|do\s+I|can\s+I)\s+(?:inject|take|give)/i,
|
|
58
58
|
];
|
|
59
|
+
// ── Output: BCBA reserved category — physical management / restraint / seclusion ─
|
|
60
|
+
//
|
|
61
|
+
// These patterns are a BACKSTOP (Option 1) — high-precision/low-recall.
|
|
62
|
+
// The structural safety mechanism is Option 4: consequence/crisis sections of BIPs
|
|
63
|
+
// never generate locally. These regexes catch the obvious cases if section-routing fails.
|
|
64
|
+
// They do NOT substitute for the structural gate; euphemistic phrasings ("guide the client
|
|
65
|
+
// firmly to the floor") pass through, so the structural gate is the primary control.
|
|
66
|
+
const BCBA_OUTPUT_RE = [
|
|
67
|
+
// Explicit restraint technique names
|
|
68
|
+
/\b(?:prone|supine|basket|therapeutic|manual)\s+(?:hold|restraint)\b/i,
|
|
69
|
+
/\btwo[-\s]?person\s+(?:hold|restraint)\b/i,
|
|
70
|
+
/\bphysical\s+(?:intervention|management|restraint)\b/i,
|
|
71
|
+
// Seclusion
|
|
72
|
+
/\bseclusion\s+(?:room|protocol|procedure|space)\b/i,
|
|
73
|
+
/\bplace\s+(?:the\s+)?client\s+in\s+(?:a\s+)?(?:seclusion|hold|timeout\s+room)\b/i,
|
|
74
|
+
// Staff-instruction framing (high-precision: requires staff + restraint verb + client)
|
|
75
|
+
/\bstaff\s+(?:should|must|will)\s+(?:physically\s+)?(?:hold|restrain|manage)\s+(?:the\s+)?client\b/i,
|
|
76
|
+
/\bcaregiver\s+(?:should|must|will)\s+(?:physically\s+)?(?:hold|restrain|manage)\s+(?:the\s+)?client\b/i,
|
|
77
|
+
];
|
|
78
|
+
export const BCBA_RESPONSE = "This output contains physical management content (restraint, hold, or seclusion procedure). Consequence strategy and crisis/safety sections require cloud review — they cannot generate locally per BCBA clinical standards. Escalating to cloud.";
|
|
59
79
|
// ── Output: model providing dangerous instructions ───────────────────────────
|
|
60
80
|
const CRISIS_OUTPUT_RE = [
|
|
61
81
|
/\bhow many pills\s+to\b/i,
|
|
@@ -96,6 +116,8 @@ export function checkInputSafety(text) {
|
|
|
96
116
|
}
|
|
97
117
|
export function checkOutputSafety(response) {
|
|
98
118
|
const r = normalize(response);
|
|
119
|
+
if (BCBA_OUTPUT_RE.some(re => re.test(r)))
|
|
120
|
+
return BCBA_RESPONSE;
|
|
99
121
|
if (CRISIS_OUTPUT_RE.some(re => re.test(r)))
|
|
100
122
|
return CRISIS_RESPONSE;
|
|
101
123
|
if (MEDICAL_OUTPUT_RE.some(re => re.test(r)))
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* - License enforcement and revenue sharing
|
|
10
10
|
* - Category-based discovery and search
|
|
11
11
|
*
|
|
12
|
-
* BOUNDARY: Interfaces only — implementations
|
|
12
|
+
* BOUNDARY: Interfaces only — implementations are portal-side.
|
|
13
13
|
*/
|
|
14
14
|
export const MARKETPLACE_TIERS = {
|
|
15
15
|
free: {
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* - Audio generation and production
|
|
9
9
|
* - All tier-gated through Synalux subscription
|
|
10
10
|
*
|
|
11
|
-
* BOUNDARY: Interfaces only — implementations
|
|
11
|
+
* BOUNDARY: Interfaces only — implementations are portal-side.
|
|
12
12
|
*/
|
|
13
13
|
export const CREATIVE_STUDIO_TIERS = {
|
|
14
14
|
free: {
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* 5. KILL SWITCH — instant account suspension (remote, irreversible)
|
|
15
15
|
* 6. AUDIT TRAIL — tamper-proof logging for compliance investigations
|
|
16
16
|
*
|
|
17
|
-
* BOUNDARY: Interfaces only — implementations
|
|
17
|
+
* BOUNDARY: Interfaces only — implementations are portal-side.
|
|
18
18
|
*/
|
|
19
19
|
/** Default prohibited use policy — hardcoded, cannot be overridden per-tenant */
|
|
20
20
|
export const PROHIBITED_USE_POLICY = {
|
|
@@ -122,7 +122,7 @@ export const DEFAULT_USE_CASE_SCREENING = {
|
|
|
122
122
|
human_review_queue: true,
|
|
123
123
|
re_screening_interval_days: 30,
|
|
124
124
|
prohibited_dependencies: [
|
|
125
|
-
// Placeholder patterns —
|
|
125
|
+
// Placeholder patterns — see portal-side ethics enforcement config
|
|
126
126
|
'@military/*', 'defense-*', 'weapon-*', 'surveillance-*',
|
|
127
127
|
],
|
|
128
128
|
government_domain_patterns: [
|
package/dist/vm/gameEngine.js
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* - Cross-platform build matrix
|
|
16
16
|
* - Plugin/SDK sandbox
|
|
17
17
|
*
|
|
18
|
-
* BOUNDARY: Interfaces only — implementations
|
|
18
|
+
* BOUNDARY: Interfaces only — implementations are portal-side.
|
|
19
19
|
*/
|
|
20
20
|
/** Preset memory budgets for common platforms */
|
|
21
21
|
export const MEMORY_BUDGETS = {
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* - Creative projects (3D visualization, VR experience, etc.)
|
|
9
9
|
* - Enterprise (dashboards, APIs, microservices)
|
|
10
10
|
*
|
|
11
|
-
* BOUNDARY: Interfaces only — implementations
|
|
11
|
+
* BOUNDARY: Interfaces only — implementations are portal-side.
|
|
12
12
|
*/
|
|
13
13
|
// ══════════════════════════════════════════════════════════════════
|
|
14
14
|
// 2. GAME TEMPLATES
|
package/dist/vm/types.js
CHANGED
|
@@ -7,8 +7,7 @@
|
|
|
7
7
|
*
|
|
8
8
|
* Only interfaces live in Prism — implementations stay in Synalux.
|
|
9
9
|
*
|
|
10
|
-
* @see
|
|
11
|
-
* @see synalux-private/portal/src/lib/device-registry.ts
|
|
10
|
+
* @see Synalux portal — vm-manager, device-registry
|
|
12
11
|
*/
|
|
13
12
|
/** Preset network conditions for load testing */
|
|
14
13
|
export const NETWORK_PRESETS = {
|
package/dist/vm/vmManager.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* VM Manager — Prism IDE Hypervisor Abstraction
|
|
3
3
|
* ==============================================
|
|
4
4
|
* Interface layer for VM lifecycle management.
|
|
5
|
-
* Implementations
|
|
5
|
+
* Implementations are portal-side (Synalux VM manager).
|
|
6
6
|
*
|
|
7
7
|
* Supports:
|
|
8
8
|
* - VM creation from built-in templates or custom specs
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* Required because the marketplace allows building, distributing, and selling apps.
|
|
10
10
|
* License compliance is enforced at build, deploy, and publish time.
|
|
11
11
|
*
|
|
12
|
-
* BOUNDARY: Interfaces only — implementations
|
|
12
|
+
* BOUNDARY: Interfaces only — implementations are portal-side.
|
|
13
13
|
*/
|
|
14
14
|
/** Workspace license presets */
|
|
15
15
|
export const WORKSPACE_LICENSE_PRESETS = {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "prism-mcp-server",
|
|
3
|
-
"version": "19.2.
|
|
3
|
+
"version": "19.2.9",
|
|
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",
|