Skip to content

fix(gateway): allow silent role upgrades for local loopback clients#59092

Merged
obviyus merged 2 commits into
openclaw:mainfrom
openperf:fix/59045-exec-loopback-pairing
Apr 2, 2026
Merged

fix(gateway): allow silent role upgrades for local loopback clients#59092
obviyus merged 2 commits into
openclaw:mainfrom
openperf:fix/59045-exec-loopback-pairing

Conversation

@openperf

@openperf openperf commented Apr 1, 2026

Copy link
Copy Markdown
Member

Summary

  • Problem: v2026.3.31 breaks exec tool connections on local loopback setups (Issue [Bug]: v2026.3.31 breaks exec on local loopback setups #59045). When a local client connects with a new role (e.g., node host connecting after an operator client), the connection is rejected with a 1008 "pairing required" error, even though the device is already paired.
  • Root Cause: The issue stems from two interacting changes in v2026.3.31:
    1. listEffectivePairedDeviceRoles (in src/infra/device-pairing.ts, line 169) incorrectly returns an empty array [] when device.tokens is an empty object {}, failing to fall back to the legacy device.roles field. This happens because the original guard if (device.tokens) is truthy for {}, but listActiveTokenRoles({}) returns undefined (since Object.values({}) yields no entries), resulting in undefined ?? [] = [].
    2. When a role upgrade is triggered (because the requested role is not in the effective roles), shouldAllowSilentLocalPairing (in src/gateway/server/ws-connection/handshake-auth-helpers.ts, line 58) does not support the role-upgrade reason. This prevents local loopback clients from automatically approving the role upgrade via silent pairing, causing a 1008 close.
  • Fix:
    1. Updated listEffectivePairedDeviceRoles to fall back to legacy role fields (device.roles, device.role) when the tokens map is present but has no entries (empty object {}). When token entries exist but are all revoked, the function still returns [] to preserve the security invariant that revoked tokens permanently remove role access.
    2. Updated shouldAllowSilentLocalPairing to explicitly allow the role-upgrade reason for local clients, ensuring that loopback connections can seamlessly acquire new roles without manual intervention.
  • What changed:
    • src/infra/device-pairing.ts: Fixed listEffectivePairedDeviceRoles fallback logic to distinguish empty tokens map from revoked tokens.
    • src/infra/device-pairing.test.ts: Added regression test for the empty tokens map fallback scenario.
    • src/gateway/server/ws-connection/handshake-auth-helpers.ts: Added role-upgrade to the allowed reasons in shouldAllowSilentLocalPairing.
    • src/gateway/server/ws-connection/handshake-auth-helpers.test.ts: Updated existing test and added new test for silent role-upgrade boundary (local vs remote).
  • What did NOT change (scope boundary):
    • Remote client pairing logic remains unchanged — silent role upgrades are still rejected for non-local clients.
    • The security property that revoked tokens permanently remove role access is preserved — the existing test "derives effective roles from active tokens instead of sticky historical roles" continues to pass unchanged.
    • Scope upgrade logic (reason === "scope-upgrade" is force-set to silent: false in requirePairing) is untouched.
    • General authentication flows, bootstrap token handling, and metadata-upgrade pairing are not affected.

Reproduction

  1. Start a local OpenClaw instance on main or v2026.3.31.
  2. Connect a local client (e.g., CLI) with the operator role to establish initial pairing.
  3. Attempt to use the exec tool or connect a node host from the same local loopback address.
  4. Observe the connection being rejected with a 1008 "pairing required" error.
  5. Apply this patch and repeat steps 1-3; the connection should succeed silently.

Risk / Mitigation

  • Risk: Allowing silent role upgrades for local clients could theoretically allow a compromised local process to escalate its privileges to other roles without user interaction.
  • Mitigation: This risk is mitigated by the existing isLocalClient guard, which restricts this behavior strictly to loopback connections (127.0.0.1 / ::1). Remote connections still require explicit approval for role upgrades. Additionally, the fix in listEffectivePairedDeviceRoles preserves the security invariant that explicitly revoked tokens cannot be bypassed — only empty token maps (from fresh or legacy pairing records) trigger the fallback. Comprehensive unit tests have been added to verify both the positive path (local role-upgrade succeeds) and the negative boundary (remote role-upgrade is rejected; revoked tokens remain ineffective).

Change Type (select all)

  • Bug fix

Scope (select all touched areas)

  • Gateway
  • Infra

Linked Issue/PR

Fixes #59045

@openperf
openperf requested a review from a team as a code owner April 1, 2026 15:06
@openclaw-barnacle openclaw-barnacle Bot added gateway Gateway runtime size: S labels Apr 1, 2026
@greptile-apps

greptile-apps Bot commented Apr 1, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a regression from v2026.3.31 that broke exec tool connections on local loopback setups (issue #59045). It addresses two interacting bugs: an incorrect empty-tokens fallback in listEffectivePairedDeviceRoles, and a missing role-upgrade case in shouldAllowSilentLocalPairing.

Key changes:

  • src/infra/device-pairing.ts: listEffectivePairedDeviceRoles now correctly distinguishes between an empty tokens map ({}, fall back to legacy role fields) and a non-empty map with all tokens revoked (authoritative revocation, return []). The old guard if (device.tokens) was truthy for {}, causing listActiveTokenRoles({})undefined to collapse to [] and discard legacy roles.
  • src/gateway/server/ws-connection/handshake-auth-helpers.ts: shouldAllowSilentLocalPairing now accepts \"role-upgrade\" as an allowed reason, enabling loopback clients to acquire new roles without manual approval (scoped to isLocalClient only; remote clients remain unchanged).
  • Tests: regression test for empty-tokens fallback added in device-pairing.test.ts; new boundary tests for role-upgrade (local → true, remote → false) and a scope-upgrade positive assertion added in handshake-auth-helpers.test.ts (addressing the prior review thread).

Confidence Score: 5/5

Safe to merge — minimal, targeted changes with strong test coverage and no logic errors found.

Both fixes are logically correct. The listEffectivePairedDeviceRoles rewrite correctly separates the three distinct states (active tokens, all-revoked tokens, absent/empty tokens) and preserves the security invariant for revoked tokens. The shouldAllowSilentLocalPairing addition is appropriately guarded by isLocalClient with no impact on remote clients. Test coverage is thorough: the regression test for empty-tokens fallback, the existing revocation test (unchanged), and the new boundary tests for role-upgrade all pass. The prior review thread concern about the missing scope-upgrade assertion was addressed in a follow-up commit. No remaining P0/P1 findings.

No files require special attention.

Reviews (2): Last reviewed commit: "fix(gateway ): allow silent role upgrade..." | Re-trigger Greptile

Comment thread src/gateway/server/ws-connection/handshake-auth-helpers.test.ts
@openperf

openperf commented Apr 1, 2026

Copy link
Copy Markdown
Member Author

Quick context on the approach and a follow-up on the greptile feedback.

The core issue is a JS truthy trap — {} passes the if (device.tokens) guard in listEffectivePairedDeviceRoles, so freshly paired devices with an empty tokens map get routed into the token-based role path instead of falling back to the legacy device.roles field. The result is an empty role set, which then triggers a role-upgrade pairing request that shouldAllowSilentLocalPairing didn't know how to handle silently. Two things going wrong in sequence, but the first one is the actual root cause introduced in v2026.3.31 — the second was a latent blind spot that just never mattered until now.

I deliberately kept the fix narrow: the Object.keys length check in listEffectivePairedDeviceRoles only kicks in for truly empty maps, so devices with revoked tokens still correctly get [] (no sneaking past a revocation). And the role-upgrade addition to shouldAllowSilentLocalPairing is gated behind the existing isLocalClient check, so remote clients are completely unaffected.

Regarding @greptile-apps's review — good catch on the scope-upgrade assertion gap. The test title promised coverage for three reasons but only asserted two. Pushed a follow-up commit (57460f1) that adds the missing scope-upgrade positive assertion. As greptile noted, this doesn't change runtime behavior today since requirePairing hard-codes silent: false for scope-upgrade anyway, but it keeps the test honest about the helper's own contract.

Ready for review.

@obviyus obviyus self-assigned this Apr 2, 2026
openperf and others added 2 commits April 2, 2026 08:11
When a local loopback client connects with a role not covered by
existing device tokens, listEffectivePairedDeviceRoles incorrectly
returns an empty role set for devices whose tokens map is an empty
object. This triggers a role-upgrade pairing request that
shouldAllowSilentLocalPairing rejects because it does not recognise
the role-upgrade reason.

Fix listEffectivePairedDeviceRoles to fall back to legacy role fields
when the tokens map has no entries, and extend
shouldAllowSilentLocalPairing to accept role-upgrade for local
clients.

Fixes openclaw#59045
@obviyus
obviyus force-pushed the fix/59045-exec-loopback-pairing branch from 57460f1 to e2d08e5 Compare April 2, 2026 02:42

@obviyus obviyus left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Reviewed latest changes; landing now.

@obviyus
obviyus merged commit 51edd30 into openclaw:main Apr 2, 2026
24 of 25 checks passed
@obviyus

obviyus commented Apr 2, 2026

Copy link
Copy Markdown
Contributor

Landed on main.

Thanks @openperf.

steipete pushed a commit to duncanita/openclaw that referenced this pull request Apr 4, 2026
…penperf)

* fix(gateway ): allow silent role upgrades for local loopback clients

When a local loopback client connects with a role not covered by
existing device tokens, listEffectivePairedDeviceRoles incorrectly
returns an empty role set for devices whose tokens map is an empty
object. This triggers a role-upgrade pairing request that
shouldAllowSilentLocalPairing rejects because it does not recognise
the role-upgrade reason.

Fix listEffectivePairedDeviceRoles to fall back to legacy role fields
when the tokens map has no entries, and extend
shouldAllowSilentLocalPairing to accept role-upgrade for local
clients.

Fixes openclaw#59045

* fix: restore local loopback role upgrades (openclaw#59092) (thanks @openperf)

---------

Co-authored-by: Ayaan Zaidi <[email protected]>
lovewanwan pushed a commit to lovewanwan/openclaw that referenced this pull request Apr 28, 2026
…penperf)

* fix(gateway ): allow silent role upgrades for local loopback clients

When a local loopback client connects with a role not covered by
existing device tokens, listEffectivePairedDeviceRoles incorrectly
returns an empty role set for devices whose tokens map is an empty
object. This triggers a role-upgrade pairing request that
shouldAllowSilentLocalPairing rejects because it does not recognise
the role-upgrade reason.

Fix listEffectivePairedDeviceRoles to fall back to legacy role fields
when the tokens map has no entries, and extend
shouldAllowSilentLocalPairing to accept role-upgrade for local
clients.

Fixes openclaw#59045

* fix: restore local loopback role upgrades (openclaw#59092) (thanks @openperf)

---------

Co-authored-by: Ayaan Zaidi <[email protected]>
ogt-redknie pushed a commit to ogt-redknie/OPENX that referenced this pull request May 2, 2026
…penperf)

* fix(gateway ): allow silent role upgrades for local loopback clients

When a local loopback client connects with a role not covered by
existing device tokens, listEffectivePairedDeviceRoles incorrectly
returns an empty role set for devices whose tokens map is an empty
object. This triggers a role-upgrade pairing request that
shouldAllowSilentLocalPairing rejects because it does not recognise
the role-upgrade reason.

Fix listEffectivePairedDeviceRoles to fall back to legacy role fields
when the tokens map has no entries, and extend
shouldAllowSilentLocalPairing to accept role-upgrade for local
clients.

Fixes openclaw#59045

* fix: restore local loopback role upgrades (openclaw#59092) (thanks @openperf)

---------

Co-authored-by: Ayaan Zaidi <[email protected]>
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request May 9, 2026
…penperf)

* fix(gateway ): allow silent role upgrades for local loopback clients

When a local loopback client connects with a role not covered by
existing device tokens, listEffectivePairedDeviceRoles incorrectly
returns an empty role set for devices whose tokens map is an empty
object. This triggers a role-upgrade pairing request that
shouldAllowSilentLocalPairing rejects because it does not recognise
the role-upgrade reason.

Fix listEffectivePairedDeviceRoles to fall back to legacy role fields
when the tokens map has no entries, and extend
shouldAllowSilentLocalPairing to accept role-upgrade for local
clients.

Fixes openclaw#59045

* fix: restore local loopback role upgrades (openclaw#59092) (thanks @openperf)

---------

Co-authored-by: Ayaan Zaidi <[email protected]>
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request May 24, 2026
…penperf)

* fix(gateway ): allow silent role upgrades for local loopback clients

When a local loopback client connects with a role not covered by
existing device tokens, listEffectivePairedDeviceRoles incorrectly
returns an empty role set for devices whose tokens map is an empty
object. This triggers a role-upgrade pairing request that
shouldAllowSilentLocalPairing rejects because it does not recognise
the role-upgrade reason.

Fix listEffectivePairedDeviceRoles to fall back to legacy role fields
when the tokens map has no entries, and extend
shouldAllowSilentLocalPairing to accept role-upgrade for local
clients.

Fixes openclaw#59045

* fix: restore local loopback role upgrades (openclaw#59092) (thanks @openperf)

---------

Co-authored-by: Ayaan Zaidi <[email protected]>
Nachx639 pushed a commit to Nachx639/clawdbot that referenced this pull request Jun 17, 2026
…penperf)

* fix(gateway ): allow silent role upgrades for local loopback clients

When a local loopback client connects with a role not covered by
existing device tokens, listEffectivePairedDeviceRoles incorrectly
returns an empty role set for devices whose tokens map is an empty
object. This triggers a role-upgrade pairing request that
shouldAllowSilentLocalPairing rejects because it does not recognise
the role-upgrade reason.

Fix listEffectivePairedDeviceRoles to fall back to legacy role fields
when the tokens map has no entries, and extend
shouldAllowSilentLocalPairing to accept role-upgrade for local
clients.

Fixes openclaw#59045

* fix: restore local loopback role upgrades (openclaw#59092) (thanks @openperf)

---------

Co-authored-by: Ayaan Zaidi <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

gateway Gateway runtime size: S

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: v2026.3.31 breaks exec on local loopback setups

2 participants