Skip to content

Commit 5f2de12

Browse files
committed
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.
1 parent d6209d9 commit 5f2de12

13 files changed

Lines changed: 96 additions & 36 deletions

File tree

.changeset/daytona-headless-edges.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
'@tanstack/ai-sandbox': patch
3-
'@tanstack/ai-sandbox-daytona': patch
3+
'@tanstack/ai-sandbox-daytona': minor
44
'@tanstack/ai-grok-build': patch
55
'@tanstack/ai-codex': patch
66
'@tanstack/ai-acp': patch

docs/sandbox/providers.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,6 @@ const sandbox = defineSandbox({
199199
}),
200200
policy: defineSandboxPolicy({
201201
default: 'allow',
202-
commands: { deny: ['rm -rf /'] },
203202
}),
204203
})
205204

@@ -210,8 +209,8 @@ const stream = chat({
210209
})
211210
```
212211

213-
Grok Build and Codex do not enforce `commands.deny`. Isolation is the Daytona
214-
VM. Use Claude Code when you need command-level deny.
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.
215214

216215
## Vercel
217216

docs/sandbox/workspace.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -170,8 +170,10 @@ 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. Package installs on those providers
174-
need `sudo` in `setup`. See [Providers](./providers) and [Policy](./policy).
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).
175177

176178
## Scripts
177179

packages/ai-acp/src/adapters/projection.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
discoverSkillDirs,
2222
isSecretRef,
2323
resolveGitSkillDir,
24+
resolveHarnessCwd,
2425
} from '@tanstack/ai-sandbox'
2526
import type {
2627
BearerRef,
@@ -127,10 +128,17 @@ export async function projectAcpWorkspace(
127128
// so the shell command resolves on every provider.
128129
await handle.fs.mkdir(`${projection.root}/${skillsDir}`)
129130
for (const skill of gitSkills) {
130-
const source = skill.into ?? resolveGitSkillDir(projection.root, skill)
131+
// Match bootstrap's remapped clone path so discovery and shell copy
132+
// use the real provider workdir (e.g. Daytona).
133+
const source = resolveHarnessCwd(
134+
handle,
135+
skill.into ?? resolveGitSkillDir(projection.root, skill),
136+
)
131137
const discovered = await discoverSkillDirs(handle, source)
132138
for (const { name, dir } of discovered) {
133-
const relSource = relativeToRoot(projection.root, dir)
139+
const realDir = resolveHarnessCwd(handle, dir)
140+
const realRoot = resolveHarnessCwd(handle, projection.root)
141+
const relSource = relativeToRoot(realRoot, realDir)
134142
const relTarget = `${skillsDir}/${name}`
135143
const cp = await handle.process.exec(
136144
`cp -r ${shellQuote(relSource)} ${shellQuote(relTarget)}`,

packages/ai-claude-code/src/adapters/projection.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {
2828
discoverSkillDirs,
2929
isSecretRef,
3030
resolveGitSkillDir,
31+
resolveHarnessCwd,
3132
} from '@tanstack/ai-sandbox'
3233
import type {
3334
BearerRef,
@@ -133,14 +134,20 @@ async function projectGitSkills(
133134
await handle.fs.mkdir(skillsDir)
134135
madeDir = true
135136
}
136-
const source = skill.into ?? resolveGitSkillDir(projection.root, skill)
137+
// Remap `/workspace` the same way bootstrap remaps clone dirs, so shell
138+
// `ln`/`cp` targets match the real provider workdir (e.g. Daytona).
139+
const source = resolveHarnessCwd(
140+
handle,
141+
skill.into ?? resolveGitSkillDir(projection.root, skill),
142+
)
137143
const discovered = await discoverSkillDirs(handle, source)
138144
for (const { name, dir } of discovered) {
139-
const target = `${skillsDir}/${name}`
140-
const lnCmd = `ln -s ${shellQuote(dir)} ${shellQuote(target)}`
145+
const target = resolveHarnessCwd(handle, `${skillsDir}/${name}`)
146+
const realDir = resolveHarnessCwd(handle, dir)
147+
const lnCmd = `ln -s ${shellQuote(realDir)} ${shellQuote(target)}`
141148
const result = await handle.process.exec(lnCmd, { cwd: projection.root })
142149
if (result.exitCode !== 0) {
143-
const cpCmd = `cp -r ${shellQuote(dir)} ${shellQuote(target)}`
150+
const cpCmd = `cp -r ${shellQuote(realDir)} ${shellQuote(target)}`
144151
const copied = await handle.process.exec(cpCmd, {
145152
cwd: projection.root,
146153
})

packages/ai-codex/src/adapters/policy-map.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,10 @@
1313
* (`'allow'` → true, `'deny'` → false; unset leaves Codex's default).
1414
* - `approval_policy`: a fully-permissive policy (`default: 'allow'` with no
1515
* `ask` rules) → `never`; a `default: 'deny'` policy → `untrusted`;
16-
* anything with `ask` rules → `on-request`. A deny list alone is a hard
17-
* block, not a human prompt, so it does not flip `on-request`. In `exec`
18-
* mode Codex will refuse (rather than prompt for) actions that need
19-
* approval.
16+
* `default: 'ask'` or any `commands.ask` rules → `on-request`. A deny list
17+
* alone is a hard block, not a human prompt, so it does not flip
18+
* `on-request`. In `exec` mode Codex will refuse (rather than prompt for)
19+
* actions that need approval.
2020
*
2121
* Returns only the knobs the policy actually constrains; the adapter merges
2222
* these with its own config (config/modelOptions still take precedence).
@@ -46,7 +46,7 @@ export function mapPolicyToCodexFlags(
4646
}
4747

4848
const hasAsk = (policy.commands?.ask?.length ?? 0) > 0
49-
if (hasAsk) {
49+
if (hasAsk || policy.default === 'ask') {
5050
flags.approvalPolicy = 'on-request'
5151
} else if (policy.default === 'deny') {
5252
flags.approvalPolicy = 'untrusted'

packages/ai-codex/src/adapters/projection.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import {
3434
discoverSkillDirs,
3535
isSecretRef,
3636
resolveGitSkillDir,
37+
resolveHarnessCwd,
3738
} from '@tanstack/ai-sandbox'
3839
import type {
3940
BearerRef,
@@ -174,14 +175,20 @@ async function projectGitSkills(
174175
await handle.fs.mkdir(skillsDir)
175176
madeDir = true
176177
}
177-
const source = skill.into ?? resolveGitSkillDir(projection.root, skill)
178+
// Remap `/workspace` the same way bootstrap remaps clone dirs, so shell
179+
// `ln`/`cp` targets match the real provider workdir (e.g. Daytona).
180+
const source = resolveHarnessCwd(
181+
handle,
182+
skill.into ?? resolveGitSkillDir(projection.root, skill),
183+
)
178184
const discovered = await discoverSkillDirs(handle, source)
179185
for (const { name, dir } of discovered) {
180-
const target = `${skillsDir}/${name}`
181-
const lnCmd = `ln -s ${shellQuote(dir)} ${shellQuote(target)}`
186+
const target = resolveHarnessCwd(handle, `${skillsDir}/${name}`)
187+
const realDir = resolveHarnessCwd(handle, dir)
188+
const lnCmd = `ln -s ${shellQuote(realDir)} ${shellQuote(target)}`
182189
const result = await handle.process.exec(lnCmd, { cwd: projection.root })
183190
if (result.exitCode !== 0) {
184-
const cpCmd = `cp -r ${shellQuote(dir)} ${shellQuote(target)}`
191+
const cpCmd = `cp -r ${shellQuote(realDir)} ${shellQuote(target)}`
185192
const copied = await handle.process.exec(cpCmd, {
186193
cwd: projection.root,
187194
})

packages/ai-codex/tests/policy-map.test.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,13 @@ describe('mapPolicyToCodexFlags', () => {
4949
).toBe('on-request')
5050
})
5151

52+
it('maps default ask without ask command rules to on-request', () => {
53+
expect(
54+
mapPolicyToCodexFlags(defineSandboxPolicy({ default: 'ask' }))
55+
.approvalPolicy,
56+
).toBe('on-request')
57+
})
58+
5259
// Headless `codex exec` refuses tools when approval_policy is on-request.
5360
// A deny list is a hard block, not a human prompt, so default: allow +
5461
// deny stays never. Issue #1081 item 1.

packages/ai-grok-build/src/adapters/projection.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import {
3838
discoverSkillDirs,
3939
isSecretRef,
4040
resolveGitSkillDir,
41+
resolveHarnessCwd,
4142
} from '@tanstack/ai-sandbox'
4243
import type {
4344
BearerRef,
@@ -260,14 +261,20 @@ async function projectGitSkills(
260261
await handle.fs.mkdir(skillsDir)
261262
madeDir = true
262263
}
263-
const source = skill.into ?? resolveGitSkillDir(projection.root, skill)
264+
// Remap `/workspace` the same way bootstrap remaps clone dirs, so shell
265+
// `ln`/`cp` targets match the real provider workdir (e.g. Daytona).
266+
const source = resolveHarnessCwd(
267+
handle,
268+
skill.into ?? resolveGitSkillDir(projection.root, skill),
269+
)
264270
const discovered = await discoverSkillDirs(handle, source)
265271
for (const { name, dir } of discovered) {
266-
const target = `${skillsDir}/${name}`
267-
const lnCmd = `ln -s ${shellQuote(dir)} ${shellQuote(target)}`
272+
const target = resolveHarnessCwd(handle, `${skillsDir}/${name}`)
273+
const realDir = resolveHarnessCwd(handle, dir)
274+
const lnCmd = `ln -s ${shellQuote(realDir)} ${shellQuote(target)}`
268275
const result = await handle.process.exec(lnCmd, { cwd: projection.root })
269276
if (result.exitCode !== 0) {
270-
const cpCmd = `cp -r ${shellQuote(dir)} ${shellQuote(target)}`
277+
const cpCmd = `cp -r ${shellQuote(realDir)} ${shellQuote(target)}`
271278
const copied = await handle.process.exec(cpCmd, {
272279
cwd: projection.root,
273280
})

packages/ai-sandbox-daytona/src/handle.ts

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -344,12 +344,16 @@ export class DaytonaHandle implements SandboxHandle {
344344

345345
// Stream logs over the WebSocket form. Still poll command status so
346346
// `kill()` can stop the client wait without waiting for the stream.
347-
void this.sandbox.process.getSessionCommandLogs(
348-
sessionId,
349-
cmdId,
350-
(chunk) => stdoutQ.push(chunk),
351-
(chunk) => stderrQ.push(chunk),
352-
)
347+
// Keep the promise so we can drain trailing chunks and so a dropped
348+
// socket cannot become an unhandled rejection.
349+
const logStream = this.sandbox.process
350+
.getSessionCommandLogs(
351+
sessionId,
352+
cmdId,
353+
(chunk) => stdoutQ.push(chunk),
354+
(chunk) => stderrQ.push(chunk),
355+
)
356+
.catch(() => undefined)
353357

354358
const pump = (async (): Promise<void> => {
355359
try {
@@ -366,6 +370,8 @@ export class DaytonaHandle implements SandboxHandle {
366370
}
367371
} finally {
368372
opts?.signal?.removeEventListener('abort', onAbort)
373+
// Let the log stream finish so trailing output is not dropped.
374+
await Promise.race([logStream, sleep(1000)])
369375
stdoutQ.end()
370376
stderrQ.end()
371377
await this.sandbox.process.deleteSession(sessionId).catch(() => {})

0 commit comments

Comments
 (0)