Skip to content

feat: add physicalai-lerobot-plugin - #265

Open
MarkRedeman wants to merge 3 commits into
mark/feat-add-first-party-robot-pluginsfrom
mark/feat-add-lerobot-plugin
Open

feat: add physicalai-lerobot-plugin#265
MarkRedeman wants to merge 3 commits into
mark/feat-add-first-party-robot-pluginsfrom
mark/feat-add-lerobot-plugin

Conversation

@MarkRedeman

@MarkRedeman MarkRedeman commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

This PR moves the lerobot plugin from my repo (https://github.com/MarkRedeman/physicalai-plugins/tree/main/packages/physicalai-lerobot-plugin) to the physicalai repo.

One security aspect to note:

  • Before b78ad65, dynamic discovery/import paths were permissive and could import any discoverable module.
  • With said commit we restrict the namespaces we look at based on LeRobot naming conventions: lerobot_robot_*, lerobot_teleoperator_*, other imports are rejected

Opens:

  • A malicious user can easily go around this security restriction by publishing under a lerobot_robot_ namespace
  • Running uv sync now also pulls in cuda dependencies due to lerobot's dependency on torch, torchvision, is there a good way to prevent this?

@MarkRedeman
MarkRedeman added this pull request to stack #264 September 11, 2026 09:38
Copilot AI lite review requested due to automatic review settings September 11, 2026 09:38
@MarkRedeman
MarkRedeman requested review from a team as code owners September 11, 2026 09:38
@MarkRedeman MarkRedeman changed the title feat: add lerobot plugin feat: add physicalai-lerobot-plugin Sep 11, 2026
@github-actions

Copy link
Copy Markdown

⚠️ Deprecation Warning: The deny-licenses option is deprecated for possible removal in the next major release. For more information, see issue 997.

Dependency Review

The following issues were found:
  • ❌ 2 vulnerable package(s)
  • ✅ 0 package(s) with incompatible licenses
  • ✅ 0 package(s) with invalid SPDX license definitions
  • ⚠️ 2 package(s) with unknown licenses.
See the Details below.

Vulnerabilities

uv.lock

NameVersionVulnerabilitySeverity
setuptools81.0.0setuptools: MANIFEST.in exclusion bypass in sdist via Unicode normalization collision (NFC/NFD) on macOS APFS/HFS+moderate
torch2.11.0PyTorch is vulnerable to memory corruption through its torch.jit.script functionlow

License Issues

packages/physicalai-lerobot-plugin/pyproject.toml

PackageVersionLicenseIssue Type
physicalai>= 0.1.1NullUnknown License
physicalai-studio-plugin>= 0.1.0NullUnknown License
Denied Licenses: GPL-1.0-only, GPL-1.0-or-later, GPL-2.0-only, GPL-2.0-or-later, GPL-2.0, GPL-3.0-only, GPL-3.0-or-later, GPL-3.0, AGPL-1.0-only, AGPL-1.0-or-later, AGPL-1.0, AGPL-3.0-only, AGPL-3.0-or-later, AGPL-3.0
Excluded from license check: pkg:pypi/typing-extensions, pkg:pypi/trossen-arm, pkg:githubactions/trufflesecurity/trufflehog

OpenSSF Scorecard

Scorecard details
PackageVersionScoreDetails
pip/lerobot >= 0.6.0 UnknownUnknown
pip/loguru >= 0.7 UnknownUnknown
pip/numpy >= 1.24 UnknownUnknown
pip/physicalai >= 0.1.1 UnknownUnknown
pip/physicalai-studio-plugin >= 0.1.0 UnknownUnknown
pip/pydantic >= 2.0 UnknownUnknown
pip/pyserial >= 3.5 UnknownUnknown
pip/setuptools 81.0.0 UnknownUnknown
pip/torch 2.11.0 UnknownUnknown
pip/cloudpickle 3.1.2 🟢 4.4
Details
CheckScoreReason
Code-Review🟢 4Found 13/29 approved changesets -- score normalized to 4
Maintained⚠️ 01 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 0
Binary-Artifacts🟢 10no binaries found in the repo
Dangerous-Workflow🟢 10no dangerous workflow patterns detected
Token-Permissions⚠️ 0detected GitHub workflow tokens with excessive permissions
Pinned-Dependencies⚠️ 0dependency not pinned by hash detected -- score normalized to 0
CII-Best-Practices⚠️ 0no effort to earn an OpenSSF best practices badge detected
Security-Policy⚠️ 0security policy file not detected
Fuzzing⚠️ 0project is not fuzzed
License🟢 9license file detected
Signed-Releases🟢 82 out of the last 2 releases have a total of 2 signed artifacts.
Branch-Protection⚠️ -1internal error: error during branchesHandler.setup: internal error: some github tokens can't read classic branch protection rules: https://github.com/ossf/scorecard-action/blob/main/docs/authentication/fine-grained-auth-token.md
Packaging🟢 10packaging workflow detected
SAST⚠️ 0SAST tool is not run on all commits -- score normalized to 0
pip/cmake 4.1.3 UnknownUnknown
pip/cuda-bindings 13.4.1 UnknownUnknown
pip/cuda-pathfinder 1.8.1 UnknownUnknown
pip/cuda-toolkit 13.0.2 UnknownUnknown
pip/draccus 0.11.6 UnknownUnknown
pip/einops 0.8.2 UnknownUnknown
pip/farama-notifications 0.0.6 UnknownUnknown
pip/gymnasium 1.3.0 UnknownUnknown
pip/lerobot 0.6.1 UnknownUnknown
pip/mpmath 1.3.0 UnknownUnknown
pip/networkx 3.6.1 UnknownUnknown
pip/nvidia-cublas 13.1.0.3 UnknownUnknown
pip/nvidia-cuda-cupti 13.0.85 UnknownUnknown
pip/nvidia-cuda-nvrtc 13.0.88 UnknownUnknown
pip/nvidia-cuda-runtime 13.0.96 UnknownUnknown
pip/nvidia-cudnn-cu13 9.19.0.56 UnknownUnknown
pip/nvidia-cufft 12.0.0.61 UnknownUnknown
pip/nvidia-cufile 1.15.1.6 UnknownUnknown
pip/nvidia-curand 10.4.0.35 UnknownUnknown
pip/nvidia-cusolver 12.0.4.66 UnknownUnknown
pip/nvidia-cusparse 12.6.3.3 UnknownUnknown
pip/nvidia-cusparselt-cu13 0.8.0 UnknownUnknown
pip/nvidia-nccl-cu13 2.28.9 UnknownUnknown
pip/nvidia-nvjitlink 13.0.88 UnknownUnknown
pip/nvidia-nvshmem-cu13 3.4.5 UnknownUnknown
pip/nvidia-nvtx 13.0.85 UnknownUnknown
pip/sympy 1.14.0 UnknownUnknown
pip/termcolor 3.3.0 UnknownUnknown
pip/toml 0.10.2 🟢 4.7
Details
CheckScoreReason
Code-Review🟢 7Found 20/26 approved changesets -- score normalized to 7
Dangerous-Workflow🟢 10no dangerous workflow patterns detected
Maintained⚠️ 00 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 0
Binary-Artifacts🟢 10no binaries found in the repo
Packaging⚠️ -1packaging workflow not detected
Token-Permissions⚠️ 0detected GitHub workflow tokens with excessive permissions
CII-Best-Practices⚠️ 0no effort to earn an OpenSSF best practices badge detected
Pinned-Dependencies⚠️ 1dependency not pinned by hash detected -- score normalized to 1
Security-Policy⚠️ 0security policy file not detected
Fuzzing🟢 10project is fuzzed
License🟢 10license file detected
Signed-Releases⚠️ -1no releases found
Branch-Protection⚠️ -1internal error: error during branchesHandler.setup: internal error: some github tokens can't read classic branch protection rules: https://github.com/ossf/scorecard-action/blob/main/docs/authentication/fine-grained-auth-token.md
SAST⚠️ 0SAST tool is not run on all commits -- score normalized to 0
pip/torchvision 0.26.0 🟢 5.6
Details
CheckScoreReason
Maintained🟢 1030 commit(s) and 7 issue activity found in the last 90 days -- score normalized to 10
Dangerous-Workflow🟢 10no dangerous workflow patterns detected
Code-Review🟢 9Found 27/30 approved changesets -- score normalized to 9
Packaging⚠️ -1packaging workflow not detected
Token-Permissions⚠️ 0detected GitHub workflow tokens with excessive permissions
Binary-Artifacts🟢 10no binaries found in the repo
CII-Best-Practices⚠️ 0no effort to earn an OpenSSF best practices badge detected
Pinned-Dependencies⚠️ 0dependency not pinned by hash detected -- score normalized to 0
Fuzzing⚠️ 0project is not fuzzed
Security-Policy⚠️ 0security policy file not detected
License🟢 10license file detected
Signed-Releases⚠️ -1no releases found
Branch-Protection🟢 4branch protection is not maximal on development and all release branches
SAST🟢 6SAST tool is not run on all commits -- score normalized to 6
pip/triton 3.6.0 UnknownUnknown
pip/typing-inspect 0.9.0 🟢 4.2
Details
CheckScoreReason
Code-Review🟢 4Found 12/30 approved changesets -- score normalized to 4
Dangerous-Workflow🟢 10no dangerous workflow patterns detected
Packaging⚠️ -1packaging workflow not detected
Maintained⚠️ 00 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 0
Binary-Artifacts🟢 10no binaries found in the repo
Token-Permissions🟢 10GitHub workflow tokens follow principle of least privilege
Pinned-Dependencies⚠️ 0dependency not pinned by hash detected -- score normalized to 0
CII-Best-Practices⚠️ 0no effort to earn an OpenSSF best practices badge detected
Security-Policy⚠️ 0security policy file not detected
Fuzzing⚠️ 0project is not fuzzed
License🟢 10license file detected
Signed-Releases⚠️ -1no releases found
Branch-Protection⚠️ 0branch protection not enabled on development/release branches
SAST⚠️ 0SAST tool is not run on all commits -- score normalized to 0

Scanned Files

  • packages/physicalai-lerobot-plugin/pyproject.toml
  • uv.lock


for _importer, module_name, is_package in pkgutil.walk_packages(package.__path__, prefix=f"{package_name}."):
if "config" in module_name and not is_package and _is_allowed_dynamic_import(module_name):
importlib.import_module(module_name) # nosemgrep: python.lang.security.audit.non-literal-import

for _importer, modname, is_pkg in pkgutil.walk_packages(lerobot.robots.__path__, prefix="lerobot.robots."):
if "config" in modname and not is_pkg and _is_allowed_dynamic_import(modname):
importlib.import_module(modname) # nosemgrep: python.lang.security.audit.non-literal-import
prefix="lerobot.teleoperators.",
):
if "config" in modname and not is_pkg and _is_allowed_dynamic_import(modname):
importlib.import_module(modname) # nosemgrep: python.lang.security.audit.non-literal-import

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

Unresolved critical runtime and discovery defects, along with packaging and dependency problems, block approval.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Adds physicalai-lerobot-plugin to Physical AI with LeRobot adapters, Studio catalog discovery, runtime examples, URDF assets, tests, and packaging metadata.

Changes:

  • Adds robot and teleoperator adapters with lifecycle and configuration handling.
  • Adds dynamic Studio registration, discovery, probing, and bundled URDF resources.
  • Adds documentation, tests, release metadata, and locked LeRobot/Torch/CUDA dependencies.
File summaries
File Summary and finalized review notes
uv.lock Locks the plugin and LeRobot dependency graph, including transitive Torch/CUDA packages.
packages/physicalai-lerobot-plugin/urdf/lerobot/urdf/lerobot.urdf Adds the bundled LeRobot URDF.
packages/physicalai-lerobot-plugin/urdf/lerobot/meshes/.gitkeep Preserves the mesh resource directory.
packages/physicalai-lerobot-plugin/tests/test_studio_catalog.py Tests catalog discovery, payloads, registration, builders, and assets.
packages/physicalai-lerobot-plugin/tests/test_lerobot_adapter.py Tests adapter behavior and lifecycle handling.
packages/physicalai-lerobot-plugin/tests/conftest.py Provides test fixtures and SDK stubs.
packages/physicalai-lerobot-plugin/src/physicalai_lerobot_plugin/studio_catalog.py Registers dynamic catalog definitions. Findings: line 606 critical (3 votes)—serial-only probing marks network robots offline; line 648 moderate (3 votes) and line 666 moderate (1 vote)—payload model construction can abort later registrations; lines 93 and 467 nit (1 vote each)—add rationale for dynamic-import suppressions; line 110 moderate (2 votes)—native discovery exceeds the allowlist; line 128 critical (1 vote)—namespace prefixes are not a sufficient trust boundary.
packages/physicalai-lerobot-plugin/src/physicalai_lerobot_plugin/lerobot_adapter.py Implements robot and teleoperator adapters. Findings: lines 136 moderate (3 votes)—device lock IDs are not canonical; lines 443 and 742 moderate (3 votes)—images are not wrapped as Frame objects; lines 364 and 484 critical (2 votes)—failed robot startup can leak partial state; lines 658 and 786 critical (2 votes)—failed teleoperator startup can leak partial state; line 159 nit (1 vote)—document the import suppression; line 162 moderate (2 votes)—runtime discovery remains insufficiently filtered.
packages/physicalai-lerobot-plugin/src/physicalai_lerobot_plugin/constants.py Defines plugin constants.
packages/physicalai-lerobot-plugin/src/physicalai_lerobot_plugin/_urdf.py Resolves bundled URDF resources. Finding: line 16 critical (3 votes)—the wheel path resolution climbs above site-packages and should probe the adjacent urdf directory first.
packages/physicalai-lerobot-plugin/src/physicalai_lerobot_plugin/__init__.py Initializes and exposes the plugin package.
packages/physicalai-lerobot-plugin/README.md Documents installation and usage. Findings: line 3 nit (1 vote)—repository attribution and links still point to the old project; line 167 nit (1 vote)—the referenced development guide does not exist.
packages/physicalai-lerobot-plugin/pyproject.toml Defines packaging, dependencies, entry points, tests, and assets. Findings: line 15 moderate (3 votes)—mandatory LeRobot dependencies pull a large CUDA/GPU stack; line 23 moderate (1 vote)—the tests extra lacks pytest-anyio; line 50 moderate (1 vote)—the package license is not included in the source distribution.
packages/physicalai-lerobot-plugin/examples/runtime/teleop.yaml Adds a teleoperation recipe. Finding: line 30 critical (3 votes)—the leader should use LeRobotTeleoperatorAdapter, not LeRobotAdapter.
packages/physicalai-lerobot-plugin/CHANGELOG.md Adds package release history through version 0.3.0.
.github/release-please-config.json Configures release automation for the package.
.github/.release-please-manifest.json Registers the package release version. Finding: line 6 moderate (1 vote)—the manifest starts at 0.2.3 while the changelog is at 0.3.0.
Review details

