Skip to content

feat(local-session): per-user session ownership and graceful cleanup of abandoned sessions - #11

Open
Rahulkaushik01 wants to merge 1 commit into
feat/local-session-apifrom
feat/local-session-owner-and-reaper
Open

feat(local-session): per-user session ownership and graceful cleanup of abandoned sessions#11
Rahulkaushik01 wants to merge 1 commit into
feat/local-session-apifrom
feat/local-session-owner-and-reaper

Conversation

@Rahulkaushik01

Copy link
Copy Markdown
Collaborator

Stacked PR — `. Merge PRs 1–3 first.

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

  1. No per-user ownership. Endpoints only checked "does this API key belong to the project?"
    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 metadata was rejected — a client can
    forge it.
  2. Zombie sessions. If the desktop crashed mid-recording, /stop never arrived and the
    session stayed READY forever: a phantom "live recording" that could not be listed or
    deleted, with its partial transcript stranded.
  3. Pause must not look like a crash. Pause deliberately stops uploading audio, so a naive
    "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).

  • Requests carry two credentials: Authorization: Token <project key> (authorises the
    project, unchanged) and X-User-Token: <team.day JWT> (identifies the person).
  • Bot.owner_user_id is stamped only from the verified token, never from the request body.
  • The algorithm is pinned to HS256 (blocks alg-confusion attacks). exp is honoured when
    present but not required — matching the auth-server's own guard exactly, so we never
    reject a token it would accept.
  • Fails closed: no secret configured → 503; missing, forged or expired token → 401.
  • Ownership is enforced as a query filter, not a post-fetch check, so a mismatch is
    indistinguishable from a missing session — a 404 never confirms another user's session exists.

Graceful reaper

A durable heartbeat stored in the existing last_heartbeat_timestamp column, written with a
single atomic UPDATE (no read-modify-write) so concurrent mic and system uploads never contend
on the row's concurrency version.

  • Every audio upload doubles as a heartbeat, plus a dedicated /heartbeat endpoint so a
    paused session keeps signalling that it is alive — this is what distinguishes paused
    from crashed.
  • A reaper finds LOCAL sessions in READY with no heartbeat for 10 minutes (or that never
    uploaded and are old) and runs the same finalize /stop would, so the partial transcript
    is preserved and the session ends as ENDED.
  • LOCAL is excluded from the two FATAL heartbeat reapers, so a local session always ends
    gracefully, never as FATAL_ERROR.

Changes made

File Lines Purpose
bots/team_day_user_auth.py +51 (new) JWT verification, fail-closed
bots/local_session_api_views.py +60 −4 owner gating, heartbeat endpoint, mark_session_alive, owner-gated transcript
bots/management/commands/clean_up_bots_… +40 −3 idle reaper + LOCAL exclusions
bots/migrations/0090_bot_owner_user_id.py +28 column + partial index (built CONCURRENTLY)
bots/local_session_api_urls.py +8 −4 heartbeat route, owner-gated transcript route
bots/models.py +7 owner_user_id field + partial index
attendee/settings/base.py, .env.example +10 TEAM_DAY_JWT_SECRET

Total: +204 / −11 (215 changed lines), 1 commit.

Testing performed

  • ruff checkAll checks passed; ruff format --checkclean
  • python manage.py checkno issues
  • Authentication and ownership — 15/15:
    no token → 401 · forged signature → 401 · expired token → 401 · valid token → 201 with owner
    stamped · client-supplied owner_user_id in the body is ignored · owner → 200 versus
    another user → 404 on heartbeat / stop / audio / transcript · owner check runs before
    body validation · sub-claim fallback · secret unset → 503 fail-closed
  • Idle reaper — 8/8: session gone quiet → ENDED · 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
  • Regression: the 7 existing heartbeat / global-runtime / never-launched tests → 7/7 OK,
    proving the LOCAL exclusions did not change meeting-bot termination
  • python manage.py test bots.tests.test_cleanup21/21 OK
  • Combined re-verification on the final commit → 14/14 PASS

Coding-guidelines compliance

  • File size (§2): new file 51 lines; local_session_api_views.py now 247 (still ≤ 250
    preferred). ⚠️ bots/models.py pre-existing ~3300 (+7 here) and attendee/settings/base.py
    346 (+5 here, over the 300 warning, under the 350 cap) — both flagged, see below.
  • Functions (§6): all new functions ≤ 50 lines.
  • Constants (§5): LOCAL_SESSION_IDLE_TIMEOUT_SECONDS, USER_TOKEN_HEADER — no magic values.
  • Security (§8): no secrets committed.env is gitignored and .env.example carries an
    empty 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.
  • Error handling (§12): explicit 401/404/503 with clear messages; no silent failures.
  • Testing (§10): unit + integration + permission and error-state cases covered.
  • Commit format (§14): type(scope): description.

Refactoring recommendation (§2 flag)

bots/models.py (~3300 lines) breaches the file cap. I recommend not splitting it in this
fork: the history shows ~3810 commits from the upstream author versus a handful of ours, and
models.py is the most-edited file upstream, so restructuring it would turn every future upstream
sync into a permanent manual conflict. Our footprint is deliberately minimal (7 lines here), with
all new logic in new compliant files. settings/base.py at 346 could be split into per-concern
modules in a small separate PR.

Risks / notes

  • Deployment ordering: TEAM_DAY_JWT_SECRET must be set (equal to the auth-server's
    JWT_SECRET) before this ships, or every local-session endpoint returns 503 by design.
  • Breaking for existing callers: local-session endpoints now require X-User-Token. No
    production clients exist yet (the desktop side is unbuilt), so blast radius is nil today.
  • Migration 0090 builds its index CONCURRENTLY with atomic = False to avoid taking an
    ACCESS EXCLUSIVE lock on the large bots table. The column is nullable with no default, so the
    AddField is metadata-only and instant. Note that a failed concurrent build leaves an INVALID
    index that must be dropped manually.
  • Pre-existing rows (bot dispatches, older sessions) have owner_user_id = NULL and are simply
    unreachable through the local-session endpoints.
  • Follow-up: EXPLAIN the new index under real data, and verify against a live team.day token.

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>
@Rahulkaushik01
Rahulkaushik01 requested a review from hd1801 July 20, 2026 07:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant