Journal de bord

Chantiers classés du plus récent au plus ancien. À l'intérieur d'un chantier, entrées les plus récentes en tête.

Ne jamais modifier une entrée existante

Uniquement en ajouter en tête du chantier concerné. Format d'une ligne : Date | Hash | Résumé.

Ce fichier est gelé depuis le 2026-08-06 — les nouvelles entrées vont dans AI-HISTORY.log

Décision de l'entretien de cadrage du 2026-08-06 (option journal-format, PROFIL.md) : convertir les entrées ci-dessous à un format borné à 250 caractères aurait contredit la condition de contenu constant qu'exige l'exception d'A-4 pour une conversion de format — ce ne sont pas des lignes compressibles sans perte. Ce fichier reste donc tel quel, en ajout seul comme avant, mais ne reçoit plus de nouvelle entrée. Tout ce qui se passe à partir du 2026-08-06 est journalisé dans AI-HISTORY.log.

Entretien de cadrage — fusion de la charte 20260804-071239 (2026-08-06)

DateHashRésumé
2026-08-0601bf9a6 Deux branches governance-sync en attente depuis le 2026-08-04 fusionnées, puis entretien de cadrage relancé (mot-clé GOUVERNANCE). Les deux branches partaient du même point (0ccad87) : la seconde (20260804-071239, révision cumulative) est entrée en conflit avec la première sur GOUVERNANCE-IA.md et GABARITS.md — résolu en retenant la version de la seconde, vérifiée par empreinte Git contre le dépôt canonique avant de committer (e0f7591 puis 01bf9a6).
2026-08-0601bf9a6 Vingt-deux options de PROFIL.md repassées en revue, deux nouvelles tranchées. support-test confirmé à 8 après relecture des cinq campagnes menées sur ce projet. Une note d'attribution devenue fausse corrigée (le nettoyage des 32 commits Claude-Session était déjà fait le 2026-08-01, la note disait encore l'inverse). registre-livrés tranché à oui — la roadmap dépasse 1700 lignes, majoritairement des chantiers clos. journal-format tranché hors du menu de la charte, après clarification explicite : pas le tableau plat HTML que l'option décrit, mais un vrai AI-HISTORY.log en texte brut (colonnes horodatage | CHANTIER | Commit/Hash | Explication, 250 caractères max). Les deux décisions sont des choix de principe ; leur migration reste à faire, consignée en roadmap.
2026-08-0601bf9a6 Table de correspondance Règle NA-x, retirée de la charte à la révision 20260803-200821, recréée localement dans AI-CONTEXT.html pour les seules références que porte encore ce journal (Règle 7 Cas B, Règle 1, Règle 3) — décision de l'entretien, la charte ne la portant plus pour personne.

configDir(), garde-fou multi-processus, journal saturé (2026-08-06)

DateHashRésumé
2026-08-062c15b1e Campagne 5 — le garde-fou validé en conditions réelles, un défaut trouvé et corrigé le jour même. Menée en deux temps, tous deux autonomes, à la demande explicite de l'utilisateur.

Temps 1, depuis l'hôte (VMware vmrun + open-vm-tools, sans clavier ni souris réels) : VM Campagne A pilotée en ligne de commande — snapshot restaurée, Tabby 1.0.235 installé et lancé pour de vrai, plugin construit depuis master. Trois mesures réelles obtenues sans accès GUI : configDir() confirmé une seconde fois (après Windows) en respectant --user-data-dir sur Linux ; le verrou single-instance d'Electron confirmé (relancer tabby sur le même profil ne crée ni second processus ni seconde fenêtre) ; et une découverte inédite — verrouiller le trousseau avant le premier lancement de Tabby produit un repli basic_text propre, sans blocage, à distinguer du verrouillage en cours de session que les campagnes de juillet avaient mesuré. Reproduire ce second cas exigeait de répondre à une vraie invite d'authentification système — hors de portée du pilotage distant, d'où le temps 2.

Temps 2, une instance Claude Code lancée par l'utilisateur DANS la VM (prompt préparé par l'instance du temps 1, déposé sur le bureau — .tempfiles/CAMPAGNE-5-PROMPT.md, hors dépôt), avec accès à la vraie session graphique. Rapport intégral : ~/CAMPAGNE-5-RAPPORT.md sur la VM (non rapatrié dans le dépôt), 38 captures d'écran à l'appui, chaque constat étiqueté [MESURÉ] ou [DÉDUIT].

Le blocage réel a été reproduit — trousseau verrouillé en cours de session, après négociation du vrai backend gnome_libsecret — avec une vraie invite d'authentification système GTK, distincte d'une fenêtre Tabby (shot-14-blocked.png). Le discriminant a été mesuré contre de vrais témoins produits par ce vrai blocage : pid vivant à 4 s / 12 s / 59,7 s → autorisé ; même pid vivant à 71,8 s → suspendu, au franchissement exact de STUCK_AGE_MS ; pid tué (SIGKILL) à âge constant (21,5 s / 25,6 s) → suspendu immédiatement, prouvant que c'est la vivacité seule qui tranche. Le témoin survit au SIGKILL, comme prévu. Cycle complet parcouru : blocage → gel → mort du processus → consignation → démarrage suivant non gelé (~6 s) → repli manuel → levée explicite depuis les réglages → vérification réelle réussie → retour à la normale.

D4 par l'interface Tabby, code à l'appui, est structurellement impossible sur Linux : verrou single-instance dur dans app.asar (c.app.requestSingleInstanceLock()||(c.app.quit(),c.app.exit(0))), « New window » conditionné à "darwin"===process.platform, zone de notification désactivée sur Linux, palette de commandes muette sur le sujet. Le seul chemin vers newWindow() passe par l'IPC app:new-window, qui empile une BrowserWindow de plus dans le même processus principal — un second renderer, pas un second processus — donc inatteignable sur Linux tel quel.

Un défaut réel trouvé sur du vrai code multi-processus, corrigé le jour même : deux processus OS réels (obtenus via deux profils --user-data-dir distincts, faute de pouvoir en obtenir deux sur un même profil) partageant un témoin fabriqué pour l'occasion ont montré que le finally de runGuarded() effaçait le témoin sans vérifier qu'il portait encore son propre pid/since — un concurrent qui repose son témoin pendant qu'un autre appel est encore gelé se le faisait effacer par erreur. clearOwnWitness() corrige : le témoin n'est effacé que s'il correspond exactement à ce qui a été écrit (fenêtre TOCTOU résiduelle documentée, assumée). Reachability mesurée : nulle sur Linux (les deux garde-fous ci-dessus s'y opposent déjà), atteignable sur macOS via « New window » (deux renderers, un seul processus principal, un seul configDir, un témoin partagé) — jamais exercée faute de machine, seul point du chantier « Deux sujets de conception » qui reste ouvert. Un cas reproduisant exactement ce défaut a été ajouté à la suite de test isolée de keychainGuard.ts (hors dépôt, comme les autres scripts de test de ce projet).

Deux constats annexes : un config.yaml réduit à un saut de ligne fait planter Tabby au démarrage (bug de Tabby, hors périmètre, non corrigé) ; et le bouton de vérification du panneau (keychainRoundTrip()) est caché dès que le plugin est activé (settingsTab.component.pug, *ngIf="!suspended && !keychain") — a compliqué la mesure elle-même, pas un chantier ouvert.

État laissé sur la VM par le temps 2 : non éteinte, non redémarrée, Tabby resté ouvert — restaurée à la snapshot Snapshot 1 par le temps 1 dès le rapport rapatrié. Aucun commit ni push depuis la VM, conformément à la consigne du prompt.
2026-08-065f60364 Validation, en autonomie, du correctif ci-dessous — sans VM, à la demande explicite de l'utilisateur (« piloté à 100% par l'agent »).

configDir() : instance Tabby jetable réelle, --user-data-dir vers un profil isolé (jonction NTFS de dev, --remote-debugging-port=9333), aucune donnée réelle touchée. better-vault.log est apparu dans le profil jetable et non dans le vrai %APPDATA%\tabby : preuve directe, en Electron réel, que --user-data-dir est respecté. Livré.

Garde-fou et journal saturé : modules réels (keychainGuard.ts, logger.ts) compilés en CommonJS et exécutés hors Electron — aucune dépendance Electron dans l'un ni l'autre, la seule qu'ils partagent (tabbyConfig.ts) retombe sur son calcul figé quand @electron/remote est absent. APPDATA redirigé vers un dossier jetable avant le premier require, pour ne jamais toucher au vrai témoin ni au vrai journal de l'utilisateur. Neuf cas pour le garde-fou, onze pour le journal, tous conformes — détail dans § Deux sujets de conception et § Journal saturé.

Ce que ça ne couvre pas, dit explicitement : le déclencheur réel de D4 (plusieurs fenêtres Tabby contre un vrai trousseau Linux verrouillé) reste hors d'atteinte — aucun outillage de VM sur ce poste (vmrun/VBoxManage absents), et les campagnes précédentes montrent que la VM a toujours été pilotée à la main par un opérateur humain dans VMware Workstation. Le déclencheur réel du journal (rafale authentique de getPassphrase() via ConfigService sur une session à configuration chiffrée) n'est, lui, pas spécifique à Linux — reproductible sur Windows sans VM — mais n'a pas été fait faute de session sous la main. La capture de pile de vaultBridge.service.ts n'a été exercée ni isolément ni en réel, couplée qu'elle est à l'injecteur Angular. Décision de l'utilisateur : documenter tel quel plutôt que de pousser plus loin cette session-ci.

Une instance Tabby jetable de test, ouverte pour valider configDir(), n'a pas pu être fermée par l'agent : Stop-Process a été refusé par le classificateur auto-mode, qui ne peut pas distinguer une fenêtre de test d'une fenêtre réelle de l'utilisateur — laissée à l'utilisateur.
2026-08-065f60364 Les deux sujets de conception restés ouverts après la clôture des correctifs c3, plus le constat du journal saturé, ont un correctif écrit — les trois relevaient de ROADMAP.html comme chantiers ouverts distincts (§ Deux sujets de conception, § Journal saturé).

tabbyConfig.ts : configDir() résout désormais app.getPath('userData') via @electron/remote — un accès à une propriété côté processus principal, pas un pont vers safeStorage, donc hors du périmètre du garde-fou — avec repli sur l'ancien calcul figé si inatteignable, et mis en cache pour le coût de l'IPC sur un chemin appelé à chaque lecture de réglages. Respecte désormais --user-data-dir et tout profil alternatif, là où le calcul figé écrivait toujours dans le profil par défaut.

keychainGuard.ts : guardState() discrimine désormais « un appel est en vol » de « un appel n'est jamais revenu » par pid vivant (process.kill(pid, 0)) et ancienneté du témoin (STUCK_AGE_MS, 60 s). Un témoin récent posé par un processus encore vivant n'est plus lu comme une suspension définitive — ce qui visait la course mesurée sur D4 (une fenêtre refusée à tort pendant que d'autres se déverrouillaient normalement dans la même seconde).

logger.ts : les répétitions immédiates d'une même ligne sont désormais étouffées pour log/warn/crit — la première occurrence part en clair, les suivantes sont comptées puis résumées après 3 s d'accalmie. Vise le constat du 2026-08-03 (jusqu'à 262 lignes de déverrouillage par jour, Tabby rappelant getPassphrase() deux fois par sauvegarde de configuration chiffrée).

vaultBridge.service.ts : en mode observation (debug), resolve() capture la pile d'appel synchrone dès son entrée — donc encore celle de Tabby, avant tout await — pour nommer l'appelant responsable d'une rafale.

Vérifié par npm run build et lint:i18n, tous deux verts. Non exercé en conditions réelles : ni multi-fenêtres, ni sur trousseau Linux verrouillé (garde-fou), ni sur une session à configuration chiffrée (journal) — la VM Ubuntu étant restée hors d'atteinte au moment d'écrire ces correctifs, comme pour la quatrième campagne (§ Quatrième campagne). Écrit sur la branche fix/configdir-guard-log-throttle, pas encore fusionné sur master.

Fusion des quatre lots et validation réelle des correctifs c3 (2026-08-05)

DateHashRésumé
2026-08-05817b673 Les quatre lots sont fusionnés sur master (9c96ec4, 710412e, 9ba5f6f, 817b673, dans cet ordre), après validation en conditions réelles des trois correctifs de suivi qui restaient à exercer — pas sur la VM Ubuntu de la quatrième campagne, restée hors d'atteinte, mais sur l'installation Windows réelle de l'utilisateur (ARA_DESKTOP), le dépôt de développement étant relié au dossier de plugins de Tabby par jonction NTFS.

Méthode : Tabby relancé en --debug --remote-debugging-port, instance vivante de VaultBridgeService retrouvée dans les records de l'injecteur racine Angular (getPlatform()._modules[0]._r3Injector). D'abord la primitive isolée — appeler safeStorage.encryptString avec le sel du lot 2 deux fois de suite dans le vrai processus — pour confirmer que le mécanisme produit des chiffrés distincts. Puis le vrai chemin applicatif : jeton réel purgé, bridge.original stubé pour répondre un mot de passe choisi sans jamais simuler de frappe sur la pop-up native ni la déverrouiller pour de vrai, puis learnFromUser() appelé directement. Résultat conforme au code : mot de passe non vérifiable par passphraseOpensVault() → rien n'est écrit.

Fausse alerte sur le point 2 (sel du chiffrement), et sa cause : les deux jetons comparés par l'utilisateur commençaient tous deux par djEw, lu comme un signe de non-hasard. C'est simplement v10 encodé en base64, l'étiquette de version que Chromium/Electron préfixe à tout blob safeStorage quel que soit son contenu — au-delà de ce préfixe constant, les deux jetons différaient entièrement. Une deuxième mesure a par ailleurs montré que safeStorage sur Windows (DPAPI) n'est pas déterministe nativement, contrairement au repli basic_text mesuré sur Linux à la campagne 4 : le sel corrige un défaut qui, sur ce poste précis, ne se serait pas manifesté sans lui de toute façon.

Les cinq points du protocole condensé sont validés : toast rouge fixe sur jeton corrompu, jeton différent à chaque ré-enregistrement, phrase corrigée dans les réglages, ligne WARN tenant sur une seule ligne, formats de date cohérents entre notification et panneau.

Deux conflits de fusion résolus comme prévu, exactement selon la note laissée dans ce document lors de la préparation : dans vaultBridge.service.ts, la structure du lot 1 (pas de purge sur simple échec de lecture) l'emporte, avec briefError() du lot 3 appliqué par-dessus ; dans store.ts, le mécanisme atomique du lot 2 l'emporte sur l'ancienne version que le lot 3 avait modifiée en parallèle, briefError() appliqué aux deux String(e)/String(cause) résiduels de writeDirectly() que le lot 3 ne connaissait pas. Piège #V28 et #V29 se retrouvent dans AI-CONTEXT.html, prochain numéro libre #V30, sans trou.

Préparation de la quatrième campagne (2026-08-02)

DateHashRésumé
2026-08-02b5738d6 Les quatre lots de correctifs sont écrits, la campagne 4 est préparée mais non menée. Chaque lot vit sur sa branche avec sa documentation ; seul le lot 4 est validé, parce qu'il ne touche pas le runtime. Détail dans #correctifs-c3 et #campagne-4.

Le dispositif d'aveuglement des trois campagnes précédentes est caduc, et c'est mesuré. Retirer .AIRules/, CLAUDE.md et .git du clone ne suffit plus : le bundle de développement porte les commentaires du code en clair, et ceux des correctifs disent littéralement ce qu'ils corrigent et à quelle campagne ils répondent. La source map porte l'intégralité des sources ; trois .d.ts sont révélateurs ; et les commentaires SCSS survivent même à la minification, le CSS étant embarqué comme chaîne JS. Le code raconte ses propres correctifs — conséquence directe d'un projet qui commente abondamment, ce qui reste une qualité partout ailleurs. D'où le passage en boîte noire : l'instance ne reçoit ni sources ni source map, et ne construit rien.

Le protocole se contredisait sur un point, et c'est une instance auditrice qui l'a relevé. Il imposait le paquet .deb, qui exige les droits d'administration, alors qu'il interdit par ailleurs à l'opérateur toute saisie non déclarée. L'instance du poste B a pris l'archive portable — même binaire, SHA-256 relevé, aucun privilège — et a déclaré ses écarts d'elle-même. C'est devenu la méthode imposée aux trois postes. Une contrainte de protocole qui oblige à en violer une autre est un défaut du protocole, pas de celui qui l'exécute.

Piège #V29 consigné, compteur porté à #V30 (A-6) : sans author dans son package.json, Tabby ignore le plugin en silencefindPlugins() déréférence author.name dans un try qui avale l'erreur. Découvert en réduisant le package.json d'un paquet aveuglé, et diagnostiqué par l'instance auditrice, pas par l'outillage. À vérifier avant la publication npm : un paquet sans ce champ s'installerait et ne se chargerait jamais.

Réserve de méthode : deux erreurs de préparation se ressemblent — imposer un format d'installation sans vérifier ce qu'il exige, et réduire un package.json sans vérifier ce que Tabby y lit. Dans les deux cas, un raisonnement sur ce que le paquet devrait contenir a tenu lieu de lecture de ce que le code en fait.

Correctifs de la troisième campagne (2026-08-02)

DateHashRésumé
2026-08-029a67513 Lot 2 — better-vault.json s'écrit dans un temporaire NEUF. Quatre constats, un seul geste sur writeSettings() : unlink puis création en wx, et le chmod postérieur disparaît. Le temporaire à nom fixe n'est plus plantable — wx refuse un chemin déjà occupé, quel qu'il soit — le mode 0600 s'applique à la création, donc le fichier n'existe jamais en 0666 avec le jeton dedans, et le try couvre les deux chemins au lieu du seul rename. Un échec total est journalisé en CRIT puis remonte : l'avaler ferait annoncer par learnFromUser() un enregistrement qui n'a pas eu lieu.

Le défaut a été mesuré avant d'être corrigé, sur un banc comparant l'ancienne séquence à la nouvelle : temporaire occupé par une entrée qui pointe ailleurs, l'ancienne écrit le jeton dans le fichier visé et fait de better-vault.json un alias de celui-ci, la nouvelle laisse la victime intacte. Les deux passent le cas nominal et la réécriture.

Deux réserves, écrites pour ne pas être reperdues. Le fsync n'est pas ajouté — la roadmap range l'atomicité sous coupure hors périmètre, et c'est le commentaire qui affirmait le contraire qui a été corrigé, pas le code : rename protège d'un lecteur concurrent et de lui seul. Et le scénario du lien symbolique n'est pas exercé : Windows n'en crée pas sans privilège, le banc a utilisé un lien matériel, qui reproduit le mécanisme (écriture à travers) sans être la même primitive. Vérification sous symlink : campagne 4.
2026-08-027608323 Lot 1 — le catch de déchiffrement de serveFromToken() ne purge plus le jeton. decrypt() échoue de la même façon quand le trousseau refuse de répondre et quand le jeton est corrompu ; purger sur cette indistinction détruisait un bon jeton dès qu'une invite d'authentification était annulée. Les deux purges qui subsistent reposent sur une preuve positive que le jeton est faux — échéance dépassée, échec PBKDF2. Pire cas désormais : un jeton réellement corrompu coûte un déchiffrement raté par démarrage, et reste révocable par forgetNow() depuis les réglages.

tokenVerified repasse quand même à false : un échec de lecture ne dit rien du contenu du jeton, et forcer une revérification PBKDF2 au prochain succès coûte 50–100 ms une fois.

Mesuré sur Windows, dans les deux régimes (configuration chiffrée et non chiffrée) : aucune régression sur le chemin nominal, le coffre s'ouvre toujours automatiquement. Le correctif lui-même reste non exercé, et ne l'est pas ici : sous DPAPI il n'existe aucune invite d'authentification à annuler, donc aucun moyen de faire échouer decrypt() pour cause de trousseau. Il attend la quatrième campagne — un correctif non exercé n'est pas une amélioration, c'est un pari, et ce projet a déjà produit deux régressions en corrigeant trois défauts.
2026-08-02d72c669 Lot 3 — motifs d'échec lisibles, dates dans la langue de Tabby. Les quatre constats de lisibilité, plus un piège qu'ils ont fait sortir.

briefError() remplace String(e) dans les motifs : une erreur qui traverse @electron/remote porte la pile de l'autre processus DANS son message, soit neuf lignes à l'écran et au journal pour un utilisateur qui venait de refuser une authentification — et une entrée sur neuf lignes dans un fichier dont la rétention raisonne par ligne. La pile est perdue à dessein : elle décrit le processus principal d'Electron, pas le chemin de code du plugin.

Une date voyage désormais en millisecondes (Message.dateParams), troisième catégorie de paramètre à côté de params et sourceParams, et pour la même raison que la deuxième existait : une valeur rendue une seule fois ne peut pas servir deux sorties. Les cinq sites appelaient toLocaleString() sans argument, donc dans la locale du système — ni celle du journal, figé en anglais, ni celle de l'interface, qui suit Tabby. Le journal rend par logDate(), l'écran par la locale de Tabby ; logger.ts emprunte le même format, de sorte qu'une ligne et les dates qu'elle cite s'écrivent pareil.

Après un refus d'authentification, le panneau dit que le redémarrage est le seul remède (#V23) : sans cette ligne, l'utilisateur reclique sur un bouton qui n'atteint plus rien. Et withSafeStorage() vérifie que les méthodes existent, non plus seulement l'objet — safeStorage est un pont IPC, un objet amputé passait le filtre et échouait plus loin, à l'endroit où le plugin manipule un secret.

Piège #V28, sorti de ce lot. La roadmap prescrivait TabbyFormatedDatePipe. Il existe, il est réexporté, et il ferait lever — formatDate exige registerLocaleData(), que Tabby n'appelle jamais (mesuré dans app.asar). Les dates passent par Intl natif. La leçon dépasse le cas : le contrôle habituel de ce projet (#V1) répond sur l'export et ne dit rien des préconditions. Non mesuré et assumé : formatDate lui-même n'a pas pu être exercé hors contexte Angular ; ce qui est mesuré, c'est l'absence d'appel.

Reste à traiter au merge : vaultBridge.service.ts déverse encore String(e) sur la ligne que le lot 1 réécrit de son côté. Elle a été laissée telle quelle pour ne pas fabriquer un conflit entre deux branches qui doivent rester dissociables.
2026-08-02405b67d Lot 4 — lint:i18n vérifie enfin l'enregistrement des tables et les collisions de msgid. Le seul lot validable sans la VM, et il l'est.

Le script tenait sa liste de langues en dur et n'ouvrait jamais src/i18n/index.ts : une table écrite, traduite, complète, mais absente de TABLES passait au vert sans jamais s'afficher. Il lit désormais TABLES, vérifie que chaque fichier y est enregistré, que chaque code enregistré a son fichier et son import, et que ce code existe dans LocaleService.allLanguages — un 'fr' au lieu de 'fr-FR' n'active jamais la locale, donc ne lit jamais la table.

Il surveille aussi les collisions avec les msgid de Tabby (#V24) : valeur identique, signalée pour mémoire ; valeur différente, échec — ce plugin changerait le vocabulaire de Tabby pour l'application entière. Les deux listes sont extraites du bundle compilé de tabby-core, où les .po sont embarqués en JSON : lecture seule, aucune exécution, aucune dépendance ajoutée.

Les quatre contrôles neufs ont été exercés par injection, pas relus — table non enregistrée, code de locale invalide, valeur divergente, bundle absent — et les codes de sortie suivent. L'état mesuré confirme la campagne : 'Cancel' est bien partagée avec Tabby dans les trois langues, valeurs identiques.

Le cas « tabby-core non installé » annonce ses contrôles comme NON VÉRIFIÉS au lieu de se taire : un lint dégradé qui ressemble à un lint complet est pire qu'un lint absent.

Troisième campagne de vérification Linux (2026-08-01)

DateHashRésumé
2026-08-01cb97067 Les correctifs D1 et D2 tiennent, et la prémisse de D1 est prouvée. Mesuré sur Ubuntu / gnome-keyring / Tabby 1.0.235 : sur trousseau verrouillé, le motif rendu est roundTripFailed, qui n'est atteignable que si keychainStatus() a d'abord répondu available: true — l'ancien test se serait donc bien déclaré satisfait. C'est la première fois qu'une campagne de ce projet établit qu'un défaut corrigé existait, au lieu de constater qu'un correctif ne casse rien.

Mais dix défauts sont sortis du hors-périmètre, tous revérifiés un par un contre les sources avant d'être consignés. Deux touchent la raison d'être du plugin : annuler une invite d'authentification détruit le mot de passe enregistré (le catch de déchiffrement ne distingue pas « le trousseau refuse maintenant » de « le jeton est corrompu », et le contrôle sans annulation montre que le jeton survit), et D4 est confirmé jusqu'en usage spontané — trois fenêtres déverrouillent, une quatrième se refuse le trousseau en invoquant une opération démarrée dans la même seconde. Aucun des dix n'est une régression de D1/D2 : ce sont des zones que les campagnes précédentes n'avaient jamais exercées, ce qui fait tomber la clause « revue de conception » au sens où elle était écrite — elle se déclenche pour un autre motif.

Pièges #V23 à #V27 consignés, compteur porté à #V28 (A-6). Le plus contre-intuitif est #V23 : la clé d'OSCrypt est mise en cache pour la vie du processus, si bien que reverrouiller un trousseau en cours de session ne reverrouille rien — tout scénario de test qui procède ainsi ne teste pas ce qu'il croit tester, et le fait conditionne l'interprétation de la campagne entière.

L'aveuglement a été perdu en cours de route, et l'instance l'a signalé elle-même : elle a reçu le document de protocole entier, hypothèses de défaut et grille de dépouillement comprises. Les défauts restent des mesures — un défaut trouvé n'est pas un défaut suggéré — mais les verdicts de conformité valent « tient sous l'angle prévu », un cran en dessous de ce qui était visé. Correction pour la campagne 4 : scinder le protocole en un fichier prompt et un fichier dépouillement. C'est la deuxième fois que l'aveuglement se perd par une omission de forme et non par une décision, d'où une séparation de fichiers plutôt qu'une discipline de lecture.

Deux consignes du protocole ont été réfutées par leur propre exécution. La purge des transcripts par l'instance elle-même était fausse sur trois plans indépendants — le glob ne couvrait pas le scratchpad, shred ne garantit rien sur ext4 + NVMe, et le classifieur de permissions lui a refusé le listage qui aurait permis de vérifier la couverture demandée. Remplacée par un instantané de VM pris avant l'exécution : ne jamais confier à un acteur la certification de son propre effacement. Et les phrases de test étaient trop longues, ce qui est un défaut de méthode et non de confort — retaper une phrase de vingt-cinq caractères des dizaines de fois décourage la répétition des scénarios, donc appauvrit la mesure, sur un plugin dont l'objet est justement d'éviter cette saisie.

Internationalisation de l'interface (2026-08-01)

DateHashRésumé
2026-08-01c189450 Les quatre pièges du chantier consignés en #V19 à #V22, compteur porté à #V23 dans le même geste (A-6). Aucun des quatre n'avait été anticipé par la préparation du chantier, pourtant détaillée en roadmap depuis le 2026-07-29 — et deux d'entre eux ne produisent ni erreur ni avertissement.
2026-08-010f3e33a Interface traduite en quatre langues, journal figé en anglais. Les 85 chaînes sources passent en anglais — contrainte et non style : Tabby cherche ses traductions par la chaîne source, donc une source française afficherait du français à un utilisateur turc. Le mécanisme est celui de Tabby : TranslateService et LocaleService sont réexportés par tabby-core (vérifié dans le bundle compilé, pas seulement dans les typings) et TranslateModule figure dans les exports du module racine, que src/index.ts importait déjà — aucune dépendance ajoutée.

Deux décisions de l'utilisateur ont modifié la demande initiale du 2026-07-29 (« les cinq langues les plus utilisées, journal d'audit compris ») : quatre langues au lieu de cinq, la cinquième n'ayant pu être justifiée — la mesure faite ce jour-là, taux de complétion des 24 .po de Tabby, ne discrimine pas : dix-huit locales sont à 100 % ; et le journal reste en anglais, non traduit, plutôt que de passer à des codes d'événement.

Les deux exigences se contredisaient sur cinq chaînes, affichées et journalisées — le motif d'indisponibilité du trousseau et la phrase du garde-fou. D'où src/messages.ts : un Message transporte sa chaîne source et ses paramètres sans être rendu ; le journal l'écrit en anglais, l'interface le traduit avec la même chaîne comme clé. Conséquence en cascade sur le témoin du garde-fou, écrit sur disque et relu au démarrage suivant : il portait la phrase française de l'opération gelée, il porte désormais un identifiant (#V22).

npm run lint:i18n ajouté : ce mécanisme ne signale jamais une traduction morte, la phrase retombe simplement en anglais. Le validateur couvre les deux sens, la cohérence des paramètres et le piège ICU de l'apostrophe collée à une accolade. Ses deux contrôles ont été éprouvés sur des cas fautifs, pas seulement sur le cas sain.

Validé en conditions réelles sur Windows 11 le même jour, cinq points : panneau traduit, reste de Tabby intact, changement de langue à chaud, locale non couverte rendue en anglais, journal en anglais. Le deuxième point est celui qui compte — le panneau se serait affiché correctement même si #V19 avait été présent. Jamais exercé sur Linux ni macOS. Vit sur feat/i18n-ui, non fusionnée.

Le rapport de campagne Linux devient une annexe (2026-08-01)

Un chemin cité plus bas n'est plus valable

L'entrée du 2026-07-29 qui acte la campagne indépendante situe le rapport dans .tempfiles/RAPPORT-TEST-LINUX.md. Il est depuis le 2026-08-01 dans annexes/RAPPORT-TEST-LINUX.md. La mention d'origine n'est pas corrigée — le journal est en ajout seul (A-4) — et c'est cet encadré qui la rattrape.

DateHashRésumé
2026-08-013fd64c8 Le rapport sortait d'un dossier dont la règle est de le supprimer. .tempfiles/ est déclaré jetable par construction par l'option tempfiles, et ses fichiers exploités « se suppriment directement, sans demander confirmation ». Or ce rapport est exploité : ses conclusions sont dans la roadmap et dans le README public. Une session appliquant la règle l'aurait supprimé — et elle aurait eu raison. Seul le hasard de son versionnement le protégeait. Il porte 516 lignes de mesures qu'aucun autre document ne contient : extraction D-Bus de la clé safeStorage, jeton forgé par une application tierce se déclarant tabby et accepté par le plugin, mot de passe maître reconstitué hors ligne, permissions partiellement réfutées, écriture non atomique.

Constat annexe : il n'était atteignable par aucun lien — deux mentions textuelles en <code>, jamais un <a>. Donc orphelin au sens d'A-8 avant même la question du dossier. Il porte maintenant son lien de retour, et la campagne le cite.

Une modification faite à son contenu, la seule : la phrase de test du coffre jetable, écrite en clair trois fois, devient {{phrase-de-test-jetable}}. Le dépôt est public et l'option visibilité exclut les mots de passe « sous toute forme, y compris dans un exemple ». La phrase était jetable et son coffre n'existe plus : le risque était nul, la règle est catégorique — et c'est au deux-centième commit qu'une règle catégorique sert.

Réécriture de l'historique — retrait des liens de session (2026-08-01)

L'option attribution ayant été tranchée le jour même à « co-auteur seul, sans Claude-Session », les 32 commits qui portaient déjà un lien de session ont été réécrits pour l'en retirer, sur décision explicite de l'utilisateur. Les trailers Co-Authored-By sont conservés.

Tous les hashes antérieurs au 2026-08-01 ont changé

La réécriture touche les 49 commits de la branche, y compris ceux qui ne portaient pas de lien de session : leur contenu est identique, mais leurs parents ont changé. Les hashes cités par les entrées ci-dessous ne résolvent plus. Ils ne sont pas corrigés — le journal est en ajout seul (A-4) — et c'est la table ci-dessous qui les rattrape. Le seul commit inchangé est le commit initial, qui n'a pas de parent.

Vérifié avant publication : arbre racine identique (0618d5b), 49 commits avant comme après, git diff vide entre l'ancien et le nouveau sommet — seuls les messages diffèrent.

Hash citéHash réelCommit
92be35992be359Initial commit — inchangé, sans parent
9572ae0147ab27Implement automatic vault unlock via the OS keychain
5b2b3a9623f768Add settings panel, token expiry and per-machine configuration
e5b192bd8c05ffPolish the settings panel, notify on storage, add CLAUDE.md
719cd0ac8ea224Add public-facing README in English and French
70cfc7210ad983Turn the log into an audit trail with configurable retention
c5a2489f9fddacAdd open and purge buttons for the audit log
4b5224aa8f86b4Notify on automatic unlock, align log wording with the UI
e008d5a2f672b4Anchor the unlock notification to the active tab
cf201ca4358ca8Document the encrypted-config/titlebar mismatch as a pitfall
a5d23d52f38b3cFix the Linux keychain guard: reachable reason, inert control
c9f434d27677beHide the keychain-dependent settings instead of greying them
75de08ae0c4411Record the independent Linux campaign and its three findings
f0548f85eac211Guard every safeStorage call behind an on-disk witness
f0867399b39222Keep the keychain off Tabby's startup path entirely
2dc049c8c22cffMake the 0600 mode an invariant and the write atomic
4c1fe0d804f44bState the guarantee the code actually delivers
a1243ba5bb5617Bring the governance folder up to charter compliance
76e950f0bf059eAdd the two mandatory callouts to the governance index
032874096a66b8Cadrer le projet et le remettre à niveau sur la charte
DateHashRésumé
2026-08-01n/a (réécriture d'historique, hors commit) 32 trailers Claude-Session retirés de l'historique public par git filter-branch --msg-filter, suivi d'un force-push. Ils publiaient une URL de session claude.ai sur un dépôt public. Sauvegarde préalable : tag local backup-pre-rewrite-20260801 et bundle Git hors dépôt. Ce que l'opération coûte : tout clone existant du dépôt diverge et doit être refait ; un lien externe vers un commit de ce dépôt est cassé. Aucun n'était connu au moment de l'opération.

Revue de gouvernance — cadrage complet et charte refondue (2026-08-01)

Revue au sens d'A-15, cas B, déclenchée par une révision de la charte touchant le noyau (A-7) : la refonte en noyau / options / entretien de cadrage, propagée par la veille automatisée sur une branche dédiée et restée non fusionnée depuis le 2026-07-31 — le périmètre avait alors été volontairement restreint au projet frère. Cette entrée acte la revue et les écarts constatés.

DateHashRésumé
2026-08-010328740 Cadrage complet du projet et remise à niveau. PROFIL.md créé — profil de départ complet, vingt-deux options tranchées, aucune ligne « défaut appliqué, non tranché ». Trois réponses changent une pratique au lieu d'enregistrer l'existant : branches = branche (le projet travaillait en direct sur master — cette entrée arrive elle-même par une branche), dépendances = minimales (aucune dépendance d'exécution dans le package.json, ngx-toastr external, déchiffrement réimplémenté maison), et attribution = co-auteur seul, sans Claude-Session — le lien de session publiait une URL claude.ai sur un dépôt public. Le REMISE-A-NIVEAU.md de la branche de propagation a été rapatrié dans le chantier Conformité de la gouvernance puis supprimé, comme A-7 le prescrit. Références « Règle N » migrées vers les identifiants A-x partout sauf ici (A-4 — la table de correspondance en fin de charte est là pour ça). Compteur de numérotation posé en tête d'AI-CONTEXT (prochain libre : #V19). Validateur html-validate installé, npm run lint:airules.
2026-08-010328740 Trois dérives constatées, dont une déjà corrigée ailleurs sans être vue ici. AI-CONTEXT.html affirmait « ce projet n'a pas encore de code » et décrivait un dépôt ne contenant que .AIRules/, LICENSE et README.md — quatre jours après l'écriture du plugin. C'est exactement l'erreur corrigée dans README.html par a1243ba le 2026-07-29 : la revue d'alors a corrigé la page où la dérive était constatée sans vérifier les autres. Ensuite, README.html annonçait Linux « jamais exercé en vrai, faute de machine » alors que deux campagnes y ont été menées. Enfin un chemin de machine complet figurait dans l'index d'un dépôt public, ce que la liste de l'option visibilité range explicitement dehors.
2026-08-010328740 Un statut déclassé à l'audit. Le chantier « Correctifs issus de la campagne Linux » était marqué ✅ sur une validation du 2026-07-29 au matin ; or osKeychain.ts, vaultBridge.service.ts et le panneau de réglages ont tous été touchés le soir même par les correctifs D1 / D2. La mesure ne porte donc plus sur ce que master contient : repassé en 🚧, ce que la troisième campagne devra lever en rejouant les mesures plutôt qu'en les relisant. Les quatre autres statuts « Livré » ont été confrontés à l'état réel du dépôt et tiennent.

Revue de gouvernance — mise en conformité à la charte (2026-07-29)

Revue au sens de la charte, Règle 7 Cas B : audit d'un .AIRules/ déjà au format mais qui a dérivé. Déclenchée depuis le projet frère tabby-better-sidebar, après un rappel de l'utilisateur sur une règle déjà écrite dans la charte (Règle 1) et restée lettre morte — les fichiers de .AIRules/ se commitent et se poussent à chaque mise à jour. Cette entrée acte la revue et les écarts constatés.

DateHashRésumé
2026-07-2976e950f Les deux callouts .important obligatoires manquaient à README.html — détection de dérive, et validation utilisateur avant toute écriture dans AI-HISTORY/ROADMAP. Défaut relevé après que la mention de conformité eut été posée au commit précédent : le marqueur promettait donc plus que la page ne tenait, et aurait étouffé la détection même qu'il existe pour déclencher, une session ultérieure comparant cette date au lieu de relire la charte. Les deux sont rédigés pour ce projet et non recopiés : la dérive y est aggravée par le socle non contractuel (#V2 — Tabby peut invalider un invariant sans le moindre message), et un test réel y est double par nature, configuration chiffrée et non chiffrée, un succès dans un seul régime ne prouvant rien.
2026-07-29a1243ba README.html décrivait encore un projet qui n'existait pas. « Dépôt Git en place, mais pas encore scaffoldé (pas de package.json, pas de src/) », vérification de faisabilité « non franchie », prochaine étape « arbitrage puis, s'il est retenu, scaffold » — tout cela dépassé depuis le 2026-07-28, l'utilisateur ayant arbitré en faveur du monkey-patch et le plugin étant livré et validé, ce que le callout en haut de la même page disait déjà. La section conserve ce qui reste vrai de cette vérification : le socle est un comportement non contractuel de Tabby, à revérifier à chaque mise à jour de l'application.

Corrigés au passage : les cadences d'écriture, restées sur « AI-HISTORY à chaque commit, ROADMAP à chaque changement de statut » alors que la charte exige depuis le 2026-07-28 le feu vert explicite de l'utilisateur pour ces deux pages ; l'absence de mention « Conforme à la charte du … » au pied de page ; et .tempfiles/, non ignoré par Git contrairement à la Règle 1.

La charte voyage désormais avec le projet : GOUVERNANCE-IA.md est copié à l'identique dans ce .AIRules/, la racine du workspace n'étant pas un dépôt Git — cette copie versionnée en est la seule sauvegarde. CLAUDE.md enregistre la décision par dépôt qu'exige la Règle 3 sur les métadonnées d'attribution IA : admises, ce qui enregistre la pratique déjà présente dans l'historique public.

Deuxième campagne — les correctifs validés, et deux défauts qu'ils ont créés (2026-07-29)

Seconde vérification adversariale, sur VM remise au propre, avec un opérateur humain pour les saisies GUI. Les cinq revendications tiennent à la mesure. Mais la campagne a réfuté la justification du garde-fou, et trouvé deux défauts dans le code écrit le matin même — dont un de sévérité haute qui annulait la garantie centrale du plugin. Résultats complets : Deuxième campagne.

DateHashRésumé
2026-07-29(cette série) D1 — la vérification du panneau ne vérifiait rien, et réarmait le gel. « Lever la suspension et vérifier » appelait keychainStatus(), qui sur Linux ne lit que le nom du backend — mesuré comme ne touchant jamais le trousseau. Le bouton annonçait donc « disponible » sur un trousseau verrouillé, effaçait le témoin, et le démarrage suivant regelait. Corrigé par keychainRoundTrip() : aller-retour chiffrement/déchiffrement réel sur une valeur jetable, réservé aux actions explicites de l'utilisateur.
2026-07-29(cette série) D2 — learnFromUser() annonçait un enregistrement sans avoir validé la phrase. Une saisie erronée était chiffrée, écrite et annoncée « enregistrée » pendant que Tabby affichait BAD_DECRYPT pour la même phrase. passphraseOpensVault() existait déjà et servait dans serveFromToken() ; seule l'écriture s'en dispensait (#V18).
2026-07-29(cette série) Réfutation mesurée de la justification du garde-fou. « L'appel ne revient jamais, sans afficher de dialogue » est faux sur ses deux termes : le système affiche une invite d'authentification et l'appel rend la main en 4 à 7 secondes dès qu'on y répond. Corrigé dans keychainGuard.ts, osKeychain.ts, #V17 et les deux README. Le dispositif reste nécessaire — un utilisateur absent bloque indéfiniment — mais c'est cette nuance qui rend praticable la vérification réelle de D1.
Une optimisation correcte à un endroit, ruineuse à un autre

Le raccourci Linux — ne plus appeler isEncryptionAvailable() quand le backend est résolu — était juste sur le chemin de resolve(), et il a été écrit en connaissance de cause, avec la mention explicite qu'il ne corrigeait pas le gel. Personne n'a vu qu'il vidait du même coup la vérification du panneau de réglages, qui passait par la même fonction. Le défaut n'est pas dans le raccourci : il est dans le fait qu'une fonction bon marché servait à la fois de diagnostic interne et de preuve montrée à l'utilisateur.

Règle qui en sort : une vérification qui ne peut pas échouer ne vérifie rien. Quand un test cesse de coûter, se demander ce qu'il mesure encore.

Campagne indépendante et ses trois correctifs (2026-07-29)

Une instance externe, aveugle au dépôt (.AIRules/, CLAUDE.md et l'historique Git retirés du clone), a mené une vérification adversariale sur Ubuntu 24.04. Elle a rapporté trois défauts, dont un de sévérité haute qui contredisait le principe fondateur du plugin. Les trois correctifs ont été écrits dans la foulée. Ils ne sont pas validés : le banc qui les a vérifiés tourne sous Windows, et le défaut principal est un gel qui ne s'y reproduit pas. Détail et périmètre de la seconde campagne : Deuxième campagne.

DateHashRésumé
2026-07-294c1fe0d README : dire la garantie tenue, pas celle qu'on aurait voulue. La ligne « the plugin never blocks access to your vault » n'a délibérément pas été restaurée — la rétablir telle quelle aurait réintroduit le défaut #3 sous un commit de correction. Remplacée par ce que le code délivre vraiment : désactivé, aucun contact avec le trousseau ; activé, un démarrage figé au plus, auto-réparé au suivant. L'exposition au repos y est dite franchement (extraction D-Bus de la clé, mot de passe reconstitué hors ligne), là où le README ne l'admettait qu'en creux. Piège #V17 ajouté.
2026-07-292dc049c 0600 devient un invariant, et l'écriture atomique. writeFileSync(…, { mode }) n'applique le mode qu'à la création : un fichier préexistant en 644 n'était jamais re-serré. Temporaire dans le même dossier + chmod + rename règle les deux — le fichier qui atterrit est toujours neuf. Choix délibéré sur le chemin d'échec : rename peut échouer en EPERM/EBUSY sous Windows, et y renoncer perdrait le jeton que l'utilisateur vient de confier, donc repli sur l'écriture directe — avant la journalisation, pour qu'un journal défaillant ne coûte pas le jeton non plus.
2026-07-29f086739 Le trousseau quitte le chemin de démarrage. install() est appelée depuis le constructeur du NgModule, et avant que enabled ne soit consulté : un keychainStatus() qui n'y servait qu'à journaliser suffisait à figer Tabby, plugin désactivé compris. Le panneau de réglages portait le même défaut à un endroit pire — il sondait à l'initialisation d'un champ, et c'est le seul endroit d'où l'on peut désactiver le plugin : le gel emportait le moyen d'en sortir.
2026-07-29f0548f8 Garde-fou : un témoin sur disque avant tout appel à safeStorage. Aucun délai d'attente n'est possible — @electron/remote fait de l'IPC synchrone, le fil est arrêté dans l'appel. Le seul recours qui survive au gel est une trace écrite avant, relue au démarrage suivant. Enveloppé au point de passage et non aux appelants : seul isEncryptionAvailable() a été mesuré bloquant, que encryptString le soit aussi est un raisonnement — garder le point de passage rend le dispositif correct dans les deux cas.
2026-07-2975de08a Consignation de la campagne et de ses trois constats, rapport intégral versionné dans .tempfiles/RAPPORT-TEST-LINUX.md.
Le correctif n'a pas la forme qu'on lui prêtait

La roadmap prévoyait « rendre keychainStatus() non bloquant ». C'est impossible, et il a fallu le mesurer pour l'admettre : on ne peut pas expirer, depuis le fil que l'on bloque, un appel qui bloque ce fil. Ce qui a été livré à la place ne supprime pas le premier gel — il le rend dernier. Garantie plus faible que celle envisagée, et c'est exactement celle que le README énonce, ni plus.

Corollaire de méthode, le même que la veille : un plan écrit avant la mesure décrit ce qu'on croit possible, pas ce qui l'est. Voir aussi #V17 sur ce qui reste, dans ce correctif, du raisonné plutôt que du mesuré.

Garde-fou Linux — première vérification en conditions réelles (2026-07-29)

Chantier mené sur la branche fix/linux-keychain-guard, fusionnée dans master après validation de l'utilisateur. Objectif initial : préparer une VM Ubuntu pour confier au test une instance externe. La préparation elle-même a suffi à mettre au jour deux défauts — aucune ligne de ce chantier n'était planifiée.

DateHashRésumé
2026-07-29c9f434d Masquer les réglages inopérants plutôt que les griser. Le premier correctif les grisait via [disabled] — ils sont apparus atténués mais restés actionnables (#V16). Un réglage qui paraît inerte sans l'être invite précisément à la fausse confiance que ce garde-fou existe pour empêcher. Remplacé par des *ngIf, qui ne dépendent d'aucun comportement tiers. Restent visibles sans trousseau : la révocation dès qu'un jeton existe, le mode observation — déplacé sous « Journal », sa vraie famille — le journal et le nom de machine. Chacun pour une raison que le code étaye, pas par prudence.
2026-07-29a5d23d5 Ordre des tests dans keychainStatus(), et contrôles neutralisés. isEncryptionAvailable() renvoie déjà false quand le backend est basic_text sur Electron 38.8.6 — le contraire de ce qu'affirmait le commentaire du code. Le test générique sortait donc en premier et le motif expliquant pourquoi basic_text n'est pas fiable n'était jamais lu (#V15). Backend interrogé en premier ; le test de disponibilité reste derrière pour Windows, macOS et les Electron antérieurs.
Ce que la préparation a appris, et qui vaut plus que les correctifs

Deux hypothèses de départ étaient fausses, et une seule mesure a suffi à les renverser chacune. La première : qu'un bureau non reconnu ferait retomber Electron sur basic_text. Une session i3 a montré gnome_libsecret — le service Secret reste joignable quel que soit le bureau. La seconde : qu'isEncryptionAvailable() restait true en mode dégradé, croyance écrite noir sur blanc dans le commentaire du code depuis sa rédaction.

Aucune des deux n'aurait été détectée par relecture : le code était cohérent avec ce qu'on croyait savoir. C'est l'exécution sur une vraie machine qui les a levées, et il aura fallu descendre jusqu'à la console des DevTools de Tabby pour obtenir la valeur qui tranche.

Barre de titre Windows sous config chiffrée (2026-07-29)

Chantier de diagnostic pur : aucune ligne de code du plugin n'a changé. Point de départ, un symptôme signalé la veille — « la barre tout en haut chevauche l'application » — avec deux suspects désignés, ce plugin et tabby-better-sidebar. Aucun des deux n'était en cause.

DateHashRésumé
2026-07-29cf201ca Piège #V14 et issue amont sur le désaccord processus principal / renderer. Les boutons natifs Windows étaient peints par-dessus la tab-bar, et la fenêtre ne pouvait plus être déplacée par le haut. Cause : le processus principal lit config.yaml avant tout déchiffrement, ne trouve donc pas appearance.frame: native et active titleBarOverlay ; le renderer, lui, tourne après déverrouillage du coffre, lit la vraie valeur et ne rend aucun window-controls-spacer. Correctif appliqué côté utilisateur : appearance.frame: thin, seule valeur sur laquelle les deux processus s'accordent quand la config est chiffrée. Signalé en amont : Eugeny/tabby#11539.
Ce que la méthode a coûté, et ce qui l'a fait gagner

Le diagnostic est passé par le CDP (piège hérité #8) plutôt que par une bissection en désactivant les plugins un à un — cinq redémarrages de Tabby au lieu d'un par suspect, et surtout des mesures plutôt que des impressions. Deux d'entre elles ont tranché seules : navigator.windowControlsOverlay.getTitlebarAreaRect(), qui a donné la zone exacte occupée par les boutons natifs (x=1911..2048), et l'énumération des enfants de .tab-bar, qui a montré le .btn-group droit à x=2002..2048 — donc dedans — et l'absence pure et simple du spacer.

L'exonération des plugins n'a pas reposé sur une bissection mais sur la géométrie : le bord droit de la tab-bar est collé à la fenêtre, que la sidebar consomme 304 px à gauche ou non. Un argument qui tient sans avoir à désactiver quoi que ce soit — donc sans détruire la jonction NTFS de dev (piège hérité #1).

Vérification faite dans les deux régimes : config déchiffrée, l'overlay disparaît (visible=false) et app-root perd 22 px au profit du vrai cadre OS ; config rechiffrée avec frame: thin, l'overlay est toujours actif mais le spacer de 138 px est là et le .btn-group s'arrête à x=1910. C'est la seconde mesure qui compte : c'est le régime réel de l'utilisateur.

Écueil rencontré au passage, consigné dans #V14 : sur l'application installée, l'introspection Angular est fermée (el.__ngContext__ est un index numérique, pas le LView, et window.ng n'existe pas). Lire config.store depuis le CDP est impossible — il a fallu déchiffrer temporairement config.yaml, ce que l'utilisateur a fait le temps des tests.

Journal d'audit et notifications (2026-07-28)

Chantier mené sur la branche feat/audit-log, fusionnée dans master après validation de l'utilisateur — conformément à la règle « .AIRules/ reflète main » : un état transitoire vit sur sa branche, doc comprise.

DateHashRésumé
2026-07-28e008d5a Notification de déverrouillage ancrée à l'onglet actif. Demande initiale : un toast « en bas à gauche », précisée ensuite en « du terminal actif, pas de Tabby au global ». ngx-toastr ne sachant positionner que par rapport à la fenêtre, la notification est devenue un élément inséré à la main dans tab-body.content-tab-active (src/inlineToast.ts). Difficulté principale : au démarrage, le coffre est déverrouillé avant qu'aucun onglet n'existe — plutôt que de retomber sur un toast de fenêtre (ce que la demande visait justement à éviter), le plugin attend le premier onglet actif via AppService.activeTabChange$ et renonce après 30 s. Limite assumée : c'est l'onglet, pas le panneau actif d'un split.
2026-07-284b5224a Notification à chaque déverrouillage automatique, et vocabulaire harmonisé. Affichée une seule fois par session : le journal réel montre trois déverrouillages en une seconde au démarrage, une notification par appel en aurait donc empilé trois. Bug d'ergonomie corrigé : l'utilisateur ne trouvait pas l'enregistrement de son mot de passe dans le journal, parce que celui-ci disait « confié au keychain de l'OS » là où l'interface dit « enregistré dans le gestionnaire d'identifiants ». Tous les messages visibles disent désormais « trousseau du système » ; « keychain » ne subsiste que dans les commentaires de code.
2026-07-28c5a2489 Boutons « Ouvrir » et « Purger » le journal dans le panneau. La purge demande confirmation (platform.showMessageBox) : le journal est le seul historique des ouvertures du coffre et l'effacer est irréversible. La purge est elle-même journalisée, un fichier vide étant autrement indiscernable d'un journal n'ayant jamais rien enregistré. Retrait au passage de la ligne « journal : … » répétée à chaque session, redondante avec le chemin affiché dans le panneau.
2026-07-2870cfc72 Le journal devient un historique d'audit. Horodatage absolu par ligne (les délais relatifs au démarrage répondaient à « combien de temps après le lancement », jamais à « quand »), niveaux INFO/WARN/CRIT, et les événements manquants : révocation manuelle (seule action délibérée ne laissant aucune trace), activation/désactivation, changement de politique d'expiration. Rétention configurable (30 / 90 / 365 j / illimitée, défaut 90), appliquée au démarrage, doublée d'un plafond de taille non configurable — la purge par date ne tournant qu'au démarrage, elle ne protégerait pas d'un incident journalisant en boucle en cours de session. Les lignes non horodatées sont conservées : mieux vaut une ligne périmée qu'un événement effacé par un changement de format. Calcul vérifié hors de Tabby sur les cas limites (frontière 89/90 j, mode illimité, troncature d'un fichier de 90 000 lignes).

Publication du dépôt (2026-07-28)

DateHashRésumé
2026-07-28719cd0a Dépôt rendu public : github.com/TooMuhtsh/tabby-better-vault. Branche feat/auto-unlock fusionnée en fast-forward dans master, puis poussée. README.md (anglais, affiché par défaut) et README.fr.md (français) reliés par un lien en tête de chacun : fonctionnalités en cases à cocher, installation depuis les sources, choix techniques et leurs raisons, sécurité et ses limites, feuille de route, projet frère, crédits. Deux partis pris assumés : annoncer dès l'introduction que le plugin patche une partie non documentée de Tabby et peut casser à une mise à jour, et créditer tabby-vault-keepassxc comme antériorité.
2026-07-28719cd0a Historique nettoyé avant le premier push — erreur de ma part. Les trois premiers commits portaient les trailers Co-Authored-By: Claude et Claude-Session, que l'utilisateur refuse sur ses dépôts publics. La consigne était connue mais notée « sur le projet frère », et je l'ai appliquée trop littéralement — la raison invoquée (dépôt public) valait pourtant à l'identique ici. Corrigé avant toute publication par git filter-branch --msg-filter sur 92be359..HEAD : aucun force-push nécessaire, l'historique public est né propre. Vérifié après coup sur origin/master : zéro trailer. Mémoire de collaboration corrigée pour que la règle s'applique désormais à tous les dépôts de l'utilisateur.

Implémentation du déverrouillage automatique (2026-07-28)

Correspondance avec les commits

Les entrées de ce chantier ont été rédigées avant d'être committées — d'où leur mention « pas encore committé », laissée telle quelle (le journal ne se réécrit pas). Les hachages ci-dessous sont ceux après le nettoyage des trailers, donc ceux qui font foi :

HashContenu
9572ae0Scaffold, spike, première implémentation fonctionnelle (osKeychain, tokenStore, vaultCrypto, tabbyConfig, vaultBridge), décision du stockage en fichier séparé.
5b2b3a9Correctif #V11 (store.ts remplace tokenStore.ts/config.ts), expiration du jeton, panneau de réglages, réglages par machine.
e5b192bHabillage du panneau, onglet renommé « Better Vault » et repositionné, notification d'enregistrement, mode observation avec aperçu, CLAUDE.md.
719cd0aREADME publics bilingues.
DateHashRésumé
2026-07-28n/a (pas encore committé) Notification à l'enregistrement du mot de passe, demandée par l'utilisateur : un toast indique où le secret est stocké, jusqu'à quand, et qu'il est révocable dans les réglages. Principe retenu : un plugin qui capte un secret ne doit jamais le faire en silence. ToastrService injecté dans le pont, délai porté à 12 s, échec de notification sans conséquence sur le déverrouillage. ngx-toastr ajouté en devDependency pour ses seuls typages — il reste external et n'entre pas dans le bundle ; il a fallu ajouter @angular/platform-browser@^15 qu'il réclame en peer, sans quoi npm tentait d'installer Angular 22 et cassait la résolution.
2026-07-28n/a (pas encore committé) Intention notée, pas ouverte : panneau de réglages unifié « Tabby Better * ». L'utilisateur souhaite à terme un onglet unique regroupant en sous-onglets tous les plugins tabby-better-* installés. Explicitement repoussé : tabby-better-sidebar est en développement actif et n'a pas encore de réglages, il n'y a donc rien à unifier. Consigné en ROADMAP § Panneau unifié avec ce qui est déjà compatible (logique et présentation séparées), ce qu'il faudra revoir (title/weight, HostBinding content-box) et la difficulté centrale non tranchée : lequel des plugins héberge l'onglet parent, sachant qu'aucun ne peut dépendre de l'autre.
2026-07-28n/a (pas encore committé) Panneau validé par l'utilisateur, puis habillage et repositionnement. Onglet renommé « Better Vault » (au lieu de « Coffre-fort auto ») pour lever toute ambiguïté avec le coffre natif de Tabby, et weight = 2 pour le classer après les onglets natifs. Détail du tri, utile à connaître : a.weight - b.weight + a.title.localeCompare(b.title), et tous les onglets natifs laissent weight à 0 — ils sont donc rangés par ordre alphabétique. Un poids ≥ 2 dépasse à coup sûr le ±1 de localeCompare ; en revanche il place l'onglet après tous les onglets natifs, l'adjacence stricte avec « Vault » étant impossible sans un titre commençant lui aussi par « Vault ». Ajout d'un SCSS (champs assombris, bordures marquées, focus visible, séparateurs de lignes) : styles portés par une classe racine .better-vault-settings et non par :host, inopérant ici (piège hérité #14). Couleurs en rgba pour rester lisibles sur les thèmes clairs comme sombres.
2026-07-28n/a (pas encore committé) Panneau de réglages et expiration du jeton. Onglet « Coffre-fort auto » via SettingsTabProvider (src/settings.ts, src/components/settingsTab.component.{ts,pug}) : activation, nom de machine, politique d'expiration, bouton « Oublier maintenant », mode observation. Loaders Pug ajoutés au webpack (piège hérité #3). Expiration en trois modes, défaut « chaque lundi 3 h » choisi par l'utilisateur ; calcul vérifié sur les cas limites (créneau du jour déjà passé ou non, passage d'année, changement d'heure). Les quatre besoins « par machine » exprimés sont couverts sans mécanique dédiée : better-vault.json étant strictement local, chaque poste a naturellement ses propres réglages. ToggleComponent vérifié comme bien exporté par TabbyCoreModule avant de l'utiliser dans le template.
2026-07-28n/a (pas encore committé) ✅ Cas « config chiffrée » validé en conditions réelles — #V4 définitivement levé. L'utilisateur ayant activé encrypted: true, le journal montre « jeton vérifié » puis « coffre déverrouillé » à +532 ms du démarrage, sans aucune pop-up — contre ~5 s en config non chiffrée, où l'appel n'arrive qu'à la première demande de secret. La preuve empirique du bon timing sur le chemin de démarrage est donc faite, là où il n'y avait jusque-là qu'un raisonnement. Plus aucune inconnue technique sur le projet.
2026-07-28n/a (pas encore committé) Défaut de conception corrigé — les réglages ne pouvaient pas vivre dans config.yaml. Objection soulevée par l'utilisateur après avoir activé le chiffrement de configuration : « ça ne peut pas fonctionner puisque le paramètre lui-même est chiffré ». Exact — vérifié sur sa config, où seules vault, encrypted et configSync restent en clair. Le plugin lisait config.store.betterVault, qui aurait toujours renvoyé le défaut enabled: false dans ce régime : le plugin était donc inopérant en config chiffrée. Correctif : ConfigProvider supprimé, état complet (activation + jeton) déplacé dans %APPDATA%\tabby\better-vault.json (src/store.ts, remplace tokenStore.ts et config.ts). Nouveau piège #V11. À noter que la vérification du mot de passe (#V10) continue de fonctionner, la clé vault: restant nécessairement en clair. L'ancien better-vault-token.bin est supprimé au démarrage ; une ressaisie du mot de passe est nécessaire.
2026-07-28n/a (pas encore committé) ✅ Validé en conditions réelles par l'utilisateur — le plugin remplit son objectif. Cycle complet observé dans better-vault.log : session 19:02 « mot de passe confié au keychain de l'OS » après ~25 s de saisie manuelle (apprentissage), puis session 19:03 « jeton vérifié » et « coffre déverrouillé depuis le keychain de l'OS » — sans aucune pop-up. Jeton de 47 octets écrit dans better-vault-token.bin, chiffré par DPAPI. La chaîne complète fonctionne : patch de getPassphrase, chiffrement OS, vérification PBKDF2 locale contre le coffre de config.yaml, service du mot de passe. Activation faite à la main via betterVault.enabled: true (pas encore de panneau de réglages). Contexte de validation : Windows 11, config non chiffrée — le cas encrypted: true reste non observé.
2026-07-28n/a (pas encore committé) Première implémentation fonctionnelle, compile. Le spike est retiré au profit du vrai service. Fichiers : osKeychain.ts (safeStorage + refus du backend basic_text), tokenStore.ts (fichier séparé better-vault-token.bin), vaultCrypto.ts (vérification locale du mot de passe), tabbyConfig.ts (lecture du coffre dans config.yaml), config.ts (ConfigProvider, opt-in par défaut à false), vaultBridge.service.ts (orchestration). Fonctionnement : jeton présent → vérifié puis servi ; absent → délégation à la pop-up native puis mémorisation ; périmé ou keychain indisponible → purge et repli silencieux. Pas encore de panneau de réglages : l'activation passe par betterVault.enabled: true dans config.yaml.
2026-07-28n/a (pas encore committé) Décision de stockage : fichier séparé, actée après une objection pertinente de l'utilisateur (« quel risque, puisque le jeton n'est utilisable que sur une seule machine ? »). L'analyse lui a donné raison sur la confidentialité — le motif retenu est fonctionnel : la synchronisation de config de Tabby provoquerait une boucle de destruction mutuelle du jeton entre machines. Voir ROADMAP.html § Stockage. Ajout au passage du garde-fou Linux basic_text (§ Linux), seul cas où le jeton exporté serait réellement lisible.
2026-07-28n/a (pas encore committé) Deux corrections de documentation imposées par l'implémentation : #V6 affirmait que le cache natif n'est « jamais » rempli — c'est faux dès qu'on délègue à la méthode native (apprentissage, repli), le piège est donc reformulé en conditionnel ; et l'invariant de purge RAM de la ROADMAP, qui parlait d'« écraser la variable après injection », a été reformulé à portée constante puisqu'il n'y a plus d'étape d'injection et qu'une chaîne JS ne peut pas être écrasée. Nouveau piège #V10 : les paramètres cryptographiques de Tabby sont répliqués dans vaultCrypto.ts et devront être revérifiés à chaque mise à jour.

Lancement du projet — état de l'art et spike (2026-07-28)

DateHashRésumé
2026-07-28n/a (pas encore committé) ✅ Verdict du spike : le projet est techniquement viable. Six sessions Tabby journalisées dans %APPDATA%\tabby\better-vault-spike.log. #V7 entièrement levé — require("@electron/remote") OK, remote.safeStorage présent, isEncryptionAvailable() renvoie true. La délégation à l'OS est donc possible depuis un plugin tiers, sans repli sur keytar. #V4 levé également : isEnabled=false au moment de la pose du patch, alors que le coffre existe dans config.yaml, prouve que le patch précède ConfigService.init() — donc aussi la pop-up du cas « config chiffrée » (cas non observé directement, la machine de test étant en config non chiffrée). Le comportement natif est resté intact : deux appels réels à getPassphrase() depuis decrypt(), le premier avec saisie utilisateur (~7 s), le second servi par le cache natif (1 ms). Prochaine étape : implémenter le vrai service de déverrouillage.
2026-07-28n/a (pas encore committé) Analyse statique du chargeur de plugins de Tabby — #V7 levé sans lancer Tabby. initModuleLookup() (dans app.asar) ajoute <app.asar>/node_modules au NODE_PATH puis force Module._initPaths() ; l'en-tête de l'archive confirme la présence de @electron/remote et de keytar parmi ses 351 modules. Le risque jugé le plus élevé du projet tombe. Deux trouvailles incidentes documentées au passage : #V8 (liste exacte des modules pré-cachés par Tabby, donc des externals webpack sûrs) et #V9 (cause racine du piège hérité #1 : TABBY_PLUGINS.split(':') codé en dur, structurellement incompatible avec les chemins Windows C:\… — à remonter au projet frère, qui n'en documente que le symptôme). Enfin, la séquence loadPlugins()bootstrapModule() combinée au setTimeout de ConfigService.init() rend #V4 probablement favorable, mais cela reste à confirmer par le spike.
2026-07-28n/a (pas encore committé) Scaffold + spike d'observation écrits et compilés. package.json / tsconfig.json / webpack.config.js / .gitignore repris du projet frère (mêmes versions verrouillées Angular 15) ; src/index.ts, src/spike.service.ts, src/spikeLogger.ts. @electron/remote déclaré en external webpack. Build OK, jonction NTFS de dev créée dans %APPDATA%\tabby\plugins\node_modules\tabby-better-vault. Le spike enveloppe getPassphrase sans jamais la substituer et journalise dans %APPDATA%\tabby\better-vault-spike.log — la passphrase n'est jamais écrite dans ce journal, même indirectement. En attente d'exécution manuelle par l'utilisateur pour rendre son verdict sur #V4 et #V7.
2026-07-28n/a Décision utilisateur : projet lancé. Le contournement par monkey-patch de getPassphrase est acté malgré son caractère non officiel. Deux éléments ont motivé l'arbitrage : l'état de l'art (125 plugins tabby-* sur npm ; un seul traite le sujet, tabby-vault-keepassxc publié le 2026-07-23, et il emploie exactement ce monkey-patch) et l'absence d'alternative. Positionnement clarifié : le créneau n'est pas « déverrouiller le Vault » — déjà couvert pour les utilisateurs de KeePassXC — mais le faire sans gestionnaire tiers, via le keychain natif de l'OS.
2026-07-28n/a Correction d'une erreur d'analyse commise le matin même : le patch d'instance avait été écarté au motif qu'il forcerait à conserver la passphrase en mémoire toute la session. Faux — en redéchiffrant à la demande, elle ne vit que le temps de l'appel. Le patch d'instance devient le mécanisme retenu et l'interception de NgbModal est abandonnée (plus fragile). Deux nouveaux pièges documentés : #V6 (le cache natif n'est plus rempli, isOpen() reste faux) et #V7 (rien ne prouve qu'un plugin tiers puisse résoudre @electron/remote — inconnue la plus risquée du projet).

Vérification de faisabilité (2026-07-28)

Exécution du garde-fou bloquant décrit dans README.html, sur tabby-core@1.0.231-nightly.0 (typings npm du projet frère + sources et bundle réels de l'app installée dans C:\Program Files\Tabby\resources\builtin-plugins\tabby-core). Aucun code de plugin écrit — lecture seule.

DateHashRésumé
2026-07-28n/a (pas de commit) Résultat de la vérification : partiellement négatif. VaultService est bien accessible depuis un plugin tiers aux DEUX niveaux (typings et bundle webpack), mais aucune méthode publique n'injecte le mot de passe maître dans le cache de Tabby — la condition exacte posée par le garde-fou n'est donc pas remplie. Le design de la ROADMAP n'est pas réalisable tel quel ; un contournement par interception du modal a été identifié et documenté comme proposition en attente de décision, pas comme design validé. Détail des constats dans AI-CONTEXT.html (pièges #V1 à #V5).

Mise en place du dépôt Git (2026-07-26 → constaté le 2026-07-28)

DateHashRésumé
2026-07-2892be359 Constat de rattrapage documentaire : le dépôt Git existe en réalité depuis le 2026-07-26 (Initial commit: governance docs and project design, remote https://github.com/TooMuhtsh/tabby-better-vault.git), avec .AIRules/ versionné dans le dépôt du plugin lui-même — conforme à la leçon du projet frère. Les mentions « aucun dépôt Git à ce jour » de README.html et ROADMAP.html étaient périmées et ont été corrigées ce jour. L'entrée du 2026-07-26 ci-dessous est laissée telle quelle (journal non réécrit).

Genèse du projet (2026-07-26)

Le déverrouillage automatique du coffre-fort avait été envisagé comme une fonctionnalité de tabby-better-sidebar, puis explicitement sorti de son périmètre (pas une fonctionnalité de sidebar, secret utilisateur le plus sensible de l'écosystème) pour devenir ce projet indépendant. Design discuté et validé pendant les sessions de travail sur le projet frère, transporté ici tel quel.

DateHashRésumé
2026-07-26n/a (pas encore de dépôt Git) Dossier de travail tabby-better-vault et gouvernance IA (.AIRules/) créés, sur le modèle de tabby-better-sidebar. Design complet du déverrouillage automatique porté depuis les décisions actées sur le projet frère (délégation OS via electron.safeStorage, injection opt-in dans VaultService, purge RAM immédiate, garde-fou d'échec, pattern d'interfaçage optionnel entre les deux plugins via l'injecteur Angular partagé). Aucun code écrit — projet au stade design uniquement.