Skip to content

Track: AI-plugin lock file + Sigstore signing #6300

Description

@samuv

Summary

Tracks extending the project-level toolhive.lock.yaml (RFC THV-0080) with a sibling plugins: key so project-scoped thv ai-plugin installs are pinned the same way skills are: source, resolvedReference, digest, contentDigest, and Sigstore provenance, plus thv ai-plugin sync / thv ai-plugin upgrade, signature verification on consume, and signing on push.

THV-0080 owns only version and skills:; it reserved sibling keys (explicitly plugins:) as a contract proposal for a later RFC to ratify. This issue ratifies that contract without a new RFC: same file, additive plugins: key, schema version stays 1. Older binaries already round-trip unknown top-level keys via Lockfile.Extra; bumping to version 2 would hard-fail mixed-version teams that only use skills.

This is the AI-plugin counterpart of the skills track (#5899). It copies that trust model rather than inventing one, and reuses the packages skills already landed (pkg/skills/lockfile.Entry including provenance/unsigned, pkg/skills/verifier, pkg/skills/signer, cmd/thv/app/exitcode.go).

The feature is gated behind TOOLHIVE_PLUGINS_LOCK_ENABLED for the whole rollout — plugin entries must not become user-visible in the lock until verification is wired, because by then the lock file is the live skills trust document (#6139 removes TOOLHIVE_SKILLS_LOCK_ENABLED). Pinning and Sigstore are one v1, not two independently shippable halves.

Prerequisite: start Stack 1 only after #6139 (skills push-signing + ungate) is merged to main. Do not stack these PRs on skills-sig/14-push-signing-ungate.

Related: #5899 (skills lock + Sigstore), #5525 (plugin lifecycle epic, RFC THV-0077).

User Story

As a team maintaining a project that consumes AI-tool plugins,
I want project-scope plugin installs pinned in the same committed toolhive.lock.yaml as skills, cryptographically verified against the publisher's Sigstore identity, and safely upgradable,
So that every teammate and CI run restores the exact same reviewed plugins, and tampered or re-signed artifacts are detected before they reach a developer's machine.

Agreed design

  • Same file, additive key, version 1. plugins: sits next to skills:. A skill and a plugin may share a name; they are separate graphs.
  • Full skills parity as one v1: pin on project-scope install, sync / upgrade, verify on consume, sign on push, TOFU provenance / unsigned: true.
  • CLI/API: thv ai-plugin sync / thv ai-plugin upgrade and POST /plugins/sync / POST /plugins/upgrade. Do not teach thv skill sync to walk plugins: — that would change existing CI --check semantics.
  • Lock only explicit plugin installs. requires is parsed today but not materialized; requiredBy / explicit stay unused until that Phase-3 wave. Do not record uninstalled deps.
  • Client-agnostic. No clients field. contentDigest is the frozen skills dirhash over the canonical plugin tree (what ExtractPlugin writes into the client plugin dir), not marketplace.json / settings.json.
  • Reuse, don't extract. Keep pkg/skills/lockfile; add Plugins []Entry plus GetPlugin / UpsertPlugin / RemovePlugin. Validate skills and plugins as separate name/requiredBy graphs. Import existing verifier/signer. Moving signer into toolhive-core stays a Track: Skills lock file + Sigstore signing (RFC THV-0080) #5899 follow-up.
  • Project-scope only, matching THV-0080.
version: 1
skills:
  - name: code-review
    source: code-review
    digest: sha256:...
    provenance: { signerIdentity: "...", certIssuer: "..." }
plugins:
  - name: my-plugin
    source: my-plugin
    resolvedReference: ghcr.io/org/my-plugin:1.0.0
    digest: sha256:...
    contentDigest: sha256:...
    provenance: { signerIdentity: "...", certIssuer: "..." }

Acceptance Criteria

Stack 1 — Lock file

  • AT: Project-scope thv ai-plugin install writes a plugins: entry in toolhive.lock.yaml pinning source, resolvedReference, digest, and contentDigest; thv ai-plugin uninstall removes it. Lock-write failure fails the entire install (rollback DB + dematerialize).
  • AT: thv ai-plugin sync --check detects drift between the lock file and installed plugins (missing, digest mismatch, on-disk tampering via contentDigest) and exits with code 2 on failure
  • AT: thv ai-plugin sync --adopt records out-of-band installs into the lock; --prune removes lock entries for manually deleted plugins
  • AT: thv ai-plugin upgrade --preview shows the candidate change without applying; moving to a different resolved reference requires --allow-ref-change
  • AT: Sync and upgrade are exposed via POST /plugins/sync and POST /plugins/upgrade with typed exit codes for CI (0 success / 2 check failure / 3 partial failure / 4 policy rejection)
  • Unit: a skills-only lock still loads; unknown top-level keys still round-trip via Extra; plugin names are unique within plugins: (not across skills:)

Stack 2 — Sigstore v1

  • AT: Project-scope install verifies OCI artifacts via the Sigstore referrers API and git plugins via gitsign; unsigned artifacts are rejected unless --allow-unsigned is passed, which records unsigned: true in the lock
  • AT: First verified install pins the publisher identity (provenance.signerIdentity + certIssuer, TOFU); subsequent installs/syncs fail with signer-mismatch if the identity diverges
  • AT: thv ai-plugin sync --check re-verifies stored Sigstore bundles offline (no registry access) against the locked provenance
  • AT: thv ai-plugin upgrade blocks candidates whose signer identity or cert issuer changed, with --allow-signer-change as the explicit override
  • AT: thv ai-plugin push signs by default with a cosign key (--key, COSIGN_PASSWORD), attaching the signature as an OCI referrer; --no-sign opts out. (plugins.PushOptions already aliases skills' Key/NoSign; this stack is what honors them.)
  • Unit: Verified bundles are persisted alongside installed plugins (SQLite) for offline re-verification
  • Lock plugin entries validate as exactly one of provenance or unsigned: true

Delivery

  • Work lands as the 10-PR stacked chain below, each PR within repo size guidelines and independently green
  • docs/arch/14-plugins-system.md documents the v1 trust model (cryptographic verification / TOFU identity / human lock-file review), with a pointer from the lock section in docs/arch/12-skills-system.md
  • TOOLHIVE_PLUGINS_LOCK_ENABLED is removed in the last PR, documented as the user-facing change

Stack 1 — Lock file mechanics

Branch chain: plugins-lock/01-schemaplugins-lock/05-upgrade, each PR targeting the previous one's branch. Feature gated; no provenance wiring yet (entries written only behind the gate, and Stack 2 lands before ungate).

Each PR includes its own tests (unit + E2E where applicable) and passes CI independently of the rest of the stack.

Stack 2 — Sigstore signing/verification

Branch chain: plugins-sig/06-bundleplugins-sig/10-push-ungate. Reuse pkg/skills/verifier and pkg/skills/signer; no new Sigstore packages.

  • PR6 — Sigstore bundle blob for plugin installs + git commit-signature plumbing
  • PR7 — Install-time verification, TOFU recording, --allow-unsigned
  • PR8 — Offline bundle re-verify in sync --check
  • PR9 — Upgrade signer-change guard + --allow-signer-change
  • PR10 — Push signing + trust-state display on ai-plugin info / install; docs; removes the TOOLHIVE_PLUGINS_LOCK_ENABLED gate (the point the feature is fully "landed")

Follow-ups (tracked separately once reached)

  • Plugin requires materialization and requiredBy locking (not installed today; a later Phase-3 wave)
  • Keyless OIDC push signing + Sigstore-staging E2E (same deferral as skills)
  • Catalog-supplied expected identity (needs toolhive-core Provenance on plugin types)
  • Unified thv lock sync that walks both skills: and plugins:
  • Extracting pkg/skills/lockfile / signer into a generic package (signer move into toolhive-core is already sequenced after Track: Skills lock file + Sigstore signing (RFC THV-0080) #5899 PR14)
  • Example repo (skills has stacklok/toolhive-skills-lock-example)

Out of scope (v1)

  • Version-constraint resolution or a toolhive.yaml manifest
  • Per-client pinning (clients field)
  • User-scope lock entries
  • Org-wide signer policy files

Definition of done

Metadata

Metadata

Assignees

Labels

cliChanges that impact CLI functionalityenhancementNew feature or requestgoPull requests that update go code

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions