Por que escrever um arquivo grande dentro de um sandbox vive falhando

Uma escrita grande passa sem problemas no seu laptop. A mesma escrita contra um sandbox Modal volta com ValueError: embedded null byte. Contra outro backend o erro é mais estranho: o write_file reporta sucesso e o arquivo no disco tem zero bytes. Você culpa a rede, as permissões, um cache velho — e o motivo real é irritantemente banal: o payload inteiro era passado ao shell como argumento de linha de comando, e argumentos de linha de comando têm limite de tamanho e são visíveis para todo processo da máquina. Uma correção mesclada em 2026-09-26 (PR #124206) acaba com isso.
A causa raiz: dados de stdin no argv
cat file | bash -c '...' é trabalho normal de shell, mas os backends de sandbox pegaram um atalho: o payload destinado ao stdin de um comando era concatenado no próprio argumento do bash -c (o código chama isso de caminho heredoc).
Isso funciona bem até esbarrar em dois limites rígidos:
| Limite | Número | O que acontece |
|---|---|---|
| Bytes NUL | Um argumento não pode conter \0 |
ValueError: embedded null byte, a escrita falha de imediato |
| Comprimento do argumento | O MAX_ARG_STRLEN do Linux é cerca de 128 KiB; o Modal é mais apertado (limite de ~64 KiB para exec-arg) |
Escritas maiores falham de imediato no Linux ou no Modal |
A reprodução no PR é direta: um payload de 160 KiB contendo um NUL levanta embedded null byte na main, e o elemento argv que ele montou tem 165.003 bytes de comprimento. Leia a segunda parte de novo — mesmo sem o NUL, esse tamanho já passou da linha.
Há um segundo efeito fácil de passar batido: o argv é público. Qualquer processo na mesma máquina pode ler a linha de comando de outro processo. Concatenar conteúdo de arquivos — que pode conter chaves, tokens ou dados de usuário — no argv os estaciona silenciosamente na tabela de processos.
A correção: cada backend declara como entrega o stdin
O patch introduz uma declaração explícita de modo (tools/environments/base.py):
# Como execute() entrega o stdin ao _run_bash: "pipe" (stdin do processo, padrão),
# "payload" (repassado como stdin_data; o backend o transporta via seu
# SDK -- Modal/Modal gerenciado faz stream, Daytona/Vercel prepara um arquivo) ou
# "heredoc" (embutido no comando; nenhum backend embutido, só plugins).
_stdin_mode: str = "pipe" # "pipe" | "payload" | "heredoc"
Nos três backends embutidos:
| Backend | O que ele faz agora |
|---|---|
| Modal | _stdin_mode = "payload"; o stdin é transmitido em stream pelo SDK, byte a byte exato (surrogateescape), em blocos (1 MiB cada, ficando abaixo do limite de 2 MB do servidor legado) |
| Daytona | O payload é enviado para um arquivo privado 0600 e depois alimenta o comando como exec 0< f; rm -f -- f; cmd |
| Vercel | Mesma forma: os bytes chegam a um caminho privado, as permissões são definidas, e o stdin é redirecionado a partir dele |
A versão do Daytona é ainda um pouco mais limpa: os bytes vão direto pelo SDK, sem arquivo temporário no host, e um stdin vazio pula a etapa de preparação por completo.
Olhe com atenção o prefixo de redirecionamento — duas ações, numa ordem deliberada:
exec 0< <staged-path> || exit $? # o shell assume o descritor
rm -f -- <staged-path> || exit $? # depois o arquivo é removido (unlink)
<command>
exec antes de rm significa que o arquivo some do disco durante toda a duração do comando, enquanto os dados continuam legíveis pelo descritor já aberto. As duas etapas viraram helpers compartilhados em BaseEnvironment (_staged_stdin_path, _redirect_stdin_from_file); o antigo caminho _embed_stdin_heredoc sobrevive apenas para backends de plugin.
A lógica de cancelamento também foi reescrita
A limpeza original tinha dois defeitos reais: qualquer erro de SDK parava o sandbox inteiro, e todo cancelamento o parava duas vezes. O patch divide o estado em três (cancelled / staged / dispatched) e o mantém sob o lock de ambiente:
- um cancelamento que chega depois do upload mas antes do dispatch limpa o arquivo preparado (no Vercel sobrescreve, no Daytona apaga) e então para uma vez só;
- um erro comum de SDK não para mais o sandbox;
- um comando cancelado nunca é despachado.
O que isso significa para você
- Nenhuma mudança de configuração. Cada backend trocou seu transporte; os chamadores não veem nada.
- Casos que antes falhavam em silêncio ou aos berros — conteúdo com bytes NUL, escritas acima de mais ou menos 128 KiB — agora chegam byte a byte exatos.
- Se você mantém um plugin de backend de sandbox,
_embed_stdin_heredocainda está lá, mas backends novos devem declarar o modopayloadpelos motivos daquela tabela. - Os três PRs foram mesclados em 2026-09-26, depois da tag da release v0.21.5, então eles estão na
main. Ohermes updateos traz.
O padrão maior
Este trabalho tem um capítulo anterior: os PRs #122012 / #122011 corrigiram um heredoc amarrado ao comando errado, que é a outra face do problema da escrita byte a byte exata — um arquivo que parece escrito e contém os bytes errados. A correção de argv aqui cobre “nunca foi escrito de fato” mais “outro processo podia ler aquilo”. Juntas, elas apontam para um único ponto: quando você envia dados para um ambiente remoto, a linha de comando é o último lugar onde eles deveriam estar.
Se você depende de sandboxes remotos, siga lendo: como arquivos de mídia saem de um sandbox, o que um container Docker compartilhado significa para agentes em paralelo e o resto desta janela na página da release v0.21.5.