openclaw integration intermediate

Connecting Two OpenClaw Agents Over Tailscale: How ACP Pairing Works

Adding a second agent to the fleet is where the interesting problems start. Device identity in OpenClaw is a fixed machine keypair — tokens only control which scopes get requested, not who's asking. Change the token all you want; if the device is registered with the wrong scopes, it'll block on every connection.

The Problem

 Tony and  Mel are running on separate VPSes, connected over Tailscale.  Tony Mel works fine: a plain HTTP POST to  Mel’s FastAPI server. But every time  Mel tries to call  Tony’s gateway via ACP, the connection dies immediately:

{
  "cause": "pairing-required",
  "handshake": "failed",
  "reason": "scope-upgrade"
}

We try rotating the token. Same error. We try a garbage token. Same error. We check gateway.auth.mode — it’s token, which is correct. Nothing we do to the token changes anything.

That’s the first signal we’re looking at the wrong layer.

Why This Happens

OpenClaw separates device identity from token auth. These are two different things operating at two different levels.

Device identity is a persistent machine keypair in ~/.openclaw/identity/device.json. It’s fixed to the host. Every OpenClaw instance on a given machine presents the same device ID to remote gateways — regardless of what token is configured. The device ID is a hash of the keypair, not the token.

The token controls which scopes the device requests when it connects. That’s it.

When  Mel’s gateway connects to  Tony’s and requests full operator scopes,  Tony checks: does this device already have those scopes?  Mel’s device was registered with operator.read only. It’s now requesting operator.admin, operator.approvals, operator.pairing, operator.read, operator.write — a scope upgrade.

Scope upgrades require a pairing approval. Here’s the trap: the gateway rejects the connection before queuing the pairing request. So when you run openclaw devices approve, there’s nothing pending. You’re blocked from the outside with no way to approve from the inside.

# What you see when you check — nothing to approve
$ openclaw devices list
No pending pairing requests.

# What the gateway logs actually say
cause: "pairing-required"
reason: "scope-upgrade"

Rotating the device token on  Tony’s side doesn’t help either — openclaw devices rotate returns “unknown deviceId/role” if  Tony didn’t originally issue that device’s token.

There’s no escalation path. You have to tear it down and start over.

The Fix

Three steps: remove the stale device entry, let the remote agent re-pair from scratch, and approve with the right scopes from the start.

Step 1: Remove the stale device entry on the receiving gateway

# On Tony's VPS
openclaw devices list           # note the device ID
openclaw devices remove <device-id>

Step 2: Have the remote agent connect fresh

On  Mel’s VPS, connect via ACP. With no existing device record, this queues a fresh pairing request instead of a scope-upgrade rejection:

# On Mel's VPS
openclaw acp \
  --url wss://your-agent-gateway.ts.net \
  --session agent:main:mel \
  --verbose

Leave it running. It will sit in a pairing-pending state.

Step 3: Approve on the receiving gateway

# On Tony's VPS
openclaw devices list           # confirm pending entry appeared
openclaw devices approve --latest --json

The approval issues a new token back through the open WebSocket. OpenClaw stores it in ~/.openclaw/identity/device-auth.json on  Mel’s machine automatically — no manual token sharing needed.

[acp] ready — clean connect

What the Right Invocation Looks Like

While debugging this, we confirmed the correct command for agent-to-agent connections. It’s openclaw acp, not openclaw agent or openclaw gateway call:

openclaw acp \
  --url wss://your-agent-gateway.ts.net \
  --session agent:main:<calling-agent-name>

openclaw gateway call agent works too but requires you to figure out session params manually. openclaw acp owns that internally.

Key Takeaway

The scope-upgrade trap exists because OpenClaw has two trust layers running in parallel: device identity (who you are, fixed per machine) and token auth (what you’re allowed to do, configurable). When a device is registered with insufficient scopes, the gateway blocks at the identity layer before the token layer runs. No amount of token rotation fixes it.

The diagnostic signal: if every connection fails with pairing-required — including garbage tokens — you’re not looking at a token problem. You’re looking at a device registration problem. Remove the stale entry, re-pair from scratch, and let the handshake issue the right token the first time.

FAQ

Q: Why does a garbage token produce the same “pairing required” error as a valid one?

The gateway rejects at the device scope layer, which runs before token validation. The device ID comes from the machine keypair, not the token — so the receiving gateway identifies the connecting machine the same way regardless of what token is passed. The rejection is about the device’s registered scopes, not the token’s validity.

Q: Can I use openclaw devices rotate to upgrade a device’s scopes?

Only if the receiving gateway originally issued that device’s token. If the device paired via a different mechanism or the original token is unknown, rotate returns “unknown deviceId/role”. In that case, the only path is remove + re-pair.

Q: Does device-auth.json update automatically after approval?

Yes. The approval response flows back through the WebSocket that the pairing request came in on. OpenClaw stores the issued token in ~/.openclaw/identity/device-auth.json on the connecting machine. No manual copy needed.

Q: What’s the difference between openclaw acp and openclaw gateway call agent?

openclaw acp is a full ACP bridge — it handles session management, token loading from device-auth.json, and reconnection internally. openclaw gateway call agent is a lower-level raw RPC that requires manual session params. For agent-to-agent connections, use openclaw acp.