codexbox: Quatre Variables d’Environnement et un mkdir Entre Toi et le CLI d’OpenAI

J’ai déjà écrit le coup de gueule « pourquoi j’ai arraché la plomberie de mes propres repos pour en faire une image de base » dans le post sur aicodebox, donc je ne le refais pas ici. Version courte: j’ai construit claudebox pour Claude Code, je lui ai fait pousser une API HTTP, un endpoint compatible OpenAI, un serveur MCP, un bot Telegram et un scheduler cron, puis j’ai voulu exactement la même chose pour un deuxième agent et j’ai refusé de copier-coller neuf cents lignes de FastAPI et de rendu markdown Telegram dans un nouveau repo une troisième fois. Alors la plomberie a été arrachée pour donner aicodebox, une image de base agnostique à l’agent, et chaque agent particulier est devenu une image enfant fine qui n’implémente qu’une classe d’adaptateur.
codexbox, c’est cet adaptateur pointé sur le CLI Codex d’OpenAI. Même base, même surface REST, même bot Telegram, même scheduler cron, la seule chose réellement nouvelle ici c’est le code qui traduit une requête générique « lance ce prompt » en la combinaison de flags délirante que Codex veut ce jour-là, plus la danse d’authentification autour de deux façons complètement différentes de payer pour ça.

Le Problème Spécifique du CLI d’OpenAI

Le CLI Codex va très bien. L’ergonomie du CLI Codex en tant que truc qu’on lance programmatiquement, c’est une autre histoire. Quelques points saillants relevés en lisant vraiment la chose au lieu de croire sa doc:

  • Il plante au démarrage à cause d’un dossier qui n’existe pas. Pose CODEX_HOME sur un chemin sans le créer d’avance, et codex se contente d’échouer au lieu de faire le mkdir -p d’une ligne que littéralement tous les autres CLI de la Terre font pour toi. Plus là-dessus plus bas, parce que c’est le meilleur et le pire détail de tout le repo.
  • Le flag de contournement du sandbox se lit comme une clause juridique. --dangerously-bypass-approvals-and-sandbox. Pas --yolo, pas -y, une phrase complète, sans doute pour que personne ne prétende ne pas avoir su ce qu’il faisait quand son container est allé rm -rf quelque chose.
  • La reprise est une sous-commande, pas un flag. Tous les autres CLI d’agent que j’ai câblés prennent --continue ou --resume <id> comme flag sur la commande de lancement normale. Codex t’oblige à appeler exec resume <id> ou exec resume --last comme verbe distinct, ce qui veut dire que l’adaptateur doit construire une forme d’argv complètement différente selon que tu reprends ou pas.
  • Il n’y a pas d’interrupteur « coupe tous les outils ». update_plan est inconditionnel et apply_patch reste tant qu’un environnement local existe, donc un vrai mode sans outils veut dire virer les outils shell et recherche web par la config et forcer le sandbox en lecture seule en ceinture et bretelles, parce que la config seule ne peut pas le neutraliser complètement.
  • La sortie structurée ne prend qu’un chemin de fichier. Codex a une validation native de schéma JSON via --output-schema, ce qui est franchement mieux que ce qu’ont les autres adaptateurs de cette base, pas besoin de reprises d’autocorrection, mais il n’accepte qu’un fichier sur disque, pas un schéma inline, donc l’adaptateur doit écrire ton schéma dans un fichier temporaire à chaque appel.
  • Son propre flux JSON te ment en se disant JSON. Lance codex exec --json et tu obtiens un flux JSONL de ThreadEvent sur stdout, sauf que codex intercale aussi des lignes de log en texte brut du genre ERROR ... directement dans ce même stdout. Tu n’as pas le droit de supposer que chaque ligne se parse. Tu parses, tu attrapes l’échec de décodage, tu le comptes, tu continues.

Rien de tout ça ne rend Codex mauvais. Ça fait de Codex un CLI construit pour un humain qui tape dans un terminal, pas pour un programme qui le lance en boucle, ce qui est précisément le trou que l’adaptateur existe pour boucher.

Quatre Variables d’Environnement et un mkdir

Le Dockerfile pose exactement quatre variables d’environnement pour câbler l’adaptateur dans l’image de base, toutes dans un seul bloc:

ENV AICODEBOX_ADAPTER=codexbox.adapter:CodexAdapter 
    AICODEBOX_AGENT_BINARY=codexbox-agent 
    CODEXBOX_IMAGE_VARIANT=minimal 
    CODEX_HOME=/home/aicode/.codex

AICODEBOX_ADAPTER pointe la machinerie de chargement d’adaptateurs de la base sur CodexAdapter, pour qu’elle sache construire argv spécifiquement pour codex. AICODEBOX_AGENT_BINARY glisse un script de lancement à la place d’appeler codex directement pour l’usage interactif et passthrough (pourquoi, plus bas). CODEXBOX_IMAGE_VARIANT signale juste sur quelle image tu es (minimal ici, full dans le build avec la chaîne d’outils). Et CODEX_HOME est celle qui compte vraiment, à cause de ce plantage au démarrage dont je parlais.
Codex lit auth.json, config.toml et ses fichiers de déroulé de session depuis $CODEX_HOME. Si cette variable n’est pas posée, codex retombe sur un défaut raisonnable. Mais si elle est posée, ce qui est obligatoire ici, puisque tout l’intérêt est de la bind-monter depuis l’hôte pour qu’un login survive à un container détruit et recréé, et que le dossier visé n’existe pas encore, codex refuse de démarrer. Pas un avertissement, pas une création automatique, une erreur dure. Donc juste après le bloc ENV, le Dockerfile fait ceci:

RUN mkdir -p /home/aicode/.codex && chown -R aicode:aicode /home/aicode/.codex

Un simple mkdir -p et un chown, cuits dans l’image au moment du build, uniquement pour qu’un CLI écrit par une boîte avec cent milliards de dollars en banque ne s’écroule pas la première fois que tu le pointes sur un bind mount tout neuf. C’est sincèrement mon détail préféré de tout le repo, pas parce que c’est malin, c’est l’inverse de malin, c’est un contournement pour un appel à mkdir qui manque. Mais c’est le genre de truc qu’on ne trouve qu’en lisant réellement le Dockerfile au lieu de croire un README, et ça explique pourquoi CODEX_HOME est pré-créé au lieu d’être simplement déclaré et laissé à codex.

Ce Que CodexAdapter Implémente Vraiment

Le contrat d’adaptateur de l’image de base te donne une poignée de méthodes à remplir, et CodexAdapter les implémente toutes: validate (rejette les valeurs d’effort de raisonnement inconnues, avertit et ignore une allowlist d’outils pour laquelle codex n’a pas d’équivalent), build_argv (la viande, traduit une requête de lancement générique en la vraie soupe de flags de codex), translate_auth (ne fait rien, parce que codex lit OPENAI_API_KEY / OPENAI_BASE_URL / auth.json nativement, pas d’aliasing nécessaire), parse_output et parse_events (décodent le flux JSONL en un résultat normalisé, en sautant les lignes de log non-JSON intercalées), parse_stream_event (transforme les lignes individuelles en deltas de flux canoniques pour les endpoints de streaming en direct), interactive_argv et passthrough_argv (invocation brute du binaire codex pour le TUI et le passthrough), et auth_paths (dit à l’image de base où vit le fichier d’identifiants pour qu’elle puisse vérifier son existence).
build_argv est l’endroit où vivent les décisions intéressantes. Un lancement sans resume ni noContinue posés retombe par défaut sur exec resume --last, continuant par défaut la session la plus récente du workspace, même idée que claudebox. systemPrompt se mappe sur -c instructions=..., qui remplace le prompt système intégré de codex; appendSystemPrompt se mappe sur -c developer_instructions=..., qui ajoute à la place un message de rôle developer à côté. L’effort de raisonnement passe par -c model_reasoning_effort=<level> plutôt que par un flag de réflexion dédié. Et chacun de ceux-là est passé comme un unique élément d’argv, pas interpolé par un shell, donc un texte de prompt multiligne et les prompts système survivent tels quels sans se faire massacrer par un shell quelque part dans le tuyau.

Ce Que codexbox-agent.sh Restaure Et Que la Base Laisse Tomber

Le mode passthrough d’aicodebox est délibérément bête: pour les invocations interactives et one-shot, il lance juste exec $AICODEBOX_AGENT_BINARY "$@" et s’écarte. Très bien pour une image de base générique, mais ça veut dire que les défauts côté container, pas de demandes d’approbation, continuation de session sensée, ne s’appliquent pas automatiquement au TUI interactif propre de Codex, seulement aux modes serveur pilotés par l’adaptateur. codexbox-agent.sh est le script branché via AICODEBOX_AGENT_BINARY pour remettre ces défauts en place, spécifiquement pour les chemins TUI et passthrough CLI:

case "${1:-}" in
    login | logout | mcp | mcp-server | doctor | completion | update | resume | review | apply | sandbox | debug | features | help | -V | --version | -h | --help)
        exec "$CODEX_BIN" "$@"
        ;;
    exec | e)
        # inject --dangerously-bypass-approvals-and-sandbox unless already present
        exec "$CODEX_BIN" "$sub" "$BYPASS" "$@"
        ;;
esac
# bare interactive TUI — defaults to resuming the workspace's last session
exec "$CODEX_BIN" resume --last "$BYPASS" ${args[@]+"${args[@]}"}

Les sous-commandes d’auth et de maintenance (login, logout, mcp, doctor, update, et ainsi de suite) tournent parfaitement telles quelles, aucun flag injecté, et c’est ce qui permet à codexbox login --device-auth de piloter le flux OAuth ChatGPT sans y toucher. Tout le reste, une session interactive nue ou un appel exec, reçoit automatiquement le flag de contournement injecté pour que tu ne tapes pas --dangerously-bypass-approvals-and-sandbox à la main à chaque fois, et le TUI nu reçoit en plus le même défaut « continue la dernière session de ce dossier » qu’utilise l’adaptateur en mode serveur. Les modes serveur (API, Telegram, cron, MCP) ne touchent jamais ce script, ils passent par CodexAdapter.build_argv et lancent codex directement.

Double Auth: Clé d’API ou Abonnement ChatGPT, Un Seul Fichier

Codex supporte deux modes d’authentification et codexbox doit garder les deux fonctionnels sans qu’ils se marchent dessus. Un script d’init qui tourne une fois par boot (10-codex-auth-config.sh) lit tout auth.json existant, vérifie son champ auth_mode, et ne sème une clé d’API via codex login --with-api-key que quand il n’y a pas d’auth existante ou que l’auth existante est déjà en mode apikey. Un login OAuth d’abonnement ChatGPT gagne toujours et n’est jamais écrasé, même si OPENAI_API_KEY se trouve posée dans l’environnement. La raison pour laquelle la clé d’API a quand même besoin d’une étape de login au lieu d’être lue directement depuis la variable d’environnement: la sous-commande exec de codex n’accepte pas une OPENAI_API_KEY nue pour l’auth, il lui faut auth.json écrit sur disque d’abord.
Les deux modes d’auth convergent vers exactement le même fichier, ce qui est aussi exactement pourquoi CODEX_HOME doit survivre à un container rasé. CodexAdapter.auth_paths() renvoie un seul chemin:

def auth_paths(self) -> list[str]:
    home = os.environ.get("HOME", "/home/aicode")
    cfg = os.environ.get("CODEX_HOME", f"{home}/.codex")
    return [f"{cfg}/auth.json"]

Bind-monte ~/.codex depuis l’hôte, et quelle que soit la façon dont tu t’es authentifié, une clé d’API stockée ou un token OAuth ChatGPT, ça survit à chaque recréation de container, parce que ça ne vit pas dans la couche inscriptible du container, ça vit sur le montage que le dossier pré-créé et chowné de la base a rendu sûr pour y pointer CODEX_HOME dès le départ.

Minimale contre Full: Même Auth, Même Adaptateur, Plus de Chaîne d’Outils

L’image par défaut psyb0t/codexbox:latest, c’est codex plus Node, Python, uv, Docker, git, jq et curl, de quoi faire tourner l’agent et pas grand-chose de plus. Dockerfile.full construit une deuxième image par-dessus la minimale, épinglée par digest sur un tag publié précis, et empile la même chaîne d’outils de dev généraliste que livre l’image full de claudebox: Go avec golangci-lint, une chaîne Python via pyenv, une chaîne Node installée depuis un lockfile pnpm commité avec les scripts de cycle de vie désactivés, GitHub CLI, Terraform, kubectl, Helm, et le tas habituel d’outils de build, de clients de bases de données et de débogueurs, éclatés dans les dossiers de dépendances full-go/, full-node/ et full-python/ pour que les entrées de chaque chaîne soient épinglées par version et vérifiées par somme de contrôle plutôt que « ce qu’apt a envie d’installer aujourd’hui ». Même adaptateur Codex, même entrypoint, même comportement d’auth, le build full ajoute juste des outils autour, et chaque téléchargement dedans est vérifié par somme de contrôle contre un SHA256 épinglé avant d’être cru.

Le Côté Hôte: wrapper.sh et install.sh

install.sh tire l’image choisie (minimale par défaut, full via CODEXBOX_FULL=1), crée ~/.codex et un dossier de clés SSH pour le git-over-SSH dans le container, télécharge wrapper.sh, y cuit le tag d’image résolu, et l’installe sur ton PATH sous le nom codexbox. À partir de là:

export OPENAI_API_KEY=sk-...          # or: codexbox login --device-auth
codexbox                              # interactive TUI, continues last session for this dir
codexbox --no-continue                # same, but forces a fresh session
codexbox exec "fix the failing test"  # one-shot exec, output to your terminal
echo "summarize README.md" | codexbox exec -
codexbox stop                         # kill this dir's running container(s)
codexbox clear-session                # drop saved codex sessions, keep auth + config

Le boulot du wrapper est délibérément étroit: résoudre l’image, monter le workspace, le ~/.codex persisté et le socket docker, transmettre l’auth et toutes les variables CODEXBOX_ENV_* / CODEXBOX_MOUNT_*, et gérer un container par dossier, par son nom. Chaque décision réelle de flag codex, injection du contournement, reprise contre session neuve, passthrough de sous-commandes, vit à l’intérieur de l’image dans codexbox-agent.sh, pas dans le script hôte. Le wrapper transmet tes arguments intacts et laisse le container décider quoi en faire.

Installer Ce Que Tu Viens de Construire, Pas Ce Qui Est sur Docker Hub

Le chemin d’installation supposait que tu voulais l’image publiée. Très bien pour l’utiliser, inutile pour bosser dessus, tu construirais une image locale, tu lancerais l’installeur, et il irait tirer la copie du registry par-dessus tes changements.

Donc il y a des cibles pour ça maintenant:

make install          # build the minimal image, then install the wrapper against it
make install-full     # same, full variant
make install-wrapper  # wrapper only, against an image you already built

Les trois passent par CODEXBOX_SRC_LOCAL=true, qui dit à install.sh de sauter le docker pull et d’utiliser l’image locale choisie. Ça échoue bruyamment si cette image n’a pas été construite, plutôt que de retomber en silence sur un pull, vu que tout l’intérêt est de ne pas prendre la publiée. Ce flag marche aussi seul si tu appelles install.sh directement.

make install-wrapper est celui que tu utiliseras vraiment en boucle: les correctifs du wrapper hôte n’exigent plus de reconstruire l’image juste pour tester un changement dans un script shell.

Les Commandes de Gestion Ont Arrêté de Squatter Ta Session

