Platform overview
Self-hostable platform for running simulators, games and custom environments for AI agents: drive them from Python, watch and play them in the browser, record and replay episodes. Runs on your own GPU machine or cluster.
Built on Simulusion, the world-models library.
Run it with Docker
Needs Docker (on Windows and macOS, Docker Desktop).
docker compose --profile build build # the platform and the example container env
docker compose up -d # http://localhost:8000PLATFORM_PORT=8080 docker compose up -d serves on another port. Recordings and
the preview cache are kept in ./data. For GPU environments add
-f compose.gpu.yaml (see the file). docker compose down stops it.
Run it from source
Needs uv and Node.js 20+.
uv sync
cd web && npm install && npm run build && cd .. # the web app, once
uv run platform-server --port 8000Open http://127.0.0.1:8000: start a session, play it with the arrow keys and
Space (or a random agent), save and restore snapshots, record episodes and
replay them. Recordings land in ./recordings in Simulusion's episode format;
previews are cached in ~/.cache/simulusion-platform. See
web/README.md for developing the web app.
From Python, with the server running:
import platform_sdk
with platform_sdk.connect("cartpole", url="http://127.0.0.1:8000") as env:
obs, info = env.reset(seed=0)
obs, reward, terminated, truncated, info = env.step(env.action_space.sample())
handle = env.snapshot() # save the full state
obs, reward, *_ = env.step(1)
obs, info = env.restore(handle) # and go back to itUse your GPU machines
Connect the GPU machines you already have, at any provider (RunPod, Vast.ai, Lambda, AWS, GCP, Azure, your own servers), over SSH, and run training and evaluation experiments on them from the platform: a repository and a command on a number of GPUs, with live logs. On the Machines page, give the machine the platform's SSH key and paste the SSH command your provider shows; then Experiments → New experiment. Nothing is installed on the machines and Docker is not needed.
Or put the platform on the GPU server itself and open it through an SSH tunnel:
python tools/gpu_server.py deploy root@203.0.113.7 -p 22045 # then http://localhost:8000A machine the internet cannot reach (behind NAT, a laptop, WSL) connects to the platform instead, through a reverse tunnel. See docs/gpu-machines.md.
Run agents
Agents run on your machines and play the platform's environments, with every
step recorded. Any Python object with start and act is an agent
(platform_sdk.agents), and the platform serves the client package a machine
needs as a wheel (GET /v0/wheels/platform-sdk). See
docs/agents.md to connect your own.
Evaluate models
Run Inspect evaluations of your models, as
agents, on your machines, from Evals → New eval: pick an Inspect task
(for example inspect_evals/cybench) and point it at your model. Scores with
error bars, per-sample results and full transcripts come back to the platform.
See docs/evals.md.
Run it on Kubernetes
The Helm chart in deploy/helm/simulusion-platform runs the API server and
starts one pod per session, so sessions spread over the cluster and GPU
environments land on GPU nodes:
helm install platform deploy/helm/simulusion-platform --namespace platform --create-namespace --set image.repository=registry.example.com/simulusion-platform --set platform.auth=oidc --set platform.oidc.issuer=https://login.example.com/realms/acme --set platform.oidc.clientId=simulusion-platform --set database.existingSecret=platform-db --set recordings.location=s3://recordings/platformSee values.yaml for the rest: GPU node selectors, warm pools, sign-in,
the network policy for environment pods. deploy/kind/e2e.sh installs it on a local kind
cluster and drives sessions through it.
Configuration
Settings come from PLATFORM_* environment variables (the chart sets them):
| Variable | Default | What it does |
|---|---|---|
PLATFORM_MODE |
self-hosted |
managed: many teams, each seeing only its own sessions, recordings and usage |
PLATFORM_AUTH |
none |
token (API tokens) or oidc (your identity provider, plus API tokens for scripts) |
PLATFORM_ADMIN_TOKEN |
Bootstraps the first admin | |
PLATFORM_OIDC_ISSUER, _CLIENT_ID, _AUDIENCE |
The identity provider; the web app signs in with PKCE | |
PLATFORM_OIDC_ROLE_MAP |
Groups to roles, e.g. platform-admins=admin |
|
PLATFORM_DATABASE_URL |
sqlite:///platform.db |
Postgres in production: postgresql+psycopg://... |
PLATFORM_RECORDINGS |
recordings |
A folder, or s3://bucket/prefix (with PLATFORM_S3_ENDPOINT for MinIO) |
PLATFORM_IDLE_TIMEOUT |
1800 |
Seconds without use before a session is stopped; 0 keeps sessions until closed |
PLATFORM_POOLS |
Warm instances per environment, e.g. cartpole=2,pong=1 |
|
PLATFORM_LAUNCHER |
local |
kubernetes: a pod per session (PLATFORM_KUBE_*) |
PLATFORM_SSH_KEY |
~/.config/simulusion-platform/ssh/id_ed25519 (/data/ssh/ in Docker) |
The platform's SSH key for GPU machines, made on first use |
PLATFORM_METRICS_TOKEN |
Guards /metrics |
|
PLATFORM_LOG_FORMAT |
text |
json for log pipelines |
Admins see usage (instance-hours per environment, CSV for invoicing) and
manage API tokens, members and quotas on the Usage and Team pages, or through
/v0/usage, /v0/tokens, /v0/members and /v0/teams. From Python, pass a
token as platform_sdk.connect(..., token=...) or PLATFORM_TOKEN.
Environments
The built-in catalog has classic control (CartPole, Acrobot, Mountain Car,
Pendulum), Atari (Pong, Breakout), all 51 DeepMind Control tasks, Gymnasium's
11 MuJoCo tasks (Ant, Humanoid, Walker2d, ...), robots from the MuJoCo Menagerie
(humanoids that stand and walk, arms that reach) and Doom through ViZDoom
(Basic, Defend the Center, Health Gathering, Deathmatch, and Freedoom's first
level). catalog.toml adds more: any Gymnasium id, a Python factory, or a
Docker image serving the environment.
Menagerie robots download on first use, 6 to 72 MB each; run
python -m platform_sdk.menagerie to fetch them ahead of time. The Docker image
already has them.
In the web player, environments with several continuous actuators get a named slider for each; the values hold from step to step while it plays.
In the web app, an environment's Options button sets all of the following for one session. The session page and the replay list its world changes, and the replay shows true values, the teacher's suggestions and success frame by frame.
Control rate. substeps sets how many physics steps each decision covers on
Menagerie robots and DeepMind Control tasks, for one session or, through a
catalog entry's kwargs, for every session of an environment. Atari has
frameskip, Gymnasium's MuJoCo tasks frame_skip and ViZDoom tics. The
session's spec reports the resulting rate.
World changes. A session can follow a timeline of changes, which the agent experiences but isn't told about:
sim = platform_sdk.connect("menagerie-franka-emika-panda-reach", kwargs={
"substeps": 100, # 5 decisions a second
"world_changes": [
{"at": 5000, "change": "mass", "body": "hand", "scale": 2.0},
{"at": 5000, "change": "sensor_noise", "std": 0.01, "indices": [0, 1, 2]},
{"at": 9000, "change": "mass", "body": "hand", "scale": 1.0},
],
})at counts the session's steps across episodes, and changes last through
resets. The kinds are mass, friction, motor_strength, position,
sensor_offset and sensor_noise; world_changes.py
lists their fields. Add "reveal": true to tell the agent about a change in
that step's info. Each change is logged, saved in the recording's
recording.json and listed at GET /v0/sessions/{id}/world-changes. Snapshots
include them, so branches replay exactly.
Agent data. An agent can send named numbers with any step: its confidence, what it spent on the decision, whether it refused to answer. They are recorded beside the step and shown on the replay page:
env.step(action, agent={"confidence": 0.82, "compute_s": 0.013, "refused": False})The same agent field works on POST /v0/sessions/{id}/step and the stream's
step message. Values are numbers, booleans (stored as 0 and 1) or lists of up
to 256 numbers; at most 32 names per step, of letters, digits, _, . and -.
A name keeps the shape of its first value for the whole recording. Each episode
file stores them as agent.<name> (float64, one row per step, NaN where that
step had none), and meta.json lists them under agent_keys.
Tasks that define success report info["is_success"] on every step. Recordings
keep it as info.is_success (one bool per step), or leave it out of an episode
where some steps did not report it.
Benchmarks will read these names: compute_s (wall time spent deciding),
refused (1 when the agent declined to answer), settle_steps and
wait_steps (an agent's own measure of work per step).
Hand control. Menagerie arms can be commanded by the hand instead of joint by joint, with inverse kinematics and workspace limits inside the environment:
sim = platform_sdk.connect("menagerie-franka-emika-panda-reach", kwargs={
"control": "hand_steps", # or "hand" for continuous
"hand_step": 0.02, # metres per decision, at most
"hand_levels": 5, # choices per axis: odd, the middle is "stay"
"workspace": 0.3, # metres from home along each axis
})
sim.action_space # MultiDiscrete([5, 5, 5, 2]): x, y, z, gripperThe catalog's Panda and xArm7 "Reach, by hand" entries use the stepped mode. In the web player
the arrow keys move the hand across the table, W and S move it up and down, and
Space opens or closes the gripper. Any environment with a MultiDiscrete action
gets a row of level buttons per choice.
Pick and place. The Panda and the xArm7 have a pick_place task: put the
block in the bin under the green ball. It runs at 5 decisions a second, and info["is_success"]
says when the block is at rest in the right bin, released. With hand control,
info["teacher_action"] is a scripted expert's suggestion for the next action;
the web player's Teacher button plays it. Recordings save each suggestion as
teacher.action and teacher.valid, beside info.is_success.
Labelled observations. Environments that define get_observation_labels
(Menagerie and DeepMind Control do) name the parts of their observation, such as
hand_position, joint_angles and target_from_hand. sim.labelled(obs) splits
an observation by name, the session API returns labels from reset and step, and
the web player shows them. They are what the agent sees, so scheduled sensor
changes affect them. Recordings save the true values as state.<name> arrays.
Bring your own: envs/byo-example is a complete custom environment, as a Python factory and as a container. Before hosting an environment, check it:
uv run platform-check --factory my_module:make_env --path ./my_envs
uv run platform-check --image my-org/my-simulator:latestIt checks the spec, observations against their space, stepping, invalid actions, seeding, snapshots and frames, and exits 1 if anything fails.
Atari environments do sticky actions in Python rather than inside ALE, because
ALE's saved state leaves out the previous action and restored runs could
diverge. Dynamics match gym.make("ALE/...-v5"); trajectories are not bit for
bit identical to it. See sdk/python/src/platform_sdk/atari.py.
Doom snapshots replay the episode from its seed rather than loading a save
game: a loaded Doom game plays on differently depending on what the engine did
before the load. Restoring takes as long as replaying the episode so far. See
sdk/python/src/platform_sdk/doom.py.
Doom: how episodes end, actions, your own maps.
-
The last step of an episode says how it ended in
info["outcome"]:died,exit(a whole level's exit reached),completed(a scenario's own goal, e.g.basic's monster shot) ortimeout. Recordings keep it per episode. The reset'sinfo["variable_names"]names the observed variables; whole levels reportHEALTH,ARMOR,SELECTED_WEAPON_AMMOandKILLCOUNT.variablesadds any ViZDoom game variable after those, e.g.kwargs={"variables": ["POSITION_X", "POSITION_Y", "ANGLE"]}for the player's position (map units, 64 to a floor tile) and facing (degrees), say for a teacher planning a route while the agent only sees the screen. -
labels=Trueaddsobs["labels"], ViZDoom's per-pixel object map ((H, W)uint8; 0 and 1 are walls and floors), and each step'sinfo["labels"]lists the objects in view:value(as in the map),id,name(e.g.MarineChainsawVzd), bounding boxx,y,width,heightand mapposition.depth=Trueaddsobs["depth"]((H, W)uint8, larger is further). Recordings keep the screen, not these maps. -
action_set="full"gives every scenario the same 23 actions in the same order: the 18 combinations of forward/back, turn left/right and fire, thenUSE,STRAFELEFT,STRAFERIGHT,NEXTWEAPONandPREVWEAPON. Buttons a scenario lacks are added, so some become easier than standard. The default,action_set="scenario", is each scenario's own menu, whereLEFTandRIGHTturn (strafe onbasic). In the web player F is use, Q/E strafe and Z/X switch weapons. -
Your own maps: upload a WAD (at most 20 MB) and play its maps on top of Freedoom, on any Doom environment. Sessions name it by id; only your team sees it.
pythonwad = platform_sdk.upload_wad("memory_test.wad") # POST /v0/doom/wads env = platform_sdk.connect("freedoom-e1m1", kwargs={ "wad": wad["id"], "doom_map": "MAP01", "action_set": "full", })The server keeps WADs under
PLATFORM_WADS(a folder ors3://...) and gives each session a copy on its own disk, so custom maps work where environments run as local processes; not yet in containers or Kubernetes pods.
How it fits together
browser ─┐ ┌─ process python -m platform_sdk
├─ HTTP/WebSocket ─▶ platform-server ┼─ process (built-in, factories)
Python ──┘ session API, broker, web app └─ container (images from catalog.toml)
gRPC: environment protocol v0.1- Environment protocol (protocol/env.proto): Spec,
Reset, Step, Snapshot, Restore, Render, Close. One server hosts one
environment instance:
python -m platform_sdk --factory my_module:make_env. - Launcher (
platform_sdk/launch.py): starts an instance as a process or a container; the broker andplatform-checkshare it. - Broker (
platform_server/broker.py): one instance per session, calls serialized; crashed instances become HTTP 410 with their log tail; idle ones are stopped (after 30 minutes without use, by default). - Session API (
platform_server/app.py):/v0/envs,/v0/sessions, per-sessionreset,step,snapshot,restore,render, a WebSocket stream, recordings and replay. Interactive docs at/docs.
Updated Nov 10, 2026