Files
wehub-resource-sync 4b6817381b
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
CI / coverage-report (push) Has been cancelled
CI / test-kubernetes (push) Has been cancelled
CI / should-run-thorough (push) Has been cancelled
CI / test-thorough (cloudwatch-demo) (push) Has been cancelled
CI / test-thorough (flink-ecs) (push) Has been cancelled
CI / test-thorough (upstream-lambda) (push) Has been cancelled
CI / test-thorough (prefect-ecs-fargate) (push) Has been cancelled
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Has been cancelled
Benchmark image — build + push to ECR (any adapter) / build + push (push) Has been cancelled
CI / quality (ubuntu-latest) (push) Has been cancelled
CI / test (tools-runtime) (push) Has been cancelled
CI / test (e2e-general) (push) Has been cancelled
CI / test (cli-runtime) (push) Has been cancelled
CI / test (e2e-provider-and-openclaw) (push) Has been cancelled
CI / test (integrations-and-misc) (push) Has been cancelled
Release / verify (push) Has been cancelled
Release / build-python-dist (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Has been cancelled
Release / publish-release (push) Has been cancelled
Release / publish-main-release (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Has been cancelled
Release / prepare (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Has been cancelled
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:10:45 +08:00

126 lines
4.2 KiB
Python

"""Slack ``ReportDeliveryAdapter`` and Slack-reactions port implementation.
Registers itself into the platform-level registries at import time so
``tools.investigation.reporting.delivery.dispatch`` and
``tools.investigation.stages.intake.node`` never import from
``integrations.slack`` directly (T-4 layering audit, issue #3352).
"""
from __future__ import annotations
import logging
from typing import Any
from platform.reporting.delivery_registry import (
DeliveryContext,
register_delivery_adapter,
)
from platform.reporting.slack_reactions import (
SlackReactionsPort,
register_slack_reactions_port,
)
logger = logging.getLogger(__name__)
class _SlackReportDeliveryAdapter:
"""Slack delivery adapter — always attempts delivery when Slack context is present.
Slack is treated as the "primary" channel: it is dispatched first (a
successful post updates the investigation's own thread) and its blocks are
reused across other channels. The dispatch loop passes the same shared
``blocks`` list so this adapter can decorate the returned Slack blocks
without vendors overwriting each other.
"""
name = "slack"
def build_action_blocks(
self,
investigation_url: str,
investigation_id: str | None,
) -> list[dict[str, Any]]:
"""Return the shared Slack Block Kit action blocks for the investigation.
Kept on the adapter so the dispatch loop asks the Slack integration for
its own blocks through the registry instead of importing
``integrations.slack.delivery`` directly.
"""
from integrations.slack.delivery import build_action_blocks
return build_action_blocks(investigation_url, investigation_id)
def deliver(
self,
state: DeliveryContext,
*,
messages: DeliveryContext,
blocks: list[dict[str, Any]],
) -> bool:
from integrations.slack.delivery import send_slack_report, swap_reaction
slack_ctx = state.get("slack_context", {}) or {}
thread_ts = slack_ctx.get("thread_ts") or slack_ctx.get("ts")
channel = slack_ctx.get("channel_id")
token = slack_ctx.get("access_token")
alert_ts = slack_ctx.get("ts") or slack_ctx.get("thread_ts")
logger.debug("[publish] slack_ctx=%s", slack_ctx)
report_posted, delivery_error = send_slack_report(
messages.get("slack_text", ""),
channel=channel,
thread_ts=thread_ts,
access_token=token,
blocks=blocks,
)
logger.debug(
"[publish] slack delivery: posted=%s channel=%s thread_ts=%s error=%s",
report_posted,
channel,
thread_ts,
delivery_error,
)
if report_posted and token and channel and alert_ts:
swap_reaction("eyes", "clipboard", channel, alert_ts, token)
elif thread_ts and not report_posted:
# Preserve the historical fail-closed behavior when Slack is the
# thread that triggered the investigation — we cannot silently drop
# the report there.
raise RuntimeError(
f"[publish] Slack delivery failed: channel={channel}, "
f"thread_ts={thread_ts}, reason={delivery_error}"
)
return True
class _SlackReactionsPort(SlackReactionsPort):
"""Concrete Slack reactions port backed by ``integrations.slack.delivery``."""
def add_reaction(self, emoji: str, channel: str, timestamp: str, token: str) -> None:
from integrations.slack.delivery import add_reaction
add_reaction(emoji, channel, timestamp, token)
def swap_reaction(
self,
remove_emoji: str,
add_emoji: str,
channel: str,
timestamp: str,
token: str,
) -> None:
from integrations.slack.delivery import swap_reaction
swap_reaction(remove_emoji, add_emoji, channel, timestamp, token)
slack_delivery_adapter = _SlackReportDeliveryAdapter()
slack_reactions_port = _SlackReactionsPort()
register_delivery_adapter(slack_delivery_adapter)
register_slack_reactions_port(slack_reactions_port)
__all__ = ["slack_delivery_adapter", "slack_reactions_port"]