@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.
- package/.turbo/turbo-build.log +1 -0
- package/.turbo/turbo-test.log +51 -0
- package/.turbo/turbo-typecheck.log +1 -0
- package/LICENSE +21 -0
- package/README.md +87 -0
- package/dist/config.d.ts +40 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +100 -0
- package/dist/config.js.map +1 -0
- package/dist/daemon/healthMonitor.d.ts +18 -0
- package/dist/daemon/healthMonitor.d.ts.map +1 -0
- package/dist/daemon/healthMonitor.js +185 -0
- package/dist/daemon/healthMonitor.js.map +1 -0
- package/dist/daemon/index.d.ts +2 -0
- package/dist/daemon/index.d.ts.map +1 -0
- package/dist/daemon/index.js +77 -0
- package/dist/daemon/index.js.map +1 -0
- package/dist/daemon/policyCache.d.ts +20 -0
- package/dist/daemon/policyCache.d.ts.map +1 -0
- package/dist/daemon/policyCache.js +124 -0
- package/dist/daemon/policyCache.js.map +1 -0
- package/dist/daemon/socketServer.d.ts +13 -0
- package/dist/daemon/socketServer.d.ts.map +1 -0
- package/dist/daemon/socketServer.js +90 -0
- package/dist/daemon/socketServer.js.map +1 -0
- package/dist/daemon/telemetryBatcher.d.ts +15 -0
- package/dist/daemon/telemetryBatcher.d.ts.map +1 -0
- package/dist/daemon/telemetryBatcher.js +143 -0
- package/dist/daemon/telemetryBatcher.js.map +1 -0
- package/dist/daemonClient.d.ts +5 -0
- package/dist/daemonClient.d.ts.map +1 -0
- package/dist/daemonClient.js +67 -0
- package/dist/daemonClient.js.map +1 -0
- package/dist/dlp.d.ts +29 -0
- package/dist/dlp.d.ts.map +1 -0
- package/dist/dlp.js +58 -0
- package/dist/dlp.js.map +1 -0
- package/dist/emitter.d.ts +34 -0
- package/dist/emitter.d.ts.map +1 -0
- package/dist/emitter.js +121 -0
- package/dist/emitter.js.map +1 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +50 -0
- package/dist/index.js.map +1 -0
- package/dist/interceptor.d.ts +38 -0
- package/dist/interceptor.d.ts.map +1 -0
- package/dist/interceptor.js +93 -0
- package/dist/interceptor.js.map +1 -0
- package/dist/policy.d.ts +41 -0
- package/dist/policy.d.ts.map +1 -0
- package/dist/policy.js +130 -0
- package/dist/policy.js.map +1 -0
- package/dist/proxy.d.ts +43 -0
- package/dist/proxy.d.ts.map +1 -0
- package/dist/proxy.js +238 -0
- package/dist/proxy.js.map +1 -0
- package/dist/stderrLog.d.ts +32 -0
- package/dist/stderrLog.d.ts.map +1 -0
- package/dist/stderrLog.js +45 -0
- package/dist/stderrLog.js.map +1 -0
- package/dummy-path.jsonl +2 -0
- package/package.json +32 -0
- package/src/__tests__/config.test.ts +82 -0
- package/src/__tests__/dlp.test.ts +103 -0
- package/src/__tests__/integration/daemon-shim.test.ts +125 -0
- package/src/__tests__/interceptor.test.ts +229 -0
- package/src/__tests__/unit/policyCache.test.ts +95 -0
- package/src/__tests__/unit/socketServer.test.ts +220 -0
- package/src/__tests__/unit/telemetryBatcher.test.ts +78 -0
- package/src/config.ts +145 -0
- package/src/daemon/healthMonitor.ts +210 -0
- package/src/daemon/index.ts +76 -0
- package/src/daemon/policyCache.ts +143 -0
- package/src/daemon/socketServer.ts +111 -0
- package/src/daemon/telemetryBatcher.ts +147 -0
- package/src/daemonClient.ts +71 -0
- package/src/dlp.ts +72 -0
- package/src/emitter.ts +144 -0
- package/src/index.ts +53 -0
- package/src/interceptor.ts +103 -0
- package/src/policy.ts +152 -0
- package/src/proxy.ts +327 -0
- package/src/stderrLog.ts +56 -0
- package/tsconfig.json +10 -0
- package/tsconfig.tsbuildinfo +1 -0
- 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"}
|
package/dummy-path.jsonl
ADDED
|
@@ -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
|
+
})
|