Skip to content

Add SkillLockService interface and managed install flag - #5893

Merged
samuv merged 3 commits into
mainfrom
skills-lock/02-lock-service
Jul 24, 2026
Merged

Add SkillLockService interface and managed install flag#5893
samuv merged 3 commits into
mainfrom
skills-lock/02-lock-service

Conversation

@samuv

@samuv samuv commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Why: RFC THV-0080's sync and upgrade commands operate over a project's whole lock file, not a single named skill — a different shape than SkillService's per-skill operations. Lock-managed installs also need a durable marker so sync --prune can tell lock-managed skills apart from ones a user installed/manages by hand outside the lock file.
  • What:
    • SkillLockService interface (Sync, Upgrade) with generated mock, kept separate from SkillService per the RFC's API design.
    • SyncOptions/SyncResult/SyncFailure, UpgradeOptions/UpgradeResult/UpgradeOutcome, and the RFC's typed FailureReason enum (registry-unreachable, digest-missing, validation-rejected, lock-write-failed, ref-change-blocked, signature-invalid, signer-mismatch, unsigned-rejected, unknown).
    • InstalledSkill.Managed + migration 003 (installed_skills.managed), wired through the SQLite store.

Part of the production stack for #5715 (RFC THV-0080). Stack: 2/6 — lockfile → lock-service → install-hooks → sync → upgrade → cli-exitcodes. Based on #5892 (PR1); no new consumers yet — skillsvc wiring lands in PR3.

Type of change

  • New feature

Test plan

  • Unit tests (task test)
  • Linting (task lint-fix)

Added: migration test proving 003 defaults existing rows to unmanaged and its Down cleanly drops the column without disturbing 001/002 tables; store-level round-trip test for Managed (create, then flip via Update).

Does this introduce a user-facing change?

Yes — InstalledSkill gains a managed field, visible in the existing GET/list skills API responses (swagger updated accordingly). No new endpoints yet.

Special notes for reviewers

  • UpgradeOptions/UpgradeStatus intentionally omit AllowSignerChange/signer-change-blocked — those are Sigstore-stack concerns (PR13). Entry-level provenance/unsigned fields land in the Sigstore stack too (PR7).
  • No deprecated fields carried over from the POC (SyncResult.Unmanaged, UpgradeOptions.DryRun) — these are new APIs, shipped clean.

🤖 Generated with Claude Code

@samuv samuv self-assigned this Jul 21, 2026
@github-actions github-actions Bot added the size/M Medium PR: 300-599 lines changed label Jul 21, 2026
@codecov

codecov Bot commented Jul 21, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 71.84%. Comparing base (97a9dfa) to head (bae6917).

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #5893      +/-   ##
==========================================
- Coverage   71.84%   71.84%   -0.01%     
==========================================
  Files         708      708              
  Lines       72811    72823      +12     
==========================================
+ Hits        52311    52317       +6     
- Misses      16765    16768       +3     
- Partials     3735     3738       +3     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@samuv
samuv force-pushed the skills-lock/02-lock-service branch from e124faf to e07cfea Compare July 21, 2026 16:57
@github-actions github-actions Bot added size/M Medium PR: 300-599 lines changed and removed size/M Medium PR: 300-599 lines changed labels Jul 21, 2026
@samuv

samuv commented Jul 21, 2026

Copy link
Copy Markdown
Contributor Author

Tracked in #5899 (RFC THV-0080 stack).

@samuv
samuv force-pushed the skills-lock/01-lockfile branch from 4997552 to f0f5060 Compare July 22, 2026 20:13
@samuv
samuv force-pushed the skills-lock/02-lock-service branch from dcca019 to ef4a99e Compare July 22, 2026 20:13
@github-actions github-actions Bot added size/M Medium PR: 300-599 lines changed and removed size/M Medium PR: 300-599 lines changed labels Jul 22, 2026

@JAORMX JAORMX left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Automated panel review — PR 2/6 (SkillLockService + managed flag)

Multi-agent review, each finding adversarially verified against the diff. Advisory. Whole stack gated behind TOOLHIVE_SKILLS_LOCK_ENABLED (off by default).

API boundary verdict: splitting SkillLockService from SkillService is the right call and conforms to the RFC — no objection there.

🟡 Minor

  • Three Sigstore FailureReason values ship with no producer (options.go:~160): signature-invalid, signer-mismatch, unsigned-rejected. Nothing in this lock-only stack can emit them (classifySyncFailure only ever produces registry-unreachable/digest-missing/validation-rejected/lock-write-failed/unknown). They're dead exported JSON-API constants pinned before the Sigstore design exists — if that stack refines the taxonomy (e.g. renames signer-mismatch), it's now a breaking change. It's also internally inconsistent with this PR deliberately deferring the sibling signer-change-blocked value. Gate them into the stack that produces them, or comment them reserved.
  • FailureReasonRegistryUnreachable is OCI-registry-specific (options.go:~154) yet is the only transport-failure reason — git-source fetch failures have no accurate typed reason for the CI/automation consumers the enum exists for.
  • InstalledSkill.Managed uses json:",omitempty" (types.go:~170), so the newly-added user-facing field is omitted whenever false — i.e. for every user-scope install and every unmanaged project install (the common case the PR says it exposes). Contradicts the swagger contract and hides the false state from list/get consumers. Drop omitempty for a bool that's meant to be observable.
  • UpgradeResult.Outcomes lacks omitempty and is a slice (options.go:~258), so an empty result marshals to {"outcomes":null} rather than [] — mildly awkward for typed clients.

🟢 Verified praise

  • Migration 003 is well covered: a test proves rows created pre-003 default to unmanaged, and Down cleanly drops only the managed column while leaving 001/002 intact — plus an app-level errManagedRequiresProjectScope guard enforcing managed-implies-project-scope with round-trip tests. These are the security-correct defaults.

@samuv
samuv force-pushed the skills-lock/02-lock-service branch from ef4a99e to 611f480 Compare July 23, 2026 09:30
@samuv

samuv commented Jul 23, 2026

Copy link
Copy Markdown
Contributor Author

All findings addressed in the latest push:

  • Unproduced FailureReason values: removed — the three Sigstore reasons and ref-change-blocked (which nothing produced either; the ref-change guard surfaces as an UpgradeStatus, not a failure reason). They land in the stack that ships their producers, so refining the taxonomy there won't be a breaking change. The type's doc comment states the policy.
  • Managed omitempty: dropped — false (unmanaged) is an observable state list/get consumers need to see; swagger regenerated.
  • registry-unreachable OCI-specific wording: doc comment now covers any remote source (OCI registry or git host), which is what classifySyncFailure actually maps to it. The RFC-pinned string itself is unchanged.
  • UpgradeResult.Outcomes marshaling null: fixed on the upgrade PR (Add thv skill upgrade: re-resolve pinned skills to newer content #5896) where the implementation lives — Upgrade now always returns a non-nil slice.

@github-actions github-actions Bot added size/M Medium PR: 300-599 lines changed and removed size/M Medium PR: 300-599 lines changed labels Jul 23, 2026
JAORMX
JAORMX previously approved these changes Jul 23, 2026

@JAORMX JAORMX left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Automated panel re-review on 611f480approving

All findings from the earlier review are resolved against the actual code:

  • ✅ Premature Sigstore FailureReason constants removed (and ref-change-blocked demoted to a UpgradeStatus, not a reason); the type doc now states the "only reasons the current feature set can produce are defined" policy.
  • FailureReasonRegistryUnreachable doc broadened to cover OCI and git remotes while keeping the RFC-pinned wire string.
  • InstalledSkill.Managed dropped omitempty (with regenerated swagger) — false now always serializes.
  • ↩️ UpgradeResult.Outcomes null-vs-[] nit reasonably deferred to producer PR #5896 (adding omitempty here wouldn't help; only a guaranteed non-nil slice at the producer yields []).

Bonus: the new errManagedRequiresProjectScope invariant guard (rejecting managed on non-project installs, tested in Create + Update) is a net improvement. No regressions. LGTM.

@samuv
samuv force-pushed the skills-lock/01-lockfile branch from 0a45fce to 141d980 Compare July 23, 2026 11:28
@samuv
samuv force-pushed the skills-lock/02-lock-service branch from 611f480 to 2e75912 Compare July 23, 2026 11:28
@github-actions github-actions Bot added size/M Medium PR: 300-599 lines changed and removed size/M Medium PR: 300-599 lines changed labels Jul 23, 2026
Base automatically changed from skills-lock/01-lockfile to main July 23, 2026 11:52
@github-actions github-actions Bot added size/XL Extra large PR: 1000+ lines changed and removed size/M Medium PR: 300-599 lines changed labels Jul 23, 2026
@samuv
samuv force-pushed the skills-lock/02-lock-service branch from 2e75912 to a72e34a Compare July 23, 2026 11:54
@github-actions github-actions Bot added size/M Medium PR: 300-599 lines changed and removed size/XL Extra large PR: 1000+ lines changed labels Jul 23, 2026
@samuv
samuv requested a review from JAORMX July 23, 2026 13:10

@JAORMX JAORMX left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Panel review (spec / standards / security / library-reuse / duplication / golang-conventions / UX) — clean across the board.

Solid foundation: SkillLockService is cleanly separated from SkillService, the managed SQLite flag + migration 003 are correct (store enforces the project-scope invariant on Create and Update, migration test verifies the default), and the diff is genuinely lean.

Non-blocking notes for follow-up layers in the stack:

  • Spec gap (deferral?): RFC THV-0080 lists a public PreserveSource install option alongside SkillLockService, but InstallOptions is untouched here. Assume it's coming in a later PR — flagging so it doesn't fall through.
  • errManagedRequiresProjectScope (skill_store.go) is unexported, so the API layer can't errors.Is-match it to a 400 vs 500. Fine if intentionally internal.
  • Migration test hardcodes entry_id = 1 — safe on a fresh DB, brittle if a pre-seeded fixture ever lands.

Approving.

samuv added 3 commits July 24, 2026 10:04
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.
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.
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.
@samuv
samuv force-pushed the skills-lock/02-lock-service branch from a72e34a to bae6917 Compare July 24, 2026 08:04
@github-actions github-actions Bot added size/M Medium PR: 300-599 lines changed and removed size/M Medium PR: 300-599 lines changed labels Jul 24, 2026
@samuv
samuv merged commit 32fa068 into main Jul 24, 2026
57 of 59 checks passed
@samuv
samuv deleted the skills-lock/02-lock-service branch July 24, 2026 08:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/M Medium PR: 300-599 lines changed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants