#!/usr/bin/env bash # provision-host-daemon.sh — set up the ZeroClaw agent node as a HOST systemd # service, NOT the App Lab container. # # WHY host, not container: the App Lab python-apps-base container can't actually # drive the board. Proven on-hardware: # • I2C/SPI: /dev is bind-mounted but the container's device cgroup blocks it # (EPERM), and the image lacks i2cdetect. # • Flashing: the image has no arduino-cli / Zephyr toolchain (uno_q_flash fails # "arduino-cli not found"). # • /admin/reload: refused (the container publishes :8080 via NAT, so even # host→localhost isn't loopback). # On the host the daemon has native /dev, arduino-cli + the Zephyr toolchain, and # loopback /admin/reload — everything the workshop needs. # # Usage (board on USB, cloud token in env): # export ANTHROPIC_OAUTH_TOKEN=sk-ant-oat01-… # ./deploy/uno-q/provision-host-daemon.sh # # Env: SERIAL (65301572), BOARD_PW (sudo password), ZEROCLAW_BIN (built aarch64 # binary). Assumes the base board provision already populated ~/.zeroclaw # (config.toml, .secret_key, agents, shared/skills) — see provision-uno-q.sh. set -u SERIAL="${SERIAL:-65301572}" BOARD_PW="${BOARD_PW:-clouddev249}" ZEROCLAW_BIN="${ZEROCLAW_BIN:-$HOME/projects/zeroclaw/target/aarch64-unknown-linux-gnu/release-fast/zeroclaw}" UNIT_DIR="$(cd "$(dirname "$0")/systemd" && pwd)" S(){ adb -s "$SERIAL" shell "$@"; } # run a command as root on the board (sudo -S reads the password from stdin) SU(){ adb -s "$SERIAL" shell "echo '$BOARD_PW' | sudo -S sh -c '$1'" 2>&1 | grep -iv 'password for'; } ok(){ printf ' \033[32m✓\033[0m %s\n' "$*"; } bad(){ printf ' \033[31m✗\033[0m %s\n' "$*"; } adb -s "$SERIAL" get-state >/dev/null 2>&1 || { bad "board $SERIAL not attached"; exit 1; } [ -f "$ZEROCLAW_BIN" ] || { bad "binary not found: $ZEROCLAW_BIN (build it first)"; exit 1; } [ -n "${ANTHROPIC_OAUTH_TOKEN:-}" ] || { bad "ANTHROPIC_OAUTH_TOKEN not set (cloud brain)"; exit 1; } echo "→ retire the App Lab container model (it can't reach the hardware)" S "cd /home/arduino/ArduinoApps/zeroclaw-node/.cache 2>/dev/null && docker compose -f app-compose.yaml down 2>/dev/null; arduino-app-cli properties set default none 2>/dev/null" >/dev/null 2>&1 ok "App Lab container down + default app cleared (won't grab :8080 on boot)" echo "→ deploy the host binary" adb -s "$SERIAL" push "$ZEROCLAW_BIN" /tmp/zeroclaw.new >/dev/null SU "install -m755 -o arduino -g arduino /tmp/zeroclaw.new /home/arduino/zeroclaw; rm -f /tmp/zeroclaw.new" ok "binary → /home/arduino/zeroclaw" echo "→ cloud credential env-file (raw token AND the config-override that wires api_key)" printf '%s' "$ANTHROPIC_OAUTH_TOKEN" | S "cat > /tmp/oat" S "T=\$(cat /tmp/oat); { printf 'ANTHROPIC_OAUTH_TOKEN=%s\n' \"\$T\"; printf 'ZEROCLAW_providers__models__anthropic__max__api_key=%s\n' \"\$T\"; } > /home/arduino/.zeroclaw/daemon.env; chmod 600 /home/arduino/.zeroclaw/daemon.env; rm -f /tmp/oat" ok "/home/arduino/.zeroclaw/daemon.env (0600, off the repo)" echo "→ Arduino flashing prerequisite (the Zephyr core hard-requires this library)" S "HOME=/home/arduino arduino-cli lib install Arduino_RouterBridge 2>&1 | tail -1" ok "Arduino_RouterBridge installed" echo "→ hardware device perms — udev rule so the agent can read I2C/SPI sensors" SU "printf 'KERNEL==\"i2c-[0-9]*\", MODE=\"0666\"\nKERNEL==\"spidev[0-9]*\", MODE=\"0666\"\n' > /etc/udev/rules.d/99-apess-hw.rules; udevadm control --reload; udevadm trigger --subsystem-match=i2c-dev >/dev/null 2>&1" ok "i2c/spi readable by the daemon user" echo "→ install + enable the systemd service (boot-persistent)" adb -s "$SERIAL" push "$UNIT_DIR/zeroclaw-daemon.service" /tmp/zeroclaw-daemon.service >/dev/null SU "install -m644 /tmp/zeroclaw-daemon.service /etc/systemd/system/zeroclaw-daemon.service; rm -f /tmp/zeroclaw-daemon.service; systemctl daemon-reload; systemctl enable --now zeroclaw-daemon.service" ok "zeroclaw-daemon.service enabled + started" echo "→ verify the gateway comes up" H="" for _ in 1 2 3 4 5 6 7 8 9 10; do sleep 3 H=$(S "curl -s -m3 http://127.0.0.1:8080/health -o /dev/null -w '%{http_code}' 2>/dev/null") [ "$H" = "200" ] && break done if [ "$H" = "200" ]; then ok "gateway healthy on :8080" echo "Done — node runs the ZeroClaw daemon on the host with full hardware access." else bad "gateway did not come up; inspect: adb -s $SERIAL shell 'sudo journalctl -u zeroclaw-daemon -n50'" exit 1 fi