[build-system]
requires = ["hatchling", "hatch-vcs"]
build-backend = "hatchling.build"

[project]
name = "strands-robots"
dynamic = ["version"]
description = "AI-powered robot control, simulation, and training for Strands Agents - integrates with MuJoCo, Isaac Sim, Newton, and many more"
readme = "README.md"
requires-python = ">=3.12"
license = {text = "Apache-2.0"}
authors = [
    {name = "AWS", email = "opensource@amazon.com"},
]
keywords = [
    "robots", "agents", "strands", "sdk",
    "mujoco", "simulation", "lerobot", "reinforcement-learning",
    "gr00t", "isaac-sim", "vla",
    "robot-learning", "imitation-learning",
    "teleoperation", "manipulation", "strands-agents",
]
classifiers = [
    "Development Status :: 4 - Beta",
    "Intended Audience :: Developers",
    "Intended Audience :: Science/Research",
    "License :: OSI Approved :: Apache Software License",
    "Operating System :: OS Independent",
    "Programming Language :: Python :: 3",
    "Programming Language :: Python :: 3.12",
    "Programming Language :: Python :: 3.13",
    "Topic :: Scientific/Engineering :: Artificial Intelligence",
    "Topic :: Scientific/Engineering :: Physics",
    "Topic :: Software Development :: Libraries :: Python Modules",
]

dependencies = [
    # CAPABILITY floor, not a nominal lower bound. The package imports
    # ``strands.types.tools.ToolContext`` (first shipped in strands-agents
    # 1.5.0) and ``strands.types._events.ToolResultEvent`` (first shipped in
    # 1.7.0), both at module import time. Measured against the released
    # wheels, every release below 1.7.0 leaves
    # ``strands_robots.simulation.mujoco.simulation`` and
    # ``strands_robots.hardware_robot`` unimportable, so a resolve inside an
    # older-but-declared range yields an install where ``import
    # strands_robots`` succeeds (those are lazy exports) and
    # ``strands_robots.Simulation`` simply does not exist. See
    # tests/test_strands_agents_floor_ships_the_imported_api.py, which fails
    # the moment a newer strands symbol is imported without raising this floor.
    "strands-agents>=1.7.0,<2.0.0",
    "numpy>=1.21.0,<3.0.0",
    "opencv-python-headless>=4.5.0,<5.0.0",
    "Pillow>=8.0.0,<13.0.0",
]

