← Back to PRs

#17425: fix(gateway): auto-approve scope/role upgrades for already-paired devices

by sauerdaniel open 2026-02-15 18:59 View on GitHub →
gateway size: XS
## Problem When a CLI version upgrade introduces new scopes (e.g. `operator.approvals`, `operator.pairing`), already-paired devices are rejected with **"pairing required"** because their paired entry lacks the new scopes. For **LAN-bound gateways** (`gateway.bind: "lan"`), this creates an unrecoverable deadlock: 1. The CLI requests new scopes during the handshake 2. The gateway creates a scope-upgrade pairing request with `silent: false` (non-loopback) 3. The pairing request requires manual approval via `openclaw devices approve` 4. But `openclaw devices approve` itself needs to connect to the gateway… 5. …which also fails with "pairing required" (same scope mismatch) The only workaround is manually editing `~/.openclaw/devices/paired.json` to add the missing scopes — not something users should need to do. ## Root Cause The `requirePairing()` closure in the WebSocket handshake handler sets `silent: isLocalClient`, meaning auto-approval only happens for loopback connections. Scope/role upgrades for already-paired remote devices are treated the same as brand-new device pairing requests, even though the device identity has already been cryptographically verified. ## Fix Set `silent: true` for scope-upgrade and role-upgrade pairing requests when the connecting device is already paired (`_paired != null`). At this point: - The device public key has been verified against the paired record - The device signature has been validated - The device token (if present) has been verified Auto-approving the upgrade is safe because the device is already trusted. This matches the existing behavior for loopback connections and simply extends it to cover the LAN-bound case. **New device pairing** (`reason: "not-paired"`) still requires explicit approval for non-local connections, preserving the existing security model. ## Changes - `src/gateway/server/ws-connection/message-handler.ts`: Compute `silent` based on both `isLocalClient` and whether this is an upgrade for an already-paired device - Added `reason` to the auto-approval log message for observability ## AI-Assisted Yes — implemented and reviewed with AI assistance. ## Testing - CI checks are running for this branch. ## Local Validation ```bash pnpm build && pnpm check && pnpm test ``` Build passes. Lint passes. 4743/4747 tests pass (4 pre-existing env-isolation failures fixed by #16658). ## Local Validation ```bash pnpm build && pnpm check ``` Build passes. Lint passes. Note: 4 pre-existing test failures in `provider-usage.auth.normalizes-keys.test.ts` (this is what PR #16658 fixes). <!-- greptile_comment --> <h3>Greptile Summary</h3> This PR fixes a deadlock for LAN-bound gateways where already-paired devices requesting new scopes (e.g., after a CLI upgrade introduces `operator.approvals` or `operator.pairing`) could not connect because the scope-upgrade pairing request required manual approval — but the approval command itself needed the new scopes to connect. The fix sets `silent: true` (auto-approve) for scope-upgrade and role-upgrade pairing requests when the connecting device is already paired. This is safe because by the time the `requirePairing` closure is reached, the device's public key has been matched against the paired record, its signature has been cryptographically verified, and nonce replay protection is enforced for non-loopback connections. New device pairing (`reason: "not-paired"`) still requires explicit approval for non-local connections, preserving the existing security model. - **Auto-approve logic**: `silent` is now `isLocalClient || (isUpgrade && _paired != null)` where `isUpgrade` checks for `"scope-upgrade"` or `"role-upgrade"` reasons, and `_paired` is only non-null when the device is already in the paired record with a matching public key - **Observability**: The auto-approval log message now includes `reason=` for easier debugging - **No test for the LAN-specific path**: The existing e2e test (`server.auth.e2e.test.ts`) validates scope upgrades but connects via `127.0.0.1`, meaning `isLocalClient` is already `true` — the new `isUpgrade && _paired != null` branch is not exercised by tests <h3>Confidence Score: 4/5</h3> - This PR is safe to merge — the change correctly extends auto-approval to already-verified devices while preserving the security boundary for new device pairing. - Score of 4 reflects that the logic change is correct and well-reasoned: the device identity is fully verified (public key match, cryptographic signature, nonce) before auto-approval triggers. The only gap is the lack of a dedicated test covering the non-loopback (LAN) path — the existing e2e test exercises scope upgrades but via localhost, so it doesn't validate the new `isUpgrade && _paired != null` branch specifically. This is a minor concern, not a blocking issue. - No files require special attention — the single changed file has been thoroughly reviewed and the security model is preserved. <sub>Last reviewed commit: 8674543</sub> <!-- greptile_other_comments_section --> <!-- /greptile_comment -->

Most Similar PRs