Dashboard¶
One command, one browser tab, on the machine the robots are reachable from.
It binds 127.0.0.1:8090 and opens the page. Nothing is exposed to the network
until a passkey guards it.
The first minute¶
- This machine, no passkey yet. The dashboard is usable from a browser on
the same machine, at
http://127.0.0.1:8090orlocalhost, and refuses every other caller - a same-host proxy orssh -Lforward, a request whoseHostis any other name (DNS rebinding), and a page from any other origin (cross-sitefetchorWebSocket). None of those is presence at the machine. - Enrol the owner passkey. The login screen asks for a bootstrap token. It
is in the
0600fileenrol_tokenbeside~/.strands_dashboard/auth.json(or theSTRANDS_DASH_AUTH_BOOTSTRAP_TOKENyou set). Paste it, name the key, let the browser create the passkey. From that moment the API is sealed: every route but the login screen and/api/healthanswers401without a session. The login screen's own route says whether setup is required and which proof it needs - never the enrolled passkeys, which only a session may list. - Bind a LAN address if you want to.
--host 0.0.0.0is refused until a passkey or a staticDASHBOARD_AUTH_TOKENexists, and says so.
What is on the page¶
| Tab | What it shows | Where the rules live |
|---|---|---|
| Fleet | every robot the registry knows, sim and real, whether its sim asset is already on this disk, and the mesh peers when the [mesh] extra is installed - a read of what is here; the page fetches nothing |
strands_robots.registry |
| Sim | a MuJoCo robot stepping in this process - an MJPEG stream and the same model in your browser. Or a mirror: that twin posed from the real arm's servo bus, read and never written | strands_robots.simulation, dashboard.mirror |
| Agent | a Strands Agent whose tools are the simulations on this page; anything that moves a robot pauses on a consent card | dashboard.agent_console, dashboard.agent_hitl, dashboard.consent |
| Settings | the file ~/.strands_robots/dashboard/settings.json - agent model, mesh endpoints, static token (shown only as set / unset) |
dashboard.settings |
Each tab has an address - #fleet, #sim, #agent, #settings. Bookmark one
and it opens there; Back and Forward move between the tabs you visited.
Every string a route serves is rendered as text, never as markup: a Fleet row can
carry a mesh peer's name, and script running in this page would be same-origin -
it carries the session cookie and names this origin as its own, so it is behind
every guard above by construction. tests/test_dashboard_static_renders_data_as_text.py
reads that rule off the files the wheel ships.
The Agent tab¶
Type a sentence; the agent answers with tool calls you can read. Its tools are the simulations - list the robots the registry can simulate, start one, read its joints, move them, reset, stop, and the e-stop. Every one goes through the same safety object the buttons use, so a latched e-stop refuses the agent exactly as it refuses a click, and stopping is never refused.
Moving a robot pauses first. sim_set_joints raises an interrupt before it runs,
the page shows what a yes would move (2 - 1.000 rad), and Allow once, Allow
for this conversation or Refuse resumes the same turn. A conversation-wide yes
covers that one session, lives in the socket and dies with it; every answer is
written to the operator-response audit log. The model is the one named by
STRANDS_MODEL_ID, and the page shows which it is.
The e-stop¶
The red button posts /api/safety/estop: every session in this process stops
stepping but keeps rendering and answering, so the robot stays on screen exactly
where it stopped, and the lockout latches locked. While it is latched, any
route that would move a sim - create, reset, joints - answers 423; stopping a
session never is. A session whose engine is still being built is frozen too, and
a create that overlapped the e-stop is refused and dropped rather than served,
because accepting it would report the lockout clear again and leaving it in the
store would hand a resume a robot no one was given. The same holds for a command
already in flight when the button was pressed: a write the worker had begun
cannot be recalled, one still on the queue is refused instead of applied, and
either way the request answers 423 and the latch keeps refusing the next
command. /api/safety/resume lifts the lockout only to unknown: a resume is a
request, and the next command a session accepts is the proof. While the lockout
is latched the same button reads RESUME and posts the resume; its label and its
action are both read from the lockout the server last reported - on page load, on
every telemetry frame, and in the answer to the button itself - so a page opened
under an e-stop engaged elsewhere shows RESUME, and a button that reads E-STOP
stops. A refused request is shown as a message beside the line, not painted as
an e-stop.
The twin follows the real arm¶
Pick a robot, change simulate to the serial port the arm is on (the list
is GET /api/sim/ports, servo buses first) and press Start. The session
that appears is marked mirror · read-only: a thread reads
Present_Position from every motor at ~20 Hz and the model is posed from the
readings - no physics steps, no Reset, and joints answers 400, because
the arm decides. Move the arm by hand and the twin moves.
What it will not do is write. lerobot's bus is opened for the handshake (a
ping and a firmware read) and closed with disable_torque=False, since the
default close writes Torque_Enable=0 to every motor. Torque stays exactly as
you left it and the footer says so. Angles are (ticks - 2048) · 2π / 4096
with no calibration applied - right up to the offset a calibration would
record, and labelled estimate in the snapshot's bus field along with the
raw ticks, the read rate and the age of the last reading. A bus that stops
answering shows stale, then error with the reason. A pose the model
refuses - one joint past its range writes nothing, so the twin would otherwise
sit still while the bus reads healthily - shows refused with the joint
named, and clears itself when the arm comes back inside: unlike error it
ends nothing, so the telemetry socket and the page's twin live through it. A
port that will not open is a 502 naming it, and nothing is left holding the device -
nor when the port opens but the engine behind it fails to build or
render: the session reports error and the port is released.
Configuration¶
| Variable | Default | Meaning |
|---|---|---|
STRANDS_DASH_AUTH_STORE |
~/.strands_dashboard/auth.json |
the passkey store; auth is on the moment it holds a credential |
STRANDS_DASH_AUTH_ENABLED |
read from the store | force on (1) or off (0); anything else is ignored with a warning |
STRANDS_DASH_AUTH_BOOTSTRAP_TOKEN |
minted into enrol_token |
the proof the first enrolment needs |
DASHBOARD_AUTH_TOKEN |
unset | a static bearer for scripts; a passkey session is still needed to remove a passkey |
DASHBOARD_SETTINGS_FILE |
~/.strands_robots/dashboard/settings.json |
where Settings are written |
STRANDS_MODEL_ID |
the model the installed SDK defaults to | which Bedrock model the Agent tab talks to |
Every auth duration knob (STRANDS_DASH_AUTH_TOKEN_TTL, SESSION_MAX_AGE,
HANDOFF_TTL) is documented in the configuration reference;
none can be widened past its cap.