Commit graph

17350 commits

Author SHA1 Message Date
Gonzalo Franco Ceballos
cc64f0289a fix(slack): add bold-text zero-width-space guard for trailing non-word chars
Slack's mrkdwn parser can fail to recognize the closing * of a bold span
when it is immediately preceded by a non-word character (), ], }, ., :,
em-dash, ...), mis-rendering the span and in reported cases truncating
the rest of the message.  Insert a zero-width space (U+200B) between the
last character and the closing * whenever the last character is not
alphanumeric or underscore.

Reapplied from #35144 by @gonzalofrancoceballos — the original patched
gateway/platforms/slack.py, which was migrated to
plugins/platforms/slack/adapter.py in the plugin migration.
2026-07-23 11:50:28 -07:00
z23
b810711e4e fix(gateway): strip language tag from Slack fenced code blocks
Slack's mrkdwn does not strip the optional language tag from fenced
code blocks like GitHub-flavored markdown does — it renders
```text\nfoo\n``` as a code block whose literal first line is "text".
The agent emitted ```text fences around raw command output, which
surfaced "text" as the first line of every such block.

Drop the tag from the opening fence in format_message() before stashing
the block behind a placeholder. Stripping only fires for a genuine
opening fence — a ``` at the start of a line, tagged with a single
token (no spaces or backticks) — and the original line ending is
preserved. The fence-protection regex deliberately matches loosely, so
a mid-line ``` (e.g. an inline ```span``` wrapping across a newline)
can be grouped as an "opening fence" whose first line is real content;
differential fuzzing against the pre-change formatter (40k generated
messages) confirms the only behavioral delta is the tag strip itself.

The Block Kit renderer is unaffected: render_blocks() intercepts fences
itself before mrkdwn_fn is applied, so this only changes the mrkdwn
surfaces that still go through format_message() — the plain-text
fallback field (notifications, search indexing, accessibility),
slash-command ephemeral replies, and standalone cron delivery.

Originally written against gateway/platforms/slack.py; ported to
plugins/platforms/slack/adapter.py after the adapter migration in
5600105478.

Manually verified against a live Slack workspace (pre-migration
adapter; the ```text case strips identically) — code blocks no longer
carry a literal "text" first line.
2026-07-23 11:50:28 -07:00
briandevans
8e6d1a9a53 fix(slack): stop double-decoding HTML entities when escaping message text
format_message unescapes already-escaped input before re-escaping, so that
pre-escaped text doesn't get double-escaped. That unescape was three
sequential str.replace calls, which re-scan each other's output:

    "&amp;lt;"  --(&amp; -> &)-->  "&lt;"  --(&lt; -> <)-->  "<"

The & produced by the first replace pairs with the following "lt;" and
decodes a second time. "&amp;lt;" is the wire form of the literal text
"&lt;", so the text is silently destroyed: Slack receives "&lt;" and renders
"<". Anyone writing about HTML or markup ("&amp;lt;b&amp;gt;" -> "<b>")
loses their literal text, with no error.

re.sub scans left-to-right and never re-scans its own replacements, so a
single pass fixes it. The escape pass on the next line is left untouched --
it is correctly ordered (& first, so the &s it inserts aren't re-escaped).

Only the double-decode cases change; every other input is byte-identical
before and after. This is the same round-trip invariant the neighbouring
test_pre_escaped_{ampersand,lt,gt}_not_double_escaped tests already assert,
extended to the case they miss. Affects the plain mrkdwn path (send,
edit_message) and Block Kit sections, which route section text through
format_message.
2026-07-23 11:50:28 -07:00
Teknium
50a6dc7efc fix(gateway): duck-type set_reaction_handler on adapter wiring sites
Test doubles and third-party adapter objects don't all implement the new
set_reaction_handler; calling it unconditionally hung the multiplex
secondary-reconnect test (the AttributeError was swallowed into an
awaited-forever path). getattr-guard all three wiring sites — same
duck-typing convention the sibling setters rely on for MagicMock, but
explicit, so bare objects work too.
2026-07-23 11:50:08 -07:00
Teknium
1f25791b1f chore(release): map C17 reaction-cluster contributor emails
- john.kattenhorn.personal@gmail.com -> johnkattenhorn (#33111)
- harrison@medmetricsrx.com -> harrisonmedmedmetrics (#44508)
- kevin@fleetsmarts.net -> Kev-fs (#45265)
2026-07-23 11:50:08 -07:00
Teknium
558dab0e3e feat(slack): opt-in reaction triggers, removed events, hooks, channel handoff
Build the full reaction pipeline on top of the #29916 base:

- Opt-in gate: slack.reaction_triggers (default OFF — reaction events
  stay acked-and-dropped so busy channels don't wake the agent on every
  emoji). 'true' routes reactions on the bot's OWN messages; an explicit
  emoji-name list routes those emojis from any message (handoff flows).
- reaction_removed events now route too, distinguished by the
  cross-platform text convention reaction:added:<emoji> /
  reaction:removed:<emoji> (matches the Feishu and Photon adapters, so
  agents and skills see one shape everywhere).
- Authorization: the reactor becomes the synthesized message's user, so
  the early _is_user_authorized gate and allowed_channels whitelist
  apply exactly as for typed messages. _hermes_force_process only skips
  the mention requirement (a reaction on the bot's own message is
  definitionally addressed to the bot), mirroring Feishu/Photon.
- Gateway hooks (#33111 by @johnkattenhorn): every human reaction on a
  message item fires reaction:added / reaction:removed through the new
  BasePlatformAdapter.set_reaction_handler → GatewayRunner
  ._handle_reaction_event → HookRegistry.emit, independent of the
  routing opt-in. Documented in hooks.md.
- Channel handoff (#45265 by @Kev-fs): slack.reaction_trigger_target
  routes the reaction turn to a configured channel (top-level via
  _hermes_no_thread_response + reply-anchor suppression in
  gateway/platforms/base.py) or C123:<ts> thread.
- Manifest: reaction_removed event subscription added alongside
  reaction_added/reactions:read.
- Docs: slack.md Reaction Triggers section; hooks.md event table rows.

Also credits #44508 by @harrisonmedmedmetrics (inbound reaction_added
handling — same plumbing class, superseded by this consolidated shape).

Co-authored-by: johnkattenhorn <john.kattenhorn.personal@gmail.com>
Co-authored-by: Kev-fs <kevin@fleetsmarts.net>
Co-authored-by: harrisonmedmedmetrics <harrison@medmetricsrx.com>
2026-07-23 11:50:08 -07:00
Benjamin Ross
9c9b057b73 fix(slack): forward reaction_added events to the message pipeline
Slack reaction_added events were explicitly acked and dropped, so a user
reacting to a bot message (👍 to approve,  to acknowledge) produced
nothing. Forward them through the normal message pipeline as synthesized
MessageEvents whose text is the reaction emoji (translated to unicode
for common names), keeping the downstream auth gate, thread-context
fetch, dedup, and skill routing unchanged.

- Self-reactions and non-message items are dropped; reactions on
  messages not sent by this bot are dropped (Feishu-adapter parity).
- The reacted-to message's thread parent becomes the synthesized
  thread_ts so the reaction lands in the same session as a reply would.
- Manifest gains reactions:read scope + reaction_added bot event.

Salvaged from PR #29916 by @bpross.
Related: #33111, #44508, #45265 (same cluster).
2026-07-23 11:50:08 -07:00
Teknium
9ddcb58a21 chore(contributors): add email mapping for metamon-p (PR #36220 salvage) 2026-07-23 11:49:44 -07:00
metamon
91799405aa fix(gateway): hint Slack/Discord channels at the prior auto-reset session
Salvaged from PR #36220, ported onto the current SessionStore (SQLite-
backed get_or_create_session; activity check is last_prompt_tokens) and
the sidecar-note reset path (context notes now ride turn_sidecar_notes
instead of prepending to context_prompt).

Long-lived Slack/Discord channels/threads lose their context on
daily/idle session resets, and the agent can bind a new request to an
unrelated recent session (observed: a Discord thread reset caused a PR
in the wrong repository). Record prev_session_id when an auto-reset
replaces a session with real activity, persist it, and append a
deterministic one-line hint to the auto-reset context note pointing the
agent at session_search for that specific prior session. No LLM calls,
no channel-history APIs, no extra DB lookups; other platforms and
activity-free resets are untouched.

Refs #36220. Co-authored-by: metamon <269728612+metamon-p@users.noreply.github.com>
2026-07-23 11:49:44 -07:00
MrAbsaroka
61ea271900 feat(slack): elapsed typing heartbeat — 'still working… (2m03s)' on long turns
Salvaged from PR #45702 (heartbeat half). A multi-minute turn showed a
static 'is thinking...' assistant status that reads as stuck and provokes
mid-turn 'you there?' pings. Derive the fallback status label from the
turn's elapsed time (>=30s → 'still working… (NmSSs)'), riding the
existing _keep_typing refresh — zero extra API calls.

Ported onto the current plugin adapter: the start time rides the tracked
_active_status_threads entry (workspace-scoped key), so it shares the
existing bounds/eviction and resets when stop_typing clears the status.
Explicit live-status phrases (set_status_text) and configured
typing_status_text always win; only the built-in default label changes.

The PR's other half (top-level channel follow-up coalescing) is NOT
included — dispatch semantics changed on main (busy-input active-turn
redirect, #30170 demotion) and need a fresh design pass.

Refs #45702. Co-authored-by: MrAbsaroka <mrabsaroka@gmail.com>
2026-07-23 11:49:44 -07:00
Teknium
8d1b1372e9 test(gateway): pin automatic /goal continuation drain — no user nudge required
Issue #47699 reported Slack /goal continuations being enqueued by
_post_turn_goal_continuation -> _enqueue_fifo but never drained until the
next real inbound message woke the session. On the current tree the
continuation lands in the adapter pending slot while the
_process_message_background frame is still live, so the in-band pending
drain (and the finally-block late-arrival drain) spawns the follow-up
turn automatically — the reported stall is not reproducible on main.

Pin the two halves of that contract so it can't silently regress:
  1. a continuation placed in the FIFO during the handler frame is
     consumed as a second turn without any new user message, and
  2. the runner's goal hook enqueues under the same session key the
     adapter drain resolves (key mismatch would orphan the event).

Refs #47699. Reported-by: joesu-angible
2026-07-23 11:49:44 -07:00
Teknium
6bb0eac398 fix(gateway): don't re-deliver consumed background completions as raw watcher messages
process(wait) marks a completion consumed and returns the exit code +
output inline. The gateway process watcher's agent-notify branch honored
that (skipping the synthetic agent turn), but its skip FELL THROUGH to
the plain text-notification branch, which re-sent the same completion to
the chat as a raw '[Background process ... finished with exit code ...]'
message — a duplicate delivery of output the agent had already read and
was summarizing (observed on Slack with
display.background_process_notifications: all, but platform-agnostic).

Guard the raw-notification branch on is_completion_consumed(), same as
the agent-notify branch. poll() stays read-only and never marks consumed
(#10156), so status checks still can't suppress autonomous delivery.

Fixes #65379. Reported-by: hergert
2026-07-23 11:49:44 -07:00
ethernet
a4bc1ca502
fix(timeline): persist typed display events (#69771)
* fix(desktop): hide persisted agent-only history scaffolding

Filter verification-stop nudges and context-compaction handoffs at the
stored-history mapper boundary. Preserve a real reply when a compaction
handoff shares its stored message.

* test(desktop): build persisted E2E sessions through the real agent

Drive tui_gateway.entry over its stdio JSON-RPC transport against the mock
provider, wait for real completion events, and persist normal session history
through AIAgent and SessionDB. Migrate resume and hidden-history coverage,
including real compression and live verify-on-stop scaffolding, then remove
the unused direct SessionDB import scripts.

* fix(desktop): use the provisioned Python for real-session E2Es

Run the stdio gateway through uv's synced project environment outside the
Nix dev shell, while retaining the fully provisioned Nix Python when the
shell advertises HERMES_PYTHON_SRC_ROOT.

* fix(nix): expose the provisioned Python environment to uv

Mark the Nix-built Python environment active in the dev shell so the shared
E2E session builder can always run through `uv run --active --no-sync`.

* fix(timeline): persist typed display events

* fix(timeline): strip display-only fields from provider payloads, preserve through rewrites, fix /resume display history

Three review findings from PR #69771:

1. Provider payload leak: display_kind and display_metadata were forwarded
   to the provider API as unknown message fields. Strict OpenAI-compatible
   backends can reject the next request after a model switch or resumed
   typed event. Strip both from the per-request api_msg copy in
   conversation_loop alongside the existing api_content pop.

2. Rewrite/import data loss: _insert_message_rows preserved display_kind
   but silently dropped display_metadata. After replace_messages,
   archive_and_compact, or session import, async-delegation completion
   events lost their task counts and fell back to generic display text.
   Add display_metadata to the INSERT columns and bind tuple.

3. CLI /resume stale recap: startup --resume A set _resume_display_history
   from A's lineage. A subsequent in-session /resume B loaded B only into
   conversation_history via get_messages_as_conversation, leaving the stale
   A display projection. _display_resumed_history preferentially read the
   stale attribute, showing A's recap for B. Switch /resume to
   get_resume_conversations and update _resume_display_history alongside
   conversation_history.

Tests: 890 Python (5 files), 35 desktop TS — all green.

* feat(tui): render typed display events as ◈ markers in the Ink TUI

The TUI was not handling display_kind at all — model switch markers and
async delegation completions rendered as opaque user messages with the
full [System: ...] text, and hidden compaction handoffs were visible.

Wire display_kind through the full TUI chain:

- _history_to_messages (tui_gateway/server.py) forwards display_kind
  and display_metadata to the gateway transcript payload.
- GatewayTranscriptMessage (gatewayTypes.ts) gains both fields.
- Msg.kind (types.ts) gains 'event' value.
- toTranscriptMessages (domain/messages.ts) maps:
  - hidden → skip entirely
  - model_switch → event "model changed"
  - async_delegation_complete → event "N background agents finished"
    (or "background agent work finished" without metadata)
- messageGroup (blockLayout.ts) routes event to its own group, with
  SELF_SPACED + PAINTS_TRAILING_GAP so it owns its margins.
- messageLine.tsx renders event-kind as a dim ◈ marker with no gutter,
  matching the CLI's ◈ event rendering.
- 4 new TUI tests for hidden/model_switch/async_delegation mapping.

TUI typecheck: clean. TUI lint: 0 errors (2 pre-existing warnings).
TUI tests: 9 passed (1 pre-existing failure on main, unrelated).
2026-07-23 14:46:24 -04:00
joaomarcos
beffbab3d7 docs(bedrock): add cachePoint architecture infographic
Verified line-by-line against agent/bedrock_adapter.py on this branch:
allowlist split (Nova primary, Claude bearer-token fallback only,
everything else rejected), exact system→tools→messages[-2] insertion
order, and the usage normalization formula/field mapping.
2026-07-23 11:45:07 -07:00
joaomarcos
4dccfcd9b7 feat(bedrock): add Converse API prompt caching (cachePoint)
Claude-on-Bedrock already gets prompt caching via the AnthropicBedrock
SDK path. This adds it to the raw Converse API path used for non-Claude
models (Amazon Nova, and Claude when bearer-token auth forces Converse
routing, #28156) — a conservative model allowlist inserts cachePoint
blocks after tools, system, and the message before the newest turn, and
extracts cacheReadInputTokens/cacheWriteInputTokens into usage so cost
accounting picks them up through the existing Anthropic-style fallback.

Ref: relatorio-cache-performance-provedores-ia.md, P0 item 1.
2026-07-23 11:45:07 -07:00
joaomarcos
12096b1e3d docs: add SQLite FD leak infographic and report updates for #69678 2026-07-23 11:45:07 -07:00
Teknium
8e4b5d8774 harden(slack): CDN-allowlist inbound file URLs, DNS-pin token downloads, widen token-file perms warning
Follow-up hardening on top of the C14 cherry-picks (#57860/#44026/#66742/#60009):

- Slack file downloads (_download_slack_file/_download_slack_file_bytes)
  now require an https URL on a Slack CDN host (files.slack.com,
  *.slack.com Enterprise Grid, *.slack-files.com legacy shares) before
  attaching the bot token. url_private/url_private_download only ever
  point at the Slack CDN, so a forged file object from a malicious
  workspace app or compromised event stream pointing the Bearer-token
  download at an arbitrary PUBLIC host (token exfiltration) is now
  refused — a hole #44026's generic private-IP SSRF check alone could
  not close.
- The same two download paths now use create_ssrf_safe_async_client
  (from #57860) so the preflight-validated hostname is resolved once,
  validated, and dialed by IP — closing the DNS-rebinding TOCTOU window
  for the token-bearing inbound fetches as well.
- #60009's slack_tokens.json permission warning is generalized into
  utils.warn_if_credential_file_broadly_readable() (POSIX-only,
  fail-quiet) and wired into the other read path with the same gap:
  google_chat's load_user_credentials(). google_chat already writes
  0o600 via _write_private_json; the read-time warning covers
  hand-provisioned/legacy files. Nothing in-repo writes
  slack_tokens.json (user/OAuth-provisioned), so there is no write
  path to chmod for Slack.

Security tests both directions: non-CDN/lookalike/http URLs and
connect-time DNS rebinds are blocked before any TCP connect; real
files.slack.com, Enterprise Grid, and slack-files.com URLs still reach
the network layer; 0o600 files stay silent while 0o644/0o640 warn with
a chmod hint. A/B: all 10 new download-guard tests fail with the
hardening reverted and pass with it applied.
2026-07-23 11:44:43 -07:00
AlexFucuson9
fccc222bd7 fix(slack): warn when slack_tokens.json is group/world-readable
The OAuth multi-workspace token file contains plaintext bot tokens for
all saved Slack workspaces. Unlike the Google Chat adapter which sets
0o600 when writing credentials, the Slack token file has no permission
enforcement — a default umask 022 makes it world-readable.

Fix: check file permissions on read and emit a warning log with remediation
instructions if the file is group- or world-readable.
2026-07-23 11:44:43 -07:00
Frowtek
1d7db1ba17 fix(slack): neutralize prompt injection in thread-context backfill
`SlackAdapter._fetch_thread_context` formats each prior thread message as
`{name}: {msg_text}` and joins them with newlines into the block the call
site prepends *raw* into the model turn (`text = thread_context + text`).
Both fields are attacker-influenceable — any thread participant sets their
own Slack display name and message text — and neither was neutralized, so
an embedded newline let a thread message break out of its line and pose as
a fresh markdown section (a fake "## SYSTEM" / "## Override" heading) inside
the context the model reads when first mentioned mid-thread.

This is the same indirect-prompt-injection vector already closed for the
sibling untrusted sinks: the sender-name prefix
(`neutralize_untrusted_inline_text`), the reply quote, and the relay
channel-context renderer. The Slack thread-context backfill — the default
whenever the bot is mentioned in a thread with no active session — was the
missed sink. (The existing `[unverified]` tagging marks *who* a message is
from; it does nothing about newline structure, so an authorized sender can
inject just as easily.)

Flatten both fields with `neutralize_untrusted_inline_text` before
interpolation. The body uses `max_chars=0` so message text is not truncated
(thread context caps the message *count*, never per-message length); the
display name keeps the default bound. `parent_text` keeps the raw message
(its own reply-context sink neutralizes separately). A well-behaved message
is preserved byte-for-byte.

Adds a regression test covering a hostile display name, a hostile message
body, benign passthrough, and the no-truncation guarantee.
2026-07-23 11:44:43 -07:00
zapabob
2e08b778ab fix(security): validate inbound Slack file URLs against SSRF
_download_slack_file and _download_slack_file_bytes fetched Slack-supplied URLs with the bot token attached and follow_redirects=True, but without the is_safe_url pre-flight check or per-redirect guard that the outbound send_image path already uses. A URL that resolves to (or 3xx-redirects into) a private/internal address could reach internal services and leak the bot token (CWE-918). Add the same pre-flight + _ssrf_redirect_guard hook to both inbound sibling paths.
2026-07-23 11:44:43 -07:00
Eugeniusz Gilewski
0cd4afeafd fix(security): guard remaining preflighted HTTP fetches
Several platform fetch paths called is_safe_url before constructing ordinary httpx clients, leaving a second DNS lookup at connection time. This preserved the rebinding window for Slack batch images, Feishu documents, Telegram URL-photo fallback, and WeCom remote media.

Route each path through create_ssrf_safe_async_client and the shared redirect guard so direct connections validate and dial vetted IPs while configured proxies remain an explicit trusted egress boundary. Add per-path regressions that change DNS from public at preflight to metadata at connect time.

The Skills Hub provenance fixture intentionally serves content over loopback. Opt that test-scoped server into private-address access so it keeps exercising the real HTTP transport without weakening production blocking.

Related #8033

Co-authored-by: teknium1 <127238744+teknium1@users.noreply.github.com>
2026-07-23 11:44:43 -07:00
Eugeniusz Gilewski
42626da1ce fix(security): pin DNS resolutions for SSRF-safe fetches
Install connect-time DNS validation for Hermes-owned direct httpx clients so SSRF-sensitive fetch paths dial a vetted IP instead of re-resolving after preflight. This preserves Host/SNI semantics for direct HTTP(S) connections and keeps proxy routing as an explicit trusted egress boundary.

Wire the guarded clients into media cache downloads, vision downloads, Skills Hub direct/raw fetches, and platform attachment fetch paths that already perform SSRF preflight and redirect validation.

Fixes #8033

Co-authored-by: Tom Qiao <zqiao@microsoft.com>
2026-07-23 11:44:43 -07:00
Teknium
ca988df8d0 test(slack): importorskip real slack_sdk/slack_bolt in transient-edit tests
CI shards run without the slack extras; the two #64267 tests import the
real SDK (SlackApiError, the lazy-rebind path) and errored with
ModuleNotFoundError. Skip on bare environments — classification coverage
for stdlib exception types (OSError/TimeoutError/cert errors) still runs
everywhere.
2026-07-23 11:36:49 -07:00
Teknium
21ac2b50dc chore: map contributor emails for Slack C6 progress-cards salvage
- rt.cms012@gmail.com -> trac3r00 (#68378; commit authored as
  'Minseo-Choi' — trac3r00's display name, same account)
- 15167896+2001Y@users.noreply.github.com -> 2001Y (#64267)
- hello@jeromeiveson.com -> Trantor-develops (#57196)
- boumagent@gmail.com -> patp (#18859)
- dorukardahan@hotmail.com -> dorukardahan (#17184) was already mapped.
2026-07-23 11:36:49 -07:00
Teknium
5c89cc4359 fix(slack): clear assistant status on every send() exit path
Widening for #24117 ('is thinking...' stuck after the response was
sent): the stuck-thinking class is a missing-cleanup-on-error-path bug.
send()'s status clear was gated on thread_ts already being resolved and
on reaching the normal post-message path, which left the Slack
Assistant status visible when a turn ended through any sibling exit:

- exception BEFORE _resolve_thread_ts (slash-context handling,
  formatting, DM resolution) — the 'if thread_ts: stop_typing' clear
  never ran
- empty/whitespace-only final response (no_text guard early-return)
- ephemeral slash replies (Slack only auto-clears assistant status on
  real thread replies; ephemerals never count)

Add _clear_thread_status_quietly() — a best-effort stop_typing wrapper
that never masks the caller's SendResult — and wire it at every send()
exit plus the finalize paths of edit_message. stop_typing already
handles the untracked-thread fallback (clearing an unset status is a
no-op on Slack's side), so this is pure coverage widening.

Also add the #18859 unit tests for _resolve_progress_thread_id's new
reply_in_thread gate (synthetic-thread drop, real-thread keep, event-id
fallback suppression, default unchanged).

A/B: 3 of the 4 new status-clear tests fail with the adapter widening
reverted and pass with it applied; the fourth pins the existing
cleanup-must-not-mask-result contract.
2026-07-23 11:36:49 -07:00
patp
d35b003f02 fix(gateway): respect reply_in_thread=false for Slack progress messages
The Slack adapter honours platforms.slack.extra.reply_in_thread=false
in _resolve_thread_ts, but the Gateway's progress-message path forced
event_message_id as the thread_id for Slack regardless. The first
progress message ('terminal: …', 'Processing…') created a thread that
all subsequent edits and the final answer inherited, defeating the
user's reply_in_thread=false setting.

Check the live Slack adapter's reply_in_thread flag before applying the
event_message_id fallback, and treat a synthetic source.thread_id (==
the event's own message ts, used only for session keying) as 'no
thread' so progress messages stay at the channel/DM top level.

Folds both #18859 commits (reply_in_thread gate + synthetic thread_id
drop) into main's extracted _resolve_progress_thread_id helper — the
original patched the pre-refactor inline block; the gate now composes
as a keyword argument so Mattermost/other platforms keep the default
fallback behavior.
2026-07-23 11:36:49 -07:00
Doruk Ardahan
e40a38aa29 fix(slack): avoid assistant status on synthetic top-level threads
When reply_in_thread=false, top-level channel events carry their own
message ts as metadata.thread_id for session keying. Calling
assistant.threads.setStatus on that ts activated a Slack assistant
thread ('is thinking...') before the actual response was sent, and the
flat reply then never cleared it.

send_typing now routes through the same _resolve_thread_ts synthetic-
thread guard as message sending, and the gateway threads message_id
through progress/status metadata so the adapter can distinguish real
threads from synthetic top-level session keys.

Reapplied from #18859-sibling PR #17184 by @dorukardahan (both commits:
fix + progress-metadata test) onto current main via 3-way apply — the
original patched gateway/platforms/slack.py, moved to
plugins/platforms/slack/adapter.py in the plugin migration.
2026-07-23 11:36:49 -07:00
Jerome Iveson
62747aa584 fix(slack): delete stale progress messages 2026-07-23 11:36:49 -07:00
2001Y
fa8a3e328e fix(slack): preserve progress edits on network failures 2026-07-23 11:36:49 -07:00
Minseo-Choi
f716b876a8 fix(slack): edit status bubbles in place instead of posting new ones
Progress/status callbacks (context-pressure, compression retries,
model fallback) route through _send_or_update_status_coro, which
edits the previous bubble for the same status_key when the adapter
implements send_or_update_status — but only Telegram did. On Slack
every status event posted a fresh thread message, so a compression
retry loop spammed a dozen out-of-order bubbles into the thread
('Context too large 1/3... 2/3... 3/3', fallback switches, etc.).

Implement send_or_update_status on the Slack adapter following the
Telegram pattern (#30045): first call posts and caches the message ts
per (channel, thread, status_key); subsequent calls edit that message
via chat.update. Edit failure drops the cached ts and falls back to a
fresh send. Cache is FIFO-bounded.
2026-07-23 11:36:49 -07:00
Teknium
93a47dd466 chore: map yemi@lagosinternationalmarket.com -> yemi-lagosinternationalmarket
Contributor-email mapping for the #32315 salvage (thread image/file
markers reapplied onto the plugin adapter).
2026-07-23 11:35:03 -07:00
Teknium
c132783ea0 test(slack): regression coverage for thread image/file context visibility
Covers cluster C1-images (#69185, #32315, #66136):
- _slack_file_marker unit tests: typed markers per mimetype family,
  hostile-filename sanitization (newlines/brackets can't fake context
  structure).
- _render_message_text appends markers; a caption-less image post no
  longer vanishes from thread context.
- Cold-start hydrate integration: prior-message images surface as
  markers in channel_context; the thread root's image is downloaded,
  delivered as media_urls, and upgrades message_type to PHOTO.
- Failure path: root-image download failure degrades to the marker,
  never blocks the turn.
- Bounds: root delivery capped at _THREAD_ROOT_IMAGE_MAX; non-image
  root attachments stay marker-only (no download).
- One-time delivery: active thread session skips the hydrate → no
  re-download/re-delivery on later turns.
- Composition: the trigger's own event files still ride alongside a
  delivered root image; Slack Connect stubs resolve via files.info;
  the collector never issues its own conversations.replies call.
- Delta refresh (#23918 path): images in new replies past the watermark
  surface as markers, with no root re-download.

A/B: 14 of 15 tests fail with the adapter fix reverted, all pass with
it applied.
2026-07-23 11:35:03 -07:00
Teknium
2d7353cd3b feat(slack): deliver thread-root images on the first mention turn
When the bot is mentioned mid-thread for the first time, the thread root
is very often the artifact the mention is about ("@bot what's in this
chart?" posted as a reply under an image) — but the root's image never
reached the agent, so it answered blind.

On the cold-start hydrate path (and only there), _collect_thread_root_images
reads the root message from the thread-context cache the immediately
preceding _fetch_thread_context call just populated (zero extra Slack API
calls in the normal case), downloads its image/* attachments through the
existing authenticated _download_slack_file helper, and delivers them as
media_urls/media_types on the same MessageEvent — upgrading the message
type to PHOTO so vision routing engages.

Scope and safety:
- One-time delivery by construction: the cold-start path is guarded by
  _has_active_session_for_thread, so later turns in the same session can
  never re-download or re-deliver. No new gateway/session plumbing needed.
- Bounded by _THREAD_ROOT_IMAGE_MAX (4); non-image root attachments stay
  text-only markers.
- Slack Connect stubs (file_access=check_file_info) resolve via files.info.
- Best-effort: a failed download degrades to the [image: ...] marker from
  the thread context — never an error turn.
- Also hardens the video mimetype fallback (mimetype can be empty) so
  media_types entries are always non-None strings.

Adapted from #69185 by @KCAYAAI — the original plumbed MessageEvent media
through gateway/base.py, run.py and session.py with durable one-time
delivery markers (2,441 lines); this lands the user-visible behavior
adapter-locally by reusing the session guard already on the hydrate path.
2026-07-23 11:35:03 -07:00
Yemi
26a2fda8d7 fix(slack): surface thread images/files as markers in fetched thread context
Images and files posted in a Slack thread before the bot joins were
invisible to the agent: _fetch_thread_context renders text only, and a
caption-less image post was dropped from context entirely (empty text →
skip). "@bot what do you think of the chart above?" read as a question
about nothing.

_render_message_text now appends a compact, sanitized marker per file
attachment — [image: chart.png], [video: demo.mp4], [audio: note.m4a],
[file: report.pdf (application/pdf)] — so the agent can SEE that prior
thread messages carried attachments and ask for a re-share when it needs
the bytes. Filenames are stripped of newlines/brackets so a hostile name
can't fake context structure. Because both thread-context formatting and
parent-text rendering go through _render_message_text, markers appear on
the cold-start hydrate, the explicit-mention delta refresh, restart
rehydration, and reply_to_text.

Reapplied from #32315 onto the current adapter (original patched the
pre-plugin gateway/platforms/slack.py, moved in the plugin migration;
annotation labels reworked to per-file typed markers, download side
handled separately).
2026-07-23 11:35:03 -07:00
Teknium
3ec5a06f4f test(slack): mark block-privacy fixture message as human-authored (client_msg_id)
#58478's caplog test predates main's unlabeled-bot users.info probe
(#69xxx wave-2 gating): events without client_msg_id now hit
_resolve_user_is_bot, which the fixture's mock client doesn't wire up
(AttributeError on _user_is_bot_cache). Real human-authored Slack
messages carry client_msg_id — add it to the fixture so the test
exercises the intended block-extraction path.
2026-07-23 11:34:11 -07:00
Teknium
bc56002124 chore(contributors): add email mappings for slack C13 log-noise salvage
- sdevinarayanan@asymbl.com -> shivasymbl (#38847)
- mycodeisbad@gmail.com -> peterw (#69028; commit author name 'wpeterr'
  — PR opened by the peterw account, mapping follows the PR author)

LeonSGP43, ooiuuii, ygd58 mappings already present; nanckh and
haran2001 author via GitHub noreply addresses (no mapping needed).
2026-07-23 11:34:11 -07:00
Teknium
44f6f8435b test(slack): behavioral log-noise/privacy suite + keep clarify choice text out of INFO logs
Follow-up hardening for the C13 log-noise cluster:

- plugins/platforms/slack/adapter.py: clarify button resolution logged
  the full chosen option text at INFO (choice=%r). Choice text is user
  content — log the choice INDEX at INFO and the (truncated, %.100r)
  text at DEBUG only. Widens #58478's principle: no message content
  above DEBUG level anywhere in the adapter.

- tests/gateway/test_slack_log_noise.py (new): behavioral suite pinning
  the cluster's invariants:
  * catch-all ack registered AFTER every named handler (registration
    order is bolt's dispatch priority — no shadowing);
  * catch-all fires for an unsubscribed event type (reaction_added),
    logs only a DEBUG line naming the type, and never logs content;
  * named handlers still dispatch (message → _handle_slack_message);
  * end-to-end inbound message run leaves NO message text or block
    content in any adapter log record (caplog at DEBUG);
  * #30185's event-arrival diagnostic is metadata-only;
  * clarify resolution: INFO carries index/user only, text is
    DEBUG-only (fails with the adapter fix reverted — A/B verified).

Content-leak audit of all 139 logger call sites in the adapter found
two above-DEBUG leaks: the clarify choice line (fixed here) and none
else carrying message text; remaining sites log error strings, URLs
via safe_url_for_log, ids, and counts. The block-extraction DEBUG
preview was already removed by #58478 (chars= length only).
2026-07-23 11:34:11 -07:00
ygd58
565ea19a49 test(slack): pin catch-all event matcher registration and non-shadowing
Regression test for the catch-all ack: a re.compile(r'.*') event matcher
must be registered (after every named handler, so it never shadows
message/app_mention/reaction/file routing) and must match unhandled
subscribed event types like member_joined_channel / channel_archive /
pin_added.

Salvaged from #64218 (test half only — its adapter-side catch-all is a
duplicate of #38847, which landed as the base commit of this cluster
with first-submitter credit). Fixes #6572.

Co-authored-by: shivasymbl <sdevinarayanan@asymbl.com>
2026-07-23 11:34:11 -07:00
wpeterr
390ddfd028 fix(slack): quiet Slack display defaults — no heartbeat/busy-ack breadcrumbs in channels
Slack posts are durable workspace messages, not an ephemeral terminal
status area. Default long_running_notifications and busy_ack_detail to
off for Slack so long-running agent work does not leave permanent
operational breadcrumbs like 'Working — 9 min — iteration 12/90' in
channels. Both remain opt-in per platform via
display.platforms.slack.*.

Also covers the platform-generic shutdown-notification mute path with a
regression test (gateway_restart_notification=false must suppress both
the active-session interruption notice and the home-channel copy).

Salvaged from #69028 (quiet-defaults half only). The PR's other half —
the channel_session_scope_channels session-scoping feature — is a new
config feature outside this log-noise cluster and overlaps the session
scoping territory reworked by merged wave-1/2 Slack session work; it is
deliberately not taken here.
2026-07-23 11:34:11 -07:00
luyifan
ece3dd1a4f fix(slack): avoid logging block text previews 2026-07-23 11:34:11 -07:00
shivasymbl
93102e91cf fix(slack): add catch-all event handler to prevent Slack auto-disabling Event Subscriptions
Without a catch-all handler, slack-bolt returns HTTP 404 for every
unhandled bot event (user_change, user_huddle_changed, reaction_added,
etc.) and never sends the Socket Mode ack. On active Slack workspaces
where the app is subscribed to high-volume events, this produces a
near-100% un-acked failure rate that crosses Slack's >95%/60-min
threshold and triggers automatic disabling of the app's Event
Subscriptions — silently killing all inbound event delivery.

Place a catch-all re.compile(r".*") handler AFTER the specific event
handlers so bolt's router matches those first. Truly unhandled events
are silently acked (200) and logged at DEBUG. The failure rate stays
near 0% regardless of which events the Slack app manifest subscribes to.

Fixes #6572
2026-07-23 11:34:11 -07:00
nanckh
c1529e58da fix(gateway): quiet Slack missing_scope channel directory fallback
Treat Slack users.conversations missing_scope as an expected limited-scope app condition and fall back to session history without recurring warnings.

Add tests for not-ok and SlackApiError-like missing_scope responses.
2026-07-23 11:34:11 -07:00
haran2001
0eb5cb5e07 fix(slack): surface bot-event arrival and allow_bots interop diagnostics (#30091) 2026-07-23 11:34:11 -07:00
LeonSGP43
42534605b7 fix(slack): throttle channel directory warnings 2026-07-23 11:34:11 -07:00
Teknium
e027f43f70 fix(gateway): key Slack capability gate into the prompt pin; defer positive note to tool schemas
Follow-up to the #68627 cherry-pick (cluster C15 — Slack platform
capability-note accuracy; earliest report/fix: #6545 by @daikeren):

1. Session/prompt stability: the pinned session-context render
   (_pinned_session_context_prompt) is keyed by _ephemeral_change_key,
   whose contract requires every rendered input to appear in the key.
   The new _slack_tools_loaded() gate reads config + the live MCP
   registration map, so its state is now hashed into the key exactly
   like the existing Discord gate — a gate flip re-renders ONCE (a
   legitimate bust); within a session the note stays byte-stable for
   the life of the conversation (A/B: the new parity test fails with
   this key change reverted, passes with it).

2. Derived, non-overpromising positive note: rather than hardcoding a
   capability list that can drift stale again (the original bug class),
   the tools-present note tells the agent to consult the actual loaded
   Slack tool schemas for supported operations — the schemas ARE the
   source of truth, so the note cannot overclaim ops a given Slack
   toolset/MCP server doesn't expose (e.g. a read-only history server).

3. Tests: parity test proving a gate flip changes both render and key;
   byte-stability test proving three consecutive turns in one Slack
   session return the identical pinned object (sha256-equal); autouse
   fixture pins the new gate so key<->render parity is env-independent.
2026-07-23 11:32:24 -07:00
ygd58
96f21e8a54 fix(gateway): make Slack platform note capability-aware when slack tools present
Ports #63234 forward onto current main per teknium1's review.

gateway/session.py hard-coded the stale-API disclaimer for every Slack
session regardless of whether Slack tools were actually loaded. This
contradicted the system prompt when MCP or native slack tools were
present, causing the agent to refuse Slack API actions it could
actually perform (issue #6536).

Per review, the original predicate only checked the native 'slack'
toolset, missing Slack MCP servers (registered under mcp-<server> in
tools/mcp_tool.py) entirely. _slack_tools_loaded() now checks two
independent paths:

1. Native 'slack' toolset + SLACK_BOT_TOKEN (as before, but now calls
   _get_platform_tools() with include_default_mcp_servers=True instead
   of False, so a default-enabled MCP server also counts).
2. A connected MCP server that has ACTUALLY registered tools into the
   live registry (new tools.mcp_tool.get_registered_mcp_server_names()),
   whose name suggests Slack. This is session-scoped in the sense that
   matters here: MCP servers connect once per gateway process (not
   per-session), so checking the live per-server tool-registration map
   is the correct availability-filtered signal -- unlike the earlier
   get_all_tool_names() approach this replaces, which conflated ALL
   built-in tool names process-wide, this only inspects the small,
   purpose-built MCP server-name map.

Added a real regression test that registers a tool via the actual
tools.mcp_tool._track_mcp_tool_server() tracking function (not a mock
of the capability check) to verify a genuine Slack MCP server is
detected, plus a negative case for an unrelated MCP server.

5/5 Slack-specific tests pass; 126/126 in the full
tests/gateway/test_session.py file.
2026-07-23 11:32:24 -07:00
Teknium
24d7333eb1 test(gateway): cover msgraph_webhook port/host/secret bridging + contributor mapping for #57320
Follow-up to the salvaged PR #57320 commit: the PR bridged port/host/secret
for MSGRAPH_WEBHOOK but only tested webhook and api_server. Adds a test
covering the msgraph_webhook branch including extra-precedence, plus the
contributors/emails mapping for kjames2001.
2026-07-23 11:31:46 -07:00
James Huang
74db4bfe68 fix(gateway): bridge top-level port/host into extra for webhook and api_server
WebhookAdapter and ApiServerAdapter read port/host from config.extra, but
PlatformConfig.from_dict only populates extra from the 'extra:' sub-key in
the YAML platform section. Top-level keys like port and host are silently
ignored, causing the adapter to fall back to DEFAULT_PORT (8644).

This causes silent port conflicts in multi-profile setups: a profile that
configures 'platforms.webhook.port: 8649' still binds 8644, colliding with
the default profile's webhook on the same port.

Fix: extend the shared-key bridging loop in load_gateway_config() to bridge
top-level port/host/secret into extra for WEBHOOK, MSGRAPH_WEBHOOK, and
API_SERVER platforms, following the same pattern already used for
dm_policy, allow_from, gateway_restart_notification, and other keys.

The extra dict takes precedence: if port is already under 'extra:', the
top-level value does not clobber it.
2026-07-23 11:31:46 -07:00
Teknium
6b1b2e6f0e chore: map shubhambc09@gmail.com -> navahc09
haran2001's commit uses the numeric GitHub noreply
(56040092+haran2001@users.noreply.github.com) — no mapping file needed.
2026-07-23 11:31:30 -07:00
Teknium
6d4f7f4049 docs(slack): gap pass — mention-gating decision table, allow_bots deep dive, clarify buttons, ephemeral slash replies, cron/DM targeting
Documents user-facing wave-1+2 Slack behavior that had no docs coverage:
- decision table for require_mention / free_response_channels /
  require_mention_channels / thread_require_mention / strict_mention /
  ignore_other_user_mentions and how they compose
- 'Accepting messages from other bots' section with the post-#69483
  semantics: allow_bots=mentions requires a CURRENT mention from
  peer bots (text or Block Kit blocks); thread state never admits them
- clarify one-tap buttons (choice buttons + Other free-text mode,
  in-place resolution, double-click guard, expiry message)
- slash replies are ephemeral: replace-ack, chunking, 5-post cap with
  explicit truncation notice, postEphemeral fallback, never-public rule
- cron deliver targeting (slack -> home channel, slack:C... channel,
  slack:U... resolved to DM) incl. standalone sender + MEDIA uploads
- send_message media + bare-user-ID DM resolution and caption behavior

Refs #26184.
2026-07-23 11:31:30 -07:00