"""Tool-Definitionen, Einstufung, Workspace-Verschärfungen und Tool-Aufrufe (→ Konzept 6.1, 10).""" import uuid from datetime import datetime from typing import Any from sqlalchemy import JSON, Boolean, ForeignKey, Index, Integer, String, Text, Uuid, false from sqlalchemy.orm import Mapped, mapped_column from app.core.database import Base, now RISK_CLASSES = ("read", "write_internal", "write_external", "destructive") ALLOW, REQUIRE_APPROVAL, DENY = "allow", "require_approval", "deny" # Reihenfolge = Strenge; Workspaces dürfen nur nach rechts verschieben DECISIONS = (ALLOW, REQUIRE_APPROVAL, DENY) # Wer freigeben darf (→ Konzept 5.3, 10.5); Reihenfolge = Strenge WORKSPACE_ADMIN, GLOBAL_APPROVER = "workspace_admin", "global_approver" LEVELS = (WORKSPACE_ADMIN, GLOBAL_APPROVER) class ToolDefinition(Base): """Bekanntes Tool mit Hash über Name, Beschreibung und Schema (→ Konzept 7.2). Gilt nur als eingestuft, solange classified_hash dem aktuellen Hash entspricht. Ändert sich die Definition, ist das Tool wieder DENY, bis ein Admin neu einstuft. """ __tablename__ = "tool_definitions" id: Mapped[int] = mapped_column(primary_key=True) name: Mapped[str] = mapped_column(String(100), unique=True) kind: Mapped[str] = mapped_column(String(20)) # Art der Quelle, z. B. git description: Mapped[str] = mapped_column(Text) schema: Mapped[dict[str, Any]] = mapped_column(JSON) hash: Mapped[str] = mapped_column(String(64)) # MCP-Tools: gleicher Fingerabdruck = gleiches Tool desselben Dienstes; die Einstufung gilt für alle fingerprint: Mapped[str | None] = mapped_column(String(64), index=True) # Einstufung von einem gleichen Tool übernommen (dessen Name) inherited_from: Mapped[str | None] = mapped_column(String(100)) suggested_risk: Mapped[str] = mapped_column(String(20)) # Einstufung durch den Plattform-Admin risk_class: Mapped[str | None] = mapped_column(String(20)) decision: Mapped[str | None] = mapped_column(String(20)) classified_hash: Mapped[str | None] = mapped_column(String(64)) # Freigabestufe und Vier-Augen-Prinzip für Anträge dieses Tools approval_level: Mapped[str] = mapped_column(String(20), default=WORKSPACE_ADMIN, server_default=WORKSPACE_ADMIN) four_eyes: Mapped[bool] = mapped_column(Boolean, default=False, server_default=false()) classified_by: Mapped[int | None] = mapped_column(ForeignKey("users.id")) classified_at: Mapped[datetime | None] updated: Mapped[datetime] = mapped_column(default=now) @property def classified(self) -> bool: return self.classified_hash == self.hash and self.decision is not None class WorkspaceToolPolicy(Base): """Verschärfung je Workspace; wird beim Auswerten nie lockernd angewendet.""" __tablename__ = "workspace_tool_policies" workspace_id: Mapped[int] = mapped_column(ForeignKey("workspaces.id", ondelete="CASCADE"), primary_key=True) tool_name: Mapped[str] = mapped_column(String(100), primary_key=True) # Leer = Entscheidung wie global (z. B. nur Vier-Augen-Prinzip verschärft) decision: Mapped[str] = mapped_column(String(20)) four_eyes: Mapped[bool] = mapped_column(Boolean, default=False, server_default=false()) updated_by: Mapped[int] = mapped_column(ForeignKey("users.id")) updated: Mapped[datetime] = mapped_column(default=now) class SourceToolPolicy(Base): """Verschärfung für ein Tool auf einer bestimmten Quelle, z. B. commit_change auf dem Produktiv-Repository nur mit Freigabe. Wie Workspace-Vorgaben nie lockernd.""" __tablename__ = "source_tool_policies" source_id: Mapped[int] = mapped_column(ForeignKey("data_sources.id", ondelete="CASCADE"), primary_key=True) tool_name: Mapped[str] = mapped_column(String(100), primary_key=True) workspace_id: Mapped[int] = mapped_column(ForeignKey("workspaces.id", ondelete="CASCADE"), index=True) decision: Mapped[str] = mapped_column(String(20)) four_eyes: Mapped[bool] = mapped_column(Boolean, default=False) updated_by: Mapped[int] = mapped_column(ForeignKey("users.id")) updated: Mapped[datetime] = mapped_column(default=now) class ToolCall(Base): """Jeder Tool-Aufruf eines Runs, auch abgewiesene (→ Konzept 8.3, 13).""" __tablename__ = "tool_calls" __table_args__ = (Index("ix_tool_calls_run_id", "run_id"),) id: Mapped[int] = mapped_column(primary_key=True) run_id: Mapped[uuid.UUID] = mapped_column(Uuid, ForeignKey("task_runs.id", ondelete="CASCADE")) workspace_id: Mapped[int] = mapped_column(ForeignKey("workspaces.id", ondelete="CASCADE")) call_id: Mapped[str] = mapped_column(String(200)) tool: Mapped[str] = mapped_column(String(200)) arguments: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict) decision: Mapped[str] = mapped_column(String(20)) status: Mapped[str] = mapped_column(String(20)) # ok | error | denied # Gekürzt; das vollständige Ergebnis steht im Verlauf der Nachricht output: Mapped[str] = mapped_column(Text, default="") # Ergebnistabelle lesender Abfragen (Spalten, Zeilen) für Anzeige, CSV und Diagramm result: Mapped[dict[str, Any] | None] = mapped_column(JSON(none_as_null=True)) duration_ms: Mapped[int] = mapped_column(Integer, default=0) created: Mapped[datetime] = mapped_column(default=now)