Skip to content

Daytona + Grok/Codex: nine edges that stop headless sandboxes from just working #1081

Description

@AlemTuzlak

Headless Grok Build + Codex on Daytona does not work from the public APIs and README examples. An app has to discover and paper over nine edges.

Found while wiring OpenTag (promo video = Grok Build grok-4.5 in Daytona; docs-PR / Linear fix / Linear triage = Codex in Daytona). Packages at the time: @tanstack/ai-sandbox 0.3.0, @tanstack/ai-sandbox-daytona 0.2.1, @tanstack/ai-grok-build 0.3.1, @tanstack/ai-codex.

Goal: a default defineSandbox + gitSkill + grokBuildText / codexText + withSandbox run works on Daytona without empty commands.deny, a provider create wrapper, hardcoded /home/daytona/workspace, or hand-written ln -s for nested skills.

Items 1–5 are the ones that made Grok produce no files until we found them. Items 6–9 are the ones every app still copies as the same 20 lines.


1. Any commands.deny kills headless Grok and Codex

Silent failure. Highest priority.

mapPolicyToGrokBuildFlags sets conservative: true when commands.deny or commands.ask is non-empty. The NDJSON path then drops --always-approve and uses --permission-mode default. Headless grok -p auto-cancels every tool (User cancelled the execution) and exits with no files.

mapPolicyToCodexFlags maps the same deny/ask to approval_policy: on-request. Headless codex exec then refuses tools.

The public sandbox README shows this as the example:

deny: ['sudo *','rm -rf *']

That example is enough to break both harnesses.

Where

  • packages/ai-grok-build/src/adapters/policy-map.ts (hasAskOrDenyconservative)
  • packages/ai-grok-build/src/adapters/text.ts (alwaysApprove = !readOnly && !conservative)
  • packages/ai-codex/src/adapters/policy-map.ts (hasAskOrDenyon-request)
  • packages/ai-sandbox/README.md (deny example)
  • Tests encode the current trap: packages/ai-grok-build/tests/policy-map.test.ts (deny: ['rm *'] → conservative)

Fix

Do not treat deny as “ask a human.” Map deny onto the harness deny list (or ignore it for the approval flag when default: 'allow'). Only flip conservative / on-request when default is deny/ask, or when commands.ask is set.

Update the README example so a deny list is legal on a headless run.


2. gitSkill clone does not create .tanstack-skills

Bootstrap clones to <root>/.tanstack-skills/<basename>. git clone does not create parent dirs. The clone fails.

Where

  • packages/ai-sandbox/src/bootstrap.ts (handle.git.clone({ url, dir }))
  • packages/ai-sandbox/src/git-exec.ts (clone runs git clone … <target> with no mkdir -p)
  • packages/ai-sandbox/src/agents-file.ts (resolveGitSkillDir)

Daytona create only mkdir -ps the workdir, not .tanstack-skills.

Fix

mkdir -p the parent in bootstrapWorkspace or in createExecBackedGit before clone. Every provider hits this, not only Daytona.


3. Skill projector links the repo basename, not each SKILL.md

gitSkill({ repo: 'AlemTuzlak/skills' }) clones the whole repo. The Grok projector then does:

.tanstack-skills/skills → .grok/skills/skills

Grok only loads a skill when .grok/skills/<name>/SKILL.md exists. Nested packs never load. Same basename-only link on Claude and Codex projectors.

Where

  • packages/ai-grok-build/src/adapters/projection.ts (projectGitSkillstarget = skillsDir/basenameOf(source))
  • packages/ai-claude-code/src/adapters/projection.ts
  • packages/ai-codex/src/adapters/projection.ts
  • packages/ai-acp/src/adapters/projection.ts

Fix

Walk the clone for SKILL.md and link each skill by its folder name. That is what npx skills add already does.


4. Virtual /workspace is not remapped inside shell strings

Daytona remaps cwd and fs paths. It does not rewrite paths inside git clone … /workspace/.tanstack-skills/foo.

Default workspace.root is /workspace (DEFAULT_WORKSPACE_ROOT). A default Daytona + gitSkill clone lands in the wrong place, or fails.

Codex already knows this trap: it refuses to pass --cd /workspace for the same reason (packages/ai-codex/src/adapters/text.ts).

