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:
profit
2026-01-24 18:39:47 -05:00
co-authored by Claude Opus 4.5
parent 4b5b3b0a2d
commit 8c6e7831e9
184 changed files with 16249 additions and 13673 deletions
+33
View File
@@ -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*
+35
View File
@@ -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*
+3
View File
@@ -0,0 +1,3 @@
from .github import GitHubIntegration, PullRequest, CommitStatus
__all__ = ['GitHubIntegration', 'PullRequest', 'CommitStatus']
+342
View File
@@ -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 []
+33
View File
@@ -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*
+42
View File
@@ -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)
+33
View File
@@ -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*
+35
View File
@@ -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*
+3
View File
@@ -0,0 +1,3 @@
from .slack import SlackIntegration, AlertSeverity, SlackMessage
__all__ = ['SlackIntegration', 'AlertSeverity', 'SlackMessage']
+394
View File
@@ -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