Skip to content

fix(agents): pin subagent gateway calls to admin scope to prevent scope-upgrade pairing failures#59555

Merged
obviyus merged 2 commits intoopenclaw:mainfrom
openperf:fix/59428-pin-subagent-admin-scope
Apr 2, 2026
Merged

fix(agents): pin subagent gateway calls to admin scope to prevent scope-upgrade pairing failures#59555
obviyus merged 2 commits intoopenclaw:mainfrom
openperf:fix/59428-pin-subagent-admin-scope

Conversation

@openperf
Copy link
Copy Markdown
Member

@openperf openperf commented Apr 2, 2026

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

@openclaw-barnacle openclaw-barnacle Bot added agents Agent runtime and tooling size: S labels Apr 2, 2026
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 2, 2026

Greptile Summary

This PR fixes a close(1008) "pairing required" regression introduced in v2026.4.1 where subagent spawn calls fail because per-method least-privilege scope negotiation pairs the gateway device at a lower tier (operator.write) on the first call, then triggers an interactive scope-upgrade handshake on subsequent admin-tier calls (sessions.patch, sessions.delete) that a headless local-loopback client cannot complete.

The fix is minimal and well-scoped:

  • src/agents/subagent-spawn.ts: callSubagentGateway now injects scopes: params.scopes ?? [ADMIN_SCOPE] before forwarding to callGateway, ensuring the device is always paired at operator.admin on the first (silent, local-loopback) handshake. The ?? operator correctly preserves any explicitly-supplied scopes value for future callers. All gateway call-sites within the file flow through this private helper, so no paths are missed.
  • src/agents/subagent-spawn.test.ts: A new test captures every callGatewayMock invocation and asserts scopes === ["operator.admin"] across all methods (sessions.patch, agent, etc.), directly exercising the pinning behavior.

The security trade-off — subagent connections now hold full operator.admin scope rather than per-method least-privilege — is explicitly acknowledged in the PR description and mitigated by: (1) loopback-only connectivity, (2) unchanged per-request authorizeOperatorScopesForMethod enforcement in the gateway, and (3) message-handler.ts scope-upgrade silent: false guard remaining untouched for external devices.

Confidence Score: 4/5

  • Safe to merge — the fix is minimal, well-reasoned, and correctly scoped to the private callSubagentGateway helper.
  • The implementation is clean: one targeted change in callSubagentGateway with a clear comment, a correct use of ?? to preserve explicit overrides, and a direct test that exercises the exact behavior being fixed. No unrelated files are modified and the security trade-off is clearly documented. Score is 4 rather than 5 only because the new test lacks a result.status assertion, which could allow a spawn regression to slip through undetected.
  • No files require special attention; both changed files are straightforward.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/agents/subagent-spawn.test.ts
Line: 178-196

Comment:
**Consider asserting spawn success in the scope-pinning test**

The test verifies that all captured gateway calls carry `["operator.admin"]`, but it doesn't assert that `spawnSubagentDirect` completed successfully. If an unrelated regression causes the function to return an error status after at least one gateway call, the test will still pass — even though spawning is broken. Adding `expect(result.status).toBe("accepted")` before the scope assertions makes the test a better regression guard and keeps it consistent with the first test in the suite.

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "fix(agents): pin subagent gateway calls ..." | Re-trigger Greptile

Comment thread src/agents/subagent-spawn.test.ts Outdated
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0e0b58a6f1

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/agents/subagent-spawn.ts Outdated
@openperf
Copy link
Copy Markdown
Member Author

openperf commented Apr 2, 2026

Quick context on the approach for reviewers:
The subagent lifecycle spans multiple scope tiers — sessions.patch / sessions.delete need operator.admin, while agent only needs operator.write. Without explicit scopes, callGatewayLeastPrivilege negotiates each call independently, which means the shared device can end up paired at a lower tier first. Any later call that needs a higher tier hits the scope-upgrade path, and since message-handler.ts rightfully forces silent: false there, a headless gateway-client has no way to complete the interactive approval.
I considered fixing this at the gateway level (e.g., allowing silent scope upgrades for local loopback clients), but that would weaken the security boundary — the silent: false guard on scope-upgrade exists for good reason and shouldn't be relaxed just to accommodate one caller.
The fix pins admin-only methods to ADMIN_SCOPE so the device is paired at the ceiling tier on the very first handshake. Non-admin methods like agent intentionally keep their least-privilege scope — this avoids flipping senderIsOwner and accidentally exposing owner-only tools to the subagent.
The latest force-push addresses the bot review feedback and tightens the test to assert per-method scope routing (admin methods get ["operator.admin"], others stay undefined).

@obviyus obviyus self-assigned this Apr 2, 2026
openperf and others added 2 commits April 2, 2026 19:36
…pe-upgrade pairing failures

callSubagentGateway forwards params to callGateway without explicit scopes,
so callGatewayLeastPrivilege negotiates the minimum scope per method
independently.  The first connection pairs the device at a lower tier and
every subsequent higher-tier call triggers a scope-upgrade handshake that
headless gateway-client connections cannot complete interactively
(close 1008 "pairing required").

Pin callSubagentGateway to operator.admin so the device is paired at the
ceiling scope on the very first (silent, local-loopback) handshake, avoiding
any subsequent scope-upgrade negotiation entirely.

Fixes openclaw#59428
@obviyus obviyus force-pushed the fix/59428-pin-subagent-admin-scope branch from 35b24d8 to 23d2a8e Compare April 2, 2026 14:08
Copy link
Copy Markdown
Contributor

@obviyus obviyus left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed latest changes; landing now.

@obviyus obviyus merged commit b40ef36 into openclaw:main Apr 2, 2026
25 checks passed
@obviyus
Copy link
Copy Markdown
Contributor

obviyus commented Apr 2, 2026

Landed on main.

Thanks @openperf.

ngutman pushed a commit that referenced this pull request Apr 3, 2026
* fix(agents): pin subagent gateway calls to admin scope to prevent scope-upgrade pairing failures

callSubagentGateway forwards params to callGateway without explicit scopes,
so callGatewayLeastPrivilege negotiates the minimum scope per method
independently.  The first connection pairs the device at a lower tier and
every subsequent higher-tier call triggers a scope-upgrade handshake that
headless gateway-client connections cannot complete interactively
(close 1008 "pairing required").

Pin callSubagentGateway to operator.admin so the device is paired at the
ceiling scope on the very first (silent, local-loopback) handshake, avoiding
any subsequent scope-upgrade negotiation entirely.

Fixes #59428

* fix: pin admin-only subagent gateway scopes (#59555) (thanks @openperf)

---------

Co-authored-by: Ayaan Zaidi <hi@obviy.us>
ancientitguybot-dev pushed a commit to KaiWalter/openclaw that referenced this pull request Apr 3, 2026
…openperf)

* fix(agents): pin subagent gateway calls to admin scope to prevent scope-upgrade pairing failures

callSubagentGateway forwards params to callGateway without explicit scopes,
so callGatewayLeastPrivilege negotiates the minimum scope per method
independently.  The first connection pairs the device at a lower tier and
every subsequent higher-tier call triggers a scope-upgrade handshake that
headless gateway-client connections cannot complete interactively
(close 1008 "pairing required").

Pin callSubagentGateway to operator.admin so the device is paired at the
ceiling scope on the very first (silent, local-loopback) handshake, avoiding
any subsequent scope-upgrade negotiation entirely.

Fixes openclaw#59428

* fix: pin admin-only subagent gateway scopes (openclaw#59555) (thanks @openperf)

---------

Co-authored-by: Ayaan Zaidi <hi@obviy.us>
steipete pushed a commit to duncanita/openclaw that referenced this pull request Apr 4, 2026
…openperf)

* fix(agents): pin subagent gateway calls to admin scope to prevent scope-upgrade pairing failures

callSubagentGateway forwards params to callGateway without explicit scopes,
so callGatewayLeastPrivilege negotiates the minimum scope per method
independently.  The first connection pairs the device at a lower tier and
every subsequent higher-tier call triggers a scope-upgrade handshake that
headless gateway-client connections cannot complete interactively
(close 1008 "pairing required").

Pin callSubagentGateway to operator.admin so the device is paired at the
ceiling scope on the very first (silent, local-loopback) handshake, avoiding
any subsequent scope-upgrade negotiation entirely.

Fixes openclaw#59428

* fix: pin admin-only subagent gateway scopes (openclaw#59555) (thanks @openperf)

---------

Co-authored-by: Ayaan Zaidi <hi@obviy.us>
@TheBlippo
Copy link
Copy Markdown

This is the one that actually got sub-agents working again on v2026.4.2 for me. The pairing required error was the last blocker. Great catch @openperf and smooth merge @obviyus.

lovewanwan pushed a commit to lovewanwan/openclaw that referenced this pull request Apr 28, 2026
…openperf)

* fix(agents): pin subagent gateway calls to admin scope to prevent scope-upgrade pairing failures

callSubagentGateway forwards params to callGateway without explicit scopes,
so callGatewayLeastPrivilege negotiates the minimum scope per method
independently.  The first connection pairs the device at a lower tier and
every subsequent higher-tier call triggers a scope-upgrade handshake that
headless gateway-client connections cannot complete interactively
(close 1008 "pairing required").

Pin callSubagentGateway to operator.admin so the device is paired at the
ceiling scope on the very first (silent, local-loopback) handshake, avoiding
any subsequent scope-upgrade negotiation entirely.

Fixes openclaw#59428

* fix: pin admin-only subagent gateway scopes (openclaw#59555) (thanks @openperf)

---------

Co-authored-by: Ayaan Zaidi <hi@obviy.us>
ogt-redknie pushed a commit to ogt-redknie/OPENX that referenced this pull request May 2, 2026
…openperf)

* fix(agents): pin subagent gateway calls to admin scope to prevent scope-upgrade pairing failures

callSubagentGateway forwards params to callGateway without explicit scopes,
so callGatewayLeastPrivilege negotiates the minimum scope per method
independently.  The first connection pairs the device at a lower tier and
every subsequent higher-tier call triggers a scope-upgrade handshake that
headless gateway-client connections cannot complete interactively
(close 1008 "pairing required").

Pin callSubagentGateway to operator.admin so the device is paired at the
ceiling scope on the very first (silent, local-loopback) handshake, avoiding
any subsequent scope-upgrade negotiation entirely.

Fixes openclaw#59428

* fix: pin admin-only subagent gateway scopes (openclaw#59555) (thanks @openperf)

---------

Co-authored-by: Ayaan Zaidi <hi@obviy.us>
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request May 9, 2026
…openperf)

* fix(agents): pin subagent gateway calls to admin scope to prevent scope-upgrade pairing failures

callSubagentGateway forwards params to callGateway without explicit scopes,
so callGatewayLeastPrivilege negotiates the minimum scope per method
independently.  The first connection pairs the device at a lower tier and
every subsequent higher-tier call triggers a scope-upgrade handshake that
headless gateway-client connections cannot complete interactively
(close 1008 "pairing required").

Pin callSubagentGateway to operator.admin so the device is paired at the
ceiling scope on the very first (silent, local-loopback) handshake, avoiding
any subsequent scope-upgrade negotiation entirely.

Fixes openclaw#59428

* fix: pin admin-only subagent gateway scopes (openclaw#59555) (thanks @openperf)

---------

Co-authored-by: Ayaan Zaidi <hi@obviy.us>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents Agent runtime and tooling size: S

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: sessions_spawn sub-agent fails with "pairing required" (1008) on v2026.4.1

3 participants