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:
python -m pip install ./packages/fastsim-plugin-server
fastsim-server --help
Install the optional real-time camera transport when using the browser workbench:
python -m pip install './packages/fastsim-plugin-server[webrtc]'
Install the optional IK service when remote clients or Agents need kinematics:
python -m pip install './packages/fastsim-plugin-server[kinematics]'
Verify that the optional provider and a software video encoder are usable:
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
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:
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:
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:
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:
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:
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.
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.
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:
{
"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:
{
"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.
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:
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:
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:
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.
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):
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:
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:
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_timeoutand cancels the open attempt. - Aggregate buffered request bytes and binary response concurrency have
independent hard limits; saturation returns
429without 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
429withrequest_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
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.