Fixes#25 (the remaining part; #33 closed the simultaneous-join race at token issue).
Problem: the limit was only checked when a token was issued, and lk-jwt-service tokens last an hour. A token fetched while the channel had space (by a custom client, a second tab, or anyone holding on to one) could be used to connect after the channel filled up.
Now: the guard's existing reconcile loop (every 3 s) also enforces the limit at connect time:
It compares each limited room's distinct Matrix users (matrix_user) with max_users.
When people who arrived since the previous sweep take the room over its limit, it calls LiveKit RemoveParticipant on them, newest first (joined_at_ms, else joined_at), removing all of that person's devices.
People already in the call are never removed, so lowering a limit mid-call kicks nobody. A person's extra devices never count twice.
The first sweep of a room only records who is there. Unlimited, unrestricted rooms aren't even listed (fast path kept).
Over-limit can only come from a late connect, since admission is serialised at token issue.
Verified
python3 -m unittest livekit/test_voice_limit_guard.py: 56 OK, 9 new. They cover the decision cases above and reconcile_room with LiveKit mocked: the late connect is removed, and an unlimited room isn't listed.
Local stack (real guard, LiveKit, Synapse), with the limit at 1:
Bob fetched a token while the channel was empty (200) and held it. Audit Main then joined normally, and Bob connected straight to LiveKit with the old token. Guard log: removed @aud_bob:localhost:EARLYTOKEN from !cwFd…: over the voice limit of 1. Audit Main stayed in the call.
Control (no limit, same steps): no removal.
Limit 2, both users joining through the UI: both stay in the call through several sweeps, with no removals.
The README voice-limit section is updated.
Deploy: merging redeploys the guard on LXC 151 through the existing hook. I'll check production afterwards.
Fixes #25 (the remaining part; #33 closed the simultaneous-join race at token issue).
**Problem:** the limit was only checked when a token was issued, and lk-jwt-service tokens last an hour. A token fetched while the channel had space (by a custom client, a second tab, or anyone holding on to one) could be used to connect after the channel filled up.
**Now:** the guard's existing reconcile loop (every 3 s) also enforces the limit at connect time:
- It compares each limited room's **distinct Matrix users** (`matrix_user`) with `max_users`.
- When people who **arrived since the previous sweep** take the room over its limit, it calls LiveKit `RemoveParticipant` on them, newest first (`joined_at_ms`, else `joined_at`), removing all of that person's devices.
- People already in the call are **never removed**, so lowering a limit mid-call kicks nobody. A person's extra devices never count twice.
- The first sweep of a room only records who is there. Unlimited, unrestricted rooms aren't even listed (fast path kept).
- Over-limit can only come from a late connect, since admission is serialised at token issue.
**Verified**
- `python3 -m unittest livekit/test_voice_limit_guard.py`: 56 OK, 9 new. They cover the decision cases above and `reconcile_room` with LiveKit mocked: the late connect is removed, and an unlimited room isn't listed.
- **Local stack** (real guard, LiveKit, Synapse), with the limit at 1:
- Bob fetched a token while the channel was empty (200) and held it. Audit Main then joined normally, and Bob connected straight to LiveKit with the old token. Guard log: `removed @aud_bob:localhost:EARLYTOKEN from !cwFd…: over the voice limit of 1`. Audit Main stayed in the call.
- Control (no limit, same steps): no removal.
- Limit 2, both users joining through the UI: both stay in the call through several sweeps, with no removals.
- The README voice-limit section is updated.
**Deploy:** merging redeploys the guard on LXC 151 through the existing hook. I'll check production afterwards.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_017PCb59uY3UKqDqt7B6hzd5
The limit was only checked at token issue, and lk-jwt tokens last an hour,
so a token fetched while there was space could be used to connect once the
room was full. The reconcile loop now compares each limited room's
distinct Matrix users with max_users and RemoveParticipants the people who
arrived since the previous sweep, newest first, until it is back within the
limit. People already in the call are never removed (lowering a limit
mid-call kicks nobody); extra devices of one person never count twice.
Fixes#25
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017PCb59uY3UKqDqt7B6hzd5
jared
merged commit 8e5cdf50b5 into main2026-10-19 23:34:04 -04:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Fixes #25 (the remaining part; #33 closed the simultaneous-join race at token issue).
Problem: the limit was only checked when a token was issued, and lk-jwt-service tokens last an hour. A token fetched while the channel had space (by a custom client, a second tab, or anyone holding on to one) could be used to connect after the channel filled up.
Now: the guard's existing reconcile loop (every 3 s) also enforces the limit at connect time:
matrix_user) withmax_users.RemoveParticipanton them, newest first (joined_at_ms, elsejoined_at), removing all of that person's devices.Verified
python3 -m unittest livekit/test_voice_limit_guard.py: 56 OK, 9 new. They cover the decision cases above andreconcile_roomwith LiveKit mocked: the late connect is removed, and an unlimited room isn't listed.removed @aud_bob:localhost:EARLYTOKEN from !cwFd…: over the voice limit of 1. Audit Main stayed in the call.Deploy: merging redeploys the guard on LXC 151 through the existing hook. I'll check production afterwards.
🤖 Generated with Claude Code
https://claude.ai/code/session_017PCb59uY3UKqDqt7B6hzd5