From 6938ca5a33d25406a9076f2bd2642a11915dea17 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 18 Jun 2025 11:09:11 +0000 Subject: [PATCH] fix: Docker validation in container environments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add CREWAI_SKIP_DOCKER_VALIDATION environment variable - Detect container environments and skip Docker validation - Improve error messages with alternative solutions - Add comprehensive tests for Docker validation scenarios - Maintain backward compatibility Fixes #3028 Co-Authored-By: João --- src/crewai/agent.py | 61 +++++++++++- tests/test_docker_validation.py | 169 ++++++++++++++++++++++++++++++++ 2 files changed, 225 insertions(+), 5 deletions(-) create mode 100644 tests/test_docker_validation.py diff --git a/src/crewai/agent.py b/src/crewai/agent.py index c8e34b2e6..ea765d2cb 100644 --- a/src/crewai/agent.py +++ b/src/crewai/agent.py @@ -1,6 +1,18 @@ +import os import shutil import subprocess -from typing import Any, Callable, Dict, List, Literal, Optional, Sequence, Tuple, Type, Union +from typing import ( + Any, + Callable, + Dict, + List, + Literal, + Optional, + Sequence, + Tuple, + Type, + Union, +) from pydantic import Field, InstanceOf, PrivateAttr, model_validator @@ -157,7 +169,7 @@ class Agent(BaseAgent): ) guardrail: Optional[Union[Callable[[Any], Tuple[bool, Any]], str]] = Field( default=None, - description="Function or string description of a guardrail to validate agent output" + description="Function or string description of a guardrail to validate agent output", ) guardrail_max_retries: int = Field( default=3, description="Maximum number of retries when guardrail fails" @@ -665,10 +677,26 @@ class Agent(BaseAgent): print(f"Warning: Failed to inject date: {str(e)}") def _validate_docker_installation(self) -> None: - """Check if Docker is installed and running.""" + """Check if Docker is installed and running, with container environment support.""" + if os.getenv("CREWAI_SKIP_DOCKER_VALIDATION", "false").lower() == "true": + return + + if self.code_execution_mode == "unsafe": + return + + if self._is_running_in_container(): + if hasattr(self, "_logger"): + self._logger.log( + "warning", + f"Running inside container - skipping Docker validation for agent: {self.role}. " + f"Set CREWAI_SKIP_DOCKER_VALIDATION=true to suppress this warning.", + ) + return + if not shutil.which("docker"): raise RuntimeError( - f"Docker is not installed. Please install Docker to use code execution with agent: {self.role}" + f"Docker is not installed. Please install Docker to use code execution with agent: {self.role}. " + f"Alternatively, set code_execution_mode='unsafe' or CREWAI_SKIP_DOCKER_VALIDATION=true." ) try: @@ -680,9 +708,32 @@ class Agent(BaseAgent): ) except subprocess.CalledProcessError: raise RuntimeError( - f"Docker is not running. Please start Docker to use code execution with agent: {self.role}" + f"Docker is not running. Please start Docker to use code execution with agent: {self.role}. " + f"Alternatively, set code_execution_mode='unsafe' or CREWAI_SKIP_DOCKER_VALIDATION=true." ) + def _is_running_in_container(self) -> bool: + """Detect if the current process is running inside a container.""" + if os.path.exists("/.dockerenv"): + return True + + try: + with open("/proc/1/cgroup", "r") as f: + content = f.read() + if ( + "docker" in content + or "container" in content + or "kubepods" in content + ): + return True + except (FileNotFoundError, PermissionError): + pass + + if os.getpid() == 1: + return True + + return False + def __repr__(self): return f"Agent(role={self.role}, goal={self.goal}, backstory={self.backstory})" diff --git a/tests/test_docker_validation.py b/tests/test_docker_validation.py new file mode 100644 index 000000000..261a68dfb --- /dev/null +++ b/tests/test_docker_validation.py @@ -0,0 +1,169 @@ +"""Test Docker validation functionality in Agent.""" + +import os +import subprocess +from unittest.mock import Mock, patch, mock_open +import pytest +from crewai import Agent + + +class TestDockerValidation: + """Test cases for Docker validation in Agent.""" + + def test_docker_validation_skipped_with_env_var(self): + """Test that Docker validation is skipped when CREWAI_SKIP_DOCKER_VALIDATION=true.""" + with patch.dict(os.environ, {"CREWAI_SKIP_DOCKER_VALIDATION": "true"}): + agent = Agent( + role="Test Agent", + goal="Test goal", + backstory="Test backstory", + allow_code_execution=True, + ) + assert agent.allow_code_execution is True + + def test_docker_validation_skipped_with_unsafe_mode(self): + """Test that Docker validation is skipped when code_execution_mode='unsafe'.""" + agent = Agent( + role="Test Agent", + goal="Test goal", + backstory="Test backstory", + allow_code_execution=True, + code_execution_mode="unsafe", + ) + assert agent.code_execution_mode == "unsafe" + + @patch("crewai.agent.os.path.exists") + def test_docker_validation_skipped_in_container_dockerenv(self, mock_exists): + """Test that Docker validation is skipped when /.dockerenv exists.""" + mock_exists.return_value = True + + agent = Agent( + role="Test Agent", + goal="Test goal", + backstory="Test backstory", + allow_code_execution=True, + ) + assert agent.allow_code_execution is True + + @patch("crewai.agent.os.path.exists") + @patch("builtins.open", new_callable=mock_open, read_data="12:memory:/docker/container123") + def test_docker_validation_skipped_in_container_cgroup(self, mock_file, mock_exists): + """Test that Docker validation is skipped when cgroup indicates container.""" + mock_exists.return_value = False + + agent = Agent( + role="Test Agent", + goal="Test goal", + backstory="Test backstory", + allow_code_execution=True, + ) + assert agent.allow_code_execution is True + + @patch("crewai.agent.os.path.exists") + @patch("crewai.agent.os.getpid") + @patch("builtins.open", side_effect=FileNotFoundError) + def test_docker_validation_skipped_in_container_pid1(self, mock_file, mock_getpid, mock_exists): + """Test that Docker validation is skipped when running as PID 1.""" + mock_exists.return_value = False + mock_getpid.return_value = 1 + + agent = Agent( + role="Test Agent", + goal="Test goal", + backstory="Test backstory", + allow_code_execution=True, + ) + assert agent.allow_code_execution is True + + @patch("crewai.agent.shutil.which") + @patch("crewai.agent.os.path.exists") + @patch("crewai.agent.os.getpid") + @patch("builtins.open", side_effect=FileNotFoundError) + def test_docker_validation_fails_no_docker(self, mock_file, mock_getpid, mock_exists, mock_which): + """Test that Docker validation fails when Docker is not installed.""" + mock_exists.return_value = False + mock_getpid.return_value = 1000 + mock_which.return_value = None + + with pytest.raises(RuntimeError, match="Docker is not installed"): + Agent( + role="Test Agent", + goal="Test goal", + backstory="Test backstory", + allow_code_execution=True, + ) + + @patch("crewai.agent.shutil.which") + @patch("crewai.agent.subprocess.run") + @patch("crewai.agent.os.path.exists") + @patch("crewai.agent.os.getpid") + @patch("builtins.open", side_effect=FileNotFoundError) + def test_docker_validation_fails_docker_not_running(self, mock_file, mock_getpid, mock_exists, mock_run, mock_which): + """Test that Docker validation fails when Docker daemon is not running.""" + mock_exists.return_value = False + mock_getpid.return_value = 1000 + mock_which.return_value = "/usr/bin/docker" + mock_run.side_effect = subprocess.CalledProcessError(1, "docker info") + + with pytest.raises(RuntimeError, match="Docker is not running"): + Agent( + role="Test Agent", + goal="Test goal", + backstory="Test backstory", + allow_code_execution=True, + ) + + @patch("crewai.agent.shutil.which") + @patch("crewai.agent.subprocess.run") + @patch("crewai.agent.os.path.exists") + @patch("crewai.agent.os.getpid") + @patch("builtins.open", side_effect=FileNotFoundError) + def test_docker_validation_passes_docker_available(self, mock_file, mock_getpid, mock_exists, mock_run, mock_which): + """Test that Docker validation passes when Docker is available.""" + mock_exists.return_value = False + mock_getpid.return_value = 1000 + mock_which.return_value = "/usr/bin/docker" + mock_run.return_value = Mock(returncode=0) + + agent = Agent( + role="Test Agent", + goal="Test goal", + backstory="Test backstory", + allow_code_execution=True, + ) + assert agent.allow_code_execution is True + + def test_container_detection_methods(self): + """Test the container detection logic directly.""" + agent = Agent( + role="Test Agent", + goal="Test goal", + backstory="Test backstory", + ) + + with patch("crewai.agent.os.path.exists", return_value=True): + assert agent._is_running_in_container() is True + + with patch("crewai.agent.os.path.exists", return_value=False), \ + patch("builtins.open", mock_open(read_data="docker")): + assert agent._is_running_in_container() is True + + with patch("crewai.agent.os.path.exists", return_value=False), \ + patch("builtins.open", side_effect=FileNotFoundError), \ + patch("crewai.agent.os.getpid", return_value=1): + assert agent._is_running_in_container() is True + + def test_reproduce_original_issue(self): + """Test that reproduces the original issue from GitHub issue #3028.""" + with patch("crewai.agent.os.path.exists", return_value=True): + agent = Agent( + role="Knowledge Pattern Synthesizer", + goal="Synthesize knowledge patterns", + backstory="You're an expert at synthesizing knowledge patterns.", + allow_code_execution=True, + verbose=True, + memory=True, + max_retry_limit=3 + ) + assert agent.allow_code_execution is True + assert agent.role == "Knowledge Pattern Synthesizer"