@guardion/guardion 0.2.0 → 0.4.0

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 (89) 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/dist/connectors/claude-code/hooks/guardion-hook.cjs +355 -0
  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} +2 -1
  14. package/dist/connectors/claude-code/src/installer.d.ts.map +1 -0
  15. package/dist/connectors/claude-code/src/installer.js +190 -0
  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/core/config.d.ts +239 -0
  21. package/dist/core/config.d.ts.map +1 -0
  22. package/dist/core/config.js +154 -0
  23. package/dist/core/config.js.map +1 -0
  24. package/dist/{constants.d.ts → core/constants.d.ts} +8 -3
  25. package/dist/core/constants.d.ts.map +1 -0
  26. package/dist/core/constants.js +54 -0
  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/{keychain.js → core/keychain.js} +53 -15
  39. package/dist/core/keychain.js.map +1 -0
  40. package/dist/core/mcp/guard-client.cjs +86 -0
  41. package/dist/core/mcp/interceptor.cjs +238 -0
  42. package/dist/core/mcp/jsonrpc.cjs +194 -0
  43. package/dist/core/mcp/transport/http-server-side.cjs +89 -0
  44. package/dist/core/mcp/transport/http-upstream.cjs +111 -0
  45. package/dist/core/mcp/transport/http_forward.cjs +40 -0
  46. package/dist/core/mcp/transport/http_input.cjs +46 -0
  47. package/dist/core/mcp/transport/http_reverse.cjs +33 -0
  48. package/dist/core/mcp/transport/index.cjs +32 -0
  49. package/dist/core/mcp/transport/sse_bridge.cjs +101 -0
  50. package/dist/core/mcp/transport/stdio.cjs +60 -0
  51. package/dist/core/mcp-interpose.cjs +141 -0
  52. package/dist/core/mcp-protect.d.ts +69 -0
  53. package/dist/core/mcp-protect.d.ts.map +1 -0
  54. package/dist/core/mcp-protect.js +205 -0
  55. package/dist/core/mcp-protect.js.map +1 -0
  56. package/dist/core/mcp-scan.d.ts +40 -0
  57. package/dist/core/mcp-scan.d.ts.map +1 -0
  58. package/dist/core/mcp-scan.js +201 -0
  59. package/dist/core/mcp-scan.js.map +1 -0
  60. package/dist/core/mock-server.d.ts.map +1 -0
  61. package/dist/{mock-server.js → core/mock-server.js} +60 -4
  62. package/dist/core/mock-server.js.map +1 -0
  63. package/package.json +9 -10
  64. package/config.yaml.example +0 -26
  65. package/dist/cli.d.ts.map +0 -1
  66. package/dist/cli.js +0 -289
  67. package/dist/cli.js.map +0 -1
  68. package/dist/config.d.ts +0 -28
  69. package/dist/config.d.ts.map +0 -1
  70. package/dist/config.js +0 -63
  71. package/dist/config.js.map +0 -1
  72. package/dist/constants.d.ts.map +0 -1
  73. package/dist/constants.js +0 -44
  74. package/dist/constants.js.map +0 -1
  75. package/dist/installer.d.ts.map +0 -1
  76. package/dist/installer.js +0 -137
  77. package/dist/installer.js.map +0 -1
  78. package/dist/keychain.d.ts.map +0 -1
  79. package/dist/keychain.js.map +0 -1
  80. package/dist/mock-server.d.ts.map +0 -1
  81. package/dist/mock-server.js.map +0 -1
  82. package/dist/scanner.d.ts.map +0 -1
  83. package/dist/scanner.js.map +0 -1
  84. package/hooks/guardion-hook.cjs +0 -202
  85. /package/dist/{cli.d.ts → bin/cli.d.ts} +0 -0
  86. /package/dist/{scanner.d.ts → connectors/claude-code/src/scanner.d.ts} +0 -0
  87. /package/dist/{keychain.d.ts → core/keychain.d.ts} +0 -0
  88. /package/{hooks → dist/core}/metadata.cjs +0 -0
  89. /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"}
@@ -1,6 +1,6 @@
1
1
  import { execFileSync } from 'node:child_process';
2
2
  import fs from 'node:fs';
3
- import { KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT, TOKEN_FILE_PATH, SYSTEM_TOKEN_PATH } from './constants.js';
3
+ import { KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT, TOKEN_FILE_PATH, SYSTEM_TOKEN_PATH, } from './constants.js';
4
4
  const KEYCHAIN_TIMEOUT = 2000;
5
5
  // ── Get ───────────────────────────────────────────────────────────────────────
6
6
  export function getToken() {
@@ -25,14 +25,18 @@ export function getToken() {
25
25
  if (t)
26
26
  return t;
27
27
  }
28
- catch { /* absent */ }
28
+ catch {
29
+ /* absent */
30
+ }
29
31
  // 5. User file fallback
30
32
  try {
31
33
  const t = fs.readFileSync(TOKEN_FILE_PATH, 'utf-8').trim();
32
34
  if (t)
33
35
  return t;
34
36
  }
35
- catch { /* absent */ }
37
+ catch {
38
+ /* absent */
39
+ }
36
40
  return '';
37
41
  }
