Skip to content

feat: add isolated Python module runtime - #3478

Open
TomCC7 wants to merge 1 commit into
cc/rust-recorderfrom
cc/feat/external-python-native-module
Open

feat: add isolated Python module runtime#3478
TomCC7 wants to merge 1 commit into
cc/rust-recorderfrom
cc/feat/external-python-native-module

Conversation

@TomCC7

@TomCC7 TomCC7 commented Aug 15, 2026

Copy link
Copy Markdown
Member

Contribution path

Problem

dimOS needs to run Python modules with dependency sets that conflict with the host environment while preserving normal typed streams, RPCs, skills, module references, lifecycle management, and restart behavior.

Solution

  • Add IsolatedPythonModule, a host-side contract backed by a concrete subclass in a sibling Python project.
  • Keep RPC definitions on the contract and forward calls through a unique internal runtime endpoint.
  • Forward streams and injected module references to the isolated runtime.
  • Support locked uv projects and Pixi-provided uv without installing runtime dependencies into the host environment.
  • Add a runnable external module example and lifecycle, validation, RPC, restart, and shutdown coverage.

How to Test

uv run pytest dimos/core/test_isolated_python_module.py dimos/core/test_isolated_python_bootstrap.py dimos/core/test_isolated_python_end_to_end.py -q
uv run python examples/external_python_module/run.py

Validated on the #3465 base: 21 focused tests passed, targeted mypy passed, pre-commit passed, and the real subprocess example exited cleanly.

Checklist

  • I have read and approved the CLA.

@mintlify

mintlify Bot commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
dimensional 🟢 Ready View Preview Aug 15, 2026, 3:49 AM

💡 Tip: Enable Workflows to automatically generate PRs for you.

@codecov

codecov Bot commented Aug 15, 2026

Copy link
Copy Markdown

❌ 6 Tests Failed:

Tests completed Failed Passed Skipped
4698 6 4692 214
View the top 3 failed test(s) by shortest run time
dimos.codebase_checks.test_blueprint_kwargs::test_blueprint_atom_kwargs_match_module_config[teleop-quest-dual-openyam]
Stack Traces | 0.003s run time
blueprint_name = 'teleop-quest-dual-openyam'

    @pytest.mark.parametrize("blueprint_name", _blueprint_params())
    def test_blueprint_atom_kwargs_match_module_config(blueprint_name: str) -> None:
        """Fail when blueprint kwargs cannot be consumed by their target module."""
        blueprint = _get_blueprint_or_skip(blueprint_name)
    
        violations: list[str] = []
        for atom in blueprint.blueprints:
            unknown_kwargs = sorted(set(atom.kwargs) - _allowed_kwarg_names(atom.module))
            if unknown_kwargs:
                violations.append(
                    f"{atom.module.__module__}.{atom.module.__name__}: unknown kwargs {unknown_kwargs}"
                )
    
        if violations:
            listing = "\n".join(f"  - {violation}" for violation in violations)
>           raise AssertionError(
                f"Blueprint {blueprint_name!r} passes unknown module kwargs:\n{listing}\n\n"
                "Blueprint kwargs are forwarded into the module constructor. For modules "
                "with an explicit `config` annotation, use fields from that config model; "
                "for legacy modules with direct constructor parameters, use the declared "
                "`__init__` keyword names."
            )
E           AssertionError: Blueprint 'teleop-quest-dual-openyam' passes unknown module kwargs:
E             - dimos.teleop.quest.quest_extensions.ArmTeleopModule: unknown kwargs ['task_names']
E           
E           Blueprint kwargs are forwarded into the module constructor. For modules with an explicit `config` annotation, use fields from that config model; for legacy modules with direct constructor parameters, use the declared `__init__` keyword names.

atom       = <[RuntimeError("Failed to pull LFS file .../dimos/data/.lfs/dual_openyam_abc_box_v2.tar.gz after 3...v2.tar.gz', '--exclude=']' returned non-zero exit status 1.") raised in repr()] BlueprintAtom object at 0xff598fc40470>
blueprint  = <[RuntimeError("Failed to pull LFS file .../dimos/data/.lfs/dual_openyam_abc_box_v2.tar.gz after 3...box_v2.tar.gz', '--exclude=']' returned non-zero exit status 1.") raised in repr()] Blueprint object at 0xff598fb53860>
blueprint_name = 'teleop-quest-dual-openyam'
listing    = "  - dimos.teleop.quest.quest_extensions.ArmTeleopModule: unknown kwargs ['task_names']"
unknown_kwargs = []
violations = ["dimos.teleop.quest.quest_extensions.ArmTeleopModule: unknown kwargs ['task_names']"]

dimos/codebase_checks/test_blueprint_kwargs.py:103: AssertionError
dimos.codebase_checks.test_blueprint_kwargs::test_blueprint_atom_kwargs_match_module_config[teleop-quest-openyam]
Stack Traces | 0.003s run time
blueprint_name = 'teleop-quest-openyam'

    @pytest.mark.parametrize("blueprint_name", _blueprint_params())
    def test_blueprint_atom_kwargs_match_module_config(blueprint_name: str) -> None:
        """Fail when blueprint kwargs cannot be consumed by their target module."""
        blueprint = _get_blueprint_or_skip(blueprint_name)
    
        violations: list[str] = []
        for atom in blueprint.blueprints:
            unknown_kwargs = sorted(set(atom.kwargs) - _allowed_kwarg_names(atom.module))
            if unknown_kwargs:
                violations.append(
                    f"{atom.module.__module__}.{atom.module.__name__}: unknown kwargs {unknown_kwargs}"
                )
    
        if violations:
            listing = "\n".join(f"  - {violation}" for violation in violations)
>           raise AssertionError(
                f"Blueprint {blueprint_name!r} passes unknown module kwargs:\n{listing}\n\n"
                "Blueprint kwargs are forwarded into the module constructor. For modules "
                "with an explicit `config` annotation, use fields from that config model; "
                "for legacy modules with direct constructor parameters, use the declared "
                "`__init__` keyword names."
            )
E           AssertionError: Blueprint 'teleop-quest-openyam' passes unknown module kwargs:
E             - dimos.teleop.quest.quest_extensions.ArmTeleopModule: unknown kwargs ['task_names']
E           
E           Blueprint kwargs are forwarded into the module constructor. For modules with an explicit `config` annotation, use fields from that config model; for legacy modules with direct constructor parameters, use the declared `__init__` keyword names.

atom       = <[RuntimeError("Failed to pull LFS file .../dimos/data/.lfs/yam_description.tar.gz after 3 attempt...on.tar.gz', '--exclude=']' returned non-zero exit status 1.") raised in repr()] BlueprintAtom object at 0xff598fce20f0>
blueprint  = <[RuntimeError("Failed to pull LFS file .../dimos/data/.lfs/yam_description.tar.gz after 3 attempt...iption.tar.gz', '--exclude=']' returned non-zero exit status 1.") raised in repr()] Blueprint object at 0xff598fce14c0>
blueprint_name = 'teleop-quest-openyam'
listing    = "  - dimos.teleop.quest.quest_extensions.ArmTeleopModule: unknown kwargs ['task_names']"
unknown_kwargs = []
violations = ["dimos.teleop.quest.quest_extensions.ArmTeleopModule: unknown kwargs ['task_names']"]

dimos/codebase_checks/test_blueprint_kwargs.py:103: AssertionError
dimos.robot.manipulators.dual_openyam.test_teleop_ik::test_quest_solver_matches_a1z_target_tracking_speed
Stack Traces | 0.003s run time
self = PosixPath('.../data/dual_openyam_abc_box_v2/dual_openyam.urdf')

    def __hash__(self):
        try:
>           return self._hash

self       = PosixPath('.../data/dual_openyam_abc_box_v2/dual_openyam.urdf')

....../github/home/.local.../uv/python/cpython-3.12.12-linux-x86_64-gnu/lib/python3.12/pathlib.py:526: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = PosixPath('.../data/dual_openyam_abc_box_v2/dual_openyam.urdf')
name = '_hash'

    def __getattribute__(self, name: str) -> object:
        # During Path.__new__(), _lfs_filename hasn't been set yet.
        # Fall through to normal Path behavior until construction is complete.
        try:
            object.__getattribute__(self, "_lfs_filename")
        except AttributeError:
            return object.__getattribute__(self, name)
    
        # After construction, allow access to our internal attributes directly
        if name in ("_lfs_filename", "_lfs_resolved_cache", "_ensure_downloaded"):
            return object.__getattribute__(self, name)
    
        # For all other attributes, ensure download first then delegate to resolved path
        resolved = object.__getattribute__(self, "_ensure_downloaded")()
>       return getattr(resolved, name)
E       AttributeError: 'PosixPath' object has no attribute '_hash'

name       = '_hash'
resolved   = PosixPath('.../data/dual_openyam_abc_box_v2/dual_openyam.urdf')
self       = PosixPath('.../data/dual_openyam_abc_box_v2/dual_openyam.urdf')

dimos/utils/data.py:371: AttributeError

During handling of the above exception, another exception occurred:

    @pytest.mark.self_hosted
    def test_quest_solver_matches_a1z_target_tracking_speed() -> None:
>       solver = _solver()


.../manipulators/dual_openyam/test_teleop_ik.py:69: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
.../manipulators/dual_openyam/test_teleop_ik.py:47: in _solver
    return DualOpenYamPinkPoseTargetSolver(config)
        config     = PoseTargetIKTaskConfig(joint_names=('left_arm/joint1', 'left_arm/joint2', 'left_arm/joint3', 'left_arm/joint4', 'left_...ilter_cutoff_hz=30.0, max_command_tracking_error_deg=10.0, feedback_limit_tolerance=0.001, command_limit_margin=0.0001)
        task       = TaskConfig(name='teleop_dual_openyam', type='teleop_ik', joint_names=['left_arm/joint1', 'left_arm/joint2', 'left_arm/...nd_tracking_error_deg': 10.0, 'max_joint_velocity_rad_s': 2.0, 'joint_command_filter_cutoff_hz': 30.0}, stream_bind={})
.../control/tasks/pose_target_ik.py:277: in __init__
    self._validate_frame_targets(
        __class__  = <class 'dimos.control.tasks.pose_target_ik.PinkPoseTargetSolver'>
        config     = PoseTargetIKTaskConfig(joint_names=('left_arm/joint1', 'left_arm/joint2', 'left_arm/joint3', 'left_arm/joint4', 'left_...ilter_cutoff_hz=30.0, max_command_tracking_error_deg=10.0, feedback_limit_tolerance=0.001, command_limit_margin=0.0001)
        self       = <dimos.robot.manipulators.dual_openyam.teleop_ik.DualOpenYamPinkPoseTargetSolver object at 0x73993df6bd40>
.../control/tasks/pose_target_ik.py:491: in _validate_frame_targets
    context = self._get_control_context(robot_model, frames, joints)
        command_limit_margin = 0.0001
        controlled_joints = ('left_arm/joint1', 'left_arm/joint2', 'left_arm/joint3', 'left_arm/joint4', 'left_arm/joint5', 'left_arm/joint6', ...)
        frame_names = ('left_grasp_frame', 'right_grasp_frame')
        frames     = ('left_grasp_frame', 'right_grasp_frame')
        joints     = ('left_arm/joint1', 'left_arm/joint2', 'left_arm/joint3', 'left_arm/joint4', 'left_arm/joint5', 'left_arm/joint6', ...)
        robot_model = RobotModelConfig(rpc_transport=<class 'dimos.protocol.rpc.pubsubrpc.LCMRPC'>, default_rpc_timeout=120.0, rpc_timeouts=...extra_links=[], home_joints=[0.0, 1.047, 1.047, 0.0, 0.0, 0.0, 0.0, 1.047, 1.047, 0.0, 0.0, 0.0], pre_grasp_offset=0.1)
        self       = <dimos.robot.manipulators.dual_openyam.teleop_ik.DualOpenYamPinkPoseTargetSolver object at 0x73993df6bd40>
.../control/tasks/pose_target_ik.py:536: in _get_control_context
    if cache_key not in self._control_contexts:
        cache_key  = (RobotModel(_source_path=PosixPath('.../data/dual_openyam_abc_box_v2/dual_openyam.urdf'), _package_paths=...'left_arm/joint1', 'left_arm/joint2', 'left_arm/joint3', 'left_arm/joint4', 'left_arm/joint5', 'left_arm/joint6', ...))
        config     = RobotModelConfig(rpc_transport=<class 'dimos.protocol.rpc.pubsubrpc.LCMRPC'>, default_rpc_timeout=120.0, rpc_timeouts=...extra_links=[], home_joints=[0.0, 1.047, 1.047, 0.0, 0.0, 0.0, 0.0, 1.047, 1.047, 0.0, 0.0, 0.0], pre_grasp_offset=0.1)
        controlled_joints = ('left_arm/joint1', 'left_arm/joint2', 'left_arm/joint3', 'left_arm/joint4', 'left_arm/joint5', 'left_arm/joint6', ...)
        frame_names = ('left_grasp_frame', 'right_grasp_frame')
        frames     = ('left_grasp_frame', 'right_grasp_frame')
        self       = <dimos.robot.manipulators.dual_openyam.teleop_ik.DualOpenYamPinkPoseTargetSolver object at 0x73993df6bd40>
<string>:3: in __hash__
    ???
        self       = RobotModel(_source_path=PosixPath('.../data/dual_openyam_abc_box_v2/dual_openyam.urdf'), _package_paths=(...dimos/data/dual_openyam_abc_box_v2')),), _xacro_args=(), _fixed_frames=(), _fixed_joints=(), _joint_position_limits=())
....../github/home/.local.../uv/python/cpython-3.12.12-linux-x86_64-gnu/lib/python3.12/pathlib.py:529: in __hash__
    return self._hash
        self       = PosixPath('.../data/dual_openyam_abc_box_v2/dual_openyam.urdf')
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = PosixPath('.../data/dual_openyam_abc_box_v2/dual_openyam.urdf')
name = '_hash'

    def __getattribute__(self, name: str) -> object:
        # During Path.__new__(), _lfs_filename hasn't been set yet.
        # Fall through to normal Path behavior until construction is complete.
        try:
            object.__getattribute__(self, "_lfs_filename")
        except AttributeError:
            return object.__getattribute__(self, name)
    
        # After construction, allow access to our internal attributes directly
        if name in ("_lfs_filename", "_lfs_resolved_cache", "_ensure_downloaded"):
            return object.__getattribute__(self, name)
    
        # For all other attributes, ensure download first then delegate to resolved path
        resolved = object.__getattribute__(self, "_ensure_downloaded")()
>       return getattr(resolved, name)
E       AttributeError: 'PosixPath' object has no attribute '_hash'

name       = '_hash'
resolved   = PosixPath('.../data/dual_openyam_abc_box_v2/dual_openyam.urdf')
self       = PosixPath('.../data/dual_openyam_abc_box_v2/dual_openyam.urdf')

dimos/utils/data.py:371: AttributeError
dimos.robot.manipulators.dual_openyam.test_teleop_ik::test_solver_uses_nominal_posture_without_manipulability
Stack Traces | 0.003s run time
self = PosixPath('.../data/dual_openyam_abc_box_v2/dual_openyam.urdf')

    def __hash__(self):
        try:
>           return self._hash

self       = PosixPath('.../data/dual_openyam_abc_box_v2/dual_openyam.urdf')

....../github/home/.local.../uv/python/cpython-3.12.12-linux-x86_64-gnu/lib/python3.12/pathlib.py:526: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = PosixPath('.../data/dual_openyam_abc_box_v2/dual_openyam.urdf')
name = '_hash'

    def __getattribute__(self, name: str) -> object:
        # During Path.__new__(), _lfs_filename hasn't been set yet.
        # Fall through to normal Path behavior until construction is complete.
        try:
            object.__getattribute__(self, "_lfs_filename")
        except AttributeError:
            return object.__getattribute__(self, name)
    
        # After construction, allow access to our internal attributes directly
        if name in ("_lfs_filename", "_lfs_resolved_cache", "_ensure_downloaded"):
            return object.__getattribute__(self, name)
    
        # For all other attributes, ensure download first then delegate to resolved path
        resolved = object.__getattribute__(self, "_ensure_downloaded")()
>       return getattr(resolved, name)
E       AttributeError: 'PosixPath' object has no attribute '_hash'

name       = '_hash'
resolved   = PosixPath('.../data/dual_openyam_abc_box_v2/dual_openyam.urdf')
self       = PosixPath('.../data/dual_openyam_abc_box_v2/dual_openyam.urdf')

dimos/utils/data.py:371: AttributeError

During handling of the above exception, another exception occurred:

    @pytest.mark.self_hosted
    def test_solver_uses_nominal_posture_without_manipulability() -> None:
>       solver = _solver()


.../manipulators/dual_openyam/test_teleop_ik.py:52: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
.../manipulators/dual_openyam/test_teleop_ik.py:47: in _solver
    return DualOpenYamPinkPoseTargetSolver(config)
        config     = PoseTargetIKTaskConfig(joint_names=('left_arm/joint1', 'left_arm/joint2', 'left_arm/joint3', 'left_arm/joint4', 'left_...ilter_cutoff_hz=30.0, max_command_tracking_error_deg=10.0, feedback_limit_tolerance=0.001, command_limit_margin=0.0001)
        task       = TaskConfig(name='teleop_dual_openyam', type='teleop_ik', joint_names=['left_arm/joint1', 'left_arm/joint2', 'left_arm/...nd_tracking_error_deg': 10.0, 'max_joint_velocity_rad_s': 2.0, 'joint_command_filter_cutoff_hz': 30.0}, stream_bind={})
.../control/tasks/pose_target_ik.py:277: in __init__
    self._validate_frame_targets(
        __class__  = <class 'dimos.control.tasks.pose_target_ik.PinkPoseTargetSolver'>
        config     = PoseTargetIKTaskConfig(joint_names=('left_arm/joint1', 'left_arm/joint2', 'left_arm/joint3', 'left_arm/joint4', 'left_...ilter_cutoff_hz=30.0, max_command_tracking_error_deg=10.0, feedback_limit_tolerance=0.001, command_limit_margin=0.0001)
        self       = <dimos.robot.manipulators.dual_openyam.teleop_ik.DualOpenYamPinkPoseTargetSolver object at 0x73993dfad520>
.../control/tasks/pose_target_ik.py:491: in _validate_frame_targets
    context = self._get_control_context(robot_model, frames, joints)
        command_limit_margin = 0.0001
        controlled_joints = ('left_arm/joint1', 'left_arm/joint2', 'left_arm/joint3', 'left_arm/joint4', 'left_arm/joint5', 'left_arm/joint6', ...)
        frame_names = ('left_grasp_frame', 'right_grasp_frame')
        frames     = ('left_grasp_frame', 'right_grasp_frame')
        joints     = ('left_arm/joint1', 'left_arm/joint2', 'left_arm/joint3', 'left_arm/joint4', 'left_arm/joint5', 'left_arm/joint6', ...)
        robot_model = RobotModelConfig(rpc_transport=<class 'dimos.protocol.rpc.pubsubrpc.LCMRPC'>, default_rpc_timeout=120.0, rpc_timeouts=...extra_links=[], home_joints=[0.0, 1.047, 1.047, 0.0, 0.0, 0.0, 0.0, 1.047, 1.047, 0.0, 0.0, 0.0], pre_grasp_offset=0.1)
        self       = <dimos.robot.manipulators.dual_openyam.teleop_ik.DualOpenYamPinkPoseTargetSolver object at 0x73993dfad520>
.../control/tasks/pose_target_ik.py:536: in _get_control_context
    if cache_key not in self._control_contexts:
        cache_key  = (RobotModel(_source_path=PosixPath('.../data/dual_openyam_abc_box_v2/dual_openyam.urdf'), _package_paths=...'left_arm/joint1', 'left_arm/joint2', 'left_arm/joint3', 'left_arm/joint4', 'left_arm/joint5', 'left_arm/joint6', ...))
        config     = RobotModelConfig(rpc_transport=<class 'dimos.protocol.rpc.pubsubrpc.LCMRPC'>, default_rpc_timeout=120.0, rpc_timeouts=...extra_links=[], home_joints=[0.0, 1.047, 1.047, 0.0, 0.0, 0.0, 0.0, 1.047, 1.047, 0.0, 0.0, 0.0], pre_grasp_offset=0.1)
        controlled_joints = ('left_arm/joint1', 'left_arm/joint2', 'left_arm/joint3', 'left_arm/joint4', 'left_arm/joint5', 'left_arm/joint6', ...)
        frame_names = ('left_grasp_frame', 'right_grasp_frame')
        frames     = ('left_grasp_frame', 'right_grasp_frame')
        self       = <dimos.robot.manipulators.dual_openyam.teleop_ik.DualOpenYamPinkPoseTargetSolver object at 0x73993dfad520>
<string>:3: in __hash__
    ???
        self       = RobotModel(_source_path=PosixPath('.../data/dual_openyam_abc_box_v2/dual_openyam.urdf'), _package_paths=(...dimos/data/dual_openyam_abc_box_v2')),), _xacro_args=(), _fixed_frames=(), _fixed_joints=(), _joint_position_limits=())
....../github/home/.local.../uv/python/cpython-3.12.12-linux-x86_64-gnu/lib/python3.12/pathlib.py:529: in __hash__
    return self._hash
        self       = PosixPath('.../data/dual_openyam_abc_box_v2/dual_openyam.urdf')
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = PosixPath('.../data/dual_openyam_abc_box_v2/dual_openyam.urdf')
name = '_hash'

    def __getattribute__(self, name: str) -> object:
        # During Path.__new__(), _lfs_filename hasn't been set yet.
        # Fall through to normal Path behavior until construction is complete.
        try:
            object.__getattribute__(self, "_lfs_filename")
        except AttributeError:
            return object.__getattribute__(self, name)
    
        # After construction, allow access to our internal attributes directly
        if name in ("_lfs_filename", "_lfs_resolved_cache", "_ensure_downloaded"):
            return object.__getattribute__(self, name)
    
        # For all other attributes, ensure download first then delegate to resolved path
        resolved = object.__getattribute__(self, "_ensure_downloaded")()
>       return getattr(resolved, name)
E       AttributeError: 'PosixPath' object has no attribute '_hash'

name       = '_hash'
resolved   = PosixPath('.../data/dual_openyam_abc_box_v2/dual_openyam.urdf')
self       = PosixPath('.../data/dual_openyam_abc_box_v2/dual_openyam.urdf')

dimos/utils/data.py:371: AttributeError
dimos.hardware.whole_body.dual_openyam_damiao.test_adapter::test_adapter_connects_complete_dual_yam_topology
Stack Traces | 3.15s run time
adapter = <dimos.hardware.whole_body.dual_openyam_damiao.adapter.DualOpenYamDamiaoAdapter object at 0xff598c667d70>

    def test_adapter_connects_complete_dual_yam_topology(
        adapter: DualOpenYamDamiaoAdapter,
    ) -> None:
>       assert adapter.connect()
E       assert False
E        +  where False = connect()
E        +    where connect = <dimos.hardware.whole_body.dual_openyam_damiao.adapter.DualOpenYamDamiaoAdapter object at 0xff598c667d70>.connect

adapter    = <dimos.hardware.whole_body.dual_openyam_damiao.adapter.DualOpenYamDamiaoAdapter object at 0xff598c667d70>

.../whole_body/dual_openyam_damiao/test_adapter.py:44: AssertionError
dimos.robot.manipulators.dual_openyam.test_blueprints::test_quest_blueprint_selects_physical_hardware_from_both_can_ports
Stack Traces | 3.25s run time
value = <[RuntimeError("Failed to pull LFS file .../dimos/data/.lfs/dual_openyam_abc_box_v2.tar.gz after 3...ox_v2.tar.gz', '--exclude=']' returned non-zero exit status 1.") raised in repr()] TaskConfig object at 0xff8c584a8950>

    def _copy_opaque(value: Any) -> Any:
        try:
>           return copy.deepcopy(value)

value      = <[RuntimeError("Failed to pull LFS file .../dimos/data/.lfs/dual_openyam_abc_box_v2.tar.gz after 3...ox_v2.tar.gz', '--exclude=']' returned non-zero exit status 1.") raised in repr()] TaskConfig object at 0xff8c584a8950>

.../coordination/blueprint_config/values.py:148: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
........................................../usr/lib/python3.12/copy.py:162: in deepcopy
    y = _reconstruct(x, memo, *rv)
        _nil       = []
        cls        = <class 'dimos.control.coordinator.TaskConfig'>
        copier     = None
        d          = 280978241784144
        memo       = {280978008416896: [['left_arm/joint1', 'left_arm/joint2', 'left_arm/joint3', 'left_arm/joint4', 'left_arm/joint5', 'le..._arm/joint1', 'left_arm/joint2', 'left_arm/joint3', 'left_arm/joint4', 'left_arm/joint5', 'left_arm/joint6', ...], ...}
        reductor   = <built-in method __reduce_ex__ of TaskConfig object at 0xff8c584a8950>
        rv         = (<function __newobj__ at 0xff8d4b1b45e0>, (<class 'dimos.control.coordinator.TaskConfig'>,), {'auto_start': False, 'jo...lter_cutoff_hz': 30.0, 'max_command_tracking_error_deg': 10.0, 'max_joint_velocity_rad_s': 2.0, ...}, ...}, None, None)
        x          = <[RuntimeError("Failed to pull LFS file .../dimos/data/.lfs/dual_openyam_abc_box_v2.tar.gz after 3...ox_v2.tar.gz', '--exclude=']' returned non-zero exit status 1.") raised in repr()] TaskConfig object at 0xff8c584a8950>
        y          = []
........................................../usr/lib/python3.12/copy.py:259: in _reconstruct
    state = deepcopy(state, memo)
        args       = <generator object _reconstruct.<locals>.<genexpr> at 0xff8cf9c58310>
        deep       = True
        deepcopy   = <function deepcopy at 0xff8d4ab46020>
        dictiter   = None
        func       = <function __newobj__ at 0xff8d4b1b45e0>
        listiter   = None
        memo       = {280978008416896: [['left_arm/joint1', 'left_arm/joint2', 'left_arm/joint3', 'left_arm/joint4', 'left_arm/joint5', 'le..._arm/joint1', 'left_arm/joint2', 'left_arm/joint3', 'left_arm/joint4', 'left_arm/joint5', 'left_arm/joint6', ...], ...}
        state      = {'auto_start': False, 'joint_names': ['left_arm/joint1', 'left_arm/joint2', 'left_arm/joint3', 'left_arm/joint4', 'lef...nt_command_filter_cutoff_hz': 30.0, 'max_command_tracking_error_deg': 10.0, 'max_joint_velocity_rad_s': 2.0, ...}, ...}
        x          = <[RuntimeError("Failed to pull LFS file .../dimos/data/.lfs/dual_openyam_abc_box_v2.tar.gz after 3...ox_v2.tar.gz', '--exclude=']' returned non-zero exit status 1.") raised in repr()] TaskConfig object at 0xff8c584a8950>
        y          = <[AttributeError("'TaskConfig' object has no attribute 'name'") raised in repr()] TaskConfig object at 0xff8d3653f890>
........................................../usr/lib/python3.12/copy.py:136: in deepcopy
    y = copier(x, memo)
        _nil       = []
        cls        = <class 'dict'>
        copier     = <function _deepcopy_dict at 0xff8d4a944ae0>
        d          = 280978249268672
        memo       = {280978008416896: [['left_arm/joint1', 'left_arm/joint2', 'left_arm/joint3', 'left_arm/joint4', 'left_arm/joint5', 'le..._arm/joint1', 'left_arm/joint2', 'left_arm/joint3', 'left_arm/joint4', 'left_arm/joint5', 'left_arm/joint6', ...], ...}
        x          = {'auto_start': False, 'joint_names': ['left_arm/joint1', 'left_arm/joint2', 'left_arm/joint3', 'left_arm/joint4', 'lef...nt_command_filter_cutoff_hz': 30.0, 'max_command_tracking_error_deg': 10.0, 'max_joint_velocity_rad_s': 2.0, ...}, ...}
        y          = []
........................................../usr/lib/python3.12/copy.py:221: in _deepcopy_dict
    y[deepcopy(key, memo)] = deepcopy(value, memo)
        deepcopy   = <function deepcopy at 0xff8d4ab46020>
        key        = 'params'
        memo       = {280978008416896: [['left_arm/joint1', 'left_arm/joint2', 'left_arm/joint3', 'left_arm/joint4', 'left_arm/joint5', 'le..._arm/joint1', 'left_arm/joint2', 'left_arm/joint3', 'left_arm/joint4', 'left_arm/joint5', 'left_arm/joint6', ...], ...}
        value      = {'bindings': [{'hand': 'left', 'target_frame': 'left_grasp_frame'}, {'hand': 'right', 'target_frame': 'right_grasp_fra..., 'joint_command_filter_cutoff_hz': 30.0, 'max_command_tracking_error_deg': 10.0, 'max_joint_velocity_rad_s': 2.0, ...}
        x          = {'auto_start': False, 'joint_names': ['left_arm/joint1', 'left_arm/joint2', 'left_arm/joint3', 'left_arm/joint4', 'lef...nt_command_filter_cutoff_hz': 30.0, 'max_command_tracking_error_deg': 10.0, 'max_joint_velocity_rad_s': 2.0, ...}, ...}
        y          = {'auto_start': False, 'joint_names': ['left_arm/joint1', 'left_arm/joint2', 'left_arm/joint3', 'left_arm/joint4', 'left_arm/joint5', 'left_arm/joint6', ...], 'name': 'teleop_dual_openyam', 'priority': 10, ...}
........................................../usr/lib/python3.12/copy.py:136: in deepcopy
    y = copier(x, memo)
        _nil       = []
        cls        = <class 'dict'>
        copier     = <function _deepcopy_dict at 0xff8d4a944ae0>
        d          = 280978028883072
        memo       = {280978008416896: [['left_arm/joint1', 'left_arm/joint2', 'left_arm/joint3', 'left_arm/joint4', 'left_arm/joint5', 'le..._arm/joint1', 'left_arm/joint2', 'left_arm/joint3', 'left_arm/joint4', 'left_arm/joint5', 'left_arm/joint6', ...], ...}
        x          = {'bindings': [{'hand': 'left', 'target_frame': 'left_grasp_frame'}, {'hand': 'right', 'target_frame': 'right_grasp_fra..., 'joint_command_filter_cutoff_hz': 30.0, 'max_command_tracking_error_deg': 10.0, 'max_joint_velocity_rad_s': 2.0, ...}
        y          = []
........................................../usr/lib/python3.12/copy.py:221: in _deepcopy_dict
    y[deepcopy(key, memo)] = deepcopy(value, memo)
        deepcopy   = <function deepcopy at 0xff8d4ab46020>
        key        = 'robot_model'
        memo       = {280978008416896: [['left_arm/joint1', 'left_arm/joint2', 'left_arm/joint3', 'left_arm/joint4', 'left_arm/joint5', 'le..._arm/joint1', 'left_arm/joint2', 'left_arm/joint3', 'left_arm/joint4', 'left_arm/joint5', 'left_arm/joint6', ...], ...}
        value      = <[RuntimeError("Failed to pull LFS file .../dimos/data/.lfs/dual_openyam_abc_box_v2.tar.gz after 3...tar.gz', '--exclude=']' returned non-zero exit status 1.") raised in repr()] RobotModelConfig object at 0xff8c4ba8dc20>
        x          = {'bindings': [{'hand': 'left', 'target_frame': 'left_grasp_frame'}, {'hand': 'right', 'target_frame': 'right_grasp_fra..., 'joint_command_filter_cutoff_hz': 30.0, 'max_command_tracking_error_deg': 10.0, 'max_joint_velocity_rad_s': 2.0, ...}
        y          = {}
........................................../usr/lib/python3.12/copy.py:143: in deepcopy
    y = copier(memo)
        _nil       = []
        cls        = <class 'dimos.manipulation.planning.spec.config.RobotModelConfig'>
        copier     = <[RuntimeError("Failed to pull LFS file .../dimos/data/.lfs/dual_openyam_abc_box_v2.tar.gz after 3...bc_box_v2.tar.gz', '--exclude=']' returned non-zero exit status 1.") raised in repr()] method object at 0xff8c4b6ad040>
        d          = 280978029861920
        memo       = {280978008416896: [['left_arm/joint1', 'left_arm/joint2', 'left_arm/joint3', 'left_arm/joint4', 'left_arm/joint5', 'le..._arm/joint1', 'left_arm/joint2', 'left_arm/joint3', 'left_arm/joint4', 'left_arm/joint5', 'left_arm/joint6', ...], ...}
        x          = <[RuntimeError("Failed to pull LFS file .../dimos/data/.lfs/dual_openyam_abc_box_v2.tar.gz after 3...tar.gz', '--exclude=']' returned non-zero exit status 1.") raised in repr()] RobotModelConfig object at 0xff8c4ba8dc20>
        y          = []
.venv/lib/python3.12.../site-packages/pydantic/main.py:978: in __deepcopy__
    _object_setattr(m, '__dict__', deepcopy(self.__dict__, memo=memo))
        cls        = <class 'dimos.manipulation.planning.spec.config.RobotModelConfig'>
        m          = RobotModelConfig()
        memo       = {280978008416896: [['left_arm/joint1', 'left_arm/joint2', 'left_arm/joint3', 'left_arm/joint4', 'left_arm/joint5', 'le..._arm/joint1', 'left_arm/joint2', 'left_arm/joint3', 'left_arm/joint4', 'left_arm/joint5', 'left_arm/joint6', ...], ...}
        self       = <[RuntimeError("Failed to pull LFS file .../dimos/data/.lfs/dual_openyam_abc_box_v2.tar.gz after 3...tar.gz', '--exclude=']' returned non-zero exit status 1.") raised in repr()] RobotModelConfig object at 0xff8c4ba8dc20>
........................................../usr/lib/python3.12/copy.py:136: in deepcopy
    y = copier(x, memo)
        _nil       = []
        cls        = <class 'dict'>
        copier     = <function _deepcopy_dict at 0xff8d4a944ae0>
        d          = 280978028878400
        memo       = {280978008416896: [['left_arm/joint1', 'left_arm/joint2', 'left_arm/joint3', 'left_arm/joint4', 'left_arm/joint5', 'le..._arm/joint1', 'left_arm/joint2', 'left_arm/joint3', 'left_arm/joint4', 'left_arm/joint5', 'left_arm/joint6', ...], ...}
        x          = {'auto_convert_meshes': True, 'base_link': 'dual_openyam_base', 'base_pose': Pose(position=Vector([          0           0           0]), orientation=Quaternion(0.000000, 0.000000, 0.000000, 1.000000)), 'collision_exclusion_pairs': [], ...}
        y          = []
........................................../usr/lib/python3.12/copy.py:221: in _deepcopy_dict
    y[deepcopy(key, memo)] = deepcopy(value, memo)
        deepcopy   = <function deepcopy at 0xff8d4ab46020>
        key        = 'model'
        memo       = {280978008416896: [['left_arm/joint1', 'left_arm/joint2', 'left_arm/joint3', 'left_arm/joint4', 'left_arm/joint5', 'le..._arm/joint1', 'left_arm/joint2', 'left_arm/joint3', 'left_arm/joint4', 'left_arm/joint5', 'left_arm/joint6', ...], ...}
        value      = <[RuntimeError("Failed to pull LFS file .../dimos/data/.lfs/dual_openyam_abc_box_v2.tar.gz after 3...ox_v2.tar.gz', '--exclude=']' returned non-zero exit status 1.") raised in repr()] RobotModel object at 0xff8cc620cfe0>
        x          = {'auto_convert_meshes': True, 'base_link': 'dual_openyam_base', 'base_pose': Pose(position=Vector([          0           0           0]), orientation=Quaternion(0.000000, 0.000000, 0.000000, 1.000000)), 'collision_exclusion_pairs': [], ...}
        y          = {'default_rpc_timeout': 120.0, 'frame_id': None, 'frame_id_prefix': None, 'g': GlobalConfig(robot_ip=None, robot_ips=N...ess=True, local_relay=False, relay_url=None, dimos_cloud_url='https://login.dimensional.org', dimos_api_key=None), ...}
........................................../usr/lib/python3.12/copy.py:162: in deepcopy
    y = _reconstruct(x, memo, *rv)
        _nil       = []
        cls        = <class 'dimos.robot.assets.model.RobotModel'>
        copier     = None
        d          = 280980084543456
        memo       = {280978008416896: [['left_arm/joint1', 'left_arm/joint2', 'left_arm/joint3', 'left_arm/joint4', 'left_arm/joint5', 'le..._arm/joint1', 'left_arm/joint2', 'left_arm/joint3', 'left_arm/joint4', 'left_arm/joint5', 'left_arm/joint6', ...], ...}
        reductor   = <built-in method __reduce_ex__ of RobotModel object at 0xff8cc620cfe0>
        rv         = (<function __newobj__ at 0xff8d4b1b45e0>, (<class 'dimos.robot.assets.model.RobotModel'>,), {'_fixed_frames': (), '_fi...xclude=']' returned non-zero exit status 1.") raised in repr()] LfsPath object at 0xff8cc5ed58d0>),), ...}, None, None)
        x          = <[RuntimeError("Failed to pull LFS file .../dimos/data/.lfs/dual_openyam_abc_box_v2.tar.gz after 3...ox_v2.tar.gz', '--exclude=']' returned non-zero exit status 1.") raised in repr()] RobotModel object at 0xff8cc620cfe0>
        y          = []
........................................../usr/lib/python3.12/copy.py:259: in _reconstruct
    state = deepcopy(state, memo)
        args       = <generator object _reconstruct.<locals>.<genexpr> at 0xff8d365d2980>
        deep       = True
        deepcopy   = <function deepcopy at 0xff8d4ab46020>
        dictiter   = None
        func       = <function __newobj__ at 0xff8d4b1b45e0>
        listiter   = None
        memo       = {280978008416896: [['left_arm/joint1', 'left_arm/joint2', 'left_arm/joint3', 'left_arm/joint4', 'left_arm/joint5', 'le..._arm/joint1', 'left_arm/joint2', 'left_arm/joint3', 'left_arm/joint4', 'left_arm/joint5', 'left_arm/joint6', ...], ...}
        state      = {'_fixed_frames': (), '_fixed_joints': (), '_joint_position_limits': (), '_package_paths': (('dual_openyam_abc_box', <...tar.gz', '--exclude=']' returned non-zero exit status 1.") raised in repr()] LfsPath object at 0xff8cc5ed58d0>),), ...}
        x          = <[RuntimeError("Failed to pull LFS file .../dimos/data/.lfs/dual_openyam_abc_box_v2.tar.gz after 3...ox_v2.tar.gz', '--exclude=']' returned non-zero exit status 1.") raised in repr()] RobotModel object at 0xff8cc620cfe0>
        y          = <[AttributeError("'RobotModel' object has no attribute '_source_path'") raised in repr()] RobotModel object at 0xff8d3653f8f0>
........................................../usr/lib/python3.12/copy.py:136: in deepcopy
    y = copier(x, memo)
        _nil       = []
        cls        = <class 'dict'>
        copier     = <function _deepcopy_dict at 0xff8d4a944ae0>
        d          = 280981951778048
        memo       = {280978008416896: [['left_arm/joint1', 'left_arm/joint2', 'left_arm/joint3', 'left_arm/joint4', 'left_arm/joint5', 'le..._arm/joint1', 'left_arm/joint2', 'left_arm/joint3', 'left_arm/joint4', 'left_arm/joint5', 'left_arm/joint6', ...], ...}
        x          = {'_fixed_frames': (), '_fixed_joints': (), '_joint_position_limits': (), '_package_paths': (('dual_openyam_abc_box', <...tar.gz', '--exclude=']' returned non-zero exit status 1.") raised in repr()] LfsPath object at 0xff8cc5ed58d0>),), ...}
        y          = []
........................................../usr/lib/python3.12/copy.py:221: in _deepcopy_dict
    y[deepcopy(key, memo)] = deepcopy(value, memo)
        deepcopy   = <function deepcopy at 0xff8d4ab46020>
        key        = '_source_path'
        memo       = {280978008416896: [['left_arm/joint1', 'left_arm/joint2', 'left_arm/joint3', 'left_arm/joint4', 'left_arm/joint5', 'le..._arm/joint1', 'left_arm/joint2', 'left_arm/joint3', 'left_arm/joint4', 'left_arm/joint5', 'left_arm/joint6', ...], ...}
        value      = <[RuntimeError("Failed to pull LFS file .../dimos/data/.lfs/dual_openyam_abc_box_v2.tar.gz after 3...c_box_v2.tar.gz', '--exclude=']' returned non-zero exit status 1.") raised in repr()] LfsPath object at 0xff8cc5ed7ed0>
        x          = {'_fixed_frames': (), '_fixed_joints': (), '_joint_position_limits': (), '_package_paths': (('dual_openyam_abc_box', <...tar.gz', '--exclude=']' returned non-zero exit status 1.") raised in repr()] LfsPath object at 0xff8cc5ed58d0>),), ...}
        y          = {}
........................................../usr/lib/python3.12/copy.py:141: in deepcopy
    copier = getattr(x, "__deepcopy__", None)
        _nil       = []
        cls        = <class 'dimos.utils.data.LfsPath'>
        copier     = None
        d          = 280980081180368
        memo       = {280978008416896: [['left_arm/joint1', 'left_arm/joint2', 'left_arm/joint3', 'left_arm/joint4', 'left_arm/joint5', 'le..._arm/joint1', 'left_arm/joint2', 'left_arm/joint3', 'left_arm/joint4', 'left_arm/joint5', 'left_arm/joint6', ...], ...}
        x          = <[RuntimeError("Failed to pull LFS file .../dimos/data/.lfs/dual_openyam_abc_box_v2.tar.gz after 3...c_box_v2.tar.gz', '--exclude=']' returned non-zero exit status 1.") raised in repr()] LfsPath object at 0xff8cc5ed7ed0>
        y          = []
dimos/utils/data.py:370: in __getattribute__
    resolved = object.__getattribute__(self, "_ensure_downloaded")()
        name       = '__deepcopy__'
        self       = <[RuntimeError("Failed to pull LFS file .../dimos/data/.lfs/dual_openyam_abc_box_v2.tar.gz after 3...c_box_v2.tar.gz', '--exclude=']' returned non-zero exit status 1.") raised in repr()] LfsPath object at 0xff8cc5ed7ed0>
dimos/utils/data.py:353: in _ensure_downloaded
    cache = get_data(filename)
        cache      = None
        filename   = 'dual_openyam_abc_box_v2/dual_openyam.urdf'
        self       = <[RuntimeError("Failed to pull LFS file .../dimos/data/.lfs/dual_openyam_abc_box_v2.tar.gz after 3...c_box_v2.tar.gz', '--exclude=']' returned non-zero exit status 1.") raised in repr()] LfsPath object at 0xff8cc5ed7ed0>
dimos/utils/data.py:310: in get_data
    archive_path = _decompress_archive(_pull_lfs_archive(archive_name))
        archive_name = 'dual_openyam_abc_box_v2'
        data_dir   = PosixPath('.../dimos/dimos/data')
        file_path  = PosixPath('.../dimos/dimos/data/dual_openyam_abc_box_v2/dual_openyam.urdf')
        name       = 'dual_openyam_abc_box_v2/dual_openyam.urdf'
        nested_path = PosixPath('dual_openyam.urdf')
        path_parts = ('dual_openyam_abc_box_v2', 'dual_openyam.urdf')
dimos/utils/data.py:254: in _pull_lfs_archive
    _lfs_pull(file_path, repo_root)
        file_path  = PosixPath('.../dimos/data/.lfs/dual_openyam_abc_box_v2.tar.gz')
        filename   = 'dual_openyam_abc_box_v2'
        repo_root  = PosixPath('.../work/dimos/dimos')
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

file_path = PosixPath('.../dimos/data/.lfs/dual_openyam_abc_box_v2.tar.gz')
repo_root = PosixPath('.../work/dimos/dimos')

    def _lfs_pull(file_path: Path, repo_root: Path, *, retries: int = 2) -> None:
        relative_path = file_path.relative_to(repo_root)
    
        env = os.environ.copy()
        env["GIT_LFS_FORCE_PROGRESS"] = "1"
    
        last_err: subprocess.CalledProcessError | None = None
        for attempt in range(1, retries + 2):  # retries + 1 total attempts
            try:
                subprocess.run(
                    # --exclude= overrides lfs.fetchexclude from .lfsconfig, which
                    # otherwise silently skips data/.lfs/* even when --include matches.
                    ["git", "lfs", "pull", "--include", str(relative_path), "--exclude="],
                    cwd=repo_root,
                    check=True,
                    env=env,
                )
                return
            except subprocess.CalledProcessError as e:
                last_err = e
                if attempt <= retries:
                    time.sleep(attempt)  # 1s, 2s backoff
    
>       raise RuntimeError(
            f"Failed to pull LFS file {file_path} after {retries + 1} attempts: {last_err}"
        )
E       RuntimeError: Failed to pull LFS file .../dimos/data/.lfs/dual_openyam_abc_box_v2.tar.gz after 3 attempts: Command '['git', 'lfs', 'pull', '--include', 'data/.lfs/dual_openyam_abc_box_v2.tar.gz', '--exclude=']' returned non-zero exit status 1.

attempt    = 3
env        = {'ACCEPT_EULA': 'Y', 'ACTIONS_ID_TOKEN_REQUEST_TOKEN': 'eyJhbGciOiJSUzI1NiIsImtpZCI6IjM4ODI2YjE3LTZhMzAtNWY5Yi1iMTY5LT...-version=2.0', 'ACTIONS_ORCHESTRATION_ID': '5d7d0fdc-c160-4180-a14f-f6e2ec4fda24.tests.ubuntu-24_04-arm_3_14_fal', ...}
file_path  = PosixPath('.../dimos/data/.lfs/dual_openyam_abc_box_v2.tar.gz')
last_err   = CalledProcessError(1, ['git', 'lfs', 'pull', '--include', 'data/.lfs/dual_openyam_abc_box_v2.tar.gz', '--exclude='])
relative_path = PosixPath('data/.lfs/dual_openyam_abc_box_v2.tar.gz')
repo_root  = PosixPath('.../work/dimos/dimos')
retries    = 2

dimos/utils/data.py:224: RuntimeError

The above exception was the direct cause of the following exception:

    def test_quest_blueprint_selects_physical_hardware_from_both_can_ports() -> None:
>       parsed = BlueprintConfigParser(teleop_quest_dual_openyam).parse(
            [
                "--left-can-port",
                "follower_l",
                "--right-can-port",
                "follower_r",
                "--manipulationmodule.visualization.host=0.0.0.0",
            ],
            environ={},
        )


.../manipulators/dual_openyam/test_blueprints.py:49: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
.../coordination/blueprint_config/parser.py:169: in parse
    key: plain(value)
        cli_tokens = ['--left-can-port', 'follower_l', '--right-can-port', 'follower_r', '--manipulationmodule.visualization.host=0.0.0.0']
        config_path = None
        environ    = {}
        global_overrides = None
        overrides  = None
        schema     = <[RuntimeError("Failed to pull LFS file .../dimos/data/.lfs/dual_openyam_abc_box_v2.tar.gz after 3..._v2.tar.gz', '--exclude=']' returned non-zero exit status 1.") raised in repr()] ParserSchema object at 0xff8c4a4abb40>
        self       = <dimos.core.coordination.blueprint_config.parser.BlueprintConfigParser object at 0xff8d36541970>
.../coordination/blueprint_config/values.py:50: in plain
    return [plain(item) for item in value]
        value      = [<[RuntimeError("Failed to pull LFS file .../dimos/data/.lfs/dual_openyam_abc_box_v2.tar.gz after ...oint5', 'right_arm/joint6'], priority=20, auto_start=False, params={'start_position_tolerance': 0.05}, stream_bind={})]
.../coordination/blueprint_config/values.py:55: in plain
    return _copy_opaque(value)
        value      = <[RuntimeError("Failed to pull LFS file .../dimos/data/.lfs/dual_openyam_abc_box_v2.tar.gz after 3...ox_v2.tar.gz', '--exclude=']' returned non-zero exit status 1.") raised in repr()] TaskConfig object at 0xff8c584a8950>
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

value = <[RuntimeError("Failed to pull LFS file .../dimos/data/.lfs/dual_openyam_abc_box_v2.tar.gz after 3...ox_v2.tar.gz', '--exclude=']' returned non-zero exit status 1.") raised in repr()] TaskConfig object at 0xff8c584a8950>

    def _copy_opaque(value: Any) -> Any:
        try:
            return copy.deepcopy(value)
        except Exception as error:
>           raise BlueprintConfigError(
                f"Configuration value of type {type(value).__name__} cannot be copied safely: {error}"
            ) from error
E           dimos.core.coordination.blueprint_config.errors.BlueprintConfigError: Configuration value of type TaskConfig cannot be copied safely: Failed to pull LFS file .../dimos/data/.lfs/dual_openyam_abc_box_v2.tar.gz after 3 attempts: Command '['git', 'lfs', 'pull', '--include', 'data/.lfs/dual_openyam_abc_box_v2.tar.gz', '--exclude=']' returned non-zero exit status 1.

value      = <[RuntimeError("Failed to pull LFS file .../dimos/data/.lfs/dual_openyam_abc_box_v2.tar.gz after 3...ox_v2.tar.gz', '--exclude=']' returned non-zero exit status 1.") raised in repr()] TaskConfig object at 0xff8c584a8950>

.../coordination/blueprint_config/values.py:150: BlueprintConfigError

To view more test analytics, go to the Test Analytics Dashboard
📋 Got 3 mins? Take this short survey to help us improve Test Analytics.

Comment thread dimos/core/isolated_python_bootstrap.py
@TomCC7
TomCC7 marked this pull request as ready for review August 16, 2026 07:02
@github-actions github-actions Bot added ready-to-merge Required CI checks have passed on this PR and removed ready-to-merge Required CI checks have passed on this PR labels Aug 16, 2026
Comment thread docs/usage/native_modules.md Outdated
Comment thread docs/usage/native_modules.md Outdated
Comment thread dimos/core/python_native_module.py Outdated
@TomCC7
TomCC7 force-pushed the cc/feat/external-python-native-module branch from 7b1680e to e9c8f9b Compare August 18, 2026 00:07
@TomCC7
TomCC7 changed the base branch from main to cc/feat/openyam-macos-support August 18, 2026 00:08
@TomCC7 TomCC7 changed the title feat: add Python native module runtime feat: add isolated Python module runtime Aug 18, 2026
@TomCC7 TomCC7 mentioned this pull request Aug 18, 2026
1 task
@TomCC7
TomCC7 force-pushed the cc/feat/external-python-native-module branch from e9c8f9b to 4379fd0 Compare August 21, 2026 05:15
@greptile-apps

greptile-apps Bot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This change adds an isolated Python runtime that executes module implementations from sibling projects while preserving host-side RPC, stream, and lifecycle interfaces. One reliability and denial-of-service issue remains: output from an isolated runtime is captured but never drained, so verbose logging can block the runtime before it processes later RPC or lifecycle work.

The earlier report that isolated runtimes survive owner termination was disproved. A real ExampleExternal runtime exited within two seconds after its owner alone received SIGKILL, so the tested runtime did not remain orphaned.

Confidence Score: 3/5

The isolated Python runtime can deadlock after producing enough captured output, preventing applications from completing RPC and lifecycle operations.

A verified denial-of-service failure remains in the isolated runtime subprocess handling.

Files Needing Attention: dimos/core/isolated_python_module.py

Security Review

The isolated runtime can become unavailable when it writes enough stdout or stderr output to fill an unconsumed subprocess pipe. This allows runtime-controlled output, including normal verbose logging, to block RPC and lifecycle processing.

T-Rex T-Rex Logs

What T-Rex did

  • T-Rex produced a proof for the posted P1 finding with supporting artifacts showing the reproduction and baseline outputs.
  • T-Rex executed the real ExampleExternal isolated-runtime lifecycle path, killed the owner process, and confirmed the runtime and bootstrap process were not alive.
  • T-Rex documented the before- and after-state around the owner SIGKILL action and validated that no isolated bootstrap runtime remained after the kill.

View all artifacts

T-Rex Ran code and verified through T-Rex

Reviews (3): Last reviewed commit: "feat: add isolated Python module runtime" | Re-trigger Greptile

Comment thread dimos/core/isolated_python_module.py
Comment thread dimos/core/isolated_python_module.py
@TomCC7
TomCC7 force-pushed the cc/feat/external-python-native-module branch from 8e4879a to 36b9b55 Compare August 25, 2026 00:56
@TomCC7
TomCC7 changed the base branch from cc/feat/openyam-macos-support to cc/rust-recorder August 25, 2026 00:57
Comment thread dimos/core/isolated_python_module.py
@TomCC7
TomCC7 force-pushed the cc/feat/external-python-native-module branch from 36b9b55 to ab3408d Compare August 25, 2026 22:50
@TomCC7
TomCC7 force-pushed the cc/feat/external-python-native-module branch 2 times, most recently from a5a137c to 4d8615e Compare August 25, 2026 23:08
@TomCC7
TomCC7 force-pushed the cc/feat/external-python-native-module branch from 4d8615e to 8280614 Compare August 25, 2026 23:17
@TomCC7
TomCC7 force-pushed the cc/feat/external-python-native-module branch from 8280614 to c4e9f2c Compare August 25, 2026 23:25
@TomCC7
TomCC7 force-pushed the cc/feat/external-python-native-module branch from c4e9f2c to 36b9b55 Compare August 25, 2026 23:50
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