From 06aa1dcc3d9636e4592d7493b873b1b5c12cce30 Mon Sep 17 00:00:00 2001 From: Don neumark Date: Tue, 30 Jun 2026 04:18:27 +0300 Subject: [PATCH] fix(sandbox-vercel): forward credentials when resuming sessions --- .changeset/tidy-sandboxes-resume.md | 5 +++ .../sandbox-vercel/src/vercel-sandbox.test.ts | 31 +++++++++++++++++-- packages/sandbox-vercel/src/vercel-sandbox.ts | 26 ++++++++++++++++ 3 files changed, 60 insertions(+), 2 deletions(-) create mode 100644 .changeset/tidy-sandboxes-resume.md diff --git a/.changeset/tidy-sandboxes-resume.md b/.changeset/tidy-sandboxes-resume.md new file mode 100644 index 000000000000..0e823dacef74 --- /dev/null +++ b/.changeset/tidy-sandboxes-resume.md @@ -0,0 +1,5 @@ +--- +'@ai-sdk/sandbox-vercel': patch +--- + +Forward Vercel Sandbox credentials when resuming named harness sessions. diff --git a/packages/sandbox-vercel/src/vercel-sandbox.test.ts b/packages/sandbox-vercel/src/vercel-sandbox.test.ts index edf19a41101f..d1af96a93962 100644 --- a/packages/sandbox-vercel/src/vercel-sandbox.test.ts +++ b/packages/sandbox-vercel/src/vercel-sandbox.test.ts @@ -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 = { @@ -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 () => { @@ -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, + }); + }); }); diff --git a/packages/sandbox-vercel/src/vercel-sandbox.ts b/packages/sandbox-vercel/src/vercel-sandbox.ts index 349f0e440999..845fc6d55a71 100644 --- a/packages/sandbox-vercel/src/vercel-sandbox.ts +++ b/packages/sandbox-vercel/src/vercel-sandbox.ts @@ -185,6 +185,7 @@ export class VercelSandboxProvider implements HarnessV1SandboxProvider { if (resolvedId == null) { resolvedId = await pollForTemplateSnapshot( templateName, + getSandboxLookupParams(baseParams), options?.abortSignal, ); } @@ -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 } : {}), }); @@ -250,6 +252,28 @@ const SNAPSHOT_CACHE_KEY = Symbol.for( ); type SnapshotCache = Map; +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 { @@ -265,12 +289,14 @@ function getSnapshotCache(): SnapshotCache { async function pollForTemplateSnapshot( name: string, + lookupParams: ReturnType, abortSignal: AbortSignal | undefined, ): Promise { 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 } : {}),