mcp-proof 0.3.11 → 0.3.12

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/bin/mcp-proof.js CHANGED
@@ -9,9 +9,9 @@
9
9
  // mcp-proof --self-test
10
10
  //
11
11
  // Exit codes:
12
- // 0 — verdict allow
12
+ // 0 — verdict allow (and, with --vetokernel-endpoint, VetoKernel authorized it)
13
13
  // 1 — verdict warn
14
- // 2 — verdict block
14
+ // 2 — verdict block, or VetoKernel denied the action
15
15
  // 64 — usage error
16
16
  // 65 — input parse error
17
17
  //
@@ -24,6 +24,7 @@ const os = require('os');
24
24
  const path = require('path');
25
25
  const { spawn } = require('child_process');
26
26
  const { evaluate, evaluateCall, scanSkillManifest, verifyPublisherAsync } = require('../engine.js');
27
+ const { authorizeCall } = require('../vetokernel-client.js');
27
28
 
28
29
  const DEFAULT_MAX_INPUT_BYTES = 5 * 1024 * 1024;
29
30
  const DEFAULT_TIMEOUT_MS = 15000;
@@ -53,6 +54,9 @@ Options:
53
54
  --skill <path> scan a Claude SKILL.md (educational/warning context aware)
54
55
  --inspect-call [<path>] inspect a tools/call payload before execution (reads stdin if path omitted)
55
56
  --execute [<path>] deprecated alias for --inspect-call
57
+ --vetokernel-endpoint <url> with --inspect-call, ask VetoKernel to authorize the
58
+ proposed action before execution and verify the returned receipt
59
+ (env: VETOKERNEL_ENDPOINT, VETOKERNEL_API_KEY, VETOKERNEL_TENANT_ID)
56
60
  --markdown emit a shareable Markdown Trust Card
57
61
  --trust-card alias for --markdown
58
62
  --sarif emit SARIF 2.1.0 for GitHub code scanning
@@ -150,14 +154,25 @@ async function main() {
150
154
 
151
155
  if (args.inspectCall) {
152
156
  let result;
157
+ let call;
153
158
  try {
154
- result = runExecute(args);
159
+ ({ result, call } = runExecute(args));
155
160
  } catch (err) {
156
161
  process.stderr.write(`mcp-proof: ${err.message}\n`);
157
162
  process.exit(err.exitCode || 65);
158
163
  }
164
+ if (args.vetokernelEndpoint) {
165
+ try {
166
+ result.authorization = await authorizeInspectedCall(result, call, args);
167
+ } catch (err) {
168
+ // Fail closed: if the authorization service is unreachable, the action
169
+ // is not authorized.
170
+ process.stderr.write(`mcp-proof: vetokernel authorization failed: ${err.message}\n`);
171
+ process.exit(2);
172
+ }
173
+ }
159
174
  emitOutput(result, args);
160
- process.exit(exitCodeForDecision(result.decision));
175
+ process.exit(exitCodeForInspectedCall(result));
161
176
  }
162
177
 
163
178
  let raw;
@@ -212,6 +227,7 @@ function parseArgs(argv) {
212
227
  source: '', transport: '', bind: '', json: false, selfTest: false,
213
228
  help: false, version: false, file: '', stdio: '', config: '', skill: '',
214
229
  inspectCall: false, deprecatedExecute: false, format: 'human', live: false, timeoutMs: DEFAULT_TIMEOUT_MS,
230
+ vetokernelEndpoint: process.env.VETOKERNEL_ENDPOINT || '',
215
231
  };
216
232
  for (let i = 0; i < argv.length; i++) {
217
233
  const a = argv[i];
@@ -239,6 +255,7 @@ function parseArgs(argv) {
239
255
  break;
240
256
  case '--live': out.live = true; break;
241
257
  case '--timeout': out.timeoutMs = parseTimeout(argv[++i] || ''); break;
258
+ case '--vetokernel-endpoint': out.vetokernelEndpoint = argv[++i] || ''; break;
242
259
  default:
243
260
  if (a.startsWith('-')) {
244
261
  process.stderr.write(`mcp-proof: unknown flag ${a}\n`);
@@ -654,7 +671,40 @@ function runExecute(args) {
654
671
  const result = evaluateCall(parsed);
655
672
  result.mode = 'inspect-call';
656
673
  result.name = args.file || 'stdin';
657
- return result;
674
+ return { result, call: parsed };
675
+ }
676
+
677
+ // authorizeInspectedCall asks VetoKernel to authorize the exact action this
678
+ // call proposes. action_type and target_id come from the call file itself,
679
+ // falling back to the tool name and arguments.target_id, so a call fixture is
680
+ // self-describing and needs no extra flags.
681
+ function authorizeInspectedCall(result, call, args) {
682
+ const callArgs = call.arguments || (call.params && call.params.arguments) || {};
683
+ const parametersJSON = JSON.stringify(callArgs);
684
+ return authorizeCall({
685
+ endpoint: args.vetokernelEndpoint,
686
+ apiKey: process.env.VETOKERNEL_API_KEY || '',
687
+ tenantId: process.env.VETOKERNEL_TENANT_ID || 'default',
688
+ agentId: call.agent_id || 'mcp-proof',
689
+ actionId: call.action_id || `mcp-proof-${Date.now()}`,
690
+ toolName: result.tool,
691
+ actionType: call.action_type || result.tool,
692
+ targetId: call.target_id || callArgs.target_id || '',
693
+ parametersJSON,
694
+ context: call.context,
695
+ scanner: {
696
+ name: 'mcp-proof',
697
+ decision: result.decision,
698
+ severity: result.call_scan.severity,
699
+ reason: result.call_scan.reason,
700
+ },
701
+ timeoutMs: args.timeoutMs,
702
+ });
703
+ }
704
+
705
+ function exitCodeForInspectedCall(result) {
706
+ if (result.authorization && result.authorization.decision !== 'allow') return 2;
707
+ return exitCodeForDecision(result.decision);
658
708
  }
659
709
 
660
710
  function exitCodeForDecision(d) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-proof",
3
- "version": "0.3.11",
3
+ "version": "0.3.12",
4
4
  "description": "Auditable, privacy-preserving MCP (Model Context Protocol) server verifier with local Trust Cards and SARIF output — test servers before connecting them to an AI system.",
5
5
  "main": "engine.js",
6
6
  "bin": {
@@ -8,6 +8,7 @@
8
8
  },
9
9
  "files": [
10
10
  "engine.js",
11
+ "vetokernel-client.js",
11
12
  "bin/mcp-proof.js",
12
13
  "README.md",
13
14
  "LICENSE"
@@ -0,0 +1,168 @@
1
+ // vetokernel-client.js — post an inspected tools/call to VetoKernel's
2
+ // execution-authorization endpoint and independently verify the receipt.
3
+ //
4
+ // Node stdlib only (http, https, crypto, url) — mcp-proof adds no dependencies.
5
+
6
+ 'use strict';
7
+
8
+ const crypto = require('crypto');
9
+ const http = require('http');
10
+ const https = require('https');
11
+ const { URL } = require('url');
12
+
13
+ const SCHEMA_VERSION = '1.0';
14
+
15
+ // goJSONString matches Go's encoding/json string encoding, which HTML-escapes
16
+ // <, > and & by default. Everything else Node's JSON.stringify already agrees on.
17
+ function goJSONString(value) {
18
+ return JSON.stringify(String(value))
19
+ .replace(/</g, '\\u003c')
20
+ .replace(/>/g, '\\u003e')
21
+ .replace(/&/g, '\\u0026');
22
+ }
23
+
24
+ // canonicalReceiptJSON reproduces, in JavaScript, the exact byte sequence
25
+ // VetoKernel hashes into receipt_hash (Go: backend/auth/execution_authorization.go
26
+ // CanonicalReceiptBytes). It is deliberately an INDEPENDENT re-implementation:
27
+ // agreement between the two is what makes the receipt independently verified
28
+ // rather than merely re-read. Field order and the omitempty set are load-bearing
29
+ // and are pinned by a golden vector in both test suites.
30
+ function canonicalReceiptJSON(receipt) {
31
+ const fields = [
32
+ ['schema_version', receipt.schema_version, false],
33
+ ['decision', receipt.decision, false],
34
+ ['action_id', receipt.action_id, false],
35
+ ['action_type', receipt.action_type, false],
36
+ ['target_id', receipt.target_id, false],
37
+ ['tool_name', receipt.tool_name, true],
38
+ ['parameters_hash', receipt.parameters_hash, true],
39
+ ['tenant_id', receipt.tenant_id, false],
40
+ ['payload_hash', receipt.payload_hash, false],
41
+ ['context_hash', receipt.context_hash, false],
42
+ ['authorized_by_agent_id', receipt.authorized_by_agent_id, true],
43
+ ['issued_at', receipt.issued_at, false],
44
+ ];
45
+ const parts = [];
46
+ for (const [key, raw, omitempty] of fields) {
47
+ const value = raw === undefined || raw === null ? '' : raw;
48
+ if (omitempty && value === '') continue;
49
+ parts.push(goJSONString(key) + ':' + goJSONString(value));
50
+ }
51
+ return '{' + parts.join(',') + '}';
52
+ }
53
+
54
+ // verifyReceipt re-derives both bindings the receipt claims: that payload_hash
55
+ // is sha256 of the exact parameter bytes we sent, and that receipt_hash is
56
+ // sha256 of the canonical receipt fields.
57
+ function verifyReceipt(receipt, parametersJSON) {
58
+ if (!receipt || typeof receipt !== 'object') {
59
+ return { verified: false, problems: ['no receipt returned'], payload_hash: '', receipt_hash: '' };
60
+ }
61
+ const problems = [];
62
+ if (receipt.schema_version !== SCHEMA_VERSION) {
63
+ problems.push(`unexpected schema_version ${receipt.schema_version}`);
64
+ }
65
+ if (receipt.decision !== 'allow') {
66
+ problems.push(`unexpected receipt decision ${receipt.decision}`);
67
+ }
68
+ const payloadHash = crypto.createHash('sha256')
69
+ .update(Buffer.from(String(parametersJSON), 'utf8')).digest('hex');
70
+ if (receipt.payload_hash !== payloadHash) {
71
+ problems.push(`payload_hash does not bind the parameters we sent (recomputed ${payloadHash})`);
72
+ }
73
+ const receiptHash = crypto.createHash('sha256')
74
+ .update(canonicalReceiptJSON(receipt), 'utf8').digest('hex');
75
+ if (receipt.receipt_hash !== receiptHash) {
76
+ problems.push(`receipt_hash mismatch (recomputed ${receiptHash})`);
77
+ }
78
+ return { verified: problems.length === 0, problems, payload_hash: payloadHash, receipt_hash: receiptHash };
79
+ }
80
+
81
+ // authorizeCall proposes a consequential action to VetoKernel and returns its
82
+ // decision. It fails closed: an allow whose receipt does not independently
83
+ // verify is downgraded to a deny.
84
+ function authorizeCall(options) {
85
+ const {
86
+ endpoint,
87
+ apiKey = '',
88
+ tenantId = 'default',
89
+ agentId = 'mcp-proof',
90
+ actionId = '',
91
+ toolName = '',
92
+ actionType = '',
93
+ targetId = '',
94
+ parametersJSON = '{}',
95
+ context,
96
+ scanner = { name: 'mcp-proof', decision: 'allow', severity: 'low', reason: 'clean' },
97
+ timeoutMs = 5000,
98
+ } = options;
99
+
100
+ const body = JSON.stringify({
101
+ schema_version: SCHEMA_VERSION,
102
+ action_id: actionId,
103
+ agent_id: agentId,
104
+ tool_name: toolName,
105
+ action_type: actionType,
106
+ target_id: targetId,
107
+ parameters_json: parametersJSON,
108
+ context: context || undefined,
109
+ scanner,
110
+ });
111
+
112
+ const url = new URL(endpoint);
113
+ const transport = url.protocol === 'https:' ? https : http;
114
+
115
+ return new Promise((resolve, reject) => {
116
+ const req = transport.request({
117
+ protocol: url.protocol,
118
+ hostname: url.hostname,
119
+ port: url.port,
120
+ path: url.pathname + url.search,
121
+ method: 'POST',
122
+ headers: {
123
+ 'Content-Type': 'application/json',
124
+ 'Content-Length': Buffer.byteLength(body),
125
+ 'X-API-Key': apiKey,
126
+ 'X-Tenant-ID': tenantId,
127
+ },
128
+ }, (res) => {
129
+ const chunks = [];
130
+ res.on('data', (c) => chunks.push(c));
131
+ res.on('end', () => {
132
+ const raw = Buffer.concat(chunks).toString('utf8');
133
+ let parsed;
134
+ try {
135
+ parsed = JSON.parse(raw);
136
+ } catch (_) {
137
+ reject(new Error(`vetokernel returned non-JSON (status ${res.statusCode}): ${raw.slice(0, 200)}`));
138
+ return;
139
+ }
140
+ const out = {
141
+ endpoint,
142
+ status: res.statusCode,
143
+ decision: parsed.decision === 'allow' ? 'allow' : 'deny',
144
+ reason: parsed.reason || '',
145
+ receipt: parsed.receipt || null,
146
+ verification: null,
147
+ };
148
+ out.verification = out.decision === 'allow'
149
+ ? verifyReceipt(parsed.receipt, parametersJSON)
150
+ : { verified: false, problems: ['denied — no receipt issued'], payload_hash: '', receipt_hash: '' };
151
+ if (out.decision === 'allow' && !out.verification.verified) {
152
+ out.decision = 'deny';
153
+ out.reason = `receipt failed independent verification: ${out.verification.problems.join('; ')}`;
154
+ }
155
+ resolve(out);
156
+ });
157
+ });
158
+
159
+ req.setTimeout(timeoutMs, () => {
160
+ req.destroy(new Error(`vetokernel request timed out after ${timeoutMs}ms`));
161
+ });
162
+ req.on('error', reject);
163
+ req.write(body);
164
+ req.end();
165
+ });
166
+ }
167
+
168
+ module.exports = { authorizeCall, verifyReceipt, canonicalReceiptJSON, SCHEMA_VERSION };