105 lines
5.2 KiB
Python
105 lines
5.2 KiB
Python
"""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)
|