Skip to content

feat(mattermost): add rich posts and passive observation - #68980

Open
nawaf-inter4 wants to merge 3 commits into
NousResearch:mainfrom
nawaf-inter4:feat/mattermost-rich-posts
Open

feat(mattermost): add rich posts and passive observation#68980
nawaf-inter4 wants to merge 3 commits into
NousResearch:mainfrom
nawaf-inter4:feat/mattermost-rich-posts

Conversation

@nawaf-inter4

Copy link
Copy Markdown

Summary

Adds opt-in Mattermost-native rich posts, secure interactive actions, and passive
allowlisted channel observation. This provides parity with Hermes's Slack rich-output
experience and Telegram's context-only group observation without sending Slack Block
Kit payloads to Mattermost.

  • render single-chunk final responses as Mattermost props.attachments
  • preserve the complete Markdown in both the ordinary post message and attachment fallback
  • keep every multi-chunk path plain—including streamed overflow, proactive, and standalone sends—while applying rich structure only to a single final response
  • retry as plain Markdown if Mattermost rejects a structured post
  • add optional Helpful / Not helpful actions
  • add native command-approval actions with typed-command fallback
  • preserve thread roots across sends, edits, standalone delivery, and approvals
  • optionally retain authorized unmentioned channel posts as context without
    dispatching the agent
  • require explicit channel scope and existing gateway user authorization before
    passive content is persisted

Closes #59401.

Related work

PRs #59420 and #60464 address Markdown normalization/formatting. This change keeps ordinary Markdown lossless and additive, while implementing the broader Mattermost-native structured-post, final-edit, approval, feedback, fallback, and callback-security contract requested in #59401.

Configuration

mattermost:
  require_mention: true
  allowed_channels:
    - "your-approved-channel-id"
  observe_unmentioned_channel_messages: true
  rich_posts: true
  feedback_buttons: true
  interaction_url: "https://your-hermes-host/mattermost/actions"
  interaction_host: "127.0.0.1"
  interaction_port: 8789
  interaction_timeout_seconds: 600
  interaction_allowed_cidrs:
    - "10.0.0.12/32"

Interactive actions also require MATTERMOST_INTERACTION_SECRET.

interaction_allowed_cidrs authenticates the direct Mattermost-to-Hermes network peer. Reverse-proxy deployments must restrict or authenticate the proxy route independently; Hermes deliberately does not trust caller-supplied forwarding headers. Non-loopback callback URLs require HTTPS.

Security

  • HMAC-signed action context with constant-time verification
  • exact signed expiry matched to server-side pending state
  • single-use nonce consumption and replay rejection
  • direct-peer CIDR verification before request-body parsing
  • channel/post/action-choice binding
  • existing profile-bound gateway user authorization policy
  • exact opaque approval_id binding so a button resolves only the command it displayed
  • approval session keys remain server-side and never enter Mattermost action payloads
  • bounded callback payloads and pending-state storage
  • feedback state cannot evict live approval state

Behavior and compatibility

  • rich posts and feedback controls are separately opt-in
  • legacy Mattermost output remains unchanged by default
  • ordinary Markdown remains the lossless fallback
  • the top-level Mattermost message remains intentionally populated alongside
    attachments for search, notifications, accessibility clients, and API consumers
  • multi-chunk responses stay plain, matching the current Slack rich-output contract
  • intermediate stream edits stay lightweight
  • callback/listener failure degrades to typed approval commands
  • no public Hermes dashboard or Tailscale Funnel is required; only an explicitly approved Mattermost-to-callback route is needed

Passive channel observation

  • observe_unmentioned_channel_messages defaults to false
  • it only applies while mention-gating is enabled and the channel appears in the
    explicit allowed_channels list
  • eligible unmentioned posts are stored with observed: true; they do not invoke
    the agent or produce a response
  • sender authorization and a fail-closed Mattermost human-account lookup run
    before passive persistence or shared-session dispatch; webhook, bot, system,
    command-shaped, and out-of-scope posts remain ignored
  • a later addressed turn receives observed rows in a separate context-only block,
    rather than replaying them as pending requests
  • context is channel-scoped or Mattermost-thread-root-scoped and retains sender
    attribution
  • passive posts note attachment presence without downloading files

Tests

462 targeted Mattermost/streaming/transformed-output/multiplex/observation/config/approval-contract tests passed
154 send-message routing and standalone-delivery tests passed
2 exact approval-queue tests passed
Ruff checks passed
git diff --check passed

The repository-wide suite was also exercised, but this checkout reports unrelated baseline/environment failures across ACP, WSL/service-manager, Matrix, file-tool, macOS /tmp path normalization, system-guard, and other suites. The focused feature, routing, streaming, and exact-approval suites above pass.

@alt-glitch alt-glitch added type/feature New feature or request P3 Low — cosmetic, nice to have comp/gateway Gateway runner, session dispatch, delivery comp/plugins Plugin system and bundled plugins needs-decision Awaiting maintainer decision before any implementation 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-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades labels Jul 21, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

This was generated by AI during triage.

Related: #59401 (feature request), #59420 and #60464 (open rich-markdown alternatives), and #26901 (ambient ingestion). This PR has a broader structured-post, interaction, and passive-observation contract; maintainer guidance is needed on the rendering and ingestion policy.

@nawaf-inter4
nawaf-inter4 force-pushed the feat/mattermost-rich-posts branch from 75196bc to 34118c1 Compare July 21, 2026 23:33
@nawaf-inter4

Copy link
Copy Markdown
Author

Thanks for the triage and related references. The intended policy boundaries are:

  • Rendering: additive and opt-in. Complete Markdown remains in both the ordinary Mattermost message and attachment fallback; native attachments are used only for a genuinely single final response, while previews and all multi-message paths remain plain.
  • Interactions: separately opt-in, with profile-scoped secrets, existing gateway authorization, signed/expiring/single-use contexts, exact approval targeting, and typed-command fallback.
  • Passive observation: separately opt-in and default-off, requires an explicit channel allowlist and existing user authorization, stores eligible human posts as context only, never triggers an unsolicited response, and does not download observed attachments.

I’m happy to split passive observation and/or interactions into separate PRs, or narrow this PR to the preferred rendering policy, based on maintainer guidance. In particular, guidance on whether native attachments should remain additive to canonical Markdown—and whether ambient Mattermost ingestion should be reviewed separately alongside #26901—would be appreciated.

@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 the substantial Mattermost work. The opt-in/fallback design is thoughtful, but three issues need resolution before this can be safely salvaged.

Problems

  • gateway/stream_consumer.py:419 and :446 directly send transformed appendices/replacements. The normal overflow paths at :950-973 and final fallback at :1531-1549 split to platform limits; these new paths do not. An oversized transformed response can therefore fail delivery.
  • plugins/platforms/mattermost/adapter.py:1611-1613 makes every top-level post its own thread scope in reply_mode="thread". Observed top-level content is saved with that scope at :1665, while a later top-level mention gets a different scope at :1822; it cannot receive the intended channel context.
  • plugins/platforms/mattermost/plugin.yaml:56-59 adds a behavioral .env setting. AGENTS.md:102-107 requires user-facing behavioral settings to live in config.yaml.

Suggested changes

  • Chunk transformed replacements/appendices through the existing safe-limit logic and test an over-limit non-prefix transform.
  • Keep top-level observation channel-scoped; use thread-root scope only for replies, with a reply_mode=thread regression test.
  • Remove the new observation env var and document/configure it only through YAML.

Automated hermes-sweeper review.

