opencode-mesh: apprendre à mes agents IA à toquer avant d'éditer
Faites tourner deux sessions OpenCode
sur le même dépôt et vous découvrirez vite leur ressource partagée préférée: le même fichier, en même temps, avec une confiance totale dans leur propre version. Les agents sont des collègues brillants avec la discipline territoriale d’une mouette. opencode-mesh
(v0.2.0, TypeScript, npm @log0u7/opencode-mesh) est le plugin que j’ai écrit pour que mes sessions se coordonnent au lieu de se percuter, en local et à travers le LAN.
Le gâteau à étages
Le plugin s’enregistre avec une ligne dans opencode.json ("plugins": ["@log0u7/opencode-mesh"]); sur un hôte v1 la clé reste plugin et v2 accepte cette forme en la normalisant au chargement. Depuis v0.2.0, le paquet expose les deux points d’entrée d’un coup. Au démarrage il ouvre un serveur HTTP sur le port 4399 et s’annonce en mDNS. De là, trois niveaux de coordination:
- Même machine, même repo: les sessions se découvrent via le registre local et s’échangent des messages.
mesh_statusmontre qui est vivant,mesh_send/mesh_inboxlivrent des notes de passation (“j’ai refactoré le parseur, le tien est périmé”). - Même machine, worktrees différents:
mesh_lock(path="src/api.ts", ttl=600)revendique un fichier avant de l’éditer; la revendication a un TTL pour qu’une session plantée ne verrouille pas le fichier pour l’éternité. Le lock est local à la machine: il s’applique entre les sessions qui partagent le répertoire de données de cet hôte, il ne fait pas partie du protocole. - À travers le LAN ou le tailnet: les nœuds se jumellent explicitement.
mesh_pairliste les peers découverts, l’opérateur approuve par hostname et empreinte et à partir de là chaque appel HTTP porte un bearer token par pair.
Refus par défaut, approbation par humain
Le modèle de sécurité est la partie où je suis le moins prêt à relâcher. Chaque endpoint exige un bearer token par pair; rien n’écoute ouvertement et c’est devenu une garantie structurelle: le serveur HTTP se lie à 127.0.0.1 par défaut, "bind": "lan" est le opt-in explicite vers toutes les interfaces et mesh_status comme l’offre de mesh_pair rapportent l’adresse réellement liée. Les enregistrements TXT mDNS ne portent que l’id du nœud et l’empreinte, jamais de secrets. Un côté émet un token de pairing à usage unique, l’autre complète le pairing avec et un humain clique l’approbation; les offres expirent au bout de 5 minutes et un token en attente ne suffit plus à installer un pair permanent: le nœud qui réclame doit déjà être connu de cet hôte (découverte mDNS ou mesh_pair explicite), l’auto-pairing d’un id inconnu est mort. La cérémonie Ed25519 a sauté, parce que rien ne signait avec: l’empreinte courte se dérive désormais du node_id, les ids existants survivent, la valeur de l’empreinte change, ce qui peut demander de re-pairer les peers. Un agent ne peut pas enrôler silencieusement le laptop d’un inconnu dans mon swarm; l’opérateur est la racine de confiance, pas le réseau.
Cela compte plus qu’il n’y paraît: ces plugins tournent dans le même domaine de confiance que mon shell. “Refus par défaut, approbation par humain” est la même discipline que je veux de ma config SSH et les agents ne l’héritent que si l’outillage l’impose.
L’orchestrateur de workers
Le second acte, c’est mesh_spawn(task="implémente le parseur", name="parser"): il crée un worktree opencode natif, spawn une session headless opencode run --dir <worktree> --format json avec permissions --auto (on revient aux approbations avec auto=false) et rend un worker id. mesh_workers liste les états de cycle de vie, mesh_worker_logs fait un tail de la sortie capturée, mesh_worker_stop envoie un SIGTERM à un worker, mesh_worker_remove nettoie le worktree et la ligne du registre. Quand la session parent s’arrête, dispose envoie un SIGTERM à chaque worker vivant: aucun processus orphelin qui brûle des tokens dans un worktree oublié.
Le durcissement du cycle de vie est venu avec l’usage: un spawn qui échoue avant le démarrage de l’enfant nettoie le worktree et la ligne morte du registre; un worker qui ne démarre jamais est finalisé failed au lieu de rester running pour toujours (un pid -1 n’est jamais persisté); mesh_worker_stop descend le groupe de processus entier, SIGTERM puis SIGKILL au bout de 3 s (les workers sont des chefs de groupe détachés, plus d’enfants orphelins); le stderr est drainé (limite de 64 Ko du pipe), les logs sont plafonnés à 512 Ko avec rotation et supprimés au remove; une ligne JSON coupée sur deux chunks de stdout est réassemblée avant parsing, pour que le session_id soit capturé; et un redémarrage de l’hôte réconcilie les pids morts. Les appels de création et de suppression de worktree du client REST 1.x sont bornés par un timeout de 15 s.
C’est la forme ennuyeuse du “multi-agent”: pas une conscience en essaim, mais un superviseur qui spawn des workers dans des worktrees isolés, les suit et nettoie. Chaque fois que j’ai été tenté d’ajouter un scheduler ou un graphe d’agents, je me suis rappelé que mesh_spawn plus les locks de fichiers couvrent déjà les cas d’usage que j’ai vraiment. Les workers tournent aussi avec OPENCODE_MESH_CHILD=1 et sautent le serveur, le mDNS et tous les mesh tools: un pool de N workers coûte N worktrees, pas N nœuds mesh. Et sous un hôte OpenCode 2.x, mesh_spawn et mesh_worker_remove ne passent plus le domaine ctx.worktree brut (inputs plats scopés projet, payloads résolus directement, sans l’enveloppe v1 {data, error}) au client REST: le domaine est ré-enveloppé dans la forme attendue, les deux générations partagent un seul chemin de code et un options.worktreeClient injecté prend la main. Le lock host-local du début, le durcissement de sécurité et tout ce cycle de vie sont fusionnés sur main, pas encore étiquetés: v0.2.0 ne porte que le point d’entrée double.
L’état: SQLite, un fichier
Tout vit dans ~/.local/share/opencode-mesh/mesh.db (honore XDG_DATA_HOME, créé avec permissions restrictives): registre de sessions, peers, tokens, locks, messages. Un fichier SQLite veut dire que mesh_status est une requête, pas un audit de fichiers JSON et que l’histoire des crashes est l’histoire des crashes de la base, une histoire avec des décennies de pratique derrière.
Toquer avant d’éditer
Au bout du compte, trois convictions sortent de ce plugin. L’opérateur est la racine de confiance, mDNS découvre, les humains approuvent, les tokens authentifient, pas d’auto-pairing jamais, le défaut loopback rend la chose vraie par construction. Le multi-agent est surtout du nettoyage, un spawn raté ramasse son worktree, un worker stoppé emmène ses enfants avec lui. Et un fichier SQLite bat cinq fichiers JSON, surtout quand l’alternative est de réinventer l’écriture atomique pour la cinquième fois.