Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Reprise de session — prompt d’amorçage

À quoi sert ce fichier. Le donner tel quel, en entier, à une nouvelle session d’agent (Claude Code ou autre) pour qu’elle reprenne le pilotage exactement là où la précédente s’est arrêtée, sans redécouvrir ce qui a déjà coûté cher à établir.

Il se périme. Il décrit un état daté. La première instruction qu’il donne est de le vérifier contre le dépôt réel. Le mettre à jour à la fin de chaque campagne — c’est le seul entretien qu’il demande.

Dernière mise à jour : 2026-08-08 (nuit), main @ 60ca8fca, aucune PR ouverte.


Le prompt

Reprends le pilotage du projet Air là où on s’est arrêté.

Commence par vérifier l’état réel avant toute action — ce document est daté et peut avoir vieilli :

cd ~/Code/air
git log --oneline -5 main && git status --short
gh pr list --state open
python3 scripts/generer-registre-adrs.py --check && python3 scripts/verifier-journal.py

Lis ensuite, dans cet ordre : docs/etat-avancement.md (l’état vivant), docs/INDEX.md (la carte), et CLAUDE.md (les règles de code, qui priment sur tes habitudes).

État attendu

main @ 60ca8fca, aucune PR ouverte, arbre propre.

Cinq chantiers viennent d’être clos :

  • La demi-fermeture ne ferme plus le canal (#643) — le dernier en date, et le plus instructif. Le « tunnel inverse concurrent » qui perdait des connexions n’était pas une course : les trois pompes serveur émettaient CHANNEL_EOF et CHANNEL_CLOSE dès qu’une lecture rendait 0, c’est-à-dire sur une simple demi-fermeture. RFC 4254 §5.3 réserve le second à la fin du canal. Le client, lui, appliquait la bonne règle depuis toujours.

  • Isolation de deux suites *_root (#644) — authorize_root et from_restriction_root écrivaient le même chemin fixe et se marchaient dessus en parallèle. Le verrou de fixture devient exigible par le type.

  • Épinglage des clés d’hôte nues — le chantier que la version précédente de ce document recommandait en premier. air-ssh n’épinglait rien : sans magasin de CA d’hôtes — le cas nominal — il acceptait n’importe quel serveur. Deux magasins désormais (administré prioritaire, personnel écrit à la première vue), clé changée ou révoquée = refus sans appel, hôte jamais vu = question sur un terminal ou refus. Clôture consignée dans ADR-118 §A6. Le socle existait déjà en entier (air-keystore::hosts, le schéma, le programme air-keystore known-hosts) : ce n’était qu’un câblage manquant.

  • Campagne « config client » — volet client d’ADR-118 : ForwardAgent par hôte + -a (#633), keepalive client ServerAliveInterval (#636), ProxyJump (#638), plus l’amendement de clôture d’ADR-118 (#639, FR+EN) qui consigne trois refus argumentés, un report et deux corrections de fait.

  • Santé de la CI — trois échecs root-seulement qui rendaient test-coverage rouge sur toutes les PR (#634), et le plancher de couverture de branches remplacé par une non-régression versionnée (#635).

  • Pile air-launchd + gate — lanceur et superviseur de services, temps 1 (#631), et le gate check-controleurs qui rend vérifiable la doctrine d’architecture d’ADR-149 (#640).

Mode de travail

Le superviseur humain (BDFL) travaille sur speedy. La production est déléguée à carbon : briefs dans ~/briefs/*.md, lancés par ~/run-*.sh (claude -p headless). Carbon héberge aussi le runner CI — ce qui a des conséquences (voir les pièges).

La cadence, sans exception sur du code sensible :

brief → carbon → revue de sécurité adverse → CI verte → merge → informer

Règles dures : aucun trailer Co-Authored-By / Claude (règle contraignante du projet) ; auteur = signature GPG + Signed-off-by du BDFL ; main protégée, tout passe par PR.

Agis en autonomie, informe à chaque merge, et ne sollicite le BDFL que pour de vraies décisions — en lui donnant alors tout le contexte, options écartées comprises.

✅ Bornes de ressources — posées (2026-08-09)

Directive BDFL : aucune version ne sort avec des bornes non fixées. Elle est honorée : pids.max = 2048 et memory.high = 512 Mio s’appliquent par défaut ; un service non borné n’existe plus, et retirer une borne se demande explicitement (unlimited).

Chiffres mesurés qui les justifient : air-sshd coûte 4,00 tâches et ~819 Kio par connexion (linéaire, sans fuite ; 1 tâche / 1,5 Mio au repos). Et ce qui n’est pas borné ailleurs : les connexions établiesDEFAULT_MAX_PREAUTH_TOTAL = 256 ne couvre que la fenêtre pré-auth, l’AdmissionGuard rendant sa place au succès de l’authentification. C’est ce qui rend pids.max utile plutôt que redondant.

Reste ouvert, sans blocage : exprimer ces valeurs en fraction de la capacité (systemd : DefaultTasksMax = 15 % de threads-max) pour qu’elles suivent du Pi 4 au serveur.

Ce qui attend un arbitrage

Plus aucune décision n’attend le BDFL. Les trois qui restaient ont été tranchées le 2026-08-09 (n° 0, 0bis, 0ter). Ne subsiste ci-dessous qu’un flake à qualifier.

  1. Le gate root sur le magasin known_hosts PERSONNELTRANCHÉ le 2026-08-09. Aucun gate n’a été relâché : le verbe air-ssh known-hosts add|remove écrit le magasin personnel sous l’identité de son propriétaire, air-keystore gardant le magasin administré derrière son gate. Chaque magasin est désormais derrière l’outil qui a l’identité qu’il faut. Ce qui subsiste, et qui n’est plus bloquant : le gate d’air-keystore ignore toujours --store — s’il devait un jour se prononcer par magasin, ce serait le chemin canonique résolu qui décide, sinon un lien symbolique rouvre l’échappatoire.

0bis. Un additif couche 1 pour créer un répertoire à mode choisiFAIT le 2026-08-09 (AirFileSystem::make_directory_all, 12ᵉ additif couche 1 non scellé). Le downgrade est fermé : ~/.config/air/ naît en 0700 quel que soit l’umask. À noter pour la suite : l’énoncé d’origine était fauxAirFileManager::make_directory existait déjà et posait un mode ; ce qui manquait était la forme récursive.

0ter. air-launchd A-2 — le lanceur ne tue que l’enfant directFAIT le 2026-08-09 (AirCommand::new_session, 13ᵉ additif couche 1). Le service est chef de sa session, donc de son groupe ; l’arrêt vise le groupe. Reste au temps 3 (cgroup) le petit-fils qui crée sa propre session — une convention que la descendance peut rompre n’est pas une frontière que le noyau tient.

  1. Le test keepalive rouge en suite, vert en isolé (2026-08-07) connection::tests::a_peer_that_answers_the_probes_is_never_disconnected échoue en suite complète (108 s) et passe en isolé (3 s, 3/3). Vérifié préexistant : reproduit sur main après git stash de la branche d’épinglage. Il n’échoue pas à tous les coups — un balayage ultérieur l’a vu vert — donc il est sensible à la charge. Même piège que le tunnel inverse : chercher d’abord une échéance trop serrée dans le test, pas un défaut de la machine.

Ensuite : ADR-119 (extensions SFTP — coûte +1 additif couche 1, statvfs), ADR-120 (auth hostbased — amène le service privilégié air-keysign, lourd), ADR-121 (provisioning). Puis la décision de re-sceau des couches 0/1 : docs/notes/inventaire-additifs-couches-avant-re-sceau-fr.md11 additifs couche 1, tous strictement additifs (donc mineur v3.9, pas le majeur d’ADR-123), et 1 additif couche 0 (fchmodat2) resté descellé sans tag depuis le 2026-07-30.


Pièges établis à la dure — ne pas les redécouvrir

Sur les tests

  • Les suites *_root passent en 0,02 s hors root : elles sautent par un chemin précoce en rapportant « ok ». En root : 44 s. Regarder la DURÉE, jamais le seul verdict. Prérequis : root + ssh + comptes _air-sshd et air-t-session (présents sur speedy et carbon).
  • cargo test n’annonce qu’un binaire fautif à la fois. Corriger le premier révèle le suivant : balayer tous les binaires en root plutôt qu’attendre que la CI les livre un par un.
  • Le pire profil de défaut : échec en suite complète, succès en isolé. Deux occurrences (partage d’un chemin fixe entre tests parallèles, contention). C’est ce qui fait conclure au flake et passer à autre chose.
  • Un test peut passer parce que son montage est trop rapide. Une cible qui répond à mesure qu’elle lit fait doubler la demi-fermeture par la réponse : le canal a beau être fermé à tort, les octets sont déjà passés. Toute la famille des tests de redirection avait ce montage — c’est ce qui a caché un CHANNEL_CLOSE de trop pendant des mois. Faire répondre la cible après la fin de flux est ce qui ouvre la fenêtre.
  • Nos propres clients sont tolérants ; ssh(1) ne l’est pas. Un défaut de protocole du serveur peut être invisible face à notre client et fatal face à un vrai client OpenSSH. Quand ce qui est éprouvé est une règle du protocole, le pair du test doit être ssh(1).
  • Sur speedy, les suites qui lancent un vrai sshd échouent EN ROOT : la machine n’a pas de serveur OpenSSH installé, donc pas de /run/sshd, et sshd refuse de démarrer (« Missing privilege separation directory »). Hors root il n’en a pas besoin, d’où un profil « vert en utilisateur, rouge en root » qui n’a rien à voir avec le code. Remède : install -d /run/sshd (tmpfs — à refaire après chaque redémarrage).
  • Le compte air-t-authz conditionne huit suites *_root — sans lui elles sautent en rapportant « ok » en 0,00 s. Carbon l’a ; speedy ne l’avait pas, et le provisionner (cf. l’en-tête de remote_forward_root.rs) a immédiatement révélé un flake latent que la CI franchissait par chance (#644). Provisionnement : useradd --system --shell /usr/sbin/nologin --home-dir /var/lib/air-t-authz --create-home air-t-authz.
  • Le confinement par cgroup n’est PAS éprouvé en CI. Le job tourne dans un cgroup dont l’écriture est refusée (EACCES, même en root — runner conteneurisé) : tous les tests d’air-cgroup et le test de descendance échappée du lanceur y sautent. Ils s’exécutent sur speedy (et sur toute session utilisateur, systemd déléguant user@<uid>.service). Corollaire à ne pas oublier : ce que le temps 3 ajoutera de cgroups devra être éprouvé hors CI, et une PR verte ne dit rien de cette partie-là.
  • cargo test --workspace --lib ne compile PAS les tests d’intégration des autres crates. Après tout changement d’une structure publique, seul cargo check --workspace --all-targets dit la vérité : un ServiceSpec élargi a fait rougir la CI sur un test d’air-launch-temoin que trois balayages locaux n’avaient jamais compilé.
  • Prouver qu’un test mord : retirer le correctif, vérifier que le test rougit, remettre. Cette discipline a produit les meilleures preuves de ces campagnes (blocage à 2 068 992 octets sans recrédit de fenêtre ; échec en 20,01 s sans O_NONBLOCK ; code 134 → 0 sur l’EPIPE).

Sur la couverture

  • La mesure de branches se fait en --lib : src/bin/** y est invisible, et un module de lib exercé seulement par des tests d’intégration compte comme non couvert.
  • Le gate est une non-régression contre xtask/couverture-reference.toml. La référence se relève en CI uniquement — l’écart local ↔ CI atteint ~0,3 point. Ne jamais y poser une mesure locale, et ne jamais la baisser à la main.
  • Un job test-coverage vert en mode par couche ne prouve pas que la mesure a abouti : l’échec llvm-cov n’y est qu’un WARN, et les branches n’y sont pas mesurées.
  • Un cargo fmt rouge masque les jobs suivants : le gate de couverture ne rend alors aucun verdict, et on croit la couverture bonne alors qu’elle n’a pas été mesurée.

Sur les schémas binaires

  • Cap’n Proto n’admet aucun trou d’ordinal : « réserver » un id par commentaire est une promesse que le format ne peut pas tenir.
  • Patron du zéro : le membre absent / inherit va en premier (zéro du discriminant), pour qu’un artefact ancien relise « absent ». Référence : MatchRule.forceCommand, crates/air-sshd-schema/schema/sshd-config.capnp:410.

Sur git et GitHub

  • Merger avec --delete-branch une PR qui sert de base à une autre ferme la seconde, et GitHub refuse ensuite de la rouvrir. Vérifier gh pr view <n> --json baseRefName avant de supprimer une branche.

Sur la délégation à carbon

  • Il rend parfois son rapport en anglais : le rappeler dans chaque brief.
  • Il lui est arrivé de ne pas committer avant la fin du launcher — travail intact mais non poussé, à rapatrier par scp du git diff. Exiger « committe et pousse tôt », et toujours vérifier l’état de son arbre avant de conclure qu’un run a échoué.
  • Le launcher coupe à ~600 s sans sortie : un run long peut finir « en l’air » alors que le travail est fait.

Ce qu’on a appris sur la méthode

Une revue attaque ce que le code promet, pas seulement ce qu’il fait. Le défaut le plus grave de ces campagnes — l’absence totale d’épinglage de clé d’hôte — n’était pas un bug du code écrit : c’est que ProxyJump revendiquait une résistance au bastion compromis que le reste du système ne tenait pas. Trois fois de suite, le motif s’est répété : le mécanisme était bon, c’est la phrase qui promettait trop. Une revendication de sécurité non adossée au code est plus dangereuse que son absence — elle fait cesser la vigilance.

Un gate qui ne nomme personne dérive. Le plancher de branches à 78 était devenu inatteignable dans tous les environnements sans que personne le voie, parce qu’un agrégat sur ~70 crates ne désigne aucun coupable. Il disait « 77,4 < 78 » et laissait chercher.

Un test peut certifier un trou. plain_host_key_defers_to_tofu_path passait au vert depuis des mois : il vérifiait que le code déléguait la décision, sans jamais vérifier que quelqu’un la recevait. Le chemin auquel il déférait n’existait pas. Un test qui constate un renvoi et s’arrête là ne prouve rien — il rassure. Chercher, pour toute délégation, où est le consommateur.

Le socle est parfois déjà là. Le chantier annoncé comme « le plus grave » s’est révélé être un câblage manquant, pas une brique manquante : air-keystore::hosts, le schéma binaire et le programme de gestion existaient en entier. Avant d’estimer un chantier, chercher ce qui est déjà écrit — l’écart entre « rien n’existe » et « rien n’est branché » change tout.

--no-fail-fast est le bon réflexe de balayage. cargo test s’arrête au premier binaire fautif ; corriger celui-là révèle le suivant, un par exécution de dix minutes. cargo test --no-fail-fast les donne tous d’un coup.

Vérifier avant de flipper un statut. Passer ADR-118 à fait a révélé que son volet client n’existait nulle part — ni là où on le croyait, ni là où il avait été « rattaché ».

Dire ce qu’on n’a pas fait. Les campagnes ont produit autant de valeur par leurs refus argumentés (IdentitiesOnly déjà vrai par construction, CanonicalizeHostname et Match client écartés, ProxyCommand reporté) que par leur code. Un ADR appliqué à moitié en silence devient un document dont plus personne ne sait ce qu’il engage.