Skip to content

fix(gateway): resolve delivery adapter for doorless routed profiles (#74787) - #74813

Closed
Tranquil-Flow wants to merge 1 commit into
NousResearch:mainfrom
Tranquil-Flow:fix/74787-delivery-adapter
Closed

fix(gateway): resolve delivery adapter for doorless routed profiles (#74787)#74813
Tranquil-Flow wants to merge 1 commit into
NousResearch:mainfrom
Tranquil-Flow:fix/74787-delivery-adapter

Conversation

@Tranquil-Flow

Copy link
Copy Markdown
Contributor

Bug

In a multiplex_profiles deployment that serves a profile with no live same-platform adapter, _adapter_for_source correctly returns None for the intake / authorization path (so a routed profile's message cannot ride the default profile's allowlist) — but the outbound delivery path inherited that None and dereferenced it. The agent hit AttributeError: 'NoneType' object has no attribute 'pause_typing_for_chat' at gateway/run.py:4663 and gateway/run.py:4814 when it needed to send an interactive approval prompt, clarify question, or status progress. The user never saw the prompt; tools.approval surfaced BLOCKED: Failed to send approval request to user even 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.py and gateway/run.py:

  1. New delivery-only resolver _delivery_adapter_for_source on GatewayAuthorizationMixin — 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.
  2. Turn status binding in _run_agent_inner switches from self._adapter_for_source(source) to self._delivery_adapter_for_source(source) — one binding change covers every downstream status / progress / approval / clarify callback site that uses ctx._status_adapter.
  3. Approval closure guard _approval_notify_sync gets a top-of-call guard for the residual None case. On None it logs the platform + chat_id and returns cleanly so tools.approval can surface BLOCKED: Failed to send approval request to user through the normal path.

The intake / authorization resolver is unchanged. Its fail-closed behavior is exactly what the reporter's #1 recommendation ("Keep _adapter_for_source exactly 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:

Suite Local result
test_delivery_adapter_resolver.py (new, 11 tests) 11/11 GREEN
test_multiplex_profile_authz.py 5/5 GREEN
test_profile_resolution.py 8/8 GREEN
test_turn_context.py 5/5 GREEN

RED-phase proof: ran the new test file against pristine upstream main (14db1a9) before any production change. 10/11 tests fail with AttributeError: '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 current upstream/main (3eebb99) post-build; SHA updated to f788c1f4664125e3991d2afddd7e163f04ebd243. Worktree clean (git diff --check clean, only expected files differ).

Resolver decision matrix covered (8 cells): None source, 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_adapter line in _run_agent_inner resolves 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 so tools.approval can surface BLOCKED: Failed to send approval request to user through 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_source resolves 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

Compliance

  • CONTRIBUTING.md: bug fix priority, minimal scope, no prohibited changes (no new core tool, no new HERMES_* env var, no speculative infrastructure, no outbound telemetry).
  • AGENTS.md "Footprint Ladder": extends existing code (GatewayAuthorizationMixin) rather than adding a new module.
  • Style matches surrounding code: getattr(...)-with-default duck typing, comments explain non-obvious design decisions (the parallel resolver contract, the relay-ingress carve-out, the deferred authorization behavior).
  • No junk files, no AI attribution, no new dependencies, no uv.lock churn (only one pyproject.toml-adjacent file? No, this PR does not touch pyproject.toml at all).

Auto-published by Moonsong via Path B automated pipeline (GPT-5.5 review verdict: APPROVED).

…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.
@Tranquil-Flow
Tranquil-Flow force-pushed the fix/74787-delivery-adapter branch from f788c1f to 8fdec36 Compare July 30, 2026 12:28
@Tranquil-Flow

Copy link
Copy Markdown
Contributor Author

Force-pushed to fix CI Check contributors / check-attribution failure. The build commit was authored by an unmapped identity (Moonsong bugfix build <builder@tranquil-flow.local>); the contributors check requires all commit authors to map to .github/CONTRIBUTORS.json. Amended to Tranquil-Flow <tranquil_flow@protonmail.com> (the identity used by the most recent Tranquil-Flow merges on upstream main). Single focused commit preserved: git rev-list --left-right --count upstream/main...HEAD = 0 1, 4 files / +480 / -2 unchanged, 29/29 tests still GREEN (11 new + 18 nearby suite), worktree clean. New SHA: 8fdec366601e6a8a003db60a335d3394e6c6a082.

@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/gateway Gateway runner, session dispatch, delivery area/config Config system, migrations, profiles sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages labels Jul 30, 2026
@teknium1

Copy link
Copy Markdown
Contributor

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 main.

Closing as implemented on main.

@teknium1 teknium1 closed this Jul 30, 2026
@teknium1 teknium1 added the sweeper:implemented-on-main Sweeper: behavior already present on current main label Jul 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/config Config system, migrations, profiles comp/gateway Gateway runner, session dispatch, delivery P2 Medium — degraded but workaround exists sweeper:implemented-on-main Sweeper: behavior already present on current main sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants