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 <noreply@anthropic.com>
This commit is contained in:
YuzuruS
2026-02-21 18:59:42 +09:00
committed by Ayaan Zaidi
parent b77e53da67
commit 3f00f0cbe8
7 changed files with 26 additions and 6 deletions

View File

@@ -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 {

View File

@@ -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"),

View File

@@ -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: [:],

View File

@@ -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();

View File

@@ -13,6 +13,8 @@ export type OperatorScope =
export const CLI_DEFAULT_OPERATOR_SCOPES: OperatorScope[] = [
ADMIN_SCOPE,
READ_SCOPE,
WRITE_SCOPE,
APPROVALS_SCOPE,
PAIRING_SCOPE,
];

View File

@@ -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,

View File

@@ -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<ReturnType<typeof loadOrCreateDeviceIdentity>> | null = null;
let canFallbackToShared = false;