← Back to PRs

#23708: fix(gateway): auto-approve scope upgrades for loopback clients

by widingmarcus-cyber open 2026-02-22 16:22 View on GitHub →
gateway size: XS trusted-contributor
## Summary Fixes subagent sessions failing with `pairing required` error when spawned on a loopback gateway. ## Problem When a device previously paired with a narrower scope (e.g. `operator.read`) reconnects from localhost requesting broader scopes (e.g. `operator.admin`), the gateway triggers the full pairing flow — requiring manual approval even though the client is local and trusted. This breaks subagent sessions because `sessions.patch` requires `operator.admin` scope, but the initial `gateway-client` connection was paired with only `operator.read`. The scope upgrade triggers `requirePairing("scope-upgrade")` which rejects the connection with `1008: pairing required`. ## Root Cause The silent auto-approve check in `message-handler.ts` only applied to `not-paired` reason: ```ts silent: isLocalClient && reason === "not-paired", ``` Scope upgrades from local clients were treated identically to remote clients — requiring explicit manual approval. ## Fix Extend the silent auto-approve to include `scope-upgrade` for local clients: ```ts silent: isLocalClient && (reason === "not-paired" || reason === "scope-upgrade"), ``` **Role upgrades** (e.g. `operator` → `node`) still require explicit approval regardless of client location, preserving the security boundary between different trust levels. ## Testing - Updated existing `requires pairing for scope upgrades` test → `auto-approves scope upgrades for local clients` - Updated legacy metadata test to verify auto-approval works for devices missing scope metadata - All 36 auth tests pass Fixes #23661 <!-- greptile_comment --> <h3>Greptile Summary</h3> Extends silent auto-approval for loopback clients from just initial pairing (`not-paired`) to also include scope upgrades (`scope-upgrade`). This fixes subagent sessions that previously failed with "pairing required" errors when escalating from `operator.read` to `operator.admin`. The change modifies the condition in `message-handler.ts:641` from: ```ts silent: isLocalClient && reason === "not-paired" ``` to: ```ts silent: isLocalClient && (reason === "not-paired" || reason === "scope-upgrade") ``` Key security properties preserved: - Only applies to loopback clients verified by `isLocalDirectRequest()` (checks IP is loopback AND host is localhost/127.0.0.1/::1/ts.net AND no untrusted proxy headers) - Role upgrades (`operator` → `node`) still require manual approval regardless of client location - Scope validation still enforced through `roleScopesAllow()` before pairing Tests updated to verify auto-approval works for both normal scope upgrades and legacy metadata (devices paired before scopes field existed). <h3>Confidence Score: 5/5</h3> - Safe to merge - targeted fix with preserved security boundaries - The change is minimal (one line), well-tested (36 auth tests pass), and maintains strong security invariants. The `isLocalClient` check requires both loopback IP AND local hostname AND absence of untrusted proxy headers. Role upgrades still require manual approval, preserving the security boundary between operator and node roles. The fix directly addresses the documented issue without introducing new attack surface. - No files require special attention <sub>Last reviewed commit: 41a0aff</sub> <!-- greptile_other_comments_section --> <!-- /greptile_comment -->

Most Similar PRs