From 6602092a40b6b489ef89e2ccb29125579acf2cdd Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 25 Apr 2026 05:12:04 +0100 Subject: [PATCH] fix(browser): require admin scope for browser request Co-authored-by: RichardCao --- CHANGELOG.md | 1 + extensions/browser/index.test.ts | 11 ++++++++++ extensions/browser/plugin-registration.ts | 2 +- src/gateway/method-scopes.test.ts | 21 ++++++++++++++++--- .../helpers/browser-bundled-plugin-fixture.ts | 2 +- 5 files changed, 32 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d5787104b8b..deff5a5a361 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -70,6 +70,7 @@ Docs: https://docs.openclaw.ai ### Fixes - Subagents: stop stale unended runs from counting as active or pending forever, while preserving restart-aborted recovery for recoverable child sessions. Fixes #71252. Thanks @hclsys. +- Browser/security: require `operator.admin` for the `browser.request` gateway method, matching the host/browser-node control authority exposed by that route. Thanks @RichardCao. - Reply media: allow sandboxed replies to deliver OpenClaw-managed `media/outbound` and `media/tool-*` attachments without treating them as sandbox escapes, while keeping alias-escape checks on the managed media root. Fixes #71138. Thanks @mayor686, @truffle-dev, and @neeravmakwana. - CLI/agent: keep `openclaw agent --json` stdout reserved for the JSON response by routing gateway, plugin, and embedded-fallback diagnostics to stderr before execution starts. Fixes #71319. - Agents/Gemini: retry reasoning-only, empty, and planning-only Gemini turns instead of letting sessions silently stall. Fixes #71074. (#71362) Thanks @neeravmakwana. diff --git a/extensions/browser/index.test.ts b/extensions/browser/index.test.ts index a20fa9b9928..a69ed193164 100644 --- a/extensions/browser/index.test.ts +++ b/extensions/browser/index.test.ts @@ -118,6 +118,17 @@ describe("browser plugin", () => { }); }); + it("registers browser.request as an admin gateway method", () => { + const { api, registerGatewayMethod } = createApi(); + registerBrowserPlugin(api); + + expect(registerGatewayMethod).toHaveBeenCalledWith( + "browser.request", + runtimeApiMocks.handleBrowserGatewayRequest, + { scope: "operator.admin" }, + ); + }); + it("declares setup auto-enable reasons for browser config surfaces", () => { const probe = registerBrowserAutoEnableProbe(); diff --git a/extensions/browser/plugin-registration.ts b/extensions/browser/plugin-registration.ts index 2386bbf57fc..f441978bf83 100644 --- a/extensions/browser/plugin-registration.ts +++ b/extensions/browser/plugin-registration.ts @@ -34,7 +34,7 @@ export function registerBrowserPlugin(api: OpenClawPluginApi) { })) as OpenClawPluginToolFactory); api.registerCli(({ program }) => registerBrowserCli(program), { commands: ["browser"] }); api.registerGatewayMethod("browser.request", handleBrowserGatewayRequest, { - scope: "operator.write", + scope: "operator.admin", }); api.registerService(createBrowserPluginService()); } diff --git a/src/gateway/method-scopes.test.ts b/src/gateway/method-scopes.test.ts index afba94d8f8d..f64f6d94cca 100644 --- a/src/gateway/method-scopes.test.ts +++ b/src/gateway/method-scopes.test.ts @@ -11,7 +11,10 @@ import { coreGatewayHandlers } from "./server-methods.js"; const RESERVED_ADMIN_PLUGIN_METHOD = "config.plugin.inspect"; -function setPluginGatewayMethodScope(method: string, scope: "operator.read" | "operator.write") { +function setPluginGatewayMethodScope( + method: string, + scope: "operator.read" | "operator.write" | "operator.admin", +) { const registry = createEmptyPluginRegistry(); registry.gatewayMethodScopes = { [method]: scope, @@ -54,12 +57,12 @@ describe("method scope resolution", () => { it("reads plugin-registered gateway method scopes from the active plugin registry", () => { const registry = createEmptyPluginRegistry(); registry.gatewayMethodScopes = { - "browser.request": "operator.write", + "browser.request": "operator.admin", }; setActivePluginRegistry(registry); expect(resolveLeastPrivilegeOperatorScopesForMethod("browser.request")).toEqual([ - "operator.write", + "operator.admin", ]); }); @@ -89,6 +92,18 @@ describe("operator scope authorization", () => { }); }); + it("requires admin for browser.request", () => { + setPluginGatewayMethodScope("browser.request", "operator.admin"); + + expect(authorizeOperatorScopesForMethod("browser.request", ["operator.write"])).toEqual({ + allowed: false, + missingScope: "operator.admin", + }); + expect(authorizeOperatorScopesForMethod("browser.request", ["operator.admin"])).toEqual({ + allowed: true, + }); + }); + it("requires pairing scope for node pairing approvals", () => { expect(authorizeOperatorScopesForMethod("node.pair.approve", ["operator.pairing"])).toEqual({ allowed: true, diff --git a/test/helpers/browser-bundled-plugin-fixture.ts b/test/helpers/browser-bundled-plugin-fixture.ts index 16eccd37299..ce333d44d7d 100644 --- a/test/helpers/browser-bundled-plugin-fixture.ts +++ b/test/helpers/browser-bundled-plugin-fixture.ts @@ -43,7 +43,7 @@ const BROWSER_FIXTURE_ENTRY = `module.exports = { program.command("browser"); }, { commands: ["browser"] }); api.registerGatewayMethod("browser.request", async () => ({ ok: true }), { - scope: "operator.write", + scope: "operator.admin", }); api.registerService({ id: "browser-control",