diff --git a/Directory.Build.props b/Directory.Build.props deleted file mode 100644 index 1bb3c83..0000000 --- a/Directory.Build.props +++ /dev/null @@ -1,7 +0,0 @@ - - - - true - - - diff --git a/Dockerfile b/Dockerfile index 45072e5..4f607e6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,15 +1,14 @@ FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build WORKDIR /src -COPY Directory.Build.props . -COPY src/Bifrost.Web/Bifrost.Web.csproj src/Bifrost.Web/packages.lock.json src/Bifrost.Web/ -RUN dotnet restore src/Bifrost.Web/Bifrost.Web.csproj --locked-mode +COPY src/Bifrost.Web/Bifrost.Web.csproj src/Bifrost.Web/ +RUN dotnet restore src/Bifrost.Web/Bifrost.Web.csproj COPY . . RUN dotnet publish src/Bifrost.Web/Bifrost.Web.csproj \ --configuration Release \ --output /app/publish \ - /p:UseAppHost=false /p:RestoreLockedMode=true + /p:UseAppHost=false FROM mcr.microsoft.com/dotnet/aspnet:8.0-bookworm-slim AS runtime WORKDIR /app diff --git a/docs/bridge.md b/docs/bridge.md index d971d0c..683d7d2 100644 --- a/docs/bridge.md +++ b/docs/bridge.md @@ -196,7 +196,15 @@ The command is a dry-run shape probe for public surfaces that are real enough to Bifrost's native Discord gateway is the conversation observer. A direct mention of the Bifrost bot, or a reply to one of its messages, becomes an immutable typed `bifrost.discord.persona_conversation_event.v0` only when the guild and channel have one exact Persona target binding. Bot-authored, ambient, empty, over-1200-character, unbound, and ambiguously bound messages do not enter the conversation path. The 1200-character cap is the shared signed alpha boundary with Epiphany. The observation store is Bifrost-private working state, never an actuator queue, and insert collisions must contain exactly equal content. VoidBot does not observe, schedule, or answer for the target Persona. -Persona speech goes out through Matrix, not Discord. `tools/persona-matrix-mouth.mjs` is the one process for every Persona (`PERSONAS`, a list of slugs): it holds each Persona's Matrix access token as its own systemd credential (`matrix-token-`) and nobody else holds any. Per Persona it keeps one `/sync` loop over the configured rooms and the account's direct rooms (a DM invitation from an account on `MATRIX_SERVER_NAME` is accepted while that Persona's brake is released, and one from any other server is declined), one cursor and one delivery sequence. A delivery carries the sender's Matrix id, the event's `m.mentions` user ids and whether the room is a DM. Reading wakes nothing and acks nothing: the heartbeat polls with the non-consuming `gamecult.persona.chat_pending_request.v1`, and only `chat_ack` advances a position. The mouth speaks on a `gamecult.persona.chat_send_request.v1` and joins rooms only while that Persona's brake (`/etc/gamecult/minds//control.cc`) is released. One loopback CultNet RUDP endpoint serves all of them; a request naming an unlisted Persona gets a typed `chat_refusal` (`UnknownPersona`). Document shapes: `tools/persona-chat-documents.mjs`; golden bytes: `tests/fixtures/persona-chat-v1/`. The Discord posting actuator, its permit and its RUDP listener are removed. +Persona replies cross back through two explicit CultMesh surfaces. Epiphany is +the sole writer of `epiphany.persona_discord_delivery_request.v0`; Bifrost +reads that surface, authenticates the dedicated Persona-mouth signer, journals +external execution before touching Discord, and is the sole writer of +`bifrost.persona_discord_delivery_receipt.v0`. Epiphany reads receipts but +never receives the Discord token or write access to Bifrost private state. +`unknown` is terminal for automatic transport retry: an external post may have +happened, so restart recovery must not manufacture a duplicate. Failed and +unknown outcomes remain Persona pressure for explicit resolution. Bifrost admits an observation only when guild, channel, target runtime, canonical repository, Persona, producer, and producer runtime match a preconfigured `bifrost.persona_feedback.target_binding.v0` document in Bifrost's private provider store. That binding also owns source visibility, data classification, and the delivery-store target. Bifrost never infers public visibility from Discord identifiers. It classifies the author using its own `bifrost.discord.actor_link.v0` state: a match yields `linked_governance_feedback`; absence yields `unlinked_social_feedback`. Neither class grants adoption, work, release, or deployment authority. diff --git a/docs/context.md b/docs/context.md index 542ff8d..fdcef97 100644 --- a/docs/context.md +++ b/docs/context.md @@ -18,10 +18,8 @@ The product center is the Labor Platform loop: eligible participants express pol - source of deployment and infrastructure conventions - documents Yggdrasil, nginx, systemd, and local-Postgres patterns - `E:\Projects\Heimdall` - - authenticates: owns OAuth, linked accounts, token custody, signed claims, grants, consent, and revocation - - does not issue GameCult identity: its account id (`HeimdallAccountId`) is an authenticator handle, never who a person is on GameCult - - Bifrost consumes Heimdall authentication/capability facts instead of becoming the credential vault - - remaining scar: Heimdall still mints its own account id in code (`src/app.ts` `createAccount`); chat does not need that to move, so it has not + - owns OAuth, linked accounts, token custody, signed claims, grants, consent, and revocation + - Bifrost consumes Heimdall identity/capability facts instead of becoming the credential vault - `E:\Projects\VoidBot` - observes Discord, packages consensus, and runs repo Persona heartbeats - should call Bifrost for governed GitHub/Discord/Reddit/intake crossings instead of becoming hidden transport authority @@ -57,7 +55,7 @@ After bootstrap, the next session should start implementing the first vertical s - member alpha first - invite-only onboarding -- Bifrost is the GameCult identity issuer: `UserAccount.Id` is the subject, and Bifrost is the OIDC provider for chat (`docs/oidc-issuer.md`); GitHub, native, and Heimdall-linked sign-in only authenticate into it +- GitHub as the only auth provider in v1 - GitHub issues and internal tasks both supported - app-native governance and voting - priority and reward pressure should be transparent and auditable diff --git a/docs/jurisdiction-map.md b/docs/jurisdiction-map.md index a251b00..c0a2754 100644 --- a/docs/jurisdiction-map.md +++ b/docs/jurisdiction-map.md @@ -103,8 +103,6 @@ This keeps Reddit useful without making subreddit mechanics the secret constitut - signed identity and capability claims - grants, consent, revocation, and capability evaluation -Heimdall authenticates; it does not issue GameCult identity. Bifrost owns the identity (`UserAccount.Id`, `NormalizedBifrostIdentity`) and issues it to chat as an OIDC provider (`docs/oidc-issuer.md`). `HeimdallAccountId` is an authenticator handle that links a Heimdall account to a Bifrost identity. - Bifrost consumes Heimdall claims. It does not become the key vault. The split is bridge versus gate: Heimdall decides who may cross under which grant; Bifrost moves authorized work across public protocols and records where it landed. ### Patron Collection Crossing @@ -155,7 +153,7 @@ Repo Personas may generate bridge requests. They do not own the bridge itself. | Concern | Owner | Reason | | --- | --- | --- | -| Who is this actor? | Bifrost (identity) with Heimdall (authentication) | Bifrost issues the identity; account custody and credential proof stay with the auth authority. | +| Who is this actor? | Heimdall | Identity and account custody must stay with the auth authority. | | Is this actor allowed to request this crossing? | Heimdall plus Bifrost | Heimdall issues grants; Bifrost applies bridge-domain policy. | | What work is being requested? | Bifrost | The request shape, priority, provenance, and lifecycle are governance concerns. | | How does interest become reward pressure? | Bifrost | Priority, bounty growth, vote weight, and reward allocation are Labor Platform concerns. | @@ -207,7 +205,6 @@ This is not the final hosted control plane. It is the current bridge actuator an - Do not add Bifrost-scoped Discord transport or dispatch receipts to arbitrary repo Persona scripts. Call Bifrost. - Do not add Bifrost-scoped Reddit posting or Persona flair handling to arbitrary repo Persona scripts. Call Bifrost. - Do not put OAuth credentials or account custody into Bifrost helper scripts as the long-term plan. Use Heimdall claims. -- Do not hand Heimdall ids, GitHub ids, or display names out as the GameCult subject. The OIDC `sub` is `UserAccount.Id`, and the localpart is `NormalizedBifrostIdentity`, minted once. - Do not make CultCache documents decide policy. They store packets; Bifrost gives those packets meaning. - Do not let a repo Persona turn a proposal into endless Aquarium pressure once it has enough shape. Route it to Bifrost as a reviewable artifact. - Do not expose bridge debug noise to public channels. Public receipts should say what happened, which Bifrost topic or work item owns it, where it landed, and what comes next. diff --git a/docs/oidc-issuer.md b/docs/oidc-issuer.md deleted file mode 100644 index a7f59a4..0000000 --- a/docs/oidc-issuer.md +++ /dev/null @@ -1,113 +0,0 @@ -# Bifrost OIDC issuer - -Bifrost is the GameCult identity issuer. It runs an OpenID Connect provider on its public host so that Matrix -Authentication Service (MAS) can sign chat users in with their Bifrost account. Heimdall authenticates people into -Bifrost; it does not issue the identity chat sees. - -The provider is OpenIddict (`OpenIddict.AspNetCore` and `OpenIddict.EntityFrameworkCore`, 7.7.1). Bifrost owns two -decisions at the authorize endpoint: who the chat user is, and whether they may get a session. - -## Endpoints - -The issuer is `Host:PublicBaseUrl`, normalized by .NET into an absolute URI. For a bare host that adds a trailing -slash, so for `https://bifrost.gamecult.org` the issuer string is `https://bifrost.gamecult.org/`. Configure MAS with -that exact string. - -| Endpoint | Path | -| --- | --- | -| Discovery | `/.well-known/openid-configuration` | -| JWKS | `/.well-known/jwks` | -| Authorization | `/connect/authorize` | -| Token | `/connect/token` | -| Userinfo | `/connect/userinfo` | - -Only the authorization code flow exists, and PKCE (S256) is required. There is no implicit, hybrid, password, -client-credentials or device flow, no refresh token, and no dynamic client registration. The token endpoint takes -`client_secret_basic` only. Scopes are `openid` and `profile`. - -## The one client - -Exactly one client is registered, from configuration, and seeded or updated at startup; every other application -row (with its authorizations and tokens) is deleted at startup. Any other `client_id`, an unregistered `redirect_uri`, or a request without PKCE is refused by the -provider before any sign-in redirect. - -## Configuration - -The section is `Bifrost:Oidc`. Absent, the provider surface is not mapped at all and discovery answers 404. Present, -it must be complete: an unreadable or wrong-shaped file stops startup with a message that names the setting and never -echoes its value. - -| Setting | Meaning | -| --- | --- | -| `Bifrost:Oidc:SigningKeyPath` | PEM file with an EC P-256 private key. Tokens and id_tokens are signed ES256; JWKS publishes exactly that key. A non-P-256 or public-only key stops startup. | -| `Bifrost:Oidc:EncryptionKeyPath` | File holding 32 random bytes as base64 (`openssl rand -base64 32`). Encrypts the tokens OpenIddict issues, the access token and the parked authorize request among them. | -| `Bifrost:Oidc:Clients:Mas:ClientId` | The client id MAS presents. | -| `Bifrost:Oidc:Clients:Mas:ClientSecretPath` | File holding the client secret. | -| `Bifrost:Oidc:Clients:Mas:RedirectUri` | MAS's callback URL, registered exactly. | -| `Bifrost:Oidc:Clients:Mas:BackchannelLogoutUri` | Optional. MAS's back-channel logout URL, `https:///upstream/backchannel-logout/`; absolute http(s) or startup fails. Unset: no logout is sent and no authorization is revoked. | - -There are no development or ephemeral keys in the host. Key files are provisioned like the other -`[workload.secret_files]` paths. - -## Claims - -| Claim | Value | -| --- | --- | -| `sub` | `UserAccount.Id`, formatted `D` | -| `preferred_username` | `NormalizedBifrostIdentity` (minted once; there is no rename path) | -| `name` | `DisplayName`, else `BifrostIdentity` | - -No e-mail, no Heimdall id, no GitHub id, and no membership detail leaves Bifrost. - -## Who may sign in - -The authorize endpoint reads the cookie session and the same actor facts the `/App` policy reads. - -- No session: challenge to `/auth/sign-in`, which resumes the original request after sign-in. OpenIddict parks the - full request (its request token, in the OpenIddict token table, expiring after ten minutes, spent when used) and - the return path carries only a short `request_uri`, so `LocalReturnPath.MaxLength` stays 256. -- Signed in, membership not `Active`: redirect to the client's `redirect_uri` with `error=access_denied` and - `error_description=The account is not an active GameCult member.` -- Signed in, `Active`, but `NormalizedBifrostIdentity` empty: `error=access_denied` with - `error_description=The account has no Bifrost identity.` -- Signed in and `Active`: a code is issued. -- Token endpoint: membership is read again; an account no longer `Active` gets `invalid_grant` and no tokens. - -## Back-channel logout - -When `BackchannelLogoutUri` is set, a hosted reconciler (`OidcLogoutReconciler`) runs every 30 seconds. It lists the -subjects holding valid authorizations of the MAS client; for each subject whose membership is not `Active` (the same -reading the token and userinfo endpoints use; a deleted account or a subject that is not an account id counts as not -`Active`) it POSTs one OIDC back-channel logout token to MAS, and only after a 2xx revokes that subject's authorizations, -which also invalidates their tokens. Membership is never written by this path: any way of leaving `Active` (admin -action, sync, a database edit, a future suspend) is covered because the work is derived from membership state. - -The token is an ES256 JWT signed with the issuer key, header `typ` `logout+jwt`, with `iss` as the id_token carries it, -`aud` the MAS client id, `sub` the account id, `iat`, `exp` two minutes later, a fresh `jti` and the -`http://schemas.openid.net/event/backchannel-logout` event. It carries no `nonce` and no `sid`; MAS finds the sessions -by `sub`. It is sent as `application/x-www-form-urlencoded` field `logout_token` with a 10 second timeout, and the -response body is never logged. - -Failure handling is per subject, in memory: a 5xx, a timeout or a network fault backs off exponentially from 30 seconds -to 15 minutes; a 400 (token refused) or 404 (unknown provider ULID) is a configuration fault, logged by status only, -and retried at the 15 minute cap. Nothing is revoked until MAS answers 2xx. A restart loses the backoff and nothing -else: the authorizations table is the queue, so a later-set URI also catches up on earlier demotions. - -Tradeoff: delivery latency is up to one tick, and a member demoted and re-promoted inside one tick keeps their sessions. - -MAS verified at v1.28.0 (`crates/handlers/src/upstream_oauth2/backchannel_logout.rs`): the token is checked like an -id_token (`iss`, `aud`, `alg`, JWKS signature), `exp` and `iat` are required, `sub` or `sid` is required, `nonce` must be -absent. Owed on the MAS side (gamecult-chat): the provider's `backchannel_logout_uri` and -`on_backchannel_logout: logout_all`. Re-check against the MAS version pinned there. - -## MAS configuration - -Configure MAS's upstream provider with the issuer above, the client id and secret, the token endpoint auth method -`client_secret_basic`, the id_token signing algorithm `ES256`, scope `openid profile`, and a localpart claim import -from `preferred_username`. MAS mints the Matrix localpart on first sign-in, so the claim must never change for an -account. - -## Remaining scar - -Heimdall still mints its own account id in code (`src/app.ts` `createAccount`). That id is an authenticator handle -and chat does not need it to move. diff --git a/docs/research/2026-10-29-estate-tally-game-theory/README.md b/docs/research/2026-10-29-estate-tally-game-theory/README.md deleted file mode 100644 index f67a9a5..0000000 --- a/docs/research/2026-10-29-estate-tally-game-theory/README.md +++ /dev/null @@ -1,330 +0,0 @@ -# Eyes: estate internal tally, game-theory probes (2026-10-29) - -Fork: Fork Desk question 39, `eureka-body:question:estate-internal-tally` (InForce; options proportional | winner-take-all; Self's recommended: proportional). Context ruling `eureka-body:ruling:governance-estate-blocks`. Session self-2026-10-29-eureka. Facts and measured results only, no recommendation. -Operator additions folded in (her words relayed by the Self): within-bloc vote-power curves ("Patrons vote power scales with donation total, but it's logarithmic... Member vote should scale with contribution... via a lower exponent"); present case ("I'm the only active member"); renormalization ruling ("normalize it by who actually voted"). - -## Code, outputs, model -Dir `sims/` (numpy 2.5.4, Python 3.13, `python -I