basecamp / entrypoint.sh
jpanasuk's picture
v1.0.6 final: tavern MCP server (connectivity toolkit + read-only docker tools), smart default model (fast 8B over slow 70B), new-user connect menu, boot progress banners
6093073
Raw
History Blame Contribute Delete
8.43 kB
#!/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