Phase 6.1: per-identity token-bucket rate limiting #19

Merged
jlxq0 merged 1 commit from phase-6-1-rate-limit into main 2026-05-15 03:31:46 +00:00
jlxq0 commented 2026-05-15 03:11:35 +00:00 (Migrated from github.com)

Summary

Adds a per-identity rate limit to every MCP tool call.

  • New `rate_limit.rs` with `governor`-backed token buckets. Two independent buckets per identity: one keyed on `sha256(bearer)[..16]`, one keyed on MAS `sub` (ULID). Either denial → request denied. Both must allow.
  • Reads (`whoami`, `list_joined_rooms`, `read_recent_messages`, `verify_status`) and writes (`send_text_message` + future side-effectful tools) have independent quotas. Defaults: 60/min reads, 30/min writes, configurable via env.
  • Bucket maps are kept memory-bounded by a 5-minute janitor that evicts idle (refilled) buckets. Cheap because matrix-sdk's per-user state is the much larger memory item; this is just identity → `Arc` indirection.
  • Denials surface as JSON-RPC error `-32029` (`rate_limited`); audit log + Prometheus metrics record `outcome="rate_limited"` instead of `"error"` so dashboards distinguish quota denials from real failures.
  • Wires through:
    • `AuthenticatedIdentity` now carries `sub` (was previously dropped after introspection).
    • `/setup` browser flow falls back to bearer-only (no MAS sub for this caller).
    • Tests cover quota independence (read vs write), distinct-bearer isolation, sub-bucket denial across bearers for the same user, and bearer-only mode.

Env knobs

var default notes
`MATRIX_MCP_RATE_LIMIT_READS_PER_MIN` 60 must be > 0
`MATRIX_MCP_RATE_LIMIT_WRITES_PER_MIN` 30 must be > 0

Test plan

  • CI green
  • After deploy: hammer `whoami` from claude.ai past the read quota and verify a 429-ish JSON-RPC error returns
  • `outcome="rate_limited"` shows up in Loki + Grafana panel
  • Read and write quotas are independently exhaustible

🤖 Generated with Claude Code

## Summary Adds a per-identity rate limit to every MCP tool call. - New \`rate_limit.rs\` with \`governor\`-backed token buckets. Two independent buckets per identity: one keyed on \`sha256(bearer)[..16]\`, one keyed on MAS \`sub\` (ULID). Either denial → request denied. Both must allow. - Reads (\`whoami\`, \`list_joined_rooms\`, \`read_recent_messages\`, \`verify_status\`) and writes (\`send_text_message\` + future side-effectful tools) have independent quotas. Defaults: **60/min reads, 30/min writes**, configurable via env. - Bucket maps are kept memory-bounded by a 5-minute janitor that evicts idle (refilled) buckets. Cheap because matrix-sdk's per-user state is the much larger memory item; this is just identity → \`Arc<RateLimiter>\` indirection. - Denials surface as JSON-RPC error \`-32029\` (\`rate_limited\`); audit log + Prometheus metrics record \`outcome=\"rate_limited\"\` instead of \`\"error\"\` so dashboards distinguish quota denials from real failures. - Wires through: - \`AuthenticatedIdentity\` now carries \`sub\` (was previously dropped after introspection). - \`/setup\` browser flow falls back to bearer-only (no MAS sub for this caller). - Tests cover quota independence (read vs write), distinct-bearer isolation, sub-bucket denial across bearers for the same user, and bearer-only mode. ## Env knobs | var | default | notes | |---|---|---| | \`MATRIX_MCP_RATE_LIMIT_READS_PER_MIN\` | 60 | must be > 0 | | \`MATRIX_MCP_RATE_LIMIT_WRITES_PER_MIN\` | 30 | must be > 0 | ## Test plan - [ ] CI green - [ ] After deploy: hammer \`whoami\` from claude.ai past the read quota and verify a 429-ish JSON-RPC error returns - [ ] \`outcome=\"rate_limited\"\` shows up in Loki + Grafana panel - [ ] Read and write quotas are independently exhaustible 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign in to join this conversation.
No description provided.