Annexe de Roadmap → Registre des livrés. Dernière revue : 2026-08-07
Ce registre ne porte que l'état d'un chantier déjà livré — le design retenu au moment de la bascule, repris ici sans être réécrit. Ce qui reste à faire vit dans ROADMAP.html ; l'événement daté de chaque livraison vit dans AI-HISTORY.html, que cette page renvoie sans répéter. Le plus récent chantier livré est en tête — l'inverse de l'ordre du journal.
Exclure des profils SSH du déverrouillage automatique : pour un profil exclu, le plugin ne sert pas la passphrase et laisse la pop-up native de Tabby demander le mot de passe, pour cette connexion-là seulement. La « voie médiane » retenue à l'arbitrage du 2026-08-07, après que la passe de faisabilité a montré que le « déverrouillage paresseux » de la fiche d'origine était structurellement impossible en config chiffrée et déjà natif en config claire. Conçu, codé (trois lots parallèles + fondation), testé et validé le même jour.
config.store.profiles sont excluables ; le nettoyage des orphelines reste synchrone.excludedProfiles dans better-vault.json
(#V11), helpers isProfileExcluded / setProfilesExcluded (lot = raccourci
groupe) / pruneExcludedProfiles. writeSettings() préserve désormais les
clés inconnues : une version antérieure du plugin n'efface plus les champs qu'elle ignore.ProfileExclusionsService enveloppe
loadPassword(profile, username) de PasswordStorageService, dernier point
où l'identité du profil existe (elle est réduite à {user, host, port} juste après).
Profil exclu → fenêtre de délégation runWithNativePrompt() du pont, compteur et non
booléen, cas limites du single-flight documentés dans le code. Toute anomalie vaut « non exclu » :
une entrée illisible n'empêche jamais un déverrouillage.ProfileProvider
de tabby-core, entrée ssh, champ passwordStorage. Jamais de
require('tabby-ssh') : voir #V30, le bug
critique que la première passe de test a attrapé (patch posé sur un singleton jumeau, jamais
déclenché). Plus aucune dépendance de chargement sur tabby-ssh.localStorage,
betterVaultExclusionsCollapsed). Texte des limites sans adoucissement : pas un
cloisonnement des secrets, boot en config chiffrée toujours automatique, clés privées non
couvertes, groupe = instantané. Purge des orphelines à l'ouverture du panneau — seul endroit où
la config fait référentiel, jamais sur le chemin du déverrouillage.BetterVaultExclusions:v1 sous clé de
chaîne (src/exclusionsBridge.ts) : isExcluded / setExcluded
(lot) / excludedIds, implémentation useValue à replis sûrs. Seuls le
jeton et l'interface se dupliquent côté sidebar ; l'entrée de menu contextuel est un chantier
sidebar, non commencé.Passe pilotée par l'injecteur (CDP), données jetables grp-zzz-test-* nettoyées
après coup : chargement, pont, écriture/persistance à travers un redémarrage, préservation d'un
champ inconnu, profil non exclu servi par jeton (56–111 ms, zéro modale), profil exclu → pop-up
native + deux lignes de journal attendues, annulation propagée nativement, fenêtre de délégation
refermée derrière elle. Passe visuelle validée par l'utilisateur (onglets, liste, pliage). La
première passe a levé le défaut critique consigné en #V30, corrigé et revalidé dans la même
session.
Quand plusieurs plugins de la famille « Better * » sont installés, un seul onglet Better Tabby apparaît dans les réglages Tabby, à deux niveaux : un onglet par plugin, les sous-onglets internes d'un plugin (Général/Fonctionnalités/Snippets côté sidebar) au niveau du dessous, une page plate (ce plugin) affichée directement. Seul de sa famille, chaque plugin garde son onglet solo, sans habillage. Conçu, codé et validé le même jour — cinq vagues de tests, 25/25, y compris poids inversés : ce plugin sait héberger la sidebar, le mécanisme est entièrement symétrique.
BetterPanelContribution:<id> dans les providers de son
module (src/betterPanel.ts, liste des tokens connus codée en dur — un futur
« Better X » s'y ajoute dans chaque dépôt).hostWeight minimal (égalité départagée par id
alphabétique) : sidebar = 10, vault = 20, espacés de 10.getComponentType() → null — le filtre
officiel du constructeur de SettingsTabComponent ; le provider reste toujours
enregistré, car SettingsHotkeyProvider parcourt tous les providers sans ce filtre
(un useFactory rendant null l'aurait fait planter). Conséquence
assumée : un hotkey fantôme « Open settings tab: Better Vault » subsiste côté non-hôte.src/components/hostPanel.component.ts, qui monte chaque contribution — la sienne
comprise — par NgComponentOutlet avec un injecteur portant le jeton
BetterPanelEmbedded ; les pages embarquées lâchent leur content-box et
ne montent jamais les autres. L'onglet unifié prend l'id better-tabby.openRequested = true sur sa contribution (objet partagé tel quel via
l'injecteur) — l'hôte, quel qu'en soit le porteur, le lit et l'efface à sa construction.@HostBinding('class.content-box') true sur le panneau de réglages était une
coquille : un champ littéralement nommé true, de valeur undefined — la
classe n'avait jamais été réellement appliquée (vérifié dans le bundle compilé).
Remplacée par contentBox = !embedded ; l'application réelle en solo, changement
d'apparence, a été validée à l'œil (vague 2).
pluginBlacklist par l'élection. En attendant, écarter un plugin de la famille se
fait en retirant sa jonction.L'événement daté et les commits vivent dans AI-HISTORY.log
(branche feat/panneau-unifie, fusionnée avec cette documentation).
Le plan ci-dessous a été arrêté le 2026-08-01, après revérification des dix constats un par un contre les sources — tous tiennent, et les mécanismes sont ceux que le rapport décrit.
Chaque lot vit sur sa branche, avec sa documentation, et arrivera sur master avec elle
(option branches) :
| Lot | Branche | État |
|---|---|---|
| 1 — le jeton détruit | fix/c3-lot1-jeton-detruit |
écrit, non exercé sur Linux |
| 2 — écriture du fichier | fix/c3-lot2-ecriture-fichier |
écrit, non exercé sur Linux |
| 3 — lisibilité | fix/c3-lot3-lisibilite |
écrit, non exercé sur Linux |
| 4 — outillage | fix/c3-lot4-outillage |
écrit et validé — ne touche pas le runtime, ses contrôles ont été déclenchés par injection sur le poste de développement |
Un lot écrit n'est pas un lot corrigé. Les trois premiers attendent la quatrième campagne ; la publication reste bloquée par les quatre.
Deux points restent ouverts et ne sont pas des oublis : le fsync du lot 2, laissé de
côté parce que l'atomicité sous coupure est hors périmètre — c'est le commentaire qui affirmait le
contraire qui a été corrigé, pas le code ; et un String(e) résiduel dans
vaultBridge.service.ts, sur la ligne que le lot 1 réécrit de son côté, à passer en
briefError() quand les deux branches se retrouveront sur master.
La fusion produira un conflit sur AI-HISTORY.html et ROADMAP.html
à partir du deuxième merge : les quatre branches écrivent le même chantier au même endroit. La
résolution est mécanique — un seul titre de chantier, les lignes de tableau concaténées, un seul
callout d'avancement. C'est le prix assumé de branches restées dissociables pour la campagne.
Les trois correctifs de suivi ont été validés en conditions réelles, pas sur la
VM Ubuntu prévue par la quatrième campagne — restée hors d'atteinte — mais
sur l'installation Windows réelle de l'utilisateur, pilotée par l'injecteur Angular (jeton réel
purgé, learnFromUser() appelé directement avec un mot de passe stubé, sans jamais
simuler de frappe sur la pop-up native). Détail de la méthode et des deux fausses pistes écartées en
route dans AI-HISTORY.html.
Les quatre lots sont fusionnés sur master
(9c96ec4 → 710412e → 9ba5f6f → 817b673) : les
deux conflits anticipés dans le callout ci-dessus se sont produits exactement comme prévu et ont été
résolus selon les notes laissées par chaque branche. Ce chantier est clos — les deux
sujets de conception ci-dessous restent ouverts, mais n'en font pas partie.
Annuler une invite d'authentification détruit le mot de passe enregistré.
serveFromToken() ne distingue pas « le trousseau refuse de répondre maintenant » de « le
jeton est corrompu » : les deux tombent dans le même catch et appellent
deleteToken(). Contrôle décisif de la campagne : même trousseau verrouillé, même blocage,
même durée, mais sans annulation → le jeton survit. Un geste anodin — fermer une fenêtre
d'authentification qui gêne — annule donc la raison d'être du plugin, et rien à l'écran ne le dit.
Aucun risque de sûreté, la dégradation va vers la saisie manuelle.
D4 est confirmé jusqu'en usage spontané. Trois observations : au banc avec contrôle apparié, dans le produit provoqué, et dans le produit sans aucune provocation — trois fenêtres déverrouillent dans la seconde, une quatrième se refuse le trousseau en affirmant qu'une opération démarrée dans la même seconde n'est jamais revenue. Course intermittente mais réelle, et la fenêtre refusée reste dégradée pour toute sa session, sans réessai.
7608323). Le
catch de déchiffrement de
serveFromToken() ne purge plus. La purge se justifie là où on a une preuve
positive que le jeton est faux — échéance dépassée, échec PBKDF2 — et ces deux chemins
existent déjà et purgent correctement. Un blob qu'on ne sait pas déchiffrer est un problème de
trousseau, pas de jeton. Pire cas si on ne purge plus : un déchiffrement raté par démarrage sur un
jeton réellement corrompu, révocable depuis les réglages. Strictement meilleur que détruire un bon
jeton.9a67513).
Quatre constats, un seul geste sur
writeSettings(). Le temporaire à nom fixe est plantable en lien symbolique :
writeFileSync écrit à travers le lien et renameSync déplace le
lien lui-même, si bien que better-vault.json devient un lien et que le jeton atterrit
hors du dossier de config — le chmod suit lui aussi vers la victime. Un dossier en
lecture seule fait échapper un EACCES non rattrapé, le try ne couvrant que
rename : la cible préexistante reste en 0666 avec son ancien contenu, sans une ligne de
journal. Le resserrage des droits est postérieur à l'écriture du contenu (82 250 observations du
temporaire en 0666 avec charge utile). Et strace compte zéro
fsync. Correctif : unlink puis création en wx — échoue si le
temporaire existe, lien compris, et garantit un fichier neuf donc le mode 0600 appliqué à la
création — le tout sous try qui journalise. Deux commentaires du code
deviennent faux et tombent avec : « le fichier qui atterrit est TOUJOURS un fichier neuf »
et « quel que soit l'état antérieur, il ressort en 0600 ».d72c669). Le motif
d'échec déversait String(e) tel quel, soit neuf
lignes dont sept de pile Node, à l'écran et au journal, pour un utilisateur qui vient
simplement de refuser une authentification. Les dates suivent la locale du système et non celle de
Tabby (toLocaleString() sans argument, cinq sites) alors que Tabby fournit
TabbyFormatedDatePipe. Après un refus, le bouton de récupération est inopérant pour
toute la session — l'OSCrypt de Chromium mémorise son échec pour la vie du processus,
et learnFromUser ne peut alors plus rien enregistrer : il faut le dire à l'écran,
puisque le redémarrage de Tabby est le seul remède. Et withSafeStorage() ne teste que
if (!safeStorage), jamais l'existence des méthodes.405b67d).
lint:i18n ne lisait jamais TABLES : sa liste
de langues est codée en dur et le script n'ouvre pas src/i18n/index.ts. Une table
écrite mais non enregistrée, ou enregistrée sous un code invalide ('fr' au lieu de
'fr-FR'), passerait au vert. Collision latente à traiter dans le même geste : la clé
'Cancel' est aussi un msgid de Tabby, et la greffe fusionnant, notre valeur
écrase celle de Tabby pour toute l'application. Sans effet aujourd'hui — les valeurs
coïncident dans les trois langues — et non surveillé.configDir() est codé en dur et ignore --user-data-dir.
Mesuré : une instance lancée avec un profil distinct a chargé le plugin et écrit sa ligne de journal
dans ~/.config/tabby, aucun better-vault.log n'apparaissant dans son propre
profil. Trois conséquences, dont la dernière est la plus grave : mauvais
better-vault.json, témoin partagé entre profils — c'est ce qui rend D4 reproductible à
volonté — et validation de la phrase contre le config.yaml d'un autre
profil. Le correctif est court (app.getPath('userData') avec repli sur le chemin
actuel) mais il déplace des fichiers : sur le profil par défaut les deux chemins coïncident, ailleurs
non.
Le garde-fou confond deux faits distincts : « un appel est en vol en ce moment »
et « un appel n'est jamais revenu ». Le pid est écrit dans le témoin
(keychainGuard.ts) mais jamais relu : un témoin dont le pid est vivant
et un témoin dont le pid est mort produisent un objet identique, un message identique, un refus
identique. Le court-circuit if (depth > 0) traite exactement ce problème en
intra-processus, et depth étant une variable de module il n'a aucun équivalent
inter-processus. Le pid seul ne tranche pas — un Tabby gelé a lui aussi un pid
vivant. Ce qui discrimine, c'est pid vivant + ancienneté : une opération gardée saine revient
en millisecondes, une gelée jamais. C'est une refonte du garde-fou, avec sa propre campagne.
configDir() — livré, validé en conditions réelles (2026-08-06)
Résout désormais app.getPath('userData') via @electron/remote — un
simple accès à une propriété côté processus principal, pas un pont vers safeStorage,
donc rien à passer par le garde-fou pour cet appel — avec repli sur l'ancien calcul figé si
@electron/remote est inatteignable, et mis en cache pour le coût de l'IPC sur un chemin
appelé à chaque lecture de réglages.
Validé en Electron réel, sans VM : instance Tabby jetable lancée avec
--user-data-dir pointant vers un profil isolé (jonction NTFS de dev, aucune donnée
réelle touchée), --remote-debugging-port pour confirmer le chargement. Résultat sans
ambiguïté — better-vault.log est apparu dans le profil jetable, pas dans
le vrai %APPDATA%\tabby : la preuve que le calcul figé aurait produite est exactement
celle que ce correctif devait éliminer. Rien ne reste ouvert sur ce point.
Le garde-fou discrimine désormais par pid vivant + ancienneté :
guardState() ne lit plus la seule présence du témoin comme suspension définitive.
Un témoin récent (STUCK_AGE_MS, 60 s — marge au-delà des 45 s mesurés) posé par un
processus dont process.kill(pid, 0) confirme qu'il est encore vivant est désormais lu
comme un appel plausiblement en vol ailleurs, pas comme un appel jamais revenu.
Ce qui n'était que testé en isolation l'a été pour de vrai — VM Linux, trousseau
réellement verrouillé en cours de session (après négociation du vrai backend
gnome_libsecret, condition qui s'est révélée décisive : verrouiller avant le
premier lancement ne produit qu'un repli basic_text propre, sans blocage — deux
scénarios distincts, tous deux désormais mesurés). Résultat : une vraie invite d'authentification
système (GTK, pas une fenêtre Tabby), un vrai blocage, un témoin réel relu par le vrai
guardState() compilé — et le discriminant fait exactement ce qu'il promet : pid vivant à
4 s / 12 s / 59,7 s → autorisé ; même pid vivant à 71,8 s → suspendu, pile au franchissement de
STUCK_AGE_MS ; pid tué à âge constant (21,5 s vs 25,6 s) → suspendu immédiatement. Le
cycle complet — blocage, SIGKILL, témoin qui survit, démarrage suivant non gelé (~6 s),
repli manuel, levée explicite, vérification réelle réussie — a été parcouru de bout en bout. Détail :
AI-HISTORY.html, entrée du 2026-08-06.
D4 par l'interface de Tabby est structurellement impossible sur Linux, code à
l'appui : verrou single-instance dur (requestSingleInstanceLock()), l'entrée « New
window » n'existe que sur macOS, la zone de notification est désactivée sur Linux, la palette de
commandes ne propose rien pour une fenêtre. Le correctif configDir() supprime en plus le
seul autre vecteur connu (témoin partagé entre profils) : deux profils distincts ont désormais deux
témoins distincts. Il ne reste donc plus rien à discriminer dans les scénarios atteignables sur cette
plateforme.
Un défaut réel trouvé, mesuré sur du vrai code multi-processus, et corrigé : le
finally de runGuarded() effaçait le témoin sans vérifier qu'il portait
encore notre pid et notre since — un appel concurrent qui repose son propre
témoin pendant que le nôtre est encore gelé (exactement ce que le discriminant autorise) se le
faisait effacer par erreur, ce qui aurait pu laisser un blocage sans protection au démarrage suivant
si ce concurrent mourait à cet instant précis. Corrigé par clearOwnWitness()
(2c15b1e) : le témoin n'est effacé que s'il correspond encore exactement à ce qui a été
écrit. Reachability mesurée : nulle sur Linux (les deux garde-fous ci-dessus s'y
opposent déjà), mais atteignable sur macOS via « New window » — deux BrowserWindow, un
seul processus principal, un seul configDir, donc un témoin partagé entre deux
renderers.
Le scénario « deux fenêtres, un seul processus principal » — le seul chemin
restant vers le défaut ci-dessus, et le dernier vrai vecteur de D4 — n'a pas pu être exercé, faute
de machine macOS. Le chemin decrypt()/operation: "read" sous trousseau
verrouillé n'a pas non plus été mesuré directement (seuls store et verify
l'ont été) ; la structure de withSafeStorage() et #V23 rendent son
comportement déductible, pas mesuré.
Risque accepté le 2026-08-06 : macOS n'est pas testable sur ce poste, et ne le sera pas. Décision de l'utilisateur — pas une déduction. Ce projet n'a jamais eu de machine macOS, à aucune campagne ; le garder « en attente de mesure » indéfiniment revenait à bloquer la publication sur une inconnue qui ne se lèvera pas. Le correctif s'applique à toutes les plateformes, sans code spécifique à Linux — ce n'est donc pas un trou dans le code, seulement dans sa vérification. Le plugin est testé sur Windows et Linux ; macOS reste non vérifié, à le dire explicitement partout où la portée « toutes plateformes » était sous-entendue (README, § Publication ci-dessous). Si un utilisateur ou un contributeur macOS mesure un jour ce scénario, la trouvaille prend le pas sur cette acceptation — elle est un choix par défaut de preuve, pas une conviction que le défaut n'existe pas.
Sans rapport avec ce plugin, relevés en passant : un config.yaml réduit à un saut de
ligne fait planter Tabby au démarrage avant toute fenêtre (TypeError: Cannot read properties of
undefined (reading 'appearance')) — bug de Tabby, non corrigé, hors périmètre.
Propre au plugin, mais sans code à changer : tant que le plugin est activé et le trousseau
disponible et non consigné, le panneau de réglages n'offre aucun moyen de déclencher
keychainRoundTrip() — settingsTab.component.pug conditionne l'encadré de
vérification à !keychain, renseigné dès que settings.enabled est vrai.
Cohérent avec l'intention (ne pas sonder sans consentement), mais l'effet de bord est que le seul
contrôle qui vérifie vraiment est cachée précisément dans l'état où l'utilisateur s'appuie sur le
plugin — et ça a rendu la mesure elle-même plus difficile à mener. Pas un chantier ouvert à ce stade,
juste à garder en tête.
Journal : AI-HISTORY.html, entrées « Correctifs
de la troisième campagne (2026-08-02) », « Fusion des quatre lots et validation réelle des
correctifs c3 (2026-08-05) », « configDir(), garde-fou multi-processus, journal saturé
(2026-08-06) » ; puis AI-HISTORY.log, entrée du 2026-08-06
(acceptation macOS).
Évolutions post-livraison : macOS accepté comme non testable le 2026-08-06 — décision de principe qui clôt ce chantier plutôt qu'une évolution de son contenu technique.
Pièges : #V26, #V28, #V29 (nouveaux). Résout aussi #V25 et #V27, déjà consignés par la 3ᵉ campagne.
Commits : 7608323, 9a67513, d72c669,
405b67d (les quatre lots) · 9c96ec4, 710412e,
9ba5f6f, 817b673 (fusions) · ce246a6 ·
5f60364, 6f16117, f0aa6e5 · 2c15b1e,
8a9a998, d5902c2 · 6ba8203 (acceptation macOS).
Fait le 2026-08-01, sur la branche feat/i18n-ui — priorisé le
2026-07-29 devant la granularité par profil, motif donné par l'utilisateur : « à l'usage c'est ce
qui est le plus important maintenant, le reste c'est du bonus ». Validé en conditions
réelles sur Windows 11 le jour même, fusionné dans master, puis
vérifié sur Linux par la 3ᵉ campagne.
Testé sur Windows & Linux ; macOS accepté comme non testable le 2026-08-06 (voir
correctifs-c3), même règle ici.
Le défaut le plus grave n'existe pas. Tabby en français puis en italien, plugin installé : ses traductions se chargent normalement, contrôle plugin retiré à l'appui. C'est le point qui justifiait à lui seul le sixième point du protocole — greffer une table sur le mécanisme de Tabby peut dégrader Tabby, et ça ne se voit qu'en regardant l'application entière.
Conformes également : le repli d'une locale non couverte (sous it-IT, panneau
intégralement anglais, zéro résidu français sur sept marqueurs), le journal resté anglais (135 lignes
après passage par cinq langues), et l'absence de clé orpheline (85 sources, trois tables, deux sens,
sur le gabarit compilé). Une prédiction de l'analyse statique est falsifiée : le
panneau déjà rendu se retraduit à chaud, sans recréation du composant — Tabby affiche pourtant un
bandeau « redémarrage nécessaire », plus prudent que nécessaire.
Deux défauts en revanche, traités en lot 3 et lot 4 : les dates
suivent la locale du système et non celle de Tabby, et lint:i18n ne vérifie pas que les
tables sont réellement enregistrées.
Fait d'environnement à retenir : avec une config chiffrée, la pop-up du coffre est toujours en anglais — le réglage de langue est à l'intérieur du blob chiffré. Ça corrobore par la mesure ce qui n'était qu'un raisonnement : toute notification antérieure au déverrouillage ne peut pas être traduite.
La demande du 2026-07-29 portait sur « les cinq langues les plus utilisées, journal d'audit compris ». Les deux termes ont bougé le 2026-08-01, sur décision de l'utilisateur :
fr-FR, en-US,
es-ES, de-DE. La cinquième n'a pas été choisie faute de pouvoir la
justifier. La mesure faite ce jour-là — taux de complétion des 24 .po livrés par
Tabby — ne discrimine pas : dix-huit locales sont à 100 %, et elle n'écarte que
en-GB (3 %), pt-PT (71 %) et da-DK (78 %). Elle ne dit rien
de l'usage réel. Ajouter une langue plus tard ne coûte qu'une table.Tabby cherche ses traductions par la chaîne source anglaise (msgid gettext) et
son CustomMissingTranslationHandler compile la clé elle-même faute de traduction.
Réécrire l'interface en anglais n'était donc pas une option : une source française afficherait du
français à un utilisateur turc. Les 85 chaînes sources du plugin sont désormais anglaises, et les
traductions vivent dans src/i18n/.
Conséquence non anticipée : ce mécanisme ne signale jamais une traduction morte —
la phrase retombe simplement en anglais. D'où npm run lint:i18n. Détail en
#V21.
Aucun n'avait été anticipé par la préparation de ce chantier, et deux d'entre eux sont silencieux par construction. Voir AI-CONTEXT.html :
.po : toute l'interface de Tabby
serait restée en anglais, pas seulement celle du plugin. Le défaut ne se voit qu'en regardant
Tabby ; c'est ce qui rend le point 2 du protocole de test indispensable.TranslateService est typé
any, contrairement au reste de tabby-core, et c'est justement le membre
le plus dangereux qui n'était pas vérifié.Cinq messages servaient les deux usages : le motif d'indisponibilité du trousseau
(KeychainStatus.reason, affiché par le panneau ET journalisé) et la phrase du garde-fou.
src/messages.ts les transporte désormais sous forme de chaîne source + paramètres, sans
les rendre : le journal les écrit en anglais, l'interface les traduit avec la même chaîne comme clé.
Une seule table de vérité, deux rendus.
Ce fichier est le point de contact entre les deux exigences. Toute nouvelle chaîne qui doit à la fois s'afficher et se journaliser passe par là — l'écrire en dur d'un côté ou de l'autre rouvre exactement le défaut que ce chantier a fermé.
feat/i18n-ui est fusionnée dans master depuis le 2026-08-01 (git
branch --merged master le confirme) — le premier point ci-dessous demandait une fusion déjà
faite. Le Linux annoncé « rien n'y a été vérifié » l'a pourtant été, par la
3ᵉ campagne du même jour (voir le paragraphe
au-dessus). macOS, resté réellement non exercé, n'est plus un point d'action : accepté comme non
testable le même jour (voir correctifs-c3).
La ligne vault unlocked from the system keychain apparaît deux fois
par démarrage (mesuré le 2026-08-01). Préexistant, sans rapport avec l'i18n : la
traduction l'a seulement rendu visible. serveFromToken() journalise à chaque résolution,
alors que la notification, elle, est dédupliquée par unlockAnnounced — le commentaire de
ce champ note que le coffre est déverrouillé « trois fois en une seconde en usage réel ».
C'est du bruit, pas une perte d'information. À corriger en même temps qu'un autre passage sur le
pont plutôt que seul : toucher au chemin de resolve() pour du confort de lecture n'est
pas un bon échange tant que la troisième campagne n'a pas eu lieu.
Journal : AI-HISTORY.html, entrée « Internationalisation de l'interface (2026-08-01) ».
Évolutions post-livraison : deux dérives de documentation corrigées le
2026-08-06 en préparant cette migration (statut de feat/i18n-ui annoncé « à fusionner »
alors que fusionnée depuis le 2026-08-01, Linux annoncé « jamais vérifié » alors que vérifié par la
3ᵉ campagne) ; macOS accepté comme non testable le même jour.
Commits : c189450, 0f3e33a.
D1 et D2 sont conformes, mesurés sur Ubuntu / gnome-keyring / Tabby 1.0.235.
Mieux que conformes : la prémisse de D1 est prouvée. 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 que le correctif ne casse rien.
Mais la campagne a trouvé dix défauts qu'elle ne cherchait pas, dont deux qui touchent la raison d'être du plugin. Ils sont traités en chantier propre. Aucun n'est une régression des correctifs D1/D2 : ce sont des zones que les campagnes précédentes n'avaient jamais exercées.
Rapport complet dans .tempfiles/ avec le protocole — comme lui, hors dépôt : il cite
les valeurs saisies et l'environnement de la VM.
L'instance a reçu le document de protocole entier, § 5 et § 6 compris : hypothèses de défaut, identification du point 2 comme correctif de D1, et grille de dépouillement. Elle l'a signalé elle-même dans ses réserves de méthode.
Ça n'entame pas les défauts — une mesure reste une mesure, et un défaut trouvé n'est pas un défaut suggéré. Ça affaiblit les verdicts de conformité : ils valent « tient sous l'angle prévu », un cran en dessous de ce que le protocole visait. Un résultat propre signifie « ces correctifs tiennent », pas « rien d'autre ne cloche ».
Correction structurelle pour la campagne 4 : scinder le protocole en un fichier prompt et un fichier dépouillement, et ne transmettre que le premier. Aucune discipline de lecture ne remplace une séparation des fichiers — c'est la deuxième fois que l'aveuglement se perd par une omission de forme et non par une décision.
Qui prépare le clone n'était écrit nulle part. Le protocole donnait les commandes
sans dire à qui elles s'adressaient — omission du même genre que celle qu'il corrige pour les valeurs
à saisir. Elle mettait l'instance auditrice en position de cloner elle-même, donc de lire
.AIRules/ et les messages de commit avant de les supprimer : l'aveuglement, qui
faisait toute la valeur de la première campagne, aurait été perdu sans qu'aucun rapport puisse le
signaler. La suppression ne rattrape pas la lecture. C'est désormais explicite : l'opérateur, ou une
instance de préparation qui ne participe pas à l'audit, jamais l'auditrice.
La branche à cloner, ensuite. Le protocole faisait cloner le dépôt par défaut
alors que l'i18n vivait sur feat/i18n-ui et touche
keychainGuard.ts et osKeychain.ts — précisément les deux fichiers que cette
campagne doit vérifier. La campagne aurait porté sur le code d'avant, et aurait été bonne à refaire
sans que rien ne le signale. Depuis la fusion du même jour, le clone simple suffit ; la vérification
reste au protocole, assortie d'un contrôle qui la rend factuelle plutôt que déclarative.
Les quatre autres : la langue de l'interface est désormais fixée pour les points 1 à 5 (citer un bouton sans fixer la locale fait chercher un texte absent de l'écran) ; les observables du journal passent en anglais — même contenu, autre langue, mais qui cherche « accès au trousseau suspendu » ne trouve plus rien ; le format du témoin a changé (#V22), ce que le point qui l'inspecte doit savoir ; et un sixième point couvre la traduction elle-même, jamais exercée hors de Windows.
La mise en place de l'environnement revient à l'instance, pas à l'opérateur : installation de Tabby, construction et installation du plugin, fabrication du coffre de test, réglage de la langue. Ces étapes ne dé-aveuglent rien, et les faire soi-même vaut mieux que d'hériter d'un environnement qu'on n'a pas vu se monter. Deux garde-fous en découlent, tirés des campagnes précédentes : la version de Tabby est imposée (1.0.235, celle des mesures antérieures — une autre les rendrait incomparables sans que rien ne le signale), et le chargement effectif du plugin doit être constaté, faute de quoi la campagne produirait des faux négatifs parfaitement cohérents entre eux.
Le recours à plusieurs agents y est explicitement autorisé, avec ses garde-fous : un seul interlocuteur pour l'opérateur humain, diffusion des phrases de test limitée au nécessaire, synthèse non déléguée.
L'effacement des traces ne peut pas être confié à l'instance. Le protocole lui
faisait lancer shred -u ~/.claude/history.jsonl ~/.claude/projects/*/*.jsonl en vérifiant
elle-même la couverture. Elle ne l'a pas fait, et elle a eu raison : la consigne était fausse sur
trois plans indépendants, dont aucun ne se voyait à la lecture. Le glob ne couvrait pas le scratchpad,
qui portait des copies de better-vault.json avec jeton et les scripts des agents
auxiliaires — une énumération écrite d'avance ne peut pas suivre un agent qui choisit où il écrit,
encore moins un agent qui délègue. shred ne garantit rien sur ext4 + NVMe, où journal du
système de fichiers et nivellement d'usure conservent des copies hors d'atteinte des passes de
réécriture : c'est une suppression de bonne foi, et l'écrire dans un protocole fait croire l'inverse.
Enfin le classifieur de permissions de son environnement lui a refusé le listage de
~/.claude : elle ne pouvait pas constater la couverture qu'on lui demandait de
vérifier.
D'où le dispositif retenu : instantané de la VM avant l'exécution, restauration après. Il emporte ce que l'instance ne sait pas avoir écrit, ce qu'elle n'a pas le droit de lister, et ce que ses agents ont laissé. Elle ne détruit plus rien ; elle déclare au fil de l'eau les emplacements qu'elle utilise hors des deux dossiers attendus. Ne jamais confier à un acteur la certification de son propre effacement, a fortiori quand il tourne dans un environnement dont il ne maîtrise pas les permissions. Pour un secret réel et non une phrase jetable, c'est la destruction de la VM qui s'impose, pas la restauration.
Les phrases de test étaient trop longues, et c'est un défaut de méthode et non de
confort. PhraseDeTestJetable-2026! était à retaper des dizaines de fois à la main : une
phrase pénible à saisir décourage la répétition des scénarios, donc appauvrit la mesure — sur un
plugin dont l'objet est précisément d'éviter cette saisie. Remplacées par vault1 et
wrong2, six caractères. Elles restent distinctes du mot de passe du compte,
qui déverrouille le trousseau de session : les confondre effacerait la frontière entre invite du
trousseau et pop-up de coffre, celle-là même que la campagne mesure.
La deuxième campagne a validé les trois correctifs du matin et trouvé deux défauts nés de ces correctifs, dont un de sévérité haute qui annulait la garantie centrale du plugin. Les correctifs de D1 et D2, écrits le soir même, n'ont jamais été exercés sur Linux — et D1 n'est pas exerçable ailleurs : il ne se manifeste que sur un trousseau qui répond à son nom sans répondre à un chiffrement, ce qui n'existe pas sous DPAPI.
La leçon à ne pas perdre : ce projet a produit deux régressions en corrigeant trois défauts. Un correctif non exercé n'est pas une amélioration, c'est un pari.
vault1, avec laquelle la fixture config.yaml est fabriquée ; et la phrase
volontairement fausse du scénario D2, wrong2. Déclarer la première sans la seconde
reviendrait à faire improviser la seconde. Courtes — voir la seconde révision
ci-dessus pour pourquoi c'est méthodologique.learnFromUser de bout en bout, resté non testé lors de la première
campagne. À reconduire tel quel..AIRules/, CLAUDE.md,
.tempfiles/ ni .git. Rappeler le faux positif attendu — des renvois vers
.AIRules/ dans les commentaires, qui résolvent dans le dépôt réel.guardState() conclut à la suspension sur la seule présence du fichier, et
depth ne compte qu'au sein d'un processus. Deux fenêtres Tabby ouvertes en même temps
devraient donc voir la seconde se refuser le trousseau et journaliser une suspension fictive.
Observable attendu si le défaut est réel : une ligne CRIT … accès au trousseau suspendu
sans qu'aucun blocage réel n'ait eu lieu, et un déverrouillage automatique qui échoue dans
la seconde fenêtre alors que le trousseau est déverrouillé. Mesurer avant de redessiner :
c'est une piste tirée du code, pas un constat.
keychainRoundTrip() ne rouvre pas le trou.WARN le mot de passe saisi n'ouvre pas le coffre — non enregistré.
Puis saisir la bonne phrase et vérifier que le chemin nominal fonctionne toujours — le risque d'un
contrôle ajouté est qu'il refuse aussi le cas valide.osKeychain.ts, vaultBridge.service.ts et le panneau de réglages ont tous
été touchés. Rejouer les mesures de la deuxième campagne, pas les relire.basic_text, non rejoué la deuxième fois. La
branche Linux de keychainStatus() a été modifiée depuis sa dernière vérification : le
garde-fou doit toujours refuser, avec son motif explicite.keychainStatus() emprunte le chemin
isEncryptionAvailable() seul, donc là où il engage davantage que sur Linux.rename(2). La mesurer
supposerait de couper l'alimentation de la VM au bon moment ; le rapport coût/information est
mauvais tant que rien ne suggère un problème. La campagne a ajouté un fait à ce sujet :
strace compte zéro fsync. L'atomicité mesurée est celle
du rename face à un lecteur concurrent, pas face à une coupure — ce ne sont pas les
mêmes garanties, et le code n'en offre qu'une.Journal : AI-HISTORY.html, entrée « Troisième campagne de vérification Linux (2026-08-01) ».
Évolutions post-livraison : aucune à ce jour — les dix défauts trouvés hors périmètre sont devenus leur propre chantier, Correctifs issus de la 3ᵉ campagne, lui-même livré et migré ici le 2026-08-06 ; pas une évolution de cette entrée.
Pièges : #V23 à
#V27 (compteur porté à #V28).
Commits : cb97067.
Menée sur la VM remise au propre, avec un opérateur humain pour toute saisie
GUI — c'est ce qui a permis d'exercer enfin learnFromUser de bout en bout, resté non
testé la première fois faute d'injection clavier atteignant le renderer de Tabby. Banc Electron
38.8.6 identique à celui de Tabby, dbus-monitor comme instrument indépendant du plugin,
CDP en lecture seule pour mesurer le blocage du renderer plutôt que le constater à l'œil.
| Revendication | Verdict | Preuve décisive |
|---|---|---|
| Plugin désactivé → Tabby démarre, zéro contact avec le trousseau | CONFORME | 0 appel Secret Service au dbus-monitor ; le contrôle « plugin retiré » est indiscernable du plugin présent |
| Plugin activé → un blocage, puis relance sur la pop-up native, état « suspendu » levable | CONFORME | Renderer muet à CDP de t=10 à 60 s, témoin relecture du mot de passe survivant au SIGKILL, relance sans aucun appel au trousseau |
encryptString/decryptString bloquent comme isEncryptionAvailable | CONFIRMÉ | Série entrelacée : 6/6 bloqués à 45 s sans réponse ; 3/3 rendus en 3,8–6,7 s avec réponse |
0600 sur fichier préexistant en 644 ou 666, et atomicité | CONFORME | L'inode de la cible devient celui du temporaire ; le comportement fautif de writeFileSync est contrôlé hors plugin |
learnFromUser de bout en bout depuis la vraie pop-up | CONFORME | Saisie humaine → jeton en 600 → jeton vérifié + coffre déverrouillé à la relance |
Deux réfutations, toutes deux mesurées, et la seconde porte sur du code écrit le matin même.
keychainGuard.ts et osKeychain.ts.keychainStatus(), qui sur Linux
ne lit que le nom du backend — l'appel dont la même campagne mesure qu'il ne touche jamais le
trousseau. Sur trousseau verrouillé, le bouton annonçait « disponible », effaçait le témoin, et le
démarrage suivant regelait à l'identique, mesuré. La garantie « le premier blocage est le
dernier » tenait face à un utilisateur passif et était annulée par le seul chemin de sortie que le
plugin lui-même désigne. Le raccourci Linux introduit le matin pour retirer un appel
bloquant du chemin de démarrage est exactement ce qui a vidé la vérification de sa substance —
une optimisation correcte à un endroit, ruineuse à un autre. Corrigé par
keychainRoundTrip() : aller-retour chiffrement/déchiffrement réel sur une valeur
jetable, réservé aux actions explicites de l'utilisateur.BAD_DECRYPT pour la
même saisie. passphraseOpensVault() existait et servait déjà dans
serveFromToken() ; learnFromUser() ne l'appelait pas. Corrigé, voir
#V18.better-vault.json à deux dates sait
si le mot de passe maître a changé, sans rien déchiffrer. Le fichier est en 600, l'exposition est
faible : dit dans les README, pas corrigé côté plugin — il n'y a rien à y corriger.guardState() conclut à la suspension sur la seule présence du fichier, et
depth ne compte qu'au sein d'un processus. Deux fenêtres Tabby démarrant ensemble
verraient donc la seconde se refuser le trousseau et journaliser une suspension fictive. À
exercer avant de redessiner quoi que ce soit : c'est une piste issue du code, pas
un constat. Premier point de la troisième campagne.isEncryptionAvailable() seul est emprunté, getSelectedStorageBackend
n'existant pas ; donc là où keychainStatus() engage davantage que sur Linux.basic_text — non rejoué cette fois (exigerait
--password-store=basic). Vérifié lors de la première campagne.rename(2), jamais provoquée.learnFromUser sur trousseau verrouillé) —
couvert par déduction depuis la mesure de encryptString, pas par une exécution Tabby
dédiée.sliding/never, rétention du journal — hors
du périmètre imposé.Faire saisir un secret par un humain déplace la discipline de test : elle ne porte plus seulement sur les données que la machine manipule, mais sur ce que l'humain tape. Toute valeur qu'il devra fournir doit donc figurer dans le protocole AVANT la campagne — jamais improvisée devant une pop-up, où le premier réflexe est de taper un vrai mot de passe.
Corollaire, sur la trace : les transcripts de l'agent conservent en clair tout ce qui a transité, et l'instance ne peut pas les purger elle-même après coup. Leur suppression appartient au protocole, pas au rattrapage.
Journal : AI-HISTORY.html, entrée « Deuxième campagne — les correctifs validés, et deux défauts qu'ils ont créés (2026-07-29) ».
Évolutions post-livraison : D1 et D2, trouvés ici, revalidés par la troisième campagne le 2026-08-01.
Commits : non individualisés au journal pour les correctifs D1/D2 de cette campagne (entrées marquées « cette série ») — réserve consignée telle quelle plutôt qu'un hash reconstitué.
Une instance externe, sans accès à .AIRules/, à CLAUDE.md ni à
l'historique Git, a mené une vérification adversariale sur Ubuntu 24.04 / GNOME / X11, Tabby 1.0.235,
Electron 38.8.6. Périmètre imposé : stockage interne et garde-fou du trousseau.
Rapport intégral, 516 lignes de mesures :
RAPPORT-TEST-LINUX.md — commandes reproduites,
tableaux de résultats, et les six pistes de son § 8. Les conclusions sont ci-dessous ; l'annexe ne
porte que le détail qui les étaye (A-8).
Sa méthode mérite d'être notée pour être rejouée : plutôt que de raisonner sur le code, elle a
compilé et exécuté les vrais store.ts et osKeychain.ts
dans un banc Electron 38.8.6, en interceptant @electron/remote et en redirigeant
configDir() vers un dossier jetable — puis a validé la causalité par une
expérience de contrôle (plugin retiré, condition maintenue).
Le plugin fige Tabby au démarrage lorsque le trousseau est présent mais verrouillé — et il le fait même désactivé. Tabby n'atteint jamais sa propre pop-up de mot de passe ; l'écran de démarrage reste affiché indéfiniment. Vérifié par contrôle : plugin retiré, trousseau toujours verrouillé, Tabby atteint normalement sa pop-up.
Chaîne causale, confirmée à la source :
src/index.ts appelle install() depuis le constructeur du
NgModule ;install() appelle keychainStatus() inconditionnellement,
à seule fin de journaliser — le test de enabled vit dans resolve(),
bien plus tard ;keychainStatus() est synchrone et passe par @electron/remote, dont
l'IPC est bloquant ;isEncryptionAvailable() ne revient jamais
(il tente de lire l'entrée témoin, ce qui demande un déverrouillage qui n'aboutit pas) — et
n'affiche aucun dialogue.Le try/catch de keychainStatus() n'attrape rien : un appel
bloquant n'est pas une exception. Le « repli sur la méthode native » n'est jamais atteint,
puisque le gel survient avant que getPassphrase ne soit invoqué.
Ce défaut contredit le principe fondateur du plugin — « ce plugin est un confort : il ne doit jamais empêcher d'ouvrir son coffre ni bloquer Tabby » — et la promesse publique du README (« the plugin never blocks access to your vault »).
Correctif écrit le 2026-07-29, les trois gestes et pas l'un des trois :
install() ne touche plus au trousseau, le diagnostic vit dans resolve()
donc après le test de enabled, et tout accès passe par withSafeStorage()
sous le garde-fou de src/keychainGuard.ts. Le panneau de réglages, qui sondait à
l'initialisation d'un champ — donc figeait le seul endroit d'où l'on peut désactiver le plugin —
sonde désormais sur demande explicite ou sous enabled. Détail du mécanisme et de ce
qu'il garantit : #V17.
Le troisième geste n'a pas la forme prévue. « Rendre non bloquant » est
impossible : @electron/remote fait de l'IPC synchrone, donc aucun délai d'attente ne
peut expirer depuis le fil que l'appel arrête. Ce qui est fait à la place — un témoin sur disque
écrit avant l'appel, relu au démarrage suivant — ne supprime pas le premier gel, il le rend
dernier. C'est une garantie plus faible que celle envisagée, et le README dit
exactement celle-là, pas davantage.
Validé sur Linux le 2026-07-29, dans les deux régimes — voir
Deuxième campagne. Plugin désactivé : zéro appel au Secret Service, et le
contrôle « plugin retiré » est indiscernable. Plugin activé : un blocage, témoin survivant au
SIGKILL, relance sur la pop-up native sans toucher au trousseau.
Mais ce correctif a engendré D1, de sévérité haute : le raccourci Linux qui
retire isEncryptionAvailable() du chemin a du même coup vidé de sa substance la
vérification du panneau de réglages, seul chemin de sortie offert à l'utilisateur. Corrigé le soir
même par keychainRoundTrip(), non revalidé.
0600 n'est pas un invariant, et l'écriture n'est pas atomique
fs.writeFileSync(path, data, { mode: 0o600 }) n'applique le mode qu'à la
création. Mesuré sur le vrai writeSettings : création fraîche → 600 ;
fichier préexistant en 644 ou 666 → mode inchangé, jamais re-serré. Le cas nominal
est donc correct, mais l'invariant « lisible par le seul propriétaire » tombe dès qu'un fichier
préexiste plus large — restauration de sauvegarde, copie manuelle, umask différent.
Au même endroit, l'écriture est directe, sans fichier temporaire ni rename : une
coupure en cours d'écriture tronque le fichier. Conséquence atténuée (la lecture défensive retombe
sur les défauts, le jeton est perdu, l'utilisateur ressaisit), mais l'atomicité n'existe pas.
Correctif écrit le 2026-07-29 : temporaire dans le même dossier (un
rename entre volumes échoue en EXDEV) + chmod explicite +
rename. Le fichier qui atterrit est donc toujours neuf, donc toujours créé en 600.
Un choix délibéré sur le chemin d'échec : rename sur une cible
existante peut échouer en EPERM/EBUSY sous Windows (antivirus, client de synchronisation). Y
renoncer perdrait l'écriture, donc le jeton que l'utilisateur vient de confier — pire que le
comportement d'avant le correctif. On retombe sur l'écriture directe, qui rétablit l'ancien
compromis plutôt que rien. L'écriture précède la journalisation, pour qu'un journal défaillant ne
coûte pas le jeton non plus.
Vérifié, et ce qui ne l'est pas. Banc sur le store.js compilé,
dossier jetable : JSON valide conservé, temporaire nettoyé, temporaire résiduel écrasé,
enchaînement attesté par une trace fs
(write(tmp, mode=600) | chmod(tmp, 600) | rename), écriture préservée avec
rename et journal en panne.
Les bits POSIX ont été mesurés sur Linux le 2026-07-29 : fichier préexistant en
644 comme en 666 → 600 après écriture, et l'inode de la cible devient celui du temporaire, donc le
fichier qui atterrit est bien neuf. La campagne a en outre contrôlé le mécanisme hors plugin
— writeFileSync(…, {mode: 0o600}) sur un fichier préexistant en 666 laisse bien 666 :
le défaut que ce correctif prétend régler existe réellement. Seule la non-troncature sous coupure
réelle reste déduite de rename(2).
Le commentaire d'osKeychain.ts et le piège
#V15 affirmaient tous deux que le repli natif rattrapait le
trousseau verrouillé. C'est réfuté (défaut #1). Corrigé le 2026-07-29 dans les deux fichiers.
README corrigé le 2026-07-29, dans les deux langues, en même temps que le correctif #1 comme prévu. La ligne « the plugin never blocks access to your vault » n'a pas été restaurée : la rétablir telle quelle aurait réintroduit le défaut #3 sous un commit de correction. Elle est remplacée par la garantie réellement tenue — désactivé, aucun contact avec le trousseau ; activé, un démarrage figé au plus, auto-réparé au suivant. Deux sections neuves : When the keychain is locked et What storing the passphrase actually costs you.
La campagne a extrait la clé safeStorage depuis le trousseau par simple appel
D-Bus, sans Electron ni Chromium, puis reconstitué le mot de passe maître hors ligne à
partir de better-vault.json. Ce n'est pas un défaut du plugin : GNOME Keyring
n'applique aucun contrôle d'accès par application, donc tout processus tournant sous le compte de
l'utilisateur lit l'entrée. Le rapport l'attribue à un « KDF décoratif » (PBKDF2-SHA1, sel fixe, une
itération) — l'analyse est inexacte sur ce point : avec gnome_libsecret l'entrée du KDF
est un secret aléatoire de 24 octets, durcir le KDF n'apporterait rien.
Le delta honnête, à écrire tel quel : sans le plugin, le mot de passe maître ne touche
jamais le disque ; avec lui, il devient récupérable au repos, en l'absence de l'utilisateur, par
n'importe quel processus de sa session. C'est un accroissement réel de l'exposition,
structurel à safeStorage sous Linux. Le README l'admettait en creux (« cannot be more
secure than the keychain it delegates to ») ; il le dit désormais franchement, sous
What storing the passphrase actually costs you, en nommant l'extraction D-Bus et en
étendant le constat à DPAPI sous Windows.
À reprendre dans une campagne ultérieure — ce sont des non testés, pas des conformes :
learnFromUser),
de bout en bout. L'injection clavier XTEST n'atteint pas le moteur de rendu Chromium de Tabby (les
clics passent, les touches non) ; la campagne a contourné en fabriquant les jetons. Les briques
(encrypt, writeToken) sont vérifiées, l'enchaînement par l'interface ne
l'est pas.isEncryptionAvailable seul est emprunté, getSelectedStorageBackend
n'existant pas.Faux positif à ne pas reprendre : le rapport signale des renvois morts vers
.AIRules/ dans le code livré. C'est un artefact du dispositif de test — le dossier avait
été retiré du clone pour préserver l'aveuglement. Dans le dépôt réel, ces renvois résolvent.
Journal : AI-HISTORY.html, entrée « Campagne indépendante et ses trois correctifs (2026-07-29) ».
Évolutions post-livraison : le correctif du défaut #1 a engendré D1 (sévérité haute), trouvé et corrigé par la deuxième campagne le même jour.
Commits : 4c1fe0d, 2dc049c, f086739,
f0548f8, 75de08a.
Onglet « Better Vault » dans les paramètres de Tabby
(SettingsTabProvider de tabby-settings, composant Angular avec template
Pug inliné — piège hérité #3).
Trois modes, l'échéance étant recalculée à chaque enregistrement de mot de passe :
| Mode | Comportement |
|---|---|
schedule (défaut) |
Prochain créneau jour + heure fixes, lundi 3 h par défaut. Choisi pour que la ressaisie tombe toujours au même moment, en début de semaine, jamais en pleine session. |
sliding |
N jours à compter de la saisie, comme le « remember for » natif de Tabby. Simple, mais l'échéance tombe à une heure imprévisible. |
never |
Le jeton reste valide jusqu'à purge manuelle ou changement du mot de passe maître. |
Cas limites vérifiés sur le calcul : un jeton créé le lundi à 2 h expire le jour même à 3 h (pas une semaine plus tard) ; à 3 h pile ou après, il vise le lundi suivant. Passage d'année et changement d'heure traités — tout est calculé en heure locale.
Un jeton enregistré avant qu'une politique d'expiration n'existe se voit attribuer une échéance au premier usage suivant, sans quoi il resterait éternellement valide alors que les réglages affichent une échéance.
À l'instant où le mot de passe est confié à l'OS, une notification l'annonce : où il est stocké, jusqu'à quand, et comment le révoquer. Un plugin qui capte un secret ne doit jamais le faire en silence — l'information doit atteindre l'utilisateur au moment où elle le concerne, pas dormir dans un panneau de réglages qu'il n'ouvrira peut-être jamais.
Affichée via ToastrService (ngx-toastr), avec un délai
allongé à 12 s : le message compte trois informations à lire. ngx-toastr doit rester
external dans webpack — Tabby le pré-cache (#V8)
et sert sa propre instance, seule reliée au conteneur de toasts réellement affiché. Le bundler
n'ayant pas de paquet à résoudre, il est installé en devDependency pour ses seuls
typages, avec @angular/platform-browser qu'il réclame en peer.
Aucune mécanique dédiée n'a été nécessaire : better-vault.json étant strictement
local et jamais synchronisé (§ Stockage), chaque
machine a naturellement ses propres réglages. Un poste fixe peut garder le mot de passe une
semaine pendant qu'un portable le redemande chaque jour, et l'exclusion d'une machine se fait
simplement en la laissant désactivée.
Journal : AI-HISTORY.html, entrée « Implémentation du déverrouillage automatique (2026-07-28) », lignes « Panneau validé par l'utilisateur, puis habillage et repositionnement » et « Panneau de réglages et expiration du jeton ».
Évolutions post-livraison : aucune sur le fond. Migré ici le 2026-08-07 : cette section vivait encore dans ROADMAP.html, marquée « non terminale » par erreur dans son § Registre des livrés alors qu'elle ne portait, en réalité, aucun point « reste ouvert ».
Pièges : piège hérité #3 (templateUrl inopérant pour un plugin
tiers) et #14 (styles de composant hors :host), cités en tête de section — aucun piège
numéroté #V propre à ce chantier.
Commits : 623f768, d8c05ff.
Horodatage absolu, niveaux INFO/WARN/CRIT, révocation
manuelle et changements de configuration tracés, rétention réglable (défaut 90 jours) doublée d'un
plafond de taille, boutons « Ouvrir » et « Purger » (avec confirmation), et notification de
déverrouillage ancrée à l'onglet actif (#V13). Le
vocabulaire des messages a été aligné sur celui de l'interface : le journal disait « keychain » là où
le panneau dit « trousseau », ce qui rendait une recherche infructueuse.
Le journal est documenté dans les README publics, avec ses deux réserves reprises telles quelles — décision de l'utilisateur : « on est honnête, et normalement un utilisateur de ce plugin le sait déjà ».
Le point « viser le panneau actif plutôt que l'onglet dans une vue splittée », reporté sans urgence à la livraison, a été rouvert le 2026-08-14 par l'utilisateur et vit désormais comme chantier actif, élargi aux couleurs des toasts par sévérité : ROADMAP.html#toasts.
better-vault.log est un véritable historique d'événements de cycle de vie : savoir
quand le coffre a été ouvert, quand un jeton a expiré, quand un garde-fou s'est déclenché.
Emplacement tranché par l'utilisateur : le fichier dédié existant, et non le flux
natif de Tabby — choix confirmé par le code, LogService ne fabriquant qu'un
ConsoleLogger qui écrit dans la console DevTools, sans rien persister.
L'invariant de sécurité s'applique sans exception : le mot de passe en clair n'est jamais journalisé, ni sa longueur. Seuls des événements anonymes sont tracés.
basic_text).manual revocation from the settings — token deleted
(settingsTab.component.ts, forgetNow()).stamp()/writeLine() dans
src/logger.ts, et non un délai relatif au démarrage.INFO/WARN/CRIT
(Level, src/logger.ts).settingsTab.component.ts, persist()).applyRetention()), doublée d'un plafond de taille fixe et non configurable
(HARD_SIZE_LIMIT, 4 Mo) qui tronque par la fin même en cours de session — la purge par
date, elle, ne tourne qu'au démarrage.Cette section portait jusqu'ici un Ce qui manque réellement listant exactement les
cinq derniers points de la liste ci-dessus comme non implémentés — en contradiction directe avec le
callout « Réalisé » juste au-dessus, qui les annonçait déjà faits. Vérification indépendante,
répétée de zéro plutôt que reprise sur parole : lecture directe de src/logger.ts
(stamp(), Level, HARD_SIZE_LIMIT,
applyRetention()) et de src/components/settingsTab.component.ts
(forgetNow(), persist()), croisée avec
AI-HISTORY.html § « Journal d'audit et notifications
(2026-07-28) » : les cinq points ont bien été livrés par 10ad983 le 2026-07-28, le jour
même où la liste erronée a été écrite. Rien dans git log ne montre un retrait ultérieur
de ces fonctions. La liste est retirée plutôt que corrigée sur place — un chantier livré n'a pas
vocation à garder la trace d'une liste de choses à faire qu'il a déjà faites.
Il est local à la machine, et le restera. L'idée de « repérer un comportement
suspect si la configuration est partagée » ne s'applique pas ici : better-vault.json
comme le journal sont délibérément exclus de toute synchronisation
(§ Stockage). Chaque poste a le sien. Consigner le nom de la
machine n'a donc d'intérêt que si les journaux sont agrégés ailleurs — ce qui reste à décider.
Il n'est pas inviolable. C'est un fichier texte modifiable par quiconque a accès à la session de l'utilisateur — c'est-à-dire précisément l'attaquant contre lequel un audit servirait. Il vaut comme outil de diagnostic et de détection a posteriori, pas comme preuve. À ne pas présenter autrement dans le README.
Journal : AI-HISTORY.html, entrée « Journal d'audit et notifications (2026-07-28) ».
Évolutions post-livraison : vocabulaire aligné sur l'interface (« trousseau » plutôt que « keychain ») le jour même. Erreur de documentation corrigée le 2026-08-07 — voir le callout ci-dessus ; migré ici le même jour depuis ROADMAP.html, marqué « non terminal » par erreur dans son § Registre des livrés à cause de cette même liste fausse.
Pièges : #V13.
Commits : 10ad983, f9fddac (chantier lui-même) ·
a8f86b4, 2f672b4 (notification à chaque déverrouillage, ancrage à l'onglet
actif — même branche feat/audit-log).