fix(agents): pin subagent gateway calls to admin scope to prevent scope-upgrade pairing failures#59555
Conversation
Greptile SummaryThis PR fixes a The fix is minimal and well-scoped:
The security trade-off — subagent connections now hold full Confidence Score: 4/5
Prompt To Fix All With AIThis 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 |
There was a problem hiding this comment.
💡 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".
0e0b58a to
35b24d8
Compare
|
Quick context on the approach for reviewers: |
…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
35b24d8 to
23d2a8e
Compare
obviyus
left a comment
There was a problem hiding this comment.
Reviewed latest changes; landing now.
* 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>
…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>
…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>
…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>
…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>
…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>
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.Reproduction
sessions_spawn(e.g. ask the agent to run a long research task)reason=scope-upgrade scopesFrom=operator.read scopesTo=operator.adminRisk / Mitigation
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.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)
Scope (select all touched areas)
Linked Issue/PR
Fixes #59428