Skip to content

Commit d5bdc36

Browse files
authored
fix: make default Daytona + Grok/Codex sandbox runs work (#1094)
* fix: make default Daytona + Grok/Codex sandbox runs work (#1081 #1083 #1084 #1085) Default defineSandbox + gitSkill + grokBuildText/codexText + withSandbox works on Daytona without empty commands.deny, create wrappers, hardcoded workdir, or hand skill symlinks. Resume starts stopped sandboxes. Secrets stay off create-time envVars and command strings. Spawn uses session log streams and stdin. Native fs/git remap /workspace. * docs: keep Daytona details on the providers page Generic lifecycle, policy, provisioning, and workspace pages now describe portable sandbox behavior only. Provider-specific idle stop, sudo, network block, and secret storage live under docs/sandbox/providers.md. * docs: fix Daytona example for kiira typecheck defineWorkspace requires source. Add githubRepo so the providers sample type-checks. * fix: address CodeRabbit feedback on Daytona headless PR Drain spawn log streams before ending queues, remap skill paths for projection shell commands, map Codex default ask to on-request, clarify docs around sudo and policy isolation, tighten Daytona env/clone tests, and bump the Daytona package changeset to minor for new surface. * test(ai-acp): raise vitest timeout for stdio sandbox fixtures ACP stdio tests spawn a real local-process sandbox and node agent. Under Nx parallel load they often need more than the default 5s budget. * fix(ai-acp): make CI stdio tests and skill projection reliable Pin vitest run --testTimeout=30000 on the package script so Nx always applies the budget. Discover skills on virtual /workspace paths (fs remaps them) and only remap absolute paths for shell ln/cp.
1 parent 45b67d7 commit d5bdc36

46 files changed

Lines changed: 1847 additions & 284 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
'@tanstack/ai-sandbox': patch
3+
'@tanstack/ai-sandbox-daytona': minor
4+
'@tanstack/ai-grok-build': patch
5+
'@tanstack/ai-codex': patch
6+
'@tanstack/ai-acp': patch
7+
'@tanstack/ai-claude-code': patch
8+
---
9+
10+
fix: make default Daytona + Grok/Codex sandbox runs work without extra wrappers
11+
12+
Headless Grok and Codex stay permissive when policy is deny-only plus default allow. Daytona remaps `/workspace`, starts stopped sandboxes on resume, keeps secrets out of create-time envVars and command strings, and uses native fs/git plus session stdin. Nested skills project by name. Durable Grok journals when durability is wired.

docs/adapters/codex.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ const stream = chat({
5353
| Option | Description |
5454
| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
5555
| `cwd` | Working directory for the harness session. Defaults to `process.cwd()`. |
56-
| `sandboxMode` | Codex sandbox: `'read-only'` (harness default), `'workspace-write'`, or `'danger-full-access'`. This is the safety boundary on a server. |
56+
| `sandboxMode` | Codex sandbox: `'read-only'`, `'workspace-write'`, or `'danger-full-access'`. Default is `'workspace-write'` on local-process and Docker. Default is `'danger-full-access'` on Daytona and Cloudflare, because those providers cannot create a nested bubblewrap namespace. Isolation is then the outer VM plus `defineSandboxPolicy`. |
5757
| `approvalPolicy` | Codex approval policy. Defaults to `'never'` — headless runs have no approval UI, so anything else can stall a turn. |
5858
| `modelReasoningEffort` | `'minimal'` \| `'low'` \| `'medium'` \| `'high'` \| `'xhigh'`. |
5959
| `skipGitRepoCheck` | Skip the harness's git-repo safety check. Defaults to `true` (server adapters routinely point at scratch directories). |

docs/adapters/grok-build.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ Adapter config (second argument to `grokBuildText`):
105105
| `grokExecutable` | Path/name of the `grok` executable inside the sandbox. Defaults to `grok`. |
106106
| `env` | Extra environment variables for the `grok` process inside the sandbox. |
107107
| `emitDiff` | Emit a `file.changed` CUSTOM event with the working-tree `git diff` after the run. Defaults to `true`. |
108+
| `protocol` | Harness wire protocol: `'acp'` or `'streaming-json'`. Defaults to `'acp'`. A durable sandbox run with no protocol set uses `'streaming-json'` so the run can journal. |
108109
| `extraArgs` | Extra raw CLI flags appended verbatim (advanced). |
109110

110111
Per-call overrides go through `modelOptions`:
@@ -114,6 +115,7 @@ Per-call overrides go through `modelOptions`:
114115
| `sessionId` | Resume an existing Grok Build session (see below). |
115116
| `cwd` | Per-call override of the harness working directory. |
116117
| `maxTurns` | Per-call cap on the number of harness turns. |
118+
| `protocol` | Per-call override of the harness wire protocol. |
117119

118120
## Stateful Sessions
119121

@@ -166,6 +168,23 @@ tools inside a live process and can't pause across an HTTP round-trip. A tool
166168
without a server `execute()` (or marked `needsApproval`) fails fast; run those
167169
with a regular provider adapter.
168170

171+
## Durable runs
172+
173+
A durable sandbox run (you pass `runs` and `durability` to `withSandbox`)
174+
journals the agent output so a later request can take the run over. ACP does
175+
not journal. When durability is wired and you do not set `protocol`, the
176+
adapter uses `'streaming-json'`.
177+
178+
Set `protocol: 'acp'` only when you want ACP on that call. A fresh ACP run
179+
with durability wired logs a warning. An attach against ACP throws
180+
`DurableAttachNotSupportedError`.
181+
182+
## Headless NDJSON flags
183+
184+
On the `'streaming-json'` path with auto-approve, the adapter adds
185+
`--always-approve --no-plan --no-auto-update`. Those flags keep Plan Mode and
186+
the CLI update check from blocking a headless run.
187+
169188
## Limitations
170189

171190
- **Requires a sandbox.** Always run it under `withSandbox(...)`; see the

docs/config.json

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -521,7 +521,7 @@
521521
"label": "Providers",
522522
"to": "sandbox/providers",
523523
"addedAt": "2026-06-29",
524-
"updatedAt": "2026-08-04"
524+
"updatedAt": "2026-08-12"
525525
},
526526
{
527527
"label": "Harnesses",
@@ -533,7 +533,7 @@
533533
"label": "Workspace",
534534
"to": "sandbox/workspace",
535535
"addedAt": "2026-06-29",
536-
"updatedAt": "2026-08-04"
536+
"updatedAt": "2026-08-12"
537537
},
538538
{
539539
"label": "Tools",
@@ -545,13 +545,13 @@
545545
"label": "Policy",
546546
"to": "sandbox/policy",
547547
"addedAt": "2026-06-29",
548-
"updatedAt": "2026-08-04"
548+
"updatedAt": "2026-08-12"
549549
},
550550
{
551551
"label": "Lifecycle & Snapshots",
552552
"to": "sandbox/lifecycle",
553553
"addedAt": "2026-06-29",
554-
"updatedAt": "2026-08-04"
554+
"updatedAt": "2026-08-12"
555555
},
556556
{
557557
"label": "Instance Durability",
@@ -596,7 +596,7 @@
596596
"label": "Provisioning",
597597
"to": "sandbox/provisioning",
598598
"addedAt": "2026-06-29",
599-
"updatedAt": "2026-08-04"
599+
"updatedAt": "2026-08-12"
600600
},
601601
{
602602
"label": "Observability",
@@ -865,7 +865,7 @@
865865
"label": "Codex",
866866
"to": "adapters/codex",
867867
"addedAt": "2026-06-12",
868-
"updatedAt": "2026-06-30"
868+
"updatedAt": "2026-08-12"
869869
},
870870
{
871871
"label": "OpenCode",
@@ -876,7 +876,8 @@
876876
{
877877
"label": "Grok Build",
878878
"to": "adapters/grok-build",
879-
"addedAt": "2026-06-29"
879+
"addedAt": "2026-06-29",
880+
"updatedAt": "2026-08-12"
880881
},
881882
{
882883
"label": "ACP-Compatible",

docs/sandbox/lifecycle.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ const sandbox = defineSandbox({
3636
| ------------------- | ------------------------------------------------------------------------------------------- |
3737
| `reuse` | `'thread'` keeps one sandbox per `threadId`; `'none'` provisions a fresh sandbox per run. |
3838
| `snapshot` | `'after-setup'` snapshots the workspace once bootstrap finishes, on snapshot-capable providers. |
39-
| `keepAlive` | Duration hint (e.g. `'30m'`) for how long the sandbox should stay warm between runs. Nothing in `@tanstack/ai-sandbox` reads it today; it is carried for providers and host apps that implement their own idle GC. |
39+
| `keepAlive` | Duration hint (e.g. `'30m'`) for how long the sandbox should stay warm between runs. Nothing in `@tanstack/ai-sandbox` reads it today; it is a hint for providers and host apps. Provider idle timers live on the [provider](./providers) config. |
4040
| `destroyOnComplete` | When `false`, the sandbox survives the run so the next one can resume it. |
4141
| `snapshotMaxAge` | Duration (e.g. `'24h'`) after which a stored snapshot is treated as stale and re-created. |
4242

@@ -58,7 +58,7 @@ destroys, exactly as an abort does. See
5858

5959
## Snapshot after setup
6060

61-
When the provider supports snapshots (e.g. [Docker](./providers)), bootstrap
61+
When the provider supports snapshots (see [Providers](./providers)), bootstrap
6262
automatically takes a snapshot after `setup` completes. The snapshot caches the
6363
fully bootstrapped [workspace](./workspace) (the cloned repo with dependencies
6464
installed) so subsequent runs resume from it instead of re-running the setup

docs/sandbox/policy.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,9 @@ This is the broad backstop: even if a specific network command isn't in your
9191
`commands` lists, `network: 'ask'` still forces an approval for anything that
9292
reaches out.
9393

94+
Some providers also enforce network at create time when `network: 'deny'`. See
95+
[Providers](./providers) for that mapping.
96+
9497
## Precedence: deny > ask > allow
9598

9699
When more than one rule could match an action, the strictest wins. The order is
@@ -164,6 +167,15 @@ throwing. Because the mapping is the adapter's job, you write the policy once
164167
and it behaves consistently no matter which provider or harness runs the
165168
sandbox.
166169

170+
A deny-only list with `default: 'allow'` stays permissive on Grok Build and
171+
Codex. Those harnesses keep auto-approve. They do not enforce
172+
`commands.deny`. Isolation is the outer sandbox. Use Claude Code when you
173+
need command-level deny.
174+
175+
Some providers run as a non-root user, so package installs in `setup` need
176+
`sudo`. Do not deny `sudo *` on those providers. See [Providers](./providers)
177+
for the list, and [Workspace](./workspace) for how to write setup commands.
178+
167179
## Wiring it on
168180

169181
A policy does nothing on its own, it takes effect when you attach it to a

docs/sandbox/providers.md

Lines changed: 71 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ same.
2222
| --- | --- | --- | --- |
2323
| Local process | `@tanstack/ai-sandbox-local-process` | none (host) | The fast, no-Docker dev loop. Trusted/dev use only. |
2424
| Docker | `@tanstack/ai-sandbox-docker` | container | Real isolation; commit-based snapshots, fork, resume-by-id. |
25-
| Daytona | `@tanstack/ai-sandbox-daytona` | cloud sandbox | Managed [Daytona](https://www.daytona.io/) sandboxes; port preview links, resume-by-id. Needs `DAYTONA_API_KEY`. |
25+
| Daytona | `@tanstack/ai-sandbox-daytona` | cloud sandbox | Managed [Daytona](https://www.daytona.io/) sandboxes; snapshots after setup, port preview links, resume-by-id. Needs `DAYTONA_API_KEY`. |
2626
| Vercel | `@tanstack/ai-sandbox-vercel` | microVM | Managed [Vercel Sandbox](https://vercel.com/docs/sandbox) microVMs; exposed-port domains, resume-by-id (persistent). Needs `VERCEL_TOKEN` + team/project. |
2727
| Sprites | `@tanstack/ai-sandbox-sprites` | stateful sandbox | Managed [Sprites](https://sprites.dev) (Fly.io) sandboxes; durable filesystem, in-place checkpoints, single proxied public-URL port, resume-by-id. Needs `SPRITES_API_KEY`. |
2828

@@ -139,18 +139,78 @@ const isolated = dockerSandbox({ image: 'node:22' })
139139
```ts
140140
import { daytonaSandbox } from '@tanstack/ai-sandbox-daytona'
141141

142-
const daytona = daytonaSandbox({ apiKey: process.env.DAYTONA_API_KEY })
142+
const daytona = daytonaSandbox({
143+
apiKey: process.env.DAYTONA_API_KEY,
144+
snapshot: 'daytona-medium',
145+
autoStopInterval: 0,
146+
})
143147
```
144148

145149
- **Isolation:** a managed cloud sandbox on a remote VM you do not run yourself.
146-
- **Auth / env:** needs `DAYTONA_API_KEY`. Harness credentials are injected as
147-
workspace secrets; there is no host login to fall back on.
148-
- **Snapshot / resume:** no snapshots; resume-by-id reconnects to a still-running
149-
sandbox (not a restored point-in-time snapshot), plus port preview links for
150-
live previews.
151-
- **Bridge:** the sandbox is remote, so a [bridged tool](./tools) call can't reach
152-
your laptop's `localhost`. In local dev, tunnel the bridge (see [tools](./tools));
153-
a deployed orchestrator is reachable out of the box.
150+
- **Auth / env:** needs `DAYTONA_API_KEY`. Put harness credentials in
151+
[workspace secrets](./provisioning). They are applied to the live sandbox
152+
at create, resume, and restore. They are not stored on the Daytona create
153+
record, and they are not written into command history.
154+
- **Snapshot / resume:** point-in-time snapshots after setup (default when
155+
`lifecycle.snapshot` is `'after-setup'`). Pass `snapshot` on
156+
`daytonaSandbox()` to pick the Daytona image (for example
157+
`'daytona-medium'`). Resume starts a `stopped` or `archived` sandbox, then
158+
returns the handle.
159+
- **Idle stop:** Daytona stops an idle sandbox after 15 minutes by default.
160+
Set `autoStopInterval` in minutes to change that. Pass `0` to turn auto-stop
161+
off. Set `ephemeral: true` to delete the sandbox when it stops.
162+
- **Network:** `policy.capabilities.network: 'deny'` blocks all outbound
163+
network on create.
164+
- **Working directory:** the portable root `/workspace` maps to
165+
`/home/daytona/workspace` by default. Override with `workdir` on
166+
`daytonaSandbox()` if you need another path.
167+
- **Stdin:** spawned processes accept host stdin (`writableStdin: true`).
168+
- **Privileges:** the Daytona user is not root. Package installs in `setup`
169+
must use `sudo -n` (for example `sudo -n apt-get install …`). Do not put
170+
`sudo *` in a [policy](./policy) deny list for this provider.
171+
- **Bridge:** the sandbox is remote, so a [bridged tool](./tools) call cannot
172+
reach your laptop's `localhost`. In local dev, tunnel the bridge (see
173+
[tools](./tools)). A deployed orchestrator is reachable without a tunnel.
174+
175+
Default headless path on Daytona:
176+
177+
```ts
178+
import { chat } from '@tanstack/ai'
179+
import { grokBuildText } from '@tanstack/ai-grok-build'
180+
import {
181+
defineSandbox,
182+
defineSandboxPolicy,
183+
defineWorkspace,
184+
gitSkill,
185+
githubRepo,
186+
withSandbox,
187+
} from '@tanstack/ai-sandbox'
188+
import { daytonaSandbox } from '@tanstack/ai-sandbox-daytona'
189+
190+
const sandbox = defineSandbox({
191+
id: 'daytona-agent',
192+
provider: daytonaSandbox({
193+
apiKey: process.env.DAYTONA_API_KEY,
194+
snapshot: 'daytona-medium',
195+
}),
196+
workspace: defineWorkspace({
197+
source: githubRepo({ repo: 'owner/app' }),
198+
skills: [gitSkill({ repo: 'owner/skills-pack' })],
199+
}),
200+
policy: defineSandboxPolicy({
201+
default: 'allow',
202+
}),
203+
})
204+
205+
const stream = chat({
206+
adapter: grokBuildText('grok-build'),
207+
messages: [{ role: 'user', content: 'List the project files.' }],
208+
middleware: [withSandbox(sandbox)],
209+
})
210+
```
211+
212+
Headless Grok Build and Codex stay on auto-approve with `default: 'allow'`.
213+
Isolation is the Daytona VM. Use Claude Code when you need command-level deny.
154214

155215
## Vercel
156216

@@ -211,7 +271,7 @@ Providers declare what they support via `capabilities()`. The flags are:
211271
| `env` | Inject environment variables. |
212272
| `ports` | Expose/forward ports (preview URLs). |
213273
| `backgroundProcesses` | Keep long-running processes alive between calls. |
214-
| `writableStdin` | A spawned process exposes a writable host→process stdin. `true` for local-process and Docker; `false` on remote/edge providers (Daytona, Vercel, Cloudflare), where stdin-fed harnesses deliver the prompt via a file + shell redirection instead. |
274+
| `writableStdin` | A spawned process exposes a writable host→process stdin. `true` for local-process, Docker, and Daytona. `false` on Vercel and Cloudflare, where stdin-fed harnesses deliver the prompt via a file + shell redirection. |
215275
| `killableProcesses` | A spawned process can be forcibly stopped via `SpawnHandle.kill()` **and** aborted mid-flight via the `signal` passed to `spawn`. |
216276
| `snapshots` | Capture and restore point-in-time snapshots. |
217277
| `networkPolicy` | Enforce network allow/deny rules. |

docs/sandbox/provisioning.md

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,10 @@ defineWorkspace({
6464
})
6565
```
6666

67-
`secrets` is [declared on the workspace](./workspace), and the values are
68-
injected into the sandbox env at create/resume time.
67+
`secrets` is [declared on the workspace](./workspace). At create, resume, and
68+
snapshot restore, the sandbox layer resolves them onto the live handle env.
69+
Providers never write those values into snapshots, the sandbox store, or the
70+
event log.
6971

7072
### Why the values never leak
7173

@@ -77,7 +79,7 @@ non-enumerable:
7779
- `Object.keys(secrets)`, spreads, and `JSON.stringify(secrets)` never expose
7880
the values.
7981
- The values are **never written to snapshots, the sandbox store, or the event
80-
log**. They are resolved only at the moment the sandbox env is built.
82+
log**. They are resolved only when the live sandbox env is built.
8183

8284
This is what makes the workspace definition safe to hash, persist, and replay
8385
for resume bookkeeping without ever persisting a credential.
@@ -175,7 +177,12 @@ defineWorkspace({
175177

176178
`gitSkill` takes an optional `into` field, an **absolute path inside the
177179
sandbox**, controlling where the repo is cloned. It defaults to
178-
`.tanstack-skills/<repo-basename>`:
180+
`.tanstack-skills/<repo-basename>`. Bootstrap creates the parent directory
181+
before the clone.
182+
183+
Paths that start with `/workspace` use the portable workspace root. The
184+
provider maps that root onto its real workdir. Keep `/workspace/...` in
185+
`into` unless you have a reason to pin a provider-specific path.
179186

180187
```ts
181188
import { createSecrets, defineWorkspace, gitSkill, githubRepo } from '@tanstack/ai-sandbox'
@@ -206,6 +213,11 @@ format:
206213
| Codex | `.codex/config.toml` |
207214
| OpenCode | `opencode.json` |
208215

216+
Each projector walks the cloned repo for folders that contain `SKILL.md`. A
217+
nested pack (`skills/foo/SKILL.md`) is linked under the skill name `foo`. A
218+
flat clone with `SKILL.md` at the root uses the clone name. You do not write
219+
`ln -s` by hand.
220+
209221
A concept a given CLI lacks (for example, `plugins` on Codex) **emits a
210222
warning and is silently skipped** rather than throwing. The same applies to
211223
`agentSkill` on Claude Code: there is no reliable primitive to install a public

docs/sandbox/workspace.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,11 @@ subshell (like the Grok CLI installer, `(curl … || curl …) | bash`) is not a
170170
valid argument to another command, and `sh` then fails at parse time with
171171
`syntax error: unexpected "("` (exit `2`) without running anything.
172172

173+
Some providers run as a non-root user. Workspace-local installs such as
174+
`pnpm install` do not need sudo. System or global package installs on those
175+
providers need non-interactive `sudo -n` in `setup`. See
176+
[Providers](./providers) and [Policy](./policy).
177+
173178
## Scripts
174179

175180
`scripts` is a map of named commands the agent and user can invoke by name,

packages/ai-acp/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,8 @@
3838
"lint:fix": "oxlint src --type-aware --fix",
3939
"test:build": "publint --strict",
4040
"test:oxlint": "oxlint src --type-aware",
41-
"test:lib": "vitest",
42-
"test:lib:dev": "pnpm test:lib --watch",
41+
"test:lib": "vitest run --testTimeout=30000",
42+
"test:lib:dev": "vitest --watch",
4343
"test:types": "tsc"
4444
},
4545
"dependencies": {

0 commit comments

Comments
 (0)