fix(honcho): pinUserPeer collapses the operator's accounts, not bot authors

with pinUserPeer on, resolve_author_peer_id returned None for every author,
so a bot dm's words were written under the human's pinned peer inside the
a2a session. the pin exists to unify one person's platform accounts. a bot
is not one of them.

bot: authors now resolve to their peer before the pin check, so a pinned
operator still gets bot speech attributed to the bot.
This commit is contained in:
Erosika
2026-09-04 15:38:54 -04:00
committed by Teknium
parent df1513b728
commit 9f2a9384dc
3 changed files with 10 additions and 5 deletions
+5 -1
View File
@@ -123,11 +123,15 @@ class SessionPeersMixin:
def resolve_author_peer_id(self, key: str, author_id: str | None, author_name: str | None = None) -> str | None:
"""Peer ID for the turn's author, or None to keep the session's peer: no author named, the
author IS the session's peer, or ``pinPeerName`` collapsing identities by operator request.
author IS the session's peer, or ``pinPeerName`` collapsing the operator's accounts. Bot authors
are never collapsed: pinned or not, a bot's words go under the bot's peer.
``author_name`` is a display name (attacker-influenceable), so it never becomes a peer ID."""
runtime_id = str(author_id).strip() if author_id else ""
if not runtime_id:
return None
# The pin collapses the operator's own accounts onto one peer; a bot is never one of those.
if runtime_id.startswith(BOT_AUTHOR_PREFIX):
return self._peer_id_for_runtime_id(runtime_id)
if self._config is not None and bool(getattr(self._config, "peer_name", None)) \
and getattr(self._config, "pin_peer_name", False) is True:
return None
+2 -2
View File
@@ -85,8 +85,8 @@ class TestA2aRouting:
provider._manager.get_or_create.assert_not_called()
def test_collapsed_bot_peer_keeps_the_default_user_peer(self):
"""pinUserPeer returns no author peer; the a2a session then falls back to the resolved peer."""
def test_unresolvable_bot_peer_keeps_the_default_user_peer(self):
"""When no author peer resolves, the a2a session still opens under the resolved peer."""
provider = _provider()
provider._manager.resolve_author_peer_id.return_value = None
@@ -99,9 +99,10 @@ class TestResolveAuthorPeerId:
mgr = _manager(_config(), runtime_id="7654321")
assert mgr.resolve_author_peer_id("telegram:dm1", "bot:") == "bot-"
def test_pin_peer_name_collapses_bot_authors_too(self):
def test_pin_peer_name_does_not_collapse_bot_authors(self):
"""The pin unifies the operator's accounts; a bot's words never land under the human's peer."""
mgr = _manager(_config(pin_peer_name=True), runtime_id="7654321")
assert mgr.resolve_author_peer_id("telegram:dm1", "bot:coder") is None
assert mgr.resolve_author_peer_id("telegram:dm1", "bot:coder") == "coder"
def test_display_name_never_becomes_a_peer_id(self):
"""Display names are attacker-influenceable on most platforms."""