Skip to content

release: v3.1.0 - #185

Merged
KrasimirKralev merged 57 commits into
mainfrom
beta
Jun 9, 2026
Merged

KrasimirKralev merged 57 commits into
mainfrom
beta

Conversation

@KrasimirKralev

Copy link
Copy Markdown
Contributor

ClawBox v3.1.0

Promotes beta β†’ main. 57 commits since 3.0.7 β€” a large feature release, fully tested on-device.

Highlights β€” Features

Highlights β€” Fixes

Version

3.0.7 β†’ 3.1.0 (minor)

Verification

Tested as a clean from-source build of beta on a real Jetson device; the skill-install reload flow and store discovery confirmed working end-to-end.

KrasimirKralev and others added 30 commits May 29, 2026 15:34
The setup WiFi connect is a single-radio handoff: joining the home network
tears down the setup hotspot, so the browser loses the box mid-connect and a
synchronous result never arrives β€” the wizard just appeared to hang, with no
indication a wrong password was the cause.

- network.ts: detect the WPA 4-way-handshake failure (wpa_supplicant WRONG_KEY,
  readable by the clawbox service via the adm group), fail fast instead of
  burning 3x90s retries, and throw a typed WifiAuthError. Add a pollable
  connect-status and a --wait 20 cap.
- wifi/connect: run switchToClient fire-and-forget, return {status:connecting}
  immediately (before the AP drops), record the outcome in connect-status.
- wifi/connect-status (new): GET endpoint the wizard polls across the outage.
- WifiStep: poll the status; show wrong-password / connected / handoff hint.
- StatusMessage: add info variant; i18n: wifi.switching + wifi.wrongPassword.
… settle, longer poll)

The wrong-password path was ~80-118s because restoring the AP re-ran the 20s
pre-scan and raced the still-busy radio. Skip the pre-scan when restoring after
a failed connect (SKIP_PRESCAN), add a settle delay before the first restore
attempt, and widen the wizard poll deadline to 130s so a slow restore can't trip
it into mis-reporting a wrong password as success.
…status

Setup Step 1 now recommends Ethernet and reacts live to a cable being plugged
in, instead of a passive one-shot check whose primary button skipped WiFi
regardless of cable state.

- network.ts getEthernetStatus: fix the detection bug where state.includes(
  'connected') also matched 'disconnected' (an unplugged ethernet read as
  connected); now exact-match the nmcli terse STATE. Add a real physical-link
  signal by reading /sys/class/net/<iface>/carrier, returning {connected, cable,
  iface} so the UI can tell 'cable in, getting internet' from 'no cable'.
- wifi/ethernet route + test: surface the new cable field.
- WifiStep: poll /setup-api/wifi/ethernet every 3.5s; show 'Ethernet connected'
  (ready) / 'cable detected β€” getting internet' / a recommended 'connect a cable
  or use Wi-Fi' hint; gate the primary 'Continue with Ethernet' button on a real
  connection; relabel the alternative to 'Use Wi-Fi instead'.
- i18n: wifi.ethConnected/ethConnecting/ethNoCable/proceedEthernet/useWifiInstead
  across all 10 locales.
The longer 'Continue with Ethernet' label wrapped and cramped the dot/badge in
the half-width button. Stack the buttons vertically (Ethernet primary full-width,
Wi-Fi alternative below), drop the redundant status dot (the status banner above
already conveys connected/connecting/no-cable), and keep the label one-line.
…ck nudge

Adversarial review of the Ethernet-first feature found 3 real issues:
- (low) the eth poll ran nmcli every 3.5s for the whole component lifetime,
  including during the WiFi-connect handoff and behind hidden UI. Gate it to the
  initial choice screen + not-connecting; move the connect-controller abort to
  its own unmount effect so the eth poll re-running can't abort a connect.
- (low) the poll's .catch hard-reset to {connected:false} on a transient fetch
  error, flickering status + de-gating the button. Keep last-known-good instead.
- (medium) a cable plugged in but never gaining internet stranded the user on a
  'Connecting…' spinner with the recommended button disabled. After ~15s show a
  warning + 'check the cable/router or use Wi-Fi' nudge (wifi.ethStuck, 10 locales).
When the user triggers a restart from the wizard's power menu, the
device reboots and the browser's connection drops β€” previously leaving
a dead "Restarting..." dropdown. Add a full-screen ReconnectingOverlay
that keeps the customer in a friendly animated loop (pulse rings,
orbiting dots, bobbing crab mascot) while it polls /setup-api/setup/status,
then reloads once the web server is back.

