feat(uno-q): LAN-open boot + Telegram self-serve + guided lockdown modalities
Boards now boot LAN-open (host 0.0.0.0, allow_public_bind, require_pairing=false) so a team reaches its node's embedded ZeroClaw dashboard + web chat directly by IP with no token — the 'Open your node' path. Two more modalities ride the same default (cloud-first) agent: - Telegram: config seeds [channels.telegram.default] disabled + pre-bound to the default agent; a team enables it from the dashboard by pasting a @BotFather token. Dashboard writes only set pending_reload, and /admin/reload is loopback-only, so a new zeroclaw-reload-watcher.sh (loopback) applies dashboard config edits within seconds — no shell. provision-fleet pushes + launches it. - Lockdown: zeroclaw-lockdown.sh flips require_pairing=true, reloads, and mints a pair code (open -> locked harden step); documented as a board/adb action since minting + reload are loopback-only. - Voice: seeds a commented [channels.voice_duplex.default] + documents the aarch64 build (embedded-web + gateway-voice-duplex); build/hardware work is separate. Onboarding scripts (apess-selfregister.sh, provision-uno-q.sh) now tolerate open-boot: when the gateway mints no pair code, they announce with a placeholder token (the open board ignores auth) instead of hard-failing. Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
af9cae0abc
commit
7233a4b1c0
+71
-8
@@ -12,6 +12,8 @@ proxies each board and streams its activity to participants and instructors.
|
|||||||
`gateway start` — only the daemon/agent/channel paths register the hardware
|
`gateway start` — only the daemon/agent/channel paths register the hardware
|
||||||
tools (`uno_q_flash`, …).
|
tools (`uno_q_flash`, …).
|
||||||
- **`llama-server`** on `:8083` — on-board Qwen for offline/fallback inference.
|
- **`llama-server`** on `:8083` — on-board Qwen for offline/fallback inference.
|
||||||
|
- **`zeroclaw-reload-watcher`** (loopback) — applies a team's browser config
|
||||||
|
edits (e.g. enabling their Telegram bot) via `/admin/reload`. See *modalities*.
|
||||||
- **Three agents**, selected per request via `?agent=` (APESS maps the harness
|
- **Three agents**, selected per request via `?agent=` (APESS maps the harness
|
||||||
provider choice → alias; see `src/lib/harness.ts` `harnessToAgent`):
|
provider choice → alias; see `src/lib/harness.ts` `harnessToAgent`):
|
||||||
| alias | provider | behaviour |
|
| alias | provider | behaviour |
|
||||||
@@ -20,6 +22,60 @@ proxies each board and streams its activity to participants and instructors.
|
|||||||
| `cloud` | `custom.cloud` | cloud only |
|
| `cloud` | `custom.cloud` | cloud only |
|
||||||
| `local` | `llamacpp.local`| on-board Qwen only (fully offline) |
|
| `local` | `llamacpp.local`| on-board Qwen only (fully offline) |
|
||||||
|
|
||||||
|
## Talking to the node — the three modalities
|
||||||
|
|
||||||
|
Beyond the APESS-proxied prompt path, a team reaches its own node three ways.
|
||||||
|
All bind to the `default` (cloud-first) agent, so responses are fast; `local`
|
||||||
|
stays the offline fallback.
|
||||||
|
|
||||||
|
1. **Web chat by IP.** The board is **LAN-open during setup** (`config.template.toml`
|
||||||
|
sets `host = "0.0.0.0"`, `allow_public_bind = true`, `require_pairing = false`),
|
||||||
|
so a team opens `http://<board-lan-ip>:8080/` — the embedded ZeroClaw
|
||||||
|
dashboard + `/ws/chat` — straight from the "Open your node →" link APESS shows
|
||||||
|
after they claim. No token while open.
|
||||||
|
2. **Telegram.** Each board is its own agent, so each needs its own bot. A team
|
||||||
|
makes one via **@BotFather**, then in the dashboard (**Config → channels →
|
||||||
|
`telegram.default`**) pastes the `bot_token`, adds their username to
|
||||||
|
`allowed_users`, and sets `enabled = true`. Dashboard writes only set
|
||||||
|
`pending_reload`; the **`zeroclaw-reload-watcher`** (loopback) applies them
|
||||||
|
within a few seconds — no shell. The channel is pre-bound to `default`.
|
||||||
|
3. **Voice (browser-mic streaming).** Talk in the web chat, the agent speaks
|
||||||
|
back — requires a daemon built with the voice feature (see **Voice** below),
|
||||||
|
then uncomment `[channels.voice_duplex.default]` in the config.
|
||||||
|
|
||||||
|
### Open → locked lifecycle
|
||||||
|
|
||||||
|
Boards boot **open** so setup is frictionless on the isolated workshop LAN. When
|
||||||
|
a team is done, **harden** the node with `zeroclaw-lockdown.sh` (run on the board
|
||||||
|
or over adb — pair-code minting + reload are loopback-only, so it can't be a LAN
|
||||||
|
button):
|
||||||
|
|
||||||
|
```sh
|
||||||
|
adb -s <serial> shell '/home/arduino/zeroclaw-lockdown.sh' # prints a pair code
|
||||||
|
```
|
||||||
|
|
||||||
|
It flips `require_pairing = true`, reloads, and mints a pair code the team enters
|
||||||
|
once at `http://<board-ip>:8080/pairing`. After lockdown only their paired
|
||||||
|
devices reach the node. (Note: locking down also cuts the shared APESS proxy's
|
||||||
|
access unless you re-register a fresh token — a deliberate "graduate to a private
|
||||||
|
node" transition; validate against your run's needs.)
|
||||||
|
|
||||||
|
### Voice — building a voice-capable board binary
|
||||||
|
|
||||||
|
`gateway-voice-duplex` is a Cargo feature and the SPA is embedded at compile
|
||||||
|
time, so the board binary must be built as:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cargo xtask web build # build the SPA into web/dist first (embedded-web needs it)
|
||||||
|
cargo build --release --target aarch64-unknown-linux-gnu \
|
||||||
|
--features "hardware,peripheral-rpi,embedded-web,zeroclaw-gateway/gateway-voice-duplex"
|
||||||
|
```
|
||||||
|
|
||||||
|
`gateway-voice-duplex` has no root-crate alias, so it's namespaced. The web chat
|
||||||
|
does client-side STT (Web Speech API) and plays the streamed `tts_chunk` audio;
|
||||||
|
the server `handle_voice_event` path is finished as part of this work. **This is
|
||||||
|
the one modality that needs an on-board build + hardware test to verify.**
|
||||||
|
|
||||||
## Prerequisites on the board
|
## Prerequisites on the board
|
||||||
|
|
||||||
- `/home/arduino/zeroclaw` — the ZeroClaw binary (aarch64), **with the
|
- `/home/arduino/zeroclaw` — the ZeroClaw binary (aarch64), **with the
|
||||||
@@ -121,17 +177,18 @@ sudo cp systemd/zeroclaw-llama.service systemd/zeroclaw-daemon.service /etc/syst
|
|||||||
sudo systemctl enable --now zeroclaw-llama zeroclaw-daemon
|
sudo systemctl enable --now zeroclaw-llama zeroclaw-daemon
|
||||||
```
|
```
|
||||||
|
|
||||||
For a LAN fleet (participants reach the board's WiFi IP directly), set in
|
For a LAN fleet (participants reach the board's WiFi IP directly), the template
|
||||||
`config.toml`:
|
now ships this **by default** (`host = "0.0.0.0"`, `allow_public_bind = true`,
|
||||||
|
`require_pairing = false`) so the dashboard/web-chat is reachable + open during
|
||||||
|
setup — see *modalities* above. Enable the reload-watcher alongside the daemon:
|
||||||
|
|
||||||
```toml
|
```sh
|
||||||
[gateway]
|
sudo cp systemd/zeroclaw-reload-watcher.service /etc/systemd/system/
|
||||||
host = "0.0.0.0"
|
sudo systemctl enable --now zeroclaw-reload-watcher
|
||||||
allow_public_bind = true
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Then pair + register once (steps 4–5 of the script) so APESS has the board's
|
Then register once so APESS has the board's `{ url, token }` (step 5; under
|
||||||
`{ url, token }`.
|
open-boot the token is a placeholder — the open board ignores auth).
|
||||||
|
|
||||||
## Resilience — surviving a disconnect (no-root boards)
|
## Resilience — surviving a disconnect (no-root boards)
|
||||||
|
|
||||||
@@ -198,4 +255,10 @@ cloud-with-fallback. In APESS, the team's provider/fallback toggle picks the ali
|
|||||||
- `apess-node.env.example` — per-board onboarding identity (KIT_ID / CLAIM_CODE / FLEET_SECRET / APESS_URL).
|
- `apess-node.env.example` — per-board onboarding identity (KIT_ID / CLAIM_CODE / FLEET_SECRET / APESS_URL).
|
||||||
- `systemd/*.service` + `apess-selfregister.timer` — production units (need root).
|
- `systemd/*.service` + `apess-selfregister.timer` — production units (need root).
|
||||||
- `zeroclaw-supervisor.sh` — on-board no-root watchdog (endpoint health + restart).
|
- `zeroclaw-supervisor.sh` — on-board no-root watchdog (endpoint health + restart).
|
||||||
|
- `zeroclaw-reload-watcher.sh` — on-board: applies browser dashboard config edits
|
||||||
|
by firing the loopback `/admin/reload` when `config.toml` changes (makes
|
||||||
|
Telegram-enable / lockdown self-serve without a shell). Root unit:
|
||||||
|
`systemd/zeroclaw-reload-watcher.service`; no-root: launch via `setsid nohup`.
|
||||||
|
- `zeroclaw-lockdown.sh` — on-board: the harden step — flips `require_pairing=true`,
|
||||||
|
reloads, and mints a pair code (open → locked).
|
||||||
- `recover-uno-q.sh` — host-side post-disconnect recovery (re-tunnel + health-check).
|
- `recover-uno-q.sh` — host-side post-disconnect recovery (re-tunnel + health-check).
|
||||||
|
|||||||
@@ -34,10 +34,16 @@ for _ in $(seq 1 60); do
|
|||||||
done
|
done
|
||||||
|
|
||||||
# Pair locally for a bearer token (the token stays server-side once APESS has it).
|
# Pair locally for a bearer token (the token stays server-side once APESS has it).
|
||||||
CODE=$("$ZEROCLAW" gateway get-paircode --new --port "$PORT" | grep -oE '[0-9]{6}' | head -1)
|
# Under LAN-open setup (require_pairing=false) the gateway mints no code — the
|
||||||
|
# open board ignores auth, so announce with a placeholder token; lockdown later
|
||||||
|
# establishes real pairing (and, if wanted, re-announces a valid token).
|
||||||
|
CODE=$("$ZEROCLAW" gateway get-paircode --new --port "$PORT" 2>/dev/null | grep -oE '[0-9]{6}' | head -1 || true)
|
||||||
|
TOKEN=""
|
||||||
|
if [ -n "$CODE" ]; then
|
||||||
TOKEN=$(curl -s -X POST "$GW/pair" -H "X-Pairing-Code: ${CODE}" \
|
TOKEN=$(curl -s -X POST "$GW/pair" -H "X-Pairing-Code: ${CODE}" \
|
||||||
| python3 -c 'import sys,json;print(json.load(sys.stdin).get("token",""))')
|
| python3 -c 'import sys,json;print(json.load(sys.stdin).get("token",""))' 2>/dev/null || true)
|
||||||
[ -n "$TOKEN" ] || { echo "local pairing failed" >&2; exit 1; }
|
fi
|
||||||
|
TOKEN="${TOKEN:-open-lan}" # placeholder while the board is LAN-open
|
||||||
|
|
||||||
# The LAN IP participants (and APESS) reach; fall back to localhost for adb-only.
|
# The LAN IP participants (and APESS) reach; fall back to localhost for adb-only.
|
||||||
IP=$(ip -4 -o addr show 2>/dev/null | grep -oE 'inet [0-9.]+' | grep -v '127.0.0.1' \
|
IP=$(ip -4 -o addr show 2>/dev/null | grep -oE 'inet [0-9.]+' | grep -v '127.0.0.1' \
|
||||||
|
|||||||
@@ -42,16 +42,23 @@ transport = "bridge"
|
|||||||
enabled = true
|
enabled = true
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Gateway — the HTTP/WS/SSE surface APESS talks to.
|
# Gateway — the HTTP/WS/SSE surface APESS talks to, AND the embedded web
|
||||||
|
# dashboard + chat UI a team opens directly at http://<board-lan-ip>:8080.
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
[gateway]
|
[gateway]
|
||||||
port = 8080
|
port = 8080
|
||||||
# Bind 0.0.0.0 + allow_public_bind for a real LAN fleet (participants reach the
|
# LAN-open by default: participants reach the board's WiFi IP directly (the
|
||||||
# board's WiFi IP). Leave default (localhost) when reaching it over adb-forward.
|
# "Open your node →" link in APESS). 0.0.0.0 + allow_public_bind make the
|
||||||
# host = "0.0.0.0"
|
# dashboard/web-chat reachable across the workshop subnet.
|
||||||
# allow_public_bind = true
|
host = "0.0.0.0"
|
||||||
# paired_tokens are added by the pairing flow (see provision-uno-q.sh); never
|
allow_public_bind = true
|
||||||
# commit a real token.
|
# Open during SETUP so a team's browser can chat + edit config (paste a Telegram
|
||||||
|
# token, etc.) with no token on the isolated workshop LAN. The reload-watcher
|
||||||
|
# (loopback) applies any dashboard config edit. Run `zeroclaw-lockdown.sh` to
|
||||||
|
# flip this on and mint a pair code once a team has finished setting up.
|
||||||
|
require_pairing = false
|
||||||
|
# paired_tokens are added by the pairing flow (lockdown / provision-uno-q.sh);
|
||||||
|
# never commit a real token.
|
||||||
|
|
||||||
[skills]
|
[skills]
|
||||||
prompt_injection_mode = "compact"
|
prompt_injection_mode = "compact"
|
||||||
@@ -74,11 +81,15 @@ strict_tool_parsing = false
|
|||||||
# Agents — one per provider strategy. The APESS harness choice routes here via
|
# Agents — one per provider strategy. The APESS harness choice routes here via
|
||||||
# ?agent= (see harnessToAgent in src/lib/harness.ts).
|
# ?agent= (see harnessToAgent in src/lib/harness.ts).
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
[agents.default] # cloud + on-board Qwen fallback
|
[agents.default] # cloud + on-board Qwen fallback — the workshop default
|
||||||
enabled = true
|
enabled = true
|
||||||
model_provider = "custom.claude"
|
model_provider = "custom.claude"
|
||||||
risk_profile = "default"
|
risk_profile = "default"
|
||||||
runtime_profile = "unoq"
|
runtime_profile = "unoq"
|
||||||
|
# Modalities this agent answers on, beyond the APESS proxy + web chat: the team's
|
||||||
|
# own Telegram bot. A team enables it from the dashboard (Config → channels →
|
||||||
|
# telegram) by pasting their @BotFather token; the reload-watcher applies it.
|
||||||
|
channels = ["telegram.default"]
|
||||||
|
|
||||||
[agents.cloud] # cloud only, no fallback
|
[agents.cloud] # cloud only, no fallback
|
||||||
enabled = true
|
enabled = true
|
||||||
@@ -91,3 +102,26 @@ enabled = true
|
|||||||
model_provider = "llamacpp.local"
|
model_provider = "llamacpp.local"
|
||||||
risk_profile = "default"
|
risk_profile = "default"
|
||||||
runtime_profile = "unoq"
|
runtime_profile = "unoq"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Channels — extra ways a team talks to its node (all bind to `default`, above).
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Telegram: each board is its own agent, so each needs its own bot. A team
|
||||||
|
# creates one live via @BotFather, then pastes the token in the dashboard
|
||||||
|
# (Config → channels → telegram.default), sets enabled = true, and adds their
|
||||||
|
# Telegram username to allowed_users. The reload-watcher restarts the listener.
|
||||||
|
# Seeded disabled so the block shows up pre-bound in the config editor.
|
||||||
|
[channels.telegram.default]
|
||||||
|
enabled = false
|
||||||
|
bot_token = "" # from @BotFather (paste via the dashboard)
|
||||||
|
# allowed_users = ["your_tg_username"] # lock the bot to your team (no leading @)
|
||||||
|
mention_only = false
|
||||||
|
|
||||||
|
# Browser-mic streaming voice — talk to the agent inside the web chat, it speaks
|
||||||
|
# back. Requires a daemon built with the voice feature (see README §Voice):
|
||||||
|
# cargo xtask web build && cargo build --release \
|
||||||
|
# --features "hardware,peripheral-rpi,embedded-web,zeroclaw-gateway/gateway-voice-duplex"
|
||||||
|
# Enable once the board runs a voice-capable binary:
|
||||||
|
# [channels.voice_duplex.default]
|
||||||
|
# enabled = true
|
||||||
|
|||||||
@@ -64,6 +64,19 @@ provision() { # kit serial -> 0 ok / 1 fail
|
|||||||
fi
|
fi
|
||||||
echo " ok — onboarding ($persisted)"
|
echo " ok — onboarding ($persisted)"
|
||||||
|
|
||||||
|
# modalities — reload-watcher (applies browser dashboard config edits, e.g. a
|
||||||
|
# team enabling its Telegram bot) + the lockdown script. Best-effort.
|
||||||
|
if [ -r "$HERE/zeroclaw-reload-watcher.sh" ]; then
|
||||||
|
adb -s "$serial" push "$HERE/zeroclaw-reload-watcher.sh" /home/arduino/ >/dev/null 2>&1
|
||||||
|
adb -s "$serial" push "$HERE/zeroclaw-lockdown.sh" /home/arduino/ >/dev/null 2>&1
|
||||||
|
adb -s "$serial" shell 'chmod +x /home/arduino/zeroclaw-reload-watcher.sh /home/arduino/zeroclaw-lockdown.sh' >/dev/null 2>&1
|
||||||
|
adb -s "$serial" shell 'pgrep -f "[z]eroclaw-reload-watcher.sh" >/dev/null 2>&1 || \
|
||||||
|
setsid nohup /home/arduino/zeroclaw-reload-watcher.sh >/tmp/zc-reload.log 2>&1 </dev/null &' >/dev/null 2>&1
|
||||||
|
adb -s "$serial" shell '(crontab -l 2>/dev/null | grep -v zeroclaw-reload-watcher.sh; \
|
||||||
|
echo "@reboot /home/arduino/zeroclaw-reload-watcher.sh") | crontab -' >/dev/null 2>&1 || true
|
||||||
|
echo " ok — modalities (reload-watcher up; lockdown staged)"
|
||||||
|
fi
|
||||||
|
|
||||||
# default boot animation — best-effort (a flash hiccup doesn't fail onboarding)
|
# default boot animation — best-effort (a flash hiccup doesn't fail onboarding)
|
||||||
if [ "$FLASH_DEFAULT" = 1 ]; then
|
if [ "$FLASH_DEFAULT" = 1 ]; then
|
||||||
if "$HERE/flash-sketch.sh" "$serial" "$FLASH_SKETCH" >/dev/null 2>&1; then
|
if "$HERE/flash-sketch.sh" "$serial" "$FLASH_SKETCH" >/dev/null 2>&1; then
|
||||||
|
|||||||
@@ -37,10 +37,15 @@ sleep 8
|
|||||||
echo "==> [4/5] pair → bearer token"
|
echo "==> [4/5] pair → bearer token"
|
||||||
a forward tcp:8080 tcp:8080 >/dev/null
|
a forward tcp:8080 tcp:8080 >/dev/null
|
||||||
until curl -sf --max-time 3 http://127.0.0.1:8080/health >/dev/null; do sleep 1; done
|
until curl -sf --max-time 3 http://127.0.0.1:8080/health >/dev/null; do sleep 1; done
|
||||||
CODE=$(a shell '/home/arduino/zeroclaw gateway get-paircode --new --port 8080' | grep -oE '[0-9]{6}' | head -1)
|
# Under LAN-open setup (require_pairing=false, the template default) the gateway
|
||||||
|
# mints no code — the open board ignores auth, so register a placeholder token.
|
||||||
|
CODE=$(a shell '/home/arduino/zeroclaw gateway get-paircode --new --port 8080' 2>/dev/null | grep -oE '[0-9]{6}' | head -1 || true)
|
||||||
|
TOKEN=""
|
||||||
|
if [ -n "$CODE" ]; then
|
||||||
TOKEN=$(curl -s -X POST http://127.0.0.1:8080/pair -H "X-Pairing-Code: ${CODE}" \
|
TOKEN=$(curl -s -X POST http://127.0.0.1:8080/pair -H "X-Pairing-Code: ${CODE}" \
|
||||||
| python3 -c 'import sys,json;print(json.load(sys.stdin)["token"])')
|
| python3 -c 'import sys,json;print(json.load(sys.stdin).get("token",""))' 2>/dev/null || true)
|
||||||
[ -n "$TOKEN" ] || { echo "pairing failed" >&2; exit 1; }
|
fi
|
||||||
|
TOKEN="${TOKEN:-open-lan}" # placeholder while the board is LAN-open
|
||||||
|
|
||||||
echo "==> [5/5] register team '$TEAM' with APESS"
|
echo "==> [5/5] register team '$TEAM' with APESS"
|
||||||
# Board's LAN IP — what participants reach in a real fleet (falls back to localhost).
|
# Board's LAN IP — what participants reach in a real fleet (falls back to localhost).
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=ZeroClaw reload-watcher (applies dashboard config edits via loopback reload)
|
||||||
|
After=zeroclaw-daemon.service
|
||||||
|
Wants=zeroclaw-daemon.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=arduino
|
||||||
|
# Watches config.toml and fires POST /admin/reload (loopback) when a team edits
|
||||||
|
# config from the browser dashboard — so Telegram-enable / allowed_users / the
|
||||||
|
# lockdown flip take effect without a shell. No-root boards: launch instead with
|
||||||
|
# setsid nohup /home/arduino/zeroclaw-reload-watcher.sh >/tmp/zc-reload.log 2>&1 </dev/null &
|
||||||
|
ExecStart=/home/arduino/zeroclaw-reload-watcher.sh
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=5
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
Executable
+62
@@ -0,0 +1,62 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# The "harden" step — flip a node from open (setup) to paired (locked). Boards
|
||||||
|
# boot LAN-open so a team can set up tokenless; once they're done, this locks the
|
||||||
|
# node so only their group can reach it.
|
||||||
|
#
|
||||||
|
# Runs ON the board (loopback): pair-code minting + /admin/reload are localhost-
|
||||||
|
# only, so this can't be a LAN-browser button. Trigger it over adb, from a board
|
||||||
|
# shell, or wire it to a physical button — then hand the team the pair code it
|
||||||
|
# prints (they enter it once in the dashboard's pairing screen).
|
||||||
|
#
|
||||||
|
# ./zeroclaw-lockdown.sh # on the board
|
||||||
|
# adb -s <serial> shell '/home/arduino/zeroclaw-lockdown.sh' # from the host
|
||||||
|
#
|
||||||
|
# Env:
|
||||||
|
# ZC_CONFIG config file (default /home/arduino/.zeroclaw/config.toml)
|
||||||
|
# GATEWAY_PORT loopback gateway port (default 8080)
|
||||||
|
# ZC_BIN zeroclaw binary (default /home/arduino/zeroclaw)
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
CONFIG="${ZC_CONFIG:-/home/arduino/.zeroclaw/config.toml}"
|
||||||
|
PORT="${GATEWAY_PORT:-8080}"
|
||||||
|
ZC_BIN="${ZC_BIN:-/home/arduino/zeroclaw}"
|
||||||
|
BASE="http://127.0.0.1:${PORT}"
|
||||||
|
|
||||||
|
echo "==> locking down this node (require pairing)"
|
||||||
|
|
||||||
|
# 1) Persist require_pairing=true. Prefer the running gateway's config API
|
||||||
|
# (loopback → allowed even while pairing is off); fall back to editing the
|
||||||
|
# toml directly if the endpoint isn't reachable.
|
||||||
|
if ! curl -sf --max-time 5 -X PUT "${BASE}/api/config/prop?path=gateway.require_pairing" \
|
||||||
|
-H 'content-type: application/json' -d 'true' >/dev/null 2>&1; then
|
||||||
|
echo " (config API unreachable — editing $CONFIG directly)"
|
||||||
|
if grep -qE '^\s*require_pairing\s*=' "$CONFIG"; then
|
||||||
|
sed -i 's/^\(\s*require_pairing\s*=\).*/\1 true/' "$CONFIG"
|
||||||
|
else
|
||||||
|
# insert under [gateway]
|
||||||
|
sed -i '/^\[gateway\]/a require_pairing = true' "$CONFIG"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 2) Reload so the live PairingGuard picks up require_pairing=true.
|
||||||
|
curl -sf --max-time 8 -X POST "${BASE}/admin/reload" >/dev/null 2>&1 || true
|
||||||
|
# give the daemon a moment to come back paired
|
||||||
|
for _ in $(seq 1 15); do
|
||||||
|
curl -sf --max-time 2 "${BASE}/health" >/dev/null 2>&1 && break
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
# 3) Mint a fresh pair code (localhost-only). Try the admin endpoint, then the CLI.
|
||||||
|
CODE="$(curl -sf --max-time 5 -X POST "${BASE}/admin/paircode/new" 2>/dev/null | grep -oE '[0-9]{6}' | head -1)"
|
||||||
|
[ -n "$CODE" ] || CODE="$("$ZC_BIN" gateway get-paircode --new --port "$PORT" 2>/dev/null | grep -oE '[0-9]{6}' | head -1)"
|
||||||
|
|
||||||
|
echo
|
||||||
|
if [ -n "$CODE" ]; then
|
||||||
|
echo " ┌───────────────────────────────────────────┐"
|
||||||
|
echo " │ Node locked. Pairing code: ${CODE} │"
|
||||||
|
echo " └───────────────────────────────────────────┘"
|
||||||
|
echo " Enter it once in the dashboard's pairing screen (http://<board-ip>:${PORT}/pairing)."
|
||||||
|
else
|
||||||
|
echo " ! Node lockdown persisted, but couldn't mint a pair code automatically."
|
||||||
|
echo " Run on the board: ${ZC_BIN} gateway get-paircode --new --port ${PORT}"
|
||||||
|
fi
|
||||||
Executable
+48
@@ -0,0 +1,48 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# On-board reload-watcher. The ZeroClaw dashboard lets a team edit config from
|
||||||
|
# their browser (enable their Telegram bot, set allowed_users, …) — but a config
|
||||||
|
# write only sets `pending_reload`; the channel listeners don't re-spawn until
|
||||||
|
# `POST /admin/reload`, which is LOOPBACK-ONLY. A LAN browser can't call it.
|
||||||
|
#
|
||||||
|
# This watcher closes that gap: it runs on the board, notices when config.toml
|
||||||
|
# changes, and fires the loopback reload — so a team's dashboard edit takes
|
||||||
|
# effect on its own within a few seconds, no shell required. It's the thing that
|
||||||
|
# makes browser self-serve (Telegram, and the lockdown flip) actually work.
|
||||||
|
#
|
||||||
|
# ./zeroclaw-reload-watcher.sh # loops; run via setsid or systemd
|
||||||
|
#
|
||||||
|
# Env:
|
||||||
|
# ZC_CONFIG config file to watch (default /home/arduino/.zeroclaw/config.toml)
|
||||||
|
# GATEWAY_PORT loopback gateway port (default 8080)
|
||||||
|
# POLL_SECS poll interval (default 3)
|
||||||
|
# SETTLE_SECS debounce after a change before reloading (default 2)
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
CONFIG="${ZC_CONFIG:-/home/arduino/.zeroclaw/config.toml}"
|
||||||
|
PORT="${GATEWAY_PORT:-8080}"
|
||||||
|
POLL="${POLL_SECS:-3}"
|
||||||
|
SETTLE="${SETTLE_SECS:-2}"
|
||||||
|
BASE="http://127.0.0.1:${PORT}"
|
||||||
|
|
||||||
|
mtime() { stat -c %Y "$CONFIG" 2>/dev/null || echo 0; }
|
||||||
|
|
||||||
|
reload() { # loopback → passes the gateway's require_localhost gate
|
||||||
|
curl -sf --max-time 5 -X POST "${BASE}/admin/reload" >/dev/null 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "[reload-watcher] watching $CONFIG → ${BASE}/admin/reload (poll ${POLL}s)"
|
||||||
|
last="$(mtime)"
|
||||||
|
while :; do
|
||||||
|
sleep "$POLL"
|
||||||
|
cur="$(mtime)"
|
||||||
|
[ "$cur" = "$last" ] && continue
|
||||||
|
# config changed — let a burst of per-field writes settle, then reload once
|
||||||
|
sleep "$SETTLE"
|
||||||
|
cur="$(mtime)"
|
||||||
|
if reload; then
|
||||||
|
echo "[reload-watcher] config changed → reloaded daemon ($(date -u +%H:%M:%SZ))"
|
||||||
|
else
|
||||||
|
echo "[reload-watcher] config changed → reload failed (daemon down?); will retry"
|
||||||
|
fi
|
||||||
|
last="$cur"
|
||||||
|
done
|
||||||
Reference in New Issue
Block a user