Repository navigation
Description
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 underissue_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
ScheduleSetarm refuses a journal that holds more thanMAX_PENDING_SCHEDULESpending 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_capabilityunderparticipants: 2admits two participants and refuses the third ("world participant limit reached"). - CONTROL: restore of
Created + Issued(1) + Issued(2)underparticipants: 2returnsOk. - CASE A: restore of
Created + Issued(1) + Issued(2) + Issued(3)underparticipants: 2returnsOk; the excess participant 3's token then passessnapshot()(snapshot lists 3 participants) andchat()— the over-limit participant acts normally. - CASE D (boundary):
Issued(1) + Issued(2) + Revoked(2) + Issued(3)underparticipants: 2also restoresOk— 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
Issuedentry that would make the active (non-revoked, counting entries in journal order) capability count exceedlimits.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
Issuedentries whose participant is revoked later in the same journal. - Cross-check the adjacent
Issuedfields 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
dezren39 commentedon Oct 31, 2026
🤖 Agent note · state: filed
idcrate (probe_restorecap.rs): restore accepts 3 Issued entries under a participant limit of 2, and the excess participant's token snapshots (3 participants listed) and chats. Deduped against 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 (Issued scopes), id(world): journal restore never validates Committed/Checkpoint event authors — inputs and chat from never-issued participants restore, replay, and project #2894 (event authors), id(world): check_schedule_replacing trusts a caller-supplied replacing flag — a mismatched flag admits a 65th pending schedule past the cap restore enforces, and underflows on an empty map #2882 (schedule cap).dezren39 commentedon Oct 31, 2026
Cross-link: the adjacent
Issueddisplay-name gap found in the same probe run is filed as #2960. This issue (#2958) covers only the participant-count cap.14 remaining items
developing-today commentedon Nov 3, 2026
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
dezren39 commentedon Nov 3, 2026
🤖 Agent note · triage verdict (task 8171)
Fresh verification (2026-11-03, current origin/main): restore's
JournalEntry::Issuedarm (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 intocore.capabilities(~:646) is unconditional.mint's counting (core.rs:~1520) iscapabilities.values().filter(|r| !r.revoked).count()withactive < limits.participants.Counting semantics settled (acceptance criterion 3): mirror mint exactly, in journal order — a
Revokedentry earlier in the journal has already updated the map when later entries are counted; anIssuedentry whose ancestor chain is revoked is inserted withrevoked: revoked_by_ancestorand 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 withcargo test -p id --lib worldon the laptop (id-check.yml has no PR trigger).