11 KiB
Architecture
CodexPi utilise quatre services applicatifs sur un réseau Compose privé.
| Service | Rôle | Port conteneur | Publication hôte |
|---|---|---|---|
nginx |
reverse proxy unique | 8080 | 0.0.0.0:8765 (LAN) |
django-web |
interface React, authentification et sessions Django | 8001 | aucune |
api |
FastAPI/Uvicorn, REST, WebSockets, terminal, Git, Docker, Codex | 8000 | aucune |
nginx envoie /api/auth/*, /api/bootstrap et les fichiers de l'interface à Django. Les autres routes /api/* et /ws/* vont à FastAPI. FastAPI valide directement la session Django stockée en base et exige le jeton CSRF de la session pour les mutations et WebSockets.
Le frontend React est compilé dans l'image puis servi par Django. Les données persistantes sont dans ./data : base Django, base CodexPi, sessions JSONL et journal de sécurité. Le conteneur API monte uniquement la racine de projets autorisée, l'identité Codex existante et le socket Docker nécessaire à la page Docker. Le bind de /home/config utilise une propagation rslave : les supports montés sous cette racine sur l'hôte, y compris après le démarrage de l'API, deviennent visibles au même chemin dans le conteneur sans propager en retour les montages du conteneur vers l'hôte.
Le connecteur de machine distante ouvre uniquement des requêtes sortantes vers nginx. Les tâches list, read et write transitent par une file SQLite ; aucun terminal ni port entrant n'est exposé sur le PC associé.
Le bridge IA PC suit un modèle différent parce qu’OpenCode doit conserver le
stream HTTP d’Ollama pendant l’inférence. Une association à usage unique
enregistre l’adresse privée du PC et un jeton chiffré côté CodexPi. Le proxy
Python du PC écoute sur le LAN, authentifie chaque requête et n’autorise que les
routes Ollama de découverte et de génération ; l’API Ollama brute reste liée à
127.0.0.1. CodexPi produit sous data/local-ai/ un fichier OpenCode et un
jeton privés, puis les transmet uniquement aux workers OpenCode. Un seul bridge
est actif à la fois afin de rendre le routage et le modèle par défaut
déterministes.
Les tâches Codex écrivent un flux JSONL persistant et peuvent reprendre après fermeture du navigateur. Le terminal utilise tmux lorsqu'il est disponible. Un projet est toujours identifié par UUID ; les chemins sont canonisés côté serveur et contrôlés contre les racines autorisées/interdites.
L’ordonnanceur autorise par défaut trois moteurs Codex simultanés pour trois projets distincts. Des verrous de fichiers partagés entre les workers imposent à la fois la capacité globale configurée par CODEXPI_MAX_CONCURRENT_AGENTS et l’exclusion mutuelle au niveau du projet. Les trois espaces globaux (Agent maître, Agent Raspberry Pi et Agent Bibliothécaire) restent des projets indépendants : leur marqueur réservé __global_agent__ sert uniquement à l’interface et ne crée pas de verrou partagé. Un groupe associe explicitement plusieurs projets locaux du même propriétaire grâce à la relation plusieurs-à-plusieurs project_group_members ; un projet peut donc participer à plusieurs groupes. Une tâche acquiert, dans un ordre stable, tous les verrous de ses groupes afin d’éviter les interblocages et les exécutions concurrentes sur une ressource partagée. Les chemins des autres membres sont injectés comme références de lecture pour Codex SAFE, sans être ajoutés aux racines d’écriture du sandbox, et les derniers comptes rendus alimentent un contexte commun asynchrone. Pour un projet en FULL ACCESS, ces chemins sont également injectés quel que soit le moteur avec une interdiction conversationnelle d’écrire ailleurs sans demande explicite ; le backend ne présente pas cette consigne comme une isolation, car le sandbox est désactivé. Les autres moteurs en SAFE ne reçoivent que les noms et comptes rendus. Ainsi, plusieurs projets indépendants avancent en parallèle et les membres d’un groupe restent sérialisés lorsqu’ils pilotent la même ressource.
Les orchestrations sont des machines à états persistantes dans SQLite. Les validations d’ébauche utilisent une empreinte de version ; les tests et la recette sont liés au commit vérifié. Une correction réutilise la discussion, la branche et le worktree existants. Une approbation est invalidée dès que la révision change.
Le chantier autonome de remédiation possède sa propre machine à états persistante (remediation_runs, remediation_step_runs, remediation_events, remediation_self_repairs). Un seul chantier peut être actif par propriétaire. Une boucle backend loue brièvement le chantier, attend la fin de la tâche courante, puis alterne agent projet et Agent maître. Le maître rend un verdict structuré approve, correct ou block; une correction réutilise la conversation du projet et le cycle est borné à trois reprises. Tout blocage est transmis à CodexPi (Self) dans une conversation persistante propre au chantier. La boucle surveille aussi cette tâche lorsque le chantier est BLOCKED; un verdict structuré fixed peut ajouter une précision bornée au prompt courant puis reprendre automatiquement le même fil, tandis que needs_user laisse le chantier bloqué avec une raison visible. Les auto-réparations sont bornées à trois incidents par chantier et leur relance de format à une tentative. Une reprise bloquée peut rebaser les textes du plan courant, mais le backend refuse toute modification du nombre, de l’ordre, de la cible ou du niveau de risque des étapes ; les conversations et preuves existantes sont conservées. Les étapes restent séquentielles pour respecter leurs dépendances, mais elles utilisent l’ordonnanceur normal : les autres projets et agents peuvent continuer en parallèle dans la limite globale. Le dernier état, les discussions, les auto-réparations et les événements survivent au redémarrage de l’API et à la fermeture de la PWA.
Le centre de coordination repose sur agent_decisions, agent_incidents, agent_runtime_policies, agent_project_policies, specialist_agents et specialist_delegations. À la fin d’une tâche, le worker interprète uniquement deux marqueurs JSON stricts : une demande de décision et un appel de compétence. Leur enregistrement appartient à la même transaction que la fin de la tâche ; une panne du sous-système de coordination ne peut toutefois pas empêcher la tâche principale de se terminer. Une décision met en pause la branche de délégation éventuelle et sa réponse est réinjectée dans la même conversation. La boucle de coordination surveille les réparations Self, l’auto-construction des spécialistes et les délégations. Une délégation imbriquée reprend d’abord le fil du spécialiste appelant, puis le résultat final remonte au projet source. La profondeur maximale vaut deux. Un bail SQLite court (agent_coordination_lease) garantit qu’une seule instance traite ces transitions pendant une bascule blue/green ; le bail expire automatiquement si l’instance disparaît.
Les politiques de ressources s’appliquent dans session_worker avant l’acquisition d’un emplacement moteur. Elles ordonnent les tâches en attente, limitent le nombre de moteurs simultanés, suspendent l’admission sous pression CPU/mémoire et réservent les tâches d’arrière-plan à leur fenêtre horaire. Les verrous déjà acquis et les processus en cours ne sont jamais retirés. Le budget de durée alimente un signal persistant et une notification, sans envoyer de signal au processus ni modifier son statut. Les préférences de notifications sont enregistrées par utilisateur et filtrées avant l’ajout à la file Web Push.
Un spécialiste est représenté par un projet global __global_agent__ distinct, un dossier sous data/specialists/ et une discussion de construction. Son projet est créé en SAFE : il écrit seulement dans son atelier et reçoit le chemin source comme référence de lecture. Le résultat revient au projet par une nouvelle tâche de conversation ; l’agent source reste l’unique intégrateur. Désactiver un spécialiste empêche les nouvelles délégations sans supprimer son atelier ni ses fils.
Le canal de l’agent maître ne passe ni par un projet ni par le socket Docker de l’API. Un petit service hôte écoute un socket Unix sous data/host-agent/, authentifie chaque requête avec un jeton 0600 et exécute le CLI sous son identité système dédiée. L’API impose en plus l’authentification administrateur, CSRF et une confirmation par demande privilégiée. Le service n’est pas activé automatiquement par Compose.
La réconciliation de stockage est séparée du runtime applicatif. Son inventaire
lit les bases SQLite en mode mode=ro, recoupe les chemins persistés avec les
worktrees Git et les verrous, puis calcule les empreintes uniquement lorsqu'un
candidat de même catégorie, nom logique et taille existe dans une autre
instance. La quarantaine éventuelle utilise un verrou flock, revalide inode,
mtime, taille et empreinte contre le plan, puis effectue un renommage atomique
vers data/quarantine/<reçu>/payload. Un manifeste 0600, une date
d'éligibilité après grâce et un audit JSONL permettent la restauration. Si
l'audit échoue, le renommage est annulé avant de rendre l'erreur. Sous le verrou,
un second inventaire relit les bases et refuse le déplacement si une référence,
une activité ou une exclusion est apparue depuis le dry-run. Une base absente
ou illisible, ainsi qu'un schéma sans les tables et colonnes de référence
attendues, fait échouer la classification de toute l'instance en mode fermé.
Pendant toute la revalidation et le renommage, une transaction
BEGIN IMMEDIATE est maintenue sur chaque base dans un ordre stable : aucune
nouvelle référence SQLite ne peut être admise entre le contrôle et le mouvement.
L'inventaire vérifie chaque composant jusqu'à data/ avec lstat et ne parcourt
jamais une instance dont ce parent est symbolique.
Les caches Chromium mutualisables sont isolés du profil (cookies, historique,
sessions) sous data/browser-cache/<version majeure>/. Seuls Cache,
Code Cache et GPUCache peuvent être rapprochés, et jamais lorsqu'un verrou
navigateur est détenu. Les volumes browser-home existants restent privés et
hors inventaire implicite.
Le worker spécialisé host_session_worker relie à ce canal les conversations du seul projet global privilégié Pi Doctor/Agent Raspberry Pi. Il conserve les tables conversations, messages et prompt_queue, les verrous de projet et la limite globale d’agents ; seule l’exécution du moteur quitte le conteneur. Le chantier autonome peut ainsi reprendre le même fil après un blocage technique et soumettre le résultat hôte à l’Agent maître. Les autres projets continuent d’utiliser session_worker dans le conteneur.