diff --git a/AGENTS.md b/AGENTS.md index 54ce32e..d525a96 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -21,11 +21,15 @@ diagnostics good enough to catch when we are lying to ourselves. - Treat `docs/implementation-plan.md` as the forward plan. - Treat `docs/lean-optimizer-algorithm-map.md` as the source-grounded live algorithm map. -- Treat `docs/gamecult-hosted-access-architecture.md` as the future shared - access architecture for GameCult-hosted experiments, not as proof that auth - machinery already exists here already. -- Treat `docs/repixelizer-hosted-access-profile.md` as the Repixelizer-specific - binding onto that future shared access architecture. +- Treat `E:\Projects\gamecult-access\docs\architecture.md` as the canonical + future shared access architecture for GameCult-hosted experiments, not as + proof that auth machinery already exists here already. +- Treat `E:\Projects\gamecult-access\docs\app-profiles\repixelizer.md` as the + canonical Repixelizer-specific binding onto that future shared access + architecture. +- Treat local `docs/gamecult-hosted-access-architecture.md` and + `docs/repixelizer-hosted-access-profile.md` as redirect stubs, not the real + design notes. - Update `state/map.yaml` when project understanding changes. - Add evidence after meaningful research, implementation, verification, or rejected paths, but keep it distilled. Routine "I just did this" proof belongs @@ -37,8 +41,8 @@ diagnostics good enough to catch when we are lying to ourselves. - Project root: `E:\Projects\repixelizer` - Implementation plan: `E:\Projects\repixelizer\docs\implementation-plan.md` - Algorithm map: `E:\Projects\repixelizer\docs\lean-optimizer-algorithm-map.md` -- Shared hosted access architecture: `E:\Projects\repixelizer\docs\gamecult-hosted-access-architecture.md` -- Repixelizer hosted access profile: `E:\Projects\repixelizer\docs\repixelizer-hosted-access-profile.md` +- Shared hosted access architecture: `E:\Projects\gamecult-access\docs\architecture.md` +- Repixelizer hosted access profile: `E:\Projects\gamecult-access\docs\app-profiles\repixelizer.md` - Handoff summary: `E:\Projects\repixelizer\notes\fresh-workspace-handoff.md` - State CLI: `E:\Projects\repixelizer\tools\repixelizer_state.py` - Pre-compaction helper: `E:\Projects\repixelizer\tools\repixelizer_prepare_compaction.py` diff --git a/docs/gamecult-hosted-access-architecture.md b/docs/gamecult-hosted-access-architecture.md index ff7f842..33e634a 100644 --- a/docs/gamecult-hosted-access-architecture.md +++ b/docs/gamecult-hosted-access-architecture.md @@ -1,399 +1,20 @@ # GameCult Hosted Access Architecture -## What this file is +This shared design moved. -This file is a future shared architecture note for GameCult-hosted experiments -that need the same basic promise: +Canonical location: -- let people sign in with Discord and/or Patreon -- decide access from GameCult/community entitlements -- keep app runtimes tied to local sessions and local accounts, not provider ids +- `E:\Projects\gamecult-access\docs\architecture.md` -It is not a map of currently landed auth code in this repo. +Repixelizer-specific binding: -## Problem +- `E:\Projects\gamecult-access\docs\app-profiles\repixelizer.md` -Repixelizer is probably not the last hosted experiment that wants this shape. +This local file remains only as a redirect stub so old notes and links do not +pretend the shared access plan still lives inside Repixelizer. -If every experiment re-implements: +Current repo truth is unchanged: -- provider OAuth -- identity linking -- entitlement refresh -- session cookies -- queue/job ownership -- "Discord role OR Patreon tier" logic - -then we will end up with five damp little auth pits that are all almost the -same and all annoying in slightly different ways. - -The reusable move is to build one GameCult hosted-access layer and let each app -bind its own policy and runtime seams onto it. - -## Design goals - -- one reusable local account/session model across hosted experiments -- provider login and linking owned by one shared access layer -- per-app capability policies instead of hard-coded one-off gate checks -- host runtimes consume signed local session claims, not raw provider tokens -- keep app-domain data separate unless there is a deliberate reason to share it -- reusable enough as a shared package first, with room to graduate into a - shared service later if that pain actually materializes - -## Non-goals - -- becoming a general public identity provider -- building multi-tenant SaaS auth machinery -- inventing global billing analytics because we got bored -- forcing cross-app single sign-on on day one before a second app exists - -## Core concepts - -- `Account` - one GameCult-local person record independent of external providers -- `LinkedIdentity` - one external provider identity attached to exactly one local account -- `EntitlementSnapshot` - cached result from a provider or grant source saying whether some claim is - currently true -- `CapabilityGrant` - a reusable local grant such as `global_member`, `app_access`, or - `admin_access`, scoped either globally or to one app -- `AppAccessProfile` - the per-app binding that names capabilities, allowed provider sources, and - policy rules -- `Session` - signed local app/browser session; the browser gets this, not provider tokens -- `AuditEvent` - durable record for login, link, unlink, refresh, denial, and admin overrides - -## System shape - -```mermaid -flowchart TD - A["Browser / app web UI"] --> B["Hosted experiment web surface"] - B --> C["GameCult access layer"] - C --> D["Discord OAuth + guild role checks"] - C --> E["Patreon OAuth + campaign/tier checks"] - C --> F["Local account/session/grant store"] - C --> G["Signed access claims"] - G --> H["Hosted experiment runtime"] -``` - -The important split: - -- the access layer owns provider OAuth, identity linking, entitlement refresh, - and signed local sessions -- the host experiment owns its actual workload, queue, editor, render path, or - whatever weird machine it is - -Do not smear provider logic directly through every host runtime. - -## Backbone reuse is not data merging - -Using the same auth backbone does **not** mean all hosted experiments should -pour their user data into one communal bucket. - -The reusable thing is: - -- provider OAuth plumbing -- signed local session mechanics -- linked-identity primitives -- entitlement refresh -- capability evaluation - -The non-reusable-by-default thing is: - -- app-domain user data -- creator data -- audience data -- queue/job payloads -- inventories, scenes, outputs, or other product state - -Recommended default: - -- shared auth/access **code** -- separate app databases or at least separate app-owned schemas/tables for - domain data -- if a shared access store exists at all, keep it limited to auth/control-plane - truth such as identities, grants, entitlement snapshots, and audit events - -Do not let "same auth backbone" quietly mutate into "same user-data swamp." - -## Reusable data model - -Suggested durable core: - -- `accounts` - - `id` - - `created_at` - - `last_seen_at` - - `display_name` - - `primary_email` nullable -- `linked_identities` - - `id` - - `account_id` - - `provider` - - `provider_user_id` - - `username` - - `access_token_encrypted` - - `refresh_token_encrypted` - - `token_expires_at` - - `profile_json` - - unique on `(provider, provider_user_id)` -- `entitlement_snapshots` - - `id` - - `account_id` - - `provider` - - `scope` - - `evaluated_at` - - `is_allowed` - - `reason_code` - - `reason_detail` - - `raw_summary_json` -- `capability_grants` - - `id` - - `account_id` - - `scope_type` enum: `global`, `app` - - `scope_id` nullable app slug - - `capability` - - `source` enum: `manual`, `invite`, `migration`, `operator` - - `status` - - `expires_at` nullable - - `note` -- `sessions` - - `id` - - `account_id` - - `app_slug` - - `created_at` - - `last_seen_at` - - `expires_at` - - `claims_json` - - `access_revision` -- `audit_events` - - `id` - - `account_id` nullable - - `session_id` nullable - - `app_slug` nullable - - `event_type` - - `event_payload_json` - - `created_at` - -Important invariants: - -- one provider identity belongs to one local account only -- provider tokens stay server-side only -- session cookies represent local sessions, not provider trust directly -- app-specific capability checks read local claims, not Discord or Patreon on - every route - -## Provider adapters - -Shared provider/gate adapters should be boring and reusable: - -- `DiscordRoleEntitlementProvider` - - login via Discord OAuth - - entitlement check via guild membership and role ids, ideally through the - GameCult bot token server-side -- `PatreonMembershipEntitlementProvider` - - login via Patreon OAuth - - entitlement check via campaign membership and tier ids -- `ManualGrantProvider` - - local override for staff, migrations, experiments, or emergency access -- `InviteGrantProvider` - - reusable invite-issued app or role grants where that pattern makes sense - -This is the good kind of bureaucracy: one place to keep the provider sludge. - -## Policy model - -The reusable core should not hard-code "Repixelizer access". It should evaluate -capabilities against an app profile. - -Suggested app-profile shape: - -```text -AppAccessProfile { - appSlug: string - displayName: string - capabilities: string[] - policyRules: CapabilityRule[] - publicRoutes: string[] - explanationStrings: map[string]string -} -``` - -Example rule style: - -```text -app_access = discord.allowed_role || patreon.allowed_tier || grant.global_member || grant.app_access -queue_submit = app_access -admin_access = grant.operator || grant.admin_access -``` - -This gives reuse in the right place: - -- the shared layer knows how to link identities, refresh entitlements, and - evaluate capabilities -- each experiment only defines which capabilities it wants and what qualifies - -## Auth and linking flow - -The generic flow should be the same across hosted experiments: - -1. user starts OAuth from the host app UI -2. shared access layer signs OAuth `state` -3. provider callback resolves the provider identity -4. shared layer finds or creates the local account -5. if the user was already signed in locally, the identity may link onto the - existing account instead of creating a new one -6. shared layer refreshes entitlements and capability claims -7. shared layer issues or updates the local session cookie - -The browser should never be the custodian of provider tokens. It is a goblin -with JavaScript. - -## App integration contract - -Every hosted experiment should integrate through the same seams: - -- define an `app_slug` -- define the app capability profile -- mark which routes are: - - public - - authenticated - - capability-gated -- attach `account_id`, `session_id`, and `access_revision` to owned resources - such as jobs, drafts, uploads, or queue entries -- require resource-owner checks on private read/update/delete paths - -If an app has a queue or job system: - -- queue entries belong to local account/session ids, not provider ids -- entitlement refresh should block new submissions after access loss -- active-job cancellation policy is app-specific, but the recommended first cut - is still "let the active job finish, block new work" - -## Deployment modes - -This is where we avoid building the cathedral first. - -### Mode 1: Embedded shared package - -Recommended first cut. - -- one shared `gamecult_access` package/module -- each hosted experiment embeds it -- each app keeps its own runtime and route integration -- sessions may still be app-local -- each app keeps its own domain-data store - -Why this is the right first cut: - -- reusable without forcing a separate always-on dependency -- small blast radius -- easy to evolve while only one or two apps use it - -### Mode 2: Same-host shared access service - -Use this when multiple apps on the same server want one shared access store and -session issuer, but full centralization still feels premature. - -### Mode 3: Dedicated GameCult access service - -Only do this when multiple independently deployed apps genuinely need: - -- shared cross-app sessions -- central grant administration -- one callback origin for all providers -- enough auth churn that duplicated embedded deployments become the bigger pain - -Do not build this first because it sounds important. - -Even in this mode, the dedicated service should own auth/control-plane data, not -absorb every app's audience or product data. If it starts trying to become the -warehouse for StreamPixels viewer profiles, Repixelizer jobs, and whatever comes -next, we have built a larger and more expensive mistake. - -## Configuration split - -Reusable shared config should stop pretending each app needs its own provider -stack. - -Suggested generic env surface: - -- `GC_ACCESS_ENABLED=1` -- `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_SECRET=...` -- `GC_ACCESS_DISCORD_BOT_TOKEN=...` -- `GC_ACCESS_DISCORD_GUILD_ID=...` -- `GC_ACCESS_PATREON_CLIENT_ID=...` -- `GC_ACCESS_PATREON_CLIENT_SECRET=...` -- `GC_ACCESS_PATREON_CAMPAIGN_ID=...` -- `GC_ACCESS_ENTITLEMENT_CACHE_TTL_SECONDS=900` -- `GC_ACCESS_PROVIDER_FAILURE_GRACE_SECONDS=3600` - -Per-app binding should live in code or profile config, not in a hundred cloned -provider env names: - -- app slug -- hostnames -- allowed Discord role ids -- allowed Patreon tier ids -- capability rules -- public queue-summary policy, if any - -## Recommended extraction path - -1. Keep the repixelizer-specific note as an app profile, not the whole world. -2. Extract the shared ideas into a reusable `gamecult_access` package or sibling - repo once the first real implementation starts. -3. Move provider OAuth, signed session handling, identity linking, entitlement - refresh, and capability evaluation into that shared layer. -4. Leave app-specific route gating and runtime ownership fields in the host app. -5. Only consider a dedicated shared access service after a second hosted - experiment proves the need. - -## StreamPixels boundary example - -StreamPixels is the obvious case where reuse must not become data mixing. - -What StreamPixels can likely reuse: - -- signed local session mechanics -- linked-identity and OAuth-state patterns -- creator/operator capability evaluation -- provider adapter structure -- grant/invite primitives where they fit - -What StreamPixels should keep separate: - -- viewer audience profiles -- creator memberships tied to streamer spaces -- creator connector credentials -- overlay/runtime state -- audience-facing app data in general - -In plain terms: - -- the same backbone can authenticate and authorize the control plane -- StreamPixels audience data should remain StreamPixels data - -If a future shared access store exists, it should not become the canonical home -for streamer-audience identity and product data. That stays with StreamPixels. - -## Audit Result - -Yes, this can be made reusable. - -The correct reusable seam is: - -- shared provider/session/entitlement/capability machinery -- thin per-app access profiles and runtime bindings - -The incorrect reusable seam is: - -- cloning a Repixelizer-specific auth blob into every future experiment and - changing a few strings with a kitchen knife. +- the hosted demo here still only has queue and limit machinery +- no Discord login, Patreon login, linked identities, local sessions, or access + gates are landed in `E:\Projects\repixelizer` diff --git a/docs/hosted-auth-architecture.md b/docs/hosted-auth-architecture.md index 24d72a9..7e18a7d 100644 --- a/docs/hosted-auth-architecture.md +++ b/docs/hosted-auth-architecture.md @@ -2,5 +2,5 @@ This file moved. Use: -- `docs/gamecult-hosted-access-architecture.md` for the reusable shared design -- `docs/repixelizer-hosted-access-profile.md` for the Repixelizer-specific binding +- `E:\Projects\gamecult-access\docs\architecture.md` for the reusable shared design +- `E:\Projects\gamecult-access\docs\app-profiles\repixelizer.md` for the Repixelizer-specific binding diff --git a/docs/repixelizer-hosted-access-profile.md b/docs/repixelizer-hosted-access-profile.md index 2308da9..0beecd4 100644 --- a/docs/repixelizer-hosted-access-profile.md +++ b/docs/repixelizer-hosted-access-profile.md @@ -1,172 +1,22 @@ # Repixelizer Hosted Access Profile -## What this file is +This app-binding note moved. -This file binds Repixelizer onto the shared future design in -`docs/gamecult-hosted-access-architecture.md`. +Canonical location: -It is still future design, not a claim that the hosted demo already has auth. +- `E:\Projects\gamecult-access\docs\app-profiles\repixelizer.md` -## Current repo boundary +Shared architecture note: -Current live hosted-demo behavior is still just: +- `E:\Projects\gamecult-access\docs\architecture.md` -- hosted-demo runtime limits and UI flags in `src/repixelizer/gui.py` -- the local single-process queue in `src/repixelizer/gui.py` -- frontend runtime-config consumption in `frontend/src/app.ts` +This local file remains only as a redirect stub so Repixelizer does not keep a +second copy of the same future auth plan fermenting in a corner. -No Discord login, Patreon login, linked identities, local sessions, or access -gates are landed yet. +Current repo truth is unchanged: -## App identity - -- `app_slug`: `repixelizer` -- primary hostname: `repixelizer.gamecult.org` -- runtime shape: one hosted GUI plus one queue-backed worker loop inside the app - -## Capabilities - -Repixelizer does not need a baroque permission lattice. - -The likely first-cut capabilities are: - -- `app_access` - - may load the protected hosted GUI -- `queue_submit` - - may create a repixelizer job -- `job_read_own` - - may read own job state, event stream, and final output -- `job_cancel_own` - - may cancel own queued or running job -- `admin_access` - - may inspect operational or grant/admin surfaces later - -For the first cut: - -```text -queue_submit = app_access -job_read_own = app_access + resource ownership check -job_cancel_own = app_access + resource ownership check -``` - -## Access policy - -Repixelizer should inherit the generic GameCult access pattern: - -```text -app_access = - discord.allowed_role - || patreon.allowed_tier - || grant.global_member - || grant.app_access - -queue_submit = app_access - -admin_access = - grant.operator - || grant.admin_access -``` - -Likely Discord-allowed examples: - -- Patreon supporter role -- founder role -- moderator role -- explicit alpha-tester role - -Likely Patreon-allowed examples: - -- any paid GameCult membership -- or one configured subset of tiers if the experiment needs a stricter gate - -## Repixelizer runtime binding - -When this lands, the shared access layer should integrate with the current queue -shape rather than replace it. - -Suggested queue/job additions: - -- `jobs.account_id` -- `jobs.session_id` -- `jobs.access_revision` -- `jobs.entitlement_checked_at` - -Route policy: - -- public: - - `/api/health` - - `/api/config` -- public or capability-gated by policy choice: - - `/api/queue` -- requires `queue_submit`: - - `POST /api/jobs` -- requires ownership: - - `GET /api/jobs/{job_id}` - - `GET /api/jobs/{job_id}/events` - - `POST /api/jobs/{job_id}/heartbeat` - - `DELETE /api/jobs/{job_id}` - -Important invariant: - -- job ownership must resolve from the local session/account, not from raw - Discord or Patreon identifiers - -## UI binding - -Once access exists, the hosted landing flow should become: - -- anonymous visitor sees: - - `Log in with Discord` - - `Log in with Patreon` -- signed-in but denied visitor sees: - - plain-English denial reason - - what qualifies - - link-the-other-provider affordance -- allowed visitor sees: - - the normal hosted GUI and queue flow - -The queue/progress UI itself should stay as close as possible to the current -runtime. The auth layer should gate entry, not rewrite the machine for fun. - -## Config mapping - -Repixelizer-specific auth config should not grow a separate provider jungle. - -Keep: - -- existing runtime/queue/limit env vars under `REPIXELIZER_*` - -Add shared access config under `GC_ACCESS_*`. - -Repixelizer-specific app-profile config can live under either: - -- code constants, or -- a small profile/config surface such as: - - `GC_ACCESS_REPIXELIZER_ALLOWED_DISCORD_ROLE_IDS=...` - - `GC_ACCESS_REPIXELIZER_ALLOWED_PATREON_TIER_IDS=...` - - `GC_ACCESS_REPIXELIZER_PUBLIC_QUEUE_SUMMARY=1` - -The exact encoding can be decided later. The important thing is not cloning the -provider credentials into a second app-specific namespace. - -## Recommended implementation order - -1. Extract or embed the shared `gamecult_access` layer described in - `docs/gamecult-hosted-access-architecture.md`. -2. Gate `POST /api/jobs` plus per-job read/cancel/event routes through local - sessions and ownership checks. -3. Add login/link/denied-state UI around the existing hosted GUI shell. -4. Decide whether `/api/queue` stays public summary or moves behind `app_access`. -5. Only after a second hosted experiment exists, decide whether the shared - access layer should leave embedded-package mode and become a shared service. - -## Invariants - -- Repixelizer should consume the shared access layer; it should not become the - template that future experiments copy by hand. -- The queue remains owned by local session/account ids. -- Provider tokens stay server-side only. -- Capability checks belong at route boundaries and resource ownership seams, not - scattered through queue internals. -- Auth should gate the hosted demo, not mutate the solver model or pretend to be - the product. +- hosted-demo runtime limits and UI flags still live in `E:\Projects\repixelizer\src\repixelizer\gui.py` +- the queue is still the local single-process queue in that same module +- frontend runtime-config consumption still lives in `E:\Projects\repixelizer\frontend\src\app.ts` +- no Discord login, Patreon login, linked identities, local sessions, or access + gates are landed here yet diff --git a/notes/fresh-workspace-handoff.md b/notes/fresh-workspace-handoff.md index 73d6b49..c8da444 100644 --- a/notes/fresh-workspace-handoff.md +++ b/notes/fresh-workspace-handoff.md @@ -33,8 +33,8 @@ Do not trust this file for the exact live HEAD. Always check git. - The current tracked weakness is the widened dark contour near the badge sword tip on tapered shapes. - `source_structure` exists because lattice-only `source_fidelity` could call visibly better outputs worse. - The old tray optimizer is dead and should stay dead unless the entire machine map changes for a real reason. -- `docs/gamecult-hosted-access-architecture.md` is the future shared design for reusable GameCult-hosted access across experiments. -- `docs/repixelizer-hosted-access-profile.md` is the Repixelizer-specific binding onto that future shared access layer. +- `E:\Projects\gamecult-access\docs\architecture.md` is the future shared design for reusable GameCult-hosted access across experiments. +- `E:\Projects\gamecult-access\docs\app-profiles\repixelizer.md` is the Repixelizer-specific binding onto that future shared access layer. - current hosted-demo truth is still just the queue and limit machinery in `src/repixelizer/gui.py` plus the frontend runtime-config plumbing. ## Critical Doctrine @@ -92,8 +92,10 @@ Rules now in force: - this handoff is a compact re-entry packet. - `docs/implementation-plan.md` is the forward plan. - `docs/lean-optimizer-algorithm-map.md` is the source-grounded control-flow map. -- `docs/gamecult-hosted-access-architecture.md` is future shared auth/access design, not live implementation truth. -- `docs/repixelizer-hosted-access-profile.md` is the app-specific future binding, also not live implementation truth. +- `E:\Projects\gamecult-access\docs\architecture.md` is future shared auth/access design, not live implementation truth. +- `E:\Projects\gamecult-access\docs\app-profiles\repixelizer.md` is the app-specific future binding, also not live implementation truth. +- local `docs/gamecult-hosted-access-architecture.md` and + `docs/repixelizer-hosted-access-profile.md` are redirect stubs. Do not let any one note become all of those things. That is how the tower grows sideways and starts calling itself architecture.