ci: pin our own MCP tool surface with truecopy#24
Conversation
support/dump-tools.mjs writes the server's real tools/list response to mcp-manifest.json (in-memory MCP client, sorted, stable) with a --check mode; the manifest is pinned in truecopy.lock and gated in CI by askalf/truecopy-action@v1.0.1 (SHA-pinned). Chain: code -> manifest (--check) -> lock (verify). A changed tool name, description, or generated schema fails the gate until consciously re-pinned.
sprayberry-reviewer
left a comment
There was a problem hiding this comment.
Automated review from the Sprayberry Labs fleet code reviewer (sprayberry-reviewer bot). This is a verdict-bearing automated review.
Verdict: no blocking issues — approving. 4 new files, 200 additions, 0 deletions. The supply-chain gate is correctly wired and CI is all green.
What I checked
support/dump-tools.mjs
The dump script correctly:
- Builds a real in-memory MCP client over
InMemoryTransport.createLinkedPair(), which produces the exacttools/listwire response a real MCP client would see — not a synthetic reconstruction. - Sorts tools by name with
tools.slice().sort((a, b) => a.name.localeCompare(b.name))for a stable, deterministic output. All three current tool names are ASCII (canon_scan,keeper_lease,warden_check), so locale-sensitivity inlocaleCompareis a non-issue in practice. - The
--checkcomparison (lines 38–43) normalises\r\n→\nbefore comparing torendered(whichJSON.stringifyalways emits with\n). Edge case handled correctly. - If the manifest file is absent in
--checkmode,committedbecomes'', which won't match a non-empty rendered JSON — exits 1 as expected. - Path resolution via
fileURLToPath(import.meta.url)is the correct ESM idiom.
.github/workflows/truecopy-gate.yml
- All three third-party actions are SHA-pinned:
actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0askalf/truecopy-action@5c6fcf00edb83ff4846206392722292d45fee227 # v1.0.1
permissions: read-allandpersist-credentials: falseare correct hardening for a security-tooling repo.- Step ordering is correct:
npm ci→dump-tools --check(manifest == code) →truecopy-action verify(manifest == pinned lock). A change to tool name, description, or schema fails step 2; a tampered manifest-on-disk that bypasses--checkwould fail step 3. The two gates catch complementary failure modes.
mcp-manifest.json
Cross-checked the three tool schemas against the definitions in mcp.mjs:
warden_check:inputisz.record(z.any()).optional()in code → onlytoolinrequired[]in manifest ✓keeper_lease:host,ttlS,usesall optional in code → onlynameinrequired[]in manifest ✓canon_scan:manifestis required in both ✓
CI's dump-tools --check step passing at the PR head is the authoritative proof that the committed manifest matches the live server output.
truecopy.lock
Lock file carries per-tool content hashes and a (manifest) hash for the whole file. verdict: "clean" with findings: 0 from redstamp 0.4.1. The CI truecopy-action verify step (14 s, green) confirms the lock is internally consistent.
CI status (all green at PR head)
CodeQL / analyze (javascript-typescript)— passtest— passverify pinned tool surface— pass (14 s)
Minor (non-blocking)
truecopy-gate.ymlline 31: thetruecopy-actionstep has nowith:block specifying the action verb. The step name says "truecopy verify" and CI confirms it runs correctly, but an explicitwith: { action: verify }(or whatever the action's input key is) would make the intent self-documenting without needing to consult the action's README.support/dump-tools.mjsline 35:await client.close()is called butserver.close()is not. For a one-shot script that exits immediately after, this is harmless — the OS reclaims everything. Worth knowing if this script is ever adapted for use inside a longer-lived process.
What's good
Dogfooding the project's own supply-chain tool on its own tool surface is exactly the right move for a security-tooling repo. The two-stage gate (code→manifest, manifest→lock) closes the obvious escape hatch where a committed manifest could be manually forged — you'd need to forge a hash that the truecopy verifier accepts. The script uses the real MCP wire protocol rather than introspecting exported metadata directly, which means schema-generation side effects from dep bumps are caught automatically. Clean work.
Why
The security stack should be gated by its own supply-chain tool. This pins the composed server's actual MCP tool surface — the
tools/listwire response — with truecopy, and verifies it in CI with the published truecopy-action.What
support/dump-tools.mjs— connects a real MCP client over an in-memory transport and writes the server's livetools/listoutput tomcp-manifest.json(sorted, stable).--checkmode exits 1 if the committed manifest no longer matches the code.mcp-manifest.json— the three trilogy tools (canon_scan,keeper_lease,warden_check) with their titles, descriptions, generated JSON schemas, and annotations.truecopy.lock— the manifest pinned (truecopy add), per-tool hashes + redstamp detection provenance..github/workflows/truecopy-gate.yml— CI chain:npm ci→dump-tools --check(manifest == code) →askalf/truecopy-action@v1.0.1(SHA-pinned)verify(manifest == pinned bytes).Any change to a tool name, description, or schema — including a dep bump that changes the SDK's generated schemas — fails the gate until the manifest is regenerated and consciously re-pinned. That's the poisoned-tool-definition failure mode this stack exists to catch, enforced on itself.
Worth noting: the scan verdict on our own manifest is clean with zero findings — tool descriptions that talk about exfiltration, secrets, and poisoning are exactly the security-tooling false-positive class from our marketplace studies, and the severity tiering handles them without
--force.Verified locally: dump →
--check→scan(clean) →add→verify(all 1 pinned skills verified) all round-trip.🤖 Generated with Claude Code