From ce635b7de6e9495d299874cf347e899fa5d315e4 Mon Sep 17 00:00:00 2001 From: Dan Caldr <22105838+dcaldr@users.noreply.github.com> Date: Sat, 12 Sep 2026 15:12:36 +0200 Subject: [PATCH] fix(docker): allow aio DooD socket preflight on Windows Git Bash (#5370) (#5371) * fix(docker): allow aio DooD socket preflight on Windows Git Bash * test(deploy): add regression tests for deploy.sh DooD socket preflight * fix(docker): restrict Windows DooD socket bypass to default path Limit the Windows socket bypass in docker.sh and deploy.sh to the default /var/run/docker.sock path, and add regression tests ensuring custom missing socket paths are rejected. * test(docker): skip unreachable socket controls on hosts with live docker socket Add @pytest.mark.skipif on Path('/var/run/docker.sock').is_socket() to prevent false test failures on daemon-live hosts. --- .../test_docker_sandbox_mode_detection.py | 294 ++++++++++++++++++ scripts/deploy.sh | 12 +- scripts/docker.sh | 10 +- 3 files changed, 311 insertions(+), 5 deletions(-) diff --git a/backend/tests/test_docker_sandbox_mode_detection.py b/backend/tests/test_docker_sandbox_mode_detection.py index e244992f5..f5e2eeb55 100644 --- a/backend/tests/test_docker_sandbox_mode_detection.py +++ b/backend/tests/test_docker_sandbox_mode_detection.py @@ -2,6 +2,8 @@ from __future__ import annotations +import os +import shutil import subprocess import tempfile from pathlib import Path @@ -316,3 +318,295 @@ require_compose_version assert result.returncode == 1, result.stdout + result.stderr assert "too old" in result.stdout + + +def test_aio_dood_socket_preflight_allows_windows_when_docker_reachable(): + """Windows Git Bash without /var/run/docker.sock proceeds when Docker daemon is reachable.""" + with tempfile.TemporaryDirectory() as tmpdir: + tmp_root = Path(tmpdir) + _seed_compose_file(tmp_root) + _seed_env_examples(tmp_root) + (tmp_root / "config.yaml").write_text( + "sandbox:\n use: deerflow.community.aio_sandbox:AioSandboxProvider\n", + encoding="utf-8", + ) + command = f""" +source '{SCRIPT_PATH}' +PROJECT_ROOT='{tmp_root}' +DOCKER_DIR='{tmp_root}' +require_compose_version() {{ :; }} +uname() {{ echo 'MINGW64_NT-10.0'; }} +docker() {{ + if [ "$1" = info ]; then + return 0 + fi + return 0 +}} +DEER_FLOW_DOCKER_SOCKET='/var/run/docker.sock' +COMPOSE_CMD=echo +start +""" + result = subprocess.run( + [BASH_EXECUTABLE, "-lc", command], + capture_output=True, + text=True, + encoding="utf-8", + ) + + assert result.returncode == 0, result.stdout + result.stderr + assert "docker-compose.dood.yaml" in result.stdout + + +def test_aio_dood_socket_preflight_rejects_missing_socket_on_posix(): + """POSIX hosts without a physical socket file must fail fast.""" + with tempfile.TemporaryDirectory() as tmpdir: + tmp_root = Path(tmpdir) + _seed_compose_file(tmp_root) + _seed_env_examples(tmp_root) + (tmp_root / "config.yaml").write_text( + "sandbox:\n use: deerflow.community.aio_sandbox:AioSandboxProvider\n", + encoding="utf-8", + ) + command = f""" +source '{SCRIPT_PATH}' +PROJECT_ROOT='{tmp_root}' +DOCKER_DIR='{tmp_root}' +require_compose_version() {{ :; }} +uname() {{ echo 'Linux'; }} +DEER_FLOW_DOCKER_SOCKET='/nonexistent/docker.sock' +COMPOSE_CMD=echo +start +""" + result = subprocess.run( + [BASH_EXECUTABLE, "-lc", command], + capture_output=True, + text=True, + encoding="utf-8", + ) + + assert result.returncode == 1, result.stdout + result.stderr + assert "Docker socket not found" in result.stdout + + +def test_aio_dood_socket_preflight_rejects_missing_custom_socket_on_windows(): + """Windows Git Bash must fail if a custom non-existent socket path is specified.""" + with tempfile.TemporaryDirectory() as tmpdir: + tmp_root = Path(tmpdir) + _seed_compose_file(tmp_root) + _seed_env_examples(tmp_root) + (tmp_root / "config.yaml").write_text( + "sandbox:\n use: deerflow.community.aio_sandbox:AioSandboxProvider\n", + encoding="utf-8", + ) + command = f""" +source '{SCRIPT_PATH}' +PROJECT_ROOT='{tmp_root}' +DOCKER_DIR='{tmp_root}' +require_compose_version() {{ :; }} +uname() {{ echo 'MINGW64_NT-10.0'; }} +docker() {{ + if [ "$1" = info ]; then + return 0 + fi + return 0 +}} +DEER_FLOW_DOCKER_SOCKET='/nonexistent/docker.sock' +COMPOSE_CMD=echo +start +""" + result = subprocess.run( + [BASH_EXECUTABLE, "-lc", command], + capture_output=True, + text=True, + encoding="utf-8", + ) + + assert result.returncode == 1, result.stdout + result.stderr + assert "Docker socket not found" in result.stdout + + +@pytest.mark.skipif( + Path("/var/run/docker.sock").is_socket(), + reason="Host has real /var/run/docker.sock", +) +def test_aio_dood_socket_preflight_rejects_windows_when_docker_unreachable(): + """Windows Git Bash must fail if Docker daemon is not reachable.""" + with tempfile.TemporaryDirectory() as tmpdir: + tmp_root = Path(tmpdir) + _seed_compose_file(tmp_root) + _seed_env_examples(tmp_root) + (tmp_root / "config.yaml").write_text( + "sandbox:\n use: deerflow.community.aio_sandbox:AioSandboxProvider\n", + encoding="utf-8", + ) + command = f""" +source '{SCRIPT_PATH}' +PROJECT_ROOT='{tmp_root}' +DOCKER_DIR='{tmp_root}' +require_compose_version() {{ :; }} +uname() {{ echo 'MINGW64_NT-10.0'; }} +docker() {{ + if [ "$1" = info ]; then + return 1 + fi + return 0 +}} +DEER_FLOW_DOCKER_SOCKET='/var/run/docker.sock' +COMPOSE_CMD=echo +start +""" + result = subprocess.run( + [BASH_EXECUTABLE, "-lc", command], + capture_output=True, + text=True, + encoding="utf-8", + ) + + assert result.returncode == 1, result.stdout + result.stderr + assert "Docker socket not found" in result.stdout + + +def _setup_deploy_worktree(tmp_path: Path) -> Path: + worktree = tmp_path / "repo" + shutil.copytree(REPO_ROOT / "scripts", worktree / "scripts") + shutil.copytree(REPO_ROOT / "docker", worktree / "docker") + (worktree / "backend").mkdir() + (worktree / "config.yaml").write_text( + "sandbox:\n use: deerflow.community.aio_sandbox:AioSandboxProvider\n", + encoding="utf-8", + ) + (worktree / "extensions_config.json").write_text("{}\n", encoding="utf-8") + (worktree / ".env").write_text("TEST=1\n", encoding="utf-8") + return worktree + + +def test_aio_deploy_socket_preflight_allows_windows_when_docker_reachable(tmp_path): + """deploy.sh on Windows Git Bash proceeds past preflight when Docker daemon is reachable.""" + worktree = _setup_deploy_worktree(tmp_path) + bin_dir = tmp_path / "bin" + bin_dir.mkdir() + capture_args = tmp_path / "docker_args.txt" + docker = bin_dir / "docker" + docker.write_text( + f'#!/usr/bin/env sh\nif [ "$1" = "info" ]; then exit 0; fi\nfor arg in "$@"; do printf "%s\\n" "$arg"; done > "{capture_args}"\nexit 0\n', + encoding="utf-8", + ) + docker.chmod(0o755) + + bash_env = tmp_path / "env.sh" + bash_env.write_text("uname() { echo 'MINGW64_NT-10.0'; }\n", encoding="utf-8") + + env = os.environ.copy() + env["PATH"] = f"{bin_dir}{os.pathsep}{env['PATH']}" + env["BASH_ENV"] = str(bash_env) + env["DEER_FLOW_DOCKER_SOCKET"] = "/var/run/docker.sock" + env["BETTER_AUTH_SECRET"] = "test-secret" + env["DEER_FLOW_INTERNAL_AUTH_TOKEN"] = "test-token" + env["UV_EXTRAS"] = "redis" + + result = subprocess.run( + [BASH_EXECUTABLE, str(worktree / "scripts" / "deploy.sh"), "start"], + cwd=worktree, + env=env, + capture_output=True, + text=True, + ) + + assert result.returncode == 0, result.stdout + result.stderr + assert "docker-compose.dood.yaml" in capture_args.read_text(encoding="utf-8") + + +def test_aio_deploy_socket_preflight_rejects_missing_socket_on_posix(tmp_path): + """deploy.sh on POSIX hosts without a physical socket file fails fast.""" + worktree = _setup_deploy_worktree(tmp_path) + bash_env = tmp_path / "env.sh" + bash_env.write_text("uname() { echo 'Linux'; }\n", encoding="utf-8") + + env = os.environ.copy() + env["BASH_ENV"] = str(bash_env) + env["DEER_FLOW_DOCKER_SOCKET"] = "/nonexistent/docker.sock" + env["BETTER_AUTH_SECRET"] = "test-secret" + env["DEER_FLOW_INTERNAL_AUTH_TOKEN"] = "test-token" + env["UV_EXTRAS"] = "redis" + + result = subprocess.run( + [BASH_EXECUTABLE, str(worktree / "scripts" / "deploy.sh"), "start"], + cwd=worktree, + env=env, + capture_output=True, + text=True, + ) + + assert result.returncode == 1, result.stdout + result.stderr + assert "Docker socket not found" in result.stdout + result.stderr + + +def test_aio_deploy_socket_preflight_rejects_missing_custom_socket_on_windows(tmp_path): + """deploy.sh on Windows Git Bash fails if a custom non-existent socket path is specified.""" + worktree = _setup_deploy_worktree(tmp_path) + bin_dir = tmp_path / "bin" + bin_dir.mkdir() + docker = bin_dir / "docker" + docker.write_text( + '#!/usr/bin/env sh\nif [ "$1" = "info" ]; then exit 0; fi\nexit 0\n', + encoding="utf-8", + ) + docker.chmod(0o755) + + bash_env = tmp_path / "env.sh" + bash_env.write_text("uname() { echo 'MINGW64_NT-10.0'; }\n", encoding="utf-8") + + env = os.environ.copy() + env["PATH"] = f"{bin_dir}{os.pathsep}{env['PATH']}" + env["BASH_ENV"] = str(bash_env) + env["DEER_FLOW_DOCKER_SOCKET"] = "/nonexistent/docker.sock" + env["BETTER_AUTH_SECRET"] = "test-secret" + env["DEER_FLOW_INTERNAL_AUTH_TOKEN"] = "test-token" + env["UV_EXTRAS"] = "redis" + + result = subprocess.run( + [BASH_EXECUTABLE, str(worktree / "scripts" / "deploy.sh"), "start"], + cwd=worktree, + env=env, + capture_output=True, + text=True, + ) + + assert result.returncode == 1, result.stdout + result.stderr + assert "Docker socket not found" in result.stdout + result.stderr + + +@pytest.mark.skipif( + Path("/var/run/docker.sock").is_socket(), + reason="Host has real /var/run/docker.sock", +) +def test_aio_deploy_socket_preflight_rejects_windows_when_docker_unreachable(tmp_path): + """deploy.sh on Windows Git Bash fails if Docker daemon is not reachable.""" + worktree = _setup_deploy_worktree(tmp_path) + bin_dir = tmp_path / "bin" + bin_dir.mkdir() + docker = bin_dir / "docker" + docker.write_text('#!/usr/bin/env sh\nif [ "$1" = "info" ]; then exit 1; fi\nexit 0\n', encoding="utf-8") + docker.chmod(0o755) + + bash_env = tmp_path / "env.sh" + bash_env.write_text("uname() { echo 'MINGW64_NT-10.0'; }\n", encoding="utf-8") + + env = os.environ.copy() + env["PATH"] = f"{bin_dir}{os.pathsep}{env['PATH']}" + env["BASH_ENV"] = str(bash_env) + env["DEER_FLOW_DOCKER_SOCKET"] = "/var/run/docker.sock" + env["BETTER_AUTH_SECRET"] = "test-secret" + env["DEER_FLOW_INTERNAL_AUTH_TOKEN"] = "test-token" + env["UV_EXTRAS"] = "redis" + + result = subprocess.run( + [BASH_EXECUTABLE, str(worktree / "scripts" / "deploy.sh"), "start"], + cwd=worktree, + env=env, + capture_output=True, + text=True, + ) + + assert result.returncode == 1, result.stdout + result.stderr + assert "Docker socket not found" in result.stdout + result.stderr diff --git a/scripts/deploy.sh b/scripts/deploy.sh index 4c3cbd317..aa1ae9b1b 100755 --- a/scripts/deploy.sh +++ b/scripts/deploy.sh @@ -373,9 +373,15 @@ fi if [ "$sandbox_mode" = "aio" ]; then if [ ! -S "$DEER_FLOW_DOCKER_SOCKET" ]; then - echo -e "${RED}⚠ Docker socket not found at $DEER_FLOW_DOCKER_SOCKET${NC}" - echo " AioSandboxProvider (DooD) will not work." - exit 1 + # On Windows (Git Bash / MSYS), Docker Desktop mounts the default + # /var/run/docker.sock into containers even though no host socket file exists. + if [ "$DEER_FLOW_DOCKER_SOCKET" = "/var/run/docker.sock" ] && [[ "$(uname -s)" =~ ^(MINGW|MSYS|CYGWIN) ]] && docker info >/dev/null 2>&1; then + : + else + echo -e "${RED}⚠ Docker socket not found at $DEER_FLOW_DOCKER_SOCKET${NC}" + echo " AioSandboxProvider (DooD) will not work." + exit 1 + fi fi echo -e "${GREEN}✓ Docker socket: $DEER_FLOW_DOCKER_SOCKET${NC}" echo -e "${YELLOW} Mounting host Docker socket into gateway (DooD = host root-equivalent). See SECURITY.md.${NC}" diff --git a/scripts/docker.sh b/scripts/docker.sh index 405bceb7e..29114267b 100755 --- a/scripts/docker.sh +++ b/scripts/docker.sh @@ -335,8 +335,14 @@ start() { if [ "$sandbox_mode" = "aio" ]; then local docker_socket="${DEER_FLOW_DOCKER_SOCKET:-/var/run/docker.sock}" if [ ! -S "$docker_socket" ]; then - echo -e "${YELLOW}⚠ Docker socket not found at $docker_socket — AioSandboxProvider (DooD) will not work.${NC}" - exit 1 + # On Windows (Git Bash / MSYS), Docker Desktop mounts the default + # /var/run/docker.sock into containers even though no host socket file exists. + if [ "$docker_socket" = "/var/run/docker.sock" ] && [[ "$(uname -s)" =~ ^(MINGW|MSYS|CYGWIN) ]] && docker info >/dev/null 2>&1; then + : + else + echo -e "${YELLOW}⚠ Docker socket not found at $docker_socket — AioSandboxProvider (DooD) will not work.${NC}" + exit 1 + fi fi echo -e "${YELLOW}Mounting host Docker socket into gateway (DooD = host root-equivalent). See SECURITY.md.${NC}" COMPOSE_CMD="$COMPOSE_CMD -f docker-compose.dood.yaml"