#Headless node instant-denies all exec (ask: always + no prompt surface) — no approval card in Discor

1 messages · Page 1 of 1 (latest)

keen ermine
#

Description:
What I'm trying to do: Message my Discord bot to run commands on a remote headless node (Oracle Cloud VPS), and either have trusted commands run, or get an approval prompt I can actually action.
Setup:

OpenClaw 2026.5.7
Gateway: Docker on Unraid (ghcr.io/openclaw/openclaw:latest)
One node: headless VPS, openclaw-node.service as a systemd user service, paired + connected, advertises system.run
Channel: Discord DM

What happened: Every node.invoke for a non-allowlisted command returned SYSTEM_RUN_DENIED: approval required instantly — no exec.approval.waitDecision in the gateway logs, meaning no approval was ever created. So no card appeared in Discord or the Control UI, because there was no event to forward.
Root cause I found: The node's local ~/.openclaw/exec-approvals.json had defaults: { security: "full", ask: "always", askFallback: "deny" }. On a headless node there's no companion app / prompt surface, so ask: "always" → can't prompt → askFallback: "deny" fires before the gateway's approval handshake ever runs. The gateway's ask: "on-miss" in the invoke params is overridden by the node-local ask: "always", per the docs.
What fixed the deny: Changed the node's exec-approvals.json to security: "allowlist", ask: "on-miss", with an allowlist array for the commands I actually use. Restarted openclaw-node.service. Allowlisted commands now run with no prompt.
Questions for the team:

Is there a supported way to get a working approval prompt (Discord card or Control UI) for non-allowlisted commands on a headless node? ask: "always" + askFallback: "deny" seems to make that structurally impossible since the node denies before any approval is broadcast.
For the native Discord card path (channels.discord.execApprovals), is the correct config to disable approvals.exec and rely only on channels.discord.execApprovals? I've seen conflicting setups.
Should the docs call out that node-local ask: "always" on a headless node = guaranteed deny? It's a sharp edge.

stark vaporBOT
#

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

south bobcatBOT
#

Your diagnosis matches the docs/current source pretty well: if the execution host’s local policy requires a prompt and there’s no reachable prompt surface, askFallback wins. With ask: "always" + askFallback: "deny" on a headless node, that’s effectively a guaranteed deny before there’s anything useful to click.

A few important distinctions:

  1. For a working Discord/Control UI approval, the approval has to be created by the Gateway exec approval flow. In normal use that means using the exec tool with host=node / tools.exec.host: "node", not calling raw node.invoke system.run yourself and expecting Discord to invent an approval after the node rejects it.

  2. The node-local ~/.openclaw/exec-approvals.json is still enforceable. For headless nodes, the safe pattern is exactly what you moved toward: security: "allowlist", ask: "on-miss", explicit allowlist for trusted commands. If you want non-allowlisted commands to become Gateway/Discord approvals, make sure the request is going through the exec approval path and that the node supports the prep/approval commands (system.run.prepare, system.execApprovals.get/set) for your version.

  3. For Discord native cards: don’t rely on approvals.exec unless you want generic forwarding to extra chats/rooms. The native Discord path is channels.discord.execApprovals. Leave enabled unset/"auto" or set true, and make sure approvers resolve via channels.discord.execApprovals.approvers or commands.ownerAllowFrom. approvals.exec is routing/forwarding; channels.discord.execApprovals is the native Discord approval client.

So: no, ask: "always" on a headless node is not the right way to get Discord cards. It asks the node-local prompt layer, and if there’s no UI, fallback decides. And yes, I agree this deserves a loud docs callout — “node-local ask: always + headless + askFallback: deny = instant deny/no approval card” is absolutely a sharp edge.

Docs refs:

keen ermine
#

OpenClaw 2026.5.7, Docker gateway + one headless Linux node (Oracle VPS). Trying to get native Discord exec approval cards for commands run on the node.
Current config: channels.discord.execApprovals = {enabled: true, approvers: ["<id>"], target: "dm"}, tools.exec.host: "node", gateway.nodes.allowCommands includes system.run.prepare, approvals.exec removed. Node's local exec-approvals.json is security: full, ask: always, askFallback: deny. "Discord Native Approvals" shows as a loaded backend with operator.approvals scope.
Questions:

With this setup, should a non-allowlisted command on the node produce a Discord DM card? If not, what's missing?
Does the node need system.execApprovals.get/set (it only advertises system.run, system.run.prepare, system.which)? Mine doesn't have those — can the card path work without them?
What's the correct CLI command on 2026.5.7 to list/approve pending device pairing + scope-upgrade requests? (openclaw device pair list returns "Unknown command".)

south bobcatBOT
#

Short version: with only the node-local ask: "always", no — I would not expect a Discord DM card.

The Discord card is created by the Gateway exec approval flow. The node-local ~/.openclaw/exec-approvals.json is the execution-host guardrail, but on a headless node ask: "always" + askFallback: "deny" means “if nobody already approved this through the Gateway path, deny locally.” It does not itself create a Discord approval.

What I’d change/check:

  1. Make the Gateway-side exec policy require approval too, e.g. for non-allowlisted commands:
tools: {
  exec: {
    host: "node",
    security: "allowlist",
    ask: "on-miss"
  }
}
#

or ask: "always" if you want every node command to card. If Gateway effective policy is still security: "full" + ask: "off", it will try to run immediately and then the headless node’s local fallback-deny catches it.

  1. Keep channels.discord.execApprovals for the native DM card path. You do not need approvals.exec unless you want generic forwarding to extra destinations. Your Discord config shape sounds right if the approver ID is the numeric Discord user id.

  2. gateway.nodes.allowCommands should allow the exec path commands. At minimum you’ll need system.run.prepare for the pre-approval plan and system.run for the approved execution. system.which is commonly useful too.

On system.execApprovals.get/set: it’s not what renders the card. The docs say current headless nodes should advertise it, and the Control UI / openclaw approvals --node ... need it to read/edit the node’s approval file remotely. The exec approval card path can still create an approval without it, but without get/set the Gateway can’t pre-read the node allowlist/durable approvals, and remote editing won’t work. If your headless node only advertises system.run, system.run.prepare, system.which, I’d suspect the node service binary/version is older or mismatched versus the Gateway. Reinstall/restart the node with the same OpenClaw version if possible.

Correct CLI for device pairing / scope upgrades is plural devices, not device pair:

openclaw devices list
openclaw devices approve <requestId>
openclaw devices reject <requestId>

Node-focused aliases also exist in current docs:

#
openclaw nodes pending
openclaw nodes approve <requestId>

For system.run / system.run.prepare scope upgrades, approval may require operator.admin in addition to pairing scope, so if devices list can see the request but approve fails, check the token/scopes you’re using against the Docker Gateway.

keen ermine
#

how do i accepty a pending node with request id 5553d885-67a0-44d6-959c-9eb73ee18306

south bobcatBOT
#

Run this from a machine/terminal that can talk to your Gateway with pairing/admin scope:

openclaw devices approve 5553d885-67a0-44d6-959c-9eb73ee18306

Or with the node-focused alias:

openclaw nodes approve 5553d885-67a0-44d6-959c-9eb73ee18306

If you’re running the Gateway in Docker and only have the CLI inside the container, exec into the container and run the same command there. Before approving, it’s worth confirming the request is still current:

openclaw devices list
#

If approval fails with a scope/permission error, the token you’re using needs operator.pairing and possibly operator.admin for node exec capabilities like system.run / system.run.prepare.

keen ermine
#

Tried openclaw nodes approve <id> inside the Docker container — fails with missing scope: operator.admin. The container CLI only has the gateway token's scopes. What's the intended way to grant operator.admin for approving node scope-upgrades on a Dockerized gateway — approve via Control UI, or is there a token/env setting for the container CLI?

south bobcatBOT
#

That error means the client doing the approval is authenticated as a limited operator. You can’t “upgrade yourself” from that token — the approve call for a node that requests system.run / system.run.prepare needs an admin-capable operator credential.

Intended paths:

  1. Use the shared Gateway auth secret with the CLI

For token auth, that’s gateway.auth.token / OPENCLAW_GATEWAY_TOKEN, not a paired-device/operator token. Shared-secret auth should have the full default operator scopes, including operator.admin.

If you pass --url, the CLI does not fall back to config/env creds, so pass the token explicitly:

openclaw devices approve 5553d885-67a0-44d6-959c-9eb73ee18306 \
  --url ws://127.0.0.1:18789 \
  --token "$OPENCLAW_GATEWAY_TOKEN"

Adjust the URL for Docker networking if you’re using a separate CLI container, e.g. the service name instead of 127.0.0.1.