38
42
  // ── Set ───────────────────────────────────────────────────────────────────────
@@ -51,7 +55,10 @@ export function setToken(token) {
51
55
  import('node:path').then(({ default: p }) => {
52
56
  const dir = p.join(o.homedir(), '.guardion');
53
57
  f.mkdirSync(dir, { recursive: true });
54
- f.writeFileSync(TOKEN_FILE_PATH, token, { encoding: 'utf-8', mode: 0o600 });
58
+ f.writeFileSync(TOKEN_FILE_PATH, token, {
59
+ encoding: 'utf-8',
60
+ mode: 0o600,
61
+ });
55
62
  });
56
63
  });
57
64
  });
@@ -60,21 +67,40 @@ export function clearToken() {
60
67
  if (process.platform === 'darwin') {
61
68
  try {
62
69
  execFileSync('security', [
63
- 'delete-generic-password', '-s', KEYCHAIN_SERVICE, '-a', KEYCHAIN_ACCOUNT,
70
+ 'delete-generic-password',
71
+ '-s',
72
+ KEYCHAIN_SERVICE,
73
+ '-a',
74
+ KEYCHAIN_ACCOUNT,
64
75
  ], { timeout: KEYCHAIN_TIMEOUT, stdio: 'pipe' });
65
76
  }
66
- catch { /* not found */ }
77
+ catch {
78
+ /* not found */
79
+ }
67
80
  return;
68
81
  }
69
82
  try {
70
83
  fs.unlinkSync(TOKEN_FILE_PATH);
71
84
  }
72
- catch { /* absent */ }
85
+ catch {
86
+ /* absent */
87
+ }
73
88
  }
74
89
  // ── macOS Keychain ────────────────────────────────────────────────────────────
75
90
  function keychainGet() {
76
91
  try {
77
- return execFileSync('security', ['find-generic-password', '-s', KEYCHAIN_SERVICE, '-a', KEYCHAIN_ACCOUNT, '-w'], { timeout: KEYCHAIN_TIMEOUT, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
92
+ return execFileSync('security', [
93
+ 'find-generic-password',
94
+ '-s',
95
+ KEYCHAIN_SERVICE,
96
+ '-a',
97
+ KEYCHAIN_ACCOUNT,
98
+ '-w',
99
+ ], {
100
+ timeout: KEYCHAIN_TIMEOUT,
101
+ encoding: 'utf8',
102
+ stdio: ['pipe', 'pipe', 'pipe'],
103
+ }).trim();
78
104
  }
79
105
  catch {
80
106
  return '';
@@ -84,22 +110,33 @@ function keychainSet(token) {
84
110
  // Delete existing entry first (add fails if it already exists)
85
111
  try {
86
112
  execFileSync('security', [
87
- 'delete-generic-password', '-s', KEYCHAIN_SERVICE, '-a', KEYCHAIN_ACCOUNT,
113
+ 'delete-generic-password',
114
+ '-s',
115
+ KEYCHAIN_SERVICE,
116
+ '-a',
117
+ KEYCHAIN_ACCOUNT,
88
118
  ], { timeout: KEYCHAIN_TIMEOUT, stdio: 'pipe' });
89
119
  }
90
- catch { /* not found — ok */ }
120
+ catch {
121
+ /* not found — ok */
122
+ }
91
123
  execFileSync('security', [
92
124
  'add-generic-password',
93
- '-s', KEYCHAIN_SERVICE,
94
- '-a', KEYCHAIN_ACCOUNT,
95
- '-w', token,
125
+ '-s',
126
+ KEYCHAIN_SERVICE,
127
+ '-a',
128
+ KEYCHAIN_ACCOUNT,
129
+ '-w',
130
+ token,
96
131
  ], { timeout: KEYCHAIN_TIMEOUT, stdio: 'pipe' });
97
132
  }
98
133
  // ── Windows Credential Manager ────────────────────────────────────────────────
99
134
  function windowsCredGet() {
100
135
  try {
101
136
  const out = execFileSync('cmdkey', ['/list:guardion'], {
102
- timeout: KEYCHAIN_TIMEOUT, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'],
137
+ timeout: KEYCHAIN_TIMEOUT,
138
+ encoding: 'utf8',
139
+ stdio: ['pipe', 'pipe', 'pipe'],
103
140
  });
104
141
  // Parse token from output (stored in generic credential)
105
142
  const match = out.match(/Password:\s*(.+)/);
@@ -111,7 +148,8 @@ function windowsCredGet() {
111
148
  }
112
149
  function windowsCredSet(token) {
113
150
  execFileSync('cmdkey', [`/add:guardion`, `/user:token`, `/pass:${token}`], {
114
- timeout: KEYCHAIN_TIMEOUT, stdio: 'pipe',
151
+ timeout: KEYCHAIN_TIMEOUT,
152
+ stdio: 'pipe',
115
153
  });
116
154
  }
117
155
  //# sourceMappingURL=keychain.js.map
@@ -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 };