fix(gateway): resolve delivery adapter for doorless routed profiles (#74787) - #74813
fix(gateway): resolve delivery adapter for doorless routed profiles (#74787)#74813Tranquil-Flow wants to merge 1 commit into
Conversation
…ousResearch#74787) The intake / authorization resolver `_adapter_for_source` is fail-closed by design: a served profile with no live same-platform adapter returns `None` so a different profile's allowlist cannot accept the message. Outbound delivery (status, progress, clarify, approval, replies) cannot use the same rule: an interactive approval prompt or a status update that *did* get authorized for a routed profile must still reach the user through a real adapter, or the agent surfaces `BLOCKED: Failed to send approval request to user` and any new command pattern becomes unapprovable for the rest of the session. Add a parallel delivery resolver `_delivery_adapter_for_source` that prefers the profile-owned adapter when one exists, falls back to the active / default profile's same-platform adapter when the routed profile has no door, and only returns `None` for a fully unresolvable source. The turn's status binding uses the new resolver so every downstream status / progress / approval / clarify callback reaches the right adapter. The approval path also guards the residual `None` case so a missing adapter degrades to a clean logged failure instead of crashing the agent thread with `AttributeError: NoneType has no attribute pause_typing_for_chat`. Fixes NousResearch#74787.
f788c1f to
8fdec36
Compare
|
Force-pushed to fix CI |
|
Thanks for the focused analysis and the care taken to preserve fail-closed authorization semantics. This is an automated hermes-sweeper review; the reported delivery guarantee is already implemented on current
Closing as implemented on main. |
Bug
In a
multiplex_profilesdeployment that serves a profile with no live same-platform adapter,_adapter_for_sourcecorrectly returnsNonefor the intake / authorization path (so a routed profile's message cannot ride the default profile's allowlist) — but the outbound delivery path inherited thatNoneand dereferenced it. The agent hitAttributeError: 'NoneType' object has no attribute 'pause_typing_for_chat'atgateway/run.py:4663andgateway/run.py:4814when it needed to send an interactive approval prompt, clarify question, or status progress. The user never saw the prompt;tools.approvalsurfacedBLOCKED: Failed to send approval request to usereven though intake was authorized. Practical effect: any new command pattern became unapprovable for the rest of the session. It looked like a policy gate and was actually a delivery failure.Fix
Three production changes, all in
gateway/authz_mixin.pyandgateway/run.py:_delivery_adapter_for_sourceonGatewayAuthorizationMixin— 4-tier ordering (receiving transport → relay ingress → profile-owned → active/default same-platform), never consults the cross-profile registry in a way that would let a secondary profile's allowlist accept a different profile's inbound._run_agent_innerswitches fromself._adapter_for_source(source)toself._delivery_adapter_for_source(source)— one binding change covers every downstream status / progress / approval / clarify callback site that usesctx._status_adapter._approval_notify_syncgets a top-of-call guard for the residualNonecase. OnNoneit logs the platform + chat_id and returns cleanly sotools.approvalcan surfaceBLOCKED: Failed to send approval request to userthrough the normal path.The intake / authorization resolver is unchanged. Its fail-closed behavior is exactly what the reporter's #1 recommendation ("Keep
_adapter_for_sourceexactly as-is — fail-closed — for intake, authorization and queue management") asks for. The new resolver is exclusively about which adapter emits the next user-visible message.Verification
11 production-path tests in
tests/gateway/test_delivery_adapter_resolver.py:test_delivery_adapter_resolver.py(new, 11 tests)test_multiplex_profile_authz.pytest_profile_resolution.pytest_turn_context.pyRED-phase proof: ran the new test file against pristine upstream
main(14db1a9) before any production change. 10/11 tests fail withAttributeError: 'GatewayRunner' object has no attribute '_delivery_adapter_for_source'— the 11th (the dereference guard) tests pre-existing production code at the approval closure and independently passes, proving the guard is a separate production-path change rather than a tautological helper test.Single focused commit:
git rev-list --left-right --count upstream/main...HEAD=0\t1. Rebased onto currentupstream/main(3eebb99) post-build; SHA updated tof788c1f4664125e3991d2afddd7e163f04ebd243. Worktree clean (git diff --checkclean, only expected files differ).Resolver decision matrix covered (8 cells):
Nonesource, doorless routed profile (the bug), profile-owned adapter, default profile, receiving transport ref (relay + chat-route), relay ingress, no live adapter for platform, and the parallel-design invariant that the intake resolver stays fail-closed.Turn status binding (2 cells): production-path guarantee that the turn's
_status_adapterline in_run_agent_innerresolves to the default adapter for a doorless routed profile, and to the profile-owned adapter when one exists.Approval dereference guard (1 cell): the only unguarded
ctx._status_adapter.<method>()site in the codebase. Pre-fix crashes the agent thread mid-execution; post-fix logs the platform + chat_id and returns sotools.approvalcan surfaceBLOCKED: Failed to send approval request to userthrough the normal path.Contributor / upstream parity
The reporter (hatschifuh) already implemented the same direction on the 0.18.2 line and explicitly said they would port it for 0.19.0 if welcome. This PR ports and adapts their design onto current main:
_delivery_adapter_for_sourceresolves in the same 3-tier order, ~16 outbound call sites collapse to one status-binding change in_run_agent_inner, and the dereference is guarded. Reporter's #1 ("keep the intake resolver fail-closed") and #4 ("guard the dereference") recommendations are honored verbatim.Pre-publication gates
74787or for thefix/74787*branch.74787,delivery adapter,_delivery_adapter_for_source,doorless routed,pause_typing_for_chat,_status_adapter, ormultiplex_profiles approval. Build-time competitor analysis (adjacent PRs feat(gateway): multi-account Telegram — one gateway, N bots, isolated sessions (#8287) #67455 by Hotragn and fix(gateway): stamp profile before busy/approval checks in handle_message #69156 by Shunkleburger) re-confirmed: neither addresses the resolver conflation.Compliance
CONTRIBUTING.md: bug fix priority, minimal scope, no prohibited changes (no new core tool, no newHERMES_*env var, no speculative infrastructure, no outbound telemetry).GatewayAuthorizationMixin) rather than adding a new module.getattr(...)-with-default duck typing, comments explain non-obvious design decisions (the parallel resolver contract, the relay-ingress carve-out, the deferred authorization behavior).uv.lockchurn (only onepyproject.toml-adjacent file? No, this PR does not touchpyproject.tomlat all).Auto-published by Moonsong via Path B automated pipeline (GPT-5.5 review verdict: APPROVED).