Un harnais d’agents qu’on peut laisser sur place. Il se débrouille seul.
On l’installe sur le Mac mini d’un client, on repart, et on le surveille à distance. Bothy se réveille sur un webhook signé, un cron, un message de chat ou un polling, fait tourner Codex sous supervision, peut garder des notes à lui sur la machine, peut consigner dans Cairn, le registre des tâches de la famille, si vous l’utilisez, et vous prévient quand il a besoin de vous.
Un bothy, c’est le refuge non gardé des montagnes écossaises, jamais fermé à clé, laissé à qui passe. Personne ne s’en occupe, et il tient toujours debout quand on repart. Il y a une lampe près de la porte et un registre sur la table.
Fig. 1Écrit avant d’être acquitté, tué en groupe avant d’être consigné
Le registre : bothy audit
0416 2026-09-24T10:14:02.118734+00:00 wake received ok pr:42 -0417 2026-09-24T10:14:02.391650+00:00 run admitted ok pr:42 run_20260924T101402Z_8d41e7b20418 2026-09-24T10:14:03.027114+00:00 run started ok pr:42 run_20260924T101402Z_8d41e7b20419 2026-09-24T10:15:07.302114+00:00 run question pending pr:42 run_20260924T101402Z_8d41e7b20420 2026-09-24T10:15:41.772301+00:00 run finished ok pr:42 run_20260924T101402Z_8d41e7b2
0416 2026-09-24T10:14:02.118734+00:00 wake received ok pr:42 -0417 2026-09-24T10:14:02.391650+00:00 run admitted ok pr:42 run_20260924T101402Z_8d41e7b20418 2026-09-24T10:14:03.027114+00:00 run started ok pr:42 run_20260924T101402Z_8d41e7b20419 2026-09-24T10:15:07.302114+00:00 run question pending pr:42 run_20260924T101402Z_8d41e7b20420 2026-09-24T10:15:41.772301+00:00 run finished ok pr:42 run_20260924T101402Z_8d41e7b2
04162026-09-24T10:14:02.118734+00:00 wake received ok pr:42 -
04172026-09-24T10:14:02.391650+00:00 run admitted ok pr:42 run_20260924T101402Z_8d41e7b2
04182026-09-24T10:14:03.027114+00:00 run started ok pr:42 run_20260924T101402Z_8d41e7b2
04192026-09-24T10:15:07.302114+00:00 run question pending pr:42 run_20260924T101402Z_8d41e7b2
04202026-09-24T10:15:41.772301+00:00 run finished ok pr:42 run_20260924T101402Z_8d41e7b2
Puis la trace : le journal d’audit toujours ; Cairn si le run porte un --ref ou si son profil accorde bothy_note ; Slack ou Discord seulement quand il faut quelqu’un, comme ici, pour une question.
No. 02laissé sur une machine
Fermé à tout sauf au tailnet, sauf si vous ouvrez une porte.
La porte du tailnet est sur un port que Funnel n’a pas le droit de toucher. Une route publique a son propre listener, car Funnel s’applique au port et non au chemin, et là, la signature fait toute la sécurité. Le chat et le polling partent de la machine, le cron aussi, qui n’a besoin de rien. Une livraison reçue deux fois obtient un 200 et n’est pas remise en file : l’expéditeur n’a rien fait de mal.
Fig. 2Seules les portes ouvertes acceptent des connexions ; chat et polling sortent
tailnetacme-mini:8787webhook via Funnelrejeu → 200slack · discordpolling0 entrante
Dessiné sur le Mac mini pour lequel il a été conçu. Voir l’état des lieux pour ce qui a déjà tourné, et où.
No. 03un seul portail
Cinq runs voient chacun 40 $ sur 50 $. Trois démarrent. Deux sont refusés, avec la raison.
Si l’on mesurait après coup, les cinq démarreraient et la journée pourrait finir à 55 $. Alors un run réserve son pire cas avant de commencer, et les réservations comptent dans le plafond. Le refus nomme le portail et chaque chiffre qui l’a déclenché. Un refus est un événement, enregistré comme les autres, jamais une erreur.
Fig. 3Les réservations comptent dans le plafond, donc le quatrième run est refusé
headroom10.007.004.001.0050.00used 40.00r1r2r3r4 et r5 atteindraient 52.00 : refusés
headroom10.007.004.001.0050.00used 40.00r1–r3r4 et r5 atteindraient 52.00 : refusés
$ bothy run "…" --subject r4refused run_20260924T140311Z_5be0c9d4 0.0000 usd refused at gate 'budget': would reach 52.00 usd against a 50.00 ceiling (40.00 used, 9.00 already reserved, 3.00 requested)
$ bothy run "…" --subject r4refused run_20260924T140311Z_5be0c9d4 0.0000 usd refused at gate 'budget': would reach 52.00 usd against a 50.00 ceiling (40.00 used, 9.00 already reserved, 3.00 requested)
Tant qu’un run tourne, il occupe toute sa réservation, et sa dépense mesurée la remplit de l’intérieur. À la fin, il se solde à ce qu’il a vraiment coûté et rend le reste. La réservation d’un run qui plante expire au bout d’un délai. Chaque job peut aussi porter une limite de tokens et une limite de durée (--max-tokens, --wall-clock), et bothy cancel RUN interrompt un run en cours.
Fig. 4Un run se solde à ce qu’il a coûté et rend le reste
reserved 3.00returned 2.59settled 0.41
reserved 3.00returned 2.59settled 0.41
No. 04une journée sur site
Une journée sur site, telle qu’on la lit dans le registre.
Une journée type chez un client. Les heures sont inventées ; chaque ligne ci-dessous a la forme exacte que Bothy lui donne.
Fig. 5 Chaque événement de la journée laisse une entrée numérotée
00:0006:0012:0018:00
No. 041203:00
Le heartbeat de 3 h ne trouve rien
Un job cron se déclenche à l’heure, emporte la checklist permanente et reçoit une consigne : si rien ne demande d’attention, répondre NO_REPLY. C’est ce qu’il a fait. NO_REPLY est filtré partout, donc personne n’a été réveillé pour lire qu’il ne s’était rien passé. Un job peut aussi porter une procédure écrite, bothy schedule add --sop NAME, qu’on gère avec bothy sop list|show|set, et vérifier son propre travail : une commande argv ou une regex sur la sortie finale, jusqu’à cinq tentatives. Il finit verified ou unverified, et unverified déclenche une alerte.
GitHub envoie une livraison signée pour la PR 42. Elle est sur disque avant que GitHub ait sa réponse, et elle prend la voie pr:42 : une deuxième livraison sur la même pull request attend son tour au lieu de lui faire la course. C’est le réveil de la fig. 1.
En cours de route, Codex s’arrête pour demander. Par défaut, Bothy n’attend pas : un tour suspendu à une personne garderait un créneau et une réservation, la question part donc sur Discord pendant que le run continue, et on dit clairement au tour qu’aucune réponse n’arrive. Un run venu du chat, ou un bothy run au premier plan, peut attendre avec question_hold_seconds, dans une limite totale inférieure à l’expiration de la réservation : une réponse d’un compte autorisé, ou bothy answer, y répond ; à l’échéance, ou à l’arrêt, la question est refusée. Discord est quand même prévenu, mais un run de webhook comme celui-ci n’est pas né dans un fil de chat : il n’a rien à attendre et refuse tout de suite.
! **WARNING** `acme-mini` — run:pr:42
! **WARNING** `acme-mini` — run:pr:42
No. 043316:30
Cairn reçoit la trace
Un run lancé avec --ref prend la tâche dans Cairn le temps du travail, écrit une note marquée de l’identifiant du run (cairn note ACME-12 - --kind note), puis la relâche. Le même identifiant est dans le journal d’audit : tout ce qui concerne un run tient en un grep. Cairn est facultatif : sans lui, le journal d’audit est toute la trace, et un run qui ne peut pas joindre Cairn fait quand même le travail en notant claim_failed. bothy trace ID [--json] [--otlp URL] affiche le run comme un arbre de spans ; --json donne de l’OTLP/JSON et --otlp URL l’exporte une fois.
$ bothy run "Summarise what changed this week" --subject weekly --ref ACME-12
$ bothy run \ "Summarise what changed this week" \ --subject weekly --ref ACME-12
No. 05capacités par tâche
Un worker démarre avec rien. Un profil nommé accorde le reste.
Un worker démarre sans rien au-delà des outils intégrés de Codex. Un profil nommé accorde des serveurs MCP, des racines de skills, un sandbox, et des outils un par un, par leur nom : « mcp: gmail » a l’air inoffensif mais peut très bien inclure l’envoi. Chaque outil peut aussi recevoir un verdict dans la liste approvals du profil : proceed, deny, guide ou confirm. Un deny ou un guide refuse, et le guide porte une raison. Un confirm attend une réponse quand une attente est configurée, ou laisse passer l’appel sur l’approbation d’une personne datant de moins de unless_approved_within secondes, et refuse dans le cas contraire. Un outil de la liste ask sans règle est refusé et déclenche une alerte. Chaque décision est inscrite au journal d’audit. Bothy héberge aussi ses propres outils sur la même connexion, sans sous-processus, port ni identifiant : c’est ainsi que l’agent tient sa checklist permanente.
Fig. 6Seuls les outils nommés sont accordés, et send_message doit encore demander : sans règle pour l’approuver, il est refusé
$ bothy run "read the checklist, add an item about the audit chain" --profile selfcarecompleted run_20260918T064159Z_0de1a197 0.0616 usd$ bothy checklist- Disk on / should stay under 85%.- Verify the audit chain is intact, with no missing or invalid links.
$ bothy run \ "read the checklist, add an item \about the audit chain" \ --profile selfcarecompleted run_20260918T064159Z_0de1a197 0.0616 usd$ bothy checklist- Disk on / should stay under 85%.- Verify the audit chain is intact, with no missing or invalid links.
No. 06la mémoire, sur demande
La mémoire est désactivée tant qu’un profil ne la demande pas. Ensuite, ce sont des fichiers sur la machine.
La mémoire propre de Bothy, ce sont des fichiers simples et un index local sous $BOTHY_HOME, hors de l’espace de travail. Cairn est un registre à part, facultatif ; elle n’en a pas besoin.
Persona et notes
Par profil, dans $BOTHY_HOME/memory : un SOUL.md écrit par l’opérateur (la persona), puis MEMORY.md et USER.md, de courtes notes choisies. Les écritures passent par l’outil bothy_memory, sous plafonds et avec une analyse du texte qui ressemble à un secret, et sont auditées. Un texte refusé n’est jamais écrit.
Notes du jour, cherchées en local
bothy_memory_note ajoute aux notes du jour, qui restent hors des prompts. bothy_memory_search et bothy_memory_get les retrouvent avec SQLite FTS5, ou un simple balayage quand FTS5 manque. Les jours anciens sont supprimés après memory_retention_days.
Une consolidation planifiée
bothy schedule add --dream, chaque nuit par exemple, intègre les nouvelles notes du jour dans MEMORY.md et USER.md. Elle refuse de réduire un fichier de plus de 25 %, et garde d’abord une préimage. bothy memory preimages les liste, et bothy memory restore en remet une en place.
Stockée sur la machine
Des fichiers simples et un index SQLite local : pas de service de mémoire, pas de stockage cloud. Comme tout autre contexte, la persona et les notes parviennent au modèle dans son prompt.
Un brief Cairn, s’il y en a un
Un run lancé avec --ref lit d’abord le résumé Cairn de la tâche, comme une donnée encadrée et non fiable : plafonné à 6 000 caractères, avec un délai de 10 secondes, et il n’arrête jamais le run. cairn_brief: false le désactive. Sans Cairn, il n’y a simplement pas de brief.
Les runs non fiables n’ont pas les outils d’écriture
Les runs venus d’un webhook ou d’un polling n’ont pas les outils d’écriture de la mémoire, sauf si le profil définit allow_from_untrusted.
No. 07six règles
Six règles, chacune payée par la panne de quelqu’un.
01Le budget se réserve, il ne se mesure pas.
Avec plusieurs runs en vol, « sommes-nous sous le plafond ? » ne se règle pas en regardant : cinq runs voient chacun 40 $ sur 50 $ et démarrent tous les cinq. Un run réserve donc son pire cas avant de commencer et se solde à la fin. L’argent est un créneau exclusif comme un autre, et la réservation d’un run qui plante expire.
02Les workers sont tués par groupe de processus.
Un app-server Codex est un arbre. Signaler le parent laisse le reste en vie : treize orphelins sur une machine, et 196 enfants plus 1 193 $ dans un emballement documenté.
03fsync, puis acquitter.
Un broker reconnu a livré l’inverse comme fonctionnalité phare, puis l’a supprimé, code de statut compris, parce qu’un tampon qui n’avait pas atteint le disque ne pouvait pas tenir la durabilité promise.
04Les refus sont des événements.
« Ça n’a pas démarré, et quel portail a dit non » : c’est la question qu’un opérateur pose vraiment. Un refus est retenté, compté, et finalement abandonné à voix haute, jamais retenté à l’infini.
05La vie d’un processus se prouve, elle ne se suppose pas.
Un processus zombie satisfait killpg(pgid, 0). Bothy énumère le groupe et ignore les morts. Celle-ci a été trouvée ici, pas héritée.
06Une signature authentifie l’expéditeur, pas le contenu.
Un webhook GitHub vérifié prouve que GitHub l’a envoyé. Le titre de la pull request qu’il contient a été écrit par un inconnu, et le prompt le dit. Les payloads de webhook et de polling sont encadrés par des délimiteurs à usage unique, et ces runs perdent l’écriture en mémoire partagée sauf si leur profil pose allow_from_untrusted.
La liste complète, avec l’incident derrière chaque règle, est dans docs/DESIGN.md.
No. 08l’essayer
Clonez-le, écrivez six lignes de config, et demandez-lui ce qui ne va pas.
Les systèmes dont Bothy s’inspire sont excellents et très gros : l’un pèse 182 Mo de JavaScript empaqueté sur 109 tables SQLite. Bothy, c’est la partie qu’on lit en un après-midi et qu’on peut défendre devant un client. C’est ce qu’on déploie quand la chose doit tourner là où on ne peut pas aller, sur du matériel qui n’est pas à nous, pour quelqu’un qui appellera quand ça casse.
~/.bothy
export BOTHY_HOME=~/.bothy && mkdir -p $BOTHY_HOMEcat > $BOTHY_HOME/config.json <<'JSON'{ "site": "acme-mini", "workspace": "/srv/work", "pool": { "max_workers": 2 }, "budget": { "mode": "api", "per_run_usd": 0.50, "per_day_usd": 25.0 }}JSONbin/bothy doctorbin/bothy run "Summarise what changed this week" --subject weekly --ref ACME-12bin/bothy status
$ bin/bothy doctor[ok ] state directory writable: /home/you/.bothy/state[ok ] audit chain intact: 14 records verified[ok ] codex present: codex-cli 0.162.1[ok ] cairn reachable: memory and task ledger[ok ] memory FTS5: FTS5 search; missing FTS5 uses a case-insensitive scan[ok ] no orphaned workers: 0 process group(s) left by a previous instance[warn] sandbox can start: bwrap: loopback: Operation not permitted — runs needing a shell will fail[warn] alert route configured: a failure nobody hears about is not handled6 passed, 2 warned, 0 failed$ bin/bothy run "Reply with exactly: BOTHY" --subject democompleted run_20260918T061227Z_a5b90382 0.0199 usd BOTHY
$ bin/bothy doctor[ok ] state directory writable: /home/you/.bothy/state[ok ] audit chain intact: 14 records verified[ok ] codex present: codex-cli 0.162.1[ok ] cairn reachable: memory and task ledger[ok ] memory FTS5: FTS5 search; missing FTS5 uses a case-insensitive scan[ok ] no orphaned workers: 0 process group(s) left by a previous instance[warn] sandbox can start: bwrap: loopback: Operation not permitted — runs needing a shell will fail[warn] alert route configured: a failure nobody hears about is not handled6 passed, 2 warned, 0 failed$ bin/bothy run \ "Reply with exactly: BOTHY" \ --subject democompleted run_20260918T061227Z_a5b90382 0.0199 usd BOTHY
Puis installez-le
Une commande, aucune question, pas de TTY, idempotente, code non nul en cas d’échec.
sudo bin/bothy install --dry-run
exactement ce qu’elle va toucher
sudo bin/bothy install --serve
launchd ou systemd, et tailscale serve
bin/bothy funnel
les commandes tailscale qu’implique votre config
Le code de sortie est un contrat : 75 redémarre-moi, 78 arrête. Les deux gestionnaires de services demandent un traitement opposé, décrit dans le runbook.
No. 09état des lieux
Ce qui est prouvé, et ce qui ne l’est pas.
Chaque test a d’abord été démontré à la main contre le vrai système avant d’être écrit. Les tests sont des preuves consignées, pas une supposition sur ce que fait le code. La liste de ce qui n’a jamais été essayé en vrai mérite donc d’être imprimée en aussi gros.
Prouvé de bout en boutvrai Codex · vrai GitHub · vrai systemd
Des tours supervisés contre un vrai app-server Codex
Des runs en parallèle, et un refus de budget sur un quota réel
La reprise après un crash sous SIGKILL
Un heartbeat qui s’est réveillé seul et a signalé une vraie panne du sandbox
Une piste d’audit chaînée par hash qui relie le tout par identifiant de run
Une vraie installation systemd
La couche mémoire, en réel face à un vrai Codex : persona, écritures et rappel, recherche, une consolidation planifiée avec restauration, et la barrière des runs non fiables
Tourne comme LaunchAgent par utilisateur sur un Mac
Pas encore prouvédit à voix haute
Aucun espace Slack réel n’a jamais été connecté, ni n’a répondu à une question mise en attente
Aucune application Discord réelle n’a jamais été connectée, ni n’a répondu à une question mise en attente, et ses réponses aux éléments d’attention de Drove n’ont tourné que contre un simulacre
Aucune réponse de chat ni aucune approbation n’a achevé un vrai aller-retour avec Codex
Aucun test n’épingle Bothy au schéma généré de Codex : les formes ont été vérifiées une fois, contre Codex 0.162.0
bothy trace et l’export OTLP ne montrent pas encore les enregistrements interrupt/* (questions, réponses, réponses tardives) ; ils ne sont que dans le journal d’audit
Le garde-fou de 25 % de la consolidation n’a pas été déclenché en réel, seulement en test
On a vu un run non fiable avec allow_from_untrusted recevoir les outils d’écriture de la mémoire, mais pas écrire
Le correctif de capture des réponses est prouvé contre un faux app-server, pas contre une séquence d’événements réelle enregistrée
Aucun run Slack ou Discord n’a utilisé la mémoire
Les protocoles, la discipline d’acquittement et les listes d’autorisation sont prouvés contre des simulacres et contre la RFC. Les chemins d’identifiants ne le sont pas
No. 10la famille
Bothy est la pièce qu’on laisse sur site. Les autres sont ce sur quoi il s’appuie.