#!/usr/bin/env bash # package-onboard-app.sh — assemble a SELF-CONTAINED, distributable App Lab app # ("APESS Onboard") that a student imports and clicks Run. Everything is baked in: # the ZeroClaw binary, the single-agent config, the cloud token, the skills, and # the resident responder sketch. No adb, no host install, no per-board setup. # # The instructor runs this ONCE to produce the bundle, then shares it via the App # Lab UI (share → QR). Students scan the QR to import, open the app, click Run: # the node comes up in one container, flashes the responder, and self-registers to # the team's APESS laptop. # # Usage: # export ANTHROPIC_OAUTH_TOKEN=sk-ant-oat01-… # baked into the app # ./deploy/uno-q/package-onboard-app.sh # # Env: # ANTHROPIC_OAUTH_TOKEN (required) cloud Max token, baked into .zeroclaw/oauth_token # ZEROCLAW_BIN aarch64 binary (default: the built release-fast one) # APESS_URL where the board self-registers (default: mDNS apess-api.local) # FLEET_SECRET shared fleet secret (default: apess2026) # OUT output bundle dir (default: deploy/uno-q/dist/apess-onboard) set -euo pipefail HERE="$(cd "$(dirname "$0")" && pwd)" REPO="$(cd "$HERE/../.." && pwd)" NODE_SRC="${NODE_SRC:-$HOME/projects/zeroclaw/firmware/zeroclaw-node}" ZEROCLAW_BIN="${ZEROCLAW_BIN:-$HOME/projects/zeroclaw/target/aarch64-unknown-linux-gnu/release-fast/zeroclaw}" APESS_URL="${APESS_URL:-http://apess-api.local:3000}" FLEET_SECRET="${FLEET_SECRET:-apess2026}" OUT="${OUT:-$HERE/dist/apess-onboard}" ok(){ printf ' \033[32m✓\033[0m %s\n' "$*"; } bad(){ printf ' \033[31m✗\033[0m %s\n' "$*"; exit 1; } [ -n "${ANTHROPIC_OAUTH_TOKEN:-}" ] || bad "ANTHROPIC_OAUTH_TOKEN not set (it gets baked into the app)" [ -f "$ZEROCLAW_BIN" ] || bad "binary not found: $ZEROCLAW_BIN (build it first)" [ -f "$NODE_SRC/app.yaml" ] || bad "node app source not found: $NODE_SRC" [ -f "$HERE/onboard-app/config.toml" ] || bad "canonical config missing: onboard-app/config.toml" echo "→ assembling the bundle at $OUT" rm -rf "$OUT" mkdir -p "$OUT/bin" "$OUT/.zeroclaw/shared" # App Lab app scaffold (manifest + entrypoint + resident sketch) cp "$NODE_SRC/app.yaml" "$OUT/app.yaml" cp -r "$NODE_SRC/python" "$OUT/python" cp -r "$NODE_SRC/sketch" "$OUT/sketch" ok "app.yaml + python + sketch (responder w/ i2c_scan)" # The ZeroClaw binary (matrix_text + i2c_scan + the works) install -m755 "$ZEROCLAW_BIN" "$OUT/bin/zeroclaw" ok "binary ($(du -h "$OUT/bin/zeroclaw" | cut -f1))" # Single-agent config (proven: anthropic.max, matrix + i2c_scan allowlisted, # telegram-ready, secrets stripped so a fresh board mints its own .secret_key) cp "$HERE/onboard-app/config.toml" "$OUT/.zeroclaw/config.toml" ok "config (single 'default' agent, matrix + i2c_scan)" # Skills — the resident copy the daemon seeds each agent's workspace from cp -r "$HERE/skills" "$OUT/.zeroclaw/shared/skills" ok "skills ($(ls "$HERE/skills" | wc -l | tr -d ' ') bundles)" # BAKED cloud token (per the workshop decision) — the instructor's Max token, # shared across the fleet. Kept in the app bundle only, never in the repo. printf '%s' "$ANTHROPIC_OAUTH_TOKEN" > "$OUT/.zeroclaw/oauth_token" chmod 600 "$OUT/.zeroclaw/oauth_token" ok "cloud token baked in (.zeroclaw/oauth_token)" # Self-register inputs. KIT_ID + CLAIM_CODE are per-board; the packaged defaults # are placeholders the app regenerates a code from if unset. APESS_URL points the # board at the team's laptop stack (default: mDNS name the deploy/lan box advertises). cat > "$OUT/.zeroclaw/apess-node.env" <` and the App Lab UI "Import an app" both # accept it (verified round-trip). This is what we HOST for students to download. ZIP="${ZIP:-$(dirname "$OUT")/$(basename "$OUT").zip}" ( cd "$(dirname "$OUT")" && rm -f "$ZIP" && zip -rq "$ZIP" "$(basename "$OUT")" -x '*.DS_Store' ) ok "import archive: $ZIP ($(du -h "$ZIP" | cut -f1))" cat <:3000. EOF