Skip to content

Add plugin lock-file sync - #6316

Open
samuv wants to merge 4 commits into
plugins-lock/03-install-hooksfrom
plugins-lock/04-sync
Open

Add plugin lock-file sync#6316
samuv wants to merge 4 commits into
plugins-lock/03-install-hooksfrom
plugins-lock/04-sync

Conversation

@samuv

@samuv samuv commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Why: A lock file that is only written on install cannot restore a fresh clone or detect on-disk tampering. Sync is the restore half of RFC THV-0080 for plugins:.
  • What:
    • thv ai-plugin sync and POST /plugins/sync restore project plugins from toolhive.lock.yaml (--check / --adopt / --prune, confirmation gate, exit codes 0/2/3/4).
    • Missing or drifted entries are reinstalled at their pinned digest via buildPinnedReference — never re-resolved from source. SyncRestore bypasses the same-digest no-op so on-disk drift is repaired.
    • --check hashes every client's canonical plugin directory (contentDigest), not marketplace.json.
    • --adopt records unmanaged project-scope installs without requiring --allow-unsigned (plugin Sigstore is Stack 2).
    • Gated by TOOLHIVE_PLUGINS_LOCK_ENABLED (403 when off). Upgrade is a 501 stub until the next stack PR.

Part of #6300. Stack 4/5 — schema → lock-service → install-hooks → sync → upgrade.

Type of change

  • New feature

Test plan

  • Unit tests (./pkg/plugins/pluginsvc sync/pin tests and ./pkg/api/v1 sync endpoint tests, with the Taskfile race/ldflags flags)
  • Linting (task lint-fix)

Does this introduce a user-facing change?

No by default — the feature is inert unless TOOLHIVE_PLUGINS_LOCK_ENABLED=true. With the gate on, thv ai-plugin sync restores project plugins from toolhive.lock.yaml.

Special notes for reviewers

  • pin.go is copied from skillsvc rather than extracted — agreed: reuse, don't extract.
  • Adopt does not require --allow-unsigned. Until Stack 2, lock validation allows an entry with neither provenance nor unsigned. Requiring the flag here would make every plugin adopt fail.
  • Upgrade on *service is a 501 stub so PluginLockService type-asserts for /sync. The next PR replaces it.
  • Confirmation and exit codes reuse cmd/thv/app/skill_confirm.go and exitcode.go.

@github-actions github-actions Bot added the size/XL Extra large PR: 1000+ lines changed label Aug 13, 2026
@samuv samuv self-assigned this Aug 13, 2026
@github-actions github-actions Bot added size/XL Extra large PR: 1000+ lines changed and removed size/XL Extra large PR: 1000+ lines changed labels Aug 13, 2026
@codecov

codecov Bot commented Aug 13, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 67.69663% with 115 lines in your changes missing coverage. Please review.
⚠️ Please upload report for BASE (plugins-lock/03-install-hooks@c70b9bc). Learn more about missing BASE report.

Files with missing lines Patch % Lines
pkg/plugins/pluginsvc/sync.go 66.50% 41 Missing and 27 partials ⚠️
pkg/plugins/adapters/claudecode.go 52.94% 12 Missing and 12 partials ⚠️
pkg/plugins/client/client.go 0.00% 11 Missing ⚠️
pkg/plugins/adapters/codex.go 57.14% 3 Missing and 3 partials ⚠️
pkg/plugins/pluginsvc/pin.go 88.23% 3 Missing and 3 partials ⚠️
Additional details and impacted files
@@                       Coverage Diff                        @@
##             plugins-lock/03-install-hooks    #6316   +/-   ##
================================================================
  Coverage                                 ?   72.95%           
================================================================
  Files                                    ?      747           
  Lines                                    ?    78910           
  Branches                                 ?        0           
================================================================
  Hits                                     ?    57571           
  Misses                                   ?    17263           
  Partials                                 ?     4076           

☔ 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.

@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 found one state-consistency bug plus missing CLI E2E coverage and a thin-wrapper boundary violation. Please address the inline findings before merge.

Comment thread pkg/plugins/pluginsvc/sync.go
Comment thread cmd/thv/app/ai_plugin_sync.go
Comment thread cmd/thv/app/ai_plugin_sync.go
@samuv
samuv force-pushed the plugins-lock/04-sync branch from eb93233 to 735241d Compare August 14, 2026 08:16
@github-actions github-actions Bot added size/XL Extra large PR: 1000+ lines changed and removed size/XL Extra large PR: 1000+ lines changed labels Aug 14, 2026
@samuv
samuv force-pushed the plugins-lock/04-sync branch from 735241d to 68fb269 Compare August 14, 2026 08:46
@github-actions github-actions Bot added size/XL Extra large PR: 1000+ lines changed and removed size/XL Extra large PR: 1000+ lines changed labels Aug 14, 2026
@samuv
samuv force-pushed the plugins-lock/04-sync branch from 68fb269 to b820980 Compare August 14, 2026 08:57
@github-actions github-actions Bot added size/XL Extra large PR: 1000+ lines changed and removed size/XL Extra large PR: 1000+ lines changed labels Aug 14, 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.

The adoption rollback and CLI E2E findings were addressed. The follow-up panel found a concurrency hole in adoption compensation and incomplete Claude Code registration health checks.

Comment thread pkg/plugins/pluginsvc/sync.go Outdated
Comment on lines +257 to +271
if err := recordLockEntry(pl.ProjectRoot, lockEntryInput{
Name: pl.Metadata.Name,
Version: pl.Metadata.Version,
Source: source,
ResolvedReference: resolved,
Digest: pl.Digest,
ContentDigest: contentDigest,
}); err != nil {
return fmt.Errorf("writing lock entry: %w", errors.Join(errLockWrite, err))
}
pl.Managed = true
if err := s.store.Update(ctx, pl); err != nil {
remErr := removeLockEntry(plugins.UninstallOptions{
Name: pl.Metadata.Name, Scope: plugins.ScopeProject, ProjectRoot: pl.ProjectRoot,
})

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.

High — failed adoption can remove a concurrent operation’s valid lock entry. Adoption does not take the existing per-plugin mutex. If install/sync upserts the same entry after recordLockEntry but before this DB update fails, the unconditional removeLockEntry deletes the other operation’s pin while its DB row can remain managed. Please serialize adoption with s.locks, re-read state under that lock, and compensate by restoring the entry observed before adoption rather than blindly removing by name.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed. adoptPlugin now holds the per-plugin lock, re-reads the row, and snapshots the existing lock entry. If Update(Managed=true) fails, we restore that snapshot (or remove the name only when none existed). A test covers restoring a pre-existing pin. 2ebd4a847.

Comment on lines +143 to +178
mp, err := readClaudeMarketplace(claudeMarketplaceFilePath(filepath.Dir(dir)))
if err != nil {
return err
}
found := false
for _, p := range mp.Plugins {
if p.Name == req.Name {
found = true
break
}
}
if !found {
return fmt.Errorf("plugin %q is missing from marketplace.json", req.Name)
}

settingsPath := a.settingsPath(req.Scope, req.ProjectRoot)
content, err := os.ReadFile(settingsPath) // #nosec G304 -- path is a known tool config file location
if err != nil {
return fmt.Errorf("reading settings.json: %w", err)
}
root, err := parseSettings(content, settingsPath)
if err != nil {
return err
}
enabled, ok := root["enabledPlugins"].(map[string]any)
if !ok {
return fmt.Errorf("plugin %q is not enabled in settings.json", req.Name)
}
val, ok := enabled[pluginKey(req.Name)]
if !ok {
return fmt.Errorf("plugin %q is not enabled in settings.json", req.Name)
}
if on, _ := val.(bool); !on {
return fmt.Errorf("plugin %q is disabled in settings.json", req.Name)
}
return nil

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.

Medium — health can report a Claude plugin current while Claude cannot discover it. This verifies enabledPlugins and only the marketplace entry’s name. It does not validate the extraKnownMarketplaces.toolhive directory registration written by enablePluginInSettings, nor that the matching marketplace entry has the expected local source path. Removing/corrupting either leaves the plugin unavailable but sync --check passes. Please validate the complete registration tuple (marketplace source/path plus plugin name/source), not just presence by name.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed. Claude Health now requires marketplace source ./<name> plus extraKnownMarketplaces.toolhive as a directory source whose path is the marketplace root. 2ebd4a847.

samuv added 4 commits August 14, 2026 14:52
Restore project plugins from the lock file via thv ai-plugin sync
and POST /plugins/sync, gated by TOOLHIVE_PLUGINS_LOCK_ENABLED.
A failed DB update after writing the lock entry left the plugin
untracked; remove the entry so the next sync can retry. Cover
thv ai-plugin sync exit codes the same way skills lock does.
Sync must not report a plugin current when a requested client is
missing or marketplace registration is gone, and must refuse to
adopt a local tag that cannot be restored later.
Adoption must hold the per-plugin lock and put back the
pre-existing entry if marking Managed fails.
@samuv
samuv force-pushed the plugins-lock/04-sync branch from b820980 to 2ebd4a8 Compare August 14, 2026 12:52
@github-actions github-actions Bot added size/XL Extra large PR: 1000+ lines changed and removed size/XL Extra large PR: 1000+ lines changed labels Aug 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/XL Extra large PR: 1000+ lines changed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants