Problem
riverctl debug bans <room> prints every ban with banned_user_id, banned_by_id and banned_at_secs, but gives no indication of whether the contract still enforces that ban.
Since deputy ban authority (#410), a ban can be present in state and completely inert. BansV1 computes enforcement separately from storage: MembersV1::check_banned_members only collects a ban's target (and their downstream subtree) when MembersV1::is_ban_authorized(ban.banned_by, ban.ban.banned_user, …) returns true at the time state is applied. That predicate is re-evaluated against current state, so a ban stops enforcing when any of these later become true:
- the banner's deputy authority was revoked (their deputizer removed them from
deputies);
- the banner was pruned or left, so
banner_is_member is false and the deputy-derived branches (steps 3 and 5) no longer grant;
- the banner's deputizer left, so the banner is no longer a deputy of a genuine invite ancestor of the target;
- the target has since deputized the banner, tripping the step-4 guardrail (and no absolute grant applies).
A moderator reading debug bans sees the ban listed and reasonably concludes the member is banned. They may well be back in the room.
Why it matters more now
#470 adds member deputized-by, so a moderator can now discover that nobody has deputized member X. What they still cannot see is the consequence: every ban X issued is dead. The two halves of the picture are split across commands, and only one of them exists.
Suggested fix
BansV1::ban_is_enforcing is already pub (common/src/room_state/ban.rs). Add an enforcing: bool to BanInfo in cli/src/commands/debug.rs, populate it from that predicate, and surface it in both the human and -f json branches. Mark the non-enforcing ones clearly in the human output, since that is the case a moderator needs to notice.
Worth considering alongside: a short reason for a non-enforcing ban (banner absent / authority revoked / guardrail), which is the information that makes it actionable.
Notes
Found while reviewing #470 (deputy-status read commands). Deliberately left out of that PR: it is ban status rather than deputy status, and it changes an existing command's output shape.
[AI-assisted - Claude]
Problem
riverctl debug bans <room>prints every ban withbanned_user_id,banned_by_idandbanned_at_secs, but gives no indication of whether the contract still enforces that ban.Since deputy ban authority (#410), a ban can be present in state and completely inert.
BansV1computes enforcement separately from storage:MembersV1::check_banned_membersonly collects a ban's target (and their downstream subtree) whenMembersV1::is_ban_authorized(ban.banned_by, ban.ban.banned_user, …)returns true at the time state is applied. That predicate is re-evaluated against current state, so a ban stops enforcing when any of these later become true:deputies);banner_is_memberis false and the deputy-derived branches (steps 3 and 5) no longer grant;A moderator reading
debug banssees the ban listed and reasonably concludes the member is banned. They may well be back in the room.Why it matters more now
#470 adds
member deputized-by, so a moderator can now discover that nobody has deputized member X. What they still cannot see is the consequence: every ban X issued is dead. The two halves of the picture are split across commands, and only one of them exists.Suggested fix
BansV1::ban_is_enforcingis alreadypub(common/src/room_state/ban.rs). Add anenforcing: booltoBanInfoincli/src/commands/debug.rs, populate it from that predicate, and surface it in both the human and-f jsonbranches. Mark the non-enforcing ones clearly in the human output, since that is the case a moderator needs to notice.Worth considering alongside: a short reason for a non-enforcing ban (banner absent / authority revoked / guardrail), which is the information that makes it actionable.
Notes
Found while reviewing #470 (deputy-status read commands). Deliberately left out of that PR: it is ban status rather than deputy status, and it changes an existing command's output shape.
[AI-assisted - Claude]