diff --git a/doc/sandbox-work-folders.md b/doc/sandbox-work-folders.md index 627b348d63b..95f94619ff8 100644 --- a/doc/sandbox-work-folders.md +++ b/doc/sandbox-work-folders.md @@ -7,6 +7,12 @@ publication. Repeated error cleanup keeps the original failure visible. A later authorized run recovers unsaved edits from the retained sandbox before loading incoming shared files; it does not rewrite the failed run as successful. +Reusable sandbox resume resolves provider configuration from the recorded lease, +as workspace operations and cleanup do. This preserves provider-selected defaults +such as Daytona's region when the environment leaves them unspecified. Existing +configuration and identity checks still reject incompatible reuse; stopping and +resuming a compatible lease must reopen the same account-scoped provider handle. + The deployed acceptance entry point is `pnpm test:e2e:work-folders:deployed`. Set `PAPERCLIP_DEPLOYED_STACK_MANIFEST` to a JSON manifest matching `tests/runner-e2e/deployed-stack.ts`, `PAPERCLIP_DEPLOYED_STACK_AUTH` to a private diff --git a/server/src/__tests__/environment-runtime.test.ts b/server/src/__tests__/environment-runtime.test.ts index 58f247b7e54..00477336399 100644 --- a/server/src/__tests__/environment-runtime.test.ts +++ b/server/src/__tests__/environment-runtime.test.ts @@ -668,6 +668,61 @@ describeEmbeddedPostgres("environmentRuntimeService", () => { expect(retry.lease.metadata?.sandboxLeaseAcquisition).toEqual({ outcome: "resumed" }); }); + it.each(["codex_local", "paperclip_runner"])("resumes %s in the provider region recorded by the lease", async (adapterType) => { + const seeded = await seedReusablePluginSandboxLease(adapterType); + // The environment leaves region selection to the provider. The provider + // records its resolved region, which execute/realize/release already use. + expect(seeded.environment.config).not.toHaveProperty("target"); + await environmentService(db).updateLeaseMetadata(seeded.reusableLease.id, { + ...seeded.reusableLease.metadata, + target: "us", + }); + const closedRegions = new Set(); + const workerManager = { + isRunning: vi.fn(() => true), + getWorker: vi.fn(() => ({ supportedMethods: ["environmentResumeLease", "environmentReleaseLease", "environmentDestroyLease"] })), + call: vi.fn(async (_pluginId: string, method: string, params: { config: { target?: string } }) => { + if (method === "environmentReleaseLease") { + closedRegions.add(params.config.target); + return; + } + if (method === "environmentResumeLease") { + closedRegions.delete(params.config.target); + return { providerLeaseId: seeded.reusableLease.providerLeaseId, metadata: { + provider: "fake-plugin", image: "fake:test", timeoutMs: 1234, reuseLease: true, target: "us", + } }; + } + if (method === "environmentRealizeWorkspace") { + if (closedRegions.has(params.config.target)) throw new Error("Sandbox lease is no longer active"); + return { cwd: "/workspace" }; + } + throw new Error(`Unexpected provider operation: ${method}`); + }), + } as unknown as PluginWorkerManager; + const runtime = environmentRuntimeService(db, { pluginWorkerManager: workerManager }); + await runtime.acquireRunLease({ + companyId: seeded.companyId, environment: seeded.environment, agentId: seeded.agentId, + heartbeatRunId: seeded.runId, issueId: null, adapterType, + persistedExecutionWorkspace: { id: seeded.executionWorkspaceId, mode: "shared_workspace" }, + }); + await runtime.releaseRunLeases(seeded.runId, "released", undefined, "stop_and_retain"); + expect(closedRegions).toEqual(new Set(["us"])); + const nextRunId = randomUUID(); + await db.insert(heartbeatRuns).values({ id: nextRunId, companyId: seeded.companyId, agentId: seeded.agentId, status: "running" }); + const acquired = await runtime.acquireRunLease({ + companyId: seeded.companyId, environment: seeded.environment, agentId: seeded.agentId, + heartbeatRunId: nextRunId, issueId: null, adapterType, + persistedExecutionWorkspace: { id: seeded.executionWorkspaceId, mode: "shared_workspace" }, + }); + expect(acquired.lease.providerLeaseId).toBe(seeded.reusableLease.providerLeaseId); + await expect(runtime.realizeWorkspace({ + environment: seeded.environment, lease: acquired.lease, + workspace: { localPath: "/workspace", mode: "shared_workspace" }, + })).resolves.toMatchObject({ cwd: "/workspace" }); + expect(workerManager.call).toHaveBeenCalledWith(seeded.pluginId, "environmentResumeLease", + expect.objectContaining({ config: expect.objectContaining({ target: "us" }) }), expect.any(Number)); + }); + it("keeps an existing task's legacy sync contract after its sandbox expires, without affecting new tasks", async () => { const seeded = await seedReusablePluginSandboxLease("codex_local"); const taskId = randomUUID(); diff --git a/server/src/services/environment-runtime.ts b/server/src/services/environment-runtime.ts index 6cbfc082a6b..ee49428295b 100644 --- a/server/src/services/environment-runtime.ts +++ b/server/src/services/environment-runtime.ts @@ -2033,9 +2033,19 @@ function createSandboxEnvironmentDriver( reusableLease = await claimReusableLeaseBeforeResume(reusableLease, input); if (!reusableLease.providerLeaseId) throw new Error("Reusable sandbox claim lost its provider identity"); try { + // Use the same effective provider configuration as execute and + // release. Providers can resolve an omitted region at creation; + // resuming with the original default would open a different + // account-scoped handle from the one that cleanup closed. + const resumeConfig = await resolvePluginSandboxRuntimeConfig({ + environment: input.environment, + lease: reusableLease, + provider: parsed.config.provider, + }); + const resumeWorkerConfig = stripSandboxProviderEnvelope(resumeConfig as SandboxEnvironmentConfig); const resumeDeadline = Date.now() + 60_000; const configuredResumeTimeoutMs = - resolvePluginSandboxRpcTimeoutMs(workerConfig) ?? 60_000; + resolvePluginSandboxRpcTimeoutMs(resumeWorkerConfig) ?? 60_000; let retryDelayMs = 250; let resumed: PluginEnvironmentLease; while (true) { @@ -2048,7 +2058,7 @@ function createSandboxEnvironmentDriver( companyId: input.companyId, environmentId: input.environment.id, issueId: input.issueId, - config: workerConfig, + config: resumeWorkerConfig, providerLeaseId: reusableLease.providerLeaseId, leaseMetadata: reusableLease.metadata ?? undefined, },