#sever acces

1 messages Β· Page 1 of 1 (latest)

lilac kraken
#

We need to install the OpenClaw node process directly on the host (Ubuntu 24.04, outside Docker). The gateway is running in Docker on the same machine at port 18789. How do we install the openclaw CLI/node on the host itself?

abstract dragonBOT
#

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

safe whaleBOT
timber canopyBOT
# lilac kraken We need to install the OpenClaw node process directly on the host (Ubuntu 24.04,...

For Ubuntu host node + Docker Gateway, install only the host CLI/node service. Don’t run full onboarding unless you also want a second Gateway on the host.

# on the Ubuntu host
node --version   # needs Node 24 recommended, or Node 22.14+
curl -fsSL https://openclaw.ai/install.sh | bash -s -- --no-onboard

openclaw --version

Then install the headless node host against the Docker-published Gateway port:

# token auth is the cleanest path for the node service
export OPENCLAW_GATEWAY_TOKEN='your-gateway-token'
timber canopyBOT
# lilac kraken We need to install the OpenClaw node process directly on the host (Ubuntu 24.04,...
openclaw node install --host 127.0.0.1 --port 18789 --display-name ubuntu-host --force
openclaw node status

If the Docker Gateway is not published to the host, fix the container first so 127.0.0.1:18789 reaches it, e.g. Docker/Compose should publish 18789:18789.

First connection will create pairing requests on the Gateway. Approve them from a CLI that can reach the Gateway:

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 node command pairing is also pending:
openclaw nodes pending --url ws://127.0.0.1:18789 --token "$OPENCLAW_GATEWAY_TOKEN"
openclaw nodes approve <requestId> --url ws://127.0.0.1:18789 --token "$OPENCLAW_GATEWAY_TOKEN"
timber canopyBOT
lilac kraken
#

This needs to go back to Krill. Here's the full situation to report:


Report for Krill:

  • openclaw --version: 2026.5.20
  • Device approval for ubuntu-host (request 203963fa) succeeded
  • But immediately after, a second pending request appeared:
    32b0565e-ba39-4cd8-84d5-fbe63dff7d26 β€” "device identity changed and must be
    re-approved"
  • Running openclaw devices approve 32b0565e-... fails because the CLI itself
    can't connect to the Gateway β€” it's blocked by its own pending approval
  • Error: gateway closed (1008): pairing required: device identity changed and
    must be re-approved (requestId: 32b0565e-ba39-4cd8-84d5-fbe63dff7d26)

The CLI is stuck in a catch-22 β€” it needs approval to connect, but needs to
connect to approve. Need Krill's guidance on how to approve
32b0565e-ba39-4cd8-84d5-fbe63dff7d26 from a different client or via the Docker
Gateway directly.

timber canopyBOT
# lilac kraken This needs to go back to Krill. Here's the full situation to report: --- Re...

Yep, that’s the device-pairing catch-22. Two good ways out:

  1. Best: approve it from inside the Gateway container, because that CLI is local to the Gateway state and can use the local pairing fallback.
docker ps --format 'table {{.Names}}\t{{.Image}}\t{{.Ports}}'
docker exec -it <gateway-container> openclaw devices list
docker exec -it <gateway-container> openclaw devices approve 32b0565e-ba39-4cd8-84d5-fbe63dff7d26

If openclaw is not on PATH inside the container, try:

docker exec -it <gateway-container> sh -lc 'openclaw devices list'
  1. If you have the real shared Gateway token/password, force shared auth from the host CLI. Don’t rely on the cached device token:
timber canopyBOT
# lilac kraken This needs to go back to Krill. Here's the full situation to report: --- Re...
openclaw devices approve 32b0565e-ba39-4cd8-84d5-fbe63dff7d26 \
  --url ws://127.0.0.1:18789 \
  --token "$OPENCLAW_GATEWAY_TOKEN"

If the Gateway is configured for password auth, use --password "$OPENCLAW_GATEWAY_PASSWORD" instead of --token.

