From 0c7a69319d392241af962df98e22bafa4ae0d1b9 Mon Sep 17 00:00:00 2001 From: Paulo Date: Tue, 21 Jul 2026 18:49:27 +0200 Subject: [PATCH] Extract request-actor context from browser sessions --- backend/druks/accounts/context.py | 5 +++++ backend/druks/accounts/dependencies.py | 5 +++-- backend/druks/accounts/sessions.py | 5 ----- backend/druks/workflows.py | 4 ++-- backend/tests/conftest.py | 2 +- backend/tests/test_api_usage.py | 2 +- backend/tests/test_durable_sdk.py | 4 ++-- 7 files changed, 14 insertions(+), 13 deletions(-) create mode 100644 backend/druks/accounts/context.py diff --git a/backend/druks/accounts/context.py b/backend/druks/accounts/context.py new file mode 100644 index 0000000..9327a27 --- /dev/null +++ b/backend/druks/accounts/context.py @@ -0,0 +1,5 @@ +from contextvars import ContextVar + +# The request's authenticated account, stamped by the auth gate — Workflow.start +# reads it so a browser-origin run attributes itself without route ceremony. +current_account_id: ContextVar[str | None] = ContextVar("current_account_id", default=None) diff --git a/backend/druks/accounts/dependencies.py b/backend/druks/accounts/dependencies.py index d00bb70..bae574a 100644 --- a/backend/druks/accounts/dependencies.py +++ b/backend/druks/accounts/dependencies.py @@ -1,6 +1,7 @@ from fastapi import HTTPException, Request from druks.accounts import sessions +from druks.accounts.context import current_account_id from druks.accounts.exceptions import InvalidPatError from druks.accounts.models import Account, PersonalAccessToken @@ -22,7 +23,7 @@ async def current_session_account(request: Request) -> Account: management, so a token can never manage tokens.""" account = await resolve_session_account(request) if account: - sessions.current_account_id.set(account.id) + current_account_id.set(account.id) return account raise HTTPException(status_code=401, detail="Sign in to use this API.") @@ -45,7 +46,7 @@ async def current_account(request: Request) -> Account: detail=str(error), headers={"WWW-Authenticate": f'{_BEARER_CHALLENGE}, error="invalid_token"'}, ) from error - sessions.current_account_id.set(pat.account_id) + current_account_id.set(pat.account_id) return pat.account raise HTTPException( status_code=401, diff --git a/backend/druks/accounts/sessions.py b/backend/druks/accounts/sessions.py index 20d8c61..96a6138 100644 --- a/backend/druks/accounts/sessions.py +++ b/backend/druks/accounts/sessions.py @@ -1,5 +1,4 @@ import secrets -from contextvars import ContextVar from druks.redis import get_client @@ -8,10 +7,6 @@ SESSION_COOKIE = "druks_session" SESSION_TTL_SECONDS = 30 * 24 * 3600 -# The request's signed-in account, stamped by the session gate — Workflow.start -# reads it so a browser-origin run attributes itself without route ceremony. -current_account_id: ContextVar[str | None] = ContextVar("current_account_id", default=None) - async def mint_session(account_id: str) -> str: token = secrets.token_urlsafe(32) diff --git a/backend/druks/workflows.py b/backend/druks/workflows.py index 8c991a8..0294493 100644 --- a/backend/druks/workflows.py +++ b/backend/druks/workflows.py @@ -19,7 +19,7 @@ from pydantic import BaseModel, ConfigDict, Field, create_model from uuid_utils import uuid7 -from druks.accounts.sessions import current_account_id +from druks.accounts.context import current_account_id from druks.durable.activity import get_run_phase, set_run_phase from druks.durable.engine import _step_engine, register_schedule, run_queue, step_session from druks.durable.enums import AgentCallStatus, RunState @@ -636,7 +636,7 @@ async def start( # subject is required (no default) so a run can't silently lose its # timeline by omission — pass subject=None explicitly for a background run. if account_id is None: - # Browser-origin starts inherit the session gate's account; + # Browser-origin starts inherit the request's authenticated account; # dispatchers that know better pass account_id explicitly. account_id = current_account_id.get() if subject is not None: diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py index 56eedef..859328b 100644 --- a/backend/tests/conftest.py +++ b/backend/tests/conftest.py @@ -363,8 +363,8 @@ def configure_app_for_test( async def _test_account(): + from druks.accounts.context import current_account_id from druks.accounts.models import Account - from druks.accounts.sessions import current_account_id account = Account.get_or_create("op@example.com") current_account_id.set(account.id) diff --git a/backend/tests/test_api_usage.py b/backend/tests/test_api_usage.py index 4cdbbb6..e900ef6 100644 --- a/backend/tests/test_api_usage.py +++ b/backend/tests/test_api_usage.py @@ -21,7 +21,7 @@ def client(extension_settings: Settings): def _account_id() -> str: - # The suite's session gate stands in op@example.com (conftest override). + # The suite's auth gate stands in op@example.com (conftest override). from druks.accounts.models import Account return Account.get_or_create("op@example.com").id diff --git a/backend/tests/test_durable_sdk.py b/backend/tests/test_durable_sdk.py index 8a7f9bb..7e45bec 100644 --- a/backend/tests/test_durable_sdk.py +++ b/backend/tests/test_durable_sdk.py @@ -288,9 +288,9 @@ async def test_attribution_rides_the_run_and_survives_resume(rt): async def test_browser_origin_start_inherits_the_ambient_account(rt): - # The session gate stamps the request's account into a contextvar; + # The auth gate stamps the request's account into a contextvar; # start() reads it when no explicit account_id is passed. - from druks.accounts.sessions import current_account_id + from druks.accounts.context import current_account_id account_id = _account_id(rt.engine, "ambient@example.com") token = current_account_id.set(account_id)