fix(devices): recover unknown requestId approvals in password mode#100546
fix(devices): recover unknown requestId approvals in password mode#100546zy84338719 wants to merge 2 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
This PR extends the devices approval fallback logic to recover from method-level unknown requestId errors that can occur in password auth mode when targeting an implicit local loopback gateway, avoiding a local-node approval deadlock.
Changes:
- Extracts reusable
isLocalLoopbackGateway()eligibility logic from the pairing-required fallback path. - Adds
tryLocalApproveForLoopback()to locally approve the original pending request (or a compatible same-device replacement) when the gateway returnsunknown requestId. - Adds/extends CLI tests to cover password-mode unknown-request recovery and fail-closed behavior.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| src/cli/devices-cli.runtime.ts | Adds loopback eligibility helper + local approval recovery path for method-level unknown requestId errors in password auth mode. |
| src/cli/devices-cli.test.ts | Adds tests for password-mode fallback behavior and explicit --url non-fallback behavior. |
| it("does not use local fallback when an explicit --url is provided", async () => { | ||
| rejectGatewayForLocalFallback(); | ||
|
|
||
| it("does not fall back locally when --url points at a remote gateway (password mode)", async () => { | ||
| // Simulate password-mode unknown-requestId error with explicit --url | ||
| callGateway.mockRejectedValueOnce( | ||
| Object.assign(new Error("unknown requestId"), { gatewayCode: "INVALID_REQUEST" }), | ||
| ); | ||
|
|
||
| await expect( | ||
| runDevicesCommand(["list", "--json", "--url", "ws://127.0.0.1:18789"]), | ||
| ).rejects.toThrow("pairing required"); | ||
| runDevicesApprove(["req-1", "--url", "ws://10.0.0.5:18789"]), | ||
| ).rejects.toThrow("unknown requestId"); | ||
| expect(listDevicePairing).not.toHaveBeenCalled(); | ||
| expect(approveDevicePairing).not.toHaveBeenCalled(); | ||
| }); | ||
| }); |
| it("approves a same-device replacement when original requestId is gone (password mode)", async () => { | ||
| // list for resolveApprovePairingGatewayContext | ||
| rejectGatewayWithUnknownRequestId(); | ||
| // approve call fails | ||
| rejectGatewayWithUnknownRequestId(); | ||
| // Local state: original request is gone, replacement exists | ||
| listDevicePairing.mockResolvedValueOnce({ | ||
| pending: [ | ||
| { |
| * | ||
| * Returns the approval result on success, `null` if the request is genuinely | ||
| * absent from local state, or `undefined` if local fallback is not applicable | ||
| * (non-loopback or no local pending entries). |
479c601 to
b964f5d
Compare
|
Codex review: found issues before merge. Reviewed July 8, 2026, 8:23 PM ET / 00:23 UTC. Summary PR surface: Source +151, Tests +272. Total +423 across 2 files. Reproducibility: yes. Source inspection shows current main returns 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:
Copy recommended automerge instructionNext step before merge
Maintainer decision needed
Security Review findings
Review detailsBest possible solution: Close this PR as superseded by #85342. Do we have a high-confidence way to reproduce the issue? Yes. Source inspection shows current main returns Is this the best way to solve the issue? No, not as submitted. The loopback fallback is a reasonable narrow fix, but the missing-scope branch must preserve remote/non-loopback authorization failures instead of converting them into no-request output. Full review comments:
Overall correctness: patch is incorrect AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against d3ff48c51f4f. Label changesLabel justifications:
Evidence reviewedPR surface: Source +151, Tests +272. Total +423 across 2 files. View PR surface stats
Acceptance criteria:
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
|
In password auth mode, the CLI connects to the gateway successfully but the pending pairing request may have been refreshed by a node-host reconnect before approval lands. This produces a method-level 'unknown requestId' error instead of the connection-level 'pairing required' error that resolveLocalPairingFallback expects. The existing fallback in approvePairingWithFallback only triggers on 'pairing required' connection errors. In password auth mode it never fires, leaving the user in an unresolvable deadlock. While openclaw#85342 added same-device replacement logic, that code only runs *after* the fallback triggers — it does not help when the fallback itself is never reached. Changes: - Extract isLocalLoopbackGateway() from resolveLocalPairingFallback - Add tryLocalApproveForLoopback(): when gateway returns unknown requestId on a loopback gateway, attempt file-based approval - Case 1: original requestId still in local pending.json → approve - Case 2: node-host refreshed it → find compatible replacement via existing findSameDeviceReplacementRequest (strict publicKey/roles/ client-metadata/scope checks) - Both forbidden results surface via formatDevicePairingForbiddenMessage - Wire into both isUnknownRequestIdError exit paths in approvePairingWithFallback (admin-retry catch and main catch) - Add focused tests: unknown requestId → approve locally, unknown requestId → approve compatible replacement, forbidden surfacing Fixes openclaw#59889
b964f5d to
37babcd
Compare
|
@clawsweeper re-review\n\nAdded live password-mode loopback proof and patched the real method-level scope-denial path observed during proof. Validation: |
|
A/B verified locally against base SHA. Ran vitest with the PR's test file against the PR's fix (B) and against the base's fix (A). Result: CLEAN A/B The new test |
What Problem This Solves
openclaw devices approve <requestId>still deadlocks for local loopback gateways in password auth mode when node-host refreshes the pending device repair request before approval lands.PR #85342 fixed the compatible replacement-request path after local fallback has already been triggered by a connection-level
pairing requirederror. Password auth mode is different: the CLI can connect successfully, then the gateway can fail at method time instead of reaching the connection-level fallback. In real loopback password auth, the CLI can also connect device-less and loseoperator.pairingbefore requestId validation.Repro path:
openclaw devices approve <requestId>connects to the local gateway.unknown requestIdormissing scope: operator.pairingrather than a connection-levelpairing requirederror.null/prints an approval failure; it never attempts local file fallback.Changes
isLocalLoopbackGateway()so fallback eligibility can be reused without requiring a pairing-required connect error.tryLocalApproveForLoopback()for method-level password auth failures.unknown requestIdand real loopback password-authmissing scope: operator.pairingdenials.findSameDeviceReplacementRequest()compatibility gate for refreshed requests.--urlremote gateways.Evidence
Local validation on this branch:
Focused tests cover:
missing scope: operator.pairingfalling back to local listmissing scope: operator.pairingfalling back to local approve--urlnot using local fallbackRedacted live proof against a running local password-mode gateway:
Fixes #59889.
Supersedes the incomplete coverage in #100455.
Related #85342, #31041, #37749.