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