feat(gateway): authoritative sender attribution in all chat contexts - #13939
feat(gateway): authoritative sender attribution in all chat contexts#139390xyg3n wants to merge 2 commits into
Conversation
Prefix every inbound message with `[from NAME (uid:USER_ID)]` so the agent can always identify the speaker by their immutable platform user_id. The human-readable name is best-effort; the uid is the source of truth. Why --- Hermes today only attaches a `[display_name]` prefix in sessions where `is_shared_multi_user_session()` returns True, which requires the non-default `group_sessions_per_user: false`. That leaves two real-world problems: 1. Group chats with multiple humans sharing one agent (a team bot, a shared assistant) silently lose sender context the moment they run with the default per-user session isolation flag. 2. Even when the prefix fires, it's just the Telegram/Discord display name, which is mutable — users can rename themselves, two people can share a first name, and display names alone cannot be trusted for identity- sensitive decisions. For a single agent in a group with three humans, the model has no reliable way to answer "who is asking" — which breaks personalization, identity- gated instructions, and any reasoning that depends on the speaker. What changes ------------ * New gateway config key `attribute_sender` (default: True). * When enabled and `source.user_id` is present, every inbound message — DM, group, channel, thread — is prefixed with `[from NAME (uid:USER_ID)] <original text>` before it reaches the agent. * Name resolution order: 1. Env var `HERMES_USER_NAME_<user_id>` (operator override) 2. Platform display name (`source.user_name`) 3. Literal `unknown` (never omit the prefix once a uid is known) * Any user-supplied leading `[from ... (uid:...)]` is stripped before the canonical prefix is added, so messages can't trivially impersonate another sender by pasting a fake header. * When `attribute_sender` is False, the legacy behaviour (display-name-only prefix in shared sessions) is preserved unchanged. Tests ----- Extends `tests/gateway/test_shared_group_sender_prefix.py` with cases for DM attribution, group attribution, env-var override, `unknown` fallback, impersonation stripping, the legacy-fallback path, and the no-op path when `user_id` is missing. Existing tests continue to pass.
ebc9d3c to
36abd44
Compare
|
Reviewed this closely while solving the same problem on a Discord deployment (one agent shared by two family members in the same server). A few notes in support, plus an independent confirmation of the design's cache behavior since that was my main worry before adopting it. The injection point is the right one for KV-cache safety. The prefix is added to The impersonation strip is the part I didn't expect but matters most.
One small thing for consideration: since the prefix now fires on DMs too (the invariance argument is reasonable), single-human-DM users who don't opt out will see Relates to #35147 and #32417 (both still open, both asking for stable-ID attribution this PR already delivers). +1 to merging. |
teknium1
left a comment
There was a problem hiding this comment.
Thanks for tackling per-message sender attribution; current main still has the display-name-only shared-session prefix at gateway/run.py:10363-10369, so the underlying gap remains.
Problems
gateway/run.py:5449uses onlysource.user_id, but current session identity deliberately preferssource.user_id_alt(gateway/session.py:935). Signal populates that alternate UUID atgateway/platforms/signal.py:706-708; this does not yet deliver the claimed authoritative identity across platforms.gateway/run.py:5454introducesHERMES_USER_NAME_<user_id>as user-facing behavioral configuration.AGENTS.md:102-106requires this class of setting to live inconfig.yaml.- The added tests cover only Telegram-style
user_idvalues (tests/gateway/test_shared_group_sender_prefix.py:82-239), not alternate-ID precedence or config loading. The new global default also needs documentation alongsidewebsite/docs/user-guide/configuration.md:1648.
Suggested changes
- Use the existing
user_id_alt or user_ididentity precedence and add Signal/Feishu coverage. - Move name overrides to documented, platform-scoped
config.yamldata and add loader coverage.
Automated hermes-sweeper review.
| # | ||
| # Set ``attribute_sender: false`` in gateway config to restore the | ||
| # legacy behaviour (display-name-only prefix, shared sessions only). | ||
| if getattr(self.config, "attribute_sender", True) and source.user_id: |
There was a problem hiding this comment.
Please resolve the same canonical participant identity used by build_session_key (source.user_id_alt or source.user_id; gateway/session.py:935). Signal provides a stable UUID in user_id_alt (gateway/platforms/signal.py:706-708), so this condition and emitted value otherwise fail the PR's authoritative-ID guarantee on a supported platform.
| # prevent trivial impersonation by pasting a fake ``[from … ]`` | ||
| # header into the message body. | ||
| message_text = _SENDER_PREFIX_RE.sub("", message_text, count=1) | ||
| _env_name = os.environ.get(f"HERMES_USER_NAME_{source.user_id}") |
There was a problem hiding this comment.
This adds a new user-facing non-secret HERMES_* configuration mechanism. The repository policy in AGENTS.md:102-106 requires behavioral settings to use config.yaml; move canonical-name overrides into documented config data instead.
Why
Hermes today only attaches a
[display_name]prefix whenis_shared_multi_user_session()returns True, which requires the non-defaultgroup_sessions_per_user: false. That leaves two real-world problems for anyone running a single agent shared by multiple humans (team bot, shared assistant, support channel):For a single agent in a group with, say, three humans, the model has no reliable way to answer "who is asking", which breaks personalization, identity-gated instructions, and any reasoning that depends on the speaker.
What changes
attribute_sender(default:true).source.user_idis present, every inbound message (DM, group, channel, thread) is prefixed with[from NAME (uid:USER_ID)] <original text>before it reaches the agent.HERMES_USER_NAME_<user_id>(operator override, useful for mapping telegram/discord ids to canonical names)source.user_name)unknown(the prefix is never omitted once a uid is known, so downstream parsing can be unconditional)[from ... (uid:...)]pasted into a message body is stripped before the canonical prefix is added, so messages can't trivially impersonate another sender.attribute_senderisfalse, the legacy behaviour (display-name-only prefix in shared sessions, nothing in DMs) is preserved unchanged.Why DMs too
The prefix is applied to DMs as well. Two reasons:
user_idis known, without branching onchat_type.For users who only ever run single-human DMs and want raw text,
attribute_sender: falserestores the old behaviour.Tests
Extends
tests/gateway/test_shared_group_sender_prefix.pywith:unknownfallback when no display name is available[from X (uid:Y)]is removed)attribute_sender: falseuser_idis missingExisting tests in the file continue to pass (they don't set
user_id, so they exercise the fallback path).Run:
```
pytest tests/gateway/test_shared_group_sender_prefix.py -v
```
Rollout
attribute_sender: falsefor anyone who has prompt-tuned around the current behaviour.group_sessions_per_user/thread_sessions_per_userflags. Attribution and session isolation are now independent concerns.