From f7dfb88a306615ce6ec90a9bf7fabba86113f529 Mon Sep 17 00:00:00 2001 From: DanielWalnut <45447813+hetaoBackend@users.noreply.github.com> Date: Tue, 28 Apr 2026 11:47:56 +0800 Subject: [PATCH] fix(aio-sandbox): redact env values in container logs (#2562) * fix(aio-sandbox): redact env values in container logs Fixes #2534 * fix(aio-sandbox): address env log review comments --- .../community/aio_sandbox/local_backend.py | 44 ++++++++- .../tests/test_aio_sandbox_local_backend.py | 92 ++++++++++++++++++- 2 files changed, 134 insertions(+), 2 deletions(-) diff --git a/backend/packages/harness/deerflow/community/aio_sandbox/local_backend.py b/backend/packages/harness/deerflow/community/aio_sandbox/local_backend.py index 4b680df2d..15cbe3b78 100644 --- a/backend/packages/harness/deerflow/community/aio_sandbox/local_backend.py +++ b/backend/packages/harness/deerflow/community/aio_sandbox/local_backend.py @@ -9,6 +9,7 @@ from __future__ import annotations import json import logging import os +import shlex import subprocess from datetime import datetime @@ -86,6 +87,46 @@ def _format_container_mount(runtime: str, host_path: str, container_path: str, r return ["-v", mount_spec] +def _redact_container_command_for_log(cmd: list[str]) -> list[str]: + """Return a Docker/Container command with environment values redacted.""" + redacted: list[str] = [] + redact_next_env = False + + for arg in cmd: + if redact_next_env: + if "=" in arg: + key = arg.split("=", 1)[0] + redacted.append(f"{key}=" if key else "") + else: + redacted.append(arg) + redact_next_env = False + continue + + if arg in {"-e", "--env"}: + redacted.append(arg) + redact_next_env = True + continue + + if arg.startswith("--env="): + value = arg.removeprefix("--env=") + if "=" in value: + key = value.split("=", 1)[0] + redacted.append(f"--env={key}=" if key else "--env=") + else: + redacted.append(arg) + continue + + redacted.append(arg) + + return redacted + + +def _format_container_command_for_log(cmd: list[str]) -> str: + if os.name == "nt": + return subprocess.list2cmdline(cmd) + return shlex.join(cmd) + + class LocalContainerBackend(SandboxBackend): """Backend that manages sandbox containers locally using Docker or Apple Container. @@ -464,7 +505,8 @@ class LocalContainerBackend(SandboxBackend): cmd.append(self._image) - logger.info(f"Starting container using {self._runtime}: {' '.join(cmd)}") + log_cmd = _format_container_command_for_log(_redact_container_command_for_log(cmd)) + logger.info(f"Starting container using {self._runtime}: {log_cmd}") try: result = subprocess.run(cmd, capture_output=True, text=True, check=True) diff --git a/backend/tests/test_aio_sandbox_local_backend.py b/backend/tests/test_aio_sandbox_local_backend.py index d0b99bec1..d74786682 100644 --- a/backend/tests/test_aio_sandbox_local_backend.py +++ b/backend/tests/test_aio_sandbox_local_backend.py @@ -1,4 +1,8 @@ -from deerflow.community.aio_sandbox.local_backend import _format_container_mount +import logging +import os +from types import SimpleNamespace + +from deerflow.community.aio_sandbox.local_backend import LocalContainerBackend, _format_container_command_for_log, _format_container_mount, _redact_container_command_for_log def test_format_container_mount_uses_mount_syntax_for_docker_windows_paths(): @@ -26,3 +30,89 @@ def test_format_container_mount_keeps_volume_syntax_for_apple_container(): "-v", "/host/path:/mnt/path:ro", ] + + +def test_redact_container_command_for_log_redacts_env_values(): + redacted = _redact_container_command_for_log( + [ + "docker", + "run", + "-e", + "API_KEY=secret-value", + "--env=TOKEN=token-value", + "--name", + "sandbox", + "image", + ] + ) + + assert "API_KEY=" in redacted + assert "--env=TOKEN=" in redacted + assert "secret-value" not in " ".join(redacted) + assert "token-value" not in " ".join(redacted) + + +def test_redact_container_command_for_log_keeps_inherited_env_names(): + redacted = _redact_container_command_for_log( + [ + "docker", + "run", + "-e", + "API_KEY", + "--env=TOKEN", + "--name", + "sandbox", + "image", + ] + ) + + assert redacted == [ + "docker", + "run", + "-e", + "API_KEY", + "--env=TOKEN", + "--name", + "sandbox", + "image", + ] + + +def test_format_container_command_for_log_uses_windows_quoting(monkeypatch): + monkeypatch.setattr(os, "name", "nt") + + command = _format_container_command_for_log(["docker", "run", "--name", "sandbox one", "image"]) + + assert command == 'docker run --name "sandbox one" image' + + +def test_start_container_logs_redacted_env_values(monkeypatch, caplog): + backend = LocalContainerBackend( + image="sandbox:latest", + base_port=8080, + container_prefix="sandbox", + config_mounts=[], + environment={"API_KEY": "secret-value", "NORMAL": "visible-value"}, + ) + monkeypatch.setattr(backend, "_runtime", "docker") + + captured_cmd: list[str] = [] + + def fake_run(cmd, **kwargs): + captured_cmd.extend(cmd) + return SimpleNamespace(stdout="container-id\n", stderr="", returncode=0) + + monkeypatch.setattr("subprocess.run", fake_run) + + with caplog.at_level(logging.INFO, logger="deerflow.community.aio_sandbox.local_backend"): + backend._start_container("sandbox-test", 18080) + + joined_cmd = " ".join(captured_cmd) + assert "API_KEY=secret-value" in joined_cmd + assert "NORMAL=visible-value" in joined_cmd + + log_output = "\n".join(record.getMessage() for record in caplog.records) + assert "API_KEY=" in log_output + assert "NORMAL=" in log_output + assert "secret-value" not in log_output + assert "visible-value" not in log_output