feat(onboard): distributable "APESS Onboard" App Lab app + packaging
Close the last onboarding gap: one shareable App Lab app a student imports and Runs, no adb / no host install. - package-onboard-app.sh: assemble a self-contained bundle — ZeroClaw binary (matrix_text + i2c_scan), single-agent config, skills, responder sketch, and the BAKED cloud token. Ships without .secret_key (each board mints its own) or a team Telegram token; dist/ is gitignored. - onboard-app/config.toml: the canonical packaged config (proven anthropic.max single 'default' agent, matrix + i2c_scan allowlisted, Telegram-ready, secrets stripped). - ONBOARDING.md: the full flow — instructor packages once, student imports + Runs, then the wizard. Notes APESS_URL (mDNS/per-team) + LAN reachability. Validated on-hardware: a freshly-imported bundle mints its key, boots the cloud agent, and runs the matrix + i2c_scan prompts in-container. Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
8ce8474d33
commit
838a0ba334
@@ -0,0 +1,71 @@
|
|||||||
|
# APESS onboarding — fully containerized
|
||||||
|
|
||||||
|
A team needs two things running: the **APESS stack on their laptop** and the
|
||||||
|
**ZeroClaw node on their Uno Q**. Both are containers. Nothing installs to a host.
|
||||||
|
|
||||||
|
```
|
||||||
|
LAPTOP: docker compose up → apess-api + apess-web (deploy/lan)
|
||||||
|
BOARD : App Lab → Run → ONE container = daemon + relay + responder
|
||||||
|
```
|
||||||
|
|
||||||
|
Everything a team does — say-hi, the module chat, Refine, Telegram, the LED
|
||||||
|
matrix, the I2C scan — runs through this. The board never needs the Zephyr flash
|
||||||
|
toolchain or Linux `/dev/i2c`: the matrix is driven by a resident responder and
|
||||||
|
I2C is scanned on the MCU (Wire), both over the RouterBridge relay.
|
||||||
|
|
||||||
|
## 1. Instructor — build the distributable app (once)
|
||||||
|
|
||||||
|
Bake the cloud token + all assets into a shareable App Lab app:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
export ANTHROPIC_OAUTH_TOKEN=sk-ant-oat01-… # baked into the bundle
|
||||||
|
./deploy/uno-q/package-onboard-app.sh # → deploy/uno-q/dist/apess-onboard
|
||||||
|
```
|
||||||
|
|
||||||
|
The bundle contains: the ZeroClaw binary (matrix_text + i2c_scan), the
|
||||||
|
single-`default`-agent config (matrix + i2c_scan allowlisted, Telegram-ready),
|
||||||
|
the skills, the responder sketch, and the baked token. It ships **without** a
|
||||||
|
`.secret_key` (each board mints its own on first Run) and **without** any team's
|
||||||
|
Telegram token. The `dist/` output is gitignored (it holds the token).
|
||||||
|
|
||||||
|
Import it once to smoke-test, or share it to students:
|
||||||
|
```sh
|
||||||
|
arduino-app-cli app new --from-app deploy/uno-q/dist/apess-onboard # instructor test
|
||||||
|
# then in App Lab: open the app → Share → QR
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. Student — bring up their board (fresh OR pre-existing)
|
||||||
|
|
||||||
|
1. Open **App Lab** on the Uno Q (it ships with the board).
|
||||||
|
2. **Import** the "APESS Onboard" app (scan the QR).
|
||||||
|
3. Click **Run**. The node comes up in one container: it flashes the resident
|
||||||
|
responder, launches the cloud agent, mints its `.secret_key`, self-registers
|
||||||
|
to the team's APESS laptop, and scrolls a **claim code** on the LED matrix.
|
||||||
|
|
||||||
|
Fresh or pre-existing board is identical — Run is idempotent; it just (re)starts
|
||||||
|
the node.
|
||||||
|
|
||||||
|
## 3. Student — the wizard (laptop)
|
||||||
|
|
||||||
|
- Open `http://<laptop>/` → **Start the workshop**.
|
||||||
|
- **Phase 1:** team name + members → type the **claim code** the matrix is
|
||||||
|
scrolling → board bound (state persists in `localStorage`; a Disconnect button
|
||||||
|
is the only thing that drops it).
|
||||||
|
- Say hi to the agent, optionally set up **Telegram** (writes the token to the
|
||||||
|
node and reloads it), toggle **Voice**.
|
||||||
|
- **Phase 2:** open the node, name the domain. Then the modules.
|
||||||
|
|
||||||
|
## `APESS_URL` — how the board finds the laptop
|
||||||
|
|
||||||
|
The board self-registers to `APESS_URL` (default `http://apess-api.local:3000`).
|
||||||
|
Options, easiest first:
|
||||||
|
- **mDNS:** have the `deploy/lan` box advertise `apess-api.local` (zero-config
|
||||||
|
for students).
|
||||||
|
- **Per-team:** set `APESS_URL` in the app's `.zeroclaw/apess-node.env`, or pass
|
||||||
|
`APESS_URL=http://<laptop-ip>:3000` when packaging.
|
||||||
|
|
||||||
|
## Reachability
|
||||||
|
|
||||||
|
The laptop's API must reach the board over the workshop WiFi. Verify the AP
|
||||||
|
allows **client-to-client** traffic (many guest networks isolate clients). See
|
||||||
|
`deploy/lan/README.md`.
|
||||||
@@ -0,0 +1,405 @@
|
|||||||
|
schema_version = 3
|
||||||
|
|
||||||
|
[providers.models.openrouter.default]
|
||||||
|
temperature = 0.3
|
||||||
|
|
||||||
|
[risk_profiles.default.delegation_policy]
|
||||||
|
mode = "forbidden"
|
||||||
|
|
||||||
|
[risk_profiles.sense_only.delegation_policy]
|
||||||
|
mode = "forbidden"
|
||||||
|
|
||||||
|
[risk_profiles.field_ops.delegation_policy]
|
||||||
|
mode = "forbidden"
|
||||||
|
|
||||||
|
[risk_profiles.field_flash.delegation_policy]
|
||||||
|
mode = "forbidden"
|
||||||
|
|
||||||
|
[risk_profiles.demo.delegation_policy]
|
||||||
|
mode = "forbidden"
|
||||||
|
|
||||||
|
[runtime_profiles.unoq.context_compression]
|
||||||
|
enabled = true
|
||||||
|
identifier_policy = "strict"
|
||||||
|
max_passes = 3
|
||||||
|
protect_first_n = 3
|
||||||
|
protect_last_n = 4
|
||||||
|
source_max_chars = 50000
|
||||||
|
summary_max_chars = 4000
|
||||||
|
summary_provider = ""
|
||||||
|
threshold_ratio = 0.5
|
||||||
|
timeout_secs = 60
|
||||||
|
tool_result_retrim_chars = 2000
|
||||||
|
tool_result_trim_exempt = []
|
||||||
|
|
||||||
|
[runtime_profiles.unoq.eval]
|
||||||
|
enabled = false
|
||||||
|
max_retries = 1
|
||||||
|
min_quality_score = 0.5
|
||||||
|
|
||||||
|
[runtime_profiles.unoq.history_pruning]
|
||||||
|
collapse_tool_results = true
|
||||||
|
enabled = false
|
||||||
|
keep_recent = 4
|
||||||
|
max_tokens = 8192
|
||||||
|
|
||||||
|
[runtime_profiles.unoq.thinking]
|
||||||
|
default_level = "medium"
|
||||||
|
native_thinking = false
|
||||||
|
|
||||||
|
[runtime_profiles.unoq.tool_receipts]
|
||||||
|
enabled = false
|
||||||
|
inject_system_prompt = true
|
||||||
|
show_in_response = false
|
||||||
|
|
||||||
|
[runtime_profiles.offline.context_compression]
|
||||||
|
enabled = true
|
||||||
|
identifier_policy = "strict"
|
||||||
|
max_passes = 3
|
||||||
|
protect_first_n = 3
|
||||||
|
protect_last_n = 4
|
||||||
|
source_max_chars = 50000
|
||||||
|
summary_max_chars = 4000
|
||||||
|
summary_provider = ""
|
||||||
|
threshold_ratio = 0.5
|
||||||
|
timeout_secs = 60
|
||||||
|
tool_result_retrim_chars = 2000
|
||||||
|
tool_result_trim_exempt = []
|
||||||
|
|
||||||
|
[runtime_profiles.offline.eval]
|
||||||
|
enabled = false
|
||||||
|
max_retries = 1
|
||||||
|
min_quality_score = 0.5
|
||||||
|
|
||||||
|
[runtime_profiles.offline.history_pruning]
|
||||||
|
collapse_tool_results = true
|
||||||
|
enabled = false
|
||||||
|
keep_recent = 4
|
||||||
|
max_tokens = 8192
|
||||||
|
|
||||||
|
[runtime_profiles.offline.thinking]
|
||||||
|
default_level = "medium"
|
||||||
|
native_thinking = false
|
||||||
|
|
||||||
|
[runtime_profiles.offline.tool_receipts]
|
||||||
|
enabled = false
|
||||||
|
inject_system_prompt = true
|
||||||
|
show_in_response = false
|
||||||
|
|
||||||
|
[agents.default.a2a]
|
||||||
|
exposed_skills = []
|
||||||
|
published = false
|
||||||
|
|
||||||
|
[agents.default.identity]
|
||||||
|
format = "openclaw"
|
||||||
|
|
||||||
|
[agents.default.memory]
|
||||||
|
backend = "sqlite"
|
||||||
|
|
||||||
|
[agents.default.precheck]
|
||||||
|
enabled = true
|
||||||
|
timeout_secs = 5
|
||||||
|
|
||||||
|
[agents.default.workspace]
|
||||||
|
read_memory_from = []
|
||||||
|
unrestricted_filesystem = false
|
||||||
|
|
||||||
|
[agents.cloud.a2a]
|
||||||
|
exposed_skills = []
|
||||||
|
published = false
|
||||||
|
|
||||||
|
[agents.cloud.identity]
|
||||||
|
format = "openclaw"
|
||||||
|
|
||||||
|
[agents.cloud.memory]
|
||||||
|
backend = "sqlite"
|
||||||
|
|
||||||
|
[agents.cloud.precheck]
|
||||||
|
enabled = true
|
||||||
|
timeout_secs = 5
|
||||||
|
|
||||||
|
[agents.cloud.workspace]
|
||||||
|
read_memory_from = []
|
||||||
|
unrestricted_filesystem = false
|
||||||
|
|
||||||
|
[agents.demo.a2a]
|
||||||
|
exposed_skills = []
|
||||||
|
published = false
|
||||||
|
|
||||||
|
[agents.demo.identity]
|
||||||
|
format = "openclaw"
|
||||||
|
|
||||||
|
[agents.demo.memory]
|
||||||
|
backend = "sqlite"
|
||||||
|
|
||||||
|
[agents.demo.precheck]
|
||||||
|
enabled = true
|
||||||
|
timeout_secs = 5
|
||||||
|
|
||||||
|
[agents.demo.workspace]
|
||||||
|
read_memory_from = []
|
||||||
|
unrestricted_filesystem = false
|
||||||
|
|
||||||
|
[[peripherals.boards]]
|
||||||
|
baud = 115200
|
||||||
|
board = "arduino-uno-q"
|
||||||
|
transport = "bridge"
|
||||||
|
|
||||||
|
[peer_groups.telegram_default]
|
||||||
|
admin_for_agent_scope = false
|
||||||
|
agents = []
|
||||||
|
channel = "telegram.default"
|
||||||
|
external_peers = ["8512813413"]
|
||||||
|
ignore = []
|
||||||
|
output_modality = "mirror"
|
||||||
|
|
||||||
|
[providers.models.custom.cloud]
|
||||||
|
uri = "http://127.0.0.1:8091/v1"
|
||||||
|
model = "sonnet"
|
||||||
|
native_tools = false
|
||||||
|
|
||||||
|
[runtime_profiles.unoq.thinking.budget_tokens]
|
||||||
|
|
||||||
|
[runtime_profiles.offline.thinking.budget_tokens]
|
||||||
|
|
||||||
|
[agents.default.workspace.access]
|
||||||
|
|
||||||
|
[agents.cloud.workspace.access]
|
||||||
|
|
||||||
|
[agents.demo.workspace.access]
|
||||||
|
|
||||||
|
# Native Anthropic via a Claude Max setup-token (sk-ant-oat01-…). Credential
|
||||||
|
# comes from the ANTHROPIC_OAUTH_TOKEN env on the daemon — kept OFF DISK.
|
||||||
|
|
||||||
|
[providers.models.anthropic.max]
|
||||||
|
model = "claude-sonnet-5"
|
||||||
|
|
||||||
|
[providers.models.custom.claude]
|
||||||
|
uri = "http://127.0.0.1:8091/v1"
|
||||||
|
model = "sonnet"
|
||||||
|
native_tools = false
|
||||||
|
fallback = ["llamacpp.local"]
|
||||||
|
|
||||||
|
[providers.models.llamacpp]
|
||||||
|
|
||||||
|
[providers.models.llamacpp.local]
|
||||||
|
uri = "http://127.0.0.1:8083/v1"
|
||||||
|
timeout_secs = 300
|
||||||
|
model = "qwen"
|
||||||
|
native_tools = false
|
||||||
|
|
||||||
|
[providers.models.custom]
|
||||||
|
|
||||||
|
[gateway]
|
||||||
|
port = 8080
|
||||||
|
host = "0.0.0.0"
|
||||||
|
allow_public_bind = true
|
||||||
|
require_pairing = false
|
||||||
|
web_dist_dir = "/app/web-dist"
|
||||||
|
paired_tokens = []
|
||||||
|
|
||||||
|
[skills]
|
||||||
|
prompt_injection_mode = "compact"
|
||||||
|
|
||||||
|
[risk_profiles.default]
|
||||||
|
level = "supervised"
|
||||||
|
allowed_tools = ["matrix_pattern", "matrix_text", "i2c_scan", "uno_q_flash", "sysfs_led", "camera", "network", "i2cdetect", "read_skill", "file_read", "content_search"]
|
||||||
|
auto_approve = ["matrix_pattern", "matrix_text", "i2c_scan", "uno_q_flash", "sysfs_led", "camera", "network", "i2cdetect", "read_skill", "file_read", "content_search", "memory_recall", "web_search_tool", "web_fetch", "calculator", "glob_search", "image_info", "weather", "tool_search", "browser", "browser_open"]
|
||||||
|
allowed_commands = ["git", "npm", "cargo", "ls", "cat", "grep", "find", "echo", "pwd", "wc", "head", "tail", "date", "df", "du", "uname", "uptime", "hostname", "python", "python3", "pip", "node", "free"]
|
||||||
|
allowed_roots = []
|
||||||
|
always_ask = []
|
||||||
|
block_high_risk_commands = true
|
||||||
|
excluded_tools = []
|
||||||
|
firejail_args = []
|
||||||
|
forbidden_paths = ["/etc", "/root", "/home", "/usr", "/bin", "/sbin", "/lib", "/opt", "/boot", "/dev", "/proc", "/sys", "/var", "/tmp", "~/.ssh", "~/.gnupg", "~/.aws", "~/.config"]
|
||||||
|
require_approval_for_medium_risk = true
|
||||||
|
shell_env_passthrough = []
|
||||||
|
workspace_only = true
|
||||||
|
|
||||||
|
[runtime_profiles.unoq]
|
||||||
|
agentic = true
|
||||||
|
max_tool_iterations = 6
|
||||||
|
strict_tool_parsing = false
|
||||||
|
max_actions_per_hour = 20
|
||||||
|
max_cost_per_day_cents = 500
|
||||||
|
max_delegation_depth = 0
|
||||||
|
shell_timeout_secs = 60
|
||||||
|
tool_call_dedup_exempt = []
|
||||||
|
tool_filter_groups = []
|
||||||
|
|
||||||
|
[agents.default]
|
||||||
|
enabled = true
|
||||||
|
model_provider = "anthropic.max"
|
||||||
|
risk_profile = "default"
|
||||||
|
runtime_profile = "unoq"
|
||||||
|
acp_enable_mcp = false
|
||||||
|
channels = ["telegram.default"]
|
||||||
|
classifier_provider = ""
|
||||||
|
cron_jobs = []
|
||||||
|
delegate_same_risk_profile = true
|
||||||
|
delegates = []
|
||||||
|
knowledge_bundles = []
|
||||||
|
mcp_bundles = []
|
||||||
|
skill_bundles = ["unoq"]
|
||||||
|
summary_provider = ""
|
||||||
|
transcription_provider = ""
|
||||||
|
tts_provider = ""
|
||||||
|
|
||||||
|
[peripherals]
|
||||||
|
enabled = true
|
||||||
|
|
||||||
|
[agents.cloud]
|
||||||
|
enabled = false
|
||||||
|
model_provider = "anthropic.max"
|
||||||
|
risk_profile = "default"
|
||||||
|
runtime_profile = "unoq"
|
||||||
|
acp_enable_mcp = false
|
||||||
|
channels = []
|
||||||
|
classifier_provider = ""
|
||||||
|
cron_jobs = []
|
||||||
|
delegate_same_risk_profile = true
|
||||||
|
delegates = []
|
||||||
|
knowledge_bundles = []
|
||||||
|
mcp_bundles = []
|
||||||
|
skill_bundles = ["unoq"]
|
||||||
|
summary_provider = ""
|
||||||
|
transcription_provider = ""
|
||||||
|
tts_provider = ""
|
||||||
|
|
||||||
|
[providers.models.custom.dead]
|
||||||
|
uri = "http://127.0.0.1:9099/v1"
|
||||||
|
model = "sonnet"
|
||||||
|
native_tools = false
|
||||||
|
fallback = ["llamacpp.local"]
|
||||||
|
|
||||||
|
[channels.voice_duplex.default]
|
||||||
|
enabled = true
|
||||||
|
excluded_tools = []
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# LEAN OFFLINE PROFILE (experiment 2026-07-19)
|
||||||
|
# Everything that controls prompt size lives on the runtime profile.
|
||||||
|
# Goal: get the prompt from ~4718 tokens down under ~800 so the on-board
|
||||||
|
# 0.5B (17 tok/s prefill) can actually answer in seconds, not minutes.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
[runtime_profiles.offline]
|
||||||
|
agentic = true
|
||||||
|
max_tool_iterations = 3
|
||||||
|
strict_tool_parsing = false
|
||||||
|
compact_context = true
|
||||||
|
prompt_injection_mode = "compact"
|
||||||
|
max_system_prompt_chars = 2000
|
||||||
|
max_context_tokens = 3000
|
||||||
|
max_history_messages = 2
|
||||||
|
memory_recall_limit = 1
|
||||||
|
parallel_tools = false
|
||||||
|
max_actions_per_hour = 20
|
||||||
|
max_cost_per_day_cents = 500
|
||||||
|
max_delegation_depth = 0
|
||||||
|
shell_timeout_secs = 60
|
||||||
|
tool_call_dedup_exempt = []
|
||||||
|
tool_filter_groups = []
|
||||||
|
|
||||||
|
# Narrow tool surface. Dropping the other peripheral tools also drops the
|
||||||
|
# hardware block + Uno-Q flash imperative from the system prompt entirely.
|
||||||
|
|
||||||
|
[risk_profiles.sense_only]
|
||||||
|
level = "supervised"
|
||||||
|
allowed_tools = ["i2cdetect"]
|
||||||
|
auto_approve = ["i2cdetect"]
|
||||||
|
allowed_commands = ["git", "npm", "cargo", "ls", "cat", "grep", "find", "echo", "pwd", "wc", "head", "tail", "date", "df", "du", "uname", "uptime", "hostname", "python", "python3", "pip", "node", "free"]
|
||||||
|
allowed_roots = []
|
||||||
|
always_ask = []
|
||||||
|
block_high_risk_commands = true
|
||||||
|
excluded_tools = []
|
||||||
|
firejail_args = []
|
||||||
|
forbidden_paths = ["/etc", "/root", "/home", "/usr", "/bin", "/sbin", "/lib", "/opt", "/boot", "/dev", "/proc", "/sys", "/var", "/tmp", "~/.ssh", "~/.gnupg", "~/.aws", "~/.config"]
|
||||||
|
require_approval_for_medium_risk = true
|
||||||
|
shell_env_passthrough = []
|
||||||
|
workspace_only = true
|
||||||
|
|
||||||
|
[risk_profiles.field_ops]
|
||||||
|
level = "supervised"
|
||||||
|
allowed_tools = ["i2cdetect", "sysfs_led", "file_read", "content_search", "network"]
|
||||||
|
auto_approve = ["i2cdetect", "sysfs_led", "file_read", "content_search", "network"]
|
||||||
|
allowed_commands = ["git", "npm", "cargo", "ls", "cat", "grep", "find", "echo", "pwd", "wc", "head", "tail", "date", "df", "du", "uname", "uptime", "hostname", "python", "python3", "pip", "node", "free"]
|
||||||
|
allowed_roots = []
|
||||||
|
always_ask = []
|
||||||
|
block_high_risk_commands = true
|
||||||
|
excluded_tools = []
|
||||||
|
firejail_args = []
|
||||||
|
forbidden_paths = ["/etc", "/root", "/home", "/usr", "/bin", "/sbin", "/lib", "/opt", "/boot", "/dev", "/proc", "/sys", "/var", "/tmp", "~/.ssh", "~/.gnupg", "~/.aws", "~/.config"]
|
||||||
|
require_approval_for_medium_risk = true
|
||||||
|
shell_env_passthrough = []
|
||||||
|
workspace_only = true
|
||||||
|
|
||||||
|
[risk_profiles.field_flash]
|
||||||
|
level = "supervised"
|
||||||
|
allowed_tools = ["i2cdetect", "sysfs_led", "file_read", "content_search", "network", "uno_q_flash"]
|
||||||
|
auto_approve = ["i2cdetect", "sysfs_led", "file_read", "content_search", "network", "uno_q_flash"]
|
||||||
|
allowed_commands = ["git", "npm", "cargo", "ls", "cat", "grep", "find", "echo", "pwd", "wc", "head", "tail", "date", "df", "du", "uname", "uptime", "hostname", "python", "python3", "pip", "node", "free"]
|
||||||
|
allowed_roots = []
|
||||||
|
always_ask = []
|
||||||
|
block_high_risk_commands = true
|
||||||
|
excluded_tools = []
|
||||||
|
firejail_args = []
|
||||||
|
forbidden_paths = ["/etc", "/root", "/home", "/usr", "/bin", "/sbin", "/lib", "/opt", "/boot", "/dev", "/proc", "/sys", "/var", "/tmp", "~/.ssh", "~/.gnupg", "~/.aws", "~/.config"]
|
||||||
|
require_approval_for_medium_risk = true
|
||||||
|
shell_env_passthrough = []
|
||||||
|
workspace_only = true
|
||||||
|
|
||||||
|
[risk_profiles.demo]
|
||||||
|
level = "supervised"
|
||||||
|
allowed_tools = ["matrix_pattern", "i2cdetect"]
|
||||||
|
auto_approve = ["matrix_pattern", "i2cdetect"]
|
||||||
|
allowed_commands = ["git", "npm", "cargo", "ls", "cat", "grep", "find", "echo", "pwd", "wc", "head", "tail", "date", "df", "du", "uname", "uptime", "hostname", "python", "python3", "pip", "node", "free"]
|
||||||
|
allowed_roots = []
|
||||||
|
always_ask = []
|
||||||
|
block_high_risk_commands = true
|
||||||
|
excluded_tools = []
|
||||||
|
firejail_args = []
|
||||||
|
forbidden_paths = ["/etc", "/root", "/home", "/usr", "/bin", "/sbin", "/lib", "/opt", "/boot", "/dev", "/proc", "/sys", "/var", "/tmp", "~/.ssh", "~/.gnupg", "~/.aws", "~/.config"]
|
||||||
|
require_approval_for_medium_risk = true
|
||||||
|
shell_env_passthrough = []
|
||||||
|
workspace_only = true
|
||||||
|
|
||||||
|
[agents.demo]
|
||||||
|
enabled = false
|
||||||
|
model_provider = "anthropic.max"
|
||||||
|
risk_profile = "default"
|
||||||
|
runtime_profile = "unoq"
|
||||||
|
skill_bundles = ["unoq"]
|
||||||
|
mcp_bundles = []
|
||||||
|
channels = ["telegram.default"]
|
||||||
|
acp_enable_mcp = false
|
||||||
|
classifier_provider = ""
|
||||||
|
cron_jobs = []
|
||||||
|
delegate_same_risk_profile = true
|
||||||
|
delegates = []
|
||||||
|
knowledge_bundles = []
|
||||||
|
summary_provider = ""
|
||||||
|
transcription_provider = ""
|
||||||
|
tts_provider = ""
|
||||||
|
|
||||||
|
[channels.telegram.default]
|
||||||
|
enabled = true
|
||||||
|
bot_token = ""
|
||||||
|
api_base_url = "https://api.telegram.org"
|
||||||
|
approval_timeout_secs = 120
|
||||||
|
draft_update_interval_ms = 1000
|
||||||
|
excluded_tools = []
|
||||||
|
interrupt_on_new_message = false
|
||||||
|
mention_only = false
|
||||||
|
reply_min_interval_secs = 0
|
||||||
|
reply_queue_depth_max = 0
|
||||||
|
stream_mode = "off"
|
||||||
|
|
||||||
|
# All Uno Q hardware skills (led-matrix, uno-q-hardware, flashing, sketch-patterns,
|
||||||
|
# modulino, bridge, i2c/spi, ADXL355 context…) loaded onto every agent so it knows
|
||||||
|
# the board it is on and how to interface with the onboard devices.
|
||||||
|
|
||||||
|
[skill_bundles.unoq]
|
||||||
|
directory = "shared/skills"
|
||||||
|
include = []
|
||||||
|
exclude = []
|
||||||
Executable
+96
@@ -0,0 +1,96 @@
|
|||||||
|
#!/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" <<EOF
|
||||||
|
KIT_ID=
|
||||||
|
CLAIM_CODE=
|
||||||
|
FLEET_SECRET=$FLEET_SECRET
|
||||||
|
APESS_URL=$APESS_URL
|
||||||
|
GATEWAY_PORT=8080
|
||||||
|
EOF
|
||||||
|
ok "apess-node.env (APESS_URL=$APESS_URL)"
|
||||||
|
|
||||||
|
# Optional embedded ZeroClaw dashboard (served at :8080/). Carried if present.
|
||||||
|
if [ -d "$HOME/projects/zeroclaw/web-dist" ]; then
|
||||||
|
cp -r "$HOME/projects/zeroclaw/web-dist" "$OUT/web-dist"; ok "web dashboard"
|
||||||
|
else
|
||||||
|
echo " (no web-dist — the node dashboard link will 404; students use the APESS wizard)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
ok "bundle ready: $OUT"
|
||||||
|
cat <<EOF
|
||||||
|
|
||||||
|
Distribute it:
|
||||||
|
1. Import onto a board (instructor test):
|
||||||
|
arduino-app-cli app new --from-app "$OUT"
|
||||||
|
2. Or share to students: open the app in App Lab → Share → QR. Students scan to
|
||||||
|
import, then Run. Set APESS_URL per team if not using mDNS
|
||||||
|
(edit .zeroclaw/apess-node.env, or point apess-api.local at the laptop).
|
||||||
|
EOF
|
||||||
Reference in New Issue
Block a user