From 6a8efa69c4ac65940f13d81e553d1df7a29671aa Mon Sep 17 00:00:00 2001 From: Viktor Barzin Date: Tue, 17 Feb 2026 21:09:12 +0000 Subject: [PATCH] [ci skip] Import Claude skills into OpenClaw moltbot - Convert setup-project and extend-vm-storage from standalone .md to directory-based SKILL.md format with YAML frontmatter - Add symlink in moltbot init container to expose Claude skills at ~/.openclaw/skills/ for auto-discovery by OpenClaw - Update CLAUDE.md skill path references --- .claude/CLAUDE.md | 4 +- .../SKILL.md} | 13 ++ .../SKILL.md} | 14 ++ modules/kubernetes/moltbot/main.tf | 192 ++++++++++++++---- 4 files changed, 176 insertions(+), 47 deletions(-) rename .claude/skills/{extend-vm-storage.md => extend-vm-storage/SKILL.md} (83%) rename .claude/skills/{setup-project.md => setup-project/SKILL.md} (95%) diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 8a9fac30..4e105c1f 100755 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -492,14 +492,14 @@ Skills are specialized workflows for common tasks. Located in `.claude/skills/`. ### Available Skills -**setup-project** (`.claude/skills/setup-project.md`) +**setup-project** (`.claude/skills/setup-project/SKILL.md`) - Deploy new self-hosted services from GitHub repos - Automated workflow: Docker image → Terraform module → Deploy - Handles database setup, ingress, DNS configuration - **When to use**: User provides GitHub URL or wants to deploy a new service - **Example**: "Deploy [GitHub repo] to the cluster" -**extend-vm-storage** (`.claude/skills/extend-vm-storage.md`) +**extend-vm-storage** (`.claude/skills/extend-vm-storage/SKILL.md`) - Extend disk storage on K8s node VMs (Proxmox-hosted) - Automates: drain → shutdown → resize → boot → expand filesystem → uncordon - **When to use**: A k8s node needs more disk space diff --git a/.claude/skills/extend-vm-storage.md b/.claude/skills/extend-vm-storage/SKILL.md similarity index 83% rename from .claude/skills/extend-vm-storage.md rename to .claude/skills/extend-vm-storage/SKILL.md index a994acbb..d387badf 100644 --- a/.claude/skills/extend-vm-storage.md +++ b/.claude/skills/extend-vm-storage/SKILL.md @@ -1,3 +1,16 @@ +--- +name: extend-vm-storage +description: | + Extend disk storage on a Kubernetes node VM (Proxmox-hosted). + Use when: (1) User wants to increase disk space on a k8s node VM, + (2) A node is running low on disk, (3) User says "extend storage" + or "add disk space". Automates: drain → shutdown → resize → boot → + expand filesystem → uncordon. +author: Claude Code +version: 1.0.0 +date: 2025-01-01 +--- + # Extend VM Storage Skill **Purpose**: Extend disk storage on a Kubernetes node VM (Proxmox-hosted). diff --git a/.claude/skills/setup-project.md b/.claude/skills/setup-project/SKILL.md similarity index 95% rename from .claude/skills/setup-project.md rename to .claude/skills/setup-project/SKILL.md index 1698f606..038c5196 100644 --- a/.claude/skills/setup-project.md +++ b/.claude/skills/setup-project/SKILL.md @@ -1,3 +1,17 @@ +--- +name: setup-project +description: | + Deploy a new self-hosted service to the Kubernetes cluster from a GitHub repository. + Use when: (1) User provides a GitHub URL or project name and wants to deploy it, + (2) User says "deploy [service]" or "set up [service]", + (3) User wants to add a new service to the cluster. + Automated workflow: Docker image → Terraform module → Deploy. + Handles database setup, ingress, DNS configuration. +author: Claude Code +version: 1.0.0 +date: 2025-01-01 +--- + # Setup Project Skill **Purpose**: Deploy a new self-hosted service to the Kubernetes cluster from a GitHub repository. diff --git a/modules/kubernetes/moltbot/main.tf b/modules/kubernetes/moltbot/main.tf index 9cc3e3ba..8e4725ec 100644 --- a/modules/kubernetes/moltbot/main.tf +++ b/modules/kubernetes/moltbot/main.tf @@ -1,6 +1,9 @@ variable "tls_secret_name" {} variable "tier" { type = string } variable "ssh_key" {} +variable "gemini_api_key" { type = string } +variable "llama_api_key" { type = string } +variable "brave_api_key" { type = string } resource "kubernetes_namespace" "moltbot" { metadata { @@ -66,26 +69,76 @@ resource "kubernetes_config_map" "openclaw_config" { data = { "openclaw.json" = jsonencode({ gateway = { - bind = "lan" + bind = "lan" + trustedProxies = ["10.0.0.0/8"] controlUi = { dangerouslyDisableDeviceAuth = true - allowedOrigins = ["https://moltbot.viktorbarzin.me"] + } + } + agents = { + defaults = { + contextTokens = 1000000 + bootstrapMaxChars = 30000 + model = { + primary = "gemini/gemini-2.5-flash" + fallbacks = ["llama-as-openai/Llama-4-Maverick-17B-128E-Instruct-FP8"] + } + models = { + "gemini/gemini-2.5-flash" = {} + "llama-as-openai/Llama-4-Maverick-17B-128E-Instruct-FP8" = {} + } + } + } + tools = { + profile = "full" + deny = ["sessions_spawn", "sessions_list", "sessions_history", "sessions_send", "subagents", "browser"] + exec = { + host = "sandbox" + security = "full" + ask = "off" + pathPrepend = ["/tools", "/workspace/infra"] + } + web = { + search = { + enabled = true + provider = "brave" + apiKey = var.brave_api_key + maxResults = 5 + } + fetch = { + enabled = true + maxChars = 50000 + timeoutSeconds = 30 + } } } models = { + mode = "merge" providers = { + gemini = { + baseUrl = "https://generativelanguage.googleapis.com/v1beta" + api = "google-generative-ai" + apiKey = var.gemini_api_key + models = [ + { id = "gemini-2.5-flash", name = "gemini-2.5-flash", reasoning = true, input = ["text", "image"], contextWindow = 1048576, maxTokens = 65536, cost = { input = 0, output = 0, cacheRead = 0, cacheWrite = 0 } }, + ] + } ollama = { baseUrl = "http://ollama.ollama.svc.cluster.local:11434/v1" - apiKey = "ollama-local" api = "openai-completions" - models = [ - { id = "qwen2.5:14b", name = "Qwen 2.5 14B" }, - { id = "qwen2.5-coder:14b", name = "Qwen 2.5 Coder 14B" }, - { id = "deepseek-r1:14b", name = "DeepSeek R1 14B" }, - { id = "qwen2.5:7b", name = "Qwen 2.5 7B" }, - { id = "qwen2.5-coder:7b", name = "Qwen 2.5 Coder 7B" }, - { id = "gemma2:9b", name = "Gemma 2 9B" }, - { id = "llama3.1:latest", name = "Llama 3.1 8B" }, + apiKey = "ollama" + models = [ + { id = "qwen2.5-coder:14b", name = "qwen2.5-coder:14b", reasoning = false, input = ["text"], contextWindow = 128000, maxTokens = 8192, cost = { input = 0, output = 0, cacheRead = 0, cacheWrite = 0 } }, + { id = "qwen2.5:14b", name = "qwen2.5:14b", reasoning = false, input = ["text"], contextWindow = 128000, maxTokens = 8192, cost = { input = 0, output = 0, cacheRead = 0, cacheWrite = 0 } }, + { id = "deepseek-r1:14b", name = "deepseek-r1:14b", reasoning = true, input = ["text"], contextWindow = 128000, maxTokens = 8192, cost = { input = 0, output = 0, cacheRead = 0, cacheWrite = 0 } }, + ] + } + llama-as-openai = { + baseUrl = "https://api.llama.com/compat/v1" + apiKey = var.llama_api_key + api = "openai-completions" + models = [ + { id = "Llama-4-Maverick-17B-128E-Instruct-FP8", name = "Llama-4-Maverick-17B-128E-Instruct-FP8", reasoning = false, input = ["text"], contextWindow = 200000, maxTokens = 8192, cost = { input = 0, output = 0, cacheRead = 0, cacheWrite = 0 } }, ] } } @@ -127,57 +180,86 @@ resource "kubernetes_deployment" "moltbot" { spec { service_account_name = kubernetes_service_account.moltbot.metadata[0].name - # Init container 1: Download kubectl, terraform, git-crypt to /tools + # Init container: Download tools + clone repo + terraform init (parallelized) init_container { - name = "install-tools" + name = "setup" image = "alpine:3.20" command = ["sh", "-c", <<-EOF set -e - apk add --no-cache curl unzip git-crypt - # kubectl - curl -sL "https://dl.k8s.io/release/v1.34.2/bin/linux/amd64/kubectl" -o /tools/kubectl - chmod +x /tools/kubectl - # terraform - curl -sL "https://releases.hashicorp.com/terraform/1.12.1/terraform_1.12.1_linux_amd64.zip" -o /tmp/tf.zip - unzip /tmp/tf.zip -d /tools - chmod +x /tools/terraform - # git-crypt (copy from apk install) - cp /usr/bin/git-crypt /tools/git-crypt - EOF - ] - volume_mount { - name = "tools" - mount_path = "/tools" - } - } + apk add --no-cache curl unzip git-crypt openssh-client git bash - # Init container 2: Clone infra repo, unlock git-crypt, run terraform init - init_container { - name = "clone-repo" - image = "alpine/git" - command = ["sh", "-c", <<-EOF - set -e - apk add --no-cache openssh-client bash git-crypt - export PATH="/tools:$PATH" # Copy OpenClaw config to writable home dir cp /openclaw-config-src/openclaw.json /openclaw-home/openclaw.json + # Setup SSH key mkdir -p /root/.ssh cp /ssh/id_rsa /root/.ssh/id_rsa chmod 600 /root/.ssh/id_rsa ssh-keyscan github.com >> /root/.ssh/known_hosts 2>/dev/null - # Clone repo if not already present + + # --- Run downloads and clone in parallel --- + # kubectl + (curl -sL "https://dl.k8s.io/release/v1.34.2/bin/linux/amd64/kubectl" -o /tools/kubectl && chmod +x /tools/kubectl) & + PID_KUBECTL=$! + + # terraform + (curl -sL "https://releases.hashicorp.com/terraform/1.12.1/terraform_1.12.1_linux_amd64.zip" -o /tmp/tf.zip && unzip -q /tmp/tf.zip -d /tools && chmod +x /tools/terraform && rm /tmp/tf.zip) & + PID_TF=$! + + # git-crypt (already installed via apk) + cp /usr/bin/git-crypt /tools/git-crypt + + # Clone/pull repo if [ ! -d /workspace/infra/.git ]; then - git clone git@github.com:ViktorBarzin/infra.git /workspace/infra + git clone git@github.com:ViktorBarzin/infra.git /workspace/infra & + PID_GIT=$! else - cd /workspace/infra && git pull --ff-only || true + (cd /workspace/infra && git pull --ff-only || true) & + PID_GIT=$! fi + + # Wait for all parallel tasks + wait $PID_KUBECTL || { echo "kubectl download failed"; exit 1; } + wait $PID_GIT || { echo "git clone/pull failed"; exit 1; } + + # Unlock git-crypt (needs clone done) cd /workspace/infra - # Unlock git-crypt echo "$GIT_CRYPT_KEY" | base64 -d > /tmp/git-crypt-key git-crypt unlock /tmp/git-crypt-key || true rm /tmp/git-crypt-key - # Terraform init + + # Mark repo as safe for the node user (different UID from init container) + git config --global --add safe.directory /workspace/infra + cp /root/.gitconfig /openclaw-home/.gitconfig 2>/dev/null || true + + # Symlink Claude skills into OpenClaw skills directory + ln -sfn /workspace/infra/.claude/skills /openclaw-home/skills + + # Generate kubeconfig from in-cluster ServiceAccount credentials + SA_TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token) + SA_CA=/var/run/secrets/kubernetes.io/serviceaccount/ca.crt + cat > /openclaw-home/kubeconfig <<-KUBEEOF + apiVersion: v1 + kind: Config + clusters: + - cluster: + certificate-authority-data: $(base64 < "$SA_CA" | tr -d '\n') + server: https://kubernetes.default.svc + name: in-cluster + contexts: + - context: + cluster: in-cluster + user: moltbot + name: in-cluster + current-context: in-cluster + users: + - name: moltbot + user: + token: $SA_TOKEN + KUBEEOF + + # Terraform init (needs terraform + clone done) + wait $PID_TF || { echo "terraform download failed"; exit 1; } /tools/terraform init EOF ] @@ -214,8 +296,8 @@ resource "kubernetes_deployment" "moltbot" { # Main container: OpenClaw container { - name = "moltbot" - image = "ghcr.io/openclaw/openclaw:latest" + name = "moltbot" + image = "ghcr.io/openclaw/openclaw:2026.2.9" command = ["node", "openclaw.mjs", "gateway", "--allow-unconfigured", "--bind", "lan"] port { container_port = 18789 @@ -232,6 +314,18 @@ resource "kubernetes_deployment" "moltbot" { name = "TF_VAR_prod" value = "true" } + env { + name = "KUBECONFIG" + value = "/home/node/.openclaw/kubeconfig" + } + env { + name = "GIT_CONFIG_GLOBAL" + value = "/home/node/.openclaw/.gitconfig" + } + env { + name = "GEMINI_API_KEY" + value = var.gemini_api_key + } volume_mount { name = "tools" mount_path = "/tools" @@ -252,6 +346,14 @@ resource "kubernetes_deployment" "moltbot" { name = "openclaw-home" mount_path = "/home/node/.openclaw" } + resources { + limits = { + memory = "4Gi" + } + requests = { + memory = "256Mi" + } + } } volume {