Warum das Schreiben einer großen Datei in eine Sandbox immer wieder scheitert

Ein großer Write läuft auf Ihrem Laptop problemlos durch. Derselbe Write gegen eine Modal-Sandbox kommt mit ValueError: embedded null byte zurück. Bei einem anderen Backend ist der Fehler seltsamer: write_file meldet Erfolg, und die Datei auf der Platte hat null Bytes. Sie geben dem Netzwerk, den Rechten oder einem veralteten Cache die Schuld — und der wahre Grund ist zum Verrücktwerden banal: Die gesamte Payload wurde der Shell als Kommandozeilen-Argument übergeben, und Kommandozeilen-Argumente haben eine Längengrenze und sind für jeden Prozess auf der Maschine sichtbar. Ein am 2026-09-26 gemergter Fix (PR #124206) beendet das.
Die Ursache: stdin-Daten in argv
cat file | bash -c '...' ist normale Shell-Arbeit, aber die Sandbox-Backends nahmen eine Abkürzung: Die Payload, die für das stdin eines Kommandos bestimmt war, wurde direkt in das bash -c-Argument hineinkonkateniert (der Code nennt das den heredoc-Pfad).
Das funktioniert gut, bis es auf zwei harte Grenzen trifft:
| Grenze | Zahl | Was passiert |
|---|---|---|
| NUL-Bytes | Ein Argument darf kein \0 enthalten |
ValueError: embedded null byte, der Write scheitert komplett |
| Argumentlänge | Linux MAX_ARG_STRLEN liegt bei etwa 128 KiB; Modal ist enger (~64 KiB Exec-Arg-Limit) |
Größere Writes scheitern auf Linux oder Modal komplett |
Die Reproduktion im PR ist unverblümt: Eine 160 KiB-Payload mit einem NUL wirft auf main embedded null byte, und das aufgebaute argv-Element ist 165003 Bytes lang. Lesen Sie den zweiten Teil noch einmal — selbst ohne das NUL liegt diese Größe schon über der Grenze.
Es gibt einen zweiten Effekt, der leicht zu übersehen ist: argv ist öffentlich. Jeder Prozess auf derselben Maschine kann die Kommandozeile eines anderen Prozesses lesen. Dateiinhalte — die Keys, Token oder Nutzerdaten enthalten können — in argv zu konkatenieren parkt sie still in der Prozesstabelle.
Der Fix: Jedes Backend erklärt, wie es stdin übergibt
Der Patch führt eine explizite Modus-Deklaration ein (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"
Über die drei eingebauten Backends hinweg:
| Backend | Was es jetzt tut |
|---|---|
| Modal | _stdin_mode = "payload"; stdin wird durch das SDK gestreamt, byte-exakt (surrogateescape), in Chunks (je 1 MiB, unter dem 2-MB-Limit des Legacy-Servers) |
| Daytona | Die Payload wird in eine private Datei mit 0600 hochgeladen und dem Kommando dann als exec 0< f; rm -f -- f; cmd zugeführt |
| Vercel | Gleiche Form: Die Bytes landen unter einem privaten Pfad, die Rechte werden gesetzt, stdin wird daraus umgeleitet |
Daytonas Version ist dabei noch etwas sauberer: Die Bytes gehen direkt durch das SDK, ohne Host-Temp-Datei, und leeres stdin überspringt das Staging komplett.
Sehen Sie sich das Redirect-Präfix genau an — zwei Aktionen in bewusst gewählter Reihenfolge:
exec 0< <staged-path> || exit $? # the shell takes over the descriptor
rm -f -- <staged-path> || exit $? # then the file is unlinked
<command>
exec vor rm bedeutet: Die Datei ist für die gesamte Laufzeit des Kommandos von der Platte verschwunden, während die Daten über den bereits geöffneten Descriptor lesbar bleiben. Beide Schritte wurden zu geteilten Helfern auf BaseEnvironment (_staged_stdin_path, _redirect_stdin_from_file); der alte Pfad _embed_stdin_heredoc überlebt nur noch für Plugin-Backends.
Auch die Cancel-Logik wurde neu geschrieben
Die ursprüngliche Bereinigung hatte zwei echte Defekte: Jeder SDK-Fehler stoppte die gesamte Sandbox, und jedes Cancel stoppte sie zweimal. Der Patch teilt den Zustand in drei (cancelled / staged / dispatched) und hält ihn unter dem Environment-Lock:
- Ein Cancel, der nach dem Upload, aber vor dem Dispatch eintrifft, räumt die gestagte Datei weg (Vercel überschreibt, Daytona löscht) und stoppt dann einmal;
- ein gewöhnlicher SDK-Fehler stoppt die Sandbox nicht mehr;
- ein abgebrochenes Kommando wird nie dispatcht.
Was das für Sie bedeutet
- Keine Konfigurationsänderungen. Jedes Backend hat seinen Transport getauscht; Aufrufer merken nichts.
- Fälle, die früher still oder laut scheiterten — Inhalte mit NUL-Bytes, Writes über etwa 128 KiB — landen jetzt byte-exakt.
- Wenn Sie ein Sandbox-Backend-Plugin pflegen:
_embed_stdin_heredocgibt es noch, aber neue Backends sollten aus den Gründen in dieser Tabelle den Moduspayloaddeklarieren. - Die drei PRs wurden am 2026-09-26 gemergt, nach dem Release-Tag v0.21.5, sie liegen also auf
main.hermes updateholt sie.
Das größere Muster
Diese Arbeit hat ein früheres Kapitel: Die PRs #122012 / #122011 behoben ein Heredoc, das an das falsche Kommando gebunden war — die andere Seite des Problems byte-exakter Writes: eine Datei, die geschrieben aussieht und die falschen Bytes enthält. Der argv-Fix hier deckt „sie wurde überhaupt nie geschrieben“ plus „ein anderer Prozess konnte sie lesen“ ab. Zusammen ergeben sie einen Punkt: Wenn Sie Daten in eine entfernte Umgebung schicken, ist die Kommandozeile der letzte Ort, an den sie gehören.
Wenn Sie sich auf Remote-Sandboxes stützen, lesen Sie weiter: wie Mediendateien aus einer Sandbox herauskommen, was ein geteilter Docker-Container für parallele Agenten bedeutet und den Rest dieses Fensters auf der Release-Seite v0.21.5.