step
This commit is contained in:
parent
591bbc701e
commit
ce8bdcf09d
|
|
@ -1 +1,2 @@
|
||||||
/target
|
target/
|
||||||
|
/data
|
||||||
File diff suppressed because it is too large
Load Diff
12
Cargo.toml
12
Cargo.toml
|
|
@ -4,9 +4,21 @@ version = "0.1.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
axum = { version = "0.8", features = ["ws"] }
|
||||||
|
axum-server = { version = "0.8", default-features = false, features = ["tls-rustls-no-provider"] }
|
||||||
|
base64 = "0.22"
|
||||||
|
clap = { version = "4.6.7", features = ["derive", "env"] }
|
||||||
extism = { version = "1.30", default-features = false, features = ["wasmtime-default-features"] }
|
extism = { version = "1.30", default-features = false, features = ["wasmtime-default-features"] }
|
||||||
|
rcgen = { version = "0.14", features = ["x509-parser"] }
|
||||||
|
reqwest = { version = "0.13", default-features = false, features = ["rustls-no-provider"] }
|
||||||
|
ring = "0.17"
|
||||||
|
rusqlite = { version = "0.40", features = ["bundled"] }
|
||||||
|
rustls = { version = "0.23", default-features = false, features = ["ring", "std", "tls12"] }
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
uuid = { version = "1", features = ["v4"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
tempfile = "3"
|
||||||
wat = "1"
|
wat = "1"
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,161 @@
|
||||||
|
# Wolloseum
|
||||||
|
|
||||||
|
Wolloseum is a deterministic, procedurally generated game server. Players
|
||||||
|
upload untrusted WASM agents (run in a sandboxed Extism/Wasmtime runtime with
|
||||||
|
fuel and memory limits) that control spawned agents in a tick-based
|
||||||
|
simulation. All identity and request authentication is certificate-based
|
||||||
|
(Ed25519 client certificates signed by a locally issued root CA); there are no
|
||||||
|
passwords or bearer tokens.
|
||||||
|
|
||||||
|
## Building the server
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cargo build --release
|
||||||
|
```
|
||||||
|
|
||||||
|
The binary is produced at `target/release/wolloseum`.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
The server takes its configuration from CLI flags (each with an environment
|
||||||
|
variable fallback), defined in [src/cli.rs](src/cli.rs):
|
||||||
|
|
||||||
|
| Flag | Env var | Default | Purpose |
|
||||||
|
| ------------ | -------------------- | ---------------- | -------------------------------------------------- |
|
||||||
|
| `--data-dir` | `WOLLOSEUM_DATA_DIR` | `data` | Directory for certificates and the SQLite database |
|
||||||
|
| `--bind` | `WOLLOSEUM_BIND` | `127.0.0.1:8080` | HTTP listener address |
|
||||||
|
|
||||||
|
On first start the server bootstraps a PKI under `<data-dir>/certificates/`:
|
||||||
|
|
||||||
|
- `root-ca.pem` / `root-ca-key.pem` — self-signed root CA
|
||||||
|
- `server.pem` / `server-key.pem` — HTTP server certificate (issued for `localhost`)
|
||||||
|
- `admin-client.pem` / `admin-client-key.pem` — the initial admin client identity
|
||||||
|
|
||||||
|
The SQLite database is created at `<data-dir>/wolloseum.sqlite3` and stores
|
||||||
|
identities, used request nonces, per-player agent WASM, and the core
|
||||||
|
simulation WASM module. Private key files and the certificate directory are
|
||||||
|
written with restrictive (owner-only) permissions on Unix.
|
||||||
|
|
||||||
|
## Running the server
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cargo run --release -- serve --data-dir data --bind 127.0.0.1:8080
|
||||||
|
```
|
||||||
|
|
||||||
|
On startup the server prints the admin certificate ID and the paths to the
|
||||||
|
admin client certificate/key and root CA certificate — you'll need these to
|
||||||
|
issue authenticated CLI/API requests.
|
||||||
|
|
||||||
|
## API endpoints
|
||||||
|
|
||||||
|
All `/api/*` endpoints except `/api/status` and `/api/events` require request
|
||||||
|
signing. Each signed request must include:
|
||||||
|
|
||||||
|
```
|
||||||
|
X-Client-Id: <sha256 hex of the client's certificate PEM>
|
||||||
|
X-Request-Timestamp: <unix seconds, within 60s of server time>
|
||||||
|
X-Request-Nonce: <unique, ASCII alphanumeric/-/_, replay-checked>
|
||||||
|
X-Request-Signature: <base64 Ed25519 signature>
|
||||||
|
```
|
||||||
|
|
||||||
|
The signed message is `METHOD\nPATH\nTIMESTAMP\nNONCE\n` followed by the raw
|
||||||
|
request body bytes.
|
||||||
|
|
||||||
|
| Method | Path | Auth | Description |
|
||||||
|
| ------ | ------------------- | ------------------- | ----------------------------------------------------------- |
|
||||||
|
| GET | `/` | none | Player web UI |
|
||||||
|
| GET | `/admin` | none | Admin web UI |
|
||||||
|
| GET | `/api/status` | none | Current game running state |
|
||||||
|
| GET | `/api/events` | none (WebSocket) | Live game/agent/core state change events |
|
||||||
|
| POST | `/api/create_user` | admin | Create a player identity, returns certificate + private key |
|
||||||
|
| POST | `/api/delete_user` | admin | Delete a player identity and their uploaded agent |
|
||||||
|
| POST | `/api/start_game` | admin | Resume the simulation |
|
||||||
|
| POST | `/api/stop_game` | admin | Pause the simulation |
|
||||||
|
| POST | `/api/update_agent` | any active identity | Upload/replace the caller's agent WASM module |
|
||||||
|
| POST | `/api/upload_core` | admin | Upload/replace the core simulation WASM module |
|
||||||
|
|
||||||
|
### `/api/upload_core`
|
||||||
|
|
||||||
|
Replaces the core simulation logic module. The request body is the raw WASM
|
||||||
|
bytes (not JSON), following the same conventions as `/api/update_agent`:
|
||||||
|
|
||||||
|
- Body limit: 1 MiB (`Content-Length` over the limit is rejected)
|
||||||
|
- The module is sandboxed-loaded (no WASI imports, bounded memory) to confirm
|
||||||
|
it is valid before being persisted
|
||||||
|
- Requires admin authentication (signed request headers as above)
|
||||||
|
- On success the module is stored in the `core_module` table (single active
|
||||||
|
row) and a `core_uploaded` event is published over `/api/events`
|
||||||
|
|
||||||
|
```sh
|
||||||
|
curl -k http://127.0.0.1:8080/api/upload_core \
|
||||||
|
-X POST \
|
||||||
|
--data-binary @core_simulation.wasm \
|
||||||
|
-H "x-client-id: $ADMIN_ID" \
|
||||||
|
-H "x-request-timestamp: $TS" \
|
||||||
|
-H "x-request-nonce: $NONCE" \
|
||||||
|
-H "x-request-signature: $SIGNATURE"
|
||||||
|
```
|
||||||
|
|
||||||
|
In practice, use the CLI (below) rather than constructing signatures by hand.
|
||||||
|
|
||||||
|
## Using the CLI client
|
||||||
|
|
||||||
|
The same binary provides a signing HTTP client:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cargo run --release -- cli \
|
||||||
|
--server http://127.0.0.1:84080\
|
||||||
|
--root-ca data/certificates/root-ca.pem \
|
||||||
|
--client-cert data/certificates/admin-client.pem \
|
||||||
|
--client-key data/certificates/admin-client-key.pem \
|
||||||
|
status
|
||||||
|
```
|
||||||
|
|
||||||
|
Available subcommands (see [src/cli.rs](src/cli.rs)): `status`,
|
||||||
|
`create-user <username>`, `delete-user <client_id>`, `stop-game`,
|
||||||
|
`start-game`, `update-agent <path>`.
|
||||||
|
|
||||||
|
## Building the example WASM agent
|
||||||
|
|
||||||
|
[example-wasm-agent/](example-wasm-agent) is a standalone crate implementing
|
||||||
|
a minimal Extism plugin agent matching the ABI expected by
|
||||||
|
[src/wasm_agent.rs](src/wasm_agent.rs) (a JSON `decide` call taking
|
||||||
|
`{abi_version, tick, x, y, adjacent_terrain}` and returning
|
||||||
|
`{abi_version, action}`). Movement is a deterministic function of the tick
|
||||||
|
and position — no RNG, clock, or host access is used, so replays are
|
||||||
|
reproducible.
|
||||||
|
|
||||||
|
Build it to a `.wasm` file:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cd example-wasm-agent
|
||||||
|
rustup target add wasm32-unknown-unknown # if not already installed
|
||||||
|
cargo build --release --target wasm32-unknown-unknown
|
||||||
|
```
|
||||||
|
|
||||||
|
The compiled module is at
|
||||||
|
`example-wasm-agent/target/wasm32-unknown-unknown/release/example_wasm_agent.wasm`.
|
||||||
|
|
||||||
|
## Uploading the example agent
|
||||||
|
|
||||||
|
First create a player identity (as admin), then upload the agent WASM signed
|
||||||
|
with that player's own certificate:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# As admin, create a player and save the returned certificate/private key:
|
||||||
|
cargo run --release -- cli \
|
||||||
|
--client-cert data/certificates/admin-client.pem \
|
||||||
|
--client-key data/certificates/admin-client-key.pem \
|
||||||
|
create-user alice
|
||||||
|
# -> write the returned certificate_pem/private_key_pem to alice.pem / alice-key.pem
|
||||||
|
|
||||||
|
# As alice, upload the compiled agent:
|
||||||
|
cargo run --release -- cli \
|
||||||
|
--client-cert alice.pem \
|
||||||
|
--client-key alice-key.pem \
|
||||||
|
update-agent example-wasm-agent/target/wasm32-unknown-unknown/release/example_wasm_agent.wasm
|
||||||
|
```
|
||||||
|
|
||||||
|
Core simulation WASM can be uploaded the same way by an admin using
|
||||||
|
`POST /api/upload_core` (no dedicated CLI subcommand yet — use `curl` as shown
|
||||||
|
above, or extend the CLI client following the same pattern as `update-agent`).
|
||||||
|
|
@ -0,0 +1,390 @@
|
||||||
|
# This file is automatically @generated by Cargo.
|
||||||
|
# It is not intended for manual editing.
|
||||||
|
version = 4
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anyhow"
|
||||||
|
version = "1.0.104"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "autocfg"
|
||||||
|
version = "1.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "base64"
|
||||||
|
version = "0.22.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bytemuck"
|
||||||
|
version = "1.25.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bytes"
|
||||||
|
version = "1.12.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "either"
|
||||||
|
version = "1.18.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "equivalent"
|
||||||
|
version = "1.0.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "example-wasm-agent"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"extism-pdk",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "extism-convert"
|
||||||
|
version = "1.30.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ad19858c4c462309a8f3a20abec53e8603bda1eefda26c8bfab51d5516b40cbb"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"base64",
|
||||||
|
"bytemuck",
|
||||||
|
"extism-convert-macros",
|
||||||
|
"prost",
|
||||||
|
"rmp-serde",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "extism-convert-macros"
|
||||||
|
version = "1.30.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5f2932799f6d9f9646f97b65287f6bb2addc75a0ee61e40fb24559a7540dd928"
|
||||||
|
dependencies = [
|
||||||
|
"manyhow",
|
||||||
|
"proc-macro-crate",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.119",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "extism-manifest"
|
||||||
|
version = "1.30.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e2f59c8dadb5e0bde9a48c6ed45312e6ef625cbcd5f67c28459dbc8fe8bc0383"
|
||||||
|
dependencies = [
|
||||||
|
"base64",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "extism-pdk"
|
||||||
|
version = "1.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "352fcb5a66eb74145a1c4a01f2bd15d59c62c85be73aac8471880c65b26b798f"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"base64",
|
||||||
|
"extism-convert",
|
||||||
|
"extism-manifest",
|
||||||
|
"extism-pdk-derive",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "extism-pdk-derive"
|
||||||
|
version = "1.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d086daea5fd844e3c5ac69ddfe36df4a9a43e7218cf7d1f888182b089b09806c"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.119",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hashbrown"
|
||||||
|
version = "0.17.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "indexmap"
|
||||||
|
version = "2.14.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855"
|
||||||
|
dependencies = [
|
||||||
|
"equivalent",
|
||||||
|
"hashbrown",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "itertools"
|
||||||
|
version = "0.14.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
|
||||||
|
dependencies = [
|
||||||
|
"either",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "itoa"
|
||||||
|
version = "1.0.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "manyhow"
|
||||||
|
version = "0.11.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b33efb3ca6d3b07393750d4030418d594ab1139cee518f0dc88db70fec873587"
|
||||||
|
dependencies = [
|
||||||
|
"manyhow-macros",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.119",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "manyhow-macros"
|
||||||
|
version = "0.11.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "46fce34d199b78b6e6073abf984c9cf5fd3e9330145a93ee0738a7443e371495"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro-utils",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "memchr"
|
||||||
|
version = "2.8.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-traits"
|
||||||
|
version = "0.2.19"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
||||||
|
dependencies = [
|
||||||
|
"autocfg",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro-crate"
|
||||||
|
version = "3.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f"
|
||||||
|
dependencies = [
|
||||||
|
"toml_edit",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro-utils"
|
||||||
|
version = "0.10.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "eeaf08a13de400bc215877b5bdc088f241b12eb42f0a548d3390dc1c56bb7071"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"smallvec",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro2"
|
||||||
|
version = "1.0.107"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "prost"
|
||||||
|
version = "0.14.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "528ac67416ff8646872a3c02cad9cc4ee5dc9f9540c9b10771855c95cb2e5ae1"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"prost-derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "prost-derive"
|
||||||
|
version = "0.14.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b570b25f7617e43d59005d0990ccb79e950a423952cea19671b7a876da390adf"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"itertools",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.119",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quote"
|
||||||
|
version = "1.0.47"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rmp"
|
||||||
|
version = "0.8.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4ba8be72d372b2c9b35542551678538b562e7cf86c3315773cae48dfbfe7790c"
|
||||||
|
dependencies = [
|
||||||
|
"num-traits",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rmp-serde"
|
||||||
|
version = "1.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "72f81bee8c8ef9b577d1681a70ebbc962c232461e397b22c208c43c04b67a155"
|
||||||
|
dependencies = [
|
||||||
|
"rmp",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde"
|
||||||
|
version = "1.0.229"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
|
||||||
|
dependencies = [
|
||||||
|
"serde_core",
|
||||||
|
"serde_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_core"
|
||||||
|
version = "1.0.229"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
|
||||||
|
dependencies = [
|
||||||
|
"serde_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_derive"
|
||||||
|
version = "1.0.229"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 3.0.6",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_json"
|
||||||
|
version = "1.0.151"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
|
||||||
|
dependencies = [
|
||||||
|
"itoa",
|
||||||
|
"memchr",
|
||||||
|
"serde",
|
||||||
|
"serde_core",
|
||||||
|
"zmij",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "smallvec"
|
||||||
|
version = "1.16.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f9395f0f0eee849a9b707b2f06bb92a6a422090e2123bb2ef8e87a0e61892a8e"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "syn"
|
||||||
|
version = "2.0.119"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "syn"
|
||||||
|
version = "3.0.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_datetime"
|
||||||
|
version = "1.1.1+spec-1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7"
|
||||||
|
dependencies = [
|
||||||
|
"serde_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_edit"
|
||||||
|
version = "0.25.15+spec-1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1340ea94a5856333492c9064b02c778b191dd2c853778d9609debdcdfea3a614"
|
||||||
|
dependencies = [
|
||||||
|
"indexmap",
|
||||||
|
"toml_datetime",
|
||||||
|
"toml_parser",
|
||||||
|
"winnow",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_parser"
|
||||||
|
version = "1.1.3+spec-1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56"
|
||||||
|
dependencies = [
|
||||||
|
"winnow",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-ident"
|
||||||
|
version = "1.0.26"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winnow"
|
||||||
|
version = "1.0.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zmij"
|
||||||
|
version = "1.0.23"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
[package]
|
||||||
|
name = "example-wasm-agent"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
crate-type = ["cdylib"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
extism-pdk = "1"
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
opt-level = "z"
|
||||||
|
lto = true
|
||||||
|
codegen-units = 1
|
||||||
|
panic = "abort"
|
||||||
|
strip = true
|
||||||
|
|
@ -0,0 +1,65 @@
|
||||||
|
use extism_pdk::*;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
const ABI_VERSION: u16 = 1;
|
||||||
|
const BOUNDARY_TERRAIN: i32 = 3;
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct DecisionInput {
|
||||||
|
#[allow(dead_code)]
|
||||||
|
abi_version: u16,
|
||||||
|
tick: u64,
|
||||||
|
x: u16,
|
||||||
|
y: u16,
|
||||||
|
adjacent_terrain: [i32; 4],
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct DecisionOutput {
|
||||||
|
abi_version: u16,
|
||||||
|
action: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deterministic decision function called once per simulation tick.
|
||||||
|
///
|
||||||
|
/// The host provides the full observation as JSON input and expects a JSON
|
||||||
|
/// decision back; no filesystem, network, clock, or RNG access is available.
|
||||||
|
/// Movement here is a reproducible function of (tick, x, y) alone, so replay
|
||||||
|
/// with the same seed and inputs always yields the same actions.
|
||||||
|
#[plugin_fn]
|
||||||
|
pub fn decide(input: Json<DecisionInput>) -> FnResult<Json<DecisionOutput>> {
|
||||||
|
let input = input.0;
|
||||||
|
|
||||||
|
// Action codes: 1=North, 2=East, 3=South, 4=West, matching adjacent_terrain order.
|
||||||
|
let open_directions: Vec<i32> = input
|
||||||
|
.adjacent_terrain
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter(|(_, terrain)| **terrain != BOUNDARY_TERRAIN)
|
||||||
|
.map(|(index, _)| (index + 1) as i32)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let action = if open_directions.is_empty() {
|
||||||
|
0 // Wait
|
||||||
|
} else {
|
||||||
|
let seed = deterministic_seed(input.tick, input.x, input.y);
|
||||||
|
open_directions[(seed as usize) % open_directions.len()]
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Json(DecisionOutput {
|
||||||
|
abi_version: ABI_VERSION,
|
||||||
|
action,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// SplitMix64-style mix so the same (tick, x, y) always yields the same choice.
|
||||||
|
fn deterministic_seed(tick: u64, x: u16, y: u16) -> u64 {
|
||||||
|
let mut state = tick
|
||||||
|
.wrapping_mul(0x9E37_79B9_7F4A_7C15)
|
||||||
|
.wrapping_add(u64::from(x).wrapping_mul(0xBF58_476D_1CE4_E5B9))
|
||||||
|
.wrapping_add(u64::from(y).wrapping_mul(0x94D0_49BB_1331_11EB));
|
||||||
|
state ^= state >> 33;
|
||||||
|
state = state.wrapping_mul(0xFF51_AFD7_ED55_8CCD);
|
||||||
|
state ^= state >> 33;
|
||||||
|
state
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,220 @@
|
||||||
|
use std::error::Error;
|
||||||
|
use std::fs;
|
||||||
|
use std::net::SocketAddr;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
|
use base64::Engine as _;
|
||||||
|
use base64::engine::general_purpose::STANDARD as BASE64;
|
||||||
|
use clap::{Args, Parser, Subcommand};
|
||||||
|
use rcgen::KeyPair;
|
||||||
|
use reqwest::{Client, Method};
|
||||||
|
use ring::signature::Ed25519KeyPair;
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
#[derive(Debug, Parser)]
|
||||||
|
#[command(
|
||||||
|
name = "wolloseum",
|
||||||
|
version,
|
||||||
|
about = "Wolloseum game server and client"
|
||||||
|
)]
|
||||||
|
pub struct Arguments {
|
||||||
|
#[command(subcommand)]
|
||||||
|
command: TopLevelCommand,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Subcommand)]
|
||||||
|
enum TopLevelCommand {
|
||||||
|
/// Start the HTTP game server.
|
||||||
|
Serve(ServeArguments),
|
||||||
|
/// Send authenticated requests to a Wolloseum server.
|
||||||
|
Cli(ClientArguments),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Args)]
|
||||||
|
struct ServeArguments {
|
||||||
|
/// Directory for certificates and the SQLite database.
|
||||||
|
#[arg(long, env = "WOLLOSEUM_DATA_DIR", default_value = "data")]
|
||||||
|
data_dir: PathBuf,
|
||||||
|
/// HTTP listener address.
|
||||||
|
#[arg(long, env = "WOLLOSEUM_BIND", default_value = "127.0.0.1:8080")]
|
||||||
|
bind: SocketAddr,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Args)]
|
||||||
|
struct ClientArguments {
|
||||||
|
/// HTTP base URL for the server.
|
||||||
|
#[arg(
|
||||||
|
long,
|
||||||
|
global = true,
|
||||||
|
env = "WOLLOSEUM_SERVER",
|
||||||
|
default_value = "http://127.0.0.1:8080"
|
||||||
|
)]
|
||||||
|
server: String,
|
||||||
|
/// Client certificate whose PEM fingerprint is the identity ID.
|
||||||
|
#[arg(
|
||||||
|
long,
|
||||||
|
global = true,
|
||||||
|
env = "WOLLOSEUM_CLIENT_CERT",
|
||||||
|
default_value = "data/certificates/admin-client.pem"
|
||||||
|
)]
|
||||||
|
client_cert: PathBuf,
|
||||||
|
/// Private key for the selected client certificate.
|
||||||
|
#[arg(
|
||||||
|
long,
|
||||||
|
global = true,
|
||||||
|
env = "WOLLOSEUM_CLIENT_KEY",
|
||||||
|
default_value = "data/certificates/admin-client-key.pem"
|
||||||
|
)]
|
||||||
|
client_key: PathBuf,
|
||||||
|
#[command(subcommand)]
|
||||||
|
command: ClientCommand,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Subcommand)]
|
||||||
|
enum ClientCommand {
|
||||||
|
/// Read the current game status.
|
||||||
|
Status,
|
||||||
|
/// Create a user and print their certificate and private key as JSON.
|
||||||
|
CreateUser { username: String },
|
||||||
|
/// Delete a player by certificate ID.
|
||||||
|
DeleteUser { client_id: String },
|
||||||
|
/// Pause the game.
|
||||||
|
StopGame,
|
||||||
|
/// Resume the game.
|
||||||
|
StartGame,
|
||||||
|
/// Upload or update this client identity's Extism WASM agent.
|
||||||
|
UpdateAgent { path: PathBuf },
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn run(arguments: Arguments) -> Result<(), Box<dyn Error>> {
|
||||||
|
match arguments.command {
|
||||||
|
TopLevelCommand::Serve(arguments) => {
|
||||||
|
crate::server::run(arguments.data_dir, arguments.bind).await
|
||||||
|
}
|
||||||
|
TopLevelCommand::Cli(arguments) => run_client(arguments).await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn run_client(arguments: ClientArguments) -> Result<(), Box<dyn Error>> {
|
||||||
|
let client = Client::builder().build()?;
|
||||||
|
|
||||||
|
let (method, path, body, needs_signature) = match arguments.command {
|
||||||
|
ClientCommand::Status => (Method::GET, "/api/status", Vec::new(), false),
|
||||||
|
ClientCommand::CreateUser { username } => (
|
||||||
|
Method::POST,
|
||||||
|
"/api/create_user",
|
||||||
|
serde_json::to_vec(&CreateUserBody { username })?,
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
ClientCommand::DeleteUser { client_id } => (
|
||||||
|
Method::POST,
|
||||||
|
"/api/delete_user",
|
||||||
|
serde_json::to_vec(&DeleteUserBody { client_id })?,
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
ClientCommand::StopGame => (Method::POST, "/api/stop_game", Vec::new(), true),
|
||||||
|
ClientCommand::StartGame => (Method::POST, "/api/start_game", Vec::new(), true),
|
||||||
|
ClientCommand::UpdateAgent { path } => {
|
||||||
|
(Method::POST, "/api/update_agent", fs::read(path)?, true)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let url = format!("{}{}", arguments.server.trim_end_matches('/'), path);
|
||||||
|
let mut request = client.request(method.clone(), url).body(body.clone());
|
||||||
|
if needs_signature {
|
||||||
|
let certificate_pem = fs::read_to_string(&arguments.client_cert)?;
|
||||||
|
let key = KeyPair::from_pem(&fs::read_to_string(&arguments.client_key)?)?;
|
||||||
|
let signer = Ed25519KeyPair::from_pkcs8(&key.serialize_der())
|
||||||
|
.map_err(|_| "client private key is not a valid Ed25519 PKCS#8 key")?;
|
||||||
|
let client_id = certificate_id(&certificate_pem);
|
||||||
|
let timestamp = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs();
|
||||||
|
let nonce = uuid::Uuid::new_v4().to_string();
|
||||||
|
let message = signature_message(method.as_str(), path, timestamp, &nonce, &body);
|
||||||
|
let signature = BASE64.encode(signer.sign(&message).as_ref());
|
||||||
|
request = request
|
||||||
|
.header("x-client-id", client_id)
|
||||||
|
.header("x-request-timestamp", timestamp.to_string())
|
||||||
|
.header("x-request-nonce", nonce)
|
||||||
|
.header("x-request-signature", signature);
|
||||||
|
}
|
||||||
|
|
||||||
|
let response = request.send().await?;
|
||||||
|
let status = response.status();
|
||||||
|
let body = response.bytes().await?;
|
||||||
|
if !status.is_success() {
|
||||||
|
return Err(format!(
|
||||||
|
"server returned {status}: {}",
|
||||||
|
String::from_utf8_lossy(&body)
|
||||||
|
)
|
||||||
|
.into());
|
||||||
|
}
|
||||||
|
if body.is_empty() {
|
||||||
|
println!("HTTP {status}");
|
||||||
|
} else if let Ok(json) = serde_json::from_slice::<serde_json::Value>(&body) {
|
||||||
|
println!("{}", serde_json::to_string_pretty(&json)?);
|
||||||
|
} else {
|
||||||
|
println!("{}", String::from_utf8_lossy(&body));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct CreateUserBody {
|
||||||
|
username: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct DeleteUserBody {
|
||||||
|
client_id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn signature_message(
|
||||||
|
method: &str,
|
||||||
|
path: &str,
|
||||||
|
timestamp: u64,
|
||||||
|
nonce: &str,
|
||||||
|
body: &[u8],
|
||||||
|
) -> Vec<u8> {
|
||||||
|
let mut message = format!("{method}\n{path}\n{timestamp}\n{nonce}\n").into_bytes();
|
||||||
|
message.extend_from_slice(body);
|
||||||
|
message
|
||||||
|
}
|
||||||
|
|
||||||
|
fn certificate_id(certificate_pem: &str) -> String {
|
||||||
|
ring::digest::digest(&ring::digest::SHA256, certificate_pem.as_bytes())
|
||||||
|
.as_ref()
|
||||||
|
.iter()
|
||||||
|
.map(|byte| format!("{byte:02x}"))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::Arguments;
|
||||||
|
use clap::Parser;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn serve_and_cli_are_explicit_top_level_commands() {
|
||||||
|
assert!(Arguments::try_parse_from(["wolloseum"]).is_err());
|
||||||
|
assert!(Arguments::try_parse_from(["wolloseum", "serve"]).is_ok());
|
||||||
|
assert!(Arguments::try_parse_from(["wolloseum", "cli", "status"]).is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cli_can_select_user_credentials_for_upload() {
|
||||||
|
assert!(
|
||||||
|
Arguments::try_parse_from([
|
||||||
|
"wolloseum",
|
||||||
|
"cli",
|
||||||
|
"--client-cert",
|
||||||
|
"player.pem",
|
||||||
|
"--client-key",
|
||||||
|
"player-key.pem",
|
||||||
|
"update-agent",
|
||||||
|
"agent.wasm",
|
||||||
|
])
|
||||||
|
.is_ok()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,2 +1,4 @@
|
||||||
|
pub mod cli;
|
||||||
|
pub mod server;
|
||||||
pub mod simulation;
|
pub mod simulation;
|
||||||
pub mod wasm_agent;
|
pub mod wasm_agent;
|
||||||
|
|
|
||||||
27
src/main.rs
27
src/main.rs
|
|
@ -1,25 +1,6 @@
|
||||||
use std::collections::BTreeMap;
|
use clap::Parser;
|
||||||
use wolloseum::simulation::{Action, AgentId, World};
|
|
||||||
|
|
||||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
#[tokio::main]
|
||||||
let mut world = World::generate(0x574F4C4C, 32, 32)?;
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let agent_id = AgentId(1);
|
wolloseum::cli::run(wolloseum::cli::Arguments::parse()).await
|
||||||
world.spawn_agent(agent_id)?;
|
|
||||||
world.step(&BTreeMap::from([(agent_id, Action::Wait)]))?;
|
|
||||||
|
|
||||||
let (grass, water, rock) = world.terrain_counts();
|
|
||||||
let agent = world.agent(agent_id).expect("agent was just spawned");
|
|
||||||
println!(
|
|
||||||
"Wolloseum seed={} map={}x{} tick={} terrain=({} grass, {} water, {} rock) agent={} at {:?}",
|
|
||||||
world.seed(),
|
|
||||||
world.dimensions().0,
|
|
||||||
world.dimensions().1,
|
|
||||||
world.tick(),
|
|
||||||
grass,
|
|
||||||
water,
|
|
||||||
rock,
|
|
||||||
agent.id().0,
|
|
||||||
agent.position(),
|
|
||||||
);
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -7,7 +7,7 @@ use serde::{Deserialize, Serialize};
|
||||||
use crate::simulation::{Action, AgentObservation, Direction, ObservedTerrain};
|
use crate::simulation::{Action, AgentObservation, Direction, ObservedTerrain};
|
||||||
|
|
||||||
const MAX_MODULE_BYTES: usize = 1_048_576;
|
const MAX_MODULE_BYTES: usize = 1_048_576;
|
||||||
const MAX_MEMORY_PAGES: u32 = 16;
|
const MAX_MEMORY_PAGES: u32 = 32;
|
||||||
pub const FUEL_PER_DECISION: u64 = 100_000;
|
pub const FUEL_PER_DECISION: u64 = 100_000;
|
||||||
pub const AGENT_ABI_VERSION: u16 = 1;
|
pub const AGENT_ABI_VERSION: u16 = 1;
|
||||||
|
|
||||||
|
|
@ -147,7 +147,7 @@ mod tests {
|
||||||
})
|
})
|
||||||
.collect::<String>();
|
.collect::<String>();
|
||||||
let memory_check = if check_memory_limit {
|
let memory_check = if check_memory_limit {
|
||||||
"(local.set $previous (memory.grow (i32.const 16))) (local.get $previous) (i32.const -1) (i32.ne) (if (then (unreachable)))"
|
"(local.set $previous (memory.grow (i32.const 64))) (local.get $previous) (i32.const -1) (i32.ne) (if (then (unreachable)))"
|
||||||
} else {
|
} else {
|
||||||
""
|
""
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue