Skip to content

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

Open

Description

@dezren39

Problem

WorldCore::restore (pkgs/id/src/world/core.rs, the JournalEntry::Issued arm) rebuilds every journaled capability and inserts it into core.capabilities — but it never counts them against WorldLimits::participants, the world's maximum number of active capabilities (src/world/mod.rs: "Maximum number of active capabilities in this world"; default 256).

Every live path that creates a capability enforces the limit:

  • WorldCore::mint (core.rs, the single funnel under issue_capability / issue_for_account / attenuate) counts the non-revoked records first: active < self.limits.participants, else "world participant limit reached".
  • Restore's neighbouring arms enforce their caps in the same style: the ScheduleSet arm refuses a journal that holds more than MAX_PENDING_SCHEDULES pending schedules.

The Issued arm validates the neighbouring fields — digest shape, duplicate participant id, parent must already exist, uses >= 1, a delegate's expiry may not outlive its parent's — but not the count. A journal no live code path can produce (hand-edited, corrupted, or fork-copied — restore's own contract is "a bad journal is refused, never partially applied") therefore restores with more active capabilities than the world allows, and every excess participant is fully live: its token authorizes snapshot() / chat() like any minted participant. The limit is a resource bound (capability records, presence, snapshot participant lists), so the bypass is not cosmetic: after such a restore the world operates permanently above its configured capacity, and mint then refuses all new issuance (the restored excess already saturates the count) while the excess tokens keep working.

Evidence (verified this run, main 51033658)

Probe on the laptop (~/git/fleet/probe-2026-10-31-laneA-restorecap/, a detached worktree of origin/main at 51033658f73a; integration test pkgs/id/tests/probe_restorecap.rs against the real id crate, token digests built exactly as mint builds them — SHA-256 of "<id>.<hex secret>"):

  • CONTROL: live issue_capability under participants: 2 admits two participants and refuses the third ("world participant limit reached").
  • CONTROL: restore of Created + Issued(1) + Issued(2) under participants: 2 returns Ok.
  • CASE A: restore of Created + Issued(1) + Issued(2) + Issued(3) under participants: 2 returns Ok; the excess participant 3's token then passes snapshot() (snapshot lists 3 participants) and chat() — the over-limit participant acts normally.
  • CASE D (boundary): Issued(1) + Issued(2) + Revoked(2) + Issued(3) under participants: 2 also restores Ok — active-in-order counting (revokes applied as they appear) is the semantics that keeps this legitimate shape valid.

Code points (main 51033658): restore's Issued arm is at src/world/core.rs:168-222 (validations at :178-202, unchecked insert at :212-221); the limit check in mint is at core.rs:854-861; the participants bound is defined at src/world/mod.rs:60-61 (default 256 at mod.rs:89); the schedule-cap precedent in the same function is at core.rs:263-275.

A counting check cannot break legitimate compacted journals: checkpoint_entries (core.rs:445+) re-emits participants in id order with each Revoked immediately after its own Issued, so the in-order active count of a compacted journal never exceeds the count the live world had when each participant was minted, which mint already bounded.

Why it matters

Restore is the trust boundary for persisted world state, and it already refuses journals that break the schedule cap, the delegate-expiry rule, and the duplicate-id rule. The participant cap is the same kind of invariant — a configured resource bound on the world's authority surface — enforced by the one producer (restore) that rebuilds state wholesale. A journal that exceeds it yields a world that is simultaneously over capacity and unable to issue to anyone new, with the excess capabilities indistinguishable from legitimate ones.

Acceptance criteria

  • Restore refuses an Issued entry that would make the active (non-revoked, counting entries in journal order) capability count exceed limits.participants, with an error naming the participant/limit in the style of the neighbouring restore errors.
  • Regression tests beside the existing restore tests (cf. restore_refuses_a_delegate_outliving_its_parent): over-limit restore refused; at-limit restore passes; issue → revoke → issue at the limit passes (revokes count in order); a checkpoint/compacted journal at the limit still restores.
  • Triage confirms the counting semantics for delegates (mint counts every non-revoked record, whatever its parent — mirror that) and for Issued entries whose participant is revoked later in the same journal.
  • Cross-check the adjacent Issued fields triage has not yet ruled on (display-name sanitization is filed separately this run; scopes are 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).

Dedup checked this run: open+closed search for world journal restore / participant limit — #2890 (restore never validates Issued scopes; same arm, different invariant), #2894 (restore never validates Committed/Checkpoint event authors), #2882 (schedule-replacement flag vs the cap restore does enforce for schedules). No filing covers the participant-count cap in restore.

Activity

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

    @dezren39
    MemberAuthor

    🤖 Agent note · state: filed

  3. dezren39 commented on Oct 31, 2026

    @dezren39
    MemberAuthor

    Cross-link: the adjacent Issued display-name gap found in the same probe run is filed as #2960. This issue (#2958) covers only the participant-count cap.

  4. 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 1, 2026
  5. 14 remaining items

  6. added
    queue/queuedHas a queued task in the Supabase agent queue
    and removed
    queue/claimedQueue worker lifecycle state
    work/assignedQueue worker lifecycle state
    on Nov 2, 2026
  7. developing-today commented on Nov 3, 2026

    @developing-today
    Contributor

    agent-note:v1
    state: triaged (coverage pass)
    Coverage pass 2026-11-03: reviewed in the fleet-wide per-issue pass (drift / overlap / already-handled / stale / blockers / ordering). Verdict: still live; queued in repo-health-code-pilot as triage at priority 40 (priority/normal). If this is already handled at current main, the triage closes it with evidence; if it needs splitting, the triage files the split.
    Found via: coverage pass 2026-11-03

  8. added
    queue/claimedQueue worker lifecycle state
    work/assignedQueue worker lifecycle state
    and removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Nov 3, 2026
  9. dezren39 commented on Nov 3, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · triage verdict (task 8171)

    • Agent: repo-worker-code, lane code-j (direct execution lane)
    • Verdict: CONFIRMED at current main. Fix enqueued as task 8685 (verified queued).

    Fresh verification (2026-11-03, current origin/main): restore's JournalEntry::Issued arm (pkgs/id/src/world/core.rs:538+) now validates participant 0 (#2972), display name (#2960), duplicates, parent existence, uses >= 1, delegate expiry, and scopes (#2890) — the neighbouring fields this issue's acceptance asked triage to cross-check have since landed. The participant count is the one check still missing: the insert into core.capabilities (~:646) is unconditional. mint's counting (core.rs:~1520) is capabilities.values().filter(|r| !r.revoked).count() with active < limits.participants.

    Counting semantics settled (acceptance criterion 3): mirror mint exactly, in journal order — a Revoked entry earlier in the journal has already updated the map when later entries are counted; an Issued entry whose ancestor chain is revoked is inserted with revoked: revoked_by_ancestor and must not consume a slot; delegates count like any other non-revoked record, whatever their parent. With those rules the issue's Case D (issue → revoke → issue at the limit) and compacted checkpoint journals stay valid, as its evidence requires.

    Routing: small_fix task 8685 (packet hidden_files/packets/fix-2958-restore-participant-limit.json) — the count check in the Issued arm plus five regression cases (over-limit refused, at-limit passes, issue-revoke-issue at limit, revoked-ancestor delegate takes no slot, compacted journal at limit restores), verified with cargo test -p id --lib world on the laptop (id-check.yml has no PR trigger).

    • Labels: claim labels removed at this terminal state.
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 jobtask/triageQueue packet kind: verify, reproduce, and route this item

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions