Verify skill signatures at install time - #6129
Conversation
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## skills-sig/10b-gitsign-verify #6129 +/- ##
=================================================================
- Coverage 72.41% 72.40% -0.02%
=================================================================
Files 738 739 +1
Lines 76054 76234 +180
=================================================================
+ Hits 55074 55194 +120
- Misses 17069 17116 +47
- Partials 3911 3924 +13 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
JAORMX
left a comment
There was a problem hiding this comment.
Panel review posted as inline comments (one critical correctness bug, plus verification-ordering and UX items). Context: reviewed this PR together with its stack (#6091, #6121) against RFC THV-0080 and tracking issue #5899; cross-cutting items are noted in-line where they live.
The headline finding: --allow-unsigned is non-functional for CLI users — the flag is added here but the HTTP client DTO (pkg/skills/client/dto.go) never carries it to the server, so every CLI --allow-unsigned install is rejected with a 403 telling the user to pass the flag they already passed. Details inline on cmd/thv/app/skill_install.go.
JAORMX
left a comment
There was a problem hiding this comment.
Inline comments from the panel review. Recap, by severity:
- Critical:
--allow-unsignednever reaches the server —pkg/skills/clientDTO drops the field (seecmd/thv/app/skill_install.go). Every CLI user of the headline flag gets a 403 telling them to pass the flag they passed. Two-line fix + a client round-trip test. - Medium: TOFU race — lock read before per-skill mutex in both
installFromGitandinstallFromOCI(installByNamehas the correct order to copy). - Medium: sync trusts
unsigned: truewithout re-verifying — theprovenance:→unsigned: truelock-diff downgrade path should at least be documented as security-relevant review surface. - Medium (UX): API error strings name the CLI flag;
signer-mismatchis a dead-end error; TOFU pins the identity silently (RFC says "displayed prominently"). - Medium (design): the
result.Provenance/Unsignedfold-back defeats opts-by-value in a way that silently drops the next decision field — consider threading*InstallOptionsbefore PR12 grows the decision shape.
Also noted for housekeeping: this PR is ~437 lines of code changes across 12 non-test/doc/generated files (over the 400/10 guideline in .claude/rules/pr-creation.md — a natural split is the API/CLI plumbing vs the service-layer verification), and two commit subjects exceed the 50-char limit ("Update lock E2E for install-time verification", "Pass allow_unsigned through the skills install API").
Project-scoped installs now verify artifact signatures before anything is extracted or recorded (RFC THV-0080): OCI artifacts through the Sigstore keyless flow, git commits through gitsign verification, both against the identity recorded in the project's lock file. On first use the observed identity is recorded (trust on first use); later installs enforce it inside the verifier. Verification runs under the per-skill mutex so concurrent first installs cannot race their TOFU anchors. Unsigned artifacts are rejected unless the caller sets allow_unsigned, which records an explicit "unsigned: true" exception in the lock entry; an entry locked to a signer identity refuses unsigned or local-build replacements outright. Lock-driven operations (sync restores, upgrade re-pins) honor the trust state the entry already records — a lock diff converting provenance to unsigned is therefore a reviewable trust downgrade, called out in the code. Verified installs persist the Sigstore bundle with the DB record for offline re-verification during sync. The installers receive install options by pointer so the verification decision reaches the lock-recording step directly, with no parallel result fields to forget. Failures classify to typed reasons via errors.Is on the verifier's sentinels. Part of #5899. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The unsigned-install exception must reach the service from every surface: the CLI flag, the HTTP client DTO (without which the flag would silently never reach the server — pinned by a round-trip test), and the API request type, with error messages worded for both surfaces. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
961b9c2 to
86f35b1
Compare
Note
Stack 2 of RFC THV-0080 (tracking issue #5899) — stacked on #6121 (gitsign verification), which stacks on #6091 (verifier wrapper). This is the highest-risk PR of the stack (it changes install behavior for project scope) — flagged for extra review scrutiny per the plan.
Summary
This PR turns the verification machinery built by the previous PRs into enforced install-time policy. Project-scoped installs (with the lock file feature enabled) now verify artifact signatures before anything is extracted or recorded:
provenance:in the lock entry and the Sigstore bundle is persisted with the DB record (for sync's offline re-verification). Subsequent installs feed the recorded identity into the verifier's Sigstore policy — mismatch is a 403 with the prior lock state untouched.--allow-unsigned(CLI + existing API field) records an explicitunsigned: trueexception. An entry already locked to a signer identity refuses unsigned or local-build replacements outright — even with the flag — since that substitution is exactly what the lock exists to catch.signature-invalid/signer-mismatch/unsigned-rejectedfailure reasons, classified viaerrors.Ison the verifier's sentinels;classifySyncFailureconsumes them.User-scoped installs and gate-off environments are completely unaffected (verified by test — the verifier mock has zero expectations there).
Type of change
Test plan
task test, full suite with -race)task lint-fix, 0 issues)unsigned: truerecorded); signer mismatch rejected with prior lock state intact; locked-unsigned requires the flag again on reinstall (verifier not consulted); user scope skips verification entirely; local-install matrix (no flag / flag / locked identity refuses local replacement / locked unsigned honored); failure-reason classification.task docsregenerated (--allow-unsignedflag docs).Does this introduce a user-facing change?
Yes, behind the experimental
TOOLHIVE_SKILLS_LOCK_ENABLEDgate: project-scoped installs of unsigned skills are now rejected unless--allow-unsignedis passed, and signed skills are verified against the lock file's recorded signer identity. User-scoped installs are unchanged.Special notes for reviewers
optsis a by-value copy), so it travels back onInstallResult(json:"-"fields) and is folded into the lock-recording step ininstallAndRegister— the one construction point both OCI and git flows share.WithVerifierconstructor option.Generated with Claude Code