@mhingston5/conduit 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/.env.example +13 -0
  2. package/.github/workflows/ci.yml +88 -0
  3. package/.github/workflows/pr-checks.yml +90 -0
  4. package/.tool-versions +2 -0
  5. package/README.md +177 -0
  6. package/conduit.yaml.test +3 -0
  7. package/docs/ARCHITECTURE.md +35 -0
  8. package/docs/CODE_MODE.md +33 -0
  9. package/docs/SECURITY.md +52 -0
  10. package/logo.png +0 -0
  11. package/package.json +74 -0
  12. package/src/assets/deno-shim.ts +93 -0
  13. package/src/assets/python-shim.py +21 -0
  14. package/src/core/asset.utils.ts +42 -0
  15. package/src/core/concurrency.service.ts +70 -0
  16. package/src/core/config.service.ts +147 -0
  17. package/src/core/execution.context.ts +37 -0
  18. package/src/core/execution.service.ts +209 -0
  19. package/src/core/interfaces/app.config.ts +17 -0
  20. package/src/core/interfaces/executor.interface.ts +31 -0
  21. package/src/core/interfaces/middleware.interface.ts +12 -0
  22. package/src/core/interfaces/url.validator.interface.ts +3 -0
  23. package/src/core/logger.ts +64 -0
  24. package/src/core/metrics.service.ts +112 -0
  25. package/src/core/middleware/auth.middleware.ts +56 -0
  26. package/src/core/middleware/error.middleware.ts +21 -0
  27. package/src/core/middleware/logging.middleware.ts +25 -0
  28. package/src/core/middleware/middleware.builder.ts +24 -0
  29. package/src/core/middleware/ratelimit.middleware.ts +31 -0
  30. package/src/core/network.policy.service.ts +106 -0
  31. package/src/core/ops.server.ts +74 -0
  32. package/src/core/otel.service.ts +41 -0
  33. package/src/core/policy.service.ts +77 -0
  34. package/src/core/registries/executor.registry.ts +26 -0
  35. package/src/core/request.controller.ts +297 -0
  36. package/src/core/security.service.ts +68 -0
  37. package/src/core/session.manager.ts +44 -0
  38. package/src/core/types.ts +47 -0
  39. package/src/executors/deno.executor.ts +342 -0
  40. package/src/executors/isolate.executor.ts +281 -0
  41. package/src/executors/pyodide.executor.ts +327 -0
  42. package/src/executors/pyodide.worker.ts +195 -0
  43. package/src/gateway/auth.service.ts +104 -0
  44. package/src/gateway/gateway.service.ts +345 -0
  45. package/src/gateway/schema.cache.ts +46 -0
  46. package/src/gateway/upstream.client.ts +244 -0
  47. package/src/index.ts +92 -0
  48. package/src/sdk/index.ts +2 -0
  49. package/src/sdk/sdk-generator.ts +245 -0
  50. package/src/sdk/tool-binding.ts +86 -0
  51. package/src/transport/socket.transport.ts +203 -0
  52. package/tests/__snapshots__/assets.test.ts.snap +97 -0
  53. package/tests/assets.test.ts +50 -0
  54. package/tests/auth.service.test.ts +78 -0
  55. package/tests/code-mode-lite-execution.test.ts +84 -0
  56. package/tests/code-mode-lite-gateway.test.ts +150 -0
  57. package/tests/concurrency.service.test.ts +50 -0
  58. package/tests/concurrency.test.ts +41 -0
  59. package/tests/config.service.test.ts +70 -0
  60. package/tests/contract.test.ts +43 -0
  61. package/tests/deno.executor.test.ts +68 -0
  62. package/tests/deno_hardening.test.ts +45 -0
  63. package/tests/dynamic.tool.test.ts +237 -0
  64. package/tests/e2e_stdio_upstream.test.ts +197 -0
  65. package/tests/fixtures/stdio-server.ts +42 -0
  66. package/tests/gateway.manifest.test.ts +82 -0
  67. package/tests/gateway.service.test.ts +58 -0
  68. package/tests/gateway.strict.unit.test.ts +74 -0
  69. package/tests/gateway.validation.unit.test.ts +89 -0
  70. package/tests/gateway_validation.test.ts +86 -0
  71. package/tests/hardening.test.ts +139 -0
  72. package/tests/hardening_v1.test.ts +72 -0
  73. package/tests/isolate.executor.test.ts +100 -0
  74. package/tests/log-limit.test.ts +55 -0
  75. package/tests/middleware.test.ts +106 -0
  76. package/tests/ops.server.test.ts +65 -0
  77. package/tests/policy.service.test.ts +90 -0
  78. package/tests/pyodide.executor.test.ts +101 -0
  79. package/tests/reference_mcp.ts +40 -0
  80. package/tests/remediation.test.ts +119 -0
  81. package/tests/routing.test.ts +148 -0
  82. package/tests/schema.cache.test.ts +27 -0
  83. package/tests/sdk/sdk-generator.test.ts +205 -0
  84. package/tests/socket.transport.test.ts +182 -0
  85. package/tests/stdio_upstream.test.ts +54 -0
  86. package/tsconfig.json +25 -0
  87. package/tsup.config.ts +22 -0
