Rename repo to Heimdall

This commit is contained in:
Meta Vens 2026-04-26 11:41:27 +01:00
commit 17096e85b1
12 changed files with 296 additions and 179 deletions

View file

@ -1,18 +1,19 @@
# GameCult Access Instructions # Heimdall Instructions
## Project Purpose ## Project Purpose
GameCult Access exists to hold the shared auth and access-control architecture Heimdall exists to hold the shared auth authority design for GameCult-hosted
for GameCult-hosted experiments without letting that shared layer metastasize experiments without letting that shared layer metastasize into a giant
into a giant mixed-data pit. mixed-data pit.
The planned reusable backbone is: The planned shared authority owns:
- provider OAuth and linking - provider OAuth and linking
- signed local sessions - signed session / claim issuance
- entitlement refresh - entitlement refresh
- capability evaluation - capability evaluation
- app-facing access claims - app-facing access claims
- audit and grant surfaces
The planned non-shared-by-default layer is: The planned non-shared-by-default layer is:
@ -21,6 +22,7 @@ The planned non-shared-by-default layer is:
- creator data - creator data
- queue/job payloads - queue/job payloads
- product-specific state - product-specific state
- app-local resource ownership checks
## Canonical State ## Canonical State
@ -38,35 +40,35 @@ The planned non-shared-by-default layer is:
## Important Paths ## Important Paths
- Project root: `E:\Projects\gamecult-access` - Project root: `E:\Projects\Heimdall`
- Shared architecture: `E:\Projects\gamecult-access\docs\architecture.md` - Shared architecture: `E:\Projects\Heimdall\docs\architecture.md`
- Implementation plan: `E:\Projects\gamecult-access\docs\implementation-plan.md` - Implementation plan: `E:\Projects\Heimdall\docs\implementation-plan.md`
- Repixelizer profile: `E:\Projects\gamecult-access\docs\app-profiles\repixelizer.md` - Repixelizer profile: `E:\Projects\Heimdall\docs\app-profiles\repixelizer.md`
- StreamPixels profile: `E:\Projects\gamecult-access\docs\app-profiles\streampixels.md` - StreamPixels profile: `E:\Projects\Heimdall\docs\app-profiles\streampixels.md`
- Handoff summary: `E:\Projects\gamecult-access\notes\fresh-workspace-handoff.md` - Handoff summary: `E:\Projects\Heimdall\notes\fresh-workspace-handoff.md`
- State CLI: `E:\Projects\gamecult-access\tools\gamecult_access_state.py` - State CLI: `E:\Projects\Heimdall\tools\heimdall_state.py`
- Pre-compaction helper: `E:\Projects\gamecult-access\tools\gamecult_access_prepare_compaction.py` - Pre-compaction helper: `E:\Projects\Heimdall\tools\heimdall_prepare_compaction.py`
## Useful Commands ## Useful Commands
This repo does not carry a dedicated virtualenv yet. Use any working Python This repo does not carry a dedicated virtualenv yet. Use any working Python
3.11+ interpreter. On this workstation, `E:\Projects\repixelizer\.venv\Scripts\python.exe` 3.11+ interpreter. On this workstation,
is known-good. `E:\Projects\repixelizer\.venv\Scripts\python.exe` is known-good.
Preferred here: Preferred here:
```powershell ```powershell
E:\Projects\repixelizer\.venv\Scripts\python.exe .\tools\gamecult_access_state.py status E:\Projects\repixelizer\.venv\Scripts\python.exe .\tools\heimdall_state.py status
E:\Projects\repixelizer\.venv\Scripts\python.exe .\tools\gamecult_access_state.py add-evidence --type design --status accepted --note "..." E:\Projects\repixelizer\.venv\Scripts\python.exe .\tools\heimdall_state.py add-evidence --type design --status accepted --note "..."
E:\Projects\repixelizer\.venv\Scripts\python.exe .\tools\gamecult_access_prepare_compaction.py E:\Projects\repixelizer\.venv\Scripts\python.exe .\tools\heimdall_prepare_compaction.py
``` ```
Fallback if a normal `python` command exists: Fallback if a normal `python` command exists:
```powershell ```powershell
python .\tools\gamecult_access_state.py status python .\tools\heimdall_state.py status
python .\tools\gamecult_access_state.py add-evidence --type design --status accepted --note "..." python .\tools\heimdall_state.py add-evidence --type design --status accepted --note "..."
python .\tools\gamecult_access_prepare_compaction.py python .\tools\heimdall_prepare_compaction.py
``` ```
## Session Bootstrap And Re-entry Protocol ## Session Bootstrap And Re-entry Protocol
@ -79,21 +81,21 @@ On fresh session load:
- `docs/architecture.md` - `docs/architecture.md`
- `docs/implementation-plan.md` - `docs/implementation-plan.md`
2. run: 2. run:
- `python .\tools\gamecult_access_state.py status` - `python .\tools\heimdall_state.py status`
- `git status --short --branch` - `git status --short --branch`
- `git log --oneline -5` - `git log --oneline -5`
3. restate the current next action from persisted state before editing 3. restate the current next action from persisted state before editing
After compaction or suspicious continuity loss: After compaction or suspicious continuity loss:
1. rerun `gamecult_access_state.py status` 1. rerun `heimdall_state.py status`
2. reread `state/map.yaml` and `notes/fresh-workspace-handoff.md` 2. reread `state/map.yaml` and `notes/fresh-workspace-handoff.md`
3. treat the persisted next action as authoritative unless fresh evidence 3. treat the persisted next action as authoritative unless fresh evidence
contradicts it contradicts it
When the user says to prepare for imminent compaction: When the user says to prepare for imminent compaction:
1. run `tools/gamecult_access_prepare_compaction.py` 1. run `tools/heimdall_prepare_compaction.py`
2. update only the state that actually changed 2. update only the state that actually changed
3. rerun the helper 3. rerun the helper
4. commit the completed persistence pass unless the work is deliberately 4. commit the completed persistence pass unless the work is deliberately
@ -102,10 +104,14 @@ When the user says to prepare for imminent compaction:
## Operating Discipline ## Operating Discipline
- Before substantial edits, restate the current mechanism and intended change. - Before substantial edits, restate the current mechanism and intended change.
- Keep the shared backbone separate from app-domain data unless there is a very - Keep identity authority central and app-domain data local unless there is a
deliberate, source-grounded reason to share something. very deliberate, source-grounded reason to share something.
- Prefer embedded shared-package mode before inventing a dedicated auth service. - Prefer the Yggdrasil-hosted Heimdall service over duplicated per-app provider
- Do not let app profiles turn into duplicate copies of the shared architecture. stacks.
- Do not require a Heimdall round-trip for every guarded app route; host apps
should verify signed claims locally and keep resource ownership checks local.
- Do not let app profiles turn into duplicate copies of the shared
architecture.
- If the diff grows while the boundary gets blurrier, stop and simplify. - If the diff grows while the boundary gets blurrier, stop and simplify.
- Before handoff or compaction, sync `state/map.yaml`, refresh - Before handoff or compaction, sync `state/map.yaml`, refresh
`notes/fresh-workspace-handoff.md`, add distilled evidence when future belief `notes/fresh-workspace-handoff.md`, add distilled evidence when future belief

View file

@ -1,17 +1,20 @@
# GameCult Access # Heimdall
GameCult Access is the planned shared auth and access-control backbone for Heimdall is the planned shared auth authority for GameCult-hosted experiments.
GameCult-hosted experiments.
The point is painfully simple: The point is painfully simple:
- stop rebuilding Discord/Patreon login glue in every app - stop rebuilding Discord/Patreon login glue in every app
- keep signed local sessions, linked identities, and capability checks reusable - keep provider OAuth, linked identities, grants, entitlements, and session
issuance in one place
- let app runtimes verify signed claims locally so "shared auth" does not
become "everything phones home for every request"
- keep app-domain data separate so "shared auth" does not become "shared swamp" - keep app-domain data separate so "shared auth" does not become "shared swamp"
## Current Shape ## Current Shape
This repo is planning and extraction work, not a landed auth service yet. This repo is still planning and service-contract work, not a landed auth
service yet.
Canonical docs: Canonical docs:
@ -25,7 +28,9 @@ Canonical docs:
## Working Loop ## Working Loop
1. Rehydrate from canonical state. 1. Rehydrate from canonical state.
2. Keep the shared backbone separate from app-domain data. 2. Keep identity authority central and app-domain data local.
3. Prefer embedded shared-package mode before inventing a central auth service. 3. Prefer a Yggdrasil-hosted shared service over duplicated per-app provider
4. Move only one organ at a time. stacks.
4. Let app backends verify signed claims locally instead of begging Heimdall on
every guarded route.
5. Commit finished passes before the notes start breeding. 5. Commit finished passes before the notes start breeding.

View file

@ -2,7 +2,8 @@
## What this file is ## What this file is
This file binds Repixelizer onto the shared design in `docs/architecture.md`. This file binds Repixelizer onto the shared Heimdall design in
`docs/architecture.md`.
It is future design, not a claim that the hosted demo already has auth. It is future design, not a claim that the hosted demo already has auth.
@ -89,8 +90,20 @@ Important invariant:
- job ownership resolves from the local session/account, not from Discord or - job ownership resolves from the local session/account, not from Discord or
Patreon ids Patreon ids
Heimdall should be involved in:
- login / link entry
- session and claim issuance
- entitlement refresh
Repixelizer should still do locally:
- claim verification on guarded routes
- ownership checks on job reads, events, heartbeat, and cancel
- queue/runtime behavior
## Config mapping ## Config mapping
- keep runtime/queue/limit env vars under `REPIXELIZER_*` - keep runtime/queue/limit env vars under `REPIXELIZER_*`
- move shared access machinery under `GC_ACCESS_*` - move Heimdall integration under `GC_ACCESS_*`
- keep per-app role/tier/capability bindings small and app-scoped - keep per-app role/tier/capability bindings small and app-scoped

View file

@ -2,7 +2,7 @@
## What this file is ## What this file is
This file records how StreamPixels should fit onto the shared access backbone in This file records how StreamPixels should fit onto Heimdall in
`docs/architecture.md` without turning streamer-audience data into shared auth `docs/architecture.md` without turning streamer-audience data into shared auth
sludge. sludge.
@ -27,6 +27,13 @@ That is already compatible with the shared backbone idea.
- creator/operator capability evaluation - creator/operator capability evaluation
- grant/invite primitives where they fit - grant/invite primitives where they fit
In the service-first Heimdall version, the most likely reusable slice is:
- provider OAuth authority
- identity-linking authority
- entitlement refresh
- shared claim semantics
## What StreamPixels should keep separate ## What StreamPixels should keep separate
- viewer audience profiles - viewer audience profiles
@ -37,7 +44,7 @@ That is already compatible with the shared backbone idea.
In plain language: In plain language:
- the same backbone can authenticate and authorize the control plane - Heimdall can authenticate and authorize the control plane
- StreamPixels audience data stays StreamPixels data - StreamPixels audience data stays StreamPixels data
## Capability direction ## Capability direction

View file

@ -1,13 +1,16 @@
# GameCult Hosted Access Architecture # Heimdall Architecture
## What this file is ## What this file is
This file is the shared architecture note for GameCult-hosted experiments that This file is the shared architecture note for Heimdall, the planned auth
need the same basic promise: authority service for GameCult-hosted experiments.
Its basic promise is:
- let people sign in with Discord and/or Patreon - let people sign in with Discord and/or Patreon
- decide access from GameCult/community entitlements - decide access from GameCult/community entitlements and local grants
- keep app runtimes tied to local sessions and local accounts, not provider ids - issue signed local claims that hosted apps can verify without trusting raw
provider tokens
It is not a map of landed auth code yet. It is not a map of landed auth code yet.
@ -19,30 +22,32 @@ If every experiment rebuilds:
- identity linking - identity linking
- entitlement refresh - entitlement refresh
- session cookies - session cookies
- queue/job ownership - grant handling
- "Discord role OR Patreon tier" logic - "Discord role OR Patreon tier" logic
then we get five near-identical auth pits and deserve what happens next. then we get five near-identical auth pits and deserve what happens next.
The reusable move is to build one GameCult hosted-access layer and let each app The reusable move is to build one Heimdall authority service and let each app
bind its own policy and runtime seams onto it. bind its own policy and runtime seams onto it.
## Design goals ## Design goals
- one reusable local account/session model across hosted experiments - one reusable local account/session model across hosted experiments
- provider login and linking owned by one shared access layer - provider login and linking owned by one shared authority
- per-app capability policies instead of hard-coded one-off gate checks - per-app capability policies instead of hard-coded one-off gate checks
- host runtimes consume signed local session claims, not raw provider tokens - host runtimes consume signed local claims, not raw provider tokens
- routine route auth stays local to the host app after claim verification
- keep app-domain data separate unless there is a deliberate reason to share it - keep app-domain data separate unless there is a deliberate reason to share it
- start as an embedded shared package before pretending we need a central auth - fit the current GameCult topology: Heimdall on Yggdrasil behind nginx, with
service hosted apps either on the same host or verifying over normal HTTPS/JWKS
## Non-goals ## Non-goals
- becoming a general public identity provider - becoming a general public identity provider
- building multi-tenant SaaS auth machinery - building multi-tenant SaaS auth machinery
- inventing global billing analytics because we got bored - inventing global billing analytics because we got bored
- forcing cross-app single sign-on on day one before a second app exists - forcing app-domain data into a shared auth store
- requiring every guarded route in every app to call Heimdall synchronously
## Core concepts ## Core concepts
@ -61,6 +66,8 @@ bind its own policy and runtime seams onto it.
policy rules policy rules
- `Session` - `Session`
signed local app/browser session; the browser gets this, not provider tokens signed local app/browser session; the browser gets this, not provider tokens
- `AccessClaim`
signed app-facing claim set derived from the account, grants, and app policy
- `AuditEvent` - `AuditEvent`
durable record for login, link, unlink, refresh, denial, and admin overrides durable record for login, link, unlink, refresh, denial, and admin overrides
@ -69,20 +76,20 @@ bind its own policy and runtime seams onto it.
```mermaid ```mermaid
flowchart TD flowchart TD
A["Browser / app web UI"] --> B["Hosted experiment web surface"] A["Browser / app web UI"] --> B["Hosted experiment web surface"]
B --> C["GameCult access layer"] B -->|start auth / link| C["Heimdall"]
C --> D["Discord OAuth + guild role checks"] C --> D["Discord OAuth + guild role checks"]
C --> E["Patreon OAuth + campaign/tier checks"] C --> E["Patreon OAuth + campaign/tier checks"]
C --> F["Local account/session/grant store"] C --> F["Local auth/control-plane store"]
C --> G["Signed access claims"] C --> G["Signed sessions / claims"]
G --> H["Hosted experiment runtime"] B -->|verify claims locally| H["Hosted experiment runtime"]
``` ```
The important split: The important split:
- the access layer owns provider OAuth, identity linking, entitlement refresh, - Heimdall owns provider OAuth, identity linking, entitlement refresh, signed
and signed local sessions sessions, claims, grants, and audit surfaces
- the host experiment owns its workload, queue, editor, render path, or other - the host experiment owns its workload, queue, editor, render path, viewer
product-specific machinery profile model, creator model, or other product-specific machinery
Do not smear provider logic directly through every host runtime. Do not smear provider logic directly through every host runtime.
@ -94,10 +101,11 @@ pour their user data into one communal bucket.
The reusable thing is: The reusable thing is:
- provider OAuth plumbing - provider OAuth plumbing
- signed local session mechanics - signed session and claim mechanics
- linked-identity primitives - linked-identity primitives
- entitlement refresh - entitlement refresh
- capability evaluation - capability evaluation
- local grant and audit surfaces
The non-reusable-by-default thing is: The non-reusable-by-default thing is:
@ -109,12 +117,12 @@ The non-reusable-by-default thing is:
Recommended default: Recommended default:
- shared auth/access **code** - shared auth/access **authority**
- separate app databases or at least separate app-owned schemas/tables for - separate app databases or at least separate app-owned schemas/tables for
domain data domain data
- if a shared access store exists at all, keep it limited to auth/control-plane - if a shared Heimdall store exists at all, keep it limited to auth/control
truth such as identities, grants, entitlement snapshots, sessions, and audit plane truth such as identities, grants, entitlement snapshots, sessions,
events claims metadata, and audit events
Do not let "same auth backbone" quietly mutate into "same user-data swamp." Do not let "same auth backbone" quietly mutate into "same user-data swamp."
@ -182,8 +190,8 @@ Important invariants:
- one provider identity belongs to one local account only - one provider identity belongs to one local account only
- provider tokens stay server-side only - provider tokens stay server-side only
- session cookies represent local sessions, not provider trust directly - session cookies represent local sessions, not provider trust directly
- app-specific capability checks read local claims, not Discord or Patreon on - app-specific capability checks read signed local claims, not Discord or
every route Patreon on every route
## Provider adapters ## Provider adapters
@ -203,7 +211,7 @@ Shared provider/gate adapters should be boring and reusable:
## Policy model ## Policy model
The reusable core should not hard-code one app's access rules. It should The shared authority should not hard-code one app's access rules. It should
evaluate capabilities against an app profile. evaluate capabilities against an app profile.
Suggested app-profile shape: Suggested app-profile shape:
@ -227,18 +235,41 @@ queue_submit = app_access
admin_access = grant.operator || grant.admin_access admin_access = grant.operator || grant.admin_access
``` ```
## Request path discipline
This is where shared authority stays useful instead of becoming a nuisance.
Heimdall should be in the loop for:
- OAuth start and callback flows
- explicit identity linking and unlinking
- session issuance and refresh
- entitlement refresh
- grant administration
- occasional explicit introspection or revocation checks
Host apps should stay in the loop for:
- routine route authorization after claim verification
- resource ownership checks
- domain-specific permission decisions that depend on app-local data
Normal guarded routes should **not** need to round-trip to Heimdall every time.
## Auth and linking flow ## Auth and linking flow
The generic flow should be the same across hosted experiments: The generic flow should be the same across hosted experiments:
1. user starts OAuth from the host app UI 1. user starts OAuth from the host app UI
2. shared access layer signs OAuth `state` 2. host app redirects to Heimdall or calls Heimdall to begin the flow
3. provider callback resolves the provider identity 3. Heimdall signs OAuth `state`
4. shared layer finds or creates the local account 4. provider callback resolves the provider identity through Heimdall
5. if the user was already signed in locally, the identity may link onto the 5. Heimdall finds or creates the local account
6. if the user was already signed in locally, the identity may link onto the
existing account instead of creating a new one existing account instead of creating a new one
6. shared layer refreshes entitlements and capability claims 7. Heimdall refreshes entitlements and capability claims
7. shared layer issues or updates the local session cookie 8. Heimdall issues or updates the local session / claim artifacts consumed by
the host app
The browser should never be the custodian of provider tokens. The browser should never be the custodian of provider tokens.
@ -248,6 +279,7 @@ Every hosted experiment should integrate through the same seams:
- define an `app_slug` - define an `app_slug`
- define the app capability profile - define the app capability profile
- verify Heimdall-issued claims locally
- mark which routes are public, authenticated, or capability-gated - mark which routes are public, authenticated, or capability-gated
- attach `account_id`, `session_id`, and `access_revision` to owned resources - attach `account_id`, `session_id`, and `access_revision` to owned resources
such as jobs, drafts, uploads, or queue entries such as jobs, drafts, uploads, or queue entries
@ -260,50 +292,64 @@ If an app has a queue or job system:
- active-job cancellation policy is app-specific, but the recommended first cut - active-job cancellation policy is app-specific, but the recommended first cut
is still "let the active job finish, block new work" is still "let the active job finish, block new work"
## Deployment topology
Current intended first deployment shape:
- Heimdall runs on `yggdrasil.gamecult.org`
- Heimdall binds a localhost port behind nginx
- public auth hostname is expected to become `heimdall.gamecult.org`
- same-host app stacks such as StreamPixels and Repixelizer call Heimdall over
localhost or normal private routing when they need auth-authority work done
- app backends verify signed claims locally for routine guarded requests
This matches the host pattern already used by other GameCult app workloads.
## Deployment modes ## Deployment modes
### Mode 1: Embedded shared package ### Mode 1: Same-host shared access service
Recommended first cut. Recommended first cut.
- one shared `gamecult_access` package/module - one Heimdall service on Yggdrasil
- each hosted experiment embeds it - each hosted experiment keeps its own runtime and route integration
- each app keeps its own runtime and route integration
- sessions may still be app-local
- each app keeps its own domain-data store - each app keeps its own domain-data store
- apps verify signed claims locally after issuance
Why this is the right first cut: Why this is the right first cut:
- reusable without forcing a separate always-on dependency - fits the current host topology
- small blast radius - avoids cross-runtime shared-library nonsense
- easy to evolve while only one or two apps use it - keeps OAuth and provider sludge in one place
- keeps routine guarded requests local inside the app
### Mode 2: Same-host shared access service ### Mode 2: Multi-host shared access service
Use this when multiple apps on the same server want one shared access store and Use this when multiple app stacks across hosts still want one shared auth
session issuer, but full centralization still feels premature. authority, but the same claim-verification discipline continues.
### Mode 3: Dedicated GameCult access service ### Mode 3: Wider cross-app session platform
Only do this when multiple independently deployed apps genuinely need: Only do this when independently deployed apps genuinely need:
- shared cross-app sessions - shared cross-app sessions
- central grant administration - central grant administration
- one callback origin for all providers - one callback origin for all providers
- enough auth churn that duplicated embedded deployments become the bigger pain - enough auth churn that Heimdall is clearly a long-lived platform surface
Do not build this first because it sounds important. Do not build this first because it sounds important.
Even in this mode, the dedicated service should own auth/control-plane data, not Even in this mode, Heimdall should own auth/control-plane data, not absorb
absorb every app's audience or product data. every app's audience or product data.
## Configuration split ## Configuration split
Suggested generic env surface: Suggested generic env surface:
- `GC_ACCESS_ENABLED=1` - `GC_ACCESS_ENABLED=1`
- `GC_ACCESS_BASE_URL=https://heimdall.gamecult.org`
- `GC_ACCESS_INTERNAL_URL=http://127.0.0.1:4100`
- `GC_ACCESS_SESSION_SECRET=...` - `GC_ACCESS_SESSION_SECRET=...`
- `GC_ACCESS_BASE_URL=https://access.gamecult.org` or host-app local equivalent
- `GC_ACCESS_DISCORD_CLIENT_ID=...` - `GC_ACCESS_DISCORD_CLIENT_ID=...`
- `GC_ACCESS_DISCORD_CLIENT_SECRET=...` - `GC_ACCESS_DISCORD_CLIENT_SECRET=...`
- `GC_ACCESS_DISCORD_BOT_TOKEN=...` - `GC_ACCESS_DISCORD_BOT_TOKEN=...`
@ -321,10 +367,12 @@ env namespaces.
The correct reusable seam is: The correct reusable seam is:
- shared provider/session/entitlement/capability machinery - one shared identity authority service
- local verification of signed claims by each host app
- thin per-app access profiles and runtime bindings - thin per-app access profiles and runtime bindings
The incorrect reusable seam is: The incorrect reusable seam is:
- cloning one app’s auth blob into every future experiment - cloning one app's auth blob into every future experiment
- or turning shared auth into shared domain-data soup - or turning shared auth into shared domain-data soup
- or forcing every guarded app route to make a Heimdall network hop

View file

@ -1,8 +1,8 @@
# GameCult Access Implementation Plan # Heimdall Implementation Plan
## What this file is ## What this file is
This file is the forward plan and active hypothesis ledger for GameCult Access. This file is the forward plan and active hypothesis ledger for Heimdall.
It is not the canonical shared architecture note. That lives in It is not the canonical shared architecture note. That lives in
`docs/architecture.md`. `docs/architecture.md`.
@ -16,86 +16,112 @@ There is no landed auth machine here yet.
What exists right now is: What exists right now is:
- the extracted shared architecture note - the shared architecture note
- app-binding notes for Repixelizer and StreamPixels - app-binding notes for Repixelizer and StreamPixels
- explicit state/doctrine scaffolding so the plan does not immediately turn into - explicit state/doctrine scaffolding so the plan does not immediately turn
soup into soup
## Primary objective ## Primary objective
Build a reusable `gamecult_access` layer that can: Build Heimdall as a shared auth authority service that can:
- own provider OAuth and identity linking - own provider OAuth and identity linking
- issue signed local sessions - issue signed sessions and app-facing claims
- refresh Discord and Patreon entitlements - refresh Discord and Patreon entitlements
- evaluate per-app capability rules - evaluate per-app capability rules
- stay separate from app-domain data - expose a clean contract to app backends without owning their domain data
## Current priorities ## Current priorities
### 1. Lock the shared boundary ### 1. Lock the service boundary
Goals: Goals:
- keep the shared layer scoped to auth/control-plane machinery - keep Heimdall scoped to auth/control-plane machinery
- keep app-domain data in app-owned stores - keep app-domain data in app-owned stores
- keep app profiles thin and explicit - keep app profiles thin and explicit
### 2. Prefer embedded-package mode first ### 2. Define the contract before the service grows teeth
Goals: Goals:
- define the reusable package/module seam - define signed claim/session shapes
- avoid a dedicated shared service until a second real app proves the need - define OAuth start/callback/link surfaces
- keep initial blast radius small - define grant and entitlement persistence boundaries
- define how app backends verify claims locally
### 3. Make Repixelizer the first binding ### 3. Make Repixelizer the first auth-blank consumer
Goals: Goals:
- gate hosted access and queue submission through local sessions - gate hosted access and queue submission through Heimdall-issued claims
- attach queue ownership to `account_id` and `session_id` - attach queue ownership to `account_id` and `session_id`
- preserve the existing hosted GUI and queue runtime - preserve the existing hosted GUI and queue runtime
### 4. Keep StreamPixels separate where it matters ### 4. Migrate StreamPixels without flattening its good local seams
Goals: Goals:
- let StreamPixels reuse auth/control-plane machinery - move provider/linking authority toward Heimdall
- keep viewer profiles, creator memberships, connector creds, and audience data - keep viewer profiles, creator memberships, connector creds, and audience data
in StreamPixels in StreamPixels
- keep creator/operator route authorization local to StreamPixels after claim
verification
### 5. Capture the Yggdrasil deployment shape early
Goals:
- treat Heimdall as a localhost service behind nginx on Yggdrasil
- support same-host callers first
- avoid per-request network chatter for routine app authorization
## Proposed phases ## Proposed phases
### Phase 1: Shared package scaffold ### Phase 1: Contract and schema scaffold
- create package/module boundaries for: - define:
- provider adapters - signed claim/session payloads
- signed session issuance/verification - JWKS or equivalent verification surface
- linked identity persistence interfaces - OAuth `state` and callback contracts
- entitlement refresh/evaluation - grant / entitlement / audit record shapes
- capability rule evaluation - app profile / capability rule inputs and outputs
### Phase 2: Repixelizer binding ### Phase 2: Heimdall service skeleton
- integrate the shared layer into the hosted demo - create the first real service layout for:
- provider OAuth start/callback routes
- account / linked-identity persistence
- session issuance and refresh
- capability evaluation
- audit logging
### Phase 3: Repixelizer binding
- integrate Heimdall into the hosted demo
- gate `POST /api/jobs` - gate `POST /api/jobs`
- gate per-job read/cancel/event routes by local session ownership - gate per-job read/cancel/event routes by local session ownership
### Phase 3: StreamPixels control-plane fit check ### Phase 4: StreamPixels authority migration
- map the shared access layer onto StreamPixels creator/operator auth seams - replace provider/linking authority paths with Heimdall calls where they truly
- explicitly reject audience-data centralization belong
- keep viewer/session expansion, creator memberships, and operator route
enforcement local where they are app-domain concerns
### Phase 4: Shared-store decision ### Phase 5: Ops and deploy
- only after two real bindings exist, decide whether auth/control-plane state - define nginx/systemd/env/runbook surfaces for Heimdall on Yggdrasil
should stay embedded or move into a shared service/store - keep callers and state stores localhost-first where practical
## Guardrails ## Guardrails
- do not let auth reuse become data mixing - do not let auth reuse become data mixing
- do not build a central service first just because it sounds grand - do not turn Heimdall into a mandatory round-trip for every guarded app route
- do not fantasize about one shared cross-runtime embedded library; the apps can
share contracts without sharing a runtime
- do not duplicate the shared architecture note into app profiles - do not duplicate the shared architecture note into app profiles
- do not rip out app-local ownership and authorization logic that still belongs
in the app
- keep one clear hypothesis per pass - keep one clear hypothesis per pass
- commit completed work before the planning notes start breeding - commit completed work before the planning notes start breeding

View file

@ -1,6 +1,6 @@
# Fresh Workspace Handoff # Fresh Workspace Handoff
This is the re-entry packet for `E:\Projects\gamecult-access`. This is the re-entry packet for `E:\Projects\Heimdall`.
It is intentionally short. Historical proof belongs in git history and the It is intentionally short. Historical proof belongs in git history and the
distilled `state/evidence.jsonl` ledger; the shared architecture belongs in distilled `state/evidence.jsonl` ledger; the shared architecture belongs in
@ -13,7 +13,7 @@ From the repo root:
```powershell ```powershell
# This repo has no dedicated venv yet. Use any working Python 3.11+. # This repo has no dedicated venv yet. Use any working Python 3.11+.
E:\Projects\repixelizer\.venv\Scripts\python.exe .\tools\gamecult_access_state.py status E:\Projects\repixelizer\.venv\Scripts\python.exe .\tools\heimdall_state.py status
Get-Content '.\state\map.yaml' Get-Content '.\state\map.yaml'
Get-Content '.\notes\fresh-workspace-handoff.md' Get-Content '.\notes\fresh-workspace-handoff.md'
Get-Content '.\docs\architecture.md' Get-Content '.\docs\architecture.md'
@ -27,23 +27,29 @@ Do not trust this file for the exact live HEAD. Always check git.
## Current Orientation ## Current Orientation
- this repo is planning and extraction work, not a landed auth service yet - this repo is still planning and service-contract work, not a landed auth
service yet
- the canonical shared architecture lives in `docs/architecture.md` - the canonical shared architecture lives in `docs/architecture.md`
- app profiles live under `docs/app-profiles/` - app profiles live under `docs/app-profiles/`
- the key boundary is explicit: - the key boundary is explicit:
- shared auth/control-plane mechanics are reusable - Heimdall owns OAuth, linking, grants, entitlement refresh, and signed claim
issuance
- app-domain data stays app-owned by default - app-domain data stays app-owned by default
- Repixelizer is the first binding target - host apps verify signed claims locally for routine auth instead of calling
- StreamPixels is the boundary case that proves backbone reuse must not imply Heimdall on every request
audience-data centralization - Repixelizer is the first auth-blank binding target
- the recommended first implementation mode is an embedded shared package, not a - StreamPixels is the migration target with useful existing auth seams that
dedicated auth service should not be flattened into mush
- the intended first deployment shape is a Heimdall service on Yggdrasil behind
nginx, not an embedded cross-runtime shared library fantasy
## Critical Doctrine ## Critical Doctrine
- Persistent state is the agent's mind. - Persistent state is the agent's mind.
- Cut stale architecture notes as ruthlessly as stale code. - Cut stale architecture notes as ruthlessly as stale code.
- Do not mistake "shared auth" for "shared database." - Do not mistake "shared auth" for "shared database."
- Do not mistake "shared auth service" for "every guarded route must call the
mothership."
- If compaction hits before boundary findings are persisted, that work is gone. - If compaction hits before boundary findings are persisted, that work is gone.
Re-gather it instead of pretending continuity happened. Re-gather it instead of pretending continuity happened.
@ -51,16 +57,17 @@ Do not trust this file for the exact live HEAD. Always check git.
Do not continue implementation automatically from a rehydrate-only request. Do not continue implementation automatically from a rehydrate-only request.
If the user asks to continue, the current next move is to define the embedded If the user asks to continue, the current next move is to define the first
`gamecult_access` package seam: Heimdall service contract and skeleton:
- storage interfaces - OAuth start/callback surfaces
- provider adapter interfaces - signed session / claim shape
- signed-session claims - claim verification contract for app backends
- capability rule evaluation boundaries - entitlement/grant persistence boundaries
- Repixelizer and StreamPixels integration seams
without central-service theatrics and without smearing app-domain data into the without dragging app-domain data into the shared layer and without forcing
shared layer. per-request auth round-trips.
## Immediate Re-entry Instruction ## Immediate Re-entry Instruction

