Skip to content
Merged
Show file tree
Hide file tree
Changes from 67 commits
Commits
Show all changes
79 commits
Select commit Hold shift + click to select a range
4a8b8dc
feat(edition): single-harness lock (openclaw|hermes|dual) + premium d…
KrasimirKralev Aug 10, 2026
c329988
feat(hermes): chat model picker + surface Anthropic subscription OAut…
KrasimirKralev Aug 10, 2026
c0afff9
feat(hermes): dashboard desktop icon via auth-gated reverse proxy
KrasimirKralev Aug 10, 2026
370100d
feat(edition): productionize hermes edition in install.sh (skip openc…
KrasimirKralev Aug 10, 2026
8406be7
feat(edition): guard dual with ed25519 license (enforced) + native de…
KrasimirKralev Aug 10, 2026
b124c44
fix(hermes): compact desktop icons (no gap from hidden harness app) +…
KrasimirKralev Aug 10, 2026
dddce8b
fix(hermes): dashboard SSO via gated-mode bind + auth-broker proxy
KrasimirKralev Aug 10, 2026
42b3d3a
fix(hermes): rewrite Origin/Referer to upstream in dashboard proxy
KrasimirKralev Aug 10, 2026
d49d0fb
feat(hermes): configure models/provider/keys from the AI-provider panel
KrasimirKralev Aug 10, 2026
8cb04db
fix(hermes): guard nullable child streams in runHermesCli for TS strict
KrasimirKralev Aug 10, 2026
597a09d
feat(hermes): Hermes-native skills store (edition-gated, registry-only)
KrasimirKralev Aug 10, 2026
8c40ebf
feat(hermes): ClawBox AI through Hermes in the provider panel
KrasimirKralev Aug 10, 2026
a7d4a19
feat(hermes): OpenClaw-style provider cards + skills Browse default l…
KrasimirKralev Aug 10, 2026
63d96b9
fix(hermes): type provider list for special/tier fields (TS strict)
KrasimirKralev Aug 10, 2026
195b988
feat(hermes): native provider OAuth (Anthropic/OpenAI/Nous/Copilot) i…
KrasimirKralev Aug 10, 2026
4f64db9
feat(hermes): rich skill detail (SKILL.md, tags, links) + better stor…
KrasimirKralev Aug 10, 2026
45c388f
feat(hermes): provider stack, skills app, and audit hardening
KrasimirKralev Aug 10, 2026
af40c7a
fix(hermes): show the model picker for ClawBox AI in the chat header
KrasimirKralev Aug 10, 2026
c46a8dd
fix(hermes): thread chat turns into one session + refresh providers a…
KrasimirKralev Aug 10, 2026
9307e6e
test: make the suite hermetic against the device's edition file
KrasimirKralev Aug 10, 2026
eb1eb54
fix(hermes): use 'chat -q' so the chat actually has memory
KrasimirKralev Aug 10, 2026
d01faf1
docs(hermes): correct the chat route header — -z has no session memory
KrasimirKralev Aug 10, 2026
c3ca70b
fix(hermes): read the chat session id from stderr, not stdout
KrasimirKralev Aug 10, 2026
f89db70
fix(hermes): per-provider reasoning levels + fresh session on provide…
KrasimirKralev Aug 10, 2026
dbbab79
feat(hermes): ClawBox AI API-token entry + honest portal button copy
KrasimirKralev Aug 10, 2026
ed16bbb
fix(hermes): preserve the chat when switching provider/model
KrasimirKralev Aug 10, 2026
905ea0a
fix: Chat icon label, harness-aware Browser copy, and a workable chat…
KrasimirKralev Aug 10, 2026
4e7f17d
fix(hermes): hide Mixture of Agents until it is actually configured
KrasimirKralev Aug 10, 2026
da37e43
fix(settings): keep Local AI local, and stop re-fetching the harness …
KrasimirKralev Aug 10, 2026
fb0ad89
fix: add the missing client-harness imports
KrasimirKralev Aug 10, 2026
51ff215
fix(hermes): stop reporting a gateway failure on a device with no gat…
KrasimirKralev Aug 10, 2026
52ede0c
perf(hermes): memoise config reads; fix: show the real chat error
KrasimirKralev Aug 10, 2026
0f5cfca
fix(hermes): register the local model with Hermes, not only OpenClaw
KrasimirKralev Aug 10, 2026
a0930cd
fix(hermes): repair a local model that was enabled before Hermes coul…
KrasimirKralev Aug 10, 2026
fadab6f
fix(hermes): give the local provider a model to offer
KrasimirKralev Aug 10, 2026
bad006c
feat(hermes): drive Telegram through Hermes native commands
KrasimirKralev Aug 10, 2026
aca6ed6
fix(hermes): name the on-device model in the picker
KrasimirKralev Aug 10, 2026
4903175
docs(hermes): correct the measured Hermes CLI latencies in comments
KrasimirKralev Aug 10, 2026
b07ee0f
fix(hermes): drop the thinking dial where it controls nothing
KrasimirKralev Aug 10, 2026
6520724
fix(hermes): key the Telegram status probe by bot token
KrasimirKralev Aug 10, 2026
03c2d04
fix(install): strip the build machine's git identity from flashed dev…
KrasimirKralev Aug 10, 2026
6322008
docs(hermes): clarify why the pairing request id is the approvable token
KrasimirKralev Aug 10, 2026
59770bf
feat: name the token ladders the wizard's surfaces are already painting
KrasimirKralev Aug 10, 2026
a53add9
refactor: compose the setup header instead of stacking it
KrasimirKralev Aug 10, 2026
0779a3a
Revert "fix(install): strip the build machine's git identity from fla…
KrasimirKralev Aug 11, 2026
4e4325f
fix(hermes): read the failure cause from whichever stream carries it
KrasimirKralev Aug 11, 2026
907ebc3
feat: light the hermes skin on a hermes device
KrasimirKralev Aug 11, 2026
c8efbbc
refactor: compose the telegram step around its token field
KrasimirKralev Aug 11, 2026
f5e7c75
fix: outline a success message in the colour it is already using
KrasimirKralev Aug 11, 2026
b6cff63
refactor: put the dashboard on the ladders and stop the reset guessing
KrasimirKralev Aug 11, 2026
d3cbe1b
refactor: put the AI provider step on the shipped ladders
KrasimirKralev Aug 11, 2026
0280e53
refactor: put the security and update steps on the ladders
KrasimirKralev Aug 11, 2026
6d96d3d
fix: pick up new skills without stopping the gateway
KrasimirKralev Aug 11, 2026
80afc1a
fix: validate stored preferences and route the language persona per h…
KrasimirKralev Aug 11, 2026
acda2c4
fix: re-check the stored locale before it reaches the mascot prompt
KrasimirKralev Aug 11, 2026
ea95176
fix: stop the chat header pills truncating their own labels
KrasimirKralev Aug 11, 2026
dd644d2
fix: track the active locale in the html lang attribute
KrasimirKralev Aug 11, 2026
188b758
feat: name the roles five component ports each had to invent
KrasimirKralev Aug 11, 2026
d4177fc
chore: delete the orphaned done step
KrasimirKralev Aug 11, 2026
c3c9a48
fix: point the product at the community server the docs use
KrasimirKralev Aug 11, 2026
3af5726
feat(hermes): open a hermes device on its own wallpaper
KrasimirKralev Aug 11, 2026
c9daefa
feat: put the wizard's primary action back above the fold
KrasimirKralev Aug 11, 2026
4a6bf85
feat: ship the measured gemma 4 qat gguf
KrasimirKralev Aug 11, 2026
a7f1c34
perf(install): accept a prebuilt llama.cpp, and pin the one we build
KrasimirKralev Aug 11, 2026
daf484e
perf(local-ai): choose whether Gemma thinks, instead of inheriting it
KrasimirKralev Aug 11, 2026
8deae1c
fix(hermes): stop the model pill vanishing when the provider changes
KrasimirKralev Aug 11, 2026
66a2a24
fix: tighten html-to-text handling in mcp web tools
KrasimirKralev Aug 11, 2026
adf66c8
chore: release 3.9.0
KrasimirKralev Aug 11, 2026
b081fa1
fix: match end tags carrying attribute text in html-to-text
KrasimirKralev Aug 11, 2026
8dcaf71
fix(e2e): run the suite and fix the three ways it was red
KrasimirKralev Aug 11, 2026
e354704
fix(install): establish where a prebuilt came from, not just that it …
KrasimirKralev Aug 11, 2026
c306674
fix(test): repair a regex broken by an escaping slip
KrasimirKralev Aug 11, 2026
8d27ad0
fix(license): validate the --days argument and the expiry field
KrasimirKralev Aug 11, 2026
106956f
fix(hermes): handle a stdin write failure on a hermes cli call
KrasimirKralev Aug 11, 2026
f591eef
refactor(preferences): check the persona language domain in the writer
KrasimirKralev Aug 11, 2026
0a7b36a
fix(ui): follow the reported harness health in the locked badge
KrasimirKralev Aug 11, 2026
f6876e3
fix(hermes): quote the username in the generated dashboard auth block
KrasimirKralev Aug 11, 2026
e721dc3
test(harness): assert the store is untouched when the edition lock re…
KrasimirKralev Aug 11, 2026
a821e27
fix(ui): keep the harness picker rendering without a harness list
KrasimirKralev Aug 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 35 additions & 13 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,19 +119,41 @@ Handles two concerns:

### MCP Server (`mcp/`)

The AI agent interface to the OS. Exposes 40+ tools via MCP so the OpenClaw agent can control the device:

- **`clawbox-mcp.ts`** — MCP server with tool categories:
- **System**: `system_stats`, `system_info`, `system_power`
- **Shell**: `run_command`
- **Files**: `file_list`, `file_read`, `file_write`, `file_mkdir`
- **Browser**: `browser_launch`, `browser_navigate`, `browser_click`, `browser_type`, `browser_scroll`, `browser_screenshot`, `browser_keypress`, `browser_close`
- **Apps**: `app_search`, `app_install`, `app_uninstall`, `webapp_create`, `webapp_update`
- **UI control**: `ui_open_app`, `ui_list_apps`, `ui_notify`
- **Network**: `wifi_scan`, `wifi_status`, `vnc_status`
- **Config**: `preferences_get`, `preferences_set`
- **Code assistant**: `code_project_init`, `code_project_list`, `code_project_build`, `code_project_delete`, `code_file_write`, `code_file_read`, `code_file_edit`, `code_file_delete`, `code_file_list`, `code_search`
- **`clawbox-cli.ts`** — shell-callable CLI wrapper (`clawbox webapp create/update`, `clawbox app open/list`, `clawbox notify`, `clawbox system stats/info`, `clawbox code init/build/files/read/write/edit/search/delete`)
The AI agent interface to the OS. See `mcp/README.md` for the authoritative tool
list, the safety rules and the layout — that file is kept in step with the code;
this section is only a map.

**The tool set depends on the device edition**, resolved once at startup from the
root-owned `/etc/clawbox/edition.env` (`src/lib/edition-source.ts`). A tool that
cannot work on the running edition is not registered at all, because Hermes runs
a per-server circuit breaker that would take every ClawBox tool offline once one
of them kept failing.

- **`clawbox-mcp.ts`** — server entry: resolve edition → probe capabilities →
register → connect. Tool families live in `mcp/tools/` (`orientation`,
`skills`, `ai`, `system`, `desktop`, `browser`, `coding`) and the shared
machinery in `mcp/lib/` (`edition`, `guard`, `api`, `errors`, `schema`,
`register`, `context`, `jobs`, `web`).
- **Both editions**: `device_status`, `clawbox_health`, `clawbox_context`,
`system_stats`, `system_info`, `system_power`, `disk_usage`, `disk_cleanup`,
`update_check`, `logs_tail`, `screen_capture`, `backup_*`,
`telegram_status`, `wifi_scan`, `wifi_status`, `vnc_status`,
`preferences_get`, `preferences_set`, `ui_open_app`, `ui_list_apps`,
`ui_notify`, `app_uninstall`, `webapp_create`, `webapp_update`,
`code_project_init/list/build/delete`, `browser_open/navigate/screenshot/close`
- **Hermes only**: `skill_search`, `skill_list`, `skill_info`, `skill_install`,
`skill_uninstall`, `ai_list_models`, `ai_set_provider`, `ai_set_model`
- **OpenClaw only**: `app_search`, `app_install`,
`browser_click/type/keypress/scroll`, and the coding family — `bash`,
`job_status`, `job_stop`, `read_file`, `write_file`, `edit_file`,
`list_directory`, `glob`, `grep`, `notebook_edit`, `web_fetch`, `web_search`
- **`clawbox-cli.ts`** — shell-callable CLI wrapper (`clawbox webapp create/update`, `clawbox app open/list`, `clawbox notify`, `clawbox system stats/info`, `clawbox edition`, `clawbox code init/build/files/read/write/edit/search/delete`)
- **Registration** — the harness spawns this server only if its own config lists
it. OpenClaw: `scripts/gateway-pre-start.sh` writes `mcp.servers.clawbox` into
`~/.openclaw/openclaw.json`. Hermes: `scripts/register-mcp.sh` writes
`mcp_servers.clawbox` into `~/.hermes/config.yaml`, and is run by
`production-server.js` at every web-server boot and by
`scripts/setup-hermes-edition.sh` at install time.

### Code Assistant (`src/lib/code-projects.ts`, `src/app/setup-api/code/`)

Expand Down
23 changes: 20 additions & 3 deletions config/clawbox-gateway.service
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,12 @@ Wants=network-online.target
# gateway-pre-start.sh. Keep the limiter window long enough to contain several
# complete slow failures; inherited 5-in-10s defaults never tripped when one
# failed cycle already took longer than ten seconds (issue #284).
# Burst is well above the number of restarts a person can provoke by hand in an
# hour (skill installs no longer restart at all; model/config changes do), so the
# limiter only ever fires on a genuine crash loop, which produces starts far
# faster than that.
StartLimitIntervalSec=3600
StartLimitBurst=5
StartLimitBurst=20

[Service]
Type=simple
Expand All @@ -19,9 +23,22 @@ ExecStartPre=/home/clawbox/clawbox/scripts/gateway-pre-start.sh
# gateway-proxy.ts injects into the SPA). Passing a literal here would override
# that and reintroduce the shared-token / UI-drift bug (issues #149, #150).
ExecStart=/home/clawbox/.npm-global/bin/openclaw gateway --allow-unconfigured --bind lan
# Retry crashes and rejected startups, but leave deliberate clean stops alone.
Restart=on-failure
# OpenClaw restarts itself by EXITING and letting its supervisor start it again:
# it detects systemd from INVOCATION_ID/JOURNAL_STREAM, logs "restart mode: full
# process restart (supervisor restart)", writes a handoff file and calls exit(0).
# Under Restart=on-failure that clean exit was final — the gateway stayed down.
# Restart=always is what OpenClaw's own `openclaw daemon` unit template uses, and
# it is the only setting that honours that contract. `systemctl stop` still stops
# the unit for good, so deliberate operator stops are unaffected.
Restart=always
RestartSec=5
# Exit 78 (EX_CONFIG) is OpenClaw saying its configuration is unusable. Restarting
# would only reproduce it, so let the unit stay down and be diagnosed instead.
RestartPreventExitStatus=78
# 143 = 128+SIGTERM, the normal shutdown path. Without this the unit is marked
# failed every time it is stopped.
SuccessExitStatus=0 143
TimeoutStopSec=30
# gateway-pre-start.sh runs as a blocking ExecStartPre. The default
# ~90s start timeout could kill a legitimately slow first-boot pre-start
# (plugin fetch/unpack on a cold Jetson) mid-flight and wedge the unit into
Expand Down
32 changes: 32 additions & 0 deletions config/clawbox-hermes-dashboard-proxy.service
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
[Unit]
Description=ClawBox Hermes Dashboard Auth Proxy (LAN :8090, clawbox_session-gated)
After=clawbox-hermes-dashboard.service clawbox-setup.service
Wants=clawbox-hermes-dashboard.service
Comment on lines +3 to +4

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Wants= omits clawbox-setup.service.

Line 3 orders the proxy after clawbox-setup.service, but Line 4 requests only the dashboard. Ordering without a dependency has no effect when clawbox-setup.service is not otherwise pulled into the transaction. The proxy reads data/.session-secret and data/config.json, both owned by the main app, so state the dependency explicitly.

♻️ Proposed change
 After=clawbox-hermes-dashboard.service clawbox-setup.service
-Wants=clawbox-hermes-dashboard.service
+Wants=clawbox-hermes-dashboard.service clawbox-setup.service
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
After=clawbox-hermes-dashboard.service clawbox-setup.service
Wants=clawbox-hermes-dashboard.service
After=clawbox-hermes-dashboard.service clawbox-setup.service
Wants=clawbox-hermes-dashboard.service clawbox-setup.service
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@config/clawbox-hermes-dashboard-proxy.service` around lines 3 - 4, Update the
Wants= declaration in the proxy service unit to include clawbox-setup.service
alongside clawbox-hermes-dashboard.service, preserving the existing After=
ordering so the proxy explicitly pulls in both required services.

Source: Path instructions


[Service]
Type=simple
User=clawbox
WorkingDirectory=/home/clawbox/clawbox
Environment=HOME=/home/clawbox
Environment=CLAWBOX_ROOT=/home/clawbox/clawbox
Environment=HERMES_DASH_PROXY_PORT=8090
ExecStart=/usr/bin/node /home/clawbox/clawbox/scripts/hermes-dashboard-proxy.js
Restart=always
RestartSec=3
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved

# ── Sandboxing ─────────────────────────────────────────────────────────────
# This process only READS (the session secret, the dashboard password) and
# forwards TCP to 127.0.0.2:9119, so it can be locked down harder than the
# dashboard itself. ProtectSystem=full rather than strict so a future need to
# write under /home doesn't turn into a silent read-only failure.
NoNewPrivileges=yes
PrivateTmp=yes
ProtectSystem=full
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectControlGroups=yes
RestrictSUIDSGID=yes
LockPersonality=yes

[Install]
WantedBy=multi-user.target
55 changes: 55 additions & 0 deletions config/clawbox-hermes-dashboard.service
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
[Unit]
Description=ClawBox Hermes Dashboard (host-local web UI, gated auth)
After=network-online.target
Wants=network-online.target
Comment on lines +1 to +4

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Add restart rate limiting.

Restart=always with RestartSec=5 and no StartLimitIntervalSec/StartLimitBurst override uses the systemd defaults. On an embedded Jetson a crash loop that builds the web dist on each start consumes CPU continuously. Set an explicit limit, or set StartLimitBurst=0 deliberately if a permanent retry is intended.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@config/clawbox-hermes-dashboard.service` around lines 1 - 4, Add explicit
systemd restart-rate limiting in the [Unit] section of
clawbox-hermes-dashboard.service using appropriate StartLimitIntervalSec and
StartLimitBurst values, while preserving the existing restart behavior; if
permanent retries are intentionally required, explicitly set StartLimitBurst=0
instead.

Source: Path instructions


[Service]
Type=simple
User=clawbox
WorkingDirectory=/home/clawbox
Environment=HOME=/home/clawbox
Environment=PATH=/home/clawbox/.local/bin:/usr/local/bin:/usr/bin:/bin
# Stop any stray hermes server first so this instance can bind :9119 cleanly
# (avoids an "address already in use" crash loop if a serve/dashboard from a
# CLI chat or a prior run is still holding the port). Best-effort (`-`).
ExecStartPre=-/home/clawbox/.local/bin/hermes dashboard --stop
# Re-assert the dashboard's password auth provider on every start. This is what
# makes a factory reset survivable: the reset wipes ~/.hermes (previous owner's
# provider keys, OAuth tokens, chat DB, dashboard signing secret) and data/
# (the proxy's copy of the password), and nothing else on the box would ever
# re-create them — install.sh does not run at boot, so the dashboard would come
# up with no auth provider on its non-loopback bind and crash-loop forever.
# The script is idempotent: it verifies the stored hash against the stored
# password and exits early when they already agree.
# `-` prefix: a provisioning failure is logged to the journal but must not stop
# the dashboard from starting (install-time validation catches a dead proxy).
ExecStartPre=-/bin/bash /home/clawbox/clawbox/scripts/setup-hermes-dashboard-auth.sh
# Bind 127.0.0.2, NOT 127.0.0.1: Hermes only engages its cookie-session auth
# gate on a non-loopback bind (plain loopback uses a legacy shared-token mode
# that ignores session cookies, which a reverse proxy can't satisfy). 127.0.0.2
# is still inside 127.0.0.0/8 — host-local, never LAN-reachable — so the
# clawbox_session-gated proxy stays the only way in.
# `hermes dashboard` builds the web dist on first run, so allow a generous
# start window before the health check gives up.
ExecStart=/home/clawbox/.local/bin/hermes dashboard --host 127.0.0.2 --port 9119 --no-open
Restart=always
RestartSec=5
TimeoutStartSec=300

# ── Sandboxing ─────────────────────────────────────────────────────────────
# Matches config/clawbox-codex-auth-sync.service. Deliberately conservative:
# ProtectSystem=full (NOT strict) because Hermes builds its web dist and writes
# its config/logs/memories under /home/clawbox — strict would make the whole
# hierarchy read-only and turn every start into a failure. No PrivateTmp: the
# ExecStartPre `dashboard --stop` above has to be able to see a stray hermes
# started from a CLI chat in the host /tmp namespace.
NoNewPrivileges=yes
ProtectSystem=full
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectControlGroups=yes
RestrictSUIDSGID=yes
LockPersonality=yes

[Install]
WantedBy=multi-user.target
7 changes: 7 additions & 0 deletions config/clawbox-root-update@.service
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@ Description=ClawBox Root Update Step (%i)
[Service]
Type=oneshot
EnvironmentFile=-/etc/clawbox/network.env
# The device SKU. Without it every updater-run step evaluated
# is_hermes_edition() as false — and UPDATE_STEPS includes openclaw_install,
# openclaw_patch and gateway_setup, so an in-app update reinstalled and STARTED
# the OpenClaw gateway on a Hermes appliance. install.sh also reads this file
# directly (it is the authority, not this environment), but exporting it here
# keeps anything the steps shell out to consistent with the resolved edition.
EnvironmentFile=-/etc/clawbox/edition.env
ExecStart=/bin/bash /home/clawbox/clawbox/install.sh --step %i
# 30 min was not enough for llamacpp_install on a cold box: that step builds
# llama.cpp from source with CUDA on a 6-core Jetson Orin AND downloads the
Expand Down
12 changes: 12 additions & 0 deletions config/clawbox-setup.service
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,18 @@ Restart=always
RestartSec=3
EnvironmentFile=-/home/clawbox/clawbox/data/network.env
EnvironmentFile=-/home/clawbox/clawbox/.env
# MUST stay LAST of the EnvironmentFile= lines. /home/clawbox/clawbox/.env is
# owned by the clawbox user — writable from SSH, the in-UI terminal and the
# agent's run_command — and config/clawbox-sudoers grants that same user
# NOPASSWD `systemctl restart clawbox-setup`. systemd documents EnvironmentFile=
# as overriding Environment=, and later files override earlier ones, so before
# this line a customer could append CLAWBOX_EDITION=dual to .env, restart, and
# unlock the premium SKU — the "root-owned edition lock" was not a lock at all.
# /etc/clawbox/edition.env is root:root 0644, written by install.sh's
# step_edition_lock, and being last it wins.
# (The web server also reads the file directly via src/lib/edition-source.ts;
# this line keeps the process environment consistent with it.)
EnvironmentFile=-/etc/clawbox/edition.env
Environment=NODE_ENV=production
Environment=BUN_ENV=production
Environment=PORT=80
Expand Down
10 changes: 10 additions & 0 deletions config/clawbox-sudoers
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,16 @@
# prompt that never gets answered.

# Service-management: each unit listed twice (with + without .service).
#
# NOTE on the clawbox-gateway grants below: sudoers has no way to express "only
# on some editions", so on the HERMES SKU — where the OpenClaw gateway is an
# unauthenticated agent surface with no role — these would otherwise let anyone
# with clawbox-level access (SSH, the in-UI terminal, the agent's run_command)
# restart it straight back onto the LAN. That is exactly why install.sh's
# step_edition_gateway_state does not merely `disable` the unit on hermes: it
# REMOVES the unit file and then persistently `mask`s it, so `systemctl start
# clawbox-gateway` fails twice over (unit not found; unit is masked). The
# `--runtime unmask` grant further down cannot undo a persistent /etc mask.
clawbox ALL=(root) NOPASSWD: /usr/bin/systemctl restart clawbox-gateway.service
clawbox ALL=(root) NOPASSWD: /usr/bin/systemctl restart clawbox-gateway
clawbox ALL=(root) NOPASSWD: /usr/bin/systemctl start clawbox-gateway.service
Expand Down
12 changes: 12 additions & 0 deletions e2e-install/10-setup-wizard.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,18 @@ test.describe("fresh-install setup wizard (UI)", () => {
// to flip ai_model_configured for the wizard to advance. The ClawBox
// AI tile opens an owner-portal modal rather than accepting a raw
// token, so it's not usable for fully-automated wizard flow.
// The provider list opens on whichever provider is in play (ClawBox AI by
// default) and keeps the rest behind its "more providers" toggle, so open
// it before reaching for OpenAI.
// The list itself is waited on first: the step's testid is also on the
// skeleton rendered until the edition resolves, so counting the toggle
// before then would read zero and skip the expansion.
const aiStep = page.getByTestId("setup-step-ai-models");
await expect(aiStep.getByRole("radiogroup", { name: "AI Provider" })).toBeVisible();
const moreProviders = aiStep.getByRole("button", { name: /more provider/i });
if (await moreProviders.count() > 0) {
await moreProviders.first().click();
}
await page.getByText("OpenAI GPT").click();
// OpenAI defaults to the "Subscription" tab (ChatGPT Plus / Pro OAuth).
// We need the "API Key" tab for the plain-token path.
Expand Down
55 changes: 50 additions & 5 deletions e2e/helpers/clawbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1123,6 +1123,54 @@ export function wizardStepAfterWifi(page: Page) {
.first();
}

/**
* Fill the credentials step the way a customer does.
*
* The hotspot password and its confirmation sit behind a disclosure on the
* hotspot's own card (CredentialsStep.tsx) — the row states the requirement,
* a tap opens the fields. Both are still mandatory: the primary action stays
* unavailable until they are supplied and matching, which is what every caller
* of this helper goes on to exercise.
*/
export async function fillCredentialsStep(page: Page) {
await page.locator("#cred-password").fill("clawbox-pass");
await page.locator("#cred-confirm").fill("clawbox-pass");
await page.getByRole("button", { name: /Hotspot Password/i }).click();
await page.locator("#hotspot-password").fill("hotspot-pass");
await page.locator("#hotspot-confirm").fill("hotspot-pass");
}

/**
* Choose an AI provider from the wizard's provider list.
*
* The list shows the provider currently in play and keeps the rest behind its
* "Show more providers" toggle (AIModelsStep.tsx), so anything other than the
* default needs the list opened first. Picking closes it again on the chosen
* row, so this is also how a test hops from one provider to the next.
*/
export async function pickAiProvider(page: Page, name: string) {
const step = page.getByTestId("setup-step-ai-models");
await expandAiProviderList(page);
await step.getByText(name, { exact: true }).first().click();
}

/**
* Open the provider list if anything is still hidden behind its toggle.
*
* Waits for the list itself first: the step's testid is also on the neutral
* skeleton AIModelsStep renders until the device edition resolves, and a
* point-in-time count of the toggle would read zero through that frame and
* silently skip the expansion.
*/
export async function expandAiProviderList(page: Page) {
const step = page.getByTestId("setup-step-ai-models");
await expect(step.getByRole("radiogroup", { name: "AI Provider" })).toBeVisible();
const moreToggle = step.getByRole("button", { name: /more provider/i });
if (await moreToggle.count() > 0) {
await moreToggle.first().click();
}
}

export async function completeSetupWizard(page: Page) {
await expect(page.getByTestId("setup-step-wifi")).toBeVisible();
// Ethernet-first happy path: a wired uplink lets the wizard advance in-page.
Expand All @@ -1142,14 +1190,11 @@ export async function completeSetupWizard(page: Page) {
}
await expect(credentialsStep).toBeVisible({ timeout: 10_000 });

await page.locator("#cred-password").fill("clawbox-pass");
await page.locator("#cred-confirm").fill("clawbox-pass");
await page.locator("#hotspot-password").fill("hotspot-pass");
await page.locator("#hotspot-confirm").fill("hotspot-pass");
await fillCredentialsStep(page);
await page.getByRole("button", { name: /^Connect$/ }).click();

await expect(page.getByTestId("setup-step-ai-models")).toBeVisible();
await page.getByText("OpenAI GPT").click();
await pickAiProvider(page, "OpenAI GPT");
await page.locator("#ai-api-key").fill("sk-test-openai-key");
await page.getByRole("button", { name: /Connect to OpenAI GPT/i }).click();

Expand Down
Loading
Loading