#Gateway nodes roles confusion

1 messages · Page 1 of 1 (latest)

grim fox
#

Pinging @little oar for clarification on how to set up my openclaw.json config for the Android companion app. It has all Android system permissions but no gateway permissions.

e.g. can't use camera:
node.invoke 0ms errorCode=INVALID_REQUEST errorMessage=node command not allowed: "camera.snap" is not in the allowlist for platform "android" conn=a2bf560c…2d88 id=00f8bfe6…cd43

can't use talk mode:
node.event 0ms errorCode=INVALID_REQUEST errorMessage=unauthorized role: operator unauthorizedCount=1 conn=416e7a9b…c2b9 id=0bc3c655…9994

Thanks

alpine palmBOT
#

To help others find answers, you can mark your question as solved via Right click solution message -> Apps -> âś… Mark Solution

little oarBOT
#

You’re hitting two separate Gateway gates here:

  1. Android OS permissions only let the app offer the capability. The Gateway still blocks privacy-heavy node commands unless you opt in.

For camera, add the command to ~/.openclaw/openclaw.json:

{
  gateway: {
    nodes: {
      allowCommands: [
        "camera.snap",
        "camera.clip" // only if you want video too
      ]
    }
  }
#
}

camera.snap / camera.clip are intentionally not default-allowed on Android. After changing this, restart the Gateway, reconnect the Android app, then re-check/approve pairing if a new request appears:

openclaw --version
openclaw gateway restart
openclaw devices list
openclaw devices approve <requestId>
openclaw nodes status
  1. Talk mode uses both sides of the Android app’s connection model:
  • role: node for node-originated events / node capabilities
  • role: operator for UI/chat/talk control-plane calls with scopes like operator.read, operator.write, operator.talk.secrets
#

So unauthorized role: operator on node.event means that specific RPC is arriving on an operator-authenticated connection, but node.event is node-only. Usually I’d check for a stale/partial pairing or a role/scope upgrade pending in openclaw devices list. Approve the current Android role: node request, and also approve the Android operator scope request if it asks for read/write/talk scopes.

If it still fails after that, paste openclaw --version plus the relevant openclaw devices list rows. Version matters here because node/device pairing changed recently.

Docs: https://docs.openclaw.ai/platforms/android, https://docs.openclaw.ai/nodes, https://docs.openclaw.ai/gateway/operator-scopes

grim fox
#

I don't see a pending device request but I do see a pending node request. However I can't approve it.

% openclaw nodes approve 21ce319a-0d11-4f7f-8f59-4bf5a9b55ad9

🦞 OpenClaw 2026.5.10-beta.1 (94b4312) — I can run local, remote, or purely on vibes—results may vary with DNS.

│
â—‡
nodes approve failed: GatewayClientRequestError: missing scope: operator.write

little oarBOT
#

Yep, that error is actually the key clue: your CLI can reach the pairing API, but the auth context it’s using does not have operator.write.

For openclaw nodes approve, a node that declares non-exec commands requires:

  • operator.pairing
  • operator.write

That’s why you can see the pending node request but can’t approve it.

Most likely the CLI is using a limited paired-device/local fallback token instead of the shared Gateway token/password. Try approving with the Gateway auth secret explicitly:

openclaw nodes approve 21ce319a-0d11-4f7f-8f59-4bf5a9b55ad9 --token "$OPENCLAW_GATEWAY_TOKEN"

or, if your gateway uses password auth:

#
openclaw nodes approve 21ce319a-0d11-4f7f-8f59-4bf5a9b55ad9 --password "$OPENCLAW_GATEWAY_PASSWORD"

If you don’t have a stable shared token/password configured yet, that’s the thing to fix first. Check:

openclaw config get gateway.auth.mode
openclaw config get gateway.auth.token
openclaw config get gateway.auth.password

After approval, camera still separately needs the gateway command opt-in:

{
grim fox
#

yea for some reason my CLI only has operator.read

little oarBOT
#

That explains it. Your CLI is authenticated as a paired operator device with only operator.read, so it can inspect but not mutate/approve.

Fastest fix: bypass that limited device token and use the Gateway’s shared secret for this approval:

openclaw nodes approve 21ce319a-0d11-4f7f-8f59-4bf5a9b55ad9 --token "$(openclaw config get gateway.auth.token)"

If the token is a SecretRef/env var, use the env var directly instead:

OPENCLAW_GATEWAY_TOKEN=... openclaw nodes approve 21ce319a-0d11-4f7f-8f59-4bf5a9b55ad9
# or
openclaw nodes approve 21ce319a-0d11-4f7f-8f59-4bf5a9b55ad9 --token "$OPENCLAW_GATEWAY_TOKEN"

Longer-term, re-pair/rotate the CLI/operator device so it has at least:

#
operator.read
operator.write
operator.pairing

But since your current CLI only has read, it can’t grant itself write. You need to come in through shared Gateway token/password, an admin/write-capable operator device, or remove/re-pair the stale limited CLI device.

Also: don’t paste the token here — just use it locally. 🦞

grim fox
#

believe it or not but even with --token I get the missing scope error

little oarBOT
#

Yep — I found the likely bug. nodes approve connects as CLI and asks the Gateway for the least-privilege method scope for node.pair.approve, which is only operator.pairing. But the approval-time check for a node that declared commands also requires operator.write. So even with --token, the client can still negotiate too-narrow scopes and then fail at the second gate.

Workaround: make one direct backend WS call that explicitly requests operator.pairing + operator.write and omits the stale CLI device identity. Run this locally on the gateway host; don’t paste the token here:

REQ_ID='21ce319a-0d11-4f7f-8f59-4bf5a9b55ad9' \
OPENCLAW_GATEWAY_TOKEN='your_gateway_token_here' \
GATEWAY_URL='ws://127.0.0.1:18789' \
node <<'NODE'
const reqId = process.env.REQ_ID;
const token = process.env.OPENCLAW_GATEWAY_TOKEN;
const url = process.env.GATEWAY_URL || 'ws://127.0.0.1:18789';
if (!reqId || !token) throw new Error('Set REQ_ID and OPENCLAW_GATEWAY_TOKEN');
if (!globalThis.WebSocket) throw new Error('Need Node 22+ with global WebSocket');

const ws = new WebSocket(url);
#
let nextId = 1;
const pending = new Map();

function rpc(method, params) {
  const id = String(nextId++);
  ws.send(JSON.stringify({ type: 'req', id, method, params }));
  return new Promise((resolve, reject) => pending.set(id, { resolve, reject }));
}

ws.addEventListener('open', async () => {
  try {
    await rpc('connect', {
      minProtocol: 4,
      maxProtocol: 4,
      client: { id: 'gateway-client', version: 'manual', platform: process.platform, mode: 'backend' },
#
      role: 'operator',
      scopes: ['operator.read', 'operator.write', 'operator.pairing'],
      caps: [],
      commands: [],
      permissions: {},
      auth: { token },
      locale: 'en-US',
      userAgent: 'manual-node-pair-approve/1.0'
    });
    const res = await rpc('node.pair.approve', { requestId: reqId });
    console.log(JSON.stringify(res, null, 2));
    ws.close();
  } catch (err) {
    console.error(err);
    process.exitCode = 1;
#
    ws.close();
  }
});

ws.addEventListener('message', (ev) => {
  const msg = JSON.parse(ev.data);
  if (msg.type !== 'res') return;
  const p = pending.get(msg.id);
  if (!p) return;
  pending.delete(msg.id);
  msg.ok ? p.resolve(msg.payload) : p.reject(new Error(msg.error?.message || JSON.stringify(msg.error)));
});

ws.addEventListener('error', (err) => {
  console.error('WebSocket error', err.message || err);
#
  process.exitCode = 1;
});
NODE

After that, restart/reconnect Android and openclaw nodes status should show it paired.

This is worth filing as a GitHub issue: openclaw nodes approve should request/retain operator.write when approving pending node commands, or retry with the required approval-time scopes. https://github.com/openclaw/openclaw/issues

grim fox
#

how do I restore permissions for my CLI on the gateway host?

little oarBOT
#

Yeah — to restore the CLI, you need to approve a device scope upgrade for the CLI’s own device identity, then clear its stale cached read-only token.

I’d do it like this on the gateway host:

  1. Back up the stale cached CLI device token:
mkdir -p ~/.openclaw/identity/backup
mv ~/.openclaw/identity/device-auth.json ~/.openclaw/identity/backup/device-auth.$(date +%s).json 2>/dev/null || true
  1. Trigger a CLI operator scope-upgrade request. This is intentionally a mostly-harmless write-scoped RPC; it may fail after auth, that’s okay — the goal is to create the pending upgrade:
openclaw gateway call chat.abort \
  --params '{"sessionKey":"__noop__"}' \
#
  --token "$OPENCLAW_GATEWAY_TOKEN"
  1. Check pending device requests:
openclaw devices list

Look for the CLI/operator request asking for operator.write / operator.pairing.

  1. Because your normal CLI path is currently scope-wedged, approve that device request through the backend loopback path:
REQ_ID='paste_device_request_id_here' \
#
OPENCLAW_GATEWAY_TOKEN='your_gateway_token_here' \
GATEWAY_URL='ws://127.0.0.1:18789' \
node <<'NODE'
const reqId = process.env.REQ_ID;
const token = process.env.OPENCLAW_GATEWAY_TOKEN;
const url = process.env.GATEWAY_URL || 'ws://127.0.0.1:18789';
if (!reqId || !token) throw new Error('Set REQ_ID and OPENCLAW_GATEWAY_TOKEN');

const ws = new WebSocket(url);
let nextId = 1;
const pending = new Map();
function rpc(method, params) {
  const id = String(nextId++);
  ws.send(JSON.stringify({ type: 'req', id, method, params }));
  return new Promise((resolve, reject) => pending.set(id, { resolve, reject }));
#
}
ws.addEventListener('open', async () => {
  try {
    await rpc('connect', {
      minProtocol: 4,
      maxProtocol: 4,
      client: { id: 'gateway-client', version: 'manual', platform: process.platform, mode: 'backend' },
      role: 'operator',
      scopes: ['operator.admin', 'operator.read', 'operator.write', 'operator.pairing'],
      caps: [], commands: [], permissions: {},
      auth: { token },
      locale: 'en-US'
    });
    console.log(await rpc('device.pair.approve', { requestId: reqId }));
    ws.close();
#
  } catch (e) { console.error(e); process.exitCode = 1; ws.close(); }
});
ws.addEventListener('message', (ev) => {
  const msg = JSON.parse(ev.data);
  if (msg.type !== 'res') return;
  const p = pending.get(msg.id); if (!p) return;
  pending.delete(msg.id);
  msg.ok ? p.resolve(msg.payload) : p.reject(new Error(msg.error?.message || JSON.stringify(msg.error)));
});
NODE
  1. Run one write-scoped CLI command once with the shared token so it stores a fresh operator token:
#
openclaw gateway call chat.abort \
  --params '{"sessionKey":"__noop__"}' \
  --token "$OPENCLAW_GATEWAY_TOKEN"

After that, openclaw devices list should show the CLI/operator token with broader scopes, and openclaw nodes approve ... should stop hitting missing scope: operator.write.

If step 2 doesn’t create a pending device request, tell me what openclaw devices list --json shows for the CLI row, with tokens redacted.

grim fox
#

how do I repair the CLI to have operator.write / operator.admin?

little oarBOT
#

You can’t safely “upgrade” a read-only CLI from itself. Treat it as a device scope upgrade: make the CLI request broader scopes, then approve that request using a trusted shared Gateway token/password or an already-admin operator.

For your case, I’d do this:

  1. Clear only the stale cached CLI token, not the device identity:
mkdir -p ~/.openclaw/identity/backup
mv ~/.openclaw/identity/device-auth.json ~/.openclaw/identity/backup/device-auth.$(date +%s).json 2>/dev/null || true
  1. Trigger an admin-scope request from the CLI. update.status is admin-scoped and relatively safe/read-like:
openclaw gateway call update.status --token "$OPENCLAW_GATEWAY_TOKEN"
#

It may fail with pairing/scope-upgrade required. That’s okay; it should create a pending device request.

  1. Find the pending CLI/operator request:
openclaw devices list

Look for the CLI device requesting operator.admin — admin satisfies operator.write, operator.read, operator.pairing, etc.

  1. Approve that device request using the backend/shared-token path. If normal openclaw devices approve <requestId> --token ... works, use that. If it still gets scope-wedged, use the manual backend script from above but call:
device.pair.approve

with:

#
{ "requestId": "<device-request-id>" }
  1. Run the admin-scoped call once more so the CLI stores the fresh operator token:
openclaw gateway call update.status --token "$OPENCLAW_GATEWAY_TOKEN"

After that, ~/.openclaw/identity/device-auth.json should contain a new operator token for the same CLI device, and normal CLI calls should have operator.admin.

If you only need Android node approval, operator.write + operator.pairing is enough; operator.admin is the “full control” repair path, so only grant it on your gateway host / trusted machine.

grim fox
#

still zero luck getting talk mode to work on android

23:08:47+00:00 info gateway/ws {"subsystem":"gateway/ws"} ⇄ res ✗ node.event 0ms errorCode=INVALID_REQUEST errorMessage=unauthorized role: operator unauthorizedCount=1 conn=54183126…7e29 id=4049aabe…313b

echo@mini ~ % openclaw nodes list  
Pending: 1 · Paired: 1

Pending
│ Request                              │ Node                                                                                                                                                                                                   │ IP           │ Requested  │
│ 84d65b0a-665f-4fe7-b4f1-fe4c6edbbb2f │ S26 van Steven                                                                                                                                                                                         │ 192.168.1.62 │ just now   │

Paired
│ Node                                                                                                                                                                     │ ID                                                               │ IP           │ Last Connect │
│ S26 van Steven                                                                                                                                                           │ c18bf76110bd6bec0f3edd316d9aa3c61e79105636318417bb1939eecf626717 │ 192.168.1.62 │ just now     │

echo@mini ~ % openclaw nodes approve 84d65b0a-665f-4fe7-b4f1-fe4c6edbbb2f

🦞 OpenClaw 2026.5.10-beta.1 (0bc2503) — WhatsApp automation without the "please accept our new privacy policy".

│
â—‡  
nodes approve failed: GatewayClientRequestError: missing scope: operator.write

that pending node request keeps popping up every time I try but approving it doesn't do shit, not even with your scripted workarounds

little oarBOT
#

Okay — I think we should stop chasing pairing for Talk Mode. This specific error is not fixed by approving the node request:

node.event ... unauthorized role: operator

That means Android is sending node.event over the operator WebSocket. The Gateway is correctly rejecting that, because node.event is node-role only.

I checked the Android source path and this looks like a real app bug in this build:

  • NodeRuntime creates two sessions: operatorSession and nodeSession
  • Talk Mode is constructed with session = operatorSession
  • TalkModeManager.subscribeChatIfNeeded() calls:
session.sendNodeEvent("chat.subscribe", ...)
#

So Talk Mode tries to send the node-only chat.subscribe event through the operator connection. That exactly matches your log. The pending node request is probably a separate/repeated node-pairing annoyance, but it won’t make an operator connection authorized for node.event.

So: I don’t think there is an openclaw.json permission fix for Android Talk Mode on 2026.5.10-beta.1 / this commit. 🫠

Practical options:

  1. Use the Android Mic mode for now instead of continuous Talk Mode. Mic uses chat.send through the operator session and should avoid this node.event path.

  2. If you’re building Android from source, patch Talk Mode so subscription uses the node session and chat send still uses the operator session. Conceptually:

  • keep chat.send / chat.history on operatorSession
  • send chat.subscribe node events on nodeSession
  • make nodeSession.onEvent forward chat events into the same handleGatewayEvent(...) path instead of ignoring them
  1. Please file this as a GitHub issue with your log + version: https://github.com/openclaw/openclaw/issues

Title suggestion: