From 373e1f4caf35612b795447255fb879a93c57965d Mon Sep 17 00:00:00 2001 From: Richard Date: Tue, 8 Sep 2026 22:14:19 +0200 Subject: [PATCH] Deploymentcenter-Anbindung auf SDK 2.8.2 nachgezogen (#218) Der Changelog seit 2.6.0 nennt sechs Fassungen, 16 action_items und fuenf brechende Aenderungen. Davon trifft fast nichts diese Anwendung - und das ist ein Befund, kein Versaeumnis: - finding_status (brechend, 2.7.0): kein Aufrufer. Weder /api/supervisor/v1/observe noch /status werden hier angesprochen. Vorgemerkt fuer #528 Stufe 3 - wer den Manager baut, liest den Befundzustand von Anfang an aus diesem Feld. - Nachpruefungs-Status awaiting_verification, in_verification, fix_insufficient (brechend, 2.6.1): Die Anwendung wertet bugtracker_items.status nirgends aus. Betrifft den Arbeitsablauf der Agenten, und der steht in CLAUDE.md bereits richtig beschrieben. - Meldungen ohne project_slug (brechend, 2.8.1): BugtrackerClient und ErrorReporter schicken project_slug und build_version seit jeher mit. - Leseweg ohne project_slug und schmales dc_roadmap (brechend, 2.8.1): betrifft den MCP-Adapter, nicht den Anwendungscode. Geblieben ist die eine Zusage, die man nicht sieht, wenn man sie verletzt: Seit 2.8.1 erbt ein Watchdog-Monitor das Projekt seines Ping-Tokens. Ohne Bindung landen die Befunde im Scheinprojekt "default", und eine Abfrage nach dem eigenen Projekt liefert fuer eine laufende, fleissig meldende Instanz null Befunde - die Aufsicht sieht aus wie "nichts auffaellig". Das ist genau die Falle, in die hier schon einmal Meldungen gelaufen sind. Deshalb neu: TokenProvisioner.WhoAmIAsync gegen GET /api/tokens/v1/whoami (SDK 2.7.1) und der Typ TokenIdentity mit IsProjectBound. DeploymentcenterService fragt beim Start das eigene Melde-Token, protokolliert das Projekt und warnt, wenn keines dransteht. Bewusst nur eine Warnung: Monitoring, das bei unklarer Rechtelage gar nicht erst anlaeuft, ist genau dann still, wenn man es braucht. Faellt die Auskunft aus - aelterer Server, kein Netz -, bleibt es bei Debug. Gemessen statt geraten: TokenProvisioner schickt keinen project_slug mit, und ob der Server die Bindung vom Master-Token erbt, steht in keiner Beschreibung, die vorliegt. Ein Sub-Token dafuer eigens auf dem Produktivserver auszustellen waere zu viel Nebenwirkung gewesen; die erste Instanz, die eines zieht, sagt es jetzt selbst ins Protokoll. #176 ist erledigt und die Regel in CLAUDE.md gestrichen: dc_roadmap ohne project_slug liefert jetzt das eigene Projekt (nachgemessen: 51 Eintraege, alle clawddotnet). Wer projektuebergreifend lesen will, schickt project_slug=all. dc_link_project auf 2.8.2 gesetzt, CLAUDE.md-Zeile hochgesetzt. Build ohne Warnungen, 717 Tests gruen, 8 uebersprungen. Co-Authored-By: Claude Opus 5 --- CLAUDE.md | 38 +++++++--- .../Services/DeploymentcenterService.cs | 50 ++++++++++++++ .../Deploymentcenter/TokenProvisioner.cs | 61 +++++++++++++++- .../Deploymentcenter/TokenProvisionerTests.cs | 69 +++++++++++++++++++ 4 files changed, 205 insertions(+), 13 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 2d8a412..c172580 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -84,14 +84,14 @@ nicht: den Fehler ganz normal erneut melden — das Item wandert von selbst auf `unauthorized` → Token prüfen, nicht wiederholen. `already_claimed` → nächstes Item. `rate_limited` → Intervall verdoppeln, später erneut. -### Zwei Stolperstellen, solange sie offen sind +### Eine Stolperstelle, solange sie offen ist -**`dc_roadmap` immer mit `project_slug: "clawddotnet"` aufrufen.** Ohne die -Angabe liefert der Server die Roadmap *fremder* Projekte, obwohl die -Werkzeugbeschreibung das eigene zusagt. Gemeldet als -[#176](https://dc.mhdf.de/index.php#tab-bugtracker) unter `deploymentcenter`. -Betrifft nur die lesenden Roadmap-Aufrufe; `dc_next_task` ist korrekt ans -eigene Projekt gebunden. +**#176 ist erledigt und die Regel dazu gestrichen.** Hier stand, `dc_roadmap` +müsse immer mit `project_slug: "clawddotnet"` aufgerufen werden, weil der Server +sonst fremde Projekte liefert. Seit SDK 2.8.1 gilt ohne Angabe das eigene +Projekt — beim Lesen wie beim Melden. Nachgemessen am 2026-09-08: `dc_roadmap` +ohne Slug liefert 51 Einträge, alle `clawddotnet`. Wer bewusst über alle +Projekte lesen will, schickt jetzt ausdrücklich `project_slug: "all"`. **`roadmap_rank` gilt hier projektweit**, nicht je Bereich — die durchgehende Reihenfolge aus `docs/Roadmap.md` hängt daran. Wer einen Eintrag im WebUI @@ -118,12 +118,28 @@ er liest `.mcp.json` beim Sitzungsbeginn. Eine neue Sitzung räumt das aus. `$DC_TOKEN` steht daneben in der Bash-Umgebung — damit lässt sich die REST-API direkt bedienen, wenn die Werkzeuge einmal ausfallen. -Deploymentcenter-Anbindung: SDK 2.6.0, integriert am 2026-08-30. -Aktuell ist 2.7.0; der Sprung ist ein eigenes Vorhaben und steht auf der -Roadmap. Vor jeder Änderung an der Anbindung: -`GET /api/updateservice/v1/changelog?since=2.6.0`, danach diese Zeile +Deploymentcenter-Anbindung: **SDK 2.8.2**, nachgezogen am 2026-09-08 (#218). +Vor jeder Änderung an der Anbindung: +`GET /api/updateservice/v1/changelog?since=2.8.2`, danach diese Zeile hochsetzen und `dc_link_project` aufrufen. +Was der Sprung 2.6.0 → 2.8.2 an dieser Anwendung geändert hat, ist wenig — und +das ist ein Befund, kein Versäumnis: Von fünf brechenden Änderungen betrafen vier +Wege, die diese Anwendung gar nicht geht (`?action=next`, Roadmap-Lesen, +`/api/supervisor/v1/observe`). Der Meldepfad schickte `project_slug` und +`build_version` bereits. Geblieben ist eine Zusage, die man nicht sieht, wenn man +sie verletzt: Seit 2.8.1 erbt ein Watchdog-Monitor das Projekt seines +Ping-Tokens; ohne Bindung landen die Befunde im Scheinprojekt „default", und +`dc_findings` liefert für eine laufende, fleißig meldende Instanz **null +Befunde** — die Aufsicht sieht aus wie „nichts auffällig". Die Anwendung fragt +deshalb beim Start ihr eigenes Melde-Token über +`GET /api/tokens/v1/whoami` (seit 2.7.1), protokolliert das Projekt und warnt, +wenn keines dransteht. + +`finding_status` (brechend seit 2.7.0) ist noch niemandes Baustelle: Diese +Anwendung ruft `observe` und `status` nicht auf. Wer den Instanzmanager baut +(#528, Stufe 3), liest den Befundzustand von Anfang an aus diesem Feld. + ## Weitere Handbücher | Dokument | Wofür | diff --git a/src/ClawdDotNet.App/Services/DeploymentcenterService.cs b/src/ClawdDotNet.App/Services/DeploymentcenterService.cs index e50cc41..92eb134 100644 --- a/src/ClawdDotNet.App/Services/DeploymentcenterService.cs +++ b/src/ClawdDotNet.App/Services/DeploymentcenterService.cs @@ -121,6 +121,8 @@ public sealed class DeploymentcenterService : IAsyncDisposable var watchdog = instance.Watchdog; var token = await ResolveInstanceTokenAsync(instance, saveInstanceConfig, ct); + await WarnIfTokenHasNoProjectAsync(token, ct); + try { _heartbeat = WatchdogHeartbeatService.Create( @@ -191,6 +193,54 @@ public sealed class DeploymentcenterService : IAsyncDisposable } } + /// + /// Prüft, ob das Melde-Token an ein Projekt gebunden ist, und sagt es, wenn nicht. + /// + /// Warum das eine eigene Anfrage wert ist. Seit SDK 2.8.1 erbt ein + /// Watchdog-Monitor das Projekt seines Ping-Tokens. Fehlt dort die Bindung, legt der + /// Server die Befunde im Scheinprojekt „default" ab — und eine Abfrage nach dem + /// eigenen Projekt liefert für eine laufende, fleißig meldende Instanz dann + /// null Befunde. Die Aufsicht sieht aus wie „nichts auffällig", und genau + /// darauf verlässt sich niemand gern. + /// + /// Der Fall ist nicht erfunden: Genau so sind schon Meldungen in „default" + /// gelandet und waren danach für ihren eigenen Melder gesperrt. Ein Heartbeat, der + /// ins Leere geht, meldet sich nicht von selbst — deshalb wird hier einmal + /// nachgefragt, statt es anzunehmen. + /// + /// Nur eine Warnung, kein Abbruch: Ein Monitoring, das bei unklarer Rechtelage + /// gar nicht erst anläuft, ist genau dann still, wenn man es braucht. + /// + private async Task WarnIfTokenHasNoProjectAsync(string token, CancellationToken ct) + { + try + { + using var api = new DeploymentcenterApi(_api.BaseUrl, token); + var identity = await new TokenProvisioner(api).WhoAmIAsync(ct); + + if (identity.IsProjectBound) + { + _logger.LogInformation( + "Deploymentcenter: Meldungen gehen an Projekt {Projekt} (Token {TokenId}, {Typ}).", + identity.ProjectSlug, identity.TokenId, identity.Type); + + return; + } + + _logger.LogWarning( + "Deploymentcenter: Das Melde-Token {TokenId} ist an kein Projekt gebunden. " + + "Watchdog-Befunde dieser Instanz landen damit im Scheinprojekt \"default\" " + + "und tauchen in der Projektansicht nicht auf.", + identity.TokenId); + } + catch (Exception ex) + { + // Aelterer Server ohne whoami (vor SDK 2.7.1), kein Netz, kein Recht: Das ist + // eine Auskunft, die ausfaellt, und kein Grund, den Heartbeat nicht zu starten. + _logger.LogDebug(ex, "Deploymentcenter: Token-Auskunft nicht verfügbar."); + } + } + private static string DescribePlatform() => $"{System.Runtime.InteropServices.RuntimeInformation.OSDescription} / " + $".NET {System.Environment.Version}"; diff --git a/src/ClawdDotNet.Core/Deploymentcenter/TokenProvisioner.cs b/src/ClawdDotNet.Core/Deploymentcenter/TokenProvisioner.cs index 4ea743c..cdfdca9 100644 --- a/src/ClawdDotNet.Core/Deploymentcenter/TokenProvisioner.cs +++ b/src/ClawdDotNet.Core/Deploymentcenter/TokenProvisioner.cs @@ -9,8 +9,30 @@ namespace ClawdDotNet.Core.Deploymentcenter; public sealed record ProvisionedToken(string Token, string TokenId, IReadOnlyList Scopes); /// -/// Tauscht das anwendungsweite Master-Token gegen ein eigenes Sub-Token je Instanz -/// (POST /api/tokens/v1/provision). +/// Was ein Token über sich selbst sagt (GET /api/tokens/v1/whoami, seit SDK 2.7.1). +/// +/// ist das Feld, auf das es ankommt. Seit SDK 2.8.1 erbt +/// ein Watchdog-Monitor das Projekt seines Ping-Tokens — ist dort nichts eingetragen, +/// landen die Befunde im Scheinprojekt „default", und eine Abfrage nach dem eigenen +/// Projekt liefert für eine laufende, meldende Instanz null Befunde. Die Aufsicht +/// sähe dann aus wie „nichts auffällig", und das ist der schlechteste aller +/// Irrtümer. +/// +public sealed record TokenIdentity( + string TokenId, + string Name, + string Type, + string? ProjectSlug, + string Environment, + IReadOnlyList Scopes) +{ + /// Ob dieses Token an ein Projekt gebunden ist — siehe Klassenkommentar. + public bool IsProjectBound => !string.IsNullOrWhiteSpace(ProjectSlug); +} + +/// +/// Der Zugang zu /api/tokens/v1/…: ein Sub-Token je Instanz ausstellen +/// (provision) und ein Token nach sich selbst fragen (whoami). /// /// Das ersetzt die frühere Selbstregistrierung über POST /api/register — /// diesen Endpunkt gibt es im Deploymentcenter nicht (und im alten WatchDog-Server war @@ -70,4 +92,39 @@ public sealed class TokenProvisioner(DeploymentcenterApi api) return new ProvisionedToken(token, tokenId, granted); } + + /// + /// Fragt das Token, mit dem diese Verbindung arbeitet, nach sich selbst. + /// + /// Nicht Neugier, sondern die einzige Möglichkeit, die Projektbindung zu + /// erfahren: Sie steht nicht im Token und ergibt sich nicht aus dem, was beim + /// Ausstellen mitgeschickt wurde — sie ist eine Eigenschaft, die der Server führt. + /// Siehe , warum das nicht gleichgültig ist. + /// + public async Task WhoAmIAsync(CancellationToken ct = default) + { + var response = await api.GetAsync("/api/tokens/v1/whoami", ct).ConfigureAwait(false); + + var scopes = new List(); + if (response.TryGetProperty("scopes", out var s) && s.ValueKind == JsonValueKind.Array) + { + scopes.AddRange(s.EnumerateArray() + .Where(e => e.ValueKind == JsonValueKind.String) + .Select(e => e.GetString()!)); + } + + return new TokenIdentity( + Text(response, "token_id") ?? "", + Text(response, "name") ?? "", + Text(response, "type") ?? "unknown", + Text(response, "project_slug"), + Text(response, "environment") ?? "", + scopes); + } + + /// Eine Zeichenkette aus der Antwort; null auch bei JSON-null. + private static string? Text(JsonElement root, string name) + => root.TryGetProperty(name, out var v) && v.ValueKind == JsonValueKind.String + ? v.GetString() + : null; } diff --git a/tests/ClawdDotNet.Core.Tests/Deploymentcenter/TokenProvisionerTests.cs b/tests/ClawdDotNet.Core.Tests/Deploymentcenter/TokenProvisionerTests.cs index 267c683..3ce21a5 100644 --- a/tests/ClawdDotNet.Core.Tests/Deploymentcenter/TokenProvisionerTests.cs +++ b/tests/ClawdDotNet.Core.Tests/Deploymentcenter/TokenProvisionerTests.cs @@ -47,4 +47,73 @@ public sealed class TokenProvisionerTests ex.Code.ShouldBe("provision_denied"); } + // ─── whoami (SDK 2.7.1) ─── + + /// + /// Die Auskunft, auf die es ankommt: an welches Projekt das Token gebunden ist. + /// + /// Seit SDK 2.8.1 erbt ein Watchdog-Monitor das Projekt seines Ping-Tokens. + /// Fehlt die Bindung, landen die Befunde im Scheinprojekt „default" — und eine + /// Abfrage nach dem eigenen Projekt liefert für eine laufende, meldende Instanz null + /// Befunde. Die Aufsicht sähe aus wie „nichts auffällig". + /// + [Fact] + public async Task Whoami_liest_die_Projektbindung() + { + var handler = new CapturingHandler(HttpStatusCode.OK, + """ + {"status":"success","token_id":"tok_m_1","name":"DEV-ClawdDotNet","type":"master", + "owner_type":"dev_agent","is_workbench":true,"project_slug":"clawddotnet", + "environment":"all","scopes":["*"]} + """); + + using var api = new DeploymentcenterApi("https://dc.example", "master", new HttpClient(handler)); + + var identity = await new TokenProvisioner(api).WhoAmIAsync(); + + handler.LastRequest!.RequestUri!.ToString().ShouldBe("https://dc.example/api/tokens/v1/whoami"); + handler.LastRequest.Method.ShouldBe(HttpMethod.Get); + + identity.TokenId.ShouldBe("tok_m_1"); + identity.Name.ShouldBe("DEV-ClawdDotNet"); + identity.Type.ShouldBe("master"); + identity.ProjectSlug.ShouldBe("clawddotnet"); + identity.IsProjectBound.ShouldBeTrue(); + identity.Scopes.ShouldContain("*"); + } + + [Fact] + public async Task Ein_Token_ohne_Projekt_faellt_auf() + { + // project_slug ist JSON-null, nicht abwesend - genau so kommt es vom Server. + var handler = new CapturingHandler(HttpStatusCode.OK, + """ + {"status":"success","token_id":"tok_s_9","name":"Instanz","type":"sub", + "project_slug":null,"environment":"production","scopes":["watchdog:ping"]} + """); + + using var api = new DeploymentcenterApi("https://dc.example", "sub", new HttpClient(handler)); + + var identity = await new TokenProvisioner(api).WhoAmIAsync(); + + identity.ProjectSlug.ShouldBeNull(); + identity.IsProjectBound.ShouldBeFalse(); + } + + [Fact] + public async Task Eine_Antwort_ohne_Felder_wirft_nicht() + { + // Ein aelterer Server kennt whoami nicht in dieser Form. Die Auskunft darf + // ausfallen; abstuerzen darf sie nicht. + var handler = new CapturingHandler(HttpStatusCode.OK, """{"status":"success"}"""); + + using var api = new DeploymentcenterApi("https://dc.example", "t", new HttpClient(handler)); + + var identity = await new TokenProvisioner(api).WhoAmIAsync(); + + identity.TokenId.ShouldBe(""); + identity.Type.ShouldBe("unknown"); + identity.IsProjectBound.ShouldBeFalse(); + identity.Scopes.ShouldBeEmpty(); + } }