Skip to content

docs(plan): version pinning for self-hosted-backed MCP clients (see Consiliency/pmcp#294) - #295

Open
ViperJuice wants to merge 22 commits into
mainfrom
plan/294-version-pin
Open

ViperJuice wants to merge 22 commits into
mainfrom
plan/294-version-pin

Conversation

@ViperJuice

@ViperJuice ViperJuice commented Sep 26, 2026 •

Copy link
Copy Markdown
Collaborator

Detailed plan for #294 proposals 1 and 2. Proposals 3 (post-update smoke probe) and 4 (client version in error hints) are sequenced as follow-up slices, with a design note each. Plan-only PR: the plan embeds a measured reference patch, and implementation is a separate PR after this one passes review.

What it plans

  • A version: field on manifest entries, plus an overlay map server_version: {firecrawl: "3.25.5"} that pins a built-in entry without restating its install matrix. It follows server_env:'s rules: it can't create a server, and a project overlay only applies after trust-store approval.
    • The pin must be an exact SemVer version (the existing is_valid_package_version). Ranges and dist-tags are refused, because npx re-resolves them on every spawn.
    • The pin is written into the entry's own npx package slot, so an overlay can choose a version but never a different package. It's all or nothing across the spawning argvs.
    • Non-npx installers are refused with a clear error.
  • pmcp update / gateway.update_server report "pinned at X, newer available: Y" (pinned_version, latest_available, latest_comparison) instead of failing.
  • An advisory warning in gateway.health and update_server when an entry's self-hosted relaxer (api_key_optional_when) is active in its real environment and the launching argv is unpinned (@latest counts as unpinned).

Measured on a reverted spike at 9ca081e

  • tests/test_version_pin.py: 37 new tests. On main, 36 fail for the stated reason; the one that passes asserts existing behaviour. All 37 pass with the patch.
  • 14 of 14 mutants fail a named test.
  • Full suite: 4108 passed. ruff check, ruff format --check and mypy are clean.
  • The spike caught a pinned-path test that silently reached the real npm registry. The plan stubs it and adds a no-network verification step.

Open questions for the maintainer (the plan picks a conservative default for each)

  1. Ship a pin on the built-in firecrawl entry? Default: no, because it would pin vendor-hosted users too.
  2. Should a partial servers: entry that carries only version: merge into the built-in entry? Default: no. server_version: does that job instead, and switching servers: from replace to merge would change the meaning of every existing overlay.
  3. Should pmcp update --all exit nonzero on failure? Default: unchanged.
  4. Should the warning fire for servers that exist only in .pmcp.json? Default: no, because nothing there declares a self-hostable backend.
  5. Treat context7's Windows cmd /c npx as npx? Default: out of scope.

See #294.

🤖 Generated with Claude Code

https://claude.ai/code/session_017HCiJU6dVVrNdcz461oN33


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

ViperJuice and others added 22 commits September 26, 2026 02:57
)

Detailed plan for proposals 1 and 2 of #294:
- a first-class `version:` on manifest entries, plus an overlay
  `server_version:` map (mirroring `server_env:`) that pins a built-in
  entry without restating its install matrix;
- `gateway.update_server` / `pmcp update` reporting "pinned at X, newer
  available: Y";
- an advisory warning in gateway.health and update_server when an entry's
  api_key_optional_when relaxer is active and its client is unpinned.
Proposals 3 (smoke probe) and 4 (version in error hints) are sequenced as
follow-up slices, each with a design note.

Design:
- Grammar: exactly one SemVer version (is_valid_package_version). Ranges
  and dist-tags are refused because they re-resolve at every spawn.
- Materialisation: one post-overlay pass rewrites the npx package slot
  of args AND every install argv. It keeps the entry's own package name,
  so an overlay can pick a version but never a different package.
- A pin that cannot be applied is dropped with a warning (all or
  nothing), and the entry stays honestly unpinned.
- uvx/pip/cargo/docker and remote entries are refused with the
  .mcp.json explicit-args escape hatch.

Measured on a spike at 9ca081e, which was then reverted:
- tests: 37 new, red on HEAD (36 failed / 1 inertness guard passed),
  green with the diff;
- mutants: 14/14 red, each for the named reason;
- full suite: 4108 passed / 0 failed;
- CI gates: ruff check, ruff format --check and mypy src/ all clean.

Two regressions were caught by measurement and fixed in the embedded
diff:
- moving update_server's contract docstring broke a test_tools
  docstring test;
- the pinned branch's new registry read made an existing
  pinned-refusal test go to the network. A no-network plugin found
  exactly one offender, which is now stubbed offline.
77 of 107 shipped entries are pinnable.

Plan-only commit: src/ and tests/ are byte-identical to HEAD.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ve the board round (see #294)

Board round on #295 @ 9d08184 (gemini AGREE, claude
PARTIALLY AGREE, codex DISAGREE x3 blocking, grok timed out). Each
blocking finding was reproduced first, then fixed with a test that is
red on the revision-1 spike and green now, plus a mutant.

- codex P1 / claude N1: an alias slot (myalias@npm:x@1) was rewritten to
  another package. Only a plain registry spec is pinned now: name, or
  name@<exact version | dist-tag>. Aliases, URLs, git, file and ranges
  are refused. The real resolver confirms myalias@3.25.5 -> npm:myalias.
- codex P2 / claude F1: a range or tag silenced the warning and printed
  [PINNED]. Now only an exact pin suppresses the warning, and
  update_server reports floating_selector; pmcp update prints
  [FLOATING]. The refusal itself is unchanged.
- codex P3: one malformed entry aborted the manifest load. Pinning is now
  contained per entry. main: 108 entries; rev1: LOAD FAILED; now: 108.
- claude F2: the advisory relaxer check now reads the environment the
  child actually spawns with (sanitized_subprocess_env). The credential
  gates are unchanged.
- claude F3: health judges the connected configs and caches manifest
  facts by a stat-only fingerprint. Steady-state cost went from 220.7 ms
  to 1.3 ms; WARNING lines over 21 calls went from 42 to 1.
- claude N2: build metadata (+...) is refused.
- gemini: the PyPI == lookup name and the descriptions-cache label are
  tabled explicitly.

Measured:
- tests: 57 (19 new cases red on rev 1); 56 red / 1 guard green on HEAD;
- mutants: 24/24 red;
- full suite: 4128 passed / 0 failed;
- no-network run: 878 passed over 19 files;
- CI gates: ruff check, ruff format --check and mypy src/ clean.

Plan-only commit: src/ and tests/ are byte-identical to HEAD.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ve board round 2 (see #294)

Board round 2 on 95968c8: the claude seat returned DISAGREE with one
blocker. Every item below was reproduced first, then fixed, with a test
that is red on the revision-2 spike and green now, plus a mutant.

- B1 (blocking): a tarball selector or bare tarball slot
  (firecrawl-mcp@corp-mcp.TGZ, corp.tgz) was pinned to the public
  registry package. The fix uses npm's own isFileType rule
  (/[.](?:tgz|tar\.gz|tar)$/i) on the selector and on an unscoped name,
  as npa does, and re-derives the whole allowlist against
  npm-package-arg class by class. The class table is in D3. On a
  461-slot corpus against the host's real npa, the old rule had 136
  violations and the new one has 0. The rule is pure grammar, so pins
  still work while identity is disabled.
- N1: the project-overlay and trust-store fingerprint components now
  have tests (mutants M32, M33).
- N2: when npm identity is disabled, npx slots are read structurally
  with the same grammar, and the warning says so. If even that can't
  decide, it says the pin cannot be verified. Measured with
  npm_config_cache set: rev 2 gave []; rev 3 warns.
- N3: a configured +metadata pin is labelled with what npm runs
  (3.25.5, "build metadata '+evil' is ignored by npm").
- N4: the tag word uses fullmatch, and x/X/v1.2.x range words are
  refused.
- N5: unchanged, as instructed.

Measured:
- tests: 89 (15 of them red on rev 2);
- mutants: 33/33 red (M26 is red on the bare-tarball cases);
- full suite: 4160 passed / 0 failed;
- no-network run: 910 passed;
- neighbouring suites: 1620 passed;
- CI gates: ruff check, ruff format --check and mypy src/ clean.

Plan-only commit: src/ and tests/ are byte-identical to HEAD.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ve board rounds 3-4, re-derived against main (see #294)

- Pin grammar: a version-shaped tarball selector (`name@3.25.5-corp.tgz`)
  or pin value is refused, as npm classifies it; the grammar inherits
  main's exact-version check (npm 10 and 11 tarball rule, core-part bound)
  rather than restating it. Reference patch re-derived against main
  959d4d4 (the validation change is on main).
- Range words with repeated `v`, reserved names, `npm exec` launches with
  identity disabled (fails loud), and the fallback's stated cause.
- Conflict with the derived gateway schemas resolved: update_server
  validates its input model itself.
- Conformance against npm-package-arg 12.0.2 and 13.0.2 (1,617,455 slots,
  41,472 pin values): 0 violations on both. 112 tests; 40/40 mutants.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LT8D8tRmExyQve69DCWRik
… (see #294)

- C1: an exact npm pin is silent only when nothing besides the argv can
  change what npm runs for it: npm_config_* keys judged by an allowlist
  derived from the npm 10.9.9 and 11.19.0 config definitions, NODE_OPTIONS,
  the overlay keys identity selects, a local prefix, and the project, user,
  global and builtin npmrc files, in both identity modes.
- C2: _is_exact_pin decides per launcher; a docker tag floats and only a
  content digest pins, for the warning and for [PINNED]/[FLOATING].
- NB-1: the fallback cause names a launcher identity does not read.
- NB-2: the floating text claims only what holds on every npm release.
- NB-3: the #300 test count is attributed correctly (416 for two files).

Re-measured on 959d4d4: 153 pin tests (41 red on rev 5), 56/56 mutants
red, full suite 4489 passed, conformance 0/0 on npa 12.0.2 and 13.0.2.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LT8D8tRmExyQve69DCWRik
…trust boundary (see #294)

Maintainer decision: trust the host. The warning now judges only what a
manifest entry or overlay controls -- its argv, its launcher and the env
block it injects -- and states the host boundary (npmrc files, shell env,
shims, caches, global bin, proxy/CA, uv/cargo/docker config) as a non-goal
in the plan and in the README text.

- B1: rev 6's host npmrc/local-prefix/env discovery is removed.
- B2: the entry-env rule covers uvx (UV_*/PIP_*) and cargo (CARGO_*/RUSTC*),
  plus uvx/cargo argv flags such as --index-url and --git.
- N1/N3/N4: an entry's npm_config_cache and proxy/CA variables are loud; the
  wording says a pin holds the top-level package.
- N5: a uvx URL requirement is never exact; a malformed docker digest is
  labelled as such; an unreadable docker/uvx argv fails loud.

Measured: 186 pin tests (54 red on rev 6), 60/60 mutants red, full suite
4539 passed on 876fd33, conformance 0/0 on npa 12.0.2 and 13.0.2.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LT8D8tRmExyQve69DCWRik
A cwd that an entry or overlay sets chooses which project configuration the
launcher reads (.npmrc / package.json up the tree, uv.toml / [tool.uv],
.cargo/config.toml), so an exact pin with an entry-set cwd is "cannot
verify" for npm/npx, uvx and cargo. docker is exempt: docker run reads no
cwd-relative configuration and a digest is content-addressed. An inherited
cwd stays the host's. The shipped manifests set no cwd (0 matches), and
shipped coverage is unchanged at 77 pinnable / 30 refused.

Measured on origin/main 876fd33: 190 pin tests (3 red on 1073923), 63/63
mutants red, full suite 4543 passed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LT8D8tRmExyQve69DCWRik
#294)

Board round 6 found that both of rev 7's entry rules were denylists, which
fail open on every spelling they do not list. Rev 8 inverts both:

- ENV: every key the entry injects must be proven inert (the server's own
  declared keys outside tool namespaces, locale/terminal keys, and short
  per-launcher logging/timing/credential lists); unknown keys, XDG_*, LD_*,
  CC and empty values of non-inert keys are "cannot verify" (X2, N1, N4).
- ARGV: an exact pin is silent only in a recognised shape where the pinned
  package is what runs (npx/npm exec, uvx and `uv tool run`, cargo install,
  docker run with nothing after the image); update_server applies the same
  check before [PINNED] (X1, N3).
- Unmodelled runners and wrappers (bunx, pnpx, pnpm/yarn dlx, uv run, node,
  shells, env) are "cannot verify"; README and D7 say a docker digest pins
  the image, not what runs in it (N2).

Shipped cost: 0 of 77 pinnable entries. Measured on origin/main 876fd33:
219 pin tests (24 red on 15dae94), 73/73 mutants red, full suite 4572
passed, conformance 0/0 on npa 12.0.2 and 13.0.2.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LT8D8tRmExyQve69DCWRik
…ons exempt a key (see #294)

Board round 7, both blockers inside the trust boundary:

- B1: gateway.provision spawns install[platform] and adopts it as the live
  server. For a manifest-sourced server every non-empty install argv must
  now run the same exact pin in a recognised shape with an inert env (the
  _config_runs_exactly rule), in the warning and before [PINNED]; the
  message names the argv. Spawn sites are enumerated in the plan.
- B2: a key is the server's own only if pmcp's shipped manifest.yaml
  declares it for that name; overlay/.pmcp.json/.mcp.json declarations
  exempt nothing, and the namespace denylist is removed.
- N1-N3: dependency-scope registry and install argvs documented; docker
  -it and npx --yes=true modelled; remaining parser spellings documented.

Shipped cost: 0 of 77. Measured on origin/main 876fd33: 230 pin tests (11
red on 253445a), 80/80 mutants red, full suite 4583 passed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LT8D8tRmExyQve69DCWRik
…see #294)

Board round 8 found a spawn path escaping the check again: the warning's
local-binary early return ran before the install argvs were judged, so an
overlay with `command: firecrawl-mcp` and the shipped unpinned install argv
was silent while gateway.provision spawned and adopted latest.

- The warning now computes the whole spawn set first (args, and for a
  manifest server every non-empty install argv, from _SERVER_SPAWN_SITES)
  and judges every member before any exemption; the local-binary exemption
  applies only when every member is literally [command, *args].
- test_every_spawn_site_is_classified walks src/pmcp with ast and fails on
  any spawn call site the classification table does not list.
- The spawn-site table is corrected (N-1: the descriptions refresh spawns
  the manifest argv with no entry env; N-2: there is no `pmcp install`);
  the handoff re-read (N-3) is documented as R15.

Shipped cost: 0 of 77. Mutants 84/84 red; 236 pin tests (5 red on
bdc13ca); full suite 4739 passed on origin/main 260cc1a.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LT8D8tRmExyQve69DCWRik
Board round 9 found two recurring classes (two readers that disagree; an
exemption applied before a check) plus unredacted diagnostics and
unguarded exceptions. Structural fixes:

- One parser per launcher (_read_argv) is the only reader of a spawning
  argv for the warning and the [PINNED] label; a repeated single-valued
  flag (uvx --from, cargo --version, docker --name) is unreadable, and
  `npm exec` flags after the spec are loud.
- One judge (_judge_spawn_set) checks every spawn member's shape, pin,
  env and cwd before any verdict; the local-binary exemption waives only
  the pin.
- Every argv shown in a diagnostic goes through the secret-safe renderer.
- A malformed member or an exception is contained to that server's own
  "cannot verify"; health catches per server.
- _SERVER_SPAWN_SITES now builds the spawn set; the AST spawn-site test
  resolves imports/aliases and counts references per site (22 of 22
  synthetic variants found).

Shipped cost: 0 of 77. Measured on origin/main 260cc1a: 263 pin tests (34
red on 0a63441), 84/84 mutants red, full suite 4766 passed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LT8D8tRmExyQve69DCWRik
…ce (see #294)

Board round 10: every command that was neither a modelled launcher nor a
listed runner counted as a "local binary", which waived the pin, so
`timeout 600 npx -y firecrawl-mcp`, sudo/nice/busybox/mise/volta/corepack
wrappers, pipx/go/python -m runners and npx.js were silent.

- A command is a local server binary only when pmcp's shipped manifest
  names that exact command for the server (read directly, as the
  declarations are); every other unrecognised command is "cannot verify",
  named. A known launcher name with an unstripped extension is never local.
- The redaction claim is narrowed to this feature's diagnostics (main's
  unknown-package line is a follow-up); cwd is escaped; a string install
  argv is one unreadable member; README notes a uv ==X pin is a version,
  not an artifact.

Shipped cost: 0 of 77 (shipped commands are only npx and uvx). Measured on
origin/main 260cc1a: 358 pin tests (95 red on ba58ade, incl. 94 generated
wrapper x launcher ids), 87/87 mutants red, full suite 4861 passed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LT8D8tRmExyQve69DCWRik
…see #294)

Board round 11:

- Whether the warning applies was read from the overlay-applied entry's
  api_key_optional_when in three places (the update wrapper, the health
  filter, relaxed_by), so an overlay replacing firecrawl without the key,
  or with requires_api_key: false, silenced it. The relaxer keys are now
  the shipped manifest's (read directly) union the overlay's; the
  credential gate is unchanged; the overlay-only-name limit is disclosed.
- A modelled launcher must be spelled as itself: the bare platform name,
  or the path the child's PATH resolves it to. /tmp/x/npx, ./npx,
  node_modules/.bin/npx, NPX, C:npx and npx.cmd on Linux are "cannot
  verify".
- The plan records a sweep of every input the apply / silent / [PINNED]
  decisions read and where each comes from.

Shipped cost: 0 of 77. Measured on origin/main 260cc1a: 368 pin tests (16
red on 791380c), 92/92 mutants red, full suite 4871 passed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LT8D8tRmExyQve69DCWRik
Board round 12: a relative launcher path was compared with realpath from
pmcp's cwd, but the OS resolves it from the child's (entry-set) cwd, so
a deep relative docker path plus an entry cwd was silent and [PINNED].

- A relative launcher path is never the launcher, on every platform.
- docker with an entry-set cwd is "cannot verify" when the child's PATH
  would search that cwd (a relative or empty entry, or always on Windows,
  where CreateProcess searches the cwd first).
- R20 widened to .mcp.json/.pmcp.json servers under unshipped names; the
  input-sweep launcher row corrected.

Shipped cost: 0 of 77. 388 pin tests (7 red on 03edb0a), 95/95 mutants
red, full suite 4891 passed on origin/main 260cc1a and on 4d2790f.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LT8D8tRmExyQve69DCWRik
Board round 13 on 78b21f4. docker's cwd exemption holds only for the bare
`docker` spelling; launcher paths are compared literally with the child's
PATH search, never resolved in pmcp's process; /proc PATH entries count as
searching the cwd; Windows .cmd/.bat argvs refuse cmd.exe metacharacters;
docker's relaxer is judged on the container's env (--env-file applies the
warning). Windows text corrected: CreateProcess searches the parent's cwd
and PATH. 407 tests (15 red on rev 14), 102 of 102 mutants red, shipped
cost 0 of 77; re-based on origin/main 7680445.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LT8D8tRmExyQve69DCWRik
…e parser (see #294)

Board round 14 on 6086712. Whether the warning applies is now decided
per spawn member from the one parser's reading: a clean docker run
launched as docker itself uses its container env (read by _read_docker,
which now parses pflag shorthand clusters); a docker argv pmcp cannot
fully read is "may"; every other member uses its own process env. The
warning applies if any member applies. The second docker-env reader is
deleted. Windows PATHEXT-case text and the sweep table are corrected, and
the input sweep lists every decision's reader. 429 tests (20 red on rev
15), 111 of 111 mutants red, shipped cost 0 of 77, on origin/main 7680445.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LT8D8tRmExyQve69DCWRik
…passed the judge (see #294)

Board round 15 on 56dfca4. A docker member's container env alone decides
the relaxer only when that member passed the per-member judge
(_member_problems empty, with the shipped declarations); a bare docker the
entry can redirect (entry PATH, entry cwd under a cwd-searching PATH) is
judged on its process env too. A launcher's identity is trusted only after
the judge. Discloses image-ENV relaxers (R22) and parse-clean argvs docker
refuses. 435 tests (4 red on rev 16), 114 of 114 mutants red, shipped cost
0 of 77; full suite with -x on origin/main 7680445: 5142 passed (a first
-x run under mutant load hit a flake in main's stderr-monitor test).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LT8D8tRmExyQve69DCWRik
…nd (see #294)

Board round 16 on 9d0a73f. For a server with relaxer keys, any spawn
member the one parser cannot fully read (any launcher, any reason: env
prefix, sh -c, npx -c, a wrapper, podman, docker by another path, an
unread flag) now makes the warning apply as "may talk to a self-hosted
backend", naming the member; only fully read members are judged by their
env. R21 narrowed and R18 widened accordingly. 449 tests (9 red on rev
17), 116 of 116 mutants red, shipped cost 0 of 77; full suite with -x on
origin/main 7680445: 5156 passed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LT8D8tRmExyQve69DCWRik
#294)

Board round 17 on 98b367e. The relaxer decision and the per-member judge
now read one result: _judge_members computes each member's problems once
(_Judged), _verdict_of builds the verdict from it, and _member_relaxer
reads the same object. For a relaxer-bearing server, any member problem
(unreadable argv, non-allowlisted env key such as NODE_OPTIONS, container
env key, entry cwd, launcher identity) makes the relaxer "may talk",
naming the problem; only a problem-free member is judged by its env. The
--env-file wording is restored; README notes the project .env case; R18
lists the measured operator cost (13 of 17 vendor-hosted setups warn).
461 tests (9 red on rev 18), 119 of 119 mutants red, shipped cost 0 of
77; full suite with -x on origin/main 7680445: 5168 passed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LT8D8tRmExyQve69DCWRik
…see #294)

Board round 18 on a9e53b8 (codex B1). uv hands the --from value or
command to PythonRequest::try_from_tool_name first, so `uvx python==3.10`
(and --from / uv tool run, cpython/pypy/graalpy/pyodide, any case, any
version-request spelling) runs an interpreter whose patch floats, not a
PyPI package. _read_uvx now refuses such a component as "uv reads '<x>'
as an interpreter request, not a package", never exact or [PINNED].
Every spelling was checked against uv 0.12.19 on this host; uv's ==X for
real packages was re-checked (only local labels and zero-padded equal
releases, both non-PyPI). 479 tests (14 red on rev 19), 124 of 124
mutants red, shipped cost 0 of 77; full suite with -x on origin/main
7680445: 5186 passed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M1b7aDmh5mzuiPQQprmsXN
…nly from the judge (see #294)

Board round 19 on 23e1e44. uv trims whitespace before a version request and
packaging accepts it, so `uvx 'python ==3.10'`, `--from 'python == 3.10'`,
`uv tool run 'cpython ==3.10'` and a tab were read as exact pins while uv
ran interpreters. _read_uvx now refuses when the parsed requirement name is
an interpreter prefix or when uv's own text reading (whitespace skipped)
says interpreter. update_server's [PINNED]/pinned_version carry only the
judge's selector. Differential against a measured uv 0.12.19 table (3,024
components x 3 forms; 880 interpreter cells): rev 20 read 891 argvs as
pins, rev 21 reads 0. 490 tests (8 red on rev 20), 127 of 127 mutants
red, shipped cost 0 of 77; full suite with -x on origin/main 7680445:
5197 passed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M1b7aDmh5mzuiPQQprmsXN
…enderer (see #294)

Board round 20 on a49dc28 (codex B1). The interpreter diagnostic quoted
the whole uv requirement through _operator_safe, which escapes but never
redacts, so a credential in a `--from "python @ https://user:TOKEN@..."`
reached the warning; the same construction leaked through the cargo
toolchain, the cwd path and a flag name. Every string the parser, the
per-member judge and the warning build is now fixed text, a value through
a closed grammar (diag_name / diag_flag / diag_selector), or the one
renderer; the interpreter message names only the interpreter; a cwd is
not echoed; update_server's label text and the loader's pin logs use the
same grammars. A property test drives every problem branch (found by ast)
with a credential-shaped sentinel; a static test checks every
interpolation. Folds in N1 (v-prefixed interpreter requests). 493 tests,
134 of 134 mutants red, shipped cost 0 of 77; full suite with -x on
origin/main 7680445: 5200 passed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M1b7aDmh5mzuiPQQprmsXN
ViperJuice added a commit that referenced this pull request Sep 30, 2026
Piece 1 of #294. A manifest `version:` or overlay `server_version:` must be one exact version. It is written into `args` and every platform's install argv, all or nothing, and only when:
- the launcher is a bare `npx`;
- every slot is a plain same-name registry spec;
- the entry's own env cannot redirect npm.

Refusals are logged as fixed text; server names are shown only if pmcp ships them. `update_server` / `pmcp update` report `[PINNED]` only when the materialised pin is exactly what runs. The self-hosted-drift warning is piece 2 (reference: #295).

Approved by the cross-vendor review panel.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RiabTgWZRfuqiUQ9siEegF

This branch has not been deployed

No deployments
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.

1 participant