@mhingston5/conduit 1.1.2 → 1.1.4

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/dist/index.js ADDED
@@ -0,0 +1,3265 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { Command } from "commander";
5
+
6
+ // src/core/config.service.ts
7
+ import { z } from "zod";
8
+ import dotenv from "dotenv";
9
+ import fs from "fs";
10
+ import path from "path";
11
+ import crypto from "crypto";
12
+ import yaml from "js-yaml";
13
+ var originalWrite = process.stdout.write;
14
+ process.stdout.write = () => true;
15
+ dotenv.config();
16
+ process.stdout.write = originalWrite;
17
+ var ResourceLimitsSchema = z.object({
18
+ timeoutMs: z.number().default(3e4),
19
+ memoryLimitMb: z.number().default(256),
20
+ maxOutputBytes: z.number().default(1024 * 1024),
21
+ // 1MB
22
+ maxLogEntries: z.number().default(1e4)
23
+ });
24
+ var UpstreamCredentialsSchema = z.object({
25
+ type: z.enum(["oauth2", "apiKey", "bearer"]),
26
+ // Align with AuthType
27
+ clientId: z.string().optional(),
28
+ clientSecret: z.string().optional(),
29
+ tokenUrl: z.string().optional(),
30
+ refreshToken: z.string().optional(),
31
+ scopes: z.array(z.string()).optional(),
32
+ apiKey: z.string().optional(),
33
+ bearerToken: z.string().optional(),
34
+ headerName: z.string().optional()
35
+ });
36
+ var HttpUpstreamSchema = z.object({
37
+ id: z.string(),
38
+ type: z.literal("http").optional().default("http"),
39
+ url: z.string(),
40
+ credentials: UpstreamCredentialsSchema.optional()
41
+ });
42
+ var StdioUpstreamSchema = z.object({
43
+ id: z.string(),
44
+ type: z.literal("stdio"),
45
+ command: z.string(),
46
+ args: z.array(z.string()).optional(),
47
+ env: z.record(z.string(), z.string()).optional()
48
+ });
49
+ var UpstreamInfoSchema = z.union([HttpUpstreamSchema, StdioUpstreamSchema]);
50
+ var ConfigSchema = z.object({
51
+ port: z.union([z.string(), z.number()]).default("3000").transform((v) => Number(v)),
52
+ nodeEnv: z.enum(["development", "production", "test"]).default("development"),
53
+ logLevel: z.enum(["debug", "info", "warn", "error"]).default("info"),
54
+ resourceLimits: ResourceLimitsSchema.default({
55
+ timeoutMs: 3e4,
56
+ memoryLimitMb: 256,
57
+ maxOutputBytes: 1024 * 1024,
58
+ maxLogEntries: 1e4
59
+ }),
60
+ secretRedactionPatterns: z.array(z.string()).default([
61
+ "[A-Za-z0-9-_]{20,}"
62
+ // Default pattern from spec
63
+ ]),
64
+ ipcBearerToken: z.string().optional().default(() => crypto.randomUUID()),
65
+ maxConcurrent: z.number().default(10),
66
+ denoMaxPoolSize: z.number().default(10),
67
+ pyodideMaxPoolSize: z.number().default(3),
68
+ metricsUrl: z.string().default("http://127.0.0.1:9464/metrics"),
69
+ opsPort: z.number().optional(),
70
+ transport: z.enum(["socket", "stdio"]).default("socket"),
71
+ upstreams: z.array(UpstreamInfoSchema).default([])
72
+ });
73
+ var ConfigService = class {
74
+ config;
75
+ constructor(overrides = {}) {
76
+ const fileConfig = this.loadConfigFile();
77
+ const envConfig = {
78
+ port: process.env.PORT,
79
+ nodeEnv: process.env.NODE_ENV,
80
+ logLevel: process.env.LOG_LEVEL,
81
+ metricsUrl: process.env.METRICS_URL,
82
+ ipcBearerToken: process.env.IPC_BEARER_TOKEN,
83
+ transport: process.argv.includes("--stdio") ? "stdio" : void 0
84
+ // upstreams: process.env.UPSTREAMS ? JSON.parse(process.env.UPSTREAMS) : undefined, // Removed per user request
85
+ };
86
+ Object.keys(envConfig).forEach((key) => envConfig[key] === void 0 && delete envConfig[key]);
87
+ const mergedConfig = {
88
+ ...fileConfig,
89
+ ...envConfig,
90
+ ...overrides
91
+ };
92
+ const result = ConfigSchema.safeParse(mergedConfig);
93
+ if (!result.success) {
94
+ const error = result.error.format();
95
+ throw new Error(`Invalid configuration: ${JSON.stringify(error, null, 2)}`);
96
+ }
97
+ this.config = result.data;
98
+ if (this.config.opsPort === void 0) {
99
+ if (this.config.transport === "stdio") {
100
+ this.config.opsPort = 0;
101
+ } else {
102
+ this.config.opsPort = this.config.port === 0 ? 0 : this.config.port + 1;
103
+ }
104
+ }
105
+ }
106
+ get(key) {
107
+ return this.config[key];
108
+ }
109
+ get all() {
110
+ return { ...this.config };
111
+ }
112
+ loadConfigFile() {
113
+ const configPath = process.env.CONFIG_FILE || (fs.existsSync(path.resolve(process.cwd(), "conduit.yaml")) ? "conduit.yaml" : fs.existsSync(path.resolve(process.cwd(), "conduit.json")) ? "conduit.json" : null);
114
+ if (!configPath) return {};
115
+ try {
116
+ const fullPath = path.resolve(process.cwd(), configPath);
117
+ let fileContent = fs.readFileSync(fullPath, "utf-8");
118
+ fileContent = fileContent.replace(/\$\{([a-zA-Z0-9_]+)(?::-([^}]+))?\}/g, (match, varName, defaultValue) => {
119
+ const value = process.env[varName];
120
+ if (value !== void 0) {
121
+ return value;
122
+ }
123
+ return defaultValue !== void 0 ? defaultValue : "";
124
+ });
125
+ if (configPath.endsWith(".yaml") || configPath.endsWith(".yml")) {
126
+ return yaml.load(fileContent);
127
+ } else {
128
+ return JSON.parse(fileContent);
129
+ }
130
+ } catch (error) {
131
+ console.warn(`Failed to load config file ${configPath}:`, error);
132
+ return {};
133
+ }
134
+ }
135
+ };
136
+
137
+ // src/core/logger.ts
138
+ import pino from "pino";
139
+ import { AsyncLocalStorage } from "async_hooks";
140
+ var loggerStorage = new AsyncLocalStorage();
141
+ function createLogger(configService) {
142
+ const logLevel = configService.get("logLevel");
143
+ const redactionPatterns = configService.get("secretRedactionPatterns");
144
+ const secretPatterns = redactionPatterns.map((p) => new RegExp(p, "g"));
145
+ const redactString = (str) => {
146
+ let result = str;
147
+ for (const pattern of secretPatterns) {
148
+ result = result.replace(pattern, "[REDACTED]");
149
+ }
150
+ return result;
151
+ };
152
+ return pino(
153
+ {
154
+ level: logLevel,
155
+ hooks: {
156
+ logMethod(inputArgs, method) {
157
+ const redactedArgs = inputArgs.map((arg) => {
158
+ try {
159
+ if (typeof arg === "string") {
160
+ return redactString(arg);
161
+ }
162
+ if (typeof arg === "object" && arg !== null) {
163
+ const clone = { ...arg };
164
+ for (const key in clone) {
165
+ if (typeof clone[key] === "string") {
166
+ clone[key] = redactString(clone[key]);
167
+ }
168
+ }
169
+ return clone;
170
+ }
171
+ } catch (err) {
172
+ return "[REDACTION_ERROR]";
173
+ }
174
+ return arg;
175
+ });
176
+ return method.apply(this, redactedArgs);
177
+ }
178
+ },
179
+ redact: {
180
+ paths: ["toolParams.*", "headers.Authorization", "headers.authorization", "params.token"],
181
+ censor: "[REDACTED]"
182
+ },
183
+ mixin() {
184
+ const store = loggerStorage.getStore();
185
+ return {
186
+ correlationId: store?.correlationId
187
+ };
188
+ },
189
+ // In stdio mode, never use pino-pretty to avoid stdout pollution
190
+ transport: configService.get("transport") !== "stdio" && configService.get("nodeEnv") === "development" ? { target: "pino-pretty", options: { colorize: true } } : void 0
191
+ },
192
+ configService.get("transport") === "stdio" ? pino.destination(2) : void 0
193
+ );
194
+ }
195
+
196
+ // src/transport/socket.transport.ts
197
+ import net from "net";
198
+ import fs2 from "fs";
199
+ import os from "os";
200
+ import path2 from "path";
201
+
202
+ // src/core/execution.context.ts
203
+ import { v4 as uuidv4 } from "uuid";
204
+ var ExecutionContext = class {
205
+ correlationId;
206
+ startTime;
207
+ tenantId;
208
+ logger;
209
+ allowedTools;
210
+ remoteAddress;
211
+ strictValidation;
212
+ constructor(options) {
213
+ this.correlationId = uuidv4();
214
+ this.startTime = Date.now();
215
+ this.tenantId = options.tenantId;
216
+ this.allowedTools = options.allowedTools;
217
+ this.remoteAddress = options.remoteAddress;
218
+ this.strictValidation = options.strictValidation ?? false;
219
+ this.logger = options.logger.child({
220
+ correlationId: this.correlationId,
221
+ tenantId: this.tenantId
222
+ });
223
+ }
224
+ getDuration() {
225
+ return Date.now() - this.startTime;
226
+ }
227
+ };
228
+
229
+ // src/transport/socket.transport.ts
230
+ var SocketTransport = class {
231
+ server;
232
+ logger;
233
+ requestController;
234
+ concurrencyService;
235
+ constructor(logger, requestController, concurrencyService) {
236
+ this.logger = logger;
237
+ this.requestController = requestController;
238
+ this.concurrencyService = concurrencyService;
239
+ this.server = net.createServer((socket) => {
240
+ this.handleConnection(socket);
241
+ });
242
+ this.server.on("error", (err) => {
243
+ this.logger.error({ err }, "Server error");
244
+ });
245
+ }
246
+ async listen(options) {
247
+ return new Promise((resolve, reject) => {
248
+ if (options.path) {
249
+ const socketPath = this.formatSocketPath(options.path);
250
+ this.logger.info({ socketPath }, "Binding to IPC socket");
251
+ if (os.platform() !== "win32" && path2.isAbsolute(socketPath)) {
252
+ try {
253
+ fs2.unlinkSync(socketPath);
254
+ } catch (error) {
255
+ if (error.code !== "ENOENT") {
256
+ this.logger.warn({ err: error, socketPath }, "Failed to unlink socket before binding");
257
+ }
258
+ }
259
+ }
260
+ this.server.listen(socketPath, () => {
261
+ this.resolveAddress(resolve);
262
+ });
263
+ } else if (options.port !== void 0) {
264
+ this.logger.info({ port: options.port, host: options.host }, "Binding to TCP port");
265
+ this.server.listen(options.port, options.host || "127.0.0.1", () => {
266
+ this.resolveAddress(resolve);
267
+ });
268
+ } else {
269
+ reject(new Error("Invalid transport configuration: neither path nor port provided"));
270
+ return;
271
+ }
272
+ this.server.on("error", reject);
273
+ });
274
+ }
275
+ resolveAddress(resolve) {
276
+ const address = this.server.address();
277
+ const addressStr = typeof address === "string" ? address : `${address?.address}:${address?.port}`;
278
+ this.logger.info({ address: addressStr }, "Transport server listening");
279
+ resolve(addressStr);
280
+ }
281
+ formatSocketPath(inputPath) {
282
+ if (os.platform() === "win32") {
283
+ if (!inputPath.startsWith("\\\\.\\pipe\\")) {
284
+ return `\\\\.\\pipe\\${inputPath}`;
285
+ }
286
+ return inputPath;
287
+ } else {
288
+ return path2.isAbsolute(inputPath) ? inputPath : path2.join(os.tmpdir(), inputPath);
289
+ }
290
+ }
291
+ handleConnection(socket) {
292
+ const remoteAddress = socket.remoteAddress || "pipe";
293
+ this.logger.debug({ remoteAddress }, "New connection established");
294
+ socket.setEncoding("utf8");
295
+ let buffer = "";
296
+ const MAX_BUFFER_SIZE = 10 * 1024 * 1024;
297
+ socket.on("data", async (chunk) => {
298
+ buffer += chunk;
299
+ if (buffer.length > MAX_BUFFER_SIZE) {
300
+ this.logger.error({ remoteAddress }, "Connection exceeded max buffer size, closing");
301
+ socket.destroy();
302
+ return;
303
+ }
304
+ socket.pause();
305
+ try {
306
+ let pos;
307
+ while ((pos = buffer.indexOf("\n")) >= 0) {
308
+ const line = buffer.substring(0, pos).trim();
309
+ buffer = buffer.substring(pos + 1);
310
+ if (!line) continue;
311
+ let request;
312
+ try {
313
+ request = JSON.parse(line);
314
+ } catch (err) {
315
+ this.logger.error({ err, line }, "Failed to parse JSON-RPC request");
316
+ const errorResponse = {
317
+ jsonrpc: "2.0",
318
+ id: null,
319
+ error: {
320
+ code: -32700,
321
+ message: "Parse error"
322
+ }
323
+ };
324
+ socket.write(JSON.stringify(errorResponse) + "\n");
325
+ continue;
326
+ }
327
+ const context = new ExecutionContext({
328
+ logger: this.logger,
329
+ remoteAddress
330
+ });
331
+ await loggerStorage.run({ correlationId: context.correlationId }, async () => {
332
+ try {
333
+ const response = await this.concurrencyService.run(
334
+ () => this.requestController.handleRequest(request, context)
335
+ );
336
+ socket.write(JSON.stringify(response) + "\n");
337
+ } catch (err) {
338
+ if (err.name === "QueueFullError") {
339
+ socket.write(JSON.stringify({
340
+ jsonrpc: "2.0",
341
+ id: request.id,
342
+ error: {
343
+ code: -32e3 /* ServerBusy */,
344
+ message: "Server busy"
345
+ }
346
+ }) + "\n");
347
+ } else {
348
+ this.logger.error({ err, requestId: request.id }, "Request handling failed");
349
+ socket.write(JSON.stringify({
350
+ jsonrpc: "2.0",
351
+ id: request.id,
352
+ error: {
353
+ code: -32603 /* InternalError */,
354
+ message: "Internal server error"
355
+ }
356
+ }) + "\n");
357
+ }
358
+ }
359
+ });
360
+ }
361
+ } catch (err) {
362
+ this.logger.error({ err }, "Unexpected error in socket data handler");
363
+ socket.destroy();
364
+ } finally {
365
+ socket.resume();
366
+ }
367
+ });
368
+ socket.on("close", () => {
369
+ this.logger.debug({ remoteAddress }, "Connection closed");
370
+ });
371
+ socket.on("error", (err) => {
372
+ this.logger.error({ err, remoteAddress }, "Socket error");
373
+ });
374
+ }
375
+ async close() {
376
+ return new Promise((resolve) => {
377
+ if (this.server.listening) {
378
+ this.server.close(() => {
379
+ this.logger.info("Transport server closed");
380
+ resolve();
381
+ });
382
+ } else {
383
+ resolve();
384
+ }
385
+ });
386
+ }
387
+ };
388
+
389
+ // src/transport/stdio.transport.ts
390
+ var StdioTransport = class {
391
+ logger;
392
+ requestController;
393
+ concurrencyService;
394
+ buffer = "";
395
+ constructor(logger, requestController, concurrencyService) {
396
+ this.logger = logger;
397
+ this.requestController = requestController;
398
+ this.concurrencyService = concurrencyService;
399
+ }
400
+ async start() {
401
+ this.logger.info("Starting Stdio transport");
402
+ process.stdin.setEncoding("utf8");
403
+ process.stdin.on("data", this.handleData.bind(this));
404
+ process.stdin.on("end", () => {
405
+ this.logger.info("Stdin closed");
406
+ });
407
+ }
408
+ handleData(chunk) {
409
+ this.buffer += chunk;
410
+ let pos;
411
+ while ((pos = this.buffer.indexOf("\n")) >= 0) {
412
+ const line = this.buffer.substring(0, pos).trim();
413
+ this.buffer = this.buffer.substring(pos + 1);
414
+ if (!line) continue;
415
+ this.processLine(line);
416
+ }
417
+ }
418
+ async processLine(line) {
419
+ let request;
420
+ try {
421
+ request = JSON.parse(line);
422
+ } catch (err) {
423
+ this.logger.error({ err, line }, "Failed to parse JSON-RPC request");
424
+ const errorResponse = {
425
+ jsonrpc: "2.0",
426
+ id: null,
427
+ error: {
428
+ code: -32700,
429
+ message: "Parse error"
430
+ }
431
+ };
432
+ this.sendResponse(errorResponse);
433
+ return;
434
+ }
435
+ const context = new ExecutionContext({
436
+ logger: this.logger,
437
+ remoteAddress: "stdio"
438
+ });
439
+ await loggerStorage.run({ correlationId: context.correlationId }, async () => {
440
+ try {
441
+ const response = await this.concurrencyService.run(
442
+ () => this.requestController.handleRequest(request, context)
443
+ );
444
+ if (response !== null) {
445
+ this.sendResponse(response);
446
+ }
447
+ } catch (err) {
448
+ if (err.name === "QueueFullError") {
449
+ this.sendResponse({
450
+ jsonrpc: "2.0",
451
+ id: request.id,
452
+ error: {
453
+ code: -32e3 /* ServerBusy */,
454
+ message: "Server busy"
455
+ }
456
+ });
457
+ } else {
458
+ this.logger.error({ err, requestId: request.id }, "Request handling failed");
459
+ this.sendResponse({
460
+ jsonrpc: "2.0",
461
+ id: request.id,
462
+ error: {
463
+ code: -32603 /* InternalError */,
464
+ message: "Internal server error"
465
+ }
466
+ });
467
+ }
468
+ }
469
+ });
470
+ }
471
+ sendResponse(response) {
472
+ process.stdout.write(JSON.stringify(response) + "\n");
473
+ }
474
+ async close() {
475
+ process.stdin.removeAllListeners();
476
+ return Promise.resolve();
477
+ }
478
+ };
479
+
480
+ // src/core/ops.server.ts
481
+ import Fastify from "fastify";
482
+ import axios from "axios";
483
+ var OpsServer = class {
484
+ fastify = Fastify();
485
+ logger;
486
+ config;
487
+ gatewayService;
488
+ requestController;
489
+ constructor(logger, config, gatewayService, requestController) {
490
+ this.logger = logger;
491
+ this.config = config;
492
+ this.gatewayService = gatewayService;
493
+ this.requestController = requestController;
494
+ this.setupRoutes();
495
+ }
496
+ setupRoutes() {
497
+ this.fastify.get("/health", async (request, reply) => {
498
+ const gatewayHealth = await this.gatewayService.healthCheck();
499
+ const requestHealth = await this.requestController.healthCheck();
500
+ const overallStatus = gatewayHealth.status === "ok" && requestHealth.status === "ok" ? "ok" : "error";
501
+ return reply.status(overallStatus === "ok" ? 200 : 503).send({
502
+ status: overallStatus,
503
+ version: "1.0.0",
504
+ gateway: gatewayHealth,
505
+ request: requestHealth
506
+ });
507
+ });
508
+ this.fastify.get("/metrics", async (request, reply) => {
509
+ try {
510
+ const metricsUrl = this.config.metricsUrl || "http://127.0.0.1:9464/metrics";
511
+ const response = await axios.get(metricsUrl);
512
+ return reply.type("text/plain").send(response.data);
513
+ } catch (err) {
514
+ this.logger.error({ err }, "Failed to fetch OTEL metrics");
515
+ const fallback = `# Metrics consolidated into OpenTelemetry. Check port 9464.
516
+ conduit_uptime_seconds ${process.uptime()}
517
+ conduit_memory_rss_bytes ${process.memoryUsage().rss}
518
+ `;
519
+ return reply.type("text/plain").send(fallback);
520
+ }
521
+ });
522
+ }
523
+ async listen() {
524
+ const port = this.config.opsPort !== void 0 ? this.config.opsPort : 3001;
525
+ try {
526
+ const address = await this.fastify.listen({ port, host: "0.0.0.0" });
527
+ this.logger.info({ address }, "Ops server listening");
528
+ return address;
529
+ } catch (err) {
530
+ this.logger.error({ err }, "Failed to start Ops server");
531
+ throw err;
532
+ }
533
+ }
534
+ async close() {
535
+ await this.fastify.close();
536
+ }
537
+ };
538
+
539
+ // src/core/concurrency.service.ts
540
+ import pLimit from "p-limit";
541
+ import { trace } from "@opentelemetry/api";
542
+
543
+ // src/core/metrics.service.ts
544
+ import { metrics as otelMetrics, ValueType } from "@opentelemetry/api";
545
+ var MetricsService = class _MetricsService {
546
+ static instance;
547
+ meter = otelMetrics.getMeter("conduit");
548
+ executionCounter;
549
+ cacheHitsCounter;
550
+ cacheMissesCounter;
551
+ executionLatency;
552
+ toolExecutionDuration;
553
+ requestQueueLength;
554
+ activeExecutionsGauge;
555
+ activeExecutionsCount = 0;
556
+ queueLengthCallback = () => 0;
557
+ constructor() {
558
+ this.executionCounter = this.meter.createCounter("conduit.executions.total", {
559
+ description: "Total number of executions"
560
+ });
561
+ this.cacheHitsCounter = this.meter.createCounter("conduit.cache.hits.total", {
562
+ description: "Total number of schema cache hits"
563
+ });
564
+ this.cacheMissesCounter = this.meter.createCounter("conduit.cache.misses.total", {
565
+ description: "Total number of schema cache misses"
566
+ });
567
+ this.executionLatency = this.meter.createHistogram("conduit.executions.latency", {
568
+ description: "Execution latency in milliseconds",
569
+ unit: "ms",
570
+ valueType: ValueType.DOUBLE
571
+ });
572
+ this.toolExecutionDuration = this.meter.createHistogram("conduit.tool.execution_duration_seconds", {
573
+ description: "Duration of tool executions",
574
+ unit: "s",
575
+ valueType: ValueType.DOUBLE
576
+ });
577
+ this.requestQueueLength = this.meter.createObservableGauge("conduit.request_queue_length", {
578
+ description: "Current request queue depth",
579
+ valueType: ValueType.INT
580
+ });
581
+ this.activeExecutionsGauge = this.meter.createObservableGauge("conduit.executions.active", {
582
+ description: "Current number of active executions"
583
+ });
584
+ this.activeExecutionsGauge.addCallback((result) => {
585
+ result.observe(this.activeExecutionsCount);
586
+ });
587
+ this.requestQueueLength.addCallback((result) => {
588
+ result.observe(this.queueLengthCallback());
589
+ });
590
+ }
591
+ static getInstance() {
592
+ if (!_MetricsService.instance) {
593
+ _MetricsService.instance = new _MetricsService();
594
+ }
595
+ return _MetricsService.instance;
596
+ }
597
+ recordExecutionStart() {
598
+ this.activeExecutionsCount++;
599
+ this.executionCounter.add(1);
600
+ }
601
+ recordExecutionEnd(durationMs, toolName) {
602
+ this.activeExecutionsCount = Math.max(0, this.activeExecutionsCount - 1);
603
+ this.executionLatency.record(durationMs, { tool: toolName || "unknown" });
604
+ }
605
+ recordToolExecution(durationMs, toolName, success) {
606
+ this.toolExecutionDuration.record(durationMs / 1e3, {
607
+ tool_name: toolName,
608
+ success: String(success)
609
+ });
610
+ }
611
+ recordCacheHit() {
612
+ this.cacheHitsCounter.add(1);
613
+ }
614
+ recordCacheMiss() {
615
+ this.cacheMissesCounter.add(1);
616
+ }
617
+ // This is now handled by OTEL Prometheus exporter,
618
+ // but we can provide a way to get the endpoint data if needed.
619
+ getMetrics() {
620
+ return {
621
+ activeExecutions: this.activeExecutionsCount,
622
+ uptime: process.uptime(),
623
+ memory: process.memoryUsage()
624
+ };
625
+ }
626
+ registerQueueLengthProvider(provider) {
627
+ this.queueLengthCallback = provider;
628
+ }
629
+ };
630
+ var metrics = MetricsService.getInstance();
631
+
632
+ // src/core/concurrency.service.ts
633
+ var QueueFullError = class extends Error {
634
+ constructor(message) {
635
+ super(message);
636
+ this.name = "QueueFullError";
637
+ }
638
+ };
639
+ var ConcurrencyService = class {
640
+ limit;
641
+ logger;
642
+ maxQueueSize;
643
+ queueDepthHistogram;
644
+ // Using explicit type locally if needed, or rely on metrics service later. Using direct OTEL for now involves refactor.
645
+ // Let's rely on internal state for rejection and let MetricsService handle reporting if possible, or add it here.
646
+ // simpler: usage of metrics service is better pattern.
647
+ constructor(logger, options) {
648
+ this.logger = logger;
649
+ this.limit = pLimit(options.maxConcurrent);
650
+ this.maxQueueSize = options.maxQueueSize || 100;
651
+ metrics.registerQueueLengthProvider(() => this.limit.pendingCount);
652
+ }
653
+ async run(fn) {
654
+ if (this.limit.pendingCount >= this.maxQueueSize) {
655
+ this.logger.warn({ pending: this.limit.pendingCount, max: this.maxQueueSize }, "Request queue full, rejecting request");
656
+ throw new QueueFullError("Server is too busy, please try again later");
657
+ }
658
+ const active = this.limit.activeCount;
659
+ const pending = this.limit.pendingCount;
660
+ this.logger.debug({ active, pending }, "Concurrency status before task");
661
+ const span = trace.getActiveSpan();
662
+ if (span) {
663
+ span.setAttributes({
664
+ "concurrency.active": active,
665
+ "concurrency.pending": pending
666
+ });
667
+ }
668
+ try {
669
+ return await this.limit(fn);
670
+ } finally {
671
+ this.logger.debug({
672
+ active: this.limit.activeCount,
673
+ pending: this.limit.pendingCount
674
+ }, "Concurrency status after task");
675
+ }
676
+ }
677
+ get stats() {
678
+ return {
679
+ activeCount: this.limit.activeCount,
680
+ pendingCount: this.limit.pendingCount
681
+ };
682
+ }
683
+ };
684
+
685
+ // src/core/request.controller.ts
686
+ var RequestController = class {
687
+ logger;
688
+ executionService;
689
+ gatewayService;
690
+ middlewares = [];
691
+ constructor(logger, executionService, gatewayService, middlewares = []) {
692
+ this.logger = logger;
693
+ this.executionService = executionService;
694
+ this.gatewayService = gatewayService;
695
+ this.middlewares = middlewares;
696
+ }
697
+ use(middleware) {
698
+ this.middlewares.push(middleware);
699
+ }
700
+ async handleRequest(request, context) {
701
+ return this.executePipeline(request, context);
702
+ }
703
+ async executePipeline(request, context) {
704
+ let index = -1;
705
+ const dispatch = async (i) => {
706
+ if (i <= index) throw new Error("next() called multiple times");
707
+ index = i;
708
+ const middleware = this.middlewares[i];
709
+ if (middleware) {
710
+ return middleware.handle(request, context, () => dispatch(i + 1));
711
+ }
712
+ return this.finalHandler(request, context);
713
+ };
714
+ return dispatch(0);
715
+ }
716
+ async handleValidateTool(request, context) {
717
+ const params = request.params;
718
+ if (!params || !params.toolName || !params.args) {
719
+ return {
720
+ jsonrpc: "2.0",
721
+ id: request.id,
722
+ error: {
723
+ code: -32602,
724
+ message: "Missing toolName or args params"
725
+ }
726
+ };
727
+ }
728
+ try {
729
+ const result = await this.gatewayService.validateTool(params.toolName, params.args, context);
730
+ return {
731
+ jsonrpc: "2.0",
732
+ id: request.id,
733
+ result
734
+ };
735
+ } catch (error) {
736
+ return {
737
+ jsonrpc: "2.0",
738
+ id: request.id,
739
+ error: {
740
+ code: -32603,
741
+ message: error.message || "Validation failed"
742
+ }
743
+ };
744
+ }
745
+ }
746
+ async finalHandler(request, context) {
747
+ const { method, params, id } = request;
748
+ switch (method) {
749
+ case "tools/list":
750
+ // Standard MCP method name
751
+ case "mcp_discover_tools":
752
+ return this.handleDiscoverTools(params, context, id);
753
+ case "mcp_list_tool_packages":
754
+ return this.handleListToolPackages(params, context, id);
755
+ case "mcp_list_tool_stubs":
756
+ return this.handleListToolStubs(params, context, id);
757
+ case "mcp_read_tool_schema":
758
+ return this.handleReadToolSchema(params, context, id);
759
+ case "mcp_validate_tool":
760
+ return this.handleValidateTool(request, context);
761
+ case "mcp_call_tool":
762
+ case "tools/call":
763
+ return this.handleCallTool(params, context, id);
764
+ case "mcp_execute_typescript":
765
+ return this.handleExecuteTypeScript(params, context, id);
766
+ case "mcp_execute_python":
767
+ return this.handleExecutePython(params, context, id);
768
+ case "mcp_execute_isolate":
769
+ return this.handleExecuteIsolate(params, context, id);
770
+ case "initialize":
771
+ return this.handleInitialize(params, context, id);
772
+ case "notifications/initialized":
773
+ return null;
774
+ // Notifications don't get responses per MCP spec
775
+ case "ping":
776
+ return { jsonrpc: "2.0", id, result: {} };
777
+ default:
778
+ return this.errorResponse(id, -32601, `Method not found: ${method}`);
779
+ }
780
+ }
781
+ async handleDiscoverTools(params, context, id) {
782
+ const tools = await this.gatewayService.discoverTools(context);
783
+ const standardizedTools = tools.map((t) => ({
784
+ name: t.name,
785
+ description: t.description,
786
+ inputSchema: t.inputSchema
787
+ }));
788
+ return {
789
+ jsonrpc: "2.0",
790
+ id,
791
+ result: {
792
+ tools: standardizedTools
793
+ }
794
+ };
795
+ }
796
+ async handleListToolPackages(params, context, id) {
797
+ const packages = await this.gatewayService.listToolPackages();
798
+ return {
799
+ jsonrpc: "2.0",
800
+ id,
801
+ result: {
802
+ packages
803
+ }
804
+ };
805
+ }
806
+ async handleListToolStubs(params, context, id) {
807
+ const { packageId } = params;
808
+ if (!packageId) {
809
+ return this.errorResponse(id, -32602, "Missing packageId parameter");
810
+ }
811
+ try {
812
+ const stubs = await this.gatewayService.listToolStubs(packageId, context);
813
+ return {
814
+ jsonrpc: "2.0",
815
+ id,
816
+ result: {
817
+ stubs
818
+ }
819
+ };
820
+ } catch (error) {
821
+ return this.errorResponse(id, -32001, error.message);
822
+ }
823
+ }
824
+ async handleReadToolSchema(params, context, id) {
825
+ const { toolId } = params;
826
+ if (!toolId) {
827
+ return this.errorResponse(id, -32602, "Missing toolId parameter");
828
+ }
829
+ try {
830
+ const schema = await this.gatewayService.getToolSchema(toolId, context);
831
+ if (!schema) {
832
+ return this.errorResponse(id, -32001, `Tool not found: ${toolId}`);
833
+ }
834
+ return {
835
+ jsonrpc: "2.0",
836
+ id,
837
+ result: {
838
+ schema
839
+ }
840
+ };
841
+ } catch (error) {
842
+ return this.errorResponse(id, -32003, error.message);
843
+ }
844
+ }
845
+ async handleCallTool(params, context, id) {
846
+ if (!params) return this.errorResponse(id, -32602, "Missing parameters");
847
+ const { name, arguments: toolArgs } = params;
848
+ switch (name) {
849
+ case "mcp_execute_typescript":
850
+ return this.handleExecuteTypeScript(toolArgs, context, id);
851
+ case "mcp_execute_python":
852
+ return this.handleExecutePython(toolArgs, context, id);
853
+ case "mcp_execute_isolate":
854
+ return this.handleExecuteIsolate(toolArgs, context, id);
855
+ }
856
+ const response = await this.gatewayService.callTool(name, toolArgs, context);
857
+ return { ...response, id };
858
+ }
859
+ async handleExecuteTypeScript(params, context, id) {
860
+ if (!params) return this.errorResponse(id, -32602, "Missing parameters");
861
+ const { code, limits, allowedTools } = params;
862
+ if (Array.isArray(allowedTools)) {
863
+ context.allowedTools = allowedTools;
864
+ }
865
+ const result = await this.executionService.executeTypeScript(code, limits, context, allowedTools);
866
+ if (result.error) {
867
+ return this.errorResponse(id, result.error.code, result.error.message);
868
+ }
869
+ return {
870
+ jsonrpc: "2.0",
871
+ id,
872
+ result: {
873
+ stdout: result.stdout,
874
+ stderr: result.stderr,
875
+ exitCode: result.exitCode
876
+ }
877
+ };
878
+ }
879
+ async handleExecutePython(params, context, id) {
880
+ if (!params) return this.errorResponse(id, -32602, "Missing parameters");
881
+ const { code, limits, allowedTools } = params;
882
+ if (Array.isArray(allowedTools)) {
883
+ context.allowedTools = allowedTools;
884
+ }
885
+ const result = await this.executionService.executePython(code, limits, context, allowedTools);
886
+ if (result.error) {
887
+ return this.errorResponse(id, result.error.code, result.error.message);
888
+ }
889
+ return {
890
+ jsonrpc: "2.0",
891
+ id,
892
+ result: {
893
+ stdout: result.stdout,
894
+ stderr: result.stderr,
895
+ exitCode: result.exitCode
896
+ }
897
+ };
898
+ }
899
+ async handleInitialize(params, context, id) {
900
+ const clientVersion = params?.protocolVersion || "2025-06-18";
901
+ return {
902
+ jsonrpc: "2.0",
903
+ id,
904
+ result: {
905
+ protocolVersion: clientVersion,
906
+ capabilities: {
907
+ tools: {
908
+ listChanged: true
909
+ },
910
+ resources: {
911
+ listChanged: true,
912
+ subscribe: true
913
+ }
914
+ },
915
+ serverInfo: {
916
+ name: "conduit",
917
+ version: process.env.npm_package_version || "1.1.0"
918
+ }
919
+ }
920
+ };
921
+ }
922
+ async handleExecuteIsolate(params, context, id) {
923
+ if (!params) return this.errorResponse(id, -32602, "Missing parameters");
924
+ const { code, limits, allowedTools } = params;
925
+ if (Array.isArray(allowedTools)) {
926
+ context.allowedTools = allowedTools;
927
+ }
928
+ const result = await this.executionService.executeIsolate(code, limits, context, allowedTools);
929
+ if (result.error) {
930
+ return this.errorResponse(id, result.error.code, result.error.message);
931
+ }
932
+ return {
933
+ jsonrpc: "2.0",
934
+ id,
935
+ result: {
936
+ stdout: result.stdout,
937
+ stderr: result.stderr,
938
+ exitCode: result.exitCode
939
+ }
940
+ };
941
+ }
942
+ errorResponse(id, code, message) {
943
+ return {
944
+ jsonrpc: "2.0",
945
+ id,
946
+ error: {
947
+ code,
948
+ message
949
+ }
950
+ };
951
+ }
952
+ async shutdown() {
953
+ await this.executionService.shutdown();
954
+ }
955
+ async healthCheck() {
956
+ const pyodideHealth = await this.executionService.healthCheck();
957
+ return {
958
+ status: pyodideHealth.status === "ok" ? "ok" : "error",
959
+ pyodide: pyodideHealth
960
+ };
961
+ }
962
+ async warmup() {
963
+ await this.executionService.warmup();
964
+ }
965
+ };
966
+
967
+ // src/gateway/upstream.client.ts
968
+ import axios2 from "axios";
969
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
970
+ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
971
+ import { z as z2 } from "zod";
972
+ var UpstreamClient = class {
973
+ logger;
974
+ info;
975
+ authService;
976
+ urlValidator;
977
+ mcpClient;
978
+ transport;
979
+ constructor(logger, info, authService, urlValidator) {
980
+ this.logger = logger.child({ upstreamId: info.id });
981
+ this.info = info;
982
+ this.authService = authService;
983
+ this.urlValidator = urlValidator;
984
+ if (this.info.type === "stdio") {
985
+ const env = { ...process.env, ...this.info.env };
986
+ const cleanEnv = Object.entries(env).reduce((acc, [k, v]) => {
987
+ if (v !== void 0) acc[k] = v;
988
+ return acc;
989
+ }, {});
990
+ this.transport = new StdioClientTransport({
991
+ command: this.info.command,
992
+ args: this.info.args,
993
+ env: cleanEnv
994
+ });
995
+ this.mcpClient = new Client({
996
+ name: "conduit-gateway",
997
+ version: "1.0.0"
998
+ }, {
999
+ capabilities: {}
1000
+ });
1001
+ }
1002
+ }
1003
+ async ensureConnected() {
1004
+ if (!this.mcpClient || !this.transport) return;
1005
+ try {
1006
+ if (!this.transport.connection) {
1007
+ await this.mcpClient.connect(this.transport);
1008
+ }
1009
+ } catch (e) {
1010
+ }
1011
+ }
1012
+ async call(request, context) {
1013
+ const isStdio = (info) => info.type === "stdio";
1014
+ if (isStdio(this.info)) {
1015
+ return this.callStdio(request);
1016
+ } else {
1017
+ return this.callHttp(request, context);
1018
+ }
1019
+ }
1020
+ async callStdio(request) {
1021
+ if (!this.mcpClient) {
1022
+ return { jsonrpc: "2.0", id: request.id, error: { code: -32603, message: "Stdio client not initialized" } };
1023
+ }
1024
+ try {
1025
+ await this.ensureConnected();
1026
+ if (request.method === "list_tools") {
1027
+ const result = await this.mcpClient.listTools();
1028
+ return {
1029
+ jsonrpc: "2.0",
1030
+ id: request.id,
1031
+ result
1032
+ };
1033
+ } else if (request.method === "call_tool") {
1034
+ const params = request.params;
1035
+ const result = await this.mcpClient.callTool({
1036
+ name: params.name,
1037
+ arguments: params.arguments
1038
+ });
1039
+ return {
1040
+ jsonrpc: "2.0",
1041
+ id: request.id,
1042
+ result
1043
+ };
1044
+ } else {
1045
+ const result = await this.mcpClient.request(
1046
+ { method: request.method, params: request.params },
1047
+ z2.any()
1048
+ );
1049
+ return {
1050
+ jsonrpc: "2.0",
1051
+ id: request.id,
1052
+ result
1053
+ };
1054
+ }
1055
+ } catch (error) {
1056
+ this.logger.error({ err: error }, "Stdio call failed");
1057
+ return {
1058
+ jsonrpc: "2.0",
1059
+ id: request.id,
1060
+ error: {
1061
+ code: error.code || -32603,
1062
+ message: error.message || "Internal error in stdio transport"
1063
+ }
1064
+ };
1065
+ }
1066
+ }
1067
+ async callHttp(request, context) {
1068
+ if (this.info.type === "stdio") throw new Error("Unreachable");
1069
+ const url = this.info.url;
1070
+ const headers = {
1071
+ "Content-Type": "application/json",
1072
+ "X-Correlation-Id": context.correlationId
1073
+ };
1074
+ if (context.tenantId) {
1075
+ headers["X-Tenant-Id"] = context.tenantId;
1076
+ }
1077
+ if (this.info.credentials) {
1078
+ const authHeaders = await this.authService.getAuthHeaders(this.info.credentials);
1079
+ Object.assign(headers, authHeaders);
1080
+ }
1081
+ const securityResult = await this.urlValidator.validateUrl(url);
1082
+ if (!securityResult.valid) {
1083
+ this.logger.error({ url }, "Blocked upstream URL (SSRF)");
1084
+ return {
1085
+ jsonrpc: "2.0",
1086
+ id: request.id,
1087
+ error: {
1088
+ code: -32003,
1089
+ message: securityResult.message || "Forbidden URL"
1090
+ }
1091
+ };
1092
+ }
1093
+ try {
1094
+ this.logger.debug({ method: request.method }, "Calling upstream MCP");
1095
+ const originalUrl = new URL(url);
1096
+ const requestUrl = securityResult.resolvedIp ? `${originalUrl.protocol}//${securityResult.resolvedIp}${originalUrl.port ? ":" + originalUrl.port : ""}${originalUrl.pathname}${originalUrl.search}${originalUrl.hash}` : url;
1097
+ headers["Host"] = originalUrl.hostname;
1098
+ const response = await axios2.post(requestUrl, request, {
1099
+ headers,
1100
+ timeout: 1e4,
1101
+ maxRedirects: 0
1102
+ });
1103
+ return response.data;
1104
+ } catch (err) {
1105
+ this.logger.error({ err: err.message }, "Upstream MCP call failed");
1106
+ return {
1107
+ jsonrpc: "2.0",
1108
+ id: request.id,
1109
+ error: {
1110
+ code: -32008,
1111
+ message: `Upstream error: ${err.message}`
1112
+ }
1113
+ };
1114
+ }
1115
+ }
1116
+ async getManifest(context) {
1117
+ if (this.info.type !== "http") return null;
1118
+ try {
1119
+ const baseUrl = this.info.url.replace(/\/$/, "");
1120
+ const manifestUrl = `${baseUrl}/conduit.manifest.json`;
1121
+ const headers = {
1122
+ "X-Correlation-Id": context.correlationId
1123
+ };
1124
+ if (this.info.credentials) {
1125
+ const authHeaders = await this.authService.getAuthHeaders(this.info.credentials);
1126
+ Object.assign(headers, authHeaders);
1127
+ }
1128
+ const securityResult = await this.urlValidator.validateUrl(manifestUrl);
1129
+ if (!securityResult.valid) {
1130
+ this.logger.warn({ url: manifestUrl }, "Blocked manifest URL (SSRF)");
1131
+ return null;
1132
+ }
1133
+ const originalUrl = new URL(manifestUrl);
1134
+ const requestUrl = securityResult.resolvedIp ? `${originalUrl.protocol}//${securityResult.resolvedIp}${originalUrl.port ? ":" + originalUrl.port : ""}${originalUrl.pathname}${originalUrl.search}${originalUrl.hash}` : manifestUrl;
1135
+ headers["Host"] = originalUrl.hostname;
1136
+ const response = await axios2.get(requestUrl, {
1137
+ headers,
1138
+ timeout: 5e3,
1139
+ maxRedirects: 0
1140
+ });
1141
+ if (response.status === 200 && response.data && Array.isArray(response.data.tools)) {
1142
+ return response.data;
1143
+ }
1144
+ } catch (error) {
1145
+ this.logger.debug({ err: error }, "Failed to fetch manifest (will fallback)");
1146
+ }
1147
+ return null;
1148
+ }
1149
+ };
1150
+
1151
+ // src/gateway/auth.service.ts
1152
+ import axios3 from "axios";
1153
+ var AuthService = class {
1154
+ logger;
1155
+ // Cache tokens separately from credentials to avoid mutation
1156
+ tokenCache = /* @__PURE__ */ new Map();
1157
+ // Prevent concurrent refresh requests for the same client
1158
+ refreshLocks = /* @__PURE__ */ new Map();
1159
+ constructor(logger) {
1160
+ this.logger = logger;
1161
+ }
1162
+ async getAuthHeaders(creds) {
1163
+ switch (creds.type) {
1164
+ case "apiKey":
1165
+ return { "X-API-Key": creds.apiKey || "" };
1166
+ case "bearer":
1167
+ return { "Authorization": `Bearer ${creds.bearerToken}` };
1168
+ case "oauth2":
1169
+ return { "Authorization": await this.getOAuth2Token(creds) };
1170
+ default:
1171
+ throw new Error(`Unsupported auth type: ${creds.type}`);
1172
+ }
1173
+ }
1174
+ async getOAuth2Token(creds) {
1175
+ if (!creds.tokenUrl || !creds.clientId) {
1176
+ throw new Error("OAuth2 credentials missing required fields (tokenUrl, clientId)");
1177
+ }
1178
+ const cacheKey = `${creds.clientId}:${creds.tokenUrl}`;
1179
+ const cached = this.tokenCache.get(cacheKey);
1180
+ if (cached && cached.expiresAt > Date.now() + 3e4) {
1181
+ return `Bearer ${cached.accessToken}`;
1182
+ }
1183
+ const existingRefresh = this.refreshLocks.get(cacheKey);
1184
+ if (existingRefresh) {
1185
+ return existingRefresh;
1186
+ }
1187
+ const refreshPromise = this.doRefresh(creds, cacheKey);
1188
+ this.refreshLocks.set(cacheKey, refreshPromise);
1189
+ try {
1190
+ return await refreshPromise;
1191
+ } finally {
1192
+ this.refreshLocks.delete(cacheKey);
1193
+ }
1194
+ }
1195
+ async doRefresh(creds, cacheKey) {
1196
+ if (!creds.tokenUrl || !creds.refreshToken || !creds.clientId || !creds.clientSecret) {
1197
+ throw new Error("OAuth2 credentials missing required fields for refresh");
1198
+ }
1199
+ this.logger.info({ tokenUrl: creds.tokenUrl, clientId: creds.clientId }, "Refreshing OAuth2 token");
1200
+ try {
1201
+ const response = await axios3.post(creds.tokenUrl, {
1202
+ grant_type: "refresh_token",
1203
+ refresh_token: creds.refreshToken,
1204
+ client_id: creds.clientId,
1205
+ client_secret: creds.clientSecret
1206
+ });
1207
+ const { access_token, expires_in } = response.data;
1208
+ this.tokenCache.set(cacheKey, {
1209
+ accessToken: access_token,
1210
+ expiresAt: Date.now() + expires_in * 1e3
1211
+ });
1212
+ return `Bearer ${access_token}`;
1213
+ } catch (err) {
1214
+ const errorMsg = err.response?.data?.error_description || err.response?.data?.error || err.message;
1215
+ this.logger.error({ err: errorMsg }, "Failed to refresh OAuth2 token");
1216
+ throw new Error(`OAuth2 refresh failed: ${errorMsg}`);
1217
+ }
1218
+ }
1219
+ };
1220
+
1221
+ // src/gateway/schema.cache.ts
1222
+ import { LRUCache } from "lru-cache";
1223
+ var SchemaCache = class {
1224
+ cache;
1225
+ logger;
1226
+ constructor(logger, max = 100, ttl = 1e3 * 60 * 60) {
1227
+ this.logger = logger;
1228
+ this.cache = new LRUCache({
1229
+ max,
1230
+ ttl
1231
+ });
1232
+ }
1233
+ get(upstreamId) {
1234
+ const result = this.cache.get(upstreamId);
1235
+ if (result) {
1236
+ metrics.recordCacheHit();
1237
+ } else {
1238
+ metrics.recordCacheMiss();
1239
+ }
1240
+ return result;
1241
+ }
1242
+ set(upstreamId, tools) {
1243
+ this.logger.debug({ upstreamId, count: tools.length }, "Caching tool schemas");
1244
+ this.cache.set(upstreamId, tools);
1245
+ }
1246
+ invalidate(upstreamId) {
1247
+ this.logger.debug({ upstreamId }, "Invalidating schema cache");
1248
+ this.cache.delete(upstreamId);
1249
+ }
1250
+ clear() {
1251
+ this.cache.clear();
1252
+ }
1253
+ };
1254
+
1255
+ // src/core/policy.service.ts
1256
+ var PolicyService = class {
1257
+ /**
1258
+ * Parse a qualified tool name string into a structured ToolIdentifier.
1259
+ * @param qualifiedName - e.g., "github__createIssue" or "github__api__listRepos"
1260
+ */
1261
+ parseToolName(qualifiedName) {
1262
+ const separatorIndex = qualifiedName.indexOf("__");
1263
+ if (separatorIndex === -1) {
1264
+ return { namespace: "", name: qualifiedName };
1265
+ }
1266
+ return {
1267
+ namespace: qualifiedName.substring(0, separatorIndex),
1268
+ name: qualifiedName.substring(separatorIndex + 2)
1269
+ };
1270
+ }
1271
+ /**
1272
+ * Format a ToolIdentifier back to a qualified string.
1273
+ */
1274
+ formatToolName(tool) {
1275
+ if (!tool.namespace) {
1276
+ return tool.name;
1277
+ }
1278
+ return `${tool.namespace}__${tool.name}`;
1279
+ }
1280
+ /**
1281
+ * Check if a tool matches any pattern in the allowlist.
1282
+ * Supports:
1283
+ * - Exact match: "github.createIssue" matches "github__createIssue"
1284
+ * - Wildcard: "github.*" matches any tool in the github namespace
1285
+ *
1286
+ * @param tool - ToolIdentifier or qualified string
1287
+ * @param allowedTools - Array of patterns (dot-notation, e.g., "github.*" or "github.createIssue")
1288
+ */
1289
+ isToolAllowed(tool, allowedTools) {
1290
+ const toolId = typeof tool === "string" ? this.parseToolName(tool) : tool;
1291
+ const toolParts = [toolId.namespace, ...toolId.name.split("__")].filter((p) => p);
1292
+ return allowedTools.some((pattern) => {
1293
+ const patternParts = pattern.split(".");
1294
+ if (patternParts[patternParts.length - 1] === "*") {
1295
+ const prefixParts = patternParts.slice(0, -1);
1296
+ if (prefixParts.length > toolParts.length) return false;
1297
+ for (let i = 0; i < prefixParts.length; i++) {
1298
+ if (prefixParts[i] !== toolParts[i]) return false;
1299
+ }
1300
+ return true;
1301
+ }
1302
+ if (patternParts.length !== toolParts.length) return false;
1303
+ for (let i = 0; i < patternParts.length; i++) {
1304
+ if (patternParts[i] !== toolParts[i]) return false;
1305
+ }
1306
+ return true;
1307
+ });
1308
+ }
1309
+ };
1310
+
1311
+ // src/gateway/gateway.service.ts
1312
+ import { Ajv } from "ajv";
1313
+ import addFormats from "ajv-formats";
1314
+ var BUILT_IN_TOOLS = [
1315
+ {
1316
+ name: "mcp_execute_typescript",
1317
+ description: "Executes TypeScript code in a secure sandbox with access to `tools.*` SDK.",
1318
+ inputSchema: {
1319
+ type: "object",
1320
+ properties: {
1321
+ code: {
1322
+ type: "string",
1323
+ description: "The TypeScript code to execute."
1324
+ },
1325
+ allowedTools: {
1326
+ type: "array",
1327
+ items: { type: "string" },
1328
+ description: 'Optional list of tools the script is allowed to call (e.g. ["github.*"]).'
1329
+ }
1330
+ },
1331
+ required: ["code"]
1332
+ }
1333
+ },
1334
+ {
1335
+ name: "mcp_execute_python",
1336
+ description: "Executes Python code in a secure sandbox with access to `tools.*` SDK.",
1337
+ inputSchema: {
1338
+ type: "object",
1339
+ properties: {
1340
+ code: {
1341
+ type: "string",
1342
+ description: "The Python code to execute."
1343
+ },
1344
+ allowedTools: {
1345
+ type: "array",
1346
+ items: { type: "string" },
1347
+ description: 'Optional list of tools the script is allowed to call (e.g. ["github.*"]).'
1348
+ }
1349
+ },
1350
+ required: ["code"]
1351
+ }
1352
+ },
1353
+ {
1354
+ name: "mcp_execute_isolate",
1355
+ description: "Executes JavaScript code in a high-speed V8 isolate (no Deno/Node APIs).",
1356
+ inputSchema: {
1357
+ type: "object",
1358
+ properties: {
1359
+ code: {
1360
+ type: "string",
1361
+ description: "The JavaScript code to execute."
1362
+ },
1363
+ allowedTools: {
1364
+ type: "array",
1365
+ items: { type: "string" },
1366
+ description: "Optional list of tools the script is allowed to call."
1367
+ }
1368
+ },
1369
+ required: ["code"]
1370
+ }
1371
+ }
1372
+ ];
1373
+ var GatewayService = class {
1374
+ logger;
1375
+ clients = /* @__PURE__ */ new Map();
1376
+ authService;
1377
+ schemaCache;
1378
+ urlValidator;
1379
+ policyService;
1380
+ ajv;
1381
+ // Cache compiled validators to avoid recompilation on every call
1382
+ validatorCache = /* @__PURE__ */ new Map();
1383
+ constructor(logger, urlValidator, policyService) {
1384
+ this.logger = logger;
1385
+ this.urlValidator = urlValidator;
1386
+ this.authService = new AuthService(logger);
1387
+ this.schemaCache = new SchemaCache(logger);
1388
+ this.policyService = policyService ?? new PolicyService();
1389
+ this.ajv = new Ajv({ strict: false });
1390
+ addFormats.default(this.ajv);
1391
+ }
1392
+ registerUpstream(info) {
1393
+ const client = new UpstreamClient(this.logger, info, this.authService, this.urlValidator);
1394
+ this.clients.set(info.id, client);
1395
+ this.logger.info({ upstreamId: info.id }, "Registered upstream MCP");
1396
+ }
1397
+ async listToolPackages() {
1398
+ return Array.from(this.clients.entries()).map(([id, client]) => ({
1399
+ id,
1400
+ description: `Upstream ${id}`,
1401
+ // NOTE: Upstream description fetching deferred to V2
1402
+ version: "1.0.0"
1403
+ }));
1404
+ }
1405
+ async listToolStubs(packageId, context) {
1406
+ const client = this.clients.get(packageId);
1407
+ if (!client) {
1408
+ throw new Error(`Upstream package not found: ${packageId}`);
1409
+ }
1410
+ let tools = this.schemaCache.get(packageId);
1411
+ if (!tools) {
1412
+ try {
1413
+ const manifest = await client.getManifest(context);
1414
+ if (manifest) {
1415
+ const stubs2 = manifest.tools.map((t) => ({
1416
+ id: `${packageId}__${t.name}`,
1417
+ name: t.name,
1418
+ description: t.description
1419
+ }));
1420
+ if (context.allowedTools) {
1421
+ return stubs2.filter((t) => this.policyService.isToolAllowed(t.id, context.allowedTools));
1422
+ }
1423
+ return stubs2;
1424
+ }
1425
+ } catch (e) {
1426
+ this.logger.debug({ packageId, err: e }, "Manifest fetch failed, falling back to RPC");
1427
+ }
1428
+ const response = await client.call({
1429
+ jsonrpc: "2.0",
1430
+ id: "discovery",
1431
+ method: "list_tools"
1432
+ }, context);
1433
+ if (response.result?.tools) {
1434
+ tools = response.result.tools;
1435
+ this.schemaCache.set(packageId, tools);
1436
+ } else {
1437
+ this.logger.warn({ upstreamId: packageId, error: response.error }, "Failed to discover tools from upstream");
1438
+ tools = [];
1439
+ }
1440
+ }
1441
+ const stubs = tools.map((t) => ({
1442
+ id: `${packageId}__${t.name}`,
1443
+ name: t.name,
1444
+ description: t.description
1445
+ }));
1446
+ if (context.allowedTools) {
1447
+ return stubs.filter((t) => this.policyService.isToolAllowed(t.id, context.allowedTools));
1448
+ }
1449
+ return stubs;
1450
+ }
1451
+ async getToolSchema(toolId, context) {
1452
+ if (context.allowedTools && !this.policyService.isToolAllowed(toolId, context.allowedTools)) {
1453
+ throw new Error(`Access to tool ${toolId} is forbidden by allowlist`);
1454
+ }
1455
+ const parsed = this.policyService.parseToolName(toolId);
1456
+ const toolName = parsed.name;
1457
+ const builtIn = BUILT_IN_TOOLS.find((t) => t.name === toolId);
1458
+ if (builtIn) return builtIn;
1459
+ const upstreamId = parsed.namespace;
1460
+ if (!this.schemaCache.get(upstreamId)) {
1461
+ await this.listToolStubs(upstreamId, context);
1462
+ }
1463
+ const tools = this.schemaCache.get(upstreamId) || [];
1464
+ const tool = tools.find((t) => t.name === toolName);
1465
+ if (!tool) return null;
1466
+ return {
1467
+ ...tool,
1468
+ name: toolId
1469
+ };
1470
+ }
1471
+ async discoverTools(context) {
1472
+ const allTools = [...BUILT_IN_TOOLS];
1473
+ for (const [id, client] of this.clients.entries()) {
1474
+ let tools = this.schemaCache.get(id);
1475
+ if (!tools) {
1476
+ const response = await client.call({
1477
+ jsonrpc: "2.0",
1478
+ id: "discovery",
1479
+ method: "list_tools"
1480
+ // Standard MCP method
1481
+ }, context);
1482
+ if (response.result?.tools) {
1483
+ tools = response.result.tools;
1484
+ this.schemaCache.set(id, tools);
1485
+ } else {
1486
+ this.logger.warn({ upstreamId: id, error: response.error }, "Failed to discover tools from upstream");
1487
+ tools = [];
1488
+ }
1489
+ }
1490
+ const prefixedTools = tools.map((t) => ({ ...t, name: `${id}__${t.name}` }));
1491
+ if (context.allowedTools) {
1492
+ allTools.push(...prefixedTools.filter((t) => this.policyService.isToolAllowed(t.name, context.allowedTools)));
1493
+ } else {
1494
+ allTools.push(...prefixedTools);
1495
+ }
1496
+ }
1497
+ return allTools;
1498
+ }
1499
+ async callTool(name, params, context) {
1500
+ if (context.allowedTools && !this.policyService.isToolAllowed(name, context.allowedTools)) {
1501
+ this.logger.warn({ name, allowedTools: context.allowedTools }, "Tool call blocked by allowlist");
1502
+ return {
1503
+ jsonrpc: "2.0",
1504
+ id: 0,
1505
+ error: {
1506
+ code: -32003,
1507
+ message: `Authorization failed: tool ${name} is not in the allowlist`
1508
+ }
1509
+ };
1510
+ }
1511
+ const toolId = this.policyService.parseToolName(name);
1512
+ const upstreamId = toolId.namespace;
1513
+ const toolName = toolId.name;
1514
+ const client = this.clients.get(upstreamId);
1515
+ if (!client) {
1516
+ return {
1517
+ jsonrpc: "2.0",
1518
+ id: 0,
1519
+ error: {
1520
+ code: -32003,
1521
+ message: `Upstream not found: ${upstreamId}`
1522
+ }
1523
+ };
1524
+ }
1525
+ if (!this.schemaCache.get(upstreamId)) {
1526
+ await this.listToolStubs(upstreamId, context);
1527
+ }
1528
+ const tools = this.schemaCache.get(upstreamId) || [];
1529
+ const toolSchema = tools.find((t) => t.name === toolName);
1530
+ if (context.strictValidation) {
1531
+ if (!toolSchema) {
1532
+ return {
1533
+ jsonrpc: "2.0",
1534
+ id: 0,
1535
+ error: {
1536
+ code: -32601,
1537
+ // Method not found / Schema missing
1538
+ message: `Strict mode: Tool schema for ${name} not found`
1539
+ }
1540
+ };
1541
+ }
1542
+ if (!toolSchema.inputSchema) {
1543
+ return {
1544
+ jsonrpc: "2.0",
1545
+ id: 0,
1546
+ error: {
1547
+ code: -32602,
1548
+ // Invalid params
1549
+ message: `Strict mode: Tool ${name} has no input schema defined`
1550
+ }
1551
+ };
1552
+ }
1553
+ }
1554
+ if (toolSchema && toolSchema.inputSchema) {
1555
+ const cacheKey = `${upstreamId}__${toolName}`;
1556
+ let validate = this.validatorCache.get(cacheKey);
1557
+ if (!validate) {
1558
+ validate = this.ajv.compile(toolSchema.inputSchema);
1559
+ this.validatorCache.set(cacheKey, validate);
1560
+ }
1561
+ const valid = validate(params);
1562
+ if (!valid) {
1563
+ return {
1564
+ jsonrpc: "2.0",
1565
+ id: 0,
1566
+ error: {
1567
+ code: -32602,
1568
+ // Invalid params
1569
+ message: `Invalid parameters for tool ${name}: ${this.ajv.errorsText(validate.errors)}`
1570
+ }
1571
+ };
1572
+ }
1573
+ }
1574
+ const startTime = performance.now();
1575
+ let success = false;
1576
+ let response;
1577
+ try {
1578
+ response = await client.call({
1579
+ jsonrpc: "2.0",
1580
+ id: context.correlationId,
1581
+ method: "call_tool",
1582
+ params: {
1583
+ name: toolName,
1584
+ arguments: params
1585
+ }
1586
+ }, context);
1587
+ success = !response.error;
1588
+ } catch (error) {
1589
+ success = false;
1590
+ throw error;
1591
+ } finally {
1592
+ const duration = performance.now() - startTime;
1593
+ metrics.recordToolExecution(duration, toolName, success);
1594
+ }
1595
+ if (response.error && response.error.code === -32008) {
1596
+ this.schemaCache.invalidate(upstreamId);
1597
+ }
1598
+ return response;
1599
+ }
1600
+ async healthCheck() {
1601
+ const upstreamStatus = {};
1602
+ const context = new ExecutionContext({ logger: this.logger });
1603
+ await Promise.all(
1604
+ Array.from(this.clients.entries()).map(async ([id, client]) => {
1605
+ try {
1606
+ const response = await client.call({
1607
+ jsonrpc: "2.0",
1608
+ id: "health",
1609
+ method: "list_tools"
1610
+ }, context);
1611
+ upstreamStatus[id] = response.error ? "degraded" : "active";
1612
+ } catch (err) {
1613
+ upstreamStatus[id] = "error";
1614
+ }
1615
+ })
1616
+ );
1617
+ const allOk = Object.values(upstreamStatus).every((s) => s === "active");
1618
+ return {
1619
+ status: allOk ? "ok" : "degraded",
1620
+ upstreams: upstreamStatus
1621
+ };
1622
+ }
1623
+ async validateTool(name, params, context) {
1624
+ const toolId = this.policyService.parseToolName(name);
1625
+ const upstreamId = toolId.namespace;
1626
+ const toolName = toolId.name;
1627
+ if (!this.schemaCache.get(upstreamId)) {
1628
+ await this.listToolStubs(upstreamId, context);
1629
+ }
1630
+ const tools = this.schemaCache.get(upstreamId) || [];
1631
+ const toolSchema = tools.find((t) => t.name === toolName);
1632
+ if (!toolSchema) {
1633
+ return { valid: false, errors: [`Tool ${name} not found`] };
1634
+ }
1635
+ if (context.strictValidation) {
1636
+ if (!toolSchema.inputSchema) {
1637
+ return { valid: false, errors: [`Strict mode: Tool ${name} has no input schema defined`] };
1638
+ }
1639
+ }
1640
+ if (!toolSchema.inputSchema) {
1641
+ return { valid: true };
1642
+ }
1643
+ const validate = this.ajv.compile(toolSchema.inputSchema);
1644
+ const valid = validate(params);
1645
+ if (!valid) {
1646
+ return {
1647
+ valid: false,
1648
+ errors: validate.errors?.map((e) => this.ajv.errorsText([e])) || ["Unknown validation error"]
1649
+ };
1650
+ }
1651
+ return { valid: true };
1652
+ }
1653
+ };
1654
+
1655
+ // src/core/network.policy.service.ts
1656
+ import dns from "dns/promises";
1657
+ import net2 from "net";
1658
+ import { LRUCache as LRUCache2 } from "lru-cache";
1659
+ var NetworkPolicyService = class {
1660
+ logger;
1661
+ privateRanges = [
1662
+ /^127\./,
1663
+ /^10\./,
1664
+ /^172\.(1[6-9]|2[0-9]|3[0-1])\./,
1665
+ /^192\.168\./,
1666
+ /^169\.254\./,
1667
+ // Link-local
1668
+ /^localhost$/i,
1669
+ /^0\.0\.0\.0$/,
1670
+ /^::1$/,
1671
+ // IPv6 localhost
1672
+ /^fc00:/i,
1673
+ // IPv6 private
1674
+ /^fe80:/i
1675
+ // IPv6 link-local
1676
+ ];
1677
+ RATE_LIMIT = 30;
1678
+ WINDOW_MS = 6e4;
1679
+ // Use LRUCache to prevent unbounded memory growth
1680
+ requestCounts;
1681
+ constructor(logger) {
1682
+ this.logger = logger;
1683
+ this.requestCounts = new LRUCache2({
1684
+ max: 1e4,
1685
+ ttl: this.WINDOW_MS
1686
+ });
1687
+ }
1688
+ async validateUrl(url) {
1689
+ try {
1690
+ const parsed = new URL(url);
1691
+ const hostname = parsed.hostname;
1692
+ for (const range of this.privateRanges) {
1693
+ if (range.test(hostname)) {
1694
+ this.logger.warn({ hostname }, "SSRF attempt detected: private range access");
1695
+ return { valid: false, message: "Access denied: private network access forbidden" };
1696
+ }
1697
+ }
1698
+ if (!net2.isIP(hostname)) {
1699
+ try {
1700
+ const lookup = await dns.lookup(hostname, { all: true });
1701
+ const resolvedIps = [];
1702
+ for (const address of lookup) {
1703
+ let ip = address.address;
1704
+ if (ip.startsWith("::ffff:")) {
1705
+ ip = ip.substring(7);
1706
+ }
1707
+ for (const range of this.privateRanges) {
1708
+ if (range.test(ip)) {
1709
+ this.logger.warn({ hostname, ip }, "SSRF attempt detected: DNS resolves to private IP");
1710
+ return { valid: false, message: "Access denied: hostname resolves to private network" };
1711
+ }
1712
+ }
1713
+ resolvedIps.push(ip);
1714
+ }
1715
+ return { valid: true, resolvedIp: resolvedIps[0] };
1716
+ } catch (err) {
1717
+ this.logger.warn({ hostname, err: err.message }, "DNS lookup failed during URL validation, blocking request");
1718
+ return { valid: false, message: "Access denied: hostname resolution failed" };
1719
+ }
1720
+ }
1721
+ return { valid: true, resolvedIp: hostname };
1722
+ } catch (err) {
1723
+ return { valid: false, message: `Invalid URL: ${err.message}` };
1724
+ }
1725
+ }
1726
+ checkRateLimit(key) {
1727
+ const now = Date.now();
1728
+ const record = this.requestCounts.get(key);
1729
+ if (!record || now > record.resetTime) {
1730
+ this.requestCounts.set(key, { count: 1, resetTime: now + this.WINDOW_MS });
1731
+ return true;
1732
+ }
1733
+ if (record.count >= this.RATE_LIMIT) {
1734
+ this.logger.warn({ key }, "Rate limit exceeded");
1735
+ return false;
1736
+ }
1737
+ record.count++;
1738
+ return true;
1739
+ }
1740
+ };
1741
+
1742
+ // src/core/session.manager.ts
1743
+ import { v4 as uuidv42 } from "uuid";
1744
+ import { LRUCache as LRUCache3 } from "lru-cache";
1745
+ var SessionManager = class {
1746
+ logger;
1747
+ sessions;
1748
+ SESSION_TTL_MS = 36e5;
1749
+ // 1 hour
1750
+ constructor(logger) {
1751
+ this.logger = logger;
1752
+ this.sessions = new LRUCache3({
1753
+ max: 1e4,
1754
+ ttl: this.SESSION_TTL_MS
1755
+ });
1756
+ }
1757
+ createSession(allowedTools) {
1758
+ const token = uuidv42();
1759
+ this.sessions.set(token, {
1760
+ allowedTools,
1761
+ createdAt: Date.now()
1762
+ });
1763
+ return token;
1764
+ }
1765
+ getSession(token) {
1766
+ return this.sessions.get(token);
1767
+ }
1768
+ invalidateSession(token) {
1769
+ this.sessions.delete(token);
1770
+ }
1771
+ cleanupSessions() {
1772
+ this.sessions.purgeStale();
1773
+ }
1774
+ };
1775
+
1776
+ // src/core/security.service.ts
1777
+ import crypto2 from "crypto";
1778
+ var SecurityService = class {
1779
+ logger;
1780
+ ipcToken;
1781
+ networkPolicy;
1782
+ sessionManager;
1783
+ constructor(logger, ipcToken) {
1784
+ this.logger = logger;
1785
+ this.ipcToken = ipcToken;
1786
+ this.networkPolicy = new NetworkPolicyService(logger);
1787
+ this.sessionManager = new SessionManager(logger);
1788
+ }
1789
+ validateCode(code) {
1790
+ if (!code || code.length > 1024 * 1024) {
1791
+ return { valid: false, message: "Code size exceeds limit or is empty" };
1792
+ }
1793
+ return { valid: true };
1794
+ }
1795
+ async validateUrl(url) {
1796
+ return this.networkPolicy.validateUrl(url);
1797
+ }
1798
+ checkRateLimit(key) {
1799
+ return this.networkPolicy.checkRateLimit(key);
1800
+ }
1801
+ validateIpcToken(token) {
1802
+ if (!this.ipcToken) {
1803
+ return true;
1804
+ }
1805
+ const expected = Buffer.from(this.ipcToken);
1806
+ const actual = Buffer.from(token);
1807
+ if (expected.length === actual.length && crypto2.timingSafeEqual(expected, actual)) {
1808
+ return true;
1809
+ }
1810
+ return !!this.sessionManager.getSession(token);
1811
+ }
1812
+ createSession(allowedTools) {
1813
+ return this.sessionManager.createSession(allowedTools);
1814
+ }
1815
+ getSession(token) {
1816
+ return this.sessionManager.getSession(token);
1817
+ }
1818
+ invalidateSession(token) {
1819
+ this.sessionManager.invalidateSession(token);
1820
+ }
1821
+ getIpcToken() {
1822
+ return this.ipcToken;
1823
+ }
1824
+ };
1825
+
1826
+ // src/core/otel.service.ts
1827
+ import { NodeSDK } from "@opentelemetry/sdk-node";
1828
+ import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";
1829
+ import { PrometheusExporter } from "@opentelemetry/exporter-prometheus";
1830
+ import { resourceFromAttributes } from "@opentelemetry/resources";
1831
+ import { SemanticResourceAttributes } from "@opentelemetry/semantic-conventions";
1832
+ import { PinoInstrumentation } from "@opentelemetry/instrumentation-pino";
1833
+ var OtelService = class {
1834
+ constructor(logger) {
1835
+ this.logger = logger;
1836
+ }
1837
+ sdk = null;
1838
+ async start() {
1839
+ this.sdk = new NodeSDK({
1840
+ resource: resourceFromAttributes({
1841
+ [SemanticResourceAttributes.SERVICE_NAME]: "conduit"
1842
+ }),
1843
+ metricReader: new PrometheusExporter({
1844
+ port: 9464
1845
+ // Default prometheus exporter port
1846
+ }),
1847
+ instrumentations: [
1848
+ getNodeAutoInstrumentations(),
1849
+ new PinoInstrumentation()
1850
+ ]
1851
+ });
1852
+ try {
1853
+ await this.sdk.start();
1854
+ this.logger.info("OpenTelemetry SDK started");
1855
+ } catch (error) {
1856
+ this.logger.error({ error }, "Error starting OpenTelemetry SDK");
1857
+ }
1858
+ }
1859
+ async shutdown() {
1860
+ if (this.sdk) {
1861
+ await this.sdk.shutdown();
1862
+ this.logger.info("OpenTelemetry SDK shut down");
1863
+ }
1864
+ }
1865
+ };
1866
+
1867
+ // src/executors/deno.executor.ts
1868
+ import { spawn, exec } from "child_process";
1869
+ import { promisify } from "util";
1870
+ import fs4 from "fs";
1871
+ import path4 from "path";
1872
+ import { platform } from "os";
1873
+ import { fileURLToPath as fileURLToPath2 } from "url";
1874
+
1875
+ // src/core/asset.utils.ts
1876
+ import path3 from "path";
1877
+ import fs3 from "fs";
1878
+ import { fileURLToPath } from "url";
1879
+ var __dirname = path3.dirname(fileURLToPath(import.meta.url));
1880
+ function resolveAssetPath(filename) {
1881
+ const candidates = [
1882
+ // Source structure: src/core/asset.utils.ts -> src/assets/
1883
+ path3.resolve(__dirname, "../assets", filename),
1884
+ // Dist structure possibility 1: dist/ (flat) with assets/ subdir
1885
+ path3.resolve(__dirname, "./assets", filename),
1886
+ // Dist structure possibility 2: dist/core/ -> dist/assets/
1887
+ path3.resolve(__dirname, "../../assets", filename),
1888
+ // Dist structure possibility 3: dist/ -> assets/ (if called from root)
1889
+ path3.resolve(process.cwd(), "assets", filename),
1890
+ // Dist structure possibility 4: dist/assets/ (from root)
1891
+ path3.resolve(process.cwd(), "dist/assets", filename)
1892
+ ];
1893
+ for (const candidate of candidates) {
1894
+ if (fs3.existsSync(candidate)) {
1895
+ return candidate;
1896
+ }
1897
+ }
1898
+ throw new Error(`Asset not found: ${filename}. Checked paths: ${candidates.join(", ")}`);
1899
+ }
1900
+
1901
+ // src/executors/deno.executor.ts
1902
+ var execAsync = promisify(exec);
1903
+ var __dirname2 = path4.dirname(fileURLToPath2(import.meta.url));
1904
+ var DenoExecutor = class {
1905
+ shimContent = "";
1906
+ // Track active processes for cleanup
1907
+ // Using 'any' for the Set because ChildProcess type import can be finicky across node versions/types
1908
+ // but at runtime it is a ChildProcess
1909
+ activeProcesses = /* @__PURE__ */ new Set();
1910
+ maxConcurrentProcesses;
1911
+ constructor(maxConcurrentProcesses = 10) {
1912
+ this.maxConcurrentProcesses = maxConcurrentProcesses;
1913
+ }
1914
+ getShim() {
1915
+ if (this.shimContent) return this.shimContent;
1916
+ try {
1917
+ const assetPath = resolveAssetPath("deno-shim.ts");
1918
+ this.shimContent = fs4.readFileSync(assetPath, "utf-8");
1919
+ return this.shimContent;
1920
+ } catch (err) {
1921
+ throw new Error(`Failed to load Deno shim: ${err.message}`);
1922
+ }
1923
+ }
1924
+ async execute(code, limits, context, config) {
1925
+ const { logger } = context;
1926
+ if (this.activeProcesses.size >= this.maxConcurrentProcesses) {
1927
+ return {
1928
+ stdout: "",
1929
+ stderr: "",
1930
+ exitCode: null,
1931
+ error: {
1932
+ code: -32e3 /* ServerBusy */,
1933
+ message: "Too many concurrent Deno processes"
1934
+ }
1935
+ };
1936
+ }
1937
+ let stdout = "";
1938
+ let stderr = "";
1939
+ let totalOutputBytes = 0;
1940
+ let totalLogEntries = 0;
1941
+ let isTerminated = false;
1942
+ let shim = this.getShim().replace("__CONDUIT_IPC_ADDRESS__", config?.ipcAddress || "").replace("__CONDUIT_IPC_TOKEN__", config?.ipcToken || "");
1943
+ if (shim.includes("__CONDUIT_IPC_ADDRESS__")) {
1944
+ throw new Error("Failed to inject IPC address into Deno shim");
1945
+ }
1946
+ if (shim.includes("__CONDUIT_IPC_TOKEN__")) {
1947
+ throw new Error("Failed to inject IPC token into Deno shim");
1948
+ }
1949
+ if (config?.sdkCode) {
1950
+ shim = shim.replace("// __CONDUIT_SDK_INJECTION__", config.sdkCode);
1951
+ if (shim.includes("// __CONDUIT_SDK_INJECTION__")) {
1952
+ throw new Error("Failed to inject SDK code into Deno shim");
1953
+ }
1954
+ }
1955
+ const fullCode = shim + "\n" + code;
1956
+ const args = [
1957
+ "run",
1958
+ `--v8-flags=--max-heap-size=${limits.memoryLimitMb}`
1959
+ ];
1960
+ if (config?.ipcAddress && !config.ipcAddress.includes("/") && !config.ipcAddress.includes("\\")) {
1961
+ try {
1962
+ const url = new URL(`http://${config.ipcAddress}`);
1963
+ let normalizedHost = url.hostname;
1964
+ normalizedHost = normalizedHost.replace(/[\[\]]/g, "");
1965
+ if (normalizedHost === "0.0.0.0" || normalizedHost === "::" || normalizedHost === "::1" || normalizedHost === "") {
1966
+ normalizedHost = "127.0.0.1";
1967
+ }
1968
+ args.push(`--allow-net=${normalizedHost}`);
1969
+ } catch (err) {
1970
+ logger.warn({ address: config.ipcAddress, err }, "Failed to parse IPC address for Deno permissions");
1971
+ }
1972
+ } else {
1973
+ }
1974
+ args.push("-");
1975
+ const child = spawn("deno", args, {
1976
+ stdio: ["pipe", "pipe", "pipe"],
1977
+ env: {
1978
+ PATH: process.env.PATH,
1979
+ HOME: process.env.HOME,
1980
+ TMPDIR: process.env.TMPDIR
1981
+ }
1982
+ });
1983
+ this.activeProcesses.add(child);
1984
+ child.on("spawn", () => {
1985
+ });
1986
+ const cleanupProcess = () => {
1987
+ this.activeProcesses.delete(child);
1988
+ };
1989
+ return new Promise((resolve) => {
1990
+ const timeout = setTimeout(() => {
1991
+ if (!isTerminated) {
1992
+ isTerminated = true;
1993
+ if (typeof monitorInterval !== "undefined") clearInterval(monitorInterval);
1994
+ child.kill("SIGKILL");
1995
+ logger.warn("Execution timed out, SIGKILL sent");
1996
+ cleanupProcess();
1997
+ resolve({
1998
+ stdout,
1999
+ stderr,
2000
+ exitCode: null,
2001
+ error: {
2002
+ code: -32008 /* RequestTimeout */,
2003
+ message: "Execution timed out"
2004
+ }
2005
+ });
2006
+ }
2007
+ }, limits.timeoutMs);
2008
+ const isWindows = platform() === "win32";
2009
+ const monitorInterval = setInterval(async () => {
2010
+ if (isTerminated || !child.pid) {
2011
+ clearInterval(monitorInterval);
2012
+ return;
2013
+ }
2014
+ try {
2015
+ let rssMb = 0;
2016
+ if (isWindows) {
2017
+ try {
2018
+ const { stdout: tasklistOut } = await execAsync(`tasklist /FI "PID eq ${child.pid}" /FO CSV /NH`);
2019
+ const match = tasklistOut.match(/"([^"]+ K)"$/m);
2020
+ if (match) {
2021
+ const memStr = match[1].replace(/[ K,]/g, "");
2022
+ const memKb = parseInt(memStr, 10);
2023
+ if (!isNaN(memKb)) {
2024
+ rssMb = memKb / 1024;
2025
+ }
2026
+ }
2027
+ } catch (e) {
2028
+ }
2029
+ } else {
2030
+ const { stdout: rssStdout } = await execAsync(`ps -o rss= -p ${child.pid}`);
2031
+ const rssKb = parseInt(rssStdout.trim());
2032
+ if (!isNaN(rssKb)) {
2033
+ rssMb = rssKb / 1024;
2034
+ }
2035
+ }
2036
+ if (rssMb > limits.memoryLimitMb) {
2037
+ isTerminated = true;
2038
+ if (typeof monitorInterval !== "undefined") clearInterval(monitorInterval);
2039
+ child.kill("SIGKILL");
2040
+ logger.warn({ rssMb, limitMb: limits.memoryLimitMb }, "Deno RSS limit exceeded, SIGKILL sent");
2041
+ cleanupProcess();
2042
+ resolve({
2043
+ stdout,
2044
+ stderr,
2045
+ exitCode: null,
2046
+ error: {
2047
+ code: -32009 /* MemoryLimitExceeded */,
2048
+ message: `Memory limit exceeded: ${rssMb.toFixed(2)}MB > ${limits.memoryLimitMb}MB`
2049
+ }
2050
+ });
2051
+ }
2052
+ } catch (err) {
2053
+ clearInterval(monitorInterval);
2054
+ }
2055
+ }, 2e3);
2056
+ child.stdout.on("data", (chunk) => {
2057
+ if (isTerminated) return;
2058
+ totalOutputBytes += chunk.length;
2059
+ const newLines = (chunk.toString().match(/\n/g) || []).length;
2060
+ totalLogEntries += newLines;
2061
+ if (totalOutputBytes > limits.maxOutputBytes || totalLogEntries > limits.maxLogEntries) {
2062
+ isTerminated = true;
2063
+ if (typeof monitorInterval !== "undefined") clearInterval(monitorInterval);
2064
+ child.kill("SIGKILL");
2065
+ logger.warn({ bytes: totalOutputBytes, lines: totalLogEntries }, "Limits exceeded, SIGKILL sent");
2066
+ cleanupProcess();
2067
+ resolve({
2068
+ stdout: stdout + chunk.toString().slice(0, limits.maxOutputBytes - (totalOutputBytes - chunk.length)),
2069
+ stderr,
2070
+ exitCode: null,
2071
+ error: {
2072
+ code: totalOutputBytes > limits.maxOutputBytes ? -32013 /* OutputLimitExceeded */ : -32014 /* LogLimitExceeded */,
2073
+ message: totalOutputBytes > limits.maxOutputBytes ? "Output limit exceeded" : "Log entry limit exceeded"
2074
+ }
2075
+ });
2076
+ return;
2077
+ }
2078
+ stdout += chunk.toString();
2079
+ });
2080
+ child.stderr.on("data", (chunk) => {
2081
+ if (isTerminated) return;
2082
+ totalOutputBytes += chunk.length;
2083
+ const newLines = (chunk.toString().match(/\n/g) || []).length;
2084
+ totalLogEntries += newLines;
2085
+ if (totalOutputBytes > limits.maxOutputBytes || totalLogEntries > limits.maxLogEntries) {
2086
+ isTerminated = true;
2087
+ if (typeof monitorInterval !== "undefined") clearInterval(monitorInterval);
2088
+ child.kill("SIGKILL");
2089
+ logger.warn({ bytes: totalOutputBytes, lines: totalLogEntries }, "Limits exceeded, SIGKILL sent");
2090
+ cleanupProcess();
2091
+ resolve({
2092
+ stdout,
2093
+ stderr: stderr + chunk.toString().slice(0, limits.maxOutputBytes - (totalOutputBytes - chunk.length)),
2094
+ exitCode: null,
2095
+ error: {
2096
+ code: totalOutputBytes > limits.maxOutputBytes ? -32013 /* OutputLimitExceeded */ : -32014 /* LogLimitExceeded */,
2097
+ message: totalOutputBytes > limits.maxOutputBytes ? "Output limit exceeded" : "Log entry limit exceeded"
2098
+ }
2099
+ });
2100
+ return;
2101
+ }
2102
+ stderr += chunk.toString();
2103
+ });
2104
+ child.on("close", (code2) => {
2105
+ clearTimeout(timeout);
2106
+ if (typeof monitorInterval !== "undefined") clearInterval(monitorInterval);
2107
+ cleanupProcess();
2108
+ if (isTerminated) return;
2109
+ resolve({
2110
+ stdout,
2111
+ stderr,
2112
+ exitCode: code2
2113
+ });
2114
+ });
2115
+ child.on("error", (err) => {
2116
+ clearTimeout(timeout);
2117
+ logger.error({ err }, "Child process error");
2118
+ cleanupProcess();
2119
+ let message = err.message;
2120
+ if (err.code === "ENOENT") {
2121
+ message = "Deno executable not found in PATH. Please ensure Deno is installed.";
2122
+ }
2123
+ resolve({
2124
+ stdout,
2125
+ stderr,
2126
+ exitCode: null,
2127
+ error: {
2128
+ code: -32603 /* InternalError */,
2129
+ message
2130
+ }
2131
+ });
2132
+ });
2133
+ child.stdin.write(fullCode);
2134
+ child.stdin.end();
2135
+ });
2136
+ }
2137
+ async shutdown() {
2138
+ for (const child of this.activeProcesses) {
2139
+ try {
2140
+ child.kill("SIGKILL");
2141
+ } catch (err) {
2142
+ }
2143
+ }
2144
+ this.activeProcesses.clear();
2145
+ }
2146
+ async healthCheck() {
2147
+ try {
2148
+ const { stdout } = await execAsync("deno --version");
2149
+ return { status: "ok", detail: stdout.split("\n")[0] };
2150
+ } catch (err) {
2151
+ return { status: "error", detail: err.message };
2152
+ }
2153
+ }
2154
+ async warmup() {
2155
+ }
2156
+ };
2157
+
2158
+ // src/executors/pyodide.executor.ts
2159
+ import { Worker } from "worker_threads";
2160
+ import fs5 from "fs";
2161
+ import path5 from "path";
2162
+ import { fileURLToPath as fileURLToPath3 } from "url";
2163
+ var __dirname3 = path5.dirname(fileURLToPath3(import.meta.url));
2164
+ var PyodideExecutor = class {
2165
+ shimContent = "";
2166
+ pool = [];
2167
+ maxPoolSize;
2168
+ maxRunsPerWorker = 1;
2169
+ constructor(maxPoolSize = 3) {
2170
+ this.maxPoolSize = maxPoolSize;
2171
+ }
2172
+ getShim() {
2173
+ if (this.shimContent) return this.shimContent;
2174
+ try {
2175
+ const assetPath = resolveAssetPath("python-shim.py");
2176
+ this.shimContent = fs5.readFileSync(assetPath, "utf-8");
2177
+ return this.shimContent;
2178
+ } catch (err) {
2179
+ throw new Error(`Failed to load Python shim: ${err.message}`);
2180
+ }
2181
+ }
2182
+ waitQueue = [];
2183
+ async getWorker(logger, limits) {
2184
+ let pooled = this.pool.find((w) => !w.busy);
2185
+ if (pooled) {
2186
+ pooled.busy = true;
2187
+ return pooled;
2188
+ }
2189
+ if (this.pool.length < this.maxPoolSize) {
2190
+ logger.info("Creating new Pyodide worker for pool");
2191
+ const worker = this.createWorker(limits);
2192
+ pooled = { worker, busy: true, runs: 0, lastUsed: Date.now() };
2193
+ this.pool.push(pooled);
2194
+ await new Promise((resolve, reject) => {
2195
+ const onMessage = (msg) => {
2196
+ if (msg.type === "ready") {
2197
+ worker.off("message", onMessage);
2198
+ resolve();
2199
+ }
2200
+ };
2201
+ worker.on("message", onMessage);
2202
+ worker.on("error", reject);
2203
+ setTimeout(() => {
2204
+ worker.terminate();
2205
+ this.pool = this.pool.filter((p) => p !== pooled);
2206
+ reject(new Error("Worker init timeout"));
2207
+ }, 1e4);
2208
+ });
2209
+ return pooled;
2210
+ }
2211
+ return new Promise((resolve) => {
2212
+ this.waitQueue.push(resolve);
2213
+ });
2214
+ }
2215
+ createWorker(limits) {
2216
+ let workerPath = path5.resolve(__dirname3, "./pyodide.worker.js");
2217
+ if (!fs5.existsSync(workerPath)) {
2218
+ workerPath = path5.resolve(__dirname3, "./pyodide.worker.ts");
2219
+ }
2220
+ return new Worker(workerPath, {
2221
+ execArgv: process.execArgv.includes("--loader") ? process.execArgv : [],
2222
+ resourceLimits: limits ? {
2223
+ maxOldSpaceSizeMb: limits.memoryLimitMb
2224
+ // Stack size and young generation are usually fine with defaults
2225
+ } : void 0
2226
+ });
2227
+ }
2228
+ async warmup(limits) {
2229
+ const needed = this.maxPoolSize - this.pool.length;
2230
+ if (needed <= 0) return;
2231
+ console.error(`Pre-warming ${needed} Pyodide workers...`);
2232
+ const promises = [];
2233
+ for (let i = 0; i < needed; i++) {
2234
+ promises.push(this.createAndPoolWorker(limits));
2235
+ }
2236
+ await Promise.all(promises);
2237
+ console.error(`Pyodide pool pre-warmed with ${this.pool.length} workers.`);
2238
+ }
2239
+ async createAndPoolWorker(limits) {
2240
+ if (this.pool.length >= this.maxPoolSize) return;
2241
+ const worker = this.createWorker(limits);
2242
+ const pooled = { worker, busy: true, runs: 0, lastUsed: Date.now() };
2243
+ this.pool.push(pooled);
2244
+ try {
2245
+ await new Promise((resolve, reject) => {
2246
+ const onMessage = (msg) => {
2247
+ if (msg.type === "ready") {
2248
+ worker.off("message", onMessage);
2249
+ resolve();
2250
+ }
2251
+ };
2252
+ worker.on("message", onMessage);
2253
+ worker.on("error", reject);
2254
+ setTimeout(() => reject(new Error("Worker init timeout")), 1e4);
2255
+ });
2256
+ pooled.busy = false;
2257
+ if (this.waitQueue.length > 0) {
2258
+ const nextResolve = this.waitQueue.shift();
2259
+ if (nextResolve) {
2260
+ pooled.busy = true;
2261
+ nextResolve(pooled);
2262
+ }
2263
+ }
2264
+ } catch (err) {
2265
+ this.pool = this.pool.filter((p) => p !== pooled);
2266
+ worker.terminate();
2267
+ }
2268
+ }
2269
+ async execute(code, limits, context, config) {
2270
+ const { logger } = context;
2271
+ const pooledWorker = await this.getWorker(logger, limits);
2272
+ const worker = pooledWorker.worker;
2273
+ return new Promise((resolve) => {
2274
+ const timeout = setTimeout(() => {
2275
+ logger.warn("Python execution timed out, terminating worker");
2276
+ worker.terminate();
2277
+ this.pool = this.pool.filter((w) => w !== pooledWorker);
2278
+ resolve({
2279
+ stdout: "",
2280
+ stderr: "Execution timed out",
2281
+ exitCode: null,
2282
+ error: {
2283
+ code: -32008 /* RequestTimeout */,
2284
+ message: "Execution timed out"
2285
+ }
2286
+ });
2287
+ }, limits.timeoutMs);
2288
+ const onMessage = (msg) => {
2289
+ if (msg.type === "ready" || msg.type === "pong") return;
2290
+ clearTimeout(timeout);
2291
+ worker.off("message", onMessage);
2292
+ worker.off("error", onError);
2293
+ pooledWorker.busy = false;
2294
+ if (this.waitQueue.length > 0) {
2295
+ const nextResolve = this.waitQueue.shift();
2296
+ if (nextResolve) {
2297
+ pooledWorker.busy = true;
2298
+ nextResolve(pooledWorker);
2299
+ }
2300
+ }
2301
+ pooledWorker.runs++;
2302
+ pooledWorker.lastUsed = Date.now();
2303
+ if (pooledWorker.runs >= this.maxRunsPerWorker) {
2304
+ logger.info("Recycling Pyodide worker after max runs");
2305
+ worker.terminate();
2306
+ this.pool = this.pool.filter((w) => w !== pooledWorker);
2307
+ }
2308
+ if (msg.success) {
2309
+ resolve({
2310
+ stdout: msg.stdout,
2311
+ stderr: msg.stderr,
2312
+ exitCode: 0
2313
+ });
2314
+ } else {
2315
+ logger.warn({ error: msg.error }, "Python execution failed or limit breached, terminating worker");
2316
+ worker.terminate();
2317
+ this.pool = this.pool.filter((w) => w !== pooledWorker);
2318
+ logger.debug({ error: msg.error }, "Python execution error from worker");
2319
+ const normalizedError = (msg.error || "").toLowerCase();
2320
+ const limitBreached = msg.limitBreached || "";
2321
+ const isLogLimit = limitBreached === "log" || normalizedError.includes("[limit_log]");
2322
+ const isOutputLimit = limitBreached === "output" || normalizedError.includes("[limit_output]");
2323
+ const isAmbiguousLimit = !isOutputLimit && !isLogLimit && (normalizedError.includes("i/o error") || normalizedError.includes("errno 29") || normalizedError.includes("limit exceeded"));
2324
+ resolve({
2325
+ stdout: msg.stdout,
2326
+ stderr: msg.stderr,
2327
+ exitCode: 1,
2328
+ error: {
2329
+ code: isLogLimit ? -32014 /* LogLimitExceeded */ : isOutputLimit || isAmbiguousLimit ? -32013 /* OutputLimitExceeded */ : -32603 /* InternalError */,
2330
+ message: isLogLimit ? "Log entry limit exceeded" : isOutputLimit || isAmbiguousLimit ? "Output limit exceeded" : msg.error
2331
+ }
2332
+ });
2333
+ }
2334
+ };
2335
+ const onError = (err) => {
2336
+ clearTimeout(timeout);
2337
+ worker.off("message", onMessage);
2338
+ worker.off("error", onError);
2339
+ logger.error({ err }, "Pyodide worker error");
2340
+ worker.terminate();
2341
+ this.pool = this.pool.filter((w) => w !== pooledWorker);
2342
+ resolve({
2343
+ stdout: "",
2344
+ stderr: err.message,
2345
+ exitCode: null,
2346
+ error: {
2347
+ code: -32603 /* InternalError */,
2348
+ message: err.message
2349
+ }
2350
+ });
2351
+ };
2352
+ worker.on("message", onMessage);
2353
+ worker.on("error", onError);
2354
+ let shim = this.getShim();
2355
+ if (config?.sdkCode) {
2356
+ shim = shim.replace("# __CONDUIT_SDK_INJECTION__", config.sdkCode);
2357
+ }
2358
+ worker.postMessage({
2359
+ type: "execute",
2360
+ data: { code, limits, ipcInfo: config, shim }
2361
+ });
2362
+ });
2363
+ }
2364
+ async shutdown() {
2365
+ for (const pooled of this.pool) {
2366
+ await pooled.worker.terminate();
2367
+ }
2368
+ this.pool = [];
2369
+ }
2370
+ async healthCheck() {
2371
+ try {
2372
+ const pooled = await this.getWorker(console, {
2373
+ timeoutMs: 5e3,
2374
+ memoryLimitMb: 128,
2375
+ maxOutputBytes: 1024,
2376
+ maxLogEntries: 10
2377
+ });
2378
+ return new Promise((resolve) => {
2379
+ let timeout;
2380
+ const onMessage = (msg) => {
2381
+ if (msg.type === "pong") {
2382
+ cleanup();
2383
+ pooled.busy = false;
2384
+ resolve({ status: "ok", workers: this.pool.length });
2385
+ }
2386
+ };
2387
+ const cleanup = () => {
2388
+ clearTimeout(timeout);
2389
+ pooled.worker.off("message", onMessage);
2390
+ };
2391
+ timeout = setTimeout(() => {
2392
+ cleanup();
2393
+ pooled.busy = false;
2394
+ resolve({ status: "error", workers: this.pool.length, detail: "Health check timeout" });
2395
+ }, 2e3);
2396
+ pooled.worker.on("message", onMessage);
2397
+ pooled.worker.postMessage({ type: "ping" });
2398
+ });
2399
+ } catch (err) {
2400
+ return { status: "error", workers: this.pool.length, detail: err.message };
2401
+ }
2402
+ }
2403
+ };
2404
+
2405
+ // src/executors/isolate.executor.ts
2406
+ import ivm from "isolated-vm";
2407
+ var IsolateExecutor = class {
2408
+ logger;
2409
+ gatewayService;
2410
+ constructor(logger, gatewayService) {
2411
+ this.logger = logger;
2412
+ this.gatewayService = gatewayService;
2413
+ }
2414
+ async execute(code, limits, context, config) {
2415
+ const logs = [];
2416
+ const errors = [];
2417
+ let isolate = null;
2418
+ try {
2419
+ isolate = new ivm.Isolate({ memoryLimit: limits.memoryLimitMb });
2420
+ const ctx = await isolate.createContext();
2421
+ const jail = ctx.global;
2422
+ let currentLogBytes = 0;
2423
+ let currentErrorBytes = 0;
2424
+ await jail.set("__log", new ivm.Callback((msg) => {
2425
+ if (currentLogBytes + msg.length + 1 > limits.maxOutputBytes) {
2426
+ throw new Error("[LIMIT_LOG]");
2427
+ }
2428
+ if (currentLogBytes < limits.maxOutputBytes) {
2429
+ logs.push(msg);
2430
+ currentLogBytes += msg.length + 1;
2431
+ }
2432
+ }));
2433
+ await jail.set("__error", new ivm.Callback((msg) => {
2434
+ if (currentErrorBytes + msg.length + 1 > limits.maxOutputBytes) {
2435
+ throw new Error("[LIMIT_OUTPUT]");
2436
+ }
2437
+ if (currentErrorBytes < limits.maxOutputBytes) {
2438
+ errors.push(msg);
2439
+ currentErrorBytes += msg.length + 1;
2440
+ }
2441
+ }));
2442
+ let requestIdCounter = 0;
2443
+ const pendingToolCalls = /* @__PURE__ */ new Map();
2444
+ await jail.set("__dispatchToolCall", new ivm.Callback((nameStr, argsStr) => {
2445
+ const requestId = ++requestIdCounter;
2446
+ const name = nameStr;
2447
+ let args = {};
2448
+ try {
2449
+ args = JSON.parse(argsStr);
2450
+ } catch (e) {
2451
+ }
2452
+ this.gatewayService.callTool(name, args, context).then((res) => {
2453
+ return ctx.evalClosure(`resolveRequest($0, $1, null)`, [requestId, JSON.stringify(res)], { arguments: { copy: true } });
2454
+ }).catch((err) => {
2455
+ return ctx.evalClosure(`resolveRequest($0, null, $1)`, [requestId, err.message || "Unknown error"], { arguments: { copy: true } });
2456
+ }).catch((e) => {
2457
+ });
2458
+ return requestId;
2459
+ }));
2460
+ const bootstrap = `
2461
+ const requests = new Map();
2462
+
2463
+ // Host calls this to resolve requests
2464
+ globalThis.resolveRequest = (id, resultJson, error) => {
2465
+ const req = requests.get(id);
2466
+ if (req) {
2467
+ requests.delete(id);
2468
+ if (error) req.reject(new Error(error));
2469
+ else req.resolve(resultJson);
2470
+ }
2471
+ };
2472
+
2473
+ // Internal tool call wrapper
2474
+ globalThis.__callTool = (name, argsJson) => {
2475
+ return new Promise((resolve, reject) => {
2476
+ const id = __dispatchToolCall(name, argsJson);
2477
+ requests.set(id, { resolve, reject });
2478
+ });
2479
+ };
2480
+
2481
+ const format = (arg) => {
2482
+ if (typeof arg === 'string') return arg;
2483
+ if (arg instanceof Error) return arg.stack || arg.message;
2484
+ if (typeof arg === 'object' && arg !== null && arg.message && arg.stack) return arg.stack; // Duck typing
2485
+ return JSON.stringify(arg);
2486
+ };
2487
+ const console = {
2488
+ log: (...args) => __log(args.map(format).join(' ')),
2489
+ error: (...args) => __error(args.map(format).join(' ')),
2490
+ };
2491
+ `;
2492
+ const bootstrapScript = await isolate.compileScript(bootstrap);
2493
+ await bootstrapScript.run(ctx, { timeout: 1e3 });
2494
+ const sdkScript = config?.sdkCode || `
2495
+ const tools = {
2496
+ $raw: async (name, args) => {
2497
+ const resStr = await __callTool(name, JSON.stringify(args || {}));
2498
+ return JSON.parse(resStr);
2499
+ }
2500
+ };
2501
+ `;
2502
+ const compiledSdk = await isolate.compileScript(sdkScript);
2503
+ await compiledSdk.run(ctx, { timeout: 1e3 });
2504
+ let executionPromiseResolve;
2505
+ const executionPromise = new Promise((resolve) => {
2506
+ executionPromiseResolve = resolve;
2507
+ });
2508
+ await jail.set("__done", new ivm.Callback(() => {
2509
+ if (executionPromiseResolve) executionPromiseResolve();
2510
+ }));
2511
+ let scriptFailed = false;
2512
+ await jail.set("__setFailed", new ivm.Callback(() => {
2513
+ scriptFailed = true;
2514
+ }));
2515
+ const wrappedCode = `void (async () => {
2516
+ try {
2517
+ ${code}
2518
+ } catch (err) {
2519
+ console.error(err);
2520
+ __setFailed();
2521
+ } finally {
2522
+ __done();
2523
+ }
2524
+ })()`;
2525
+ const script = await isolate.compileScript(wrappedCode);
2526
+ await script.run(ctx, { timeout: limits.timeoutMs });
2527
+ let timedOut = false;
2528
+ const timeoutPromise = new Promise((_, reject) => {
2529
+ setTimeout(() => {
2530
+ timedOut = true;
2531
+ reject(new Error("Script execution timed out"));
2532
+ }, limits.timeoutMs);
2533
+ });
2534
+ try {
2535
+ await Promise.race([executionPromise, timeoutPromise]);
2536
+ } catch (err) {
2537
+ if (err.message === "Script execution timed out") {
2538
+ return {
2539
+ stdout: logs.join("\n"),
2540
+ stderr: errors.join("\n"),
2541
+ exitCode: null,
2542
+ error: {
2543
+ code: -32008 /* RequestTimeout */,
2544
+ message: "Execution timed out"
2545
+ }
2546
+ };
2547
+ }
2548
+ throw err;
2549
+ }
2550
+ return {
2551
+ stdout: logs.join("\n"),
2552
+ stderr: errors.join("\n"),
2553
+ exitCode: scriptFailed ? 1 : 0
2554
+ };
2555
+ } catch (err) {
2556
+ const message = err.message || "Unknown error";
2557
+ if (message.includes("Script execution timed out")) {
2558
+ return {
2559
+ stdout: logs.join("\n"),
2560
+ stderr: errors.join("\n"),
2561
+ exitCode: null,
2562
+ error: {
2563
+ code: -32008 /* RequestTimeout */,
2564
+ message: "Execution timed out"
2565
+ }
2566
+ };
2567
+ }
2568
+ if (message.includes("memory limit") || message.includes("disposed")) {
2569
+ return {
2570
+ stdout: logs.join("\n"),
2571
+ stderr: errors.join("\n"),
2572
+ exitCode: null,
2573
+ error: {
2574
+ code: -32009 /* MemoryLimitExceeded */,
2575
+ message: "Memory limit exceeded"
2576
+ }
2577
+ };
2578
+ }
2579
+ this.logger.error({ err }, "Isolate execution failed");
2580
+ return {
2581
+ stdout: logs.join("\n"),
2582
+ stderr: message,
2583
+ exitCode: 1,
2584
+ error: {
2585
+ code: -32603 /* InternalError */,
2586
+ message
2587
+ }
2588
+ };
2589
+ } finally {
2590
+ if (isolate) {
2591
+ isolate.dispose();
2592
+ }
2593
+ }
2594
+ }
2595
+ async shutdown() {
2596
+ }
2597
+ async healthCheck() {
2598
+ try {
2599
+ const isolate = new ivm.Isolate({ memoryLimit: 8 });
2600
+ isolate.dispose();
2601
+ return { status: "ok" };
2602
+ } catch (err) {
2603
+ return { status: "error", detail: err.message };
2604
+ }
2605
+ }
2606
+ async warmup() {
2607
+ }
2608
+ };
2609
+
2610
+ // src/core/registries/executor.registry.ts
2611
+ var ExecutorRegistry = class {
2612
+ executors = /* @__PURE__ */ new Map();
2613
+ register(name, executor) {
2614
+ this.executors.set(name, executor);
2615
+ }
2616
+ get(name) {
2617
+ return this.executors.get(name);
2618
+ }
2619
+ has(name) {
2620
+ return this.executors.has(name);
2621
+ }
2622
+ async shutdownAll() {
2623
+ for (const executor of this.executors.values()) {
2624
+ if (executor.shutdown) {
2625
+ await executor.shutdown();
2626
+ }
2627
+ }
2628
+ this.executors.clear();
2629
+ }
2630
+ };
2631
+
2632
+ // src/sdk/tool-binding.ts
2633
+ function parseToolName(qualifiedName) {
2634
+ const separatorIndex = qualifiedName.indexOf("__");
2635
+ if (separatorIndex === -1) {
2636
+ return { namespace: "", name: qualifiedName };
2637
+ }
2638
+ return {
2639
+ namespace: qualifiedName.substring(0, separatorIndex),
2640
+ name: qualifiedName.substring(separatorIndex + 2)
2641
+ };
2642
+ }
2643
+ function toToolBinding(name, inputSchema, description) {
2644
+ const toolId = parseToolName(name);
2645
+ return {
2646
+ name,
2647
+ namespace: toolId.namespace || "default",
2648
+ methodName: toolId.name || name,
2649
+ inputSchema,
2650
+ description
2651
+ };
2652
+ }
2653
+ function groupByNamespace(bindings) {
2654
+ const groups = /* @__PURE__ */ new Map();
2655
+ for (const binding of bindings) {
2656
+ const existing = groups.get(binding.namespace) || [];
2657
+ existing.push(binding);
2658
+ groups.set(binding.namespace, existing);
2659
+ }
2660
+ return groups;
2661
+ }
2662
+
2663
+ // src/sdk/sdk-generator.ts
2664
+ var SDKGenerator = class {
2665
+ /**
2666
+ * Convert camelCase to snake_case for Python
2667
+ */
2668
+ toSnakeCase(str) {
2669
+ return str.replace(/([A-Z])/g, "_$1").toLowerCase().replace(/^_/, "");
2670
+ }
2671
+ /**
2672
+ * Escape a string for use in generated code
2673
+ */
2674
+ escapeString(str) {
2675
+ return str.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/\n/g, "\\n");
2676
+ }
2677
+ /**
2678
+ * Generate TypeScript SDK code to be injected into Deno sandbox.
2679
+ * Creates: tools.namespace.method(args) => __internalCallTool("namespace__method", args)
2680
+ * @param bindings Tool bindings to generate SDK for
2681
+ * @param allowedTools Optional allowlist for $raw() enforcement
2682
+ * @param enableRawFallback Enable $raw() escape hatch (default: true)
2683
+ */
2684
+ generateTypeScript(bindings, allowedTools, enableRawFallback = true) {
2685
+ const grouped = groupByNamespace(bindings);
2686
+ const lines = [];
2687
+ lines.push("// Generated SDK - Do not edit");
2688
+ if (allowedTools && allowedTools.length > 0) {
2689
+ const normalizedList = allowedTools.map((t) => t.replace(/\./g, "__"));
2690
+ lines.push(`const __allowedTools = ${JSON.stringify(normalizedList)};`);
2691
+ } else {
2692
+ lines.push("const __allowedTools = null;");
2693
+ }
2694
+ lines.push("const tools = {");
2695
+ for (const [namespace, tools] of grouped.entries()) {
2696
+ const safeNamespace = this.isValidIdentifier(namespace) ? namespace : `["${this.escapeString(namespace)}"]`;
2697
+ if (this.isValidIdentifier(namespace)) {
2698
+ lines.push(` ${namespace}: {`);
2699
+ } else {
2700
+ lines.push(` "${this.escapeString(namespace)}": {`);
2701
+ }
2702
+ for (const tool of tools) {
2703
+ const methodName = this.isValidIdentifier(tool.methodName) ? tool.methodName : `["${this.escapeString(tool.methodName)}"]`;
2704
+ if (tool.description) {
2705
+ lines.push(` /** ${this.escapeString(tool.description)} */`);
2706
+ }
2707
+ if (this.isValidIdentifier(tool.methodName)) {
2708
+ lines.push(` async ${tool.methodName}(args) {`);
2709
+ } else {
2710
+ lines.push(` "${this.escapeString(tool.methodName)}": async function(args) {`);
2711
+ }
2712
+ lines.push(` return await __internalCallTool("${this.escapeString(tool.name)}", args);`);
2713
+ lines.push(` },`);
2714
+ }
2715
+ lines.push(` },`);
2716
+ }
2717
+ if (enableRawFallback) {
2718
+ lines.push(` /** Call a tool by its full name (escape hatch for dynamic/unknown tools) */`);
2719
+ lines.push(` async $raw(name, args) {`);
2720
+ lines.push(` const normalized = name.replace(/\\./g, '__');`);
2721
+ lines.push(` if (__allowedTools) {`);
2722
+ lines.push(` const allowed = __allowedTools.some(p => {`);
2723
+ lines.push(` if (p.endsWith('__*')) return normalized.startsWith(p.slice(0, -1));`);
2724
+ lines.push(` return normalized === p;`);
2725
+ lines.push(` });`);
2726
+ lines.push(` if (!allowed) throw new Error(\`Tool \${name} is not in the allowlist\`);`);
2727
+ lines.push(` }`);
2728
+ lines.push(` return await __internalCallTool(normalized, args);`);
2729
+ lines.push(` },`);
2730
+ }
2731
+ lines.push("};");
2732
+ lines.push("(globalThis as any).tools = tools;");
2733
+ return lines.join("\n");
2734
+ }
2735
+ /**
2736
+ * Generate Python SDK code to be injected into Pyodide sandbox.
2737
+ * Creates: tools.namespace.method(args) => _internal_call_tool("namespace__method", args)
2738
+ * @param bindings Tool bindings to generate SDK for
2739
+ * @param allowedTools Optional allowlist for raw() enforcement
2740
+ * @param enableRawFallback Enable raw() escape hatch (default: true)
2741
+ */
2742
+ generatePython(bindings, allowedTools, enableRawFallback = true) {
2743
+ const grouped = groupByNamespace(bindings);
2744
+ const lines = [];
2745
+ lines.push("# Generated SDK - Do not edit");
2746
+ if (allowedTools && allowedTools.length > 0) {
2747
+ const normalizedList = allowedTools.map((t) => t.replace(/\./g, "__"));
2748
+ lines.push(`_allowed_tools = ${JSON.stringify(normalizedList)}`);
2749
+ } else {
2750
+ lines.push("_allowed_tools = None");
2751
+ }
2752
+ lines.push("");
2753
+ lines.push("class _ToolNamespace:");
2754
+ lines.push(" def __init__(self, methods):");
2755
+ lines.push(" for name, fn in methods.items():");
2756
+ lines.push(" setattr(self, name, fn)");
2757
+ lines.push("");
2758
+ lines.push("class _Tools:");
2759
+ lines.push(" def __init__(self):");
2760
+ for (const [namespace, tools] of grouped.entries()) {
2761
+ const safeNamespace = this.toSnakeCase(namespace);
2762
+ const methodsDict = [];
2763
+ for (const tool of tools) {
2764
+ const methodName = this.toSnakeCase(tool.methodName);
2765
+ const fullName = tool.name;
2766
+ methodsDict.push(` "${methodName}": lambda args, n="${this.escapeString(fullName)}": _internal_call_tool(n, args)`);
2767
+ }
2768
+ lines.push(` self.${safeNamespace} = _ToolNamespace({`);
2769
+ lines.push(methodsDict.join(",\n"));
2770
+ lines.push(` })`);
2771
+ }
2772
+ if (enableRawFallback) {
2773
+ lines.push("");
2774
+ lines.push(" async def raw(self, name, args):");
2775
+ lines.push(' """Call a tool by its full name (escape hatch for dynamic/unknown tools)"""');
2776
+ lines.push(' normalized = name.replace(".", "__")');
2777
+ lines.push(" if _allowed_tools is not None:");
2778
+ lines.push(" allowed = any(");
2779
+ lines.push(' normalized.startswith(p[:-1]) if p.endswith("__*") else normalized == p');
2780
+ lines.push(" for p in _allowed_tools");
2781
+ lines.push(" )");
2782
+ lines.push(" if not allowed:");
2783
+ lines.push(' raise PermissionError(f"Tool {name} is not in the allowlist")');
2784
+ lines.push(" return await _internal_call_tool(normalized, args)");
2785
+ }
2786
+ lines.push("");
2787
+ lines.push("tools = _Tools()");
2788
+ return lines.join("\n");
2789
+ }
2790
+ /**
2791
+ * Generate JavaScript SDK code for isolated-vm (V8 Isolate).
2792
+ * Creates: tools.namespace.method(args) => __callToolSync("namespace__method", JSON.stringify(args))
2793
+ * @param bindings Tool bindings to generate SDK for
2794
+ * @param allowedTools Optional allowlist for $raw() enforcement
2795
+ * @param enableRawFallback Enable $raw() escape hatch (default: true)
2796
+ */
2797
+ generateIsolateSDK(bindings, allowedTools, enableRawFallback = true) {
2798
+ const grouped = groupByNamespace(bindings);
2799
+ const lines = [];
2800
+ lines.push("// Generated SDK for isolated-vm");
2801
+ if (allowedTools && allowedTools.length > 0) {
2802
+ const normalizedList = allowedTools.map((t) => t.replace(/\./g, "__"));
2803
+ lines.push(`const __allowedTools = ${JSON.stringify(normalizedList)};`);
2804
+ } else {
2805
+ lines.push("const __allowedTools = null;");
2806
+ }
2807
+ lines.push("const tools = {");
2808
+ for (const [namespace, tools] of grouped.entries()) {
2809
+ const safeNamespace = this.isValidIdentifier(namespace) ? namespace : `["${this.escapeString(namespace)}"]`;
2810
+ if (this.isValidIdentifier(namespace)) {
2811
+ lines.push(` ${namespace}: {`);
2812
+ } else {
2813
+ lines.push(` "${this.escapeString(namespace)}": {`);
2814
+ }
2815
+ for (const tool of tools) {
2816
+ const methodName = this.isValidIdentifier(tool.methodName) ? tool.methodName : `["${this.escapeString(tool.methodName)}"]`;
2817
+ if (this.isValidIdentifier(tool.methodName)) {
2818
+ lines.push(` async ${methodName}(args) {`);
2819
+ } else {
2820
+ lines.push(` "${this.escapeString(tool.methodName)}": async function(args) {`);
2821
+ }
2822
+ lines.push(` const resStr = await __callTool("${this.escapeString(tool.name)}", JSON.stringify(args || {}));`);
2823
+ lines.push(` return JSON.parse(resStr);`);
2824
+ lines.push(` },`);
2825
+ }
2826
+ lines.push(` },`);
2827
+ }
2828
+ if (enableRawFallback) {
2829
+ lines.push(` async $raw(name, args) {`);
2830
+ lines.push(` const normalized = name.replace(/\\./g, '__');`);
2831
+ lines.push(` if (__allowedTools) {`);
2832
+ lines.push(` const allowed = __allowedTools.some(p => {`);
2833
+ lines.push(` if (p.endsWith('__*')) return normalized.startsWith(p.slice(0, -1));`);
2834
+ lines.push(` return normalized === p;`);
2835
+ lines.push(` });`);
2836
+ lines.push(` if (!allowed) throw new Error(\`Tool \${name} is not in the allowlist\`);`);
2837
+ lines.push(` }`);
2838
+ lines.push(` const resStr = await __callTool(normalized, JSON.stringify(args || {}));`);
2839
+ lines.push(` return JSON.parse(resStr);`);
2840
+ lines.push(` },`);
2841
+ }
2842
+ lines.push("};");
2843
+ return lines.join("\n");
2844
+ }
2845
+ /**
2846
+ * Check if a string is a valid JavaScript/Python identifier
2847
+ */
2848
+ isValidIdentifier(str) {
2849
+ return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(str);
2850
+ }
2851
+ };
2852
+
2853
+ // src/core/execution.service.ts
2854
+ var ExecutionService = class {
2855
+ logger;
2856
+ executorRegistry;
2857
+ sdkGenerator = new SDKGenerator();
2858
+ defaultLimits;
2859
+ gatewayService;
2860
+ securityService;
2861
+ _ipcAddress = "";
2862
+ constructor(logger, defaultLimits, gatewayService, securityService, executorRegistry) {
2863
+ this.logger = logger;
2864
+ this.defaultLimits = defaultLimits;
2865
+ this.gatewayService = gatewayService;
2866
+ this.securityService = securityService;
2867
+ this.executorRegistry = executorRegistry;
2868
+ }
2869
+ set ipcAddress(addr) {
2870
+ this._ipcAddress = addr;
2871
+ }
2872
+ async executeTypeScript(code, limits, context, allowedTools) {
2873
+ const effectiveLimits = { ...this.defaultLimits, ...limits };
2874
+ const securityResult = this.securityService.validateCode(code);
2875
+ if (!securityResult.valid) {
2876
+ return this.createErrorResult(-32003 /* Forbidden */, securityResult.message || "Access denied");
2877
+ }
2878
+ const cleanCode = code.replace(/\/\*[\s\S]*?\*\/|([^:]|^)\/\/.*$/gm, "$1");
2879
+ const hasImports = /^\s*import\s/m.test(cleanCode) || /^\s*export\s/m.test(cleanCode) || /\bDeno\./.test(cleanCode) || /\bDeno\b/.test(cleanCode);
2880
+ if (!hasImports && this.executorRegistry.has("isolate")) {
2881
+ return await this.executeIsolate(code, effectiveLimits, context, allowedTools);
2882
+ }
2883
+ if (!this._ipcAddress) {
2884
+ return this.createErrorResult(-32603 /* InternalError */, "IPC address not initialized");
2885
+ }
2886
+ if (!this.executorRegistry.has("deno")) {
2887
+ return this.createErrorResult(-32603 /* InternalError */, "Deno execution not available");
2888
+ }
2889
+ const executor = this.executorRegistry.get("deno");
2890
+ const bindings = await this.getToolBindings(context);
2891
+ const sdkCode = this.sdkGenerator.generateTypeScript(bindings, allowedTools);
2892
+ const sessionToken = this.securityService.createSession(allowedTools);
2893
+ try {
2894
+ return await executor.execute(code, effectiveLimits, context, {
2895
+ ipcAddress: this._ipcAddress,
2896
+ ipcToken: sessionToken,
2897
+ sdkCode
2898
+ });
2899
+ } finally {
2900
+ this.securityService.invalidateSession(sessionToken);
2901
+ }
2902
+ }
2903
+ async executePython(code, limits, context, allowedTools) {
2904
+ const effectiveLimits = { ...this.defaultLimits, ...limits };
2905
+ if (!this.executorRegistry.has("python")) {
2906
+ return this.createErrorResult(-32603 /* InternalError */, "Python execution not available");
2907
+ }
2908
+ if (!this._ipcAddress) {
2909
+ return this.createErrorResult(-32603 /* InternalError */, "IPC address not initialized");
2910
+ }
2911
+ const executor = this.executorRegistry.get("python");
2912
+ const securityResult = this.securityService.validateCode(code);
2913
+ if (!securityResult.valid) {
2914
+ return this.createErrorResult(-32003 /* Forbidden */, securityResult.message || "Access denied");
2915
+ }
2916
+ const bindings = await this.getToolBindings(context);
2917
+ const sdkCode = this.sdkGenerator.generatePython(bindings, allowedTools);
2918
+ const sessionToken = this.securityService.createSession(allowedTools);
2919
+ try {
2920
+ return await executor.execute(code, effectiveLimits, context, {
2921
+ ipcAddress: this._ipcAddress,
2922
+ ipcToken: sessionToken,
2923
+ sdkCode
2924
+ });
2925
+ } finally {
2926
+ this.securityService.invalidateSession(sessionToken);
2927
+ }
2928
+ }
2929
+ async getToolBindings(context) {
2930
+ const packages = await this.gatewayService.listToolPackages();
2931
+ const allBindings = [];
2932
+ for (const pkg of packages) {
2933
+ try {
2934
+ const stubs = await this.gatewayService.listToolStubs(pkg.id, context);
2935
+ allBindings.push(...stubs.map((s) => toToolBinding(s.id, void 0, s.description)));
2936
+ } catch (err) {
2937
+ this.logger.warn({ packageId: pkg.id, err: err.message }, "Failed to list stubs for package");
2938
+ }
2939
+ }
2940
+ return allBindings;
2941
+ }
2942
+ async executeIsolate(code, limits, context, allowedTools) {
2943
+ if (!this.executorRegistry.has("isolate")) {
2944
+ return this.createErrorResult(-32603 /* InternalError */, "IsolateExecutor not available");
2945
+ }
2946
+ const executor = this.executorRegistry.get("isolate");
2947
+ const effectiveLimits = { ...this.defaultLimits, ...limits };
2948
+ const securityResult = this.securityService.validateCode(code);
2949
+ if (!securityResult.valid) {
2950
+ return this.createErrorResult(-32003 /* Forbidden */, securityResult.message || "Access denied");
2951
+ }
2952
+ const bindings = await this.getToolBindings(context);
2953
+ const sdkCode = this.sdkGenerator.generateIsolateSDK(bindings, allowedTools);
2954
+ try {
2955
+ return await executor.execute(code, effectiveLimits, context, { sdkCode });
2956
+ } catch (err) {
2957
+ return this.createErrorResult(-32603 /* InternalError */, err.message);
2958
+ }
2959
+ }
2960
+ createErrorResult(code, message) {
2961
+ return {
2962
+ stdout: "",
2963
+ stderr: "",
2964
+ exitCode: null,
2965
+ error: { code, message }
2966
+ };
2967
+ }
2968
+ async shutdown() {
2969
+ await this.executorRegistry.shutdownAll();
2970
+ }
2971
+ async warmup() {
2972
+ const pythonExecutor = this.executorRegistry.get("python");
2973
+ if (pythonExecutor && "warmup" in pythonExecutor) {
2974
+ await pythonExecutor.warmup(this.defaultLimits);
2975
+ }
2976
+ }
2977
+ async healthCheck() {
2978
+ const pythonExecutor = this.executorRegistry.get("python");
2979
+ if (pythonExecutor && "healthCheck" in pythonExecutor) {
2980
+ return pythonExecutor.healthCheck();
2981
+ }
2982
+ return { status: "ok" };
2983
+ }
2984
+ };
2985
+
2986
+ // src/core/middleware/error.middleware.ts
2987
+ var ErrorHandlingMiddleware = class {
2988
+ async handle(request, context, next) {
2989
+ try {
2990
+ return await next();
2991
+ } catch (err) {
2992
+ context.logger.error({ err }, "Error handling request");
2993
+ return {
2994
+ jsonrpc: "2.0",
2995
+ id: request.id,
2996
+ error: {
2997
+ code: -32603 /* InternalError */,
2998
+ message: err.message || "Internal Server Error"
2999
+ }
3000
+ };
3001
+ }
3002
+ }
3003
+ };
3004
+
3005
+ // src/core/middleware/logging.middleware.ts
3006
+ var LoggingMiddleware = class {
3007
+ async handle(request, context, next) {
3008
+ const { method, id } = request;
3009
+ const childLogger = context.logger.child({ method, id });
3010
+ context.logger = childLogger;
3011
+ metrics.recordExecutionStart();
3012
+ const startTime = Date.now();
3013
+ try {
3014
+ const response = await next();
3015
+ metrics.recordExecutionEnd(Date.now() - startTime, method);
3016
+ return response;
3017
+ } catch (err) {
3018
+ metrics.recordExecutionEnd(Date.now() - startTime, method);
3019
+ throw err;
3020
+ }
3021
+ }
3022
+ };
3023
+
3024
+ // src/core/middleware/auth.middleware.ts
3025
+ var AuthMiddleware = class {
3026
+ constructor(securityService) {
3027
+ this.securityService = securityService;
3028
+ }
3029
+ async handle(request, context, next) {
3030
+ const providedToken = request.auth?.bearerToken || "";
3031
+ const masterToken = this.securityService.getIpcToken();
3032
+ const isMaster = !masterToken || providedToken === masterToken;
3033
+ const isSession = !isMaster && this.securityService.validateIpcToken(providedToken);
3034
+ if (!isMaster && !isSession) {
3035
+ return {
3036
+ jsonrpc: "2.0",
3037
+ id: request.id,
3038
+ error: {
3039
+ code: -32003 /* Forbidden */,
3040
+ message: "Invalid bearer token"
3041
+ }
3042
+ };
3043
+ }
3044
+ if (isSession) {
3045
+ const allowedMethods = ["initialize", "notifications/initialized", "mcp_discover_tools", "mcp_call_tool", "ping", "tools/list", "tools/call"];
3046
+ if (!allowedMethods.includes(request.method)) {
3047
+ return {
3048
+ jsonrpc: "2.0",
3049
+ id: request.id,
3050
+ error: {
3051
+ code: -32003 /* Forbidden */,
3052
+ message: "Session tokens are restricted to tool discovery and calling only"
3053
+ }
3054
+ };
3055
+ }
3056
+ const session = this.securityService.getSession(providedToken);
3057
+ if (session?.allowedTools && !context.allowedTools) {
3058
+ context.allowedTools = session.allowedTools;
3059
+ }
3060
+ }
3061
+ return next();
3062
+ }
3063
+ };
3064
+
3065
+ // src/core/middleware/ratelimit.middleware.ts
3066
+ var RateLimitMiddleware = class {
3067
+ constructor(securityService) {
3068
+ this.securityService = securityService;
3069
+ }
3070
+ async handle(request, context, next) {
3071
+ const providedToken = request.auth?.bearerToken;
3072
+ const rateLimitKey = providedToken || context.remoteAddress || "unknown";
3073
+ if (!this.securityService.checkRateLimit(rateLimitKey)) {
3074
+ return {
3075
+ jsonrpc: "2.0",
3076
+ id: request.id,
3077
+ error: {
3078
+ code: -32005,
3079
+ // Rate limit exceeded code
3080
+ message: "Rate limit exceeded"
3081
+ }
3082
+ };
3083
+ }
3084
+ return next();
3085
+ }
3086
+ };
3087
+
3088
+ // src/core/middleware/middleware.builder.ts
3089
+ function buildDefaultMiddleware(securityService) {
3090
+ return [
3091
+ new ErrorHandlingMiddleware(),
3092
+ new LoggingMiddleware(),
3093
+ new AuthMiddleware(securityService),
3094
+ new RateLimitMiddleware(securityService)
3095
+ ];
3096
+ }
3097
+
3098
+ // src/auth.cmd.ts
3099
+ import Fastify2 from "fastify";
3100
+ import axios4 from "axios";
3101
+ import open from "open";
3102
+ import { v4 as uuidv43 } from "uuid";
3103
+ async function handleAuth(options) {
3104
+ const port = options.port || 3333;
3105
+ const redirectUri = `http://localhost:${port}/callback`;
3106
+ const state = uuidv43();
3107
+ const fastify = Fastify2();
3108
+ return new Promise((resolve, reject) => {
3109
+ fastify.get("/callback", async (request, reply) => {
3110
+ const { code, state: returnedState, error, error_description } = request.query;
3111
+ if (error) {
3112
+ reply.send(`Authentication failed: ${error} - ${error_description}`);
3113
+ reject(new Error(`OAuth error: ${error}`));
3114
+ return;
3115
+ }
3116
+ if (returnedState !== state) {
3117
+ reply.send("Invalid state parameter");
3118
+ reject(new Error("State mismatch"));
3119
+ return;
3120
+ }
3121
+ try {
3122
+ const response = await axios4.post(options.tokenUrl, {
3123
+ grant_type: "authorization_code",
3124
+ code,
3125
+ redirect_uri: redirectUri,
3126
+ client_id: options.clientId,
3127
+ client_secret: options.clientSecret
3128
+ });
3129
+ const { refresh_token, access_token } = response.data;
3130
+ console.log("\n--- Authentication Successful ---\n");
3131
+ console.log("Use these values in your conduit.yaml:\n");
3132
+ console.log("credentials:");
3133
+ console.log(" type: oauth2");
3134
+ console.log(` clientId: ${options.clientId}`);
3135
+ console.log(` clientSecret: ${options.clientSecret}`);
3136
+ console.log(` tokenUrl: "${options.tokenUrl}"`);
3137
+ console.log(` refreshToken: "${refresh_token || "N/A (No refresh token returned)"}"`);
3138
+ if (!refresh_token) {
3139
+ console.log('\nWarning: No refresh token was returned. Ensure your app has "offline_access" scope or similar.');
3140
+ }
3141
+ console.log("\nRaw response data:", JSON.stringify(response.data, null, 2));
3142
+ reply.send("Authentication successful! You can close this window and return to the terminal.");
3143
+ resolve();
3144
+ } catch (err) {
3145
+ const msg = err.response?.data?.error_description || err.response?.data?.error || err.message;
3146
+ reply.send(`Failed to exchange code for token: ${msg}`);
3147
+ reject(new Error(`Token exchange failed: ${msg}`));
3148
+ } finally {
3149
+ setTimeout(() => fastify.close(), 1e3);
3150
+ }
3151
+ });
3152
+ fastify.listen({ port, host: "127.0.0.1" }, async (err) => {
3153
+ if (err) {
3154
+ reject(err);
3155
+ return;
3156
+ }
3157
+ const authUrl = new URL(options.authUrl);
3158
+ authUrl.searchParams.append("client_id", options.clientId);
3159
+ authUrl.searchParams.append("redirect_uri", redirectUri);
3160
+ authUrl.searchParams.append("response_type", "code");
3161
+ authUrl.searchParams.append("state", state);
3162
+ if (options.scopes) {
3163
+ authUrl.searchParams.append("scope", options.scopes);
3164
+ }
3165
+ console.log(`Opening browser to: ${authUrl.toString()}`);
3166
+ console.log("Waiting for callback...");
3167
+ await open(authUrl.toString());
3168
+ });
3169
+ });
3170
+ }
3171
+
3172
+ // src/index.ts
3173
+ var program = new Command();
3174
+ program.name("conduit").description("A secure Code Mode execution substrate for MCP agents").version("1.0.0");
3175
+ program.command("serve", { isDefault: true }).description("Start the Conduit server").option("--stdio", "Use stdio transport").action(async (options) => {
3176
+ try {
3177
+ await startServer();
3178
+ } catch (err) {
3179
+ console.error("Failed to start Conduit:", err);
3180
+ process.exit(1);
3181
+ }
3182
+ });
3183
+ program.command("auth").description("Help set up OAuth for an upstream MCP server").requiredOption("--client-id <id>", "OAuth Client ID").requiredOption("--client-secret <secret>", "OAuth Client Secret").requiredOption("--auth-url <url>", "OAuth Authorization URL").requiredOption("--token-url <url>", "OAuth Token URL").option("--scopes <scopes>", "OAuth Scopes (comma separated)").option("--port <port>", "Port for the local callback server", "3333").action(async (options) => {
3184
+ try {
3185
+ await handleAuth({
3186
+ clientId: options.clientId,
3187
+ clientSecret: options.clientSecret,
3188
+ authUrl: options.authUrl,
3189
+ tokenUrl: options.tokenUrl,
3190
+ scopes: options.scopes,
3191
+ port: parseInt(options.port, 10)
3192
+ });
3193
+ console.log("\nSuccess! Configuration generated.");
3194
+ } catch (err) {
3195
+ console.error("Authentication helper failed:", err.message);
3196
+ process.exit(1);
3197
+ }
3198
+ });
3199
+ async function startServer() {
3200
+ const configService = new ConfigService();
3201
+ const logger = createLogger(configService);
3202
+ const otelService = new OtelService(logger);
3203
+ await otelService.start();
3204
+ await loggerStorage.run({ correlationId: "system" }, async () => {
3205
+ const isStdio = configService.get("transport") === "stdio";
3206
+ const ipcToken = isStdio ? void 0 : configService.get("ipcBearerToken");
3207
+ const securityService = new SecurityService(logger, ipcToken);
3208
+ const gatewayService = new GatewayService(logger, securityService);
3209
+ const upstreams = configService.get("upstreams") || [];
3210
+ for (const upstream of upstreams) {
3211
+ gatewayService.registerUpstream(upstream);
3212
+ }
3213
+ const executorRegistry = new ExecutorRegistry();
3214
+ executorRegistry.register("deno", new DenoExecutor(configService.get("denoMaxPoolSize")));
3215
+ executorRegistry.register("python", new PyodideExecutor(configService.get("pyodideMaxPoolSize")));
3216
+ const isolateExecutor = new IsolateExecutor(logger, gatewayService);
3217
+ executorRegistry.register("isolate", isolateExecutor);
3218
+ const executionService = new ExecutionService(
3219
+ logger,
3220
+ configService.get("resourceLimits"),
3221
+ gatewayService,
3222
+ securityService,
3223
+ executorRegistry
3224
+ );
3225
+ const requestController = new RequestController(
3226
+ logger,
3227
+ executionService,
3228
+ gatewayService,
3229
+ buildDefaultMiddleware(securityService)
3230
+ );
3231
+ const opsServer = new OpsServer(logger, configService.all, gatewayService, requestController);
3232
+ await opsServer.listen();
3233
+ const concurrencyService = new ConcurrencyService(logger, {
3234
+ maxConcurrent: configService.get("maxConcurrent")
3235
+ });
3236
+ let transport;
3237
+ let address;
3238
+ if (configService.get("transport") === "stdio") {
3239
+ transport = new StdioTransport(logger, requestController, concurrencyService);
3240
+ await transport.start();
3241
+ address = "stdio";
3242
+ } else {
3243
+ transport = new SocketTransport(logger, requestController, concurrencyService);
3244
+ const port = configService.get("port");
3245
+ address = await transport.listen({ port });
3246
+ }
3247
+ executionService.ipcAddress = address;
3248
+ await requestController.warmup();
3249
+ logger.info("Conduit server started");
3250
+ const shutdown = async () => {
3251
+ logger.info("Shutting down...");
3252
+ await Promise.all([
3253
+ transport.close(),
3254
+ opsServer.close(),
3255
+ requestController.shutdown(),
3256
+ otelService.shutdown()
3257
+ ]);
3258
+ process.exit(0);
3259
+ };
3260
+ process.on("SIGINT", shutdown);
3261
+ process.on("SIGTERM", shutdown);
3262
+ });
3263
+ }
3264
+ program.parse(process.argv);
3265
+ //# sourceMappingURL=index.js.map