L'atelier — l'architecture des outils de Lyra

Un essai narratif de fond sur le système d'outils de Roxabi Factory — du panorama en cinq strates jusqu'à un message qui voyage du clavier à la réponse.

Version 1.0 · Mis à jour

01

Pourquoi on est là

Avant de pousser la porte, prenons une seconde pour regarder l'état actuel. Depuis plusieurs mois, on parle d'outils. On en a construit, démonté, redessiné. À chaque conversation, le mot tool a glissé d'une réalité à l'autre : tantôt une fonction Python qu'un agent invoque directement, tantôt un service NATS qui tourne en arrière-plan sur une autre machine, tantôt un sidecar Quadlet qui attend ses requêtes, tantôt une compétence que le modèle apprend à orchestrer sans même savoir qu'il le fait. Quatre espaces de design vivent côte à côte sans vraiment se parler.

C'est comme un atelier où quatre artisans travaillent dans la même pièce, chacun à son établi, chacun avec son patois pour désigner les mêmes objets. Tant que chacun reste dans son coin, ça tient. Mais le jour où l'un d'eux demande un marteau à un autre, on s'aperçoit qu'ils ne pointent pas vers la même chose.

Ce qui suit, c'est le plan de l'atelier. Pas un nouveau dessin — juste la mise au propre de ce qui existe déjà, recadré pour qu'on parle d'une seule voix. On va monter sur la mezzanine, embrasser l'ensemble, redescendre objet par objet, ouvrir les portes, suivre les flux, regarder les serrures. À la fin, tu devrais pouvoir prendre n'importe quelle pièce et savoir précisément où elle se range, ce qu'elle touche, et à quoi elle sert.

Ce texte n'invente rien. Il met d'aplomb ce qui existe déjà — un langage commun, du built-in au sub-agent, sans drift.

02

Le panorama

Monte avec moi sur la mezzanine, deux mètres au-dessus du sol. D'ici, l'atelier se lit en cinq strates empilées. Pas cinq pièces à côté — cinq strates posées les unes sur les autres, comme on lirait la coupe géologique d'un terrain. Chaque strate repose sur celle qui la précède ; chaque strate sert celle qui lui succède.

Tout en bas, la strate de l'identité. C'est là qu'on dépose les marques — qui est cet agent, à quel groupe il appartient, quels outils il a le droit de toucher. Cette strate est physique : elle se matérialise en fichiers de configuration sur le disque, en clés d'authentification montées comme on glisse une clé dans un coffre, en volumes nommés qui découpent le territoire. Sans cette strate, rien n'a d'identité ; tout flotte.

Juste au-dessus, la strate du harness. Le harness, c'est l'enveloppe vivante qui contient la boucle de l'agent. Il prend le prompt, le passe au modèle, capte les demandes d'outils, les fait exécuter, renvoie les résultats au modèle, recommence — jusqu'à ce que l'agent dise j'ai fini. Le harness ne tient en main qu'une chose à la fois : la mémoire du tour en cours. Tout le reste — l'historique long, les données partagées, les gros fichiers — vit ailleurs.

Plus haut, la strate des outils. C'est le rayon où l'agent vient piocher ce dont il a besoin. Quatre catégories d'outils se côtoient sur ce rayon : ceux qui tiennent directement dans la main du harness, ceux qu'il appelle à distance par messages, ceux qui enchaînent plusieurs outils en une séquence déterministe, et ceux qui sont en réalité d'autres harness imbriqués. On y reviendra en détail — c'est la section la plus dense.

Encore au-dessus, la strate des contrats partagés. C'est le langage commun. Chaque pièce de l'atelier importe ces contrats : la définition de ce qu'est un tool, la forme d'un résultat, la structure d'un manifeste. Sans cette strate, chaque artisan retombe dans son patois — et c'est exactement ce qu'on est en train de corriger.

Et tout en haut, le modèle. Il regarde l'ensemble par-dessus l'épaule. Il ne voit que la surface : la liste d'outils qu'on lui tend, leur description, leurs paramètres. Il ne sait pas, et n'a pas besoin de savoir, qu'un outil tient dans la paume du harness pendant que l'autre est à dix mètres au bout d'un câble. Pour lui, c'est la même poignée.

Cinq strates, lisibles de bas en haut. Chacune fait une chose, ne déborde pas, et repose proprement sur la suivante.

03

Le harness

Redescends. Pousse cette porte — celle au milieu, marquée harness. C'est là que vit l'agent quand il pense.

À l'intérieur, c'est étonnamment simple. Au centre de la pièce, une boucle. Le harness reçoit un prompt complet, l'envoie au modèle, attend la réponse, regarde ce que le modèle lui demande de faire. Si le modèle réclame un outil, le harness va le chercher dans le rayon, attend le résultat, le redonne au modèle, et la boucle recommence. Elle tourne jusqu'à ce que le modèle décide qu'il n'y a plus rien à faire — ce qu'on appelle un end_turn. À ce moment-là, la boucle se referme, le harness rend la main, et le tour se termine.

Pendant que la boucle tourne, le harness ne tient dans la main qu'une seule chose : la mémoire du tour. Quels outils il a déjà appelés, quels résultats il a déjà reçus, à quel stade de la conversation il en est. Cette mémoire est volontairement éphémère. Dès que le tour se termine, elle est jetée. Le harness ne garde rien.

C'est important parce qu'on confond souvent les rôles. Tout ce qui doit survivre au tour vit ailleurs. L'historique long de la conversation est stocké par le hub, qui sait qui a parlé à qui et quand. Les gros artefacts — les images, les fichiers audio, tout ce qui pèse — sont déposés dans le blobstore, et seules leurs références circulent. La connaissance durable d'un agent — ses notes, ses préférences, ses traces — vit dans son propre volume sur disque. Le harness, lui, est volontairement amnésique. C'est ce qui le rend interchangeable.

Et il faut bien comprendre l'autre dimension : il n'y a qu'un seul binaire harness. Un seul. La même image de conteneur, le même code, partout. Ce qui change d'un agent à l'autre, ce n'est jamais le code du harness — c'est sa configuration au moment de l'instanciation. Même corps, identités différentes.

04

L'agent comme instance

Comment ce harness unique devient-il cinq agents distincts ? C'est là qu'on touche au pattern le plus élégant de toute l'articulation.

Quadlet — l'outil de Podman qui nous sert à orchestrer les conteneurs sous systemd — supporte nativement une notion qu'on appelle le template d'instance. Concrètement, on dépose un fichier .container avec un arobase dans son nom : factory-agent@.container. Ce n'est pas un conteneur fonctionnel à proprement parler — c'est un moule.

Quand on veut faire vivre un agent, on demande à systemd de démarrer factory-agent@telegram.service. systemd lit le moule, remplace l'arobase par telegram partout où il apparaît, et fabrique une instance concrète. Le volume monté n'est plus ~/.roxabi/factory/agents/ — c'est ~/.roxabi/factory/agents/telegram/. Le fichier d'environnement n'est plus env/.env — c'est env/telegram.env. Le secret NATS n'est plus factory-nats- — c'est factory-nats-telegram. Tout se résout proprement à partir de l'arobase.

Si demain on veut un agent Discord, un agent bot GitHub, un agent assistant personnel, on démarre factory-agent@discord.service, factory-agent@gh-bot.service, factory-agent@assistant.service. Chaque instance utilise la même image, le même binaire harness, mais habite un territoire isolé. Sa propre liste d'outils autorisés. Son propre prompt système. Ses propres secrets. Ses propres données sur le disque.

L'image qu'il faut garder en tête, c'est celle du moule à madeleines. Un seul moule, plusieurs empreintes, chacune unique mais cuite au même four, avec la même pâte. L'isolation est physique : un agent ne peut pas lire les volumes d'un autre, ne peut pas signer avec la clé d'un autre, ne peut pas usurper son identité. Mais on ne paie pas la duplication du binaire. La maintenance d'un harness pour tous ; l'identité propre à chacun.

Tout ce qui décide vraiment de ce qu'un agent peut faire — sa personnalité, sa liste d'outils, son modèle, son ton — vit dans le fichier d'environnement, sur le disque, versionné dans git, auditable par quiconque ouvre le repo. Lisible, traçable, révocable d'un commit.

05

Quatre couches d'outils

Sur ces deux primitives — le harness qui boucle et le bus de messages — on va maintenant pouvoir poser les outils. Et c'est là que l'architecture devient vraiment intéressante, parce qu'elle est à la fois simple en surface et stratifiée en profondeur.

Approche-toi un instant du rayon. Ce que tu vois, c'est une rangée d'objets qui se ressemblent. Chacun a un nom, une description, un contour de paramètres. Pour l'agent qui tend la main, ils sont interchangeables dans le geste : il saisit le nom, il passe les arguments, il attend le résultat. Mais si tu retournes l'un d'eux et que tu regardes en dessous, tu découvres que le transport varie complètement d'un outil à l'autre. C'est ce que j'appelle la surface uniforme sur un fond hétérogène — et c'est le cœur de ce qu'il faut comprendre dans cette section.

L0a — ce qui tient dans la paume. La première couche, L0a, c'est le fond du rayon. Ce sont les outils built-in, compilés directement dans le binaire du harness. Quand le modèle décide d'exécuter bash, ou de lire un fichier, ou d'appeler web_fetch, ou de publier sur un sujet NATS, ou d'invoquer l'outil gh — il n'y a aucune découverte, aucun appel réseau, aucun IPC. Le code s'exécute dans le même process, à côté du dispatcher, en mémoire partagée. La latence est proche de zéro. Tu tiens l'outil dans la paume, littéralement. Ce groupe est volontairement petit : bash, file_read, file_write, web_fetch, nats_publish et gh. Six primitives. Chacune fait une seule chose et la fait vite.

L0b — ce qui est à portée de fil. Un pas plus loin sur l'étagère, tu trouves L0b. À l'œil, ces outils sont identiques aux précédents : même nom, même description, même forme de paramètres. La main de l'agent les saisit exactement pareil. Mais la différence est sous la surface, dans le transport. L0b regroupe les outils distants — la voix (ses deux faces : TTS et STT), la génération d'image, le LLM, Postiz, XCLI. Chacun de ces outils vit dans son propre processus, souvent dans son propre Quadlet sur une machine séparée. Pour les atteindre, le harness envoie un message sur un sujet NATS. Il attend la réponse. Le résultat voyage par le réseau avant d'atterrir. Et voilà le point qu'il faut marteler : le modèle ne voit pas la différence entre L0a et L0b. Jamais. Pour lui, bash et voice.tts ont exactement le même poids, la même texture. Le transport — en process ou par NATS — est une affaire de couche inférieure qui se règle en silence.

L1 — la séquence chorégraphiée. Monte d'un cran. L1, c'est ce qu'on appelle un outil macro. Il est toujours présenté comme un seul outil à l'agent — une poignée, un nom, une description. Mais derrière, il exécute une séquence déterministe d'outils de niveau inférieur, sans jamais repasser par le modèle entre les étapes. Prends l'exemple concret qui cristallise le mieux cette idée : vault.add_from_url. Tu donnes une URL à cet outil, et il fait trois choses dans l'ordre — il va chercher la page (scrape, un appel L0b), il la résume (llm.summarize, encore un L0b), il range le résultat dans le vault (vault.add, L0a). Trois étapes, un seul geste visible depuis l'extérieur. L'agent n'a pas à orchestrer ces étapes lui-même — il délègue la séquence à l'outil macro, qui la pilote de manière déterministe. L'enchaînement est codé, testé, reproductible. Le modèle délègue la chorégraphie à quelque chose de plus fiable que lui-même pour cette tâche précise.

L2 — la partition glissée dans le prompt. L2 est d'une nature différente. Ce n'est plus un outil invoqué à l'exécution. C'est une instruction injectée dans le prompt système avant même que la conversation commence. Une skill — au format SKILL.md, dans la convention Anthropic — est une partition. Elle n'ajoute pas un nouvel outil à la liste ; elle apprend au modèle comment enchaîner les outils L0 et L1 qui existent déjà, dans un domaine particulier. Une skill pour la gestion de vault lui dit comment combiner web_fetch, vault.add_from_url et une série de vérifications sans que ces règles aient besoin d'être énoncées à chaque tour. La logique est posée une fois, à l'amont, dans le texte du prompt. Pour l'agent, L2 est invisible comme couche distincte. Il pense simplement mieux dans ce domaine.

L3 — un harness qui appelle un autre harness. Tout en haut de l'étagère, là où les objets pèsent le plus lourd, se trouve L3. Le sous-agent. Là encore, l'agent parent voit une poignée, une description, des paramètres. Mais quand il saisit cet outil, ce qu'il déclenche, c'est un deuxième harness complet — avec sa propre boucle de tour, son propre contexte, son propre jeu d'outils. Ce harness fils travaille dans l'isolement, accumule ses propres traces, rend un résultat et s'arrête. Le parent ne voit que le résumé final. Il ne perçoit ni la profondeur de la récursion, ni le nombre de tours que le fils a nécessités, ni les outils que le fils a appelés en chemin. C'est la couche la plus puissante et la plus coûteuse. Réservée aux tâches qui méritent vraiment leur propre espace de travail — des investigations longues, des refontes de code sur plusieurs fichiers, des analyses qui mobilisent elles-mêmes une palette d'outils.

COMPOSITION ↑ L3 — Sub-agent harness imbriqué et isolé · rend un résumé vers le parent L2 — Skill instruction dans le prompt système · enseigne le séquençage L1 — Macro séquence déterministe de primitives · sans second passage LLM L0b — Atomique distant satellite sur le bus NATS · découvert par heartbeat L0a — Atomique built-in compilé en process · toujours disponible DISPATCH UNIFORME
Quatre couches, un seul dispatcher. Du built-in primitif au sous-agent récursif, chaque outil passe par la même porte de contrôle d'accès et renvoie le même type de résultat — seul le transport change en dessous.

Recule d'un pas et regarde ce rayon dans son ensemble. Ces quatre couches ont toutes exactement le même contour visible pour le modèle : un nom, une description en prose, un schéma JSON de paramètres. Le modèle n'a pas de capteur pour distinguer ce qui va s'exécuter en mémoire de ce qui va traverser le réseau. L'abstraction est totale côté modèle ; la complexité est entièrement portée par le dispatcher — cet aiguilleur silencieux qui lit le type de l'outil, vérifie les droits, valide le schéma, et décide du transport sans consulter le modèle.

06

Worker n'est pas tool

Avant d'aller plus loin, il faut saisir une distinction que l'architecture défend avec soin — parce que la confondre coûte cher en déploiement et en clarté mentale : un worker n'est pas un tool.

Regarde les mains. Quand un charpentier tend un marteau, tu ne confonds pas la main et le marteau. La main, c'est ce qui tient, ce qui sélectionne, ce qui rend disponible. Le marteau, c'est ce qu'on saisit pour frapper. Un worker, c'est la main. Les tools sont les objets qu'elle tend.

Prends imageCLI comme exemple concret, parce qu'il rend la chose très tangible. imageCLI est un seul worker — une seule unité déployée, un seul processus qui tourne en arrière-boutique. Et pourtant, il expose plusieurs outils distincts : générer une image, lister les moteurs disponibles, annuler une génération en cours. Trois gestes différents, trois objets dans la main. Mais une seule main. Le modèle, quand il décide d'agir, ne voit jamais le worker. Il voit l'outil — il voit l'objet tendu, pas la main qui le tient. Le worker reste en coulisses, invisible au modèle, silencieux derrière le rideau.

Cette distinction a une conséquence très concrète au moment du déploiement. On ne déploie pas une unité conteneur par outil — ce serait absurde, comme commander un atelier entier pour chaque clé à molette. On déploie une unité par worker : un fichier .container Quadlet, un service systemd, une seule chose à démarrer, surveiller, redémarrer. Un worker, plusieurs outils, une seule unité. La granularité de déploiement est celle du worker, jamais celle de l'outil. Si imageCLI gagne un quatrième outil demain — disons, inspecter les métadonnées d'une génération — rien ne change dans l'infrastructure : la main est déjà là, elle tend simplement un objet de plus.

Le modèle voit une surface nette. L'arrière-boutique est propre. Un worker, pas un tool — une unité, plusieurs outils, déployés une fois.

07

La frontière invisible

Il reste une tension à résoudre, et elle est moins évidente que la précédente. Certains outils sont built-in — ils vivent dans le binaire du harness, sous la paume, prêts à répondre sans aucun aller-retour réseau. D'autres sont remote — ils attendent à l'autre bout d'un sujet NATS, derrière un worker qui tourne peut-être sur une autre machine. Ces deux catégories semblent mériter deux traitements différents. L'instinct d'ingénieur dit : deux chemins, deux aiguilleurs, une logique par type.

La Factory dit non.

Le dispatcher — l'aiguilleur qui exécute les outils — est uniforme. Quand un outil built-in est appelé, et quand un outil remote est appelé, le geste d'invocation côté agent est rigoureusement identique. Même contrôle d'accès, même format de résultat, même surface visible pour le modèle. Seul le transport change en sous-main : l'un s'exécute en mémoire, l'autre part sur le réseau. Mais cette différence est un détail de plomberie, pas une distinction de surface. L'agent ne sait pas, et n'a pas besoin de savoir, si l'outil qu'il vient d'invoquer était sous sa paume ou à l'autre bout du fil.

Cette approche s'inspire de deux référentiels : Claude Code, qui normalise les outils derrière une interface stable quelle que soit leur origine, et smolagents, qui pousse la même idée — un registre d'outils, un seul chemin d'exécution. Le point commun entre ces deux référentiels, c'est qu'ils ont tous les deux reconnu que la frontière interne/externe est réelle pour l'infrastructure — déploiement, latence, observabilité — mais qu'elle ne doit pas remonter jusqu'à la surface cognitive du modèle.

L'alternative rejetée, c'est ce que fait Codex CLI : built-in et externe passent par deux chemins différents, deux aiguilleurs, deux pipelines. Ça paraît logique au premier regard. Mais ça signifie concrètement deux pipelines de sécurité, deux points d'entrée pour des règles d'accès potentiellement divergentes, deux fois plus de surface d'erreur à maintenir. Quand une règle change, il faut la changer aux deux endroits. Le coût s'accumule en silence, et on ne le voit vraiment qu'au moment où les deux pipelines commencent à dériver l'un par rapport à l'autre.

08

Les contrats partagés

Ce qui rend le dispatcher uniforme possible, ce n'est pas de la magie : c'est un plancher commun, coulé sous les pieds de tout le monde avant même que le premier outil ne soit écrit.

Ce plancher est composé de deux dalles, chacune avec son rôle propre.

La première, on l'appelle roxabi-contracts. C'est un module de contrats, un sous-paquetage du workspace qui vit dans packages/roxabi-contracts/. Il porte les formes-types que tous les outils importent : la définition d'une requête TTS, la structure d'une réponse d'image, le schéma d'une erreur unifiée, les sujets NATS qui servent de noms officiels sur le réseau. Chaque outil qui veut parler à la Factory puise dans ce même registre, côté hub comme côté satellite. Un champ renommé dans les contrats devient immédiatement une erreur de typage dans les deux camps — le désaccord silencieux entre un éditeur et un lecteur, ce piège classique, est transformé en erreur visible avant même que le code ne tourne. Les contrats portent aussi les indices de comportement : cet outil est en lecture seule, celui-ci est idempotent, cet autre est destructeur. Le hub les lit pour décider comment se comporter face à une panne.

La seconde dalle, c'est roxabi-nats, l'autre sous-paquetage, dans packages/roxabi-nats/. Là réside la plomberie : l'adaptateur qui branche un outil sur le réseau NATS, le middleware qui propage l'identité de l'agent tout le long du chemin, le client de registre qui tient à jour la liste des workers vivants grâce à des battements de cœur réguliers. C'est le transport pur, sans aucune connaissance des sujets Factory ou des domaines métier. Il ne sait pas ce qu'il transporte. Il sait seulement comment le transporter en sécurité.

Deux briques distinctes pour deux raisons distinctes. Les contrats évoluent quand un domaine change de forme — une nouvelle requête, un nouveau champ. Le transport évolue quand la mécanique réseau change. Les mêler dans un seul paquetage condamnerait les satellites à prendre des mises à jour de plomberie chaque fois qu'un contrat de voix se précise, et vice versa.

Sans ce plancher, chaque satellite réécrit les mêmes vingt lignes. Le drift N×M — le nombre de paires hub-satellite multiplie la surface de désaccord potentiel — est traité à la racine. Une seule source partagée, importée par tous.

09

La circulation des données

Maintenant que le plancher tient, regarde comment la donnée se déplace dans cet atelier. Ce n'est pas un flot unique — c'est plusieurs cours d'eau distincts, chacun taillé pour ce qu'il transporte.

Le territoire sur disque est organisé en trois niveaux d'isolation. Il y a d'abord ce qui est partagé en lecture seule entre plusieurs outils : les poids de modèles HuggingFace, par exemple, qui coûtent cher à télécharger et n'ont aucune raison d'exister en double sur la même machine. Un seul exemplaire, plusieurs consommateurs. Ensuite vient ce qui appartient à un outil précis, posé dans ~/.roxabi/<outil>/ — son espace propre, son bureau, ses données de travail. À l'intérieur de cet espace, un sous-dossier agents/<id>/ isole la mémoire de chaque agent individuellement. Trois niveaux d'emboîtement : le commun, le propre à l'outil, le propre à l'agent. Chacun a sa raison d'être ; chacun a sa frontière.

Pour les artefacts lourds — les fichiers audio générés par la synthèse vocale, les images produites par imageCLI, les pièces jointes entrantes des plateformes de messagerie — il y a le blobstore. C'est un service HTTP dédié, factory-blobstore, qui tourne sur la machine hub dans son propre conteneur Quadlet et écoute sur le port 8449. Pense à lui comme au grenier de l'atelier : on y dépose les choses volumineuses, et on en revient avec une étiquette. Cette étiquette, c'est une référence — une URL, un identifiant de contenu — qui circule dans les messages NATS à la place des octets bruts. Jamais les octets bruts ne traversent le réseau de messages. Le message NATS dit « le fichier audio est là-bas » ; il ne le porte pas sur lui. Ce principe tient pour voiceCLI, pour imageCLI, pour tout outil qui produit ou consomme des artefacts de poids.

La mémoire durable de session, elle, vit ailleurs encore. Elle repose dans un magasin clé-valeur hébergé sur JetStream — le bus de messages persistant qui sous-tend NATS. C'est le coffre : ce qui y est déposé survit à un redémarrage du hub, à une reconnexion d'adaptateur, à une coupure réseau passagère. La conversation ne tombe pas si le processus tombe.

Le harness, lui, ne tient qu'une seule chose : la mémoire du tour en cours. Ce que l'agent est en train de faire, le contexte de cette unique interaction. Une fois le tour terminé, cette mémoire est jetée. Éphémère par conception, pour ne pas accumuler ce qui n'a plus à être tenu.

La règle d'or : NATS pour les petits messages structurés qui doivent traverser vite ; le blobstore pour ce qui pèse et ne mérite pas d'encombrer le réseau de messages ; JetStream KV pour ce qui doit survivre entre les sessions. Trois cours d'eau, trois natures de données, trois destinations.

10

Le pouls

Tu venais de voir les cours d'eau — NATS pour les messages structurés, le blobstore pour les artefacts lourds, JetStream pour ce qui doit survivre. Mais ces cours d'eau ne coulent pas dans le silence. Il y a un rythme sous tout ça, une pulsation régulière qui court le long des tuyaux et que tu peux prendre sous le bout des doigts si tu sais où poser la main.

Ce rythme, ce sont les battements de cœur. Chaque outil distant — le worker de synthèse vocale, le worker d'image, le worker LLM — publie à intervalles réguliers une impulsion sur un sujet dédié : factory.voice.tts.heartbeat, factory.llm.heartbeat, factory.image.heartbeat. Ce n'est pas une déclaration, ce n'est pas un rapport — c'est juste la pulsation, l'onde fine qu'on voit traverser le registre des workers vivants. Le hub l'attrape, la note, met à jour sa carte. Tant que l'onde arrive, le worker est là. Si elle s'arrête, le silence lui-même est une information.

Le flux principal de travail suit un autre chemin. Quand le hub veut faire parler un modèle, il dépose sa requête sur factory.llm.generate.request. Quand il veut synthétiser de la voix, il pousse sur factory.voice.tts.request. La requête part en direction du worker approprié, et la réponse revient par une boîte personnelle — ce que NATS appelle une inbox, une adresse de réponse qu'on glisse à l'intérieur de la requête et que le worker utilise pour renvoyer son résultat directement à l'appelant. Le pattern s'appelle request-reply : une question, une réponse, un chemin aller-retour propre et traçable. Ni diffusion aveugle, ni sondage en boucle.

Pour le harness — la boucle d'exécution qui pilote les tours d'agent — c'est le même principe. Il publie sa demande sur factory.harness.turn.request ; le hub écoute, reçoit l'onde, et prend la main. Un sujet nommé, une adresse connue, une convention tenue.

Il y a encore une chose à voir. Quand un agent veut savoir comment utiliser un outil — quels paramètres, quelle API, ce qu'il accepte en entrée — il n'a pas besoin de consulter une documentation externe. NATS Micro expose une convention de découverte sous le préfixe $SRV.INFO. L'agent envoie une requête ; la carte de l'outil lui revient à la demande. Rien de statique, rien à compiler à l'avance. La topologie est vivante, et on peut la lire en la touchant.

Un protocole pour ce qui circule vite et léger ; un pour ce qui pèse. HTTP n'apparaît qu'à un seul endroit — le blobstore. Tout le reste voyage par sujets NATS.

11

Les serrures et les clés

Maintenant qu'on a vu l'onde, regardons les passages. Pas tout le monde ne peut aller partout dans cet atelier. Il y a des portes, des serrures, des clés — et elles sont posées avec soin, parce que la négligence ici a un coût qui se voit.

La première décision, c'est le grain de l'identité. On aurait pu donner une clé à chaque agent — un compte NATS par agent, une NKey par modèle instancié. Avec une dizaine d'agents en circulation, ça ferait une dizaine de serrures à tailler, distribuer, révoquer. Disproportionné pour un gain marginal. Le choix est différent : une identité par rôle. Le hub a sa clé. Le worker voix-TTS a la sienne. Le worker d'image a la sienne. Des rôles d'infrastructure — stables, comptables, administrables à la main si besoin.

La clé elle-même — la NKey, cette signature cryptographique qui prouve qu'on est bien qui on dit être — est stockée comme un secret Podman, montée en fichier dans le conteneur via un tmpfs. type=mount, dans le jargon Quadlet. Jamais une variable d'environnement, jamais une valeur dans une variable que n'importe quel sous-processus pourrait lire en inspectant son propre environnement. Le fichier est posé à un endroit précis, lu au démarrage, et c'est tout. La clé ne circule pas — elle est rangée dans son coffre, et on s'en sert sur place.

L'identité de l'agent appelant, en revanche, voyage différemment. Ce n'est pas la clé réseau qui la porte, pas un en-tête de protocole que NATS lirait et interpréterait. C'est un champ dans le corps JSON du message, un champ agent_id glissé dans l'enveloppe du contrat. Ce choix a une raison précise : toucher à la couche transport pour y inscrire des informations applicatives crée un couplage difficile à faire évoluer. Le corps JSON, lui, appartient à la couche applicative — on peut le faire évoluer, le versionner, l'étendre selon les règles de versionnement des contrats. L'identité de l'appelant reste là où elle est lisible par ceux qui en ont besoin, sans brouiller la plomberie.

La dernière pièce de cette mécanique, c'est la règle d'entrée. Tout est interdit par défaut — c'est le principe deny-first, visible dans le fichier auth.conf : default_permissions avec deny: [">"] sur publish et subscribe à la fois. Rien ne passe sauf ce qu'on a explicitement ouvert. Le hub peut publier sur les sujets de sortie, écouter les heartbeats, répondre aux demandes entrantes. Le worker TTS peut publier son pouls, écouter les requêtes qui lui sont destinées, envoyer ses réponses. Rien de plus. Cette permission est posée au moment où l'agent est instancié — pas au moment de l'appel, pas à la volée. C'est hérité de la discipline de Claude Code, qui ouvre explicitement les droits à la création et ne les révise plus en cours de route.

Ce qui garantit que cette règle tient, c'est l'endroit où la vérification est implémentée. Il n'y a pas une copie de cette logique dans chaque outil, pas une réimplémentation par satellite. Il y a un seul endroit — la bibliothèque SDK, le roxabi-nats dont on a vu le rôle dans la partie précédente. Un seul code de serrure, un seul endroit à auditer, un seul endroit où mettre à jour la logique si le modèle de permission évolue. Les outils font confiance à la bibliothèque pour poser la bonne serrure. Ils n'ont pas à la fabriquer eux-mêmes.

12

Suivre un message

On a passé les sections précédentes à examiner les serrures et les clés — qui peut entrer, qui ne peut pas, comment les accréditations voyagent depuis un fichier d'environnement jusqu'au moment précis où une porte s'ouvre ou reste close. Mais un atelier ne se comprend vraiment qu'en regardant travailler la matière. Alors suis un message, du premier geste jusqu'à la réponse.

Tu ouvres Telegram. Tu écris quelque chose. Tu appuies sur entrée.

À l'autre bout du fil, l'adaptateur factory-telegram est en écoute. Il capte le message — nom d'utilisateur, identifiant de conversation, texte brut — et le reformate en un événement qu'il publie sur le bus NATS. Ce n'est pas encore une décision : c'est une traduction. L'adaptateur ne sait pas ce que le message signifie, il sait seulement le mettre en forme et le pousser vers le hub.

Le hub reçoit l'événement. Il tient le rôle qu'on lui connaît depuis le début : il assemble le contexte, reconstruit la tranche d'historique utile, décide à qui router. Puis il publie une demande de tour sur le sujet factory.harness.turn.request — un ticket qui dit, en substance : quelqu'un a besoin d'un tour d'agent, voici tout ce qu'il lui faut.

L'instance factory-agent@telegram était là, en attente dans la file de queue. Elle capte la requête. C'est l'empreinte du moule harness — la même structure, instanciée pour ce canal, avec sa propre identité. Elle ne choisit pas de répondre ; elle est le travailleur que la queue désigne.

Le harness compose le prompt. Pour cela, il lit la liste d'outils autorisés pour cet agent, inscrite dans son fichier d'environnement. C'est exactement la serrure qu'on a visitée : la liste dit ce que cet agent a le droit de demander, et pas un outil de plus. Le prompt constitué, le harness appelle le modèle via le worker llmCLI qui fait le relais vers le modèle réel.

Le modèle examine la demande et décide qu'il lui faut générer une image. Il demande l'outil image.generate.

La demande sort du modèle et tombe dans le dispatcher. Le dispatcher est uniforme — c'est la même pièce pour tous les outils. Il consulte le SDK, qui connaît le chemin : cette demande part par NATS Micro vers le worker imageCLI. Le message traverse le bus, arrive au worker, et l'image se génère.

Une fois générée, l'image est déposée dans le blobstore. Et c'est là que quelque chose de discret se passe : ce qui revient dans la réponse, ce n'est pas l'image elle-même — c'est sa référence, un blob_ref, une adresse légère qui dit où trouver la chose sans transporter la chose. Le fil NATS ne porte pas des mégaoctets ; il porte des pointeurs.

Le harness reçoit cette réponse et la rejette dans le modèle. C'est le deuxième tour de la boucle. Le modèle voit maintenant que l'image existe, que la demande a été satisfaite, et il produit la réponse en texte — la phrase que l'utilisateur attend.

Le harness émet des événements vers le hub. Le hub diffuse vers l'adaptateur Telegram. L'adaptateur pousse la réponse à l'utilisateur. Telegram affiche le message.

Quelques secondes se sont écoulées. Chaque pièce a joué sa note, à son moment, dans un seul flux continu. L'adaptateur a traduit, le hub a assemblé, le harness a orchestré, le dispatcher a routé, le worker a produit, le blobstore a reçu, le modèle a bouclé. Aucune pièce ne savait tout faire — chacune savait exactement ce qu'elle avait à faire.

Comprendre chaque rôle séparément, c'est ce qui rend le flux lisible d'un bout à l'autre. C'est la récompense de toutes les sections précédentes.

13

L'horizon

On est maintenant dans la lumière du seuil. Derrière toi, l'atelier avec toutes ses pièces en place, ses circuits qu'on peut suivre du regard. Devant toi, quelques portes encore fermées — pas des murs, des portes. Il est honnête de les nommer.

La première concerne le moteur interne du harness. Tout ce qu'on a décrit — la boucle, les tours, la gestion des outils — suppose un moteur qui orchestre ces étapes. Mais ce moteur n'est pas encore choisi. LangGraph est sur la table, qui apporte des garanties de graphe et de reprise. Une boucle maison légère est aussi envisagée, plus simple, plus directement contrôlable. Et Hermes — le fork de Nous Research qui vit dans cet écosystème — pourrait jouer ce rôle. Un prototype tranchera. C'est la bonne façon de décider : on ne choisit pas un moteur sur le papier, on le fait tourner.

La deuxième porte, c'est la couche des skills — les partitions SKILL.md qu'un agent peut lire pour savoir quand et comment appeler un outil. Cette couche viendra, probablement au format Anthropic. Mais c'est une itération suivante, pas la fondation.

La troisième, c'est la question des sub-agents : un harness qui appelle un harness, une instance qui en délègue une partie à une autre. L'architecture y est prête dans ses grandes lignes, parce qu'un harness sans état persistant peut s'imbriquer sans friction. Mais s'en charger dès la première version ou le remettre à l'itération suivante — cette ligne de crête reste à franchir.

Il y a aussi un terme qui a flotté un jour dans une conversation : lightpool. Personne ne sait exactement ce qu'il voulait dire. Probablement une confusion avec clipool, le pool de processus Claude qu'on connaît bien. Peut-être une idée en gestation qui n'a pas encore trouvé son contour. Ce mystère-là mérite d'être élucidé avant qu'il devienne une fausse certitude dans les échanges.

Et puis il y a le passage concret, opérationnel : aller du pool partagé actuel vers les instances Quadlet à arobase — ces unités nommées, isolées, une par canal. Ce chemin est tracé, mais le valider sur un prototype avant de le déployer partout est la précaution qui s'impose. On ne bascule pas une topologie de production sur une intuition, même bien fondée.

Ces portes n'obscurcissent pas ce qu'on a vu. Elles indiquent simplement où la promenade continue.

Il y a quelques heures, tu es entré dans cet atelier par la grande porte — le hub, ses sujets NATS, ses adaptateurs qui traduisent le monde extérieur. Tu as traversé la forge des outils, remonté les fils du dispatcher, reconnu l'empreinte du moule harness dans chaque instance, compris comment les serrures distribuent les droits sans les concentrer. Et tu as suivi un message, du premier geste sur un clavier jusqu'à la phrase qui revient en réponse. L'atelier est cohérent. Pas parfait, pas fini, mais cohérent — et c'est ce qui compte pour construire dessus.