#!/bin/bash set -e echo "================================================" echo " BASECAMP — AI Agent + Local Inference" echo " Portable. Auto-discovering. Stack-agnostic." echo " (Isolated: config lives only in basecamp state)" echo "================================================" echo "" # Start Ollama via supervisor in background supervisord -c /etc/supervisor/conf.d/supervisord.conf & # Wait for Ollama to be ready echo "Starting local Ollama..." for i in $(seq 1 60); do if curl -s http://127.0.0.1:11434/api/tags > /dev/null 2>&1; then echo "Local Ollama is up." break fi sleep 1 done # First run: show connect screen. Everything written lives inside basecamp's # own state (the mounted volume) — never touches a host hermes install. CONFIG_FILE="/root/.hermes/basecamp_config.json" ENV_FILE="/opt/basecamp/basecamp.env" # FIX 2026-08-10: ALWAYS re-scan, even when a config exists. The old logic # skipped discovery when config was found ("Config found. Regenerating # tavern toolkit..."), so newly-added stack services stayed invisible until # the user manually ran `tavern rediscover`. The box must discover fresh # on every boot — that's the whole point of auto-discovery. echo "" echo "=== Scanning for AI services ===" echo "" if python3 /opt/basecamp/discover.py serve; then : else echo "" echo "Discovery failed. Falling back to basecamp's own Ollama." python3 /opt/basecamp/discover.py env || true # Install the secret-free static tavern so stack commands still work cp /opt/basecamp/tavern.sh /usr/local/bin/tavern 2>/dev/null || true chmod +x /usr/local/bin/tavern 2>/dev/null || true fi # Apply Hermes runtime config — process-scoped env vars, no config.yaml/.env writes if [ -f "$ENV_FILE" ]; then # shellcheck disable=SC1090 . "$ENV_FILE" fi # Best-effort: if Hermes has no model configured yet, set it via Hermes' own # Best-effort: if Hermes has no model configured yet, set it via Hermes' own # config CLI (schema-safe). This only ever writes inside basecamp's own HERMES_HOME. if [ -n "${HERMES_MODEL:-}" ] && ! hermes config get model > /dev/null 2>&1; then hermes config set model "$HERMES_MODEL" > /dev/null 2>&1 || true fi # CRITICAL (v0.20+): the stock config.yaml ships a default model+base_url # (openrouter), so the guard above never fires and env-only wiring # (OPENAI_BASE_URL / OPENAI_API_KEY) is IGNORED for non-authoritative hosts — # hermes's #28660 security gate blocks env keys for LAN/custom endpoints and # sends the "no-key-required" sentinel instead (auth 401). The sanctioned path # is a NAMED custom provider with an inline api_key. Configure it in # basecamp's OWN HERMES_HOME (never a host install). if [ -n "${OPENAI_BASE_URL:-}" ]; then hermes config set providers.basecamp.api "$OPENAI_BASE_URL" > /dev/null 2>&1 || true hermes config set providers.basecamp.default_model "$HERMES_MODEL" > /dev/null 2>&1 || true if [ -n "${OPENAI_API_KEY:-}" ]; then hermes config set providers.basecamp.api_key "$OPENAI_API_KEY" > /dev/null 2>&1 || true fi hermes config set providers.basecamp.context_length 131072 > /dev/null 2>&1 || true hermes config set model.provider basecamp > /dev/null 2>&1 || true hermes config set model.default "$HERMES_MODEL" > /dev/null 2>&1 || true # Drop the stock openrouter base_url so model.base_url doesn't lie about # where traffic actually goes (the named provider's api is authoritative). hermes config set model.base_url "" > /dev/null 2>&1 || true # Local engines (ollama/tabbyapi/vllm) generally don't support hermes's # reasoning-effort params — sending them yields "does not support thinking" # HTTP 400. Turn reasoning off for the basecamp provider. hermes config set agent.reasoning_effort off > /dev/null 2>&1 || true # Clear stale context probes so the live endpoint is re-queried. rm -f "${HERMES_HOME:-/root/.hermes}/context_length_cache.yaml" 2>/dev/null || true fi # Basecamp's own skin — install into basecamp's HERMES_HOME and activate it. # (Writes only inside the container's own state, never a host install.) if [ -d /opt/basecamp/skins ] && [ -f /opt/basecamp/skins/indigo.yaml ]; then mkdir -p "${HERMES_HOME:-/root/.hermes}/skins" cp /opt/basecamp/skins/indigo.yaml "${HERMES_HOME:-/root/.hermes}/skins/indigo.yaml" hermes config set display.skin indigo > /dev/null 2>&1 || true fi # ── The tavern MCP server — exposes the connectivity toolkit as REAL # Hermes tools (mcp_tavern_status, mcp_tavern_self_check, mcp_tavern_wire, # mcp_tavern_rediscover, mcp_tavern_models, mcp_tavern_chat). The agent # sees them in its tool list and CALLS them — it can't invent commands # that don't exist. Writes only inside basecamp's own HERMES_HOME. # Injected DIRECTLY into config.yaml with yq (idempotent) — the # `hermes config set`/`hermes mcp add` paths get clobbered by hermes's # first-run config regeneration, but a yq write to the file survives. # Runs again just before exec hermes (below) so the injection happens # AFTER hermes's first-run config write, on every boot. inject_tavern_mcp() { if [ -f /opt/basecamp/tavern_mcp.py ]; then CFG="${HERMES_HOME:-/root/.hermes}/config.yaml" if [ -f "$CFG" ]; then yq -i '.mcp_servers.tavern.command = "python3" | .mcp_servers.tavern.args = ["/opt/basecamp/tavern_mcp.py"] | .mcp_servers.tavern.timeout = 120' "$CFG" 2>/dev/null || true fi fi } inject_tavern_mcp # Basecamp's own identity — install the SOUL.md that makes this Hermes # KNOW it must load the basecamp-stack skill for any stack question. # (Writes only inside the container's own state, never a host install.) if [ -f /opt/basecamp/SOUL.md ]; then cp /opt/basecamp/SOUL.md "${HERMES_HOME:-/root/.hermes}/SOUL.md" fi # Pull default model in background — AFTER discovery so the network scan # isn't competing with a multi-GB download (missed-service boot race). if [ -n "$BASECAMP_MODEL" ]; then if ! ollama list 2>/dev/null | grep -q "$BASECAMP_MODEL"; then echo "Pulling $BASECAMP_MODEL in background..." ollama pull "$BASECAMP_MODEL" > /dev/null 2>&1 & fi fi # Stack wiring audit — show every boot what's connected and what needs a fix. # (Read-only: audits and prints fixes; never edits other containers' configs.) if [ -x /opt/basecamp/discover.py ]; then echo "" echo "════════════════════════════════════════════" echo " ✓ Connected & wired — auditing your stack" echo "════════════════════════════════════════════" python3 /opt/basecamp/discover.py wire 2>/dev/null || true fi echo "" echo "════════════════════════════════════════════" echo " 🚀 Starting Basecamp Hermes..." echo " (this is the agent you'll talk to — loading" echo " its tools, skills, and memory. ~10-30s)" echo "════════════════════════════════════════════" echo "" # Re-inject the tavern MCP server — config.yaml definitely exists now # (hermes's first-run write happened above), so the connectivity toolkit # is registered on EVERY boot, first boot included. inject_tavern_mcp # Handle arguments if [ $# -gt 0 ]; then case "$1" in tavern) shift exec tavern "$@" ;; ollama) shift exec ollama "$@" ;; bash|shell) shift exec bash "$@" ;; rediscover) rm -f "$CONFIG_FILE" exec python3 /opt/basecamp/discover.py serve ;; *) # Force-load the stack skill so hermes always has the discovered # services + fix recipes in context (description matching alone # isn't reliable with 67 skills competing). exec hermes -s basecamp-stack "$@" ;; esac else echo "Type 'tavern help' for stack commands." echo "Type 'tavern rediscover' to re-scan for services." echo "" exec hermes -s basecamp-stack fi