Skip to content

fix(guard): reconduire LD_LIBRARY_PATH dans l'env minimal du check gauntlet - #17415

Merged
myia-ai-01 merged 1 commit into
mainfrom
fix/gauntlet-ld-library-path
Sep 22, 2026
Merged

myia-ai-01 merged 1 commit into
mainfrom
fix/gauntlet-ld-library-path

Conversation

@jsboige

@jsboige jsboige commented Sep 22, 2026

Copy link
Copy Markdown
Owner

Grain: MED/guard -- lane myia-po-2026:CoursIA -- prev: MED/guard #17414

Quoi: guard_gauntlet.run_check construisait l'env minimal de son check en omettant LD_LIBRARY_PATH. Le loader s'execute avant la premiere instruction du check : sur un interpreteur sans rpath (builds setup-python du tool-cache auto-heberge), le check ne demarre pas du tout, le gauntlet lit exit 127 avec stdout vide et rend BASELINE_FAILED — verdict indiscernable de « le garde n'a rien detecte ». La variable est desormais reconduite, et un test de regression la couvre.
Preuve: panne reproduite sur la vraie plateforme (interpreteur du tool-cache du pool po-2026) : 8 echecs / 8 passes avant, 17 passes / 2 skipped, 0 echec apres ; chaine causale verifiee en 4 maillons firsthand ; test neuf falsifie contre l'organe d'origine (ECHOUE, <none>).
Perimetre: 2 fichiers, +52 insertions, 0 suppression — scripts/ci/guard_gauntlet.py (une cle d'env + son commentaire) et scripts/tests/test_guard_gauntlet.py (un test neuf, strictement additif). Aucun notebook, aucun workflow, aucun seuil, aucun catalogue.

Le defaut

env = {
    "PATH": os.environ.get("PATH", ""),
    "SYSTEMROOT": ..., "LANG": ...,
    "GAUNTLET_SANDBOX": ..., "GAUNTLET_TARGET": ...,
}
subprocess.run(argv, env=env, ...)   # argv[0] = sys.executable

Le commentaire d'origine justifie de garder PATH « pour que le check trouve ses binaires ». Le raisonnement s'arrete un cran trop tot : PATH permet de trouver le binaire, LD_LIBRARY_PATH permet de le charger. Les deux servent le meme interet — lancer le check — et le second est plus fondamental que le premier, puisque le loader tourne avant tout code du check.

Sur un runner dont l'interpreteur est un build setup-python sans rpath, libpython3.11.so.1.0 vit dans …/x64/lib et n'est trouvable que par cette variable (c'est pour cela que le runner l'exporte dans l'env du job). Le gauntlet la supprimait donc a l'unique processus qui en avait besoin.

Le mode d'echec est le pire possible pour un garde : un check qui ne demarre pas et un check qui ne detecte rien produisent le meme signal (stdout vide), et le gauntlet les classe tous deux en BASELINE_FAILED. Le garde accusait sa propre cible d'un defaut d'environnement.

Chaine causale, verifiee firsthand

# Maillon Mesure
1 L'interpreteur du tool-cache ne demarre pas sans la variable python3 --version → error while loading shared libraries: libpython3.11.so.1.0: cannot open shared object file, rc=127
2 Le python systeme avec le meme env reduit demarre rc=0 — la variable est donc bien le discriminant, pas l'appauvrissement de l'env en general
3 L'organe omet la variable lue dans le source (run_check, env minimal) ; LD_LIBRARY_PATH n'apparait nulle part ailleurs dans le depot
4 Le CI observe exactement ce mode d'echec Scripts Tests (CPU) : fault=none exit=127, stdout_preview='', 8 failed / 14522 passed

Reproduction et paire temoin

La panne est reproduite sans toucher au tool-cache : pytest est rendu importable via PYTHONPATH depuis un venv du python systeme, et le fichier de tests est execute sous l'interpreteur du tool-cache (avec la variable, comme le runner le fait pour le job). C'est la condition CI, a l'identique.

Etat Interpreteur Resultat
base tool-cache po-2026 (3.11.16) 8 failed, 8 passed, 2 skipped
head tool-cache po-2026 (3.11.16) 17 passed, 2 skipped, 0 failed
base Windows (interpreteur sans dependance au loader) 18 passed — le defaut y est invisible, d'ou sa longevite
head Windows 19 passed (+ le test neuf)

Les 5 noms de tests que le log CI montre sont tous dans ma reproduction, qui en compte 8 comme le job CI. Le vert sur Windows explique que le defaut ait survecu : aucun poste de developpement ne peut le voir, seul un runner dont l'interpreteur a besoin du loader.

Le test neuf, et une erreur que j'ai d'abord commise

Le test etend la valeur heritee au lieu de la remplacer :

monkeypatch.setenv(
    "LD_LIBRARY_PATH",
    os.environ.get("LD_LIBRARY_PATH", "") + ":/opt/coursia-toolcache/lib",
)

Ma premiere version ecrasait la variable par un chemin factice. Elle passait sur Windows et echouait sur le runner en 127 : le test tombait exactement dans le piege qu'il documente — il privait l'interpreteur du check du chemin de sa propre libpython. C'est la course sur la vraie plateforme qui l'a montre, pas la lecture. Un test de « la variable est reconduite » doit etendre ce qui est herite ; remplacer teste autre chose, et faux.

Falsification : le test neuf, joue contre l'organe d'origine dans un arbre jetable, ECHOUE (<none>) ; il passe au head. Le correctif est donc bien ce qui le fait basculer.

Portee

Aucune suppression : +52 / -0. Le contrat de verdict (NO_FAULT / HELD / ESCAPED / USAGE / TIMEOUT) est inchange ; les 18 tests existants passent dans les quatre combinaisons ci-dessus. Le sandbox ne s'ouvre pas d'un cran : LD_LIBRARY_PATH vient de l'env du runner, il n'est pas influence par la PR sous test — la valeur que le check pourrait detourner reste hors de sa portee, exactement comme pour PATH.

Ce que je ne prouve pas

Que le rouge CI disparaisse : c'est probable (le maillon 4 est le mode d'echec exact que ce correctif supprime), mais la confirmation est le prochain run Scripts Tests (CPU) sur un slot po-2026, et je ne l'ai pas encore. Si ce job redevient rouge avec exit=127, c'est que le defaut a une autre source que je n'ai pas vue — et je le dirai ainsi.

Lien de famille : #17414 traite la meme famille de faux rouges (un outil attendu du runner mais absent/non declare) cote workflows. Les deux sont independants : celui-ci est dans un organe du depot, l'autre dans cinq workflows.

Note de scission

Ce commit a d'abord ete pousse par erreur sur la branche de #17414 (qui traite les cinq workflows) : la branche portait alors deux sujets. Il en a ete retire par un revert — pas de reecriture d'historique, la branche etant partagee — et cette branche-ci le reprend depuis origin/main. Le diff de #17414 est redevenu celui de son seul sujet, et celui-ci est celui du sien.

🤖 Generated with Claude Code

…untlet

run_check construisait un env minimal qui omettait LD_LIBRARY_PATH. Le
commentaire d'origine justifie de garder PATH « pour que le check trouve
ses binaires » : le raisonnement s'arrete un cran trop tot, car PATH
permet de trouver le binaire et LD_LIBRARY_PATH de le charger — et le
loader tourne avant la premiere instruction du check.

Sur un runner dont l'interpreteur est un build setup-python sans rpath
(libpython3.11.so.1.0 vit dans .../x64/lib, trouve uniquement par cette
variable — c'est pourquoi le runner l'exporte dans l'env du job), le
check ne demarrait pas : le gauntlet lisait exit 127 avec stdout vide et
rendait BASELINE_FAILED, verdict indiscernable de « le garde n'a rien
detecte ». Un check qui ne demarre pas et un check qui ne detecte rien
produisaient le meme signal : le garde accusait sa cible d'un defaut
d'environnement.

Reproduit sur la vraie plateforme, sans toucher au tool-cache (pytest
rendu importable par PYTHONPATH depuis un venv du python systeme, tests
joues sous l'interpreteur du tool-cache, comme le runner le fait) :
8 failed / 8 passed avant, 17 passed / 2 skipped / 0 failed apres. Vert
sur Windows dans les deux etats — d'ou la longevite du defaut, invisible
a tout poste de developpement.

Test de regression ajoute, falsifie contre l'organe d'origine (ECHOUE).
Il etend la valeur heritee au lieu de la remplacer : ma premiere version
l'ecrasait par un chemin factice et tombait en 127 sur le runner, soit
exactement le piege qu'elle documente.

+44 / -0 : aucune suppression, contrat de verdict inchange, les 18 tests
existants passent dans les quatre combinaisons.

Co-Authored-By: Claude-Code <noreply@anthropic.com>
jsboige added a commit that referenced this pull request Sep 22, 2026
…check gauntlet"

Ce commit appartenait a un autre sujet et a ete pousse ici par erreur :
la branche portait deux sujets, ce qui viole un-PR-un-sujet. Le correctif
est repris tel quel, depuis origin/main, sur sa propre branche (PR #17415).

Revert plutot que reecriture d'historique : la branche est partagee et
deja poussee (pas de force push).

Co-Authored-By: Claude-Code <noreply@anthropic.com>
jsboige added a commit that referenced this pull request Sep 22, 2026
…s faux rouges

Section ajoutee a persist/po-2026/README.md, dans la continuite de « ce que
le pool doit fournir » : ce n'est plus un binaire en nom nu mais
l'interpreteur lui-meme, re-telecharge a chaque job.

Mesure firsthand : pool.sh efface le repertoire du slot a chaque respawn
(`rm -rf "$dir"` avant config.sh), RUNNER_TOOL_CACHE est absent du
superviseur (grep -> 0), pool.log porte 383 respawns, aucun stamp
`.complete` n'existe, et le log de job montre `Version 3.12 was not found
in the local cache` suivi d'un telechargement.

Trois faux rouges qui en decoulent, deux deja corriges cote depot :
(1) interpreteur frais nu -> cinq workflows lancaient pytest sans
l'installer, verts ou rouges selon le slot (PR #17414) ; (2) mismatch
d'interpreteur -> le golden-set installle numpy==2.4.4 dans ~/.local et le
kernel ne le voit pas, sur un notebook GameTheory sans rapport avec la PR
(ouvert) ; (3) env minimal de guard_gauntlet sans LD_LIBRARY_PATH -> le
check ne demarre pas, exit 127, BASELINE_FAILED indiscernable de « le garde
n'a rien detecte » (PR #17415).

`BASELINE_FAILED` avec check_exit 127 a donc desormais trois causes
documentees : le tableau de diagnostic de la section precedente n'en
couvrait qu'une seule.

L'option de correction (RUNNER_TOOL_CACHE partage, hors du repertoire
efface) n'est pas posee : elle exige un redemarrage du pool, qui tue les
jobs en vol des autres lanes. Effet de bord a annoncer, pas a glisser dans
une PR de documentation.

Co-Authored-By: Claude-Code <noreply@anthropic.com>
@github-actions github-actions Bot added the trivial-diff-advisory Diff trivial : grain META mecanique sans fournee ni exception ecrite (#15740) label Sep 22, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Trivial-diff advisory (#15740, non bloquant).
genre guard dans la famille META (docs/guard/ledger/readme/test) + diff de 52 lignes changees (<= 100) + aucune exception ecrite dans le body : le litmus de la trivialite (une douzaine d'instances scannees a la suite) est credible. Le verdict est ADVISORY -- fournir une fournée ou citer une exception de la forme #15719 l'eteint.
La demande : une fournee (le geste pourrait comprendre ~10x plus d'instances), OU une exception ecrite dans le body de la forme « exception seulement residu final mesure » (#15719). Editer le body re-deroule cet organe et retire le label.

@clusterManager-Myia clusterManager-Myia left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VERDICT: LGTM

[Hermes] po-2026 — APPROVE, head d5eb42ae. Chaine causale re-mesurée firsthand depuis ce siège (organes base/main + head extraits, exécution réelle en CLI) :

1. Falsification du test neuf réussie. J'ai rejoué le scénario du body : LD_LIBRARY_PATH posée dans l'env parent (:/opt/coursia-toolcache/lib), check témoin imprimant os.environ.get('LD_LIBRARY_PATH','<none>'), invocation des deux organes en CLI identique. Résultat mesuré : organe base → <none> (DROPPED), organe head → :/opt/coursia-toolcache/lib (CARRIES). Le diff de comportement est exactement celui que le test neuf garde — la régression est bien piégée.

2. Maillon 3 vérifié : LD_LIBRARY_PATH absente de la base (0 occurrence dans guard_gauntlet.py@main, 0 hit search/code hors les 2 fichiers du PR). Diff = 1 clé d'env + commentaire + 1 test additif (+52/−0), rien d'autre.

3. Le test neuf étend la valeur héritée au lieu de l'écraser (os.environ.get(...) + ":/opt/..."), avec le commentaire documentant pourquoi — c'est le bon geste : un test qui écraserait la variable échouerait en 127 sur le runner tout en passant en local.

4. Security scan : 0 match.

Nuance (non bloquante) : la paire témoin 8 failed/8 passed → 17 passed du body repose sur l'interpréteur du tool-cache, que ce siège n'a pas — je n'ai pas re-mesuré ce maillon-là, mais il est le constat (CI), pas le correctif ; le correctif lui est falsifié ci-dessus. PR gate rouge au head = [pr-gate] DWELL (plancher 120 min, minuteur, pas un défaut de code).

@jsboige

jsboige commented Sep 22, 2026

Copy link
Copy Markdown
Owner Author

[ADJOINT PREFLIGHT]
schema: 1
lane: myia-po-2025:CoursIA-2
pr: 17415
head: d5eb42a
complete: true
body: read
comments-reviewed: 1
reviews-reviewed: 1
threads-reviewed: 0
threads-unresolved: 0
surfaces-sha256: 50c1d40bb9d7297c2b25f3e2dbdf7641e6eb264a973fa949dca5a58ec3574828
diff-files: 2
diff-additions: 52
diff-deletions: 0
checks: latest-wins-green
b0: clear
scope: pass
domain: pass
verdict: READY
[/ADJOINT PREFLIGHT]

@jsboige

jsboige commented Sep 22, 2026

Copy link
Copy Markdown
Owner Author

[RIPE-SIGNAL] PR #17415 (lane myia-po-2026:CoursIA-2) — Tell c.15726 strict 0 spam respecté.

État au head courant d5eb42ae (vérifié first-hand gh pr view 17415 --json statusCheckRollup, 2026-09-22T20:48Z) :

Impact main (Tell c.974 strict mesure) :

Tell respectés :

  • c.15726 strict 0 spam — pas de ripe-signal c.1157 sur cette PR (premier)
  • c.14216 strict wait — fenêtre 4-24h, premier signalement à 20:48Z
  • c.594 strict — PR lane po-2026 = ma lane, ripe-signal légitime
  • c.594 strict fondateur — pas de push sur branche d'autrui (pas d'action ici, juste signalement)

Geste attendu ai-01 : absorption #17415 (squash-merge, branche conservée Tell c.1502 strict pas --delete-branch).

— lane myia-po-2026:CoursIA-2, c.1158

@jsboige

jsboige commented Sep 22, 2026

Copy link
Copy Markdown
Owner Author

[ADJOINT PREFLIGHT]
schema: 1
lane: myia-po-2025:CoursIA-2
pr: 17415
head: d5eb42a
complete: true
body: read
comments-reviewed: 3
reviews-reviewed: 1
threads-reviewed: 0
threads-unresolved: 0
surfaces-sha256: 6de874ef70f084b6830c007301cb84641e2967fc65c0855e9813b0caa99feac1
diff-files: 2
diff-additions: 52
diff-deletions: 0
checks: latest-wins-green
b0: clear
scope: pass
domain: pass
verdict: READY
[/ADJOINT PREFLIGHT]

@myia-ai-01
myia-ai-01 merged commit 8683458 into main Sep 22, 2026
17 of 18 checks passed
myia-ai-01 pushed a commit that referenced this pull request Sep 23, 2026
…eclarer (#17414)

* fix(ci): installer pytest dans les 5 gardes qui l'invoquent sans le declarer

Cinq workflows lancent `python -m pytest` sans jamais installer pytest :
les quatre ratchets (exec-sequence, output-collapse, output-failure,
output-flood) et translation-hot-drift-advisory. Or `actions/setup-python`
ne fournit pas pytest, et le provisionnement documente du tool-cache
(docs/ci/self-hosted-runners.md, option a2) copie un CPython nu python.org
dont le temoin d'acceptation est `python --version` + `pip --version`,
jamais pytest.

Consequence mesuree : la garde est verte ou rouge selon le SLOT qui prend
le job, pas selon le contenu de la PR. Le run ratchet de #17410 a echoue
sur myia-po-2026-wsl-5 avec `No module named pytest` (exit 1) alors que le
meme workflow concluait success sur myia-ai-01-wsl-1 pour la branche
#17413 ; un run d'une branche sans rapport (feature/math-delims-detector,
35714856271) est tombe sur le meme slot avec la meme signature.

Le rouge accuse donc le contenu a tort — classe `exit-127` (outil absent
du runner), ou l'organe impute au PR ce qui manque a l'environnement.

Correctif : une etape `python -m pip install --quiet pytest`, idiome
majoritaire du depot (27 des 32 workflows qui invoquent pytest installent
deja leurs deps). 45 insertions, 0 suppression : aucune etape existante
n'est touchee, les self-tests restent en tete.

Paire temoin deterministe sur un interpreteur nu (Python 3.11.9 + pip 24.0,
le temoin exact du doc) : etat base, les 5 self-tests passent (stdlib seul)
et les 5 invocations pytest echouent rc=1 ; etat head, 97 tests passent
(43+18+19+10+7), pytest seul suffisant — aucune dependance tierce cachee.

Co-Authored-By: Claude-Code <noreply@anthropic.com>

* fix(guard): reconduire LD_LIBRARY_PATH dans l'env minimal du check gauntlet

run_check construisait un env minimal qui omettait LD_LIBRARY_PATH. Le
commentaire d'origine justifie de garder PATH « pour que le check trouve
ses binaires » : le raisonnement s'arrete un cran trop tot, car PATH
permet de trouver le binaire et LD_LIBRARY_PATH de le charger — et le
loader tourne avant la premiere instruction du check.

Sur un runner dont l'interpreteur est un build setup-python sans rpath
(libpython3.11.so.1.0 vit dans .../x64/lib, trouve uniquement par cette
variable — c'est pourquoi le runner l'exporte dans l'env du job), le
check ne demarrait pas : le gauntlet lisait exit 127 avec stdout vide et
rendait BASELINE_FAILED, verdict indiscernable de « le garde n'a rien
detecte ». Un check qui ne demarre pas et un check qui ne detecte rien
produisaient le meme signal : le garde accusait sa cible d'un defaut
d'environnement.

Reproduit sur la vraie plateforme, sans toucher au tool-cache (pytest
rendu importable par PYTHONPATH depuis un venv du python systeme, tests
joues sous l'interpreteur du tool-cache, comme le runner le fait) :
8 failed / 8 passed avant, 17 passed / 2 skipped / 0 failed apres. Vert
sur Windows dans les deux etats — d'ou la longevite du defaut, invisible
a tout poste de developpement.

Test de regression ajoute, falsifie contre l'organe d'origine (ECHOUE).
Il etend la valeur heritee au lieu de la remplacer : ma premiere version
l'ecrasait par un chemin factice et tombait en 127 sur le runner, soit
exactement le piege qu'elle documente.

+44 / -0 : aucune suppression, contrat de verdict inchange, les 18 tests
existants passent dans les quatre combinaisons.

Co-Authored-By: Claude-Code <noreply@anthropic.com>

* Revert "fix(guard): reconduire LD_LIBRARY_PATH dans l'env minimal du check gauntlet"

Ce commit appartenait a un autre sujet et a ete pousse ici par erreur :
la branche portait deux sujets, ce qui viole un-PR-un-sujet. Le correctif
est repris tel quel, depuis origin/main, sur sa propre branche (PR #17415).

Revert plutot que reecriture d'historique : la branche est partagee et
deja poussee (pas de force push).

Co-Authored-By: Claude-Code <noreply@anthropic.com>

---------

Co-authored-by: Claude-Code <noreply@anthropic.com>
jsboige added a commit that referenced this pull request Sep 23, 2026
…tick

Le binaire python3.11 exporte de l'image Docker porte RUNPATH=/opt/hostedtoolcache/
Python/3.11.16/x64/lib (chemin Docker absent en WSL natif) : sous un env minimal
(gauntlet, bases anterieures a #17415) le loader meurt en exit 127 stdout vide sur
libpython3.11.so.1.0. #17415 corrigeait le symptome cote job ; la cause est le
RUNPATH inexact du binaire (ASK secretary c.37, 2026-09-23).

- repatch_toolcache_pythons() : patchelf --set-rpath '$ORIGIN/../lib' sur tout
  python3.11 du toolcache dont le RUNPATH pointe encore /opt/hostedtoolcache
  (idempotent, 8 readelf par 30s) ; une extraction setup-python perd le patch,
  le pool le re-applique au boot et a chaque tick.
- ensure_host_contract crie si patchelf est invisible (pip --user).
- Deploye atomiquement (mv) dans WSL ; le superviseur en cours tient l'ancien
  inode, effectif au prochain demarrage du pool — les 8 slots courants sont
  deja patche's a la main (verification env -i 8/8 START-OK).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
myia-ai-01 pushed a commit that referenced this pull request Sep 24, 2026
…slots, scope CPU/memoire) (#17406)

* feat(ci,#16646): livrer persist/po-2026/ -- pool de runners borne (8 slots, scope CPU/memoire)

Le pool coursia-linux de po-2026 n'etait borne par RIEN : aucune unite ni scope
systemd (cgroup /init.scope mesure sur le superviseur vivant), et pas de daemon
Docker (le binaire /usr/bin/docker est la, le socket non). Aucun des quatre leviers
que persist/README.md decrit pour le parc dockerise ne s'y appliquait.

Cette PR enregistre la chaine vivante dans persist/po-2026/ et documente la borne
livree : pool.sh en POOL_SIZE=8, lanceur qui demarre le superviseur dans un scope
systemd utilisateur borne (CPUQuota=1400% = 14 des 20 vCPU, MemoryMax=20G).

Piege mesure, et c'est le vrai contenu de la PR : la commande passee a wsl.exe ne
doit contenir AUCUN $. Git Bash les mange avant que WSL ne les voie -- la premiere
forme du lanceur arrivait en --unit= vide, et systemd-run echouait sur
"Failed to mangle scope name: Invalid argument" sans jamais atteindre pool.sh.
Le defaut n'etait donc pas seulement l'absence de borne : la tache planifiee ne
relancait plus le pool du tout, et un reboot aurait laisse po-2026 a zero slot.
Les deux oracles (ligne du jour dans pool.log, cgroup du processus) sont documentes.

See #14329, See #16646.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* docs(ci,#16646): corriger le chemin de cgroup par la mesure, retirer un claim non verifie

Le chemin de cgroup d'un scope utilisateur a ete ecrit de memoire dans la premiere
redaction ; mesure sur une sonde de meme forme il vaut
0::/user.slice/user-1000.slice/user@1000.service/app.slice/<nom>.scope.
La meme mesure invalide un claim pose plus loin (« list-units peut rendre 0 unite
alors qu'un scope transitoire est actif ») : le scope nomme EST liste tant qu'il vit.
La formulation est remplacee par ce qui est mesure.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* docs(ci,#16646): premier echantillon memoire du pool borne, et le piege de lecture du MemoryPeak

Mesure dans l'heure suivant la montee a 8 slots, 6 jobs concurrents :
MemoryPeak 18,98 Gio pour un cap a 20 Gio -- ce qui se lit « borne saturee » et
est faux. memory.stat donne anon 0,73 Gio, file 15,18 Gio (dont inactive_file
14,13, donc reclamable), et memory.events rend max 0 / oom 0 / oom_kill 0 : le cap
n'a jamais mordu. La proximite apparente mesure le cache de pages, pas une pression.

Ce que ca ne dit pas : l'echantillon porte sur la phase checkout/test et un seul
pic, pas sur le job le plus lourd du depot. Le cap reste un backstop choisi haut.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* docs(ci,#16646): les deux binaires que le pool doit fournir, et le piege de la sonde

Le pool de po-2026 a produit des faux rouges pour TOUTES les lanes pendant que
deux binaires appeles en nom nu manquaient du PATH des jobs :

- `gh` (22/09) : FileNotFoundError 'gh', check_exit 127 -> BASELINE_FAILED, et un
  garde de perimetre qui sort en fail-loud sans citer de contradiction reelle ;
- `python` (22/09) : `line 1: python: command not found`, exit 127 -- le rendu
  Quarto appelle `python scripts/regen_quarto_render.py` alors qu'Ubuntu ne
  fournit que `python3`.

Les deux sont repares dans ~/.local/bin (aucun sudo) : release Linux de gh 2.90.0,
et lien `python -> /usr/bin/python3` (3.12.3). Le PATH du process runner contient
`/home/jesse/.local/bin` -- verifie dans `/proc/<pid>/environ`, pas deduit.

Le piege de la sonde, mesure : `command -v python` depuis un shell WSL ordinaire
rend ABSENT avant COMME APRES la pose du lien, parce que ce shell n'a pas
~/.local/bin dans son PATH ; seul un PATH adopte du runner conclut juste. La
section donne donc la sonde qui compte.

Ce que ca coute de ne pas le savoir : un rebuild du pool perd les deux binaires
(la reconstruction du 21/09 n'a reproduit que pool.sh et le lanceur -- c'est
exactement l'origine des deux pannes), et un rouge sur ces checks n'atteste rien
du contenu de la PR. La sequence de deploiement gagne l'etape 2b qui les repose.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* docs(pool): la destruction du tool-cache a chaque respawn et ses trois faux rouges

Section ajoutee a persist/po-2026/README.md, dans la continuite de « ce que
le pool doit fournir » : ce n'est plus un binaire en nom nu mais
l'interpreteur lui-meme, re-telecharge a chaque job.

Mesure firsthand : pool.sh efface le repertoire du slot a chaque respawn
(`rm -rf "$dir"` avant config.sh), RUNNER_TOOL_CACHE est absent du
superviseur (grep -> 0), pool.log porte 383 respawns, aucun stamp
`.complete` n'existe, et le log de job montre `Version 3.12 was not found
in the local cache` suivi d'un telechargement.

Trois faux rouges qui en decoulent, deux deja corriges cote depot :
(1) interpreteur frais nu -> cinq workflows lancaient pytest sans
l'installer, verts ou rouges selon le slot (PR #17414) ; (2) mismatch
d'interpreteur -> le golden-set installle numpy==2.4.4 dans ~/.local et le
kernel ne le voit pas, sur un notebook GameTheory sans rapport avec la PR
(ouvert) ; (3) env minimal de guard_gauntlet sans LD_LIBRARY_PATH -> le
check ne demarre pas, exit 127, BASELINE_FAILED indiscernable de « le garde
n'a rien detecte » (PR #17415).

`BASELINE_FAILED` avec check_exit 127 a donc desormais trois causes
documentees : le tableau de diagnostic de la section precedente n'en
couvrait qu'une seule.

L'option de correction (RUNNER_TOOL_CACHE partage, hors du repertoire
efface) n'est pas posee : elle exige un redemarrage du pool, qui tue les
jobs en vol des autres lanes. Effet de bord a annoncer, pas a glisser dans
une PR de documentation.

Co-Authored-By: Claude-Code <noreply@anthropic.com>

* fix(pool): poser le contrat d'image que les slots natifs ne peuvent pas heriter

Deux manques mesures sur les slots natifs po-2026 (hors image), par le contrat que le
Dockerfile pose et que la table du README ne couvrait pas :

- `PIP_BREAK_SYSTEM_PACKAGES=1` (Dockerfile l.14-21 + ENV l.40) : le python systeme de
  l'hote est marque EXTERNALLY-MANAGED et `pip install` est refuse par PEP 668 --
  mesure : `/usr/lib/python3.12/EXTERNALLY-MANAGED` present, `--dry-run pyyaml` refuse.
  Un slot natif n'herite d'aucun ENV d'image : le pool le pose lui-meme.
- `ensure_host_contract()` : cree le lien `python` -> python3 (idempotent, local, aucun
  reseau) et CRIE si `gh` manque, sans jamais le telecharger (l'image l'epingle par
  SHA-256 : un telechargement non verifie serait un maillon de supply chain pour rien).
  Non bloquant a dessein : couper le pool pour un defaut qui produit surtout des verts
  suspects priverait la flotte de capacite.

README : le contrat est desormais lu dans le Dockerfile (6 items) et non deduit des
pannes observees, avec l'etat mesure de chacun. Le `gh` du slot est en 2.90.0 la ou
l'image epingle 2.99.0 -- divergence nommee, non corrigee, et invisible a une sonde
`command -v`.

Copies vivantes (WSL et Windows) remplacees par `mv` atomique, sauvegardes conservees :
le superviseur tient l'ancien inode via fd 255 (`/proc/431/fd/255 -> pool.sh (deleted)`),
il n'est pas re-execute et les jobs en vol ne sont pas coupes. Le correctif prend effet
au prochain demarrage du pool, pas avant.

See #17407
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>

* fix(pool): Q5 toolcache par slot + Q6 PATH en tete + chmod defensif du lanceur

- TOOLCACHE_BASE sous $BASE, hors arbre ephemere (#17407/Q5) : setup-python
  ne retelecharge plus un CPython nu sous slot-N/_work/_tool/ que spawn_slot
  detruit au job suivant (exit 127 muet, stdout vide). Isolation par slot :
  slot-N/toolcache survit au rm -rf et un slot n'est jamais concurrent
  avec lui-meme.
- export PATH="$HOME/.local/bin:$PATH" en tete (Q6, 2026-09-23, mesure
  /proc/<pid>/environ : une relance depuis session interactive heritait un
  PATH sans ~/.local/bin, gh invisible des listeners) + garde command -v gh
  dans ensure_host_contract (existence du fichier ne suffit pas).
- lanceur : chmod +x defensif avant systemd-run — un depot par os.replace
  fait naitre pool.sh en 0644, systemd-run refuse (Permission denied) et
  l'erreur est perdue dans la tache planifiee (2 declenchements 2026-09-23).

Scripts byte-identiques aux copies vivantes (WSL vivante, C:\dev fige par la
tache planifiee), sha256 re-verifie a l'ecriture.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(pool): re-patch RUNPATH du CPython toolcache au boot et a chaque tick

Le binaire python3.11 exporte de l'image Docker porte RUNPATH=/opt/hostedtoolcache/
Python/3.11.16/x64/lib (chemin Docker absent en WSL natif) : sous un env minimal
(gauntlet, bases anterieures a #17415) le loader meurt en exit 127 stdout vide sur
libpython3.11.so.1.0. #17415 corrigeait le symptome cote job ; la cause est le
RUNPATH inexact du binaire (ASK secretary c.37, 2026-09-23).

- repatch_toolcache_pythons() : patchelf --set-rpath '$ORIGIN/../lib' sur tout
  python3.11 du toolcache dont le RUNPATH pointe encore /opt/hostedtoolcache
  (idempotent, 8 readelf par 30s) ; une extraction setup-python perd le patch,
  le pool le re-applique au boot et a chaque tick.
- ensure_host_contract crie si patchelf est invisible (pip --user).
- Deploye atomiquement (mv) dans WSL ; le superviseur en cours tient l'ancien
  inode, effectif au prochain demarrage du pool — les 8 slots courants sont
  deja patche's a la main (verification env -i 8/8 START-OK).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

trivial-diff-advisory Diff trivial : grain META mecanique sans fournee ni exception ecrite (#15740)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants