Skip to content

feat(doctor): diagnose an unsupported Claude plugin/runtime pairing (#729, slice 3) - #916

Merged
github-actions[bot] merged 2 commits into
mainfrom
fix/729-plugin-runtime-pairing
Nov 6, 2026
Merged

github-actions[bot] merged 2 commits into
mainfrom
fix/729-plugin-runtime-pairing

Conversation

@cdeust

@cdeust cdeust commented Nov 6, 2026 •

Copy link
Copy Markdown
Owner

Summary

Refs #729 (tranche 3 : « définir les paires plugin/runtime supportées et diagnostiquer une paire non supportée », tranche restante n° 2 du commentaire du 2026-11-06). Tranches restantes listées sur l'issue.

Cause racine. Le plugin Claude n'épingle pas de runtime par version : scripts/launcher.py (_resolve_paths, _set_import_paths) place $CLAUDE_PLUGIN_ROOT en tête de sys.path et de PYTHONPATH, donc le code exécuté est le paquet mcp_server de ce répertoire. Rien ne vérifiait que le processus importe bien ce paquet-là, ni que .claude-plugin/plugin.json déclare la version du code qu'il lance. Un correctif publié dans la version du plugin ne pouvait donc pas être attribué au code en cours d'exécution. Côté Codex, runtime.py installed_python vérifie déjà la paire au lancement.

Correctif. Nouveau check doctor optionnel Plugin/runtime pairing (mcp_server/doctor_pairing.py), présent dans les deux listes (PostgreSQL et SQLite) :

  • sans CLAUDE_PLUGIN_ROOT (checkout, runtime Codex, .mcpb) : il passe et dit qu'aucun plugin n'a lancé le processus. Il ne devine pas de répertoire de plugin ;
  • mcp_server importé hors de $CLAUDE_PLUGIN_ROOT/mcp_server : échec, avec le chemin réel et la commande via le launcher ;
  • version du manifeste ≠ version du code (roue installée qui localise ce paquet, sinon pyproject.toml [project] version) : échec ;
  • manifeste ou pyproject illisible ou sans version : échec qui nomme la cause.

Type of change

  • New feature (non-breaking change which adds functionality)

Test plan

  • pytest tests_py/test_doctor_pairing.py : 19 passés. Le checkout lui-même est testé comme répertoire de plugin (manifeste 4.23.5 = pyproject 4.23.5).
  • pytest tests_py/test_doctor*.py tests_py/scripts --ignore=tests_py/scripts/test_mutation_isolation.py : 2014 passés, 18 ignorés (avant le commit de tests ; test_mutation_isolation exige mutmut, absent au premier passage). Après le second commit : pytest tests_py/test_doctor*.py 140 passés, 3 ignorés.
  • Porte de mutation ADR-1122 en local (GITHUB_BASE_REF=main python scripts/ci_mutation_diff.py) : 2 file(s), 0 failed, 0 untested, 0 survivant. Le premier passage en avait laissé 33 (nom, optional et fix non vérifiés, version vide ou non textuelle, [project] qui n'est pas une table) ; le second commit les tue tous.
  • Exécution réelle : CLAUDE_PLUGIN_ROOT=$PWD → True Claude plugin 4.23.5 runs its own mcp_server 4.23.5 (…/pyproject.toml) ; CLAUDE_PLUGIN_ROOT=/tmp → échec cannot read the Claude plugin pairing: [Errno 2] … /tmp/.claude-plugin/plugin.json.
  • ruff check et ruff format --check (0.17.3) sur les fichiers modifiés ; python scripts/check_craftsmanship.py : OK.
  • Non vérifié : un vrai plugin Claude installé (marketplace) lançant le doctor via le launcher ; la suite complète (torch refusé par le proxy, d'où --no-install-package torch). Windows et macOS sont laissés à la CI.

Audit notes

  • Revue : routine autopilot uniquement.
  • Le check ne lit que le côté Claude. Le côté Codex reste vérifié au lancement par runtime.py ; le doctor exécuté dans le runtime Codex ne connaît pas le chemin du manifeste Codex, donc il n'est pas comparé ici.

Coding-standards compliance

  • Couches : doctor_pairing.py n'importe que la stdlib, mcp_server et deux constantes de doctor_artifact.
  • 300 lignes par fichier et 40 par méthode : nouveau module (127 lignes) ; doctor.py, déjà en baseline, ne gagne que le câblage (8 lignes).
  • Chaque constante porte son # source:.

https://claude.ai/code/session_018kyZYs18rVW23Ta85PHji4


Generated by Claude Code

…729, slice 3)

The Claude plugin runs the mcp_server package of its own directory
(scripts/launcher.py puts CLAUDE_PLUGIN_ROOT first on the import path), so a
supported pairing is: mcp_server imported from $CLAUDE_PLUGIN_ROOT/mcp_server,
and .claude-plugin/plugin.json declaring that code's version (installed
wheel, else pyproject.toml). The new optional doctor check fails on either
mismatch and names it; without CLAUDE_PLUGIN_ROOT it passes and says no
plugin launched the process. The Codex runtime already checks its pairing at
launch (runtime.py installed_python).

Refs #729

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_018kyZYs18rVW23Ta85PHji4
The ADR-1122 diff mutation gate left 33 survivors in doctor_pairing: the
tests checked neither the check's name, optional flag nor fix text, nor a
version that is an empty string or not a string, nor a [project] that is not
a table. The tests now pin each. The manifest is read as bytes (json decodes
UTF-8 itself, RFC 8259 section 8.1) and pyproject through tomllib.load on a
binary file, which removes the encoding argument whose mutants no test can
tell apart.

Refs #729

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_018kyZYs18rVW23Ta85PHji4
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 6, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-06T18:46:23.652361Z edce2d7 PR opened
🔒 Security Review ✅ Completed 2026-11-06T18:49:57.055310Z edce2d7 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@claude

claude Bot commented Nov 6, 2026 •

Copy link
Copy Markdown

Claude finished @cdeust's task in 2m 24s —— View job


PR review #916 (review 1 of 3)

  • Gather the diff and context
  • Run the five reviews (code quality, security, performance, tests, docs)
  • Check findings against the ADR-1119 Blocker definition
  • Post the verdict: review comment

I found no Blockers. There are three Notes, none needing action before merge:

  • An uncaught RuntimeError or RecursionError aborts the whole doctor.
  • One test is missing: an installed dist outside the plugin directory.
  • The two doctor checks can report different version sources for the same code.

I did not run the tests: the sandbox blocked fetching origin, so I diffed against HEAD~2.

Verdict: approve

@claude

claude Bot commented Nov 6, 2026

Copy link
Copy Markdown

Review 1 of 3 (ADR-1119), commit edce2d7

I found no Blockers. The five reviews (code quality, security, performance, test coverage, docs accuracy) traced the following:

  • Each Check(...) call matches the constructor (doctor.py:63-70).
  • The new module imports only the stdlib, mcp_server and doctor_artifact, so the layer rules hold. It is 128 lines and no method is longer than 40.
  • The wheel → pyproject.toml fallback is not silent: the output names which source it read.
  • The new check is inserted at index 2, so test_doctor_recorded_failures_685, which expects CHECKS[-1] to be _recorded_failures, still holds. No other test counts the check lists.
  • The docstring's claims about launcher.py, Codex runtime.py installed_python and .claude-plugin/plugin.json are accurate.

Notes, no action before merge

  • doctor_pairing.py:103-104: on Python 3.11 and 3.12, a symlink loop in CLAUDE_PLUGIN_ROOT makes Path.resolve() raise RuntimeError. A deeply nested manifest or pyproject raises RecursionError. Neither is caught by except (OSError, ValueError), and _run_full_check runs checks unguarded, so the whole doctor aborts instead of this optional check failing.
  • doctor_pairing.py:77: no test covers an installed dist located outside the plugin, so mutating == root to True survives the unit tests (in CI the editable dist's version equals the pyproject version).
  • doctor_artifact.py:196 and doctor_pairing.py:77-85: when the dist does not own root, "Runtime artifact identity" reports the dist's version while "Plugin/runtime pairing" reads pyproject.toml, so one doctor run can show two versions for the same code.

Verdict: approve

cdeust commented Nov 6, 2026

Copy link
Copy Markdown
Owner Author

Réponse aux trois notes de la revue 1 (aucune n'est bloquante, donc pas de push pour elles seules) :

  1. RuntimeError / RecursionError non attrapés : c'est exact pour une boucle de liens symboliques dans CLAUDE_PLUGIN_ROOT sous Python 3.11 et 3.12. doctor_artifact.artifact_identity a la même garde (OSError, ValueError). Le correctif ajoutera RuntimeError dans les deux modules, avec un test de boucle de liens, et partira avec le prochain push de code sur cette PR.
  2. Distribution installée hors du plugin : la porte de mutation locale (ci_mutation_diff.py) a tué ce mutant. Le test test_this_checkout_is_a_supported_pairing passe par une install éditable dont locate_file ne désigne pas la racine. Le test explicite d'une roue hors du plugin partira avec le même push.
  3. Deux sources de version dans un même passage du doctor : c'est voulu. « Runtime artifact identity » nomme la distribution installée, et « Plugin/runtime pairing » lit ce que le plugin exécute réellement. Chaque check nomme sa source dans son détail. Je n'y touche pas.

Generated by Claude Code

@github-actions
github-actions Bot merged commit 1ab2427 into main Nov 6, 2026
18 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants