diff --git a/.agentworkforce/trajectories/completed/2026-09/traj_h8qiliylqnav/summary.md b/.agentworkforce/trajectories/completed/2026-09/traj_h8qiliylqnav/summary.md new file mode 100644 index 0000000..dfe3c72 --- /dev/null +++ b/.agentworkforce/trajectories/completed/2026-09/traj_h8qiliylqnav/summary.md @@ -0,0 +1,32 @@ +# Trajectory: Finalize and publish E2B home contract + +> **Status:** ✅ Completed +> **Task:** sandbox#44 +> **Confidence:** 97% +> **Started:** September 1, 2026 at 11:16 AM +> **Completed:** September 1, 2026 at 11:18 AM + +--- + +## Summary + +Reconciled sandbox 0.1.12 lock metadata, strengthened E2B preflight validation coverage, and passed 817 tests, typecheck, package smoke, and npm ci. + +**Approach:** Standard approach + +--- + +## Key Decisions + +### Keep package and lockfile release versions identical +- **Chose:** Keep package and lockfile release versions identical +- **Reasoning:** The manual publish workflow consumes the checked-out lockfile and package metadata, so the 0.1.12 release candidate must be internally consistent. + +--- + +## Chapters + +### 1. Work +*Agent: default* + +- Keep package and lockfile release versions identical: Keep package and lockfile release versions identical diff --git a/.agentworkforce/trajectories/completed/2026-09/traj_h8qiliylqnav/trajectory.json b/.agentworkforce/trajectories/completed/2026-09/traj_h8qiliylqnav/trajectory.json new file mode 100644 index 0000000..46aae1f --- /dev/null +++ b/.agentworkforce/trajectories/completed/2026-09/traj_h8qiliylqnav/trajectory.json @@ -0,0 +1,57 @@ +{ + "id": "traj_h8qiliylqnav", + "version": 1, + "task": { + "title": "Finalize and publish E2B home contract", + "source": { + "system": "plain", + "id": "sandbox#44" + } + }, + "status": "completed", + "startedAt": "2026-09-01T09:16:35.911Z", + "completedAt": "2026-09-01T09:18:23.343Z", + "agents": [ + { + "name": "default", + "role": "lead", + "joinedAt": "2026-09-01T09:18:22.563Z" + } + ], + "chapters": [ + { + "id": "chap_2r53v77klt5l", + "title": "Work", + "agentName": "default", + "startedAt": "2026-09-01T09:18:22.563Z", + "endedAt": "2026-09-01T09:18:23.343Z", + "events": [ + { + "ts": 1788254302564, + "type": "decision", + "content": "Keep package and lockfile release versions identical: Keep package and lockfile release versions identical", + "raw": { + "question": "Keep package and lockfile release versions identical", + "chosen": "Keep package and lockfile release versions identical", + "alternatives": [], + "reasoning": "The manual publish workflow consumes the checked-out lockfile and package metadata, so the 0.1.12 release candidate must be internally consistent." + }, + "significance": "high" + } + ] + } + ], + "retrospective": { + "summary": "Reconciled sandbox 0.1.12 lock metadata, strengthened E2B preflight validation coverage, and passed 817 tests, typecheck, package smoke, and npm ci.", + "approach": "Standard approach", + "confidence": 0.97 + }, + "commits": [], + "filesChanged": [], + "projectId": "AgentWorkforce/sandbox", + "tags": [], + "_trace": { + "startRef": "1455ce7494fbb762bde4595026735e8b366dd0a5", + "endRef": "1455ce7494fbb762bde4595026735e8b366dd0a5" + } +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index f6a0698..69ac089 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@agent-relay/sandbox", - "version": "0.1.11", + "version": "0.1.12", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@agent-relay/sandbox", - "version": "0.1.11", + "version": "0.1.12", "license": "Apache-2.0", "devDependencies": { "@aws-sdk/client-bedrock-agentcore": "^3.1115.0", diff --git a/package.json b/package.json index 1359593..f321c38 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@agent-relay/sandbox", - "version": "0.1.11", + "version": "0.1.12", "description": "Provider-agnostic sandbox runtimes and orchestration for agent workloads.", "license": "Apache-2.0", "type": "module", diff --git a/src/e2b/runtime.test.ts b/src/e2b/runtime.test.ts index 499e7f1..0fb1541 100644 --- a/src/e2b/runtime.test.ts +++ b/src/e2b/runtime.test.ts @@ -80,9 +80,51 @@ describe("E2BSandboxRuntime public contract", () => { ); assert.equal(calls.create.length, 0); }); + + it("must-not-fire: rejects an explicitly empty default home directory", () => { + const { statics, calls } = fakeStatics(); + + assert.throws( + () => new E2BSandboxRuntime({ + apiKey: API_KEY, + template: TEMPLATE, + defaultHomeDir: " ", + sandbox: statics, + }), + /default home directory must not be empty/u, + ); + assert.equal(calls.create.length, 0); + }); }); describe("E2BSandboxRuntime launch and lookup", () => { + it("must-fire: carries the template home directory onto launch and lookup handles", async () => { + const { statics } = fakeStatics({ + pages: [[sandboxInfo("sbx-existing", "running")]], + }); + const runtime = createRuntime(statics, { defaultHomeDir: "/home/user" }); + + const launched = await runtime.launch(); + const [lookedUp] = await runtime.findAllByLabels({ purpose: "worker" }); + + assert.equal(launched.homeDir, "/home/user"); + assert.equal(lookedUp?.homeDir, "/home/user"); + }); + + it("must-not-fire: blank attach and lookup home overrides cannot erase the default home", async () => { + const connected = fakeSandbox({ id: "sbx-attached" }); + const { statics } = fakeStatics({ + connected: new Map([["sbx-lookup", connected]]), + }); + const runtime = createRuntime(statics, { defaultHomeDir: "/home/user" }); + + const lookedUp = await runtime.getById("sbx-lookup", { homeDir: " " }); + const attached = runtime.attachSandbox(connected, { homeDir: " " }); + + assert.equal(lookedUp?.homeDir, "/home/user"); + assert.equal(attached.homeDir, "/home/user"); + }); + it("review guard: applies an explicit sandbox lifetime to create, reattach, and sync execution", async () => { const created = fakeSandbox({ id: "sbx-lifetime-created" }); const reattached = fakeSandbox({ id: "sbx-lifetime-reattached" }); @@ -1267,6 +1309,7 @@ function createRuntime( syncRunBudgetMs?: number; createTimeoutMs?: number; sandboxLifetimeMs?: number; + defaultHomeDir?: string; } = {}, ) { return new E2BSandboxRuntime({ diff --git a/src/e2b/runtime.ts b/src/e2b/runtime.ts index 7ad0023..923d0f1 100644 --- a/src/e2b/runtime.ts +++ b/src/e2b/runtime.ts @@ -122,6 +122,7 @@ export interface E2BSandboxStatics { } export interface E2BAttachedSandboxOptions { + /** Optional handle home; blank values fall back to the runtime default. */ homeDir?: string; workdir?: string; owned?: boolean; @@ -167,6 +168,8 @@ export type E2BSandboxRuntimeOptions = { apiKey: string; /** Account-specific template or snapshot identifier used for every launch. */ template: string; + /** Stable home directory supplied by the selected E2B template. */ + defaultHomeDir?: string; /** Maximum lifetime granted to an asynchronous command and its sandbox. */ runBudgetMs?: number; /** @@ -217,6 +220,7 @@ export class E2BSandboxRuntime implements SandboxRuntime, WorkflowRuntime { private readonly apiKey: string; private readonly template: string; + private readonly defaultHomeDir?: string; private readonly runBudgetMs: number; private readonly syncRunBudgetMs: number; private readonly sandboxLifetimeMs: number; @@ -235,8 +239,13 @@ export class E2BSandboxRuntime implements SandboxRuntime, WorkflowRuntime { if (!template) { throw new Error("E2B sandbox template is required"); } + const defaultHomeDir = options.defaultHomeDir?.trim(); + if (options.defaultHomeDir !== undefined && !defaultHomeDir) { + throw new Error("E2B default home directory must not be empty"); + } this.apiKey = apiKey; this.template = template; + this.defaultHomeDir = defaultHomeDir; this.runBudgetMs = positiveDuration(options.runBudgetMs, DEFAULT_RUN_BUDGET_MS); this.syncRunBudgetMs = positiveDuration(options.syncRunBudgetMs, this.runBudgetMs); this.sandboxLifetimeMs = positiveDuration( @@ -362,6 +371,7 @@ export class E2BSandboxRuntime implements SandboxRuntime, WorkflowRuntime { }); return this.registerSandbox(sandbox, { owned: true, + homeDir: this.defaultHomeDir, workdir: options.workdir, }); } @@ -393,7 +403,7 @@ export class E2BSandboxRuntime implements SandboxRuntime, WorkflowRuntime { ): RuntimeHandle { return this.registerSandbox(sandbox, { owned: options.owned ?? false, - homeDir: options.homeDir, + homeDir: this.resolveHomeDir(options.homeDir), workdir: options.workdir, }); } @@ -779,7 +789,9 @@ export class E2BSandboxRuntime implements SandboxRuntime, WorkflowRuntime { }); return { ...handleFromE2BInfo(info), - ...(options.homeDir ? { homeDir: options.homeDir } : {}), + ...(this.resolveHomeDir(options.homeDir) + ? { homeDir: this.resolveHomeDir(options.homeDir) } + : {}), ...(options.workdir ? { workdir: options.workdir } : {}), }; } @@ -796,11 +808,18 @@ export class E2BSandboxRuntime implements SandboxRuntime, WorkflowRuntime { return { id: sandbox.sandboxId, state: "STARTED", - ...(options.homeDir ? { homeDir: options.homeDir } : {}), + ...(this.resolveHomeDir(options.homeDir) + ? { homeDir: this.resolveHomeDir(options.homeDir) } + : {}), ...(options.workdir ? { workdir: options.workdir } : {}), }; } + private resolveHomeDir(homeDir?: string): string | undefined { + const normalized = homeDir?.trim(); + return normalized || this.defaultHomeDir; + } + private async requireSandbox(handle: RuntimeHandle): Promise { const registered = this.registrations.get(handle.id); if (registered?.state === "paused") {