Fahad
2025-06-11 07:09:28 +04:00
parent ede44cc1d2
commit 14ccbede43
6 changed files with 42 additions and 17 deletions

View File

@@ -31,7 +31,9 @@ services:
environment: environment:
- GEMINI_API_KEY=${GEMINI_API_KEY} - GEMINI_API_KEY=${GEMINI_API_KEY}
- REDIS_URL=redis://redis:6379/0 - REDIS_URL=redis://redis:6379/0
- WORKSPACE_ROOT=${WORKSPACE_ROOT:-/workspace} # Use HOME not PWD: Claude needs access to any absolute file path, not just current project,
# and Claude Code could be running from multiple locations at the same time
- WORKSPACE_ROOT=${WORKSPACE_ROOT:-${HOME}}
volumes: volumes:
- ${HOME:-/tmp}:/workspace:ro - ${HOME:-/tmp}:/workspace:ro
stdin_open: true stdin_open: true

View File

@@ -37,8 +37,12 @@ GEMINI_API_KEY=$API_KEY_VALUE
# Redis configuration (automatically set for Docker Compose) # Redis configuration (automatically set for Docker Compose)
REDIS_URL=redis://redis:6379/0 REDIS_URL=redis://redis:6379/0
# Workspace root (automatically set for Docker Compose) # Workspace root - host path that maps to /workspace in container
WORKSPACE_ROOT=/workspace # This should be the host directory path that contains all files Claude might reference
# We use $HOME (not $PWD) because Claude needs access to ANY absolute file path,
# not just files within the current project directory. Additionally, Claude Code
# could be running from multiple locations at the same time.
WORKSPACE_ROOT=$HOME
EOF EOF
echo "✅ Created .env file with Redis configuration" echo "✅ Created .env file with Redis configuration"
echo "" echo ""

View File

@@ -235,9 +235,9 @@ class TestCollaborationWorkflow:
) )
response = json.loads(result[0].text) response = json.loads(result[0].text)
assert ( assert response["status"] == "requires_clarification", (
response["status"] == "requires_clarification" "Should request clarification when asked about dependencies without package files"
), "Should request clarification when asked about dependencies without package files" )
clarification = json.loads(response["content"]) clarification = json.loads(response["content"])
assert "package.json" in str(clarification["files_needed"]), "Should specifically request package.json" assert "package.json" in str(clarification["files_needed"]), "Should specifically request package.json"

View File

@@ -300,7 +300,7 @@ class TestConversationFlow:
# REQUEST 6: Try to exceed MAX_CONVERSATION_TURNS limit - should fail # REQUEST 6: Try to exceed MAX_CONVERSATION_TURNS limit - should fail
turns_at_limit = [ turns_at_limit = [
ConversationTurn( ConversationTurn(
role="assistant" if i % 2 == 0 else "user", content=f"Turn {i+1}", timestamp="2023-01-01T00:00:30Z" role="assistant" if i % 2 == 0 else "user", content=f"Turn {i + 1}", timestamp="2023-01-01T00:00:30Z"
) )
for i in range(MAX_CONVERSATION_TURNS) for i in range(MAX_CONVERSATION_TURNS)
] ]
@@ -423,7 +423,9 @@ class TestConversationFlow:
# Mock context with current turns # Mock context with current turns
turns = [ turns = [
ConversationTurn( ConversationTurn(
role="user" if i % 2 == 0 else "assistant", content=f"Turn {i+1}", timestamp="2023-01-01T00:00:00Z" role="user" if i % 2 == 0 else "assistant",
content=f"Turn {i + 1}",
timestamp="2023-01-01T00:00:00Z",
) )
for i in range(turn_num) for i in range(turn_num)
] ]
@@ -445,7 +447,7 @@ class TestConversationFlow:
# Now we should be at the limit - create final context # Now we should be at the limit - create final context
final_turns = [ final_turns = [
ConversationTurn( ConversationTurn(
role="user" if i % 2 == 0 else "assistant", content=f"Turn {i+1}", timestamp="2023-01-01T00:00:00Z" role="user" if i % 2 == 0 else "assistant", content=f"Turn {i + 1}", timestamp="2023-01-01T00:00:00Z"
) )
for i in range(MAX_CONVERSATION_TURNS) for i in range(MAX_CONVERSATION_TURNS)
] ]

View File

@@ -32,9 +32,9 @@ class TestThinkingModes:
] ]
for tool, expected_default in tools: for tool, expected_default in tools:
assert ( assert tool.get_default_thinking_mode() == expected_default, (
tool.get_default_thinking_mode() == expected_default f"{tool.__class__.__name__} should default to {expected_default}"
), f"{tool.__class__.__name__} should default to {expected_default}" )
@pytest.mark.asyncio @pytest.mark.asyncio
@patch("tools.base.BaseTool.create_model") @patch("tools.base.BaseTool.create_model")

View File

@@ -28,8 +28,9 @@ from .token_utils import MAX_CONTEXT_TOKENS, estimate_tokens
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# Get workspace root for Docker path translation # Get workspace root for Docker path translation
# When running in Docker with a mounted workspace, WORKSPACE_ROOT contains # IMPORTANT: WORKSPACE_ROOT should contain the HOST path (e.g., /Users/john/project)
# the host path that corresponds to /workspace in the container # that gets mounted to /workspace in the Docker container. This enables proper
# path translation between host absolute paths and container workspace paths.
WORKSPACE_ROOT = os.environ.get("WORKSPACE_ROOT") WORKSPACE_ROOT = os.environ.get("WORKSPACE_ROOT")
CONTAINER_WORKSPACE = Path("/workspace") CONTAINER_WORKSPACE = Path("/workspace")
@@ -43,6 +44,7 @@ DANGEROUS_WORKSPACE_PATHS = {
"/var", "/var",
"/root", "/root",
"/home", "/home",
"/workspace", # Container path - WORKSPACE_ROOT should be host path
"C:\\", "C:\\",
"C:\\Windows", "C:\\Windows",
"C:\\Program Files", "C:\\Program Files",
@@ -54,7 +56,17 @@ if WORKSPACE_ROOT:
# Resolve to canonical path for comparison # Resolve to canonical path for comparison
resolved_workspace = Path(WORKSPACE_ROOT).resolve() resolved_workspace = Path(WORKSPACE_ROOT).resolve()
# Check against dangerous paths # Special check for /workspace - common configuration mistake
if str(resolved_workspace) == "/workspace":
raise RuntimeError(
f"Configuration Error: WORKSPACE_ROOT should be set to the HOST path, not the container path. "
f"Found: WORKSPACE_ROOT={WORKSPACE_ROOT} "
f"Expected: WORKSPACE_ROOT should be set to your host directory path (e.g., $HOME) "
f"that contains all files Claude might reference. "
f"This path gets mounted to /workspace inside the Docker container."
)
# Check against other dangerous paths
if str(resolved_workspace) in DANGEROUS_WORKSPACE_PATHS: if str(resolved_workspace) in DANGEROUS_WORKSPACE_PATHS:
raise RuntimeError( raise RuntimeError(
f"Security Error: WORKSPACE_ROOT '{WORKSPACE_ROOT}' is set to a dangerous system directory. " f"Security Error: WORKSPACE_ROOT '{WORKSPACE_ROOT}' is set to a dangerous system directory. "
@@ -181,12 +193,17 @@ def translate_path_for_environment(path_str: str) -> str:
This is the unified path translation function that should be used by all This is the unified path translation function that should be used by all
tools and utilities throughout the codebase. It handles: tools and utilities throughout the codebase. It handles:
1. Docker host-to-container path translation 1. Docker host-to-container path translation (host paths -> /workspace/...)
2. Direct mode (no translation needed) 2. Direct mode (no translation needed)
3. Security validation and error handling 3. Security validation and error handling
Docker Path Translation Logic:
- Input: /Users/john/project/src/file.py (host path from Claude)
- WORKSPACE_ROOT: /Users/john/project (host path in env var)
- Output: /workspace/src/file.py (container path for file operations)
Args: Args:
path_str: Original path string from the client path_str: Original path string from the client (absolute host path)
Returns: Returns:
Translated path appropriate for the current environment Translated path appropriate for the current environment