mirror of
https://github.com/Monadical-SAS/cubbi.git
synced 2025-12-20 12:19:07 +00:00
feat: include new image opencode (#14)
* feat: include new image opencode * docs: update readme
This commit is contained in:
@@ -42,6 +42,7 @@ Then compile your first image:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
cubbi image build goose
|
cubbi image build goose
|
||||||
|
cubbi image build opencode
|
||||||
```
|
```
|
||||||
|
|
||||||
### For Developers
|
### For Developers
|
||||||
@@ -81,6 +82,7 @@ cubbi session close SESSION_ID
|
|||||||
|
|
||||||
# Create a session with a specific image
|
# Create a session with a specific image
|
||||||
cubbix --image goose
|
cubbix --image goose
|
||||||
|
cubbix --image opencode
|
||||||
|
|
||||||
# Create a session with environment variables
|
# Create a session with environment variables
|
||||||
cubbix -e VAR1=value1 -e VAR2=value2
|
cubbix -e VAR1=value1 -e VAR2=value2
|
||||||
@@ -131,12 +133,11 @@ cubbi image list
|
|||||||
|
|
||||||
# Get detailed information about an image
|
# Get detailed information about an image
|
||||||
cubbi image info goose
|
cubbi image info goose
|
||||||
|
cubbi image info opencode
|
||||||
|
|
||||||
# Build an image
|
# Build an image
|
||||||
cubbi image build goose
|
cubbi image build goose
|
||||||
|
cubbi image build opencode
|
||||||
# Build and push an image
|
|
||||||
cubbi image build goose --push
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Images are defined in the `cubbi/images/` directory, with each subdirectory containing:
|
Images are defined in the `cubbi/images/` directory, with each subdirectory containing:
|
||||||
|
|||||||
64
cubbi/images/opencode/Dockerfile
Normal file
64
cubbi/images/opencode/Dockerfile
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
FROM python:3.12-slim
|
||||||
|
|
||||||
|
LABEL maintainer="team@monadical.com"
|
||||||
|
LABEL description="Goose with MCP servers for Cubbi"
|
||||||
|
|
||||||
|
# Install system dependencies including gosu for user switching and shadow for useradd/groupadd
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
gosu \
|
||||||
|
passwd \
|
||||||
|
bash \
|
||||||
|
curl \
|
||||||
|
bzip2 \
|
||||||
|
iputils-ping \
|
||||||
|
iproute2 \
|
||||||
|
libxcb1 \
|
||||||
|
libdbus-1-3 \
|
||||||
|
nano \
|
||||||
|
tmux \
|
||||||
|
git-core \
|
||||||
|
vim \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Install deps
|
||||||
|
WORKDIR /tmp
|
||||||
|
RUN curl -fsSL https://astral.sh/uv/install.sh -o install.sh && \
|
||||||
|
sh install.sh && \
|
||||||
|
mv /root/.local/bin/uv /usr/local/bin/uv && \
|
||||||
|
mv /root/.local/bin/uvx /usr/local/bin/uvx && \
|
||||||
|
rm install.sh
|
||||||
|
|
||||||
|
# Install opencode-ai
|
||||||
|
RUN mkdir -p /opt/node && \
|
||||||
|
curl -fsSL https://nodejs.org/dist/v22.16.0/node-v22.16.0-linux-x64.tar.gz -o node.tar.gz && \
|
||||||
|
tar -xf node.tar.gz -C /opt/node --strip-components=1 && \
|
||||||
|
rm node.tar.gz
|
||||||
|
|
||||||
|
ENV PATH="/opt/node/bin:$PATH"
|
||||||
|
RUN npm i -g opencode-ai
|
||||||
|
|
||||||
|
# Create app directory
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy initialization system
|
||||||
|
COPY cubbi_init.py /cubbi/cubbi_init.py
|
||||||
|
COPY opencode_plugin.py /cubbi/opencode_plugin.py
|
||||||
|
COPY cubbi_image.yaml /cubbi/cubbi_image.yaml
|
||||||
|
COPY init-status.sh /cubbi/init-status.sh
|
||||||
|
RUN chmod +x /cubbi/cubbi_init.py /cubbi/init-status.sh
|
||||||
|
RUN echo 'PATH="/opt/node/bin:$PATH"' >> /etc/bash.bashrc
|
||||||
|
RUN echo '[ -x /cubbi/init-status.sh ] && /cubbi/init-status.sh' >> /etc/bash.bashrc
|
||||||
|
|
||||||
|
# Set up environment
|
||||||
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
ENV PYTHONDONTWRITEBYTECODE=1
|
||||||
|
ENV UV_LINK_MODE=copy
|
||||||
|
|
||||||
|
# Pre-install the cubbi_init
|
||||||
|
RUN /cubbi/cubbi_init.py --help
|
||||||
|
|
||||||
|
# Set WORKDIR to /app, common practice and expected by cubbi-init.sh
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
ENTRYPOINT ["/cubbi/cubbi_init.py"]
|
||||||
|
CMD ["tail", "-f", "/dev/null"]
|
||||||
55
cubbi/images/opencode/README.md
Normal file
55
cubbi/images/opencode/README.md
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
# Opencode Image for Cubbi
|
||||||
|
|
||||||
|
This image provides a containerized environment for running [Opencode](https://opencode.ai).
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Pre-configured environment for Opencode AI
|
||||||
|
- Langfuse logging support
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
### Opencode Configuration
|
||||||
|
|
||||||
|
| Variable | Description | Required | Default |
|
||||||
|
|----------|-------------|----------|---------|
|
||||||
|
| `CUBBI_MODEL` | Model to use with Opencode | No | - |
|
||||||
|
| `CUBBI_PROVIDER` | Provider to use with Opencode | No | - |
|
||||||
|
|
||||||
|
### Cubbi Core Variables
|
||||||
|
|
||||||
|
| Variable | Description | Required | Default |
|
||||||
|
|----------|-------------|----------|---------|
|
||||||
|
| `CUBBI_USER_ID` | UID for the container user | No | `1000` |
|
||||||
|
| `CUBBI_GROUP_ID` | GID for the container user | No | `1000` |
|
||||||
|
| `CUBBI_RUN_COMMAND` | Command to execute after initialization | No | - |
|
||||||
|
| `CUBBI_NO_SHELL` | Exit after command execution | No | `false` |
|
||||||
|
| `CUBBI_CONFIG_DIR` | Directory for persistent configurations | No | `/cubbi-config` |
|
||||||
|
| `CUBBI_PERSISTENT_LINKS` | Semicolon-separated list of source:target symlinks | No | - |
|
||||||
|
|
||||||
|
### MCP Integration Variables
|
||||||
|
|
||||||
|
| Variable | Description | Required |
|
||||||
|
|----------|-------------|----------|
|
||||||
|
| `MCP_COUNT` | Number of available MCP servers | No |
|
||||||
|
| `MCP_NAMES` | JSON array of MCP server names | No |
|
||||||
|
| `MCP_{idx}_NAME` | Name of MCP server at index | No |
|
||||||
|
| `MCP_{idx}_TYPE` | Type of MCP server | No |
|
||||||
|
| `MCP_{idx}_HOST` | Hostname of MCP server | No |
|
||||||
|
| `MCP_{idx}_URL` | Full URL for remote MCP servers | No |
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
To build this image:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd drivers/opencode
|
||||||
|
docker build -t monadical/cubbi-opencode:latest .
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create a new session with this image
|
||||||
|
cubbix -i opencode
|
||||||
|
```
|
||||||
18
cubbi/images/opencode/cubbi_image.yaml
Normal file
18
cubbi/images/opencode/cubbi_image.yaml
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
name: opencode
|
||||||
|
description: Opencode AI environment
|
||||||
|
version: 1.0.0
|
||||||
|
maintainer: team@monadical.com
|
||||||
|
image: monadical/cubbi-opencode:latest
|
||||||
|
|
||||||
|
init:
|
||||||
|
pre_command: /cubbi-init.sh
|
||||||
|
command: /entrypoint.sh
|
||||||
|
|
||||||
|
environment: []
|
||||||
|
ports: []
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
- mountPath: /app
|
||||||
|
description: Application directory
|
||||||
|
|
||||||
|
persistent_configs: []
|
||||||
255
cubbi/images/opencode/opencode_plugin.py
Normal file
255
cubbi/images/opencode/opencode_plugin.py
Normal file
@@ -0,0 +1,255 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Opencode-specific plugin for Cubbi initialization
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Dict
|
||||||
|
|
||||||
|
from cubbi_init import ToolPlugin
|
||||||
|
|
||||||
|
# Map of environment variables to provider names in auth.json
|
||||||
|
API_KEY_MAPPINGS = {
|
||||||
|
"ANTHROPIC_API_KEY": "anthropic",
|
||||||
|
"GOOGLE_API_KEY": "google",
|
||||||
|
"OPENAI_API_KEY": "openai",
|
||||||
|
"OPENROUTER_API_KEY": "openrouter",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class OpencodePlugin(ToolPlugin):
|
||||||
|
"""Plugin for Opencode AI tool initialization"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def tool_name(self) -> str:
|
||||||
|
return "opencode"
|
||||||
|
|
||||||
|
def _get_user_ids(self) -> tuple[int, int]:
|
||||||
|
"""Get the cubbi user and group IDs from environment"""
|
||||||
|
user_id = int(os.environ.get("CUBBI_USER_ID", "1000"))
|
||||||
|
group_id = int(os.environ.get("CUBBI_GROUP_ID", "1000"))
|
||||||
|
return user_id, group_id
|
||||||
|
|
||||||
|
def _set_ownership(self, path: Path) -> None:
|
||||||
|
"""Set ownership of a path to the cubbi user"""
|
||||||
|
user_id, group_id = self._get_user_ids()
|
||||||
|
try:
|
||||||
|
os.chown(path, user_id, group_id)
|
||||||
|
except OSError as e:
|
||||||
|
self.status.log(f"Failed to set ownership for {path}: {e}", "WARNING")
|
||||||
|
|
||||||
|
def _get_user_config_path(self) -> Path:
|
||||||
|
"""Get the correct config path for the cubbi user"""
|
||||||
|
return Path("/home/cubbi/.config/opencode")
|
||||||
|
|
||||||
|
def _get_user_data_path(self) -> Path:
|
||||||
|
"""Get the correct data path for the cubbi user"""
|
||||||
|
return Path("/home/cubbi/.local/share/opencode")
|
||||||
|
|
||||||
|
def _ensure_user_config_dir(self) -> Path:
|
||||||
|
"""Ensure config directory exists with correct ownership"""
|
||||||
|
config_dir = self._get_user_config_path()
|
||||||
|
|
||||||
|
# Create the full directory path
|
||||||
|
try:
|
||||||
|
config_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
except FileExistsError:
|
||||||
|
# Directory already exists, which is fine
|
||||||
|
pass
|
||||||
|
except OSError as e:
|
||||||
|
self.status.log(
|
||||||
|
f"Failed to create config directory {config_dir}: {e}", "ERROR"
|
||||||
|
)
|
||||||
|
return config_dir
|
||||||
|
|
||||||
|
# Set ownership for the directories
|
||||||
|
config_parent = config_dir.parent
|
||||||
|
if config_parent.exists():
|
||||||
|
self._set_ownership(config_parent)
|
||||||
|
|
||||||
|
if config_dir.exists():
|
||||||
|
self._set_ownership(config_dir)
|
||||||
|
|
||||||
|
return config_dir
|
||||||
|
|
||||||
|
def _ensure_user_data_dir(self) -> Path:
|
||||||
|
"""Ensure data directory exists with correct ownership"""
|
||||||
|
data_dir = self._get_user_data_path()
|
||||||
|
|
||||||
|
# Create the full directory path
|
||||||
|
try:
|
||||||
|
data_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
except FileExistsError:
|
||||||
|
# Directory already exists, which is fine
|
||||||
|
pass
|
||||||
|
except OSError as e:
|
||||||
|
self.status.log(f"Failed to create data directory {data_dir}: {e}", "ERROR")
|
||||||
|
return data_dir
|
||||||
|
|
||||||
|
# Set ownership for the directories
|
||||||
|
data_parent = data_dir.parent
|
||||||
|
if data_parent.exists():
|
||||||
|
self._set_ownership(data_parent)
|
||||||
|
|
||||||
|
if data_dir.exists():
|
||||||
|
self._set_ownership(data_dir)
|
||||||
|
|
||||||
|
return data_dir
|
||||||
|
|
||||||
|
def _create_auth_file(self) -> bool:
|
||||||
|
"""Create auth.json file with configured API keys"""
|
||||||
|
# Ensure data directory exists
|
||||||
|
data_dir = self._ensure_user_data_dir()
|
||||||
|
if not data_dir.exists():
|
||||||
|
self.status.log(
|
||||||
|
f"Data directory {data_dir} does not exist and could not be created",
|
||||||
|
"ERROR",
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
auth_file = data_dir / "auth.json"
|
||||||
|
auth_data = {}
|
||||||
|
|
||||||
|
# Check each API key and add to auth data if present
|
||||||
|
for env_var, provider in API_KEY_MAPPINGS.items():
|
||||||
|
api_key = os.environ.get(env_var)
|
||||||
|
if api_key:
|
||||||
|
auth_data[provider] = {"type": "api", "key": api_key}
|
||||||
|
self.status.log(f"Added {provider} API key to auth configuration")
|
||||||
|
|
||||||
|
# Only write file if we have at least one API key
|
||||||
|
if not auth_data:
|
||||||
|
self.status.log("No API keys found, skipping auth.json creation")
|
||||||
|
return True
|
||||||
|
|
||||||
|
try:
|
||||||
|
with auth_file.open("w") as f:
|
||||||
|
json.dump(auth_data, f, indent=2)
|
||||||
|
|
||||||
|
# Set ownership of the auth file to cubbi user
|
||||||
|
self._set_ownership(auth_file)
|
||||||
|
|
||||||
|
# Set secure permissions (readable only by owner)
|
||||||
|
auth_file.chmod(0o600)
|
||||||
|
|
||||||
|
self.status.log(f"Created OpenCode auth configuration at {auth_file}")
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
self.status.log(f"Failed to create auth configuration: {e}", "ERROR")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def initialize(self) -> bool:
|
||||||
|
"""Initialize Opencode configuration"""
|
||||||
|
self._ensure_user_config_dir()
|
||||||
|
|
||||||
|
# Create auth.json file with API keys
|
||||||
|
auth_success = self._create_auth_file()
|
||||||
|
|
||||||
|
# Set up tool configuration
|
||||||
|
config_success = self.setup_tool_configuration()
|
||||||
|
|
||||||
|
return auth_success and config_success
|
||||||
|
|
||||||
|
def setup_tool_configuration(self) -> bool:
|
||||||
|
"""Set up Opencode configuration file"""
|
||||||
|
# Ensure directory exists before writing
|
||||||
|
config_dir = self._ensure_user_config_dir()
|
||||||
|
if not config_dir.exists():
|
||||||
|
self.status.log(
|
||||||
|
f"Config directory {config_dir} does not exist and could not be created",
|
||||||
|
"ERROR",
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
config_file = config_dir / "config.json"
|
||||||
|
|
||||||
|
# Load or initialize configuration
|
||||||
|
if config_file.exists():
|
||||||
|
with config_file.open("r") as f:
|
||||||
|
config_data = json.load(f) or {}
|
||||||
|
else:
|
||||||
|
config_data = {}
|
||||||
|
|
||||||
|
# Update with environment variables
|
||||||
|
opencode_model = os.environ.get("CUBBI_MODEL")
|
||||||
|
opencode_provider = os.environ.get("CUBBI_PROVIDER")
|
||||||
|
|
||||||
|
if opencode_model and opencode_provider:
|
||||||
|
config_data["model"] = f"{opencode_provider}/{opencode_model}"
|
||||||
|
self.status.log(f"Set model to {config_data['model']}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
with config_file.open("w") as f:
|
||||||
|
json.dump(config_data, f, indent=2)
|
||||||
|
|
||||||
|
# Set ownership of the config file to cubbi user
|
||||||
|
self._set_ownership(config_file)
|
||||||
|
|
||||||
|
self.status.log(f"Updated Opencode configuration at {config_file}")
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
self.status.log(f"Failed to write Opencode configuration: {e}", "ERROR")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def integrate_mcp_servers(self, mcp_config: Dict[str, Any]) -> bool:
|
||||||
|
"""Integrate Opencode with available MCP servers"""
|
||||||
|
if mcp_config["count"] == 0:
|
||||||
|
self.status.log("No MCP servers to integrate")
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Ensure directory exists before writing
|
||||||
|
config_dir = self._ensure_user_config_dir()
|
||||||
|
if not config_dir.exists():
|
||||||
|
self.status.log(
|
||||||
|
f"Config directory {config_dir} does not exist and could not be created",
|
||||||
|
"ERROR",
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
config_file = config_dir / "config.json"
|
||||||
|
|
||||||
|
if config_file.exists():
|
||||||
|
with config_file.open("r") as f:
|
||||||
|
config_data = json.load(f) or {}
|
||||||
|
else:
|
||||||
|
config_data = {}
|
||||||
|
|
||||||
|
if "mcp" not in config_data:
|
||||||
|
config_data["mcp"] = {}
|
||||||
|
|
||||||
|
for server in mcp_config["servers"]:
|
||||||
|
server_name = server["name"]
|
||||||
|
server_host = server.get("host")
|
||||||
|
server_url = server.get("url")
|
||||||
|
|
||||||
|
if server_name and server_host:
|
||||||
|
mcp_url = f"http://{server_host}:8080/sse"
|
||||||
|
self.status.log(f"Adding MCP extension: {server_name} - {mcp_url}")
|
||||||
|
|
||||||
|
config_data["mcp"][server_name] = {
|
||||||
|
"type": "remote",
|
||||||
|
"url": mcp_url,
|
||||||
|
}
|
||||||
|
elif server_name and server_url:
|
||||||
|
self.status.log(
|
||||||
|
f"Adding remote MCP extension: {server_name} - {server_url}"
|
||||||
|
)
|
||||||
|
|
||||||
|
config_data["mcp"][server_name] = {
|
||||||
|
"type": "remote",
|
||||||
|
"url": server_url,
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
with config_file.open("w") as f:
|
||||||
|
json.dump(config_data, f, indent=2)
|
||||||
|
|
||||||
|
# Set ownership of the config file to cubbi user
|
||||||
|
self._set_ownership(config_file)
|
||||||
|
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
self.status.log(f"Failed to integrate MCP servers: {e}", "ERROR")
|
||||||
|
return False
|
||||||
Reference in New Issue
Block a user