The transport is kept outside FastSim core. A FastSim installation that does not start this package pays no HTTP, serialization, network, or per-physics-tick cost. The server does not implement simulation behavior and never imports a simulator backend directly.

Install from this repository

Use the same Python environment as FastSim:

bash
python -m pip install ./packages/fastsim-plugin-server
fastsim-server --help

Install the optional real-time camera transport when using the browser workbench:

bash
python -m pip install './packages/fastsim-plugin-server[webrtc]'

Install the optional IK service when remote clients or Agents need kinematics:

bash
python -m pip install './packages/fastsim-plugin-server[kinematics]'

Verify that the optional provider and a software video encoder are usable:

bash
python -c "from fastsim_plugin_server.media import select_media_transport; print(select_media_transport('webrtc').active_transport)"

The command must print webrtc. The base installation does not import or probe aiortc or PyAV until media selection is requested, so installations that do not enable browser video retain the same startup and simulation hot path.

--media-mode webrtc requires that extra and fails with the exact installation command when it is unavailable. --media-mode snapshot explicitly uses JPEG frame requests. --media-mode auto advertises and warns about any compatibility fallback. Run state still uses the persistent WebSocket session in every media mode.

Package version 0.8.4 supports FastSim >=0.1.0a19,<0.2 and retains websockets>=12,<17. The bounded range follows the stable fastsim-http/1 and public Application contracts across compatible Core alpha releases while keeping the next minor line as an explicit review boundary. See the changelog. Source-tree versioning does not imply that a package has been published to PyPI.

Start one Run

bash
fastsim-server run.yaml

The default listener is http://127.0.0.1:8000. The process uses one Uvicorn worker and one FastSim application loop. Reload and multiple workers are not available because two processes must never claim the same simulator Run. Without --launch-root, this positional form preserves the established non-replaceable Run behavior. Adding an explicit launch root opts the initial Run into the replaceable persistent slot.

Planning scene, frame, and geometry publication is opt-in:

bash
fastsim-server run.yaml --planning-reads

Persistent launch service

An independently hosted Web Client can connect to a persistent server that starts without an initial Run. The operator exposes only named configuration roots:

bash
fastsim-server \
  --launch-root examples=/srv/fastsim/runs \
  --browser-origin http://127.0.0.1:8080

GET /api/v1/launch reports the slot state, generation, public root aliases, accepted formats, and exact upload byte limit. Launch an allowlisted server configuration with:

bash
curl -sS -X POST http://127.0.0.1:8000/api/v1/run/launch \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: launch-demo-001' \
  -d '{
    "schema": "fastsim-run-launch/1",
    "source": {
      "kind": "server_path",
      "root": "examples",
      "config": "kitchen/run.yaml"
    },
    "target_state": "running"
  }'

Or upload one standalone YAML, JSON, or TOML configuration as UTF-8 text:

bash
jq -n --rawfile content ./run.yaml '{
  schema: "fastsim-run-launch/1",
  source: {kind: "upload", filename: "run.yaml", content: $content},
  target_state: "running"
}' | curl -sS -X POST http://127.0.0.1:8000/api/v1/run/launch \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: launch-upload-001' \
  --data-binary @-

Launch is asynchronous. Poll the returned operation URL. After POST /api/v1/run/close completes application and plugin cleanup, the slot returns to idle and can accept another Run. target_state defaults to running; prepared enters and prepares FastSim without starting it, while open only compiles and attaches a lazy application and does not open the simulator.

Dynamic launch also accepts optional "planning_reads": true. It enables scene, frame, and planning-geometry services for that Run only. When omitted, the request inherits the Server's --planning-reads default; without that flag, ordinary camera and control Runs stay lightweight.

Uploaded input is one isolated configuration document. Installed component packages are available, but local companion files, ancestor project discovery, and uploaded lock files are intentionally unavailable. Dynamic server-path launch also does not accept a lock; the established positional fastsim-server run.yaml --lock run.lock.yaml form remains available.

Useful launch options:

