Skip to content

Add plugin key rotation workflows - #6663

Open
samuv wants to merge 3 commits into
samuv/skills-key-reanchorfrom
samuv/plugins-key-reanchor
Open

samuv wants to merge 3 commits into
samuv/skills-key-reanchorfrom
samuv/plugins-key-reanchor

Conversation

@samuv

@samuv samuv commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Plugin projects have the same key-rotation and adoption gap as skills. This mirrors the lock-driven re-anchor contract so both user-facing surfaces apply the same trust rules.
  • Add --public-key support to plugin upgrade and sync adoption, require --allow-signer-change for replacement anchors, and retain the recorded key whenever it still verifies.
  • Share project transaction locking between skills and plugins while preserving per-plugin locking for user-scoped operations.
  • Update the CLI, API, SDK, architecture documentation, and regression coverage for plugin key rotation.

Fixes #6640

Type of change

  • Bug fix
  • New feature
  • Refactoring (no behavior change)
  • Dependency update
  • Documentation
  • Other (describe):

Test plan

  • Unit tests (task test)
  • E2E tests (task test-e2e)
  • Linting (task lint-fix)
  • Manual testing (describe below)

task gen, task docs, and task build pass. Changed-package unit tests pass. The full task test run reaches five unrelated pkg/api failures because no container runtime is available. The focused plugin key-trust E2E suite compiles and reaches setup, where it stops for the same missing runtime.

Changes

Area Change
Plugin CLI and API Accept and validate replacement public keys for upgrade and sync adoption.
Plugin service Re-anchor only after successful verification and update trust state transactionally.
Project transactions Move shared project locking into pkg/projecttxn for skills and plugins.
Generated reference Regenerate CLI, OpenAPI, and Go SDK outputs.
Tests and architecture Cover rotation, adoption, rollback, concurrency, and documented trust semantics.

Does this introduce a user-facing change?

Yes. thv ai-plugin upgrade --allow-signer-change --public-key <PUBLIC_KEY_PATH> can re-anchor a key-pinned OCI plugin, and thv ai-plugin sync --adopt --public-key <PUBLIC_KEY_PATH> can adopt an installed key-signed plugin.

Implementation plan

Approved implementation plan
  1. Mirror the approved skill key-rotation contract for plugin upgrade and sync adoption.
  2. Keep lock-first policy selection, try the recorded key first, and consider the replacement only after a conclusive mismatch.
  3. Reuse strict OCI snapshots for explicit replacement-key re-anchors and preserve ordinary upgrade compatibility.
  4. Extract project transaction locking for shared skill and plugin use while retaining user-scope plugin locks.
  5. Add unit and E2E coverage, then regenerate CLI and API documentation.

Special notes for reviewers

This is the second PR in the stack and is based on #6662. The approved PR-size exception keeps plugin parity, shared transaction extraction, tests, and generated outputs in one reviewable change.

@samuv
samuv added this pull request to stack #6664 September 15, 2026 13:17
@github-actions github-actions Bot added the size/XL Extra large PR: 1000+ lines changed label Sep 15, 2026
@samuv samuv mentioned this pull request Sep 15, 2026
10 tasks
@codecov

codecov Bot commented Sep 15, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 82.50000% with 84 lines in your changes missing coverage. Please review.
✅ Project coverage is 79.16%. Comparing base (987021e) to head (dbe24af).

Files with missing lines Patch % Lines
pkg/plugins/pluginsvc/upgrade.go 74.73% 71 Missing ⚠️
pkg/projecttxn/transaction.go 82.85% 6 Missing ⚠️
pkg/plugins/pluginsvc/sync.go 94.11% 3 Missing ⚠️
pkg/plugins/pluginsvc/verify.go 92.00% 2 Missing ⚠️
pkg/plugins/pluginsvc/install_extraction.go 88.88% 1 Missing ⚠️
pkg/skills/lockfile/lockfile.go 87.50% 1 Missing ⚠️
Additional details and impacted files
@@                      Coverage Diff                      @@
##           samuv/skills-key-reanchor    #6663      +/-   ##
=============================================================
+ Coverage                      79.10%   79.16%   +0.05%     
=============================================================
  Files                            786      787       +1     
  Lines                          78848    79203     +355     
=============================================================
+ Hits                           62375    62699     +324     
- Misses                         16468    16499      +31     
  Partials                           5        5              

☔ 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 samuv/plugins-key-reanchor branch from 3592c58 to 32904c3 Compare September 15, 2026 14:06
@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 Sep 15, 2026
@samuv
samuv force-pushed the samuv/plugins-key-reanchor branch from 32904c3 to f155300 Compare September 16, 2026 08:31
@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 Sep 16, 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.

Reviewed the plugin-only stack against #6662: initially 32904c3 vs 560a05b, then verified the exact trees at f155300 vs d655077. The rebase changes only go.mod/go.sum relative to the initially reviewed plugin head; all implementation blobs are identical.

Spec: the plugin flow follows #6640's old-key-first, lock-first, explicit replacement and offline-adoption contract. No separate spec mismatch found.
Standards: the new projecttxn package exposes implementation-oriented Lock/LockPath helpers; reducing that API or documenting its canonical-root precondition would be worthwhile, but current production callers normalize roots, so this is nonblocking.
Domain: one newly added metadata-refresh rollback path can leave SQLite metadata inconsistent with the lock after cancellation (inline). Shared project transaction locking and the bounded trust-only rollback otherwise follow the intended design.

I excluded inherited SDK issues and concerns outside the documented contract, including crash-atomic journaling and treating offline adoption as authentication of already-materialized filesystem bytes. The keyless fallback for an unrecorded legacy entry is also consistent with the per-entry policy, not a replacement-key bypass.

Nonblocking: document public_key's base64-encoded DER SPKI representation and allowed option combinations in the API fields so generated SDK/OpenAPI docs explain the wire contract.

Core #297 has now been re-reviewed and approved at e5c9df0. The local cancellation issue on skills #6662 still needs its separate fix. All 46 checks passed on the originally reviewed plugin head; the new head's CI was still running at the last check. No local tests or linters were run.

Comment on lines +128 to +132
if opts.RefreshMetadata && !becomesManaged {
updated := buildInstalledPlugin(opts, scope, clientTypes, existing.Clients)
updated.Managed = existing.Managed
if err := s.store.Update(ctx, updated); err != nil {
return nil, err

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.

[P2] Make rollback of the new metadata-only update independent of request cancellation

An allowed reference change at the same digest now persists the refreshed Reference/signature bundle here before the expected-entry lock CAS. If the request is cancelled after this update and that CAS then conflicts (for example, an older process or external writer changed the lock), rollbackInstall tries to restore the previous database row using the cancelled context. SQLite can reject that transaction immediately. The conflict path correctly preserves the competing lock entry, but the failed upgrade can leave its own new metadata in SQLite, inconsistent with that lock.

Managed is preserved here, so this is distinct from the unmanaged-row symptom in the skills variant. Before this change the equal-digest no-op did not rewrite metadata. Please run the compensating store/file operations under a bounded context.WithoutCancel context, as persistTrustOnlyUpgrade already does, retaining rollback errors. Add a cancellation-plus-CAS-conflict regression asserting the old DB record is restored while the competing lock entry is not overwritten.

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 in dbe24af. Plugin rollback now runs store/file compensation under a bounded five-second context detached from request cancellation while still preserving rollback errors. The regression cancels the request and forces the expected-entry CAS conflict, then asserts the old database record is restored and the competing lock entry is left untouched. The full pkg/plugins/pluginsvc suite passes with -race on the restacked head, inheriting toolhive-core v0.0.49 from #6662.

@samuv
samuv force-pushed the samuv/plugins-key-reanchor branch from f155300 to ab54e7c Compare September 16, 2026 09:40
@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 Sep 16, 2026
@samuv
samuv force-pushed the samuv/plugins-key-reanchor branch from ab54e7c to dbe24af Compare September 16, 2026 16:17
@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 Sep 16, 2026
Signed-off-by: Samuele Verzi <samu@stacklok.com>
Signed-off-by: Samuele Verzi <samu@stacklok.com>
Signed-off-by: Samuele Verzi <samu@stacklok.com>
@samuv
samuv force-pushed the samuv/plugins-key-reanchor branch from dbe24af to bd94161 Compare September 16, 2026 16:38
@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 Sep 16, 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.

Key-pinned entries need a lock-driven re-anchor path (skills and plugins)

2 participants