fix: add operator.read and operator.write to default CLI scopes (#22582)

Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 8569fc88c9
Co-authored-by: YuzuruS <1485195+YuzuruS@users.noreply.github.com>
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
Reviewed-by: @obviyus
This commit is contained in:
Yuzuru Suzuki
2026-02-22 20:06:18 +09:00
committed by GitHub
parent b77e53da67
commit 6f7e5f92c3
9 changed files with 43 additions and 6 deletions

View File

@@ -59,6 +59,7 @@ Docs: https://docs.openclaw.ai
- 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.
- Gateway/Pairing: preserve existing approved token scopes when processing repair pairings that omit `scopes`, preventing empty-scope token regressions on reconnecting clients. (#21906) Thanks @paki81.
- Gateway/Scopes: include `operator.read` and `operator.write` in default operator connect scope bundles across CLI, Control UI, and macOS clients so write-scoped announce/sub-agent follow-up calls no longer hit `pairing required` disconnects on loopback gateways. (#22582) thanks @YuzuruS.
- Plugins/CLI: make `openclaw plugins enable` and plugin install/link flows update allowlists via shared plugin-enable policy so enabled plugins are not left disabled by allowlist mismatch. (#23190) Thanks @downwind7clawd-ctrl.
- 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:<id>]]`, `[[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.

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] = defaultOperatorConnectScopes
var help: Bool = false
static func parse(_ args: [String]) -> ConnectOptions {

View File

@@ -0,0 +1,7 @@
let defaultOperatorConnectScopes: [String] = [
"operator.admin",
"operator.read",
"operator.write",
"operator.approvals",
"operator.pairing",
]

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 = defaultOperatorConnectScopes
let client: [String: ProtoAnyCodable] = [
"id": ProtoAnyCodable(clientId),
"displayName": ProtoAnyCodable(Host.current().localizedName ?? "OpenClaw macOS Wizard CLI"),

View File

@@ -127,6 +127,14 @@ private enum ConnectChallengeError: Error {
case timeout
}
private let defaultOperatorConnectScopes: [String] = [
"operator.admin",
"operator.read",
"operator.write",
"operator.approvals",
"operator.pairing",
]
public actor GatewayChannelActor {
private let logger = Logger(subsystem: "ai.openclaw", category: "gateway")
private var task: WebSocketTaskBox?
@@ -318,7 +326,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: defaultOperatorConnectScopes,
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

@@ -61,6 +61,13 @@ export type GatewayBrowserClientOptions = {
// 4008 = application-defined code (browser rejects 1008 "Policy Violation")
const CONNECT_FAILED_CLOSE_CODE = 4008;
const DEFAULT_OPERATOR_CONNECT_SCOPES = [
"operator.admin",
"operator.read",
"operator.write",
"operator.approvals",
"operator.pairing",
];
export class GatewayBrowserClient {
private ws: WebSocket | null = null;
@@ -145,7 +152,7 @@ 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 = DEFAULT_OPERATOR_CONNECT_SCOPES;
const role = "operator";
let deviceIdentity: Awaited<ReturnType<typeof loadOrCreateDeviceIdentity>> | null = null;
let canFallbackToShared = false;