openclaw - ✅(Solved) Fix Misleading error: pairing required when cron.add fails due to insufficient scopes [1 pull requests, 1 comments, 2 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
openclaw/openclaw#64421Fetched 2026-04-11 06:15:01
View on GitHub
Comments
1
Participants
2
Timeline
2
Reactions
0
Author
Participants
Timeline (top)
commented ×1cross-referenced ×1

Error Message

When calling cron.add via QQ channel, users receive a misleading error message: 4. Error: pairing required instead of insufficient permissions

  • Expected: Error should clearly indicate insufficient permissions (requires operator.admin for cron.add)
  • Actual: Error shows pairing required which is misleading However, all reasons result in the same error message: "gateway closed (1008): pairing required" The error at line 26185 does not include the reason parameter:
  1. Include the reason in the error message: close(1008, \pairing required: ${reason}`)`
  2. Or return a more specific error code for scope insufficiency

Root Cause

In gateway-cli-DlnlX7IW.js, the requirePairing function is called with different reason values:

  • "not-paired" - public key mismatch
  • "metadata-upgrade" - platform info changed
  • "role-upgrade" - role changed
  • "scope-upgrade" - scopes insufficient

However, all reasons result in the same error message: "gateway closed (1008): pairing required"

The error at line 26185 does not include the reason parameter:

close(1008, "pairing required");  // reason is not included in the close message

Fix Action

Fixed

PR fix notes

PR #64527: fix(gateway): surface pairing reason in close frame and error message

Description (problem / solution / changelog)

Summary

  • Problem: cron.add (and any other admin-scope call from a device with insufficient scopes) fails with gateway closed (1008): pairing required. That message is emitted for all four requirePairing reasons (not-paired, role-upgrade, scope-upgrade, metadata-upgrade), so users cannot tell whether they hit a fresh pairing, a scope elevation, or a metadata/role change without digging into the gateway logs.
  • Why it matters: the reporter on #64421 spent time debugging "pairing required" before realising the real cause was scope-upgrade (the device had operator.read/operator.write but cron.add requires operator.admin). Same flow hits QQ, iMessage, Telegram, and any other channel that calls admin-scoped methods.
  • What changed: the requirePairing closure in src/gateway/server/ws-connection/message-handler.ts now builds pairing required (${reason}) once and threads it into both the send() error frame and the close(1008, ...) frame. The close message goes through the same truncateCloseReason wrapper the other close(1008, ...) sites in this file already use.
  • What did NOT change (scope boundary): ErrorCodes.NOT_PAIRED, ConnectErrorDetailCodes.PAIRING_REQUIRED, and the structured details.reason field are all unchanged. Every existing consumer that matches via .includes("pairing required"), /pairing required/i, or .contains(...) still works — the new suffix is additive. No changes to scope enforcement or pairing semantics.

Change Type (select all)

  • Bug fix
  • Feature
  • Refactor required for the fix
  • Docs
  • Security hardening
  • Chore/infra

Scope (select all touched areas)

  • Gateway / orchestration
  • Skills / tool execution
  • Auth / tokens
  • Memory / storage
  • Integrations
  • API / contracts
  • UI / DX
  • CI/CD / infra

Linked Issue/PR

  • Closes #64421
  • Related #
  • This PR fixes a bug or regression

Root Cause (if applicable)

  • Root cause: the requirePairing closure in message-handler.ts (lines 842–1009) already knows the exact reason that forced the pairing (and even passes it into details.reason and setCloseCause), but the human-readable message passed to errorShape(...) and close(1008, ...) was a hardcoded string literal. The structured signal was there; only the textual surface was dropped.
  • Missing detection / guardrail: no runtime test asserted on the close-frame reason — only on the res-frame error.message. The close frame is what CLI/TUI surfaces to users, and it was never covered. This PR adds that coverage.
  • Contributing context: the four reasons (not-paired, role-upgrade, scope-upgrade, metadata-upgrade) all fall through to the same message-emitting branch, so there was no obvious place in the diff history where one of them was "supposed to" differentiate.

Regression Test Plan (if applicable)

  • Coverage level that should have caught this:
    • Unit test
    • Seam / integration test
    • End-to-end test
    • Existing coverage already sufficient
  • Target test or file: src/gateway/server.silent-scope-upgrade-reconnect.poc.test.ts
  • Scenario the test should lock in: a shared-auth reconnect that attempts operator.admin against a device approved for operator.read must produce both (a) res.error.message === "pairing required (scope-upgrade)" and (b) a WS close frame with code === 1008 and reason === "pairing required (scope-upgrade)".
  • Why this is the smallest reliable guardrail: it exercises the real WS path end-to-end through startServerWithClientconnectReqrequirePairing, and asserts on the exact two surfaces users see (res frame + close frame). A helper-only unit test would not catch a regression in the close-frame wiring, which is the surface the original reporter hit.
  • Existing test that already covers this: server.silent-scope-upgrade-reconnect.poc.test.ts already covered the res-frame message for scope-upgrade and not-paired reasons, but not the close frame.

User-visible / Behavior Changes

  • Gateway close messages and res error messages for pairing failures now include the specific reason, e.g.:
    • pairing required (not-paired)
    • pairing required (role-upgrade)
    • pairing required (scope-upgrade)
    • pairing required (metadata-upgrade)
  • CLI/TUI surfaces that previously printed gateway closed (1008): pairing required now print gateway closed (1008): pairing required (<reason>). This is additive — all existing .includes("pairing required") and /pairing required/i matchers continue to work unchanged.

Diagram (if applicable)

Before:
cron.add via QQ -> operator.read only -> scope-upgrade -> close(1008, "pairing required")
                                                               -> CLI shows: gateway closed (1008): pairing required  ← misleading

After:
cron.add via QQ -> operator.read only -> scope-upgrade -> close(1008, "pairing required (scope-upgrade)")
                                                               -> CLI shows: gateway closed (1008): pairing required (scope-upgrade)

Security Impact (required)

  • New permissions/capabilities? No
  • Secrets/tokens handling changed? No
  • New/changed network calls? No
  • Command/tool execution surface changed? No
  • Data access scope changed? No

Purely a human-readable error message change. Scope enforcement, pairing approval flow, and ErrorCodes.NOT_PAIRED / ConnectErrorDetailCodes.PAIRING_REQUIRED detail codes are unchanged.

Repro + Verification

Environment

  • OS: Linux (Ubuntu 22.04 on WSL2)
  • Runtime/container: Node v24.11.1, pnpm 10.32.1
  • Model/provider: N/A (gateway-side message-handler test)
  • Integration/channel (if any): N/A
  • Relevant config (redacted): N/A

Steps (automated reproduction via POC test)

  1. pnpm install --frozen-lockfile
  2. pnpm tsgo (type-check, clean)
  3. pnpm check (oxlint + boundary checks, EXIT 0)
  4. pnpm test src/gateway/server.silent-scope-upgrade-reconnect.poc.test.ts

Expected

  • All 5 POC tests pass.
  • The updated assertions bind to the new message format for both the res frame and the WS close frame.

Actual

Test Files  1 passed (1)
Tests  5 passed (5)
Duration  38.17s

Evidence

  • Failing test/log before + passing after
  • Trace/log snippets

Before (source reverted to old close(1008, "pairing required")), the new close-frame assertion fails with:

expected { code: 1008, reason: "pairing required" } to deeply equal { code: 1008, reason: "pairing required (scope-upgrade)" }

After (this PR):

✓ does not silently widen a read-scoped paired device to admin on shared-auth reconnect
✓ does not let backend reconnect bypass the paired scope baseline
✓ does not rebroadcast a deleted silent pairing request after a concurrent rejection
✓ returns the replacement pending request id when a silent request is superseded
Test Files  1 passed (1)
Tests  5 passed (5)

Broader regression check (tests that reference pairing required): TUI 27/27, CLI devices+logs 26/26, agents 5/5, full gateway project 2254/2254 passing (two unrelated pre-existing flakes openresponses-http.test.ts and gateway-server-sessions-a.test.ts reproduced on unmodified upstream/main via git stash; not introduced by this PR).

Human Verification (required)

  • Verified scenarios:
    • scope-upgrade (shared-auth reconnect, paired device with operator.read attempting operator.admin) — covered by POC test, now asserts both res-frame and close-frame.
    • not-paired (fresh identity, no paired device) — covered by POC test via updated .toBe("pairing required (not-paired)") assertion.
  • Edge cases checked:
    • Backward compat for .includes("pairing required") consumers: devices-cli.ts:110, logs-cli.ts:99, nodes-tool.ts:79, 7 control-ui suite assertions, Swift .contains("pairing required") in GatewayConnectionIssue.swift / SettingsTab.swift / ToolResultTextFormatter.swift. All still match.
    • TUI regex /pairing required/i.test(...) at tui.ts:106. Still matches.
    • Close-frame UTF-8 length: longest reason is metadata-upgrade, yielding a 35-byte close reason — well under the RFC 6455 123-byte limit. truncateCloseReason wrapper matches the pattern at message-handler.ts:486 and :571 for safety.
    • ErrorCodes.NOT_PAIRED and ConnectErrorDetailCodes.PAIRING_REQUIRED detail codes unchanged — structured consumers (UI ui/src/ui/connect-error.ts, Swift GatewayConnectAuthError) still route by code.
  • What I did NOT verify:
    • A live cron.add request through an actual QQ channel — I do not have QQ paired locally. I reproduced the underlying mechanism with the same requirePairing("scope-upgrade", paired) code path via the POC test, which hits message-handler.ts:1090 the same way cron.add does.
    • role-upgrade and metadata-upgrade reasons have no new runtime-level test of the message shape in this PR. The template-literal mechanism is identical across all four reasons, and structured details.reason coverage in server.auth.control-ui.suite.ts:1003 already validates the role-upgrade path end-to-end.

Review Conversations

  • I replied to or resolved every bot review conversation I addressed in this PR.
  • I left unresolved only the conversations that still need reviewer or maintainer judgment.

Compatibility / Migration

  • Backward compatible? Yes
  • Config/env changes? No
  • Migration needed? No

All existing consumers use substring/regex matching for "pairing required". The new suffix is additive.

Risks and Mitigations

  • Risk: a downstream consumer I missed relies on an exact-string match for "pairing required".
    • Mitigation: audited the repo — the only exact-match assertions were the 4 .toBe(...) in the POC test, which this PR updates. All other 30+ consumers (CLI, TUI, UI, iOS/macOS Swift, control-ui suite) use .includes(...), .contains(...), or regex matches, which all stay compatible with the new suffix. Structured consumers route on ErrorCodes.NOT_PAIRED / ConnectErrorDetailCodes.PAIRING_REQUIRED which are unchanged.

AI-assisted: Claude Opus 4.6. Fully tested locally (pnpm tsgo, pnpm check, pnpm test src/gateway/server.silent-scope-upgrade-reconnect.poc.test.ts). I understand the code I'm submitting.

Changed files

  • src/gateway/server.silent-scope-upgrade-reconnect.poc.test.ts (modified, +16/-4)
  • src/gateway/server/ws-connection/message-handler.ts (modified, +3/-2)

Code Example

gateway closed (1008): pairing required

---

close(1008, "pairing required");  // reason is not included in the close message
RAW_BUFFERClick to expand / collapse

Problem Description

When calling cron.add via QQ channel, users receive a misleading error message:

gateway closed (1008): pairing required

However, the actual root cause is insufficient scopes (requires operator.admin), not a pairing issue.

Steps to Reproduce

  1. Set up OpenClaw with QQ channel
  2. Agent device has only operator.read and operator.write scopes in paired.json
  3. Try to create a cron job via QQ (e.g., "remind me in 5 minutes")
  4. Error: pairing required instead of insufficient permissions

Expected vs Actual Behavior

  • Expected: Error should clearly indicate insufficient permissions (requires operator.admin for cron.add)
  • Actual: Error shows pairing required which is misleading

Root Cause Analysis

In gateway-cli-DlnlX7IW.js, the requirePairing function is called with different reason values:

  • "not-paired" - public key mismatch
  • "metadata-upgrade" - platform info changed
  • "role-upgrade" - role changed
  • "scope-upgrade" - scopes insufficient

However, all reasons result in the same error message: "gateway closed (1008): pairing required"

The error at line 26185 does not include the reason parameter:

close(1008, "pairing required");  // reason is not included in the close message

Additional Context

  • cron.list works because it only requires operator.read
  • cron.add fails because it requires operator.admin (see method-scopes-DgElWZYI.js:2645)
  • ownerAllowFrom: ["*"] is needed to bypass ownerOnly check for cron tool

Suggested Fix

  1. Include the reason in the error message: close(1008, \pairing required: ${reason}`)`
  2. Or return a more specific error code for scope insufficiency

Environment

  • OpenClaw version: 2026.3.28
  • Channel: QQ (qqbot)
  • Gateway mode: local/loopback

extent analysis

TL;DR

Modify the close function in gateway-cli-DlnlX7IW.js to include the reason parameter in the error message to provide a more accurate error description.

Guidance

  • Review the requirePairing function calls in gateway-cli-DlnlX7IW.js to ensure the reason parameter is correctly passed and utilized.
  • Update the close function at line 26185 to include the reason in the error message, such as close(1008, pairing required: ${reason}).
  • Consider introducing a more specific error code for scope insufficiency to differentiate it from other pairing-related issues.
  • Verify the fix by attempting to create a cron job via QQ with insufficient scopes and checking that the error message accurately indicates the issue.

Example

// Modified close function
close(1008, `pairing required: ${reason}`);

Notes

The suggested fix assumes that the reason parameter is correctly set when calling the requirePairing function. Additional debugging may be necessary to ensure the reason value is accurate.

Recommendation

Apply the workaround by modifying the close function to include the reason parameter, as this provides a more accurate error description and helps identify the root cause of the issue.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING