From 9385ab09e4aef578f40c15fa8c5f18e3c70eb457 Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 3 Dec 2025 15:20:07 +0100 Subject: [PATCH] Fix docker volume mounts for host directory access MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace named volume (agent_data) with bind mounts for /opt/letsbe/{env,stacks,nginx} - Update ALLOWED_FILE_ROOT default from /opt/agent_data to /opt/letsbe - Add startup validation that warns (but doesn't block) if host dirs missing This fixes ENV_UPDATE writes going to container filesystem instead of host, and DOCKER_RELOAD failing with "File does not exist" errors. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .claude/settings.local.json | 35 +++++++++++++++++++++++++++++++++++ app/config.py | 2 +- app/main.py | 30 ++++++++++++++++++++++++++++++ docker-compose.yml | 10 +++++----- 4 files changed, 71 insertions(+), 6 deletions(-) create mode 100644 .claude/settings.local.json diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..e1e0cd3 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,35 @@ +{ + "permissions": { + "allow": [ + "Bash(python -m venv:*)", + "Bash(.venv/Scripts/python.exe -m pip:*)", + "Bash(.venv/Scripts/python.exe:*)", + "Bash(timeout 15 .venv/Scripts/python.exe:*)", + "Bash(ORCHESTRATOR_URL=http://localhost:8000 AGENT_TOKEN=test-token timeout 20 .venv/Scripts/python.exe:*)", + "Bash(ORCHESTRATOR_URL=http://nonexistent:9999 AGENT_TOKEN=test-token timeout 15 .venv/Scripts/python.exe:*)", + "Bash(docker compose:*)", + "Bash(curl:*)", + "Bash(docker logs:*)", + "Bash(powershell -Command:*)", + "Bash(jq:*)", + "Bash(pytest:*)", + "Bash(python -m pytest:*)", + "Bash(.venvScriptspython.exe -m pip install -r requirements.txt -q)", + "Bash(.venvScriptspython.exe -m pytest tests/executors/test_env_update_executor.py -v)", + "Bash(..venvScriptspython.exe -m pytest tests/executors/test_env_update_executor.py -v)", + "mcp__serena__get_symbols_overview", + "mcp__serena__find_symbol", + "Bash(..venvScriptspython.exe -m pytest tests/executors/test_composite_executor.py -v)", + "Bash(git init:*)", + "Bash(git remote add:*)", + "Bash(git add:*)", + "Bash(git commit:*)", + "Bash(git push:*)", + "Bash(git remote:*)", + "Bash(git config:*)", + "Bash(git fetch:*)" + ], + "deny": [], + "ask": [] + } +} diff --git a/app/config.py b/app/config.py index 348d6bf..d7ebdcc 100644 --- a/app/config.py +++ b/app/config.py @@ -54,7 +54,7 @@ class Settings(BaseSettings): circuit_breaker_cooldown: int = Field(default=300, ge=30, le=900, description="Cooldown period in seconds") # Security - File operations - allowed_file_root: str = Field(default="/opt/agent_data", description="Root directory for file operations") + allowed_file_root: str = Field(default="/opt/letsbe", description="Root directory for file operations") allowed_env_root: str = Field(default="/opt/letsbe/env", description="Root directory for ENV file operations") max_file_size: int = Field(default=10 * 1024 * 1024, description="Max file size in bytes (default 10MB)") diff --git a/app/main.py b/app/main.py index 60782b7..4ee4349 100644 --- a/app/main.py +++ b/app/main.py @@ -3,6 +3,7 @@ import asyncio import signal import sys +from pathlib import Path from typing import Optional from app import __version__ @@ -28,6 +29,34 @@ def print_banner() -> None: print(banner) +def validate_mounted_directories() -> None: + """Check that required host directories are mounted. + + Logs warnings if directories are missing but does not prevent startup. + """ + logger = get_logger("main") + + required_dirs = [ + "/opt/letsbe/env", + "/opt/letsbe/stacks", + "/opt/letsbe/nginx", + ] + + missing = [] + for dir_path in required_dirs: + if not Path(dir_path).is_dir(): + missing.append(dir_path) + + if missing: + logger.warning( + "mounted_directories_missing", + missing=missing, + message="Some host directories are not mounted. Tasks requiring these paths will fail.", + ) + else: + logger.info("mounted_directories_ok", directories=required_dirs) + + async def main() -> int: """Main async entry point. @@ -41,6 +70,7 @@ async def main() -> int: logger = get_logger("main") print_banner() + validate_mounted_directories() logger.info( "agent_starting", diff --git a/docker-compose.yml b/docker-compose.yml index ce40cb0..49cf1d3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -28,7 +28,7 @@ services: - CIRCUIT_BREAKER_COOLDOWN=${CIRCUIT_BREAKER_COOLDOWN:-300} # Security - - ALLOWED_FILE_ROOT=${ALLOWED_FILE_ROOT:-/opt/agent_data} + - ALLOWED_FILE_ROOT=${ALLOWED_FILE_ROOT:-/opt/letsbe} - MAX_FILE_SIZE=${MAX_FILE_SIZE:-10485760} - SHELL_TIMEOUT=${SHELL_TIMEOUT:-60} @@ -39,8 +39,10 @@ services: # Hot reload in development - ./app:/app/app:ro - # Agent data directory - - agent_data:/opt/agent_data + # Host directory mounts for real infrastructure access + - /opt/letsbe/env:/opt/letsbe/env + - /opt/letsbe/stacks:/opt/letsbe/stacks + - /opt/letsbe/nginx:/opt/letsbe/nginx # Pending results persistence - agent_home:/home/agent/.letsbe-agent @@ -62,7 +64,5 @@ services: memory: 64M volumes: - agent_data: - name: letsbe-agent-data agent_home: name: letsbe-agent-home