From 37e7c26fe3f12654426d6529e47deaedd1e0ae28 Mon Sep 17 00:00:00 2001 From: Clawdie Operator Date: Thu, 4 Jun 2026 12:57:54 +0000 Subject: [PATCH] =?UTF-8?q?Add=20clawdie=20rc.d=20service=20=E2=80=94=20co?= =?UTF-8?q?herent=20control=20plane=20entrypoint?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wraps colibri-daemon + skills + Glasspane into a single 'service clawdie'. Works on USB live and future disk installs. Commands: service clawdie start — verifies socket, writes startup marker service clawdie health — checks daemon, skills, Glasspane (3/3 ✓) service clawdie inventory — runtime manifest (pi, node, npm, colibri) service clawdie status — startup marker check build.sh: installed to /usr/local/etc/rc.d/clawdie, enabled YES. Skills: added disk-deploy and clawdie-health to seed catalog. --- build.sh | 7 +- live/operator-session/clawdie | 221 ++++++++++++++++++++++++++++++++++ 2 files changed, 227 insertions(+), 1 deletion(-) create mode 100755 live/operator-session/clawdie diff --git a/build.sh b/build.sh index e8c23415..07a52112 100755 --- a/build.sh +++ b/build.sh @@ -801,7 +801,9 @@ install_colibri_service() { ('$(uuidgen || echo 00000000-0000-0000-0000-000000000001)', 'freebsd-live-usb', 'FreeBSD live USB operator workstation procedures', 'freebsd', '${_now}'), ('$(uuidgen || echo 00000000-0000-0000-0000-000000000002)', 'colibri-smoke', 'Colibri daemon smoke test and validation', 'colibri', '${_now}'), ('$(uuidgen || echo 00000000-0000-0000-0000-000000000003)', 'iso-build', 'Clawdie ISO build and staging workflow', 'iso', '${_now}'), - ('$(uuidgen || echo 00000000-0000-0000-0000-000000000004)', 'tailscale-join', 'Tailscale mesh join procedure for operator USB', 'networking', '${_now}');" 2>/dev/null || true + ('$(uuidgen || echo 00000000-0000-0000-0000-000000000004)', 'tailscale-join', 'Tailscale mesh join procedure for operator USB', 'networking', '${_now}'), + ('$(uuidgen || echo 00000000-0000-0000-0000-000000000005)', 'disk-deploy', 'Deploy Clawdie from USB live to permanent disk install. Provisions ZFS pool, installs FreeBSD boot environment, migrates config, and sets up clawdie service for persistent operation.', 'clawdie', '${_now}'), + ('$(uuidgen || echo 00000000-0000-0000-0000-000000000006)', 'clawdie-health', 'Run clawdie service health check — verifies colibri daemon, skills catalog, Glasspane, and runtime inventory. Use for post-deploy validation.', 'clawdie', '${_now}');" 2>/dev/null || true chroot "${MOUNT_POINT}" chown colibri:colibri /var/db/colibri/colibri.sqlite 2>/dev/null || true echo " colibri skills seeded: 4 entries" fi @@ -1093,6 +1095,8 @@ EOF "${MOUNT_POINT}/usr/local/etc/rc.d/clawdie_live_wifi" install -m 0755 "${LIVE_SESSION_DIR}/clawdie-live-seed" \ "${MOUNT_POINT}/usr/local/etc/rc.d/clawdie_live_seed" + install -m 0755 "${LIVE_SESSION_DIR}/clawdie" \ + "${MOUNT_POINT}/usr/local/etc/rc.d/clawdie" install -m 0755 "${LIVE_SESSION_DIR}/clawdie-live-resolver" \ "${MOUNT_POINT}/usr/local/etc/rc.d/clawdie_live_resolver" install -m 0755 "${LIVE_SESSION_DIR}/clawdie-live-audio" \ @@ -1425,6 +1429,7 @@ EOF set_config_line "${MOUNT_POINT}/etc/rc.conf" 'clawdie_live_gpu_enable="YES"' set_config_line "${MOUNT_POINT}/etc/rc.conf" 'clawdie_live_wifi_enable="YES"' set_config_line "${MOUNT_POINT}/etc/rc.conf" 'clawdie_live_seed_enable="YES"' + set_config_line "${MOUNT_POINT}/etc/rc.conf" 'clawdie_enable="YES"' set_config_line "${MOUNT_POINT}/etc/rc.conf" 'clawdie_live_resolver_enable="YES"' set_config_line "${MOUNT_POINT}/etc/rc.conf" 'clawdie_live_audio_enable="YES"' set_config_line "${MOUNT_POINT}/etc/rc.conf" 'clawdie_live_power_enable="YES"' diff --git a/live/operator-session/clawdie b/live/operator-session/clawdie new file mode 100755 index 00000000..9cd17a74 --- /dev/null +++ b/live/operator-session/clawdie @@ -0,0 +1,221 @@ +#!/bin/sh +# +# Clawdie agent service — coherent entrypoint for the colibri control plane. +# +# On USB live: wraps colibri-daemon, skills catalog, Glasspane, DeepSeek cache. +# On disk install: adds runtime inventory, watchdog, and persistent agent config. +# +# This service does NOT replace the operator's terminal session (XFCE/bootstrap). +# It ensures the control plane is alive and healthy before the operator logs in. +# +# PROVIDE: clawdie +# REQUIRE: LOGIN colibri_daemon +# KEYWORD: shutdown + +. /etc/rc.subr + +name="clawdie" +rcvar="clawdie_enable" + +load_rc_config "$name" + +: ${clawdie_enable:="YES"} +: ${clawdie_user:="clawdie"} +: ${clawdie_group:="clawdie"} +: ${clawdie_config_dir:="/usr/local/etc/clawdie"} +: ${clawdie_data_dir:="/var/db/clawdie"} +: ${clawdie_log_dir:="/var/log/clawdie"} +: ${clawdie_run_dir:="/var/run/clawdie"} + +start_precmd="clawdie_prestart" +start_cmd="clawdie_start" +stop_cmd="clawdie_stop" +status_cmd="clawdie_status" +extra_commands="health inventory" +health_cmd="clawdie_health" +inventory_cmd="clawdie_inventory" + +PATH="/usr/local/bin:/opt/clawdie/npm-global/bin:${PATH}" +export PATH + +SOCKET="/var/run/colibri/colibri.sock" + +clawdie_prestart() +{ + install -d -o "${clawdie_user}" -g "${clawdie_group}" -m 0755 "${clawdie_data_dir}" + install -d -o "${clawdie_user}" -g "${clawdie_group}" -m 0755 "${clawdie_log_dir}" + install -d -o "${clawdie_user}" -g "${clawdie_group}" -m 0755 "${clawdie_run_dir}" + install -d -m 0755 "${clawdie_config_dir}" + + export PATH="/usr/local/bin:/opt/clawdie/npm-global/bin:${PATH}" + export COLIBRI_DAEMON_SOCKET="${SOCKET}" + export COLIBRI_HOST="$(/bin/hostname)" + export CLAWDIE_CONFIG_DIR="${clawdie_config_dir}" + export CLAWDIE_DATA_DIR="${clawdie_data_dir}" + export CLAWDIE_LOG_DIR="${clawdie_log_dir}" +} + +clawdie_start() +{ + touch "${clawdie_log_dir}/agent.log" + chown "${clawdie_user}:${clawdie_group}" "${clawdie_log_dir}/agent.log" 2>/dev/null || true + + _timeout=10 + _waited=0 + while [ ! -S "${SOCKET}" ] && [ $_waited -lt $_timeout ]; do + sleep 1 + _waited=$((_waited + 1)) + done + + if [ ! -S "${SOCKET}" ]; then + echo "${name}: WARNING: colibri socket not ready after ${_timeout}s" + fi + + echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) ${name} started, host=$(/bin/hostname)" \ + > "${clawdie_run_dir}/startup-marker" + + echo "${name}: colibri control plane ready (host=$COLIBRI_HOST)" +} + +clawdie_stop() +{ + echo "${name}: stopping (colibri_daemon handles subprocess lifecycle)" + rm -f "${clawdie_run_dir}/startup-marker" +} + +clawdie_status() +{ + if [ -f "${clawdie_run_dir}/startup-marker" ]; then + echo "${name}: running (marker: $(cat ${clawdie_run_dir}/startup-marker))" + else + echo "${name}: not running (no startup marker)" + return 1 + fi +} + +clawdie_health() +{ + /usr/local/bin/python3.11 - "$SOCKET" << 'PYHEALTH' +import json, socket, sys +sock_path = sys.argv[1] +ok = 0 +fail = 0 + +def query(cmd): + s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + s.settimeout(3) + s.connect(sock_path) + s.sendall(f'{{"cmd":"{cmd}"}}\n'.encode()) + data = b'' + while True: + chunk = s.recv(4096) + if not chunk: break + data += chunk + if b'\n' in data: break + s.close() + return json.loads(data.decode().strip()) + +# 1. Daemon reachable +if query("status").get("ok"): + print(" ✓ colibri-daemon: responding") + ok += 1 +else: + print(" ✗ colibri-daemon: no response") + fail += 1 + +# 2. Skills catalog +skills = query("list-skills").get("data", []) +if len(skills) > 0: + print(f" ✓ skills catalog: {len(skills)} skills") + ok += 1 +else: + print(" ✗ skills catalog: empty or unreachable") + fail += 1 + +# 3. Glasspane +snap = query("glasspane-snapshot").get("data", {}) +panes = snap.get("panes", []) +print(f" ✓ glasspane: {len(panes)} panes") +ok += 1 + +# 4. Runtime inventory +import os +if os.path.exists("/usr/local/bin/colibri-host-status"): + print(" ✓ runtime inventory: available") + ok += 1 +else: + print(" - runtime inventory: not installed (USB live mode)") + +print() +if fail == 0: + print(f"clawdie: healthy ({ok} checks passed)") + sys.exit(0) +else: + print(f"clawdie: degraded ({ok} passed, {fail} failed)") + sys.exit(1) +PYHEALTH +} + +clawdie_inventory() +{ + /usr/local/bin/python3.11 - "$SOCKET" "$(/bin/hostname)" << 'PYINV' +import json, socket, sys, platform, subprocess + +sock_path = sys.argv[1] +hostname = sys.argv[2] + +def run(cmd): + try: + return subprocess.check_output(cmd, shell=True, text=True, stderr=subprocess.DEVNULL).strip() + except: + return None + +def query(cmd): + s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + s.settimeout(3) + s.connect(sock_path) + s.sendall(f'{{"cmd":"{cmd}"}}\n'.encode()) + data = b'' + while True: + chunk = s.recv(4096) + if not chunk: break + data += chunk + if b'\n' in data: break + s.close() + return json.loads(data.decode().strip()) + +inv = { + 'schema': 'clawdie.runtime-version-inventory.v1', + 'host': hostname, + 'os': run('freebsd-version') or platform.system(), + 'node': run('node --version'), + 'npm': run('npm --version'), + 'package_manager': 'pkg', +} + +pi_ver = run('/opt/clawdie/npm-global/bin/pi --version 2>&1') +if pi_ver: + for line in pi_ver.split('\n'): + line = line.strip() + if line: + parts = line.split() + inv['pi'] = parts[-1] if parts else line + break + +try: + status = query('status').get('data', {}) + inv['notes'] = [ + f"colibri host={status.get('host', '?')}", + f"colibri version={status.get('version', '?')}", + f"agents={status.get('agents', 0)}", + ] +except Exception: + inv['notes'] = ['colibri daemon unreachable'] + +print(json.dumps(inv, indent=2)) +PYINV +} + +load_rc_config "$name" +: ${clawdie_enable:="YES"} +run_rc_command "$1" -- 2.45.3