Le wrapper faisait passer tout par le container interactif persistant. Correct pour une vraie session de code et faux pour les sous-commandes de ménage, plugin, doctor, sandbox, debug, review, apply, resume, archive, delete, qui reçoivent maintenant chacune un container --rm jetable.

Lié: chaque sous-commande Codex de premier niveau supportée est désormais transmise telle quelle, au lieu d’être traitée comme une invite de reprise interactive. Taper une vraie sous-commande te donnait avant une session que tu n’avais pas demandée.

Tu peux garder ce que Codex a réellement dit

Par défaut la réponse est le résumé utile. Pose eventMode sur "full" au lancement et tu récupères à la place les enregistrements propres de Codex, intacts: raisonnement, exécution de commandes, changements de fichiers, activité MCP, activité web, mises à jour de todo, usage. Rien de replié dans un tour, rien de tronqué pour garder la charge utile propre. Ça compte surtout quand un lancement a fait quelque chose que tu n’attendais pas et que le résumé est précisément la couche qui a jeté les preuves.

Trois correctifs qui valent le coup de dépasser ton pin

codex update échouait avec EACCES. codex était installé dans le préfixe global par défaut de npm sous /usr, ce qui laisse le paquet possédé par root dans /usr/lib/node_modules, alors que le container tourne sous l’utilisateur non privilégié aicode. Du coup l’auto-updater du CLI ne pouvait pas écrire dans sa propre installation. Il s’installe sous /home/aicode/.local maintenant, avec un /home/aicode/.npmrc qui épingle le même préfixe à travers la descente de privilèges de l’entrypoint.
L’image full héritait d’un fossile. Dockerfile.full pointait par défaut sur une base qui avait dérivé jusqu’à v0.2.0, si bien que latest-full était construite sur une image en retard de plusieurs mois sur latest. Le workflow étagé publie maintenant l’image minimale d’abord et la variante full hérite de l’actuelle, ce que « même base, plus d’outils » était censé vouloir dire depuis le début. Toute cette classe de bug a disparu depuis la v0.5.11, parce que codexbox a cessé de posséder une chaîne d’outils susceptible de dériver: l’outillage partagé Go, Node, Python, éditeurs, clients de bases de données et ops est remonté dans psyb0t/aicodebox:v0.15.0-full, et l’image full de codexbox en part simplement. En supprimant au passage environ 15 800 lignes de lockfiles. Il ne reste plus de deuxième copie pour prendre du retard.
Les sous-agents volaient la conversation. La continuation de Codex est reprise par workspace, et un déroulé de sous-agent plus récent pouvait détourner le fil que le cron, l’API, MCP ou Telegram continuaient. Il persiste maintenant le fil exec de premier niveau confirmé par workspace canonique et reprend exactement cet ID, donc un lancement de sous-agent ne peut pas devenir en douce la chose depuis laquelle ton job cron continuera demain.

Des boîtes qui lancent des boîtes

Installe codexbox, claudebox et pibox dans le même dossier et chaque wrapper monte les deux autres en lecture seule sur /usr/local/bin/<name>. Codex peut alors appeler un autre agent en pleine session, ce qui sert surtout quand tu veux un deuxième modèle sur le même diff.
Les lancements imbriqués portent un contexte hôte versionné, parce que « où est le workspace » a une réponse différente dans un container et sur la machine qui l’a démarré: AICODEBOX_LAUNCH_CONTEXT_VERSION=1, plus AICODEBOX_HOST_HOME, AICODEBOX_HOST_WORKSPACE, AICODEBOX_HOST_WRAPPER_DIR et un home et un chemin de wrapper par agent, pour chacun des trois. Un lancement imbriqué saute aussi les écritures de fichiers d’auth qu’effectue un lancement de premier niveau, donc un enfant ne peut pas réécrire les identifiants de l’appelant.
AICODEBOX_ENV_* et AICODEBOX_MOUNT_* transmettent environnement et montages à toutes les boîtes d’un coup, à côté des CODEXBOX_ENV_* et CODEXBOX_MOUNT_* par boîte. AICODEBOX_MANAGED_INSTALL=1 donne aux scripts de provisioning une installation non interactive qui n’écrasera pas une clé SSH existante, avec CODEXBOX_INSTALL_DIR et CODEXBOX_BIN_NAME pour décider où ça atterrit et comment ça s’appelle. L’installeur va aussi chercher wrapper.sh sur le tag de release correspondant plutôt que sur master, donc une installation épinglée récupère le wrapper de cette version.

