perf(plugin-daytona): cache the started sandbox handle per lease (#10335)

## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The Daytona sandbox adapter spends time on repeated per-exec sandbox
lookups during a lease
> - That repeated lookup is pure overhead once the started sandbox
handle is already known and trusted for the lease
> - The adapter still needs strict isolation and fail-closed behavior
because the handle is an authenticated compute object, not an inert
value
> - This pull request memoizes the started sandbox handle per lease in a
process-scoped cache, and now advances freshness only after successful
reuse so failed commands cannot suppress stale-handle refreshes
> - The benefit is lower provider-get latency on the hot path while
keeping resume safety, teardown safety, and observability intact

## Linked Issues or Issue Description

This is a Daytona performance fix, not a standalone public GitHub issue.

### Problem / Motivation

- Repeated `client.get(sandboxId)` calls on the sandbox hot path
re-fetch a handle that is already started and trusted for the current
lease.
- The extra provider round-trip is pure overhead on repeated exec, sync,
resume, and interactive-cancel flows.
- Cache freshness also has to be tied to successful reuse, or a failed
command can make a stale sandbox look freshly used.

### Proposed Solution

- Cache the started `Sandbox` handle in process memory, keyed by a
non-secret composite lease scope.
- Enforce strict identity checks and eviction on release, destroy,
interactive cancel, and resume-sentinel mismatch.
- Block teardown cleanup until active lease operations finish so
delete/stop cannot race in-flight execute or sync work.
- Advance freshness only after successful execute, sync, or resume
reuse, so failed operations do not mask an auto-stopped sandbox.
- Preserve `getDurationMs` in exec metadata so provider-get latency
remains observable.

### Alternatives Considered

- Keep fetching the sandbox on every exec path.
- Cache only by bare lease id.

### Roadmap Alignment

- This change is part of the Daytona performance work and narrows
per-call overhead without changing the public adapter contract.

## What Changed

- Memoized the started Daytona `Sandbox` handle in a process-scoped
cache keyed by a composite lease scope.
- Added fail-closed identity checks so cache hits and single-flight
populate paths reject mismatched sandbox ids.
- Evicted cached handles on release, destroy, interactive cancel, and
resume sentinel mismatch.
- Blocked release, destroy, and interactive cancel teardown cleanup
until active lease operations drain.
- Advanced cache freshness only after successful execute, sync, and
resume reuse.
- Kept `getDurationMs` in exec metadata so provider-get latency remains
observable.
- Expanded the Daytona plugin tests to cover same-lease reuse,
cross-scope isolation, eviction paths, rejected populate handling,
concurrent single-flight behavior, cached-resume sentinel revalidation,
teardown cancellation safety, and failed-execute freshness handling.

## Verification

- `pnpm exec vitest run --config vitest.config.ts` in
`packages/plugins/sandbox-providers/daytona` — 81/81 passing, including
the teardown-cancel, snapshot-capture, syncIn-cancel, and failed-execute
freshness regressions.
- `git rev-parse origin/perf/daytona-sandbox-handle-cache` matched the
authorized submit SHA `528158f998fa88bb0748300f156b38d86d6589cd` before
the fixup commits.
- `git log --oneline
origin/master..origin/perf/daytona-sandbox-handle-cache` shows the
expected focused Daytona changes.
- GitHub duplicate/related PR search and ROADMAP review were completed
before opening the PR.
- Remote CI and Greptile completed successfully after this description
was updated; the PR is now ready for board handoff.

## Risks

- The cache is process-scoped, so correctness depends on the eviction
paths staying complete.
- A bug in the scope key or identity checks could leak reuse across the
wrong lease boundaries, but the implementation fails closed on id
mismatches.
- Teardown now waits for active operations to drain, so any missed
activity bookkeeping could delay cleanup instead of racing it.
- Freshness updates now happen after success, which is safer, but it
means any missed success-path call would trigger an extra refresh rather
than silently masking staleness.

## Model Used

OpenAI GPT-5 via Codex, tool-using coding agent; exact context window
not surfaced in the workspace.

## 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 searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Nicky Leach 2026-07-27 19:14:53 -07:00 committed by GitHub
parent c274f10abc
commit 3d23c3b2c3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 1442 additions and 130 deletions

View File

@ -28,7 +28,11 @@ vi.mock("@daytonaio/sdk", () => ({
DaytonaTimeoutError: MockDaytonaTimeoutError, DaytonaTimeoutError: MockDaytonaTimeoutError,
})); }));
import plugin, { setDaytonaTimingClockForTest } from "./plugin.js"; import plugin, {
setDaytonaTimingClockForTest,
setDaytonaHandleFreshnessClockForTest,
__resetDaytonaSandboxHandleCacheForTest,
} from "./plugin.js";
import manifest from "./manifest.js"; import manifest from "./manifest.js";
function createMockSandbox(overrides: { function createMockSandbox(overrides: {
@ -50,6 +54,10 @@ function createMockSandbox(overrides: {
start: vi.fn().mockResolvedValue(undefined), start: vi.fn().mockResolvedValue(undefined),
stop: vi.fn().mockResolvedValue(undefined), stop: vi.fn().mockResolvedValue(undefined),
recover: vi.fn().mockResolvedValue(undefined), recover: vi.fn().mockResolvedValue(undefined),
// Real `refreshData` re-reads live provider state and mutates `state` in
// place; the default mock leaves state untouched, and tests that exercise a
// provider-initiated auto-stop override it to flip `state` to "stopped".
refreshData: vi.fn().mockResolvedValue(undefined),
resize: vi.fn().mockResolvedValue(undefined), resize: vi.fn().mockResolvedValue(undefined),
delete: vi.fn().mockResolvedValue(undefined), delete: vi.fn().mockResolvedValue(undefined),
archive: vi.fn().mockResolvedValue(undefined), archive: vi.fn().mockResolvedValue(undefined),
@ -87,6 +95,9 @@ describe("Daytona sandbox provider plugin", () => {
mockSnapshotDelete.mockReset(); mockSnapshotDelete.mockReset();
vi.restoreAllMocks(); vi.restoreAllMocks();
delete process.env.DAYTONA_API_KEY; delete process.env.DAYTONA_API_KEY;
// The started-sandbox handle cache is process-scoped; clear it between tests
// so a handle memoized under a reused composite key never leaks forward.
__resetDaytonaSandboxHandleCacheForTest();
}); });
it("declares environment lifecycle handlers", async () => { it("declares environment lifecycle handlers", async () => {
@ -1295,6 +1306,767 @@ describe("Daytona sandbox provider plugin", () => {
expect(result).toMatchObject({ exitCode: null, timedOut: true }); expect(result).toMatchObject({ exitCode: null, timedOut: true });
expect(result?.stderr).toMatch(/unreachable|credentials/i); expect(result?.stderr).toMatch(/unreachable|credentials/i);
}); });
// ─── Per-lease started-sandbox handle cache ────────────────────────────────
// These prove the security conditions: single-fetch-per-lease, strict
// composite-key isolation (no cross-lease / cross-company / cross-env reuse),
// eviction at every teardown, no caching of failed populates, single-flight
// concurrency, and sentinel re-verification on a cached resume — plus that a
// handle left idle past the provider auto-stop window is refreshed before
// reuse so a provider-initiated stop is not hidden behind a stale snapshot.
describe("started-sandbox handle cache", () => {
function execParams(
providerLeaseId: string,
overrides: { companyId?: string; environmentId?: string; driverKey?: string } = {},
) {
return {
driverKey: overrides.driverKey ?? "daytona",
companyId: overrides.companyId ?? "company-1",
environmentId: overrides.environmentId ?? "env-1",
config: { timeoutMs: 300000, reuseLease: false },
lease: { providerLeaseId, metadata: {} },
command: "printf",
args: ["hi"],
timeoutMs: 1000,
};
}
it("reuses the cached handle across execs on one lease (single client.get)", async () => {
process.env.DAYTONA_API_KEY = "host-key";
const sandbox = createMockSandbox({ id: "lease-a" });
mockGet.mockResolvedValue(sandbox);
await plugin.definition.onEnvironmentExecute?.(execParams("lease-a"));
await plugin.definition.onEnvironmentExecute?.(execParams("lease-a"));
// Second exec is served from the cache: no second REST re-fetch.
expect(mockGet).toHaveBeenCalledTimes(1);
expect(sandbox.process.executeCommand).toHaveBeenCalledTimes(2);
});
it("keeps getDurationMs present (≈0) on a cache hit", async () => {
process.env.DAYTONA_API_KEY = "host-key";
const sandbox = createMockSandbox({ id: "lease-a" });
mockGet.mockResolvedValue(sandbox);
await plugin.definition.onEnvironmentExecute?.(execParams("lease-a"));
const hit = await plugin.definition.onEnvironmentExecute?.(execParams("lease-a"));
// The observability contract holds even when the fetch is elided.
expect(typeof (hit!.metadata as Record<string, unknown>)?.getDurationMs).toBe("number");
});
it("never serves lease A's handle to lease B (distinct fetch per lease)", async () => {
process.env.DAYTONA_API_KEY = "host-key";
const sandboxA = createMockSandbox({ id: "lease-a" });
const sandboxB = createMockSandbox({ id: "lease-b" });
mockGet.mockImplementation(async (id: string) => (id === "lease-a" ? sandboxA : sandboxB));
await plugin.definition.onEnvironmentExecute?.(execParams("lease-a"));
await plugin.definition.onEnvironmentExecute?.(execParams("lease-b"));
expect(mockGet).toHaveBeenCalledTimes(2);
expect(sandboxA.process.executeCommand).toHaveBeenCalledTimes(1);
expect(sandboxB.process.executeCommand).toHaveBeenCalledTimes(1);
});
it("does not share a handle across companies or environments for the same providerLeaseId", async () => {
process.env.DAYTONA_API_KEY = "host-key";
mockGet.mockImplementation(async () => createMockSandbox({ id: "sandbox-x" }));
await plugin.definition.onEnvironmentExecute?.(execParams("sandbox-x", { companyId: "company-1", environmentId: "env-1" }));
await plugin.definition.onEnvironmentExecute?.(execParams("sandbox-x", { companyId: "company-2", environmentId: "env-1" }));
await plugin.definition.onEnvironmentExecute?.(execParams("sandbox-x", { companyId: "company-1", environmentId: "env-2" }));
// Three distinct composite keys → three independent fetches; the bare
// providerLeaseId is never a shared cache slot.
expect(mockGet).toHaveBeenCalledTimes(3);
});
it("rejects a queued execute after release teardown closes the lease", async () => {
process.env.DAYTONA_API_KEY = "host-key";
mockGet.mockImplementation(async () => createMockSandbox({ id: "lease-a" }));
await plugin.definition.onEnvironmentExecute?.(execParams("lease-a")); // miss → get #1 (cached)
const releasePromise = plugin.definition.onEnvironmentReleaseLease?.({
driverKey: "daytona",
companyId: "company-1",
environmentId: "env-1",
providerLeaseId: "lease-a",
config: { timeoutMs: 300000, reuseLease: false },
});
await new Promise((resolve) => setTimeout(resolve, 0));
const queuedExecute = plugin.definition.onEnvironmentExecute?.(execParams("lease-a"));
await expect(queuedExecute).rejects.toThrow(/no longer active/);
await releasePromise;
// The tombstone closes the lease, so the queued execute never reacquires
// the sandbox after teardown.
expect(mockGet).toHaveBeenCalledTimes(1);
});
it("rejects an overlapping execute after release teardown closes the lease", async () => {
process.env.DAYTONA_API_KEY = "host-key";
const sandbox = createMockSandbox({ id: "lease-a" });
let stopStarted = false;
let resolveRelease!: () => void;
const releaseGate = new Promise<void>((resolve) => {
resolveRelease = resolve;
});
sandbox.stop.mockImplementation(() => {
stopStarted = true;
return releaseGate;
});
mockGet.mockResolvedValue(sandbox);
await plugin.definition.onEnvironmentExecute?.(execParams("lease-a"));
const releasePromise = plugin.definition.onEnvironmentReleaseLease?.({
driverKey: "daytona",
companyId: "company-1",
environmentId: "env-1",
providerLeaseId: "lease-a",
config: { timeoutMs: 300000, reuseLease: true },
});
await new Promise((resolve) => setTimeout(resolve, 0));
expect(stopStarted).toBe(true);
const overlappingExec = plugin.definition.onEnvironmentExecute?.(execParams("lease-a"));
await expect(overlappingExec).rejects.toThrow(/no longer active/);
resolveRelease();
await releasePromise;
expect(mockGet).toHaveBeenCalledTimes(1);
expect(sandbox.stop).toHaveBeenCalledTimes(1);
expect(sandbox.process.executeCommand).toHaveBeenCalledTimes(1);
});
it("rejects a late execute after the release tombstone is set", async () => {
process.env.DAYTONA_API_KEY = "host-key";
const sandbox = createMockSandbox({ id: "lease-a" });
let stopStarted = false;
let resolveRelease!: () => void;
const releaseGate = new Promise<void>((resolve) => {
resolveRelease = resolve;
});
sandbox.stop.mockImplementation(() => {
stopStarted = true;
return releaseGate;
});
mockGet.mockResolvedValue(sandbox);
await plugin.definition.onEnvironmentExecute?.(execParams("lease-a"));
const releasePromise = plugin.definition.onEnvironmentReleaseLease?.({
driverKey: "daytona",
companyId: "company-1",
environmentId: "env-1",
providerLeaseId: "lease-a",
config: { timeoutMs: 300000, reuseLease: true },
});
await new Promise((resolve) => setTimeout(resolve, 0));
expect(stopStarted).toBe(true);
const overlappingExec = plugin.definition.onEnvironmentExecute?.(execParams("lease-a"));
await expect(overlappingExec).rejects.toThrow(/no longer active/);
resolveRelease();
await releasePromise;
expect(mockGet).toHaveBeenCalledTimes(1);
expect(sandbox.stop).toHaveBeenCalledTimes(1);
expect(sandbox.process.executeCommand).toHaveBeenCalledTimes(1);
});
it("waits for an in-flight execute before teardown cleanup starts", async () => {
process.env.DAYTONA_API_KEY = "host-key";
const sandbox = createMockSandbox({ id: "lease-a" });
let resolveExecute!: () => void;
sandbox.process.executeCommand.mockImplementation(async () => {
await new Promise<void>((resolve) => {
resolveExecute = resolve;
});
return {
exitCode: 0,
result: "bash",
artifacts: { stdout: "bash" },
};
});
mockGet.mockResolvedValue(sandbox);
const executePromise = plugin.definition.onEnvironmentExecute?.(execParams("lease-a"));
await new Promise((resolve) => setTimeout(resolve, 0));
const releasePromise = plugin.definition.onEnvironmentReleaseLease?.({
driverKey: "daytona",
companyId: "company-1",
environmentId: "env-1",
providerLeaseId: "lease-a",
config: { timeoutMs: 300000, reuseLease: true },
});
await new Promise((resolve) => setTimeout(resolve, 0));
expect(sandbox.stop).not.toHaveBeenCalled();
resolveExecute();
await Promise.all([executePromise, releasePromise]);
expect(mockGet).toHaveBeenCalledTimes(1);
expect(sandbox.process.executeCommand).toHaveBeenCalledTimes(1);
expect(sandbox.stop).toHaveBeenCalledTimes(1);
});
it("keeps the teardown gate closed until overlapping teardowns both finish", async () => {
process.env.DAYTONA_API_KEY = "host-key";
const sandbox = createMockSandbox({ id: "lease-a" });
let stopStarted = false;
let deleteStarted = false;
let resolveStop!: () => void;
let resolveDelete!: () => void;
const stopGate = new Promise<void>((resolve) => {
resolveStop = resolve;
});
const deleteGate = new Promise<void>((resolve) => {
resolveDelete = resolve;
});
sandbox.stop.mockImplementation(() => {
stopStarted = true;
return stopGate;
});
sandbox.delete.mockImplementation(() => {
deleteStarted = true;
return deleteGate;
});
mockGet.mockResolvedValue(sandbox);
await plugin.definition.onEnvironmentExecute?.(execParams("lease-a"));
const releasePromise = plugin.definition.onEnvironmentReleaseLease?.({
driverKey: "daytona",
companyId: "company-1",
environmentId: "env-1",
providerLeaseId: "lease-a",
config: { timeoutMs: 300000, reuseLease: true },
});
await new Promise((resolve) => setTimeout(resolve, 0));
expect(stopStarted).toBe(true);
const destroyPromise = plugin.definition.onEnvironmentDestroyLease?.({
driverKey: "daytona",
companyId: "company-1",
environmentId: "env-1",
providerLeaseId: "lease-a",
config: { timeoutMs: 300000, reuseLease: true },
});
await new Promise((resolve) => setTimeout(resolve, 0));
expect(deleteStarted).toBe(true);
resolveDelete();
await destroyPromise;
const overlappingExec = plugin.definition.onEnvironmentExecute?.(execParams("lease-a"));
await expect(overlappingExec).rejects.toThrow(/no longer active/);
resolveStop();
await releasePromise;
expect(mockGet).toHaveBeenCalledTimes(2);
expect(sandbox.stop).toHaveBeenCalledTimes(1);
expect(sandbox.delete).toHaveBeenCalledTimes(1);
expect(sandbox.process.executeCommand).toHaveBeenCalledTimes(1);
});
it("rejects a queued execute after destroy teardown closes the lease", async () => {
process.env.DAYTONA_API_KEY = "host-key";
mockGet.mockImplementation(async () => createMockSandbox({ id: "lease-a" }));
await plugin.definition.onEnvironmentExecute?.(execParams("lease-a"));
const destroyPromise = plugin.definition.onEnvironmentDestroyLease?.({
driverKey: "daytona",
companyId: "company-1",
environmentId: "env-1",
providerLeaseId: "lease-a",
config: { timeoutMs: 300000, reuseLease: false },
});
await new Promise((resolve) => setTimeout(resolve, 0));
const queuedExecute = plugin.definition.onEnvironmentExecute?.(execParams("lease-a"));
await expect(queuedExecute).rejects.toThrow(/no longer active/);
await destroyPromise;
expect(mockGet).toHaveBeenCalledTimes(1);
});
it("rejects a queued execute after interactive cancel closes the lease", async () => {
process.env.DAYTONA_API_KEY = "host-key";
mockGet.mockImplementation(async () => createMockSandbox({ id: "lease-a" }));
await plugin.definition.onEnvironmentExecute?.(execParams("lease-a"));
const cancelPromise = plugin.definition.onEnvironmentCancelInteractiveSetup?.({
driverKey: "daytona",
companyId: "company-1",
environmentId: "env-1",
providerLeaseId: "lease-a",
config: { timeoutMs: 300000, reuseLease: false },
reason: "cancelled",
});
await new Promise((resolve) => setTimeout(resolve, 0));
const queuedExecute = plugin.definition.onEnvironmentExecute?.(execParams("lease-a"));
await expect(queuedExecute).rejects.toThrow(/no longer active/);
await cancelPromise;
expect(mockGet).toHaveBeenCalledTimes(1);
});
it("waits for an in-flight execute before interactive cancel cleanup starts", async () => {
process.env.DAYTONA_API_KEY = "host-key";
const sandbox = createMockSandbox({ id: "lease-a" });
let resolveExecute!: () => void;
sandbox.process.executeCommand.mockImplementation(async () => {
await new Promise<void>((resolve) => {
resolveExecute = resolve;
});
return {
exitCode: 0,
result: "bash",
artifacts: { stdout: "bash" },
};
});
mockGet.mockResolvedValue(sandbox);
const executePromise = plugin.definition.onEnvironmentExecute?.(execParams("lease-a"));
await new Promise((resolve) => setTimeout(resolve, 0));
const cancelPromise = plugin.definition.onEnvironmentCancelInteractiveSetup?.({
driverKey: "daytona",
companyId: "company-1",
environmentId: "env-1",
providerLeaseId: "lease-a",
config: { timeoutMs: 300000, reuseLease: false },
reason: "cancelled",
});
await new Promise((resolve) => setTimeout(resolve, 0));
expect(sandbox.delete).not.toHaveBeenCalled();
resolveExecute();
await Promise.all([executePromise, cancelPromise]);
expect(mockGet).toHaveBeenCalledTimes(1);
expect(sandbox.process.executeCommand).toHaveBeenCalledTimes(1);
expect(sandbox.delete).toHaveBeenCalledTimes(1);
});
it("waits for an in-flight syncIn before interactive cancel cleanup starts", async () => {
process.env.DAYTONA_API_KEY = "host-key";
const hostDir = await fs.mkdtemp(path.join(os.tmpdir(), "daytona-cancel-sync-"));
const source = path.join(hostDir, "payload.txt");
await fs.writeFile(source, "payload");
const remoteDir = "/home/daytona/paperclip-workspace";
const sandbox = createMockSandbox({ id: "lease-a" });
let resolveUpload!: () => void;
sandbox.fs.uploadFiles.mockImplementation(async () => {
await new Promise<void>((resolve) => {
resolveUpload = resolve;
});
});
mockGet.mockResolvedValue(sandbox);
const syncPromise = plugin.definition.onEnvironmentSyncIn?.({
driverKey: "daytona",
companyId: "company-1",
environmentId: "env-1",
config: { timeoutMs: 300000, reuseLease: false },
lease: { providerLeaseId: "lease-a", metadata: { remoteCwd: remoteDir } },
operations: [
{
operationId: "sync-op-1",
files: [{ sourcePath: source, targetPath: `${remoteDir}/payload.txt`, kind: "file" }],
},
],
});
// Let syncIn register on the activity gate and reach the hung upload.
await new Promise((resolve) => setTimeout(resolve, 0));
const cancelPromise = plugin.definition.onEnvironmentCancelInteractiveSetup?.({
driverKey: "daytona",
companyId: "company-1",
environmentId: "env-1",
providerLeaseId: "lease-a",
config: { timeoutMs: 300000, reuseLease: false },
reason: "cancelled",
});
await new Promise((resolve) => setTimeout(resolve, 0));
// Cancel must drain the active sync before deleting the sandbox out from
// under it — the same activity-gate contract the execute path relies on.
expect(sandbox.delete).not.toHaveBeenCalled();
resolveUpload();
await Promise.all([syncPromise, cancelPromise]);
expect(sandbox.fs.uploadFiles).toHaveBeenCalledTimes(1);
expect(sandbox.delete).toHaveBeenCalledTimes(1);
await fs.rm(hostDir, { recursive: true, force: true });
});
it("rejects a queued execute once interactive cancel tombstones the lease", async () => {
process.env.DAYTONA_API_KEY = "host-key";
const sandbox = createMockSandbox({ id: "lease-a" });
let resolveFirstExecute!: () => void;
let cancelResolved = false;
let queuedExecuteRejected = false;
sandbox.process.executeCommand.mockImplementation(async () => {
await new Promise<void>((resolve) => {
resolveFirstExecute = resolve;
});
return {
exitCode: 0,
result: "bash",
artifacts: { stdout: "bash" },
};
});
mockGet.mockResolvedValue(sandbox);
const firstExecutePromise = plugin.definition.onEnvironmentExecute?.(execParams("lease-a"));
await new Promise((resolve) => setTimeout(resolve, 0));
const cancelPromise = plugin.definition.onEnvironmentCancelInteractiveSetup?.({
driverKey: "daytona",
companyId: "company-1",
environmentId: "env-1",
providerLeaseId: "lease-a",
config: { timeoutMs: 300000, reuseLease: false },
reason: "cancelled",
});
await new Promise((resolve) => setTimeout(resolve, 0));
const queuedExecutePromise = plugin.definition.onEnvironmentExecute?.(execParams("lease-a"));
queuedExecutePromise?.catch(() => {
queuedExecuteRejected = true;
});
await new Promise((resolve) => setTimeout(resolve, 0));
expect(mockGet).toHaveBeenCalledTimes(1);
expect(sandbox.delete).not.toHaveBeenCalled();
resolveFirstExecute();
await cancelPromise.then(() => {
cancelResolved = true;
});
await expect(queuedExecutePromise).rejects.toThrow(/no longer active/);
await firstExecutePromise;
expect(cancelResolved).toBe(true);
expect(queuedExecuteRejected).toBe(true);
expect(sandbox.process.executeCommand).toHaveBeenCalledTimes(1);
expect(sandbox.delete).toHaveBeenCalledTimes(1);
});
it("waits for an in-flight snapshot capture before destroy cleanup starts", async () => {
process.env.DAYTONA_API_KEY = "host-key";
const sandbox = createMockSandbox({ id: "lease-a" });
let resolveSnapshot!: () => void;
sandbox._experimental_createSnapshot.mockImplementation(async () => {
await new Promise<void>((resolve) => {
resolveSnapshot = resolve;
});
});
mockGet.mockResolvedValue(sandbox);
const capturePromise = plugin.definition.onEnvironmentCaptureTemplate?.({
driverKey: "daytona",
companyId: "company-1",
environmentId: "env-1",
providerLeaseId: "lease-a",
config: { timeoutMs: 300000, reuseLease: false },
templateLabel: "snapshot-check",
});
await new Promise((resolve) => setTimeout(resolve, 0));
const destroyPromise = plugin.definition.onEnvironmentDestroyLease?.({
driverKey: "daytona",
companyId: "company-1",
environmentId: "env-1",
providerLeaseId: "lease-a",
config: { timeoutMs: 300000, reuseLease: false },
});
await new Promise((resolve) => setTimeout(resolve, 0));
expect(sandbox.delete).not.toHaveBeenCalled();
resolveSnapshot();
await Promise.all([capturePromise, destroyPromise]);
expect(sandbox._experimental_createSnapshot).toHaveBeenCalledTimes(1);
expect(sandbox.delete).toHaveBeenCalledTimes(1);
});
it("does not cache a failed populate (NotFound) — the next lookup re-fetches", async () => {
process.env.DAYTONA_API_KEY = "host-key";
const sandbox = createMockSandbox({ id: "lease-a" });
mockGet
.mockRejectedValueOnce(new MockDaytonaNotFoundError("missing"))
.mockResolvedValue(sandbox);
const first = await plugin.definition.onEnvironmentResumeLease?.({
driverKey: "daytona",
companyId: "company-1",
environmentId: "env-1",
providerLeaseId: "lease-a",
config: { timeoutMs: 300000, reuseLease: true },
});
expect(first).toEqual({ providerLeaseId: null, metadata: { expired: true } });
// The rejected populate must not linger; the exec re-fetches successfully.
await plugin.definition.onEnvironmentExecute?.(execParams("lease-a"));
expect(mockGet).toHaveBeenCalledTimes(2);
});
it("single-flights concurrent misses on one lease into a single client.get", async () => {
process.env.DAYTONA_API_KEY = "host-key";
const sandbox = createMockSandbox({ id: "lease-a" });
let resolveGet: ((value: unknown) => void) | undefined;
mockGet.mockImplementation(
() => new Promise((resolve) => { resolveGet = resolve; }),
);
const p1 = plugin.definition.onEnvironmentExecute?.(execParams("lease-a"));
const p2 = plugin.definition.onEnvironmentExecute?.(execParams("lease-a"));
// Let both execs reach the shared in-flight populate before it resolves.
await Promise.resolve();
resolveGet?.(sandbox);
await Promise.all([p1, p2]);
expect(mockGet).toHaveBeenCalledTimes(1);
expect(sandbox.process.executeCommand).toHaveBeenCalledTimes(2);
});
it("keeps concurrent different-lease populates isolated (no promise crossing)", async () => {
process.env.DAYTONA_API_KEY = "host-key";
const sandboxA = createMockSandbox({ id: "lease-a" });
const sandboxB = createMockSandbox({ id: "lease-b" });
mockGet.mockImplementation(async (id: string) => (id === "lease-a" ? sandboxA : sandboxB));
await Promise.all([
plugin.definition.onEnvironmentExecute?.(execParams("lease-a")),
plugin.definition.onEnvironmentExecute?.(execParams("lease-b")),
]);
expect(mockGet).toHaveBeenCalledTimes(2);
expect(mockGet).toHaveBeenCalledWith("lease-a");
expect(mockGet).toHaveBeenCalledWith("lease-b");
// Each lease executed in its OWN sandbox, never the other's handle.
expect(sandboxA.process.executeCommand).toHaveBeenCalledTimes(1);
expect(sandboxB.process.executeCommand).toHaveBeenCalledTimes(1);
});
it("re-verifies the workspace sentinel on a cached resume and evicts on mismatch", async () => {
process.env.DAYTONA_API_KEY = "host-key";
const sandbox = createMockSandbox({ id: "lease-a", state: "started" });
// Every executeCommand (exec body + sentinel `cat`) returns a NON-matching token.
sandbox.process.executeCommand.mockResolvedValue({
exitCode: 0,
result: JSON.stringify({ token: "other-token" }),
artifacts: { stdout: JSON.stringify({ token: "other-token" }) },
});
mockGet.mockImplementation(async () => sandbox);
// Prime the cache with a successful exec on this lease.
await plugin.definition.onEnvironmentExecute?.(execParams("lease-a"));
expect(mockGet).toHaveBeenCalledTimes(1);
const sentinelCallsBefore = sandbox.process.executeCommand.mock.calls.length;
// Resume hits the cache but MUST still verify the sentinel; mismatch expires.
const resumed = await plugin.definition.onEnvironmentResumeLease?.({
driverKey: "daytona",
companyId: "company-1",
environmentId: "env-1",
providerLeaseId: "lease-a",
config: { timeoutMs: 300000, reuseLease: true },
leaseMetadata: {
workspaceSentinel: {
path: "/home/daytona/paperclip-workspace/.paperclip-runtime/reusable-sandbox-lease.json",
token: "expected-token",
result: "written",
},
},
});
expect(resumed).toMatchObject({
providerLeaseId: null,
metadata: { expired: true, workspaceSentinel: { result: "mismatch" } },
});
// The sentinel `cat` ran on the cached handle — verification was not skipped.
expect(sandbox.process.executeCommand.mock.calls.length).toBeGreaterThan(sentinelCallsBefore);
// The mismatched entry was evicted, so the next exec re-fetches.
await plugin.definition.onEnvironmentExecute?.(execParams("lease-a"));
expect(mockGet).toHaveBeenCalledTimes(2);
});
it("refreshes a handle left idle past the auto-stop window and restarts a provider-stopped sandbox", async () => {
process.env.DAYTONA_API_KEY = "host-key";
const sandbox = createMockSandbox({ id: "lease-a", state: "started" });
// Daytona auto-stopped the sandbox while our cached handle sat idle: a live
// refresh reveals the true "stopped" state that the cached snapshot hid.
sandbox.refreshData.mockImplementation(async () => {
sandbox.state = "stopped";
});
mockGet.mockResolvedValue(sandbox);
let nowMs = 1_000_000;
const restoreFreshness = setDaytonaHandleFreshnessClockForTest(() => nowMs);
try {
// Prime the cache (single fetch, snapshot "started").
await plugin.definition.onEnvironmentExecute?.(execParams("lease-a"));
expect(sandbox.refreshData).not.toHaveBeenCalled();
expect(sandbox.start).not.toHaveBeenCalled();
// Idle past half of the default 15-min auto-stop interval (> 7.5 min).
nowMs += 8 * 60_000;
await plugin.definition.onEnvironmentExecute?.(execParams("lease-a"));
} finally {
restoreFreshness();
}
// The stale handle was refreshed in place (no second REST fetch — the same
// authenticated handle), the refresh exposed the stopped state, and the
// sandbox was restarted before the exec instead of running against a
// stopped sandbox.
expect(mockGet).toHaveBeenCalledTimes(1);
expect(sandbox.refreshData).toHaveBeenCalledTimes(1);
expect(sandbox.start).toHaveBeenCalledTimes(1);
expect(sandbox.process.executeCommand).toHaveBeenCalledTimes(2);
});
it("does not refresh a handle reused within the auto-stop window", async () => {
process.env.DAYTONA_API_KEY = "host-key";
const sandbox = createMockSandbox({ id: "lease-a" });
mockGet.mockResolvedValue(sandbox);
let nowMs = 5_000_000;
const restoreFreshness = setDaytonaHandleFreshnessClockForTest(() => nowMs);
try {
await plugin.definition.onEnvironmentExecute?.(execParams("lease-a"));
// Two more execs, each 6 min after the previous — always inside the
// 7.5-min window measured from the last reuse.
nowMs += 6 * 60_000;
await plugin.definition.onEnvironmentExecute?.(execParams("lease-a"));
nowMs += 6 * 60_000;
await plugin.definition.onEnvironmentExecute?.(execParams("lease-a"));
} finally {
restoreFreshness();
}
// Each reuse resets the freshness marker (an operation follows, resetting
// the provider idle clock), so an actively-used lease never pays a refresh.
expect(sandbox.refreshData).not.toHaveBeenCalled();
expect(mockGet).toHaveBeenCalledTimes(1);
});
it("does not advance freshness when an execute fails before succeeding", async () => {
process.env.DAYTONA_API_KEY = "host-key";
const sandbox = createMockSandbox({ id: "lease-a" });
sandbox.process.executeCommand
.mockRejectedValueOnce(new Error("command failed"))
.mockResolvedValue({
exitCode: 0,
result: "bash",
artifacts: { stdout: "bash" },
});
mockGet.mockResolvedValue(sandbox);
let nowMs = 7_000_000;
const restoreFreshness = setDaytonaHandleFreshnessClockForTest(() => nowMs);
try {
await expect(plugin.definition.onEnvironmentExecute?.(execParams("lease-a"))).rejects.toThrow(
"command failed",
);
nowMs += 8 * 60_000;
await plugin.definition.onEnvironmentExecute?.(execParams("lease-a"));
} finally {
restoreFreshness();
}
expect(sandbox.refreshData).toHaveBeenCalledTimes(1);
expect(mockGet).toHaveBeenCalledTimes(1);
expect(sandbox.process.executeCommand).toHaveBeenCalledTimes(2);
});
it("does not advance freshness when an execute times out before succeeding", async () => {
process.env.DAYTONA_API_KEY = "host-key";
const sandbox = createMockSandbox({ id: "lease-a" });
sandbox.process.executeCommand
.mockRejectedValueOnce(new MockDaytonaTimeoutError("timed out"))
.mockResolvedValue({
exitCode: 0,
result: "bash",
artifacts: { stdout: "bash" },
});
mockGet.mockResolvedValue(sandbox);
let nowMs = 8_000_000;
const restoreFreshness = setDaytonaHandleFreshnessClockForTest(() => nowMs);
try {
const first = await plugin.definition.onEnvironmentExecute?.(execParams("lease-a"));
expect(first).toMatchObject({ timedOut: true, exitCode: null });
nowMs += 8 * 60_000;
await plugin.definition.onEnvironmentExecute?.(execParams("lease-a"));
} finally {
restoreFreshness();
}
expect(sandbox.refreshData).toHaveBeenCalledTimes(1);
expect(mockGet).toHaveBeenCalledTimes(1);
expect(sandbox.process.executeCommand).toHaveBeenCalledTimes(2);
});
it("never refreshes when auto-stop is disabled, even after a long idle gap", async () => {
process.env.DAYTONA_API_KEY = "host-key";
const sandbox = createMockSandbox({ id: "lease-a" });
mockGet.mockResolvedValue(sandbox);
const disabledAutoStop = { timeoutMs: 300000, reuseLease: false, autoStopInterval: 0 };
let nowMs = 2_000_000;
const restoreFreshness = setDaytonaHandleFreshnessClockForTest(() => nowMs);
try {
await plugin.definition.onEnvironmentExecute?.({ ...execParams("lease-a"), config: disabledAutoStop });
nowMs += 60 * 60_000; // an hour idle
await plugin.definition.onEnvironmentExecute?.({ ...execParams("lease-a"), config: disabledAutoStop });
} finally {
restoreFreshness();
}
// Auto-stop off → the provider never stops the sandbox out from under the
// handle, so the cached started snapshot is trusted without a refresh.
expect(sandbox.refreshData).not.toHaveBeenCalled();
expect(mockGet).toHaveBeenCalledTimes(1);
});
it("evicts the handle when a freshness refresh fails so the next lookup re-fetches", async () => {
process.env.DAYTONA_API_KEY = "host-key";
const first = createMockSandbox({ id: "lease-a" });
first.refreshData.mockRejectedValue(new MockDaytonaNotFoundError("sandbox vanished"));
const second = createMockSandbox({ id: "lease-a" });
mockGet.mockResolvedValueOnce(first).mockResolvedValue(second);
let nowMs = 3_000_000;
const restoreFreshness = setDaytonaHandleFreshnessClockForTest(() => nowMs);
try {
await plugin.definition.onEnvironmentExecute?.(execParams("lease-a")); // fetch #1 → first
nowMs += 8 * 60_000; // idle past the refresh window
// The refresh rejects; execute surfaces it (fail closed) and the bad
// entry is evicted.
await expect(
plugin.definition.onEnvironmentExecute?.(execParams("lease-a")),
).rejects.toThrow("sandbox vanished");
// Evicted → the following exec re-fetches a fresh handle.
await plugin.definition.onEnvironmentExecute?.(execParams("lease-a"));
} finally {
restoreFreshness();
}
expect(mockGet).toHaveBeenCalledTimes(2);
expect(second.process.executeCommand).toHaveBeenCalledTimes(1);
});
});
}); });
describe("daytona native file-sync hooks", () => { describe("daytona native file-sync hooks", () => {
@ -1315,7 +2087,9 @@ describe("daytona native file-sync hooks", () => {
} }
beforeEach(() => { beforeEach(() => {
mockGet.mockReset();
process.env.DAYTONA_API_KEY = "host-key"; process.env.DAYTONA_API_KEY = "host-key";
__resetDaytonaSandboxHandleCacheForTest();
}); });
afterEach(async () => { afterEach(async () => {

View File

@ -57,6 +57,24 @@ export function setDaytonaTimingClockForTest(now: () => number): () => void {
}; };
} }
// Injectable clock for the handle cache's freshness bookkeeping, deliberately
// kept separate from the provider-timing clock so tests can advance virtual time
// past a lease's auto-stop interval without perturbing the `getDurationMs` /
// `durationMs` measurements that ride on `timingNow`.
let handleFreshnessNow: () => number = () => Date.now();
/**
* Test seam: override the handle-cache freshness clock and return a restore
* function. Not used in production, where the default wall clock always applies.
*/
export function setDaytonaHandleFreshnessClockForTest(now: () => number): () => void {
const previous = handleFreshnessNow;
handleFreshnessNow = now;
return () => {
handleFreshnessNow = previous;
};
}
interface DaytonaDriverConfig { interface DaytonaDriverConfig {
apiKey: string | null; apiKey: string | null;
apiUrl: string | null; apiUrl: string | null;
@ -723,14 +741,365 @@ async function createSandbox(
return sandbox; return sandbox;
} }
async function getSandbox(config: DaytonaDriverConfig, sandboxId: string): Promise<Sandbox> { // ─── Per-lease started-sandbox handle cache ──────────────────────────────────
const client = createDaytonaClient(config); // Memoize the started Daytona `Sandbox` handle so repeated exec/sync/resume/
return await client.get(sandboxId); // teardown calls on one lease skip the per-call `client.get(sandboxId)` REST
// re-fetch (measured ~4,938 ms on `stage.sync`) and the client construction it
// implies. The cache is process-memory only — no handle, API key, or credential
// is ever persisted or logged (Stage-1 security review C6).
//
// Isolation is the whole game here: the cached object is an authenticated
// compute handle, so a mis-keyed or un-evicted entry could run one lease/tenant's
// commands inside another's sandbox. The guarantees below map 1:1 to the Stage-1
// required-fix conditions:
// C1 Key by a NON-SECRET composite scope, never the bare providerLeaseId:
// {driverKey, companyId, environmentId, providerLeaseId, account}. The
// account discriminator is a hash of the resolved endpoint + credentials
// so two environments pointing at different Daytona accounts (or a rotated
// key) never collide — without storing the secret in the key.
// C2 Every read (cache hit AND resolved single-flight populate) asserts the
// handle's `sandbox.id === providerLeaseId`; a mismatch evicts and throws
// (fail closed) rather than serving the wrong sandbox.
// C4 Callers evict at every teardown hook. Populate rejections (NotFound,
// network, id mismatch) are never cached — they drop from the map so the
// next call re-fetches.
// C5 In-flight populate promises live under the composite key only; there is
// no fallback lookup by bare providerLeaseId, so lease A's in-flight
// promise can never be awaited for lease B.
type SandboxScope = {
driverKey: string;
companyId: string;
environmentId: string;
providerLeaseId: string;
config: DaytonaDriverConfig;
};
// Non-secret provider/account fingerprint. Uses the *resolved* key (config or
// DAYTONA_API_KEY env fallback) so an env-provided credential is still scoped,
// but only its sha256 digest — never the key itself — enters the cache key (C1/C6).
function sandboxAccountDiscriminator(config: DaytonaDriverConfig): string {
const resolvedApiKey = config.apiKey ?? process.env.DAYTONA_API_KEY?.trim() ?? null;
return createHash("sha256")
.update(stableStringify({
apiUrl: config.apiUrl,
target: config.target,
apiKey: resolvedApiKey,
}))
.digest("hex");
} }
async function getSandboxOrNull(config: DaytonaDriverConfig, sandboxId: string): Promise<Sandbox | null> { function sandboxHandleCacheKey(scope: SandboxScope): string {
return stableStringify({
driverKey: scope.driverKey,
companyId: scope.companyId,
environmentId: scope.environmentId,
providerLeaseId: scope.providerLeaseId,
account: sandboxAccountDiscriminator(scope.config),
});
}
function assertHandleMatchesLease(sandbox: Sandbox, providerLeaseId: string): void {
// C2: a handle must never stand in for a different sandbox than the lease
// asked for. Belt-and-suspenders against a provider that returns a renamed or
// substituted sandbox, and against any future key collision.
if (sandbox.id !== providerLeaseId) {
throw new Error(
`Daytona sandbox handle mismatch: handle ${sandbox.id} does not belong to lease ${providerLeaseId}.`,
);
}
}
// A cached `Sandbox` carries the provider state captured when it was last
// fetched/refreshed. Daytona auto-stops an idle sandbox after `autoStopInterval`
// minutes, at which point that snapshot ("started") no longer matches reality
// and `ensureSandboxStarted` would wrongly skip the restart, sending every
// subsequent exec/sync at a stopped sandbox. Before reusing a handle that has
// gone untouched for this fraction of the auto-stop interval we re-read the live
// state so the restart decision is made against the truth. Reusing a handle for
// an operation resets Daytona's idle clock, so an actively-used lease stays well
// inside the window and never pays the refresh — only a lease resumed after an
// idle gap does.
const STALE_HANDLE_REFRESH_SAFETY_FRACTION = 0.5;
function staleHandleRefreshThresholdMs(autoStopIntervalMinutes: number | null): number | null {
// Auto-stop disabled (0 / null): the provider never stops the sandbox out from
// under a live handle, so the started snapshot stays valid until we evict it
// and no refresh is warranted.
if (autoStopIntervalMinutes == null || autoStopIntervalMinutes <= 0) return null;
return Math.floor(autoStopIntervalMinutes * 60_000 * STALE_HANDLE_REFRESH_SAFETY_FRACTION);
}
type SandboxHandleCacheEntry = {
sandbox: Promise<Sandbox>;
// Last time we know the live state was accurate: set when the handle is
// fetched/refreshed and on every reuse (an operation follows, resetting the
// provider idle clock).
verifiedAtMs: number;
};
type SandboxLookupOptions = {
bypassTeardownGate?: boolean;
};
type SandboxHandleTeardownGate = {
promise: Promise<void>;
release: () => void;
refCount: number;
};
const sandboxHandleTeardownGates = (() => {
const gates = new Map<string, SandboxHandleTeardownGate>();
function begin(scope: SandboxScope): SandboxHandleTeardownGate {
const key = sandboxHandleCacheKey(scope);
const existing = gates.get(key);
if (existing) {
existing.refCount += 1;
return existing;
}
let release!: () => void;
const gate: SandboxHandleTeardownGate = {
promise: new Promise<void>((resolve) => {
release = resolve;
}),
release: () => release(),
refCount: 1,
};
gates.set(key, gate);
return gate;
}
function current(scope: SandboxScope): SandboxHandleTeardownGate | null {
return gates.get(sandboxHandleCacheKey(scope)) ?? null;
}
function end(scope: SandboxScope, gate: SandboxHandleTeardownGate): void {
const key = sandboxHandleCacheKey(scope);
gate.refCount -= 1;
if (gate.refCount > 0) return;
if (gates.get(key) === gate) {
gates.delete(key);
}
gate.release();
}
function reset(): void {
gates.clear();
}
return { begin, current, end, reset };
})();
type SandboxHandleActivityGate = {
promise: Promise<void>;
release: () => void;
refCount: number;
};
const sandboxHandleActivityGates = (() => {
const gates = new Map<string, SandboxHandleActivityGate>();
async function begin(scope: SandboxScope): Promise<SandboxHandleActivityGate> {
const key = sandboxHandleCacheKey(scope);
const existing = gates.get(key);
if (existing) {
existing.refCount += 1;
return existing;
}
let release!: () => void;
const gate: SandboxHandleActivityGate = {
promise: new Promise<void>((resolve) => {
release = resolve;
}),
release: () => release(),
refCount: 1,
};
gates.set(key, gate);
return gate;
}
async function waitForIdle(scope: SandboxScope): Promise<void> {
const gate = gates.get(sandboxHandleCacheKey(scope));
if (!gate) return;
await gate.promise;
}
function end(scope: SandboxScope, gate: SandboxHandleActivityGate): void {
const key = sandboxHandleCacheKey(scope);
gate.refCount -= 1;
if (gate.refCount > 0) return;
if (gates.get(key) === gate) {
gates.delete(key);
}
gate.release();
}
function reset(): void {
gates.clear();
}
return { begin, waitForIdle, end, reset };
})();
type SandboxLeaseAdmissionOptions = {
allowClosed?: boolean;
};
const sandboxHandleLeaseAdmissionStates = (() => {
const states = new Map<string, boolean>();
function key(scope: SandboxScope): string {
return sandboxHandleCacheKey(scope);
}
function open(scope: SandboxScope): void {
states.set(key(scope), false);
}
function close(scope: SandboxScope): void {
states.set(key(scope), true);
}
function isClosed(scope: SandboxScope): boolean {
return states.get(key(scope)) === true;
}
function reset(): void {
states.clear();
}
return { open, close, isClosed, reset };
})();
async function withSandboxActivityGate<T>(
scope: SandboxScope,
fn: () => Promise<T>,
options: SandboxLeaseAdmissionOptions = {},
): Promise<T> {
while (true) {
if (!options.allowClosed && sandboxHandleLeaseAdmissionStates.isClosed(scope)) {
throw new Error(`Daytona sandbox lease ${scope.providerLeaseId} is no longer active.`);
}
const teardownGate = sandboxHandleTeardownGates.current(scope);
if (teardownGate) {
await teardownGate.promise;
if (!options.allowClosed && sandboxHandleLeaseAdmissionStates.isClosed(scope)) {
throw new Error(`Daytona sandbox lease ${scope.providerLeaseId} is no longer active.`);
}
continue;
}
const activityGate = await sandboxHandleActivityGates.begin(scope);
try {
// A teardown can still begin between the initial check above and the
// activity-gate admission. If that happens, back out and wait for the
// teardown to finish instead of proceeding into a race with cleanup.
if (sandboxHandleTeardownGates.current(scope)) {
continue;
}
if (!options.allowClosed && sandboxHandleLeaseAdmissionStates.isClosed(scope)) {
throw new Error(`Daytona sandbox lease ${scope.providerLeaseId} is no longer active.`);
}
return await fn();
} finally {
sandboxHandleActivityGates.end(scope, activityGate);
}
}
}
const sandboxHandleCache = (() => {
const entries = new Map<string, SandboxHandleCacheEntry>();
function markFresh(scope: SandboxScope): void {
const entry = entries.get(sandboxHandleCacheKey(scope));
if (entry) {
entry.verifiedAtMs = handleFreshnessNow();
}
}
async function get(scope: SandboxScope, options: SandboxLookupOptions = {}): Promise<Sandbox> {
const key = sandboxHandleCacheKey(scope);
const entry = entries.get(key);
if (entry) {
const sandbox = await entry.sandbox;
// Re-assert on every hit; evict + fail closed on any mismatch (C2).
try {
assertHandleMatchesLease(sandbox, scope.providerLeaseId);
} catch (error) {
entries.delete(key);
throw error;
}
// Refresh the live provider state if the handle may have been auto-stopped
// since we last confirmed it, so the cached `state` snapshot can't hide a
// provider-initiated stop from `ensureSandboxStarted`. A failed refresh
// means the handle is no longer trustworthy — evict and fail closed.
const thresholdMs = staleHandleRefreshThresholdMs(scope.config.autoStopInterval);
if (thresholdMs != null && handleFreshnessNow() - entry.verifiedAtMs >= thresholdMs) {
try {
await sandbox.refreshData();
} catch (error) {
entries.delete(key);
throw error;
}
}
return sandbox;
}
// Single-flight: the first miss stores the in-flight promise under the
// composite key so concurrent misses on the same lease share one `client.get`
// instead of double-fetching. The promise lives only under this key (C5).
const populate = (async () => {
const client = createDaytonaClient(scope.config);
const sandbox = await client.get(scope.providerLeaseId);
assertHandleMatchesLease(sandbox, scope.providerLeaseId);
return sandbox;
})();
const populated: SandboxHandleCacheEntry = { sandbox: populate, verifiedAtMs: handleFreshnessNow() };
entries.set(key, populated);
try {
const sandbox = await populate;
return sandbox;
} catch (error) {
// A rejected populate (NotFound, network, id mismatch) must never remain
// cached (C4/C5). Guard against clobbering a newer entry under the key.
if (entries.get(key) === populated) {
entries.delete(key);
}
throw error;
}
}
function clear(scope: SandboxScope): void {
entries.delete(sandboxHandleCacheKey(scope));
}
function reset(): void {
entries.clear();
}
return { get, clear, reset, markFresh };
})();
/**
* Test seam: clear the process-scoped handle cache between tests so a handle
* memoized under a reused composite key in one test never leaks into the next.
* Not used in production.
*/
export function __resetDaytonaSandboxHandleCacheForTest(): void {
sandboxHandleCache.reset();
sandboxHandleTeardownGates.reset();
sandboxHandleActivityGates.reset();
sandboxHandleLeaseAdmissionStates.reset();
}
async function getSandbox(scope: SandboxScope, options: SandboxLookupOptions = {}): Promise<Sandbox> {
return await sandboxHandleCache.get(scope, options);
}
async function getSandboxOrNull(scope: SandboxScope, options: SandboxLookupOptions = {}): Promise<Sandbox | null> {
try { try {
return await getSandbox(config, sandboxId); return await getSandbox(scope, options);
} catch (error) { } catch (error) {
if (error instanceof DaytonaNotFoundError) { if (error instanceof DaytonaNotFoundError) {
return null; return null;
@ -739,6 +1108,10 @@ async function getSandboxOrNull(config: DaytonaDriverConfig, sandboxId: string):
} }
} }
function evictSandboxHandle(scope: SandboxScope): void {
sandboxHandleCache.clear(scope);
}
// One-shot command execution via Daytona's `process.executeCommand`. The // One-shot command execution via Daytona's `process.executeCommand`. The
// session-based API (`createSession` + `executeSessionCommand` with // session-based API (`createSession` + `executeSessionCommand` with
// `runAsync: false`) hangs indefinitely when the supplied command ends with // `runAsync: false`) hangs indefinitely when the supplied command ends with
@ -951,6 +1324,13 @@ const plugin = definePlugin({
config, config,
timeoutSeconds: toTimeoutSeconds(config.timeoutMs), timeoutSeconds: toTimeoutSeconds(config.timeoutMs),
}); });
sandboxHandleLeaseAdmissionStates.open({
driverKey: params.driverKey,
companyId: params.companyId,
environmentId: params.environmentId,
providerLeaseId: sandbox.id,
config,
});
return { return {
providerLeaseId: sandbox.id, providerLeaseId: sandbox.id,
metadata: leaseMetadata({ metadata: leaseMetadata({
@ -972,14 +1352,26 @@ const plugin = definePlugin({
params: PluginEnvironmentResumeLeaseParams, params: PluginEnvironmentResumeLeaseParams,
): Promise<PluginEnvironmentLease> { ): Promise<PluginEnvironmentLease> {
const config = parseDriverConfig(params.config); const config = parseDriverConfig(params.config);
const sandbox = await getSandboxOrNull(config, params.providerLeaseId); const scope: SandboxScope = {
if (!sandbox) { driverKey: params.driverKey,
return { providerLeaseId: null, metadata: { expired: true } }; companyId: params.companyId,
} environmentId: params.environmentId,
providerLeaseId: params.providerLeaseId,
config,
};
return await withSandboxActivityGate(scope, async () => {
const sandbox = await getSandboxOrNull(scope, { bypassTeardownGate: true });
if (!sandbox) {
return { providerLeaseId: null, metadata: { expired: true } };
}
await ensureSandboxStarted(sandbox, toTimeoutSeconds(config.timeoutMs)); await ensureSandboxStarted(sandbox, toTimeoutSeconds(config.timeoutMs));
try { try {
const remoteCwd = await resolveSandboxWorkingDirectory(sandbox); const remoteCwd = await resolveSandboxWorkingDirectory(sandbox);
// C3: a resumed lease must clear the workspace sentinel before it is
// trusted, even when the handle came from the cache. On any non-match we
// evict the cached handle and expire the lease so a stale/foreign sandbox
// is never reused on the subsequent (sentinel-skipping) exec path.
const workspaceSentinel = await verifyWorkspaceSentinel({ const workspaceSentinel = await verifyWorkspaceSentinel({
sandbox, sandbox,
remoteCwd, remoteCwd,
@ -987,9 +1379,12 @@ const plugin = definePlugin({
timeoutSeconds: toTimeoutSeconds(config.timeoutMs), timeoutSeconds: toTimeoutSeconds(config.timeoutMs),
}); });
if (workspaceSentinel.result !== "matched") { if (workspaceSentinel.result !== "matched") {
evictSandboxHandle(scope);
return { providerLeaseId: null, metadata: { expired: true, workspaceSentinel } }; return { providerLeaseId: null, metadata: { expired: true, workspaceSentinel } };
} }
const shellCommand = await detectSandboxShellCommand(sandbox, toTimeoutSeconds(config.timeoutMs)); const shellCommand = await detectSandboxShellCommand(sandbox, toTimeoutSeconds(config.timeoutMs));
sandboxHandleCache.markFresh(scope);
sandboxHandleLeaseAdmissionStates.open(scope);
return { return {
providerLeaseId: sandbox.id, providerLeaseId: sandbox.id,
metadata: leaseMetadata({ metadata: leaseMetadata({
@ -1001,10 +1396,12 @@ const plugin = definePlugin({
workspaceSentinel, workspaceSentinel,
}), }),
}; };
} catch (error) { } catch (error) {
await sandbox.delete(toTimeoutSeconds(config.timeoutMs)).catch(() => undefined); evictSandboxHandle(scope);
throw error; await sandbox.delete(toTimeoutSeconds(config.timeoutMs)).catch(() => undefined);
} throw error;
}
}, { allowClosed: true });
}, },
async onEnvironmentReleaseLease( async onEnvironmentReleaseLease(
@ -1012,43 +1409,63 @@ const plugin = definePlugin({
): Promise<void> { ): Promise<void> {
if (!params.providerLeaseId) return; if (!params.providerLeaseId) return;
const config = parseDriverConfig(params.config); const config = parseDriverConfig(params.config);
const sandbox = await getSandboxOrNull(config, params.providerLeaseId); const scope: SandboxScope = {
if (!sandbox) return; driverKey: params.driverKey,
companyId: params.companyId,
environmentId: params.environmentId,
providerLeaseId: params.providerLeaseId,
config,
};
// C4: the lease's handle must not outlive its teardown. A teardown gate
// blocks fresh cache reads while cleanup is in flight so overlapping
// exec/sync calls cannot reacquire the same sandbox mid-stop/delete.
const teardownGate = sandboxHandleTeardownGates.begin(scope);
sandboxHandleLeaseAdmissionStates.close(scope);
try {
const sandbox = await getSandboxOrNull(scope, { bypassTeardownGate: true });
if (!sandbox) return;
if (config.reuseLease) { evictSandboxHandle(scope);
if (sandbox.state !== "stopped") { await sandboxHandleActivityGates.waitForIdle(scope);
if (config.reuseLease) {
if (sandbox.state !== "stopped") {
try {
await sandbox.stop(toTimeoutSeconds(config.timeoutMs));
} catch (error) {
console.warn(
`Failed to stop Daytona sandbox during lease release: ${formatErrorMessage(error)}. Attempting delete instead.`,
);
await sandbox.delete(toTimeoutSeconds(config.timeoutMs)).catch((deleteError) => {
console.warn(
`Failed to delete Daytona sandbox after stop failure: ${formatErrorMessage(deleteError)}`,
);
});
}
}
return;
}
if (config.archiveOnRelease) {
try { try {
await sandbox.stop(toTimeoutSeconds(config.timeoutMs)); if (sandbox.state !== "stopped") {
await sandbox.stop(toTimeoutSeconds(config.timeoutMs));
}
await sandbox.setAutoDeleteInterval(ARCHIVE_ON_RELEASE_AUTO_DELETE_MINUTES);
await sandbox.archive();
return;
} catch (error) { } catch (error) {
console.warn( console.warn(
`Failed to stop Daytona sandbox during lease release: ${formatErrorMessage(error)}. Attempting delete instead.`, `Failed to archive Daytona sandbox during lease release: ${formatErrorMessage(error)}. Falling back to delete.`,
); );
await sandbox.delete(toTimeoutSeconds(config.timeoutMs)).catch((deleteError) => {
console.warn(
`Failed to delete Daytona sandbox after stop failure: ${formatErrorMessage(deleteError)}`,
);
});
} }
} }
return;
}
if (config.archiveOnRelease) { await sandbox.delete(toTimeoutSeconds(config.timeoutMs));
try { } finally {
if (sandbox.state !== "stopped") { sandboxHandleTeardownGates.end(scope, teardownGate);
await sandbox.stop(toTimeoutSeconds(config.timeoutMs)); evictSandboxHandle(scope);
}
await sandbox.setAutoDeleteInterval(ARCHIVE_ON_RELEASE_AUTO_DELETE_MINUTES);
await sandbox.archive();
return;
} catch (error) {
console.warn(
`Failed to archive Daytona sandbox during lease release: ${formatErrorMessage(error)}. Falling back to delete.`,
);
}
} }
await sandbox.delete(toTimeoutSeconds(config.timeoutMs));
}, },
async onEnvironmentDestroyLease( async onEnvironmentDestroyLease(
@ -1056,9 +1473,28 @@ const plugin = definePlugin({
): Promise<void> { ): Promise<void> {
if (!params.providerLeaseId) return; if (!params.providerLeaseId) return;
const config = parseDriverConfig(params.config); const config = parseDriverConfig(params.config);
const sandbox = await getSandboxOrNull(config, params.providerLeaseId); const scope: SandboxScope = {
if (!sandbox) return; driverKey: params.driverKey,
await sandbox.delete(toTimeoutSeconds(config.timeoutMs)); companyId: params.companyId,
environmentId: params.environmentId,
providerLeaseId: params.providerLeaseId,
config,
};
// C4: the teardown gate blocks fresh cache reads while delete is in flight
// so overlapping exec/sync calls cannot reacquire the same sandbox mid-teardown.
const teardownGate = sandboxHandleTeardownGates.begin(scope);
sandboxHandleLeaseAdmissionStates.close(scope);
try {
const sandbox = await getSandboxOrNull(scope, { bypassTeardownGate: true });
if (!sandbox) return;
evictSandboxHandle(scope);
await sandboxHandleActivityGates.waitForIdle(scope);
await sandbox.delete(toTimeoutSeconds(config.timeoutMs));
} finally {
sandboxHandleTeardownGates.end(scope, teardownGate);
evictSandboxHandle(scope);
}
}, },
async onEnvironmentRealizeWorkspace( async onEnvironmentRealizeWorkspace(
@ -1072,9 +1508,18 @@ const plugin = definePlugin({
: params.workspace.remotePath ?? params.workspace.localPath ?? "/paperclip-workspace"; : params.workspace.remotePath ?? params.workspace.localPath ?? "/paperclip-workspace";
if (params.lease.providerLeaseId) { if (params.lease.providerLeaseId) {
const sandbox = await getSandbox(config, params.lease.providerLeaseId); const scope: SandboxScope = {
await ensureSandboxStarted(sandbox, toTimeoutSeconds(config.timeoutMs)); driverKey: params.driverKey,
await sandbox.fs.createFolder(remoteCwd, "755"); companyId: params.companyId,
environmentId: params.environmentId,
providerLeaseId: params.lease.providerLeaseId,
config,
};
await withSandboxActivityGate(scope, async () => {
const sandbox = await getSandbox(scope, { bypassTeardownGate: true });
await ensureSandboxStarted(sandbox, toTimeoutSeconds(config.timeoutMs));
await sandbox.fs.createFolder(remoteCwd, "755");
});
} }
return { return {
@ -1099,6 +1544,13 @@ const plugin = definePlugin({
sandbox, sandbox,
resolveConnectionExpiresInMinutes(params.connectionExpiresInMinutes), resolveConnectionExpiresInMinutes(params.connectionExpiresInMinutes),
); );
sandboxHandleLeaseAdmissionStates.open({
driverKey: params.driverKey,
companyId: params.companyId,
environmentId: params.environmentId,
providerLeaseId: sandbox.id,
config,
});
return { return {
providerLeaseId: sandbox.id, providerLeaseId: sandbox.id,
status: "waiting_for_user", status: "waiting_for_user",
@ -1134,22 +1586,30 @@ const plugin = definePlugin({
}, },
}; };
} }
const sandbox = await getSandboxOrNull(config, params.providerLeaseId); const scope = {
if (!sandbox) { driverKey: params.driverKey,
return { companyId: params.companyId,
providerLeaseId: null, environmentId: params.environmentId,
status: "missing", providerLeaseId: params.providerLeaseId,
connectionSummary: null, config,
connectionPayload: null, };
metadata: { return await withSandboxActivityGate(scope, async () => {
provider: "daytona", const sandbox = await getSandboxOrNull(scope, { bypassTeardownGate: true });
missing: true, if (!sandbox) {
}, return {
}; providerLeaseId: null,
} status: "missing",
connectionSummary: null,
connectionPayload: null,
metadata: {
provider: "daytona",
missing: true,
},
};
}
await ensureSandboxStarted(sandbox, toTimeoutSeconds(config.timeoutMs)); await ensureSandboxStarted(sandbox, toTimeoutSeconds(config.timeoutMs));
const remoteCwd = await resolveSandboxWorkingDirectory(sandbox); const remoteCwd = await resolveSandboxWorkingDirectory(sandbox);
const shellCommand = await detectSandboxShellCommand(sandbox, toTimeoutSeconds(config.timeoutMs)); const shellCommand = await detectSandboxShellCommand(sandbox, toTimeoutSeconds(config.timeoutMs));
const connection = params.includeConnectionPayload === true const connection = params.includeConnectionPayload === true
? await createSshConnection(sandbox, resolveConnectionExpiresInMinutes(params.connectionExpiresInMinutes)) ? await createSshConnection(sandbox, resolveConnectionExpiresInMinutes(params.connectionExpiresInMinutes))
@ -1167,17 +1627,18 @@ const plugin = definePlugin({
connectionPayload: null, connectionPayload: null,
}; };
return { return {
providerLeaseId: sandbox.id, providerLeaseId: sandbox.id,
status: "waiting_for_user", status: "waiting_for_user",
...connection, ...connection,
metadata: interactiveSetupMetadata({ metadata: interactiveSetupMetadata({
config, config,
sandbox, sandbox,
shellCommand, shellCommand,
remoteCwd, remoteCwd,
}), }),
}; };
});
}, },
async onEnvironmentCaptureTemplate( async onEnvironmentCaptureTemplate(
@ -1187,7 +1648,15 @@ const plugin = definePlugin({
if (!params.providerLeaseId) { if (!params.providerLeaseId) {
throw new Error("Cannot capture a Daytona template without a setup sandbox lease."); throw new Error("Cannot capture a Daytona template without a setup sandbox lease.");
} }
const sandbox = await getSandbox(config, params.providerLeaseId); const scope = {
driverKey: params.driverKey,
companyId: params.companyId,
environmentId: params.environmentId,
providerLeaseId: params.providerLeaseId,
config,
};
return await withSandboxActivityGate(scope, async () => {
const sandbox = await getSandbox(scope, { bypassTeardownGate: true });
const createSnapshot = (sandbox as DaytonaInteractiveSandbox)._experimental_createSnapshot; const createSnapshot = (sandbox as DaytonaInteractiveSandbox)._experimental_createSnapshot;
if (typeof createSnapshot !== "function") { if (typeof createSnapshot !== "function") {
throw new Error( throw new Error(
@ -1202,20 +1671,21 @@ const plugin = definePlugin({
? Math.trunc(params.timeoutMs) ? Math.trunc(params.timeoutMs)
: config.timeoutMs; : config.timeoutMs;
await createSnapshot.call(sandbox, templateRef, toTimeoutSeconds(timeoutMs)); await createSnapshot.call(sandbox, templateRef, toTimeoutSeconds(timeoutMs));
return { return {
templateKind: "snapshot", templateKind: "snapshot",
templateRef, templateRef,
metadata: { metadata: {
provider: "daytona", provider: "daytona",
sandboxId: sandbox.id, sandboxId: sandbox.id,
capturedAt: new Date().toISOString(), capturedAt: new Date().toISOString(),
sourceTemplateRefRedacted: Boolean(params.sourceTemplateRef), sourceTemplateRefRedacted: Boolean(params.sourceTemplateRef),
previousTemplateRefRedacted: Boolean(params.previousTemplateRef), previousTemplateRefRedacted: Boolean(params.previousTemplateRef),
timeoutMs, timeoutMs,
}, },
}; };
});
}, },
async onEnvironmentCancelInteractiveSetup( async onEnvironmentCancelInteractiveSetup(
@ -1232,26 +1702,44 @@ const plugin = definePlugin({
}, },
}; };
} }
const sandbox = await getSandboxOrNull(config, params.providerLeaseId); const scope: SandboxScope = {
if (!sandbox) { driverKey: params.driverKey,
companyId: params.companyId,
environmentId: params.environmentId,
providerLeaseId: params.providerLeaseId,
config,
};
// C4: cancelling an interactive-setup lease deletes the sandbox, so the
// teardown gate blocks fresh cache reads while delete is in flight.
const teardownGate = sandboxHandleTeardownGates.begin(scope);
sandboxHandleLeaseAdmissionStates.close(scope);
try {
const sandbox = await getSandboxOrNull(scope, { bypassTeardownGate: true });
if (!sandbox) {
return {
status: "missing",
metadata: {
provider: "daytona",
missing: true,
reason: params.reason ?? null,
},
};
}
evictSandboxHandle(scope);
await sandboxHandleActivityGates.waitForIdle(scope);
await sandbox.delete(toTimeoutSeconds(config.timeoutMs));
return { return {
status: "missing", status: params.reason === "timed_out" ? "timed_out" : "cancelled",
metadata: { metadata: {
provider: "daytona", provider: "daytona",
missing: true, sandboxId: sandbox.id,
reason: params.reason ?? null, reason: params.reason ?? null,
}, },
}; };
} finally {
sandboxHandleTeardownGates.end(scope, teardownGate);
evictSandboxHandle(scope);
} }
await sandbox.delete(toTimeoutSeconds(config.timeoutMs));
return {
status: params.reason === "timed_out" ? "timed_out" : "cancelled",
metadata: {
provider: "daytona",
sandboxId: sandbox.id,
reason: params.reason ?? null,
},
};
}, },
async onEnvironmentDeleteTemplate( async onEnvironmentDeleteTemplate(
@ -1293,19 +1781,47 @@ const plugin = definePlugin({
} }
const config = parseDriverConfig(params.config); const config = parseDriverConfig(params.config);
// Time the `client.get` sandbox re-fetch (Open Q1) separately from the const providerLeaseId = params.lease.providerLeaseId;
// `executeCommand` round-trip so telemetry can split the per-call REST-get return await withSandboxActivityGate({
// cost from the exec cost. `ensureSandboxStarted` is a no-op for an driverKey: params.driverKey,
// already-started sandbox, so it is excluded from the get measurement. companyId: params.companyId,
const getStart = timingNow(); environmentId: params.environmentId,
const sandbox = await getSandbox(config, params.lease.providerLeaseId); providerLeaseId,
const getDurationMs = timingNow() - getStart; config,
await ensureSandboxStarted(sandbox, toTimeoutSeconds(resolveTimeoutMs(params.timeoutMs, config))); }, async () => {
const result = await executeOneShot(sandbox, params, config); // Time the sandbox handle lookup (Open Q1) separately from the
return { // `executeCommand` round-trip so telemetry can split the per-call get cost
...result, // from the exec cost. With the per-lease handle cache this collapses to ~0
metadata: { ...(result.metadata ?? {}), getDurationMs }, // on a hit (no `client.get` REST round-trip), but the field stays present so
}; // `providerGetMs` remains observable — and still captures the occasional
// freshness refresh the cache issues after an idle gap. `ensureSandboxStarted`
// is a no-op for an already-started sandbox, so it is excluded from the get
// measurement.
const getStart = timingNow();
const sandbox = await getSandbox({
driverKey: params.driverKey,
companyId: params.companyId,
environmentId: params.environmentId,
providerLeaseId,
config,
}, { bypassTeardownGate: true });
const getDurationMs = timingNow() - getStart;
await ensureSandboxStarted(sandbox, toTimeoutSeconds(resolveTimeoutMs(params.timeoutMs, config)));
const result = await executeOneShot(sandbox, params, config);
if (!result.timedOut) {
sandboxHandleCache.markFresh({
driverKey: params.driverKey,
companyId: params.companyId,
environmentId: params.environmentId,
providerLeaseId,
config,
});
}
return {
...result,
metadata: { ...(result.metadata ?? {}), getDurationMs },
};
});
}, },
// Opt-in native inbound transfer. Defining this hook (with onEnvironmentSyncOut) // Opt-in native inbound transfer. Defining this hook (with onEnvironmentSyncOut)
@ -1323,13 +1839,24 @@ const plugin = definePlugin({
const config = parseDriverConfig(params.config); const config = parseDriverConfig(params.config);
const remoteDir = resolveSyncRemoteDir(params.lease); const remoteDir = resolveSyncRemoteDir(params.lease);
const timeoutSeconds = toTimeoutSeconds(config.timeoutMs); const timeoutSeconds = toTimeoutSeconds(config.timeoutMs);
const sandbox = await getSandbox(config, params.lease.providerLeaseId); const scope = {
await ensureSandboxStarted(sandbox, timeoutSeconds); driverKey: params.driverKey,
return await performSyncIn({ companyId: params.companyId,
sandbox, environmentId: params.environmentId,
operations: params.operations, providerLeaseId: params.lease.providerLeaseId,
remoteDir, config,
timeoutSeconds, };
return await withSandboxActivityGate(scope, async () => {
const sandbox = await getSandbox(scope, { bypassTeardownGate: true });
await ensureSandboxStarted(sandbox, timeoutSeconds);
const result = await performSyncIn({
sandbox,
operations: params.operations,
remoteDir,
timeoutSeconds,
});
sandboxHandleCache.markFresh(scope);
return result;
}); });
}, },
@ -1343,13 +1870,24 @@ const plugin = definePlugin({
const config = parseDriverConfig(params.config); const config = parseDriverConfig(params.config);
const remoteDir = resolveSyncRemoteDir(params.lease); const remoteDir = resolveSyncRemoteDir(params.lease);
const timeoutSeconds = toTimeoutSeconds(config.timeoutMs); const timeoutSeconds = toTimeoutSeconds(config.timeoutMs);
const sandbox = await getSandbox(config, params.lease.providerLeaseId); const scope = {
await ensureSandboxStarted(sandbox, timeoutSeconds); driverKey: params.driverKey,
return await performSyncOut({ companyId: params.companyId,
sandbox, environmentId: params.environmentId,
operations: params.operations, providerLeaseId: params.lease.providerLeaseId,
remoteDir, config,
timeoutSeconds, };
return await withSandboxActivityGate(scope, async () => {
const sandbox = await getSandbox(scope, { bypassTeardownGate: true });
await ensureSandboxStarted(sandbox, timeoutSeconds);
const result = await performSyncOut({
sandbox,
operations: params.operations,
remoteDir,
timeoutSeconds,
});
sandboxHandleCache.markFresh(scope);
return result;
}); });
}, },
}); });