Where

  • packages/ai-sandbox-daytona/src/handle.ts (abs() only on cwd / fs, not on git dir)
  • packages/ai-sandbox/src/bootstrap.ts (clones against workspace.root ?? '/workspace')
  • packages/ai-sandbox/src/git-exec.ts (passes dir straight into the shell)

Fix (pick one)

  • Daytona git.clone remaps /workspace/… the same way fs does
  • or bootstrap clones against handle.workspaceRoot, not workspace.root ?? '/workspace'

Then apps can keep the virtual root and it still works.


5. Grok default protocol is ACP, and ACP does not journal

Default is 'acp'. ACP has no NDJSON journal. withSandbox durability then warns, and a host restart cannot resume. An ATTACH on the ACP path throws DurableAttachNotSupportedError.

Where

  • packages/ai-grok-build/src/adapters/text.ts (protocol ?? 'acp', chatStreamAcpdurable: false)
  • packages/ai-grok-build/src/provider-options.ts

Fix

If durability is wired, use streaming-json (or journal ACP). Do not default a durable sandbox run onto a path that cannot recover.


6. --always-approve still opens Plan Mode

Grok still hits Plan Mode and the CLI update check. Those are not covered by the policy flag.

Fix

On the headless NDJSON path, when policy is permissive, add --no-plan and --no-auto-update. Apps should not need to know Grok CLI flags to run unattended.


7. Daytona advertises snapshots: false

Daytona can snapshot. The adapter sets snapshots: false, so lifecycle.snapshot: 'after-setup' is ignored (caps.snapshots ? 'after-setup' : 'none').

Every new box re-runs apt, Grok install, ffmpeg, and skill clones. A failed box is also unsafe to reuse, because setup runs only on create.

Where

  • packages/ai-sandbox-daytona/src/handle.ts (DAYTONA_CAPS.snapshots: false)
  • packages/ai-sandbox/src/sandbox.ts (effective snapshot strategy)

Fix

Implement snapshot/restore on the Daytona adapter, then honor after-setup.


8. Codex default sandbox is nested bubblewrap

Default sandboxMode is workspace-write. Inside Daytona that is:

bwrap: No permissions to create a new namespace

Where

  • packages/ai-codex/src/adapters/text.ts (default 'workspace-write')

Fix

Detect the nested-bwrap failure and fall back, or default to danger-full-access when the provider has no user namespaces. Isolation is then the Daytona VM + defineSandboxPolicy. The ts-react-chat / Cloudflare examples already do this by hand.


9. Daytona user is not root

Bare apt-get fails with Acquire (13: Permission denied) on /var/lib/apt. The user daytona has passwordless sudo.

Fix (any of)

  • Document that setup must use sudo -n and must not deny sudo *
  • Ship a privileged() / sudoSerial() helper for setup commands
  • Or recommend / ship a snapshot that already has git, gh, ffmpeg, and the Grok CLI

GROK_CLI_INSTALL_COMMAND already falls back to GCS when Daytona blocks x.ai. That part is correct. Keep it.


Suggested order

  1. Item 1 — deny-list must not flip headless Grok/Codex into auto-cancel (plus README)
  2. Item 2 — mkdir -p before gitSkill clone
  3. Item 3 — project nested SKILL.md dirs by skill name
  4. Item 4 — remap /workspace in Daytona git.clone / bootstrap
  5. Item 5 — durable Grok runs must journal
  6. Item 6 — headless Grok --no-plan --no-auto-update
  7. Item 8 — Codex: no nested bwrap on Daytona-like providers
  8. Item 7 — Daytona snapshots: true + after-setup
  9. Item 9 — sudo / snapshot / docs

Out of scope (not library bugs)

  • killableProcesses: false on Daytona is honest. kill() only stops the client poll. Cancel = destroy.
  • daytona-medium missing Grok CLI / ffmpeg is snapshot content, not an adapter bug.

Acceptance

A new app can do all of the following on Daytona and get a working headless run:

  • daytonaSandbox({ apiKey, snapshot: 'daytona-medium' }) with no workdir
  • defineWorkspace({ skills: [gitSkill({ repo: 'owner/skills-pack' })] }) with default root (/workspace)
  • defineSandboxPolicy({ default: 'allow', commands: { deny: ['rm -rf /'] } })
  • grokBuildText(model) with durability wired (no protocol / extraArgs required)
  • codexText(model) without sandboxMode: 'danger-full-access'

No provider create wrapper. No hand ln -s of nested skills. No empty deny list as a load-bearing hack.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions