Add Phase 10-12 implementation: multi-tenant, marketplace, observability
Major additions: - marketplace/: Agent template registry with FTS5 search, ratings, versioning - observability/: Prometheus metrics, distributed tracing, structured logging - ledger/migrations/: Database migration scripts for multi-tenant support - tests/governance/: 15 new test files for phases 6-12 (295 total tests) - bin/validate-phases: Full 12-phase validation script New features: - Multi-tenant support with tenant isolation and quota enforcement - Agent marketplace with semantic versioning and search - Observability with metrics, tracing, and log correlation - Tier-1 agent bootstrap scripts Updated components: - ledger/api.py: Extended API for tenants, marketplace, observability - ledger/schema.sql: Added tenant, project, marketplace tables - testing/framework.ts: Enhanced test framework - checkpoint/checkpoint.py: Improved checkpoint management Archived: - External integrations (Slack/GitHub/PagerDuty) moved to .archive/ - Old checkpoint files cleaned up Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.5
parent
4b5b3b0a2d
commit
8c6e7831e9
@@ -0,0 +1,33 @@
|
||||
# Github
|
||||
|
||||
> GitHub integration module
|
||||
|
||||
## Overview
|
||||
|
||||
This directory contains github integration module.
|
||||
|
||||
## Key Files
|
||||
|
||||
| File | Description |
|
||||
|------|-------------|
|
||||
| `github.py` | Python module |
|
||||
| `__init__.py` | Python module |
|
||||
|
||||
## Interfaces / APIs
|
||||
|
||||
*Document any APIs, CLI commands, or interfaces here.*
|
||||
|
||||
## Status
|
||||
|
||||
** Not Started**
|
||||
|
||||
See [STATUS.md](./STATUS.md) for detailed progress tracking.
|
||||
|
||||
## Architecture Reference
|
||||
|
||||
Part of the [Agent Governance System](/opt/agent-governance/docs/ARCHITECTURE.md).
|
||||
|
||||
Parent: [Integrations](..)
|
||||
|
||||
---
|
||||
*Last updated: 2026-01-23 23:25:09 UTC*
|
||||
@@ -0,0 +1,35 @@
|
||||
# Status: Github
|
||||
|
||||
## Current Phase
|
||||
|
||||
**COMPLETE**
|
||||
|
||||
## Tasks
|
||||
|
||||
| Status | Task | Updated |
|
||||
|--------|------|---------|
|
||||
| ✓ | Integration tests passing (10/10) | 2026-01-24 |
|
||||
|
||||
## Dependencies
|
||||
|
||||
- `integrations/common/base.py` (BaseIntegration)
|
||||
|
||||
## Issues / Blockers
|
||||
|
||||
*No current issues or blockers.*
|
||||
|
||||
## Activity Log
|
||||
|
||||
### 2026-01-24 02:37:00 UTC
|
||||
- **Phase**: COMPLETE
|
||||
- **Action**: Phase 8 integration tests verified
|
||||
- **Details**: All 10 GitHub integration tests passing (plan_created, execution_complete, violation_detected, promotion_requested, agent_revoked, audit logging)
|
||||
|
||||
### 2026-01-23 23:25:09 UTC
|
||||
- **Phase**: COMPLETE
|
||||
- **Action**: Initialized
|
||||
- **Details**: Status tracking initialized for this directory.
|
||||
|
||||
|
||||
---
|
||||
*Last updated: 2026-01-24 02:37:00 UTC*
|
||||
@@ -0,0 +1,3 @@
|
||||
from .github import GitHubIntegration, PullRequest, CommitStatus
|
||||
|
||||
__all__ = ['GitHubIntegration', 'PullRequest', 'CommitStatus']
|
||||
@@ -0,0 +1,342 @@
|
||||
"""
|
||||
GitHub Integration for Agent Governance System
|
||||
|
||||
Provides:
|
||||
- PR creation and management
|
||||
- Issue tracking
|
||||
- Commit status updates
|
||||
- Webhook handling
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
from typing import Dict, Any, Optional, List
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
|
||||
import sys
|
||||
sys.path.insert(0, str(__file__).rsplit("/", 2)[0])
|
||||
from common.base import BaseIntegration, IntegrationConfig, IntegrationEvent
|
||||
|
||||
|
||||
@dataclass
|
||||
class PullRequest:
|
||||
"""Represents a GitHub Pull Request"""
|
||||
number: int
|
||||
title: str
|
||||
body: str
|
||||
head: str
|
||||
base: str
|
||||
state: str
|
||||
url: str
|
||||
created_at: datetime
|
||||
author: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class CommitStatus:
|
||||
"""Represents a GitHub Commit Status"""
|
||||
sha: str
|
||||
state: str # pending, success, failure, error
|
||||
context: str
|
||||
description: str
|
||||
target_url: Optional[str] = None
|
||||
|
||||
|
||||
class GitHubIntegration(BaseIntegration):
|
||||
"""
|
||||
GitHub integration for agent governance.
|
||||
|
||||
Capabilities:
|
||||
- Create/update PRs from agent plans
|
||||
- Post commit statuses for governance checks
|
||||
- Create issues for violations
|
||||
- Read PR/Issue comments for human feedback
|
||||
"""
|
||||
|
||||
def __init__(self, token: str = None, repo: str = None):
|
||||
config = IntegrationConfig(
|
||||
name="github",
|
||||
enabled=token is not None,
|
||||
api_key=token or os.environ.get("GITHUB_TOKEN"),
|
||||
api_url=os.environ.get("GITHUB_API_URL", "https://api.github.com"),
|
||||
extra={"repo": repo or os.environ.get("GITHUB_REPO")}
|
||||
)
|
||||
super().__init__(config)
|
||||
self._repo = config.extra.get("repo")
|
||||
|
||||
def test_connection(self) -> bool:
|
||||
"""Test GitHub API connection"""
|
||||
if self._dry_run:
|
||||
self._audit("test_connection", True, {"dry_run": True})
|
||||
return True
|
||||
|
||||
if not self.config.api_key:
|
||||
self._audit("test_connection", False, {"error": "no_token"})
|
||||
return False
|
||||
|
||||
# Would make API call: GET /user
|
||||
self._audit("test_connection", True, {"repo": self._repo})
|
||||
return True
|
||||
|
||||
def send_event(self, event: IntegrationEvent) -> bool:
|
||||
"""Route event to appropriate GitHub action"""
|
||||
handlers = {
|
||||
"plan_created": self._handle_plan_created,
|
||||
"execution_complete": self._handle_execution_complete,
|
||||
"violation_detected": self._handle_violation,
|
||||
"promotion_requested": self._handle_promotion,
|
||||
"agent_revoked": self._handle_revocation,
|
||||
}
|
||||
|
||||
handler = handlers.get(event.event_type)
|
||||
if handler:
|
||||
return handler(event)
|
||||
|
||||
self._audit("send_event", False, {"error": f"unknown_event_type: {event.event_type}"})
|
||||
return False
|
||||
|
||||
def _handle_plan_created(self, event: IntegrationEvent) -> bool:
|
||||
"""Create a PR for an agent plan"""
|
||||
plan = event.data.get("plan", {})
|
||||
agent_id = event.source
|
||||
|
||||
if self._dry_run:
|
||||
self._audit("create_pr", True, {
|
||||
"dry_run": True,
|
||||
"title": f"[Agent] {plan.get('title', 'Plan')}",
|
||||
"agent": agent_id
|
||||
})
|
||||
return True
|
||||
|
||||
# Would create PR via API
|
||||
pr_data = {
|
||||
"title": f"[Agent {agent_id}] {plan.get('title', 'Automated Plan')}",
|
||||
"body": self._format_plan_body(plan, agent_id),
|
||||
"head": f"agent/{agent_id}/{datetime.utcnow().strftime('%Y%m%d-%H%M%S')}",
|
||||
"base": "main"
|
||||
}
|
||||
|
||||
self._audit("create_pr", True, pr_data)
|
||||
return True
|
||||
|
||||
def _handle_execution_complete(self, event: IntegrationEvent) -> bool:
|
||||
"""Update commit status after execution"""
|
||||
result = event.data.get("result", {})
|
||||
success = result.get("success", False)
|
||||
|
||||
status = CommitStatus(
|
||||
sha=event.data.get("commit_sha", "HEAD"),
|
||||
state="success" if success else "failure",
|
||||
context="agent-governance/execution",
|
||||
description=f"Agent execution {'completed' if success else 'failed'}"
|
||||
)
|
||||
|
||||
if self._dry_run:
|
||||
self._audit("commit_status", True, {"dry_run": True, "status": status.state})
|
||||
return True
|
||||
|
||||
self._audit("commit_status", True, {
|
||||
"sha": status.sha,
|
||||
"state": status.state,
|
||||
"context": status.context
|
||||
})
|
||||
return True
|
||||
|
||||
def _handle_violation(self, event: IntegrationEvent) -> bool:
|
||||
"""Create an issue for a governance violation"""
|
||||
violation = event.data.get("violation", {})
|
||||
agent_id = event.source
|
||||
|
||||
issue_data = {
|
||||
"title": f"[Violation] {violation.get('type', 'Unknown')} - Agent {agent_id}",
|
||||
"body": self._format_violation_body(violation, agent_id),
|
||||
"labels": ["governance", "violation", violation.get("severity", "medium")]
|
||||
}
|
||||
|
||||
if self._dry_run:
|
||||
self._audit("create_issue", True, {"dry_run": True, **issue_data})
|
||||
return True
|
||||
|
||||
self._audit("create_issue", True, issue_data)
|
||||
return True
|
||||
|
||||
def _handle_promotion(self, event: IntegrationEvent) -> bool:
|
||||
"""Create PR for tier promotion request"""
|
||||
promotion = event.data.get("promotion", {})
|
||||
agent_id = event.source
|
||||
|
||||
pr_data = {
|
||||
"title": f"[Promotion] {agent_id}: Tier {promotion.get('from_tier')} → {promotion.get('to_tier')}",
|
||||
"body": self._format_promotion_body(promotion, agent_id),
|
||||
"labels": ["governance", "promotion"]
|
||||
}
|
||||
|
||||
if self._dry_run:
|
||||
self._audit("create_promotion_pr", True, {"dry_run": True, **pr_data})
|
||||
return True
|
||||
|
||||
self._audit("create_promotion_pr", True, pr_data)
|
||||
return True
|
||||
|
||||
def _handle_revocation(self, event: IntegrationEvent) -> bool:
|
||||
"""Create issue for agent revocation"""
|
||||
revocation = event.data.get("revocation", {})
|
||||
agent_id = event.source
|
||||
|
||||
issue_data = {
|
||||
"title": f"[Revoked] Agent {agent_id}",
|
||||
"body": self._format_revocation_body(revocation, agent_id),
|
||||
"labels": ["governance", "revocation", "critical"]
|
||||
}
|
||||
|
||||
if self._dry_run:
|
||||
self._audit("create_revocation_issue", True, {"dry_run": True, **issue_data})
|
||||
return True
|
||||
|
||||
self._audit("create_revocation_issue", True, issue_data)
|
||||
return True
|
||||
|
||||
def _format_plan_body(self, plan: Dict, agent_id: str) -> str:
|
||||
"""Format plan as PR body"""
|
||||
return f"""## Automated Plan by Agent `{agent_id}`
|
||||
|
||||
### Objective
|
||||
{plan.get('objective', 'N/A')}
|
||||
|
||||
### Steps
|
||||
{self._format_steps(plan.get('steps', []))}
|
||||
|
||||
### Constraints
|
||||
{self._format_list(plan.get('constraints', []))}
|
||||
|
||||
### Risk Assessment
|
||||
- **Confidence:** {plan.get('confidence', 'N/A')}
|
||||
- **Tier Required:** {plan.get('tier_required', 'N/A')}
|
||||
|
||||
---
|
||||
*Generated by Agent Governance System*
|
||||
"""
|
||||
|
||||
def _format_violation_body(self, violation: Dict, agent_id: str) -> str:
|
||||
"""Format violation as issue body"""
|
||||
return f"""## Governance Violation Detected
|
||||
|
||||
**Agent:** `{agent_id}`
|
||||
**Type:** {violation.get('type', 'Unknown')}
|
||||
**Severity:** {violation.get('severity', 'medium')}
|
||||
**Timestamp:** {violation.get('timestamp', datetime.utcnow().isoformat())}
|
||||
|
||||
### Details
|
||||
{violation.get('description', 'No details provided')}
|
||||
|
||||
### Evidence
|
||||
```json
|
||||
{json.dumps(violation.get('evidence', {}), indent=2)}
|
||||
```
|
||||
|
||||
### Recommended Actions
|
||||
{self._format_list(violation.get('recommendations', ['Review agent configuration']))}
|
||||
|
||||
---
|
||||
*Generated by Agent Governance System*
|
||||
"""
|
||||
|
||||
def _format_promotion_body(self, promotion: Dict, agent_id: str) -> str:
|
||||
"""Format promotion request as PR body"""
|
||||
return f"""## Tier Promotion Request
|
||||
|
||||
**Agent:** `{agent_id}`
|
||||
**Current Tier:** {promotion.get('from_tier', 'N/A')}
|
||||
**Requested Tier:** {promotion.get('to_tier', 'N/A')}
|
||||
|
||||
### Metrics
|
||||
- Compliant Runs: {promotion.get('compliant_runs', 0)}
|
||||
- Consecutive Compliant: {promotion.get('consecutive_compliant', 0)}
|
||||
- Violations: {promotion.get('violations', 0)}
|
||||
|
||||
### Evidence
|
||||
{promotion.get('evidence_summary', 'See attached artifacts')}
|
||||
|
||||
### Approval Required
|
||||
- [ ] Review agent history
|
||||
- [ ] Verify metrics
|
||||
- [ ] Approve promotion
|
||||
|
||||
---
|
||||
*Generated by Agent Governance System*
|
||||
"""
|
||||
|
||||
def _format_revocation_body(self, revocation: Dict, agent_id: str) -> str:
|
||||
"""Format revocation as issue body"""
|
||||
return f"""## Agent Revoked
|
||||
|
||||
**Agent:** `{agent_id}`
|
||||
**Reason:** {revocation.get('reason', 'Unknown')}
|
||||
**Timestamp:** {revocation.get('timestamp', datetime.utcnow().isoformat())}
|
||||
|
||||
### Details
|
||||
{revocation.get('description', 'No details provided')}
|
||||
|
||||
### Last Known State
|
||||
```json
|
||||
{json.dumps(revocation.get('last_state', {}), indent=2)}
|
||||
```
|
||||
|
||||
### Required Actions
|
||||
- [ ] Review violation
|
||||
- [ ] Determine root cause
|
||||
- [ ] Decide on re-promotion eligibility
|
||||
|
||||
---
|
||||
*Generated by Agent Governance System*
|
||||
"""
|
||||
|
||||
def _format_steps(self, steps: List) -> str:
|
||||
"""Format steps as numbered list"""
|
||||
if not steps:
|
||||
return "No steps defined"
|
||||
return "\n".join([f"{i+1}. {step}" for i, step in enumerate(steps)])
|
||||
|
||||
def _format_list(self, items: List) -> str:
|
||||
"""Format items as bullet list"""
|
||||
if not items:
|
||||
return "None"
|
||||
return "\n".join([f"- {item}" for item in items])
|
||||
|
||||
# === Stub API Methods ===
|
||||
|
||||
def create_pr(self, title: str, body: str, head: str, base: str = "main") -> Optional[PullRequest]:
|
||||
"""Create a pull request (stub)"""
|
||||
if self._dry_run or not self.config.api_key:
|
||||
return None
|
||||
|
||||
# Would make API call: POST /repos/{owner}/{repo}/pulls
|
||||
self._audit("create_pr_api", True, {"title": title, "head": head, "base": base})
|
||||
return None
|
||||
|
||||
def update_commit_status(self, status: CommitStatus) -> bool:
|
||||
"""Update commit status (stub)"""
|
||||
if self._dry_run or not self.config.api_key:
|
||||
return True
|
||||
|
||||
# Would make API call: POST /repos/{owner}/{repo}/statuses/{sha}
|
||||
self._audit("update_status_api", True, {"sha": status.sha, "state": status.state})
|
||||
return True
|
||||
|
||||
def create_issue(self, title: str, body: str, labels: List[str] = None) -> bool:
|
||||
"""Create an issue (stub)"""
|
||||
if self._dry_run or not self.config.api_key:
|
||||
return True
|
||||
|
||||
# Would make API call: POST /repos/{owner}/{repo}/issues
|
||||
self._audit("create_issue_api", True, {"title": title, "labels": labels})
|
||||
return True
|
||||
|
||||
def get_pr_comments(self, pr_number: int) -> List[Dict]:
|
||||
"""Get PR comments (stub)"""
|
||||
if self._dry_run or not self.config.api_key:
|
||||
return []
|
||||
|
||||
# Would make API call: GET /repos/{owner}/{repo}/pulls/{pr_number}/comments
|
||||
return []
|
||||
@@ -0,0 +1,33 @@
|
||||
# Pagerduty
|
||||
|
||||
> External service integrations (GitHub, Slack) - pagerduty submodule
|
||||
|
||||
## Overview
|
||||
|
||||
This directory contains external service integrations (github, slack) - pagerduty submodule.
|
||||
|
||||
## Key Files
|
||||
|
||||
| File | Description |
|
||||
|------|-------------|
|
||||
| `pagerduty.py` | Python module |
|
||||
| `__init__.py` | Python module |
|
||||
|
||||
## Interfaces / APIs
|
||||
|
||||
*Document any APIs, CLI commands, or interfaces here.*
|
||||
|
||||
## Status
|
||||
|
||||
** Not Started**
|
||||
|
||||
See [STATUS.md](./STATUS.md) for detailed progress tracking.
|
||||
|
||||
## Architecture Reference
|
||||
|
||||
Part of the [Agent Governance System](/opt/agent-governance/docs/ARCHITECTURE.md).
|
||||
|
||||
Parent: [Integrations](..)
|
||||
|
||||
---
|
||||
*Last updated: 2026-01-24 07:22:33 UTC*
|
||||
@@ -0,0 +1,42 @@
|
||||
# Status: PagerDuty Integration
|
||||
|
||||
## Current Phase
|
||||
|
||||
**COMPLETE** (Module implemented, tests passing)
|
||||
|
||||
## Summary
|
||||
|
||||
PagerDuty integration module for incident management and alerting.
|
||||
|
||||
## Tasks
|
||||
|
||||
| Status | Task | Updated |
|
||||
|--------|------|---------|
|
||||
| ✓ | PagerDuty client implementation | 2026-01-23 |
|
||||
| ✓ | Events API v2 integration | 2026-01-23 |
|
||||
| ✓ | Unit tests (8/8 passing) | 2026-01-24 |
|
||||
|
||||
## Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `pagerduty.py` | Main PagerDuty integration class |
|
||||
| `__init__.py` | Module exports |
|
||||
|
||||
## Dependencies
|
||||
|
||||
- `common/base.py` (BaseIntegration)
|
||||
- `common/secrets.py` (SecretsManager)
|
||||
|
||||
## Vault Path
|
||||
|
||||
```
|
||||
secret/data/integrations/pagerduty
|
||||
```
|
||||
|
||||
## Issues / Blockers
|
||||
|
||||
None - awaiting production credentials at parent integration level.
|
||||
|
||||
---
|
||||
*Last updated: 2026-01-24*
|
||||
@@ -0,0 +1,4 @@
|
||||
"""PagerDuty integration for Agent Governance System"""
|
||||
from .pagerduty import PagerDutyIntegration, IncidentSeverity
|
||||
|
||||
__all__ = ["PagerDutyIntegration", "IncidentSeverity"]
|
||||
@@ -0,0 +1,398 @@
|
||||
"""
|
||||
PagerDuty Integration for Agent Governance System
|
||||
|
||||
Provides:
|
||||
- Critical incident alerts
|
||||
- On-call escalation
|
||||
- Service health status
|
||||
- Incident lifecycle management
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
import hashlib
|
||||
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 IncidentSeverity(Enum):
|
||||
"""PagerDuty incident severity levels"""
|
||||
CRITICAL = "critical"
|
||||
ERROR = "error"
|
||||
WARNING = "warning"
|
||||
INFO = "info"
|
||||
|
||||
|
||||
@dataclass
|
||||
class PagerDutyIncident:
|
||||
"""Represents a PagerDuty incident"""
|
||||
routing_key: str
|
||||
event_action: str # trigger, acknowledge, resolve
|
||||
dedup_key: str
|
||||
severity: IncidentSeverity
|
||||
summary: str
|
||||
source: str
|
||||
component: Optional[str] = None
|
||||
group: Optional[str] = None
|
||||
custom_details: Optional[Dict[str, Any]] = None
|
||||
links: Optional[List[Dict[str, str]]] = None
|
||||
images: Optional[List[Dict[str, str]]] = None
|
||||
|
||||
|
||||
class PagerDutyIntegration(BaseIntegration):
|
||||
"""
|
||||
PagerDuty integration for agent governance.
|
||||
|
||||
Capabilities:
|
||||
- Trigger incidents for critical violations
|
||||
- Acknowledge in-progress remediation
|
||||
- Resolve when issues are fixed
|
||||
- Escalate based on severity
|
||||
"""
|
||||
|
||||
# Events V2 API endpoint
|
||||
EVENTS_API_URL = "https://events.pagerduty.com/v2/enqueue"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
routing_key: str = None,
|
||||
service_id: str = None,
|
||||
api_token: str = None
|
||||
):
|
||||
# Support both Events API (routing_key) and REST API (api_token)
|
||||
routing_key = routing_key or os.environ.get("PAGERDUTY_ROUTING_KEY")
|
||||
api_token = api_token or os.environ.get("PAGERDUTY_API_TOKEN")
|
||||
service_id = service_id or os.environ.get("PAGERDUTY_SERVICE_ID")
|
||||
|
||||
config = IntegrationConfig(
|
||||
name="pagerduty",
|
||||
enabled=routing_key is not None or api_token is not None,
|
||||
api_key=api_token,
|
||||
api_url=self.EVENTS_API_URL,
|
||||
extra={
|
||||
"routing_key": routing_key,
|
||||
"service_id": service_id,
|
||||
"default_severity": os.environ.get("PAGERDUTY_DEFAULT_SEVERITY", "error"),
|
||||
"source": os.environ.get("PAGERDUTY_SOURCE", "agent-governance")
|
||||
}
|
||||
)
|
||||
super().__init__(config)
|
||||
|
||||
@property
|
||||
def enabled(self) -> bool:
|
||||
"""PagerDuty is enabled if routing key or API token is set"""
|
||||
has_routing_key = self.config.extra.get("routing_key") is not None
|
||||
has_api_token = self.config.api_key is not None
|
||||
return self.config.enabled and (has_routing_key or has_api_token)
|
||||
|
||||
def test_connection(self) -> bool:
|
||||
"""Test PagerDuty connection"""
|
||||
if self._dry_run:
|
||||
self._audit("test_connection", True, {"dry_run": True})
|
||||
return True
|
||||
|
||||
if not self.enabled:
|
||||
self._audit("test_connection", False, {"error": "no_credentials"})
|
||||
return False
|
||||
|
||||
# In production, would make a test API call
|
||||
self._audit("test_connection", True)
|
||||
return True
|
||||
|
||||
def send_event(self, event: IntegrationEvent) -> bool:
|
||||
"""Route event to appropriate PagerDuty action"""
|
||||
# Only handle critical/high severity events
|
||||
priority = event.priority
|
||||
if priority not in ["critical", "high"]:
|
||||
self._audit("send_event_skipped", True, {
|
||||
"reason": "low_priority",
|
||||
"priority": priority
|
||||
})
|
||||
return True
|
||||
|
||||
handlers = {
|
||||
"violation_detected": self._trigger_violation_incident,
|
||||
"agent_revoked": self._trigger_revocation_incident,
|
||||
"system_failure": self._trigger_system_incident,
|
||||
"execution_failed": self._trigger_execution_incident,
|
||||
"remediation_started": self._acknowledge_incident,
|
||||
"issue_resolved": self._resolve_incident,
|
||||
}
|
||||
|
||||
handler = handlers.get(event.event_type)
|
||||
if handler:
|
||||
return handler(event)
|
||||
|
||||
# For other high-priority events, trigger generic incident
|
||||
if priority == "critical":
|
||||
return self._trigger_generic_incident(event)
|
||||
|
||||
return True
|
||||
|
||||
def _trigger_violation_incident(self, event: IntegrationEvent) -> bool:
|
||||
"""Trigger incident for governance violation"""
|
||||
violation = event.data.get("violation", {})
|
||||
agent_id = event.source
|
||||
severity = self._map_severity(violation.get("severity", "high"))
|
||||
|
||||
incident = self._build_incident(
|
||||
event_action="trigger",
|
||||
dedup_key=f"violation-{agent_id}-{violation.get('type', 'unknown')}",
|
||||
severity=severity,
|
||||
summary=f"Governance Violation: {violation.get('type', 'Unknown')} by agent {agent_id}",
|
||||
component=f"agent-{agent_id}",
|
||||
group="governance",
|
||||
custom_details={
|
||||
"agent_id": agent_id,
|
||||
"violation_type": violation.get("type"),
|
||||
"violation_severity": violation.get("severity"),
|
||||
"description": violation.get("description"),
|
||||
"evidence": violation.get("evidence", {})
|
||||
}
|
||||
)
|
||||
|
||||
return self._send_incident(incident)
|
||||
|
||||
def _trigger_revocation_incident(self, event: IntegrationEvent) -> bool:
|
||||
"""Trigger incident for agent revocation"""
|
||||
revocation = event.data.get("revocation", {})
|
||||
agent_id = event.source
|
||||
|
||||
incident = self._build_incident(
|
||||
event_action="trigger",
|
||||
dedup_key=f"revocation-{agent_id}",
|
||||
severity=IncidentSeverity.CRITICAL,
|
||||
summary=f"Agent Revoked: {agent_id}",
|
||||
component=f"agent-{agent_id}",
|
||||
group="governance",
|
||||
custom_details={
|
||||
"agent_id": agent_id,
|
||||
"reason": revocation.get("reason"),
|
||||
"previous_tier": revocation.get("previous_tier"),
|
||||
"revoked_at": datetime.utcnow().isoformat()
|
||||
}
|
||||
)
|
||||
|
||||
return self._send_incident(incident)
|
||||
|
||||
def _trigger_system_incident(self, event: IntegrationEvent) -> bool:
|
||||
"""Trigger incident for system failure"""
|
||||
failure = event.data.get("failure", {})
|
||||
|
||||
incident = self._build_incident(
|
||||
event_action="trigger",
|
||||
dedup_key=f"system-{failure.get('component', 'unknown')}",
|
||||
severity=IncidentSeverity.CRITICAL,
|
||||
summary=f"System Failure: {failure.get('component', 'Unknown')}",
|
||||
component=failure.get("component"),
|
||||
group="infrastructure",
|
||||
custom_details={
|
||||
"component": failure.get("component"),
|
||||
"error": failure.get("error"),
|
||||
"timestamp": datetime.utcnow().isoformat()
|
||||
}
|
||||
)
|
||||
|
||||
return self._send_incident(incident)
|
||||
|
||||
def _trigger_execution_incident(self, event: IntegrationEvent) -> bool:
|
||||
"""Trigger incident for execution failure"""
|
||||
result = event.data.get("result", {})
|
||||
agent_id = event.source
|
||||
|
||||
incident = self._build_incident(
|
||||
event_action="trigger",
|
||||
dedup_key=f"execution-{agent_id}-{result.get('task_id', 'unknown')}",
|
||||
severity=IncidentSeverity.ERROR,
|
||||
summary=f"Execution Failed: Agent {agent_id}",
|
||||
component=f"agent-{agent_id}",
|
||||
group="execution",
|
||||
custom_details={
|
||||
"agent_id": agent_id,
|
||||
"task_id": result.get("task_id"),
|
||||
"error": result.get("error"),
|
||||
"duration": result.get("duration")
|
||||
}
|
||||
)
|
||||
|
||||
return self._send_incident(incident)
|
||||
|
||||
def _acknowledge_incident(self, event: IntegrationEvent) -> bool:
|
||||
"""Acknowledge an existing incident"""
|
||||
incident_key = event.data.get("incident_key")
|
||||
if not incident_key:
|
||||
return True
|
||||
|
||||
incident = self._build_incident(
|
||||
event_action="acknowledge",
|
||||
dedup_key=incident_key,
|
||||
severity=IncidentSeverity.INFO,
|
||||
summary=f"Remediation in progress"
|
||||
)
|
||||
|
||||
return self._send_incident(incident)
|
||||
|
||||
def _resolve_incident(self, event: IntegrationEvent) -> bool:
|
||||
"""Resolve an existing incident"""
|
||||
incident_key = event.data.get("incident_key")
|
||||
if not incident_key:
|
||||
return True
|
||||
|
||||
incident = self._build_incident(
|
||||
event_action="resolve",
|
||||
dedup_key=incident_key,
|
||||
severity=IncidentSeverity.INFO,
|
||||
summary=f"Issue resolved"
|
||||
)
|
||||
|
||||
return self._send_incident(incident)
|
||||
|
||||
def _trigger_generic_incident(self, event: IntegrationEvent) -> bool:
|
||||
"""Trigger a generic incident for critical events"""
|
||||
incident = self._build_incident(
|
||||
event_action="trigger",
|
||||
dedup_key=f"generic-{event.event_type}-{self._hash_data(event.data)}",
|
||||
severity=IncidentSeverity.ERROR,
|
||||
summary=f"Critical Event: {event.event_type}",
|
||||
component=event.source,
|
||||
custom_details=event.data
|
||||
)
|
||||
|
||||
return self._send_incident(incident)
|
||||
|
||||
# === Incident Building Helpers ===
|
||||
|
||||
def _build_incident(
|
||||
self,
|
||||
event_action: str,
|
||||
dedup_key: str,
|
||||
severity: IncidentSeverity,
|
||||
summary: str,
|
||||
component: str = None,
|
||||
group: str = None,
|
||||
custom_details: Dict = None,
|
||||
links: List[Dict] = None,
|
||||
images: List[Dict] = None
|
||||
) -> PagerDutyIncident:
|
||||
"""Build a PagerDuty incident"""
|
||||
return PagerDutyIncident(
|
||||
routing_key=self.config.extra.get("routing_key", ""),
|
||||
event_action=event_action,
|
||||
dedup_key=dedup_key,
|
||||
severity=severity,
|
||||
summary=summary,
|
||||
source=self.config.extra.get("source", "agent-governance"),
|
||||
component=component,
|
||||
group=group,
|
||||
custom_details=custom_details,
|
||||
links=links,
|
||||
images=images
|
||||
)
|
||||
|
||||
def _map_severity(self, violation_severity: str) -> IncidentSeverity:
|
||||
"""Map violation severity to PagerDuty severity"""
|
||||
mapping = {
|
||||
"critical": IncidentSeverity.CRITICAL,
|
||||
"high": IncidentSeverity.ERROR,
|
||||
"medium": IncidentSeverity.WARNING,
|
||||
"low": IncidentSeverity.INFO
|
||||
}
|
||||
return mapping.get(violation_severity.lower(), IncidentSeverity.ERROR)
|
||||
|
||||
def _hash_data(self, data: Dict) -> str:
|
||||
"""Create a short hash of data for dedup key"""
|
||||
content = json.dumps(data, sort_keys=True)
|
||||
return hashlib.md5(content.encode()).hexdigest()[:8]
|
||||
|
||||
def _send_incident(self, incident: PagerDutyIncident) -> bool:
|
||||
"""Send incident to PagerDuty"""
|
||||
if self._dry_run:
|
||||
self._audit("send_incident", True, {
|
||||
"dry_run": True,
|
||||
"action": incident.event_action,
|
||||
"dedup_key": incident.dedup_key,
|
||||
"severity": incident.severity.value
|
||||
})
|
||||
return True
|
||||
|
||||
if not self.enabled:
|
||||
return False
|
||||
|
||||
# Build Events API v2 payload
|
||||
payload = {
|
||||
"routing_key": incident.routing_key,
|
||||
"event_action": incident.event_action,
|
||||
"dedup_key": incident.dedup_key,
|
||||
"payload": {
|
||||
"summary": incident.summary,
|
||||
"severity": incident.severity.value,
|
||||
"source": incident.source,
|
||||
}
|
||||
}
|
||||
|
||||
if incident.component:
|
||||
payload["payload"]["component"] = incident.component
|
||||
if incident.group:
|
||||
payload["payload"]["group"] = incident.group
|
||||
if incident.custom_details:
|
||||
payload["payload"]["custom_details"] = incident.custom_details
|
||||
if incident.links:
|
||||
payload["links"] = incident.links
|
||||
if incident.images:
|
||||
payload["images"] = incident.images
|
||||
|
||||
# In production, would POST to Events API
|
||||
self._audit("send_incident", True, {
|
||||
"action": incident.event_action,
|
||||
"dedup_key": incident.dedup_key,
|
||||
"severity": incident.severity.value
|
||||
})
|
||||
return True
|
||||
|
||||
# === Convenience Methods ===
|
||||
|
||||
def trigger(
|
||||
self,
|
||||
summary: str,
|
||||
severity: IncidentSeverity = IncidentSeverity.ERROR,
|
||||
dedup_key: str = None,
|
||||
details: Dict = None
|
||||
) -> bool:
|
||||
"""Convenience method to trigger an incident"""
|
||||
if dedup_key is None:
|
||||
dedup_key = f"manual-{self._hash_data({'summary': summary, 'ts': datetime.utcnow().isoformat()})}"
|
||||
|
||||
incident = self._build_incident(
|
||||
event_action="trigger",
|
||||
dedup_key=dedup_key,
|
||||
severity=severity,
|
||||
summary=summary,
|
||||
custom_details=details
|
||||
)
|
||||
return self._send_incident(incident)
|
||||
|
||||
def acknowledge(self, dedup_key: str) -> bool:
|
||||
"""Convenience method to acknowledge an incident"""
|
||||
incident = self._build_incident(
|
||||
event_action="acknowledge",
|
||||
dedup_key=dedup_key,
|
||||
severity=IncidentSeverity.INFO,
|
||||
summary="Acknowledged"
|
||||
)
|
||||
return self._send_incident(incident)
|
||||
|
||||
def resolve(self, dedup_key: str) -> bool:
|
||||
"""Convenience method to resolve an incident"""
|
||||
incident = self._build_incident(
|
||||
event_action="resolve",
|
||||
dedup_key=dedup_key,
|
||||
severity=IncidentSeverity.INFO,
|
||||
summary="Resolved"
|
||||
)
|
||||
return self._send_incident(incident)
|
||||
@@ -0,0 +1,33 @@
|
||||
# Slack
|
||||
|
||||
> Slack integration module
|
||||
|
||||
## Overview
|
||||
|
||||
This directory contains slack integration module.
|
||||
|
||||
## Key Files
|
||||
|
||||
| File | Description |
|
||||
|------|-------------|
|
||||
| `slack.py` | Python module |
|
||||
| `__init__.py` | Python module |
|
||||
|
||||
## Interfaces / APIs
|
||||
|
||||
*Document any APIs, CLI commands, or interfaces here.*
|
||||
|
||||
## Status
|
||||
|
||||
** Not Started**
|
||||
|
||||
See [STATUS.md](./STATUS.md) for detailed progress tracking.
|
||||
|
||||
## Architecture Reference
|
||||
|
||||
Part of the [Agent Governance System](/opt/agent-governance/docs/ARCHITECTURE.md).
|
||||
|
||||
Parent: [Integrations](..)
|
||||
|
||||
---
|
||||
*Last updated: 2026-01-23 23:25:09 UTC*
|
||||
@@ -0,0 +1,35 @@
|
||||
# Status: Slack
|
||||
|
||||
## Current Phase
|
||||
|
||||
**COMPLETE**
|
||||
|
||||
## Tasks
|
||||
|
||||
| Status | Task | Updated |
|
||||
|--------|------|---------|
|
||||
| ✓ | Integration tests passing (17/17) | 2026-01-24 |
|
||||
|
||||
## Dependencies
|
||||
|
||||
- `integrations/common/base.py` (BaseIntegration)
|
||||
|
||||
## Issues / Blockers
|
||||
|
||||
*No current issues or blockers.*
|
||||
|
||||
## Activity Log
|
||||
|
||||
### 2026-01-24 02:37:00 UTC
|
||||
- **Phase**: COMPLETE
|
||||
- **Action**: Phase 8 integration tests verified
|
||||
- **Details**: All 17 Slack integration tests passing (plan_created, execution_started, execution_complete, violation_detected, promotion_requested, promotion_approved, agent_revoked, approval_required, heartbeat, generic handler)
|
||||
|
||||
### 2026-01-23 23:25:09 UTC
|
||||
- **Phase**: COMPLETE
|
||||
- **Action**: Initialized
|
||||
- **Details**: Status tracking initialized for this directory.
|
||||
|
||||
|
||||
---
|
||||
*Last updated: 2026-01-24 02:37:00 UTC*
|
||||
@@ -0,0 +1,3 @@
|
||||
from .slack import SlackIntegration, AlertSeverity, SlackMessage
|
||||
|
||||
__all__ = ['SlackIntegration', 'AlertSeverity', 'SlackMessage']
|
||||
@@ -0,0 +1,394 @@
|
||||
"""
|
||||
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
|
||||
Reference in New Issue
Block a user