Simplified /workspace to map to a project scoped WORKSPACE_ROOT

This commit is contained in:
Fahad
2025-06-13 20:49:37 +04:00
parent ebf5cfaa9e
commit 26b22a1d53
7 changed files with 140 additions and 156 deletions

View File

@@ -64,7 +64,3 @@ DEFAULT_THINKING_MODE_THINKDEEP=high
# ERROR: Shows only errors # ERROR: Shows only errors
LOG_LEVEL=DEBUG LOG_LEVEL=DEBUG
# Optional: Project root override for file sandboxing
# If set, overrides the default sandbox directory
# Use with caution - this controls which files the server can access
# MCP_PROJECT_ROOT=/path/to/specific/project

View File

@@ -1,4 +1,3 @@
#!/usr/bin/env python3
""" """
Communication Simulator Test for Zen MCP Server Communication Simulator Test for Zen MCP Server
@@ -13,20 +12,28 @@ Test Flow:
4. Cleanup and report results 4. Cleanup and report results
Usage: Usage:
python communication_simulator_test.py [--verbose] [--keep-logs] [--tests TEST_NAME...] [--individual TEST_NAME] [--skip-docker] python communication_simulator_test.py [--verbose] [--keep-logs] [--tests TEST_NAME...] [--individual TEST_NAME] [--rebuild]
--tests: Run specific tests only (space-separated) --tests: Run specific tests only (space-separated)
--list-tests: List all available tests --list-tests: List all available tests
--individual: Run a single test individually --individual: Run a single test individually
--skip-docker: Skip Docker setup (assumes containers are already running) --rebuild: Force rebuild Docker environment using setup-docker.sh
Available tests: Available tests:
basic_conversation - Basic conversation flow with chat tool basic_conversation - Basic conversation flow with chat tool
content_validation - Content validation and duplicate detection
per_tool_deduplication - File deduplication for individual tools per_tool_deduplication - File deduplication for individual tools
cross_tool_continuation - Cross-tool conversation continuation scenarios cross_tool_continuation - Cross-tool conversation continuation scenarios
content_validation - Content validation and duplicate detection cross_tool_comprehensive - Comprehensive cross-tool integration testing
logs_validation - Docker logs validation logs_validation - Docker logs validation
redis_validation - Redis conversation memory validation redis_validation - Redis conversation memory validation
model_thinking_config - Model thinking configuration testing
o3_model_selection - O3 model selection and routing testing
ollama_custom_url - Ollama custom URL configuration testing
openrouter_fallback - OpenRouter fallback mechanism testing
openrouter_models - OpenRouter models availability testing
token_allocation_validation - Token allocation and limits validation
conversation_chain_validation - Conversation chain continuity validation
Examples: Examples:
# Run all tests # Run all tests
@@ -38,8 +45,8 @@ Examples:
# Run a single test individually (with full Docker setup) # Run a single test individually (with full Docker setup)
python communication_simulator_test.py --individual content_validation python communication_simulator_test.py --individual content_validation
# Run a single test individually (assuming Docker is already running) # Force rebuild Docker environment before running tests
python communication_simulator_test.py --individual content_validation --skip-docker python communication_simulator_test.py --rebuild
# List available tests # List available tests
python communication_simulator_test.py --list-tests python communication_simulator_test.py --list-tests
@@ -52,16 +59,18 @@ import shutil
import subprocess import subprocess
import sys import sys
import tempfile import tempfile
import time
class CommunicationSimulator: class CommunicationSimulator:
"""Simulates real-world Claude CLI communication with MCP Gemini server""" """Simulates real-world Claude CLI communication with MCP Gemini server"""
def __init__(self, verbose: bool = False, keep_logs: bool = False, selected_tests: list[str] = None): def __init__(
self, verbose: bool = False, keep_logs: bool = False, selected_tests: list[str] = None, rebuild: bool = False
):
self.verbose = verbose self.verbose = verbose
self.keep_logs = keep_logs self.keep_logs = keep_logs
self.selected_tests = selected_tests or [] self.selected_tests = selected_tests or []
self.rebuild = rebuild
self.temp_dir = None self.temp_dir = None
self.container_name = "zen-mcp-server" self.container_name = "zen-mcp-server"
self.redis_container = "zen-mcp-redis" self.redis_container = "zen-mcp-redis"
@@ -98,7 +107,7 @@ class CommunicationSimulator:
return run_test return run_test
def setup_test_environment(self) -> bool: def setup_test_environment(self) -> bool:
"""Setup fresh Docker environment""" """Setup test environment"""
try: try:
self.logger.info("Setting up test environment...") self.logger.info("Setting up test environment...")
@@ -106,72 +115,71 @@ class CommunicationSimulator:
self.temp_dir = tempfile.mkdtemp(prefix="mcp_test_") self.temp_dir = tempfile.mkdtemp(prefix="mcp_test_")
self.logger.debug(f"Created temp directory: {self.temp_dir}") self.logger.debug(f"Created temp directory: {self.temp_dir}")
# Setup Docker environment # Only run setup-docker.sh if rebuild is requested
return self._setup_docker() if self.rebuild:
if not self._run_setup_docker():
return False
# Always verify containers are running (regardless of rebuild)
return self._verify_existing_containers()
except Exception as e: except Exception as e:
self.logger.error(f"Failed to setup test environment: {e}") self.logger.error(f"Failed to setup test environment: {e}")
return False return False
def _setup_docker(self) -> bool: def _run_setup_docker(self) -> bool:
"""Setup fresh Docker environment""" """Run the setup-docker.sh script"""
try: try:
self.logger.info("Setting up Docker environment...") self.logger.info("Running setup-docker.sh...")
# Stop and remove existing containers # Check if setup-docker.sh exists
self._run_command(["docker", "compose", "down", "--remove-orphans"], check=False, capture_output=True) setup_script = "./setup-docker.sh"
if not os.path.exists(setup_script):
self.logger.error(f"setup-docker.sh not found at {setup_script}")
return False
# Clean up any old containers/images # Make sure it's executable
old_containers = [self.container_name, self.redis_container] result = self._run_command(["chmod", "+x", setup_script], capture_output=True)
for container in old_containers:
self._run_command(["docker", "stop", container], check=False, capture_output=True)
self._run_command(["docker", "rm", container], check=False, capture_output=True)
# Build and start services
self.logger.info("Building Docker images...")
result = self._run_command(["docker", "compose", "build", "--no-cache"], capture_output=True)
if result.returncode != 0: if result.returncode != 0:
self.logger.error(f"Docker build failed: {result.stderr}") self.logger.error(f"Failed to make setup-docker.sh executable: {result.stderr}")
return False return False
self.logger.info("Starting Docker services...") # Run the setup script
result = self._run_command(["docker", "compose", "up", "-d"], capture_output=True) result = self._run_command([setup_script], capture_output=True)
if result.returncode != 0: if result.returncode != 0:
self.logger.error(f"Docker startup failed: {result.stderr}") self.logger.error(f"setup-docker.sh failed: {result.stderr}")
return False return False
# Wait for services to be ready self.logger.info("setup-docker.sh completed successfully")
self.logger.info("Waiting for services to be ready...")
time.sleep(10) # Give services time to initialize
# Verify containers are running
if not self._verify_containers():
return False
self.logger.info("Docker environment ready")
return True return True
except Exception as e: except Exception as e:
self.logger.error(f"Docker setup failed: {e}") self.logger.error(f"Failed to run setup-docker.sh: {e}")
return False return False
def _verify_containers(self) -> bool: def _verify_existing_containers(self) -> bool:
"""Verify that required containers are running""" """Verify that required containers are already running (no setup)"""
try: try:
self.logger.info("Verifying existing Docker containers...")
result = self._run_command(["docker", "ps", "--format", "{{.Names}}"], capture_output=True) result = self._run_command(["docker", "ps", "--format", "{{.Names}}"], capture_output=True)
running_containers = result.stdout.decode().strip().split("\\n") running_containers = result.stdout.decode().strip().split("\n")
required = [self.container_name, self.redis_container] required = [self.container_name, self.redis_container]
for container in required: for container in required:
if container not in running_containers: if container not in running_containers:
self.logger.error(f"Container not running: {container}") self.logger.error(f"Required container not running: {container}")
self.logger.error(
"Please start Docker containers first, or use --rebuild to set them up automatically"
)
return False return False
self.logger.debug(f"Verified containers running: {required}") self.logger.info(f"All required containers are running: {required}")
return True return True
except Exception as e: except Exception as e:
self.logger.error(f"Container verification failed: {e}") self.logger.error(f"Container verification failed: {e}")
self.logger.error("Please ensure Docker is running and containers are available, or use --rebuild")
return False return False
def simulate_claude_cli_session(self) -> bool: def simulate_claude_cli_session(self) -> bool:
@@ -236,8 +244,8 @@ class CommunicationSimulator:
self.logger.error(f"Test {test_name} failed with exception: {e}") self.logger.error(f"Test {test_name} failed with exception: {e}")
return False return False
def run_individual_test(self, test_name: str, skip_docker_setup: bool = False) -> bool: def run_individual_test(self, test_name: str) -> bool:
"""Run a single test individually with optional Docker setup skip""" """Run a single test individually"""
try: try:
if test_name not in self.available_tests: if test_name not in self.available_tests:
self.logger.error(f"Unknown test: {test_name}") self.logger.error(f"Unknown test: {test_name}")
@@ -246,11 +254,10 @@ class CommunicationSimulator:
self.logger.info(f"Running individual test: {test_name}") self.logger.info(f"Running individual test: {test_name}")
# Setup environment unless skipped # Setup environment
if not skip_docker_setup: if not self.setup_test_environment():
if not self.setup_test_environment(): self.logger.error("Environment setup failed")
self.logger.error("Environment setup failed") return False
return False
# Run the single test # Run the single test
test_function = self.available_tests[test_name] test_function = self.available_tests[test_name]
@@ -267,7 +274,7 @@ class CommunicationSimulator:
self.logger.error(f"Individual test {test_name} failed with exception: {e}") self.logger.error(f"Individual test {test_name} failed with exception: {e}")
return False return False
finally: finally:
if not skip_docker_setup and not self.keep_logs: if not self.keep_logs:
self.cleanup() self.cleanup()
def get_available_tests(self) -> dict[str, str]: def get_available_tests(self) -> dict[str, str]:
@@ -281,9 +288,9 @@ class CommunicationSimulator:
def print_test_summary(self): def print_test_summary(self):
"""Print comprehensive test results summary""" """Print comprehensive test results summary"""
print("\\n" + "=" * 70) self.logger.info("\n" + "=" * 70)
print("ZEN MCP COMMUNICATION SIMULATOR - TEST RESULTS SUMMARY") self.logger.info("ZEN MCP COMMUNICATION SIMULATOR - TEST RESULTS SUMMARY")
print("=" * 70) self.logger.info("=" * 70)
passed_count = sum(1 for result in self.test_results.values() if result) passed_count = sum(1 for result in self.test_results.values() if result)
total_count = len(self.test_results) total_count = len(self.test_results)
@@ -293,25 +300,28 @@ class CommunicationSimulator:
# Get test description # Get test description
temp_instance = self.test_registry[test_name](verbose=False) temp_instance = self.test_registry[test_name](verbose=False)
description = temp_instance.test_description description = temp_instance.test_description
print(f"{description}: {status}") if result:
self.logger.info(f"{description}: {status}")
else:
self.logger.error(f"{description}: {status}")
print(f"\\nOVERALL RESULT: {'SUCCESS' if passed_count == total_count else 'FAILURE'}") if passed_count == total_count:
print(f"{passed_count}/{total_count} tests passed") self.logger.info("\nOVERALL RESULT: SUCCESS")
print("=" * 70) else:
self.logger.error("\nOVERALL RESULT: FAILURE")
self.logger.info(f"{passed_count}/{total_count} tests passed")
self.logger.info("=" * 70)
return passed_count == total_count return passed_count == total_count
def run_full_test_suite(self, skip_docker_setup: bool = False) -> bool: def run_full_test_suite(self) -> bool:
"""Run the complete test suite""" """Run the complete test suite"""
try: try:
self.logger.info("Starting Zen MCP Communication Simulator Test Suite") self.logger.info("Starting Zen MCP Communication Simulator Test Suite")
# Setup # Setup
if not skip_docker_setup: if not self.setup_test_environment():
if not self.setup_test_environment(): self.logger.error("Environment setup failed")
self.logger.error("Environment setup failed") return False
return False
else:
self.logger.info("Skipping Docker setup (containers assumed running)")
# Main simulation # Main simulation
if not self.simulate_claude_cli_session(): if not self.simulate_claude_cli_session():
@@ -327,7 +337,7 @@ class CommunicationSimulator:
self.logger.error(f"Test suite failed: {e}") self.logger.error(f"Test suite failed: {e}")
return False return False
finally: finally:
if not self.keep_logs and not skip_docker_setup: if not self.keep_logs:
self.cleanup() self.cleanup()
def cleanup(self): def cleanup(self):
@@ -335,11 +345,11 @@ class CommunicationSimulator:
try: try:
self.logger.info("Cleaning up test environment...") self.logger.info("Cleaning up test environment...")
# Note: We don't stop Docker services ourselves - let setup-docker.sh handle Docker lifecycle
if not self.keep_logs: if not self.keep_logs:
# Stop Docker services self.logger.info("Test completed. Docker containers left running (use setup-docker.sh to manage)")
self._run_command(["docker", "compose", "down", "--remove-orphans"], check=False, capture_output=True)
else: else:
self.logger.info("Keeping Docker services running for log inspection") self.logger.info("Keeping logs and Docker services running for inspection")
# Remove temp directory # Remove temp directory
if self.temp_dir and os.path.exists(self.temp_dir): if self.temp_dir and os.path.exists(self.temp_dir):
@@ -365,15 +375,7 @@ def parse_arguments():
parser.add_argument("--tests", "-t", nargs="+", help="Specific tests to run (space-separated)") parser.add_argument("--tests", "-t", nargs="+", help="Specific tests to run (space-separated)")
parser.add_argument("--list-tests", action="store_true", help="List available tests and exit") parser.add_argument("--list-tests", action="store_true", help="List available tests and exit")
parser.add_argument("--individual", "-i", help="Run a single test individually") parser.add_argument("--individual", "-i", help="Run a single test individually")
parser.add_argument( parser.add_argument("--rebuild", action="store_true", help="Force rebuild Docker environment using setup-docker.sh")
"--skip-docker",
action="store_true",
default=True,
help="Skip Docker setup (assumes containers are already running) - DEFAULT",
)
parser.add_argument(
"--rebuild-docker", action="store_true", help="Force rebuild Docker environment (overrides --skip-docker)"
)
return parser.parse_args() return parser.parse_args()
@@ -381,57 +383,59 @@ def parse_arguments():
def list_available_tests(): def list_available_tests():
"""List all available tests and exit""" """List all available tests and exit"""
simulator = CommunicationSimulator() simulator = CommunicationSimulator()
print("Available tests:") # Create a simple logger for this function
logger = logging.getLogger("list_tests")
logging.basicConfig(level=logging.INFO, format="%(message)s")
logger.info("Available tests:")
for test_name, description in simulator.get_available_tests().items(): for test_name, description in simulator.get_available_tests().items():
print(f" {test_name:<25} - {description}") logger.info(f" {test_name:<25} - {description}")
def run_individual_test(simulator, test_name, skip_docker): def run_individual_test(simulator, test_name):
"""Run a single test individually""" """Run a single test individually"""
logger = simulator.logger
try: try:
success = simulator.run_individual_test(test_name, skip_docker_setup=skip_docker) success = simulator.run_individual_test(test_name)
if success: if success:
print(f"\\nINDIVIDUAL TEST {test_name.upper()}: PASSED") logger.info(f"\nINDIVIDUAL TEST {test_name.upper()}: PASSED")
return 0 return 0
else: else:
print(f"\\nINDIVIDUAL TEST {test_name.upper()}: FAILED") logger.error(f"\nINDIVIDUAL TEST {test_name.upper()}: FAILED")
return 1 return 1
except KeyboardInterrupt: except KeyboardInterrupt:
print(f"\\nIndividual test {test_name} interrupted by user") logger.warning(f"\nIndividual test {test_name} interrupted by user")
if not skip_docker: simulator.cleanup()
simulator.cleanup()
return 130 return 130
except Exception as e: except Exception as e:
print(f"\\nIndividual test {test_name} failed with error: {e}") logger.error(f"\nIndividual test {test_name} failed with error: {e}")
if not skip_docker: simulator.cleanup()
simulator.cleanup()
return 1 return 1
def run_test_suite(simulator, skip_docker=False): def run_test_suite(simulator):
"""Run the full test suite or selected tests""" """Run the full test suite or selected tests"""
logger = simulator.logger
try: try:
success = simulator.run_full_test_suite(skip_docker_setup=skip_docker) success = simulator.run_full_test_suite()
if success: if success:
print("\\nCOMPREHENSIVE MCP COMMUNICATION TEST: PASSED") logger.info("\nCOMPREHENSIVE MCP COMMUNICATION TEST: PASSED")
return 0 return 0
else: else:
print("\\nCOMPREHENSIVE MCP COMMUNICATION TEST: FAILED") logger.error("\nCOMPREHENSIVE MCP COMMUNICATION TEST: FAILED")
print("Check detailed results above") logger.error("Check detailed results above")
return 1 return 1
except KeyboardInterrupt: except KeyboardInterrupt:
print("\\nTest interrupted by user") logger.warning("\nTest interrupted by user")
if not skip_docker: simulator.cleanup()
simulator.cleanup()
return 130 return 130
except Exception as e: except Exception as e:
print(f"\\nUnexpected error: {e}") logger.error(f"\nUnexpected error: {e}")
if not skip_docker: simulator.cleanup()
simulator.cleanup()
return 1 return 1
@@ -445,16 +449,15 @@ def main():
return return
# Initialize simulator consistently for all use cases # Initialize simulator consistently for all use cases
simulator = CommunicationSimulator(verbose=args.verbose, keep_logs=args.keep_logs, selected_tests=args.tests) simulator = CommunicationSimulator(
verbose=args.verbose, keep_logs=args.keep_logs, selected_tests=args.tests, rebuild=args.rebuild
)
# Determine execution mode and run # Determine execution mode and run
# Override skip_docker if rebuild_docker is specified
skip_docker = args.skip_docker and not args.rebuild_docker
if args.individual: if args.individual:
exit_code = run_individual_test(simulator, args.individual, skip_docker) exit_code = run_individual_test(simulator, args.individual)
else: else:
exit_code = run_test_suite(simulator, skip_docker) exit_code = run_test_suite(simulator)
sys.exit(exit_code) sys.exit(exit_code)

View File

@@ -31,11 +31,11 @@ import config # noqa: E402
importlib.reload(config) importlib.reload(config)
# Set MCP_PROJECT_ROOT to a temporary directory for tests # Set WORKSPACE_ROOT to a temporary directory for tests
# This provides a safe sandbox for file operations during testing # This provides a safe sandbox for file operations during testing
# Create a temporary directory that will be used as the project root for all tests # Create a temporary directory that will be used as the workspace for all tests
test_root = tempfile.mkdtemp(prefix="zen_mcp_test_") test_root = tempfile.mkdtemp(prefix="zen_mcp_test_")
os.environ["MCP_PROJECT_ROOT"] = test_root os.environ["WORKSPACE_ROOT"] = test_root
# Configure asyncio for Windows compatibility # Configure asyncio for Windows compatibility
if sys.platform == "win32": if sys.platform == "win32":
@@ -55,11 +55,11 @@ ModelProviderRegistry.register_provider(ProviderType.OPENAI, OpenAIModelProvider
@pytest.fixture @pytest.fixture
def project_path(tmp_path): def project_path(tmp_path):
""" """
Provides a temporary directory within the PROJECT_ROOT sandbox for tests. Provides a temporary directory within the WORKSPACE_ROOT sandbox for tests.
This ensures all file operations during tests are within the allowed directory. This ensures all file operations during tests are within the allowed directory.
""" """
# Get the test project root # Get the test workspace root
test_root = Path(os.environ.get("MCP_PROJECT_ROOT", "/tmp")) test_root = Path(os.environ.get("WORKSPACE_ROOT", "/tmp"))
# Create a subdirectory for this specific test # Create a subdirectory for this specific test
test_dir = test_root / f"test_{tmp_path.name}" test_dir = test_root / f"test_{tmp_path.name}"

View File

@@ -79,7 +79,6 @@ def test_docker_security_validation():
original_env = os.environ.copy() original_env = os.environ.copy()
try: try:
os.environ["WORKSPACE_ROOT"] = str(host_workspace) os.environ["WORKSPACE_ROOT"] = str(host_workspace)
os.environ["MCP_PROJECT_ROOT"] = str(host_workspace)
# Reload the module # Reload the module
importlib.reload(utils.file_utils) importlib.reload(utils.file_utils)

View File

@@ -60,10 +60,10 @@ class TestPrecommitToolWithMockStore:
temp_dir, _ = temp_repo temp_dir, _ = temp_repo
tool = Precommit() tool = Precommit()
# Mock the Redis client getter and PROJECT_ROOT to allow access to temp files # Mock the Redis client getter and SECURITY_ROOT to allow access to temp files
with ( with (
patch("utils.conversation_memory.get_redis_client", return_value=mock_redis), patch("utils.conversation_memory.get_redis_client", return_value=mock_redis),
patch("utils.file_utils.PROJECT_ROOT", Path(temp_dir).resolve()), patch("utils.file_utils.SECURITY_ROOT", Path(temp_dir).resolve()),
): ):
yield tool yield tool

View File

@@ -34,7 +34,7 @@ class TestFileUtils:
# Try to read a file outside the project root # Try to read a file outside the project root
content, tokens = read_file_content("/etc/passwd") content, tokens = read_file_content("/etc/passwd")
assert "--- ERROR ACCESSING FILE:" in content assert "--- ERROR ACCESSING FILE:" in content
assert "Path outside project root" in content assert "Path outside workspace" in content
assert tokens > 0 assert tokens > 0
def test_read_file_content_relative_path_rejected(self): def test_read_file_content_relative_path_rejected(self):

View File

@@ -82,32 +82,18 @@ if WORKSPACE_ROOT:
f"Please set WORKSPACE_ROOT to a specific project directory." f"Please set WORKSPACE_ROOT to a specific project directory."
) )
# Get project root from environment or use current directory # Security boundary
# This defines the sandbox directory where file access is allowed # In Docker: use /workspace (container directory)
# # In tests/direct mode: use WORKSPACE_ROOT (host directory)
# Simplified Security model: if CONTAINER_WORKSPACE.exists():
# 1. If MCP_PROJECT_ROOT is explicitly set, use it as sandbox (override) # Running in Docker container
# 2. If WORKSPACE_ROOT is set (Docker mode), auto-use /workspace as sandbox SECURITY_ROOT = CONTAINER_WORKSPACE
# 3. Otherwise, use home directory (direct usage) elif WORKSPACE_ROOT:
env_root = os.environ.get("MCP_PROJECT_ROOT") # Running in tests or direct mode with WORKSPACE_ROOT set
if env_root: SECURITY_ROOT = Path(WORKSPACE_ROOT).resolve()
# If explicitly set, use it as sandbox (allows custom override)
PROJECT_ROOT = Path(env_root).resolve()
elif WORKSPACE_ROOT and CONTAINER_WORKSPACE.exists():
# Running in Docker with workspace mounted - auto-use /workspace
PROJECT_ROOT = CONTAINER_WORKSPACE
else: else:
# Running directly on host - default to home directory for normal usage # Fallback for backward compatibility (should not happen in normal usage)
# This allows access to any file under the user's home directory SECURITY_ROOT = Path.home()
PROJECT_ROOT = Path.home()
# Additional security check for explicit PROJECT_ROOT
if env_root and PROJECT_ROOT.parent == PROJECT_ROOT:
raise RuntimeError(
"Security Error: MCP_PROJECT_ROOT cannot be the filesystem root. "
"This would give access to the entire filesystem. "
"Please set MCP_PROJECT_ROOT to a specific directory."
)
# Directories to exclude from recursive file search # Directories to exclude from recursive file search
@@ -293,15 +279,15 @@ def resolve_and_validate_path(path_str: str) -> Path:
# Step 5: Security Policy - Ensure the resolved path is within PROJECT_ROOT # Step 5: Security Policy - Ensure the resolved path is within PROJECT_ROOT
# This prevents directory traversal attacks (e.g., /project/../../../etc/passwd) # This prevents directory traversal attacks (e.g., /project/../../../etc/passwd)
try: try:
resolved_path.relative_to(PROJECT_ROOT) resolved_path.relative_to(SECURITY_ROOT)
except ValueError: except ValueError:
# Provide detailed error for debugging while avoiding information disclosure # Provide detailed error for debugging while avoiding information disclosure
logger.warning( logger.warning(
f"Access denied - path outside project root. " f"Access denied - path outside workspace. "
f"Requested: {path_str}, Resolved: {resolved_path}, Root: {PROJECT_ROOT}" f"Requested: {path_str}, Resolved: {resolved_path}, Workspace: {SECURITY_ROOT}"
) )
raise PermissionError( raise PermissionError(
f"Path outside project root: {path_str}\nProject root: {PROJECT_ROOT}\nResolved path: {resolved_path}" f"Path outside workspace: {path_str}\nWorkspace: {SECURITY_ROOT}\nResolved path: {resolved_path}"
) )
return resolved_path return resolved_path
@@ -358,16 +344,16 @@ def expand_paths(paths: list[str], extensions: Optional[set[str]] = None) -> lis
if not path_obj.exists(): if not path_obj.exists():
continue continue
# Safety check: Prevent reading entire home directory or workspace root # Safety check: Prevent reading entire workspace root
# This could expose too many files and cause performance issues # This could expose too many files and cause performance issues
if path_obj.is_dir(): if path_obj.is_dir():
resolved_project_root = PROJECT_ROOT.resolve() resolved_workspace = SECURITY_ROOT.resolve()
resolved_path = path_obj.resolve() resolved_path = path_obj.resolve()
# Check if this is the entire project root/home directory # Check if this is the entire workspace root directory
if resolved_path == resolved_project_root: if resolved_path == resolved_workspace:
logger.warning( logger.warning(
f"Ignoring request to read entire project root directory: {path}. " f"Ignoring request to read entire workspace directory: {path}. "
f"Please specify individual files or subdirectories instead." f"Please specify individual files or subdirectories instead."
) )
continue continue