sec: hash-pin every dependency install and add coverage-guided fuzzing - #244
Merged
Conversation
Closes the 22 open OpenSSF Scorecard alerts: 21 Pinned-Dependencies and the Fuzzing check. No Dependabot/CVE alerts were open. Pinning ------- An exact version is not a pin. `foo==1.2.3` still resolves to whatever the index serves under that version today; only a hash pins the bytes, which is what Scorecard's check encodes. `--require-hashes` is all-or-nothing, so it needs a resolved lock — uv.lock becomes that single source of truth: * pyproject.toml gains [dependency-groups] for the tools each job runs (ruff, pyright, build+hatchling, atheris) so they are locked rather than restated as bare version pins in two workflow files. * [[tool.uv.index]] declares the PyTorch CPU index and [tool.uv.sources] binds torch to it on Linux. The three containers previously passed --index-url at the call site, so the lock described PyPI's artifact while the image installed a different one — no source of truth could produce a hash for what was actually installed. The lock now records torch 2.13.0+cpu, and resolution drops 18 nvidia/cuda packages plus triton. torch is named in a `container` dependency-group only so the source can bind to it; PEP 735 groups are not published, so nothing changes for anyone installing hypermnesia-mcp from PyPI. * scripts/generate_pip_constraints.py exports one hashed file per call site into requirements/ and refuses an export that is empty or carries an unhashed requirement. `--check` is a blocking Lint step, so a lock change that is not re-exported fails there rather than at install time. * All 21 sites rewired: ci.yml x7, release.yml x2, setup.sh, and the three Dockerfiles. The project itself installs with --no-deps against the hashed set; the root image builds a wheel instead, because an editable install leaves a .pth pointing at a build directory the runtime stage never copies. The two non-pip findings could not be pinned and had to stop being what they were: * docker/Dockerfile piped https://deb.nodesource.com/setup_22.x into bash — an unreviewed remote script executed as root at build time, with no hash to check a pipe against. Replaced by what that script does: fetch the signing key, register the signed apt source, install the signed package. * `npm install -g @anthropic-ai/claude-code` was unversioned, so the image tracked whatever the registry served that minute. Now `npm ci` against a committed lockfile, which is also the only form Scorecard accepts and which records a sha512 integrity hash per transitive package. Fuzzing ------- Two harnesses over pure parsers that read untrusted text: the hand-rolled YAML frontmatter parser, and the wiki source-path canonicaliser. Wired to ClusterFuzzLite (.clusterfuzzlite/, .github/workflows/fuzz.yml) — a short batch on PRs, a longer scheduled run that does not block. Writing the path harness found a live bug. normalize_source_path stripped "./" in a loop and then "/" once, so removing the slashes could expose a "./" the loop had already walked past: ".//./x" came out as "./x", still carrying the prefix the function exists to remove, and not idempotent. extract_document_paths dedupes on that result, so one document reachable by two spellings counted as two. Fixed by iterating to a fixed point; the four reproducers are committed as corpus inputs and fail on the pre-fix code. fuzz/replay_corpus.py runs every corpus input through its harness with no atheris, so the properties execute in the ordinary pytest suite on every platform — atheris publishes manylinux x86_64 wheels for cpython 3.12-3.14 and nothing else, and a property only one CI job can run is one that rots. Also fixed here --------------- * docker/Dockerfile copied /usr/local/lib/python3.12/site-packages against a python:3.14 base — a path absent from both stages, so that image could not build at all. Invisible because no CI job builds it; both it and the devcontainer image now get build jobs, which is also the evidence #203 asks for and closes the gap its own note flags. * scripts/setup.sh hid pip's stderr behind 2>/dev/null and printed success regardless — a resolution failure, a hash mismatch and a network error all reported "Python packages installed". Its hand-written package list had also drifted from pyproject.toml (sentence-transformers>=2.2.0 against a real floor of >=3.0.0). * .gitattributes marks fuzz/corpus/** as binary: EOL normalisation would have rewritten the CRLF seed and deleted the case it exists to cover. Refs #203 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BJt9KN23rUNQzwoJv4jRWw
The docker/ and .devcontainer/ images had no CI build, which is why docker/Dockerfile could sit unbuildable against a python:3.14 base. Both now build on every push and PR — that is also the evidence #203 asks for, and it closes the gap that issue's own note flags rather than deferring it to a follow-up. Advertised suite size synced 6348 -> 6383 across 11 sites in 5 files (35 new tests: constraint-set mapping, fuzz corpus replay, path canonicalisation regressions). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BJt9KN23rUNQzwoJv4jRWw
…mmand The lock-drift gate added in 570d4b3 installed its own tool with `pipx install uv==0.11.3 2>/dev/null || pip install uv==0.11.3`. Scorecard scores that fallback as "pipCommand not pinned by hash" — the same finding already open on `pip install pyright==1.1.410` (alert 142) — because an exact version is not a pin: `==` resolves to whatever bytes the index serves under that version today. So the commit that closes the other 21 Pinned-Dependencies alerts would have minted a 22nd, in a new file of its own making. That is not a partial win: the repo's policy clears PinnedDependenciesID only at `score: 10, mode: enforced`, so one unpinned command leaves all 21 open. uv cannot come from a hash-pinned requirements file — it is the tool that generates those files. It comes from the SHA-pinned action instead, the form release.yml's SBOM job already uses, with `version` preserving the 0.11.3 pin so `--check`'s byte comparison stays deterministic. Verified: `generate_pip_constraints.py --check` exits 0 (13 files), `check_doc_claims.py` exits 0, ci.yml parses, and no unpinned pip/npm command remains at any of the 23 install sites. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RahDoFS4HWmqPzYghhYFar
Root cause, not the symptom. `render` located uv, ran it, and judged its
output in one function, so the `shutil.which("uv")` guard ran before every
rule. No test could reach the validation logic without a real uv on PATH —
stubbing subprocess.run did not help, because the guard fired first. On CI,
where uv is only in the Lint job, four tests failed with "uv is not
installed" while asserting nothing about hashing or drift.
The symptomatic fix was to install uv into all three test jobs. That would
have made red jobs green while leaving unit tests dependent on an external
binary they never needed.
Split instead:
* export() — locates and runs uv. The only part that touches the world.
* compose() — judges the export and assembles the file. Pure.
* render() — compose(set, export(set)).
The rules are now reachable from a string literal, so the hashing and
empty-export tests need no uv, no stub and no PATH.
TestDriftGate now exercises the gate's DECISION (compare committed text to
what the lock would produce; map that to an exit code) rather than the
working tree's current state. Whether the committed files actually agree
with uv.lock is a fact about the tree, already asserted by the Lint job's
`--check` on every push and PR. Asserting it inside pytest too did not make
it truer; it made every test job carry uv. One gate, where it belongs.
Verified with uv removed from PATH entirely: 247 passed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BJt9KN23rUNQzwoJv4jRWw
The lock was STALE, not constrained. That is the whole root cause.
Exporting it pinned tree-sitter-language-pack 1.6.2 while CI's previous
`pip install -e .[...]` fresh-resolved 1.13.5, and pyright failed on the
older package's stricter `SupportedLanguage` signature. Measured against a
real 3.12 resolve, the exported lock disagreed on 66 of 120 packages.
Two wrong answers were tried and reverted before this one:
* a `cast` at the pyright call site — masking one visible symptom of a
graph-wide version skew;
* switching the generator to `uv pip compile --python-version`, which
resolves FRESH FROM THE INDEX and does not read uv.lock at all. That
made the files non-deterministic, broke the "generated from uv.lock"
contract, and would have made Dependabot's uv.lock updates decorative —
a security regression, since Dependabot is what keeps these pins from
freezing on vulnerable versions.
`uv lock --upgrade` was the fix. tree-sitter-language-pack moves 1.6.2 ->
1.13.5 and the divergence from a fresh 3.12 resolve drops 66 -> 10. The
remaining 10 (numpy, scipy, scikit-learn, transformers, huggingface-hub,
onnxruntime, networkx, fsspec, rpds-py, aiofile) are the `requires-python
= ">=3.10"` floor: their newer releases dropped 3.10, so a 3.10 install
gets exactly these. That is a property of the supported range, not of this
change, and Dependabot raises security PRs against uv.lock when one of them
needs moving.
The generator stays lock-derived, so a Dependabot bump to uv.lock flows
into every requirements file through `--check`.
Full suite: 6389 passed. Generation is deterministic across repeated runs.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BJt9KN23rUNQzwoJv4jRWw
#243 landed the self-hosted README badges, which conflicts with this branch's shields.io badge row and with the advertised test count. Resolutions: * README badge row — main's committed assets/*.svg supersede this branch's shields.io markup outright. * ci.yml Lint job — BOTH gates kept. main added the committed-badge check, this branch added the hash-pinned-requirements check; neither supersedes the other. * Test count — recomputed on the merged tree rather than picking a side: 6414 collected, synced across 11 sites in 5 files, and assets/badge-tests.svg regenerated so the doc-claims gate reads the same figure it asserts. * mcp_server/core/ast_parser.py — restored to main byte-for-byte. The `cast("SupportedLanguage", ...)` this branch carried was left over from an abandoned fix and had lost its type-only import (ruff F821). It is also moot: the refreshed lock ships tree-sitter-language-pack 1.13.5, whose signature main already passes pyright against. Zero divergence. Fixed en route (§14), pre-existing and unrelated to this branch: tests_py/invariants/test_I2_canonical_writer.py excluded scan paths by testing the ABSOLUTE path for "worktree". An agent worktree lives at <repo>/.claude/worktrees/<name>/, so running the suite from one made every path match and skipped the entire package — the scan found zero heat_base writers, `unexpected` was empty, and the invariant passed VACUOUSLY. It could not have caught a new unauthorized writer from a worktree at all; only the stale-allow-list half of the assertion made it visible. Both scan loops now test the path relative to the scan root. Verified from the worktree (52 passed) and from the primary checkout (3 passed). Full suite on the merged tree: 6414 passed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BJt9KN23rUNQzwoJv4jRWw
Owner
Author
|
Heads-up from #248: |
Brings f606c37 (marketplace-pin guard on the root manifest.json). The six conflicts were all one claim — the advertised collected-test count — where main had moved 6373 -> 6376 and this branch 6373 -> 6414. Resolved to the branch's number here; the merged tree's real count is measured and applied in a later commit, once the tests that move it exist. Repairs a defect found while resolving: .bestpractices.json was committed at c090278 carrying FOUR unresolved conflict blocks from the previous merge, which left it invalid JSON. Both sides of all four were byte-identical, so collapsing them is lossless (verified by comparing the sides before collapsing, not by picking one). The file is transcribed into the OpenSSF Best Practices questionnaire, so an unparseable copy is a broken consumer and not merely a stale number. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Yu6EnWspTfqHoGkExyS6u
…had no cp310 artifact
`Test (Python 3.10)` failed at install with
ERROR: Could not find a version that satisfies the requirement
onnxruntime==1.24.3 (from versions: ..., 1.23.0, 1.23.1, 1.23.2)
ERROR: No matching distribution found for onnxruntime==1.24.3
(CI run 30436365825 job 90524943669; reproduced 2026-07-29 in
python:3.10-slim/linux-amd64 against requirements/ci-postgresql.txt.)
Root cause is the lockfile, not the workflow. onnxruntime 1.24.x publishes
no cp310 artifact and no sdist — 1.24.3 ships 24 files whose lowest
interpreter tag is cp311, and 1.24.0 declares no Requires-Python at all, so
uv accepted it for the `python_full_version < '3.11'` fork and recorded it
there with a wheel list containing nothing installable on 3.10. A lock entry
for the 3.10 fork with no 3.10 artifact is broken by inspection.
The defect is pre-existing on main (`git show origin/main:uv.lock` carries
the same entry) and stayed latent only because main installs from
pyproject.toml, where pip re-resolves onnxruntime down to 1.23.2 itself.
This branch makes the lock the install source, which turns the latent lock
defect into a hard failure. Fixed at the lock, not at the throw site.
A `[tool.uv] constraint-dependencies` entry, not a project dependency:
constraints steer only our resolution and are never published in the wheel
metadata, so consumers on 3.11+ still get the current onnxruntime. Declared
before the `[[tool.uv.index]]` / `[tool.uv.sources]` tables so it never
re-opens a `tool.uv` super-table they created implicitly.
Verified across every (requirements file, python) pair its consumers use,
on linux/amd64 (CI's platform; this host is arm64) with
`pip install --dry-run --require-hashes`. onnxruntime was the only package
with this defect: every other pin resolves unchanged.
Lock delta: onnxruntime 1.24.3 -> 1.23.2 on the <3.11 fork only (1.28.0 on
>=3.11 is untouched), plus its three transitive deps (coloredlogs,
humanfriendly, pyreadline3). 9 of 13 requirements files regenerated with
uv 0.11.3 — the exact version .github/workflows/ci.yml pins via
astral-sh/setup-uv, because the Lint gate byte-compares a `uv export`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Yu6EnWspTfqHoGkExyS6u
…JSON
The four unresolved conflict blocks repaired in the merge commit had passed
this gate, CodeQL, and 18 green checks. .bestpractices.json is one of the
gate's own SCANNED_FILES, so the gate READ the broken file and reported
"doc claims OK" — every check it runs is a claim regex, and a regex matches
the first side of a conflict and never looks at the file's structure. That
is the §6 root cause: the gate had no notion of a file that says two things.
Two checks close it, both derived from SCANNED_FILES so a file added to the
gate is enrolled in them with no further edit (§1.2):
* check_no_conflict_markers — matches only the LABELLED markers
(`<<<<<<< HEAD`, `>>>>>>> origin/main`; git always writes a ref after the
seven characters). A bare `=======` is deliberately not matched: it is a
legal setext H1 underline in Markdown and most scanned files are
Markdown, so matching it would fail honest documents. Pinned by
test_a_markdown_setext_underline_is_not_a_conflict_marker.
* check_scanned_json_parses — the OpenSSF answers are transcribed into the
badge questionnaire and manifest.json is read by the plugin loader, so a
file that no longer parses is a broken consumer, not a stale number.
Both fail closed on a missing file, matching check_badge's rule that a check
which silently skips its subject is worse than none because it still prints
OK.
Non-vacuity, measured rather than asserted: against the pre-fix
.bestpractices.json (git show c090278:.bestpractices.json),
test_the_real_repository_tree_is_structurally_clean fails with all 8 marker
lines reported; against the repaired file the 9 new tests pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Yu6EnWspTfqHoGkExyS6u
… 6426
Measured, not carried over:
$ pytest --collect-only -q | tail -1
6426 tests collected in 9.42s
(python 3.12, the leg CI runs this check on.) The arithmetic reconciles:
6414 on this branch + 3 from main's f606c37 (marketplace-pin guard tests)
+ 9 from the doc-claim structural gate = 6426.
Updates the five claim sites the gate scans and regenerates
assets/badge-tests.svg, which carries the same figure in its <title>.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Yu6EnWspTfqHoGkExyS6u
Formatting only, no behaviour change; kept off the logic commit per §H5. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Yu6EnWspTfqHoGkExyS6u
…ker gate Both belong to the #244 entry: the lock defect is what making the lock the install source exposed, and the conflict-marker gate is the root-cause fix for a defect this branch itself carried through a green CI run. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Yu6EnWspTfqHoGkExyS6u
Every CI job on this PR emitted, per run:
##[warning]Node.js 20 is deprecated. The following actions target
Node.js 20 but are being forced to run on Node.js 24:
actions/checkout@11d5960a, actions/upload-artifact@ea165f8d
(CI run 30436365825, all 12 jobs). The runner is ALREADY forcing these onto
Node 24, so the repository was relying on a compatibility shim GitHub has
announced it will remove — the actions were running on a runtime they were
never built against.
The PR body previously reported this as "unrelated, pre-existing" with no
issue number, which is precisely the deferral §14.3 forbids. These refs live
in .github/workflows/ci.yml, a file this diff modifies, so §14.1 puts them in
touched material: fixed here rather than filed.
Bumped, each SHA-pinned as before and each verified node24 by reading
`runs.using` out of its action.yml at the target SHA (not by trusting the
tag):
actions/checkout v4 -> v7.0.1 (6 workflows)
actions/cache v4 -> v6.1.0 (2 workflows)
actions/upload-artifact v4 -> v7.0.1 (2 workflows)
actions/download-artifact v4 -> v8.0.1 (release.yml)
docker/build-push-action v6 -> v7.3.0 (ci.yml)
github/codeql-action v3 -> v4.37.3 (scorecard.yml)
upload-artifact and download-artifact move together because release.yml
pairs them; download-artifact was node20 as well, so leaving it would have
kept the same defect and mismatched the pair.
codeql-action needed the v4 line, not the newest v3: v3.37.3 is still
node20. Checked rather than assumed.
A repo-wide sweep of all 16 pinned action refs now reports **0 on node20**
(the rest are `docker` or `composite` actions, which have no node runtime).
Scorecard re-measured on this tree after the bumps: Pinned-Dependencies
10/10 (43/43 GH actions, 18/18 third-party, 6/6 containerImage, 22/22
pipCommand, 1/1 npmCommand) and Fuzzing 10/10 — SHA pinning is preserved,
so the alert closure this PR exists for is unaffected.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Yu6EnWspTfqHoGkExyS6u
This was referenced Jul 29, 2026
cdeust
added a commit
that referenced
this pull request
Jul 29, 2026
* ci: add a safe workflow_dispatch verification path to release.yml (#246) Every uses: in .github/workflows/ already resolved to actions/checkout v7.0.1, actions/cache v6.1.0, actions/upload-artifact v7.0.1, actions/download-artifact v8.0.1, and github/codeql-action/upload-sarif v4.37.3 (all node24, confirmed by reading action.yml at each pinned SHA) — PR #244 bumped these SHAs to their current majors as a side effect of its hash-pinning pass, ahead of this issue landing. Nothing left to bump. What remained was #246's AC4: one dispatch-verified green run of each affected workflow with zero "forced to run on Node.js 24" lines, quoted in the PR — and AC3: the upload-artifact/download-artifact round trip in release.yml proven by one real run. release.yml is push-tag-only, so proving that round trip meant either cutting a real release (out of scope: it would publish a real PyPI version and GitHub Release for a CI verification) or adding the seam to test it safely. Add workflow_dispatch to release.yml, gated so a manual run exercises checkout/cache/upload-artifact/download-artifact exactly as a real release would, but never creates a GitHub Release, never attaches assets to one, and never publishes to PyPI: - github-release job: if: github.event_name == 'push' - build job's "Attach attested distributions" step: same guard (softprops/action-gh-release creates the tag_name release if absent — an unguarded dispatch run would forge one) - sbom job's "Upload SBOM" step: same guard - publish-pypi job: only the pypa/gh-action-pypi-publish step is guarded; the download-artifact step above it stays unconditional, since that is the exact half of the round trip this exists to verify Refs #246. * ci(release): bump actions/attest-build-provenance v2.4.0 -> v4.1.1 (#246) Real dispatch-verification run (30498168023) caught what #246's own investigation missed: this action's `runs.using: composite` at v2.4.0 still shells out internally to `attest-build-provenance/predicate@1176ef5` and an older `actions/attest`, both node20 -- a real "forced to run on Node.js 24" warning fired twice in that run (sbom job, build job), on an action #246 had listed as needing no change. v3.0.0's release notes (actions/attest-build-provenance#691, #693) name the fix directly: "Bump actions/attest ... Bump to node24 runtime" + "Bump attest-build-provenance/predicate to v2.0.0 ... Bump to node24 runtime". v4.0.0 restructures further: the composite step now calls actions/attest@v4.1.1 (SHA a1948c3f..., confirmed node24 by reading its action.yml) directly, dropping the separate predicate step. Inputs are unchanged (subject-path/subject-digest/subject-name/... identical names in v4.1.1's action.yml vs our v2 usage) -- no call-site change needed. Refs #246.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes all 22 open code-scanning alerts: 21 Pinned-Dependencies + 1 Fuzzing. There were no Dependabot alerts and no known-CVE vulnerabilities open — every finding was OpenSSF Scorecard.
Closes #203, and goes past it: that issue covers the three Dockerfiles (11 alerts). The other 10 pinning alerts (
ci.yml×7,release.yml×2,scripts/setup.sh) and the Fuzzing alert had no issue at all, and are fixed here rather than filed for later.The core point
An exact version is not a pin.
ruff==0.15.20andtorch==2.11.0both counted as unpinned, and Scorecard is right: a version names a release, not the bytes the index serves for it today. Only a hash pins the artifact.pip install --require-hashesis all-or-nothing — one hash means every requirement including transitive ones needs one — so it needs a resolved lock.uv.lockbecomes the single source of truth.scripts/generate_pip_constraints.pyrequirements/. Refuses an export that is empty or carries an unhashed requirement.--checkis a blocking Lint step, so a lock change that isn't re-exported fails there, not at install time.[dependency-groups][[tool.uv.index]]+[tool.uv.sources]The torch finding was worse than "unpinned"
The containers passed
--index-url https://download.pytorch.org/whl/cpuat the call site. Souv.lockrecorded PyPI's torch and its PyPI hashes, while the image installed a different artifact from a different index. No source of truth could produce a hash for what was actually installed — which is why--require-hasheswasn't reachable before.The lock now carries
torch 2.13.0+cpuwith 22 hashes, and resolution drops 18 nvidia/cuda packages plus triton. torch is named in acontainerdependency-group only so the source can bind to it — PEP 735 groups aren't published, so nothing changes for anyone installinghypermnesia-mcpfrom PyPI.Two findings couldn't be pinned — they had to stop being what they were
curl https://deb.nodesource.com/setup_22.x | bash— an unreviewed remote script executed as root at build time, and there is no hash to check a pipe against. Replaced by what that script does: fetch the signing key, register the signed apt source, install the signed package. curl now feedsgpg --dearmor, which executes nothing.npm install -g @anthropic-ai/claude-code— unversioned, so the image tracked whatever the registry served that minute. Nownpm ciagainst a committed lockfile, which is also the only form Scorecard accepts (an exact version on the command line is not enough) and which records a sha512 integrity hash per transitive package.Accepted forms, from the checker's source
I read
isUnpinnedPipInstallrather than inferring it. A barepip install --no-deps .is unpinned —.is neither a flag nor a.whl, so it setshasAdditionalArgs;isPinnedEditableSourceis consulted only for-e. Hence:--require-hashes, or-e <local> --no-deps, or a.whlpath. The root image therefore builds a wheel — an editable install there would leave a.pthpointing at/build, which the runtime stage never copies, and the venv would arrive broken.Fuzzing — and what it found
Two harnesses over pure parsers that read untrusted text (§13.1 D2): the hand-rolled YAML frontmatter parser and the wiki source-path canonicaliser. ClusterFuzzLite runs a 120s batch on PRs (blocking) and a longer scheduled run (non-blocking — a fuzzer left running eventually finds something, and holding the merge queue hostage to an unrelated input makes the check ignored within a week).
Writing the path harness found a live bug.
normalize_source_pathstripped./in a loop, then/exactly once. Removing the slashes can expose a./the loop already walked past:The result was not idempotent, and
extract_document_pathsdedupes on it — so one document reachable by two spellings counted as two. Fixed by iterating to a fixed point. The four reproducers are committed as corpus inputs and fail on the pre-fix code (verified by stashing the fix and re-running).fuzz/replay_corpus.pyreplays every corpus input with no atheris, so the properties run in the ordinarypytestsuite on every platform. Atheris publishes manylinux x86_64 wheels for cpython 3.12–3.14 and nothing else — a property only one CI job can run is one that rots.Also fixed (§14, seen en route)
docker/Dockerfilecould not build at all — it copied/usr/local/lib/python3.12/site-packagesagainst apython:3.14base, a path absent from both stages. Invisible because no CI job built this image. Now installs into a version-free venv (the rule the root Dockerfile already documents).scripts/setup.shreported success over any failure — the install ended in2>/dev/null, which is exactly where a resolution failure, hash mismatch and network error appear, and it printed "Python packages installed" regardless. Exit status is now checked..gitattributesmarksfuzz/corpus/**binary — EOL normalisation would have rewritten the CRLF seed on checkout and deleted the case it exists to cover.Unblocking pass (2026-07-29)
The first push left
Test (Python 3.10)red. Root-caused to the lock, notto this PR's design, and fixed there (§6).
The 3.10 blocker
uv.lockrecordedonnxruntime 1.24.3for thepython_full_version < '3.11'fork, and onnxruntime 1.24.x publishes no cp310 artifact and no sdist —
1.24.3 ships 24 files whose lowest interpreter tag is
cp311, and 1.24.0declares no
Requires-Pythonat all, which is why uv accepted it there. A lockentry for the 3.10 fork with no 3.10 artifact is broken by inspection.
The defect is pre-existing on
main(git show origin/main:uv.lockcarriesthe same entry). It stayed invisible because
maininstalls frompyproject.toml, where pip re-resolves onnxruntime down to 1.23.2 on 3.10 byitself. A hash-pinned install cannot re-resolve — so making the lock the install
source is exactly what turned a latent lock defect into a visible failure. That
is the change working as intended, not a regression it introduced.
Fixed with a
[tool.uv] constraint-dependenciesentry rather than a projectdependency: constraints steer only our resolution and are never published in the
wheel metadata, so anyone installing
hypermnesia-mcpon 3.11+ still resolvesthe current onnxruntime. Declared before the
[[tool.uv.index]]/[tool.uv.sources]tables so it never re-opens atool.uvsuper-table theycreated implicitly.
Regenerated with uv 0.11.3 — the exact version
ci.ymlpins viaastral-sh/setup-uv, because the blocking Lint step byte-compares auv exportresult and any other uv version fails it.
Lock delta is minimal and forkwise:
onnxruntime 1.24.3 -> 1.23.2on the<3.11fork only (1.28.0on>=3.11untouched), plus its three transitivedeps (
coloredlogs,humanfriendly,pyreadline3). 9 of 13 requirements fileschanged.
onnxruntime was the only package with this defect — proven by resolving
every requirements file against every Python its consumers use, below.
§14 defects found and fixed in this pass
.bestpractices.jsonwas committed carrying four unresolved merge-conflictblocks (at c090278), leaving it invalid JSON — and it is transcribed into
the OpenSSF Best Practices questionnaire, so an unparseable copy is a broken
consumer, not a stale number. It had passed the doc-claim gate, CodeQL and 18
green checks. Root cause (§6):
.bestpractices.jsonis one of that gate'sown
SCANNED_FILES, but every check it runs is a claim regex — a regex matchesthe first side of a conflict and never looks at the file's structure. The gate
read the broken file and printed
doc claims OK. Both sides of all four blockswere byte-identical, so the repair is lossless (verified by comparing the
sides, not by picking one). Two new checks close the class, both derived from
SCANNED_FILESso a newly scanned file is enrolled with no further edit, andboth failing closed on a file they cannot read.
reported this as "unrelated, pre-existing" with no issue number — precisely
the deferral §14.3 forbids. These refs live in
ci.yml, a file this diffmodifies, so §14.1 puts them in touched material: fixed here, not filed.
Completion Ledger (§13.2)
Path enumeration
named_index_url— foundtest_every_local_version_pin_names_a_serving_indexnamed_index_url— absent →ExportErrortest_undeclared_index_is_refusedConstraintSet.command()— extras / groups / only-groupstest_export_asserts_the_lock_matches_pyprojectheader()— with and without index directivetest_every_local_version_pin_names_a_serving_indexrender()— uv missing →ExportErrortest_export_failure_exits_two_not_onerender()— uv non-zero exit →ExportErrortest_export_failure_exits_two_not_onerender()— empty export refusedtest_empty_export_is_refusedrender()— unhashed requirement refusedtest_unhashed_export_is_refusedrender()— local pin absent from locktest_local_pin_absent_from_the_lock_is_refusedwrite()— changed / unchangedtest_check_passes_on_the_committed_treestale()— missing filetest_check_fails_when_a_file_is_missingstale()— content differstest_check_fails_on_a_mutated_filestale()— current → Nonetest_check_passes_on_the_committed_treemain()—--checkclean → 0 / drift → 1 / cannot run → 2test_check_passes_on_the_committed_tree,test_check_fails_on_a_mutated_file,test_export_failure_exits_two_not_onetest_drift_message_names_the_regeneration_commandtest_every_set_has_a_committed_file,test_no_committed_file_is_orphaned,test_filenames_are_uniquetest_every_requirement_is_version_pinned_and_hashedtest_no_editable_or_directory_requirementsnormalize_source_path— canonical /.//// backslash / blank / separators-onlyTestNormalizeSourcePath(6 tests)normalize_source_path— fixed-point regressionsTestNormalizeSourcePathReachesAFixedPoint(4 + idempotence)normalize_source_path—..left intact (negative)test_parent_traversal_is_left_intactextract_document_paths— two spellings collapsetest_two_spellings_of_one_document_collapse_to_onetest_at_least_one_harness_existstest_harness_holds_on_its_whole_corpusfuzz_yaml_frontmatter.consumeover 10 corpus inputs/, no leading./, non-empty, idempotentfuzz_source_path.consumeover 10 corpus inputs[tool.uv] constraint-dependencies— 3.10 fork resolves an installable onnxruntimepip install --dry-run --require-hashespairs on linux/amd64 (table below)generate_pip_constraints.py --check→requirements OK (13 checked)check_no_conflict_markers— labelled marker foundtest_conflict_markers_are_reported_with_path_and_linecheck_no_conflict_markers— clean file → no failuretest_a_clean_file_reports_nothingcheck_no_conflict_markers— Markdown setext=======NOT flagged (negative)test_a_markdown_setext_underline_is_not_a_conflict_markercheck_no_conflict_markers— missing file fails closedtest_a_missing_scanned_file_fails_closedcheck_scanned_json_parses— invalid JSON reportedtest_unparseable_json_is_reportedcheck_scanned_json_parses— valid JSON → no failuretest_valid_json_reports_nothingcheck_scanned_json_parses— non-JSON member skipped (negative)test_markdown_is_not_json_checkedcollect_failurestest_both_checks_run_inside_collect_failurestest_the_real_repository_tree_is_structurally_clean§13.1 checklist
A. Correctness & behavior
6421 passed, 5 skipped, 117 subtests passed in 653.86s (0:10:53)(6421 + 5 = the 6426 collected). CI agrees on the exact pushed tree:collected 6426 tests→doc claims OK→badges OK (5 checked)(job 90574807372).--require-hashesinstall proven end-to-end (ruff 0.15.20installed fromrequirements/lint.txt, exit 0).--require-hashesis the validation. Adversarial case tested directly: every hash corrupted → pip exits 1 with "THESE PACKAGES DO NOT MATCH THE HASHES". A single corrupted hash is correctly not enough (pip accepts if any listed hash matches) — the first attempt at this test was invalid for that reason and was redone.--checknever writes; generation idempotent;normalize_source_pathidempotent (the property the bug violated).write()no-ops when content matches.B. Concurrency — N/A: single-shot synchronous scripts, no shared mutable state, no async. CI jobs are independent.
C. Resources & performance
read_bytes. Image size falls — 18 nvidia/cuda packages + triton removed from every container.D. Security — this is the point of the change
subprocess.runuses a fixed argv, no shell, no user input (# noqa: S603with that rationale).permissions: read-allon the fuzz workflow. Net effect: one unreviewed remote script no longer executes as root at build time, and every installed artifact is hash-verified. The one--extra-index-urladded is documented as safe only because the file is hash-pinned — an artifact from either index that isn't the locked one fails the hash check before unpacking.E. Interfaces & compatibility
requirements/, new groups. No public API change; PEP 735 groups are not published, so PyPI consumers see identical metadata.ci.yml(all 7 sites),release.yml(2),scripts/setup.sh,Dockerfile,docker/Dockerfile,.devcontainer/Dockerfile,.clusterfuzzlite/build.sh.extract_document_pathsandtests_py/handlers/test_wiki_page_sources.pyre-verified against the changed normalisation (637 passed)..gitattributeshandles the cross-platform EOL case explicitly. The Windows CI leg installsci-sqlite-min.txt, which resolves without a tree-sitter toolchain by design.F. Observability & operations
run scripts/generate_pip_constraints.py); asserted bytest_drift_message_names_the_regeneration_command. Success printsrequirements OK (13 checked).setup.shno longer swallows pip's stderr.G. Tests
git stash-ing the fix and re-running the replayer (it failed onrepro-backslash-mixed, then passed once restored).test_no_editable_or_directory_requirements,test_no_committed_file_is_orphaned,test_parent_traversal_is_left_intact, corrupted-hash rejection.ruff check .→ All checks passed!;ruff format --check .→ 1064 files already formatted;check_doc_claims.pyandgenerate_pip_constraints.py --checkboth green.H. Code quality & delivery
S603fixed argv;PLC0415atheris has no wheel for macOS/aarch64).--upgrade pip buildremoved from the root image: it was itself an unpinned install andbuildwas never invoked there.check_doc_claims.py,generate_repo_badges.py— same fail-closed--checkidiom).ruff format(formatting only), count refresh, CHANGELOG, action bumps.Test (Python 3.10)now passes.docker/Dockerfile,setup.sh's swallowed stderr and drifted package list, the non-idempotent path canonicaliser, the CRLF-corruptible corpus, andhatchlingbeing fetched unpinned during build isolation. No unfixed, un-issued seen defect remains.Resolvability matrix — every (requirements file, python) pair its consumers use
pip install --dry-run --require-hashes, run on linux/amd64 (CI's platform;this host is arm64, so a same-arch check would have validated a different wheel
set than CI resolves). Before the fix, the first row fails with
No matching distribution found for onnxruntime==1.24.3— reproduced, thenre-run to green.
17/17.
fuzz.txtis verified on 3.12 only and by design: atheris publishesmanylinux x86_64 wheels for cpython 3.12–3.14 and nothing else, so that set does
not resolve elsewhere and is not expected to.
Scorecard — measured, not assumed
Real scanner (
gcr.io/openssf/scorecard:stable) against this working tree,re-run after the action bumps to prove they did not regress the pinning:
Both checks close only at max score, and both are at max. The PR's own added
lines were also swept against the checker's unpinned-install predicates: every
pip installadded by this diff carries--require-hashes, or-eplus--no-deps; no newcurl | sh, no unversionednpm install.Gates run on the merged tree
ruff format --check .1067 files already formattedruff check .All checks passed!python -m pyright mcp_server/0 errors, 0 warnings, 0 informationspytest6421 passed, 5 skipped, 117 subtests passed in 653.86sgenerate_pip_constraints.py --checkrequirements OK (13 checked)check_doc_claims.py --test-count 6426doc claims OKgenerate_repo_badges.py --check --test-count 6426badges OK (5 checked)The pyright run is quoted from a venv built exactly as CI builds it
(
ci-typecheck.txt+-e . --no-deps+typecheck-tool.txt). A first attemptusing the test env reported 4
reportMissingImportserrors onmcp_server/infrastructure/otel_exporter.py— that env simply lacks the[otel]extra, and an unresolved import makes a type-checker report fewer real errors,
not more. Quoted only after the environment could resolve the imports.
Still measured by CI rather than locally, and why
Stated plainly rather than implied green:
Colima on arm64 with no buildx, so
--platform linux/amd64is not across-build: the legacy builder installs
aarch64toolchains and then failsthe platform assertion. The root Dockerfile builds clean natively
(
Successfully tagged cortex:pin-check), and the authoritative amd64evidence is this PR's own green Docker Build (runtime image), Docker
Build (devcontainer image) and Docker Smoke jobs.
wheel. The harness properties run locally through the corpus replayer inside
the ordinary pytest suite; the coverage-guided campaign is the green
Fuzz (PR batch) jobs.
mainafter merge. Scorecard's alertsare written by the
scorecard.ymlrun on the default branch, so the 22 → 0count must be read there, never off this PR ref. The 10/10 above is the
scanner's verdict on this tree, which is the strongest pre-merge evidence
available.
Nothing deferred
No un-issued deferral remains. The Node 20 deprecation — the one item the
previous body carried as "unrelated, pre-existing" without an issue number — is
fixed in this PR: all 16 pinned action refs now sweep to 0 on node20, each
still SHA-pinned and each verified by reading
runs.usingout of itsaction.ymlat the target SHA rather than trusting the tag.codeql-actionneeded the v4 line, since v3.37.3 is still node20.
🤖 Generated with Claude Code
https://claude.ai/code/session_012Yu6EnWspTfqHoGkExyS6u