Configuration¶
Environment variables¶
| Variable | Description | Default |
|---|---|---|
STRANDS_ROBOT_MODE |
Robot() factory mode: sim / real / auto |
sim |
STRANDS_ASSETS_DIR |
Robot model asset cache directory | ~/.strands_robots/assets/ |
STRANDS_MEMORY_DIR |
Harness memory store (harness_memory tool: task solution traces + global success rules / failure models) |
~/.strands_robots/memory/ |
STRANDS_ROBOTS_RENDER_ROOT |
Sandbox directory that Simulation.render(output_path=...) may write into |
~/.strands_robots/renders/ |
STRANDS_ROBOTS_RENDER_ALLOW_ABS |
Set 1 to allow render(output_path=...) to write absolute paths outside the render sandbox |
unset |
STRANDS_ROBOTS_RENDER_MAX_BYTES |
Max PNG size render(output_path=...) will persist |
52428800 (50 MB) |
STRANDS_ROBOTS_VIDEO_ROOT |
Opt-in sandbox for video/recording output paths (run_policy(video=...), start_cameras_recording). Unset = absolute paths allowed (historic contract); set to confine writes |
unset |
STRANDS_ROBOTS_VIDEO_ALLOW_ABS |
Set 1 to re-permit absolute paths when STRANDS_ROBOTS_VIDEO_ROOT is set |
unset |
STRANDS_TRUST_REMOTE_CODE |
Set 1 to allow HF trust_remote_code for lerobot_local |
unset |
STRANDS_TRAIN_EXTRA_FLAGS_ALLOW |
Comma-separated lerobot_train extra_flags names pre-approved past the operator gate on the flags that control output paths, telemetry and code loading (output_dir, config_path, wandb.enable / .project / .entity / .api_key, dataset.root, policy.pretrained_path, push_to_hub, policy.push_to_hub, hub_repo_id). An entry names the blocked flag, so it also clears every argparse abbreviation of it (ou for output_dir); the two push_to_hub spellings are separate flags and one entry clears one of them. A blocked flag no entry names prompts the operator, and is refused where no operator can be prompted - this variable is the headless remedy that refusal names. BYPASS_TOOL_CONSENT=true waives the gate for every flag with a WARNING |
unset (every blocked flag prompts) |
STRANDS_TRAIN_RDZV_TIMEOUT_S |
Seconds a multi-GPU training launch (num_gpus > 1 on LerobotTrainer, GrootTrainer or Cosmos3Trainer) may spend in torch's elastic rendezvous before it fails rather than waiting. Handed to torch, whose own defaults are minutes long and spent inside C++ socket code no Python timeout can reach. Junk, zero, negative or non-finite values fall back to the default rather than restoring the unbounded wait |
120 |
STRANDS_TRAIN_LOCAL_ADDR |
Address a multi-GPU training launch publishes as MASTER_ADDR. Unset, a single-node launch is pinned to 127.0.0.1 and a multi-node one keeps torch's getfqdn() resolution. Set it when a multi-node host's name resolves to a reverse-DNS pointer (*.in-addr.arpa / *.ip6.arpa), which no peer can dial - the launch warns naming this variable rather than hanging on "Rendezvous'ing worker group" |
unset |
STRANDS_ROBOTS_NO_DYLD_SHIM |
Set 1 to disable the macOS auto-fix that puts Homebrew ffmpeg on the dyld path for torchcodec video streaming (see Recording) |
unset |
MUJOCO_GL |
MuJoCo GL backend (egl, osmesa, glfw) |
auto |
STRANDS_ISAAC_HEADLESS |
Isaac Sim backend: run without a GUI. On (1/true/yes/on) = headless, off (0/false/no/off) = windowed, any other spelling is refused. Overrides IsaacConfig(headless=...) (#2062) |
unset (config default true) |
STRANDS_ISAAC_RTX_PATHTRACING |
Isaac Sim backend: on (1/true/yes/on) enables RTX path-tracing (photorealistic, slow) instead of the default render mode; off leaves the render mode alone, any other spelling is refused |
unset |
STRANDS_ISAAC_NUCLEUS_URL |
Isaac Sim backend: override the Omniverse Nucleus asset-server URL | unset (Isaac default) |
GROOT_API_TOKEN |
API token for the GR00T inference service | unset |
STRANDS_MESH |
Opt a bare Robot() into the Zenoh mesh: true/1/yes turns it on. false/0/no is a hard kill switch that also overrides an explicit mesh=True, refuses the robot-less gateway peer the robot_mesh tool would otherwise start in a coordinator process, and refuses the shared transport itself - so a direct get_session() / ZenohTransport caller opens no session and binds no STRANDS_MESH_PORT listener either. Any other value -- including off and on -- is ignored, leaving the mesh neither opted into nor disabled, and is logged once as a warning naming the spellings above |
unset (mesh off) |
STRANDS_MESH_LOCAL_DEV |
Set 1 for a one-var localhost preset (auth none, no second factor needed) |
unset |
STRANDS_ROS2_BRIDGE_I_KNOW_THIS_IS_INSECURE |
Second factor to expose a Robot(ros2_transport="rtps") inbound joint_command surface with no dds_security_config (DDS Security). Truthy: 1/true/yes |
unset |
STRANDS_ROS2_COMMAND_ALLOW |
Comma-separated ROS 2 surfaces pre-approved for use_ros commands, for headless use where no operator can be prompted (e.g. /cmd_vel,/navigate_to_pose). An entry matches by base name, so /cmd_vel also pre-approves every namespaced cmd_vel in the graph - name the namespace (/turtle1/cmd_vel) to scope the approval to one robot. Every blocklisted surface with a base name no entry lists stays gated, including a zero-velocity halt: the gate is keyed on the surface, not on the payload, so a deployment that must halt unattended pre-approves its cmd_vel topic. Reads are never gated. See safety-critical command surfaces |
unset |
Mesh / IoT / GR00T-container env vars (advanced)
| Variable | Description | Default | |----------|-------------|---------| | `STRANDS_MESH_BACKEND` | Mesh transport: `zenoh` (LAN), `iot` (AWS IoT Core), or `bridge` (both - Zenoh locally, IoT for the bridged topics). Case and surrounding whitespace are ignored. This selects the transport; the `[mesh-iot]` extra only installs the dependency `iot`/`bridge` need, so installing it without setting this leaves the fleet on Zenoh. An unrecognized value falls back to the default and is logged once, naming the valid values | `zenoh` | | `STRANDS_MESH_AUTH_MODE` | Wire auth: `mtls` or `none` (`none` needs a second factor) | `mtls` | | `STRANDS_MESH_TLS_CA` | Path to the CA bundle that validates peer certificates. Required under `AUTH_MODE=mtls` (the default) | unset | | `STRANDS_MESH_TLS_CERT` | Path to this peer's certificate (PEM). Its CN is what an operator ACL pins. Required under `AUTH_MODE=mtls` | unset | | `STRANDS_MESH_TLS_KEY` | Path to this peer's private key (PEM). Enforced mode `0600` on POSIX; on Windows the mode gate is skipped with a one-shot WARNING, so restrict it by NTFS ACL. Required under `AUTH_MODE=mtls` | unset | | `STRANDS_MESH_I_KNOW_THIS_IS_INSECURE` | Second factor required to bring up `AUTH_MODE=none` | unset | | `STRANDS_MESH_PORT` | TCP port for the local Zenoh router | `7447` | | `STRANDS_MESH_FALLBACK_MODE` | Zenoh mode for a process that did NOT win the `STRANDS_MESH_PORT` listener and so connects to that hub: `client` (the hub relays, so siblings hear each other) or `peer` (direct links only, which the operator must then arrange). A Zenoh 1.x peer refuses relayed traffic, so `peer` children hear nothing a sibling child publishes. An unrecognized value warns and uses the default | `client` | | `ZENOH_CONNECT` | Comma-separated remote Zenoh endpoints to connect to | unset | | `ZENOH_LISTEN` | Comma-separated endpoints for the local Zenoh listener | unset | | `STRANDS_MESH_MULTICAST` | Opt in to multicast scouting for LAN discovery. Off by default: any device on the LAN can enumerate and attract the fleet, so enabling it logs a WARNING. Prefer explicit `ZENOH_CONNECT` endpoints | `false` | | `STRANDS_MESH_FILTER_INTERFACES` | Comma-separated network interface names (e.g. `eth0,wlan0`) the per-message size caps (`low_pass_filter`) are bound to. Unset or blank binds the caps to every link (Zenoh's wildcard), which is the safe default: an enumerated list that misses a NIC exempts that NIC from the cap | unset (all interfaces) | | `STRANDS_MESH_MAX_CMD_BYTES` | Per-message byte cap on `cmd` / `broadcast` topics, applied at the transport (an over-cap message is dropped before the JSON parser runs) and pre-checked by `Mesh.send` so the sender fails loudly instead. Integer in `[128, 16777216]`; a value outside it, or not an integer, raises at session build naming the variable | `16384` (16 KiB) | | `STRANDS_MESH_MAX_CAMERA_BYTES` | Per-message byte cap on `camera` topics, ingress only. Integer in `[1024, 134217728]`; out of range or non-integer raises | `1048576` (1 MiB) | | `STRANDS_MESH_MAX_SAFETY_BYTES` | Per-message byte cap on `safety/*` topics, both flows. Integer in `[128, 1048576]`; out of range or non-integer raises | `4096` (4 KiB) | | `STRANDS_MESH_CMD_RATE_HZ` | Transport-level rate cap on inbound `cmd` publishes per peer; faster publishes are dropped before parsing, so a flood costs the receiver almost nothing. Finite float in `[0.001, 10000]`; `nan`/`inf` or out of range raises rather than disabling the cap | `20.0` | | `STRANDS_MESH_SAFETY_RATE_HZ` | The same cap for `safety/*` publishes, kept lower so a peer holding any fleet cert cannot flood `safety/estop` with novel timestamps past the replay cache. Finite float in `[0.001, 1000]` | `2.0` | | `STRANDS_MESH_MAX_SESSIONS` | Zenoh `transport/unicast/max_sessions`: how many peers one session accepts. Integer in `[1, 65535]` | `256` | | `STRANDS_MESH_NAMESPACE` | Fleet namespace prefix on every mesh key-expression. The Zenoh `namespace` config field provides routing isolation -- two fleets with different namespaces cannot exchange messages even when their key-expressions collide, so this is the knob that keeps a co-located test fleet from receiving a production fleet's commands. Must match on every peer of one fleet; a mismatch is silent (peers connect at the transport layer and exchange no application traffic). Empty / whitespace values fall back to the default so `STRANDS_MESH_NAMESPACE=""` cannot accidentally produce keys like `"//presence"` | `strands` | | `STRANDS_MESH_AUDIT_DIR` | Directory for the safety audit log (`mesh_audit.jsonl`) | `~/.strands_robots/` | | `STRANDS_MESH_AUDIT_PSK` | Pre-shared key that keys the per-record HMAC in the audit log. When set, `verify_audit_integrity` refuses a record whose HMAC does not match and refuses the whole log if the PSK changes mid-run; when unset, the `sig` field is absent and a writer with directory access can edit records without failing the check. Set on every peer that writes to the same directory | unset | | `STRANDS_MESH_AUDIT_MAX_BYTES` | Rotate the active audit file once it crosses this size (bytes). Values above the 10 GiB hard cap are clamped with a warning; non-integer, zero, or negative values fall back to the default with a warning, so a misconfiguration cannot silently disable rotation | `104857600` (100 MiB) | | `STRANDS_MESH_AUDIT_MAX_FILES` | Number of rotated `mesh_audit.jsonl.N` files kept alongside the active file. Older rotations are deleted as new ones arrive; total disk use is bounded by `_MAX_BYTES × _MAX_FILES`. Same clamping/warning behaviour as `_MAX_BYTES`; hard upper cap 100 | `5` | | `STRANDS_MESH_CA_PINS` | Additional SHA-256 CA pins (comma-separated 64-char hex) | unset | | `STRANDS_MESH_DISABLE_CA_PIN` | Skip CA pin check on download path (break-glass) | `false` | | `STRANDS_MESH_CAMERA_S3_BUCKET` | S3 bucket that turns the camera offload on under the `iot` / `bridge` backends: frames are uploaded there and the mesh publishes a presigned URL instead of inline JPEG bytes. Unset leaves camera publishing inline (Zenoh) and logs `offload off` at DEBUG | unset (offload off) | | `STRANDS_MESH_CAMERA_S3_PREFIX` | Key prefix inside `STRANDS_MESH_CAMERA_S3_BUCKET`; surrounding `/` are stripped | unset (bucket root) | | `STRANDS_MESH_CAMERA_PRESIGN_TTL` | TTL (s) for S3 presigned camera URLs; capped at 3600 | `60` | | `STRANDS_MESH_ACL_FILE` | Path to a JSON5 Zenoh ACL file; unset = permissive default. See `examples/mesh/mesh_acl_example.json5` (role-scoped) and `examples/mesh/mesh_acl_strict_per_peer.json5` (per-peer). **⚠️ Required on any WAN/cloud router: mTLS gives identity, not least-privilege — without a topic-level ACL one device cert can read all fleet traffic and command any robot. See [security docs](../security.md#production-posture-required-off-trusted-networks).** | unset | | `STRANDS_MESH_ACCEPT_PERMISSIVE_ACL` | Acknowledgement token with **three** distinct effects, all of them widening the mesh posture — set on a production fleet at your peril. (1) A `STRANDS_MESH_ACL_FILE` with `default_permission: "allow"` **and** one or more `rules` is refused at load by `_acl_config._load_acl_file` with `PermissiveACLError` unless this variable is set to `1`/`true`/`yes` — the allow + rules shape is a blacklist policy where any gap in the rule set exposes the mesh, and the refusal exists so it is not shipped by copy-paste from a lab template. (2) When `STRANDS_MESH_AUTH_MODE=mtls` **and** the resolved ACL is permissive-by-shape (built-in default *or* operator file with `allow` + no rules), `Mesh.start`'s refuse-to-start gate downgrades from `ERROR` refusal to an `INFO` acknowledgement — the token is the opt-in that lets the wire come up under the built-in permissive default. (3) The per-session-open `WARNING` that fires when the built-in permissive default is in use is suppressed — session and start emit one line each about the same posture, so silencing the WARNING here avoids contradicting the operator's explicit opt-in on every session open. Set to `1`/`true`/`yes` (case-insensitive, whitespace-stripped) to accept all three; `on` and every other spelling are not acknowledgements, and `strands-robots doctor` reads the same predicate (`_acl_config.permissive_acl_acknowledged`) so its Mesh row agrees with the gates. The acknowledgement does not narrow the ACL; it records that an operator has accepted a wire-open posture. | unset (refuses blacklist ACL, refuses to start under built-in permissive default, warns on every session open) | | `STRANDS_MESH_POLICY_HOST_ALLOW` | Comma-separated allowlist of VLA policy-server hosts/CIDRs for inference | loopback only | | `STRANDS_MESH_POLICY_TYPE_ALLOW` | Comma-separated extras appended to the built-in `policy_type` / `policy_provider` allowlist (`validate_command` gates the mesh `execute` / `start` payloads against the union). Widens both vocabularies at once because the two share one allowlist by design. Each entry is charset-validated against a lowercase-identifier regex (`^[a-z][a-z0-9_]*$`) at parse time; case-variant typos are normalised via `.lower()` before the compare, so `"FOO,BAR"` matches `"foo,bar"`. A malformed entry drops with a WARNING naming the variable and the offender rather than widening the allowlist silently. Does not relax any adjacent gate: `policy_host`, `server_address`, `pretrained_name_or_path` and `model_path` are still allowlisted regardless of which provider the payload names. Adding a provider to `registry/policies.json` must include the corresponding edit to `_REGISTRY_POLICY_PROVIDERS` beside it; a guard test refuses any registry spelling that set omits, so the omission fails CI rather than shipping as a mesh-only availability bug the operator has to break-glass around with this variable | unset (built-in list only) | | `STRANDS_MESH_HITL_ACTIONS` | `robot_mesh` actions needing a human-in-the-loop interrupt: `all` / `none` / subset of `emergency_stop,broadcast,tell,send,stop,rpc,subscribe,watch` | actuation default | | `STRANDS_DASH_AGENT_PHYSICAL_MOTION` | Grant that lets an agent start motion on a **physical** peer by itself (`strands_robots.dashboard.agent_motion`), read at call time so it can be withdrawn mid-session. Only motion-starting actions are gated -- every way of stopping a robot is outside the gate. `1`/`true`/`yes`/`on` grants | unset (agent-initiated physical motion refused, an operator has to start it) | | `STRANDS_DASH_TASK_REQUIRES_CONFIRM` | Set `1`/`true`/`yes`/`on` to make a real-motion task request carry an explicit operator confirmation. The confirmation is read as a JSON boolean, so `true` carries it and `"true"` does not -- every non-empty string is truthy, and `"false"` would otherwise confirm | unset (no confirmation flag required) | | `STRANDS_MESH_SUBSCRIBE_ALLOW` | Extra Zenoh key-expr patterns the `robot_mesh` `subscribe` action may target, beyond the built-in low-impact set | shared classes only | | `STRANDS_MESH_OVERRIDE_CODE` | Shared secret for e-stop resume HMAC proof; unset means no remote resume possible | unset | | `STRANDS_MESH_INPUT_VALUE_ABS` | Absolute value clamp for teleop joint commands, in frame units -- whichever unit the leader driver puts on the wire (shipped SO leaders stream degrees, and a 0-100 gripper). Narrow it for radian or normalized -1..1 actuators, whose units are smaller | `720` (two full turns) | | `STRANDS_MESH_INPUT_MAX_HZ` | Per-receiver teleop apply-rate ceiling (0 = unlimited). A value no rate check can be built from -- unparsable, or non-finite like `inf`/`nan` -- falls back to the default so the ceiling stays enforced | `100` | | `STRANDS_MESH_INPUT_SLEW_ABS` | Per-joint speed bound for the mesh receive path, in frame units per second (narrow for radian or normalized actuators, whose units are smaller; cannot be disabled) | `1440` (the value envelope traversed once per second) | | `STRANDS_TELEOP_SLEW_ABS` | Per-joint speed bound for the local `teleoperate()` loop, in frame units per second (default accommodates degree-valued and range-0-100 devices; cannot be disabled) | `500.0` | | `STRANDS_MESH_POSE_HZ`, `_IMU_HZ`, `_ODOM_HZ`, `_HEALTH_HZ`, `_LIDAR_SUMMARY_HZ`, `_HAND_HZ`, `_MAP_INFO_HZ` | Per-topic sensor publish rate; `0` (or any non-positive value) switches that topic off. A value the loop cannot pace itself with keeps the built-in rate | per topic: `10`/`10`/`10`/`0.5`/`5`/`50`/`0.2` | | `STRANDS_MESH_CAMERA_HZ` | Camera publish rate; opt-in because frames are large. Unset, non-positive, or unusable leaves camera publishing off | `0` (off) | | `STRANDS_MESH_CAMERA_DISABLED` | Privacy kill switch for the camera publisher: `true`/`1`/`yes`/`on` publishes no frames at all (nothing built, signed or sent) whatever `STRANDS_MESH_CAMERA_HZ` says; `false`/`0`/`no`/`off` leaves it to the rate. Any other spelling raises rather than silently re-enabling a privacy flag | unset (cameras follow the rate) | | `STRANDS_MESH_STREAM_HZ` | Per-step task telemetry rate while a robot or a rollout is executing. Non-positive or unusable -- unparsable, or non-finite like `inf`/`nan` -- switches step publishing off rather than changing the rate, so an unreadable value cannot remove the throttle | `10` | | `STRANDS_MESH_GATEWAY_DISCOVERY_WAIT_S` | How long a robot-less `robot_mesh` gateway waits once at bring-up for presence to populate before the first `peers` read. `0` means do not wait; a value no sleep can honor -- unparsable, negative, or non-finite -- falls back to the default | `3` | | `STRANDS_MESH_MAX_PEERS` | Peer registry cap; evicts oldest on overflow | `1024` | | `STRANDS_MESH_PEER_RETENTION_S` | Extra retention (s) past the peer timeout before a silent peer is deleted; retained peers report `reachable: false`. A value no retention can be built from - unparsable, negative, or non-finite - falls back to off with one warning per spelling | `0` (off) | | `STRANDS_MESH_RESUME_MAX_FAILS` | Failed resume attempts before cooldown engages | `5` | | `STRANDS_MESH_RESUME_BACKOFF_S` | Cooldown (seconds) after exceeding resume fail threshold. A value no cooldown instant can be built from -- unparsable, negative, or non-finite like `inf`/`nan` -- falls back to the default, so the throttle both engages and expires (shared with `STRANDS_MESH_RESUME_FRESHNESS_S` / `_FORWARD_SKEW_S`) | `30` | | `STRANDS_MESH_RESUME_FRESHNESS_S` | How far in the past a resume envelope's timestamp may be before a receiver refuses it as stale. A receiver whose clock is more than this *ahead of* the operator reads every resume as stale and stays locked out, so keep fleet clocks in NTP sync or widen this on every peer (a receiver *behind* the operator is refused by `STRANDS_MESH_RESUME_FORWARD_SKEW_S` instead) | `60` | | `STRANDS_MESH_RESUME_FORWARD_SKEW_S` | How far in the future a resume envelope's timestamp may be before a receiver refuses it as future-dated. This is the tighter of the two bounds: a receiver whose clock is more than this *behind* the operator sees every resume as future-dated and stays locked out, so widen this on every peer (a receiver *ahead of* the operator is refused by `STRANDS_MESH_RESUME_FRESHNESS_S` instead) | `5` | | `STRANDS_MESH_RESUME_REPLAY_CACHE_MAX` | Entries in the per-receiver resume replay cache; also bounds the per-issuer fairness cap (max/4) so one flooding issuer cannot evict a legitimate operator's slot | `4096` | | `STRANDS_MESH_INPUT_AUDIT_EVERY` | Emit `input_stream_applied` audit event every N frames (0 = off) | `100` | | `STRANDS_ESTOP_DEDUP_TTL_S` | E-stop fan-out Lambda dedup window (seconds) | `30` | | `STRANDS_MESH_DEDUP_TTL` | Window (seconds) the Zenoh<->IoT bridge remembers a delivered `(sender_id, turn_id, command)` triple for cross-transport deduplication. Unparsable, non-positive or non-finite falls back to the default, so a legitimately recurring heartbeat is forgotten again | `120` | | `STRANDS_MESH_BRIDGE_DEDUP_STRICT` | `1`/`true`/`yes` makes the Zenoh<->IoT bridge dedup a sample that carries no `(sender_id, turn_id, command)` triple by hashing its whole payload, so a heartbeat-style message arriving on both transports is delivered once. Off delivers such samples as-is. Read once at bridge construction; any other spelling warns and stays off | `0` (off) | | `STRANDS_MESH_BRIDGE_TOPICS` | Comma-separated topic suffixes the Zenoh<->IoT bridge forwards (exact match). Unset = the safe default set (`presence,health,safety/event,safety/estop,safety/resume,cmd,response,broadcast`). High-volume topics (`state,pose,imu,odom,lidar`) and LAN-only topics (`camera,input,hand`) are deliberately NOT bridged | default set | | `STRANDS_MESH_BRIDGE_TOPICS_PREFIX` | Comma-separated topic suffixes the bridge matches as a path **prefix** (so `response` matches `response/Isaac Sim backend env vars (strands-robots[sim-isaac])
These are read by the built-in, in-tree Isaac Sim backend
(`pip install 'strands-robots[sim-isaac]'`) when it builds its
`IsaacConfig`. An explicit `create_simulation("isaac", ...)` kwarg wins for
`nucleus_url`; the two switches override their field whenever they are set
([#2062](https://github.com/strands-labs/robots/issues/2062)). Both switches
accept `1`/`true`/`yes`/`on` and `0`/`false`/`no`/`off` (case-insensitive,
surrounding whitespace ignored); unset or empty leaves the field alone and any
other spelling is refused. See
[`docs/simulation/isaac.md`](../simulation/isaac.md).
| Variable | Description | Default |
|----------|-------------|---------|
| `STRANDS_ISAAC_NUCLEUS_URL` | Override the Omniverse Nucleus server URL (when `nucleus_url` is not passed) | unset (Isaac defaults) |
| `STRANDS_ISAAC_HEADLESS` | On forces headless; off forces a window | unset (uses `headless` kwarg) |
| `STRANDS_ISAAC_RTX_PATHTRACING` | On forces `render_mode="rtx_pathtracing"`; off leaves `render_mode` alone | unset |
| `STRANDS_ISAAC_CAMERA_WARMUP_STEPS` | Render-bearing world steps `add_camera` takes before returning, so a new RTX camera's first `get_rgba()` is a real frame rather than the empty buffer the pipeline returns until it has been stepped. Raise on a slow GPU. Positive integer; anything else is reported with a warning naming the variable and falls back to the default | `10` |
Diagnostic env vars (GR00T bisection)
| Variable | Description | Default | |----------|-------------|---------| | `STRANDS_GROOT_WIRE_LOG` / `_MAX_CALLS` | Directory to dump pre/post inference payloads to, e.g. `/tmp/groot-wire`, to verify LOCAL vs SERVICE parity | unset / `10` | | `STRANDS_ROBOTS_VERBOSE_MUJOCO` | `1`/`true`/`yes` lets MuJoCo's attach-conflict chatter (`timestep: parent has 0.002, child has 0.005, keeping parent value` and its siblings, emitted on every `add_robot` / world build) reach stderr instead of being captured. Captured lines are re-emitted at DEBUG with a count; every other stderr line is forwarded unchanged either way | unset (captured) |Dashboard passkey (WebAuthn) auth env vars
Read by `strands_robots.dashboard.auth`, which fronts the dashboard routes that command real hardware. The credential store is the source of truth for whether auth is on: the moment a passkey is enrolled, it is. The three durations and the two challenge caps are **refused, not defaulted**, when they hold a value the module cannot use - each is a knob an operator uses to *narrow* a window, and a substituted default lands in the wider direction - so a misspelled value stops the server at import rather than surfacing as a failed login later. | Variable | Description | Default | |----------|-------------|---------| | `STRANDS_DASH_AUTH_ENABLED` | Overrides the store's verdict only when spelled as a recognised boolean: on (`1`/`true`/`yes`/`on`) guards the API with no passkey enrolled yet, off (`0`/`false`/`no`/`off`) disables the guard. Any other spelling is logged and ignored, so an enrolled passkey still guards the API rather than being dropped by a typo | unset (the store decides) | | `STRANDS_DASH_AUTH_STORE` | Path of the credential store: the enrolled passkeys and the secret session tokens are signed with | `~/.strands_dashboard/auth.json` | | `STRANDS_DASH_AUTH_RP_NAME` | Human-readable relying-party name shown by the authenticator during enrollment | `strands robots dashboard` | | `STRANDS_DASH_AUTH_RP_ID` | Pins the WebAuthn relying-party id when the dashboard's hostname legitimately changed and existing passkeys must keep working. Unset, it is derived from the `Host` the request arrived on; a host that cannot be an rpId is refused naming this variable | unset (derived) | | `STRANDS_DASH_AUTH_ORIGIN` | The origin the dashboard is served at, e.g. `https://robots.example:8443` - scheme and any non-default port included, since WebAuthn compares origins byte-for-byte. Unset, it is read off the connection (the transport's own scheme plus `Host`). Set it when a proxy rewrites `Host` or `Origin`, or when TLS is terminated upstream and uvicorn is not configured to forward that fact (`--proxy-headers` with `--forwarded-allow-ips`) | unset (read off the connection) | | `STRANDS_DASH_AUTH_BOOTSTRAP_TOKEN` | The secret the **first** passkey enrollment must present. Unset, the module mints one itself into a `0600` file beside the store and the operator reads it off the machine - a loopback peer is never proof of presence, so the first enrollment is never admitted on the strength of where the connection appears to come from | unset (minted into a file) | | `STRANDS_DASH_AUTH_ENROLL_TOKEN_FILE` | Relocates the minted first-enrollment token file. Only read when `STRANDS_DASH_AUTH_BOOTSTRAP_TOKEN` is unset | `Asset cache¶
~/.strands_robots/
└── assets/ # auto-downloaded MJCF + meshes
├── trs_so_arm100/
├── franka_emika_panda/
└── ...
Clear with rm -rf ~/.strands_robots/assets/; relocate with
export STRANDS_ASSETS_DIR=/path/to/dir.
CA Pin Rotation Runbook¶
The AWS IoT transport pins the SHA-256 of the canonical Amazon Root CA1 PEM, so a network-level attacker (DNS hijack, captive portal, BGP, malicious local proxy) cannot substitute a rogue CA at the download URL. The accepted set is a collection, not a scalar, so old and new pins can both be valid at once - that is what makes a rotation expressible without a flag-day deploy.
When AWS rotates the root, every fleet member refuses the new certificate until a pin covering it is accepted, on both the download path and the on-disk re-use path. Deleting the cached PEM does not help: the re-download fetches the same unpinned bytes and is refused again. Rotation therefore needs an ordered procedure, which is this one.
Recompute the pin of whatever the URL currently serves:
python -c "import hashlib, urllib.request as u; \
print(hashlib.sha256(u.urlopen( \
'https://www.amazontrust.com/repository/AmazonRootCA1.pem' \
).read()).hexdigest())"
Monitor for rotations before they bite: AWS announces root-CA changes in its security bulletins with a deprecation timeline, so a planned rotation can be shipped ahead of the cutover rather than during an outage.
Rotate (planned):
- Verify the new certificate out of band. A digest computed from the same connection that served the bytes proves nothing. Confirm the certificate against an independent source before it becomes a pin.
- Ship a release that adds the new pin and keeps the old one. Both stay valid, so peers still on the previous release keep verifying.
- Wait for fleet uptake. The overlap is bounded by the slowest fleet member, not by the release cadence.
- Drop the old pin in a follow-up release once uptake is complete.
Emergency (a rotation lands faster than a release can ship): stage the
verified new pin in STRANDS_MESH_CA_PINS (comma-separated, 64-char lowercase
hex). It is additive - the built-in pin stays accepted and verification stays
on - so it buys the grace period a release would have provided. Entries that are
not valid hex digests are rejected with a warning and skipped rather than
weakening the set. Remove the override once the release carrying the pin is
deployed.
STRANDS_MESH_DISABLE_CA_PIN is not part of this procedure. It turns the
download-path pin check off rather than widening it, accepts whatever the URL
serves, and marks the result as unverified-origin so later runs warn about
re-using it. It is a break-glass for a broken pin, never the response to a
rotation - a rotation has a verified pin to stage.