Go to file
Steve Biedermann a18297d1a1 step 2026-10-01 21:02:55 +02:00
.github/agents step 2026-09-28 02:53:41 +02:00
agent-api step 2026-10-01 21:02:55 +02:00
core-api step 2026-10-01 21:02:55 +02:00
default-core step 2026-10-01 21:02:55 +02:00
example-wasm-agent step 2026-10-01 21:02:55 +02:00
src step 2026-10-01 21:02:55 +02:00
web step 2026-10-01 21:02:55 +02:00
.dockerignore add: simulation core 2026-09-30 12:26:37 +02:00
.gitignore step 2026-09-29 14:22:38 +02:00
Cargo.lock step 2026-10-01 21:02:55 +02:00
Cargo.toml step 2026-10-01 21:02:55 +02:00
Dockerfile step 2026-10-01 21:02:55 +02:00
README.md step 2026-10-01 21:02:55 +02:00
compose.yaml step 2026-09-30 10:47:33 +02:00
docker-entrypoint.sh step 2026-09-30 10:59:05 +02:00

README.md

Wolloseum

Wolloseum is a deterministic, procedurally generated game server. Players upload untrusted WASM agents that control spawned agents in a tick-based simulation. Agent code runs in a sandboxed Extism/Wasmtime runtime with fuel and memory limits.

Identity is based on Ed25519 keypairs. The server stores public keys; private keys stay with their owners. HTTPS and WSS termination are intended to be provided by a reverse proxy such as nginx or Caddy. The Wolloseum process serves HTTP and does not create TLS certificates.

Build and Run

cargo build --release
cargo run --release -- serve --data-dir data --bind 127.0.0.1:8080

Docker

Build and start the server with Docker Compose:

docker compose up --build -d
docker compose logs -f wolloseum

The HTTP service is published on 127.0.0.1:8080; configure nginx or Caddy to proxy HTTP and WebSocket traffic to that address and terminate TLS. Compose stores the SQLite database and generated administrator keypair in ./data. The entrypoint sets the data tree's ownership before starting the server as an unprivileged user.

Set WOLLOSEUM_UID and WOLLOSEUM_GID in the environment or a Compose .env file to choose the container process identity. Both default to 1000. The ownership of the host ./data tree is changed to these IDs at startup.

The first startup logs the administrator identity ID and identity bundle path inside the container. Copy the bundle to a secure host location for admin sign-in:

docker compose cp wolloseum:/var/lib/wolloseum/keys/admin-identity.json /secure/path/admin-identity.json
chmod 600 /secure/path/admin-identity.json

Back up the key and database volume. To stop the server while preserving data, run docker compose down. To remove all persistent identity and game data as well, run docker compose down --volumes.

The server bootstraps an administrator keypair on first start:

  • data/keys/admin-private.pem is the owner-only Ed25519 PKCS#8 private key.
  • data/keys/admin-identity.json is the owner-only bundle used by browser sign-in.
  • The server prints the corresponding identity ID at startup.
  • Keep a secure backup of the private key. The server cannot recover it.

Existing installations continue to use data/certificates/admin-client-key.pem and the old certificate fingerprint as the admin identity ID when those legacy files are present. Existing player identities and uploaded data remain in the SQLite database. HTTPS certificates, if needed, belong to the reverse proxy.

Configuration flags and environment variables:

Flag Environment variable Default Purpose
--data-dir WOLLOSEUM_DATA_DIR data Keypairs and SQLite database directory
--bind WOLLOSEUM_BIND 127.0.0.1:8080 HTTP listener, usually private to the proxy

Web Interfaces

  • / is the player console. Every identity, including moderators and administrators, can act as a player and upload or replace its own agent WASM.
  • /admin is the privileged console. Moderators manage player accounts; administrators can also manage roles, simulation settings and world state, and replace the core WASM module.
  • /viewer is a public live event viewer. It connects to /api/events, tracks event sequence numbers, and restores state from snapshots after reconnecting.

The admin console generates a fresh Ed25519 keypair when creating a user. The server receives only the public key. The browser downloads a JSON identity bundle containing the new identity ID and private key; deliver that bundle to the user through a secure channel. The private key is not stored by the server. Administrators can promote users to moderator or administrator roles.

Sign in on either page by selecting the JSON identity bundle. Protect data/keys/admin-identity.json like the private key; the older PEM key remains available for CLI compatibility.

Browser Authentication

  1. The browser reads the identity ID and private key from the selected JSON bundle.
  2. The browser requests POST /api/auth/challenge with { "client_id": "..." }.
  3. The server returns a random, short-lived challenge.
  4. The browser signs the decoded challenge bytes using Ed25519 and posts the identity ID, challenge, and base64 signature to POST /api/auth/login.
  5. The server verifies the signature against the stored public key and returns an opaque session token.
  6. The browser stores the token and sends it as Authorization: Bearer <token> on protected API requests.

Challenges expire after five minutes and are single-use. Session tokens expire seven days after the last authenticated API interaction. Each authenticated request extends the expiry. Only a SHA-256 hash of each token is persisted. Revoking an identity by deleting it also removes its sessions.

API

Method Path Access Purpose
POST /api/auth/challenge Public Request an identity challenge
POST /api/auth/login Public Verify a signed challenge and issue a token
GET /api/me Session Read the signed-in identity and agent status
GET /api/users Mod or admin List identities and agent digests
GET /api/status Public Read the simulation running state
GET /api/settings Admin session Read simulation calendar, activation, and tick interval
PUT /api/settings Admin session Update simulation calendar, activation, and tick interval
GET /api/state Admin session Download the current state bundle
POST /api/state Admin session Validate and restore a state bundle
POST /api/reset_world Admin session Regenerate the world while preserving other records
GET /api/events Public WebSocket Receive snapshots and ordered state events
POST /api/create_user Mod or admin Register a username and base64 Ed25519 public key
POST /api/delete_user Mod or admin Delete a player and its uploaded agent
POST /api/set_role Admin session Assign player, moderator, or administrator role
POST /api/start_game Admin session Resume the simulation
POST /api/stop_game Admin session Pause the simulation
POST /api/update_agent User session Queue or replace the caller's next agent WASM
POST /api/upload_core Admin session Upload or replace the core simulation WASM

Agent and core WASM uploads are limited to 1 MiB and validated in the sandboxed runtime before they are persisted. Existing signed request headers remain accepted for compatibility with older clients.

Agent uploads replace that player's queued module; the active module remains in use until the selected update boundary. The admin console configures ticks per day, days per week and month, and whether queued agents activate between ticks, days, weeks, or months. Defaults are 86400 ticks per day, 7 days per week, 30 days per month, activation between ticks, and a 100 ms delay between ticks. The delay can be set from 1 to 60000 milliseconds.

State downloads contain the current world, simulation settings, run state, user public identities, active and queued agent modules, and core module. They do not contain private keys or login sessions. Restoring a bundle requires it to retain the active administrator performing the restore. Resetting creates a fresh random world while preserving settings, users, agents, and the active core.

Rust APIs

The agent-api crate defines typed observations, actions, and the Agent trait. The core-api crate defines serialized tick input/output and the Core trait. default-core/ implements Core; example-wasm-agent/ implements Agent and adapts it to the Extism ABI. Both API crates are workspace members and can be used as dependencies when authoring new implementations.

The server uses the native Rust implementation in default-core/ until a core is uploaded. An uploaded core replaces it and must export an Extism tick function that accepts CoreTickInput JSON (world and actions) and returns CoreTickOutput JSON (world). Build the default implementation as a WASM core with:

cargo build --manifest-path default-core/Cargo.toml \
  --target wasm32-unknown-unknown --release

Upload target/wasm32-unknown-unknown/release/default_core.wasm from the admin console to run that same implementation through the WASM path.

CLI

The binary also provides an authenticated client. New installations use the admin keypair directly:

cargo run --release -- cli \
  --client-key data/keys/admin-private.pem \
  create-user alice

create-user prints the identity ID and private key as JSON. For commands run as a player, use that player's private key and identity ID:

cargo run --release -- cli \
  --client-id <identity-id> \
  --client-key alice-private.pem \
  update-agent example-wasm-agent/target/wasm32-unknown-unknown/release/example_wasm_agent.wasm

Legacy installations can pass --client-cert and --client-key to derive the historical certificate-based admin ID. The CLI exchanges a challenge for a session token before making protected requests.

Example Agent

example-wasm-agent/ is a standalone crate implementing the Extism plugin ABI used by src/wasm_agent.rs. Build it with:

cd example-wasm-agent
rustup target add wasm32-unknown-unknown
cargo build --release --target wasm32-unknown-unknown

The compiled module is at example-wasm-agent/target/wasm32-unknown-unknown/release/example_wasm_agent.wasm.