Skip to content

feat(mattermost): add interactive approval buttons via send_exec_approval - #29373

Open
clean-logic wants to merge 3 commits into
NousResearch:mainfrom
clean-logic:feat/mattermost-approval-buttons
Open

feat(mattermost): add interactive approval buttons via send_exec_approval#29373
clean-logic wants to merge 3 commits into
NousResearch:mainfrom
clean-logic:feat/mattermost-approval-buttons

Conversation

@clean-logic

@clean-logic clean-logic commented May 20, 2026

Copy link
Copy Markdown

Summary

Adds full interactive-button support to the Mattermost gateway plugin — exec
approval, slash confirm, update prompt, and clarify — bringing it to parity with
Discord and beyond.

Re-architected onto the bundled-plugin model introduced in af973e407 (Mattermost
moved from gateway/platforms/mattermost.py to plugins/platforms/mattermost/).
Interactive callbacks are served by a plugin-owned aiohttp server (the Teams/Line
pattern) — zero core edits.

What it does

  • send_exec_approval — Allow Once / Allow Session / Always Allow / Deny → resolve_gateway_approval
  • send_slash_confirm — Approve Once / Always / Cancel → tools.slash_confirm.resolve
  • send_update_prompt — Yes / No update decisions
  • send_clarify — per-option buttons → resolve_gateway_clarify
  • Local HTTP callback server (three-tier callback_host/callback_port, default
    127.0.0.1:18065), MATTERMOST_ALLOWED_USERS auth, double-click guard,
    original-message updates on click.

Why buttons (not text)

Mattermost intercepts messages starting with /, so the plain-text /approve
fallback in #27587 is impossible to action. Buttons are the only working approval
path.

Relation to #26537

Supersedes @shawnfeng0's #26537 (same goal). This PR is plugin-native (his targets
the now-deleted core adapter), adds send_clarify, full registration hooks, and
tests + docs. Credit to his callback-server design and review exchange.

Fixes #27587 (primary item; the slash-command-registration secondary item is out of
scope).

@alt-glitch

Copy link
Copy Markdown
Collaborator

Competing with #26537 (by @shawnfeng0), which is broader in scope (covers send_exec_approval, send_slash_confirm, and send_update_prompt). Implements #27587. Prior closed attempt: #8184. Maintainers should compare the two architectural approaches (reuse WebhookAdapter routes vs standalone aiohttp listener).

@llealloo

llealloo commented Jul 1, 2026

Copy link
Copy Markdown

hermes mattermost integration is dead without this!

please approve this PR with a cherry on top

@clean-logic
clean-logic force-pushed the feat/mattermost-approval-buttons branch from 1edb3fe to 9350833 Compare July 2, 2026 17:12

@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 carrying the Mattermost implementation into the bundled-plugin layout. The interactive gap is real on current main, but this needs correction before salvage.

Problems

  • plugins/platforms/mattermost/adapter.py:1059 can return SendResult(success=False) when the callback listener is unavailable. Current gateway/run.py:14655-14664 ignores that result, sets sent_buttons = True, and suppresses the text fallback; the prompt is then marked pending at gateway/run.py:14678-14683.
  • adapter.py:930 resolves by session only. tools/approval.py:1488-1521 resolves the oldest pending entry for that session, so a stale button can resolve a newer command approval.
  • gateway/platforms/webhook.py:152-200 adds an extra-route API with no PR caller. The adapter instead creates its own app at adapter.py:329-338; please remove the unused core hook.

Suggested changes

  • Check SendResult.success in the update watcher and add an end-to-end regression for listener startup failure.
  • Bind approval clicks to an exact approval ID, with a stale-button/newer-approval regression test.
  • Keep the plugin-owned listener and drop the unused webhook change.

Automated hermes-sweeper review.

Comment thread plugins/platforms/mattermost/adapter.py Outdated
if choice not in {"once", "session", "always", "deny"}:
raise ValueError(f"invalid approval choice: {choice!r}")
from tools.approval import resolve_gateway_approval
resolve_gateway_approval(payload["session_key"], choice, resolve_all=False)

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 resolves only by session_key, but tools.approval.resolve_gateway_approval() resolves the session FIFO. A click on an old button can therefore resolve a newer command approval in the same session. Please plumb and resolve an exact approval ID before enabling this command-approval path.

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.

@.git_pr_reply_approval_id.md

Comment thread gateway/platforms/webhook.py Outdated
# Extra-route facility (used by sibling adapters, e.g. Mattermost)
# ------------------------------------------------------------------

def register_extra_route(self, method: str, path: str, handler) -> bool:

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 PR-head Mattermost adapter never calls register_extra_route; it starts a separate aiohttp app instead. Please remove this unused core hook rather than adding speculative infrastructure and middleware to every webhook request.

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.

@.git_pr_reply_webhook.md

@teknium1 teknium1 added sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state 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 sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform labels Jul 13, 2026
Anton Rukhlin and others added 3 commits July 23, 2026 13:59
resolve_gateway_approval() resolves the oldest pending approval for a
session (FIFO). With more than one approval pending in the same
session, a stale button click can resolve the wrong one. Add
_ApprovalEntry.approval_id (generated per entry, surfaced to notify
callbacks via approval_data["approval_id"]) and
resolve_gateway_approval_by_id(), which resolves only the exact
matching entry and never falls back to FIFO on a stale/unknown id.

Plumb approval_id through gateway/run.py's _approval_notify_sync into
send_exec_approval, and accept (currently unused) approval_id on every
adapter implementing that method -- Telegram, Teams, Slack, Matrix,
Feishu, Discord, WhatsApp Cloud, QQ. Text /approve and /deny keep
resolving FIFO unchanged; adopting by-id resolution per adapter is a
separate, additive step (Mattermost adopts it next).

Flagged in PR NousResearch#29373 review (teknium1): plugins/platforms/mattermost/
adapter.py:930 resolved by session only, which could let a stale
button resolve a newer command approval.

Co-authored-by: Cursor <cursoragent@cursor.com>
The update watcher set sent_buttons = True unconditionally whenever
adapter.send_update_prompt() didn't raise, even when it returned a
non-raising SendResult(success=False) (e.g. the adapter's interactive-
button listener isn't running). That suppressed the plain-text
fallback and left the prompt marked pending with no way for the user
to answer it.

Check the returned SendResult.success, matching the pattern already
used for send_slash_confirm. Only the update-prompt path was missing
this check -- the exec-approval notify path already handles it.

Flagged in PR NousResearch#29373 review (teknium1): plugins/platforms/mattermost/
adapter.py:1059 can return SendResult(success=False) when its callback
listener is unavailable, and gateway/run.py ignored that result.

Co-authored-by: Cursor <cursoragent@cursor.com>
…oval

Adds full interactive-button support to the Mattermost gateway plugin
-- exec approval, slash confirm, update prompt, and clarify -- via a
plugin-owned aiohttp callback server (the Teams/Line pattern). Zero
core edits. Mattermost intercepts messages starting with "/", so the
plain-text /approve fallback is unusable; buttons are the only working
approval path (NousResearch#27587).

- send_exec_approval accepts allow_permanent/allow_session/smart_denied
  (matching every other adapter's current contract) and hides Allow
  Session / Always Allow accordingly, mirroring Discord/Slack.
- Exact approval-ID resolution: send_exec_approval accepts approval_id
  and stores it on the pending entry; _resolve_approval resolves via
  resolve_gateway_approval_by_id so a stale button can never resolve a
  different, newer pending approval in the same session. Falls back to
  legacy session-FIFO resolve for prompts registered before this
  adapter started stamping approval_id (e.g. across a gateway
  restart). Shows an honest "Approval expired" message on a stale tap.
- Clarify renders Telegram-style: every option's full text is listed
  in the post body (numbered), with short numeric buttons (1, 2, ...)
  plus "Other (type answer)" -- Mattermost attachment button labels
  were getting cut off for long options.
- Mass-mention hardening (matching the equivalent upstream commit)
  extended to the new prompt-posting paths: approval/slash/update/
  clarify prompts and the slash follow-up post all disable_mentions,
  since the posted text can echo back agent- or user-influenced
  content that may contain "@channel"/"@ALL".
- Reconciled against the 3 adapter.py commits upstream landed since
  this branch was cut: leading-space slash commands, per-platform
  multiplex config validation (validate_mattermost_config), and the
  mass-mention hardening above.
- Dropped: the unused gateway/platforms/webhook.py register_extra_route
  hook (the adapter uses its own aiohttp app and never called it) and
  a latent bug in this branch's env-enablement seeding that silently
  discarded MATTERMOST_HOME_CHANNEL_NAME by omitting "name" from its
  home_channel seed dict -- neither is needed for this feature.

Local HTTP callback server (three-tier callback_host/callback_port,
default 127.0.0.1:18065), MATTERMOST_ALLOWED_USERS auth, double-click
guard, original-message updates on click, bounded _pending_actions
(oldest-evicted at 500 entries).

Fixes NousResearch#27587. Supersedes NousResearch#26537 (plugin-native vs. the now-deleted
core adapter it targeted).

Addresses PR NousResearch#29373 review (teknium1):
- adapter.py:930 resolved by session only -> exact approval_id above.
- webhook.py register_extra_route had no caller -> removed.
(SendResult.success handling for the exec-approval path was already
correct upstream; the update-prompt watcher gap is fixed separately.)

Co-authored-by: Cursor <cursoragent@cursor.com>
@clean-logic
clean-logic force-pushed the feat/mattermost-approval-buttons branch from 9350833 to cad414c Compare July 23, 2026 18:36
@clean-logic

Copy link
Copy Markdown
Author

Thanks for the review — rebased onto current main and addressed all three points.

Rebase

Upstream advanced ~2,900 commits since this branch was cut, but only 3 touched plugins/platforms/mattermost/adapter.py (leading-space slash commands, per-platform multiplex config validation, mass-mention hardening). Rebuilt as 3 commits instead of a mechanical rebase of the old history:

  1. fix(approval): add exact approval-ID resolution for gateway buttons — core plumbing
  2. fix(gateway): honor SendResult.success in the update-prompt watcher — core fix
  3. feat(mattermost): add interactive approval buttons via send_exec_approval — the plugin itself

Review point 1 — SendResult(success=False) on listener failure

adapter.py:1059 can return SendResult(success=False) ... gateway/run.py:14655-14664 ignores that result ... the prompt is then marked pending

Confirmed and split into two states:

  • The exec-approval notify path (gateway/run.py, _approval_notify_sync) already checks SendResult.success and falls back to text on main — that part had already been fixed upstream since this PR was opened.
  • The update-prompt watcher had the identical bug and was still unfixed: it set sent_buttons = True unconditionally whenever send_update_prompt() didn't raise, even on a non-raising success=False. Fixed in commit 2, with a regression test (tests/gateway/test_update_streaming.py::test_falls_back_to_text_when_button_prompt_send_fails) that fails on the old code and passes with the fix.

Review point 2 — exact approval-ID resolution

adapter.py:930 resolves by session only ... a stale button could resolve a newer command approval

Added core support in commit 1 (tools/approval.py):

  • _ApprovalEntry.approval_id — generated per pending entry, surfaced to notify callbacks via approval_data["approval_id"] (no callback signature change).
  • resolve_gateway_approval_by_id(session_key, approval_id, choice, reason=None) — resolves only the exact matching entry; a stale/unknown id resolves nothing (returns 0) and never falls back to FIFO.
  • Plumbed through gateway/run.py's _approval_notify_sync into send_exec_approval(..., approval_id=...), and accepted (currently unused) on every other adapter implementing that method — Telegram, Teams, Slack, Matrix, Feishu, Discord, WhatsApp Cloud, QQ — so the signature is uniform. Only Mattermost adopts it in this PR; converting the others is the separate cross-platform PR you mentioned.
  • Mattermost's _resolve_approval now resolves by approval_id when present, with a legacy session-FIFO fallback for prompts registered before this adapter started stamping ids (e.g. across a gateway restart). A stale tap shows "⌛ Approval expired — command was not run" instead of silently resolving the wrong entry.
  • Regression test: tests/tools/test_approval.py::TestApprovalIdResolution::test_stale_button_does_not_resolve_newer_approval — two approvals pending in the same session, resolving by the older id leaves the newer one untouched.

Review point 3 — unused webhook.py hook

The PR-head Mattermost adapter never calls register_extra_route

Removed. The adapter uses its own plugin-owned aiohttp app; gateway/platforms/webhook.py is untouched in this version.

Also picked up while rebasing

  • Upstream's send_exec_approval contract grew allow_permanent / allow_session / smart_denied kwargs since this branch was cut. Mattermost now accepts them and hides Allow Session / Always Allow the same way Discord and Slack do.
  • Clarify now renders Telegram-style: full option text goes in the post body (numbered), buttons shrink to short numeric labels (1, 2, ...) plus "Other (type answer)". Mattermost attachment button labels were getting cut off for long options.
  • Applied the same mass-mention hardening upstream added for send/edit_message/file uploads to the new prompt-posting paths (approval/slash/update/clarify), since prompt text can echo back content that may contain @channel/@all.
  • Found and removed a latent bug in this branch's own _env_enablement hook (unrelated to buttons): it seeded home_channel without a name key, silently discarding MATTERMOST_HOME_CHANNEL_NAME and breaking an existing upstream test (test_mattermost_home_channel). Not needed for this feature — removed rather than fixed in place.

Tests: tests/gateway/test_mattermost.py (63) + tests/gateway/test_mattermost_approval_buttons.py (49) + the two core test files above all pass locally; full suite via CI.

@alt-glitch alt-glitch added comp/plugins Plugin system and bundled plugins needs-decision Awaiting maintainer decision before any implementation and removed 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 25, 2026
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-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages 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: Add interactive button-based approval for dangerous commands

4 participants