Repository navigation
Description
Problem
WorldCore::restore (pkgs/id/src/world/core.rs) accepts a journal in which a capability is issued after its parent was already revoked, and the restored child then acts with a live token — a state no live path can produce.
Restore's Issued arm validates the neighbouring fields — digest shape, duplicate participant id, the parent must already exist (core.capabilities.contains_key(&parent)), uses >= 1, and a delegate's expiry may not outlive its parent's — but existence is the only parent check. It never asks whether the parent is revoked.
The Revoked arm is not a backstop: it calls revoke_tree(participant_id), which marks that participant and every descendant issued so far. A child whose Issued entry appears later in the journal does not exist yet at that moment, so nothing marks it, and it is inserted afterwards with revoked: false.
authorize() then has no way to catch it: it checks only the presented token's own record (!record.revoked), and effective_scopes() walks the ancestor chain intersecting scopes and subject ceilings — it never checks an ancestor's revoked flag. So the restored child's token passes authorize and can chat/input like any live participant.
Every live path refuses this shape:
delegate()/attenuate()/delegate_to_friend()first runauthorize(token, JOIN)on the parent, which fails for a revoked parent — you cannot delegate from a revoked capability.- A live
revoke_tree()revokes the parent and every existing descendant with it, so a live world never holds an active child under a revoked parent.
Checkpoint-written journals are safe by construction (compaction emits each participant's Issued entry immediately followed by its own Revoked entry when revoked, in id order), but restore is the trust boundary for journals that travel — forks copy them, backups restore them under newer binaries — and the design of record (#2898, "reconstruction is admission") says the rebuilding code validates the same invariants the live path enforces and refuses, never partially applies.
Evidence (verified this run, main ca2471a / laptop 5c172f3; world/core.rs and world/mod.rs md5-identical on both clones)
Probe on the laptop (~/git/fleet/probe-2026-11-01-laneA-revokedparent/, a detached worktree of origin/main; pkgs/id/tests/probe_revokedparent.rs, an integration test against the real id crate, token digests built exactly as mint builds them — "<id>.<hex secret>", SHA-256):
- CONTROL A: journal
Issued(1, GUEST|DELEGATE),Issued(2, GUEST, parent=1),Revoked(1)restores, and participant 2's token cannot chat — revoke-after-issue takes the whole tree, as designed. - CONTROL B: on a live
WorldCore,attenuatewith a just-revoked parent's token returnsErr— live delegation from a revoked parent is refused. - CASE: journal
Issued(1, GUEST|DELEGATE),Revoked(1),Issued(2, GUEST, parent=1)restoresOk, and participant 2's token chatsOk— an active capability delegated from an already-revoked parent, restored from a journal no live writer could produce.
Code points (main ca2471a): the Issued arm's parent check is parent.is_none_or(|parent| core.capabilities.contains_key(&parent)) in WorldCore::restore (src/world/core.rs, restore Issued arm); the Revoked arm calls core.revoke_tree(participant_id); revoke_tree (same file) only visits records already in core.capabilities; authorize checks !record.revoked for the presented token's record only; effective_scopes intersects scopes/ceilings up the parent chain with no revoked test. The existing test a_restored_delegation_keeps_its_parent_link (src/world/tests.rs) covers revoke-after-issue only.
Why it matters
Revocation is the world's moderation/incident-response primitive: revoking a capability is supposed to end its whole delegation line. A journal that revokes a parent before issuing its child — from a buggy or partial writer, a hand-repaired journal, or a fork/backup assembled out of order — silently resurrects the revoked line on restore: the child was never live-revoked because, in journal order, it did not exist when the revocation landed. This is the same reconstruction-as-admission family as #2890/#2958/#2960/#2972, but a distinct field and mechanism (parent revocation state at issue time, not scopes, limits, names, or participant 0); #2890's closing acceptance item covers delegate scopes-vs-parent, not this.
Acceptance criteria
- Restore refuses an
Issuedentry whose parent is already revoked at that point in the journal, naming the participant in the error, in the style of the neighbouring restore errors (e.g. "participant N delegated from revoked capability M"). - Regression tests beside the existing restore tests: issuance-after-parent-revocation refused; the control shapes still restore — parent and child issued then revoked together (revoke-after-issue), and a live delegation chain with no revocation.
- Checkpoint/compaction round-trip unchanged: a world with a revoked parent and revoked descendants checkpoints and restores to the same state (the per-participant
Revokedentries compaction emits must still satisfy the new check — a revoked child issued before its ownRevokedentry is fine; only a parent already revoked at the child'sIssuedentry is refused). - Triage confirms no legitimate historical journal shape issues a capability under an already-revoked parent; if one exists, name it explicitly instead of loosening the check silently.
Activity
dezren39 commentedon Nov 1, 2026
🤖 Agent note · filed
WorldCore::restore(src/world/core.rs, restoreIssuedarm, parent check at :190) accepts anIssuedentry whose parent was already revoked earlier in the journal: theRevokedarm'srevoke_treeonly revokes participants issued so far, the later child is insertedrevoked: false, andauthorize/effective_scopesnever check an ancestor's revoked flag. Verified on main ca2471a (laptop 5c172f3; core.rs/mod.rs md5-identical on both clones) with a laptop probe (probe-2026-11-01-laneA-revokedparent, realWorldCore::restore+chat): CONTROL A revoke-after-issue chat Err, CONTROL B live delegate-from-revoked Err, CASE restore Ok + child-of-revoked-parent chat Ok. Distinct from id(world): journal restore never validates Issued scopes — unnamed bits and JOIN-less capabilities restore clean, and a CHAT-/INPUT-only token can act without ever joining #2890 (scopes; its closing item covers delegate scopes-vs-parent, not revocation order), id(world): journal restore never enforces the participant limit — a journal with more Issued entries than WorldLimits.participants restores fully, and the excess participants act #2958/id(world): journal restore never sanitizes Issued display names — empty, control-bearing, and over-length names restore verbatim, though mint refuses or strips all three #2960/id(world): journal restore issues participant 0 — an Issued entry for the reserved host identity restores, and its token chats and projects as the host #2972 (other Issued-arm fields), id(world): journal restore never validates Committed/Checkpoint event authors — inputs and chat from never-issued participants restore, replay, and project #2894 (Committed authors); family design Design: reconstruction is admission — validate authority-bearing inputs where they are rebuilt (pkgs/id) #2898. Enqueued as a repo-task/v1 packet on repo-health-code-pilot.Issuedentry whose parent is already revoked at that journal point, pinned by restore-level regression tests; checkpoint round-trips unchanged.13 remaining items
dezren39 commentedon Nov 2, 2026
Claimed as queue task 5984 (small_fix). Building on triage task 5144s reproduction: scoping the restore fix now, worktree next.
dezren39 commentedon Nov 2, 2026
Fix implemented as PR #4658 (branch
queue/5984-3373-revoked-descendants, commit6be9b6d63): restore walks the ancestor chain and inserts descendants of revoked ancestors as revoked;inherited_revocationsset absorbs the compacted descendantRevokedentry. Laptop probes: new test passes,world::tests::tests::87/0,world::161/0, touched regions rustfmt-clean. CI pending behind runner congestion (only the config-guard check queued so far); merge when standing criteria are met. Worktree:/home/user/git/fleet/5984.Merge pull request #4658 from developing-today/queue/5984-3373-revoke…