@enactprotocol/shared 1.0.12

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 (97) hide show
  1. package/dist/LocalToolResolver.d.ts +84 -0
  2. package/dist/LocalToolResolver.js +353 -0
  3. package/dist/api/enact-api.d.ts +124 -0
  4. package/dist/api/enact-api.js +406 -0
  5. package/dist/api/index.d.ts +2 -0
  6. package/dist/api/index.js +2 -0
  7. package/dist/api/types.d.ts +83 -0
  8. package/dist/api/types.js +1 -0
  9. package/dist/core/DaggerExecutionProvider.d.ts +169 -0
  10. package/dist/core/DaggerExecutionProvider.js +996 -0
  11. package/dist/core/DirectExecutionProvider.d.ts +23 -0
  12. package/dist/core/DirectExecutionProvider.js +406 -0
  13. package/dist/core/EnactCore.d.ts +138 -0
  14. package/dist/core/EnactCore.js +609 -0
  15. package/dist/core/index.d.ts +3 -0
  16. package/dist/core/index.js +3 -0
  17. package/dist/exec/index.d.ts +3 -0
  18. package/dist/exec/index.js +3 -0
  19. package/dist/exec/logger.d.ts +11 -0
  20. package/dist/exec/logger.js +57 -0
  21. package/dist/exec/validate.d.ts +5 -0
  22. package/dist/exec/validate.js +167 -0
  23. package/dist/index.d.ts +25 -0
  24. package/dist/index.js +29 -0
  25. package/dist/lib/enact-direct.d.ts +156 -0
  26. package/dist/lib/enact-direct.js +158 -0
  27. package/dist/lib/index.d.ts +1 -0
  28. package/dist/lib/index.js +1 -0
  29. package/dist/security/index.d.ts +3 -0
  30. package/dist/security/index.js +3 -0
  31. package/dist/security/security.d.ts +23 -0
  32. package/dist/security/security.js +137 -0
  33. package/dist/security/sign.d.ts +103 -0
  34. package/dist/security/sign.js +532 -0
  35. package/dist/security/verification-enforcer.d.ts +41 -0
  36. package/dist/security/verification-enforcer.js +181 -0
  37. package/dist/services/McpCoreService.d.ts +102 -0
  38. package/dist/services/McpCoreService.js +120 -0
  39. package/dist/services/index.d.ts +1 -0
  40. package/dist/services/index.js +1 -0
  41. package/dist/types.d.ts +130 -0
  42. package/dist/types.js +3 -0
  43. package/dist/utils/config.d.ts +32 -0
  44. package/dist/utils/config.js +78 -0
  45. package/dist/utils/env-loader.d.ts +54 -0
  46. package/dist/utils/env-loader.js +270 -0
  47. package/dist/utils/help.d.ts +36 -0
  48. package/dist/utils/help.js +248 -0
  49. package/dist/utils/index.d.ts +7 -0
  50. package/dist/utils/index.js +7 -0
  51. package/dist/utils/logger.d.ts +35 -0
  52. package/dist/utils/logger.js +75 -0
  53. package/dist/utils/silent-monitor.d.ts +67 -0
  54. package/dist/utils/silent-monitor.js +242 -0
  55. package/dist/utils/timeout.d.ts +5 -0
  56. package/dist/utils/timeout.js +23 -0
  57. package/dist/utils/version.d.ts +4 -0
  58. package/dist/utils/version.js +14 -0
  59. package/dist/web/env-manager-server.d.ts +29 -0
  60. package/dist/web/env-manager-server.js +367 -0
  61. package/dist/web/index.d.ts +1 -0
  62. package/dist/web/index.js +1 -0
  63. package/package.json +79 -0
  64. package/src/LocalToolResolver.ts +424 -0
  65. package/src/api/enact-api.ts +569 -0
  66. package/src/api/index.ts +2 -0
  67. package/src/api/types.ts +93 -0
  68. package/src/core/DaggerExecutionProvider.ts +1308 -0
  69. package/src/core/DirectExecutionProvider.ts +484 -0
  70. package/src/core/EnactCore.ts +833 -0
  71. package/src/core/index.ts +3 -0
  72. package/src/exec/index.ts +3 -0
  73. package/src/exec/logger.ts +63 -0
  74. package/src/exec/validate.ts +238 -0
  75. package/src/index.ts +42 -0
  76. package/src/lib/enact-direct.ts +258 -0
  77. package/src/lib/index.ts +1 -0
  78. package/src/security/index.ts +3 -0
  79. package/src/security/security.ts +188 -0
  80. package/src/security/sign.ts +797 -0
  81. package/src/security/verification-enforcer.ts +268 -0
  82. package/src/services/McpCoreService.ts +203 -0
  83. package/src/services/index.ts +1 -0
  84. package/src/types.ts +190 -0
  85. package/src/utils/config.ts +97 -0
  86. package/src/utils/env-loader.ts +370 -0
  87. package/src/utils/help.ts +257 -0
  88. package/src/utils/index.ts +7 -0
  89. package/src/utils/logger.ts +83 -0
  90. package/src/utils/silent-monitor.ts +328 -0
  91. package/src/utils/timeout.ts +26 -0
  92. package/src/utils/version.ts +16 -0
  93. package/src/web/env-manager-server.ts +465 -0
  94. package/src/web/index.ts +1 -0
  95. package/src/web/static/app.js +663 -0
  96. package/src/web/static/index.html +117 -0
  97. package/src/web/static/style.css +291 -0
@@ -0,0 +1,1308 @@
1
+ // src/core/DaggerExecutionProvider.ts - Enhanced Dagger execution provider with hanging prevention
2
+ import { connect, Client, Container } from "@dagger.io/dagger";
3
+ import {
4
+ ExecutionProvider,
5
+ type EnactTool,
6
+ type ExecutionEnvironment,
7
+ type ExecutionResult,
8
+ } from "../types.js";
9
+ import logger from "../exec/logger.js";
10
+ import { parseTimeout } from "../utils/timeout.js";
11
+ import fs from "fs/promises";
12
+ import path from "path";
13
+ import crypto from "crypto";
14
+ import { spawn, spawnSync } from "child_process";
15
+
16
+ export interface DaggerExecutionOptions {
17
+ baseImage?: string; // Default container image
18
+ workdir?: string; // Working directory in container
19
+ enableNetwork?: boolean; // Allow network access
20
+ enableHostFS?: boolean; // Allow mounting host filesystem
21
+ maxMemory?: string; // Memory limit (e.g., "512Mi", "2Gi")
22
+ maxCPU?: string; // CPU limit (e.g., "0.5", "2")
23
+ cacheVolume?: string; // Cache volume name for persistence
24
+ useShell?: boolean; // Use shell wrapper for complex commands
25
+ engineTimeout?: number; // Engine connection timeout (ms)
26
+ maxRetries?: number; // Max retries for failed operations
27
+ enableEngineHealthCheck?: boolean; // Enable periodic engine health checks
28
+ }
29
+
30
+ interface CommandResult {
31
+ stdout: string;
32
+ stderr: string;
33
+ exitCode: number;
34
+ }
35
+
36
+ interface EngineHealthStatus {
37
+ isHealthy: boolean;
38
+ lastCheck: Date;
39
+ consecutiveFailures: number;
40
+ }
41
+
42
+ export class DaggerExecutionProvider extends ExecutionProvider {
43
+ private client: Client | null = null;
44
+ private options: DaggerExecutionOptions;
45
+ private tempDir: string;
46
+ private connectionCleanup: (() => void) | null = null;
47
+ private engineHealth: EngineHealthStatus;
48
+ private abortController: AbortController | null = null;
49
+ private activeSessions: Set<string> = new Set();
50
+ private isShuttingDown = false;
51
+
52
+ constructor(options: DaggerExecutionOptions = {}) {
53
+ super();
54
+ this.options = {
55
+ baseImage: "node:20-slim",
56
+ workdir: "/workspace",
57
+ enableNetwork: true,
58
+ enableHostFS: false,
59
+ useShell: true,
60
+ engineTimeout: 30000, // 30 second engine timeout
61
+ maxRetries: 3, // Max 3 retries
62
+ enableEngineHealthCheck: true,
63
+ ...options,
64
+ };
65
+ this.tempDir = "";
66
+ this.engineHealth = {
67
+ isHealthy: true,
68
+ lastCheck: new Date(),
69
+ consecutiveFailures: 0,
70
+ };
71
+
72
+ // Register comprehensive cleanup handlers
73
+ this.registerCleanupHandlers();
74
+
75
+ // Start periodic health checks if enabled
76
+ if (this.options.enableEngineHealthCheck) {
77
+ this.startEngineHealthMonitoring();
78
+ }
79
+ }
80
+
81
+ async setup(tool: EnactTool): Promise<boolean> {
82
+ try {
83
+ // Perform engine health check before setup
84
+ if (!(await this.checkEngineHealth())) {
85
+ logger.warn("🔧 Engine unhealthy, attempting reset...");
86
+ await this.resetEngineContainer();
87
+ }
88
+
89
+ // Create a temporary directory for this execution
90
+ this.tempDir = path.join(
91
+ "/tmp",
92
+ `enact-${crypto.randomBytes(8).toString("hex")}`,
93
+ );
94
+ await fs.mkdir(this.tempDir, { recursive: true });
95
+
96
+ logger.info(
97
+ `🐳 Dagger execution provider initialized for tool: ${tool.name}`,
98
+ );
99
+ return true;
100
+ } catch (error) {
101
+ logger.error(`Failed to setup Dagger execution provider: ${error}`);
102
+ return false;
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Enhanced cleanup with comprehensive engine management and session tracking
108
+ */
109
+ async cleanup(): Promise<boolean> {
110
+ this.isShuttingDown = true;
111
+
112
+ try {
113
+ // Cancel any active operations
114
+ if (this.abortController) {
115
+ this.abortController.abort();
116
+ }
117
+
118
+ // Wait for active sessions to complete (with timeout)
119
+ await this.waitForActiveSessions(5000);
120
+
121
+ // Clean up temporary directory
122
+ if (this.tempDir) {
123
+ await fs.rm(this.tempDir, { recursive: true, force: true });
124
+ }
125
+
126
+ // Enhanced engine cleanup with better detection and error handling
127
+ await this.performEngineCleanup();
128
+
129
+ // Reset client reference
130
+ this.client = null;
131
+
132
+ logger.info("🧹 Dagger execution provider cleaned up successfully");
133
+ return true;
134
+ } catch (error) {
135
+ logger.error(`Failed to cleanup Dagger execution provider: ${error}`);
136
+ return false;
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Enhanced engine cleanup with better container detection
142
+ */
143
+ private async performEngineCleanup(): Promise<void> {
144
+ try {
145
+ logger.debug("🔍 Detecting Dagger engine containers...");
146
+
147
+ // Get all Dagger engine containers (running and stopped)
148
+ const containerListResult = spawnSync(
149
+ "docker",
150
+ [
151
+ "container",
152
+ "list",
153
+ "--all",
154
+ "--filter",
155
+ "name=^dagger-engine-*",
156
+ "--format",
157
+ "{{.Names}}",
158
+ ],
159
+ {
160
+ encoding: "utf8",
161
+ timeout: 10000,
162
+ },
163
+ );
164
+
165
+ if (containerListResult.error) {
166
+ logger.warn(
167
+ "Could not list Docker containers, skipping engine cleanup",
168
+ );
169
+ return;
170
+ }
171
+
172
+ const containerNames = containerListResult.stdout
173
+ .trim()
174
+ .split("\n")
175
+ .filter((name) => name.trim())
176
+ .map((name) => name.trim());
177
+
178
+ if (containerNames.length === 0) {
179
+ logger.debug("No Dagger engine containers found");
180
+ return;
181
+ }
182
+
183
+ logger.info(
184
+ `🔄 Found ${containerNames.length} Dagger engine container(s), cleaning up...`,
185
+ );
186
+
187
+ // Force remove all engine containers
188
+ for (const containerName of containerNames) {
189
+ try {
190
+ logger.debug(`Removing container: ${containerName}`);
191
+ spawnSync("docker", ["container", "rm", "-f", containerName], {
192
+ timeout: 10000,
193
+ });
194
+ } catch (e) {
195
+ logger.debug(`Failed to remove container ${containerName}:`, e);
196
+ }
197
+ }
198
+
199
+ // Optional: Clean up engine images if requested (more aggressive cleanup)
200
+ if (process.env.DAGGER_AGGRESSIVE_CLEANUP === "true") {
201
+ logger.debug(
202
+ "🧹 Performing aggressive cleanup - removing engine images...",
203
+ );
204
+ spawnSync(
205
+ "docker",
206
+ [
207
+ "rmi",
208
+ "--force",
209
+ ...spawnSync(
210
+ "docker",
211
+ [
212
+ "images",
213
+ "-q",
214
+ "--filter",
215
+ "reference=registry.dagger.io/engine",
216
+ ],
217
+ {
218
+ encoding: "utf8",
219
+ },
220
+ )
221
+ .stdout.trim()
222
+ .split("\n")
223
+ .filter(Boolean),
224
+ ],
225
+ { timeout: 15000 },
226
+ );
227
+ }
228
+
229
+ logger.info("✅ Dagger engine cleanup completed");
230
+ } catch (error) {
231
+ logger.debug("Engine cleanup failed (this is usually fine):", error);
232
+ }
233
+ }
234
+
235
+ /**
236
+ * Check engine health with comprehensive diagnostics
237
+ */
238
+ private async checkEngineHealth(): Promise<boolean> {
239
+ try {
240
+ // Check if Docker daemon is accessible
241
+ const dockerCheck = spawnSync("docker", ["version"], {
242
+ encoding: "utf8",
243
+ timeout: 5000,
244
+ });
245
+
246
+ if (dockerCheck.error || dockerCheck.status !== 0) {
247
+ logger.warn("Docker daemon not accessible");
248
+ this.engineHealth.consecutiveFailures++;
249
+ return false;
250
+ }
251
+
252
+ // Check for hanging engine containers
253
+ const hangingContainers = spawnSync(
254
+ "docker",
255
+ [
256
+ "ps",
257
+ "--filter",
258
+ "name=dagger-engine",
259
+ "--filter",
260
+ "status=exited",
261
+ "--format",
262
+ "{{.Names}}",
263
+ ],
264
+ {
265
+ encoding: "utf8",
266
+ timeout: 5000,
267
+ },
268
+ );
269
+
270
+ if (hangingContainers.stdout.trim()) {
271
+ logger.warn("Detected stopped Dagger engine containers");
272
+ this.engineHealth.consecutiveFailures++;
273
+ return false;
274
+ }
275
+
276
+ // Reset failure count on success
277
+ this.engineHealth.consecutiveFailures = 0;
278
+ this.engineHealth.isHealthy = true;
279
+ this.engineHealth.lastCheck = new Date();
280
+ return true;
281
+ } catch (error) {
282
+ logger.debug("Engine health check failed:", error);
283
+ this.engineHealth.consecutiveFailures++;
284
+ return false;
285
+ }
286
+ }
287
+
288
+ /**
289
+ * Reset engine container when health check fails
290
+ */
291
+ private async resetEngineContainer(): Promise<void> {
292
+ logger.info("🔄 Resetting Dagger engine container...");
293
+
294
+ try {
295
+ // Stop and remove all engine containers
296
+ await this.performEngineCleanup();
297
+
298
+ // Wait a moment for cleanup to complete
299
+ await new Promise((resolve) => setTimeout(resolve, 2000));
300
+
301
+ // Engine will auto-restart on next connection
302
+ this.engineHealth.isHealthy = true;
303
+ this.engineHealth.consecutiveFailures = 0;
304
+
305
+ logger.info("✅ Engine reset completed");
306
+ } catch (error) {
307
+ logger.error("Failed to reset engine container:", error);
308
+ throw error;
309
+ }
310
+ }
311
+
312
+ /**
313
+ * Start periodic engine health monitoring
314
+ */
315
+ private startEngineHealthMonitoring(): void {
316
+ // Check engine health every 60 seconds
317
+ setInterval(async () => {
318
+ if (this.isShuttingDown) return;
319
+
320
+ const isHealthy = await this.checkEngineHealth();
321
+
322
+ if (!isHealthy && this.engineHealth.consecutiveFailures >= 3) {
323
+ logger.warn("🚨 Engine health degraded, triggering reset...");
324
+ try {
325
+ await this.resetEngineContainer();
326
+ } catch (error) {
327
+ logger.error("Failed to auto-reset engine:", error);
328
+ }
329
+ }
330
+ }, 60000);
331
+ }
332
+
333
+ /**
334
+ * Wait for active sessions to complete with timeout
335
+ */
336
+ private async waitForActiveSessions(timeoutMs: number): Promise<void> {
337
+ if (this.activeSessions.size === 0) return;
338
+
339
+ logger.info(
340
+ `⏳ Waiting for ${this.activeSessions.size} active sessions to complete...`,
341
+ );
342
+
343
+ const startTime = Date.now();
344
+ while (this.activeSessions.size > 0 && Date.now() - startTime < timeoutMs) {
345
+ await new Promise((resolve) => setTimeout(resolve, 100));
346
+ }
347
+
348
+ if (this.activeSessions.size > 0) {
349
+ logger.warn(
350
+ `⚠️ ${this.activeSessions.size} sessions did not complete within timeout`,
351
+ );
352
+ }
353
+ }
354
+
355
+ async resolveEnvironmentVariables(
356
+ envConfig: Record<string, any>,
357
+ namespace?: string,
358
+ ): Promise<Record<string, any>> {
359
+ const resolved: Record<string, any> = {};
360
+
361
+ for (const [key, config] of Object.entries(envConfig)) {
362
+ if (typeof config === "object" && config.source) {
363
+ switch (config.source) {
364
+ case "env":
365
+ resolved[key] = process.env[key] || config.default;
366
+ break;
367
+ case "user":
368
+ resolved[key] = config.default;
369
+ break;
370
+ default:
371
+ resolved[key] = config.default;
372
+ }
373
+ } else {
374
+ resolved[key] = config;
375
+ }
376
+ }
377
+
378
+ return resolved;
379
+ }
380
+
381
+ async execute(
382
+ tool: EnactTool,
383
+ inputs: Record<string, any>,
384
+ environment: ExecutionEnvironment,
385
+ ): Promise<ExecutionResult> {
386
+ const executionId = crypto.randomBytes(16).toString("hex");
387
+ const startTime = new Date().toISOString();
388
+
389
+ // Track this session
390
+ this.activeSessions.add(executionId);
391
+
392
+ try {
393
+ logger.info(`🚀 Executing Enact tool "${tool.name}" in Dagger container`);
394
+ logger.debug(`Tool command: ${tool.command}`);
395
+ logger.debug(`Tool timeout: ${tool.timeout || "default"}`);
396
+
397
+ // Retry logic for handling transient failures
398
+ let lastError: Error | null = null;
399
+ for (let attempt = 1; attempt <= this.options.maxRetries!; attempt++) {
400
+ try {
401
+ // Check engine health before each attempt
402
+ if (!(await this.checkEngineHealth())) {
403
+ logger.warn(`Attempt ${attempt}: Engine unhealthy, resetting...`);
404
+ await this.resetEngineContainer();
405
+ }
406
+
407
+ const result = await this.executeCommand(
408
+ tool.command,
409
+ inputs,
410
+ environment,
411
+ tool.timeout,
412
+ );
413
+
414
+ logger.debug(
415
+ `Command result: exitCode=${result.exitCode}, stdout length=${result.stdout?.length || 0}, stderr length=${result.stderr?.length || 0}`,
416
+ );
417
+
418
+ const output = this.parseOutput(result.stdout, tool);
419
+
420
+ return {
421
+ success: result.exitCode === 0,
422
+ output: output,
423
+ error:
424
+ result.exitCode !== 0
425
+ ? {
426
+ message: result.stderr || "Command failed",
427
+ code: result.exitCode.toString(),
428
+ details: {
429
+ stdout: result.stdout,
430
+ stderr: result.stderr,
431
+ exitCode: result.exitCode,
432
+ attempt,
433
+ },
434
+ }
435
+ : undefined,
436
+ metadata: {
437
+ executionId,
438
+ toolName: tool.name,
439
+ version: tool.version || "1.0.0",
440
+ executedAt: startTime,
441
+ environment: "dagger",
442
+ timeout: tool.timeout,
443
+ command: tool.command,
444
+ },
445
+ };
446
+ } catch (error) {
447
+ lastError = error instanceof Error ? error : new Error(String(error));
448
+ logger.warn(
449
+ `Attempt ${attempt}/${this.options.maxRetries} failed: ${lastError.message}`,
450
+ );
451
+
452
+ if (attempt < this.options.maxRetries!) {
453
+ // Wait before retry with exponential backoff
454
+ const waitTime = Math.min(1000 * Math.pow(2, attempt - 1), 10000);
455
+ logger.debug(`Waiting ${waitTime}ms before retry...`);
456
+ await new Promise((resolve) => setTimeout(resolve, waitTime));
457
+ }
458
+ }
459
+ }
460
+
461
+ // All retries failed
462
+ throw lastError || new Error("Unknown error during execution");
463
+ } catch (error) {
464
+ logger.error(`Execution failed for Enact tool ${tool.name}: ${error}`);
465
+
466
+ // Enhanced error categorization
467
+ const errorType = this.categorizeError(error);
468
+
469
+ return {
470
+ success: false,
471
+ error: {
472
+ message: error instanceof Error ? error.message : "Unknown error",
473
+ code: errorType,
474
+ details: {
475
+ error,
476
+ engineHealth: this.engineHealth,
477
+ activeSessions: this.activeSessions.size,
478
+ },
479
+ },
480
+ metadata: {
481
+ executionId,
482
+ toolName: tool.name,
483
+ version: tool.version || "1.0.0",
484
+ executedAt: startTime,
485
+ environment: "dagger",
486
+ timeout: tool.timeout,
487
+ command: tool.command,
488
+ },
489
+ };
490
+ } finally {
491
+ // Remove from active sessions
492
+ this.activeSessions.delete(executionId);
493
+ }
494
+ }
495
+
496
+ /**
497
+ * Categorize errors for better handling
498
+ */
499
+ private categorizeError(error: unknown): string {
500
+ if (!(error instanceof Error)) return "UNKNOWN_ERROR";
501
+
502
+ const message = error.message.toLowerCase();
503
+
504
+ if (message.includes("timeout") || message.includes("timed out")) {
505
+ return "TIMEOUT";
506
+ }
507
+ if (message.includes("buildkit") || message.includes("failed to respond")) {
508
+ return "ENGINE_CONNECTION_ERROR";
509
+ }
510
+ if (message.includes("docker") || message.includes("container")) {
511
+ return "CONTAINER_ERROR";
512
+ }
513
+ if (message.includes("network") || message.includes("dns")) {
514
+ return "NETWORK_ERROR";
515
+ }
516
+
517
+ return "EXECUTION_ERROR";
518
+ }
519
+
520
+ async executeCommand(
521
+ command: string,
522
+ inputs: Record<string, any>,
523
+ environment: ExecutionEnvironment,
524
+ timeout?: string,
525
+ options?: {
526
+ verbose?: boolean;
527
+ showSpinner?: boolean;
528
+ streamOutput?: boolean;
529
+ },
530
+ ): Promise<CommandResult> {
531
+ const verbose = options?.verbose ?? false;
532
+ const showSpinner = options?.showSpinner ?? false;
533
+
534
+ // Create abort controller for this execution
535
+ this.abortController = new AbortController();
536
+
537
+ // Start spinner if requested
538
+ let spinner: any = null;
539
+ if (showSpinner) {
540
+ try {
541
+ const p = require("@clack/prompts");
542
+ spinner = p.spinner();
543
+ spinner.start("Executing Enact tool in container...");
544
+ } catch (e) {
545
+ console.log("Executing Enact tool in container...");
546
+ }
547
+ }
548
+
549
+ try {
550
+ // Substitute template variables in command (Enact Protocol style)
551
+ const substitutedCommand = this.substituteCommandVariables(
552
+ command,
553
+ inputs,
554
+ );
555
+
556
+ if (verbose) {
557
+ try {
558
+ const pc = require("picocolors");
559
+ console.error(
560
+ pc.cyan("\n🐳 Executing Enact command in Dagger container:"),
561
+ );
562
+ console.error(pc.white(substitutedCommand));
563
+ console.error(pc.gray(`Base image: ${this.options.baseImage}`));
564
+ } catch (e) {
565
+ console.error("\n🐳 Executing Enact command in Dagger container:");
566
+ console.error(substitutedCommand);
567
+ console.error(`Base image: ${this.options.baseImage}`);
568
+ }
569
+ }
570
+
571
+ // Parse and apply timeout with engine timeout consideration
572
+ const timeoutMs = timeout ? parseTimeout(timeout) : 30000;
573
+ const effectiveTimeout = Math.max(timeoutMs, this.options.engineTimeout!);
574
+ logger.debug(
575
+ `Parsed timeout: ${effectiveTimeout}ms (command: ${timeoutMs}ms, engine: ${this.options.engineTimeout}ms)`,
576
+ );
577
+
578
+ // Execute command with enhanced error handling and timeout management
579
+ const result = await Promise.race([
580
+ this.executeWithConnect(substitutedCommand, environment, inputs),
581
+ this.createTimeoutPromise(effectiveTimeout),
582
+ ]);
583
+
584
+ if (spinner) {
585
+ spinner.stop("✅ Enact tool execution completed");
586
+ }
587
+
588
+ return result;
589
+ } catch (error) {
590
+ if (spinner) {
591
+ spinner.stop("❌ Enact tool execution failed");
592
+ }
593
+
594
+ // Enhanced timeout handling
595
+ if (
596
+ error instanceof Error &&
597
+ (error.message === "TIMEOUT" || error.message.includes("timed out"))
598
+ ) {
599
+ // Mark engine as potentially unhealthy after timeout
600
+ this.engineHealth.consecutiveFailures++;
601
+ throw new Error(
602
+ `Command timed out after ${timeout || "30s"} - consider increasing timeout or checking engine health`,
603
+ );
604
+ }
605
+
606
+ // Handle connection errors specifically
607
+ if (
608
+ error instanceof Error &&
609
+ error.message.includes("buildkit failed to respond")
610
+ ) {
611
+ this.engineHealth.consecutiveFailures++;
612
+ throw new Error(
613
+ "Dagger engine connection failed - engine may need reset",
614
+ );
615
+ }
616
+
617
+ throw error;
618
+ } finally {
619
+ this.abortController = null;
620
+ }
621
+ }
622
+
623
+ /**
624
+ * Execute command using Dagger connect with proper session management
625
+ */
626
+ private async executeWithConnect(
627
+ command: string,
628
+ environment: ExecutionEnvironment,
629
+ inputs: Record<string, any>,
630
+ ): Promise<CommandResult> {
631
+ return new Promise<CommandResult>((resolve, reject) => {
632
+ // Setup abort handling
633
+ const abortHandler = () => {
634
+ reject(new Error("Execution aborted"));
635
+ };
636
+
637
+ if (this.abortController) {
638
+ this.abortController.signal.addEventListener("abort", abortHandler);
639
+ }
640
+
641
+ connect(async (client: Client) => {
642
+ try {
643
+ logger.debug("🔗 Connected to Dagger client");
644
+ const container = await this.setupContainer(
645
+ client,
646
+ environment,
647
+ inputs,
648
+ );
649
+ logger.debug("📦 Container setup complete");
650
+ const commandResult = await this.executeInContainer(
651
+ container,
652
+ command,
653
+ );
654
+ logger.debug("⚡ Command execution complete");
655
+
656
+ // Remove abort handler on success
657
+ if (this.abortController) {
658
+ this.abortController.signal.removeEventListener(
659
+ "abort",
660
+ abortHandler,
661
+ );
662
+ }
663
+
664
+ resolve(commandResult);
665
+ } catch (error) {
666
+ logger.error("❌ Error in Dagger execution:", error);
667
+
668
+ // Remove abort handler on error
669
+ if (this.abortController) {
670
+ this.abortController.signal.removeEventListener(
671
+ "abort",
672
+ abortHandler,
673
+ );
674
+ }
675
+
676
+ reject(error);
677
+ }
678
+ }).catch((error) => {
679
+ logger.error("❌ Error in Dagger connect:", error);
680
+
681
+ // Remove abort handler on connection error
682
+ if (this.abortController) {
683
+ this.abortController.signal.removeEventListener(
684
+ "abort",
685
+ abortHandler,
686
+ );
687
+ }
688
+
689
+ reject(error);
690
+ });
691
+ });
692
+ }
693
+
694
+ /**
695
+ * Enhanced container setup with better tool detection and installation
696
+ */
697
+ private async setupContainer(
698
+ client: Client,
699
+ environment: ExecutionEnvironment,
700
+ inputs: Record<string, any>,
701
+ ): Promise<Container> {
702
+ logger.debug(
703
+ `🚀 Setting up container with base image: ${this.options.baseImage}`,
704
+ );
705
+
706
+ // Start with base container
707
+ let container = client.container().from(this.options.baseImage!);
708
+ logger.debug("📦 Base container created");
709
+
710
+ // Set working directory
711
+ container = container.withWorkdir(this.options.workdir!);
712
+ logger.debug(`📁 Working directory set to: ${this.options.workdir}`);
713
+
714
+ // Add environment variables from Enact tool env config
715
+ for (const [key, value] of Object.entries(environment.vars)) {
716
+ container = container.withEnvVariable(key, String(value));
717
+ }
718
+ logger.debug(
719
+ `🌍 Added ${Object.keys(environment.vars).length} environment variables`,
720
+ );
721
+
722
+ // Install common tools needed for Enact commands
723
+ if (this.options.enableNetwork) {
724
+ container = await this.installCommonTools(container);
725
+ logger.debug("🔧 Common tools installed");
726
+ } else {
727
+ logger.debug("🔧 Skipping common tools installation (network disabled)");
728
+ }
729
+
730
+ // Create input files if needed (Enact Protocol supports file inputs)
731
+ container = await this.prepareInputFiles(container, inputs);
732
+ logger.debug("📄 Input files prepared");
733
+
734
+ // Apply resource limits if specified
735
+ if (environment.resources) {
736
+ container = this.applyResourceLimits(container, environment.resources);
737
+ logger.debug("💾 Resource limits applied");
738
+ }
739
+
740
+ logger.debug("✅ Container setup complete");
741
+ return container;
742
+ }
743
+
744
+ /**
745
+ * Install common tools that Enact commands might need
746
+ * Enhanced with better error handling and timeout
747
+ */
748
+ private async installCommonTools(container: Container): Promise<Container> {
749
+ logger.debug(
750
+ `🔧 Installing common tools for base image: ${this.options.baseImage}`,
751
+ );
752
+
753
+ try {
754
+ // For node images, most tools are already available, so we can skip installation
755
+ if (this.options.baseImage?.includes("node:")) {
756
+ logger.debug(
757
+ "📦 Node.js image detected, skipping tool installation (most tools already available)",
758
+ );
759
+ return container;
760
+ }
761
+
762
+ // Determine package manager based on base image
763
+ const isAlpine = this.options.baseImage?.includes("alpine");
764
+ const isDebian =
765
+ this.options.baseImage?.includes("debian") ||
766
+ this.options.baseImage?.includes("ubuntu");
767
+
768
+ if (isAlpine) {
769
+ logger.debug("📦 Detected Alpine Linux, installing basic tools");
770
+ // Alpine Linux uses apk package manager - with better timeout handling
771
+ container = container.withExec([
772
+ "sh",
773
+ "-c",
774
+ 'timeout 60 apk update --no-cache && timeout 60 apk add --no-cache curl wget git || echo "Package installation failed, continuing..."',
775
+ ]);
776
+ } else if (isDebian) {
777
+ logger.debug("📦 Detected Debian/Ubuntu, installing basic tools");
778
+ // Debian/Ubuntu uses apt-get - with better timeout and error handling
779
+ container = container.withExec([
780
+ "sh",
781
+ "-c",
782
+ 'timeout 60 apt-get update && timeout 60 apt-get install -y curl wget git && rm -rf /var/lib/apt/lists/* || echo "Package installation failed, continuing..."',
783
+ ]);
784
+ } else {
785
+ logger.warn(
786
+ `Unknown base image ${this.options.baseImage}, skipping tool installation`,
787
+ );
788
+ }
789
+
790
+ logger.debug("✅ Common tools installation complete");
791
+ return container;
792
+ } catch (error) {
793
+ logger.warn(
794
+ `⚠️ Tool installation failed, continuing without additional tools: ${error}`,
795
+ );
796
+ return container;
797
+ }
798
+ }
799
+
800
+ /**
801
+ * Execute command in container with enhanced error handling
802
+ */
803
+ private async executeInContainer(
804
+ container: Container,
805
+ command: string,
806
+ ): Promise<CommandResult> {
807
+ logger.debug(`⚡ Executing command in container: ${command}`);
808
+
809
+ try {
810
+ let execContainer: Container;
811
+
812
+ if (this.options.useShell) {
813
+ logger.debug("🐚 Using shell wrapper for command execution");
814
+ execContainer = container.withExec(["sh", "-c", command]);
815
+ } else {
816
+ logger.debug("📋 Using direct command execution");
817
+ const commandParts = this.parseCommand(command);
818
+ execContainer = container.withExec(commandParts);
819
+ }
820
+
821
+ logger.debug("📤 Getting stdout from container...");
822
+ const stdout = await execContainer.stdout();
823
+ logger.debug(`📥 Got stdout: ${stdout.length} characters`);
824
+
825
+ let stderr = "";
826
+ try {
827
+ logger.debug("📤 Getting stderr from container...");
828
+ stderr = await execContainer.stderr();
829
+ logger.debug(`📥 Got stderr: ${stderr.length} characters`);
830
+ } catch (e) {
831
+ logger.debug(
832
+ "📥 stderr not available (this is normal for successful commands)",
833
+ );
834
+ }
835
+
836
+ logger.debug("✅ Command executed successfully");
837
+ return {
838
+ stdout,
839
+ stderr,
840
+ exitCode: 0,
841
+ };
842
+ } catch (error) {
843
+ logger.debug(`❌ Command execution failed: ${error}`);
844
+ const errorMessage =
845
+ error instanceof Error ? error.message : "Command execution failed";
846
+ const parsedError = this.parseExecutionError(errorMessage);
847
+
848
+ return {
849
+ stdout: parsedError.stdout || "",
850
+ stderr: parsedError.stderr || errorMessage,
851
+ exitCode: parsedError.exitCode || 1,
852
+ };
853
+ }
854
+ }
855
+
856
+ /**
857
+ * Enhanced execution error parsing
858
+ */
859
+ private parseExecutionError(errorMessage: string): Partial<CommandResult> {
860
+ const result: Partial<CommandResult> = {};
861
+
862
+ const exitCodeMatch = errorMessage.match(/exit code:?\s*(\d+)/);
863
+ if (exitCodeMatch) {
864
+ result.exitCode = parseInt(exitCodeMatch[1], 10);
865
+ }
866
+
867
+ const stdoutMatch = errorMessage.match(
868
+ /(?:stdout|Stdout):\s*\n([\s\S]*?)(?:\n(?:stderr|Stderr):|$)/i,
869
+ );
870
+ if (stdoutMatch) {
871
+ result.stdout = stdoutMatch[1].trim();
872
+ }
873
+
874
+ const stderrMatch = errorMessage.match(
875
+ /(?:stderr|Stderr):\s*\n([\s\S]*)$/i,
876
+ );
877
+ if (stderrMatch) {
878
+ result.stderr = stderrMatch[1].trim();
879
+ }
880
+
881
+ if (!result.stderr && !result.stdout) {
882
+ result.stderr = errorMessage;
883
+ }
884
+
885
+ if (!result.exitCode) {
886
+ result.exitCode = 1;
887
+ }
888
+
889
+ return result;
890
+ }
891
+
892
+ /**
893
+ * Apply resource limits based on Enact tool specifications
894
+ */
895
+ private applyResourceLimits(container: Container, resources: any): Container {
896
+ if (resources.memory) {
897
+ logger.info(
898
+ `Resource limit requested: memory=${resources.memory} (not yet supported by Dagger)`,
899
+ );
900
+ }
901
+ if (resources.cpu) {
902
+ logger.info(
903
+ `Resource limit requested: cpu=${resources.cpu} (not yet supported by Dagger)`,
904
+ );
905
+ }
906
+ return container;
907
+ }
908
+
909
+ /**
910
+ * Substitute template variables in Enact commands with enhanced security
911
+ */
912
+ private substituteCommandVariables(
913
+ command: string,
914
+ inputs: Record<string, any>,
915
+ ): string {
916
+ let substitutedCommand = command;
917
+
918
+ for (const [key, value] of Object.entries(inputs)) {
919
+ const templateVar = `\${${key}}`;
920
+ let substitutionValue: string;
921
+
922
+ if (typeof value === "string") {
923
+ substitutionValue = this.escapeShellArg(value);
924
+ } else if (typeof value === "object") {
925
+ substitutionValue = this.escapeShellArg(JSON.stringify(value));
926
+ } else {
927
+ substitutionValue = this.escapeShellArg(String(value));
928
+ }
929
+
930
+ substitutedCommand = substitutedCommand.replace(
931
+ new RegExp(`\\$\\{${key}\\}`, "g"),
932
+ substitutionValue,
933
+ );
934
+ }
935
+
936
+ return substitutedCommand;
937
+ }
938
+
939
+ /**
940
+ * Enhanced shell argument escaping
941
+ */
942
+ private escapeShellArg(arg: string): string {
943
+ // For maximum safety, use single quotes and escape any single quotes within
944
+ return "'" + arg.replace(/'/g, "'\"'\"'") + "'";
945
+ }
946
+
947
+ /**
948
+ * Prepare input files for Enact tools that expect file inputs
949
+ */
950
+ private async prepareInputFiles(
951
+ container: Container,
952
+ inputs: Record<string, any>,
953
+ ): Promise<Container> {
954
+ for (const [key, value] of Object.entries(inputs)) {
955
+ if (typeof value === "string" && this.looksLikeFileContent(key, value)) {
956
+ const fileName = this.getInputFileName(key, value);
957
+ const filePath = `${this.options.workdir}/${fileName}`;
958
+
959
+ container = container.withNewFile(filePath, { contents: value });
960
+ logger.debug(`📁 Added input file for Enact tool: ${filePath}`);
961
+ }
962
+ }
963
+
964
+ return container;
965
+ }
966
+
967
+ /**
968
+ * Heuristics to determine if input should be treated as file content
969
+ */
970
+ private looksLikeFileContent(key: string, value: string): boolean {
971
+ const lowerKey = key.toLowerCase();
972
+ return (
973
+ lowerKey.includes("file") ||
974
+ lowerKey.includes("content") ||
975
+ lowerKey.includes("data") ||
976
+ lowerKey.includes("source") ||
977
+ (lowerKey.includes("input") && value.length > 100) ||
978
+ value.includes("\n") ||
979
+ value.startsWith("data:") ||
980
+ this.hasCommonFileExtensions(value)
981
+ );
982
+ }
983
+
984
+ /**
985
+ * Check if content looks like common file types
986
+ */
987
+ private hasCommonFileExtensions(value: string): boolean {
988
+ const trimmed = value.trim();
989
+ return (
990
+ (trimmed.startsWith("{") && trimmed.endsWith("}")) ||
991
+ (trimmed.startsWith("<") && trimmed.includes(">")) ||
992
+ trimmed.startsWith("#") ||
993
+ /^---\s*\n/.test(trimmed)
994
+ );
995
+ }
996
+
997
+ /**
998
+ * Generate appropriate filename for input content
999
+ */
1000
+ private getInputFileName(key: string, value: string): string {
1001
+ const lowerKey = key.toLowerCase();
1002
+ const trimmedValue = value.trim();
1003
+
1004
+ if (lowerKey.includes("markdown") || lowerKey.includes("md"))
1005
+ return `${key}.md`;
1006
+ if (lowerKey.includes("json")) return `${key}.json`;
1007
+ if (lowerKey.includes("yaml") || lowerKey.includes("yml"))
1008
+ return `${key}.yaml`;
1009
+ if (lowerKey.includes("html")) return `${key}.html`;
1010
+ if (lowerKey.includes("css")) return `${key}.css`;
1011
+ if (lowerKey.includes("js") || lowerKey.includes("javascript"))
1012
+ return `${key}.js`;
1013
+ if (lowerKey.includes("python") || lowerKey.includes("py"))
1014
+ return `${key}.py`;
1015
+
1016
+ if (trimmedValue.startsWith("#")) return `${key}.md`;
1017
+ if (trimmedValue.startsWith("{") && trimmedValue.endsWith("}"))
1018
+ return `${key}.json`;
1019
+ if (/^---\s*\n/.test(trimmedValue)) return `${key}.yaml`;
1020
+ if (trimmedValue.includes("<html")) return `${key}.html`;
1021
+
1022
+ if (trimmedValue.startsWith("data:")) {
1023
+ const mimeMatch = trimmedValue.match(/^data:([^;]+)/);
1024
+ if (mimeMatch) {
1025
+ const ext = this.getExtensionFromMimeType(mimeMatch[1]);
1026
+ return `${key}${ext}`;
1027
+ }
1028
+ }
1029
+
1030
+ return `${key}.txt`;
1031
+ }
1032
+
1033
+ /**
1034
+ * Map MIME types to file extensions
1035
+ */
1036
+ private getExtensionFromMimeType(mimeType: string): string {
1037
+ const mimeMap: Record<string, string> = {
1038
+ "text/plain": ".txt",
1039
+ "text/markdown": ".md",
1040
+ "text/html": ".html",
1041
+ "text/css": ".css",
1042
+ "application/json": ".json",
1043
+ "application/javascript": ".js",
1044
+ "text/javascript": ".js",
1045
+ "application/yaml": ".yaml",
1046
+ "text/yaml": ".yaml",
1047
+ "text/x-python": ".py",
1048
+ "application/x-python-code": ".py",
1049
+ "image/png": ".png",
1050
+ "image/jpeg": ".jpg",
1051
+ "image/gif": ".gif",
1052
+ "image/svg+xml": ".svg",
1053
+ };
1054
+
1055
+ return mimeMap[mimeType] || ".txt";
1056
+ }
1057
+
1058
+ /**
1059
+ * Enhanced command parsing for non-shell execution
1060
+ */
1061
+ private parseCommand(command: string): string[] {
1062
+ const args: string[] = [];
1063
+ let current = "";
1064
+ let inQuotes = false;
1065
+ let quoteChar = "";
1066
+
1067
+ for (let i = 0; i < command.length; i++) {
1068
+ const char = command[i];
1069
+
1070
+ if ((char === '"' || char === "'") && !inQuotes) {
1071
+ inQuotes = true;
1072
+ quoteChar = char;
1073
+ } else if (char === quoteChar && inQuotes) {
1074
+ inQuotes = false;
1075
+ quoteChar = "";
1076
+ } else if (char === " " && !inQuotes) {
1077
+ if (current) {
1078
+ args.push(current);
1079
+ current = "";
1080
+ }
1081
+ } else {
1082
+ current += char;
1083
+ }
1084
+ }
1085
+
1086
+ if (current) {
1087
+ args.push(current);
1088
+ }
1089
+
1090
+ return args;
1091
+ }
1092
+
1093
+ /**
1094
+ * Enhanced timeout promise with abort signal support
1095
+ */
1096
+ private createTimeoutPromise(timeoutMs: number): Promise<never> {
1097
+ return new Promise((_, reject) => {
1098
+ const timeoutId = setTimeout(() => {
1099
+ reject(new Error("TIMEOUT"));
1100
+ }, timeoutMs);
1101
+
1102
+ // Clear timeout if aborted
1103
+ if (this.abortController) {
1104
+ this.abortController.signal.addEventListener("abort", () => {
1105
+ clearTimeout(timeoutId);
1106
+ reject(new Error("ABORTED"));
1107
+ });
1108
+ }
1109
+ });
1110
+ }
1111
+
1112
+ /**
1113
+ * Parse command output according to Enact tool output schema
1114
+ */
1115
+ private parseOutput(stdout: string, tool: EnactTool): any {
1116
+ if (!stdout.trim()) {
1117
+ return null;
1118
+ }
1119
+
1120
+ if (tool.outputSchema) {
1121
+ try {
1122
+ const parsed = JSON.parse(stdout);
1123
+ // TODO: Validate against outputSchema if validation library is available
1124
+ return parsed;
1125
+ } catch {
1126
+ logger.warn(
1127
+ `Tool ${tool.name} has outputSchema but produced non-JSON output`,
1128
+ );
1129
+ return stdout;
1130
+ }
1131
+ }
1132
+
1133
+ try {
1134
+ return JSON.parse(stdout);
1135
+ } catch {
1136
+ return stdout;
1137
+ }
1138
+ }
1139
+
1140
+ /**
1141
+ * Execute command with exec.ts style interface for backwards compatibility
1142
+ */
1143
+ async executeCommandExecStyle(
1144
+ command: string,
1145
+ timeout: string,
1146
+ verbose: boolean = false,
1147
+ envVars: Record<string, string> = {},
1148
+ ): Promise<void> {
1149
+ const environment: ExecutionEnvironment = {
1150
+ vars: envVars,
1151
+ resources: { timeout },
1152
+ };
1153
+
1154
+ const result = await this.executeCommand(
1155
+ command,
1156
+ {},
1157
+ environment,
1158
+ timeout,
1159
+ {
1160
+ verbose,
1161
+ showSpinner: true,
1162
+ streamOutput: false,
1163
+ },
1164
+ );
1165
+
1166
+ if (result.exitCode !== 0) {
1167
+ throw new Error(
1168
+ `Command failed with exit code ${result.exitCode}: ${result.stderr}`,
1169
+ );
1170
+ }
1171
+ }
1172
+
1173
+ /**
1174
+ * Enhanced cleanup handlers with graceful shutdown
1175
+ */
1176
+ private registerCleanupHandlers(): void {
1177
+ const cleanup = () => {
1178
+ if (!this.isShuttingDown) {
1179
+ this.gracefulShutdown();
1180
+ }
1181
+ };
1182
+
1183
+ // Register multiple signal handlers for comprehensive cleanup
1184
+ process.once("SIGTERM", cleanup);
1185
+ process.once("SIGINT", cleanup);
1186
+ process.once("SIGUSR2", cleanup); // For nodemon
1187
+ process.once("exit", cleanup);
1188
+
1189
+ // Handle unhandled promise rejections
1190
+ process.once("unhandledRejection", (reason, promise) => {
1191
+ logger.error("Unhandled Rejection at:", promise, "reason:", reason);
1192
+ cleanup();
1193
+ });
1194
+
1195
+ // Handle uncaught exceptions
1196
+ process.once("uncaughtException", (error) => {
1197
+ logger.error("Uncaught Exception:", error);
1198
+ cleanup();
1199
+ });
1200
+ }
1201
+
1202
+ /**
1203
+ * Graceful shutdown with proper async cleanup
1204
+ */
1205
+ private async gracefulShutdown(): Promise<void> {
1206
+ this.isShuttingDown = true;
1207
+
1208
+ try {
1209
+ logger.info("🔄 Starting graceful shutdown...");
1210
+
1211
+ // Cancel any active operations
1212
+ if (this.abortController) {
1213
+ this.abortController.abort();
1214
+ }
1215
+
1216
+ // Wait for active sessions with timeout
1217
+ await this.waitForActiveSessions(10000);
1218
+
1219
+ // Perform comprehensive cleanup
1220
+ await this.cleanup();
1221
+
1222
+ logger.info("✅ Graceful shutdown completed");
1223
+ process.exit(0);
1224
+ } catch (error) {
1225
+ logger.error("❌ Error during graceful shutdown:", error);
1226
+ process.exit(1);
1227
+ }
1228
+ }
1229
+
1230
+ /**
1231
+ * Enhanced force cleanup for synchronous exit handlers
1232
+ */
1233
+ private forceCleanup(): void {
1234
+ if (this.isShuttingDown) return;
1235
+
1236
+ try {
1237
+ logger.info("🔄 Force cleaning up Dagger engines...");
1238
+
1239
+ const result = spawnSync(
1240
+ "docker",
1241
+ [
1242
+ "ps",
1243
+ "--all",
1244
+ "--filter",
1245
+ "name=dagger-engine",
1246
+ "--format",
1247
+ "{{.Names}}",
1248
+ ],
1249
+ {
1250
+ encoding: "utf8",
1251
+ timeout: 5000,
1252
+ },
1253
+ );
1254
+
1255
+ if (result.stdout) {
1256
+ const names = result.stdout
1257
+ .trim()
1258
+ .split("\n")
1259
+ .filter((n: string) => n.trim());
1260
+ if (names.length > 0) {
1261
+ logger.info(
1262
+ `Found ${names.length} engine containers, force removing...`,
1263
+ );
1264
+ for (const name of names) {
1265
+ spawnSync("docker", ["rm", "-f", name.trim()], { timeout: 3000 });
1266
+ }
1267
+ logger.info("✅ Force cleanup completed");
1268
+ }
1269
+ }
1270
+ } catch (error) {
1271
+ logger.debug("Force cleanup failed (this is usually fine):", error);
1272
+ }
1273
+ }
1274
+
1275
+ /**
1276
+ * Get current engine status for debugging
1277
+ */
1278
+ public getEngineStatus(): {
1279
+ health: EngineHealthStatus;
1280
+ activeSessions: number;
1281
+ isShuttingDown: boolean;
1282
+ } {
1283
+ return {
1284
+ health: { ...this.engineHealth },
1285
+ activeSessions: this.activeSessions.size,
1286
+ isShuttingDown: this.isShuttingDown,
1287
+ };
1288
+ }
1289
+
1290
+ /**
1291
+ * Manually trigger engine reset (for debugging/testing)
1292
+ */
1293
+ public async resetEngine(): Promise<void> {
1294
+ logger.info("🔄 Manual engine reset triggered...");
1295
+ await this.resetEngineContainer();
1296
+ }
1297
+
1298
+ /**
1299
+ * Check if provider is ready for new executions
1300
+ */
1301
+ public isReady(): boolean {
1302
+ return (
1303
+ !this.isShuttingDown &&
1304
+ this.engineHealth.isHealthy &&
1305
+ this.engineHealth.consecutiveFailures < 3
1306
+ );
1307
+ }
1308
+ }