@@ -0,0 +1,42 @@
1
+ import path from 'node:path';
2
+ import fs from 'node:fs';
3
+ import { fileURLToPath } from 'node:url';
4
+
5
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
6
+
7
+ /**
8
+ * Resolves the absolute path to an asset file, handling both source (dev)
9
+ * and distribution (prod) directory structures.
10
+ *
11
+ * Strategies:
12
+ * 1. ../assets/{filename} (Source: src/core -> src/assets)
13
+ * 2. ./assets/{filename} (Dist: dist/ -> dist/assets, if core is merged or similar)
14
+ * 3. ../../assets/{filename} (Dist: dist/core -> dist/assets)
15
+ * 4. assets/{filename} (Relative to cwd, unlikely but fallback)
16
+ *
17
+ * @param filename The name of the asset file (e.g., 'deno-shim.ts')
18
+ * @returns The absolute path to the asset if found
19
+ * @throws Error if the asset cannot be found
20
+ */
21
+ export function resolveAssetPath(filename: string): string {
22
+ const candidates = [
23
+ // Source structure: src/core/asset.utils.ts -> src/assets/
24
+ path.resolve(__dirname, '../assets', filename),
25
+ // Dist structure possibility 1: dist/ (flat) with assets/ subdir
26
+ path.resolve(__dirname, './assets', filename),
27
+ // Dist structure possibility 2: dist/core/ -> dist/assets/
28
+ path.resolve(__dirname, '../../assets', filename),
29
+ // Dist structure possibility 3: dist/ -> assets/ (if called from root)
30
+ path.resolve(process.cwd(), 'assets', filename),
31
+ // Dist structure possibility 4: dist/assets/ (from root)
32
+ path.resolve(process.cwd(), 'dist/assets', filename)
33
+ ];
34
+
35
+ for (const candidate of candidates) {
36
+ if (fs.existsSync(candidate)) {
37
+ return candidate;
38
+ }
39
+ }
40
+
41
+ throw new Error(`Asset not found: ${filename}. Checked paths: ${candidates.join(', ')}`);
42
+ }
@@ -0,0 +1,70 @@
1
+ import pLimit from 'p-limit';
2
+ import { Logger } from 'pino';
3
+ import { trace } from '@opentelemetry/api';
4
+ import { metrics } from './metrics.service.js';
5
+
6
+ export interface ConcurrencyOptions {
7
+ maxConcurrent: number;
8
+ maxQueueSize?: number;
9
+ }
10
+
11
+ export class QueueFullError extends Error {
12
+ constructor(message: string) {
13
+ super(message);
14
+ this.name = 'QueueFullError';
15
+ }
16
+ }
17
+
18
+ export class ConcurrencyService {
19
+ private limit: ReturnType<typeof pLimit>;
20
+ private logger: Logger;
21
+ private maxQueueSize: number;
22
+ private queueDepthHistogram: any; // Using explicit type locally if needed, or rely on metrics service later. Using direct OTEL for now involves refactor.
23
+ // Let's rely on internal state for rejection and let MetricsService handle reporting if possible, or add it here.
24
+ // simpler: usage of metrics service is better pattern.
25
+
26
+ constructor(logger: Logger, options: ConcurrencyOptions) {
27
+ this.logger = logger;
28
+ this.limit = pLimit(options.maxConcurrent);
29
+ this.maxQueueSize = options.maxQueueSize || 100; // Default to 100
30
+
31
+ metrics.registerQueueLengthProvider(() => this.limit.pendingCount);
32
+ }
33
+
34
+ async run<T>(fn: () => Promise<T>): Promise<T> {
35
+ if (this.limit.pendingCount >= this.maxQueueSize) {
36
+ this.logger.warn({ pending: this.limit.pendingCount, max: this.maxQueueSize }, 'Request queue full, rejecting request');
37
+ throw new QueueFullError('Server is too busy, please try again later');
38
+ }
39
+
40
+ const active = this.limit.activeCount;
41
+ const pending = this.limit.pendingCount;
42
+
43
+ this.logger.debug({ active, pending }, 'Concurrency status before task');
44
+
45
+ // Add attributes to current OTEL span if exists
46
+ const span = trace.getActiveSpan();
47
+ if (span) {
48
+ span.setAttributes({
49
+ 'concurrency.active': active,
50
+ 'concurrency.pending': pending,
51
+ });
52
+ }
53
+
54
+ try {
55
+ return await this.limit(fn);
56
+ } finally {
57
+ this.logger.debug({
58
+ active: this.limit.activeCount,
59
+ pending: this.limit.pendingCount
60
+ }, 'Concurrency status after task');
61
+ }
62
+ }
63
+
64
+ get stats() {
65
+ return {
66
+ activeCount: this.limit.activeCount,
67
+ pendingCount: this.limit.pendingCount,
68
+ };
69
+ }
70
+ }
@@ -0,0 +1,147 @@
1
+ import { z } from 'zod';
2
+ import dotenv from 'dotenv';
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import yaml from 'js-yaml';
6
+
7
+ dotenv.config();
8
+
9
+ import { AppConfig } from './interfaces/app.config.js';
10
+
11
+
12
+ export const ResourceLimitsSchema = z.object({
13
+ timeoutMs: z.number().default(30000),
14
+ memoryLimitMb: z.number().default(256),
15
+ maxOutputBytes: z.number().default(1024 * 1024), // 1MB
16
+ maxLogEntries: z.number().default(10000),
17
+ });
18
+
19
+ export const UpstreamCredentialsSchema = z.object({
20
+ type: z.enum(['oauth2', 'apikey']), // Add other types as needed
21
+ clientId: z.string().optional(),
22
+ clientSecret: z.string().optional(),
23
+ tokenUrl: z.string().optional(),
24
+ scopes: z.array(z.string()).optional(),
25
+ apiKey: z.string().optional(),
26
+ headerName: z.string().optional(),
27
+ });
28
+
29
+ export const HttpUpstreamSchema = z.object({
30
+ id: z.string(),
31
+ type: z.literal('http').optional().default('http'),
32
+ url: z.string(),
33
+ credentials: UpstreamCredentialsSchema.optional(),
34
+ });
35
+
36
+ export const StdioUpstreamSchema = z.object({
37
+ id: z.string(),
38
+ type: z.literal('stdio'),
39
+ command: z.string(),
40
+ args: z.array(z.string()).optional(),
41
+ env: z.record(z.string(), z.string()).optional(),
42
+ });
43
+
44
+ export const UpstreamInfoSchema = z.union([HttpUpstreamSchema, StdioUpstreamSchema]);
45
+
46
+ export type ResourceLimits = z.infer<typeof ResourceLimitsSchema>;
47
+
48
+ export const ConfigSchema = z.object({
49
+ port: z.union([z.string(), z.number()]).default('3000').transform((v) => Number(v)),
50
+ nodeEnv: z.enum(['development', 'production', 'test']).default('development'),
51
+ logLevel: z.enum(['debug', 'info', 'warn', 'error']).default('info'),
52
+ resourceLimits: ResourceLimitsSchema.default({
53
+ timeoutMs: 30000,
54
+ memoryLimitMb: 256,
55
+ maxOutputBytes: 1024 * 1024,
56
+ maxLogEntries: 10000,
57
+ }),
58
+ secretRedactionPatterns: z.array(z.string()).default([
59
+ '[A-Za-z0-9-_]{20,}', // Default pattern from spec
60
+ ]),
61
+ ipcBearerToken: z.string().optional().default(() => Math.random().toString(36).substring(7)),
62
+ maxConcurrent: z.number().default(10),
63
+ denoMaxPoolSize: z.number().default(10),
64
+ pyodideMaxPoolSize: z.number().default(3),
65
+ metricsUrl: z.string().default('http://127.0.0.1:9464/metrics'),
66
+ opsPort: z.number().optional(),
67
+ upstreams: z.array(UpstreamInfoSchema).default([]),
68
+ });
69
+
70
+ export type Config = z.infer<typeof ConfigSchema>;
71
+
72
+ export class ConfigService {
73
+ private config: AppConfig;
74
+
75
+ constructor(overrides: Partial<AppConfig> = {}) {
76
+ const fileConfig = this.loadConfigFile();
77
+
78
+ const envConfig = {
79
+ port: process.env.PORT,
80
+ nodeEnv: process.env.NODE_ENV,
81
+ logLevel: process.env.LOG_LEVEL,
82
+ metricsUrl: process.env.METRICS_URL,
83
+ // upstreams: process.env.UPSTREAMS ? JSON.parse(process.env.UPSTREAMS) : undefined, // Removed per user request
84
+ };
85
+
86
+ // Remove undefined keys from envConfig
87
+ Object.keys(envConfig).forEach(key => envConfig[key as keyof typeof envConfig] === undefined && delete envConfig[key as keyof typeof envConfig]);
88
+
89
+ const mergedConfig = {
90
+ ...fileConfig,
91
+ ...envConfig,
92
+ ...overrides,
93
+ };
94
+
95
+ const result = ConfigSchema.safeParse(mergedConfig);
96
+ if (!result.success) {
97
+ const error = result.error.format();
98
+ throw new Error(`Invalid configuration: ${JSON.stringify(error, null, 2)}`);
99
+ }
100
+
101
+ this.config = result.data as AppConfig;
102
+
103
+ // Default opsPort if not set
104
+ if (!this.config.opsPort) {
105
+ this.config.opsPort = this.config.port === 0 ? 0 : this.config.port + 1;
106
+ }
107
+ }
108
+
109
+ get<K extends keyof AppConfig>(key: K): AppConfig[K] {
110
+ return this.config[key];
111
+ }
112
+
113
+ get all(): AppConfig {
114
+ return { ...this.config };
115
+ }
116
+
117
+ private loadConfigFile(): Partial<AppConfig> {
118
+ const configPath = process.env.CONFIG_FILE ||
119
+ (fs.existsSync(path.resolve(process.cwd(), 'conduit.yaml')) ? 'conduit.yaml' :
120
+ (fs.existsSync(path.resolve(process.cwd(), 'conduit.json')) ? 'conduit.json' : null));
121
+
122
+ if (!configPath) return {};
123
+
124
+ try {
125
+ const fullPath = path.resolve(process.cwd(), configPath);
126
+ let fileContent = fs.readFileSync(fullPath, 'utf-8');
127
+
128
+ // Env var substitution: ${VAR} or ${VAR:-default}
129
+ fileContent = fileContent.replace(/\$\{([a-zA-Z0-9_]+)(?::-([^}]+))?\}/g, (match, varName, defaultValue) => {
130
+ const value = process.env[varName];
131
+ if (value !== undefined) {
132
+ return value;
133
+ }
134
+ return defaultValue !== undefined ? defaultValue : '';
135
+ });
136
+
137
+ if (configPath.endsWith('.yaml') || configPath.endsWith('.yml')) {
138
+ return yaml.load(fileContent) as Partial<AppConfig>;
139
+ } else {
140
+ return JSON.parse(fileContent);
141
+ }
142
+ } catch (error) {
143
+ console.warn(`Failed to load config file ${configPath}:`, error);
144
+ return {};
145
+ }
146
+ }
147
+ }
@@ -0,0 +1,37 @@
1
+ import { v4 as uuidv4 } from 'uuid';
2
+ import { Logger } from 'pino';
3
+
4
+ export interface ExecutionContextOptions {
5
+ tenantId?: string;
6
+ logger: Logger;
7
+ allowedTools?: string[];
8
+ remoteAddress?: string;
9
+ strictValidation?: boolean;
10
+ }
11
+
12
+ export class ExecutionContext {
13
+ public readonly correlationId: string;
14
+ public readonly startTime: number;
15
+ public readonly tenantId?: string;
16
+ public logger: Logger;
17
+ public allowedTools?: string[];
18
+ public readonly remoteAddress?: string;
19
+ public readonly strictValidation: boolean;
20
+
21
+ constructor(options: ExecutionContextOptions) {
22
+ this.correlationId = uuidv4();
23
+ this.startTime = Date.now();
24
+ this.tenantId = options.tenantId;
25
+ this.allowedTools = options.allowedTools;
26
+ this.remoteAddress = options.remoteAddress;
27
+ this.strictValidation = options.strictValidation ?? false;
28
+ this.logger = options.logger.child({
29
+ correlationId: this.correlationId,
30
+ tenantId: this.tenantId,
31
+ });
32
+ }
33
+
34
+ getDuration(): number {
35
+ return Date.now() - this.startTime;
36
+ }
37
+ }
@@ -0,0 +1,209 @@
1
+ import { Logger } from 'pino';
2
+ import { ExecutorRegistry } from './registries/executor.registry.js';
3
+ import { ResourceLimits } from './config.service.js';
4
+ import { GatewayService } from '../gateway/gateway.service.js';
5
+ import { SecurityService } from './security.service.js';
6
+ import { SDKGenerator, toToolBinding } from '../sdk/index.js';
7
+ import { ExecutionContext } from './execution.context.js';
8
+ import { ConduitError } from './types.js';
9
+ import { ExecutionResult } from './interfaces/executor.interface.js';
10
+
11
+ export { ExecutionResult };
12
+
13
+ export class ExecutionService {
14
+ private logger: Logger;
15
+ private executorRegistry: ExecutorRegistry;
16
+ private sdkGenerator = new SDKGenerator();
17
+ private defaultLimits: ResourceLimits;
18
+ private gatewayService: GatewayService;
19
+ private securityService: SecurityService;
20
+ private _ipcAddress: string = '';
21
+
22
+ constructor(
23
+ logger: Logger,
24
+ defaultLimits: ResourceLimits,
25
+ gatewayService: GatewayService,
26
+ securityService: SecurityService,
27
+ executorRegistry: ExecutorRegistry
28
+ ) {
29
+ this.logger = logger;
30
+ this.defaultLimits = defaultLimits;
31
+ this.gatewayService = gatewayService;
32
+ this.securityService = securityService;
33
+ this.executorRegistry = executorRegistry;
34
+ }
35
+
36
+ set ipcAddress(addr: string) {
37
+ this._ipcAddress = addr;
38
+ }
39
+
40
+ async executeTypeScript(
41
+ code: string,
42
+ limits: ResourceLimits,
43
+ context: ExecutionContext,
44
+ allowedTools?: string[]
45
+ ): Promise<ExecutionResult> {
46
+ const effectiveLimits = { ...this.defaultLimits, ...limits };
47
+
48
+ // 1. Validation
49
+ const securityResult = this.securityService.validateCode(code);
50
+ if (!securityResult.valid) {
51
+ return this.createErrorResult(ConduitError.Forbidden, securityResult.message || 'Access denied');
52
+ }
53
+
54
+ // 2. Routing Logic (Isolate vs Deno)
55
+ // Strip simple comments to avoid false positives
56
+ const cleanCode = code.replace(/\/\*[\s\S]*?\*\/|([^:]|^)\/\/.*$/gm, '$1');
57
+ const hasImports = /^\s*import\s/m.test(cleanCode) ||
58
+ /^\s*export\s/m.test(cleanCode) ||
59
+ /\bDeno\./.test(cleanCode) ||
60
+ /\bDeno\b/.test(cleanCode);
61
+
62
+ // Try to use IsolateExecutor if available and code is simple
63
+ if (!hasImports && this.executorRegistry.has('isolate')) {
64
+ return await this.executeIsolate(code, effectiveLimits, context, allowedTools);
65
+ }
66
+
67
+ // Fix Sev2: Ensure IPC address is set before execution if Deno is needed
68
+ if (!this._ipcAddress) {
69
+ return this.createErrorResult(ConduitError.InternalError, 'IPC address not initialized');
70
+ }
71
+
72
+ // 3. Fallback to Deno
73
+ if (!this.executorRegistry.has('deno')) {
74
+ return this.createErrorResult(ConduitError.InternalError, 'Deno execution not available');
75
+ }
76
+
77
+ const executor = this.executorRegistry.get('deno')!;
78
+
79
+ // 4. SDK Generation
80
+ const bindings = await this.getToolBindings(context);
81
+ const sdkCode = this.sdkGenerator.generateTypeScript(bindings, allowedTools);
82
+
83
+ // 5. Session & Execution
84
+ const sessionToken = this.securityService.createSession(allowedTools);
85
+ try {
86
+ return await executor.execute(code, effectiveLimits, context, {
87
+ ipcAddress: this._ipcAddress,
88
+ ipcToken: sessionToken,
89
+ sdkCode
90
+ });
91
+ } finally {
92
+ this.securityService.invalidateSession(sessionToken);
93
+ }
94
+ }
95
+
96
+ async executePython(
97
+ code: string,
98
+ limits: ResourceLimits,
99
+ context: ExecutionContext,
100
+ allowedTools?: string[]
101
+ ): Promise<ExecutionResult> {
102
+ const effectiveLimits = { ...this.defaultLimits, ...limits };
103
+
104
+ if (!this.executorRegistry.has('python')) {
105
+ return this.createErrorResult(ConduitError.InternalError, 'Python execution not available');
106
+ }
107
+
108
+ // Fix Sev2: Ensure IPC address is set before execution for Python
109
+ if (!this._ipcAddress) {
110
+ return this.createErrorResult(ConduitError.InternalError, 'IPC address not initialized');
111
+ }
112
+
113
+ const executor = this.executorRegistry.get('python')!;
114
+
115
+ const securityResult = this.securityService.validateCode(code);
116
+ if (!securityResult.valid) {
117
+ return this.createErrorResult(ConduitError.Forbidden, securityResult.message || 'Access denied');
118
+ }
119
+
120
+ const bindings = await this.getToolBindings(context);
121
+ const sdkCode = this.sdkGenerator.generatePython(bindings, allowedTools);
122
+
123
+ const sessionToken = this.securityService.createSession(allowedTools);
124
+ try {
125
+ return await executor.execute(code, effectiveLimits, context, {
126
+ ipcAddress: this._ipcAddress,
127
+ ipcToken: sessionToken,
128
+ sdkCode
129
+ });
130
+ } finally {
131
+ this.securityService.invalidateSession(sessionToken);
132
+ }
133
+ }
134
+
135
+ private async getToolBindings(context: ExecutionContext) {
136
+ // Phase 1: Lazy loading - fetch stubs instead of full schemas
137
+ const packages = await this.gatewayService.listToolPackages();
138
+ const allBindings = [];
139
+
140
+ for (const pkg of packages) {
141
+ try {
142
+ // Determine if we need to fetch tools for this package
143
+ // Optimization: if allowedTools is strict, we could filter packages here
144
+
145
+ const stubs = await this.gatewayService.listToolStubs(pkg.id, context);
146
+ allBindings.push(...stubs.map(s => toToolBinding(s.id, undefined, s.description)));
147
+ } catch (err: any) {
148
+ this.logger.warn({ packageId: pkg.id, err: err.message }, 'Failed to list stubs for package');
149
+ }
150
+ }
151
+ return allBindings;
152
+ }
153
+
154
+ async executeIsolate(
155
+ code: string,
156
+ limits: ResourceLimits,
157
+ context: ExecutionContext,
158
+ allowedTools?: string[]
159
+ ): Promise<ExecutionResult> {
160
+ if (!this.executorRegistry.has('isolate')) {
161
+ return this.createErrorResult(ConduitError.InternalError, 'IsolateExecutor not available');
162
+ }
163
+ const executor = this.executorRegistry.get('isolate')!;
164
+
165
+ const effectiveLimits = { ...this.defaultLimits, ...limits };
166
+ const securityResult = this.securityService.validateCode(code);
167
+ if (!securityResult.valid) {
168
+ return this.createErrorResult(ConduitError.Forbidden, securityResult.message || 'Access denied');
169
+ }
170
+
171
+ const bindings = await this.getToolBindings(context);
172
+ const sdkCode = this.sdkGenerator.generateIsolateSDK(bindings, allowedTools);
173
+
174
+ try {
175
+ return await executor.execute(code, effectiveLimits, context, { sdkCode });
176
+ } catch (err: any) {
177
+ return this.createErrorResult(ConduitError.InternalError, err.message);
178
+ }
179
+ }
180
+
181
+ private createErrorResult(code: number, message: string): ExecutionResult {
182
+ return {
183
+ stdout: '',
184
+ stderr: '',
185
+ exitCode: null,
186
+ error: { code, message }
187
+ };
188
+ }
189
+
190
+ async shutdown(): Promise<void> {
191
+ await this.executorRegistry.shutdownAll();
192
+ }
193
+
194
+ async warmup(): Promise<void> {
195
+ const pythonExecutor = this.executorRegistry.get('python');
196
+ if (pythonExecutor && 'warmup' in pythonExecutor) {
197
+ // Cast to any because warmup is not in general Executor interface yet
198
+ await (pythonExecutor as any).warmup(this.defaultLimits);
199
+ }
200
+ }
201
+
202
+ async healthCheck(): Promise<any> {
203
+ const pythonExecutor = this.executorRegistry.get('python');
204
+ if (pythonExecutor && 'healthCheck' in pythonExecutor) {
205
+ return (pythonExecutor as any).healthCheck();
206
+ }
207
+ return { status: 'ok' };
208
+ }
209
+ }
@@ -0,0 +1,17 @@
1
+ import { ResourceLimits } from '../config.service.js';
2
+ import { UpstreamInfo } from '../../gateway/upstream.client.js';
3
+
4
+ export interface AppConfig {
5
+ port: number;
6
+ nodeEnv: 'development' | 'production' | 'test';
7
+ logLevel: 'debug' | 'info' | 'warn' | 'error';
8
+ resourceLimits: ResourceLimits;
9
+ secretRedactionPatterns: string[];
10
+ ipcBearerToken: string;
11
+ maxConcurrent: number;
12
+ denoMaxPoolSize: number;
13
+ pyodideMaxPoolSize: number;
14
+ metricsUrl: string;
15
+ opsPort?: number;
16
+ upstreams?: UpstreamInfo[];
17
+ }
@@ -0,0 +1,31 @@
1
+ import { ResourceLimits } from '../config.service.js';
2
+ import { ExecutionContext } from '../execution.context.js';
3
+
4
+ export interface ExecutionResult {
5
+ stdout: string;
6
+ stderr: string;
7
+ exitCode: number | null;
8
+ error?: {
9
+ code: number;
10
+ message: string;
11
+ };
12
+ }
13
+
14
+ export interface ExecutorConfig {
15
+ ipcAddress?: string;
16
+ ipcToken?: string;
17
+ sdkCode?: string;
18
+ }
19
+
20
+ export interface Executor {
21
+ execute(
22
+ code: string,
23
+ limits: ResourceLimits,
24
+ context: ExecutionContext,
25
+ config?: ExecutorConfig
26
+ ): Promise<ExecutionResult>;
27
+
28
+ shutdown?(): Promise<void>;
29
+ healthCheck?(): Promise<{ status: string; workers?: number; detail?: string }>;
30
+ warmup?(limits?: ResourceLimits): Promise<void>;
31
+ }
@@ -0,0 +1,12 @@
1
+ import { ExecutionContext } from '../execution.context.js';
2
+ import { JSONRPCRequest, JSONRPCResponse } from '../types.js';
3
+
4
+ export type NextFunction = () => Promise<JSONRPCResponse>;
5
+
6
+ export interface Middleware {
7
+ handle(
8
+ request: JSONRPCRequest,
9
+ context: ExecutionContext,
10
+ next: NextFunction
11
+ ): Promise<JSONRPCResponse>;
12
+ }
@@ -0,0 +1,3 @@
1
+ export interface IUrlValidator {
2
+ validateUrl(url: string): Promise<{ valid: boolean; message?: string; resolvedIp?: string }>;
3
+ }
@@ -0,0 +1,64 @@
1
+ import pino from 'pino';
2
+ import { AsyncLocalStorage } from 'node:async_hooks';
3
+ import { ConfigService } from './config.service.js';
4
+
5
+ export const loggerStorage = new AsyncLocalStorage<{ correlationId: string }>();
6
+
7
+ export function createLogger(configService: ConfigService) {
8
+ const logLevel = configService.get('logLevel');
9
+ const redactionPatterns = configService.get('secretRedactionPatterns');
10
+ const secretPatterns = redactionPatterns.map(p => new RegExp(p, 'g'));
11
+
12
+ const redactString = (str: string) => {
13
+ let result = str;
14
+ for (const pattern of secretPatterns) {
15
+ result = result.replace(pattern, '[REDACTED]');
16
+ }
17
+ return result;
18
+ };
19
+
20
+ return pino({
21
+ level: logLevel,
22
+ hooks: {
23
+ logMethod(inputArgs, method) {
24
+ const redactedArgs = inputArgs.map(arg => {
25
+ try {
26
+ if (typeof arg === 'string') {
27
+ return redactString(arg);
28
+ }
29
+ if (typeof arg === 'object' && arg !== null) {
30
+ // Shallow clone and redact keys if they are strings
31
+ const clone = { ...arg } as any;
32
+ for (const key in clone) {
33
+ if (typeof clone[key] === 'string') {
34
+ clone[key] = redactString(clone[key]);
35
+ }
36
+ }
37
+ return clone;
38
+ }
39
+ } catch (err) {
40
+ return '[REDACTION_ERROR]';
41
+ }
42
+ return arg;
43
+ });
44
+ return method.apply(this, redactedArgs as any);
45
+ }
46
+ },
47
+ redact: {
48
+ paths: ['toolParams.*', 'headers.Authorization', 'headers.authorization', 'params.token'],
49
+ censor: '[REDACTED]',
50
+ },
51
+ mixin() {
52
+ const store = loggerStorage.getStore();
53
+ return {
54
+ correlationId: store?.correlationId,
55
+ };
56
+ },
57
+ transport: configService.get('nodeEnv') === 'development' ? {
58
+ target: 'pino-pretty',
59
+ options: {
60
+ colorize: true,
61
+ }
62
+ } : undefined,
63
+ });
64
+ }