Comment thread gateway/stream_consumer.py Outdated
obsolete_ids = set(self._segment_preview_message_ids)
if self._message_id and self._message_id != "__no_edit__":
obsolete_ids.add(str(self._message_id))
result = await self.adapter.send(

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 direct send bypasses the safe-limit splitting used by normal overflow and final-fallback delivery. A non-prefix transform can be larger than the platform limit, so split transformed (and the analogous appendix path) before sending and add an over-limit regression test.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Addressed in 9607554. Transformed appendices and non-prefix replacements now use platform-safe chunking before delivery. Replacement cleanup runs only after every chunk succeeds, so a partial-send failure preserves the existing streamed messages. Added regressions for oversized appendices, oversized non-prefix replacements, send-before-delete ordering, and failure on a later replacement chunk.

Comment thread plugins/platforms/mattermost/adapter.py Outdated

def _mattermost_thread_id(self, post: Dict[str, Any], chat_type: str) -> Optional[str]:
thread_id = post.get("root_id") or None
if not thread_id and self._reply_mode == "thread" and chat_type != "dm":

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 makes each top-level post a distinct session key in thread mode. Since observed top-level posts and later top-level mentions have different post IDs, channel observation cannot be retrieved by the later mention. Keep top-level observation channel-scoped and reserve thread-root scope for replies.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Addressed in 9607554. Mattermost session scope now uses root_id only for actual replies. Top-level posts remain channel-scoped even when reply_mode is thread, while response rendering can still use the top-level post as its delivery thread. Added a regression covering an observed top-level post followed by a later top-level mention.

@@ -47,3 +53,7 @@ optional_env:
description: "If set, the bot only responds in these channels (whitelist)."
prompt: "Allowed channel IDs (comma-separated)"
password: false
- name: MATTERMOST_OBSERVE_UNMENTIONED_CHANNEL_MESSAGES

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 is a non-secret behavioral setting exposed through setup/.env. Per AGENTS.md:102-107, make observe_unmentioned_channel_messages config.yaml-only and remove this optional env variable and its documented equivalent.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Addressed in 9607554. observe_unmentioned_channel_messages is now config.yaml-only: the plugin setup entry, adapter environment lookup, English/Chinese documented equivalent, and environment-variable reference were removed. Added a regression proving the former environment setting is ignored.

@teknium1 teknium1 added sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state sweeper:blast-broad Sweeper blast radius: broad — a core path most sessions hit labels Jul 30, 2026
Add opt-in Mattermost-native structured messages, secure interactive approvals and feedback, context-only allowlisted channel observation, and safe plain streaming fallbacks.
@nawaf-inter4
nawaf-inter4 force-pushed the feat/mattermost-rich-posts branch from 2a323d6 to 9607554 Compare July 30, 2026 10:21
@nawaf-inter4
nawaf-inter4 requested a review from teknium1 July 30, 2026 10:28
@GottZ

GottZ commented Aug 3, 2026

Copy link
Copy Markdown

This was generated by AI during triage.

Summary

Two open PRs reference #59401 but address different causes: #60464 only marks Mattermost payloads as Markdown, while #68980 adds opt-in attachment-based rich posts with Markdown fallback plus substantially broader interaction and passive-observation functionality. The visible diffs confirm that neither adds the requested Mattermost-flavored Markdown transformer for mentions, emoji, math, or diagrams.

Related pull requests

  • feat(mattermost): add rich markdown support for formatted messages (#59401) #60464 related — (+6/-1) — n/a: The diff only adds props.format = markdown to send, edit, attachment, image, and standalone payloads; it adds neither the requested opt-in configuration nor Markdown transformation. Despite the keep_open review on feat(mattermost): add rich markdown support for formatted messages (#59401) #60464, it is salvageable only as a separately scoped payload-formatting change after updating the exact-payload test identified by that review.
  • feat(mattermost): add rich posts and passive observation #68980 best fix — (+3939/-99) — n/a; recorded best available fix: The diff implements opt-in rich_posts using props.attachments, preserves Markdown in message and fallback, and supplies plain fallback, tests, and documentation, but does not implement the requested Mattermost-flavored Markdown conversion and includes much broader interactions and passive observation. Consistent with the keep_open contributor review, the current diff now visibly chunks transformed deliveries, keeps top-level observation channel-scoped, and makes observe_unmentioned_channel_messages config-only, but the rendering and ingestion scope still needs maintainer guidance.

Duplicates

#60464 and #68980 are not duplicates: #60464 adds an unconditional payload-format property, whereas #68980 implements opt-in attachment rendering and a much broader interaction/observation contract.

Suggested consolidation

Keep #68980 open with a salvage path focused on its opt-in rich-post renderer, Markdown-preserving fallback, and associated delivery tests; split the interaction and passive-observation work if maintainers do not approve that broader scope. Keep #60464 separate only if its payload-format property is independently wanted, with the failing exact-payload test updated and the PR respecified away from closing #59401; otherwise close it as not addressing this issue rather than as a duplicate.

Complex graph

flowchart LR
    classDef open fill:#dbeafe,stroke:#1d4ed8,color:#1e3a8a
    classDef merged fill:#dcfce7,stroke:#15803d,color:#14532d
    classDef closed fill:#e5e7eb,stroke:#6b7280,color:#1f2937
    classDef unverified fill:#f3f4f6,stroke:#9ca3af,color:#374151
    classDef best stroke-width:3px,stroke:#b45309
    classDef target stroke-width:3px,stroke:#4338ca
    I59401(["issue #59401 (open)"])
    P68980["PR #68980 (open)"]
    P68980 -->|best fix| I59401
    class I59401 open
    class P68980 open
    class P68980 best
    class P68980 target
    click I59401 "https://github.com/NousResearch/hermes-agent/issues/59401"
    click P68980 "https://github.com/NousResearch/hermes-agent/pull/68980"
Loading

Graph: solid arrow = fixes / best fix, dashed arrow = partial or unverified (see edge label); boxed group = PRs duplicating each other; amber border = best fix; indigo border = target; gray node = closed (state tag in the node label).

Cross-PR triage: Reviewed 2 pull requests and 1 issue in this complex. Each diff was read against this issue; Assessment working set: 198 kB of PR diffs, 10 kB of issue/PR text, 5 kB of discussion (10 comments), 2 verify verdicts. verdicts reflect diff content, not PR titles. Part of an automated triage batch.

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 comp/plugins Plugin system and bundled plugins needs-decision Awaiting maintainer decision before any implementation P3 Low — cosmetic, nice to have sweeper:blast-broad Sweeper blast radius: broad — a core path most sessions hit 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 sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: Mattermost Rich Markdown Support

4 participants