fix(node-pairing): require operator.admin to approve browser.proxy nodes#104491
Conversation
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.
|
Codex review: needs maintainer review before merge. Reviewed July 11, 2026, 9:39 AM ET / 13:39 UTC. Summary PR surface: Source +2, Tests +7. Total +9 across 2 files. Reproducibility: yes. Current main maps Review metrics: 1 noteworthy metric.
Root-cause cluster Members:
Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything. Merge readiness Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch. Rank-up moves:
Risk before merge
Maintainer options:
Next step before merge
Security Review detailsBest possible solution: Land the focused resolver change while explicitly accepting the write-to-admin approval upgrade impact, keeping the shared browser command constant as the command identity and the existing resolver as the single approval-policy owner. Do we have a high-confidence way to reproduce the issue? Yes. Current main maps Is this the best way to solve the issue? Yes. Updating the existing approval-scope resolver with the shared browser command constant is the narrowest maintainable fix and covers both approval entry points without adding another authorization path. AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against fa77fe10d526. Label changesLabel changes:
Label justifications:
Evidence reviewedPR surface: Source +2, Tests +7. Total +9 across 2 files. View PR surface stats
What I checked:
Likely related people:
What the crustacean ranks mean
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics. How this review workflow works
|
|
Merged via squash.
|
…des (#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. (cherry picked from commit 68c18ca) (cherry picked from commit f902110)
…des (openclaw#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.
What Problem This Solves
Approving a node whose pairing surface advertises the
browser.proxycapability only requiresoperator.write, even though directly invokingbrowser.proxyrequiresoperator.admin. Alower-privileged
operator.writeoperator can therefore approve (trust) abrowser.proxy-capablenode. Because the bundled browser tool auto-routes its traffic to a trusted paired node, that
write-scoped approval is the one-time trust decision that lets a node become the target for
admin-level browser traffic (page content, cookies, credentials).
Root cause:
resolveNodePairApprovalScopesbumps the required approval scope tooperator.adminonly when a declared command is in
NODE_SYSTEM_RUN_COMMANDS.browser.proxy(
NODE_BROWSER_PROXY_COMMAND) is not in that set, so abrowser.proxysurface falls into thegeneric branch and returns
[operator.pairing, operator.write].src/infra/node-pairing-authz.ts(before):The invoke path already gates
browser.proxyatoperator.admin(
src/gateway/server-methods/nodes.ts):PR #85916 added that invoke-time admin gate but never updated the sibling approval-scope
resolver, leaving the two gates inconsistent.
Why This Change Was Made
Add
browser.proxyto the command set that triggers theoperator.adminapproval bump, soapproval is at least as strict as invocation.
src/infra/node-pairing-authz.ts(after):Both constants already live in
src/infra/node-commands.tsand are reused, not re-declared.resolveNodePairApprovalScopesis the single owner of the approval-scope mapping; both callers(
approveNodePairinginsrc/infra/node-pairing.tsandnode.pair.approveinsrc/gateway/server-methods/nodes.ts) route their scope check through it, so one change coversevery approval path. The only command gated at
operator.adminon the invoke side isbrowser.proxy;system.run.*was already covered, so after this change the approval-scopeadmin set is at least as strict as the invoke-time admin gate for every command.
Note on the internal browser dispatch: the gateway
browser.requestmethod(
extensions/browser/src/gateway/browser-request.ts) is registered withscope: operator.admin(
extensions/browser/src/browser-gateway-contract.ts) and that scope is enforced atauthorizeGatewayMethodbefore the handler runs, so its directnodeRegistry.invokecall isnot an authorization bypass and is intentionally out of scope here. This change only corrects the
approval-scope asymmetry.
User Impact
A non-admin
operator.writeoperator can no longer approve a node advertisingbrowser.proxy;that approval now requires
operator.admin, matching the scope already required to invokebrowser.proxy. Existing admin approvals and all non-browser, non-exec surface approvals areunaffected.
Evidence
node scripts/run-vitest.mjs src/infra/node-pairing-authz.test.ts- 5 passed. The newassertion fails on pristine main (returns
[operator.pairing, operator.write]) and passeswith the patch.
node scripts/run-oxlint.mjsandoxfmt --checkon both changed files - clean.run-tsgo.mjs -p tsconfig.core.jsonand-p test/tsconfig/tsconfig.core.test.json- clean.Real behavior proof
Behavior addressed: an operator.write (non-admin) operator can approve a node advertising the browser.proxy capability, whereas invoking browser.proxy requires operator.admin; the fix makes approval require operator.admin too.
Real environment tested: drove the real approveNodePairing reducer through its on-disk paired-device store in a fresh temp base directory on pristine main e681646 and on the patched tree; the real requestDevicePairing, approveDevicePairing, requestNodePairing, approveNodePairing, and resolveMissingRequestedScope all ran against the real store, with nothing stubbed (the store was seeded through the real pairing request/approve functions).
Exact steps or command run after this patch: seed a paired node device, request a node surface declaring commands ["browser.proxy"], then call approveNodePairing(requestId, { callerScopes: ["operator.pairing", "operator.write"] }, baseDir) and record the returned outcome.
Evidence after fix:
Observed result after fix: the same write-scoped approval that previously trusted the browser.proxy node is now refused with missingScope operator.admin, matching the invoke-time gate.
What was not tested: no live gateway RPC round trip or real remote node; the full build was not run.