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>
293 lines
11 KiB
Bash
Executable file
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."
|