Merged
Size
S
Change Breakdown
Bug Fix70%
Testing30%
#59555fix(agents): pin subagent gateway calls to admin scope to prevent scope-upgrade pairing failures

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_spawn sub-agent calls fail with close(1008) "pairing required" on v2026.4.1. Every callSubagentGateway invocation in src/agents/subagent-spawn.ts delegates to callGateway without explicit scopes, causing callGatewayLeastPrivilege to negotiate the minimum scope per method independently. The first connection (e.g. agentoperator.write) silently pairs the device at a lower tier. Subsequent calls requiring a higher tier (e.g. sessions.patchoperator.admin) trigger a scope-upgrade handshake that the headless gateway-client cannot complete interactively.

  • Root Cause: callSubagentGateway (line 148–152 of subagent-spawn.ts) forwards params to callGateway without scopes. callGateway falls through to callGatewayLeastPrivilege, which resolves the minimum scope for each method via resolveLeastPrivilegeOperatorScopesForMethod. Because subagent lifecycle spans multiple scope tiers (sessions.patch/sessions.deleteoperator.admin, agentoperator.write), the device gets paired at a lower tier on the first call, and every subsequent higher-tier call triggers scope-upgrade. The gateway's message-handler.ts:806 intentionally forces silent: false for 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 callSubagentGateway to scopes: [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-critical silent: false enforcement in message-handler.ts for external devices.

  • What changed:

    • src/agents/subagent-spawn.ts: callSubagentGateway now injects scopes: params.scopes ?? [ADMIN_SCOPE] before forwarding to callGateway, 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 from spawnSubagentDirect carries scopes: ["operator.admin"].
  • What did NOT change (scope boundary):

    • message-handler.ts — the scope-upgrade silent: false security guard is untouched.
    • handshake-auth-helpers.tsshouldAllowSilentLocalPairing logic is untouched.
    • server.silent-scope-upgrade-reconnect.poc.test.ts — all existing security tests pass as-is.
    • call.ts and method-scopes.ts — the least-privilege resolution logic is untouched.
    • No gateway auth flow, bootstrap token, or device pairing logic is modified.

Reproduction

  1. Install openclaw v2026.4.1 with device pairing enabled
  2. Start a conversation and trigger sessions_spawn (e.g. ask the agent to run a long research task)
  3. Observe gateway-client log: reason=scope-upgrade scopesFrom=operator.read scopesTo=operator.admin
  4. Connection closes with code 1008 "pairing required"
  5. Sub-agent never starts; main agent reports spawn failure

Risk / Mitigation

  • Risk: Subagent gateway calls now always request operator.admin instead 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 authorizeOperatorScopesForMethod per-request, so method-level authorization is unchanged. (3) The callSubagentGateway wrapper respects params.scopes if 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

© 2026 · via Gitpulse