Skip to content

feat(mattermost): thread context seeding, allowlist-filtered - #43805

Open
crisap94 wants to merge 2 commits into
NousResearch:mainfrom
crisap94:feat/mattermost-thread-context
Open

feat(mattermost): thread context seeding, allowlist-filtered#43805
crisap94 wants to merge 2 commits into
NousResearch:mainfrom
crisap94:feat/mattermost-thread-context

Conversation

@crisap94

Copy link
Copy Markdown

What does this PR do?

Seeds prior thread history as context when the bot is @mentioned inside an existing thread for the first time, so the agent understands the conversation it was pulled into — not just the single message it was tagged in. History is injected via MessageEvent.channel_context (consumed in gateway/run.py).

🔗 Stacked on #43804 (in-thread auto-response). This branch contains that PR's commit plus one more — please review the thread context seeding commit here, and merge after #43804. (Cross-fork PRs can't target a fork branch as base, hence the cumulative diff.)

Security: the allowlist must apply to seeded context

The user allowlist is enforced in gateway/authz_mixin.py::_is_user_authorized, per message, by the triggering author — downstream of the adapter. Injected channel_context rides inside the authorized user's message and is not re-checked by authz. So seeding the whole thread verbatim would feed a non-allowlisted user's messages into the model (an unauthorized-content / prompt-injection vector).

This PR filters seeded history to authors in MATTERMOST_ALLOWED_USERS (or everyone when *_ALLOW_ALL_USERS is set), and adds MATTERMOST_THREAD_CONTEXT to control the policy explicitly:

Value Behavior
allowlisted (default) Seed only messages from allowlisted authors
off Never seed thread history (no fetch)
all Seed the full thread regardless of author, without widening who may invoke the bot

