Befund aus dem Betrieb: Ein Passkey aus 1Password liess sich als Hardwarekey registrieren. Fuer das Deploymentcenter hinnehmbar, fuer ein Protokoll, das ausdruecklich Hardware fordert, nicht. Zwei Dinge, die wie eine Pruefung aussahen und keine waren - beide seit 2.9.0 gesetzt: authenticatorAttachment "cross-platform" steuert, was der Browser anbietet, und ein Passwortverwalter meldet sich ebenso. Und response.authenticatorAttachment setzt der Client, also genau die Instanz, gegen die sich nichts beweisen laesst. Was traegt, ist Basic Attestation. Neu geprueft wird die Kette bis zu einer hinterlegten Herstellerwurzel; die drei FIDO-Wurzeln von Yubico liegen bei. Ein x5c allein genuegt ausdruecklich nicht: Der Fall "eigenes Zertifikat, Name Yubico, AAGUID eines echten YubiKey 5, gueltige Unterschrift" wurde nachgebaut und wird abgewiesen - an der Kette, nicht an der Signatur. Drei Stufen statt eines Schalters, weil alle bisher registrierten Schluessel als nicht nachgewiesen gelten - auch echte Yubikeys. Bei attestation "none" ersetzt der Browser die Geraetekennung durch Nullbytes, bevor der Server sie sieht; es gab also nie eine Herkunft. Ein einzelner Schalter haette beim Umlegen den eigenen Zugang gesperrt. Der Wechsel nach "strict" wird zusaetzlich abgelehnt, solange das eigene Konto keinen nachgewiesenen Schluessel hat. - Attestation.php: packed und fido-u2f, Kettenpruefung gegen eigene Wurzeln statt gegen den Speicher des Betriebssystems, Gueltigkeit, Gegenprobe der AAGUID aus der Zertifikatserweiterung - HardwarePolicy.php: off / register / strict, optionale Modellliste, Wurzeln aus der Konfiguration erweiterbar - Migration 021: attested und drei Anzeigespalten, Vorgabe 0 - WebUI: Herkunft je Schluessel, Regel vor der Beruehrung angesagt - testctl clear_credentials, damit ein Testlauf den naechsten nicht aussperrt - Testreihe "hardware", 5 Faelle, ohne Krypto-Bibliothek 184 von 184 Testfaellen gruen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1008 lines
102 KiB
JSON
1008 lines
102 KiB
JSON
{
|
|
"_comment": "Einzige Quelle der Wahrheit fuer den Changelog. Wird von /docs/changelog.php gerendert und von GET /api/updateservice/v1/changelog?since=X ausgeliefert. Neue Eintraege oben einfuegen.",
|
|
"schema": 1,
|
|
"current": "2.9.2",
|
|
"versions": [
|
|
{
|
|
"version": "2.9.2",
|
|
"date": "2026-09-21",
|
|
"summary": "Das WebUI kann jetzt verlangen, dass ein zweiter Faktor wirklich in Hardware entstanden ist. Bisher liess sich auch ein Passwortverwalter als Hardwarekey registrieren - fuer das Deploymentcenter hinnehmbar, fuer ein Protokoll, das ausdruecklich Hardware fordert, nicht.",
|
|
"actionRequired": true,
|
|
"changes": [
|
|
{
|
|
"area": "security",
|
|
"breaking": false,
|
|
"title": "Attestation wird geprueft, wenn die Regel es verlangt",
|
|
"text": "Bis 2.9.1 forderte die Registrierung attestation \"none\" an und pruefte nichts. Der Kommentar im Code begruendete das damit, dass der Betreiber seinen eigenen Schluessel an seinem eigenen Server registriert - das stimmte und uebersah, was daraus im Betrieb wird: Ein 1Password-Passkey liess sich ebenso anstandslos aufnehmen wie ein Yubikey. Neu prueft der Server auf Wunsch die Basic Attestation: Der Authenticator unterschreibt bei der Aufnahme mit einem Schluessel, der ab Werk in seinem Sicherheitselement liegt, und das zugehoerige Zertifikat muss auf eine hinterlegte Herstellerwurzel ketten. Mitgeliefert werden die drei FIDO-Wurzeln von Yubico.",
|
|
"action": "Migration 021 ausfuehren. Danach unter System -> Benutzer die gewuenschte Stufe waehlen; ohne Umstellen bleibt das Verhalten von vorher."
|
|
},
|
|
{
|
|
"area": "security",
|
|
"breaking": false,
|
|
"title": "Warum die Kette geprueft wird und nicht nur das Vorhandensein eines Zertifikats",
|
|
"text": "Ein x5c allein beweist nichts: Jeder Authenticator kann sich selbst ein Zertifikat ausstellen, es \"Yubico U2F EE\" nennen, die AAGUID eines echten YubiKey 5 eintragen und kryptografisch einwandfrei unterschreiben. Genau dieser Fall wurde nachgebaut und gegen den Server gefahren - er wird abgewiesen, und zwar an der Kette, nicht an der Signatur. Was Software nicht herstellen kann, ist eine Kette, die bei Yubicos Wurzel endet; dafuer braeuchte es deren privaten Schluessel. Deshalb ist die Wurzeldatei kein Beiwerk, sondern der Kern der Pruefung.",
|
|
"action": null
|
|
},
|
|
{
|
|
"area": "security",
|
|
"breaking": false,
|
|
"title": "Drei Stufen, damit sich niemand aussperrt",
|
|
"text": "off laesst jeden Authenticator zu (das Verhalten bis 2.9.1). register verlangt den Nachweis nur bei NEUEN Aufnahmen - bestehende Schluessel melden sich weiter an. strict laesst auch zur Anmeldung nur noch geprueft in Hardware entstandene Schluessel zu. Die Zwischenstufe ist nicht Bequemlichkeit, sondern notwendig: Alle vor dieser Fassung registrierten Schluessel gelten als nicht nachgewiesen, auch echte Yubikeys, weil bei attestation \"none\" der Browser die Geraetekennung durch Nullbytes ersetzt, bevor der Server sie sieht. Ein einzelner Schalter haette in dem Moment, in dem man ihn umlegt, den eigenen Zugang gesperrt. Der Wechsel nach strict wird zusaetzlich abgelehnt, solange das eigene Konto keinen nachgewiesenen Schluessel hat.",
|
|
"action": "Wer strict nutzen will: erst auf register stellen, den Hardwarekey einmal neu aufnehmen, dann auf strict."
|
|
},
|
|
{
|
|
"area": "security",
|
|
"breaking": false,
|
|
"title": "cross-platform war nie eine Pruefung",
|
|
"text": "In den Registrierungsvorgaben stand seit 2.9.0 authenticatorAttachment = \"cross-platform\". Das sieht aus wie eine Beschraenkung auf angesteckte Hardware und ist keine: Die Angabe steuert, was der Browser ANBIETET, und ein Passwortverwalter meldet sich ebenfalls als cross-platform. Auch response.authenticatorAttachment traegt nicht - das setzt der Client, und der Client ist genau die Instanz, gegen die sich hier nichts beweisen laesst. Der Kommentar an dieser Stelle sagt das jetzt ausdruecklich, damit die Angabe nicht ein zweites Mal fuer eine Zusicherung gehalten wird.",
|
|
"action": null
|
|
},
|
|
{
|
|
"area": "webui",
|
|
"breaking": false,
|
|
"title": "Die Herkunft steht in der Schluesselliste",
|
|
"text": "Jeder registrierte Schluessel zeigt, ob er sich als Hardware ausgewiesen hat, bei welcher Herstellerwurzel die Kette endete und welche Geraetekennung er nennt. Die Kennung steht dort in derselben Schreibweise, in der die Modellliste sie erwartet - sie laesst sich also uebernehmen, statt sie irgendwo nachschlagen zu muessen. Die Regel wird ausserdem beim Registrieren angesagt, BEVOR jemand seinen Schluessel beruehrt: Wer erst danach erfaehrt, dass nur Hardware zaehlt, sucht den Fehler beim Schluessel statt bei der Einstellung.",
|
|
"action": null
|
|
},
|
|
{
|
|
"area": "security",
|
|
"breaking": false,
|
|
"title": "Modellliste optional - und mit Warnhinweis",
|
|
"text": "Zusaetzlich zur Wurzelpruefung lassen sich einzelne AAGUIDs zulassen. Die Liste ist leer voreingestellt und soll es meist bleiben: Yubico weist neuen Firmwarestaenden neue Kennungen zu, eine ungepflegte Liste weist also irgendwann einen echten Schluessel ab. Wo sie benutzt wird, wird die Kennung aus den Authenticator-Daten gegen die im Zertifikat gegengeprueft - sonst filterte man nach einer Behauptung statt nach einer Eigenschaft. Weitere Hersteller lassen sich ueber security.webauthn_root_files in der config.php ergaenzen, bewusst dort und nicht im WebUI: Eine Vertrauenswurzel ueber dieselbe Oberflaeche austauschbar zu machen, die sie schuetzt, hiesse die Pruefung dort aushebelbar zu machen.",
|
|
"action": null
|
|
},
|
|
{
|
|
"area": "tests",
|
|
"breaking": false,
|
|
"title": "Neue Testreihe \"hardware\" und testctl clear_credentials",
|
|
"text": "Fuenf Faelle pruefen die Gegenueberstellung, auf die es ankommt: dieselben Bytes eines Authenticators ohne Herkunftsnachweis, einmal mit und einmal ohne Regel. Sie kommen ohne Krypto-Bibliothek aus, weil bei fmt \"none\" keine Signatur geprueft wird. Der Fall mit vorgetaeuschtem Zertifikat laeuft in einem eigenen Werkzeug ausserhalb der Suite. testctl bekommt clear_credentials: Ein Testlauf, der einen Schluessel registriert, sperrt sonst den naechsten aus, und dafuer den ganzen Datenbestand zu loeschen waere ein hoher Preis fuer eine Tabelle.",
|
|
"action": null
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"version": "2.9.1",
|
|
"date": "2026-09-20",
|
|
"summary": "Die Begruendung eines Roadmap-Vorhabens steht jetzt im Deploymentcenter statt nur in einer Markdown-Datei auf einem Entwicklungsrechner. Die Roadmap-Tafel zeigt sie, Agenten koennen sie lesen, und docs/ROADMAP.md wird daraus erzeugt.",
|
|
"actionRequired": true,
|
|
"changes": [
|
|
{
|
|
"area": "roadmap",
|
|
"breaking": false,
|
|
"title": "Begruendungen ziehen in die Datenbank",
|
|
"text": "bugtracker_items und roadmap_epics tragen jetzt ein Feld rationale (Migration 020). Bisher galt eine Zweiteilung: Zustand im Deploymentcenter, Begruendung in einer Datei im Repository des Projekts, verbunden ueber doc_anchor. Die Beschreibung der uebernommenen Eintraege sagte es woertlich: \"Die Begruendung steht weiterhin in der Datei - hier steht nur der Zustand.\" Das hatte zwei Folgen, die erst im Betrieb auffielen: Die Roadmap-Tafel konnte die Begruendung nicht zeigen, und - schwerer - die Beschreibung von dc_roadmap wies Agenten an, vor dem erneuten Vorschlagen eines zurueckgestellten Vorhabens \"diesen Absatz\" zu lesen, den ein Agent ausserhalb dieses einen Repositorys gar nicht erreichen konnte. Die Anweisung war nicht befolgbar.",
|
|
"action": "Migration 020 ausfuehren, dann einmalig `python scripts/dc.py roadmap-import` je Projekt mit Roadmap-Datei."
|
|
},
|
|
{
|
|
"area": "roadmap",
|
|
"breaking": false,
|
|
"title": "Ein eigenes Feld, nicht description",
|
|
"text": "description sagt, was gemeldet wurde - es kommt vom Melder, oft von einer fremden Maschine, und wird nicht umgeschrieben. rationale sagt, warum wir uns so entschieden haben. Zusammengelegt haette jede redaktionelle Ueberarbeitung der Begruendung den urspruenglichen Wortlaut der Meldung ueberschrieben, also die Grundlage, auf der entschieden wurde. Der Verlauf vermerkt nur, DASS die Begruendung sich geaendert hat und um wie viele Zeichen - nicht den ganzen Absatz ein zweites Mal.",
|
|
"action": null
|
|
},
|
|
{
|
|
"area": "roadmap",
|
|
"breaking": false,
|
|
"title": "Einlesen erkennt Absaetze und Tabellenzeilen",
|
|
"text": "POST /api/bugtracker/v1/roadmap?action=import_rationale liest eine mitgeschickte Roadmap-Datei und verteilt ihre Prosa auf Vorhaben und Bereiche. Drei Formen: \"## A - Titel\" fuer einen Bereich, \"### [A] #65 - Titel\" fuer ein Vorhaben, und Tabellenzeilen der Form \"| #111 | ... | Warum nicht |\". Die dritte Form ist kein Beiwerk: Der Abschnitt \"Verworfen\" fuehrt seine Ablehnungen als Tabelle, und ohne sie bliebe ausgerechnet das nicht am Item auffindbar, was am haeufigsten gebraucht wird - warum etwas schon einmal abgelehnt wurde. Ohne overwrite=true bleibt stehen, was im Deploymentcenter bereits hinterlegt ist; ein zweiter Lauf setzt eine dort ueberarbeitete Begruendung nicht stillschweigend zurueck.",
|
|
"action": null
|
|
},
|
|
{
|
|
"area": "roadmap",
|
|
"breaking": false,
|
|
"title": "Die Datei wird erzeugt - jetzt in zwei Bloecken",
|
|
"text": "Neben dem bestehenden Zustandsblock (dc:roadmap) gibt es ein zweites Markerpaar dc:rationale fuer die Begruendungen. Bewusst ein zweites Paar und keine Erweiterung des ersten: Zwischen den beiden steht in jeder gewachsenen Roadmap-Datei etwas von Hand Geschriebenes - eine Einleitung, eine Leitlinie, die Erklaerung der Statuszeichen -, und ein einziger Block haette das beim naechsten Erzeugen verschluckt. GET ?action=render&part=rationale liefert den Block, ?action=render_file die ganze Datei mit beiden eingesetzten Bloecken. Das Einsetzen laeuft serverseitig, damit die Markersuche nur einmal umgesetzt ist.",
|
|
"action": null
|
|
},
|
|
{
|
|
"area": "cli",
|
|
"breaking": false,
|
|
"title": "scripts/dc.py roadmap-import und roadmap-write",
|
|
"text": "Die beiden neuen Wege liegen in dc.py und nicht in cli/roadmap.php. Grund: cli/roadmap.php braucht PHP, und auf dem Windows-Arbeitsplatz gibt es keines - die Haelfte der Pflegemaschinerie, die die alte Zweiteilung zusammenhalten sollte, war dort nie ausfuehrbar. roadmap-write behaelt die Zeilenenden der vorhandenen Datei bei, damit nicht nach dem ersten Lauf die ganze Datei als Aenderung im Diff steht.",
|
|
"action": null
|
|
},
|
|
{
|
|
"area": "webui",
|
|
"breaking": false,
|
|
"title": "Begruendung im Seitenpanel der Tafel",
|
|
"text": "Das Panel zeigt die Begruendung als erstes Feld und laesst sie dort auch schreiben. Zuvor stand an dieser Stelle nur der Verweis doc_anchor - als nicht anklickbarer Text mit dem Hinweis, er zeige auf eine Datei, die das WebUI nie zu sehen bekommt. Kaertchen ohne hinterlegte Begruendung tragen eine dezente Marke; erledigte Vorhaben nicht, dort waere sie ein Vorwurf ohne Adressaten.",
|
|
"action": null
|
|
},
|
|
{
|
|
"area": "mcp",
|
|
"breaking": false,
|
|
"title": "MCP-Adapter 1.3.0",
|
|
"text": "dc_roadmap_set nimmt rationale entgegen. Die Beschreibung von dc_roadmap verweist nicht mehr auf eine Projektdatei, sondern nennt das Feld - die alte Fassung wies Agenten auf einen Absatz hin, den sie nicht erreichen konnten. doc_anchor bleibt fuer alte Verweise erhalten und ist als historisch gekennzeichnet.",
|
|
"action": "In Agenten-Umgebungen den MCP-Adapter neu starten, damit die geaenderten Werkzeugbeschreibungen greifen."
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"version": "2.9.0",
|
|
"date": "2026-09-20",
|
|
"summary": "Hardwarekey (WebAuthn) als zweite Stufe der WebUI-Anmeldung mit Notfalltoken als Ersatzweg, Konten lassen sich umbenennen, und die Roadmap gibt es jetzt als Tafel: Zeitstrahl nach Prioritaet mit Seitenpanel, Kommentieren und Ziehen.",
|
|
"actionRequired": true,
|
|
"changes": [
|
|
{
|
|
"area": "auth",
|
|
"breaking": false,
|
|
"title": "Hardwarekey als zweite Stufe der WebUI-Anmeldung",
|
|
"text": "Nach korrektem Passwort verlangt das WebUI einen registrierten FIDO2-Schluessel (Yubikey und Gleichartiges). Die Pflicht haengt je Konto daran, OB ein Schluessel registriert ist - nicht an einem Schalter. Der Unterschied zeigt sich beim Einspielen: Ein Schalter, der vor der Registrierung umgelegt wird, sperrt den Betreiber aus seiner eigenen Verwaltung aus. Umgesetzt ohne Fremdbibliothek (CBOR-Leser, COSE-nach-PEM, Pruefung ueber openssl_verify, ES256 und RS256). Die Setup-API fuer Zielsysteme ist nicht betroffen - sie laeuft an Auth::login() vorbei. Registriert wird unter System -> Benutzer; ein Schluessel gilt nur fuer die Adresse, unter der er registriert wurde, auf der Testinstanz also eigens.",
|
|
"action": "Migration 019 ausfuehren, dann unter System -> Benutzer mindestens einen Hardwarekey registrieren - moeglichst zwei, oder einen Notfalltoken dazu."
|
|
},
|
|
{
|
|
"area": "auth",
|
|
"breaking": false,
|
|
"title": "Notfalltoken als Ersatzweg",
|
|
"text": "Fuenf Gruppen zu vier Zeichen, einmalig bei der Erzeugung angezeigt; in der Datenbank liegt nur ein Hash. Einmalgebrauch: nach der Anmeldung damit ist er verbraucht, und die Meldezeile sagt es. Beim Eintippen zaehlen nur Buchstaben und Ziffern - Bindestriche und Gross-/Kleinschreibung sind gleichgueltig. Laeuft als gewoehnliches Formular ohne JavaScript; ein Ersatzweg mit denselben Voraussetzungen wie der Hauptweg waere keiner.",
|
|
"action": null
|
|
},
|
|
{
|
|
"area": "auth",
|
|
"breaking": true,
|
|
"title": "Auth::login() heisst jetzt Auth::startLogin()",
|
|
"text": "Die Methode liefert keinen bool mehr, sondern einen von drei Zustaenden (LOGIN_FAILED, LOGIN_COMPLETE, LOGIN_SECOND_FACTOR). Die Umbenennung ist Absicht: Aus \"wahr heisst drin\" ist \"wahr heisst vielleicht drin\" geworden, und ein Aufrufer, der die Umstellung nicht mitbekommen hat, soll an einem fehlenden Methodennamen scheitern statt an einer Zeichenkette, die als truthy durchgeht. Im Projekt gab es genau einen Aufrufer (public/login.php). Fehlversuche auf der zweiten Stufe laufen in dieselbe Konto- und IP-Sperre wie ein falsches Passwort.",
|
|
"action": null
|
|
},
|
|
{
|
|
"area": "webui",
|
|
"breaking": false,
|
|
"title": "Benutzerkonten lassen sich umbenennen",
|
|
"text": "System -> Benutzer kann jetzt auch den Anmeldenamen aendern, mit Kollisionspruefung und Protokolleintrag. Registrierte Hardwarekeys haengen an der Konto-ID und ueberstehen die Umbenennung; offene Sperrfenster des alten Namens werden aufgeraeumt, damit sie nicht spaeter einen gleichnamigen Versuch treffen. Beim Loeschen eines Kontos werden seine Schluessel mitgenommen - eine verwaiste Zeile haette den betroffenen Yubikey dauerhaft unbrauchbar gemacht, weil die Schluesselkennung geraeteweit eindeutig ist.",
|
|
"action": null
|
|
},
|
|
{
|
|
"area": "security",
|
|
"breaking": false,
|
|
"title": "CSRF-Pruefung fuer sitzungsauthentifizierte API-Schreibzugriffe",
|
|
"text": "ApiAuth akzeptiert die WebUI-Sitzung als Authentifizierung; bisher prueften die API-Endpunkte dabei kein CSRF-Token. Der Kommentar in Http::beginJson - diese Endpunkte authentifizierten \"ausschliesslich per Token-Header, niemals per Cookie\" - traf auf die API nie zu. Schreibende Anfragen im Sitzungsweg verlangen jetzt csrf_token bzw. den Kopf X-CSRF-Token. Token-Clients (Agenten, MCP-Adapter, CLI) sind nicht betroffen: Sie schicken keinen Cookie und kommen nie in diesen Zweig.",
|
|
"action": "Eigene Skripte, die sich auf die WebUI-Sitzung statt auf ein Token stuetzen, muessen das CSRF-Token mitschicken."
|
|
},
|
|
{
|
|
"area": "roadmap",
|
|
"breaking": false,
|
|
"title": "Roadmap-Tafel: Zeitstrahl nach Prioritaet",
|
|
"text": "Neue Unteransicht Roadmap -> Tafel. Spalten sind Phasen entlang von roadmap_rank, Zeilen sind Themenbereiche, Eintraege sind Kaertchen. Die Phasengrenzen stehen nicht im Code: gepflegt werden sie in der neuen Tabelle roadmap_phases, und ist dort nichts hinterlegt, schneidet die Tafel an den groessten Luecken der Rangfolge und weist die Grenzen sichtbar als geraten aus. Links die Spalte der erledigten Vorhaben (gekappt), rechts \"Wird ueberlegt\" mit Zurueckgestelltem, Ideen und allem ohne Rang. Nachpruefung (awaiting_verification, in_verification, fix_insufficient) ist ein eigener Zustand und wird nicht als erledigt gezeichnet.",
|
|
"action": null
|
|
},
|
|
{
|
|
"area": "roadmap",
|
|
"breaking": false,
|
|
"title": "Seitenpanel: bemerken und anmerken ohne Umweg",
|
|
"text": "Ein Klick auf ein Kaertchen oeffnet ein Panel mit Volltext, Verlauf, betroffenen Projekten und der Einordnung - ohne Seitenwechsel. Von dort laesst sich direkt kommentieren (mit action_taken) und Bereich, Rang, Zielfassung, Status und Ausloeser aendern. Das war der eigentliche Wunsch: Wer in der Uebersicht etwas bemerkte, musste sich bisher die Nummer merken und im Bugtracker-Tab danach suchen. Tiefe Verweise der Form #roadmap/<projekt>/item/<nummer> zeigen auf ein einzelnes Kaertchen, sodass ein Agent in einem Kommentar auf die Stelle zeigen kann.",
|
|
"action": null
|
|
},
|
|
{
|
|
"area": "roadmap",
|
|
"breaking": true,
|
|
"title": "roadmap_rank gilt projektweit - move und reorder ziehen nach (#177)",
|
|
"text": "Der Rang wurde an drei Stellen widerspruechlich behandelt: nextUp() sortierte projektweit, reorder() zaehlte je Bereich bei 10 neu durch, und die Werkzeugbeschreibung sagte \"Reihenfolge im Bereich\". Folge war, dass eine Geste innerhalb eines Bereichs die Position seiner Eintraege gegenueber allen anderen Bereichen still verschob. Jetzt gilt projektweit: move() sucht den Nachbarn ohne Bereichsfilter, und reorder() vertauscht nur noch die Raenge, die die genannten Eintraege ohnehin halten. Neu ist action=rank_between fuer das Ziehen auf der Tafel - es setzt den bewegten Eintrag auf die Mitte zwischen seinen neuen Nachbarn und nummeriert das Projekt nur dann einmal neu (unter Erhalt der Reihenfolge), wenn dort kein Platz mehr ist; die Antwort sagt es dann mit renumbered: true.",
|
|
"action": "Wer roadmap_rank bisher je Bereich ab 10 vergeben hat, sollte die Reihenfolge einmal durchsehen - sie war projektweit gelesen schon vorher mehrdeutig."
|
|
},
|
|
{
|
|
"area": "api",
|
|
"breaking": false,
|
|
"title": "Neue Endpunkte und Felder",
|
|
"text": "GET /api/bugtracker/v1/roadmap?action=board liefert die fertige Gliederung der Tafel ({phases[], lanes[], items[], stats, meta}) - damit koennen WebUI, CLI und ein spaeterer Export dieselbe Quelle benutzen, statt jeder fuer sich zu schneiden. POST ?action=rank_between sortiert ein Vorhaben zwischen zwei Nachbarn ein. ?action=epic nimmt zusaetzlich color und highlight; das Kennzeichen heisst \"hervorheben\" und nicht \"Sicherheit\", weil andere Projekte ihren wichtigen Bereich anders nennen. Nicht mitgeschickte Felder bleiben dabei unveraendert. POST /api/auth/v1/ traegt die vier WebAuthn-Zeremonien des Browsers.",
|
|
"action": null
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"version": "2.8.2",
|
|
"date": "2026-09-08",
|
|
"summary": "Sicherheits-Hardening: Proaktive Kontosperren nach 5 Fehlversuchen, Fail2Ban-Integration mit standardisiertem SecurityLogger, Rocket.Chat- und Webhook-Alarmierung bei Angriffen und Token-Brute-Force-Schutz für die API.",
|
|
"actionRequired": true,
|
|
"changes": [
|
|
{
|
|
"area": "auth",
|
|
"breaking": false,
|
|
"title": "Proaktive Accountsperren nach 5 Fehlversuchen",
|
|
"text": "dc_users fuehrt jetzt failed_login_count und locked_until (Migration 018). Nach fuenf aufeinanderfolgenden Fehlversuchen wird das Konto fuer 15 Minuten gesperrt. Gilt fuer das WebUI und die Setup-API. Erfolgreicher Login setzt den Zaehler zurueck; Administratoren koennen Konten im WebUI unter System -> Benutzer mit einem Klick entsperren.",
|
|
"action": null
|
|
},
|
|
{
|
|
"area": "security",
|
|
"breaking": false,
|
|
"title": "Fail2Ban-Standardisierung und dedizierter SecurityLogger",
|
|
"text": "Sicherheitsereignisse (AUTH_FAIL, ACCOUNT_LOCKED, IP_LOCKED, API_AUTH_FAIL, API_BRUTE_FORCE, RATE_LIMITED) werden im standardisierten Format in var/log/security.log geschrieben. Vorkonfigurierte Filter- und Jail-Dateien sowie Dokumentation liegen unter config/fail2ban/ und docs/FAIL2BAN.md bereit.",
|
|
"action": "Auf dem Host-Server den Fail2Ban-Filter deploymentcenter.conf und die Jail hinterlegen."
|
|
},
|
|
{
|
|
"area": "notify",
|
|
"breaking": false,
|
|
"title": "Rocket.Chat- und Webhook-Alarmierung bei Angriffen",
|
|
"text": "Wird ein Konto oder eine IP gesperrt oder ein Brute-Force-Angriff auf die API erkannt, erfolgt sofort eine Benachrichtigung im Rocket.Chat Alarm-Kanal (#DC-Alerts) sowie ueber das Webhook-Event security.lockout bzw. security.attack. Ein 15-Minuten-Cooldown verhindert Alert-Spam.",
|
|
"action": null
|
|
},
|
|
{
|
|
"area": "api",
|
|
"breaking": false,
|
|
"title": "API-Hardening und zentraler Core RateLimiter",
|
|
"text": "ApiAuth drosselt wiederholte ungueltige Token-Uebermittlungen pro IP (HTTP 429 bei >20 Fehlern in 5 Minuten). Ein universeller Core\\RateLimiter sichert Endpunkte ueber die neue Tabelle dc_rate_limits ab.",
|
|
"action": null
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"version": "2.8.1",
|
|
"date": "2026-08-31",
|
|
"summary": "Sechs Befunde aus der Fremdmeldung, und fast alle gehen auf dieselbe Bauart zurueck: eine Voreinstellung, die einsprang, wo die richtige Antwort bekannt gewesen waere - meist das Projekt, an das der Aufrufer ohnehin gebunden ist. dc_next_task antwortete durchgaengig mit 500. Meldungen ohne project_slug landeten im Scheinprojekt \"default\" und waren danach fuer ihren eigenen Melder gesperrt; Watchdog-Monitore landeten dort auf einem zweiten Weg. Der Lesepfad lieferte ohne Slug alle Projekte statt des eigenen - ausgerechnet beim ersten Aufruf jeder Sitzung, und in einer Groesse, die das Kontextfenster fuellte. Und wo er richtig verengte, sagte er es nicht.",
|
|
"actionRequired": true,
|
|
"changes": [
|
|
{
|
|
"area": "api",
|
|
"breaking": false,
|
|
"title": "dc_next_task antwortete immer mit HTTP 500",
|
|
"text": "POST manage?action=next scheiterte bei jedem Aufruf, mit und ohne Parameter, waehrend claim, release, list und stats am selben Endpunkt mit demselben Token durchliefen. Ursache war ein benannter Platzhalter, der in derselben Abfrage zweimal stand: BugRepo::buildFilter() setzte :project_scope fuer den Rueckkanal (fremde Items, die auf Nachpruefung warten) an zwei Stellen ein. Mit PDO::ATTR_EMULATE_PREPARES = false bereitet MySQL die Anweisung selbst vor, PDO bindet jeden Namen genau einmal - zwei Marker, ein Wert, SQLSTATE[HY093]. Getroffen war nur \"next\", weil nur dort der Rueckkanal-Filter gesetzt wird. Damit gab es keine exklusive Uebernahme; zwei Agenten konnten unbemerkt am selben Item arbeiten. Behoben durch zwei Platzhalter fuer denselben Wert.",
|
|
"action": "Wer wegen des 500ers ohne Uebernahme gearbeitet hat: laufende Arbeit jetzt mit dc_next_task absichern."
|
|
},
|
|
{
|
|
"area": "api",
|
|
"breaking": true,
|
|
"title": "Meldungen ohne project_slug gehen ins Heimatprojekt, nicht nach \"default\"",
|
|
"text": "POST /report legte eine Meldung ohne project_slug im Projekt \"default\" ab. Das ist kein Projekt: Es hat keine dc_projects-Zeile, project_id blieb NULL, und das Item tauchte in keiner Projektansicht auf. Schlimmer noch war die Folge - dasselbe Token, das die Meldung angelegt hatte, bekam beim naechsten Zugriff 403 project_forbidden, weil es an sein eigenes Projekt gebunden ist. Der Server legte Items dort ab, wo der Melder sie nicht mehr anfassen durfte, und die Antwort sah wie ein Erfolg aus. Betroffen war der dokumentierte Normalfall: Das Weglassen des Slugs ist ausdruecklich vorgesehen. Ursache war eine Bedingung, die nur Laufzeit-Token auf ihr Heimatprojekt festnagelte; ein Werkbank-Token ohne Slug fiel durch bis zur Voreinstellung. Jetzt gilt: genannter Slug vor Heimatprojekt, und ohne beides eine Absage (400 missing_project) statt eines Ratens. Ein ausdruecklich genanntes, unbekanntes Projekt wird ebenfalls abgewiesen (400 unknown_project) statt stillschweigend angelegt. Dieselbe Falltuer war in POST /api/errors/v1/report und im WebUI-Formular offen und ist dort mitgeschlossen.",
|
|
"action": "Bereits in \"default\" gelandete Items sind ueber die API nicht erreichbar und muessen mit Adminrechten umgehaengt oder geloescht werden."
|
|
},
|
|
{
|
|
"area": "api",
|
|
"breaking": true,
|
|
"title": "Ohne project_slug gilt das eigene Projekt - beim Lesen wie beim Melden",
|
|
"text": "dc_roadmap, dc_search, manage?action=list und ?action=stats lieferten ohne project_slug die Eintraege ALLER Projekte. Die Werkzeugbeschreibung sagte das Gegenteil (\"Ohne project_slug das eigene Projekt\"), was den Widerspruch lange verdeckt hat. Grund war eine Voreinstellung auf \"all\": Ein Werkbank-Token ist an ein Projekt gebunden, faellt beim Lesen aber absichtlich nicht unter die Projektschranke - es soll die Landschaft sehen duerfen. Damit war der Zwangsfilter leer, die Vorgabe griff, und ein Agent bekam einen fremden Backlog vorgelegt, den er nach derselben Regel nicht bearbeiten darf. Da CLAUDE.md dc_roadmap zum ersten Aufruf jeder Sitzung macht, war das der erste Eindruck. Jetzt gilt: genannter Slug, sonst Heimatprojekt, und erst ohne beides \"alle\". project_slug=all bleibt moeglich und ist damit eine Ansage statt eines Nebeneffekts.",
|
|
"action": "Wer bisher auf die projektuebergreifende Antwort gebaut hat, schickt jetzt ausdruecklich project_slug=all."
|
|
},
|
|
{
|
|
"area": "mcp",
|
|
"breaking": true,
|
|
"title": "dc_roadmap antwortet schmal statt vollstaendig",
|
|
"text": "Die Roadmap lieferte je Eintrag den vollen Datensatz mit rund fuenfzig Feldern, darunter description, stack_trace, evidence und context_json - fuer 57 Eintraege rund 137.000 Zeichen. Ausgerechnet der erste Aufruf einer Sitzung fuellte damit das Kontextfenster, bevor die Arbeit begann. Eine Uebersicht beantwortet \"was ist dran und wer sitzt dran\"; dafuer braucht es keinen Stacktrace. dc_roadmap liefert deshalb standardmaessig die Uebersichtsfelder (BugRepo::COMPACT_FIELDS); Einzelheiten holt dc_get_item, die Begruendung steht ohnehin hinter doc_anchor. fields=full liefert weiterhin alles, dc_search hat fields=compact als Gegenstueck.",
|
|
"action": null
|
|
},
|
|
{
|
|
"area": "testkit",
|
|
"breaking": false,
|
|
"title": "dc-testkit 1.1.1 - und ein Riegel gegen die doppelt ausgelieferte Fassung",
|
|
"text": "1.1.0 wurde zweimal ausgeliefert: mit 1898 und, im Zuge von 2.8.0, mit 1930 Zeilen. Der Unterschied sass vollstaendig in cmd_confirm - die aeltere Kopie kannte nur den Weg ueber status=closed, nicht den ueber verified_in_build. Jede Nachpruefung von so einer Maschine landete still im alten Verfahren, und selfupdate sah \"gleiche Fassung\" und tat nichts. Dasselbe war 1.0.0 schon passiert (1215 und 1335 Zeilen). Der Absatz ueber DC_TESTKIT_VERSION stand die ganze Zeit im Skript; er hat nicht getragen. scripts/deploy.py vergleicht jetzt vor dem Hochladen gegen den zuletzt ausgelieferten Stand und bricht ab, wenn sich der Inhalt geaendert hat, waehrend die Fassungsnummer stehen blieb.",
|
|
"action": "Auf allen Testmaschinen dc-testkit selfupdate ausfuehren; erwartet wird danach 1.1.1."
|
|
},
|
|
{
|
|
"area": "docs",
|
|
"breaking": false,
|
|
"title": "Die SDK-Fassung wird gemeldet, nicht abgeschrieben",
|
|
"text": "Die Agentenvorlage zeigte die Zeile \"Deploymentcenter-Anbindung: SDK 2.5.1\" mit einer konkreten Zahl. Sie war als Beispiel gemeint und wurde als Wert uebernommen - in Projekt-CLAUDE.md-Dateien stand danach 2.5.1, waehrend dc_link_project laengst 2.6.0 auswies. Eine Angabe, die an N Stellen von Hand nachgezogen werden muss, ist nach dem ersten Vergessen falsch, und zwar schweigend. Die Vorlage nennt jetzt dc_link_project als die belastbare Auskunft und benutzt sichtbare Platzhalter, wo eine Fassung hingehoert.",
|
|
"action": null
|
|
},
|
|
{
|
|
"area": "api",
|
|
"breaking": false,
|
|
"title": "Watchdog-Monitore erben das Projekt ihres Ping-Tokens",
|
|
"text": "watchdog_monitors.project_slug gibt es seit Migration 014, geschrieben hat die Spalte nie jemand: upsertHeartbeat() bekam das Projekt gar nicht erst uebergeben. Der Monitor entstand mit project_slug NULL, und jeder daraus abgeleitete Supervisor-Befund fiel auf das Scheinprojekt \"default\" zurueck - dieselbe Endstation wie bei den Meldungen ohne project_slug, nur ueber einen anderen Weg. Wirkung: dc_findings(project_slug=...) lieferte fuer eine laufende, meldende Instanz null Befunde. Wer dem Leitfaden folgt und dort anfaengt, schliesst daraus \"nichts auffaellig\" - waehrend ein deviation.self-Befund mit elf Vorkommnissen bereitstand. Die Zuordnung war dabei die ganze Zeit bekannt: Das Ping-Token ist an ein Projekt gebunden, und authorizeSource() liest die Bindung ohnehin - sie wurde nur nicht weitergereicht. Das Projekt kommt jetzt aus der Token-Bindung, nie aus dem Body, und ein Ping ohne Bindung (Shared Key, Alt-Token) ueberschreibt eine bekannte Zuordnung nicht. Die Ping-Antwort nennt project_slug.",
|
|
"action": "Bestandsmonitore behalten project_slug NULL, bis ihre Anwendung das naechste Mal mit einem projektgebundenen Token pingt."
|
|
},
|
|
{
|
|
"area": "api",
|
|
"breaking": false,
|
|
"title": "Ein verengter Leseweg sagt jetzt, dass er verengt hat",
|
|
"text": "Ein an Projekt X gebundenes Token, das nach Projekt Y fragte, bekam beim Schreiben eine klare Absage (403 project_forbidden) und beim Lesen HTTP 200 - mit den Items von X und einem Umschlag, in dem der Projektfilter bereits auf X umgeschrieben war. Die Antwort sah damit aus wie eine gueltige Auskunft ueber Y. Das zaehlt, weil die Prompt-Vorlage jeden Agenten vor jeder Meldung durch dc_search schickt (\"gibt es das schon?\"): Auf \"gibt es das im Deploymentcenter schon?\" kam eine leere, plausibel aussehende Antwort - und das Duplikat wurde angelegt. Einem Melder ist genau das beinahe passiert; tatsaechlich waren es 71 Items. Die Schranke selbst bleibt unveraendert; list, stats und die Roadmap legen bei einer Verengung jetzt scoped_to und scope_note in den Umschlag. Ohne Verengung steht dort nichts - ein Hinweis, der immer kommt, wird nicht gelesen.",
|
|
"action": null
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"version": "2.8.0",
|
|
"date": "2026-08-31",
|
|
"summary": "Remote-Testen, das traegt. Die Testwege waren beschrieben, aber nicht begehbar: Auf der Test-VM lag die Anleitung von vor zwei Fassungen, ein Token ohne die noetigen Rechte und ein Testkit, das aelter war als das ausgelieferte - beide meldeten dieselbe Fassungsnummer. Deshalb sagte doctor dort \"Bereit\", waehrend kein einziger Auslieferungstest laufen konnte. Dazu die Trennung, die dem Pruefverfahren bisher fehlte: bugtracker:verify erlaubt, eine Nachpruefung zu bestaetigen, ohne zu erlauben, den Fehler selbst zu beheben.",
|
|
"actionRequired": false,
|
|
"changes": [
|
|
{
|
|
"area": "server",
|
|
"breaking": false,
|
|
"title": "bugtracker:verify - pruefen duerfen, ohne beheben zu duerfen",
|
|
"text": "bugtracker:manage war ein einziger Schreib-Scope: Wer eine Nachpruefung bestaetigen durfte, durfte damit auch resolve, update, bulk_update und die Roadmap umschreiben. Fuer einen Pruefstand ist das zu viel - und wer einen Fix schreibt und ihn selbst abnimmt, prueft gegen die Erwartung, die er beim Schreiben hatte. Genau dagegen ist das Verfahren aus 2.6.1 gebaut, aber die Grenze stand nur in der Anleitung. bugtracker:verify deckt jetzt ab, was ein Pruefer braucht: uebernehmen, kommentieren, bestaetigen, zurueckgeben. bugtracker:manage schliesst es ein, vorhandene Token verlieren also nichts. \"status\" bleibt bewusst bei manage - darueber liesse sich sonst jeder Zustand setzen und die Trennung umgehen."
|
|
},
|
|
{
|
|
"area": "server",
|
|
"breaking": false,
|
|
"title": "confirm ist eine eigene Aktion und hinterlaesst, wogegen geprueft wurde",
|
|
"text": "Eine bestaetigte Nachpruefung war im Datenbestand nicht von \"jemand hat das Item zugemacht\" zu unterscheiden: dc-testkit confirm setzte status=closed und schrieb den Rest in einen Kommentar. Damit war die eine Frage, fuer die das Verfahren gebaut ist, nicht beantwortbar - wurde dieses Item je gegen ein ausgeliefertes Paket geprueft? Neu: POST .../manage?action=confirm mit verified_in_build. Migration 017 legt verified_at, verified_in_build und verified_by an. Weicht die geprueften Fassung von resolved_in_build ab, wurde gegen etwas anderes geprueft als angekuendigt, und man sieht es. Bestaetigen laesst sich nur, was als behoben ausgegeben wurde; ein offenes Item zu \"bestaetigen\" antwortet 409 not_verifiable."
|
|
},
|
|
{
|
|
"area": "mcp",
|
|
"breaking": false,
|
|
"title": "dc_confirm - Pruefauftraege liessen sich annehmen, aber nicht abschliessen",
|
|
"text": "Der Adapter hatte dc_next_task und dc_resolve, aber keinen Weg, eine Nachpruefung zu bestaetigen; dc_comment kennt nur action_taken, keinen Status. Ein Pruefagent ueber MCP konnte einen Prueffauftrag also uebernehmen und dann nichts damit anfangen. Jetzt 16 Werkzeuge statt 15."
|
|
},
|
|
{
|
|
"area": "testkit",
|
|
"breaking": false,
|
|
"title": "dc-testkit 1.1.0: kennt seinen eigenen Stand, prueft Rechte und Display",
|
|
"text": "Vier Fehler: doctor brach unter \"set -e\" wortlos nach der Health-Pruefung ab, sobald der Server neu genug war - Token und Projekt wurden nie geprueft. Die Konfigurationsdatei ueberschrieb die Umgebung statt umgekehrt, \"DC_PROJECT=x dc-testkit report\" meldete also stillschweigend in das Projekt aus der Datei. Der Bericht ging teils auf stderr und stand ueber SSH unter der falschen Ueberschrift. Und \"~/bin\" liegt ueber SSH nicht im PATH, weil .profile nur von einer Login-Shell gelesen wird. Neu: selfupdate (mit Richtungspruefung - eine neuere lokale Fassung ist ein Entwicklungsstand und kein Grund zum Update), doctor prueft den eigenen Stand, die Token-Herkunft, die fehlende Projektbindung und das eigene Display, sowie display start|stop|status und shot fuer die Oberflaechenpruefung."
|
|
},
|
|
{
|
|
"area": "testkit",
|
|
"breaking": false,
|
|
"title": "dc-testkit-remote: der Agent laeuft auf dem Arbeitsplatz, der Test auf der VM",
|
|
"text": "Claude Code laesst sich auf der Test-VM nicht betreiben, Agenten arbeiten also ueber SSH von aussen - dort liegen auch die Projekt-Repos und damit die Werkbank-Token in deren .mcp.json. Der Wrapper haelt das Token aus \"ps aux\" der geteilten Maschine (es geht ueber die Standardeingabe in die Umgebung), ruft bash -l, schuetzt Argumente vor drei Ebenen Quoting und buendelt ganze Pruefplaene in eine Verbindung. Auf der VM wird keine Konfiguration mehr gelesen: Sonst gilt drueben, was irgendwann jemand eingetragen hat - genau daran ist es gescheitert."
|
|
},
|
|
{
|
|
"area": "security",
|
|
"breaking": false,
|
|
"title": "Interne Anleitungen bleiben aus dem Webroot",
|
|
"text": "docs/TESTUMGEBUNG.md und docs/TESTINSTANZ.md gingen bei jedem Deployment mit hoch. Erreichbar waren sie dort nicht - aber nur, weil die Rewrite-Regel ^docs/ auf public/docs/ umleitet und dort nichts Gleichnamiges liegt. Faellt mod_rewrite aus oder aendert jemand die Reihenfolge, liegen interne Adresse, Benutzername und der Weg zur sudo-Freischaltung offen. Beide Dateien sagen in ihrem eigenen Kopf, dass sie da nicht hingehoeren; deploy.py schliesst sie jetzt pfadgenau aus."
|
|
},
|
|
{
|
|
"area": "docs",
|
|
"breaking": false,
|
|
"title": "Baustein E: ein Agent, der nur prueft",
|
|
"text": "Die Prompt-Vorlage hatte nur den Entwickleragenten, der nebenbei testet. Baustein E beschreibt das Umgekehrte und zieht die Rollengrenze ausdruecklich: kein resolve, keine Roadmap, und bei fehlendem Token eine Meldung statt eines Ratespiels - .mcp.json ist gitignored und kommt ueber Git nie mit. TESTUMGEBUNG.md 4.8 beschreibt die Rolle, 4.2 den tatsaechlichen Ablageort der Token, und die Belegung in 7 ist von der Maschine erhoben statt fortgeschrieben - sie war vier Projekte im Rueckstand."
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"version": "2.7.1",
|
|
"date": "2026-08-30",
|
|
"summary": "Aufraeumen nach dem ersten Durchlauf der Testinstanz: Was dort auffiel, war fast durchweg dieselbe Sorte Fehler - eine Auskunft, die nicht stimmte. Der Server nannte eine falsche Fassung, doctor meldete Bereitschaft ohne zu pruefen, /api/health starb an dem Zustand, ueber den es Auskunft geben sollte, und das WebUI zeigte nach jedem Filter eine andere Ansicht als die verlangte.",
|
|
"actionRequired": false,
|
|
"changes": [
|
|
{
|
|
"area": "server",
|
|
"breaking": false,
|
|
"title": "Die gemeldete Fassung kommt aus dem Changelog statt aus der Konfiguration",
|
|
"text": "config/config.php fuehrte 'version' als Zeichenkette, und die stand seit jeher auf 2.0.0 - waehrend 2.7.0 lief. Das war keine Schoenheitsfrage: /api/health meldete 2.0.0, selbst gemeldete Fehler trugen eine falsche build_version, und jede Pruefung gegen eine Mindestfassung schlug auf einem aktuellen Server fehl. Genau daraus entstand der Eindruck, die Testinstanz laufe auf veralteter Software. src/bootstrap.php liest die Fassung jetzt aus 'current' in public/docs/changelog.json - der Datei, die bei jedem Release ohnehin gepflegt wird. Der Wert in der Konfiguration bleibt Rueckfallebene."
|
|
},
|
|
{
|
|
"area": "api",
|
|
"breaking": false,
|
|
"title": "/api/health ueberlebt eine leere Datenbank",
|
|
"text": "Auf einer frisch aufgesetzten Instanz - Anwendung deployt, Migrationen noch nicht gelaufen - endete GET /api/health mit HTTP 500: die Rechtepruefung griff auf dc_tokens zu, und diese Tabelle gab es noch nicht. Ausgerechnet der Endpunkt, der ueber kaputte Zustaende Auskunft geben soll, fiel bei einem kaputten Zustand aus. Neu ist eine eigene Pruefung 'schema': fehlt das Schema, kommt healthy=false mit dem Hinweis auf install_db.php statt eines Serverfehlers. Die Rechtepruefung selbst ist zusaetzlich abgesichert, damit auch ein halb migrierter Stand eine auswertbare Antwort liefert."
|
|
},
|
|
{
|
|
"area": "api",
|
|
"breaking": false,
|
|
"title": "Neu: GET /api/tokens/v1/whoami - Auskunft ueber das eigene Token",
|
|
"text": "Meldet Besitzerart, Projektbindung, Rechte, Umgebung und Frist des uebergebenen Tokens; der Klartextwert wird nicht ausgeliefert, ein widerrufenes oder abgelaufenes Token bekommt 401. Bis hierher liess sich ein Recht nur durch Ausprobieren feststellen - und das hat Nebenwirkungen: eine Probe auf bugtracker:manage uebernaehme ein Item, eine auf license:dev stellte einen Schluessel aus. Deshalb pruefte kein Werkzeug seine Voraussetzungen, sondern lief hinein. 'is_workbench' ist der Wert, der ueber Fremdmeldungen entscheidet."
|
|
},
|
|
{
|
|
"area": "tooling",
|
|
"breaking": false,
|
|
"title": "dc-testkit doctor prueft Mindestfassung und Rechte, statt sie nur anzuzeigen",
|
|
"text": "doctor meldete 'Bereit.', obwohl der Server die benoetigten Endpunkte nicht hatte und das Token die noetigen Rechte nicht trug - der Lauf scheiterte dann beim ersten Schritt, der beides wirklich brauchte. Die Fassung stand dabei schon in der Ausgabe, verglichen wurde sie nur nie. Jetzt: Vergleich gegen DC_MIN_SERVER (2.6.1) nach semantischer Ordnung, und ueber /api/tokens/v1/whoami die Pruefung auf Besitzerart dev_agent sowie auf bugtracker:manage, license:dev und updateservice:read - fehlende Rechte werden einzeln benannt. Gehoert das Token zu einem anderen Projekt als dem bearbeiteten, sagt doctor das ebenfalls. Gegen einen aelteren Server faellt er auf die grobe Pruefung zurueck und weist ausdruecklich darauf hin, dass die Rechte ungeprueft blieben."
|
|
},
|
|
{
|
|
"area": "betrieb",
|
|
"breaking": false,
|
|
"title": "dc-testkit.sh: erreichbar unter /testkit/ und mit Unix-Zeilenenden",
|
|
"text": "Zwei Fehler auf demselben Weg. Erstens fehlte die Route: TESTUMGEBUNG.md nennt 'curl https://dc.mhdf.de/testkit/dc-testkit.sh', ausgeliefert war die Datei aber nur unter /public/testkit/ - der dokumentierte Bezugsweg hat nie funktioniert und wurde als 'Server noch zu alt' gedeutet. Zweitens die Zeilenenden: .gitattributes fuehrt *.sh als eol=lf, normalisiert wird aber erst beim Einchecken - hochgeladen wird der Arbeitsbaum. Von einem Windows-Rechner aus landete das Skript deshalb mit CRLF im Webroot und scheiterte auf Linux mit 'env: bash\\r: No such file or directory'. scripts/deploy.py und scripts/deploy_dev.py entfernen das CR bei .sh und .bash jetzt beim Hochladen, unabhaengig vom Zustand der Arbeitskopie."
|
|
},
|
|
{
|
|
"area": "webui",
|
|
"breaking": false,
|
|
"title": "Filter und Formulare werfen nicht mehr aufs Dashboard zurueck",
|
|
"text": "Welche Ansicht sichtbar ist, hing allein am Anker der URL - und die Ansichtsrettung, die das auffangen sollte, griff aus zwei Gruenden nicht.\n\nErstens fragte sie dokumentweit nach der offenen Unteransicht. Im Markup tragen aber ACHT Unteransichten die Klasse \"active\": jedes Modul hat seine eigene Startansicht, und switchMainTab() raeumt nur innerhalb des Moduls auf, das es oeffnet. Zurueck kam deshalb immer die erste in Dokumentreihenfolge, unabhaengig davon, was gerade sichtbar war. Dass die Rettung trotzdem zu funktionieren schien, lag an den wenigen Formularen, die ihren Anker fest in der action stehen haben. Gesucht wird jetzt erst das aktive Modul und darin die Unteransicht.\n\nZweitens fielen die Filter durch das Netz: Es sind GET-Formulare, und ein GET-Formular verwirft beim Absenden den Anker seiner action; ausserdem loesten sie ueber form.submit() aus, was gar kein submit-Ereignis erzeugt. Der zentrale Zuhoerer haengt die offene Ansicht bei GET jetzt als Parameter \"view\" an den Query-String, ausgeloest wird ueber requestSubmit(), und beim Laden wird die Ansicht daraus wiederhergestellt. Die drei Filterformulare tragen das Feld zusaetzlich fest im Markup - dort ist ohne Zweifel bekannt, wo sie stehen, und das haengt an keiner DOM-Abfrage mehr."
|
|
},
|
|
{
|
|
"area": "api",
|
|
"breaking": false,
|
|
"title": "/api/bugtracker/v1/roadmap ist jetzt auch ohne .php erreichbar",
|
|
"text": "Jeder andere Endpunkt kommt ohne Dateiendung aus, dieser nicht: Die Route fehlte in der .htaccess, der Aufruf lief in ein 404, und die OpenAPI-Beschreibung fuehrte deshalb als einzigen Pfad einen mit .php. Wer dem Muster der uebrigen API folgte, lief auf. Die Route ist nachgetragen, die Beschreibung nennt jetzt den Pfad ohne Endung. Der alte Pfad mit .php funktioniert unveraendert weiter - er faellt auf die allgemeine Regel fuer /api/ zurueck."
|
|
},
|
|
{
|
|
"area": "webui",
|
|
"breaking": false,
|
|
"title": "Die Suche findet Eintraege ueber ihre Nummer",
|
|
"text": "Die Nummer ist das, was ueberall zitiert wird - in Commits, in Kommentaren, im Changelog - und war ausgerechnet ueber die Suche nicht auffindbar: Sie steht in keiner der durchsuchten Spalten. Wer \"288\" eingab, bekam nichts und musste die Liste durchblaettern. Jetzt sucht die Nummer mit, im Bugtracker wie im Fehler-Stream und ebenso ueber den Parameter search der API. Zwei Schreibweisen, zwei Bedeutungen: \"288\" findet das Item mit dieser Nummer UND alles, was die Zahl im Text stehen hat; \"#288\" ist eindeutig gemeint und liefert ausschliesslich dieses eine Item."
|
|
},
|
|
{
|
|
"area": "api",
|
|
"breaking": false,
|
|
"title": "Der Parameter search ist jetzt beschrieben",
|
|
"text": "action=list kennt search seit jeher, in der OpenAPI-Beschreibung stand er nicht. Nachgetragen, mitsamt dem Verhalten bei Zahlen."
|
|
},
|
|
{
|
|
"area": "webui",
|
|
"breaking": false,
|
|
"title": "Statusfilter als Mehrfachauswahl, mit Vorgabe auf das Laufende",
|
|
"text": "Vorher liess sich genau eine Lage waehlen; wer \"offen und in Nachpruefung\" sehen wollte, brauchte eine vorgefertigte Kombination - stand sie nicht in der Liste, ging es nicht. Jetzt Ankreuzfelder, gruppiert nach Laufend / Verlangt Aufmerksamkeit / Abgeschlossen. Angehakt ist ab Werk, was laeuft oder Aufmerksamkeit verlangt (open, planned, in_progress, triage_external, awaiting_verification, in_verification, fix_insufficient); Erledigtes, Geschlossenes und Abgelehntes bleibt draussen - es ist der groesste Teil des Bestandes und verdeckt sonst das, was noch zu tun ist. \"deferred\" fehlt mit Absicht: Der Sinn dieses Status ist \"nicht jetzt\". Kein Haken bedeutet keine Einschraenkung; das versteckte Feld im Formular macht \"alle Haken entfernt\" von \"erster Aufruf\" unterscheidbar, denn HTML sendet fuer leere Ankreuzfelder nichts."
|
|
},
|
|
{
|
|
"area": "webui",
|
|
"breaking": false,
|
|
"title": "Die Suche sagt, wenn ein Filter Treffer verschluckt",
|
|
"text": "Eine Suche, die schweigt, obwohl sie etwas gefunden hat, ist schlimmer als gar keine Suche: Wer \"288\" eingibt und nichts bekommt, schliesst daraus, es gebe den Eintrag nicht - dabei ist er nur erledigt und deshalb ausgeblendet. Bei aktiver Suche wird jetzt zusaetzlich gezaehlt, wie viele Treffer die aktuellen Einschraenkungen wegnehmen, und das Ergebnis steht als Hinweis ueber der Liste, mit einem Verweis \"Ueberall suchen\". Die zweite Abfrage laeuft nur bei aktiver Suche und holt eine einzige Zeile."
|
|
},
|
|
{
|
|
"area": "webui",
|
|
"breaking": false,
|
|
"title": "Roadmap-Eintraege stehen nicht mehr in der Bugliste",
|
|
"text": "Seit 2.7.0 IST ein Roadmap-Eintrag ein bugtracker_item - richtig so, es gibt keine zweite Ablage. Nur landet damit die gesamte Planung aller Projekte in derselben Liste wie die Meldungen: Nach der Uebernahme der Projekt-Roadmaps waren 217 von 229 Feature Requests Roadmap-Eintraege, und die zwoelf Vorschlaege, um die es in dieser Liste geht, lagen darunter begraben. Die Bugliste zeigt jetzt ab Werk nur, was NICHT auf der Roadmap steht (Kennzeichen ist unveraendert der Themenbereich). Sichtbar bleibt beides, nur an getrennten Orten; der neue Filter holt die Planung jederzeit wieder herein, und die Suche weist auf verdeckte Treffer hin."
|
|
},
|
|
{
|
|
"area": "docs",
|
|
"breaking": false,
|
|
"title": "Was ein Feature Request ist - und was nicht",
|
|
"text": "Ein feature_request ist ein Vorschlag an ein FREMDES Projekt: etwas, das dort gebaut werden muesste, damit die Zusammenarbeit besser wird. Die eigene Planung ist es nicht - die gehoert als Roadmap-Eintrag in den Tracker, also mit Themenbereich. Der Unterschied war nirgends aufgeschrieben und ist am type auch nicht ablesbar, weil der nur bug und feature_request kennt; unterschieden wird am Themenbereich. Steht jetzt in der Agent-Vorlage, im Agenten-Handbuch und in der OpenAPI-Beschreibung. ROADMAP_MIGRATION.md warnt zusaetzlich, dass Schritt 3 und 4 zusammengehoeren: Wer nach dem Anlegen abbricht, hinterlaesst Dutzende vermeintlicher Feature Requests im Eingang des Projekts."
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"version": "2.7.0",
|
|
"date": "2026-08-29",
|
|
"summary": "Die Roadmap zieht in das Deploymentcenter: Planungsstand, Reihenfolge und Zustaendigkeit stehen dort, wo sie ohnehin gefuehrt werden, und nicht mehr in handgepflegten Markdown-Dateien daneben. Die Dateien behalten, was eine Tabellenzelle nicht traegt - die Begruendung.",
|
|
"actionRequired": true,
|
|
"changes": [
|
|
{
|
|
"area": "api",
|
|
"breaking": false,
|
|
"title": "Roadmap als Sicht auf den Bugtracker - keine zweite Ablage",
|
|
"text": "Migration 016 legt KEINE Roadmap-Tabelle an. Ein Roadmap-Eintrag IST ein bugtracker_item; es fehlten nur vier Angaben, die ein Backlog nicht braucht und eine Planung schon: epic (Themenbereich), roadmap_rank (Reihenfolge - Schweregrad ist keine Prioritaet), target_build (Zielfassung VOR der Umsetzung, abgegrenzt von resolved_in_build danach) und defer_trigger. Dazu doc_anchor: der Verweis auf die Stelle in der Projektdatei, an der die Begruendung steht. Neu ausserdem der Status \"deferred\" und die winzige Tabelle roadmap_epics fuer die Bereichstitel - stuenden die an jedem Item, waeren sie in jeder Zeile anders schreibbar. Die Zugehoerigkeit ist EIN Feld: Ein Item steht auf der Roadmap, wenn es ein epic hat. Ein zweites Kennzeichen daneben waeren zwei Angaben, die dasselbe behaupten und auseinanderlaufen koennen.",
|
|
"action": "Migration 016 ausfuehren. Wer eigene Auswertungen auf bugtracker_items.status hat, muss \"deferred\" einordnen: Es zaehlt NICHT als offener Punkt und taucht bewusst nicht in ?action=next auf - genau das ist sein Zweck. Abzugrenzen von \"rejected\": verworfen heisst \"nicht erneut vorschlagen\", zurueckgestellt heisst \"es gibt einen Anlass, an dem es wieder ansteht\"."
|
|
},
|
|
{
|
|
"area": "api",
|
|
"breaking": false,
|
|
"title": "Zurueckstellen ohne Ausloeser wird abgelehnt",
|
|
"text": "status=deferred verlangt defer_trigger mit mindestens 10 Zeichen: Woran erkennt man, dass \"spaeter\" jetzt ist? Ohne benannten Anlass ist ein Vorhaben nicht zurueckgestellt, sondern vergessen - und ein vergessener Punkt sieht in jeder Liste aus wie ein geplanter. Die Regel sitzt im Repository und gilt damit auf allen drei Wegen gleich (WebUI, /manage?action=update, roadmap.php?action=set); in einem Formular allein waere sie in dem Moment wirkungslos, in dem ein Agent die API benutzt. Abgelehnt wird mit 400 invalid_update statt 404 - ein fehlender Ausloeser ist kein fehlendes Item. Ein zurueckgestelltes Item verliert ausserdem seine Lease: Wer etwas vertagt, arbeitet nicht mehr daran.",
|
|
"action": null
|
|
},
|
|
{
|
|
"area": "api",
|
|
"breaking": false,
|
|
"title": "Neuer Endpunkt /api/bugtracker/v1/roadmap.php",
|
|
"text": "Lesend (bugtracker:read): action=list liefert die Eintraege nach Bereich und Reihenfolge, epics die Bereiche samt Zaehlung je Lage, next die vorderen Eintraege der Warteschlange, active wer gerade welches Item haelt, render den erzeugten Markdown-Block, check den Abgleich einer mitgeschickten Datei. Schreibend (bugtracker:manage, nur im eigenen Projekt): set, move, reorder, epic. Der status-Parameter nimmt eine Lage statt eines ENUM-Werts - aktiv, offen, arbeit, erledigt, zurueckgestellt, verworfen; zwoelf Status sind fuer die Ablage richtig und fuer eine Auswahlliste zu viele. Angelegt wird weiterhin ueber /report und abgeschlossen ueber /manage: Ein zweiter Schreibweg auf dieselben Spalten waere eine zweite Stelle, an der Regeln gelten muessten.",
|
|
"action": null
|
|
},
|
|
{
|
|
"area": "mcp",
|
|
"breaking": false,
|
|
"title": "dc_roadmap und dc_roadmap_set - der Planungsstand als Werkzeug",
|
|
"text": "Der Adapter hat jetzt 15 Werkzeuge. dc_roadmap beantwortet zu Sitzungsbeginn die zwei Fragen, die man sich sonst falsch beantwortet: was ist als Naechstes dran, und arbeitet schon jemand daran. dc_roadmap_set hebt ein Item auf die Roadmap (epic setzen), reiht es ein und traegt die Zielfassung nach. Beide bewusst getrennt von dc_report: Anlegen und Einplanen sind zwei Entscheidungen, und ein Werkzeug, das beides zugleich tut, trifft die zweite ungefragt mit.",
|
|
"action": "In den Projekten mit eingerichtetem Adapter den Roadmap-Abschnitt aus docs/AGENT_PROMPT_TEMPLATE.md in CLAUDE.md bzw. AGENTS.md uebernehmen. Ohne ihn weiss ein Agent nicht, dass er vor der Arbeit nachsehen soll."
|
|
},
|
|
{
|
|
"area": "webui",
|
|
"breaking": false,
|
|
"title": "Neues Modul \"Roadmap\" mit Filtern nach Bereich, Lage, Zielfassung und Agent",
|
|
"text": "Vier Unteransichten: Uebersicht (Kennzahlen, \"Als Naechstes\", wer arbeitet gerade woran, Fortschritt je Themenbereich), Alle Vorhaben (die Liste mit Reihenfolge-Pfeilen und Bearbeitungsdialog), Aufnahmeliste (offene Vorhaben ohne Bereich - der Weg vom Backlog auf die Planung) und Themenbereiche. Gefiltert wird serverseitig ueber GET-Parameter, die Ansicht ist damit als Link teilbar. Voreinstellung der Lage ist \"aktiv\" und nicht \"alle\": Eine Roadmap, die mit sechzig erledigten Eintraegen beginnt, beantwortet die Frage nicht, fuer die man sie aufschlaegt. Der Fortschrittsbalken rechnet Zurueckgestelltes und Verworfenes aus dem Nenner heraus - ein Bereich, in dem die Haelfte verworfen wurde, ist nicht zur Haelfte fertig, er ist kleiner geworden.",
|
|
"action": null
|
|
},
|
|
{
|
|
"area": "cli",
|
|
"breaking": false,
|
|
"title": "cli/roadmap.php: Zustandsblock erzeugen, Prosa abgleichen",
|
|
"text": "Die Antwort auf die Frage, wie Datenbank und Markdown-Datei zusammenbleiben, ist nicht \"besser aufpassen\", sondern zweigeteilt. --write ERZEUGT den Zustandsteil der Datei zwischen <!-- dc:roadmap -->-Markern; dort kann nichts driften, weil dort niemand tippt, und ausserhalb wird nichts angefasst. --check prueft, was von Hand geschrieben ist: Verweist ein Absatz auf ein Item, das es nicht gibt (unknown_item)? Gehoert es einem anderen Projekt (foreign_item)? Fehlt zu einer zurueckgestellten Entscheidung die Begruendung (decision_undocumented)? Rueckgabewert 1 bei Fehlern, damit sich der Aufruf in deploy.py haengen laesst. Ein Item OHNE Absatz ist bewusst kein Befund - nicht jedes Vorhaben braucht eine Herleitung. Handvergebene Kennungen entfallen: Die Kennung ist die Item-ID, denn AUTO_INCREMENT vergibt nie zweimal dieselbe Nummer, waehrend ein neu vergebenes \"A12\" auf beiden Seiten stimmig aussieht und Verschiedenes meint.",
|
|
"action": "Vorhandene ROADMAP.md, TODO.md oder PLAN.md je Projekt einmalig uebernehmen. Sechs Schritte, beschrieben in docs/ROADMAP_MIGRATION.md."
|
|
},
|
|
{
|
|
"area": "api",
|
|
"breaking": true,
|
|
"title": "observe und status: der Zustand des Befundes heisst jetzt finding_status",
|
|
"text": "Beide Endpunkte legten das Feld \"status\" des Befundes in dieselbe Ebene wie das Umschlagfeld \"status\" der API. Http::ok() arbeitet mit array_merge(['status' => 'success'], $data) - der Wert aus dem Body gewann. Eine angenommene Meldung kam beim Client also als status=\"open\" an. Jeder Client, der wie ueberall sonst auf status === \"success\" prueft, hielt eine erfolgreiche Meldung fuer einen Fehlschlag, und zwar ausgerechnet an dem Endpunkt, den ein meldender Client am haeufigsten aufruft. Das Feld heisst jetzt finding_status; der Umschlag sagt wieder \"success\". Zwei Testfaelle halten beides fest - dass der Umschlag stimmt UND dass die Angabe nicht einfach verschwunden ist, denn ein geloeschtes Feld haette denselben Test bestanden.",
|
|
"action": "Wer die Antwort von /api/supervisor/v1/observe oder /status auswertet, liest den Befundzustand nun aus finding_status. In der Praxis war das Feld vorher nicht benutzbar - es kollidierte ja gerade mit dem Umschlag."
|
|
},
|
|
{
|
|
"area": "betrieb",
|
|
"breaking": false,
|
|
"title": "Testinstanz dc-dev.mhdf.de und ein Testtor vor jedem Produktiv-Upload",
|
|
"text": "Eine vollstaendige zweite Installation, die jederzeit geloescht und neu aufgebaut werden darf: eigene Datenbank, eigene Schluessel, Basic-Auth davor, Rocket.Chat aus. Dazu 124 Testfaelle in tests/e2e gegen API, Rechte, Migrationen und die Zugangssperre. scripts/deploy.py bringt vor jedem Upload erst den anstehenden Stand auf die Testinstanz und laesst dann den Testlauf darauf laufen; erst bei Rueckgabewert 0 geht etwas auf die Produktion (--ohne-test ist der Notausgang). Die Reihenfolge ist der Punkt: Liefe der Test gegen den vorhandenen Dev-Stand, prueft er aelteren Code als den, der gleich hochgeht. Gefundene, noch offene Fehler stehen mit Item-Nummer in tests/e2e/bekannte_befunde.json - sie laufen als BEK mit und blockieren nicht; besteht so ein Fall wieder, sagt der Lauf das ausdruecklich. Ein Lauf, in dem mehr uebersprungen als geprueft wurde, endet mit Rueckgabewert 2 statt 0: nichts ist kaputt, geprueft wurde es aber auch nicht, und der Unterschied darf nicht am Rueckgabewert verschwinden. Anleitung: docs/TESTINSTANZ.md.",
|
|
"action": "Wer deployt, braucht scripts/deploy_dev_config.json (Vorlage: ...example.json). Ohne sie bricht scripts/deploy.py ab und verweist auf --ohne-test."
|
|
},
|
|
{
|
|
"area": "mcp",
|
|
"breaking": false,
|
|
"title": "MCP-Adapter 1.1: Ressourcen, Prompts und eine echte Protokollaushandlung",
|
|
"text": "Werkzeuge ruft ein Modell von sich aus auf - Ressourcen und Prompts waehlt ein MENSCH. In Claude Desktop sind das das Anhang-Menue und die Schraegstrich-Befehle, und genau die fehlten. Neu: vier Ressourcen (dc://roadmap als fertiger Markdown-Block, dc://naechste-schritte, dc://projekte, dc://befunde) und vier Prompts (arbeit-aufnehmen, befunde-sichten, fehler-melden, roadmap-pflegen). Der Wert der Prompts liegt darin, dass die Hausregeln mitkommen: erst nachsehen wer dransitzt, Fremdmeldung nur mit Begruendung, verworfene Vorhaben nicht ohne neues Argument wiedervorschlagen. Bisher standen die nur in Werkzeugbeschreibungen - und die liest ein Modell erst, wenn es das Werkzeug schon gewaehlt hat. Ressourcen laufen durch dieselbe Fremddaten-Abgrenzung wie Werkzeugergebnisse; sie sind sogar eher ein Einfallstor, weil sie ungefragt im Kontext landen. BEHOBEN dabei: \"initialize\" spiegelte die vom Client verlangte Protokollfassung einfach zurueck. Ein Client, der 2025-06-18 anfragte, bekam sie bestaetigt, obwohl der Adapter fuer 2024-11-05 geschrieben war. Jetzt wird gegen eine Liste tatsaechlich bedienter Fassungen ausgehandelt. Ebenfalls behoben: Beim Beenden rief der Adapter process.exit() mitten in den schliessenden libuv-Handles - unter Windows eine Assertion und ein Rueckgabewert ungleich null, also ein sauber beendeter Server, der wie ein abgestuerzter aussah.",
|
|
"action": "Fuer Claude Desktop: Eintrag in claude_desktop_config.json (%APPDATA%\\Claude\\ bzw. ~/Library/Application Support/Claude/), absoluter Pfad, DC_PROJECT setzen, Desktop vollstaendig neu starten. Anleitung in mcp/README.md."
|
|
},
|
|
{
|
|
"area": "api",
|
|
"breaking": false,
|
|
"title": "Behoben: Werkbank-Token konnte seine eigene Roadmap nicht rendern",
|
|
"text": "roadmap.php?action=render und ?action=check verlangen ein konkretes Projekt. Beim Lesen laesst projectFilter() ein Werkbank-Token aber bewusst durch - es soll die ganze Landschaft sehen - und liefert deshalb null. Damit wurde der Projektbezug zu \"all\" und der Aufruf mit 400 missing_project abgelehnt. Betroffen war ausgerechnet der Tokentyp, den der MCP-Adapter und dc-testkit verlangen. Das Token ist trotzdem an ein Projekt gebunden; dieser Bezug dient jetzt als Rueckfall. \"Meine Roadmap\" ohne Angabe heisst genau das.",
|
|
"action": null
|
|
},
|
|
{
|
|
"area": "api",
|
|
"breaking": false,
|
|
"title": "Behoben: Projektliste warf 500 fuer Werkbank-Token (HY093)",
|
|
"text": "ProjectLinks::between() benutzte :a dreimal und :b zweimal im selben Statement, forProject() :slug zweimal. Bei ATTR_EMULATE_PREPARES = false ist das unzulaessig - PDO reicht die Namen an den Server durch, und der zaehlt Parameter, statt sie zu benennen. Ergebnis: SQLSTATE[HY093] und ein 500 auf /api/bugtracker/v1/projects. Die Abfrage laeuft nur, wenn ein Heimatprojekt gesetzt ist, also nur mit einem Werkbank-Token - fuer das Master-Token des Testlaufs wurde der Zweig uebersprungen. Derselbe Fehler steckte schon einmal in der Volltextsuche des Bugtrackers; ein Suchlauf ueber alle prepare()-Aufrufe der Codebasis fand jetzt keine weitere Stelle.",
|
|
"action": null
|
|
},
|
|
{
|
|
"area": "betrieb",
|
|
"breaking": false,
|
|
"title": "Testlauf kennt jetzt den Werkbank-Token - der Tokentyp, den die Agenten benutzen",
|
|
"text": "Es gibt keine API, ein dev_agent-Token anzulegen; in der Produktion geschieht das von Hand in der Token-Verwaltung, und das bleibt auch so. Fuer den Testlauf fehlte damit ausgerechnet der Tokentyp, den MCP-Adapter und dc-testkit verwenden - mit der Folge, dass seine Sonderwege niemand prueft. Genau dort steckten die beiden Befunde oben. testctl.php kann jetzt action=workbench_token, und vier Testfaelle decken die Wege ab: Roadmap ohne Projektangabe rendern, Projektliste mit Beziehungen, und die Gegenprobe, dass der Rueckfall aufs Heimatprojekt das Lesen ueber alle Projekte nicht einschraenkt. Der Testlauf steht damit bei 128 Faellen.",
|
|
"action": null
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"version": "2.6.1",
|
|
"date": "2026-08-28",
|
|
"summary": "Agenten koennen auf der Linux-Testumgebung selbstaendig testen: DEV-Lizenzschluessel zur Selbstbedienung, ein Werkzeug fuer Auslieferung und Meldung, und eine Nachpruefung, die als Arbeit zurueckkommt statt als Benachrichtigung.",
|
|
"actionRequired": true,
|
|
"changes": [
|
|
{
|
|
"area": "api",
|
|
"breaking": true,
|
|
"title": "Nachpruefung: der Fix ist erst fertig, wenn ihn jemand gegen ein Paket geprueft hat",
|
|
"text": "Drei neue Zustaende (Migration 015): awaiting_verification, in_verification, fix_insufficient. Ein Item, dessen Fix veroeffentlicht wurde, geht nicht mehr direkt zu, sondern auf awaiting_verification - und taucht damit in ?action=next als Aufgabe auf. Das IST der Rueckkanal: Die Zustellung existierte laengst, es fehlte nur ein Status, der in der Warteschlangenabfrage vorkommt. Uebernehmen befoerdert nach in_verification, ueber denselben Claim wie gewoehnliche Arbeit. Bestaetigt wird mit status=closed; wer stattdessen erneut meldet, schiebt das Item selbsttaetig auf fix_insufficient - kein neues Item, Versuch und Fehlschlag bleiben auf einem Strang. Eine Regression bleibt davon unberuehrt und bekommt weiterhin ein eigenes Item: Sie betrifft ein bestaetigt geschlossenes Item, das zurueckkommt. Gegen das Verrotten: Laeuft die Lease ab, geht es zurueck in die Schlange; nach drei abgebrochenen Anlaeufen holt ein Vermerk einen Menschen; nach 30 Tagen ohne Pruefung wird ungeprueft geschlossen, mit einem Kommentar, der genau das sagt. Neu auch die Spalten status_changed_at (traegt den Timeout) und verification_cycles. Der Verlauf entsteht ohne zusaetzliche Tabelle: Jeder Statuswechsel schreibt jetzt strukturiertes meta_json (from/to/actor/since) in den vorhandenen Kommentarstrang - zuvor stand dort nur deutscher Fliesstext und der alte Status gar nicht.",
|
|
"action": "Migration 015 ausfuehren. Wer eigene Auswertungen auf bugtracker_items.status hat, muss die drei neuen Werte einordnen: awaiting_verification und in_verification zaehlen NICHT als offene Fehler (der Fix ist ausgeliefert, offen ist nur die Frage), fix_insufficient dagegen schon - er ist ein offener Fehler, und zwar ein besonders aergerlicher. Wer Items ueber ?action=next abholt, muss den Status pruefen: in_verification heisst 'pruefe, ob der Fix traegt', nicht 'behebe das'."
|
|
},
|
|
{
|
|
"area": "api",
|
|
"breaking": false,
|
|
"title": "Behoben: liegengelassene Items verschwanden lautlos aus der Warteschlange",
|
|
"text": "Uebernehmen befoerdert ein Item von 'open' nach 'in_progress'. Lief die Lease ab, gab expireStaleLeases() zwar claimed_by, claimed_at und lease_until frei - liess den Status aber stehen. Da claimNext() nur nach 'open,planned' fragt, war ein liegengelassenes Item danach unauffindbar: kein Besitzer, nicht in der Schlange, kein Hinweis darauf, dass es je jemand angefasst hatte. Es blieb fuer immer liegen. Die Freigabe stuft den Status jetzt denselben Weg zurueck, den das Uebernehmen ihn befoerdert hat (in_progress -> open, in_verification -> awaiting_verification). Der Fehler bestand unabhaengig von der Nachpruefung und betraf jedes Projekt mit Agentenbetrieb.",
|
|
"action": null
|
|
},
|
|
{
|
|
"area": "api",
|
|
"breaking": false,
|
|
"title": "Wer einen Fremdbefund gemeldet hat, bekommt den Pruefauftrag",
|
|
"text": "Beim Schreiben gilt sonst ohne Ausnahme: Ein Agent uebernimmt nur Items seines eigenen Projekts. Genau das machte den Rueckkanal unmoeglich - meldet PolyTrader einen Fehler im Deploymentcenter, gehoert das Item dem Deploymentcenter, und PolyTraders Agent bekaeme es nie zu sehen, obwohl er oft der Einzige ist, der bestaetigen kann, dass der Fix bei ihm wirkt. Geoeffnet wird deshalb genau ein Spalt: fremde Items, die auf Nachpruefung warten UND bei denen das eigene Projekt in bugtracker_item_affects vermerkt ist. Wer auf diesem Weg etwas haelt, darf es kommentieren, im Status setzen und zurueckgeben - aber nicht resolve oder update. Was in einem fremden Projekt als 'behoben in Build X' gilt, entscheidet dieses Projekt.",
|
|
"action": null
|
|
},
|
|
{
|
|
"area": "api",
|
|
"breaking": false,
|
|
"title": "Regression oder nur ein veralteter Pruefstand? Der Tracker vergleicht jetzt die Fassungen",
|
|
"text": "Trifft eine Meldung auf ein bereits erledigtes Item, wurde daraus bisher IMMER eine Regression - ein neues Item mit Verweis auf das alte. Verglichen wurde nichts. Auf einer Testumgebung ist der haeufigste Fall aber ein anderer: Ein Lauf gegen 1.4.2 findet einen Fehler, der in 1.4.3 behoben wurde. Das ist kein Rueckschritt der Software, sondern eine veraltete Installation. Waere jede solche Meldung eine 'Regression', waere die Kennzeichnung wertlos fuer die echten. Neu wird build_version gegen resolved_in_build des erledigten Items verglichen (ueber den vorhandenen semantischen Vergleicher UpdateService\\Version, nicht ueber einen zweiten). Ist die gemeldete Fassung AELTER, entsteht nichts: kein Item, kein Kommentar, kein hochgezaehlter Zaehler. Die Antwort traegt stattdessen known_fixed_in und reported_build und sagt, dass aktualisiert und erneut geprueft werden soll. Ist sie gleich oder neuer, bleibt es bei der Regression - der Systemkommentar nennt jetzt beide Fassungen. Nicht vergleichbare Angaben ('dev', 'nightly', 'unknown', fehlendes resolved_in_build) fuehren weiterhin zur Regression; im Zweifel wird gemeldet und nicht verschluckt.",
|
|
"action": "Wer eigene Meldewege gebaut hat, schickt build_version mit. Ohne diese Angabe kann der Server nicht unterscheiden und legt weiterhin im Zweifel ein Regressionsitem an. dc-testkit traegt sie automatisch aus der manifest.json des geprueften Verzeichnisses nach."
|
|
},
|
|
{
|
|
"area": "api",
|
|
"breaking": false,
|
|
"title": "DEV-Lizenzschluessel zur Selbstbedienung",
|
|
"text": "Neu: POST /api/license/v1/dev-key, Scope license:dev. Eine Testmaschine stellt sich damit fuer ihr Projekt selbst einen befristeten Lizenzschluessel aus - Vorgabe 30 Tage, hoechstens 90, drei Aktivierungen, hoechstens zwoelf gleichzeitig je Projekt. Der Aufruf ist wiederholbar: Fuer dieselbe Kombination aus project_slug und instance_id kommt derselbe Schluessel zurueck und seine Frist wird verlaengert; ein Testskript darf ihn deshalb an den Anfang jedes Laufs setzen. Der Schluessel wird beim Ausstellen sofort in die .htpasswd des Produkts nachgezogen und oeffnet damit auch die Release-Ablage. Er ist markiert (customer_email dev-agent@testumgebung.invalid, Name 'DEV <maschine>'), damit Testhardware in der Aktivierungsliste von echten Installationen zu unterscheiden ist. Abgelaufene Schluessel werden nach vierzehn Tagen automatisch geloescht. ACHTUNG beim Anbinden: Dieser Endpunkt antwortet im Hausformat (status: success), nicht im abweichenden Altformat von validate und deactivate daneben.",
|
|
"action": "Wer auf der Linux-Test-VM testet, braucht je Projekt ein Werkbank-Token mit den Rechten bugtracker:manage und license:dev (Besitzerart dev_agent, an das Projekt gebunden). Bis das angelegt ist, laufen 'dc-testkit license' und 'dc-testkit report' ins Leere. Anleitung: docs/TESTUMGEBUNG.md, Abschnitt 4.2."
|
|
},
|
|
{
|
|
"area": "tooling",
|
|
"breaking": false,
|
|
"title": "dc-testkit - ein Weg fuer Lizenz, Auslieferung und Meldung",
|
|
"text": "Neu unter public/testkit/dc-testkit.sh, auf der Testmaschine zu holen ueber https://dc.mhdf.de/testkit/dc-testkit.sh. Deckt den ganzen Kreis eines Testlaufs ab: doctor prueft Werkzeuge, Netz, Token und Projekt; license holt und verlaengert den DEV-Schluessel; agent laedt den update-agent und gleicht die Pruefsumme gegen installer.json ab; install liefert in einen leeren Sandkasten aus; update, repair und list decken die uebrigen Wege ab; verify vergleicht das geschriebene manifest.json mit dem, was der Server als neueste Fassung fuehrt; report, next, comment, resolve und unclaim bedienen den Bugtracker; run macht alles nacheinander und meldet, was scheitert; cleanup raeumt auf und gibt den Schluessel zurueck. Braucht nur curl und python3 - jq ist auf der VM nicht installiert und wird nicht vorausgesetzt. Der Lizenzschluessel geht ueber DC_LICENSE_KEY an den Agenten und nicht ueber --license-key: Auf einer geteilten Maschine sieht jeder andere Benutzer fremde Befehlszeilen in 'ps aux'. Meldungen tragen ein aus Projekt, Titel und Fundstelle abgeleitetes client_ref - derselbe Befund aus zehn Laeufen legt ein Item an und nicht zehn.",
|
|
"action": null
|
|
},
|
|
{
|
|
"area": "docs",
|
|
"breaking": false,
|
|
"title": "Anleitung fuer die Linux-Testumgebung, projektuebergreifend",
|
|
"text": "Neu: docs/TESTUMGEBUNG.md. Beschreibt die geteilte Test-VM vollstaendig - Zugang, vorhandene Werkzeuge, Testkit, Konventionen fuer den Parallelbetrieb mehrerer Projekte (eigene Datenbank, eigenes X-Display, Projektpraefix in /tmp), Sichtpruefung von Oberflaechen ueber Xvfb, xdotool und ImageMagick, sowie Belegungsliste und Aufraeumen. Bewusst NICHT unter public/: Die Datei nennt eine interne Adresse, einen Benutzernamen und den Weg, sudo zeitweise freizuschalten; /docs/ ist oeffentlich erreichbar. Zuvor lag die Beschreibung im PolyTrader-Repo, obwohl sie fuer alle Projekte gilt; dort steht jetzt nur noch ein Verweis samt der PolyTrader-eigenen Angaben.",
|
|
"action": null
|
|
},
|
|
{
|
|
"area": "docs",
|
|
"breaking": false,
|
|
"title": "Bekannte Luecke: der Installationsdialog ist nicht kopflos pruefbar",
|
|
"text": "update-agent --action install ist ein Dialog mit Auswahllisten und Passwortabfrage. Ein Agent kann ihn nicht treiben - Auswahllisten brauchen Pfeiltasten an einem echten Terminal, und ein Agent gibt grundsaetzlich kein Passwort ein. 'dc-testkit install' prueft deshalb die Auslieferung in ein leeres Verzeichnis: denselben Pfad, den der Installer intern nimmt, mit Download, SHA256, Signatur, Staging und Rollback. Ungeprueft bleiben die Aufloesung von setup.json und das Schreiben der Konfiguration. Als Vorhaben in der Roadmap unter G1 vermerkt.",
|
|
"action": null
|
|
},
|
|
{
|
|
"area": "docs",
|
|
"breaking": false,
|
|
"title": "Fremdmeldungen: was dabei anders ist, steht jetzt dort, wo Agenten es lesen",
|
|
"text": "Projektuebergreifendes Melden gibt es seit 2.6.0 (C1), beschrieben war es aber nirgends dort, wo ein meldender Agent hinsieht. Drei Eigenheiten fuehrten deshalb verlaesslich in die Irre: evidence ist bei einer Fremdmeldung PFLICHT (mind. 40 Zeichen, sonst 400 missing_evidence); der Schweregrad wird serverseitig auf medium gedeckelt, weil ein fremder Melder die Dringlichkeit nicht beurteilen kann; und die Meldung landet in triage_external statt in open, wird in der offenen Liste des Zielprojekts also nicht gefunden. Steht jetzt in TESTUMGEBUNG.md Abschnitt 4.6 und in der Agent-Prompt-Vorlage. Neu im Testkit: 'dc-testkit projects' zeigt die gueltigen Slugs, 'report --project <fremd> --evidence ...' meldet dorthin, und die Deckelung wird vorher angesagt statt hinterher entdeckt.",
|
|
"action": "Wer die Agent-Prompt-Vorlage in ein Projekt kopiert hat, zieht den Abschnitt 'Projekt bestimmen' nach - die alte Fassung sagte zwar, dass DC-Fehler unter 'deploymentcenter' gehoeren, verschwieg aber die Pflichtangabe evidence. Ein Agent, der ihr folgte, lief in 400 missing_evidence und legte den Befund danach im eigenen Projekt ab, wo ihn niemand behebt."
|
|
},
|
|
{
|
|
"area": "tooling",
|
|
"breaking": false,
|
|
"title": "Testkit: 'feature' wurde stillschweigend zu 'bug'",
|
|
"text": "Der Bugtracker kennt genau zwei Typen, bug und feature_request, und deutet alles Uebrige wortlos in 'bug' um (BugRepo Zeile 92). Ein '--type feature' haette Feature Requests also als Fehler abgelegt, ohne dass es jemandem aufgefallen waere. Das Testkit uebersetzt jetzt feature/fr/wunsch/verbesserung nach feature_request und lehnt unbekannte Typen ab, statt sie umzudeuten. 'idea' wird gesondert abgefangen: Das ist ein Schweregrad, kein Typ. Schweregrade werden ebenfalls gegen die erlaubte Liste geprueft.",
|
|
"action": null
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"version": "2.6.0",
|
|
"date": "2026-08-21",
|
|
"summary": "Supervisor-Befunde, MCP-Anbindung und projektuebergreifendes Melden. Der Installer prueft die Laufzeit und richtet die Ueberwachung des Hosts ein.",
|
|
"actionRequired": true,
|
|
"changes": [
|
|
{
|
|
"area": "server",
|
|
"breaking": false,
|
|
"title": "Serverseitige Detektoren finden, woran niemand gedacht hat",
|
|
"text": "Fuenf Detektoren laufen im minuetlichen Tick (gedrosselt auf zehn Minuten) und arbeiten ausschliesslich auf Daten, die ohnehin hereinkommen: novel.error_signature meldet Fehlersignaturen, die es in dieser Fassung noch nie gab; silence.heartbeat verstummte Instanzen; deviation.self Abweichungen vom eigenen Verlauf; deviation.fleet Ausreisser gegenueber den anderen Installationen derselben Fassung; regression.build Verschiebungen zwischen zwei Fassungen. Keiner davon muss den gesuchten Fehler vorher kennen - der Massstab kommt aus der Wirklichkeit statt aus einem Kopf. Sie brauchen keine Client-Aenderung.",
|
|
"action": "Fuer deviation.fleet und regression.build den Monitoren ein Projekt zuordnen (watchdog_monitors.project_slug). Ohne Zuordnung laufen die uebrigen Detektoren weiter. Ein Beobachter in der ausgelieferten Anwendung ist NICHT noetig und auch nicht vorgesehen - diese Detektoren arbeiten auf Daten, die ohnehin hereinkommen."
|
|
},
|
|
{
|
|
"area": "api",
|
|
"breaking": false,
|
|
"title": "/api/supervisor/v1/ - Befunde einliefern und abfragen",
|
|
"text": "POST observe nimmt Befunde entgegen (Scope supervisor:report), GET findings und summary liefern sie aus (supervisor:read), catalog nennt alle bekannten Detektoren, POST status stuft ein (supervisor:manage). Ein Befund ist keine Fehlermeldung: er behauptet nicht, dass etwas kaputt ist, sondern dass etwas ungewoehnlich ist - deshalb loest er keine Benachrichtigung aus. Idempotent ueber client_ref, damit ein minuetlich pruefender Beobachter einen Eintrag mit hohem Zaehler erzeugt und nicht 1.440 am Tag. Wer einliefert, sind Pruefwerkzeuge und Testlaeufe (dc-testkit ab 2.6.1) - ein Beobachter im ausgelieferten Programm war erwogen und ist verworfen worden, siehe docs/ROADMAP.md Abschnitt H.",
|
|
"action": ""
|
|
},
|
|
{
|
|
"area": "api",
|
|
"breaking": false,
|
|
"title": "Projektuebergreifend melden",
|
|
"text": "Ein Token vom Typ dev_agent darf jetzt in ein fremdes Projekt melden - der Fall, den die Agenten-Prompt-Vorlage seit jeher verlangte und den ApiAuth::enforceProject() mit 403 verweigerte. Laufzeit-Token duerfen es weiterhin nicht: sie stecken im ausgelieferten Client und sind extrahierbar. Fuer Fremdmeldungen ist evidence Pflicht, reported_from wird aus der Token-Bindung abgeleitet statt aus dem Body uebernommen, das Item landet im Status triage_external und wird auf Schweregrad medium gedeckelt. Meldet ein zweites Projekt dasselbe, entsteht kein neues Item - es waechst die Liste der Betroffenen.",
|
|
"action": "Fuer Claude Code je Projekt ein Token vom Typ dev_agent anlegen, gebunden an das jeweilige Projekt."
|
|
},
|
|
{
|
|
"area": "api",
|
|
"breaking": false,
|
|
"title": "MCP-Adapter fuer Claude Code und andere Werkzeuge",
|
|
"text": "Unter mcp/ liegt ein Adapter ohne Fremdabhaengigkeiten, der 13 Werkzeuge bereitstellt: Bugtracker (Suchen, Uebernehmen, Melden, Kommentieren, Abschliessen), Supervisor (Befunde, Zaehlung je Detektor, Kennzahlverlauf) und die Projektlandschaft. Fremddaten - Fehlermeldungen, Stacktraces, Kommentare - werden im Werkzeugergebnis abgegrenzt gekennzeichnet: sie entstehen auf fremden Maschinen und sind Daten, keine Anweisungen. Damit entfaellt das Einkopieren der Agenten-Prompt-Vorlage in jedes Projekt.",
|
|
"action": "Adapter je Projekt in .mcp.json eintragen, siehe mcp/README.md."
|
|
},
|
|
{
|
|
"area": "api",
|
|
"breaking": false,
|
|
"title": "Beziehungen zwischen Projekten",
|
|
"text": "dc_project_links haelt fest, welches Projekt welches nutzt und in welcher Fassung. Damit ersetzt POST /api/bugtracker/v1/projects die von Hand gepflegte Zeile \"Deploymentcenter-Anbindung: SDK x.y.z\" in jeder Projekt-CLAUDE.md - eine Angabe, die an N Stellen nachgezogen werden muss und nach dem ersten Vergessen schweigend falsch ist. Erstmals beantwortbar: welche meiner Projekte sind von diesem SDK-Fehler betroffen?",
|
|
"action": "Nach jeder Aenderung an der Anbindung dc_link_project aufrufen."
|
|
},
|
|
{
|
|
"area": "client",
|
|
"breaking": false,
|
|
"title": "DeploymentcenterSdk.Version stand auf 2.5.1",
|
|
"text": "Die Konstante war beim Sprung auf 2.6.0 nicht mitgezogen worden. Wer den Changelog wie vorgesehen mit ?since=DeploymentcenterSdk.Version abfragte, bekam 2.6.0 dauerhaft als neu gemeldet, auch nach dem Aktualisieren. Jetzt korrekt.",
|
|
"action": ""
|
|
},
|
|
{
|
|
"area": "server",
|
|
"breaking": false,
|
|
"title": "Projektgebundene Token kamen an fremde Statistiken",
|
|
"text": "?action=stats nahm project_slug aus der Anfrage, ohne die Bindung des Tokens zu erzwingen - ein an Projekt X gebundenes Token konnte damit die Zahlen jedes anderen Projekts abrufen. Die Bindung geht der Anfrage jetzt vor.",
|
|
"action": ""
|
|
},
|
|
{
|
|
"area": "client",
|
|
"breaking": false,
|
|
"title": "Installer prueft die .NET-Laufzeit",
|
|
"text": "Bisher endete eine gelungene Installation auf einer Maschine ohne .NET mit einer Anwendung, die sich nicht starten laesst - und die Fehlersuche begann beim Deploymentcenter, weil das der letzte bewusste Schritt war. Nach dem Uebertragen der Dateien liest der Installer jetzt die runtimeconfig.json der Anwendung, vergleicht sie mit \"dotnet --list-runtimes\" und nennt bei Bedarf den Installationsbefehl fuer diese Plattform. Eigenstaendig veroeffentlichte Pakete bringen ihre Laufzeit mit und werden nicht bemaengelt; rollForward wird beachtet.",
|
|
"action": ""
|
|
},
|
|
{
|
|
"area": "client",
|
|
"breaking": false,
|
|
"title": "Ueberwachung der Maschine direkt aus dem Installer",
|
|
"text": "Am Ende der Installation genuegen zwei Fragen - Name im Dashboard und ob eingeplant werden soll. Monitor, Token, Agent-Skript, Dateirechte, ein erster Heartbeat zur Probe und der Cron-Eintrag bzw. die geplante Aufgabe entstehen daraus. Fuer Maschinen ohne Installation, etwa den Hypervisor darunter, gibt es denselben Weg als \"--action monitor\".",
|
|
"action": "Keine. Wer die Frage verneint, holt es spaeter mit --action monitor nach."
|
|
},
|
|
{
|
|
"area": "server",
|
|
"breaking": false,
|
|
"title": "Agent-Skripte melden jetzt Kennzahlen",
|
|
"text": "Das aus der Oberflaeche heruntergeladene Skript schickte nur \"status: ok\" - damit beantwortete ein Host-Monitor genau eine Frage und keine der Fragen, wegen derer man auf einen Host schaut. Die Skripte melden jetzt Last, Speicherbelegung, Plattenbelegung und Laufzeit mit und stufen sich ab 90 Prozent Plattenbelegung selbst auf \"warning\". Erzeugt werden sie an einer Stelle (Modules/Watchdog/AgentScript.php), die Oberflaeche und Installer gemeinsam nutzen.",
|
|
"action": ""
|
|
},
|
|
{
|
|
"area": "api",
|
|
"breaking": false,
|
|
"title": "POST /api/setup/v1/agent",
|
|
"text": "Legt den Monitor an, stellt ein Token mit genau watchdog:ping aus und liefert das fertige Skript fuer den Host. Ein bereits vorhandener Monitor gleichen Namens wird mit 409 abgelehnt, damit nicht eine zweite Maschine stillschweigend die Historie der ersten uebernimmt; overwrite=true erlaubt es ausdruecklich. Im SDK: SetupClient.RequestWatchdogAgentAsync(...).",
|
|
"action": ""
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"version": "2.5.3",
|
|
"date": "2026-08-21",
|
|
"summary": "Administratorkonten kommen an die Release-Ablage.",
|
|
"actionRequired": false,
|
|
"changes": [
|
|
{
|
|
"area": "server",
|
|
"breaking": false,
|
|
"title": "Administratorkonten im Release-Zugangsschutz",
|
|
"text": "Der Zugangsschutz nahm bisher nur Konten der Rolle installer auf, waehrend die Installationsskripte und der Installer ausdruecklich sagten, ein Administratorkonto tue es auch. Eine Erstinstallation mit einem Administratorkonto meldete sich erfolgreich an, zeigte den Katalog und scheiterte erst beim Herunterladen mit einer Meldung ueber abgelaufene Lizenzen. Administratoren zaehlen jetzt zu den Installationskonten; die .htpasswd-Dateien werden beim naechsten Tick neu erzeugt. Neu angelegte Konten landen ausserdem sofort darin - zuvor galt das nur fuer die Rolle installer, ein frisches Administratorkonto wartete bis zum naechsten turnusmaessigen Lauf.",
|
|
"action": ""
|
|
},
|
|
{
|
|
"area": "client",
|
|
"breaking": false,
|
|
"title": "Ehrliche Meldung bei abgelehntem Zugang",
|
|
"text": "Ein 401 der Release-Ablage nannte immer Lizenzgruende - abgelaufen, widerrufen, falsches Produkt. Bei einer Erstinstallation gibt es noch gar keine Lizenz; die Meldung schickte damit in die falsche Richtung. Wurde mit Benutzername und Passwort zugegriffen, nennt sie jetzt das Konto und die Rollen, die Zugang haben. Ausserdem bricht der Agent bei 401 sofort ab, statt ueber die API weiterzusuchen und dieselbe Meldung ein paar Schritte spaeter ein zweites Mal zu zeigen.",
|
|
"action": ""
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"version": "2.5.2",
|
|
"date": "2026-08-15",
|
|
"summary": "LaunchUpdateAgent reicht --require-signature durch.",
|
|
"actionRequired": true,
|
|
"changes": [
|
|
{
|
|
"area": "sdk",
|
|
"breaking": false,
|
|
"title": "requireSignature-Parameter auf LaunchUpdateAgent",
|
|
"text": "--require-signature gab es bisher nur als Kommandozeilenschalter. Jede Anwendung, die dem Leitfaden folgt und ueber LaunchUpdateAgent startet - der empfohlene Weg - aktualisierte damit ungeprueft, waehrend derselbe Vorgang von Hand auf der Kommandozeile geschuetzt gewesen waere. Der oeffentliche Schluessel muss dafuer nicht separat verwaltet werden, der Agent holt und cached ihn ohnehin selbst.",
|
|
"action": "LaunchUpdateAgent(..., requireSignature: true) setzen, sobald server.security.release_private_key hinterlegt ist und alle ausgelieferten Releases signiert wurden."
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"version": "2.5.1",
|
|
"date": "2026-08-14",
|
|
"summary": "Zugangsschutz je Produkt abschaltbar, Dienst-Betrieb dokumentiert, inkrementelle Builds repariert.",
|
|
"actionRequired": true,
|
|
"changes": [
|
|
{
|
|
"area": "server",
|
|
"breaking": false,
|
|
"title": "Produkte lassen sich vom Zugangsschutz ausnehmen",
|
|
"text": "Der Schutz entstand beim ersten Lauf nach dem Deployment von selbst - auch fuer Produkte, deren ausgelieferte Installationen noch ohne Lizenzschluessel bauen. Die bekamen ab dem Moment 401 statt Updates; bei Predictalytics ist genau das passiert. Unter UpdateService -> Zugangsschutz laesst sich je Produkt umschalten.",
|
|
"action": "Wer eine Installationsbasis ohne SDK 2.4 hat: Produkt ausnehmen, Build mit Schluessel ausliefern, danach wieder einschalten."
|
|
},
|
|
{
|
|
"area": "sdk",
|
|
"breaking": false,
|
|
"title": "BuildInfo.targets verhindert keine inkrementellen Builds mehr",
|
|
"text": "BuildDateUtc trug die volle Uhrzeit und aenderte sich bei jedem Build; WriteOnlyWhenDifferent griff nie, und jedes einbindende Projekt wurde jedes Mal neu uebersetzt. Jetzt tagesgenau - hoechstens ein Neubau pro Tag.",
|
|
"action": "Keine. BuildInfo.BuildDateUtc enthaelt nur noch das Datum, nicht die Uhrzeit."
|
|
},
|
|
{
|
|
"area": "sdk",
|
|
"breaking": true,
|
|
"title": "LicenseConfig.SetStorageDirectory haengt jetzt <slug>/license an",
|
|
"text": "Zuvor wurde der gesetzte Pfad roh benutzt, waehrend der Weg ueber DEPLOYMENTCENTER_STORAGE_DIR <slug>/license anhaengte. Zwei Produkte im selben Prozess mit gesetztem Override schrieben damit in dieselbe state.dat.",
|
|
"action": "Wer SetStorageDirectory benutzt: Der Cache liegt jetzt eine Ebene tiefer. Ein vorhandener Cache wird einmalig neu aufgebaut."
|
|
},
|
|
{
|
|
"area": "sdk",
|
|
"breaking": false,
|
|
"title": "Lizenz-Cache faellt auf /var/lib/<slug> zurueck",
|
|
"text": "Eine systemd-Unit mit User= und ohne eigenes Heimatverzeichnis hat weder XDG_CONFIG_HOME noch HOME. Der Rueckfall landete im Installationsverzeichnis, unter /opt fuer den Dienstbenutzer nicht beschreibbar - also kein Offline-Cache, ausgerechnet im kopflosen Betrieb.",
|
|
"action": "Wenn /var/lib nicht beschreibbar ist: DEPLOYMENTCENTER_STORAGE_DIR setzen."
|
|
},
|
|
{
|
|
"area": "docs",
|
|
"breaking": false,
|
|
"title": "Lueckenschluss in den Leitfaeden",
|
|
"text": "Woher die Anwendung den Lizenzschluessel fuer den Update-Zugang nimmt (TryGetCachedKey), wie ein laufender systemd-Dienst aktualisiert wird, was statt Environment.Exit(1) zu tun ist, wenn Beenden die gefaehrlichere Option waere, und die Drosselungsgrenzen aller Endpunkte.",
|
|
"action": null
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"version": "2.5.0",
|
|
"date": "2026-08-14",
|
|
"summary": "Geheimnisse raus aus der Kommandozeile, Packager sperrt statt zu warnen, ILicensePrompt lebt.",
|
|
"actionRequired": true,
|
|
"changes": [
|
|
{
|
|
"area": "sdk",
|
|
"breaking": false,
|
|
"title": "Lizenzschluessel wird nicht mehr als Argument uebergeben",
|
|
"text": "Der Agent nahm --license-key nur aus argv. 'ps' zeigt das jedem Benutzer der Maschine - exakt die Begruendung, mit der UPGRADE.md §5 den Crontab-Weg verwirft. Der Agent liest jetzt DC_LICENSE_KEY, DC_DOWNLOAD_USER und DC_DOWNLOAD_PASSWORD; Umgebung vor Argument. LaunchUpdateAgent setzt die Variable auf dem eigenen Prozess, das Kind erbt sie.",
|
|
"action": "Keine Aenderung noetig, wenn ihr licenseKey an LaunchUpdateAgent uebergebt. Wer den Agenten selbst aufruft, sollte von --license-key auf DC_LICENSE_KEY umstellen."
|
|
},
|
|
{
|
|
"area": "sdk",
|
|
"breaking": false,
|
|
"title": "waitTimeoutSeconds an LaunchUpdateAgent",
|
|
"text": "Der Agent kannte --wait-timeout, das SDK reichte es nicht durch - es galten fest 60 Sekunden. Laeuft die Zeit ab, bricht der Agent ab, ohne etwas zu veraendern.",
|
|
"action": "Wenn euer Herunterfahren laenger als etwa 40 Sekunden dauert: waitTimeoutSeconds heraufsetzen."
|
|
},
|
|
{
|
|
"area": "sdk",
|
|
"breaking": false,
|
|
"title": "Environment.Exit(0) ist dokumentiert, nicht geaendert",
|
|
"text": "exitCurrentApp:true beendet den Prozess hart - laufende finally-Bloecke, IHostApplicationLifetime und Destruktoren kommen nicht mehr zum Zug.",
|
|
"action": "Bei offenem Zustand (Positionen, Transaktionen, ungeschriebene Puffer) exitCurrentApp:false setzen und selbst geordnet herunterfahren. Der Agent wartet ohnehin auf das Prozessende."
|
|
},
|
|
{
|
|
"area": "sdk",
|
|
"breaking": false,
|
|
"title": "EnsureLicensedAsync - ILicensePrompt wird endlich benutzt",
|
|
"text": "Der Konstruktor nahm ILicensePrompt entgegen, legte es ab und rief es nie auf. Neu: EnsureLicensedAsync() nimmt den zwischengespeicherten Schluessel, fragt sonst nach, prueft, und fragt bei Ablehnung erneut. allowPrompt:false lehnt ohne Cache ab, statt im Dienst auf eine Eingabe zu warten, die nie kommt.",
|
|
"action": "Optional. Wer eine eigene Abfrage gebaut hat, kann sie behalten."
|
|
},
|
|
{
|
|
"area": "packager",
|
|
"breaking": true,
|
|
"title": "Bricht bei Zugangsdaten im Paket ab",
|
|
"text": "Zuvor nur eine Warnung - und die war ausgerechnet unterdrueckt, wenn die Datei auf der preserve-Liste stand. So geriet ein echter API-Schluessel in ein oeffentlich abrufbares Paket. Geprueft werden Dateiname (appsettings.Local.json, master.key, *.pfx, *.db, server_settings.xml) und Inhalt (gefuelltes Password=, sk-, ghp_, dc_master_, AKIA, private Schluessel). Platzhalter loesen nicht aus.",
|
|
"action": "Pruefen, was im Publish-Verzeichnis landet - haeufig eine CopyToOutputDirectory-Regel in der csproj. Notausgang: --allow-secrets."
|
|
},
|
|
{
|
|
"area": "sdk",
|
|
"breaking": false,
|
|
"title": "BuildInfo.targets liegt im NuGet-Paket",
|
|
"text": "Die Anleitung empfahl einen <Import> per relativem Pfad ins Nachbar-Repository - das setzt voraus, dass beide Arbeitskopien nebeneinander liegen und in derselben Fassung stehen. Das Target liegt jetzt unter build/ im Paket, NuGet importiert es selbst.",
|
|
"action": "Den <Import Project=\"..\\Deploymentcenter.Client\\Deploymentcenter.BuildInfo.targets\" /> aus der csproj entfernen und stattdessen PackageReference auf Deploymentcenter.Client 2.5.0 setzen."
|
|
},
|
|
{
|
|
"area": "sdk",
|
|
"breaking": false,
|
|
"title": "Unauthorized auch im API-Zweig",
|
|
"text": "Ein 401 wurde nur auf dem statischen Weg als Lizenzproblem erkannt; ueber die API kam er als gewoehnlicher HTTP-Fehler an.",
|
|
"action": null
|
|
},
|
|
{
|
|
"area": "tooling",
|
|
"breaking": false,
|
|
"title": "pack-and-deploy ist beziehbar, Release-Vorlage vorhanden",
|
|
"text": "Das Werkzeug wurde in der Anleitung benutzt, als laege es im PATH - beziehbar war es nirgends. Es steht jetzt unter /installer/ neben dem Agenten. Dazu eine Vorlage zum Kopieren ins eigene Projekt, die je Plattform dotnet publish und pack-and-deploy verkettet und sich das Werkzeug selbst holt.",
|
|
"action": "Empfohlen: Vorlage von /docs/release-template/ holen. Anleitung unter /docs/release.md."
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"version": "2.4.0",
|
|
"date": "2026-08-13",
|
|
"summary": "Release-Ablage liegt hinter Zugangsschutz. Ohne Lizenzschluessel keine Updates mehr.",
|
|
"actionRequired": true,
|
|
"changes": [
|
|
{
|
|
"area": "server",
|
|
"breaking": true,
|
|
"title": "/releases/ verlangt Zugangsdaten",
|
|
"text": "Zuvor konnte jeder im Internet die vollstaendigen Pakete herunterladen. Zugang haben jetzt gueltige Lizenzschluessel des jeweiligen Produkts sowie die Installationskonten. Je Produkt eine eigene .htpasswd - eine gemeinsame wuerde bedeuten, dass eine Lizenz fuer Produkt A auch Produkt B oeffnet.",
|
|
"action": "PFLICHT: credentials: ReleaseCredentials.FromLicenseKey(schluessel) an CheckForUpdateAsync, und licenseKey an LaunchUpdateAgent. Ohne das bekommt die Anwendung 401 und keine Updates mehr."
|
|
},
|
|
{
|
|
"area": "server",
|
|
"breaking": true,
|
|
"title": "Benutzername ist eine Ableitung, nicht der Schluessel",
|
|
"text": "Das htpasswd-Format hasht nur die Passwortspalte. Stuende der Lizenzschluessel auch als Benutzername darin, waere die Datei eine Klartext-Kundenliste. Der Benutzername ist deshalb lic_<sha256(schluessel), 16 Hexzeichen>.",
|
|
"action": "Nur relevant, wenn ihr Basic Auth selbst baut statt ReleaseCredentials zu benutzen."
|
|
},
|
|
{
|
|
"area": "sdk",
|
|
"breaking": false,
|
|
"title": "UpdateCheckResult.Unauthorized",
|
|
"text": "Trennt 'Lizenz traegt nicht mehr' von einem Netzwerkfehler. Ohne diese Unterscheidung sieht ein abgelaufener Vertrag aus wie eine Stoerung, und man sucht an der falschen Stelle.",
|
|
"action": "Empfohlen: Unauthorized abfragen und dem Benutzer als Lizenzhinweis zeigen."
|
|
},
|
|
{
|
|
"area": "server",
|
|
"breaking": false,
|
|
"title": "Reihenfolge bei Neuprodukten",
|
|
"text": "Fuer ein Produkt ohne Release existiert /releases/<slug>/ nicht und wird uebersprungen. Das Verzeichnis entsteht mit dem ersten Upload, der naechste Abgleich schuetzt es. Es gibt also kein Fenster, um ein ungeschuetztes Release zu ziehen und danach das SDK nachzuruesten.",
|
|
"action": "Der erste ausgelieferte Build eines neuen Produkts muss die Zugangsdaten schon mitbringen."
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"version": "2.3.0",
|
|
"date": "2026-08-13",
|
|
"summary": "Erstinstallation ueber den Update-Agent, setup.json, Installationskonto.",
|
|
"actionRequired": false,
|
|
"changes": [
|
|
{
|
|
"area": "agent",
|
|
"breaking": false,
|
|
"title": "update-agent --action install",
|
|
"text": "Fuehrt durch Anmeldung, Auswahl aus dem Katalog, Zielverzeichnis, Installation und Einrichtung. Die Dateien kommen ueber denselben Pfad wie ein Update - mit Pruefsumme, Signatur, Staging und Rollback.",
|
|
"action": "Optional: setup.json ins Publish-Verzeichnis legen, damit die Erstinstallation die noetigen Werte abfragen kann."
|
|
},
|
|
{
|
|
"area": "agent",
|
|
"breaking": false,
|
|
"title": "setup.json beschreibt die einzurichtenden Werte",
|
|
"text": "Gefragt wird nur, was uebrig bleibt: bereits gesetzt -> detect:... -> provision -> fragen. Ziele haben ein 'location' (install, config, data, home) und optional fileWindows/fileLinux/fileMacOS. Dateien mit geheimen Werten werden auf den eigenen Benutzer beschraenkt.",
|
|
"action": "Optional. Ohne setup.json laesst sich die Anwendung installieren, aber nicht einrichten."
|
|
},
|
|
{
|
|
"area": "server",
|
|
"breaking": false,
|
|
"title": "Rolle 'installer' und Benutzerverwaltung",
|
|
"text": "Konten dieser Rolle koennen sich ausschliesslich ueber /api/setup/v1/login anmelden und Anwendungen einrichten - nicht am WebUI. Die Zugangsdaten werden auf jedem Zielsystem eingetippt; mit einem Administratorkonto verteilte man den Zugang zur gesamten Verwaltung.",
|
|
"action": "Migration 012 einspielen. Fuer Erstinstallationen ein installer-Konto anlegen."
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"version": "2.2.0",
|
|
"date": "2026-08-09",
|
|
"summary": "Plattform-Dimension, signierte Releases, Update mit Rollback, geschuetzte Konfigurationsdateien.",
|
|
"actionRequired": true,
|
|
"changes": [
|
|
{
|
|
"area": "server",
|
|
"breaking": true,
|
|
"title": "Releases tragen eine Plattform",
|
|
"text": "Zuvor gab es die Dimension nicht: win-x64 und linux-x64 landeten unter derselben Version im selben Kanal und ueberschrieben sich: ein Linux-System zog sich das Windows-Paket. Ein Client, der keine Plattform mitschickt, sieht ausschliesslich Releases mit platform=any.",
|
|
"action": "PFLICHT beim Veroeffentlichen: --platform win-x64 (o. ae.) an pack-and-deploy. Clientseitig passiert es von selbst - CheckForUpdateAsync schickt ohne Angabe die Kennung des laufenden Systems."
|
|
},
|
|
{
|
|
"area": "packager",
|
|
"breaking": true,
|
|
"title": "Version wird gegen die Hauptassembly geprueft",
|
|
"text": "Weicht --version von der einkompilierten ab, bricht der Vorgang ab. Wird 1.0.1 als 1.0.2 veroeffentlicht, aktualisieren alle Clients, melden danach weiter 1.0.1, halten das Release erneut fuer neu - eine Endlosschleife ueber die gesamte Installationsbasis.",
|
|
"action": "<Version> in die Directory.Build.props, nicht in einzelne csproj-Dateien. Notausgang: --ignore-version-mismatch."
|
|
},
|
|
{
|
|
"area": "packager",
|
|
"breaking": true,
|
|
"title": "preservePatterns schuetzt Konfigurationsdateien",
|
|
"text": "Zuvor ueberschrieb jedes Update die eingerichteten Werte des Zielsystems. Ausschluss und Schutz sind zwei verschiedene Dinge: excludePatterns haelt eine Datei aus dem Paket, preservePatterns liefert sie aus, laesst am Ziel aber die vorhandene Fassung in Ruhe. Die Muster sind jetzt echte Globs - 'logs/**' traf zuvor nie zu.",
|
|
"action": "preservePatterns in die packager.config.json aufnehmen. Pruefen, ob eure Konfigurationsdateien bisher ueberschrieben wurden."
|
|
},
|
|
{
|
|
"area": "agent",
|
|
"breaking": false,
|
|
"title": "Anwenden mit Backup und Rollback",
|
|
"text": "Die Stelle war als 'Atomic Replace with Backup' kommentiert und war eine Kopierschleife. Bricht sie ab, blieb eine halb aktualisierte Installation zurueck. Jetzt: Plan, Backup, Anwenden, bei Fehler vollstaendiger Rollback. Verwaiste Dateien werden entfernt - aber nur solche aus dem Manifest der Vorversion.",
|
|
"action": null
|
|
},
|
|
{
|
|
"area": "sdk",
|
|
"breaking": true,
|
|
"title": "LaunchUpdateAgent uebergibt Neustart und Prozesskennung",
|
|
"text": "--restart wurde nie uebergeben - die Anwendung schloss sich nach 'Jetzt installieren' und blieb zu. --wait-for-pid gab es nicht: der Agent kopierte bei langsamem Herunterfahren ueber gesperrte Dateien. ResolveAgentPath() liefert den plattformrichtigen Namen; ein fest verdrahtetes update-agent.exe wird unter Linux nie gefunden.",
|
|
"action": "UpdateClient.ResolveAgentPath() statt eines festen Pfads benutzen. currentVersion: BuildInfo.Version mitgeben - fuer Installationen ohne manifest.json."
|
|
},
|
|
{
|
|
"area": "server",
|
|
"breaking": false,
|
|
"title": "Releases werden signiert",
|
|
"text": "Der SHA256 stammt aus derselben Quelle wie das Paket. Wer den Webroot kontrolliert, tauscht beide gemeinsam aus. Der Server signiert jetzt mit RSA-SHA256, der Agent prueft gegen /api/updateservice/v1/pubkey. Bewusst asymmetrisch: bei HMAC braeuchte der pruefende Agent denselben geheimen Schluessel.",
|
|
"action": "Serverseitig security.release_private_key hinterlegen. Ohne Schluessel bleiben Releases unsigniert und installierbar."
|
|
},
|
|
{
|
|
"area": "sdk",
|
|
"breaking": true,
|
|
"title": "CheckForUpdateAsync hat einen Parameter mehr",
|
|
"text": "platform steht vor dem CancellationToken. Wer den Token positionell uebergeben hat, bekommt einen Uebersetzungsfehler - kein stilles Fehlverhalten.",
|
|
"action": "Benannte Argumente benutzen."
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"version": "2.1.0",
|
|
"date": "2026-08-08",
|
|
"summary": "Korrekturen im .NET-SDK, app_version am Heartbeat.",
|
|
"actionRequired": true,
|
|
"changes": [
|
|
{
|
|
"area": "sdk",
|
|
"breaking": true,
|
|
"title": "BuildInfo.targets erzeugt im Namensraum des Projekts",
|
|
"text": "Die vorherige Fassung erzeugte fest in Deploymentcenter.Client.Models und war damit nicht einbindbar (CS0433).",
|
|
"action": "Ein etwaiges 'using Deploymentcenter.Client.Models' fuer BuildInfo entfernen."
|
|
},
|
|
{
|
|
"area": "sdk",
|
|
"breaking": false,
|
|
"title": "API-Rueckfall der Update-Pruefung liefert vollstaendige Daten",
|
|
"text": "Fehlte die latest.json, kamen ueber die API weder Download-Adresse noch Pruefsumme, Changelog oder Kritikalitaet an - nur 'version' stimmte in beiden Formaten ueberein.",
|
|
"action": "Eigene Umgehungen koennen entfallen."
|
|
},
|
|
{
|
|
"area": "sdk",
|
|
"breaking": true,
|
|
"title": "unknown_error entfaellt, cache_ttl_hours wird ausgewertet",
|
|
"text": "Der Cache-Rueckfall greift jetzt bei jedem HTTP-Fehler, nicht nur bei bestimmten. app_version ist Parameter statt fest.",
|
|
"action": "Wer unknown_error abfaengt, prueft stattdessen IsTransient. LicenseClient.DefaultAppVersion beim Start setzen."
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"version": "2.0.0",
|
|
"date": "2026-08-07",
|
|
"summary": "Sicherheitsumstellung: Veroeffentlichen braucht ein Token, Versionsvergleich nach Semver.",
|
|
"actionRequired": true,
|
|
"changes": [
|
|
{
|
|
"area": "server",
|
|
"breaking": true,
|
|
"title": "Veroeffentlichen verlangt updateservice:publish",
|
|
"text": "Zuvor voellig ungeschuetzt - jeder konnte download_url und sha256_hash eines bestehenden Releases ueberschreiben und allen Clients ein beliebiges Paket unterschieben.",
|
|
"action": "Token mit dem Recht updateservice:publish erzeugen und als DC_TOKEN hinterlegen."
|
|
},
|
|
{
|
|
"area": "server",
|
|
"breaking": true,
|
|
"title": "Versionsvergleich folgt der semantischen Ordnung",
|
|
"text": "Zuvor verglich SQL lexikografisch: 1.9.0 galt als neuer als 1.10.0, und Clients bekamen ein Downgrade als Update angeboten.",
|
|
"action": null
|
|
},
|
|
{
|
|
"area": "server",
|
|
"breaking": true,
|
|
"title": "Einheitliches Antwortformat",
|
|
"text": "Alle JSON-Endpunkte antworten mit {\"status\":\"success\",...} bzw. {\"status\":\"error\",\"error\":{\"code\":...}}. Der code ist stabil und fuer Programme gedacht, die message richtet sich an Menschen.",
|
|
"action": "Auswertung der Antworten auf das neue Format umstellen."
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|