Skip to content

fix(delegation): bind subagent.interrupt to the caller's session, like subagent.steer - #80988

Open
pierrenode wants to merge 1 commit into
NousResearch:mainfrom
pierrenode:fix/subagent-interrupt-ownership-check
Open

fix(delegation): bind subagent.interrupt to the caller's session, like subagent.steer#80988
pierrenode wants to merge 1 commit into
NousResearch:mainfrom
pierrenode:fix/subagent-interrupt-ownership-check

Conversation

@pierrenode

Copy link
Copy Markdown
Contributor

Summary

This week's 60e1f7517/a94ebf5f5/9d4ef04ed series hardened steer_subagent() to require exact owner_session_id + owner_transport (ContextVar-bound, unforgeable) + owner_session_record identity before a steer is accepted, closing a cross-session authority hole. interrupt_subagent() — which steer_subagent()'s own docstring says it mirrors ("The redirection-side mirror of interrupt_subagent()") — never got the same treatment: it takes only subagent_id and performs zero ownership validation. The subagent.interrupt RPC handler doesn't even resolve a caller identity before calling it.

_active_subagents is a bare module-level dict, not session- or profile-scoped. delegation.status (list_active_subagents()) returns every active subagent's id system-wide with no session filter, so any session can enumerate ids and then hard-stop a child it does not own via subagent.interrupt.

Fix

Mirrors steer_subagent()'s exact ownership contract:

  • tools/delegate_tool.py::interrupt_subagent() gains owner_session_id/owner_transport/owner_session_record kwargs, checked identically to steer_subagent(). owner_session_id=None (the default) deliberately preserves the internal in-process helper contract — the one existing caller (tests/agent/test_interrupt_compat.py) needs no changes.
  • tui_gateway/methods_session.py's subagent.interrupt handler now resolves caller authority via _current_session_steer_authority(), identical to subagent.steer's handler.
  • The only production client, ui-tui's agentsOverlay.tsx, never sent a session_id at all — without threading it through, the ownership check would have silently broken the interrupt button for every legitimate caller. Added a sessionId prop (AgentsOverlayPaneui.sid, mirroring ModelPicker's existing sessionId threading pattern) and pass session_id in the RPC params.

Testing

  • Added registry-level ownership tests for interrupt_subagent() (owned succeeds, foreign session denied, foreign transport with correct session_id denied) and a TestSubagentInterruptRPC class mirroring the existing TestSubagentSteerRPC coverage to tests/tools/test_subagent_steer.py.
  • Mutation-verified: stashed the backend fix, confirmed all 7 new authority-check tests fail on pre-fix code.
  • tests/tools/test_subagent_steer.py (41), tests/agent/test_interrupt_compat.py, tests/tools/test_delegate.py + 5 neighboring delegate/subagent suites (96 tests), and the full tests/test_tui_gateway_server.py (517/518 — the one failure is a pre-existing threading-timing flake in an unrelated write_json test, confirmed by isolated re-runs and against pre-fix code) all pass.
  • ui-tui: npm run typecheck clean; full vitest suite 138/138 files, 1528 passed.
  • ruff check clean on all changed Python files.

Note on a related open PR

#70899 ("Mission Control" async-delegation panel, branched 2026-07-24 before the steer_subagent hardening landed) adds a parallel send_to_subagent() whose docstring explicitly states it matches interrupt_subagent()'s current no-ownership-check posture, and touches the same four files this PR touches. The maintainer has commented it will be salvaged/expanded. This fix doesn't depend on or block that PR — it hardens a real, currently-exploitable gap in main today — but whichever lands second will likely need a rebase against the other.

…e subagent.steer

steer_subagent() (this week's 60e1f75/a94ebf5f5/9d4ef04ed series) was
hardened to require exact owner_session_id + owner_transport (ContextVar-
bound, unforgeable) + owner_session_record identity before a steer is
accepted -- closing a cross-session authority hole. interrupt_subagent(),
the function steer_subagent()'s own docstring says it mirrors, was never
given the same treatment: it takes only subagent_id and performs zero
ownership validation. The subagent.interrupt RPC handler didn't even
resolve a caller identity.

_active_subagents is a bare module-level dict, not session- or
profile-scoped. delegation.status (list_active_subagents()) returns every
active subagent's id system-wide with no session filter, so any session
could enumerate ids and then hard-stop a child it does not own via
subagent.interrupt.

Fix mirrors steer_subagent()'s exact ownership contract:
- tools/delegate_tool.py::interrupt_subagent() gains owner_session_id/
  owner_transport/owner_session_record kwargs, checked the same way
  steer_subagent() checks them. owner_session_id=None (the default)
  deliberately preserves the internal in-process helper contract (test
  harnesses, same-process callers) -- the one existing caller
  (tests/agent/test_interrupt_compat.py) needs no changes.
- tui_gateway/methods_session.py's subagent.interrupt handler now resolves
  caller authority via _current_session_steer_authority(), identical to
  subagent.steer's handler.
- The only production client, ui-tui's agentsOverlay.tsx, never sent a
  session_id at all -- without threading it through, the ownership check
  would have silently broken the interrupt button for every legitimate
  caller (invoking_transport/invoking_session would always resolve to
  None). Added a sessionId prop (AgentsOverlayPane -> ui.sid, mirroring
  ModelPicker's existing sessionId threading) and pass session_id in the
  RPC params.

Testing:
- Added registry-level ownership tests for interrupt_subagent() (owned
  succeeds, foreign session denied, foreign transport with correct
  session_id denied) and a TestSubagentInterruptRPC class mirroring the
  existing TestSubagentSteerRPC coverage (missing subagent_id, owned
  child interrupted, unknown child not-found, foreign session/transport
  denied, no invoking identity denied, recycled id does not inherit old
  authority) to tests/tools/test_subagent_steer.py.
- Mutation-verified: stashed the backend fix, confirmed all 7 new
  authority-check tests fail on pre-fix code (assert True is False /
  KeyError: 'error').
- tests/tools/test_subagent_steer.py (41), tests/agent/test_interrupt_compat.py,
  tests/tools/test_delegate.py + 5 neighboring delegate/subagent suites (96),
  and the full tests/test_tui_gateway_server.py (517/518, the one failure
  is a pre-existing threading-timing flake in an unrelated write_json test,
  confirmed by re-running in isolation 5x clean and against pre-fix code)
  all pass.
- ui-tui: npm run typecheck clean; full vitest suite 138/138 files,
  1528 passed after building the hermes-ink package dependency
  (pre-existing missing-build-artifact issue, unrelated to this change).
- ruff check clean on all changed Python files.

Note: PR NousResearch#70899 ("Mission Control" async-delegation panel, branched
2026-07-24 before the steer_subagent hardening landed) adds a parallel
send_to_subagent() whose docstring explicitly states it matches
interrupt_subagent()'s current no-ownership-check posture, and touches
the same four files this fix touches. The maintainer has commented it
will be salvaged/expanded. This fix does not depend on or block that
PR -- it hardens a real, currently-exploitable gap in main today -- but
whichever lands second will likely need a rebase.
@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/tui Terminal UI (ui-tui/ + tui_gateway/) tool/delegate Subagent delegation sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state labels Aug 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/tui Terminal UI (ui-tui/ + tui_gateway/) P2 Medium — degraded but workaround exists sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state tool/delegate Subagent delegation type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants