Skip to content

fix(gateway): preserve per-device auth token + self-heal orphaned config keys - #155

Merged
KrasimirKralev merged 3 commits into
betafrom
fix/gateway-token-and-config-selfheal
May 28, 2026
Merged

KrasimirKralev merged 3 commits into
betafrom
fix/gateway-token-and-config-selfheal

Conversation

@KrasimirKralev

@KrasimirKralev KrasimirKralev commented May 28, 2026

Copy link
Copy Markdown
Contributor

Closes #149, closes #150.

The bug (security)

The gateway auth token gates LAN access to the agent's privileged MCP tools (run_command, file_write, system_power). Two code paths shipped the public literal clawbox — documented in the open-source history (gateway-proxy.ts LEGACY_GATEWAY_TOKEN) — as the live token, so anyone on the same WiFi could connect to the gateway WebSocket and drive the agent, bypassing the wizard login:

  1. gateway-pre-start.sh ran set_if(auth, "token", "clawbox") on every gateway start — clobbering the strong per-device token the configure route already generates, so the rotation never stuck.
  2. clawbox-gateway.service launched with --token clawbox, overriding config at runtime and drifting from the token the Control UI reads from gateway.auth.token (the Control UI gateway bootstrap should use the same token source as the live gateway service #150 source-of-truth mismatch).

Reported by @jamesachurchill.

The fix — config is the single source of truth

The gateway resolves the token as OPENCLAW_GATEWAY_TOKEN ?? gateway.auth.token (verified in the installed core), so:

  • gateway-pre-start.sh preserves a strong token — configure-route random hex, a ${ENV} interpolation, or a SecretRef object with a known env/file/exec key — and only generates a fresh secrets.token_hex(32) when missing or the weak legacy literal.
  • clawbox-gateway.service drops --token; the gateway resolves gateway.auth.token from openclaw.json — the same value gateway-proxy.ts injects into the SPA. One source, no service↔UI drift.
  • install.sh / install-x64.sh seed a random token instead of the literal, only when missing/weak. The install-x64 JS predicate is kept byte-for-byte in lockstep with the python one (both reject arrays, empty/keyless objects, and empty ${}).

Bonus: config-residue self-heal

gateway-pre-start.sh now strips an orphaned agentRuntime key from agents.defaults.models[*] — written by @openclaw/codex >= 2026.5.27 and left behind when the plugin is realigned to the pinned core, which fails strict config validation and bricks the AI provider page until a manual openclaw doctor --fix. This makes affected devices self-heal on the next gateway start (the live customer incident from this week).

Tests

New src/tests/unit/gateway-pre-start-token.test.ts extracts the real predicate from the shipped script and exercises both the predicate and the rotation wiring via python3 (skips gracefully where python3 is absent), including python↔JS parity cases.

Validated on a real Jetson

  • clawbox and empty-dict tokens rotate to 64-hex; strong tokens preserved across restarts (idempotent, no churn)
  • Service runs with no --token; Control UI connects and authenticates against the config token ([ws] webchat connected client=openclaw-control-ui) — proves single-source-of-truth
  • Orphaned agentRuntime stripped, unrelated keys preserved; gateway [gateway] ready, no crash-loop

Scope notes

Test plan

  • Predicate + rotation validated against the real shipped function (python3)
  • python↔JS predicate parity (10 cases)
  • End-to-end on Jetson (rotate / preserve / Control-UI auth / agentRuntime strip)
  • bash -n on all three shell scripts
  • CI (Tests + e2e-install)
  • CodeRabbit review

Summary by CodeRabbit

  • New Features

    • Per-device authentication tokens: generate strong unique tokens when missing or weak; preserve strong tokens.
    • Service startup no longer embeds a shared hard-coded token and relies on resolved per-device tokens.
  • Bug Fixes / Improvements

    • Cleanup of legacy config leftovers to prevent validation issues.
    • Installer and pre-start logic avoid unnecessary token rotation.
  • Tests

    • End-to-end unit tests for token strength detection and rotation behavior.

Review Change Stack

…fig keys

Closes #149, #150.

The gateway auth token gates LAN access to the agent's privileged tools
(run_command / file_write / system_power). Two bugs shipped the public
literal "clawbox" (documented in open-source history) as the live token:

1. gateway-pre-start.sh ran `set_if(auth, "token", "clawbox")` on EVERY
   gateway start, clobbering the strong per-device token the configure
   route generates — so the rotation never stuck.
2. clawbox-gateway.service launched with `--token clawbox`, overriding
   config at runtime and drifting from what the Control UI (which reads
   gateway.auth.token) used — the #150 source-of-truth mismatch.

Fix — config is the single source of truth:
- gateway-pre-start.sh preserves a strong token (configure-route random
  hex, a ${ENV} interpolation, or a SecretRef object with a known
  env/file/exec key) and only generates a fresh secrets.token_hex(32)
  when the value is missing or the weak legacy literal.
- clawbox-gateway.service drops --token; the gateway resolves
  gateway.auth.token from openclaw.json (OPENCLAW_GATEWAY_TOKEN ??
  configuredToken), the same value gateway-proxy.ts injects into the SPA.
- install.sh / install-x64.sh seed a random token instead of the literal,
  only when missing/weak. The install-x64 JS predicate is kept in lockstep
  with the python one (rejects arrays + empty/keyless objects + empty ${}).

Also self-heal a config-validation residue class (the agentRuntime
incident): gateway-pre-start.sh strips an orphaned `agentRuntime` key from
agents.defaults.models[*] — written by @openclaw/codex >= 2026.5.27 and
left behind when the plugin is realigned to the pinned core, which fails
strict validation and bricks the AI provider page until `doctor --fix`.

Adds src/tests/unit/gateway-pre-start-token.test.ts, which extracts the
real predicate from the shipped script and asserts preserve/rotate +
python↔JS parity. Validated end-to-end on a Jetson: clawbox/empty-dict
rotate to 64-hex, strong tokens preserved across restarts, Control UI
authenticates against the config token with no --token flag.
@KrasimirKralev
KrasimirKralev requested a review from a team as a code owner May 28, 2026 20:29
@coderabbitai

coderabbitai Bot commented May 28, 2026

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 0d797ef9-ed80-4292-a5d2-eded9c4b03b7

📥 Commits

Reviewing files that changed from the base of the PR and between 9a69528 and 4fc0ddc.

📒 Files selected for processing (3)
  • install-x64.sh
  • install.sh
  • src/tests/unit/gateway-pre-start-token.test.ts

📝 Walkthrough

Walkthrough

Service ExecStart no longer embeds --token clawbox. Installers seed per-device 32-byte hex tokens when missing/weak. The gateway pre-start script classifies tokens (env refs, SecretRef objects, or strings ≥32 bytes as strong), preserves strong tokens, rotates weak ones, and removes stale agentRuntime keys. Tests validate the predicate and rotation.

Changes

Gateway token lifecycle: from hardcoded literal to per-device strong-token preservation

Layer / File(s) Summary
Service configuration and token resolution architecture
config/clawbox-gateway.service
ExecStart removes --token clawbox and adds comments explaining token resolution via pre-start and proxy injection.
Token strength validation predicate
scripts/gateway-pre-start.sh
Adds Python is_strong_gateway_token() that treats ${...} interpolations, SecretRef-like objects, and non-legacy strings ≥32 bytes as strong; legacy "clawbox" and short strings are weak.
Pre-start reconciliation and cleanup
scripts/gateway-pre-start.sh
Replaces unconditional token assignment with conditional generation based on the strength predicate and adds a migration to remove agentRuntime from agents.defaults.models entries.
Installer token seeding and Node config update
install-x64.sh, install.sh
Installers and embedded Node config now preserve strong tokens and only generate a new 32-byte hex token when the current token is missing, weak, or legacy.
Unit test verification of token policy
src/tests/unit/gateway-pre-start-token.test.ts
Vitest tests extract and evaluate the token-policy block with Python to assert strong/weak classification and confirm rotation behavior (weak→new 64-hex token, strong→preserved).

Sequence Diagram

sequenceDiagram
  participant SystemD as systemd
  participant PreStart as gateway-pre-start.sh
  participant Config as openclaw.json
  participant Installer as installer scripts
  participant Gateway as gateway process
  SystemD->>PreStart: run pre-start script
  PreStart->>Config: read gateway.auth.token
  PreStart->>PreStart: is_strong_gateway_token(token)?
  alt token is weak or missing
    PreStart->>PreStart: generate new 32-byte hex token
    PreStart->>Config: write new token
  else token is strong
    PreStart->>Config: leave token unchanged
  end
  Installer->>Config: seed token when installing (if missing/weak)
  PreStart-->>SystemD: pre-start complete
  SystemD->>Gateway: exec openclaw gateway (--allow-unconfigured --bind lan)
  Gateway->>Config: read resolved token at runtime
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • ID-Robots/clawbox#112: Introduces per-device token generation paths and related gateway token rotation/config-reset logic.

Suggested reviewers

  • yalexx

Poem

🐰 I hopped through config, token in my paw,

No more "clawbox" left to gnaw,
Strong seeds planted, weak ones replaced,
Pre-start guards the secret place,
Now each device keeps its own small law.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely summarizes the main changes: preserving per-device auth tokens and adding self-healing for orphaned config keys.
Description check ✅ Passed The description provides comprehensive context covering root causes, fixes, validation, and testing approach; fully structured per template with summary, type of change, testing, and checklist.
Linked Issues check ✅ Passed Changes comprehensively address #149 (preserve secret-managed tokens, drop literal token, establish strong token predicate) and #150 (single source of truth via gateway.auth.token from config).
Out of Scope Changes check ✅ Passed All changes directly serve the stated objectives: token preservation, config-as-source-of-truth, config self-healing, tests, and validation on real hardware.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/gateway-token-and-config-selfheal

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 May 28, 2026

Copy link
Copy Markdown

CI Summary

✅ Tests

  • Result: passed
  • View run
  • Coverage: statements 70.94%, branches 60.32%, functions 66.78%, lines 73.12%

✅ E2E

✅ E2E Install

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with 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.

Inline comments:
In `@install-x64.sh`:
- Around line 355-360: The EXISTING_GW_TOKEN check in install-x64.sh (the block
that reads EXISTING_GW_TOKEN via as_user "$OPENCLAW_BIN" and may generate
GW_TOKEN) must skip rotating tokens that are literal interpolation placeholders
like ${VAR}; update the conditional that currently tests empty/"clawbox"/length
to also detect and preserve strings matching the ${...} pattern (e.g. use a bash
regex check for ^\$\{.+\}$) so that if EXISTING_GW_TOKEN is an interpolation
token you do not overwrite it with a new random GW_TOKEN before the later
Node.js check runs.

In `@install.sh`:
- Around line 978-989: The current predicate that checks EXISTING_GW_TOKEN only
treats empty, literal "clawbox", or length <32 as weak and will rotate
interpolation tokens like ${GATEWAY_TOKEN}; update the condition used where
EXISTING_GW_TOKEN is evaluated so it treats ${...} style tokens as valid/strong
(e.g. add a test for tokens matching the interpolation pattern like starting
with '${' and ending with '}' or a regex /^\$\{[^}]+\}$/) before deciding to
generate GW_TOKEN and call as_clawbox config set gateway.auth.token; keep the
existing GW_TOKEN generation and as_clawbox calls intact but skip them when
EXISTING_GW_TOKEN is an interpolation token.

In `@src/tests/unit/gateway-pre-start-token.test.ts`:
- Around line 82-87: Add tests covering SecretRef objects with "file" and "exec"
keys to match the predicate logic in classify (used by
scripts/gateway-pre-start.sh). Specifically, add test cases that call classify({
file: "some/path" }) and classify({ exec: "some-command" }) and assert they
return "strong" (mirroring the existing env test), so removal of accepted keys
"file" or "exec" will be caught; reference the classify function and the
SecretRef shape when adding these expectations.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: c6040842-821e-4003-a3d4-77365de37670

📥 Commits

Reviewing files that changed from the base of the PR and between e2e3b1a and 9a69528.

📒 Files selected for processing (5)
  • config/clawbox-gateway.service
  • install-x64.sh
  • install.sh
  • scripts/gateway-pre-start.sh
  • src/tests/unit/gateway-pre-start-token.test.ts

Comment thread install-x64.sh
Comment thread install.sh
Comment thread src/tests/unit/gateway-pre-start-token.test.ts
…t file/exec SecretRefs

Address CodeRabbit review on #155:
- install.sh / install-x64.sh: the bash seed predicate only treated
  empty / "clawbox" / <32-char as weak, so a `${ENV}` interpolation
  token (e.g. ${OPENCLAW_GATEWAY_TOKEN}, 16 chars) was rotated to a
  random value — clobbering an externally-managed token before the
  later checks run. Add a `^\$\{.+\}$` regex branch so interpolation
  tokens are preserved, matching is_strong_gateway_token in
  gateway-pre-start.sh and the JS predicate in install-x64.sh.
- test: cover SecretRef objects with `file` and `exec` keys (not just
  `env`) so dropping an accepted key from the predicate is caught.
The EXISTING_GW_TOKEN read I added runs `openclaw config get
gateway.auth.token`, which exits non-zero on a fresh install (the key
doesn't exist yet). Under `set -euo pipefail` the bare assignment
propagated that non-zero status and aborted install.sh right after
setting gateway.auth.mode — bricking first-boot install. The e2e-install
harness caught it ("install.sh did not finish within 2400000ms").

Append `|| true` so the probe never aborts the installer; an empty
result correctly falls through to seeding a fresh per-device token.
Same fix in install-x64.sh. Reproduced the abort and verified the fix
under set -euo pipefail.
@coderabbitai

coderabbitai Bot commented May 28, 2026

Copy link
Copy Markdown

Actionable comments posted: 0

@KrasimirKralev
KrasimirKralev merged commit dc5fc5b into beta May 28, 2026
7 checks passed
KrasimirKralev added a commit that referenced this pull request May 29, 2026
The chat fetched /setup-api/gateway/ws-config without cache:'no-store', and the
route set no Cache-Control. When the per-device gateway token is regenerated
(reseed, settings change, post-update), a cached ws-config response replays the
OLD token on every reconnect, so the gateway rejects it with 'token mismatch'
indefinitely — only a hard reload recovers. Add cache:'no-store' to the fetch
and Cache-Control:no-store on the route so reconnects always pick up the current
token. This matters for the #155 token-hardening rollout: open chat tabs would
otherwise get stuck after the token changes on update.
@KrasimirKralev
KrasimirKralev deleted the fix/gateway-token-and-config-selfheal branch June 3, 2026 17:21
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.

1 participant