From 55d492b4cd84f08952ea89781d35ce65a46b0d16 Mon Sep 17 00:00:00 2001 From: Vignesh Natarajan Date: Sat, 21 Feb 2026 19:37:04 -0800 Subject: [PATCH] Gateway: allow operator admin scope for pairing and approvals --- CHANGELOG.md | 1 + src/shared/operator-scope-compat.test.ts | 27 ++++++++++++++++++++++++ src/shared/operator-scope-compat.ts | 12 +++++------ 3 files changed, 34 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2487de0f0..3f9d04c03 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ Docs: https://docs.openclaw.ai - Agents/Fallbacks: treat JSON payloads with `type: "api_error"` + `"Internal server error"` as transient failover errors so Anthropic 500-style failures trigger model fallback. (#23193) Thanks @jarvis-lane. - Agents/Diagnostics: include resolved lifecycle error text in `embedded run agent end` warnings so UI/TUI “Connection error” runs expose actionable provider failure reasons in gateway logs. (#23054) Thanks @Raize. - Gateway/Pairing: treat operator.admin pairing tokens as satisfying operator.write requests so legacy devices stop looping through scope-upgrade prompts introduced in 2026.2.19. (#23125, #23006) Thanks @vignesh07. +- Gateway/Pairing: treat `operator.admin` as satisfying other `operator.*` scope checks during device-auth verification so local CLI/TUI sessions stop entering pairing-required loops for pairing/approval-scoped commands. (#22062, #22193, #21191) Thanks @Botaccess, @jhartshorn, and @ctbritt. - Memory/QMD: add optional `memory.qmd.mcporter` search routing so QMD `query/search/vsearch` can run through mcporter keep-alive flows (including multi-collection paths) to reduce cold starts, while keeping searches on agent-scoped QMD state for consistent recall. (#19617) Thanks @nicole-luxe and @vignesh07. - Chat/UI: strip inline reply/audio directive tags (`[[reply_to_current]]`, `[[reply_to:]]`, `[[audio_as_voice]]`) from displayed chat history, live chat event output, and session preview snippets so control tags no longer leak into user-visible surfaces. - BlueBubbles/DM history: restore DM backfill context with account-scoped rolling history, bounded backfill retries, and safer history payload limits. (#20302) Thanks @Ryan-Haines. diff --git a/src/shared/operator-scope-compat.test.ts b/src/shared/operator-scope-compat.test.ts index 166d7b18c..118106736 100644 --- a/src/shared/operator-scope-compat.test.ts +++ b/src/shared/operator-scope-compat.test.ts @@ -43,6 +43,33 @@ describe("roleScopesAllow", () => { ).toBe(true); }); + it("treats operator.approvals/operator.pairing as satisfied by operator.admin", () => { + expect( + roleScopesAllow({ + role: "operator", + requestedScopes: ["operator.approvals"], + allowedScopes: ["operator.admin"], + }), + ).toBe(true); + expect( + roleScopesAllow({ + role: "operator", + requestedScopes: ["operator.pairing"], + allowedScopes: ["operator.admin"], + }), + ).toBe(true); + }); + + it("does not treat operator.admin as satisfying non-operator scopes", () => { + expect( + roleScopesAllow({ + role: "operator", + requestedScopes: ["system.run"], + allowedScopes: ["operator.admin"], + }), + ).toBe(false); + }); + it("uses strict matching for non-operator roles", () => { expect( roleScopesAllow({ diff --git a/src/shared/operator-scope-compat.ts b/src/shared/operator-scope-compat.ts index ac53d7414..4b1d954b7 100644 --- a/src/shared/operator-scope-compat.ts +++ b/src/shared/operator-scope-compat.ts @@ -2,6 +2,7 @@ const OPERATOR_ROLE = "operator"; const OPERATOR_ADMIN_SCOPE = "operator.admin"; const OPERATOR_READ_SCOPE = "operator.read"; const OPERATOR_WRITE_SCOPE = "operator.write"; +const OPERATOR_SCOPE_PREFIX = "operator."; function normalizeScopeList(scopes: readonly string[]): string[] { const out = new Set(); @@ -15,15 +16,14 @@ function normalizeScopeList(scopes: readonly string[]): string[] { } function operatorScopeSatisfied(requestedScope: string, granted: Set): boolean { + if (granted.has(OPERATOR_ADMIN_SCOPE) && requestedScope.startsWith(OPERATOR_SCOPE_PREFIX)) { + return true; + } if (requestedScope === OPERATOR_READ_SCOPE) { - return ( - granted.has(OPERATOR_READ_SCOPE) || - granted.has(OPERATOR_WRITE_SCOPE) || - granted.has(OPERATOR_ADMIN_SCOPE) - ); + return granted.has(OPERATOR_READ_SCOPE) || granted.has(OPERATOR_WRITE_SCOPE); } if (requestedScope === OPERATOR_WRITE_SCOPE) { - return granted.has(OPERATOR_WRITE_SCOPE) || granted.has(OPERATOR_ADMIN_SCOPE); + return granted.has(OPERATOR_WRITE_SCOPE); } return granted.has(requestedScope); }