View file

@ -2,7 +2,7 @@
"branches": [ "branches": [
{ {
"id": "main", "id": "main",
"hypothesis": "A shared GameCult access backbone can be reused across hosted experiments if OAuth/session/capability machinery is centralized while app-domain data stays app-owned.", "hypothesis": "A shared Heimdall authority service on Yggdrasil can be reused across hosted experiments if OAuth, sessions, grants, and capability machinery are centralized while app-domain data and routine route authorization stay app-local.",
"status": "active", "status": "active",
"artifacts": [ "artifacts": [
"state/map.yaml", "state/map.yaml",
@ -11,7 +11,7 @@
"docs/architecture.md", "docs/architecture.md",
"docs/implementation-plan.md" "docs/implementation-plan.md"
], ],
"notes": "Current mainline. Repixelizer is the first binding target; StreamPixels is the boundary stress test." "notes": "Current mainline. Repixelizer is the first auth-blank binding target; StreamPixels is the migration and boundary stress test."
} }
] ]
} }

View file

@ -1,3 +1,4 @@
{"ts":"2026-04-26T00:00:00Z","type":"design","status":"accepted","note":"The shared hosted-access plan was extracted from Repixelizer into its own repo so the backbone can evolve separately from app-specific notes."} {"ts":"2026-04-26T00:00:00Z","type":"design","status":"accepted","note":"The shared hosted-access plan was extracted from Repixelizer into its own repo so the backbone can evolve separately from app-specific notes."}
{"ts":"2026-04-26T00:00:00Z","type":"design","status":"accepted","note":"The recommended first implementation mode is an embedded shared gamecult_access package, not a dedicated auth service."} {"ts":"2026-04-26T00:00:00Z","type":"design","status":"accepted","note":"The recommended first implementation mode is an embedded shared gamecult_access package, not a dedicated auth service."}
{"ts":"2026-04-26T00:00:00Z","type":"boundary","status":"accepted","note":"StreamPixels can reuse the same auth/control-plane backbone, but streamer-audience data, creator memberships, connector credentials, and runtime state should remain StreamPixels-owned."} {"ts":"2026-04-26T00:00:00Z","type":"boundary","status":"accepted","note":"StreamPixels can reuse the same auth/control-plane backbone, but streamer-audience data, creator memberships, connector credentials, and runtime state should remain StreamPixels-owned."}
{"ts":"2026-04-26T06:00:00Z","type":"design","status":"accepted","note":"The cross-runtime split and current Yggdrasil topology changed the first implementation shape from an embedded shared package to Heimdall: a shared auth authority service whose signed claims are verified locally by each app."}

