colibri/packaging/mother/setup-mother.sh
Sam & Claude e941fdd494
Some checks failed
CI / rust (pull_request) Has been cancelled
CI / markdown (pull_request) Has been cancelled
CI / port (pull_request) Has been cancelled
CI / agent-jail-pkgs (pull_request) Has been cancelled
mother: rename usb_nodes→hive_nodes (+node_type), harden setup/register
Review pass on the mother MCP infra:

- Rename usb_nodes → hive_nodes: a node is any host that joined the hive
  (live-usb/disk/vps/mother), not just a USB boot. Add a first-class
  node_type column (live-usb|disk|vps|mother|unknown). The schema migrates an
  existing osa DB in place (ALTER TABLE + ALTER SEQUENCE, guarded by
  to_regclass) and ADD COLUMN IF NOT EXISTS for already-renamed tables — data
  preserved, idempotent. FKs/trigger/indexes follow.
- node-register-mcp: accepts + validates node_type, UPSERTs into hive_nodes.
  Add ON_ERROR_STOP=1 (psql otherwise exits 0 on SQL error → false success)
  and fold stderr into the captured result so failures are reported.
- setup-mother.sh: apply schema BEFORE granting on its tables (fresh installs
  had no tables when grants ran); pipe the schema via stdin so the postgres
  user need not read the repo checkout; locate pg_hba via SHOW hba_file (was
  hardcoded) and PREPEND the peer rule (pg_hba is first-match); grants target
  hive_nodes/hive_nodes_id_seq.
- build-colibri.sh: fast-forward a checked-out branch to origin so it builds
  current upstream code, not a stale local copy.

Validated: prettier + sh -n green. Schema migration/UPSERT to be exercised on
osa (no local postgres server here).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 09:45:39 +02:00

293 lines
11 KiB
Bash
Executable file

#!/bin/sh
# setup-mother.sh — idempotent mother-node MCP setup.
#
# Deploys the mother MCP infrastructure on a FreeBSD host (osa or equivalent).
# Run as root. Safe to re-run: every step is idempotent.
#
# What it does:
# 1. Installs colibri binaries (colibri-mcp, colibri-daemon, colibri) from
# the prebuilt artifact dir (default: ../target/release).
# 2. Installs MCP server scripts (geodesic-dome-mcp, build-colibri.sh,
# node-register-mcp, colibri-mcp-ssh).
# 3. jq-merges mother servers into external-mcp.json (preserves existing).
# 4. Creates the colibri OS user if missing, sets up SSH authorized_keys
# for the mother-mcp key (command="/usr/local/bin/colibri-mcp-ssh").
# 5. Configures PostgreSQL peer auth for the colibri user on mother_hive.
# Generates the mother-mcp keypair if absent, prints the private key for
# placement on a node's seed partition.
# 5. Applies the schema (create + usb_nodes→hive_nodes migration), then
# configures PostgreSQL peer auth + grants for the colibri user.
# 6. Updates colibri daemon env (COLIBRI_AUTOSPAWN, COLIBRI_MCP_EXTERNAL_CALL).
# 7. Reloads colibri_daemon (or starts it).
#
# Idempotency: every step checks before acting. Running twice is a no-op.
#
# Usage:
# sudo ./setup-mother.sh # default artifact dir
# sudo COLIBRI_ARTIFACT_DIR=/path ./setup-mother.sh
set -eu
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
ARTIFACT_DIR="${COLIBRI_ARTIFACT_DIR:-${SCRIPT_DIR}/../../target/release}"
COST_MODE="${COLIBRI_COST_MODE:-smart}"
MCP_CONFIG="/usr/local/etc/colibri/external-mcp.json"
COLIBRI_HOME="/var/db/colibri"
SSH_DIR="${COLIBRI_HOME}/.ssh"
PROVIDER_ENV="/usr/local/etc/colibri/provider.env"
echo "=== mother MCP setup ==="
echo " artifact dir : ${ARTIFACT_DIR}"
echo " cost mode : ${COST_MODE}"
echo ""
# ── helpers ───────────────────────────────────────────────────────────
idempotent() {
local desc="$1" fn="$2"
printf " %-50s" "${desc}..."
eval "$fn" && echo "OK" || { echo "FAIL"; exit 1; }
}
ensure_user() {
if id colibri >/dev/null 2>&1; then
echo "(already exists)"
return 0
fi
pw useradd colibri -d "${COLIBRI_HOME}" -s /usr/sbin/nologin
}
ensure_dir() {
mkdir -p "$1"
chown colibri:colibri "$1"
chmod 750 "$1"
}
# ── 1. colibri binaries ───────────────────────────────────────────────
echo "--- 1. colibri binaries ---"
idempotent "colibri-mcp" '
if [ -x "$ARTIFACT_DIR/colibri-mcp" ]; then
install -m 755 "$ARTIFACT_DIR/colibri-mcp" /usr/local/bin/colibri-mcp
else
echo "MISSING: $ARTIFACT_DIR/colibri-mcp — build colibri first"; exit 1
fi
'
idempotent "colibri-daemon" '
if [ -x "$ARTIFACT_DIR/colibri-daemon" ]; then
install -m 755 "$ARTIFACT_DIR/colibri-daemon" /usr/local/bin/colibri-daemon
else
echo "MISSING: $ARTIFACT_DIR/colibri-daemon"; exit 1
fi
'
idempotent "colibri (CLI)" '
if [ -x "$ARTIFACT_DIR/colibri" ]; then
install -m 755 "$ARTIFACT_DIR/colibri" /usr/local/bin/colibri
else
echo "MISSING: $ARTIFACT_DIR/colibri"; exit 1
fi
'
# ── 2. MCP server scripts ─────────────────────────────────────────────
echo "--- 2. MCP server scripts ---"
idempotent "colibri-mcp-ssh wrapper" '
install -m 755 "$SCRIPT_DIR/colibri-mcp-ssh" /usr/local/bin/colibri-mcp-ssh
'
idempotent "geodesic-dome-mcp" '
install -m 755 "$SCRIPT_DIR/geodesic-dome-mcp" /usr/local/bin/geodesic-dome-mcp
'
idempotent "build-colibri.sh" '
install -m 755 "$SCRIPT_DIR/build-colibri.sh" /usr/local/bin/build-colibri.sh
'
idempotent "node-register-mcp" '
install -m 755 "$SCRIPT_DIR/node-register-mcp" /usr/local/bin/node-register-mcp
'
# ── 3. external-mcp.json (jq-merge, preserve existing) ────────────────
echo "--- 3. external-mcp.json ---"
MOTHER_SERVERS='{
"mother-build": {
"command": "/usr/local/bin/build-colibri.sh",
"args": [],
"env": {}
},
"geodesic-dome": {
"command": "/usr/local/bin/geodesic-dome-mcp",
"args": [],
"env": {}
},
"node-register": {
"command": "/usr/local/bin/node-register-mcp",
"args": [],
"env": {}
}
}'
mkdir -p "$(dirname "$MCP_CONFIG")"
if [ -f "$MCP_CONFIG" ]; then
# Preserve servers we don't own; overwrite our three with latest.
TMP_MCP=$(mktemp)
jq --argjson ours "$MOTHER_SERVERS" '.servers = (.servers // {}) * $ours' \
"$MCP_CONFIG" > "$TMP_MCP"
mv "$TMP_MCP" "$MCP_CONFIG"
echo " merged node-register + geodesic-dome + mother-build"
else
printf '{"servers": %s}\n' "$MOTHER_SERVERS" | jq . > "$MCP_CONFIG"
echo " created"
fi
chmod 644 "$MCP_CONFIG"
# ── 4. colibri user + SSH ────────────────────────────────────────────
echo "--- 4. colibri user + SSH ---"
idempotent "colibri user" 'ensure_user'
idempotent "colibri home" 'ensure_dir "$COLIBRI_HOME"'
idempotent "colibri .ssh" 'ensure_dir "$SSH_DIR"'
idempotent "mother-mcp keypair" '
if [ -f "${SSH_DIR}/mother-mcp" ]; then
echo "(already exists)"
else
ssh-keygen -t ed25519 -f "${SSH_DIR}/mother-mcp" \
-C "mother-mcp-$(date +%Y%m%d)" -N "" >/dev/null 2>&1
chown colibri:colibri "${SSH_DIR}/mother-mcp" "${SSH_DIR}/mother-mcp.pub"
chmod 600 "${SSH_DIR}/mother-mcp"
chmod 644 "${SSH_DIR}/mother-mcp.pub"
fi
'
idempotent "authorized_keys command= wrapper" '
PUBKEY=$(cat "${SSH_DIR}/mother-mcp.pub")
AUTH_FILE="${SSH_DIR}/authorized_keys"
EXPECTED="command=\"/usr/local/bin/colibri-mcp-ssh\",restrict $PUBKEY"
if [ -f "$AUTH_FILE" ] && grep -qF "$PUBKEY" "$AUTH_FILE" 2>/dev/null; then
echo "(already present)"
else
printf "command=\"/usr/local/bin/colibri-mcp-ssh\",restrict %s\n" "$PUBKEY" >> "$AUTH_FILE"
chown colibri:colibri "$AUTH_FILE"
chmod 600 "$AUTH_FILE"
fi
'
# Print the private key for seed placement
echo ""
echo " >>> mother-mcp private key (copy to USB seed partition) <<<"
echo " ==========================================================="
cat "${SSH_DIR}/mother-mcp"
echo " ==========================================================="
echo ""
# ── 5. PostgreSQL peer auth ──────────────────────────────────────────
echo "--- 5. PostgreSQL peer auth ---"
idempotent "DB role colibri" '
su - postgres -c "psql -tAc \"DO \\\$\\\$ BEGIN
IF NOT EXISTS (SELECT FROM pg_catalog.pg_roles WHERE rolname = '\''colibri'\'') THEN
CREATE ROLE colibri WITH LOGIN;
END IF;
END \\\$\\\$;\"" 2>&1
'
# Apply the schema BEFORE granting on its tables (fresh installs have no tables
# until now; this also runs the usb_nodes → hive_nodes migration). Pipe the file
# via stdin so the postgres user need not read it from the repo checkout, and
# stop on the first error so a failed migration is not silently skipped.
idempotent "mother schema (create + migrate)" '
su - postgres -c "psql -d mother_hive -v ON_ERROR_STOP=1 -f -" \
< "$SCRIPT_DIR/mother_schema.sql" 2>&1 | tail -1
'
idempotent "GRANT CONNECT on mother_hive" '
su - postgres -c "psql -d mother_hive -tAc \
\"GRANT CONNECT ON DATABASE mother_hive TO colibri;\"" 2>&1
'
idempotent "GRANT INSERT,UPDATE on hive_nodes" '
su - postgres -c "psql -d mother_hive -tAc \
\"GRANT INSERT, UPDATE ON hive_nodes TO colibri;
GRANT USAGE ON SEQUENCE hive_nodes_id_seq TO colibri;\"" 2>&1
'
idempotent "GRANT INSERT on audit_log" '
su - postgres -c "psql -d mother_hive -tAc \
\"GRANT INSERT ON audit_log TO colibri;
GRANT USAGE ON SEQUENCE audit_log_id_seq TO colibri;\"" 2>&1
'
# Configure pg_hba.conf for peer auth. Locate the real file (path varies by
# PostgreSQL major/pkg) instead of hardcoding it.
PG_HBA=$(su - postgres -c "psql -tAc 'SHOW hba_file;'" 2>/dev/null | tr -d '[:space:]')
if [ -n "$PG_HBA" ] && [ -f "$PG_HBA" ]; then
PEER_LINE="local mother_hive colibri peer"
if grep -qF "$PEER_LINE" "$PG_HBA"; then
echo " pg_hba peer auth: (already present)"
else
# Prepend: pg_hba is first-match, so this specific rule must precede any
# generic 'local all all ...' line. cat-redirect preserves owner/perms.
TMP_HBA=$(mktemp)
{ printf '%s\n' "$PEER_LINE"; cat "$PG_HBA"; } > "$TMP_HBA"
cat "$TMP_HBA" > "$PG_HBA"
rm -f "$TMP_HBA"
service postgresql reload >/dev/null 2>&1 || true
echo " pg_hba peer auth: prepended + reloaded"
fi
else
echo " pg_hba: could not locate hba_file — configure peer auth manually"
fi
# ── 6. daemon env update ──────────────────────────────────────────────
echo "--- 6. daemon env ---"
mkdir -p /usr/local/etc/colibri
if [ -f "$PROVIDER_ENV" ]; then
# Update COLIBRI_AUTOSPAWN_PI → COLIBRI_AUTOSPAWN (0.12 rename)
if grep -q 'COLIBRI_AUTOSPAWN_PI' "$PROVIDER_ENV"; then
sed -i '' 's/COLIBRI_AUTOSPAWN_PI/COLIBRI_AUTOSPAWN/' "$PROVIDER_ENV"
echo " renamed COLIBRI_AUTOSPAWN_PI → COLIBRI_AUTOSPAWN"
fi
# Ensure external calls are enabled
if ! grep -q 'COLIBRI_MCP_EXTERNAL_CALL' "$PROVIDER_ENV"; then
echo 'COLIBRI_MCP_EXTERNAL_CALL="1"' >> "$PROVIDER_ENV"
echo " added COLIBRI_MCP_EXTERNAL_CALL=1"
else
echo " COLIBRI_MCP_EXTERNAL_CALL already present"
fi
# Set cost mode if missing
if ! grep -q 'COLIBRI_COST_MODE' "$PROVIDER_ENV"; then
echo "COLIBRI_COST_MODE=\"${COST_MODE}\"" >> "$PROVIDER_ENV"
echo " added COLIBRI_COST_MODE=${COST_MODE}"
fi
else
cat > "$PROVIDER_ENV" << ENVEOF
COLIBRI_AUTOSPAWN=YES
COLIBRI_MCP_EXTERNAL_CALL="1"
COLIBRI_COST_MODE="${COST_MODE}"
ENVEOF
chmod 600 "$PROVIDER_ENV"
echo " created provider.env"
fi
# ── 7. reload daemon ──────────────────────────────────────────────────
echo "--- 7. colibri daemon ---"
if service colibri_daemon status >/dev/null 2>&1; then
service colibri_daemon restart >/dev/null 2>&1 || true
echo " restarted"
else
service colibri_daemon start >/dev/null 2>&1 || true
echo " started"
fi
# ── verify ────────────────────────────────────────────────────────────
echo ""
echo "=== setup complete ==="
echo ""
echo "Verification:"
echo " colibri-mcp tools | grep external"
echo " sudo -u colibri psql -d mother_hive -c \"SELECT hostname, node_type, status FROM hive_nodes;\""
echo " ssh colibri@localhost tools"
echo ""
echo "To add a node: copy the mother-mcp private key above to the node's"
echo "seed partition (CLAWDIESEED/colibri/ssh/mother-mcp) or install it"
echo "manually at ~/.ssh/mother-mcp on the node."