Dev #11
@@ -1,5 +1,11 @@
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { CommandExitError, Sandbox, SandboxNotFoundError, TimeoutError } from "e2b";
|
import { randomUUID } from "node:crypto";
|
||||||
|
import {
|
||||||
|
CommandExitError,
|
||||||
|
Sandbox,
|
||||||
|
SandboxNotFoundError,
|
||||||
|
TimeoutError,
|
||||||
|
} from "e2b";
|
||||||
import { definePlugin } from "@paperclipai/plugin-sdk";
|
import { definePlugin } from "@paperclipai/plugin-sdk";
|
||||||
import type {
|
import type {
|
||||||
PluginEnvironmentAcquireLeaseParams,
|
PluginEnvironmentAcquireLeaseParams,
|
||||||
@@ -345,13 +351,38 @@ const plugin = definePlugin({
|
|||||||
|
|
||||||
const config = parseDriverConfig(params.config);
|
const config = parseDriverConfig(params.config);
|
||||||
const sandbox = await connectSandbox(config, params.lease.providerLeaseId);
|
const sandbox = await connectSandbox(config, params.lease.providerLeaseId);
|
||||||
const command = buildCommandLine(params.command, params.args);
|
const baseCommand = buildCommandLine(params.command, params.args);
|
||||||
if (params.stdin == null) {
|
const timeoutMs = params.timeoutMs ?? config.timeoutMs;
|
||||||
|
|
||||||
|
// For commands with stdin, stage the payload to a temp file inside the
|
||||||
|
// sandbox and shell-redirect it. Streaming stdin via `sendStdin` raced
|
||||||
|
// with fast-failing commands (the process exits before the RPC lands),
|
||||||
|
// and the previous code awaited a foreground `run` before sending stdin
|
||||||
|
// at all, so the data was never delivered. The staged-file approach
|
||||||
|
// keeps execution synchronous, avoids the race, and is unaffected by
|
||||||
|
// whether the command exits in microseconds or minutes.
|
||||||
|
let stagedStdinPath: string | null = null;
|
||||||
|
if (params.stdin != null) {
|
||||||
|
stagedStdinPath = `/tmp/paperclip-stdin-${randomUUID()}`;
|
||||||
|
try {
|
||||||
|
await sandbox.files.write(stagedStdinPath, params.stdin);
|
||||||
|
} catch (error) {
|
||||||
|
// Best-effort cleanup in case the write partially succeeded; ignore
|
||||||
|
// remove failures so the original error is what propagates.
|
||||||
|
await sandbox.files.remove(stagedStdinPath).catch(() => undefined);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const command = stagedStdinPath
|
||||||
|
? `${baseCommand} < ${shellQuote(stagedStdinPath)}`
|
||||||
|
: baseCommand;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await sandbox.commands.run(command, {
|
const result = await sandbox.commands.run(command, {
|
||||||
cwd: params.cwd,
|
cwd: params.cwd,
|
||||||
envs: params.env,
|
envs: params.env,
|
||||||
timeoutMs: params.timeoutMs ?? config.timeoutMs,
|
timeoutMs,
|
||||||
}) as Awaited<ReturnType<Sandbox["commands"]["run"]>> & {
|
}) as Awaited<ReturnType<Sandbox["commands"]["run"]>> & {
|
||||||
exitCode: number;
|
exitCode: number;
|
||||||
stdout: string;
|
stdout: string;
|
||||||
@@ -365,59 +396,21 @@ const plugin = definePlugin({
|
|||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof CommandExitError) {
|
if (error instanceof CommandExitError) {
|
||||||
const commandError = error as CommandExitError;
|
|
||||||
return {
|
return {
|
||||||
exitCode: commandError.exitCode,
|
exitCode: error.exitCode,
|
||||||
timedOut: false,
|
timedOut: false,
|
||||||
stdout: commandError.stdout,
|
stdout: error.stdout,
|
||||||
stderr: commandError.stderr,
|
stderr: error.stderr,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (error instanceof TimeoutError) {
|
if (error instanceof TimeoutError) {
|
||||||
return buildTimeoutExecuteResult(error);
|
return buildTimeoutExecuteResult(error);
|
||||||
}
|
}
|
||||||
throw error;
|
throw error;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const started = await sandbox.commands.run(command, {
|
|
||||||
stdin: true,
|
|
||||||
cwd: params.cwd,
|
|
||||||
envs: params.env,
|
|
||||||
timeoutMs: params.timeoutMs ?? config.timeoutMs,
|
|
||||||
}) as Awaited<ReturnType<Sandbox["commands"]["run"]>> & {
|
|
||||||
pid: number;
|
|
||||||
exitCode: number;
|
|
||||||
stdout: string;
|
|
||||||
stderr: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
try {
|
|
||||||
await sandbox.commands.sendStdin(started.pid, params.stdin);
|
|
||||||
} finally {
|
} finally {
|
||||||
await sandbox.commands.closeStdin(started.pid);
|
if (stagedStdinPath) {
|
||||||
|
await sandbox.files.remove(stagedStdinPath).catch(() => undefined);
|
||||||
}
|
}
|
||||||
return {
|
|
||||||
exitCode: started.exitCode,
|
|
||||||
timedOut: false,
|
|
||||||
stdout: started.stdout,
|
|
||||||
stderr: started.stderr,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof CommandExitError) {
|
|
||||||
const commandError = error as CommandExitError;
|
|
||||||
return {
|
|
||||||
exitCode: commandError.exitCode,
|
|
||||||
timedOut: false,
|
|
||||||
stdout: commandError.stdout,
|
|
||||||
stderr: commandError.stderr,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (error instanceof TimeoutError) {
|
|
||||||
return buildTimeoutExecuteResult(error);
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user