Fuenf von sechs Befunden einer externen Integration. Der sechste - unsignierte
Lizenzurteile - ist bestaetigt, aber bewusst nicht Teil dieses Commits.
1. Lizenzschluessel stand in der Kommandozeile
Der Agent nahm --license-key nur als Argument und las keine
Umgebungsvariablen. "ps" zeigt argv jedem Benutzer der Maschine - exakt die
Begruendung, mit der UPGRADE.md §5 den Crontab-Weg verwirft. Damit nahm das
SDK einen bereits geloesten Sicherheitsbefund zurueck.
Der Agent liest jetzt DC_LICENSE_KEY, DC_DOWNLOAD_USER und
DC_DOWNLOAD_PASSWORD, Umgebung vor Argument. LaunchUpdateAgent uebergibt
den Schluessel nicht mehr als Argument, sondern setzt die Variable auf dem
eigenen Prozess: das Kind erbt den Umgebungsblock, danach wird sie wieder
entfernt. Das funktioniert auch mit UseShellExecute=true, wo sich
ProcessStartInfo.Environment nicht setzen laesst.
2. --wait-timeout war nicht durchgereicht
Der Agent kannte den Parameter, LaunchUpdateAgent hatte keinen dafuer - es
galten fest 60 Sekunden. Eine Anwendung, die allein fuer host.StopAsync 30
Sekunden braucht, kommt damit gefaehrlich nah an die Grenze. Neu:
waitTimeoutSeconds. Ausserdem ist im Quelltext und in der Doku jetzt
festgehalten, dass exitCurrentApp:true ueber Environment.Exit(0) laeuft und
damit finally-Bloecke und IHostApplicationLifetime uebergeht - bei offenem
Zustand die falsche Wahl.
3. ILicensePrompt war tot
Der Konstruktor nahm es entgegen, legte es in _prompt ab und benutzte es
nirgends. Wer darauf eine headless-Story aufbaute, baute auf Sand. Neu:
EnsureLicensedAsync() - zwischengespeicherten Schluessel nehmen, sonst
fragen, pruefen, bei Ablehnung erneut fragen. allowPrompt:false lehnt ohne
Cache ab, statt auf eine Eingabe zu warten, die im Dienst nie kommt. Ein
voruebergehender Netzfehler fuehrt nicht zur erneuten Abfrage - der
Schluessel ist ja nicht falsch.
4. Der Packager warnte nur
Er bricht jetzt ab. Anlass war ein echter API-Schluessel in einem
oeffentlich abrufbaren Paket - und die Warnung war damals ausgerechnet
unterdrueckt, weil die Datei auf der preserve-Liste stand.
Zwei Stufen: Dateiname (appsettings.Local.json, master.key, *.pfx, *.db,
server_settings.xml) und Inhalt (gefuelltes Password=, sk-, ghp_,
dc_master_, AKIA, private Schluessel). Die Inhaltspruefung findet auch
Dateien mit unverdaechtigem Namen.
Platzhalter loesen bewusst nicht aus: "sk-DEIN-SCHLUESSEL-HIER" haette
sonst jede ausgelieferte Vorlage blockiert, und --allow-secrets waere nach
einer Woche Gewohnheit. Beim Erproben zuerst genau in diese Falle gelaufen.
5. BuildInfo.targets war nur per Pfad-Import zu haben
Die Anleitung empfahl einen <Import> ins Nachbar-Repository - das setzt
voraus, dass beide Arbeitskopien nebeneinander liegen und in derselben
Fassung stehen. Das Client-Projekt ist jetzt packbar und legt das Target
unter build/ ins Paket, wo NuGet es selbst importiert.
Ausserdem: Unauthorized wurde nur im statischen Zweig erkannt, im API-Zweig
kam ein 401 als gewoehnlicher HTTP-Fehler an.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Die Anleitung benutzte pack-and-deploy, als laege es im PATH - beziehbar war
es nirgends. Ein Projekt, das den UpdateService einbindet, konnte also nicht
veroeffentlichen, ohne dieses Repository auszuchecken und selbst zu
uebersetzen. Das Werkzeug existierte, nur kam niemand daran.
- build_installer.ps1 baut pack-and-deploy fuer dieselben Laufzeitkennungen
mit und fuehrt es in installer.json unter "tools". Damit steht es neben dem
Agenten unter /installer/ bereit.
- Neue Vorlage unter public/docs/release-template/: release.ps1, release.sh
und release.config.example.json. Kopieren, Konfiguration ausfuellen, fertig -
die Skripte selbst bleiben unveraendert und lassen sich bei einer neuen
Fassung einfach ersetzen.
- Sie orchestrieren nur: je Laufzeitkennung einmal dotnet publish, dann
pack-and-deploy. Pruefsummen, Dateimanifest, latest.json und die Anmeldung
bleiben im Werkzeug - ein zweiter Ort fuer dieselbe Logik waere ein zweiter
Ort fuer dieselben Fehler.
- Das Werkzeug wird beim ersten Lauf selbst geholt, gegen die .sha256 geprueft
und unter .dc-tools/ abgelegt. Die Vorlage ist damit wirklich eine Datei.
- setup.json wird ins Publish-Verzeichnis kopiert, sonst faende der Installer
sie nicht.
- Rueckgabewert 1 (Konfigurations- oder Versionsfehler) bricht sofort ab; die
weiteren Plattformen wuerden genauso scheitern. Bei 2 laeuft es weiter und
meldet am Ende, welche betroffen sind.
Anleitung: public/docs/release.md, oeffentlich unter /docs/release.md - dort,
wo auch das Bugtracker-Handbuch liegt. Das Entwickler-docs/ wird nicht
ausgeliefert; ein erster Anlauf legte die Vorlage dort ab und war deshalb
nicht abrufbar.
Beim Erproben in einem leeren Projekt aufgefallen und behoben:
- Die Vorlage verlangte jq. Das ist auf den wenigsten Systemen vorinstalliert;
sie kommt jetzt auch mit Python aus.
- Windows legt unter WindowsApps einen python3-Platzhalter ab, der gefunden
wird, beim Aufruf aber nur auf den Store verweist. Die Erkennung erprobt den
Interpreter deshalb, statt nur seine Existenz zu pruefen.
- Der Ternary-Operator in release.ps1 gibt es erst ab PowerShell 7; die
Vorlage laeuft jetzt auch mit dem mitgelieferten 5.1.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Zwei Beobachtungen aus der laufenden Integration.
1. Der Installer schrieb Geheimnisse weltlesbar
SetupWriter benutzte File.WriteAllText ohne Rechteanpassung - unter Linux
also die Standardmaske und damit ueblicherweise 644. In genau dieser Datei
stehen Lizenzschluessel und Anwendungstoken; jeder Benutzer des Systems
konnte sie lesen. Der Lizenz-Cache in StateStore wird aus demselben Grund
seit jeher auf 600 gesetzt - der Installer zog nicht nach.
Enthaelt ein Ziel mindestens einen geheimen Wert, wird die Datei jetzt auf
den eigenen Benutzer beschraenkt. Als geheim gilt type=secret UND
source=provision: ein so geholtes Token traegt oft den Typ "string", ist
aber genauso schutzbeduerftig. Unter Windows bleibt es beim Profil-ACL.
2. Zielnamen koennen plattformabhaengig unterschiedlich sein
%APPDATA%\MeineAnwendung gegen $XDG_CONFIG_HOME/meineanwendung - eine
setup.json kannte nur eine Schreibweise. Die kleingeschriebene Form allein
traegt, weil NTFS die Schreibweise ignoriert, aber nur solange das
Dateisystem tatsaechlich unempfindlich ist; auf APFS mit Beachtung der
Schreibweise oder bei groesseren Unterschieden entstuende ein zweites,
leeres Verzeichnis neben dem, aus dem die Anwendung liest.
Ziele haben deshalb optional fileWindows, fileLinux und fileMacOS; ohne
Angabe gilt weiterhin file.
Die Dokumentation haelt ausserdem fest, dass der Installer bewusst Klartext
schreibt und die Anwendung selbst entscheidet, ob und wie sie ihn danach
schuetzt - und dass eine Entschluesselung, die Klartext durchreicht, deshalb
kein Altlast-Zweig mehr ist, sondern ein aktiv genutzter Pfad.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Vier Befunde aus einer externen Durchsicht der 2.4-Integration.
1. Die .htpasswd war eine Klartext-Kundenliste
Das htpasswd-Format hasht nur die Passwortspalte. Benutzername UND Passwort
waren der Lizenzschluessel - der Schluessel stand also im Klartext direkt
neben seinem eigenen bcrypt-Hash, und der Hash war Dekoration. Geschuetzt
hat das Ganze nur die FilesMatch-Regel in derselben Datei.
Der Benutzername wird jetzt abgeleitet: lic_<sha256(schluessel), 16 Hex>.
Die Datei enthaelt damit nur noch eine Einwegableitung und einen Hash ueber
einen hochentropen Schluessel.
Server und SDK muessen dabei zeichengenau uebereinstimmen; ein Test prueft
die C#-Ableitung gegen die PHP-Formel.
2. Ein Formatwechsel blieb unbemerkt liegen
Beim Umbau auf 1. faellt auf: reconcile() sah keinen Anlass zur
Neuerzeugung, die Dateien behielten das alte Format, waehrend die Clients
bereits das neue schickten. Die erzeugten Dateien tragen deshalb jetzt eine
Formatkennung; weicht sie ab, wird neu erzeugt.
3. Doku beschrieb Nginx, der Schutz ist Apache-only
.htaccess wird von Nginx ignoriert - dort waeren die Verzeichnisse offen und
die .htpasswd oeffentlich abrufbar. Die Statusanzeige pruefte nur, ob die
Dateien existieren, und haette in dem Fall "GESCHUETZT" gemeldet.
Neu: ein echter Selbsttest ruft die eigene Paket-Adresse OHNE Zugangsdaten
ab und erwartet 401. Er laeuft beim manuellen Erzeugen und nach jeder
automatischen Neuerzeugung; das Ergebnis steht in der Oberflaeche, ein
Fehlschlag im Log. Er findet nebenbei auch abgeschaltetes AllowOverride und
Tippfehler in der erzeugten Datei. Doku korrigiert, Nginx-Vorlage ergaenzt.
4. Erstinstallation schrieb an einen Ort, an dem Linux-Anwendungen nicht lesen
setup.json-Ziele waren immer installationsrelativ. Eine Anwendung, die sich
unter Linux richtig verhaelt, liest aus $XDG_CONFIG_HOME - /opt/<app> ist
fuer den Dienstbenutzer meist nicht schreibbar. Der Installer legte die
Datei also dorthin, wo nie jemand nachsieht.
Ziele haben jetzt ein "location": install (Vorgabe), config, data, home,
plus ${VAR}- und %VAR%-Ersetzung in "file". Unbekannte Variablen bleiben
stehen statt leer zu werden - ein Platzhalter faellt auf, ein falscher Pfad
nicht. Der Installer gibt den aufgeloesten Pfad aus, weil bei config das
Konto entscheidet, unter dem er laeuft.
Ausserdem
- Doku zeigte "status": "ok" fuer update/delete; Http::ok() erzeugt
"status": "success".
- UPGRADE §16.1 deckte Neuprodukte nicht ab: Fuer ein Produkt ohne Release
existiert /releases/<slug>/ nicht und wird uebersprungen. Das Verzeichnis
entsteht erst mit dem ersten Upload, der naechste Tick schuetzt es. Der erste
ausgelieferte Build muss die Zugangsdaten also schon mitbringen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
/releases/ wurde bisher offen ausgeliefert, damit ausgelieferte Anwendungen
ohne Zugangsdaten nach Updates suchen koennen. Das bedeutete aber auch, dass
jeder im Internet die vollstaendigen Pakete herunterladen konnte - mitsamt
allem, was versehentlich darin liegt. Genau so lag ein echter API-Schluessel
in einer mitgelieferten appsettings.json oeffentlich abrufbar.
Zugang haengt jetzt am Lizenzschluessel: Wer eine gueltige Lizenz fuer ein
Produkt hat, kommt an dessen Updates. Die Anwendung kennt ihren Schluessel
ohnehin und versorgt sich damit selbst - es muss nichts verteilt werden.
Server
- ReleaseGuard erzeugt je Produktverzeichnis .htaccess und .htpasswd.
Bewusst getrennt: eine gemeinsame Datei wuerde bedeuten, dass eine Lizenz
fuer Produkt A auch Produkt B oeffnet. license_licenses.product_id bindet
jeden Schluessel ohnehin an genau ein Projekt.
- Eingetragen werden aktive, nicht abgelaufene Lizenzen (Benutzername =
Passwort = Schluessel; Basic Auth braucht zwei Felder, es gibt aber nur ein
Geheimnis) sowie alle Installationskonten - bei einer Erstinstallation gibt
es noch keinen Schluessel, mit dem sich das Paket holen liesse.
- Deren Hash wird unveraendert aus dc_users uebernommen: password_hash()
erzeugt bcrypt im Format $2y$, genau das versteht Apache. Ein
Klartextpasswort wird nirgends gebraucht. Argon2-Hashes werden erkannt und
uebersprungen statt eine unbrauchbare Datei zu erzeugen.
- Lizenzschluessel werden mit Kosten 8 gehasht statt 12: 29 Zeichen
maschineller Zufall sind kein Menschenpasswort, Apache prueft aber bei
*jeder* Anfrage neu.
- Geschrieben wird ueber eine temporaere Datei mit rename() - ein Abbruch
wuerde sonst eine halbe Zugangsdatei hinterlassen und in dem Moment die
halbe Kundschaft aussperren.
- Neu erzeugt bei jeder Lizenz- und Kontoaenderung. Abgelaufene Lizenzen
loesen anders als ein Widerruf nichts aus; dafuer gleicht cli/tick.php nach
und erzeugt spaetestens alle sechs Stunden neu.
- Statusanzeige und Schaltflaeche im WebUI unter UpdateService.
Client
- ReleaseCredentials: Lizenzschluessel oder Installationskonto als Basic Auth.
- UpdateClient und Agent senden sie fuer latest.json und package.tar.gz.
- UpdateCheckResult.Unauthorized trennt "Lizenz traegt nicht mehr" von einem
Netzwerkfehler. Ohne diese Unterscheidung sucht man an der falschen Stelle.
- LaunchUpdateAgent reicht licenseKey als --license-key durch.
- Der Installer benutzt die beim Anmelden eingegebenen Zugangsdaten auch fuer
den Paketabruf; das Setup-Token taugt dafuer nicht, weil Apache prueft und
nicht die Anwendung.
Sonstiges
- deploy.py klammert artifacts/ aus. Ohne das landeten die gebauten
Installer-Binaries zusaetzlich unter /artifacts/ im Webroot.
ACHTUNG Reihenfolge: Der Schutz sperrt jede Anwendung aus, die noch mit dem
alten SDK gebaut ist. Erst ausliefern, dann scharfschalten - siehe
UPGRADE.md §16.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Ohne .gitattributes wandelt Git auf einem Windows-Arbeitsplatz beim Auschecken
in CRLF um. Fuer install.sh ist das kein Schoenheitsfehler, sondern ein
Ausfall: Das Skript wird auf Linux-Zielsystemen ausgefuehrt und scheitert dort
mit "bad interpreter: /bin/sh^M". Aufgefallen an der Umwandlungswarnung beim
vorigen Commit - die ausgelieferte Fassung auf dem Server hat noch LF, die
naechste aus dem Repository gebaute haette es nicht mehr gehabt.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Bisher gab es nur den Update-Weg: eine Anwendung musste bereits installiert und
eingerichtet sein, damit sich etwas aktualisieren liess. Die Erstinstallation
auf einem neuen System war Handarbeit - Paket kopieren, Konfiguration
abtippen, Token besorgen.
Setup-API (neu)
- POST /api/setup/v1/login tauscht Benutzername und Passwort gegen ein Token
mit 30 Minuten Gueltigkeit und ausschliesslich setup:install. Es wird nicht
mitgeschrieben und lebt im Installer nur im Speicher.
- GET /api/setup/v1/catalog zeigt nur, was zur Laufzeitkennung des anfragenden
Systems passt. Ein Projekt mit ausschliesslich Windows-Paket taucht auf einem
Linux-Rechner gar nicht erst auf.
- POST /api/setup/v1/token stellt das Dauertoken der Anwendung aus. Welche
Rechte vergeben werden, entscheidet der Server; die Anfrage kann nur
einschraenken. Sonst waere der Umweg ueber ein kurzlebiges Token wirkungslos.
Rollentrennung (Migration 012)
- dc_users bekommt role, disabled und last_login_at. Die Rolle "installer"
darf sich ueber den Setup-Weg anmelden und nicht am WebUI. Die Zugangsdaten
werden auf jedem Zielsystem eingetippt; mit einem Administratorkonto
verteilte man damit den Zugang zu Tokens, Lizenzen und Monitoren auf jeden
Rechner, auf dem je etwas installiert wurde.
- Auth::verifyCredentials() prueft sessionfrei, damit Setup- und WebUI-Login
nicht zwei verschiedene Haertungsgrade haben (Drosselung, Timing-Angleichung,
Rehash gelten fuer beide).
- Konten mit hinterlegtem TOTP-Geheimnis werden am Setup-Weg mit 501
abgewiesen. Eine TOTP-Pruefung gibt es im Deploymentcenter noch nicht; sie
stillschweigend zu uebergehen waere ein Rueckschritt.
- Benutzerverwaltung im WebUI - es gab bisher gar keine, nur den einen von
install_db.php angelegten Admin. Das letzte aktive Administratorkonto laesst
sich weder deaktivieren noch loeschen.
Installer
- update-agent --action install fuehrt durch Anmeldung, Auswahl,
Zielverzeichnis, Installation und Einrichtung. Die Dateien kommen ueber
denselben Pfad wie ein Update - mit Pruefsumme, Signatur, Staging und
Rollback. Ein zweiter Download-Weg waere ein zweiter Ort fuer dieselben
Fehler.
- --action configure holt die Einrichtung nachtraeglich.
- setup.json im Paket beschreibt die benoetigten Werte. Bewusst im Paket und
nicht zentral: so ist sie mit der Anwendung versioniert.
- Gefragt wird nur, was uebrig bleibt: bereits gesetzt -> detect:... ->
provision -> fragen. Platzhalter wie changeme oder <dein-wert> gelten dabei
nicht als eingerichtet, sonst liefe die Anwendung mit der Vorlage los.
- SetupWriter erhaelt vorhandene Inhalte. Eine appsettings.json fuehrt neben
den abgefragten Werten meist Logging und anderes; sie neu zu erzeugen waere
bequemer und verloere das - bei einer Neuinstallation ohne Backup.
int und bool landen als JSON-Typ, nicht als Zeichenkette.
Downloads
- scripts/build_installer.ps1 baut selbstenthaltende Einzeldateien fuer
win-x64, linux-x64 und linux-arm64 (rund 34 MB, .NET-Laufzeit inbegriffen).
Ohne NativeAOT und ohne Trimming: Spectre.Console loest ueber Reflexion auf
und braeche sonst erst beim Anwender.
- scripts/upload_installer.py laedt sie nach /installer/. Getrennt von
deploy.py, das client-dotnet bewusst ausklammert.
- Bereich "Installer" auf der UpdateService-Seite mit Groessen, Pruefsummen
und den wget-Befehlen; die Angaben stammen aus installer.json statt aus fest
eingetragenem Text.
- install.sh und install.ps1 laden, pruefen die Pruefsumme und legen ab -
sie richten bewusst nichts selbst ein. Das Manifest wird BOM-frei
geschrieben, sonst scheitert json_decode() daran.
Enthaelt ausserdem die bislang nicht committete Arbeit an den
RocketChat-Benachrichtigungen (Migrationen 010 und 011) sowie die Loesch- und
Editierfunktion des UpdateService; die betroffenen Dateien liessen sich nicht
getrennt stagen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Behebt eine Reihe zusammenhaengender Fehler im Update-Weg, die zusammen
verhindert haben, fuer mehr als eine Plattform auszuliefern - und die im
Fehlerfall halb aktualisierte Installationen hinterliessen.
Server
- Migration 009: Spalte platform samt neuem Unique-Key. Zuvor verdraengte das
zuletzt veroeffentlichte Paket alle anderen Plattformen derselben Version,
weil ON DUPLICATE KEY auf (slug, version, channel) griff. Ein Linux-System
zog sich damit das Windows-Paket.
- Aufloesungsregel: je Version das plattformgenaue Paket, sonst das
plattformunabhaengige. Ein Client ohne Plattformangabe sieht ausschliesslich
'any' - lieber kein Update als das falsche.
- manifest_json wird endlich befuellt; die Spalte blieb bisher immer leer,
wodurch die API nie der Rueckfall sein konnte, als der sie gedacht war.
- Releases werden serverseitig mit RSA-SHA256 signiert, neuer Endpunkt
/api/updateservice/v1/pubkey. Bewusst kein HMAC: der Pruefende laeuft auf
fremden Systemen und darf den Signierschluessel nicht besitzen.
Packager
- Bricht ab, statt die Versionshistorie zu verlieren. Schlug das Lesen der
bestehenden latest.json fehl, ersetzte ein leeres catch die komplette
Historie durch einen einzigen Eintrag - ohne jede Meldung.
- Echte Glob-Muster. Zuvor trafen "logs/**" und "scratch/**" aus der
mitgelieferten Beispielkonfiguration nie zu.
- preservePatterns: Konfigurationsvorlagen werden ausgeliefert, ersetzen am
Ziel aber keine vorhandene Datei. Eine settings.json mit Zugangsdaten
ueberschrieb bisher beim Update die Konfiguration jedes Zielsystems.
- Warnt vor Dateien, die nach Zugangsdaten aussehen und auf keiner Liste stehen.
- Prueft --version gegen die Hauptassembly. Eine Abweichung fuehrte zu einer
Endlosschleife: Clients aktualisieren, melden weiter die alte Version,
halten das Release erneut fuer neu.
- --platform mit Ableitung aus dem Publish-Pfad.
Agent
- Anwenden mit Plan, Backup und vollstaendigem Rollback. Die Stelle war als
"Atomic Replace with Backup" kommentiert und war eine Kopierschleife.
- Verwaiste Dateien werden entfernt, aber nur solche aus dem Manifest der
Vorversion. Was nicht aus einem Release stammt, bleibt liegen.
- Das laufende Agent-Binary wird zur Seite gelegt statt ueberschrieben.
- API-Rueckfall in FetchManifestAsync; bisher nur im SDK vorhanden, weshalb
die Anwendung "Update verfuegbar" und der Agent "kein Release" sagen konnte.
- Installierte Version aus --current-version oder manifest.json statt des
Textes "Unbekannt", der als 0 gelesen wurde und jede Version neuer erscheinen
liess. Reparatur funktioniert damit auch ohne manifest.json.
- Setzt das Ausfuehrungsbit fuer Linux-Pakete, die unter Windows gebaut wurden.
SDK
- ResolveAgentPath() liefert den plattformrichtigen Namen; ein fest verdrahtetes
"update-agent.exe" wird unter Linux nie gefunden.
- LaunchUpdateAgent uebergibt jetzt --restart (wurde nie uebergeben, die
Anwendung blieb nach dem Update zu), --wait-for-pid (kein Wettlauf mehr mit
dem Herunterfahren) und --platform.
Enthaelt ausserdem die bislang nicht committete Arbeit an Watchdog, Lizenz-
Client und cli/tick.php samt Migration 008; die betroffenen Dateien liessen
sich nicht getrennt stagen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Watchdog-Anleitung vollständig überarbeitet
- Alle drei Codebeispiele trugen das geseedete Demo-Token fest im Quelltext.
Es ist an die Source "srv-db-01" gebunden — wer es übernommen hätte, wäre für
jeden anderen Dienst abgewiesen worden. Jetzt Umgebungsvariable und eine
Anleitung, wie man ein eigenes Token erzeugt.
- Der Ratschlag "sende beim Beenden einen Ping mit Status stopped" beschrieb
etwas, das die API nicht konnte. Statt die Anleitung an die Lücke anzupassen,
ist die Lücke geschlossen: status akzeptiert jetzt "stopped" und
"maintenance". Der Evaluator lässt solche Monitore in Ruhe, statt wenige
Minuten nach jedem sauberen Shutdown einen Fehlalarm zu erzeugen.
- Neu dokumentiert: checks (Gesundheitszustand per Push, ohne offene Ports),
metrics samt Verlauf und Abweichungsvergleich, Alarmunterdrückung über die
Hierarchie, die Schwellen des Evaluators (2x warning, 4x down) und der
erforderliche Cron-Job.
Lizenz-Anleitung
- Neuer Abschnitt zum Antwortformat. Die Lizenz-Endpunkte antworten bewusst
ohne den status/error-Umschlag der übrigen API; das Feld status auf oberster
Ebene trägt den Lizenzzustand. Genau diese Besonderheit hatte ich beim Umbau
übersehen, weshalb sie jetzt ausdrücklich festgehalten ist — samt Tabelle
aller Zustände.
- Ergänzt: Deaktivierung braucht den shared_key, mit Beispiel für den .NET-
Client und curl. Verhalten bei Ratenbegrenzung.
UpdateService-Anleitung
- Prüf-Endpunkte dokumentiert (check, latest, releases) samt Antwortformat.
- Tabelle zum Versionsvergleich mit den Fällen, die vorher falsch liefen.
- Auto-Resolve beim Veröffentlichen beschrieben.
Agent-Prompt-Vorlage
- Fehler-Schnittstelle ergänzt, inklusive Hinweis auf "ignored": true, damit
ein Agent bekannte Fehler nicht untersucht.
Alle in der Dokumentation genannten API-Pfade und Scopes wurden maschinell
gegen Routing und TokenManager abgeglichen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Rücksprung auf das Dashboard
Nach jedem Speichern landete man auf der Standard-Unteransicht des Moduls
statt dort, wo man gearbeitet hatte. Drei Ursachen:
- Die Handler setzten nur den Modul-Anker (#tab-watchdog). Damit blieb die
Unteransicht unbestimmt und es wurde die erste angezeigt.
- Die Wiederherstellung aus dem Anker leitete den Modulnamen aus dem Namen der
Unteransicht ab (sub-<modul>-<rest>). Für "sub-update-releases" ergab das
"update" statt "updateservice".
- Der zugehörige Navigationsknopf wurde über seinen Beschriftungstext gesucht,
der den Modulnamen nie enthält. Der Rückweg schlug also immer fehl.
Jetzt teilt das Formular beim Absenden mit, welche Unteransicht offen war
(_return), und der Server leitet dorthin zurück. Umgesetzt als zentraler
submit-Zuhörer, damit künftige Formulare das Verhalten erben. Die Zuordnung
Unteransicht -> Modul kommt aus derselben Liste, die auch die Navigation
aufbaut, und die Knöpfe tragen eine eindeutige Kennung statt Text.
Zusätzlich wird die Ansicht jetzt auch bei Vor- und Zurück-Navigation im
Browser wiederhergestellt.
Lizenzmodul
Vier Unteransichten für eine Aufgabe sind auf zwei sinnvolle reduziert:
- "Dashboard" zeigte drei Zahlen; sie stehen jetzt über der Liste.
- "Lizenz Bearbeiten & HW" ist entfallen. Zum Bearbeiten musste man die gerade
angeklickte Lizenz in einer Auswahlliste erneut suchen. Jetzt öffnet sich ein
Dialog direkt über der Tabelle, die Hardware-Liste steht daneben.
- "Settings" enthielt zwei fest verdrahtete URLs und ist in "Integration"
aufgegangen, zusammen mit einem Beispielaufruf.
- Der dort angezeigte "Ed25519 Public Key" war frei erfunden und wurde
entfernt; die Offline-Vorschau nennt jetzt das tatsächlich verwendete
HMAC-SHA256 und weist darauf hin, dass nur der Download signiert ist.
Nebenbei
Der Inhalt des Projekte-Moduls lag direkt im Modul-Container, ohne
subtab-content. Der Navigationseintrag "Projekte Übersicht" zeigte damit ins
Leere. Jetzt einheitlich wie alle anderen Module.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Die Seite brach beim Aufbau ab und lieferte im Browser
"switchMainTab is not defined": das Rendering endete vor dem Script-Block,
sodass keine einzige Funktion definiert wurde.
Ursache: e() war als ?string deklariert. PHP wandelt numerische Array-Schlüssel
automatisch in int um, sodass die Zeitraum-Auswahl im Fehler-Stream mit den
Schlüsseln 0/1/24/168/720 einen int übergab. Unter declare(strict_types=1) warf
das einen TypeError mitten in der Ausgabe.
Die enge Signatur war generell unpassend: mit ATTR_STRINGIFY_FETCHES = false
liefert PDO numerische Spalten als int, und genau diese Werte werden über e()
ausgegeben. Die Funktion nimmt jetzt jeden skalaren Typ entgegen.
Gefunden hat den Fehler die Selbstmeldung: die unbehandelte Exception landete
als Item im eigenen Bugtracker, inklusive Datei und Zeilennummer.
Die übrigen typisierten Helfer (dc_dt, dc_ago, getMonitorIconUrl, btUrl,
errUrl) wurden gegengeprüft; sie erhalten ausschließlich Strings bzw. Arrays.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Fehler-Schnittstelle
- Neuer schlanker Eingang POST /api/errors/v1/report für den globalen
Exception-Handler einer Anwendung. Titel und Dringlichkeit leitet der Server
ab; gespeichert wird in derselben Tabelle wie der Bugtracker. Ein zweiter
Speicher wäre nur ein zweiter Ort, an dem man suchen müsste.
- error_level (fatal/error/warning) trennt die technische Art des Ereignisses
von der geschäftlichen Dringlichkeit. Ein Duplicate-Entry ist technisch ein
error, geschäftlich belanglos — beides zu vermischen war der Grund, warum
solche Meldungen als Bug im Dashboard landeten.
Ignore-Regeln gegen bekanntes Rauschen
- bugtracker_ignore_rules mit contains/regex/exception_class, Pflichtfeld für
die Begründung und optionaler Alarmschwelle.
- Ein Treffer bedeutet nicht "wegwerfen": Der Fehler wird weiterhin erfasst und
hochgezählt, bleibt aber aus der Übersicht heraus und löst keine
Benachrichtigung aus. Der Zähler ist der eigentliche Zweck — dass ein
bekannter Fehler auftritt, ist normal; dass er plötzlich hundertmal so oft
auftritt, ist ein Signal. Dafür das rollende Stundenfenster und
error.rate_exceeded.
- Neue Regeln lassen sich rückwirkend auf bestehende Einträge anwenden.
Gruppierung überarbeitet
- Der Schlüssel nahm bisher 300 Zeichen Stacktrace auf. Derselbe Fehler
zersplitterte dadurch, sobald ein Aufrufer den Stack einmal mitschickte und
einmal nicht. Jetzt zählt der Ursprungsort: bevorzugt die Dateiangabe, sonst
der erste Rahmen des Stacktrace.
- Die Normalisierung ersetzte nur Zahlen ab vier Stellen, wodurch
'AA-1' und 'BB-2' getrennt blieben. Werte in Anführungszeichen, die Ziffern
enthalten, gelten jetzt als veränderlich — der Schlüsselname bleibt erhalten,
sodass verschiedene Unique-Keys unterscheidbar sind. Mit 9 Testfällen belegt.
Metrik-Verlauf
- watchdog_metrics speichert numerische Heartbeat-Werte mit Zeitstempel.
Zuvor wurde metrics_json bei jedem Heartbeat überschrieben; damit ließ sich
"die Platte läuft seit drei Tagen voll" nicht erkennen, nur "sie ist voll".
- GET /api/watchdog/v1/metrics liefert den verdichteten Verlauf und die
Abweichung vom eigenen Sieben-Tage-Durchschnitt. Dieser relative Ansatz
braucht keine projektspezifischen Schwellwerte.
- Aufbewahrung 14 Tage, Bereinigung stündlich durch den Evaluator.
Health-Checks per Push statt Abruf
- Der Heartbeat nimmt ein checks-Objekt entgegen, das die Anwendung selbst
ermittelt. Das Deploymentcenter interpretiert die Namen nicht, es liest nur
ok und message — was "gesund" bedeutet, entscheidet jede Anwendung selbst.
Schlägt eine Prüfung fehl, wird ein als ok gemeldeter Heartbeat auf warning
herabgestuft.
- Bewusst ausgehend: auf den Zielmaschinen müssen keine Ports geöffnet werden.
Abhängigkeitsbewusste Alarmierung
- Fällt ein Monitor aus, dessen Parent selbst unten ist, wird der Alarm
unterdrückt. Der Zustand bleibt sichtbar. Vorher erzeugte ein ausgefallener
Hypervisor mit zwölf VMs dreizehn Meldungen für ein Problem.
- Mehrere Ebenen und fehlerhafte Hierarchien (Zyklen, gelöschte Parents) sind
abgesichert; mit 10 Testfällen belegt.
WebUI
- Neue Ansicht "Fehler-Stream" mit Filtern nach Projekt, Fehlerklasse,
Umgebung, Zeitraum und Sichtbarkeit sowie Volltextsuche und Pagination.
Stummgeschaltete Einträge sind standardmäßig ausgeblendet.
- Verwaltung der Ignore-Regeln inklusive Trefferzähler.
- Die Detailansicht zeigt Fehlerklasse, Stummschaltungsgrund und die Häufung
im laufenden Stundenfenster.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Regression aus dem vorigen Commit
- /api/license/v1/validate lieferte die Antwort im neuen status/error-Umschlag.
Der Vertrag dieses Endpunkts ist aber bereits ausgerollt: das Feld "status"
auf oberster Ebene trägt den Lizenzzustand (valid, revoked, expired ...).
LicenseClient las dadurch "success" statt "valid" — jeder ausgelieferte
Client hätte seine Lizenz für ungültig gehalten. Die Lizenz-Endpunkte
antworten jetzt wieder ohne Umschlag (Http::raw).
Gefunden durch Ausführen der projekteigenen Test-Suite gegen den Server.
Packager
- FTP-Zugangsdaten standen als Standardwerte im Quelltext und zusätzlich in
packager.config.json und in der Integrationsanleitung. Alle drei Fundstellen
bereinigt; die Konfigurationsdatei ist nicht mehr versioniert. Zugangsdaten
kommen aus Datei, Umgebungsvariablen oder CLI-Argument, sonst bricht das
Programm mit einer klaren Meldung ab.
- Das Veröffentlichen sendet jetzt ein Token (updateservice:publish) und nutzt
den Endpunkt /api/updateservice/v1/publish.
- Fehler wurden von einem leeren catch verschluckt, und ohne Erfolgsfall wurde
gar nichts ausgegeben. Das Werkzeug meldete am Ende immer Erfolg und lieferte
Rückgabewert 0, selbst wenn FTP-Upload und API-Aufruf fehlgeschlagen waren.
Jetzt ehrliche Meldungen und Rückgabewerte 0/1/2.
- packager.config.json wurde vom csproj nie ins Ausgabeverzeichnis kopiert,
weshalb sie dort nie gefunden wurde und stets die hartkodierten Werte griffen.
UpdateClient
- IsVersionNewer entfernte die Vorabkennung, aber kein führendes "v". Damit
scheiterte Version.TryParse bei "v1.4.2" und es wurde auf einen
alphabetischen Vergleich zurückgefallen, in dem "v1.9.0" als neuer gilt als
"v1.10.0" — derselbe Fehler wie zuvor serverseitig im SQL. Ersetzt durch
einen vollständigen semantischen Vergleich, verifiziert mit 16 Testfällen.
- Der Rückfall auf die API lag in einem catch-Block, aber GetAsync wirft bei
einem 404 keine Exception. Fehlte die statische latest.json, brach die
Prüfung ab, statt die API zu befragen.
Dokumentation
- BUGTRACKER_INTEGRATION_GUIDE.md beschrieb denselben Workflow ein zweites Mal
und war bereits auseinandergelaufen: Aufrufe ohne Token, alte Pfade, weder
Claim/Lease noch Idempotenz. Ersetzt durch einen Verweis auf das gepflegte
Agenten-Handbuch samt Übersicht der Änderungen.
- UPDATESERVICE_INTEGRATION_GUIDE.md um Token, Umgebungsvariablen und
Rückgabewerte ergänzt.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sicherheit
- install_db.php war ohne Authentifizierung erreichbar und setzte bei jedem
Aufruf das Admin-Passwort auf einen fest im Code stehenden Wert zurück.
Jetzt Auth-Pflicht; ein Konto wird nur bei leerer Benutzertabelle angelegt.
- Stored XSS im Bugtracker-Detail-Modal: Titel, Beschreibung, Fehlermeldung,
Stacktrace und Kommentare gingen ungefiltert durch innerHTML.
- report.php, projects.php und das Veröffentlichen von Releases verlangen jetzt
zwingend ein Token. Publish war zuvor völlig ungeschützt.
- CSRF-Token in allen Formularen, Session-Regenerierung nach Login,
Drosselung fehlgeschlagener Anmeldeversuche.
- Zugangsdaten aus der Versionskontrolle entfernt (Serverdaten.txt,
config.php, .htpasswd, deploy_config.json). Historie enthält sie weiterhin,
Rotation erforderlich (siehe docs/UPGRADE.md).
- Token-Validierung nur noch über SHA-256-Hash; expires_at wird ausgewertet.
Behobene 500er
- Audit::log() war in index.php weder eingebunden noch importiert. Jeder
Klick auf "Aktivierung freigeben" endete in einem Fatal Error.
- Derselbe benannte PDO-Platzhalter mehrfach je Statement (:id in
revokeToken/deleteToken, :q siebenfach in der Volltextsuche). Bei
EMULATE_PREPARES=false ist das nicht zulässig und warf HY093.
- Migration 005 nutzte dynamisches SQL, dessen Semikolons in String-Literalen
vom alten explode(';')-Installer als Statement-Ende gelesen wurden. Sie
schlug still fehl, wodurch push_id/target_agent/tags dauerhaft fehlten.
- Monitor-Umbenennung ohne Transaktion, verschachtelte Transaktionen im
RateLimiter.
Funktionale Korrekturen
- Der Watchdog-Evaluator fehlte vollständig: Monitor-Zustände änderten sich nur
beim Eintreffen eines Heartbeats, ein ausgefallenes System blieb dauerhaft
"up". Erster Lauf auf dem Produktivsystem: 7 von 10 Monitoren waren
tatsächlich seit über einem Tag nicht erreichbar.
- Das Feld "os" fehlte im Monitor-Dialog, wurde aber gespeichert und löschte
damit bei jedem Speichern das Betriebssystem.
- Der Resolve-Dialog existierte im HTML nicht; der Button war funktionslos.
- Versionsvergleich erfolgte lexikografisch, wodurch 1.9.0 als neuer galt
als 1.10.0.
- Schreiboperationen meldeten Erfolg auch für nicht existierende IDs.
- Post/Redirect/Get gegen doppelte Einträge beim Neuladen.
Neue Struktur
- src/bootstrap.php mit PSR-4-Autoloader ersetzt die require-Ketten.
- Core: Config, Http, Csrf, ApiAuth, Logger, Migrator, ErrorReporter.
- Migrator mit zeichenweisem SQL-Parser, dc_migrations und Baseline-Verfahren,
damit bestehende Installationen keine Beispieldaten zurückbekommen.
Agenten-Workflow
- Claim/Lease: Items werden exklusiv übernommen, damit nicht zwei Agenten am
selben Problem arbeiten. action=next holt und reserviert in einem Zug.
- Idempotenz über client_ref, Deduplizierung auch für Feature Requests,
Erkennung von Regressionen, automatische Eskalation des Schweregrads.
- Strukturierter Code-Kontext (repo_url, commit_sha, file_path, line_no).
- Delta-Abfragen über updated_since, Pagination, Bulk-Update.
- Beim Veröffentlichen eines Releases schließen sich Items mit passendem
resolved_in_build selbst.
- Ausgehende Webhooks mit HMAC-Signatur, /api/health, /api/openapi.json.
- Unbehandelte Fehler meldet die Plattform in ihren eigenen Bugtracker.
WebUI
- Serverseitige Filterung mit Pagination statt Rendern aller Datensätze.
- Migrations-Schranke, Evaluator-Warnung, Übersicht aktiver Agenten.
Zeitstempel liegen in der Datenbank durchgängig in UTC und werden für die
Anzeige in die App-Zeitzone umgerechnet.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>