Compare commits

...

4 Commits

Author SHA1 Message Date
Devin AI
3129e7a4bc fix: Update I18N mocking strategy to use constructor mock
- Replace @patch('load_prompts') with @patch('I18N') decorator
- Mock I18N constructor to return MagicMock instance
- Prevent 'Prompt file None not found' errors during Agent instantiation
- Follow same mocking pattern as other tests in codebase

Co-Authored-By: João <joao@crewai.com>
2025-06-18 11:37:24 +00:00
Devin AI
469ddea415 fix: Update I18N mocking strategy for Docker validation tests
- Replace @patch decorator with module-level load_prompts mocking
- Prevent 'Prompt file None not found' errors during Agent instantiation
- Ensure tests are isolated and don't require external prompt files

Co-Authored-By: João <joao@crewai.com>
2025-06-18 11:28:45 +00:00
Devin AI
968d0a0e2c fix: Add proper I18N mocking to Docker validation tests
- Mock I18N initialization to prevent 'Prompt file None not found' errors
- Follow existing test patterns for Agent dependency mocking
- Ensure tests are isolated and don't require external files

Co-Authored-By: João <joao@crewai.com>
2025-06-18 11:17:09 +00:00
Devin AI
6938ca5a33 fix: Docker validation in container environments
- 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 <joao@crewai.com>
2025-06-18 11:09:11 +00:00
2 changed files with 237 additions and 5 deletions

View File

@@ -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})"

View File

@@ -0,0 +1,181 @@
"""Test Docker validation functionality in Agent."""
import os
import subprocess
from unittest.mock import Mock, patch, mock_open, MagicMock
import pytest
from crewai import Agent
@patch('crewai.utilities.i18n.I18N')
class TestDockerValidation:
"""Test cases for Docker validation in Agent."""
def test_docker_validation_skipped_with_env_var(self, mock_i18n):
"""Test that Docker validation is skipped when CREWAI_SKIP_DOCKER_VALIDATION=true."""
mock_i18n.return_value = MagicMock()
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, mock_i18n):
"""Test that Docker validation is skipped when code_execution_mode='unsafe'."""
mock_i18n.return_value = MagicMock()
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, mock_i18n):
"""Test that Docker validation is skipped when /.dockerenv exists."""
mock_exists.return_value = True
mock_i18n.return_value = MagicMock()
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, mock_i18n):
"""Test that Docker validation is skipped when cgroup indicates container."""
mock_exists.return_value = False
mock_i18n.return_value = MagicMock()
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, mock_i18n):
"""Test that Docker validation is skipped when running as PID 1."""
mock_exists.return_value = False
mock_getpid.return_value = 1
mock_i18n.return_value = MagicMock()
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, mock_i18n):
"""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
mock_i18n.return_value = MagicMock()
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, mock_i18n):
"""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")
mock_i18n.return_value = MagicMock()
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, mock_i18n):
"""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)
mock_i18n.return_value = MagicMock()
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, mock_i18n):
"""Test the container detection logic directly."""
mock_i18n.return_value = MagicMock()
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, mock_i18n):
"""Test that reproduces the original issue from GitHub issue #3028."""
mock_i18n.return_value = MagicMock()
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"