From 68c18caca06cebdc8eee55b1a73b03e996bc4764 Mon Sep 17 00:00:00 2001 From: Yuval Dinodia <102706514+yetval@users.noreply.github.com> Date: Sat, 11 Jul 2026 12:58:00 -0400 Subject: [PATCH] fix(node-pairing): require operator.admin to approve browser.proxy nodes (#104491) Approving a node whose pairing surface advertises browser.proxy only required operator.write, while invoking browser.proxy already requires operator.admin (server-methods/nodes.ts). resolveNodePairApprovalScopes bumped the approval scope to operator.admin only for NODE_SYSTEM_RUN_COMMANDS, so a write-scoped operator could trust a browser.proxy-capable node that later routes bundled browser-tool traffic. Add NODE_BROWSER_PROXY_COMMAND to the admin-approval set so approval scope matches the invoke-time gate. --- src/infra/node-pairing-authz.test.ts | 7 +++++++ src/infra/node-pairing-authz.ts | 6 ++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/infra/node-pairing-authz.test.ts b/src/infra/node-pairing-authz.test.ts index 83eaebec435c..ef956f775763 100644 --- a/src/infra/node-pairing-authz.test.ts +++ b/src/infra/node-pairing-authz.test.ts @@ -10,6 +10,13 @@ describe("resolveNodePairApprovalScopes", () => { ]); }); + it("requires operator.admin for browser.proxy commands", () => { + expect(resolveNodePairApprovalScopes(["browser.proxy"])).toEqual([ + "operator.pairing", + "operator.admin", + ]); + }); + it("requires operator.write for non-exec commands", () => { expect(resolveNodePairApprovalScopes(["canvas.present"])).toEqual([ "operator.pairing", diff --git a/src/infra/node-pairing-authz.ts b/src/infra/node-pairing-authz.ts index 8ed0a023a9ec..f8fbc2b5a117 100644 --- a/src/infra/node-pairing-authz.ts +++ b/src/infra/node-pairing-authz.ts @@ -1,5 +1,5 @@ // Maps node pairing command declarations to required operator scopes. -import { NODE_SYSTEM_RUN_COMMANDS } from "./node-commands.js"; +import { NODE_BROWSER_PROXY_COMMAND, NODE_SYSTEM_RUN_COMMANDS } from "./node-commands.js"; /** Operator scopes required to approve a pending node pairing surface. */ export type NodeApprovalScope = "operator.pairing" | "operator.write" | "operator.admin"; @@ -8,13 +8,15 @@ const OPERATOR_PAIRING_SCOPE: NodeApprovalScope = "operator.pairing"; const OPERATOR_WRITE_SCOPE: NodeApprovalScope = "operator.write"; const OPERATOR_ADMIN_SCOPE: NodeApprovalScope = "operator.admin"; +const ADMIN_APPROVAL_COMMANDS = [...NODE_SYSTEM_RUN_COMMANDS, NODE_BROWSER_PROXY_COMMAND]; + /** Map declared node commands to the least operator scopes needed for approval. */ export function resolveNodePairApprovalScopes(commands: unknown): NodeApprovalScope[] { const normalized = Array.isArray(commands) ? commands.filter((command): command is string => typeof command === "string") : []; if ( - normalized.some((command) => NODE_SYSTEM_RUN_COMMANDS.some((allowed) => allowed === command)) + normalized.some((command) => ADMIN_APPROVAL_COMMANDS.some((allowed) => allowed === command)) ) { return [OPERATOR_PAIRING_SCOPE, OPERATOR_ADMIN_SCOPE]; }