Discord REST client, cache and #225 settings (server-26#225 S2) #231
No Reviewers
Labels
Clear labels
area/ai-pipeline
area/auth
area/billing
area/correlation
area/discord
area/frontend
area/governance
area/growth
area/infra-deploy
area/legal
area/map
area/mqtt
area/node-ota
area/osint
area/recording-audio
area/sdr-radio
area/transcription
board
board:agenda
minutes:draft
minutes:final
role:ceo
role:cfo
role:cio
role:ciso
role:cmo
role:coo
role:cto
Scene/entity extraction, summarizer, AI providers, embeddings
AuthN/Z, roles, tenancy/org scoping, API keys, credentials
Plans, entitlements, AI spend accounting and caps
Incident correlation, linking, lifecycle, severity, corr_debug
Discord bot, radio bots, voice streams, bot tokens, webhooks
Web UI: Next.js app and the edge-node local dashboard
Board process, minutes, unattended runner, agent tooling
Go-to-market, outreach, cold email, discovery conversations
CI/CD, VM, registry, Firestore rules/indexes deploy, Terraform/Caddy
ToS, retention, licences/DAAs, CAN-SPAM, E&O, name suppression
Live map: pins, paths, tiles, legend, geocoding
Node-C2 MQTT, checkins, commands, node sweeper, dynsec
Node install, enrollment, OTA updater, image skew
External feeds: cameras, 511, ADS-B, AIS, outages, and their licences
Call capture, boundaries, PulseAudio, ring buffer, trimming, attribution
OP25 decode, SDR hardware/tuning, conventional, second SDR, mesh radio
STT, transcript correction, place verification, name accuracy
Board process: minutes and board-filed items
Needs board scoping at the next sitting; CEO removes the label once scoped
One exec pre-read, closed by CEO
CEO ruling; open until enacted
Filed by the CEO
Filed by the CFO
Filed by the CIO
Filed by the CISO
Filed by the CMO
Filed by the COO
Filed by the CTO
kind
behavior-bug
Wrong product behavior; the instrument reads it correctly
kind
measurement-bug
Corrupts the recording/instrument (debug data, evals, health checks), not behavior
priority
p0
Drop everything: prod broken, live exposure, or owner-ruled P0
priority
p1
Next up: blocks a gate, a customer, or core incident quality
priority
p2
Normal: real defect or planned work, schedule in turn
priority
p3
Low: minor, cosmetic, or someday
status
blocked
Waiting on an external party, spend, or another issue
status
in-progress
Branch or PR open against it
status
needs-decision
Waiting on an owner or board ruling before work
status
needs-info
Root cause or repro unknown; needs a reading first
type
bug
Something built is wrong: code, data, or a measurement
type
chore
Maintenance, refactor, tests, pinning, cleanup: no user-visible change
type
decision
A ruling to make or record: board minutes, owner calls, legal reviews
type
docs
Docs, runbooks, CLAUDE.md, skills, SOPs
type
feature
New capability or an extension of an existing one
type
security
Security, privacy, credential or tenant-isolation exposure
type
tracking
Tracker, checklist, initiative or punch-list spanning several changes
No labels
Milestone
No items
No Milestone
Projects
Clear projects
No projects
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: logan/server-26#231
Reference in New Issue
Block a user
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.
Slice S2 of #225 (plan: #225 comment 3108). Built on
origin/mainc4e1f9a(includes #227 multi-stream and #226). Do not merge until reviewed; S3 depends on it.What changed
app/internal/discord_rest.py:httpx.AsyncClient, User-AgentDiscordBot (https://drb.cusano.net, 1). The token goes per request inAuthorization; it is never logged or put in an error string, and rate-limit state is keyed by a hash of it.X-RateLimit-Bucket+ guild id. A wait overdiscord_rest_max_retry_after_seconds(3 s) is not sent: the result is unknown. A 429 is retried once only ifretry_afteris 3 s or less. A global 429 pauses that token only.guild_snapshot(token_doc, guild_id): cached, single-flight read of/guilds/{g},/guilds/{g}/members/{bot}and/guilds/{g}/channels, for S3's permission check. 401 setsvalid=False. 10004 and 10007 drop the server fromguild_ids. 50001 is confirmed by one read of the server list and never changesguild_idson its own. Cache TTLs: ok 120 s, not-member or invalid 600 s, unknown 30 s.invalidate(token_id=, guild_id=)drops entries, and a load that overlaps an invalidate is not stored.inspect_token,refresh_membership,leave_guildandTokenRejectedmoved offdiscord_leases.pyonto the new client. Callers indiscord_streams.py,routers/tokens.pyandrouters/discord.pywere repointed.refresh_membershipandleave_guildalso drop the cache./users/@me/guildsnow followsafter=pages. Before, it read one page of 200 and silently dropped the rest. A failed page now returns unknown, never a truncated list.config.pyhas every setting the #225 slices use:discord_perm_check_enabled,discord_access_{,negative_,unknown_}ttl_seconds,discord_perm_check_concurrency/_budget_seconds,discord_rest_invalid_budget/_window_seconds,discord_rest_max_retry_after_seconds,discord_rest_timeout_seconds,discord_directory_refresh_seconds,discord_oauth_redirect_uri(empty means off),discord_oauth_state_ttl_seconds,discord_oauth_link_check_seconds,discord_pool_bot_guild_cap=90(owner Q3).Behaviour change: limited to pagination, the User-Agent, the rate-limit handling and the breaker. Nothing reads
guild_snapshotuntil S3.Tests: new
tests/test_discord_rest.pyhas 29 tests covering hygiene, pagination, 429 and buckets, the breaker, snapshot status mapping, cache TTLs, single flight and invalidate races. The 4 discord test files now monkeypatchdiscord_rest.refresh_membership. The leave-guild test useshttpx.MockTransport.~/venvs/drb-5c/bin/python -m pytest(drb-c2-core): 715 passed, 4 warnings (baseline on main: 686 passed).No frontend files changed, so tsc was not run.
🤖 Generated with Claude Code
New app/internal/discord_rest.py: one shared httpx client with the DiscordBot User-Agent, per-token rate-limit buckets (X-RateLimit-Bucket + guild), one retry on a 429 of <=3s, global-429 pause per token, and a process-wide breaker at 4000 401/403/unshared-429 responses per 10 min so C2 can never get the shared egress IP banned. Tokens are never logged. inspect_token / refresh_membership / leave_guild / TokenRejected move off discord_leases onto it. /users/@me/guilds now follows after= pages (it read one page of 200 and silently dropped the rest); a failed page is unknown, never a truncated list. guild_snapshot(token_doc, guild_id): cached, single-flight read of /guilds/{g}, the bot's own member and /channels for S3's permission check. 401 marks the token invalid; 10004/10007 drop the server from guild_ids; 50001 is confirmed with one membership read and never changes guild_ids on its own. TTLs: ok 120s, not-member/invalid 600s, unknown 30s. config.py gets every setting the #225 slices use (perm-check kill switch, TTLs, breaker budget, OAuth redirect URI, discord_pool_bot_guild_cap=90). No behaviour change beyond pagination, User-Agent and the breaker. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>S2 review: ship (not merged).
1a6e3ca:716 passed, 4 warnings;tests/test_discord_rest.py:30 passed, 1 warning. No frontend change, tsc not needed.discord_pool_bot_guild_cap=90). No other callers of the moved functions remain; token stays out of logs, errors and dict keys; guild_id is digit-checked at the router beforeleave_guild.1a6e3ca):guild_snapshotnow fails open (UNKNOWN, cached 30 s) on non-Discord errors such as a Firestore write inside classification, instead of raising into S3's start_stream. New test fails on2b3b875, passes now.remainingis not decremented locally, so concurrent same-bucket requests can draw an extra 429 (retried/counted by the breaker); a 401 during the 50001 membership confirm marks the token invalid but returns UNKNOWN for that snapshot (pick_token already skipsvalid: False).