From f66cb8e376c5e36a4b6353d9182ae3904be1e70b Mon Sep 17 00:00:00 2001 From: Dan Caldr <22105838+dcaldr@users.noreply.github.com> Date: Mon, 14 Sep 2026 00:51:27 +0200 Subject: [PATCH] fix(deploy): prevent Git Bash path conversion of default docker socket on Windows (#5400) (#5402) * fix(deploy): prevent Git Bash path conversion of default docker socket on Windows (#5400) * fix(deploy): resolve docker socket from dotenv and assert custom socket preservation * docs: trim backend guidance to satisfy inherited size budget --------- Co-authored-by: Willem Jiang --- .../test_docker_sandbox_mode_detection.py | 148 ++++++++++++++++++ scripts/deploy.sh | 20 ++- 2 files changed, 161 insertions(+), 7 deletions(-) diff --git a/backend/tests/test_docker_sandbox_mode_detection.py b/backend/tests/test_docker_sandbox_mode_detection.py index f5e2eeb55..c7ad193c5 100644 --- a/backend/tests/test_docker_sandbox_mode_detection.py +++ b/backend/tests/test_docker_sandbox_mode_detection.py @@ -610,3 +610,151 @@ def test_aio_deploy_socket_preflight_rejects_windows_when_docker_unreachable(tmp assert result.returncode == 1, result.stdout + result.stderr assert "Docker socket not found" in result.stdout + result.stderr + + +def test_aio_deploy_socket_unsets_default_on_windows(tmp_path): + """deploy.sh on Windows Git Bash unsets default /var/run/docker.sock before calling Compose.""" + worktree = _setup_deploy_worktree(tmp_path) + bin_dir = tmp_path / "bin" + bin_dir.mkdir() + capture_socket_env = tmp_path / "docker_socket_env.txt" + docker = bin_dir / "docker" + docker.write_text( + f'#!/usr/bin/env sh\nif [ "$1" = "info" ]; then exit 0; fi\nprintf "%s" "$DEER_FLOW_DOCKER_SOCKET" > "{capture_socket_env}"\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 capture_socket_env.read_text(encoding="utf-8") == "" + + +def test_aio_deploy_socket_preserves_unset_default_on_windows(tmp_path): + """deploy.sh does not export default /var/run/docker.sock when unset initially.""" + worktree = _setup_deploy_worktree(tmp_path) + bin_dir = tmp_path / "bin" + bin_dir.mkdir() + capture_socket_env = tmp_path / "docker_socket_env.txt" + docker = bin_dir / "docker" + docker.write_text( + f'#!/usr/bin/env sh\nif [ "$1" = "info" ]; then exit 0; fi\nprintf "%s" "$DEER_FLOW_DOCKER_SOCKET" > "{capture_socket_env}"\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.pop("DEER_FLOW_DOCKER_SOCKET", None) + 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 capture_socket_env.read_text(encoding="utf-8") == "" + + +def test_aio_deploy_socket_preserves_custom_socket_on_windows(tmp_path): + """deploy.sh preserves and exports custom DEER_FLOW_DOCKER_SOCKET on Windows.""" + worktree = _setup_deploy_worktree(tmp_path) + bin_dir = tmp_path / "bin" + bin_dir.mkdir() + capture_socket_env = tmp_path / "docker_socket_env.txt" + docker = bin_dir / "docker" + docker.write_text( + f'#!/usr/bin/env sh\nif [ "$1" = "info" ]; then exit 0; fi\nprintf "%s" "$DEER_FLOW_DOCKER_SOCKET" > "{capture_socket_env}"\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[() {\n if [[ "$1" == "!" && "$2" == "-S" ]]; then\n return 1\n fi\n builtin [ "$@"\n}\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"] = "/custom/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 capture_socket_env.read_text(encoding="utf-8") == "/custom/docker.sock" + + +def test_aio_deploy_socket_reads_from_dotenv(tmp_path): + """deploy.sh resolves DEER_FLOW_DOCKER_SOCKET from .env when unset in shell environment.""" + worktree = _setup_deploy_worktree(tmp_path) + (worktree / ".env").write_text("DEER_FLOW_DOCKER_SOCKET=/var/run/docker.sock\n", encoding="utf-8") + bin_dir = tmp_path / "bin" + bin_dir.mkdir() + capture_socket_env = tmp_path / "docker_socket_env.txt" + docker = bin_dir / "docker" + docker.write_text( + f'#!/usr/bin/env sh\nif [ "$1" = "info" ]; then exit 0; fi\nprintf "%s" "$DEER_FLOW_DOCKER_SOCKET" > "{capture_socket_env}"\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.pop("DEER_FLOW_DOCKER_SOCKET", None) + 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 capture_socket_env.read_text(encoding="utf-8") == "" diff --git a/scripts/deploy.sh b/scripts/deploy.sh index aa1ae9b1b..dbc168b4c 100755 --- a/scripts/deploy.sh +++ b/scripts/deploy.sh @@ -367,23 +367,29 @@ fi # appended here, so the default (local) and provisioner modes never expose the # host daemon. Mounting the socket = root-equivalent host control; see SECURITY.md. -if [ -z "$DEER_FLOW_DOCKER_SOCKET" ]; then - export DEER_FLOW_DOCKER_SOCKET="/var/run/docker.sock" -fi +docker_socket="$(read_dotenv_value DEER_FLOW_DOCKER_SOCKET)" +docker_socket="${docker_socket:-/var/run/docker.sock}" if [ "$sandbox_mode" = "aio" ]; then - if [ ! -S "$DEER_FLOW_DOCKER_SOCKET" ]; then + if [ ! -S "$docker_socket" ]; then # 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 + if [ "$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 -e "${RED}⚠ Docker socket not found at $docker_socket${NC}" echo " AioSandboxProvider (DooD) will not work." exit 1 fi fi - echo -e "${GREEN}✓ Docker socket: $DEER_FLOW_DOCKER_SOCKET${NC}" + # On Windows (Git Bash / MSYS), exporting /var/run/docker.sock causes MSYS to + # convert it to C:\Program Files\Git\var\run\docker.sock when invoking native + # docker compose, triggering mkdir errors. Unsetting the default allows Compose + # to evaluate its own default literal fallback (${DEER_FLOW_DOCKER_SOCKET:-/var/run/docker.sock}). + if [[ "$(uname -s)" =~ ^(MINGW|MSYS|CYGWIN) ]] && [ "$DEER_FLOW_DOCKER_SOCKET" = "/var/run/docker.sock" ]; then + unset DEER_FLOW_DOCKER_SOCKET + fi + echo -e "${GREEN}✓ Docker socket: $docker_socket${NC}" echo -e "${YELLOW} Mounting host Docker socket into gateway (DooD = host root-equivalent). See SECURITY.md.${NC}" COMPOSE_CMD+=(-f "$DOCKER_DIR/docker-compose.dood.yaml") fi