Pourquoi écrire un gros fichier dans un sandbox échoue toujours


Une grosse écriture passe sans problème sur votre portable. La même écriture contre un sandbox Modal revient avec ValueError: embedded null byte. Contre un autre backend, l’erreur est encore plus étrange : write_file annonce un succès et le fichier sur disque fait zéro octet. Vous accusez le réseau, les permissions, un cache périmé — alors que la vraie raison est d’une banalité exaspérante : tout le payload était passé au shell comme argument de ligne de commande, et un argument de ligne de commande a une limite de longueur et reste visible de tous les processus de la machine. Un correctif fusionné le 2026-09-26 (PR #124206) met fin à cela.

La cause racine : des données stdin dans argv

cat file | bash -c '...' est du travail de shell classique, mais les backends sandbox ont pris un raccourci : le payload destiné au stdin d’une commande était concaténé directement dans l’argument de bash -c (le code appelle cela le chemin heredoc).

Ça fonctionne très bien jusqu’à buter sur deux limites dures :

Limite Valeur Ce qui se passe
Octets NUL Un argument ne peut pas contenir \0 ValueError: embedded null byte, l’écriture échoue purement et simplement
Longueur d’argument Le MAX_ARG_STRLEN de Linux est d’environ 128 KiB ; Modal est plus strict (~64 KiB de limite d’argument exec) Les écritures plus grosses échouent purement et simplement sur Linux ou Modal

La reproduction dans la PR est sans détour : un payload de 160 KiB contenant un NUL lève embedded null byte sur main, et l’élément argv qu’il construit fait 165003 octets de long. Relisez la seconde moitié — même sans le NUL, cette taille est déjà au-delà de la limite.

Il y a un second effet facile à manquer : argv est public. N’importe quel processus de la même machine peut lire la ligne de commande d’un autre processus. Concaténer le contenu d’un fichier — qui peut contenir des clés, des tokens ou des données utilisateur — dans argv les gare discrètement dans la table des processus.

Le correctif : chaque backend déclare comment il transmet stdin

Le patch introduit une déclaration de mode explicite (tools/environments/base.py) :

# How execute() hands stdin to _run_bash: "pipe" (process stdin, default),
# "payload" (passed through as stdin_data; the backend transports it via its
# SDK -- Modal/managed Modal stream it, Daytona/Vercel stage a file) or
# "heredoc" (embedded in the command; no built-in backend, plugins only).
_stdin_mode: str = "pipe"  # "pipe" | "payload" | "heredoc"

Dans les trois backends intégrés :

Backend Ce qu’il fait désormais
Modal _stdin_mode = "payload" ; stdin est streamé via le SDK, exact à l’octet (surrogateescape), par morceaux (1 MiB chacun, en restant sous le plafond de 2 MB de l’ancien serveur)
Daytona Le payload est téléversé dans un fichier privé en 0600, puis transmis à la commande via exec 0< f; rm -f -- f; cmd
Vercel Même forme : les octets atterrissent dans un chemin privé, les permissions sont posées, stdin y est redirigé

La version de Daytona est encore un peu plus propre : les octets passent directement par le SDK, sans fichier temporaire sur l’hôte, et un stdin vide saute entièrement l’étape de mise en attente.

Regardez de près le préfixe de redirection — deux actions, dans un ordre délibéré :

exec 0< <staged-path> || exit $?      # the shell takes over the descriptor
rm -f -- <staged-path> || exit $?     # then the file is unlinked
<command>

exec avant rm signifie que le fichier a disparu du disque pendant toute la durée de la commande, tandis que les données restent lisibles via le descripteur déjà ouvert. Les deux étapes sont devenues des helpers partagés sur BaseEnvironment (_staged_stdin_path, _redirect_stdin_from_file) ; l’ancien chemin _embed_stdin_heredoc ne survit que pour les backends en plugin.

La logique d’annulation a aussi été réécrite

Le nettoyage d’origine avait deux vrais défauts : n’importe quelle erreur du SDK arrêtait tout le sandbox, et chaque annulation l’arrêtait deux fois. Le patch découpe l’état en trois (annulé / mis en attente / envoyé) et le protège sous le verrou de l’environnement :

  • une annulation qui arrive après le téléversement mais avant l’envoi nettoie le fichier mis en attente (Vercel l’écrase, Daytona le supprime) puis arrête une seule fois ;
  • une erreur SDK ordinaire n’arrête plus le sandbox ;
  • une commande annulée n’est jamais envoyée.

Ce que cela change pour vous

  • Aucun changement de configuration. Chaque backend a changé son transport ; les appelants ne voient rien.
  • Les cas qui échouaient silencieusement ou bruyamment — du contenu avec des octets NUL, des écritures au-delà d’environ 128 KiB — passent désormais exacts à l’octet.
  • Si vous maintenez un plugin de backend sandbox, _embed_stdin_heredoc est toujours là, mais les nouveaux backends devraient déclarer le mode payload pour les raisons du tableau ci-dessus.
  • Les trois PR ont été fusionnées le 2026-09-26, après le tag de release v0.21.5, donc elles sont sur main. hermes update les récupère.

Le schéma plus large

Ce travail a un chapitre antérieur : les PR #122012 / #122011 ont corrigé un heredoc lié à la mauvaise commande, qui est l’autre face du problème de l’écriture exacte à l’octet — un fichier qui semble écrit et contient les mauvais octets. Le correctif argv traité ici couvre le cas le fichier n’a jamais été écrit du tout plus un autre processus pouvait le lire. Ensemble, ils martèlent un seul point : quand vous expédiez des données vers un environnement distant, la ligne de commande est le dernier endroit où elles devraient atterrir.

Si vous vous appuyez sur des sandboxes distants, poursuivez la lecture : comment les fichiers média sortent d’un sandbox, ce qu’un conteneur Docker partagé implique pour les agents parallèles, et le reste de cette fenêtre sur la page de release v0.21.5.