Make harness login the account door: sessions and application auth (ENG-703)#15
Merged
czpython merged 1 commit intoJul 16, 2026
Conversation
czpython
added a commit
that referenced
this pull request
Jul 16, 2026
Review pass over PR #15: drop the tests that re-proved another test's property — the 4-assert 401 spot-check (one canary stays; the boundary enumeration owns the surface), the boundary file folded 4 tests into the two directions that matter, single-use loginId, unknown-harness 404, the settings-card re-asserts (pinned in test_api_settings), the FE credentials config-echo and inline-error tests. Session mechanics, legacy-seat protection, and the gate enumeration keep full coverage. Checklist sweep on the touched files: truthiness for row-absence asserts, imports to module top, one-caller _jwt inlined. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
czpython
added a commit
that referenced
this pull request
Jul 16, 2026
- Read the trusted proxy identity at login/complete instead of
snapshotting it at start: proxy_email leaves CompletedLogin, the flow
state, and login_start's signature.
- Move the notification respond route to
/_external/notifications/{token}/respond, beside the webhooks it works
like. The edge only admits POST /_external/* publicly, so under /api it
wasn't reachable from outside; capability_router and its exempt-list
entry die with the move.
- Drop is_email_mismatch: one hint line wasn't worth a schema field and
the lowered-compare dance; provider_email stays in the payload.
- Replace SessionCookieReissue with native response.set_cookie at login,
logout, and GET /session (the slide every app load performs); the
request.state.session_token seam dies.
- Comment/docstring diet across everything the PR added, and the last
'seat' prose swapped for login/connection.
- ruff format over the touched files CI's ruff check never flagged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…NG-703)
Completing a harness OAuth resolves an account and mints a 30-day
sliding Redis session (druks_session cookie — rotated on login, cleared
on logout, re-issued on every GET /session). Every /api route sits
behind the session gate via include-level dependencies, extension
routes and SSE streams included; the login surface, the health probe,
and the JSON-404 catch-all are the only exemptions, and a boundary test
pins the split in both directions. The notification respond route moves
to /_external/notifications/{token}/respond beside the webhooks, where
the edge actually admits it.
Identity is one plane: the edge (Caddy) is pure admission and the app
never reads the identity header. Actor-less runs (webhooks, schedules)
execute as the fallback account — user_settings.fallback_account_id,
set by the very first login, resolved through
HarnessConnection.get_for_account(harness, fallback=True) — replacing
the per-harness is_default election (column, partial index, and chip
dropped; the migration backfills from the old default).
The frontend mounts the app behind an AuthProvider: session check,
landing screen sharing the PKCE paste-back flow with Settings
reconnect, and auth expiry broadcast as a native window event that
unmounts the app and its per-mount query cache.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
czpython
force-pushed
the
commonzenpython/eng-703-make-harness-login-the-account-door-sessions-and-application
branch
from
July 16, 2026 22:04
d4d45ee to
59d1412
Compare
czpython
deleted the
commonzenpython/eng-703-make-harness-login-the-account-door-sessions-and-application
branch
July 16, 2026 22:10
This was referenced Jul 17, 2026
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.
Multi-tenant accounts arc, PR 2 of 4 (ENG-703). Harness login becomes the account door: internal APIs and SSE require a session, capability routes keep their own auth, and the Caddy gate loosens to any nonempty trusted identity.
What this delivers
Canonical login API (
druks/accounts/routes.py, the only login surface):GET /api/auth/session→AccountResponseor 401;POST /api/auth/harnesses/{name}/login/start→{authorizeUrl, loginId};POST .../login/complete{loginId, code}→ attaches the seat, mints the session cookie;POST /api/auth/logout./api/authclient methods.Account resolution (ticket order, exactly): valid session keeps its account → proxy identity stored at flow start → provider-verified email; normalized exact-equality lookup; new empty account when none.
HarnessLogin.connectis account-first: a provider seat attached to a different account raisesSeatClaimedError→ 409, never an implicit move. Mismatched authoritative/provider emails surface asisEmailMismatchon the settings card — visible, never blocking.Sessions (
druks/accounts/sessions.py+dependencies.py+middleware.py):druks:session:{token}→ account id, 30-day sliding TTL via atomicGETEX.druks_sessioncookie: HttpOnly, SameSite=Lax, Path=/, 30-day Max-Age, Secure when the browser endpoint is HTTPS (via X-Forwarded-Proto).current_account, withresolve_session_accountas the seam a future bearer/PAT resolver slots beside.Auth boundary: every internal platform router and every discovered extension router (generic transcript/board/subject
/streamincluded) mounts behindDepends(current_account). Exempt, each with its own auth or safe by construction: health,/_external/*, login start/complete + logout, static SPA/extension assets, andPOST /api/notifications/{token}/respond— split onto its own capability router with behavior unchanged.test_auth_boundary.pyenumerates both directions (nothing unguarded slips in; nothing exempt grows the gate; every/streamfamily guarded).Frontend:
AuthProvideraboveApp— only an authenticated state mounts queries/EventSources; landing screen with Connect Codex / Connect Claude (same paste-back flow via a shared hook); centralizedcredentials: "same-origin"and typed 401 (UnauthorizedError) that broadcasts expiry and unmounts the app (closing every stream);useSSErechecks/api/auth/sessionon error and closes the source when the session died — no blind reconnects; the settings card shows authoritative email, provider email, mismatch warning, expiry, and default-seat state.Deploy/env/docs:
DRUKS_AUTH_HEADERis now a backendSettingsfield;DRUKS_DASHBOARD_EMAILdeleted from setup-env generation/tests, the Compose pass-through, and Caddy (it remains only in the PR1 migration + its upgrade window, by design); Caddy admits any nonempty trusted identity and keeps the no-header exe.dev redirect + separate webhook listener; configuration/full-local/deployment/troubleshooting docs updated; the deploy README's cutover section now spells out replacing host-copied Compose/Caddy, removing the stale.envvalue, and recreatingweb+caddy.Acceptance criteria → tests
test_auth_boundary.py(4 tests, both directions + stream families),test_auth.py::test_protected_routes_401_without_a_sessiontest_auth.py::test_dashboard_identity_resolves_the_migrated_account,::test_new_identity_cannot_acquire_legacy_seatstest_auth.py::test_proxy_identity_is_authoritative_for_the_account,test_api_settings.py::test_harness_card_reports_mismatch_and_default_statetest_auth.py: login flow, rotation, logout, eviction, proxy mismatch, single-use,::test_claimed_seat_conflicts_with_409,::test_session_keeps_its_account_across_reconnects; flow-binding tests intest_harness_login.pyuser_settings/routes.py+ shareduseHarnessLoginhook;HarnessConnect.test.tsxdrives/api/authAuthProvider.test.tsx(landing/session transitions),client.test.ts(typed 401 + same-origin),sse.test.tsx(expiry recheck, no blind reconnects),HarnessConnect.test.tsx(settings connection) — 34/34 locallyRollout (from the ticket)
Deploy backend and SPA behind the old single-email Caddy gate first (compatible during transition). Then replace the host-copied Compose/Caddy, remove
DRUKS_DASHBOARD_EMAILfrom.env, and recreateweb+caddy(steps now in deploy/README.md). Each browser performs one harness login to mint its session.Codex review
Adversarial review through the codex companion, read-only (
gpt-5.5, reasoning effort high —solis rejected on this account's ChatGPT plan). Five findings:account_idcompletes only under that same live session — otherwise 422, re-start. Pinned bytest_bound_reconnect_requires_its_session_at_complete.UserPreferencesProvidersat above the auth gate and fired/api/settingson unauthenticated cold loads. The provider tree is now AuthProvider-first; every query mounts below the session.QueryClientsurvived logout/account switches, so account A's cached settings/harness/events data could render for account B. The query cache is now created per signed-in account (AuthedApp, keyed onaccount.id) — a new session always starts empty./api/mcp-serverschanges the MCP OAuth callback's auth despite the scope fence." The ticket's exemption list is explicit and exhaustive, and the MCP callback is not on it — exempting it here would be the actual scope deviation. Behaviorally: the callback is a top-level redirect,SameSite=Laxsends the session cookie, so live sessions complete normally; a session that dies mid-connect means redoing that MCP connect. If that UX matters it's a one-line exemption for the MCP arc to take deliberately.docs/concepts.mdstill described the single-email edge gate; rewritten to the session/account-door model.No refutations beyond these: the boundary enumeration, session mechanics, PKCE single-use shape, seat-conflict 409, and legacy-seat protections all held.
Stack
Supersedes #11 (auto-closed when its base branch merged as #10). Rebased onto the landed main — the replay adopts the review direction that shipped there: the
HarnessConnectionrename, citext emails stored as-given (Python email comparisons mirror citext's case-insensitivity),connect()upserting purely by(harness, account)with the provider-seat 409 and the cross-harness flow guard dropped per the review rationale. ENG-704 (PR #13) and ENG-705 (PR #14) still point at the pre-rebase branches; they get rebased when their turn comes.🤖 Generated with Claude Code