Skip to content

Security

At the end of this page you can name every control between a language model and a moving robot, the environment variable that widens or narrows each one, and where the evidence is written.

The posture in one sentence: an agent may read anything; a command that can move a robot stops for an operator, travels only on an authenticated wire, is validated against allowlists on both ends, and leaves a signed row.

The layers

Top: a command that can move a robot, from an agent's tool call, a dashboard button or a mesh peer. A row of six cards it crosses in order, lit one after another: the operator gate (gate_motion, whether this command may move a robot at all); the motion grant (a browser yes spent once, for the exact tool input it approved); command validation (validate_command, an allowed action, provider, host and checkpoint); the wire (mTLS under STRANDS_MESH_AUTH_MODE, the ACL in STRANDS_MESH_ACL_FILE, the transport caps before deserialisation); the safety envelope (an e-stop or resume that is fresh, unreplayed and, for resume, proven); the driver gate and bus access (the hardware is in a state where a write is safe, one caller owns the bus). Below the row, the one green element: the audit log, where every layer leaves its row, in order, tamper-evident. Right of it, path validation: a tool writes only where it was told. Footnote: a read crosses none of this; refusals are audited too.Top: a command that can move a robot, from an agent's tool call, a dashboard button or a mesh peer. A row of six cards it crosses in order, lit one after another: the operator gate (gate_motion, whether this command may move a robot at all); the motion grant (a browser yes spent once, for the exact tool input it approved); command validation (validate_command, an allowed action, provider, host and checkpoint); the wire (mTLS under STRANDS_MESH_AUTH_MODE, the ACL in STRANDS_MESH_ACL_FILE, the transport caps before deserialisation); the safety envelope (an e-stop or resume that is fresh, unreplayed and, for resume, proven); the driver gate and bus access (the hardware is in a state where a write is safe, one caller owns the bus). Below the row, the one green element: the audit log, where every layer leaves its row, in order, tamper-evident. Right of it, path validation: a tool writes only where it was told. Footnote: a read crosses none of this; refusals are audited too.
layer what it decides where
operator gate whether this command may move a robot at all _command_gate.gate_motion, agents
motion grants that a browser "yes" is spent once, for the exact tool input it approved _motion_grants
command validation that a mesh command names an allowed action, provider, host and checkpoint mesh.security.validate_command
wire auth who may publish on the mesh mTLS, STRANDS_MESH_AUTH_MODE
ACL which certificate may publish or subscribe on which key STRANDS_MESH_ACL_FILE, bridges
transport caps how many bytes and how often, before deserialisation STRANDS_MESH_MAX_*, STRANDS_MESH_*_RATE_HZ
safety envelopes that an e-stop or resume is fresh, unreplayed and, for resume, proven safety and e-stop
driver gates that the hardware is in a state where a write is safe each driver, drivers
bus access that one caller at a time owns a serial bus bus_access
path validation that a tool writes only where it was told _path_validation
audit log what happened, in order, tamper-evident audit, _hitl_audit

The operator gate

Every path from a model to an actuator ends in gate_motion: allowlist variable, then BYPASS_TOOL_CONSENT=true (a WARNING), then a Strands interrupt the operator answers out of band, failing closed when nobody can be asked. Reading is never gated. Stopping is never gated. The operator's reply never reaches the model; it goes to the audit log. The verb tables and the exact refusal text are on agents.

The gate lives at the package root because six tools and the hardware Robot call it; the ROS blocklist is one set for three transports because it describes a physical surface; a refusal names the value that would pre-approve the call (STRANDS_POSE_COMMAND_ALLOW=move_motor), never =true.

Allowlists on the mesh

validate_command runs on the sender and the receiver. Beyond ALLOWED_ACTIONS, an execute may only name a policy_provider or policy_type in the shipped set plus STRANDS_MESH_POLICY_TYPE_ALLOW (the two share one allowlist; a provider missing from registry/policies.json belongs in the registry, not the variable), a policy server host in localhost, 127.0.0.1, ::1 plus STRANDS_MESH_POLICY_HOST_ALLOW, and a checkpoint under a Hub org in nvidia, huggingface, lerobot plus STRANDS_MESH_HF_REPO_ALLOW. Local checkpoint paths are refused on the wire. Each variable's entries must be lowercase identifiers (^[a-z][a-z0-9_]*$), so STRANDS_MESH_HF_REPO_ALLOW="nvidia,;rm -rf /" is a refusal, not an allowlist. Silent defaults are not honoured on the boundary: an execute without a provider is refused before it leaves the process.

Teleoperation frames are bounded too: STRANDS_MESH_INPUT_VALUE_ABS (720), STRANDS_MESH_INPUT_SLEW_ABS, STRANDS_MESH_INPUT_MAX_HZ (100), and the local loop's STRANDS_TELEOP_SLEW_ABS (500). An over-speed frame is refused and counted, never clamped toward the command.

The wire

STRANDS_MESH_AUTH_MODE=mtls is the default and cannot be turned off without a second factor (STRANDS_MESH_I_KNOW_THIS_IS_INSECURE=1, or the one-machine preset STRANDS_MESH_LOCAL_DEV=true). Multicast discovery is off by default. A permissive ACL under mTLS refuses to start until acknowledged. The IoT leg binds each robot's X.509 CN to its Thing name and scopes its topics with ${iot:Connection.Thing.ThingName}; a direct reply goes only to strands/<sender>/response/<self>/<turn> (direct). The pure-RTPS ROS 2 bridge refuses an inbound command surface without DDS Security unless STRANDS_ROS2_BRIDGE_I_KNOW_THIS_IS_INSECURE=1 (ROS 2). rosbridge is unauthenticated by design, for trusted networks.

Paths, buses, subprocesses

validate_save_path refuses a write into /etc/, /usr/, /dev/, /proc/ and their macOS and Windows equivalents, and resolve_output_path refuses a file name that leaves the directory it was given; every tool that writes a caller-supplied path (lerobot_camera, reachy_camera, the judge's write_label, training) runs both. Dataset ids, bucket names and run_ids that reach the hf CLI are matched against allowlists before any subprocess. use_lerobot refuses lerobot.scripts, push_to_hub, upload_folder and save_to_disk by name so a prompt-injected call cannot push or spawn training. lerobot_train's extra_flags are gated by STRANDS_TRAIN_EXTRA_FLAGS_ALLOW.

bus_access.bus_lock is an RLock on the device, held by every reader (state probe, camera publisher, sensors, IoT offload), the teleop writer and a rollout, so a serial bus is one conversation.

The dashboard

Loopback by default; a non-loopback bind is refused without a passkey or a static token. Every route but the login screen and /api/health takes require_session. Details on dashboard.

Evidence

~/.strands_robots/mesh_audit.jsonl (STRANDS_MESH_AUDIT_DIR, mode 0600) is the one trail: every operator verdict (llm_tool_action), every e-stop and resume event, every command refused under lockout, every mesh tool refusal. Rows carry a monotonic seq and, with STRANDS_MESH_AUDIT_PSK, an HMAC-SHA256 sig; a degraded write says so in sig (PSK_DEGRADED, SIGN_FAILED, ...) instead of leaving a hole, and verify_audit_integrity() reports edited rows, gaps and unsigned rows. Rotation by STRANDS_MESH_AUDIT_MAX_BYTES and STRANDS_MESH_AUDIT_MAX_FILES.

When the file cannot be written, log_safety_event logs [audit] failed to write at WARNING and swallows the error: the peer keeps running with the trail off, so monitor that line.

Reporting

Refusal codes are the stable contract; prose is not: reference/refusal-codes. Vulnerabilities go to SECURITY.md, linked from project/security-policy.

Edit page