openvibers/openvibe.games Commits

Docs: ADR-0006 (canonical subjects, platform boundary), README platform integration (env, grants, mods API), architecture and roadmap notes, platform env in the unit file

OP OpenVibers committed 23 Sept 2026 · 208f41b40b · 6 files, +144 −5


Changes

ddiff --git a/ARCHITECTURE.md b/ARCHITECTURE.md
iindex ecfb252..ec86a96 100644
---- a/ARCHITECTURE.md
++++ b/ARCHITECTURE.md
@@@ -93,8 +93,19 @@ SQLite (WAL) behind `PersistenceStore` repositories: `world_entities`
(props/resources with motion + kind-specific state JSON), `players` (token,
transform, inventory JSON), `meta` (schema version, world-seeded flag).
Dirty tracking batches writes; flush on interval, on settle, and on shutdown.
--First boot seeds from the world definition; afterwards the DB is the world's
--source of truth.
++Each flush is one transaction, together with the outbox events describing
++it. First boot seeds from the world definition; afterwards the DB is the
++world's source of truth. Platform tables (schema 12): `identity_legacy_map`,
++`mods`, `mod_grants`, `mod_placements`, `mod_audit`, `media_mirrors`, and the
++SDK's `event_outbox`.
++
++## Platform boundary
++
++Adapters to the OpenVibe platform live in `apps/server/src/platform` and
++`apps/server/src/mods` (ADR-0006): canonical subject accounts, the `games`
++service principal, the events outbox, the Media mirror, and the mod registry
++with its runtime seam (`ModApi`: capability-checked bindings, the only thing a
++mod can reach). No package under `packages/` imports any of it.
## Zones
@@@ -121,4 +132,6 @@ entity context fields and never spam per-tick.
Assume hostile clients: protocol validation at the edge, range checks on every
interaction (gather, place, physgun grab/unfreeze), zone rules, ownership
recorded on placement, inventory ops validated against authoritative state,
--one live session per identity token. Client-side rays exist purely for UX.
++one live session per identity token, guest tokens restricted to a format
++that can never name an account key. Client-side rays
++exist purely for UX.
ddiff --git a/README.md b/README.md
iindex 57d17df..c3a61af 100644
---- a/README.md
++++ b/README.md
@@@ -84,7 +84,76 @@ STATIC_DIR=apps/client/dist PORT=8000 DB_PATH=data/world.db pnpm --filter @openv
The server serves the built client, `/healthz`, `/metrics`, and the game
WebSocket on one port. Environment: `PORT`, `HOST`, `DB_PATH`, `STATIC_DIR`,
--`MAX_PLAYERS`, `LOG_LEVEL`.
++`MAX_PLAYERS`, `LOG_LEVEL` (platform variables below).
Deployed at https://openvibe.games (nginx TLS termination → server on :8000,
systemd unit `openvibe-games.service`; play.openvibe.games Host-routes to the same process).
++
++## Platform integration (OpenVibe network)
++
++Games integrates with the rest of OpenVibe at its service boundary only
++(`apps/server/src/platform`, `apps/server/src/mods`); the simulation packages
++do not know the platform exists. See [ADR-0006](docs/adr/0006-canonical-subjects-and-platform-boundary.md).
++
++- **Identity.** Signed-in players are keyed by their canonical
++ openvibe.network subject (`usr_…`/`gst_…`, the token's `subject_id`).
++ Characters created under the old `ovn:<network id>` key are adopted on the
++ next sign-in, or in bulk with `apps/server/scripts/migrateIdentity.ts`
++ (`--dry-run` first). Guest tokens that look like account keys (`ovn:…`, `usr_…`) are refused.
++- **Service principal.** Calls to Events, Media and Network identity carry a
++ client-credentials token of the `games` OAuth client (same client id and
++ secret as SSO), one per audience. There are no shared keys. The SSO calls
++ made for a player (`/api/auth/me`, code and FedCM exchanges) keep using the
++ player's own token.
++- **Events** (OpenVibe.Events, through a transactional outbox in
++ `world.db`, table `event_outbox`): `games.player.joined|left`,
++ `games.skill.leveled`, `games.blueprint.unlocked`, `games.world.saved`
++ (at most one checkpoint per `WORLD_SAVED_EVENT_MINUTES`, plus shutdown) and
++ `games.mod.installed|enabled|disabled|grants_changed|revoked`. There is no
++ achievement system in Scraplandia, so there are no achievement events.
++ Progression events come from what a save actually wrote, in the same
++ transaction.
++- **Media.** Map-editor assets (textures, paint masks, glb models; the only
++ uploaded files Games has) are still served from local disk and are also
++ copied into Media objects (`kind: asset`, namespace `MEDIA_NAMESPACE`), with
++ a restart-safe queue (`media_mirrors`). There are no screenshots or blueprint
++ files to upload: blueprints are per-player recipe unlocks.
++- **Mods** (ADR-013 in OpenVibe.Contracts). Manifests follow
++ `mods/mod-manifest.v1` (proposed in `docs/contracts-proposal/`), validated
++ with the JSON Schema itself. The only runtime today is `games-content@1`:
++ declarative data packs checked against `@openvibe/content` (announcements;
++ inert, mod-owned props). Each install stores the approved subset of its
++ requested capabilities; every runtime binding checks it at call time, a
++ revoked install or capability stops affecting the world on the next tick,
++ and install/grant/use/deny/revoke are audited (`mod_audit`). Trust tiers are
++ metadata only. Executable mods (scripts) are refused until sandboxed
++ execution exists in OpenVibe.Host (Stage C). API: `GET /api/v1/mods`,
++ `GET /api/v1/mods/:id` (public); `POST /api/v1/mods`,
++ `POST /api/v1/mods/:id/enable|disable|revoke|grants`,
++ `DELETE /api/v1/mods/:id/grants/:capability`, `GET /api/v1/mods/:id/audit`
++ (owner/admin session, or a principal token with `games.mod.manage`).
++- **Status:** `GET /api/v1/platform` reports whether events and the Media
++ mirror are on, the outbox backlog and mirror counts (no secrets).
++- **Shared chrome.** The portal, `/play` and `/editor` load the network
++ navbar and footer from `https://openvibe.network/shared/`.
++- **Persistence proof.** `apps/server/src/game/platformIntegration.test.ts`
++ boots the real server twice on one database and checks characters, world
++ props, the world clock and the mod registry survive the restart;
++ `sliceTest.ts` covers the gameplay state end to end.
++
++Platform environment (all optional; unset = off):
++
++| Variable | Purpose |
++| --------------------------- | ------------------------------------------------------------------------------------- |
++| `OV_OAUTH_CLIENT_ID` | OAuth client / principal id (default `games`) |
++| `OV_OAUTH_CLIENT_SECRET` | Its secret: enables SSO and every service call |
++| `OV_NETWORK_INTERNAL_URL` | Network base for `/oauth/token`, identity and the JWKS (e.g. `http://127.0.0.1:4000`) |
++| `EVENTS_URL` | OpenVibe.Events base; enables the outbox (`EVENTS_PUBLISH=off` disables) |
++| `MEDIA_URL` | OpenVibe.Media base; enables the asset mirror (`MEDIA_MIRROR=off` disables) |
++| `MEDIA_NAMESPACE` | Media tenant for the copies (default `games`) |
++| `WORLD_SAVED_EVENT_MINUTES` | Checkpoint event interval (default 15) |
++
++Grants the `games` principal needs in OpenVibe.Network:
++`events.event.publish` (openvibe.events), `media.object.upload`
++(openvibe.media, namespace `games`), `identity.subject.resolve`
++(openvibe.network).
ddiff --git a/ROADMAP.md b/ROADMAP.md
iindex 5c3d89c..3c43a71 100644
---- a/ROADMAP.md
++++ b/ROADMAP.md
@@@ -151,6 +151,17 @@ with live world reconciliation.
→ extraction → sell → blueprint → restart-verified persistence)
- Hostile-client test sweep across every network action
++## Platform integration (network roadmap Wave 12) — partial
++
++- done: canonical Network subjects as account keys + legacy adoption;
++ `games` service principal; durable events via outbox; Media copies of
++ map-editor assets; mod manifest validation, registry (install / grant /
++ enable / disable / revoke, audit) and the capability-checked runtime seam
++ for declarative `games-content@1` packs
++- waiting on the platform: mod principals and grants in OpenVibe.Network;
++ sandboxed executable mods (OpenVibe.Host Stage C); `games.progress.summary`
++ user-module writes are granted but not implemented
++
## Standing engineering rules
- The slice test must stay green; new systems extend it
ddiff --git a/deploy/openvibe-games.service b/deploy/openvibe-games.service
iindex 0b12269..608a25d 100644
---- a/deploy/openvibe-games.service
++++ b/deploy/openvibe-games.service
@@@ -1,6 +1,8 @@
# OpenVibe.Games — authoritative game server (Scraplandia) + portal static host.
# Install: cp deploy/openvibe-games.service /etc/systemd/system/ && systemctl daemon-reload
# Env secrets (OV_OAUTH_CLIENT_SECRET, EDITOR_KEY, ...) live in /etc/openvibe/games.env (0600).
++# Platform (optional, see README "Platform integration"): OV_NETWORK_INTERNAL_URL=http://127.0.0.1:4000,
++# EVENTS_URL=http://127.0.0.1:4300, MEDIA_URL=http://127.0.0.1:4100 (MEDIA_NAMESPACE=games) in the same file.
[Unit]
Description=OpenVibe.Games server (Scraplandia)
After=network.target
ddiff --git a/docs/adr/0004-interim-token-identity.md b/docs/adr/0004-interim-token-identity.md
iindex d48688a..bf45f14 100644
---- a/docs/adr/0004-interim-token-identity.md
++++ b/docs/adr/0004-interim-token-identity.md
@@@ -1,6 +1,6 @@
# ADR-0004: Interim identity = client-generated persistent token
--**Status:** accepted (explicitly temporary) · **Date:** 2026-08-09
++**Status:** accepted (explicitly temporary) · **Date:** 2026-08-09 · signed-in accounts: superseded by ADR-0006 (guest tokens stay, restricted to the client-minted format)
## Decision
ddiff --git a/docs/adr/0006-canonical-subjects-and-platform-boundary.md b/docs/adr/0006-canonical-subjects-and-platform-boundary.md
nnew file mode 100644
iindex 0000000..b99728b
---- /dev/null
++++ b/docs/adr/0006-canonical-subjects-and-platform-boundary.md
@@@ -0,0 +1,44 @@
++# ADR-0006: Canonical Network subjects, and the platform boundary
++
++**Status:** accepted · **Date:** 2026-09-22 · partly supersedes ADR-0004
++
++## Decision
++
++1. **Identity.** A signed-in account's characters are keyed by its canonical
++ openvibe.network subject (`usr_…`, or `gst_…` for a Network guest), read
++ from the token's `subject_id` claim once the Network has accepted the
++ token (`/api/auth/me`). The subject is stored as the account key
++ (`players.token`) and in `players.subject_id`.
++ Accounts created earlier were keyed `ovn:<network user id>`. They are
++ adopted into the subject key on their next sign-in, or in bulk by
++ `apps/server/scripts/migrateIdentity.ts` (Network identity service,
++ `identity.subject.resolve`). Adoption is idempotent, keeps every player id
++ (friends, prop ownership and trust keep working), never overwrites a slot
++ the subject already uses, and records `legacy_key -> subject` in
++ `identity_legacy_map`. A token older than subjects keeps the legacy key.
++2. **Guests** stay local (ADR-0004), but a guest token must be 8–64 of
++ `[A-Za-z0-9_-]` and may not start with a subject prefix (`usr_`, `gst_`,
++ …); the client mints 32 alphanumerics. Before this, any 8–64 character
++ token was accepted, so a guest could present `ovn:<id>` and open that
++ account's characters; now a token shaped like an account key is refused.
++3. **Service calls** to other OpenVibe services carry a client-credentials
++ token of the `games` principal (one per audience). No shared keys.
++4. **Platform adapters live in `apps/server/src/platform` and
++ `apps/server/src/mods`**, outside the simulation packages: durable events
++ through a transactional outbox, Media copies of map assets, and the mod
++ registry and runtime seam. The gameplay packages are unchanged; the
++ persistence package gained repositories and a `transaction()` so a flush
++ and the events describing it commit together.
++
++## Why
++
++The platform identifies people by subject (roadmap §16.6, ADR-001 in
++OpenVibe.Contracts); the Network's integer ids are a service-local detail.
++Keying by subject is also what lets events, Media owners and mod grants name
++the same person Games does.
++
++## Consequences
++
++- A character's account key changes once; its player id never does.
++- Characters left under a legacy key because of a slot conflict stay
++ reachable under that key and are reported by the migration.