diff --git a/.agent/domain-map.json b/.agent/domain-map.json index b09f64e809..7b8c9a8051 100644 --- a/.agent/domain-map.json +++ b/.agent/domain-map.json @@ -92,6 +92,7 @@ "packages/ai-sandbox-e2b", "packages/ai-sandbox-local-process", "packages/ai-sandbox-railway", + "packages/ai-sandbox-render", "packages/ai-sandbox-sprites", "packages/ai-sandbox-upstash-box", "packages/ai-sandbox-vercel" diff --git a/.changeset/add-ai-sandbox-render.md b/.changeset/add-ai-sandbox-render.md new file mode 100644 index 0000000000..88cfe6ef6c --- /dev/null +++ b/.changeset/add-ai-sandbox-render.md @@ -0,0 +1,17 @@ +--- +'@tanstack/ai-sandbox-render': minor +--- + +Add `@tanstack/ai-sandbox-render`, a sandbox provider backed by +[Render](https://render.com/docs/sandboxes) sandboxes through `@renderinc/sdk`. +It implements the `SandboxProvider` / `SandboxHandle` contract: native file +transfer for reads and writes, shell-backed `list`, `mkdir`, `remove`, +`rename`, `exists` and a non-following `lstat`, commands with separate stdout +and stderr, background processes, a shared env overlay, filesystem or +runtime snapshots, resume by id, and an enforced `deny-all` network policy. + +`killableProcesses` is `true` and measured against a real sandbox: every +command records its process group, and `kill()` or an aborted `signal` sends +the requested signal (`TERM` by default) to the group from a second command, +then `KILL` after 2 s. `ports`, +`writableStdin` and `fork` are `false`. diff --git a/docs/sandbox/providers-advanced.md b/docs/sandbox/providers-advanced.md index 68479eeb81..6e3f9802b4 100644 --- a/docs/sandbox/providers-advanced.md +++ b/docs/sandbox/providers-advanced.md @@ -380,10 +380,50 @@ export function provider(env: SandboxAgentEnv) { The agent coordinator, durable runs, live previews through a quick tunnel, callback hosts, and the Durable Object run log are on [Cloudflare](./cloudflare) and [Cloudflare Edge Details](./cloudflare-advanced). +## Render + +```ts +import { renderSandbox } from '@tanstack/ai-sandbox-render' + +const render = renderSandbox({ + plan: '2c-4g', + idleTimeoutSeconds: 900, + // Optional: when the policy allows network, allow only these hosts. + networkPolicy: { + type: 'allow-list', + rules: [{ domain: 'registry.npmjs.org' }, { domain: 'api.anthropic.com' }], + }, +}) +``` + +- **Isolation:** a managed [Render](https://render.com/docs/sandboxes) sandbox. It boots from the base image (Debian 12, `root` user), a custom image (`buildName`), or a named snapshot (`snapshotName`). +- **Auth:** the SDK reads `RENDER_API_KEY` and `RENDER_WORKSPACE_ID`. Pass `token` and `ownerId` to set them in code. Harness credentials are [workspace secrets](./provisioning). The provider uploads them to a shared file in `/dev/tsai` that only `root` can read. `/dev` is held in memory, so the secrets never reach the sandbox disk. Reconnecting with fresh secrets replaces that file and removes older overlays. No value goes into a command string. +- **Lifetime:** Render suspends an idle sandbox after `idleTimeoutSeconds` (default 900) and keeps its disk, so `durableFilesystem` is `true`. The next command or file call resumes it. After `timeoutSeconds` (default 86400), Render terminates it. +- **Network:** `policy.capabilities.network: 'deny'` sends `deny-all`, and Render enforces it. Otherwise the provider sends `networkPolicy`, or allows all egress. +- **Not available:** inbound ports, stdin for a spawned process, and fork. `ports.connect` throws. Harnesses that need a port or stdin, such as OpenCode and ACP harnesses, do not run on Render. + +Snapshots and resume on Render: + +Take snapshots while commands are stopped. A runtime snapshot also captures memory, so the provider removes the env file for the capture and writes it back after. When multiple orchestrator processes share a sandbox, use your lifecycle lock to serialize runtime snapshots and secret updates. + +- `snapshot(label)` captures a `filesystem` snapshot named `tsai-