Skip to content
UHPUHPDeveloper Guide
Independent developer guide. Not affiliated with HarnessRouter or the official UHP project.

Routing Identity in Multiplexed Agent Harnesses

A multiplexed harness should not independently rediscover who received an event, who may authorize it, where it executes, which session namespace owns it or which adapter may answer it. Resolve canonical identity once, preserve transport provenance, derive keys only afterward, and keep intake policy separate from delivery resolution.

Page reviewed 4 Oct 2026 Source and review policy

Evidence: Hermes PRs #115665 + #115666 + #115802 + #115848 + #120103Case-study protocol scope: 2026-09-12

A multiplexed agent gateway can have several valid ownership coordinates for one inbound event: the profile that owns the receiving transport, the profile authorized to admit the request, the profile that executes the turn, the home/configuration root used at runtime and the namespace used for session keys and persistence. Those coordinates may be related without being identical.

The dangerous design is to derive them independently at different stages. A route can be admitted correctly, then an adapter can build a queue key before the runner stamps the routed profile, or a later authorization/runtime lookup can fall through a different fallback chain. The result is a split identity: the same event is treated as belonging to different owners by different subsystems.

Nous Research merged Hermes PR #115665 on 19 September 2026 as ad651b825076d31f66cf77c76304c60ab5511a66, introducing one frozen RoutingIdentity per inbound gateway event. PR #115666, merged immediately afterward as c07708671d2952f1790f696fc134b1cb8b54ea96, routes adapter-side session-key derivation through one owner-aware seam so queues, batching and busy-state keys agree with the runner.

PR #115802, merged later on 19 September as 8df0a03793784205833f9e0db12395aa33ada433, closes the remaining ordering gap across the reviewed gateway ingress paths: identity is canonicalized before any queue/session/control key is derived. The regression is concrete: before the fix, /stop on a rejected route could reach agent:main and clear another lane’s active session; afterward the unresolved route is dropped before the handler/key path is entered.

PR #115848, merged on 19 September with merge commit 22d9d147b53987687f6c5c62b01d8061e6aedc0e, then separates two questions that phase 3 deliberately left implicit: which adapter received the event? and which adapter is allowed to answer it? Live intake policy now resolves only from receiving-transport provenance and fails closed when that provenance is absent under multiplexing. Delivery prefers the known receiving adapter, but a restored/synthetic source with no live provenance may use only the unique owner of (platform, runtime_profile); it does not borrow the default bot.

These changes are now included in stable v2026.9.21 / Hermes Agent v0.21.4. The phase-4 merge 22d9d147b53987687f6c5c62b01d8061e6aedc0e is an ancestor of the v0.21.4 tag commit d337b736aa1e8ebecfab043842d13e4a2d2f48a3. They do not revise UHP, MCP, ACP or A2A wire semantics, do not change Hermes’ serialized SessionSource.profile field or historical session-key format, and do not establish native UHP adoption by Hermes.

Trace a wrong-owner control to its first divergent key

Section titled “Trace a wrong-owner control to its first divergent key”

For a misrouted /stop, preserve the original event and log an owner trace through ingress, admission, queueing, session lookup and control dispatch. The Hermes canonicalization-first fix prevents unresolved traffic from acquiring the default lane before rejection.

transport provenance → canonical owner tuple → authorization
→ queue/session/control key
→ runtime → delivery owner

Use a stable event/attempt ID to join the records. Record transport profile, runtime profile, authorization home, runtime home, namespace and derived key at each seam; redact secret values. Find the first stage that diverges from the canonical decision rather than repairing the final transcript label alone.

Failure probeExpected invariant
Same user/chat through two botsKeys remain owner-distinct
Route rejected under multiplexingNo default lane is addressed
Restored source lacks live provenanceIntake authority is not invented
Two candidate delivery adaptersAmbiguity remains unresolved
Delayed event after runtime replacementStale generation cannot address replacement work

The last probe composes with managed-runtime attestation; the article’s routing tuple alone is not a worker-generation proof. If old records are already in the wrong store, use durable-state repair after preventing new writes. Do not rewrite provenance to make a trace appear consistent.

Identity is a tuple, not one profile string

Section titled “Identity is a tuple, not one profile string”

Hermes’ reviewed RoutingIdentity makes the distinction explicit. It carries:

CoordinateMeaning
transport_profileprofile that owns the receiving adapter/credential
runtime_profileprofile whose agent/runtime executes the turn
authorization_homehome whose admission policy authorizes the source
runtime_homehome/configuration root used by the executing runtime
multiplexedwhether multiplex ownership rules apply
transport provenanceweak reference to the live receiving transport; provenance, not identity equality

The object also exposes derived session/store coordinates such as the namespace and session_key_profile. Inside the identity, the default owner is explicit rather than encoded as an ambiguous None fallback.

That matters because transport owner and runtime owner can legitimately differ. A shared bot may receive a message under one profile and route it to a satellite profile for execution. Treating either coordinate as the universal owner would erase information required by authorization, runtime scoping, persistence or later delivery.

Resolve once at ingress — before any key

Section titled “Resolve once at ingress — before any key”

PR #115665 centralizes the decision in resolve_identity(...). The runner uses that path when admitting primary sources and when stamping routed secondary events. Once resolved, the frozen identity is pinned to the source as wire-invisible process-local state.

PR #115802 makes the ordering requirement explicit at the adapter boundary. BasePlatformAdapter._canonicalize(source) is called first in the reviewed fresh-message, text-queue, active-session/busy and source-key paths, with Telegram photo/album routing covered as well. Runner-side duplicate stamping is folded through one GatewayAdapterLifecycleMixin._canonicalize seam.

The architectural sequence is therefore:

  1. observe the transport and requested/routed destination;
  2. resolve/canonicalize the full owner tuple once;
  3. fail explicitly when the route cannot be resolved under multiplexing;
  4. attach that decision and transport provenance to the work item;
  5. only then derive queue, batch, busy-state, session or control keys; and
  6. make downstream readers consume the same identity rather than independently repeating route/default logic.

Under multiplexing, unresolved routes are dropped/rejected at the first canonical seam instead of being keyed into agent:main. Outside canonical ingress, legacy/restored/hand-built sources can still use compatibility paths, but phase 4 constrains what those paths may infer from incomplete provenance.

Intake and delivery are different authority questions

Section titled “Intake and delivery are different authority questions”

PR #115848 replaces the old all-purpose _adapter_for_source resolver with two explicit seams.

_intake_adapter_for(source) answers which bot actually received this event? It uses live provenance only: the registered receiving transport, the process-level relay socket where applicable, or the canonical identity’s transport_profile after a reconnect. In a multiplexed gateway, a restored or hand-built source with none of that provenance returns None. That fail-closed result is used where the receiving bot’s policy is itself the authority: ignored-channel checks, startup re-dispatch of still-live events, /sethome relay fronting and the follow-up on_processing_start hook.

_delivery_adapter_for(source) answers which bot may answer this source now? When a receiving adapter is known, delivery stays on that adapter even if execution was routed elsewhere; this preserves conversation continuity for shared-bot and route-override topologies. If live provenance is absent, the resolver may use the unique owner of (platform, runtime_profile). A disconnected or ambiguous secondary resolves to no delivery adapter rather than silently borrowing the default bot.

The distinction is subtle but security-relevant. A restored session can contain durable runtime/profile state without proving which credential originally admitted the event. Treating that runtime owner as historical intake provenance can re-run admission-only behavior through a guessed bot. Conversely, refusing all restored delivery would make legitimate resume/completion paths unusable even when one runtime owner is unambiguous. The two-seam model preserves both constraints instead of forcing one resolver to answer incompatible questions.

Hermes documents the resulting transport matrix in its gateway instructions. The durable invariant is:

TopologyRuntime ownerIntake authorityDelivery authority
per-credential bot, no routeownerowner adapterowner adapter
shared credential → satelliterouted profilereceiving adapterreceiving adapter
shared bot → profile with its own botrouted profilereceiving adapterreceiving adapter
secondary bot → explicit default runtimedefault / agent:mainreceiving adapterreceiving adapter
restored/synthetic, no live provenancestored runtime profilenone / fail closedunique (platform, runtime_profile) owner, else none

This is a host-runtime ownership matrix, not a new protocol message or wire field.

Control commands are routing-sensitive too

Section titled “Control commands are routing-sensitive too”

Identity errors are not limited to transcript placement. PR #115802 demonstrates that control delivery depends on the same key ordering.

Its regression case covers two bots on one multiplexed gateway with the same human/chat identifier. Before canonicalization-first ingress, a rejected route could derive the default lane key and let /stop clear the wrong active session. The fixed path drops the event before that fallback-derived key reaches the handler. The same owner-first rule applies to /new, /reset and clarify replies in the reviewed test matrix.

Phase 4 does not change those key semantics. Instead, it prevents restored/provenance-less sources from re-entering intake policy through a guessed runtime bot and makes outbound/control-adjacent delivery use the same explicit delivery resolver as authorization fallbacks.

That yields a general control-plane rule: if interruption, reset, pending clarification, busy-state or admission state is keyed by session identity, canonical ownership must be established before those controls can address a lane, and transport-only policy must not be reconstructed from runtime state after provenance has been lost.

Every owner-sensitive reader must consume the same decision

Section titled “Every owner-sensitive reader must consume the same decision”

After #115665, the pinned identity is the first source for several previously independent readers:

  • transport-owner lookup;
  • authorization-home lookup;
  • runtime-home/profile resolution;
  • adapter-side session-key profile selection; and
  • session-store profile resolution.

PR #115848 adds an explicit reader split rather than another fallback chain: intake-sensitive sites call _intake_adapter_for, while sends, edits, typing, progress, pickers, pending slots, busy-queue behavior and provenance-less authorization fallback consume _delivery_adapter_for.

Copies of a source use the identity-aware replacement helper so transport provenance and the frozen routing decision survive the copy. PR #115802 also routes topic-recovery copies through replace_source and Slack thread sources through the canonical source builder rather than a bare SessionSource constructor.

This is a general multi-tenant runtime rule: once an event has crossed an ownership/admission boundary, later code should carry the admitted identity and provenance forward rather than asking ambient process state to answer the ownership question again. If provenance is genuinely unavailable, the system should say which later decisions can be reconstructed and which must fail closed.

Routing identity must also bind stores, caches and ambient readers

Section titled “Routing identity must also bind stores, caches and ambient readers”

Merged Hermes PR #120103 on 23 September 2026 extends the same ownership rule beyond ingress and session-key construction. It groups seven background/control-plane fixes where work had already been routed to the correct profile but a later reader still consulted launch-process state, a process-global cache or a store with the wrong ownership boundary. The merge commit is bd970b0588bb663dde5569b3e60eeb0e15541e21. GitHub ancestry now confirms that merge is included in stable v2026.9.24 / Hermes Agent v0.21.5 at tag commit f97608f178d1ffeca59860195ab7da295f7c8e5f.

The clearest security case is /v1/responses. Before the salvaged #120009 fix, every mirrored /p/<profile>/ route authenticated with that profile’s key but shared one ResponseStore created at the API adapter’s launch home. Conversation names are client-chosen, so profile B could use the same conversation name as profile A and receive A’s stored transcript, instructions and session id as context, become that conversation’s newest tip, or GET/DELETE A’s stored response if it knew the response id. The corrected path keeps one ResponseStore per routed profile home and captures that store in streaming state when the request begins, so a later asynchronous snapshot cannot drift to another profile after request scope unwinds.

The same PR makes a second distinction explicit: the owner of a resource is not always the routed profile. The delivery ledger is intentionally a host-process recovery record, so a served profile’s delivery row belongs in the launch home’s state.db, where the single gateway boot sweep looks for recoverable work; adapter_profile preserves the per-bot routing coordinate. By contrast, log redaction policy, API response history, MCP registry identity, child-environment scrubbing and terminal-bridge decisions are profile-sensitive and must use the routed/recorded profile rather than the launch profile.

PR #120103 therefore separates two identities that ambient HERMES_HOME had been conflating. get_routing_process_hermes_home() pins the launch home only for decisions such as “is this a routed profile?” while ordinary owner-aware lookups continue to follow the bound profile scope. Its follow-up also fixes profile service cleanup so deleting or renaming a secondary profile cannot mistake the host multiplexer service for that profile’s own service. Log formatting similarly re-enters the log record’s stamped profile before applying security.redact_secrets, so a launch-profile redaction opt-out cannot make another profile’s credentials appear raw in its routed log file.

This sharpens the earlier rule: canonical ingress identity is necessary but not sufficient. Every store, cache, formatter, recovery ledger and deferred reader must have an explicit owner predicate, and that predicate must match the resource’s lifecycle rather than whichever process-global profile happens to be visible when the code runs.

Session keys expose split identity quickly

Section titled “Session keys expose split identity quickly”

PR #115666 demonstrates why canonical identity is useful only if derived keys also pass through a single seam. Before the fix, Yuanbao’s dispatch middleware called the free build_session_key(source) without a profile. For a secondary-owned bot, its per-group dispatch queue and RecallGuard bookkeeping therefore landed under agent:main: while the runner handled the same message under agent:<owner>.

The upstream red probe records the mismatch explicitly for owner acme:

  • adapter queue key before the fix: agent:main:yuanbao:group:grp-1:u1;
  • owner-aware seam key: agent:acme:yuanbao:group:grp-1:u1.

That is not a cosmetic prefix error. Two bots can then share a serial queue or busy/recall state that should have been owner-scoped, while the actual turn is stored and executed in another namespace.

