@moonglade/provenance 0.0.0-stage → 2.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/NOTICE.md +9 -0
- package/README.md +92 -2
- package/SPEC.md +176 -0
- package/TIMESTAMP.md +21 -0
- package/dist/artifact-cli.d.ts +2 -0
- package/dist/artifact-cli.js +113 -0
- package/dist/artifactEnvelope.d.ts +38 -0
- package/dist/artifactEnvelope.js +61 -0
- package/dist/artifactHook.d.ts +30 -0
- package/dist/artifactHook.js +114 -0
- package/dist/badge.d.ts +83 -0
- package/dist/badge.js +403 -0
- package/dist/hookInstaller.d.ts +75 -0
- package/dist/hookInstaller.js +410 -0
- package/dist/index.d.ts +228 -0
- package/dist/index.js +782 -0
- package/dist/mcp.d.ts +87 -0
- package/dist/mcp.js +352 -0
- package/dist/merkle.d.ts +71 -0
- package/dist/merkle.js +176 -0
- package/dist/self-test.d.ts +21 -0
- package/dist/self-test.js +280 -0
- package/dist/timestamp.d.ts +274 -0
- package/dist/timestamp.js +1103 -0
- package/package.json +80 -4
- package/trust/README.md +19 -0
- package/trust/registry.json +12 -0
- package/trust/root-public.asc +10 -0
- package/wasm/mgc_wasm.d.ts +53 -0
- package/wasm/mgc_wasm.js +388 -0
- package/wasm/mgc_wasm_bg.wasm +0 -0
- package/wasm/mgc_wasm_bg.wasm.d.ts +20 -0
package/dist/mcp.d.ts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
export declare const PACKAGE_VERSION: string;
|
|
3
|
+
/**
|
|
4
|
+
* Native Model Context Protocol (MCP) Server for Epistemic Provenance Gate.
|
|
5
|
+
* Communicates via bounded stdio JSON-RPC 2.0; crypto dependencies are declared.
|
|
6
|
+
*/
|
|
7
|
+
export declare const MCP_TOOLS: ({
|
|
8
|
+
name: string;
|
|
9
|
+
description: string;
|
|
10
|
+
inputSchema: {
|
|
11
|
+
type: string;
|
|
12
|
+
properties: {
|
|
13
|
+
file_path: {
|
|
14
|
+
type: string;
|
|
15
|
+
description: string;
|
|
16
|
+
};
|
|
17
|
+
public_key_pem: {
|
|
18
|
+
type: string;
|
|
19
|
+
description: string;
|
|
20
|
+
};
|
|
21
|
+
required_scope: {
|
|
22
|
+
type: string;
|
|
23
|
+
description: string;
|
|
24
|
+
};
|
|
25
|
+
vault_root?: undefined;
|
|
26
|
+
expected_root?: undefined;
|
|
27
|
+
gate_type?: undefined;
|
|
28
|
+
prerequisite_token_path?: undefined;
|
|
29
|
+
};
|
|
30
|
+
required: string[];
|
|
31
|
+
};
|
|
32
|
+
} | {
|
|
33
|
+
name: string;
|
|
34
|
+
description: string;
|
|
35
|
+
inputSchema: {
|
|
36
|
+
type: string;
|
|
37
|
+
properties: {
|
|
38
|
+
vault_root: {
|
|
39
|
+
type: string;
|
|
40
|
+
description: string;
|
|
41
|
+
};
|
|
42
|
+
expected_root: {
|
|
43
|
+
type: string;
|
|
44
|
+
description: string;
|
|
45
|
+
};
|
|
46
|
+
file_path?: undefined;
|
|
47
|
+
public_key_pem?: undefined;
|
|
48
|
+
required_scope?: undefined;
|
|
49
|
+
gate_type?: undefined;
|
|
50
|
+
prerequisite_token_path?: undefined;
|
|
51
|
+
};
|
|
52
|
+
required: string[];
|
|
53
|
+
};
|
|
54
|
+
} | {
|
|
55
|
+
name: string;
|
|
56
|
+
description: string;
|
|
57
|
+
inputSchema: {
|
|
58
|
+
type: string;
|
|
59
|
+
properties: {
|
|
60
|
+
gate_type: {
|
|
61
|
+
type: string;
|
|
62
|
+
enum: string[];
|
|
63
|
+
};
|
|
64
|
+
prerequisite_token_path: {
|
|
65
|
+
type: string;
|
|
66
|
+
description: string;
|
|
67
|
+
};
|
|
68
|
+
public_key_pem: {
|
|
69
|
+
type: string;
|
|
70
|
+
description: string;
|
|
71
|
+
};
|
|
72
|
+
required_scope: {
|
|
73
|
+
type: string;
|
|
74
|
+
description: string;
|
|
75
|
+
};
|
|
76
|
+
file_path?: undefined;
|
|
77
|
+
vault_root?: undefined;
|
|
78
|
+
expected_root?: undefined;
|
|
79
|
+
};
|
|
80
|
+
required: string[];
|
|
81
|
+
};
|
|
82
|
+
})[];
|
|
83
|
+
export declare function handleMcpToolCall(name: string, args: Record<string, any>): Promise<string>;
|
|
84
|
+
/**
|
|
85
|
+
* Starts the stdio JSON-RPC 2.0 MCP server loop.
|
|
86
|
+
*/
|
|
87
|
+
export declare function startMcpServer(): void;
|
package/dist/mcp.js
ADDED
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
4
|
+
if (k2 === undefined) k2 = k;
|
|
5
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
6
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
7
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
8
|
+
}
|
|
9
|
+
Object.defineProperty(o, k2, desc);
|
|
10
|
+
}) : (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
o[k2] = m[k];
|
|
13
|
+
}));
|
|
14
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
15
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
16
|
+
}) : function(o, v) {
|
|
17
|
+
o["default"] = v;
|
|
18
|
+
});
|
|
19
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
20
|
+
var ownKeys = function(o) {
|
|
21
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
22
|
+
var ar = [];
|
|
23
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
24
|
+
return ar;
|
|
25
|
+
};
|
|
26
|
+
return ownKeys(o);
|
|
27
|
+
};
|
|
28
|
+
return function (mod) {
|
|
29
|
+
if (mod && mod.__esModule) return mod;
|
|
30
|
+
var result = {};
|
|
31
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
32
|
+
__setModuleDefault(result, mod);
|
|
33
|
+
return result;
|
|
34
|
+
};
|
|
35
|
+
})();
|
|
36
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
37
|
+
exports.MCP_TOOLS = exports.PACKAGE_VERSION = void 0;
|
|
38
|
+
exports.handleMcpToolCall = handleMcpToolCall;
|
|
39
|
+
exports.startMcpServer = startMcpServer;
|
|
40
|
+
const path = __importStar(require("path"));
|
|
41
|
+
const fs_1 = require("fs");
|
|
42
|
+
const index_1 = require("./index");
|
|
43
|
+
exports.PACKAGE_VERSION = require('../package.json').version;
|
|
44
|
+
/**
|
|
45
|
+
* Native Model Context Protocol (MCP) Server for Epistemic Provenance Gate.
|
|
46
|
+
* Communicates via bounded stdio JSON-RPC 2.0; crypto dependencies are declared.
|
|
47
|
+
*/
|
|
48
|
+
exports.MCP_TOOLS = [
|
|
49
|
+
{
|
|
50
|
+
name: 'verify_artifact_provenance',
|
|
51
|
+
description: "Verifies a document's Ed25519 signature against a caller-supplied public key. WITHOUT public_key_pem this performs a hash-only comparison against an editable sidecar and is NOT cryptographic verification. Unsupported attestation methods are indeterminate. Timestamp token signatures and imprints are checked; without caller anchors they are integrity-only.",
|
|
52
|
+
inputSchema: {
|
|
53
|
+
type: 'object',
|
|
54
|
+
properties: {
|
|
55
|
+
file_path: { type: 'string', description: 'Absolute or relative path to target markdown or code artifact' },
|
|
56
|
+
public_key_pem: { type: 'string', description: 'Optional SPKI PEM public key to verify Ed25519 claim against' },
|
|
57
|
+
required_scope: { type: 'string', description: 'Optional. If given, the signed scope must equal it or verification fails.' }
|
|
58
|
+
},
|
|
59
|
+
required: ['file_path']
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
name: 'audit_vault_merkle_root',
|
|
64
|
+
description: 'Computes the current RFC 9162 SHA-256 Merkle root across visible workspace files. This is a snapshot, not a comparison: pass expected_root to detect drift. Without it, no drift, deletion or rollback can be detected. Hidden directories and node_modules are excluded and are therefore uncovered.',
|
|
65
|
+
inputSchema: {
|
|
66
|
+
type: 'object',
|
|
67
|
+
properties: {
|
|
68
|
+
vault_root: { type: 'string', description: 'Root directory of the workspace or vault' },
|
|
69
|
+
expected_root: { type: 'string', description: 'Optional. A previously published root to compare against; without it no drift can be reported.' }
|
|
70
|
+
},
|
|
71
|
+
required: ['vault_root']
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
name: 'assert_gate_status',
|
|
76
|
+
description: 'Agentic execution barrier. Requires public_key_pem and a valid Ed25519 signature; fails closed without a key. A hash-only check is not enforcement, because anyone able to write the artifact can write its sidecar.',
|
|
77
|
+
inputSchema: {
|
|
78
|
+
type: 'object',
|
|
79
|
+
properties: {
|
|
80
|
+
gate_type: { type: 'string', enum: ['GATE_A_ARCHITECTURE', 'GATE_B_PR_MERGE'] },
|
|
81
|
+
prerequisite_token_path: { type: 'string', description: 'Path to target artifact to evaluate' },
|
|
82
|
+
public_key_pem: { type: 'string', description: 'REQUIRED. SPKI PEM public key the signature must verify against.' },
|
|
83
|
+
required_scope: { type: 'string', description: 'Optional. If given, the signed scope must equal it.' }
|
|
84
|
+
},
|
|
85
|
+
required: ['gate_type', 'prerequisite_token_path', 'public_key_pem']
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
];
|
|
89
|
+
async function handleMcpToolCall(name, args) {
|
|
90
|
+
if (!args || typeof args !== 'object' || Array.isArray(args))
|
|
91
|
+
return '[ERROR] Invalid arguments';
|
|
92
|
+
const schema = exports.MCP_TOOLS.find(t => t.name === name);
|
|
93
|
+
if (schema) {
|
|
94
|
+
for (const field of schema.inputSchema.required) {
|
|
95
|
+
if (typeof args[field] !== 'string' || !args[field].length)
|
|
96
|
+
return (name === 'assert_gate_status' ? '[BLOCKED 🛑]' : '[ERROR]') + ' Missing required argument: ' + field;
|
|
97
|
+
}
|
|
98
|
+
for (const [key, value] of Object.entries(args)) {
|
|
99
|
+
if (!(key in schema.inputSchema.properties) || typeof value !== 'string' || !value.length)
|
|
100
|
+
return '[ERROR] Invalid argument: ' + key;
|
|
101
|
+
}
|
|
102
|
+
if (args.gate_type && !['GATE_A_ARCHITECTURE', 'GATE_B_PR_MERGE'].includes(args.gate_type))
|
|
103
|
+
return '[BLOCKED 🛑] Unknown gate type';
|
|
104
|
+
const target = name === 'verify_artifact_provenance' ? args.file_path : name === 'audit_vault_merkle_root' ? args.vault_root : args.prerequisite_token_path;
|
|
105
|
+
if (!target)
|
|
106
|
+
return '[ERROR] Missing artifact or root path';
|
|
107
|
+
}
|
|
108
|
+
switch (name) {
|
|
109
|
+
case 'verify_artifact_provenance': {
|
|
110
|
+
const { file_path, public_key_pem, required_scope } = args;
|
|
111
|
+
if (!(0, fs_1.existsSync)(file_path)) {
|
|
112
|
+
return `[ERROR] File not found: ${file_path}`;
|
|
113
|
+
}
|
|
114
|
+
const sidecarPath = `${file_path}.provenance.json`;
|
|
115
|
+
if (!(0, fs_1.existsSync)(sidecarPath)) {
|
|
116
|
+
const { getArtifactBadge } = await Promise.resolve().then(() => __importStar(require('./badge')));
|
|
117
|
+
const badgeInfo = await getArtifactBadge(file_path);
|
|
118
|
+
return `[UNATTESTED] ${badgeInfo.badgeText} : No provenance sidecar found for ${path.basename(file_path)}`;
|
|
119
|
+
}
|
|
120
|
+
try {
|
|
121
|
+
const sidecarRaw = (await (0, index_1.readRegularFile)(sidecarPath, index_1.MAX_SIDECAR_BYTES)).toString('utf8');
|
|
122
|
+
const sidecar = JSON.parse(sidecarRaw);
|
|
123
|
+
(0, index_1.validateSidecar)(sidecar);
|
|
124
|
+
// A supplied key is the only path that performs cryptographic verification.
|
|
125
|
+
if (public_key_pem) {
|
|
126
|
+
const res = await index_1.ProvenanceEngine.verifyDocument(file_path, public_key_pem, { expectedScope: required_scope });
|
|
127
|
+
if (!res.verified) {
|
|
128
|
+
return `[FAIL ❌] Verification failed for ${path.basename(file_path)}: ${res.error}`;
|
|
129
|
+
}
|
|
130
|
+
// A signed scope that is never compared is decoration. Enforce it when asked.
|
|
131
|
+
if (required_scope && res.scope !== required_scope) {
|
|
132
|
+
return `[FAIL ❌] Scope mismatch on ${path.basename(file_path)}: signed scope is "${res.scope}", required "${required_scope}".`;
|
|
133
|
+
}
|
|
134
|
+
return [
|
|
135
|
+
`[VERIFIED ✅] [SIGNATURE_VALID] ${path.basename(file_path)}`,
|
|
136
|
+
` • Badge: ${res.badge ?? '🛡️ [SEALED]'}`,
|
|
137
|
+
` • Signer (as claimed in the signed payload): ${res.signer}`,
|
|
138
|
+
` • Scope: ${res.scope ?? 'n/a'}`,
|
|
139
|
+
` • Timestamp: ${res.timestamp || 'none recorded'}${res.timestamp ? ' (' + res.timestampTier + '; revocation not checked)' : ''}`,
|
|
140
|
+
` • Note: a valid signature proves key possession, not the identity or honesty of its holder.`
|
|
141
|
+
].join('\n');
|
|
142
|
+
}
|
|
143
|
+
// Structural and hash assertion
|
|
144
|
+
const canonical = await index_1.ProvenanceEngine.canonicalizeFile(file_path);
|
|
145
|
+
const crypto = await Promise.resolve().then(() => __importStar(require('crypto')));
|
|
146
|
+
const currentHash = crypto.createHash('sha256').update(canonical).digest('hex');
|
|
147
|
+
if (currentHash !== sidecar.sha256_at_last_write) {
|
|
148
|
+
return `[FAIL ❌] Hash mismatch on ${path.basename(file_path)}. Recorded: ${sidecar.sha256_at_last_write.slice(0, 16)}.., Computed: ${currentHash.slice(0, 16)}..`;
|
|
149
|
+
}
|
|
150
|
+
const rfcAtt = sidecar.attestations?.find((a) => a.method === 'rfc3161');
|
|
151
|
+
const edAtt = sidecar.attestations?.find((a) => /ed25519/.test(a.method));
|
|
152
|
+
const pgpAtt = sidecar.attestations?.find((a) => a.method === 'openpgp');
|
|
153
|
+
const { getArtifactBadge } = await Promise.resolve().then(() => __importStar(require('./badge')));
|
|
154
|
+
const badgeInfo = await getArtifactBadge(file_path);
|
|
155
|
+
// No key was supplied, so NOTHING here has been cryptographically verified.
|
|
156
|
+
// Identity must never be inferred from an unsigned string, and absent evidence
|
|
157
|
+
// must never be rendered as a default assurance tier.
|
|
158
|
+
return [
|
|
159
|
+
`[UNVERIFIED - HASH ONLY] ${path.basename(file_path)}`,
|
|
160
|
+
` • Badge: ${badgeInfo.badgeText}`,
|
|
161
|
+
` The bytes match the hash recorded in the sidecar. That is all this check establishes.`,
|
|
162
|
+
` No signature was verified. Anyone able to write this file can write its sidecar,`,
|
|
163
|
+
` so this result does not resist a filesystem writer. Supply public_key_pem for`,
|
|
164
|
+
` cryptographic verification.`,
|
|
165
|
+
``,
|
|
166
|
+
` • SHA-256 matches recorded: ${currentHash}`,
|
|
167
|
+
` • Signature attestations present (unverified): ${[edAtt && 'ed25519', pgpAtt && 'openpgp'].filter(Boolean).join(', ') || 'none'}`,
|
|
168
|
+
` • Claimed signer (unverified, attacker-controllable): ${pgpAtt?.signer || edAtt?.signer || 'none recorded'}`,
|
|
169
|
+
` • Timestamp attestation: ${rfcAtt ? `${rfcAtt.tier ?? 'tier not recorded'} via ${rfcAtt.tsa ?? 'TSA not recorded'} at ${rfcAtt.tsa_time ?? 'time not recorded'}` : 'none recorded'}`
|
|
170
|
+
].join('\n');
|
|
171
|
+
}
|
|
172
|
+
catch (err) {
|
|
173
|
+
return `[ERROR] Verification error: ${err.message}`;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
case 'audit_vault_merkle_root': {
|
|
177
|
+
const { vault_root, expected_root } = args;
|
|
178
|
+
if (!(0, fs_1.existsSync)(vault_root)) {
|
|
179
|
+
return `[ERROR] Vault root directory not found: ${vault_root}`;
|
|
180
|
+
}
|
|
181
|
+
try {
|
|
182
|
+
const vault = new index_1.ProvenanceVault(vault_root);
|
|
183
|
+
const { merkleRoot, algorithm, fileCount } = await vault.scanAndAudit();
|
|
184
|
+
// "PRISTINE" was previously hardcoded, so a deleted file still reported clean.
|
|
185
|
+
// A snapshot cannot report drift; only a comparison can.
|
|
186
|
+
const status = expected_root
|
|
187
|
+
? (merkleRoot === expected_root
|
|
188
|
+
? 'MATCHES the supplied expected root'
|
|
189
|
+
: `DIFFERS from the supplied expected root (${expected_root.slice(0, 16)}..) - content added, changed, or DELETED`)
|
|
190
|
+
: 'NOT COMPARED - no expected_root supplied, so drift, deletion and rollback are undetectable here';
|
|
191
|
+
return [
|
|
192
|
+
`[MERKLE SNAPSHOT]`,
|
|
193
|
+
` • Root: ${merkleRoot}`,
|
|
194
|
+
` • Algorithm: ${algorithm}`,
|
|
195
|
+
` • Files: ${fileCount} (hidden directories and node_modules are excluded and uncovered)`,
|
|
196
|
+
` • Status: ${status}`
|
|
197
|
+
].join('\n');
|
|
198
|
+
}
|
|
199
|
+
catch (err) {
|
|
200
|
+
return `[ERROR] Merkle audit failed: ${err.message}`;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
case 'assert_gate_status': {
|
|
204
|
+
const { gate_type, prerequisite_token_path, public_key_pem, required_scope } = args;
|
|
205
|
+
if (!prerequisite_token_path) {
|
|
206
|
+
return `[BLOCKED 🛑] ${gate_type} requires a target artifact path to evaluate.`;
|
|
207
|
+
}
|
|
208
|
+
if (!(0, fs_1.existsSync)(prerequisite_token_path)) {
|
|
209
|
+
return `[BLOCKED 🛑] ${gate_type} prerequisite artifact not found: ${prerequisite_token_path}`;
|
|
210
|
+
}
|
|
211
|
+
const sidecarPath = `${prerequisite_token_path}.provenance.json`;
|
|
212
|
+
if (!(0, fs_1.existsSync)(sidecarPath)) {
|
|
213
|
+
return `[BLOCKED 🛑] ${gate_type} requires valid provenance token at ${path.basename(sidecarPath)}, but none exists.`;
|
|
214
|
+
}
|
|
215
|
+
// Fail closed. A hash-only gate is not a gate: the writer who tampers with the
|
|
216
|
+
// artifact can write the sidecar hash too, and needs no key to do it.
|
|
217
|
+
if (!public_key_pem) {
|
|
218
|
+
return `[BLOCKED 🛑] ${gate_type} requires public_key_pem. A hash comparison against an editable sidecar is not cryptographic enforcement and will not clear this gate.`;
|
|
219
|
+
}
|
|
220
|
+
try {
|
|
221
|
+
const res = await index_1.ProvenanceEngine.verifyDocument(prerequisite_token_path, public_key_pem, { expectedScope: required_scope });
|
|
222
|
+
if (!res.verified) {
|
|
223
|
+
return `[BLOCKED 🛑] ${gate_type} failed: signature did not verify for ${path.basename(prerequisite_token_path)}: ${res.error}`;
|
|
224
|
+
}
|
|
225
|
+
if (required_scope && res.scope !== required_scope) {
|
|
226
|
+
return `[BLOCKED 🛑] ${gate_type} failed: signed scope "${res.scope}" does not match required "${required_scope}".`;
|
|
227
|
+
}
|
|
228
|
+
return `[PASSED 🟢] ${gate_type} cleared. ${path.basename(prerequisite_token_path)} carries a valid Ed25519 signature under the supplied key${required_scope ? ' at the required scope' : ''}. This authenticates key possession, not identity or authorization.`;
|
|
229
|
+
}
|
|
230
|
+
catch (err) {
|
|
231
|
+
return `[BLOCKED 🛑] ${gate_type} evaluation error: ${err.message}`;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
default:
|
|
235
|
+
return `[ERROR] Unknown tool: ${name}`;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Starts the stdio JSON-RPC 2.0 MCP server loop.
|
|
240
|
+
*/
|
|
241
|
+
function startMcpServer() {
|
|
242
|
+
async function dispatch(line) {
|
|
243
|
+
if (!line.trim())
|
|
244
|
+
return;
|
|
245
|
+
let req;
|
|
246
|
+
try {
|
|
247
|
+
req = JSON.parse(line);
|
|
248
|
+
if (!req || typeof req !== 'object' || Array.isArray(req) || req.jsonrpc !== '2.0' || typeof req.method !== 'string' || (req.id !== undefined && req.id !== null && typeof req.id !== 'string' && typeof req.id !== 'number'))
|
|
249
|
+
throw new Error('Invalid JSON-RPC request');
|
|
250
|
+
const { id, method, params } = req;
|
|
251
|
+
if (id === undefined)
|
|
252
|
+
return;
|
|
253
|
+
if (method === 'initialize') {
|
|
254
|
+
const resp = {
|
|
255
|
+
jsonrpc: '2.0',
|
|
256
|
+
id,
|
|
257
|
+
result: {
|
|
258
|
+
protocolVersion: '2024-11-05',
|
|
259
|
+
capabilities: { tools: {} },
|
|
260
|
+
serverInfo: { name: 'provenance-gate', version: exports.PACKAGE_VERSION }
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
process.stdout.write(JSON.stringify(resp) + '\n');
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
if (method === 'notifications/initialized' || method === 'ping') {
|
|
267
|
+
if (id !== undefined) {
|
|
268
|
+
process.stdout.write(JSON.stringify({ jsonrpc: '2.0', id, result: {} }) + '\n');
|
|
269
|
+
}
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
if (method === 'tools/list') {
|
|
273
|
+
const resp = {
|
|
274
|
+
jsonrpc: '2.0',
|
|
275
|
+
id,
|
|
276
|
+
result: { tools: exports.MCP_TOOLS }
|
|
277
|
+
};
|
|
278
|
+
process.stdout.write(JSON.stringify(resp) + '\n');
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
if (method === 'tools/call') {
|
|
282
|
+
const { name, arguments: toolArgs } = params || {};
|
|
283
|
+
const outputText = await handleMcpToolCall(name, toolArgs || {});
|
|
284
|
+
const resp = {
|
|
285
|
+
jsonrpc: '2.0',
|
|
286
|
+
id,
|
|
287
|
+
result: {
|
|
288
|
+
content: [{ type: 'text', text: outputText }],
|
|
289
|
+
isError: /^\[(ERROR|FAIL|BLOCKED)/.test(outputText)
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
process.stdout.write(JSON.stringify(resp) + '\n');
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
if (id !== undefined) {
|
|
296
|
+
const resp = {
|
|
297
|
+
jsonrpc: '2.0',
|
|
298
|
+
id,
|
|
299
|
+
error: { code: -32601, message: `Method ${method} not found` }
|
|
300
|
+
};
|
|
301
|
+
process.stdout.write(JSON.stringify(resp) + '\n');
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
catch (err) {
|
|
305
|
+
const errResp = {
|
|
306
|
+
jsonrpc: '2.0',
|
|
307
|
+
id: req?.id ?? null,
|
|
308
|
+
error: { code: req === undefined ? -32700 : -32600, message: req === undefined ? 'Parse error' : 'Invalid request or tool arguments' }
|
|
309
|
+
};
|
|
310
|
+
process.stdout.write(JSON.stringify(errResp) + '\n');
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
void (async () => {
|
|
314
|
+
let pending = Buffer.alloc(0), discard = false;
|
|
315
|
+
const limit = 1024 * 1024;
|
|
316
|
+
for await (const raw of process.stdin) {
|
|
317
|
+
const chunk = Buffer.from(raw);
|
|
318
|
+
let offset = 0;
|
|
319
|
+
while (offset < chunk.length) {
|
|
320
|
+
const newline = chunk.indexOf(10, offset);
|
|
321
|
+
const end = newline < 0 ? chunk.length : newline;
|
|
322
|
+
const piece = chunk.subarray(offset, end);
|
|
323
|
+
if (!discard && pending.length + piece.length > limit) {
|
|
324
|
+
discard = true;
|
|
325
|
+
pending = Buffer.alloc(0);
|
|
326
|
+
process.stdout.write(JSON.stringify({ jsonrpc: '2.0', id: null, error: { code: -32600, message: 'MCP frame exceeds 1 MiB limit' } }) + '\n');
|
|
327
|
+
}
|
|
328
|
+
if (!discard)
|
|
329
|
+
pending = Buffer.concat([pending, piece]);
|
|
330
|
+
if (newline >= 0) {
|
|
331
|
+
if (!discard) {
|
|
332
|
+
try {
|
|
333
|
+
await dispatch(new TextDecoder('utf-8', { fatal: true }).decode(pending));
|
|
334
|
+
}
|
|
335
|
+
catch {
|
|
336
|
+
process.stdout.write(JSON.stringify({ jsonrpc: '2.0', id: null, error: { code: -32700, message: 'Invalid UTF-8 frame' } }) + '\n');
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
discard = false;
|
|
340
|
+
pending = Buffer.alloc(0);
|
|
341
|
+
}
|
|
342
|
+
offset = end + 1;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
if (pending.length && !discard)
|
|
346
|
+
await dispatch(new TextDecoder('utf-8', { fatal: true }).decode(pending));
|
|
347
|
+
})().catch(() => { process.exitCode = 1; });
|
|
348
|
+
}
|
|
349
|
+
// Auto-run if executed directly
|
|
350
|
+
if (require.main === module) {
|
|
351
|
+
startMcpServer();
|
|
352
|
+
}
|
package/dist/merkle.d.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RFC 6962 Merkle tree over vault artifacts, with inclusion proofs.
|
|
3
|
+
*
|
|
4
|
+
* WHY THIS REPLACES scanAndAudit's ROOT
|
|
5
|
+
* -------------------------------------
|
|
6
|
+
* `ProvenanceVault.scanAndAudit` computed its "merkleRoot" as a single SHA-256
|
|
7
|
+
* over every hash concatenated together. That is a manifest checksum. It detects
|
|
8
|
+
* that something changed and nothing else - in particular it cannot produce an
|
|
9
|
+
* inclusion proof, which is the only reason to use a tree in the first place.
|
|
10
|
+
*
|
|
11
|
+
* With a tree, a third party can verify ONE document against a published root
|
|
12
|
+
* without downloading the whole registry: log2(n) hashes, about 9 for a few
|
|
13
|
+
* hundred artifacts. With a linear digest they would need every artifact hash in
|
|
14
|
+
* the vault to check a single file.
|
|
15
|
+
*
|
|
16
|
+
* DOMAIN SEPARATION
|
|
17
|
+
* -----------------
|
|
18
|
+
* Leaves are hashed with a 0x00 prefix and internal nodes with 0x01. Without it
|
|
19
|
+
* an internal node can be presented as a leaf - a second-preimage attack on the
|
|
20
|
+
* tree shape.
|
|
21
|
+
*
|
|
22
|
+
* Odd nodes are PROMOTED, never duplicated. Duplicating the last leaf lets two
|
|
23
|
+
* distinct trees share a root, which is the Bitcoin CVE-2012-2459 class of bug.
|
|
24
|
+
*
|
|
25
|
+
* PARITY WITH THE PYTHON IMPLEMENTATION
|
|
26
|
+
* -------------------------------------
|
|
27
|
+
* This is a deliberate port of the vault's `_merkle.py` and must agree with it
|
|
28
|
+
* byte for byte - two disagreeing provenance implementations would be worse than
|
|
29
|
+
* one honest flat digest. Leaf ordering, leaf encoding (note the TWO spaces
|
|
30
|
+
* between hash and path) and the empty-tree value are all fixed by that
|
|
31
|
+
* requirement, and `merkle.test.ts` checks against roots generated by the Python.
|
|
32
|
+
*/
|
|
33
|
+
export declare const MERKLE_ALGORITHM = "merkle-sha256-rfc6962";
|
|
34
|
+
export declare const leafHash: (data: Buffer) => Buffer;
|
|
35
|
+
export declare const nodeHash: (left: Buffer, right: Buffer) => Buffer;
|
|
36
|
+
export interface ArtifactEntry {
|
|
37
|
+
sha256: string;
|
|
38
|
+
}
|
|
39
|
+
export type ArtifactMap = Record<string, ArtifactEntry>;
|
|
40
|
+
export interface ProofStep {
|
|
41
|
+
side: 'left' | 'right';
|
|
42
|
+
hash: string;
|
|
43
|
+
}
|
|
44
|
+
export interface InclusionProof {
|
|
45
|
+
algorithm: string;
|
|
46
|
+
artifact: string;
|
|
47
|
+
leaf_sha256: string;
|
|
48
|
+
artifact_sha256: string;
|
|
49
|
+
leaf_index: number;
|
|
50
|
+
tree_size: number;
|
|
51
|
+
proof: ProofStep[];
|
|
52
|
+
merkle_root: string;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Deterministic leaf order: sorted by path. Walk order is not stable across
|
|
56
|
+
* filesystems; sorting is.
|
|
57
|
+
*/
|
|
58
|
+
export declare function leavesFromRegistry(artifacts: ArtifactMap): Array<[string, Buffer]>;
|
|
59
|
+
/** Every level, leaves first, root last. */
|
|
60
|
+
export declare function buildLevels(leaves: Buffer[]): Buffer[][];
|
|
61
|
+
export declare function merkleRoot(artifacts: ArtifactMap): string;
|
|
62
|
+
/** The sibling hashes needed to rebuild the root from one leaf. */
|
|
63
|
+
export declare function inclusionProof(artifacts: ArtifactMap, target: string): InclusionProof | null;
|
|
64
|
+
/**
|
|
65
|
+
* Recompute the root from a leaf plus its audit path.
|
|
66
|
+
* If expectedRoot is provided, validates that the proof resolves to it.
|
|
67
|
+
*/
|
|
68
|
+
export declare function verifyProof(proof: InclusionProof, expectedRoot?: string, expectedArtifact?: {
|
|
69
|
+
path: string;
|
|
70
|
+
sha256: string;
|
|
71
|
+
}): boolean;
|