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