# 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. - [x] 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/*` - [x] 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/*` - [x] 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` - [x] 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 - [x] 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 - [x] 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` - [x] 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/*` - [x] 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` - [x] 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 - [x] 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.