From 6c47599b5c2c6bbcbed50bf24bdd1f7b0ea042d6 Mon Sep 17 00:00:00 2001 From: Josue Nina Date: Tue, 23 Jun 2026 12:47:21 -0500 Subject: [PATCH] fix $type being stripped from live command payload --- lean/components/docker/docker_manager.py | 11 +++-- .../components/docker/test_docker_manager.py | 48 +++++++++++++++++++ 2 files changed, 54 insertions(+), 5 deletions(-) create mode 100644 tests/components/docker/test_docker_manager.py diff --git a/lean/components/docker/docker_manager.py b/lean/components/docker/docker_manager.py index dbb95391..887130a7 100644 --- a/lean/components/docker/docker_manager.py +++ b/lean/components/docker/docker_manager.py @@ -495,10 +495,11 @@ def write_to_file(self, docker_container_name: str, docker_file: Path, data: Dic raise ValueError(f"Container {docker_container_name} is not running") data = dumps(data, cls=DecimalEncoder) - data = data.replace('"','\\"') - command = f'docker exec {docker_container_name} bash -c "echo \'{data}\' > {docker_file.as_posix()}"' + command = ["docker", "exec", docker_container_name, "bash", "-c", + f"echo '{data}' > {docker_file.as_posix()}"] + # No shell=True so the host shell does not replace $type with an empty value try: - run(command, shell=True, check=True) + run(command, check=True) except CalledProcessError as exception: raise ValueError(f"Failed to write to {docker_file.name}: {exception.output.decode('utf-8')}") except Exception as e: @@ -517,7 +518,7 @@ def read_from_file(self, docker_container_name: str, docker_file: Path, interval from subprocess import Popen, PIPE, CalledProcessError from time import sleep, time - command = f'docker exec {docker_container_name} bash -c "cat {docker_file.as_posix()}"' + command = ["docker", "exec", docker_container_name, "bash", "-c", f"cat {docker_file.as_posix()}"] start = time() success = False error_message = None @@ -529,7 +530,7 @@ def read_from_file(self, docker_container_name: str, docker_file: Path, interval error_message = f"Container {docker_container_name} does not exist" container_running = False break - p = Popen(command, shell=True, stdin=PIPE, stdout=PIPE, stderr=PIPE) + p = Popen(command, stdin=PIPE, stdout=PIPE, stderr=PIPE) output = p.stdout.read().decode('utf-8') if output is not None and output != "": success = True diff --git a/tests/components/docker/test_docker_manager.py b/tests/components/docker/test_docker_manager.py new file mode 100644 index 00000000..c3a146d4 --- /dev/null +++ b/tests/components/docker/test_docker_manager.py @@ -0,0 +1,48 @@ +# QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. +# Lean CLI v1.0. Copyright 2021 QuantConnect Corporation. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from pathlib import Path +from unittest import mock + +from lean.components.docker.docker_manager import DockerManager + + +def _create_docker_manager() -> DockerManager: + return DockerManager(mock.Mock(), mock.Mock(), mock.Mock()) + + +def test_write_to_file_does_not_let_the_host_shell_expand_the_payload() -> None: + docker_manager = _create_docker_manager() + + container = mock.Mock() + container.status = "running" + + payload = {"$type": "QuantConnect.Orders.MarketOrder", "quantity": 100} + + with mock.patch.object(docker_manager, "get_container_by_name", return_value=container), \ + mock.patch("subprocess.run") as run_mock: + docker_manager.write_to_file("my-container", Path("/tmp/command.json"), payload) + + run_mock.assert_called_once() + args, kwargs = run_mock.call_args + + # The command must be passed as a list so the host shell never parses it + command = args[0] + assert isinstance(command, list) + assert kwargs.get("shell", False) is False + + echo_command = command[-1] + + # $type must reach the container untouched (raw JSON: real double quotes, no backslash escaping) + assert '"$type"' in echo_command + assert "\\" not in echo_command