#22583: fix(gateway): add operator.write to scope hierarchy (#22574)
size: S
experienced-contributor
Cluster:
Device Pairing and Gateway Fixes
## Summary
- **Bug**: gateway-client enters a "pairing required" loop when connecting with `operator.write` scope but device has `operator.admin` granted
- **Root cause**: `operatorScopeSatisfied()` in `src/shared/operator-scope-compat.ts` only has hierarchy logic for `operator.read`; `operator.write` falls through to strict `granted.has(requestedScope)` which returns false when only `operator.admin` is granted
- **Fix**: add `operator.write` hierarchy check so `operator.admin` satisfies `operator.write` requests
Fixes #22574
## Problem
The operator scope hierarchy was incomplete. The expected hierarchy is `admin > write > read`, but only `read` had upward resolution:
```typescript
// Before: only operator.read checks higher scopes
if (requestedScope === OPERATOR_READ_SCOPE) {
return granted.has(OPERATOR_READ_SCOPE) ||
granted.has(OPERATOR_WRITE_SCOPE) ||
granted.has(OPERATOR_ADMIN_SCOPE);
}
return granted.has(requestedScope); // operator.write falls through here
```
**Before fix:**
Input: `roleScopesAllow({ role: "operator", requestedScopes: ["operator.write"], allowedScopes: ["operator.admin"] })`
Output: `false`
This caused gateway-client to enter a pairing loop: it requests `operator.write`, the device has `operator.admin`, but the scope check fails, triggering `requirePairing("scope-upgrade")` on every connection attempt.
## Changes
- `src/shared/operator-scope-compat.ts` — add hierarchy check for `operator.write` (satisfied by `operator.write` or `operator.admin`)
- `src/shared/operator-scope-compat.test.ts` — replace buggy assertion with comprehensive hierarchy tests for `operator.write` and `operator.admin`
- `CHANGELOG.md` — add fix entry
**After fix:**
Input: `roleScopesAllow({ role: "operator", requestedScopes: ["operator.write"], allowedScopes: ["operator.admin"] })`
Output: `true`
## Test plan
- [x] New test: `operator.write` satisfied by `operator.write` and `operator.admin`, rejected by `operator.read`
- [x] New test: `operator.admin` only satisfied by exact `operator.admin` match
- [x] All 4 existing tests pass
- [x] Format check passes
## Effect on User Experience
**Before:** gateway-client with `operator.write` scope enters infinite "pairing required" loop when device has `operator.admin` granted, blocking `sessions_spawn` with (1008) after fresh installs or restarts.
**After:** `operator.admin` correctly satisfies `operator.write` requests, gateway-client connects without requiring re-pairing.
<!-- greptile_comment -->
<h3>Greptile Summary</h3>
Fixes an incomplete operator scope hierarchy in `operatorScopeSatisfied()` where `operator.write` requests fell through to strict equality matching instead of checking upward in the `admin > write > read` hierarchy. This caused gateway-client to enter a pairing loop when connecting with `operator.write` scope against a device that had `operator.admin` granted.
- Added `operator.write` hierarchy check in `src/shared/operator-scope-compat.ts` so `operator.admin` satisfies `operator.write` requests, matching the existing pattern for `operator.read`
- Replaced the test that was asserting the buggy behavior with comprehensive hierarchy tests covering `operator.write` and `operator.admin` scope resolution
- Added changelog entry
<h3>Confidence Score: 5/5</h3>
- This PR is safe to merge — it's a minimal, well-scoped bug fix with comprehensive tests.
- The change is a 3-line addition to the scope hierarchy function that follows the exact same pattern as the existing `operator.read` check. The fix is consistent with the already-correct `DEVICE_SCOPE_IMPLICATIONS` map in `device-pairing.ts`. The old test was asserting the buggy behavior and has been correctly replaced with comprehensive hierarchy tests. No new security concerns — the change only makes the authorization check more permissive in the intended direction (higher scopes satisfy lower ones).
- No files require special attention.
<sub>Last reviewed commit: 908539a</sub>
<!-- greptile_other_comments_section -->
<!-- /greptile_comment -->
Most Similar PRs
#22666: fix(gateway): operator.admin should imply all operator scopes
by maximveksler · 2026-02-21
90.5%
#21622: fix(gateway): include read/write in CLI default operator scopes
by zerone0x · 2026-02-20
87.4%
#17195: fix: Add operator.read/write scopes to Dashboard auto-pairing
by MisterGuy420 · 2026-02-15
85.3%
#22374: Allow operator.admin to satisfy operator scope compatibility
by AIflow-Labs · 2026-02-21
85.2%
#12802: fix(gateway): default unscoped operator connections to read-only
by yubrew · 2026-02-09
84.8%
#17127: fix(webchat): include operator.read and operator.write in connect s...
by brandonwise · 2026-02-15
84.1%
#23708: fix(gateway): auto-approve scope upgrades for loopback clients
by widingmarcus-cyber · 2026-02-22
83.7%
#23039: fix: subagent announce fails with pairing required due to missing o...
by ascott · 2026-02-21
82.8%
#21664: fix(gateway): require re-pairing for legacy devices that lack scope...
by AI-Reviewer-QS · 2026-02-20
81.6%
#21666: fix(gateway): restrict auto-paired device scopes to safe defaults
by AI-Reviewer-QS · 2026-02-20
81.6%