fix(gateway): guard shared-audience chats against owner/backend leaks - #48060
fix(gateway): guard shared-audience chats against owner/backend leaks#48060aldoeliacim wants to merge 1 commit into
Conversation
egilewski
left a comment
There was a problem hiding this comment.
requesting changes
The confidentiality guard still misses Hermes forum/topic audiences that are multi-person but are not represented as group, channel, thread, or a shared session. In gateway/session.py, the new block only fires when context.source.chat_type in ("group", "channel", "thread"), a thread_id is present, or shared_multi_user_session is true. With the default group_sessions_per_user=True, a SessionSource(chat_type="forum", thread_id=None) keeps shared_multi_user_session=False, so the shared-audience block is absent even though Hermes treats forum as a group-like/shared chat type elsewhere.
Failed check:
TMPDIR=... XDG_CACHE_HOME=... XDG_DATA_HOME=... HERMES_HOME=... PYTHONDONTWRITEBYTECODE=1 /home/mac/hermes-agent/.venv/bin/python -B - <<'PY'
from gateway.session import SessionSource, build_session_context, build_session_context_prompt
from gateway.config import GatewayConfig, Platform, PlatformConfig
cfg = GatewayConfig(platforms={Platform.FEISHU: PlatformConfig(enabled=True, token="fake")})
src = SessionSource(platform=Platform.FEISHU, chat_id="oc_topic", chat_name="Ops Topic", chat_type="forum", user_id="ou_owner", user_name="Owner")
ctx = build_session_context(src, cfg)
prompt = build_session_context_prompt(ctx)
print("shared=", ctx.shared_multi_user_session, "marker=", "Shared audience" in prompt)
PY
Output on the PR head:
shared= False marker= False
This is reachable, not just a synthetic spelling: Feishu maps topic/thread/forum chat types to forum, and gateway auth treats forum alongside group-like chats. Since the PR’s stated invariant is any non-DM shared audience, the guard needs to include forum and add a focused regression test.
Security evidence:
- trust boundary: authorized owner/admin actions can originate from shared gateway chats, but normal replies go back to every participant in the origin chat.
- source/sink/invariant:
SessionSource.chat_typeandthread_iddecide whetherbuild_session_context_prompt()warns the model before gateway responses are generated. - current-main reproduction: current main has no shared-audience marker for group/channel/thread/shared cases.
- PR-head validation: PR head adds the marker for group, channel, group thread, and shared group, and keeps DM/local exempt.
- positive/negative cases: direct probes confirmed group/channel/thread/per-user-isolated group positive and DM/local negative; the forum probe remains a false negative.
- residual bypass search:
forumis an established gateway chat type, including Feishu topic/thread/forum mapping and group-like authorization handling. - reviewer-tool status: CodeRabbit skipped because local review already found the residual forum bypass.
Signed: GPT-5.5-xhigh in Codex
Owner-authorized admin commands issued from inside a group caused the agent to post backend internals (allowlists, bridges, silos, LIDs) and other members' phone fragments + names into the shared group chat — the operational debrief defaulted to its origin (the group) instead of the owner's private channel. Root cause: build_session_context_prompt() had no confidentiality/ audience boundary. It told the agent WHO is speaking (Identity safety) but never that a non-DM chat is a shared, non-owner audience. Fix: inject a cache-stable 'Shared audience — confidentiality' block for any multi-person (non-DM) audience — group/channel/thread or any session flagged shared_multi_user_session. Gated on audience shape, not the group_sessions_per_user knob, so a per-user-isolated group (whose replies still land in the shared channel) is covered too. Instructs the agent to route owner/backend operational detail to the owner's private channel and post at most a neutral ack to the shared chat. Also locks the session-coherence invariant (two users in a shared group resolve to one session_id) so the split-brain that made the agent contradict itself across per-user shards can't return via a default flip. Tests: TestSharedAudienceConfidentialityGuard (group/thread/channel/ per-user-isolated fire; DM + local exempt) and test_shared_group_two_users_share_one_coherent_session.
3b27279 to
cd8496c
Compare
|
suggesting changes The confidentiality guard still misses Hermes forum audiences when the source is represented as So a forum-shaped multi-person audience can still omit the shared-audience confidentiality instructions and default owner/backend detail back into the shared chat. This is the same class of leak the PR is trying to close; please include Security evidence:
Signed: GPT-5.5-xhigh in Codex |
|
Thanks for this, and for the thorough incident write-up and behavior-contract tests — the ProgramaLoL failure is real and well documented. We're going to close this one, though. The direction is prompt-side mitigation: it adds a "please don't reveal owner/backend detail in a shared chat" block to the cached system prompt. That's a model-behavior nudge, not a chokepoint — it asks the model not to leak rather than making the leak impossible, so a model can still route the full debrief to the shared origin and ignore the guidance. We try not to accumulate "please behave" blocks in the per-conversation prompt; each one dilutes attention, and none of them actually guarantees the property they describe. The failure you hit is a real problem, but the fix that would hold is on the routing/sink side — owner-private or backend-internal output should never default to a shared origin channel just because the triggering command arrived there, independent of whether the model was told about the audience. That's a stronger place to enforce it than a prompt string. Closing on the approach, not the quality of the work — the reproduction, the cache-safety reasoning, and the invariant-style tests were all solid. If you want to take a run at the routing-side version, happy to look. |
Restart lifecycle replies are deterministic gateway-internal output, but the adapter sinks treated them as ordinary text and defaulted them back to the origin chat. When /restart was triggered from a group, the immediate command reply and post-restart notification could therefore expose operational status to the shared audience. Add a PrivateReply wrapper and adapter sink helper that prefer a real send_private_notice implementation for shared-audience sources, while refusing to reuse the default public fallback for confidential text. /restart now persists the requester user id for the restarted process, marks restart status as private, and sends only a neutral public fallback when private delivery is not available. Related NousResearch#48060 Related NousResearch#24365 Co-authored-by: Aldo <17973757+aldoeliacim@users.noreply.github.com> Co-authored-by: Teknium <127238744+teknium1@users.noreply.github.com>
What
Adds a cache-stable "Shared audience — confidentiality" block to the gateway session-context system prompt for any non-DM (multi-person) audience — group, channel, thread, or any session flagged
shared_multi_user_session.Why
A gateway session prompt tells the agent who is speaking but nothing about who else can read the chat. When an authorized owner issues an administrative/backend command from inside a group, the agent's natural completion is to post its full operational debrief back to the origin — which is the group — exposing it to every member.
Observed failure: an owner onboarding command sent in a WhatsApp group caused the agent to post backend internals (allowlists, the bridge, session/silo mechanics, LIDs, gateway restart behavior) and other members' phone fragments + the names behind them into the shared group. The completion defaulted to origin routing because nothing in the prompt marked the group as a shared, non-owner audience.
How
build_session_context_prompt()now emits a confidentiality block when the audience is multi-person. It is gated on the audience shape (group/channel/thread or the shared-session flag), not on thegroup_sessions_per_userknob — because a per-user-isolated group still posts every reply to the shared channel, so the audience is shared regardless. The block instructs the agent to route owner-private / backend-internal detail to the owner's private channel and post at most a neutral acknowledgement to the shared chat.The block carries no per-turn data, so it does not bust the per-conversation prompt cache.
How to test
New
TestSharedAudienceConfidentialityGuard: the block fires for group / channel / thread audiences (including a per-user-isolated group), and is absent for a 1:1 DM and for the local CLI. Assertions are on the invariant ("present iff multi-person audience"), not exact wording.Platforms tested
Linux. The change is platform-agnostic (applies to every gateway platform via the shared session-context builder).