Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion apps/worker/src/automation-sandbox.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -558,7 +558,9 @@ describe("fresh automation sandbox", () => {

await runInFreshAutomationSandbox({ ...input, keepPaused: true, resumeState: { sandboxId: "sandbox-1" } }, dependencies);

expect(client.resume).toHaveBeenCalledWith({ sandboxId: "sandbox-1" });
// The saved state leaves out the API key; the resumed session's file
// transfers need it.
expect(client.resume).toHaveBeenCalledWith({ apiKey: "daytona-test", sandboxId: "sandbox-1" });
expect(dependencies.createSession).not.toHaveBeenCalled();
expect(dependencies.configure).not.toHaveBeenCalled();
expect(dependencies.prepare).not.toHaveBeenCalled();
Expand Down
3 changes: 2 additions & 1 deletion apps/worker/src/automation-sandbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
maxDaytonaSandboxBaseNameLength,
prepareDaytonaSandbox,
replaceDaytonaSandboxSecrets,
resumeDaytonaSandbox,
sandboxesLeftAfterFailedCreation,
type DaytonaSandboxSecretMount,
} from "./sandbox.js";
Expand Down Expand Up @@ -265,7 +266,7 @@ export async function runInFreshAutomationSandbox<T>(
if (input.resumeState) {
try {
session = await abortable(
client.resume(await client.deserializeSessionState(input.resumeState)),
resumeDaytonaSandbox(client, input.resumeState, input.config),
input.signal,
);
// Daytona recreates a sandbox it no longer has; that one needs setup.
Expand Down
5 changes: 2 additions & 3 deletions apps/worker/src/investigate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ import {
createDaytonaSandboxSession,
prepareDaytonaSandbox,
pauseDaytonaSandbox,
resumeDaytonaSandbox,
} from "./sandbox.js";
import { startSandboxMeter, type SandboxMeter } from "./sandbox-metering.js";
import { agentUsageIsBillable, recordAgentRunUsage } from "./agent-usage.js";
Expand Down Expand Up @@ -967,9 +968,7 @@ export async function runInvestigationAgent(
const persistedState = sessionRuntime?.sandboxSessionState;
if (threadMode && persistedState) {
try {
session = await client.resume(
await client.deserializeSessionState(persistedState),
);
session = await resumeDaytonaSandbox(client, persistedState, config);
} catch (error) {
// Daytona deletes a paused thread sandbox after a day; the thread
// continues in a fresh one.
Expand Down
31 changes: 30 additions & 1 deletion apps/worker/src/sandbox.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import type { DaytonaSandboxSession } from "@openai/agents-extensions/sandbox/daytona";
import type {
DaytonaSandboxClient,
DaytonaSandboxSession,
} from "@openai/agents-extensions/sandbox/daytona";
import { describe, expect, it, vi } from "vitest";
import {
closeDaytonaSandbox,
Expand All @@ -10,6 +13,7 @@ import {
prepareDaytonaPatchSandbox,
prepareDaytonaSandbox,
replaceDaytonaSandboxSecrets,
resumeDaytonaSandbox,
sandboxDeletedAfterFailedCreation,
sandboxesLeftAfterFailedCreation,
} from "./sandbox.js";
Expand Down Expand Up @@ -776,6 +780,31 @@ describe("Daytona sandbox cleanup", () => {
});
});

describe("Daytona sandbox resume", () => {
it("gives the resumed session the API key its saved state left out", async () => {
const session = { state: { sandboxId: "sandbox-1" } };
const client = {
deserializeSessionState: vi.fn(async (state: Record<string, unknown>) => ({
...state,
apiKey: undefined,
})),
resume: vi.fn().mockResolvedValue(session),
};

await expect(
resumeDaytonaSandbox(
client as unknown as DaytonaSandboxClient,
{ sandboxId: "sandbox-1" },
{ daytonaApiKey: "daytona-key" },
),
).resolves.toBe(session);
expect(client.resume).toHaveBeenCalledWith({
apiKey: "daytona-key",
sandboxId: "sandbox-1",
});
});
});

describe("Daytona thread sandbox pause", () => {
it("stops the sandbox through the session", async () => {
const harness = cleanupHarness();
Expand Down
11 changes: 11 additions & 0 deletions apps/worker/src/sandbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -497,6 +497,17 @@ export async function replaceDaytonaSandboxSecrets(
}
}

// A paused sandbox's saved state leaves out the API key. The resumed
// session gets it back, because its file transfers read it from the state.
export async function resumeDaytonaSandbox(
client: DaytonaSandboxClient,
persistedState: Record<string, unknown>,
config: DaytonaClientConfig,
): Promise<DaytonaSandboxSession> {
const state = await client.deserializeSessionState(persistedState);
return client.resume({ ...state, apiKey: config.daytonaApiKey });
}

// A thread sandbox that fails to stop keeps running until Daytona's
// auto-stop, and the next turn resumes or replaces it. The failure is
// reported without failing the turn that already has its answer.
Expand Down
Loading