Option Meaning
--lock run.lock.yaml Use an immutable resolved Run lock
--launch-root ALIAS=/ABS/PATH Allow configuration selection below one named server directory; repeat as needed
--output-root /ABS/PATH Enable one private, generation-scoped Run output directory and artifact API
--authority exclusive|operator|background|read_only Select one Core-owned control authority
--planning-reads Ask FastSim to publish scene, frame, and geometry services
--timeout SECONDS Set the FastSim deadline and derive HTTP control/query deadlines unless overridden
--application-open-timeout SECONDS Bound lazy FastSim application startup; defaults to --timeout or 120 seconds
--control-timeout SECONDS Override the HTTP control-operation deadline
--query-timeout SECONDS Override the deadline applied to every public read
--body-read-timeout SECONDS Bound receipt of one request body; default 10
--max-in-flight-requests N Bound concurrent HTTP requests; default 256, maximum 10000
--max-buffered-body-bytes N Bound request bodies buffered across all clients; default 32 MiB
--max-binary-requests N Bound concurrent camera, fluid, geometry, and artifact responses; default 4
--host / --port Select the listener; default 127.0.0.1:8000
--trusted-host HOST Accept one exact browser-facing API host; repeat as needed; required for browser access through a wildcard listener
--browser-origin ORIGIN Allow one exact independent Web Client origin; repeat as needed; disabled by default
--media-mode webrtc|snapshot|auto Require WebRTC, request explicit JPEG snapshots, or allow a warned compatibility fallback
--quiet-access-log Suppress repetitive HTTP access lines while retaining FastSim diagnostics

The first lifecycle or control request that owns Run startup opens FastSim lazily; read-only data routes never trigger startup. This step always has an independent hard deadline, even when FastSim itself was created with no explicit --timeout. The launcher also passes a finite timeout no greater than the application-open deadline into the FastSim facade, so Core cleanup and the outer HTTP cancellation boundary agree. Expiry returns 504 application_open_timeout, leaves health and operation history available, and allows a later request to retry startup. The effective deadline is published by discovery and the capabilities document.

Check discovery and the generated API reference after launch:

bash
curl -sS http://127.0.0.1:8000/api/v1 | jq
  • OpenAPI: GET /api/v1/openapi.json
  • Interactive reference: GET /api/v1/docs
  • Static capability declaration: GET /api/v1/capabilities

Request model

Lifecycle and control writes are asynchronous. A successful submission returns 202 Accepted, an operation ID, and a Location header. Poll the operation until state is succeeded, failed, or cancelled.

Scene commands are different: they settle through the public FastSimApplication.scene facade and return their Core settlement directly as fastsim-scene-command-result/1. The Server does not run a second command queue, drag solver, or idempotency cache.

bash
BASE=http://127.0.0.1:8000

START_ID=$(curl -sS -X POST "$BASE/api/v1/run/start" | jq -r .operation_id)
curl -sS "$BASE/api/v1/operations/$START_ID" | jq

Run launch and physical control routes require Idempotency-Key. Replaying the same key with the same request returns the original operation; reusing it with a different request returns 409 Conflict. Other lifecycle routes accept the same header but keep it optional.

For a replaceable slot, every lifecycle mutation after launch and every control write must also send X-FastSim-Slot-Generation with the generation returned by GET /api/v1/launch or /api/v1/run. This precondition is checked under the slot mutation lock before FastSim is touched. A stale client receives 409 stale_slot_generation, so a command prepared for Run 1 cannot close or control a subsequently launched Run 2. The header is not required by the legacy non-replaceable positional or embedded Server.

bash
CONTROL_ID=$(curl -sS -X POST "$BASE/api/v1/control/joint-paths" \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: droid-arm-demo-001' \
  -H 'X-FastSim-Slot-Generation: 1' \
  -d '{
    "actor": "robots.droid",
    "group": "arm",
    "path": [[0.0, -0.4], [0.2, -0.2]],
    "dt": 0.0166666667,
    "preempt": false,
    "timeout_s": 30.0
  }' | jq -r .operation_id)

curl -sS "$BASE/api/v1/operations/$CONTROL_ID" | jq

Accepted work is owned by the Run, not by the TCP connection. Disconnecting a client does not cancel it. Use DELETE /api/v1/operations/{operation_id} for cancellable work.

Endpoint matrix

Discovery and health

Method and path Result
GET /health/live HTTP process liveness; never opens the simulator
GET /health/ready HTTP admission readiness plus application_opened; does not open the simulator
GET /api/v1 API version, links, hard limits, and binary media types
GET /api/v1/capabilities Authority-aware and launch-aware feature declaration
GET /api/v1/metrics/process Demand-sampled CPU, RSS, and optional NVML memory for this Server process
GET /api/v1/launch Run-slot state, generation, source roots, formats, and upload limit
GET /api/v1/openapi.json Authenticated OpenAPI document
GET /api/v1/docs Authenticated interactive API reference

Demand-driven process metrics

GET /api/v1/metrics/process returns one fastsim-process-metrics/1 document for the current Server PID. It remains available when the replaceable Run slot is idle:

json
{
  "schema": "fastsim-process-metrics/1",
  "sampled_at": "2026-08-27T00:00:00Z",
  "pid": 12345,
  "cpu_percent": 17.5,
  "rss_bytes": 536870912,
  "gpu_memory_bytes": null,
  "gpu_memory_available": false,
  "gpu_memory_unavailable_reason": "pynvml_not_installed",
  "gpu_device_count": null
}

cpu_percent is the current Server process CPU time consumed between demand samples divided by elapsed wall time; the first sample is 0.0, and a multithreaded process may exceed 100 percent. rss_bytes is the current process resident set. GPU memory is the sum attributed to the current PID across NVML devices; duplicate compute/graphics views of the same device allocation are not double-counted. If optional pynvml/NVML support is missing or cannot answer the process query, GPU memory is null and gpu_memory_unavailable_reason contains a stable reason code. The Server never launches nvidia-smi.

Realtime channel metrics publishes the same document through fastsim-realtime/1, at 1 Hz by default and at a requested max_hz no greater than 5. Sampling is strictly demand-driven: with no endpoint read and no active metrics subscription, the Server creates no metrics task, reads no process filesystem counters, and does not import or initialize NVML. Closing the last subscription cancels its sampler.

Applied-control realtime stream

Run-scoped realtime channel control_applied exposes the actual frames accepted by the backend. It is strictly demand-driven: the Server opens one shared Core subscribe_applied_frames handle only while at least one WebSocket subscriber exists, with capacity=64, max_item_bytes=1048576, and overload=drop_oldest. Closing the last subscriber, changing the slot generation, a failed launch, or terminal Run stop/close releases the Core handle and sampler. A worker projects at most one ordered, size-bounded reliable batch per sample at no more than 30 Hz; excess stays in the bounded Core queue, whose drop_oldest loss ledger is emitted as explicit gaps. Applied-control batches are history-free at the Server transport: reconnect resume receives stream.reset with history_unavailable instead of a potentially multi-megabyte reliable replay burst.

The value inside a stream.snapshot has this stable shape:

json
{
  "schema": "fastsim-control-applied-batch/1",
  "items": [
    {
      "schema": "fastsim-control-audit/1",
      "kind": "applied_frame",
      "sequence": 7,
      "record_sequence": 11,
      "generation": 2,
      "sim_tick": 17,
      "sim_time_s": 0.2833333333333333,
      "received_monotonic_ns": 123456,
      "estimated_size_bytes": 512,
      "payload": {"schema_version": "fastsim-applied-control-frame/1", "apply_tick": 17}
    }
  ],
  "first_source_sequence": 7,
  "last_source_sequence": 7,
  "item_count": 1
}

payload is the complete canonical applied_frame_to_dict mapping; it is abbreviated above. Core overload loss is never hidden: an item with kind: "gap" retains reason, source/record sequence bounds, tick bounds, item_count, and byte_count. The WebSocket envelope's top-level generation is the Server Run slot generation used for fencing. Each item's generation is the independent Core runtime generation, which can change on reset and need not equal the slot generation.

Run-scoped subscriptions accepted during dynamic launch stay alive across the temporary cold/launch boundary and retry with a bounded cadence. One-shot scene scenario/catalog reads stop only after a successful sample. Resume replay is generation-aware, so history from a previous Run can never suppress a fresh same-content scene snapshot.

Run and Scenario

Method and path Result
GET /api/v1/run Side-effect-free Run metadata; provider identity is added only in a stable Run
GET /api/v1/run/snapshot Public FastSim application snapshot
GET /api/v1/run/artifacts List committed, allowlisted outputs for one exact slot generation
GET /api/v1/run/artifacts/{path} Download one output completely or with a single byte Range
POST /api/v1/run/launch Launch an allowlisted server configuration or uploaded standalone configuration
POST /api/v1/run/prepare Prepare the Run
POST /api/v1/run/start Start simulation
POST /api/v1/run/pause Pause simulation
POST /api/v1/run/resume Resume simulation
POST /api/v1/run/step Advance a bounded number of steps
POST /api/v1/run/reset Reset the Run
POST /api/v1/run/stop Stop runtime and plugin work
POST /api/v1/run/close Terminally close the Run and release application resources
GET /api/v1/scenario Scenario digest and available-section summary
GET /api/v1/scenario/source Frozen authored/resolved Scenario source
GET /api/v1/scenario/scene Frozen scene section
GET /api/v1/scenario/behavior Optional behavior section; 404 when absent
GET /api/v1/scenario/evaluation Optional evaluation section; 404 when absent

Run artifacts are opt-in. Create an operator-owned directory and start the Server with --output-root /absolute/output/root. Each accepted launch generation receives a distinct private child directory; FastSim's trusted run.output@1 policy writes Recorder FSR output there. Other producers must commit their final filename by atomic rename and keep temporary files hidden. The Server installs no watcher or simulation callback and scans only the current private generation when the list endpoint is explicitly called.

Both artifact routes require the exact X-FastSim-Slot-Generation returned by GET /api/v1/launch, including while the slot is idle after terminal close. The next launch immediately fences the previous generation. The allowlist contains FSR, MP4/WebM, JSON/JSONL, CSV, PNG/JPEG, text, log, and Markdown files. Empty, hidden, temporary, unknown, symbolic-link, FIFO, and other special files are not exposed.

bash
GENERATION=1
curl -sS "$BASE/api/v1/run/artifacts" \
  -H "X-FastSim-Slot-Generation: $GENERATION" | jq

curl -sS "$BASE/api/v1/run/artifacts/episode.fsr" \
  -H "X-FastSim-Slot-Generation: $GENERATION" \
  -H 'Range: bytes=0-16777215' \
  -o episode.part

Listings and downloads are bounded by the advertised discovery limits. Paths are resolved beneath pinned directory descriptors with no symlink following. Use terminal run/close before treating plugin output as final: close drains control, lets Recorder and other plugins finalize, and then leaves the completed generation readable until a new Run is launched. The Server never deletes completed generation directories; retention and archival of the operator-owned output root remain deployment responsibilities.

run/close first cancels and drains active control operations, then performs FastSim plugin and application cleanup. In persistent launch mode the slot returns to idle; a non-replaceable embedded application remains closed for backward compatibility. The HTTP operation registry remains available so the caller can poll the close operation. Repeating a close request with the same idempotency key returns the original operation. Process shutdown remains safe after a Run has already been closed.

Only one lifecycle transition is admitted at a time. New lifecycle and control submissions receive 409 lifecycle_transition_in_progress while it runs. Every lifecycle transition closes admission for new data-plane reads and waits boundedly for already admitted reads before touching the simulator. reset, stop, and close additionally cancel and fully drain active control, including the underlying operation release. pause, resume, and step remain usable while control is active.

Raw data-plane reads are admitted only while the lifecycle is stably prepared, running, or paused. Application snapshots, plugin status and events, state/control inspection, camera/fluid frames, and planning reads return the deterministic 503 data_plane_unavailable response while the Run is cold, transitioning, stopped, closed, or failed. A rejected read never lazily opens FastSim and is never queued on its ApplicationEventLoop. Health, operation history, /api/v1/run metadata, capabilities, and immutable Scenario routes remain readable across those states and transitions.

When the persistent slot is empty, /api/v1/run returns present: false and Run-dependent reads and writes fail immediately with 409 run_not_configured.

For Python embedding with manage_application=False, the caller owns an already-entered application. Its stable open state is therefore data-plane readable without being relabeled running; transition, draining, terminal, and failure gates still apply.

Plugins, state, and control

Method and path Result
GET /api/v1/plugins/status Bounded public plugin-host snapshot
GET /api/v1/plugins/{instance}/status Exact public status for one plugin instance plus bounded host context
GET /api/v1/plugins/events Plugin event page; supports sequence, instance, and topic filters
GET /api/v1/state Coherent public World/Observation envelope
GET /api/v1/control/targets Discover actors, groups, axes, command spaces, and controllers
GET /api/v1/control/state Current public control state; optional timeout_s
POST /api/v1/control/joint-paths Submit a joint-position path
POST /api/v1/control/tracks Submit generic physical tracks, including multi-resource chunks
GET /api/v1/operations Paginated operation list; filters: state, kind
GET /api/v1/operations/{operation_id} Operation state, progress, and terminal result
DELETE /api/v1/operations/{operation_id} Request cancellation where supported

Scene, frames, and geometry

These routes need the corresponding public FastSim service. Launch with --planning-reads when the Run needs planning data. Scene commands use Core's command service and do not require planning reads.

Method and path Result
GET /api/v1/scene/catalog Filtered static entity/link/articulation catalog
GET /api/v1/scene/state Filtered dynamic scene state
POST /api/v1/scene/entities/{entity_id}/pose Set one existing entity's world pose
POST /api/v1/scene/entities/{entity_id}/attachments Fix one rigid body or articulation link to another physical endpoint and return attachment_id
POST /api/v1/scene/entities/{entity_id}/attachments/{attachment_id}/detach Remove one attachment without resetting the child articulation
POST /api/v1/scene/entities/{entity_id}/drags Begin one drag transaction and return its drag_id
POST /api/v1/scene/entities/{entity_id}/drags/{drag_id}/updates Move an active drag to a world pose
POST /api/v1/scene/entities/{entity_id}/drags/{drag_id}/end End an active drag
POST /api/v1/scene/entities/{entity_id}/drags/{drag_id}/cancel Cancel an active drag
GET /api/v1/frames/catalog Filtered frame catalog
GET /api/v1/frames/transform?source=...&target=... One public frame transform
GET /api/v1/geometry/capabilities Geometry representations and service limits
GET /api/v1/geometry/catalog Filtered immutable geometry catalog; no large bytes in JSON
GET /api/v1/geometry/transforms Filtered dynamic geometry transforms
GET /api/v1/geometry/resources/{geometry_id} Complete or bounded immutable resource byte range

Kinematics

Method and path Result
GET /api/v1/kinematics/descriptor Selected solver identity, supported model/joint/collision modes, and algorithms
POST /api/v1/kinematics/ik Solve an end-effector pose from the committed current joint state without moving the robot

The beginner request names the robot, base and tip frames, target xyz_m and quat_xyzw, with optional group, tolerances, timeout, collision mode, seed, and solution count. FastSim owns current-state capture, model selection, generation fencing, and provider execution. The HTTP layer only validates and transports the public result. The bundled Portable solver is local DLS and does not claim collision-aware global planning.

Catalog routes accept offset and limit. Frame transforms also accept an optional immutable generation constraint.

Scene commands operate only on an entity already loaded by the Run. They do not spawn assets, solve IK, or add scene semantics. The shortest pose request is:

bash
curl -sS -X POST "$BASE/api/v1/scene/entities/objects.cube/pose" \
  -H 'Content-Type: application/json' \
  -d '{"xyz_m":[0.35,0.0,0.55]}' | jq

The orientation defaults to identity. generation and command_id are both optional: omission binds/generates them in Core at submission; advanced clients may provide them for generation fencing and exact retry idempotency. The HTTP Idempotency-Key header is simply another spelling of command_id; if both are present they must be identical. Replaceable Servers additionally require the independent X-FastSim-Slot-Generation header. A Core rejected settlement is still a successful HTTP 200 response with status: "rejected"; boundary failures preserve Core's error code and command/entity/generation details.

Filters use repeated query parameters and form an intersection. For example:

bash
curl -G "$BASE/api/v1/scene/state" \
  --data-urlencode 'entity_id=robots.droid' \
  --data-urlencode 'entity_id=objects.cube' \
  --data-urlencode 'enabled=true'

curl -G "$BASE/api/v1/geometry/catalog" \
  --data-urlencode 'purpose=collision' \
  --data-urlencode 'motion_class=static' \
  --data-urlencode 'representation=triangle_mesh'

Geometry IDs may contain /. The resource route captures the complete ID. The server closes the scoped FastSim resource lease after every successful or failed read and never returns provider locators, lease tokens, or local file paths. Use the standard single Range: bytes=... header, or the compatible offset and length query parameters, for a range of at most 64 MiB. The two forms cannot be combined:

bash
curl -sS "$BASE/api/v1/geometry/resources/robots.droid/base/collision?offset=0&length=65536" \
  -o geometry.bin

curl -sS -H 'Range: bytes=0-65535' \
  "$BASE/api/v1/geometry/resources/robots.droid/base/collision" -o geometry.bin

Cameras, particle fluids, and deformables

Method and path Media type Layout
GET /api/v1/cameras/{entity_id}/rgb application/vnd.fastsim.rgb24 Packed row-major uint8 RGB24
GET /api/v1/cameras/{entity_id}/png image/png Lossless browser-friendly RGB PNG
GET /api/v1/fluids/{entity_id}/particles application/vnd.fastsim.particle-fluid-f64 Little-endian float64 positions [N,3], then velocities [N,3]
GET /api/v1/deformables/{entity_id}/nodes application/vnd.fastsim.deformable-f64 Little-endian float64 node positions [N,3], then velocities [N,3]

Run ID, entity ID, frame generation, simulation tick/time, shape, dtype, and encoding are returned in X-FastSim-* headers. PNG compression runs only when requested and outside the ASGI event loop. Raw RGB performs no image encoding.

bash
curl -sS "$BASE/api/v1/cameras/sensors.front/png?timeout_s=5" -o front.png
curl -sS "$BASE/api/v1/fluids/fluids.water/particles?timeout_s=5" -o water.f64
curl -sS "$BASE/api/v1/deformables/deformables.cloth/nodes?timeout_s=5" -o cloth.f64

Remote listener security

Loopback is the default. A non-loopback bind requires both TLS and a bearer token of 32 to 4096 RFC 6750 b64token characters stored in a regular file readable only by its owner. The accepted token body is ASCII letters, digits, -._~+/, with optional = padding only at the end; Unicode and control characters are rejected. The TLS private key must also be a non-symlink regular file with mode 0600 (or stricter):

bash
chmod 600 ./fastsim-server.token
chmod 600 ./server.key

fastsim-server run.yaml --planning-reads \
  --host 192.0.2.10 \
  --browser-origin https://console.example \
  --token-file ./fastsim-server.token \
  --tls-cert-file ./server.crt \
  --tls-key-file ./server.key

Send Authorization: Bearer ... on every request. Authentication also protects health, OpenAPI, and interactive documentation. Tokens are not accepted inline, in cookies, or from Run configuration.

Cross-origin browser access is disabled by default. An independently hosted static Web Client is enabled with one or more repeated exact origins:

bash
fastsim-server run.yaml \
  --browser-origin http://127.0.0.1:4173 \
  --browser-origin https://console.example

Each value is an origin only: http or https, a host, and an optional port. Wildcards, null, userinfo, paths, queries, and fragments are rejected. Omitted and explicit default ports are equivalent. Plain HTTP is accepted only for a loopback frontend; non-loopback frontend origins require HTTPS. The list is bounded to 64 entries. A browser uses Authorization: Bearer ... and credentials: "omit"; cookie credentials are not enabled.

The gateway answers a valid OPTIONS preflight before bearer authentication, body receipt, or FastAPI/application dispatch. It accepts only real API route methods and the bounded request-header set Accept, Authorization, Content-Type, Idempotency-Key, Range, X-FastSim-Slot-Generation, and X-Request-Id. Actual browser requests still pass bearer authentication and all normal API limits. Allowed responses expose Location, Retry-After, range metadata, X-Request-Id, and the complete portable X-FastSim-* metadata set; Access-Control-Allow-Credentials is never emitted.

Origin, Host, and Fetch Metadata are checked before lazy application open or camera encoding. A same-origin browser request remains compatible without an allowlist and must provide one trusted Host plus unique Site/Mode metadata. It must not carry cookies, may use cors/same-origin API fetches or a GET navigate request, and must report Site same-origin (or none). A separately hosted allowed frontend must report same-site or cross-site and CORS fetch mode. Duplicate security headers, inconsistent Fetch Metadata, missing Origin on cross-site resource probes, and mismatched Host are rejected. SDK and command-line clients that send neither Origin nor browser Fetch Metadata remain compatible.

Browser mode requires the API authority to match the exact configured --host/--port, or an exact --trusted-host plus the configured --port. A wildcard bind such as 0.0.0.0 is never a trusted public browser authority by itself. Declare every browser-facing host explicitly when binding all interfaces, for example:

bash
fastsim-server run.yaml \
  --host 0.0.0.0 --port 8443 \
  --trusted-host 127.0.0.1 \
  --browser-origin http://127.0.0.1:8090 \
  --token-file ./fastsim-server.token \
  --tls-cert-file ./server.crt \
  --tls-key-file ./server.key

--trusted-host accepts a DNS name, IPv4 address, or IPv6 address only; it does not accept a scheme, port, wildcard address, or path. Values are exact, canonicalized, deduplicated, and bounded to 64 entries. The same authority policy protects HTTP and realtime WebSocket requests. Reverse-proxy headers are not used to infer public authorities.

Launch-root trust boundary

A server-path request selects a public root alias plus a strict relative POSIX path; clients never submit an accepted absolute host path. Empty or dot segments, traversal, drive prefixes, backslashes, symbolic links, non-regular files, and root escapes are rejected. The configuration is read through root directory file descriptors with no-follow semantics, and its source directory remains pinned through compilation so path replacement cannot redirect it.

--launch-root authorizes configuration selection within a trusted FastSim Project tree; it is not a sandbox for the complete asset graph. A selected configuration can use that Project's registries and trusted_roots, installed component packages, and digest-pinned HTTPS resources. The launch-root tree and its Project metadata must therefore be controlled by the Server operator and must not be writable by untrusted users.

Bounds and errors

  • Request bodies: at most 4 MiB by default and checked before FastAPI parsing; GET and DELETE bodies are rejected.
  • The usable upload content limit is published by GET /api/v1/launch; it is lower than the body limit because the JSON envelope and worst-case escaping are included in admission. A small body limit may disable upload while server-path launch remains available.
  • Body receipt, public queries, geometry lease operations, and control all have independent deadlines. Query deadline expiry returns 504 query_timeout.
  • Lazy FastSim startup is independently bounded to 120 seconds by default; expiry returns 504 application_open_timeout and cancels the open attempt.
  • Aggregate buffered request bytes and binary response concurrency have independent hard limits; saturation returns 429 without waiting.
  • Portable JSON: at most 16 MiB total and 1 MiB per UTF-8 string.
  • Query filters: at most 256 values per field.
  • Operation pages: at most 200 records; retained history is bounded and expires.
  • Geometry byte reads and particle-fluid payloads: at most 64 MiB.
  • Concurrent HTTP requests: bounded globally; saturation returns 429 with request_capacity_exhausted.
  • Application construction runs in the FastSim process and may include a backend's non-interruptible compiler. During process shutdown the Server waits for an already-running factory to return, then closes any abandoned application before releasing upload staging. This prevents ownership leaks, but a faulty third-party factory that never returns can delay SIGTERM; use an external process supervisor with a final kill deadline.

Errors use fastsim-http-error/1 with a stable code, bounded message, retryable flag, and request ID. Backend tracebacks, failure messages, paths, private plugin configuration, bindings, and provider diagnostics are not returned. Individual plugins are inspected but not started or stopped independently: their lifecycle remains coupled to the owning FastSim application so dependency ordering and cleanup stay deterministic.

Python embedding

python
import fastsim
from fastsim_plugin_server import ServerSettings, create_http_app

simulation = fastsim.app("run.yaml", planning_reads=True)
http_application = create_http_app(
    simulation,
    settings=ServerSettings(
        planning_reads_enabled=True,
        application_open_timeout_s=120.0,
        browser_origins=("http://127.0.0.1:4173",),
    ),
)

The HTTP layer supports only the bounded single-document launch described above. It deliberately excludes arbitrary filesystem access, multi-file asset upload, arbitrary Python execution, backend-native handles, semantic actions such as pick/place, and unbounded streaming subscriptions. Those responsibilities belong to trusted Run roots, FastSim plugins, or a separate trusted control plane.