RFC: Project-level skills lock file#80
Conversation
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
JAORMX
left a comment
There was a problem hiding this comment.
Panel review — cross-cutting summary
Conducted a three-axis panel review (spec / standards / domain) with specialist reviewers (secure-code-reviewer, software-architect, library-reuse-reviewer, devex-reviewer, ux-reviewer) plus verified research against go.sum, Cargo.lock, npm, pnpm, and poetry.lock. All factual claims were verified against source code/docs before posting. Inline comments below cover location-specific findings; this comment captures the cross-cutting picture and the non-location-specific ones.
Overall
The RFC is well-researched and consciously cites the right prior art. It correctly avoids several npm mistakes (pins content digests not registry names, refuses to rewrite source, treats --prune as opt-in, hard-errors on unknown schema versions). But it inherits npm's exact supply-chain gaps while claiming parity with go.sum/Cargo.lock — and go.sum/Cargo.lock each close those gaps with an external trust root (GOSUMDB / curated registry) that this design lacks. For skills (instructions an AI executes), the stakes are higher than inert code.
Critical findings (inline comments posted)
-
No binding between
sourceanddigest— a committed digest is an assertion, not a verification. A tampered PR can pin any digest that passes format/name validation, andsyncfaithfully installs malicious AI instructions. go.sum has GOSUMDB (Trillian Merkle transparency log, Google-operated); Cargo.lock has crates.io's curated content-addressed registry. This RFC has neither. -
SHA-1 git commit hashes accepted for a new 2026 supply-chain system — all three comparators (go.sum SHA-256, Cargo SHA-256, npm SHA-512) avoid SHA-1 for content integrity. The "inherited from git" defense doesn't hold for a new system when git offers sha256 and no comparator uses SHA-1.
-
Transitive skill dependencies (
toolhive.requires) not locked — two machines with identical locks can install different transitive skill content. Every mature ecosystem (npm v2/v3, Cargo.lock) locks the full transitive graph. This is the canonical npm reproducibility mistake. -
sync/--checkcompares against SQLite, not re-hashed content —npm cire-hashes files against the lock;go mod verifyre-hashes the module cache. This RFC's CI gate can pass on tampered installs. For prompt-instruction artifacts, undetected on-disk tamper is high-impact. -
syncis a one-command prompt-injection amplifier with no pre-install gate — the RFC's own threat model names this, but the mitigation is post-install reporting, not pre-install gating. For AI-instruction artifacts, reporting after install is too late.
High findings (inline comments posted)
sourcere-resolution on upgrade is a catalog-redirect/typosquat vector (TOCTOU) — same shape as npm'sresolvedURL malleability.- No signature/transparency log/provenance in v1 — "future Sigstore" is a forward pointer, not a v1 control. THV-0030:472 commits to Sigstore; this RFC never reconciles.
sync --check≠cargo --locked— the analogy oversells the guarantee. There's no command that catches a stale lock (dev bumped catalog, forgot to commit lock update).
Medium findings (inline comments posted)
SkillServiceGo interface break is real — verified: exported interface with implementers intoolhive-git-skillsandstacklok-enterprise-platform. "If any exist" should be "yes." §Compatibility's "fully backward compatible" contradicts §API Changes.- Best-effort lock write → silent stale lock / silent downgrade on sync — Cargo makes lock write atomic with install (fails the build). This RFC's "warn and continue" is strictly weaker.
--dry-runis a misnomer — fetches to cache/clones. cargo/npm/git--dry-runare side-effect-free.--pruneconflates "never-managed" with "removed-from-lock-by-tampered-PR" — both collapse tounmanaged; a habitualsync --prunedeletes legitimate skills indistinguishably.projectRootvalidation underspecified at the trust boundary (CWE-22) — "validated the same way existing paths are" is vague for the one new API-body trust boundary.failedcollapses unrecoverable/retryable/policy-rejected causes — callers can't branch; exit codes undocumented.- General project lock defined under a skills RFC — THV-0077 has zero mention of
toolhive.lock.yaml; this RFC silently commits it to a contract. Layering violation. - Lockfile discovery breaks on worktrees —
.gitis a file, not a dir, in worktrees. Usegit rev-parsesemantics. - No
--adoptpath — first-run treats existing installs asunmanaged(the same flag--prunedeletes).
The top three highest-leverage fixes (per the operator's stated priority: avoid npm, adopt Go/Rust rigor)
- Lock transitive dependencies (write lock entries for materialized
toolhive.requiresskills with arequiredByprovenance marker — Cargo.lock records, doesn't resolve). - Make
syncre-hash content + always-on digest verification on every install path including cache hits (go mod verify/npm cirigor). - Add a
publisher/provenance field with change-detection onupgrade— a cheap, local mitigation for the event-stream/ua-parser-js class ("digest correct, content malicious") that needs no Sigstore in v1, while explicitly naming GOSUMDB-style transparency logging + Sigstore (THV-0030) as the v2 trust layer.
Verified comparators (sources cited in inline comments)
| Mechanism | Go | Cargo | npm | RFC-0080 |
|---|---|---|---|---|
| Hash algorithm | SHA-256 (h1:) |
SHA-256 (tarball) | SHA-512 (SRI) | SHA-256 (OCI) or SHA-1 (git) ❌ |
| Transitive deps locked | ✅ | ✅ | ✅ (v2/v3) | ❌ |
| Re-hash on verify | ✅ (go mod verify) |
✅ (registry check) | ✅ (npm ci) |
❌ (SQLite only) |
| External trust root | ✅ GOSUMDB (Trillian) | ✅ curated registry | ❌ | |
| Lock-write atomic | ✅ | ✅ (fails build) | ✅ | ❌ (best-effort warn) |
| CI frozen gate | ✅ -mod=readonly |
✅ --locked/--frozen |
✅ npm ci |
--check (weaker) |
| Executable code on install | N/A | N/A | ❌ undefined for skill scripts |
Happy to drill into any specific finding or help draft the source→digest binding design.
Co-authored-by: Cursor <cursoragent@cursor.com>
|
@JAORMX Thanks for the thorough panel review — I've pushed a revision (
Happy to iterate further, especially on v2 trust-layer scope or POC alignment in toolhive#5715. |
|
|
||
| ## Summary | ||
|
|
||
| Add a project-level lock file (`toolhive.lock.yaml`) that pins the name, version, source, resolved reference, checkout digest, and content digest of every project-scoped skill install (including transitively materialized dependencies), plus `thv skill sync` and `thv skill upgrade` commands to restore pinned state and pull newer content from the catalog. The lock borrows the *shape* of `package-lock.json`, `Cargo.lock`, and `go.sum` (identity, source, integrity in a committed file) but does **not** claim their external trust roots in v1: the trust root for a committed pin is PR review of the lock diff, with a v2 milestone for Sigstore signing (per THV-0030) and transparency-log-style source-to-digest binding. |
There was a problem hiding this comment.
I know you're explicitly calling out a v1 and v2, but can we just do v1? I'd really want to ship this with signing to begin with.
There was a problem hiding this comment.
I was trying to defer this to a future update because I don't know much about Sigstore, but I'll try to do a full deep dive on it
There was a problem hiding this comment.
@JAORMX Yes — pushed cd35106 folding Sigstore into v1. The v2 trust-layer milestone is gone.
What ships in v1:
- Sign on publish:
thv skill pushkeyless Sigstore signing (Fulcio + Rekor, OCI referrer);--keyfor key-based setups. Catalog skills signed in CI (coordinated workstream). - Verify on consume: install/sync/upgrade verify signatures by default. Unsigned artifacts rejected unless
--allow-unsigned(recorded asunsigned: truein the lock, visible in diffs). - Identity pinning: each lock entry carries a
provenanceblock (signerIdentity,certIssuer, optionalrepositoryUri/sigstoreUrl) — mirrors the existing registry provenance shape. Catalog-first on first install, TOFU for direct refs. Upgrade refuses signer changes without--allow-signer-change. - Git sources: gitsign verification on pinned commits (unsigned git still needs
--allow-unsigned). - Offline CI: Sigstore bundles stored with install state;
sync --checkre-hashescontentDigestand re-verifies bundle + locked identity offline (hermetic, no Rekor round-trip). - Implementation: extends toolhive-core
container/verifier(sigstore-go, same stack as MCP image verification).
This directly closes the unauthenticated-trust-root finding via signature + pinned publisher identity + Rekor transparency, rather than the earlier "PR review only until v2" framing. See §Trust model, §Signing on publish, §Verification on consume, and Phase 5 in the Implementation Plan.
Happy to walk through any of the enforcement edge cases (--allow-unsigned, catalog-first vs TOFU, gitsign scope).
Co-authored-by: Cursor <cursoragent@cursor.com>
Summary
Proposes a project-level lock file (
toolhive.lock.yaml) that pins the name, version, source, resolved reference, and digest of every project-scoped skill install, bringing skills the reproducibility guarantees thatpackage-lock.json,Cargo.lock, andgo.sumprovide elsewhere.thv skill install --scope projectleaves no shareable, version-controlled pin, so teammates cloning a repo get whatever the catalog currently serves.skills:today, room forplugins:later).thv skill sync— restore the pinned skill set (drift reported distinctly;--checkgives CI anpm ci-style gate;--pruneremoves unmanaged skills).thv skill upgrade— re-resolve each entry's original source and rewrite the pin on digest change; immutable pins (OCI digests, full commit hashes) are never re-resolved.POC implementation: stacklok/toolhive#5715