Ich habe claudebox gebaut, weil es mir Brustschmerzen bereitete, Claude Code frei auf meinem Host herumlaufen zu lassen. Es hat funktioniert. Es hat eine HTTP-API bekommen, einen OpenAI-kompatiblen Endpoint, einen MCP-Server, einen Telegram-Bot und einen Cron-Scheduler. Monate an Arbeit, alles davon nützlich.
Dann tauchte pi-coding-agent auf und ich wollte exakt dasselbe für ihn. Dann brachte OpenAI die Codex-CLI raus und ich wollte es ein drittes Mal.
Und genau da habe ich mich hingesetzt und mir ernsthaft angeschaut, was ich da eigentlich tun würde: einen FastAPI-Server, einen Telegram-Bot mit Workspaces pro Chat, einen Cron-Scheduler mit Verzeichnissen für die Historie und einen MCP-Server in ein zweites Repo kopieren. Dann in ein drittes. Drei Kopien derselben neunhundert Zeilen, die in der Sekunde auseinanderdriften, in der ich in einer einen Bug fixe und die anderen vergesse. Drei Stellen zum Patchen, wenn Telegram eine Rendering-Regel ändert. Drei Testsuites, die identische Logik gegen verschiedene Agenten testen.
Fick das. Nichts an dieser Installation ist Claude-spezifisch. Nichts davon ist pi-spezifisch. Es läuft alles auf „nimm einen Prompt, ruf irgendeine CLI auf, fang auf was zurückkommt, gib es dem, der gefragt hat“ hinaus. Der Agent ist ein Detail. Die Oberflächen sind das Produkt.
Also habe ich sie herausgerissen und in ein Basis-Image gesteckt. Das ist aicodebox.
Das Problem Jedes „Setz Einen Agenten Ins Netz“-Projekts
Jedes dieser Werkzeuge macht denselben Fehler, meines eingeschlossen, beim ersten Mal: es schweißt den Transport ans Gehirn.
Du willst deinen Agenten über HTTP erreichbar haben, also schreibst du einen HTTP-Server, der weiß, wie man genau dieses Binary aufruft, genau dieses Ausgabeformat parst und genau diesen Satz Flags behandelt. Sechs Monate später gibt es einen besseren Agenten, und dir gehört ein Haufen Infrastruktur, der nur mit dem alten spricht.
Die Symptome sind immer dieselben:
- Upgrades per Fork und Metzelei. Du willst dieselben Oberflächen für einen anderen Agenten? Forke das Repo, suche jede Stelle, an der der Name des alten Binarys hartkodiert ist, und hoffe, dass du sie alle erwischt hast.
- Bugfixes, die auseinanderlaufen. Der Markdown-Renderer für Telegram hat einen Bug. Du fixt ihn in einem Repo. Die anderen zwei behalten den Bug einen Monat lang, bis es dir wieder einfällt.
- Inkonsistente Oberflächen. Eine Box hat asynchrone Runs, eine andere nicht. Eine hat einen OpenAI-Endpoint, eine andere die Hälfte davon. Nichts komponiert, weil sich nichts auf eine Form auf der Leitung einigt.
- Agent-Lock-in aus Versehen. Nicht weil sich jemand fürs Einsperren entschieden hätte, sondern weil die Installation um die Macken eines einzigen Binarys herumgewachsen und dort versteinert ist.
Die Lösung ist kein besserer Wrapper. Sie ist das Eingeständnis, dass der Wrapper nicht wissen sollte, was er umwickelt.
Wie Der Scheiß Tatsächlich Funktioniert
aicodebox ist ein Docker-Basis-Image. Du forkst es nicht. Du machst FROM darauf.
FROM psyb0t/aicodebox
RUN npm install -g @earendil-works/[email protected]
COPY mypkg /opt/mypkg
RUN pip3 install --break-system-packages /opt/mypkg
ENV AICODEBOX_ADAPTER=mypkg.adapter:MyAdapter
AICODEBOX_AGENT_BINARY=piDas ist die gesamte Integration. Die npm install-Zeile ist nebensächlich, so wird pi-coding-agent eben ausgeliefert. Es könnte genauso gut ein apt-get install sein, ein aus einer Go-Build-Stage kopiertes Binary oder irgendetwas anderes, das ein Executable auf den PATH legt; siehe wie der Agent installiert wird weiter unten. Der Basis gehört jede Netzwerkoberfläche. Dein Adapter übersetzt „führ diesen Prompt aus“ in das, was die CLI deines Agenten erwartet. Ein neuer Agent landet an einem Nachmittag, und er landet mit einer HTTP-API, einem OpenAI-kompatiblen Endpoint, einem MCP-Server, einem Telegram-Bot und einem Cron-Scheduler bereits dran, weil die nicht mehr deine sind zu schreiben.
Was tatsächlich im Image steckt:
- Das Betriebssystem: Ubuntu 24.04, ein
aicode-Benutzer mit UID 1000, passwortlosem sudo und Mitgliedschaft in der docker-Gruppe. - Die Runtimes: Node.js 24 LTS (die meisten Agenten kommen als npm-Pakete), Python 3.14 und Docker CE mit buildx und compose, für den Fall, dass dein Agent beschließt, eigene Container gebären zu müssen.
- Das Paket:
aicodebox, der Adapter-Vertrag plus vier Modus-Dispatcher. Reines Python, null Seiteneffekte, bis du tatsächlich einen Modus hochfährst. - Der Zustand: Overrides pro Chat und die Cron-Historie leben unter
$HOME/.aicodebox/. Mach einen Bind-Mount darauf, wenn es den Container überleben soll. Das Paket selbst speichert nichts.
Der Adapter-Vertrag Sind Zwei Methoden
Das ist der Teil, auf den ich wirklich stolz bin, weil er klein ist. Alles läuft über eine einzige Schnittstelle, und ihre Pflichtoberfläche besteht aus zwei Methoden.
from aicodebox.adapters.base import AgentAdapter, RunRequest, RunResult, StreamEvent
class MyAdapter(AgentAdapter):
name = "my-agent"
available_models = ["fast", "smart"]
available_thinking_levels = ["off", "low", "high"]
def build_argv(self, req: RunRequest) -> list[str]:
argv = ["my-agent", "-p", req.prompt]
if req.model: argv += ["--model", req.model]
if req.workspace: argv += ["--cwd", req.workspace]
return argv
def parse_output(self, stdout: str, req: RunRequest) -> RunResult:
return RunResult(text=stdout.strip(), raw_stdout="", raw_stderr="", exit_code=0)build_argv sagt, wie man das Ding aufruft. parse_output sagt, wie man liest, was zurückkam. Das ist der Vertrag. Alles andere, validate, build_env, translate_auth, post_validate_json, parse_events, parse_stream_event, ist optional, mit Defaults, die das langweilige Richtige tun.
parse_stream_event ist eine Erwähnung wert: der Default lautet „ein Delta pro stdout-Zeile“, was für ein Binary, das einfach Prosa ausspuckt, völlig in Ordnung ist. Überschreib es nur, wenn dein Agent einen strukturierten JSON-Eventstream ausgibt und du im OpenAI-Streaming Granularität pro Token oder pro Tool willst. Die meisten Adapter werden sich die Mühe nicht machen.
Der Adapter wird beim ersten Aufruf aufgelöst und für die Lebensdauer des Prozesses gecacht. Jeder Modus zieht denselben, also ist das, was build_argv zu fahren weiß, genau das, was über HTTP, MCP, Telegram und Cron nach außen geht. Null Integrationsarbeit pro Modus. Einmal schreiben, fünf Wege hinein bekommen.
Vier Modi, Du Setzt Ein Flag
Modi sind Umgebungsvariablen. Setz das Flag, der Entrypoint fährt diesen Modus hoch. Kein Flag, kein Modus, der Container lässt deinen Agenten einfach interaktiv laufen wie eine normale Shell.
Die Vordergrundmodi (API, Telegram, Cron) schließen sich gegenseitig aus, mit einer bewussten Ausnahme: Telegram und Cron teilen sich einen Prozess, weil der Cron als Thread innerhalb des Telegram-Bots läuft. API gewinnt, wenn du sie neben etwas anderes setzt. MCP ist unabhängig und koexistiert mit jedem davon.
Der API-Modus
AICODEBOX_API_MODE=1 fährt FastAPI auf :8080 hoch.
POST /run # sync run
POST /run {"async": true} # returns {runId, status} immediately
GET /run/result?runId=X # poll an async run
DELETE /run/{id} # kill an in-flight run
GET|PUT|DELETE /files/{path} # workspace file CRUD
POST /v1/chat/completions # OpenAI-compatible
GET /v1/models # model list from the adapter
POST /mcp # MCP, when AICODEBOX_MCP_MODE=1Ein einziges Flag bestimmt die Form der Antwort. POST /run ohne jsonSchema gibt dir die magere Variante: {runId, workspace, exitCode, text}. Nur die Prosa. Übergib ein jsonSchema und du bekommst die volle Diagnoseoberfläche, text, json, events, sessionId, usage, attempts. Unter der Haube wird der Agent im json-verbose-Modus aufgerufen, seine Ausgabe dekodiert und gegen dein Schema validiert.
Das war eine Weile lang das ganze Design: Schema heißt volle Oberfläche, kein Schema heißt mager, zwei Formen auf der Leitung und ein Flag. Es hielt, bis es nicht mehr hielt. Den rohen Eventstream zu wollen, ohne auch ein Schema zu wollen, ist ein echter Wunsch, und die alte Form zwang dich, ein Schema zu erfinden, um ihn zu bekommen. Also gibt es seit v0.14.6 einen zweiten Regler, eventMode, und es ist die ehrliche Reparatur statt eines Flags, das den ersten heimlich verbiegt: auto behält das alte Verhalten, none wirft Events komplett weg, und full gibt dir die Aufzeichnungen des Providers unangetastet zurück, in einem stabilen Umschlag {sequence, attempt, backend, eventType, event}. Die Event-Aufbewahrung ist jetzt unabhängig davon, ob du strukturierte Ausgabe verlangt hast. Wenn ein Schema-Retry drei Versuche verbrennt, bekommst du die Aufzeichnungen aller drei, was genau der eine Moment ist, in dem du sie wirklich willst.
Retries sind ehrlich darüber, was sie kosten. Bei einem Parse- oder Validierungsfehler prompted der Wrapper bis zu dreimal nach (JSON_RETRY_MAX = 3), mit der vorherigen schlechten Ausgabe und dem konkreten Fehler. Wenn alle drei scheitern, ersetzen parseError und jsonRetries das json, aber text, events, sessionId, usage und attempts kommen trotzdem zurück, denn ein gescheiterter strukturierter Lauf ist genau der Moment, in dem du die Diagnose brauchst.
Und usage ist die Summe über alle Versuche, nicht der letzte. Dein Provider stellt dir pro Versuch in Rechnung; nur den finalen Versuch zu melden wäre eine Lüge. attempts trägt die Aufschlüsselung pro Versuch, damit du „Retry 2 von 3 hat so viel gekostet“ anzeigen oder weiterverrechnen kannst.
Der Retry-Trick, den ich am liebsten mag. Schema-Anfragen, die keinen Workspace benennen, bekommen einen ephemeren pro Anfrage unter /tmp/aicodebox/<uuid>/, in einem finally aufgeräumt. Weil die Session bestehen bleibt, schicken Retries einen minimalen korrigierenden Prompt, den Fehler, eine Anweisung, das Schema, statt deinen kompletten ursprünglichen Input erneut abzuspielen. Bei einem großen Prompt sind das ungefähr hundertmal weniger Input-Kosten pro Retry. Wenn du doch deinen eigenen Workspace übergibst, fällt es auf Retries mit frischer Session zurück, die die Aufgabe neu formulieren: sicher in jedem Workspace, nur teurer.
Der OpenAI-kompatible Endpoint
POST /v1/chat/completions, mit und ohne Streaming. Richte LiteLLM, Open WebUI oder irgendein OpenAI-SDK darauf und dein Coding-Agent taucht als Modell auf.
Strukturierte Ausgabe läuft über das Standardfeld, response_format mit {"type":"json_object"} für permissiv oder {"type":"json_schema", ...} für echte strukturierte Ausgaben. Es gibt einen x-aicodebox-json-schema-Header als Rückfalloption für Clients, die das Body-Feld nicht setzen können; der Body gewinnt, wenn beide da sind. Aufgebrauchte Retries liefern 422. Ein abgestürzter Agentenprozess liefert 500 mit Exit-Code und stderr in detail.
Client-ausgeführtes Tool-Calling. Schick ein Standard-tools-Array und die Box verhält sich wie ein ganz normales Function-Calling-Modell: sie antwortet mit tool_calls und finish_reason: "tool_calls", dein Client führt das Tool aus und schickt das role: "tool"-Ergebnis zurück, die Schleife läuft weiter. Zustandslos, du schickst die volle Historie jede Runde neu, exakt wie bei OpenAI. tool_choice unterstützt auto / none / required / eine benannte Funktion.
Und tools komponiert mit response_format. Eine Tool-Call-Runde gibt Tool-Calls zurück und wird bewusst nicht gegen das Schema geprüft; die finale Antwortrunde des Modells wird mit Retries gegen dein Schema validiert und kommt als kanonisches JSON zurück. Ein agentischer Multi-Tool-Ablauf kann also trotzdem in einer strukturierten Antwort enden, was genau das ist, was du willst, wenn du das Ding in eine Pipeline hängst.
Ein ehrlicher Vorbehalt: Streaming mit tools oder response_format ist gepuffertes SSE. Die vollständige Antwort wird berechnet und dann in einem Rutsch als Eventstream abgespielt, Rollen-Chunk zum Öffnen, ein Delta, Abschluss-Chunk, [DONE]. Es ist ein gültiger Stream, er ist nur nicht Token-inkrementell. Normaler Chat streamt weiterhin ordentlich.
Der Telegram-Modus
AICODEBOX_TELEGRAM_MODE=1 plus ein Bot-Token. Text geht rein, ein Agentenlauf passiert, die Antwort kommt gestückelt zurück und von Markdown in Telegrams HTML-Dialekt gerendert. Uploads, Dokumente, Fotos, Video, Sprache, landen im Workspace dieses Chats. Der Agent schiebt Dateien zurück, indem er [SEND_FILE: relative/path] in seiner Ausgabe ausgibt.
Overrides pro Chat für /model, /effort, /system_prompt und /append_system_prompt, auf Platte persistiert. /cancel killt den laufenden Run, /reload liest das YAML neu, /config kippt die zusammengeführte Config aus, /fetch zieht eine Datei aus dem Workspace, /status listet beschäftigte Chats.
Das Detail, das es im Alltag brauchbar macht: auf eine vom Cron abgefeuerte Nachricht zu antworten injiziert Anweisung und Ergebnis dieses Jobs in den Kontext, sodass deine Nachfrage für den Agenten tatsächlich Sinn ergibt, statt bei ihm anzukommen, ohne dass er eine Ahnung hätte, wovon du redest.
Der Cron-Modus
Croniter-Zeitpläne mit sechs Feldern, Workspace pro Job, optionale Telegram-Benachrichtigung.
jobs:
- name: morning-report
schedule: "0 0 9 * * *"
instruction: |
Summarize yesterday's git activity in {workspace}.
workspace: shared
telegram_chat_id: -100123
model: claude-sonnetJeder Lauf bekommt sein eigenes Historienverzeichnis, meta.json, stdout.log, stderr.log, result.txt und telegram.json, falls er benachrichtigt hat. Dann bekommt der Prompt des nächsten Laufs einen Hinweis, der auf dieses Verzeichnis zeigt.
Dieses letzte Stück ist klein und es verändert, was diese Jobs sein können. Der Agent kann seine eigene vergangene Ausgabe lesen, ohne dass du irgendetwas von dieser Installation baust. „Was hat sich seit gestern geändert“, „ist das regrediert“, „vergleich mit letzter Woche“, das alles funktioniert, weil die Historie auf Platte liegt und dem Agenten gesagt wurde, wo er nachschauen soll.
Der MCP-Modus
AICODEBOX_MCP_MODE=1. Im API-Modus hängt er sich unter /mcp auf denselben Port, ohne zusätzlichen Prozess. Unter Telegram, Cron oder einfachem Passthrough läuft er als uvicorn-Sidecar auf AICODEBOX_MCP_MODE_PORT (Standard 8081).
Fünf Tools: run_prompt, list_files, read_file, write_file, delete_file. Richte Claude Desktop, Cursor oder einen anderen Agenten darauf und dein Coding-Agent wird zu einem Tool, das andere Agenten aufrufen können.
Die Auth ist AICODEBOX_MCP_MODE_TOKEN, ein eigener Bearer, ohne Rückfall auf das API-Token. Das ist bewusst so. MCP ist eine getrennte Oberfläche mit getrennter Exposition, und den API-Bearer dort still zu akzeptieren hieße, jedem API-Client ein Werkzeug zur Agentenausführung in die Hand zu drücken, für das er nie vorgesehen war. Es gibt einen ?apiToken=-Query-Parameter für Clients, die keine Header setzen können.
Konfiguration, Mit Einer Echten Konvention
Alles läuft über Umgebungsvariablen, und die Benennung folgt einer einzigen Regel: <MODE>_MODE ist das An/Aus-Flag, <MODE>_MODE_<KNOB> ist dessen Konfiguration, und alles, was nicht an einen Modus gebunden ist, steht nackt da.
AICODEBOX_ADAPTER # required — pkg.module:Class
AICODEBOX_AGENT_BINARY # required — the CLI binary name
AICODEBOX_WORKSPACE # default /workspace
AICODEBOX_AVAILABLE_MODELS # required for API mode
AICODEBOX_API_MODE # 0/1 + _PORT, _TOKEN
AICODEBOX_TELEGRAM_MODE # 0/1 + _TOKEN, _CONFIG, _OVERRIDES
AICODEBOX_CRON_MODE # 0/1 + _FILE
AICODEBOX_MCP_MODE # 0/1 + _PORT, _TOKENDie Modellliste löst sich in zwei Schritten auf: AICODEBOX_AVAILABLE_MODELS gewinnt, wenn sie gesetzt ist, sonst fällt es auf das zurück, was dein Adapter in seiner Klassenvariable available_models deklariert. Die Umgebungsvariable ist also nicht streng verpflichtend, sie ist der Override. Deklarier die Liste im Adapter und du musst sie nie setzen.
Was verpflichtend ist: dass eine der beiden etwas produziert. Kommen beide leer zurück, weigert sich der API-Modus zu starten, loggt den Grund und geht mit einem Exit-Code ungleich null raus, statt kaputt hochzufahren. Das ist die richtige Entscheidung: /v1/models braucht eine echte Liste und es gibt keinen sicheren Rückfall, weil der Name des Adapters kein Modellname ist. Lieber laut beim Boot krachen, als eine Müll-Modellliste auszuliefern, die drei Schichten weiter unten den Client von jemandem zerlegt.
Zwei Images, und die Kinder hörten auf, dieselbe Toolchain neu zu bauen
Die Basis kommt jetzt in zwei Varianten. psyb0t/aicodebox:latest ist die oben beschriebene minimale, und psyb0t/aicodebox:latest-full (versionsgetaggt als v0.15.0-full) trägt die gemeinsame Entwicklungs-Toolchain: Go, Node, Python, Editoren, Diagnosewerkzeuge, Datenbankclients, Ops-Tooling.
Diese Aufteilung existiert wegen eines Duplikationsproblems, das still wieder aufgetaucht war. Jedes Kind-Image, das eine echte Entwicklungsumgebung wollte, installierte dasselbe Go, dasselbe Node, dasselbe Python, dasselbe psql und redis-cli, aus dem eigenen Dockerfile und den eigenen Lockfiles. Drei Kopien der Toolchain, die exakt so auseinanderdrifteten wie einst die drei Kopien des HTTP-Servers. Dieselbe Krankheit, eine Schicht tiefer.
Also zog es in die Basis. Die Kinder erben sie, indem sie von der full-Variante ausgehen, statt sie selbst zu bauen, und das Aufräumen war alles andere als subtil: codexbox löschte rund 15.800 Zeilen eigener Toolchain-Lockfiles, claudeboxs Dockerfile.full verlor 173 Zeilen. make build-full, make build-all und make test-full-image decken die Variante ab, und die CI veröffentlicht zuerst das minimale Image und baut dann das volle darauf.
Die Familie
Drei Images laufen gerade auf dieser Basis, alle derzeit auf psyb0t/aicodebox:v0.15.1 festgenagelt:
- claudebox: Claude Code. Seine v2.0.0 war ein kompletter Rebase auf diese Basis; es steuert jetzt
ClaudecodeAdapterbei und erbt jede Oberfläche. - pibox: pi-coding-agent, auf welches LLM du willst gerichtet. Das ist das Referenzkind: es benutzt die Basis unverändert und ergänzt
PiAdapter. - codexbox: OpenAIs Codex-CLI, über
CodexAdapter.
Wie der Agent tatsächlich installiert wird
Alle drei installieren ihren Agenten zufällig mit npm install -g, aber lies das nicht als das Rezept, es liegt nur daran, dass claude-code, pi und codex allesamt als npm-Globals ausgeliefert werden. Die Basis hat keine Meinung dazu, wie dein Binary dort hingekommen ist.
Der eigentliche Vertrag besteht aus zwei Dingen: das von AICODEBOX_AGENT_BINARY benannte Binary muss auf dem PATH existieren, und dein Adapter-Paket muss importierbar sein. Das war’s. Was dich dorthin bringt, ist deine Sache:
- npm:
RUN npm install -g @vendor/[email protected]. Node 24 LTS steckt schon in der Basis, genau deshalb haben die drei vorhandenen Kinder diesen Weg genommen. - apt: es ist Ubuntu 24.04 mit sudo und einem funktionierenden apt.
RUN apt-get update && apt-get install -y your-agentgeht völlig in Ordnung, wenn tatsächlich jemand ein deb ausliefert. - pip / uv: Python 3.14 ist da, und uv auch, auf Digest festgenagelt. Genau so installiert ohnehin jedes Kind seinen Adapter:
uv pip install --system --break-system-packages --no-deps /opt/yourpkg. - Ein kompiliertes Binary: Go, Rust, was auch immer. Beachte, dass die Basis keine Go- oder Rust-Toolchain mitbringt, erwarte also nicht, dass
go installauf Anhieb funktioniert. Entweder bringst du die Toolchain in deiner eigenen Schicht mit, oder du machst das Vernünftige und holst perCOPY --from=aus einer Build-Stage, damit der Compiler nie im Runtime-Image landet:
FROM golang:1.26 AS build
RUN go install github.com/someone/[email protected]
FROM psyb0t/aicodebox
COPY --from=build /go/bin/some-agent /usr/local/bin/some-agent
COPY myadapter /opt/myadapter
RUN uv pip install --system --break-system-packages --no-deps /opt/myadapter
ENV AICODEBOX_ADAPTER=myadapter.adapter:MyAdapter
AICODEBOX_AGENT_BINARY=some-agentDieselben fünf Oberflächen kommen hinten wieder raus. Die Basis erfährt nie, in welcher Sprache dein Agent geschrieben wurde, und das ist der ganze Sinn, sie ruft nur einen Namen vom PATH auf und reicht die Bytes an dein parse_output weiter.
pibox ist das ehrliche Minimum. Den Agenten per npm installieren, das Adapter-Paket per uv pip install, exakt drei Umgebungsvariablen setzen, AICODEBOX_ADAPTER, AICODEBOX_AGENT_BINARY und ein PIBOX_IMAGE_VARIANT-Label, und ihm einen kleinen gebrandeten Entrypoint mitgeben, der PIBOX_* auf AICODEBOX_* aliast. Das ist das ganze Kind-Image. Nicht ohne Grund ist es die Referenzimplementierung.
Die anderen beiden sind größer, und das ist der interessante Teil. claudebox setzt fünf Umgebungsvariablen, codexbox vier, und beide liefern zusätzlich zum Adapter ein Launcher-Skript mit. Nicht weil die Basis undicht wäre, sondern weil jeder Agent sein eigenes Zustandsproblem mitschleppt, von dem eine agentenagnostische Basis nichts wissen darf:
- claudebox setzt
CLAUDE_CONFIG_DIR, damit Claude Code.claude.jsonund seine Credentials auf den Bind-Mount schreibt statt in ein nicht gemountetes$HOME, sonst machst du bei jedem Neuerstellen des Containers die Themenauswahl und das Login erneut. Nur claudebox weiß, dass die Nutzlast Claude Code ist, also kann nur claudebox das setzen. - codexbox setzt
CODEX_HOMEaus demselben Grund (ein API-Key oder ein OAuth-Token eines ChatGPT-Abos, das ein Neuerstellen überleben muss) und muss es zur Build-Zeit permkdiranlegen und perchownübereignen, weil codex beim Start abbricht, wennCODEX_HOMEauf ein Verzeichnis zeigt, das nicht schon existiert. - Beide zeigen mit
AICODEBOX_AGENT_BINARYauf ein Launcher-Skript statt direkt auf das Agenten-Binary. Das von claudebox stellt die interaktiven Defaults wieder her, die die agentenagnostische Basis bewusst hat fallen lassen:--continuemit einem Rückfall,--permission-mode bypassPermissionsund die Always-Skills-Injektion über--append-system-prompt. Server-Modi umgehen den Launcher vollständig und bauen argv über den Adapter.
Das ist die wirkliche Form dieser Abstraktion, und ich beschreibe sie lieber genau, als so zu tun, als wäre jedes Kind vier Zeilen lang. Der Basis gehört alles, was nicht agentenspezifisch ist. Was in jedem Kind übrig bleibt, ist genau das, was es ist: der Adapter, der weiß, dass dieses Binary -p will, während jenes --prompt will, dass eines stream-json ausgibt, während ein anderes reinen Text druckt, plus die Config-Persistenz-Macke, die dir genau dieser Agent aufzwingt.
Alles andere, die API, der OpenAI-Shim mit seinen Schema-Retries und dem Tool-Calling, der Telegram-Bot, der Cron-Scheduler mit seiner Historie, der MCP-Server, wird geerbt. Du fixt einen Bug in der Basis, taggst, ziehst den Pin in drei Kindern hoch, fertig. Was der ganze Sinn ist und der Grund, warum claudebox v2 überhaupt existiert.
Eine Davon Starten
docker run --rm -p 8080:8080
-e AICODEBOX_API_MODE=1
-e AICODEBOX_API_MODE_TOKEN=$(openssl rand -hex 16)
-e AICODEBOX_AVAILABLE_MODELS=fast,smart
-v "$PWD/workspace:/workspace"
your/child-image:latestDann redest du damit, als wäre es OpenAI, oder haust direkt auf /run, oder richtest einen MCP-Client darauf, oder fasst HTTP nie an und lässt den Cron es einfach um neun Uhr morgens abfeuern.
Die Container-Neustartschleife, die nach überhaupt nichts aussah
Der beste Bug, den dieses Ding hervorgebracht hat, weil jedes Signal, das er dir gab, gelogen war.
Symptom: im api-Modus startet der Container periodisch neu. Kein Crash, kein OOM, kein Stacktrace. Der Exit-Code ist 0. Die Schleife korreliert mit der Request-Aktivität statt mit Speicher oder Uptime, was dich komplett an der falschen Stelle suchen lässt.
Was tatsächlich passierte: der Subprozess des Agenten teilte sich die Prozessgruppe mit dem, was den Server gestartet hatte. Im api-Modus ist der Server PID 1. Wenn also die Agenten-CLI, oder irgendein Tool, das der Agent selbst gebar, ein SIGTERM oder SIGINT an diese gemeinsame Gruppe schickte, fing uvicorn es ebenfalls ab und fuhr herunter. Sauber. Absichtlich. Exit 0, Arbeit erledigt.
Und du hast es nie gesehen, weil uvicorn.run(..., log_config=None) seine eigenen Shutdown-Zeilen unterdrückt. Ein ordentliches, stilles, vollkommen korrektes Herunterfahren, das die Restart-Policy des Containers dann wieder rückgängig machte, immer und immer wieder.
Die Reparatur ist ein einziges Flag in aicodebox/shared/runner.py, start_new_session=True beim Starten des Agenten, sowohl im synchronen run()-Pfad als auch im Streaming-Pfad run_stream(). Eigene Session, eigene Prozessgruppe, Signale wandern nicht mehr nach oben.
Regressionstests belauern beide Spawn-Primitive und prüfen, dass das Flag übergeben wird, sodass ein Weglassen an einer der beiden Stellen den Build zerlegt, statt still eine Schleife wieder einzuführen, die niemand aus den Logs lesen kann.
Nagle v0.14.5 fest, nicht v0.14.4. Dieselbe Reparatur, aber pyproject.toml stand von v0.14.1 bis v0.14.3 auf 0.14.0, während das Makefile den Image-Tag daraus ableitet, und v0.14.5 ist die Neuveröffentlichung, in der Datei und Tag sich endlich einig sind.
Unterm Strich
Der Agent, den du heute benutzt, ist nicht der Agent, den du in einem Jahr benutzen wirst. Das ist kein Pessimismus, das ist einfach der Release-Takt, dieser Bereich schreibt sich alle paar Monate selbst neu, und alles, was du eng an eine einzige CLI gekoppelt baust, ist ein Gerüst mit Verfallsdatum.
Also koppel dich nicht daran. Steck die Oberflächen in ein Basis-Image, steck den Agenten hinter einen Adapter mit zwei Methoden, und wenn der nächste landet, schreibst du vierzig Zeilen, statt neunhundert zu forken. Ich habe es jetzt dreimal gemacht. Das dritte Mal dauerte einen Nachmittag.
github.com/psyb0t/docker-aicodebox
Lizenziert unter WTFPL, denn ein Basis-Image, das genau dafür existiert, dass du es nicht forken musst, wäre eine dumme Sache, auf die man eine restriktive Lizenz klatscht.