Handschlag: --managed hebelt die Lizenzpruefung nicht mehr aus (#535)

Mit E-M3 wanderte die Lizenzpruefung in den Manager: Eine gefuehrte Instanz prueft
nicht mehr selbst, weil der Manager es zentral getan hat. Woran sie "gefuehrt"
erkannte, war der Schalter --managed - und ein Schalter ist keine Erkennung.
ClawdDotNet --managed startete damit eine Instanz ganz ohne Lizenzpruefung. Vor
E-M3 gab es die Luecke nicht; sie ist mit der Zentralisierung entstanden.

ManagerHandshake unterschreibt Instanzverzeichnis und Ablauf mit HMAC-SHA256. An
die Signierstelle kommt der Manager nur, wenn seine Lizenzpruefung bestanden hat -
StartInstanceAsync weist vorher mit StartRefusal.NoLicense ab. Dass die
Unterschrift existiert, ist der Beweis.

- Gebunden an das Verzeichnis, und geprueft wird gegen das tatsaechliche, nicht
  gegen das behauptete: Sonst unterschriebe man das Argument statt der Sache.
- Fuenf Minuten gueltig, der Ablauf geht in die Unterschrift ein.
- Ueber die Umgebung statt ueber die Befehlszeile: Eine Befehlszeile steht in
  jeder Prozessliste.
- Ungueltig heisst "nicht gefuehrt", nicht Abbruch. Dann prueft die Instanz ihre
  Lizenz selbst - der Weg von vor E-M3, also kein neuer Fehlerfall.

AppHost.IsManaged ist jetzt der gepruefte Befund. CanSwitchInstance und der
Wechseldialog haengen daran statt am Schalter.

Die Grenze steht ausdruecklich im Konzept (9.9): Der Schluessel liegt in jeder
ausgelieferten Programmdatei. Es geht um den gewoehnlichen Weg an der Lizenz
vorbei, nicht um jemanden mit Werkzeugen und Willen.

Abgenommen an echten Prozessen, Unterschrift fuer den Positivfall unabhaengig in
Python gerechnet: ohne Unterschrift "Lizenz erforderlich"; mit gueltiger kommt die
Instanz hoch; Unterschrift fuer ein fremdes Verzeichnis und hochgedrehter Ablauf
scheitern beide.

820 Tests gruen, 0 Warnungen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Richard
2026-09-10 15:11:49 +02:00
co-authored by Claude Opus 5
parent dcffcdcff5
commit e3403087c2
6 changed files with 346 additions and 10 deletions
+45 -3
View File
@@ -116,6 +116,17 @@ public sealed class AppHost : IAsyncDisposable
/// </summary>
public LicenseGate? License { get; private set; }
/// <summary>
/// Ob diese Instanz nachweislich vom Instanzmanager geführt wird (#535).
///
/// <para><b>Der geprüfte Befund, nicht der Schalter.</b> <c>--managed</c> ist eine
/// Behauptung, die jeder aufstellen kann; hier steht das Ergebnis der Prüfung von
/// <see cref="ManagerHandshake"/>. Wer daran eine Entscheidung hängt — ob die Instanz
/// sich selbst wechseln darf, ob die Lizenz zentral geprüft wurde —, fragt diesen
/// Wert und nicht die Befehlszeile.</para>
/// </summary>
public bool IsManaged { get; private set; }
/// <summary>
/// Die laufende Nachprüfung. Der Aufrufer hängt sich an
/// <see cref="LicenseWatch.Revoked"/> und beendet die Anwendung, wenn es feuert.
@@ -196,7 +207,15 @@ public sealed class AppHost : IAsyncDisposable
/// <summary>Wie der Lizenz-Torwächter mit dem Benutzer spricht. Null im geführten Modus.</summary>
public ILicensePrompt? License { get; init; }
/// <summary>Wahr, wenn die Instanz durch den Instanzmanager gestartet und geführt wird.</summary>
/// <summary>
/// Die Behauptung, vom Instanzmanager geführt zu werden — der Schalter
/// <c>--managed</c>.
///
/// <para><b>Eine Behauptung, kein Befund.</b> Ob sie stimmt, entscheidet allein
/// die Unterschrift des Managers (<see cref="ManagerHandshake"/>); dieser Schalter
/// sagt nur, dass überhaupt jemand den Anspruch erhebt. Ohne gültige Unterschrift
/// prüft die Instanz ihre Lizenz selbst.</para>
/// </summary>
public bool IsManaged { get; init; }
/// <summary>
@@ -289,8 +308,28 @@ public sealed class AppHost : IAsyncDisposable
}
// ─── 4. Lizenz ───
//
// Gefuehrt ist, wer es nachweisen kann (#535). Der Schalter --managed allein
// genuegte frueher und war damit der Weg an der Lizenz vorbei: Die Instanz
// uebersprang die Pruefung, weil sie glaubte, der Manager habe sie schon
// erledigt. Jetzt zaehlt die Unterschrift, und zwar gegen das Verzeichnis, in
// dem diese Instanz tatsaechlich laeuft.
var gefuehrt = callbacks.IsManaged
&& ManagerHandshake.Verify(
instancePath, Environment.GetEnvironmentVariable(
ManagerHandshake.EnvironmentVariable));
if (callbacks.IsManaged && !gefuehrt)
{
// Kein Abbruch: Ohne Nachweis ist die Instanz eben nicht gefuehrt und prueft
// selbst - der Weg von vor E-M3. Wer eine Lizenz hat, merkt davon nichts.
logger.LogWarning(
"Start mit --managed, aber ohne gültige Unterschrift des Instanzmanagers. "
+ "Die Instanz gilt als eigenständig und prüft ihre Lizenz selbst.");
}
LicenseGate? license = null;
if (!callbacks.IsManaged)
if (!gefuehrt)
{
var prompt = callbacks.License ?? new NonInteractiveLicensePrompt(Console.Error.WriteLine);
license = new LicenseGate(
@@ -308,7 +347,9 @@ public sealed class AppHost : IAsyncDisposable
}
else
{
logger.LogInformation("Instanz wird vom Manager geführt (--managed): Lizenzprüfung erfolgt zentral im Manager.");
logger.LogInformation(
"Instanz wird vom Instanzmanager geführt (Unterschrift geprüft): "
+ "Die Lizenzprüfung ist dort schon erfolgt.");
}
// ─── 5. Werkzeuge ───
@@ -334,6 +375,7 @@ public sealed class AppHost : IAsyncDisposable
// Nichts freizugeben: Der Torwächter nutzt den gemeinsamen HttpClient des SDK.
host.License = license;
host.IsManaged = gefuehrt;
if (license is not null && license.IsEnforcementConfigured)
{