Unix

Unix hat das 1971 zufriedenstellend gelöst. Wir brauchten zwei Tage, um das zu merken.

Eine Workshop-App sollte vom Modell erzeugten Python-Code sicher ausführen. Wir probierten Pyodide, zogen Firecracker in Betracht, bauten eine Docker-in-Docker-Sandbox und stießen auf echte Reibung. Dann erinnerte uns ein Umweg über macOS-Benutzerkonten für Agenten daran, dass das Unix-Mehrbenutzermodell dieses Problem seit über fünfzig Jahren löst.

Ich habe zwei Tage lang mit Claude Code einen in einer Sandbox laufenden Code-Executor für eine kleine Workshop-PWA entworfen und gebaut. Dieser Text hält fest, was wir falsch gemacht haben, was wir richtig gemacht haben und was wir erst verstanden, als der Code lief. Jede hier beschriebene Entscheidung und jede Kehrtwende findet sich in den ausgelieferten Commits wieder. Das Interessante ist nicht die endgültige Architektur, die unkompliziert ist, sondern der Weg dorthin, samt der zweieinhalb Irrwege unterwegs.

Ich sage bewusst „wir“. Die Designsitzung war ein laufendes Gespräch zwischen mir und Claude Code im Terminal. Wo es auf den genauen Wortlaut ankommt, zitiere ich die KI, den Rest erzähle ich selbst.

Das Problem

Wir betreiben einen kleinen Playground: eine PWA, die Workshop-Teilnehmer für Chats mit mehreren Modellen, HTML-Generierung und einen E-Mail-gesteuerten Process Runner nutzen. Vanilla-JS-Frontend, Node-Backend, deployt als Docker-Compose-Stack auf einem einzelnen VPS. Kein Framework, kein Build-Schritt, ein Betreiber, zu jedem Zeitpunkt ein paar Dutzend authentifizierte Benutzer.

Wir wollten Agent Skills unterstützen, das Format, das wiederverwendbare LLM-Fähigkeiten als Zips verpackt, mit einer SKILL.md und optional mitgelieferten Skripten. Im bestehenden Skills-Tab konnten Benutzer Skills bereits durchsuchen und herunterladen. Als Nächstes wollten wir eine Laufzeitumgebung: Benutzer aktivieren einen Skill im Chat-Tab, und das Modell verwendet ihn tatsächlich, einschließlich der Ausführung der mitgelieferten Python-Skripte.

Die Frage: Wie führt man vom Modell erzeugten Python-Code sicher aus, auf einem einzelnen VPS, für authentifizierte Benutzer, mit echter Bibliotheksunterstützung, ohne eine Infrastruktur auf Serverless-Niveau zu bauen?

Das Kandidatenfeld ist größer, als Sie denken

Stellt man die Frage als „welches Isolationsprimitiv für die Code-Ausführung“, kommen mindestens sieben Kandidaten in Frage: Pyodide (WASM im Browser), Docker als Subprozess auf dem Host, Docker plus gVisor, Firecracker-MicroVMs, externe Dienste wie e2b.dev oder Modal, V8-Isolates oder WASI auf dem Server und schlanke Namespace-Werkzeuge wie nsjail oder bubblewrap. Am Ende prüften wir drei davon ernsthaft und verwarfen den Rest. Die Option, die wir von Anfang an hätten erwägen sollen, kam erst an Tag zwei auf die Liste.

Pyodide: das Problem der Bibliotheksabdeckung

Der erste Reflex war Pyodide. Python läuft über WASM im Browser, innerhalb des Sandbox-Iframes, das wir für die HTML-Vorschau im Code-Tab ohnehin verwenden. Keine Serverkosten, und viele der gängigen wissenschaftlichen Python-Pakete sind schon vorgebaut dabei. Die Isolation ist das, was der Browser hergibt. Für eine Workshop-App ist das eine starke Standardwahl.

Die eigentliche Grenze von Pyodide ist aber die Bibliotheksabdeckung, auch wenn die fehlende Mehrsprachigkeit ebenfalls zählt. Wir mussten den offiziellen pdf-Skill von Anthropic ausführen, der von pypdf, pdfplumber, pypdfium2, reportlab, tesseract (über pytesseract) und poppler (über pdf2image) abhängt. Davon braucht pypdfium2 native Bindings, die sich nicht nach WASM kompilieren lassen. Tesseract ist ein System-Binary. Poppler ist ein System-Binary. Drei der sechs zentralen Abhängigkeiten scheiden damit aus.

Für reine Python-Daten-Skills ist Pyodide wirklich stark. Für Skills aus der Praxis, die jeden Interpreter und jede Toolchain mitbringen, die ihr Autor gebraucht hat, kommt es nicht in Frage. Wir zogen weiter.

Firecracker: das richtige Werkzeug, das falsche Projekt

Als Mehrsprachigkeit und native Abhängigkeiten auf dem Tisch lagen, griff Claude Code zu Firecracker, der MicroVM-Runtime von AWS, mit der Lambda seine Mandanten isoliert. Bootzeiten unter 200 ms in den von AWS veröffentlichten Benchmarks. Isolation auf Hypervisor-Ebene. Für „nicht vertrauenswürdigen Code sicher ausführen, in jeder Sprache“ sieht das perfekt aus.

Ich widersprach, und zwar nicht wegen der Fähigkeiten. Firecracker kann das. Mir ging es um die passende Dimensionierung. Die entscheidende Frage: Was bringt Firecracker gegenüber Docker? Für die Mehrsprachigkeit nichts, denn jedes Docker-Image liefert jede Sprache. Für die Isolation bringt es die Trennung auf Hypervisor-Ebene, und die zählt, wenn Sie Code von Tausenden nicht vertrauenswürdigen Fremden auf gemeinsamer Hardware ausführen. AWS vertraut Firecracker, weil CVEs für Kernel-Ausbrüche bei dieser Mandantenzahl ein realistischer Angriffsweg sind.

Wir haben höchstens ein paar Dutzend authentifizierte Workshop-Benutzer. Sie haben sich angemeldet, sie haben einen Magic Link bekommen, sie sind drin. Das Bedrohungsmodell lautet nicht „gegen feindliche Eingaben im Internetmaßstab verteidigen“, sondern „jemand führt versehentlich ein falsches rm aus, oder jemand schreibt ein Skript, das nach Hause telefonieren will“. Das ist ein anderes Problem, und zu seiner Lösung braucht es keinen Hypervisor.

Auch die Betriebskosten von Firecracker auf einem einzelnen VPS sind real: KVM-Verfügbarkeit prüfen (viele günstigere VPS-Anbieter stellen /dev/kvm gar nicht bereit), Gast-Kernel bauen und laufend patchen, eine Pipeline für Rootfs-Images, Jailer-Konfiguration pro VM, Tap-Devices pro VM mit NAT und iptables, ein Lifecycle-Manager und die Behandlung von Panics und Abstürzen. Für einen einzelnen Betreiber ist das struktureller Aufwand für eine Isolation, die das Bedrohungsmodell nicht verlangt.

An diesem Punkt war das Argument noch abstrakt: „mehr bewegliche Teile, mehr Betrieb“. Konkret wurde es später.

Die Scope-Falle

Mit Docker als gewählter Runtime mussten wir den Ausführungsvertrag entwerfen. Der erste Entwurf war sauber: Skills bringen fertig geschriebene Skripte in ihrem Zip mit, der Playground stellt dem Modell für jedes Skript ein Tool bereit, das Modell wählt eines aus und ruft es auf, der Benutzer genehmigt das in einem Berechtigungsdialog, Docker führt es aus, die Ausgabe kommt zurück.

Ich war mir bei diesem Scope sicher. Dann sah ich mir einen echten Skill an.

Der offizielle pdf-Skill von Anthropic besteht aus 315 Zeilen SKILL.md. Er hat acht mitgelieferte Skripte in scripts/, ja. Der Großteil der Datei ist aber ein Tutorial, das dem Modell zeigt, wie es pypdf, pdfplumber und reportlab direkt verwendet, mit Ad-hoc-Python-Schnipseln, die das Modell spontan schreiben und ausführen soll. Die mitgelieferten Skripte decken nur die kniffligen, wiederverwendbaren Operationen ab. Der Rest ist Beispielcode, den das Modell anpassen und ausführen soll.

In Claude Code oder Codex ist dieses „Ausführen“ ein nativer Bash-Tool-Aufruf. In unserem Playground hätte dieser Code mit dem engen Scope nirgends hingekonnt. Das Modell hätte korrektes Python produziert, und der Benutzer hätte nicht gerenderten Text gesehen.

Zehn Minuten Lektüre eines einzigen entpackten Skills korrigierten eine Designentscheidung, deren Rückbau eine Woche gekostet hätte. Wir kehrten zu einem einheitlichen run_code-Tool zurück: ein Endpunkt, beliebiger Quellcode, das Skill-Verzeichnis read-only gemountet. Will das Modell ein mitgeliefertes Skript verwenden, ruft es dieses aus dem Code heraus auf, den es schreibt. Eine künstliche Trennung zwischen kuratierter und Ad-hoc-Ausführung gibt es nicht mehr.

Damit musste sich auch das Berechtigungsmodell ändern. Der ursprüngliche Cache pro Skript („dieses Skript immer erlauben“) beruhte auf der Annahme, dass das Skript feststeht. Im neuen Design bringt jeder Aufruf frischen Quellcode mit. Eine Zustimmung für Code, den es noch gar nicht gibt, lässt sich nicht sinnvoll cachen. Wir ersetzten den Cache durch einen Genehmigungsdialog pro Aufruf und eine Opt-in-Checkbox pro Chat („Alle Code-Ausführungen in diesem Chat erlauben“), gespeichert in sessionStorage, damit sie beim Neuladen verfällt. Weniger clever, aber ehrlich. Eine Zustimmung für Code zu cachen, der bei jedem Aufruf neu ist, ist Sicherheitstheater.

Wir haben es gebaut. Es lief nicht.

Wir implementierten den Docker-Subprozess-Executor in vier Commits. Pro Turn ein docker run mit --network=none --cap-drop=ALL --security-opt no-new-privileges --memory=512m --cpus=1.0 --pids-limit=128, read-only Bind Mounts und ein kuratiertes playground-python-Image mit Python 3.12 und allen Abhängigkeiten des PDF-Skills. Drei API-Endpunkte: Upload, Ausführung, Download der Ausgabe. Dazu eine Tool-Use-Schleife im Frontend, die von Streaming auf Non-Streaming umschaltet, wenn ein Skill aktiv ist, den Berechtigungsdialog inline darstellt und Audit-Einträge mit Quellcode, stdout, stderr und herunterladbaren Ausgabedateien anlegt.

Ein Code-Review fand vier blockierende Bugs und zwei Sicherheitsprobleme: camelCase gegen snake_case im API-Vertrag, einen fehlenden Query-Parameter, eine Endlosschleife beim 11. Tool-Aufruf, die still API-Guthaben verbrannt hätte, und eine Kollision der Ausführungsverzeichnisse zwischen Benutzern, durch die zwei gleichzeitig aktive Benutzer sich Ein- und Ausgabeverzeichnisse hätten teilen können, weil die Turn-IDs keinen Benutzer-Namespace hatten. Alle sechs behoben wir in einem Folge-Commit.

Dann wollten wir es testen.

Claude Code machte das Problem sichtbar, bevor ich einen Browser geöffnet hatte: „Bevor wir weitermachen, muss ich auf eine echte Lücke hinweisen: Der api-Container kann Docker nicht starten.“

Der Executor ruft spawn('docker', ['run', ...]) in server/index.js auf. Der api-Container ist aber node:20-alpine. Er hat weder eine Docker-CLI noch einen Mount der /var/run/docker.sock des Hosts. Spawn würde ENOENT zurückgeben. Im Design war das nicht vorgesehen.

Wir verdrahteten Docker-in-Docker: docker-cli beim Start in den api-Container installiert, /var/run/docker.sock vom Host gemountet, und sofort kam das nächste Problem. Wenn der api-Container einen Befehl docker run --mount source=/app/data/exec/foo baut, interpretiert der Docker-Daemon des Hosts diesen Pfad im Dateisystem des Hosts, nicht im aufrufenden Container. /app/data/exec/foo gibt es auf dem Host nicht. Der echte Pfad sieht etwa so aus: /Users/martintreiber/.../server/data/exec/foo.

Wir lösten das mit einem fünfzeiligen toHostPath-Helper und einer Umgebungsvariable (HOST_SERVER_DIR=${PWD}/server), die Container-Pfade auf Host-Pfade zurückführt. Dann behoben wir noch eine Port-Abweichung zwischen dem laufenden Stack (8082) und der Compose-Datei (8081), die ein paar Minuten reines Debugging des Infrastrukturzustands kostete.

Vier Klebestellen, und jede verlangte Nachdenken:

  1. Der Socket-Mount, samt der Erkenntnis, dass er dem api-Container root-äquivalenten Zugriff auf den Docker-Daemon des Hosts gibt.
  2. Die Installation von docker-cli im Container beim Start.
  3. Die Umgebungsvariable HOST_SERVER_DIR plus Pfadübersetzung, damit die Bind Mounts des Daemons korrekt aufgelöst werden.
  4. Die Port-Abweichung, die Sorte Bug im Konfigurationszustand, die erst auftaucht, wenn man das Ding tatsächlich laufen lässt.

Hier hörte das Firecracker-Argument auf, abstrakt zu sein. Wenn schon der Docker-Weg einen Nachmittag nicht trivialer Verdrahtung verlangte, dann rechnen Sie das gegen die Firecracker-Liste hoch (KVM-Prüfung, Gast-Kernel-Build, Rootfs-Image, Jailer-Konfiguration, Tap-Devices pro VM, NAT und iptables, Lifecycle-Manager), und Sie landen beim fünf- bis zehnfachen Aufwand, plus laufender Wartung bei jeder neuen Kernel-CVE.

Fairerweise: Ein Teil dieser Docker-Verdrahtung betrifft nur die lokale Entwicklung. In Produktion läuft der api-Prozess auf dem Host und spricht direkt mit dem Docker-Daemon des Hosts, ohne Socket-Mount und ohne Pfadübersetzung. Aber selbst in diesem besten Fall wird das Produktionsgegenstück bei Firecracker nicht einfacher, denn KVM, Kernel und Jailer sind Anforderungen der Produktion und keine Eigenheiten der Entwicklungsumgebung.

Außerdem hatten wir eine neue Angriffsfläche geschaffen. Wenn /var/run/docker.sock in den api-Container gemountet ist, kann jeder kompromittierte Node-Prozess docker run --privileged -v /:/host ... ausführen und den VPS übernehmen. Für die lokale Entwicklung nahmen wir das hin, weil die Angriffsfläche des api-Containers auf Code begrenzt ist, den wir selbst geschrieben haben. Für die Produktion war ein docker-socket-proxy geplant (das tecnativa-Image ist das übliche Muster), der nur die Docker-API-Aufrufe zulässt, die der Executor braucht, und --privileged, beliebige Bind Mounts und Host-Networking ablehnt.

Das Feature funktionierte. Der End-to-End-Test lief durch: nginx → api-Container → Auth → Profil-Whitelist → Docker-in-Docker über den Socket → Sandbox-Container → Output-Erfassung, 171 ms Round-Trip, Netzwerk korrekt blockiert, Skill-Verzeichnis read-only gemountet.

Aber etwas an der Architektur fühlte sich falsch an. Drei Services in der Compose-Datei. Ein eigenes Docker-Image, das gebaut und gepflegt werden will. Ein Socket-Proxy, den man deployen muss. Pfadübersetzung. Das Ganze lief, aber die Komplexität passte nicht zum Problem.

Die Unix-Erleuchtung

Ungefähr zur selben Zeit dachte ich über ein anderes Problem nach: KI-Agenten auf meinem Mac laufen zu lassen. Die Idee war, jedem Agenten ein eigenes macOS-Benutzerkonto zu geben, keinen Admin, sondern einen Standardbenutzer mit vollen Rechten im eigenen Home-Verzeichnis und sonst nichts. Eine einfache Begrenzung des Schadensradius: Wird der Agentenprozess kompromittiert, kann er Dateien manipulieren, die er erreicht, aber keine Systemeinstellungen ändern, keine anderen Benutzer verwalten und keine Software systemweit installieren.

Beim Durchdenken der Details fiel mir etwas Offensichtliches auf, das ich übersehen hatte: macOS ist Unix. Das Benutzer- und Gruppenrechtemodell, UID/GID, Dateieigentum, Prozessisolation nach Benutzer: Das ist Unix-Technologie aus den frühen 1970ern, und sie ist seit über fünfzig Jahren eine produktive Isolationsschicht für mehrere Benutzer. Unter macOS bekommen Sie dasselbe Benutzermodell plus Apple-spezifische Werkzeuge wie launchd. Unter Linux bekommen Sie dasselbe Modell plus Namespaces und cgroups.

Und dann machte es klick. Auf dem Playground habe ich Benutzer. Sie authentifizieren sich, sie haben E-Mail-Adressen, sie haben Sessions. Im api-Container läuft Linux, mit der gesamten Unix-Benutzerverwaltung. Ich kann in diesem Container Linux-Benutzer anlegen, einen pro authentifiziertem Playground-Benutzer, und ihren Code unter ihrer eigenen UID ausführen. Keine Sub-Container. Kein Docker-Daemon. Kein Socket-Mount. Keine Pfadübersetzung. Nur useradd, unshare und gosu.

Das Unix-Mehrbenutzermodell ist wohl das am härtesten erprobte Isolationsprimitiv der Informatik. Es wurde über Milliarden von Deployments gehärtet. Jeder Sysadmin auf dem Planeten versteht es. Und trotzdem hatten in zwei Tagen Sandbox-Design weder ich noch Claude Code daran gedacht, bis ein Umweg über macOS es unmöglich machte, es zu ignorieren.

Das UID-im-Container-Modell

Das neue Design ist so einfach, dass es sich in einem Absatz skizzieren lässt. Jeder authentifizierte Benutzer bekommt im api-Container eine Linux-UID, die beim ersten Code-Lauf bei Bedarf angelegt wird. Jeder run_code-Aufruf startet den Code des Benutzers über unshare -p -m --fork (PID- und Mount-Namespaces pro Turn, sodass der Kindprozess nur seinen eigenen Prozess und seine eigene Mount-Sicht sieht), dann wechselt gosu auf die UID/GID des Benutzers, bevor Python per exec gestartet wird. Die Netzwerkrichtlinie besteht aus iptables-Regeln mit Owner-Match, die einmal beim Containerstart gesetzt werden und an der GID der Executor-Gruppe hängen: Alle UIDs im Bereich 10000 bis 65000 (eine willkürliche Reservierung, die System-UIDs meidet) dürfen weder localhost noch RFC-1918-Adressen noch Link-Local-Adressen erreichen. Der api-Prozess läuft als root (UID 0, außerhalb des Executor-Bereichs) und ist von den Regeln nicht betroffen.

Der dockerproxy-Service ist weg. Das playground-python-Image ist weg. DOCKER_HOST ist weg. toHostPath ist weg. Der Socket-Mount ist weg. Die Compose-Datei schrumpft von drei Services auf zwei. Der geschätzte Kaltstart sinkt von 500 bis 1500 ms (docker run pro Turn mit Overlay-Einrichtung, Seccomp-Auswertung und Netzwerkinitialisierung) auf etwa 15 ms (unshare plus gosu plus Mount-Einrichtung; das ist geschätzt und noch nicht gemessen, aber die beteiligten Operationen sind Syscalls und keine Daemon-Round-Trips).

Der Container selbst ist die Isolationsgrenze zum Host. Innerhalb davon sorgen UIDs pro Benutzer und Namespaces pro Turn für die Isolation zwischen den Benutzern. Das ist keine neuartige Architektur. So funktionieren Unix-Mehrbenutzersysteme schon seit der Zeit, bevor die meisten von uns geboren wurden.

Die iptables-Entdeckung, an der es fast gescheitert wäre

Ganz geradlinig war das Design nicht. Eine frühere Iteration verwendete unshare -U --map-user=UID, einen User-Namespace, der die UID in der Sandbox in die Benutzertabelle des Containers abbildet. So braucht der Container kein CAP_SYS_ADMIN, was sich nach einer saubereren Sicherheitslage anfühlte. Die Mount-Einrichtung funktionierte. Der UID-Wechsel funktionierte. Alles sah richtig aus.

Dann ließen wir die empirische Probe laufen. Zwei HTTP-Requests aus der Sandbox, einer an localhost:3000, einer an die IP der Docker-Bridge, kamen beide ans Ziel. Die Trefferzähler von iptables standen auf null. Die Regeln existierten, hatten aber nie gegriffen.

Der Grund ist subtil, und es lohnt sich, ihn zu verstehen. Wenn Sie mit --map-user einen User-Namespace anlegen, versieht der Kernel Sockets, die von Prozessen in diesem Namespace erzeugt werden, mit der äußeren UID und GID (typischerweise root) und nicht mit der abgebildeten inneren UID. Der iptables-Match -m owner --gid-owner liest sk_gid aus der Socket-Struktur, und das ist ein kgid_t im init-User-Namespace. Die abgebildete UID taucht dort nie auf. Die Regeln sind syntaktisch gültig und semantisch wirkungslos.

Die Lösung war, den User-Namespace ganz wegzulassen. unshare -p -m --fork (nur PID- und Mount-Namespaces, kein -U), wobei gosu ein echtes setuid/setgid im init-User-Namespace des Containers ausführt. Weil das ein echter Wechsel der Credentials ist und keine Namespace-Abbildung, passen sk_uid/sk_gid auf jedem Socket, den der Kindprozess öffnet, zu dem, was iptables erwartet. Probe wiederholt: beide URLs blockiert, Regel 1 und Regel 3 verzeichneten je einen Paket-Treffer.

Der Preis ist CAP_SYS_ADMIN auf dem api-Container, weil unshare -m (Mount-Namespace) ohne User-Namespace das verlangt. SYS_ADMIN ist eine weit gefasste Capability: Der Prozess kann damit Dinge mounten, Namespaces beitreten und verschiedene administrative Operationen innerhalb des Containers ausführen. Einen Ausbruch über die Host-Grenze erlaubt sie nicht, denn das Standard-Seccomp-Profil von Docker und der Mount-Namespace des Containers setzen diese Grenze weiterhin durch. Sie erweitert aber, was ein kompromittierter api-Prozess in seinem eigenen Container tun könnte, und das ist ein echter Kompromiss, den wir für unser Bedrohungsmodell eingehen.

Die Kalibrierungsleiter

Die ehrliche Antwort auf „welche Option ist die richtige“ ist keine einzelne. Sie ist eine Leiter, und jede Sprosse wird durch eine bestimmte Änderung im Bedrohungsmodell ausgelöst:

Unix-UIDs im Container (das, was wir ausgeliefert haben) ist richtig, solange Sie authentifizierte Benutzer haben, eine kleine Gruppe, keine mandantensensiblen Daten auf demselben Host und Linux ohnehin im Stack. Da stehen wir.

Docker plus gVisor (--runtime=runsc) wird zur richtigen Wahl, wenn Sie den Upload von Skills durch nicht vertrauenswürdige Autoren erlauben, die Ausführung für nicht authentifizierte Benutzer öffnen oder mit Daten arbeiten, bei denen der Schadensradius eines Kernel-Ausbruchs zählt. Es ist ein Drop-in-Ersatz: ein Paket installieren, ein Flag ergänzen. Google Cloud Run isoliert damit seine Mandanten. Die Selbsteinschätzung von Claude Code war hier ehrlich: gVisor im ursprünglichen Vergleich nicht zu nennen, war ein echtes Versäumnis. Es ist der Hebel, um „die Sandbox später zu verstärken, ohne die Architektur umzubauen“.

Firecracker wird zur richtigen Wahl, wenn Sie Skills für viele Mandanten gleichzeitig ausführen, die einander nicht vertrauen, oder wenn ein Compliance-Review klar für Isolation auf Hypervisor-Ebene spricht. Die Betriebskosten rechtfertigt dann das Bedrohungsmodell, nicht der Wunsch nach Defense-in-Depth als ästhetische Vorliebe.

Externe Dienste (e2b.dev, Modal, Cloud Run) werden zur richtigen Wahl, wenn die Betriebslast der eigenen Ausführungsinfrastruktur die Kosten pro Aufruf übersteigt, typischerweise bei einem kleineren Team, das Docker gar nicht betreiben will, oder bei einem sehr sprunghaften Durchsatzprofil, das sich lokal schwer dimensionieren lässt.

Keiner der Auslöser für die höheren Sprossen ist für unser Projekt in den nächsten sechs bis zwölf Monaten plausibel. Tritt doch einer ein, ist gVisor die erste Eskalationsstufe, nicht Firecracker. Vergleichen sollte man die beiden dann direkt, denn je nach Auslöser kann gVisor immer noch die richtige Antwort sein.

Was es gekostet und was es gebracht hat

Die gesamte Implementierung, also die Docker-Version, die wir gebaut und dann ersetzt haben, und die UID-im-Container-Version, die wir ausgeliefert haben, kostete zwei Tage Arbeitssitzungen mit Claude Code. Die Docker-Version lehrte uns Dinge, die die UID-Version brauchte (den Vertrag des run_code-Tools, den Berechtigungsdialog, den Audit-Trail, den Ablauf für Datei-Upload und -Download). Der Schmerz beim Verdrahten lehrte uns, dass die Komplexität des Docker-Ansatzes in keinem Verhältnis zum Problem stand. Und ein völlig unabhängiges Gedankenexperiment über macOS-Benutzerkonten für Agenten führte uns zu der Lösung, die wir von Anfang an hätten sehen sollen.

Wenn es ein allgemeines Muster gibt, dann dieses: Ehrliche Design-Iteration hängt davon ab, dass man bereit ist, die eigene frühere Empfehlung umzudrehen, sobald neue Informationen auftauchen. Die erste Runtime-Wahl war brauchbar, aber überdimensioniert. Die erste Scope-Entscheidung fühlte sich sauber an und war falsch. Das erste Berechtigungsmodell fühlte sich clever an und war falsch. Der Ansatz mit iptables und User-Namespaces sah korrekt aus und war still kaputt. Jede Korrektur machte das Design besser. Keine davon wäre mit einer „Wir halten uns an den Plan“-Mentalität passiert.

Das Unix-Mehrbenutzermodell ist das am stärksten vernachlässigte Isolationsprimitiv in der aktuellen Welle der KI-Agenten-Infrastruktur. Viele greifen zu Containern, MicroVMs und Cloud-Sandboxes, allesamt legitime Werkzeuge für legitime Bedrohungsmodelle, während das Betriebssystem, auf dem sie arbeiten, eine praxiserprobte, gut verstandene und völlig ausreichende Isolationsschicht für Benutzer mitbringt, und das seit der Zeit, bevor das erste Ethernet-Kabel eingesteckt wurde. Sie genügt nicht für jedes Szenario. Für eine Workshop-App auf einem einzelnen VPS ist sie aber mehr als genug: Sie ist die richtige Antwort. Wir haben zwei Tage und drei Irrwege gebraucht, um das zu sehen.

Aus dem Englischen übersetzt: zum Originalbeitrag

Gestalten Sie die Zukunft Ihres Unternehmens mit KI

Tauchen Sie ein in unsere spannenden Workshops und rüsten Sie Ihr Team mit den Werkzeugen und dem Wissen aus, um im Zeitalter der KI führend zu sein.

Kontaktieren Sie uns
Scroll to top