prism-mcp-server 20.0.5 → 20.0.7
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/agent/agentTools.js +453 -0
- package/dist/agent/mcpBridge.js +234 -0
- package/dist/agent/platformUtils.js +470 -0
- package/dist/agent/terminalUI.js +198 -0
- package/dist/auth.js +218 -0
- package/dist/darkfactory/cloudDelegate.js +173 -0
- package/dist/plugins/pluginManager.js +199 -0
- package/dist/prism-cloud.js +110 -0
- package/dist/scm/ciPipeline.js +220 -0
- package/dist/server.js +1 -1
- package/dist/storage/inferMetricsLedger.js +159 -0
- package/dist/sync/encryptedSync.js +172 -0
- package/dist/sync/synaluxProxy.js +177 -0
- package/dist/tools/__tests__/layer1Integration.test.js +34 -0
- package/dist/tools/adaptiveDefinitions.js +148 -0
- package/dist/tools/interactiveDefinitions.js +87 -0
- package/dist/tools/interactiveHandlers.js +164 -0
- package/dist/tools/ledgerHandlers.js +40 -13
- package/dist/tools/prismInferHandler.js +48 -6
- package/dist/tools/projects.js +214 -0
- package/dist/tools/sessionMemoryDefinitions.js +12 -5
- package/dist/tools/skillRouting.js +57 -8
- package/dist/utils/changelogGenerator.js +158 -0
- package/dist/utils/fallbackClient.js +52 -0
- package/dist/utils/groundingVerifier.js +3 -3
- package/dist/utils/inferenceMetrics.js +38 -1
- package/dist/utils/memoryAttestation.js +163 -0
- package/dist/utils/rbac.js +321 -0
- package/dist/utils/skillBudget.js +58 -0
- package/dist/utils/tavilyApi.js +70 -0
- package/dist/vm/quotaEnforcer.js +192 -0
- package/package.json +1 -1
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v12.4: AI-Generated Changelogs from Session Ledger History
|
|
3
|
+
*
|
|
4
|
+
* Reads session ledger entries and generates human-readable changelogs
|
|
5
|
+
* in Keep a Changelog format. Uses local LLM (if available) for
|
|
6
|
+
* natural language summarization, with rule-based fallback.
|
|
7
|
+
*/
|
|
8
|
+
import { debugLog } from "./logger.js";
|
|
9
|
+
// ─── Changelog Generation ────────────────────────────────────
|
|
10
|
+
/**
|
|
11
|
+
* Classify a ledger summary into a changelog section.
|
|
12
|
+
*/
|
|
13
|
+
export function classifySummary(summary) {
|
|
14
|
+
const lower = summary.toLowerCase();
|
|
15
|
+
if (/\b(fix|bug|patch|resolve|repair|correct)\b/.test(lower))
|
|
16
|
+
return "fixed";
|
|
17
|
+
if (/\b(remov|delet|drop|deprecat|rip out)\b/.test(lower))
|
|
18
|
+
return "removed";
|
|
19
|
+
if (/\b(secur|vulnerab|cve|auth|encrypt|permission)\b/.test(lower))
|
|
20
|
+
return "security";
|
|
21
|
+
if (/\b(deprecat|sunset|legacy|phase out)\b/.test(lower))
|
|
22
|
+
return "deprecated";
|
|
23
|
+
if (/\b(add|creat|new|implement|introduc|support|feat)\b/.test(lower))
|
|
24
|
+
return "added";
|
|
25
|
+
return "changed";
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Generate a changelog from ledger entries (rule-based).
|
|
29
|
+
*/
|
|
30
|
+
export function generateChangelog(entries, options = {}) {
|
|
31
|
+
const { fromDate, toDate, groupBy = "date", includeFileChanges = false, } = options;
|
|
32
|
+
// Filter by date range
|
|
33
|
+
let filtered = entries;
|
|
34
|
+
if (fromDate) {
|
|
35
|
+
filtered = filtered.filter(e => new Date(e.created_at) >= new Date(fromDate));
|
|
36
|
+
}
|
|
37
|
+
if (toDate) {
|
|
38
|
+
filtered = filtered.filter(e => new Date(e.created_at) <= new Date(toDate));
|
|
39
|
+
}
|
|
40
|
+
// Sort by date (newest first)
|
|
41
|
+
filtered.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime());
|
|
42
|
+
const sections = {
|
|
43
|
+
added: [],
|
|
44
|
+
changed: [],
|
|
45
|
+
fixed: [],
|
|
46
|
+
removed: [],
|
|
47
|
+
security: [],
|
|
48
|
+
deprecated: [],
|
|
49
|
+
};
|
|
50
|
+
for (const entry of filtered) {
|
|
51
|
+
const section = classifySummary(entry.summary);
|
|
52
|
+
let line = entry.summary;
|
|
53
|
+
if (includeFileChanges && entry.files_changed?.length) {
|
|
54
|
+
line += ` (${entry.files_changed.join(", ")})`;
|
|
55
|
+
}
|
|
56
|
+
sections[section].push(line);
|
|
57
|
+
// Also classify decisions
|
|
58
|
+
if (entry.decisions) {
|
|
59
|
+
for (const decision of entry.decisions) {
|
|
60
|
+
const dSection = classifySummary(decision);
|
|
61
|
+
sections[dSection].push(`Decision: ${decision}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const now = new Date().toISOString().split("T")[0];
|
|
66
|
+
return {
|
|
67
|
+
version: "Unreleased",
|
|
68
|
+
date: now,
|
|
69
|
+
sections,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Format a changelog entry as Markdown (Keep a Changelog format).
|
|
74
|
+
*/
|
|
75
|
+
export function formatChangelogMarkdown(entry) {
|
|
76
|
+
const lines = [];
|
|
77
|
+
lines.push(`## [${entry.version}] - ${entry.date}`);
|
|
78
|
+
lines.push("");
|
|
79
|
+
const sectionNames = [
|
|
80
|
+
["added", "Added"],
|
|
81
|
+
["changed", "Changed"],
|
|
82
|
+
["fixed", "Fixed"],
|
|
83
|
+
["removed", "Removed"],
|
|
84
|
+
["security", "Security"],
|
|
85
|
+
["deprecated", "Deprecated"],
|
|
86
|
+
];
|
|
87
|
+
for (const [key, label] of sectionNames) {
|
|
88
|
+
if (entry.sections[key].length > 0) {
|
|
89
|
+
lines.push(`### ${label}`);
|
|
90
|
+
for (const item of entry.sections[key]) {
|
|
91
|
+
lines.push(`- ${item}`);
|
|
92
|
+
}
|
|
93
|
+
lines.push("");
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return lines.join("\n");
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Format a changelog entry as HTML.
|
|
100
|
+
*/
|
|
101
|
+
export function formatChangelogHtml(entry) {
|
|
102
|
+
let html = `<h2>[${entry.version}] - ${entry.date}</h2>\n`;
|
|
103
|
+
const sectionNames = [
|
|
104
|
+
["added", "Added"],
|
|
105
|
+
["changed", "Changed"],
|
|
106
|
+
["fixed", "Fixed"],
|
|
107
|
+
["removed", "Removed"],
|
|
108
|
+
["security", "Security"],
|
|
109
|
+
["deprecated", "Deprecated"],
|
|
110
|
+
];
|
|
111
|
+
for (const [key, label] of sectionNames) {
|
|
112
|
+
if (entry.sections[key].length > 0) {
|
|
113
|
+
html += `<h3>${label}</h3>\n<ul>\n`;
|
|
114
|
+
for (const item of entry.sections[key]) {
|
|
115
|
+
html += ` <li>${escapeHtml(item)}</li>\n`;
|
|
116
|
+
}
|
|
117
|
+
html += `</ul>\n`;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return html;
|
|
121
|
+
}
|
|
122
|
+
function escapeHtml(str) {
|
|
123
|
+
return str
|
|
124
|
+
.replace(/&/g, "&")
|
|
125
|
+
.replace(/</g, "<")
|
|
126
|
+
.replace(/>/g, ">")
|
|
127
|
+
.replace(/"/g, """);
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Generate a changelog with optional LLM summarization.
|
|
131
|
+
*/
|
|
132
|
+
export async function generateChangelogWithLlm(entries, options = {}) {
|
|
133
|
+
const changelog = generateChangelog(entries, options);
|
|
134
|
+
const format = options.format || "markdown";
|
|
135
|
+
if (!options.useLlm) {
|
|
136
|
+
return format === "html"
|
|
137
|
+
? formatChangelogHtml(changelog)
|
|
138
|
+
: format === "json"
|
|
139
|
+
? JSON.stringify(changelog, null, 2)
|
|
140
|
+
: formatChangelogMarkdown(changelog);
|
|
141
|
+
}
|
|
142
|
+
// LLM-enhanced: summarize each section
|
|
143
|
+
try {
|
|
144
|
+
const { callLocalLlm } = await import("./localLlm.js");
|
|
145
|
+
const rawMarkdown = formatChangelogMarkdown(changelog);
|
|
146
|
+
const enhanced = await callLocalLlm(`You are a technical writer. Rewrite this changelog to be more concise and professional. ` +
|
|
147
|
+
`Keep the Keep a Changelog format. Combine duplicate entries. ` +
|
|
148
|
+
`Use action verbs (Added, Fixed, Changed). Output markdown only:\n\n${rawMarkdown}`);
|
|
149
|
+
return enhanced || rawMarkdown;
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
debugLog("Changelog: LLM unavailable, using rule-based output");
|
|
153
|
+
return format === "html"
|
|
154
|
+
? formatChangelogHtml(changelog)
|
|
155
|
+
: formatChangelogMarkdown(changelog);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
debugLog("v12.4: Changelog generator loaded");
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Supabase Direct Fallback Client
|
|
3
|
+
*
|
|
4
|
+
* When prism-mcp Railway/Fly endpoints are unreachable, this module
|
|
5
|
+
* allows critical tools (load_context, save_ledger, save_handoff) to
|
|
6
|
+
* call Supabase REST API directly — bypassing the prism-mcp server.
|
|
7
|
+
*
|
|
8
|
+
* Used by smithery-bridge.mjs health monitor: if /healthz fails 3× in
|
|
9
|
+
* a row, responses include a Retry-After + x-fallback-mode: supabase
|
|
10
|
+
* header so Claude Code can switch to direct-Supabase mode.
|
|
11
|
+
*
|
|
12
|
+
* Architecture:
|
|
13
|
+
* Normal: Agent → Railway prism-mcp → Supabase
|
|
14
|
+
* Fallback: Agent → Supabase REST API directly (read-only for safety)
|
|
15
|
+
*/
|
|
16
|
+
const SUPABASE_URL = process.env.SUPABASE_URL || '';
|
|
17
|
+
const SUPABASE_ANON_KEY = process.env.SUPABASE_ANON_KEY || '';
|
|
18
|
+
/**
|
|
19
|
+
* Direct Supabase read — returns last handoff for a project.
|
|
20
|
+
* Called when prism-mcp is unreachable.
|
|
21
|
+
*/
|
|
22
|
+
export async function directLoadContext(project) {
|
|
23
|
+
if (!SUPABASE_URL || !SUPABASE_ANON_KEY)
|
|
24
|
+
return null;
|
|
25
|
+
try {
|
|
26
|
+
const res = await fetch(`${SUPABASE_URL}/rest/v1/session_handoffs?project=eq.${encodeURIComponent(project)}&order=updated_at.desc&limit=1`, { headers: { apikey: SUPABASE_ANON_KEY, Authorization: `Bearer ${SUPABASE_ANON_KEY}` } });
|
|
27
|
+
if (!res.ok)
|
|
28
|
+
return null;
|
|
29
|
+
const [row] = await res.json();
|
|
30
|
+
if (!row)
|
|
31
|
+
return null;
|
|
32
|
+
return {
|
|
33
|
+
project,
|
|
34
|
+
lastSummary: row.last_summary ?? '',
|
|
35
|
+
openTodos: row.open_todos ?? [],
|
|
36
|
+
keyContext: row.key_context ?? '',
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/** Returns true if Railway prism-mcp primary is reachable. */
|
|
44
|
+
export async function isPrimaryHealthy(primaryUrl) {
|
|
45
|
+
try {
|
|
46
|
+
const res = await fetch(`${primaryUrl}/healthz`, { signal: AbortSignal.timeout(3000) });
|
|
47
|
+
return res.ok;
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -9,9 +9,9 @@
|
|
|
9
9
|
* stateless MCP), pointed at free-form generation instead of tool-call
|
|
10
10
|
* responses.
|
|
11
11
|
*
|
|
12
|
-
* Cascade role:
|
|
12
|
+
* Cascade role: prism-coder:4b is the default verifier (fast, 2.5GB).
|
|
13
13
|
* 14b drafts; 4b verifies. Different model = Patronus rule satisfied.
|
|
14
|
-
* Falls back to
|
|
14
|
+
* Falls back to 1b7 on devices with <4GB free RAM.
|
|
15
15
|
*
|
|
16
16
|
* Failure modes:
|
|
17
17
|
* - Verifier model unreachable / timeout → fail-closed refusal
|
|
@@ -93,7 +93,7 @@ function refusalText(action, failedClaim) {
|
|
|
93
93
|
}
|
|
94
94
|
}
|
|
95
95
|
export async function verifyGrounding(opts) {
|
|
96
|
-
const verifierModel = opts.verifierModel ?? "
|
|
96
|
+
const verifierModel = opts.verifierModel ?? "prism-coder:4b";
|
|
97
97
|
const timeoutMs = opts.timeoutMs ?? 2000;
|
|
98
98
|
const ollamaUrl = opts.ollamaUrl ?? PRISM_LOCAL_LLM_URL;
|
|
99
99
|
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* undercounts on repeated system-prompt calls; submittedEst shows actual load.
|
|
13
13
|
*/
|
|
14
14
|
import { debugLog } from "./logger.js";
|
|
15
|
+
import { appendInferMetric, queryInferMetrics } from "../storage/inferMetricsLedger.js";
|
|
15
16
|
// T1 fix: content-aware token estimator. Replaces flat text.length / 4 which
|
|
16
17
|
// underestimates emoji (~2 UTF-16 units but 1.5-2.5 BPE tokens) and CJK
|
|
17
18
|
// (~1 char ≈ 1 token) by 15-40%, and overestimates dense code (~3.3 chars/token).
|
|
@@ -52,6 +53,19 @@ export function recordPromptSeen(delegated) {
|
|
|
52
53
|
export function recordInference(result) {
|
|
53
54
|
if (result.backend === "safety_gate")
|
|
54
55
|
return;
|
|
56
|
+
// Durable ledger row (fire-and-forget; in-memory counters below remain the
|
|
57
|
+
// per-session view). safety_gate is excluded by the early return above.
|
|
58
|
+
appendInferMetric({
|
|
59
|
+
backend: result.backend,
|
|
60
|
+
model: result.model_picked,
|
|
61
|
+
used_cloud: result.used_cloud,
|
|
62
|
+
mode: result.mode,
|
|
63
|
+
gate_outcome: result.quality_gate_failed ? "gate_failed_served" : undefined,
|
|
64
|
+
prompt_tokens: result.prompt_tokens,
|
|
65
|
+
completion_tokens: result.completion_tokens,
|
|
66
|
+
latency_ms: result.latency_ms,
|
|
67
|
+
ram_free_mb: result.ram_free_mb,
|
|
68
|
+
});
|
|
55
69
|
const key = result.model_picked ?? result.backend;
|
|
56
70
|
if (result.used_cloud) {
|
|
57
71
|
cloudCalls++;
|
|
@@ -135,7 +149,30 @@ export function resetInferenceMetrics() {
|
|
|
135
149
|
}
|
|
136
150
|
debugLog("[inference-metrics] Session metrics reset");
|
|
137
151
|
}
|
|
138
|
-
export async function inferenceMetricsHandler() {
|
|
152
|
+
export async function inferenceMetricsHandler(args) {
|
|
153
|
+
if (args?.period === "all") {
|
|
154
|
+
const agg = await queryInferMetrics();
|
|
155
|
+
if (!agg || agg.total === 0) {
|
|
156
|
+
return { content: [{ type: "text", text: "No persisted prism_infer calls yet (ledger empty)." }] };
|
|
157
|
+
}
|
|
158
|
+
const localPct = agg.total ? Math.round((agg.local / agg.total) * 100) : 0;
|
|
159
|
+
const cloudPct = agg.total ? Math.round((agg.cloud / agg.total) * 100) : 0;
|
|
160
|
+
const span = agg.first_ts && agg.last_ts
|
|
161
|
+
? `${new Date(agg.first_ts).toISOString().slice(0, 10)} → ${new Date(agg.last_ts).toISOString().slice(0, 10)}`
|
|
162
|
+
: "n/a";
|
|
163
|
+
const byB = Object.entries(agg.by_backend)
|
|
164
|
+
.sort((a, b) => b[1] - a[1])
|
|
165
|
+
.map(([k, v]) => ` ${k}: ${v}`).join("\n");
|
|
166
|
+
return {
|
|
167
|
+
content: [{
|
|
168
|
+
type: "text",
|
|
169
|
+
text: `📊 Delegation Metrics — ALL TIME (persisted ledger, ${span})\n` +
|
|
170
|
+
`Total calls: ${agg.total} — Local: ${agg.local} (${localPct}%) | Cloud: ${agg.cloud} (${cloudPct}%)\n` +
|
|
171
|
+
`Prompt tokens: ${agg.prompt_tokens} | Completion tokens: ${agg.completion_tokens}\n` +
|
|
172
|
+
`Avg latency: ${agg.avg_latency_ms}ms\nBy backend:\n${byB}`,
|
|
173
|
+
}],
|
|
174
|
+
};
|
|
175
|
+
}
|
|
139
176
|
const block = formatInferenceMetrics();
|
|
140
177
|
return {
|
|
141
178
|
content: [{
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v12.4: Memory Attestation — SHA-256 Merkle Tree
|
|
3
|
+
*
|
|
4
|
+
* Provides cryptographic proof of memory integrity.
|
|
5
|
+
* Builds a Merkle tree from session entries, enabling:
|
|
6
|
+
* - Tamper detection on individual entries
|
|
7
|
+
* - Provable audit trail for compliance
|
|
8
|
+
* - Efficient partial verification
|
|
9
|
+
*/
|
|
10
|
+
import { createHash } from "node:crypto";
|
|
11
|
+
import { debugLog } from "./logger.js";
|
|
12
|
+
// ─── Hashing ─────────────────────────────────────────────────
|
|
13
|
+
/**
|
|
14
|
+
* SHA-256 hash of a string.
|
|
15
|
+
*/
|
|
16
|
+
export function sha256(data) {
|
|
17
|
+
return createHash("sha256").update(data, "utf8").digest("hex");
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Hash two child hashes into a parent hash.
|
|
21
|
+
*/
|
|
22
|
+
export function hashPair(left, right) {
|
|
23
|
+
return sha256(left + right);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Hash a memory entry's content for use as a leaf node.
|
|
27
|
+
*/
|
|
28
|
+
export function hashEntry(entryId, content, timestamp) {
|
|
29
|
+
return sha256(`${entryId}:${content}:${timestamp}`);
|
|
30
|
+
}
|
|
31
|
+
// ─── Merkle Tree Construction ────────────────────────────────
|
|
32
|
+
/**
|
|
33
|
+
* Build a Merkle tree from leaf hashes.
|
|
34
|
+
*/
|
|
35
|
+
export function buildMerkleTree(entries) {
|
|
36
|
+
if (entries.length === 0)
|
|
37
|
+
return null;
|
|
38
|
+
// Create leaf nodes
|
|
39
|
+
let nodes = entries.map(e => ({
|
|
40
|
+
hash: e.hash,
|
|
41
|
+
entryId: e.id,
|
|
42
|
+
depth: 0,
|
|
43
|
+
}));
|
|
44
|
+
// If odd number of leaves, duplicate the last one
|
|
45
|
+
if (nodes.length % 2 !== 0) {
|
|
46
|
+
nodes.push({ ...nodes[nodes.length - 1] });
|
|
47
|
+
}
|
|
48
|
+
let depth = 0;
|
|
49
|
+
// Build tree bottom-up
|
|
50
|
+
while (nodes.length > 1) {
|
|
51
|
+
depth++;
|
|
52
|
+
const next = [];
|
|
53
|
+
for (let i = 0; i < nodes.length; i += 2) {
|
|
54
|
+
const left = nodes[i];
|
|
55
|
+
const right = nodes[i + 1] || left; // Duplicate last if odd
|
|
56
|
+
next.push({
|
|
57
|
+
hash: hashPair(left.hash, right.hash),
|
|
58
|
+
left,
|
|
59
|
+
right,
|
|
60
|
+
depth,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
nodes = next;
|
|
64
|
+
}
|
|
65
|
+
return nodes[0];
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Get the Merkle root hash.
|
|
69
|
+
*/
|
|
70
|
+
export function getMerkleRoot(entries) {
|
|
71
|
+
const tree = buildMerkleTree(entries);
|
|
72
|
+
return tree?.hash || sha256("empty");
|
|
73
|
+
}
|
|
74
|
+
// ─── Proof Generation & Verification ─────────────────────────
|
|
75
|
+
/**
|
|
76
|
+
* Generate a Merkle proof for a specific entry.
|
|
77
|
+
*/
|
|
78
|
+
export function generateProof(entries, targetEntryId) {
|
|
79
|
+
const targetIdx = entries.findIndex(e => e.id === targetEntryId);
|
|
80
|
+
if (targetIdx === -1)
|
|
81
|
+
return null;
|
|
82
|
+
// Pad to even length
|
|
83
|
+
const padded = [...entries];
|
|
84
|
+
if (padded.length % 2 !== 0) {
|
|
85
|
+
padded.push({ ...padded[padded.length - 1] });
|
|
86
|
+
}
|
|
87
|
+
const proof = [];
|
|
88
|
+
let idx = targetIdx;
|
|
89
|
+
let currentLevel = padded.map(e => e.hash);
|
|
90
|
+
while (currentLevel.length > 1) {
|
|
91
|
+
const nextLevel = [];
|
|
92
|
+
const siblingIdx = idx % 2 === 0 ? idx + 1 : idx - 1;
|
|
93
|
+
if (siblingIdx < currentLevel.length) {
|
|
94
|
+
proof.push({
|
|
95
|
+
hash: currentLevel[siblingIdx],
|
|
96
|
+
position: idx % 2 === 0 ? "right" : "left",
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
for (let i = 0; i < currentLevel.length; i += 2) {
|
|
100
|
+
const left = currentLevel[i];
|
|
101
|
+
const right = currentLevel[i + 1] || left;
|
|
102
|
+
nextLevel.push(hashPair(left, right));
|
|
103
|
+
}
|
|
104
|
+
idx = Math.floor(idx / 2);
|
|
105
|
+
currentLevel = nextLevel;
|
|
106
|
+
}
|
|
107
|
+
const root = currentLevel[0];
|
|
108
|
+
const entryHash = entries[targetIdx].hash;
|
|
109
|
+
return {
|
|
110
|
+
entryId: targetEntryId,
|
|
111
|
+
entryHash,
|
|
112
|
+
proof,
|
|
113
|
+
root,
|
|
114
|
+
verified: verifyProof(entryHash, proof, root),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Verify a Merkle proof.
|
|
119
|
+
*/
|
|
120
|
+
export function verifyProof(entryHash, proof, expectedRoot) {
|
|
121
|
+
let currentHash = entryHash;
|
|
122
|
+
for (const step of proof) {
|
|
123
|
+
if (step.position === "right") {
|
|
124
|
+
currentHash = hashPair(currentHash, step.hash);
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
currentHash = hashPair(step.hash, currentHash);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return currentHash === expectedRoot;
|
|
131
|
+
}
|
|
132
|
+
// ─── Attestation Report ──────────────────────────────────────
|
|
133
|
+
/**
|
|
134
|
+
* Generate a full attestation report for a project's memory.
|
|
135
|
+
*/
|
|
136
|
+
export function generateAttestationReport(project, entries) {
|
|
137
|
+
const hashedEntries = entries.map(e => ({
|
|
138
|
+
id: e.id,
|
|
139
|
+
hash: hashEntry(e.id, e.content, e.timestamp),
|
|
140
|
+
}));
|
|
141
|
+
const tree = buildMerkleTree(hashedEntries);
|
|
142
|
+
const report = {
|
|
143
|
+
root: tree?.hash || sha256("empty"),
|
|
144
|
+
entryCount: entries.length,
|
|
145
|
+
treeDepth: tree?.depth || 0,
|
|
146
|
+
generatedAt: new Date().toISOString(),
|
|
147
|
+
project,
|
|
148
|
+
entries: hashedEntries,
|
|
149
|
+
};
|
|
150
|
+
debugLog(`Attestation: Generated report for '${project}' — ${entries.length} entries, root: ${report.root.slice(0, 16)}...`);
|
|
151
|
+
return report;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Verify that a single entry hasn't been tampered with.
|
|
155
|
+
*/
|
|
156
|
+
export function verifyEntry(entryId, content, timestamp, report) {
|
|
157
|
+
const expectedHash = hashEntry(entryId, content, timestamp);
|
|
158
|
+
const storedEntry = report.entries.find(e => e.id === entryId);
|
|
159
|
+
if (!storedEntry)
|
|
160
|
+
return false;
|
|
161
|
+
return storedEntry.hash === expectedHash;
|
|
162
|
+
}
|
|
163
|
+
debugLog("v12.4: Memory attestation (Merkle tree) loaded");
|