Skip to content

Pairing: forward caller scopes during approval#55950

Merged
jacobtomlinson merged 2 commits into
openclaw:mainfrom
jacobtomlinson:fix/operator-pairing
Mar 27, 2026
Merged

Pairing: forward caller scopes during approval#55950
jacobtomlinson merged 2 commits into
openclaw:mainfrom
jacobtomlinson:fix/operator-pairing

Conversation

@jacobtomlinson

Copy link
Copy Markdown
Contributor

Summary

  • forwards gateway caller scopes into pairing approvals initiated by /pair approve
  • fail-closes operator-scope approvals when caller scopes are missing

Changes

  • updated approveDevicePairing() to reject operator approvals that do not carry caller scopes
  • passed caller scopes from the device-pair command, local CLI fallback, and silent local handshake approvals
  • updated regression coverage and test fixtures to use explicit approval scopes

Validation

  • Ran pnpm test -- src/infra/device-pairing.test.ts
  • Ran pnpm test -- extensions/device-pair/index.test.ts
  • Ran pnpm test -- src/cli/devices-cli.test.ts
  • Ran pnpm test -- src/gateway/server.silent-scope-upgrade-reconnect.poc.test.ts
  • Ran pnpm build
  • Ran pnpm check
  • Ran local agentic review via claude -p "/review"; the command stopped immediately because the local Claude CLI requested interactive approval for gh pr list, so I covered the change with the repo verification gates above instead

Notes

  • Residual risk or follow-up: src/gateway/server.sessions-send.test.ts currently fails in this worktree on an unrelated harness path (gatewayCallDeps.loadConfig is not a function), so I did not use it as a gate for this change

@jacobtomlinson
jacobtomlinson requested a review from a team as a code owner March 27, 2026 17:48
@openclaw-barnacle openclaw-barnacle Bot added gateway Gateway runtime cli CLI command changes extensions: device-pair size: S maintainer Maintainer-authored PR labels Mar 27, 2026
@greptile-apps

greptile-apps Bot commented Mar 27, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR forwards gateway caller scopes into approveDevicePairing() and adds fail-close behaviour when those scopes are absent for operator-role approvals. The scope propagation through the /pair approve command, the local CLI fallback, and test helpers is correct, and the new { status: "forbidden" } return branch in device-pairing.ts is well-implemented.\n\nHowever, one issue warrants attention before merging:\n\n- Silent-pairing gate bypass in message-handler.ts (line 844): The pairing-required guard uses a bare truthiness check — (approved || resolvedByConcurrentApproval) — which now evaluates to true when approveDevicePairing returns { status: \"forbidden\", missingScope: \"...\" }. This is a truthy non-null object even though the pairing was rejected, so a device whose silent pairing was scope-forbidden skips setHandshakeState(\"failed\") and close(1008), returning true as if it were successfully paired. The correct fix is to use approved?.status === \"approved\" instead of the bare approved reference.

Confidence Score: 4/5

The fail-close logic is correct everywhere except the silent-pairing gate in message-handler.ts, where a forbidden result's truthiness lets a rejected device bypass the connection-close path.

One P1 defect: in the silent pairing path the old truthiness guard on approved now passes for { status: "forbidden" } objects, allowing a scope-forbidden device to proceed past the pairing-required check. All other scope-propagation changes look correct.

src/gateway/server/ws-connection/message-handler.ts — line 844 truthiness guard on approved

Important Files Changed

Filename Overview
src/gateway/server/ws-connection/message-handler.ts Silent-pairing auto-approval updated to pass callerScopes and check approved?.status === "approved", but the downstream pairing-required gate at line 844 still uses a bare truthiness check on approved, which is now truthy for forbidden results too — allowing a scope-forbidden silent pairing to bypass rejection.
src/infra/device-pairing.ts New fail-close logic for operator approvals when callerScopes are absent, overloads updated to reflect broader return type — logic looks correct.
extensions/device-pair/index.ts Passes gatewayClientScopes to approveDevicePairing and handles the new forbidden status cleanly.
src/cli/devices-cli.ts Local CLI fallback hardcodes operator.admin as caller scopes with good justification; forbidden path re-throws with the original catch-block error as cause.

Comments Outside Diff (1)

  1. src/gateway/server/ws-connection/message-handler.ts, line 844 (link)

    P1 Forbidden result bypasses pairing-required gate

    approved is now typed as ApproveDevicePairingResult, which means it can be { status: "forbidden", missingScope: "..." } — a truthy non-null object. When a silent pairing attempt is rejected due to a scope mismatch, this truthy value makes the guard evaluate as !(true && true)false, so the function skips setHandshakeState("failed") and returns true instead of rejecting the connection.

    In practice: a device whose silent pairing was forbidden (operator scope missing from scopes) will pass the pairing check and continue to session establishment as if it were successfully paired.

    The condition should narrow to the "approved" discriminant rather than relying on truthiness:

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: src/gateway/server/ws-connection/message-handler.ts
    Line: 844
    
    Comment:
    **Forbidden result bypasses pairing-required gate**
    
    `approved` is now typed as `ApproveDevicePairingResult`, which means it can be `{ status: "forbidden", missingScope: "..." }` — a truthy non-null object. When a silent pairing attempt is rejected due to a scope mismatch, this truthy value makes the guard evaluate as `!(true && true)``false`, so the function skips `setHandshakeState("failed")` and returns `true` instead of rejecting the connection.
    
    In practice: a device whose silent pairing was **forbidden** (operator scope missing from `scopes`) will pass the pairing check and continue to session establishment as if it were successfully paired.
    
    The condition should narrow to the `"approved"` discriminant rather than relying on truthiness:
    
    
    
    How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/gateway/server/ws-connection/message-handler.ts
Line: 844

Comment:
**Forbidden result bypasses pairing-required gate**

`approved` is now typed as `ApproveDevicePairingResult`, which means it can be `{ status: "forbidden", missingScope: "..." }` — a truthy non-null object. When a silent pairing attempt is rejected due to a scope mismatch, this truthy value makes the guard evaluate as `!(true && true)``false`, so the function skips `setHandshakeState("failed")` and returns `true` instead of rejecting the connection.

In practice: a device whose silent pairing was **forbidden** (operator scope missing from `scopes`) will pass the pairing check and continue to session establishment as if it were successfully paired.

The condition should narrow to the `"approved"` discriminant rather than relying on truthiness:

```suggestion
            if (!(pairing.request.silent === true && (approved?.status === "approved" || resolvedByConcurrentApproval))) {
```

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "Pairing: require caller scopes on approv..." | Re-trigger Greptile

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bb39afc1af

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/gateway/server/ws-connection/message-handler.ts
@jacobtomlinson

Copy link
Copy Markdown
Contributor Author

Addressed the Greptile finding in commit 0ba8c456a4 by narrowing the silent-pairing gate to approved?.status === "approved" before allowing the handshake to proceed. Re-ran pnpm test -- src/gateway/server.silent-scope-upgrade-reconnect.poc.test.ts and pnpm check.

@jacobtomlinson
jacobtomlinson merged commit 4ee4960 into openclaw:main Mar 27, 2026
27 of 35 checks passed
@jacobtomlinson
jacobtomlinson deleted the fix/operator-pairing branch March 27, 2026 18:55
johnkhagler pushed a commit to johnkhagler/openclaw that referenced this pull request Mar 29, 2026
* Pairing: require caller scopes on approvals

* Gateway: reject forbidden silent pairing results
alexcode-cc pushed a commit to alexcode-cc/clawdbot that referenced this pull request Mar 30, 2026
* Pairing: require caller scopes on approvals

* Gateway: reject forbidden silent pairing results
lovewanwan pushed a commit to lovewanwan/openclaw that referenced this pull request Apr 28, 2026
* Pairing: require caller scopes on approvals

* Gateway: reject forbidden silent pairing results
ogt-redknie pushed a commit to ogt-redknie/OPENX that referenced this pull request May 2, 2026
* Pairing: require caller scopes on approvals

* Gateway: reject forbidden silent pairing results
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request May 9, 2026
* Pairing: require caller scopes on approvals

* Gateway: reject forbidden silent pairing results
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request May 24, 2026
* Pairing: require caller scopes on approvals

* Gateway: reject forbidden silent pairing results
Nachx639 pushed a commit to Nachx639/clawdbot that referenced this pull request Jun 17, 2026
* Pairing: require caller scopes on approvals

* Gateway: reject forbidden silent pairing results
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cli CLI command changes extensions: device-pair gateway Gateway runtime maintainer Maintainer-authored PR size: S

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant