fix(gateway): require admin scope for browser proxy invoke#85916
Conversation
|
Codex review: needs maintainer review before merge. Reviewed June 29, 2026, 7:14 PM ET / 23:14 UTC. Summary PR surface: Source +139, Tests +351, Docs +4. Total +494 across 16 files. Reproducibility: yes. Source inspection shows current main has Review metrics: 2 noteworthy metrics.
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 refreshed branch only after exact-head CI is green and maintainers intentionally own the stricter browser proxy boundary plus the documented trusted-only runtime scope contract. Do we have a high-confidence way to reproduce the issue? Yes. Source inspection shows current main has Is this the best way to solve the issue? Yes, with an explicit maintainer acceptance requirement. The gate belongs at the Gateway AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against 5dae3d49e623. Label changesLabel changes:
Label justifications:
Evidence reviewedPR surface: Source +139, Tests +351, Docs +4. Total +494 across 16 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
|
There was a problem hiding this comment.
Pull request overview
Closes an authorization-mismatch gap where direct node.invoke(browser.proxy) could be reached by operator.write callers even though the equivalent browser.request gateway method is operator.admin. The fix adds an explicit admin-scope check in the node.invoke handler for command === "browser.proxy" and locks it in with two new Gateway-level tests.
Changes:
- Add
BROWSER_PROXY_REQUIRED_SCOPEadmin gate inside thenode.invokehandler, just after the persistent-profile-mutation guard. - Add a local
clientHasOperatorAdminScopehelper that reads scopes fromclient.connect.scopes. - Extend
server.node-invoke-approval-bypass.test.tswith aconnectDeviceTokenOperatorhelper and tests for both write-rejected and admin-allowed directbrowser.proxyflows; also addsbrowser.proxyto the test gateway allowlist.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| src/gateway/server-methods/nodes.ts | Adds admin-scope guard for node.invoke(browser.proxy) and a local scope helper. |
| src/gateway/server.node-invoke-approval-bypass.test.ts | Adds scoped device-token helper and write/admin scope tests for direct browser.proxy invocation. |
| if (command === "browser.proxy" && !clientHasOperatorAdminScope(client)) { | ||
| respond( | ||
| false, | ||
| undefined, | ||
| errorShape(ErrorCodes.INVALID_REQUEST, `missing scope: ${BROWSER_PROXY_REQUIRED_SCOPE}`), | ||
| ); | ||
| return; | ||
| } |
| function clientHasOperatorAdminScope(client: GatewayClient | null): boolean { | ||
| const scopes = Array.isArray(client?.connect?.scopes) ? client.connect.scopes : []; | ||
| return scopes.includes(BROWSER_PROXY_REQUIRED_SCOPE); | ||
| } |
|
ClawSweeper PR egg 🔥 Warming up: real-behavior proof passed; findings, security review, or rank-up moves are still in progress. Hatch commandComment Hatchability rules:
What is this egg doing here?
|
8dcece7 to
1d16d0d
Compare
|
Follow-up for the ClawSweeper P1 compatibility finding. I kept the new external direct-invoke boundary intact and updated the first-party browser proxy paths to use explicit admin-scoped dispatch:
Local verification after the follow-up patch: Observed locally: This should address both rank-up items: official browser proxy callers now request an admin-scoped route, and there is focused regression coverage for the browser tool, Google Meet transport, and plugin runtime scope handoff. @clawsweeper re-review |
|
🦞🧹 I asked ClawSweeper to review this item again. Re-review progress:
|
|
Clean, minimal scope fix. The gap is real: The fix is correctly layered. The |
This comment was marked as spam.
This comment was marked as spam.
|
Heads up: this PR needs to be updated against current |
1d16d0d to
d0e4e78
Compare
d0e4e78 to
faa29c9
Compare
|
@LaPhilosophie Thanks for the work here. I’m taking maintainer ownership to finish this security hardening. The earlier Dependency Guard warning is no longer the blocker; that check now passes. The branch conflicts with current I accept the intentional behavior change that direct |
1ceea79 to
a62d0fd
Compare
|
Maintainer takeover update: refreshed onto current Changes made during takeover:
Validation:
Remote Testbox proof could not be started from this checkout because neither the Crabbox nor Blacksmith CLI is installed; the wrapper failed before allocation. Fresh GitHub CI is now running on the exact pushed SHA and will provide the remote gates. @clawsweeper re-review |
|
🦞🧹 I asked ClawSweeper to review this item again. |
|
Exact-head verification is complete for
The PR is mergeable and clean. The remaining action is the final maintainer merge decision; no further code repair is indicated. |
…85916) * fix(gateway): require admin scope for browser proxy invoke * fix(gateway): document trusted node scopes * fix(gateway): align browser scope test * fix(gateway): satisfy node test lint --------- Co-authored-by: Agustin Rivera <agustin@rivera-web.com>
…85916) * fix(gateway): require admin scope for browser proxy invoke * fix(gateway): document trusted node scopes * fix(gateway): align browser scope test * fix(gateway): satisfy node test lint --------- Co-authored-by: Agustin Rivera <agustin@rivera-web.com>
…85916) * fix(gateway): require admin scope for browser proxy invoke * fix(gateway): document trusted node scopes * fix(gateway): align browser scope test * fix(gateway): satisfy node test lint --------- Co-authored-by: Agustin Rivera <agustin@rivera-web.com>
…85916) * fix(gateway): require admin scope for browser proxy invoke * fix(gateway): document trusted node scopes * fix(gateway): align browser scope test * fix(gateway): satisfy node test lint --------- Co-authored-by: Agustin Rivera <agustin@rivera-web.com>
Summary
browser.requestis admin-scoped, but the equivalent node-host browser proxy command could also be reached through directnode.invoke(browser.proxy)from anoperator.writecaller.operator.adminbefore forwarding directnode.invokerequests forbrowser.proxy, while keeping the bundled Google Meet browser proxy path working through an explicit official-plugin runtime scope request.node.invoke(browser.proxy)is rejected before node dispatch; admin-scoped callers still dispatch; bundled/trusted official plugin runtime calls may request admin scope from their real plugin metadata; third-party plugin runtime callers cannot mint admin by passingscopesthrough either gateway or CLI runtime paths.browser.requestbehavior is unchanged; node-host browser proxy implementation is unchanged; persistent browser profile mutation blocking remains unchanged.Change Type
Scope
Linked Issue/PR
Motivation
The browser gateway surface already treats
browser.requestas admin-scoped. That route can reach a node-hosted browser through the samebrowser.proxybackend capability when a browser-capable node is connected.Before this patch, a scoped operator token with only
operator.writecould not callbrowser.request, but could call the underlyingbrowser.proxycommand through directnode.invoke. That created an authorization mismatch between two routes to the same browser proxy capability.This PR aligns the route semantics: the direct node invoke route for
browser.proxynow requires the same admin scope as the browser gateway route. The Google Meet integration keeps its intended bundled-plugin path by requesting admin scope through the plugin runtime; both gateway and CLI runtime scope-forwarding paths honor that request only for bundled/trusted official plugin metadata, not for arbitrary third-party plugin runtime callers.Real behavior proof (required for external PRs)
browser.proxythrough directnode.invokenow follows the same admin boundary asbrowser.request; bundled Google Meet can still use the browser proxy through official plugin runtime metadata; third-party plugin runtime callers cannot mint admin scope by passingscopesthrough gateway or CLI runtime paths.browser-proxy-admin-scope, head1ceea79bec.node --import tsx .artifacts/browser-proxy-current-head-proof.tsnode scripts/run-vitest.mjs run --config test/vitest/vitest.bundled.config.ts src/plugins/cli-gateway-nodes-runtime.test.ts src/plugins/registry.runtime-config.test.tsOPENCLAW_VITEST_MAX_WORKERS=2 OPENCLAW_VITEST_SHARD_NAME=agentic-control-plane-http-plugin-ws OPENCLAW_TEST_PROJECTS_PARALLEL=2 OPENCLAW_VITEST_INCLUDE_FILE=<generated include json> NODE_OPTIONS=--max-old-space-size=8192 npm exec --yes pnpm@11.1.0 -- node scripts/test-projects.mjs test/vitest/vitest.gateway-server.config.tsnpm exec --yes pnpm@11.1.0 -- node scripts/run-tsgo.mjs -p test/tsconfig/tsconfig.core.test.json --incremental --tsBuildInfoFile .artifacts/tsgo-cache/core-test.tsbuildinfonpm exec --yes pnpm@11.1.0 -- node scripts/run-tsgo.mjs -p test/tsconfig/tsconfig.extensions.test.json --incremental --tsBuildInfoFile .artifacts/tsgo-cache/extensions-test.tsbuildinfonpm exec --yes pnpm@11.1.0 -- oxfmt --check src/plugins/runtime/gateway-request-scope.ts src/plugins/registry.ts src/gateway/server-plugins.ts src/gateway/server-plugins.test.tsCI shard reproduction after the fix:
CLI runtime scope-forwarding regression coverage after the fix:
node.invoke(browser.proxy).browser.proxyto the node.browser.proxywhen it explicitly requests admin scope through official plugin metadata.operator.adminis still downgraded and rejected before node dispatch.node.invokeauth gate, plugin runtime owner metadata propagation, and node dispatch boundary locally; targeted regression tests cover the CLI runtime scope-forwarding guardrail.node.invoke(browser.proxy)reached node dispatch whilebrowser.requestwas blocked byoperator.admin.Root Cause
node.invokeenforced the genericoperator.writescope for node commands, but did not apply the stricteroperator.adminscope used by the equivalentbrowser.requestgateway route.browser.requestand directnode.invokefor the samebrowser.proxybackend command, and no test proving official plugin runtime scope requests cannot be forged by third-party plugin runtime callers through gateway or CLI runtime paths.node.invoke; the remaining non-persistent browser proxy paths still needed route-level admin scope alignment.Regression Test Plan
src/gateway/server.node-invoke-approval-bypass.test.tssrc/gateway/server-methods/nodes.invoke-wake.test.tssrc/gateway/server-plugins.test.tssrc/plugins/cli-gateway-nodes-runtime.test.tssrc/plugins/registry.runtime-config.test.tsextensions/google-meet/src/transports/chrome-browser-proxy.test.tsextensions/browser/src/browser-tool.test.tsbrowser.requestroute by directly invokingbrowser.proxy; admin-scoped callers still work; bundled/trusted official plugin runtime callers can request admin scope; third-party plugin runtime callers cannot mint admin scope through gateway or CLI runtime paths.User-visible / Behavior Changes
Direct
node.invokecallers now needoperator.adminto invokebrowser.proxy. Admin-scoped callers andbrowser.requestbehavior are unchanged. Bundled Google Meet browser proxy usage remains supported through the plugin runtime path.Diagram
Security Impact
Yes, explain risk + mitigation: this reduces the callable surface for scoped write-only operator tokens by preventing direct access to the node-hosted browser proxy route. The official plugin runtime compatibility path is metadata-bound to bundled/trusted official plugin records in both gateway and CLI runtime paths and does not let arbitrary plugin runtime callers mint admin scope.Repro + Verification
Environment
browser.proxySteps
node.invoke(browser.proxy)as a write-only operator caller.node.invoke(browser.proxy)as an admin operator caller.nodes.invokewithscopes: ["operator.admin"].nodes.invokewith the same requested admin scope.Expected
callGateway.Actual
Evidence
Human Verification
browser.proxyrejection before node dispatch; admin directbrowser.proxydispatch; bundled Google Meet runtime dispatch with requested admin scope; third-party runtime rejection despite requested admin scope; bundled CLI runtime forwards requested scope; third-party CLI runtime drops requested scope; CI shardagentic-control-plane-http-plugin-wspasses locally after the scope metadata fix.browser.proxy; command is allowlisted so rejection is specifically the scope check; plugin runtime metadata survives through the real registry proxy; stale global registry state does not decide official-plugin scope by itself; CLI runtime does not forward requested scopes without bundled/trusted official plugin metadata.Review Conversations
Compatibility / Migration
browser.proxythroughnode.invokenow needoperator.admin, matching the existingbrowser.requestscope.Risks and Mitigations
node.invokeforbrowser.proxywill now be rejected.browser.requestroute.