Add project-level skills lock file (POC)#5715
Conversation
There was a problem hiding this comment.
Large PR Detected
This PR exceeds 1000 lines of changes and requires justification before it can be reviewed.
How to unblock this PR:
Add a section to your PR description with the following format:
## Large PR Justification
[Explain why this PR must be large, such as:]
- Generated code that cannot be split
- Large refactoring that must be atomic
- Multiple related changes that would break if separated
- Migration or data transformationAlternative:
Consider splitting this PR into smaller, focused changes (< 1000 lines each) for easier review and reduced risk.
See our Contributing Guidelines for more details.
This review will be automatically dismissed once you add the justification section.
|
Love it! Let's talk on Monday about this. Shall we have an RFC too? Just for docs |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #5715 +/- ##
==========================================
- Coverage 70.74% 69.75% -1.00%
==========================================
Files 683 704 +21
Lines 69194 70884 +1690
==========================================
+ Hits 48950 49443 +493
- Misses 16663 17741 +1078
- Partials 3581 3700 +119 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Yeah 🚀 , I opened this RFC: stacklok/toolhive-rfcs#80. I just refined it a bit. |
Large PR justification has been provided. Thank you!
|
✅ Large PR justification has been provided. The size review has been dismissed and this PR can now proceed with normal review. |
dafb744 to
ae2f41a
Compare
Introduce toolhive.lock.yaml, committed at the project root, that pins the
name, version, source, resolved reference, and digest of every project-scoped
skill install. This gives teams reproducible skill installs (restorable via
"thv skill sync") and a path to refresh pinned content when the catalog moves
("thv skill upgrade"), mirroring the role package-lock.json plays for npm.
- New pkg/skills/lockfile package: schema, load/save, and file-locked
upsert/remove using pkg/fileutils.WithFileLock; entries sorted by name for
stable diffs.
- skillsvc install/uninstall hooks: project-scope installs upsert a lock
entry (recording the original source string for later re-resolution);
uninstalls remove it. User-scope installs never touch the lock.
- Sync: installs each entry strictly at its pinned resolvedReference@digest,
reports unmanaged project-scope skills, and prunes them with --prune.
- Upgrade: re-resolves each entry's original source, compares digests, and
installs + rewrites the entry on change; immutable sources (OCI digest,
git commit) reported as not-upgradable; --dry-run supported.
- API: POST /skills/sync and POST /skills/upgrade plus skills client methods.
- CLI: thv skill sync and thv skill upgrade with git-root auto-detection and
--project-root override; JSON/text output.
- Docs: regenerated CLI/swagger docs; updated docs/arch/12-skills-system.md
with a Project Lock File section, diagram, and endpoint/CLI tables.
Co-authored-by: Cursor <cursoragent@cursor.com>
RFC THV-0080 needs a way to restore a project's pinned skill set on any machine (a fresh clone, CI, a teammate's laptop) and to verify on-disk content still matches the lock file without installing anything. This adds the full sync vertical slice: service logic, HTTP endpoint, client, and CLI command. - Sync reconciles installed skills against toolhive.lock.yaml: missing/drifted entries reinstall at the pinned reference (never re-resolving source, see buildPinnedReference), --check reports drift read-only, --adopt/--prune handle unlocked installs - Fixes a real bug this surfaced: reinstalling at an unchanged pinned digest hit both the OCI and git install paths' "same digest means content is already correct" fast path, silently skipping re-extraction — exactly the drift sync exists to repair. A new internal SyncRestore option bypasses that fast path. - POST /api/v1beta/skills/sync via a narrow skillSyncer interface (pkg/api/v1), so this PR doesn't need to ship an Upgrade stub - `thv skill sync` CLI (--check/--adopt/--prune/--clients/ --project-root, auto-detected via .git when omitted) Part of the production stack for #5715 (RFC THV-0080). Stack: 4/6.
RFC THV-0080 needs a controlled way to pull newer content for skills pinned by source, without silently drifting the lock file on every install. This adds the full upgrade vertical slice: service logic, HTTP endpoint, client, and CLI command. - Upgrade re-resolves each targeted entry's Source (via the same git/OCI/registry dispatch order Install uses, stopping short of extraction — resolveLatestState) and installs newer content when the digest changed; Source itself is never rewritten - Entries pinned to an immutable reference (an OCI digest or full git commit hash) report not-upgradable — there's nothing newer to resolve to - --preview reports what would change without persisting it; --allow-ref-change permits the resolved reference itself changing; --fail-on-changes gives CI a freshness gate - var _ skills.SkillLockService = (*service)(nil) lands here now that both Sync and Upgrade exist; SkillsRouter widens from the narrow skillSyncer interface (PR4) to the full interface - `thv skill upgrade [name...]` CLI Part of the production stack for #5715 (RFC THV-0080). Stack: 5/6.
RFC THV-0080 gives CI a scriptable contract for sync/upgrade: exit 2 for check/freshness failures, 3 for partial failures, 4 for policy rejections, distinct from the generic 1 cobra already uses. It also requires a pre-install confirmation gate — skill content is a set of AI-followed instructions, so a non-interactive run must refuse outright without --yes rather than silently proceeding. - cmd/thv/app/exitcode.go: exitCodeError carries a specific exit code through a RunE return; main.go maps it via ExitCodeFromError instead of a hardcoded os.Exit(1) - cmd/thv/app/skill_confirm.go: requireConfirmation prompts on a TTY, refuses with ExitCodePolicyRejection off one - sync/upgrade gain --yes, skipped automatically by --check/ --preview (which never install anything); their exit-code mapping prioritizes partial failure over drift/ref-change-blocked - docs/arch/12-skills-system.md: new Project Lock File section covering rollout gating, schema, install/uninstall hooks, sync, upgrade, and the exit-code contract - CLI E2E coverage for all four exit codes (0/2/4, plus --yes) This is the last PR of Stack 1 (lock file + sync + upgrade) for RFC THV-0080. Stack 2 (Sigstore signing/verification) builds on top once this stack is fully merged; TOOLHIVE_SKILLS_LOCK_ENABLED stays off by default until that full v1 lands. Part of the production stack for #5715 (RFC THV-0080). Stack: 6/6.
RFC THV-0080 requires every project-scoped skill install to be pinned in toolhive.lock.yaml, including transitively materialized toolhive.requires dependencies, with a fail-hard guarantee so a lock-write failure never leaves an install silently unpinned. The whole feature stays inert on main until the full RFC v1 (this lock-file stack plus the later Sigstore stack) has landed — gated behind TOOLHIVE_SKILLS_LOCK_ENABLED, following the existing TOOLHIVE_DEV precedent for staged rollouts. Each PR in the stack is independently mergeable without exposing partial behavior. - Install hooks into the single choke point (installAndRegister): compute contentDigest, upsert the lock entry, roll back the DB record if the lock write fails - toolhive.requires dependencies materialize recursively (visited set guards cycles, skills.MaxDependencies bounds the whole tree), with RequiredBy merged across shared dependencies - Uninstall cascades to orphaned, non-explicit dependencies via Lockfile.RemoveParentFromRequiredBy, itself cycle-safe - E2E coverage for the real HTTP API + thv serve subprocess path Part of the production stack for #5715 (RFC THV-0080). Stack: 3/6.
RFC THV-0080 needs a way to restore a project's pinned skill set on any machine (a fresh clone, CI, a teammate's laptop) and to verify on-disk content still matches the lock file without installing anything. This adds the full sync vertical slice: service logic, HTTP endpoint, client, and CLI command. - Sync reconciles installed skills against toolhive.lock.yaml: missing/drifted entries reinstall at the pinned reference (never re-resolving source, see buildPinnedReference), --check reports drift read-only, --adopt/--prune handle unlocked installs - Fixes a real bug this surfaced: reinstalling at an unchanged pinned digest hit both the OCI and git install paths' "same digest means content is already correct" fast path, silently skipping re-extraction — exactly the drift sync exists to repair. A new internal SyncRestore option bypasses that fast path. - POST /api/v1beta/skills/sync via a narrow skillSyncer interface (pkg/api/v1), so this PR doesn't need to ship an Upgrade stub - `thv skill sync` CLI (--check/--adopt/--prune/--clients/ --project-root, auto-detected via .git when omitted) Part of the production stack for #5715 (RFC THV-0080). Stack: 4/6.
RFC THV-0080 needs a controlled way to pull newer content for skills pinned by source, without silently drifting the lock file on every install. This adds the full upgrade vertical slice: service logic, HTTP endpoint, client, and CLI command. - Upgrade re-resolves each targeted entry's Source (via the same git/OCI/registry dispatch order Install uses, stopping short of extraction — resolveLatestState) and installs newer content when the digest changed; Source itself is never rewritten - Entries pinned to an immutable reference (an OCI digest or full git commit hash) report not-upgradable — there's nothing newer to resolve to - --preview reports what would change without persisting it; --allow-ref-change permits the resolved reference itself changing; --fail-on-changes gives CI a freshness gate - var _ skills.SkillLockService = (*service)(nil) lands here now that both Sync and Upgrade exist; SkillsRouter widens from the narrow skillSyncer interface (PR4) to the full interface - `thv skill upgrade [name...]` CLI Part of the production stack for #5715 (RFC THV-0080). Stack: 5/6.
RFC THV-0080 gives CI a scriptable contract for sync/upgrade: exit 2 for check/freshness failures, 3 for partial failures, 4 for policy rejections, distinct from the generic 1 cobra already uses. It also requires a pre-install confirmation gate — skill content is a set of AI-followed instructions, so a non-interactive run must refuse outright without --yes rather than silently proceeding. - cmd/thv/app/exitcode.go: exitCodeError carries a specific exit code through a RunE return; main.go maps it via ExitCodeFromError instead of a hardcoded os.Exit(1) - cmd/thv/app/skill_confirm.go: requireConfirmation prompts on a TTY, refuses with ExitCodePolicyRejection off one - sync/upgrade gain --yes, skipped automatically by --check/ --preview (which never install anything); their exit-code mapping prioritizes partial failure over drift/ref-change-blocked - docs/arch/12-skills-system.md: new Project Lock File section covering rollout gating, schema, install/uninstall hooks, sync, upgrade, and the exit-code contract - CLI E2E coverage for all four exit codes (0/2/4, plus --yes) This is the last PR of Stack 1 (lock file + sync + upgrade) for RFC THV-0080. Stack 2 (Sigstore signing/verification) builds on top once this stack is fully merged; TOOLHIVE_SKILLS_LOCK_ENABLED stays off by default until that full v1 lands. Part of the production stack for #5715 (RFC THV-0080). Stack: 6/6.
RFC THV-0080 needs a way to restore a project's pinned skill set on any machine (a fresh clone, CI, a teammate's laptop) and to verify on-disk content still matches the lock file without installing anything. This adds the full sync vertical slice: service logic, HTTP endpoint, client, and CLI command. - Sync reconciles installed skills against toolhive.lock.yaml: missing/drifted entries reinstall at the pinned reference (never re-resolving source, see buildPinnedReference), --check reports drift read-only, --adopt/--prune handle unlocked installs - Fixes a real bug this surfaced: reinstalling at an unchanged pinned digest hit both the OCI and git install paths' "same digest means content is already correct" fast path, silently skipping re-extraction — exactly the drift sync exists to repair. A new internal SyncRestore option bypasses that fast path. - POST /api/v1beta/skills/sync via a narrow skillSyncer interface (pkg/api/v1), so this PR doesn't need to ship an Upgrade stub - `thv skill sync` CLI (--check/--adopt/--prune/--clients/ --project-root, auto-detected via .git when omitted) Part of the production stack for #5715 (RFC THV-0080). Stack: 4/6.
RFC THV-0080 needs a controlled way to pull newer content for skills pinned by source, without silently drifting the lock file on every install. This adds the full upgrade vertical slice: service logic, HTTP endpoint, client, and CLI command. - Upgrade re-resolves each targeted entry's Source (via the same git/OCI/registry dispatch order Install uses, stopping short of extraction — resolveLatestState) and installs newer content when the digest changed; Source itself is never rewritten - Entries pinned to an immutable reference (an OCI digest or full git commit hash) report not-upgradable — there's nothing newer to resolve to - --preview reports what would change without persisting it; --allow-ref-change permits the resolved reference itself changing; --fail-on-changes gives CI a freshness gate - var _ skills.SkillLockService = (*service)(nil) lands here now that both Sync and Upgrade exist; SkillsRouter widens from the narrow skillSyncer interface (PR4) to the full interface - `thv skill upgrade [name...]` CLI Part of the production stack for #5715 (RFC THV-0080). Stack: 5/6.
RFC THV-0080 gives CI a scriptable contract for sync/upgrade: exit 2 for check/freshness failures, 3 for partial failures, 4 for policy rejections, distinct from the generic 1 cobra already uses. It also requires a pre-install confirmation gate — skill content is a set of AI-followed instructions, so a non-interactive run must refuse outright without --yes rather than silently proceeding. - cmd/thv/app/exitcode.go: exitCodeError carries a specific exit code through a RunE return; main.go maps it via ExitCodeFromError instead of a hardcoded os.Exit(1) - cmd/thv/app/skill_confirm.go: requireConfirmation prompts on a TTY, refuses with ExitCodePolicyRejection off one - sync/upgrade gain --yes, skipped automatically by --check/ --preview (which never install anything); their exit-code mapping prioritizes partial failure over drift/ref-change-blocked - docs/arch/12-skills-system.md: new Project Lock File section covering rollout gating, schema, install/uninstall hooks, sync, upgrade, and the exit-code contract - CLI E2E coverage for all four exit codes (0/2/4, plus --yes) This is the last PR of Stack 1 (lock file + sync + upgrade) for RFC THV-0080. Stack 2 (Sigstore signing/verification) builds on top once this stack is fully merged; TOOLHIVE_SKILLS_LOCK_ENABLED stays off by default until that full v1 lands. Part of the production stack for #5715 (RFC THV-0080). Stack: 6/6.
RFC THV-0080 needs a controlled way to pull newer content for skills pinned by source, without silently drifting the lock file on every install. This adds the full upgrade vertical slice: service logic, HTTP endpoint, client, and CLI command. - Upgrade re-resolves each targeted entry's Source (via the same git/OCI/registry dispatch order Install uses, stopping short of extraction — resolveLatestState) and installs newer content when the digest changed; Source itself is never rewritten - Entries pinned to an immutable reference (an OCI digest or full git commit hash) report not-upgradable — there's nothing newer to resolve to - --preview reports what would change without persisting it; --allow-ref-change permits the resolved reference itself changing; --fail-on-changes gives CI a freshness gate - var _ skills.SkillLockService = (*service)(nil) lands here now that both Sync and Upgrade exist; SkillsRouter widens from the narrow skillSyncer interface (PR4) to the full interface - `thv skill upgrade [name...]` CLI Part of the production stack for #5715 (RFC THV-0080). Stack: 5/6.
RFC THV-0080 gives CI a scriptable contract for sync/upgrade: exit 2 for check/freshness failures, 3 for partial failures, 4 for policy rejections, distinct from the generic 1 cobra already uses. It also requires a pre-install confirmation gate — skill content is a set of AI-followed instructions, so a non-interactive run must refuse outright without --yes rather than silently proceeding. - cmd/thv/app/exitcode.go: exitCodeError carries a specific exit code through a RunE return; main.go maps it via ExitCodeFromError instead of a hardcoded os.Exit(1) - cmd/thv/app/skill_confirm.go: requireConfirmation prompts on a TTY, refuses with ExitCodePolicyRejection off one - sync/upgrade gain --yes, skipped automatically by --check/ --preview (which never install anything); their exit-code mapping prioritizes partial failure over drift/ref-change-blocked - docs/arch/12-skills-system.md: new Project Lock File section covering rollout gating, schema, install/uninstall hooks, sync, upgrade, and the exit-code contract - CLI E2E coverage for all four exit codes (0/2/4, plus --yes) This is the last PR of Stack 1 (lock file + sync + upgrade) for RFC THV-0080. Stack 2 (Sigstore signing/verification) builds on top once this stack is fully merged; TOOLHIVE_SKILLS_LOCK_ENABLED stays off by default until that full v1 lands. Part of the production stack for #5715 (RFC THV-0080). Stack: 6/6.
RFC THV-0080 gives CI a scriptable contract for sync/upgrade: exit 2 for check/freshness failures, 3 for partial failures, 4 for policy rejections, distinct from the generic 1 cobra already uses. It also requires a pre-install confirmation gate — skill content is a set of AI-followed instructions, so a non-interactive run must refuse outright without --yes rather than silently proceeding. - cmd/thv/app/exitcode.go: exitCodeError carries a specific exit code through a RunE return; main.go maps it via ExitCodeFromError instead of a hardcoded os.Exit(1) - cmd/thv/app/skill_confirm.go: requireConfirmation prompts on a TTY, refuses with ExitCodePolicyRejection off one - sync/upgrade gain --yes, skipped automatically by --check/ --preview (which never install anything); their exit-code mapping prioritizes partial failure over drift/ref-change-blocked - docs/arch/12-skills-system.md: new Project Lock File section covering rollout gating, schema, install/uninstall hooks, sync, upgrade, and the exit-code contract - CLI E2E coverage for all four exit codes (0/2/4, plus --yes) This is the last PR of Stack 1 (lock file + sync + upgrade) for RFC THV-0080. Stack 2 (Sigstore signing/verification) builds on top once this stack is fully merged; TOOLHIVE_SKILLS_LOCK_ENABLED stays off by default until that full v1 lands. Part of the production stack for #5715 (RFC THV-0080). Stack: 6/6.
RFC THV-0080 needs a controlled way to pull newer content for skills pinned by source, without silently drifting the lock file on every install. This adds the full upgrade vertical slice: service logic, HTTP endpoint, client, and CLI command. - Upgrade re-resolves each targeted entry's Source (via the same git/OCI/registry dispatch order Install uses, stopping short of extraction — resolveLatestState) and installs newer content when the digest changed; Source itself is never rewritten - Entries pinned to an immutable reference (an OCI digest or full git commit hash) report not-upgradable — there's nothing newer to resolve to - --preview reports what would change without persisting it; --allow-ref-change permits the resolved reference itself changing; --fail-on-changes gives CI a freshness gate - var _ skills.SkillLockService = (*service)(nil) lands here now that both Sync and Upgrade exist; SkillsRouter widens from the narrow skillSyncer interface (PR4) to the full interface - `thv skill upgrade [name...]` CLI Part of the production stack for #5715 (RFC THV-0080). Stack: 5/6.
RFC THV-0080 gives CI a scriptable contract for sync/upgrade: exit 2 for check/freshness failures, 3 for partial failures, 4 for policy rejections, distinct from the generic 1 cobra already uses. It also requires a pre-install confirmation gate — skill content is a set of AI-followed instructions, so a non-interactive run must refuse outright without --yes rather than silently proceeding. - cmd/thv/app/exitcode.go: exitCodeError carries a specific exit code through a RunE return; main.go maps it via ExitCodeFromError instead of a hardcoded os.Exit(1) - cmd/thv/app/skill_confirm.go: requireConfirmation prompts on a TTY, refuses with ExitCodePolicyRejection off one - sync/upgrade gain --yes, skipped automatically by --check/ --preview (which never install anything); their exit-code mapping prioritizes partial failure over drift/ref-change-blocked - docs/arch/12-skills-system.md: new Project Lock File section covering rollout gating, schema, install/uninstall hooks, sync, upgrade, and the exit-code contract - CLI E2E coverage for all four exit codes (0/2/4, plus --yes) This is the last PR of Stack 1 (lock file + sync + upgrade) for RFC THV-0080. Stack 2 (Sigstore signing/verification) builds on top once this stack is fully merged; TOOLHIVE_SKILLS_LOCK_ENABLED stays off by default until that full v1 lands. Part of the production stack for #5715 (RFC THV-0080). Stack: 6/6.
RFC THV-0080 gives CI a scriptable contract for sync/upgrade: exit 2 for check/freshness failures, 3 for partial failures, 4 for policy rejections, distinct from the generic 1 cobra already uses. It also requires a pre-install confirmation gate — skill content is a set of AI-followed instructions, so a non-interactive run must refuse outright without --yes rather than silently proceeding. - cmd/thv/app/exitcode.go: exitCodeError carries a specific exit code through a RunE return; main.go maps it via ExitCodeFromError instead of a hardcoded os.Exit(1) - cmd/thv/app/skill_confirm.go: requireConfirmation prompts on a TTY, refuses with ExitCodePolicyRejection off one - sync/upgrade gain --yes, skipped automatically by --check/ --preview (which never install anything); their exit-code mapping prioritizes partial failure over drift/ref-change-blocked - docs/arch/12-skills-system.md: new Project Lock File section covering rollout gating, schema, install/uninstall hooks, sync, upgrade, and the exit-code contract - CLI E2E coverage for all four exit codes (0/2/4, plus --yes) This is the last PR of Stack 1 (lock file + sync + upgrade) for RFC THV-0080. Stack 2 (Sigstore signing/verification) builds on top once this stack is fully merged; TOOLHIVE_SKILLS_LOCK_ENABLED stays off by default until that full v1 lands. Part of the production stack for #5715 (RFC THV-0080). Stack: 6/6.
RFC THV-0080 gives CI a scriptable contract for sync/upgrade: exit 2 for check/freshness failures, 3 for partial failures, 4 for policy rejections, distinct from the generic 1 cobra already uses. It also requires a pre-install confirmation gate — skill content is a set of AI-followed instructions, so a non-interactive run must refuse outright without --yes rather than silently proceeding. - cmd/thv/app/exitcode.go: exitCodeError carries a specific exit code through a RunE return; main.go maps it via ExitCodeFromError instead of a hardcoded os.Exit(1) - cmd/thv/app/skill_confirm.go: requireConfirmation prompts on a TTY, refuses with ExitCodePolicyRejection off one - sync/upgrade gain --yes, skipped automatically by --check/ --preview (which never install anything); their exit-code mapping prioritizes partial failure over drift/ref-change-blocked - docs/arch/12-skills-system.md: new Project Lock File section covering rollout gating, schema, install/uninstall hooks, sync, upgrade, and the exit-code contract - CLI E2E coverage for all four exit codes (0/2/4, plus --yes) This is the last PR of Stack 1 (lock file + sync + upgrade) for RFC THV-0080. Stack 2 (Sigstore signing/verification) builds on top once this stack is fully merged; TOOLHIVE_SKILLS_LOCK_ENABLED stays off by default until that full v1 lands. Part of the production stack for #5715 (RFC THV-0080). Stack: 6/6.
RFC THV-0080 gives CI a scriptable contract for sync/upgrade: exit 2 for check/freshness failures, 3 for partial failures, 4 for policy rejections, distinct from the generic 1 cobra already uses. It also requires a pre-install confirmation gate — skill content is a set of AI-followed instructions, so a non-interactive run must refuse outright without --yes rather than silently proceeding. - cmd/thv/app/exitcode.go: exitCodeError carries a specific exit code through a RunE return; main.go maps it via ExitCodeFromError instead of a hardcoded os.Exit(1) - cmd/thv/app/skill_confirm.go: requireConfirmation prompts on a TTY, refuses with ExitCodePolicyRejection off one - sync/upgrade gain --yes, skipped automatically by --check/ --preview (which never install anything); their exit-code mapping prioritizes partial failure over drift/ref-change-blocked - docs/arch/12-skills-system.md: new Project Lock File section covering rollout gating, schema, install/uninstall hooks, sync, upgrade, and the exit-code contract - CLI E2E coverage for all four exit codes (0/2/4, plus --yes) This is the last PR of Stack 1 (lock file + sync + upgrade) for RFC THV-0080. Stack 2 (Sigstore signing/verification) builds on top once this stack is fully merged; TOOLHIVE_SKILLS_LOCK_ENABLED stays off by default until that full v1 lands. Part of the production stack for #5715 (RFC THV-0080). Stack: 6/6.
RFC THV-0080 gives CI a scriptable contract for sync/upgrade: exit 2 for check/freshness failures, 3 for partial failures, 4 for policy rejections, distinct from the generic 1 cobra already uses. It also requires a pre-install confirmation gate — skill content is a set of AI-followed instructions, so a non-interactive run must refuse outright without --yes rather than silently proceeding. - cmd/thv/app/exitcode.go: exitCodeError carries a specific exit code through a RunE return; main.go maps it via ExitCodeFromError instead of a hardcoded os.Exit(1) - cmd/thv/app/skill_confirm.go: requireConfirmation prompts on a TTY, refuses with ExitCodePolicyRejection off one - sync/upgrade gain --yes, skipped automatically by --check/ --preview (which never install anything); their exit-code mapping prioritizes partial failure over drift/ref-change-blocked - docs/arch/12-skills-system.md: new Project Lock File section covering rollout gating, schema, install/uninstall hooks, sync, upgrade, and the exit-code contract - CLI E2E coverage for all four exit codes (0/2/4, plus --yes) This is the last PR of Stack 1 (lock file + sync + upgrade) for RFC THV-0080. Stack 2 (Sigstore signing/verification) builds on top once this stack is fully merged; TOOLHIVE_SKILLS_LOCK_ENABLED stays off by default until that full v1 lands. Part of the production stack for #5715 (RFC THV-0080). Stack: 6/6.
RFC THV-0080 gives CI a scriptable contract for sync/upgrade: exit 2 for check/freshness failures, 3 for partial failures, 4 for policy rejections, distinct from the generic 1 cobra already uses. It also requires a pre-install confirmation gate — skill content is a set of AI-followed instructions, so a non-interactive run must refuse outright without --yes rather than silently proceeding. - cmd/thv/app/exitcode.go: exitCodeError carries a specific exit code through a RunE return; main.go maps it via ExitCodeFromError instead of a hardcoded os.Exit(1) - cmd/thv/app/skill_confirm.go: requireConfirmation prompts on a TTY, refuses with ExitCodePolicyRejection off one - sync/upgrade gain --yes, skipped automatically by --check/ --preview (which never install anything); their exit-code mapping prioritizes partial failure over drift/ref-change-blocked - docs/arch/12-skills-system.md: new Project Lock File section covering rollout gating, schema, install/uninstall hooks, sync, upgrade, and the exit-code contract - CLI E2E coverage for all four exit codes (0/2/4, plus --yes) This is the last PR of Stack 1 (lock file + sync + upgrade) for RFC THV-0080. Stack 2 (Sigstore signing/verification) builds on top once this stack is fully merged; TOOLHIVE_SKILLS_LOCK_ENABLED stays off by default until that full v1 lands. Part of the production stack for #5715 (RFC THV-0080). Stack: 6/6.
* Add SkillLockService interface and managed install flag
RFC THV-0080's sync and upgrade commands operate over a project's
whole lock file rather than a single named skill, so they need
their own service boundary (SkillLockService) instead of extending
SkillService. Installed skills also need a durable "is this
tracked in the lock file" marker so sync --prune can distinguish
lock-managed installs from ones a user manages by hand.
- SkillLockService{Sync,Upgrade} interface with generated mock
- Sync/Upgrade option and result types, RFC's typed failure reasons
- installed_skills.managed column (migration 003)
Part of the production stack for #5715 (RFC THV-0080). Stack: 2/6.
No consumers yet — skillsvc wiring lands in PR3.
* Enforce Managed only for project-scoped skills
Managed is documented as pinning a skill in a project's lock file,
but Create/Update wrote it unconditionally regardless of scope, and
the existing round-trip test created a user-scoped skill with
Managed: true. Reject the invalid combination in the store instead
of only in a doc comment. Also fix a self-contradictory SyncResult
doc comment and an inconsistent JSON tag on AlreadyCurrent.
* Trim unproduced FailureReason values and expose Managed=false
Four typed failure reasons shipped in the JSON API with no code able
to produce them: the three Sigstore verification reasons (deferred to
the signing stack) and ref-change-blocked (which surfaces as an
UpgradeStatus, not a failure). Pinning unproducible API constants now
would make refining them later a breaking change — they land with
their producers instead. Also drop omitempty from InstalledSkill's
Managed flag so list/get consumers can observe the false state, and
reword registry-unreachable's doc to cover git hosts, which map to
the same reason.
RFC THV-0080 gives CI a scriptable contract for sync/upgrade: exit 2 for check/freshness failures, 3 for partial failures, 4 for policy rejections, distinct from the generic 1 cobra already uses. It also requires a pre-install confirmation gate — skill content is a set of AI-followed instructions, so a non-interactive run must refuse outright without --yes rather than silently proceeding. - cmd/thv/app/exitcode.go: exitCodeError carries a specific exit code through a RunE return; main.go maps it via ExitCodeFromError instead of a hardcoded os.Exit(1) - cmd/thv/app/skill_confirm.go: requireConfirmation prompts on a TTY, refuses with ExitCodePolicyRejection off one - sync/upgrade gain --yes, skipped automatically by --check/ --preview (which never install anything); their exit-code mapping prioritizes partial failure over drift/ref-change-blocked - docs/arch/12-skills-system.md: new Project Lock File section covering rollout gating, schema, install/uninstall hooks, sync, upgrade, and the exit-code contract - CLI E2E coverage for all four exit codes (0/2/4, plus --yes) This is the last PR of Stack 1 (lock file + sync + upgrade) for RFC THV-0080. Stack 2 (Sigstore signing/verification) builds on top once this stack is fully merged; TOOLHIVE_SKILLS_LOCK_ENABLED stays off by default until that full v1 lands. Part of the production stack for #5715 (RFC THV-0080). Stack: 6/6.
* Record project-scope skill installs in the lock file RFC THV-0080 requires every project-scoped skill install to be pinned in toolhive.lock.yaml, including transitively materialized toolhive.requires dependencies, with a fail-hard guarantee so a lock-write failure never leaves an install silently unpinned. The whole feature stays inert on main until the full RFC v1 (this lock-file stack plus the later Sigstore stack) has landed — gated behind TOOLHIVE_SKILLS_LOCK_ENABLED, following the existing TOOLHIVE_DEV precedent for staged rollouts. Each PR in the stack is independently mergeable without exposing partial behavior. - Install hooks into the single choke point (installAndRegister): compute contentDigest, upsert the lock entry, roll back the DB record if the lock write fails - toolhive.requires dependencies materialize recursively (visited set guards cycles, skills.MaxDependencies bounds the whole tree), with RequiredBy merged across shared dependencies - Uninstall cascades to orphaned, non-explicit dependencies via Lockfile.RemoveParentFromRequiredBy, itself cycle-safe - E2E coverage for the real HTTP API + thv serve subprocess path Part of the production stack for #5715 (RFC THV-0080). Stack: 3/6. * Fix CI failure: gitRef test fixture hardcoded http scheme gitresolver.ParseGitReference only uses "http://" when TOOLHIVE_DEV=true; the correct default (and what GitHub Actions CI runners use, since they don't set that var) is "https://". The gitRef test helper hardcoded "http://" for the fixture registration key, so every test using it failed in CI with "no fixture registered for https://...". Compute the scheme the same way the resolver does instead of hardcoding either one. * Fix install rollback and dependency cycle detection gaps installAndRegister's rollback only deleted the DB record, so a dependency-materialization failure after the parent's own lock entry had already been written left toolhive.lock.yaml claiming a pin that Info() couldn't find. Also key materializeDependencies' cycle-check visited set by the reference a skill was installed with instead of its resolved name, matching how dependency edges reference it — the mismatch let a cycle through unresolved names re-materialize a skill as its own dependency and corrupt its RequiredBy list. * Make install rollback restorative and uninstall resurrection-proof Three panel-review findings on the install/uninstall lifecycle: rollback deleted only the top-level skill, leaking earlier-succeeded sibling dependencies as managed orphans whose RequiredBy pointed at a rolled-back parent; rollback was also unconditionally destructive, so a --force reinstall of a valid, lock-managed skill that failed on a new dependency destroyed its pre-existing DB record and lock entry; and uninstall removed the lock entry last and best-effort, so a lock-write failure after files and DB were gone left a stale entry the next sync silently reinstalled. Rollback now snapshots pre-install state (DB record via InstallResult.PreExisting, lock entry before any write) and restores it when the record pre-existed; when this call created it, removal runs the same dependency cascade as uninstall so fresh orphans are cleaned while shared or explicit deps survive. Uninstall removes the lock entry first and aborts intact on failure. Materialized dependencies also now join the parent's --group instead of falling back to the default group.
RFC THV-0080 gives CI a scriptable contract for sync/upgrade: exit 2 for check/freshness failures, 3 for partial failures, 4 for policy rejections, distinct from the generic 1 cobra already uses. It also requires a pre-install confirmation gate — skill content is a set of AI-followed instructions, so a non-interactive run must refuse outright without --yes rather than silently proceeding. - cmd/thv/app/exitcode.go: exitCodeError carries a specific exit code through a RunE return; main.go maps it via ExitCodeFromError instead of a hardcoded os.Exit(1) - cmd/thv/app/skill_confirm.go: requireConfirmation prompts on a TTY, refuses with ExitCodePolicyRejection off one - sync/upgrade gain --yes, skipped automatically by --check/ --preview (which never install anything); their exit-code mapping prioritizes partial failure over drift/ref-change-blocked - docs/arch/12-skills-system.md: new Project Lock File section covering rollout gating, schema, install/uninstall hooks, sync, upgrade, and the exit-code contract - CLI E2E coverage for all four exit codes (0/2/4, plus --yes) This is the last PR of Stack 1 (lock file + sync + upgrade) for RFC THV-0080. Stack 2 (Sigstore signing/verification) builds on top once this stack is fully merged; TOOLHIVE_SKILLS_LOCK_ENABLED stays off by default until that full v1 lands. Part of the production stack for #5715 (RFC THV-0080). Stack: 6/6.
* Add thv skill sync: restore project skills from the lock file RFC THV-0080 needs a way to restore a project's pinned skill set on any machine (a fresh clone, CI, a teammate's laptop) and to verify on-disk content still matches the lock file without installing anything. This adds the full sync vertical slice: service logic, HTTP endpoint, client, and CLI command. - Sync reconciles installed skills against toolhive.lock.yaml: missing/drifted entries reinstall at the pinned reference (never re-resolving source, see buildPinnedReference), --check reports drift read-only, --adopt/--prune handle unlocked installs - Fixes a real bug this surfaced: reinstalling at an unchanged pinned digest hit both the OCI and git install paths' "same digest means content is already correct" fast path, silently skipping re-extraction — exactly the drift sync exists to repair. A new internal SyncRestore option bypasses that fast path. - POST /api/v1beta/skills/sync via a narrow skillSyncer interface (pkg/api/v1), so this PR doesn't need to ship an Upgrade stub - `thv skill sync` CLI (--check/--adopt/--prune/--clients/ --project-root, auto-detected via .git when omitted) Part of the production stack for #5715 (RFC THV-0080). Stack: 4/6. * Preserve ResolvedReference across sync drift repair reinstallPinned reinstalls at a pinned reference derived from the lock entry, and that pinned string was flowing straight into ResolvedReference via the install path — a drift repair overwrote it with an internal restore form instead of preserving what Source had actually resolved to, which later corrupts upgrade's ref-change detection. Add a LockResolvedReference override on InstallOptions, mirroring the existing LockSource one. * Regenerate swagger docs for SyncResult field changes Picks up the AlreadyCurrent JSON tag rename and Drifted doc comment fix from the PR2 review pass. * Report missing installs in sync --check and pin restores properly The panel review's blocker: drift was only recorded for entries with an install record, so a lock entry with none — every entry on a fresh clone or CI runner, since SQLite state is per-machine — landed in no result bucket and sync --check green-lit a checkout missing all of its locked skills. Add a Missing bucket, recorded before the check return. Restores had three related correctness gaps: restoring a parent re-materialized its toolhive.requires from mutable source strings (silently upgrading and re-pinning a dependency whose upstream tag moved), a restore promoted non-explicit dependencies to explicit (permanently exempting them from cascade removal), and verification hashed only the first client's directory (tampering with any other client's copy was invisible, install-order-dependently). Skip requires recursion on SyncRestore, preserve Explicit verbatim, and verify every client directory. Also classify lock-write failures via a sentinel instead of mapping every HTTP 500 to lock-write-failed, and fix the E2E decode struct reading AlreadyCurrent from a stale JSON key — a silently-passing assertion.
RFC THV-0080 gives CI a scriptable contract for sync/upgrade: exit 2 for check/freshness failures, 3 for partial failures, 4 for policy rejections, distinct from the generic 1 cobra already uses. It also requires a pre-install confirmation gate — skill content is a set of AI-followed instructions, so a non-interactive run must refuse outright without --yes rather than silently proceeding. - cmd/thv/app/exitcode.go: exitCodeError carries a specific exit code through a RunE return; main.go maps it via ExitCodeFromError instead of a hardcoded os.Exit(1) - cmd/thv/app/skill_confirm.go: requireConfirmation prompts on a TTY, refuses with ExitCodePolicyRejection off one - sync/upgrade gain --yes, skipped automatically by --check/ --preview (which never install anything); their exit-code mapping prioritizes partial failure over drift/ref-change-blocked - docs/arch/12-skills-system.md: new Project Lock File section covering rollout gating, schema, install/uninstall hooks, sync, upgrade, and the exit-code contract - CLI E2E coverage for all four exit codes (0/2/4, plus --yes) This is the last PR of Stack 1 (lock file + sync + upgrade) for RFC THV-0080. Stack 2 (Sigstore signing/verification) builds on top once this stack is fully merged; TOOLHIVE_SKILLS_LOCK_ENABLED stays off by default until that full v1 lands. Part of the production stack for #5715 (RFC THV-0080). Stack: 6/6.
* Add thv skill upgrade: re-resolve pinned skills to newer content RFC THV-0080 needs a controlled way to pull newer content for skills pinned by source, without silently drifting the lock file on every install. This adds the full upgrade vertical slice: service logic, HTTP endpoint, client, and CLI command. - Upgrade re-resolves each targeted entry's Source (via the same git/OCI/registry dispatch order Install uses, stopping short of extraction — resolveLatestState) and installs newer content when the digest changed; Source itself is never rewritten - Entries pinned to an immutable reference (an OCI digest or full git commit hash) report not-upgradable — there's nothing newer to resolve to - --preview reports what would change without persisting it; --allow-ref-change permits the resolved reference itself changing; --fail-on-changes gives CI a freshness gate - var _ skills.SkillLockService = (*service)(nil) lands here now that both Sync and Upgrade exist; SkillsRouter widens from the narrow skillSyncer interface (PR4) to the full interface - `thv skill upgrade [name...]` CLI Part of the production stack for #5715 (RFC THV-0080). Stack: 5/6. * Make --fail-on-changes a true no-mutate CI gate Upgrade installed each target in sequence and only checked FailOnChanges after each one returned, so a multi-skill upgrade would install the first changed skill for real before reporting the conflict — a partial mutation a read-only CI gate must never produce. Split into a resolve-only planning pass (also reused to avoid re-resolving Source a second time when applying) and an apply pass that only runs once every target has been checked. Upgrade also now preserves a skill's existing clients instead of expanding to every detected client, matching Sync's behavior. * Fix broken OCI upgrade path and make fail-on-changes report-only Two panel-review majors left the OCI upgrade path broken (and invisible, since coverage was git-only): resolveOCILatest returned the unqualified ref.String() while install records qualifiedOCIRef, so every digest change on a tag-less source tripped the ref-change guard instead of upgrading — the single most common upgrade case; and resolveLatestState dropped Install's registry-catalogue fallback for ambiguous names, so a skill installed through that fallback could never be upgraded. Fix both and add an OCI test matrix (tag-less upgrade, tagged up-to-date, ref-change blocked/allowed, registry-fallback source). fail_on_changes previously returned a 409 that discarded the partial result — hiding which skills were stale and conflating a genuine resolution failure with "would change". It now evaluates the plan and returns the full outcomes with a 200, mirroring sync's check mode; exit-code mapping moves entirely to the CLI. Also accept uppercase hex in isFullCommitHash, state honestly in the help text that OCI previews still fetch artifacts, and return a non-nil Outcomes slice. * Fix forwarding notification context cancellation race --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
RFC THV-0080 gives CI a scriptable contract for sync/upgrade: exit 2 for check/freshness failures, 3 for partial failures, 4 for policy rejections, distinct from the generic 1 cobra already uses. It also requires a pre-install confirmation gate — skill content is a set of AI-followed instructions, so a non-interactive run must refuse outright without --yes rather than silently proceeding. - cmd/thv/app/exitcode.go: exitCodeError carries a specific exit code through a RunE return; main.go maps it via ExitCodeFromError instead of a hardcoded os.Exit(1) - cmd/thv/app/skill_confirm.go: requireConfirmation prompts on a TTY, refuses with ExitCodePolicyRejection off one - sync/upgrade gain --yes, skipped automatically by --check/ --preview (which never install anything); their exit-code mapping prioritizes partial failure over drift/ref-change-blocked - docs/arch/12-skills-system.md: new Project Lock File section covering rollout gating, schema, install/uninstall hooks, sync, upgrade, and the exit-code contract - CLI E2E coverage for all four exit codes (0/2/4, plus --yes) This is the last PR of Stack 1 (lock file + sync + upgrade) for RFC THV-0080. Stack 2 (Sigstore signing/verification) builds on top once this stack is fully merged; TOOLHIVE_SKILLS_LOCK_ENABLED stays off by default until that full v1 lands. Part of the production stack for #5715 (RFC THV-0080). Stack: 6/6.
* Add typed exit codes and confirmation gate to skill sync/upgrade RFC THV-0080 gives CI a scriptable contract for sync/upgrade: exit 2 for check/freshness failures, 3 for partial failures, 4 for policy rejections, distinct from the generic 1 cobra already uses. It also requires a pre-install confirmation gate — skill content is a set of AI-followed instructions, so a non-interactive run must refuse outright without --yes rather than silently proceeding. - cmd/thv/app/exitcode.go: exitCodeError carries a specific exit code through a RunE return; main.go maps it via ExitCodeFromError instead of a hardcoded os.Exit(1) - cmd/thv/app/skill_confirm.go: requireConfirmation prompts on a TTY, refuses with ExitCodePolicyRejection off one - sync/upgrade gain --yes, skipped automatically by --check/ --preview (which never install anything); their exit-code mapping prioritizes partial failure over drift/ref-change-blocked - docs/arch/12-skills-system.md: new Project Lock File section covering rollout gating, schema, install/uninstall hooks, sync, upgrade, and the exit-code contract - CLI E2E coverage for all four exit codes (0/2/4, plus --yes) This is the last PR of Stack 1 (lock file + sync + upgrade) for RFC THV-0080. Stack 2 (Sigstore signing/verification) builds on top once this stack is fully merged; TOOLHIVE_SKILLS_LOCK_ENABLED stays off by default until that full v1 lands. Part of the production stack for #5715 (RFC THV-0080). Stack: 6/6. * Gate upgrade's ref-change exit code behind --preview upgradeExitError mapped a ref-change-blocked outcome to exit 4 unconditionally, even during --preview — where nothing was actually blocked, only reported. Thread preview through, mirroring how syncExitError already gates Drifted behind --check. Also reconcile a docs contradiction about --preview's side effects, drop a stale signer-change reference in the exit-code doc comment (that guard doesn't exist until Stack 2), and add upgrade exit-code E2E coverage. * Honor the exit-code contract under fail-on-changes and harden prompts The panel review's major: the CLI mapped any 409 unconditionally to exit 2, silently downgrading a genuine resolution failure during the CI gate from "something broke" (exit 3) to "lock is stale" (exit 2) and discarding the partial result. With the server now returning outcomes instead of a 409, exit codes derive entirely from outcomes with the correct precedence (failed > would-change > ref-blocked), sync --check counts missing installs toward exit 2, and plan-only runs print "would upgrade" instead of claiming an install happened. The confirmation prompt also gets teeth and armor: it now prints the lock entries being acted on (a bare "proceed?" gave the human gate nothing to judge), goes to stderr so --format json stays parseable, and strips non-graphic runes so a hostile directory or entry name cannot repaint the prompt with terminal escapes. Both commands are marked experimental in their help text, the architecture doc states the pre-Sigstore trust model plainly (drift detection over a repository-editable file, not verified integrity), and E2E coverage adds the missing exit-3 path and the fresh-clone exit-2 gate.
Summary
sync/upgradewould restore or advance skills safely.toolhive.lock.yamlfor project scope withthv skill syncandthv skill upgrade, then fold RFC THV-0080 v1 Sigstore signing/verification into the same POC: verify on consume, sign on push, pin publisher identity (provenance) or recordunsigned: true, and re-verify stored bundles offline insync --check.POC scope (RFC THV-0080)
This PR is a feasibility vertical slice, not production-hardening split across many small PRs yet. It proves:
source,resolvedReference,digest,contentDigestcontentDigestdirhash +sync --checksync --adoptwrites lock entries (with provenance orunsigned)toolhive.requiresmaterialized;requiredBy+ cascade uninstall--allow-ref-change;--allow-signer-changethv skill pushsigns with--key(cosign);--no-signescape hatch--yes, exit codes 2/3/4Trust model (v1)
Three layers (see
docs/arch/12-skills-system.md):sync --checkprovenance.signerIdentity+provenance.certIssuerpinned at first verified install;signer-mismatchon divergencetoolhive.lock.yaml;unsigned: trueis an explicit reviewed exceptionDeferred: keyless OIDC push (E2E against Sigstore staging); catalog-supplied expected identity (needs toolhive-core
ProvenanceonSkilltypes).Lock entry schema (v1)
CLI surface
thv skill sync—--check,--adopt,--prune,--yes,--clients,--project-rootthv skill upgrade [name...]—--preview,--fail-on-changes,--allow-ref-change,--allow-signer-change,--yesthv skill install—--allow-unsigned(project scope)thv skill push—--key,--no-signExit codes:
0success ·2check/freshness failure ·3partial failure ·4policy rejectionCommit structure (19 commits, atomic by layer)
Lock file core
Sigstore v1
Type of change
Test plan
task test—pkg/skills/...passes)task lint-fix)sync --checkAPI compatibility
Additive:
POST /skills/sync,POST /skills/upgrade; new request fields (allowUnsigned,allowSignerChange,key,skipSigning).SkillLockServiceis separate fromSkillService. No operator CRD changes.Large PR justification
~6.6k lines / 74 files. Kept unified for POC feasibility: lock integrity, sync/upgrade guardrails, and Sigstore trust are coupled — splitting mid-stack leaves non-functional intermediate states. If validated, split into the stack below for production merge.
Review focus
contentDigestthe right integrity primitive?requiredBy/explicit) correct?--checksufficient?SkillLockServicethe right API boundary?Does this introduce a user-facing change?
Yes — new
sync/upgradecommands, lock file on project install, Sigstore flags, push signing.