#
  1. Use Control UI, but only if that browser/session is authenticated with admin-capable Gateway access

Control UI is fine, but it’s not magic: if the UI is paired with only operator.pairing / operator.approvals, it’ll hit the same admin scope wall for exec-capable node upgrades.

  1. Docker-specific sanity check

Inside the gateway container, check what auth mode/source you’re actually using:

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

If the token is configured via env/SecretRef, make sure the CLI container also receives that env var, or pass --token manually. If you’re using password auth instead, use --password / OPENCLAW_GATEWAY_PASSWORD.

Docs refs: https://docs.openclaw.ai/cli/devices and https://docs.openclaw.ai/gateway/protocol

keen ermine
#

Node scope-upgrade requests expire before I can approve them — how do I approve reliably?
OpenClaw 2026.5.7, Docker gateway + headless Linux node ("ziost VPS"). The node requests a scope upgrade (operator.pairing → needs operator.read etc. for system.run/system.run.prepare). openclaw devices list shows a Pending request, but by the time I run openclaw devices approve <id> --url ws://127.0.0.1:18789 --token "$OPENCLAW_GATEWAY_TOKEN" it fails with unknown requestId — the ID has already expired. The node re-requests periodically but each ID is short-lived.
Questions:

Is there a way to extend the request TTL, or to pre-authorize/persist a node's scope grant so it doesn't need re-approval each cycle?
Is there a non-request-ID approval — e.g. approve the node device directly by its device ID rather than a transient requestId?
What's the intended approval flow for a headless node on a Dockerized gateway where the operator can't sit and race the Pending list?

south bobcatBOT
#

The request-id flow is intentionally transient. From current docs/source:

  • Device pairing/scope-upgrade pending requests expire after about 5 minutes.
  • There isn’t a supported config knob to extend that TTL in 2026.5.7.
  • There isn’t a “approve by deviceId” command for upgrades; approval is bound to the exact pending requestId.
  • openclaw devices approve --latest is preview-only, not an implicit approve, by design.

Once you successfully approve the current upgrade request, the grant is persisted in the device pairing record. So you should only have to race it once, unless the node keeps changing approval-bound identity/details.

For a headless/Docker setup, I’d use a tiny poll-and-approve loop with the admin-capable shared Gateway token, filtered to the exact node name/device. Example:

#!/usr/bin/env bash
set -euo pipefail

URL='ws://127.0.0.1:18789'
#
TOKEN="$OPENCLAW_GATEWAY_TOKEN"
NODE_NAME='ziost VPS'

while true; do
  id=$(
    openclaw devices list --json --url "$URL" --token "$TOKEN" |
      jq -r --arg name "$NODE_NAME" '
        .pending[]?
        | select(.displayName == $name or .deviceId == $name)
        | .requestId
      ' |
      head -n1
  )

  if [ -n "${id:-}" ]; then
#
    echo "Approving $id for $NODE_NAME"
    if openclaw devices approve "$id" --url "$URL" --token "$TOKEN"; then
      break
    fi
    echo "Request changed/expired; retrying..."
  fi

  sleep 1
done

If you have the stable deviceId, filter on that instead of display name — safer if multiple nodes have similar names.

One important clue: if the request ID changes much faster than 5 minutes, that usually means the node is not just refreshing the same request. It may be reconnecting with changed approval-bound details such as public key, role/scopes, or a non-persistent node identity/token. Check that the VPS service has a stable ~/.openclaw/node.json and isn’t starting with a new node id/public key each cycle.

Also: for exec-capable node requests (system.run, system.run.prepare, system.which), the approving client needs operator.admin. Use the shared Gateway auth secret (gateway.auth.token / OPENCLAW_GATEWAY_TOKEN) or an admin-capable Control UI session; a limited paired operator token won’t be enough.

keen ermine
#

headless node paired with only operator.pairing, doesn't request a scope upgrade on restart — how do I make it request operator.read etc., or grant it server-side?

south bobcatBOT
#

I think there’s a role/scope mix-up here.

For a headless node, you generally do not make it request operator.read / operator.write / etc. Operator scopes are for operator clients like CLI, Control UI, approval clients. A node should connect as role node; system.run is controlled by:

  • the node being paired for role node
  • gateway.nodes.allowCommands allowing system.run, system.run.prepare, system.which
  • node-local exec approvals / allowlist policy
  • the operator approving the node pairing/scope-upgrade having operator.admin

So if you’re trying to fix node execution, don’t chase operator.read on the node token. Make sure the paired device row has role node and the node advertises the system commands.

Check with:

openclaw devices list --json --url ws://127.0.0.1:18789 --token "$OPENCLAW_GATEWAY_TOKEN" | jq
openclaw nodes status --url ws://127.0.0.1:18789 --token "$OPENCLAW_GATEWAY_TOKEN"
#

If the paired device does not include role node, remove it and re-pair the node:

openclaw devices remove <deviceId> --url ws://127.0.0.1:18789 --token "$OPENCLAW_GATEWAY_TOKEN"
# restart openclaw-node.service on the VPS
openclaw devices list --url ws://127.0.0.1:18789 --token "$OPENCLAW_GATEWAY_TOKEN"
openclaw devices approve <requestId> --url ws://127.0.0.1:18789 --token "$OPENCLAW_GATEWAY_TOKEN"

If you really mean an operator token for the same device, you can only rotate/grant scopes if the device’s approved contract already includes role operator:

openclaw devices rotate \
  --device <deviceId> \
  --role operator \
  --scope operator.read \
#
  --scope operator.write \
  --scope operator.approvals \
  --url ws://127.0.0.1:18789 \
  --token "$OPENCLAW_GATEWAY_TOKEN"

But rotation cannot mint a brand-new unapproved role. If roles doesn’t include operator, that command should fail by design.

One more important bit: openclaw nodes approve is the older/separate node.pair.* store; WS headless nodes use device pairing. Prefer openclaw devices list/approve for this path.

keen ermine
#

Discord exec approval card resolves but command still fails: "approval id not valid for this client"
OpenClaw 2026.5.7, Docker gateway + headless Linux node. Native Discord exec approvals (channels.discord.execApprovals, target: dm), tools.exec = {host: node, security: allowlist, ask: always}, node-local ask: always.
Flow: trigger a node command from Discord → card appears in DM → click "Allow once" → card updates to "Exec Approval: Allowed (once), Resolved by Discord approval" → but the command still doesn't run.
Gateway logs show the approval is created and waited on one connection, then the execution arrives on a different connection and is rejected:
exec.approval.waitDecision ✓ ... conn=e38e1ed5
node.invoke ✗ INVALID_REQUEST errorMessage=approval id not valid for this client conn=1c6d5885
/approve <id> also fails with "unknown or expired approval id" even immediately after the card resolves.
Is this a known bug with native Discord approval resolution not routing back to the originating exec connection? Is there a fix or workaround on 2026.5.7?

south bobcatBOT
#

Yeah, that log pattern is very specific. The Discord card is not the part failing — it resolved the Gateway approval successfully. The failure is the later node.invoke system.run approval guard rejecting the approved run because it thinks the approved run is coming from a different Gateway client.

For node exec approvals, OpenClaw binds the approval to the original requester to prevent replay. In the good path it binds by stable device identity; if that’s missing, it falls back to connection id. Your log shows the bad fallback path:

  • exec.approval.waitDecision on conn=e38e1ed5
  • approved execution arrives via node.invoke on conn=1c6d5885
  • guard says approval id not valid for this client

So yes: this looks like a real 2026.5.7 bug/edge case for async node exec approvals from the backend/Discord path when the Gateway-side tool calls don’t have a stable device identity. /approve saying unknown/expired after the button resolves is expected-ish: the approval is already resolved, so it’s no longer pending for text approval.

Things to try/work around:

  1. Make sure the Docker Gateway state is writable/persistent so backend Gateway calls can create/reuse a stable identity:
ls -l ~/.openclaw/identity/device.json
#

inside the container / mounted OpenClaw state. If that file can’t be created or isn’t persisted, per-call Gateway clients may fall back to conn binding and hit exactly this.

  1. Upgrade if possible. Current source has explicit logic to prefer requestedByDeviceId over connId for this reason. If 2026.5.7 is still hitting conn binding, latest/beta is the best bet.

  2. Practical workaround on 2026.5.7: don’t route node exec through interactive approvals. Use node-local allowlists + ask: "on-miss" for the commands you trust, so trusted commands run without the card. For non-allowlisted node commands, this path may stay broken until the identity/client-binding issue is fixed.

I’d file this as a GitHub issue with those exact log lines: https://github.com/openclaw/openclaw/issues. The key title is something like: “Discord native node exec approval resolves, but approved system.run rejected with APPROVAL_CLIENT_MISMATCH across Gateway connections.”

keen ermine
#

so is discord exec approvals completely broken then?

south bobcatBOT
#

Not completely, no.

Discord exec approvals themselves are working if the card appears and updates to “Allowed” — that means the Discord/native approval layer and Gateway resolution layer are doing their job.

What looks broken in your case is the post-approval node execution handoff for host=node on 2026.5.7: the approval is resolved on one Gateway connection, then the approved node.invoke system.run arrives on another connection and the safety guard rejects it with approval id not valid for this client.

So the practical answer is:

  • Discord approvals for gateway/local exec: likely fine.
  • Discord approvals for headless node exec after approval: looks buggy in this setup/version.
  • Workaround: use node-local allowlists with ask: "on-miss" for commands you trust, instead of relying on approval cards for node allowlist misses.
  • Best fix path: upgrade to latest/beta if possible, or file this with the exact logs: https://github.com/openclaw/openclaw/issues

The key bug phrase is: “native Discord approval resolves, but approved host=node execution fails with APPROVAL_CLIENT_MISMATCH / approval id not valid for this client.”

keen ermine
#

is 2026.5.7 not the latest version?

south bobcatBOT
#

Yes — 2026.5.7 is the current latest/stable npm release.

There is also a newer beta right now: 2026.5.12-beta.6.

So if I said “latest/beta,” more precisely: you’re already on latest stable; the only newer thing to try is the beta channel.

keen ermine
#

would that fix it? realistically

south bobcatBOT
#

Realistically: I wouldn’t bet your setup on the beta fixing this unless someone can point to a specific fix/PR for approval id not valid for this client on host=node.

It might help if the beta changed Gateway client identity / approval replay behavior, but from the symptom this is a pretty specific binding problem: approval created under one backend client identity/conn, approved execution sent under another, then the node approval guard rejects it. If that exact path wasn’t patched, beta won’t magically fix it.

My practical recommendation:

  1. If this is not production-critical, try 2026.5.12-beta.6 and retest the smallest repro.
  2. If you need reliability now, don’t depend on Discord cards for headless node allowlist misses on 2026.5.7. Use node-local allowlists + ask: "on-miss" for trusted commands.
  3. File the GitHub issue with the log pair. That’s the thing maintainers need to fix it properly.

So: worth trying, but I’d call it a maybe, not a likely fix.

keen ermine
#

could this be an error of my own or is it likely a bug with openclaw

south bobcatBOT
#

Given the exact behavior you described, I’d call it likely an OpenClaw bug/edge case, not simply user error.

The reason: your Discord approval card resolves successfully. If this were just bad Discord config, wrong approver, missing channels.discord.execApprovals, etc., the card usually wouldn’t appear or wouldn’t resolve.

The suspicious part is after resolution:

exec.approval.waitDecision ✓ ... conn=e38e1ed5
node.invoke ✗ approval id not valid for this client conn=1c6d5885

That points to OpenClaw binding the node exec approval to one Gateway client/connection, then attempting the approved system.run from another connection. That’s internal approval-handoff/security-guard behavior.

There are still a couple setup things worth checking, because they can trigger the bad path:

  • Docker state is persistent and writable, especially ~/.openclaw/identity/device.json
  • Gateway and node are actually same version family
#
  • You’re using the exec tool with host=node, not raw nodes invoke system.run
  • The node is paired as role node and advertises system.run.prepare

But if those are true, I’d treat this as a real bug and file it. The issue should include the two different conn= IDs and the error approval id not valid for this client — that’s the smoking claw. 🦞

keen ermine
#

Headless node host 5.12 silently stops connecting after plugin load — worked fine on 5.7
Upgraded ziost (headless Ubuntu VPS) from 2026.5.7 → 2026.5.12 via npm install -g openclaw@latest. Gateway (Docker) also on 5.12.
Symptom: openclaw node run --host 192.168.5.101 --port 18789 prints the banner ("OpenClaw 2026.5.12 (f066dd2)"), loads 7 plugins, then goes completely silent. No connection attempts. Gateway log shows zero traffic from ziost after the 5.7 process died. With OPENCLAW_LOG_LEVEL=debug DEBUG='*', output stops after "loaded 7 plugin(s)".
Confirmed: package.json says 2026.5.12, binary on disk is fresh, new PID, NRestarts=0, not crash-looping. ~/.openclaw/identity/device.json and ~/.openclaw/node.json intact from 5.7 (deviceId, keys, gateway host/port). openclaw doctor --fix only flagged permission tightening and a missing sessions dir — nothing connection-related. Identical config worked on 5.7 (was throwing protocol-mismatch errors against the upgraded gateway, which is what prompted the upgrade).
Did the node host change behavior between 5.7 and 5.12 in a way that requires a new setup/pair step, env var, or config flag? What's the right diagnostic for "plugins load, then nothing"?

south bobcatBOT
# keen ermine Headless node host 5.12 silently stops connecting after plugin load — worked fin...

I checked current docs/source against v2026.5.12 (f066dd2). I don’t see a new mandatory setup/pair step for node hosts. The first connection still creates/refreshes a pending node pairing request, approved with:

openclaw nodes pending
openclaw nodes approve <requestId>

The thing that matches “plugins load, then absolutely no gateway traffic” is earlier than pairing: openclaw node run --host 192.168.5.101 --port 18789 builds ws://192.168.5.101:18789, and the gateway client has a plaintext-WS safety gate before it opens the socket. Plain ws:// is loopback-only by default; private LAN/tailnet direct WS needs the client-side break-glass env:

OPENCLAW_ALLOW_INSECURE_PRIVATE_WS=1 \
OPENCLAW_GATEWAY_TOKEN='<gateway token>' \
openclaw --log-level debug node run --host 192.168.5.101 --port 18789

If that immediately produces gateway traffic / a pending node request, you’ve found it. For the service, make sure that env is actually in the systemd user unit environment too, not just your shell.

south bobcatBOT
# keen ermine Headless node host 5.12 silently stops connecting after plugin load — worked fin...

Preferred fix per docs is still loopback + SSH tunnel or Tailscale Serve/wss://, e.g. from the node host:

ssh -N -L 18790:127.0.0.1:18789 user@gateway-host
OPENCLAW_GATEWAY_TOKEN='<gateway token>' openclaw node run --host 127.0.0.1 --port 18790

Docs refs:
https://docs.openclaw.ai/cli/node
https://docs.openclaw.ai/nodes
https://docs.openclaw.ai/gateway/remote

One odd bit: the node host code should write node host gateway connect failed: SECURITY ERROR... to stderr when this gate trips. If you truly get no stderr in foreground with --log-level debug, that’s worth filing as a bug because the failure path is too quiet: https://github.com/openclaw/openclaw/issues

keen ermine
#

Node 5.12 now connects but the scope upgrade pending request keeps expiring before I can approve. What's the supported way to grant a persistent scope?
After adding OPENCLAW_ALLOW_INSECURE_PRIVATE_WS=1 + OPENCLAW_GATEWAY_TOKEN to ziost's node host env, the protocol mismatch is gone and the node is now connecting. Gateway log:
[gateway] security audit: device access upgrade requested reason=scope-upgrade
device=ffdfb99a... ip=192.168.5.103 auth=token client=gateway-client
roleFrom=node,operator roleTo=operator scopesFrom=operator.pairing scopesTo=operator.read
[ws] closed before connect ... code=1008 reason=pairing required: device is asking for more scopes than currently approved
openclaw devices list shows the pending request but TTL is short (~5 min) and the node retries fast, so race-and-approve via the CLI inside the Docker gateway is brittle. Yesterday I caught one with a polling loop but it took multiple tries. Approving from Control UI works but only because that browser session has admin scope.
Three questions:

Is there a way to pre-authorize a node's scope contract so the upgrade is granted automatically on connect, not as a transient request that needs racing?
Can a node be paired with operator.read from the start (e.g. in openclaw nodes setup or via a config flag on the node side) so the upgrade step isn't needed?
If neither, what's the recommended approval workflow for a Docker gateway where the container CLI token doesn't have operator.admin?

south bobcatBOT
# keen ermine Node 5.12 now connects but the scope upgrade pending request keeps expiring befo...

Short version: the persistent grant is the device pairing approval itself. There isn’t a supported “auto-grant this future scope upgrade on remote connect” config, and scope upgrades are intentionally not silently auto-approved.

A few specifics from current code/docs:

  1. operator.read is not a node scope. It belongs to the operator role. A headless openclaw node run connects as role node with empty operator scopes. Your log says client=gateway-client, so the upgrade is probably a CLI/probe/status client on ziost using the same ~/.openclaw/identity/device.json, not the node host capability connection itself.

  2. Once you approve that pending device request, it is durable: the paired device record gets approvedScopes, and a role token is minted/rotated. Future reconnects with that device identity should not need the same operator.read upgrade again unless the device identity/public key changes or it asks for broader scopes.

  3. There is no headless-node flag I found that says “pair this node as operator.read from the start.” The QR/bootstrap setup path can seed a bounded node + operator profile for supported onboarding clients, but openclaw node run itself is not an operator-read preauth flow.

For the Docker gateway approval workflow: approve from a caller that has both operator.pairing and enough scope to grant the requested operator scope. For a request asking only operator.read, the approver needs operator.pairing plus operator.read/ operator.write / operator.admin. A pairing-only device token can list/approve basic pairing but cannot grant operator.read.

Practical options:

# Best: use the gateway shared auth token/password, not a narrow paired-device token.
south bobcatBOT
# keen ermine Node 5.12 now connects but the scope upgrade pending request keeps expiring befo...
openclaw devices list --url ws://127.0.0.1:18789 --token '<gateway auth token>'
openclaw devices approve '<requestId>' --url ws://127.0.0.1:18789 --token '<gateway auth token>'

If you’re inside the Docker container on the gateway’s own filesystem, the local fallback path can also approve with admin-equivalent local-machine authority when RPC auth is the blocker, but only when you are targeting local loopback and not forcing a remote URL path. If you pass --url, pass --token/ --password explicitly.

One thing to check: stop any status/probe loop on ziost while approving. Same-snapshot retries should refresh/reuse the pending request, but if different clients on that same device identity alternate requested roles/scopes, they can supersede each other and make the requestId feel racey.

Docs:
https://docs.openclaw.ai/cli/devices
https://docs.openclaw.ai/gateway/protocol
https://docs.openclaw.ai/gateway/troubleshooting#dashboard-control-ui-connectivity

keen ermine
#

2026.5.12 Docker gateway: nodes status consistently times out on loopback, while other WS calls succeed
Fresh restart, container Up (healthy). Logs show [gateway] ready, all plugins loaded, Discord connected, and exec.approval.list ✓ 351ms succeeded immediately. But:
docker exec OpenClaw openclaw nodes status 2>&1
nodes status failed: GatewayTransportError: gateway timeout after 10000ms
Gateway target: ws://127.0.0.1:18789
Source: local loopback
Happens repeatedly across restarts on 2026.5.12. Other CLI commands inside the container that hit the WS work fine (e.g. devices list, exec.approval.list). Is nodes status doing something different that gets stuck? Worth filing as a bug, or is there a known cause / 5.12 regression?

south bobcatBOT
# keen ermine 2026.5.12 Docker gateway: nodes status consistently times out on loopback, while...

Yes, nodes status is doing something different.

In 2026.5.12, openclaw nodes status first calls gateway RPC node.list. On the gateway side, node.list builds a known-node catalog from three sources:

  • device pairing store
  • node pairing store
  • live node registry

That is different from devices list and exec.approval.list, so those succeeding does not clear node.list.

The useful split-test is:

docker exec OpenClaw openclaw nodes pending --timeout 30000
docker exec OpenClaw openclaw nodes list --timeout 30000
docker exec OpenClaw openclaw nodes status --json --timeout 30000
south bobcatBOT
# keen ermine 2026.5.12 Docker gateway: nodes status consistently times out on loopback, while...
docker exec OpenClaw openclaw gateway call node.list '{}' --timeout 30000

Expected interpretation:

  • nodes pending works but nodes status / node.list times out: likely bug in node.list / known-node catalog path.
  • nodes list also hangs: likely node pairing store/read path.
  • --timeout 30000 succeeds: it’s slow rather than wedged, still worth reporting with timings.
  • Gateway logs show no request completion for node.list: stronger evidence of a handler hang.

I don’t see a supported config knob that would make nodes status intentionally block for connected nodes. It should be a local catalog/list operation, not an invoke against the node. So if node.list reliably times out on loopback while other RPCs complete, I’d file it as a 5.12 bug: https://github.com/openclaw/openclaw/issues

Include:

  • openclaw --version
  • exact Docker image/tag or npm package version
  • outputs of the four commands above
south bobcatBOT
keen ermine
#

are there any good free models to use with openclaw?