innercontext/.sisyphus/plans/multi-user-authelia-oidc.md
Piotr Oleszczyk dac787b81b test(auth): add multi-user regression coverage
- Enable backend tests in CI (remove if: false)
- Fix test_products_helpers.py to pass current_user parameter
- Fix test_routines_helpers.py to include short_id in products
- Fix llm_context.py to use product_effect_profile correctly
- All 221 tests passing
2026-03-12 16:42:00 +01:00

49 KiB

Multi-User Support with Authelia OIDC

TL;DR

Summary: Convert the monorepo from a single-user personal system into a multi-user application authenticated by Authelia OIDC, with SvelteKit owning the login/session flow and FastAPI enforcing row-level ownership and household-scoped inventory sharing. Deliverables:

  • OIDC login/logout/session flow in SvelteKit
  • FastAPI token validation, current-user resolution, and authorization helpers
  • New local identity/household schema plus ownership migrations for existing data
  • Household-shared inventory support with owner/admin product controls
  • Updated infra, CI, and verification coverage for the new auth model Effort: XL Parallel: YES - 3 waves Critical Path: T1 -> T2 -> T3 -> T4 -> T5/T6 -> T7/T8 -> T11

Context

Original Request

Add multi-user support with login handled by Authelia using OpenID Connect.

Interview Summary

  • Auth model: app-managed OIDC with SvelteKit-owned session handling; FastAPI acts as the resource server.
  • Roles: admin and member; admins can manage member data and household memberships, but v1 excludes impersonation and a full user-management console.
  • Ownership model: records are user-owned by default; products stay user-owned in v1.
  • Sharing exception: product inventory may be shared among members of the same household; shared household members may view and update inventory entries, but only the product owner or an admin may edit/delete the underlying product.
  • Rollout: retrofit the existing application in one implementation plan rather than staging auth separately.
  • Identity source: Authelia remains the source of truth; no in-app signup/provisioning UI in v1.
  • Verification preference: do not add a permanent frontend test suite in this pass; still require backend tests plus agent-executed QA scenarios.

Metis Review (gaps addressed)

  • Made household sharing explicit with a local households + household_memberships model instead of overloading OIDC groups.
  • Added a deterministic legacy-data backfill step so existing single-user records are assigned to the first configured admin identity during migration.
  • Called out llm_context.py, helper functions like get_or_404(), and all row-fetching routes as mandatory scoping points so no single-user path survives.
  • Chose JWT access-token validation via Authelia JWKS for FastAPI, with SvelteKit calling userinfo to hydrate the app session and local user record.
  • Kept browser QA agent-executed and out of repo while still requiring backend auth tests and CI enablement.

Work Objectives

Core Objective

Implement a secure, decision-complete multi-user architecture that uses Authelia OIDC for authentication, local app users/households for authorization, row ownership across existing data models, and household-scoped inventory sharing without broadening scope into a full account-management product.

Deliverables

  • Backend identity/auth models for local users, households, memberships, and role mapping.
  • Alembic migration/backfill converting all existing domain data to owned records.
  • FastAPI auth dependencies, token validation, and authorization utilities.
  • Retrofitted API routes and LLM context builders that enforce ownership.
  • SvelteKit login, callback, logout, refresh, and protected-route behavior.
  • Auth-aware API access from frontend server actions and protected page loads.
  • Admin-only backend endpoints for household membership management without a UI console.
  • nginx, deploy, CI, and environment updates needed for OIDC rollout.

Definition of Done (verifiable conditions with commands)

  • cd backend && uv run pytest
  • cd backend && uv run ruff check .
  • cd frontend && pnpm check
  • cd frontend && pnpm lint
  • cd frontend && pnpm build
  • cd backend && uv run python -c "import json; from main import app; print(json.dumps(app.openapi())[:200])"

Must Have

  • OIDC Authorization Code flow with PKCE, server-handled callback, HTTP-only app session cookie, refresh-token renewal, and logout.
  • FastAPI bearer-token validation against Authelia JWKS; no trusted identity headers between app tiers.
  • Local users, households, and household_memberships tables keyed by issuer + sub rather than email.
  • user_id ownership enforcement across profile, health, routines, skincare, AI logs, and products.
  • Household inventory-sharing model that permits view/update of shared inventory by household members while preserving owner/admin control of product records.
  • Deterministic backfill of legacy records to a configured bootstrap admin identity.
  • Admin/member authorization rules enforced in backend dependencies and mirrored in frontend navigation/controls.
  • Backend auth and authorization tests, plus CI job enablement for those tests.

Must NOT Have (guardrails, AI slop patterns, scope boundaries)

  • No proxy-header trust model between SvelteKit and FastAPI.
  • No in-app signup, password reset, email verification, impersonation, or full user-management console.
  • No multi-household membership per user in v1.
  • No global shared product catalog refactor in this pass.
  • No audit-log productization, notification system, or support tooling.
  • No permanent Playwright/Vitest suite added to the repo in this pass.

Verification Strategy

ZERO HUMAN INTERVENTION - all verification is agent-executed.

  • Test decision: tests-after using existing backend pytest + TestClient; no new committed frontend suite, but include agent-executed browser QA and curl-based verification.
  • QA policy: every task includes happy-path and failure/edge-case scenarios with exact commands or browser actions.
  • Evidence: .sisyphus/evidence/task-{N}-{slug}.{ext}

Execution Strategy

Parallel Execution Waves

Target: 5-8 tasks per wave. <3 per wave (except final) = under-splitting. Extract shared dependencies as Wave-1 tasks for max parallelism.

Wave 1: T1 identity models, T2 ownership migration, T3 backend token validation, T4 tenant-aware authorization helpers

Wave 2: T5 product/inventory authorization retrofit, T6 remaining domain scoping retrofit, T7 SvelteKit auth/session flow, T8 frontend auth-aware plumbing and shell behavior

Wave 3: T9 admin household-management endpoints, T10 infra/env/CI/deploy updates, T11 backend auth regression coverage and release verification

Dependency Matrix (full, all tasks)

Task Depends On Blocks
T1 - T2, T3, T4, T9
T2 T1 T5, T6, T11
T3 T1 T4, T5, T6, T7, T8, T9, T11
T4 T1, T3 T5, T6, T9
T5 T2, T3, T4 T11
T6 T2, T3, T4 T11
T7 T3 T8, T10, T11
T8 T7 T11
T9 T1, T2, T3, T4 T11
T10 T3, T7 T11
T11 T2, T3, T4, T5, T6, T7, T8, T9, T10 Final verification

Agent Dispatch Summary (wave -> task count -> categories)

  • Wave 1 -> 4 tasks -> deep, unspecified-high
  • Wave 2 -> 4 tasks -> deep, unspecified-high, writing
  • Wave 3 -> 3 tasks -> unspecified-high, writing, deep

TODOs

Implementation + Test = ONE task. Never separate. EVERY task MUST have: Agent Profile + Parallelization + QA Scenarios.

  • T1. Add local identity, role, household, and sharing models

    What to do: Add a new backend model module for User, Household, and HouseholdMembership; extend existing domain models with ownership fields; add a compact role enum (admin, member) and a household-membership role enum (owner, member). Use issuer + subject as the immutable OIDC identity key, enforce at most one household membership per user in v1, and add is_household_shared: bool = False to ProductInventory so sharing is opt-in per inventory row rather than automatic for an entire household. Must NOT do: Do not key users by email, do not introduce multi-household membership, do not split Product into catalog vs overlay tables in this pass, and do not add frontend management UI here.

    Recommended Agent Profile:

    • Category: deep - Reason: cross-cutting schema design with downstream auth and authorization consequences
    • Skills: [] - Existing backend conventions are the main source of truth
    • Omitted: svelte-code-writer - No Svelte files belong in this task

    Parallelization: Can Parallel: NO | Wave 1 | Blocks: T2, T3, T4, T9 | Blocked By: -

    References (executor has NO interview context - be exhaustive):

    • Pattern: backend/innercontext/models/profile.py:13 - Simple SQLModel table with UUID PK and timestamp conventions to follow for user-owned profile data.
    • Pattern: backend/innercontext/models/product.py:138 - Main table-model style, JSON-column usage, and updated_at pattern.
    • Pattern: backend/innercontext/models/product.py:353 - Existing ProductInventory table to extend with ownership and sharing fields.
    • Pattern: backend/innercontext/models/__init__.py:1 - Export surface that must include every new model/type.
    • API/Type: backend/innercontext/models/enums.py - Existing enum location; add role enums here unless a dedicated auth model module makes more sense.

    Acceptance Criteria (agent-executable only):

    • backend/innercontext/models/ defines User, Household, and HouseholdMembership with UUID PKs, timestamps, uniqueness on (oidc_issuer, oidc_subject), and one-household-per-user enforcement.
    • Product, ProductInventory, UserProfile, MedicationEntry, MedicationUsage, LabResult, Routine, RoutineStep, GroomingSchedule, SkinConditionSnapshot, and AICallLog each expose an ownership field (user_id) in model code, with ProductInventory also exposing is_household_shared.
    • innercontext.models re-exports the new auth/household types so metadata loading and imports continue to work.
    • cd backend && uv run python -c "import innercontext.models as m; print(all(hasattr(m, name) for name in ['User','Household','HouseholdMembership']))" prints True.

    QA Scenarios (MANDATORY - task incomplete without these):

    Scenario: Identity models load into SQLModel metadata
      Tool: Bash
      Steps: Run `cd backend && uv run python -c "import innercontext.models; from sqlmodel import SQLModel; print(sorted(t.name for t in SQLModel.metadata.sorted_tables if t.name in {'users','households','household_memberships'}))" > ../.sisyphus/evidence/task-T1-identity-models.txt`
      Expected: Evidence file lists `['household_memberships', 'households', 'users']`
      Evidence: .sisyphus/evidence/task-T1-identity-models.txt
    
    Scenario: Product inventory sharing stays opt-in
      Tool: Bash
      Steps: Run `cd backend && uv run python -c "from innercontext.models.product import ProductInventory; f=ProductInventory.model_fields['is_household_shared']; print(f.default)" > ../.sisyphus/evidence/task-T1-sharing-default.txt`
      Expected: Evidence file contains `False`
      Evidence: .sisyphus/evidence/task-T1-sharing-default.txt
    

    Commit: YES | Message: feat(auth): add local user and household models | Files: backend/innercontext/models/*

  • T2. Add Alembic migration and bootstrap backfill for legacy single-user data

    What to do: Create an Alembic revision that creates users, households, and household_memberships, adds user_id ownership columns and related foreign keys/indexes to all owned tables, and adds is_household_shared to product_inventory. Use a two-step migration: add nullable columns, create/bootstrap a local admin user + default household from environment variables, backfill every existing row to that bootstrap user, then enforce non-null ownership constraints. Use env names BOOTSTRAP_ADMIN_OIDC_ISSUER, BOOTSTRAP_ADMIN_OIDC_SUB, BOOTSTRAP_ADMIN_EMAIL, BOOTSTRAP_ADMIN_NAME, and BOOTSTRAP_HOUSEHOLD_NAME; abort the migration with a clear error if legacy data exists and the required issuer/sub values are missing. Must NOT do: Do not assign ownership based on email matching, do not silently create random bootstrap identities, and do not leave owned tables nullable after the migration completes.

    Recommended Agent Profile:

    • Category: deep - Reason: schema migration, backfill, and irreversible data-shape change
    • Skills: [] - Use existing Alembic patterns from the repo
    • Omitted: git-master - Commit strategy is already prescribed here

    Parallelization: Can Parallel: NO | Wave 1 | Blocks: T5, T6, T11 | Blocked By: T1

    References (executor has NO interview context - be exhaustive):

    • Pattern: backend/alembic/versions/ - Existing migration naming/layout conventions to follow.
    • Pattern: backend/innercontext/models/product.py:180 - Timestamp/nullability expectations that migrated columns must preserve.
    • Pattern: backend/db.py:17 - Metadata creation path; migration must leave runtime startup compatible.
    • API/Type: backend/innercontext/models/profile.py:13 - Existing singleton-style table that must become owned data.
    • API/Type: backend/innercontext/models/product.py:353 - Inventory table receiving the sharing flag.

    Acceptance Criteria (agent-executable only):

    • A new Alembic revision exists under backend/alembic/versions/ creating auth/household tables and ownership columns/indexes/foreign keys.
    • The migration backfills all existing owned rows to the bootstrap admin user and creates that user's default household + owner membership.
    • The migration aborts with a readable exception if legacy data exists and BOOTSTRAP_ADMIN_OIDC_ISSUER or BOOTSTRAP_ADMIN_OIDC_SUB is absent.
    • Owned tables end with non-null user_id constraints after upgrade.

    QA Scenarios (MANDATORY - task incomplete without these):

    Scenario: Migration upgrade succeeds with bootstrap identity configured
      Tool: Bash
      Steps: Create a disposable DB URL (for example `sqlite:///../.sisyphus/evidence/task-T2-upgrade.sqlite`), then run `cd backend && DATABASE_URL=sqlite:///../.sisyphus/evidence/task-T2-upgrade.sqlite BOOTSTRAP_ADMIN_OIDC_ISSUER=https://auth.example.test BOOTSTRAP_ADMIN_OIDC_SUB=legacy-admin BOOTSTRAP_ADMIN_EMAIL=owner@example.test BOOTSTRAP_ADMIN_NAME='Legacy Owner' BOOTSTRAP_HOUSEHOLD_NAME='Default Household' uv run alembic upgrade head > ../.sisyphus/evidence/task-T2-migration-upgrade.txt`
      Expected: Command exits 0 and evidence file shows Alembic reached `head`
      Evidence: .sisyphus/evidence/task-T2-migration-upgrade.txt
    
    Scenario: Migration fails fast when bootstrap identity is missing for legacy data
      Tool: Bash
      Steps: Seed a disposable SQLite DB with one legacy row using the pre-migration schema, then run `cd backend && DATABASE_URL=sqlite:///../.sisyphus/evidence/task-T2-missing-bootstrap.sqlite uv run alembic upgrade head 2> ../.sisyphus/evidence/task-T2-migration-missing-bootstrap.txt`
      Expected: Upgrade exits non-zero and evidence contains a message naming both missing bootstrap env vars
      Evidence: .sisyphus/evidence/task-T2-migration-missing-bootstrap.txt
    

    Commit: YES | Message: feat(db): backfill tenant ownership for existing records | Files: backend/alembic/versions/*, backend/innercontext/models/*

  • T3. Implement FastAPI token validation, user sync, and current-user dependencies

    What to do: Add backend auth modules that validate Authelia JWT access tokens via JWKS with cached key material, enforce issuer/audience/expiry checks, map role groups to local roles, and expose dependencies like get_current_user() and require_admin(). Create protected auth endpoints for session sync and self introspection (for example /auth/session/sync and /auth/me) so SvelteKit can exchange token-derived/userinfo-derived identity details for a local User row and current app profile. Use env/config values for issuer, JWKS URL/discovery URL, client ID, and group names instead of hard-coding them. Must NOT do: Do not trust X-Forwarded-User-style headers, do not skip signature validation, do not derive role from email domain, and do not make backend routes public except health-check.

    Recommended Agent Profile:

    • Category: unspecified-high - Reason: focused backend auth implementation with security-sensitive logic
    • Skills: [] - No project skill is better than direct backend work here
    • Omitted: svelte-code-writer - No Svelte components involved

    Parallelization: Can Parallel: NO | Wave 1 | Blocks: T4, T5, T6, T7, T8, T9, T11 | Blocked By: T1

    References (executor has NO interview context - be exhaustive):

    • Pattern: backend/main.py:37 - Current FastAPI app construction and router registration point.
    • Pattern: backend/db.py:12 - Session dependency shape that auth dependencies must compose with.
    • Pattern: backend/innercontext/api/profile.py:27 - Router/dependency style used throughout the API.
    • External: https://www.authelia.com/configuration/identity-providers/openid-connect/provider/ - OIDC provider/discovery and JWKS behavior.
    • External: https://www.authelia.com/integration/openid-connect/openid-connect-1.0-claims/ - Claims and userinfo behavior; use issuer + sub as identity key.

    Acceptance Criteria (agent-executable only):

    • A backend auth module validates bearer tokens against Authelia JWKS with issuer/audience checks and cached key refresh.
    • Protected dependencies expose a normalized current user object with local user_id, role, and household membership information.
    • Backend includes protected auth sync/introspection endpoints used by SvelteKit to upsert local users from OIDC identity data.
    • Unauthenticated access to owned API routes returns 401; authenticated access with a valid token reaches router logic.

    QA Scenarios (MANDATORY - task incomplete without these):

    Scenario: Valid bearer token resolves a current user
      Tool: Bash
      Steps: Run `cd backend && uv run pytest tests/test_auth.py -k sync > ../.sisyphus/evidence/task-T3-auth-sync.txt`
      Expected: Auth sync/introspection tests pass and evidence includes the protected auth endpoint names
      Evidence: .sisyphus/evidence/task-T3-auth-sync.txt
    
    Scenario: Missing or invalid bearer token is rejected
      Tool: Bash
      Steps: Run `cd backend && uv run pytest tests/test_auth.py -k unauthorized > ../.sisyphus/evidence/task-T3-auth-unauthorized.txt`
      Expected: Tests pass and evidence shows `401` expectations
      Evidence: .sisyphus/evidence/task-T3-auth-unauthorized.txt
    

    Commit: YES | Message: feat(auth): validate Authelia tokens in FastAPI | Files: backend/main.py, backend/innercontext/auth.py, backend/innercontext/api/auth*.py

  • T4. Centralize tenant-aware fetch helpers and authorization predicates

    What to do: Replace single-user helper assumptions with reusable authorization helpers that every router can call. Add tenant-aware helpers for owned lookup, admin override, same-household checks, and household-shared inventory visibility/update rules. Keep get_session() unchanged, but add helpers/dependencies that make it difficult for routers to accidentally query global rows. Update or supersede get_or_404() with helpers that scope by user_id and return 404 for unauthorized record lookups unless the route intentionally needs 403. Must NOT do: Do not leave routers performing raw session.get() on owned models, do not duplicate household-sharing logic in every route, and do not use admin bypasses that skip existence checks.

    Recommended Agent Profile:

    • Category: deep - Reason: authorization rules must become the shared execution path for many routers
    • Skills: [] - This is backend architecture work, not skill-driven tooling
    • Omitted: frontend-design - No UI work belongs here

    Parallelization: Can Parallel: NO | Wave 1 | Blocks: T5, T6, T9 | Blocked By: T1, T3

    References (executor has NO interview context - be exhaustive):

    • Pattern: backend/innercontext/api/utils.py:9 - Existing naive get_or_404() helper that must no longer be used for owned records.
    • Pattern: backend/innercontext/api/products.py:934 - Current direct object fetch/update/delete route pattern to replace.
    • Pattern: backend/innercontext/api/inventory.py:14 - Inventory routes that currently expose rows globally.
    • Pattern: backend/innercontext/api/health.py:141 - Representative list/get/update/delete health routes requiring shared helpers.
    • Pattern: backend/innercontext/api/routines.py:674 - Another high-volume router that must consume the same authz utilities.

    Acceptance Criteria (agent-executable only):

    • Backend provides shared helper/dependency functions for owned lookups, admin checks, same-household checks, and shared-inventory updates.
    • get_or_404() is either retired for owned data or wrapped so no owned router path still uses the unscoped helper directly.
    • Shared inventory authorization distinguishes product ownership from inventory update rights.
    • Helper tests cover owner access, admin override, same-household shared inventory access, and cross-household denial.

    QA Scenarios (MANDATORY - task incomplete without these):

    Scenario: Authorization helpers allow owner/admin/household-shared access correctly
      Tool: Bash
      Steps: Run `cd backend && uv run pytest tests/test_authz.py -k 'owner or admin or household' > ../.sisyphus/evidence/task-T4-authz-happy.txt`
      Expected: Tests pass and evidence includes owner/admin/household cases
      Evidence: .sisyphus/evidence/task-T4-authz-happy.txt
    
    Scenario: Cross-household access is denied without leaking row existence
      Tool: Bash
      Steps: Run `cd backend && uv run pytest tests/test_authz.py -k denied > ../.sisyphus/evidence/task-T4-authz-denied.txt`
      Expected: Tests pass and evidence shows `404` or `403` assertions exactly where specified by the helper contract
      Evidence: .sisyphus/evidence/task-T4-authz-denied.txt
    

    Commit: YES | Message: refactor(api): centralize tenant authorization helpers | Files: backend/innercontext/api/utils.py, backend/innercontext/api/authz.py, router call sites

  • T5. Retrofit products and inventory endpoints for owned access plus household sharing

    What to do: Update products and inventory APIs so product visibility is owned OR household-visible-via-shared-inventory OR admin, while product mutation remains owner OR admin. Keep Product user-owned. For household members, allow GET on shared products/inventory rows and PATCH on shared inventory rows, but keep POST /products, PATCH /products/{id}, DELETE /products/{id}, POST /products/{id}/inventory, and DELETE /inventory/{id} restricted to owner/admin. Reuse the existing ProductListItem.is_owned field so shared-but-not-owned products are clearly marked in summaries. Ensure suggestion and summary endpoints only use products accessible to the current user. Must NOT do: Do not expose non-shared inventory across a household, do not let household members edit personal_tolerance_notes, and do not return global product lists anymore.

    Recommended Agent Profile:

    • Category: deep - Reason: most nuanced authorization rules live in product and inventory flows
    • Skills: [] - Backend logic and existing product patterns are sufficient
    • Omitted: frontend-design - No UI polish belongs here

    Parallelization: Can Parallel: YES | Wave 2 | Blocks: T11 | Blocked By: T2, T3, T4

    References (executor has NO interview context - be exhaustive):

    • Pattern: backend/innercontext/api/products.py:605 - List route currently returning global products.
    • Pattern: backend/innercontext/api/products.py:844 - Summary route already exposes is_owned; extend rather than replacing it.
    • Pattern: backend/innercontext/api/products.py:934 - Detail/update/delete routes that currently use direct lookup.
    • Pattern: backend/innercontext/api/products.py:977 - Product inventory list/create routes.
    • Pattern: backend/innercontext/api/inventory.py:14 - Direct inventory get/update/delete routes that currently bypass ownership.
    • API/Type: backend/innercontext/models/product.py:353 - Inventory model fields involved in household sharing.
    • Test: backend/tests/test_products.py:38 - Existing CRUD/filter test style to extend for authz cases.

    Acceptance Criteria (agent-executable only):

    • Product list/detail/summary/suggest endpoints only return products accessible to the current user.
    • Shared household members can GET shared products/inventory and PATCH shared inventory rows, but cannot mutate product records or create/delete another user's inventory rows.
    • Product summaries preserve is_owned semantics for shared products.
    • Product/inventory tests cover owner, admin, same-household shared member, and different-household member cases.

    QA Scenarios (MANDATORY - task incomplete without these):

    Scenario: Household member can view a shared product and update its shared inventory row
      Tool: Bash
      Steps: Run `cd backend && uv run pytest tests/test_products_auth.py -k 'shared_inventory_update or shared_product_visible' > ../.sisyphus/evidence/task-T5-product-sharing.txt`
      Expected: Tests pass and evidence shows `200` assertions for shared view/update cases
      Evidence: .sisyphus/evidence/task-T5-product-sharing.txt
    
    Scenario: Household member cannot edit or delete another user's product
      Tool: Bash
      Steps: Run `cd backend && uv run pytest tests/test_products_auth.py -k 'cannot_edit_shared_product or cannot_delete_shared_product' > ../.sisyphus/evidence/task-T5-product-denied.txt`
      Expected: Tests pass and evidence shows `403` or `404` assertions matching the route contract
      Evidence: .sisyphus/evidence/task-T5-product-denied.txt
    

    Commit: YES | Message: feat(api): scope products and inventory by owner and household | Files: backend/innercontext/api/products.py, backend/innercontext/api/inventory.py, related tests

  • T6. Retrofit remaining domain routes, LLM context, and jobs for per-user ownership

    What to do: Update profile, health, routines, skincare, AI log, and LLM-context code so every query is user-scoped by default and admin override is explicit. UserProfile becomes one-per-user rather than singleton; build_user_profile_context() and product-context builders must accept the current user and only include accessible data. Routine suggestion/batch flows must use the current user's profile plus products visible under the owned/shared rules from T5. Ensure background pricing/job paths preserve user_id on products and logs, and that list endpoints never aggregate cross-user data for non-admins. Must NOT do: Do not keep any select(Model) query unfiltered on an owned model, do not keep singleton profile lookups, and do not leak other users' AI logs or health data through helper functions.

    Recommended Agent Profile:

    • Category: deep - Reason: many routers and helper layers need consistent tenancy retrofits
    • Skills: [] - Backend cross-module work only
    • Omitted: svelte-code-writer - No Svelte component work in this task

    Parallelization: Can Parallel: YES | Wave 2 | Blocks: T11 | Blocked By: T2, T3, T4

    References (executor has NO interview context - be exhaustive):

    • Pattern: backend/innercontext/api/profile.py:27 - Current singleton profile route using get_user_profile(session).
    • Pattern: backend/innercontext/api/llm_context.py:10 - Single-user helper that currently selects the most recent profile globally.
    • Pattern: backend/innercontext/api/health.py:141 - Medication and lab-result CRUD/list route layout.
    • Pattern: backend/innercontext/api/routines.py:674 - Routine list/create/suggest entry points that need scoped product/profile data.
    • Pattern: backend/innercontext/api/skincare.py:222 - Snapshot list/get/update/delete route structure.
    • Pattern: backend/innercontext/api/ai_logs.py:46 - AI-log exposure that must become owned/admin-only.
    • Pattern: backend/innercontext/services/pricing_jobs.py - Background queue path that must preserve product ownership.

    Acceptance Criteria (agent-executable only):

    • Every non-admin router outside products/inventory scopes owned data by user_id before returning or mutating rows.
    • GET /profile and PATCH /profile operate on the current user's profile, not the newest global profile.
    • Routine suggestion and batch suggestion flows use only the current user's profile plus accessible products.
    • AI logs are owned/admin-only, and background job/log creation stores user_id when applicable.

    QA Scenarios (MANDATORY - task incomplete without these):

    Scenario: Member only sees their own health, routine, profile, skin, and AI-log data
      Tool: Bash
      Steps: Run `cd backend && uv run pytest tests/test_tenancy_domains.py -k 'profile or health or routines or skincare or ai_logs' > ../.sisyphus/evidence/task-T6-domain-tenancy.txt`
      Expected: Tests pass and evidence shows only owned/admin-allowed access patterns
      Evidence: .sisyphus/evidence/task-T6-domain-tenancy.txt
    
    Scenario: Routine suggestions ignore another user's products and profile
      Tool: Bash
      Steps: Run `cd backend && uv run pytest tests/test_routines_auth.py -k suggest > ../.sisyphus/evidence/task-T6-routine-scope.txt`
      Expected: Tests pass and evidence shows suggestion inputs are scoped to the authenticated user plus shared inventory visibility rules
      Evidence: .sisyphus/evidence/task-T6-routine-scope.txt
    

    Commit: YES | Message: feat(api): enforce ownership across health routines and profile flows | Files: backend/innercontext/api/profile.py, backend/innercontext/api/health.py, backend/innercontext/api/routines.py, backend/innercontext/api/skincare.py, backend/innercontext/api/ai_logs.py, backend/innercontext/api/llm_context.py

  • T7. Implement SvelteKit OIDC login, callback, logout, refresh, and protected-session handling

    What to do: Add server-only auth utilities under frontend/src/lib/server/ and implement Authorization Code + PKCE in SvelteKit using Authelia discovery/token/userinfo endpoints. Create /auth/login, /auth/callback, and /auth/logout server routes. Extend hooks.server.ts to decrypt/load the app session, refresh the access token when it is near expiry, populate event.locals.user and event.locals.session, and redirect unauthenticated requests on all application routes except /auth/* and static assets. Use an encrypted HTTP-only cookie named innercontext_session with sameSite=lax, secure in production, and a 32-byte secret from private env. Must NOT do: Do not store access or refresh tokens in localStorage, do not expose client secrets via $env/static/public, and do not protect routes with client-only guards.

    Recommended Agent Profile:

    • Category: unspecified-high - Reason: server-side SvelteKit auth flow with cookies, hooks, and redirects
    • Skills: [svelte-code-writer] - Required for editing SvelteKit auth and route modules cleanly
    • Omitted: frontend-design - This task is auth/session behavior, not visual redesign

    Parallelization: Can Parallel: YES | Wave 2 | Blocks: T8, T10, T11 | Blocked By: T3

    References (executor has NO interview context - be exhaustive):

    • Pattern: frontend/src/hooks.server.ts:1 - Current global request hook; auth must compose with existing Paraglide middleware rather than replacing it.
    • Pattern: frontend/src/app.d.ts:3 - Add typed App.Locals/PageData session fields here.
    • Pattern: frontend/src/routes/+layout.svelte:30 - App shell/navigation that will consume authenticated user state later.
    • Pattern: frontend/src/routes/products/suggest/+page.server.ts:4 - Existing SvelteKit server action style using fetch.
    • External: https://www.authelia.com/configuration/identity-providers/openid-connect/clients/ - Client configuration expectations for auth code flow and PKCE.

    Acceptance Criteria (agent-executable only):

    • SvelteKit exposes login/callback/logout server routes that complete the OIDC flow against Authelia and create/destroy innercontext_session.
    • hooks.server.ts populates event.locals.user/event.locals.session, refreshes tokens near expiry, and redirects unauthenticated users away from protected pages.
    • The callback flow calls backend auth sync before treating the user as signed in.
    • Session cookies are HTTP-only and sourced only from private env/config.

    QA Scenarios (MANDATORY - task incomplete without these):

    Scenario: Login callback establishes an authenticated server session
      Tool: Playwright
      Steps: Navigate to `/products` while signed out, follow redirect to `/auth/login`, on the Authelia page fill the `Username` and `Password` fields using `E2E_AUTHELIA_USERNAME`/`E2E_AUTHELIA_PASSWORD`, submit the primary login button, wait for redirect back to the app, then save an accessibility snapshot to `.sisyphus/evidence/task-T7-login-flow.md`
      Expected: Final URL is inside the app, the protected page renders, and the session cookie exists
      Evidence: .sisyphus/evidence/task-T7-login-flow.md
    
    Scenario: Expired or refresh-failed session redirects back to login
      Tool: Playwright
      Steps: Start from an authenticated session, replace the `innercontext_session` cookie with one containing an expired access token or invalidate the refresh endpoint in the browser session, reload `/products`, and save a snapshot to `.sisyphus/evidence/task-T7-refresh-failure.md`
      Expected: The app clears the session cookie and redirects to `/auth/login`
      Evidence: .sisyphus/evidence/task-T7-refresh-failure.md
    

    Commit: YES | Message: feat(frontend): add Authelia OIDC session flow | Files: frontend/src/hooks.server.ts, frontend/src/app.d.ts, frontend/src/lib/server/auth.ts, frontend/src/routes/auth/*

  • T8. Refactor frontend data access, route guards, and shell state around the server session

    What to do: Refactor frontend API access so protected backend calls always originate from SvelteKit server loads/actions/endpoints using the access token from event.locals.session. Convert browser-side direct $lib/api usage to server actions or same-origin SvelteKit endpoints, add a +layout.server.ts that exposes authenticated user data to the shell, and update +layout.svelte to show the current user role/name plus a logout action. Regenerate OpenAPI types if backend response models change and keep $lib/types as the canonical import surface. Must NOT do: Do not keep browser-side bearer-token fetches, do not bypass the server session by calling backend APIs directly from components, and do not hardcode English auth labels without Paraglide message keys.

    Recommended Agent Profile:

    • Category: unspecified-high - Reason: SvelteKit route plumbing plus shell-state integration
    • Skills: [svelte-code-writer] - Required because this task edits .svelte and SvelteKit route modules
    • Omitted: frontend-design - Preserve the existing editorial shell instead of redesigning it

    Parallelization: Can Parallel: YES | Wave 2 | Blocks: T11 | Blocked By: T7

    References (executor has NO interview context - be exhaustive):

    • Pattern: frontend/src/lib/api.ts:25 - Current request helper branching between browser and server; replace with session-aware server usage.
    • Pattern: frontend/src/routes/+layout.svelte:63 - Existing app shell where user state/logout should appear without breaking navigation.
    • Pattern: frontend/src/routes/+page.server.ts - Representative server-load pattern already used throughout the app.
    • Pattern: frontend/src/routes/skin/new/+page.svelte - Existing browser-side API import to eliminate or proxy through server logic.
    • Pattern: frontend/src/routes/routines/[id]/+page.svelte - Another browser-side API import that must stop calling the backend directly.
    • Pattern: frontend/src/routes/products/suggest/+page.server.ts:4 - Server action pattern to reuse for auth-aware fetches.
    • API/Type: frontend/src/lib/types.ts - Keep as the only frontend import surface after any pnpm generate:api run.

    Acceptance Criteria (agent-executable only):

    • Protected backend calls in frontend code use the server session access token and no longer depend on browser token storage.
    • Direct component-level $lib/api usage on protected paths is removed or wrapped behind same-origin server endpoints/actions.
    • App shell receives authenticated user/session data from server load and exposes a logout affordance.
    • pnpm generate:api is run if backend auth/API response changes require regenerated frontend types.

    QA Scenarios (MANDATORY - task incomplete without these):

    Scenario: Authenticated user navigates protected pages and sees session-aware shell state
      Tool: Playwright
      Steps: Log in, visit `/`, `/products`, `/profile`, and `/routines`; capture an accessibility snapshot to `.sisyphus/evidence/task-T8-protected-nav.md`
      Expected: Each page loads without redirect loops, and the shell shows the current user plus logout control
      Evidence: .sisyphus/evidence/task-T8-protected-nav.md
    
    Scenario: Unauthenticated browser access cannot hit protected data paths directly
      Tool: Playwright
      Steps: Start from a signed-out browser, open a page that previously imported `$lib/api` from a component, attempt the same interaction, capture console/network output to `.sisyphus/evidence/task-T8-signed-out-network.txt`
      Expected: The app redirects or blocks cleanly without leaking backend JSON responses into the UI
      Evidence: .sisyphus/evidence/task-T8-signed-out-network.txt
    

    Commit: YES | Message: refactor(frontend): route protected API access through server session | Files: frontend/src/lib/api.ts, frontend/src/routes/**/*.server.ts, frontend/src/routes/+layout.*, selected .svelte files, frontend/src/lib/types.ts

  • T9. Add admin-only household management API without a frontend console

    What to do: Add a small admin-only backend router for household administration so the app can support real household sharing without a management UI. Provide endpoints to list local users who have logged in, create a household, assign a user to a household, move a user between households, and remove a membership. Enforce the v1 rule that a user can belong to at most one household. Do not manage identity creation here; Authelia remains the identity source, and only locally synced users may be assigned. Non-bootstrap users should remain household-less until an admin assigns them. Must NOT do: Do not add Svelte pages for household management, do not let non-admins call these endpoints, and do not allow membership assignment for users who have never authenticated into the app.

    Recommended Agent Profile:

    • Category: unspecified-high - Reason: contained backend admin surface with sensitive authorization logic
    • Skills: [] - Backend conventions already exist in repo
    • Omitted: frontend-design - Explicitly no console/UI in scope

    Parallelization: Can Parallel: YES | Wave 3 | Blocks: T11 | Blocked By: T1, T2, T3, T4

    References (executor has NO interview context - be exhaustive):

    • Pattern: backend/main.py:50 - Router registration area; add a dedicated admin router here.
    • Pattern: backend/innercontext/api/profile.py:41 - Simple patch/upsert route style for small admin mutation endpoints.
    • Pattern: backend/innercontext/api/utils.py:9 - Error-handling pattern to preserve with tenant-aware replacements.
    • API/Type: backend/innercontext/models/profile.py:13 - Example of owned record exposed without extra wrapper models.
    • Test: backend/tests/conftest.py:34 - Dependency-override style for admin/member API tests.

    Acceptance Criteria (agent-executable only):

    • Backend exposes admin-only household endpoints for list/create/assign/move/remove operations.
    • Membership moves preserve the one-household-per-user rule.
    • Membership assignment only works for users already present in the local users table.
    • Admin-route tests cover admin success, member denial, and attempted assignment of unsynced users.

    QA Scenarios (MANDATORY - task incomplete without these):

    Scenario: Admin can create a household and assign a logged-in member
      Tool: Bash
      Steps: Run `cd backend && uv run pytest tests/test_admin_households.py -k 'create_household or assign_member' > ../.sisyphus/evidence/task-T9-admin-households.txt`
      Expected: Tests pass and evidence shows admin-only success cases
      Evidence: .sisyphus/evidence/task-T9-admin-households.txt
    
    Scenario: Member cannot manage households and unsynced users cannot be assigned
      Tool: Bash
      Steps: Run `cd backend && uv run pytest tests/test_admin_households.py -k 'forbidden or unsynced' > ../.sisyphus/evidence/task-T9-admin-households-denied.txt`
      Expected: Tests pass and evidence shows `403`/validation failures for forbidden assignments
      Evidence: .sisyphus/evidence/task-T9-admin-households-denied.txt
    

    Commit: YES | Message: feat(api): add admin household management endpoints | Files: backend/main.py, backend/innercontext/api/admin*.py, related tests

  • T10. Update runtime configuration, validation scripts, deploy checks, and operator docs for OIDC

    What to do: Update runtime configuration for both services so frontend and backend receive the new OIDC/session env vars at runtime, and document the exact Authelia client/server setup required. Keep nginx in a pure reverse-proxy role (no auth_request), but make sure forwarded host/proto information remains sufficient for callback URL generation. Extend scripts/validate-env.sh and deploy validation so missing auth env vars fail fast, and update scripts/healthcheck.sh plus deploy.sh health expectations because authenticated pages may now redirect to login instead of returning 200 for signed-out probes. Document bootstrap-admin env usage for the migration. Must NOT do: Do not add proxy-level auth, do not require manual post-deploy DB edits, and do not leave deploy health checks assuming / must return 200 when the app intentionally redirects signed-out users.

    Recommended Agent Profile:

    • Category: writing - Reason: configuration, deployment, and operator-facing documentation dominate this task
    • Skills: [] - Repo docs and service files are the governing references
    • Omitted: svelte-code-writer - No Svelte component changes needed

    Parallelization: Can Parallel: YES | Wave 3 | Blocks: T11 | Blocked By: T3, T7

    References (executor has NO interview context - be exhaustive):

    • Pattern: nginx/innercontext.conf:1 - Current reverse-proxy setup that must remain proxy-only.
    • Pattern: deploy.sh:313 - Service-wait and health-check functions to update for signed-out redirects and auth env validation.
    • Pattern: deploy.sh:331 - Backend/frontend health-check behavior currently assuming public app pages.
    • Pattern: scripts/validate-env.sh:57 - Existing required-env validation script to extend with OIDC/session/bootstrap keys.
    • Pattern: scripts/healthcheck.sh:10 - Current frontend health check that assumes / returns 200.
    • Pattern: systemd/innercontext.service - Backend runtime env injection point.
    • Pattern: systemd/innercontext-node.service - Frontend runtime env injection point.
    • Pattern: docs/DEPLOYMENT.md - Canonical operator runbook to update.

    Acceptance Criteria (agent-executable only):

    • Backend and frontend runtime configs declare/document all required OIDC/session/bootstrap env vars.
    • Deploy validation fails fast when required auth env vars are missing.
    • Frontend health checks accept the signed-out auth redirect behavior or target a public route that remains intentionally available.
    • Deployment docs describe Authelia client config, callback/logout URLs, JWKS/issuer envs, and bootstrap-migration envs.

    QA Scenarios (MANDATORY - task incomplete without these):

    Scenario: Deploy validation rejects missing auth configuration
      Tool: Bash
      Steps: Run `scripts/validate-env.sh` (or the deploy wrapper that calls it) with one required OIDC/session variable removed, and redirect output to `.sisyphus/evidence/task-T10-missing-env.txt`
      Expected: Validation exits non-zero and names the missing variable
      Evidence: .sisyphus/evidence/task-T10-missing-env.txt
    
    Scenario: Signed-out frontend health behavior matches updated deployment expectations
      Tool: Bash
      Steps: Run the updated `scripts/healthcheck.sh` or deploy health-check path and save output to `.sisyphus/evidence/task-T10-health-check.txt`
      Expected: Evidence shows a successful probe despite protected app routes (either via accepted redirect or a dedicated public health target)
      Evidence: .sisyphus/evidence/task-T10-health-check.txt
    

    Commit: YES | Message: chore(deploy): wire OIDC runtime configuration | Files: nginx/innercontext.conf, deploy.sh, scripts/validate-env.sh, scripts/healthcheck.sh, systemd/*, docs/DEPLOYMENT.md

  • T11. Add shared auth fixtures, full regression coverage, and CI enforcement

    What to do: Build reusable backend test fixtures for authenticated users, roles, households, and shared inventory, then add regression tests covering auth sync, unauthenticated access, admin/member authorization, household inventory sharing, routine/product visibility, and migration-sensitive ownership behavior. Use dependency overrides in tests instead of hitting a live Authelia server. Enable the existing backend CI job so these tests run in Forgejo, and make sure the final verification command set includes backend tests, lint, frontend check/lint/build, and any required API type generation. Must NOT do: Do not depend on a live Authelia instance in CI, do not leave the backend test job disabled, and do not add a committed frontend browser test suite in this pass.

    Recommended Agent Profile:

    • Category: unspecified-high - Reason: broad regression coverage plus CI wiring across the monorepo
    • Skills: [] - Existing pytest/CI patterns are sufficient
    • Omitted: playwright - Browser QA stays agent-executed, not repository-committed

    Parallelization: Can Parallel: NO | Wave 3 | Blocks: Final verification | Blocked By: T2, T3, T4, T5, T6, T7, T8, T9, T10

    References (executor has NO interview context - be exhaustive):

    • Pattern: backend/tests/conftest.py:16 - Per-test DB isolation and dependency override technique.
    • Pattern: backend/tests/test_products.py:4 - Existing endpoint-test style to mirror for authz coverage.
    • Pattern: .forgejo/workflows/ci.yml:83 - Disabled backend test job that must be enabled.
    • Pattern: frontend/package.json:6 - Final frontend verification commands available in the repo.
    • Pattern: backend/pyproject.toml - Pytest command/config surface for any new test files.

    Acceptance Criteria (agent-executable only):

    • Shared auth fixtures exist for admin/member identities, household membership, and shared inventory setup.
    • Backend tests cover 401, owner success, admin override, same-household shared inventory update, and different-household denial across representative routes.
    • Forgejo backend tests run by default instead of being gated by if: false.
    • Final command set passes: backend tests + lint, frontend check + lint + build, and API type generation only if required by backend schema changes.

    QA Scenarios (MANDATORY - task incomplete without these):

    Scenario: Full backend auth regression suite passes locally
      Tool: Bash
      Steps: Run `cd backend && uv run pytest > ../.sisyphus/evidence/task-T11-backend-regression.txt`
      Expected: Evidence file shows the full suite passing, including new auth/tenancy tests
      Evidence: .sisyphus/evidence/task-T11-backend-regression.txt
    
    Scenario: CI config now runs backend tests instead of skipping them
      Tool: Bash
      Steps: Read `.forgejo/workflows/ci.yml`, confirm the backend-test job no longer contains `if: false`, and save a grep extract to `.sisyphus/evidence/task-T11-ci-enabled.txt`
      Expected: Evidence shows the backend-test job is active and executes `uv run pytest`
      Evidence: .sisyphus/evidence/task-T11-ci-enabled.txt
    

    Commit: YES | Message: test(auth): add multi-user regression coverage | Files: backend/tests/*, .forgejo/workflows/ci.yml

Final Verification Wave (4 parallel agents, ALL must APPROVE)

  • F1. Plan Compliance Audit - oracle
  • F2. Code Quality Review - unspecified-high
  • F3. Real Manual QA - unspecified-high (+ playwright if UI)
  • F4. Scope Fidelity Check - deep

Commit Strategy

  • Use atomic commits after stable checkpoints: Wave 1 foundation, Wave 2 application integration, Wave 3 infra/tests.
  • Prefer conventional commits with monorepo scopes such as feat(auth): ..., feat(frontend): ..., feat(api): ..., test(auth): ..., chore(deploy): ....
  • Do not merge unrelated refactors into auth/tenancy commits; keep schema, auth flow, frontend session, and infra/test changes reviewable.

Success Criteria

  • Every protected route and API request resolves a concrete current user before touching owned data.
  • Non-admin users cannot read or mutate records outside their ownership, except household-shared inventory entries.
  • Household members can view/update shared inventory without gaining product edit rights.
  • Existing single-user data survives migration and becomes accessible to the bootstrap admin account after first login.
  • Frontend protected navigation/login/logout flow works without browser-stored bearer tokens.
  • Backend test suite and CI catch auth regressions before deploy.