mirror of
https://github.com/Monadical-SAS/cubbi.git
synced 2025-12-20 20:29:06 +00:00
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
This commit is contained in:
@@ -72,14 +72,16 @@ mc driver build goose
|
|||||||
mc driver build goose --push
|
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
|
- `Dockerfile`: Docker image definition
|
||||||
- `entrypoint.sh`: Container entrypoint script
|
- `entrypoint.sh`: Container entrypoint script
|
||||||
- `mai-init.sh`: Standardized initialization script
|
- `mc-init.sh`: Standardized initialization script
|
||||||
- `mai-driver.yaml`: Driver metadata and configuration
|
- `mc-driver.yaml`: Driver metadata and configuration
|
||||||
- `README.md`: Driver documentation
|
- `README.md`: Driver documentation
|
||||||
|
|
||||||
|
MC automatically discovers and loads driver definitions from the YAML files.
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -8,35 +8,10 @@ DEFAULT_CONFIG_DIR = Path.home() / ".config" / "mc"
|
|||||||
DEFAULT_CONFIG_FILE = DEFAULT_CONFIG_DIR / "config.yaml"
|
DEFAULT_CONFIG_FILE = DEFAULT_CONFIG_DIR / "config.yaml"
|
||||||
DEFAULT_DRIVERS_DIR = Path.home() / ".config" / "mc" / "drivers"
|
DEFAULT_DRIVERS_DIR = Path.home() / ".config" / "mc" / "drivers"
|
||||||
PROJECT_ROOT = Path(__file__).parent.parent
|
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
|
# Dynamically loaded from drivers directory at runtime
|
||||||
DEFAULT_DRIVERS = {
|
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],
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class ConfigManager:
|
class ConfigManager:
|
||||||
@@ -46,6 +21,10 @@ class ConfigManager:
|
|||||||
self.drivers_dir = DEFAULT_DRIVERS_DIR
|
self.drivers_dir = DEFAULT_DRIVERS_DIR
|
||||||
self.config = self._load_or_create_config()
|
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:
|
def _load_or_create_config(self) -> Config:
|
||||||
"""Load existing config or create a new one with defaults"""
|
"""Load existing config or create a new one with defaults"""
|
||||||
if self.config_path.exists():
|
if self.config_path.exists():
|
||||||
@@ -80,18 +59,12 @@ class ConfigManager:
|
|||||||
self.config_dir.mkdir(parents=True, exist_ok=True)
|
self.config_dir.mkdir(parents=True, exist_ok=True)
|
||||||
self.drivers_dir.mkdir(parents=True, exist_ok=True)
|
self.drivers_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
# Load built-in drivers from directories
|
# Initial config without drivers
|
||||||
builtin_drivers = self.load_builtin_drivers()
|
|
||||||
|
|
||||||
# Merge with default drivers, with directory drivers taking precedence
|
|
||||||
drivers = {**DEFAULT_DRIVERS, **builtin_drivers}
|
|
||||||
|
|
||||||
config = Config(
|
config = Config(
|
||||||
docker={
|
docker={
|
||||||
"socket": "/var/run/docker.sock",
|
"socket": "/var/run/docker.sock",
|
||||||
"network": "mc-network",
|
"network": "mc-network",
|
||||||
},
|
},
|
||||||
drivers=drivers,
|
|
||||||
defaults={
|
defaults={
|
||||||
"driver": "goose",
|
"driver": "goose",
|
||||||
},
|
},
|
||||||
@@ -115,12 +88,23 @@ class ConfigManager:
|
|||||||
yaml.dump(config_dict, f)
|
yaml.dump(config_dict, f)
|
||||||
|
|
||||||
def get_driver(self, name: str) -> Optional[Driver]:
|
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)
|
return self.config.drivers.get(name)
|
||||||
|
|
||||||
def list_drivers(self) -> Dict[str, Driver]:
|
def list_drivers(self) -> Dict[str, Driver]:
|
||||||
"""List all available drivers"""
|
"""List all available drivers (both builtin and user-configured)"""
|
||||||
return self.config.drivers
|
# 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:
|
def add_session(self, session_id: str, session_data: dict) -> None:
|
||||||
"""Add a session to the config"""
|
"""Add a session to the config"""
|
||||||
@@ -140,10 +124,10 @@ class ConfigManager:
|
|||||||
|
|
||||||
def load_driver_from_dir(self, driver_dir: Path) -> Optional[Driver]:
|
def load_driver_from_dir(self, driver_dir: Path) -> Optional[Driver]:
|
||||||
"""Load a driver configuration from a directory"""
|
"""Load a driver configuration from a directory"""
|
||||||
yaml_path = (
|
# Try with mc-driver.yaml first (new format), then mai-driver.yaml (legacy)
|
||||||
driver_dir / "mai-driver.yaml"
|
yaml_path = driver_dir / "mc-driver.yaml"
|
||||||
) # Keep this name for backward compatibility
|
if not yaml_path.exists():
|
||||||
|
yaml_path = driver_dir / "mai-driver.yaml" # Backward compatibility
|
||||||
if not yaml_path.exists():
|
if not yaml_path.exists():
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -159,28 +143,33 @@ class ConfigManager:
|
|||||||
print(f"Driver config {yaml_path} missing required fields")
|
print(f"Driver config {yaml_path} missing required fields")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Create driver object
|
# Use Driver.model_validate to handle all fields from YAML
|
||||||
driver = Driver(
|
# This will map all fields according to the Driver model structure
|
||||||
name=driver_data["name"],
|
try:
|
||||||
description=driver_data["description"],
|
# Ensure image field is set if not in YAML
|
||||||
version=driver_data["version"],
|
if "image" not in driver_data:
|
||||||
maintainer=driver_data["maintainer"],
|
driver_data["image"] = f"monadical/mc-{driver_data['name']}:latest"
|
||||||
image=f"monadical/mc-{driver_data['name']}:latest",
|
|
||||||
ports=driver_data.get("ports", []),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
driver = Driver.model_validate(driver_data)
|
||||||
return driver
|
return driver
|
||||||
|
except Exception as validation_error:
|
||||||
|
print(
|
||||||
|
f"Error validating driver data from {yaml_path}: {validation_error}"
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error loading driver from {yaml_path}: {e}")
|
print(f"Error loading driver from {yaml_path}: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def load_builtin_drivers(self) -> Dict[str, Driver]:
|
def _load_package_drivers(self) -> Dict[str, Driver]:
|
||||||
"""Load all built-in drivers from the drivers directory"""
|
"""Load all package drivers from the mcontainer/drivers directory"""
|
||||||
drivers = {}
|
drivers = {}
|
||||||
|
|
||||||
if not BUILTIN_DRIVERS_DIR.exists():
|
if not BUILTIN_DRIVERS_DIR.exists():
|
||||||
return drivers
|
return drivers
|
||||||
|
|
||||||
|
# Search for mc-driver.yaml files in each subdirectory
|
||||||
for driver_dir in BUILTIN_DRIVERS_DIR.iterdir():
|
for driver_dir in BUILTIN_DRIVERS_DIR.iterdir():
|
||||||
if driver_dir.is_dir():
|
if driver_dir.is_dir():
|
||||||
driver = self.load_driver_from_dir(driver_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]:
|
def get_driver_path(self, driver_name: str) -> Optional[Path]:
|
||||||
"""Get the directory path for a driver"""
|
"""Get the directory path for a driver"""
|
||||||
# Check built-in drivers first
|
# Check package drivers first (these are the bundled ones)
|
||||||
builtin_path = BUILTIN_DRIVERS_DIR / driver_name
|
package_path = BUILTIN_DRIVERS_DIR / driver_name
|
||||||
if builtin_path.exists() and builtin_path.is_dir():
|
if package_path.exists() and package_path.is_dir():
|
||||||
return builtin_path
|
return package_path
|
||||||
|
|
||||||
# Then check user drivers
|
# Then check user drivers
|
||||||
user_path = self.drivers_dir / driver_name
|
user_path = self.drivers_dir / driver_name
|
||||||
|
|||||||
@@ -225,25 +225,35 @@ class ContainerManager:
|
|||||||
env_vars["MC_CONFIG_DIR"] = "/mc-config"
|
env_vars["MC_CONFIG_DIR"] = "/mc-config"
|
||||||
env_vars["MC_DRIVER_CONFIG_DIR"] = f"/mc-config/{driver_name}"
|
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:
|
if driver.persistent_configs:
|
||||||
print("Setting up persistent configuration directories:")
|
print("Setting up persistent configuration directories:")
|
||||||
for config in driver.persistent_configs:
|
for config in driver.persistent_configs:
|
||||||
# Get target directory path on host
|
# Get target directory path on host
|
||||||
target_dir = project_config_path / config.target.lstrip(
|
target_dir = project_config_path / config.target.removeprefix(
|
||||||
"/mc-config/"
|
"/mc-config/"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create directory if it's a directory type config
|
# Create directory if it's a directory type config
|
||||||
if config.type == "directory":
|
if config.type == "directory":
|
||||||
|
dir_existed = target_dir.exists()
|
||||||
target_dir.mkdir(parents=True, exist_ok=True)
|
target_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
if not dir_existed:
|
||||||
print(f" - Created directory: {target_dir}")
|
print(f" - Created directory: {target_dir}")
|
||||||
|
|
||||||
# For files, make sure parent directory exists
|
# For files, make sure parent directory exists
|
||||||
elif config.type == "file":
|
elif config.type == "file":
|
||||||
target_dir.parent.mkdir(parents=True, exist_ok=True)
|
target_dir.parent.mkdir(parents=True, exist_ok=True)
|
||||||
# File will be created by the container if needed
|
# 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
|
# Create container
|
||||||
container = self.client.containers.create(
|
container = self.client.containers.create(
|
||||||
image=driver.image,
|
image=driver.image,
|
||||||
|
|||||||
@@ -2,13 +2,13 @@ name: goose
|
|||||||
description: Goose AI environment
|
description: Goose AI environment
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
maintainer: team@monadical.com
|
maintainer: team@monadical.com
|
||||||
|
image: monadical/mc-goose:latest
|
||||||
|
|
||||||
init:
|
init:
|
||||||
pre_command: /mc-init.sh
|
pre_command: /mc-init.sh
|
||||||
command: /entrypoint.sh
|
command: /entrypoint.sh
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
|
|
||||||
- name: LANGFUSE_INIT_PROJECT_PUBLIC_KEY
|
- name: LANGFUSE_INIT_PROJECT_PUBLIC_KEY
|
||||||
description: Langfuse public key
|
description: Langfuse public key
|
||||||
required: false
|
required: false
|
||||||
@@ -54,6 +54,10 @@ volumes:
|
|||||||
|
|
||||||
persistent_configs:
|
persistent_configs:
|
||||||
- source: "/app/.goose"
|
- source: "/app/.goose"
|
||||||
target: "/mc-config/goose"
|
target: "/mc-config/goose-app"
|
||||||
type: "directory"
|
type: "directory"
|
||||||
description: "Goose memory and configuration"
|
description: "Goose memory"
|
||||||
|
- source: "/root/.config/goose"
|
||||||
|
target: "/mc-config/goose-config"
|
||||||
|
type: "directory"
|
||||||
|
description: "Goose configuration"
|
||||||
@@ -8,26 +8,6 @@ exec > >(tee -a /init.log) 2>&1
|
|||||||
echo "=== MC Initialization started at $(date) ==="
|
echo "=== MC Initialization started at $(date) ==="
|
||||||
echo "INIT_COMPLETE=false" > /init.status
|
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
|
# Project initialization
|
||||||
if [ -n "$MC_PROJECT_URL" ]; then
|
if [ -n "$MC_PROJECT_URL" ]; then
|
||||||
echo "Initializing project: $MC_PROJECT_URL"
|
echo "Initializing project: $MC_PROJECT_URL"
|
||||||
@@ -57,16 +37,10 @@ if [ -n "$MC_PROJECT_URL" ]; then
|
|||||||
bash /app/.mc/init.sh
|
bash /app/.mc/init.sh
|
||||||
fi
|
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
|
if [ -n "$MC_CONFIG_DIR" ] && [ -d "$MC_CONFIG_DIR" ]; then
|
||||||
echo "Setting up persistent configuration symlinks after project clone"
|
echo "Using persistent configuration volumes (direct mounts)"
|
||||||
|
|
||||||
# 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"
|
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -25,15 +25,26 @@ class PersistentConfig(BaseModel):
|
|||||||
description: str = ""
|
description: str = ""
|
||||||
|
|
||||||
|
|
||||||
|
class VolumeMount(BaseModel):
|
||||||
|
mountPath: str
|
||||||
|
description: str = ""
|
||||||
|
|
||||||
|
|
||||||
|
class DriverInit(BaseModel):
|
||||||
|
pre_command: Optional[str] = None
|
||||||
|
command: str
|
||||||
|
|
||||||
|
|
||||||
class Driver(BaseModel):
|
class Driver(BaseModel):
|
||||||
name: str
|
name: str
|
||||||
description: str
|
description: str
|
||||||
version: str
|
version: str
|
||||||
maintainer: str
|
maintainer: str
|
||||||
image: str
|
image: str
|
||||||
|
init: Optional[DriverInit] = None
|
||||||
environment: List[DriverEnvironmentVariable] = []
|
environment: List[DriverEnvironmentVariable] = []
|
||||||
ports: List[int] = []
|
ports: List[int] = []
|
||||||
volumes: List[Dict[str, str]] = []
|
volumes: List[VolumeMount] = []
|
||||||
persistent_configs: List[PersistentConfig] = []
|
persistent_configs: List[PersistentConfig] = []
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user