Der Sandbox¶
Ein Container, in dem ein Agent Dateien schreiben und Befehle ausführen kann.
So liest ein Run die Tabelle, die jemand angehängt hat, zeichnet etwas, klont ein Repository oder führt Notizen zwischen Nachrichten.
Dies ist der eine Teil der Plattform, der Code ausführt, den niemand hier geschrieben hat
Deshalb verwendet diese Seite ebenso viel Zeit darauf, was einen Sandbox einschließt, wie darauf, was in einem ist.
Diese Seite ist das ganze Bild: was wo läuft, was eine Session ist, welche Umgebungen ein Agent anfordern darf und wie man sie ändert, was eine Organisation von einer anderen isoliert, und wie lange davon etwas überlebt.
Konfiguration ist die Referenz Variable für Variable.
Was wo läuft¶
Drei Prozesse, und die Anordnung ist das Sicherheitsmodell:
flowchart LR
B["app - the API<br/><i>no docker.sock</i>"] -->|HTTP + SANDBOXD_TOKEN| S["sandboxd<br/><i>holds /var/run/docker.sock</i>"]
S -->|start a container| D[["the host's Docker daemon"]]
D --> C["a session's container<br/><i>a sibling of sandboxd, not a child</i>"]
- Der API-Container hält keinen Docker-Socket. Das ist der ganze Grund, weshalb
sandboxdein eigener Service ist und kein Bibliotheksaufruf: einen Container zu starten erfordert den Daemon, und den Daemon zu erreichen ist gleichbedeutend mit root auf dem Host. sandboxdhält den Socket und bittet den Daemon des Hosts, einen Container zu starten. Ein Sandbox ist also ein Geschwister vonsandboxdund kein Container darin. Es gibt hier kein Docker-in-Docker, nichts ist--privileged, und es läuft kein verschachtelter Daemon.- Deshalb wird das Workspace-Verzeichnis auf beiden Seiten unter demselben Pfad
eingehängt:
sandboxdlegt es an und bittet dann den Daemon, es zu mounten, und der Daemon löst diesen Pfad auf dem Host auf. Ein benanntes Volume, oder ein Pfad, der nur innerhalb dessandboxd-Containers existiert, wird mitmounts deniedabgelehnt.
sandboxd ist der Server aus pydantic-ai-backend,
ausgeliefert als ghcr.io/vstorm-co/sandboxd; das Backend spricht über HTTP mit
ihm, mit dem Token in SANDBOXD_TOKEN. Die beiden anderen Sandbox-Backends, die
ein Spec benennen kann — daytona und state — sind ein gehosteter Dienst und ein
Dokument in Postgres, und keines von beiden hat mit dem oben Genannten zu tun.
Dienst und Backend ändern sich gemeinsam¶
Der Dienst und der Client des Backends stammen aus demselben Release von
pydantic-ai-backend, und ein Client ruft Routen auf, die nur sein eigenes
Release bedient. Deshalb pinnt jede Compose-Datei das Image auf genau die Version
in backend/uv.lock, und ein Backend-Test schlägt fehl, wenn die Compose-Dateien
oder das Lizenzinventar eine andere nennen. Heben Sie Lock, Compose-Dateien und
Inventar in einer Änderung an. Ein neuer Pin ist ein Tag, den der Host noch nicht
hat, also ziehen make dev und scripts/deploy.sh ihn beim nächsten Start.
Der Pin steht im Checkout, nicht in AGENTICOS_VERSION. Ein Host, der das Backend
von Hand pinnt oder zurückrollt, checkt deshalb auch den Tag dieses Releases aus
(Ein Release pinnen). Eine Installation ohne Checkout, wie
scripts/quickstart.sh sie einrichtet, lädt stattdessen die docker-compose.yml
dieses Releases herunter (Installation).
Ein beweglicher Tag hat das schon einmal kaputt gemacht (#2484). Ein
zwischengespeichertes sandboxd:0.2 bediente eine ältere API, als der Client
sprach, und jeder Befehl scheiterte mit sandboxd session '…' is gone, obwohl die
Sitzung lebte. Die Plattform unterscheidet die beiden Fälle jetzt. Trägt eine 404
das nackte {"detail":"Not Found"} von FastAPI statt des dienst-eigenen
No such session, liest die Plattform den Index des Dienstes unter /. Fehlt
im Index eine Route, die der Client braucht, schlägt der Tool-Aufruf fehl, und
der Agent erfährt:
The sandbox service does not serve /sessions/{session_id}/run,
/sessions/{session_id}/runs/{run_id}/stop - its version does not match this
platform (expected <version>). Run ghcr.io/vstorm-co/sandboxd:<version> and
redeploy it
Dieselbe Prüfung behandelt einen 409 beim Verbinden einer vor dem Upgrade
gespeicherten Sitzungsreferenz: Ein alter Dienst kann sie trotz bestehender Sitzung
ablehnen. Nur ein bestätigter fehlender Endpunkt macht daraus einen Versionsfehler;
normale Konflikte und nicht erreichbare Indizes behalten ihren ursprünglichen Fehler.
Die Meldung lässt die Adresse des Dienstes weg, die Mitglieder und das Modell
womöglich nicht sehen dürfen; das Backend protokolliert sie als
sandboxd_version_mismatch. Die Sitzung wird nicht als verloren gemeldet, aber
die Unterhaltung vergisst ihre gespeicherte Referenz darauf: Ein älterer Dienst
verweigert das Anhängen an eine lebende Sitzung per Referenz. Der nächste Zug
öffnet die Sitzung über ihren Namen, also findet die Unterhaltung nach dem
Upgrade dieselbe Sitzung wieder, wenn der Dienst sie behalten hat.
agenticos cmd doctor macht dieselbe Prüfung für jede registrierte Verbindung und
nennt das Image, das laufen muss. Deployen Sie den Stack, der den Dienst
betreibt, neu: scripts/deploy.sh in Produktion (es startet das Profil sandbox,
wenn SANDBOXD_TOKEN gesetzt ist), make dev auf dem Laptop. make prod lässt
den Dienst in Ruhe, weil es dieses Profil nicht startet; auf einem so gestarteten
Stack ziehen und erstellen Sie ihn selbst neu:
docker compose --env-file backend/.env -f docker-compose-prod.yml --profile sandbox pull sandboxd
docker compose --env-file backend/.env -f docker-compose-prod.yml --profile sandbox up -d sandboxd
Der Token ist root-äquivalent
Wer ihn hält, kann auf diesem Host Container starten. Behandeln Sie ihn wie den Docker-Socket, vor dem er sitzt.
make sandbox-token erzeugt einmalig einen in backend/.env und lässt ihn danach
in Ruhe, denn ihn neu zu erzeugen verwaist jeden Workspace, den der Service gerade
hält. Auch deshalb ist das eigene Dashboard des Services aus
(SANDBOXD_UI_ENABLED: 0): diese Seite bittet einen Menschen, den Token in einen
Browser einzufügen.
Eine Session, und was sich eine teilt¶
Ein Container pro Session, niemals einer für alle
Was der Schlüssel einer Session zusammenfasst — Scope, Organisation, Backend-Art und Host — ist genau das, was entscheidet, welche Runs sich einen Container und ein Verzeichnis teilen.
Eine Session wird über einen Schlüssel identifiziert, den das Backend ableitet, und dieser Schlüssel entscheidet, was geteilt wird:
xc-4f2a91c8-7b3e5d10-9c1f… backend · scope · organization · host · subject
^^ `x` a container service, `d` a document; `c` the conversation scope
Der Scope ist ein Feld des Specs des Agents — run, conversation, channel,
user oder agent. conversation, die übliche Wahl, bedeutet also einen Container
und ein Verzeichnis pro Chat; agent bedeutet, dass sich jeder Run dieses Agents
einen teilt.
user gehört der angemeldeten Person, also hat ein Run, der für jemanden
einspringt, keinen: Der Besucher eines Widgets und das Auslösen eines Triggers
werden abgewiesen, statt den Workspace des Herausgebers oder des Erstellers zu
bekommen — es sei denn, der Trigger
läuft als sein Ersteller.
Der Schlüssel ist auch der Name der Session auf dem Host — die Session-ID bei
sandboxd, der Name einer Daytona-Sandbox —, also öffnet jeder Run eines Scopes
sie über diesen Namen: Der erste legt sie an, die anderen hängen sich an,
gleichzeitige eingeschlossen, und nichts muss erst die eigene ID eines Anbieters
speichern, bevor der zweite Run sie finden kann.
Ist eine Session einmal geöffnet, hängt sich ein späterer Run nur noch an sie an.
Die Workspace-Zeile hält sie fest, wenn der Run endet, der sie geöffnet hat, und der
nächste Run fragt den Host nach dieser Session statt nach dem Namen. Eine Session,
deren Dateien gelöscht wurden — durch SANDBOXD_WORKSPACE_TTL entfernt, ein Host neu
aufgesetzt —, wird dem Agenten dann als verloren gemeldet, statt stillschweigend durch
eine leere ersetzt zu werden, in der er weiterarbeiten würde. Die Zeile vergisst sie am
Ende dieses Runs, sodass der nächste Turn eine frische Session beginnt.
Ebenfalls in den Schlüssel eingefaltet: welche Backend-Art und welcher Host
den Workspace trägt. Ein state-Dokument und das Volume eines Containers sind nicht
dieselbe Sache unter verschiedenen Namen, und zwei sandboxd-Installationen sind es
ebenso wenig — einen zweiten Host zu registrieren und ihn als Voreinstellung der
Organisation zu markieren verschob früher jeden bestehenden Workspace, ohne dass
jemand ein Spec bearbeitet hätte.
Was einen Tenant vom anderen trennt:
- ein eigener Container und ein eigenes Host-Verzeichnis pro Session;
- Session-Schlüssel sind von
uuid4abgeleitet und daher nicht erratbar — das lesbare Organisationspräfix dient dem Lesen eines Dashboards, es ist nicht die Grenze; - die Organisationsprüfung auf jeder Zeile, die diese Schlüssel erzeugt, was die Grenze tatsächlich ist;
tenant(die Id der Organisation), gesendet beim Öffnen der Session, was der Service gegenSANDBOXD_MAX_SESSIONS_PER_TENANT(10) innerhalb eines Pools vonSANDBOXD_MAX_SESSIONS(20) zählt — eine Organisation kann die Installation also nicht einnehmen. Über der Obergrenze lehnt der Service mitalready holds 10 of 10ab.
Welche Umgebungen ein Agent anfordern darf¶
Eine Runtime wird ausgeliefert, und sie ist in diesem Repository definiert —
backend/app/core/catalog/sandbox_runtimes.json:
workbench — 1,93 GB, in etwa 65 s auf einem warmen Host gebaut |
|
|---|---|
| Gebaut auf | python:3.12-slim |
| Sprachen | Python 3.12; Node 24.19.0 LTS mit npm 11 und tsx für TypeScript |
| Coding Agents | Claude Code 2.1.288 und Codex 0.160.0, für den Coding Agent |
| Werkzeuge | git, curl, ripgrep, fd, jq, less, procps, unzip, zip, uv, pdftotext/pdfinfo |
| Lesen | liteparse (lit) — PDFs und Bilder zu Text oder Markdown, OCR eingeschlossen; poppler-utils für den schnellen Weg über die Textebene und eine Seitenzahl |
| Dokumente | pypdf, python-docx, openpyxl, python-pptx, reportlab; LibreOffice headless für die Konvertierung und die Altformate |
| Daten | pandas, duckdb, tabulate |
| Diagramme und Bilder | matplotlib (Agg), pillow |
| Web | httpx, requests, beautifulsoup4, lxml, markdownify |
| Sonstiges | pyyaml |
| Speicher | 2 GiB |
| Netzwerk | ja — die einzige Runtime, die eines hat |
Eine statt acht, und der Grund ist prewarm: der Service baut beim Start jeden
Eintrag seiner Allowlist, acht Aliase sind also acht pip installs in einem
Startvorgang, den niemand beobachtet, acht auf dem Host zwischengespeicherte Images,
und ein Agent, der ein PDF lesen soll, bekommt denjenigen Alias, den sein Spec
zufällig benannt hat. workbench ist darauf gebaut, die Antwort auf schreibe und
führe etwas Code aus, lies, was der Nutzer angehängt hat, zeichne es, hole eine
Seite zu sein.
Bis #1040 war dieser Katalog BUILTIN_RUNTIMES aus der Sandbox-Bibliothek: fünfzehn
Rezepte, von denen ein von diesem Projekt gestartetes sandboxd drei erlaubte. Es
bedeutete außerdem, dass ein Paket zu einem Image hinzuzufügen ein Release einer
Abhängigkeit, ein Versionssprung und ein Pin war.
Was darin ist, und was absichtlich nicht¶
Gemessen auf python:3.12-slim (205 MB), arm64:
- liteparse kommt aus
pip, und das ist die ganze Antwort. Das Wheel ist 13,8 MB groß, trägt die Rust-Binary und daslit-CLI, hat keine Python-Abhängigkeiten und bringt OCR mit — gemessen mit 1,3 s für ein einseitiges PDF mit OCR und 79 ms für ein PNG.cargo install(eine Rust-Toolchain), das npm-Paket (eine zweite Kopie derselben Binary) und der WASM-Build (für Browser) bringen hier also nichts. - LibreOffice, mit +683 MB, und es ist das wert. Es bringt drei Dinge, die
hier sonst nichts bringt:
litkann die Altformate.doc,.xlsund.pptlesen, was ein Geschäftsnutzer tatsächlich anhängt;soffice --headless --convert-to pdf deck.pptxrendert ein Deck, das der Agent mitpython-pptxgebaut hat, womit eine Präsentation zu etwas wird, das eine Person öffnen kann; und--convert-to pngmacht aus einer Folie ein Bild, das der Agent zurücklesen und ansehen kann, dennread_fileist hier multimodal. Etwa eine Sekunde pro Dokument nach dem ersten. Writer und Calc sind +135 MB von diesen 683 und aus einem Grund dabei: eine Office-Konvertierung, die für Präsentationen funktionierte und für Dokumente nicht, wäre eine Ausnahme im Produkt und eine Ausnahme im Prompt. - Es funktioniert nur, weil die Runtime gebaut wird. LibreOffice legt beim
ersten Lauf ein Benutzerprofil an, es braucht also ein echtes Konto mit einem
beschreibbaren Home — das der Builder anlegt, wenn
SANDBOXD_SANDBOX_UIDgesetzt ist (useradd --uid 10001 --create-home). Führen Sie dasselbe Image als nackte uid ohne passwd-Eintrag aus, und jede Konvertierung scheitert mitUser installation could not be completed. Auch deshalb kann eine fertigeimage-Runtime nicht einfach LibreOffice ergänzen: die beiden Entscheidungen sind eine Entscheidung. - Node von nodejs.org, nicht aus apt. Debians
nodejs npmist +398 MB und liefert npm 9; das offizielle Tarball ist +239 MB und aktuell — und Node 20, auf das dieses Rezept zuerst gepinnt war, ist seit April 2026 End-of-Life. Die Architektur wird im Befehl erkannt, denn derselbe Katalog baut auf amd64 und arm64. poppler-utils(+67 MB) neben liteparse, nicht statt seiner.litist der bessere Leser — Layout, Tabellen, Markdown — undpdftotextder schnellere bei einem PDF, das bereits Text hat: ein 120-seitiges Buch in unter einer Sekunde.pdfinfoist der eigentliche Grund, weshalb es hier ist, denn eine Seitenzahl in Millisekunden ist das, was aus „extrahiere dieses Buch" einen Plan macht.- OCR ist der Kostenpunkt, auf den es ankommt, und er ist gemessen.
litführt OCR nur auf Seiten ohne Textebene aus, 120 generierte Seiten kosten also den Bruchteil einer Sekunde — eine gescannte Seite kostet aber 8,8 s, ein 300-seitiger Scan sind also etwa 44 Minuten gegen eine Befehlsobergrenze von 300 Sekunden: abgebrochen, ohne etwas vorzuweisen.--target-pages 1-40begrenzt das (drei gescannte Seiten in 1,6 s), und--no-ocrauf einem Scan gelingt und gibt 179 Bytes zurück — eine stille, nahezu leere Antwort, was das schlimmere der beiden Scheitern ist. Beides steht im Briefing unten, denn genau das ist die Anfrage, die ein Nutzer stellt: „fasse dieses Buch zusammen". - Kein
build-essential(+94 MB) und keinscikit-learn/scipy(~200 MB). Beides ist auf einer Runtime mit Netzwerk einuv pip installentfernt. Sie wegzulassen kostet eine einmalige Installation; sie einzubacken bezahlt jeder Host bei jedem Start. requestsnebenhttpxundtabulatenebenpandas, mit einem halben Megabyte zusammen: ein Modell schreibtimport requestsunddf.to_markdown()aus dem Muskelgedächtnis, und keines von beiden ist ein fehlgeschlagenes Skript und einen erneuten Versuch wert.tzdataundfonts-dejavu-coresind in der apt-Schicht, weilpython:slimkeines von beiden hat,zoneinfoalso eine Exception wirft undPIL.ImageDraw.textkeine Schrift laden kann — beides vorher und nachher verifiziert.env_varsstatt einer Gewohnheit.MPLBACKEND=Agg,PYTHONUTF8=1,PYTHONUNBUFFERED=1,PYTHONDONTWRITEBYTECODE=1sind Eigenschaften des Images; ein Run, der sich an sie erinnern muss, ist ein Run, der es nicht tun wird.
Das Modell erfährt all das¶
Ein Container nützt einem Agent nichts, der nicht weiß, was darin ist.
Vor #1040 hätte ein Agent, den man um ein Diagramm bat, import plotly geschrieben,
und einer, dem man ein PDF reichte, hätte seinen eigenen Extraktor neben das lit
geschrieben, das es liest — beide hätten es sonst gelernt, indem sie mitten in der
Anfrage von jemandem scheitern.
Und es erfährt, wie zu arbeiten ist, nicht nur, was installiert ist.
Die Anweisung, die ihren Platz verdient, ist die, die nichts anderes lehren kann:
lesen Sie keine große Datei, um sie zu durchsehen. Extrahieren Sie sie einmal in
eine Textdatei, rg -n für die Stellen, auf die es ankommt, sed -n '400,460p', um
eine zu lesen.
Ein Buch hat Tausende von Zeilen, und eine Antwort braucht Dutzende davon. Ein Modell, das das Ganze in seinen eigenen Kontext zieht, gibt das Budget des Runs für Seiten aus, nach denen niemand gefragt hat. Derselbe Absatz trägt die beiden OCR-Fallen von oben, denn ein gescanntes Buch ist der Ort, an dem beide zugleich landen.
Jedem Run auf einer Runtime, die dieses Deployment ausliefert, wird deshalb ein
Absatz an seine Instruktionen angehängt: welche Runtime er bekommen hat, die
Paketliste, die lit-Zeile, was soffice konvertiert, die eine Lücke (kein
C-Compiler), und ob er ein Netzwerk hat.
Er ist aus dem Katalog komponiert, nicht daneben geschrieben.
runtime_briefing liest die Paketliste aus der Definition, ein zur Datei
hinzugefügtes Paket erreicht den Prompt also in derselben Bearbeitung, in der es das
Image erreicht. Nur was sich nicht ableiten lässt, ist Prosa, in der briefing-Liste
des Eintrags.
Zwei Konsequenzen, die man kennen sollte:
- Er wird pro Run angehängt, so wie der Prompt einer Channel-Bindung, denn welche Runtime ein Run bekommt, wird beim Start des Runs aus dem Spec, der Connection und dem Host aufgelöst. Das veröffentlichte Spec bleibt unangetastet.
- Ein Alias, den dieses Deployment nicht ausliefert, bekommt keinen Absatz. Ein Host, der mit einer eigenen Allowlist gestartet wurde, ist keiner, dessen Images wir ehrlich beschreiben können, und ein Prompt, der rät, ist schlimmer als ein Prompt, der schweigt.
Ihn ändern¶
$EDITOR backend/app/core/catalog/sandbox_runtimes.json
make sandbox-runtimes # writes SANDBOXD_RUNTIMES into all three compose files
docker compose up -d sandboxd # prewarm rebuilds what the list now names
Ein Eintrag hat eine von zwei Formen, nie beide:
{
"alias": "workbench",
"description": "What it is for - shown in the connection dialog",
"base_image": "python:3.12-slim",
"setup_commands": ["apt-get update && apt-get install -y --no-install-recommends git"],
"packages": ["pillow"],
"mem_limit": "2g",
"needs_network": true
}
| Feld | |
|---|---|
alias |
Was ein Spec benennt. Kleinschreibung, [a-z][a-z0-9-]* |
description |
Gezeigt unter Default runtime im Connection-Dialog. Sagen Sie, wofür sie ist |
image |
Ein fertiges Image. Startet in der Zeit, die ein Pull braucht, und installiert nichts |
base_image |
Einmal bei der ersten Nutzung gebaut und danach zwischengespeichert — die Form, die installieren kann |
setup_commands |
Shell zur Build-Zeit, vor den Paketen: eine apt-Schicht, ein Installer |
packages |
pip, zur Build-Zeit installiert. Braucht ein base_image |
env_vars |
In jedem Container auf dieser Runtime gesetzt — MPLBACKEND, PYTHONUTF8 |
briefing |
Sätze, die dem Modell gesagt werden und sich nicht aus den obigen Feldern ableiten lassen |
mem_limit |
Dockers eigene Syntax (2g). Fehlt er, gilt SANDBOXD_MEM_LIMIT |
needs_network |
Ob eine Session darauf ein Netzwerk bekommt. Ein Build hat immer eines |
Vier Dinge, die die Datei Sie nicht falsch machen lässt, oder die zubeißen, wenn Sie diesen Abschnitt überspringen:
imageundbase_imageschließen einander aus, und einepackages- odersetup_commands-Liste an einemimage-Eintrag wird beim Import abgelehnt. Angenommen, wäre das eine Runtime, deren Pakete im Katalog stehen, in der Compose-Datei stehen und nicht im Container sind.- Der erste Eintrag ist die Voreinstellung für einen Agent, dessen Spec keine Runtime benennt, die Reihenfolge der Datei ist also tragend.
network_modewird nicht vererbt.SANDBOXD_NETWORK_MODEgilt für den gesamten Service und jede ausgelieferte Compose-Datei setzt es aufnone, ein Eintrag, der zur Laufzeit etwas installiert, braucht also ein eigenes Netzwerk.needs_networkist diese Entscheidung, einmal dort getroffen, wo die Pakete sind, statt pro Compose-Datei erinnert; übersehen, ist das Scheitern ein Agent, dessenuv pip installin einen Timeout läuft.- Ein fehlerhafter Eintrag stoppt das Deployment, absichtlich — der Katalog wird beim Import validiert und nicht bei der ersten Nutzung, denn eine Auswahl mit einem Loch darin wird von einem Nutzer entdeckt.
Warum der Wert auch in den Compose-Dateien steht¶
SANDBOXD_RUNTIMES ist der einzige Kanal, auf dem der Service Runtimes annimmt.
Eine Compose-Datei kann keinen Befehl aufrufen, der Wert dort ist also eine
generierte Kopie, und die einzige Frage, die zu beantworten sich lohnt, ist, ob sie
auseinanderlaufen kann: backend/tests/test_sandbox_runtime_catalog.py schlägt fehl,
wenn eine das getan hat, benennt die Datei und sagt Ihnen, dass Sie
make sandbox-runtimes ausführen sollen.
Generiert in eine versionierte Datei, statt beim Start aus einer Nebendatei
gelesen zu werden, denn docker compose up muss funktionieren, ohne vorher etwas zu
generieren — die Alternative ist ein Deployment, das stillschweigend die eigene
Standard-Allowlist der Bibliothek übernimmt.
Und es ist kein Formular im Produkt. PUT /policy ändert Obergrenzen und
Lebensdauern zur Laufzeit und lehnt die Zusammensetzung dieser Liste absichtlich
ab, ebenso network_mode, oci_runtime, sandbox_uid, work_dir und
persist_containers: ein Image zu benennen ist eine Entscheidung über Isolation, und
der Service-Token wird von einer Anwendung gehalten und nicht von demjenigen, der den
Host betreibt. Die Liste zu ändern ist ein Neustart.
Zwei Listen im Produkt, die verschiedene Fragen beantworten¶
Das Default runtime des Connection-Dialogs bietet diesen Katalog an — das, was
die Compose-Dateien dem Service gegeben haben — befüllt, bevor irgendein Host gefragt
wurde, und markiert, sobald einer geantwortet hat. Das Runtime des Agent
Builders bietet an, was der Service auf dieser Connection tatsächlich erlaubt,
live von ihm gelesen, ein Alias, den er benennt, ist also einer, den der nächste
Tool-Aufruf annimmt. Wo die beiden uneins sind, hat die zweite recht: ein Host kann
mit einer anderen Allowlist gestartet worden sein, und ein Deployment, das eine
eigene generiert hat, ist genau der Fall, den man nicht fallen lassen sollte.
Deshalb wird der Host gefragt, bevor die Connection gespeichert wird, und deshalb
kann der Service, den make dev startet, ganz ohne Schlüssel gefragt werden.
Diesen einen hinzuzufügen ist der häufigste Weg durch den Dialog, und er benennt bis zur Absendung keinen Vault-Schlüssel — es gab also nichts, womit man hätte testen können, und ein veralteter lokaler Service konnte mit einer Default-Runtime registriert werden, die sein erster Tool-Aufruf ablehnt.
Ein Probe-Aufruf ohne Schlüssel fällt nur bei zwei Adressen auf SANDBOXD_TOKEN zurück
Den beiden, die die eigene Compose-Datei dieses Projekts verwendet. Dieser Token startet Container auf jedem Host, der ihn annimmt, und ein Probe-Aufruf darf niemals ein Weg sein, ihn irgendwohin Neues zu schicken.
Jede andere Adresse wird mit einem Schlüssel aus dem Vault gefragt, und nur dann, wenn ein Betreiber die Schaltfläche drückt.
Wann ein Container erscheint¶
Bei der ersten Workspace-Operation: einem Tool, das der Agent dort aufruft, oder einer angehängten Datei, die in den Workspace gelegt wird. Die Skills des Agents werden in diesem Moment in den Workspace geschrieben, kurz bevor die Operation läuft, und nicht schon, während der Run vorbereitet wird. Ein Zug, der den Sandbox nie benutzt, öffnet also keine Session und startet keinen Container. Ein Zug, der ihn benutzt, findet die Skills bereits auf der Platte. In einer Session-Liste kann trotzdem ein Sandbox stehen, dessen Aktivitätslog nur Schreibvorgänge enthält: einer, in den eine Datei gelegt wurde und der danach unbenutzt blieb.
Was in einem getan wurde, und wo dieser Nachweis liegt¶
In der eigenen Tabelle dieser Plattform, sandbox_operations — nicht im Service.
Der Service führt ein eigenes Aktivitätslog, und das ist ein Ringpuffer mit 200
Einträgen im Speicher dieses Prozesses. Was er verworfen hat, konnte nicht abgefragt
werden, eine Unterhaltung, in der den ganzen Tag gearbeitet wurde, hatte ihren
Vormittag verloren, und sandboxd neu zu starten verlor jedes Log auf dem Host.
Nichts außerhalb dieses Prozesses hat die Einträge je gesehen (#1061).
Jeder Workspace-Aufruf geht ohnehin durch diese Anwendung — der Run ruft uns auf, wir rufen den Service auf — der Nachweis ist also unserer.
RecordingWorkspace umhüllt den Workspace, in dem der Run arbeitet, weshalb ein
neuntes Tool hinzuzufügen das Aufzeichnen nicht vergessen kann. Aufgezeichnet wird,
was die Sandbox erreicht hat — read, write, ls_info, mkdir, remove und
execute —, sodass ein Tool, das aus mehreren Operationen besteht, als jede davon
erscheint: ein edit_file ist ein read und ein write, ein glob oder grep
das execute des find oder grep, das es ausgeführt hat. exists und stat
sind Fragen und keine Operationen, und ein Log voll davon würde die
Schreibvorgänge begraben, wegen derer jemand gekommen ist.
Er trägt zwei Tatsachen, die der Service nie tragen konnte, und es sind die beiden,
nach denen ein Audit tatsächlich fragt: welcher Agent, und welcher Run. Beide
sind beim Löschen SET NULL, denn der Nachweis dessen, was geschehen ist, muss den
Agent überleben, der danach gelöscht wurde.
Ein Pfad, nie eine Nutzlast
write zeichnet den Pfad auf und dass es gelungen ist. execute zeichnet den
Befehl auf und nie dessen Ausgabe — ein Exit ungleich null wird als Fehlschlag
mit seinem numerischen Status aufgezeichnet (exit 2), die eine sichere
Tatsache über einen fehlgeschlagenen Befehl. read zeichnet den Pfad und eine
Byte-Anzahl auf.
Diese Zeilen sind für jeden lesbar, der den Sandbox sehen kann, ein Log mit Inhalten wäre also eine Möglichkeit, die Arbeit eines Agents zu lesen, statt ein Audit darüber — dieselbe Linie, die der Service zieht, hier erneut gezogen.
Die eine Zeile über den Ausgang wird von uns geschrieben, nie von unten zitiert: die Meldung einer Shell ist die Ausgabe des Befehls (#423).
Zeilen landen, wenn die Transaktion des Runs committet, denn sie werden in die eigene Session des Runs geschrieben und nicht über eine Verbindung pro Tool-Aufruf. Die Operationen eines Zuges erscheinen also gemeinsam, etwa eine Sekunde nach dem Ende des Zuges.
Der Live-Ticker der Dashboard-Zeile liest aus genau diesem Grund weiterhin den Puffer des Services: er antwortet mitten im Zug, wo der Nachweis eine Woche später antwortet.
GET /api/v1/sandbox-connections/operations blättert darin, und seine Filter grenzen
die Abfrage ein: die Suche des Dialogs, sein Operationsfilter und sein Schalter
für „nur fehlgeschlagene" sind Requests, ein Pager über dreihundert Operationen hat
also etwas, wohin er blättern kann.
Wann ein Build bezahlt wird¶
prewarm ist an, die Allowlist wird also beim Start des Services im Hintergrund
geholt und gebaut, statt in der ersten Anfrage von jemandem — ein Build sind zehn
Sekunden und mehr. Images werden zwischengespeichert, ein Host zahlt also einmal.
Ein Deployment baut sie noch früher. scripts/deploy.sh führt das
python -m pydantic_ai_backends.remote.prepare der Bibliothek in einem einmaligen
Container des neuen sandboxd-Images aus, bevor der alte Dienst ersetzt wird. Ein
Release, das eine Runtime ändert, bezahlt den Build also, während der alte Dienst
noch bedient. Vorher wartete am 18.10.2026 der erste Chat mit Sandbox nach einem
solchen Release minutenlang, und jede Session-Anfrage bekam 409, bis der Build
fertig war. Ein fehlgeschlagener Build wird dort dreimal versucht und beim Start
zweimal. Eine Runtime, die danach noch nicht gebaut ist, wird wie bisher bei
Bedarf gebaut, und das Deploy-Log nennt sie.
Solange eine Session auf einen solchen Build wartet, sagt der Chat das über dem Eingabefeld: „Die Sandbox wird vorbereitet“. Das Öffnen wartet bis zu zehn Minuten darauf, nicht die 30 Sekunden einer gewöhnlichen Anfrage.
Was zu warten bleibt: eine Runtime, die das Deployment nicht vorbereiten konnte,
und ein Host, dessen Image-Cache geleert wurde.
SANDBOXD_PERSIST_CONTAINERS: true beseitigt dann den größten Teil des Rests — eine
geschlossene Session behält ihren Container, die nächste Session auf diesem Workspace
startet also ohne Build und mit allem, was der Agent beim letzten Mal installiert
hat, weiterhin installiert.
Isolation, schlicht gesagt¶
Was hält:
- der Sandbox kann den Docker-Socket nicht sehen; nur
sandboxdkann das; - überhaupt kein Netzwerk, sofern die Runtime nicht eines anfordert, und das tut nur
workbench; - 2 CPUs, 512 Prozesse und ein 64 MiB großes
tmpfsunter/tmppro Sandbox, dazuSANDBOXD_EXECUTE_TIMEOUT(300 s) auf jedem Befehl undSANDBOXD_MAX_READ_BYTES(8 MiB) auf jedem Lesevorgang; SANDBOXD_SANDBOX_UID: 10001— ein Sandbox läuft als unprivilegierter Nutzer und nicht als root, und jede Datei, die ein Agent schreibt, gehört auf dem Host dieser uid. Sie muss die eigene uid des Services sein: das Öffnen einer Session führt einchowndes Workspace auf diesen Nutzer aus, und ein unprivilegiertessandboxdkann das nur für sich selbst tun, eine andere Nummer scheitert also bei der ersten Session statt beim Start. Sie gilt für eine Runtime, die dieses Deployment baut — ein fertiges Image hat kein solches Konto und kein Virtualenv, ein Agent darin könnte also nichts installieren.
Was bleibt, ausgesprochen statt angedeutet:
- Der Service-Token ist root-äquivalent. Siehe oben.
- Aus dem Container auszubrechen heißt, auf den Host auszubrechen. Gewöhnliches
runc;oci_runtimekann ein gesandboxtes benennen (gVisorsrunsc), wo ein Deployment diesen Handel will. - Eine Runtime mit Netzwerk kann auf dem Host veröffentlichte Ports erreichen.
docker-compose.ymlveröffentlicht Postgres und Redis für die lokale Entwicklung, mitpostgres/postgres;docker-compose-prod.ymlveröffentlicht keines von beiden.
Ein Laptop-Vorbehalt, aber prüfen Sie ihn, bevor Sie die lokale Compose-Datei kopieren
docker-compose.yml veröffentlicht Postgres und Redis mit postgres/postgres,
und workbench ist die eine Runtime mit einem Netzwerk. Auf einem geteilten
Host ist das aus einem Sandbox heraus erreichbar; docker-compose-prod.yml
veröffentlicht keines von beiden.
Was der Dateibrowser zeigt, und was er auslässt¶
Documents (/documents) listet auf, was ein Agent für eine Person aufbewahrt, was nicht
dieselbe Menge ist wie das, was auf dem Volume liegt. Drei Präfixe fallen aus jeder
Auflistung, die eine Person liest - der Liste aller Dateien, den Dateien eines
Ordners, dem Panel einer Unterhaltung und den Dateizahlen:
skills/— der Rumpf eines Skills und seine Ressourcen, geschrieben zu Beginn jedes Runs, der sowohl Skills als auch einen Workspace hat. Sie werden dort gebraucht: eine Ressource ist ein Skript, das die Shell ausführt, undcollect_changesbildet aus diesen Dateien einen Vorschlag, den jemand annimmt. Sie wurden einmal entfernt, weil die Auflistung größtenteils aus ihnen bestand, was die richtige Beschwerde über die falsche Sache war (#1064).- das Spill-Verzeichnis — wohin die überlaufende Ausgabe eines Tools geschrieben wurde.
.agenticos/— die Ereignisse einer Coding-Aufgabe und die Sitzung, die die nächste fortsetzt.
Ebenso die Ordner, die eine Toolchain füllt, wo immer sie liegen: .venv, venv,
node_modules, __pycache__, .pytest_cache, .mypy_cache, .ruff_cache,
.tox, .git, .next und .cache. Ein Coding Agent, der eine FastAPI-App
aufsetzte, brachte tausend davon auf die Seite, zwischen das Dutzend Dateien, das
er schrieb.
Eine Zählung muss sie ebenfalls weglassen, sonst ist ein Workspace, der vier Dateien meldet, während eine sichtbar ist, eine Zahl, die niemand prüfen kann. Alle Dateien gruppiert, was übrig bleibt, nach dem Workspace, der es hält, das neueste zuerst, unter der Unterhaltung, zu der es gehört: lose Dateien und Anhänge als Zeilen, jeder Ordner der obersten Ebene als eine Zeile mit seinen ersten Dateien, und ein Link zum Ordner für den Rest. Die neuesten Dateien stehen als Vorschau über den Gruppen, und eine Suche listet ihre Treffer flach.
Wessen Workspace es ist, und wer ihn sonst sehen kann¶
Zwei Antworten, und die Tabelle trägt beide.
access_label ist der Scope in Worten — „jeder, der mit diesem Agent spricht",
„wer in dieser Unterhaltung ist". Es benennt niemanden, was genau die Frage ist, die
ein Betreiber zu einem agent-scoped Workspace hat, den sich sechs Personen teilen.
Die Zeile trägt deshalb auch owner_name: die E-Mail eines Kontos, oder die
Plattform-Id eines Owners, der über einen Channel kam und hier kein Konto hat. Es
wird als Wort gezeichnet und nie als Link, denn die Hälfte davon sind keine Konten,
auf die man verlinken könnte. Und es ist für jeden Scope außer user null, der als
einziger überhaupt einen Owner festhält — drei der vier haben ehrlicherweise keinen,
und die Spalte sagt das, statt den Scope zu wiederholen.
Eine Datei sagt, wer sie dort abgelegt hat¶
uploads/ ist der Ort, an dem ein Anhang landet, ein Pfad darunter ist also eine
Datei, die eine Person angehängt hat, und alles andere ist die eigene Arbeit des
Agents. Es wird als Filter angeboten und auf der Kachel gesagt.
Es ist das einzige verfügbare Signal: ein Host hält keinen Urheber fest, und das
State-Dokument tut es ebenso wenig. Seine Grenze folgt daraus — ein Agent, der selbst
in uploads/ schreibt, ist von einer Person nicht zu unterscheiden, und nichts hält
ihn davon ab.
Einen Container aufzulisten kostet Roundtrips, deshalb sind zwei Dinge begrenzt¶
Das ls des Archivs liest ein Verzeichnis, die Auflistung fragt den Host deshalb
nach seinem tree: eine Anfrage, in die Breite zuerst auf der eigenen Platte des
Hosts durchlaufen, höchstens sechs Ebenen tief, mit Halt bei 2.000 Einträgen — denn
ein Host, der ein node_modules hält, darf aus einem Workspace nicht zehntausend
Zeilen machen.
Beide Grenzen werden gemeldet. Die Seite eines Workspace sagt klar, dass dies nicht jede Datei ist, denn ein Baum, der ohne Ankündigung aufhört, ist einer, den jemand als alles liest, was der Agent aufbewahrt.
Ein Verzeichnis, das nicht antwortet, wird übersprungen, und die Auflistung meldet sich als unvollständig. Nur wenn die Wurzel sich weigert, wird der Workspace unlesbar, denn ein Ordner, den der Agent weg-chmodet hat, ist kein Host, den niemand lesen kann.
Und das Vorschaubild eines Bildes ist ein read_bytes für diese Datei: die Endung
und die Größe werden am Auflistungseintrag geprüft, bevor irgendetwas geholt wird,
und ein Request zeichnet höchstens 24. Darüber hinaus behält eine Kachel das Glyph.
Ein gespeicherter Workspace zahlt keines von beidem — seine Dateien und deren Bytes sind eine Spalte der Zeile, die die Auflistung ohnehin gelesen hat.
Das Files-Panel im Chat und die eigene Liste eines Workspace zeichnen ihre Kacheln wie All files: die ersten Zeilen einer gespeicherten Textdatei und ein Bild als Vorschau innerhalb derselben Grenze.
Wie lange irgendetwas überlebt¶
Dateien liegen auf dem Host, unter
{SANDBOXD_WORKSPACE_ROOT}/{session_id}/workspace —
/tmp/agenticos-sandbox-workspaces lokal, /var/lib/agenticos/sandbox-workspaces
auf dem Dev-Server und in der Produktion. Dieser Bind Mount ist auch das, was das
Files-Panel des Produkts möglich macht: einen Workspace zu lesen startet nie einen
Container.
| Einstellung | Was geschieht | |
|---|---|---|
| Eine untätige Session | SANDBOXD_IDLE_TIMEOUT 1800 s |
Der Container wird geschlossen und abgeräumt. Die Dateien bleiben |
| Ein gestoppter Container | SANDBOXD_CONTAINER_TTL 86400 s |
Was die Session installiert hat — der Build, die Wheels, node_modules — wird zurückgeholt. Der Workspace bleibt unangetastet |
| Das Workspace-Verzeichnis | SANDBOXD_WORKSPACE_TTL nicht gesetzt |
Unbegrenzt aufbewahrt |
| Der Nachweis dessen, was getan wurde | OPERATION_RETENTION_DAYS 30 |
Der tägliche sandbox-log-sweep löscht die Zeilen. Die Dateien bleiben unangetastet |
Die letzte Zeile ist die Voreinstellung der Bibliothek und sie ist Absicht — die Notizen und Skripte sind die Arbeit, und der Nutzer eines Agents erwartet sie nächste Woche.
Der Plattenverbrauch wächst nur
Nichts räumt einen Workspace ab, dessen Unterhaltung niemand mehr öffnen wird.
Setzen Sie SANDBOXD_WORKSPACE_TTL auf das, was Ihre Aufbewahrungsrichtlinie
sagt, und die Dateien, die älter sind, verschwinden.
/tmp wird auf einem Laptop von einem Neustart geleert; /var/lib nicht.
Eine Unterhaltung zu löschen räumt ihren Workspace über das Produkt ab, hier geht es also um das, was niemand löscht, und nicht darum, was sie tun.
Dieses Aufräumen hängt nicht davon ab, ob der Anbieter gerade antwortet. Das
Löschen schreibt in derselben Transaktion einen Grabstein für jede entfernte
Sandbox, und ein Destroy entfernt ihn, sobald der Anbieter ihn bestätigt. Ist der
Sandbox-Host down, wird ein Daytona-Schlüssel abgelehnt oder startet der Worker
dazwischen neu, versucht es der Sweep sandbox-destroy-retry des Workers erneut.
Die Pause verdoppelt sich jedes Mal, von 2 Minuten bis 6 Stunden, bis der Destroy
gelingt. Hinter einer Daytona-Sandbox steht keine TTL, also beendet nur das sie.
Auch eine abgeschaltete Verbindung bekommt ihre Sandboxes entfernt, denn Abschalten stoppt neue Arbeit, nicht die Rechnung. Eine Verbindung mit verbliebenen Tombstones lässt sich nicht löschen: Die Tombstones gingen mit ihr, und sie sind die einzige Aufzeichnung dessen, was beim Anbieter noch läuft. Kann der Sweep nie gelingen, weil die Zugangsdaten der Verbindung gelöscht oder widerrufen wurden, lösche die Sandboxes in der Konsole des Anbieters. Lösche dann die Verbindung erneut und bestätige das: Ihre Tombstones werden vergessen, und ihre Anzahl landet im Audit-Log.
Zusammenfassung¶
- Der API-Container hält keinen Docker-Socket.
sandboxdtut es, und ein Sandbox ist sein Geschwister und kein Container darin. - Ein Session-Schlüssel faltet Scope, Organisation, Backend-Art und Host zusammen — was genau das ist, was entscheidet, wer sich einen Container teilt.
- Eine Runtime wird ausgeliefert,
workbench, und dem Modell wird gesagt, was darin ist, komponiert aus demselben Katalog, der sie gebaut hat. - Der Nachweis dessen, was ein Agent getan hat, liegt in der Tabelle dieser Plattform, hält einen Pfad und nie eine Nutzlast, und überlebt den Agent.
- Dateien werden unbegrenzt aufbewahrt, sofern Sie
SANDBOXD_WORKSPACE_TTLnicht setzen. Der Plattenverbrauch wächst nur.
Das Backend state zählt Dateien und leere Verzeichnisse zum Speicherlimit, auch beim abschließenden Zusammenführen gleichzeitiger Änderungen. Passen einzeln akzeptierte Dokumente nicht gemeinsam hinein, bleibt das gemeinsame Dokument unverändert und der spätere Stand wird als Wiederherstellungskopie unter Documents bestätigt. Der Run speichert einen Fehler mit der Workspace-ID der Kopie; eine Projektänderung meldet einen Konflikt statt Erfolg. Projektkopien erfordern sowohl Anzeige- als auch Nutzungszugriff. Ein Fremdschlüssel löscht die Kopie zusammen mit ihrer Quelle, auch bei gleichzeitiger Datenlöschung. Wird das Limit unter die Größe eines bestehenden Dokuments gesenkt, darf dieses weiterhin schrumpfen.
Die Workbench-Anweisungen erklären Agenten, dass Befehle unter POSIX /bin/sh und nicht unter Bash laufen. Zum Beispiel erstellt mkdir -p sources inbox notes drei Ordner; {sources,inbox,notes} wird von dieser Shell nicht expandiert. Projektdateien gehören nach /workspace; das Benutzerverzeichnis (~) liegt außerhalb der gemeinsamen Dateien. Jeder Befehl startet einen neuen Prozess: Shellvariablen und Verzeichniswechsel gelten nicht für den nächsten Aufruf. Agenten sollen Fehler prüfen und Dateien vor einer Erfolgsmeldung verifizieren.