[project.optional-dependencies]
groot-service = [
    "pyzmq>=27.0.0,<28.0.0",
    "msgpack>=1.0.0,<2.0.0",
]
cosmos3-service = [
    # Self-contained msgpack+NumPy WebSocket client for the Cosmos 3 RoboLab
    # policy server (cosmos_framework.scripts.action_policy_server_robolab).
    #
    # No openpi-client dependency: the wire protocol is implemented directly
    # via `websockets` + `msgpack` plus a vendored numpy packer
    # (strands_robots/policies/cosmos3/_msgpack_numpy.py). This keeps the
    # extra numpy-version agnostic so it composes cleanly with `lerobot`
    # (numpy>=2) for dataset recording in the same env.
    "msgpack>=1.0.0,<2.0.0",
    # >=13.0 is the floor of the websockets API the package reaches for, not a
    # preference: `websockets.sync.server.Server` (annotating
    # PolicyServer._server) first ships in 13.0 - 12.0 spells that class
    # `WebSocketServer`. One environment resolves one websockets, so this floor
    # and [inference]'s are deliberately the same number. Owned and measured by
    # tests/test_websockets_floor_ships_the_imported_api.py.
    "websockets>=13.0",
]
# In-process Cosmos 3 backend (Cosmos3Policy(backend="diffusers")). Loads the
# Cosmos3OmniPipeline directly via native Hugging Face `diffusers` (no RoboLab
# policy server, no GPU isolation) - one forward pass returns the predicted
# world video + sound + action chunk. This is a HEAVY GPU stack (diffusers +
# torch + transformers); the default backend stays "service", so a plain
# `pip install strands-robots[cosmos3-service]` (msgpack + websockets only) is
# unaffected. diffusers composes with numpy>=2, so this extra is co-installable
# with cosmos3-service and lerobot.
#
# The floor is the CAPABILITY floor, not a nominal lower bound: measured against
# the released wheels, Cosmos3OmniPipeline + CosmosActionCondition first ship in
# diffusers 0.39.0 (0.36.0/0.37.1/0.38.0 carry neither symbol), so any floor below
# it resolves to a diffusers that cannot build the pipeline at all. A checkpoint
# newer than the floor can still need a newer diffusers than it (Cosmos3-Edge is
# built against 0.40.0.dev0); that is a per-checkpoint fact a version range cannot
# express, so Cosmos3DiffusersBackend reports it at load time instead.
cosmos3-diffusers = [
    "diffusers>=0.39",
    "torch>=2.0",
    "transformers>=4.40",
    "accelerate>=0.26",
]
# Cosmos 3 -> MuJoCo sim-loop bridge. The diffusers backend emits the model's
# raw unified action ([-1, 1] quantile-normalized relative end-effector pose,
# not joint radians); turning that into joint targets a MuJoCo arm can track
# needs de-normalization (bundled per-domain q01/q99 stats, pure NumPy) plus an
# inverse-kinematics solve. This extra pulls `mink` (differential IK on the same
# mujoco.MjModel) + `mujoco`. Install it to drive a sim arm with
# Cosmos3Policy(backend="diffusers") via decode_cosmos_chunk_to_targets /
# MinkIKBridge. numpy>=2 compatible (co-installable with cosmos3-diffusers,
# cosmos3-service, sim-mujoco, and lerobot).
cosmos3-sim = [
    "mink>=0.0.4",
    "mujoco>=3.5.0,<4.0.0",
    # qpsolvers QP backend for mink.solve_ik. quadprog is the dependency-light
    # default that is always available; the bridge auto-selects whatever
    # qpsolvers reports (preferring daqp when present).
    "qpsolvers[quadprog]>=4.0.0",
]
# MoveIt2 sidecar client (ROS 2 / moveit_py runs out-of-process; the
# client is a lightweight ZMQ + msgpack peer so users without ROS 2
# sourced are unaffected). See policies/moveit2/server/ for the
# sidecar reference implementation.
moveit2 = [
    "pyzmq>=27.0.0,<28.0.0",
    "msgpack>=1.0.0,<2.0.0",
]
# cuRobo collision-aware motion planning. Runs in-process as a CUDA
# library (no sidecar, unlike MoveIt2). Users without a CUDA-capable GPU
# should not install this extra. See policies/curobo/ for the policy
# wrapper.
#
# IMPORTANT: cuRobo is NOT distributed on PyPI. The package named
# ``nvidia-curobo`` on PyPI is an unrelated v0.1 squatter — installing
# it does not get you a working cuRobo. The real cuRobo is published
# only as source on GitHub at https://github.com/NVlabs/curobo. Users
# must install it from source before using this policy:
#
#   git clone https://github.com/NVlabs/curobo.git
#   pip install -e ./curobo
#
# Once cuRobo's API surface stabilises and a real PyPI release exists,
# this extra will pin to it. Until then, keep the extra empty so
# ``pip install strands-robots[curobo]`` is a no-op rather than a
# silently-broken install of the squatter package. Tracked in the
# follow-up issue linked from PR #306.
curobo = []
# GR00T Whole-Body-Control (SONIC) locomotion policy. Runs the ONNX
# controllers in-process via onnxruntime (no torch, no sidecar) - a light
# extra, like the client side of moveit2. Model weights are NOT bundled;
# they are fetched at runtime under the NVIDIA Open Model License
# (e.g. nvidia/GEAR-SONIC). See policies/wbc/ and issue #466.
wbc = [
    "onnxruntime>=1.17.0,<2.0.0",
    # The upstream GR00T-WBC checkpoint config (g1_gear_wbc.yaml) is YAML;
    # WBCConfig.from_file reads it via pyyaml. JSON configs work without it.
    "pyyaml>=5.1,<7.0.0",
    # Resolving a HuggingFace model id checkpoint (e.g. the default
    # nvidia/GEAR-SONIC, or any "org/repo") downloads it via huggingface_hub.
    # Required so create_policy("wbc") with no local checkpoint works out of the
    # box - WBCPolicy._maybe_download_checkpoint raises RuntimeError naming the
    # [wbc] extra when it is absent.
    # Floor at >=1.5 (cap the MAJOR only, <2.0.0): the docs' bucket-sync
    # guidance (README streamed-training section + dataset_recorder.sync_to_bucket)
    # instructs `pip install -U "huggingface_hub>=1.5"` because the `hf buckets`/
    # `hf sync` CLI subcommands first ship in huggingface_hub 1.5.0
    # (huggingface_hub/cli/buckets.py, registered by cli/hf.py). Every earlier
    # release installs an `hf` entry point WITHOUT them - 0.36.x, but also
    # 1.0-1.4.x, which answer `hf buckets create` with
    # "Error: No such command 'buckets'". A floor below 1.5 therefore lets a
    # fresh resolve land on a CLI that cannot run a bucket sync while satisfying
    # the documented minimum. lerobot floors it higher still
    # (lerobot 0.6.1 requires huggingface-hub>=1.6.0) and [wbc] is pulled into
    # [all] alongside lerobot, so the two co-resolve on the stricter of the two.
    "huggingface_hub>=1.5,<2.0.0",
]
motionbricks = [
    # MotionBricks (NVlabs/GR00T-WholeBodyControl `motionbricks/`) generative
    # kinematic motion for the G1. The `motionbricks` package itself is NOT on
    # PyPI - install it editable from the upstream checkout AFTER this extra:
    #   git clone https://github.com/NVlabs/GR00T-WholeBodyControl
    #   git -C GR00T-WholeBodyControl lfs pull --include="motionbricks/out/**" --exclude=""
    #   pip install -e GR00T-WholeBodyControl/motionbricks
    # This extra pulls the PyPI support libraries its model code imports so the
    # editable install resolves cleanly. Model weights are NOT bundled; they are
    # fetched with git-LFS under the NVIDIA Open Model License. See
    # policies/motionbricks/ and docs/policies/motionbricks.md.
    "torch>=2.0.0",
    "vector-quantize-pytorch>=1.14.0",
    "adam-atan2-pytorch>=0.0.12",
    "hydra-core>=1.3.0,<2.0.0",
    "omegaconf>=2.3.0,<3.0.0",
    "pytorch-lightning>=2.0.0,<3.0.0",
    "scipy>=1.10.0",
]
kimodo = [
    # Kimodo text-conditioned motion diffusion for the Unitree G1. Drives a
    # `diffusers` pipeline checkpoint (one carrying a `model_index.json`) with
    # `trust_remote_code=True` (gated by STRANDS_TRUST_REMOTE_CODE at the
    # factory layer). NVIDIA's own `nvidia/Kimodo-G1-RP-v1` publishes bare
    # weights for its own runtime instead, so that checkpoint is driven through
    # a `motion_agent=` sampler rather than by this extra alone. Weights are
    # fetched on demand; nothing bundled.
    # See strands_robots/policies/kimodo/ and docs/policies/kimodo.md.
    "torch>=2.0.0",
    "diffusers>=0.30.0",
    "transformers>=4.40.0",
    # Same >=1.5 floor and MAJOR-only cap as [wbc]: the documented bucket-sync
    # guidance needs the `hf buckets`/`hf sync` CLI subcommands, which first
    # ship in huggingface_hub 1.5.0. See the [wbc] comment for the full reason.
    "huggingface_hub>=1.5,<2.0.0",
    "accelerate>=0.30.0",
    "scipy>=1.10.0",
]
protomotions = [
    # ProtoMotions Generalist Tracking Policy (GTP) - the ONNX whole-body
    # motion tracker that turns a kinematic reference clip (e.g. one Kimodo
    # samples) into balanced joint targets for the Unitree G1. Same three
    # support libraries as [wbc], for the same three reasons: onnxruntime runs
    # the exported graph, pyyaml reads the `unified_pipeline.yaml` sidecar next
    # to it, and huggingface_hub fetches the checkpoint from a model id. Weights
    # are NOT bundled. See strands_robots/policies/protomotions/ and
    # docs/policies/protomotions.md.
    "onnxruntime>=1.17.0,<2.0.0",
    "pyyaml>=5.1,<7.0.0",
    # Same >=1.5 floor and MAJOR-only cap as [wbc]: the documented bucket-sync
    # guidance needs the `hf buckets`/`hf sync` CLI subcommands, which first
    # ship in huggingface_hub 1.5.0. See the [wbc] comment for the full reason.
    "huggingface_hub>=1.5,<2.0.0",
]
lerobot = [
    # [dataset] bundles the read-back stack the streaming data loop needs:
    # datasets + pandas + pyarrow (parquet streaming) + av + torchcodec
    # (remote video decode for StreamingLeRobotDataset / StreamingDatasetReader).
    # Declaring it here makes `sim.stream_dataset(...)` work out of the box
    # instead of pushing torchcodec/av plumbing onto user code. See
    # reports/STREAMING_DATA_LOOP_DEEP_DIVE.md Appendix C.
    #
    # lerobot 0.6 ships mature, platform-correct torch/torchvision/torchcodec
    # dependency markers, so the codec/decoder stack resolves ABI-consistently
    # on every platform (torch 2.11 + torchcodec 0.11.x + torchvision 0.26 on
    # linux x86_64/aarch64 and macOS arm64) WITHOUT the per-platform torchcodec
    # overrides the 0.5.1 range needed. The aarch64 torch>=2.11 requirement --
    # which fixes the NVIDIA Thor/Jetson sm_110 cuBLAS bug present in torch 2.10
    # -- now falls out of lerobot 0.6's own `torchcodec>=0.11,<0.12; aarch64`
    # marker. NOTE: torch/torchcodec ship as +cuXXX wheels resolved by
    # UV_TORCH_BACKEND; a plain `pip install` on Thor may need the matching CUDA
    # index -- see the README Installation section for the Thor/Jetson caveat.
    #
    # The floor is 0.6.1 rather than 0.6.0 because bucket streaming
    # (`stream_dataset(repo_type="bucket")`) needs a StreamingLeRobotDataset
    # that accepts `repo_type`, and 0.6.1 is the first release that does
    # (0.6.0's constructor has no such parameter). Flooring here makes the
    # flagship streaming path resolver-guaranteed instead of docs-guaranteed;
    # `strands_robots.streaming_dataset.BUCKET_STREAMING_MIN_LEROBOT` carries
    # the same version for the runtime guard that protects an environment with
    # a pre-existing older lerobot.
    "lerobot[feetech,dataset]>=0.6.1,<0.7.0",
]
# Remote/offloaded inference via lerobot's native async-inference gRPC
# transport (the `lerobot_async` policy provider, LerobotAsyncPolicy). Layers
# grpcio on top of the base lerobot stack so a light robot host can drive a
# policy running on a separate GPU server (lerobot.async_inference.policy_server).
# Mirrors lerobot's own [async] extra dependency (grpcio) rather than pinning
# it here, so the gRPC/protobuf floor tracks lerobot.
lerobot-async = [
    "strands-robots[lerobot]",
    "lerobot[async]>=0.6.1,<0.7.0",
]
# MolmoAct2 transformers-native VLA (e.g. allenai/MolmoAct2-SO100_101). The
# MolmoAct2Policy ships in lerobot >= 0.6 (landed via lerobot PR #3604). Rather
# than hand-mirror lerobot's aux-dep pins here (transformers/peft/scipy) -- which
# silently drifts whenever lerobot bumps them -- we defer entirely to lerobot's
# own ``[molmoact2]`` extra. That extra pulls lerobot[transformers-dep],
# lerobot[peft-dep], lerobot[scipy-dep] at exactly the versions the MolmoAct2
# modeling/processor code was built against, so the two stay in lock-step by
# construction. The base ``strands-robots[lerobot]`` pin (>=0.6.1,<0.7.0) already
# fixes the lerobot floor; ``lerobot[molmoact2]`` just adds the aux deps.
molmoact2 = [
    "strands-robots[lerobot]",
    "lerobot[molmoact2]>=0.6.1,<0.7.0",
]
sim = [
    # 1.23.0 is the oldest release that ships a description module for every
    # robot the built-in registry names, not merely the oldest that imports.
    # registry/robots.json declares 57 ``asset.robot_descriptions_module``
    # values and the package grows one module per new robot, so an older
    # release is missing some of them outright: 1.11.0 lacks 27 (including
    # so100 and so101), and every release below 1.23.0 lacks at least one
    # (openarm's ``openarm_v1_mj_description`` arrives last). None of those
    # robots declares an ``asset.source`` GitHub fallback, so
    # assets.download.auto_download_robot has nothing to fall back to and
    # ``Robot("so101", mode="sim")`` fails to resolve a model file.
    # tests/test_robot_descriptions_floor_ships_the_registry_modules.py owns
    # the number and derives it from the registry; keep the two in step.
    "robot_descriptions>=1.23.0,<2.0.0",
]
sim-mujoco = [
    "strands-robots[sim]",
    # 3.5.0 is the oldest release the backend can drive, not merely import:
    # every scene is built through the MjSpec procedural API, and URDF loading
    # through it first works in 3.5.0 (3.4.0 and earlier refuse add_robot with
    # "Could not find decoder for resource '<path>.urdf'"), while MjSpec.delete
    # - which world creation needs on its first call - first ships in 3.3.4.
    # simulation.mujoco.backend._MUJOCO_API_FLOOR owns the number and refuses an
    # older build at the import funnel; keep the two in step.
    "mujoco>=3.5.0,<4.0.0",
    "imageio>=2.28.0,<3.0.0",
    "imageio-ffmpeg>=0.4.0,<1.0.0",
    # Differential-IK solver stack. `move_to`, the Cartesian transport
    # primitive in this backend's agent-callable action enum, solves IK on the
    # same mujoco.MjModel via strands_robots.simulation.ik.MinkIKBridge, so the
    # solver is part of what this extra ships rather than an add-on the caller
    # has to name themselves. `qpsolvers` is declared here because
    # simulation/ik.py imports it directly, and the `[daqp]` backend extra
    # because qpsolvers on its own solves nothing: with no backend installed
    # `mink.solve_ik` cannot run and `move_to` returns "IK bridge unavailable:
    # No qpsolvers backend is installed", whose remedy names THIS extra. daqp is
    # both the bridge's first preference (ik._PREFERRED_QP_SOLVERS) and the
    # backend `mink` pulls for itself, so this changes nothing about what
    # resolves - it stops the guarantee resting on a transitive of `mink` that
    # this extra does not control. `[cosmos3-sim]`, which drives the same
    # mink + qpsolvers stack, declares its backend the same way.
    "mink>=0.0.4",
    "qpsolvers[daqp]>=4.0.0",
]
# Newton GPU-native backend (newton-physics/newton on NVIDIA Warp +
# MuJoCo-Warp). GPU-batched parallel envs, headless ray-traced rendering, and
# the same MJCF assets as the MuJoCo backend. Requires an NVIDIA GPU (Maxwell+,
# driver 545+, CUDA 12); on CPU-only hosts Warp falls back to the CPU device.
# trimesh loads the collision/visual meshes referenced by the MJCF assets;
# mujoco-warp provides the GPU MuJoCo solver used by the default `solver`.
sim-newton = [
    "strands-robots[sim-mujoco]",
    "newton>=1.3.0,<2.0.0",
    "warp-lang>=1.14.0,<2.0.0",
    "mujoco-warp>=3.8.0",
    "trimesh>=4.0.0,<5.0.0",
]
# Isaac Sim GPU-native backend (NVIDIA Isaac Sim / Omniverse). Selected via
# create_simulation("isaac") / Robot(..., backend="isaac"); the vendored
# in-tree backend lives at strands_robots.simulation.isaac (parent epic #1144).
# Provides photorealistic RTX rendering, synthetic-data generation, and
# GPU-batched sensor simulation. GPU/heavy - kept OUT of `[all]` (like
# sim-newton's spirit but even more so: Isaac Sim needs an RTX GPU + an
# out-of-band Kit runtime).
#
# IMPORTANT: Isaac Sim itself is NOT installed by this extra. It is an Omniverse
# Kit application provisioned separately - via the runtime's own pip wheels
# (`pip install 'isaacsim[all,extscache]==6.0.*' --extra-index-url
# https://pypi.nvidia.com`, viable on Python 3.12; the `extscache` extra is
# required, and see docs/simulation/isaac.md for the coverage-downgrade /
# EULA / torch-bump caveats, #1803), the NVIDIA Omniverse Launcher,
# Isaac Lab (`./isaaclab.sh -i`), or the NGC docker image
# (`nvcr.io/nvidia/isaac-sim:6.0`; see
# strands_robots/simulation/isaac/_install.py for the pinned image tag). Those
# routes ship a complete, bootable Kit (Python 3.12 interpreter + the
# full `isaacsim-*` / `isaacsim-extscache-*` extension set that `SimulationApp`
# needs to boot). We still do NOT pin `isaacsim` here: the bare `isaacsim[all]`
# metapackage is incomplete on its own (it omits the `isaacsim-extscache-*`
# packages, so `SimulationApp` aborts resolving its extension graph), it needs
# the NVIDIA extra index, and its transitive pins (coverage==7.4.4, a
# torch/torchvision bump) would degrade every non-Isaac install of this extra.
#
# This extra pulls ONLY the pip-installable Python helpers the backend uses
# out-of-band from an Isaac Sim install:
#   * `usd-core` - pure-Python USD runtime used by the procedural scene
#     builders / loaders (loaders.load_usd) even when Kit isn't booted. usd-core
#     uses CalVer (25.x/26.x); per AGENTS.md `>=1.0` deps cap the major.
#   * `imageio` / `imageio-ffmpeg` - rollout-video encoding.
# All heavy omni/isaacsim imports stay lazy - installing this extra never
# imports Isaac Sim.
sim-isaac = [
    "strands-robots[sim]",
    "usd-core>=25.5,<27.0.0",
    "imageio>=2.28.0,<3.0.0",
    "imageio-ffmpeg>=0.4.0,<1.0.0",
]
# 3D Gaussian Splatting hybrid rendering (strands_robots.rendering:
# GsplatBackground + HybridCompositor). GPU/heavy - kept OUT of `[all]`
# (sim-isaac precedent).
#
# IMPORTANT: `gsplat` ships to PyPI as a source distribution that JIT-compiles
# its CUDA kernels with `nvcc` on FIRST USE (same supply-chain class as cuRobo
# above). On a box with a CUDA *runtime* but no CUDA *toolkit* - e.g. the
# Isaac Sim container - `pip install gsplat` succeeds and imports fine, yet the
# first rasterization call fails ("No CUDA toolkit found"). Call
# `strands_robots.rendering.gsplat_rasterizer_available()` to probe an install
# before relying on it, or install a pre-built wheel from the gsplat wheel
# index (ships compiled kernels, no nvcc needed):
# https://docs.gsplat.studio/main/installation.html
# `PanoramaBackground` is the zero-GPU-dep fallback; torch is pinned without a
# CUDA constraint (the probe fails loud on CPU-only builds instead of dying
# deep inside the first frame).
sim-gs = [
    "gsplat>=1.4.0,<2.0.0",
    "plyfile>=1.0.0,<2.0.0",
    "torch>=2.2.0,<3.0.0",
]
benchmark-libero = [
    "libero>=0.1.0,<1.0.0",
]
mesh = [
    "eclipse-zenoh>=1.0.0,<2.0.0",
    "json5>=0.9.0,<1.0.0",
]
# AWS IoT Core integration for the mesh - MQTT5/mTLS transport, Device
# Shadow mirror, S3 camera offload, account-wide bootstrap (Rules /
# Lambda / DynamoDB / Fleet Provisioning template). Requires:
#   * awsiotsdk (high-level MQTT5 + mTLS client builder)
#   * awscrt    (low-level CRT runtime; pulled in by awsiotsdk but pinned
#                here for transitive predictability)
#   * boto3     (used for IoT control-plane provisioning + S3 frame upload)
mesh-iot = [
    "strands-robots[mesh]",
    "awsiotsdk>=1.21.0,<2.0.0",
    "awscrt>=0.20.0,<1.0.0",
    "boto3>=1.34.0,<2.0.0",
]
# Device Connect - device-aware networking layer (discovery, RPC, events,
# safety). The primary transport in server mode; when this extra is not
# installed, robot_mesh() falls back to the built-in Zenoh mesh.
device-connect = [
    "device-connect-edge>=0.2.0",
    "device-connect-agent-tools>=0.1.0",
]
# ROS 2 bridge for the use_ros tool (strands_robots/tools/use_ros.py). The tool
# runs entirely in-process through rclpy - no `ros2` CLI shelling, no docker. It
# needs `rclpy` + `rosidl_runtime_py` importable in the interpreter, which means
# a sourced system ROS 2 distro (e.g. `source /opt/ros/jazzy/setup.bash`).
# Those core client libraries are NOT distributed on PyPI - they ship with the
# system ROS 2 install (apt / RoboStack / conda) and cannot be `pip install`ed.
# So, like the [curobo] extra, this stays minimal: it pulls only the
# pip-installable cyclonedds Python binding (a DDS RMW rclpy can use). Installing
# this extra does NOT by itself provision ROS 2 - source a real distro first.
ros2 = [
    "cyclonedds>=0.10.2,<1.0.0",
]
# NOTE: There is intentionally no `vera` extra. VERA is only distributed as a
# git repository (https://github.com/sizhe-li/VERA) and PyPI rejects any package
# whose metadata contains a direct URL/VCS reference (HTTP 400 on upload). The
# VERA provider launches the git-installed `vera` package as a subprocess
# (`python -m vera.server...`), so install it directly alongside strands-robots:
#     pip install "strands-robots[vera-sim]" websockets msgpack "numpy>=1.24"
#     pip install "vera @ git+https://github.com/sizhe-li/VERA.git"
# See README (VERA row) and strands_robots/policies/vera/__init__.py.
vera-sim = [
    "gymnasium==0.29.1",
    "gym-pusht==0.1.5",
    "robosuite==1.4.1",
    # robomimic v0.5.0 is NOT published to PyPI -- the highest PyPI release is
    # 0.3.0, and v0.5.0 exists only as an ARISE-Initiative GitHub tag. Pinning
    # `robomimic==0.5.0` here is therefore both unresolvable (uv lock fails) and
    # a dependency-confusion risk (anyone publishing 0.5.0 to PyPI would be
    # installed). robomimic is not imported anywhere in the source; VERA pulls
    # it in itself. If you need robomimic v0.5.0, install it from source:
    #     pip install "robomimic @ git+https://github.com/ARISE-Initiative/robomimic.git@v0.5.0"
    # MimicGen (NVlabs) is NOT published to PyPI -- the `mimicgen` name on PyPI
    # is an unaffiliated third-party package, so pinning it here is a
    # dependency-confusion risk and it is not imported anywhere in the source.
    # The `mimicgen` VERA *embodiment* is just a config string and does not need
    # the package. If you genuinely need NVlabs MimicGen for data generation,
    # install it from source:
    #     pip install "mimicgen @ git+https://github.com/NVlabs/mimicgen.git"
    "mujoco>=3.5.0,<4.0.0",
    "imageio",
]
# Local-LLM agent path (no API key / no AWS -- the natural choice on an edge
# device): strands.models.ollama.OllamaModel does a top-level `import ollama`.
# Delegate to strands-agents' own [ollama] extra (bounded pin, ollama>=0.4.8,<1.0.0)
# so the client version stays in sync with the code that imports it.
ollama = [
    "strands-agents[ollama]>=1.7.0,<2.0.0",
]
# Remote policy inference (client/server split): stream observations from a
# CPU/edge robot host to a remote GPU PolicyServer and receive action chunks
# back over a portable WS-JSON WebSocket protocol. Pulls only `websockets`
# (numpy-version agnostic, so it composes with lerobot's numpy>=2). See
# strands_robots/inference/ and docs/inference/remote.md.
inference = [
    # See [cosmos3-service]: >=13.0 is where `websockets.sync.server.Server`
    # first ships, and both extras must declare the same floor.
    "websockets>=13.0",
]
# rosbridge WebSocket transport: drive ROS1 (or remote ROS 2) robots from a host
# with no ROS install at all. Pure-pip `roslibpy` speaks the rosbridge JSON
# protocol over a WebSocket. See strands_robots/tools/use_rosbridge.py and
# docs/rosbridge-integration.md.
rosbridge = [
    "roslibpy>=1.7.0,<2.0.0",
]
all = [
    "strands-robots[groot-service]",
    "strands-robots[moveit2]",
    "strands-robots[lerobot]",
    "strands-robots[lerobot-async]",
    "strands-robots[sim-mujoco]",
    "strands-robots[mesh]",
    "strands-robots[mesh-iot]",
    "strands-robots[device-connect]",
    "strands-robots[wbc]",
    "strands-robots[motionbricks]",
    "strands-robots[kimodo]",
    "strands-robots[protomotions]",
    "strands-robots[molmoact2]",
    "strands-robots[ollama]",
    "strands-robots[inference]",
    "strands-robots[rosbridge]",
]
dev = [
    "pytest>=6.0,<10.0.0",
    "pytest-cov>=4.0.0,<6.0.0",
    "ruff>=0.15.12,<0.16.0",
    "mypy>=1.0.0,<2.0.0",
    "pytest-timeout>=2.0.0,<3.0.0",
]

