Skip to content

strands robots v0.5.2

Choose a tag to compare

@cagataycali cagataycali released this 17 Sep 08:24
01ab04d

strands-robots v0.5.2

Release date: 2026-09-16 Previous release: v0.5.1 (2026-08-06)

1500+ commits over six weeks - the biggest release since v0.5.0 and the first that
drives real robots without lerobot in the middle. A native driver layer (Unitree G1/Go2,
Booster T1, Franka, UR, Robotiq, Crazyflie, EarthRover, Reachy Mini, Pollen Microduck,
Feetech/Dynamixel buses), the Microduck as a sim + real robot with its own policy provider,
the operator-dashboard modules with a human-in-the-loop gate in front of every call that can
move hardware, the security pass (8 findings, 11 PRs, all merged), and a late pass
in which the tools stopped telling the agent things that were not true. vera,
motionbricks, the vendored LIBERO suite and lerobot_calibrate leave.

strands-robots is the robot-control library for Strands Agents:
policy inference, teleoperation, recording, and simulation (MuJoCo, Newton, Isaac) behind one
agent-callable tool surface - and, from this release, native drivers for the robots themselves.


Highlights

  • Native drivers. Robot(name, mode="real", driver="strands") builds a driver the library
    owns instead of a lerobot RobotConfig (#2734): Unitree G1 over CycloneDDS with a 500 Hz
    run_policy loop (#2739, #2779) and Go2 (#3039), Booster T1 (#3045), Franka
    panda/fr3 over FCI (#3040), UR5e/UR10e over RTDE (#3058), Robotiq 2F-85 (#3053),
    Crazyflie (#3046), EarthRover Mini+ (#3081), Reachy Mini (#2762), and the
    Feetech (#2786, #3043) / Dynamixel (#2750) bus codecs that move an SO-100/101.
    list_driver_coverage() says which driver builds which robot (#3042).

    arm = Robot("so101", mode="real", port="/dev/ttyACM0", driver="strands")
  • Pollen Microduck, sim + real. The 14-DOF biped joins the registry (#2856) with a
    microduck policy provider for Pollen's ONNX skills ([microduck], #2862), weights fetched
    by bare name from pollen-robotics/microduck-policies (#3704), a native robotd driver for
    the real duck, and examples/microduck/render_video.py --scene for the roller and ball scenes
    (#2922) - the ball now sits where the kick was trained to find it (#3712) and the clip says
    when the duck runs off the rendered floor (#3713).

microduck_roller microduck_walk

alpha_walking.onnx at 0.3 m/s · roller.onnx on scene_rollers.xml, headless on an L40S.

  • The tool tells the agent the truth. Eight agent prompts replayed against the same sim arm
    before and after this release's last fixes: 90 tool calls / 15 errors → 63 / 4. Nothing got
    smarter; replies that were true-but-useless got honest. The README quickstart -
    Robot("so100"), "pick up the red cube" - used to end:

    [tool←] 'red_cube' attached to 'so100/Fixed_Jaw' (mode=kinematic, relpos=[0.0098, -0.028, 0.0621])
    [llm]   🎉 The red cube has been successfully picked up!        ← it was 7 cm below the fingers
    

    and now ends:

    [tool←] set_gripper: … Closed on 'red_cube' (6 contacts).
    [tool←] 'red_cube' attached to 'so100/Fixed_Jaw' … The bodies are touching.
    [llm]   Attached the cube (gap = 0.0 m, touching); cube at z=0.106 m.
    

    set_gripper names what it closed on or says "Closed on nothing … move_to the object
    first"
    (#3705); attach_bodies measures the gap and calls a weld a weld (#3703); SO-arm
    joints answer to shoulder_pan, not servo "2" (#3694); step under a recording records
    (#3724); an evaluation with no success criterion prints no 0% to quote (#3700); the sim
    description says the world already exists (#3687); MockPolicy stays inside ctrlrange
    (#3688); a registry entry can declare the tool point a Menagerie model lacks - so100 now
    lands the low targets it missed 0/3 (#3728).

  • A paused real-robot call tells the script how to resume (#3719). Every call that would
    move real hardware raises the SDK's interrupt; its reason["how_to_answer"] gives the resume
    form and the exact ..._COMMAND_ALLOW=<value> that pre-approves that one command for an
    unattended run. docs/security.md → Answering a command gate from a script.

    result = agent("Rotate the wrist 5 degrees.")
    while result.stop_reason == "interrupt":
        for q in result.interrupts:
            print(q.reason["warning"], q.reason["how_to_answer"])
        result = agent([{"interruptResponse": {"interruptId": q.id, "response": input("approve? [y/N] ")}}
                        for q in result.interrupts])
  • G1 in simulation, end to end. The GR00T decoupled-WBC Balance+Walk pair drives the
    Menagerie G1 through run_policy(policy_provider="wbc", …): 0 → 2.8 m in 8 s at 0.45 m/s.
    scripted_g1.py keeps every segment it records (#3708); docs/policies/wbc.md says to mount a
    camera on the pelvis before recording - the fixed view loses a walking G1 after 1.5 m (#3716).

g1_wbc_walk
  • The operator dashboard serves ([dashboard]). python -m strands_robots dashboard is back
    (#3695) on the rails that landed as atomic slices: passkey/WebAuthn auth (#3050, hardened
    #3433/#3509, log-forgery-proof #3710), settings with redaction/timeouts/bounded cache (#3051,
    #3242), a Lockout type for what a UI may say about an e-stop (#3185), a policy preflight that
    reports what it verified (#3186), and the HITL gate - a BeforeToolCallEvent hook that pauses
    the agent on any call that would move a real robot (#3184). Stop verbs are never gated. It fails
    closed without its auth store and refuses cross-origin or DNS-rebound callers. Fleet and Sim tabs are
    live: one MuJoCo session per thread (max 4), MJPEG frames, telemetry over WebSocket, an e-stop
    that answers 423 under lockout and is never refused (#3726).
dashboard_settings
  • Security hardening (ROBOTS-52). Eight findings on the actuation path, eleven PRs, all merged:
    nothing reaches a real actuator without an operator's answer or an explicit envelope. See below.

Also new

  • Two robots of one type in one Agent - Robot("so101", tool_name="left_arm") (#3731); a
    bimanual pair or a real arm beside its sim twin no longer dies on Tool name … already exists.
  • Dependencies say what to install. Every optional-dependency door refuses with the install
    that fixes it, graded against what the extra really pulls in (#3777): unitree_sdk2py names the
    recipe at all 16 sites - PyPI's wheel does not import, so no extra can declare it (#3764);
    Robot(mode="real") on a core install names [lerobot] (#3769); serial_tool/pose_tool name
    pyserial (#3770); the MoveIt 2 sidecar names the system ROS 2 (#3780). New [rl] extra for
    create_trainer("ppo") (#3776); [ros2] stops promising a Linux-aarch64 wheel no cyclonedds
    release ever shipped (#3782); the build reads its version from release tags only, so a fork's
    own tags no longer break pip install -e . (#3779).
  • G1 agent surface - 25 verbs behind one use_unitree dispatcher instead of 120 names (#3037,
    #3070, −7,825 LOC): posture transitions, move_velocity, set_fsm, gestures, SLAM/lidar/BMS/IMU;
    every mutative verb asks an operator first (#3418, #3508).
  • Policy providers - kimodo text-to-motion for the G1 (#2270), protomotions ONNX GTP tracker
    (#2287), rl deploys a FastSAC/FastTD3 checkpoint (#3414) with FastTd3Trainer (#3206); a policy
    can declare the body poses it needs (#2271).
  • Mesh fleets - one MobileBaseRobot + Transport contract (#2696), AckermannRosRobot for ROS 2
    cars like the DeepRacer (#2689), peer retention with freshness bounds (#3352), five fleet examples
    with a read-only Rerun dashboard (#2197–#2506).
  • Isaac parity - IK move_to (#2218), set_gripper/rotate_wrist (#2200), synchronized
    run_multi_policy with merged recording (#2178, #2212, base-class contract #2164), LIBERO objects
    as real meshes (#2498).
  • Evaluation that answers the question - pass_hat_k estimated as C(c,k)/C(n,k), not
    success_rate**k (#3521); a rollout that never commanded the robot is an error (#3680).
  • Works on Jetson Thor - JetPack R38.2.2 / torch 2.11+cu130: doctor all green, SO-101 sim
    rollout rendered in 9 s; the Jetson page drops the numpy<2 pre-pin lerobot ≥0.6 undid (#3717).
thor_jetson_doctor

Breaking changes

  • Removed: lerobot_calibrate tool - use lerobot-calibrate (#3386); vera and motionbricks
    providers (#3389, #3419); vendored LIBERO strands_robots.benchmarks with its
    libero/robosuite/gymnasium pins - use physical-intelligence/libero + the predicate DSL,
    examples/libero/run.py stays (#3056, #2497); strands_robots.training.reward (#3443); the
    episode-augmentation transforms added and withdrawn this cycle (#3426).
  • Mutative tools pause for an operator. use_unitree, serial_tool, pose_tool, the Robot
    tool's execute/start, and every ROS 2 command verb raise the SDK's interrupt before real
    actuators move. Headless callers answer it (how_to_answer, #3719) or run in sim.
  • device_connect: an unset RPC allowlist admits nobody (#3423); an unauthenticated transport is
    refused (#3432). Set the allowlist explicitly.
  • strands-agents floor >=1.13.0 (BeforeToolCallEvent) · predicate DSL drops the <name>_main
    fallback (#3384) · move_to default tol 0.01 → 0.015 m · read_dataset_episode_indices moved to
    strands_robots.verify_dataset (#3446) · lerobot_local drops the norm_stats.json fallback (#3411).

New robots, drivers and policies

Registry 72 → 74: microduck (MuJoCo, 3 scenes; robotd real) (#2856, #2862) ·
bi_so_follower (aliases bi_so100/bi_so101; lerobot bimanual 2× SO follower) (#3470).

Native drivers (strands_robots.drivers, new): g1, go2 (CycloneDDS) · booster · franka
· ur · robotiq · crazyflie · earthrover · reachy (14 agent verbs, #3082) · microduck ·
feetech · dynamixel. The driver registry is joined against the robot registry so a coverage gap
is derived, not declared (#3042).

Policy providers (14; +4 −2):

Provider Extra What
microduck [microduck] Pollen ONNX skills; episodic behaviours auto-return (#2906)
kimodo [kimodo] text-to-motion diffusion, G1 (#2270, #2279)
protomotions [protomotions] ONNX GTP tracker, G1 (#2287)
rl core deploy a FastSac/FastTd3 checkpoint (#3414)

Removed vera, motionbricks. cosmos3 gains OpenArm (#2492); a sagemaker trainer submits a
TrainSpec as a managed job (#2488).

Simulation: articulated-container pouring task (#2483); set_obs_noise in the tool schema, and an
unadvertised action is refused at the agent boundary (#2095); scene-lit compositing and view-dependent
GS backgrounds (#2331, #2326); VLM judge for episode labeling (#2486);
examples/18_so101_pick_and_lift.py - a reference pick that actually lifts (#2685).

so101_pick_and_lift_smooth

Security hardening (ROBOTS-52)

Eight findings, eleven PRs, merged 2026-09-10/11. One rule: nothing reaches a real actuator
without an operator's answer or an explicit envelope.

Finding PR Fix
F-001 #3418, #3508 use_unitree asks before a mutative RPC; reads are an allowlist, everything else gated; private _Call refused
F-003 #3423, #3432 device_connect: unset allowlist admits nobody; plaintext zenoh refused
F-004 #3425 G1Driver.send_action refuses (not clamps) a target outside the per-joint URDF envelope
F-005 #3428 target_velocity held to ±2 m/s, ±2 rad/s on the wire and in WBC
F-007 #3433, #3509 First passkey enrollment refuses proxied loopback; needs a 0600 bootstrap token on disk
F-009 / F-010 / F-011 #3420 / #3430 / #3431 serial_tool, pose_tool, and the Robot tool's execute/start ask an operator first

Also: every ROS 2 transport's command verbs are gated (#2313, #2693); control characters refused in
wire instructions (#2403); use_rosbridge refuses a call missing type/topic before dialing
(#3682); every workflow uses: SHA-pinned (#2574); pose_tool scales the gripper off its range
like every other joint (#3735).


Dependency floors

Dependency v0.5.1 v0.5.2 Reason
strands-agents >=1.0.0,<2 >=1.13.0,<2 BeforeToolCallEvent (HITL gate)
eclipse-zenoh ([mesh]) >=1.0.0,<2 >=1.6.1,<2
cyclonedds >=0.10.2,<1 >=0.10.2,<12 native G1/Go2 drivers
mujoco-warp / mujoco ([sim-newton]) >=3.8 / >=3.2 >=3.11,<3.12 both newton>=1.5,<1.6 ABI
robot_descriptions · websockets >=1.11 · >=12 >=1.23 · >=17
new - fastapi, uvicorn, PyJWT, webauthn, python-multipart [dashboard]
new - ur-rtde, cflib, cbor2, onnxruntime [ur], [crazyflie], [microduck]
new - torch>=2, gymnasium>=0.29,<2 [rl] (#3776), in [all]
removed libero, robosuite, gymnasium, gym-pusht, hydra-core, omegaconf, pytorch-lightning, … - LIBERO / vera / motionbricks

requires-python stays >=3.12.


Fixed

1,011 fix: commits; one theme - refuse, don't guess: a non-finite pose, a misspelled flag, a
posture given as "no", a camera that cannot render, is an error at the door, not a silent wrong
answer downstream. The user-visible ones:

  • Simulation. Record-then-rollout works first try - an omitted rate follows the one in force
    (#3791); stop_policy returns once the robot is free and a bare call stops the only rollout
    (#3743, #3788); run_policy/eval_policy refused while another rollout drives the robot (#3741);
    replay_episode reads the dataset this session recorded (#3786); a hardware-only kwarg on a sim
    robot is refused instead of building a sim under status=success while the real arm sits still
    (#3740); a provider the constructor refuses is a status=error envelope, not "started" (#3746);
    the JSON schema names every knob the runtime accepts (#3781); every listed camera renders (#3683,
    #3693, #3691); a joint write says its unit and a range refusal names the degrees→radians
    conversion (#3723); get_robot_state says which way the arm extends (#3727); a move_to or
    rotate_wrist the servo could not finish names the contact pair or joint limit that stopped it
    (#3733, #3754); relative scene paths land in ~/.strands_robots/scenes, not your checkout
    (#3711); render_dir= on the constructor (#3697); stop_when/benchmark clauses true at reset are
    reported, never scored, and a clause that cannot fire is refused (#3633–#3642, #3497, #3502);
    out-of-range or non-finite poses/seeds/scales refused everywhere (#3429, #3462, #2740–#2881);
    boolean posture flags checked as booleans (#3365–#3575); start_recording names its cameras and
    refuses one it cannot render (#3730, #3494); EGL renderers closed before the display terminates
    (#3706).
  • Isaac / Newton. A recorded rollout leaves the robot idle (#3341); undriven state columns filled
    (#2844); IK solves on the bound model (#3220); stage paths are USD prim paths (#3269).
  • Policies / inference. lerobot_local camera pre-flight compares against the checkpoint's own
    config.json (#3737); camera_key_map on the declarative path (#3555); RTC on public flow-matching
    checkpoints (#3474); create_policy("groot", hots=…) refuses the typo instead of dialing localhost
    (#3747); GR00T's server fails fast with the URI, timeout and a TCP probe (#3742), mounts checkpoints
    read-only (#3793) and accepts hf_local_dir under the caller's home (#3755); a task envelope says
    when the policy never read the instruction it echoes (#3763); cuRobo/ProtoMotions remedies name
    installs that work (#3777); CompositePolicy children never see an empty observation (#3372);
    Microduck gravity slot, velocity gate and finite-observation checks (#3228, #2902, #2890).
  • Drivers / hardware. Feetech Present_Position carries its sign (#3387), whole-arm SYNC_READ
    (#3393), torque confirmed by the servo (#3407), degrees against the arm's own calibration (#3397);
    G1/Go2 telemetry fields the message lacks are None, not zero (#3376–#3382), the write gate shuts
    when the robot is not free (#3609); UR/Crazyflie/EarthRover/Reachy refuse what their wire cannot
    carry (#3210, #3326, #3353, #3679, #3493). A doomed real-robot command is refused before the
    operator is asked (#3753, #3756); lerobot_local with no checkpoint is refused before the bus is
    claimed (#3752); a bring-up that fails after connect releases the arm instead of leaving torque on
    (#3778); a real robot's status names the device, its port, and each camera (#3748, #3790); a task
    reports how long it actually ran (#3775).
  • Mesh / teleop. Posture warnings once per process (#3690); the discovery example reads the
    type key and waits a heartbeat (#3689); Mesh.tell says an execute needs a policy (#3750);
    emergency_stop halts the issuer's own robot and stop survives lockout (#3450); non-finite wire
    timestamps are neither stale nor future (#3243); JointState names its joints (#3648); DDS
    credentials that are not strings refused (#3666, #3674).
  • Tools / training. serial_tool/pose_tool name pyserial (#3770); lerobot_train refuses a
    topology it cannot honour (#3121); session/calibration/pose files written whole or not at all
    (#3328–#3348); a torchcodec wheel that cannot load costs one warning, not lerobot's 150-line wall
    (#3792); recorder features are HWC and add_frame refuses a missing column (#3451, #3449);
    evaluate() says whether success_rate measured the policy (#3639); [rl] extra (#3776).
  • Dashboard. A credential store that cannot serve is recovered, not a 500 (#3738); no
    header can forge a log line (#3710); passkey ceremonies verified against the connection (#3080);
    a one-shot motion grant names the call approved (#3254).
  • doctor / packaging / registry / rendering. doctor reaches the runtime's verdict per row and
    fits the torch remedy to the board (#3458, #3454, #2893); version from release tags only (#3779);
    torchcodec locked to 0.11.1 for torch 2.11 (#3424); [ros2] aarch64 story honest (#3601, #3782);
    aloha is Dynamixel ViperX, not Feetech (#3406); a symlink in a downloaded robot tree is never
    followed into the cache (#3325); a composited frame is its own camera's size (#3140); an MP4 request
    without a backend is refused as the missing dependency (#3320).

Docs & CI

  • README is 120 lines (#3469); every environment variable is documented and a grader says so
    (#3313). Install docs create the venv first (#3696). Robot pages are generated cards from
    robots.json (#3470). Quickstart step 1 records a real arm with lerobot_teleoperate (#3749) and
    the closing line names what steps 4–5 need (#3751). New: docs/policies/microduck.md (#2927), the
    CA-pin rotation runbook (#2576), mesh security knobs (#2945–#3003), a Linux-aarch64 section in
    docs/rtps-integration.md (#3782), "Installing the Unitree SDK" in docs/humanoids.md (#3764).
    Removed: dashboard pages that launched a command the package lacked (#3472).
  • CI. The required check evaluates the merge commit (#3395); every uses: is SHA-pinned
    (#2574); every job is bounded (#2241); changelog.d/ fragments are the only way a PR logs a change,
    placeholders refused with one owner of the rule (#3715, #3721, #3722); CHANGELOG.md is assembled at
    tag time with python scripts/assemble_changelog.py --apply.

Upgrade

uv venv --python 3.12 && source .venv/bin/activate        # uv pip install needs an active venv
uv pip install -U "strands-robots==0.5.2"
uv pip install -U "strands-robots[sim-mujoco,microduck]==0.5.2"   # sim + the duck (weights from the Hub on first use)
uv pip install -U "strands-robots[dashboard]==0.5.2"               # then: python -m strands_robots dashboard
UV_TORCH_BACKEND=auto uv pip install "strands-robots[sim-mujoco,lerobot]==0.5.2"   # Jetson / aarch64

Before upgrading, check for: lerobot_calibrate, vera/motionbricks, strands_robots.benchmarks,
strands_robots.training.reward, and any headless caller of use_unitree / serial_tool /
pose_tool / the Robot tool on real hardware - those now raise an interrupt an operator must
answer, and the interrupt says how (reason["how_to_answer"]). One screen of this: UPGRADE.md.
From a clone or fork, the version comes from git describe --match 'v[0-9]*'; with no release tag
reachable it builds as 0.1.dev… - git fetch upstream --tags restores the real number (#3779).


Contributors

@cagataycali, @yinsong1986, @sundargthb, @rmncardoso, @Vivek0712.