Skip to content

feat(ai-sandbox): serverless/edge run model + Cloudflare example (stacked on #774)#801

Merged
tombeckenham merged 25 commits into
feat/sandboxesfrom
feat/sandbox-serverless-runtime
Jun 23, 2026
Merged

feat(ai-sandbox): serverless/edge run model + Cloudflare example (stacked on #774)#801
tombeckenham merged 25 commits into
feat/sandboxesfrom
feat/sandbox-serverless-runtime

Conversation

@tombeckenham

Copy link
Copy Markdown
Contributor

Stacked on #774 (feat/sandboxes). Base this PR's review/merge on #774 — the diff here is only the serverless/edge additions on top of the sandbox layer.

What this adds

The serverless/edge execution model for the sandbox layer: a trigger starts an agent run and returns immediately while a durable orchestrator drives it and clients tail the stream from a resumable cursor. This is the piece that makes the sandbox layer work where a request-scoped Worker can't hold a multi-minute run open.

Core (@tanstack/ai-sandbox)

  • Resumable run event-logRunEventLog / InMemoryRunEventLog: append-only, seq-indexed, replay-then-tail. A dropped connection / new tab / hibernated orchestrator reconnect by passing their last-seen seq.
  • Run driverpipeToRunLog + RunController (start-without-blocking, resumable attach, drain for waitUntil-style flushing).
  • Transport-agnostic tool-bridge — split into a portable core (createToolBridgeCore / handleBridgeJsonRpc) + transport. startHostToolBridge remains the node:http host transport; an edge orchestrator serves the same core from its own fetch handler (no raw TCP listener). New ToolBridgeProvisioner capability injects the transport.

Adapters

  • claude-code, codex, gemini-cli, opencode resolve the bridge from the ToolBridgeProvisioner capability (default = host transport) instead of hardcoding node:http. Host/Docker behaviour unchanged.
  • Claude Code now runs on the Cloudflare sandbox: a new SandboxCapabilities.writableStdin flag lets a provider advertise no writable host→process stdin; the adapter then delivers the prompt via a file + in-shell stdin-redirection (claude -p … < file) instead of a host stdin write.

Example

  • examples/sandbox-cloudflare-agent — Worker (trigger → 202 {runId}) → RunCoordinator Durable Object (drives the run under ctx.waitUntil, DO-storage-backed run-log, fetch-served MCP bridge, hibernatable WebSocket with cursor resume) → Container. Compile-only / not runtime-verified in CI (no Workers runtime here; examples aren't built by Nx) — typechecks against the real @cloudflare/workers-types + @cloudflare/sandbox. Run locally with wrangler dev (see its README).

Security (addresses findings from the #774 review)

  • Fixed — bearer token out of argv. The bridge token is no longer passed inline via --mcp-config '{…Bearer…}'; the config is written to a file and claude gets the path, so the token isn't readable via ps / /proc/<pid>/cmdline.
  • Fixed — constant-time bearer compare (timingSafeBearerEqual; Web-Crypto variant in the DO).
  • Improved — bind scope. node:http transport binds loopback by default, widening to 0.0.0.0 only for the Docker (host.docker.internal) case; the edge transport opens no TCP listener at all.
  • Follow-up (not in this PR) — secrets in agent env (review H1). The example still env-injects ANTHROPIC_API_KEY. Cloudflare's own tutorial keeps the key out of the container via an outboundByHost proxy that swaps a sentinel on egress — worth adopting in a follow-up.

Verification

  • @tanstack/ai-sandbox: 119 tests (incl. new run-log + run suites), tsc + eslint clean.
  • claude-code / codex / gemini-cli / opencode: tsc + tests + eslint clean.
  • ai-sandbox-cloudflare + the example: tsc clean (example against real CF SDK types).
  • Not runtime-verified: the Cloudflare Worker/DO/Container path (no Workers runtime in CI).

🤖 Generated with Claude Code

Adds the serverless/edge execution model so a trigger can start an agent run
and return immediately while a durable orchestrator drives it and clients tail
from a resumable cursor.

- Resumable run event-log (RunEventLog/InMemoryRunEventLog) + run driver
  (pipeToRunLog, RunController).
- Transport-agnostic tool-bridge (createToolBridgeCore/handleBridgeJsonRpc) +
  ToolBridgeProvisioner capability. node:http transport hardened: loopback bind
  unless Docker needs host.docker.internal, constant-time bearer compare.
- Harness adapters (claude-code/codex/gemini-cli/opencode) resolve the bridge
  from the provisioner capability instead of hardcoding node:http.
- Claude Code runs on Cloudflare: SandboxCapabilities.writableStdin flag +
  prompt delivered via file/in-shell stdin-redirection.
- Tool-bridge bearer token moved out of argv (--mcp-config via file).
- examples/sandbox-cloudflare-agent: Worker -> Durable Object -> Container
  reference (compile-only, not runtime-verified in-repo).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 62357a6c-8b21-4245-8193-47439baf1e63

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/sandbox-serverless-runtime

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

🚀 Changeset Version Preview

9 package(s) bumped directly, 31 bumped as dependents.

🟥 Major bumps

Package Version Reason
@tanstack/ai-claude-code 0.1.0 → 1.0.0 Changeset
@tanstack/ai-codex 0.1.0 → 1.0.0 Changeset
@tanstack/ai-gemini-cli 0.1.0 → 1.0.0 Changeset
@tanstack/ai-opencode 0.1.0 → 1.0.0 Changeset
@tanstack/ai-sandbox 0.1.0 → 1.0.0 Changeset
@tanstack/ai-sandbox-cloudflare 0.1.0 → 1.0.0 Changeset
@tanstack/ai-sandbox-docker 0.1.0 → 1.0.0 Changeset
@tanstack/ai-sandbox-local-process 0.1.0 → 1.0.0 Changeset
@tanstack/ai-angular 0.1.2 → 1.0.0 Dependent
@tanstack/ai-anthropic 0.15.3 → 1.0.0 Dependent
@tanstack/ai-code-mode 0.2.7 → 1.0.0 Dependent
@tanstack/ai-code-mode-skills 0.2.7 → 1.0.0 Dependent
@tanstack/ai-elevenlabs 0.2.22 → 1.0.0 Dependent
@tanstack/ai-event-client 0.6.1 → 1.0.0 Dependent
@tanstack/ai-fal 0.8.1 → 1.0.0 Dependent
@tanstack/ai-gemini 0.16.1 → 1.0.0 Dependent
@tanstack/ai-grok 0.11.4 → 1.0.0 Dependent
@tanstack/ai-groq 0.4.4 → 1.0.0 Dependent
@tanstack/ai-isolate-node 0.1.32 → 1.0.0 Dependent
@tanstack/ai-isolate-quickjs 0.1.32 → 1.0.0 Dependent
@tanstack/ai-ollama 0.8.3 → 1.0.0 Dependent
@tanstack/ai-openai 0.14.3 → 1.0.0 Dependent
@tanstack/ai-openrouter 0.13.3 → 1.0.0 Dependent
@tanstack/ai-preact 0.9.7 → 1.0.0 Dependent
@tanstack/ai-react 0.15.7 → 1.0.0 Dependent
@tanstack/ai-react-ui 0.8.8 → 1.0.0 Dependent
@tanstack/ai-solid 0.13.7 → 1.0.0 Dependent
@tanstack/ai-solid-ui 0.7.8 → 1.0.0 Dependent
@tanstack/ai-svelte 0.13.7 → 1.0.0 Dependent
@tanstack/ai-vue 0.13.7 → 1.0.0 Dependent
@tanstack/openai-base 0.8.3 → 1.0.0 Dependent

🟨 Minor bumps

Package Version Reason
@tanstack/ai 0.30.0 → 0.31.0 Changeset

🟩 Patch bumps

Package Version Reason
@tanstack/ai-client 0.17.2 → 0.17.3 Dependent
@tanstack/ai-devtools-core 0.4.10 → 0.4.11 Dependent
@tanstack/ai-isolate-cloudflare 0.2.23 → 0.2.24 Dependent
@tanstack/ai-mcp 0.1.2 → 0.1.3 Dependent
@tanstack/ai-vue-ui 0.2.19 → 0.2.20 Dependent
@tanstack/preact-ai-devtools 0.1.53 → 0.1.54 Dependent
@tanstack/react-ai-devtools 0.2.53 → 0.2.54 Dependent
@tanstack/solid-ai-devtools 0.2.53 → 0.2.54 Dependent

@nx-cloud

nx-cloud Bot commented Jun 22, 2026

Copy link
Copy Markdown

View your CI Pipeline Execution ↗ for commit 2758669

Command Status Duration Result
nx run-many --targets=build --exclude=examples/... ✅ Succeeded 1m 26s View ↗

☁️ Nx Cloud last updated this comment at 2026-06-23 10:55:04 UTC

@nx-cloud

nx-cloud Bot commented Jun 22, 2026

Copy link
Copy Markdown

View your CI Pipeline Execution ↗ for commit db8821d

Command Status Duration Result
nx run-many --targets=build --exclude=examples/... ✅ Succeeded 1m 36s View ↗

☁️ Nx Cloud last updated this comment at 2026-06-22 08:26:06 UTC

@pkg-pr-new

pkg-pr-new Bot commented Jun 22, 2026

Copy link
Copy Markdown

Open in StackBlitz

@tanstack/ai

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai@801

@tanstack/ai-angular

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-angular@801

@tanstack/ai-anthropic

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-anthropic@801

@tanstack/ai-claude-code

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-claude-code@801

@tanstack/ai-client

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-client@801

@tanstack/ai-code-mode

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-code-mode@801

@tanstack/ai-code-mode-skills

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-code-mode-skills@801

@tanstack/ai-codex

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-codex@801

@tanstack/ai-devtools-core

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-devtools-core@801

@tanstack/ai-elevenlabs

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-elevenlabs@801

@tanstack/ai-event-client

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-event-client@801

@tanstack/ai-fal

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-fal@801

@tanstack/ai-gemini

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-gemini@801

@tanstack/ai-gemini-cli

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-gemini-cli@801

@tanstack/ai-grok

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-grok@801

@tanstack/ai-groq

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-groq@801

@tanstack/ai-isolate-cloudflare

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-isolate-cloudflare@801

@tanstack/ai-isolate-node

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-isolate-node@801

@tanstack/ai-isolate-quickjs

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-isolate-quickjs@801

@tanstack/ai-mcp

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-mcp@801

@tanstack/ai-ollama

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-ollama@801

@tanstack/ai-openai

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-openai@801

@tanstack/ai-opencode

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-opencode@801

@tanstack/ai-openrouter

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-openrouter@801

@tanstack/ai-preact

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-preact@801

@tanstack/ai-react

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-react@801

@tanstack/ai-react-ui

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-react-ui@801

@tanstack/ai-sandbox

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-sandbox@801

@tanstack/ai-sandbox-cloudflare

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-sandbox-cloudflare@801

@tanstack/ai-sandbox-docker

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-sandbox-docker@801

@tanstack/ai-sandbox-local-process

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-sandbox-local-process@801

@tanstack/ai-solid

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-solid@801

@tanstack/ai-solid-ui

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-solid-ui@801

@tanstack/ai-svelte

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-svelte@801

@tanstack/ai-utils

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-utils@801

@tanstack/ai-vue

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-vue@801

@tanstack/ai-vue-ui

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-vue-ui@801

@tanstack/openai-base

npm i https://pkg.pr.new/TanStack/ai/@tanstack/openai-base@801

@tanstack/preact-ai-devtools

npm i https://pkg.pr.new/TanStack/ai/@tanstack/preact-ai-devtools@801

@tanstack/react-ai-devtools

npm i https://pkg.pr.new/TanStack/ai/@tanstack/react-ai-devtools@801

@tanstack/solid-ai-devtools

npm i https://pkg.pr.new/TanStack/ai/@tanstack/solid-ai-devtools@801

commit: b783979

tombeckenham and others added 8 commits June 22, 2026 20:07
…bridge in-container

Adds the co-located variant: the harness loop AND its MCP tool-bridge run INSIDE
the container (the in-container sandbox is just local-process — native stdin, a
localhost node:http bridge, no public bridge URL). The Durable Object stays
outside as a thin durable coordinator. Only chat()-tool EXECUTION crosses the
container→orchestrator boundary, shrinking the public surface from the whole MCP
protocol to one authenticated tool-exec call.

- New @tanstack/ai-sandbox exports: remoteToolStubs, toolDescriptors,
  httpRemoteToolExecutor, executeHostTool (+ RemoteToolExecutor). The container
  rebuilds chat() tools as delegating stubs; the orchestrator answers one
  tool-exec call with the real tool's execute(). (6 unit tests, local-process.)
- Integration test (claude-code): a real adapter in a real local-process sandbox
  with a fake claude that performs an MCP tools/call against the in-container
  bridge, proving the round-trip agent → localhost bridge → stub → executeHostTool
  → real tool → stream. (34 tests pass.)
- examples/sandbox-cloudflare-agent-colocated: Worker → DO → Container reference
  with the in-container runner (compile-only; tsc -b clean).
- docs: "Edge execution: two models" section contrasting DO-drives vs co-located.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…l edge agent DX

Promotes the CF serverless/edge orchestration out of the examples and into the
package, so an app's worker.ts is a single configured function call. The
coordinator, durable run-log, WebSocket streaming, tool-bridge, and in-container
runner all move into @tanstack/ai-sandbox-cloudflare.

New `@tanstack/ai-sandbox-cloudflare/agent` (Workers entry):
- SandboxCoordinator (abstract base): durable run-log, resumable hibernatable
  WebSocket tail, startRun→pipe-under-waitUntil, watchdog alarm, status.
- ChatSandboxCoordinator (DO-drives) + ContainerSandboxCoordinator (co-located).
- createCloudflareSandboxAgent(config) → { Coordinator, Sandbox, worker };
  createSandboxAgentWorker() router; DurableObjectRunEventLog;
  timingSafeBearerEqualWeb (constant-time, no non-null assertions).

New `@tanstack/ai-sandbox-cloudflare/runner` (node/container entry):
- runInContainerHarness({ resolveAdapter }) — the in-container HTTP server +
  chat() wiring for the co-located model; the app supplies only the adapter.

Both CF examples collapse to a ~10-line worker.ts (one createCloudflareSandboxAgent
call + the wrangler-required DO re-exports); coordinator/run-log/protocol files
deleted. Net −1,900 lines in examples. Build externalizes cloudflare:workers;
compile-only (not runtime-verified in-repo).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Address the PR #801 review findings on the edge/serverless run layer.

Critical
- coordinator: guard against duplicate concurrent WebSocket pumps on one
  socket (WeakSet) so a client message can't double-deliver events / corrupt
  the resume cursor; add WebSocket.OPEN guards before send/close.
- coordinator: open the run BEFORE building its stream, recording a RUN_ERROR
  + finish('error') if buildRunStream throws, so a build-time failure is still
  observed by tailing clients instead of hanging.
- chat/container coordinators: guard request.json() in serveBridge /
  serveToolExec (JSON-RPC -32700 / 400 instead of an opaque DO 500).
- coordinator: make the watchdog alarm a real backstop — fail runs stalled past
  WATCHDOG_STALL_MS (orchestrator presumed dead) and re-arm even on storage error.

Important
- remote-tools / container-coordinator: thread the abort signal + run context
  across the tool-exec boundary (per-run AbortController, fetch signal forwarding)
  so a cancelled run cancels in-flight host tools and tools receive their context.
- container-coordinator: parse NDJSON via parseChunkLine — unparseable/non-chunk
  lines become terminal RUN_ERROR chunks, never thrown or silently dropped.
- diagnostics: surface the last /health probe error from ensureRunner; log +
  400-on-malformed-body in startHostToolBridge; server-side log in the tail pump.
- container-coordinator: memoize the in-flight runner boot (no EADDRINUSE race).
- run-log: derive InMemory seq from lastSeq+1 to match the DO; drop unused
  'pending' status; add named TerminalRunStatus.

Tidy-ups
- contracts: make SandboxCapabilities.writableStdin a required boolean; set it
  on the docker / local-process providers.
- tool-bridge: replace the lone `as` in listTools with a guard (toObjectSchema).
- remote-tools: centralize ToolExecRequest + isToolExecRequest; protocol derives
  HarnessId from HARNESS_IDS.
- claude-code: clean up the per-run token / prompt temp files in finally.
- docs/skill: SKILL.md edge-execution section; bump docs updatedAt; clarify the
  co-located .dev.vars OAuth hint.

Tests
- new unit suites: parseContainerRunRequest branches, DurableObjectRunEventLog
  (incl. eviction/re-poll), handleBridgeJsonRpc branches + permission path,
  timingSafeBearerEqual / timingSafeBearerEqualWeb truth tables.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…stream

Two stacked bugs left runs stuck at `status:running` with nothing streamed
(claude never started its turn) in the Cloudflare sandbox agent:

- ai-claude-code: the adapter defaults `--permission-mode bypassPermissions`,
  which claude maps to `--dangerously-skip-permissions` and refuses to run as
  root. Sandbox containers run as root, so claude died instantly. Set
  `IS_SANDBOX=1` in the CLI env (claude's documented escape hatch), merged over
  any caller-provided env.
- ai-sandbox-cloudflare: `spawn()` used `@cloudflare/sandbox`'s background
  process API (`startProcess` + `streamProcessLogs`), whose `onOutput`/`onExit`
  callbacks never fire — so the stdout-NDJSON harness hung forever. Stream over
  `exec({ stream: true, onOutput })` instead (the same proven path one-shot
  `exec` uses) and resolve the exit code from its result. Do not forward the
  caller's AbortSignal across the Durable Object RPC boundary (Workers RPC
  cannot serialize an AbortSignal, which threw before the command ran); a failed
  command now rejects `wait()` so the adapter surfaces a RUN_ERROR instead of a
  silent zero-output run.

Verified end-to-end on Docker Desktop (`wrangler dev`): a run streams
RUN_STARTED → text deltas → RUN_FINISHED and settles `done`.

Also wires the Sandbox Agent page into the ts-code-mode-web example (nav link,
route, proxy, README) and bumps the example's deploy config.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…agent; drop colocated example

Convert `examples/sandbox-cloudflare-agent` into a TanStack Start app that ships
the UI, the agent, the run-coordinator Durable Object, and the `@cloudflare/sandbox`
container in one Cloudflare Worker (one `wrangler deploy`).

- `src/server.ts`: custom Cloudflare entry (`@cloudflare/vite-plugin` + TanStack
  Start). Re-exports the `RunCoordinator`/`Sandbox` DO classes and composes
  `proxyToSandbox` (preview ports) → `agent.worker` (`/runs`, `/_bridge`,
  `/tool-exec`) → Start SSR (UI + `/api/*`). Typed via `satisfies ExportedHandler`.
- `src/agent.ts`: the `createCloudflareSandboxAgent({ adapter: claudeCodeText('sonnet') })`
  config + demo host tool (DO-drives mode).
- `src/routes/index.tsx`: vanilla `useChat` chat UI.
- `src/routes/api.run.ts`: same-origin SSE proxy bridging the agent's
  POST-then-WebSocket protocol; uses the workerd `fetch(Upgrade)` API.
- Generated `worker-configuration.d.ts` committed (`pnpm cf-typegen`); drop
  `@cloudflare/workers-types` per wrangler's runtime-types guidance.

Delete `examples/sandbox-cloudflare-agent-colocated`. Colocation stays a supported
package mode (`mode: 'colocated'` + `runInContainerHarness`); the DO-drives vs
colocated tradeoff is documented in prose (docs/sandbox/overview.md, changeset,
example README) rather than exemplified.

Remove the now-superseded `_sandbox-agent` proxy route from
`examples/ts-code-mode-web` (its UI moved into the new example).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@socket-security

socket-security Bot commented Jun 23, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Added@​cloudflare/​vite-plugin@​1.42.110010087100100
Addedwrangler@​4.103.0991009296100

View full report

autofix-ci Bot and others added 9 commits June 23, 2026 07:57
Restore examples/ts-code-mode-web to its main state. This branch had added a
README and the `_sandbox-agent` proxy route (and the earlier commit removed the
Header link + routeTree entries); none of it belongs here now that the edge
sandbox agent is its own TanStack Start example. Net ts-code-mode-web diff vs
main is now zero.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…agent

Make the sandbox-cloudflare-agent example demonstrate something real: ask the
agent to build a TanStack AI chatbot.

- `tanstackAiRecipe` host tool (replaces the throwaway `lookup`): returns the
  current TanStack AI chatbot recipe (packages, server route, client hook, run
  step), grounded in docs/getting-started. The agent calls it before scaffolding,
  which exercises the `/_bridge` MCP path AND yields a working app. Recipe targets
  the Anthropic adapter so it reuses the sandbox's `ANTHROPIC_API_KEY`.
- Prompt suggestions + empty-state rewritten around the chatbot build → run →
  preview URL flow.
- Configurable sandbox env: an explicit `sandbox` resolver injects `createSecrets`
  from the Worker env, so adding a var is one line + `.dev.vars` (with a typed
  `AppEnv extends SandboxAgentEnv` extension documented inline). README gains a
  "Setting sandbox env" section and a Limitations note that env is host-controlled,
  not per-user (the run trigger carries only threadId + messages).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… Intent

Make the sandbox-cloudflare-agent example demonstrate something real and
zero-config: ask the agent to build a self-contained TanStack Start app (kanban,
dashboard, small game…) that runs with NO env, API keys, or external services —
so its sandbox preview URL works for anyone.

- Dockerfile ships the `tanstack` CLI (`npm i -g @tanstack/cli`). The in-container
  `claude` is otherwise a bare install with none of the host's skills; rather than
  copy skill files in, we ship the CLI that provisions them. The agent scaffolds
  with `tanstack create … --intent`, which writes TanStack Intent skill mappings
  into the generated project for coding agents.
- `tanstackStartRecipe` host tool (replaces the throwaway `lookup`): bridged over
  the `/_bridge` MCP path, it returns the scaffold command (`tanstack create`),
  what to build (a no-env app), and the sandbox-specific run step (bind 0.0.0.0,
  expose the port, return the preview URL) — the bits the generic skill can't know.
- Prompt suggestions + empty-state rewritten around the no-env build → run →
  preview URL flow.
- Configurable sandbox env: an explicit `sandbox` resolver injects `createSecrets`
  from the Worker env (the demo app needs none; `ANTHROPIC_API_KEY` is only for
  the `claude` CLI). README gains a "Setting sandbox env" section and a Limitations
  note that env is host-controlled, not per-user.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Turn "the agent built and ran an app" into "here's the running app" — a clickable
preview link in the chat UI.

- `exposePreview` host tool (bridged over `/_bridge`): the agent calls it with the
  dev-server port once it's listening; the tool runs host-side, calls
  `sandbox.exposePort(port, { hostname: PUBLIC_HOSTNAME })` on the run's container,
  and returns the public URL. `proxyToSandbox` (already in server.ts) routes it back
  into the container.
- `namedCloudflareSandbox` (src/sandbox-provider.ts): pins the container Durable
  Object to the run's `threadId` instead of a random UUID, so the host tool can
  address the same container the dev server runs in. Side benefit: deterministic
  per-thread reuse across DO eviction.
- UI renders the `exposePreview` result as an "Open preview ↗" link.
- Recipe `run` step + README updated; documents the deployed-vs-local wildcard-DNS
  caveat (preview URLs resolve on a deployed Worker, not under localhost).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…hability docs

The in-sandbox agent's container reaches the host over `PUBLIC_HOSTNAME` for the
`tanstack` MCP tool-bridge (`/_bridge`) and preview ports. A local `pnpm dev`
worker isn't reachable from that container, so runs failed with "the tanstack MCP
server hasn't come up" (the bridge 404s against the wrong instance).

- `pnpm dev:tunnel` (scripts/dev-tunnel.mjs): starts a `cloudflared` quick tunnel,
  writes the assigned `*.trycloudflare.com` host into `.dev.vars` as
  `PUBLIC_HOSTNAME`, then runs vite — so the container can reach the bridge and
  agent runs work locally. (Quick tunnel = one hostname, so the bridge works but
  wildcard preview URLs still need a named tunnel or a deploy.)
- wrangler.jsonc `PUBLIC_HOSTNAME` comment + README Limitations now spell out the
  requirement and the exact "hasn't come up" symptom, and the README documents the
  tunnel workflow (quick vs named).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@tombeckenham tombeckenham requested a review from a team as a code owner June 23, 2026 10:00
autofix-ci Bot and others added 4 commits June 23, 2026 10:02
…e cloudflared)

The `@cloudflare/vite-plugin` has first-class Cloudflare Tunnel support and
downloads `cloudflared` itself — so the local-dev bridge tunnel needs no separate
install and no custom script.

- Drop `scripts/dev-tunnel.mjs`; `pnpm dev:tunnel` is now `TUNNEL=1 vite dev`,
  which enables the plugin's `tunnel: { autoStart: true }`. (Or press `t + Enter`
  in a running `pnpm dev`.)
- vite.config gates the tunnel on `TUNNEL` so plain `pnpm dev` is unchanged.
- README updated: point `PUBLIC_HOSTNAME` at the tunnel host (quick tunnel = copy
  the printed `*.trycloudflare.com`; named tunnel = stable host + wildcard route
  for previews).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…T_REQUEST_HOST)

The agent's container reaches the host over the bridge / tool-exec / preview URL.
Those were built from a static `PUBLIC_HOSTNAME`, which doesn't fit a dev tunnel
(dynamic host) — leading to "the tanstack MCP server hasn't come up" locally.

Add `StartRunInput.publicHost`: the Worker captures the `POST /runs` request host
and the coordinators can build the callback URL from it, so opening the UI at the
tunnel URL "just works" with no hostname to copy into config.

SECURITY: the request `Host` is client-controlled and the per-run bearer token
rides the derived URL, so trusting it blindly is a Host-injection / token-exfil
vector. It is therefore **off by default** — `PUBLIC_HOSTNAME` stays authoritative
unless `env.TRUST_REQUEST_HOST === '1'` (dev-only opt-in; never production). Both
coordinators and the example's `exposePreview` gate on the same flag.

Example: `.dev.vars.example` documents `TRUST_REQUEST_HOST=1` (with the warning),
and the README tunnel flow drops the manual `PUBLIC_HOSTNAME` copy in favor of it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@tombeckenham tombeckenham merged commit b180490 into feat/sandboxes Jun 23, 2026
10 checks passed
@tombeckenham tombeckenham deleted the feat/sandbox-serverless-runtime branch June 23, 2026 11:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant