From 96a44ef5679fc22f6c6984206ceaecccbc1909db Mon Sep 17 00:00:00 2001 From: Mathieu Virbel Date: Tue, 11 Mar 2025 19:37:11 -0600 Subject: [PATCH] refactor: move drivers directory into mcontainer package - Relocate goose driver to mcontainer/drivers/ - Update ConfigManager to dynamically scan for driver YAML files - Add support for mc-driver.yaml instead of mai-driver.yaml - Update Driver model to support init commands and other YAML fields - Auto-discover drivers at runtime instead of hardcoding them - Update documentation to reflect new directory structure --- README.md | 8 +- mcontainer/config.py | 107 ++++++++---------- mcontainer/container.py | 18 ++- .../drivers}/goose/Dockerfile | 0 .../drivers}/goose/README.md | 0 .../drivers}/goose/entrypoint.sh | 0 .../drivers}/goose/init-status.sh | 0 .../drivers}/goose/mc-driver.yaml | 14 ++- .../drivers}/goose/mc-init.sh | 34 +----- mcontainer/models.py | 13 ++- 10 files changed, 92 insertions(+), 102 deletions(-) rename {drivers => mcontainer/drivers}/goose/Dockerfile (100%) rename {drivers => mcontainer/drivers}/goose/README.md (100%) rename {drivers => mcontainer/drivers}/goose/entrypoint.sh (100%) rename {drivers => mcontainer/drivers}/goose/init-status.sh (100%) rename {drivers => mcontainer/drivers}/goose/mc-driver.yaml (80%) rename {drivers => mcontainer/drivers}/goose/mc-init.sh (61%) diff --git a/README.md b/README.md index 540d3f6..7a01f77 100644 --- a/README.md +++ b/README.md @@ -72,14 +72,16 @@ mc driver build goose mc driver build goose --push ``` -Drivers are defined in the `drivers/` directory, with each subdirectory containing: +Drivers are defined in the `mcontainer/drivers/` directory, with each subdirectory containing: - `Dockerfile`: Docker image definition - `entrypoint.sh`: Container entrypoint script -- `mai-init.sh`: Standardized initialization script -- `mai-driver.yaml`: Driver metadata and configuration +- `mc-init.sh`: Standardized initialization script +- `mc-driver.yaml`: Driver metadata and configuration - `README.md`: Driver documentation +MC automatically discovers and loads driver definitions from the YAML files. + ## Development ```bash diff --git a/mcontainer/config.py b/mcontainer/config.py index ad7f607..b62455d 100644 --- a/mcontainer/config.py +++ b/mcontainer/config.py @@ -8,35 +8,10 @@ DEFAULT_CONFIG_DIR = Path.home() / ".config" / "mc" DEFAULT_CONFIG_FILE = DEFAULT_CONFIG_DIR / "config.yaml" DEFAULT_DRIVERS_DIR = Path.home() / ".config" / "mc" / "drivers" PROJECT_ROOT = Path(__file__).parent.parent -BUILTIN_DRIVERS_DIR = PROJECT_ROOT / "drivers" +BUILTIN_DRIVERS_DIR = Path(__file__).parent / "drivers" # mcontainer/drivers -# Default built-in driver configurations -DEFAULT_DRIVERS = { - "goose": Driver( - name="goose", - description="Goose with MCP servers", - version="1.0.0", - maintainer="team@monadical.com", - image="monadical/mc-goose:latest", - ports=[8000, 22], - ), - "aider": Driver( - name="aider", - description="Aider coding assistant", - version="1.0.0", - maintainer="team@monadical.com", - image="monadical/mc-aider:latest", - ports=[22], - ), - "claude-code": Driver( - name="claude-code", - description="Claude Code environment", - version="1.0.0", - maintainer="team@monadical.com", - image="monadical/mc-claude-code:latest", - ports=[22], - ), -} +# Dynamically loaded from drivers directory at runtime +DEFAULT_DRIVERS = {} class ConfigManager: @@ -46,6 +21,10 @@ class ConfigManager: self.drivers_dir = DEFAULT_DRIVERS_DIR self.config = self._load_or_create_config() + # Always load package drivers on initialization + # These are separate from the user config + self.builtin_drivers = self._load_package_drivers() + def _load_or_create_config(self) -> Config: """Load existing config or create a new one with defaults""" if self.config_path.exists(): @@ -80,18 +59,12 @@ class ConfigManager: self.config_dir.mkdir(parents=True, exist_ok=True) self.drivers_dir.mkdir(parents=True, exist_ok=True) - # Load built-in drivers from directories - builtin_drivers = self.load_builtin_drivers() - - # Merge with default drivers, with directory drivers taking precedence - drivers = {**DEFAULT_DRIVERS, **builtin_drivers} - + # Initial config without drivers config = Config( docker={ "socket": "/var/run/docker.sock", "network": "mc-network", }, - drivers=drivers, defaults={ "driver": "goose", }, @@ -115,12 +88,23 @@ class ConfigManager: yaml.dump(config_dict, f) def get_driver(self, name: str) -> Optional[Driver]: - """Get a driver by name""" + """Get a driver by name, checking builtin drivers first, then user-configured ones""" + # Check builtin drivers first (package drivers take precedence) + if name in self.builtin_drivers: + return self.builtin_drivers[name] + # If not found, check user-configured drivers return self.config.drivers.get(name) def list_drivers(self) -> Dict[str, Driver]: - """List all available drivers""" - return self.config.drivers + """List all available drivers (both builtin and user-configured)""" + # Start with user config drivers + all_drivers = dict(self.config.drivers) + + # Add builtin drivers, overriding any user drivers with the same name + # This ensures that package-provided drivers always take precedence + all_drivers.update(self.builtin_drivers) + + return all_drivers def add_session(self, session_id: str, session_data: dict) -> None: """Add a session to the config""" @@ -140,12 +124,12 @@ class ConfigManager: def load_driver_from_dir(self, driver_dir: Path) -> Optional[Driver]: """Load a driver configuration from a directory""" - yaml_path = ( - driver_dir / "mai-driver.yaml" - ) # Keep this name for backward compatibility - + # Try with mc-driver.yaml first (new format), then mai-driver.yaml (legacy) + yaml_path = driver_dir / "mc-driver.yaml" if not yaml_path.exists(): - return None + yaml_path = driver_dir / "mai-driver.yaml" # Backward compatibility + if not yaml_path.exists(): + return None try: with open(yaml_path, "r") as f: @@ -159,28 +143,33 @@ class ConfigManager: print(f"Driver config {yaml_path} missing required fields") return None - # Create driver object - driver = Driver( - name=driver_data["name"], - description=driver_data["description"], - version=driver_data["version"], - maintainer=driver_data["maintainer"], - image=f"monadical/mc-{driver_data['name']}:latest", - ports=driver_data.get("ports", []), - ) + # Use Driver.model_validate to handle all fields from YAML + # This will map all fields according to the Driver model structure + try: + # Ensure image field is set if not in YAML + if "image" not in driver_data: + driver_data["image"] = f"monadical/mc-{driver_data['name']}:latest" + + driver = Driver.model_validate(driver_data) + return driver + except Exception as validation_error: + print( + f"Error validating driver data from {yaml_path}: {validation_error}" + ) + return None - return driver except Exception as e: print(f"Error loading driver from {yaml_path}: {e}") return None - def load_builtin_drivers(self) -> Dict[str, Driver]: - """Load all built-in drivers from the drivers directory""" + def _load_package_drivers(self) -> Dict[str, Driver]: + """Load all package drivers from the mcontainer/drivers directory""" drivers = {} if not BUILTIN_DRIVERS_DIR.exists(): return drivers + # Search for mc-driver.yaml files in each subdirectory for driver_dir in BUILTIN_DRIVERS_DIR.iterdir(): if driver_dir.is_dir(): driver = self.load_driver_from_dir(driver_dir) @@ -191,10 +180,10 @@ class ConfigManager: def get_driver_path(self, driver_name: str) -> Optional[Path]: """Get the directory path for a driver""" - # Check built-in drivers first - builtin_path = BUILTIN_DRIVERS_DIR / driver_name - if builtin_path.exists() and builtin_path.is_dir(): - return builtin_path + # Check package drivers first (these are the bundled ones) + package_path = BUILTIN_DRIVERS_DIR / driver_name + if package_path.exists() and package_path.is_dir(): + return package_path # Then check user drivers user_path = self.drivers_dir / driver_name diff --git a/mcontainer/container.py b/mcontainer/container.py index e74b66f..782a943 100644 --- a/mcontainer/container.py +++ b/mcontainer/container.py @@ -225,25 +225,35 @@ class ContainerManager: env_vars["MC_CONFIG_DIR"] = "/mc-config" env_vars["MC_DRIVER_CONFIG_DIR"] = f"/mc-config/{driver_name}" - # Create driver-specific config directories + # Create driver-specific config directories and set up direct volume mounts if driver.persistent_configs: print("Setting up persistent configuration directories:") for config in driver.persistent_configs: # Get target directory path on host - target_dir = project_config_path / config.target.lstrip( + target_dir = project_config_path / config.target.removeprefix( "/mc-config/" ) # Create directory if it's a directory type config if config.type == "directory": + dir_existed = target_dir.exists() target_dir.mkdir(parents=True, exist_ok=True) - print(f" - Created directory: {target_dir}") - + if not dir_existed: + print(f" - Created directory: {target_dir}") # For files, make sure parent directory exists elif config.type == "file": target_dir.parent.mkdir(parents=True, exist_ok=True) # File will be created by the container if needed + # Mount persistent config directly to container path + session_volumes[str(target_dir)] = { + "bind": config.source, + "mode": "rw", + } + print( + f" - Created direct volume mount: {target_dir} -> {config.source}" + ) + # Create container container = self.client.containers.create( image=driver.image, diff --git a/drivers/goose/Dockerfile b/mcontainer/drivers/goose/Dockerfile similarity index 100% rename from drivers/goose/Dockerfile rename to mcontainer/drivers/goose/Dockerfile diff --git a/drivers/goose/README.md b/mcontainer/drivers/goose/README.md similarity index 100% rename from drivers/goose/README.md rename to mcontainer/drivers/goose/README.md diff --git a/drivers/goose/entrypoint.sh b/mcontainer/drivers/goose/entrypoint.sh similarity index 100% rename from drivers/goose/entrypoint.sh rename to mcontainer/drivers/goose/entrypoint.sh diff --git a/drivers/goose/init-status.sh b/mcontainer/drivers/goose/init-status.sh similarity index 100% rename from drivers/goose/init-status.sh rename to mcontainer/drivers/goose/init-status.sh diff --git a/drivers/goose/mc-driver.yaml b/mcontainer/drivers/goose/mc-driver.yaml similarity index 80% rename from drivers/goose/mc-driver.yaml rename to mcontainer/drivers/goose/mc-driver.yaml index 8b4b2d1..0afabdb 100644 --- a/drivers/goose/mc-driver.yaml +++ b/mcontainer/drivers/goose/mc-driver.yaml @@ -2,13 +2,13 @@ name: goose description: Goose AI environment version: 1.0.0 maintainer: team@monadical.com +image: monadical/mc-goose:latest init: pre_command: /mc-init.sh command: /entrypoint.sh environment: - - name: LANGFUSE_INIT_PROJECT_PUBLIC_KEY description: Langfuse public key required: false @@ -45,8 +45,8 @@ environment: sensitive: true ports: - - 8000 # Main application - - 22 # SSH server + - 8000 # Main application + - 22 # SSH server volumes: - mountPath: /app @@ -54,6 +54,10 @@ volumes: persistent_configs: - source: "/app/.goose" - target: "/mc-config/goose" + target: "/mc-config/goose-app" type: "directory" - description: "Goose memory and configuration" \ No newline at end of file + description: "Goose memory" + - source: "/root/.config/goose" + target: "/mc-config/goose-config" + type: "directory" + description: "Goose configuration" diff --git a/drivers/goose/mc-init.sh b/mcontainer/drivers/goose/mc-init.sh similarity index 61% rename from drivers/goose/mc-init.sh rename to mcontainer/drivers/goose/mc-init.sh index b2dc631..09b4793 100755 --- a/drivers/goose/mc-init.sh +++ b/mcontainer/drivers/goose/mc-init.sh @@ -8,26 +8,6 @@ exec > >(tee -a /init.log) 2>&1 echo "=== MC Initialization started at $(date) ===" echo "INIT_COMPLETE=false" > /init.status -# Set up persistent configuration symlinks -if [ -n "$MC_CONFIG_DIR" ] && [ -d "$MC_CONFIG_DIR" ]; then - echo "Setting up persistent configuration in $MC_CONFIG_DIR" - - # Create Goose configuration directory - mkdir -p "$MC_CONFIG_DIR/goose" - - # Create symlink for Goose directory - if [ -d "/app" ]; then - # Make sure .goose directory exists in the target - mkdir -p "$MC_CONFIG_DIR/goose" - - # Create the symlink - echo "Creating symlink for Goose configuration: /app/.goose -> $MC_CONFIG_DIR/goose" - ln -sf "$MC_CONFIG_DIR/goose" "/app/.goose" - else - echo "Warning: /app directory does not exist yet, symlinks will be created after project initialization" - fi -fi - # Project initialization if [ -n "$MC_PROJECT_URL" ]; then echo "Initializing project: $MC_PROJECT_URL" @@ -57,16 +37,10 @@ if [ -n "$MC_PROJECT_URL" ]; then bash /app/.mc/init.sh fi - # Set up symlinks after project is cloned (if MC_CONFIG_DIR exists) + # Persistent configs are now directly mounted as volumes + # No need to create symlinks anymore if [ -n "$MC_CONFIG_DIR" ] && [ -d "$MC_CONFIG_DIR" ]; then - echo "Setting up persistent configuration symlinks after project clone" - - # Create Goose configuration directory - mkdir -p "$MC_CONFIG_DIR/goose" - - # Create symlink for Goose directory - echo "Creating symlink for Goose configuration: /app/.goose -> $MC_CONFIG_DIR/goose" - ln -sf "$MC_CONFIG_DIR/goose" "/app/.goose" + echo "Using persistent configuration volumes (direct mounts)" fi fi @@ -84,4 +58,4 @@ echo "MC driver initialization complete" # Mark initialization as complete echo "=== MC Initialization completed at $(date) ===" -echo "INIT_COMPLETE=true" > /init.status \ No newline at end of file +echo "INIT_COMPLETE=true" > /init.status diff --git a/mcontainer/models.py b/mcontainer/models.py index cd03de1..c103eab 100644 --- a/mcontainer/models.py +++ b/mcontainer/models.py @@ -25,15 +25,26 @@ class PersistentConfig(BaseModel): description: str = "" +class VolumeMount(BaseModel): + mountPath: str + description: str = "" + + +class DriverInit(BaseModel): + pre_command: Optional[str] = None + command: str + + class Driver(BaseModel): name: str description: str version: str maintainer: str image: str + init: Optional[DriverInit] = None environment: List[DriverEnvironmentVariable] = [] ports: List[int] = [] - volumes: List[Dict[str, str]] = [] + volumes: List[VolumeMount] = [] persistent_configs: List[PersistentConfig] = []