Skip to content

Reject unsigned upgrade candidates in both modes - #6629

Merged
samuv merged 1 commit into
mainfrom
samuv/upgrade-unsigned-arm
Sep 11, 2026
Merged

samuv merged 1 commit into
mainfrom
samuv/upgrade-unsigned-arm

Conversation

@samuv

@samuv samuv commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

Summary

Stacked on #6628. Closes the last divergence between thv skill upgrade and thv ai-plugin upgrade on how a signer-pinned entry treats a candidate that has lost its signature, and in doing so fixes a real hole on the skills side.

Why. The keyless signer guard on both services reported an unsigned candidate as signer-change-blocked, so the CLI told the user to pass --allow-signer-change. That flag cannot resolve it: upgrade has no unsigned-consent flag, and the override re-verifies from scratch, which an unsigned artifact fails just the same. Writing the test for the override mode showed the two services then diverge:

  • Plugins: the guard is skipped under the override, the plan proceeds to install, and the install refuses the unsigned artifact with a message about the lock entry recording "no trust decision" and pointing at thv ai-plugin sync --allow-unsigned. Both claims are wrong for an entry that records a signer. A dead end with a misleading remedy.
  • Skills: the guard is skipped, the override clears the lock expectation, and isAllowedUnsigned grants an unsigned artifact to any lock-driven install with no expectation. Upgrade's own install is lock-driven. So thv skill upgrade --allow-signer-change against an unsigned candidate succeeded, dropped the recorded signer identity, and recorded the entry as unsigned: true, with no --allow-unsigned ever given. A project-wide flag meant for "the publisher changed identity" silently converted a signer-pinned skill into an unsigned exception.

What changed, mirrored on both services:

  • guardSignerChange now runs in both modes and takes the override as a parameter. Only the identity-or-provenance-field-changed arm is waived by it; an unsigned candidate is failed [unsigned-rejected] either way, and an unverifiable one fails either way. The unsigned failure names the runnable uninstall … --scope project then install … --scope project --allow-unsigned sequence, reusing projectReinstallCommand from Fail closed on keyed skill upgrade verdicts #6628.
  • resolveSignerPolicy narrows the override to false for an entry with no recorded signer. There is no signer change to authorize on such an entry, and forwarding the flag only cleared expectUnsigned for an entry the lock already accepts as unsigned.
  • Skills only: isAllowedUnsigned no longer honours the lock-driven grant when AllowSignerChange is set. A nil expectation under the override is not evidence of what the entry recorded. This is defence in depth behind the plan-time guard, since the guard is the only caller that sets the flag today.

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)
  • Generated API and CLI documentation (task docs) — no CLI or API surface changed

TestUpgrade_UnsignedCandidateRejectedAgainstSignedEntry on both services now runs in both modes and asserts failed, unsigned-rejected, the reinstall remedy, and an unchanged lock with Unsigned still false. Before the fix this test failed in override mode on both sides with exactly the two behaviours described above (skills: upgraded with Provenance nil; plugins: the misleading install-time message). TestUpgrade_UnsignedEntryUpgradesUnderSignerChangeOverride on both services proves an unsigned: true entry still upgrades under the project-wide override with no probe. TestIsAllowedUnsignedNotGrantedUnderSignerChangeOverride pins the skills install-time gate.

Changes

File Change
pkg/skills/skillsvc/upgrade.go, pkg/plugins/pluginsvc/upgrade.go Guard runs in both modes; unsigned is a failure with a runnable remedy; override narrowed to entries that record a signer
pkg/skills/skillsvc/verify.go isAllowedUnsigned lock-driven grant gated on !AllowSignerChange
*_test.go on both services Override-mode coverage for the unsigned arm; unsigned-entry-under-override regression; install-time gate unit test
docs/arch/12-skills-system.md, docs/arch/14-plugins-system.md Trust-model paragraph no longer claims --allow-signer-change covers an unsigned candidate

Does this introduce a user-facing change?

Yes.

  • thv skill upgrade --allow-signer-change no longer converts a signer-pinned skill into an unsigned entry when the candidate is unsigned. It fails with unsigned-rejected and the reinstall command. Anyone relying on the old behaviour was relying on the override doubling as --allow-unsigned, which it was never documented to do.
  • On both thv skill upgrade and thv ai-plugin upgrade, an unsigned candidate under a signer-pinned entry reports failed [unsigned-rejected] with a runnable remedy instead of signer change blocked pointing at a flag that cannot resolve it.

Special notes for reviewers

  • The skills-side conversion is on main today. It needs a project-scoped skill with a recorded signer, a newer unsigned artifact at the same source, and --allow-signer-change. That is a plausible sequence for someone told by the previous message to pass that flag.
  • The override is now run through the guard rather than skipping it, which adds one verification call on the override path. That call was always going to happen at install time anyway, so this moves it earlier and lets the plan fail without touching the lock.
  • The resolveSignerPolicy narrowing for anchorless entries is what makes the skills install-time gate safe to add: a pre-verification or unsigned: true entry never sees AllowSignerChange at install, so the lock-driven grant still applies to it as before.

🤖 Generated with Claude Code

@github-actions github-actions Bot added the size/S Small PR: 100-299 lines changed label Sep 10, 2026
@samuv
samuv added this pull request to stack #6630 September 10, 2026 09:30
@samuv samuv self-assigned this Sep 10, 2026
@samuv
samuv force-pushed the samuv/upgrade-unsigned-arm branch from 96bc6df to ac8994f Compare September 10, 2026 09:35
@github-actions github-actions Bot added size/S Small PR: 100-299 lines changed and removed size/S Small PR: 100-299 lines changed labels Sep 10, 2026
@codecov

codecov Bot commented Sep 10, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 78.90%. Comparing base (c25e508) to head (8bc9582).
⚠️ Report is 3 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #6629      +/-   ##
==========================================
- Coverage   78.94%   78.90%   -0.05%     
==========================================
  Files         781      781              
  Lines       77784    77800      +16     
==========================================
- Hits        61410    61387      -23     
- Misses      16369    16408      +39     
  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.

JAORMX
JAORMX previously approved these changes Sep 10, 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.

Panel review completed against ac8994fa9b74a7fcc8926ccea313da6a3d258e93.

No merge blockers found. The guard now remains active under --allow-signer-change, while only the actual signer/provenance-change arm is waived. An unsigned candidate consequently remains rejected without mutating the recorded signer state. The paired skills install-time gate supplies appropriate defence in depth, and coverage includes both override modes plus already-unsigned entries.

Exact-head CI is green.

stack merge was automatically disabled September 10, 2026 11:23

Pull Request is not mergeable

Base automatically changed from samuv/skills-upgrade-keyed-verdict to main September 10, 2026 11:23
@samuv
samuv force-pushed the samuv/upgrade-unsigned-arm branch from ac8994f to 2b48bc0 Compare September 10, 2026 11:23
@github-actions github-actions Bot added size/S Small PR: 100-299 lines changed and removed size/S Small PR: 100-299 lines changed labels Sep 10, 2026
The keyless signer guard reported an unsigned candidate as a signer
change, so the CLI pointed at --allow-signer-change. That flag cannot
resolve it: upgrade has no unsigned-consent flag, and the override
re-verifies from scratch, which an unsigned artifact fails just the
same. On plugins that was a dead end with a misleading remedy. On
skills it was worse: the override cleared the lock expectation, and
isAllowedUnsigned granted any lock-driven install with no expectation,
so `thv skill upgrade --allow-signer-change` silently turned a
signer-pinned skill into an unsigned entry with no --allow-unsigned
ever given.

Run the guard in both modes and waive only the identity-change arm
under the override; unsigned and unverifiable candidates fail either
way, naming the project-scoped reinstall that records the exception.
Narrow the override to entries that record a signer, since an
anchorless entry has nothing to change, and stop the skills install
grant from applying when the override cleared the expectation.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Samuele Verzi <samu@stacklok.com>
@samuv
samuv force-pushed the samuv/upgrade-unsigned-arm branch from 2b48bc0 to 8bc9582 Compare September 10, 2026 14:12
@github-actions github-actions Bot added size/S Small PR: 100-299 lines changed and removed size/S Small PR: 100-299 lines changed labels Sep 10, 2026
@samuv
samuv merged commit 2bb9996 into main Sep 11, 2026
55 of 56 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/S Small PR: 100-299 lines changed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants