#!/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 agent 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 " Agent is live." break fi sleep 1 _p=$(( _p + 1 )) done [ "$_p" -lt 10 ] || echo " NOTE: no 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 # 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-' && { # 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" ' echo " TAILSCALE_AUTH_KEY written to provider.env." # 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 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 # 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