Skip to content

[Bug]: Internal gateway operations (cron, announce, sessions_spawn) fail with "pairing required" due to device scope enforcement on self-calls #21655

@ethduke

Description

@ethduke

Summary

Internal gateway operations — cron job delivery, announce queue drain, and sessions_spawn — route through the gateway's own device credentials and fail because the paired device lacks operator.write scope. The gateway is effectively blocking itself from performing write operations.

gateway install pairs the service device using CLI_DEFAULT_OPERATOR_SCOPES ([operator.admin, operator.approvals, operator.pairing]), which is correct for CLI management. But internal runtime operations (send, poll, agent, agent.wait, announce delivery) require operator.write per METHOD_SCOPE_GROUPS. The gateway enforces device scopes on its own internal calls and rejects them.

Steps to reproduce

  1. Standard server install: npm i -g openclaw && openclaw gateway install && openclaw gateway start
  2. Configure a cron job with Discord delivery target
  3. Wait for cron execution or trigger any write operation (send, announce)
  4. Gateway enters a scope-upgrade → "pairing required" retry loop

Expected behavior

Operations originating from the gateway process itself (cron, announce, subagent spawn) should execute without being blocked by device scope enforcement. The gateway is the authority that enforces scopes — it shouldn't need to authorize against itself.

Actual behavior

Gateway repeatedly fails with scope-upgrade rejection:

warn  gateway  security audit: device access upgrade requested
      reason=scope-upgrade
      device=bd129ab6...
      scopesFrom=operator.admin,operator.approvals,operator.pairing
      scopesTo=operator.write
error gateway  connect failed: Error: pairing required
error gateway  connect failed: Error: pairing required
error announce  queue drain failed for agent:main:discord:channel:1469556013123502103:
      Error: gateway closed (1008): pairing required

OpenClaw version

2026.2.19-2 (45d9b20)

Operating system

Ubuntu 24.04.3 LTS (noble), kernel 6.14.0-1018-aws, x86_64

Install method

npm i -g openclaw, systemd service via openclaw gateway install

Logs, screenshots, and evidence

Device scopes after fresh gateway install — no operator.write:

json{
    "scopes": ["operator.admin", "operator.approvals", "operator.pairing"],
    "tokens": {
        "operator": {
            "scopes": ["operator.admin", "operator.approvals", "operator.pairing"]
        }
    }
}

Auto-approve only handles not-paired — gateway-cli-B1gfvmQb.js:19521:

silent: isLocalClient && reason === "not-paired"

Local clients get auto-approved when they first pair (not-paired), but scope upgrades always go to the reject path — even for local/internal connections.

Note: a simple fix of extending this to reason === "not-paired" || reason === "scope-upgrade" would weaken security — an external process on the same host could silently escalate scopes. The current auto-approve for not-paired already has this gap (new device IDs get auto-approved with any scopes), but widening it further isn't ideal.

Impact and severity

Affected users: Every fresh gateway install that uses cron jobs, announce/Discord delivery, or subagent spawning — likely the majority of headless/server deployments
Severity: Blocks workflow. Cron delivery, Discord output, and subagent spawning are completely broken out of the box with no warning
Frequency: Always. 100% reproducible on any fresh install
Consequence: Missed Discord messages, silent cron failures, subagent spawn failures. Difficult to diagnose — the "pairing required" error suggests an auth/pairing problem rather than a scope omission. No documentation mentions that operator.write needs to be manually added after install
Workaround is fragile: Any subsequent gateway install --force, openclaw update + doctor --repair, or backup restore will re-create the paired device with default scopes, silently reverting the fix

Additional information

suggested fix: In src/gateway/server/ws-connection/message-handler.ts, skip device scope enforcement for internal gateway-client connections (same pattern as the existing allowControlUiBypass):

// Current:
if (device && devicePublicKey && !(allowControlUiBypass && sharedAuthOk)) {

// Fixed:
const isInternalGatewayClient = isLocalClient && connectParams.client.id === "gateway-client";
if (device && devicePublicKey && !(allowControlUiBypass && sharedAuthOk) && !isInternalGatewayClient) {

So when the gateway's own cron/announce/subagent connects (client.id === "gateway-client" + isLocalClient), the condition becomes false → skips the entire scope check block → connection proceeds without checking paired.json scopes at all → no "scope-upgrade" → no "pairing required".

Workaround

openclaw devices rotate --device <id> --role operator \
  --scope operator.admin \
  --scope operator.approvals \
  --scope operator.pairing \
  --scope operator.write

openclaw gateway restart

Device ID via openclaw devices list. If gateway is down, manually add operator.write to both scope arrays in ~/.openclaw/devices/paired.json.

Related issues:

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingstaleMarked as stale due to inactivity

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions