Skip to content

feat: mcpm verify + GitHub Action — repo-only lockfile integrity gate (D2)#122

Merged
m1ngshum merged 2 commits into
mainfrom
feat/mcpm-verify-d2
Jul 3, 2026
Merged

feat: mcpm verify + GitHub Action — repo-only lockfile integrity gate (D2)#122
m1ngshum merged 2 commits into
mainfrom
feat/mcpm-verify-d2

Conversation

@m1ngshum

@m1ngshum m1ngshum commented Jul 3, 2026

Copy link
Copy Markdown
Member

What

Wave-1 item D2 from docs/ROADMAP-ADOPTION.md: a repo-only, client-free CI gate. The critique's premise held — none of the existing commands run on a hosted runner (up hard-fails at Step 3 on zero detected clients; sync --check/audit are vacuously green there) — so mcpm verify is a genuinely new path, not a thin wrapper.

Changes

  • src/stack/frozen-verify.ts (new, shared)classifyIntegrity (moved from up.ts) + a new pure frozenVerdict (the structured pass/block decision). Both are client-free and output-free.
  • up --frozen rewired to consume them. Its output and block matrix are byte-identical — the 13 exact-string frozen tests pass unchanged (they're the refactor's safety net).
  • mcpm verify [--json] — loads mcpm-lock.yaml and runs that same fail-closed pass with the same BLOCK semantics + exit codes as up --frozen:
    • exit 1 on integrity drift / unverifiable record / format mismatch / suspicious mixed missing-baseline
    • benign refuse-to-run on a lock-wide no-baseline (never fetches)
    • pypi/oci/url reported as unenforceable (notice, never a block)
    • exit 1 on no lock file — and verify never auto-locks (a deterministic gate)
    • no client detection, no ~/.mcpm reads, no writes
    • --json emits a structured VerifyModel (schemaVersion: 1)
  • Composite Action .github/actions/mcpm-verify — wraps mcpm verify, writes a job step summary from --json, ships a README with a static badge + pre-commit snippet.
  • completions (bash/zsh/fish) + the completions↔Commander invariant test updated for the new command.

Design notes

  • ONE verb (cross-cutting Add disable, enable, completions, and alias commands #2): B3 later extends mcpm verify with Sigstore provenance — integrity now, provenance later, never two meanings.
  • v1 scope = npm dist.integrity. Stack-vs-lock staleness (declared server missing from the lock) is a deferred follow-up — the first slice is exactly the extracted --frozen integrity pass per the roadmap.
  • Honesty boundary (inherited from F3): a block means npm's published record diverged from your lock — not that mcpm caught malicious bytes. Asserted by a test (not /different bytes|you are protected|.../).

Tests & verification

  • verify.test.ts (11 new): the full block matrix (equal / drift / could-not-verify / format / lock-wide-no-baseline / mixed-gap / pypi-only / no-lock), honesty copy, --json (block + clean). Tests verifyHandler directly with injected parseLock/fetchNpmIntegrity — no temp files, no clients.
  • up-frozen.test.ts (13) unchanged and green → the extraction is behavior-preserving.
  • Full suite 1940/1940 (FORCE_COLOR=0); typecheck + lint clean.
  • End-to-end on the built CLI: pypi-only → coverage note + exit 0; --json clean; no-lock → exit 1. Action step-summary script validated against both ok + blocked --json.

Docs

CHANGELOG [Unreleased] (Added + Internal), CONTRACTS (mcpm verify exit-code row + verify in the --json list), ROADMAP-ADOPTION (D2 → ✅ SHIPPED), README (command row + a CI section), CLAUDE.md (commands list + decisions log).

m1ngshum added 2 commits July 3, 2026 18:28
… (D2)

None of the existing commands run on a hosted CI runner (up hard-fails on zero
detected clients; sync --check/audit are vacuously green there), so verify is a
genuinely new client-free path — not a thin wrapper.

- Extract classifyIntegrity + a new pure frozenVerdict from up.ts into
  src/stack/frozen-verify.ts. up --frozen now consumes them; its output and
  block matrix are byte-identical (13 frozen tests pass unchanged).
- mcpm verify [--json]: loads mcpm-lock.yaml and runs that same fail-closed
  integrity pass with the same BLOCK semantics + exit codes as up --frozen
  (exit 1 on drift / unverifiable / format mismatch / mixed missing baseline;
  benign refuse on a lock-wide no-baseline; pypi/oci/url reported unenforceable;
  exit 1 on no-lock — and verify NEVER auto-locks). No client detection, no
  ~/.mcpm, no writes. --json emits a structured VerifyModel.
- Composite Action .github/actions/mcpm-verify: wraps verify, writes a job step
  summary from --json, static badge + pre-commit snippet in its README.
- completions (bash/zsh/fish) + the invariant test updated for the new command.

ONE verb: B3 later extends mcpm verify with Sigstore provenance, never forks it.
v1 scope = npm dist.integrity; stack-vs-lock staleness deferred.

1940 tests green; verified e2e on the built CLI.
…s (review MEDIUM)

Review follow-ups on the D2 integrity gate:
- verifyHandler now wraps its whole body in try/catch: a malformed / Zod-invalid
  lock (parseLock throws) — a plausible CI trigger via a bad merge or hand-edit —
  now resolves to a structured fail-closed error model + exit 1 through verify's
  OWN output (colored text / --json), instead of only exiting 1 via the global
  index.ts catch. Makes the exported handler total (never throws) for programmatic
  reuse. New test asserts it resolves to 1, not rejects.
- up.ts's frozen block-message switch gains a default exhaustiveness guard
  (const _never: never), so a future 5th FrozenBlock reason fails the build rather
  than silently emitting undefined in a security-relevant block message (parity
  with verify.ts's already-exhaustive REASON_PHRASE Record).
@m1ngshum m1ngshum merged commit b4818fa into main Jul 3, 2026
8 checks passed
@m1ngshum m1ngshum deleted the feat/mcpm-verify-d2 branch July 3, 2026 10:38
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