From ee72132a44e2da2cb9539b40f88d057ea7a3cdd6 Mon Sep 17 00:00:00 2001 From: Devin Foley Date: Thu, 5 Nov 2026 11:39:00 -0800 Subject: [PATCH] fix(daytona): bound one-shot exec requests by their command deadline (#19298) Bound the Daytona one-shot executeCommand request at its command deadline plus 5 s and return an attributed timed-out result, instead of waiting for the host RPC deadline. Bound the stdin cleanup the same way. Co-Authored-By: Claude Opus 5.5 --- .../src/one-shot-exec-deadline.test.ts | 76 +++++++++++++++++++ .../sandbox-providers/daytona/src/plugin.ts | 22 +++++- 2 files changed, 96 insertions(+), 2 deletions(-) create mode 100644 packages/plugins/sandbox-providers/daytona/src/one-shot-exec-deadline.test.ts diff --git a/packages/plugins/sandbox-providers/daytona/src/one-shot-exec-deadline.test.ts b/packages/plugins/sandbox-providers/daytona/src/one-shot-exec-deadline.test.ts new file mode 100644 index 00000000000..c52b678d9e8 --- /dev/null +++ b/packages/plugins/sandbox-providers/daytona/src/one-shot-exec-deadline.test.ts @@ -0,0 +1,76 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +const getSandbox = vi.hoisted(() => vi.fn()); +vi.mock("@daytonaio/sdk", () => ({ + Daytona: class { get = getSandbox; }, + DaytonaNotFoundError: class extends Error {}, + DaytonaTimeoutError: class extends Error {}, +})); + +import plugin, { __resetDaytonaSandboxHandleCacheForTest } from "./plugin.js"; + +function fixture() { + const process = { executeCommand: vi.fn() }; + const fs = { uploadFile: vi.fn().mockResolvedValue(undefined), deleteFile: vi.fn().mockResolvedValue(undefined) }; + getSandbox.mockResolvedValue({ id: "fixture-sandbox", state: "started", process, fs }); + const execute = (stdin?: string) => plugin.definition.onEnvironmentExecute!({ + driverKey: "daytona", companyId: "fixture-company", environmentId: "fixture-environment", + config: { apiKey: "fixture-not-a-key", timeoutMs: 300_000 }, bypassSession: true, + lease: { providerLeaseId: "fixture-sandbox", metadata: {} }, + command: "/pack/bin/opencode", args: ["--version"], timeoutMs: 30_000, + ...(stdin !== undefined ? { stdin } : {}), + }); + return { process, fs, execute }; +} + +describe("Daytona one-shot exec request deadline", () => { + beforeEach(() => { + __resetDaytonaSandboxHandleCacheForTest(); + getSandbox.mockReset(); + vi.useFakeTimers(); + }); + afterEach(() => { + vi.useRealTimers(); + __resetDaytonaSandboxHandleCacheForTest(); + }); + + it("returns a timed-out result when the provider exec request outlives the command deadline", async () => { + const { process, execute } = fixture(); + process.executeCommand.mockImplementation(() => new Promise(() => undefined)); + let settled = false; + const pending = execute().finally(() => { settled = true; }); + await vi.advanceTimersByTimeAsync(34_999); + expect(settled).toBe(false); + await vi.advanceTimersByTimeAsync(1); + expect(await pending).toMatchObject({ + exitCode: null, timedOut: true, + metadata: { timeoutScope: "provider_exec_request", commandExitConfirmed: false }, + }); + expect(process.executeCommand).toHaveBeenCalledOnce(); + }); + + it("keeps a slow exec that answers within the request deadline", async () => { + const { process, execute } = fixture(); + process.executeCommand.mockImplementation(() => new Promise(resolve => setTimeout( + () => resolve({ exitCode: 0, result: "1.2.3", artifacts: { stdout: "1.2.3" } }), 34_000))); + const pending = execute(); + await vi.advanceTimersByTimeAsync(34_000); + expect(await pending).toMatchObject({ exitCode: 0, timedOut: false, stdout: "1.2.3" }); + }); + + it("bounds a stalled stdin cleanup after a timed-out exec request", async () => { + const { process, fs, execute } = fixture(); + process.executeCommand.mockImplementation(() => new Promise(() => undefined)); + fs.deleteFile.mockImplementation(() => new Promise(() => undefined)); + let settled = false; + const pending = execute("input").finally(() => { settled = true; }); + await vi.advanceTimersByTimeAsync(39_999); + expect(settled).toBe(false); + await vi.advanceTimersByTimeAsync(1); + expect(await pending).toMatchObject({ + exitCode: null, timedOut: true, + metadata: { timeoutScope: "provider_exec_request", commandExitConfirmed: false }, + }); + expect(fs.deleteFile).toHaveBeenCalledOnce(); + }); +}); diff --git a/packages/plugins/sandbox-providers/daytona/src/plugin.ts b/packages/plugins/sandbox-providers/daytona/src/plugin.ts index d311516412e..488e6480b4b 100644 --- a/packages/plugins/sandbox-providers/daytona/src/plugin.ts +++ b/packages/plugins/sandbox-providers/daytona/src/plugin.ts @@ -1941,7 +1941,11 @@ async function executeOneShot( // attribute a step's exec time to the provider boundary through the // free-form `metadata.durationMs`. execStart = timingNow(); - const result = await sandbox.process.executeCommand(command, undefined, undefined, timeoutSeconds); + const result = await withLivenessTimeout( + "process.executeCommand", + timeoutSeconds * 1_000 + LIVENESS_START_TIMEOUT_MARGIN_MS, + () => sandbox.process.executeCommand(command, undefined, undefined, timeoutSeconds), + ); const durationMs = timingNow() - execStart; return { @@ -1952,6 +1956,19 @@ async function executeOneShot( metadata: { durationMs }, }; } catch (error) { + if (error instanceof SandboxLivenessTimeoutError && execStart != null) { + return { + exitCode: null, + timedOut: true, + stdout: "", + stderr: `${error.message}\n`, + metadata: { + durationMs: timingNow() - execStart, + timeoutScope: "provider_exec_request", + commandExitConfirmed: false, + }, + }; + } if (error instanceof DaytonaTimeoutError) { const timeoutMessage = gitNet ? `Git network operation timed out after ${Math.round(effectiveTimeoutMs / 1000)} s — the remote may be unreachable or noninteractive credentials are not configured.` @@ -1972,7 +1989,8 @@ async function executeOneShot( throw error; } finally { if (stdinPath) { - await sandbox.fs.deleteFile(stdinPath).catch(() => undefined); + await withLivenessTimeout("fs.deleteFile", LIVENESS_START_TIMEOUT_MARGIN_MS, + () => sandbox.fs.deleteFile(stdinPath)).catch(() => undefined); } } }