Skip to content

Search guides, algorithms and the platform.

PlatformPlatform overview

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).

Terminal
docker compose --profile build build     # the platform and the example container env
docker compose up -d                     # http://localhost:8000

PLATFORM_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+.

Terminal
uv sync
cd web && npm install && npm run build && cd ..   # the web app, once
uv run platform-server --port 8000

Open 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:

python
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 it

Use 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:

Terminal
python tools/gpu_server.py deploy root@203.0.113.7 -p 22045   # then http://localhost:8000

A 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:

Terminal
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/platform

See 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:

python
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:

python
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:

python
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, gripper

The 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:

Terminal
uv run platform-check --factory my_module:make_env --path ./my_envs
uv run platform-check --image my-org/my-simulator:latest

It 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) or timeout. Recordings keep it per episode. The reset's info["variable_names"] names the observed variables; whole levels report HEALTH, ARMOR, SELECTED_WEAPON_AMMO and KILLCOUNT. variables adds 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=True adds obs["labels"], ViZDoom's per-pixel object map ((H, W) uint8; 0 and 1 are walls and floors), and each step's info["labels"] lists the objects in view: value (as in the map), id, name (e.g. MarineChainsawVzd), bounding box x, y, width, height and map position. depth=True adds obs["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, then USE, STRAFELEFT, STRAFERIGHT, NEXTWEAPON and PREVWEAPON. Buttons a scenario lacks are added, so some become easier than standard. The default, action_set="scenario", is each scenario's own menu, where LEFT and RIGHT turn (strafe on basic). 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.

    python
    wad = 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 or s3://...) 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

Code
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 and platform-check share 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-session reset, step, snapshot, restore, render, a WebSocket stream, recordings and replay. Interactive docs at /docs.

Updated Nov 10, 2026