From 3f00f0cbe8996bc6562b82670235e9ec08ff9482 Mon Sep 17 00:00:00 2001 From: YuzuruS Date: Sat, 21 Feb 2026 18:59:42 +0900 Subject: [PATCH] fix: add operator.read and operator.write to default CLI scopes The 2026.2.19-2 release tightened scope enforcement on the gateway handshake, but the default CLI operator scopes only included admin, approvals, and pairing. Cron announce delivery and sub-agent result delivery use methods gated behind operator.write (e.g. "send", "poll"), causing a scope-upgrade rejection: `gateway closed (1008): pairing required`. Add operator.read and operator.write to the default scope set across all runtime bundles (Node.js, browser Control UI, macOS CLI, OpenClawKit). Fixes #21787 Co-Authored-By: Claude Opus 4.6 --- apps/macos/Sources/OpenClawMacCLI/ConnectCommand.swift | 2 +- apps/macos/Sources/OpenClawMacCLI/WizardCommand.swift | 2 +- .../OpenClawKit/Sources/OpenClawKit/GatewayChannel.swift | 2 +- src/gateway/call.test.ts | 8 +++++++- src/gateway/method-scopes.ts | 2 ++ src/gateway/server.auth.e2e.test.ts | 8 +++++++- ui/src/ui/gateway.ts | 8 +++++++- 7 files changed, 26 insertions(+), 6 deletions(-) diff --git a/apps/macos/Sources/OpenClawMacCLI/ConnectCommand.swift b/apps/macos/Sources/OpenClawMacCLI/ConnectCommand.swift index 0989164a01e6..d2cf9a5e2dd4 100644 --- a/apps/macos/Sources/OpenClawMacCLI/ConnectCommand.swift +++ b/apps/macos/Sources/OpenClawMacCLI/ConnectCommand.swift @@ -15,7 +15,7 @@ struct ConnectOptions { var clientMode: String = "ui" var displayName: String? var role: String = "operator" - var scopes: [String] = ["operator.admin", "operator.approvals", "operator.pairing"] + var scopes: [String] = ["operator.admin", "operator.read", "operator.write", "operator.approvals", "operator.pairing"] var help: Bool = false static func parse(_ args: [String]) -> ConnectOptions { diff --git a/apps/macos/Sources/OpenClawMacCLI/WizardCommand.swift b/apps/macos/Sources/OpenClawMacCLI/WizardCommand.swift index 2d36bac3c490..ec8c706d3ba7 100644 --- a/apps/macos/Sources/OpenClawMacCLI/WizardCommand.swift +++ b/apps/macos/Sources/OpenClawMacCLI/WizardCommand.swift @@ -251,7 +251,7 @@ actor GatewayWizardClient { let clientMode = "ui" let role = "operator" // Explicit scopes; gateway no longer defaults empty scopes to admin. - let scopes: [String] = ["operator.admin", "operator.approvals", "operator.pairing"] + let scopes: [String] = ["operator.admin", "operator.read", "operator.write", "operator.approvals", "operator.pairing"] let client: [String: ProtoAnyCodable] = [ "id": ProtoAnyCodable(clientId), "displayName": ProtoAnyCodable(Host.current().localizedName ?? "OpenClaw macOS Wizard CLI"), diff --git a/apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayChannel.swift b/apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayChannel.swift index 1aa1b5ae385e..0836d00ec9a6 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayChannel.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayChannel.swift @@ -318,7 +318,7 @@ public actor GatewayChannelActor { let primaryLocale = Locale.preferredLanguages.first ?? Locale.current.identifier let options = self.connectOptions ?? GatewayConnectOptions( role: "operator", - scopes: ["operator.admin", "operator.approvals", "operator.pairing"], + scopes: ["operator.admin", "operator.read", "operator.write", "operator.approvals", "operator.pairing"], caps: [], commands: [], permissions: [:], diff --git a/src/gateway/call.test.ts b/src/gateway/call.test.ts index ab07d3357fa4..2bc4d4ddc778 100644 --- a/src/gateway/call.test.ts +++ b/src/gateway/call.test.ts @@ -206,7 +206,13 @@ describe("callGateway url resolution", () => { { label: "keeps legacy admin scopes for explicit CLI callers", call: () => callGatewayCli({ method: "health" }), - expectedScopes: ["operator.admin", "operator.approvals", "operator.pairing"], + expectedScopes: [ + "operator.admin", + "operator.read", + "operator.write", + "operator.approvals", + "operator.pairing", + ], }, ])("scope selection: $label", async ({ call, expectedScopes }) => { setLocalLoopbackGatewayConfig(); diff --git a/src/gateway/method-scopes.ts b/src/gateway/method-scopes.ts index 1fd9377ead68..20629c3d1c0c 100644 --- a/src/gateway/method-scopes.ts +++ b/src/gateway/method-scopes.ts @@ -13,6 +13,8 @@ export type OperatorScope = export const CLI_DEFAULT_OPERATOR_SCOPES: OperatorScope[] = [ ADMIN_SCOPE, + READ_SCOPE, + WRITE_SCOPE, APPROVALS_SCOPE, PAIRING_SCOPE, ]; diff --git a/src/gateway/server.auth.e2e.test.ts b/src/gateway/server.auth.e2e.test.ts index 20680cb62f31..23b4b29f33bc 100644 --- a/src/gateway/server.auth.e2e.test.ts +++ b/src/gateway/server.auth.e2e.test.ts @@ -873,7 +873,13 @@ describe("gateway server auth/connect", () => { const { randomUUID } = await import("node:crypto"); const os = await import("node:os"); const path = await import("node:path"); - const scopes = ["operator.admin", "operator.approvals", "operator.pairing"]; + const scopes = [ + "operator.admin", + "operator.read", + "operator.write", + "operator.approvals", + "operator.pairing", + ]; const { device } = await createSignedDevice({ token: "secret", scopes, diff --git a/ui/src/ui/gateway.ts b/ui/src/ui/gateway.ts index 27f212c24344..c6c9e824f982 100644 --- a/ui/src/ui/gateway.ts +++ b/ui/src/ui/gateway.ts @@ -145,7 +145,13 @@ export class GatewayBrowserClient { // Gateways may reject this unless gateway.controlUi.allowInsecureAuth is enabled. const isSecureContext = typeof crypto !== "undefined" && !!crypto.subtle; - const scopes = ["operator.admin", "operator.approvals", "operator.pairing"]; + const scopes = [ + "operator.admin", + "operator.read", + "operator.write", + "operator.approvals", + "operator.pairing", + ]; const role = "operator"; let deviceIdentity: Awaited> | null = null; let canFallbackToShared = false;