Record plugin installs in the project lock file - #6314
Conversation
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #6314 +/- ##
==========================================
- Coverage 73.08% 73.05% -0.04%
==========================================
Files 745 748 +3
Lines 78804 79264 +460
==========================================
+ Hits 57597 57909 +312
- Misses 17177 17267 +90
- Partials 4030 4088 +58 ☔ 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 found three transaction-boundary issues that can leave filesystem, DB, group, and lock state inconsistent. Please address the inline findings before merge.
1e86051 to
7a77833
Compare
JAORMX
left a comment
There was a problem hiding this comment.
The original three findings were addressed, but the follow-up panel found two remaining rollback hazards: unresolved client paths can be mistaken for absent installs, and compensation failures are still discarded after destructive replacement.
7a77833 to
c70b9bc
Compare
JAORMX
left a comment
There was a problem hiding this comment.
Two merge-blocking rollback gaps remain in the current head.
c70b9bc to
098d44a
Compare
JAORMX
left a comment
There was a problem hiding this comment.
Re-reviewed commit 098d44a against the current PR diff. The compensation fixes are directionally correct, but the new unconditional snapshot failure path regresses unmanaged uninstall and is failing the unit suite. Requesting changes for the inline blocker.
Update
|
|
Rebased onto current |
17cfb13 to
be97960
Compare
be97960 to
89a2208
Compare
jhrozek
left a comment
There was a problem hiding this comment.
Went through this one carefully. Overall the transactional design is a real improvement over both what's on main and what skillsvc does today, and the commit-by-commit progression is easy to follow. Build, vet and tests are green here.
Leaving this as a comment rather than blocking, since nothing I found will bite a user in normal operation. Two things I'd like to see addressed before merge though, and both are cheap:
restoreClientTreescan re-register a plugin whose file restore just failed (inline below, with a suggestion).TestInstallProjectScope_RollbackRestoresPreExistingStatedoesn't actually test the thing it's named for. The chmod prevents the lock write from ever happening, so the restore assertion passes trivially.
Three broader notes that don't fit on a line:
The PR description says the change is inert without TOOLHIVE_PLUGINS_LOCK_ENABLED, and that isn't quite right. The gate only covers lock-entry recording and the content digest (install.go:316, install_extraction.go:673). The snapshot/restore machinery, group-membership rollback, the uninstall reordering and requireMaterializers are all live by default, since materializeAndPersist keys snapshotting off s.clientManager != nil with no feature check. Worth correcting in the description, because it changes how much of this needs scrutiny now versus when the gate flips.
docs/arch/14-plugins-system.md needs an update. Three spots are stale now: the adapter box at lines 28-29 doesn't list EnsureRegistered/Health; section 3 still says only "Failure rolls back already-materialized clients"; and section 4 describes the old unconditional dematerialize / delete record / remove from groups flow, which is exactly what this replaces for managed installs. #6135 and #5782 both updated that doc, so there's precedent.
On divergence from skillsvc: the lock-recording halves match closely, but the failure semantics are now opposite designs. Skills discards rollback errors and fails forward on uninstall; plugins joins them and can abort. I think the plugins model is the better one, but the "Mirror of skillsvc.X" comments in install.go, install_oci.go, install_git.go and install_extraction.go are misleading now. Either qualify them with where and why they diverge, or file a follow-up to converge skills. Related: skills has an errLockWrite sentinel that classifySyncFailure keys on, and since sync is PR4 of this stack, adding it now is cheaper than re-threading it later.
One last small thing. The prevEntry snapshot in installAndRegister calls lockfile.Load outside the file lock, then recordLockState takes it via Update. Same pattern in removeManagedLockEntry. The in-process pluginLock doesn't cover a second thv process, so there's a narrow cross-process TOCTOU. Probably fine given how plugins actually get installed, but noting it.
|
@jhrozek thanks for the careful pass — everything is addressed in
|
Project-scope installs must pin plugins: in toolhive.lock.yaml, and a lock-write failure must roll back the install so nothing is left silently unpinned. Gated until sync, upgrade, and Sigstore land. Signed-off-by: Samuele Verzi <samu@stacklok.com>
Hold the per-plugin lock across materialize, DB, group, and lock-file writes so uninstall cannot race. Restore on-disk trees and lock entries when a later step fails, matching the AC that rollback undoes DB and dematerialization together. Signed-off-by: Samuele Verzi <samu@stacklok.com>
Group membership, executable modes, and client marketplace registration must come back with the files so a failed lock write cannot leave a half-installed plugin. Signed-off-by: Samuele Verzi <samu@stacklok.com>
A missing client path must abort before mutation, and every compensation failure has to travel with the original error. Signed-off-by: Samuele Verzi <samu@stacklok.com>
Compensation must surface a failed pin restore, and a Materialize that extracts then fails has to clean that client. Signed-off-by: Samuele Verzi <samu@stacklok.com>
Unmanaged uninstalls no longer snapshot without managed rollback, managed uninstalls require every adapter, and fresh installs restore pre-existing trees instead of dematerializing them on failure. Signed-off-by: Samuele Verzi <samu@stacklok.com>
Split lock-held uninstall into helpers so managed compensation paths stay readable under the gocyclo budget. Signed-off-by: Samuele Verzi <samu@stacklok.com>
Uninstall snapshots memberships so partial group removal or a DB delete failure re-adds them, and tree restores replay the adapter Health probe instead of registering unmanaged trees. Signed-off-by: Samuele Verzi <samu@stacklok.com>
Skip re-registration after a failed tree restore, preserve empty directories through hardened contained writes, degrade gracefully without a ClientManager everywhere, drop unreachable lock fallbacks and dead group helper, and exercise the prevEntry restore for real. Signed-off-by: Samuele Verzi <samu@stacklok.com>
ab70cbf to
9788c10
Compare
jhrozek
left a comment
There was a problem hiding this comment.
Re-checked everything from my earlier pass against 9788c10. All ten inline findings are addressed, plus the three from the summary — description, arch doc, and the mirror-of-skillsvc comments. CI is fully green (47/47) including E2E.
A few of the fixes came out better than what I suggested:
- Rather than duplicating the ClientManager guard,
installExtractionUpgradeDigestnow delegates tomaterializeAndPersist, which drops ~25 lines of duplicated snapshot/persist logic. I checked the semantics survive the move (resultClients/existingClients/managed/create line up with the oldbuildInstalledPlugin+Managed+Update). - The unreachable
contentDigestfallback became an explicit invariant error instead of just being deleted, so if the invariant ever breaks you get a clear failure rather than a silently recomputed digest. restoreDirpicked up directory capture in walk order, afilepath.IsLocalguard, andWriteContainedFile, and the symlink assumption is now documented ontreeSnapshotrather than left implicit.
On the test in particular, this is the bit that makes it real:
assert.Equal(t, validLockDigestAlt(), digestAtFailure,
"precondition: the lock entry must have been overwritten before the injected failure")
Injecting on the second store.Update so the entry is already rewritten is exactly right, and the added RollbackCompensationErrorIsJoined and RollbackKeepsPreExistingGroupMembership cover the two gaps I'd only mentioned in passing.
Two things I raised are deliberately not in here, both fine by me: the errLockWrite sentinel (worth picking up in PR4 where sync actually needs it) and the cross-process TOCTOU on the lockfile.Load outside the file lock at install.go:330, which I still think is acceptable given how plugins get installed.
Note this doesn't clear @JAORMX's outstanding changes-requested — that needs their re-review.
Summary
plugins:key is only useful if project-scope installs actually pin intotoolhive.lock.yaml, and a failed lock write must not leave an install silently unpinned.TOOLHIVE_PLUGINS_LOCK_ENABLEDso plugin entries stay out of the live skills trust document until verification is wired.Installupserts aplugins:entry (source,resolvedReference,digest,contentDigest) and marks the store recordmanaged. Lock-write failure rolls back the DB record (restore pre-existing, otherwise delete).Uninstallof a managed plugin removes the lock entry first; a lock-write failure aborts while the install is still intact.contentDigestis the frozen skills dirhash over the canonical plugin tree (in-memory ExtractPlugin file set), notmarketplace.json/settings.json.requiresis not materialized;requiredByis unused in this v1.Part of #6300. Stack 3/5 — schema → lock-service → install-hooks → sync → upgrade.
Type of change
Test plan
./pkg/pluginsand./pkg/plugins/pluginsvcwith the Taskfile race/ldflagsflags; lock tests cover record, gate-off, user-scope, skills-key isolation, lock-write rollback, pre-existing restore, uninstall, and uninstall abort-before-destroy)task lint-fix)Does this introduce a user-facing change?
Mostly gated, partly live:
TOOLHIVE_PLUGINS_LOCK_ENABLED=true: lock-entry recording and the content digest. With the gate on, project-scopethv ai-plugin install/uninstallwrite and removeplugins:entries intoolhive.lock.yaml.ClientManager, not the gate), group-membership compensation, and the uninstall reordering — group removal now runs before the DB delete, so a group-backend failure aborts uninstall retryably where it previously succeeded with leaked memberships.MaterializationAdaptergains two required methods,EnsureRegisteredandHealth(everything in-tree is updated). Should be called out in release notes.Implementation plan
Approved implementation plan (PR3 slice)
Mirror skills PR3 (
#5894) at theinstallAndRegisterchoke point:LockSourceas the caller's original name before internal resolution.contentDigestfrom the layer file set (same files ExtractPlugin writes) before recording.PreExistingor deletes the new record.plugins:entry first (no requires cascade).SyncRestoreyet — that bypass ofisExtractionNoOplands with sync (PR4).Special notes for reviewers
requiresmaterialization and no uninstall cascade — agreed v1: lock only explicit plugin installs.isExtractionNoOphas a comment that PR4'sSyncRestorewill need to bypass it for on-disk drift repair at the same digest.LockSource/LockResolvedReferenceare onInstallOptionsnow so sync/upgrade can preserve the original source without a follow-up options change.