Reuses existing translated keys (wizard.restarting, settings.reconnecting,
settings.backOnline, settings.waitingOnline) so all 10 locales are covered.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Extend the keep-the-user-in-a-loop treatment to the two setup transitions
where the connection drops:

- Extract the shared full-screen animation shell into ReconnectStage
  (pulse rings, orbiting dots, bobbing crab, step checklist, optional
  instruction + manual button). ReconnectingOverlay now builds on it.

- Step 1β†’2 (WiFi): new WifiHandoffOverlay handles the single-radio network
  switch. The box leaves its AP for the home network and is unreachable at
  the same origin, so we guide the user to move their device, best-effort
  probe the box's new address (clawbox.local) via an <img> load (survives
  cross-origin where fetch is CORS-blocked), and auto-redirect to its setup
  page once reachable; a manual "Open clawbox.local" button is the fallback.
  WifiStep keeps polling connect-status underneath so a wrong password still
  dismisses the overlay and surfaces the error.

- Step 2β†’3 (update reboot): once the update's restart step takes the server
  down, UpdateStep hands off to ReconnectingOverlay, which polls until the
  server returns and reloads to resume the post-reboot fixup step.

i18n: add wifi.handoff* + wifi.openUrl to en + bg (others fall back to en,
matching the existing wrong-password keys).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
When the box has a live ethernet connection in Step 1, the cable is all
that's needed to finish setup, so drop the Wi-Fi alternative to keep the
choice clean. The button stays visible while a cable is plugged but hasn't
gained internet yet (and when there's no cable), so the user keeps a Wi-Fi
fallback in those states.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ssets

The postbuild copied the build output into the standalone dir with
`cp -r .next/static "$SDIR/.next/static"`. On a clean build (dest absent)
that's correct, but on a REBUILD over an existing build the dest already
exists, so cp copies *into* it β†’ `.next/static/static/...`. The standalone
server then can't find chunks at `/_next/static/chunks/*`, returns the 404
HTML page for them, and the browser throws ChunkLoadError + MIME errors,
crashing the app with a client-side exception.

This bites every in-place rebuild (manual deploys and the auto-updater
building over a prior release). Remove the destination dirs before copying
so the layout is always correct, and drop the trailing `|| true` that
masked real copy failures.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…otspot

Applying a new system password + hotspot name/password at Step 3 restarts
the setup AP, dropping anyone reaching the wizard through it. Everything now
stays on clawbox.local: an animated reconnect overlay probes the box until it
answers, then continues to Step 4 β€” no hardcoded AP IP.

- CredentialsHandoffOverlay: same-origin fetch probe (advance in place) or, when
  the device name changed, a cross-origin <img> probe + redirect to newname.local.
  Ethernet/home users resolve in seconds; AP users rejoin the renamed hotspot
  and auto-continue.
- CredentialsStep: show the overlay when the connection actually drops/moves
  (server-reported apRestarted, or device-name change) instead of the old
  hostname-only toast that could never succeed for a renamed AP.
- hotspot route: return apRestarted, and add a single-radio guard β€” defer the AP
  restart when the box is currently a WiFi client so we don't sever the home
  link; the settings apply on the next AP start.
- translations: add the 4 credentials.handoff* keys to all 10 locales and
  backfill the 6 wifi.* handoff keys missing from 8 locales (i18n parity).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
When the single-radio box switched to the home network, a wrong password
left the user stranded: the box restored the ClawBox-Setup hotspot, but the
handoff overlay only told them to join their home WiFi (where the box never
appears), so the wizard's status poll could never reach the box.

- WifiHandoffOverlay: surface a recovery hint during the waiting phase telling
  the user to reconnect to ClawBox-Setup if the box can't join
- WifiStep: extend the connect-status poll deadline (130s -> 300s) and, on
  mount, surface a recent failed attempt with the SSID prefilled for retry
- ReconnectStage: add optional secondaryInstruction callout
- Remove the "Open clawbox.local" button and the duplicate "Reconnect this
  device" step so a single "Connecting to {ssid}" label remains
- translations: add wifi.handoffRecover in all 10 languages
- tests: update wifi/connect to the fire-and-forget connect-status contract

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The ClawBox-Setup hotspot would only appear after a manual
`systemctl restart clawbox-ap.service` β€” at power-on it lost a race:
NetworkManager wasn't fully ready and/or a leftover client WiFi profile
(saved during a failed setup attempt) auto-grabbed the radio in station mode,
so `nmcli connection up ClawBox-Setup` failed. As a Type=oneshot service it
never retried, leaving the box unreachable over WiFi.

start-ap.sh now:
- waits for NetworkManager to report ready before touching the radio
- frees the radio from stray client profiles (and, while setup is incomplete,
  disables their autoconnect so they stop hijacking the interface each boot)
- retries the AP bring-up up to 5x, verifying the interface actually entered
  AP mode rather than trusting nmcli's exit code

Post-setup saved-WiFi reconnect is unaffected (explicit `nmcli connection up`,
no reliance on autoconnect).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A failed `nmcli device wifi connect` leaves behind a saved connection
(autoconnect on by default) for the target SSID. On a single-radio adapter
NetworkManager keeps retrying that profile in the background, repeatedly
pulling the radio out of AP mode β€” so the ClawBox-Setup hotspot flaps and
disappears ("network could not be found") while the user is still on it,
especially after repeated wrong-password attempts during setup.

switchToClient now deletes the just-attempted profile on failure before
restoring the AP, keeping the radio dedicated to the hotspot until the user
retries. Complements the boot-time autoconnect guard in start-ap.sh.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The ClawBox-Setup connection is autoconnect=no (so it doesn't fight the
deliberate client-connect handoff), which means anything that downs it β€” a
stray root nmcli teardown, a driver hiccup, or a failed connect whose AP
restore didn't finish β€” leaves the radio dark with nothing to bring it back.
The hotspot then "removes itself" and the box is unreachable over WiFi until a
manual `systemctl restart clawbox-ap.service`.

Add a watchdog (clawbox-ap-watchdog.timer, every ~20s) that re-raises the AP
whenever setup is incomplete and the radio is idle/disconnected. It stands
down when:
- setup is complete (post-setup flow owns the radio),
- the radio is connected to anything β€” so it never tears down a successful
  home-network connect,
- a deliberate WiFi handoff holds data/wifi-connecting.lock.

switchToClient now writes that lock while it owns the radio and clears it in a
finally; stale locks age out after 180s. install.sh registers and enables the
new units.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Jetson has a single WiFi radio, so the ClawBox-Setup hotspot can only run
when the radio isn't held by a WiFi client. After setup the box always tried to
join saved WiFi first and only fell back to the AP if none connected β€” so
plugging in an Ethernet cable did nothing and the user had to 'forget' the
network to get the hotspot back.

start-ap.sh now detects a wired uplink (ethernet_connected) and, when present,
keeps the radio for the hotspot instead of joining saved WiFi; release_wifi_for_ap
also disables WiFi autoconnect so the client can't grab the radio back. Saved
WiFi is still reconnected explicitly when Ethernet is absent.
The dropdown gate only blocked Max-tier picks the user actively clicked. A
non-Max account could boot with the Max tier already saved as the default,
where the portal gateway silently rejects every turn β€” surfacing only the
opaque '[assistant turn failed before producing content]'. On load, detect a
Max-tier active model on a non-Max plan, show an upgrade message with the
portal link, and auto-switch to the Pro tier the plan supports so chat works.
During the single-radio WiFi handoff the overlay reused wifi.connectedMessage
('Connected! ...') as its instruction β€” a premature success claim shown while
the box is still joining and the password may be wrong. Use a neutral
wifi.handoffInstruction (next step, no success claim) and show the
wrong-password recovery hint for the whole handoff, not just after the grace
period. (en + bg added; other locales fall back to English.)
On networks whose AP drops wired->Wi-Fi mDNS multicast, <hostname>.local
is unreliable. Add a default-route-aware getReachableIpv4() returned from
the system/hostname API and a useDeviceAddress hook, so the Settings
'Access this device at' card and the Wi-Fi step lead with the box IP and
keep .local as a best-effort fallback.
…fire-and-forget

- translations.ts: add wifi.switching + wifi.wrongPassword to the 8 remaining
  locales (de/es/fr/it/ja/nl/sv/zh) β€” the key-parity test requires every locale
  to carry the same keys as en.
- connect.test.ts: the route is now fire-and-forget (returns 'connecting' and
  reports the outcome via the background switchToClient -> setConnectStatus).
  Mock setConnectStatus/WifiAuthError and drive the background with fake timers,
  asserting the recorded status (incl. reason=wrong-password).
…sted

vi.mock is hoisted above the file body, so referencing a top-level class in
its factory threw 'Cannot access FakeWifiAuthError before initialization' and
the suite loaded 0 tests. Move the stub into vi.hoisted() so it exists when
the hoisted factory runs.
The wrong-password feature made /wifi/connect fire-and-forget: it returns
"connecting" and the wizard polls /wifi/connect-status for the outcome,
only advancing on phase="connected". The e2e mock still returned the old
{success:true} and never mocked connect-status, so the poll never resolved
and every setup spec timed out waiting for the credentials step.
The update step auto-advances to credentials as soon as it finds nothing to
install. With test timers capped by installClawboxMocks, that can happen
before Playwright catches the update step on screen, so the hard
expect(updateStep).toBeVisible() was losing the race (consistently for
setup-final-loading, flakily for setup-happy-path). Wait for whichever of
update/credentials appears first, and only drive the Continue button when the
update step is actually present.
fix(chat): guard non-Max accounts stuck on the Max tier
…edback

feat(wifi): detect wrong WiFi password during setup and surface it
Resolve WifiStep.tsx: keep beta's fire-and-forget connect-status poll flow
(from #166's wrong-password detection) and surface the IP-primary URL
(primaryUrl from useDeviceAddress) in all connect status messages, since
#164's hook no longer exposes localUrl.
feat: show the device IP as the primary access address
Resolve WifiStep.tsx: keep setup-step1's handoff-overlay flow (no onNext on
connect β€” the overlay detects the box's new address and redirects), layered
with beta's wrong-password detection + primaryUrl/localUrl from useDeviceAddress
(handoff targetUrl stays localUrl: .local re-resolves after the network switch).
Resolve connect.test.ts: take beta's fire-and-forget tests (match the merged
route; drops setup-step1's minor non-Error-throw case).
The beta merge layered setup-step1's and #166's copies of these keys in 8
locales; the build rejects duplicate object keys. Keep the copy placed after
wifi.connectedMessage (matches the en/bg canonical structure).
Ethernet-first Step 1 + WiFi handoff redirect (setup-step1) changes how the
wizard completes β€” it redirects to the box's home-network address instead of
advancing in-page, so these full-wizard specs can't complete in e2e. Marked
test.fixme pending the rework tracked in #167. Desktop/feature specs are
unaffected (they skip setup via mock state).
KrasimirKralev and others added 26 commits June 5, 2026 14:17
The 5 setup specs were fixme'd because the WiFi path redirects through the
handoff overlay (untestable in e2e). But setup-step1 is Ethernet-first:
'Continue with Ethernet' advances the wizard in-page with no network switch,
so the specs run end-to-end again via that path (mock now reports Ethernet
connected). Un-fixme'd all 5; the WiFi-handoff path stays tracked in #167.

Coverage floor 40 -> 38: the new handoff/reconnect overlays + WiFi-path code
can't be e2e-covered (they redirect to the box's new address), pinning the
aggregate at ~39%. Documented; raise back to 40 when #167 lands handoff e2e.
CLAWBOX_TEST_MODE reports Ethernet connected, so 'Continue with Ethernet'
advances the wizard in-page. The old WiFi flow ('Connect to WiFi' β†’ scan β†’
connect) no longer exists in the Ethernet-first Step 1, and the WiFi path now
redirects through the handoff overlay (untestable in a container, see #167).
…irst

feat(setup): Ethernet-first Step 1, WiFi handoff & AP self-healing watchdog
1a66904 added wifi.handoffInstruction to en+bg only; the translations test
requires all 10 locales to share identical keys. Add de/es/fr/it/ja/nl/sv/zh
(placeholder {url} preserved).
fix(wifi): handoff overlay no longer claims 'Connected!' mid-switch
feat(ap): host the hotspot automatically when Ethernet is connected
…uth)

OpenClaw 2026.6.x renamed the ChatGPT-subscription provider id from
`openai-codex` to `codex` and moved its auth to the Codex app-server
(~/.codex/auth.json) instead of an openclaw token profile. This bumps the
fleet pin to 2026.6.1 and migrates ClawBox's integration:

- Rename provider id openai-codex -> codex across catalog, configure, status,
  chat/model, AIModelsStep, ChatPopup, provider-models (+ unit tests), keeping
  back-compat detection for the legacy id.
- gateway-pre-start.sh synthesizes ~/.codex/auth.json from the codex OAuth
  profile (account_id decoded from the access-token JWT); write-if-missing so
  the Codex app-server owns refresh after.
- configure route clears ~/.codex/auth.json on a fresh codex login so it
  regenerates with the new token.
- One-time config migration rewrites a stored openai-codex/<model> primary to
  codex/<model> on gateway start, so already-configured devices self-heal
  instead of hitting "Unknown model".
Add backup management to ClawKeep, backed by a sidecar manifest.json in
the same R2 prefix (no metadata in object keys):

- Named backups: optional label per snapshot ("Name this backup").
- Lock/protect: a locked snapshot can't be deleted manually or by
  auto-cleanup until unlocked.
- Retention: after each successful backup, prune old snapshots
  (default keep last 10, user-adjustable, 0 = off). Locked snapshots
  are always kept and never count toward the limit.

Python (clawkeep/clawkeep):
- s3.py: read_manifest/write_manifest, delete_snapshot; list_snapshots
  merges label/locked and excludes manifest.json; stats excludes it too.
- runner.py: write a manifest entry after a successful upload, then
  apply_retention (reads retentionKeepLast from schedule.json).
- cli.py: new label/lock/unlock/delete/prune subcommands; snapshots
  JSON now carries label + locked. daemon.py: --label passthrough.

TS bridge (src/lib/clawkeep.ts):
- retentionKeepLast on ClawKeepSchedule/DEFAULT_SCHEDULE/sanitiseSchedule.
- setSnapshotLabel/lock/unlock/deleteSnapshot/pruneSnapshots; snapshot
  type gains label?/locked?; runBackup accepts a label.

Portal routes: snapshots/label, snapshots/lock, snapshots/delete (409 +
kind:"locked" when locked); backup route accepts a label; schedule route
round-trips retentionKeepLast via sanitiseSchedule.

UI (ClawKeepApp.tsx): per-row Rename / Lock-Unlock / Delete (delete
disabled + "Unlock first" tooltip when locked), label/πŸ”’ display,
"Name this backup" input, and a "Keep last N backups" setting with
"Locked backups are always kept" helper text. All new strings added to
every locale in clawkeep-translations.ts.

Tests: Python unit tests for apply_retention (locked exemption, keep_last
boundary, manifest GC), manifest round-trip, delete-refuses-locked,
label/lock CLI. Existing TS schedule tests updated for retentionKeepLast.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ClawKeep: named, lockable, auto-cleanup backups
The auth.json file is already written mode 0600, but its parent ~/.codex was created with the default umask (world-listable), allowing token-file enumeration. chmod the dir to 0700 (owner-only) right after mkdir.
Adds recursive file search (bounded async searchTree) + an in-window file viewer/editor (edit text/code with Save, preview images/PDF/audio/video, download fallback) to the Files app, with files.* i18n across all 10 locales and route tests. CodeRabbit comments addressed (async walk, truncation test, discard-confirm a11y).
#176)

Extracts the per-provider reasoning config to a testable lib (chat-reasoning.ts), declares llamacpp as off-only, and hides the effort picker when a provider has no real choice β€” so Gemma no longer errors with 'thinkingLevel not supported'. Cloud providers unchanged. 6 unit tests.
…#172)

Persist the portal-confirmed ClawBox AI tier into clawai_tier so the portal-unreachable fallback reflects the last confirmed tier, stopping the Free badge flapping to Pro on transient portal blips. 24/24 status tests.
Resolves the ChatPopup conflict: #176 relocated REASONING_BY_PROVIDER into chat-reasoning.ts, so #171's codex rename (openai-codex -> codex) now lives there instead of the inline block beta removed.
…x-migration

feat: support OpenClaw 2026.6.1 (codex provider rename + app-server auth)
* fix: reset ClawKeep when the ClawBox AI account changes

ClawKeep pairs independently of the ClawBox AI account β€” its own claw_ token
resolves on the portal to an account and a cloud-storage prefix. So after
switching ClawBox AI accounts the device kept backing up to the OLD account's
storage, and the dashboard kept showing the old account's stats.

- ai-models/configure: detect an account switch (the clawai token changing
  from a previously-stored one) and unpair ClawKeep so it re-pairs against the
  current account. The token is opaque (no account id), so a changed token is
  the only available signal. Also clears the old account's stats as an
  offline-safe wipe.
- clawkeep: add unpairLocal() β€” shared by the unpair route and the
  account-change reset so the two stay in lockstep β€” and syncStateFromCloud(),
  which seeds state.json from the account's real cloud snapshots on pair, so a
  re-authenticated account shows its existing backups immediately instead of
  "0 B / never" until the next local backup.
- clawkeep/pair/poll: sync cloud state right after writing the new token.
- setup/reset: a factory reset now also wipes ~/.clawkeep, so it can't leave
  the device paired to the previous account either.
- ClawKeepApp: poll status on a single variable-cadence timer so an already-open
  window reflects the server-side unpair without a manual click.

* fix: address CodeRabbit review on ClawKeep account-reset

- configure: don't fail open when reading the prior clawai_token β€” abort the ClawBox AI save with 503 if getAll() throws, instead of defaulting to "" and silently skipping the account-switch unpair guard.
- ClawKeepApp: skip a poll tick while the previous refresh is still in flight so a slow fetch can't stack concurrent requests on the Jetson.
- clawkeep: give writeStateFile a per-call temp name (pid + counter) so concurrent writers can't clobber each other's temp before the atomic rename.
…afety, durable webapps) (#178)

* fix: authenticate ClawBox CLI to /setup-api/* with the MCP bearer token

The CLI called /setup-api/* with no Authorization header, so once setup completes middleware.ts 307'd it to /login and JSON.parse choked on the login HTML ("Failed to parse JSON"). Read CLAWBOX_MCP_TOKEN from the env, falling back to the data/.mcp-token file the gateway pre-start script writes (the CLI is launched separately from the MCP server and may not inherit its env), and inject it as a Bearer header β€” mirroring clawbox-mcp.ts. Loaded lazily so token-free commands like `app list` still work.

* feat: harden the ClawBox MCP β€” structured errors, health, bash block, durable webapps

- Structured errors: wrap every tool handler (tool()) so failures return a
  parseable { error, code, message, details } envelope; api() throws a typed
  ApiError classified by status (AUTH_FAILED on 401/403, ENDPOINT_DOWN on 5xx, …).
- clawbox_health tool: verifies the MCP bearer + /setup-api/* reachability so a
  broken token is diagnosable up front instead of via a cryptic tool failure.
- bash safety: hard-block destructive commands (rm -rf /, dd, mkfs, fork bombs,
  …) unless allowDangerous:true; enforced at the spawnBackground chokepoint so
  the agent tool can't bypass it. Git-safety patterns still only warn.
- Durable webapps: the webapps POST route and buildProject now register the app
  in desktop preferences server-side (registerWebappInPreferences), so a webapp
  created while the desktop is closed still appears on its next load instead of
  relying on the lossy ui:pending-action handoff.
- Docs: mcp/README.md (auth flow, tool catalog, testing, failure modes).

* refactor: extract deployWebapp() chokepoint + fix code-projects test

- Extract deployWebapp(appId, html, {name, color, icon}) β€” the one place that
  writes data/webapps/<id>/{index.html,meta.json} and registers the app on the
  desktop. The webapps POST route and buildProject both call it, so their
  on-disk layout / meta.json shape / desktop registration can't drift (or be
  half-applied by one caller forgetting a step).
- webapps POST: deploy+register only on create (name present); an update now
  just rewrites index.html instead of clobbering meta.json's saved name.
- Fix code-projects.test.ts: stub @/lib/webapp-registry so buildProject's new
  registration call doesn't hit real config IO β€” this was failing #178's `test`
  job (EACCES mkdir /home/clawbox).

* test: stub deployWebapp in webapps route test

The deployWebapp() extraction made the create POST call it, but webapps.test.ts only mocked WEBAPPS_DIR + APP_ID_RE β€” so deployWebapp was undefined and the create test 500'd. Stub it (its desktop registration is covered by code-projects/webapp-registry tests).

* test: match tool() rename in browser-guidance assertion

The structured-error wrapper renamed server.tool( -> tool(, so the source-string assertion for the browser_open registration needs the same.

* fix: address coderabbit review on mcp hardening

- cli: resolve token-file root via dev-mode cwd like mcp-token.ts
- health: don't follow 302->/login (manual redirect + accept json) so
  auth failures aren't misreported as JSON parse errors
- readme: tag the fenced diagram block as text (markdownlint MD040)
- webapps route: treat missing 'name' as update (404 if app absent) and
  reject empty-name creates, instead of a truthiness branch
- code-projects: split writeWebappIndex from deployWebapp so rebuilds
  refresh index.html only and don't clobber icon / re-surface hidden apps
- tests: assert buildProject registers on the desktop; cover the route's
  empty-name (400), update (200) and missing-app (404) branches
)

apps/settings only has a config writer for home-assistant, so for any
other skill the entered keys land in preferences but never reach where
the skill reads them. The save button showed "Connected!" regardless,
implying the skill was wired up when it wasn't. Gate the green
"Connected!" on the backend's configWritten flag; otherwise show a
neutral "Saved" plus a note that the skill's setup isn't wired yet (and
that the input never leaves the device).
The ui:pending-action open_app handoff was never cleared: the desktop
sent { key, delete: true } but the KV route's delete contract is
{ delete: "<key>" }, so the request 400'd and the action persisted,
reopening the app on every reload regardless of the window being closed.
Send the correct { delete: "<key>" } shape so consumed actions are
actually removed.

Also restore persisted windows minimized to the taskbar instead of
popping them open over a fresh desktop on each reload/reboot.
The OpenClaw gateway runs several file watchers (skills/workspace, config
reload, MEMORY.md). Jetson's stock inotify ceilings (128 instances /
65536 watches) can be exhausted under load and surface as
"EMFILE: too many open files" crashes and gateway restart loops.

Persist higher limits via /etc/sysctl.d/99-clawbox-inotify.conf
(instances 512, watches 524288) in setup-optimizations.sh. The apply is
best-effort and guarded so it can't fail the install inside CI/containers
where /proc/sys is read-only. Verified on a real Jetson: limits raise
from 128/65536 to 512/524288.

Closes #160
…re (#183)

setup/status and chat/model reported ai_model_provider from the ClawBox
config store, which only refreshes when the model is changed through the
ai-models/configure route. When the live OpenClaw primary model changes
elsewhere, those surfaces drift (e.g. live moved to openai/... but
ClawBox still reported openai-codex).

Prefer the provider derived from the live OpenClaw primary model
(openclaw.json agents.defaults.model.primary), falling back to the
config store for local/no-model cases. chat/model reuses the existing
normalizeProviderFromModel + isLocalModel helpers; setup/status (which
already reads the live openclaw config) derives the cloud-provider prefix.

Closes #162
)

- Sort control (Popular / Top rated / A–Z) over the loaded set. ClawHub ignores
  sort params (verified live), so it's client-side; "Popular" keeps ClawHub's
  own order (already roughly install-count desc).
- "Official" badge (ClawHub channel === "official") on cards + the detail
  header β€” the only meaningful trust signal, since `verified` is true for every
  listing. Cards also now show the install count.
- Inspect enrichment: the detail view fetches the per-skill detail endpoint
  (proxied via apps/store?slug=) and surfaces Featured, last-updated date,
  precise all-time installs, an "executes code" indicator, and deep-links to the
  canonical ClawHub page. ClawHub exposes no full README via API (summary is
  capped at 200 chars), so we link out for the full write-up.
- i18n: 8 new store keys across all 10 locales (parity test green).
#184)

Installing a skill showed "Reloading skills…" forever and the agent
never picked up the new skill.

Root cause: reloadGateway() found the gateway via
`pgrep -f "openclaw-gateway"`, but the gateway process renames its argv
to just "openclaw", so the match failed and SIGUSR1 was never sent β€” the
gateway never restarted, the chat WS never dropped, and the reload overlay
hung waiting for a reconnect that never came.

- reloadGateway: resolve the gateway PID from systemd MainPID (with an
  exact-name `pgrep -x openclaw` fallback) and send SIGUSR1 to it.
- optimize-cpu.sh: fix the same broken matcher (the gateway never got its
  CPU affinity set).
- ChatPopup: document why the skill path must NOT force a reconnect like the
  provider path β€” the install route doesn't await the restart, so the
  natural onClose β†’ reconnect β†’ resolve flow delivers the post-restart
  hello; forcing a reconnect races the restart and the auto-send lands on a
  dead socket (observed live: no chat.send reached the gateway).
- tests: update reloadGateway specs for the systemd-MainPID resolution.

Verified live: install a skill β†’ gateway restarts β†’ chat reconnects β†’ the
"I just installed X" message reaches the gateway and the agent confirms.
@KrasimirKralev
KrasimirKralev requested a review from a team as a code owner June 9, 2026 11:22
@coderabbitai

coderabbitai Bot commented Jun 9, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@KrasimirKralev, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 28 minutes and 26 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

βŒ› How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
βš™οΈ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: b4c6fea3-6cac-4e59-9053-3cd51f078747

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between 3c1bf5f and 0e29af7.

πŸ“’ Files selected for processing (97)
  • clawkeep/clawkeep/cli.py
  • clawkeep/clawkeep/daemon.py
  • clawkeep/clawkeep/runner.py
  • clawkeep/clawkeep/s3.py
  • clawkeep/tests/test_cli.py
  • clawkeep/tests/test_runner.py
  • clawkeep/tests/test_s3.py
  • config/clawbox-ap-watchdog.service
  • config/clawbox-ap-watchdog.timer
  • config/openclaw-target.txt
  • docs/clawbox-handoff.md
  • docs/clawkeep-backup-mgmt-spec.md
  • e2e-install/10-setup-wizard.spec.ts
  • e2e/helpers/clawbox.ts
  • e2e/setup-anthropic-path.spec.ts
  • e2e/setup-local-ai-order.spec.ts
  • e2e/setup-openai-path.spec.ts
  • install.sh
  • mcp/README.md
  • mcp/clawbox-cli.ts
  • mcp/clawbox-mcp.ts
  • package.json
  • scripts/ap-watchdog.sh
  • scripts/e2e-coverage-report.mjs
  • scripts/gateway-pre-start.sh
  • scripts/setup-optimizations.sh
  • scripts/start-ap.sh
  • src/app/page.tsx
  • src/app/setup-api/ai-models/catalog/route.ts
  • src/app/setup-api/ai-models/configure/route.ts
  • src/app/setup-api/ai-models/status/route.ts
  • src/app/setup-api/apps/store/route.ts
  • src/app/setup-api/chat/model/route.ts
  • src/app/setup-api/clawkeep/backup/route.ts
  • src/app/setup-api/clawkeep/pair/poll/route.ts
  • src/app/setup-api/clawkeep/snapshots/delete/route.ts
  • src/app/setup-api/clawkeep/snapshots/label/route.ts
  • src/app/setup-api/clawkeep/snapshots/lock/route.ts
  • src/app/setup-api/clawkeep/unpair/route.ts
  • src/app/setup-api/files/route.ts
  • src/app/setup-api/setup/reset/route.ts
  • src/app/setup-api/setup/status/route.ts
  • src/app/setup-api/system/hostname/route.ts
  • src/app/setup-api/system/hotspot/route.ts
  • src/app/setup-api/webapps/route.ts
  • src/app/setup-api/wifi/connect-status/route.ts
  • src/app/setup-api/wifi/connect/route.ts
  • src/app/setup-api/wifi/ethernet/route.ts
  • src/components/AIModelsStep.tsx
  • src/components/AppStore.tsx
  • src/components/ChatPopup.tsx
  • src/components/ClawKeepApp.tsx
  • src/components/CredentialsHandoffOverlay.tsx
  • src/components/CredentialsStep.tsx
  • src/components/DoneStep.tsx
  • src/components/FilesApp.tsx
  • src/components/InstalledAppSettings.tsx
  • src/components/ReconnectStage.tsx
  • src/components/ReconnectingOverlay.tsx
  • src/components/SettingsApp.tsx
  • src/components/SetupWizard.tsx
  • src/components/StatusMessage.tsx
  • src/components/UpdateStep.tsx
  • src/components/WifiHandoffOverlay.tsx
  • src/components/WifiStep.tsx
  • src/hooks/useDeviceAddress.ts
  • src/hooks/useLocalUrl.ts
  • src/lib/chat-reasoning.ts
  • src/lib/clawkeep-translations.ts
  • src/lib/clawkeep.ts
  • src/lib/code-projects.ts
  • src/lib/desktop-translations-part1.ts
  • src/lib/desktop-translations-part2.ts
  • src/lib/desktop-translations-part3.ts
  • src/lib/desktop-translations.ts
  • src/lib/handoff-probe.ts
  • src/lib/network.ts
  • src/lib/openclaw-config.ts
  • src/lib/provider-models.ts
  • src/lib/system-info.ts
  • src/lib/translations.ts
  • src/lib/webapp-registry.ts
  • src/tests/routes/ai-models/configure.test.ts
  • src/tests/routes/ai-models/status.test.ts
  • src/tests/routes/clawkeep-schedule.test.ts
  • src/tests/routes/files/route.test.ts
  • src/tests/routes/webapps.test.ts
  • src/tests/routes/wifi/connect.test.ts
  • src/tests/routes/wifi/ethernet.test.ts
  • src/tests/unit/chat-reasoning.test.ts
  • src/tests/unit/clawbox-mcp-browser-guidance.test.ts
  • src/tests/unit/clawkeep-persistence.test.ts
  • src/tests/unit/clawkeep-schedule.test.ts
  • src/tests/unit/code-projects.test.ts
  • src/tests/unit/openclaw-config.test.ts
  • src/tests/unit/provider-models.test.ts
  • src/tests/unit/webapp-registry.test.ts
✨ Finishing Touches
πŸ§ͺ Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch beta

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❀️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions

github-actions Bot commented Jun 9, 2026

Copy link
Copy Markdown

CI Summary

βœ… Tests

  • Result: passed
  • View run
  • Coverage: statements 69.46%, branches 59.38%, functions 64.47%, lines 71.52%

βœ… E2E

βœ… E2E Install

@KrasimirKralev
KrasimirKralev merged commit 27fd2d0 into main Jun 9, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants