@mhingston5/conduit 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/.env.example +13 -0
  2. package/.github/workflows/ci.yml +88 -0
  3. package/.github/workflows/pr-checks.yml +90 -0
  4. package/.tool-versions +2 -0
  5. package/README.md +177 -0
  6. package/conduit.yaml.test +3 -0
  7. package/docs/ARCHITECTURE.md +35 -0
  8. package/docs/CODE_MODE.md +33 -0
  9. package/docs/SECURITY.md +52 -0
  10. package/logo.png +0 -0
  11. package/package.json +74 -0
  12. package/src/assets/deno-shim.ts +93 -0
  13. package/src/assets/python-shim.py +21 -0
  14. package/src/core/asset.utils.ts +42 -0
  15. package/src/core/concurrency.service.ts +70 -0
  16. package/src/core/config.service.ts +147 -0
  17. package/src/core/execution.context.ts +37 -0
  18. package/src/core/execution.service.ts +209 -0
  19. package/src/core/interfaces/app.config.ts +17 -0
  20. package/src/core/interfaces/executor.interface.ts +31 -0
  21. package/src/core/interfaces/middleware.interface.ts +12 -0
  22. package/src/core/interfaces/url.validator.interface.ts +3 -0
  23. package/src/core/logger.ts +64 -0
  24. package/src/core/metrics.service.ts +112 -0
  25. package/src/core/middleware/auth.middleware.ts +56 -0
  26. package/src/core/middleware/error.middleware.ts +21 -0
  27. package/src/core/middleware/logging.middleware.ts +25 -0
  28. package/src/core/middleware/middleware.builder.ts +24 -0
  29. package/src/core/middleware/ratelimit.middleware.ts +31 -0
  30. package/src/core/network.policy.service.ts +106 -0
  31. package/src/core/ops.server.ts +74 -0
  32. package/src/core/otel.service.ts +41 -0
  33. package/src/core/policy.service.ts +77 -0
  34. package/src/core/registries/executor.registry.ts +26 -0
  35. package/src/core/request.controller.ts +297 -0
  36. package/src/core/security.service.ts +68 -0
  37. package/src/core/session.manager.ts +44 -0
  38. package/src/core/types.ts +47 -0
  39. package/src/executors/deno.executor.ts +342 -0
  40. package/src/executors/isolate.executor.ts +281 -0
  41. package/src/executors/pyodide.executor.ts +327 -0
  42. package/src/executors/pyodide.worker.ts +195 -0
  43. package/src/gateway/auth.service.ts +104 -0
  44. package/src/gateway/gateway.service.ts +345 -0
  45. package/src/gateway/schema.cache.ts +46 -0
  46. package/src/gateway/upstream.client.ts +244 -0
  47. package/src/index.ts +92 -0
  48. package/src/sdk/index.ts +2 -0
  49. package/src/sdk/sdk-generator.ts +245 -0
  50. package/src/sdk/tool-binding.ts +86 -0
  51. package/src/transport/socket.transport.ts +203 -0
  52. package/tests/__snapshots__/assets.test.ts.snap +97 -0
  53. package/tests/assets.test.ts +50 -0
  54. package/tests/auth.service.test.ts +78 -0
  55. package/tests/code-mode-lite-execution.test.ts +84 -0
  56. package/tests/code-mode-lite-gateway.test.ts +150 -0
  57. package/tests/concurrency.service.test.ts +50 -0
  58. package/tests/concurrency.test.ts +41 -0
  59. package/tests/config.service.test.ts +70 -0
  60. package/tests/contract.test.ts +43 -0
  61. package/tests/deno.executor.test.ts +68 -0
  62. package/tests/deno_hardening.test.ts +45 -0
  63. package/tests/dynamic.tool.test.ts +237 -0
  64. package/tests/e2e_stdio_upstream.test.ts +197 -0
  65. package/tests/fixtures/stdio-server.ts +42 -0
  66. package/tests/gateway.manifest.test.ts +82 -0
  67. package/tests/gateway.service.test.ts +58 -0
  68. package/tests/gateway.strict.unit.test.ts +74 -0
  69. package/tests/gateway.validation.unit.test.ts +89 -0
  70. package/tests/gateway_validation.test.ts +86 -0
  71. package/tests/hardening.test.ts +139 -0
  72. package/tests/hardening_v1.test.ts +72 -0
  73. package/tests/isolate.executor.test.ts +100 -0
  74. package/tests/log-limit.test.ts +55 -0
  75. package/tests/middleware.test.ts +106 -0
  76. package/tests/ops.server.test.ts +65 -0
  77. package/tests/policy.service.test.ts +90 -0
  78. package/tests/pyodide.executor.test.ts +101 -0
  79. package/tests/reference_mcp.ts +40 -0
  80. package/tests/remediation.test.ts +119 -0
  81. package/tests/routing.test.ts +148 -0
  82. package/tests/schema.cache.test.ts +27 -0
  83. package/tests/sdk/sdk-generator.test.ts +205 -0
  84. package/tests/socket.transport.test.ts +182 -0
  85. package/tests/stdio_upstream.test.ts +54 -0
  86. package/tsconfig.json +25 -0
  87. package/tsup.config.ts +22 -0
@@ -0,0 +1,58 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { GatewayService } from '../src/gateway/gateway.service.js';
3
+ import { ExecutionContext } from '../src/core/execution.context.js';
4
+ import pino from 'pino';
5
+ import axios from 'axios';
6
+
7
+ vi.mock('axios');
8
+ const logger = pino({ level: 'silent' });
9
+
10
+ describe('GatewayService', () => {
11
+ let gateway: GatewayService;
12
+ let context: ExecutionContext;
13
+
14
+ beforeEach(() => {
15
+ const securityService = {
16
+ validateUrl: vi.fn().mockReturnValue({ valid: true }),
17
+ } as any;
18
+ gateway = new GatewayService(logger, securityService);
19
+ context = new ExecutionContext({ logger });
20
+ vi.clearAllMocks();
21
+ });
22
+
23
+ it('should discover tools from multiple upstreams', async () => {
24
+ gateway.registerUpstream({ id: 'u1', url: 'http://u1' });
25
+ gateway.registerUpstream({ id: 'u2', url: 'http://u2' });
26
+
27
+ (axios.post as any).mockImplementation((url: string) => {
28
+ if (url === 'http://u1') return { data: { result: { tools: [{ name: 't1', inputSchema: {} }] } } };
29
+ if (url === 'http://u2') return { data: { result: { tools: [{ name: 't2', inputSchema: {} }] } } };
30
+ return { data: { result: { tools: [] } } };
31
+ });
32
+
33
+ const tools = await gateway.discoverTools(context);
34
+ expect(tools).toHaveLength(2);
35
+ expect(tools.find(t => t.name === 'u1__t1')).toBeDefined();
36
+ expect(tools.find(t => t.name === 'u2__t2')).toBeDefined();
37
+ });
38
+
39
+ it('should route tool calls to correct upstream', async () => {
40
+ gateway.registerUpstream({ id: 'u1', url: 'http://u1' });
41
+
42
+ (axios.post as any).mockResolvedValue({
43
+ data: { result: { stdout: 'done' } }
44
+ });
45
+
46
+ const response = await gateway.callTool('u1__t1', { arg1: 'val' }, context);
47
+
48
+ expect(axios.post).toHaveBeenCalledWith(
49
+ 'http://u1',
50
+ expect.objectContaining({
51
+ method: 'call_tool',
52
+ params: expect.objectContaining({ name: 't1' })
53
+ }),
54
+ expect.anything()
55
+ );
56
+ expect(response.result.stdout).toBe('done');
57
+ });
58
+ });
@@ -0,0 +1,74 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { GatewayService } from '../src/gateway/gateway.service.js';
3
+ import { ExecutionContext } from '../src/core/execution.context.js';
4
+ import { createLogger } from '../src/core/logger.js';
5
+ import { ConfigService } from '../src/core/config.service.js';
6
+ import { PolicyService } from '../src/core/policy.service.js';
7
+ import { ToolSchema } from '../src/gateway/schema.cache.js';
8
+
9
+ describe('GatewayService (Strict Verification)', () => {
10
+ let gateway: GatewayService;
11
+ let logger: any;
12
+ let mockClient: any;
13
+
14
+ beforeEach(() => {
15
+ logger = createLogger(new ConfigService());
16
+ gateway = new GatewayService(logger, { validateUrl: vi.fn().mockResolvedValue({ valid: true }) } as any, new PolicyService());
17
+
18
+ mockClient = {
19
+ call: vi.fn(),
20
+ getManifest: vi.fn(),
21
+ };
22
+ (gateway as any).clients.set('mock-tool', mockClient);
23
+ });
24
+
25
+ const mockSchema: ToolSchema = {
26
+ name: 'test_tool',
27
+ description: 'A test tool',
28
+ inputSchema: {
29
+ type: 'object',
30
+ properties: {
31
+ foo: { type: 'string' }
32
+ }
33
+ }
34
+ };
35
+
36
+ const mockStub: ToolSchema = {
37
+ name: 'stub_tool',
38
+ description: 'A stub tool',
39
+ inputSchema: undefined,
40
+ };
41
+
42
+ it('should allow call with missing schema in non-strict mode', async () => {
43
+ const context = new ExecutionContext({ logger, strictValidation: false });
44
+ (gateway as any).schemaCache.set('mock-tool', [mockStub]);
45
+ mockClient.call.mockResolvedValue({ result: 'ok' });
46
+
47
+ const response = await gateway.callTool('mock-tool__stub_tool', {}, context);
48
+
49
+ expect(response.error).toBeUndefined();
50
+ expect(mockClient.call).toHaveBeenCalled();
51
+ });
52
+
53
+ it('should block call with missing schema in strict mode', async () => {
54
+ const context = new ExecutionContext({ logger, strictValidation: true });
55
+ (gateway as any).schemaCache.set('mock-tool', [mockStub]);
56
+
57
+ const response = await gateway.callTool('mock-tool__stub_tool', {}, context);
58
+
59
+ expect(response.error).toBeDefined();
60
+ expect(response.error?.code).toBe(-32602);
61
+ expect(response.error?.message).toContain('Strict mode');
62
+ expect(mockClient.call).not.toHaveBeenCalled();
63
+ });
64
+
65
+ it('should block unknown tool in strict mode', async () => {
66
+ const context = new ExecutionContext({ logger, strictValidation: true });
67
+ (gateway as any).schemaCache.set('mock-tool', [mockSchema]);
68
+
69
+ const response = await gateway.callTool('mock-tool__unknown', {}, context);
70
+
71
+ expect(response.error).toBeDefined();
72
+ expect(response.error?.code).toBe(-32601);
73
+ });
74
+ });
@@ -0,0 +1,89 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { GatewayService } from '../src/gateway/gateway.service.js';
3
+ import { ExecutionContext } from '../src/core/execution.context.js';
4
+ import { createLogger } from '../src/core/logger.js';
5
+ import { ConfigService } from '../src/core/config.service.js';
6
+ import { PolicyService } from '../src/core/policy.service.js';
7
+ import { ToolSchema } from '../src/gateway/schema.cache.js';
8
+
9
+ describe('GatewayService (ValidateTool)', () => {
10
+ let gateway: GatewayService;
11
+ let logger: any;
12
+ let mockClient: any;
13
+
14
+ beforeEach(() => {
15
+ logger = createLogger(new ConfigService());
16
+ gateway = new GatewayService(logger, { validateUrl: vi.fn().mockResolvedValue({ valid: true }) } as any, new PolicyService());
17
+
18
+ mockClient = {
19
+ call: vi.fn(),
20
+ getManifest: vi.fn(),
21
+ };
22
+ (gateway as any).clients.set('mock-package', mockClient);
23
+ });
24
+
25
+ const mockSchema: ToolSchema = {
26
+ name: 'test_tool',
27
+ description: 'A test tool',
28
+ inputSchema: {
29
+ type: 'object',
30
+ properties: {
31
+ foo: { type: 'string' },
32
+ bar: { type: 'number' }
33
+ },
34
+ required: ['foo']
35
+ }
36
+ };
37
+
38
+ it('should return valid:true for valid parameters', async () => {
39
+ const context = new ExecutionContext({ logger });
40
+
41
+ // Mock listToolStubs behavior by pre-loading cache or mocking cache
42
+ // Let's use internal method to seed cache
43
+ (gateway as any).schemaCache.set('mock-package', [mockSchema]);
44
+
45
+ const result = await gateway.validateTool('mock-package__test_tool', { foo: 'hello', bar: 123 }, context);
46
+
47
+ expect(result.valid).toBe(true);
48
+ expect(result.errors).toBeUndefined();
49
+ });
50
+
51
+ it('should return valid:false for invalid parameters', async () => {
52
+ const context = new ExecutionContext({ logger });
53
+ (gateway as any).schemaCache.set('mock-package', [mockSchema]);
54
+
55
+ const result = await gateway.validateTool('mock-package__test_tool', { bar: 'wrong type' }, context);
56
+
57
+ expect(result.valid).toBe(false);
58
+ expect(result.errors).toBeDefined();
59
+ // Check for specific error message if possible, but generic check is fine for now
60
+ expect(JSON.stringify(result.errors)).toContain('must have required property');
61
+ });
62
+
63
+ it('should lazy load schema if missing', async () => {
64
+ const context = new ExecutionContext({ logger });
65
+
66
+ // Mock listToolStubs to populate cache
67
+ // We can spy on listToolStubs but it's easier to verify behavior by effect
68
+
69
+ // Actually, listToolStubs populates cache from RPC/Manifest
70
+ mockClient.call.mockResolvedValue({
71
+ result: { tools: [mockSchema] }
72
+ });
73
+
74
+ const result = await gateway.validateTool('mock-package__test_tool', { foo: 'lazy' }, context);
75
+
76
+ expect(mockClient.call).toHaveBeenCalled(); // Should fetch since cache was empty
77
+ expect(result.valid).toBe(true);
78
+ });
79
+
80
+ it('should return error if tool not found', async () => {
81
+ const context = new ExecutionContext({ logger });
82
+ mockClient.call.mockResolvedValue({ result: { tools: [] } });
83
+
84
+ const result = await gateway.validateTool('mock-package__non_existent', {}, context);
85
+
86
+ expect(result.valid).toBe(false);
87
+ expect(result.errors?.[0]).toContain('not found');
88
+ });
89
+ });
@@ -0,0 +1,86 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import { GatewayService } from '../src/gateway/gateway.service.js';
3
+ import { ExecutionContext } from '../src/core/execution.context.js';
4
+ import { UpstreamInfo } from '../src/gateway/upstream.client.js';
5
+ import { createLogger } from '../src/core/logger.js';
6
+ import { ConfigService } from '../src/core/config.service.js';
7
+ import { IUrlValidator } from '../src/core/interfaces/url.validator.interface.js';
8
+ import { PolicyService } from '../src/core/policy.service.js';
9
+
10
+ class MockUrlValidator implements IUrlValidator {
11
+ async validateUrl(url: string) { return { valid: true }; }
12
+ }
13
+
14
+ describe('GatewayService validation', () => {
15
+ let gateway: GatewayService;
16
+ let logger: any;
17
+ let urlValidator: IUrlValidator;
18
+ let policyService: PolicyService;
19
+
20
+ const mockUpstream: UpstreamInfo = {
21
+ id: 'mock-upstream',
22
+ type: 'http',
23
+ url: 'http://localhost:3000/mcp'
24
+ };
25
+
26
+ beforeEach(() => {
27
+ logger = createLogger(new ConfigService());
28
+ urlValidator = new MockUrlValidator();
29
+ policyService = new PolicyService();
30
+ gateway = new GatewayService(logger, urlValidator, policyService);
31
+ gateway.registerUpstream(mockUpstream);
32
+ });
33
+
34
+ afterEach(() => {
35
+ vi.restoreAllMocks();
36
+ });
37
+
38
+ it('should validate params against schema', async () => {
39
+ const client = (gateway as any).clients.get('mock-upstream');
40
+ const callSpy = vi.spyOn(client, 'call').mockResolvedValue({
41
+ jsonrpc: '2.0',
42
+ id: 1,
43
+ result: {
44
+ tools: [{
45
+ name: 'test',
46
+ inputSchema: {
47
+ type: 'object',
48
+ properties: { count: { type: 'number' } },
49
+ required: ['count']
50
+ }
51
+ }]
52
+ }
53
+ });
54
+
55
+ const context = new ExecutionContext({ logger });
56
+
57
+ // Fail validation
58
+ const failParams = { count: 'not a number' };
59
+ const failRes = await gateway.callTool('mock-upstream__test', failParams, context);
60
+ expect(failRes.error).toBeDefined();
61
+ expect(failRes.error?.code).toBe(-32602);
62
+
63
+ // Pass validation
64
+ callSpy.mockResolvedValueOnce({ jsonrpc: '2.0', id: 2, result: { success: true } });
65
+ const passParams = { count: 123 };
66
+ const passRes = await gateway.callTool('mock-upstream__test', passParams, context);
67
+ expect(passRes.error).toBeUndefined();
68
+ });
69
+
70
+ it('should allow tool call if schema is missing (backward compatibility / permissive)', async () => {
71
+ const client = (gateway as any).clients.get('mock-upstream');
72
+ const callSpy = vi.spyOn(client, 'call').mockResolvedValue({
73
+ jsonrpc: '2.0',
74
+ id: 1,
75
+ result: { tools: [{ name: 'noschema' }] } // no inputSchema
76
+ });
77
+
78
+ const context = new ExecutionContext({ logger });
79
+ const params = { any: 'thing' };
80
+
81
+ callSpy.mockResolvedValueOnce({ jsonrpc: '2.0', id: 2, result: { success: true } });
82
+ const res = await gateway.callTool('mock-upstream__noschema', params, context);
83
+
84
+ expect(res.error).toBeUndefined();
85
+ });
86
+ });
@@ -0,0 +1,139 @@
1
+
2
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
3
+ import { SocketTransport } from '../src/transport/socket.transport.js';
4
+ import { RequestController } from '../src/core/request.controller.js';
5
+ import { SecurityService } from '../src/core/security.service.js';
6
+ import { ConcurrencyService } from '../src/core/concurrency.service.js';
7
+ import { ExecutionService } from '../src/core/execution.service.js';
8
+ import { ExecutorRegistry } from '../src/core/registries/executor.registry.js';
9
+ import { buildDefaultMiddleware } from '../src/core/middleware/middleware.builder.js';
10
+ import { pino } from 'pino';
11
+ import net from 'net';
12
+ import os from 'os';
13
+ import path from 'path';
14
+
15
+ const logger = pino({ level: 'silent' });
16
+
17
+ describe('V1 Hardening Tests', () => {
18
+ let transport: SocketTransport;
19
+ let securityService: SecurityService;
20
+ let requestController: RequestController;
21
+ let socketPath: string;
22
+ let mockDenoExecutor: any;
23
+
24
+ beforeEach(async () => {
25
+ const ipcToken = 'master-token';
26
+ securityService = new SecurityService(logger, ipcToken);
27
+ const concurrencyService = new ConcurrencyService(logger, { maxConcurrent: 10 });
28
+
29
+ const gatewayService = {
30
+ callTool: vi.fn(),
31
+ discoverTools: vi.fn().mockResolvedValue([]), // Return empty array for SDK generation
32
+ listToolPackages: vi.fn().mockResolvedValue([]),
33
+ listToolStubs: vi.fn().mockResolvedValue([])
34
+ } as any;
35
+
36
+ const defaultLimits = { timeoutMs: 1000, memoryLimitMb: 128, maxOutputBytes: 1024, maxLogEntries: 5 }; // Low log limit
37
+
38
+ const executorRegistry = new ExecutorRegistry();
39
+
40
+ // Mock DenoExecutor
41
+ mockDenoExecutor = {
42
+ execute: vi.fn().mockResolvedValue({ stdout: '', stderr: '', exitCode: 0 })
43
+ };
44
+ executorRegistry.register('deno', mockDenoExecutor as any);
45
+
46
+ const executionService = new ExecutionService(
47
+ logger,
48
+ defaultLimits,
49
+ gatewayService,
50
+ securityService,
51
+ executorRegistry
52
+ );
53
+ executionService.ipcAddress = '127.0.0.1:0'; // Dummy address for tests
54
+ // Ensure executionService has required methods for RequestController healthCheck/warmup delegation
55
+ vi.spyOn(executionService, 'shutdown').mockResolvedValue();
56
+ vi.spyOn(executionService, 'warmup').mockResolvedValue();
57
+ vi.spyOn(executionService, 'healthCheck').mockResolvedValue({ status: 'ok' });
58
+
59
+ requestController = new RequestController(logger, executionService, gatewayService, buildDefaultMiddleware(securityService));
60
+
61
+ transport = new SocketTransport(logger, requestController, concurrencyService);
62
+
63
+ socketPath = path.join(os.tmpdir(), `conduit-test-${Math.random().toString(36).substring(7)}.sock`);
64
+ if (os.platform() === 'win32') {
65
+ socketPath = '\\\\.\\pipe\\conduit-test-' + Math.random().toString(36).substring(7);
66
+ }
67
+ await transport.listen({ path: socketPath });
68
+ });
69
+
70
+ afterEach(async () => {
71
+ await transport.close();
72
+ });
73
+
74
+ async function sendRequest(request: any): Promise<any> {
75
+ return new Promise((resolve, reject) => {
76
+ const client = net.createConnection({ path: socketPath }, () => {
77
+ client.write(JSON.stringify(request) + '\n');
78
+ });
79
+
80
+ client.once('data', (data) => {
81
+ resolve(JSON.parse(data.toString()));
82
+ client.end();
83
+ });
84
+
85
+ client.on('error', reject);
86
+ });
87
+ }
88
+
89
+ it('should deny executeTypeScript with session token', async () => {
90
+ const sessionToken = securityService.createSession();
91
+
92
+ const response = await sendRequest({
93
+ jsonrpc: '2.0',
94
+ id: 1,
95
+ method: 'mcp.executeTypeScript',
96
+ params: { code: 'console.log("hi")' },
97
+ auth: { bearerToken: sessionToken }
98
+ });
99
+
100
+ expect(response.error).toBeDefined();
101
+ expect(response.error.code).toBe(-32003); // Forbidden
102
+ expect(response.error.message).toContain('Session tokens are restricted');
103
+ });
104
+
105
+ it('should allow discoverTools with session token', async () => {
106
+ const sessionToken = securityService.createSession();
107
+
108
+ // Mock handleDiscoverTools response logic via requestController
109
+ // We mocked gatewayService but requestController calls it.
110
+ // We need to ensure requestController.handleDiscoverTools works.
111
+ // It calls gatewayService.discoverTools.
112
+ (requestController as any).gatewayService.discoverTools.mockResolvedValue([]);
113
+
114
+ const response = await sendRequest({
115
+ jsonrpc: '2.0',
116
+ id: 2,
117
+ method: 'mcp.discoverTools',
118
+ params: {},
119
+ auth: { bearerToken: sessionToken }
120
+ });
121
+
122
+ expect(response.error).toBeUndefined();
123
+ expect(response.result).toBeDefined();
124
+ });
125
+
126
+ it('should allow executeTypeScript with master token', async () => {
127
+ const response = await sendRequest({
128
+ jsonrpc: '2.0',
129
+ id: 3,
130
+ method: 'mcp.executeTypeScript',
131
+ params: { code: 'import * as os from "os"; console.log("hi")' },
132
+ auth: { bearerToken: 'master-token' }
133
+ });
134
+
135
+ // It should call the executor (which we mocked above to success)
136
+ expect(response.error).toBeUndefined();
137
+ expect(mockDenoExecutor.execute).toHaveBeenCalled();
138
+ });
139
+ });
@@ -0,0 +1,72 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import axios from 'axios';
3
+ import dns from 'node:dns/promises';
4
+ import { UpstreamClient } from '../src/gateway/upstream.client.js';
5
+ import { SecurityService } from '../src/core/security.service.js';
6
+ import { AuthService } from '../src/gateway/auth.service.js';
7
+ import { ExecutionContext } from '../src/core/execution.context.js';
8
+ import pino from 'pino';
9
+
10
+ const logger = pino({ level: 'silent' });
11
+
12
+ vi.mock('axios');
13
+ vi.mock('node:dns/promises');
14
+
15
+ describe('V1 Hardening', () => {
16
+ let securityService: SecurityService;
17
+ let upstreamClient: UpstreamClient;
18
+ let authService: AuthService;
19
+
20
+ beforeEach(() => {
21
+ securityService = new SecurityService(logger, 'ipctoken');
22
+ authService = new AuthService(logger);
23
+ upstreamClient = new UpstreamClient(
24
+ logger,
25
+ { id: 'test', url: 'http://example.com/mcp' },
26
+ authService,
27
+ securityService
28
+ );
29
+ vi.clearAllMocks();
30
+ });
31
+
32
+ describe('SSRF Protection', () => {
33
+ it('should disable redirects in axios config', async () => {
34
+ // Mock security check to pass
35
+ vi.spyOn(securityService, 'validateUrl').mockResolvedValue({ valid: true });
36
+ (axios.post as any).mockResolvedValue({ data: { jsonrpc: '2.0', id: 1, result: {} } });
37
+
38
+ await upstreamClient.call(
39
+ { jsonrpc: '2.0', id: 1, method: 'tools/list' },
40
+ new ExecutionContext({ logger })
41
+ );
42
+
43
+ expect(axios.post).toHaveBeenCalledWith(
44
+ 'http://example.com/mcp',
45
+ expect.any(Object),
46
+ expect.objectContaining({ maxRedirects: 0 })
47
+ );
48
+ });
49
+
50
+ it('should check all resolved addresses for private IPs', async () => {
51
+ // Mock dns.lookup to return a private IP in the list
52
+ (dns.lookup as any).mockResolvedValue([
53
+ { address: '1.1.1.1', family: 4 },
54
+ { address: '127.0.0.1', family: 4 } // Private!
55
+ ]);
56
+
57
+ const result = await securityService.validateUrl('http://example.com');
58
+ expect(result.valid).toBe(false);
59
+ expect(result.message).toContain('resolves to private network');
60
+ expect(dns.lookup).toHaveBeenCalledWith('example.com', { all: true });
61
+ });
62
+
63
+ it('should allow benign public IPs', async () => {
64
+ (dns.lookup as any).mockResolvedValue([
65
+ { address: '93.184.216.34', family: 4 }
66
+ ]);
67
+
68
+ const result = await securityService.validateUrl('http://example.com');
69
+ expect(result.valid).toBe(true);
70
+ });
71
+ });
72
+ });
@@ -0,0 +1,100 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { IsolateExecutor } from '../src/executors/isolate.executor.js';
3
+ import { ExecutionContext } from '../src/core/execution.context.js';
4
+ import pino from 'pino';
5
+
6
+ const logger = pino({ level: 'silent' });
7
+
8
+ describe('IsolateExecutor', () => {
9
+ let executor: IsolateExecutor;
10
+ let gatewayService: any;
11
+
12
+ beforeEach(() => {
13
+ gatewayService = {
14
+ callTool: vi.fn().mockResolvedValue({ result: { content: 'test' } }),
15
+ discoverTools: vi.fn().mockResolvedValue([]),
16
+ };
17
+ executor = new IsolateExecutor(logger, gatewayService);
18
+ });
19
+
20
+ it('should execute simple JavaScript code', async () => {
21
+ const code = 'console.log("Hello from isolate")';
22
+ const context = new ExecutionContext({ logger });
23
+ const limits = { timeoutMs: 5000, memoryLimitMb: 128, maxOutputBytes: 1024, maxLogEntries: 100 };
24
+
25
+ const result = await executor.execute(code, limits, context);
26
+
27
+ expect(result.exitCode).toBe(0);
28
+ expect(result.stdout).toContain('Hello from isolate');
29
+ });
30
+
31
+ it('should capture console.error output', async () => {
32
+ const code = 'console.error("Error message")';
33
+ const context = new ExecutionContext({ logger });
34
+ const limits = { timeoutMs: 5000, memoryLimitMb: 128, maxOutputBytes: 1024, maxLogEntries: 100 };
35
+
36
+ const result = await executor.execute(code, limits, context);
37
+
38
+ expect(result.exitCode).toBe(0);
39
+ expect(result.stderr).toContain('Error message');
40
+ });
41
+
42
+ it('should timeout on long execution', async () => {
43
+ const code = 'while(true) {}'; // Infinite loop
44
+ const context = new ExecutionContext({ logger });
45
+ const limits = { timeoutMs: 100, memoryLimitMb: 128, maxOutputBytes: 1024, maxLogEntries: 100 };
46
+
47
+ const result = await executor.execute(code, limits, context);
48
+
49
+ expect(result.error).toBeDefined();
50
+ expect(result.error?.code).toBe(-32008); // RequestTimeout
51
+ }, 10000);
52
+
53
+ it('should expose tools.$raw for calling tools', async () => {
54
+ gatewayService.callTool.mockResolvedValue({
55
+ result: { message: 'Tool called!' }
56
+ });
57
+
58
+ const code = `
59
+ const result = await tools.$raw('test__hello', { arg: 1 });
60
+ console.log(JSON.stringify(result));
61
+ `;
62
+ const context = new ExecutionContext({ logger });
63
+ const limits = { timeoutMs: 5000, memoryLimitMb: 128, maxOutputBytes: 1024, maxLogEntries: 100 };
64
+
65
+ const result = await executor.execute(code, limits, context);
66
+
67
+ expect(result.exitCode).toBe(0);
68
+ expect(gatewayService.callTool).toHaveBeenCalledWith('test__hello', { arg: 1 }, context);
69
+ });
70
+
71
+ it('should use injected SDK script for typed access', async () => {
72
+ gatewayService.callTool.mockResolvedValue({
73
+ result: { content: 'typed result' }
74
+ });
75
+
76
+ const sdkScript = `
77
+ const tools = {
78
+ mock: {
79
+ async hello(args) {
80
+ const res = await __callTool('mock__hello', JSON.stringify(args));
81
+ return JSON.parse(res);
82
+ }
83
+ }
84
+ };
85
+ `;
86
+
87
+ const code = `
88
+ await tools.mock.hello({ name: 'Typed' });
89
+ console.log('Typed call done');
90
+ `;
91
+ const context = new ExecutionContext({ logger });
92
+ const limits = { timeoutMs: 5000, memoryLimitMb: 128, maxOutputBytes: 1024, maxLogEntries: 100 };
93
+
94
+ const result = await executor.execute(code, limits, context, { sdkCode: sdkScript });
95
+
96
+ expect(result.exitCode).toBe(0);
97
+ expect(gatewayService.callTool).toHaveBeenCalledWith('mock__hello', { name: 'Typed' }, context);
98
+ expect(result.stdout).toContain('Typed call done');
99
+ });
100
+ });
@@ -0,0 +1,55 @@
1
+ import { describe, it, expect, beforeEach, beforeAll, afterAll } from 'vitest';
2
+ import { DenoExecutor } from '../src/executors/deno.executor.js';
3
+ import { PyodideExecutor } from '../src/executors/pyodide.executor.js';
4
+ import { ExecutionContext } from '../src/core/execution.context.js';
5
+ import { ConduitError } from '../src/core/request.controller.js';
6
+ import pino from 'pino';
7
+
8
+ const logger = pino({ level: 'silent' });
9
+
10
+ describe('LogLimitExceeded Verification', () => {
11
+ let context: ExecutionContext;
12
+
13
+ beforeEach(() => {
14
+ context = new ExecutionContext({ logger });
15
+ });
16
+
17
+ describe('DenoExecutor', () => {
18
+ it('should return LogLimitExceeded when log entry limit breached', async () => {
19
+ const executor = new DenoExecutor();
20
+ const result = await executor.execute('for(let i=0; i<100; i++) console.log(i)', {
21
+ timeoutMs: 5000,
22
+ memoryLimitMb: 128,
23
+ maxOutputBytes: 1024 * 1024,
24
+ maxLogEntries: 10,
25
+ }, context);
26
+
27
+ expect(result.error?.code).toBe(ConduitError.LogLimitExceeded);
28
+ expect(result.error?.message).toContain('Log entry limit exceeded');
29
+ });
30
+ });
31
+
32
+ describe('PyodideExecutor', () => {
33
+ let pyExecutor: PyodideExecutor;
34
+
35
+ beforeAll(() => {
36
+ pyExecutor = new PyodideExecutor();
37
+ });
38
+
39
+ afterAll(async () => {
40
+ await pyExecutor.shutdown();
41
+ });
42
+
43
+ it('should return LogLimitExceeded when log entry limit breached', async () => {
44
+ const result = await pyExecutor.execute('for i in range(100): print(i)', {
45
+ timeoutMs: 10000,
46
+ memoryLimitMb: 128,
47
+ maxOutputBytes: 1024 * 1024,
48
+ maxLogEntries: 10,
49
+ }, context);
50
+
51
+ expect(result.error?.code).toBe(ConduitError.LogLimitExceeded);
52
+ expect(result.error?.message).toContain('Log entry limit exceeded');
53
+ }, 20000);
54
+ });
55
+ });