Hermes sends adapter-side key construction through BasePlatformAdapter._source_session_key / _event_session_key. The same seam absorbs the Weixin text-batch path, Telegram photo/album batching, Slack thread keys and Raft wake keys. An advisory CI pattern, P32, flags free build_session_key( and direct SessionSource( construction under adapter directories except the canonical base seam.

Phase 3 closes key ordering; phase 4 closes adapter-role ambiguity

Section titled “Phase 3 closes key ordering; phase 4 closes adapter-role ambiguity”

The earlier phase-2 evidence explicitly left four direct SessionSource( hits in adapter code for later work. PR #115802 addresses that documented boundary: its P32 check reports 0 direct-construction hits after moving Slack thread creation through the canonical builder and identity-preserving copies through replace_source.

The same PR removes the runner’s duplicated profile-stamping sites in favor of the canonical lifecycle seam. Its tests cover fresh, batched, busy and control ingress plus a shared-bot-to-satellite route.

PR #115848 then addresses a different residual ambiguity: canonical identity can tell the harness both transport and runtime owners, but a single adapter resolver still cannot safely serve both receiving-transport policy and outbound delivery. The split resolver makes that distinction load-bearing and specifically prevents provenance-less restored sources from being re-dispatched through an adapter inferred only from runtime state.

This still does not justify a global claim that every historical, restored, synthetic or third-party source in Hermes must carry canonical live-ingress identity. The verified narrower claim is stronger and useful: for reviewed multiplexed live ingress, identity precedes lane/control-key derivation; for provenance-less sources, intake fails closed while delivery is limited to a unique runtime owner rather than default-bot fallback.

The upstream validation reported for #115665 and #115666 includes:

  • focused identity invariants covering reader agreement, unresolved multiplex routing versus explicit default behavior, identity-preserving source copies and owner-aware adapter keys;
  • a real runner A→B→A sequence across two homes and a shared-bot route, where identity, ambient HERMES_HOME, authorization home, adapter key and runner key agree without cross-turn contamination; and
  • 9,846 passed, 0 failed, 0 flaky across the stated gateway/scripts/platforms test command for the stacked phase-2 cutoff.

PR #115802 adds:

  • red-before/green-after tests for two bots sharing the same external chat/user id, including /stop and clarify isolation;
  • a live rejected-route /stop repro where the pre-fix path cleared agent:main but the fixed path drops before the handler;
  • E2E A→B→A, satellite, busy-path and three negative-drop cases through the installed handlers;
  • 9,847 passed, 0 failed across the stated gateway/plugins/platforms/scripts command, with one unrelated pre-existing flaky test passing on retry; and
  • 0 P32 hits in the adapter construction/session-key pattern check.

PR #115848 adds:

  • a transport-matrix regression covering live per-credential, shared-bot, satellite, inverse-default and reconnect cases plus restored sources;
  • red-before/green-after evidence that a provenance-less restored row no longer resolves a receiving adapter for intake;
  • a live startup-restore probe where pre-fix code re-dispatched two events, including one through a guessed bot, while the fixed path drains only the provenance-bearing event (drained == 1);
  • authorization checks showing a provenance-less source uses the same unique delivery owner that will answer it; and
  • 11,085 passed, 0 failed across the stated gateway/plugins/platforms/cron test command, with one unrelated timing flake reported green on rerun, plus zero profile-scope pattern findings and clean lint/compatibility/diff checks.

PR #120103 adds red-before/green-after invariants for routed response-store isolation, launch-home pinning, routed log-redaction policy, delivery-ledger recovery, profile reconnect and kanban/cron ownership. Its review follow-up reports 29,405 passed and 4 failed across tests/hermes_cli tests/agent tests/gateway; upstream states the same four failures reproduce on a pristine main worktree, with the timeout-sensitive file passing alone. That is upstream implementation evidence, not independent certification by this site.

Those are upstream implementation results, not independent benchmarking by this site. The phase-4 merge remains released since v2026.9.21 / Hermes Agent v0.21.4, and #120103 is now also included in stable v2026.9.24 / v0.21.5. Release inclusion changes the maturity label, not the scope of the underlying routing evidence.

  1. Resolve ownership once at admission. Transport, authorization, runtime and persistence coordinates should come from one explicit routing decision, not several ambient fallback chains.
  2. Canonicalize before key derivation. Queue, batch, busy-state, session and control keys must not be built from a source whose owner has not yet been resolved.
  3. Keep distinct coordinates distinct. Transport owner and runtime owner may legitimately differ; preserving both is safer than collapsing them into one overloaded profile field.
  4. Separate intake authority from delivery authority. The adapter that can prove it received an event is not always the same thing as the adapter that can safely deliver a resumed or deferred result.
  5. Fail unresolved multiplex routes closed. “Could not resolve owner” must not silently become the default owner in a multi-profile process.
  6. Do not invent lost provenance. A persisted runtime profile can support a later unique-owner delivery decision without proving which credential originally admitted the event.
  7. Derive keys from the admitted identity. Queues, batching, busy-state maps, persistence keys, caches and recovery maps must use the same owner seam as execution.
  8. Preserve identity across copies and deferred handoffs. Copying only serialized/public fields can discard process-local provenance that later authorization or routing needs.
  9. Treat control delivery as owner-sensitive. /stop, reset/new-session controls and clarification replies must address the canonical lane, not a fallback-derived default lane.
  10. Make the default owner explicit internally. Null sentinels are dangerous when the same null can mean default, unknown or wildcard in different layers.
  11. Test with conflicting owners and missing provenance. Strong regressions use distinct homes, A→B→A routing and restored sources that lack a receiving transport.
  12. Lint secondary derivations. Canonicalization erodes when adapter/plugin authors can freely reconstruct session keys, source objects or adapter ownership beside the official seams.
  13. Assign every durable or cached resource an explicit owner. A profile response store, process recovery ledger, log formatter and client cache may legitimately have different ownership coordinates; scope each to the resource’s actual lifecycle instead of treating the active profile as universal.

A multiplexed harness should be able to demonstrate at least these cases:

  • shared transport owned by A routes execution to B → authorization remains bound to the admitting transport owner where intended, runtime/home/session namespace resolve to B, and every key reader agrees;
  • unresolved/rejected route while multiplexing → event is dropped or explicitly rejected before key derivation, never executed or controlled under default;
  • two bots share the same external user/chat id → their active-session, busy, clarification and control lanes remain isolated;
  • /stop, /new or /reset arrives on a rejected or differently owned route → it cannot reach another owner’s active run;
  • restored/synthetic source with no live transport provenance → intake-sensitive policy resolves no receiving adapter, while delivery succeeds only if (platform, runtime_profile) has one unambiguous owner;
  • disconnected/ambiguous secondary owner → delivery returns no adapter rather than borrowing the default bot;
  • default/non-multiplexed event → historical default namespace remains byte-stable;
  • source copied for deferred/background work → routing identity and transport provenance survive the copy;
  • secondary-owned bot uses per-chat/group batching → queue key equals the owner-aware runner/session key rather than agent:main:;
  • two secondary bots share the same external chat/group id → their queue/busy/recall state remains isolated by owner namespace;
  • two authenticated /p/<profile>/v1/responses clients use the same conversation name → neither can read, chain onto, become the tip of, GET or DELETE the other profile’s stored response state;
  • routed logging runs after the producer thread hands the record to a process-level QueueListener → redaction policy still comes from the record’s profile rather than the launch profile; and
  • canonical adapter code introduces a free session-key derivation or direct source constructor → static analysis/test evidence identifies the second seam before release.

Profile isolation asks a broader question: do secrets, configuration, MCP connections, child environments, background work, persistence and delivery remain inside the correct owner scope across the whole lifecycle?

Routing identity is one enabling mechanism for that invariant. It does not replace owner-scoped secret resolution, scoped caches, destination-owned child environments or deferred-work context propagation. Instead, it provides a durable answer to the question those subsystems repeatedly need: which ownership coordinates were actually admitted for this work item? Phase 4 adds the equally important qualifier: which later decisions still require live receiving-transport provenance, and which may be reconstructed from an unambiguous runtime owner? PR #120103 adds another qualifier: which resources are profile-owned, and which are intentionally host/process-owned?

This page documents a harness-runtime architecture pattern using Hermes source evidence.

  • UHP: does not standardize Hermes profiles, RoutingIdentity, HERMES_HOME, Hermes session-key prefixes, profile-local response stores or the intake/delivery adapter split. A UHP server may multiplex internal harness instances, but UHP does not acquire these local owner coordinates from PRs #115665/#115666/#115802/#115848/#120103.
  • MCP: may be one resource controlled by an owner-scoped runtime, but these merges do not change MCP transport, authentication or tool semantics.
  • ACP: may carry session/control messages to an agent runtime, but these merges do not change ACP session identifiers or wire behavior.
  • A2A: is unchanged; Hermes’ local gateway routing identity does not become an A2A agent-identity primitive.
  • HarnessRouter: current stable/reviewed main snapshot is v0.23.12 / 5c2d3bb3e8b07297f2e82fd3a5075d430fb8f446 at this cutoff. Nothing in these Hermes merges is new HarnessRouter conformance evidence or native Hermes UHP adoption.

This is an independent technical guide. “Routing identity” here is an architectural description grounded in the cited Hermes implementation; it is not a new interoperability standard or a vendor certification.