Files
ClawdDotNet/docs/archiv/Lizenz-HardwareId-v2-Implementierungsvorschlag.md
RichardandClaude Opus 5 740649789e Eine Roadmap statt neun verteilter Listen
Der Stand lag ueber eine Bestandsaufnahme, drei Konzeptpapiere, vier
Umsetzungsplaene und zwei Deploymentcenter-Dokumente verteilt - jedes mit
eigener Reihenfolge, teils widersprueglich. Alle offenen Punkte daraus sind
in docs/Roadmap.md zusammengefuehrt.

Aufbau der neuen Roadmap
- Statusvokabular: erledigt / beschlossen-offen / Entscheidung noetig /
  zurueckgestellt / Idee. Damit steht das Geparkte sichtbar drin, statt in
  einem Konzeptpapier zu verschwinden.
- Herkunft-Spalte traegt das alte Kuerzel (S4, K2, B9, T4, F-A1, C1, DC1),
  damit die archivierten Papiere auffindbar bleiben, ohne sie zu lesen.
- Abschnitt 1 Reihenfolge, 2 offene Entscheidungen, 3 die Vorhaben nach
  Bereich, 4 Ideenspeicher, 5 Chronik des Erledigten, 6 Modell-Einstufung,
  7 Herkunftskarte.

Was dabei sichtbar wurde
- Neun Entscheidungen blockieren Arbeit, ohne dass sie Aufwand kosten -
  allen voran Matrix oder Rocket.Chat. Sie stehen jetzt gesammelt in
  Abschnitt 2 statt verstreut in den Diskussionsteilen der Konzepte.
- B8 (keine Tiefenbegrenzung bei AgentComm) ist unveraendert offen. Das ist
  keine Theorie: A haelt sein Gate, waehrend es auf B wartet - ruft B nun A,
  warten beide bis zum Timeout. Der Testfall A13 dafuer fehlt bis heute.
- Die vier Umsetzungsplaene vom 2026-08-05 sind alle unumgesetzt und waren
  in keiner Roadmap verzeichnet.

Archiv
Sechs Dokumente ziehen nach docs/archiv/: Bestandsaufnahme, Konzepte
Backup/Finanz/Analyse, Linux-Portierung-Analyse, Lizenz-HardwareId-v2
(gegenstandslos - LicenseLabrador ist ersetzt), Deploymentcenter-Review und
der 2.4-Integrationsplan. Sie bleiben als Begruendung lesbar, werden aber
nicht mehr fortgeschrieben; das README ordnet jedes einzeln ein und warnt,
dass ihre Quelltext-Verweise ins Leere gehen koennen.

Bauplan bleibt Bauplan
Taskboard, Audit, Staging, Memory, Agentenkommunikation, RocketChat und
Deploymentcenter-Integration bleiben in docs/ - sie sind die Detailvorgabe
fuer die Umsetzung, nicht Vorhabenlisten. Jedes bekommt oben eine Zeile,
die seine Rolle und den Umsetzungsstand nennt und auf die Roadmap zeigt.
Dasselbe fuer die vier Umsetzungsplaene: die Reihenfolge gilt in der
Roadmap, nicht im Plan.

Nebenbei repariert: Taskboard-Konzept und Teststrategie verwiesen auf
AgentScheduler und ToolJobScheduler, die seit der Scanner-Konsolidierung
geloescht sind. Alle Dokument-Verweise in docs/ sind geprueft.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 18:31:06 +02:00

23 KiB
Raw Permalink Blame History

Hardware-ID v2 — Implementierungsvorschlag

Stand: 2026-08-06. Betrifft LicenseLabrador (Client + Server) und die Aufrufseite in ClawdDotNet (Services/LicenseGate.cs).

Anlass: Für den Linux-Umzug muss die Hardware-Bindung auf beiden Plattformen funktionieren. Bei der Durchsicht sind dabei zwei Probleme aufgefallen, die nichts mit Linux zu tun haben, aber denselben Code betreffen — die sollten in einem Zug mit erledigt werden.


1. Befund

1.1 Der Rechnername steckt im Hash — das ist das eigentliche Problem

HardwareId.cs:43:

rawBuilder.Append(Environment.MachineName);   // "for system isolation"

Folge: Ein umbenannter Rechner ist eine neue Maschine. Er verbraucht einen weiteren Aktivierungsplatz, und der alte bleibt für immer belegt (max_activations ist standardmäßig 2 — nach zwei Umbenennungen ist die Lizenz dicht). Das gilt bereits heute unter Windows.

Unter Linux wird daraus ein Totalausfall: In einem Container ist der Hostname standardmäßig die gekürzte Container-ID, also bei jedem Start ein anderer. Die Lizenz wäre nach dem zweiten docker run verbraucht.

Die Absicht („system isolation") ist auch nicht erfüllt: Der Rechnername steht ohnehin im Feld hostname, das der Server bei jeder Prüfung mitschreibt (LicenseService.php:112). Diagnostisch verlieren wir nichts, wenn er aus dem Hash verschwindet.

1.2 Die MAC-Ausweichlösung ist unter Linux instabil

HardwareId.cs:86 nimmt die alphabetisch erste physische MAC. Unter Linux:

  • Die Stoppwortliste kennt docker und veth, aber nicht br- (Bridges), virbr (libvirt), cni, flannel, cali (Kubernetes), zt (ZeroTier).
  • NetworkInterfaceType meldet unter Linux für die meisten virtuellen Geräte schlicht Ethernet — die Typprüfung greift also nicht.
  • Bridge- und veth-MACs werden von systemd je Boot neu zufällig vergeben.

Sortiert man solche Adressen mit, wechselt die Hardware-ID beim Neustart. Die Ausweichlösung ist damit unter Linux schlimmer als keine.

1.3 Der Zustandsspeicher fällt still auf Klartext zurück

StateStore.cs:41 beim Lesen und :90 beim Schreiben:

try   { decryptedData = ProtectedData.Unprotect(rawData, null, ...); }
catch { decryptedData = rawData; }        // ← Klartext wird akzeptiert

Unter Linux wirft DPAPI immer, also läuft alles über den Klartextzweig. Zwei Folgen:

  • SECURITY.md behauptet, der Cache sei „strikt an die hardware_id gebunden". Das stimmt für die Hülle (die Prüfung in LicenseClient.cs:198), nicht für die Cache-Datei selbst.
  • Ernster: max_seen_time ist die Uhr-Rückdreh-Sperre (StateStore.cs:107). Wer eine state.dat von Hand schreiben kann, setzt den Wert auf 0 und stellt die Systemuhr zurück. Der Klartext-Rückfall beim Lesen macht das möglich, und zwar auf jeder Plattform, auf der DPAPI nicht greift.

1.4 Ablageort bricht bei einem systemd-Dienst weg

LicenseConfig.cs:22 verwendet Environment.GetFolderPath(SpecialFolder.ApplicationData). Läuft der Dienst unter User=clawd ohne Heimatverzeichnis, ist HOME nicht gesetzt und GetFolderPath liefert einen leeren String. Path.Combine("", slug, "license") ergibt einen relativen Pfad — die Lizenz landet im Arbeitsverzeichnis oder gar nicht.

1.5 Kein Formatkennzeichen, keine Plattformangabe

Die Hardware-ID ist heute ein nackter SHA-256-Hex-String. Es gibt keine Möglichkeit, im Server zu erkennen, aus welcher Quelle oder von welchem Betriebssystem eine Aktivierung stammt — und keinen Weg, das Format je zu wechseln, ohne alle bestehenden Aktivierungen zu verlieren.

Randnotiz: OperatingSystemHelpers.IsWindows() nutzt Environment.OSVersion.Platform == PlatformID.Win32NT. Das funktioniert zufällig richtig (Linux liefert Unix), ist aber die veraltete API. RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ist in netstandard2.0 verfügbar und der korrekte Weg.


2. Zielbild: das Format

2:<plattform>:<64 Hex-Zeichen>

Beispiele:
  2:win:9f3ab7c1…      (Windows, MachineGuid)
  2:lin:41e0d5aa…      (Linux, /etc/machine-id)
  2:lin:7c9182ff…      (Linux, Vorgabe per Umgebungsvariable)

68 Zeichen — passt in activations.hardware_id VARCHAR(128) ohne Schemaänderung. Der Doppelpunkt ist unproblematisch, die Spalte ist utf8mb4_unicode_ci und wird nur verglichen.

Der Hash selbst:

sha256( "LicenseLabrador-HWID-v2" ‖ "\n" ‖ plattform ‖ "\n" ‖ quelle ‖ "\n" ‖ rohwert )
  • Kein MachineName. (1.1)
  • Die Domänenzeichenkette verhindert, dass derselbe Rohwert in anderem Zusammenhang wiederverwendbar ist.
  • quelle geht mit in den Hash: Findet der Client später eine bessere Quelle, ändert sich die ID bewusst und nachvollziehbar, statt zufällig.

Zusätzlich gehen drei neue Felder mit in die Anfrage — nicht in den Hash, nur zur Diagnose und für die Migration:

Feld Beispiel Zweck
hwid_version 2 Formaterkennung serverseitig
hwid_source machine-id Admin sieht, wie stabil die Bindung ist
legacy_hardware_id <v1-Hash> Migration ohne Platzverlust (Abschnitt 4)

3. Quellen je Plattform

Reihenfolge = Priorität. Die erste Quelle, die einen nichtleeren, plausiblen Wert liefert, gewinnt.

3.1 Vorgabe (alle Plattformen, höchste Priorität)

LicenseConfig.HardwareIdOverride     (Code)
LICENSELABRADOR_HWID                 (Umgebungsvariable)

Quelle: override. Der Rohwert wird trotzdem gehasht, damit das Format einheitlich bleibt.

Das ist der ehrliche Weg für Container und Serverbetrieb. Heuristik kann dort nicht gewinnen — in einem Container gibt es keine Hardware, an die man binden könnte. Der Betreiber setzt einen stabilen Wert, hinterlegt ihn im Deployment-Geheimnis, und die Bindung ist so verlässlich wie dieser Wert. Eine Zeile in der systemd-Unit statt eines Ratespiels.

3.2 Windows

# Quelle hwid_source
1 HKLM\SOFTWARE\Microsoft\Cryptography\MachineGuid (Registry64) machine-guid
2 Stabile physische MAC (Abschnitt 3.4) mac
3 Erzeugte Datei (Abschnitt 3.5) keyfile

Unverändert zu heute — nur ohne MachineName im Hash.

3.3 Linux

# Quelle hwid_source Anmerkung
1 /etc/machine-id machine-id Von systemd bei der Installation erzeugt, überlebt Neustarts und Kernel-Updates. Die richtige Wahl auf einem echten System.
2 /var/lib/dbus/machine-id dbus-machine-id Ältere Systeme ohne systemd.
3 /sys/class/dmi/id/product_uuid dmi-uuid SMBIOS-UUID, echte Hardware-Bindung. Meist nur für root lesbar (0400) — Versuch in try einpacken, kein Fehler wenn nicht lesbar. Bei VMs vom Hypervisor gesetzt und dort stabil.
4 Stabile physische MAC (Abschnitt 3.4) mac
5 Erzeugte Datei (Abschnitt 3.5) keyfile

Zwei Fallen bei /etc/machine-id, die geprüft werden müssen:

  • Leer oder nur Zeilenumbruch. Auf Systemen mit systemd-firstboot oder in manchen Images existiert die Datei, ist aber leer. Muss als „nicht vorhanden" behandelt werden, nicht als gültiger Wert — sonst haben alle diese Installationen dieselbe ID.
  • Der Wert uninitialized. Genau diese Zeichenkette schreibt systemd, wenn die ID im laufenden Betrieb noch nicht festgelegt ist. Ebenfalls verwerfen.
private static bool IsPlausibleMachineId(string? v)
    => !string.IsNullOrWhiteSpace(v)
       && v.Trim().Length >= 16
       && !v.Trim().Equals("uninitialized", StringComparison.OrdinalIgnoreCase)
       && v.Trim().Trim('0').Length > 0;      // nicht alles Nullen

3.4 MAC-Ausweichlösung, überarbeitet

Die heutige Fassung nimmt FirstOrDefault() der sortierten Liste. Wenn eine Schnittstelle dazukommt oder wegfällt, kann sich damit die gewählte MAC ändern. Besser: alle gültigen MACs sortiert verketten — dann ändert sich der Wert nur, wenn sich die Netzwerkausstattung wirklich ändert, und nicht schon, weil eine Adresse hinzukommt, die vorher sortiert davor lag.

Stoppwortliste erweitern um: br-, virbr, cni, flannel, cali, weave, zt, tailscale, ipsec, sit, gre, dummy, bond, macvlan, ovs.

Zusätzlich hart ausschließen (unabhängig vom Namen):

  • Schnittstellen mit gesetztem „locally administered"-Bit (zweites Bit des ersten Oktetts, mac[0] & 0x02). Genau das setzt systemd bei zufällig erzeugten MACs für veth und Bridges. Ein sauberer, namensunabhängiger Filter — und der wirksamste von allen.
  • Unter Linux zusätzlich prüfen: existiert /sys/class/net/<name>/device? Fehlt das Verzeichnis, hat die Schnittstelle kein physisches Gerät und ist virtuell. Das ist zuverlässiger als jede Namensliste.
// Namensunabhängig: zufällig erzeugte MACs tragen dieses Bit.
private static bool IsLocallyAdministered(PhysicalAddress addr)
{
    var b = addr.GetAddressBytes();
    return b.Length > 0 && (b[0] & 0x02) != 0;
}

3.5 Erzeugte Datei als letzte Stufe

<StorageDirectory>/machine.key — 32 Zufallsbytes, Base64, Dateirechte 0600. Wird nur angelegt, wenn keine Quelle davor greift.

Das ist eine Installations- und keine Hardware-Bindung. Für einen Container ohne Vorgabe ist das aber die Wahrheit, und mit einem gemounteten Datenverzeichnis bleibt sie über Container-Neustarts stabil. hwid_source = keyfile macht dem Admin sichtbar, dass diese Aktivierung schwächer gebunden ist als die anderen.

Wichtig: Die Datei gehört ins Datenverzeichnis, nicht neben die Programmdatei. Sonst ist sie bei jedem Deployment weg.


4. Migration v1 → v2 ohne Platzverlust

Der Kern: Der Client kennt beide IDs und schickt beide mit. Der Server zieht die alte Aktivierung auf die neue ID um, statt eine zweite anzulegen.

ClientHardwareId bekommt neben GetHardwareId() (v2) ein GetLegacyHardwareId(), das die heutige v1-Berechnung unverändert beibehält (inklusive MachineName, damit sie zu bestehenden Aktivierungen passt). Beides geht in die Anfrage:

hardware_id        = "2:lin:41e0…",
legacy_hardware_id = "8fa2…",          // nur solange v1-Aktivierungen existieren
hwid_version       = 2,
hwid_source        = "machine-id",

Server — in LicenseService::validate, an der Stelle der heutigen Suche (LicenseService.php:100):

1. Aktivierung mit hardware_id = <v2> suchen
   → gefunden: normaler Weg (last_seen, hostname, app_version aktualisieren)

2. nicht gefunden, und legacy_hardware_id ist gesetzt:
   Aktivierung mit hardware_id = <v1> suchen
   → gefunden: UPDATE activations SET hardware_id = <v2>, hwid_version = 2,
               hwid_source = <quelle> WHERE id = …
               + audit_log-Eintrag 'hwid_migrated'
               → weiter wie unter 1. KEIN neuer Platz verbraucht.

3. weder noch: neue Aktivierung anlegen, max_activations prüfen (wie heute)

Damit wandern alle bestehenden Windows-Installationen beim ersten Start nach dem Update lautlos auf v2 — niemand merkt etwas, kein Aktivierungsplatz geht verloren. Das legacy_hardware_id-Feld kann nach einer Übergangszeit (etwa zwei Veröffentlichungen) aus dem Client fallen.

4.1 Der lokale Cache muss einmal verworfen werden

Nicht übersehen: Die Hardware-ID geht in zwei weitere Berechnungen ein — CalculateHmac(state, licenseKey, _hardwareId) für die Prüfsumme in LicenseResult (LicenseClient.cs:272) und den Seed des Speicherschutzes (:287). Nach dem Formatwechsel schlägt VerifyChecksum für jede zwischengespeicherte Hülle fehl.

Das ist kein Fehler, sondern erwartet — muss aber als Cache-Fehltreffer behandelt werden (einmal online neu prüfen), nicht als TamperSuspected. Sonst sperrt sich jede bestehende Installation beim ersten Start nach dem Update selbst aus. Der Weg dorthin: Cache-Version im LocalCacheData mitführen (schema_version: 2) und einen Datensatz mit abweichender Version verwerfen, bevor die Prüfsumme überhaupt geprüft wird.

4.2 Schemaerweiterung

ALTER TABLE activations
  ADD COLUMN hwid_version TINYINT NOT NULL DEFAULT 1 AFTER hardware_id,
  ADD COLUMN hwid_source  VARCHAR(32) NULL      AFTER hwid_version,
  ADD COLUMN platform     VARCHAR(8)  NULL      AFTER hwid_source;

Alles mit Vorgabewerten, also rückwärtskompatibel — ein alter Client, der die Felder nicht schickt, funktioniert unverändert weiter.


5. Umzug Windows → Linux

Das ist etwas anderes als die Formatmigration: hier wechselt die Maschine wirklich, die ID muss sich also ändern. Drei Wege, alle drei sinnvoll parallel:

5.1 Der Normalfall braucht gar nichts

max_activations ist standardmäßig 2. Ein Windows-Entwicklungsrechner und ein Linux-Server passen also ohne jeden Eingriff hinein. Für den anstehenden Umzug ist das wahrscheinlich die ganze Antwort — die anderen beiden Punkte sind für den Fall danach.

5.2 Abmelden vor dem Umzug (existiert, aber nicht erreichbar)

LicenseService::deactivate löscht die Aktivierungszeile und gibt den Platz frei (LicenseService.php:153), und LicenseClient.DeactivateAsync ruft es auf. In ClawdDotNet ist die Methode aber nur über LicenseGate.cs:39 erreichbar und dort an die GUI gebunden.

Nachzuliefern: ein Kommandozeilenschalter am Host, damit das auch ohne Oberfläche geht.

clawddotnet --license-deactivate

Das braucht der kopflose Betrieb ohnehin (siehe Linux-Portierung-Analyse.md, 2.6 — der Lizenzdialog ist ein MessageBox, der einen Dienst blockieren würde).

5.3 Umbinden aus der Verwaltung (fehlt noch)

Für den Fall, dass die alte Maschine schon weg ist: In public/admin/license_detail.php je Aktivierungszeile eine Schaltfläche „Aktivierung freigeben" (löscht die Zeile, gibt den Platz frei). Ein echtes „Umbinden" auf eine bekannte neue ID ist unnötig — Freigeben plus Neuaktivierung auf dem Zielsystem ist derselbe Vorgang mit weniger Code und einer klareren Spur im Prüfprotokoll.

Beides sollte in audit_log landen, mit altem und neuem Wert.


6. Zustandsspeicher härten

Zusammen mit dem HW-ID-Umbau, weil dieselbe Datei betroffen ist und die Verschlüsselung den HW-ID als Schlüsselmaterial braucht.

Format — feste Hülle statt „mal so, mal so":

Magic "LLS2" (4 Byte) │ Nonce (12) │ Ciphertext │ GCM-Tag (16)
  • AES-GCM, Schlüssel abgeleitet aus HW-ID + ProductSlug per HKDF-SHA256.
  • Auf Windows das Ergebnis zusätzlich in DPAPI wickeln (Gürtel und Hosenträger, kostet nichts).
  • Dateirechte 0600 auf Unix.

Der entscheidende Punkt: den Klartext-Rückfall beim Lesen entfernen. Eine Datei, die sich nicht entschlüsseln oder nicht authentifizieren lässt, ist kein Cache — sie wird verworfen und der Client prüft online. Nicht als Klartext akzeptieren. Genau dieser Rückfall macht heute die Uhr-Rückdreh-Sperre umgehbar (1.3).

Einmalig weiterhin lesbar bleiben muss das alte Format (Datei ohne LLS2-Magic): einlesen, in v2 neu schreiben, fertig. Nach einer Veröffentlichung kann der Pfad weg.

6.1 netstandard2.0 hat kein AesGcm — Empfehlung: mehrfach zielen

System.Security.Cryptography.AesGcm gibt es erst ab .NET Core 3.0, HKDF erst ab .NET 5, File.SetUnixFileMode erst ab .NET 7. Das Projekt zielt heute auf netstandard2.0.

Zwei Wege:

  1. <TargetFrameworks>netstandard2.0;net8.0</TargetFrameworks>empfohlen. ClawdDotNet (net10.0) zieht automatisch das net8.0-Ziel und bekommt AesGcm, HKDF und File.SetUnixFileMode ohne Umwege. Der netstandard2.0-Zweig bleibt für andere Abnehmer erhalten und nutzt dort BouncyCastle. Kosten: ein paar #if NET8_0_OR_GREATER-Blöcke an genau drei Stellen.
  2. Durchgängig BouncyCastle (GcmBlockCipher, HkdfBytesGenerator) — die Bibliothek ist mit BouncyCastle.Cryptography bereits als Abhängigkeit da, also kein neues Paket. Kein Mehrfachziel nötig, aber die Dateirechte bleiben ein Problem: chmod müsste per P/Invoke laufen.

Weg 1 ist sauberer, weil er nebenbei das Dateirechte-Problem löst.


7. Ablageort (1.4)

Auflösungskette in LicenseConfig.StorageDirectory, erste nutzbare gewinnt:

  1. Explizit gesetzter Wert (ClawdDotNet setzt ihn künftig — der Host hat ohnehin eine eigene XDG-Auflösung).
  2. LICENSELABRADOR_STORAGE_DIR.
  3. Unix: $XDG_CONFIG_HOME/<slug>/license, sonst $HOME/.config/<slug>/license.
  4. Windows: SpecialFolder.ApplicationData wie heute.
  5. Letzter Ausweg: <AppContext.BaseDirectory>/license.

Und in jedem Fall: nie einen leeren Pfad durchlassen. Der heutige Code kann Path.Combine("", …) erzeugen, ohne dass es auffällt. Ein if (string.IsNullOrEmpty(...)) throw an dieser Stelle ist besser als eine Lizenzdatei, die im Arbeitsverzeichnis landet und beim nächsten Start nicht mehr gefunden wird.


8. Änderungsliste

LicenseLabrador — Client

Datei Was
HardwareId.cs Neuschreiben: v2-Format, Quellenkette je Plattform, GetLegacyHardwareId(), HwidSource/Platform als Eigenschaften, MAC-Filter (locally-administered-Bit, /sys/class/net/*/device), Plausibilitätsprüfung für machine-id, machine.key-Erzeugung
LicenseConfig.cs HardwareIdOverride, Auflösungskette für StorageDirectory, leeren Pfad ausschließen
StateStore.cs LLS2-Hülle, AES-GCM, schema_version, Klartext-Rückfall beim Lesen entfernen, v1-Einmalmigration, 0600
LicenseClient.cs Neue Felder in validate/deactivate senden; Cache mit abweichender schema_version als Fehltreffer behandeln, nicht als TamperSuspected
OperatingSystemHelpers RuntimeInformation.IsOSPlatform, dazu IsLinux()/IsMacOs()
LicenseLabrador.Client.csproj netstandard2.0;net8.0

LicenseLabrador — Server

Datei Was
sql/schema.sql + Migrationsskript hwid_version, hwid_source, platform
src/LicenseService.php legacy_hardware_id entgegennehmen; Migrationssuche (Abschnitt 4); neue Felder speichern
src/Audit.php Ereignisart hwid_migrated, activation_released
public/admin/license_detail.php Quelle/Plattform je Aktivierung anzeigen, „Aktivierung freigeben"
docs/SECURITY.md Aussage zur Cache-Bindung korrigieren (1.3)

ClawdDotNet

Datei Was
Services/LicenseGate.cs StorageDirectory explizit setzen; MessageBox/frm_License hinter eine Schnittstelle (ILicensePrompt) legen, damit der kopflose Host eine Konsolenfassung einsetzen kann
Host (neu) --license-deactivate, --license-set-key, --license-status
docs/Integrationsplan-WatchDog-LicenseLabrador.md Format v2 und Migrationsweg nachtragen

9. Testplan

Das Wichtigste zuerst — die Fälle, die heute schiefgehen würden:

Fall Erwartung
Rechner umbenennen ID unverändert (Kern von 1.1)
Container zweimal starten, /etc/machine-id im Abbild beide Male dieselbe ID
Container ohne machine-id, Datenverzeichnis gemountet ID über Neustarts stabil, hwid_source = keyfile
Container ohne machine-id, ohne Mount ID wechselt — muss so sein, und im Protokoll erkennbar
LICENSELABRADOR_HWID gesetzt gewinnt gegen alles, hwid_source = override
/etc/machine-id leer bzw. uninitialized wird verworfen, nächste Quelle greift
Docker-Bridge und veth vorhanden, keine machine-id MAC-Wahl ignoriert sie, ID über Neustart stabil
Bestehende v1-Windows-Aktivierung, Client aktualisiert Zeile wird auf v2 umgeschrieben, max_activations unverändert, Prüfprotokolleintrag
v1-Cache-Datei nach dem Update einmal online geprüft, dann v2-Cache — kein TamperSuspected
state.dat von Hand mit max_seen_time = 0 Datei wird verworfen, Uhr-Rückdreh-Sperre bleibt wirksam
systemd-Dienst ohne HOME Ablageort auflösbar, keine Datei im Arbeitsverzeichnis
Alter Client gegen neuen Server funktioniert unverändert (Felder haben Vorgabewerte)
Neuer Client gegen alten Server funktioniert, Zusatzfelder werden ignoriert

Die letzten beiden Zeilen sind nicht optional: Client und Server werden nicht gleichzeitig ausgerollt.


10. Aufwand

Block PT
HardwareId v2 samt Quellenkette, MAC-Filter, machine.key 23
Client mehrfach zielen + StateStore-Härtung 23
Server: Migrationssuche, Schema, Prüfprotokoll, Verwaltungsansicht 23
ClawdDotNet: ILicensePrompt, Lizenz-Kommandozeile 12
Tests (Container-Fälle brauchen echtes Docker) und Abnahme 12
Summe 813

Das ist mehr als die 35 PT, die in der Linux-Analyse für „Lizenz" standen — weil dort nur die Plattformverträglichkeit gerechnet war. Die Punkte 1.1 und 1.3 sind bestehende Fehler, die unabhängig vom Umzug behoben werden sollten; sie machen den Unterschied aus.

Der Block ist unabhängig vom übrigen Linux-Umzug und kann sofort beginnen — er hängt an keiner der offenen GUI-Entscheidungen.


11. Was ich anders machen würde als heute — kurz begründet

Drei Entscheidungen im Vorschlag verdienen eine Begründung, weil sie vom bisherigen Ansatz abweichen:

Rechnername raus. Er ist der Grund, warum die heutige Bindung fragiler ist als nötig, und er trägt nichts bei, was activations.hostname nicht schon festhält. Eine Bindung, die bei einer Umbenennung bricht, bindet nicht an Hardware, sondern an eine Konfiguration.

Vorgabe per Umgebungsvariable statt besserer Heuristik für Container. Man kann eine Container-Umgebung nicht sinnvoll erraten — es gibt dort keine Hardware. Jede zusätzliche Heuristik verschiebt nur, wo es falsch wird. Eine explizite Vorgabe ist ein bewusster Betreiberentscheid, in der Unit-Datei sichtbar, im Prüfprotokoll nachvollziehbar.

Kein Klartext-Rückfall, auch nicht „zur Sicherheit". Der heutige Rückfall sollte Robustheit bringen, kostet aber genau die Eigenschaft, für die der Cache existiert. Ein verworfener Cache bedeutet: einmal online prüfen. Das ist der mildere Schaden — und wer keine Verbindung hat, hat immer noch die Offline-Gnadenfrist aus der signierten Hülle, die von dieser Datei nicht abhängt.