Skip to content

Commit 6d80fcc

Browse files
feat: simulation foundation — models, ABC, factory, model registry, assets (#84)
Simulation foundation layer for `strands-robots`. Pure Python, no MuJoCo dependency. Unblocks #85 (MuJoCo backend) and #86 (Robot factory). ## What's in **Simulation abstractions** (`strands_robots/simulation/`) - `models.py` — `SimWorld`, `SimRobot`, `SimObject`, `SimCamera`, `TrajectoryStep`, `SimStatus` dataclasses. Backend-agnostic: engine handles live in `_model`/`_data`, everything else in `_backend_state: dict`. - `base.py` — `SimEngine` ABC. 12 required abstract methods + 4 optional (raise `NotImplementedError`). Context-manager protocol. `__del__` logs cleanup errors at warning level. - `factory.py` — `create_simulation()` + `register_backend()` with duplicate/alias shadow protection (raises `ValueError`; `force=True` for intentional overrides). Descriptive `ImportError` when a built-in backend module isn't installed. - `model_registry.py` — URDF/MJCF resolution: user-registered → `STRANDS_ASSETS_DIR` → `~/.strands_robots/assets/` → CWD → `robot_descriptions` fallback. Resolves search paths at call time (no import-time `Path.cwd()` snapshot). - `__init__.py` — thin re-exports with lazy `__getattr__`. **Assets** (`strands_robots/assets/`) - `__init__.py` — thin exports only (repo convention). - `manager.py` — path resolution with `safe_join()` traversal protection. `_has_meshes()` uses `os.scandir` + early-exit, cached by `(path, mtime)`. Module-level guard for optional `[sim]` extra — no circular import with `download.py`. - `download.py` — all download logic (`robot_descriptions` → git clone fallback). `_shallow_clone()` enforces `_ALLOWED_CLONE_URL_RE` (HTTPS github.com only). `_copy_and_clean` filters ignored patterns at `copytree()` time so user files in the cache aren't clobbered. **Tools** (`strands_robots/tools/`) - `download_assets.py` — thin `@tool` wrapper (~78 lines) that delegates to `assets.download.download_robots()`. No duplicated logic. **Registry** (`strands_robots/registry/`) - `user_registry.py` — `register_robot()` / `unregister_robot()` persisted to `~/.strands_robots/user_robots.json`. Fails closed on missing asset dir. Warns on alias collisions at registration time. Docstring warns this must not be exposed as an agent `@tool` without `STRANDS_TRUST_REMOTE_CODE` gating (MJCF → MuJoCo plugin code-exec risk). - `loader.py` — merges user-local registry on top of package `robots.json`. Public `invalidate_cache()` API (no private imports from callers). - `robots.json` — 38 → 68 robots (adds aerial, expressive, mobile_manip categories). - `__init__.py` — re-exports `register_robot`, `unregister_robot`, `list_user_robots`, `invalidate_cache`. **Utils** (`strands_robots/utils.py`) - `get_base_dir()` reads `STRANDS_BASE_DIR` — decoupled from `STRANDS_ASSETS_DIR` so setting the assets path no longer drops `user_robots.json` into an unexpected parent. - `get_assets_dir()`, `resolve_asset_path()`, `safe_join()`, `get_search_paths()` — single source of truth; consumed by model_registry, user_registry, assets/manager. **Docs & packaging** - `README.md` — environment variables table (`STRANDS_BASE_DIR`, `STRANDS_ASSETS_DIR`, `GROOT_API_TOKEN`) + cache directory docs. - `AGENTS.md` — documents nested-asset-path convention (e.g. `xmls/asimov.xml` matching upstream layout) and the `auto_download` strategy invariant. - `pyproject.toml` — new `[sim]` extra (`robot_descriptions>=1.11.0,<2.0.0`); included in `[all]`. ## Design decisions **SimEngine ABC contract.** 12 required methods every physics engine must implement; 4 optional (`load_scene`, `run_policy`, `randomize`, `get_contacts`) raise `NotImplementedError` so unimplemented features are explicit during development. `get_observation`/`send_action` are deliberately facade methods bridging Sim ↔ Policy — the agent tool sees a single interface without needing to know the Robot vs Sim split. **Asset resolution order.** Customer assets always win over defaults: `STRANDS_ASSETS_DIR` → `~/.strands_robots/assets/` → `CWD/assets/` → `robot_descriptions` fallback. Single env var for the asset tree (`STRANDS_ASSETS_DIR`); separate `STRANDS_BASE_DIR` for the base dir that holds `user_robots.json`. **Backend registration.** `register_backend()` rejects duplicates by default and blocks shadowing of built-in aliases (`mj`, `mjc`, `mjx`) unless `force=True`. Alias conflicts caught at both the `name` and `aliases` parameters. **Security.** - `safe_join()` applied everywhere registry values flow into filesystem paths (manager + download + user registry). - `_shallow_clone()` URL regex rejects `ssh://`, `git://`, `file://`, non-github hosts. - `register_robot()` is library-only; not surfaced as `@tool`. Docstring spells out the MJCF-plugin exec risk. ## Testing - 338 unit tests pass, 6 skipped, 0 failures - `ruff check` + `ruff format --check`: clean (57 files) - `mypy`: 0 issues in 57 source files - New test files: - `tests/test_simulation_foundation.py` — ABC contracts, factory round-trip, context-manager cleanup - `tests/test_simulation_factory.py` — duplicate rejection, alias shadowing, missing-backend ImportError - `tests/test_user_registry.py` — register/unregister, persistence, validation, path traversal; asserts `STRANDS_ASSETS_DIR` does NOT move the base dir / registry - `tests/test_registry_integrity.py` — auto-download invariant, alias uniqueness, canonical-shadow protection, lerobot_type presence on hardware-only robots ## Review history | Reviewer | Status | Threads | |-------------------|---------------------------------------|-----------------| | @yinsong1986 | APPROVED | 3/3 resolved | | @awsarron | CHANGES_REQUESTED → all addressed | 50/50 addressed | | @max-rattray-aws | COMMENTED → all addressed | 3/3 resolved | Closes #84. --------- Co-authored-by: cagataycali <cagataycali@icloud.com> Co-authored-by: strands-agent <217235299+strands-agent@users.noreply.github.com>
1 parent 0e97061 commit 6d80fcc

24 files changed

Lines changed: 3969 additions & 270 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@ build
99
dist
1010
.strands_robots
1111
.coverage
12+
.ideation/

AGENTS.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,3 +84,19 @@ hatch run format # ruff check --fix, ruff format
8484
4. Open PR from your fork, address all review comments
8585
5. Track follow-up items as issues on the [project board](https://github.com/orgs/strands-labs/projects/2)
8686
6. Squash merge into `main`
87+
88+
89+
## Registry conventions (strands_robots/registry/robots.json)
90+
91+
- **Flat asset paths** (e.g. `"model_xml": "scene.xml"`) are the common case.
92+
- **Nested asset paths** (e.g. `"model_xml": "xmls/asimov.xml"`) are allowed when
93+
the upstream source repo uses a subdir layout. Example: `asimov_v0` maps to
94+
`asimovinc/asimov-v0` which has `sim-model/xmls/asimov.xml` +
95+
`sim-model/assets/`. The `_safe_join` helper in `strands_robots/utils.py`
96+
guards against traversal (`..`).
97+
- **Auto-download strategy** — every robot with an `asset` block must declare
98+
exactly one of:
99+
1. `asset.robot_descriptions_module` (preferred)
100+
2. `asset.source` with `type: "github"`
101+
3. `asset.auto_download: false` (explicit opt-out)
102+
Enforced by `tests/test_registry_integrity.py`.

README.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -486,6 +486,31 @@ while True:
486486
agent.tool.gr00t_inference(action="stop", port=8000)
487487
```
488488

489+
## Configuration
490+
491+
### Environment Variables
492+
493+
| Variable | Description | Default |
494+
|----------|-------------|---------|
495+
| `STRANDS_ASSETS_DIR` | Custom directory for robot model assets (MJCF, meshes) | `~/.strands_robots/assets/` |
496+
| `GROOT_API_TOKEN` | API token for GR00T inference service ||
497+
498+
### Cache Directory
499+
500+
Robot model assets (MJCF XML files and meshes) are cached in:
501+
502+
```
503+
~/.strands_robots/
504+
└── assets/ # Downloaded robot models (from robot_descriptions / MuJoCo Menagerie)
505+
├── trs_so_arm100/
506+
├── franka_emika_panda/
507+
└── ...
508+
```
509+
510+
To clear the cache: `rm -rf ~/.strands_robots/assets/`
511+
512+
To change the cache location: `export STRANDS_ASSETS_DIR=/path/to/custom/dir`
513+
489514
## Contributing
490515

491516
We welcome contributions! Please see:

pyproject.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,13 @@ groot-service = [
4848
lerobot = [
4949
"lerobot>=0.5.0,<0.6.0",
5050
]
51+
sim = [
52+
"robot_descriptions>=1.11.0,<2.0.0",
53+
]
5154
all = [
5255
"strands-robots[groot-service]",
5356
"strands-robots[lerobot]",
57+
"strands-robots[sim]",
5458
]
5559
dev = [
5660
"pytest>=6.0,<9.0.0",
@@ -124,7 +128,7 @@ ignore_missing_imports = false
124128

125129
# Third-party libs without type stubs
126130
[[tool.mypy.overrides]]
127-
module = ["lerobot.*", "gr00t.*", "draccus.*", "msgpack.*", "zmq.*", "huggingface_hub.*", "serial.*", "psutil.*", "torch.*", "torchvision.*", "transformers.*", "einops.*"]
131+
module = ["lerobot.*", "gr00t.*", "draccus.*", "msgpack.*", "zmq.*", "huggingface_hub.*", "serial.*", "psutil.*", "torch.*", "torchvision.*", "transformers.*", "einops.*", "robot_descriptions.*"]
128132
ignore_missing_imports = true
129133

130134
# @tool decorator injects runtime signatures mypy cannot check

strands_robots/assets/__init__.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
"""Robot Asset Manager for Strands Robots Simulation.
2+
3+
Assets are resolved from ``robot_descriptions`` package or downloaded from
4+
MuJoCo Menagerie GitHub, cached in ``~/.strands_robots/assets/``.
5+
Override with ``STRANDS_ASSETS_DIR`` env var.
6+
7+
Implementation lives in ``assets/manager.py`` — this file is thin exports only.
8+
"""
9+
10+
from strands_robots.assets.manager import (
11+
get_robot_info,
12+
list_available_robots,
13+
resolve_model_dir,
14+
resolve_model_path,
15+
)
16+
from strands_robots.registry import (
17+
format_robot_table,
18+
get_robot,
19+
list_aliases,
20+
list_robots,
21+
list_robots_by_category,
22+
)
23+
from strands_robots.registry import (
24+
resolve_name as resolve_robot_name,
25+
)
26+
from strands_robots.utils import get_assets_dir, get_search_paths
27+
28+
__all__ = [
29+
"resolve_model_path",
30+
"resolve_model_dir",
31+
"resolve_robot_name",
32+
"get_robot_info",
33+
"list_available_robots",
34+
"list_robots_by_category",
35+
"list_aliases",
36+
"format_robot_table",
37+
"get_assets_dir",
38+
"get_search_paths",
39+
"get_robot",
40+
"list_robots",
41+
]

0 commit comments

Comments
 (0)