fix(gateway): preserve stored scopes when reconnecting with device token#46032
Conversation
Greptile SummaryThis PR fixes scope loss during gateway reconnection on Windows by reading previously authorized scopes from
Confidence Score: 3/5
Prompt To Fix All With AIThis is a comment left during a code review.
Path: src/gateway/client.ts
Line: 341-342
Comment:
**Empty stored scopes bypass the fallback**
`storedScopes` can be an empty array (`[]`), which is truthy in JavaScript. When `storedScopes = []` and `resolvedDeviceToken` is set, the condition `storedScopes && resolvedDeviceToken` evaluates to truthy, causing `scopes` to be set to `[]` instead of falling back to `this.opts.scopes ?? ["operator.admin"]`.
This can occur because `storeDeviceAuthToken` is called with `scopes: authInfo.scopes ?? []` — if the server's `HelloOk` response doesn't include a `scopes` field, an empty array is persisted. On the next reconnect, `storedScopes = []` and the client sends zero scopes to the server, which may cause the server to reject the connection or grant fewer permissions than expected.
The condition should explicitly guard against the empty array case:
```suggestion
const scopes =
storedScopes && storedScopes.length > 0 && resolvedDeviceToken ? storedScopes : (this.opts.scopes ?? ["operator.admin"]);
```
How can I resolve this? If you propose a fix, please make it concise.Last reviewed commit: 032211c |
src/gateway/client.ts
Outdated
| const scopes = | ||
| storedScopes && resolvedDeviceToken ? storedScopes : (this.opts.scopes ?? ["operator.admin"]); |
There was a problem hiding this comment.
Empty stored scopes bypass the fallback
storedScopes can be an empty array ([]), which is truthy in JavaScript. When storedScopes = [] and resolvedDeviceToken is set, the condition storedScopes && resolvedDeviceToken evaluates to truthy, causing scopes to be set to [] instead of falling back to this.opts.scopes ?? ["operator.admin"].
This can occur because storeDeviceAuthToken is called with scopes: authInfo.scopes ?? [] — if the server's HelloOk response doesn't include a scopes field, an empty array is persisted. On the next reconnect, storedScopes = [] and the client sends zero scopes to the server, which may cause the server to reject the connection or grant fewer permissions than expected.
The condition should explicitly guard against the empty array case:
| const scopes = | |
| storedScopes && resolvedDeviceToken ? storedScopes : (this.opts.scopes ?? ["operator.admin"]); | |
| const scopes = | |
| storedScopes && storedScopes.length > 0 && resolvedDeviceToken ? storedScopes : (this.opts.scopes ?? ["operator.admin"]); |
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/gateway/client.ts
Line: 341-342
Comment:
**Empty stored scopes bypass the fallback**
`storedScopes` can be an empty array (`[]`), which is truthy in JavaScript. When `storedScopes = []` and `resolvedDeviceToken` is set, the condition `storedScopes && resolvedDeviceToken` evaluates to truthy, causing `scopes` to be set to `[]` instead of falling back to `this.opts.scopes ?? ["operator.admin"]`.
This can occur because `storeDeviceAuthToken` is called with `scopes: authInfo.scopes ?? []` — if the server's `HelloOk` response doesn't include a `scopes` field, an empty array is persisted. On the next reconnect, `storedScopes = []` and the client sends zero scopes to the server, which may cause the server to reject the connection or grant fewer permissions than expected.
The condition should explicitly guard against the empty array case:
```suggestion
const scopes =
storedScopes && storedScopes.length > 0 && resolvedDeviceToken ? storedScopes : (this.opts.scopes ?? ["operator.admin"]);
```
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 032211cb8d
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
src/gateway/client.ts
Outdated
| const scopes = | ||
| storedScopes && resolvedDeviceToken ? storedScopes : (this.opts.scopes ?? ["operator.admin"]); |
There was a problem hiding this comment.
Respect explicit scopes when explicit device token is used
This now overrides this.opts.scopes whenever resolvedDeviceToken is set, but resolvedDeviceToken is also set for an explicitly provided deviceToken (not just cached token reuse). In that case, any cached storedScopes from device-auth.json can replace the caller’s requested scopes and cause explicit device-token auth to fail when scopes differ, because server-side verification checks requested scopes against the explicit token’s allowed scopes (verifyDeviceToken enforces this in src/infra/device-pairing.ts). Previously, explicit scopes were honored for explicit device-token connects.
Useful? React with 👍 / 👎.
When the gateway client reconnects using a stored device token, it was defaulting to ["operator.admin"] scopes instead of preserving the previously authorized scopes from the stored token. This caused the operator device token to be regenerated without operator.read scope, breaking status/probe/health commands. This fix: 1. Loads the stored scopes along with the stored token in selectConnectAuth 2. Uses the stored scopes when reconnecting with a valid device token 3. Falls back to explicitly requested scopes or default admin-only scope when no stored scopes exist Fixes openclaw#46000
032211c to
71f78ef
Compare
|
Landed via temp rebase onto main. Gate:
Thanks @caicongyang! |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 71f78efd88
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const scopes = | ||
| usingStoredDeviceToken && storedScopes && storedScopes.length > 0 | ||
| ? storedScopes | ||
| : (this.opts.scopes ?? ["operator.admin"]); |
There was a problem hiding this comment.
Honor caller scopes on cached-token reconnects
This branch replaces this.opts.scopes with storedScopes whenever a cached device token is reused, even when the caller explicitly passed scopes. That regresses the least-privilege behavior used by gateway calls (for example, src/gateway/call.ts passes per-method scopes): a command that requested operator.read can reconnect with broader cached scopes (like admin/write) and run the session with more privilege than requested. The fallback to cached scopes should only happen when no explicit scopes were provided by the caller.
Useful? React with 👍 / 👎.
Description
On Windows, when the local gateway reissues the operator device token (after restart or reconnect), it was losing the
operator.readscope, breakingstatus,probe, andhealthcommands.Root Cause
When the gateway client reconnects using a stored device token, it was defaulting to
["operator.admin"]scopes instead of preserving the previously authorized scopes from the stored token indevice-auth.json. This caused the token to be regenerated with onlyoperator.adminscope.Changes
Modified
src/gateway/client.ts:selectConnectAuth()to also load the stored scopes along with the stored token["operator.admin"]when no stored scopes existThis ensures that when the CLI reconnects to the gateway (e.g., after gateway restart), it preserves the previously authorized scopes including
operator.read,operator.write,operator.approvals, andoperator.pairing.Related Issue
Fixes: #46000