""" Database Models =============== CTO-201: Approval & AuditLog persistence Schema 設計原則: - UUID 主鍵 (PostgreSQL 相容) - JSON 欄位儲存複雜結構 - 完整時間戳記 - 索引優化查詢 """ from datetime import UTC, datetime from typing import Any from uuid import uuid4 from sqlalchemy import ( JSON, DateTime, Index, Integer, String, Text, ) from sqlalchemy import ( Enum as SQLEnum, ) from sqlalchemy.orm import Mapped, mapped_column from src.db.base import Base from src.models.approval import ApprovalStatus, RiskLevel from src.models.incident import IncidentStatus, Severity # ============================================================================= # Helper Functions # ============================================================================= def utc_now() -> datetime: """Get current UTC datetime""" return datetime.now(UTC) def generate_uuid() -> str: """Generate UUID string""" return str(uuid4()) # ============================================================================= # ApprovalRecord - 授權記錄持久化 # ============================================================================= class ApprovalRecord(Base): """ 授權記錄 - 對應 Pydantic ApprovalRequest Note: 與 in-memory TrustEngine 的 ApprovalRequest 同步 """ __tablename__ = "approval_records" # Primary Key id: Mapped[str] = mapped_column( String(36), primary_key=True, default=generate_uuid, ) # Core Fields action: Mapped[str] = mapped_column(String(500), nullable=False) description: Mapped[str] = mapped_column(Text, nullable=False) status: Mapped[str] = mapped_column( SQLEnum(ApprovalStatus), default=ApprovalStatus.PENDING, nullable=False, ) risk_level: Mapped[str] = mapped_column( SQLEnum(RiskLevel), nullable=False, ) # Signature Tracking required_signatures: Mapped[int] = mapped_column(Integer, default=1) current_signatures: Mapped[int] = mapped_column(Integer, default=0) signatures: Mapped[dict[str, Any]] = mapped_column(JSON, default=list) # Blast Radius (JSON) blast_radius: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict) # Dry-Run Checks (JSON) dry_run_checks: Mapped[list[dict[str, Any]]] = mapped_column(JSON, default=list) # Metadata requested_by: Mapped[str] = mapped_column(String(100), nullable=False) rejection_reason: Mapped[str | None] = mapped_column(Text, nullable=True) extra_metadata: Mapped[dict[str, Any] | None] = mapped_column(JSON, nullable=True) # ========================================================================== # 戰略 B: 告警風暴收斂 (Alert Storm Convergence) # ========================================================================== # 告警指紋 - 根據 namespace + deployment + alert_name 產生的唯一 Hash fingerprint: Mapped[str | None] = mapped_column( String(64), nullable=True, index=True, comment="SHA256 hash of alert identity (namespace:deployment:alert_name)", ) # 聚合次數 - 相同指紋告警的累計觸發次數 hit_count: Mapped[int] = mapped_column( Integer, default=1, nullable=False, comment="Number of times this alert pattern was triggered", ) # 最後觸發時間 - 同指紋告警最近一次出現的時間 last_seen_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=utc_now, nullable=False, comment="Last time this alert pattern was seen", ) # Timestamps created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=utc_now, ) updated_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=utc_now, onupdate=utc_now, ) expires_at: Mapped[datetime | None] = mapped_column( DateTime(timezone=True), nullable=True, ) resolved_at: Mapped[datetime | None] = mapped_column( DateTime(timezone=True), nullable=True, ) # Indexes __table_args__ = ( Index("ix_approval_status", "status"), Index("ix_approval_risk_level", "risk_level"), Index("ix_approval_created_at", "created_at"), Index("ix_approval_requested_by", "requested_by"), Index("ix_approval_fingerprint", "fingerprint"), # 戰略 B: 指紋查詢優化 ) # ============================================================================= # AuditLog - 稽核日誌 # ============================================================================= class TimelineEvent(Base): """ 時間軸事件 - Phase 4 Action Timeline 事件類型: - system: 系統告警接收 - agent: OpenClaw AI 分析 - security: 權限阻擋 - human: 人類授權 - exec: 執行完成 """ __tablename__ = "timeline_events" # Primary Key id: Mapped[str] = mapped_column( String(36), primary_key=True, default=generate_uuid, ) # Event Type & Status event_type: Mapped[str] = mapped_column( String(20), nullable=False, comment="system, agent, security, human, exec", ) status: Mapped[str] = mapped_column( String(20), nullable=False, default="info", comment="info, success, warning, error", ) # Content title: Mapped[str] = mapped_column(String(500), nullable=False) description: Mapped[str | None] = mapped_column(Text, nullable=True) # Actor actor: Mapped[str | None] = mapped_column(String(100), nullable=True) actor_role: Mapped[str | None] = mapped_column(String(50), nullable=True) # Context risk_level: Mapped[str | None] = mapped_column(String(20), nullable=True) approval_id: Mapped[str | None] = mapped_column(String(36), nullable=True, index=True) # Timestamp created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=utc_now, ) # Indexes __table_args__ = ( Index("ix_timeline_event_type", "event_type"), Index("ix_timeline_created_at", "created_at"), ) class AuditLog(Base): """ 稽核日誌 - 記錄所有執行結果 每次 K8s 操作完成後寫入一筆記錄 """ __tablename__ = "audit_logs" # Primary Key id: Mapped[str] = mapped_column( String(36), primary_key=True, default=generate_uuid, ) # Reference to Approval approval_id: Mapped[str] = mapped_column( String(36), nullable=False, index=True, ) # Operation Details operation_type: Mapped[str] = mapped_column( String(50), nullable=False, comment="e.g., RESTART_DEPLOYMENT, DELETE_POD", ) target_resource: Mapped[str] = mapped_column( String(200), nullable=False, comment="e.g., deployment/api-backend, pod/nginx-xxx", ) namespace: Mapped[str] = mapped_column( String(63), default="default", nullable=False, ) # Execution Result success: Mapped[bool] = mapped_column(default=False, nullable=False) error_message: Mapped[str | None] = mapped_column(Text, nullable=True) # K8s Response (Raw) k8s_response: Mapped[dict[str, Any] | None] = mapped_column( JSON, nullable=True, comment="Raw Kubernetes API response", ) # Execution Context executed_by: Mapped[str] = mapped_column( String(100), nullable=False, comment="Who triggered the execution", ) execution_duration_ms: Mapped[int | None] = mapped_column( Integer, nullable=True, comment="Execution time in milliseconds", ) # Dry-Run Result (pre-execution validation) dry_run_passed: Mapped[bool] = mapped_column( default=True, nullable=False, ) dry_run_message: Mapped[str | None] = mapped_column(Text, nullable=True) # Timestamps created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=utc_now, ) # Indexes __table_args__ = ( Index("ix_audit_approval_id", "approval_id"), Index("ix_audit_operation_type", "operation_type"), Index("ix_audit_success", "success"), Index("ix_audit_created_at", "created_at"), ) # ============================================================================= # IncidentRecord - Phase 6.2 Episodic Memory (PostgreSQL) # ============================================================================= class IncidentRecord(Base): """ 事件記錄 - 對應 Pydantic Incident Schema v0.3 Phase 6.2: Episodic Memory (長期記憶) - 從 Working Memory (Redis) 遷移過來 - 永久保留,供 RAG 檢索 - 複雜結構使用 JSONB 欄位 三層記憶架構: - Working Memory (Redis): 7 天 TTL - Episodic Memory (PostgreSQL): 此表,永久保留 - Semantic Memory (Vector DB): Phase 6.3+ """ __tablename__ = "incidents" # === 主鍵 === incident_id: Mapped[str] = mapped_column( String(30), primary_key=True, comment="事件唯一識別碼 (如 INC-20260322-A1B2C3)", ) # === 狀態與嚴重度 === status: Mapped[str] = mapped_column( SQLEnum(IncidentStatus), default=IncidentStatus.INVESTIGATING, nullable=False, comment="事件狀態 (investigating, mitigating, resolved, closed, escalated)", ) severity: Mapped[str] = mapped_column( SQLEnum(Severity), nullable=False, comment="事件嚴重度 (P0, P1, P2, P3)", ) # === 感知層 (Signals) - JSONB === signals: Mapped[list[dict[str, Any]]] = mapped_column( JSON, default=list, nullable=False, comment="關聯的告警信號列表 (JSONB)", ) affected_services: Mapped[list[str]] = mapped_column( JSON, default=list, nullable=False, comment="受影響的服務列表", ) # === 認知層 (AI Decision Chain) - JSONB === decision_chain: Mapped[dict[str, Any] | None] = mapped_column( JSON, nullable=True, comment="AI 決策鏈 (完整推論過程)", ) # === 決策層 (Proposals) === proposal_ids: Mapped[list[str]] = mapped_column( JSON, default=list, nullable=False, comment="關聯的 ApprovalRequest ID 列表", ) # === 結果層 (Outcome) - JSONB === outcome: Mapped[dict[str, Any] | None] = mapped_column( JSON, nullable=True, comment="事件結果與人類回饋", ) # === 時間軸 === created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=utc_now, nullable=False, ) updated_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=utc_now, onupdate=utc_now, nullable=False, ) resolved_at: Mapped[datetime | None] = mapped_column( DateTime(timezone=True), nullable=True, ) closed_at: Mapped[datetime | None] = mapped_column( DateTime(timezone=True), nullable=True, ) # === 記憶管理 === ttl_days: Mapped[int] = mapped_column( Integer, default=7, nullable=False, comment="Working Memory TTL (天)", ) vectorized: Mapped[bool] = mapped_column( default=False, nullable=False, comment="是否已向量化到 Vector DB (Semantic Memory)", ) # === 索引 === __table_args__ = ( Index("ix_incident_status", "status"), Index("ix_incident_severity", "severity"), Index("ix_incident_created_at", "created_at"), Index("ix_incident_resolved_at", "resolved_at"), )