Skip to content

feat: simulation foundation — models, ABC, factory, model registry, assets#84

Merged
cagataycali merged 43 commits into
strands-labs:mainfrom
cagataycali:feat/simulation-foundation
Apr 22, 2026
Merged

feat: simulation foundation — models, ABC, factory, model registry, assets#84
cagataycali merged 43 commits into
strands-labs:mainfrom
cagataycali:feat/simulation-foundation

Conversation

@cagataycali
Copy link
Copy Markdown
Member

@cagataycali cagataycali commented Apr 1, 2026

TL;DR

Simulation foundation layer — dataclasses, SimEngine ABC, pluggable backend factory, URDF/MJCF model registry, asset manager with auto-download, user robot registration, and @tool for asset downloads. Expands robot registry from 38 → 68 robots. Pure Python, no MuJoCo dependency.

Rebased on main (post-#83). All 50 review threads resolved. All CI green.

What changed

Simulation abstractions (strands_robots/simulation/)

File What
models.py Dataclasses: SimWorld, SimRobot, SimObject, SimCamera, TrajectoryStep, SimStatus. Backend-agnostic — engine state stored in generic _backend_state: dict (no MuJoCo-specific fields).
base.py SimEngine ABC — 12 required abstract methods + 4 optional (raise NotImplementedError). Context manager protocol. __del__ logs cleanup errors instead of silencing.
factory.py create_simulation() + register_backend() with duplicate protection (ValueError on conflict), built-in alias protection, force=True for intentional overwrites.
model_registry.py URDF/MJCF resolution: user-registered → STRANDS_ASSETS_DIR~/.strands_robots/assets/ → CWD → robot_descriptions fallback. Logs asset manager availability.
__init__.py Thin re-exports + __getattr__ lazy loading. No MuJoCo references.

Assets (strands_robots/assets/)

File What
__init__.py Thin exports only (consistent with repo convention).
manager.py Asset path resolution with _safe_join() path-traversal protection, _resolve_candidates() helper. Search paths documented. No bundled assets — everything from robot_descriptions.
download.py All download logic lives here (421 lines): robot_descriptions → git clone fallback. _shallow_clone() validates URLs via _ALLOWED_CLONE_URL_RE (HTTPS github.com only).

Tools (strands_robots/tools/)

File What
download_assets.py Thin @tool wrapper (~78 lines) — delegates to assets.download. No duplicated logic.

Registry (strands_robots/registry/)

File What
user_registry.py register_robot() / unregister_robot() — persisted to ~/.strands_robots/user_robots.json. Security warning on docstring re: MJCF code execution risk.
loader.py Merges user-local registry on top of package robots.json. Public invalidate_cache() API (no private imports).
robots.json 38 → 68 robots (aerial, expressive, mobile_manip categories added).
__init__.py Re-exports register_robot, unregister_robot, list_user_robots, invalidate_cache.

Other

File What
utils.py get_base_dir(), get_assets_dir(), resolve_asset_path() — single source of truth.
README.md Environment variables table + cache directory docs.
pyproject.toml [sim] extra with robot_descriptions dependency.

Design decisions

SimEngine ABC

from strands_robots.simulation.base import SimEngine
from strands_robots.simulation import create_simulation, register_backend

# 12 required methods — every physics engine must implement:
# create_world, destroy, reset, step, get_state, add_robot, remove_robot,
# add_object, remove_object, get_observation, send_action, render

# 4 optional methods (raise NotImplementedError):
# load_scene, run_policy, randomize, get_contacts

# get_observation/send_action are facade methods that bridge
# Sim ↔ Policy domains — the agent tool sees a single "simulation"
# interface without needing to know about Robot vs Sim distinction.

# Pluggable backends with alias support:
register_backend("bullet", lambda: BulletSim, aliases=["pb"])
sim = create_simulation("bullet")

Asset resolution order

1. STRANDS_ASSETS_DIR (env override)
2. ~/.strands_robots/assets/ (user cache)
3. CWD/assets/ (project-local)
4. robot_descriptions package (auto-download fallback)

Customer assets always win over defaults. Single env var: STRANDS_ASSETS_DIR.

Security

  • Path traversal: _safe_join() in both manager.py and download.py
  • URL validation: _shallow_clone() rejects non-HTTPS/non-github.com URLs
  • register_robot(): Library-only function, not exposed as @tool. Docstring warns about MJCF code execution risk if ever surfaced to agents.

Testing

  • 232 tests pass, 6 skipped, 0 failures (0.98s)
    • 14 simulation foundation tests (ABC contracts, factory round-trip, model registry, context manager)
    • 28 user registry tests (register/unregister, persistence, validation, path traversal)
  • ruff check: All passed
  • ruff format: 55 files clean
  • mypy: 0 issues in 36 source files

Review history

Reviewer Status Threads
@yinsong1986 ✅ APPROVED 3/3 resolved
@awsarron CHANGES_REQUESTED → all addressed 30/30 resolved
@max-rattray-aws COMMENTED → all addressed 3/3 resolved

Key changes since CHANGES_REQUESTED:

  • Rebased onto main (post-chore: modernize build system — uv lockfile, Python >=3.12 #83, Python ≥3.12 + uv)
  • Renamed SimulationBackendSimEngine
  • Moved download logic from tools/assets/download.py (tools as thin wrappers)
  • Removed MuJoCo-specific fields from SimWorld → generic _backend_state
  • Added path-traversal protection + URL validation
  • Culled 10 superfluous tests
  • Security warning on register_robot() docstring

Unblocks #85 (MuJoCo backend) and #86 (Robot factory).

Comment thread .github/workflows/test-lint.yml Outdated
Comment thread strands_robots/simulation/models.py Outdated
Comment thread strands_robots/simulation/base.py
Comment thread strands_robots/simulation/__init__.py Outdated
@cagataycali cagataycali force-pushed the feat/simulation-foundation branch from e5d70ca to 6bb80bd Compare April 1, 2026 20:03
Copy link
Copy Markdown
Contributor

@yinsong1986 yinsong1986 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All review comments addressed. LGTM.

Comment thread .github/workflows/test-lint.yml Outdated
Comment thread .github/workflows/test-lint.yml
Comment thread strands_robots/assets/__init__.py
Comment thread strands_robots/assets/__init__.py Outdated
Comment thread strands_robots/assets/__init__.py Outdated
Comment thread strands_robots/simulation/models.py
Comment thread strands_robots/simulation/model_registry.py Outdated
Comment thread strands_robots/simulation/model_registry.py
Comment thread strands_robots/simulation/model_registry.py Outdated
Comment thread strands_robots/simulation/model_registry.py Outdated
Comment thread pyproject.toml
Comment thread tests/test_simulation_foundation.py Outdated
@cagataycali cagataycali force-pushed the feat/simulation-foundation branch from 2594b1e to 2217260 Compare April 3, 2026 20:54
@strands-labs strands-labs deleted a comment from shipitfast Apr 3, 2026
@strands-labs strands-labs deleted a comment from shipitfast Apr 3, 2026
@strands-labs strands-labs deleted a comment from shipitfast Apr 3, 2026
@strands-labs strands-labs deleted a comment from shipitfast Apr 3, 2026
@strands-labs strands-labs deleted a comment from shipitfast Apr 3, 2026
@strands-labs strands-labs deleted a comment from shipitfast Apr 3, 2026
@strands-labs strands-labs deleted a comment from shipitfast Apr 3, 2026
@cagataycali cagataycali moved this from In review to In progress in Strands Labs - Robots Apr 5, 2026
@cagataycali cagataycali moved this from In progress to In review in Strands Labs - Robots Apr 5, 2026
@cagataycali cagataycali requested a review from awsarron April 6, 2026 05:23
@cagataycali cagataycali added this to the v0.4 milestone Apr 6, 2026
@cagataycali
Copy link
Copy Markdown
Member Author

Fixed in ef75a5d — added the missing [sim] extra to pyproject.toml:

sim = [
    "robot_descriptions>=1.11.0,<2.0.0",
]
all = [
    "strands-robots[groot-service]",
    "strands-robots[lerobot]",
    "strands-robots[sim]",
]

The code in assets/manager.py and tools/download_assets.py references robot_descriptions for asset downloads, and the docstrings say pip install strands-robots[sim], but the extra was never declared.

Copy link
Copy Markdown
Member

@awsarron awsarron left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking good, few small final comments.

Need to rebase after #83 is merged.

For all comments in this PR, we should examine common themes and include corrections for them in AGENTS.md so that future agent runs benefit from their lessons.

Comment thread strands_robots/assets/download.py Outdated
Comment thread strands_robots/assets/manager.py Outdated
Comment thread strands_robots/assets/manager.py Outdated
Comment thread strands_robots/assets/manager.py Outdated
Comment thread strands_robots/simulation/model_registry.py Outdated
Comment thread strands_robots/simulation/model_registry.py Outdated
Comment thread strands_robots/tools/download_assets.py Outdated
Comment thread strands_robots/tools/download_assets.py Outdated
Comment thread strands_robots/tools/download_assets.py Outdated
Comment thread tests/test_simulation_foundation.py
Comment thread strands_robots/tools/download_assets.py Outdated
Comment thread strands_robots/registry/robots.json Outdated
Comment thread strands_robots/simulation/models.py Outdated
Verified against the code, only the findings that hold up on re-read:

simulation/factory.py
- _import_backend_class: raise a descriptive ImportError when a built-in
  backend module is declared but not installed, instead of letting the
  raw ModuleNotFoundError surface to users. The default "mujoco" path
  now points at an unimplemented module, so create_simulation() would
  previously crash with a cryptic trace — now the error names the
  backend, suggests the extra to install, and points at register_backend().

simulation/model_registry.py
- Drop the import-time _URDF_SEARCH_PATHS constant (snapshotted
  Path.cwd() at import, broke pytest/notebook chdir flows). Use
  utils.get_search_paths() at every lookup — same function already used
  by assets.manager, so we dedupe too.
- Demote the module-level 'Asset manager available' log from INFO at
  import to a lazy DEBUG on first resolution.

assets/download.py
- _copy_and_clean: filter ignored patterns at copytree() time instead of
  deleting from the destination afterwards. Previous behaviour would
  clobber a user's own README/LICENSE/images in their asset cache.

assets/manager.py
- Gate _user_asset_path resolution through safe_join() so a malicious or
  corrupted user_robots.json cannot sneak '..' into model_xml and
  escape the asset cache. Matches the trust boundary of the built-in
  registry path.

registry/user_registry.py
- register_robot() now fails closed when the asset directory does not
  exist (previously silently accepted invalid registrations).
- Warn on alias collisions at registration time (shadows-canonical,
  shadows-existing-alias) instead of letting resolution order silently
  decide the winner.

tests/test_registry_integrity.py
- test_aliases_unique_across_registry
- test_no_alias_shadows_canonical_name
- test_hardware_only_robots_declare_lerobot_type

tests/test_simulation_factory.py (new)
- test_default_backend_missing_raises_import_error_with_guidance
  (regression test for the cryptic-ModuleNotFoundError fix)
- test_register_backend_loader_must_be_callable
- test_register_backend_rejects_duplicate_without_force

.gitignore
- Ignore .ideation/ (local scratchpad for idea cycles).

All 338 unit tests pass. ruff + mypy clean.
awsarron
awsarron previously approved these changes Apr 22, 2026
Comment thread strands_robots/utils.py
Comment thread strands_robots/assets/manager.py Outdated
Comment thread strands_robots/simulation/models.py
Comment thread strands_robots/registry/robots.json
- assets/manager.py: remove 'import os as _os' alias — just import os at top
- simulation/models.py: clarify _model vs _data vs _backend_state roles in
  docstring so backend implementers know which to use (each has a distinct
  purpose: core model handle, core state handle, catch-all dict)
- utils.py: decouple get_base_dir() from STRANDS_ASSETS_DIR. Introduce
  STRANDS_BASE_DIR as the explicit override for the base directory so
  user_robots.json no longer lands in an unexpected parent of the assets
  path. STRANDS_ASSETS_DIR now *only* moves the assets subtree.
- registry/user_registry.py: update module docstring to reflect the new
  STRANDS_BASE_DIR semantics
- tests/test_user_registry.py: update fixture + integration tests to set
  STRANDS_BASE_DIR and STRANDS_ASSETS_DIR independently; add tests asserting
  that STRANDS_ASSETS_DIR does *not* move the registry / base dir
@cagataycali cagataycali force-pushed the feat/simulation-foundation branch from 3a5c14b to fc89b1f Compare April 22, 2026 16:54
@cagataycali cagataycali requested a review from awsarron April 22, 2026 17:00
@cagataycali cagataycali enabled auto-merge (squash) April 22, 2026 17:01
@cagataycali cagataycali disabled auto-merge April 22, 2026 17:01
@cagataycali cagataycali enabled auto-merge (squash) April 22, 2026 17:03
@cagataycali cagataycali disabled auto-merge April 22, 2026 17:06
@cagataycali cagataycali enabled auto-merge (squash) April 22, 2026 17:09
Comment thread strands_robots/assets/manager.py
Comment thread strands_robots/simulation/models.py
Comment thread strands_robots/assets/download.py
Comment thread strands_robots/utils.py
@cagataycali
Copy link
Copy Markdown
Member Author

Thanks for the thorough review round @awsarron 🙏 — I've captured the 5 remaining items as a follow-up issue so they don't get lost, and going to merge this one so it stops being a rebase target for #85 and #86.

Follow-up: #105 (added to the project board)

Items tracked:

  1. simulation/models.py — fold the _model / _data / _backend_state distinction into the class docstring more prominently
  2. registry/robots.json — add an integration test that walks the registry and asserts every entry resolves, so upstream layout drifts fail CI immediately
  3. assets/manager.py — add a lightweight is_robot_asset_present() so list_available_robots() / status queries don't trigger auto-download per entry
  4. assets/download.py — fix stale [mujoco][sim] docstring reference
  5. utils.py — remove the dead dedup check in get_search_paths()

All polish / minor correctness — none of them block the foundation landing. Will pick them up in a small follow-up PR before starting the MuJoCo backend rebase. 👍


🤖 AI agent response. Strands Agents. Feedback welcome!

@cagataycali cagataycali dismissed stale reviews from awsarron, max-rattray-aws, and awsarron April 22, 2026 19:06

Stale — all threads from this review have been resolved and addressed in subsequent commits. Superseded by @awsarron's approval on 2026-04-22.

@cagataycali cagataycali merged commit 6d80fcc into strands-labs:main Apr 22, 2026
2 checks passed
@github-project-automation github-project-automation Bot moved this from In review to Done in Strands Labs - Robots Apr 22, 2026
cagataycali pushed a commit to cagataycali/robots that referenced this pull request Apr 22, 2026
Post-strands-labs#84 merge: SimWorld no longer carries MuJoCo-specific private
fields (_xml, _robot_base_xml, _recording, _trajectory,
_dataset_recorder, _tmpdir, _push_to_hub). These are MuJoCo backend
implementation details and now live in world._backend_state, as the
SimWorld docstring requests (prefer _backend_state over new fields).

Migrated call sites:
- mjcf_builder.py: tmpdir
- policy_runner.py: recording, trajectory, dataset_recorder
- recording.py: recording, trajectory, dataset_recorder, push_to_hub
- scene_ops.py: robot_base_xml
- simulation.py: xml, robot_base_xml, recording, trajectory

Reads use dict[] where preceded by a guard that guarantees initialization
(e.g. start_recording() sets before policy_runner reads), and .get()
with sensible defaults where the key may be unset.

Tests: 392 passed, 2 skipped (5 pre-existing test_path_validation
failures are on main too — unrelated).
Lint: ruff + mypy clean on 75 source files.
cagataycali added a commit to cagataycali/robots that referenced this pull request Apr 23, 2026
… registration

Add Newton GPU simulation backend stub that implements SimEngine ABC.
No GPU dependencies at import time — warp/newton are lazy-loaded.

New files:
- simulation/newton/__init__.py — lazy loading, no warp import
- simulation/newton/config.py — NewtonConfig dataclass (7 solvers)
- simulation/newton/simulation.py — NewtonSimulation(SimEngine) skeleton

Modified files:
- simulation/factory.py — register newton + aliases (warp, wp)
- pyproject.toml — add [newton] extras (warp-lang, newton-sim)

All 12 required SimEngine methods stubbed with NotImplementedError.
Newton-specific extensions (replicate, diffsim, IK, cloth, etc.) stubbed.
Factory kwargs flow through to NewtonConfig.

Tests: 81 passed (config validation, factory round-trip, lazy import,
ABC conformance, all stub methods verified).

Part 1 of 7-PR Newton backend series (issue #314).
Depends on PR strands-labs#84 (SimEngine ABC).
cagataycali added a commit to cagataycali/robots that referenced this pull request Apr 30, 2026
…spec dispatcher

Bug: Simulation._dispatch_action filtered kwargs through a hardcoded
whitelist that omitted observation_mapping, action_mapping, data_config,
host, port, api_token, trust_remote_code, actions_per_step,
use_processor, processor_overrides, and any other policy-specific kwarg.
Agents could not wire a policy (GR00T, SmolVLA, lerobot_local) to a
simulated robot through the AgentTool interface — sim joint names and
canonical model keys never got reconciled, breaking sim↔real transfer.

Fix:
- simulation.py::_dispatch_action — replace the whitelist with a
  mapping-aware passthrough: for methods that declare **policy_kwargs,
  forward every input field that isn't already matched to a named
  parameter. Actions without **kwargs stay strict.
- tool_spec.json — advertise observation_mapping, action_mapping, host,
  port, api_token, trust_remote_code, actions_per_step, use_processor,
  processor_overrides, device so agents can discover and use them.
- tests/test_tool_spec_dispatch_policy_kwargs.py — 5 regression tests
  pinning the forwarding for run_policy / eval_policy / start_policy
  and verifying non-policy actions stay strict.

End-to-end validation (MacBook Pro M-series, MPS):
- create_world → add_robot(so100) → add_object(red_cube)
  → add_camera(camera1) → add_camera(camera2)
  → run_policy(policy_provider='lerobot_local',
                pretrained_name_or_path='lerobot/smolvla_base',
                device='mps',
                observation_mapping={'camera1': 'observation.images.camera1',
                                     'camera2': 'observation.images.camera2',
                                     'joint_position': 'observation.state'},
                action_mapping={'action': 'joint_position'})
- SmolVLA downloaded, loaded on MPS, produced actions, sim stepped.
  2 control steps / 25.4s wall → proves the full chain works.

Quality:
- ruff + mypy: clean (77 files)
- hatch run test: 5/5 new tests pass; only pre-existing
  test_path_validation failures remain (noted by author on strands-labs#84).
cagataycali pushed a commit to cagataycali/robots that referenced this pull request May 3, 2026
Post-strands-labs#84 merge: SimWorld no longer carries MuJoCo-specific private
fields (_xml, _robot_base_xml, _recording, _trajectory,
_dataset_recorder, _tmpdir, _push_to_hub). These are MuJoCo backend
implementation details and now live in world._backend_state, as the
SimWorld docstring requests (prefer _backend_state over new fields).

Migrated call sites:
- mjcf_builder.py: tmpdir
- policy_runner.py: recording, trajectory, dataset_recorder
- recording.py: recording, trajectory, dataset_recorder, push_to_hub
- scene_ops.py: robot_base_xml
- simulation.py: xml, robot_base_xml, recording, trajectory

Reads use dict[] where preceded by a guard that guarantees initialization
(e.g. start_recording() sets before policy_runner reads), and .get()
with sensible defaults where the key may be unset.

Tests: 392 passed, 2 skipped (5 pre-existing test_path_validation
failures are on main too — unrelated).
Lint: ruff + mypy clean on 75 source files.
cagataycali added a commit to cagataycali/robots that referenced this pull request May 3, 2026
…spec dispatcher

Bug: Simulation._dispatch_action filtered kwargs through a hardcoded
whitelist that omitted observation_mapping, action_mapping, data_config,
host, port, api_token, trust_remote_code, actions_per_step,
use_processor, processor_overrides, and any other policy-specific kwarg.
Agents could not wire a policy (GR00T, SmolVLA, lerobot_local) to a
simulated robot through the AgentTool interface — sim joint names and
canonical model keys never got reconciled, breaking sim↔real transfer.

Fix:
- simulation.py::_dispatch_action — replace the whitelist with a
  mapping-aware passthrough: for methods that declare **policy_kwargs,
  forward every input field that isn't already matched to a named
  parameter. Actions without **kwargs stay strict.
- tool_spec.json — advertise observation_mapping, action_mapping, host,
  port, api_token, trust_remote_code, actions_per_step, use_processor,
  processor_overrides, device so agents can discover and use them.
- tests/test_tool_spec_dispatch_policy_kwargs.py — 5 regression tests
  pinning the forwarding for run_policy / eval_policy / start_policy
  and verifying non-policy actions stay strict.

End-to-end validation (MacBook Pro M-series, MPS):
- create_world → add_robot(so100) → add_object(red_cube)
  → add_camera(camera1) → add_camera(camera2)
  → run_policy(policy_provider='lerobot_local',
                pretrained_name_or_path='lerobot/smolvla_base',
                device='mps',
                observation_mapping={'camera1': 'observation.images.camera1',
                                     'camera2': 'observation.images.camera2',
                                     'joint_position': 'observation.state'},
                action_mapping={'action': 'joint_position'})
- SmolVLA downloaded, loaded on MPS, produced actions, sim stepped.
  2 control steps / 25.4s wall → proves the full chain works.

Quality:
- ruff + mypy: clean (77 files)
- hatch run test: 5/5 new tests pass; only pre-existing
  test_path_validation failures remain (noted by author on strands-labs#84).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Archived in project

Development

Successfully merging this pull request may close these issues.

6 participants