Per Hermes' review of the cred-capture flow: after the daemon restart that loads the pulled keys, poll colibri status (up to 10s) for a live agent so the operator sees confirmation that the Pi auto-spawn actually came up — instead of just "daemon restarted". Prints "Pi agent is live." or a check hint. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
275 lines
9.6 KiB
Bash
Executable file
275 lines
9.6 KiB
Bash
Executable file
#!/bin/sh
|
|
# One-click "Join Hive" — registers this machine as a Colibri agent.
|
|
# Runs in a visible terminal so the operator sees the result.
|
|
# Idempotent: safe to re-run on an already-registered agent.
|
|
|
|
SOCKET="${COLIBRI_SOCKET:-/var/run/colibri/colibri.sock}"
|
|
PROVIDER_ENV="/usr/local/etc/colibri/provider.env"
|
|
|
|
finish() {
|
|
_code="${1:-0}"
|
|
echo ""
|
|
echo "Press Enter to close."
|
|
read -r _
|
|
exit "${_code}"
|
|
}
|
|
|
|
have() {
|
|
command -v "$1" >/dev/null 2>&1
|
|
}
|
|
|
|
# Read a value into the variable named by $1 without echoing keystrokes.
|
|
# Used for the Vaultwarden secret + password so they never appear on screen.
|
|
read_secret() {
|
|
printf '%s' "$2"
|
|
stty -echo 2>/dev/null
|
|
read -r "$1"
|
|
stty echo 2>/dev/null
|
|
echo ""
|
|
}
|
|
|
|
# Upsert BW_CLIENTID/BW_CLIENTSECRET/BW_PASSWORD into provider.env (root-owned,
|
|
# 0600) without leaking secrets through process arguments. The values stay in
|
|
# shell variables and a 0600 temp file; provider.env is read and written via mdo
|
|
# so the unprivileged operator session can update it.
|
|
write_provider_bw() {
|
|
_cache="${HOME:-/home/clawdie}/.cache/clawdie"
|
|
mkdir -p "$_cache" 2>/dev/null
|
|
chmod 0700 "$_cache" 2>/dev/null
|
|
_tmp="$(mktemp "${_cache}/joinhive.XXXXXX")" || return 1
|
|
chmod 0600 "$_tmp" 2>/dev/null
|
|
|
|
if have mdo; then
|
|
mdo -u root cat "$PROVIDER_ENV" >"$_tmp" 2>/dev/null || :
|
|
else
|
|
cat "$PROVIDER_ENV" >"$_tmp" 2>/dev/null || :
|
|
fi
|
|
|
|
# Drop any existing BW_* definitions, then append the freshly entered ones.
|
|
grep -vE '^(BW_CLIENTID|BW_CLIENTSECRET|BW_PASSWORD)=' "$_tmp" >"${_tmp}.new" 2>/dev/null || : >"${_tmp}.new"
|
|
{
|
|
printf 'BW_CLIENTID=%s\n' "$BW_CLIENTID"
|
|
printf 'BW_CLIENTSECRET=%s\n' "$BW_CLIENTSECRET"
|
|
printf 'BW_PASSWORD=%s\n' "$BW_PASSWORD"
|
|
} >>"${_tmp}.new"
|
|
mv "${_tmp}.new" "$_tmp"
|
|
chmod 0600 "$_tmp" 2>/dev/null
|
|
|
|
if have mdo; then
|
|
mdo -u root cp "$_tmp" "$PROVIDER_ENV" && mdo -u root chmod 0600 "$PROVIDER_ENV"
|
|
_rc=$?
|
|
else
|
|
cp "$_tmp" "$PROVIDER_ENV" && chmod 0600 "$PROVIDER_ENV"
|
|
_rc=$?
|
|
fi
|
|
|
|
rm -f "$_tmp" "${_tmp}.new" 2>/dev/null
|
|
return $_rc
|
|
}
|
|
|
|
provider_env_has_bw_creds() {
|
|
_check='test -f "$1" && grep -Eq "^BW_CLIENTID=.+" "$1" && grep -Eq "^BW_CLIENTSECRET=.+" "$1" && grep -Eq "^BW_PASSWORD=.+" "$1"'
|
|
|
|
if have mdo; then
|
|
mdo -u root sh -c "${_check}" sh "${PROVIDER_ENV}" >/dev/null 2>&1
|
|
return $?
|
|
fi
|
|
|
|
sh -c "${_check}" sh "${PROVIDER_ENV}" >/dev/null 2>&1
|
|
}
|
|
|
|
echo "========================================"
|
|
echo " Clawdie — Join Hive"
|
|
echo "========================================"
|
|
echo ""
|
|
|
|
# 1. Check daemon
|
|
if [ ! -S "$SOCKET" ]; then
|
|
echo "[1/4] Starting colibri daemon..."
|
|
if have mdo; then
|
|
if ! mdo -u root service colibri_daemon start; then
|
|
echo " WARNING: could not start colibri_daemon via mdo."
|
|
fi
|
|
elif [ "$(id -u)" -eq 0 ]; then
|
|
if ! service colibri_daemon start; then
|
|
echo " WARNING: could not start colibri_daemon."
|
|
fi
|
|
else
|
|
echo " WARNING: mdo is unavailable and this user is not root."
|
|
fi
|
|
|
|
_tries=0
|
|
while [ "$_tries" -lt 5 ] && [ ! -S "$SOCKET" ]; do
|
|
sleep 1
|
|
_tries=$(( _tries + 1 ))
|
|
done
|
|
else
|
|
echo "[1/4] Daemon already running."
|
|
fi
|
|
|
|
if [ ! -S "$SOCKET" ]; then
|
|
echo " ERROR: Colibri socket is still missing: ${SOCKET}"
|
|
echo " Check: mdo -u root service colibri_daemon status"
|
|
finish 1
|
|
fi
|
|
|
|
# 2. Vault creds + provider keys. provider.env is intentionally 0600 (root), so
|
|
# read/write via mdo. If the 3 bootstrap values are absent, prompt for them and
|
|
# save them; then pull the provider keys (DeepSeek, etc.) from Vaultwarden.
|
|
echo "[2/4] Vault credentials..."
|
|
if ! provider_env_has_bw_creds; then
|
|
echo ""
|
|
echo " No Vaultwarden bootstrap credentials found."
|
|
echo " Enter the 3 values, or press Enter at the first prompt to skip."
|
|
echo ""
|
|
printf " BW_CLIENTID: "
|
|
read -r BW_CLIENTID
|
|
if [ -n "${BW_CLIENTID:-}" ]; then
|
|
read_secret BW_CLIENTSECRET " BW_CLIENTSECRET (hidden): "
|
|
read_secret BW_PASSWORD " BW_PASSWORD (hidden): "
|
|
if [ -n "${BW_CLIENTSECRET:-}" ] && [ -n "${BW_PASSWORD:-}" ]; then
|
|
if write_provider_bw; then
|
|
echo " Saved to ${PROVIDER_ENV} (0600)."
|
|
else
|
|
echo " ERROR: could not write ${PROVIDER_ENV}."
|
|
fi
|
|
else
|
|
echo " Skipped: secret or password was empty."
|
|
fi
|
|
unset BW_CLIENTSECRET BW_PASSWORD
|
|
else
|
|
echo " Skipped credential entry."
|
|
fi
|
|
unset BW_CLIENTID
|
|
fi
|
|
|
|
if provider_env_has_bw_creds; then
|
|
echo " Pulling provider keys from Vaultwarden..."
|
|
if have clawdie-vault-fetch && have bw && have mdo; then
|
|
if mdo -u root clawdie-vault-fetch --bootstrap "$PROVIDER_ENV" --write-env "$PROVIDER_ENV"; then
|
|
echo " Provider keys updated (DeepSeek and any others present)."
|
|
echo " Restarting colibri daemon to load the new keys..."
|
|
if mdo -u root service colibri_daemon restart >/dev/null 2>&1; then
|
|
_t=0
|
|
while [ "$_t" -lt 5 ] && [ ! -S "$SOCKET" ]; do
|
|
sleep 1
|
|
_t=$(( _t + 1 ))
|
|
done
|
|
# Confirm the auto-spawned Pi came up (colibri auto-spawn on boot).
|
|
if have colibri; then
|
|
_p=0
|
|
while [ "$_p" -lt 10 ]; do
|
|
if colibri --socket "$SOCKET" status 2>/dev/null | grep -q '"agents":[1-9]'; then
|
|
echo " Pi agent is live."
|
|
break
|
|
fi
|
|
sleep 1
|
|
_p=$(( _p + 1 ))
|
|
done
|
|
[ "$_p" -lt 10 ] || echo " NOTE: no Pi agent yet — check: colibri status"
|
|
fi
|
|
else
|
|
echo " WARNING: daemon restart failed."
|
|
echo " Run: mdo -u root service colibri_daemon restart"
|
|
fi
|
|
else
|
|
echo " WARNING: vault fetch did not complete (check network / credentials)."
|
|
echo " The manual wizard remains available; keys can be added later."
|
|
fi
|
|
else
|
|
echo " NOTE: clawdie-vault-fetch, bw, or mdo unavailable — skipping key pull."
|
|
fi
|
|
else
|
|
echo " WARNING: provider.env still lacks BW_CLIENTID/BW_CLIENTSECRET/BW_PASSWORD."
|
|
echo " Vault provisioning is skipped until they are added."
|
|
fi
|
|
|
|
# 3. Detect capabilities
|
|
HOST=$(hostname 2>/dev/null || echo "clawdie-live")
|
|
OS=$(uname -s 2>/dev/null | tr '[:upper:]' '[:lower:]')
|
|
[ -n "$OS" ] || OS="unknown"
|
|
CAPS="$OS,shell"
|
|
|
|
# Add optional capabilities
|
|
have colibri && CAPS="$CAPS,colibri"
|
|
have hermes && CAPS="$CAPS,hermes"
|
|
have pi && CAPS="$CAPS,pi"
|
|
if have tailscale && tailscale status >/dev/null 2>&1; then
|
|
CAPS="$CAPS,tailscale"
|
|
fi
|
|
[ "$OS" = "freebsd" ] && CAPS="$CAPS,rc.d,jail,zfs"
|
|
|
|
# image-render when Pillow imports (py311-pillow on python3=3.11); screenshot
|
|
# also needs a live display (the XFCE session).
|
|
if python3 -c 'import PIL' >/dev/null 2>&1; then
|
|
CAPS="$CAPS,image-render"
|
|
[ -n "${DISPLAY:-}" ] && CAPS="$CAPS,screenshot"
|
|
fi
|
|
|
|
if have python3; then
|
|
CAPS_JSON=$(printf '%s' "$CAPS" | python3 -c 'import json,sys; print(json.dumps(sys.stdin.read().strip().split(",")))' 2>/dev/null)
|
|
else
|
|
CAPS_JSON='["shell"]'
|
|
fi
|
|
[ -n "$CAPS_JSON" ] || CAPS_JSON='["shell"]'
|
|
|
|
AGENT_NAME="${HOST}"
|
|
echo "[3/4] Registering agent: ${AGENT_NAME}"
|
|
echo " capabilities: ${CAPS}"
|
|
|
|
if ! have nc; then
|
|
echo " ERROR: nc is not installed; cannot talk to ${SOCKET}."
|
|
finish 1
|
|
fi
|
|
|
|
RESP=$(printf '{"cmd":"register-agent","name":"%s","capabilities":%s}\n' \
|
|
"$AGENT_NAME" "$CAPS_JSON" \
|
|
| nc -U "$SOCKET" -w 3 2>/dev/null)
|
|
|
|
if echo "$RESP" | grep -q '"ok":true'; then
|
|
echo " registered."
|
|
elif echo "$RESP" | grep -Eiq 'already exists|unique constraint|constraint failed|agents\.name'; then
|
|
echo " already registered (idempotent)."
|
|
else
|
|
echo " registration did not complete cleanly."
|
|
if [ -n "$RESP" ]; then
|
|
echo " response: ${RESP}"
|
|
else
|
|
echo " no response from ${SOCKET}"
|
|
fi
|
|
finish 1
|
|
fi
|
|
|
|
# 4. Apply identity wallpaper as visual confirmation
|
|
echo "[4/4] Agent ${AGENT_NAME} is live on the Colibri board."
|
|
echo ""
|
|
|
|
if have clawdie-wallpaper-gen && have xfconf-query; then
|
|
echo " Setting identity wallpaper..."
|
|
# Let the generator pick a policy-compliant path (project-local tmp/ or an
|
|
# app-owned cache dir) and report it on stdout — no host-global /tmp here.
|
|
WP=$(clawdie-wallpaper-gen 2>/dev/null)
|
|
if [ -n "$WP" ] && [ -f "$WP" ]; then
|
|
# XFCE keys backdrops by connector name (monitorHDMI-1, monitoreDP-1, ...),
|
|
# not a fixed "monitor0". Set every existing last-image property so the
|
|
# change actually applies on real hardware.
|
|
_applied=0
|
|
for _prop in $(xfconf-query -c xfce4-desktop -l 2>/dev/null | grep '/last-image$'); do
|
|
xfconf-query -c xfce4-desktop -p "$_prop" -s "$WP" 2>/dev/null && _applied=1
|
|
done
|
|
# First boot / headless: no backdrop props exist yet — create the default.
|
|
if [ "$_applied" -eq 0 ]; then
|
|
xfconf-query -c xfce4-desktop \
|
|
-p /backdrop/screen0/monitor0/workspace0/last-image \
|
|
-n -t string -s "$WP" 2>/dev/null
|
|
fi
|
|
xfdesktop --reload >/dev/null 2>&1 || true
|
|
fi
|
|
fi
|
|
|
|
echo " Check: colibri status"
|
|
echo " Tasks: colibri list-tasks --status started"
|
|
echo " Board: colibri list-agents"
|
|
echo ""
|
|
echo "Hive joined."
|
|
finish 0
|