67 lines
3.7 KiB
Python
67 lines
3.7 KiB
Python
"""Freigabeanträge (→ Konzept 10.4, 10.5, 9.2).
|
|
|
|
Ein Antrag bezieht sich auf genau einen Tool-Aufruf. Parameter, Vorschau (Diff)
|
|
und Vorbedingungen sind ein unveränderlicher Snapshot; ausgeführt wird nur,
|
|
was zum gespeicherten Hash passt.
|
|
"""
|
|
|
|
import uuid
|
|
from datetime import datetime
|
|
from typing import Any
|
|
|
|
from sqlalchemy import JSON, Boolean, ForeignKey, Index, String, Text, Uuid, false, text
|
|
from sqlalchemy.orm import Mapped, mapped_column
|
|
|
|
from app.core.database import Base, now
|
|
|
|
# pending → approved → executing → executed | failed
|
|
# pending → rejected | expired | cancelled (Run abgebrochen)
|
|
# executing → unknown: Ausgang unklar, Run wechselt nach needs_reconciliation (→ Konzept 9.2)
|
|
PENDING, APPROVED, REJECTED, EXPIRED, CANCELLED = "pending", "approved", "rejected", "expired", "cancelled"
|
|
EXECUTING, EXECUTED, FAILED, UNKNOWN = "executing", "executed", "failed", "unknown"
|
|
NOT_EXECUTED = (REJECTED, EXPIRED, CANCELLED)
|
|
LABELS = {PENDING: "offen", APPROVED: "genehmigt", REJECTED: "abgelehnt", EXPIRED: "abgelaufen",
|
|
CANCELLED: "zurückgezogen", EXECUTING: "wird ausgeführt", EXECUTED: "ausgeführt", FAILED: "fehlgeschlagen",
|
|
UNKNOWN: "Ausgang unklar"}
|
|
|
|
|
|
class ApprovalRequest(Base):
|
|
__tablename__ = "approval_requests"
|
|
__table_args__ = (
|
|
Index("ix_approval_requests_pending", "workspace_id", postgresql_where=text("status = 'pending'")),
|
|
Index("ix_approval_requests_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"))
|
|
source_id: Mapped[int | None] = mapped_column(ForeignKey("data_sources.id", ondelete="SET NULL"))
|
|
call_id: Mapped[str] = mapped_column(String(200))
|
|
tool: Mapped[str] = mapped_column(String(100))
|
|
risk_class: Mapped[str] = mapped_column(String(20))
|
|
reason: Mapped[str] = mapped_column(String(200)) # warum Freigabe nötig (Policy)
|
|
# Snapshot: geprüfte Argumente, echte Vorschau, Vorbedingungen im Zielsystem
|
|
arguments: Mapped[dict[str, Any]] = mapped_column(JSON)
|
|
preview: Mapped[str] = mapped_column(Text)
|
|
preconditions: Mapped[dict[str, Any]] = mapped_column(JSON)
|
|
# Vom Modell formulierte Beschreibung, nur als gekennzeichneter Zusatz angezeigt
|
|
model_summary: Mapped[str] = mapped_column(Text, default="")
|
|
# Policy zum Zeitpunkt des Antrags: Tool-Hash, Risikoklasse, Entscheidung, Modus (→ Konzept 10.4)
|
|
policy: Mapped[dict[str, Any]] = mapped_column(JSON)
|
|
hash: Mapped[str] = mapped_column(String(64))
|
|
status: Mapped[str] = mapped_column(String(20), default=PENDING)
|
|
requested_by: Mapped[int] = mapped_column(ForeignKey("users.id"))
|
|
requested_at: Mapped[datetime] = mapped_column(default=now)
|
|
expires_at: Mapped[datetime]
|
|
decided_by: Mapped[int | None] = mapped_column(ForeignKey("users.id"))
|
|
decided_at: Mapped[datetime | None]
|
|
comment: Mapped[str] = mapped_column(Text, default="")
|
|
self_approved: Mapped[bool] = mapped_column(Boolean, default=False)
|
|
# Wer entscheiden darf (→ Konzept 10.5): Workspace-Admins oder globale Approver; mit four_eyes
|
|
# nie die Person, die den Antrag gestellt hat
|
|
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())
|
|
# Ergebnis der Ausführung (Branch, Commit, Pull Request) oder Fehlermeldung
|
|
result: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict)
|
|
# Erneut beantragt aus einem abgelaufenen Antrag
|
|
replaces_id: Mapped[int | None] = mapped_column(ForeignKey("approval_requests.id"))
|