2026-06-20 10:59:07 +02:00
#!/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"
2026-06-20 12:16:11 +02:00
finish( ) {
_code = " ${ 1 :- 0 } "
echo ""
echo "Press Enter to close."
read -r _
exit " ${ _code } "
}
have( ) {
command -v " $1 " >/dev/null 2>& 1
}
2026-06-21 18:22:45 +02:00
# 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
}
2026-06-20 12:16:11 +02:00
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
}
2026-06-20 10:59:07 +02:00
echo "========================================"
echo " Clawdie — Join Hive"
echo "========================================"
echo ""
# 1. Check daemon
if [ ! -S " $SOCKET " ] ; then
echo "[1/4] Starting colibri daemon..."
2026-06-20 12:16:11 +02:00
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
2026-06-20 10:59:07 +02:00
else
echo "[1/4] Daemon already running."
fi
2026-06-20 12:16:11 +02:00
if [ ! -S " $SOCKET " ] ; then
echo " ERROR: Colibri socket is still missing: ${ SOCKET } "
echo " Check: mdo -u root service colibri_daemon status"
finish 1
fi
2026-06-21 18:22:45 +02:00
# 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
2026-06-20 12:16:11 +02:00
if provider_env_has_bw_creds; then
2026-06-21 18:22:45 +02:00
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
2026-06-23 18:08:58 +02:00
# Confirm the auto-spawned agent came up (colibri auto-spawn on boot).
2026-06-21 18:59:15 +02:00
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
2026-06-23 18:08:58 +02:00
echo " Agent is live."
2026-06-21 18:59:15 +02:00
break
fi
sleep 1
_p = $(( _p + 1 ))
done
2026-06-23 18:08:58 +02:00
[ " $_p " -lt 10 ] || echo " NOTE: no agent yet — check: colibri status"
2026-06-21 18:59:15 +02:00
fi
2026-06-21 18:22:45 +02:00
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
2026-06-20 10:59:07 +02:00
else
2026-06-21 18:22:45 +02:00
echo " WARNING: provider.env still lacks BW_CLIENTID/BW_CLIENTSECRET/BW_PASSWORD."
echo " Vault provisioning is skipped until they are added."
2026-06-20 10:59:07 +02:00
fi
2026-06-21 21:11:37 +02:00
# 2b. Tailscale auth key (separate from vault-fetch — a single standalone item).
if have bw && provider_env_has_bw_creds && ! tailscale status >/dev/null 2>& 1; then
echo " Fetching Tailscale auth key from vault..."
_tskey = " $( mdo -u root sh -c '
set -eu
. /usr/local/etc/colibri/provider.env
# Reuse existing session if possible; otherwise unlock fresh.
SESSION = " $( bw unlock --passwordenv BW_PASSWORD --raw 2>/dev/null || true ) "
if [ -z " $SESSION " ] ; then
bw login --apikey >/dev/null 2>& 1
SESSION = " $( bw unlock --passwordenv BW_PASSWORD --raw 2>/dev/null) "
fi
# Handle both naming conventions: hyphenated in vault, underscored env var.
ITEM = " $( bw list items --search tailscale-auth-key --session " $SESSION " 2>/dev/null | \
python3 -c "import sys,json; items=json.load(sys.stdin); ts=[i for i in items if i.get(\"name\",\"\") in (\"tailscale-auth-key\",\"tailscale_auth_key\")]; print(ts[0][\"notes\"] if ts else \"\")" 2>/dev/null || true ) "
bw lock >/dev/null 2>& 1 || true
printf "%s" " $ITEM "
' 2>/dev/null) "
if [ -n " ${ _tskey :- } " ] ; then
echo " $_tskey " | grep -q '^tskey-auth-' && {
2026-06-21 21:48:14 +02:00
# Pass the key via stdin, not argv, so it never appears in ps.
printf '%s' " $_tskey " | mdo -u root sh -c '
set -eu
f = "/usr/local/etc/colibri/provider.env"
read -r k
printf "TAILSCALE_AUTH_KEY=%s\n" " $k " >> " $f "
chmod 0600 " $f "
'
2026-06-21 21:11:37 +02:00
echo " TAILSCALE_AUTH_KEY written to provider.env."
2026-06-21 21:48:14 +02:00
# onestart: the service defaults to enable=NO on the OOTB image, and
# onestart bypasses rcvar. With required_files removed it reads the
# key from provider.env and strips it after a successful join.
if mdo -u root service clawdie_tailscale_up onestart >/dev/null 2>& 1; then
2026-06-21 21:11:37 +02:00
echo " Tailscale joined ( $( tailscale status 2>/dev/null | head -1 || echo 'up' ) ). "
else
echo " WARNING: tailscale up failed — check the key in Vaultwarden."
fi
} || echo " WARNING: Vaultwarden item found but does not look like an auth key."
else
echo " No tailscale-auth-key item in Vaultwarden (create one to auto-join)."
fi
fi
2026-06-20 10:59:07 +02:00
# 3. Detect capabilities
2026-06-20 12:16:11 +02:00
HOST = $( hostname 2>/dev/null || echo "clawdie-live" )
OS = $( uname -s 2>/dev/null | tr '[:upper:]' '[:lower:]' )
[ -n " $OS " ] || OS = "unknown"
2026-06-20 10:59:07 +02:00
CAPS = " $OS ,shell "
# Add optional capabilities
2026-06-20 12:16:11 +02:00
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
2026-06-20 10:59:07 +02:00
[ " $OS " = "freebsd" ] && CAPS = " $CAPS ,rc.d,jail,zfs "
2026-06-21 09:57:25 +02:00
# 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
2026-06-20 12:16:11 +02:00
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"]'
2026-06-20 10:59:07 +02:00
AGENT_NAME = " ${ HOST } "
echo " [3/4] Registering agent: ${ AGENT_NAME } "
echo " capabilities: ${ CAPS } "
2026-06-20 12:16:11 +02:00
if ! have nc; then
echo " ERROR: nc is not installed; cannot talk to ${ SOCKET } . "
finish 1
fi
2026-06-20 10:59:07 +02:00
RESP = $( printf '{"cmd":"register-agent","name":"%s","capabilities":%s}\n' \
2026-06-20 12:16:11 +02:00
" $AGENT_NAME " " $CAPS_JSON " \
2026-06-20 10:59:07 +02:00
| nc -U " $SOCKET " -w 3 2>/dev/null)
if echo " $RESP " | grep -q '"ok":true' ; then
echo " registered."
2026-06-20 12:16:11 +02:00
elif echo " $RESP " | grep -Eiq 'already exists|unique constraint|constraint failed|agents\.name' ; then
2026-06-20 10:59:07 +02:00
echo " already registered (idempotent)."
else
2026-06-20 12:16:11 +02:00
echo " registration did not complete cleanly."
if [ -n " $RESP " ] ; then
echo " response: ${ RESP } "
else
echo " no response from ${ SOCKET } "
fi
finish 1
2026-06-20 10:59:07 +02:00
fi
2026-06-20 12:16:11 +02:00
# 4. Apply identity wallpaper as visual confirmation
2026-06-20 10:59:07 +02:00
echo " [4/4] Agent ${ AGENT_NAME } is live on the Colibri board. "
echo ""
2026-06-20 12:16:11 +02:00
2026-06-20 14:38:03 +02:00
if have clawdie-wallpaper-gen && have xfconf-query; then
2026-06-20 12:16:11 +02:00
echo " Setting identity wallpaper..."
2026-06-20 14:38:03 +02:00
# 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
2026-06-20 12:16:11 +02:00
fi
fi
2026-06-20 10:59:07 +02:00
echo " Check: colibri status"
echo " Tasks: colibri list-tasks --status started"
echo " Board: colibri list-agents"
echo ""
2026-06-20 12:16:11 +02:00
echo "Hive joined."
finish 0