Toutes les Entrées de la Boîte

Au-delà du shell interactif et de l’exec one-shot, les mêmes surfaces serveur d’aicodebox s’appliquent ici, avec codex qui tourne dessous:

  • Mode API: un serveur FastAPI avec /run (lancements d’agent sync ou async), /files/* (lister, lire, écrire, supprimer dans le workspace, avec contrôle de traversée de chemin), et un endpoint /openai/v1/chat/completions compatible OpenAI. Exige que CODEXBOX_AVAILABLE_MODELS soit posée explicitement, parce que codex n’a pas de liste de modèles en dur, elle est pilotée par le serveur, donc il n’y a pas de défaut raisonnable sur lequel retomber.
  • Mode Telegram: du texte entre, codex tourne, le Markdown est rendu en HTML au retour. Des surcharges par chat pour le modèle, l’effort de raisonnement et les prompts système, persistées à travers les redémarrages.
  • Mode Cron: des jobs planifiés pilotés par croniter qui déclenchent codex avec une instruction fixe selon un horaire, chaque lancement journalisé dans un dossier d’historique par job.
  • Mode MCP: la surface MCP propre à l’image de base, avec ses opérations de fichiers et son lancement de prompts, montée sur /mcp quand le mode API est actif ou en processus sidecar sinon. C’est distinct des capacités client et serveur MCP propres à codex, qui ne sont câblées dans rien de tout ça.

La seule chose qui mérite d’être dite explicitement: toolsAllowlist et noTools sont acceptés sur /run pour la compatibilité d’API avec les autres agents de cette base, mais codex n’a pas d’allowlist d’outils intégrés nommés. Une allowlist est journalisée et ignorée. noTools est la seule exception réellement honorée, elle retire les outils shell et recherche web de la requête et force le sandbox en lecture seule, parce que la config seule ne peut pas retirer apply_patch ni update_plan.

Est-Ce Que Ça Valait le Coup

codexbox n’est la réécriture de rien du tout. C’est ce que « ajouter un agent de plus » est censé coûter une fois le vrai travail d’infrastructure fait ailleurs: un Dockerfile, quatre variables d’environnement, un mkdir -p pour un dossier que le propre CLI d’OpenAI ne se crée pas tout seul, et une classe Python qui connaît le vocabulaire de flags spécifique à Codex. Tout le reste, l’API, le bot Telegram, le scheduler cron, la surface MCP, tout le wrapper hôte, appartient à aicodebox, intact.
Si tu veux toute la logique de l’image de base, lis le post sur aicodebox. Si tu veux le frère Claude Code, c’est claudebox. Si tu veux juste une boîte qui fait tourner Codex sans que le CLI d’OpenAI se vautre sur un dossier manquant, c’est sur GitHub. Elle fait un seul boulot et elle a arrêté de se vautrer, ce qui est tout ce que j’ai jamais voulu d’elle.

L’Installer Dans Ton Agent

Codex peut installer la chose qui fait tourner Codex. Tout ce qui est sous .agents/ est catalogué dans un seul marketplace, donc ça fait deux commandes:

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

Codex utilise le même marketplace avec un verbe différent, codex plugin add codexbox@psyb0t, parce qu’il n’existe pas de codex plugin install. Il trouve aussi le skill tout seul dans un checkout du repo, puisqu’il scanne .agents/skills/ nativement sans que rien ne soit installé. Il est aussi listé sur le MCP Registry officiel maintenant, donc un client qui résout ses serveurs depuis là peut le trouver sans qu’on lui donne une URL.