The gap
#1143 brought each grant-ledger row's recipientEncPk/recipientIdentityPk under owner authority (per-row ownerSig) and decided the policy for a row that fails it: skip, not abort.
crates/engine/src/rotation/reseal.rs::adopt_recipients seals no grant blob for an unattested row.
crates/engine/src/net/rotation.rs::remint_grants drops it from the moved root's set.
Both are correct — refusing would let the grantee a rotation is cutting veto its own revocation — but neither is reported to the host. The only trace is the published record itself: a committed tag with no blob, which grants::revocation::classify reads as revocation. The owner's UI therefore shows an innocent co-grantee as revoked with no indication that a rogue writer caused it.
#1143's Fix section named this explicitly: "publish no blob for that tag while leaving the committed set intact and raising an attributable abuse event". The event half is not landed.
Checkable condition
- A skipped or dropped row produces a
grants::AbuseEvent (the type already exists in crates/engine/src/grants/owner_entry.rs and maps to the facade's AttributableAbuse) carrying the tag and no key material.
- The event reaches the host through the re-seal's and the re-mint's own return channel, not a log line.
reseal_scope_root currently returns Result<GrantSection, ResealError> with 44 call sites, so this is a return-type change plus its mechanical tail.
- Attribution: the write body that carried the poisoned row is structure-signed by a committed writer pseudonym, so the event can name which committed pseudonym authored it.
- A test at each boundary asserts the event is raised alongside the skip, and that no key material appears in it.
Why it is not in the PR that found it
Sized out of the #1143/#1294/#1295 batch: the return-type change touches 44 call sites across seven engine files, on top of a crates/core wire change and a KAT regeneration. The security-relevant half — never wrapping a scope seed to a writer-chosen key — landed there; this is the observability half.
The gap
#1143 brought each grant-ledger row's
recipientEncPk/recipientIdentityPkunder owner authority (per-rowownerSig) and decided the policy for a row that fails it: skip, not abort.crates/engine/src/rotation/reseal.rs::adopt_recipientsseals no grant blob for an unattested row.crates/engine/src/net/rotation.rs::remint_grantsdrops it from the moved root's set.Both are correct — refusing would let the grantee a rotation is cutting veto its own revocation — but neither is reported to the host. The only trace is the published record itself: a committed tag with no blob, which
grants::revocation::classifyreads as revocation. The owner's UI therefore shows an innocent co-grantee as revoked with no indication that a rogue writer caused it.#1143's Fix section named this explicitly: "publish no blob for that tag while leaving the committed set intact and raising an attributable abuse event". The event half is not landed.
Checkable condition
grants::AbuseEvent(the type already exists incrates/engine/src/grants/owner_entry.rsand maps to the facade'sAttributableAbuse) carrying the tag and no key material.reseal_scope_rootcurrently returnsResult<GrantSection, ResealError>with 44 call sites, so this is a return-type change plus its mechanical tail.Why it is not in the PR that found it
Sized out of the #1143/#1294/#1295 batch: the return-type change touches 44 call sites across seven engine files, on top of a
crates/corewire change and a KAT regeneration. The security-relevant half — never wrapping a scope seed to a writer-chosen key — landed there; this is the observability half.