- 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
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:
adminandmember; 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;
productsstay 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_membershipsmodel 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 likeget_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
userinfoto 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 pytestcd backend && uv run ruff check .cd frontend && pnpm checkcd frontend && pnpm lintcd frontend && pnpm buildcd 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, andhousehold_membershipstables keyed byissuer + subrather than email. user_idownership 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, andHouseholdMembership; extend existing domain models with ownership fields; add a compact role enum (admin,member) and a household-membership role enum (owner,member). Useissuer + subjectas the immutable OIDC identity key, enforce at most one household membership per user in v1, and addis_household_shared: bool = FalsetoProductInventoryso 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 splitProductinto 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, andupdated_atpattern. - Pattern:
backend/innercontext/models/product.py:353- ExistingProductInventorytable 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/definesUser,Household, andHouseholdMembershipwith 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, andAICallLogeach expose an ownership field (user_id) in model code, withProductInventoryalso exposingis_household_shared.innercontext.modelsre-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']))"printsTrue.
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.txtCommit: YES | Message:
feat(auth): add local user and household models| Files:backend/innercontext/models/* - Category:
-
T2. Add Alembic migration and bootstrap backfill for legacy single-user data
What to do: Create an Alembic revision that creates
users,households, andhousehold_memberships, addsuser_idownership columns and related foreign keys/indexes to all owned tables, and addsis_household_sharedtoproduct_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 namesBOOTSTRAP_ADMIN_OIDC_ISSUER,BOOTSTRAP_ADMIN_OIDC_SUB,BOOTSTRAP_ADMIN_EMAIL,BOOTSTRAP_ADMIN_NAME, andBOOTSTRAP_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_ISSUERorBOOTSTRAP_ADMIN_OIDC_SUBis absent. - Owned tables end with non-null
user_idconstraints 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.txtCommit: YES | Message:
feat(db): backfill tenant ownership for existing records| Files:backend/alembic/versions/*,backend/innercontext/models/* - Category:
-
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()andrequire_admin(). Create protected auth endpoints for session sync and self introspection (for example/auth/session/syncand/auth/me) so SvelteKit can exchange token-derived/userinfo-derived identity details for a localUserrow 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 trustX-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; useissuer + subas 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.txtCommit: YES | Message:
feat(auth): validate Authelia tokens in FastAPI| Files:backend/main.py,backend/innercontext/auth.py,backend/innercontext/api/auth*.py - Category:
-
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 supersedeget_or_404()with helpers that scope byuser_idand return404for unauthorized record lookups unless the route intentionally needs403. Must NOT do: Do not leave routers performing rawsession.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 naiveget_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.txtCommit: YES | Message:
refactor(api): centralize tenant authorization helpers| Files:backend/innercontext/api/utils.py,backend/innercontext/api/authz.py, router call sites - Category:
-
T5. Retrofit products and inventory endpoints for owned access plus household sharing
What to do: Update
productsandinventoryAPIs so product visibility isowned OR household-visible-via-shared-inventory OR admin, while product mutation remainsowner OR admin. KeepProductuser-owned. For household members, allowGETon shared products/inventory rows andPATCHon shared inventory rows, but keepPOST /products,PATCH /products/{id},DELETE /products/{id},POST /products/{id}/inventory, andDELETE /inventory/{id}restricted to owner/admin. Reuse the existingProductListItem.is_ownedfield 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 editpersonal_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 exposesis_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
GETshared products/inventory andPATCHshared inventory rows, but cannot mutate product records or create/delete another user's inventory rows. - Product summaries preserve
is_ownedsemantics 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.txtCommit: 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 - Category:
-
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.
UserProfilebecomes 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 preserveuser_idon products and logs, and that list endpoints never aggregate cross-user data for non-admins. Must NOT do: Do not keep anyselect(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 usingget_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_idbefore returning or mutating rows. GET /profileandPATCH /profileoperate 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_idwhen 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.txtCommit: 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 - Category:
-
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 implementAuthorization Code + PKCEin SvelteKit using Authelia discovery/token/userinfo endpoints. Create/auth/login,/auth/callback, and/auth/logoutserver routes. Extendhooks.server.tsto decrypt/load the app session, refresh the access token when it is near expiry, populateevent.locals.userandevent.locals.session, and redirect unauthenticated requests on all application routes except/auth/*and static assets. Use an encrypted HTTP-only cookie namedinnercontext_sessionwithsameSite=lax,securein production, and a 32-byte secret from private env. Must NOT do: Do not store access or refresh tokens inlocalStorage, 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 typedApp.Locals/PageDatasession 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 usingfetch. - 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.tspopulatesevent.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.mdCommit: 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/* - Category:
-
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/apiusage to server actions or same-origin SvelteKit endpoints, add a+layout.server.tsthat exposes authenticated user data to the shell, and update+layout.svelteto show the current user role/name plus a logout action. Regenerate OpenAPI types if backend response models change and keep$lib/typesas 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.svelteand 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 anypnpm generate:apirun.
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/apiusage 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:apiis 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.txtCommit: 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.sveltefiles,frontend/src/lib/types.ts - Category:
-
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
userstable. - 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.txtCommit: YES | Message:
feat(api): add admin household management endpoints| Files:backend/main.py,backend/innercontext/api/admin*.py, related tests - Category:
-
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. Extendscripts/validate-env.shand deploy validation so missing auth env vars fail fast, and updatescripts/healthcheck.shplusdeploy.shhealth expectations because authenticated pages may now redirect to login instead of returning200for 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 return200when 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/returns200. - 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.txtCommit: 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 - Category:
-
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.txtCommit: YES | Message:
test(auth): add multi-user regression coverage| Files:backend/tests/*,.forgejo/workflows/ci.yml - Category:
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.