Skip to content

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

  1. Parse arguments and initialise tracing.
  2. Create a FrameManager with the configured dust allocation.
  3. If --brand-wasm is provided:
  4. Validate the URL against url_policy (reject http:// unless --dev).
  5. Load the WASM bytes from disk or network.
  6. Instantiate a wasmtime Component from the bytes.
  7. Wrap it in a BrandComponentService and register it with the FrameManager.
  8. Start the event bridge (frame ticks to broadcast channel).
  9. Optionally start the log writer task.
  10. Optionally start the timeout task.
  11. 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]
  1. After on_frame_active, the host calls drain-signals on the WASM component.
  2. Each SignalEnvelope is wrapped in a StreamEvent with event = schema key and path = "/brand/signal".
  3. The StreamEvent is sent to a tokio::sync::broadcast channel.
  4. WebSocket clients connected to /stream receive every event.
  5. If --log-dir is configured, a separate task writes events to rolling JsonLines files.