Skip to content
Open
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
5 changes: 5 additions & 0 deletions .changeset/tidy-sandboxes-resume.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@ai-sdk/sandbox-vercel': patch
---

Forward Vercel Sandbox credentials when resuming named harness sessions.
31 changes: 29 additions & 2 deletions packages/sandbox-vercel/src/vercel-sandbox.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@ import type { Sandbox } from '@vercel/sandbox';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { createVercelSandbox } from './vercel-sandbox';

const { createMock } = vi.hoisted(() => ({ createMock: vi.fn() }));
const { createMock, getMock } = vi.hoisted(() => ({
createMock: vi.fn(),
getMock: vi.fn(),
}));

vi.mock('@vercel/sandbox', () => ({
Sandbox: { create: createMock },
Sandbox: { create: createMock, get: getMock },
}));

type MockSpies = {
Expand Down Expand Up @@ -222,6 +225,7 @@ describe('createVercelSandbox (wrap existing)', () => {
describe('createVercelSandbox (create from scratch)', () => {
beforeEach(() => {
createMock.mockReset();
getMock.mockReset();
});

it('applies a 30 minute default timeout when none is provided', async () => {
Expand Down Expand Up @@ -270,4 +274,27 @@ describe('createVercelSandbox (create from scratch)', () => {
expect(spies.stop).toHaveBeenCalledTimes(1);
expect(spies.delete).toHaveBeenCalledTimes(1);
});

it('forwards credentials when resuming a named session', async () => {
const { sandbox } = makeMockSandbox();
getMock.mockResolvedValueOnce(sandbox);
const abortController = new AbortController();

await createVercelSandbox({
token: 'token_test',
teamId: 'team_test',
projectId: 'prj_test',
}).resumeSession?.({
sessionId: 'session-123',
abortSignal: abortController.signal,
});

expect(getMock).toHaveBeenCalledWith({
name: 'ai-sdk-harness-session-session-123',
token: 'token_test',
teamId: 'team_test',
projectId: 'prj_test',
signal: abortController.signal,
});
});
});
26 changes: 26 additions & 0 deletions packages/sandbox-vercel/src/vercel-sandbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@ export class VercelSandboxProvider implements HarnessV1SandboxProvider {
if (resolvedId == null) {
resolvedId = await pollForTemplateSnapshot(
templateName,
getSandboxLookupParams(baseParams),
options?.abortSignal,
);
}
Expand Down Expand Up @@ -228,6 +229,7 @@ export class VercelSandboxProvider implements HarnessV1SandboxProvider {
}

const sandbox = await Sandbox.get({
...getSandboxLookupParams(this.settings),
name: sessionSandboxName(options.sessionId),
...(options.abortSignal ? { signal: options.abortSignal } : {}),
});
Expand All @@ -250,6 +252,28 @@ const SNAPSHOT_CACHE_KEY = Symbol.for(
);

type SnapshotCache = Map<string, string>;
type SandboxLookupParams = {
fetch?: typeof fetch;
projectId?: string;
teamId?: string;
token?: string;
};

function getSandboxLookupParams(
settings: VercelSandboxSettings,
): SandboxLookupParams {
if ('sandbox' in settings && settings.sandbox != null) {
return {};
}

const { fetch, projectId, teamId, token } = settings as SandboxLookupParams;
return {
...(fetch ? { fetch } : {}),
...(projectId ? { projectId } : {}),
...(teamId ? { teamId } : {}),
...(token ? { token } : {}),
};
}

function getSnapshotCache(): SnapshotCache {
const globals = globalThis as {
Expand All @@ -265,12 +289,14 @@ function getSnapshotCache(): SnapshotCache {

async function pollForTemplateSnapshot(
name: string,
lookupParams: ReturnType<typeof getSandboxLookupParams>,
abortSignal: AbortSignal | undefined,
): Promise<string> {
const deadline = Date.now() + SNAPSHOT_POLL_TIMEOUT_MS;
while (Date.now() < deadline) {
abortSignal?.throwIfAborted();
const refreshed = await Sandbox.get({
...lookupParams,
name,
resume: false,
...(abortSignal ? { signal: abortSignal } : {}),
Expand Down