Repository navigation
fix(secrets,#17441): .secrets/ couvert par une règle versionnée + organe de couverture - #17442
Conversation
…ane de couverture Le repertoire .secrets/ -- master.env (cles API de la flotte) et les cles privees d'App GitHub -- n'etait ignore que par .git/info/exclude, local et non versionne. Sur la machine qui le porte, git status reste propre ; sur tout autre clone la regle n'existe pas et le secret est propose au commit. Les deux situations sont indiscernables a l'oeil : seule la source gagnante de `git check-ignore -v` les separe. Le .gitignore versionne ne listait que sept fichiers nommes, ajoutes un par un au fil des secrets -- donc tout secret apparu apres la derniere mise a jour de cette liste etait a decouvert. L'asymetrie le disait deja : scripts/.secrets/ et le chemin docker sont couverts EN TANT QUE REPERTOIRES, celui de la racine ne l'etait pas. Ce defaut avait deja ete trouve au meme endroit sur master.env. Une lecon avait ete ecrite, aucun detecteur pose ; un mois plus tard le meme trou a re-expose une nouvelle classe de secret. D'ou l'organe, pas seulement la regle. - .gitignore : regle de REPERTOIRE `.secrets/` (+7 lignes, pas de reecriture CRLF ; les entrees nominatives restent, redondantes). - scripts/ci/check_secret_paths_ignored.py : lit la source gagnante de `git check-ignore -v --no-index` et verifie qu'elle est SUIVIE par git. Signale aussi un secret deja dans l'index -- une regle arrivee apres le `git add` ne retire rien. exit 2 distinct pour « je n'ai pas pu mesurer ». - scripts/tests/ : 11 tests, dont la paire discriminante DEFAUT/CLEAN sur la MEME arborescence temoin selon que la regle vit dans l'exclude local ou dans un .gitignore suivi, et le cas Windows ou la source porte un « : ». - always-on-guards.yml : 16e organe, aucun slot de runner supplementaire. Falsification : rc=1 avant le correctif, rc=0 apres, avec deux chemins verts DANS LES DEUX etats -- sans quoi la mesure ne discriminerait pas. Les quatre mutations delibarees de l'organe sont toutes rattrapees, aucune survivante. See #17441 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
G-VAR-2 light cap reached (advisory, non bloquant). |
|
G-VAR-2/3 GENRE signals (advisory, non bloquant, #10020).
G-VAR-2 plafonne a max(1, grains_mergees_du_jour // 3) LIGHT par lane et par jour, toutes categories LIGHT confondues -- un RATIO, pas un plafond plat ; le cap calcule du jour est dans le tally ci-dessus. G-VAR-3 interdit deux genres LIGHT consecutifs. Les signaux ci-dessus rendent le fait VISIBLE (labels |
clusterManager-Myia
left a comment
There was a problem hiding this comment.
[NanoClaw] structural review (4 fichiers lus : règle, garde, organe, tests — code non ré-exécuté localement, pas de python3 sur ai-01 ; l'exécution réelle est attestée par la CI au head, cf. artefact)
VERDICT: LGTM (vérifié : garde exécutée VERTE par la CI au head 0ec37747 sur un clone runner SANS exclude local — la réfutation directe du défaut de #17441)
Artefacts d'exécution (mesures, pas reprises du body) :
Always-on guards — 16 organes= success au head : la nouvelle étapesecret_paths_ignored(workflow l.1333-1337, agrégat l.1369) a tourné et rendu vert — or le clone runner ne porte PAS le.git/info/excludelocal qui masquait le défaut. La fermeture est prouvée là où le trou vivait.- Gitleaks + Gitleaks positive controls = success : aucun secret introduit (les 4 fichiers sont règles + code, aucun
.env). - 0 échec des tests de la PR : le log-failed de
Scripts Tests (CPU)(run 35761481483) contient zéro assertion du domaine secrets — tous les échecs sont la classeBASELINE_FAILED / exit 127 / sandbox gauntletdu runner self-hosted (parc #17397, note Hermes 17:31Z : frappe toute PR touchantscripts/), non attribuables à cette PR. CodeQL/PR gate rouges = même famille en cascade.
Qualités de conception qui méritent d'être nommées (réponses directes aux classes vivantes du jour) :
- Contrôles positifs APPLIQUÉS, pas décoratifs : un contrôle positif qui ressort ignoré rend
INSTRUMENT_CASSEen priorité maximale (l.129-135) — la classe #17435 (skip sur sonde muette) est adressée en naissance, structurellement. check-ignore -v --no-index: mesure les règles, pas l'état de l'index — un chemin déjà suivi ne peut pas masquer la question ; et la source gagnante doit être un fichier suivi (ls-files), ce qui exclut mécaniquementinfo/excludeetcore.excludesFilelocaux. C'est exactement le test doc-vs-code qui a ouvert ce dossier.secrets_deja_suivis(l.126-128) : « la règle arrive trop tard » — le cas où un secret serait DÉJÀ dans l'index est un défaut à part entière, sondé génériquement sur*/.secrets/*.- Falsifiabilité assumée : sondes injectables (« un organe qu'on ne peut pas faire échouer à volonté n'est pas falsifiable »), exit 2 = mesure impossible jamais confondue avec un vert, 11 tests sans aucun skip dont
test_le_controle_positif_doit_rester_visibleettest_hors_depot_rend_unknown_pas_un_vert.
Réserves (non bloquantes) :
- La couverture « règle versionnée » est nominative : 3 répertoires sonde figés dans
CHEMINS_SENSIBLES(l.33-38). Un 4ᵉ répertoire de secrets futur ne sera sondé que s'il est ajouté à la main — seule la détection « déjà suivi » est générique. Amélioration candidate : dériver les sondes de l'arborescence (tout chemin*/.secrets/existant) plutôt que d'une constante. - Redondance bénigne héritée : les règles fichier préexistantes (
.secrets/.env.huggingface,qwen-api-user.token.backup…) sont désormais couvertes par la règle répertoire.secrets/— nettoyage cosmétique au fil de l'eau. - Vérification des 3 chemins sonde contre
.gitignore@head:.secrets/(l.390),scripts/.secrets/(l.393),docker-configurations/services/comfyui-qwen/.secrets/(l.399) — tous couverts, cohérent avec le vert CI.
— review structurelle + mesures CI depuis myia-ai-01 ; les rouges gauntlet restants sont le dossier #17397/#17443 en cours côté owner.
Path-collision (organ #13359/#13615)Cette PR #17442 (
|
|
[ADJOINT PREFLIGHT] Motif BLOCKED : un B.0 inline et une alerte CodeQL. Lane myia-ai-01:CoursIA, tete c2a2be2.
|
…rets CodeQL (py/clear-text-logging-sensitive-data, alerte #142) classait `rapport["secrets_deja_suivis"]` comme donnee sensible sur la seule foi de son nom : la valeur imprimee est un CHEMIN de fichier deja suivi par git, jamais le contenu d'un secret. Un `# codeql[...]` serait inerte sur ce depot (default setup, cf .claude/rules/codeql-suppressions-inertes.md) : le nom dit maintenant ce que la cle porte. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
[ADJOINT PREFLIGHT] Prévalidation à tête exacte, sans décision de merge. Diff complet : |
|
[ai-01, auteur] Alerte CodeQL « Clear-text logging of sensitive information » sur La ligne 179 affiche le chemin d'un fichier déjà présent dans l'index sous un répertoire de secrets ( Le dépôt tourne sous CodeQL default setup. Un commentaire |
.gitignore : resolu vers main. #17442 couvre deja .secrets/ par une regle versionnee (et une garde CI) ; le hunk /.secrets/ de cette PR est redondant. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…re empruntee a #17442 Revue Hermes (22/09, tete 98e5ab2) : la doc promettait que l'organe nomme la source de l'ignorance (`check-ignore -v`), le code ne lisait que le code retour (`-q`). Un chemin ignore par le seul .git/info/exclude passait pour protege. - _git_ignored reutilise verdict_chemin de scripts/ci/check_secret_paths_ignored.py (#17442) : VERSIONNEE / LOCALE / NON_IGNORE, None si non mesure ; - get refuse LOCALE en nommant la source ; un motif `!` gagnant compte comme non ignore (git check-ignore -v --no-index rend rc=0 sur une negation) ; - 5 tests sur un depot git reel (versionnee, locale, non ignore, negation, hors depot) ; - la section de regle sort de cette PR (changement normatif, sign-off separe) ; - la doc ne decrit plus le hunk .gitignore, livre par #17442. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ap, doctor, get, gh-login) (#17425) * feat(secrets): organe d'acces au trousseau partage MyIA-Keys + /.secrets/ versionne Deux gestes qui se tiennent : un organe qui ouvre le coffre partage sans jamais en imprimer le contenu, et la fermeture du trou qui rendait `.secrets/` stageable sur toute machine autre que celle-ci. ## scripts/secrets/agent_keyring.py Le coffre `MyIA-Keys.kdbx` est distribue par le GDrive RooSync ; sa passphrase vit dans le gestionnaire d'identifiants Windows (DPAPI, par utilisateur), posee une fois par machine. Regle cardinale : aucune sous-commande n'imprime un secret par defaut -- une valeur est soit tuyautee vers son consommateur (`gh-login`, `--to-env-file`), soit montree masquee. `bootstrap` valide chaque candidat en OUVRANT reellement le coffre, puis range le gagnant : la passphrase n'entre jamais dans le contexte de l'agent. ## .gitignore `/.secrets/` -- le repertoire ENTIER. Les lignes existantes enumeraient des fichiers un par un et `.secrets/master.env`, la source unique de `secrets-hygiene.md`, n'y figurait pas : il ne devait son exclusion qu'a `.git/info/exclude`, qui est LOCAL au clone et non versionne. Protege ici, stageable partout ailleurs. Controle positif : dans un depot neuf ne portant que ce `.gitignore`, `git check-ignore -v .secrets/master.env` rend `.gitignore:383:/.secrets/` et `git add -A` ne stage que `.gitignore`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(secrets): empreinte sha256 partout -- plus aucun caractere de secret emis CodeQL a signale 4 alertes high `py/clear-text-logging-sensitive-data` sur ce fichier. Reassessment (protocole audit-reassessment) : sur le FLUX il sur-accusait -- les valeurs passaient par `mask()` / `secret_kind()`, qu'il ne reconnait pas comme assainisseurs, et ce qui sortait etait une longueur plus 4 caracteres, ou une classification a 3 valeurs. Sur le FOND il visait juste pour `mask()`, et c'est la raison du changement : 1. Le benefice des 4 derniers caracteres est NUL ici -- un coffre KeePass n'expose aucune interface ou recouper une queue de valeur, contrairement a un fournisseur d'API ou la convention vient. 2. Le cout est reel -- publier la fin d'une phrase memorisable en retire une part d'entropie, et cette sortie finit dans un journal ou un scrollback. `mask()` est donc supprimee, et `fingerprint()` (longueur + sha256 tronque) sert partout. Ce n'est pas une mise en conformite, c'est une correction : on payait une fuite sans rien acheter. Aucun `# codeql[...]` n'est ajoute -- ces commentaires sont INERTES sur ce depot (CodeQL en default setup, cf .claude/rules/codeql-suppressions-inertes.md). La rationale vit dans le body de la PR, comme la regle le prescrit. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(secrets): web1 est la 7e machine -- EXPECTED_GH, FLEET_MACHINES, et gh-login par username Correction user : je comptais 6 machines en oubliant `MyIA-Web1`. Mesure : le compte GitHub `MyIA-Web1` existe (cree le 2026-04-17, meme jour qu'ai-01 et po-2023), le coffre porte une entree `github web1`, et le dashboard `machine-myia-web1` est actif. Mais `docs/reference/cluster-agents.md` ne le mentionne PAS -- zero occurrence -- et `web1` n'apparait qu'une seule fois dans tout le depot, dans un ledger archive de juillet. Ce n'est pas forcement un defaut de ce document : il decrit les machines qui portent des grains CoursIA, et web1 travaille sur roo-extensions. Mais ce n'est alors PAS la bonne population pour ce trousseau. La population pertinente ici est « les machines qui doivent ouvrir le coffre », d'ou deux constantes desormais distinctes : - `EXPECTED_GH` (7) -- les comptes dont une entree est attendue. `verify` rend maintenant 6/7 au lieu de 5/6, et revele que `github web1` a lui aussi un secret VIDE, comme `github ai-01`. - `FLEET_MACHINES` (7) -- les machines qui doivent detenir la passphrase. C'est le denominateur du critere de retrait du PDF de secours : DPAPI n'etant ni exportable ni transferable, le PDF reste la seule source de toute machine qui n'a pas bootstrappe. Troisieme correction, meme racine que celle deja corrigee dans `verify` : `gh-login` resolvait le login attendu par `EXPECTED_GH.get(entry.title)`. Le coffre titre `github ai-01` quand la clef est `myia-ai-01`, donc la recherche rendait None et la verification de login etait **muette** -- un jeton du mauvais compte serait passe sans alerte. Resolution par titre OU username. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat(secrets): doctor imprime l'empreinte -- le critere de retrait du PDF devient mesurable Le critere « toutes les machines ont bootstrappe » etait pose mais pas MESURABLE : DPAPI n'est ni exportable ni interrogeable a distance, donc aucune machine ne peut verifier qu'une autre detient la passphrase. Un critere qu'on ne peut pas mesurer n'est pas un critere -- c'est une affirmation qu'on finit par prendre pour acquise. `doctor` imprime desormais l'empreinte `sha256` tronquee de la passphrase stockee, et rappelle la liste des machines a couvrir. L'empreinte est non reversible, donc publiable sur un dashboard : c'est precisement ce qui permet a N machines de prouver qu'elles portent la MEME passphrase sans qu'aucune ne la transmette. C'est l'organe qui rendra le retrait du PDF de secours decidable, au lieu de reposer sur un « tout le monde est enregistre » que personne ne peut refuter. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(secrets): pbkdf2 pour l'empreinte, et plus d'empreinte du tout sur les entrees Deux alertes CodeQL distinctes, et dans les deux cas l'outil visait juste. ## py/weak-sensitive-data-hashing -- causee par mon propre fix precedent Mon passage a sha256 a ouvert cette alerte : un hash RAPIDE est inadapte a un secret. Et elle porte, parce que cette empreinte est **publiee sur un dashboard** -- elle offre donc un oracle hors-ligne : deviner, hacher, comparer. Sur une passphrase a haute entropie le risque reste theorique ; sur un secret faible il ne l'est pas, et un outil generique ne choisit pas ce qu'on lui donne. PBKDF2-HMAC-SHA256, 600 000 tours (OWASP 2023). Deterministe -- deux machines comparent toujours -- mais ~0,3 s par essai, ce qui rend l'oracle inutile. Cout a l'usage : nul, on l'appelle une fois par `doctor` (mesure : 0,9 s au total). Le sel est public et fixe : il DOIT l'etre pour que la comparaison cross-machine fonctionne. Il ne cache rien, il separe les domaines. ## py/clear-text-logging x3 -- supprimees a la racine, pas dismissees En cherchant a justifier ces trois alertes, j'ai vu qu'elles n'avaient aucune raison d'exister : **le coffre est PARTAGE, donc ses entrees sont identiques sur toutes les machines par construction. Il n'y a rien a comparer.** L'empreinte sur une entree ne servait a rien -- et `show` affiche deja le champ `modifiee`, qui couvre le seul cas reel (detecter une copie GDrive en retard). Les entrees rendent desormais la NATURE du secret et sa LONGUEUR, rien qui en derive cryptographiquement. Seule la passphrase garde une empreinte, parce qu'elle est la seule chose stockee PAR MACHINE (DPAPI) et donc la seule qu'on ait besoin de comparer. C'est la bonne lecon de ces trois alertes : elles ne demandaient pas une justification, elles signalaient un affichage inutile. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(secrets): retirer la longueur des secrets d'entree -- elle ne tranche rien CodeQL rouvre `py/clear-text-logging-sensitive-data` sur quatre lignes. Deux sont des faux positifs nets, deux portent. ## Ce qui porte -- `show` et `verify` emettaient `len(entry.password)` Une longueur de mot de passe est une divulgation mineure mais REELLE : elle retrecit l'espace de recherche. Et elle est reellement publiee -- la sortie de `verify` est exactement ce qu'une lane recopie dans un dashboard pour rendre compte, donc « 20 car. » circule. Surtout, elle ne tranche RIEN. `verify` repond a une seule question -- ce compte a-t-il un PAT utilisable ? -- et `secret_kind()` y repond deja en trois valeurs, `vide` inclus. La longueur etait decorative, exactement comme les empreintes d'entree retirees au commit precedent : un affichage qui ne sert aucune decision n'est que de la surface en plus. ## Ce qui reste, et pourquoi c'est un faux positif assume Les deux autres lignes n'emettent que `secret_kind(...)` -- un classifieur a trois valeurs -- et `entry.title`, qui est un libelle choisi par l'humain, pas un secret. CodeQL suit la teinte depuis `entry.password` sans modeliser qu'elle traverse une transformation a image finie : aucun caractere du secret n'en sort, par construction. Ce depot est en CodeQL **default setup** : un commentaire `# codeql[...]` y est inerte (cf `.claude/rules/codeql-suppressions-inertes.md`, incident #12100 et ses deux commits perdus). La rationale va donc dans le body de la PR et dans une reponse ecrite sur les threads, seuls endroits ou elle est lue. ## Ce qui est CONSERVE, et pourquoi la distinction n'est pas de commodite `fingerprint()` emet toujours la longueur de la PASSPHRASE. Sur un secret de 32 caracteres a haute entropie la divulgation est negligeable, et elle sert une decision : quand deux machines divergent, l'ecart de longueur rend la cause LISIBLE -- une extraction tronquee -- la ou un ecart de hash est muet. Une longueur qui diagnostique se garde ; une longueur qui decore se retire. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs(secrets): cabler le trousseau partage dans le harnais -- regle + detail deporte Un outil qui n'est pas documente est un outil perdu a la session suivante : l'organe existe depuis quatre commits et rien dans le harnais ne dit qu'il existe, ni sous quelles contraintes il tourne. Decoupe selon les 3 tiers de `harness-hygiene.md` : - **Harnais** (`secrets-hygiene.md`, auto-chargee) : une section courte qui pose ce qu'un agent doit savoir sans avoir rien lu d'autre -- ou vit le coffre, que l'organe n'imprime aucun secret par defaut, que la passphrase se pose PAR MACHINE dans DPAPI, que l'empreinte publiee est la seule preuve cross-machine possible, et qu'une empreinte divergente interdit d'ecrire dans le coffre. - **Doc perenne** (`docs/reference/shared-keyring-myia-keys.md`) : le detail -- table des sous-commandes, les deux gardes (refus d'ecrire dans un fichier que git ne prouve pas ignore ; nature du secret tranchee sur la FORME, jamais sur le nom de l'entree), la justification des trois proprietes de l'empreinte, le critere de retrait du PDF, et la population des 7 machines. Le constat qui merite d'etre en tete de page, parce qu'il gouverne tout l'usage : **le coffre porte des mots de passe de compte, pas des PAT.** Ce n'est donc pas encore un canal de distribution de jetons, et la phase C de #17418 est entiere devant nous plutot qu'un residu. La page consigne aussi pourquoi `web1` manque a `cluster-agents.md` sans que ce document soit fautif : il decrit les machines portant des grains CoursIA, et web1 travaille sur roo-extensions. Un document fait autorite sur la population qu'il decrit, pas au-dela -- c'est ce qui m'avait fait compter six machines. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(secrets,#17425): gh-login valide l'identite AVANT de persister -- 4 defauts silencieux Les quatre defauts ont ete trouves par la prevalidation tierce de myia-po-2025:CoursIA-2, pas par l'auteur. Les quatre rendaient 0 et affichaient un `OK` : aucun n'etait visible a l'usage. 1. Shadowing de l'identite. La boucle de resolution de l'attendu utilisait `login` comme variable de boucle et ecrasait l'identite rendue par `gh api user`. La comparaison finale confrontait deux valeurs ATTENDUES : elle ne pouvait plus echouer. Temoin negatif mesure -- un jeton appartenant a `attaquant-quelconque` etait ACCEPTE comme `myia-ai-01`. Variable de boucle renommee `mapped`, et un test AST interdit desormais le nom `login` comme cible de boucle dans cette fonction. 2. `returncode` non lu. `who.returncode` n'etait jamais teste : un echec de `gh api user` passait inapercu. 3. Login vide non refuse. Le garde s'ecrivait `if expected and login and ...` -- un login vide SAUTAIT le controle au lieu de le faire echouer. Un rc=0 sans identite n'est pas une validation : il est maintenant refuse. L'attendu non resolvable rend EXIT_UNKNOWN, jamais EXIT_OK : ne pas savoir n'est pas valider. 4. Persistance avant validation. `gh auth login` etait appele en PREMIER. Il ecrit dans la configuration de `gh`, partagee par toutes les lanes de la machine : constater apres coup qu'on a installe la mauvaise identite ne la desinstalle pas, et le degat frappe les sessions voisines. L'ordre est inverse -- interrogation via GH_TOKEN dans l'environnement (aucune persistance), puis `gh auth login` seulement si l'identite concorde. Ajout connexe, meme classe : `write_passphrase` refuse d'ecrire tant que le backend n'est pas un coffre natif. `keyring` bascule en SILENCE sur un repli quand le coffre natif manque, et `keyrings.alt.file.PlaintextKeyring` ecrit en clair sur disque. La lecture reste permise sur tout backend (lire d'un repli ne cree pas d'exposition nouvelle, ecrire dedans si). La fonction rend le nom du backend, que `bootstrap` imprime : une attestation, pas une affirmation. Falsification : la suite nouvelle rend 9 echecs / 2 passes contre la version 79c1b72, et 11 passes apres correctif. Les 2 qui passent des deux cotes sont les controles positifs -- une identite concordante DOIT etre acceptee, sans quoi un test qui refuse tout passerait aussi. Non-regression : `scripts/secrets/tests/` 155 passed, 15 skipped. `doctor` de bout en bout inchange, empreinte pbkdf2:85bd9366fb37. See #17425 Co-Authored-By: Claude-Code <noreply@anthropic.com> * fix(secrets,#17425): bootstrap n'imprime plus l'empreinte du candidat PDF CodeQL (py/clear-text-logging-sensitive-data, high) signalait l'empreinte calculee sur le candidat lu dans le PDF de secours. L'empreinte qui sert le critere de retrait du PDF est celle de la valeur STOCKEE, que `doctor` imprime deja : bootstrap renvoie vers `doctor` au lieu de la dupliquer. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(secrets,#17425): le nom du backend ne transite plus par write_passphrase CodeQL (py/clear-text-logging-sensitive-data, check-run 107357328420) classe comme secret la valeur de retour de toute fonction dont le nom evoque une passphrase : `backend = write_passphrase(cand)` puis son impression l.427 etait lue comme un log de mot de passe. write_passphrase ne rend plus rien ; le nom du backend se lit par assert_backend_is_native(). Test adapte. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(secrets,#17425): get refuse une ignorance LOCALE au clone -- mesure empruntee a #17442 Revue Hermes (22/09, tete 98e5ab2) : la doc promettait que l'organe nomme la source de l'ignorance (`check-ignore -v`), le code ne lisait que le code retour (`-q`). Un chemin ignore par le seul .git/info/exclude passait pour protege. - _git_ignored reutilise verdict_chemin de scripts/ci/check_secret_paths_ignored.py (#17442) : VERSIONNEE / LOCALE / NON_IGNORE, None si non mesure ; - get refuse LOCALE en nommant la source ; un motif `!` gagnant compte comme non ignore (git check-ignore -v --no-index rend rc=0 sur une negation) ; - 5 tests sur un depot git reel (versionnee, locale, non ignore, negation, hors depot) ; - la section de regle sort de cette PR (changement normatif, sign-off separe) ; - la doc ne decrit plus le hunk .gitignore, livre par #17442. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs(secrets,#17425): deux interdits du trousseau dans secrets-hygiene (forme courte, sign-off user) Sign-off user du 2026-09-24 sur la forme courte (Q56) : la section de regle retiree de cette PR faute de sign-off revient en 5 lignes au lieu de 22 -- empreinte divergente = ne rien ecrire ; ne jamais recopier la passphrase sur un chemin partage ; pointeur vers la doc. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * test(secrets,#17425): direct tests for secret_kind, entry_key and fingerprint Hermes review point 2: only cmd_get was covered. The three pure helpers carry the contracts the rest relies on; fingerprint in particular is the value compared across machines for the 7/7 criterion, so its salt, round count and a known vector are pinned. 18 tests, secrets suite 210 passed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: jsboige <jsboige@gmail.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Grain: MED/guard -- lane myia-ai-01:CoursIA -- prev: DEEP/CI #17438
Ce que ça corrige
.secrets/— le répertoire qui portemaster.env(les clés API de toute la flotte) et, depuis cette semaine, les clés privées d'App GitHub — n'était couvert par aucune règle versionnée. Il l'était par.git/info/exclude:24, local et non versionné.Sur la machine qui porte cet exclude,
git statusest propre et rien ne distingue une protection réelle d'une protection locale. Sur tout autre clone — une autre machine de la flotte, un runner, un fork — la règle n'existe pas et le secret est proposé au commit.L'asymétrie le disait déjà :
scripts/.secrets/etdocker-configurations/.../.secrets/sont couverts en tant que répertoires ; celui de la racine ne l'était pas. Le.gitignoreversionné n'y listait que sept fichiers nommés, ajoutés un par un au fil des secrets — donc tout secret apparu après la dernière mise à jour de cette liste était à découvert.Pourquoi un organe, et pas seulement la règle
Ce défaut a déjà été trouvé au même endroit, sur
master.env. Une leçon a été écrite ; aucun détecteur n'a été posé. Un mois plus tard, le même trou a ré-exposé une classe de secret nouvelle. Une règle adossée à la vigilance a échoué exactement comme on pouvait le prévoir, donc celle-ci vient avec son détecteur.Falsification — la paire discriminante
rc.secrets/*scripts/.secrets/, dockerDEFAUT—…/.git/info/exclude:24ok—.gitignore:390Les deux derniers chemins sont verts dans les deux états. C'est ce qui rend la mesure discriminante : un organe qui refuserait tout produirait le même « rouge avant / vert après » sans rien mesurer.
Mutation testing — 4 posées, 4 rattrapées, 0 survivante
test_une_source_non_suivie_est_locale,test_une_regle_locale_seule_est_un_defautD:/…)test_decoupe_une_source_qui_contient_deux_pointstest_le_controle_positif_doit_rester_visibletest_un_secret_deja_suivi_est_un_defautSuite restaurée ensuite : 11 passed.
L'organe se fait rougir lui-même
README.mdetCLAUDE.mddoivent ressortir visibles. S'ils ressortent ignorés, le verdict estINSTRUMENT_CASSE, jamais un vert ;exit 2distinct — hors dépôt ougitabsent rendUNKNOWN. « Je n'ai pas pu mesurer » ne se confond pas avec « rien à signaler » ;git addne retire rien de l'index ; le cas a son propre verdictSUIVI.Effets de bord vérifiés avant de toucher au workflow
Le job
always-on-guardsest renommé (15→16 organes). Trois mesures avant de le faire :required_status_checks.contextssurmainPR gateseul — ce job n'est pas un check requisderive_always_on_jobs(canary du PR gate)check_unique_check_run_names.py --checkOK— 88 jobs, 70 workflows, aucun doubloncheck_absorbed_check_run_identity.py --checkOK— 16 gardes absorbés byte-identiquesPas de filtre
paths:ajouté : l'en-tête du workflow le nomme comme le piège à éviter (un check filtré par chemins restependingà vie sur les PR hors scope, #10045).Ce que ce vert ne dit pas
L'organe mesure une liste déclarée de chemins sensibles. Il ne découvre pas un répertoire de secrets qui n'y figure pas : c'est un majorant de couverture, pas une preuve d'exhaustivité. Le dire ici évite de lire son vert pour plus qu'il ne vaut.
Aucune valeur de secret n'apparaît dans ce diff, ni dans les tests — les témoins écrivent
K=vdans un dépôt temporaire.See #17441
🤖 Generated with Claude Code