pibox: Drei Umgebungsvariablen Waren Alles, Um pi-coding-agent an aicodebox Zu Hängen

Ich habe aicodebox als Abstraktion gebaut, ein Basis-Image, ein AgentAdapter-Vertrag, du steckst die CLI irgendeines Coding-Agenten hinein und bekommst die API/MCP/Telegram/Cron-Oberfläche geschenkt. Schöne Theorie. Theorien sind einen Scheiß wert, solange du nicht tatsächlich ein zweites, völlig anderes Agenten-Binary an den Vertrag verdrahtet und zugeschaut hast, wo es blutet. Also habe ich mir pi-coding-agent geschnappt, eine CLI, die ich nicht geschrieben habe und nicht kontrolliere, und sie durch den Adapter gezwungen. Das ist pibox. Es ist kein funktionsprotziges Flaggschiff-Image, es ist der Beweis, dass die Abstraktion keine Lüge ist, und inzwischen die Referenz, von der jedes andere Kind-Image abgeschrieben wird, das von Claude Code eingeschlossen.

Wenn du den aicodebox-Beitrag schon gelesen hast, kennst du den Pitch: Modi, Adapter, REST, OpenAI-Kompatibilität, MCP, Telegram, Cron, alles in der Basisschicht, alles kostenlos, sobald du einen Adapter schreibst. Das tippe ich hier nicht noch einmal. Dieser Beitrag handelt davon, was es tatsächlich gekostet hat, ein fremdes Agenten-Binary ohne Schummeln an diesen Vertrag zu schrauben, und davon, wie klein die Oberfläche am Ende wurde, nachdem ich aufgehört hatte, Scheiß hinzuzufügen, der nichts trug.

Was pi Dir Nicht Gibt

pi-coding-agent ist eine völlig ordentliche CLI. Sie ist außerdem, wie jede Agenten-CLI auf diesem Planeten, für einen Menschen an einem Terminal gebaut, nicht für einen Serverprozess, der strukturierte Metadaten zurückbraucht. Sie in aicodebox zu verdrahten hieß, jede einzelne der sehr vernünftigen, sehr terminalförmigen Annahmen von pi zu umgehen:

  • Zwei Ausgabemodi, einer davon für eine API unbrauchbar. pi hat --mode text und --mode json. Der Textmodus gibt dir die Worte des Assistenten und sonst absolut nichts, keine Session-ID, keinen Verbrauch, keine Events pro Turn. Gut für einen Menschen, nutzlos für eine Route, die Tokens abrechnen und Sitzungen fortsetzen muss.
  • Null native JSON-Schema-Durchsetzung. pi hat kein --schema-Flag, keinen Modus für strukturierte Ausgabe. Es redet einfach.
  • Keine native ANTHROPIC_BASE_URL-Unterstützung. pis eigene Doku sagt „nimm models.json“, das heißt, es liest die Umgebungsvariable nicht, über die ein Anthropic-kompatibler Proxy (Z.AI, OpenRouter, dein eigenes Gateway) zu funktionieren erwartet.
  • Ein eingebauter Provider, der dir still das Routing kapert. pis zai-Provider beansprucht automatisch jeden glm-*-Modellnamen, was heißt, dass eine Anfrage, die über deine ANTHROPIC_BASE_URL-Überschreibung hätte laufen sollen, leise zu einem Provider umgeleitet werden kann, den du nie verlangt hast.
  • Keine erstklassige MCP-aus-der-Workspace-Config-Unterstützung so wie Claude Code sie hat, also kein automatisches Aufgreifen einer .mcp.json aus dem Workspace.

Nichts davon ist pis Schuld, es ist eine CLI, sie macht CLI-Dinge. Aber „der Agent kann nichts dafür“ verschafft dir keine API. Irgendjemand muss übersetzen. Dieser Irgendjemand ist eine einzige Python-Klasse.

Drei Umgebungsvariablen Sind Die Ganze Verankerung

