Skip to content

fix(gateway): guard shared-audience chats against owner/backend leaks - #48060

Closed
aldoeliacim wants to merge 1 commit into
NousResearch:mainfrom
aldoeliacim:fix/shared-audience-confidentiality
Closed

fix(gateway): guard shared-audience chats against owner/backend leaks#48060
aldoeliacim wants to merge 1 commit into
NousResearch:mainfrom
aldoeliacim:fix/shared-audience-confidentiality

Conversation

@aldoeliacim

Copy link
Copy Markdown
Contributor

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 the group_sessions_per_user knob — 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

pytest tests/gateway/test_session.py -q   # 87 passed

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

@alt-glitch alt-glitch added type/security Security vulnerability or hardening comp/gateway Gateway runner, session dispatch, delivery P1 High — major feature broken, no workaround labels Jun 17, 2026

@egilewski egilewski left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_type and thread_id decide whether build_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: forum is 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

@teknium1 teknium1 added sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data sweeper:risk-caching Sweeper risk: may break/degrade prompt caching or cache-key stability (invariant) sweeper:blast-broad Sweeper blast radius: broad — a core path most sessions hit labels Jun 21, 2026
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.
@aldoeliacim
aldoeliacim force-pushed the fix/shared-audience-confidentiality branch from 3b27279 to cd8496c Compare June 29, 2026 10:34
@egilewski

Copy link
Copy Markdown
Contributor

suggesting changes

The confidentiality guard still misses Hermes forum audiences when the source is represented as chat_type="forum" without a thread_id. The new predicate in gateway/session.py only treats group, channel, thread, thread_id, or shared_multi_user_session as shared audiences, but existing gateway/auth code treats forum as group-like traffic. On the PR head, a scoped build_session_context_prompt() probe over group, forum, threaded forum, and dm source shapes shows the residual gap remains:

{('group', None): True, ('forum', None): False, ('forum', 'topic-1'): True, ('dm', None): False}

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 forum in the shared-audience predicate and add a regression test for that source shape.

Security evidence:

  • trust boundary: gateway session-context prompt decides whether owner-private/backend-internal detail may be posted back into a shared messaging audience.
  • source/sink/invariant: SessionSource.chat_type and thread_id feed build_session_context_prompt(); any non-DM multi-person audience must get the confidentiality block before the model replies to the origin chat.
  • current-main reproduction: current main emits no Shared audience block for group, forum, or threaded forum shapes in the same probe.
  • PR-head or patch-replay validation: PR head fixes group and threaded forum cases, but leaves ("forum", None) false.
  • positive/negative cases: PR head correctly keeps DM false and group true; the negative case is the unthreaded forum source shape.
  • residual bypass search: existing Hermes code treats forum as group-like traffic in authorization and hook/source contracts, so the predicate should not rely only on group/channel/thread literals.
  • reviewer validation: git merge-tree --write-tree dc5ef20d89f0fc787a97ebd05bb8c41fbce10ab7 cd8496c7f7267ead16ec6b7094275b5b1f24a601 passed; git diff --check dc5ef20d89f0fc787a97ebd05bb8c41fbce10ab7...cd8496c7f7267ead16ec6b7094275b5b1f24a601 passed; tests/gateway/test_session.py -q passed with 100 tests on the PR head. Review stopped at this source-backed blocker.

Signed: GPT-5.5-xhigh in Codex

@teknium1

teknium1 commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

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.

@teknium1 teknium1 closed this Jul 1, 2026
egilewski added a commit to egilewski/hermes-agent that referenced this pull request Aug 8, 2026
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/gateway Gateway runner, session dispatch, delivery P1 High — major feature broken, no workaround sweeper:blast-broad Sweeper blast radius: broad — a core path most sessions hit sweeper:risk-caching Sweeper risk: may break/degrade prompt caching or cache-key stability (invariant) sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data type/security Security vulnerability or hardening

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants