Repository navigation
Trust map keys inline items by description, so same-named components in one pack collide and re-prompt forever #393
Description
Symptom
Latent — no shipped pack reproduces it today, but the manifest that would is legal.
A pack that declares two shell: components with the same displayName, or two doctor checks with
the same name, records one trust hash for the pair. On every mcs update the other one is
reported as new-or-modified, the trust prompt fires, and approving cannot fix it: the map is
rebuilt from the same colliding key, so the next run asks again. Non-interactive runs answer the
prompt with its false default, so the pack's registry refresh is declined instead.
This is the same loop the .mcpServer case hit until its description was scoped to
component.id — there it was guaranteed rather than latent, because every MCP item shared one
constant description string.
Root cause
PackTrustManager.syntheticKey(for:) keys every inline item (relativePath == nil) as
inline:<sha256(item.description)>. The description is whatever the construction site wrote, so
uniqueness is a convention each case has to remember rather than something the types enforce:
| Item | Description embeds | Unique? |
|---|---|---|
.hookInterpreter |
component.id |
yes — validated by ManifestError.duplicateComponentID |
.mcpServer |
component.id |
yes — was a constant string, fixed |
.shellCommand |
component.displayName |
no — never validated |
.doctorCommand / .doctorScript / .fixScript |
check.name |
no — never validated |
computeScriptHashes writes the analyzed items into one dictionary in declaration order, so the
last write wins for a colliding key. mismatch then compares the earlier item against its
sibling's hash and returns .mismatched, which puts it in newOrChanged on every check.
PackUpdater.trustNeedsRenewal runs on the already-up-to-date path too, so it fires on every
invocation, not only after a fetch.
Fix
Two levels; they are not exclusive.
- Per-site (recommended now). Scope
.shellCommandtocomponent.idand the doctor items to
component.idplus the check name. One line each, one regression test each. Blast radius is
limited to packs that actually ship those item types: their keys change, so each sees one
.neverTrustedprompt on the next update. - Structural (later). Stop keying on the description at all — give
TrustableIteman explicit
identity (component id + a type discriminator) and havesyntheticKeyhash that, so no future
item type can collide by forgetting to write a unique description. This forecloses the class,
but every inline key changes shape at once, so every installed pack re-prompts once whether or
not it was ever affected. Worth bundling with a change that already forces a re-trust rather
than doing pre-emptively.
Either way, changing a description changes the key, so affected packs re-prompt once.
TrustableItem.representsDefaultBehavior would suppress that, but it must not be used here: these
items run third-party code, and waiving them permanently would also silence a genuinely new one
added by a later pack update.
Cheap complement: a PackHeuristics warning at mcs pack validate time when a pack declares two
components with the same displayName, or two doctor checks with the same name.
Activity