Am construit aicodebox ca pe o abstracție, o singură imagine de bază, un contract AgentAdapter, bagi CLI-ul oricărui agent de cod în el și primești gratis suprafața de API/MCP/Telegram/cron. Frumoasă teorie. Teoriile nu fac doi bani până nu legi efectiv un al doilea binar de agent, complet diferit, la contract și nu te uiți unde sângerează. Așa că am luat pi-coding-agent, un CLI pe care nu l-am scris eu și nu îl controlez, și l-am forțat prin adaptor. Ăsta e pibox. Nu e o imagine flagship plină de funcții, e dovada că abstracția nu e o minciună, iar acum e referința după care se copiază orice altă imagine copil, inclusiv cea cu Claude Code.
Dacă ai citit deja postarea despre aicodebox știi pitch-ul: moduri, adaptoare, REST, compatibilitate OpenAI, MCP, Telegram, cron, toate în stratul de bază, toate gratis odată ce scrii un adaptor. Nu le mai scriu o dată aici. Postarea asta e despre ce a cerut de fapt să înșurubezi un binar de agent străin pe contractul ăla fără să trișezi și despre cât de mică a ieșit suprafața odată ce am încetat să mai adaug căcaturi care nu susțineau nimic.
Ce Nu Îți Dă pi
pi-coding-agent e un CLI în regulă. E și el, ca orice CLI de agent de pe planetă, construit pentru un om care stă la un terminal, nu pentru un proces de server care are nevoie de metadate structurate înapoi. Să îl legi la aicodebox a însemnat să ocolești fiecare dintre presupunerile foarte rezonabile și foarte de terminal ale lui pi:
- Două moduri de ieșire, unul inutil pentru un API. pi are
--mode textși--mode json. Modul text îți dă cuvintele asistentului și absolut nimic altceva, fără id de sesiune, fără consum, fără evenimente per tură. În regulă pentru un om, inutil pentru o rută care trebuie să factureze tokeni și să reia sesiuni. - Zero validare nativă de schemă JSON. pi nu are flag
--schema, nu are mod de ieșire structurată. Doar vorbește. - Fără suport nativ pentru
ANTHROPIC_BASE_URL. Documentația lui pi zice „folosește models.json”, adică nu va citi variabila de mediu prin care se așteaptă să funcționeze un proxy compatibil cu Anthropic (Z.AI, OpenRouter, propriul tău gateway). - Un provider încorporat care îți deturnează în tăcere rutarea. Providerul
zaial lui pi își revendică automat orice nume de modelglm-*, ceea ce înseamnă că o cerere care trebuia să treacă prin suprascrierea ta deANTHROPIC_BASE_URLpoate fi rerutată pe tăcute spre un provider pe care nu l-ai cerut. - Fără suport de primă clasă pentru MCP din configul de workspace așa cum are Claude Code, adică fără preluare automată a unui
.mcp.jsondin workspace.
Nimic din toate astea nu e vina lui pi, e un CLI, face lucruri de CLI. Dar „nu e vina agentului” nu îți dă un API. Cineva trebuie să traducă. Cineva ăla e o singură clasă de Python.
Trei Variabile de Mediu Sunt Tot Pinul
Aici e partea care chiar îi câștigă lui pibox titlul de „referință”. Dezbraci Dockerfile-ul până la ce setează specific pentru pibox și obții exact trei variabile de mediu:
ENV AICODEBOX_ADAPTER=pibox.adapter:PiAdapter
AICODEBOX_AGENT_BINARY=pi
PIBOX_IMAGE_VARIANT=minimalAtât. Asta e toată suprafața declarativă de care are nevoie o imagine copil: îndrepți AICODEBOX_ADAPTER spre un module:ClassName importabil, spui bazei la ce nume de binar să dea shell, iar toată mașinăria de moduri, REST, compatibilitate OpenAI, MCP, Telegram, cron, prinde viață în jurul lui. PIBOX_IMAGE_VARIANT nu face parte deloc din contractul ăla, doar etichetează în care dintre cele două imagini publicate stai, minimală sau full. Tot restul din Dockerfile ori instalează binarul agentului, ori instalează pachetul Python care implementează adaptorul, ori e branding. Dacă îți construiești propria imagine copil și Dockerfile-ul are nevoie de o a treia variabilă de mediu ca să ruleze agentul, faci ceva ce contractul n-a fost gândit să te pună să faci, du-te și recitește clasa AgentAdapter a bazei.
Restul buildului e plictisitor intenționat: npm install -g @earendil-works/[email protected], fixat, nu @latest, pentru că „merge azi” și „merge peste șase luni” nu sunt aceeași afirmație, apoi uv pip install --system --break-system-packages --no-deps /opt/pibox pentru pachetul de adaptor în sine (--no-deps pentru că aicodebox e deja în imaginea de bază, iar să îl rezolvi din nou e muncă în gol).
Adaptorul: build_argv E Obligatoriu, Tot Restul E O Alegere
Clasa AgentAdapter a bazei are exact o metodă care aruncă NotImplementedError dacă nu o suprascrii: build_argv. Orice alt hook, validate, translate_auth, parse_output, parse_events, parse_stream_event, interactive_argv, passthrough_argv, auth_paths, vine cu o implementare implicită funcțională. PiAdapter le suprascrie oricum pe toate, pentru că CLI-ul lui pi e destul de ciudat cât să producă gunoi cu valorile implicite. Uite ce îți cumpără de fapt fiecare, verificat pe pibox/pibox/adapter.py:
build_argv(obligatoriu): invocă mereupi -p --mode json, niciodată--mode text, exact ca adaptorul să primească fluxul complet de evenimente de sesiune în loc de proză goală. Tratarea sesiunii se ramifică în trei:--session <id>la reluare,--no-sessionpentru efemer,--continueîn rest.validate(opțional, baza verifică doaroutput_format): respinge o valoare dethinkingdin afara luioff/minimal/low/medium/high/xhighși respingetools_allowlistcombinat cuno_tools, ca prostie reciproc exclusivă.translate_auth(opțional, implicit în bază nu face nimic): suprascris oricum, tot nu întoarce nimic, pentru că pi citește nativANTHROPIC_API_KEY/OPENAI_API_KEY/OPENROUTER_API_KEY/GEMINI_API_KEY/ZAI_API_KEY. Suprascrierea există ca să documenteze faptul ăsta în cod, nu ca să schimbe comportamentul.parse_output(opțional, implicit baza doar curăță stdout ca text simplu): parcurge NDJSON-ul lui pi, luândsessionpentru id-ul de sesiune,message_endpentru textul asistentului și consum, șiturn_endca rezervă pentru consum. Tot aici se populeazăprovider_error: când o tură de asistent ducestopReason=errorplus unerrorMessage, felul lui pi de a raporta o respingere din amonte, un rate limit sau un eșec de autentificare, adaptorul îl prinde și îl transmite peRunResult.provider_error, ca ruta OpenAI să poată întoarce un400adevărat în loc de un200cu text gol.parse_events(opțional, implicit baza întoarce o listă goală): decodează JSON fiecare linie NDJSON pentru modul de ieșirejson-verbose; liniile stricate se aruncă cu un avertisment, nu cu un crash.parse_stream_event(opțional, implicit baza tratează fiecare linie ca pe un delta de text brut): decodează fluxulmessage_update.assistantMessageEvental lui pi, trimite mai departe pe fir doartext_deltași înghite în tăcere deltelethinking_*și cele de folosire de unelte, ca raționamentul intern al modelului să nu se scurgă niciodată în câmpulcontentcompatibil cu OpenAI.auth_paths(opțional, implicit baza nu întoarce nimic de persistat): listează starea reală a lui pi,~/.pi/agent/auth.json,settings.json,models.jsonși directorulsessions, ca tokenurile OAuth și istoricul de sesiuni să supraviețuiască unuidocker startîn loc să se evapore.
Mai e și un hack vârât în build_argv cu care nu mă mândresc, dar pe care îl apăr fără rezerve: dacă ANTHROPIC_BASE_URL e setată și apelantul n-a ales deja un provider în extra_args, adaptorul injectează forțat --provider anthropic. De ce? Pentru că providerul încorporat zai al lui pi își revendică automat orice nume de model glm-* și ocolește complet suprascrierea ta de base URL. Fără flagul forțat, dacă îndrepți pibox spre un proxy compatibil Z.AI și ceri un model glm-4.6, îți ignoră în tăcere proxy-ul și vorbește direct cu ce crede providerul zai al lui pi că ar trebui. Ăsta e genul de bug care costă pe cineva o după-amiază și un tichet de suport înainte să observe careva că traficul n-a atins niciodată proxy-ul.
Modul Schemă: pi N-are Validare Nativă, Deci E Un Adaos la Promptul de Sistem
pi nu validează scheme JSON. N-are niciun flag pentru asta. Așa că atunci când o cerere duce jsonSchema, singura mișcare a adaptorului e să adauge o directivă la promptul de sistem care îi spune modelului, în engleză simplă, „răspunde cu un singur document JSON conform acestei scheme, fără proză, fără garduri”, și apoi să îi întindă și schema brută în JSON. Toată validarea propriu-zisă, parsarea rezultatului, verificarea lui pe schemă, reîncercarea de până la trei ori cu un prompt corectiv când pică, se întâmplă în stratul de bază aicodebox, nu în pibox. Singura treabă a adaptorului e să împingă modelul spre conformare; n-are niciun cuvânt de spus dacă modelul chiar se conformează.
Despicătura aia contează din cauza a ce a venit mai târziu. Ajutorul de reîncercare pe schemă al bazei s-a deșteptat progresiv fără ca vreo linie de cod de adaptor din pibox să se schimbe, iar comentariile din changelogul propriu al Dockerfile-ului se citesc ca un jurnal de comportament al bazei care evoluează sub un contract de adaptor complet stabil: reîncercări care reformulează sarcina originală în loc de doar eroarea (ca o schemă care cere alegerea dintr-un enum mare să nu reîncerce orbește), workspace-uri efemere per cerere, ca o cerere de 100k de tokeni care are nevoie de trei reîncercări să plătească vreo 1,5k de tokeni de supraîncărcare corectivă în loc să redea tot cei 100k de trei ori, și, cel mai recent, stream:true combinat cu apelarea de unelte sau cu modul schemă nu mai întoarce un 400 sec. Acum calculează răspunsul complet fără streaming și îl redă ca pe un singur flux SSE tamponat: un chunk de rol, un delta de content sau de tool_calls, un chunk de final, [DONE]. Chatul simplu tot face streaming token cu token. Nimic din toate astea n-a atins pibox/pibox/adapter.py. Ăsta e tot rostul contractului, imaginea copil nu are voie să știe sau să îi pese că baza s-a deșteptat sub ea.
Extensia mcp-bridge: Să Îi Dai lui pi Formatul de Config al Altcuiva
pi nu citește nativ un .mcp.json din workspace așa cum face Claude Code. pibox livrează o extensie TypeScript, pibox/extensions/mcp-bridge/index.ts, care face asta pentru pi: la pornirea sesiunii citește .mcp.json din workspace folosind schema claude-code (mcpServers.<name>.{command,args,env}), naște fiecare server peste stdio, cheamă listTools() și înregistrează fiecare unealtă la pi sub un nume igienizat mcp__<server>__<tool> prin pi.registerTool(). Apelurile de unelte sunt trimise mai departe către serverul MCP, iar rezultatul se întoarce pe același canal pe care îl folosesc uneltele încorporate ale lui pi, adică modelul nu poate face diferența.
Instalarea npm pentru extensia aia se întâmplă la build, RUN cd /opt/pibox/extensions/mcp-bridge && npm install --omit=dev --no-audit --no-fund, nu la prima pornire a containerului. E o alegere deliberată: nimeni nu vrea ca prima lui rulare de agent să se blocheze pe un resolve de npm. Un script init.d, pibox/init.d/10-pi-extensions.sh, leagă calea extensiei preinstalate în array-ul extensions din ~/.pi/agent/settings.json la prima rulare, idempotent, printr-un merge cu jq care deduplică prin unique, ca rerularea să nu îngrămădească intrări duplicate.
Mai e acolo și o cursă la închidere care merită arătată, pentru că e genul de bug care apare doar în producție: pi -p se agață după ce își tipărește răspunsul final, dacă subprocesele de server MCP născute încă țin bucla de evenimente deschisă. Extensia ascultă după session_shutdown, aleargă închiderea fiecărui client MCP contra unui timeout de 2 secunde și apoi, curea plus bretele, cheamă forțat process.exit(0) o jumătate de secundă mai târziu, pentru că unele versiuni de Node țin bucla vie chiar și după ce close() se rezolvă. Fără asta, un apel de API dintr-un foc ar sta pur și simplu acolo până l-ar omorî ceva din exterior.
pibox-entrypoint: 17 Aliasuri și O Regenerare de Config la Boot
pibox-entrypoint.sh există pur și simplu ca să îi dea lui pibox propria suprafață brandată de variabile de mediu fără să dubleze vreo logică din bază. Definește o listă de 17 sufixe, 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, WORKSPACE, AVAILABLE_MODELS, AVAILABLE_EFFORTS, CONTAINER_NAME, iar pentru fiecare, dacă PIBOX_<suffix> e setată și AICODEBOX_<suffix> nu, copiază valoarea. AICODEBOX_* câștigă dacă sunt setate amândouă, ca utilizatorii avansați să nu fie blocați în afara numelor de dedesubt. Cele două variabile de selecție a adaptorului, ADAPTER și AGENT_BINARY, sunt excluse deliberat din lista aia; alea sunt fixate de Dockerfile și nu sunt ceva ce ar trebui să poată suprascrie un utilizator la runtime.
Entrypointul rulează și setup-provider-env.sh la fiecare boot, nu doar la primul, iar acel „fiecare boot” e o reparație pentru un bug real, nu o alegere de stil. Scriptul regenerează intrarea de provider anthropic a lui pi din ~/.pi/agent/models.json pe baza ANTHROPIC_BASE_URL / ANTHROPIC_MODEL de fiecare dată când pornește containerul. Înainte rula prin init.d, care prin design se declanșează o singură dată pe viața unui container, în regulă pentru un container de aruncat, stricat în clipa în care cineva montează ~/.aicodebox sau ~/.pi ca volum persistent, pentru că atunci markerul de init supraviețuiește reconstrucției și un ANTHROPIC_BASE_URL schimbat nu mai ajunge niciodată în tăcere la models.json. Mutarea lui în entrypoint înseamnă că configul de base URL se împrospătează la fiecare pornire, cu volum persistent sau fără.
v0.16.0: ruta Anthropic e acum doar una din patru
Tot ce e mai sus e scris în jurul unei singure forme, îndoind providerul anthropic al lui pi spre orice endpoint spre care îndrepți ANTHROPIC_BASE_URL. Asta încă merge și tot aia rulează dacă atât setezi. Dar acum e calea moștenită.
v0.16.0 a făcut providerul generic. Cinci variabile descriu orice endpoint HTTP: PIBOX_PROVIDER_BASE_URL, PIBOX_PROVIDER_API, PIBOX_PROVIDER_API_KEY, PIBOX_PROVIDER_MODEL, PIBOX_PROVIDER_NAME. Cea cu API alege forma de pe fir, iar acolo sunt patru: openai-completions, openai-responses, anthropic-messages, google-generative-ai. Ceea ce acoperă LiteLLM și Z.AI fără să se prefacă vreunul că e Anthropic.
Hackul de injecție forțată din build_argv a crescut și el pe măsură. Acum e un selector cu două ramuri: folosește providerul configurat dacă există unul, altfel cade înapoi pe anthropic, și injectează --model odată cu el.
Setezi PIBOX_PROVIDER_* și ANTHROPIC_BASE_URL în același timp și pibox iese în loc să aleagă una. Bine. Două scheme de rutare pe jumătate configurate care se contrazic sunt exact eșecul pe care îl vrei zgomotos și la boot, nu tăcut și la trei apeluri de unelte adâncime.
Un detaliu care merită furat: cheia din amonte trăiește doar în mediul procesului Pi. Ce aterizează în models.json e o referință de tipul $OPENAI_API_KEY, nu secretul în sine, deci fișierul de config de pe disc rămâne plictisitor dacă îl citește cineva.
v0.18.0: în sfârșit a primit un wrapper
Tot ce e mai jos era înainte singurul mod de a-l rula: linii de docker run scrise de mână, cu mounturile și mediul scrise pe litere de fiecare dată. claudebox și codexbox aveau amândouă un wrapper de host și un instalator. pibox nu avea, pentru că era copilul fără fason și n-a apucat nimeni.
Acum are unul. install.sh îți pune o comandă pibox pe PATH, iar wrapper-ul se ocupă de instalația de containere, deci să îl rulezi într-un director înseamnă doar pibox. Zece variabile îl conduc: PIBOX_DATA_DIR, PIBOX_STATE_DIR și PIBOX_SSH_DIR pentru unde stă starea, PIBOX_IMAGE și PIBOX_FULL pentru ce imagine, PIBOX_DETACH pentru rulări în fundal, PIBOX_ENV_* și PIBOX_MOUNT_* ca să transmiți mediu și mounturi, plus PIBOX_INSTALL_DIR cu PIBOX_BIN_NAME ca să decizi unde aterizează comanda și cum se numește.
v0.17.0 i-a dat și o variantă psyb0t/pibox:latest-full, construită pe aicodebox:v0.15.0-full, acolo unde s-a mutat toolchainul comun de Go, Node, Python, clienți de baze de date și ops. pibox a încetat să mai construiască vreo bucată din el.
Și cutiile se pot chema una pe alta
Instalezi pibox, claudebox și codexbox în același director și fiecare wrapper le montează pe celelalte două doar în citire la /usr/local/bin/<name>, deci pi poate da o sarcină altui agent fără să îi părăsească sesiunea. Rulările imbricate duc un context de host versionat, AICODEBOX_LAUNCH_CONTEXT_VERSION=1 plus AICODEBOX_HOST_HOME, AICODEBOX_HOST_WORKSPACE, AICODEBOX_HOST_WRAPPER_DIR și câte un home și o cale de wrapper per agent, pentru că o cale din interiorul unui container nu spune nimic despre host. O rulare imbricată sare peste scrierile de fișiere de autentificare pe care le face una de nivel superior, deci un copil nu poate rescrie credențialele celui care l-a chemat. AICODEBOX_ENV_* și AICODEBOX_MOUNT_* transmit spre fiecare cutie deodată, iar AICODEBOX_MANAGED_INSTALL=1 e o instalare neinteractivă care refuză să suprascrie o cheie SSH existentă.
Bugul care făcea din lista de modele o minciună
Merită menționat, pentru că secțiunea de mai sus promite ceva ce v0.16.0 n-a livrat de fapt. Puteai lista mai multe modele în PIBOX_AVAILABLE_MODELS, dar doar cel numit în PIBOX_PROVIDER_MODEL era înregistrat la provider. Ceri oricare dintre celelalte și rularea murea cu Stream ended without finish_reason, care nu îți spune nimic despre cauza reală. v0.16.2 înregistrează fiecare model anunțat, deci lista înseamnă ce zice. Pornirea avertizează acum și când PIBOX_PROVIDER_BASE_URL și PIBOX_PROVIDER_API se contrazic despre ce protocol vorbești, adică celălalt mod de a obține un eșec confuz la trei apeluri adâncime.
Încă unul moștenit din bază: eventMode pe o rulare controlează reținerea evenimentelor independent de faptul că ai cerut sau nu o schemă. full îți dă înapoi înregistrările native ale providerului neatinse, nu versiunea rezumată.
Utilizare
Dintr-un foc, fără 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"Server de API, aceeași aliasare de variabile de mediu care apare în fiecare imagine psyb0t:
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:latestSau ți-o construiești singur peste baza fixată:
# derives VERSION from pibox/pyproject.toml, pulls
# psyb0t/aicodebox:v0.14.0, tags :v<VERSION> and :latest
make buildPIBOX_API_MODE=1 ridică FastAPI pe :8080 și refuză să pornească fără PIBOX_AVAILABLE_MODELS setată, pentru că nu există o valoare implicită rezonabilă, pi putând conduce lista de modele a oricărui provider, iar o listă de modele goală pe tăcute e mai rea decât un eșec la pornire. Expune /run, /openai/v1/chat/completions, /files/* și, când PIBOX_MCP_MODE=1, /mcp montat pe același port. Modurile Telegram și cron primesc același tratament acoperit în postarea despre aicodebox; du-te și citește aia dacă vrei matricea de moduri în loc de instalația specifică lui pi.
Copilul Fără Fason
pibox nu e imaginea copil cu fason. E aia care dovedește că contractul bazei supraviețuiește contactului cu un binar de agent care a presupus că un om, nu un API, o să îi citească ieșirea. Două variabile de mediu fixează adaptorul, o a treia doar etichetează imaginea, o clasă de Python traduce CLI-ul în formă de terminal al lui pi în ceva ce poate conduce baza, iar munca proprie de funcții a bazei, streaming, reîncercări pe schemă, apelare de unelte, a aterizat pe dedesubt fără să se miște o linie de cod de adaptor. Asta ar trebui să însemne „implementare de referință”: nu cea mai mare, ci aia care dovedește că suprafața cea mai mică tot funcționează.
Dacă vrei agentul mai plin, aceeași bază, Claude Code în loc de pi, aia e postarea despre claudebox. Dacă vrei să vezi fratele cu aromă de Z.AI al lui pibox rulând ca provider adevărat într-un stack mai mare în loc de de sine stătător, aia e în postarea despre aigate, pibox-zai fiind unul dintre providerii spre care rutează aigate. Codul e pe GitHub. E cea mai puțin interesantă imagine din familie și aia spre care aș arăta primul dacă ți-o scrii pe a ta.
Cum O Instalezi în Agentul Tău
Cea mai puțin interesantă imagine din familie livrează totuși aceeași cale de instalare ca restul. Tot ce e sub .agents/ e catalogat într-un singur marketplace, deci sunt două comenzi:
claude plugin marketplace add psyb0t/agents
claude plugin install pibox@psyb0tCodex folosește același marketplace cu alt verb, codex plugin add pibox@psyb0t, pentru că nu există codex plugin install. Găsește singur și skillul într-un checkout al repo-ului, pentru că scanează .agents/skills/ nativ, fără să fie instalat absolut nimic. E listat acum și pe MCP Registry-ul oficial, deci un client care rezolvă servere de acolo îl poate găsi fără să i se dea un URL.