@intutic/mcp-governance-proxy 1.4.1 → 1.6.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.
- package/package.json +26 -12
- package/.turbo/turbo-build.log +0 -1
- package/.turbo/turbo-test.log +0 -51
- package/.turbo/turbo-typecheck.log +0 -1
- package/dummy-path.jsonl +0 -2
- package/src/__tests__/config.test.ts +0 -82
- package/src/__tests__/dlp.test.ts +0 -103
- package/src/__tests__/integration/daemon-shim.test.ts +0 -125
- package/src/__tests__/interceptor.test.ts +0 -229
- package/src/__tests__/unit/policyCache.test.ts +0 -95
- package/src/__tests__/unit/socketServer.test.ts +0 -220
- package/src/__tests__/unit/telemetryBatcher.test.ts +0 -78
- package/src/config.ts +0 -145
- package/src/daemon/healthMonitor.ts +0 -210
- package/src/daemon/index.ts +0 -76
- package/src/daemon/policyCache.ts +0 -143
- package/src/daemon/socketServer.ts +0 -111
- package/src/daemon/telemetryBatcher.ts +0 -147
- package/src/daemonClient.ts +0 -71
- package/src/dlp.ts +0 -72
- package/src/emitter.ts +0 -144
- package/src/index.ts +0 -53
- package/src/interceptor.ts +0 -103
- package/src/policy.ts +0 -152
- package/src/proxy.ts +0 -327
- package/src/stderrLog.ts +0 -56
- package/tsconfig.json +0 -10
- package/tsconfig.tsbuildinfo +0 -1
- package/vitest.config.ts +0 -9
package/package.json
CHANGED
|
@@ -1,32 +1,46 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intutic/mcp-governance-proxy",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
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
5
|
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/intutic/intutic",
|
|
10
|
+
"directory": "packages/mcp-proxy"
|
|
11
|
+
},
|
|
6
12
|
"main": "./dist/index.js",
|
|
7
13
|
"types": "./dist/index.d.ts",
|
|
8
14
|
"bin": {
|
|
9
15
|
"intutic-mcp-proxy": "./dist/index.js",
|
|
10
16
|
"intutic-mcp-daemon": "./dist/daemon/index.js"
|
|
11
17
|
},
|
|
12
|
-
"
|
|
13
|
-
"
|
|
14
|
-
"
|
|
15
|
-
"
|
|
16
|
-
|
|
17
|
-
"test:watch": "vitest"
|
|
18
|
-
},
|
|
18
|
+
"files": [
|
|
19
|
+
"dist/",
|
|
20
|
+
"README.md",
|
|
21
|
+
"LICENSE"
|
|
22
|
+
],
|
|
19
23
|
"dependencies": {
|
|
20
|
-
"@intutic/id": "workspace:*",
|
|
21
|
-
"@intutic/logger": "workspace:*",
|
|
22
24
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
23
25
|
"ioredis": "^5",
|
|
24
|
-
"zod": "^3"
|
|
26
|
+
"zod": "^3",
|
|
27
|
+
"@intutic/id": "1.6.0",
|
|
28
|
+
"@intutic/logger": "1.6.0"
|
|
25
29
|
},
|
|
26
30
|
"devDependencies": {
|
|
27
31
|
"@types/node": "^22",
|
|
28
32
|
"tsx": "^4",
|
|
29
33
|
"typescript": "^5.5",
|
|
30
34
|
"vitest": "^3"
|
|
35
|
+
},
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": ">=18"
|
|
38
|
+
},
|
|
39
|
+
"scripts": {
|
|
40
|
+
"build": "tsc",
|
|
41
|
+
"dev": "tsx src/index.ts",
|
|
42
|
+
"typecheck": "tsc --noEmit",
|
|
43
|
+
"test": "vitest run",
|
|
44
|
+
"test:watch": "vitest"
|
|
31
45
|
}
|
|
32
|
-
}
|
|
46
|
+
}
|
package/.turbo/turbo-build.log
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
$ tsc
|
package/.turbo/turbo-test.log
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
$ vitest run
|
|
2
|
-
|
|
3
|
-
RUN v3.2.6 /Users/ishangupta/intutic-enterprise/packages/mcp-proxy
|
|
4
|
-
|
|
5
|
-
✓ src/__tests__/dlp.test.ts (14 tests) 9ms
|
|
6
|
-
stderr | src/__tests__/config.test.ts > loadConfig > loads variables from runtime.env and parses CLI arguments
|
|
7
|
-
[mcp-proxy] INTUTIC_MCP_PROXY_MODE=daemon is stored but not yet active — running in per-session mode (Phase 4). Will be activated in Phase 5.
|
|
8
|
-
|
|
9
|
-
✓ src/__tests__/config.test.ts (2 tests) 20ms
|
|
10
|
-
{"level":"info","time":"2026-07-22T18:22:42.382Z","pid":56861,"hostname":"Ishans-MacBook-Pro-9.local","name":"mcp-proxy.policyCache","workspaceId":"ws_test_cache_miss","msg":"policy_cache.invalidated"}
|
|
11
|
-
{"level":"info","time":"2026-07-22T18:22:42.419Z","pid":56861,"hostname":"Ishans-MacBook-Pro-9.local","name":"mcp-proxy.policyCache","workspaceId":"ws_test_cache_miss","msg":"policy_cache.invalidated"}
|
|
12
|
-
✓ src/__tests__/unit/policyCache.test.ts (4 tests) 210ms
|
|
13
|
-
{"level":"debug","time":"2026-07-22T18:22:42.429Z","name":"mcp-proxy-interceptor","action":"interceptor_decide","toolName":"Bash","msg":"Evaluating tool call"}
|
|
14
|
-
{"level":"warn","time":"2026-07-22T18:22:42.429Z","name":"mcp-proxy-interceptor","action":"dlp_block","toolName":"Bash","findings":[{"pattern":"sk-[A-Za-z0-9]{20,}","description":"OpenAI API key pattern"}],"msg":"DLP block"}
|
|
15
|
-
{"level":"debug","time":"2026-07-22T18:22:42.430Z","name":"mcp-proxy-interceptor","action":"interceptor_decide","toolName":"Write","msg":"Evaluating tool call"}
|
|
16
|
-
{"level":"warn","time":"2026-07-22T18:22:42.431Z","name":"mcp-proxy-interceptor","action":"dlp_block","toolName":"Write","findings":[{"pattern":"sk-ant-[A-Za-z0-9\\-_]{20,}","description":"Anthropic API key pattern"}],"msg":"DLP block"}
|
|
17
|
-
{"level":"debug","time":"2026-07-22T18:22:42.431Z","name":"mcp-proxy-interceptor","action":"interceptor_decide","toolName":"Bash","msg":"Evaluating tool call"}
|
|
18
|
-
{"level":"warn","time":"2026-07-22T18:22:42.431Z","name":"mcp-proxy-interceptor","action":"dlp_block","toolName":"Bash","findings":[{"pattern":"rm\\s+-rf?\\s+\\/","description":"Destructive rm -rf / command"}],"msg":"DLP block"}
|
|
19
|
-
{"level":"debug","time":"2026-07-22T18:22:42.431Z","name":"mcp-proxy-interceptor","action":"interceptor_decide","toolName":"mcp__database__execute","msg":"Evaluating tool call"}
|
|
20
|
-
{"level":"warn","time":"2026-07-22T18:22:42.431Z","name":"mcp-proxy-interceptor","action":"dlp_block","toolName":"mcp__database__execute","findings":[{"pattern":"DROP\\s+TABLE","description":"SQL DROP TABLE statement"}],"msg":"DLP block"}
|
|
21
|
-
{"level":"debug","time":"2026-07-22T18:22:42.431Z","name":"mcp-proxy-interceptor","action":"interceptor_decide","toolName":"Read","msg":"Evaluating tool call"}
|
|
22
|
-
{"level":"debug","time":"2026-07-22T18:22:42.431Z","name":"mcp-proxy-interceptor","action":"interceptor_decide","toolName":"Bash","msg":"Evaluating tool call"}
|
|
23
|
-
{"level":"warn","time":"2026-07-22T18:22:42.432Z","name":"mcp-proxy-interceptor","action":"policy_block","toolName":"Bash","ruleId":"rule-1","reason":"Bash execution not permitted in this workspace","msg":"Policy block"}
|
|
24
|
-
{"level":"debug","time":"2026-07-22T18:22:42.433Z","name":"mcp-proxy-interceptor","action":"interceptor_decide","toolName":"Read","msg":"Evaluating tool call"}
|
|
25
|
-
{"level":"debug","time":"2026-07-22T18:22:42.433Z","name":"mcp-proxy-interceptor","action":"interceptor_decide","toolName":"mcp__postgres__query","msg":"Evaluating tool call"}
|
|
26
|
-
{"level":"warn","time":"2026-07-22T18:22:42.433Z","name":"mcp-proxy-interceptor","action":"policy_block","toolName":"mcp__postgres__query","ruleId":"rule-2","reason":"Production database access blocked","msg":"Policy block"}
|
|
27
|
-
{"level":"debug","time":"2026-07-22T18:22:42.433Z","name":"mcp-proxy-interceptor","action":"interceptor_decide","toolName":"mcp__postgres__query","msg":"Evaluating tool call"}
|
|
28
|
-
{"level":"debug","time":"2026-07-22T18:22:42.436Z","name":"mcp-proxy-interceptor","action":"interceptor_decide","toolName":"Write","msg":"Evaluating tool call"}
|
|
29
|
-
{"level":"warn","time":"2026-07-22T18:22:42.438Z","name":"mcp-proxy-interceptor","action":"policy_approval_required","toolName":"Write","ruleId":"rule-3","msg":"Tool requires human approval per SOP rule rule-3: File writes require review"}
|
|
30
|
-
{"level":"debug","time":"2026-07-22T18:22:42.439Z","name":"mcp-proxy-interceptor","action":"interceptor_decide","toolName":"Read","msg":"Evaluating tool call"}
|
|
31
|
-
{"level":"error","time":"2026-07-22T18:22:42.439Z","name":"mcp-proxy-interceptor","action":"policy_error","err":"Policy engine down","msg":"Policy evaluation error"}
|
|
32
|
-
{"level":"debug","time":"2026-07-22T18:22:42.440Z","name":"mcp-proxy-interceptor","action":"interceptor_decide","toolName":"Read","msg":"Evaluating tool call"}
|
|
33
|
-
{"level":"error","time":"2026-07-22T18:22:42.440Z","name":"mcp-proxy-interceptor","action":"policy_error","err":"Policy engine down","msg":"Policy evaluation error"}
|
|
34
|
-
{"level":"debug","time":"2026-07-22T18:22:42.440Z","name":"mcp-proxy-interceptor","action":"interceptor_decide","toolName":"Read","msg":"Evaluating tool call"}
|
|
35
|
-
{"level":"error","time":"2026-07-22T18:22:42.446Z","name":"mcp-proxy-interceptor","action":"dlp_error","err":"Converting circular structure to JSON\n --> starting at object with constructor 'Object'\n --- property 'self' closes the circle","msg":"DLP scan error — skipping"}
|
|
36
|
-
✓ src/__tests__/interceptor.test.ts (13 tests) 19ms
|
|
37
|
-
{"level":"info","time":"2026-07-22T18:22:42.562Z","pid":56863,"hostname":"Ishans-MacBook-Pro-9.local","name":"mcp-proxy.policyCache","workspaceId":"ws_test","msg":"policy_cache.invalidated"}
|
|
38
|
-
{"level":"info","time":"2026-07-22T18:22:42.570Z","name":"mcp-proxy-policy","action":"policy_refreshed_from_daemon","ruleCount":1,"msg":"SOP rules refreshed from daemon"}
|
|
39
|
-
✓ src/__tests__/unit/socketServer.test.ts (5 tests) 144ms
|
|
40
|
-
{"level":"debug","time":"2026-07-22T18:22:42.571Z","name":"mcp-proxy-emitter","action":"telemetry_enqueued","msg":"Telemetry successfully enqueued to daemon"}
|
|
41
|
-
{"level":"warn","time":"2026-07-22T18:22:42.572Z","name":"mcp-proxy-policy","action":"policy_daemon_failed","err":"connect ENOENT /tmp/nonexistent-1784744562571.sock","msg":"Failed to refresh policy from daemon socket — falling back to HTTP"}
|
|
42
|
-
{"level":"debug","time":"2026-07-22T18:22:42.572Z","name":"mcp-proxy-policy","action":"policy_refresh","url":"http://127.0.0.1:51982/api/v1/sop/rules?workspaceId=ws_fallback&active=true","msg":"Fetching SOP rules from control plane"}
|
|
43
|
-
{"level":"info","time":"2026-07-22T18:22:42.573Z","name":"mcp-proxy-policy","action":"policy_refreshed","ruleCount":1,"msg":"SOP rules refreshed"}
|
|
44
|
-
✓ src/__tests__/integration/daemon-shim.test.ts (4 tests) 141ms
|
|
45
|
-
✓ src/__tests__/unit/telemetryBatcher.test.ts (1 test) 446ms
|
|
46
|
-
|
|
47
|
-
Test Files 7 passed (7)
|
|
48
|
-
Tests 43 passed (43)
|
|
49
|
-
Start at 14:22:41
|
|
50
|
-
Duration 1.15s (transform 482ms, setup 0ms, collect 1.25s, tests 989ms, environment 1ms, prepare 1.18s)
|
|
51
|
-
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
$ tsc --noEmit
|
package/dummy-path.jsonl
DELETED
|
@@ -1,2 +0,0 @@
|
|
|
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"}
|
|
@@ -1,82 +0,0 @@
|
|
|
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
|
-
})
|
|
@@ -1,103 +0,0 @@
|
|
|
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
|
-
})
|
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
|
|
2
|
-
import * as net from 'node:net'
|
|
3
|
-
import * as path from 'node:path'
|
|
4
|
-
import * as os from 'node:os'
|
|
5
|
-
import * as fs from 'node:fs/promises'
|
|
6
|
-
import * as http from 'node:http'
|
|
7
|
-
import { createSocketServer } from '../../daemon/socketServer.js'
|
|
8
|
-
import { callDaemonSocket } from '../../daemonClient.js'
|
|
9
|
-
import { PolicyClient } from '../../policy.js'
|
|
10
|
-
import { GovernanceEmitter } from '../../emitter.js'
|
|
11
|
-
|
|
12
|
-
describe('Daemon-Shim Integration Tests', () => {
|
|
13
|
-
const socketPath = path.join(os.tmpdir(), `mcp-daemon-shim-test-${Date.now()}.sock`)
|
|
14
|
-
let server: net.Server
|
|
15
|
-
let mockCp: http.Server
|
|
16
|
-
let cpPort: number
|
|
17
|
-
let lastWorkspaceId: string | null = null
|
|
18
|
-
let telemetryEvents: any[] = []
|
|
19
|
-
|
|
20
|
-
beforeAll(async () => {
|
|
21
|
-
process.env['MCP_DAEMON_SOCKET'] = socketPath
|
|
22
|
-
await fs.rm(socketPath, { force: true })
|
|
23
|
-
|
|
24
|
-
// Setup mock control plane HTTP server
|
|
25
|
-
mockCp = http.createServer((req, res) => {
|
|
26
|
-
const url = new URL(req.url ?? '', `http://${req.headers.host}`)
|
|
27
|
-
if (url.pathname === '/api/v1/policy/resolve') {
|
|
28
|
-
lastWorkspaceId = url.searchParams.get('workspaceId')
|
|
29
|
-
res.writeHead(200, { 'Content-Type': 'application/json' })
|
|
30
|
-
res.end(
|
|
31
|
-
JSON.stringify({
|
|
32
|
-
workspaceId: lastWorkspaceId,
|
|
33
|
-
sopRules: [
|
|
34
|
-
{ id: 'rule_xyz', toolPattern: 'Bash', action: 'block', reason: 'Blocked in test' }
|
|
35
|
-
],
|
|
36
|
-
dlpPatterns: [],
|
|
37
|
-
interventionMode: 'BLOCK'
|
|
38
|
-
})
|
|
39
|
-
)
|
|
40
|
-
} else if (url.pathname === '/api/v1/sop/rules') {
|
|
41
|
-
lastWorkspaceId = url.searchParams.get('workspaceId')
|
|
42
|
-
res.writeHead(200, { 'Content-Type': 'application/json' })
|
|
43
|
-
res.end(
|
|
44
|
-
JSON.stringify({
|
|
45
|
-
rules: [
|
|
46
|
-
{ id: 'rule_xyz', toolPattern: 'Bash', action: 'block', reason: 'Blocked in test' }
|
|
47
|
-
]
|
|
48
|
-
})
|
|
49
|
-
)
|
|
50
|
-
} else if (url.pathname === '/api/v1/hook-events') {
|
|
51
|
-
let body = ''
|
|
52
|
-
req.on('data', c => body += c)
|
|
53
|
-
req.on('end', () => {
|
|
54
|
-
try {
|
|
55
|
-
telemetryEvents.push(JSON.parse(body))
|
|
56
|
-
} catch {}
|
|
57
|
-
res.writeHead(200, { 'Content-Type': 'application/json' })
|
|
58
|
-
res.end(JSON.stringify({ ok: true }))
|
|
59
|
-
})
|
|
60
|
-
} else {
|
|
61
|
-
res.writeHead(404)
|
|
62
|
-
res.end()
|
|
63
|
-
}
|
|
64
|
-
})
|
|
65
|
-
|
|
66
|
-
await new Promise<void>((resolve) => {
|
|
67
|
-
mockCp.listen(0, '127.0.0.1', () => {
|
|
68
|
-
const addr = mockCp.address() as net.AddressInfo | null
|
|
69
|
-
cpPort = addr?.port ?? 3001
|
|
70
|
-
process.env['CONTROL_PLANE_URL'] = `http://127.0.0.1:${cpPort}`
|
|
71
|
-
process.env['INTUTIC_API_KEY'] = 'test-api-key'
|
|
72
|
-
resolve()
|
|
73
|
-
})
|
|
74
|
-
})
|
|
75
|
-
|
|
76
|
-
// Start daemon socket server
|
|
77
|
-
server = createSocketServer()
|
|
78
|
-
await new Promise<void>((resolve) => server.listen(socketPath, () => resolve()))
|
|
79
|
-
})
|
|
80
|
-
|
|
81
|
-
afterAll(async () => {
|
|
82
|
-
await new Promise<void>((resolve) => server.close(() => resolve()))
|
|
83
|
-
await new Promise<void>((resolve) => mockCp.close(() => resolve()))
|
|
84
|
-
await fs.rm(socketPath, { force: true })
|
|
85
|
-
})
|
|
86
|
-
|
|
87
|
-
it('client callDaemonSocket requests policy.get and gets cached policy', async () => {
|
|
88
|
-
const res = await callDaemonSocket('policy.get', { workspaceId: 'ws_integration' })
|
|
89
|
-
expect(res).toBeDefined()
|
|
90
|
-
expect(res.workspaceId).toBe('ws_integration')
|
|
91
|
-
expect(res.sopRules).toHaveLength(1)
|
|
92
|
-
expect(res.sopRules[0].id).toBe('rule_xyz')
|
|
93
|
-
})
|
|
94
|
-
|
|
95
|
-
it('PolicyClient uses daemon when mcpProxyMode is daemon', async () => {
|
|
96
|
-
const client = new PolicyClient(`http://127.0.0.1:${cpPort}`, 'test-api-key', 'ws_integration', 60000, 'daemon')
|
|
97
|
-
await client.refresh()
|
|
98
|
-
const rules = client.getRules()
|
|
99
|
-
expect(rules).toHaveLength(1)
|
|
100
|
-
expect(rules[0].id).toBe('rule_xyz')
|
|
101
|
-
})
|
|
102
|
-
|
|
103
|
-
it('GovernanceEmitter enqueues telemetry to daemon', async () => {
|
|
104
|
-
const emitter = new GovernanceEmitter(`http://127.0.0.1:${cpPort}`, 'test-api-key', 'dummy-path.jsonl', 'ws_integration', 'daemon')
|
|
105
|
-
emitter.emit('tool_blocked', 'Bash', { args: ['rm -rf'] }, 'Blocked in test')
|
|
106
|
-
|
|
107
|
-
const res = await callDaemonSocket('proxy.health_check', {})
|
|
108
|
-
expect(res.status).toBe('ok')
|
|
109
|
-
})
|
|
110
|
-
|
|
111
|
-
it('shim client falls back to direct control plane requests if daemon socket is offline', async () => {
|
|
112
|
-
// Set socket path to invalid location
|
|
113
|
-
process.env['MCP_DAEMON_SOCKET'] = path.join(os.tmpdir(), `nonexistent-${Date.now()}.sock`)
|
|
114
|
-
|
|
115
|
-
const client = new PolicyClient(`http://127.0.0.1:${cpPort}`, 'test-api-key', 'ws_fallback', 60000, 'daemon')
|
|
116
|
-
await client.refresh()
|
|
117
|
-
const rules = client.getRules()
|
|
118
|
-
expect(rules).toHaveLength(1)
|
|
119
|
-
expect(rules[0].id).toBe('rule_xyz')
|
|
120
|
-
expect(lastWorkspaceId).toBe('ws_fallback') // hit CP directly
|
|
121
|
-
|
|
122
|
-
// Restore
|
|
123
|
-
process.env['MCP_DAEMON_SOCKET'] = socketPath
|
|
124
|
-
})
|
|
125
|
-
})
|