diff --git a/AGENTS.md b/AGENTS.md index 11f6de1..54ce32e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -21,8 +21,11 @@ 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/hosted-auth-architecture.md` as a future architecture note for - hosted auth work, not as proof that auth machinery already exists. +- 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. - 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 @@ -34,7 +37,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` -- Hosted auth architecture: `E:\Projects\repixelizer\docs\hosted-auth-architecture.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` - 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 new file mode 100644 index 0000000..22109db --- /dev/null +++ b/docs/gamecult-hosted-access-architecture.md @@ -0,0 +1,333 @@ +# GameCult Hosted Access Architecture + +## What this file is + +This file is a future shared architecture note for GameCult-hosted experiments +that need the same basic promise: + +- 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 + +It is not a map of currently landed auth code in this repo. + +## Problem + +Repixelizer is probably not the last hosted experiment that wants this shape. + +If every experiment re-implements: + +- 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 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 +- 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. + +## 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 + +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. + +## 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. + +## 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. diff --git a/docs/hosted-auth-architecture.md b/docs/hosted-auth-architecture.md index 36e6f04..24d72a9 100644 --- a/docs/hosted-auth-architecture.md +++ b/docs/hosted-auth-architecture.md @@ -1,441 +1,6 @@ -# Repixelizer Hosted Auth Architecture +This file moved. -## What this file is +Use: -This file is a future architecture note for hosted-demo authentication and -entitlement work. - -It is not a map of code that already exists in the repo. Current live hosted -demo behavior is still just: - -- hosted-demo runtime limits and UI flags in `src/repixelizer/gui.py` -- the local single-process queue in `src/repixelizer/gui.py` -- frontend consumption of `hostedDemo` runtime config in `frontend/src/app.ts` - -If future auth work lands and this note drifts away from the code, trust the -code first and either update this document or cut the stale parts instead of -pretending the architecture already exists. - -## Summary - -The hosted Repixelizer demo should support both: - -- Patreon-backed access for supporters who do not use Discord -- Discord role-backed access for supporters or community members who do - -Access should be granted if **either** entitlement source says yes. - -That means we do **not** build two separate protected apps, two separate queue paths, or two separate user models. We build one local account system with multiple linked identities and one access policy. - -## Goals - -- Let a user sign in with either Discord or Patreon -- Grant access if the user: - - has one of the allowed Discord roles in the GameCult server, or - - has an allowed Patreon entitlement -- Allow optional account linking so one human can attach both providers to one local account -- Keep queue ownership, job cancellation, and hosted limits tied to one local user/session model -- Avoid duplicated entitlement logic spread across unrelated login handlers - -## Non-goals - -- Turning Repixelizer into a general identity provider -- Requiring every Patreon supporter to join Discord -- Requiring every Discord member to have Patreon connected -- Complex billing state history or subscription analytics -- Multi-tenant auth machinery - -## External providers - -### Discord - -Use Discord OAuth2 authorization code flow for login. - -Minimum use: - -- identify the user - -Recommended access pattern: - -- log the user in with Discord OAuth -- use the existing GameCult bot token server-side to inspect that user's guild membership and role ids - -Why this path: - -- role truth already lives in Discord -- the Patreon bot already manages supporter roles there -- role checks stay anchored to the same operational source of truth -- the web app does not need to ask the user for broader guild scopes unless we later decide it is necessary - -If needed, we can still support the OAuth `guilds.members.read` path later. It is not the preferred first cut. - -### Patreon - -Use Patreon OAuth2 authorization code flow for login. - -After login: - -- fetch the logged-in user's identity and membership data -- inspect membership to the GameCult campaign -- inspect current entitled tiers or equivalent supporter status fields - -This path exists specifically for the "I want access to Repixelizer and I do not care about Discord" crowd. - -## Core model - -### Local user - -Repixelizer should have one internal user record that is independent of any provider. - -Suggested shape: - -- `users` - - `id` - - `created_at` - - `last_seen_at` - - `display_name` - - `primary_email` nullable - - `access_state` cached summary, optional - -### Linked identities - -Each local user may have zero or more linked external identities. - -- `linked_identities` - - `id` - - `user_id` - - `provider` enum: `discord`, `patreon` - - `provider_user_id` - - `username` - - `access_token_encrypted` - - `refresh_token_encrypted` - - `token_expires_at` - - `profile_json` - - unique on `(provider, provider_user_id)` - -Important invariant: - -- one provider identity can belong to only one local user - -### Entitlement snapshots - -Do not make every page hit go out to Discord or Patreon. Cache the last evaluated entitlement state locally. - -- `entitlement_snapshots` - - `id` - - `user_id` - - `provider` - - `evaluated_at` - - `is_allowed` - - `reason_code` - - `reason_detail` - - `raw_summary_json` - -Examples: - -- Discord provider says `is_allowed = true`, `reason_code = allowed_role` -- Patreon provider says `is_allowed = false`, `reason_code = no_active_membership` - -### Sessions - -Hosted GUI sessions should be local application sessions, not raw provider tokens sprayed into the browser. - -- `sessions` - - `id` - - `user_id` - - `created_at` - - `last_seen_at` - - `expires_at` - - `access_granted` - - `access_reason` - -Store the session id in an HttpOnly secure cookie. - -## Access policy - -This is the heart of it. - -Authentication answers: - -- who is this user - -Entitlement answers: - -- should this user get through the gate - -Those are different questions and should stay different in code. - -### Provider-level entitlement checks - -Implement two providers: - -- `DiscordRoleEntitlementProvider` -- `PatreonEntitlementProvider` - -Each provider returns something like: - -```text -EntitlementResult { - provider: "discord" | "patreon" - allowed: boolean - reason_code: string - reason_detail: string - checked_at: datetime -} -``` - -### Unified policy - -Implement one access policy: - -```text -access = discord_allowed || patreon_allowed -``` - -That is the only rule that should matter to the hosted demo. - -Everything else is explanation and caching. - -### Allowed sources - -#### Discord allowed - -Grant access if the user has at least one configured allowed role in the configured guild. - -Examples: - -- Patreon supporter role -- founder role -- moderator role -- special alpha-tester role - -Keep the role list environment-configured, not hardcoded in some grimy branch. - -#### Patreon allowed - -Grant access if the user has at least one allowed Patreon entitlement. - -Examples: - -- any paid membership -- one or more specific tiers - -Keep this configurable too. - -## Login and linking flows - -### Flow A: Login with Discord - -1. User clicks `Log in with Discord` -2. Complete Discord OAuth -3. Find or create linked identity `(discord, discord_user_id)` -4. Attach to existing local user if already linked -5. Otherwise create a new local user and attach the identity -6. Evaluate entitlements -7. Create local session with access granted or denied - -### Flow B: Login with Patreon - -1. User clicks `Log in with Patreon` -2. Complete Patreon OAuth -3. Find or create linked identity `(patreon, patreon_user_id)` -4. Attach to existing local user if already linked -5. Otherwise create a new local user and attach the identity -6. Evaluate entitlements -7. Create local session with access granted or denied - -### Flow C: Link second provider - -Once logged in, the user may choose `Link Discord` or `Link Patreon`. - -Rules: - -- linking requires an authenticated local session -- linking a provider already attached to another local user is rejected -- after linking, re-run unified entitlement evaluation - -This turns "Patreon-only supporter later joins Discord" and "Discord member later subscribes on Patreon" into boring account maintenance instead of support tickets from hell. - -## Queue and job integration - -The hosted queue should attach jobs to local sessions and local users, not directly to provider ids. - -Suggested additions: - -- `jobs.user_id` -- `jobs.session_id` -- `jobs.entitlement_checked_at` - -Rules: - -- only authenticated sessions with `access_granted = true` may enqueue jobs -- queue position and active job state are scoped to the local user session -- heartbeat and cancel behavior stay exactly as already designed - -If a user loses entitlement mid-session: - -- do not allow new jobs -- queued jobs may be canceled on next entitlement refresh -- active job policy can be either: - - let current job finish, block future jobs - - or cancel active job immediately - -Recommended first cut: - -- let current active job finish -- reject future submissions -- cancel queued-but-not-started jobs after entitlement loss - -That is less spiteful and avoids turning transient API issues into ruined runs. - -## Entitlement refresh strategy - -Do not ask Discord and Patreon on every request like a lunatic. - -Use a cached snapshot with refresh triggers. - -Refresh on: - -- successful login -- provider link -- explicit `refresh access` action -- first protected request after snapshot TTL expires - -Recommended TTL: - -- 10 to 15 minutes - -### Discord refresh - -Fetch guild membership and roles using the bot token and configured guild id. - -If the user is not in the guild, the Discord entitlement is false. - -### Patreon refresh - -Refresh via Patreon API using the stored Patreon identity and tokens. - -If the membership lookup fails because tokens are stale, attempt refresh. - -If refresh fails, mark Patreon entitlement unknown or false depending on the failure mode. - -Recommended behavior: - -- provider outage should not instantly hard-lock already active users if a recent positive snapshot exists -- use a short grace window for transient provider failures - -## Configuration - -Suggested environment variables: - -- `REPIXELIZER_AUTH_ENABLED=1` -- `REPIXELIZER_SESSION_SECRET=...` -- `REPIXELIZER_BASE_URL=https://repixelizer.gamecult.org` - -- `REPIXELIZER_DISCORD_CLIENT_ID=...` -- `REPIXELIZER_DISCORD_CLIENT_SECRET=...` -- `REPIXELIZER_DISCORD_BOT_TOKEN=...` -- `REPIXELIZER_DISCORD_GUILD_ID=...` -- `REPIXELIZER_DISCORD_ALLOWED_ROLE_IDS=role1,role2,role3` - -- `REPIXELIZER_PATREON_CLIENT_ID=...` -- `REPIXELIZER_PATREON_CLIENT_SECRET=...` -- `REPIXELIZER_PATREON_CAMPAIGN_ID=...` -- `REPIXELIZER_PATREON_ALLOWED_TIER_IDS=tier1,tier2` - -- `REPIXELIZER_ENTITLEMENT_CACHE_TTL_SECONDS=900` -- `REPIXELIZER_PROVIDER_FAILURE_GRACE_SECONDS=3600` - -## UI behavior - -The hosted landing state should show: - -- `Log in with Discord` -- `Log in with Patreon` - -If access is denied after login: - -- say why in plain English -- show what qualifies -- offer linking the other provider if the user is logged in locally - -Examples: - -- `Your Discord account is signed in, but you do not have one of the allowed GameCult roles.` -- `Your Patreon account is signed in, but no active Repixelizer-eligible membership was found.` -- `Already a supporter on the other platform? Link it here.` - -Once allowed: - -- the queue UI and hosted GUI behave normally - -## Security notes - -- Use authorization code flow for both providers -- Use `state` properly for both OAuth flows -- Store provider tokens server-side only -- Encrypt stored refresh/access tokens at rest -- Use secure, HttpOnly session cookies -- Keep local session invalidation separate from provider logout -- Audit log link/unlink, login, entitlement refresh, and submission denial events - -## Recommended implementation order - -### Phase 1: Local auth scaffolding - -- add local user, linked identity, session, and entitlement snapshot models -- add session cookie handling -- add protected-session middleware/helpers - -### Phase 2: Discord login and entitlement - -- implement Discord OAuth login -- implement bot-backed guild role entitlement check -- gate the hosted queue behind Discord-based access - -### Phase 3: Patreon login and entitlement - -- implement Patreon OAuth login -- implement Patreon membership/tier entitlement check -- unify access policy across both providers - -### Phase 4: Identity linking - -- allow linking the second provider -- re-run entitlement evaluation on link/unlink - -### Phase 5: Operational hardening - -- add entitlement refresh TTLs and provider-failure grace behavior -- add UI for denied states and manual refresh -- add audit logs and admin diagnostics - -## Invariants - -Keep these sacred: - -- login provider is not the same thing as entitlement provider -- the queue only knows local users and local sessions -- access is granted if either provider says yes -- provider-specific logic stays isolated behind entitlement providers -- linking identities must never merge two existing local users implicitly - -If we violate those rules, the auth system will become a damp little pit of edge cases almost immediately. - -## Audit Result - -This design is still compatible with the current Epiphany workflow after one -important correction: it must be treated as future architecture, not current -truth. - -That boundary is now explicit. The note can keep its job if it stays focused on: - -- the proposed local-user plus linked-identity model -- the `discord_allowed || patreon_allowed` entitlement rule -- the implementation phases and invariants for future auth work - -It should not quietly become: - -- a handoff note -- a claim that auth is already landed -- a duplicate of the live queue behavior already described by the code +- `docs/gamecult-hosted-access-architecture.md` for the reusable shared design +- `docs/repixelizer-hosted-access-profile.md` for the Repixelizer-specific binding diff --git a/docs/repixelizer-hosted-access-profile.md b/docs/repixelizer-hosted-access-profile.md new file mode 100644 index 0000000..2308da9 --- /dev/null +++ b/docs/repixelizer-hosted-access-profile.md @@ -0,0 +1,172 @@ +# Repixelizer Hosted Access Profile + +## What this file is + +This file binds Repixelizer onto the shared future design in +`docs/gamecult-hosted-access-architecture.md`. + +It is still future design, not a claim that the hosted demo already has auth. + +## Current repo boundary + +Current live hosted-demo behavior is still just: + +- 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` + +No Discord login, Patreon login, linked identities, local sessions, or access +gates are landed yet. + +## 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. diff --git a/notes/fresh-workspace-handoff.md b/notes/fresh-workspace-handoff.md index 5869786..73d6b49 100644 --- a/notes/fresh-workspace-handoff.md +++ b/notes/fresh-workspace-handoff.md @@ -33,7 +33,9 @@ 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/hosted-auth-architecture.md` is a future design note for a not-yet-landed hosted auth system; current hosted-demo truth is still just the queue and limit machinery in `src/repixelizer/gui.py` plus the frontend runtime-config plumbing. +- `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. +- 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 @@ -90,7 +92,8 @@ 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/hosted-auth-architecture.md` is future auth design, not live implementation truth. +- `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. Do not let any one note become all of those things. That is how the tower grows sideways and starts calling itself architecture. diff --git a/state/evidence.jsonl b/state/evidence.jsonl index a39fed0..b5b3e42 100644 --- a/state/evidence.jsonl +++ b/state/evidence.jsonl @@ -2,3 +2,4 @@ {"ts":"2026-04-26T00:00:00Z","type":"metric","status":"accepted","note":"source_structure is tracked alongside source_fidelity because lattice-only scoring could rank visibly better outputs as worse."} {"ts":"2026-04-26T00:00:00Z","type":"regression","status":"rejected","note":"The d9fa411 phase-field tuning pass was a real regression on the pinned badge case; reverting it restores the known-good badge result exactly."} {"ts": "2026-04-26T03:57:11+00:00", "type": "docs", "status": "ok", "note": "Audit pass clarified note boundaries: the algorithm map remains the live control-flow truth, the implementation plan remains the forward hypothesis ledger, and hosted-auth-architecture is explicitly future design rather than landed auth machinery."} +{"ts": "2026-04-26T04:05:06+00:00", "type": "design", "status": "accepted", "note": "Hosted auth was split into a reusable GameCult hosted-access architecture plus a thin Repixelizer-specific access profile. Recommended extraction path is an embedded shared gamecult_access layer first, with dedicated service only after a second app proves the need."}