Suppressed comments (12)

.github/.release-please-manifest.json:6

  • The release manifest initializes this package at 0.2.3, while the checked-in changelog already has 0.3.0 as its latest release. Release Please will therefore compute from a stale version and can attempt to recreate or mis-sequence the existing release; set the manifest to the latest version represented by the migrated changelog.
  "packages/physicalai-lerobot-plugin": "0.2.3",

packages/physicalai-lerobot-plugin/README.md:167

  • This link targets docs/creating-a-studio-plugin.md, but that file does not exist in this repository, so the development guide in the new package README is broken. Link to an existing guide or add the referenced document before publishing the package.
See [`docs/creating-a-studio-plugin.md`](../../docs/creating-a-studio-plugin.md) for the full plugin development guide.

packages/physicalai-lerobot-plugin/README.md:3

  • The package was moved into this repository, but the README still identifies it as part of the original MarkRedeman/physicalai-plugins monorepo (and the nearby screenshot links still point there). This contradicts the migration described by the PR and leaves users directed to the old project; update the repository attribution and links.
Bridges [LeRobot](https://github.com/huggingface/lerobot) robot and teleoperator configs into [PhysicalAI](https://github.com/openvinotoolkit/physicalai), the Python library and runtime for robot control, transport, and CLI workflows. It registers with [Physical AI Studio](https://github.com/open-edge-platform/physical-ai-studio), the application that discovers catalog plugins and provides robot setup, teleoperation, and workflow experiences. Part of the [physicalai-plugins](https://github.com/MarkRedeman/physicalai-plugins) monorepo.

packages/physicalai-lerobot-plugin/pyproject.toml:50

  • There is no packages/physicalai-lerobot-plugin/LICENSE in this change, so the sdist include entry is relative to a nonexistent file and the published source distribution omits the repository license. Use the root-license force-include pattern used by the bimanual plugin instead.
    "LICENSE",

packages/physicalai-lerobot-plugin/pyproject.toml:23

  • The added suite uses @pytest.mark.anyio in tests/test_studio_catalog.py:358, but the package's tests extra installs only pytest. A standalone test install therefore lacks the async test plugin; include pytest-anyio in this extra.
tests = ["pytest"]

packages/physicalai-lerobot-plugin/src/physicalai_lerobot_plugin/lerobot_adapter.py:488

  • After a successful connect followed by disconnect(), _robot remains non-None, so this check permits send_action() to call the LeRobot device even though is_connected() is false. Require the wrapped robot to be connected here; otherwise a runtime can command a closed/disconnected device.
        robot = self._robot
        if robot is None:
            msg = "Robot is not connected. Call connect() first."
            raise ConnectionError(msg)
        robot.send_action(action_dict)

packages/physicalai-lerobot-plugin/src/physicalai_lerobot_plugin/lerobot_adapter.py:789

  • The teleoperator adapter has the same disconnected-device hole: disconnect() leaves _teleoperator set, and this check does not test is_connected() before forwarding feedback. A follower-role teleoperator can therefore receive actions after disconnect; require an active connection before calling send_feedback().
        teleop = self._teleoperator
        if teleop is None:
            msg = "Teleoperator is not connected. Call connect() first."
            raise ConnectionError(msg)

packages/physicalai-lerobot-plugin/src/physicalai_lerobot_plugin/lerobot_adapter.py:743

  • The teleoperator path repeats the same invalid cast: a NumPy image array is stored as a Frame without the required metadata. If a teleoperator emits an image-valued action, RobotObservation.images is structurally invalid; construct real Frame instances with timestamps and sequences instead.
            if isinstance(value, np.ndarray) and value.ndim >= _DIM_THRESHOLD_IMAGE:
                images[key] = cast("Frame", value)

packages/physicalai-lerobot-plugin/src/physicalai_lerobot_plugin/lerobot_adapter.py:159

  • This dynamic-import suppression has no adjacent explanation of why the imported name is safe. Document that package_name is exact-root allowlisted and module_name is restricted by _is_allowed_dynamic_import() before keeping the # nosemgrep (rule 1).
            importlib.import_module(module_name)  # nosemgrep: python.lang.security.audit.non-literal-import

packages/physicalai-lerobot-plugin/src/physicalai_lerobot_plugin/studio_catalog.py:666

  • The leader loop repeats the same pre-registration failure: _make_payload_model() runs outside the try block in register_physicalai_studio_plugin(). An incompatible third-party teleoperator schema will therefore stop all subsequent catalog entries instead of being skipped. Move this model construction under per-definition error handling.
        payload_cls = _make_payload_model(config_cls)

packages/physicalai-lerobot-plugin/src/physicalai_lerobot_plugin/studio_catalog.py:93

  • This # nosemgrep suppresses the dynamic-import finding without the required rationale. Add an adjacent comment explaining that modname is enumerated from the allowlisted lerobot.robots tree and checked by _is_allowed_dynamic_import() before importing it (rule 1).
            importlib.import_module(modname)  # nosemgrep: python.lang.security.audit.non-literal-import

packages/physicalai-lerobot-plugin/src/physicalai_lerobot_plugin/studio_catalog.py:467

  • This second # nosemgrep suppression also lacks a safety justification. Explain inline that the module name is discovered only under the allowlisted lerobot.teleoperators package and passes _is_allowed_dynamic_import() before retaining the suppression (rule 1).
            importlib.import_module(modname)  # nosemgrep: python.lang.security.audit.non-literal-import
  • Files reviewed: 15/17 changed files
  • Comments generated: 12
  • Review effort level: Lite

💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

class_path: physicalai.runtime.TeleopSource
init_args:
leader:
class_path: physicalai_lerobot_plugin.lerobot_adapter.LeRobotAdapter
Comment on lines +16 to +19
def get_urdf_path() -> Path:
traversal = ir.files("physicalai_lerobot_plugin")
with ir.as_file(traversal) as p:
return p.parent.parent.joinpath("urdf")
Comment on lines +364 to +368
try:
self._connect_robot()
except Exception as e:
msg = f"Failed to connect LeRobot {self._robot}: {e}"
raise ConnectionError(msg) from e
Comment on lines +658 to +662
try:
self._connect_teleoperator()
except Exception as e:
msg = f"Failed to connect LeRobot teleoperator {self._teleoperator}: {e}"
raise ConnectionError(msg) from e
Comment on lines +128 to +132
_ALLOWED_DYNAMIC_IMPORT_PREFIXES: tuple[str, ...] = (
"lerobot.",
"lerobot_robot_",
"lerobot_teleoperator_",
)
Comment on lines +136 to +139
def _device_ids(config_type: str, config_kwargs: dict[str, Any]) -> tuple[str, ...]:
"""Return stable serial-device identities without touching hardware."""
ports = sorted(set(_collect_device_ports(config_kwargs)))
return tuple(f"lerobot:{config_type}:{port}" for port in ports)
Comment on lines +162 to +165
def _register_third_party_plugins() -> None:
from lerobot.utils.import_utils import register_third_party_plugins # noqa: PLC0415

register_third_party_plugins()
Comment on lines +443 to +444
if isinstance(value, np.ndarray) and value.ndim >= _DIM_THRESHOLD_IMAGE:
images[key] = cast("Frame", value)
Comment on lines +110 to +112
from lerobot.utils.import_utils import register_third_party_plugins

register_third_party_plugins()
continue

display_name = f"LeRobot {type_str}"
payload_cls = _make_payload_model(config_cls)

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

The adapter has unsupported action layouts, invalid image handling, packaging and example failures, and unresolved dependency and plugin-import risks.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Suppressed comments (11)

packages/physicalai-lerobot-plugin/src/physicalai_lerobot_plugin/lerobot_adapter.py:610

  • This produces an empty joint/action layout for registered teleoperators whose actions do not use .pos keys. For example, LeRobot 0.6.1 registers keyboard/gamepad teleoperators with delta_x/delta_y/delta_z or velocity keys; get_observation() then returns a zero-length vector, making these catalog entries unusable. Use the teleoperator's action_features as the authoritative layout or skip incompatible types.
        pos_keys = sorted(k for k in action if k.endswith(_POSITION_KEY_SUFFIX))
        self._joint_order = [k[: -len(_POSITION_KEY_SUFFIX)] for k in pos_keys]
        self._num_joints = len(self._joint_order)
        self._action_position_keys = list(pos_keys)

packages/physicalai-lerobot-plugin/src/physicalai_lerobot_plugin/lerobot_adapter.py:444

  • Casting the camera array does not create a Frame; images still contains a raw np.ndarray. Runtime policy input later reads frame.data, and robot verification also requires timestamp and sequence, so any LeRobot config with a camera fails when embedded images are consumed. Wrap each image in a real Frame and maintain a per-source sequence counter.
            if isinstance(value, np.ndarray) and value.ndim >= _DIM_THRESHOLD_IMAGE:
                images[key] = cast("Frame", value)

packages/physicalai-lerobot-plugin/src/physicalai_lerobot_plugin/lerobot_adapter.py:139

  • Including config_type in the lock identity means two adapters configured for the same serial device under different LeRobot aliases receive different device_ids, so physicalai.robot.transport can open the same hardware concurrently. Derive serial identities with device_id_from_serial_port() and classify non-serial endpoints separately so identity depends on the physical endpoint, not the selected config type.
def _device_ids(config_type: str, config_kwargs: dict[str, Any]) -> tuple[str, ...]:
    """Return stable serial-device identities without touching hardware."""
    ports = sorted(set(_collect_device_ports(config_kwargs)))
    return tuple(f"lerobot:{config_type}:{port}" for port in ports)

packages/physicalai-lerobot-plugin/src/physicalai_lerobot_plugin/lerobot_adapter.py:368

  • If the initial observation fails after robot.connect() succeeds, this rethrows while leaving the hardware connected. A retry then returns early from connect() because is_connected() is true, but joint discovery is still incomplete, leaving the adapter unusable and the port open. Disconnect before rethrowing so failed initialization is atomic.
        try:
            self._connect_robot()
        except Exception as e:
            msg = f"Failed to connect LeRobot {self._robot}: {e}"
            raise ConnectionError(msg) from e

packages/physicalai-lerobot-plugin/src/physicalai_lerobot_plugin/lerobot_adapter.py:488

  • This guard checks only whether an object was constructed, not whether it remains connected. After disconnect(), _robot is retained and joint order is retained, so this path still invokes send_action() on a disconnected device instead of raising the documented ConnectionError. Check robot.is_connected here as well.
        robot = self._robot
        if robot is None:
            msg = "Robot is not connected. Call connect() first."
            raise ConnectionError(msg)
        robot.send_action(action_dict)

packages/physicalai-lerobot-plugin/src/physicalai_lerobot_plugin/lerobot_adapter.py:662

  • As with the robot adapter, a failure while reading the initial action can occur after the teleoperator connects. The exception leaves it connected, and subsequent connect() calls no-op without discovering the action layout. Disconnect before wrapping and rethrowing the initialization error.
        try:
            self._connect_teleoperator()
        except Exception as e:
            msg = f"Failed to connect LeRobot teleoperator {self._teleoperator}: {e}"
            raise ConnectionError(msg) from e

packages/physicalai-lerobot-plugin/src/physicalai_lerobot_plugin/lerobot_adapter.py:790

  • This checks only whether a teleoperator object exists. Because disconnect() retains _teleoperator and the discovered keys, follower-role callers can still reach send_feedback() after disconnection. Include not teleop.is_connected in the guard so commands are never forwarded to a disconnected device.
        teleop = self._teleoperator
        if teleop is None:
            msg = "Teleoperator is not connected. Call connect() first."
            raise ConnectionError(msg)
        teleop.send_feedback(feedback_dict)

packages/physicalai-lerobot-plugin/examples/runtime/teleop.yaml:30

  • so101_leader is a TeleoperatorConfig, but this class resolves names only through RobotConfig; running the documented example therefore fails with a missing config choice before connecting. Instantiate LeRobotTeleoperatorAdapter for the leader.
        class_path: physicalai_lerobot_plugin.lerobot_adapter.LeRobotAdapter

packages/physicalai-lerobot-plugin/src/physicalai_lerobot_plugin/_urdf.py:19

  • The wheel force-includes urdf at the site-packages root, one level above the Python package, but this always returns two levels above. It works in the editable src/ layout tested here and returns a nonexistent path after wheel installation. Probe both p.parent / "urdf" and the editable-layout location, as the other bundled plugins do.
def get_urdf_path() -> Path:
    traversal = ir.files("physicalai_lerobot_plugin")
    with ir.as_file(traversal) as p:
        return p.parent.parent.joinpath("urdf")

packages/physicalai-lerobot-plugin/pyproject.toml:15

  • Because this package is matched by the root workspace wildcard and workspace members are synced by default, making lerobot mandatory pulls Torch and the Linux CUDA stack into the normal repository uv sync (the lock now contains several gigabytes of NVIDIA wheels). Keep the plugin out of the workspace's default members or otherwise make LeRobot installation opt-in, while retaining it as a required dependency when this plugin itself is installed.
    "lerobot>=0.6.0",

packages/physicalai-lerobot-plugin/src/physicalai_lerobot_plugin/studio_catalog.py:113

  • This automatically imports every installed package accepted by LeRobot's name-prefix discovery. The local prefix predicate does not establish publisher trust, so an installed typosquatted lerobot_robot_* or lerobot_teleoperator_* distribution executes code during Studio startup. Require explicit administrator opt-in/allowlisting for third-party modules, or clearly make trusted installation of extensions the security boundary rather than treating the prefixes as a security restriction.
    from lerobot.utils.import_utils import register_third_party_plugins

    register_third_party_plugins()
    _LEROBOT_THIRD_PARTY_PLUGINS_IMPORTED = True
  • Files reviewed: 15/17 changed files
  • Comments generated: 3
  • Review effort level: Balanced

Comment on lines +304 to +310
pos_keys = sorted(k for k in obs if k.endswith(_POSITION_KEY_SUFFIX))
if not pos_keys:
pos_keys = sorted(k for k in obs if "position" in k.lower() or k.endswith("pos"))
self._joint_order = [_strip_position_suffix(k) for k in pos_keys]
self._num_joints = len(self._joint_order)
self._obs_position_keys = list(pos_keys)
self._act_position_keys = list(pos_keys)
"packages/physicalai-studio-plugin": "0.1.0",
"packages/physicalai-bimanual-so101-plugin": "0.2.3",
"packages/physicalai-rebot-b601-plugin": "0.4.3",
"packages/physicalai-lerobot-plugin": "0.2.3",
@@ -0,0 +1,167 @@
# PhysicalAI LeRobot Plugin

Bridges [LeRobot](https://github.com/huggingface/lerobot) robot and teleoperator configs into [PhysicalAI](https://github.com/openvinotoolkit/physicalai), the Python library and runtime for robot control, transport, and CLI workflows. It registers with [Physical AI Studio](https://github.com/open-edge-platform/physical-ai-studio), the application that discovers catalog plugins and provides robot setup, teleoperation, and workflow experiences. Part of the [physicalai-plugins](https://github.com/MarkRedeman/physicalai-plugins) monorepo.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants