feat(local-session): per-user session ownership and graceful cleanup of abandoned sessions - #11
Open
Rahulkaushik01 wants to merge 1 commit into
Open
Conversation
Owner (B): stamp Bot.owner_user_id from the caller's verified team.day JWT (X-User-Token, HS256, fail-closed if the secret is unset) when a session is created, and require an owner match on every local endpoint -- a mismatch returns 404 so a session's existence never leaks. Migration 0090 adds the column plus a partial index, built CONCURRENTLY so it's zero-downtime in prod. Idle reaper (A3): a durable heartbeat (audio uploads plus a new /heartbeat endpoint, so a paused session is not mistaken for a crashed one) and a cron reaper that gracefully ENDs abandoned READY sessions. LOCAL is excluded from the FATAL heartbeat reapers so it always ends gracefully, never as FATAL_ERROR. Co-Authored-By: Claude <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Makes local sessions private to the user who created them, and makes sessions that die
unexpectedly (crash, kill, network loss) clean themselves up gracefully instead of hanging
around forever.
Problem
The project API key is shared by the desktop app, so it identifies the application, not the
person. Nothing tied a session to a user, and nothing stopped one user reading another's
transcript. Carrying the user id in client-controlled
metadatawas rejected — a client canforge it.
/stopnever arrived and thesession stayed
READYforever: a phantom "live recording" that could not be listed ordeleted, with its partial transcript stranded.
"no audio means dead" reaper would end a session while the user was merely paused.
How it works
Ownership — contract derived from real code, not assumed
Tracing the desktop's login shows it stores team.day's own JWT: payload
{ id, email },HS256, signed with the secret the auth-server shares (
apps/auth-server/src/auth).Authorization: Token <project key>(authorises theproject, unchanged) and
X-User-Token: <team.day JWT>(identifies the person).Bot.owner_user_idis stamped only from the verified token, never from the request body.alg-confusion attacks).expis honoured whenpresent but not required — matching the auth-server's own guard exactly, so we never
reject a token it would accept.
503; missing, forged or expired token →401.indistinguishable from a missing session — a
404never confirms another user's session exists.Graceful reaper
A durable heartbeat stored in the existing
last_heartbeat_timestampcolumn, written with asingle atomic
UPDATE(no read-modify-write) so concurrent mic and system uploads never contendon the row's concurrency version.
/heartbeatendpoint so apaused session keeps signalling that it is alive — this is what distinguishes paused
from crashed.
LOCALsessions inREADYwith no heartbeat for 10 minutes (or that neveruploaded and are old) and runs the same finalize
/stopwould, so the partial transcriptis preserved and the session ends as
ENDED.LOCALis excluded from the two FATAL heartbeat reapers, so a local session always endsgracefully, never as
FATAL_ERROR.Changes made
bots/team_day_user_auth.pybots/local_session_api_views.pymark_session_alive, owner-gated transcriptbots/management/commands/clean_up_bots_…bots/migrations/0090_bot_owner_user_id.pybots/local_session_api_urls.pybots/models.pyowner_user_idfield + partial indexattendee/settings/base.py,.env.exampleTEAM_DAY_JWT_SECRETTotal: +204 / −11 (215 changed lines), 1 commit.
Testing performed
ruff check→ All checks passed;ruff format --check→ cleanpython manage.py check→ no issuesno token → 401 · forged signature → 401 · expired token → 401 · valid token → 201 with owner
stamped · client-supplied
owner_user_idin the body is ignored · owner → 200 versusanother user → 404 on heartbeat / stop / audio / transcript · owner check runs before
body validation ·
sub-claim fallback · secret unset → 503 fail-closedENDED· never-uploaded old session →ENDED·paused-but-pinging session stays
READY· FATAL reaper leaves LOCAL untouched ·heartbeat semantics (first pinned, last advances) · heartbeat endpoint returns 200
proving the LOCAL exclusions did not change meeting-bot termination
python manage.py test bots.tests.test_cleanup→ 21/21 OKCoding-guidelines compliance
local_session_api_views.pynow 247 (still ≤ 250preferred).
bots/models.pypre-existing ~3300 (+7 here) andattendee/settings/base.py346 (+5 here, over the 300 warning, under the 350 cap) — both flagged, see below.
LOCAL_SESSION_IDLE_TIMEOUT_SECONDS,USER_TOKEN_HEADER— no magic values..envis gitignored and.env.examplecarries anempty
TEAM_DAY_JWT_SECRET=. Auth fails closed; a 404 (not 403) avoids leaking existence;the algorithm is pinned; owner is never taken from client input.
type(scope): description.Refactoring recommendation (§2 flag)
bots/models.py(~3300 lines) breaches the file cap. I recommend not splitting it in thisfork: the history shows ~3810 commits from the upstream author versus a handful of ours, and
models.pyis the most-edited file upstream, so restructuring it would turn every future upstreamsync into a permanent manual conflict. Our footprint is deliberately minimal (7 lines here), with
all new logic in new compliant files.
settings/base.pyat 346 could be split into per-concernmodules in a small separate PR.
Risks / notes
TEAM_DAY_JWT_SECRETmust be set (equal to the auth-server'sJWT_SECRET) before this ships, or every local-session endpoint returns503by design.X-User-Token. Noproduction clients exist yet (the desktop side is unbuilt), so blast radius is nil today.
0090builds its indexCONCURRENTLYwithatomic = Falseto avoid taking anACCESS EXCLUSIVE lock on the large
botstable. The column is nullable with no default, so theAddFieldis metadata-only and instant. Note that a failed concurrent build leaves anINVALIDindex that must be dropped manually.
owner_user_id = NULLand are simplyunreachable through the local-session endpoints.
EXPLAINthe new index under real data, and verify against a live team.day token.