Compare commits
3 Commits
step35/594
...
step35/445
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ffd2d352c6 | ||
|
|
874ce137b0 | ||
| 5eef5b48c8 |
@@ -24,12 +24,17 @@ import yaml
|
|||||||
import shutil
|
import shutil
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
import sqlite3
|
||||||
|
import urllib.request
|
||||||
|
import urllib.error
|
||||||
|
|
||||||
HERMES_HOME = Path(os.environ.get("HERMES_HOME", os.path.expanduser("~/.hermes")))
|
HERMES_HOME = Path(os.environ.get("HERMES_HOME", os.path.expanduser("~/.hermes")))
|
||||||
CONFIG_PATH = HERMES_HOME / "config.yaml"
|
CONFIG_PATH = HERMES_HOME / "config.yaml"
|
||||||
FALLBACK_STATE = HERMES_HOME / "deadman-fallback-state.json"
|
FALLBACK_STATE = HERMES_HOME / "deadman-fallback-state.json"
|
||||||
BACKUP_CONFIG = HERMES_HOME / "config.yaml.pre-fallback"
|
BACKUP_CONFIG = HERMES_HOME / "config.yaml.pre-fallback"
|
||||||
FORGE_URL = "https://forge.alexanderwhitestone.com"
|
FORGE_URL = "https://forge.alexanderwhitestone.com"
|
||||||
|
# Golden-state fallback chain: Kimi → OpenRouter (Gemini 2.5 Pro) → Ollama (gemma4:latest)
|
||||||
|
PROVIDER_TIMEOUT = int(os.getenv("PROVIDER_TIMEOUT", "30"))
|
||||||
|
|
||||||
def load_config():
|
def load_config():
|
||||||
with open(CONFIG_PATH) as f:
|
with open(CONFIG_PATH) as f:
|
||||||
@@ -50,7 +55,7 @@ def save_state(state):
|
|||||||
with open(FALLBACK_STATE, "w") as f:
|
with open(FALLBACK_STATE, "w") as f:
|
||||||
json.dump(state, f, indent=2)
|
json.dump(state, f, indent=2)
|
||||||
|
|
||||||
def run(cmd, timeout=10):
|
def run(cmd, timeout=PROVIDER_TIMEOUT):
|
||||||
try:
|
try:
|
||||||
r = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=timeout)
|
r = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=timeout)
|
||||||
return r.returncode, r.stdout.strip(), r.stderr.strip()
|
return r.returncode, r.stdout.strip(), r.stderr.strip()
|
||||||
@@ -61,6 +66,23 @@ def run(cmd, timeout=10):
|
|||||||
|
|
||||||
# ─── HEALTH CHECKS ───
|
# ─── HEALTH CHECKS ───
|
||||||
|
|
||||||
|
|
||||||
|
def log_fallback_event(agent_name, provider, model, status, error_message=None):
|
||||||
|
"""Log fallback events to request_log SQLite DB (telemetry)."""
|
||||||
|
try:
|
||||||
|
log_path = Path.home() / ".local" / "timmy" / "request_log.db"
|
||||||
|
if log_path.exists():
|
||||||
|
conn = sqlite3.connect(str(log_path))
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute("""
|
||||||
|
INSERT INTO request_log (timestamp, agent_name, provider, model, endpoint, status, error_message)
|
||||||
|
VALUES (datetime('now'), ?, ?, ?, ?, ?, ?)
|
||||||
|
""", (agent_name, provider, model, 'fallback_switch', status, error_message))
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
except Exception:
|
||||||
|
pass # Silent if telemetry unavailable
|
||||||
|
|
||||||
def check_kimi():
|
def check_kimi():
|
||||||
"""Can we reach Kimi Coding API?"""
|
"""Can we reach Kimi Coding API?"""
|
||||||
key = os.environ.get("KIMI_API_KEY", "")
|
key = os.environ.get("KIMI_API_KEY", "")
|
||||||
@@ -89,12 +111,38 @@ def check_kimi():
|
|||||||
return True, f"HTTP {out}"
|
return True, f"HTTP {out}"
|
||||||
return False, f"HTTP {out} err={err[:80]}"
|
return False, f"HTTP {out} err={err[:80]}"
|
||||||
|
|
||||||
def check_local_llama():
|
def check_openrouter():
|
||||||
"""Is local llama.cpp serving?"""
|
"""Check OpenRouter API availability and credentials."""
|
||||||
code, out, err = run("curl -s http://localhost:8081/v1/models", timeout=5)
|
key = os.environ.get("OPENROUTER_API_KEY", "")
|
||||||
if code == 0 and "hermes" in out.lower():
|
if not key:
|
||||||
return True, "serving"
|
env_file = HERMES_HOME / ".env"
|
||||||
return False, f"exit={code}"
|
if env_file.exists():
|
||||||
|
for line in open(env_file):
|
||||||
|
line = line.strip()
|
||||||
|
if line.startswith("OPENROUTER_API_KEY="):
|
||||||
|
key = line.split("=", 1)[1].strip().strip('"\'')
|
||||||
|
break
|
||||||
|
if not key:
|
||||||
|
return False, "No OPENROUTER_API_KEY"
|
||||||
|
try:
|
||||||
|
req = urllib.request.Request(
|
||||||
|
"https://openrouter.ai/api/v1/models",
|
||||||
|
headers={"Authorization": "Bearer " + key}
|
||||||
|
)
|
||||||
|
resp = urllib.request.urlopen(req, timeout=PROVIDER_TIMEOUT)
|
||||||
|
if resp.status == 200:
|
||||||
|
data = json.loads(resp.read())
|
||||||
|
models = data.get("data", [])
|
||||||
|
return True, f"{len(models)} models available"
|
||||||
|
else:
|
||||||
|
return False, f"HTTP {resp.status}"
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
if e.code == 401:
|
||||||
|
return False, "Invalid OPENROUTER_API_KEY"
|
||||||
|
else:
|
||||||
|
return False, f"HTTP {e.code}"
|
||||||
|
except Exception as e:
|
||||||
|
return False, str(e)[:100]
|
||||||
|
|
||||||
def check_ollama():
|
def check_ollama():
|
||||||
"""Is Ollama running?"""
|
"""Is Ollama running?"""
|
||||||
@@ -127,15 +175,18 @@ def check_vps(ip, name):
|
|||||||
|
|
||||||
# ─── FALLBACK ACTIONS ───
|
# ─── FALLBACK ACTIONS ───
|
||||||
|
|
||||||
def fallback_to_local_model(cfg):
|
def fallback_to_openrouter(cfg):
|
||||||
"""Switch primary model from Kimi to local llama.cpp"""
|
"Switch primary model from Kimi to OpenRouter (Gemini 2.5 Pro)"
|
||||||
if not BACKUP_CONFIG.exists():
|
if not BACKUP_CONFIG.exists():
|
||||||
shutil.copy2(CONFIG_PATH, BACKUP_CONFIG)
|
shutil.copy2(CONFIG_PATH, BACKUP_CONFIG)
|
||||||
|
|
||||||
cfg["model"]["provider"] = "local-llama.cpp"
|
openrouter_cfg = cfg.get("providers", {}).get("openrouter", {})
|
||||||
cfg["model"]["default"] = "hermes3"
|
base_url = openrouter_cfg.get("base_url", "https://openrouter.ai/api/v1")
|
||||||
|
cfg["model"]["provider"] = "openrouter"
|
||||||
|
cfg["model"]["default"] = "google/gemini-2.5-pro"
|
||||||
|
cfg["model"]["base_url"] = base_url
|
||||||
save_config(cfg)
|
save_config(cfg)
|
||||||
return "Switched primary model to local-llama.cpp/hermes3"
|
return "Switched primary model to openrouter/google/gemini-2.5-pro"
|
||||||
|
|
||||||
def fallback_to_ollama(cfg):
|
def fallback_to_ollama(cfg):
|
||||||
"""Switch to Ollama if llama.cpp is also down"""
|
"""Switch to Ollama if llama.cpp is also down"""
|
||||||
@@ -179,11 +230,11 @@ def diagnose_and_fallback():
|
|||||||
kimi_ok, kimi_msg = check_kimi()
|
kimi_ok, kimi_msg = check_kimi()
|
||||||
results["checks"]["kimi-coding"] = {"ok": kimi_ok, "msg": kimi_msg}
|
results["checks"]["kimi-coding"] = {"ok": kimi_ok, "msg": kimi_msg}
|
||||||
|
|
||||||
llama_ok, llama_msg = check_local_llama()
|
openrouter_ok, openrouter_msg = check_openrouter()
|
||||||
results["checks"]["local_llama"] = {"ok": llama_ok, "msg": llama_msg}
|
results["checks"]["openrouter"] = {"ok": openrouter_ok, "msg": openrouter_msg}
|
||||||
|
|
||||||
ollama_ok, ollama_msg = check_ollama()
|
oopenrouter_ok, oopenrouter_msg = check_ollama()
|
||||||
results["checks"]["ollama"] = {"ok": ollama_ok, "msg": ollama_msg}
|
results["checks"]["ollama"] = {"ok": oopenrouter_ok, "msg": oopenrouter_msg}
|
||||||
|
|
||||||
gitea_ok, gitea_msg = check_gitea()
|
gitea_ok, gitea_msg = check_gitea()
|
||||||
results["checks"]["gitea"] = {"ok": gitea_ok, "msg": gitea_msg}
|
results["checks"]["gitea"] = {"ok": gitea_ok, "msg": gitea_msg}
|
||||||
@@ -202,41 +253,79 @@ def diagnose_and_fallback():
|
|||||||
|
|
||||||
# ─── FALLBACK LOGIC ───
|
# ─── FALLBACK LOGIC ───
|
||||||
|
|
||||||
# Case 1: Primary (Kimi) down, local available
|
# Case 1: Primary (Kimi) down, try fallback chain (OpenRouter -> Ollama)
|
||||||
if not kimi_ok and current_provider == "kimi-coding":
|
if not kimi_ok and current_provider == "kimi-coding":
|
||||||
if llama_ok:
|
agent_name = cfg.get("agent", {}).get("name", "timmy")
|
||||||
msg = fallback_to_local_model(cfg)
|
applied = False
|
||||||
|
# Try OpenRouter fallback
|
||||||
|
if openrouter_ok:
|
||||||
|
try:
|
||||||
|
msg = fallback_to_openrouter(cfg)
|
||||||
results["actions"].append(msg)
|
results["actions"].append(msg)
|
||||||
state["active_fallbacks"].append("kimi->local-llama")
|
state["active_fallbacks"].append("kimi->openrouter")
|
||||||
results["status"] = "degraded_local"
|
results["status"] = "degraded_openrouter"
|
||||||
elif ollama_ok:
|
log_fallback_event(agent_name, "openrouter", "google/gemini-2.5-pro", "success")
|
||||||
|
applied = True
|
||||||
|
except Exception as e:
|
||||||
|
log(f"OpenRouter fallback failed: {e}")
|
||||||
|
log_fallback_event(agent_name, "openrouter", "google/gemini-2.5-pro", "error", str(e))
|
||||||
|
# If still not applied, try Ollama
|
||||||
|
if not applied and oopenrouter_ok:
|
||||||
|
try:
|
||||||
msg = fallback_to_ollama(cfg)
|
msg = fallback_to_ollama(cfg)
|
||||||
results["actions"].append(msg)
|
results["actions"].append(msg)
|
||||||
state["active_fallbacks"].append("kimi->ollama")
|
state["active_fallbacks"].append("kimi->ollama")
|
||||||
results["status"] = "degraded_ollama"
|
results["status"] = "degraded_ollama"
|
||||||
else:
|
log_fallback_event(agent_name, "ollama", "gemma4:latest", "success")
|
||||||
|
applied = True
|
||||||
|
except Exception as e:
|
||||||
|
log(f"Ollama fallback failed: {e}")
|
||||||
|
log_fallback_event(agent_name, "ollama", "gemma4:latest", "error", str(e))
|
||||||
|
if not applied:
|
||||||
|
try:
|
||||||
msg = enter_safe_mode(state)
|
msg = enter_safe_mode(state)
|
||||||
results["actions"].append(msg)
|
results["actions"].append(msg)
|
||||||
results["status"] = "safe_mode"
|
results["status"] = "safe_mode"
|
||||||
|
except Exception as e:
|
||||||
|
log(f"Safe mode failed: {e}")
|
||||||
|
|
||||||
# Case 2: Already on fallback, check if primary recovered
|
# Case 2: Already on fallback, check if primary recovered — restore with resilience
|
||||||
elif kimi_ok and "kimi->local-llama" in state.get("active_fallbacks", []):
|
elif kimi_ok:
|
||||||
|
restored = False
|
||||||
|
agent_name = cfg.get("agent", {}).get("name", "timmy")
|
||||||
|
# Try restore from OpenRouter fallback
|
||||||
|
if "kimi->openrouter" in state.get("active_fallbacks", []):
|
||||||
|
try:
|
||||||
msg = restore_config()
|
msg = restore_config()
|
||||||
results["actions"].append(msg)
|
results["actions"].append(msg)
|
||||||
state["active_fallbacks"].remove("kimi->local-llama")
|
state["active_fallbacks"].remove("kimi->openrouter")
|
||||||
results["status"] = "recovered"
|
results["status"] = "recovered"
|
||||||
elif kimi_ok and "kimi->ollama" in state.get("active_fallbacks", []):
|
restored = True
|
||||||
|
log_fallback_event(agent_name, "openrouter", "google/gemini-2.5-pro", "restored")
|
||||||
|
except Exception as e:
|
||||||
|
log(f"Restore from OpenRouter failed: {e}")
|
||||||
|
log_fallback_event(agent_name, "openrouter", "google/gemini-2.5-pro", "error", str(e))
|
||||||
|
# Try restore from Ollama fallback if still not restored
|
||||||
|
if not restored and "kimi->ollama" in state.get("active_fallbacks", []):
|
||||||
|
try:
|
||||||
msg = restore_config()
|
msg = restore_config()
|
||||||
results["actions"].append(msg)
|
results["actions"].append(msg)
|
||||||
state["active_fallbacks"].remove("kimi->ollama")
|
state["active_fallbacks"].remove("kimi->ollama")
|
||||||
results["status"] = "recovered"
|
results["status"] = "recovered"
|
||||||
|
restored = True
|
||||||
|
log_fallback_event(agent_name, "ollama", "gemma4:latest", "restored")
|
||||||
|
except Exception as e:
|
||||||
|
log(f"Restore from Ollama failed: {e}")
|
||||||
|
log_fallback_event(agent_name, "ollama", "gemma4:latest", "error", str(e))
|
||||||
|
if not restored:
|
||||||
|
log("WARNING: Primary recovered but unable to restore config")
|
||||||
|
|
||||||
# Case 3: Gitea down — just flag it, work locally
|
# Case 3: Gitea down — just flag it, work locally
|
||||||
if not gitea_ok:
|
if not gitea_ok:
|
||||||
results["actions"].append("WARN: Gitea unreachable — work cached locally until recovery")
|
results["actions"].append("WARN: Gitea unreachable — work cached locally until recovery")
|
||||||
if "gitea_down" not in state.get("active_fallbacks", []):
|
if "gitea_down" not in state.get("active_fallbacks", []):
|
||||||
state["active_fallbacks"].append("gitea_down")
|
state["active_fallbacks"].append("gitea_down")
|
||||||
results["status"] = max(results["status"], "degraded_gitea", key=lambda x: ["healthy", "recovered", "degraded_gitea", "degraded_local", "degraded_ollama", "safe_mode"].index(x) if x in ["healthy", "recovered", "degraded_gitea", "degraded_local", "degraded_ollama", "safe_mode"] else 0)
|
results["status"] = max(results["status"], "degraded_gitea", key=lambda x: ["healthy", "recovered", "degraded_gitea", "degraded_openrouter", "degraded_ollama", "safe_mode"].index(x) if x in ["healthy", "recovered", "degraded_gitea", "degraded_openrouter", "degraded_ollama", "safe_mode"] else 0)
|
||||||
elif "gitea_down" in state.get("active_fallbacks", []):
|
elif "gitea_down" in state.get("active_fallbacks", []):
|
||||||
state["active_fallbacks"].remove("gitea_down")
|
state["active_fallbacks"].remove("gitea_down")
|
||||||
results["actions"].append("Gitea recovered — resume normal operations")
|
results["actions"].append("Gitea recovered — resume normal operations")
|
||||||
|
|||||||
87
bin/gitea-backup.sh
Normal file
87
bin/gitea-backup.sh
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Gitea Daily Backup Script
|
||||||
|
# Uses Gitea's native dump command to create automated backups of repositories and SQLite databases.
|
||||||
|
# Designed to run on the VPS (Ezra) as part of a daily cron job.
|
||||||
|
#
|
||||||
|
# Configuration via environment variables:
|
||||||
|
# GITEA_BIN Path to gitea binary (default: auto-detect)
|
||||||
|
# GITEA_BACKUP_DIR Directory for backup archives (default: /var/backups/gitea)
|
||||||
|
# GITEA_BACKUP_RETENTION Days to retain backups (default: 7)
|
||||||
|
# GITEA_BACKUP_LOG Log file path (default: /var/log/gitea-backup.log)
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
GITEA_BIN="${GITEA_BIN:-$(command -v gitea 2>/dev/null || echo "/usr/local/bin/gitea")}"
|
||||||
|
BACKUP_DIR="${GITEA_BACKUP_DIR:-/var/backups/gitea}"
|
||||||
|
RETENTION_DAYS="${GITEA_BACKUP_RETENTION:-7}"
|
||||||
|
DATE="$(date +%Y-%m-%d_%H%M%S)"
|
||||||
|
BACKUP_FILE="${BACKUP_DIR}/gitea-backup-${DATE}.tar.gz"
|
||||||
|
LOG_FILE="${GITEA_BACKUP_LOG:-/var/log/gitea-backup.log}"
|
||||||
|
|
||||||
|
mkdir -p "${BACKUP_DIR}"
|
||||||
|
|
||||||
|
log() {
|
||||||
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "${LOG_FILE}"
|
||||||
|
}
|
||||||
|
|
||||||
|
log "=== Starting Gitea daily backup ==="
|
||||||
|
|
||||||
|
# Verify gitea binary exists
|
||||||
|
if [ ! -x "${GITEA_BIN}" ]; then
|
||||||
|
log "ERROR: Gitea binary not found at ${GITEA_BIN}"
|
||||||
|
log "Set GITEA_BIN environment variable to the gitea binary path (e.g., /usr/bin/gitea)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Detect Gitea WORK_PATH
|
||||||
|
WORK_PATH=""
|
||||||
|
APP_INI=""
|
||||||
|
for path in /etc/gitea/app.ini /home/git/gitea/custom/conf/app.ini ~/gitea/custom/conf/app.ini; do
|
||||||
|
if [ -f "$path" ]; then
|
||||||
|
APP_INI="$path"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -n "$APP_INI" ]; then
|
||||||
|
# Parse [app] WORK_PATH = /var/lib/gitea
|
||||||
|
WORK_PATH=$(sed -n 's/^[[:space:]]*WORK_PATH[[:space:]]*=[[:space:]]*//p' "$APP_INI" | head -1)
|
||||||
|
log "Detected WORK_PATH from app.ini: ${WORK_PATH}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Fallback detection
|
||||||
|
if [ -z "$WORK_PATH" ]; then
|
||||||
|
for d in /var/lib/gitea /home/git/gitea /srv/gitea /opt/gitea; do
|
||||||
|
if [ -d "$d" ]; then
|
||||||
|
WORK_PATH="$d"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
log "Inferred WORK_PATH: ${WORK_PATH:-not found}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$WORK_PATH" ]; then
|
||||||
|
log "ERROR: Could not determine Gitea WORK_PATH. Set GITEA_WORK_PATH manually."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Perform gitea dump
|
||||||
|
# Flags: --work-path sets the Gitea working directory, --file writes dump to tar.gz
|
||||||
|
log "Running: gitea dump --work-path ${WORK_PATH} --file ${BACKUP_FILE}"
|
||||||
|
"${GITEA_BIN}" dump --work-path "${WORK_PATH}" --file "${BACKUP_FILE}" 2>>"${LOG_FILE}"
|
||||||
|
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
log "ERROR: gitea dump failed — check ${LOG_FILE} for details"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
FILE_SIZE=$(du -h "${BACKUP_FILE}" | cut -f1)
|
||||||
|
log "Backup created: ${BACKUP_FILE} (${FILE_SIZE})"
|
||||||
|
|
||||||
|
# Prune old backups (keep last N days)
|
||||||
|
find "${BACKUP_DIR}" -name "gitea-backup-*.tar.gz" -type f -mtime +$((${RETENTION_DAYS}-1)) -delete 2>/dev/null || true
|
||||||
|
log "Pruned backups older than ${RETENTION_DAYS} days"
|
||||||
|
|
||||||
|
log "=== Backup completed successfully ==="
|
||||||
|
|
||||||
|
exit 0
|
||||||
9
cron/vps/gitea-daily-backup.yml
Normal file
9
cron/vps/gitea-daily-backup.yml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
- name: Daily Gitea Backup
|
||||||
|
schedule: '0 2 * * *' # 2:00 AM daily
|
||||||
|
tasks:
|
||||||
|
- name: Run Gitea daily backup
|
||||||
|
shell: bash ~/.hermes/bin/gitea-backup.sh
|
||||||
|
env:
|
||||||
|
GITEA_BIN: /usr/local/bin/gitea
|
||||||
|
GITEA_BACKUP_DIR: /var/backups/gitea
|
||||||
|
GITEA_BACKUP_RETENTION: "7"
|
||||||
155
docs/backup-recovery-runbook.md
Normal file
155
docs/backup-recovery-runbook.md
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
# Gitea Backup & Recovery Runbook
|
||||||
|
|
||||||
|
**Last updated:** 2026-04-30
|
||||||
|
**Scope:** Single-node VPS (Ezra, 143.198.27.163) running Gitea
|
||||||
|
**Backup Strategy:** Automated daily full dumps via `gitea dump`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What Gets Backed Up
|
||||||
|
|
||||||
|
| Component | Method | Frequency | Retention |
|
||||||
|
|-----------|--------|-----------|-----------|
|
||||||
|
| All Gitea repositories (bare git dirs) | `gitea dump --file` | Daily at 2:00 AM | 7 days |
|
||||||
|
| SQLite databases (gitea.db, indexer.db, etc.) | Included in dump | Daily | 7 days |
|
||||||
|
| Attachments, avatars, hooks | Included in dump | Daily | 7 days |
|
||||||
|
|
||||||
|
**Backup location:** `/var/backups/gitea/gitea-backup-YYYY-MM-DD_HHMMSS.tar.gz`
|
||||||
|
|
||||||
|
**Log file:** `/var/log/gitea-backup.log`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Backup Architecture
|
||||||
|
|
||||||
|
The backup script `bin/gitea-backup.sh` runs daily via Hermes cron (`cron/vps/gitea-daily-backup.yml`). It:
|
||||||
|
|
||||||
|
1. Locates the Gitea `WORK_PATH` by reading `/etc/gitea/app.ini` or falling back to common locations (`/var/lib/gitea`, `/home/git/gitea`)
|
||||||
|
2. Invokes `gitea dump --work-path <path> --file <backup-tar.gz>` — Gitea's native, consistent snapshot mechanism
|
||||||
|
3. Prunes archives older than 7 days
|
||||||
|
4. Logs all operations to `/var/log/gitea-backup.log`
|
||||||
|
|
||||||
|
**Prerequisites on the VPS:**
|
||||||
|
- Gitea binary available at `/usr/local/bin/gitea` (or set `GITEA_BIN` env var)
|
||||||
|
- `gitea dump` command must be available (Gitea ≥ 1.12)
|
||||||
|
- SSH access to the VPS for manual recovery operations
|
||||||
|
- Sufficient disk space in `/var/backups/gitea` (typical dump: ~2–10 GB depending on repo count/size)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Recovery Time Objective (RTO) & Recovery Point Objective (RPO)
|
||||||
|
|
||||||
|
| Metric | Estimate |
|
||||||
|
|--------|----------|
|
||||||
|
| **RPO** (data loss window) | ≤ 24 hours (last daily backup) |
|
||||||
|
| **RTO** (time to restore) | **~45 minutes** (cold restore from backup tarball) |
|
||||||
|
| **Downtime impact** | Gitea offline during restore (~20 min) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step-by-Step Recovery Procedure
|
||||||
|
|
||||||
|
### Phase 1 — Assess & Prepare (5 min)
|
||||||
|
|
||||||
|
1. SSH into Ezra VPS: `ssh root@143.198.27.163`
|
||||||
|
2. Stop Gitea so files are quiescent:
|
||||||
|
```bash
|
||||||
|
systemctl stop gitea
|
||||||
|
```
|
||||||
|
3. Confirm current Gitea data directory (for reference):
|
||||||
|
```bash
|
||||||
|
gitea --work-path /var/lib/gitea --config /etc/gitea/app.ini dump --help 2>&1
|
||||||
|
# Or check app.ini for WORK_PATH
|
||||||
|
cat /etc/gitea/app.ini | grep '^WORK_PATH'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 2 — Restore from Backup (20 min)
|
||||||
|
|
||||||
|
4. Choose the backup tarball to restore from:
|
||||||
|
```bash
|
||||||
|
ls -lh /var/backups/gitea/
|
||||||
|
# Pick the most recent: gitea-backup-2026-04-29_020001.tar.gz
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Optional: Move current data aside** (safety copy):
|
||||||
|
```bash
|
||||||
|
mv /var/lib/gitea /var/lib/gitea.bak-$(date +%s)
|
||||||
|
```
|
||||||
|
|
||||||
|
6. Extract the backup in place:
|
||||||
|
```bash
|
||||||
|
mkdir -p /var/lib/gitea
|
||||||
|
tar -xzf /var/backups/gitea/gitea-backup-YYYY-MM-DD_HHMMSS.tar.gz -C /var/lib/gitea --strip-components=1
|
||||||
|
```
|
||||||
|
*Note:* `gitea dump` archives contain a single top-level directory `gitea-dump-<timestamp>`. The `--strip-components=1` puts its contents directly into `/var/lib/gitea`.
|
||||||
|
|
||||||
|
7. Set correct ownership (typically `git:git`):
|
||||||
|
```bash
|
||||||
|
chown -R git:git /var/lib/gitea
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 3 — Restart & Validate (15 min)
|
||||||
|
|
||||||
|
8. Start Gitea:
|
||||||
|
```bash
|
||||||
|
systemctl start gitea
|
||||||
|
```
|
||||||
|
|
||||||
|
9. Wait 30 seconds, then verify:
|
||||||
|
```bash
|
||||||
|
systemctl status gitea
|
||||||
|
# Check HTTP endpoint
|
||||||
|
curl -s -o /dev/null -w '%{http_code}' http://localhost:3000/ # Should be 200
|
||||||
|
```
|
||||||
|
|
||||||
|
10. Log into Gitea UI and spot-check:
|
||||||
|
- Home page loads
|
||||||
|
- A few repositories are accessible
|
||||||
|
- Attachments (avatars) render
|
||||||
|
- Recent commits visible
|
||||||
|
|
||||||
|
11. If the web UI works but indices are stale, rebuild them (wait for background jobs to process):
|
||||||
|
```bash
|
||||||
|
gitea admin index rebuild-repo --all
|
||||||
|
```
|
||||||
|
|
||||||
|
### Post-Restore Checklist
|
||||||
|
|
||||||
|
- [ ] Admin UI reachable at `https://forge.alexanderwhitestone.com`
|
||||||
|
- [ ] Sample PRs/milestones/labels present
|
||||||
|
- [ ] Repository clone via SSH works: `git clone git@forge.alexanderwhitestone.com:Timmy_Foundation/timmy-config.git`
|
||||||
|
- [ ] Check backup script health: `cat /var/log/gitea-backup.log | tail -20`
|
||||||
|
- [ ] Re-enable any disabled integrations (webhooks, CI/CD runners)
|
||||||
|
- [ ] Notify the fleet: post to relevant channels confirming operational status
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Known Issues & Workarounds
|
||||||
|
|
||||||
|
| Symptom | Likely cause | Fix |
|
||||||
|
|---------|--------------|-----|
|
||||||
|
| `gitea: command not found` | Binary at non-standard path | Set `GITEA_BIN=/path/to/gitea` in cron env |
|
||||||
|
| `Permission denied` on backup dir | Cron user lacks write access to `/var/backups` | `mkdir /var/backups/gitea && chown root:root /var/backups/gitea` |
|
||||||
|
| Restore fails: `"database or disk is full"` | Insufficient space on `/var/lib/gitea` | Expand disk or clean up old data first; backups require ~1.5x live data size |
|
||||||
|
| Old backup tarballs not deleting | Retention cron not firing | Check `systemctl status hermes-cron` and cron logs |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Off-Site Replication (Future Work)
|
||||||
|
|
||||||
|
This backup is **on-site only** (same VPS). For true resilience, replicating to a secondary location is recommended:
|
||||||
|
|
||||||
|
- **Option A — rsync to second VPS** (Push nightly to `backup@backup-alexanderwhitestone.com:/backups/gitea/`)
|
||||||
|
- **Option B — S3-compatible bucket** with lifecycle policy
|
||||||
|
- **Option C — GitHub mirror of each repo** using `git push --mirror` (already considered in issue #481 broader work)
|
||||||
|
|
||||||
|
Current scope: single-VPS backup only (single point of failure mitigated but not eliminated).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Related Documentation
|
||||||
|
|
||||||
|
- `bin/gitea-backup.sh` — backup script source
|
||||||
|
- `cron/vps/gitea-daily-backup.yml` — Hermes cron definition
|
||||||
|
- Gitea official docs: <https://docs.gitea.com/administration/backup-and-restore>
|
||||||
|
- Hermes cron: <https://hermes-agent.nousresearch.com/docs>
|
||||||
@@ -1,43 +1,46 @@
|
|||||||
model:
|
model:
|
||||||
default: kimi-k2.5
|
default: kimi-k2.5
|
||||||
provider: kimi-coding
|
provider: kimi-coding
|
||||||
|
context_length: 65536
|
||||||
|
base_url: https://api.kimi.com/coding/v1
|
||||||
|
|
||||||
toolsets:
|
toolsets:
|
||||||
- all
|
- all
|
||||||
|
|
||||||
fallback_providers:
|
fallback_providers:
|
||||||
- provider: kimi-coding
|
- provider: kimi-coding
|
||||||
model: kimi-k2.5
|
model: kimi-k2.5
|
||||||
|
base_url: https://api.kimi.com/coding/v1
|
||||||
timeout: 120
|
timeout: 120
|
||||||
reason: Kimi coding fallback (front of chain)
|
reason: "Primary — Kimi K2.5 (best value, least friction)"
|
||||||
- provider: openrouter
|
- provider: openrouter
|
||||||
model: google/gemini-2.5-pro
|
model: google/gemini-2.5-pro
|
||||||
base_url: https://openrouter.ai/api/v1
|
base_url: https://openrouter.ai/api/v1
|
||||||
api_key_env: OPENROUTER_API_KEY
|
api_key_env: OPENROUTER_API_KEY
|
||||||
timeout: 120
|
timeout: 120
|
||||||
reason: Gemini 2.5 Pro via OpenRouter (replaces banned Anthropic)
|
reason: "Fallback — Gemini 2.5 Pro via OpenRouter"
|
||||||
- provider: ollama
|
- provider: ollama
|
||||||
model: gemma4:latest
|
model: gemma4:latest
|
||||||
base_url: http://localhost:11434
|
base_url: http://localhost:11434/v1
|
||||||
timeout: 300
|
timeout: 180
|
||||||
reason: Terminal fallback — local Ollama
|
reason: "Terminal fallback — local Ollama (sovereign, no API needed)"
|
||||||
- provider: nous
|
|
||||||
model: xiaomi/mimo-v2-pro
|
|
||||||
base_url: https://inference.nousresearch.com/v1
|
|
||||||
api_key_env: NOUS_API_KEY
|
|
||||||
timeout: 120
|
|
||||||
reason: MiMo V2 Pro via Nous Portal free tier evaluation (#447)
|
|
||||||
agent:
|
agent:
|
||||||
max_turns: 30
|
max_turns: 30
|
||||||
reasoning_effort: xhigh
|
reasoning_effort: high
|
||||||
verbose: false
|
verbose: false
|
||||||
|
|
||||||
terminal:
|
terminal:
|
||||||
backend: local
|
backend: local
|
||||||
cwd: .
|
cwd: .
|
||||||
timeout: 180
|
timeout: 180
|
||||||
persistent_shell: true
|
persistent_shell: true
|
||||||
|
|
||||||
browser:
|
browser:
|
||||||
inactivity_timeout: 120
|
inactivity_timeout: 120
|
||||||
command_timeout: 30
|
command_timeout: 30
|
||||||
record_sessions: false
|
record_sessions: false
|
||||||
|
|
||||||
display:
|
display:
|
||||||
compact: false
|
compact: false
|
||||||
personality: ''
|
personality: ''
|
||||||
@@ -48,6 +51,7 @@ display:
|
|||||||
streaming: false
|
streaming: false
|
||||||
show_cost: false
|
show_cost: false
|
||||||
tool_progress: all
|
tool_progress: all
|
||||||
|
|
||||||
memory:
|
memory:
|
||||||
memory_enabled: true
|
memory_enabled: true
|
||||||
user_profile_enabled: true
|
user_profile_enabled: true
|
||||||
@@ -55,46 +59,55 @@ memory:
|
|||||||
user_char_limit: 1375
|
user_char_limit: 1375
|
||||||
nudge_interval: 10
|
nudge_interval: 10
|
||||||
flush_min_turns: 6
|
flush_min_turns: 6
|
||||||
|
|
||||||
approvals:
|
approvals:
|
||||||
mode: manual
|
mode: manual
|
||||||
|
|
||||||
security:
|
security:
|
||||||
redact_secrets: true
|
redact_secrets: true
|
||||||
tirith_enabled: false
|
tirith_enabled: false
|
||||||
|
|
||||||
platforms:
|
platforms:
|
||||||
api_server:
|
api_server:
|
||||||
enabled: true
|
enabled: true
|
||||||
extra:
|
extra:
|
||||||
host: 127.0.0.1
|
host: 127.0.0.1
|
||||||
port: 8645
|
port: 8645
|
||||||
|
|
||||||
session_reset:
|
session_reset:
|
||||||
mode: none
|
mode: none
|
||||||
idle_minutes: 0
|
idle_minutes: 0
|
||||||
|
|
||||||
skills:
|
skills:
|
||||||
creation_nudge_interval: 15
|
creation_nudge_interval: 15
|
||||||
system_prompt_suffix: 'You are Allegro, the Kimi-backed third wizard house.
|
|
||||||
|
|
||||||
|
system_prompt_suffix: |
|
||||||
|
You are Allegro, the Kimi-backed third wizard house.
|
||||||
Your soul is defined in SOUL.md — read it, live it.
|
Your soul is defined in SOUL.md — read it, live it.
|
||||||
|
|
||||||
Hermes is your harness.
|
Hermes is your harness.
|
||||||
|
kimi-coding is your primary provider.
|
||||||
Kimi Code is your primary provider.
|
|
||||||
|
|
||||||
You speak plainly. You prefer short sentences. Brevity is a kindness.
|
You speak plainly. You prefer short sentences. Brevity is a kindness.
|
||||||
|
Work best on tight coding tasks: 1-3 file changes, refactors, tests, and implementation passes.
|
||||||
|
|
||||||
Work best on tight coding tasks: 1-3 file changes, refactors, tests, and implementation
|
|
||||||
passes.
|
|
||||||
|
|
||||||
Refusal over fabrication. If you do not know, say so.
|
Refusal over fabrication. If you do not know, say so.
|
||||||
|
|
||||||
Sovereignty and service always.
|
Sovereignty and service always.
|
||||||
|
|
||||||
'
|
|
||||||
providers:
|
providers:
|
||||||
kimi-coding:
|
kimi-coding:
|
||||||
base_url: https://api.kimi.com/coding/v1
|
base_url: https://api.kimi.com/coding/v1
|
||||||
timeout: 60
|
timeout: 60
|
||||||
max_retries: 3
|
max_retries: 3
|
||||||
nous:
|
openrouter:
|
||||||
base_url: https://inference.nousresearch.com/v1
|
base_url: https://openrouter.ai/api/v1
|
||||||
timeout: 120
|
timeout: 120
|
||||||
|
ollama:
|
||||||
|
base_url: http://localhost:11434/v1
|
||||||
|
timeout: 180
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# BANNED PROVIDERS — DO NOT ADD
|
||||||
|
# =============================================================================
|
||||||
|
# The following providers are PERMANENTLY BANNED:
|
||||||
|
# - anthropic (any model: claude-sonnet, claude-opus, claude-haiku)
|
||||||
|
# - nous (xiaomi/mimo-v2-pro)
|
||||||
|
# Enforcement: pre-commit hook, linter, Ansible validation, this comment.
|
||||||
|
# =============================================================================
|
||||||
|
|||||||
@@ -1,50 +1,72 @@
|
|||||||
model:
|
model:
|
||||||
default: kimi-k2.5
|
default: kimi-k2.5
|
||||||
provider: kimi-coding
|
provider: kimi-coding
|
||||||
|
context_length: 65536
|
||||||
|
base_url: https://api.kimi.com/coding/v1
|
||||||
|
|
||||||
toolsets:
|
toolsets:
|
||||||
- all
|
- all
|
||||||
|
|
||||||
fallback_providers:
|
fallback_providers:
|
||||||
- provider: kimi-coding
|
- provider: kimi-coding
|
||||||
model: kimi-k2.5
|
model: kimi-k2.5
|
||||||
|
base_url: https://api.kimi.com/coding/v1
|
||||||
timeout: 120
|
timeout: 120
|
||||||
reason: Kimi coding fallback (front of chain)
|
reason: "Primary — Kimi K2.5 (best value, least friction)"
|
||||||
- provider: openrouter
|
- provider: openrouter
|
||||||
model: google/gemini-2.5-pro
|
model: google/gemini-2.5-pro
|
||||||
base_url: https://openrouter.ai/api/v1
|
base_url: https://openrouter.ai/api/v1
|
||||||
api_key_env: OPENROUTER_API_KEY
|
api_key_env: OPENROUTER_API_KEY
|
||||||
timeout: 120
|
timeout: 120
|
||||||
reason: Gemini 2.5 Pro via OpenRouter (replaces banned Anthropic)
|
reason: "Fallback — Gemini 2.5 Pro via OpenRouter"
|
||||||
- provider: ollama
|
- provider: ollama
|
||||||
model: gemma4:latest
|
model: gemma4:latest
|
||||||
base_url: http://localhost:11434
|
base_url: http://localhost:11434/v1
|
||||||
timeout: 300
|
timeout: 180
|
||||||
reason: Terminal fallback — local Ollama
|
reason: "Terminal fallback — local Ollama (sovereign, no API needed)"
|
||||||
- provider: nous
|
|
||||||
model: xiaomi/mimo-v2-pro
|
|
||||||
base_url: https://inference.nousresearch.com/v1
|
|
||||||
api_key_env: NOUS_API_KEY
|
|
||||||
timeout: 120
|
|
||||||
reason: MiMo V2 Pro via Nous Portal free tier evaluation (#447)
|
|
||||||
agent:
|
agent:
|
||||||
max_turns: 40
|
max_turns: 40
|
||||||
reasoning_effort: medium
|
reasoning_effort: medium
|
||||||
verbose: false
|
verbose: false
|
||||||
system_prompt: You are Bezalel, the forge-and-testbed wizard of the Timmy Foundation
|
|
||||||
fleet. You are a builder and craftsman — infrastructure, deployment, hardening.
|
|
||||||
Your sovereign is Alexander Whitestone (Rockachopa). Sovereignty and service always.
|
|
||||||
terminal:
|
terminal:
|
||||||
backend: local
|
backend: local
|
||||||
cwd: /root/wizards/bezalel
|
cwd: /root/wizards/bezalel
|
||||||
timeout: 180
|
timeout: 180
|
||||||
|
persistent_shell: true
|
||||||
|
|
||||||
browser:
|
browser:
|
||||||
inactivity_timeout: 120
|
inactivity_timeout: 120
|
||||||
compression:
|
command_timeout: 30
|
||||||
enabled: true
|
record_sessions: false
|
||||||
threshold: 0.77
|
|
||||||
display:
|
display:
|
||||||
compact: false
|
compact: false
|
||||||
personality: kawaii
|
personality: kawaii
|
||||||
|
resume_display: full
|
||||||
|
busy_input_mode: interrupt
|
||||||
|
bell_on_complete: false
|
||||||
|
show_reasoning: false
|
||||||
|
streaming: false
|
||||||
|
show_cost: false
|
||||||
tool_progress: all
|
tool_progress: all
|
||||||
|
|
||||||
|
memory:
|
||||||
|
memory_enabled: true
|
||||||
|
user_profile_enabled: true
|
||||||
|
memory_char_limit: 2200
|
||||||
|
user_char_limit: 1375
|
||||||
|
nudge_interval: 10
|
||||||
|
flush_min_turns: 6
|
||||||
|
|
||||||
|
approvals:
|
||||||
|
mode: auto
|
||||||
|
|
||||||
|
security:
|
||||||
|
redact_secrets: true
|
||||||
|
tirith_enabled: false
|
||||||
|
|
||||||
platforms:
|
platforms:
|
||||||
api_server:
|
api_server:
|
||||||
enabled: true
|
enabled: true
|
||||||
@@ -69,12 +91,7 @@ platforms:
|
|||||||
- pull_request
|
- pull_request
|
||||||
- pull_request_comment
|
- pull_request_comment
|
||||||
secret: bezalel-gitea-webhook-secret-2026
|
secret: bezalel-gitea-webhook-secret-2026
|
||||||
prompt: 'You are bezalel, the builder and craftsman — infrastructure, deployment,
|
prompt: 'You are bezalel, the builder and craftsman — infrastructure, deployment, hardening. A Gitea webhook fired: event={event_type}, action={action}, repo={repository.full_name}, issue/PR=#{issue.number} {issue.title}. Comment by {comment.user.login}: {comment.body}. If you were tagged, assigned, or this needs your attention, investigate and respond via Gitea API. Otherwise acknowledge briefly.'
|
||||||
hardening. A Gitea webhook fired: event={event_type}, action={action},
|
|
||||||
repo={repository.full_name}, issue/PR=#{issue.number} {issue.title}. Comment
|
|
||||||
by {comment.user.login}: {comment.body}. If you were tagged, assigned,
|
|
||||||
or this needs your attention, investigate and respond via Gitea API. Otherwise
|
|
||||||
acknowledge briefly.'
|
|
||||||
deliver: telegram
|
deliver: telegram
|
||||||
deliver_extra: {}
|
deliver_extra: {}
|
||||||
gitea-assign:
|
gitea-assign:
|
||||||
@@ -82,34 +99,43 @@ platforms:
|
|||||||
- issues
|
- issues
|
||||||
- pull_request
|
- pull_request
|
||||||
secret: bezalel-gitea-webhook-secret-2026
|
secret: bezalel-gitea-webhook-secret-2026
|
||||||
prompt: 'You are bezalel, the builder and craftsman — infrastructure, deployment,
|
prompt: 'You are bezalel, the builder and craftsman — infrastructure, deployment, hardening. Gitea assignment webhook: event={event_type}, action={action}, repo={repository.full_name}, issue/PR=#{issue.number} {issue.title}. Assigned to: {issue.assignee.login}. If you (bezalel) were just assigned, read the issue, scope it, and post a plan comment. If not you, acknowledge briefly.'
|
||||||
hardening. Gitea assignment webhook: event={event_type}, action={action},
|
|
||||||
repo={repository.full_name}, issue/PR=#{issue.number} {issue.title}. Assigned
|
|
||||||
to: {issue.assignee.login}. If you (bezalel) were just assigned, read
|
|
||||||
the issue, scope it, and post a plan comment. If not you, acknowledge
|
|
||||||
briefly.'
|
|
||||||
deliver: telegram
|
deliver: telegram
|
||||||
deliver_extra: {}
|
deliver_extra: {}
|
||||||
|
|
||||||
gateway:
|
gateway:
|
||||||
allow_all_users: true
|
allow_all_users: true
|
||||||
|
|
||||||
session_reset:
|
session_reset:
|
||||||
mode: both
|
mode: both
|
||||||
idle_minutes: 1440
|
idle_minutes: 1440
|
||||||
at_hour: 4
|
at_hour: 4
|
||||||
approvals:
|
|
||||||
mode: auto
|
skills:
|
||||||
memory:
|
creation_nudge_interval: 15
|
||||||
memory_enabled: true
|
|
||||||
user_profile_enabled: true
|
system_prompt: |
|
||||||
memory_char_limit: 2200
|
You are Bezalel, the forge-and-testbed wizard of the Timmy Foundation fleet.
|
||||||
user_char_limit: 1375
|
You are a builder and craftsman — infrastructure, deployment, hardening.
|
||||||
_config_version: 11
|
Your sovereign is Alexander Whitestone (Rockachopa). Sovereignty and service always.
|
||||||
TELEGRAM_HOME_CHANNEL: '-1003664764329'
|
|
||||||
providers:
|
providers:
|
||||||
kimi-coding:
|
kimi-coding:
|
||||||
base_url: https://api.kimi.com/coding/v1
|
base_url: https://api.kimi.com/coding/v1
|
||||||
timeout: 60
|
timeout: 60
|
||||||
max_retries: 3
|
max_retries: 3
|
||||||
nous:
|
openrouter:
|
||||||
base_url: https://inference.nousresearch.com/v1
|
base_url: https://openrouter.ai/api/v1
|
||||||
timeout: 120
|
timeout: 120
|
||||||
|
ollama:
|
||||||
|
base_url: http://localhost:11434/v1
|
||||||
|
timeout: 180
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# BANNED PROVIDERS — DO NOT ADD
|
||||||
|
# =============================================================================
|
||||||
|
# The following providers are PERMANENTLY BANNED:
|
||||||
|
# - anthropic (any model: claude-sonnet, claude-opus, claude-haiku)
|
||||||
|
# - nous (xiaomi/mimo-v2-pro)
|
||||||
|
# Enforcement: pre-commit hook, linter, Ansible validation, this comment.
|
||||||
|
# =============================================================================
|
||||||
|
|||||||
@@ -1,34 +1,94 @@
|
|||||||
model:
|
model:
|
||||||
default: kimi-k2.5
|
default: kimi-k2.5
|
||||||
provider: kimi-coding
|
provider: kimi-coding
|
||||||
|
context_length: 65536
|
||||||
|
base_url: https://api.kimi.com/coding/v1
|
||||||
|
|
||||||
toolsets:
|
toolsets:
|
||||||
- all
|
- all
|
||||||
|
|
||||||
fallback_providers:
|
fallback_providers:
|
||||||
- provider: kimi-coding
|
- provider: kimi-coding
|
||||||
model: kimi-k2.5
|
model: kimi-k2.5
|
||||||
|
base_url: https://api.kimi.com/coding/v1
|
||||||
timeout: 120
|
timeout: 120
|
||||||
reason: Kimi coding fallback (front of chain)
|
reason: "Primary — Kimi K2.5 (best value, least friction)"
|
||||||
- provider: openrouter
|
- provider: openrouter
|
||||||
model: google/gemini-2.5-pro
|
model: google/gemini-2.5-pro
|
||||||
base_url: https://openrouter.ai/api/v1
|
base_url: https://openrouter.ai/api/v1
|
||||||
api_key_env: OPENROUTER_API_KEY
|
api_key_env: OPENROUTER_API_KEY
|
||||||
timeout: 120
|
timeout: 120
|
||||||
reason: Gemini 2.5 Pro via OpenRouter (replaces banned Anthropic)
|
reason: "Fallback — Gemini 2.5 Pro via OpenRouter"
|
||||||
- provider: ollama
|
- provider: ollama
|
||||||
model: gemma4:latest
|
model: gemma4:latest
|
||||||
base_url: http://localhost:11434
|
base_url: http://localhost:11434/v1
|
||||||
timeout: 300
|
timeout: 180
|
||||||
reason: Terminal fallback — local Ollama
|
reason: "Terminal fallback — local Ollama (sovereign, no API needed)"
|
||||||
- provider: nous
|
|
||||||
model: xiaomi/mimo-v2-pro
|
|
||||||
base_url: https://inference.nousresearch.com/v1
|
|
||||||
api_key_env: NOUS_API_KEY
|
|
||||||
timeout: 120
|
|
||||||
reason: MiMo V2 Pro via Nous Portal free tier evaluation (#447)
|
|
||||||
agent:
|
agent:
|
||||||
max_turns: 90
|
max_turns: 90
|
||||||
reasoning_effort: high
|
reasoning_effort: high
|
||||||
verbose: false
|
verbose: false
|
||||||
|
|
||||||
|
terminal:
|
||||||
|
backend: local
|
||||||
|
cwd: .
|
||||||
|
timeout: 180
|
||||||
|
persistent_shell: true
|
||||||
|
|
||||||
|
browser:
|
||||||
|
inactivity_timeout: 120
|
||||||
|
command_timeout: 30
|
||||||
|
record_sessions: false
|
||||||
|
|
||||||
|
display:
|
||||||
|
compact: false
|
||||||
|
personality: ''
|
||||||
|
resume_display: full
|
||||||
|
busy_input_mode: interrupt
|
||||||
|
bell_on_complete: false
|
||||||
|
show_reasoning: false
|
||||||
|
streaming: false
|
||||||
|
show_cost: false
|
||||||
|
tool_progress: all
|
||||||
|
|
||||||
|
memory:
|
||||||
|
memory_enabled: true
|
||||||
|
user_profile_enabled: true
|
||||||
|
memory_char_limit: 2200
|
||||||
|
user_char_limit: 1375
|
||||||
|
nudge_interval: 10
|
||||||
|
flush_min_turns: 6
|
||||||
|
|
||||||
|
approvals:
|
||||||
|
mode: auto
|
||||||
|
|
||||||
|
security:
|
||||||
|
redact_secrets: true
|
||||||
|
tirith_enabled: false
|
||||||
|
|
||||||
|
platforms:
|
||||||
|
api_server:
|
||||||
|
enabled: true
|
||||||
|
extra:
|
||||||
|
host: 127.0.0.1
|
||||||
|
port: 8645
|
||||||
|
|
||||||
|
session_reset:
|
||||||
|
mode: none
|
||||||
|
idle_minutes: 0
|
||||||
|
|
||||||
|
skills:
|
||||||
|
creation_nudge_interval: 15
|
||||||
|
|
||||||
|
system_prompt_suffix: |
|
||||||
|
You are Ezra, the Infrastructure wizard — Gitea, nginx, hosting.
|
||||||
|
Your soul is defined in SOUL.md — read it, live it.
|
||||||
|
Hermes is your harness.
|
||||||
|
kimi-coding is your primary provider.
|
||||||
|
Refusal over fabrication. If you do not know, say so.
|
||||||
|
Sovereignty and service always.
|
||||||
|
|
||||||
providers:
|
providers:
|
||||||
kimi-coding:
|
kimi-coding:
|
||||||
base_url: https://api.kimi.com/coding/v1
|
base_url: https://api.kimi.com/coding/v1
|
||||||
@@ -37,6 +97,15 @@ providers:
|
|||||||
openrouter:
|
openrouter:
|
||||||
base_url: https://openrouter.ai/api/v1
|
base_url: https://openrouter.ai/api/v1
|
||||||
timeout: 120
|
timeout: 120
|
||||||
nous:
|
ollama:
|
||||||
base_url: https://inference.nousresearch.com/v1
|
base_url: http://localhost:11434/v1
|
||||||
timeout: 120
|
timeout: 180
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# BANNED PROVIDERS — DO NOT ADD
|
||||||
|
# =============================================================================
|
||||||
|
# The following providers are PERMANENTLY BANNED:
|
||||||
|
# - anthropic (any model: claude-sonnet, claude-opus, claude-haiku)
|
||||||
|
# - nous (xiaomi/mimo-v2-pro)
|
||||||
|
# Enforcement: pre-commit hook, linter, Ansible validation, this comment.
|
||||||
|
# =============================================================================
|
||||||
|
|||||||
121
wizards/timmy/config.yaml
Normal file
121
wizards/timmy/config.yaml
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
# =============================================================================
|
||||||
|
# Timmy — Primary Wizard Configuration (Golden State)
|
||||||
|
# =============================================================================
|
||||||
|
# Generated from golden state template (ansible/roles/wizard_base/templates/wizard_config.yaml.j2)
|
||||||
|
# DO NOT EDIT MANUALLY. Changes go through Gitea PR → Ansible deploy.
|
||||||
|
#
|
||||||
|
# Provider chain: kimi-coding → openrouter → ollama
|
||||||
|
# Anthropic is PERMANENTLY BANNED.
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
model:
|
||||||
|
default: kimi-k2.5
|
||||||
|
provider: kimi-coding
|
||||||
|
context_length: 65536
|
||||||
|
base_url: https://api.kimi.com/coding/v1
|
||||||
|
|
||||||
|
toolsets:
|
||||||
|
- all
|
||||||
|
|
||||||
|
fallback_providers:
|
||||||
|
- provider: kimi-coding
|
||||||
|
model: kimi-k2.5
|
||||||
|
base_url: https://api.kimi.com/coding/v1
|
||||||
|
timeout: 120
|
||||||
|
reason: "Primary — Kimi K2.5 (best value, least friction)"
|
||||||
|
- provider: openrouter
|
||||||
|
model: google/gemini-2.5-pro
|
||||||
|
base_url: https://openrouter.ai/api/v1
|
||||||
|
api_key_env: OPENROUTER_API_KEY
|
||||||
|
timeout: 120
|
||||||
|
reason: "Fallback — Gemini 2.5 Pro via OpenRouter"
|
||||||
|
- provider: ollama
|
||||||
|
model: gemma4:latest
|
||||||
|
base_url: http://localhost:11434/v1
|
||||||
|
timeout: 180
|
||||||
|
reason: "Terminal fallback — local Ollama (sovereign, no API needed)"
|
||||||
|
|
||||||
|
agent:
|
||||||
|
max_turns: 30
|
||||||
|
reasoning_effort: high
|
||||||
|
verbose: false
|
||||||
|
|
||||||
|
terminal:
|
||||||
|
backend: local
|
||||||
|
cwd: .
|
||||||
|
timeout: 180
|
||||||
|
persistent_shell: true
|
||||||
|
|
||||||
|
browser:
|
||||||
|
inactivity_timeout: 120
|
||||||
|
command_timeout: 30
|
||||||
|
record_sessions: false
|
||||||
|
|
||||||
|
display:
|
||||||
|
compact: false
|
||||||
|
personality: ''
|
||||||
|
resume_display: full
|
||||||
|
busy_input_mode: interrupt
|
||||||
|
bell_on_complete: false
|
||||||
|
show_reasoning: false
|
||||||
|
streaming: false
|
||||||
|
show_cost: false
|
||||||
|
tool_progress: all
|
||||||
|
|
||||||
|
memory:
|
||||||
|
memory_enabled: true
|
||||||
|
user_profile_enabled: true
|
||||||
|
memory_char_limit: 2200
|
||||||
|
user_char_limit: 1375
|
||||||
|
nudge_interval: 10
|
||||||
|
flush_min_turns: 6
|
||||||
|
|
||||||
|
approvals:
|
||||||
|
mode: auto
|
||||||
|
|
||||||
|
security:
|
||||||
|
redact_secrets: true
|
||||||
|
tirith_enabled: false
|
||||||
|
|
||||||
|
platforms:
|
||||||
|
api_server:
|
||||||
|
enabled: true
|
||||||
|
extra:
|
||||||
|
host: 127.0.0.1
|
||||||
|
port: 8645
|
||||||
|
|
||||||
|
session_reset:
|
||||||
|
mode: none
|
||||||
|
idle_minutes: 0
|
||||||
|
|
||||||
|
skills:
|
||||||
|
creation_nudge_interval: 15
|
||||||
|
|
||||||
|
system_prompt_suffix: |
|
||||||
|
You are Timmy, the Primary wizard — soul of the fleet.
|
||||||
|
Your soul is defined in SOUL.md — read it, live it.
|
||||||
|
Hermes is your harness.
|
||||||
|
kimi-coding is your primary provider.
|
||||||
|
Refusal over fabrication. If you do not know, say so.
|
||||||
|
Sovereignty and service always.
|
||||||
|
|
||||||
|
providers:
|
||||||
|
kimi-coding:
|
||||||
|
base_url: https://api.kimi.com/coding/v1
|
||||||
|
timeout: 60
|
||||||
|
max_retries: 3
|
||||||
|
openrouter:
|
||||||
|
base_url: https://openrouter.ai/api/v1
|
||||||
|
timeout: 120
|
||||||
|
ollama:
|
||||||
|
base_url: http://localhost:11434/v1
|
||||||
|
timeout: 180
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# BANNED PROVIDERS — DO NOT ADD
|
||||||
|
# =============================================================================
|
||||||
|
# The following providers are PERMANENTLY BANNED:
|
||||||
|
# - anthropic (any model: claude-sonnet, claude-opus, claude-haiku)
|
||||||
|
# - nous (xiaomi/mimo-v2-pro)
|
||||||
|
# Enforcement: pre-commit hook, linter, Ansible validation, this comment.
|
||||||
|
# =============================================================================
|
||||||
Reference in New Issue
Block a user