Files
roboco/docs/glittery-marinating-yeti.md
T
2025-12-25 23:06:41 +01:00

1714 lines
44 KiB
Markdown

# RoboCo Open Core + Kubernetes Migration Plan
## Overview
**Goal:** Restructure RoboCo for commercialization with an Open Core model, then
deploy to Kubernetes with ArgoCD on the UGREEN NAS.
**Order:** Separation first, then K8s migration.
---
## What's Open vs Closed
| Component | Status | Reason |
|-----------|--------|--------|
| Backend API (`roboco/api/`, `roboco/services/`) | **CLOSED** | Core product
value |
| Orchestrator (`roboco/runtime/`) | **CLOSED** | Competitive advantage |
| MCP Servers (`roboco/mcp/`) | **CLOSED** | Business logic |
| Frontend (future dashboard) | **OPEN** | Drives adoption |
| Python/JS SDK | **OPEN** | Ecosystem growth |
| CLI Tool | **OPEN** | Developer experience |
| Agent Blueprints | **OPEN** | Community templates |
---
## Phase 1: API Separation (7-10 days)
### Why This First
- SDK/CLI can be developed independently of infra
- Validates API surface before K8s migration
- Creates public-facing documentation
- Allows community to start building agents
### Step 1.1: Create OSS Repo Structure (1 day)
Create public `roboco-agents` repo on GitHub:
```
roboco-agents/
├── sdk/
│ └── python/
│ ├── roboco_sdk/
│ │ ├── __init__.py
│ │ ├── client.py # Main RobocoClient class
│ │ ├── tasks.py # TasksAPI wrapper
│ │ ├── messages.py # MessagesAPI wrapper
│ │ ├── journals.py # JournalsAPI wrapper
│ │ ├── notifications.py # NotificationsAPI wrapper
│ │ └── models.py # Pydantic models (Task, Message, etc.)
│ ├── pyproject.toml
│ ├── README.md
│ └── tests/
├── cli/
│ └── roboco_cli/
│ ├── __init__.py
│ ├── main.py # Typer app entry
│ ├── commands/
│ │ ├── task.py # roboco task <cmd>
│ │ ├── agent.py # roboco agent <cmd>
│ │ ├── message.py # roboco message <cmd>
│ │ └── config.py # roboco config <cmd>
│ └── config.py # Config file handling
├── blueprints/
│ ├── templates/
│ │ ├── developer.md # Generic dev blueprint
│ │ ├── qa.md # Generic QA blueprint
│ │ ├── pm.md # Generic PM blueprint
│ │ └── documenter.md # Generic documenter blueprint
│ └── examples/
│ └── simple-team/ # Example 3-agent team
├── docker/
│ ├── agent-base.Dockerfile # Base image for agents
│ └── claude-code.Dockerfile # Claude Code agent image
├── examples/
│ ├── hello-world/ # Minimal agent example
│ ├── task-worker/ # Task processing example
│ └── multi-agent/ # Team coordination example
├── docs/
│ ├── getting-started.md
│ ├── sdk-reference.md
│ ├── cli-reference.md
│ └── blueprint-guide.md
├── LICENSE # MIT or Apache 2.0
├── README.md
└── pyproject.toml # Workspace/monorepo config
```
**Actions:**
1. `gh repo create roboco-agents --public`
2. Initialize with pyproject.toml workspace
3. Setup GitHub Actions for CI/CD
4. Configure PyPI publishing workflow
---
### Step 1.2: Extract SDK from MCP Utils (2-3 days)
**Source files to extract from:**
- `roboco/mcp/utils.py` - `ApiClient` class
- `roboco/api/schemas/*.py` - Response models
- `roboco/models/*.py` - Core domain models
**SDK Structure:**
```python
# roboco_sdk/client.py
import httpx
from typing import Optional
class RobocoClient:
"""Main client for RoboCo API."""
def __init__(
self,
base_url: str = "http://localhost:8000/api/v1",
api_key: Optional[str] = None,
agent_id: Optional[str] = None,
agent_role: Optional[str] = None,
):
self.base_url = base_url
self.headers = {}
if api_key:
self.headers["Authorization"] = f"Bearer {api_key}"
if agent_id:
self.headers["X-Agent-ID"] = agent_id
if agent_role:
self.headers["X-Agent-Role"] = agent_role
self._http = httpx.AsyncClient(
base_url=base_url,
headers=self.headers,
timeout=30.0
)
@property
def tasks(self) -> "TasksAPI":
return TasksAPI(self._http)
@property
def messages(self) -> "MessagesAPI":
return MessagesAPI(self._http)
@property
def journals(self) -> "JournalsAPI":
return JournalsAPI(self._http)
@property
def notifications(self) -> "NotificationsAPI":
return NotificationsAPI(self._http)
async def close(self):
await self._http.aclose()
async def __aenter__(self):
return self
async def __aexit__(self, *args):
await self.close()
```
```python
# roboco_sdk/tasks.py
from typing import Optional, List
from .models import Task, TaskCreate, TaskUpdate
class TasksAPI:
def __init__(self, http: httpx.AsyncClient):
self._http = http
async def list(
self,
status: Optional[str] = None,
team: Optional[str] = None,
assigned_to: Optional[str] = None,
limit: int = 50,
) -> List[Task]:
params = {"limit": limit}
if status:
params["status"] = status
if team:
params["team"] = team
if assigned_to:
params["assigned_to"] = assigned_to
resp = await self._http.get("/tasks", params=params)
resp.raise_for_status()
return [Task(**t) for t in resp.json()["items"]]
async def get(self, task_id: str) -> Task:
resp = await self._http.get(f"/tasks/{task_id}")
resp.raise_for_status()
return Task(**resp.json())
async def create(self, data: TaskCreate) -> Task:
resp = await self._http.post("/tasks", json=data.model_dump())
resp.raise_for_status()
return Task(**resp.json())
async def claim(self, task_id: str) -> Task:
resp = await self._http.post(f"/tasks/{task_id}/claim")
resp.raise_for_status()
return Task(**resp.json())
async def start(self, task_id: str) -> Task:
resp = await self._http.post(f"/tasks/{task_id}/start")
resp.raise_for_status()
return Task(**resp.json())
async def progress(
self, task_id: str, message: str, percentage: int
) -> Task:
resp = await self._http.post(
f"/tasks/{task_id}/progress",
json={"message": message, "percentage": percentage}
)
resp.raise_for_status()
return Task(**resp.json())
async def submit_for_qa(self, task_id: str) -> Task:
resp = await self._http.post(f"/tasks/{task_id}/submit-qa")
resp.raise_for_status()
return Task(**resp.json())
# ... more methods
```
```python
# roboco_sdk/models.py
from pydantic import BaseModel
from typing import Optional, List
from datetime import datetime
from enum import Enum
class TaskStatus(str, Enum):
BACKLOG = "backlog"
PENDING = "pending"
CLAIMED = "claimed"
IN_PROGRESS = "in_progress"
VERIFYING = "verifying"
AWAITING_QA = "awaiting_qa"
AWAITING_DOCUMENTATION = "awaiting_documentation"
AWAITING_PM_REVIEW = "awaiting_pm_review"
COMPLETED = "completed"
BLOCKED = "blocked"
CANCELLED = "cancelled"
class Task(BaseModel):
id: str
title: str
description: Optional[str] = None
status: TaskStatus
priority: int = 3
team: Optional[str] = None
assigned_to: Optional[str] = None
parent_task_id: Optional[str] = None
created_at: datetime
updated_at: datetime
# ... more fields
class TaskCreate(BaseModel):
title: str
description: Optional[str] = None
priority: int = 3
team: Optional[str] = None
parent_task_id: Optional[str] = None
acceptance_criteria: Optional[List[str]] = None
```
**Testing:**
```python
# tests/test_tasks.py
import pytest
from roboco_sdk import RobocoClient
@pytest.mark.asyncio
async def test_list_tasks():
async with RobocoClient() as client:
tasks = await client.tasks.list(status="pending")
assert isinstance(tasks, list)
```
---
### Step 1.3: Create CLI Tool (2-3 days)
**CLI using Typer:**
```python
# roboco_cli/main.py
import typer
from roboco_cli.commands import task, agent, message, config
app = typer.Typer(
name="roboco",
help="RoboCo CLI - Manage AI agent workflows"
)
app.add_typer(task.app, name="task")
app.add_typer(agent.app, name="agent")
app.add_typer(message.app, name="message")
app.add_typer(config.app, name="config")
if __name__ == "__main__":
app()
```
```python
# roboco_cli/commands/task.py
import typer
from rich.console import Console
from rich.table import Table
from roboco_sdk import RobocoClient
import asyncio
app = typer.Typer(help="Task management commands")
console = Console()
@app.command("list")
def list_tasks(
status: str = typer.Option(None, "--status", "-s"),
team: str = typer.Option(None, "--team", "-t"),
limit: int = typer.Option(20, "--limit", "-n"),
):
"""List tasks with optional filters."""
async def _list():
async with RobocoClient() as client:
return await client.tasks.list(
status=status, team=team, limit=limit
)
tasks = asyncio.run(_list())
table = Table(title="Tasks")
table.add_column("ID", style="cyan")
table.add_column("Title")
table.add_column("Status", style="green")
table.add_column("Team")
table.add_column("Assigned")
for t in tasks:
table.add_row(
t.id[:8],
t.title[:40],
t.status.value,
t.team or "-",
t.assigned_to[:8] if t.assigned_to else "-"
)
console.print(table)
@app.command("get")
def get_task(task_id: str):
"""Get task details."""
async def _get():
async with RobocoClient() as client:
return await client.tasks.get(task_id)
task = asyncio.run(_get())
console.print(task)
@app.command("claim")
def claim_task(task_id: str):
"""Claim a task."""
async def _claim():
async with RobocoClient() as client:
return await client.tasks.claim(task_id)
task = asyncio.run(_claim())
console.print(f"✓ Claimed task: {task.title}")
# ... more commands
```
**Config Management:**
```python
# roboco_cli/config.py
from pathlib import Path
import toml
CONFIG_PATH = Path.home() / ".roboco" / "config.toml"
def get_config() -> dict:
if not CONFIG_PATH.exists():
return {"api_url": "http://localhost:8000/api/v1"}
return toml.load(CONFIG_PATH)
def set_config(key: str, value: str):
config = get_config()
config[key] = value
CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
with open(CONFIG_PATH, "w") as f:
toml.dump(config, f)
```
**CLI Commands:**
```bash
# Task management
roboco task list --status pending --team backend
roboco task get abc123
roboco task claim abc123
roboco task start abc123
roboco task progress abc123 "50% done" --percent 50
# Agent info
roboco agent list
roboco agent status be-dev-1
# Messages
roboco message send --channel backend-cell --content "Hello team"
roboco message history backend-cell
# Config
roboco config set api-url https://api.roboco.io
roboco config set api-key sk-xxx
roboco config show
```
---
### Step 1.4: Package Blueprints (1 day)
**Genericize blueprints for open source:**
Current (RoboCo-specific):
```markdown
# Backend Developer 1 (be-dev-1)
You are Backend Developer 1 in the RoboCo organization...
Channel: #backend-cell
```
Generic (OSS template):
```markdown
# Developer Agent Blueprint
## Role
You are a Developer agent responsible for implementing features and fixing bugs.
## Capabilities
- Claim and work on assigned tasks
- Write and test code
- Submit work for QA review
- Communicate progress via channels
## Workflow
1. SCAN - Check for pending/assigned tasks
2. CLAIM - Lock and take ownership
3. PLAN - Break down into subtasks
4. EXECUTE - Write code, commit frequently
5. VERIFY - Self-check against acceptance criteria
6. SUBMIT - Send for QA review
## Required Tools
- roboco_task_scan
- roboco_task_claim
- roboco_task_start
- roboco_task_progress
- roboco_task_submit_qa
- roboco_message_send
- roboco_journal_entry
## Configuration
```yaml
agent:
role: developer
team: ${TEAM}
channel: ${TEAM}-cell
```
```
**Files to move:**
- `agents/blueprints/backend/be-dev.md` → `blueprints/templates/developer.md`
- `agents/blueprints/backend/be-qa.md` → `blueprints/templates/qa.md`
- `agents/blueprints/backend/be-pm.md` → `blueprints/templates/pm.md`
- `agents/blueprints/backend/be-documenter.md` →
`blueprints/templates/documenter.md`
---
### Step 1.5: Restructure Private Repo (1-2 days)
**After extraction, private repo becomes:**
```
roboco/ # PRIVATE - Commercial product
├── roboco/
│ ├── api/ # FastAPI backend (unchanged)
│ ├── services/ # Business logic (unchanged)
│ ├── runtime/ # Orchestrator (unchanged)
│ ├── mcp/ # MCP servers (unchanged)
│ ├── models/ # Domain models (unchanged)
│ ├── db/ # Database layer (unchanged)
│ └── config.py
├── agents/
│ └── blueprints/ # RoboCo-specific blueprints (keep)
│ ├── backend/
│ ├── frontend/
│ ├── ux_ui/
│ └── board/
├── deploy/ # K8s manifests (Phase 2)
│ ├── base/
│ └── overlays/
├── docker/
│ ├── api.Dockerfile
│ ├── orchestrator.Dockerfile
│ └── docker-compose.yml
├── tests/
├── pyproject.toml
└── README.md
```
**Changes needed:**
1. Remove SDK extraction from `mcp/utils.py` (keep minimal internal client)
2. Update imports if needed
3. Document private repo setup
---
## Phase 2: Kubernetes + ArgoCD (12-16 days)
### Why K8s Over Docker Compose
- **Auto-healing**: Pods restart automatically on failure
- **Scaling**: Easy horizontal scaling of API/orchestrator
- **GitOps**: ArgoCD enables push-to-deploy workflow
- **Resource limits**: Proper CPU/memory management for agents
- **Secrets management**: Sealed Secrets for git-safe secrets
- **Observability**: Built-in metrics, logs aggregation
---
### Step 2.1: Setup K8s on NAS (1-2 days)
**Prerequisites on UGREEN NAS:**
```bash
# Disable swap (required for K8s)
sudo swapoff -a
sudo sed -i '/ swap / s/^/#/' /etc/fstab
# Enable required kernel modules
cat <<EOF | sudo tee /etc/modules-load.d/k8s.conf
overlay
br_netfilter
EOF
sudo modprobe overlay
sudo modprobe br_netfilter
# Sysctl settings
cat <<EOF | sudo tee /etc/sysctl.d/k8s.conf
net.bridge.bridge-nf-call-iptables = 1
net.bridge.bridge-nf-call-ip6tables = 1
net.ipv4.ip_forward = 1
EOF
sudo sysctl --system
```
**Install kubeadm, kubelet, kubectl:**
```bash
# Add K8s apt repo
curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.29/deb/Release.key | sudo gpg
--dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg
echo 'deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg]
https://pkgs.k8s.io/core:/stable:/v1.29/deb/ /' | sudo tee
/etc/apt/sources.list.d/kubernetes.list
sudo apt-get update
sudo apt-get install -y kubelet kubeadm kubectl
sudo apt-mark hold kubelet kubeadm kubectl
```
**Initialize cluster:**
```bash
# Initialize single-node cluster
sudo kubeadm init \
--pod-network-cidr=10.244.0.0/16 \
--apiserver-advertise-address=192.168.50.111
# Setup kubectl for current user
mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config
# Allow scheduling on control plane (single-node)
kubectl taint nodes --all node-role.kubernetes.io/control-plane-
# Install Flannel CNI
kubectl apply -f
https://github.com/flannel-io/flannel/releases/latest/download/kube-flannel.yml
# Verify
kubectl get nodes
kubectl get pods -n kube-system
```
---
### Step 2.2: Install ArgoCD (1 day)
```bash
# Create namespace and install ArgoCD
kubectl create namespace argocd
kubectl apply -n argocd -f
https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml
# Wait for pods
kubectl wait --for=condition=Ready pods --all -n argocd --timeout=300s
# Get initial admin password
kubectl -n argocd get secret argocd-initial-admin-secret -o
jsonpath="{.data.password}" | base64 -d
# Expose via NodePort (for local access)
kubectl patch svc argocd-server -n argocd -p '{"spec": {"type": "NodePort"}}'
# Or use port-forward
kubectl port-forward svc/argocd-server -n argocd 8080:443
# Access at https://192.168.50.111:8080
# Username: admin, Password: (from above)
```
**Configure ArgoCD for private repo:**
```bash
# Add SSH key for repo access
argocd repo add git@github.com:renzof/roboco.git \
--ssh-private-key-path ~/.ssh/id_ed25519
# Or via UI: Settings → Repositories → Connect Repo
```
---
### Step 2.3: Setup NFS Storage (1 day)
**On NAS - Create NFS share:**
```bash
# Create data directories
sudo mkdir -p /volume1/roboco/k8s-data/{postgres,redis,qdrant}
sudo chmod 777 /volume1/roboco/k8s-data/*
# Add to /etc/exports
echo "/volume1/roboco/k8s-data *(rw,sync,no_subtree_check,no_root_squash)" |
sudo tee -a /etc/exports
sudo exportfs -ra
```
**Install NFS CSI Driver:**
```bash
# Install NFS CSI driver
helm repo add csi-driver-nfs
https://raw.githubusercontent.com/kubernetes-csi/csi-driver-nfs/master/charts
helm install csi-driver-nfs csi-driver-nfs/csi-driver-nfs \
--namespace kube-system \
--set driver.mountPermissions=0777
```
**StorageClass:**
```yaml
# deploy/base/storage/storageclass.yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: nfs-roboco
provisioner: nfs.csi.k8s.io
parameters:
server: 192.168.50.111
share: /volume1/roboco/k8s-data
reclaimPolicy: Retain
volumeBindingMode: Immediate
mountOptions:
- nfsvers=4.1
```
---
### Step 2.4: Create K8s Manifests (2-3 days)
**Full manifest structure:**
```
deploy/
├── base/
│ ├── namespace.yaml
│ ├── storage/
│ │ └── storageclass.yaml
│ ├── postgres/
│ │ ├── statefulset.yaml
│ │ ├── service.yaml
│ │ ├── pvc.yaml
│ │ └── configmap.yaml
│ ├── redis/
│ │ ├── deployment.yaml
│ │ └── service.yaml
│ ├── qdrant/
│ │ ├── statefulset.yaml
│ │ ├── service.yaml
│ │ └── pvc.yaml
│ ├── api/
│ │ ├── deployment.yaml
│ │ ├── service.yaml
│ │ └── configmap.yaml
│ ├── orchestrator/
│ │ ├── deployment.yaml
│ │ ├── serviceaccount.yaml
│ │ ├── role.yaml
│ │ ├── rolebinding.yaml
│ │ └── configmap.yaml
│ └── kustomization.yaml
├── overlays/
│ ├── production/
│ │ ├── kustomization.yaml
│ │ ├── secrets.yaml # Sealed secrets
│ │ └── patches/
│ │ └── api-replicas.yaml
│ └── development/
│ └── kustomization.yaml
└── argocd/
└── apps/
└── roboco.yaml
```
**Namespace:**
```yaml
# deploy/base/namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
name: roboco
```
**PostgreSQL StatefulSet:**
```yaml
# deploy/base/postgres/statefulset.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: postgres
namespace: roboco
spec:
serviceName: postgres
replicas: 1
selector:
matchLabels:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
containers:
- name: postgres
image: pgvector/pgvector:pg16
ports:
- containerPort: 5432
env:
- name: POSTGRES_USER
valueFrom:
secretKeyRef:
name: roboco-secrets
key: postgres-user
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: roboco-secrets
key: postgres-password
- name: POSTGRES_DB
value: roboco
volumeMounts:
- name: postgres-data
mountPath: /var/lib/postgresql/data
resources:
requests:
memory: "512Mi"
cpu: "250m"
limits:
memory: "2Gi"
cpu: "1000m"
volumeClaimTemplates:
- metadata:
name: postgres-data
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: nfs-roboco
resources:
requests:
storage: 10Gi
```
**API Deployment:**
```yaml
# deploy/base/api/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: roboco-api
namespace: roboco
spec:
replicas: 2
selector:
matchLabels:
app: roboco-api
template:
metadata:
labels:
app: roboco-api
spec:
containers:
- name: api
image: ghcr.io/renzof/roboco-api:latest
ports:
- containerPort: 8000
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: roboco-secrets
key: database-url
- name: REDIS_URL
value: redis://redis:6379
- name: QDRANT_URL
value: http://qdrant:6333
- name: ENVIRONMENT
value: production
resources:
requests:
memory: "256Mi"
cpu: "100m"
limits:
memory: "1Gi"
cpu: "500m"
livenessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 10
periodSeconds: 10
readinessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 5
periodSeconds: 5
```
**Kustomization:**
```yaml
# deploy/base/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: roboco
resources:
- namespace.yaml
- storage/storageclass.yaml
- postgres/statefulset.yaml
- postgres/service.yaml
- redis/deployment.yaml
- redis/service.yaml
- qdrant/statefulset.yaml
- qdrant/service.yaml
- api/deployment.yaml
- api/service.yaml
- orchestrator/deployment.yaml
- orchestrator/serviceaccount.yaml
- orchestrator/role.yaml
- orchestrator/rolebinding.yaml
images:
- name: ghcr.io/renzof/roboco-api
newTag: latest
- name: ghcr.io/renzof/roboco-orchestrator
newTag: latest
```
---
### Step 2.5: Orchestrator RBAC (part of 2.4)
**ServiceAccount:**
```yaml
# deploy/base/orchestrator/serviceaccount.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: roboco-orchestrator
namespace: roboco
```
**Role (for spawning agent Jobs):**
```yaml
# deploy/base/orchestrator/role.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: roboco-orchestrator
namespace: roboco
rules:
# Create/manage agent Jobs
- apiGroups: ["batch"]
resources: ["jobs"]
verbs: ["create", "delete", "get", "list", "watch", "patch"]
# Watch pods for job status
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list", "watch"]
# Read pod logs for debugging
- apiGroups: [""]
resources: ["pods/log"]
verbs: ["get"]
# Manage ConfigMaps for agent configs
- apiGroups: [""]
resources: ["configmaps"]
verbs: ["create", "delete", "get", "list"]
```
**RoleBinding:**
```yaml
# deploy/base/orchestrator/rolebinding.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: roboco-orchestrator
namespace: roboco
subjects:
- kind: ServiceAccount
name: roboco-orchestrator
namespace: roboco
roleRef:
kind: Role
name: roboco-orchestrator
apiGroup: rbac.authorization.k8s.io
```
---
### Step 2.6: Modify Orchestrator for K8s (2-3 days)
**Critical file:** `roboco/runtime/orchestrator.py`
**Current Docker approach (to replace):**
```python
# Lines ~430-500 in spawn_agent method
cmd = [
"docker", "run", "-d",
"--name", container_name,
"-e", f"AGENT_ID={agent_id}",
...
]
subprocess.run(cmd)
```
**New K8s approach:**
```python
# roboco/runtime/orchestrator.py
from kubernetes import client, config
from kubernetes.client.rest import ApiException
class AgentOrchestrator:
def __init__(self):
# Load in-cluster config (when running in K8s)
# or kubeconfig for local dev
try:
config.load_incluster_config()
except config.ConfigException:
config.load_kube_config()
self.batch_v1 = client.BatchV1Api()
self.core_v1 = client.CoreV1Api()
self.namespace = settings.k8s_namespace or "roboco"
async def spawn_agent(
self,
agent_id: str,
task_id: str,
initial_prompt: str,
) -> str:
"""Spawn an agent as a K8s Job."""
job_name = f"agent-{agent_id}-{task_id[:8]}"
# Get agent config (blueprint, role, etc.)
agent_config = self._get_agent_config(agent_id)
job = client.V1Job(
api_version="batch/v1",
kind="Job",
metadata=client.V1ObjectMeta(
name=job_name,
namespace=self.namespace,
labels={
"app": "roboco-agent",
"agent-id": agent_id,
"task-id": task_id[:8],
},
),
spec=client.V1JobSpec(
ttl_seconds_after_finished=3600, # Cleanup after 1 hour
backoff_limit=0, # No retries
template=client.V1PodTemplateSpec(
metadata=client.V1ObjectMeta(
labels={
"app": "roboco-agent",
"agent-id": agent_id,
},
),
spec=client.V1PodSpec(
restart_policy="Never",
service_account_name="roboco-agent",
containers=[
client.V1Container(
name="agent",
image=f"ghcr.io/renzof/roboco-agent:{agent_id}",
env=[
client.V1EnvVar(
name="AGENT_ID",
value=agent_id,
),
client.V1EnvVar(
name="TASK_ID",
value=task_id,
),
client.V1EnvVar(
name="INITIAL_PROMPT",
value=initial_prompt,
),
client.V1EnvVar(
name="API_URL",
value=settings.internal_api_url,
),
client.V1EnvVar(
name="ANTHROPIC_API_KEY",
value_from=client.V1EnvVarSource(
secret_key_ref=client.V1SecretKeySelector(
name="roboco-secrets",
key="anthropic-api-key",
),
),
),
],
resources=client.V1ResourceRequirements(
requests={"memory": "256Mi", "cpu": "100m"},
limits={"memory": "1Gi", "cpu": "500m"},
),
volume_mounts=[
client.V1VolumeMount(
name="blueprints",
mount_path="/app/agents/blueprints",
read_only=True,
),
],
),
],
volumes=[
client.V1Volume(
name="blueprints",
config_map=client.V1ConfigMapVolumeSource(
name="roboco-blueprints",
),
),
],
),
),
),
)
try:
self.batch_v1.create_namespaced_job(
namespace=self.namespace,
body=job,
)
logger.info(
"Spawned agent job",
job_name=job_name,
agent_id=agent_id,
task_id=task_id,
)
return job_name
except ApiException as e:
logger.error(
"Failed to spawn agent job",
error=str(e),
agent_id=agent_id,
)
raise
async def stop_agent(self, agent_id: str) -> None:
"""Stop an agent by deleting its Job."""
# Find jobs for this agent
jobs = self.batch_v1.list_namespaced_job(
namespace=self.namespace,
label_selector=f"agent-id={agent_id}",
)
for job in jobs.items:
self.batch_v1.delete_namespaced_job(
name=job.metadata.name,
namespace=self.namespace,
propagation_policy="Background",
)
logger.info("Deleted agent job", job_name=job.metadata.name)
async def get_agent_status(self, agent_id: str) -> AgentState:
"""Get agent status from Job status."""
jobs = self.batch_v1.list_namespaced_job(
namespace=self.namespace,
label_selector=f"agent-id={agent_id}",
)
if not jobs.items:
return AgentState.STOPPED
job = jobs.items[-1] # Most recent
if job.status.succeeded:
return AgentState.COMPLETED
elif job.status.failed:
return AgentState.FAILED
elif job.status.active:
return AgentState.ACTIVE
else:
return AgentState.PENDING
```
**Config changes:**
```python
# roboco/config.py
class Settings(BaseSettings):
# ... existing settings ...
# K8s settings
k8s_namespace: str = "roboco"
k8s_agent_image: str = "ghcr.io/renzof/roboco-agent"
k8s_agent_memory_request: str = "256Mi"
k8s_agent_memory_limit: str = "1Gi"
k8s_job_ttl: int = 3600 # seconds
```
---
### Step 2.7: Secrets with Sealed Secrets (1 day)
**Install kubeseal CLI:**
```bash
# macOS
brew install kubeseal
# Linux
wget https://github.com/bitnami-labs/sealed-secrets/releases/download/v0.24.5/ku
beseal-0.24.5-linux-amd64.tar.gz
tar -xvf kubeseal-*.tar.gz
sudo mv kubeseal /usr/local/bin/
```
**Install controller:**
```bash
kubectl apply -f https://github.com/bitnami-labs/sealed-secrets/releases/downloa
d/v0.24.5/controller.yaml
```
**Create secrets:**
```yaml
# secrets.yaml (NOT committed to git)
apiVersion: v1
kind: Secret
metadata:
name: roboco-secrets
namespace: roboco
type: Opaque
stringData:
postgres-user: roboco
postgres-password: your-secure-password
database-url: postgresql://roboco:your-secure-password@postgres:5432/roboco
anthropic-api-key: sk-ant-xxx
redis-url: redis://redis:6379
```
**Seal the secrets:**
```bash
kubeseal --format yaml < secrets.yaml >
deploy/overlays/production/sealed-secrets.yaml
rm secrets.yaml # Don't keep plaintext!
```
---
### Step 2.8: ArgoCD Applications (1 day)
```yaml
# deploy/argocd/apps/roboco.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: roboco
namespace: argocd
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
project: default
source:
repoURL: git@github.com:renzof/roboco.git
targetRevision: main
path: deploy/overlays/production
destination:
server: https://kubernetes.default.svc
namespace: roboco
syncPolicy:
automated:
prune: true
selfHeal: true
allowEmpty: false
syncOptions:
- CreateNamespace=true
- PrunePropagationPolicy=foreground
retry:
limit: 5
backoff:
duration: 5s
factor: 2
maxDuration: 3m
# Health checks
ignoreDifferences:
- group: apps
kind: Deployment
jsonPointers:
- /spec/replicas # Allow HPA to manage
```
**Apply:**
```bash
kubectl apply -f deploy/argocd/apps/roboco.yaml
```
---
### Step 2.9: Testing & Validation (2-3 days)
**Validation checklist:**
```bash
# 1. Verify all pods running
kubectl get pods -n roboco
# Expected: postgres-0, redis-xxx, qdrant-0, roboco-api-xxx (x2),
roboco-orchestrator-xxx
# 2. Check PostgreSQL
kubectl exec -it postgres-0 -n roboco -- psql -U roboco -c "SELECT 1"
# 3. Check Redis
kubectl exec -it deploy/redis -n roboco -- redis-cli ping
# 4. Check API health
kubectl port-forward svc/roboco-api -n roboco 8000:8000 &
curl http://localhost:8000/health
# 5. Check API endpoints
curl http://localhost:8000/api/v1/agents
curl http://localhost:8000/api/v1/tasks
# 6. Test agent spawning
# Trigger a task that spawns an agent
curl -X POST http://localhost:8000/api/v1/tasks \
-H "Content-Type: application/json" \
-d '{"title": "Test task", "team": "backend"}'
# Watch for agent job
kubectl get jobs -n roboco -w
# 7. Check ArgoCD sync
argocd app get roboco
argocd app sync roboco
# 8. Test GitOps flow
git commit --allow-empty -m "test: trigger deploy"
git push
# Watch ArgoCD sync
```
**Monitoring setup:**
```bash
# Install metrics-server
kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/late
st/download/components.yaml
# View resource usage
kubectl top pods -n roboco
kubectl top nodes
```
---
## Critical Files to Modify
### Phase 1 Files:
| File | Change |
|------|--------|
| `roboco/mcp/utils.py` | Extract `ApiClient` → SDK |
| `roboco/api/schemas/*.py` | Copy models to SDK |
| `agents/blueprints/` | Genericize and move to OSS |
### Phase 2 Files:
| File | Change |
|------|--------|
| `roboco/runtime/orchestrator.py` | Docker CLI → K8s API |
| `roboco/config.py` | Add K8s settings |
| `docker/orchestrator.Dockerfile` | Add `kubernetes` package |
| NEW: `deploy/` | All K8s manifests |
---
## Git Strategy
**Monorepo approach:**
```
roboco/ # Private repo
├── roboco/ # Python source
├── deploy/ # K8s manifests (ArgoCD watches this)
│ ├── base/
│ └── overlays/production/
└── docker/
roboco-agents/ # Public repo (separate)
├── sdk/
├── cli/
├── blueprints/
└── docs/
```
ArgoCD flow: `git push` → ArgoCD detects → auto-sync → pods updated
---
## Timeline Summary
| Phase | Task | Days |
|-------|------|------|
| **1.1** | Create OSS repo | 1 |
| **1.2** | Extract SDK | 2-3 |
| **1.3** | Create CLI | 2-3 |
| **1.4** | Package blueprints | 1 |
| **1.5** | Restructure private repo | 1-2 |
| | **Phase 1 Total** | **7-10** |
| **2.1** | Setup K8s on NAS | 1-2 |
| **2.2** | Install ArgoCD | 1 |
| **2.3** | NFS storage | 1 |
| **2.4** | K8s manifests | 2-3 |
| **2.5** | RBAC setup | (included) |
| **2.6** | Orchestrator K8s migration | 2-3 |
| **2.7** | Sealed Secrets | 1 |
| **2.8** | ArgoCD apps | 1 |
| **2.9** | Testing & validation | 2-3 |
| | **Phase 2 Total** | **12-16** |
| | **Grand Total** | **19-26 days** |
---
## Dependencies & Prerequisites
### Before Phase 1:
- [ ] GitHub account for `roboco-agents` repo
- [ ] PyPI account for publishing SDK
- [ ] Decide on license (MIT vs Apache 2.0)
### Before Phase 2:
- [ ] UGREEN NAS accessible via SSH
- [ ] NAS has Docker removed or disabled
- [ ] SSH key for GitHub access from NAS
- [ ] Anthropic API key for production
---
## Risk Mitigation
| Risk | Mitigation |
|------|------------|
| K8s learning curve | Start with minimal manifests, iterate |
| Data loss during migration | Keep Docker Compose as fallback |
| Network issues between pods | Use ClusterIP services, test connectivity |
| Secret leakage | Use Sealed Secrets from day 1 |
---
## Success Criteria
### Phase 1 Complete When:
- [ ] `pip install roboco-sdk` works from PyPI
- [ ] `roboco task list` CLI works
- [ ] Community can build agents with templates
- [ ] Private repo has no SDK dependencies
### Phase 2 Complete When:
- [ ] `kubectl get pods -n roboco` shows all healthy
- [ ] Agent jobs spawn and complete successfully
- [ ] `git push` triggers ArgoCD deploy
- [ ] API accessible from outside cluster
- [ ] Zero downtime during rolling updates
---
## Phase 3: Production Hardening
### Step 3.1: Ingress & External Access
```yaml
# deploy/base/ingress/ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: roboco-ingress
namespace: roboco
annotations:
nginx.ingress.kubernetes.io/proxy-body-size: "50m"
cert-manager.io/cluster-issuer: "letsencrypt-prod"
spec:
ingressClassName: nginx
tls:
- hosts:
- api.roboco.local
secretName: roboco-tls
rules:
- host: api.roboco.local
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: roboco-api
port:
number: 8000
```
**Install nginx ingress:**
```bash
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm install ingress-nginx ingress-nginx/ingress-nginx \
--namespace ingress-nginx --create-namespace \
--set controller.service.type=NodePort \
--set controller.service.nodePorts.http=30080 \
--set controller.service.nodePorts.https=30443
```
---
### Step 3.2: Monitoring Stack (Prometheus + Grafana)
```bash
# Install kube-prometheus-stack
helm repo add prometheus-community
https://prometheus-community.github.io/helm-charts
helm install monitoring prometheus-community/kube-prometheus-stack \
--namespace monitoring --create-namespace \
--set grafana.adminPassword=your-password \
--set prometheus.prometheusSpec.retention=30d \
--set prometheus.prometheusSpec.storageSpec.volumeClaimTemplate.spec.storageCl
assName=nfs-roboco \
--set prometheus.prometheusSpec.storageSpec.volumeClaimTemplate.spec.resources
.requests.storage=20Gi
```
**ServiceMonitor for RoboCo API:**
```yaml
# deploy/base/monitoring/servicemonitor.yaml
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: roboco-api
namespace: roboco
spec:
selector:
matchLabels:
app: roboco-api
endpoints:
- port: http
path: /metrics
interval: 30s
```
**Grafana dashboards to create:**
- API request latency & throughput
- Agent job success/failure rates
- Task lifecycle metrics
- Database connection pool
- Memory/CPU per component
---
### Step 3.3: Log Aggregation (Loki)
```bash
helm repo add grafana https://grafana.github.io/helm-charts
helm install loki grafana/loki-stack \
--namespace monitoring \
--set promtail.enabled=true \
--set loki.persistence.enabled=true \
--set loki.persistence.storageClassName=nfs-roboco \
--set loki.persistence.size=20Gi
```
**Add to Grafana datasources** for querying logs like:
```
{namespace="roboco", app="roboco-api"} |= "error"
{namespace="roboco"} | json | level="ERROR"
```
---
### Step 3.4: CI/CD Pipeline (GitHub Actions)
```yaml
# .github/workflows/build-and-push.yaml
name: Build and Push Images
on:
push:
branches: [main]
paths:
- 'roboco/**'
- 'docker/**'
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
build-api:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- name: Log in to Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push API image
uses: docker/build-push-action@v5
with:
context: .
file: docker/api.Dockerfile
push: true
tags: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-api:${{ github.sha }}
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-api:latest
- name: Update Kustomize image tag
run: |
cd deploy/overlays/production
kustomize edit set image ghcr.io/renzof/roboco-api:${{ github.sha }}
git config user.name "GitHub Actions"
git config user.email "actions@github.com"
git add .
git commit -m "chore: update image to ${{ github.sha }}"
git push
build-orchestrator:
runs-on: ubuntu-latest
# Similar to above...
build-agent:
runs-on: ubuntu-latest
# Build base agent image...
```
---
### Step 3.5: Backup & Restore
**PostgreSQL backup CronJob:**
```yaml
# deploy/base/postgres/backup-cronjob.yaml
apiVersion: batch/v1
kind: CronJob
metadata:
name: postgres-backup
namespace: roboco
spec:
schedule: "0 2 * * *" # Daily at 2 AM
jobTemplate:
spec:
template:
spec:
containers:
- name: backup
image: postgres:16
command:
- /bin/sh
- -c
- |
pg_dump -h postgres -U roboco roboco | gzip >
/backups/roboco-$(date +%Y%m%d).sql.gz
# Keep last 7 days
find /backups -name "*.sql.gz" -mtime +7 -delete
env:
- name: PGPASSWORD
valueFrom:
secretKeyRef:
name: roboco-secrets
key: postgres-password
volumeMounts:
- name: backups
mountPath: /backups
volumes:
- name: backups
persistentVolumeClaim:
claimName: postgres-backups
restartPolicy: OnFailure
```
**Restore procedure:**
```bash
# 1. Scale down API and orchestrator
kubectl scale deployment roboco-api --replicas=0 -n roboco
kubectl scale deployment roboco-orchestrator --replicas=0 -n roboco
# 2. Restore from backup
kubectl exec -it postgres-0 -n roboco -- bash -c \
"gunzip -c /backups/roboco-20241225.sql.gz | psql -U roboco roboco"
# 3. Scale back up
kubectl scale deployment roboco-api --replicas=2 -n roboco
kubectl scale deployment roboco-orchestrator --replicas=1 -n roboco
```
---
### Step 3.6: Rollback Procedures
**ArgoCD rollback:**
```bash
# View history
argocd app history roboco
# Rollback to previous version
argocd app rollback roboco <revision-number>
# Or via kubectl
kubectl rollout undo deployment/roboco-api -n roboco
kubectl rollout undo deployment/roboco-orchestrator -n roboco
```
**Emergency rollback script:**
```bash
#!/bin/bash
# scripts/emergency-rollback.sh
set -e
REVISION=${1:-1} # Default: rollback 1 revision
echo "Rolling back roboco-api..."
kubectl rollout undo deployment/roboco-api -n roboco --to-revision=$REVISION
echo "Rolling back roboco-orchestrator..."
kubectl rollout undo deployment/roboco-orchestrator -n roboco
--to-revision=$REVISION
echo "Waiting for rollout..."
kubectl rollout status deployment/roboco-api -n roboco
kubectl rollout status deployment/roboco-orchestrator -n roboco
echo "Rollback complete!"
```
---
## Transition Strategy (Docker → K8s)
### Week 1: Parallel Running
1. Deploy K8s stack alongside existing Docker Compose
2. Point K8s to SAME database (careful!)
3. Test API endpoints, verify responses match
4. Run shadow traffic (duplicate requests to both)
### Week 2: Gradual Cutover
1. Update DNS/ingress to point 10% traffic to K8s
2. Monitor error rates and latency
3. Increase to 50%, then 100%
4. Keep Docker Compose as fallback
### Week 3: Cleanup
1. Verify all agents running on K8s
2. Shut down Docker Compose services
3. Migrate remaining data if any
4. Remove Docker Compose files (keep in git history)
---
## Cost Estimation (Self-Hosted)
| Resource | Usage | Notes |
|----------|-------|-------|
| UGREEN NAS | Already owned | 36TB storage |
| Electricity | ~50W avg | ~$5/month |
| Anthropic API | Variable | Based on agent usage |
| Domain (optional) | $12/year | For external access |
| **Total** | **~$5-10/month** | Excluding API costs |
---
## What's NOT Included (Future)
- [ ] Multi-node K8s cluster (HA)
- [ ] External database (RDS/Cloud SQL)
- [ ] CDN for static assets
- [ ] Rate limiting / API gateway
- [ ] Multi-region deployment
- [ ] Disaster recovery site