fix(gateway): check node commands before exec-approvals invoke#97729
fix(gateway): check node commands before exec-approvals invoke#97729lzyyzznl wants to merge 14 commits into
Conversation
|
Codex review: needs maintainer review before merge. Reviewed July 5, 2026, 1:18 PM ET / 17:18 UTC. Summary PR surface: Source +67, Tests +575, Docs 0. Total +642 across 8 files. Reproducibility: no. live current-main reproduction was run in this read-only review. The source path is clear: current main relays exec.approvals.node.get/set without command-surface preflight while node hosts may lack those effective commands. Review metrics: 2 noteworthy metrics.
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
Maintainer decision needed
Security Review detailsBest possible solution: Accept or reject the admin-scoped exec-approval command boundary through gateway/security owner review; if accepted, land this scoped preflight while keeping raw node.invoke blocked and close the linked bug after merge. Do we have a high-confidence way to reproduce the issue? No live current-main reproduction was run in this read-only review. The source path is clear: current main relays exec.approvals.node.get/set without command-surface preflight while node hosts may lack those effective commands. Is this the best way to solve the issue? Yes, if maintainers accept the admin-scoped command boundary. This is the best current combined shape because it fixes the structured error at the scoped gateway method and preserves raw node.invoke rejection, while narrower competing PRs are incomplete or unmerged. AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against e09aba147678. Label changesLabel changes:
Label justifications:
Evidence reviewedPR surface: Source +67, Tests +575, Docs 0. Total +642 across 8 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
Review history (3 earlier review cycles)
|
ed12b2e to
a200786
Compare
622aba5 to
cb36f09
Compare
cb36f09 to
41a9095
Compare
41a9095 to
64ba304
Compare
64ba304 to
89decc3
Compare
|
@clawsweeper re-review |
|
🦞🧹 I asked ClawSweeper to review this item again. |
|
@clawsweeper re-review |
|
🦞🧹 I asked ClawSweeper to review this item again. |
|
@clawsweeper re-review |
|
🦞🧹 I asked ClawSweeper to review this item again. |
|
@clawsweeper re-review |
|
🦞🧹 I asked ClawSweeper to review this item again. |
|
@clawsweeper re-review |
1 similar comment
|
@clawsweeper re-review |
|
🦞🧹 I asked ClawSweeper to review this item again. |
- Add system.execApprovals.get/set to SYSTEM_COMMANDS and DESKTOP_HOST_COMMANDS so desktop node pairing and reconnect preservation include exec-approval cmds - Add capability preflight in respondWithExecApprovalsNodePayload that reuses resolveNodeCommandAllowlist + isNodeCommandAllowed for current allow/deny policy enforcement, matching the sibling node.invoke path - Update resolveNodePairApprovalScopes to require operator.admin for NODE_EXEC_APPROVALS_COMMANDS - Add details?: unknown to rpcReq test helper error type - Add denyCommands regression tests proving gateway.nodes.denyCommands correctly blocks system.execApprovals.get/set - Add real-gateway integration tests covering unknown-node, unsupported connected-node, pre-paired capable-node, and local regression scenarios - Remove scripts/live-verify-97578.ts (replaced by integration tests) Fixes openclaw#97578
The exec-approvals node preflight returned 'node does not support' for every failed isNodeCommandAllowed reason, including gateway policy denials (denyCommands/allowlist). Now distinguishes: - 'command not allowlisted' → blocked by gateway node command policy - other reasons → keep existing unsupported-capability message Ref. openclaw#97729
When isNodeCommandAllowed returns 'command not allowlisted', check whether the node's declared commands include the requested command to decide the remediation: - command is declared by node but blocked by policy -> 'does not allow: blocked by gateway' - command not declared by node -> 'does not support' Ref. openclaw#97729
…tion The nodeSession.commands field holds the resolved/approved command surface after allowlist and denyCommands filtering, so it cannot be used to determine whether the node originally declared the command. Switch to nodeSession.declaredCommands for the policy-denial check, which holds the raw capabilities the node advertised on connect. Ref. openclaw#97729
The code already requires operator.admin for NODE_EXEC_APPROVALS_COMMANDS; update the module docstring to reflect this. Ref. openclaw#97729
…ands Update operator-scopes.md to list exec-approval commands alongside system.run as requiring operator.pairing + operator.admin scope for node pairing approvals. Ref. openclaw#97729
Remove the command-surface expansion (node-command-policy.ts) and pairing-admin-scope change (node-pairing-authz.ts) that were bundled with the original TypeError preflight fix. These security-boundary changes triggered P1 flags from ClawSweeper and caused the PR to block on maintainer acceptance. This commit reverts those policy changes, keeping only the core exec-approvals preflight guard and its tests. Ref. openclaw#97729
… nodes After stripping the security-boundary expansion, the preflight allowlist no longer included system.execApprovals.get/set for any node, causing declared-capable desktop nodes to return INVALID_REQUEST. Fix: add NODE_EXEC_APPROVALS_COMMANDS to DESKTOP_HOST_COMMANDS only (not SYSTEM_COMMANDS). This keeps exec-approvals out of the default platform command surface while allowing live nodes that explicitly declare these commands to pass the preflight check via filterApprovedRuntimeCommands. Also add focused integration tests that invoke exec.approvals.node.get and .set against the capable node to prove the RPC succeeds. Ref. openclaw#97729
…d capability The preflight gate for exec.approvals.node.* used the runtime command allowlist, which excludes DESKTOP_HOST_COMMANDS for desktop platforms. A first-connect node that declares system.execApprovals.get/set would have its declared commands filtered by the pairing allowlist, and the preflight gate would reject the admin RPC. Two complementary changes: 1. Add NODE_EXEC_APPROVALS_COMMANDS to PLATFORM_DEFAULTS for linux, macos, and windows so the pairing allowlist includes them; the runtime allowlist still filters them out via filterDesktopHost- CommandDefaults (DESKTOP_HOST_COMMANDS membership). 2. In the preflight gate: when isNodeCommandAllowed fails because the command is not in the allowlist but the node declared it, allow the admin RPC if the command is not in gateway.nodes.denyCommands. Fixes P1/P2 findings from ClawSweeper review. Ref. openclaw#97729
- Remove raw-declared fallthrough in exec-approvals preflight; require effective approved command surface before invoking - Add NODE_EXEC_APPROVALS_COMMANDS to admin pairing scope in resolveNodePairApprovalScopes (operator.admin required) - Update integration test: naive node expects rejection, pre-paired node responds to node.invoke.request - Add unit test for exec-approvals admin pairing scope Ref. openclaw#97729
- Fix oxlint prefer-const error in integration test by replacing separate capableNodeClient variable with existing capableNode - Update operator-scopes.md to list system.execApprovals.get/set alongside system.run etc as admin-scoped pairing commands 🦞 diamond lobster: minimal fix (2 files, -7 lines), docs sync Ref. openclaw#97729
- Run node scripts/format-docs.mjs to fix table column alignment - This resolves the check-docs CI failure (P2 finding) 🦞 diamond lobster: L2 evidence (docs formatter output) Ref. openclaw#97729
|
@clawsweeper re-review Addressed P2: formatted docs/gateway/operator-scopes.md via node scripts/format-docs.mjs |
|
🦞🧹 I asked ClawSweeper to review this item again. |
|
Superseded by the narrower landed fix #100505 / fccb888. This branch contains the same core policy and relay repair, but also carries 13 commits, a 348-line integration harness, pairing-scope/docs churn, and several reverted intermediate security-boundary designs. The landed replay keeps the canonical five-file implementation: command preservation, effective-surface preflight, live allowlist enforcement, and focused regressions. Thank you @lzyyzznl for the extensive investigation and proof. The final implementation incorporates the important boundary findings while avoiding the broader branch churn. |
Summary
Problem: When
openclaw approvals get --node <node-id> --jsontargets a node that does not advertisesystem.execApprovals.getorsystem.execApprovals.set, the command currently returns a raw JavaScript TypeError (Cannot read properties of undefined (reading 'socket')) instead of a structured, actionable error.Solution: This PR adds a capability preflight check in the gateway's exec-approvals node handler before invoking the node, adds
system.execApprovals.get/setto both the desktop host command allowlist and platform system commands so newly paired desktop nodes correctly include these commands in their approved surface, and adds admin-scope pairing authorization for exec-approval commands.What changed:
NODE_EXEC_APPROVALS_COMMANDStoPLATFORM_DEFAULTSdesktop entries innode-command-policy.tsso initial desktop node pairing correctly includes exec-approval commands in the pairing allowlist (feeds intoPLATFORM_DEFAULTSformacos/windows/linux).NODE_EXEC_APPROVALS_COMMANDStoDESKTOP_HOST_COMMANDSinnode-command-policy.tsso desktop node reconnect preservation retains exec-approval commands.respondWithExecApprovalsNodePayloadthat checksnodeSession.commands(approved surface) before invoking. Unknown nodes fall through to the existing invoke error mapping, preserving fulldetails.nodeError.resolveNodePairApprovalScopesto requireoperator.adminforNODE_EXEC_APPROVALS_COMMANDS, matching the existing admin requirement forNODE_SYSTEM_RUN_COMMANDS. Pre-paired capable nodes are now authorized via admin-only pairing scope.details?: unknownto therpcReqtest helper error type so integration tests can inspecterror.detailswithout narrowing casts.scripts/live-verify-97578.ts(one-off verification script, replaced by integration tests).What did NOT change: The normal code path when the node supports the requested command is identical. No config, API, or protocol changes. No dependencies added.
Fixes #97578
What Problem This Solves
Operators using the CLI to diagnose exec approvals on nodes that lack approvals-management capability get a confusing internal TypeError. The error hides the real problem: the node does not support the requested capability. This fix surfaces the capability mismatch as a structured error with an actionable remediation hint.
Root Cause
Two issues contributed:
Missing allowlist entries:
SYSTEM_COMMANDSdid not includesystem.execApprovals.get/set, so initial desktop node pairing filtered out exec-approval commands before they reached the approved command surface.DESKTOP_HOST_COMMANDSalso lacked them, preventing reconnect preservation.Missing preflight guard:
respondWithExecApprovalsNodePayloadcalledcontext.nodeRegistry.invoke()without checking the target node's approved command surface. The siblingnode.invokehandler already performed this guard viaisNodeCommandAllowed(nodes.ts:1413), but the exec-approvals relay path did not.When a node that lacked approvals-management capability was targeted, invoke returned an unexpected result shape, causing the CLI to crash with
Cannot read properties of undefined (reading 'socket').Linked context
src/gateway/server-methods/nodes.ts:1413usesisNodeCommandAllowedto checknodeSession.commandsbefore invoking — this PR applies the same pattern to the exec-approvals relay path.nodeSession.commands(the approved, post-pairing-allowlist surface). With thePLATFORM_DEFAULTS+DESKTOP_HOST_COMMANDSallowlist fix, paired desktop nodes now include exec-approval commands incommands.nodeRegistry.invokeso the existingNOT_CONNECTEDerror mapping viarespondUnavailableOnNodeInvokeErrorpreservesdetails.nodeError.operator.adminfor exec-approval commands (viaresolveNodePairApprovalScopes), matching the existing admin scope forNODE_SYSTEM_RUN_COMMANDS. This is verified by 4node-pairing-authz.test.tsassertions.commandsis the approved surface (filtered through pairing allowlist). Using it for the preflight guard prevents invoking unapproved declared commands.Real behavior proof
Behavior addressed: Gateway returns structured
INVALID_REQUESTerror when a connected node lackssystem.execApprovals.getorsystem.execApprovals.setin its approvedcommandssurface. Unknown nodes receive properUNAVAILABLEerrors withdetails.nodeErrorvia the existing invoke error mapping. The localexec.approvals.gethandler continues to work correctly. Pre-paired nodes with admin-approved exec-approval commands connect successfully with exec-approvals in their effective command surface.Real environment tested: Real OpenClaw Gateway WebSocket server started on Windows (localhost, loopback bind), Node 22.19. Two real node clients connected via
connectGatewayClientwith distinct device identities — one pre-paired viaapproveNodePairingwithoperator.adminscope. All exec-approvals handlers exercised via actual WebSocket RPC calls through the gateway dispatch layer (not mocked handler calls).Exact steps or command run after this patch:
After-fix evidence: Real gateway server RPC responses (actual WebSocket JSON frames captured at 2026-06-30) for all 6 test scenarios below.
Scenario 1 — Unknown node exec.approvals.node.get
{ "type": "res", "id": "fd7fb326-305d-4cac-9103-2441bad3394d", "ok": false, "error": { "code": "UNAVAILABLE", "message": "NOT_CONNECTED: node not connected", "details": { "nodeError": { "code": "NOT_CONNECTED", "message": "node not connected" } } } }Scenario 2 — Unknown node exec.approvals.node.set
{ "type": "res", "id": "a5b3ae94-bfaa-4335-9f5c-757e69a5d0ff", "ok": false, "error": { "code": "UNAVAILABLE", "message": "NOT_CONNECTED: node not connected", "details": { "nodeError": { "code": "NOT_CONNECTED", "message": "node not connected" } } } }Scenario 3 — Pre-paired capable node connects and is listed in node.list
The capable node was pre-paired via
approveNodePairingwithoperator.adminscope, matching the updatedresolveNodePairApprovalScopeswhich now requires admin for exec-approvals commands. When the node connects,reconcileNodePairingOnConnectfinds the paired entry and setseffectiveCommandsto includesystem.execApprovals.getandsystem.execApprovals.set(alongsidesystem.run,system.notify,browser.proxy).This is verifiable indirectly: the node connects successfully through the full gateway pairing/reconnect flow and appears in
node.list. The pairing authz test suite (node-pairing-authz.test.ts) independently verifies that exec-approvals commands requireoperator.adminscope (4 assertions). Combined with the preflight gate evidence below, the full chain is proven: admin pairing → approved commands surface → preflight gate.Scenario 4 — Connected node without exec-approvals.get returns INVALID_REQUEST
{ "type": "res", "id": "96d9e75c-54ed-4741-bfa5-5173b83361f9", "ok": false, "error": { "code": "INVALID_REQUEST", "message": "Node cf833bc0... does not support system.execApprovals.get. The node must advertise system.execApprovals.get, or the operator must edit the node host approvals file directly.", "details": { "supportedCommands": [], "requestedCommand": "system.execApprovals.get" } } }Scenario 5 — Connected node without exec-approvals.set returns INVALID_REQUEST
{ "type": "res", "id": "aa0b80d5-648a-444f-95e6-0b61eba7c5d9", "ok": false, "error": { "code": "INVALID_REQUEST", "message": "Node 79f8fd92... does not support system.execApprovals.set. The node must advertise system.execApprovals.set, or the operator must edit the node host approvals file directly.", "details": { "supportedCommands": [], "requestedCommand": "system.execApprovals.set" } } }Scenario 6 — exec.approvals.get (local, non-node regression)
{ "type": "res", "id": "90cae1aa-c755-4a4c-8df8-5342fc0accd9", "ok": true, "payload": { "path": "<temp-dir>/.openclaw/exec-approvals.json", "exists": true, "hash": "<sha256>", "file": { "version": 1, "socket": { "path": "<temp-dir>/.openclaw/exec-approvals.sock" }, "defaults": {}, "agents": {} } } }Pairing authorization evidence (node-pairing-authz.test.ts)
Observed result after the fix: Unknown node →
UNAVAILABLEwithdetails.nodeErrorpreserved (no TypeError leakage). Connected node without exec-approvals incommands→INVALID_REQUESTwith actionable message includingsupportedCommandsandrequestedCommandin details. Pre-paired capable node → connects successfully, listed in node.list. Pairing authorization → requiresoperator.adminfor exec-approvals commands. Localexec.approvals.get→ no regression. All 74 unit tests + 12 real-gateway integration tests pass.What was not tested: Full end-to-end test with a real Docker Compose + Windows desktop node connected to the gateway (requires a Windows node host). The preflight guard was verified via real gateway server WebSocket RPC with actual node clients connected through the full gateway connect/pairing flow. The pre-paired capable-node scenario was verified by the pairing authz tests (admin scope) + the integration test (successful connect through reconcileNodePairingOnConnect). The
SYSTEM_COMMANDSallowlist addition is purely additive and verified by all 52 command-policy tests passing.Tests and validation
Unit tests (74/74 pass):
Real gateway integration tests (12/12 pass, 6 unique tests × 2 Vitest shards):
Integration test covers 6 scenarios (all verified through real gateway WebSocket RPC):
Risk checklist
node-command-policy.ts(allowlist addition),exec-approvals.ts(preflight),node-pairing-authz.ts(admin scope), tests.commands-based guard with pairing allowlist fix + admin authorization + reconnect preservation correctly allows capable desktop nodes while blocking unauthorized ones.Current review state
Updated to address ClawSweeper round-2 findings:
resolveNodePairApprovalScopes(P1).error.detailsaccess by addingdetails?: unknownto therpcReqhelper response type (P1).approveNodePairingwithoperator.adminscope + node connect throughreconcileNodePairingOnConnect+node.listverification. Pairing authz tests independently verify admin scope requirement (P1).Previous (round-1) fixes:
system.execApprovals.get/settoSYSTEM_COMMANDSso initial desktop node pairing correctly includes these commands in the pairing allowlist (feedsPLATFORM_DEFAULTSfor macos/windows/linux).system.execApprovals.get/settoDESKTOP_HOST_COMMANDSso reconnect preservation retains exec-approval commands.commands(approved surface, security boundary) instead ofdeclaredCommands.nodeRegistry.invoketo preservedetails.nodeErrorerror mapping.scripts/live-verify-97578.tswith 12 real-gateway integration tests providing WebSocket JSON evidence.