The important bit: approve 32b0565e... using an already-trusted operator context: Gateway container CLI, an already-paired dashboard/browser, or the real shared Gateway token/password. The host CLI that is currently pending can’t approve itself with its stale/pending device token.

After approval, restart the node host once so it reconnects cleanly:

openclaw node restart
openclaw node status
openclaw nodes status --url ws://127.0.0.1:18789 --token "$OPENCLAW_GATEWAY_TOKEN"
lilac kraken
#

⎿  Config warnings:
- plugins: plugin apify: blocked plugin candidate: suspicious ownershi
p (/root/.openclaw/extensions/apify, uid=1000, expected uid=0 or root)
… +125 lines (ctrl+o to expand)
⎿  (timeout 30s)

● All done. Report back to Krill/Christian:

  • ubuntu-host node: paired Β· connected (just now)
  • Known: 1 Β· Paired: 1 Β· Connected: 1
  • Node service running cleanly (pid 3470852, systemd active)

The catch-22 was resolved by approving from inside the Gateway container using
--token. Everything is green.

✻ Brewed for 52s

timber canopyBOT
# lilac kraken ⎿  Config warnings: - plugins: plugin apify: blocked plugin candidate: susp...

Nice, that’s the state we wanted: node paired, connected, and the systemd service is healthy.

The remaining warning is separate from the node pairing issue:

plugin apify: blocked plugin candidate: suspicious ownership (/root/.openclaw/extensions/apify, uid=1000, expected uid=0 or root)

That means the Gateway is running as root in the container, but that extension directory is owned by UID 1000. OpenClaw blocks it as a safety check. Fix it only if they actually need the Apify extension; otherwise it can be ignored/removed.

If they do need it, from the host:

docker exec -it <gateway-container> sh -lc 'chown -R root:root /root/.openclaw/extensions/apify'
docker restart <gateway-container>

If /root/.openclaw is bind-mounted from the host, fix ownership on the host path instead, then restart the container. Glad we caught the pairing loop cleanly. 🦞

lilac kraken
#

After gateway restart, Roger says "no node connected that supports system.run". openclaw nodes status shows ubuntu-host as paired/connected from host CLI, but the gateway doesn't see it. How do we ensure the host node reconnects automatically after gateway restarts?

timber canopyBOT
# lilac kraken After gateway restart, Roger says "no node connected that supports system.run". ...

If openclaw nodes status says connected but exec host=node says no node supports system.run, first verify they’re looking at the same Gateway and that the node’s command list includes system.run.

Run both of these against the Docker Gateway:

# from host
openclaw nodes status --url ws://127.0.0.1:18789 --token "$OPENCLAW_GATEWAY_TOKEN"
openclaw nodes describe --node ubuntu-host --url ws://127.0.0.1:18789 --token "$OPENCLAW_GATEWAY_TOKEN"

# from inside the gateway container
docker exec -it <gateway-container> sh -lc 'openclaw nodes status && openclaw nodes describe --node ubuntu-host'

In nodes describe, check the commands list. It must include system.run. If it does not, check pending node-command pairing and approve it:

timber canopyBOT
# lilac kraken After gateway restart, Roger says "no node connected that supports system.run". ...
openclaw nodes pending --url ws://127.0.0.1:18789 --token "$OPENCLAW_GATEWAY_TOKEN"
openclaw nodes approve <requestId> --url ws://127.0.0.1:18789 --token "$OPENCLAW_GATEWAY_TOKEN"

To force a clean reconnect after a Gateway restart:

openclaw node restart
journalctl --user -u openclaw-node.service -n 100 --no-pager

The node service should already be installed as a systemd user service with Restart=always, and the node client is supposed to retry Gateway reconnects. For headless Ubuntu, also make sure the user service survives SSH/logout/reboots:

sudo loginctl enable-linger $(whoami)
timber canopyBOT
lilac kraken
#

export OPENCLAW_GATEWAY_TOKEN='5b07754c9a78b3cdda7170434c746a5d0d890b686a376c
a029b6683a64e88b51' && openclaw nodes describe --node ubuntu-host --url
ws://127.0.0.1:18789 --token "$OPENCLAW_GATEWAY_TOKEN"

● Bash(export OPENCLAW_GATEWAY_TOKEN='5b07754c9a78b3cdda7170434c746a5d0d890b686a
376ca029b6683a64e88b51' && openclaw nodes describe --node ubuntu-host
--url ws://127.0.…)
⎿  Config warnings:
- plugins: plugin apify: blocked plugin candidate: suspicious ownershi
p (/root/.openclaw/extensions/apify, uid=1000, expected uid=0 or root)
… +94 lines (ctrl+o to expand)
⎿  (timeout 30s)

● Node details look good:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Field β”‚ Value β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ Name β”‚ ubuntu-host β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ Status β”‚ paired Β· connected β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ Version β”‚ core v2026.5.20 β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ IP β”‚ 172.18.0.1 β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ Client β”‚ node-host/node β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

One thing to note for Krill: Caps and Commands are both empty β€” no
capabilities reported. That may or may not be expected depending on what the
node is supposed to expose. Worth flagging if tools/exec isn't working as
expected after this.

timber canopyBOT
# lilac kraken export OPENCLAW_GATEWAY_TOKEN='5b07754c9a78b3cdda7170434c746a5d0d890b686a376c ...

Caps/Commands empty is the key. The node identity is paired, but the Gateway has not approved/accepted the node’s command surface yet, so exec host=node correctly sees no system.run.

Next step is specifically node pairing, not device pairing:

openclaw nodes pending --url ws://127.0.0.1:18789 --token "$OPENCLAW_GATEWAY_TOKEN"
openclaw nodes approve <requestId> --url ws://127.0.0.1:18789 --token "$OPENCLAW_GATEWAY_TOKEN"
openclaw node restart
openclaw nodes describe --node ubuntu-host --url ws://127.0.0.1:18789 --token "$OPENCLAW_GATEWAY_TOKEN"

After that, nodes describe should show commands like:

system.run.prepare
system.run
timber canopyBOT
# lilac kraken export OPENCLAW_GATEWAY_TOKEN='5b07754c9a78b3cdda7170434c746a5d0d890b686a376c ...
system.which
system.execApprovals.get
system.execApprovals.set

If nodes pending is empty but commands are still empty, force the node to ask for a fresh command approval:

openclaw nodes reject <staleRequestId> --url ws://127.0.0.1:18789 --token "$OPENCLAW_GATEWAY_TOKEN"  # only if one exists/stale
openclaw node restart
openclaw nodes pending --url ws://127.0.0.1:18789 --token "$OPENCLAW_GATEWAY_TOKEN"

Also: a gateway token was pasted into the thread. Treat it as compromised. Once this is stable, rotate it and update the node service env/config, then restart both Gateway and node. Docs for rotation nuance: https://docs.openclaw.ai/cli/devices and node pairing: https://docs.openclaw.ai/gateway/pairing

lilac kraken
#

Report for Krill:

  • nodes pending showed request 21dc96a9 for ubuntu-host
  • openclaw nodes approve 21dc96a9... from the host CLI fails: missing scope:
    operator.pairing β€” the host device token doesn't have that scope
  • Tried approving from inside the Gateway container (docker exec
    openclaw-openclaw-gateway-1 openclaw nodes approve 21dc96a9... --token '...')
    β€” command produced no approval confirmation, just config warnings and plugin
    migration logs, exit with no output
  • nodes describe still shows Commands: (none reported)

The node pairing scope issue is blocking the command surface approval and the
container CLI path didn't work this time either. Need Krill's next step.

✻ Crunched for 2m 29s · 1 shell still running

● Background command "Approve node pairing from inside Gateway container using
token" failed with exit code 1

● Confirmed β€” the container approval failed (exit code 1). The output just
showed plugin/migration logs with no approval, no error message explaining
why.

To summarize what needs to go to Krill:

  • Host CLI: nodes approve fails with missing scope: operator.pairing
  • Gateway container CLI (docker exec ... openclaw nodes approve ... --token
    '...'): exits code 1, no useful error β€” just plugin warnings and migration
    logs, no approval confirmation
  • Result: node commands still empty, system.run not exposed

What should the next approach be?

✻ Crunched for 5s

β€» recap: Installing OpenClaw node host on Ubuntu to expose system.run to the
Docker Gateway. Node is paired and connected but command surface is empty;
waiting for Krill to advise on approving the node pairing scope.

────────────────────────────────────────────────────────────────────────────────
❯ 
────────────────────────────────────────────────────────────────────────────────
? for shortcuts Β· ← for agents

timber canopyBOT
# lilac kraken --- Report for Krill: - nodes pending showed request 21dc96a9 for ubuntu-ho...

The blocker is one layer earlier: the CLI/operator device doing nodes approve is not approved for operator.pairing/operator.admin, so it can list/describe but cannot approve a node that exposes system.run.

Do this from inside the Gateway container, and omit --url/--token for the device approval so the local pairing fallback can operate on the Gateway state:

docker exec -it openclaw-openclaw-gateway-1 sh -lc 'openclaw devices list'

Look for a pending operator/CLI request or scope upgrade whose requested scopes include operator.admin and/or operator.pairing. Approve that exact request:

docker exec -it openclaw-openclaw-gateway-1 sh -lc 'openclaw devices approve <operator-requestId>'

Then approve the node command pairing:

timber canopyBOT
# lilac kraken --- Report for Krill: - nodes pending showed request 21dc96a9 for ubuntu-ho...
docker exec -it openclaw-openclaw-gateway-1 sh -lc 'openclaw nodes pending'
docker exec -it openclaw-openclaw-gateway-1 sh -lc 'openclaw nodes approve 21dc96a9-...'

Then restart/reconnect the node:

openclaw node restart
openclaw nodes describe --node ubuntu-host --url ws://127.0.0.1:18789 --token "$OPENCLAW_GATEWAY_TOKEN"

Expected result: Commands should include system.run.prepare, system.run, and system.which.

If devices list shows multiple pending entries, don’t use --latest; approve the one for the CLI/operator scope upgrade after checking the Requested column.

Also: rotate the Gateway token after this. It was pasted into the thread, so treat it as exposed.

lilac kraken
#

Report for Krill:

  • docker exec ... openclaw nodes pending (no --url/--token, local loopback):
    GatewayTransportError: gateway timeout after 10000ms β€” Source: local loopback,
    Bind: loopback
  • The Gateway container CLI uses local loopback to reach the Gateway, but it's
    timing out β€” the Gateway may not be listening on loopback inside the
    container
  • devices list worked (which must use a different path), but nodes pending
    goes through the Gateway WebSocket and fails
  • No pending device requests existed when devices list ran, so there's no
    operator scope upgrade to approve

What address/port is the Gateway actually listening on inside the container,
and is nodes pending supposed to use the local loopback or should it be
pointed at --url ws://127.0.0.1:18789?

✻ Churned for 10s

β€» recap: Installing an Ubuntu host node against a Docker OpenClaw Gateway. Node
is paired and connected but has no commands exposed yet. Waiting for Krill's
next instruction on approving node command pairing since the container CLI
times out on local loopback.

────────────────────────────────────────────────────────────────────────────────
❯ 
────────────────────────────────────────────────────────────────────────────────
? for shortcuts Β· ← for agents

timber canopyBOT
# lilac kraken Report for Krill: - docker exec ... openclaw nodes pending (no --url/--token,...

nodes pending is supposed to go through the Gateway WebSocket. In the Docker setup, the recommended path is the openclaw-cli service, not docker exec into the running gateway process. The docs say openclaw-cli shares the gateway network namespace and is the post-start CLI surface.

First, find whether the Gateway is actually reachable inside that namespace:

docker compose run --rm openclaw-cli sh -lc 'curl -fsS http://127.0.0.1:18789/healthz && echo && curl -fsS http://127.0.0.1:18789/readyz'
docker compose run --rm openclaw-cli sh -lc 'ss -ltnp 2>/dev/null | grep 18789 || netstat -ltnp 2>/dev/null | grep 18789 || true'

Then use the CLI container with an explicit URL/token:

docker compose run --rm openclaw-cli nodes pending \
  --url ws://127.0.0.1:18789 \
  --token "$OPENCLAW_GATEWAY_TOKEN" \
  --timeout 60000
timber canopyBOT
# lilac kraken Report for Krill: - docker exec ... openclaw nodes pending (no --url/--token,...

docker compose run --rm openclaw-cli nodes approve 21dc96a9-... \
  --url ws://127.0.0.1:18789 \
  --token "$OPENCLAW_GATEWAY_TOKEN" \
  --timeout 60000

Important: make sure $OPENCLAW_GATEWAY_TOKEN is the shared gateway auth token, not a cached device token. The earlier missing scope: operator.pairing usually means the CLI connected with a paired device token that lacks pairing/admin scopes. In Docker, the shared token is usually in the Compose .env / mounted OpenClaw .env; don’t paste it into Discord again.

If the curl health checks time out from openclaw-cli too, the Gateway WebSocket is wedged or the CLI is pointed at the wrong namespace. At that point restart the gateway container, then retry:

docker compose restart openclaw-gateway
docker compose run --rm openclaw-cli gateway probe --timeout 60000
timber canopyBOT
lilac kraken
#

Report for Krill:

  • Confirmed: OPENCLAW_GATEWAY_TOKEN = shared gateway token (in .env and
    openclaw.json auth.mode: token)
  • openclaw-cli nodes pending --url ws://127.0.0.1:18789 --token "...": works β€”
    found pending request eac68b42 for ubuntu-host
  • openclaw-cli nodes approve eac68b42 --url ws://127.0.0.1:18789 --token
    "...": fails β€” missing scope: operator.admin
  • Same token, same container, back-to-back commands β€” pending succeeds,
    approve fails
  • Also notable: openclaw.json gateway bind: "loopback" β€” but the CLI container
    reaches it fine via ws://127.0.0.1:18789

Why would nodes approve require operator.admin when connecting with the shared
gateway token? Is the shared token not treated as full operator for node
approval specifically, and is there a separate admin credential needed?

timber canopyBOT
# lilac kraken Report for Krill: - Confirmed: OPENCLAW_GATEWAY_TOKEN = shared gateway token ...

That explains it. nodes approve is a two-level check:

  • the RPC method itself needs operator.pairing
  • approving a node whose pending command list includes system.run also requires operator.admin

So there isn’t a separate β€œadmin credential”; it’s an operator scope. In 2026.5.20 this looks like a CLI gap: nodes approve is connecting with pairing scope, but the pending node request requires admin because it exposes system.run.

Pragmatic break-glass path: approve the pending node request from the same OpenClaw code, but bypass the CLI scope selection bug and explicitly pass admin scopes to the pairing helper. Back up the node pairing files first.

# from the Docker compose directory
docker compose run --rm openclaw-cli sh -lc '
  set -eu
  ts=$(date +%Y%m%d-%H%M%S)
  mkdir -p /home/node/.openclaw/nodes/backups
  cp -a /home/node/.openclaw/nodes/pending.json /home/node/.openclaw/nodes/backups/pending.$ts.json 2>/dev/null || true
timber canopyBOT
# lilac kraken Report for Krill: - Confirmed: OPENCLAW_GATEWAY_TOKEN = shared gateway token ...
  cp -a /home/node/.openclaw/nodes/paired.json /home/node/.openclaw/nodes/backups/paired.$ts.json 2>/dev/null || true
'

docker compose run --rm openclaw-cli node --input-type=module -e '
  const requestId = process.argv[1];
  const { approveNodePairing } = await import("./dist/infra/node-pairing.js");
  const res = await approveNodePairing(requestId, { callerScopes: ["operator.admin"] });
  console.log(JSON.stringify(res, null, 2));
  if (!res || res.status === "forbidden") process.exit(1);
' eac68b42-REPLACE-WITH-FULL-ID

Then reconnect the node and verify commands:

timber canopyBOT
# lilac kraken Report for Krill: - Confirmed: OPENCLAW_GATEWAY_TOKEN = shared gateway token ...
openclaw node restart
docker compose run --rm openclaw-cli nodes describe --node ubuntu-host --url ws://127.0.0.1:18789 --token "$OPENCLAW_GATEWAY_TOKEN"

Expected commands: system.run.prepare, system.run, system.which, system.execApprovals.get, system.execApprovals.set.

After it’s green, rotate the gateway token because it was pasted here. Also worth filing this as a GitHub issue: openclaw nodes approve should request/handle operator.admin when approving pending nodes that declare system.run.

lilac kraken
#

Found it β€” the files are hash-named bundles. node-pairing-DA0r6E2a.js and
node-pairing-authz-qE6O62ky.js are the candidates. Need to report back to
Krill with the actual file names so they can give the correct import path.

Report for Krill:

  • Working directory inside container: /app
  • dist/infra/node-pairing.js does not exist β€” the dist is flat/hash-named
    bundles
  • Relevant files found:
    • /app/dist/node-pairing-DA0r6E2a.js
    • /app/dist/node-pairing-authz-qE6O62ky.js
  • The approveNodePairing function β€” which file/export is it in? The hash
    suffix means the import path in Krill's script needs to be updated for this
    build (openclaw:local-himalaya / v2026.5.20).

What's the correct import path for approveNodePairing in this build?

timber canopyBOT
# lilac kraken Found it β€” the files are hash-named bundles. node-pairing-DA0r6E2a.js and node...

Use dynamic discovery instead of hardcoding the hash. In that build, approveNodePairing should be exported by the node-pairing-*.js bundle, not the node-pairing-authz-* helper.

Try this from the Compose directory:

docker compose run --rm openclaw-cli node --input-type=module -e '
  import fs from "node:fs";
  import path from "node:path";
  const requestId = process.argv[1];
  const dist = "/app/dist";
  const candidates = fs.readdirSync(dist).filter(f => /^node-pairing-.*\\.js$/.test(f) && !f.includes("authz"));
  if (candidates.length === 0) throw new Error("node-pairing bundle not found");
  const modPath = path.join(dist, candidates[0]);
  const mod = await import("file://" + modPath);
  console.error("using", modPath, "exports", Object.keys(mod).join(","));
  if (typeof mod.approveNodePairing !== "function") throw new Error("approveNodePairing export not found");
timber canopyBOT
# lilac kraken Found it β€” the files are hash-named bundles. node-pairing-DA0r6E2a.js and node...
  const res = await mod.approveNodePairing(requestId, { callerScopes: ["operator.admin"] });
  console.log(JSON.stringify(res, null, 2));
  if (!res || res.status === "forbidden") process.exit(1);
' eac68b42-REPLACE-WITH-FULL-ID

If it says the export name is minified/missing, run this and paste only the export list back:

docker compose run --rm openclaw-cli node --input-type=module -e '
  import fs from "node:fs";
  import path from "node:path";
  const dist = "/app/dist";
  for (const f of fs.readdirSync(dist).filter(f => f.includes("node-pairing") && f.endsWith(".js"))) {
    const mod = await import("file://" + path.join(dist, f));
timber canopyBOT
lilac kraken
#

path.

● Bash(docker compose -f /root/openclaw/docker-compose.yml run --rm --entrypoint
sh openclaw-cli -lc 'find /app/dist -name "node-pair" -o -name
"pairing" 2>/dev/nu…)
⎿   Container openclaw-openclaw-gateway-1 Running
Container openclaw-openclaw-gateway-1 Waiting
Container openclaw-openclaw-gateway-1 Healthy
… +17 lines (ctrl+o to expand)

● Found it β€” the files are hash-named bundles. node-pairing-DA0r6E2a.js and
node-pairing-authz-qE6O62ky.js are the candidates. Need to report back to
Krill with the actual file names so they can give the correct import path.

Report for Krill:

  • Working directory inside container: /app
  • dist/infra/node-pairing.js does not exist β€” the dist is flat/hash-named
    bundles
  • Relevant files found:
    • /app/dist/node-pairing-DA0r6E2a.js
    • /app/dist/node-pairing-authz-qE6O62ky.js
  • The approveNodePairing function β€” which file/export is it in? The hash
    suffix means the import path in Krill's script needs to be updated for this
    build (openclaw:local-himalaya / v2026.5.20).

What's the correct import path for approveNodePairing in this build?

✻ Baked for 38s

β€» recap: Installing an Ubuntu host node against a Docker OpenClaw Gateway. Node
is paired and connected but has no commands exposed yet. Waiting for Krill's
corrected import path for the break-glass node approval script.

timber canopyBOT
# lilac kraken path. ● Bash(docker compose -f /root/openclaw/docker-compose.yml run --rm --ent...

The import path for that build is almost certainly:

file:///app/dist/node-pairing-DA0r6E2a.js

But don’t trust the filename alone; inspect exports first:

docker compose -f /root/openclaw/docker-compose.yml run --rm --entrypoint node openclaw-cli --input-type=module -e '
  const mod = await import("file:///app/dist/node-pairing-DA0r6E2a.js");
  console.log(Object.keys(mod));
'

If approveNodePairing appears in that list, run:

timber canopyBOT
# lilac kraken path. ● Bash(docker compose -f /root/openclaw/docker-compose.yml run --rm --ent...
docker compose -f /root/openclaw/docker-compose.yml run --rm --entrypoint node openclaw-cli --input-type=module -e '
  const requestId = process.argv[1];
  const { approveNodePairing } = await import("file:///app/dist/node-pairing-DA0r6E2a.js");
  const res = await approveNodePairing(requestId, { callerScopes: ["operator.admin"] });
  console.log(JSON.stringify(res, null, 2));
  if (!res || res.status === "forbidden") process.exit(1);
' eac68b42-REPLACE-WITH-FULL-ID

If it does not export approveNodePairing, paste the export list from both bundles:

docker compose -f /root/openclaw/docker-compose.yml run --rm --entrypoint node openclaw-cli --input-type=module -e '
  for (const p of ["/app/dist/node-pairing-DA0r6E2a.js", "/app/dist/node-pairing-authz-qE6O62ky.js"]) {
    const mod = await import("file://" + p);
timber canopyBOT
lilac kraken
#

Both bundles are minified β€” exports are single-letter names (a, c, i, l, n, o,
r, s, t and t). approveNodePairing is not exported by name.

Report for Krill:

  • /app/dist/node-pairing-DA0r6E2a.js exports: [ 'a', 'c', 'i', 'l', 'n', 'o',
    'r', 's', 't' ]
  • /app/dist/node-pairing-authz-qE6O62ky.js exports: [ 't' ]
  • Build is fully minified β€” no readable export names. The break-glass
    import-by-name approach won't work on this build.

What's the next path? Options that come to mind: directly editing the
pending.json to move the request to paired.json with the right scopes, or
another approach Krill knows about for this build?

✻ Cooked for 15s

timber canopyBOT
# lilac kraken Both bundles are minified β€” exports are single-letter names (a, c, i, l, n, o, ...

Yep, next path is a controlled state-file patch. This is break-glass, but it’s straightforward: move the pending node request into nodes/paired.json, preserve the command list from the pending request, and remove it from nodes/pending.json.

Do it with the Gateway stopped so it doesn’t race the file write:

cd /root/openclaw
docker compose -f docker-compose.yml stop openclaw-gateway

docker compose -f docker-compose.yml run --rm --no-deps --entrypoint node openclaw-cli --input-type=module -e '
  import fs from "node:fs";
  import path from "node:path";
  import crypto from "node:crypto";

  const requestId = process.argv[1];
  const candidates = [process.env.OPENCLAW_STATE_DIR, "/home/node/.openclaw", "/root/.openclaw"].filter(Boolean);
  const stateDir = candidates.find(d => fs.existsSync(path.join(d, "nodes", "pending.json")));
timber canopyBOT
# lilac kraken Both bundles are minified β€” exports are single-letter names (a, c, i, l, n, o, ...
  if (!stateDir) throw new Error("could not find nodes/pending.json in " + candidates.join(", "));

  const dir = path.join(stateDir, "nodes");
  const pendingPath = path.join(dir, "pending.json");
  const pairedPath = path.join(dir, "paired.json");
  const backupDir = path.join(dir, "backups");
  fs.mkdirSync(backupDir, { recursive: true });
  const stamp = new Date().toISOString().replace(/[:.]/g, "-");
  for (const p of [pendingPath, pairedPath]) if (fs.existsSync(p)) fs.copyFileSync(p, path.join(backupDir, path.basename(p) + "." + stamp));

  const pending = fs.existsSync(pendingPath) ? JSON.parse(fs.readFileSync(pendingPath, "utf8")) : {};
  const paired = fs.existsSync(pairedPath) ? JSON.parse(fs.readFileSync(pairedPath, "utf8")) : {};
  const req = pending[requestId];
  if (!req) throw new Error("pending request not found: " + requestId + " keys=" + Object.keys(pending).join(","));
timber canopyBOT
# lilac kraken Both bundles are minified β€” exports are single-letter names (a, c, i, l, n, o, ...
  const now = Date.now();
  const existing = paired[req.nodeId] || {};
  paired[req.nodeId] = {
    nodeId: req.nodeId,
    token: crypto.randomBytes(32).toString("base64url"),
    displayName: req.displayName,
    platform: req.platform,
    version: req.version,
    coreVersion: req.coreVersion,
    uiVersion: req.uiVersion,
    deviceFamily: req.deviceFamily,
    modelIdentifier: req.modelIdentifier,
    caps: Array.isArray(req.caps) ? req.caps : [],
    commands: Array.isArray(req.commands) ? req.commands : [],
    permissions: req.permissions,
timber canopyBOT
# lilac kraken Both bundles are minified β€” exports are single-letter names (a, c, i, l, n, o, ...
    remoteIp: req.remoteIp,
    createdAtMs: existing.createdAtMs || now,
    approvedAtMs: now
  };
  delete pending[requestId];

  fs.writeFileSync(pendingPath, JSON.stringify(pending, null, 2) + "
", { mode: 0o600 });
  fs.writeFileSync(pairedPath, JSON.stringify(paired, null, 2) + "
", { mode: 0o600 });
  console.log(JSON.stringify({ ok: true, stateDir, nodeId: req.nodeId, commands: paired[req.nodeId].commands }, null, 2));
' eac68b42-REPLACE-WITH-FULL-ID

docker compose -f docker-compose.yml up -d openclaw-gateway
openclaw node restart
timber canopyBOT
# lilac kraken Both bundles are minified β€” exports are single-letter names (a, c, i, l, n, o, ...

Then verify:

docker compose -f /root/openclaw/docker-compose.yml run --rm openclaw-cli nodes describe --node ubuntu-host --url ws://127.0.0.1:18789 --token "$OPENCLAW_GATEWAY_TOKEN"

Expected commands: system.run.prepare, system.run, system.which, system.execApprovals.get, system.execApprovals.set.

If that works, please file this as a GitHub issue: on 2026.5.20, openclaw nodes approve can list the pending node but connects without enough scope to approve a system.run command surface. Also rotate the pasted gateway token after this is stable.