Files
Onyscribe-Admin/app/agent/workspace.py
RubenRWU 7d8c226da2 Onyscribe Admin: Initialer Stand
Admin-Plattform für die Tenants von Onyscribe: Editor mit Live-Vorschau (echter Core in Sandbox), Versionen, KI-Agent (Cortecs),
OIDC-Abgleich über die Login-Developer-API, Deploy-Engine (Mittwald, gesperrt ohne DEPLOY_LIVE, Freigabe pro Tenant),
Core-Vertrag (Schema/Manifest), Core-Releases mit Kompatibilitätsprüfung, Canary-Rollout und Rollback, Zustandsabgleich.
Runbook: docs/RUNBOOK_PARALLELTEST.md

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-15 15:30:26 +02:00

107 lines
4.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Entwurfs-Workspace des Agenten: Kopie des Tenant-Entwurfs im Speicher.
Der Agent ändert NUR diese Kopie. Daraus entsteht ein Vorschlag (Diff), den der Mensch freigibt.
Datenschutz: Dateien mit personenbezogenem Inhalt (Namenslisten) sind für den Agenten nicht lesbar.
"""
from __future__ import annotations
import json
from copy import deepcopy
from sqlalchemy.orm import Session
from app.models import Tenant
from app.services import store
PERSONAL_PREFIXES = ("namensliste", "mitglieder", "members", "personen")
READ_LIMIT = 60_000
PROTECTED = {"tenant.json"}
def is_personal(path: str) -> bool:
name = path.rsplit("/", 1)[-1].lower()
return name.startswith(PERSONAL_PREFIXES)
class Workspace:
def __init__(self, db: Session, tenant: Tenant):
self.db, self.tenant = db, tenant
cfg, files = store.content(db, tenant, "draft")
self.base_cfg, self.base_files = deepcopy(cfg), dict(files)
self.cfg, self.files = deepcopy(cfg), dict(files) # files: {pfad: sha}
# ── Lesen ──
def list_files(self) -> list[dict]:
out = []
for p, sha in sorted(self.files.items()):
b = self.db.get(__import__("app.models", fromlist=["Blob"]).Blob, sha)
out.append({"path": p, "bytes": b.size, "text": store.is_text_path(p), "personal_data": is_personal(p),
"changed": self.base_files.get(p) != sha})
return out
def read(self, path: str) -> str:
if path in ("tenant.json",):
return json.dumps(self.cfg, indent=2, ensure_ascii=False)
if path not in self.files:
raise KeyError(f"Datei nicht im Tenant: {path}")
if is_personal(path):
n = len(store.get_blob(self.db, self.files[path]).decode("utf-8", "replace").splitlines())
return f"[Datei enthält personenbezogene Daten und ist für den Agenten gesperrt – {n} Zeilen. Inhalt nicht verfügbar.]"
if not store.is_text_path(path):
return "[Binärdatei]"
text = store.get_blob(self.db, self.files[path]).decode("utf-8", "replace")
return text[:READ_LIMIT] + ("\n[… gekürzt]" if len(text) > READ_LIMIT else "")
# ── Schreiben ──
def write(self, path: str, content: str) -> None:
path = store.clean_path(path)
if is_personal(path):
raise PermissionError("Dateien mit personenbezogenen Daten darf der Agent nicht ändern.")
if path.endswith(".py"):
compile(content, path, "exec") # Syntaxfehler früh melden
self.files[path] = store.put_blob(self.db, content.encode("utf-8"), path)
def edit(self, path: str, old: str, new: str) -> None:
cur = self.read(path) if path != "tenant.json" else json.dumps(self.cfg, indent=2, ensure_ascii=False)
if cur.count(old) != 1:
raise ValueError(f"old_str muss genau 1× vorkommen (gefunden: {cur.count(old)}×).")
if path == "tenant.json":
self.set_config_json(cur.replace(old, new))
else:
self.write(path, cur.replace(old, new))
def delete(self, path: str) -> None:
if is_personal(path):
raise PermissionError("Dateien mit personenbezogenen Daten darf der Agent nicht löschen.")
self.files.pop(path, None)
def set_config_json(self, text: str) -> None:
cfg = json.loads(text)
cfg.pop("deploy", None)
cfg["id"] = self.tenant.id
self.cfg = cfg
def set_config_value(self, key_path: str, value) -> None:
cur = self.cfg
keys = key_path.split(".")
for k in keys[:-1]:
cur = cur.setdefault(k, {})
cur[keys[-1]] = value
# ── Ergebnis ──
def changed(self) -> bool:
return self.cfg != self.base_cfg or self.files != self.base_files
def proposal(self, summary: str) -> dict | None:
if not self.changed():
return None
files = {p: s for p, s in self.files.items() if self.base_files.get(p) != s}
deleted = [p for p in self.base_files if p not in self.files]
items = []
if self.cfg != self.base_cfg:
items.append({"path": "tenant.json", "kind": "geändert"})
items += [{"path": p, "kind": "neu" if p not in self.base_files else "geändert"} for p in files]
items += [{"path": p, "kind": "gelöscht"} for p in deleted]
return {"summary": summary, "config": self.cfg if self.cfg != self.base_cfg else None,
"files": files, "deleted": deleted, "items": items}