Hier ist der Teil, der pibox den Titel „Referenz“ tatsächlich verdient. Zieh das Dockerfile auf das herunter, was es pibox-spezifisch setzt, und du bekommst exakt drei Umgebungsvariablen:

ENV AICODEBOX_ADAPTER=pibox.adapter:PiAdapter \
    AICODEBOX_AGENT_BINARY=pi \
    PIBOX_IMAGE_VARIANT=minimal

Das war’s. Das ist die gesamte deklarative Oberfläche, die ein Kind-Image braucht: richte AICODEBOX_ADAPTER auf ein importierbares module:ClassName, sag der Basis, welchen Binary-Namen sie aufrufen soll, und die ganze Modus-Maschinerie, REST, OpenAI-Kompatibilität, MCP, Telegram, Cron, erwacht darum herum. PIBOX_IMAGE_VARIANT gehört überhaupt nicht zu diesem Vertrag, es beschriftet nur, in welchem der beiden veröffentlichten Images du gerade sitzt, dem minimalen oder dem vollen. Alles andere im Dockerfile installiert entweder das Agenten-Binary, oder das Python-Paket, das den Adapter implementiert, oder es ist Branding. Wenn du dein eigenes Kind-Image baust und das Dockerfile eine dritte Umgebungsvariable braucht, damit der Agent läuft, tust du etwas, wozu der Vertrag dich nie zwingen wollte, geh und lies die AgentAdapter-Klasse der Basis nochmal.

Der Rest des Builds ist mit Absicht langweilig: npm install -g --ignore-scripts @earendil-works/[email protected], festgenagelt, nicht @latest, weil „funktioniert heute“ und „funktioniert in sechs Monaten“ nicht dieselbe Behauptung sind, dann uv pip install --system --break-system-packages --no-deps /opt/pibox für das Adapter-Paket selbst (--no-deps, weil aicodebox ohnehin im Basis-Image steckt und es erneut aufzulösen vergeudete Arbeit ist).

Der Adapter: build_argv Ist Pflicht, Alles Andere Ist Eine Wahl

Die AgentAdapter-Klasse der Basis hat genau eine Methode, die NotImplementedError wirft, wenn du sie nicht überschreibst: build_argv. Jeder andere Hook, validate, build_env, translate_auth, parse_output, post_validate_json, parse_events, parse_stream_event, interactive_argv, passthrough_argv, auth_paths, bringt einen funktionierenden Default mit. PiAdapter überschreibt sie alle bis auf build_env und post_validate_json, weil pis CLI schräg genug ist, dass die Defaults Müll produzieren würden. Die zwei lässt er aus gutem Grund in Ruhe: build_env übernimmt schon alles, was translate_auth zurückgibt, und der Runner parst Schema-Output selbst. Hier ist, was dir jeder einzelne tatsächlich bringt, geprüft gegen pibox/pibox/adapter.py:

  • build_argv (Pflicht): ruft immer pi -p --mode json auf, nie --mode text, genau damit der Adapter den vollen Session-Event-Stream bekommt statt nackter Prosa. Die Session-Behandlung verzweigt dreifach: --session <id> beim Fortsetzen, --no-session für Flüchtiges, sonst --continue.
  • validate (optional, die Basis prüft nur output_format): weist einen thinking-Wert außerhalb von off/minimal/low/medium/high/xhigh zurück und weist tools_allowlist in Kombination mit no_tools als sich gegenseitig ausschließenden Unsinn zurück.
  • translate_auth (optional, der Default der Basis tut nichts): trotzdem überschrieben, gibt weiterhin nichts zurück, weil pi ANTHROPIC_API_KEY / OPENAI_API_KEY / OPENROUTER_API_KEY / GEMINI_API_KEY / ZAI_API_KEY nativ liest. Die Überschreibung existiert, um diese Tatsache im Code zu dokumentieren, nicht um das Verhalten zu ändern.
  • parse_output (optional, der Default der Basis trimmt stdout bloß als Klartext): läuft durch pis NDJSON, zieht session für die Session-ID, message_end für Assistententext und Verbrauch, und turn_end als Verbrauchs-Rückfall. Hier wird auch provider_error gefüllt: wenn ein Assistenten-Turn stopReason=error plus eine errorMessage trägt, pis Art, eine Ablehnung von oben, ein Rate Limit oder einen Auth-Fehler zu melden, fängt der Adapter das ab und reicht es über RunResult.provider_error weiter, damit die OpenAI-Route einen echten 400 zurückgeben kann statt einer 200 mit leerem Text.
  • parse_events (optional, der Default der Basis gibt eine leere Liste zurück): dekodiert jede NDJSON-Zeile als JSON für den Ausgabemodus json-verbose; kaputte Zeilen werden mit einer Warnung verworfen, nicht mit einem Absturz. Nur ist dieser Hook inzwischen toter Code: Seit aicodebox v0.16.0 ruft der Runner ihn nie mehr auf, weil eventMode: full jede rohe stdout-Zeile selbst aufbewahrt, ganz ohne Filterung im Adapter. Die Überschreibung liegt einfach rum, bis zur nächsten Major-Version des Adapter-Vertrags.
  • parse_stream_event (optional, der Default der Basis behandelt jede Zeile als rohes Text-Delta): dekodiert pis message_update.assistantMessageEvent-Stream, leitet nur text_delta auf die Leitung weiter und schluckt thinking_*– und Tool-Use-Deltas stillschweigend, damit das interne Nachdenken des Modells nie in das OpenAI-kompatible content-Feld leckt.
  • auth_paths (optional, der Default der Basis gibt nichts zum Persistieren zurück): listet pis echten Zustand auf, ~/.pi/agent/auth.json, settings.json, models.json, das sessions-Verzeichnis und pibox’ eigenen Provider-Marker pibox-provider.json, damit OAuth-Tokens und Sitzungshistorie ein docker start überleben, statt sich in Luft aufzulösen.

Es steckt außerdem ein Hack in build_argv, auf den ich nicht stolz bin, den ich aber vorbehaltlos verteidigen werde: wenn ANTHROPIC_BASE_URL gesetzt ist und der Aufrufer nicht schon einen Provider in extra_args gewählt hat, injiziert der Adapter zwangsweise --provider anthropic. Warum? Weil pis eingebauter zai-Provider automatisch jeden glm-*-Modellnamen beansprucht und deine Base-URL-Überschreibung komplett umgeht. Ohne das erzwungene Flag ignoriert pibox, auf einen Z.AI-kompatiblen Proxy gerichtet und nach einem glm-4.6-Modell gefragt, still deinen Proxy und redet direkt mit dem, was pis zai-Provider für richtig hält. Das ist die Sorte Bug, die jemanden einen Nachmittag und ein Support-Ticket kostet, bevor irgendwer bemerkt, dass der Verkehr den Proxy nie berührt hat.

Der Schema-Modus: pi Hat Keine Native Durchsetzung, Also Ist Es Ein Anbau am System-Prompt

pi validiert keine JSON-Schemas. Es hat kein Flag dafür. Wenn eine Anfrage also ein jsonSchema trägt, bleibt dem Adapter nur ein Zug: eine Anweisung an den System-Prompt hängen, die dem Modell auf einfachem Englisch sagt „antworte mit einem einzigen JSON-Dokument, das diesem Schema entspricht, keine Prosa, keine Codezäune“, und ihm das rohe Schema-JSON dazu reichen. Die gesamte tatsächliche Durchsetzung, das Ergebnis parsen, es gegen das Schema prüfen, bei Fehlschlag bis zu dreimal mit einem korrigierenden Prompt erneut versuchen, passiert in der aicodebox-Basisschicht, nicht in pibox. Die einzige Aufgabe des Adapters ist, das Modell Richtung Konformität zu lenken; ob das Modell tatsächlich konform ist, hat er nicht mitzureden.

Diese Trennung ist wichtig wegen dem, was danach kam. Der Schema-Retry-Helfer der Basis wurde schrittweise schlauer, ohne dass sich der Adaptercode von pibox überhaupt geändert hätte, und die Changelog-Kommentare im Dockerfile selbst lesen sich wie ein Verhaltenstagebuch der Basis, die sich unter einem völlig stabilen Adaptervertrag weiterentwickelt: Retries, die die ursprüngliche Aufgabe neu formulieren statt nur den Fehler (damit ein Schema, das eine Auswahl aus einem großen Enum verlangt, nicht blind erneut versucht), flüchtige Workspaces pro Anfrage, damit eine 100k-Token-Anfrage, die drei Retries braucht, grob 1,5k Tokens korrigierenden Mehraufwand zahlt, statt die vollen 100k dreimal abzuspielen, und, direkt aus dem v0.14.0-Changelog von aicodebox statt aus dem Dockerfile, stream:true in Kombination mit Tool-Calling oder Schema-Modus gibt keinen trockenen 400 mehr zurück. Es berechnet jetzt die vollständige Antwort ungestreamt und spielt sie als einen einzigen gepufferten SSE-Stream ab: ein Rollen-Chunk, ein Content- oder Tool-Calls-Delta, ein Abschluss-Chunk, [DONE]. Normaler Chat streamt weiterhin Token für Token. Nichts davon hat pibox/pibox/adapter.py berührt. Genau das ist der Sinn des Vertrags, das Kind-Image darf gar nicht wissen oder sich darum scheren, dass die Basis unter ihm schlauer geworden ist.

Die mcp-bridge-Erweiterung: pi das Config-Format von jemand anderem geben

pi liest eine Workspace-.mcp.json nicht nativ, so wie Claude Code es tut. pibox liefert eine TypeScript-Erweiterung mit, pibox/extensions/mcp-bridge/index.ts, die das für pi erledigt: beim Sitzungsstart liest sie die .mcp.json aus dem Workspace im claude-code-Schema (mcpServers.<name>.{command,args,env}), startet jeden Server über stdio, ruft listTools() auf und registriert jedes Tool bei pi unter einem bereinigten Namen mcp__<server>__<tool> über pi.registerTool(). Tool-Aufrufe werden an den MCP-Server weitergereicht und das Ergebnis kommt über denselben Kanal zurück, den pis eingebaute Tools benutzen, das Modell merkt keinen Unterschied.

Das npm install für diese Erweiterung passiert zur Build-Zeit, RUN cd /opt/pibox/extensions/mcp-bridge && npm install --omit=dev --ignore-scripts --no-audit --no-fund, nicht beim ersten Containerstart. Das ist eine bewusste Entscheidung: niemand will, dass sein erster Agentenlauf an einem npm-Resolve hängen bleibt. Ein init.d-Skript, pibox/init.d/10-pi-extensions.sh, verdrahtet den Pfad der vorinstallierten Erweiterung einmal pro Container (seit aicodebox v0.17.0 heißt das: in jedem neuen Container, nicht mehr einmal pro gemountetem State-Verzeichnis) in das extensions-Array von ~/.pi/agent/settings.json, idempotent, über einen jq-Merge, der mit unique dedupliziert, damit ein erneuter Lauf keine doppelten Einträge anhäuft.

Darin steckt außerdem ein Shutdown-Rennen, das Erwähnung verdient, weil es die Sorte Bug ist, die nur in Produktion auftaucht: pi -p hängt, nachdem es seine finale Antwort ausgegeben hat, wenn die gestarteten MCP-Server-Subprozesse die Event-Loop noch offen halten. Die Erweiterung lauscht auf session_shutdown, lässt das Schließen jedes MCP-Clients gegen einen Timeout von 2 Sekunden antreten und ruft dann, Gürtel und Hosenträger, eine halbe Sekunde später zwangsweise process.exit(0), weil manche Node-Versionen die Loop selbst dann am Leben halten, wenn close() längst aufgelöst hat. Ohne das würde ein einmaliger API-Aufruf einfach dasitzen, bis ihn etwas von außen killt.

pibox-entrypoint: 19 Aliase und eine Config-Neuerzeugung beim Boot

pibox-entrypoint.sh existiert einzig dazu, pibox seine eigene gebrandete Umgebungsvariablen-Oberfläche zu geben, ohne irgendeine Logik der Basis zu duplizieren. Es definiert eine Liste von 19 Suffixen, API_MODE, API_MODE_PORT, API_MODE_TOKEN, TELEGRAM_MODE, TELEGRAM_MODE_TOKEN, TELEGRAM_MODE_CONFIG, TELEGRAM_MODE_OVERRIDES, CRON_MODE, CRON_MODE_FILE, CRON_MODE_HISTORY_DIR, MCP_MODE, MCP_MODE_PORT, MCP_MODE_TOKEN, MCP_MODE_ALLOWED_HOSTS, MCP_MODE_ALLOWED_ORIGINS, WORKSPACE, AVAILABLE_MODELS, AVAILABLE_EFFORTS, CONTAINER_NAME, und für jedes davon kopiert es den Wert herüber, falls PIBOX_<suffix> gesetzt ist und AICODEBOX_<suffix> nicht. AICODEBOX_* gewinnt, wenn beide gesetzt sind, damit fortgeschrittene Nutzer nicht von den darunterliegenden Namen ausgesperrt werden. Die beiden Adapter-Auswahlvariablen, ADAPTER und AGENT_BINARY, sind bewusst von dieser Liste ausgenommen; die werden vom Dockerfile festgenagelt und sind nichts, was ein Nutzer zur Laufzeit überschreiben können sollte.

Der Entrypoint führt außerdem setup-provider-env.sh bei jedem einzelnen Boot aus, nicht nur beim ersten, und dieses „bei jedem Boot“ ist die Reparatur eines echten Bugs, keine Stilentscheidung. Das Skript erzeugt pis anthropic-Provider-Eintrag in ~/.pi/agent/models.json aus ANTHROPIC_BASE_URL / ANTHROPIC_MODEL bei jedem Containerstart neu. Früher lief es über init.d, das damals nur einmal pro gemountetem State-Verzeichnis feuerte, weil der Marker in ~/.aicodebox lag, in Ordnung für einen Wegwerfcontainer, kaputt in dem Moment, in dem jemand ~/.aicodebox oder ~/.pi als persistentes Volume einhängt, denn dann überlebt der Init-Marker den Neubau und eine geänderte ANTHROPIC_BASE_URL erreicht models.json still nie wieder. Es in den Entrypoint zu verschieben heißt, dass die Base-URL-Konfiguration bei jedem Start aufgefrischt wird, persistentes Volume hin oder her. aicodebox v0.17.0 hat diesen Marker inzwischen ins Dateisystem des Containers verlegt, also läuft init.d in jedem neuen Container sowieso wieder. Egal, dem Entrypoint ist scheißegal, wie die Basis das Init steuert.

v0.16.0: die Anthropic-Route ist jetzt nur eine von vieren

Alles oben ist um eine einzige Form herum geschrieben, pis anthropic-Provider auf den Endpoint zu biegen, auf den du ANTHROPIC_BASE_URL richtest. Das funktioniert weiterhin, und das läuft auch weiterhin, wenn du nur das setzt. Aber es ist jetzt der Altpfad.

v0.16.0 hat den Provider generisch gemacht. Fünf Variablen beschreiben jeden beliebigen HTTP-Endpoint: PIBOX_PROVIDER_BASE_URL, PIBOX_PROVIDER_API, PIBOX_PROVIDER_API_KEY, PIBOX_PROVIDER_MODEL, PIBOX_PROVIDER_NAME. Die mit API wählt die Form auf der Leitung, und davon gibt es vier: openai-completions, openai-responses, anthropic-messages, google-generative-ai. Was LiteLLM und Z.AI abdeckt, ohne so zu tun, als wäre eines von beiden Anthropic.

Der Zwangsinjektions-Hack in build_argv ist entsprechend erwachsen geworden. Er ist jetzt ein Selektor mit zwei Zweigen: nimm den konfigurierten Provider, falls es einen gibt, und injiziere dessen Modell als --model, sofern der Aufrufer keins gewählt hat, sonst fall auf anthropic zurück, wenn ANTHROPIC_BASE_URL gesetzt ist.

Setz PIBOX_PROVIDER_* und ANTHROPIC_BASE_URL gleichzeitig und pibox steigt aus, statt sich für eines zu entscheiden. Gut. Zwei halb konfigurierte Routing-Schemata, die sich widersprechen, sind genau der Fehlschlag, den du laut und beim Boot willst, nicht leise und drei Tool-Aufrufe tief.

Ein Detail, das es wert ist, geklaut zu werden: der Upstream-Schlüssel lebt ausschließlich in der Umgebung des Pi-Prozesses. Was in models.json landet, ist eine Referenz der Art $OPENAI_API_KEY, nicht das Geheimnis selbst, die Konfigurationsdatei auf der Platte bleibt also langweilig, falls jemand sie liest.

v0.18.0: endlich hat es einen Wrapper bekommen

Alles darunter war früher die einzige Art, es zu starten: handgeschriebene docker run-Zeilen, bei denen die Mounts und die Umgebung jedes Mal ausbuchstabiert wurden. claudebox und codexbox hatten beide einen Host-Wrapper und einen Installer. pibox nicht, weil es das unglamouröse Kind war und niemand dazu kam.

Jetzt hat es einen. install.sh legt einen pibox-Befehl auf deinen PATH, und der Wrapper erledigt die Container-Klempnerei, es in einem Verzeichnis zu starten ist also einfach pibox. Zehn eigene Variablen steuern den Wrapper: PIBOX_DATA_DIR, PIBOX_STATE_DIR und PIBOX_SSH_DIR dafür, wo der Zustand liegt, PIBOX_IMAGE und PIBOX_FULL für welches Image, PIBOX_DETACH für Läufe im Hintergrund, PIBOX_CONTAINER_NAME für den Containernamen (sonst aus dem Workspace-Pfad gebaut), PIBOX_MAX_MEM für das Speicherlimit (10g, solange du nichts anderes sagst), und PIBOX_ENV_* und PIBOX_MOUNT_* zum Durchreichen von Umgebung und Mounts. Der Installer nimmt PIBOX_INSTALL_DIR mit PIBOX_BIN_NAME, um zu entscheiden, wo der Befehl landet und wie er heißt. Seit v0.19.0 hält das State-Verzeichnis (~/.aicodebox, sofern PIBOX_STATE_DIR nichts anderes sagt) auch deine eigenen Hooks: Programme in seinem bin/ stehen ganz vorne im PATH, für pi in jedem Modus, für Init-Skripte und für docker exec-Shells, und init.d/*.sh läuft einmal pro neuem Container, nach den Init-Skripten des Images, als aicode mit passwortlosem sudo. Ein Skript, das abkackt, wird geloggt und der Rest läuft trotzdem, also schreib sie so, dass sie einen zweiten Lauf überleben, denn jeder neue Container führt sie wieder aus.

v0.17.0 hat ihm außerdem eine psyb0t/pibox:latest-full-Variante gegeben, gebaut auf dem Full-Image von aicodebox (damals aicodebox:v0.15.0-full, seit v0.19.0 v0.17.0-full), wohin die gemeinsame Toolchain aus Go, Node, Python, Datenbankclients und Ops-Werkzeug gewandert ist. pibox hat aufgehört, irgendetwas davon selbst zu bauen.

Und die Boxen können sich gegenseitig aufrufen

Installier pibox, claudebox und codexbox in dasselbe Verzeichnis und jeder Wrapper mountet alle drei, sich selbst eingeschlossen, schreibgeschützt unter /usr/local/bin/<name>, pi kann also eine Aufgabe an einen anderen Agenten oder an eine frische pi-Instanz weitergeben, ohne seine Sitzung zu verlassen. Verschachtelte Läufe tragen einen versionierten Host-Kontext, AICODEBOX_LAUNCH_CONTEXT_VERSION=1 plus AICODEBOX_HOST_HOME, AICODEBOX_HOST_WORKSPACE, AICODEBOX_HOST_WRAPPER_DIR und je Agent ein Home und einen Wrapper-Pfad, weil ein Pfad innerhalb eines Containers nichts über den Host aussagt. pibox schreibt überhaupt keine Auth-Dateien, das Einzige, was ein verschachtelter Lauf also auslässt, ist das mkdir für das pi-Home, das State- und das SSH-Verzeichnis, das ein Lauf auf oberster Ebene macht, weil das Host-Pfade sind und sie im aufrufenden Container anzulegen, würde den nur mit leeren Verzeichnissen zumüllen. AICODEBOX_ENV_* und AICODEBOX_MOUNT_* reichen an alle Boxen auf einmal weiter, und mit AICODEBOX_MANAGED_INSTALL=1 überspringen die Installer von claudebox und codexbox die Frage, ob der SSH-Schlüssel ersetzt werden soll, und behalten den vorhandenen, während der Installer von pibox gar nicht erst fragt und ihn sowieso nie ersetzt.

Der Bug, der die Modellliste zur Lüge machte

Erwähnenswert, weil der Abschnitt oben etwas verspricht, was v0.16.0 tatsächlich nicht geliefert hat. Du konntest mehrere Modelle in PIBOX_AVAILABLE_MODELS auflisten, aber nur das in PIBOX_PROVIDER_MODEL genannte war beim Provider registriert. Frag nach irgendeinem der anderen und der Lauf starb mit Stream ended without finish_reason, was dir über die eigentliche Ursache nichts sagt. v0.16.2 registriert jedes beworbene Modell, die Liste bedeutet also, was sie sagt. Der Start warnt jetzt auch, wenn PIBOX_PROVIDER_BASE_URL und PIBOX_PROVIDER_API sich uneinig sind, welches Protokoll du sprichst, was der andere Weg zu einem verwirrenden Fehlschlag drei Aufrufe tief ist.

Noch eines von der Basis geerbt: eventMode an einem Lauf steuert die Event-Aufbewahrung unabhängig davon, ob du ein Schema verlangt hast. full gibt dir die nativen Aufzeichnungen des Providers unangetastet zurück statt der zusammengefassten Fassung.

Benutzung

Einmalig, ohne Server:

docker run --rm \
  -e ANTHROPIC_AUTH_TOKEN=your-token \
  -e ANTHROPIC_BASE_URL=https://api.z.ai/api/anthropic \
  -e ANTHROPIC_MODEL=glm-4.6 \
  psyb0t/pibox:latest \
  -p "list the files in /workspace"

API-Server, dasselbe Umgebungsvariablen-Aliasing, das in jedem psyb0t-Image auftaucht:

docker run -d --network host \
  -e PIBOX_API_MODE=1 \
  -e PIBOX_API_MODE_TOKEN=your-secret \
  -e PIBOX_AVAILABLE_MODELS=glm-4.6,glm-4.5-air \
  -e ANTHROPIC_AUTH_TOKEN=your-token \
  -e ANTHROPIC_BASE_URL=https://api.z.ai/api/anthropic \
  -e ANTHROPIC_MODEL=glm-4.6 \
  -v "$PWD/workspace:/workspace" \
  psyb0t/pibox:latest

Oder bau es dir selbst auf der festgenagelten Basis:

# derives VERSION from pibox/pyproject.toml, pulls
# psyb0t/aicodebox:v0.17.0, tags :v<VERSION> and :latest
make build

PIBOX_API_MODE=1 fährt FastAPI auf :8080 hoch und weigert sich zu starten, wenn PIBOX_AVAILABLE_MODELS nicht gesetzt ist, denn es gibt keinen sinnvollen Default, da pi die Modellliste jedes beliebigen Providers fahren kann, und eine still leere Modellliste ist schlimmer als ein Startfehler. Es stellt /run, /openai/v1/chat/completions, /files/* bereit, und, wenn PIBOX_MCP_MODE=1, /mcp/ auf demselben Port eingehängt, wobei /mcp ohne Slash beim selben Handler landet, kein Redirect. MCP lässt den Schutz gegen DNS-Rebinding an, hinter einem Reverse Proxy oder einem öffentlichen Hostnamen trägst du also den exakten Host und Origin in PIBOX_MCP_MODE_ALLOWED_HOSTS und PIBOX_MCP_MODE_ALLOWED_ORIGINS ein, sonst fliegt jeder Request raus. PIBOX_CRON_MODE_HISTORY_DIR ist die komplette Zustandswurzel von Cron (Default ~/.aicodebox/cron), also ziehen die Laufhistorie unter history/, die Zusammenfassungen pro Job und die Telegram-Antwort-Metadaten alle mit um. Telegram- und Cron-Modus bekommen dieselbe Behandlung, die im aicodebox-Beitrag abgedeckt ist; lies den, wenn du die Modus-Matrix willst statt der pi-spezifischen Klempnerei.

Das Unglamouröse Kind

pibox ist nicht das schillernde Kind-Image. Es ist das, welches beweist, dass der Vertrag der Basis den Kontakt mit einem Agenten-Binary übersteht, das davon ausging, ein Mensch und keine API werde seine Ausgabe lesen. Zwei Umgebungsvariablen verankern den Adapter, eine dritte beschriftet nur das Image, eine Python-Klasse übersetzt pis terminalförmige CLI in etwas, das die Basis fahren kann, und die Feature-Arbeit der Basis selbst, Streaming, Schema-Retries, Tool-Calling, ist darunter gelandet, ohne dass sich eine einzige Zeile Adaptercode bewegt hätte. Genau das soll „Referenzimplementierung“ heißen: nicht die größte, sondern die, die beweist, dass die kleinste Oberfläche immer noch funktioniert.

Wenn du den volleren Agenten willst, dieselbe Basis, Claude Code statt pi, das ist der claudebox-Beitrag. Wenn du piboxs Z.AI-gewürzten Bruder als echten Provider in einem größeren Stack laufen sehen willst statt allein, das steht in dem aigate-Beitrag, pibox-zai ist einer der Provider, zu denen aigate routet, und das nackte pibox läuft da auch, angesetzt auf die Modelle, die aigate selbst ausliefert, kostet also genau das, was die kosten. Der Code liegt auf GitHub. Es ist das uninteressanteste Image der Familie und das, auf das ich als erstes zeigen würde, wenn du dein eigenes schreibst.

Wie Du Es In Deinen Agenten Installierst

Das uninteressanteste Image der Familie liefert trotzdem denselben Installationsweg wie der Rest. Alles unter .agents/ ist in einem einzigen Marktplatz katalogisiert, es sind also zwei Befehle:

claude plugin marketplace add psyb0t/agents
claude plugin install pibox@psyb0t

Codex benutzt denselben Marktplatz mit einem anderen Verb, codex plugin add pibox@psyb0t, weil es kein codex plugin install gibt. Es findet den Skill in einem Checkout des Repos auch von allein, da es .agents/skills/ nativ scannt, ganz ohne dass irgendetwas installiert ist. Es ist inzwischen auch in der offiziellen MCP Registry gelistet, ein Client, der Server von dort auflöst, kann es also finden, ohne eine URL in die Hand gedrückt zu bekommen.