@intutic/mcp-governance-proxy 1.0.3

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 (87) hide show
  1. package/.turbo/turbo-build.log +1 -0
  2. package/.turbo/turbo-test.log +51 -0
  3. package/.turbo/turbo-typecheck.log +1 -0
  4. package/LICENSE +21 -0
  5. package/README.md +87 -0
  6. package/dist/config.d.ts +40 -0
  7. package/dist/config.d.ts.map +1 -0
  8. package/dist/config.js +100 -0
  9. package/dist/config.js.map +1 -0
  10. package/dist/daemon/healthMonitor.d.ts +18 -0
  11. package/dist/daemon/healthMonitor.d.ts.map +1 -0
  12. package/dist/daemon/healthMonitor.js +185 -0
  13. package/dist/daemon/healthMonitor.js.map +1 -0
  14. package/dist/daemon/index.d.ts +2 -0
  15. package/dist/daemon/index.d.ts.map +1 -0
  16. package/dist/daemon/index.js +77 -0
  17. package/dist/daemon/index.js.map +1 -0
  18. package/dist/daemon/policyCache.d.ts +20 -0
  19. package/dist/daemon/policyCache.d.ts.map +1 -0
  20. package/dist/daemon/policyCache.js +124 -0
  21. package/dist/daemon/policyCache.js.map +1 -0
  22. package/dist/daemon/socketServer.d.ts +13 -0
  23. package/dist/daemon/socketServer.d.ts.map +1 -0
  24. package/dist/daemon/socketServer.js +90 -0
  25. package/dist/daemon/socketServer.js.map +1 -0
  26. package/dist/daemon/telemetryBatcher.d.ts +15 -0
  27. package/dist/daemon/telemetryBatcher.d.ts.map +1 -0
  28. package/dist/daemon/telemetryBatcher.js +143 -0
  29. package/dist/daemon/telemetryBatcher.js.map +1 -0
  30. package/dist/daemonClient.d.ts +5 -0
  31. package/dist/daemonClient.d.ts.map +1 -0
  32. package/dist/daemonClient.js +67 -0
  33. package/dist/daemonClient.js.map +1 -0
  34. package/dist/dlp.d.ts +29 -0
  35. package/dist/dlp.d.ts.map +1 -0
  36. package/dist/dlp.js +58 -0
  37. package/dist/dlp.js.map +1 -0
  38. package/dist/emitter.d.ts +34 -0
  39. package/dist/emitter.d.ts.map +1 -0
  40. package/dist/emitter.js +121 -0
  41. package/dist/emitter.js.map +1 -0
  42. package/dist/index.d.ts +24 -0
  43. package/dist/index.d.ts.map +1 -0
  44. package/dist/index.js +50 -0
  45. package/dist/index.js.map +1 -0
  46. package/dist/interceptor.d.ts +38 -0
  47. package/dist/interceptor.d.ts.map +1 -0
  48. package/dist/interceptor.js +93 -0
  49. package/dist/interceptor.js.map +1 -0
  50. package/dist/policy.d.ts +41 -0
  51. package/dist/policy.d.ts.map +1 -0
  52. package/dist/policy.js +130 -0
  53. package/dist/policy.js.map +1 -0
  54. package/dist/proxy.d.ts +43 -0
  55. package/dist/proxy.d.ts.map +1 -0
  56. package/dist/proxy.js +238 -0
  57. package/dist/proxy.js.map +1 -0
  58. package/dist/stderrLog.d.ts +32 -0
  59. package/dist/stderrLog.d.ts.map +1 -0
  60. package/dist/stderrLog.js +45 -0
  61. package/dist/stderrLog.js.map +1 -0
  62. package/dummy-path.jsonl +2 -0
  63. package/package.json +32 -0
  64. package/src/__tests__/config.test.ts +82 -0
  65. package/src/__tests__/dlp.test.ts +103 -0
  66. package/src/__tests__/integration/daemon-shim.test.ts +125 -0
  67. package/src/__tests__/interceptor.test.ts +229 -0
  68. package/src/__tests__/unit/policyCache.test.ts +95 -0
  69. package/src/__tests__/unit/socketServer.test.ts +220 -0
  70. package/src/__tests__/unit/telemetryBatcher.test.ts +78 -0
  71. package/src/config.ts +145 -0
  72. package/src/daemon/healthMonitor.ts +210 -0
  73. package/src/daemon/index.ts +76 -0
  74. package/src/daemon/policyCache.ts +143 -0
  75. package/src/daemon/socketServer.ts +111 -0
  76. package/src/daemon/telemetryBatcher.ts +147 -0
  77. package/src/daemonClient.ts +71 -0
  78. package/src/dlp.ts +72 -0
  79. package/src/emitter.ts +144 -0
  80. package/src/index.ts +53 -0
  81. package/src/interceptor.ts +103 -0
  82. package/src/policy.ts +152 -0
  83. package/src/proxy.ts +327 -0
  84. package/src/stderrLog.ts +56 -0
  85. package/tsconfig.json +10 -0
  86. package/tsconfig.tsbuildinfo +1 -0
  87. package/vitest.config.ts +9 -0
package/dist/proxy.js ADDED
@@ -0,0 +1,238 @@
1
+ /**
2
+ * proxy.ts — McpGovernanceProxy: transparent stdio MCP proxy with governance interception.
3
+ *
4
+ * Architecture (proxy mode):
5
+ * Harness stdin → [McpGovernanceProxy] → real MCP server stdin
6
+ * Real MCP server stdout → [McpGovernanceProxy] → Harness stdout
7
+ *
8
+ * Architecture (standalone mode — the `intutic` harness entry):
9
+ * Harness ↔ [McpGovernanceProxy as MCP Server] ↔ Control Plane REST API
10
+ * Exposes governance tools: intutic_governance_status, intutic_list_sops, intutic_list_incidents.
11
+ *
12
+ * CRITICAL: Never write to process.stdout except for valid JSON-RPC frames.
13
+ * All logging MUST go to process.stderr via @intutic/logger.
14
+ *
15
+ * JSON-RPC framing: newline-delimited JSON (one JSON object per line).
16
+ *
17
+ * @module
18
+ */
19
+ import * as node_child from 'node:child_process';
20
+ import * as node_readline from 'node:readline';
21
+ import { createStderrLogger as createLogger } from './stderrLog.js';
22
+ import { PolicyClient } from './policy.js';
23
+ import { GovernanceEmitter } from './emitter.js';
24
+ import { ToolCallInterceptor } from './interceptor.js';
25
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
26
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
27
+ import { z } from 'zod';
28
+ const log = createLogger('mcp-governance-proxy');
29
+ /**
30
+ * Build a JSON-RPC 2.0 error response for a blocked tool call.
31
+ */
32
+ function buildBlockResponse(id, reason) {
33
+ return {
34
+ jsonrpc: '2.0',
35
+ id,
36
+ error: {
37
+ code: -32603, // Internal error (closest standard code to "blocked")
38
+ message: `[Intutic Governance] Tool call blocked: ${reason}`,
39
+ },
40
+ };
41
+ }
42
+ /**
43
+ * Write a JSON-RPC frame to stdout (the ONLY valid place to write in a stdio MCP proxy).
44
+ */
45
+ function writeFrame(frame) {
46
+ process.stdout.write(JSON.stringify(frame) + '\n');
47
+ }
48
+ export class McpGovernanceProxy {
49
+ config;
50
+ policy;
51
+ emitter;
52
+ interceptor;
53
+ realServer = null;
54
+ constructor(config) {
55
+ this.config = config;
56
+ this.policy = new PolicyClient(config.controlPlaneUrl, config.apiKey, config.workspaceId, config.policyTtlMs, config.mcpProxyMode);
57
+ this.emitter = new GovernanceEmitter(config.controlPlaneUrl, config.apiKey, config.eventsFilePath, config.workspaceId, config.mcpProxyMode);
58
+ this.interceptor = new ToolCallInterceptor(this.policy, this.emitter, config.failOpen);
59
+ }
60
+ /**
61
+ * Start the proxy. Dispatches to standalone or proxy mode based on config.
62
+ */
63
+ async run() {
64
+ if (this.config.standalone) {
65
+ return this.runStandalone();
66
+ }
67
+ return this.runProxy();
68
+ }
69
+ // ─── Standalone MCP Server Mode ──────────────────────────────────────────────
70
+ /**
71
+ * Run as a standalone MCP server (the `intutic` harness entry).
72
+ * Exposes Intutic governance tools directly to the harness.
73
+ * Gracefully degrades when the control plane is unreachable.
74
+ */
75
+ async runStandalone() {
76
+ log.info({ action: 'standalone_start', workspaceId: this.config.workspaceId }, 'Starting Intutic MCP governance server (standalone mode)');
77
+ const server = new McpServer({
78
+ name: 'intutic',
79
+ version: '0.1.0',
80
+ });
81
+ const cpUrl = this.config.controlPlaneUrl;
82
+ const apiKey = this.config.apiKey;
83
+ const workspaceId = this.config.workspaceId;
84
+ /**
85
+ * Helper: call control plane REST API.
86
+ * Returns null on any error (graceful degradation — never crashes the MCP server).
87
+ */
88
+ async function callControlPlane(path) {
89
+ try {
90
+ const res = await fetch(`${cpUrl}${path}`, {
91
+ headers: {
92
+ 'Authorization': `Bearer ${apiKey}`,
93
+ 'x-workspace-id': workspaceId,
94
+ },
95
+ signal: AbortSignal.timeout(5000),
96
+ });
97
+ if (!res.ok)
98
+ return null;
99
+ return res.json();
100
+ }
101
+ catch {
102
+ return null;
103
+ }
104
+ }
105
+ // Tool: intutic_governance_status
106
+ server.tool('intutic_governance_status', 'Returns the current governance status and health of the Intutic control plane for this workspace.', {}, async () => {
107
+ const health = await callControlPlane('/healthz');
108
+ const status = health
109
+ ? { connected: true, controlPlane: cpUrl, workspaceId, ...health }
110
+ : { connected: false, controlPlane: cpUrl, workspaceId, error: 'Control plane unreachable' };
111
+ return {
112
+ content: [{ type: 'text', text: JSON.stringify(status, null, 2) }],
113
+ };
114
+ });
115
+ // Tool: intutic_list_sops
116
+ server.tool('intutic_list_sops', 'Lists Standard Operating Procedures (SOPs) active in this workspace.', { limit: z.number().int().min(1).max(50).default(10).describe('Number of SOPs to return (1–50)') }, async ({ limit }) => {
117
+ const data = await callControlPlane(`/api/v1/sops?workspaceId=${workspaceId}&limit=${limit}`);
118
+ const text = data
119
+ ? JSON.stringify(data, null, 2)
120
+ : 'Could not reach control plane to list SOPs.';
121
+ return {
122
+ content: [{ type: 'text', text }],
123
+ };
124
+ });
125
+ // Tool: intutic_list_incidents
126
+ server.tool('intutic_list_incidents', 'Lists recent governance incidents (policy violations, blocked tool calls) in this workspace.', { limit: z.number().int().min(1).max(50).default(10).describe('Number of incidents to return (1–50)') }, async ({ limit }) => {
127
+ const data = await callControlPlane(`/api/v1/incidents?workspaceId=${workspaceId}&limit=${limit}`);
128
+ const text = data
129
+ ? JSON.stringify(data, null, 2)
130
+ : 'Could not reach control plane to list incidents.';
131
+ return {
132
+ content: [{ type: 'text', text }],
133
+ };
134
+ });
135
+ const transport = new StdioServerTransport();
136
+ await server.connect(transport);
137
+ log.info({ action: 'standalone_ready' }, 'Intutic MCP server ready');
138
+ // Keep alive until stdin closes (harness disconnects)
139
+ await new Promise((resolve) => {
140
+ process.stdin.on('close', resolve);
141
+ process.on('SIGINT', resolve);
142
+ process.on('SIGTERM', resolve);
143
+ });
144
+ await server.close();
145
+ }
146
+ // ─── Proxy Mode ───────────────────────────────────────────────────────────────
147
+ /**
148
+ * Start the proxy. Spawns the real MCP server and begins proxying stdin/stdout.
149
+ * Returns a promise that resolves when the real server exits.
150
+ */
151
+ async runProxy() {
152
+ const [cmd, ...args] = this.config.realServerCommand;
153
+ log.info({ action: 'proxy_start', cmd, args, workspaceId: this.config.workspaceId }, 'Starting MCP governance proxy');
154
+ // Start background policy refresh
155
+ this.policy.start();
156
+ // Spawn the real MCP server
157
+ const realServer = node_child.spawn(cmd, args, {
158
+ stdio: ['pipe', 'pipe', 'inherit'], // inherit stderr so real server logs appear normally
159
+ });
160
+ this.realServer = realServer;
161
+ // Handle graceful shutdown
162
+ const shutdown = (signal) => {
163
+ log.info({ action: 'proxy_shutdown', signal }, 'Shutting down MCP governance proxy');
164
+ this.policy.stop();
165
+ if (!realServer.killed) {
166
+ realServer.kill();
167
+ }
168
+ process.exit(0);
169
+ };
170
+ process.on('SIGINT', () => shutdown('SIGINT'));
171
+ process.on('SIGTERM', () => shutdown('SIGTERM'));
172
+ // Real server stdout → harness stdout (pass through)
173
+ realServer.stdout.on('data', (chunk) => {
174
+ process.stdout.write(chunk);
175
+ });
176
+ // Harness stdin → governance interceptor → real server stdin
177
+ const rl = node_readline.createInterface({ input: process.stdin, terminal: false });
178
+ rl.on('line', (line) => {
179
+ const trimmed = line.trim();
180
+ if (!trimmed)
181
+ return;
182
+ let msg;
183
+ try {
184
+ msg = JSON.parse(trimmed);
185
+ }
186
+ catch {
187
+ log.warn({ action: 'parse_error', line: trimmed.slice(0, 100) }, 'Failed to parse JSON-RPC line');
188
+ return;
189
+ }
190
+ // Intercept tools/call
191
+ if (msg.method === 'tools/call') {
192
+ const params = msg.params;
193
+ const toolName = params?.name ?? '<unknown>';
194
+ const toolInput = params?.arguments ?? {};
195
+ // Run governance check asynchronously
196
+ this.interceptor.decide(toolName, toolInput).then((decision) => {
197
+ if (decision.action === 'block') {
198
+ log.warn({ action: 'tool_blocked', toolName, reason: decision.reason }, 'Tool call blocked by governance proxy');
199
+ writeFrame(buildBlockResponse(msg.id, decision.reason));
200
+ }
201
+ else {
202
+ // Allow: forward to real server
203
+ realServer.stdin.write(line + '\n');
204
+ }
205
+ }).catch((err) => {
206
+ // Governance check failed — fail-open: forward to real server
207
+ log.error({ action: 'interceptor_error', err: err.message }, 'Interceptor error — failing open');
208
+ realServer.stdin.write(line + '\n');
209
+ });
210
+ }
211
+ else {
212
+ // Non-tools/call messages pass through immediately
213
+ realServer.stdin.write(line + '\n');
214
+ }
215
+ });
216
+ rl.on('close', () => {
217
+ log.info({ action: 'stdin_closed' }, 'Harness stdin closed — shutting down');
218
+ this.policy.stop();
219
+ if (!realServer.killed) {
220
+ realServer.kill();
221
+ }
222
+ });
223
+ // Wait for the real server to exit
224
+ return new Promise((resolve, reject) => {
225
+ realServer.on('exit', (code, signal) => {
226
+ log.info({ action: 'real_server_exit', code, signal }, 'Real MCP server exited');
227
+ this.policy.stop();
228
+ resolve();
229
+ });
230
+ realServer.on('error', (err) => {
231
+ log.error({ action: 'real_server_error', err: err.message }, 'Real MCP server process error');
232
+ this.policy.stop();
233
+ reject(err);
234
+ });
235
+ });
236
+ }
237
+ }
238
+ //# sourceMappingURL=proxy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"proxy.js","sourceRoot":"","sources":["../src/proxy.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,32 @@
1
+ /**
2
+ * stderrLog.ts — Lightweight structured logger for the MCP governance proxy.
3
+ *
4
+ * CRITICAL: In an MCP stdio server, stdout is exclusively for JSON-RPC frames.
5
+ * This module bypasses pino (which defaults to stdout) and writes structured
6
+ * JSON logs directly to process.stderr (fd 2).
7
+ *
8
+ * Do NOT use @intutic/logger anywhere in packages/mcp-proxy — it routes to
9
+ * stdout unless PINO_DEST=stderr is set, which is fragile across harnesses.
10
+ *
11
+ * @module
12
+ */
13
+ interface LogBindings {
14
+ [key: string]: unknown;
15
+ }
16
+ /**
17
+ * A stderr-only structured logger that matches the @intutic/logger call surface.
18
+ */
19
+ export declare class StderrLogger {
20
+ private readonly name;
21
+ private readonly staticBindings;
22
+ constructor(name: string, bindings?: LogBindings);
23
+ private write;
24
+ debug(bindings: LogBindings, msg: string): void;
25
+ info(bindings: LogBindings, msg: string): void;
26
+ warn(bindings: LogBindings, msg: string): void;
27
+ error(bindings: LogBindings, msg: string): void;
28
+ child(bindings: LogBindings): StderrLogger;
29
+ }
30
+ export declare function createStderrLogger(name: string, bindings?: LogBindings): StderrLogger;
31
+ export {};
32
+ //# sourceMappingURL=stderrLog.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stderrLog.d.ts","sourceRoot":"","sources":["../src/stderrLog.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAIH,UAAU,WAAW;IACnB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;CACvB;AAED;;GAEG;AACH,qBAAa,YAAY;IACvB,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAQ;IAC7B,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAa;gBAEhC,IAAI,EAAE,MAAM,EAAE,QAAQ,GAAE,WAAgB;IAKpD,OAAO,CAAC,KAAK;IAYb,KAAK,CAAC,QAAQ,EAAE,WAAW,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI;IAC/C,IAAI,CAAC,QAAQ,EAAE,WAAW,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI;IAC9C,IAAI,CAAC,QAAQ,EAAE,WAAW,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI;IAC9C,KAAK,CAAC,QAAQ,EAAE,WAAW,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI;IAE/C,KAAK,CAAC,QAAQ,EAAE,WAAW,GAAG,YAAY;CAG3C;AAED,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,WAAW,GAAG,YAAY,CAErF"}
@@ -0,0 +1,45 @@
1
+ /**
2
+ * stderrLog.ts — Lightweight structured logger for the MCP governance proxy.
3
+ *
4
+ * CRITICAL: In an MCP stdio server, stdout is exclusively for JSON-RPC frames.
5
+ * This module bypasses pino (which defaults to stdout) and writes structured
6
+ * JSON logs directly to process.stderr (fd 2).
7
+ *
8
+ * Do NOT use @intutic/logger anywhere in packages/mcp-proxy — it routes to
9
+ * stdout unless PINO_DEST=stderr is set, which is fragile across harnesses.
10
+ *
11
+ * @module
12
+ */
13
+ /**
14
+ * A stderr-only structured logger that matches the @intutic/logger call surface.
15
+ */
16
+ export class StderrLogger {
17
+ name;
18
+ staticBindings;
19
+ constructor(name, bindings = {}) {
20
+ this.name = name;
21
+ this.staticBindings = bindings;
22
+ }
23
+ write(level, bindings, msg) {
24
+ const entry = {
25
+ level,
26
+ time: new Date().toISOString(),
27
+ name: this.name,
28
+ ...this.staticBindings,
29
+ ...bindings,
30
+ msg,
31
+ };
32
+ process.stderr.write(JSON.stringify(entry) + '\n');
33
+ }
34
+ debug(bindings, msg) { this.write('debug', bindings, msg); }
35
+ info(bindings, msg) { this.write('info', bindings, msg); }
36
+ warn(bindings, msg) { this.write('warn', bindings, msg); }
37
+ error(bindings, msg) { this.write('error', bindings, msg); }
38
+ child(bindings) {
39
+ return new StderrLogger(this.name, { ...this.staticBindings, ...bindings });
40
+ }
41
+ }
42
+ export function createStderrLogger(name, bindings) {
43
+ return new StderrLogger(name, bindings);
44
+ }
45
+ //# sourceMappingURL=stderrLog.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stderrLog.js","sourceRoot":"","sources":["../src/stderrLog.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAQH;;GAEG;AACH,MAAM,OAAO,YAAY;IACN,IAAI,CAAQ;IACZ,cAAc,CAAa;IAE5C,YAAY,IAAY,EAAE,WAAwB,EAAE;QAClD,IAAI,CAAC,IAAI,GAAG,IAAI,CAAA;QAChB,IAAI,CAAC,cAAc,GAAG,QAAQ,CAAA;IAChC,CAAC;IAEO,KAAK,CAAC,KAAe,EAAE,QAAqB,EAAE,GAAW;QAC/D,MAAM,KAAK,GAAG;YACZ,KAAK;YACL,IAAI,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;YAC9B,IAAI,EAAE,IAAI,CAAC,IAAI;YACf,GAAG,IAAI,CAAC,cAAc;YACtB,GAAG,QAAQ;YACX,GAAG;SACJ,CAAA;QACD,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC,CAAA;IACpD,CAAC;IAED,KAAK,CAAC,QAAqB,EAAE,GAAW,IAAU,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE,QAAQ,EAAE,GAAG,CAAC,CAAA,CAAC,CAAC;IACtF,IAAI,CAAC,QAAqB,EAAE,GAAW,IAAW,IAAI,CAAC,KAAK,CAAC,MAAM,EAAG,QAAQ,EAAE,GAAG,CAAC,CAAA,CAAC,CAAC;IACtF,IAAI,CAAC,QAAqB,EAAE,GAAW,IAAW,IAAI,CAAC,KAAK,CAAC,MAAM,EAAG,QAAQ,EAAE,GAAG,CAAC,CAAA,CAAC,CAAC;IACtF,KAAK,CAAC,QAAqB,EAAE,GAAW,IAAU,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE,QAAQ,EAAE,GAAG,CAAC,CAAA,CAAC,CAAC;IAEtF,KAAK,CAAC,QAAqB;QACzB,OAAO,IAAI,YAAY,CAAC,IAAI,CAAC,IAAI,EAAE,EAAE,GAAG,IAAI,CAAC,cAAc,EAAE,GAAG,QAAQ,EAAE,CAAC,CAAA;IAC7E,CAAC;CACF;AAED,MAAM,UAAU,kBAAkB,CAAC,IAAY,EAAE,QAAsB;IACrE,OAAO,IAAI,YAAY,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAA;AACzC,CAAC"}
@@ -0,0 +1,2 @@
1
+ {"incidentId":"535a62554f3fd91f7c4d6c4af61fdcc230ff682f","kind":"tool_blocked","toolName":"Bash","toolInput":{"args":["rm -rf"]},"workspaceId":"ws_integration","harnessType":"mcp-governance-proxy","reason":"Blocked in test","timestamp":"2026-06-21T17:41:55.853Z"}
2
+ {"incidentId":"c981afc5cd4f0b9c9b5f320bf958b04a2da43163","kind":"tool_blocked","toolName":"Bash","toolInput":{"args":["rm -rf"]},"workspaceId":"ws_integration","harnessType":"mcp-governance-proxy","reason":"Blocked in test","timestamp":"2026-06-21T17:42:18.325Z"}
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@intutic/mcp-governance-proxy",
3
+ "version": "1.0.3",
4
+ "description": "Intutic MCP Governance Proxy — transparent stdio proxy that intercepts all tools/call JSON-RPC messages and applies workspace SOP policy before forwarding to real MCP servers",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "bin": {
9
+ "intutic-mcp-proxy": "./dist/index.js",
10
+ "intutic-mcp-daemon": "./dist/daemon/index.js"
11
+ },
12
+ "scripts": {
13
+ "build": "tsc",
14
+ "dev": "tsx src/index.ts",
15
+ "typecheck": "tsc --noEmit",
16
+ "test": "vitest run",
17
+ "test:watch": "vitest"
18
+ },
19
+ "dependencies": {
20
+ "@intutic/id": "workspace:*",
21
+ "@intutic/logger": "workspace:*",
22
+ "@modelcontextprotocol/sdk": "^1.29.0",
23
+ "ioredis": "^5",
24
+ "zod": "^3"
25
+ },
26
+ "devDependencies": {
27
+ "@types/node": "^22",
28
+ "tsx": "^4",
29
+ "typescript": "^5.5",
30
+ "vitest": "^3"
31
+ }
32
+ }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * config.test.ts — Unit tests for MCP Governance Proxy configuration loading.
3
+ *
4
+ * Zero vi.mock — tests the actual env parsing and CLI arguments using temporary disk files.
5
+ *
6
+ * @module
7
+ */
8
+
9
+ import { describe, it, expect, beforeAll, afterAll } from 'vitest'
10
+ import * as node_fs from 'node:fs/promises'
11
+ import * as node_os from 'node:os'
12
+ import * as node_path from 'node:path'
13
+ import { loadConfig } from '../config.js'
14
+
15
+ describe('loadConfig', () => {
16
+ const runtimeEnvPath = node_path.join(node_os.homedir(), '.intutic', 'env', 'runtime.env')
17
+ const backupPath = runtimeEnvPath + '.bak'
18
+ let hadOriginalFile = false
19
+
20
+ beforeAll(async () => {
21
+ try {
22
+ // Backup original runtime.env if it exists
23
+ await node_fs.access(runtimeEnvPath)
24
+ await node_fs.copyFile(runtimeEnvPath, backupPath)
25
+ hadOriginalFile = true
26
+ } catch {
27
+ hadOriginalFile = false
28
+ }
29
+
30
+ // Ensure directory exists
31
+ await node_fs.mkdir(node_path.dirname(runtimeEnvPath), { recursive: true })
32
+ })
33
+
34
+ afterAll(async () => {
35
+ // Restore backup
36
+ try {
37
+ await node_fs.unlink(runtimeEnvPath).catch(() => {})
38
+ if (hadOriginalFile) {
39
+ await node_fs.copyFile(backupPath, runtimeEnvPath)
40
+ await node_fs.unlink(backupPath).catch(() => {})
41
+ }
42
+ } catch {
43
+ // Cleanup failed
44
+ }
45
+ })
46
+
47
+ it('loads variables from runtime.env and parses CLI arguments', async () => {
48
+ // Write test runtime.env
49
+ const testEnvContent = [
50
+ '# Test config',
51
+ 'INTUTIC_CONTROL_PLANE_URL=http://test-control-plane:3005',
52
+ 'INTUTIC_API_KEY=sk-test-api-key-value',
53
+ 'INTUTIC_WORKSPACE_ID=ws-test-1234',
54
+ 'INTUTIC_MCP_FAIL_OPEN=false',
55
+ 'INTUTIC_MCP_PROXY_MODE=daemon',
56
+ ].join('\n')
57
+
58
+ await node_fs.writeFile(runtimeEnvPath, testEnvContent, 'utf-8')
59
+
60
+ const config = await loadConfig(['--workspace-id', 'cli-override-ws', '--', 'node', 'server.js', '--port', '8080'])
61
+
62
+ expect(config.controlPlaneUrl).toBe('http://test-control-plane:3005')
63
+ expect(config.apiKey).toBe('sk-test-api-key-value')
64
+ // CLI takes precedence over runtime.env for workspaceId
65
+ expect(config.workspaceId).toBe('cli-override-ws')
66
+ expect(config.failOpen).toBe(false)
67
+ expect(config.mcpProxyMode).toBe('daemon')
68
+ expect(config.realServerCommand).toEqual(['node', 'server.js', '--port', '8080'])
69
+ })
70
+
71
+ it('defaults failOpen to true when not specified', async () => {
72
+ const testEnvContent = [
73
+ 'INTUTIC_WORKSPACE_ID=ws-test-1234',
74
+ ].join('\n')
75
+
76
+ await node_fs.writeFile(runtimeEnvPath, testEnvContent, 'utf-8')
77
+
78
+ const config = await loadConfig(['--', 'node', 'server.js'])
79
+ expect(config.failOpen).toBe(true)
80
+ expect(config.workspaceId).toBe('ws-test-1234')
81
+ })
82
+ })
@@ -0,0 +1,103 @@
1
+ /**
2
+ * dlp.test.ts — Unit tests for the DLP argument scanner.
3
+ *
4
+ * @module
5
+ */
6
+
7
+ import { describe, it, expect } from 'vitest'
8
+ import { scanToolInput, formatDlpBlockReason } from '../dlp.js'
9
+
10
+ describe('scanToolInput', () => {
11
+ it('returns no findings for benign content', () => {
12
+ const result = scanToolInput({ path: '/tmp/file.txt', content: 'hello world' })
13
+ expect(result.hasFinding).toBe(false)
14
+ expect(result.findings).toHaveLength(0)
15
+ })
16
+
17
+ it('detects OpenAI API keys', () => {
18
+ const result = scanToolInput({ env: 'OPENAI_KEY=sk-abc123def456ghi789jkl012mno345pqr' })
19
+ expect(result.hasFinding).toBe(true)
20
+ expect(result.findings.some((f) => f.description.includes('OpenAI'))).toBe(true)
21
+ })
22
+
23
+ it('detects Anthropic API keys', () => {
24
+ const result = scanToolInput({ key: 'sk-ant-api03-verylongantkeyhere12345678901234' })
25
+ expect(result.hasFinding).toBe(true)
26
+ expect(result.findings.some((f) => f.description.includes('Anthropic'))).toBe(true)
27
+ })
28
+
29
+ it('detects GitHub personal access tokens', () => {
30
+ const result = scanToolInput({ token: 'ghp_abcdefghijklmnopqrstuvwxyz123456789012' })
31
+ expect(result.hasFinding).toBe(true)
32
+ expect(result.findings.some((f) => f.description.includes('GitHub'))).toBe(true)
33
+ })
34
+
35
+ it('detects AWS Access Key IDs', () => {
36
+ const result = scanToolInput({ key: 'AKIAIOSFODNN7EXAMPLE' })
37
+ expect(result.hasFinding).toBe(true)
38
+ expect(result.findings.some((f) => f.description.includes('AWS'))).toBe(true)
39
+ })
40
+
41
+ it('detects PEM private keys', () => {
42
+ const result = scanToolInput({ cert: '-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAK...' })
43
+ expect(result.hasFinding).toBe(true)
44
+ expect(result.findings.some((f) => f.description.includes('private key'))).toBe(true)
45
+ })
46
+
47
+ it('detects destructive rm -rf / commands', () => {
48
+ const result = scanToolInput({ command: 'rm -rf /var/data' })
49
+ expect(result.hasFinding).toBe(true)
50
+ expect(result.findings.some((f) => f.description.includes('Destructive'))).toBe(true)
51
+ })
52
+
53
+ it('detects SQL DROP TABLE', () => {
54
+ const result = scanToolInput({ query: 'DROP TABLE users; SELECT 1' })
55
+ expect(result.hasFinding).toBe(true)
56
+ expect(result.findings.some((f) => f.description.includes('DROP TABLE'))).toBe(true)
57
+ })
58
+
59
+ it('detects SQL DROP DATABASE', () => {
60
+ const result = scanToolInput({ sql: 'DROP DATABASE production' })
61
+ expect(result.hasFinding).toBe(true)
62
+ })
63
+
64
+ it('handles null/undefined input gracefully', () => {
65
+ expect(() => scanToolInput(null)).not.toThrow()
66
+ expect(() => scanToolInput(undefined)).not.toThrow()
67
+ const result = scanToolInput(undefined)
68
+ expect(result.hasFinding).toBe(false)
69
+ })
70
+
71
+ it('handles deeply nested objects', () => {
72
+ const result = scanToolInput({ outer: { inner: { key: 'sk-abc123def456ghi789jkl012mno345pqr' } } })
73
+ expect(result.hasFinding).toBe(true)
74
+ })
75
+
76
+ it('returns multiple findings when multiple patterns match', () => {
77
+ const result = scanToolInput({
78
+ openai: 'sk-abc123def456ghi789jkl012mno345pqr',
79
+ aws: 'AKIAIOSFODNN7EXAMPLE',
80
+ })
81
+ expect(result.findings.length).toBeGreaterThanOrEqual(2)
82
+ })
83
+ })
84
+
85
+ describe('formatDlpBlockReason', () => {
86
+ it('formats a single finding into readable text', () => {
87
+ const findings = [{ pattern: 'sk-.*', description: 'OpenAI API key pattern' }]
88
+ const reason = formatDlpBlockReason(findings)
89
+ expect(reason).toContain('DLP scanner')
90
+ expect(reason).toContain('OpenAI API key pattern')
91
+ expect(reason).toContain('•')
92
+ })
93
+
94
+ it('formats multiple findings', () => {
95
+ const findings = [
96
+ { pattern: 'sk-.*', description: 'OpenAI API key' },
97
+ { pattern: 'AKIA.*', description: 'AWS Access Key' },
98
+ ]
99
+ const reason = formatDlpBlockReason(findings)
100
+ expect(reason).toContain('OpenAI API key')
101
+ expect(reason).toContain('AWS Access Key')
102
+ })
103
+ })