This filtering is the piece the existing thread-context PRs (#38362, #38152) do not have.

Type of Change

  • ✨ New feature (thread-context seeding)
  • 🔒 Security fix (allowlist-filtered context)
  • ✅ Tests
  • 📝 Documentation update

Changes Made

All in plugins/platforms/mattermost/adapter.py (the thread-context commit):

  • _fetch_thread_context() — fetches GET /posts/{root}/thread, fails open via the existing _api_get, TTL-cached, excludes the trigger/bot/system posts, filters to allowlisted authors, injects via channel_context.
  • _thread_context_author_allowed() — mirrors the authz allowlist; honors MATTERMOST_THREAD_CONTEXT=all.
  • _thread_context_mode()off / allowlisted / all.
  • First-turn injection guarded by _has_active_session_for_thread (from feat(mattermost): in-thread auto-response (Slack parity) #43804) so it runs once per thread.
  • Tests: TestMattermostThreadContext (filter, allow-all, knob off/all, fail-open, DM).
  • Docs: MATTERMOST_THREAD_CONTEXT + the allowlist-context warning.

How to Test

  1. MATTERMOST_ALLOWED_USERS=<you>. @mention the bot in a thread with prior messages → reply reflects them.
  2. A non-allowlisted user posts in the thread → ignored, and their text does not appear in the bot's context.
  3. MATTERMOST_THREAD_CONTEXT=off → no seeding; =all → full thread seeded without opening invocation.
  4. pytest tests/gateway/test_mattermost.py -q → 63 passed.

Checklist

crisap94 added 2 commits June 10, 2026 17:58
After the bot is @mentioned in a thread, subsequent messages in that thread
auto-trigger it without a new @mention (Slack parity). Opt out with
MATTERMOST_STRICT_MENTION=true.

- _mentioned_threads tracks engaged threads; _has_active_session_for_thread
  passes the adapter's real chat_type so the session key matches what
  handle_message persists.
- Includes the root-post session-continuity fix (same as NousResearch#37144 by
  @brendanstennett): a Mattermost root post has an empty root_id, so in thread
  mode thread_id is seeded from the post's own id to keep the root and its
  replies on one session. Auto-response depends on this, so it is included here
  with credit; happy to drop it if NousResearch#37144 lands first.
- Auto-response does not bypass authz: every message is still authorized by
  author downstream.

Adds TestMattermostThreadSessionContinuity + TestMattermostInThreadAutoResponse
and documents MATTERMOST_STRICT_MENTION.

Related: NousResearch#37144 (session fix, credited)
When @mentioned inside an existing thread for the first time, seed the prior
thread history as context (GET /posts/{root}/thread) via MessageEvent.channel_context,
so the agent sees the whole conversation, not just the triggering message.

Security: the authz layer only checks the *triggering* message's author;
injected channel_context bypasses it. So seeded history is filtered to authors
in MATTERMOST_ALLOWED_USERS (or everyone when *_ALLOW_ALL_USERS is set) to avoid
leaking a non-allowlisted user's messages into the model. MATTERMOST_THREAD_CONTEXT
controls the policy: allowlisted (default), off, or all (full thread without
widening who may invoke the bot). Fails open on fetch error.

Stacked on the in-thread auto-response change (uses _has_active_session_for_thread
as the first-turn guard).

Adds TestMattermostThreadContext and documents MATTERMOST_THREAD_CONTEXT.

Supersedes NousResearch#38362, NousResearch#38152 (thread context without the allowlist filter).
@crisap94
crisap94 force-pushed the feat/mattermost-thread-context branch from c603a2d to e8b02c9 Compare June 10, 2026 22:59

@teknium1 teknium1 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.

Thanks for addressing a real Mattermost context gap: current main creates the event without thread history at plugins/platforms/mattermost/adapter.py:949-958, while gateway/run.py:10394-10395 already consumes channel_context.

Problems

  • plugins/platforms/mattermost/adapter.py:723-746 reimplements only part of authorization. It misses the global allowlist and pairing-store grants used by gateway/authz_mixin.py:454-465. Current main’s adapter callback (gateway/platforms/base.py:2838-2869, gateway/run.py:8785-8815) exists specifically to reuse that full chain for fetched context.
  • The cache at proposed adapter.py:795-797 is keyed only by thread root although the rendered result excludes current_post_id at lines 811-813. A second user’s first turn within the TTL can therefore receive stale context that omits the first user’s now-prior post. The added tests (tests/gateway/test_mattermost.py:946-1010) do not cover that sequence.
  • The two new controls are documented but absent from the Mattermost setup manifest (plugins/platforms/mattermost/plugin.yaml:15-49).

Suggested changes

  • Use _is_sender_authorized(...) and test global/pairing authorization.
  • Make cache results trigger-specific or regenerate them from cached raw posts; test two first-session users.
  • Register both controls in the Mattermost plugin manifest.

Automated hermes-sweeper review.

return True
allowed = {
u.strip()
for u in os.getenv("MATTERMOST_ALLOWED_USERS", "").split(",")

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.

This duplicates only a subset of gateway authorization: it misses GATEWAY_ALLOWED_USERS and pairing-store grants. Current main provides _is_sender_authorized(author, chat_type=chat_type, chat_id=channel_id) specifically so fetched context uses the complete GatewayRunner._is_user_authorized chain; please use that callback instead of re-parsing env vars.

leaving the agent with just the triggering message.
"""
now = time.monotonic()
cached = self._thread_context_cache.get(thread_root_id)

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.

The cached string depends on current_post_id because the loop excludes that post, but this key is only thread_root_id. A later first turn by another user during the TTL receives the earlier rendered string and loses the first trigger as prior context. Cache raw posts or make this result trigger-specific, with a two-user regression test.

with patch.dict(os.environ, {"MATTERMOST_THREAD_CONTEXT": "bogus"}):
assert self.adapter._thread_context_mode() == "allowlisted"

@pytest.mark.asyncio

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.

Please add regression coverage for two authorized users entering the same thread within the cache TTL: the second user’s seeded context must include the first user’s earlier triggering post.

@teknium1 teknium1 added sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform labels Jul 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/plugins Plugin system and bundled plugins P3 Low — cosmetic, nice to have sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades 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/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants