Check first skill install against catalog-declared provenance - #6420
Merged
Merged
Conversation
samuv
requested review from
ChrisJBurns,
JAORMX,
amirejaz,
jhrozek,
rdimitrov and
tgrunnagle
as code owners
August 24, 2026 14:43
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #6420 +/- ##
==========================================
+ Coverage 77.71% 77.77% +0.05%
==========================================
Files 759 759
Lines 72889 72979 +90
==========================================
+ Hits 56646 56756 +110
+ Misses 16238 16218 -20
Partials 5 5 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
rdimitrov
previously approved these changes
Aug 24, 2026
Needed for the Skill.Provenance field (#6310 prerequisite) landing in the next commit. v0.0.42 also migrated toolhive-core's cel package to the renamed cel.dev/cel-go module; update the two callers that pass it EnvOptions to match, or the build breaks. Signed-off-by: Samuele Verzi <samu@stacklok.com>
Trust-on-first-use had nothing to verify a skill's very first install against — the observed signer identity was simply recorded, never checked. When a skill resolves through the registry/catalog and that entry declares an expected provenance, use it as the first-install expectation instead of blind trust; a lock entry, once one exists, still always wins. A catalog-declared attestation constraint is refused rather than silently skipped: the skill verifier has no attestation-comparison logic today, so honoring identity while ignoring attestation would let a catalog author believe a guarantee is enforced when it isn't. Closes #6310 Signed-off-by: Samuele Verzi <samu@stacklok.com>
toolhive-core's Skill type gained a Provenance field in the v0.0.42 bump, and it's exposed through the registry API — task docs was missed for this in the prior commit. Signed-off-by: Samuele Verzi <samu@stacklok.com>
Signed-off-by: Samuele Verzi <samu@stacklok.com>
Signed-off-by: Samuele Verzi <samu@stacklok.com>
Signed-off-by: Samuele Verzi <samu@stacklok.com>
samuv
force-pushed
the
skills-catalog-provenance
branch
from
August 24, 2026 18:18
e1c5ca1 to
d4edb31
Compare
rdimitrov
approved these changes
Aug 24, 2026
samuv
added a commit
that referenced
this pull request
Aug 25, 2026
The verifier interface gained a ProvenanceExpectation wrapper on main (#6420) so it can tell a strict lock pin from the independently-optional catalog constraints. The rebase merged textually clean but stopped compiling; plugins only ever present a lock expectation today, and NewLockExpectation(nil) is nil, so the trust-on-first-use case is unchanged. Part of #6300. Signed-off-by: Samuele Verzi <samu@stacklok.com>
10 tasks
samuv
added a commit
that referenced
this pull request
Aug 25, 2026
The verifier interface gained a ProvenanceExpectation wrapper on main (#6420) so it can tell a strict lock pin from the independently-optional catalog constraints. The rebase merged textually clean but stopped compiling; plugins only ever present a lock expectation today, and NewLockExpectation(nil) is nil, so the trust-on-first-use case is unchanged. Part of #6300. Signed-off-by: Samuele Verzi <samu@stacklok.com>
3 tasks
samuv
added a commit
that referenced
this pull request
Aug 26, 2026
* Verify plugin signatures at install time Project-scoped plugin 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, which plugins reuse from pkg/skills/verifier so the pinned ref/runner checks come along too. Verification runs under the per-plugin mutex so concurrent first installs cannot race their TOFU anchors, and is scoped to installs that record lock state — including the plugins lock feature gate, since a disabled lock file has nowhere to anchor trust. 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. Unlike skills, a local-store upgrade deliberately clears resolvedReference, so ExpectedCanonicalName joins the lock-driven markers. Verified installs persist the Sigstore bundle with the DB record for offline re-verification during sync. The unsigned exception reaches 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. Failures classify to typed reasons via errors.Is on the verifier's sentinels. Part of #6300. Signed-off-by: Samuele Verzi <samu@stacklok.com> * Adapt plugin verify to ProvenanceExpectation The verifier interface gained a ProvenanceExpectation wrapper on main (#6420) so it can tell a strict lock pin from the independently-optional catalog constraints. The rebase merged textually clean but stopped compiling; plugins only ever present a lock expectation today, and NewLockExpectation(nil) is nil, so the trust-on-first-use case is unchanged. Part of #6300. Signed-off-by: Samuele Verzi <samu@stacklok.com> --------- Signed-off-by: Samuele Verzi <samu@stacklok.com>
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.
Summary
Closes #6310. Trust-on-first-use had no expected identity to check a
skill's very first install against — the verifier only recorded whatever
signer identity it observed, never validated it against anything. Every
install after that was enforced against the recorded lock entry, but the
first one was taken on faith.
toolhive-core v0.0.42 added
Provenancetoregistry.Skill(stacklok/toolhive-core#247, #248, #251) so a catalog entry can declare who
is expected to sign a given skill. This wires that into install-time
verification:
thv skill install code-review, not a direct OCI or git reference) andthe resolved entry declares a
provenance, it becomes the expectedidentity for verification — but only on a true first install. A lock
entry, once one exists, always takes precedence over the catalog; the
catalog only fills the gap before any trust decision has been recorded.
other provenance field's "absent means unconstrained" contract — most
catalog entries won't declare one for a while, and that must not block
installs.
than silently skipped:
pkg/skills/verifierhas no attestation-comparisonlogic today (unlike toolhive-core's own generic
Sigstore.VerifyServerpath used for MCP servers), so honoring the identity fields while quietly
ignoring
attestationwould let a catalog author believe a guarantee isenforced when it isn't.
Also bumps
toolhive-coreto v0.0.42 as a separate commit (required for theSkill.Provenancefield) — that release also migrated toolhive-core'scelpackage to the renamed
cel.dev/cel-gomodule, which broke the two CEL callsites in this repo (
pkg/auth/awssts,pkg/authserver/server/tokenexchange)that pass it
EnvOptions from the oldgithub.laiyagushi.com/google/cel-gopath. Fixedas a mechanical import-path swap (confirmed
Variable/MapType/StringType/DynTypeall exist identically in the new module) in the same commit as thebump, since the two are inseparable — any bump to v0.0.42 breaks the build
without it.
Type of change
Test plan
task test)task lint-fix)New tests:
provenanceInfoFromCatalog's conversion (nil, fully-populated,and attestation-refused cases), plus two install-flow tests —
TestInstallVerification_CatalogProvenanceUsedOnFirstUse(a registry entrywith no prior lock entry gets its declared identity enforced instead of
blind TOFU) and
TestInstallVerification_LockEntryTakesPrecedenceOverCatalog(once a lock entry exists, it wins over a catalog that has since changed
its declared identity).
task testpasses forpkg/skills/...,pkg/auth/..., andpkg/authserver/.... Ran the full suite once too: three failures appeared(
pkg/transport/proxy/streamable— a documented pre-existing port-8096conflict from a stray unrelated process in this environment;
pkg/vmcp/server'sTestForwarding_Logging_RealBackendandTestIntegration_SSEGetConnectionSurvivesWriteTimeout— both timing-basedintegration tests, confirmed passing when rerun in isolation, i.e. flakes
under full-suite resource contention, not a regression from this change).
go build ./...andgo vet ./...are clean across the whole module.Changes
go.mod,go.sumtoolhive-coreto v0.0.42pkg/auth/awssts/role_mapper.go,pkg/authserver/server/tokenexchange/multi_issuer_validator.goEnvOptionimport tocel.dev/cel-go/cel(mechanical, required by the bump)pkg/skills/options.goInstallOptions.CatalogProvenance(internal-only)pkg/skills/skillsvc/registry.goProvenancethroughregistryResolveResultpkg/skills/skillsvc/install.goinstallFromResolvedRegistrypkg/skills/skillsvc/verify.goprovenanceInfoFromCatalogconversion (fail-closed on attestation); fall back to it inverifyOCIInstall/verifyGitInstallonly on true first usedocs/arch/12-skills-system.mddocs/server/*task docs—toolhive-core'sSkilltype (exposed through the registry API) gained theprovenancefield along with the version bumpDoes this introduce a user-facing change?
Yes, two ways:
entry declares an expected
provenanceis now checked against it onfirst install. Previously any correctly-signed artifact was accepted and
its observed identity recorded unconditionally. No change for entries
with no declared provenance (the common case today), and no change to
any already-locked skill.
Skillschema (GET /registry/{name}/v0.1/x/dev.toolhive/skillsand friends) gains an optional
provenancefield, inherited directlyfrom the
toolhive-corebump — additive and backward compatible.Special notes for reviewers
Three commits: the dependency bump + CEL fix, the feature, then a
task docsregeneration I initially missed (CI's Swagger check caught it —bumping
toolhive-corealone was enough to change the registry API'sgenerated schema, since
Skillgained the field independent of anythingthis PR's own code touches). The CEL fix is unrelated to this issue but
unavoidable — any PR bumping to
toolhive-corev0.0.42 breaks the buildwithout it, so it travels with the bump rather than as a separate PR.