@enactprotocol/execution 2.2.4 → 2.3.1
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/dist/docker-provider.d.ts +87 -0
- package/dist/docker-provider.d.ts.map +1 -0
- package/dist/docker-provider.js +406 -0
- package/dist/docker-provider.js.map +1 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -1
- package/dist/index.js.map +1 -1
- package/dist/local-provider.d.ts +95 -0
- package/dist/local-provider.d.ts.map +1 -0
- package/dist/local-provider.js +369 -0
- package/dist/local-provider.js.map +1 -0
- package/dist/provider.d.ts +24 -1
- package/dist/provider.d.ts.map +1 -1
- package/dist/provider.js +305 -20
- package/dist/provider.js.map +1 -1
- package/dist/remote-provider.d.ts +43 -0
- package/dist/remote-provider.d.ts.map +1 -0
- package/dist/remote-provider.js +154 -0
- package/dist/remote-provider.js.map +1 -0
- package/dist/router.d.ts +62 -0
- package/dist/router.d.ts.map +1 -0
- package/dist/router.js +109 -0
- package/dist/router.js.map +1 -0
- package/package.json +2 -2
- package/src/docker-provider.ts +575 -0
- package/src/index.ts +32 -1
- package/src/local-provider.ts +513 -0
- package/src/provider.ts +409 -28
- package/src/remote-provider.ts +231 -0
- package/src/router.ts +143 -0
- package/tests/docker-provider.test.ts +207 -0
- package/tests/local-provider.test.ts +256 -0
- package/tests/remote-provider.test.ts +206 -0
- package/tests/router.test.ts +272 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,575 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Docker Execution Provider
|
|
3
|
+
*
|
|
4
|
+
* Executes commands inside Docker/Podman/nerdctl containers using direct CLI invocation.
|
|
5
|
+
* Simpler than the Dagger provider — no SDK dependency, just spawns `docker run`.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { spawn } from "node:child_process";
|
|
9
|
+
import { createHash } from "node:crypto";
|
|
10
|
+
import { existsSync } from "node:fs";
|
|
11
|
+
import { join, resolve } from "node:path";
|
|
12
|
+
import type { Action, ActionsManifest, ToolManifest } from "@enactprotocol/shared";
|
|
13
|
+
import {
|
|
14
|
+
applyDefaults,
|
|
15
|
+
detectRuntime,
|
|
16
|
+
getEffectiveInputSchema,
|
|
17
|
+
prepareActionCommand,
|
|
18
|
+
validateInputs,
|
|
19
|
+
} from "@enactprotocol/shared";
|
|
20
|
+
import type {
|
|
21
|
+
ContainerRuntime,
|
|
22
|
+
EngineHealth,
|
|
23
|
+
ExecutionErrorCode,
|
|
24
|
+
ExecutionInput,
|
|
25
|
+
ExecutionMetadata,
|
|
26
|
+
ExecutionOptions,
|
|
27
|
+
ExecutionOutput,
|
|
28
|
+
ExecutionProvider,
|
|
29
|
+
ExecutionResult,
|
|
30
|
+
} from "@enactprotocol/shared";
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Configuration for the Docker execution provider
|
|
34
|
+
*/
|
|
35
|
+
export interface DockerProviderConfig {
|
|
36
|
+
/** Default timeout in milliseconds */
|
|
37
|
+
defaultTimeout?: number;
|
|
38
|
+
/** Enable verbose logging */
|
|
39
|
+
verbose?: boolean;
|
|
40
|
+
/** Preferred container runtime (auto-detected if not set) */
|
|
41
|
+
preferredRuntime?: ContainerRuntime;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Provider for executing commands inside Docker/Podman containers
|
|
46
|
+
*
|
|
47
|
+
* Uses direct CLI invocation (`docker run`, `podman run`, etc.)
|
|
48
|
+
* for simpler, dependency-free container execution.
|
|
49
|
+
*/
|
|
50
|
+
export class DockerExecutionProvider implements ExecutionProvider {
|
|
51
|
+
readonly name = "docker";
|
|
52
|
+
private defaultTimeout: number;
|
|
53
|
+
private verbose: boolean;
|
|
54
|
+
private runtime: ContainerRuntime | null = null;
|
|
55
|
+
|
|
56
|
+
constructor(config: DockerProviderConfig = {}) {
|
|
57
|
+
this.defaultTimeout = config.defaultTimeout ?? 300000; // 5 minutes
|
|
58
|
+
this.verbose = config.verbose ?? false;
|
|
59
|
+
if (config.preferredRuntime) {
|
|
60
|
+
this.runtime = config.preferredRuntime;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async initialize(): Promise<void> {
|
|
65
|
+
if (!this.runtime) {
|
|
66
|
+
const detection = detectRuntime();
|
|
67
|
+
if (detection.found && detection.runtime) {
|
|
68
|
+
this.runtime = detection.runtime;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async isAvailable(): Promise<boolean> {
|
|
74
|
+
const detection = detectRuntime();
|
|
75
|
+
return detection.found;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async getHealth(): Promise<EngineHealth> {
|
|
79
|
+
const detection = detectRuntime();
|
|
80
|
+
return {
|
|
81
|
+
healthy: detection.found,
|
|
82
|
+
runtime: this.runtime ?? "docker",
|
|
83
|
+
consecutiveFailures: 0,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async shutdown(): Promise<void> {
|
|
88
|
+
// Nothing to clean up
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Execute a tool using its manifest.command
|
|
93
|
+
*/
|
|
94
|
+
async execute(
|
|
95
|
+
manifest: ToolManifest,
|
|
96
|
+
input: ExecutionInput,
|
|
97
|
+
options: ExecutionOptions = {}
|
|
98
|
+
): Promise<ExecutionResult> {
|
|
99
|
+
const startTime = new Date();
|
|
100
|
+
const executionId = this.generateExecutionId();
|
|
101
|
+
|
|
102
|
+
if (!manifest.command) {
|
|
103
|
+
return this.createErrorResult(
|
|
104
|
+
manifest.name,
|
|
105
|
+
executionId,
|
|
106
|
+
startTime,
|
|
107
|
+
"COMMAND_ERROR",
|
|
108
|
+
"No command specified in manifest"
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (!this.runtime) {
|
|
113
|
+
return this.createErrorResult(
|
|
114
|
+
manifest.name,
|
|
115
|
+
executionId,
|
|
116
|
+
startTime,
|
|
117
|
+
"CONTAINER_ERROR",
|
|
118
|
+
"No container runtime available"
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const sourceDir = this.resolveSourceDir(options);
|
|
123
|
+
const timeoutMs = this.parseTimeout(options.timeout ?? manifest.timeout);
|
|
124
|
+
const image = await this.resolveImage(manifest, sourceDir);
|
|
125
|
+
|
|
126
|
+
// Merge params as env vars so ${param} shell substitution works in the container
|
|
127
|
+
const env: Record<string, string> = { ...input.envOverrides };
|
|
128
|
+
for (const [key, value] of Object.entries(input.params)) {
|
|
129
|
+
if (value !== undefined && value !== null) {
|
|
130
|
+
env[key] = String(value);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
// Run build commands if specified (e.g., manifest.build or hooks.build)
|
|
136
|
+
if (manifest.build) {
|
|
137
|
+
const buildCommands = Array.isArray(manifest.build) ? manifest.build : [manifest.build];
|
|
138
|
+
for (const cmd of buildCommands) {
|
|
139
|
+
const buildArgs = this.buildRunArgs(image, sourceDir, env, cmd);
|
|
140
|
+
const buildOutput = await this.runContainer(buildArgs, 600000); // 10 min build timeout
|
|
141
|
+
if (buildOutput.exitCode !== 0) {
|
|
142
|
+
return this.createErrorResult(
|
|
143
|
+
manifest.name,
|
|
144
|
+
executionId,
|
|
145
|
+
startTime,
|
|
146
|
+
"BUILD_ERROR",
|
|
147
|
+
`Build failed (exit ${buildOutput.exitCode}): ${buildOutput.stderr}`
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const dockerArgs = this.buildRunArgs(image, sourceDir, env, manifest.command);
|
|
154
|
+
const output = await this.runContainer(dockerArgs, timeoutMs);
|
|
155
|
+
|
|
156
|
+
const endTime = new Date();
|
|
157
|
+
const metadata: ExecutionMetadata = {
|
|
158
|
+
toolName: manifest.name,
|
|
159
|
+
containerImage: image,
|
|
160
|
+
startTime,
|
|
161
|
+
endTime,
|
|
162
|
+
durationMs: endTime.getTime() - startTime.getTime(),
|
|
163
|
+
cached: false,
|
|
164
|
+
executionId,
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
if (output.exitCode !== 0) {
|
|
168
|
+
return {
|
|
169
|
+
success: false,
|
|
170
|
+
output,
|
|
171
|
+
metadata,
|
|
172
|
+
error: {
|
|
173
|
+
code: "COMMAND_ERROR",
|
|
174
|
+
message: `Command exited with code ${output.exitCode}`,
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return { success: true, output, metadata };
|
|
180
|
+
} catch (error) {
|
|
181
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
182
|
+
const code: ExecutionErrorCode = message === "TIMEOUT" ? "TIMEOUT" : "CONTAINER_ERROR";
|
|
183
|
+
return this.createErrorResult(manifest.name, executionId, startTime, code, message);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Execute a raw command in a container
|
|
189
|
+
*/
|
|
190
|
+
async exec(
|
|
191
|
+
manifest: ToolManifest,
|
|
192
|
+
command: string,
|
|
193
|
+
options: ExecutionOptions = {}
|
|
194
|
+
): Promise<ExecutionResult> {
|
|
195
|
+
return this.execute({ ...manifest, command }, { params: {} }, options);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Execute an action from ACTIONS.yaml
|
|
200
|
+
*/
|
|
201
|
+
async executeAction(
|
|
202
|
+
manifest: ToolManifest,
|
|
203
|
+
actionsManifest: ActionsManifest,
|
|
204
|
+
actionName: string,
|
|
205
|
+
action: Action,
|
|
206
|
+
input: ExecutionInput,
|
|
207
|
+
options: ExecutionOptions = {}
|
|
208
|
+
): Promise<ExecutionResult> {
|
|
209
|
+
const startTime = new Date();
|
|
210
|
+
const executionId = this.generateExecutionId();
|
|
211
|
+
const toolLabel = `${manifest.name}:${actionName}`;
|
|
212
|
+
|
|
213
|
+
if (!this.runtime) {
|
|
214
|
+
return this.createErrorResult(
|
|
215
|
+
toolLabel,
|
|
216
|
+
executionId,
|
|
217
|
+
startTime,
|
|
218
|
+
"CONTAINER_ERROR",
|
|
219
|
+
"No container runtime available"
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Validate inputs
|
|
224
|
+
const effectiveSchema = getEffectiveInputSchema(action);
|
|
225
|
+
const validation = validateInputs(input.params, effectiveSchema);
|
|
226
|
+
if (!validation.valid) {
|
|
227
|
+
const errorMessages = validation.errors.map((e) => `${e.path}: ${e.message}`);
|
|
228
|
+
return this.createErrorResult(
|
|
229
|
+
toolLabel,
|
|
230
|
+
executionId,
|
|
231
|
+
startTime,
|
|
232
|
+
"VALIDATION_ERROR",
|
|
233
|
+
`Input validation failed: ${errorMessages.join(", ")}`
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const params = applyDefaults(input.params, effectiveSchema);
|
|
238
|
+
|
|
239
|
+
// Prepare command
|
|
240
|
+
let commandArray: string[];
|
|
241
|
+
try {
|
|
242
|
+
if (typeof action.command === "string") {
|
|
243
|
+
commandArray = action.command.split(/\s+/).filter((s) => s.length > 0);
|
|
244
|
+
} else {
|
|
245
|
+
commandArray = prepareActionCommand(action.command, params, effectiveSchema);
|
|
246
|
+
}
|
|
247
|
+
} catch (error) {
|
|
248
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
249
|
+
return this.createErrorResult(
|
|
250
|
+
toolLabel,
|
|
251
|
+
executionId,
|
|
252
|
+
startTime,
|
|
253
|
+
"COMMAND_ERROR",
|
|
254
|
+
`Failed to prepare action command: ${message}`
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (commandArray.length === 0) {
|
|
259
|
+
return this.createErrorResult(
|
|
260
|
+
toolLabel,
|
|
261
|
+
executionId,
|
|
262
|
+
startTime,
|
|
263
|
+
"COMMAND_ERROR",
|
|
264
|
+
"Action command is empty"
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const sourceDir = this.resolveSourceDir(options);
|
|
269
|
+
const timeoutMs = this.parseTimeout(options.timeout ?? manifest.timeout);
|
|
270
|
+
const image = await this.resolveImage(manifest, sourceDir);
|
|
271
|
+
|
|
272
|
+
// Build environment
|
|
273
|
+
const env: Record<string, string> = { ...input.envOverrides };
|
|
274
|
+
if (actionsManifest.env) {
|
|
275
|
+
for (const [key, envVar] of Object.entries(actionsManifest.env)) {
|
|
276
|
+
if (envVar.default && !env[key]) {
|
|
277
|
+
env[key] = envVar.default;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
try {
|
|
283
|
+
// Run build if needed
|
|
284
|
+
if (actionsManifest.build) {
|
|
285
|
+
const buildCommands = Array.isArray(actionsManifest.build)
|
|
286
|
+
? actionsManifest.build
|
|
287
|
+
: [actionsManifest.build];
|
|
288
|
+
for (const cmd of buildCommands) {
|
|
289
|
+
const buildArgs = this.buildRunArgs(image, sourceDir, env, cmd);
|
|
290
|
+
const buildOutput = await this.runContainer(buildArgs, 600000); // 10 min build timeout
|
|
291
|
+
if (buildOutput.exitCode !== 0) {
|
|
292
|
+
return this.createErrorResult(
|
|
293
|
+
toolLabel,
|
|
294
|
+
executionId,
|
|
295
|
+
startTime,
|
|
296
|
+
"BUILD_ERROR",
|
|
297
|
+
`Build failed (exit ${buildOutput.exitCode}): ${buildOutput.stderr}`
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Execute action
|
|
304
|
+
const dockerArgs = this.buildExecArgs(image, sourceDir, env, commandArray);
|
|
305
|
+
const output = await this.runContainer(dockerArgs, timeoutMs);
|
|
306
|
+
|
|
307
|
+
const endTime = new Date();
|
|
308
|
+
const metadata: ExecutionMetadata = {
|
|
309
|
+
toolName: toolLabel,
|
|
310
|
+
containerImage: image,
|
|
311
|
+
startTime,
|
|
312
|
+
endTime,
|
|
313
|
+
durationMs: endTime.getTime() - startTime.getTime(),
|
|
314
|
+
cached: false,
|
|
315
|
+
executionId,
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
if (manifest.version) {
|
|
319
|
+
metadata.toolVersion = manifest.version;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (output.exitCode !== 0) {
|
|
323
|
+
return {
|
|
324
|
+
success: false,
|
|
325
|
+
output,
|
|
326
|
+
metadata,
|
|
327
|
+
error: {
|
|
328
|
+
code: "COMMAND_ERROR",
|
|
329
|
+
message: `Action "${actionName}" exited with code ${output.exitCode}`,
|
|
330
|
+
...(output.stderr && { details: { stderr: output.stderr } }),
|
|
331
|
+
},
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Try to parse JSON output
|
|
336
|
+
if (action.outputSchema && output.stdout) {
|
|
337
|
+
try {
|
|
338
|
+
output.parsed = JSON.parse(output.stdout);
|
|
339
|
+
} catch {
|
|
340
|
+
// Not JSON — leave as string
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return { success: true, output, metadata };
|
|
345
|
+
} catch (error) {
|
|
346
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
347
|
+
let code: ExecutionErrorCode = "CONTAINER_ERROR";
|
|
348
|
+
if (message.includes("build")) code = "BUILD_ERROR";
|
|
349
|
+
else if (message === "TIMEOUT") code = "TIMEOUT";
|
|
350
|
+
return this.createErrorResult(toolLabel, executionId, startTime, code, message);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// ---------------------------------------------------------------------------
|
|
355
|
+
// Private helpers
|
|
356
|
+
// ---------------------------------------------------------------------------
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Resolve the source directory from execution options.
|
|
360
|
+
* Checks mountDirs for a /workspace mount first, then falls back to workdir or cwd.
|
|
361
|
+
*/
|
|
362
|
+
private resolveSourceDir(options: ExecutionOptions): string {
|
|
363
|
+
if (options.mountDirs) {
|
|
364
|
+
for (const [hostPath, containerPath] of Object.entries(options.mountDirs)) {
|
|
365
|
+
if (containerPath === "/workspace") return hostPath;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
return options.workdir ?? process.cwd();
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
private generateExecutionId(): string {
|
|
372
|
+
return `docker-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
private parseTimeout(timeout?: string): number {
|
|
376
|
+
if (!timeout) return this.defaultTimeout;
|
|
377
|
+
const match = timeout.match(/^(\d+)(ms|s|m|h)?$/);
|
|
378
|
+
if (!match) return this.defaultTimeout;
|
|
379
|
+
const value = Number.parseInt(match[1] ?? "300000", 10);
|
|
380
|
+
switch (match[2] ?? "ms") {
|
|
381
|
+
case "ms":
|
|
382
|
+
return value;
|
|
383
|
+
case "s":
|
|
384
|
+
return value * 1000;
|
|
385
|
+
case "m":
|
|
386
|
+
return value * 60000;
|
|
387
|
+
case "h":
|
|
388
|
+
return value * 3600000;
|
|
389
|
+
default:
|
|
390
|
+
return value;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Resolve the container image to use.
|
|
396
|
+
* If the skill directory has a Containerfile/Dockerfile, build it.
|
|
397
|
+
* Otherwise use manifest.from or a default image.
|
|
398
|
+
*/
|
|
399
|
+
private async resolveImage(manifest: ToolManifest, sourceDir: string): Promise<string> {
|
|
400
|
+
const containerfile = existsSync(join(sourceDir, "Containerfile"))
|
|
401
|
+
? "Containerfile"
|
|
402
|
+
: existsSync(join(sourceDir, "Dockerfile"))
|
|
403
|
+
? "Dockerfile"
|
|
404
|
+
: null;
|
|
405
|
+
|
|
406
|
+
if (containerfile) {
|
|
407
|
+
const hash = createHash("sha256")
|
|
408
|
+
.update(`${manifest.name}:${sourceDir}`)
|
|
409
|
+
.digest("hex")
|
|
410
|
+
.slice(0, 12);
|
|
411
|
+
const tag = `enact-${hash}`;
|
|
412
|
+
|
|
413
|
+
if (this.verbose) {
|
|
414
|
+
console.error(`[docker] Building image ${tag} from ${containerfile}`);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
await this.buildImage(sourceDir, containerfile, tag);
|
|
418
|
+
return tag;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Use manifest.from if available, otherwise default
|
|
422
|
+
return ((manifest as unknown as Record<string, unknown>).from as string) ?? "alpine:latest";
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Build a container image from a Containerfile/Dockerfile
|
|
427
|
+
*/
|
|
428
|
+
private async buildImage(contextDir: string, file: string, tag: string): Promise<void> {
|
|
429
|
+
const runtime = this.runtime!;
|
|
430
|
+
const args = ["build", "-t", tag, "-f", join(contextDir, file), contextDir];
|
|
431
|
+
|
|
432
|
+
const output = await this.spawnProcess(runtime, args, 600000); // 10 min build timeout
|
|
433
|
+
|
|
434
|
+
if (output.exitCode !== 0) {
|
|
435
|
+
throw new Error(
|
|
436
|
+
`BUILD_ERROR: Image build failed (exit ${output.exitCode}): ${output.stderr}`
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Build docker run args for a shell command string (used by execute and build)
|
|
443
|
+
*/
|
|
444
|
+
private buildRunArgs(
|
|
445
|
+
image: string,
|
|
446
|
+
sourceDir: string,
|
|
447
|
+
env: Record<string, string>,
|
|
448
|
+
command: string
|
|
449
|
+
): string[] {
|
|
450
|
+
const args = ["run", "--rm", "-w", "/workspace"];
|
|
451
|
+
|
|
452
|
+
// Mount source directory
|
|
453
|
+
args.push("-v", `${resolve(sourceDir)}:/workspace`);
|
|
454
|
+
|
|
455
|
+
// Environment variables
|
|
456
|
+
for (const [key, value] of Object.entries(env)) {
|
|
457
|
+
args.push("-e", `${key}=${value}`);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
args.push(image, "sh", "-c", command);
|
|
461
|
+
return args;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Build docker run args for an array-form command (used by executeAction)
|
|
466
|
+
*/
|
|
467
|
+
private buildExecArgs(
|
|
468
|
+
image: string,
|
|
469
|
+
sourceDir: string,
|
|
470
|
+
env: Record<string, string>,
|
|
471
|
+
commandArray: string[]
|
|
472
|
+
): string[] {
|
|
473
|
+
const args = ["run", "--rm", "-w", "/workspace"];
|
|
474
|
+
|
|
475
|
+
args.push("-v", `${resolve(sourceDir)}:/workspace`);
|
|
476
|
+
|
|
477
|
+
for (const [key, value] of Object.entries(env)) {
|
|
478
|
+
args.push("-e", `${key}=${value}`);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
args.push(image, ...commandArray);
|
|
482
|
+
return args;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Run a container and capture output
|
|
487
|
+
*/
|
|
488
|
+
private runContainer(dockerArgs: string[], timeoutMs: number): Promise<ExecutionOutput> {
|
|
489
|
+
const runtime = this.runtime!;
|
|
490
|
+
|
|
491
|
+
if (this.verbose) {
|
|
492
|
+
console.error(`[docker] ${runtime} ${dockerArgs.join(" ")}`);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
return this.spawnProcess(runtime, dockerArgs, timeoutMs);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Spawn a process and capture its output
|
|
500
|
+
*/
|
|
501
|
+
private spawnProcess(
|
|
502
|
+
command: string,
|
|
503
|
+
args: string[],
|
|
504
|
+
timeoutMs: number
|
|
505
|
+
): Promise<ExecutionOutput> {
|
|
506
|
+
return new Promise((resolve, reject) => {
|
|
507
|
+
const child = spawn(command, args, {
|
|
508
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
let stdout = "";
|
|
512
|
+
let stderr = "";
|
|
513
|
+
let timedOut = false;
|
|
514
|
+
|
|
515
|
+
const timer = setTimeout(() => {
|
|
516
|
+
timedOut = true;
|
|
517
|
+
child.kill("SIGTERM");
|
|
518
|
+
setTimeout(() => child.kill("SIGKILL"), 5000);
|
|
519
|
+
}, timeoutMs);
|
|
520
|
+
|
|
521
|
+
child.stdout?.on("data", (data: Buffer) => {
|
|
522
|
+
stdout += data.toString();
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
child.stderr?.on("data", (data: Buffer) => {
|
|
526
|
+
stderr += data.toString();
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
child.on("error", (error) => {
|
|
530
|
+
clearTimeout(timer);
|
|
531
|
+
reject(error);
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
child.on("close", (exitCode) => {
|
|
535
|
+
clearTimeout(timer);
|
|
536
|
+
if (timedOut) {
|
|
537
|
+
reject(new Error("TIMEOUT"));
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
resolve({ stdout, stderr, exitCode: exitCode ?? 1 });
|
|
541
|
+
});
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
private createErrorResult(
|
|
546
|
+
toolName: string,
|
|
547
|
+
executionId: string,
|
|
548
|
+
startTime: Date,
|
|
549
|
+
code: ExecutionErrorCode,
|
|
550
|
+
message: string
|
|
551
|
+
): ExecutionResult {
|
|
552
|
+
const endTime = new Date();
|
|
553
|
+
return {
|
|
554
|
+
success: false,
|
|
555
|
+
output: { stdout: "", stderr: message, exitCode: 1 },
|
|
556
|
+
metadata: {
|
|
557
|
+
toolName,
|
|
558
|
+
containerImage: "docker",
|
|
559
|
+
startTime,
|
|
560
|
+
endTime,
|
|
561
|
+
durationMs: endTime.getTime() - startTime.getTime(),
|
|
562
|
+
cached: false,
|
|
563
|
+
executionId,
|
|
564
|
+
},
|
|
565
|
+
error: { code, message },
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* Create a Docker execution provider
|
|
572
|
+
*/
|
|
573
|
+
export function createDockerProvider(config: DockerProviderConfig = {}): DockerExecutionProvider {
|
|
574
|
+
return new DockerExecutionProvider(config);
|
|
575
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @enactprotocol/execution
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Pluggable execution backends for Enact tools.
|
|
5
5
|
* This package contains Node.js-only code and should NOT be imported in browser environments.
|
|
6
6
|
*/
|
|
7
7
|
|
|
@@ -34,3 +34,34 @@ export {
|
|
|
34
34
|
executeToolWithDagger,
|
|
35
35
|
type DaggerProviderConfig,
|
|
36
36
|
} from "./provider.js";
|
|
37
|
+
|
|
38
|
+
// Local execution provider (no containerization)
|
|
39
|
+
export {
|
|
40
|
+
LocalExecutionProvider,
|
|
41
|
+
createLocalProvider,
|
|
42
|
+
hasContainerfile,
|
|
43
|
+
selectExecutionMode,
|
|
44
|
+
type LocalProviderConfig,
|
|
45
|
+
} from "./local-provider.js";
|
|
46
|
+
|
|
47
|
+
// Docker execution provider (direct CLI, no Dagger SDK)
|
|
48
|
+
export {
|
|
49
|
+
DockerExecutionProvider,
|
|
50
|
+
createDockerProvider,
|
|
51
|
+
type DockerProviderConfig,
|
|
52
|
+
} from "./docker-provider.js";
|
|
53
|
+
|
|
54
|
+
// Remote execution provider (delegates to remote endpoint)
|
|
55
|
+
export {
|
|
56
|
+
RemoteExecutionProvider,
|
|
57
|
+
createRemoteProvider,
|
|
58
|
+
type RemoteProviderConfig,
|
|
59
|
+
} from "./remote-provider.js";
|
|
60
|
+
|
|
61
|
+
// Execution router (config-driven backend selection)
|
|
62
|
+
export {
|
|
63
|
+
ExecutionRouter,
|
|
64
|
+
createRouter,
|
|
65
|
+
type ExecutionRoutingConfig,
|
|
66
|
+
type ProviderSelectionOptions,
|
|
67
|
+
} from "./router.js";
|