prism-mcp-server 20.2.7 → 20.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 +40 -9
- package/dist/cli.js +45 -0
- package/dist/connect.js +7 -0
- package/dist/evidenceWorkflowPolicy.js +19 -0
- package/dist/lifecycle.js +3 -0
- package/dist/mcpTransportHealth.js +70 -0
- package/dist/server.js +12 -1
- package/dist/skillManifestSync.js +9 -6
- package/dist/storage/synalux.js +41 -1
- package/dist/tools/behavioralVerifierHandler.js +20 -4
- package/dist/tools/ledgerHandlers.js +125 -80
- package/dist/tools/prismInferHandler.js +193 -7
- package/dist/tools/sessionMemoryDefinitions.js +2 -1
- package/dist/tools/skillRouting.js +3 -1
- package/dist/utils/codingQualityPolicy.js +9 -1
- package/dist/utils/entitlements.js +1 -0
- package/dist/utils/qualityGate.js +16 -13
- package/dist/utils/routeContract.js +319 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -25,6 +25,10 @@ A paid subscription adds cloud sync, higher model tiers, and team features throu
|
|
|
25
25
|
- **Local-first inference** — bounded work is routed through local Ollama models
|
|
26
26
|
first, with automatic 2B/4B/9B/27B selection based on installed models,
|
|
27
27
|
available RAM, context fit, and subscription entitlements.
|
|
28
|
+
- **Route-output enforcement** — route mode returns only well-formed calls to
|
|
29
|
+
tools the host actually advertised. Standard and higher plans can add
|
|
30
|
+
authenticated deterministic correction; `route_guard: "local"` keeps the
|
|
31
|
+
prompt and draft entirely on-device.
|
|
28
32
|
- **One setup for every agent** — `prism connect` configures Claude Code,
|
|
29
33
|
Claude Desktop, Cursor, Gemini CLI, and Codex while preserving unrelated
|
|
30
34
|
settings.
|
|
@@ -144,10 +148,16 @@ that would require a host lifecycle hook, launcher, extension, or Prism-owned
|
|
|
144
148
|
panel. Context loading itself remains complete even when a host shortens the
|
|
145
149
|
visible reply.
|
|
146
150
|
|
|
147
|
-
Free accounts receive the
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
+
Free accounts receive only the public hook-free `prism-startup` package; the MCP
|
|
152
|
+
server still supplies a compact, non-proprietary safety and evidence contract.
|
|
153
|
+
Authenticated paid accounts receive the protected behavioral and engineering
|
|
154
|
+
packages plus the current subscribed routing set. The paid
|
|
155
|
+
`evidence-first-protocol` keeps ordinary coding lightweight: one correlated
|
|
156
|
+
reproduction is enough to begin an edit, while strict acceptance starts only
|
|
157
|
+
before a completion claim, push, or release and inspects only the exact artifacts
|
|
158
|
+
used as proof. Upgrades install newly entitled packages; verified downgrades
|
|
159
|
+
remove only Prism-owned packages while preserving local skills and locally
|
|
160
|
+
modified conflicts.
|
|
151
161
|
|
|
152
162
|
When upgrading an older Claude Code installation, `prism connect` removes only
|
|
153
163
|
the exact Prism-owned startup, skill-sync, handoff, and drift hook actions from
|
|
@@ -448,10 +458,15 @@ The free tier runs entirely on your machine. Paid tiers add cloud sync through t
|
|
|
448
458
|
| Inference | Local Ollama models | Local models + Gemini 3.6 Flash fallback |
|
|
449
459
|
| API keys required | None | Synalux subscription key |
|
|
450
460
|
| Web search / scrape | Not included | Via Synalux portal (provider keys server-side) |
|
|
451
|
-
| What leaves your machine | Nothing | Memory text
|
|
461
|
+
| What leaves your machine | Nothing | Memory text, file paths, search queries, and inference prompts/drafts when their cloud feature is used, sent to the portal over TLS. Cloud memory writes are PHI-redacted; inference and route requests are transient. |
|
|
452
462
|
| Works offline | ✅ | Local features yes; sync/cloud no |
|
|
453
463
|
|
|
454
|
-
**Handling sensitive data.**
|
|
464
|
+
**Handling sensitive data.** Cloud memory writes pass through automatic
|
|
465
|
+
redaction (SSNs, dates of birth, medical record numbers, phone numbers, emails,
|
|
466
|
+
and clinical identifiers are stripped before storage). Cloud inference and
|
|
467
|
+
route correction send the request over TLS for processing and do not store it
|
|
468
|
+
as Prism memory; use `route_guard: "local"` or the **local tier** for a full
|
|
469
|
+
air-gap. **Enterprise** includes a HIPAA Business Associate Agreement.
|
|
455
470
|
|
|
456
471
|
---
|
|
457
472
|
|
|
@@ -461,6 +476,14 @@ The `prism-coder` fleet uses Qwen3.5 for MCP tool-routing AND general inference.
|
|
|
461
476
|
|
|
462
477
|
`prism_infer` supports three modes: `route` (tool routing, fast, nothink), `chat` (conversation with thinking), and `code` (code generation with thinking). In chat/code modes, the model uses `<think>` blocks for chain-of-thought reasoning, which are stripped before the response is served. If the local model fails a quality gate (empty, think-only, or truncated), paid tiers automatically escalate to Gemini 3.6 Flash via the Synalux portal.
|
|
463
478
|
|
|
479
|
+
Every route-mode result is parsed locally and checked against `allowed_tools`
|
|
480
|
+
before it reaches the host. Malformed or unadvertised calls become `NO_TOOL`.
|
|
481
|
+
With `route_guard: "auto"` (the default), Standard and higher plans also send
|
|
482
|
+
a well-formed draft for one of Prism's seven trained tools—or an unadvertised
|
|
483
|
+
draft that may need correction—to Synalux for authenticated deterministic
|
|
484
|
+
correction. Advertised custom host tools remain local. Set
|
|
485
|
+
`route_guard: "local"` for a fully on-device route path.
|
|
486
|
+
|
|
464
487
|
| Model | Ollama tag | Size | [BFCL](https://gorilla.cs.berkeley.edu/blogs/12_bfcl_v3_multi_turn.html) Accuracy | Role | Automatic routing tier |
|
|
465
488
|
|---|---|---|---|---|---|
|
|
466
489
|
| Qwen3.5-4B Q3_K_M | `prism-coder:2b` | 2.3 GB | 99.1% × 3 seeds | iPhone / mobile first gate | Free |
|
|
@@ -486,17 +509,25 @@ query → prism-coder:9b (local router, default)
|
|
|
486
509
|
|
|
487
510
|
### Multi-Layer Verification
|
|
488
511
|
|
|
489
|
-
|
|
512
|
+
Route output and evidence-grounded answers use separate gates. Every tier gets
|
|
513
|
+
the local route parser and advertised-tool registry; Standard and higher plans
|
|
514
|
+
can add the private deterministic route correction. Evidence verification is
|
|
515
|
+
opt-in (or automatic when evidence is supplied) and remains separate from route
|
|
516
|
+
selection.
|
|
490
517
|
|
|
491
518
|
| Layer | What | Model | Cost |
|
|
492
519
|
|---|---|---|---|
|
|
493
520
|
| **L1** | Crisis/medical safety gate | None (regex) | 0 ms |
|
|
494
|
-
| **L3-
|
|
521
|
+
| **L3-Registry** | Envelope validation + advertised-tool enforcement (all tiers) | None | 0 ms |
|
|
522
|
+
| **L3-Route** | Authenticated deterministic route correction (Standard+) | None | Network latency |
|
|
495
523
|
| **L3-Tier0** | Integer grounding (set membership) | None (deterministic) | 0 ms |
|
|
496
524
|
| **L3-Tier2** | NLI verifier (claim → ENTAILED/NEUTRAL/CONTRADICTED) | prism-coder:2b | ~200 ms |
|
|
497
525
|
| **L4** | Hallucination judge (opt-out for clinical) | prism-coder:4b | ~500 ms |
|
|
498
526
|
|
|
499
|
-
Fail-closed on the verified path: when the grounding verifier runs
|
|
527
|
+
Fail-closed on the verified path: when the grounding verifier runs, timeout,
|
|
528
|
+
ambiguity, or missing evidence yields a refusal, not pass-through. If the paid
|
|
529
|
+
route correction is unavailable, the local registry still blocks malformed
|
|
530
|
+
and unadvertised calls and reports an allowed preserved route as degraded.
|
|
500
531
|
|
|
501
532
|
---
|
|
502
533
|
|
package/dist/cli.js
CHANGED
|
@@ -12,6 +12,7 @@ import { configureClaudeAgentPolicy, configureClaudeNativeStartup, configureCode
|
|
|
12
12
|
import { runBrowserCli } from './browserCli.js';
|
|
13
13
|
import { filterPrismMemoryContext } from './utils/memoryQuality.js';
|
|
14
14
|
import { isRecoverableStartupStorageError } from './utils/startupRecovery.js';
|
|
15
|
+
import { verifyBehaviorHandler } from './tools/behavioralVerifierHandler.js';
|
|
15
16
|
const program = new Command();
|
|
16
17
|
/** Build the stable `prism load --json` envelope from depth-specific context. */
|
|
17
18
|
export function buildLoadJsonOutput(project, data, level, metadata) {
|
|
@@ -111,6 +112,50 @@ program
|
|
|
111
112
|
.command('bootstrap')
|
|
112
113
|
.description('Print the canonical dashboard-configured first-turn Prism greeting')
|
|
113
114
|
.action(runBootstrapCommand);
|
|
115
|
+
/**
|
|
116
|
+
* One-shot fallback for hosts whose long-lived MCP transport has closed.
|
|
117
|
+
* This calls the canonical handler, including its authenticated portal path
|
|
118
|
+
* and fail-closed offline scenario; it never asks the host to invent one.
|
|
119
|
+
*/
|
|
120
|
+
export async function runVerifyBehaviorCommand(options) {
|
|
121
|
+
try {
|
|
122
|
+
const baseUrl = process.env.PRISM_SYNALUX_BASE_URL?.trim() ||
|
|
123
|
+
process.env.SYNALUX_BASE_URL?.trim() ||
|
|
124
|
+
(await getSetting('PRISM_SYNALUX_BASE_URL', '')).trim() ||
|
|
125
|
+
(await getSetting('SYNALUX_BASE_URL', '')).trim() ||
|
|
126
|
+
'https://synalux.ai';
|
|
127
|
+
const apiKey = process.env.PRISM_SYNALUX_API_KEY?.trim() ||
|
|
128
|
+
(await getSetting('PRISM_SYNALUX_API_KEY', '')).trim();
|
|
129
|
+
process.env.PRISM_SYNALUX_BASE_URL = baseUrl.replace(/\/+$/, '');
|
|
130
|
+
if (apiKey)
|
|
131
|
+
process.env.PRISM_SYNALUX_API_KEY = apiKey;
|
|
132
|
+
const result = await verifyBehaviorHandler({
|
|
133
|
+
file_path: options.file,
|
|
134
|
+
change_summary: options.summary,
|
|
135
|
+
...(options.project ? { project: options.project } : {}),
|
|
136
|
+
...(options.workspaceId ? { workspace_id: options.workspaceId } : {}),
|
|
137
|
+
});
|
|
138
|
+
const output = result.content
|
|
139
|
+
?.map((part) => part?.text)
|
|
140
|
+
.filter(Boolean)
|
|
141
|
+
.join('\n') || '';
|
|
142
|
+
if (!output)
|
|
143
|
+
throw new Error('verify_behavior returned no scenario');
|
|
144
|
+
console.log(output);
|
|
145
|
+
}
|
|
146
|
+
catch (err) {
|
|
147
|
+
console.error(`Behavioral verification failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
148
|
+
process.exitCode = 1;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
program
|
|
152
|
+
.command('verify-behavior')
|
|
153
|
+
.description('Get the behavioral edit scenario when an MCP transport is unavailable')
|
|
154
|
+
.requiredOption('--file <path>', 'Path of the file about to be edited')
|
|
155
|
+
.requiredOption('--summary <text>', 'Brief description of the intended change')
|
|
156
|
+
.option('--project <project>', 'Project identifier for workspace-scoped scenarios')
|
|
157
|
+
.option('--workspace-id <id>', 'Workspace ID for custom scenarios')
|
|
158
|
+
.action(runVerifyBehaviorCommand);
|
|
114
159
|
// Parsed by the direct dispatch at the bottom so all Python CLI flags pass
|
|
115
160
|
// through unchanged. Registering it here keeps the command visible in help.
|
|
116
161
|
program
|
package/dist/connect.js
CHANGED
|
@@ -4,6 +4,7 @@ import { basename, dirname, isAbsolute, join, relative, resolve, sep, win32 as w
|
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { isDeepStrictEqual } from "node:util";
|
|
6
6
|
import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
|
|
7
|
+
import { EVIDENCE_WORKFLOW_POLICY_LINES } from "./evidenceWorkflowPolicy.js";
|
|
7
8
|
import { LOCAL_FIRST_POLICY_ID, LOCAL_FIRST_POLICY_LINES } from "./localFirstPolicy.js";
|
|
8
9
|
export const CONNECT_HOSTS = [
|
|
9
10
|
"claude-code",
|
|
@@ -46,6 +47,8 @@ const CODEX_STARTUP_BODY = [
|
|
|
46
47
|
"block is managed by `prism connect`; do not edit it manually.",
|
|
47
48
|
"",
|
|
48
49
|
...LOCAL_FIRST_POLICY_LINES,
|
|
50
|
+
"",
|
|
51
|
+
...EVIDENCE_WORKFLOW_POLICY_LINES,
|
|
49
52
|
];
|
|
50
53
|
const CONNECT_STORAGE_BACKENDS = ["auto", "local", "synalux", "supabase"];
|
|
51
54
|
const LEGACY_CLAUDE_PROJECT_PRISM_ENTRY = {
|
|
@@ -542,6 +545,8 @@ function serializeClaudeStartupBlock(newline) {
|
|
|
542
545
|
"managed by `prism connect`; do not edit it manually.",
|
|
543
546
|
"",
|
|
544
547
|
...LOCAL_FIRST_POLICY_LINES,
|
|
548
|
+
"",
|
|
549
|
+
...EVIDENCE_WORKFLOW_POLICY_LINES,
|
|
545
550
|
CLAUDE_STARTUP_MANAGED_END,
|
|
546
551
|
"",
|
|
547
552
|
].join(newline);
|
|
@@ -617,6 +622,8 @@ function serializeGeminiStartupBlock(newline) {
|
|
|
617
622
|
"session_detect_drift calls. This block is managed by `prism connect`; do not edit it manually.",
|
|
618
623
|
"",
|
|
619
624
|
...LOCAL_FIRST_POLICY_LINES,
|
|
625
|
+
"",
|
|
626
|
+
...EVIDENCE_WORKFLOW_POLICY_LINES,
|
|
620
627
|
GEMINI_STARTUP_MANAGED_END,
|
|
621
628
|
"",
|
|
622
629
|
].join(newline);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimum evidence contract shared by every MCP host.
|
|
3
|
+
*
|
|
4
|
+
* Native-skill hosts also receive the full evidence-first protocol. Keep this
|
|
5
|
+
* compact copy in MCP initialize instructions so hosts without a filesystem
|
|
6
|
+
* skill surface, including Claude Desktop, still follow the same workflow.
|
|
7
|
+
*/
|
|
8
|
+
export const EVIDENCE_WORKFLOW_POLICY_LINES = [
|
|
9
|
+
"## Prism evidence workflow",
|
|
10
|
+
"During diagnosis and editing, one trustworthy correlated reproduction is enough; do not block coding on",
|
|
11
|
+
"inspecting unrelated diagnostic screenshots, trace frames, or abandoned attempts.",
|
|
12
|
+
"Before a completion claim, push, or release, exercise the corrected path with fresh evidence from the current",
|
|
13
|
+
"build and bind stateful proof to the exact run and entity. Inspect every artifact used to support the claim.",
|
|
14
|
+
"When a screenshot is requested or used as proof, the active agent must open it and compare its visible state",
|
|
15
|
+
"with the issue's expected and forbidden states. Complete that review yourself; do not ask the user to verify it.",
|
|
16
|
+
"A screenshot is an observation, not absolute truth. Reject stale, wrong-run, wrong-entity, or visibly failing",
|
|
17
|
+
"evidence even when its metadata says passed.",
|
|
18
|
+
];
|
|
19
|
+
export const EVIDENCE_WORKFLOW_POLICY_TEXT = EVIDENCE_WORKFLOW_POLICY_LINES.join(" ");
|
package/dist/lifecycle.js
CHANGED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
const DEFAULT_KEEPALIVE_INTERVAL_MS = 60_000;
|
|
2
|
+
const DEFAULT_PING_TIMEOUT_MS = 15_000;
|
|
3
|
+
/**
|
|
4
|
+
* Keeps long-idle stdio MCP sessions active and converts a silently dead
|
|
5
|
+
* client transport into a clean server shutdown. Some hosts retain the child
|
|
6
|
+
* pipes after their protocol worker closes, so stdin "close" alone cannot
|
|
7
|
+
* detect the failure.
|
|
8
|
+
*/
|
|
9
|
+
export function monitorMcpTransport(server, options) {
|
|
10
|
+
const intervalMs = options.intervalMs ?? DEFAULT_KEEPALIVE_INTERVAL_MS;
|
|
11
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_PING_TIMEOUT_MS;
|
|
12
|
+
const previousOnClose = server.onclose;
|
|
13
|
+
let stopped = false;
|
|
14
|
+
let pingInFlight = false;
|
|
15
|
+
const stop = () => {
|
|
16
|
+
if (stopped)
|
|
17
|
+
return;
|
|
18
|
+
stopped = true;
|
|
19
|
+
clearInterval(timer);
|
|
20
|
+
if (server.onclose === handleClose) {
|
|
21
|
+
server.onclose = previousOnClose;
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
const fail = (reason, error) => {
|
|
25
|
+
if (stopped)
|
|
26
|
+
return;
|
|
27
|
+
stop();
|
|
28
|
+
options.onFailure(reason, error);
|
|
29
|
+
};
|
|
30
|
+
const handleClose = () => {
|
|
31
|
+
let closeError;
|
|
32
|
+
try {
|
|
33
|
+
previousOnClose?.();
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
closeError = error instanceof Error ? error : new Error(String(error));
|
|
37
|
+
}
|
|
38
|
+
finally {
|
|
39
|
+
fail("MCP_TRANSPORT_CLOSED", closeError);
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
server.onclose = handleClose;
|
|
43
|
+
const check = async () => {
|
|
44
|
+
if (stopped || pingInFlight)
|
|
45
|
+
return;
|
|
46
|
+
pingInFlight = true;
|
|
47
|
+
let timeout;
|
|
48
|
+
try {
|
|
49
|
+
await Promise.race([
|
|
50
|
+
server.ping(),
|
|
51
|
+
new Promise((_, reject) => {
|
|
52
|
+
timeout = setTimeout(() => reject(new Error(`MCP keepalive timed out after ${timeoutMs}ms`)), timeoutMs);
|
|
53
|
+
}),
|
|
54
|
+
]);
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
fail("MCP_KEEPALIVE_FAILED", error instanceof Error ? error : new Error(String(error)));
|
|
58
|
+
}
|
|
59
|
+
finally {
|
|
60
|
+
if (timeout)
|
|
61
|
+
clearTimeout(timeout);
|
|
62
|
+
pingInFlight = false;
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
const timer = setInterval(() => {
|
|
66
|
+
void check();
|
|
67
|
+
}, intervalMs);
|
|
68
|
+
timer.unref?.();
|
|
69
|
+
return stop;
|
|
70
|
+
}
|
package/dist/server.js
CHANGED
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
*/
|
|
40
40
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
41
41
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
42
|
+
import { monitorMcpTransport } from "./mcpTransportHealth.js";
|
|
42
43
|
import { buildVSCodePrompt } from "./aba-protocol.js";
|
|
43
44
|
import { CallToolRequestSchema, ListToolsRequestSchema,
|
|
44
45
|
// ─── v0.4.0: MCP Prompts support (Enhancement #1) ───
|
|
@@ -84,6 +85,7 @@ import { inferenceMetricsHandler } from "./utils/inferenceMetrics.js";
|
|
|
84
85
|
import { recordInvocation } from "./utils/analytics.js";
|
|
85
86
|
import { BOUNDARIES_TEXT } from "./boundaries/boundaries.js";
|
|
86
87
|
import { triggerSkillManifestSync } from "./skillManifestSync.js";
|
|
88
|
+
import { EVIDENCE_WORKFLOW_POLICY_TEXT } from "./evidenceWorkflowPolicy.js";
|
|
87
89
|
import { LOCAL_FIRST_POLICY_TEXT } from "./localFirstPolicy.js";
|
|
88
90
|
// ─── Import Tool Definitions (schemas) and Handlers (implementations) ─────
|
|
89
91
|
import { WEB_SEARCH_TOOL, BRAVE_WEB_SEARCH_CODE_MODE_TOOL, LOCAL_SEARCH_TOOL, BRAVE_LOCAL_SEARCH_CODE_MODE_TOOL, CODE_MODE_TRANSFORM_TOOL, BRAVE_ANSWERS_TOOL, RESEARCH_PAPER_ANALYSIS_TOOL, webSearchHandler, braveWebSearchCodeModeHandler, localSearchHandler, braveLocalSearchCodeModeHandler, codeModeTransformHandler, braveAnswersHandler, researchPaperAnalysisHandler, } from "./tools/index.js";
|
|
@@ -327,6 +329,7 @@ export const PRISM_SERVER_INSTRUCTIONS = `Prism MCP — The Mind Palace for AI A
|
|
|
327
329
|
`Reuse the conversation_id returned by session_bootstrap in structuredContent for those saves and for ` +
|
|
328
330
|
`session_detect_drift, the 60-minute goal-alignment drift check. Do not add the id to the visible greeting.\n\n` +
|
|
329
331
|
`${LOCAL_FIRST_POLICY_TEXT}\n\n` +
|
|
332
|
+
`${EVIDENCE_WORKFLOW_POLICY_TEXT}\n\n` +
|
|
330
333
|
`Architecture: session_save_ledger and session_save_handoff require context loaded by session_bootstrap ` +
|
|
331
334
|
`or session_load_context when a conversation_id is supplied. ${BOUNDARIES_TEXT} ` +
|
|
332
335
|
`All cloud inference routes through the Synalux portal for billing, tier-gating, and audit.`;
|
|
@@ -1252,7 +1255,15 @@ export async function startServer() {
|
|
|
1252
1255
|
// Register graceful shutdown handlers (SIGTERM, SIGINT, SIGHUP, stdin close).
|
|
1253
1256
|
// The stdin close handler is critical — when MCP clients disconnect, they
|
|
1254
1257
|
// often just close the pipe without sending a signal, leaving zombie processes.
|
|
1255
|
-
registerShutdownHandlers();
|
|
1258
|
+
const requestShutdown = registerShutdownHandlers();
|
|
1259
|
+
monitorMcpTransport(server, {
|
|
1260
|
+
onFailure: (reason, error) => {
|
|
1261
|
+
if (error) {
|
|
1262
|
+
console.error(`[Prism] MCP transport health check failed: ${error.message}`);
|
|
1263
|
+
}
|
|
1264
|
+
requestShutdown(reason);
|
|
1265
|
+
},
|
|
1266
|
+
});
|
|
1256
1267
|
// Pre-warm storage AFTER connecting — fired async so we never block the
|
|
1257
1268
|
// stdio handshake. Supabase REST initialization can take 500ms–5s; blocking
|
|
1258
1269
|
// on it before server.connect() was the root cause of the 1m 56s CLI delay.
|
|
@@ -4,7 +4,7 @@ import { access, lstat, mkdir, mkdtemp, open, readFile, readdir, readlink, realp
|
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
6
6
|
import { applyManagedSkillManifest, getSetting, refreshConfigStorageCache, } from "./storage/configStorage.js";
|
|
7
|
-
import { REQUIRED_NATIVE_SKILL_NAMES } from "./tools/skillRouting.js";
|
|
7
|
+
import { FREE_NATIVE_SKILL_NAMES, REQUIRED_NATIVE_SKILL_NAMES } from "./tools/skillRouting.js";
|
|
8
8
|
import { getSynaluxJwt, invalidateSynaluxJwt } from "./utils/synaluxJwt.js";
|
|
9
9
|
const OWNER = "prism-skill-sync-v1";
|
|
10
10
|
const MARKER = ".prism-managed.json";
|
|
@@ -181,17 +181,20 @@ export function validateSkillManifest(payload) {
|
|
|
181
181
|
}
|
|
182
182
|
}
|
|
183
183
|
}
|
|
184
|
-
const
|
|
185
|
-
|
|
184
|
+
const requiredForTier = value.tier === "free"
|
|
185
|
+
? FREE_NATIVE_SKILL_NAMES
|
|
186
|
+
: REQUIRED_NATIVE_SKILL_NAMES;
|
|
187
|
+
const requiredNames = new Set(requiredForTier);
|
|
188
|
+
for (const required of requiredForTier) {
|
|
186
189
|
const requiredSkill = skills.find((skill) => skill.name === required);
|
|
187
190
|
if (!requiredSkill)
|
|
188
|
-
throw new Error(`manifest is missing required
|
|
191
|
+
throw new Error(`manifest is missing required native skill: ${required}`);
|
|
189
192
|
if (!requiredSkill.metadata.protected || !requiredSkill.metadata.categories.includes("universal")) {
|
|
190
|
-
throw new Error(`required skill is not protected universal: ${required}`);
|
|
193
|
+
throw new Error(`required native skill is not protected universal: ${required}`);
|
|
191
194
|
}
|
|
192
195
|
}
|
|
193
196
|
if (value.tier === "free" && (skills.length !== requiredNames.size || skills.some((skill) => !requiredNames.has(skill.name)))) {
|
|
194
|
-
throw new Error("free manifest must contain exactly the
|
|
197
|
+
throw new Error("free manifest must contain exactly the public startup package");
|
|
195
198
|
}
|
|
196
199
|
const normalized = {
|
|
197
200
|
schema_version: 1,
|
package/dist/storage/synalux.js
CHANGED
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
* Methods migrated to portal:
|
|
21
21
|
* - saveLedger → POST /api/v1/prism/memory action=save_ledger
|
|
22
22
|
* - saveHandoff → POST /api/v1/prism/memory action=save_handoff
|
|
23
|
+
* - saveHistorySnapshot → POST /api/v1/prism/memory action=save_history_snapshot
|
|
23
24
|
* - loadContext → POST /api/v1/prism/memory action=load_context
|
|
24
25
|
* - searchKnowledge → POST /api/v1/prism/memory action=search
|
|
25
26
|
* - softDeleteLedger → POST /api/v1/prism/memory action=forget_memory (Phase 3 Tier A)
|
|
@@ -203,7 +204,46 @@ export class SynaluxStorage extends SupabaseStorage {
|
|
|
203
204
|
role: handoff.role,
|
|
204
205
|
expected_version: expectedVersion ?? undefined,
|
|
205
206
|
});
|
|
206
|
-
|
|
207
|
+
const candidate = Object.prototype.hasOwnProperty.call(result, "result")
|
|
208
|
+
? result.result
|
|
209
|
+
: Object.prototype.hasOwnProperty.call(result, "handoff")
|
|
210
|
+
? result.handoff
|
|
211
|
+
: result;
|
|
212
|
+
if (!candidate || typeof candidate !== "object" || Array.isArray(candidate)) {
|
|
213
|
+
throw new Error("[SynaluxStorage] Invalid save_handoff response: missing result");
|
|
214
|
+
}
|
|
215
|
+
const value = candidate;
|
|
216
|
+
if (value.status === "conflict" && Number.isSafeInteger(value.current_version)) {
|
|
217
|
+
return {
|
|
218
|
+
status: "conflict",
|
|
219
|
+
current_version: value.current_version,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
if ((value.status === "created" || value.status === "updated")
|
|
223
|
+
&& Number.isSafeInteger(value.version)) {
|
|
224
|
+
return {
|
|
225
|
+
status: value.status,
|
|
226
|
+
version: value.version,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
// Older portal RPC wrappers returned only the new version. Preserve that
|
|
230
|
+
// rolling-upgrade contract without accepting an unversioned success.
|
|
231
|
+
if (value.status === undefined && Number.isSafeInteger(value.version)) {
|
|
232
|
+
return {
|
|
233
|
+
status: "updated",
|
|
234
|
+
version: value.version,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
throw new Error("[SynaluxStorage] Invalid save_handoff response: malformed OCC result");
|
|
238
|
+
}
|
|
239
|
+
async saveHistorySnapshot(handoff, branch = "main") {
|
|
240
|
+
await this.portalPost("/api/v1/prism/memory", {
|
|
241
|
+
action: "save_history_snapshot",
|
|
242
|
+
project: handoff.project,
|
|
243
|
+
version: handoff.version,
|
|
244
|
+
snapshot: handoff,
|
|
245
|
+
branch,
|
|
246
|
+
});
|
|
207
247
|
}
|
|
208
248
|
// ─── Context ─────────────────────────────────────────────────
|
|
209
249
|
async loadContext(project, level, userId, role) {
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* FAIL-CLOSED: if the portal is unreachable, returns a generic
|
|
9
9
|
* verification challenge rather than skipping verification.
|
|
10
10
|
*/
|
|
11
|
-
import { PRISM_SYNALUX_BASE_URL
|
|
11
|
+
import { PRISM_SYNALUX_BASE_URL } from "../config.js";
|
|
12
12
|
import { getSynaluxJwt } from "../utils/synaluxJwt.js";
|
|
13
13
|
const FALLBACK_SCENARIO = [
|
|
14
14
|
"⚠️ BEHAVIORAL VERIFICATION (OFFLINE MODE)",
|
|
@@ -33,7 +33,13 @@ export async function verifyBehaviorHandler(args) {
|
|
|
33
33
|
return { content: [{ type: "text", text: await buildScenarioText(args) }] };
|
|
34
34
|
}
|
|
35
35
|
async function buildScenarioText(args) {
|
|
36
|
-
|
|
36
|
+
const baseUrl = process.env.PRISM_SYNALUX_BASE_URL?.trim() ||
|
|
37
|
+
process.env.SYNALUX_BASE_URL?.trim() ||
|
|
38
|
+
PRISM_SYNALUX_BASE_URL;
|
|
39
|
+
// OAuth/JWT-backed installs intentionally do not copy a long-lived API key
|
|
40
|
+
// into host configuration. A valid portal URL is enough to attempt the
|
|
41
|
+
// short-lived JWT flow; getSynaluxJwt() remains the fail-closed auth gate.
|
|
42
|
+
if (!baseUrl) {
|
|
37
43
|
return FALLBACK_SCENARIO;
|
|
38
44
|
}
|
|
39
45
|
const jwt = await getSynaluxJwt();
|
|
@@ -42,7 +48,7 @@ async function buildScenarioText(args) {
|
|
|
42
48
|
return FALLBACK_SCENARIO;
|
|
43
49
|
}
|
|
44
50
|
try {
|
|
45
|
-
const url = `${
|
|
51
|
+
const url = `${baseUrl.replace(/\/+$/, "")}/api/v1/prism/verify-behavior`;
|
|
46
52
|
const res = await fetch(url, {
|
|
47
53
|
method: "POST",
|
|
48
54
|
headers: {
|
|
@@ -60,7 +66,11 @@ async function buildScenarioText(args) {
|
|
|
60
66
|
console.error(`[verify-behavior] ⚠️ portal returned ${res.status} — fail-closed. URL: ${url}`);
|
|
61
67
|
return FALLBACK_SCENARIO;
|
|
62
68
|
}
|
|
63
|
-
const data =
|
|
69
|
+
const data = await res.json();
|
|
70
|
+
if (!isVerifyBehaviorResult(data)) {
|
|
71
|
+
console.error("[verify-behavior] ⚠️ portal returned a malformed response — fail-closed");
|
|
72
|
+
return FALLBACK_SCENARIO;
|
|
73
|
+
}
|
|
64
74
|
return formatResult(data);
|
|
65
75
|
}
|
|
66
76
|
catch (err) {
|
|
@@ -68,6 +78,12 @@ async function buildScenarioText(args) {
|
|
|
68
78
|
return FALLBACK_SCENARIO;
|
|
69
79
|
}
|
|
70
80
|
}
|
|
81
|
+
function isVerifyBehaviorResult(value) {
|
|
82
|
+
return typeof value === "object" &&
|
|
83
|
+
value !== null &&
|
|
84
|
+
!Array.isArray(value) &&
|
|
85
|
+
typeof value.requires_verification === "boolean";
|
|
86
|
+
}
|
|
71
87
|
function formatResult(data) {
|
|
72
88
|
if (!data.requires_verification) {
|
|
73
89
|
return JSON.stringify({ requires_verification: false, reason: data.reason || "non-behavioral file" });
|