Add E2B sandbox provider plugin (#4452)

## Thinking Path

> - Paperclip orchestrates AI agents for zero-human companies
> - Sandbox environments are part of that execution layer, and the
recent core refactor moved provider-specific behavior to a generic
plugin seam
> - This pull request adds a dedicated `@paperclipai/plugin-e2b` package
so E2B can live entirely outside core host code
> - Because the feature is still unreleased, the plugin should model
third-party packaging directly instead of carrying extra
backward-compatibility complexity in core or the workspace lockfile
> - This branch therefore makes the E2B provider a standalone
publishable package, documents the package-local dev flow, and keeps the
publish manifest/runtime dependency story correct
> - The benefit is that E2B becomes a true plugin reference
implementation that can be installed by package name without reopening
core Paperclip code

## What Changed

- Added `packages/plugins/paperclip-plugin-e2b` as the E2B sandbox
provider plugin package
- Implemented config validation, lease acquire/resume/release/destroy
handlers, workspace realization, and command execution for E2B sandboxes
- Excluded the E2B plugin package from the root workspace so the repo no
longer needs `pnpm-lock.yaml` churn for its third-party dependency graph
- Added package-local development/install support plus a prepack
manifest generator so the published tarball still declares
`@paperclipai/plugin-sdk` and `e2b` runtime dependencies
- Addressed review feedback by fixing sandbox cleanup on acquire
failures, rejecting blank templates, normalizing fractional `timeoutMs`,
and always passing the configured template name to the E2B SDK
- Updated focused Vitest coverage for config normalization, validation,
acquire cleanup, command execution, and lease release behavior
- Updated the Dockerfile deps stage to copy the E2B package manifest so
the policy check stays in sync

## Verification

- `cd packages/plugins/paperclip-plugin-e2b && pnpm install
--ignore-workspace --no-lockfile`
- `cd packages/plugins/paperclip-plugin-e2b && pnpm build`
- `cd packages/plugins/paperclip-plugin-e2b && pnpm --ignore-workspace
test`
- `cd packages/plugins/paperclip-plugin-e2b && pnpm --ignore-workspace
typecheck`
- `cd packages/plugins/paperclip-plugin-e2b && npm pack --dry-run`

## Risks

- The package now relies on a prepack manifest rewrite so the
publish-time dependency list stays correct while the repo-local dev
manifest stays workspace-light
- The current repo snapshot is still unreleased, so the generated
publish manifest points at the repo SDK version until the normal release
flow rewrites versions before publish
- Real-world E2B environments may still expose edge cases around
lifecycle timing or sandbox metadata beyond the mocked unit coverage

> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.

## Model Used

- OpenAI Codex via `codex_local`
- Model ID: `gpt-5.4`
- Reasoning effort: `high`
- Context window observed in runtime session metadata: `258400` tokens
- Capabilities used: terminal tool execution, git, GitHub CLI, and local
build/test inspection

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [ ] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge
This commit is contained in:
Devin Foley 2026-04-25 11:01:11 -07:00 committed by GitHub
parent 5bd0f578fd
commit 4ef969f084
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 1279 additions and 38 deletions

View file

@ -0,0 +1,53 @@
declare module "e2b" {
export class CommandExitError extends Error {
exitCode: number;
stdout: string;
stderr: string;
}
export class SandboxNotFoundError extends Error {}
export class TimeoutError extends Error {}
export interface SandboxRunResult {
exitCode: number;
stdout: string;
stderr: string;
}
export interface SandboxBackgroundHandle {
pid: number;
stdout: string;
stderr: string;
wait(): Promise<SandboxRunResult>;
}
export class Sandbox {
sandboxId: string;
sandboxDomain?: string;
static create(
templateOrOptions?: string | Record<string, unknown>,
maybeOptions?: Record<string, unknown>,
): Promise<Sandbox>;
static connect(
sandboxId: string,
options?: Record<string, unknown>,
): Promise<Sandbox>;
setTimeout(timeoutMs: number): Promise<void>;
kill(): Promise<void>;
pause(): Promise<void>;
commands: {
run(
command: string,
options?: {
background?: boolean;
stdin?: boolean;
cwd?: string;
envs?: Record<string, string>;
timeoutMs?: number;
},
): Promise<SandboxRunResult | SandboxBackgroundHandle>;
sendStdin(pid: number, input: string): Promise<void>;
closeStdin(pid: number): Promise<void>;
};
}
}

View file

@ -0,0 +1,2 @@
export { default as manifest } from "./manifest.js";
export { default as plugin } from "./plugin.js";

View file

@ -0,0 +1,57 @@
import type { PaperclipPluginManifestV1 } from "@paperclipai/plugin-sdk";
const PLUGIN_ID = "paperclip.e2b-sandbox-provider";
const PLUGIN_VERSION = "0.1.0";
const manifest: PaperclipPluginManifestV1 = {
id: PLUGIN_ID,
apiVersion: 1,
version: PLUGIN_VERSION,
displayName: "E2B Sandbox Provider",
description:
"First-party sandbox provider plugin that provisions E2B cloud sandboxes as Paperclip execution environments.",
author: "Paperclip",
categories: ["automation"],
capabilities: ["environment.drivers.register"],
entrypoints: {
worker: "./dist/worker.js",
},
environmentDrivers: [
{
driverKey: "e2b",
kind: "sandbox_provider",
displayName: "E2B Cloud Sandbox",
description:
"Provisions E2B cloud sandboxes with configurable templates, timeouts, and lease reuse.",
configSchema: {
type: "object",
properties: {
template: {
type: "string",
description: "E2B sandbox template name.",
default: "base",
},
apiKey: {
type: "string",
format: "secret-ref",
description:
"Paperclip secret reference for the E2B API key. Falls back to E2B_API_KEY if omitted.",
},
timeoutMs: {
type: "number",
description: "Sandbox timeout in milliseconds.",
default: 300000,
},
reuseLease: {
type: "boolean",
description: "Whether to pause and reuse sandboxes across runs.",
default: false,
},
},
required: ["template"],
},
},
],
};
export default manifest;

View file

@ -0,0 +1,415 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
const mockCreate = vi.hoisted(() => vi.fn());
const mockConnect = vi.hoisted(() => vi.fn());
const { MockCommandExitError, MockSandboxNotFoundError, MockTimeoutError } = vi.hoisted(() => {
class MockCommandExitError extends Error {
exitCode: number;
stdout: string;
stderr: string;
constructor(result: { exitCode: number; stdout: string; stderr: string }) {
super("command failed");
this.exitCode = result.exitCode;
this.stdout = result.stdout;
this.stderr = result.stderr;
}
}
class MockSandboxNotFoundError extends Error {}
class MockTimeoutError extends Error {}
return { MockCommandExitError, MockSandboxNotFoundError, MockTimeoutError };
});
vi.mock("e2b", () => ({
CommandExitError: MockCommandExitError,
SandboxNotFoundError: MockSandboxNotFoundError,
TimeoutError: MockTimeoutError,
Sandbox: {
create: mockCreate,
connect: mockConnect,
},
}));
import plugin from "./plugin.js";
function createMockSandbox(overrides: {
sandboxId?: string;
sandboxDomain?: string;
pwd?: string;
waitResult?: { exitCode: number; stdout: string; stderr: string };
} = {}) {
const handle = {
pid: 42,
stdout: "",
stderr: "",
wait: vi.fn().mockResolvedValue(overrides.waitResult ?? {
exitCode: 0,
stdout: "ok\n",
stderr: "",
}),
};
return {
sandboxId: overrides.sandboxId ?? "sandbox-123",
sandboxDomain: overrides.sandboxDomain ?? "sandbox.example.test",
setTimeout: vi.fn().mockResolvedValue(undefined),
kill: vi.fn().mockResolvedValue(undefined),
pause: vi.fn().mockResolvedValue(undefined),
commands: {
run: vi.fn(async (command: string, options?: { background?: boolean }) => {
if (options?.background) return handle;
if (command === "pwd") {
return {
exitCode: 0,
stdout: `${overrides.pwd ?? "/home/user"}\n`,
stderr: "",
};
}
return {
exitCode: 0,
stdout: "",
stderr: "",
};
}),
sendStdin: vi.fn().mockResolvedValue(undefined),
closeStdin: vi.fn().mockResolvedValue(undefined),
},
handle,
};
}
describe("E2B sandbox provider plugin", () => {
beforeEach(() => {
mockCreate.mockReset();
mockConnect.mockReset();
vi.restoreAllMocks();
delete process.env.E2B_API_KEY;
});
it("declares environment lifecycle handlers", async () => {
expect(await plugin.definition.onHealth?.()).toEqual({
status: "ok",
message: "E2B sandbox provider plugin healthy",
});
expect(plugin.definition.onEnvironmentAcquireLease).toBeTypeOf("function");
expect(plugin.definition.onEnvironmentExecute).toBeTypeOf("function");
});
it("normalizes E2B config through the generic provider shape", async () => {
const result = await plugin.definition.onEnvironmentValidateConfig?.({
driverKey: "e2b",
config: {
template: " base ",
apiKey: " e2b_test_key ",
timeoutMs: "450000.9",
reuseLease: true,
},
});
expect(result).toEqual({
ok: true,
normalizedConfig: {
template: "base",
apiKey: "e2b_test_key",
timeoutMs: 450000,
reuseLease: true,
},
});
});
it("rejects empty template strings instead of silently normalizing them", async () => {
await expect(plugin.definition.onEnvironmentValidateConfig?.({
driverKey: "e2b",
config: {
template: " ",
},
})).resolves.toEqual({
ok: false,
errors: ["E2B sandbox environments require a template."],
});
});
it("uses resolved config keys before falling back to E2B_API_KEY", async () => {
const sandbox = createMockSandbox();
mockCreate.mockResolvedValue(sandbox);
process.env.E2B_API_KEY = "host-key";
const lease = await plugin.definition.onEnvironmentAcquireLease?.({
driverKey: "e2b",
companyId: "company-1",
environmentId: "env-1",
runId: "run-1",
config: {
template: "base",
apiKey: "resolved-key",
timeoutMs: 300000,
reuseLease: false,
},
});
expect(mockCreate).toHaveBeenCalledWith("base", expect.objectContaining({
apiKey: "resolved-key",
timeoutMs: 300000,
}));
expect(lease).toMatchObject({
providerLeaseId: "sandbox-123",
metadata: {
provider: "e2b",
remoteCwd: "/home/user/paperclip-workspace",
},
});
expect(sandbox.commands.run).toHaveBeenNthCalledWith(1, "pwd");
expect(sandbox.commands.run).toHaveBeenNthCalledWith(2, "mkdir -p '/home/user/paperclip-workspace'");
});
it("kills the sandbox if acquire setup fails after creation", async () => {
const sandbox = createMockSandbox();
const failure = new Error("set-timeout failed");
sandbox.setTimeout.mockRejectedValueOnce(failure);
mockCreate.mockResolvedValue(sandbox);
await expect(plugin.definition.onEnvironmentAcquireLease?.({
driverKey: "e2b",
companyId: "company-1",
environmentId: "env-1",
runId: "run-1",
config: {
template: "base",
apiKey: "resolved-key",
timeoutMs: 300000,
reuseLease: false,
},
})).rejects.toThrow("set-timeout failed");
expect(sandbox.kill).toHaveBeenCalled();
});
it("falls back to host E2B_API_KEY when config omits the API key", async () => {
process.env.E2B_API_KEY = "host-key";
const sandbox = createMockSandbox();
mockCreate.mockResolvedValue(sandbox);
await expect(plugin.definition.onEnvironmentAcquireLease?.({
driverKey: "e2b",
companyId: "company-1",
environmentId: "env-1",
runId: "run-1",
config: {
template: "base",
apiKey: null,
timeoutMs: 300000,
reuseLease: false,
},
})).resolves.toMatchObject({
providerLeaseId: "sandbox-123",
});
expect(mockCreate).toHaveBeenCalledWith("base", expect.objectContaining({ apiKey: "host-key" }));
});
it("kills the sandbox if resume setup fails after reconnect", async () => {
const sandbox = createMockSandbox();
const failure = new Error("set-timeout failed");
sandbox.setTimeout.mockRejectedValueOnce(failure);
mockConnect.mockResolvedValue(sandbox);
await expect(plugin.definition.onEnvironmentResumeLease?.({
driverKey: "e2b",
companyId: "company-1",
environmentId: "env-1",
runId: "run-1",
providerLeaseId: "sandbox-123",
config: {
template: "base",
apiKey: "resolved-key",
timeoutMs: 300000,
reuseLease: false,
},
})).rejects.toThrow("set-timeout failed");
expect(sandbox.kill).toHaveBeenCalled();
});
it("executes commands through a connected sandbox", async () => {
const sandbox = createMockSandbox();
mockConnect.mockResolvedValue(sandbox);
const result = await plugin.definition.onEnvironmentExecute?.({
driverKey: "e2b",
companyId: "company-1",
environmentId: "env-1",
config: {
template: "base",
apiKey: "resolved-key",
timeoutMs: 300000,
reuseLease: false,
},
lease: { providerLeaseId: "sandbox-123", metadata: {} },
command: "printf",
args: ["hello"],
cwd: "/workspace",
env: { FOO: "bar" },
stdin: "input",
timeoutMs: 1000,
});
expect(mockConnect).toHaveBeenCalledWith("sandbox-123", expect.objectContaining({ apiKey: "resolved-key" }));
expect(sandbox.commands.run).toHaveBeenCalledWith("exec 'printf' 'hello'", expect.objectContaining({
background: true,
cwd: "/workspace",
envs: { FOO: "bar" },
stdin: true,
timeoutMs: 1000,
}));
expect(sandbox.commands.sendStdin).toHaveBeenCalledWith(42, "input");
expect(sandbox.commands.closeStdin).toHaveBeenCalledWith(42);
expect(result).toEqual({
exitCode: 0,
timedOut: false,
stdout: "ok\n",
stderr: "",
});
});
it("closes stdin even when sendStdin throws unexpectedly", async () => {
const sandbox = createMockSandbox();
const failure = new Error("send failed");
sandbox.commands.sendStdin.mockRejectedValueOnce(failure);
mockConnect.mockResolvedValue(sandbox);
await expect(plugin.definition.onEnvironmentExecute?.({
driverKey: "e2b",
companyId: "company-1",
environmentId: "env-1",
config: {
template: "base",
apiKey: "resolved-key",
timeoutMs: 300000,
reuseLease: false,
},
lease: { providerLeaseId: "sandbox-123", metadata: {} },
command: "printf",
args: ["hello"],
cwd: "/workspace",
env: { FOO: "bar" },
stdin: "input",
timeoutMs: 1000,
})).rejects.toThrow("send failed");
expect(sandbox.commands.closeStdin).toHaveBeenCalledWith(42);
expect(sandbox.handle.wait).not.toHaveBeenCalled();
});
it("pauses reusable leases and kills ephemeral leases on release", async () => {
const reusable = createMockSandbox({ sandboxId: "sandbox-reusable" });
const ephemeral = createMockSandbox({ sandboxId: "sandbox-ephemeral" });
mockConnect.mockResolvedValueOnce(reusable).mockResolvedValueOnce(ephemeral);
await plugin.definition.onEnvironmentReleaseLease?.({
driverKey: "e2b",
companyId: "company-1",
environmentId: "env-1",
config: {
template: "base",
apiKey: "resolved-key",
timeoutMs: 300000,
reuseLease: true,
},
providerLeaseId: "sandbox-reusable",
});
await plugin.definition.onEnvironmentReleaseLease?.({
driverKey: "e2b",
companyId: "company-1",
environmentId: "env-1",
config: {
template: "base",
apiKey: "resolved-key",
timeoutMs: 300000,
reuseLease: false,
},
providerLeaseId: "sandbox-ephemeral",
});
expect(reusable.pause).toHaveBeenCalled();
expect(reusable.kill).not.toHaveBeenCalled();
expect(ephemeral.kill).toHaveBeenCalled();
});
it("falls back to kill when pausing a reusable lease fails", async () => {
const sandbox = createMockSandbox({ sandboxId: "sandbox-reusable" });
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => undefined);
sandbox.pause.mockRejectedValueOnce(new Error("pause failed"));
mockConnect.mockResolvedValue(sandbox);
await expect(plugin.definition.onEnvironmentReleaseLease?.({
driverKey: "e2b",
companyId: "company-1",
environmentId: "env-1",
config: {
template: "base",
apiKey: "resolved-key",
timeoutMs: 300000,
reuseLease: true,
},
providerLeaseId: "sandbox-reusable",
})).resolves.toBeUndefined();
expect(sandbox.pause).toHaveBeenCalled();
expect(sandbox.kill).toHaveBeenCalled();
expect(warnSpy).toHaveBeenCalled();
});
it("creates the remote workspace before returning it", async () => {
const sandbox = createMockSandbox({ sandboxId: "sandbox-realize" });
mockConnect.mockResolvedValue(sandbox);
await expect(plugin.definition.onEnvironmentRealizeWorkspace?.({
driverKey: "e2b",
companyId: "company-1",
environmentId: "env-1",
config: {
template: "base",
apiKey: "resolved-key",
timeoutMs: 300000,
reuseLease: false,
},
lease: {
providerLeaseId: "sandbox-realize",
metadata: { remoteCwd: "/home/user/paperclip-workspace" },
},
workspace: {
localPath: "/tmp/paperclip-workspace",
},
})).resolves.toEqual({
cwd: "/home/user/paperclip-workspace",
metadata: {
provider: "e2b",
remoteCwd: "/home/user/paperclip-workspace",
},
});
expect(mockConnect).toHaveBeenCalledWith("sandbox-realize", expect.objectContaining({ apiKey: "resolved-key" }));
expect(sandbox.commands.run).toHaveBeenCalledWith("mkdir -p '/home/user/paperclip-workspace'");
});
it("swallows destroy kill errors after logging them", async () => {
const sandbox = createMockSandbox({ sandboxId: "sandbox-destroy" });
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => undefined);
sandbox.kill.mockRejectedValueOnce(new Error("kill failed"));
mockConnect.mockResolvedValue(sandbox);
await expect(plugin.definition.onEnvironmentDestroyLease?.({
driverKey: "e2b",
companyId: "company-1",
environmentId: "env-1",
config: {
template: "base",
apiKey: "resolved-key",
timeoutMs: 300000,
reuseLease: false,
},
providerLeaseId: "sandbox-destroy",
})).resolves.toBeUndefined();
expect(sandbox.kill).toHaveBeenCalled();
expect(warnSpy).toHaveBeenCalled();
});
});

View file

@ -0,0 +1,371 @@
import path from "node:path";
import { CommandExitError, Sandbox, SandboxNotFoundError, TimeoutError } from "e2b";
import { definePlugin } from "@paperclipai/plugin-sdk";
import type {
PluginEnvironmentAcquireLeaseParams,
PluginEnvironmentDestroyLeaseParams,
PluginEnvironmentExecuteParams,
PluginEnvironmentExecuteResult,
PluginEnvironmentLease,
PluginEnvironmentProbeParams,
PluginEnvironmentProbeResult,
PluginEnvironmentRealizeWorkspaceParams,
PluginEnvironmentRealizeWorkspaceResult,
PluginEnvironmentReleaseLeaseParams,
PluginEnvironmentResumeLeaseParams,
PluginEnvironmentValidateConfigParams,
PluginEnvironmentValidationResult,
} from "@paperclipai/plugin-sdk";
interface E2bDriverConfig {
template: string;
apiKey: string | null;
timeoutMs: number;
reuseLease: boolean;
}
function parseDriverConfig(raw: Record<string, unknown>): E2bDriverConfig {
const template = typeof raw.template === "string" && raw.template.trim().length > 0
? raw.template.trim()
: "base";
const timeoutMs = Number(raw.timeoutMs ?? 300_000);
return {
template,
apiKey: typeof raw.apiKey === "string" && raw.apiKey.trim().length > 0 ? raw.apiKey.trim() : null,
timeoutMs: Number.isFinite(timeoutMs) ? Math.trunc(timeoutMs) : 300_000,
reuseLease: raw.reuseLease === true,
};
}
function resolveApiKey(config: E2bDriverConfig): string {
if (config.apiKey) {
return config.apiKey;
}
const envApiKey = process.env.E2B_API_KEY?.trim() ?? "";
if (!envApiKey) {
throw new Error("E2B sandbox environments require an API key in config or E2B_API_KEY.");
}
return envApiKey;
}
async function createSandbox(config: E2bDriverConfig): Promise<Sandbox> {
const options = {
apiKey: resolveApiKey(config),
timeoutMs: config.timeoutMs,
metadata: {
paperclipProvider: "e2b",
},
};
return await Sandbox.create(config.template, options);
}
function formatErrorMessage(error: unknown): string {
return error instanceof Error ? error.message : String(error);
}
async function ensureSandboxWorkspace(sandbox: Sandbox, remoteCwd: string): Promise<void> {
await sandbox.commands.run(`mkdir -p ${shellQuote(remoteCwd)}`);
}
async function resolveSandboxWorkingDirectory(sandbox: Sandbox): Promise<string> {
const result = await sandbox.commands.run("pwd");
const cwd = result.stdout.trim();
const remoteCwd = path.posix.join(cwd.length > 0 ? cwd : "/", "paperclip-workspace");
await ensureSandboxWorkspace(sandbox, remoteCwd);
return remoteCwd;
}
async function connectSandbox(config: E2bDriverConfig, providerLeaseId: string): Promise<Sandbox> {
return await Sandbox.connect(providerLeaseId, {
apiKey: resolveApiKey(config),
timeoutMs: config.timeoutMs,
});
}
async function connectForCleanup(config: E2bDriverConfig, providerLeaseId: string): Promise<Sandbox | null> {
try {
return await connectSandbox(config, providerLeaseId);
} catch (error) {
if (error instanceof SandboxNotFoundError) return null;
throw error;
}
}
function leaseMetadata(input: {
config: E2bDriverConfig;
sandbox: Sandbox;
remoteCwd: string;
resumedLease: boolean;
}) {
return {
provider: "e2b",
template: input.config.template,
timeoutMs: input.config.timeoutMs,
reuseLease: input.config.reuseLease,
sandboxId: input.sandbox.sandboxId,
sandboxDomain: input.sandbox.sandboxDomain,
remoteCwd: input.remoteCwd,
resumedLease: input.resumedLease,
};
}
function shellQuote(value: string) {
return `'${value.replace(/'/g, `'"'"'`)}'`;
}
function buildCommandLine(command: string, args: string[] = []) {
return `exec ${[command, ...args].map(shellQuote).join(" ")}`;
}
async function killSandboxBestEffort(sandbox: Sandbox, reason: string): Promise<void> {
await sandbox.kill().catch((error) => {
console.warn(`Failed to kill E2B sandbox during ${reason}: ${formatErrorMessage(error)}`);
});
}
async function releaseSandboxBestEffort(sandbox: Sandbox, reuseLease: boolean): Promise<void> {
if (!reuseLease) {
await killSandboxBestEffort(sandbox, "lease release");
return;
}
try {
await sandbox.pause();
} catch (error) {
console.warn(
`Failed to pause E2B sandbox during lease release: ${formatErrorMessage(error)}. Attempting kill instead.`,
);
await killSandboxBestEffort(sandbox, "lease release fallback cleanup");
}
}
const plugin = definePlugin({
async setup(ctx) {
ctx.logger.info("E2B sandbox provider plugin ready");
},
async onHealth() {
return { status: "ok", message: "E2B sandbox provider plugin healthy" };
},
async onEnvironmentValidateConfig(
params: PluginEnvironmentValidateConfigParams,
): Promise<PluginEnvironmentValidationResult> {
const config = parseDriverConfig(params.config);
const errors: string[] = [];
if (typeof params.config.template === "string" && params.config.template.trim().length === 0) {
errors.push("E2B sandbox environments require a template.");
}
if (config.timeoutMs < 1 || config.timeoutMs > 86_400_000) {
errors.push("timeoutMs must be between 1 and 86400000.");
}
if (errors.length > 0) {
return { ok: false, errors };
}
return {
ok: true,
normalizedConfig: { ...config },
};
},
async onEnvironmentProbe(
params: PluginEnvironmentProbeParams,
): Promise<PluginEnvironmentProbeResult> {
const config = parseDriverConfig(params.config);
try {
const sandbox = await createSandbox(config);
try {
await sandbox.setTimeout(config.timeoutMs);
const remoteCwd = await resolveSandboxWorkingDirectory(sandbox);
return {
ok: true,
summary: `Connected to E2B sandbox template ${config.template}.`,
metadata: {
provider: "e2b",
template: config.template,
timeoutMs: config.timeoutMs,
reuseLease: config.reuseLease,
sandboxId: sandbox.sandboxId,
sandboxDomain: sandbox.sandboxDomain,
remoteCwd,
},
};
} finally {
await sandbox.kill().catch(() => undefined);
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
return {
ok: false,
summary: `E2B sandbox probe failed for template ${config.template}.`,
metadata: {
provider: "e2b",
template: config.template,
timeoutMs: config.timeoutMs,
reuseLease: config.reuseLease,
error: message,
},
};
}
},
async onEnvironmentAcquireLease(
params: PluginEnvironmentAcquireLeaseParams,
): Promise<PluginEnvironmentLease> {
const config = parseDriverConfig(params.config);
const sandbox = await createSandbox(config);
try {
await sandbox.setTimeout(config.timeoutMs);
const remoteCwd = await resolveSandboxWorkingDirectory(sandbox);
return {
providerLeaseId: sandbox.sandboxId,
metadata: leaseMetadata({ config, sandbox, remoteCwd, resumedLease: false }),
};
} catch (error) {
await sandbox.kill().catch(() => undefined);
throw error;
}
},
async onEnvironmentResumeLease(
params: PluginEnvironmentResumeLeaseParams,
): Promise<PluginEnvironmentLease> {
const config = parseDriverConfig(params.config);
try {
const sandbox = await connectSandbox(config, params.providerLeaseId);
try {
await sandbox.setTimeout(config.timeoutMs);
const remoteCwd = await resolveSandboxWorkingDirectory(sandbox);
return {
providerLeaseId: sandbox.sandboxId,
metadata: leaseMetadata({ config, sandbox, remoteCwd, resumedLease: true }),
};
} catch (error) {
await sandbox.kill().catch(() => undefined);
throw error;
}
} catch (error) {
if (error instanceof SandboxNotFoundError) {
return { providerLeaseId: null, metadata: { expired: true } };
}
throw error;
}
},
async onEnvironmentReleaseLease(
params: PluginEnvironmentReleaseLeaseParams,
): Promise<void> {
if (!params.providerLeaseId) return;
const config = parseDriverConfig(params.config);
const sandbox = await connectForCleanup(config, params.providerLeaseId);
if (!sandbox) return;
await releaseSandboxBestEffort(sandbox, config.reuseLease);
},
async onEnvironmentDestroyLease(
params: PluginEnvironmentDestroyLeaseParams,
): Promise<void> {
if (!params.providerLeaseId) return;
const config = parseDriverConfig(params.config);
const sandbox = await connectForCleanup(config, params.providerLeaseId);
if (!sandbox) return;
await killSandboxBestEffort(sandbox, "lease destroy");
},
async onEnvironmentRealizeWorkspace(
params: PluginEnvironmentRealizeWorkspaceParams,
): Promise<PluginEnvironmentRealizeWorkspaceResult> {
const config = parseDriverConfig(params.config);
const remoteCwd =
typeof params.lease.metadata?.remoteCwd === "string" &&
params.lease.metadata.remoteCwd.trim().length > 0
? params.lease.metadata.remoteCwd.trim()
: params.workspace.remotePath ?? params.workspace.localPath ?? "/paperclip-workspace";
if (params.lease.providerLeaseId) {
const sandbox = await connectSandbox(config, params.lease.providerLeaseId);
await ensureSandboxWorkspace(sandbox, remoteCwd);
}
return {
cwd: remoteCwd,
metadata: {
provider: "e2b",
remoteCwd,
},
};
},
async onEnvironmentExecute(
params: PluginEnvironmentExecuteParams,
): Promise<PluginEnvironmentExecuteResult> {
if (!params.lease.providerLeaseId) {
return {
exitCode: 1,
timedOut: false,
stdout: "",
stderr: "No provider lease ID available for execution.",
};
}
const config = parseDriverConfig(params.config);
const sandbox = await connectSandbox(config, params.lease.providerLeaseId);
const started = await sandbox.commands.run(buildCommandLine(params.command, params.args), {
background: true,
stdin: params.stdin != null,
cwd: params.cwd,
envs: params.env,
timeoutMs: params.timeoutMs ?? config.timeoutMs,
}) as Awaited<ReturnType<Sandbox["commands"]["run"]>> & {
pid: number;
stdout: string;
stderr: string;
wait(): Promise<{ exitCode: number; stdout: string; stderr: string }>;
};
try {
if (params.stdin != null) {
try {
await sandbox.commands.sendStdin(started.pid, params.stdin);
} finally {
await sandbox.commands.closeStdin(started.pid);
}
}
const result = await started.wait();
return {
exitCode: result.exitCode,
timedOut: false,
stdout: result.stdout,
stderr: result.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) {
const timeoutError = error as TimeoutError;
return {
exitCode: null,
timedOut: true,
stdout: started.stdout,
stderr: started.stderr || `${timeoutError.message}\n`,
};
}
throw error;
}
},
});
export default plugin;

View file

@ -0,0 +1,5 @@
import { runWorker } from "@paperclipai/plugin-sdk";
import plugin from "./plugin.js";
export default plugin;
runWorker(plugin, import.meta.url);