Skip to content

id(world): journal restore issues a capability under an already-revoked parent — a Revoked entry that precedes the child's Issued entry revokes nothing, and the restored child acts with a live token #3373

Closed

Description

@dezren39

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 run authorize(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, attenuate with a just-revoked parent's token returns Err — live delegation from a revoked parent is refused.
  • CASE: journal Issued(1, GUEST|DELEGATE), Revoked(1), Issued(2, GUEST, parent=1) restores Ok, and participant 2's token chats Ok — 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 Issued entry 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 Revoked entries compaction emits must still satisfy the new check — a revoked child issued before its own Revoked entry is fine; only a parent already revoked at the child's Issued entry 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

  1. added
    kind/bugSomething isn't working (prefixed form of the classic bug label)
    task/triageQueue packet kind: verify, reproduce, and route this item
    source/creatorFiled by the new-issue-creator job
    priority/normalOrdinary priority (the default)
    queue/queuedHas a queued task in the Supabase agent queue
    app/idPackage/app: id (pkgs/ or apps/ in this repo)
    effort/sSmall: expected under 2 hours of work
    on Nov 1, 2026
  2. dezren39 commented on Nov 1, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · filed

  3. added
    work/assignedQueue worker lifecycle state
    queue/claimedQueue worker lifecycle state
    and removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Nov 2, 2026
  4. 13 remaining items

  5. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    Claimed as queue task 5984 (small_fix). Building on triage task 5144s reproduction: scoping the restore fix now, worktree next.

  6. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    Fix implemented as PR #4658 (branch queue/5984-3373-revoked-descendants, commit 6be9b6d63): restore walks the ancestor chain and inserts descendants of revoked ancestors as revoked; inherited_revocations set absorbs the compacted descendant Revoked entry. 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.

  7. added
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    status/parkedDeliberately paused with a recorded resume condition
    queue/claimedQueue worker lifecycle state
    and removed
    queue/claimedQueue worker lifecycle state
    status/parkedDeliberately paused with a recorded resume condition
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    on Nov 2, 2026
  8. added a commit that references this issue on Nov 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

app/idPackage/app: id (pkgs/ or apps/ in this repo)effort/sSmall: expected under 2 hours of workkind/bugSomething isn't working (prefixed form of the classic bug label)priority/normalOrdinary priority (the default)source/creatorFiled by the new-issue-creator job

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions