Am construit claudebox pentru că să las Claude Code liber pe host-ul meu îmi dădea dureri în piept. A mers. I-a crescut un API HTTP, un endpoint compatibil cu OpenAI, un server MCP, un bot de Telegram și un scheduler de cron. Luni de muncă, toată folositoare.
Apoi a apărut pi-coding-agent și am vrut exact același lucru și pentru el. Apoi OpenAI a scos Codex CLI și l-am vrut a treia oară.
Și exact atunci m-am așezat și m-am uitat serios la ce urma să fac: să copiez cu copy-paste un server FastAPI, un bot de Telegram cu workspace-uri per chat, un scheduler de cron cu directoare de istoric și un server MCP într-un al doilea repo. Apoi într-al treilea. Trei copii ale acelorași nouă sute de linii, care se despart una de alta în clipa în care repar un bug într-una și le uit pe celelalte. Trei locuri de peticit când Telegram schimbă o regulă de randare. Trei suite de teste care testează logică identică pe agenți diferiți.
În mă-sa. Nimic din instalația aia nu e specific lui Claude. Nimic nu e specific lui pi. Totul se reduce la „ia un prompt, dă shell la un CLI, prinde ce vine înapoi, dă-i-l celui care a cerut”. Agentul e un detaliu. Suprafețele sunt produsul.
Așa că le-am smuls afară într-o imagine de bază. Asta e aicodebox.
Problema Fiecărui Proiect de Tip „Pune Un Agent Pe Rețea”
Fiecare dintre uneltele astea face aceeași greșeală, a mea inclusiv, prima dată: sudează transportul de creier.
Vrei agentul accesibil peste HTTP, așa că scrii un server HTTP care știe să invoce exact binarul ăla, să parseze exact formatul ăla de ieșire și să trateze exact setul ăla de flaguri. Șase luni mai târziu apare un agent mai bun și tu rămâi cu un morman de infrastructură care vorbește doar cu cel vechi.
Simptomele sunt mereu aceleași:
- Upgrade-uri prin fork și măcel. Vrei aceleași suprafețe pentru alt agent? Faci fork la repo, cauți fiecare loc unde e hardcodat numele vechiului binar și speri că le-ai prins pe toate.
- Fixuri de bug-uri care divergează. Randorul de markdown din Telegram are un bug. Îl repari într-un repo. Celelalte două rămân cu bug-ul o lună, până îți aduci aminte.
- Suprafețe inconsistente. O cutie are rulări async, alta nu. Una are un endpoint OpenAI, alta are jumătate din el. Nimic nu se compune pentru că nimic nu cade de acord pe o formă de fir.
- Lock-in pe agent din greșeală. Nu pentru că a decis cineva să se blocheze, ci pentru că instalația a crescut în jurul ciudățeniilor unui singur binar și s-a pietrificat acolo.
Soluția nu e un wrapper mai bun. E să recunoști că wrapper-ul n-ar trebui să știe ce înfășoară.
Cum Funcționează Căcatul Ăsta De Fapt
aicodebox e o imagine Docker de bază. Nu îi dai fork. Îi dai FROM.
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=piAsta e toată integrarea. Linia cu npm install e întâmplătoare, pur și simplu așa se livrează pi-coding-agent. Putea la fel de bine să fie un apt-get install, un binar copiat dintr-un stage de build de Go sau orice altceva care pune un executabil pe PATH; vezi cum ajunge agentul instalat mai jos. Baza deține fiecare suprafață de rețea. Adaptorul tău traduce „rulează promptul ăsta” în ce așteaptă CLI-ul agentului tău. Un agent nou aterizează într-o după-amiază și aterizează cu un API HTTP, un endpoint compatibil cu OpenAI, un server MCP, un bot de Telegram și un scheduler de cron deja atașate, pentru că alea nu mai sunt ale tale de scris.
Ce e de fapt în imagine:
- Sistemul de operare: Ubuntu 24.04, un utilizator
aicodecu UID 1000, cu sudo fără parolă și membru în grupul docker. - Runtime-uri: Node.js 24 LTS (majoritatea agenților se livrează ca pachete npm), Python 3.14 și Docker CE cu buildx și compose, pentru când agentul tău decide că are nevoie să nască propriile containere.
- Pachetul:
aicodebox, contractul de adaptor plus patru dispecere de moduri. Python curat, zero efecte secundare până când chiar pornești un mod. - Starea: override-urile per chat și istoricul de cron stau sub
$HOME/.aicodebox/. Fă-i bind-mount dacă vrei să supraviețuiască containerului. Pachetul în sine nu stochează nimic.
Contractul de Adaptor E Două Metode
Asta e partea de care chiar sunt mândru, pentru că e mică. Totul trece printr-o singură interfață, iar suprafața ei obligatorie e de două metode.
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 spune cum se invocă chestia. parse_output spune cum se citește ce a venit înapoi. Ăsta e contractul. Tot restul, validate, build_env, translate_auth, post_validate_json, parse_events, parse_stream_event, e opțional, cu valori implicite care fac lucrul plictisitor și corect.
parse_stream_event merită menționat: valoarea implicită e „un delta per linie de stdout”, ceea ce e în regulă pentru un binar care doar scuipă proză. Suprascrie-l doar dacă agentul tău emite un flux structurat de evenimente JSON și vrei granularitate per token sau per unealtă în streamingul OpenAI. Majoritatea adaptoarelor nu se vor obosi.
Adaptorul se rezolvă la primul apel și se ține în cache pe toată viața procesului. Fiecare mod îl trage pe același, deci ce știe build_argv să conducă e exact ce se expune peste HTTP, MCP, Telegram și cron. Zero muncă de integrare per mod. Îl scrii o dată, primești cinci căi înăuntru.
Patru Moduri, Setezi Un Flag
Modurile sunt variabile de mediu. Setezi flagul, entrypointul pornește modul ăla. Fără flag, fără mod, containerul pur și simplu îți rulează agentul interactiv ca un shell normal.
Modurile de prim-plan (API, Telegram, Cron) se exclud reciproc, cu o excepție deliberată: Telegram și Cron împart un proces, pentru că cronul rulează în thread înăuntrul botului de Telegram. API câștigă dacă îl setezi lângă altceva. MCP e independent și coexistă cu oricare dintre ele.
Modul API
AICODEBOX_API_MODE=1 pornește FastAPI pe :8080.
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=1Un singur flag dictează forma răspunsului. POST /run fără jsonSchema îți dă varianta slabă: {runId, workspace, exitCode, text}. Doar proza. Dai un jsonSchema și primești toată suprafața de diagnostic, text, json, events, sessionId, usage, attempts. Pe sub capotă agentul e invocat în mod json-verbose, ieșirea lui decodată și validată pe schema ta.
Ăsta a fost tot designul o vreme: schemă înseamnă suprafață completă, fără schemă înseamnă slab, două forme de fir și un flag. A ținut până n-a mai ținut. Să vrei fluxul brut de evenimente fără să vrei și o schemă e o dorință reală, iar forma veche te punea să inventezi o schemă ca să îl obții. Așa că de la v0.14.6 există un al doilea buton, eventMode, și e reparația cinstită, nu un flag care îndoaie pe ascuns primul: auto păstrează comportamentul vechi, none aruncă evenimentele complet, iar full îți dă înapoi înregistrările providerului neatinse, într-un plic stabil {sequence, attempt, backend, eventType, event}. Reținerea evenimentelor e acum independentă de faptul că ai cerut sau nu ieșire structurată. Dacă o reîncercare pe schemă arde trei tentative, primești înregistrările de la toate trei, adică exact momentul în care chiar le vrei.
Reîncercările sunt cinstite cu privire la cât costă. La o eroare de parsare sau de validare, wrapper-ul repromptează de până la trei ori (JSON_RETRY_MAX = 3) cu ieșirea proastă anterioară și cu eroarea specifică. Dacă toate trei pică, parseError și jsonRetries înlocuiesc json, dar text, events, sessionId, usage și attempts tot se întorc, pentru că o rulare structurată care a eșuat e exact momentul în care ai nevoie de diagnostice.
Iar usage e suma pe toate tentativele, nu ultima. Providerul tău te taxează per tentativă; să raportezi doar tentativa finală ar fi o minciună. attempts duce defalcarea per tentativă, ca să poți randa „reîncercarea 2 din 3 a costat atât” sau să o facturezi mai departe.
Trucul de reîncercare care îmi place cel mai mult. Cererile cu schemă care nu numesc un workspace primesc unul efemer per cerere sub /tmp/aicodebox/<uuid>/, curățat într-un finally. Pentru că sesiunea persistă, reîncercările trimit un prompt corectiv minimal, eroarea, o directivă, schema, în loc să îți redea tot inputul original. Pe un prompt mare asta înseamnă cam de o sută de ori mai puțin cost de input la reîncercare. Dacă totuși îi dai propriul tău workspace, cade înapoi pe reîncercări cu sesiune proaspătă care reformulează sarcina: sigur în orice workspace, doar mai scump.
Endpointul compatibil cu OpenAI
POST /v1/chat/completions, cu și fără streaming. Îndreaptă LiteLLM, Open WebUI sau orice SDK de OpenAI spre el și agentul tău de cod apare ca un model.
Ieșirea structurată trece prin câmpul standard, response_format cu {"type":"json_object"} pentru permisiv sau {"type":"json_schema", ...} pentru ieșiri structurate adevărate. Există un header x-aicodebox-json-schema ca variantă de rezervă pentru clienții care nu pot seta câmpul din body; bodyul câștigă dacă sunt prezente amândouă. Reîncercările epuizate întorc 422. Crăparea procesului agentului întoarce 500 cu exit code-ul și stderr în detail.
Apelare de unelte executate de client. Trimiți un array tools standard și cutia se poartă ca un model normal de function calling: răspunde cu tool_calls și finish_reason: "tool_calls", clientul tău rulează unealta și trimite înapoi rezultatul role: "tool", bucla continuă. Fără stare, retrimiți tot istoricul la fiecare rundă, exact ca la OpenAI. tool_choice suportă auto / none / required / o funcție numită.
Iar tools se compune cu response_format. O tură cu apel de unealtă întoarce apeluri de unelte și nu e verificată pe schemă, în mod deliberat; tura cu răspunsul final al modelului e validată pe schema ta cu reîncercări și se întoarce ca JSON canonic. Deci un flux agentic cu mai multe unelte se poate totuși termina într-un răspuns structurat, adică exact ce vrei când bagi chestia asta într-un pipeline.
O rezervă cinstită: streamingul cu tools sau response_format e SSE tamponat. Răspunsul complet se calculează, apoi se redă ca un flux de evenimente dintr-o singură bucată, chunk de rol la deschidere, un delta, chunk de final, [DONE]. E un flux valid, doar că nu e incremental pe token. Chatul simplu tot face streaming ca lumea.
Modul Telegram
AICODEBOX_TELEGRAM_MODE=1 plus un token de bot. Intră text, se întâmplă o rulare de agent, răspunsul vine înapoi bucățit și randat din Markdown în dialectul HTML al lui Telegram. Încărcările, documente, poze, video, voce, aterizează în workspace-ul chatului ăluia. Agentul împinge fișiere înapoi emițând [SEND_FILE: relative/path] în ieșirea lui.
Override-uri per chat pentru /model, /effort, /system_prompt și /append_system_prompt, persistate pe disc. /cancel omoară rularea în curs, /reload recitește yamlul, /config scuipă configul combinat, /fetch trage un fișier din workspace, /status listează chaturile ocupate.
Detaliul care o face utilizabilă zi de zi: dacă răspunzi la un mesaj tras de cron, se injectează instrucțiunea și rezultatul jobului ăluia în context, așa că întrebarea ta de continuare chiar are sens pentru agent, în loc să ajungă la el fără nicio idee despre ce vorbești.
Modul Cron
Programări croniter pe șase câmpuri, workspace per job, notificare opțională pe Telegram.
jobs:
- name: morning-report
schedule: "0 0 9 * * *"
instruction: |
Summarize yesterday's git activity in {workspace}.
workspace: shared
telegram_chat_id: -100123
model: claude-sonnetFiecare rulare primește propriul director de istoric, meta.json, stdout.log, stderr.log, result.txt și telegram.json dacă a notificat. Apoi promptul rulării următoare primește un indiciu care arată spre directorul ăla.
Ultima bucată e mică și schimbă ce pot fi joburile astea. Agentul își poate citi propria ieșire din trecut fără să construiești tu nimic din instalația aia. „Ce s-a schimbat de ieri”, „a regresat asta”, „compară cu săptămâna trecută”, toate merg pentru că istoricul e pe disc și agentului i s-a spus unde să se uite.
Modul MCP
AICODEBOX_MCP_MODE=1. În modul API se montează la /mcp pe același port, fără proces în plus. Sub Telegram, cron sau passthrough simplu rulează ca un uvicorn sidecar pe AICODEBOX_MCP_MODE_PORT (implicit 8081).
Cinci unelte: run_prompt, list_files, read_file, write_file, delete_file. Îndreaptă Claude Desktop, Cursor sau alt agent spre el și agentul tău de cod devine o unealtă pe care alți agenți o pot chema.
Autentificarea e AICODEBOX_MCP_MODE_TOKEN, propriul lui bearer, fără cădere înapoi pe tokenul de API. Asta e deliberat. MCP e o suprafață separată cu expunere separată, iar să accepți pe tăcute bearerul de API acolo ar însemna să dai fiecărui client de API o unealtă de execuție de agent pentru care n-a fost niciodată în scop. Există un parametru de query ?apiToken= pentru clienții care nu pot seta headere.
Configurare, Cu O Convenție Adevărată
Totul sunt variabile de mediu, iar numirea urmează o singură regulă: <MODE>_MODE e flagul de pornit/oprit, <MODE>_MODE_<KNOB> e configul lui, iar orice nu e legat de un mod e gol.
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, _TOKENLista de modele se rezolvă în doi pași: AICODEBOX_AVAILABLE_MODELS câștigă dacă e setată, altfel cade înapoi pe ce declară adaptorul tău în variabila lui de clasă available_models. Deci variabila de mediu nu e strict obligatorie, e override-ul. Declari lista în adaptor și nu mai trebuie să o setezi niciodată.
Ce e obligatoriu e ca una dintre cele două să producă ceva. Dacă amândouă vin goale, modul API refuză să pornească, loghează motivul și iese cu cod diferit de zero în loc să pornească stricat. Asta e decizia corectă: /v1/models are nevoie de o listă reală și nu există cădere sigură, pentru că numele adaptorului nu e un nume de model. Mai bine crapi zgomotos la boot decât să servești o listă de modele de gunoi care strică clientul cuiva trei straturi mai încolo.
Două imagini, și copiii au încetat să reconstruiască același toolchain
Baza se livrează acum în două variante. psyb0t/aicodebox:latest e cea minimală descrisă mai sus, iar psyb0t/aicodebox:latest-full (etichetată pe versiune ca v0.15.0-full) cară toolchainul de dezvoltare comun: Go, Node, Python, editoare, diagnostice, clienți de baze de date, unelte de ops.
Despicătura aia există din cauza unei probleme de duplicare care reapăruse pe tăcute. Fiecare imagine copil care voia un mediu de dezvoltare adevărat instala același Go, același Node, același Python, același psql și redis-cli, din propriul Dockerfile și propriile lockfile-uri. Trei copii ale toolchainului, despărțindu-se exact cum se despărțeau odată cele trei copii ale serverului HTTP. Aceeași boală, cu un strat mai jos.
Așa că s-a mutat în bază. Copiii o moștenesc pornind de la varianta full în loc să o construiască singuri, iar curățenia n-a fost subtilă: codexbox a șters cam 15.800 de linii de lockfile-uri de toolchain proprii, iar Dockerfile.full al lui claudebox a pierdut 173 de linii. make build-full, make build-all și make test-full-image acoperă varianta, iar CI publică întâi imaginea minimală, apoi o construiește pe cea full peste ea.
Familia
Trei imagini rulează pe baza asta chiar acum, toate fixate momentan pe psyb0t/aicodebox:v0.15.1:
- claudebox: Claude Code. v2.0.0 al lui a fost un rebase complet pe baza asta; acum contribuie cu
ClaudecodeAdapterși moștenește fiecare suprafață. - pibox: pi-coding-agent, îndreptat spre ce LLM îți place. Ăsta e copilul de referință: folosește baza ca atare și adaugă
PiAdapter. - codexbox: Codex CLI de la OpenAI, prin
CodexAdapter.
Cum ajunge de fapt agentul instalat
Toate trei se nimeresc să își instaleze agentul cu npm install -g, dar nu citi asta ca pe rețetă, e doar că claude-code, pi și codex se livrează toate ca globale npm. Baza n-are nicio părere despre cum a ajuns binarul tău acolo.
Contractul adevărat e două lucruri: binarul numit de AICODEBOX_AGENT_BINARY trebuie să existe pe PATH, iar pachetul tău de adaptor trebuie să fie importabil. Atât. Ce te duce acolo e treaba ta:
- npm:
RUN npm install -g @vendor/[email protected]. Node 24 LTS e deja în bază, exact motivul pentru care cei trei copii existenți au luat drumul ăsta. - apt: e Ubuntu 24.04 cu sudo și un apt funcțional.
RUN apt-get update && apt-get install -y your-agente în regulă dacă chiar livrează cineva un deb. - pip / uv: Python 3.14 e acolo, la fel și uv, fixat pe digest. Așa își instalează deja fiecare copil adaptorul:
uv pip install --system --break-system-packages --no-deps /opt/yourpkg. - Un binar compilat: Go, Rust, orice. Ține minte că baza nu cară un toolchain de Go sau Rust, deci nu te aștepta ca
go installsă meargă din prima. Ori îți aduci toolchainul în propriul strat, ori faci lucrul sănătos și daiCOPY --from=dintr-un stage de build, ca să nu aterizeze compilatorul deloc în imaginea de runtime:
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-agentAceleași cinci suprafețe pe partea cealaltă. Baza nu află niciodată în ce limbaj a fost scris agentul tău, și ăsta e tot rostul, nu face decât să dea shell la un nume de pe PATH și să întindă octeții către parse_output-ul tău.
pibox e minimul cinstit. Instalezi agentul cu npm, instalezi pachetul de adaptor cu uv pip install, setezi exact trei variabile de mediu, AICODEBOX_ADAPTER, AICODEBOX_AGENT_BINARY și o etichetă PIBOX_IMAGE_VARIANT, și îi dai un entrypoint mic și brandat care aliasează PIBOX_* la AICODEBOX_*. Asta e toată imaginea copil. Nu degeaba e implementarea de referință.
Celelalte două sunt mai mari, și asta e partea interesantă. claudebox setează cinci variabile de mediu, codexbox patru, și amândouă livrează un script de lansare în plus peste adaptor. Nu pentru că baza are scurgeri, ci pentru că fiecare agent târăște după el propria problemă de stare despre care o bază agnostică la agent n-are voie să știe:
- claudebox setează
CLAUDE_CONFIG_DIRca Claude Code să scrie.claude.jsonși credențialele lui pe bind mount în loc de un$HOMEnemontat, altfel refaci alegerea temei și loginul la fiecare recreare de container. Doar claudebox știe că încărcătura e Claude Code, deci doar claudebox poate seta asta. - codexbox setează
CODEX_HOMEdin același motiv (o cheie de API sau un token OAuth de abonament ChatGPT care trebuie să supraviețuiască unei recreări) și trebuie să facămkdirșichownpe el la build, pentru că codex crapă la pornire dacăCODEX_HOMEarată spre un director care nu există deja. - Amândouă îndreaptă
AICODEBOX_AGENT_BINARYspre un script de lansare, nu spre binarul agentului direct. Cel al lui claudebox restaurează valorile implicite interactive pe care baza agnostică la agent le-a scăpat deliberat:--continuecu o cădere de rezervă,--permission-mode bypassPermissionsși injecția always-skills prin--append-system-prompt. Modurile de server ocolesc lansatorul complet și construiesc argv prin adaptor.
Asta e forma reală a abstracției ăsteia și prefer să o descriu exact decât să mă prefac că fiecare copil are patru linii. Baza deține tot ce nu e specific agentului. Ce rămâne în fiecare copil e exact ce este: adaptorul care știe că ăsta binar vrea -p în timp ce ăla vrea --prompt, că unul emite stream-json în timp ce altul scrie text simplu, plus orice ciudățenie de persistare a configului îți bagă pe gât agentul ăla anume.
Tot restul, API-ul, shimul de OpenAI cu reîncercările lui pe schemă și apelarea de unelte, botul de Telegram, schedulerul de cron cu istoricul lui, serverul MCP, e moștenit. Repari un bug în bază, îi dai tag, ridici pinul în trei copii, gata. Ceea ce e tot rostul și motivul pentru care claudebox v2 există în primul rând.
Cum Rulezi Una
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:latestApoi îi vorbești ca și cum ar fi OpenAI, sau lovești /run direct, sau îndrepți un client MCP spre el, sau nu atingi HTTP deloc și doar lași cronul să îl tragă la ora nouă dimineața.
Bucla de restart a containerului care arăta ca absolut nimic
Cel mai bun bug pe care l-a produs chestia asta, pentru că fiecare semnal pe care ți-l dădea era o minciună.
Simptom: în modul api containerul repornește periodic. Fără crash, fără OOM, fără stack trace. Codul de ieșire e 0. Bucla se corelează cu activitatea de cereri, nu cu memoria sau cu uptime-ul, ceea ce te trimite să te uiți complet aiurea.
Ce se întâmpla de fapt: subprocesul agentului împărțea grupul de procese cu ce pornise serverul. În modul api serverul e PID 1. Așa că atunci când CLI-ul agentului, sau orice unealtă născută de agentul însuși, livra un SIGTERM sau SIGINT grupului ăluia comun, îl prindea și uvicorn și se închidea. Curat. Deliberat. Ieșire 0, treabă făcută.
Și nu îl vedeai niciodată, pentru că uvicorn.run(..., log_config=None) îi suprimă propriile linii de închidere. O închidere ordonată, tăcută și absolut corectă, pe care politica de restart a containerului o desfăcea apoi, iar și iar.
Reparația e un singur flag în aicodebox/shared/runner.py, start_new_session=True la nașterea agentului, atât pe calea sincronă run(), cât și pe cea de streaming run_stream(). Sesiune proprie, grup de procese propriu, semnalele nu mai călătoresc în sus.
Testele de regresie spionează ambele primitive de naștere și verifică faptul că flagul e transmis, așa că dacă îl scapi din oricare loc pică buildul, în loc să reintroduci pe tăcute o buclă pe care nimeni n-o poate citi din loguri.
Fixează v0.14.5, nu v0.14.4. Aceeași reparație, dar pyproject.toml stătuse la 0.14.0 de la v0.14.1 până la v0.14.3, în timp ce Makefile-ul derivă tagul imaginii din el, iar v0.14.5 e relansarea în care fișierul și tagul se pun în sfârșit de acord.
Pe Scurt
Agentul pe care îl folosești azi nu e agentul pe care îl vei folosi peste un an. Nu e pesimism, e pur și simplu ritmul de lansare, spațiul ăsta se rescrie la câteva luni, iar orice construiești strâns lipit de un singur CLI e schelă cu termen de expirare.
Deci nu te lipi de el. Pune suprafețele într-o imagine de bază, pune agentul în spatele unui adaptor cu două metode, iar când aterizează următorul scrii patruzeci de linii în loc să dai fork la nouă sute. Am făcut-o de trei ori până acum. A treia a durat o după-amiază.
github.com/psyb0t/docker-aicodebox
Licențiat sub WTFPL, pentru că o imagine de bază care există special ca să nu fii nevoit să îi dai fork ar fi un lucru prost pe care să pui o licență restrictivă.