""" Slack Integration for Agent Governance System Provides: - Alert notifications - Status updates - Interactive approval workflows - Agent activity feeds """ import os import json from typing import Dict, Any, Optional, List from dataclasses import dataclass from datetime import datetime from enum import Enum import sys sys.path.insert(0, str(__file__).rsplit("/", 2)[0]) from common.base import BaseIntegration, IntegrationConfig, IntegrationEvent class AlertSeverity(Enum): INFO = "info" WARNING = "warning" ERROR = "error" CRITICAL = "critical" @dataclass class SlackMessage: """Represents a Slack message""" channel: str text: str blocks: Optional[List[Dict]] = None attachments: Optional[List[Dict]] = None thread_ts: Optional[str] = None class SlackIntegration(BaseIntegration): """ Slack integration for agent governance. Capabilities: - Send alerts for violations - Post status updates - Request human approval - Notify on promotions/revocations """ def __init__(self, webhook_url: str = None, bot_token: str = None): config = IntegrationConfig( name="slack", enabled=webhook_url is not None or bot_token is not None, api_key=bot_token or os.environ.get("SLACK_BOT_TOKEN"), api_url=webhook_url or os.environ.get("SLACK_WEBHOOK_URL"), extra={ "default_channel": os.environ.get("SLACK_DEFAULT_CHANNEL", "#agent-governance"), "alert_channel": os.environ.get("SLACK_ALERT_CHANNEL", "#alerts") } ) super().__init__(config) def test_connection(self) -> bool: """Test Slack connection""" if self._dry_run: self._audit("test_connection", True, {"dry_run": True}) return True if not self.config.api_url and not self.config.api_key: self._audit("test_connection", False, {"error": "no_credentials"}) return False self._audit("test_connection", True) return True def send_event(self, event: IntegrationEvent) -> bool: """Route event to appropriate Slack notification""" handlers = { "plan_created": self._notify_plan, "execution_started": self._notify_execution_start, "execution_complete": self._notify_execution_complete, "violation_detected": self._alert_violation, "promotion_requested": self._notify_promotion, "promotion_approved": self._notify_promotion_approved, "agent_revoked": self._alert_revocation, "approval_required": self._request_approval, "heartbeat": self._update_status, } handler = handlers.get(event.event_type) if handler: return handler(event) # Default: post as generic message return self._post_generic(event) def _notify_plan(self, event: IntegrationEvent) -> bool: """Notify about a new plan""" plan = event.data.get("plan", {}) agent_id = event.source message = self._build_message( channel=self.config.extra["default_channel"], text=f"New plan created by agent `{agent_id}`", blocks=[ self._header_block(f":clipboard: Plan Created"), self._section_block(f"*Agent:* `{agent_id}`\n*Title:* {plan.get('title', 'Untitled')}"), self._section_block(f"*Objective:*\n{plan.get('objective', 'N/A')}"), self._context_block(f"Confidence: {plan.get('confidence', 'N/A')} | Tier: {plan.get('tier_required', 'N/A')}") ] ) return self._send_message(message) def _notify_execution_start(self, event: IntegrationEvent) -> bool: """Notify that execution has started""" agent_id = event.source task = event.data.get("task", {}) message = self._build_message( channel=self.config.extra["default_channel"], text=f"Agent `{agent_id}` started execution", blocks=[ self._header_block(":rocket: Execution Started"), self._section_block(f"*Agent:* `{agent_id}`\n*Task:* {task.get('title', 'N/A')}"), ] ) return self._send_message(message) def _notify_execution_complete(self, event: IntegrationEvent) -> bool: """Notify that execution completed""" result = event.data.get("result", {}) agent_id = event.source success = result.get("success", False) emoji = ":white_check_mark:" if success else ":x:" status = "Success" if success else "Failed" message = self._build_message( channel=self.config.extra["default_channel"], text=f"Agent `{agent_id}` execution {status.lower()}", blocks=[ self._header_block(f"{emoji} Execution {status}"), self._section_block(f"*Agent:* `{agent_id}`"), self._section_block(f"*Duration:* {result.get('duration', 'N/A')}s"), ] ) return self._send_message(message) def _alert_violation(self, event: IntegrationEvent) -> bool: """Alert about a governance violation""" violation = event.data.get("violation", {}) agent_id = event.source severity = violation.get("severity", "medium") emoji_map = { "low": ":warning:", "medium": ":warning:", "high": ":rotating_light:", "critical": ":fire:" } message = self._build_message( channel=self.config.extra["alert_channel"], text=f"Governance violation by agent `{agent_id}`", blocks=[ self._header_block(f"{emoji_map.get(severity, ':warning:')} Violation Detected"), self._section_block( f"*Agent:* `{agent_id}`\n" f"*Type:* {violation.get('type', 'Unknown')}\n" f"*Severity:* {severity.upper()}" ), self._section_block(f"*Details:*\n{violation.get('description', 'No details')}"), ], attachments=[{ "color": self._severity_color(severity), "text": f"Evidence: {json.dumps(violation.get('evidence', {}))[:500]}" }] ) return self._send_message(message) def _notify_promotion(self, event: IntegrationEvent) -> bool: """Notify about a promotion request""" promotion = event.data.get("promotion", {}) agent_id = event.source message = self._build_message( channel=self.config.extra["default_channel"], text=f"Promotion requested for agent `{agent_id}`", blocks=[ self._header_block(":arrow_up: Promotion Requested"), self._section_block( f"*Agent:* `{agent_id}`\n" f"*From Tier:* {promotion.get('from_tier')}\n" f"*To Tier:* {promotion.get('to_tier')}" ), self._section_block( f"*Metrics:*\n" f"- Compliant Runs: {promotion.get('compliant_runs', 0)}\n" f"- Consecutive: {promotion.get('consecutive_compliant', 0)}" ), ] ) return self._send_message(message) def _notify_promotion_approved(self, event: IntegrationEvent) -> bool: """Notify that a promotion was approved""" promotion = event.data.get("promotion", {}) agent_id = event.source message = self._build_message( channel=self.config.extra["default_channel"], text=f"Agent `{agent_id}` promoted!", blocks=[ self._header_block(":tada: Promotion Approved"), self._section_block( f"*Agent:* `{agent_id}`\n" f"*New Tier:* {promotion.get('to_tier')}\n" f"*Approved By:* {promotion.get('approved_by', 'System')}" ), ] ) return self._send_message(message) def _alert_revocation(self, event: IntegrationEvent) -> bool: """Alert about agent revocation""" revocation = event.data.get("revocation", {}) agent_id = event.source message = self._build_message( channel=self.config.extra["alert_channel"], text=f"Agent `{agent_id}` has been revoked!", blocks=[ self._header_block(":no_entry: Agent Revoked"), self._section_block( f"*Agent:* `{agent_id}`\n" f"*Reason:* {revocation.get('reason', 'Unknown')}" ), self._section_block(f"*Details:*\n{revocation.get('description', 'No details')}"), ], attachments=[{ "color": "danger", "text": "This agent's token has been revoked and it can no longer execute actions." }] ) return self._send_message(message) def _request_approval(self, event: IntegrationEvent) -> bool: """Request human approval""" request = event.data.get("request", {}) agent_id = event.source message = self._build_message( channel=self.config.extra["default_channel"], text=f"Approval required for agent `{agent_id}`", blocks=[ self._header_block(":raising_hand: Approval Required"), self._section_block( f"*Agent:* `{agent_id}`\n" f"*Action:* {request.get('action', 'Unknown')}\n" f"*Target:* {request.get('target', 'N/A')}" ), self._section_block(f"*Details:*\n{request.get('details', 'No details')}"), { "type": "actions", "elements": [ { "type": "button", "text": {"type": "plain_text", "text": "Approve"}, "style": "primary", "action_id": f"approve_{event.data.get('request_id', 'unknown')}" }, { "type": "button", "text": {"type": "plain_text", "text": "Deny"}, "style": "danger", "action_id": f"deny_{event.data.get('request_id', 'unknown')}" } ] } ] ) return self._send_message(message) def _update_status(self, event: IntegrationEvent) -> bool: """Update agent status (usually silent)""" # Status updates typically don't need notifications self._audit("heartbeat", True, {"agent": event.source}) return True def _post_generic(self, event: IntegrationEvent) -> bool: """Post a generic event notification""" message = self._build_message( channel=self.config.extra["default_channel"], text=f"Agent event: {event.event_type}", blocks=[ self._header_block(f":robot_face: {event.event_type}"), self._section_block(f"*Source:* `{event.source}`"), self._section_block(f"```{json.dumps(event.data, indent=2)[:1000]}```") ] ) return self._send_message(message) # === Message Building Helpers === def _build_message(self, channel: str, text: str, blocks: List[Dict] = None, attachments: List[Dict] = None) -> SlackMessage: """Build a Slack message""" return SlackMessage( channel=channel, text=text, blocks=blocks, attachments=attachments ) def _header_block(self, text: str) -> Dict: """Create a header block""" return { "type": "header", "text": {"type": "plain_text", "text": text} } def _section_block(self, text: str) -> Dict: """Create a section block""" return { "type": "section", "text": {"type": "mrkdwn", "text": text} } def _context_block(self, text: str) -> Dict: """Create a context block""" return { "type": "context", "elements": [{"type": "mrkdwn", "text": text}] } def _severity_color(self, severity: str) -> str: """Get color for severity level""" colors = { "low": "#2eb886", # green "medium": "#daa038", # yellow "high": "#cc4444", # orange-red "critical": "#ff0000" # red } return colors.get(severity, "#808080") def _send_message(self, message: SlackMessage) -> bool: """Send a message to Slack""" if self._dry_run: self._audit("send_message", True, { "dry_run": True, "channel": message.channel, "text": message.text[:100] }) return True if not self.config.api_url and not self.config.api_key: return False # Would send via webhook or API self._audit("send_message", True, { "channel": message.channel, "has_blocks": message.blocks is not None }) return True # === Stub API Methods === def post_webhook(self, payload: Dict) -> bool: """Post to webhook URL (stub)""" if self._dry_run or not self.config.api_url: return True # Would POST to webhook URL self._audit("webhook_post", True) return True def api_call(self, method: str, **kwargs) -> Optional[Dict]: """Make Slack API call (stub)""" if self._dry_run or not self.config.api_key: return None # Would call Slack API self._audit("api_call", True, {"method": method}) return None