@enactprotocol/shared 1.2.13 → 2.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 (134) hide show
  1. package/README.md +44 -0
  2. package/package.json +16 -58
  3. package/src/config.ts +476 -0
  4. package/src/constants.ts +36 -0
  5. package/src/execution/command.ts +314 -0
  6. package/src/execution/index.ts +73 -0
  7. package/src/execution/runtime.ts +308 -0
  8. package/src/execution/types.ts +379 -0
  9. package/src/execution/validation.ts +508 -0
  10. package/src/index.ts +237 -30
  11. package/src/manifest/index.ts +36 -0
  12. package/src/manifest/loader.ts +187 -0
  13. package/src/manifest/parser.ts +173 -0
  14. package/src/manifest/validator.ts +309 -0
  15. package/src/paths.ts +108 -0
  16. package/src/registry.ts +219 -0
  17. package/src/resolver.ts +345 -0
  18. package/src/types/index.ts +30 -0
  19. package/src/types/manifest.ts +255 -0
  20. package/src/types.ts +5 -188
  21. package/src/utils/fs.ts +281 -0
  22. package/src/utils/logger.ts +270 -59
  23. package/src/utils/version.ts +304 -36
  24. package/tests/config.test.ts +515 -0
  25. package/tests/execution/command.test.ts +317 -0
  26. package/tests/execution/validation.test.ts +384 -0
  27. package/tests/fixtures/invalid-tool.yaml +4 -0
  28. package/tests/fixtures/valid-tool.md +62 -0
  29. package/tests/fixtures/valid-tool.yaml +40 -0
  30. package/tests/index.test.ts +8 -0
  31. package/tests/manifest/loader.test.ts +291 -0
  32. package/tests/manifest/parser.test.ts +345 -0
  33. package/tests/manifest/validator.test.ts +394 -0
  34. package/tests/manifest-types.test.ts +358 -0
  35. package/tests/paths.test.ts +153 -0
  36. package/tests/registry.test.ts +231 -0
  37. package/tests/resolver.test.ts +272 -0
  38. package/tests/utils/fs.test.ts +388 -0
  39. package/tests/utils/logger.test.ts +480 -0
  40. package/tests/utils/version.test.ts +390 -0
  41. package/tsconfig.json +12 -0
  42. package/tsconfig.tsbuildinfo +1 -0
  43. package/dist/LocalToolResolver.d.ts +0 -84
  44. package/dist/LocalToolResolver.js +0 -353
  45. package/dist/api/enact-api.d.ts +0 -130
  46. package/dist/api/enact-api.js +0 -428
  47. package/dist/api/index.d.ts +0 -2
  48. package/dist/api/index.js +0 -2
  49. package/dist/api/types.d.ts +0 -103
  50. package/dist/api/types.js +0 -1
  51. package/dist/constants.d.ts +0 -7
  52. package/dist/constants.js +0 -10
  53. package/dist/core/DaggerExecutionProvider.d.ts +0 -169
  54. package/dist/core/DaggerExecutionProvider.js +0 -1029
  55. package/dist/core/DirectExecutionProvider.d.ts +0 -23
  56. package/dist/core/DirectExecutionProvider.js +0 -406
  57. package/dist/core/EnactCore.d.ts +0 -162
  58. package/dist/core/EnactCore.js +0 -597
  59. package/dist/core/NativeExecutionProvider.d.ts +0 -9
  60. package/dist/core/NativeExecutionProvider.js +0 -16
  61. package/dist/core/index.d.ts +0 -3
  62. package/dist/core/index.js +0 -3
  63. package/dist/exec/index.d.ts +0 -3
  64. package/dist/exec/index.js +0 -3
  65. package/dist/exec/logger.d.ts +0 -11
  66. package/dist/exec/logger.js +0 -57
  67. package/dist/exec/validate.d.ts +0 -5
  68. package/dist/exec/validate.js +0 -167
  69. package/dist/index.d.ts +0 -21
  70. package/dist/index.js +0 -25
  71. package/dist/lib/enact-direct.d.ts +0 -150
  72. package/dist/lib/enact-direct.js +0 -159
  73. package/dist/lib/index.d.ts +0 -1
  74. package/dist/lib/index.js +0 -1
  75. package/dist/security/index.d.ts +0 -3
  76. package/dist/security/index.js +0 -3
  77. package/dist/security/security.d.ts +0 -23
  78. package/dist/security/security.js +0 -137
  79. package/dist/security/sign.d.ts +0 -103
  80. package/dist/security/sign.js +0 -666
  81. package/dist/security/verification-enforcer.d.ts +0 -53
  82. package/dist/security/verification-enforcer.js +0 -204
  83. package/dist/services/McpCoreService.d.ts +0 -98
  84. package/dist/services/McpCoreService.js +0 -124
  85. package/dist/services/index.d.ts +0 -1
  86. package/dist/services/index.js +0 -1
  87. package/dist/types.d.ts +0 -132
  88. package/dist/types.js +0 -3
  89. package/dist/utils/config.d.ts +0 -111
  90. package/dist/utils/config.js +0 -342
  91. package/dist/utils/env-loader.d.ts +0 -54
  92. package/dist/utils/env-loader.js +0 -270
  93. package/dist/utils/help.d.ts +0 -36
  94. package/dist/utils/help.js +0 -248
  95. package/dist/utils/index.d.ts +0 -7
  96. package/dist/utils/index.js +0 -7
  97. package/dist/utils/logger.d.ts +0 -35
  98. package/dist/utils/logger.js +0 -75
  99. package/dist/utils/silent-monitor.d.ts +0 -67
  100. package/dist/utils/silent-monitor.js +0 -242
  101. package/dist/utils/timeout.d.ts +0 -5
  102. package/dist/utils/timeout.js +0 -23
  103. package/dist/utils/version.d.ts +0 -4
  104. package/dist/utils/version.js +0 -35
  105. package/dist/web/env-manager-server.d.ts +0 -29
  106. package/dist/web/env-manager-server.js +0 -367
  107. package/dist/web/index.d.ts +0 -1
  108. package/dist/web/index.js +0 -1
  109. package/src/LocalToolResolver.ts +0 -424
  110. package/src/api/enact-api.ts +0 -604
  111. package/src/api/index.ts +0 -2
  112. package/src/api/types.ts +0 -114
  113. package/src/core/DaggerExecutionProvider.ts +0 -1357
  114. package/src/core/DirectExecutionProvider.ts +0 -484
  115. package/src/core/EnactCore.ts +0 -847
  116. package/src/core/index.ts +0 -3
  117. package/src/exec/index.ts +0 -3
  118. package/src/exec/logger.ts +0 -63
  119. package/src/exec/validate.ts +0 -238
  120. package/src/lib/enact-direct.ts +0 -254
  121. package/src/lib/index.ts +0 -1
  122. package/src/services/McpCoreService.ts +0 -201
  123. package/src/services/index.ts +0 -1
  124. package/src/utils/config.ts +0 -438
  125. package/src/utils/env-loader.ts +0 -370
  126. package/src/utils/help.ts +0 -257
  127. package/src/utils/index.ts +0 -7
  128. package/src/utils/silent-monitor.ts +0 -328
  129. package/src/utils/timeout.ts +0 -26
  130. package/src/web/env-manager-server.ts +0 -465
  131. package/src/web/index.ts +0 -1
  132. package/src/web/static/app.js +0 -663
  133. package/src/web/static/index.html +0 -117
  134. package/src/web/static/style.css +0 -291
@@ -1,1029 +0,0 @@
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, undefined, tool);
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, tool) {
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
- // Determine which container image to use - prefer tool's 'from' field over default baseImage
414
- const containerImage = tool?.from || this.options.baseImage;
415
- try {
416
- const pc = require("picocolors");
417
- console.error(pc.cyan("\n🐳 Executing Enact command in Dagger container:"));
418
- console.error(pc.white(substitutedCommand));
419
- console.error(pc.gray(`Container image: ${containerImage}${tool?.from ? ' (from tool.from)' : ' (default baseImage)'}`));
420
- }
421
- catch (e) {
422
- console.error("\n🐳 Executing Enact command in Dagger container:");
423
- console.error(substitutedCommand);
424
- console.error(`Container image: ${containerImage}${tool?.from ? ' (from tool.from)' : ' (default baseImage)'}`);
425
- }
426
- }
427
- // Parse and apply timeout with engine timeout consideration
428
- const timeoutMs = timeout ? parseTimeout(timeout) : 30000;
429
- const effectiveTimeout = Math.max(timeoutMs, this.options.engineTimeout);
430
- logger.debug(`Parsed timeout: ${effectiveTimeout}ms (command: ${timeoutMs}ms, engine: ${this.options.engineTimeout}ms)`);
431
- // Execute command with enhanced error handling and timeout management
432
- const result = await Promise.race([
433
- this.executeWithConnect(substitutedCommand, environment, inputs, tool),
434
- this.createTimeoutPromise(effectiveTimeout),
435
- ]);
436
- if (spinner) {
437
- spinner.stop("✅ Enact tool execution completed");
438
- }
439
- return result;
440
- }
441
- catch (error) {
442
- if (spinner) {
443
- spinner.stop("❌ Enact tool execution failed");
444
- }
445
- // Enhanced timeout handling
446
- if (error instanceof Error &&
447
- (error.message === "TIMEOUT" || error.message.includes("timed out"))) {
448
- // Mark engine as potentially unhealthy after timeout
449
- this.engineHealth.consecutiveFailures++;
450
- throw new Error(`Command timed out after ${timeout || "30s"} - consider increasing timeout or checking engine health`);
451
- }
452
- // Handle connection errors specifically
453
- if (error instanceof Error &&
454
- error.message.includes("buildkit failed to respond")) {
455
- this.engineHealth.consecutiveFailures++;
456
- throw new Error("Dagger engine connection failed - engine may need reset");
457
- }
458
- throw error;
459
- }
460
- finally {
461
- this.abortController = null;
462
- }
463
- }
464
- /**
465
- * Execute command using Dagger connect with proper session management
466
- */
467
- async executeWithConnect(command, environment, inputs, tool) {
468
- return new Promise((resolve, reject) => {
469
- // Setup abort handling
470
- const abortHandler = () => {
471
- reject(new Error("Execution aborted"));
472
- };
473
- if (this.abortController) {
474
- this.abortController.signal.addEventListener("abort", abortHandler);
475
- }
476
- connect(async (client) => {
477
- try {
478
- logger.debug("🔗 Connected to Dagger client");
479
- const container = await this.setupContainer(client, environment, inputs, tool);
480
- logger.debug("📦 Container setup complete");
481
- const commandResult = await this.executeInContainer(container, command);
482
- logger.debug("⚡ Command execution complete");
483
- // Remove abort handler on success
484
- if (this.abortController) {
485
- this.abortController.signal.removeEventListener("abort", abortHandler);
486
- }
487
- resolve(commandResult);
488
- }
489
- catch (error) {
490
- logger.error("❌ Error in Dagger execution:", error);
491
- // Remove abort handler on error
492
- if (this.abortController) {
493
- this.abortController.signal.removeEventListener("abort", abortHandler);
494
- }
495
- reject(error);
496
- }
497
- }).catch((error) => {
498
- logger.error("❌ Error in Dagger connect:", error);
499
- // Remove abort handler on connection error
500
- if (this.abortController) {
501
- this.abortController.signal.removeEventListener("abort", abortHandler);
502
- }
503
- reject(error);
504
- });
505
- });
506
- }
507
- /**
508
- * Setup directory mounting for the container
509
- */
510
- async setupDirectoryMount(client, container, mountSpec) {
511
- try {
512
- // Parse mount specification (format: "localPath" or "localPath:containerPath")
513
- let localPath;
514
- let containerPath;
515
- // Handle Windows drive letters (e.g., C:\path) vs mount separator (:)
516
- const colonIndex = mountSpec.indexOf(':');
517
- if (colonIndex > 0) {
518
- // Check if this might be a Windows drive letter (single letter followed by colon)
519
- const potentialDriveLetter = mountSpec.substring(0, colonIndex);
520
- const isWindowsDrive = potentialDriveLetter.length === 1 && /[A-Za-z]/.test(potentialDriveLetter);
521
- if (isWindowsDrive) {
522
- // Look for the next colon that separates local from container path
523
- const nextColonIndex = mountSpec.indexOf(':', colonIndex + 1);
524
- if (nextColonIndex > 0) {
525
- localPath = mountSpec.substring(0, nextColonIndex);
526
- containerPath = mountSpec.substring(nextColonIndex + 1);
527
- }
528
- else {
529
- // No container path specified, use default
530
- localPath = mountSpec;
531
- containerPath = '/workspace/src';
532
- }
533
- }
534
- else {
535
- // Regular path:container split
536
- localPath = mountSpec.substring(0, colonIndex);
537
- containerPath = mountSpec.substring(colonIndex + 1);
538
- }
539
- }
540
- else if (colonIndex === 0) {
541
- // Starts with colon (e.g., ":/app")
542
- localPath = '';
543
- containerPath = mountSpec.substring(1);
544
- }
545
- else {
546
- localPath = mountSpec;
547
- containerPath = '/workspace/src'; // Default container path
548
- }
549
- // Resolve local path to absolute path
550
- const path = require('path');
551
- const resolvedLocalPath = path.resolve(localPath);
552
- // Check if local directory exists
553
- const fs = require('fs');
554
- if (!fs.existsSync(resolvedLocalPath)) {
555
- throw new Error(`Mount source directory does not exist: ${resolvedLocalPath}`);
556
- }
557
- // Create Directory object from local path
558
- const hostDirectory = client.host().directory(resolvedLocalPath);
559
- // Mount directory in container using withMountedDirectory for better performance
560
- container = container.withMountedDirectory(containerPath, hostDirectory);
561
- logger.debug(`📂 Mounted ${resolvedLocalPath} -> ${containerPath}`);
562
- return container;
563
- }
564
- catch (error) {
565
- logger.error(`Failed to setup directory mount: ${error}`);
566
- throw error;
567
- }
568
- }
569
- /**
570
- * Enhanced container setup with better tool detection and installation
571
- */
572
- async setupContainer(client, environment, inputs, tool) {
573
- // Determine which container image to use - prefer tool's 'from' field over default baseImage
574
- const containerImage = tool?.from || this.options.baseImage;
575
- logger.debug(`🚀 Setting up container with image: ${containerImage}${tool?.from ? ' (from tool.from)' : ' (default baseImage)'}`);
576
- // Start with base container
577
- let container = client.container().from(containerImage);
578
- logger.debug("📦 Base container created");
579
- // Set working directory
580
- container = container.withWorkdir(this.options.workdir);
581
- logger.debug(`📁 Working directory set to: ${this.options.workdir}`);
582
- // Handle directory mounting if specified
583
- if (environment.mount) {
584
- container = await this.setupDirectoryMount(client, container, environment.mount);
585
- }
586
- // Add environment variables from Enact tool env config
587
- for (const [key, value] of Object.entries(environment.vars)) {
588
- container = container.withEnvVariable(key, String(value));
589
- }
590
- logger.debug(`🌍 Added ${Object.keys(environment.vars).length} environment variables`);
591
- // Install common tools needed for Enact commands
592
- if (this.options.enableNetwork) {
593
- container = await this.installCommonTools(container, containerImage);
594
- logger.debug("🔧 Common tools installed");
595
- }
596
- else {
597
- logger.debug("🔧 Skipping common tools installation (network disabled)");
598
- }
599
- // Create input files if needed (Enact Protocol supports file inputs)
600
- container = await this.prepareInputFiles(container, inputs);
601
- logger.debug("📄 Input files prepared");
602
- // Apply resource limits if specified
603
- if (environment.resources) {
604
- container = this.applyResourceLimits(container, environment.resources);
605
- logger.debug("💾 Resource limits applied");
606
- }
607
- logger.debug("✅ Container setup complete");
608
- return container;
609
- }
610
- /**
611
- * Install common tools that Enact commands might need
612
- * Enhanced with better error handling and timeout
613
- */
614
- async installCommonTools(container, containerImage) {
615
- logger.debug(`🔧 Installing common tools for container image: ${containerImage}`);
616
- try {
617
- // For node images, most tools are already available, so we can skip installation
618
- if (containerImage.includes("node:")) {
619
- logger.debug("📦 Node.js image detected, skipping tool installation (most tools already available)");
620
- return container;
621
- }
622
- // Determine package manager based on base image
623
- const isAlpine = containerImage.includes("alpine");
624
- const isDebian = containerImage.includes("debian") ||
625
- containerImage.includes("ubuntu");
626
- if (isAlpine) {
627
- logger.debug("📦 Detected Alpine Linux, installing basic tools");
628
- // Alpine Linux uses apk package manager - with better timeout handling
629
- container = container.withExec([
630
- "sh",
631
- "-c",
632
- 'timeout 60 apk update --no-cache && timeout 60 apk add --no-cache curl wget git || echo "Package installation failed, continuing..."',
633
- ]);
634
- }
635
- else if (isDebian) {
636
- logger.debug("📦 Detected Debian/Ubuntu, installing basic tools");
637
- // Debian/Ubuntu uses apt-get - with better timeout and error handling
638
- container = container.withExec([
639
- "sh",
640
- "-c",
641
- '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..."',
642
- ]);
643
- }
644
- else {
645
- logger.warn(`Unknown container image ${containerImage}, skipping tool installation`);
646
- }
647
- logger.debug("✅ Common tools installation complete");
648
- return container;
649
- }
650
- catch (error) {
651
- logger.warn(`⚠️ Tool installation failed, continuing without additional tools: ${error}`);
652
- return container;
653
- }
654
- }
655
- /**
656
- * Execute command in container with enhanced error handling
657
- */
658
- async executeInContainer(container, command) {
659
- logger.debug(`⚡ Executing command in container: ${command}`);
660
- try {
661
- let execContainer;
662
- if (this.options.useShell) {
663
- logger.debug("🐚 Using shell wrapper for command execution");
664
- execContainer = container.withExec(["sh", "-c", command]);
665
- }
666
- else {
667
- logger.debug("📋 Using direct command execution");
668
- const commandParts = this.parseCommand(command);
669
- execContainer = container.withExec(commandParts);
670
- }
671
- logger.debug("📤 Getting stdout from container...");
672
- const stdout = await execContainer.stdout();
673
- logger.debug(`📥 Got stdout: ${stdout.length} characters`);
674
- let stderr = "";
675
- try {
676
- logger.debug("📤 Getting stderr from container...");
677
- stderr = await execContainer.stderr();
678
- logger.debug(`📥 Got stderr: ${stderr.length} characters`);
679
- }
680
- catch (e) {
681
- logger.debug("📥 stderr not available (this is normal for successful commands)");
682
- }
683
- logger.debug("✅ Command executed successfully");
684
- return {
685
- stdout,
686
- stderr,
687
- exitCode: 0,
688
- };
689
- }
690
- catch (error) {
691
- logger.debug(`❌ Command execution failed: ${error}`);
692
- const errorMessage = error instanceof Error ? error.message : "Command execution failed";
693
- const parsedError = this.parseExecutionError(errorMessage);
694
- return {
695
- stdout: parsedError.stdout || "",
696
- stderr: parsedError.stderr || errorMessage,
697
- exitCode: parsedError.exitCode || 1,
698
- };
699
- }
700
- }
701
- /**
702
- * Enhanced execution error parsing
703
- */
704
- parseExecutionError(errorMessage) {
705
- const result = {};
706
- const exitCodeMatch = errorMessage.match(/exit code:?\s*(\d+)/);
707
- if (exitCodeMatch) {
708
- result.exitCode = parseInt(exitCodeMatch[1], 10);
709
- }
710
- const stdoutMatch = errorMessage.match(/(?:stdout|Stdout):\s*\n([\s\S]*?)(?:\n(?:stderr|Stderr):|$)/i);
711
- if (stdoutMatch) {
712
- result.stdout = stdoutMatch[1].trim();
713
- }
714
- const stderrMatch = errorMessage.match(/(?:stderr|Stderr):\s*\n([\s\S]*)$/i);
715
- if (stderrMatch) {
716
- result.stderr = stderrMatch[1].trim();
717
- }
718
- if (!result.stderr && !result.stdout) {
719
- result.stderr = errorMessage;
720
- }
721
- if (!result.exitCode) {
722
- result.exitCode = 1;
723
- }
724
- return result;
725
- }
726
- /**
727
- * Apply resource limits based on Enact tool specifications
728
- */
729
- applyResourceLimits(container, resources) {
730
- if (resources.memory) {
731
- logger.info(`Resource limit requested: memory=${resources.memory} (not yet supported by Dagger)`);
732
- }
733
- if (resources.cpu) {
734
- logger.info(`Resource limit requested: cpu=${resources.cpu} (not yet supported by Dagger)`);
735
- }
736
- return container;
737
- }
738
- /**
739
- * Substitute template variables in Enact commands with enhanced security
740
- */
741
- substituteCommandVariables(command, inputs) {
742
- let substitutedCommand = command;
743
- for (const [key, value] of Object.entries(inputs)) {
744
- const templateVar = `\${${key}}`;
745
- let substitutionValue;
746
- if (typeof value === "string") {
747
- substitutionValue = this.escapeShellArg(value);
748
- }
749
- else if (typeof value === "object") {
750
- substitutionValue = this.escapeShellArg(JSON.stringify(value));
751
- }
752
- else {
753
- substitutionValue = this.escapeShellArg(String(value));
754
- }
755
- substitutedCommand = substitutedCommand.replace(new RegExp(`\\$\\{${key}\\}`, "g"), substitutionValue);
756
- }
757
- return substitutedCommand;
758
- }
759
- /**
760
- * Enhanced shell argument escaping
761
- */
762
- escapeShellArg(arg) {
763
- // For maximum safety, use single quotes and escape any single quotes within
764
- return "'" + arg.replace(/'/g, "'\"'\"'") + "'";
765
- }
766
- /**
767
- * Prepare input files for Enact tools that expect file inputs
768
- */
769
- async prepareInputFiles(container, inputs) {
770
- for (const [key, value] of Object.entries(inputs)) {
771
- if (typeof value === "string" && this.looksLikeFileContent(key, value)) {
772
- const fileName = this.getInputFileName(key, value);
773
- const filePath = `${this.options.workdir}/${fileName}`;
774
- container = container.withNewFile(filePath, { contents: value });
775
- logger.debug(`📁 Added input file for Enact tool: ${filePath}`);
776
- }
777
- }
778
- return container;
779
- }
780
- /**
781
- * Heuristics to determine if input should be treated as file content
782
- */
783
- looksLikeFileContent(key, value) {
784
- const lowerKey = key.toLowerCase();
785
- return (lowerKey.includes("file") ||
786
- lowerKey.includes("content") ||
787
- lowerKey.includes("data") ||
788
- lowerKey.includes("source") ||
789
- (lowerKey.includes("input") && value.length > 100) ||
790
- value.includes("\n") ||
791
- value.startsWith("data:") ||
792
- this.hasCommonFileExtensions(value));
793
- }
794
- /**
795
- * Check if content looks like common file types
796
- */
797
- hasCommonFileExtensions(value) {
798
- const trimmed = value.trim();
799
- return ((trimmed.startsWith("{") && trimmed.endsWith("}")) ||
800
- (trimmed.startsWith("<") && trimmed.includes(">")) ||
801
- trimmed.startsWith("#") ||
802
- /^---\s*\n/.test(trimmed));
803
- }
804
- /**
805
- * Generate appropriate filename for input content
806
- */
807
- getInputFileName(key, value) {
808
- const lowerKey = key.toLowerCase();
809
- const trimmedValue = value.trim();
810
- if (lowerKey.includes("markdown") || lowerKey.includes("md"))
811
- return `${key}.md`;
812
- if (lowerKey.includes("json"))
813
- return `${key}.json`;
814
- if (lowerKey.includes("yaml") || lowerKey.includes("yml"))
815
- return `${key}.yaml`;
816
- if (lowerKey.includes("html"))
817
- return `${key}.html`;
818
- if (lowerKey.includes("css"))
819
- return `${key}.css`;
820
- if (lowerKey.includes("js") || lowerKey.includes("javascript"))
821
- return `${key}.js`;
822
- if (lowerKey.includes("python") || lowerKey.includes("py"))
823
- return `${key}.py`;
824
- if (trimmedValue.startsWith("#"))
825
- return `${key}.md`;
826
- if (trimmedValue.startsWith("{") && trimmedValue.endsWith("}"))
827
- return `${key}.json`;
828
- if (/^---\s*\n/.test(trimmedValue))
829
- return `${key}.yaml`;
830
- if (trimmedValue.includes("<html"))
831
- return `${key}.html`;
832
- if (trimmedValue.startsWith("data:")) {
833
- const mimeMatch = trimmedValue.match(/^data:([^;]+)/);
834
- if (mimeMatch) {
835
- const ext = this.getExtensionFromMimeType(mimeMatch[1]);
836
- return `${key}${ext}`;
837
- }
838
- }
839
- return `${key}.txt`;
840
- }
841
- /**
842
- * Map MIME types to file extensions
843
- */
844
- getExtensionFromMimeType(mimeType) {
845
- const mimeMap = {
846
- "text/plain": ".txt",
847
- "text/markdown": ".md",
848
- "text/html": ".html",
849
- "text/css": ".css",
850
- "application/json": ".json",
851
- "application/javascript": ".js",
852
- "text/javascript": ".js",
853
- "application/yaml": ".yaml",
854
- "text/yaml": ".yaml",
855
- "text/x-python": ".py",
856
- "application/x-python-code": ".py",
857
- "image/png": ".png",
858
- "image/jpeg": ".jpg",
859
- "image/gif": ".gif",
860
- "image/svg+xml": ".svg",
861
- };
862
- return mimeMap[mimeType] || ".txt";
863
- }
864
- /**
865
- * Enhanced command parsing for non-shell execution
866
- */
867
- parseCommand(command) {
868
- const args = [];
869
- let current = "";
870
- let inQuotes = false;
871
- let quoteChar = "";
872
- for (let i = 0; i < command.length; i++) {
873
- const char = command[i];
874
- if ((char === '"' || char === "'") && !inQuotes) {
875
- inQuotes = true;
876
- quoteChar = char;
877
- }
878
- else if (char === quoteChar && inQuotes) {
879
- inQuotes = false;
880
- quoteChar = "";
881
- }
882
- else if (char === " " && !inQuotes) {
883
- if (current) {
884
- args.push(current);
885
- current = "";
886
- }
887
- }
888
- else {
889
- current += char;
890
- }
891
- }
892
- if (current) {
893
- args.push(current);
894
- }
895
- return args;
896
- }
897
- /**
898
- * Enhanced timeout promise with abort signal support
899
- */
900
- createTimeoutPromise(timeoutMs) {
901
- return new Promise((_, reject) => {
902
- const timeoutId = setTimeout(() => {
903
- reject(new Error("TIMEOUT"));
904
- }, timeoutMs);
905
- // Clear timeout if aborted
906
- if (this.abortController) {
907
- this.abortController.signal.addEventListener("abort", () => {
908
- clearTimeout(timeoutId);
909
- reject(new Error("ABORTED"));
910
- });
911
- }
912
- });
913
- }
914
- /**
915
- * Parse command output according to Enact tool output schema
916
- */
917
- parseOutput(stdout, tool) {
918
- if (!stdout.trim()) {
919
- return null;
920
- }
921
- if (tool.outputSchema) {
922
- try {
923
- const parsed = JSON.parse(stdout);
924
- // TODO: Validate against outputSchema if validation library is available
925
- return parsed;
926
- }
927
- catch {
928
- logger.warn(`Tool ${tool.name} has outputSchema but produced non-JSON output`);
929
- return stdout;
930
- }
931
- }
932
- try {
933
- return JSON.parse(stdout);
934
- }
935
- catch {
936
- return stdout;
937
- }
938
- }
939
- /**
940
- * Execute command with exec.ts style interface for backwards compatibility
941
- */
942
- async executeCommandExecStyle(command, timeout, verbose = false, envVars = {}) {
943
- const environment = {
944
- vars: envVars,
945
- resources: { timeout },
946
- };
947
- const result = await this.executeCommand(command, {}, environment, timeout, {
948
- verbose,
949
- showSpinner: true,
950
- streamOutput: false,
951
- }, undefined);
952
- if (result.exitCode !== 0) {
953
- throw new Error(`Command failed with exit code ${result.exitCode}: ${result.stderr}`);
954
- }
955
- }
956
- /**
957
- * Enhanced cleanup handlers with graceful shutdown
958
- */
959
- registerCleanupHandlers() {
960
- const cleanup = () => {
961
- if (!this.isShuttingDown) {
962
- this.gracefulShutdown();
963
- }
964
- };
965
- // Register multiple signal handlers for comprehensive cleanup
966
- process.once("SIGTERM", cleanup);
967
- process.once("SIGINT", cleanup);
968
- process.once("SIGUSR2", cleanup); // For nodemon
969
- process.once("exit", cleanup);
970
- // Handle unhandled promise rejections
971
- process.once("unhandledRejection", (reason, promise) => {
972
- logger.error("Unhandled Rejection at:", promise, "reason:", reason);
973
- cleanup();
974
- });
975
- // Handle uncaught exceptions
976
- process.once("uncaughtException", (error) => {
977
- logger.error("Uncaught Exception:", error);
978
- cleanup();
979
- });
980
- }
981
- /**
982
- * Graceful shutdown with proper async cleanup
983
- */
984
- async gracefulShutdown() {
985
- this.isShuttingDown = true;
986
- try {
987
- logger.info("🔄 Starting graceful shutdown...");
988
- // Cancel any active operations
989
- if (this.abortController) {
990
- this.abortController.abort();
991
- }
992
- // Wait for active sessions with timeout
993
- await this.waitForActiveSessions(10000);
994
- // Perform comprehensive cleanup
995
- await this.cleanup();
996
- logger.info("✅ Graceful shutdown completed");
997
- process.exit(0);
998
- }
999
- catch (error) {
1000
- logger.error("❌ Error during graceful shutdown:", error);
1001
- process.exit(1);
1002
- }
1003
- }
1004
- /**
1005
- * Get current engine status for debugging
1006
- */
1007
- getEngineStatus() {
1008
- return {
1009
- health: { ...this.engineHealth },
1010
- activeSessions: this.activeSessions.size,
1011
- isShuttingDown: this.isShuttingDown,
1012
- };
1013
- }
1014
- /**
1015
- * Manually trigger engine reset (for debugging/testing)
1016
- */
1017
- async resetEngine() {
1018
- logger.info("🔄 Manual engine reset triggered...");
1019
- await this.resetEngineContainer();
1020
- }
1021
- /**
1022
- * Check if provider is ready for new executions
1023
- */
1024
- isReady() {
1025
- return (!this.isShuttingDown &&
1026
- this.engineHealth.isHealthy &&
1027
- this.engineHealth.consecutiveFailures < 3);
1028
- }
1029
- }