View file

@ -1,23 +1,24 @@
project: project:
name: GameCult Access name: Heimdall
status: active status: active
phase: architecture_extraction phase: service_topology_alignment
premise: > premise: >
Build one reusable GameCult-hosted access backbone for OAuth, linked Build Heimdall as one reusable GameCult auth authority service for OAuth,
identities, local sessions, and capability checks without mixing in linked identities, local sessions, grants, and capability claims without
app-domain audience or product data. mixing in app-domain audience or product data.
objective: objective:
primary: > primary: >
Create a reusable shared access layer that Repixelizer, StreamPixels, and Create a shared auth authority service that Repixelizer, StreamPixels, and
future GameCult-hosted experiments can bind onto. future GameCult-hosted experiments can consume.
current: > current: >
Lock the shared architecture, extraction path, and app-boundary rules before Lock the Heimdall service boundary, Yggdrasil deployment shape, and app
implementation starts breeding myths. integration rules before implementation starts breeding myths.
constraints: constraints:
- Shared auth/access code is reusable; app-domain data is separate by default. - Heimdall owns auth/control-plane authority; app-domain data stays app-owned by default.
- Prefer an embedded shared package before a dedicated auth service. - Prefer a same-host Heimdall service on Yggdrasil over duplicated per-app provider stacks.
- Host apps verify signed claims locally for routine guarded routes.
- App profiles stay thin; the shared architecture note owns the main model. - App profiles stay thin; the shared architecture note owns the main model.
- StreamPixels audience and creator data must not be absorbed into a shared auth store. - StreamPixels audience and creator data must not be absorbed into a shared auth store.
@ -25,10 +26,10 @@ invariants:
- The map is canonical and slow-changing. - The map is canonical and slow-changing.
- Scratch is temporary and disposable. - Scratch is temporary and disposable.
- Evidence is durable and distilled; routine activity logs belong in git history and commit messages. - Evidence is durable and distilled; routine activity logs belong in git history and commit messages.
- The shared access layer owns OAuth, local sessions, entitlement refresh, capability evaluation, and audit surfaces. - Heimdall owns OAuth, local sessions, grants, entitlement refresh, capability evaluation, and audit surfaces.
- App-domain data stays with the host app unless there is a deliberate, source-grounded reason to share it. - App-domain data stays with the host app unless there is a deliberate, source-grounded reason to share it.
- Embedded shared-package mode is the default first implementation strategy. - Host apps verify signed claims locally and keep resource ownership checks local.
- StreamPixels can reuse control-plane auth mechanics without centralizing streamer-audience data. - StreamPixels can reuse shared auth authority without centralizing streamer-audience data.
- Persistent state cruft is the same failure class as architecture cruft. - Persistent state cruft is the same failure class as architecture cruft.
data_flow: data_flow:
@ -37,45 +38,48 @@ data_flow:
- shared architecture note - shared architecture note
- app profile bindings - app profile bindings
- current app/runtime seams in Repixelizer and StreamPixels - current app/runtime seams in Repixelizer and StreamPixels
- Yggdrasil host topology and nginx/service shape
process: process:
- rehydrate from canonical state - rehydrate from canonical state
- choose one bounded architecture or implementation slice - choose one bounded architecture or implementation slice
- keep shared code and app-domain data boundaries explicit - keep auth authority and app-domain data boundaries explicit
- keep routine route auth local after claim verification
- record only belief-changing evidence - record only belief-changing evidence
- commit or backtrack - commit or backtrack
cut: cut:
- reject shared-data creep - reject shared-data creep
- reject duplicated auth blobs per app - reject duplicated auth blobs per app
- reject central-service theatrics before two real bindings justify them - reject per-request auth round-trip chatter
active_subgoals: active_subgoals:
- Keep the shared architecture explicit and separate from app profiles. - Keep the shared architecture explicit and separate from app profiles.
- Preserve the default boundary that app-domain data remains app-owned. - Preserve the default boundary that app-domain data remains app-owned.
- Make Repixelizer the first binding and StreamPixels the boundary stress test. - Align the plan to Heimdall as a Yggdrasil-hosted authority service.
- Define the extraction seam for an embedded `gamecult_access` package. - Define the claim-verification contract that Repixelizer and StreamPixels will consume locally.
accepted_design: accepted_design:
- `docs/architecture.md` is the canonical shared architecture note. - `docs/architecture.md` is the canonical shared architecture note.
- `docs/app-profiles/*.md` are thin app bindings, not duplicate brains. - `docs/app-profiles/*.md` are thin app bindings, not duplicate brains.
- The reusable backbone owns provider OAuth, identity linking, signed local sessions, entitlement refresh, capability evaluation, and audit events. - Heimdall owns provider OAuth, identity linking, signed local sessions, grants, entitlement refresh, capability evaluation, and audit events.
- Host apps own their domain data, runtime state, and product-specific resources. - Host apps own their domain data, runtime state, and product-specific resources.
- A future shared access store, if it exists, should hold auth/control-plane truth only. - Host apps verify signed claims locally for routine route authorization.
- Repixelizer is the first hosted-app binding. - A shared Heimdall store, if it exists, should hold auth/control-plane truth only.
- StreamPixels is the boundary case that proves backbone reuse must not imply audience-data centralization. - Repixelizer is the first auth-blank hosted-app binding.
- StreamPixels is the migration case that proves shared authority must not imply audience-data centralization.
landed_baseline: landed_baseline:
- Shared architecture extracted into its own repo. - Shared architecture extracted into its own repo and renamed to Heimdall.
- Repixelizer and StreamPixels app-profile notes created. - Repixelizer and StreamPixels app-profile notes created.
- Repo-local state, handoff, and compaction scaffolding created. - Repo-local state, handoff, and compaction scaffolding created.
rejected_paths: rejected_paths:
- name: per_app_auth_copy_paste - name: per_app_auth_copy_paste
reason: duplicates the same provider/session sludge across every experiment reason: duplicates the same provider/session sludge across every experiment
- name: central_auth_service_first - name: per_request_auth_round_trip
reason: adds deployment weight before two real bindings justify it reason: turns shared auth into a noisy bottleneck instead of local claim verification
- name: shared_user_data_swamp - name: shared_user_data_swamp
reason: reusing auth mechanics does not justify centralizing app-domain audience or product data reason: reusing auth mechanics does not justify centralizing app-domain audience or product data
current_status: current_status:
summary: The shared hosted-access planning work has been extracted into its own repo with a canonical architecture note, a forward implementation plan, and thin app profiles for Repixelizer and StreamPixels. The key boundary is explicit: reusable auth/control-plane code is shared, app-domain data remains app-owned by default. The recommended first implementation mode is an embedded shared package rather than a dedicated auth service. summary: The shared hosted-access planning work now lives in Heimdall, a service-first auth authority repo with a Yggdrasil-hosted topology in mind. The key boundary is explicit: Heimdall owns OAuth, linking, grants, entitlement refresh, and signed claims; app-domain data remains app-owned; routine route authorization stays local after claim verification.
next_action: Start the first implementation-oriented pass by defining the embedded `gamecult_access` package seam: storage interfaces, provider adapter interfaces, signed-session claims, and capability rule evaluation boundaries, while keeping Repixelizer as the first binding target. next_action: Start the first implementation-oriented pass by defining Heimdall's service contract and skeleton: OAuth start/callback surfaces, signed session and claim payloads, claim verification material for app backends, and the first Repixelizer and StreamPixels integration seams.

View file

@ -199,10 +199,10 @@ def add_handoff_checks(findings: list[Finding]) -> None:
def add_agents_checks(findings: list[Finding]) -> None: def add_agents_checks(findings: list[Finding]) -> None:
text = read_text(AGENTS_PATH) text = read_text(AGENTS_PATH)
if "gamecult_access_prepare_compaction.py" in text: if "heimdall_prepare_compaction.py" in text:
findings.append(Finding("ok", "AGENTS.md tells agents to use the compaction helper")) findings.append(Finding("ok", "AGENTS.md tells agents to use the compaction helper"))
else: else:
findings.append(Finding("error", "AGENTS.md does not mention tools/gamecult_access_prepare_compaction.py")) findings.append(Finding("error", "AGENTS.md does not mention tools/heimdall_prepare_compaction.py"))
if "prepare for imminent compaction" in text.lower(): if "prepare for imminent compaction" in text.lower():
findings.append(Finding("ok", "AGENTS.md names the imminent-compaction trigger")) findings.append(Finding("ok", "AGENTS.md names the imminent-compaction trigger"))
else: else:
@ -232,7 +232,7 @@ def render_report(findings: list[Finding], status: str, log: str, evidence: list
} }
lines = [ lines = [
"GameCult Access pre-compaction persistence check", "Heimdall pre-compaction persistence check",
f"Workspace: {ROOT}", f"Workspace: {ROOT}",
f"Findings: {counts['ok']} ok, {counts['warn']} warn, {counts['error']} error", f"Findings: {counts['ok']} ok, {counts['warn']} warn, {counts['error']} error",
"", "",
@ -280,7 +280,7 @@ def render_report(findings: list[Finding], status: str, log: str, evidence: list
def build_parser() -> argparse.ArgumentParser: def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description="Audit GameCult Access persistent state before imminent compaction." description="Audit Heimdall persistent state before imminent compaction."
) )
parser.add_argument( parser.add_argument(
"--strict", "--strict",

View file

@ -131,7 +131,7 @@ def cmd_close_branch(args: argparse.Namespace) -> int:
def build_parser() -> argparse.ArgumentParser: def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description="Inspect and update GameCult Access persistent state files." description="Inspect and update Heimdall persistent state files."
) )
subparsers = parser.add_subparsers(dest="command", required=True) subparsers = parser.add_subparsers(dest="command", required=True)