Add clawdie rc.d service — coherent control plane entrypoint #21
2 changed files with 227 additions and 1 deletions
7
build.sh
7
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"'
|
||||
|
|
|
|||
221
live/operator-session/clawdie
Executable file
221
live/operator-session/clawdie
Executable file
|
|
@ -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"
|
||||
Loading…
Add table
Reference in a new issue