openclaw - 💡(How to fix) Fix sub-agent dispatch: in-process spawn hits gateway pairing gate (no in-process lane shortcut) [1 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

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#70053Fetched 2026-04-23 07:29:54
View on GitHub
Comments
0
Participants
1
Timeline
0
Reactions
0
Author
Participants

Native sub-agent dispatch via sessions_spawn({runtime: "subagent", ...}) opens a fresh WebSocket back to the local gateway from src/agents/subagent-spawn.ts:657 (callGateway({method: "agent", ...})). That connect goes through loadOrCreateDeviceIdentity() → presents a brand-new device fingerprint → hits the pairing gate at src/gateway/server/ws-connection/message-handler.ts:802 → connection closed with code=1008 reason=pairing required.

In a typical desktop install this is invisible because device pairing was set up by the CLI bootstrap. In a freshly-bootstrapped server-side install (gateway and agent runtime in the same process, no operator-driven pairing step), every sessions_spawn for a native sub-agent fails at the gate.

Error Message

[gateway] security audit: device access upgrade requested reason=scope-upgrade device=6d3e97b4e247e137a7e07fc97dc07d62a9b5756f8ee04983bbf074a87bcd10f9 ip=unknown-ip auth=token roleFrom=operator roleTo=operator scopesFrom=operator.read scopesTo=operator.admin client=cli conn=e4e33139-c42e-4d6c-917a-cb3c47910da2 gateway connect failed: GatewayClientRequestError: pairing required [ws] closed before connect conn=e4e33139-... peer=127.0.0.1:59136->127.0.0.1:18789 code=1008 reason=pairing required

Root Cause

In a typical desktop install this is invisible because device pairing was set up by the CLI bootstrap. In a freshly-bootstrapped server-side install (gateway and agent runtime in the same process, no operator-driven pairing step), every sessions_spawn for a native sub-agent fails at the gate.

Fix Action

Fix / Workaround

Native sub-agent dispatch via sessions_spawn({runtime: "subagent", ...}) opens a fresh WebSocket back to the local gateway from src/agents/subagent-spawn.ts:657 (callGateway({method: "agent", ...})). That connect goes through loadOrCreateDeviceIdentity() → presents a brand-new device fingerprint → hits the pairing gate at src/gateway/server/ws-connection/message-handler.ts:802 → connection closed with code=1008 reason=pairing required.

  1. src/agents/subagent-spawn.ts:657callGateway({method: "agent", params: {lane: AGENT_LANE_SUBAGENT, ...}}) opens a WebSocket.
  2. src/gateway/server-methods/agent.ts:197agentHandlers.agent receives the request.
  3. Same file, line 145: dispatchAgentRunFromGatewayagentCommandFromIngress(...) (now in-process).
  4. src/agents/pi-embedded-runner/run.ts:217enqueueCommandInLane(globalLane, ...) where globalLane = CommandLane.Subagent.

The in-process queue lane (step 4) is reached only after the WebSocket handshake completes. So in-process dispatch happens, but only after passing through the gateway's full WebSocket auth gate that was designed for external CLI clients.

Code Example

[gateway] security audit: device access upgrade requested
  reason=scope-upgrade
  device=6d3e97b4e247e137a7e07fc97dc07d62a9b5756f8ee04983bbf074a87bcd10f9
  ip=unknown-ip auth=token roleFrom=operator roleTo=operator
  scopesFrom=operator.read scopesTo=operator.admin
  client=cli  conn=e4e33139-c42e-4d6c-917a-cb3c47910da2
gateway connect failed: GatewayClientRequestError: pairing required
[ws] closed before connect conn=e4e33139-... peer=127.0.0.1:59136->127.0.0.1:18789
  code=1008 reason=pairing required
RAW_BUFFERClick to expand / collapse

Summary

Native sub-agent dispatch via sessions_spawn({runtime: "subagent", ...}) opens a fresh WebSocket back to the local gateway from src/agents/subagent-spawn.ts:657 (callGateway({method: "agent", ...})). That connect goes through loadOrCreateDeviceIdentity() → presents a brand-new device fingerprint → hits the pairing gate at src/gateway/server/ws-connection/message-handler.ts:802 → connection closed with code=1008 reason=pairing required.

In a typical desktop install this is invisible because device pairing was set up by the CLI bootstrap. In a freshly-bootstrapped server-side install (gateway and agent runtime in the same process, no operator-driven pairing step), every sessions_spawn for a native sub-agent fails at the gate.

Documentation says one thing, code does another

docs/tools/subagents.md:277-282:

Sub-agents use a dedicated in-process queue lane — Lane name: subagent.

There is no in-process shortcut implemented today. Tracing the failing call:

  1. src/agents/subagent-spawn.ts:657callGateway({method: "agent", params: {lane: AGENT_LANE_SUBAGENT, ...}}) opens a WebSocket.
  2. src/gateway/server-methods/agent.ts:197agentHandlers.agent receives the request.
  3. Same file, line 145: dispatchAgentRunFromGatewayagentCommandFromIngress(...) (now in-process).
  4. src/agents/pi-embedded-runner/run.ts:217enqueueCommandInLane(globalLane, ...) where globalLane = CommandLane.Subagent.

The in-process queue lane (step 4) is reached only after the WebSocket handshake completes. So in-process dispatch happens, but only after passing through the gateway's full WebSocket auth gate that was designed for external CLI clients.

The pairing-gate failure

src/gateway/server/ws-connection/handshake-auth-helpers.ts:63-86shouldSkipBackendSelfPairing would bypass the pairing gate, but it requires sharedAuthOk = true (token or password auth passed). In a server install where gateway.auth.mode = "token" with an empty gateway.auth.token (a common config when the operator hasn't seeded a token), sharedAuthOk = false, so backend-self-pairing-skip never fires and the spawn fails.

shouldAllowSilentLocalPairing at :49-61 returns true for local clients with reason = "not-paired", but the silent pairing request flag does not auto-approve reason = "scope-upgrade" paths in our observed traces.

Live worker log at the moment of failure:

[gateway] security audit: device access upgrade requested
  reason=scope-upgrade
  device=6d3e97b4e247e137a7e07fc97dc07d62a9b5756f8ee04983bbf074a87bcd10f9
  ip=unknown-ip auth=token roleFrom=operator roleTo=operator
  scopesFrom=operator.read scopesTo=operator.admin
  client=cli  conn=e4e33139-c42e-4d6c-917a-cb3c47910da2
gateway connect failed: GatewayClientRequestError: pairing required
[ws] closed before connect conn=e4e33139-... peer=127.0.0.1:59136->127.0.0.1:18789
  code=1008 reason=pairing required

(client=cli is misleading — it's the agent runtime's spawn call, not an external CLI session.)

Reproduction (rough — need an openclaw maintainer to confirm minimal repro)

  1. Run openclaw gateway in a fresh container (STATE_DIR=/tmp/openclaw-fresh, no prior pairing).
  2. Configure gateway.auth.mode = "token" with gateway.auth.token = "" (or mode = "none" — both reportedly fail the same way for the spawn-time pairing gate).
  3. Concierge agent calls sessions_spawn({runtime: "subagent", agentId: "<some-native-agent>", mode: "run", task: "..."}).
  4. Observe pairing required (1008) in worker logs and the spawn tool returns an error.

Suggested fix directions (in priority order)

  1. In-process dispatch shortcut. When subagent-spawn.ts detects the gateway is running in the same process (e.g. a singleton handle injected at boot), call agentCommandFromIngress(...) directly and skip callGateway entirely. This honors the docs (in-process queue lane) and eliminates the WebSocket handshake for the same-process case.
  2. Auto-approve self-pairing for backend clients on local connections. Loosen shouldSkipBackendSelfPairing to also accept auth.mode = "none" (without requiring sharedAuthOk to be true), since "none" mode by design means the deployment trusts the network boundary.
  3. Document a config flag (e.g. gateway.subagentAuth.disable: true, mirroring the existing gateway.controlUi.dangerouslyDisableDeviceAuth) so operators of single-process server deployments can opt out of the pairing gate for the subagent dispatch surface specifically.

(1) is the cleanest because it makes the docs accurate and removes WebSocket overhead for in-process spawns. (2) is the smallest patch. (3) is the least invasive but requires operators to know about the flag.

Workaround (currently in use downstream)

Plugins that need autonomous delivery of long-running results spawn an in-plugin setInterval poller inside the tool's execute() and deliver to the user's channel directly, sidestepping sessions_spawn entirely. See the sidekyk-video and sidekyk-hf-video-generator plugins in sidekyk-verticals for the recipe.

Severity

Medium — blocks autonomous-delivery patterns built around sessions_spawn for any server-side openclaw install where the operator hasn't seeded a paired device. Workaround exists (in-plugin polling) but it's per-plugin boilerplate that shouldn't be necessary for what the docs say is a first-class queue-lane primitive.

Related downstream work

extent analysis

TL;DR

The most likely fix for the issue is to implement an in-process dispatch shortcut for sub-agents when the gateway is running in the same process.

Guidance

  • Identify if the gateway is running in the same process as the agent runtime and inject a singleton handle at boot to enable in-process dispatch.
  • Modify subagent-spawn.ts to call agentCommandFromIngress directly when the gateway is in the same process, skipping the WebSocket handshake.
  • Consider loosening shouldSkipBackendSelfPairing to accept auth.mode = "none" for local connections to auto-approve self-pairing for backend clients.
  • Document a config flag (e.g., gateway.subagentAuth.disable) to allow operators to opt out of the pairing gate for subagent dispatch.

Example

// subagent-spawn.ts
if (isSameProcess()) {
  // Call agentCommandFromIngress directly, skipping WebSocket handshake
  agentCommandFromIngress(...);
} else {
  // Proceed with WebSocket handshake
  callGateway({ method: "agent", ... });
}

Notes

The provided workaround using in-plugin polling can be used temporarily, but it's recommended to implement a proper fix to avoid unnecessary boilerplate code.

Recommendation

Apply the in-process dispatch shortcut workaround, as it is the cleanest solution and makes the documentation accurate. This approach removes the WebSocket overhead for in-process spawns and aligns with the intended behavior of the in-process queue lane primitive.

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

openclaw - 💡(How to fix) Fix sub-agent dispatch: in-process spawn hits gateway pairing gate (no in-process lane shortcut) [1 participants]