@intutic/mcp-governance-proxy 1.5.0 → 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 CHANGED
@@ -1,32 +1,46 @@
1
1
  {
2
2
  "name": "@intutic/mcp-governance-proxy",
3
- "version": "1.5.0",
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
- "scripts": {
13
- "build": "tsc",
14
- "dev": "tsx src/index.ts",
15
- "typecheck": "tsc --noEmit",
16
- "test": "vitest run",
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
+ }
@@ -1 +0,0 @@
1
- $ tsc
@@ -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
- })