fix(browser): require admin scope for browser request

Co-authored-by: RichardCao <RichardCao@users.noreply.github.com>
This commit is contained in:
Peter Steinberger
2026-04-25 05:12:04 +01:00
parent b84e57fca3
commit 6602092a40
5 changed files with 32 additions and 5 deletions

View File

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

View File

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

View File

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

View File

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

View File

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