Subagent spawns unblocked from "pairing required" failures
Subagent spawning now works reliably — gateway calls are pinned to admin scope on first handshake, eliminating the scope-upgrade negotiation that was causing 1008 "pairing required" errors in v2026.4.1.
Sub-agent spawning broke in v2026.4.1 — every attempt to run a background task via sessions_spawn failed immediately with WebSocket close code 1008 "pairing required". The root cause was scope negotiation: each gateway call independently resolved the minimum scope for its method, causing the first connection to pair at a lower tier. Subsequent calls requiring higher privileges triggered a scope-upgrade handshake that the headless gateway-client could not complete, since the gateway intentionally requires interactive confirmation for scope upgrades.
The fix pins subagent gateway calls to the ceiling scope (operator.admin) on first handshake. All subsequent calls match the already-paired scope and never trigger an upgrade. Non-admin methods like agent retain least-privilege scoping, preserving the security boundary that prevents the caller from being treated as owner.
In the agents subsystem, subagent spawn calls now carry explicit admin scope from the start. This restores the ability to run long-running background tasks without the spawn process dying on connection setup.
View Original GitHub DescriptionFact Check
Summary
-
Problem:
sessions_spawnsub-agent calls fail with close(1008) "pairing required" on v2026.4.1. EverycallSubagentGatewayinvocation insrc/agents/subagent-spawn.tsdelegates tocallGatewaywithout explicitscopes, causingcallGatewayLeastPrivilegeto negotiate the minimum scope per method independently. The first connection (e.g.agent→operator.write) silently pairs the device at a lower tier. Subsequent calls requiring a higher tier (e.g.sessions.patch→operator.admin) trigger ascope-upgradehandshake that the headless gateway-client cannot complete interactively. -
Root Cause:
callSubagentGateway(line 148–152 ofsubagent-spawn.ts) forwards params tocallGatewaywithoutscopes.callGatewayfalls through tocallGatewayLeastPrivilege, which resolves the minimum scope for each method viaresolveLeastPrivilegeOperatorScopesForMethod. Because subagent lifecycle spans multiple scope tiers (sessions.patch/sessions.delete→operator.admin,agent→operator.write), the device gets paired at a lower tier on the first call, and every subsequent higher-tier call triggersscope-upgrade. The gateway'smessage-handler.ts:806intentionally forcessilent: falsefor scope-upgrade to prevent silent privilege escalation by external devices — this is correct security behavior, but it blocks the legitimate local gateway-client self-connection. -
Fix: Pin
callSubagentGatewaytoscopes: [ADMIN_SCOPE]so the device is paired at the ceiling scope (operator.admin) on the very first (silent, local-loopback) handshake. All subsequent calls match the already-paired scope and never trigger scope-upgrade. This preserves the security-criticalsilent: falseenforcement inmessage-handler.tsfor external devices. -
What changed:
src/agents/subagent-spawn.ts:callSubagentGatewaynow injectsscopes: params.scopes ?? [ADMIN_SCOPE]before forwarding tocallGateway, bypassing per-method least-privilege negotiation and ensuring a consistent admin-tier pairing.src/agents/subagent-spawn.test.ts: Added test asserting every gateway call fromspawnSubagentDirectcarriesscopes: ["operator.admin"].
-
What did NOT change (scope boundary):
message-handler.ts— the scope-upgradesilent: falsesecurity guard is untouched.handshake-auth-helpers.ts—shouldAllowSilentLocalPairinglogic is untouched.server.silent-scope-upgrade-reconnect.poc.test.ts— all existing security tests pass as-is.call.tsandmethod-scopes.ts— the least-privilege resolution logic is untouched.- No gateway auth flow, bootstrap token, or device pairing logic is modified.
Reproduction
- Install openclaw v2026.4.1 with device pairing enabled
- Start a conversation and trigger
sessions_spawn(e.g. ask the agent to run a long research task) - Observe gateway-client log:
reason=scope-upgrade scopesFrom=operator.read scopesTo=operator.admin - Connection closes with code 1008 "pairing required"
- Sub-agent never starts; main agent reports spawn failure
Risk / Mitigation
- Risk: Subagent gateway calls now always request
operator.admininstead of least-privilege per method. A compromised subagent process could theoretically invoke admin-only methods it previously could not reach in a single connection. - Mitigation: (1) Subagent gateway-client connections are local loopback only — they never traverse the network. (2) The gateway still enforces
authorizeOperatorScopesForMethodper-request, so method-level authorization is unchanged. (3) ThecallSubagentGatewaywrapper respectsparams.scopesif explicitly provided, preserving the ability to narrow scope for future callers. (4) Test added to verify the scope pinning behavior.
Change Type (select all)
- Bug fix
Scope (select all touched areas)
- Agents
- Subagent Spawn
Linked Issue/PR
Fixes #59428