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
Changes
@@@ -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 everyinteraction (gather, place, physgun grab/unfreeze), zone rules, ownershiprecorded 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.@@@ -84,7 +84,76 @@ STATIC_DIR=apps/client/dist PORT=8000 DB_PATH=data/world.db pnpm --filter @openvThe server serves the built client, `/healthz`, `/metrics`, and the gameWebSocket 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).@@@ -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@@@ -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@@@ -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)## Decisionnnew file mode 100644@@@ -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.
@@ -93,8 +93,19 @@ SQLite (WAL) behind `PersistenceStore` repositories: `world_entities`
@@ -93,8 +93,19 @@ SQLite (WAL) behind `PersistenceStore` repositories: `world_entities`
(props/resources with motion + kind-specific state JSON), `players` (token,
(props/resources with motion + kind-specific state JSON), `players` (token,
transform, inventory JSON), `meta` (schema version, world-seeded flag).
transform, inventory JSON), `meta` (schema version, world-seeded flag).
Dirty tracking batches writes; flush on interval, on settle, and on shutdown.
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
## Zones
@@ -121,4 +132,6 @@ entity context fields and never spam per-tick.
@@ -121,4 +132,6 @@ entity context fields and never spam per-tick.
Assume hostile clients: protocol validation at the edge, range checks on every
Assume hostile clients: protocol validation at the edge, range checks on every
interaction (gather, place, physgun grab/unfreeze), zone rules, ownership
interaction (gather, place, physgun grab/unfreeze), zone rules, ownership
recorded on placement, inventory ops validated against authoritative state,
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.
@@ -84,7 +84,76 @@ STATIC_DIR=apps/client/dist PORT=8000 DB_PATH=data/world.db pnpm --filter @openv
@@ -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
The server serves the built client, `/healthz`, `/metrics`, and the game
WebSocket on one port. Environment: `PORT`, `HOST`, `DB_PATH`, `STATIC_DIR`,
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,
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).
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).
@@ -151,6 +151,17 @@ with live world reconciliation.
@@ -151,6 +151,17 @@ with live world reconciliation.
→ extraction → sell → blueprint → restart-verified persistence)
→ extraction → sell → blueprint → restart-verified persistence)
- Hostile-client test sweep across every network action
- 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
## Standing engineering rules
- The slice test must stay green; new systems extend it
- The slice test must stay green; new systems extend it
@@ -1,6 +1,8 @@
@@ -1,6 +1,8 @@
# OpenVibe.Games — authoritative game server (Scraplandia) + portal static host.
# OpenVibe.Games — authoritative game server (Scraplandia) + portal static host.
# Install: cp deploy/openvibe-games.service /etc/systemd/system/ && systemctl daemon-reload
# 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).
# 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]
[Unit]
Description=OpenVibe.Games server (Scraplandia)
Description=OpenVibe.Games server (Scraplandia)
After=network.target
After=network.target
@@ -1,6 +1,6 @@
@@ -1,6 +1,6 @@
# ADR-0004: Interim identity = client-generated persistent token
# 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
## Decision
new file mode 100644
new file mode 100644
@@ -0,0 +1,44 @@
@@ -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.