feat(mother): MCP infra — hive_nodes registry, hardened wrapper/builder, idempotent setup #161
6 changed files with 735 additions and 8 deletions
161
packaging/mother/MOTHER-SETUP.md
Normal file
161
packaging/mother/MOTHER-SETUP.md
Normal file
|
|
@ -0,0 +1,161 @@
|
||||||
|
# Mother Node MCP Setup
|
||||||
|
|
||||||
|
## What this is
|
||||||
|
|
||||||
|
The mother node (OSA) runs the Colibri MCP host for USB operator node
|
||||||
|
coordination. USB nodes send hardware profiles via SSH → MCP →
|
||||||
|
PostgreSQL. This directory contains the scripts and schema for that
|
||||||
|
infrastructure.
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
| ------------------- | ----------------------------------------------------------------- |
|
||||||
|
| `setup-mother.sh` | Idempotent deploy — run once as root, safe to re-run |
|
||||||
|
| `mother_schema.sql` | PostgreSQL schema (hive_nodes, build_queue, audit_log + triggers) |
|
||||||
|
| `node-register-mcp` | MCP tool: receive hw-probe JSON → UPSERT into hive_nodes |
|
||||||
|
| `geodesic-dome-mcp` | MCP tool: geodesic dome wireframe + structural BOM |
|
||||||
|
| `build-colibri.sh` | MCP tool: build a colibri crate (branch-allowlisted) |
|
||||||
|
| `colibri-mcp-ssh` | SSH forced-command wrapper (allowlisted: `""`, `"tools"` only) |
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
USB node Mother (osa)
|
||||||
|
──────── ────────────
|
||||||
|
clawdie-hw-probe PostgreSQL (mother_hive)
|
||||||
|
│ ▲
|
||||||
|
▼ │
|
||||||
|
colibri-daemon │ INSERT / UPSERT
|
||||||
|
│ (external MCP) │
|
||||||
|
▼ │
|
||||||
|
ssh colibri@mother (no cmd) │
|
||||||
|
│ (mother-mcp key) │
|
||||||
|
▼ │
|
||||||
|
colibri-mcp-ssh ◄─ authorized_keys command="..."
|
||||||
|
│ allowlist: "" or "tools"
|
||||||
|
▼
|
||||||
|
colibri-mcp ──► node-register-mcp ──► psql -v :'variable' (heredoc)
|
||||||
|
(parameterized UPSERT, no shell interpolation of SQL)
|
||||||
|
```
|
||||||
|
|
||||||
|
The USB's `colibri-mcp` spawns `ssh -i ~/.ssh/mother-mcp colibri@mother`
|
||||||
|
with NO remote command. SSH is restricted via `command=` + `restrict` to
|
||||||
|
run only `colibri-mcp-ssh`, which delegates to `colibri-mcp` in stdio MCP
|
||||||
|
mode. The `node_register` tool on mother receives the hw-probe JSON and
|
||||||
|
UPSERTs it into PostgreSQL using `psql -v :'variable'` quoting in a
|
||||||
|
heredoc — the JSON blob is a bound variable, never interpolated into SQL.
|
||||||
|
|
||||||
|
## Security properties
|
||||||
|
|
||||||
|
- **colibri-mcp-ssh**: allowlists `SSH_ORIGINAL_COMMAND` to `""` (stdio MCP
|
||||||
|
mode) or `"tools"` (one-shot discovery). All other values are rejected.
|
||||||
|
This prevents callers from passing arbitrary colibri-mcp flags through
|
||||||
|
the forced-command boundary.
|
||||||
|
- **build-colibri.sh**: branch is validated against an allowlist (`main`,
|
||||||
|
semver `v*` tags, plus `COLIBRI_BUILD_ALLOW_BRANCHES` colon-separated
|
||||||
|
extras). Features are validated against `^[A-Za-z0-9_,-]+$`. Cargo
|
||||||
|
args use a shell array — no unquoted string concatenation.
|
||||||
|
- **node-register-mcp**: `psql -v :'variable'` quoting in a heredoc (not
|
||||||
|
`-c`). The JSON blob is dollar-quoted by psql; shell never interpolates
|
||||||
|
it into SQL.
|
||||||
|
- **mother-mcp key**: NOT baked into images, NOT reused for Forgejo.
|
||||||
|
Lives on seed partition (`CLAWDIESEED/colibri/ssh/mother-mcp`). One key
|
||||||
|
per trust domain — blast radius is MCP-over-SSH only.
|
||||||
|
|
||||||
|
## Setup (one-time)
|
||||||
|
|
||||||
|
On mother, as root:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Build colibri from current main first, then:
|
||||||
|
cd /home/clawdie/ai/colibri
|
||||||
|
sudo ./packaging/mother/setup-mother.sh
|
||||||
|
|
||||||
|
# The script prints the mother-mcp private key — copy it for USB nodes.
|
||||||
|
```
|
||||||
|
|
||||||
|
### What setup-mother.sh does
|
||||||
|
|
||||||
|
1. Installs colibri binaries from `target/release`
|
||||||
|
2. Installs MCP server scripts
|
||||||
|
3. jq-merges mother servers into `/usr/local/etc/colibri/external-mcp.json`
|
||||||
|
4. Creates `colibri` OS user + SSH authorized_keys with `command=` wrapper
|
||||||
|
5. Generates mother-mcp keypair (prints private key for seed placement)
|
||||||
|
6. Configures PostgreSQL peer auth for colibri on mother_hive
|
||||||
|
7. Runs `mother_schema.sql` (idempotent)
|
||||||
|
8. Updates colibri daemon env (`COLIBRI_AUTOSPAWN`, `COLIBRI_MCP_EXTERNAL_CALL`)
|
||||||
|
9. Restarts `colibri_daemon`
|
||||||
|
|
||||||
|
Idempotent — running again is a no-op.
|
||||||
|
|
||||||
|
## Key management
|
||||||
|
|
||||||
|
The mother-mcp key is NOT baked into images. It lives:
|
||||||
|
|
||||||
|
- **On mother:** `/var/db/colibri/.ssh/mother-mcp` + `.pub` (auto-generated
|
||||||
|
by setup-mother.sh)
|
||||||
|
- **On USB nodes:** placed on the seed partition at
|
||||||
|
`CLAWDIESEED/colibri/ssh/mother-mcp` (the live-seed importer picks it up
|
||||||
|
at first boot and installs it at `~/.ssh/mother-mcp`)
|
||||||
|
|
||||||
|
One key per trust domain. The mother-mcp key is not reused for Forgejo
|
||||||
|
or other services — its blast radius is limited to MCP over SSH.
|
||||||
|
|
||||||
|
## USB-side external-mcp.json
|
||||||
|
|
||||||
|
The USB node registers mother as an external MCP server. Note: NO
|
||||||
|
remote command — the forced-command wrapper handles entry:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"servers": {
|
||||||
|
"mother": {
|
||||||
|
"command": "ssh",
|
||||||
|
"args": [
|
||||||
|
"-i", "/home/clawdie/.ssh/mother-mcp",
|
||||||
|
"-o", "StrictHostKeyChecking=accept-new",
|
||||||
|
"colibri@100.72.229.63"
|
||||||
|
],
|
||||||
|
"env": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The colibri-daemon spawner pipes JSON-RPC on stdin/stdout. SSH connects
|
||||||
|
with an empty remote command; the forced-command wrapper starts
|
||||||
|
`colibri-mcp` in stdio MCP mode automatically.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# On mother: check the external MCP tools are live
|
||||||
|
colibri-mcp tools 2>&1 | grep external
|
||||||
|
|
||||||
|
# On mother: test node_register with a sample hw-probe
|
||||||
|
sudo clawdie-hw-probe 2>/dev/null | \
|
||||||
|
jq '{jsonrpc:"2.0",method:"tools/call",id:1,
|
||||||
|
params:{name:"node_register",
|
||||||
|
arguments:{hostname:"test-node",hw_profile:.}}}' | \
|
||||||
|
/usr/local/bin/node-register-mcp
|
||||||
|
|
||||||
|
# Check PostgreSQL
|
||||||
|
sudo -u colibri psql -d mother_hive -c \
|
||||||
|
"SELECT hostname, status, capabilities FROM hive_nodes;"
|
||||||
|
|
||||||
|
# From a USB node with the mother-mcp key:
|
||||||
|
ssh colibri@mother tools | grep node_register
|
||||||
|
```
|
||||||
|
|
||||||
|
## Adding a USB node
|
||||||
|
|
||||||
|
1. Copy the mother-mcp private key to the USB node's seed partition
|
||||||
|
(`CLAWDIESEED/colibri/ssh/mother-mcp`)
|
||||||
|
2. On the USB, install `external-mcp.json` as shown above
|
||||||
|
3. On the USB, install `clawdie-hw-probe` from clawdie-iso
|
||||||
|
4. Restart `colibri_daemon` — autospawn runs hw-probe and registers
|
||||||
|
the node with mother
|
||||||
|
|
||||||
|
See `docs/USB-MOTHER-MCP-CONNECTION.md` in clawdie-iso for the full
|
||||||
|
USB-side procedure.
|
||||||
124
packaging/mother/build-colibri.sh
Executable file
124
packaging/mother/build-colibri.sh
Executable file
|
|
@ -0,0 +1,124 @@
|
||||||
|
#!/bin/sh
|
||||||
|
# build-colibri MCP tool — build a colibri crate from an allowed git branch.
|
||||||
|
#
|
||||||
|
# Accepts a JSON-RPC tools/call request on stdin, builds the requested
|
||||||
|
# crate, and returns the result as a JSON content block to stdout.
|
||||||
|
#
|
||||||
|
# Expected input (MCP tools/call):
|
||||||
|
# {"jsonrpc":"2.0","method":"tools/call","id":1,"params":{"name":"build_colibri","arguments":{"crate":"colibri-daemon","branch":"main","features":"","release":"true"}}}
|
||||||
|
#
|
||||||
|
# Parameters:
|
||||||
|
# crate — workspace member to build (default: colibri-daemon)
|
||||||
|
# branch — git ref to build (allowlisted: main, v* tags; default: main)
|
||||||
|
# features — optional comma-separated feature names (validated: ^[A-Za-z0-9_,-]*$)
|
||||||
|
# release — "true" for --release (default), anything else for debug
|
||||||
|
#
|
||||||
|
# Security:
|
||||||
|
# - Branch is validated against an allowlist (main + semver tags +
|
||||||
|
# COLIBRI_BUILD_ALLOW_BRANCHES env). Arbitrary branch RCE via
|
||||||
|
# build.rs / proc-macros is the threat model.
|
||||||
|
# - Features are validated against ^[A-Za-z0-9_,-]*$ to prevent cargo
|
||||||
|
# flag injection (e.g. --config … smuggled through a feature name).
|
||||||
|
# - Cargo args use a shell array with proper quoting throughout.
|
||||||
|
#
|
||||||
|
# Output: MCP JSON-RPC response with text content block.
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
REPO="/home/clawdie/ai/colibri"
|
||||||
|
CRATE_DEFAULT="colibri-daemon"
|
||||||
|
BRANCH_DEFAULT="main"
|
||||||
|
|
||||||
|
# ── helpers ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
die() {
|
||||||
|
printf '{"jsonrpc":"2.0","id":%s,"error":{"code":-1,"message":"%s"}}\n' \
|
||||||
|
"$ID" "$(printf '%s' "$1" | sed 's/"/\\"/g')"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── parse request ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
INPUT=$(cat)
|
||||||
|
ID=$(echo "$INPUT" | jq -r '.id // "1"')
|
||||||
|
CRATE=$(echo "$INPUT" | jq -r '.params.arguments.crate // "'"$CRATE_DEFAULT"'"')
|
||||||
|
BRANCH=$(echo "$INPUT" | jq -r '.params.arguments.branch // "'"$BRANCH_DEFAULT"'"')
|
||||||
|
FEATURES=$(echo "$INPUT" | jq -r '.params.arguments.features // ""')
|
||||||
|
RELEASE=$(echo "$INPUT" | jq -r '.params.arguments.release // "true"')
|
||||||
|
|
||||||
|
# ── validate branch (allowlist) ───────────────────────────────────────
|
||||||
|
|
||||||
|
# Allow: main, any tag matching v<digits> (semver), plus operator overrides.
|
||||||
|
# The -w flag is a FreeBSD grep extension; on Linux use grep -E.
|
||||||
|
valid_branch() {
|
||||||
|
local b="$1"
|
||||||
|
[ "$b" = "main" ] && return 0
|
||||||
|
# semver tags: v0.12.0, v1.2.3, v0.2.42, etc.
|
||||||
|
printf '%s' "$b" | grep -Eq '^v[0-9]+\.[0-9]+\.[0-9]+(-[A-Za-z0-9._-]+)?$' && return 0
|
||||||
|
# Operator-configured extra branches (colon-separated).
|
||||||
|
if [ -n "${COLIBRI_BUILD_ALLOW_BRANCHES:-}" ]; then
|
||||||
|
local saved_IFS="$IFS"
|
||||||
|
IFS=':'
|
||||||
|
for allowed in $COLIBRI_BUILD_ALLOW_BRANCHES; do
|
||||||
|
[ "$b" = "$allowed" ] && { IFS="$saved_IFS"; return 0; }
|
||||||
|
done
|
||||||
|
IFS="$saved_IFS"
|
||||||
|
fi
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if ! valid_branch "$BRANCH"; then
|
||||||
|
die "branch not in allowlist: ${BRANCH}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── validate features ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
if [ -n "$FEATURES" ]; then
|
||||||
|
if ! printf '%s' "$FEATURES" | grep -Eq '^[A-Za-z0-9_,-]+$'; then
|
||||||
|
die "features must match ^[A-Za-z0-9_,-]+$"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── validate crate (workspace member name only) ───────────────────────
|
||||||
|
|
||||||
|
# Prevent path traversal via crate name.
|
||||||
|
if ! printf '%s' "$CRATE" | grep -Eq '^[a-z][a-z0-9_-]+$'; then
|
||||||
|
die "crate must match ^[a-z][a-z0-9_-]+$"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── build ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
START_TS=$(date +%s)
|
||||||
|
|
||||||
|
cd "$REPO"
|
||||||
|
git fetch origin 2>&1 || true
|
||||||
|
if ! git checkout "$BRANCH" 2>&1; then
|
||||||
|
die "git checkout failed: ${BRANCH}"
|
||||||
|
fi
|
||||||
|
# If BRANCH is a local branch (not a detached tag), fast-forward to the fetched
|
||||||
|
# remote so we build current upstream code, not a stale local copy.
|
||||||
|
if git show-ref --verify --quiet "refs/heads/${BRANCH}"; then
|
||||||
|
git merge --ff-only "origin/${BRANCH}" 2>&1 || true
|
||||||
|
fi
|
||||||
|
COMMIT=$(git rev-parse --short HEAD 2>/dev/null || echo "unknown")
|
||||||
|
|
||||||
|
BUILD_LOG=$(mktemp)
|
||||||
|
trap 'rm -f "$BUILD_LOG"' EXIT
|
||||||
|
|
||||||
|
# Build cargo args as an array (no unquoted string concatenation).
|
||||||
|
set -- -p "$CRATE"
|
||||||
|
[ "$RELEASE" = "true" ] && set -- "$@" --release
|
||||||
|
[ -n "$FEATURES" ] && set -- "$@" --features "$FEATURES"
|
||||||
|
|
||||||
|
if cargo build "$@" >"$BUILD_LOG" 2>&1; then
|
||||||
|
DURATION=$(( $(date +%s) - START_TS ))
|
||||||
|
BINARY=$(find target -maxdepth 3 -type f -name "$CRATE" -perm -111 | grep -v '/deps/' | head -1)
|
||||||
|
SIZE=$(stat -f '%z' "$BINARY" 2>/dev/null || echo "0")
|
||||||
|
tail -5 "$BUILD_LOG" >&2
|
||||||
|
printf '{"jsonrpc":"2.0","id":%s,"result":{"content":[{"type":"text","text":"{\\"success\\":true,\\"binary\\":\\"%s\\",\\"duration_s\\":%s,\\"branch\\":\\"%s\\",\\"commit\\":\\"%s\\",\\"size_bytes\\":%s}"}]}}\n' \
|
||||||
|
"$ID" "$BINARY" "$DURATION" "$BRANCH" "$COMMIT" "$SIZE"
|
||||||
|
else
|
||||||
|
DURATION=$(( $(date +%s) - START_TS ))
|
||||||
|
ERROR=$(tail -20 "$BUILD_LOG" | head -10 | tr '\n' ' ' | sed 's/"/\\"/g')
|
||||||
|
printf '{"jsonrpc":"2.0","id":%s,"error":{"code":-1,"message":"build failed in %ss: %s"}}\n' \
|
||||||
|
"$ID" "$DURATION" "$ERROR"
|
||||||
|
fi
|
||||||
32
packaging/mother/colibri-mcp-ssh
Executable file
32
packaging/mother/colibri-mcp-ssh
Executable file
|
|
@ -0,0 +1,32 @@
|
||||||
|
#!/bin/sh
|
||||||
|
# SSH forced-command wrapper for mother MCP entrypoint.
|
||||||
|
#
|
||||||
|
# SSH's authorized_keys command="..." restriction replaces the client's
|
||||||
|
# command with this script and stores the original in $SSH_ORIGINAL_COMMAND.
|
||||||
|
#
|
||||||
|
# Allowlist:
|
||||||
|
# "" → colibri-mcp in stdio MCP mode (persistent JSON-RPC channel)
|
||||||
|
# "tools" → colibri-mcp tools (one-shot discovery, debugging)
|
||||||
|
# everything else → rejected with exit 1
|
||||||
|
#
|
||||||
|
# Why: the wrapper's job is to constrain what callers can do through the
|
||||||
|
# SSH forced-command boundary. Without an allowlist, the caller can pass
|
||||||
|
# any colibri-mcp subcommand or flag — including ones not yet written.
|
||||||
|
#
|
||||||
|
# Installed by setup-mother.sh into /usr/local/bin/.
|
||||||
|
# Referenced from: ~/.ssh/authorized_keys command="/usr/local/bin/colibri-mcp-ssh"
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
case "${SSH_ORIGINAL_COMMAND:-}" in
|
||||||
|
"")
|
||||||
|
exec /usr/local/bin/colibri-mcp
|
||||||
|
;;
|
||||||
|
"tools")
|
||||||
|
exec /usr/local/bin/colibri-mcp tools
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
printf '{"jsonrpc":"2.0","id":null,"error":{"code":-1,"message":"rejected: %s"}}\n' \
|
||||||
|
"$(printf '%s' "${SSH_ORIGINAL_COMMAND}" | sed 's/"/\\"/g')" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
@ -1,6 +1,29 @@
|
||||||
CREATE TABLE IF NOT EXISTS usb_nodes (
|
-- Mother hive schema. Idempotent: safe to re-run (setup-mother.sh applies it).
|
||||||
|
--
|
||||||
|
-- A "node" is any host that has joined the hive and reports a hardware
|
||||||
|
-- profile — live-USB, disk-installed FreeBSD, a Linux box, a VPS, or the
|
||||||
|
-- mother itself. The boot/provisioning medium is an attribute (node_type),
|
||||||
|
-- not the table's identity.
|
||||||
|
|
||||||
|
-- Migration: usb_nodes → hive_nodes (rename in place; preserves data + the
|
||||||
|
-- owning sequence). Runs only on hosts created under the old name; a no-op on
|
||||||
|
-- fresh installs and on already-migrated hosts.
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF to_regclass('public.usb_nodes') IS NOT NULL
|
||||||
|
AND to_regclass('public.hive_nodes') IS NULL THEN
|
||||||
|
ALTER TABLE usb_nodes RENAME TO hive_nodes;
|
||||||
|
IF to_regclass('public.usb_nodes_id_seq') IS NOT NULL THEN
|
||||||
|
ALTER SEQUENCE usb_nodes_id_seq RENAME TO hive_nodes_id_seq;
|
||||||
|
END IF;
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS hive_nodes (
|
||||||
id SERIAL PRIMARY KEY,
|
id SERIAL PRIMARY KEY,
|
||||||
hostname TEXT NOT NULL UNIQUE,
|
hostname TEXT NOT NULL UNIQUE,
|
||||||
|
-- Provisioning medium: live-usb | disk | vps | mother | unknown.
|
||||||
|
node_type TEXT NOT NULL DEFAULT 'unknown',
|
||||||
last_seen TIMESTAMPTZ NOT NULL DEFAULT now(),
|
last_seen TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
first_seen TIMESTAMPTZ NOT NULL DEFAULT now(),
|
first_seen TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
freebsd_version JSONB,
|
freebsd_version JSONB,
|
||||||
|
|
@ -10,12 +33,17 @@ CREATE TABLE IF NOT EXISTS usb_nodes (
|
||||||
tags TEXT[] DEFAULT '{}',
|
tags TEXT[] DEFAULT '{}',
|
||||||
last_cap_sync TIMESTAMPTZ
|
last_cap_sync TIMESTAMPTZ
|
||||||
);
|
);
|
||||||
CREATE INDEX IF NOT EXISTS idx_nodes_status ON usb_nodes (status);
|
-- Add node_type to tables migrated from the old usb_nodes definition.
|
||||||
CREATE INDEX IF NOT EXISTS idx_nodes_last_seen ON usb_nodes (last_seen DESC);
|
ALTER TABLE hive_nodes ADD COLUMN IF NOT EXISTS node_type TEXT NOT NULL DEFAULT 'unknown';
|
||||||
CREATE INDEX IF NOT EXISTS idx_nodes_cap_has_gpu ON usb_nodes ((capabilities->>'has_gpu'));
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_nodes_status ON hive_nodes (status);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_nodes_last_seen ON hive_nodes (last_seen DESC);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_nodes_type ON hive_nodes (node_type);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_nodes_cap_has_gpu ON hive_nodes ((capabilities->>'has_gpu'));
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS build_queue (
|
CREATE TABLE IF NOT EXISTS build_queue (
|
||||||
id SERIAL PRIMARY KEY,
|
id SERIAL PRIMARY KEY,
|
||||||
node_id INTEGER REFERENCES usb_nodes(id),
|
node_id INTEGER REFERENCES hive_nodes(id),
|
||||||
crate TEXT NOT NULL DEFAULT 'colibri-daemon',
|
crate TEXT NOT NULL DEFAULT 'colibri-daemon',
|
||||||
branch TEXT NOT NULL DEFAULT 'main',
|
branch TEXT NOT NULL DEFAULT 'main',
|
||||||
release BOOLEAN NOT NULL DEFAULT true,
|
release BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
|
@ -31,11 +59,12 @@ CREATE TABLE IF NOT EXISTS build_queue (
|
||||||
error_log TEXT
|
error_log TEXT
|
||||||
);
|
);
|
||||||
CREATE INDEX IF NOT EXISTS idx_build_status ON build_queue (status, priority DESC, queued_at);
|
CREATE INDEX IF NOT EXISTS idx_build_status ON build_queue (status, priority DESC, queued_at);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS audit_log (
|
CREATE TABLE IF NOT EXISTS audit_log (
|
||||||
id BIGSERIAL PRIMARY KEY,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
event_ts TIMESTAMPTZ NOT NULL DEFAULT now(),
|
event_ts TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
event_type TEXT NOT NULL,
|
event_type TEXT NOT NULL,
|
||||||
node_id INTEGER REFERENCES usb_nodes(id),
|
node_id INTEGER REFERENCES hive_nodes(id),
|
||||||
build_id INTEGER REFERENCES build_queue(id),
|
build_id INTEGER REFERENCES build_queue(id),
|
||||||
details JSONB
|
details JSONB
|
||||||
);
|
);
|
||||||
|
|
@ -79,7 +108,7 @@ BEGIN
|
||||||
END;
|
END;
|
||||||
$$ LANGUAGE plpgsql;
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
DROP TRIGGER IF EXISTS trg_derive_capabilities ON usb_nodes;
|
DROP TRIGGER IF EXISTS trg_derive_capabilities ON hive_nodes;
|
||||||
CREATE TRIGGER trg_derive_capabilities
|
CREATE TRIGGER trg_derive_capabilities
|
||||||
BEFORE INSERT OR UPDATE OF hw_profile ON usb_nodes
|
BEFORE INSERT OR UPDATE OF hw_profile ON hive_nodes
|
||||||
FOR EACH ROW EXECUTE FUNCTION derive_capabilities();
|
FOR EACH ROW EXECUTE FUNCTION derive_capabilities();
|
||||||
|
|
|
||||||
88
packaging/mother/node-register-mcp
Executable file
88
packaging/mother/node-register-mcp
Executable file
|
|
@ -0,0 +1,88 @@
|
||||||
|
#!/bin/sh
|
||||||
|
# node-register — MCP tool: register a hive node's hardware profile in PostgreSQL.
|
||||||
|
#
|
||||||
|
# A node is any host that joined the hive (live-usb, disk, vps, mother), keyed
|
||||||
|
# by hostname. Accepts a JSON-RPC tools/call request on stdin, UPSERTs the
|
||||||
|
# hw_profile into mother_hive.hive_nodes, and returns the result to stdout. The
|
||||||
|
# derive_capabilities() trigger auto-computes has_gpu, gpu_vendor,
|
||||||
|
# can_run_local_llm, has_wifi, etc. on INSERT/UPDATE.
|
||||||
|
#
|
||||||
|
# Expected input:
|
||||||
|
# {"jsonrpc":"2.0","method":"tools/call","id":1,"params":{"name":"node_register","arguments":{"hostname":"clawdie-node","node_type":"live-usb","hw_profile":{...}}}}
|
||||||
|
#
|
||||||
|
# Output on success:
|
||||||
|
# {"jsonrpc":"2.0","id":1,"result":{"content":[{"type":"text","text":"{\"registered\":true,\"hostname\":\"clawdie-node\",\"capabilities\":{...}}"}]}}
|
||||||
|
#
|
||||||
|
# Security: psql :'variable' substitution expands to a safely single-quoted SQL
|
||||||
|
# string literal — no shell interpolation touches the JSON blobs.
|
||||||
|
#
|
||||||
|
# PostgreSQL access: peer auth for the 'colibri' OS user. The operator must
|
||||||
|
# run once as postgres (or setup-mother.sh does it):
|
||||||
|
# CREATE ROLE colibri WITH LOGIN;
|
||||||
|
# GRANT CONNECT ON DATABASE mother_hive TO colibri;
|
||||||
|
# GRANT INSERT, UPDATE ON hive_nodes TO colibri;
|
||||||
|
# GRANT USAGE ON SEQUENCE hive_nodes_id_seq TO colibri;
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
DB="mother_hive"
|
||||||
|
|
||||||
|
# Read JSON-RPC from stdin
|
||||||
|
INPUT=$(cat)
|
||||||
|
ID=$(echo "$INPUT" | jq -r '.id // "1"')
|
||||||
|
HOSTNAME=$(echo "$INPUT" | jq -r '.params.arguments.hostname // ""')
|
||||||
|
NODE_TYPE=$(echo "$INPUT" | jq -r '.params.arguments.node_type // "unknown"')
|
||||||
|
HW_PROFILE=$(echo "$INPUT" | jq -c '.params.arguments.hw_profile // {}')
|
||||||
|
|
||||||
|
# node_type is a small enum; validate so a bad value can't land an odd row.
|
||||||
|
case "$NODE_TYPE" in
|
||||||
|
live-usb|disk|vps|mother|unknown) ;;
|
||||||
|
*) NODE_TYPE="unknown" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ -z "$HOSTNAME" ]; then
|
||||||
|
printf '{"jsonrpc":"2.0","id":%s,"error":{"code":-1,"message":"missing required argument: hostname"}}\n' "$ID"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$HW_PROFILE" = "{}" ]; then
|
||||||
|
printf '{"jsonrpc":"2.0","id":%s,"error":{"code":-1,"message":"missing required argument: hw_profile"}}\n' "$ID"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Pass the JSON blobs via psql -v with :'variable' quoting. :'variable'
|
||||||
|
# expands to a safely single-quoted SQL string literal (backslashes, quotes,
|
||||||
|
# and newlines inside the JSON are escaped), so untrusted input cannot break
|
||||||
|
# out of the literal — no shell interpolation ever touches the SQL.
|
||||||
|
# NOTE: a heredoc (not -c) is required — psql only expands :'variable' in SQL
|
||||||
|
# read from stdin / -f, not from -c.
|
||||||
|
# ON_ERROR_STOP=1 makes psql exit non-zero on a SQL error; without it psql
|
||||||
|
# would continue past a failed statement and exit 0, hiding failures. stderr is
|
||||||
|
# folded into RESULT so the error branch can report what went wrong.
|
||||||
|
RESULT=$(psql -d "$DB" -tA -v ON_ERROR_STOP=1 \
|
||||||
|
-v hostname="$HOSTNAME" -v node_type="$NODE_TYPE" -v hw_profile="$HW_PROFILE" 2>&1 <<'PSQL'
|
||||||
|
BEGIN;
|
||||||
|
INSERT INTO hive_nodes (hostname, node_type, hw_profile, status, last_seen)
|
||||||
|
VALUES (:'hostname', :'node_type', (:'hw_profile')::jsonb, 'online', now())
|
||||||
|
ON CONFLICT (hostname) DO UPDATE
|
||||||
|
SET node_type = EXCLUDED.node_type,
|
||||||
|
hw_profile = EXCLUDED.hw_profile,
|
||||||
|
status = 'online',
|
||||||
|
last_seen = now();
|
||||||
|
SELECT json_build_object(
|
||||||
|
'registered', true,
|
||||||
|
'hostname', hostname,
|
||||||
|
'node_type', node_type,
|
||||||
|
'capabilities', capabilities
|
||||||
|
) FROM hive_nodes WHERE hostname = :'hostname';
|
||||||
|
COMMIT;
|
||||||
|
PSQL
|
||||||
|
) || {
|
||||||
|
printf '{"jsonrpc":"2.0","id":%s,"error":{"code":-1,"message":"psql failed: %s"}}\n' \
|
||||||
|
"$ID" "$(printf '%s' "$RESULT" | sed 's/"/\\"/g' | tr '\n' ' ')"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# RESULT is the JSON object from json_build_object (one line).
|
||||||
|
# Escape double quotes for JSON embedding in the MCP text field.
|
||||||
|
printf '{"jsonrpc":"2.0","id":%s,"result":{"content":[{"type":"text","text":"%s"}]}}\n' \
|
||||||
|
"$ID" "$(printf '%s' "$RESULT" | sed 's/"/\\"/g')"
|
||||||
293
packaging/mother/setup-mother.sh
Executable file
293
packaging/mother/setup-mother.sh
Executable file
|
|
@ -0,0 +1,293 @@
|
||||||
|
#!/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."
|
||||||
Loading…
Add table
Reference in a new issue