Registry¶
Resolve an alias to a canonical robot, list robots by category, inspect a provider entry, add your own.
The registry is strands_robots/registry/robots.json and policies.json, read through the functions below: resolve an alias to a canonical robot name, list robots by category, inspect a provider entry, add your own robot without editing the package.
Robots¶
Robot registry - query, resolve, and list robot definitions.
All robot definitions live in robots.json. This module provides the public read API; the JSON file is the only thing you edit to add or modify robots.
resolve_name ¶
Resolve a robot name or alias to the canonical name.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
Any robot name, alias, or data_config string. |
required |
Returns:
| Type | Description |
|---|---|
str
|
Canonical robot name (e.g. "so100", "panda", "unitree_g1"). |
Examples::
resolve_name("franka") # → "panda"
resolve_name("SO100_follower") # → "so100"
resolve_name("g1") # → "unitree_g1"
get_robot ¶
Get full robot definition by name or alias.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
Robot name, alias, or data_config. |
required |
Returns:
| Type | Description |
|---|---|
dict[str, Any] | None
|
Robot dict with keys like description, category, joints, asset, |
dict[str, Any] | None
|
hardware - or None if not found. A |
dict[str, Any] | None
|
robot (see :func: |
dict[str, Any] | None
|
gets a synthesized entry with |
dict[str, Any] | None
|
always wins over it. |
list_robots ¶
List available robots, optionally filtered.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
mode
|
str
|
Filter, one of :data:
|
'all'
|
Returns:
| Type | Description |
|---|---|
list[dict[str, Any]]
|
List of dicts with name, aliases, description, category, joints, |
list[dict[str, Any]]
|
has_sim, has_real and source. |
list[dict[str, Any]]
|
func: |
list[dict[str, Any]]
|
declares none), so |
list[dict[str, Any]]
|
|
list[dict[str, Any]]
|
entry and |
list[dict[str, Any]]
|
backend compiles on first use; a URDF robot whose description does |
list[dict[str, Any]]
|
not build is listed with |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
list_robots_by_category ¶
Group every registered robot under the category name it is listed by.
A group name is something a caller switches on and a reader sees in a table
cell, so every group here has one. A robot whose registry entry declares no
category - category is optional in both the package registry and the
user overlay, and :func:register_robot accepts category="" - is
grouped under "other", and a declared name is stripped of surrounding
whitespace so a padded spelling joins its own group instead of opening a
blank-looking second one beside it. :func:list_robots still reports each
robot's category exactly as the entry declares it; the normalization is
this grouping's, not the registry's.
Returns:
| Type | Description |
|---|---|
dict[str, list[dict[str, Any]]]
|
Group name to the :func: |
dict[str, list[dict[str, Any]]]
|
in exactly one group, so the group sizes sum to |
joint_labels ¶
Meaningful names for a robot's simulation joints, {joint: label}.
Some assets name their joints by servo id (SO-101: 1..6) or by
CAD term (SO-100: Rotation, Jaw), while the same arm's driver and
LeRobot datasets speak shoulder_pan .. gripper. The registry's
optional joint_labels block bridges the two so an agent can address a
joint by what it does. Returns {} for an unknown robot or one that
declares no labels.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
Robot name, alias, or data_config. |
required |
Returns:
| Type | Description |
|---|---|
dict[str, str]
|
Mapping from the asset's joint name (as |
dict[str, str]
|
without the robot namespace) to its label. |
has_sim ¶
Check if Robot(name) can obtain a simulation model (MJCF/URDF).
An entry declaring auto_download: false is never fetched, so it counts
only once its model file is on disk; any other asset entry is downloaded on
first use. A pure filesystem check - no download, no network.
has_hardware ¶
Check if a robot declares a real-hardware backend.
Reads the registry entry's hardware block, which has two independent
fields: lerobot_type names a lerobot robot type, and driver names
which driver builds the robot. Either alone is a hardware declaration --
the Reachy Mini and the Microduck declare only a driver, because lerobot
has no robot type for them at all -- so this reads the block rather than one
field of it.
A robot may be drivable without declaring anything: a native driver
registered through
:func:~strands_robots.drivers.register_native_driver needs no registry
declaration (every driver shipped here declares one; a driver package
outside this one may not).
Declaration and registration are two different facts and this predicate
reports only the first, because it is the one a caller can read without
importing a driver package.
:func:~strands_robots.drivers.list_driver_coverage joins both and is what
answers "can this robot be driven for real" completely.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
Robot name, alias, or data_config. |
required |
Returns:
| Type | Description |
|---|---|
bool
|
True when the robot's registry entry carries a |
bool
|
False when it does not or the robot is not registered at all. |
get_driver ¶
Get the driver a robot declares, verbatim, or None if it declares none.
A pure reader, like its sibling :func:get_hardware_type: it reports what
the registry says and applies no default. hardware.driver is optional and
most robots declare none. Where it is declared it says which of two possible
drivers wins: one declarer has no lerobot robot type at all, so the native
driver is the only one that can build it; the other has a working lerobot
type and prefers its native driver anyway.
An absent declaration therefore means "no preference", not "no native
driver" - a robot may have one registered and declare nothing, in which case
the default still routes to lerobot. Deciding what an absent - or "auto"
- declaration means is
:func:~strands_robots.drivers.resolve_driver's job, which is also where a
caller's explicit choice outranks the registry.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
Robot name, alias, or data_config. |
required |
Returns:
| Type | Description |
|---|---|
str | None
|
The declared driver name, or |
str | None
|
(or is not registered at all). |
get_hardware_type ¶
Get the LeRobot hardware type for a robot.
Returns:
| Type | Description |
|---|---|
str | None
|
LeRobot type string (e.g. "so100_follower"), or None. |
format_robot_table ¶
Human-readable table of all robots for CLI/tool output.
The Sim and Real columns hold the ASCII token "yes" when the
robot declares that mode and are left blank otherwise. Real reads the
registry's hardware block, the same declaration
:func:has_hardware reports: a robot a native driver can build without
declaring one is left blank here, and
:func:~strands_robots.drivers.list_driver_coverage is what answers "can
this robot be driven for real" completely. The output is
pure ASCII so it aligns correctly in any monospace terminal and is safe
to embed in logs and tool responses.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
max_width
|
int
|
Target terminal width. The |
100
|
Returns:
| Type | Description |
|---|---|
str
|
Multi-line string: a header row, a rule, one row per robot grouped |
str
|
by category (common categories first, then any custom categories in |
str
|
sorted order), then a totals footer. Every registered robot gets |
str
|
exactly one row, so the body row count always matches the footer |
str
|
|
Loader¶
JSON registry loader with content-keyed hot-reload and validation.
Loads robots.json and policies.json from the registry directory, re-reading only when the on-disk source changes. Validates uniqueness of aliases, shorthands, and URL patterns on every reload.
The robots registry is not a single file: its effective contents are the
package robots.json merged with the user-local overlay
($STRANDS_BASE_DIR/user_robots.json, read through :mod:._overlay - see
:func:_merge_user_robots). The
hot-reload signature therefore covers both files, so an edit to the user
overlay made outside this process (a second process, a manual edit, or any
writer that does not call :func:invalidate_cache) is picked up on the next
read - honoring the "re-read when the source changes" contract for the overlay
just as for the package file.
The signature is the file contents, not a stat. A timestamp cannot express
"the source changed": the kernel stamps st_mtime from a coarse clock, so
two writes inside one tick share a timestamp and the second one is invisible -
permanently, because that timestamp never changes again. The bytes are the
only field that always differs when the contents differ, so a cached value is
served only while the file still holds the bytes it was parsed from. The read
is what licenses the hit; the parse and validation are the work it saves.
normalize_robot_name ¶
Fold a robot name or alias to the key every registry lookup uses.
One rule, spelled once: lowercase, trim surrounding whitespace, and read a
dash as an underscore. It is the rule
:func:~strands_robots.registry.robots.resolve_name applies to a caller's
query, so it is also the rule the things a query is matched against have to
be keyed by - a canonical robot name (normalized by
:func:~strands_robots.registry.user_registry.register_robot), an alias
(keyed by :func:~strands_robots.registry.robots._build_alias_map), and the
uniqueness constraints :func:_validate_robots enforces over both. A
consumer that folds while a producer or a validator does not is how
"Franka-Panda" becomes a lookup nobody can satisfy, or worse, one that
lands on a different robot. A user_robots.json key that is not already
its own fold is refused when the registry loads, naming the spelling to
rename it to.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
A robot name or alias, in any spelling. |
required |
Returns:
| Type | Description |
|---|---|
str
|
The lookup key for |
Raises:
| Type | Description |
|---|---|
ValueError
|
|
invalidate_cache ¶
Invalidate cached registry data, forcing a reload on next access.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str | None
|
Registry name to invalidate (e.g. "robots"). If None, clears all. |
None
|
Discovery through robot_descriptions¶
Auto-discovery of robots from the optional robot_descriptions package.
The curated registry (robots.json) deliberately carries only robots that
need project-specific metadata: hardware ports, custom joint counts, scene
tweaks, aliases, or local mesh overrides. The much larger long tail of standard
robots shipped by robot_descriptions (MuJoCo Menagerie and friends) does not
need a hand-written entry - this module resolves those on demand so
Robot("go2", mode="sim") works without touching robots.json.
Resolution rules
- A curated
robots.jsonentry always wins. Discovery is consulted only for names unknown to the curated registry (see :func:strands_robots.registry.get_robot). - Curated-registry / MJCF discovery (
descriptions_module,list_discoverable,discover_robot) is MJCF-only: the MuJoCo backend and the curated registry need an.xmlmodel. - URDF discovery (
urdf_descriptions_module,list_urdf_discoverable,discover_urdf_path) is a parallel surface for URDF-native backends (Newton viaModelBuilder.add_urdf). It covers the large URDF-only long tail that MJCF discovery cannot (humanoids, quadrupeds, hands). - The MuJoCo backend reaches that same long tail through
:func:
discover_robot: a name with a URDF description and no MJCF one gets an entry whoseasset.source.typeis"urdf", and :mod:strands_robots.assets.urdfcompiles the URDF intorobot.xml+scene.xmlon first resolution (:func:list_urdf_onlynames them). - :func:
descriptions_module, :func:is_discoverable, and :func:list_discoverableare cheap - a static dict lookup with no module import and no network. :func:discover_robotis heavy: importing a description module makesrobot_descriptionsclone the upstream asset repository on first use, so it is called only from asset-resolution paths that are already allowed to download.
is_discoverable ¶
Return True if name resolves from robot_descriptions (cheap).
list_discoverable ¶
Return sorted canonical names resolvable from robot_descriptions.
This is the MJCF long tail - the standard robots that work in the MuJoCo
backend without a curated robots.json entry (e.g. go2, spot,
h1, anymal_c, cassie). Cheap: no import, no network.
discover_robot ¶
Synthesize a registry entry for name from robot_descriptions.
Heavy: imports the description module, which makes robot_descriptions
clone the upstream asset repository on first use. Call only from
asset-resolution paths that are allowed to download. Results (including
misses) are cached.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
Robot name or alias (e.g. |
required |
Returns:
| Type | Description |
|---|---|
dict[str, Any] | None
|
A registry-style entry with the same shape as a |
dict[str, Any] | None
|
an |
dict[str, Any] | None
|
or |
dict[str, Any] | None
|
robot. |
descriptions_module ¶
Return the MJCF robot_descriptions module for name, or None.
Cheap: a dict lookup against the static description table, with no module
import and no network. Returns None when the robot is not an
MJCF-capable robot_descriptions robot or when the package is missing.
Examples::
descriptions_module("go2") # -> "go2_mj_description"
descriptions_module("so100") # -> None (curated, not a description)
is_urdf_discoverable ¶
Return True if name resolves to a URDF robot_descriptions model.
list_urdf_discoverable ¶
Return sorted canonical names resolvable to a URDF from robot_descriptions.
This is the URDF long tail consumable by URDF-native backends (Newton). It
includes robots with no MJCF model at all (e.g. atlas_v4, baxter,
b1), which is why it is disjoint from :func:list_discoverable for those
entries. Cheap: no import, no network.
discover_urdf_path ¶
Resolve the on-disk URDF path for name via robot_descriptions.
Heavy: imports the description module, which makes robot_descriptions
clone the upstream asset repository on first use. Call only from
asset-resolution paths that are allowed to download.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
Robot name or alias (e.g. |
required |
Returns:
| Type | Description |
|---|---|
str | None
|
Absolute path to the URDF file, or |
str | None
|
URDF-capable |
str | None
|
imported, or it exposes no readable |
urdf_descriptions_module ¶
Return the URDF robot_descriptions module for name, or None.
Cheap: a dict lookup against the static description table, with no module
import and no network. Returns None when name is not a URDF-capable
robot_descriptions robot or when the package is missing.
Examples::
urdf_descriptions_module("panda") # -> "panda_description"
urdf_descriptions_module("atlas_v4") # -> "atlas_v4_description"
urdf_descriptions_module("so100") # -> None (curated, not a description)
User robots¶
User-local robot registry - runtime registration without editing package JSON.
Provides register_robot() and unregister_robot() for adding custom
robots that persist across sessions via a user_robots.json file stored
alongside the asset cache.
File location (in priority order):
1. $STRANDS_BASE_DIR/user_robots.json
2. ~/.strands_robots/user_robots.json
Note
STRANDS_ASSETS_DIR only controls where assets live, not the
user registry. Use STRANDS_BASE_DIR to relocate user metadata.
At load time the user overlay is merged on top of the package
robots.json - user entries win on name collision, so you can also
override built-in robots locally.
Usage::
from strands_robots.registry import register_robot, unregister_robot
# Register a custom robot with MJCF
register_robot(
name="my_arm",
model_xml="my_arm.xml",
description="My custom 6-DOF arm",
category="arm",
joints=6,
asset_dir="my_arm", # resolved relative to assets dir
)
# Now works everywhere:
from strands_robots.simulation import create_simulation
sim = create_simulation()
sim.create_world()
sim.add_robot("my_arm") # auto-resolved
# Remove it
unregister_robot("my_arm")
register_robot ¶
register_robot(name: str, *, model_xml: str | None = None, description: str = '', category: str = 'arm', joints: int = 0, asset_dir: str | None = None, scene_xml: str | None = None, aliases: list[str] | None = None, robot_descriptions_module: str | None = None, hardware: dict[str, Any] | None = None, overwrite: bool = False) -> dict[str, Any]
Register a custom robot in the user-local registry.
.. warning:: Security
This function is a **library-only** API and must NOT be exposed
as an agent @tool without additional safeguards. A malicious
agent could register a robot pointing to attacker-controlled MJCF
that executes code via MuJoCo plugins. If tool exposure is needed
in the future, gate it behind STRANDS_TRUST_REMOTE_CODE and
validate all paths with :func:`strands_robots.utils.safe_join`.
The robot becomes immediately available in get_robot(),
list_robots(), resolve_model_path(), sim.add_robot(), etc.
A robot registered without model_xml is available to the registry
readers only, not to resolve_model_path() or sim.add_robot().
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
Canonical robot name (lowercase, underscores). |
required |
model_xml
|
str | None
|
Path to MJCF/URDF model file, relative to |
None
|
description
|
str
|
Human-readable description. |
''
|
category
|
str
|
Robot category, one of the groups the catalog shows (arm,
bimanual, hand, humanoid, expressive, mobile, mobile_manip,
aerial) or a new group of your own. A spelling close to a known
group ( |
'arm'
|
joints
|
int
|
Number of actuated joints. |
0
|
asset_dir
|
str | None
|
Directory containing the model file and meshes.
- Absolute path: used as-is ( |
None
|
scene_xml
|
str | None
|
Scene XML (with ground/lights). Defaults to |
None
|
aliases
|
list[str] | None
|
Alternative names for this robot. Folded to a lookup key the
same way |
None
|
robot_descriptions_module
|
str | None
|
Optional |
None
|
hardware
|
dict[str, Any] | None
|
Hardware config dict: |
None
|
overwrite
|
bool
|
If False (default), raises ValueError if name is already
registered, in the user overlay or as a built-in robot. True
replaces the user entry, or shadows the built-in until
:func: |
False
|
Returns:
| Type | Description |
|---|---|
dict[str, Any]
|
The registered robot definition dict. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If name already exists (user overlay or built-in) and
|
TypeError
|
If hardware is given and is not a dict, or model_xml is
given and is not a |
FileNotFoundError
|
If |
Example::
register_robot(
name="my_arm",
model_xml="my_arm.xml",
asset_dir="~/robots/my_arm_v2",
description="Custom 6-DOF arm with gripper",
category="arm",
joints=7,
aliases=["myarm", "custom_arm"],
)
register_robot("drone", category="aerial", hardware={"driver": "strands"})
unregister_robot ¶
Remove a robot from the user-local registry.
Does not affect the package robots.json. If the robot exists
only in the package registry, this is a no-op.
A key spelled exactly as name is removed first, so a hand-written key the
loader refuses as not folded (rover-001) can be removed by the spelling
the refusal quotes. Otherwise name is folded and that key is removed. The
overlay file is read directly, not through the merged registry, which fails
to load while such a key is present.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
Robot name to remove, as the exact key in |
required |
Returns:
| Type | Description |
|---|---|
bool
|
True if the robot was removed, False if it wasn't in the user registry. |
list_user_robots ¶
List all user-registered robots.
Returns:
| Type | Description |
|---|---|
list[dict[str, Any]]
|
List of dicts with name, description, category, path info. |
Policy providers¶
Policy registry - resolve, import, and configure policy providers.
All provider definitions live in policies.json. This module provides the public read API for resolving smart policy strings, importing provider classes, and building provider-specific kwargs.
get_policy_provider ¶
Get policy provider config by name or alias.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
Provider name or alias (e.g. "lerobot", "cosmos", "remote"). |
required |
Returns:
| Type | Description |
|---|---|
dict[str, Any] | None
|
Provider dict with module, class, config_keys, defaults, etc. |
dict[str, Any] | None
|
None if not found. |
list_policy_providers ¶
List all registered policy provider names (canonical only).
list_policy_aliases ¶
Return the full alias -> canonical provider mapping.
:func:list_policy_providers reports canonical names only, but
:func:get_policy_provider, :func:resolve_policy and
create_policy all accept a provider's declared aliases and
shorthands as well. This is the surface that enumerates them, so a
caller can discover every spelling the registry honours instead of
having to already know it.
A provider that redundantly lists its own canonical name among its
aliases contributes no entry: a name is not an alias of itself, and
such an entry would double-count the spelling
:func:list_policy_providers already reports.
Returns:
| Type | Description |
|---|---|
dict[str, str]
|
Mapping of alias to the canonical provider name it resolves to. |
resolve_policy ¶
Resolve a smart policy string to (provider_name, kwargs).
Accepts HuggingFace model IDs, server URLs, or shorthand names and returns the canonical provider + ready-to-use kwargs.
Resolution order
- URL patterns declared in
policies.json(ws://, wss://, cosmos3://) - Shorthand names (mock, lerobot_local, remote, ...); a removed
provider's spelling (:data:
REMOVED_PROVIDERS) is refused here - HuggingFace model IDs (org/model)
- Registered provider name
- Fallback to lerobot_local
Stage 1 recognises exactly the forms the registry declares: the
url_patterns entries providers carry are the whole vocabulary. A
scheme:// outside it is refused here, naming
:func:_declared_url_schemes, because it is an address and no later stage
can dial one -- it would reach stage 3 or stage 5 and be forwarded to
lerobot_local as a checkpoint id, so the caller's next report is a
HuggingFace lookup failure for a string that was never a repo id.
A scheme-less host:port address is matched only by a provider that
declares a scheme-less pattern -- the generic server_address branch
below exists for that -- and none of the shipped providers declares one, so
with the shipped registry such a string reaches stage 5 and is forwarded to
lerobot_local as a checkpoint id rather than dialled as an address. It
carries no scheme, so the refusal above does not read it as an address
either.
Every stage matches case-insensitively. A URL scheme is folded per RFC 3986
section 3.1 (WS://gpu:8765 resolves exactly as ws://gpu:8765, and
the emitted URL carries the lowercased scheme); shorthands and provider
names are lowercased; a HuggingFace org is matched lowercased while the repo
id itself is forwarded exactly as given, since repo ids are case-sensitive.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
policy
|
str
|
Smart string - HF model ID, URL, or provider name. |
required |
**extra_kwargs
|
Additional kwargs merged into result. |
{}
|
Returns:
| Type | Description |
|---|---|
tuple[str, dict[str, Any]]
|
(provider_name, kwargs_dict) tuple. |
Raises:
| Type | Description |
|---|---|
ValueError
|
The string carries a |
Examples:: resolve_policy("lerobot/act_aloha_sim") # → ("lerobot_local", {"pretrained_name_or_path": "lerobot/act_aloha_sim"})
resolve_policy("ws://gpu-box:8765")
# → ("remote", {"url": "ws://gpu-box:8765"})
resolve_policy("mock")
# → ("mock", {})
build_policy_kwargs ¶
build_policy_kwargs(provider: str, policy_port: int | None = None, policy_host: str | None = None, model_path: str | None = None, server_address: str | None = None, policy_type: str | None = None, data_config: Any = None, **extra) -> dict[str, Any]
Build provider-specific kwargs from generic parameters.
Maps generic parameter names (policy_port, model_path, ...) to the provider-specific keys declared in policies.json.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
provider
|
str
|
Policy provider name. |
required |
policy_port
|
int | None
|
Port number (cosmos3, moveit2, remote). |
None
|
policy_host
|
str | None
|
Hostname. |
None
|
model_path
|
str | None
|
Local model path or HF ID. |
None
|
server_address
|
str | None
|
Full server address host:port (grpc:// URLs, remote providers). |
None
|
policy_type
|
str | None
|
Sub-type (pi0, act, smolvla, ...). |
None
|
**extra
|
Any additional provider-specific kwargs. A key declared in
the provider's |
{}
|
Returns:
| Type | Description |
|---|---|
dict[str, Any]
|
Dict of kwargs ready for create_policy(provider, **kwargs). |
dict[str, Any]
|
Where two sources name the same key, the more explicit one wins: |
dict[str, Any]
|
a provider-specific value in |
dict[str, Any]
|
that maps onto it ( |
dict[str, Any]
|
provider's registry default. A default only ever fills a key the |
dict[str, Any]
|
caller left unset. |