Compare commits

...

3 Commits

Author SHA1 Message Date
Peter Steinberger
41f021fed7 fix: cover control ui connect scopes (#52104) (thanks @artwalker) 2026-03-22 14:48:16 -07:00
XING
094562e57f fix(control-ui): add missing operator.read and operator.write scopes to connect params
The Control UI websocket connect params declared only admin, approvals,
and pairing scopes, omitting operator.read and operator.write. This
caused the gateway to reject all agent/send RPC calls from the dashboard
webchat with "missing scope: operator.write".

Add the two missing scopes to the connect params array so dashboard
webchat can send messages and read session state. Also update the test
fixture in gateway.node.test.ts to match the new scope list.

Fixes #52087

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 14:20:00 -07:00
XING
6760226dd4 fix(control-ui): add missing operator.read and operator.write scopes to connect params
The Control UI websocket connect params declared only admin, approvals,
and pairing scopes, omitting operator.read and operator.write. This
caused the gateway to reject all agent/send RPC calls from the dashboard
webchat with "missing scope: operator.write".

Add the two missing scopes to the connect params array so dashboard
webchat can send messages and read session state.

Fixes #52087

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 14:20:00 -07:00
3 changed files with 36 additions and 3 deletions

View File

@@ -218,6 +218,7 @@ Docs: https://docs.openclaw.ai
- Tools/image generation: add bundled fal image generation support so `image_generate` can target `fal/*` models with `FAL_KEY`, including single-image edit flows via FLUX image-to-image. Thanks @vincentkoc.
- Messages/polls: treat zero-valued poll params on `message.send` as unset defaults while keeping non-zero poll params on the poll validation path. (#52150) Fixes #52118. Thanks @Bartok9.
- xAI/web search: add missing Grok credential metadata so the bundled provider registration type-checks again. (#49472) thanks @scoootscooob.
- Control UI/webchat: request `operator.read` and `operator.write` in websocket connect params so dashboard sessions stop failing follow-up RPCs with missing operator scope errors. (#52104) Thanks @artwalker.
- Signal/runtime API: re-export `SignalAccountConfig` so Signal account resolution type-checks again. (#49470) Thanks @scoootscooob.
- Google Chat/runtime API: thin the private runtime barrel onto the curated public SDK surface while keeping public Google Chat exports intact. (#49504) Thanks @scoootscooob.
- WhatsApp: stabilize inbound monitor and setup tests (#50007) Thanks @joshavant.

View File

@@ -79,7 +79,7 @@ vi.mock("./device-identity.ts", () => ({
signDevicePayload: signDevicePayloadMock,
}));
const { GatewayBrowserClient } = await import("./gateway.ts");
const { CONTROL_UI_OPERATOR_SCOPES, GatewayBrowserClient } = await import("./gateway.ts");
function createStorageMock(): Storage {
const store = new Map<string, string>();
@@ -143,7 +143,7 @@ describe("GatewayBrowserClient", () => {
deviceId: "device-1",
role: "operator",
token: "stored-device-token",
scopes: ["operator.admin", "operator.approvals", "operator.pairing"],
scopes: [...CONTROL_UI_OPERATOR_SCOPES],
});
});
@@ -152,6 +152,30 @@ describe("GatewayBrowserClient", () => {
vi.unstubAllGlobals();
});
it("requests the full control ui operator scope bundle on connect", async () => {
const client = new GatewayBrowserClient({
url: "ws://127.0.0.1:18789",
token: "shared-auth-token",
});
client.start();
const ws = getLatestWebSocket();
ws.emitOpen();
ws.emitMessage({
type: "event",
event: "connect.challenge",
payload: { nonce: "nonce-1" },
});
await vi.waitFor(() => expect(ws.sent.length).toBeGreaterThan(0));
const connectFrame = JSON.parse(ws.sent.at(-1) ?? "{}") as {
method?: string;
params?: { scopes?: string[] };
};
expect(connectFrame.method).toBe("connect");
expect(connectFrame.params?.scopes).toEqual([...CONTROL_UI_OPERATOR_SCOPES]);
});
it("prefers explicit shared auth over cached device tokens", async () => {
const client = new GatewayBrowserClient({
url: "ws://127.0.0.1:18789",

View File

@@ -36,6 +36,14 @@ export type GatewayErrorInfo = {
details?: unknown;
};
export const CONTROL_UI_OPERATOR_SCOPES = [
"operator.admin",
"operator.read",
"operator.write",
"operator.approvals",
"operator.pairing",
] as const;
export class GatewayRequestError extends Error {
readonly gatewayCode: string;
readonly details?: unknown;
@@ -242,7 +250,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 = [...CONTROL_UI_OPERATOR_SCOPES];
const role = "operator";
const explicitGatewayToken = this.opts.token?.trim() || undefined;
const explicitPassword = this.opts.password?.trim() || undefined;