Approving a new version activates it only about half the time: #91 activates the lexicographically LOWEST digest, and a digest is a hash #242
Labels
No labels
code-review
correctness
dos
performance
security
severity/high
severity/low
severity/medium
tech-debt
Kind/Breaking
Kind/Bug
Kind/Documentation
Kind/Enhancement
Kind/Feature
Kind/Security
Kind/Testing
Priority
Critical
Priority
High
Priority
Low
Priority
Medium
Reviewed
Confirmed
Reviewed
Duplicate
Reviewed
Invalid
Reviewed
Won't Fix
Status
Abandoned
Status
Blocked
Status
Need More Info
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
h-dv/code-index#242
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Found by the #237 lane. Operator decision taken: fix activation for both paths — the most-recently-approved digest activates, rather than the lexicographically-lowest one.
The defect
ApprovalRecord::setde-duplicates by digest. So when an operator approves a new version of a package they already have, both grants stand — and by #91 the one that activates is the lexicographically lowest digest.A digest is a hash. There is no relationship between its byte ordering and which version is newer. So on any given pair it is a coin the bytes already flipped:
It is not an auto-update bug —
plugin addhas it tooThis surfaced in #237 because an unattended apply has no reader, but the defect is in the manual path and predates auto-update entirely.
plugin addcannot even report it:domain_betweenruns beforeStore::install, soApprovedPackageIds::resolvecannot see the candidate at the point the command would say something. A human meets the fact on their nextplugin status, if they look — and they have no reason to look, because they just ran a command that succeeded.So the current experience is: you approve v2, the command succeeds, and v1 keeps extracting. Silently, half the time.
Why "identical to
plugin add" was the wrong acceptance criterion#237 asked that an auto-apply leave state identical to the operator having run
plugin addby hand. That is satisfiable, and it was the wrong target: parity with a path that silently no-ops half the time buys a feature that silently no-ops half the time. Worth recording, because the criterion looked obviously correct when it was written.The decision
The most-recently-approved digest activates.
Chosen over the two alternatives deliberately:
plugin addshadowing half the time. Rejected: it treats the symptom in one caller.update_activeline already names when the new version is approved-but-shadowed and gives the repair, so nothing is silent. Rejected as an endpoint: honest is not the same as working, and there is no human present on the auto path to act on it.Fixing activation fixes both callers from one clause, which is this repo's standing rule.
What this changes, and why it needs its own measurement
This changes #91's activation semantics for every package, not just updated ones. That is the whole risk, and it is why this is its own issue rather than a fold-in.
plugin_update'supdate_activedisclosure stays either way: after this lands it should report the new digest as live, and the fact that it keeps working across the change is one of the cheaper checks available.What a fix must prove
plugin addandplugin update --autoreach the same activation, from the same clause.plugin rollbackstill works: rolling back to an older digest must activate the older one, which means "most recently approved" has to mean approval order, not version order or install time. Worth stating explicitly because those three coincide in the happy path and diverge exactly on rollback.What must NOT be done
ApprovalRecord::set's de-duplication. Digest identity is correct; the bug is downstream, in which of the retained grants wins.Related
#237 (where this was found; its
update_activedisclosure is the interim honesty), #91 (the activation rule being changed), #86 F1 (undecline/ withdrawn digests, which the fix must not disturb).Filed 2026-09-09 against
masterf75ee20.Fixed on
masterat24486d5(merged0279c0b, inorigin/master). 6 mutations run, 6 RED, 0 survivors — and the lane found three things wrong with this issue, two of which would have shipped a defect if followed literally.The rule
Approval::approved_seq: u64,#[serde(default)], whose only writer isApprovalRecord::setstampingmax(existing) + 1.ApprovedPackageIds::from_grantssorts each id's digests by(Reverse(seq), digest ascending);active_digesttakesfirst().The signature change is load-bearing:
from_pairs(Item = (String, String))becamefrom_grants(Item = (&Approval, String)), so the digest and its sequence arrive together, from the record. A caller cannot supply one without the other or pair a digest with somebody else's sequence.resolveandapproved_activation_with_idsare the only two callers, andplugin add,plugin enable,plugin update --autoand the MCPplugin_addtool all write throughsetand read throughresolve— so no call site was patched.Ties keep #91's rule. A record written before the field reads back
0everywhere, ties, and the lowest digest settles it — so nothing anyone holds today moves until they next approve something.The before/after enumeration — exactly one row moved
Five ids, five states (
solo,up,dn,gone,spare):dndid not move because #91 already happened to pick the second approval there — which is what makes "only where it was wrong" a measurement rather than an assertion.Both digest orderings, RED before / GREEN after
upgrade_pair(…, v2_sorts_below)measures candidate versions viaStoredPackage::parseand returns a pair in the requested order, asserting the ordering held before anything else. Againstmaster:The
v2_sorts_below = truearm passed against the unfixed binary — that is precisely the 50% that would have looked like proof. GREEN after: 13 passed.Three corrections to this issue
1.
plugin rollbackis not the rollback command. It restores the retained index generation and its own doc says it "does NOT withdraw the approval". It names no package. The requirement I wrote was right; the command in it was wrong. The operator-level rollback isplugin enable <the older digest>, and that is whatre_approving_an_older_digest_rolls_the_activation_backexercises, in both digest orderings.2. I missed the witness, and following this issue literally would have shipped a live-daemon hole.
approval::witness_inis what a running daemon polls to notice activation moved. A rollback re-approves identical bytes under an identical grant, so grants, capabilities, bridges,.ciplengths, sidecars and trust set are all byte-for-byte unchanged — the CLI would have printed the right thing while the daemon kept extracting under the version just rolled back from, silently and permanently until restart.seq=is now in the witness, with an anti-vacuity block proving nothing else about it moved.3. "The bug is downstream, in which retained grant wins" is true of the decision but not of the data. The winner is decided downstream; the fact it needs can only be recorded upstream, in
set.set's de-duplication is untouched — it now also stamps the order. Worth stating because my phrasing read as "do not touchset".Also:
_prdoc/specs/90-activation-on-demand.mdhad rejected this option as "needs a timestampApprovaldoes not carry, and adding one to answer a tie-break is a schema change for a diagnostic". Both halves are false — a counter needs no clock, and#[serde(default)]needs no schema bump. I verified the compatibility claim myself rather than accepting it: there is nodeny_unknown_fieldsanywhere inapproval.rs, so an older binary reading a new record ignores the field and falls back to #91's tie-break, which is what M242-3 grades. The spec bullet is corrected in place rather than deleted, so the original reasoning survives beside the reversal.Mutations
setstampsapproved_seq = 0from_grantssorts by digest alone (#91 restored)next_approved_seqreturnslen() + 1seq=from the witnessdeclinekeeps the grant / only ids with >1 grantI re-ran M242-2 myself on the merged tree: RED across the suite. Restored by
cp, md5 verified, re-run 13 passed.Three tests that would have graded a coin were FIXED rather than reported as survivors:
plugin_update_cli's two real packages happened to sort favourably, andplugin_duplicate_id_cliandduplicate_package_id_disclosurereturned(min, max). Each now measures an unfavourable ordering deliberately.A limitation operators will meet
The fix moves no activation on a record written before it. A project already holding two grants that never runs another approve command keeps its (possibly wrong) activation — there is no approval order recorded for those grants, and inventing one would be guessing. The repair is one command, and
PACKAGE_ID_ALREADY_ACTIVE's witness now names it first:code-index plugin enable <this digest>re-approves and activates while keeping both grants.Verification
fmt0 ·clippy --workspace --all-targets -D warnings0 ·RUSTDOCFLAGS="-D warnings" cargo doc --document-private-items0 ·cargo test --workspace330 suites 0 failed ·COSI_E2E_LEG=daemon62 suites.tests/corpus/baseline.jsonuntouched. CI 11/11 green on1d3228e, which carries this commit. Post-push re-run by me:duplicate_package_id13 passed, EXIT=0.Closing — and noting that this issue sat fixed-and-open for several hours, which is the exact defect I closed six other issues for today.