You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
#6442 shipped cosign key-pair verification for skills (#6447, #6478) and plugins (#6524, #6525, #6528) with a deliberately narrow v1 scope, recorded in docs/arch/12-skills-system.md ("Scope for v1") and mirrored in 14-plugins-system.md:
--public-key is accepted on install only.
upgrade and sync use the anchor the lock already records and take no key of their own.
sync --adopt refuses a key-signed install outright.
There is no in-place re-anchor: key → different key and keyless → key both refuse with "uninstall and reinstall with --public-key".
Every one of those is an honest fail-closed state, and v1 only made the messages truthful. This issue tracks the follow-up that was explicitly deferred (option A in the design comment on #6442).
Problem
A publisher rotating their cosign key, or moving from keyless to a key, leaves every consuming project with N key-pinned entries and no lock-driven way to move them: each one is a thv skill uninstall … --scope project followed by thv skill install … --scope project --public-key <new> (same for thv ai-plugin). upgrade --allow-signer-change cannot help by design — its contract is "verify chain of trust only and re-record what is observed", and a key-pair bundle carries nothing to observe, so honouring a key under it would re-anchor on the caller's say-so alone.
Adoption has the same shape: sync --adopt of an existing key-signed project install has no way to supply the anchor, so it refuses and points at a fresh install.
Proposed scope
Same contract on both surfaces, same PR sequencing as #6442 (skills first, plugins mirror, or both together if the diff stays reviewable):
--public-key on upgrade. Accepted only together with --allow-signer-change; the pair means "I am re-anchoring this entry to this key". The candidate is verified against the supplied key (VerifyOCIWithKey), and only on success is Provenance.publicKey rewritten. Without the key, --allow-signer-change keeps today's semantics (key → keyless is the only move it can make on its own). A supplied key without --allow-signer-change is a 400, as resolveKeyAnchor already enforces on install.
--public-key on sync --adopt. The stored bundle is verified against the supplied key with VerifyBundleOfflineWithKey against the installed digest, and adoption records publicKey instead of refusing.
resolveKeyAnchor gains the re-anchor arm so a locked key and a different supplied key, under AllowSignerChange, verify against the supplied one and record it — replacing today's keyAnchorConflict.
Context
#6442 shipped cosign key-pair verification for skills (#6447, #6478) and plugins (#6524, #6525, #6528) with a deliberately narrow v1 scope, recorded in
docs/arch/12-skills-system.md("Scope for v1") and mirrored in14-plugins-system.md:--public-keyis accepted oninstallonly.upgradeandsyncuse the anchor the lock already records and take no key of their own.sync --adoptrefuses a key-signed install outright.--public-key".Every one of those is an honest fail-closed state, and v1 only made the messages truthful. This issue tracks the follow-up that was explicitly deferred (option A in the design comment on #6442).
Problem
A publisher rotating their cosign key, or moving from keyless to a key, leaves every consuming project with N key-pinned entries and no lock-driven way to move them: each one is a
thv skill uninstall … --scope projectfollowed bythv skill install … --scope project --public-key <new>(same forthv ai-plugin).upgrade --allow-signer-changecannot help by design — its contract is "verify chain of trust only and re-record what is observed", and a key-pair bundle carries nothing to observe, so honouring a key under it would re-anchor on the caller's say-so alone.Adoption has the same shape:
sync --adoptof an existing key-signed project install has no way to supply the anchor, so it refuses and points at a fresh install.Proposed scope
Same contract on both surfaces, same PR sequencing as #6442 (skills first, plugins mirror, or both together if the diff stays reviewable):
--public-keyonupgrade. Accepted only together with--allow-signer-change; the pair means "I am re-anchoring this entry to this key". The candidate is verified against the supplied key (VerifyOCIWithKey), and only on success isProvenance.publicKeyrewritten. Without the key,--allow-signer-changekeeps today's semantics (key → keyless is the only move it can make on its own). A supplied key without--allow-signer-changeis a 400, asresolveKeyAnchoralready enforces on install.--public-keyonsync --adopt. The stored bundle is verified against the supplied key withVerifyBundleOfflineWithKeyagainst the installed digest, and adoption recordspublicKeyinstead of refusing.resolveKeyAnchorgains the re-anchor arm so a locked key and a different supplied key, underAllowSignerChange, verify against the supplied one and record it — replacing today'skeyAnchorConflict.resolveSignerPolicy, Fail closed on keyed skill upgrade verdicts #6628/Reject unsigned upgrade candidates in both modes #6629): a project-wide--allow-signer-change --public-keymust only re-anchor the entries whose candidate actually verifies against the new key, and must never unpin an entry whose candidate still verifies against its old key.Invariants to keep
--allow-signer-changealone never records a key. The lock is the only source of a key on upgrade/sync unless the caller names one explicitly.--allow-unsignednever substitutes for a key in either direction (ErrKeySignedis distinct fromErrUnsignedfor exactly this reason).judgeKeyedCandidate's undecided verdict fails in both modes).Related
--keypush signing has no install-time verification path (plugins) #6442 (parent decision and v1)docs/arch/12-skills-system.md#project-lock-file,docs/arch/14-plugins-system.md#trust-model