fix(gateway): authorize plugin methods from attached registry#94343
Conversation
|
@clawsweeper re-review PR #94343 fixes #92044 (workboard dispatch scope classification). This is the clean-history version that supersedes #94047 (closed). Summary of changes:
Real behavior proof:
This is the root-cause fix (option a). PR #92066 (option b, workboard CLI removes hardcoded scopes) is complementary and authored separately; this PR makes option b's behavior correct in the server. Revert test (proof the fix is necessary, not redundant): with |
|
🦞🧹 I asked ClawSweeper to review this item again. Re-review progress:
|
|
Codex review: needs maintainer review before merge. Reviewed June 19, 2026, 6:53 AM ET / 10:53 UTC. Summary PR surface: Source +20, Tests +65. Total +85 across 3 files. Reproducibility: yes. Source inspection shows current main authorizes Workboard dispatch through the global fallback path before using the attached dispatch registry, while Workboard declares the method as operator.write; I did not run a live gateway in this read-only review. 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. Risk before merge
Maintainer options:
Next step before merge
Security Review detailsBest possible solution: Land the attached-registry authorization fix after maintainers accept the auth-boundary change and required exact-head checks pass, then close the linked Workboard bug and retire the Workboard-only workaround path. Do we have a high-confidence way to reproduce the issue? Yes. Source inspection shows current main authorizes Workboard dispatch through the global fallback path before using the attached dispatch registry, while Workboard declares the method as operator.write; I did not run a live gateway in this read-only review. Is this the best way to solve the issue? Yes. Authorizing against the same attached registry used for dispatch is narrower and cleaner than a Workboard-only CLI workaround or the earlier cross-surface global lookup because handler and scope metadata stay paired. AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against 92d1f04de340. Label changesLabel changes:
Label justifications:
Evidence reviewedPR surface: Source +20, Tests +65. Total +85 across 3 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
|
|
@clawsweeper re-review P3 finding addressed in commit 2acbbb2:
No code-path changes; comment + wording only. PR remains at 🐚 platinum |
|
🦞🧹 I asked ClawSweeper to review this item again. Re-review progress:
|
|
@steipete As the workboard owner, could you please review this PR? It fixes the critical scope resolution bug (#92044) that was preventing from working correctly. @vincentkoc You've been very active recently with Gateway-related fixes. Could you also take a look at this method-scopes resolution fix? The PR has:
This is a focused fix for the Gateway scope resolution that affects workboard and other plugins. Thanks! |
…closes openclaw#92044) Plugin-registered gateway methods were silently requiring operator.admin because resolveScopedMethod only consulted activeRegistry.gatewayMethodDescriptors. Plugin descriptors may live in the http-route or channel surface instead, and authorizeOperatorScopesForMethod fell through to the admin default when the active surface missed the descriptor. Look across all three active surfaces (active, http route, channel) so plugin-declared scopes reach the resolver regardless of which surface a request entered. Active surface still wins when it has the descriptor. The admin fallback for unknown methods is preserved. Sibling coverage for openclaw#78894 (memory-core dream-promotion cron) confirms the same fix path works for any plugin that uses api.registerGatewayMethod. - src/gateway/method-scopes.ts: cross-surface descriptor lookup helper - src/gateway/method-scopes.test.ts: 3 surface + 1 regression + 2 sibling tests - scripts/repro/issue-92044-workboard-scope.mjs: standalone live proof
…wlist The standalone repro script for openclaw#92044 (scripts/repro/issue-92044-workboard-scope.mjs) suppresses typescript/use-unknown-in-catch-callback-variable on its main().catch() handler because the script is plain JS (.mjs) and cannot carry the type annotation oxlint would normally expect. Register the suppression in the explicit allowlist that lint-suppressions.test.ts enforces so the tooling CI gate stays green.
…face counts Standalone proof that setActivePluginRegistry syncs a plugin descriptor across all three active surfaces (active / httpRoute / channel), and that resolveRequiredOperatorScopeForMethod reaches the declared scope through the production code path the gateway server uses. Surfaces the gap that a unit-test-only proof can leave open.
2acbbb2 to
9a4afec
Compare
|
Land-ready verification:
Thanks @wangmiao0668000666. |
|
🎉 So happy to see PR #94343 merged — and even more so to see it landed by @steipete himself! Huge thanks for taking the time to rebase, run verification, and clean up the commits. Your "Land-ready verification" was a great learning moment for me — deleting the one-off repro scripts and having authorization flow through the dispatch-time attached registry is exactly the cleaner design, and watching you tighten it up at merge time taught me more than the original PR work did. Also thanks to @vincentkoc for the steady Gateway-area work that helped frame where this fix belongs. Each contribution teaches me something new. This round was all about the subtle invariants between attached-registry scope resolution vs the global resolver — and how easy it is to drift toward a "fix the CLI instead of fixing the seam" workaround when the symptom happens to surface in one plugin. Contributing to OpenClaw is genuinely fun, even when the reviewer bot makes me re-prove things 😄. I'll keep going — looking forward to the next opportunity to give back 🤖🦞 |
…gistered methods Selecting opts.methodRegistry unconditionally dropped late-registered plugin RPC methods; an earlier revision replaced it outright, which regressed the attached-registry authorization path (openclaw#94343). Prefer the attached registry only when it owns the requested method, otherwise rebuild from the live plugin registry so late-registered plugin methods (openclaw#94127) stay reachable. Adds scripts/proof-gateway-plugin-dispatch.mts as a live runtime proof harness.
…aw#94343) Authorize plugin gateway methods against the exact registry attached to dispatch, preserving fallback behavior for dynamic methods and deleting one-off repro scripts. Fixes openclaw#92044. Co-authored-by: wangmiao0668000666 <[email protected]>
…gistered methods Selecting opts.methodRegistry unconditionally dropped late-registered plugin RPC methods; an earlier revision replaced it outright, which regressed the attached-registry authorization path (openclaw#94343). Prefer the attached registry only when it owns the requested method, otherwise rebuild from the live plugin registry so late-registered plugin methods (openclaw#94127) stay reachable. Adds scripts/proof-gateway-plugin-dispatch.mts as a live runtime proof harness.
…gistered methods Selecting opts.methodRegistry unconditionally dropped late-registered plugin RPC methods; an earlier revision replaced it outright, which regressed the attached-registry authorization path (openclaw#94343). Prefer the attached registry only when it owns the requested method, otherwise rebuild from the live plugin registry so late-registered plugin methods (openclaw#94127) stay reachable. Adds scripts/proof-gateway-plugin-dispatch.mts as a live runtime proof harness.
Summary
openclaw workboard dispatchCLI was failing withmissing scope: operator.admineven thoughworkboard.cards.dispatchis registered withscope: operator.write. The root cause:resolveScopedMethodonly consultedactiveRegistry.gatewayMethodDescriptors, but plugin descriptors can also live in thehttpRouteorchannelsurface, so a request entering through those surfaces silently fell through to the admin default inauthorizeOperatorScopesForMethod. The fix looks across all three active surfaces, with the active surface still winning when it has the descriptor. The admin fallback for genuinely unknown methods is preserved.This is the same root-cause shape as #78894 (memory-core dream-promotion cron) and a latent risk for every other plugin that uses
api.registerGatewayMethod(workboard, memory-wiki, voice-call, google-meet, matrix, browser). Fixing it once in the resolver covers all of them.Supersedes #94047 (which accumulated noise commits during CI re-runs and real-time fix discovery).
Problem
workboard.cards.dispatchis registered with{ scope: operator.write }and is not in a reserved-admin namespace.resolveScopedMethodlooked the method up ingetPluginRegistryState()?.activeRegistry?.gatewayMethodDescriptorsand found no descriptor for it, so it returnedundefined.authorizeOperatorScopesForMethodthen didresolveRequiredOperatorScopeForMethod(method) ?? ADMIN_SCOPEand rejected the CLI's["operator.write", "operator.read"]withmissing scope: operator.admin.operator.admin, which short-circuits the check.openclaw gateway call workboard.cards.dispatch --params '{}'masked the bug because it requests the full default operator scope set (which includes admin) for unclassified methods.Solution
resolveScopedMethodnow consults all three active surfaces in priority order —active→httpRoute→channel— when looking up a plugin descriptor. The active surface still wins when it has the descriptor. The admin fallback for unknown methods is preserved (covered by a regression test).Changes
src/gateway/method-scopes.ts:43-71— extractfindPluginMethodDescriptorhelper that walks all three active surfaces;resolveScopedMethodnow uses it.src/gateway/method-scopes.test.ts:208-381— 6 new tests:scripts/repro/issue-92044-workboard-scope.mjs— standalone live proof with 5 cases against real production code (no vitest mocks).scripts/repro/issue-92044-workboard-e2e.mjs— production-surface proof showing descriptor counts aftersetActivePluginRegistryand the resulting scope resolution.test/scripts/lint-suppressions.test.ts— register the newuse-unknown-in-catch-callback-variablesuppression from the repro script in the explicit allowlist.Real behavior proof
Behavior addressed: plugin-declared scopes reach the scope resolver regardless of which active surface (active / http-route / channel) holds the descriptor; unknown methods still fall through to the admin default.
Real environment tested: Linux x86_64, Node 22.22.0, OpenClaw source checkout, branch
fix/workboard-scope-classification-92044-clean. Reproduced against compiled prod build (pnpm buildexit 0).Exact steps or command run after this patch:
node --import tsx scripts/repro/issue-92044-workboard-scope.mjs— PASS, 5/5 casesnode --import tsx scripts/repro/issue-92044-workboard-e2e.mjs— PASSnode scripts/run-vitest.mjs src/gateway/method-scopes.test.ts --run— 324/324 passnode scripts/run-vitest.mjs src/gateway/methods/registry.test.ts --run— 20/20 passnode scripts/run-vitest.mjs src/gateway/server-plugins.test.ts --run— 98/98 passnode scripts/run-vitest.mjs src/plugins/runtime.test.ts --run— 16/16 passnode scripts/run-vitest.mjs extensions/workboard/src/gateway.test.ts --run— 4/4 passnode scripts/run-vitest.mjs test/scripts/lint-suppressions.test.ts --run— 3/3 passnode scripts/run-oxlint.mjs <touched files>— cleanpnpm tsgo— exit 0pnpm build— exit 0Evidence after fix: terminal output of the standalone repro script on Linux x86_64 / Node 22.22.0, captured locally and pasted below.
Observed result after fix: all five cases produce the expected scope and authorization result. The unknown-method admin default is preserved. The same code path the CLI hits is exercised end-to-end with real production code (no vitest mocks).
What was not tested: an end-to-end run of
openclaw workboard dispatchagainst a live gateway with token-auth CLI. The repro covers the same code path the CLI hits, but I did not exercise the bundled binary on a real install.Revert test (proof the fix is necessary, not redundant)
I temporarily reverted
src/gateway/method-scopes.ts:43-58to the pre-fix single-surface lookup and re-ran the standalone repro. The repro flipped from PASS to FAIL on Case 1, proving it actually exercises the bug condition the fix addresses.Pre-fix output (with
findPluginMethodDescriptorremoved):This matches the user-visible symptom in #92044 exactly:
resolveRequiredOperatorScopeForMethodreturnsundefined, thenauthorizeOperatorScopesForMethoddefaults tooperator.admin, and the CLI's["operator.write", "operator.read"]is rejected.Post-fix output (this branch):
Notes for reviewers
extensions/workboard/src/cli.ts:91removes the hardcodedscopes: ["operator.write", "operator.read"]) is a complementary change that the author can land separately; this fix makes option b's behavior correct in the server (CLI's declared scopes now reach the resolver as intended).api.registerGatewayMethodcallers benefit. No new public API.sessions_spawnfailing despite full-scope token) has a different mechanism (device-scope enforcement, not scope classification) and is not addressed by this PR.Related
openclaw workboard dispatchfails withmissing scope: operator.adminthough the method is registered operator.write #92044🤖 Generated with Claude Code