[project.scripts]
strands-robots = "strands_robots.__main__:main"

[project.urls]
Homepage = "https://github.com/strands-labs/robots"
Documentation = "https://github.com/strands-labs/robots#readme"
Repository = "https://github.com/strands-labs/robots.git"
Issues = "https://github.com/strands-labs/robots/issues"
Changelog = "https://github.com/strands-labs/robots/releases"

[tool.hatch.version]
source = "vcs"

[tool.hatch.build.targets.wheel]
packages = ["strands_robots"]

[tool.hatch.envs.default]
installer = "uv"
features = ["all"]
dependencies = [
    "pytest>=6.0,<10.0.0",
    "pytest-cov>=4.0.0,<6.0.0",
    "pytest-timeout>=2.0.0,<3.0.0",
    "ruff>=0.15.12,<0.16.0",
    "mypy>=1.0.0,<2.0.0",
    "Pillow>=8.0.0,<13.0.0",
    "requests>=2.28.0,<3.0.0",
    "msgpack>=1.0.0,<2.0.0",
    "pyzmq>=27.0.0,<28.0.0",
]

# ---------------------------------------------------------------------------
# uv resolution overrides.
#
# lerobot 0.6 declares platform-correct torch/torchvision/torchcodec markers
# (torch>=2.7,<2.12; on linux aarch64 its `torchcodec>=0.11,<0.12` marker pulls
# torch 2.11 -- the build that fixes the NVIDIA Thor/Jetson sm_110 cuBLAS bug
# that afflicts torch 2.10), so the whole torch/torchvision/torchcodec stack
# resolves ABI-consistently on every platform (torch 2.11 + torchcodec 0.11.x +
# torchvision 0.26) with NO strands torch override. Paired with
# UV_TORCH_BACKEND=auto below, a Thor `hatch env` resolves the +cu130 wheels and
# inference runs on GPU. The sole remaining override is the diffusers security
# floor.
[tool.uv]
# The legacy VERA sim stack ([vera-sim]) pins gymnasium==0.29.1, which is
# mutually exclusive with lerobot>=0.6.0 (requires gymnasium>=1.1.1). uv
# resolves all extras jointly by default, so without a fork declaration this
# single pin drags the WHOLE resolution below lerobot 0.6 (previously froze the
# lock at lerobot 0.5.1, making vla_jepa/molmoact2/eo1/lerobot.rl unreachable).
# Declaring the conflict makes uv fork the resolution: install [vera-sim] OR the
# lerobot-0.6 extras, never both in one environment.
conflicts = [
    [{ extra = "vera-sim" }, { extra = "lerobot" }],
    [{ extra = "vera-sim" }, { extra = "lerobot-async" }],
    [{ extra = "vera-sim" }, { extra = "molmoact2" }],
    [{ extra = "vera-sim" }, { extra = "all" }],
    [{ extra = "vera-sim" }, { extra = "benchmark-libero" }],
]
# SECURITY floors for transitive packages. Each of these arrives only through
# another dependency (cbor2/ujson under autobahn, twisted under roslibpy,
# pyopenssl under twisted), so nothing in [project] declares a version for
# them and the resolver is free to pick any release the graph allows --
# including one inside a HIGH advisory range. A constraint bounds a transitive
# without claiming it as a direct dependency, and unlike an override it fails
# the resolution loudly if something genuinely requires less, which is the
# wanted behaviour for a security floor.
#
# Each floor is the first version that clears every HIGH/CRITICAL advisory for
# that package, not the version currently resolved: the floor states the
# requirement, so it stays correct as the resolution moves.
constraint-dependencies = [
    # GHSA-3c37-wwvx-h642 (HIGH): uncontrolled recursion DoS in cbor2.loads, <= 5.8.0.
    "cbor2>=5.9.0",
    # GHSA-grgv-6hw6-v9g4 (HIGH): twisted.names DNS compression-pointer DoS,
    # <= 25.5.0. First patched in 26.4.0rc2; 26.4.0 is the first stable release
    # that clears it, and a floor at the rc would admit a prerelease.
    "twisted>=26.4.0",
    # GHSA-c38f-wx89-p2xg (HIGH): memory leak in ujson.dump() on write failure, <= 5.12.0.
    "ujson>=5.12.1",
    # GHSA-5pwr-322w-8jr4 (HIGH): DTLS cookie-callback buffer overflow, >= 22.0.0 < 26.0.0.
    "pyopenssl>=26.0.0",
]
override-dependencies = [
        # SECURITY + CAPABILITY: diffusers <0.36 is vulnerable to a GHSA
    # trust_remote_code bypass + a TOCTOU RCE (fixed in 0.38.0). uv *overrides*
    # replace a requirement rather than intersecting with it, so this pin is the
    # effective floor for the whole resolution and must not fall below the
    # cosmos3-diffusers extra's own floor - at 0.38.0 it silently discarded that
    # floor and locked a diffusers carrying no Cosmos3OmniPipeline. 0.39 clears
    # both bounds (the CVE fix landed in 0.38.0; the pipeline first ships in
    # 0.39.0). diffusers is only exercised by the cosmos3-diffusers extra
    # (Cosmos3Policy backend="diffusers"); lerobot core does not import it.
    "diffusers>=0.39",
]

# Auto-select the right PyTorch wheel for the host accelerator. On NVIDIA
# Thor / Jetson (linux + aarch64) the plain-PyPI torch wheel is CPU-ONLY, so
# torch.cuda.is_available() is False and all inference silently runs on CPU.
# "auto" makes uv probe the local driver/SoC and resolve the matching build
# (e.g. +cu130 on Thor) WITHOUT changing the pinned version.
#
# IMPORTANT: uv's "auto" detection is HOST-based, not target-platform based.
# If "auto" is hardcoded here it contaminates EVERY resolution -- including
# x86 CI -- with the build host's CUDA variant. On a Thor that means x86 CI
# resolves +cu130 wheels (proven: `uv pip compile --python-platform
# x86_64-unknown-linux-gnu` yields torchcodec==0.10.0+cu130), which cannot
# import on the GPU-less runner, so tests/conftest.py silently falls back to
# the numpy torch mock and torch-dependent tests break.
#
# So we INHERIT the value from the process env and only DEFAULT to "auto":
#   - Thor/Jetson dev: nothing set -> "auto" -> GPU wheels.
#   - x86 CI: workflow exports UV_TORCH_BACKEND=cpu -> CPU wheels, importable.
# A hatch pyproject env-var normally OVERRIDES the process env; the
# {env:VAR:default} form is what lets the process value win.
[tool.hatch.envs.default.env-vars]
UV_TORCH_BACKEND = "{env:UV_TORCH_BACKEND:auto}"

[tool.hatch.envs.default.scripts]
test = "pytest {args:tests}"
test-integ = "pytest tests_integ/ -v --timeout=300 {args}"
lint = ["ruff check strands_robots tests tests_integ", "ruff format --check strands_robots tests tests_integ", "mypy strands_robots tests tests_integ"]
format = ["ruff check --fix strands_robots tests tests_integ", "ruff format strands_robots tests tests_integ"]

[tool.ruff]
line-length = 120
target-version = "py312"

[tool.ruff.lint]
select = [
    "E",     # pycodestyle errors
    "W",     # pycodestyle warnings
    "F",     # pyflakes
    "I",     # isort
    "UP",    # pyupgrade
    # Two flake8-bugbear codes, selected individually rather than as "B".
    # They carry the no-op-statement capability that CodeQL's
    # py/ineffectual-statement used to nominally provide before
    # .github/codeql/codeql-config.yml filtered it (#1810): every one of that
    # rule's 27 alerts was a `...` typing-construct body, which ruff exempts,
    # so the rule reported only unavoidable idioms while the real bug class
    # went unenforced by any merge-blocking check. Ruff is that check here.
    "B015",  # useless comparison - `x == 1` as a statement, the real bug class
    "B018",  # useless expression - a value computed and discarded
]
ignore = [
    "E501",  # line too long - handled by formatter
]

[tool.ruff.lint.isort]
known-first-party = ["strands_robots"]

[tool.ruff.format]
quote-style = "double"
indent-style = "space"

[tool.mypy]
python_version = "3.12"
# The VERA docker/ scripts run INSIDE the strands-vera-server container
# (they import vera / torchmetrics / omegaconf, present only in that image),
# so exclude them from the host-side mypy run.
exclude = "strands_robots/policies/vera/docker/"
warn_return_any = true
warn_unused_configs = true
disallow_untyped_defs = true
ignore_missing_imports = false

# Third-party libs without type stubs
[[tool.mypy.overrides]]
module = ["lerobot.*", "gr00t.*", "tyro", "draccus.*", "msgpack.*", "websockets", "websockets.*", "zmq.*", "huggingface_hub.*", "serial.*", "psutil.*", "torch.*", "torchvision.*", "transformers.*", "einops.*", "robot_descriptions.*", "mujoco.*", "imageio.*", "pyarrow", "pyarrow.*", "libero.*", "zenoh.*", "boto3", "boto3.*", "awscrt", "awscrt.*", "awsiot", "awsiot.*", "botocore.*", "strands_robots.policies.cosmos3._msgpack_numpy", "diffusers", "diffusers.*", "accelerate", "accelerate.*", "openpi_client", "openpi_client.*", "openpi_server", "openpi_server.*", "openpi", "openpi.*", "device_connect_edge", "device_connect_edge.*", "device_connect_agent_tools", "device_connect_agent_tools.*", "rclpy", "rclpy.*", "cyclonedds", "cyclonedds.*", "roslibpy", "roslibpy.*", "rosidl_runtime_py", "rosidl_runtime_py.*", "moveit", "moveit.*", "moveit_configs_utils", "moveit_configs_utils.*", "geometry_msgs", "geometry_msgs.*", "mink", "mink.*", "qpsolvers", "qpsolvers.*", "onnxruntime", "onnxruntime.*", "motionbricks", "motionbricks.*", "trimesh", "trimesh.*", "stable_baselines3", "stable_baselines3.*", "strands_robots_sim", "strands_robots_sim.*", "isaacsim", "isaacsim.*", "omni", "omni.*", "gsplat", "gsplat.*", "plyfile", "plyfile.*"]
ignore_missing_imports = true

# Device Connect drivers - thin wrappers over the untyped device_connect_edge
# SDK; their RPC handlers return dicts that bubble Any (same posture as tools.*).
# disallow_untyped_defs stays relaxed because the SDK's @rpc/@emit/@on
# decorators are themselves untyped (they make decorated methods "untyped" to
# mypy, which the global config cannot see through). disallow_any_generics is
# enabled to keep the public dict/list/Callable surfaces fully parameterized
# rather than silently bare (which a developer's IDE/strict mypy would flag).
[[tool.mypy.overrides]]
module = ["strands_robots.device_connect.*"]
disallow_untyped_defs = false
disallow_any_generics = true
warn_return_any = false

# @tool decorator injects runtime signatures mypy cannot check
[[tool.mypy.overrides]]
module = ["strands_robots.tools.*"]
disallow_untyped_defs = false
warn_return_any = false
ignore_missing_imports = true

# RL trainers (training.rl.*) build torch modules with dynamic tensor-dict
# returns; torch's untyped surface makes warn_return_any noise here, same as the
# policies posture. Public signatures are still annotated.
[[tool.mypy.overrides]]
module = ["strands_robots.training.rl.*"]
warn_return_any = false
disallow_untyped_defs = false

# LeRobot local policy uses dynamic attr access via draccus PreTrainedConfig
[[tool.mypy.overrides]]
module = ["strands_robots.policies.*"]
warn_return_any = false
disallow_untyped_defs = false

# Robot core - complex dynamic assembly, optional deps
[[tool.mypy.overrides]]
module = ["strands_robots.robot"]
disallow_untyped_defs = false
warn_return_any = false

# RTPS IDL bundle - cyclonedds IdlStruct uses metaclass keyword args (typename=)
# that mypy reads as __init_subclass__ kwargs on object. The classes are dynamic
# by construction (DDS type registration), so relax call-arg checking here.
[[tool.mypy.overrides]]
module = ["strands_robots.rtps.idl"]
disable_error_code = ["call-arg"]
disallow_untyped_defs = false

# __init__.py has lazy import helpers
[[tool.mypy.overrides]]
module = ["strands_robots.__init__"]
disallow_untyped_defs = false

# AWS IoT bootstrap modules - boto3 always returns Any from API calls.
# We type the public surface explicitly (ProvisionedThing, BootstrappedAccount)
# but the internal helpers naturally bubble Any from boto3.
[[tool.mypy.overrides]]
module = ["strands_robots.mesh.iot.*", "strands_robots.mesh.transport.iot_transport"]
warn_return_any = false

# Registry modules - dynamic JSON loading returns Any
[[tool.mypy.overrides]]
module = ["strands_robots.registry.*"]
warn_return_any = false
disallow_untyped_defs = false

# MuJoCo simulation - mixins use cooperative self._world patterns
# attr-defined: Mixins access self._world/self._lock/etc. from Simulation (cooperative pattern)
# assignment: PEP 484 implicit Optional (= None on typed params)
# override: Subclass signatures extend base with extra params (orientation, mesh_path)
# misc: Multiple inheritance method resolution conflicts between mixin + ABC
[[tool.mypy.overrides]]
module = ["strands_robots.simulation.mujoco.*"]
disallow_untyped_defs = false
warn_return_any = false

# Isaac Sim backend - same override-signature / cooperative-mixin patterns as
# the MuJoCo backend. The subclass extends the SimEngine ABC with extra,
# backend-specific params on add_robot (mjcf_path / usd_path) and add_object
# (**kwargs escape hatch), and narrows send_action's action type to the
# dict|ndarray the Isaac articulation API consumes -- all deliberate,
# documented extensions. Heavy omni/isaacsim imports are untyped (no stubs).
# override: add_robot / add_object / send_action extend the base signature.
[[tool.mypy.overrides]]
module = ["strands_robots.simulation.isaac.*"]
disallow_untyped_defs = false
warn_return_any = false
disable_error_code = ["override"]

# Async utils and dataset recorder - thin wrappers with dynamic types
[[tool.mypy.overrides]]
module = ["strands_robots._async_utils", "strands_robots.dataset_recorder"]
disallow_untyped_defs = false
warn_return_any = false

# Test files - relaxed type checking for mocks, fixtures, and test utilities
[[tool.mypy.overrides]]
module = ["tests.*", "tests_integ.*"]
disallow_untyped_defs = false
warn_return_any = false

[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = "-v --cov=strands_robots --cov-report=term-missing --cov-fail-under=80 --strict-markers --timeout=120"
markers = [
    "gpu: requires CUDA GPU and real model weights (run with: pytest -m gpu)",
    "slow: long-running test (deselect with: pytest -m 'not slow')",
    "integration: requires live Device Connect infra (Zenoh router + etcd via Docker); skipped unless DEVICE_CONNECT_ALLOW_INSECURE is set (deselect with: pytest -m 'not integration')",
    "moveit2: requires a running MoveIt2 sidecar (ROS 2 / moveit_py). Run with: pytest -m moveit2",
    "curobo: requires NVIDIA cuRobo + a CUDA-capable GPU. Run with: pytest -m curobo (set CUROBO_LIVE=1)",
    "wbc: requires onnxruntime + a downloaded GR00T-WBC (SONIC) checkpoint + mujoco. Run with: pytest -m wbc (set WBC_LIVE=1)",
    "motionbricks: requires the [motionbricks] extra + the upstream `motionbricks` package + git-LFS checkpoints + mujoco. Run with: pytest -m motionbricks (set MOTIONBRICKS_CKPT=<out dir>)",
    "molmoact2: requires lerobot >= 0.5.2 (from source) + the [molmoact2] extra (transformers, peft, scipy). Run with: pytest -m molmoact2",
    "rtps: requires cyclonedds ([ros2] extra) + a live ROS 2 peer (e.g. turtlesim) on the DDS domain. Run with: RTPS_LIVE=1 pytest -m rtps",
]
