@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,297 @@
1
+ import { ExecutionContext } from './execution.context.js';
2
+ import { Logger } from 'pino';
3
+
4
+ import { GatewayService } from '../gateway/gateway.service.js';
5
+ import { metrics } from './metrics.service.js';
6
+ import { ExecutionService } from './execution.service.js';
7
+
8
+ import { Middleware } from './interfaces/middleware.interface.js';
9
+
10
+ import { ConduitError, JSONRPCRequest, JSONRPCResponse } from './types.js';
11
+
12
+ export { ConduitError, JSONRPCRequest, JSONRPCResponse };
13
+
14
+ export class RequestController {
15
+ private logger: Logger;
16
+ private executionService: ExecutionService;
17
+ private gatewayService: GatewayService;
18
+
19
+ private middlewares: Middleware[] = [];
20
+
21
+ constructor(
22
+ logger: Logger,
23
+ executionService: ExecutionService,
24
+ gatewayService: GatewayService,
25
+ middlewares: Middleware[] = []
26
+ ) {
27
+ this.logger = logger;
28
+ this.executionService = executionService;
29
+ this.gatewayService = gatewayService;
30
+ this.middlewares = middlewares;
31
+ }
32
+
33
+ use(middleware: Middleware) {
34
+ this.middlewares.push(middleware);
35
+ }
36
+
37
+
38
+
39
+ async handleRequest(request: JSONRPCRequest, context: ExecutionContext): Promise<JSONRPCResponse> {
40
+ return this.executePipeline(request, context);
41
+ }
42
+
43
+ private async executePipeline(request: JSONRPCRequest, context: ExecutionContext): Promise<JSONRPCResponse> {
44
+ let index = -1;
45
+
46
+ const dispatch = async (i: number): Promise<JSONRPCResponse> => {
47
+ if (i <= index) throw new Error('next() called multiple times');
48
+ index = i;
49
+
50
+ const middleware = this.middlewares[i];
51
+ if (middleware) {
52
+ return middleware.handle(request, context, () => dispatch(i + 1));
53
+ }
54
+
55
+ return this.finalHandler(request, context);
56
+ };
57
+
58
+ return dispatch(0);
59
+ }
60
+
61
+ private async handleValidateTool(request: JSONRPCRequest, context: ExecutionContext): Promise<JSONRPCResponse> {
62
+ const params = request.params as { toolName: string; args: any };
63
+ if (!params || !params.toolName || !params.args) {
64
+ return {
65
+ jsonrpc: '2.0',
66
+ id: request.id,
67
+ error: {
68
+ code: -32602,
69
+ message: 'Missing toolName or args params',
70
+ },
71
+ };
72
+ }
73
+
74
+ try {
75
+ const result = await this.gatewayService.validateTool(params.toolName, params.args, context);
76
+ return {
77
+ jsonrpc: '2.0',
78
+ id: request.id,
79
+ result,
80
+ };
81
+ } catch (error: any) {
82
+ return {
83
+ jsonrpc: '2.0',
84
+ id: request.id,
85
+ error: {
86
+ code: -32603,
87
+ message: error.message || 'Validation failed',
88
+ },
89
+ };
90
+ }
91
+ }
92
+
93
+ private async finalHandler(request: JSONRPCRequest, context: ExecutionContext): Promise<JSONRPCResponse> {
94
+ const { method, params, id } = request;
95
+ // Logging and metrics handled by middlewares now
96
+
97
+ // Try/catch handled by ErrorMiddleware, but we handle logic errors here if needed
98
+ // Actually routing logic should just throw and let middleware catch?
99
+ // Or specific logic.
100
+
101
+ switch (method) {
102
+ case 'mcp.discoverTools':
103
+ return this.handleDiscoverTools(params, context, id);
104
+ case 'mcp.listToolPackages':
105
+ return this.handleListToolPackages(params, context, id);
106
+ case 'mcp.listToolStubs':
107
+ return this.handleListToolStubs(params, context, id);
108
+ case 'mcp.readToolSchema':
109
+ return this.handleReadToolSchema(params, context, id);
110
+ case 'mcp.validateTool':
111
+ return this.handleValidateTool(request, context);
112
+ case 'mcp.callTool':
113
+ return this.handleCallTool(params, context, id);
114
+ case 'mcp.executeTypeScript':
115
+ return this.handleExecuteTypeScript(params, context, id);
116
+ case 'mcp.executePython':
117
+ return this.handleExecutePython(params, context, id);
118
+ case 'mcp.executeIsolate':
119
+ return this.handleExecuteIsolate(params, context, id);
120
+ default:
121
+ // metrics.recordExecutionEnd is handled by LoggingMiddleware??
122
+ // Wait, if 404, LoggingMiddleware records execution end?
123
+ // Yes, handle() in LoggingMiddleware wraps next().
124
+ return this.errorResponse(id, -32601, `Method not found: ${method}`);
125
+ }
126
+ }
127
+
128
+ private async handleDiscoverTools(params: any, context: ExecutionContext, id: string | number): Promise<JSONRPCResponse> {
129
+ const tools = await this.gatewayService.discoverTools(context);
130
+ return {
131
+ jsonrpc: '2.0',
132
+ id,
133
+ result: {
134
+ tools,
135
+ },
136
+ };
137
+ }
138
+
139
+ private async handleListToolPackages(params: any, context: ExecutionContext, id: string | number): Promise<JSONRPCResponse> {
140
+ const packages = await this.gatewayService.listToolPackages();
141
+ return {
142
+ jsonrpc: '2.0',
143
+ id,
144
+ result: {
145
+ packages
146
+ }
147
+ };
148
+ }
149
+
150
+ private async handleListToolStubs(params: any, context: ExecutionContext, id: string | number): Promise<JSONRPCResponse> {
151
+ const { packageId } = params;
152
+ if (!packageId) {
153
+ return this.errorResponse(id, -32602, 'Missing packageId parameter');
154
+ }
155
+
156
+ try {
157
+ const stubs = await this.gatewayService.listToolStubs(packageId, context);
158
+ return {
159
+ jsonrpc: '2.0',
160
+ id,
161
+ result: {
162
+ stubs
163
+ }
164
+ };
165
+ } catch (error: any) {
166
+ return this.errorResponse(id, -32001, error.message);
167
+ }
168
+ }
169
+
170
+ private async handleReadToolSchema(params: any, context: ExecutionContext, id: string | number): Promise<JSONRPCResponse> {
171
+ const { toolId } = params;
172
+ if (!toolId) {
173
+ return this.errorResponse(id, -32602, 'Missing toolId parameter');
174
+ }
175
+
176
+ try {
177
+ const schema = await this.gatewayService.getToolSchema(toolId, context);
178
+ if (!schema) {
179
+ return this.errorResponse(id, -32001, `Tool not found: ${toolId}`);
180
+ }
181
+ return {
182
+ jsonrpc: '2.0',
183
+ id,
184
+ result: {
185
+ schema
186
+ }
187
+ };
188
+ } catch (error: any) {
189
+ return this.errorResponse(id, -32003, error.message);
190
+ }
191
+ }
192
+
193
+ private async handleCallTool(params: any, context: ExecutionContext, id: string | number): Promise<JSONRPCResponse> {
194
+ const { name, arguments: toolArgs } = params;
195
+ const response = await this.gatewayService.callTool(name, toolArgs, context);
196
+ return { ...response, id };
197
+ }
198
+
199
+ private async handleExecuteTypeScript(params: any, context: ExecutionContext, id: string | number): Promise<JSONRPCResponse> {
200
+ const { code, limits, allowedTools } = params;
201
+
202
+ if (Array.isArray(allowedTools)) {
203
+ context.allowedTools = allowedTools;
204
+ }
205
+
206
+ const result = await this.executionService.executeTypeScript(code, limits, context, allowedTools);
207
+
208
+ if (result.error) {
209
+ return this.errorResponse(id, result.error.code, result.error.message);
210
+ }
211
+
212
+ return {
213
+ jsonrpc: '2.0',
214
+ id,
215
+ result: {
216
+ stdout: result.stdout,
217
+ stderr: result.stderr,
218
+ exitCode: result.exitCode,
219
+ },
220
+ };
221
+ }
222
+
223
+ private async handleExecutePython(params: any, context: ExecutionContext, id: string | number): Promise<JSONRPCResponse> {
224
+ const { code, limits, allowedTools } = params;
225
+
226
+ if (Array.isArray(allowedTools)) {
227
+ context.allowedTools = allowedTools;
228
+ }
229
+
230
+ const result = await this.executionService.executePython(code, limits, context, allowedTools);
231
+
232
+ if (result.error) {
233
+ return this.errorResponse(id, result.error.code, result.error.message);
234
+ }
235
+
236
+ return {
237
+ jsonrpc: '2.0',
238
+ id,
239
+ result: {
240
+ stdout: result.stdout,
241
+ stderr: result.stderr,
242
+ exitCode: result.exitCode,
243
+ },
244
+ };
245
+ }
246
+
247
+ private async handleExecuteIsolate(params: any, context: ExecutionContext, id: string | number): Promise<JSONRPCResponse> {
248
+ const { code, limits, allowedTools } = params;
249
+
250
+ if (Array.isArray(allowedTools)) {
251
+ context.allowedTools = allowedTools;
252
+ }
253
+
254
+ const result = await this.executionService.executeIsolate(code, limits, context, allowedTools);
255
+
256
+ if (result.error) {
257
+ return this.errorResponse(id, result.error.code, result.error.message);
258
+ }
259
+
260
+ return {
261
+ jsonrpc: '2.0',
262
+ id,
263
+ result: {
264
+ stdout: result.stdout,
265
+ stderr: result.stderr,
266
+ exitCode: result.exitCode,
267
+ },
268
+ };
269
+ }
270
+
271
+ private errorResponse(id: string | number, code: number, message: string): JSONRPCResponse {
272
+ return {
273
+ jsonrpc: '2.0',
274
+ id,
275
+ error: {
276
+ code,
277
+ message,
278
+ },
279
+ };
280
+ }
281
+
282
+ async shutdown() {
283
+ await this.executionService.shutdown();
284
+ }
285
+
286
+ async healthCheck() {
287
+ const pyodideHealth = await this.executionService.healthCheck();
288
+ return {
289
+ status: pyodideHealth.status === 'ok' ? 'ok' : 'error',
290
+ pyodide: pyodideHealth
291
+ };
292
+ }
293
+
294
+ async warmup() {
295
+ await this.executionService.warmup();
296
+ }
297
+ }
@@ -0,0 +1,68 @@
1
+ import { Logger } from 'pino';
2
+ import { NetworkPolicyService } from './network.policy.service.js';
3
+ import { SessionManager, Session } from './session.manager.js';
4
+ import { IUrlValidator } from './interfaces/url.validator.interface.js';
5
+ import crypto from 'node:crypto';
6
+
7
+ export { Session };
8
+
9
+ export class SecurityService implements IUrlValidator {
10
+ private logger: Logger;
11
+ private ipcToken: string;
12
+ private networkPolicy: NetworkPolicyService;
13
+ private sessionManager: SessionManager;
14
+
15
+ constructor(logger: Logger, ipcToken: string) {
16
+ this.logger = logger;
17
+ this.ipcToken = ipcToken;
18
+ this.networkPolicy = new NetworkPolicyService(logger);
19
+ this.sessionManager = new SessionManager(logger);
20
+ }
21
+
22
+ validateCode(code: string): { valid: boolean; message?: string } {
23
+ // [IMPORTANT] This is a SANITY CHECK only.
24
+ // We rely on RUNTIME isolation (Deno permissions, Isolate context) for actual security.
25
+ // Static analysis of code is fundamentally unable to prevent all sandbox escapes.
26
+ if (!code || code.length > 1024 * 1024) { // 1MB limit for sanity
27
+ return { valid: false, message: 'Code size exceeds limit or is empty' };
28
+ }
29
+ return { valid: true };
30
+ }
31
+
32
+ async validateUrl(url: string): Promise<{ valid: boolean; message?: string; resolvedIp?: string }> {
33
+ return this.networkPolicy.validateUrl(url);
34
+ }
35
+
36
+ checkRateLimit(key: string): boolean {
37
+ return this.networkPolicy.checkRateLimit(key);
38
+ }
39
+
40
+ validateIpcToken(token: string): boolean {
41
+ // Fix Sev1: Use timing-safe comparison for sensitive tokens
42
+ const expected = Buffer.from(this.ipcToken);
43
+ const actual = Buffer.from(token);
44
+
45
+ if (expected.length === actual.length && crypto.timingSafeEqual(expected, actual)) {
46
+ return true;
47
+ }
48
+
49
+ return !!this.sessionManager.getSession(token);
50
+ }
51
+
52
+ createSession(allowedTools?: string[]): string {
53
+ return this.sessionManager.createSession(allowedTools);
54
+ }
55
+
56
+ getSession(token: string): Session | undefined {
57
+ return this.sessionManager.getSession(token);
58
+ }
59
+
60
+ invalidateSession(token: string): void {
61
+ this.sessionManager.invalidateSession(token);
62
+ }
63
+
64
+
65
+ getIpcToken(): string {
66
+ return this.ipcToken;
67
+ }
68
+ }
@@ -0,0 +1,44 @@
1
+ import { Logger } from 'pino';
2
+ import { v4 as uuidv4 } from 'uuid';
3
+ import { LRUCache } from 'lru-cache';
4
+
5
+ export interface Session {
6
+ allowedTools?: string[];
7
+ createdAt: number;
8
+ }
9
+
10
+ export class SessionManager {
11
+ private logger: Logger;
12
+ private sessions: LRUCache<string, Session>;
13
+ private readonly SESSION_TTL_MS = 3600000; // 1 hour
14
+
15
+ constructor(logger: Logger) {
16
+ this.logger = logger;
17
+ this.sessions = new LRUCache({
18
+ max: 10000,
19
+ ttl: this.SESSION_TTL_MS,
20
+ });
21
+ }
22
+
23
+ createSession(allowedTools?: string[]): string {
24
+ const token = uuidv4();
25
+ this.sessions.set(token, {
26
+ allowedTools,
27
+ createdAt: Date.now()
28
+ });
29
+ return token;
30
+ }
31
+
32
+ getSession(token: string): Session | undefined {
33
+ return this.sessions.get(token);
34
+ }
35
+
36
+ invalidateSession(token: string): void {
37
+ this.sessions.delete(token);
38
+ }
39
+
40
+ cleanupSessions() {
41
+ // LRUCache handles this automatically via TTL
42
+ this.sessions.purgeStale();
43
+ }
44
+ }
@@ -0,0 +1,47 @@
1
+ export enum ConduitError {
2
+ InternalError = -32603,
3
+ RequestTimeout = -32008,
4
+ Forbidden = -32003,
5
+ OutputLimitExceeded = -32013,
6
+ MemoryLimitExceeded = -32009,
7
+ LogLimitExceeded = -32014,
8
+ ServerBusy = -32000,
9
+ }
10
+
11
+ export interface JSONRPCRequest {
12
+ jsonrpc: '2.0';
13
+ id: string | number;
14
+ method: string;
15
+ params?: any;
16
+ auth?: {
17
+ bearerToken: string;
18
+ };
19
+ }
20
+
21
+ export interface JSONRPCResponse {
22
+ jsonrpc: '2.0';
23
+ id: string | number;
24
+ result?: any;
25
+ error?: {
26
+ code: number;
27
+ message: string;
28
+ data?: any;
29
+ };
30
+ }
31
+
32
+ export interface ToolPackage {
33
+ id: string; // e.g., "github"
34
+ description?: string;
35
+ version?: string;
36
+ }
37
+
38
+ export interface ToolStub {
39
+ id: string; // e.g., "github__create_issue"
40
+ name: string; // e.g., "create_issue"
41
+ description?: string;
42
+ }
43
+
44
+ export interface ToolManifest {
45
+ version: string;
46
+ tools: ToolStub[];
47
+ }