World Runner
world-runner is the server-side simulation host. It boots a FrameManager, optionally loads a brand WASM component, and exposes an HTTP + WebSocket API for clock control and signal streaming.
Quick Start
# Build with brand-plugin support
cargo build -p plant_world --features server,brand-plugin --bin plant_world_runner
# Run with a brand component
./target/debug/plant_world_runner \
--brand-wasm path/to/my_brand_component.wasm \
--dev
Command-Line Flags
| Flag | Default | Description |
|---|---|---|
--bind <ADDR> |
127.0.0.1:8080 |
HTTP bind address. |
--brand-wasm <URL_OR_PATH> |
— | Path or URL to a brand WASM component. Accepts file paths, file:// URLs, and https:// URLs. http:// only in --dev mode. |
--dust <N> |
10000 |
Initial dust allocation for the FrameManager. |
--dev |
false |
Development mode. Permits http:// URLs and relaxed policy checks. Never use in production. |
--timeout <SECONDS> |
— | Auto-exit after N seconds. Useful for CI and test harnesses. |
--log-dir <PATH> |
— | Directory for rolling frame log chunks (JsonLines format). |
Boot Sequence
- Parse arguments and initialise tracing.
- Create a
FrameManagerwith the configured dust allocation. - If
--brand-wasmis provided: - Validate the URL against
url_policy(rejecthttp://unless--dev). - Load the WASM bytes from disk or network.
- Instantiate a wasmtime
Componentfrom the bytes. - Wrap it in a
BrandComponentServiceand register it with theFrameManager. - Start the event bridge (frame ticks to broadcast channel).
- Optionally start the log writer task.
- Optionally start the timeout task.
- Bind the HTTP server and begin accepting connections.
URL Policy
The --brand-wasm flag enforces a URL security policy:
| Scheme | --dev off |
--dev on |
|---|---|---|
| Plain path | Allowed | Allowed |
file:// |
Allowed | Allowed |
https:// |
Not yet supported | Not yet supported |
http:// |
Rejected | Allowed |
Remote loading (https://) is planned but not yet implemented. For now, pass a local file path or file:// URL.
HTTP API
GET /status
Returns the current clock status as JSON.
curl http://127.0.0.1:8080/status
{
"frame_id": 42,
"phase": "Idle",
"running": true,
"interval_ms": 100
}
POST /clock
Dispatch a clock action. The request body is JSON with an action field.
Actions
| Action | Extra Fields | Description |
|---|---|---|
"step" |
— | Advance one frame immediately. |
"pause" |
— | Pause the automatic interval. |
"resume" |
— | Resume the automatic interval. |
"set_interval" |
"interval_ms": u64 |
Set the tick interval in milliseconds. |
"status" |
— | Return status without mutation. |
Example: step one frame
curl -X POST http://127.0.0.1:8080/clock \
-H 'Content-Type: application/json' \
-d '{"action": "step"}'
Example: start auto-ticking at 50ms
curl -X POST http://127.0.0.1:8080/clock \
-H 'Content-Type: application/json' \
-d '{"action": "set_interval", "interval_ms": 50}'
GET /stream
WebSocket upgrade. Streams StreamEvent JSON messages for every frame tick and brand signal.
websocat ws://127.0.0.1:8080/stream
Each message is a JSON object:
{
"event": "counter.tick.v1",
"path": "/brand/signal",
"value": { "count": 1 }
}
| Field | Type | Description |
|---|---|---|
event |
string |
The signal schema key (e.g. "racing.signal.v1") or a system event name. |
path |
string |
Routing path. Brand signals use "/brand/signal". |
value |
object |
The JSON payload. |
GET /log-dir
Returns the configured log directory path, or 404 if not configured.
Feature Flags
world-runner is built from the plant_world crate with feature flags:
| Feature | Purpose |
|---|---|
server |
Enables the HTTP server, axum, tokio runtime. Required for world-runner. |
brand-plugin |
Enables wasmtime, wit-bindgen, and the BrandComponentService loader. |
Without brand-plugin, the binary compiles and runs but does not accept --brand-wasm.
# Minimal build (no brand support)
cargo build -p plant_world --features server --bin plant_world_runner
# Full build (with brand loading)
cargo build -p plant_world --features server,brand-plugin --bin plant_world_runner
How Brand Signals Reach /stream
flowchart LR
A[Brand Component] -->|drain-signals| B[BrandComponentService]
B -->|StreamEvent| C[broadcast::Sender]
C --> D[/stream WebSocket]
C --> E[Log Writer]
- After
on_frame_active, the host callsdrain-signalson the WASM component. - Each
SignalEnvelopeis wrapped in aStreamEventwithevent= schema key andpath="/brand/signal". - The
StreamEventis sent to atokio::sync::broadcastchannel. - WebSocket clients connected to
/streamreceive every event. - If
--log-diris configured, a separate task writes events to rolling JsonLines files.