From b5c3e3d53f2b52e1f5c0d63a2d25906d532dee66 Mon Sep 17 00:00:00 2001 From: Marc Codina Date: Thu, 9 Jul 2026 11:21:01 +0200 Subject: [PATCH 1/4] feat(ai-sandbox-vercel): tag sandbox API requests with a framework user-agent Wrap the Vercel Sandbox SDK's fetch so every control-plane request appends a `@tanstack/ai-sandbox-vercel` token to the user-agent, letting Vercel attribute sandbox traffic to this framework. Mirrors vercel/eve#332. Co-Authored-By: Claude Opus 4.8 --- .changeset/vercel-sandbox-user-agent.md | 5 ++ packages/ai-sandbox-vercel/src/provider.ts | 16 +++- packages/ai-sandbox-vercel/src/user-agent.ts | 33 +++++++++ .../tests/user-agent.test.ts | 73 +++++++++++++++++++ 4 files changed, 125 insertions(+), 2 deletions(-) create mode 100644 .changeset/vercel-sandbox-user-agent.md create mode 100644 packages/ai-sandbox-vercel/src/user-agent.ts create mode 100644 packages/ai-sandbox-vercel/tests/user-agent.test.ts diff --git a/.changeset/vercel-sandbox-user-agent.md b/.changeset/vercel-sandbox-user-agent.md new file mode 100644 index 000000000..559587d9a --- /dev/null +++ b/.changeset/vercel-sandbox-user-agent.md @@ -0,0 +1,5 @@ +--- +'@tanstack/ai-sandbox-vercel': patch +--- + +Vercel sandbox requests now append a `@tanstack/ai-sandbox-vercel` token to the `user-agent`. diff --git a/packages/ai-sandbox-vercel/src/provider.ts b/packages/ai-sandbox-vercel/src/provider.ts index b172c0d65..c4da4beeb 100644 --- a/packages/ai-sandbox-vercel/src/provider.ts +++ b/packages/ai-sandbox-vercel/src/provider.ts @@ -1,5 +1,6 @@ import { APIError, Sandbox } from '@vercel/sandbox' import { VERCEL_CAPS, VercelHandle } from './handle' +import { withSandboxUserAgent } from './user-agent' import type { SandboxCapabilities, SandboxCreateInput, @@ -67,6 +68,8 @@ export function isDirAlreadyExistsError(error: unknown): boolean { class VercelProvider implements SandboxProvider { readonly name = 'vercel' + private readonly fetch = withSandboxUserAgent() + constructor(private readonly config: VercelSandboxConfig) {} capabilities(): SandboxCapabilities { @@ -101,6 +104,7 @@ class VercelProvider implements SandboxProvider { async create(input: SandboxCreateInput): Promise { const sandbox = await Sandbox.create({ ...this.auth(), + fetch: this.fetch, runtime: this.config.runtime ?? DEFAULT_RUNTIME, ...(this.config.timeout !== undefined ? { timeout: this.config.timeout } @@ -133,7 +137,11 @@ class VercelProvider implements SandboxProvider { async resume(input: SandboxResumeInput): Promise { try { - const sandbox = await Sandbox.get({ name: input.id, ...this.auth() }) + const sandbox = await Sandbox.get({ + name: input.id, + ...this.auth(), + fetch: this.fetch, + }) return new VercelHandle({ sandbox, workdir: this.workdir, @@ -147,7 +155,11 @@ class VercelProvider implements SandboxProvider { async destroy(input: SandboxDestroyInput): Promise { try { - const sandbox = await Sandbox.get({ name: input.id, ...this.auth() }) + const sandbox = await Sandbox.get({ + name: input.id, + ...this.auth(), + fetch: this.fetch, + }) await sandbox.stop() } catch { // Already stopped / gone. diff --git a/packages/ai-sandbox-vercel/src/user-agent.ts b/packages/ai-sandbox-vercel/src/user-agent.ts new file mode 100644 index 000000000..14105f481 --- /dev/null +++ b/packages/ai-sandbox-vercel/src/user-agent.ts @@ -0,0 +1,33 @@ +/** + * User-agent tagging for Vercel Sandbox traffic. + * + * The `@vercel/sandbox` SDK sends a `user-agent` of + * `vercel/sandbox/ (…)` on every API request. We wrap the SDK's + * `fetch` so each request also carries a `@tanstack/ai-sandbox-vercel` token. + */ + +export const USER_AGENT_TOKEN = '@tanstack/ai-sandbox-vercel' + +/** + * Wraps a `fetch` so every request's `user-agent` contains the package name + * (e.g. `vercel/sandbox/2.2.1 (…) @tanstack/ai-sandbox-vercel`). When no + * `user-agent` is present, the token becomes the whole header. + */ +export function withSandboxUserAgent( + inner: typeof globalThis.fetch = globalThis.fetch, +): typeof globalThis.fetch { + return (input, init) => { + const headers = new Headers( + init?.headers ?? + (typeof input === 'object' && 'headers' in input + ? input.headers + : undefined), + ) + const existing = headers.get('user-agent') + headers.set( + 'user-agent', + existing ? `${existing} ${USER_AGENT_TOKEN}` : USER_AGENT_TOKEN, + ) + return inner(input, { ...init, headers }) + } +} diff --git a/packages/ai-sandbox-vercel/tests/user-agent.test.ts b/packages/ai-sandbox-vercel/tests/user-agent.test.ts new file mode 100644 index 000000000..e75c21c0e --- /dev/null +++ b/packages/ai-sandbox-vercel/tests/user-agent.test.ts @@ -0,0 +1,73 @@ +import { describe, expect, it, vi } from 'vitest' +import { USER_AGENT_TOKEN, withSandboxUserAgent } from '../src/user-agent' + +function userAgentOf(init: RequestInit | undefined): string | null { + return new Headers(init?.headers).get('user-agent') +} + +describe('withSandboxUserAgent', () => { + it('appends the token to an existing user-agent (as the SDK sends)', async () => { + const inner = vi + .fn() + .mockResolvedValue(new Response()) + const wrapped = withSandboxUserAgent(inner) + + await wrapped('https://api.vercel.com/v2/sandboxes', { + headers: { 'user-agent': 'vercel/sandbox/2.2.1' }, + }) + + const [, init] = inner.mock.calls[0]! + expect(userAgentOf(init)).toBe(`vercel/sandbox/2.2.1 ${USER_AGENT_TOKEN}`) + }) + + it('sets the token as the user-agent when none is present', async () => { + const inner = vi + .fn() + .mockResolvedValue(new Response()) + const wrapped = withSandboxUserAgent(inner) + + await wrapped('https://api.vercel.com/v2/sandboxes') + + const [, init] = inner.mock.calls[0]! + expect(userAgentOf(init)).toBe(USER_AGENT_TOKEN) + }) + + it('preserves other request init fields (method, body, signal)', async () => { + const inner = vi + .fn() + .mockResolvedValue(new Response()) + const wrapped = withSandboxUserAgent(inner) + const signal = new AbortController().signal + + await wrapped('https://api.vercel.com/v2/sandboxes', { + method: 'POST', + body: '{"ok":true}', + signal, + }) + + const [, init] = inner.mock.calls[0]! + expect(init?.method).toBe('POST') + expect(init?.body).toBe('{"ok":true}') + expect(init?.signal).toBe(signal) + }) + + it('reads the user-agent off a Request input when no init is given', async () => { + const inner = vi + .fn() + .mockResolvedValue(new Response()) + const wrapped = withSandboxUserAgent(inner) + + await wrapped( + new Request('https://api.vercel.com/v2/sandboxes', { + headers: { 'user-agent': 'vercel/sandbox/2.2.1' }, + }), + ) + + const [, init] = inner.mock.calls[0]! + expect(userAgentOf(init)).toBe(`vercel/sandbox/2.2.1 ${USER_AGENT_TOKEN}`) + }) + + it('defaults to globalThis.fetch when no inner fetch is supplied', () => { + expect(typeof withSandboxUserAgent()).toBe('function') + }) +}) From 5cd432be6ac084cfb0a64524c1627b8d69694424 Mon Sep 17 00:00:00 2001 From: Marc Codina Date: Thu, 9 Jul 2026 11:52:00 +0200 Subject: [PATCH 2/4] feat(ai-sandbox-vercel): send @tanstack/ai as the user-agent token Identify the TanStack AI framework as a whole rather than this specific sandbox package. Co-Authored-By: Claude Opus 4.8 --- .changeset/vercel-sandbox-user-agent.md | 2 +- packages/ai-sandbox-vercel/src/user-agent.ts | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/.changeset/vercel-sandbox-user-agent.md b/.changeset/vercel-sandbox-user-agent.md index 559587d9a..3911be4d1 100644 --- a/.changeset/vercel-sandbox-user-agent.md +++ b/.changeset/vercel-sandbox-user-agent.md @@ -2,4 +2,4 @@ '@tanstack/ai-sandbox-vercel': patch --- -Vercel sandbox requests now append a `@tanstack/ai-sandbox-vercel` token to the `user-agent`. +Sandbox API requests now append a `@tanstack/ai` token to the `user-agent` so the Vercel sandbox control plane can attribute traffic to the TanStack AI framework. diff --git a/packages/ai-sandbox-vercel/src/user-agent.ts b/packages/ai-sandbox-vercel/src/user-agent.ts index 14105f481..31f8db85b 100644 --- a/packages/ai-sandbox-vercel/src/user-agent.ts +++ b/packages/ai-sandbox-vercel/src/user-agent.ts @@ -3,15 +3,16 @@ * * The `@vercel/sandbox` SDK sends a `user-agent` of * `vercel/sandbox/ (…)` on every API request. We wrap the SDK's - * `fetch` so each request also carries a `@tanstack/ai-sandbox-vercel` token. + * `fetch` so each request also carries a `@tanstack/ai` token, identifying the + * TanStack AI framework to the Vercel sandbox control plane. */ -export const USER_AGENT_TOKEN = '@tanstack/ai-sandbox-vercel' +export const USER_AGENT_TOKEN = '@tanstack/ai' /** - * Wraps a `fetch` so every request's `user-agent` contains the package name - * (e.g. `vercel/sandbox/2.2.1 (…) @tanstack/ai-sandbox-vercel`). When no - * `user-agent` is present, the token becomes the whole header. + * Wraps a `fetch` so every request's `user-agent` carries the + * {@link USER_AGENT_TOKEN} (e.g. `vercel/sandbox/2.2.1 (…) @tanstack/ai`). When + * no `user-agent` is present, the token becomes the whole header. */ export function withSandboxUserAgent( inner: typeof globalThis.fetch = globalThis.fetch, From 9a03d530d6d3cc38711dcdc3c0495f23d744ac04 Mon Sep 17 00:00:00 2001 From: Marc Codina Date: Thu, 9 Jul 2026 12:05:12 +0200 Subject: [PATCH 3/4] docs(ai-sandbox-vercel): drop unrelated prose churn from the token change Revert the JSDoc and changeset rewording accidentally bundled into the previous commit; keep only the `@tanstack/ai` user-agent token swap. Co-Authored-By: Claude Opus 4.8 --- .changeset/vercel-sandbox-user-agent.md | 2 +- packages/ai-sandbox-vercel/src/user-agent.ts | 9 ++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/.changeset/vercel-sandbox-user-agent.md b/.changeset/vercel-sandbox-user-agent.md index 3911be4d1..1af7df9b1 100644 --- a/.changeset/vercel-sandbox-user-agent.md +++ b/.changeset/vercel-sandbox-user-agent.md @@ -2,4 +2,4 @@ '@tanstack/ai-sandbox-vercel': patch --- -Sandbox API requests now append a `@tanstack/ai` token to the `user-agent` so the Vercel sandbox control plane can attribute traffic to the TanStack AI framework. +Vercel sandbox requests now append a `@tanstack/ai` token to the `user-agent`. diff --git a/packages/ai-sandbox-vercel/src/user-agent.ts b/packages/ai-sandbox-vercel/src/user-agent.ts index 31f8db85b..2a96f9077 100644 --- a/packages/ai-sandbox-vercel/src/user-agent.ts +++ b/packages/ai-sandbox-vercel/src/user-agent.ts @@ -3,16 +3,15 @@ * * The `@vercel/sandbox` SDK sends a `user-agent` of * `vercel/sandbox/ (…)` on every API request. We wrap the SDK's - * `fetch` so each request also carries a `@tanstack/ai` token, identifying the - * TanStack AI framework to the Vercel sandbox control plane. + * `fetch` so each request also carries a `@tanstack/ai` token. */ export const USER_AGENT_TOKEN = '@tanstack/ai' /** - * Wraps a `fetch` so every request's `user-agent` carries the - * {@link USER_AGENT_TOKEN} (e.g. `vercel/sandbox/2.2.1 (…) @tanstack/ai`). When - * no `user-agent` is present, the token becomes the whole header. + * Wraps a `fetch` so every request's `user-agent` contains the package name + * (e.g. `vercel/sandbox/2.2.1 (…) @tanstack/ai`). When no + * `user-agent` is present, the token becomes the whole header. */ export function withSandboxUserAgent( inner: typeof globalThis.fetch = globalThis.fetch, From 76d15d9f00e4531d980122c492056a3c0b9ef676 Mon Sep 17 00:00:00 2001 From: Marc Codina Date: Thu, 9 Jul 2026 16:29:00 +0200 Subject: [PATCH 4/4] chore: re-trigger CI Co-Authored-By: Claude Opus 4.8