openclaw - 💡(How to fix) Fix openclaw cron run: websocket handshake fails (protocol mismatch + missing device auth) [3 comments, 3 participants]

Official PRs (…)
ON THIS PAGE

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#49291Fetched 2026-04-08 00:56:53
View on GitHub
Comments
3
Participants
3
Timeline
5
Reactions
0
Timeline (top)
commented ×3mentioned ×1subscribed ×1

Root Cause

Root Cause (Debugged)

Fix Action

Workaround

I wrote a Node.js script that implements the full v3 handshake with device signing and successfully triggers cron jobs:

// Key connect frame structure that works:
{
  type: 'req', id: '1', method: 'connect',
  params: {
    minProtocol: 3, maxProtocol: 3,
    client: { id: 'cli', version: '2026.3.13', platform: 'linux', mode: 'backend' },
    caps: [],
    auth: { token: gatewayToken, deviceToken: storedDeviceToken },
    role: 'operator',
    scopes: ['operator.admin'],
    device: { id: deviceId, publicKey, signature, signedAt, nonce }
  }
}

Code Example

$ openclaw cron run gnl-pending-matters
# Hangs, then fails silently

---

[ws] handshake timeout
[ws] closed before connect conn=... remote=127.0.0.1

---

{"code": "INVALID_REQUEST", "message": "protocol mismatch", "details": {"expectedProtocol": 3}}

---

{"code": "INVALID_REQUEST", "message": "missing scope: operator.admin"}

---

payload = "v3|deviceId|clientId|clientMode|role|scopes|signedAtMs|token|nonce|platform|deviceFamily"
signature = Ed25519.sign(privateKey, payload)

---

{
  "device": {
    "id": "<deviceId>",
    "publicKey": "<base64url raw public key>",
    "signature": "<base64url Ed25519 signature>",
    "signedAt": <timestampMs>,
    "nonce": "<from connect.challenge>"
  }
}

---

// Key connect frame structure that works:
{
  type: 'req', id: '1', method: 'connect',
  params: {
    minProtocol: 3, maxProtocol: 3,
    client: { id: 'cli', version: '2026.3.13', platform: 'linux', mode: 'backend' },
    caps: [],
    auth: { token: gatewayToken, deviceToken: storedDeviceToken },
    role: 'operator',
    scopes: ['operator.admin'],
    device: { id: deviceId, publicKey, signature, signedAt, nonce }
  }
}
RAW_BUFFERClick to expand / collapse

Bug Description

openclaw cron run <job-name> fails to connect to the local gateway. The CLI websocket handshake times out, and the job is never triggered.

Version: OpenClaw 2026.3.13 (61d171a)
Platform: Linux (Ubuntu, amd64)
Gateway: Running, healthy, dispatches scheduled cron jobs normally

Symptoms

$ openclaw cron run gnl-pending-matters
# Hangs, then fails silently

Gateway logs show:

[ws] handshake timeout
[ws] closed before connect conn=... remote=127.0.0.1

Root Cause (Debugged)

Through reverse-engineering the gateway protocol, I identified two issues in the CLI→gateway websocket connect flow:

1. Protocol version mismatch

The CLI sends a connect frame with minProtocol: 1, maxProtocol: 1, but the gateway requires protocol version 3.

Gateway rejects with:

{"code": "INVALID_REQUEST", "message": "protocol mismatch", "details": {"expectedProtocol": 3}}

2. Missing device identity signing (no operator.admin scope)

Even after fixing the protocol version, sending a connect frame with only auth.token (no device identity) results in the client not receiving operator.admin scope. The cron.run method requires this scope:

{"code": "INVALID_REQUEST", "message": "missing scope: operator.admin"}

The gateway's connect handler requires a signed device payload (v3 format) with Ed25519 signature for admin scope:

payload = "v3|deviceId|clientId|clientMode|role|scopes|signedAtMs|token|nonce|platform|deviceFamily"
signature = Ed25519.sign(privateKey, payload)

The connect frame must include a device object:

{
  "device": {
    "id": "<deviceId>",
    "publicKey": "<base64url raw public key>",
    "signature": "<base64url Ed25519 signature>",
    "signedAt": <timestampMs>,
    "nonce": "<from connect.challenge>"
  }
}

3. Nonce extraction

The challenge event structure is { type: "event", event: "connect.challenge", payload: { nonce: "..." } } — the nonce is in msg.payload.nonce, which the CLI may be reading from the wrong path.

Workaround

I wrote a Node.js script that implements the full v3 handshake with device signing and successfully triggers cron jobs:

// Key connect frame structure that works:
{
  type: 'req', id: '1', method: 'connect',
  params: {
    minProtocol: 3, maxProtocol: 3,
    client: { id: 'cli', version: '2026.3.13', platform: 'linux', mode: 'backend' },
    caps: [],
    auth: { token: gatewayToken, deviceToken: storedDeviceToken },
    role: 'operator',
    scopes: ['operator.admin'],
    device: { id: deviceId, publicKey, signature, signedAt, nonce }
  }
}

Impact

  • openclaw cron run is completely broken for manual job triggers
  • Scheduled cron runs work fine (gateway dispatches internally)
  • Affects any CLI→gateway websocket operation that requires operator.admin scope

Expected Behavior

openclaw cron run <job-name> should successfully connect to the gateway and trigger the job.

Steps to Reproduce

  1. Have a running gateway (v2026.3.13)
  2. Have at least one enabled cron job
  3. Run openclaw cron run <job-name>
  4. Observe handshake timeout in gateway logs

extent analysis

Fix Plan

To fix the openclaw cron run issue, we need to update the CLI to use protocol version 3 and include a signed device payload in the connect frame. Here are the steps:

  • Update the protocol version in the connect frame to minProtocol: 3, maxProtocol: 3.
  • Generate a signed device payload using Ed25519 signature.
  • Include the device object in the connect frame with the required fields: id, publicKey, signature, signedAt, and nonce.

Example code:

const crypto = require('crypto');
const ed25519 = require('ed25519');

// Generate a signed device payload
const deviceId = 'your-device-id';
const privateKey = 'your-private-key';
const publicKey = 'your-public-key';
const nonce = 'nonce-from-connect-challenge';
const signedAt = Date.now();
const token = 'your-gateway-token';

const payload = `v3|${deviceId}|cli|backend|operator|operator.admin|${signedAt}|${token}|${nonce}|linux|your-device-family`;
const signature = ed25519.sign(privateKey, payload);

// Create the connect frame
const connectFrame = {
  type: 'req',
  id: '1',
  method: 'connect',
  params: {
    minProtocol: 3,
    maxProtocol: 3,
    client: { id: 'cli', version: '2026.3.13', platform: 'linux', mode: 'backend' },
    caps: [],
    auth: { token: token, deviceToken: 'your-stored-device-token' },
    role: 'operator',
    scopes: ['operator.admin'],
    device: {
      id: deviceId,
      publicKey: publicKey,
      signature: signature.toString('base64url'),
      signedAt: signedAt,
      nonce: nonce
    }
  }
};

Verification

To verify that the fix worked, run the openclaw cron run command and check the gateway logs for a successful connection and job trigger.

Extra Tips

  • Make sure to update the openclaw CLI to use the latest protocol version and include the signed device payload in the connect frame.
  • Verify that the gatewayToken and storedDeviceToken are valid and correctly configured.
  • If you encounter any issues with the Ed25519 signature, ensure that the privateKey and publicKey are correctly generated and formatted.

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