@guardion/guardion 0.3.0 → 0.4.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.
Files changed (84) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +202 -0
  3. package/dist/bin/cli.d.ts.map +1 -0
  4. package/dist/bin/cli.js +590 -0
  5. package/dist/bin/cli.js.map +1 -0
  6. package/dist/connectors/claude-code/hooks/enforce.cjs +58 -0
  7. package/{hooks → dist/connectors/claude-code/hooks}/guardion-hook.cjs +123 -1
  8. package/dist/connectors/claude-code/hooks/tool-scanner.cjs +272 -0
  9. package/dist/connectors/claude-code/src/collect.d.ts +5 -0
  10. package/dist/connectors/claude-code/src/collect.d.ts.map +1 -0
  11. package/dist/connectors/claude-code/src/collect.js +17 -0
  12. package/dist/connectors/claude-code/src/collect.js.map +1 -0
  13. package/dist/{installer.d.ts → connectors/claude-code/src/installer.d.ts} +1 -1
  14. package/dist/connectors/claude-code/src/installer.d.ts.map +1 -0
  15. package/dist/{installer.js → connectors/claude-code/src/installer.js} +2 -2
  16. package/dist/connectors/claude-code/src/installer.js.map +1 -0
  17. package/dist/connectors/claude-code/src/scanner.d.ts.map +1 -0
  18. package/dist/{scanner.js → connectors/claude-code/src/scanner.js} +1 -1
  19. package/dist/connectors/claude-code/src/scanner.js.map +1 -0
  20. package/dist/{config.d.ts → core/config.d.ts} +96 -0
  21. package/dist/core/config.d.ts.map +1 -0
  22. package/dist/{config.js → core/config.js} +44 -0
  23. package/dist/core/config.js.map +1 -0
  24. package/dist/{constants.d.ts → core/constants.d.ts} +1 -1
  25. package/dist/core/constants.d.ts.map +1 -0
  26. package/dist/{constants.js → core/constants.js} +1 -1
  27. package/dist/core/constants.js.map +1 -0
  28. package/dist/core/discover.d.ts +36 -0
  29. package/dist/core/discover.d.ts.map +1 -0
  30. package/dist/core/discover.js +154 -0
  31. package/dist/core/discover.js.map +1 -0
  32. package/dist/core/fingerprint.cjs +84 -0
  33. package/dist/core/inventory.d.ts +35 -0
  34. package/dist/core/inventory.d.ts.map +1 -0
  35. package/dist/core/inventory.js +69 -0
  36. package/dist/core/inventory.js.map +1 -0
  37. package/dist/core/keychain.d.ts.map +1 -0
  38. package/dist/core/keychain.js.map +1 -0
  39. package/dist/core/mcp/guard-client.cjs +86 -0
  40. package/dist/core/mcp/interceptor.cjs +238 -0
  41. package/dist/core/mcp/jsonrpc.cjs +194 -0
  42. package/dist/core/mcp/transport/http-server-side.cjs +89 -0
  43. package/dist/core/mcp/transport/http-upstream.cjs +111 -0
  44. package/dist/core/mcp/transport/http_forward.cjs +40 -0
  45. package/dist/core/mcp/transport/http_input.cjs +46 -0
  46. package/dist/core/mcp/transport/http_reverse.cjs +33 -0
  47. package/dist/core/mcp/transport/index.cjs +32 -0
  48. package/dist/core/mcp/transport/sse_bridge.cjs +101 -0
  49. package/dist/core/mcp/transport/stdio.cjs +60 -0
  50. package/dist/core/mcp-interpose.cjs +141 -0
  51. package/dist/core/mcp-protect.d.ts +69 -0
  52. package/dist/core/mcp-protect.d.ts.map +1 -0
  53. package/dist/core/mcp-protect.js +205 -0
  54. package/dist/core/mcp-protect.js.map +1 -0
  55. package/dist/core/mcp-scan.d.ts +40 -0
  56. package/dist/core/mcp-scan.d.ts.map +1 -0
  57. package/dist/core/mcp-scan.js +201 -0
  58. package/dist/core/mcp-scan.js.map +1 -0
  59. package/dist/core/mock-server.d.ts.map +1 -0
  60. package/dist/{mock-server.js → core/mock-server.js} +41 -0
  61. package/dist/core/mock-server.js.map +1 -0
  62. package/package.json +10 -10
  63. package/config.yaml.example +0 -84
  64. package/dist/cli.d.ts.map +0 -1
  65. package/dist/cli.js +0 -298
  66. package/dist/cli.js.map +0 -1
  67. package/dist/config.d.ts.map +0 -1
  68. package/dist/config.js.map +0 -1
  69. package/dist/constants.d.ts.map +0 -1
  70. package/dist/constants.js.map +0 -1
  71. package/dist/installer.d.ts.map +0 -1
  72. package/dist/installer.js.map +0 -1
  73. package/dist/keychain.d.ts.map +0 -1
  74. package/dist/keychain.js.map +0 -1
  75. package/dist/mock-server.d.ts.map +0 -1
  76. package/dist/mock-server.js.map +0 -1
  77. package/dist/scanner.d.ts.map +0 -1
  78. package/dist/scanner.js.map +0 -1
  79. /package/dist/{cli.d.ts → bin/cli.d.ts} +0 -0
  80. /package/dist/{scanner.d.ts → connectors/claude-code/src/scanner.d.ts} +0 -0
  81. /package/dist/{keychain.d.ts → core/keychain.d.ts} +0 -0
  82. /package/dist/{keychain.js → core/keychain.js} +0 -0
  83. /package/{hooks → dist/core}/metadata.cjs +0 -0
  84. /package/dist/{mock-server.d.ts → core/mock-server.d.ts} +0 -0
@@ -0,0 +1,69 @@
1
+ // core/inventory — UNIVERSAL tool-inventory types + Guard submission. The
2
+ // connector-specific local discovery (collectInventory) lives in each connector
3
+ // (e.g. connectors/claude-code/src/collect.ts); this module only defines the
4
+ // shared ScannedTool shape, the rug-pull pin, and the /v1/guard submit.
5
+ import os from 'node:os';
6
+ import path from 'node:path';
7
+ import http from 'node:http';
8
+ import https from 'node:https';
9
+ import { createRequire } from 'node:module';
10
+ import { fileURLToPath } from 'node:url';
11
+ /** Attach prev_* fingerprints from the local pin (~/.guardion/fingerprints.json)
12
+ * so the server can flag a changed tool as a rug-pull. Best-effort; never throws. */
13
+ export function pinInventory(tools) {
14
+ try {
15
+ const requireCjs = createRequire(import.meta.url);
16
+ // fingerprint.cjs lives beside this module (core/); dist mirrors source.
17
+ const fpPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), 'fingerprint.cjs');
18
+ const { pinAndEnrich } = requireCjs(fpPath);
19
+ return pinAndEnrich(tools, path.join(os.homedir(), '.guardion', 'fingerprints.json'));
20
+ }
21
+ catch {
22
+ return tools;
23
+ }
24
+ }
25
+ /** POST the inventory to Guard API /v1/guard, tagged snapshot_source=plugin_scan. */
26
+ export function submitInventory(opts) {
27
+ const { apiUrl, token, policy, application } = opts;
28
+ const tools = pinInventory(opts.tools); // attach rug-pull prev_* fingerprints
29
+ const body = JSON.stringify({
30
+ tools,
31
+ policy: policy || undefined,
32
+ application: application || 'claude-code',
33
+ log: true,
34
+ snapshot_source: 'plugin_scan',
35
+ });
36
+ return new Promise((resolve, reject) => {
37
+ let url;
38
+ try {
39
+ url = new URL('/v1/guard', apiUrl);
40
+ }
41
+ catch (e) {
42
+ return reject(e);
43
+ }
44
+ const transport = url.protocol === 'https:' ? https : http;
45
+ const req = transport.request({
46
+ hostname: url.hostname,
47
+ port: url.port,
48
+ path: url.pathname,
49
+ method: 'POST',
50
+ timeout: 8000,
51
+ headers: {
52
+ 'Content-Type': 'application/json',
53
+ Authorization: `Bearer ${token}`,
54
+ 'Content-Length': Buffer.byteLength(body),
55
+ },
56
+ }, (res) => {
57
+ res.resume();
58
+ res.on('end', () => resolve({ status: res.statusCode ?? 0 }));
59
+ });
60
+ req.on('error', reject);
61
+ req.on('timeout', () => {
62
+ req.destroy();
63
+ reject(new Error('timeout'));
64
+ });
65
+ req.write(body);
66
+ req.end();
67
+ });
68
+ }
69
+ //# sourceMappingURL=inventory.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inventory.js","sourceRoot":"","sources":["../../core/inventory.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ {"version":3,"file":"keychain.d.ts","sourceRoot":"","sources":["../../core/keychain.ts"],"names":[],"mappings":"AAaA,wBAAgB,QAAQ,IAAI,MAAM,CAiCjC;AAID,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAsB5C;AAED,wBAAgB,UAAU,IAAI,IAAI,CAwBjC"}
@@ -0,0 +1 @@
1
+ {"version":3,"file":"keychain.js","sourceRoot":"","sources":["../../core/keychain.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,86 @@
1
+ 'use strict';
2
+ /**
3
+ * Guard control-plane client for the interposer. We delegate ALL detection to
4
+ * Guard (POST /v1/guard) — never re-implement it here. This module only does the
5
+ * HTTP round-trip and interprets the response (block / sanitize / reason).
6
+ *
7
+ * Decision mapping (AARM → MCP):
8
+ * res.deny === true → BLOCK (JSON-RPC error)
9
+ * res.correction / res.redacted → SANITIZE (replace content with redacted text)
10
+ * else → ALLOW (forward)
11
+ * Unreachable Guard (null) → fail-open unless failClosed.
12
+ */
13
+ const http = require('http');
14
+ const https = require('https');
15
+
16
+ function createGuardClient(opts) {
17
+ const { apiUrl, token, policy, application, timeout, failClosed } = opts;
18
+
19
+ // `message` is a single message object OR an ordered array of messages (one per
20
+ // redactable leaf). Correction choices come back 1:1 in the same order.
21
+ function evaluate(message, tools) {
22
+ return new Promise((resolve) => {
23
+ if (!token) return resolve(null);
24
+ let url; try { url = new URL('/v1/guard', apiUrl); } catch { return resolve(null); }
25
+ const tr = url.protocol === 'https:' ? https : http;
26
+ const payload = { application: application || 'mcp-interpose', policy, log: true };
27
+ if (message) payload.messages = Array.isArray(message) ? message : [message];
28
+ if (tools) payload.tools = tools;
29
+ const body = JSON.stringify(payload);
30
+ const req = tr.request({
31
+ hostname: url.hostname, port: url.port || (url.protocol === 'https:' ? 443 : 80),
32
+ path: url.pathname, method: 'POST', timeout: timeout || 3000,
33
+ headers: {
34
+ 'Content-Type': 'application/json',
35
+ 'Authorization': `Bearer ${token}`,
36
+ 'Content-Length': Buffer.byteLength(body),
37
+ },
38
+ }, (r) => {
39
+ let d = ''; r.setEncoding('utf8');
40
+ r.on('data', (c) => { d += c; });
41
+ r.on('end', () => { try { resolve(JSON.parse(d)); } catch { resolve(null); } });
42
+ });
43
+ req.on('error', () => resolve(null));
44
+ req.on('timeout', () => { req.destroy(); resolve(null); });
45
+ req.write(body); req.end();
46
+ });
47
+ }
48
+
49
+ // null (unreachable) ⇒ fail-open unless failClosed. res.deny ⇒ block.
50
+ function shouldBlock(res) {
51
+ if (!res) return !!failClosed;
52
+ return res.deny === true;
53
+ }
54
+
55
+ function reasonOf(res) {
56
+ try {
57
+ const b = res && Array.isArray(res.breakdown) ? res.breakdown[0] : null;
58
+ const l = b && (b.top_label || b.label);
59
+ return l ? `Guardion: ${l}` : 'Guardion governance';
60
+ } catch { return 'Guardion governance'; }
61
+ }
62
+
63
+ // Sanitized text from a correction (the redacted message content), or null.
64
+ function correctionText(res) {
65
+ try {
66
+ if (!res) return null;
67
+ const ch = res.correction && Array.isArray(res.correction.choices) ? res.correction.choices : null;
68
+ if (ch && ch.length && typeof ch[0].content === 'string') return ch[0].content;
69
+ } catch { /* ignore */ }
70
+ return null;
71
+ }
72
+
73
+ // Redacted content per submitted message, in order (choices are 1:1 with the
74
+ // messages sent). Entry is null when that choice carried no string content.
75
+ function corrections(res) {
76
+ try {
77
+ const ch = res && res.correction && Array.isArray(res.correction.choices) ? res.correction.choices : null;
78
+ if (!ch) return [];
79
+ return ch.map((c) => (c && typeof c.content === 'string' ? c.content : null));
80
+ } catch { return []; }
81
+ }
82
+
83
+ return { evaluate, shouldBlock, reasonOf, correctionText, corrections };
84
+ }
85
+
86
+ module.exports = { createGuardClient };
@@ -0,0 +1,238 @@
1
+ 'use strict';
2
+ /**
3
+ * Transport-agnostic interception brain (Layer 2, AGT MCP-Security-Gateway-1.0).
4
+ *
5
+ * Given injected `sendClient` / `sendServer` and a Guard client, it returns the
6
+ * two relay handlers a transport drives:
7
+ * onClient(msg) — host → server (requests we scan as INPUT)
8
+ * onServer(msg) — server → host (results we scan as OUTPUT; server-initiated
9
+ * sampling requests we scan as INPUT; tools/list integrity)
10
+ *
11
+ * It is the same logic for stdio / http / sse — only the I/O differs (see
12
+ * transport/*). Detection is delegated to Guard; this file only decides
13
+ * block / sanitize / forward and tracks request↔response pairing.
14
+ *
15
+ * Improvements over the fire-and-forget v1:
16
+ * #1 broaden scanned methods: resources/read, prompts/get, sampling/createMessage
17
+ * #5 tool-list integrity AT CONNECT: local rug-pull (fingerprint drift) strips
18
+ * the changed tools; a Guard deny on the toolset refuses the poisoned server
19
+ * (synchronous when enforcing) — no longer fire-and-forget
20
+ * #6 in-proxy redaction: a Guard correction rewrites the result content (SANITIZE)
21
+ */
22
+ const J = require('./jsonrpc.cjs');
23
+
24
+ // host → server requests whose ARGUMENTS we scan as tool input.
25
+ const INPUT_REQUESTS = new Set(['tools/call', 'resources/read', 'prompts/get']);
26
+ // server → host requests we scan as input (the server feeding the host LLM).
27
+ const SERVER_INPUT_REQUESTS = new Set(['sampling/createMessage']);
28
+ // results (matched via the pending request) whose payload we scan as tool output.
29
+ const OUTPUT_METHODS = new Set(['tools/call', 'resources/read', 'prompts/get']);
30
+
31
+ function _inputMessage(serverName, method, params) {
32
+ const ns = serverName ? serverName + '/' : '';
33
+ if (method === 'tools/call') {
34
+ const name = (params && params.name) || '';
35
+ return { role: 'tool_input', name: ns + name,
36
+ content: `${name} ${JSON.stringify((params && params.arguments) || {})}` };
37
+ }
38
+ if (method === 'resources/read') {
39
+ const uri = (params && params.uri) || '';
40
+ return { role: 'tool_input', name: ns + 'resources/read', content: uri };
41
+ }
42
+ if (method === 'prompts/get') {
43
+ const name = (params && params.name) || '';
44
+ return { role: 'tool_input', name: ns + 'prompts/get:' + name,
45
+ content: `${name} ${JSON.stringify((params && params.arguments) || {})}` };
46
+ }
47
+ return null;
48
+ }
49
+
50
+ function createInterceptor(opts) {
51
+ const {
52
+ guard, enforce, dlp = false, integrity = true, redact = true,
53
+ fingerprint, pinPath, serverName, log = () => {},
54
+ sendClient, sendServer,
55
+ } = opts;
56
+
57
+ // Synchronous scan path (await Guard before forwarding) runs for hard
58
+ // enforcement OR standalone DLP. Observe-only stays fire-and-forget. Blocking
59
+ // is gated by `enforce`; anonymization by `redact` — so `dlp` = anonymize,
60
+ // never block.
61
+ const sync = enforce || dlp;
62
+
63
+ const pending = new Map(); // request id → { method, toolName }
64
+
65
+ // ── #5: tool-list integrity ────────────────────────────────────────────────
66
+ // Local rug-pull via the fingerprint pin (deterministic, no Guard round-trip):
67
+ // a tool whose description/schema changed from the pinned baseline is drift.
68
+ function driftedNames(tools) {
69
+ const out = new Set();
70
+ try {
71
+ let defs = tools.map((t) => ({
72
+ name: t.name, description: t.description || '', server: serverName || 'mcp',
73
+ source: 'mcp', snapshot_source: 'mcp_interpose', parameters: J.paramsToList(t.inputSchema),
74
+ }));
75
+ if (fingerprint && fingerprint.pinAndEnrich) {
76
+ defs = fingerprint.pinAndEnrich(defs, pinPath); // attaches prev_* + updates pin
77
+ for (const d of defs) {
78
+ const dh = fingerprint.descriptionHash(d.description);
79
+ const sh = fingerprint.schemaHash(d.parameters);
80
+ if ((d.prev_description_hash && d.prev_description_hash !== dh)
81
+ || (d.prev_schema_hash && d.prev_schema_hash !== sh)) {
82
+ out.add(d.name);
83
+ }
84
+ }
85
+ }
86
+ } catch { /* best-effort */ }
87
+ return out;
88
+ }
89
+
90
+ function toolDefs(tools) {
91
+ let defs = tools.map((t) => ({
92
+ name: t.name, description: t.description || '', server: serverName || 'mcp',
93
+ source: 'mcp', snapshot_source: 'mcp_interpose', parameters: J.paramsToList(t.inputSchema),
94
+ }));
95
+ try { if (fingerprint && fingerprint.pinAndEnrich) defs = fingerprint.pinAndEnrich(defs, pinPath); } catch { /* pin best-effort */ }
96
+ return defs;
97
+ }
98
+
99
+ async function handleToolsList(msg) {
100
+ const tools = msg.result.tools;
101
+ const drifted = (enforce && integrity) ? driftedNames(tools) : new Set();
102
+
103
+ if (!enforce) { // observe-only: fire-and-forget + pin
104
+ try { guard.evaluate(null, toolDefs(tools)); } catch { /* ignore */ }
105
+ return sendClient(msg);
106
+ }
107
+
108
+ // Synchronous poisoning verdict on the toolset (connect-time). A deny means
109
+ // the server is poisoned → refuse to expose ANY of its tools.
110
+ let res = null;
111
+ if (integrity) { try { res = await guard.evaluate(null, toolDefs(tools)); } catch { res = null; } }
112
+ if (integrity && guard.shouldBlock(res)) {
113
+ log(`tools/list blocked (poisoned server): ${guard.reasonOf(res)}`);
114
+ return sendClient(J.rpcError(msg.id, `[guardion] blocked — ${guard.reasonOf(res)}`));
115
+ }
116
+
117
+ if (drifted.size) { // rug-pull: strip changed tools
118
+ const safe = tools.filter((t) => !drifted.has(t.name));
119
+ log(`tools/list rug-pull: stripped ${[...drifted].join(', ')}`);
120
+ msg.result.tools = safe;
121
+ }
122
+ return sendClient(msg);
123
+ }
124
+
125
+ // ── #6: output redaction / block (leaf-level, structure-preserving) ──────────
126
+ async function handleResult(msg, method, toolName) {
127
+ const name = toolName || method;
128
+ if (!sync) { // observe-only: fire-and-forget
129
+ try { guard.evaluate({ role: J.ROLES.output, name, content: J.resultText(msg.result) }); } catch { /* ignore */ }
130
+ return sendClient(msg);
131
+ }
132
+
133
+ // Scan each redactable string leaf; fall back to a flat message for shapes
134
+ // with no text leaves (e.g. image-only) so a deny is still enforced.
135
+ const leaves = J.collectOutputLeaves(method, msg.result);
136
+ const messages = leaves.length
137
+ ? leaves.map((l) => ({ role: J.ROLES.output, name, content: l.text }))
138
+ : { role: J.ROLES.output, name, content: J.resultText(msg.result) };
139
+ const res = await guard.evaluate(messages);
140
+
141
+ if (enforce && guard.shouldBlock(res)) {
142
+ log(`${method} result blocked: ${guard.reasonOf(res)}`);
143
+ return sendClient(J.rpcError(msg.id, `[guardion] blocked — ${guard.reasonOf(res)}`));
144
+ }
145
+ if (redact && res && res.redacted && leaves.length) {
146
+ const corr = guard.corrections(res);
147
+ const applied = leaves.map((l, i) => ({ path: l.path, text: corr[i] != null ? corr[i] : l.text }));
148
+ J.applyLeaves(msg.result, applied); // write back in place; structure preserved
149
+ log(`${method} result sanitized (redaction)`);
150
+ }
151
+ return sendClient(msg);
152
+ }
153
+
154
+ // Input scan messages: one per redactable argument leaf (for redaction
155
+ // write-back) PLUS a trailing identity envelope (tool/prompt name) so
156
+ // tool-name threat detection and fail-closed still fire when arguments are
157
+ // empty. Corrections map 1:1 to `leaves` by index; the envelope is last and
158
+ // ignored for redaction.
159
+ function _inputScan(method, params) {
160
+ const toolName = (params && params.name) || '';
161
+ const scanName = serverName ? serverName + '/' + toolName : toolName;
162
+ const leaves = J.collectInputLeaves(method, params);
163
+ const messages = leaves.map((l) => ({ role: J.ROLES.input, name: scanName, content: l.text }));
164
+ if (method === 'tools/call' && toolName) {
165
+ messages.push({ role: J.ROLES.input, name: scanName, content: toolName });
166
+ } else if (method === 'prompts/get') {
167
+ const pn = serverName ? serverName + '/prompts/get' : 'prompts/get';
168
+ messages.push({ role: J.ROLES.input, name: pn, content: toolName || 'prompts/get' });
169
+ }
170
+ return { leaves, messages };
171
+ }
172
+
173
+ // ── host → server ──────────────────────────────────────────────────────────
174
+ async function onClient(msg) {
175
+ if (J.isRequest(msg) && INPUT_REQUESTS.has(msg.method)) {
176
+ const toolName = (msg.params && msg.params.name) || '';
177
+ pending.set(msg.id, { method: msg.method, toolName });
178
+ if (sync) {
179
+ const { leaves, messages } = _inputScan(msg.method, msg.params);
180
+ if (messages.length) {
181
+ const res = await guard.evaluate(messages);
182
+ if (enforce && guard.shouldBlock(res)) {
183
+ pending.delete(msg.id);
184
+ log(`${msg.method} blocked (input): ${guard.reasonOf(res)}`);
185
+ return sendClient(J.rpcError(msg.id, `[guardion] blocked — ${guard.reasonOf(res)}`));
186
+ }
187
+ if (redact && res && res.redacted && leaves.length) {
188
+ const corr = guard.corrections(res);
189
+ const applied = leaves.map((l, i) => ({ path: l.path, text: corr[i] != null ? corr[i] : l.text }));
190
+ J.applyLeaves(msg.params, applied); // anonymize args in place before forwarding
191
+ log(`${msg.method} input anonymized (redaction)`);
192
+ }
193
+ }
194
+ } else {
195
+ const message = _inputMessage(serverName, msg.method, msg.params);
196
+ if (message) { try { guard.evaluate(message); } catch { /* observe-only */ } }
197
+ }
198
+ return sendServer(msg);
199
+ }
200
+ if (J.isRequest(msg)) pending.set(msg.id, { method: msg.method });
201
+ return sendServer(msg);
202
+ }
203
+
204
+ // ── server → host ────────────────────────────────────────────────────────────
205
+ async function onServer(msg) {
206
+ // server-initiated request (sampling/createMessage): scan what the server is
207
+ // feeding into the host LLM; a deny is returned to the SERVER (it asked).
208
+ if (J.isRequest(msg) && SERVER_INPUT_REQUESTS.has(msg.method)) {
209
+ const content = J.samplingText(msg.params);
210
+ const message = { role: J.ROLES.input, name: (serverName ? serverName + '/' : '') + msg.method, content };
211
+ if (enforce) {
212
+ const res = await guard.evaluate(message);
213
+ if (guard.shouldBlock(res)) {
214
+ log(`${msg.method} blocked (server sampling): ${guard.reasonOf(res)}`);
215
+ return sendServer(J.rpcError(msg.id, `[guardion] blocked — ${guard.reasonOf(res)}`));
216
+ }
217
+ } else {
218
+ try { guard.evaluate(message); } catch { /* observe-only */ }
219
+ }
220
+ return sendClient(msg);
221
+ }
222
+
223
+ const info = J.isResponse(msg) ? pending.get(msg.id) : null;
224
+ if (info) pending.delete(msg.id);
225
+
226
+ if (info && info.method === 'tools/list' && msg.result && Array.isArray(msg.result.tools)) {
227
+ return handleToolsList(msg);
228
+ }
229
+ if (info && OUTPUT_METHODS.has(info.method) && msg.result) {
230
+ return handleResult(msg, info.method, info.toolName);
231
+ }
232
+ return sendClient(msg);
233
+ }
234
+
235
+ return { onClient, onServer };
236
+ }
237
+
238
+ module.exports = { createInterceptor, INPUT_REQUESTS, SERVER_INPUT_REQUESTS, OUTPUT_METHODS };
@@ -0,0 +1,194 @@
1
+ 'use strict';
2
+ /**
3
+ * JSON-RPC / MCP framing + helpers shared by every transport.
4
+ *
5
+ * Zero-dep (node builtins only) so the interposer runs when launched by arbitrary
6
+ * hosts without node_modules. Pure functions — unit-testable standalone.
7
+ */
8
+
9
+ // Canonical Guard message roles for the two MCP scan directions. These MUST match
10
+ // Guard's MessagesRole enum (guard/guard/core/schemas.py) — `tool_response`, NOT
11
+ // `tool_output` (which is a Target name, not a role). A mismatch makes /v1/guard
12
+ // 422 and silently fail-open, so both directions are pinned here once.
13
+ const ROLES = { input: 'tool_input', output: 'tool_response' };
14
+
15
+ // Keys that hold base64 binary — never sent for detection, never rewritten.
16
+ const BINARY_KEYS = new Set(['data', 'blob']);
17
+ // MCP structural / metadata keys carried alongside text in result shapes. Skipped
18
+ // when walking result content so we only redact human-readable text leaves.
19
+ const META_KEYS = new Set(['type', 'mimeType', 'annotations', '_meta', 'role', 'isError', 'size']);
20
+
21
+ /** Incremental newline-delimited JSON-RPC framer (stdio / SSE data lines). */
22
+ class LineFramer {
23
+ constructor() { this.buf = ''; }
24
+ /** Feed a chunk; returns the complete lines it produced (trailing partial kept). */
25
+ push(chunk) {
26
+ this.buf += chunk;
27
+ const out = [];
28
+ let nl;
29
+ while ((nl = this.buf.indexOf('\n')) >= 0) {
30
+ const line = this.buf.slice(0, nl);
31
+ this.buf = this.buf.slice(nl + 1);
32
+ if (line.trim()) out.push(line);
33
+ }
34
+ return out;
35
+ }
36
+ }
37
+
38
+ function parse(line) {
39
+ try { return JSON.parse(line); } catch { return null; }
40
+ }
41
+
42
+ function serialize(obj) {
43
+ return JSON.stringify(obj);
44
+ }
45
+
46
+ function isRequest(msg) {
47
+ return !!(msg && typeof msg === 'object' && msg.method && msg.id != null);
48
+ }
49
+ function isNotification(msg) {
50
+ return !!(msg && typeof msg === 'object' && msg.method && msg.id == null);
51
+ }
52
+ function isResponse(msg) {
53
+ return !!(msg && typeof msg === 'object' && msg.id != null && !msg.method
54
+ && (Object.prototype.hasOwnProperty.call(msg, 'result')
55
+ || Object.prototype.hasOwnProperty.call(msg, 'error')));
56
+ }
57
+
58
+ function rpcError(id, message, code) {
59
+ return { jsonrpc: '2.0', id: id == null ? null : id, error: { code: code || -32000, message } };
60
+ }
61
+
62
+ /** Flatten an MCP tool/resource/prompt result into scannable text. */
63
+ function resultText(result) {
64
+ try {
65
+ if (result && Array.isArray(result.content)) { // tools/call
66
+ return result.content.map((c) => (c && typeof c.text === 'string' ? c.text : '')).join('\n');
67
+ }
68
+ if (result && Array.isArray(result.contents)) { // resources/read
69
+ return result.contents.map((c) => (c && typeof c.text === 'string' ? c.text : '')).join('\n');
70
+ }
71
+ if (result && Array.isArray(result.messages)) { // prompts/get
72
+ return result.messages.map((m) => {
73
+ const c = m && m.content;
74
+ if (c && typeof c.text === 'string') return c.text;
75
+ return typeof c === 'string' ? c : '';
76
+ }).join('\n');
77
+ }
78
+ } catch { /* ignore */ }
79
+ return typeof result === 'string' ? result : JSON.stringify(result || '');
80
+ }
81
+
82
+ /** Replace the textual payload of a result in place with `text` (best-effort SANITIZE). */
83
+ function replaceResultText(result, text) {
84
+ if (!result || typeof result !== 'object') return result;
85
+ if (Array.isArray(result.content)) { result.content = [{ type: 'text', text }]; return result; }
86
+ if (Array.isArray(result.contents)) {
87
+ const uri = (result.contents[0] && result.contents[0].uri) || '';
88
+ result.contents = [{ uri, mimeType: 'text/plain', text }];
89
+ return result;
90
+ }
91
+ if (Array.isArray(result.messages)) {
92
+ result.messages = [{ role: 'user', content: { type: 'text', text } }];
93
+ return result;
94
+ }
95
+ return result;
96
+ }
97
+
98
+ /** Extract the text a server-initiated sampling/createMessage wants the host LLM to run. */
99
+ function samplingText(params) {
100
+ if (!params || typeof params !== 'object') return '';
101
+ const parts = [];
102
+ if (typeof params.systemPrompt === 'string') parts.push(params.systemPrompt);
103
+ for (const m of (Array.isArray(params.messages) ? params.messages : [])) {
104
+ const c = m && m.content;
105
+ if (c && typeof c.text === 'string') parts.push(c.text);
106
+ else if (typeof c === 'string') parts.push(c);
107
+ }
108
+ return parts.join('\n');
109
+ }
110
+
111
+ // ── leaf-level, structure-preserving redaction ───────────────────────────────
112
+ // Detection/redaction is a string transform: we extract every human-readable
113
+ // string leaf (with its JSON path), let Guard redact each, then write the result
114
+ // back IN PLACE — so multiple content blocks, structuredContent, images/audio,
115
+ // blobs, isError, _meta and mimeType all survive untouched. Guard never has to
116
+ // reserialize an MCP payload.
117
+
118
+ /** Recursively collect string leaves of `node` as { path, text }. Skips base64
119
+ * binary keys always; skips MCP metadata keys when `skipMeta` (result shapes). */
120
+ function collectStringLeaves(node, basePath, out, skipMeta) {
121
+ if (node == null) return out;
122
+ if (typeof node === 'string') { out.push({ path: basePath.slice(), text: node }); return out; }
123
+ if (Array.isArray(node)) {
124
+ for (let i = 0; i < node.length; i++) collectStringLeaves(node[i], basePath.concat(i), out, skipMeta);
125
+ return out;
126
+ }
127
+ if (typeof node === 'object') {
128
+ for (const k of Object.keys(node)) {
129
+ // Skip MCP binary (base64) + structural/metadata keys ONLY in result shapes
130
+ // (skipMeta). Tool-input arguments are arbitrary tool-defined keys — a field
131
+ // named `data`/`blob`/`type` there can be redactable text, so scan it.
132
+ if (skipMeta && (BINARY_KEYS.has(k) || META_KEYS.has(k))) continue;
133
+ collectStringLeaves(node[k], basePath.concat(k), out, skipMeta);
134
+ }
135
+ }
136
+ return out; // numbers / booleans are not redactable text
137
+ }
138
+
139
+ /** Redactable string leaves of a tool-call INPUT (relative to `params`). */
140
+ function collectInputLeaves(method, params) {
141
+ const out = [];
142
+ if (!params || typeof params !== 'object') return out;
143
+ if (method === 'tools/call' || method === 'prompts/get') {
144
+ collectStringLeaves(params.arguments, ['arguments'], out, false); // arbitrary tool keys
145
+ } else if (method === 'resources/read') {
146
+ if (typeof params.uri === 'string') out.push({ path: ['uri'], text: params.uri });
147
+ }
148
+ return out;
149
+ }
150
+
151
+ /** Redactable string leaves of a tool/resource/prompt OUTPUT (relative to `result`). */
152
+ function collectOutputLeaves(method, result) {
153
+ const out = [];
154
+ if (!result || typeof result !== 'object') return out;
155
+ if (method === 'tools/call') {
156
+ collectStringLeaves(result.content, ['content'], out, true); // text / resource_link / embedded resource text
157
+ if (result.structuredContent != null) collectStringLeaves(result.structuredContent, ['structuredContent'], out, false);
158
+ } else if (method === 'resources/read') {
159
+ collectStringLeaves(result.contents, ['contents'], out, true); // TextResourceContents.text / .uri (blob skipped)
160
+ } else if (method === 'prompts/get') {
161
+ collectStringLeaves(result.messages, ['messages'], out, true);
162
+ }
163
+ return out;
164
+ }
165
+
166
+ function setByPath(root, p, value) {
167
+ let n = root;
168
+ for (let i = 0; i < p.length - 1; i++) { if (n == null) return; n = n[p[i]]; }
169
+ if (n != null && p.length) n[p[p.length - 1]] = value;
170
+ }
171
+
172
+ /** Write redacted strings back into `root` by path, preserving everything else. */
173
+ function applyLeaves(root, leaves) {
174
+ for (const l of (leaves || [])) {
175
+ if (l && Array.isArray(l.path) && typeof l.text === 'string') setByPath(root, l.path, l.text);
176
+ }
177
+ return root;
178
+ }
179
+
180
+ function paramsToList(inputSchema) {
181
+ const props = inputSchema && inputSchema.properties ? inputSchema.properties : {};
182
+ return Object.keys(props).map((k) => ({
183
+ name: k,
184
+ type: (props[k] && props[k].type) || '',
185
+ description: (props[k] && props[k].description) || '',
186
+ }));
187
+ }
188
+
189
+ module.exports = {
190
+ ROLES,
191
+ LineFramer, parse, serialize, isRequest, isNotification, isResponse,
192
+ rpcError, resultText, replaceResultText, samplingText, paramsToList,
193
+ collectStringLeaves, collectInputLeaves, collectOutputLeaves, applyLeaves, setByPath,
194
+ };