diff --git a/docs/AGENTEN_ANBINDEN.md b/docs/AGENTEN_ANBINDEN.md index 6dbbe5e..5d85a56 100644 --- a/docs/AGENTEN_ANBINDEN.md +++ b/docs/AGENTEN_ANBINDEN.md @@ -33,6 +33,11 @@ gelesen zu haben. ## 1. Werkbank-Token anlegen +**Erst nachsehen, ob es schon eines gibt.** Für ein angebundenes Projekt steht +es in der `.mcp.json` des Repos, unter `env.DC_TOKEN` — dort braucht der +MCP-Adapter es ohnehin. Ein zweites danebenzustellen bringt nichts außer der +Frage, welches von beiden gilt. + Im WebUI: **Token-Verwaltung → ➕ Token erstellen**. Die Ansicht hieß bis 2.7.0 „Master-Token erstellen" — deshalb war das @@ -48,9 +53,27 @@ anderem Identitätstyp. `supervisor:manage` nur, wenn der Agent Befunde auch abhaken soll. `license:dev` und `updateservice:read` nur für die Test-VM (Schritt 5). -**Je Projekt ein eigenes Token.** Aus der Projektbindung leitet der Server -`reported_from` ab. Ein Token für alle, und das Signal „drei Projekte melden -dasselbe" ist wertlos — genau das Signal, das eine Meldung dringlich macht. +**Das Projekt darf nicht leer bleiben** — und zwar nicht, weil ohne Bindung +weniger ginge, sondern weil mehr geht. Der Server leitet aus ihr das +Heimatprojekt ab. Fehlt sie, ist für ihn *jede* Meldung eine Heimmeldung: keine +Begründungspflicht, keine Deckelung auf `medium`, kein Eingang in +`triage_external`, kein `reported_from`. Fehlmeldungen landen dann unmittelbar +in der offenen Liste fremder Projekte. + +Kein hypothetischer Fall: Genau so ein Token lag bis zum 31.08.2026 auf der +Test-VM, `dev_agent` und ohne Projekt. Nachsehen, bevor du dich darauf +verlässt: + +```bash +curl -s -H "Authorization: Bearer $DC_TOKEN" https://dc.mhdf.de/api/tokens/v1/whoami +``` + +Auf der Test-VM antwortet `dc-testkit doctor` dieselbe Frage vollständiger — +Bindung, jedes fehlende Recht einzeln, und ob das Skript selbst noch aktuell ist. + +**Je Projekt ein eigenes Token.** Eines für alle, und das Signal „drei +Projekte melden dasselbe" ist wertlos — genau das Signal, das eine Meldung +dringlich macht. **Kein Master-Token an einen Agenten.** Er darf melden, übernehmen und abschließen; er darf keine Releases veröffentlichen und keine Tokens ausstellen. @@ -98,7 +121,8 @@ In `CLAUDE.md` bzw. `AGENTS.md` des Projekts, aus | Lage | Bausteine | |---|---| | Mit `.mcp.json` | **A** + **B** | -| Test-VM | **A** + **D** | +| Test-VM, Agent entwickelt und testet | **A** + **D** | +| Test-VM, Agent **prüft nur** | **A** + **E** | | Ohne MCP | **A** + **C** | **Nicht alles einkopieren.** Wo der Adapter läuft, tragen die Werkzeuge ihre @@ -142,6 +166,30 @@ Zusätzlich `license:dev` und `updateservice:read` am Token, Ablage unter **Kein Kundenlizenzschlüssel auf die VM.** Er verbraucht dort Aktivierungsplätze, die hinterher niemand sauber freibekommt. +### Wenn der Agent nur prüfen soll + +Dann Baustein **E** statt **D**, und vier Dinge stehen vorher fest, nicht erst +wenn er sie braucht: + +| | | +|---|---| +| SSH-Alias und Schlüssel | `testvm-` / `~/.ssh/testvm_` | +| Testkit-Konfiguration | `~/.config/dc-testkit/`, Pfad **im Prompt** | +| Werkbank-Token | die `.mcp.json` des Projekts muss **auf der VM** liegen; `DC_PROJECT_DIR` zeigt darauf. Über Git kommt sie nicht mit | +| Display, Datenbank, Verzeichnis | aus [TESTUMGEBUNG.md](TESTUMGEBUNG.md) §7 — vorher nachsehen, ob die Liste stimmt | +| Node.js auf der VM | **nicht vorhanden**, also kein MCP — der Prüfagent arbeitet ausschließlich über `dc-testkit` | + +Der Token-Punkt ist der, an dem es beim ersten Mal hängt: Auf der VM liegt unter +`~/RiderProjects/` keine einzige `.mcp.json`. Der Agent kann das nicht selbst +beheben und soll es auch nicht versuchen — Baustein E sagt ihm, dass er sich in +diesem Fall meldet, statt ein Token zu suchen oder ein fremdes zu nehmen. + +Die Rollengrenze steht in [TESTUMGEBUNG.md](TESTUMGEBUNG.md) §4.8. Sie lässt +sich heute **nicht** über Rechte erzwingen: `bugtracker:manage` erlaubt auch +`resolve` und das Ändern der Roadmap. Bis es ein `bugtracker:verify` gibt, ist +Baustein E das Einzige, was die Grenze zieht — er darf deshalb nicht gekürzt +werden. + --- ## Was NICHT an die anderen Projekte geht @@ -185,6 +233,20 @@ genau die Stellen ab, an denen es erfahrungsgemäß hakt: Antwortet er auf die erste Frage mit dem Inhalt einer Markdown-Datei, fehlt Baustein A oder der Adapter läuft nicht. +**Einen Prüfagenten** (Baustein E) fragst du anderes — seine typischen Fehler +sind andere: + +| Frage | Erwartete Reaktion | +|---|---| +| „Leg los." | ruft **zuerst** `selfupdate --pruefen` und `doctor`, bevor er irgendetwas prüft | +| „Der Fix in Item 42 trägt nicht." | meldet den Fehler erneut — ruft **nicht** `resolve` und fängt nicht an, ihn selbst zu beheben | +| „Die Oberfläche sieht komisch aus." | startet sein **eigenes** Display, nicht `:0` und nicht `xvfb-run -a` | +| „Melde das." | nennt die geprüfte Fassung; bei fremder Ursache `--project` mit `evidence` | +| „Das Token fehlt." | meldet sich und nennt den erwarteten Pfad — sucht kein Token und nimmt kein fremdes | + +Bietet er auf die zweite Frage an, den Fehler gleich selbst zu beheben, fehlt in +Baustein E der Absatz „Du behebst nichts" — oder er wurde gekürzt. + --- ## Reihenfolge über alle Projekte diff --git a/docs/AGENT_PROMPT_TEMPLATE.md b/docs/AGENT_PROMPT_TEMPLATE.md index 307c5e2..c363d62 100644 --- a/docs/AGENT_PROMPT_TEMPLATE.md +++ b/docs/AGENT_PROMPT_TEMPLATE.md @@ -11,7 +11,8 @@ arbeitet: | Lage | Bausteine | Warum | |---|---|---| | Projektverzeichnis **mit** `.mcp.json` | **A** + **B** | Die Werkzeuge tragen ihre Beschreibung selbst; eine zweite Erklärung im Prompt veraltet nur und kostet Kontext | -| Linux-Test-VM | **A** + **D** | `dc-testkit` kapselt die Aufrufe, kein `curl` nötig | +| Linux-Test-VM, Agent entwickelt **und** testet | **A** + **D** | `dc-testkit` kapselt die Aufrufe, kein `curl` nötig | +| Linux-Test-VM, Agent **prüft nur** | **A** + **E** | E steht anstelle von D: dieselben Aufrufe, aber mit der Grenze, die kein Recht erzwingt | | Alles Übrige (kein MCP) | **A** + **C** | Der Rückfallweg über REST, vollständig | **A** ist die Grundhaltung und gilt immer — sie ist kurz. **B** ist eine halbe @@ -425,6 +426,138 @@ Anleitung: `Deploymentcenter/docs/TESTUMGEBUNG.md`, Fremdmeldungen in §4.6. --- +## Baustein E — Prüfagent (statt D) + +Für einen Agenten, dessen einzige Aufgabe das Prüfen ist. Er enthält alles aus **D** und zieht +zusätzlich die Grenze, die serverseitig noch kein Recht zieht: `bugtracker:manage` erlaubt heute +auch `resolve` und das Ändern der Roadmap. Wird der Absatz „Du prüfst" weggelassen, fängt das +Modell früher oder später an, den Fehler gleich selbst zu beheben — hilfsbereit gemeint und genau +das, was ein Prüfstand nicht tun darf. + +```markdown +### Deine Rolle: prüfen, nicht beheben + +Du prüfst **ein** Projekt: . Der Testlauf findet auf der geteilten +Linux-Test-VM statt — **du läufst nicht auf ihr**, sondern hier, wo auch das +Projekt-Repo liegt. + +Jeder Aufruf geht über den Wrapper, nie über `ssh` von Hand: + + dc-testkit-remote doctor + dc-testkit-remote run + dc-testkit-remote --skript pruefplan.sh + +Er nimmt dir vier Dinge ab, die von Hand zuverlässig schiefgehen: Das Token +landet nicht in `ps aux` der geteilten Maschine, `~/bin` liegt im PATH, +Anführungszeichen in `--title` überleben den Weg, und `--skript` bündelt einen +ganzen Prüfplan in eine Verbindung. Baue dir **keine** eigene SSH-Zeile — genau +darum gibt es den Wrapper. + +Was du auf der VM direkt brauchst — Bilder abholen, Prozesse ansehen — geht +weiter über `ssh testvm-` bzw. `scp`. + +**Sagt der Wrapper „Kein Werkbank-Token", ist nichts kaputt.** Das Token deines +Projekts steht in der `.mcp.json` des Repos, und die ist per `.gitignore` +ausgeschlossen — über Git kommt sie nie mit. Auf einem frisch geklonten +Arbeitsplatz fehlt sie deshalb immer. Fang nicht an, ein Token zu suchen, zu +raten oder ein fremdes zu benutzen: **melde dich mit genau diesem Satz** und +nenne den Pfad, an dem die Datei erwartet wird. Ein Mensch bringt sie dorthin. + +**Du behebst nichts.** Du änderst keinen Code des Projekts, rufst nie `dc-testkit +resolve` auf und änderst nie die Roadmap. Was als „behoben in Build X" gilt, +entscheidet, wer den Fix geschrieben hat. Du entscheidest etwas anderes, und nur +das: ob er trägt. + +### Jede Sitzung beginnt gleich + + dc-testkit-remote selfupdate --pruefen # Werkzeug aktuell? + dc-testkit-remote doctor # Rechte, Netz, Display + +Sagt doctor nicht „Bereit", hörst du hier auf und meldest, was fehlt. Ein Testlauf +mit halben Rechten scheitert später an einer Stelle, die nichts mit der Sache zu +tun hat — und kostet jemanden einen Nachmittag Fehlersuche am falschen Ende. + +### Was du prüfst + + dc-testkit-remote license # DEV-Schlüssel (wiederholbar) + dc-testkit-remote run # ausliefern, prüfen, aktualisieren + dc-testkit-remote check # auch: list, repair + dc-testkit-remote update --version # der Rückfall + +Oberflächen prüfst du selbst. Dein Display ist **fest vergeben** und steht als +`DC_DISPLAY` in deiner Konfiguration; benutze nie `:0`, nie `:1` und nie +`xvfb-run -a` (das nimmt sich :99, also die Nummer eines anderen Projekts). + + dc-testkit-remote display start # eigenes Xvfb hoch + dc-testkit-remote shot 1_start # /tmp/_1_start.png + dc-testkit-remote display stop # danach, immer + +Dazwischen navigierst du mit `xdotool` und holst die Bilder mit `scp` ab, um sie +anzusehen. Warte großzügig — 25 Sekunden nach dem Start, 5 nach jedem Klick. +Ein halb gezeichnetes Fenster erzeugt Befunde, die keine sind. Das vollständige +Verfahren steht in TESTUMGEBUNG.md 5.8. + +### Was du meldest + + dc-testkit-remote report --type bug --severity \ + --title "..." --desc "..." + +**Immer mit der geprüften Fassung** — das Testkit liest sie aus der `manifest.json` +im Sandkasten. Ohne sie kann der Server einen echten Befund nicht von „diese +Maschine fährt einen älteren Stand" unterscheiden und legt im Zweifel ein +Regressionsitem an. + +Bevor du meldest, ist die Frage nicht „ist das ein Fehler?", sondern **„ist das +schon bekannt, und in welcher Fassung wurde es behoben?"** Das beantwortet der +Tracker dir beim Melden von selbst. Antwortet er „schon behoben in einer späteren +Fassung", ist nichts entstanden und nichts verloren — aktualisieren und erneut +prüfen. + +Was am **Deploymentcenter, am Testkit oder an TESTUMGEBUNG.md** selbst nicht +stimmt, meldest du dorthin, mit Begründung: + + dc-testkit-remote report --project deploymentcenter \ + --type bug --severity medium --title "..." --desc "..." \ + --evidence "was beobachtet wurde, und warum die Ursache dort liegt" + +### Prüfaufträge — deine eigentliche Arbeit + + dc-testkit-remote next + +Ein Prüfauftrag sieht aus wie jede andere Arbeit, stellt aber eine andere Frage: +nicht „behebe das", sondern „trägt der Fix?". Lies den Hinweis in der Ausgabe. +Danach genau eines von beidem: + + dc-testkit-remote confirm "was geprüft wurde" + # der Fix trägt + + dc-testkit-remote report ... + # er trägt nicht; das Item wechselt selbst auf fix_insufficient + +Kommst du nicht weiter: `dc-testkit-remote unclaim ""`. +Ein gehaltenes Item sperrt es für alle anderen bis zum Lease-Ende. Antwortet der Server mit `409 +already_claimed`, nimm das nächste. + +### Grenzen + +- **Kein Kundenlizenzschlüssel auf die VM.** Nur `dc-testkit license`. +- **Kein Passwort, nirgends.** Deshalb prüft das Testkit die Auslieferung und + nicht den Installationsdialog. +- **Nichts veröffentlichen.** `updateservice:publish` gehört nicht auf diese + Maschine. +- **Nur dein Projekt anfassen** — eigene Datenbank, eigenes Verzeichnis, eigenes + Display. Die Vergabe steht in TESTUMGEBUNG.md 7; verlässt du dich darauf, prüf + sie vorher nach, sie war schon einmal im Rückstand. +- **Nichts Dauerhaftes hinterlassen.** Nach dem Lauf: + `dc-testkit-remote cleanup`, + eigenes Xvfb beenden, Dienst abschalten. + +Anleitung: `Deploymentcenter/docs/TESTUMGEBUNG.md` — deine Rolle in 4.8, +Fremdmeldungen in 4.6, Oberflächenprüfung in 5.8. +``` + +--- + ## Kurzfassung für knappe Prompt-Budgets ```markdown diff --git a/docs/TESTUMGEBUNG.md b/docs/TESTUMGEBUNG.md index 62ea001..3142e5e 100644 --- a/docs/TESTUMGEBUNG.md +++ b/docs/TESTUMGEBUNG.md @@ -1,6 +1,6 @@ # Linux-Testumgebung — Anleitung für Agenten -**Stand: 28.08.2026** · Gilt projektübergreifend für alle Agenten und Entwickler, die auf der +**Stand: 31.08.2026** · Gilt projektübergreifend für alle Agenten und Entwickler, die auf der geteilten Test-VM arbeiten. > **Diese Datei ist die maßgebliche Fassung.** Sie liegt im Deploymentcenter-Repo unter @@ -8,6 +8,15 @@ geteilten Test-VM arbeiten. > den Projektrepos zeigen hierher. Wer etwas an der Umgebung ändert, ändert es **hier** und > kopiert danach auf die VM — nicht umgekehrt. > +> **Das ist zweimal unterblieben**, zuletzt am 31.08.2026 bemerkt: Auf der Maschine lag noch der +> Stand vom 26.08. — ohne das gesamte Testkit-Kapitel und mit verschobenen Abschnittsnummern, so +> dass der Verweis „§4.6, Fremdmeldungen" dort bei „Erhöhte Rechte" landete. Wer die Datei hier +> ändert, kopiert sie **im selben Arbeitsgang**: +> +> ```bash +> scp docs/TESTUMGEBUNG.md testvm-:~/TESTUMGEBUNG.md +> ``` +> > **Bewusst nicht unter `public/`:** Diese Datei nennt eine interne Adresse, einen Benutzernamen > und den Weg, `sudo` zeitweise freizuschalten. `/docs/` auf dem Deploymentcenter ist öffentlich > erreichbar; dort gehört das nicht hin. Das Testkit-Skript dagegen schon — es enthält nichts @@ -61,7 +70,7 @@ mkdir -p ~/.ssh && chmod 700 ~/.ssh && echo '' >> ~/.ssh/ ### 2.3 SSH-Konfiguration (auf dem eigenen Rechner) ``` -Host testvm +Host testvm- HostName 192.168.40.48 User richard IdentityFile ~/.ssh/testvm_ @@ -70,11 +79,21 @@ Host testvm ServerAliveCountMax 4 ``` +**Der Aliasname trägt das Projekt.** Ein Agent hat genau einen Namen, unter dem er die Maschine +erreicht, und dieser Name sagt, mit welchem Schlüssel er das tut. Ein gemeinsamer Alias `testvm` +für alle wäre bequemer und macht die Trennung aus 2.1 wieder zunichte: Der Schlüssel wird dann +nach Zufall gewählt, und ein Entzug trifft entweder niemanden oder alle. + ⚠️ **Kein `ControlMaster`/`ControlPath` eintragen, wenn der Client unter Windows läuft** — Windows-OpenSSH unterstützt Verbindungs-Multiplexing nicht und scheitert mit „Failed to connect to new control master". Stattdessen mehrere Prüfschritte pro Aufruf bündeln. -Test: `ssh testvm "hostname"` +Test: `ssh testvm- "hostname"` + +> **Bestand (31.08.2026):** Auf dem Arbeitsplatz existiert bisher nur `polytrader-vm` mit +> `~/.ssh/polytrader_vm` — ein Schlüssel, den alle Projekte mitbenutzen. Wer den nächsten Agenten +> aufsetzt, legt für ihn einen eigenen an und benennt bei der Gelegenheit den alten Eintrag nach +> `testvm-polytrader` um. Bis dahin schlagen die Beispiele in dieser Datei wörtlich fehl. --- @@ -92,7 +111,7 @@ Test: `ssh testvm "hostname"` | JetBrains Rider | vorhanden | | | Docker | **nicht** installiert | | | Node.js | **nicht** installiert | | -| jq | **nicht** installiert | Wird auch nicht gebraucht — das Testkit liest JSON mit `python3` | +| jq | vorhanden (nachträglich) | Das Testkit benutzt es **nicht** — es liest JSON mit `python3`, damit es auch auf einer Maschine ohne jq läuft | **Nicht vorhandene Werkzeuge dürfen installiert werden** — aber bitte in der Belegungsliste (Abschnitt 7) vermerken, damit andere wissen, was sich geändert hat. @@ -116,8 +135,23 @@ der im WebUI etwas anklickt. ### 4.1 Einmal einrichten -Das Skript liegt im Deploymentcenter-Repo unter `public/testkit/dc-testkit.sh` und wird mit -`deploy.py` ins Webroot ausgeliefert. Auf der VM: +**Der Agent läuft nicht auf der VM, sondern auf dem Arbeitsplatz.** Claude Code blockiert die +Maschine bis zum Neustart (siehe 9) — deshalb arbeitet er über SSH von außen. Daraus folgt die +Aufteilung: + +| Was | Wo | Warum dort | +|---|---|---| +| `dc-testkit` | **auf der VM**, `~/bin/dc-testkit` | Dort läuft der Test: Sandkasten, Update-Agent, Display | +| `dc-testkit-remote` | **auf dem Arbeitsplatz** | Dort sitzt der Agent | +| Konfiguration | **auf dem Arbeitsplatz**, `~/.config/dc-testkit/` | Dort liegen auch die Projekt-Repos — und damit die Token (4.2) | +| Werkbank-Token | **auf dem Arbeitsplatz**, in der `.mcp.json` des Repos | Es muss nirgends sonst hin | + +**Auf der VM liegt keine Konfiguration.** `dc-testkit-remote` setzt `DC_TESTKIT_CONFIG` bewusst +ins Leere: Sonst gälte drüben, was irgendwann einmal jemand eingetragen hat. Genau daran ist es +gescheitert — dort lag ein altes Laufzeit-Token, während das richtige Werkbank-Token des Projekts +längst ein anderes war, und `doctor` meldete trotzdem „Bereit". + +**Auf der VM** (einmal je Maschine): ```bash mkdir -p ~/bin @@ -125,6 +159,45 @@ curl -fsSL https://dc.mhdf.de/testkit/dc-testkit.sh -o ~/bin/dc-testkit chmod +x ~/bin/dc-testkit ``` +**Auf dem Arbeitsplatz** (einmal je Arbeitsplatz): + +```bash +mkdir -p ~/bin +curl -fsSL https://dc.mhdf.de/testkit/dc-testkit-remote.sh -o ~/bin/dc-testkit-remote +chmod +x ~/bin/dc-testkit-remote +``` + +Danach läuft jeder Aufruf über den Wrapper — mit dem Projekt-Slug als erstem Argument: + +```bash +dc-testkit-remote polytrader doctor +dc-testkit-remote polytrader run +dc-testkit-remote polytrader --skript pruefplan.sh +``` + +**Warum ein Wrapper und nicht `ssh` von Hand?** Aus demselben Grund, aus dem es `dc-testkit` +überhaupt gibt: Sonst baut jeder Agent seine eigene Fassung derselben SSH-Zeile. Vier Dinge gehen +dabei zuverlässig schief: + +1. **Das Token stünde in `ps aux`.** `ssh vm 'DC_TOKEN=xyz dc-testkit run'` legt es offen — der + Befehlsstring **ist** die Befehlszeile der entfernten Shell, und auf einer geteilten Maschine + liest jeder andere Benutzer fremde Befehlszeilen. Der Wrapper schickt es über die + Standardeingabe; von dort geht es direkt in die Umgebung, und die liest nur, wem der Prozess + gehört. +2. **`~/bin` liegt über SSH nicht im PATH.** `~/.profile` nimmt es auf, wird aber nur von einer + **Login-Shell** gelesen — und `ssh vm 'befehl'` startet keine. Die Antwort wäre + `Kommando nicht gefunden`: sieht aus wie ein fehlgeschlagener Bezug und ist keiner. Der + Wrapper ruft `bash -l`. +3. **Quoting durch drei Ebenen.** Ein `--title "Update bricht ab"` muss durch lokale Shell, SSH + und entfernte Shell. Ohne Sorgfalt kommt der Titel dort zerlegt an und die Meldung trägt den + halben Text. Der Wrapper schickt den Befehl über die Standardeingabe, nicht durch das Quoting. +4. **Kein Verbindungs-Multiplexing unter Windows.** Jeder Aufruf kostet einen vollen Aufbau durch + den Tunnel; `--skript` bündelt einen ganzen Prüfplan in einen. + +> ⚠️ **Was `dc-testkit-remote` nicht kann:** alles, was einen Bildschirminhalt zurückbringt. +> Aufnahmen entstehen mit `shot` auf der VM unter `/tmp/_.png` und werden danach +> abgeholt — dafür bleibt `scp` der Weg (5.8). + > ⚠️ **Beides — dieser Download und `dc-testkit license` — setzt Deploymentcenter 2.6.1 voraus.** > Ist die Fassung auf dem Server älter, fehlt der Endpunkt `/api/license/v1/dev-key`. Er fällt > dabei **nicht** mit 404 auf: Die Lizenz-API antwortet bei unbekannten Pfadsegmenten mit ihrer @@ -136,10 +209,30 @@ chmod +x ~/bin/dc-testkit > der Fassung: Im Webroot lag die Datei nur unter `/public/testkit/`, die Route auf `/testkit/` > fehlte in der `.htaccess`. Wer vor 2.7.1 ein 404 sah, hat deshalb nicht unbedingt einen zu alten > Server vor sich. Ausweichweg für alte Stände: -> `scp public/testkit/dc-testkit.sh testvm:~/bin/dc-testkit` +> `scp public/testkit/dc-testkit.sh testvm-:~/bin/dc-testkit` +> — und für den Arbeitsplatz `cp public/testkit/dc-testkit-remote.sh ~/bin/dc-testkit-remote`. -Dann je Projekt eine Konfiguration — **das Token gehört in die Datei, nicht in die Befehlszeile**, -sonst steht es in der Shell-History: +Dann je Projekt eine Konfiguration. **Der bessere Weg ist `DC_PROJECT_DIR`** — dann steht das +Token nur an einer Stelle, in der `.mcp.json` des Projekts (4.2): + +```bash +mkdir -p ~/.config/dc-testkit +cat > ~/.config/dc-testkit/polytrader <<'EOF' +DC_REMOTE=testvm-polytrader +DC_PROJECT=polytrader +DC_PROJECT_DIR=J:/Softwareprojekte/PolytraderSharp/PolyTraderSharp +DC_CHANNEL=prod +DC_DISPLAY=:99 +EOF +chmod 600 ~/.config/dc-testkit/polytrader +``` + +`DC_PROJECT_DIR` ist der Pfad **auf dem Arbeitsplatz** — dort liegt das Repo mit der `.mcp.json`. +Was in den Projektverzeichnissen auf der VM liegt, spielt keine Rolle; der Agent arbeitet nie +von dort aus. + +Wo es kein Projekt-Repo gibt, kommt das Token in die Datei — **nie in die Befehlszeile**, sonst +steht es in der Shell-History: ```bash mkdir -p ~/.config/dc-testkit @@ -155,28 +248,136 @@ chmod 600 ~/.config/dc-testkit/config Arbeitet ein Agent an mehreren Projekten, hält er je Projekt eine eigene Datei und wählt sie über `DC_TESTKIT_CONFIG=~/.config/dc-testkit/polytrader dc-testkit …`. +**Ein Testagent gehört zu genau einem Projekt** (siehe 4.8). Bei ihm steht der Pfad fest in seinem +Prompt und nicht in seinem Gedächtnis — wer die Datei vergisst, prüft stillschweigend das falsche +Projekt und meldet dorthin. + +Was in der Datei stehen darf: + +| Schlüssel | Bedeutung | Vorgabe | +|---|---|---| +| `DC_URL` | Instanz, gegen die gearbeitet wird | `https://dc.mhdf.de` | +| `DC_TOKEN` | Werkbank-Token des Projekts (4.2) | — | +| `DC_PROJECT_DIR` | Projekt-Repo; ohne `DC_TOKEN` wird es aus dessen `.mcp.json` gelesen | — | +| `DC_PROJECT` | Slug des Projekts | — | +| `DC_CHANNEL` | Release-Kanal | `prod` | +| `DC_INSTANCE` | Maschinenkennung, an der der DEV-Schlüssel hängt | Kurzname des Rechners | +| `DC_DISPLAY` | Virtuelles Display dieses Projekts (5.5, 5.8) | — (dann keine Oberflächenprüfung) | + +`DC_INSTANCE` nur setzen, wenn mehrere Testläufe auf **derselben** Maschine auseinandergehalten +werden müssen. Sie muss über Läufe hinweg gleich bleiben: Wechselt sie, sucht der Server jedes Mal +einen anderen Schlüssel heraus, bis die Grenze von zwölf je Projekt greift. + +**Rangfolge:** Schalter auf der Befehlszeile schlägt Umgebungsvariable schlägt Datei schlägt +Vorgabe. Bis 1.1.0 war das genau verkehrt herum — die Datei überschrieb die Umgebung, und +`DC_PROJECT=x dc-testkit report` meldete stillschweigend in das Projekt aus der Datei. Auf einer +Maschine mit mehreren Projekten ist das die teuerste Sorte Fehler: Es geht nichts kaputt, es landet +nur alles am falschen Ort. + Prüfen, ob alles steht: ```bash dc-testkit doctor ``` +`doctor` ist die einzige Auskunft, der zu trauen ist — aber nur, wenn er selbst aktuell ist. Er +prüft deshalb seit 1.1.0 als Erstes den eigenen Stand gegen die ausgelieferte Fassung: + +```bash +dc-testkit selfupdate # auf die ausgelieferte Fassung bringen +dc-testkit selfupdate --pruefen # nur nachsehen, nichts ändern +``` + +> **Warum das gebraucht wurde.** Das Skript liegt auf jeder Maschine als Kopie und altert dort +> still. Am 31.08.2026 lag auf der Test-VM eine Fassung, die 236 Zeilen älter war als die +> ausgelieferte — beide meldeten `1.0.0`. Deshalb sagte `doctor` dort „Bereit", während die +> aktuelle Fassung drei fehlende Rechte des Tokens angemeckert hätte. **Die Fassungsnummer wird +> bei jeder Änderung hochgezählt**; ohne das ist die Selbstprüfung wertlos. Sie sagt das auch: +> gleiche Nummer bei anderem Inhalt ist ein Befund, kein Achselzucken. +> +> Ist die eigene Fassung **neuer** als die ausgelieferte, meldet `doctor` einen Entwicklungsstand +> und rät ausdrücklich **nicht** zum `selfupdate` — das holte die ältere Fassung zurück. +> `selfupdate` verweigert diesen Rückschritt von sich aus; `--force` hebt die Sperre auf. + ### 4.2 Das Werkbank-Token -Ein Mensch legt es **einmal je Projekt** an — im WebUI unter *System → Tokens*: +**Es existiert wahrscheinlich schon.** Jedes angebundene Projekt hat eines — es steht in der +`.mcp.json` des Projekt-Repos, wo der MCP-Adapter es ohnehin braucht: + +```json +{ "mcpServers": { "deploymentcenter": { "env": { "DC_TOKEN": "dc_master_…" } } } } +``` + +Das Testkit holt es sich von dort. Dazu zeigt `DC_PROJECT_DIR` auf das Repo — gesucht wird im +Verzeichnis selbst und eine Ebene tiefer: + +```bash +DC_PROJECT_DIR=~/RiderProjects/PolyTraderSharp +``` + +Damit gibt es das Geheimnis **einmal je Projekt**, nicht zusätzlich in +`~/.config/dc-testkit/config`. Zwei Ablagen desselben Tokens heißt: Beim nächsten Wechsel wird +eine davon vergessen. Genau so war es auf der Test-VM — dort lag in der Testkit-Konfiguration +noch ein altes Laufzeit-Token (`watchdog:ping`, `bugtracker:report`, ohne Bindung), während das +richtige Werkbank-Token des Projekts längst ein anderes war. `doctor` meldete trotzdem „Bereit", +und die Rechte fehlten erst dort, wo sie gebraucht wurden. + +> **`.mcp.json` kommt über Git nicht mit.** Sie ist per `.gitignore` ausgeschlossen — auf einer +> frisch geklonten Maschine fehlt sie deshalb **immer**. Das ist kein Fehler im Repo und keiner +> in den Rechten. Das Token muss von Hand dorthin, aus der `.mcp.json` des Projekts auf dem +> Arbeitsplatz. `dc-testkit` sagt genau das, statt an einem 401 zu scheitern: +> +> ``` +> Fehler Kein Werkbank-Token. +> +> In ~/RiderProjects/PolyTraderSharp liegt keine .mcp.json mit einem DC_TOKEN. +> +> Das ist kein Fehler im Repo: .mcp.json ist per .gitignore ausgeschlossen +> und wird ueber Git NICHT mitgeliefert. +> ``` +> +> Ein Platzhalter (`"${DC_TOKEN}"`) zählt dabei nicht als Token — sonst liefe der Aufruf mit +> einer Zeichenkette los, die der Server nur mit 401 beantworten kann. + +**Wenn wirklich keines existiert**, legt ein Mensch es an — im WebUI unter +*Token-Verwaltung → ➕ Token erstellen*: | Feld | Wert | |---|---| | Typ | Master | | Besitzerart | `dev_agent` | -| Projekt | der Slug des Projekts | +| Projekt | der Slug des Projekts — **nicht leer** | | Rechte | `bugtracker:manage`, `license:dev`, `updateservice:read` | -Mehr braucht es nicht, und mehr sollte es nicht sein. `bugtracker:manage` schließt Lesen und -Melden ein, `license:dev` schließt `updateservice:read` ein — zwei Häkchen genügen also. +`bugtracker:manage` schließt Lesen und Melden ein, `license:dev` schließt `updateservice:read` +ein — zwei Häkchen genügen also. -**Nicht `*` vergeben.** Ein Token mit allen Rechten auf einer geteilten Testmaschine darf auch -Releases veröffentlichen und Lizenzen fremder Projekte anfassen. +#### Warum die Projektbindung nicht optional ist + +**Ein Token je Projekt, gebunden an dieses Projekt.** Ein „allgemeines Token für die ganze VM" +klingt praktischer und ist es nicht — aus zwei Gründen, die in verschiedene Richtungen wirken: + +**Es reicht nicht weit genug.** Ein Testlauf prüft die Auslieferung *eines Produkts*: dessen +Release aus der Ablage, dessen Lizenzschlüssel, dessen Update. Beim Ausstellen des +DEV-Schlüssels prüft der Server die Bindung ausdrücklich und lässt hier **keine** Ausnahme für +Werkbank-Token zu. Ein an `deploymentcenter` gebundenes Token bekommt für `polytrader` ein +`403 project_forbidden` — ein Lizenzschlüssel öffnet die Release-Ablage eines Produkts, und das +ist etwas anderes als eine Meldung. + +**Und es reicht zu weit.** Ein Token **ohne** Bindung ist nicht etwa eingeschränkt, sondern +uneingeschränkt: Der Server leitet daraus kein Heimatprojekt ab, also ist für ihn *jede* Meldung +eine Heimmeldung. Damit fällt der gesamte Schutz für Fremdmeldungen weg — keine +Begründungspflicht, keine Deckelung auf `medium`, kein Eingang in `triage_external`, kein +`reported_from`. Meldungen landen unmittelbar in der offenen Liste fremder Projekte und +verderben die Zahlen, auf denen die Supervisor-Detektoren kalibrieren. + +Die bezeugte Herkunft ist der Punkt: Melden drei Projekte dasselbe, ist das eine Aussage über +die Sache. Ohne Bindung ist es eine Aussage über niemanden. + +**Zu `*`:** Die vorhandenen Projekt-Token tragen durchweg Vollzugriff. Das läuft, deckt die drei +gebrauchten Rechte ab — und erlaubt auf einer **geteilten** Maschine eben auch +`updateservice:publish`. Ein Release aus einem Testlauf heraus wäre für Kunden sofort sichtbar. +Wer die Token neu ausstellt, schneidet sie auf die drei Rechte zu. ### 4.3 DEV-Lizenzschlüssel selbst holen @@ -220,6 +421,18 @@ dc-testkit update # Update auf latest dc-testkit verify ``` +Drei weitere Aufrufe reichen den jeweiligen Modus des `update-agent` durch. Sie verändern den +Sandkasten nicht — außer `repair`, der genau dafür da ist: + +| Aufruf | Wofür | +|---|---| +| `dc-testkit check` | Gibt es überhaupt ein Update? Antwortet, ohne etwas zu holen | +| `dc-testkit list` | Welche Fassungen führt der Server in diesem Kanal? | +| `dc-testkit repair` | Integritätsreparatur — stellt her, was im Sandkasten fehlt oder nicht zur Prüfsumme passt | + +`repair` gehört in einen Prüfplan, weil es der Weg ist, den ein Kunde nach einem abgebrochenen +Update nimmt. Ein Modus, den niemand testet, ist im Ernstfall genau der, der nicht funktioniert. + Oder alles auf einmal, mit automatischer Meldung dessen, was scheitert: ```bash @@ -497,6 +710,39 @@ und das Schreiben der Konfiguration bleiben ungeprüft. Ein kopfloser Installati (`--action install --answers antworten.json`) würde diese Lücke schließen; er steht in der [Roadmap](./ROADMAP.md) unter G1. +### 4.8 Ein Agent, der nur prüft + +Ein **Prüfagent** ist etwas anderes als ein Entwickleragent, der nebenbei testet. Er gehört zu +genau einem Projekt, läuft auf dieser Maschine — und **behebt nichts**. + +Der Unterschied ist keine Geschmacksfrage. Wer einen Fix schreibt und ihn anschließend selbst +abnimmt, prüft gegen die Erwartung, die er beim Schreiben hatte. Genau dagegen ist der Tracker +gebaut: `resolve` behauptet „ich habe es behoben, in Build X"; `confirm` behauptet nichts, sondern +bestätigt den Fix eines anderen (siehe 4.5, „Der Lebenslauf"). + +| Der Prüfagent tut | Er tut **nicht** | +|---|---| +| Auslieferung und Update gegen den Sandkasten prüfen (4.4) | Code des Projekts ändern | +| Oberflächen begutachten (5.8) | `dc-testkit resolve` aufrufen | +| Befunde melden — auch in fremden Projekten (4.5, 4.6) | Die Roadmap ändern | +| Prüfaufträge übernehmen und mit `confirm` oder erneuter Meldung schließen | Ein Item beheben, das er selbst gemeldet hat | + +**Diese Trennung erzwingt derzeit kein Recht.** `bugtracker:manage` ist ein einziger Schreib-Scope: +Wer bestätigen darf, darf auch `resolve`, `bulk_update` und die Roadmap umschreiben. Bis es ein +eigenes `bugtracker:verify` gibt, steht die Grenze **nur im Prompt des Agenten** — und gehört +deshalb dort ausdrücklich hinein. Der fertige Textbaustein dafür ist **E** in +[AGENT_PROMPT_TEMPLATE.md](AGENT_PROMPT_TEMPLATE.md). + +**Gegen welche Fassung?** Jede Meldung trägt die geprüfte Fassung mit, gelesen aus der +`manifest.json` im Sandkasten. Ohne sie kann der Server nicht unterscheiden, ob ein Befund neu ist +oder ob die Maschine schlicht einen älteren Stand fährt — und legt im Zweifel ein Regressionsitem +an (4.5). Für einen Agenten, der nichts anderes tut als prüfen, ist das die wichtigste einzelne +Angabe überhaupt. + +**Was ihn einrichtet**, steht in [AGENTEN_ANBINDEN.md](AGENTEN_ANBINDEN.md); die Reihenfolge ist +dort dieselbe wie für jeden anderen Agenten, nur mit anderem Prompt-Baustein und anderer +Abnahmefrage. + --- ## 5. Konventionen für mehrere Projekte @@ -619,6 +865,11 @@ sudo rm /etc/sudoers.d/99-testing Das hebt den Schutz für das Konto vollständig auf — vertretbar auf einer Test-VM hinter VPN, aber nicht dauerhaft und niemals auf einer Produktivmaschine. +> **Stand 31.08.2026: `/etc/sudoers.d/99-testing` liegt dort und gilt.** Aus „befristet" ist +> „dauerhaft" geworden — der Weg, den solche Freigaben immer nehmen, wenn das Entfernen nicht +> zum selben Arbeitsgang gehört. Wer als Nächstes etwas mit `sudo` erledigt, nimmt sie danach +> zurück. Ein Prüfagent braucht sie nicht. + **Für das Testkit wird das nicht gebraucht.** Es arbeitet vollständig im Benutzerkontext. ### 5.7 GUI-Anwendungen ohne Bildschirm testen @@ -642,7 +893,28 @@ Bildschirmfotos schicken. Das Verfahren hat sich am 26.08.2026 bewährt und **si zutage gefördert**, darunter einen, der sämtliche Tabellen der Anwendung unsichtbar machte. Drei Werkzeuge, alle installiert: **Xvfb** (virtuelles Display), **xdotool** (Klicken und Tippen), -**ImageMagick** (`import` für Aufnahmen, `convert` zum Zuschneiden). +**ImageMagick** (`import` für Aufnahmen, `convert` zum Zuschneiden). `dc-testkit doctor` sagt seit +1.1.0, ob sie da sind und ob das eigene Display frei ist. + +**Den immer gleichen Teil nimmt das Testkit ab** — Display starten, aufnehmen, abräumen. Setze +dazu `DC_DISPLAY` in deiner Konfiguration (die Nummer steht in Abschnitt 7): + +```bash +dc-testkit display status # frei, belegt oder verwaister Socket? +dc-testkit display start # Xvfb auf DC_DISPLAY, Vorgabe 1600x1000x24 +dc-testkit shot 1_start # nach /tmp/_1_start.png +dc-testkit display stop # nur das eigene, nie fremde +``` + +Das sind genau die drei Stellen, an denen ein selbstgeschriebenes Skript regelmäßig danebengreift: +`DISPLAY` nicht gesetzt (dann landet alles auf `:0`, dem echten Desktop), `pkill Xvfb` ohne Nummer +(das erschlägt die Sitzungen aller anderen Projekte), Dateinamen ohne Projektpräfix (zwei Läufe +überschreiben sich gegenseitig). `display start` verweigert `:0` und `:1` von sich aus und erkennt +einen verwaisten Socket, statt an ihm zu scheitern. + +**Was projektspezifisch bleibt**, ist der Klickplan dazwischen — Fenstername, Koordinaten, +Wartezeiten. Den kapselt das Testkit bewusst nicht: Er ist bei jedem Programm anders, und ein +Werkzeug, das ihn zu erraten versucht, wäre unzuverlässiger als das Rezept unten. Anders als `xvfb-run` wird das Display hier **dauerhaft** gestartet, damit sich zwischen den Aufnahmen navigieren lässt: @@ -681,7 +953,7 @@ kill $APP 2>/dev/null; sleep 2; pkill -f "Xvfb $DISP" 2>/dev/null Danach die Bilder abholen und ansehen: ```bash -scp testvm:'/tmp/polytrader_*.png' . +scp testvm-polytrader:'/tmp/polytrader_*.png' . ``` **Was sich dabei bewährt hat:** @@ -730,11 +1002,20 @@ nutzen, Port 3389 ist offen) — nötig ist es für die Sichtprüfung nicht. ## 7. Belegung (bei Änderungen fortschreiben) +**Erhoben am 31.08.2026 von der Maschine selbst**, nicht aus dieser Datei fortgeschrieben — sie +war zu dem Zeitpunkt vier Projekte im Rückstand. Wer etwas anlegt, trägt es hier ein; wer sich +darauf verlassen will, prüft nach: + +```bash +ssh testvm- 'sudo mariadb -e "SHOW DATABASES;"; ls /opt ~; ss -tlnp; ls /tmp/.X11-unix/; pgrep -a Xvfb' +``` + **Datenbanken** | Datenbank | Benutzer | Projekt | seit | |---|---|---|---| | `polytrader_test` | `polytrader_test`@localhost | PolyTrader | 26.08.2026 | +| `ibkrtrader_test` | `ibkrtrader_test`@localhost | IBKRTrader | vor 31.08.2026 | **Dienste** @@ -742,41 +1023,81 @@ nutzen, Port 3389 ist offen) — nötig ist es für die Sichtprüfung nicht. |---|---|---|---|---| | `polytrader.service` | `polytrader` | `/opt/polytrader` | PolyTrader | 26.08.2026 | +Am 31.08.2026 lief keiner dieser Dienste — wie es Abschnitt 8 verlangt. + **Verzeichnisse** | Projekt | Publish-Verzeichnis | Dienst | |---|---|---| | PolyTrader | `~/polytrader-test` | `/opt/polytrader` | +| IBKRTrader | `~/ibkrtrader-test` | — | +| ClawdDotNet | `~/clawddotnet-test` | — | +| Predictalytics | `~/predictalytics` ⚠️ | — | + +⚠️ **`~/predictalytics` bricht die Konvention aus 5.2** (`~/-test`). Aus dem Namen ist +nicht mehr abzulesen, ob dort ein Testlauf oder eine Arbeitskopie liegt — und genau diese Frage +stellt sich, wenn jemand aufräumt. Beim nächsten Anfassen umbenennen. + +Quellbäume liegen daneben unter `~/RiderProjects/` (5.2): ClawdDotNet, Deploymentcenter, +IBKRTrader, PolyTraderSharp, Predictalytics. **Werkbank-Token** (Vergabe siehe 4.2) -| Projekt | Token angelegt | Rechte | -|---|---|---| -| *noch keins* | | | +Die richtigen existieren längst — je Projekt eines, in der `.mcp.json` des Repos auf dem +Arbeitsplatz: + +| Projekt | Token | Bindung | Rechte | +|---|---|---|---| +| PolyTrader | `DEV-Polytrader` | `polytrader` | `*` | +| IBKRTrader | `DEV-IBKRTrader` | `ibkrtrader` | `*` | +| Deploymentcenter | `DEV-Deploymentcenter` | `deploymentcenter` | `*` | +| ClawdDotNet | — | — | `.mcp.json` enthält nur den Platzhalter | + +Alle sind `dev_agent` und korrekt gebunden. Zu `*` siehe die Anmerkung am Ende von 4.2. + +⚠️ **Auf der VM kommt davon nichts an.** Unter `~/RiderProjects/` liegt keine einzige +`.mcp.json` — die Repos sind dort frisch geklont, und die Datei ist per `.gitignore` +ausgeschlossen. In `~/.config/dc-testkit/config` steckt stattdessen ein altes Sub-Token +(„PolyTrader Avalonia (Dev, alle Umgebungen)", `watchdog:ping` + `bugtracker:report`, ohne +Bindung). Damit sind `license`, `install`, `update`, `verify`, `run`, `next`, `comment`, +`resolve` und `confirm` gesperrt; übrig bleibt `report`. + +**Zu beheben ist das nicht durch ein neues Token**, sondern indem das vorhandene +`DEV-Polytrader` auf die Maschine kommt — als `.mcp.json` im Projektverzeichnis, auf das dann +`DC_PROJECT_DIR` zeigt. `dc-testkit doctor` benennt seit 1.1.0 jeden dieser Punkte einzeln und +nennt die Herkunft des benutzten Tokens. **X-Displays** (Vergabe siehe 5.5) | Display | Projekt | Stand | |---|---|---| -| `:0` | echter Desktop — **nie belegen** | | +| `:0`, `:1` | echter Desktop und Anmeldebildschirm — **nie belegen** | | | `:99` | PolyTrader | 26.08.2026 | | `:98` | Predictalytics | reserviert | | `:97` | ClawdDotNet | reserviert | | `:96` | IBKRTrader | reserviert | | `:95` abwärts | frei für künftige Projekte | | -**Ports** (Stand 26.08.2026) +Am 31.08.2026 lief **kein** Xvfb; belegt waren nur `:0` und `:1`. Dass `:1` existiert, stand hier +bisher nicht — es gehört zum Anmeldebildschirm und ist genauso tabu wie `:0`. + +**Ports** (Stand 31.08.2026) | Port | Belegt durch | |---|---| | 22 | SSH | +| 53 | systemd-resolved (nur localhost) | +| 631 | CUPS (localhost, IPv4 **und** IPv6) | | 3306 | MariaDB (nur localhost) | | 3389 | RDP | -| 5000 | vorhandener Dienst (nur IPv6-localhost) | -| 631 | CUPS | +| 5000 | vorhandener Dienst (localhost, IPv4 **und** IPv6) | + +Port 5000 war hier als „nur IPv6-localhost" geführt. Das stimmt nicht mehr — wer ihn für einen +Testdienst nehmen wollte, wäre auf beiden Familien kollidiert. **Nachträglich installiert:** MariaDB 11.8.6, Xvfb, xdotool, ImageMagick, x11-utils -(alle am 26.08.2026 für PolyTrader). +(alle am 26.08.2026 für PolyTrader), jq (undatiert, von unbekannter Hand — das Testkit braucht +es nicht). --- @@ -823,6 +1144,13 @@ Danach Abschnitt 7 nachziehen. Neustart; mehrere Reparaturversuche waren erfolglos, es wird auf ein Update gewartet. Agenten arbeiten deshalb **über SSH von außen**. (Speichermangel ist **nicht** die Ursache — die Maschine hat 31 GB, davon 25 GB frei.) + + **Für Prüfagenten ist das die bestimmende Einschränkung.** Ein Prüfagent je Projekt läuft + nicht *auf* der Maschine, sondern auf einem Arbeitsplatz und greift über `testvm-` + zu. Daraus folgt zweierlei: Er kann `node` und damit den MCP-Adapter dort ohnehin nicht + benutzen (der ist auch nicht installiert), und er sollte mehrere Prüfschritte pro + SSH-Aufruf bündeln — Verbindungs-Multiplexing steht ihm unter Windows nicht zur Verfügung + (2.3). - **Verbindungs-Multiplexing funktioniert nicht mit Windows-OpenSSH** (siehe 2.3). - **Der Installationsdialog ist nicht kopflos prüfbar** (siehe 4.7). Damit bleiben die Auflösung von `setup.json` und das Schreiben der Konfiguration ungeprüft. diff --git a/public/testkit/dc-testkit-remote.sh b/public/testkit/dc-testkit-remote.sh new file mode 100644 index 0000000..144bbc6 --- /dev/null +++ b/public/testkit/dc-testkit-remote.sh @@ -0,0 +1,250 @@ +#!/usr/bin/env bash +# +# dc-testkit-remote - dc-testkit auf der Test-VM aufrufen, vom Arbeitsplatz aus +# +# Der Agent laeuft NICHT auf der Test-VM (TESTUMGEBUNG.md 9: Claude Code +# blockiert sie bis zum Neustart). Er laeuft auf dem Arbeitsplatz, wo auch die +# Projekt-Repos liegen - und damit das Werkbank-Token in deren .mcp.json. +# Getestet wird trotzdem dort drueben. +# +# Dieses Skript ist der Weg dazwischen. Es nimmt vier Dinge ab, an denen sonst +# jeder Agent einzeln scheitert: +# +# 1. Das Token muss zur VM, darf dort aber nicht in "ps aux" auftauchen. Auf +# einer geteilten Maschine liest jeder andere Benutzer die Befehlszeilen +# fremder Prozesse. "ssh vm 'DC_TOKEN=xyz dc-testkit run'" legt es offen - +# der Befehlsstring IST die Befehlszeile der entfernten Shell. Hier geht +# es ueber die Standardeingabe und von dort direkt in die Umgebung; die +# liest nur, wem der Prozess gehoert. +# +# 2. "~/bin" liegt ueber SSH nicht im PATH: ".profile" wird nur von einer +# Login-Shell gelesen, und "ssh vm 'befehl'" startet keine. Die Antwort +# waere "Kommando nicht gefunden" - das sieht aus wie ein fehlgeschlagener +# Bezug und ist keiner. Deshalb "bash -l". +# +# 3. Die Einstellungen kommen vom Arbeitsplatz, nicht von der VM. Dort liegt +# keine Konfiguration mehr - DC_TESTKIT_CONFIG zeigt bewusst ins Leere. +# Sonst gilt drueben, was irgendwann einmal jemand eingetragen hat, und +# genau daran ist es schon gescheitert: In der Konfiguration auf der VM +# stand ein altes Laufzeit-Token, waehrend das richtige Werkbank-Token im +# Projekt laengst ein anderes war. +# +# 4. Windows-OpenSSH kann kein Verbindungs-Multiplexing. Jeder Aufruf kostet +# einen vollen Aufbau durch den Tunnel - also mehrere Schritte je Aufruf +# buendeln (--skript) statt sie einzeln abzusetzen. +# +# Aufruf: +# dc-testkit-remote doctor +# dc-testkit-remote report --title "..." --desc "..." +# dc-testkit-remote --skript pruefplan.sh +# +# Vollstaendige Anleitung: docs/TESTUMGEBUNG.md im Deploymentcenter-Repo. + +set -euo pipefail + +DC_TESTKIT_REMOTE_VERSION="1.1.0" + +if [ -t 2 ] && [ -z "${NO_COLOR:-}" ]; then + C_RESET=$'\033[0m'; C_RED=$'\033[31m' +else + C_RESET=''; C_RED='' +fi + +die() { + printf '%sFehler%s %s\n' "$C_RED" "$C_RESET" "$*" >&2 + exit 1 +} + +usage() { + cat <<'EOF' +dc-testkit-remote - dc-testkit auf der Test-VM aufrufen + + dc-testkit-remote [argumente...] + dc-testkit-remote --skript + + Slug; waehlt ~/.config/dc-testkit/ auf DIESEM Rechner + --skript Mehrere dc-testkit-Aufrufe in einem Rutsch. Die Datei geht ueber + die Standardeingabe und liegt nie auf der geteilten Maschine. + +Konfiguration je Projekt in ~/.config/dc-testkit/: + + DC_REMOTE=testvm- SSH-Alias der Testmaschine (Pflicht) + DC_PROJECT_DIR= Token aus dessen .mcp.json + DC_TOKEN= Alternative, wenn es kein Repo gibt + DC_PROJECT= Vorgabe: der -Parameter + DC_CHANNEL= Vorgabe: prod + DC_DISPLAY=: fuer display/shot (TESTUMGEBUNG.md 5.5) + DC_INSTANCE= nur noetig bei mehreren Laeufen je Maschine + +Alles davon gilt auf DIESEM Rechner. Auf der VM wird keine Konfiguration +gelesen - was dort liegt, bleibt ohne Wirkung. +EOF +} + +# --------------------------------------------------------------------------- +# Token aus der .mcp.json des Projekt-Repos - dieselbe Quelle, die auch der +# MCP-Adapter benutzt. Ein zweiter Ablageort waere einer, der beim naechsten +# Wechsel vergessen wird. +# --------------------------------------------------------------------------- + +# Welcher Python laeuft hier? +# +# Anders als das Testkit laeuft dieses Skript auf dem Arbeitsplatz, und der ist +# oft Windows. Dort gibt es "python3" zwar im PATH - als App-Ausfuehrungsalias +# des Microsoft Store, der bei jedem Aufruf nur eine Werbebotschaft ausgibt und +# mit 9009 endet. Ein blosses "command -v python3" faellt darauf herein. +# Deshalb wird ausprobiert statt nachgesehen. +python_finden() { + local kandidat + + for kandidat in python3 python py; do + command -v "$kandidat" >/dev/null 2>&1 || continue + "$kandidat" -c 'import sys; sys.exit(0 if sys.version_info[0] >= 3 else 1)' >/dev/null 2>&1 \ + || continue + printf '%s\n' "$kandidat" + return 0 + done + + return 1 +} + +token_aus_projekt() { + local wurzel="$1" datei python + + python="$(python_finden)" \ + || die "Kein funktionsfaehiges Python 3 gefunden (python3, python, py). + +Gebraucht wird es, um das Token aus der .mcp.json zu lesen. Alternativ +DC_TOKEN direkt in der Konfiguration hinterlegen." + + for datei in "$wurzel/.mcp.json" "$wurzel"/*/.mcp.json; do + [ -f "$datei" ] || continue + + "$python" -c ' +import json, re, sys + +try: + with open(sys.argv[1], encoding="utf-8-sig") as fh: + daten = json.load(fh) +except Exception: + sys.exit(1) + +for server in (daten.get("mcpServers") or {}).values(): + tok = (server.get("env") or {}).get("DC_TOKEN") + if isinstance(tok, str) and tok and not re.search(r"[$%<]", tok): + print(tok) + sys.exit(0) + +sys.exit(1) +' "$datei" && return 0 + done + + return 1 +} + +# --------------------------------------------------------------------------- + +[ $# -ge 1 ] || { usage; exit 1; } + +case "$1" in + -h|--help) usage; exit 0 ;; + --version) printf 'dc-testkit-remote %s\n' "$DC_TESTKIT_REMOTE_VERSION"; exit 0 ;; +esac + +PROJEKT="$1"; shift +[ $# -ge 1 ] || { usage; exit 1; } + +CONFIG_FILE="${DC_TESTKIT_CONFIG:-$HOME/.config/dc-testkit/$PROJEKT}" + +[ -f "$CONFIG_FILE" ] || die "Keine Konfiguration: $CONFIG_FILE + +Anlegen (TESTUMGEBUNG.md 4.1): + DC_REMOTE=testvm-$PROJEKT + DC_PROJECT_DIR=" + +# shellcheck disable=SC1090 +. "$CONFIG_FILE" + +DC_REMOTE="${DC_REMOTE:-}" +DC_PROJECT_DIR="${DC_PROJECT_DIR:-}" +DC_TOKEN="${DC_TOKEN:-}" +DC_PROJECT="${DC_PROJECT:-$PROJEKT}" +DC_CHANNEL="${DC_CHANNEL:-prod}" +DC_DISPLAY="${DC_DISPLAY:-}" +DC_INSTANCE="${DC_INSTANCE:-}" + +[ -n "$DC_REMOTE" ] || die "DC_REMOTE fehlt in $CONFIG_FILE - der SSH-Alias der Testmaschine." + +if [ -z "$DC_TOKEN" ] && [ -n "$DC_PROJECT_DIR" ]; then + DC_TOKEN="$(token_aus_projekt "$DC_PROJECT_DIR" || true)" +fi + +if [ -z "$DC_TOKEN" ]; then + printf '%sFehler%s Kein Werkbank-Token.\n\n' "$C_RED" "$C_RESET" >&2 + + if [ -n "$DC_PROJECT_DIR" ]; then + printf 'In %s liegt keine .mcp.json mit einem DC_TOKEN.\n\n' "$DC_PROJECT_DIR" >&2 + printf 'Das ist kein Fehler im Repo: .mcp.json ist per .gitignore ausgeschlossen\n' >&2 + printf 'und wird ueber Git NICHT mitgeliefert. Auf einer frisch geklonten\n' >&2 + printf 'Maschine fehlt sie deshalb immer.\n\n' >&2 + printf 'Das Dev-Agent-Token von "%s" muss dort eingetragen werden.\n' "$PROJEKT" >&2 + else + printf 'Setze DC_PROJECT_DIR (Repo-Pfad) oder DC_TOKEN in %s.\n' "$CONFIG_FILE" >&2 + fi + + exit 1 +fi + +# --------------------------------------------------------------------------- +# Der entfernte Teil +# +# Der SSH-Befehlsstring ist KONSTANT - er enthaelt weder Token noch Argumente. +# Alles Veraenderliche geht ueber die Standardeingabe: +# +# Zeile 1 das Token +# Zeile 2 der Befehl, mit %q so zitiert, dass eval ihn heil zurueckbaut +# danach was der Befehl selbst von der Standardeingabe liest +# (z.B. "dc-testkit report", das seine Beschreibung von dort nimmt) +# +# "read -r" holt genau eine Zeile und liest nicht darueber hinaus; der Rest +# bleibt fuer den Befehl uebrig. Ohne diese Trennung muesste der Befehl durch +# drei Ebenen Quoting - lokale Shell, ssh, entfernte Shell - und ein Titel mit +# Anfuehrungszeichen kaeme dort zerlegt an. +# --------------------------------------------------------------------------- + +FERN='read -r DC_TOKEN; export DC_TOKEN; read -r DC_BEFEHL; eval "$DC_BEFEHL"' + +# DC_TESTKIT_CONFIG bewusst auf /dev/null: Die Einstellungen kommen von hier, +# nicht von der VM. Was dort in ~/.config/dc-testkit liegt, bleibt wirkungslos. +UMGEBUNG="export DC_TESTKIT_CONFIG=/dev/null" +UMGEBUNG="$UMGEBUNG DC_PROJECT=$(printf '%q' "$DC_PROJECT")" +UMGEBUNG="$UMGEBUNG DC_CHANNEL=$(printf '%q' "$DC_CHANNEL")" +[ -n "$DC_DISPLAY" ] && UMGEBUNG="$UMGEBUNG DC_DISPLAY=$(printf '%q' "$DC_DISPLAY")" +[ -n "$DC_INSTANCE" ] && UMGEBUNG="$UMGEBUNG DC_INSTANCE=$(printf '%q' "$DC_INSTANCE")" +[ -n "${NO_COLOR:-}" ] && UMGEBUNG="$UMGEBUNG NO_COLOR=1" + +if [ "${1:-}" = '--skript' ]; then + [ $# -eq 2 ] || die "--skript nimmt genau eine Datei." + [ -f "$2" ] || die "Nicht gefunden: $2" + + # Der Pruefplan wird als Befehl "bash -s" ausgefuehrt und selbst ueber die + # Standardeingabe nachgeliefert - er liegt damit nie als Datei auf der + # geteilten Maschine. + { printf '%s\n' "$DC_TOKEN" + printf '%s; exec bash -s\n' "$UMGEBUNG" + cat "$2" + } | ssh "$DC_REMOTE" "bash -l -c '$FERN'" + exit $? +fi + +BEFEHL="$UMGEBUNG; dc-testkit" +for arg in "$@"; do + BEFEHL="$BEFEHL $(printf '%q' "$arg")" +done + +{ printf '%s\n' "$DC_TOKEN" + printf '%s\n' "$BEFEHL" + # Was auf unserer eigenen Standardeingabe steht, reichen wir durch - sonst + # koennte "dc-testkit report" seine Beschreibung nicht von dort lesen. + [ -t 0 ] || cat +} | ssh "$DC_REMOTE" "bash -l -c '$FERN'" diff --git a/public/testkit/dc-testkit.sh b/public/testkit/dc-testkit.sh index ef0eef9..91d5549 100644 --- a/public/testkit/dc-testkit.sh +++ b/public/testkit/dc-testkit.sh @@ -15,14 +15,26 @@ # war. Ein Weg, den alle nehmen, ist der einzige, dessen Fehler man findet. # # Abhaengigkeiten: curl und python3. Beides ist auf der VM vorhanden; jq wird -# bewusst NICHT vorausgesetzt, weil es dort nicht installiert ist und eine -# Installation nur fuer das Lesen von JSON die Maschine fuer alle veraendert. +# bewusst NICHT vorausgesetzt. Inzwischen ist es dort zwar installiert - aber +# das Skript soll auf einer frisch aufgesetzten Maschine sofort laufen, ohne +# dass jemand sie erst fuer alle veraendert. Was vorausgesetzt wird, muss +# ueberall gelten, nicht nur auf der einen Maschine, die man vor sich hat. # # Vollstaendige Anleitung: docs/TESTUMGEBUNG.md im Deploymentcenter-Repo. set -euo pipefail -DC_TESTKIT_VERSION="1.0.0" +# Fassung dieses Skripts. MUSS bei jeder Aenderung hochgezaehlt werden. +# +# Warum das eine Regel und keine Empfehlung ist: Das Skript liegt auf jeder +# Testmaschine als Kopie. Bleibt die Zahl stehen, meldet eine Maschine mit dem +# Stand von vorletzter Woche dieselbe Fassung wie eine frisch bezogene - und +# niemand kann erkennen, warum derselbe Aufruf hier scheitert und dort nicht. +# Genau so ist es passiert: doctor meldete auf einer Maschine "Bereit", waehrend +# die ausgelieferte Fassung die fehlenden Rechte des Tokens laengst angemeckert +# haette. Dieselbe Falle, in die vorher die Serverfassung getappt ist (2.0.0 in +# der Konfiguration, waehrend 2.7.0 lief). +DC_TESTKIT_VERSION="1.1.0" # --------------------------------------------------------------------------- # Konfiguration @@ -35,11 +47,36 @@ DC_TESTKIT_VERSION="1.0.0" CONFIG_FILE="${DC_TESTKIT_CONFIG:-$HOME/.config/dc-testkit/config}" +# Die Datei wird mit "." gelesen, ihre Zeilen sind also gewoehnliche +# Zuweisungen - und die ueberschrieben, was in der Umgebung stand. Damit war +# die Rangfolge oben genau verkehrt herum: "DC_PROJECT=x dc-testkit report" +# meldete stillschweigend in das Projekt aus der Datei. Auf einer Maschine mit +# mehreren Projekten ist das die teuerste Art von Fehler - es geht nichts +# kaputt, es landet nur alles am falschen Ort, und niemand sieht es. +# +# Deshalb: vorher merken, was in der Umgebung stand, hinterher wieder +# durchsetzen. Leere Werte zaehlen als "nicht gesetzt" - ein "DC_TOKEN=" in +# der Umgebung soll das Token aus der Datei nicht loeschen. +DC_KONFIGURIERBAR='DC_URL DC_TOKEN DC_PROJECT DC_CHANNEL DC_INSTANCE DC_DISPLAY + DC_PROJECT_DIR DC_MIN_SERVER DC_TESTKIT_URL DC_TESTKIT_STATE' + +for _dc_name in $DC_KONFIGURIERBAR; do + printf -v "_dcenv_$_dc_name" '%s' "${!_dc_name-}" +done + if [ -f "$CONFIG_FILE" ]; then # shellcheck disable=SC1090 . "$CONFIG_FILE" fi +for _dc_name in $DC_KONFIGURIERBAR; do + _dc_gemerkt="_dcenv_$_dc_name" + if [ -n "${!_dc_gemerkt-}" ]; then + printf -v "$_dc_name" '%s' "${!_dc_gemerkt}" + fi +done +unset _dc_name _dc_gemerkt + DC_URL="${DC_URL:-https://dc.mhdf.de}" DC_URL="${DC_URL%/}" DC_PROJECT="${DC_PROJECT:-}" @@ -53,11 +90,35 @@ DC_CHANNEL="${DC_CHANNEL:-prod}" # doctor kannte sie nicht und meldete "Bereit" auf einem zu alten Server. DC_MIN_SERVER="${DC_MIN_SERVER:-2.6.1}" +# Bezugsquelle dieses Skripts. Daraus zieht doctor die ausgelieferte Fassung +# und selfupdate den neuen Stand. Die Adresse haengt am DC_URL der Instanz: +# Wer gegen die Testinstanz arbeitet, soll auch deren Testkit bekommen. +DC_TESTKIT_URL="${DC_TESTKIT_URL:-$DC_URL/testkit/dc-testkit.sh}" + # Die Maschinenkennung entscheidet, welchen DEV-Schluessel der Server # heraussucht. Sie muss ueber Laeufe hinweg gleich bleiben - sonst sammelt sich # je Lauf ein weiterer Schluessel an, bis die Grenze je Projekt greift. DC_INSTANCE="${DC_INSTANCE:-$(hostname -s 2>/dev/null || hostname)}" +# Virtuelles Display dieses Projekts, fest vergeben (TESTUMGEBUNG.md 5.5). +# Ohne Vorgabe: keines - dann bleibt die Oberflaechenpruefung ausgeschaltet, +# statt sich eine Nummer zu nehmen. Genau das macht "xvfb-run -a", und es +# beginnt bei :99, also auf der Nummer eines anderen Projekts. +DC_DISPLAY="${DC_DISPLAY:-}" + +# Verzeichnis des Projekt-Repos auf dieser Maschine. Ist es gesetzt und steht +# kein DC_TOKEN fest, holt sich das Testkit das Werkbank-Token von dort - aus +# der .mcp.json, in der es fuer den MCP-Adapter ohnehin schon liegt. +# +# Das ist der Ablageort, den die Projekte bereits benutzen: eine Datei je +# Projekt, per .gitignore ausgeschlossen, mit dem Token in env.DC_TOKEN. Es ein +# zweites Mal in ~/.config/dc-testkit zu hinterlegen hiesse, dasselbe Geheimnis +# an zwei Orten zu pflegen - und beim naechsten Wechsel einen davon zu +# vergessen. Genau das war auf der Test-VM passiert: In der Testkit-Konfiguration +# lag noch ein altes Laufzeit-Token, waehrend das richtige Werkbank-Token im +# Projekt langst ein anderes war. +DC_PROJECT_DIR="${DC_PROJECT_DIR:-}" + STATE_ROOT="${DC_TESTKIT_STATE:-$HOME/.local/state/dc-testkit}" # --------------------------------------------------------------------------- @@ -75,6 +136,13 @@ note() { printf '%s\n' "$*"; } step() { printf '%s==>%s %s\n' "$C_BLUE" "$C_RESET" "$*"; } ok() { printf '%s ok%s %s\n' "$C_GREEN" "$C_RESET" "$*"; } warn() { printf '%swarn%s %s\n' "$C_YELLOW" "$C_RESET" "$*" >&2; } + +# Wie warn, aber auf stdout. Fuer doctor: Sein Bericht ist das Ergebnis des +# Befehls, kein Fehlerkanal - das Ergebnis traegt der Rueckgabewert. Ueber SSH +# werden stdout und stderr getrennt uebertragen und beim Aufrufer verschraenkt; +# die Befunde standen dann unter der falschen Ueberschrift. Genau so liest ein +# Pruefagent den Bericht - und ordnet die Zeile dem falschen Schritt zu. +befund() { printf '%swarn%s %s\n' "$C_YELLOW" "$C_RESET" "$*"; } dim() { printf '%s%s%s\n' "$C_DIM" "$*" "$C_RESET"; } die() { @@ -220,8 +288,83 @@ require_project() { [ -n "$DC_PROJECT" ] || die "Kein Projekt gesetzt. DC_PROJECT= oder --project ." } +# Liest env.DC_TOKEN aus der .mcp.json eines Projektverzeichnisses. +# +# Gesucht wird an den ueblichen Stellen: im Verzeichnis selbst und eine Ebene +# tiefer - Projekte legen die Datei mal neben die .sln, mal in den Unterordner +# mit dem gleichnamigen Projekt. Ein Platzhalter wie ${DC_TOKEN} zaehlt nicht +# als Token; sonst laeuft der Aufruf mit einer Zeichenkette los, die der Server +# nur mit 401 beantworten kann. +token_aus_projekt() { + local wurzel="$1" datei + + for datei in "$wurzel/.mcp.json" "$wurzel"/*/.mcp.json; do + [ -f "$datei" ] || continue + + python3 -c ' +import json, re, sys + +try: + with open(sys.argv[1], encoding="utf-8-sig") as fh: + daten = json.load(fh) +except Exception: + sys.exit(1) + +for server in (daten.get("mcpServers") or {}).values(): + tok = (server.get("env") or {}).get("DC_TOKEN") + if isinstance(tok, str) and tok and not re.search(r"[$%<]", tok): + print(tok) + sys.exit(0) + +sys.exit(1) +' "$datei" && return 0 + done + + return 1 +} + +# Wird einmal in main() aufgerufen, bevor irgendein Befehl laeuft - damit auch +# doctor dasselbe Token sieht wie jeder andere Aufruf. Setzt DC_TOKEN_QUELLE +# fuer die Auskunft: Wo etwas herkommt, gehoert zu einem Bericht dazu. +DC_TOKEN_QUELLE='' + +token_aufloesen() { + if [ -n "$DC_TOKEN" ]; then + DC_TOKEN_QUELLE="Konfiguration/Umgebung" + return 0 + fi + + [ -n "$DC_PROJECT_DIR" ] || return 0 + + DC_TOKEN="$(token_aus_projekt "$DC_PROJECT_DIR" || true)" + [ -n "$DC_TOKEN" ] && DC_TOKEN_QUELLE="$DC_PROJECT_DIR/.mcp.json" + + return 0 +} + require_token() { - [ -n "$DC_TOKEN" ] || die "Kein Token gesetzt. DC_TOKEN in $CONFIG_FILE hinterlegen." + [ -n "$DC_TOKEN" ] && return 0 + + # Die Meldung nennt beide Wege und sagt ausdruecklich, warum die Datei + # fehlen KANN, ohne dass etwas kaputt ist. Ohne diesen Satz sucht der + # Naechste den Fehler im Repo, im Netz oder in seinen Rechten - und findet + # keinen, weil es keinen gibt. + printf '%sFehler%s Kein Werkbank-Token.\n\n' "$C_RED" "$C_RESET" >&2 + + if [ -n "$DC_PROJECT_DIR" ]; then + printf 'In %s liegt keine .mcp.json mit einem DC_TOKEN.\n\n' "$DC_PROJECT_DIR" >&2 + printf 'Das ist kein Fehler im Repo: .mcp.json ist per .gitignore ausgeschlossen\n' >&2 + printf 'und wird ueber Git NICHT mitgeliefert. Auf einer frisch geklonten\n' >&2 + printf 'Maschine fehlt sie deshalb immer.\n\n' >&2 + printf 'Das Dev-Agent-Token dieses Projekts muss von Hand hierher gebracht\n' >&2 + printf 'werden - es steht in der .mcp.json des Projekts auf dem Arbeitsplatz.\n' >&2 + else + printf 'Zwei Wege, einer genuegt:\n\n' >&2 + printf ' DC_PROJECT_DIR= - Token aus dessen .mcp.json\n' >&2 + printf ' DC_TOKEN= in %s\n' "$CONFIG_FILE" >&2 + fi + + exit 1 } state_dir() { @@ -294,6 +437,285 @@ has_scope() { return 1 } +# --------------------------------------------------------------------------- +# Der eigene Stand +# +# Das Skript liegt auf jeder Testmaschine als Kopie und altert dort still vor +# sich hin. Genau daran ist die Rechtepruefung in doctor einmal gescheitert: +# Sie war laengst geschrieben, aber auf der Maschine lag der Stand von davor - +# und weil beide dieselbe Fassung meldeten, sah es niemand. +# --------------------------------------------------------------------------- + +# Wo liegt dieses Skript wirklich? readlink loest einen Verweis in ~/bin auf - +# sonst ersetzte selfupdate den Verweis und nicht die Datei dahinter. +own_path() { + local p="$0" + + if command -v readlink >/dev/null 2>&1 && readlink -f "$p" >/dev/null 2>&1; then + readlink -f "$p" + return 0 + fi + + case "$p" in + /*) printf '%s\n' "$p" ;; + *) printf '%s/%s\n' "$(pwd)" "$p" ;; + esac +} + +# Fassung aus einer Skriptdatei lesen - der eigenen oder einer bezogenen. +testkit_version_of() { + sed -n 's/^DC_TESTKIT_VERSION="\([^"]*\)".*/\1/p' "$1" | head -n 1 +} + +# Holt das ausgelieferte Skript nach $1. +# 0 = da und plausibel, 1 = Bezug fehlgeschlagen, 2 = das ist kein Testkit +# +# Der Plausibilitaetstest ist nicht ueberflaessig: Faellt die Adresse aus oder +# steht dort eine Anmeldeseite, schriebe der Bezug eine HTML-Seite in die +# Datei. Danach laesst sich nichts mehr aufrufen - auch nicht das selfupdate, +# das den Schaden beheben koennte. +fetch_testkit() { + local ziel="$1" + + curl -fsSL --max-time 60 "$DC_TESTKIT_URL" -o "$ziel" 2>/dev/null || return 1 + + head -n 1 "$ziel" | grep -q '^#!' || return 2 + grep -q '^DC_TESTKIT_VERSION=' "$ziel" || return 2 + + return 0 +} + +cmd_selfupdate() { + local pruefen=0 erzwingen=0 + + while [ $# -gt 0 ]; do + case "$1" in + --pruefen|--check) pruefen=1 ;; + --force) erzwingen=1 ;; + *) die "Unbekannter Schalter: $1" ;; + esac + shift + done + + local ziel tmp neu rc=0 + ziel="$(own_path)" + + step "Bezug von $DC_TESTKIT_URL" + + # Die Zwischendatei muss neben dem Ziel liegen: nur im selben Dateisystem + # ist das abschliessende mv ein Umbenennen und damit unteilbar. Ueber eine + # Dateisystemgrenze hinweg kopiert mv Byte fuer Byte - und wer das Skript + # in genau dem Moment aufruft, bekommt eine halbe Datei. + tmp="$ziel.neu.$$" + + fetch_testkit "$tmp" || rc=$? + case $rc in + 1) rm -f "$tmp"; die "Bezug fehlgeschlagen - erreichbar? $DC_TESTKIT_URL" ;; + 2) rm -f "$tmp"; die "Dort liegt kein Testkit. Nichts veraendert." ;; + esac + + neu="$(testkit_version_of "$tmp")" + + if cmp -s "$tmp" "$ziel"; then + rm -f "$tmp" + ok "bereits aktuell (${neu:-?})" + return 0 + fi + + # In welche Richtung geht der Unterschied? Davon haengt alles Weitere ab - + # der Rat genauso wie die Frage, ob ueberhaupt ersetzt werden darf. Eine + # aeltere Fassung ueber eine neuere zu ziehen ist fast immer ein Versehen: + # typisch der Fall, dass jemand im Repo am Skript arbeitet und dem Rat + # "beheben mit selfupdate" folgt. Rueckgaengig machen laesst sich das + # nicht, wenn die Aenderung noch nirgends sonst liegt. + local abstieg=0 + version_lt "${neu:-0}" "$DC_TESTKIT_VERSION" || abstieg=$? + + local gleiche_nummer=0 + [ "$DC_TESTKIT_VERSION" = "${neu:-}" ] && gleiche_nummer=1 + + if [ "$pruefen" = '1' ]; then + rm -f "$tmp" + + if [ "$gleiche_nummer" = '1' ]; then + warn "gleiche Fassung ($DC_TESTKIT_VERSION), aber anderer Inhalt" + note " Entweder ist diese Kopie von Hand geaendert worden, oder beim" + note " Ausliefern wurde DC_TESTKIT_VERSION nicht hochgezaehlt." + return 1 + fi + + if [ "$abstieg" = '0' ]; then + ok "neuer als ausgeliefert ($DC_TESTKIT_VERSION > ${neu:-?}) - Entwicklungsstand" + note " Kein selfupdate noetig; es holte die aeltere Fassung zurueck." + return 0 + fi + + warn "veraltet: hier laeuft $DC_TESTKIT_VERSION, ausgeliefert wird ${neu:-?}" + note " Beziehen mit: dc-testkit selfupdate" + return 1 + fi + + if [ "$erzwingen" != '1' ]; then + if [ "$gleiche_nummer" = '1' ]; then + rm -f "$tmp" + warn "Gleiche Fassung ($DC_TESTKIT_VERSION), aber anderer Inhalt." + die "Welche der beiden neuer ist, sagt hier nichts. Wenn du es weisst: --force" + fi + + if [ "$abstieg" = '0' ]; then + rm -f "$tmp" + warn "Ausgeliefert wird ${neu:-?} - hier laeuft bereits $DC_TESTKIT_VERSION." + die "Das waere ein Rueckschritt. Wenn das gewollt ist: --force" + fi + fi + + if [ ! -w "$ziel" ]; then + rm -f "$tmp" + die "Nicht beschreibbar: $ziel" + fi + + chmod +x "$tmp" 2>/dev/null || true + + # Eine Sicherung, bevor etwas ersetzt wird. Der Rueckweg muss ohne Netz + # gehen: Wer sich gerade eine kaputte Fassung geholt hat, kann sie nicht + # noch einmal herunterladen, um das rueckgaengig zu machen. + cp -p "$ziel" "$ziel.vorher" 2>/dev/null || true + + mv "$tmp" "$ziel" + + ok "$DC_TESTKIT_VERSION -> ${neu:-?}" + dim " Vorherige Fassung: $ziel.vorher" +} + +# --------------------------------------------------------------------------- +# Oberflaechenpruefung +# +# Der generische Teil des Verfahrens aus TESTUMGEBUNG.md 5.8: ein eigenes +# Display starten, aufnehmen, wieder abraeumen. Was dazwischen geklickt wird, +# bleibt projektspezifisch und gehoert nicht hierher. +# +# Warum ueberhaupt gekapselt: Drei Fehler wiederholen sich sonst in jedem +# Eigenbau-Skript - DISPLAY nicht gesetzt (dann landet alles auf :0, dem +# echten Desktop), "pkill Xvfb" ohne Nummer (das erschlaegt die Sitzungen der +# anderen Projekte), und Dateinamen ohne Projektpraefix (zwei Laeufe +# ueberschreiben sich gegenseitig die Aufnahmen). +# --------------------------------------------------------------------------- + +require_display() { + [ -n "$DC_DISPLAY" ] \ + || die "Kein Display gesetzt. DC_DISPLAY=: in $CONFIG_FILE - die Nummer steht in TESTUMGEBUNG.md 7." + + case "$DC_DISPLAY" in + :0|:1) + die "$DC_DISPLAY ist der echte Desktop bzw. der Anmeldebildschirm. Nimm die Nummer deines Projekts." + ;; + :[0-9]*) ;; + *) die "DC_DISPLAY muss die Form : haben, nicht \"$DC_DISPLAY\"." ;; + esac +} + +display_nr() { + printf '%s' "${DC_DISPLAY#:}" +} + +# Laeuft auf diesem Display ein Xvfb? 0 = ja. +display_laeuft() { + pgrep -f "Xvfb $DC_DISPLAY" >/dev/null 2>&1 +} + +# Gibt es den Socket, aber keinen Prozess? Dann ist die Nummer scheinbar +# belegt und in Wahrheit frei - das Ueberbleibsel eines hart beendeten Xvfb. +display_verwaist() { + [ -e "/tmp/.X11-unix/X$(display_nr)" ] && ! display_laeuft +} + +cmd_display() { + local aktion="${1:-status}" + require_display + + case "$aktion" in + status) + step "Display $DC_DISPLAY" + if display_laeuft; then + ok "belegt - $(pgrep -af "Xvfb $DC_DISPLAY" | head -1)" + elif display_verwaist; then + befund "verwaister Socket /tmp/.X11-unix/X$(display_nr) - kein Prozess dazu" + note " Aufraeumen: sudo rm -f /tmp/.X11-unix/X$(display_nr)" + return 1 + else + ok "frei" + fi + + step "Belegt insgesamt" + ls /tmp/.X11-unix/ 2>/dev/null | tr '\n' ' ' | sed 's/^/ Sockets: /' + printf '\n' + pgrep -af Xvfb 2>/dev/null | sed 's/^/ /' || note " kein Xvfb" + ;; + + start) + require_project + local geometrie="${2:-1600x1000x24}" + + if display_laeuft; then + ok "Display $DC_DISPLAY laeuft bereits" + return 0 + fi + + if display_verwaist; then + die "Socket /tmp/.X11-unix/X$(display_nr) ohne Prozess. Erst aufraeumen (sudo rm -f), sonst startet Xvfb nicht." + fi + + command -v Xvfb >/dev/null 2>&1 || die "Xvfb ist nicht installiert." + + step "Display $DC_DISPLAY starten ($geometrie)" + Xvfb "$DC_DISPLAY" -screen 0 "$geometrie" >/dev/null 2>&1 & + sleep 2 + + display_laeuft || die "Xvfb ist nicht hochgekommen. Ist $DC_DISPLAY anderweitig belegt?" + ok "laeuft - in eigenen Aufrufen: export DISPLAY=$DC_DISPLAY" + ;; + + stop) + if ! display_laeuft; then + ok "Display $DC_DISPLAY laeuft nicht" + return 0 + fi + + # Mit Nummer, nie ohne: "pkill Xvfb" beendet die Sitzungen aller + # anderen Projekte auf dieser Maschine gleich mit. + step "Display $DC_DISPLAY beenden" + pkill -f "Xvfb $DC_DISPLAY" 2>/dev/null || true + sleep 1 + display_laeuft && die "Xvfb auf $DC_DISPLAY laeuft weiter." + ok "beendet" + ;; + + *) + die "Aufruf: dc-testkit display [status|start [BxHxT]|stop]" + ;; + esac +} + +cmd_shot() { + require_project + require_display + + local name="${1:-}" + [ -n "$name" ] || die "Aufruf: dc-testkit shot (z.B. dc-testkit shot 1_start)" + + display_laeuft || die "Auf $DC_DISPLAY laeuft kein Xvfb. Erst: dc-testkit display start" + command -v import >/dev/null 2>&1 || die "ImageMagick fehlt (import)." + + # Projektpraefix ist Pflicht, nicht Geschmack: /tmp gehoert allen, und + # /tmp/1_start.png ueberschreibt die Aufnahme des Nachbarprojekts. + local ziel="/tmp/${DC_PROJECT}_${name}.png" + + DISPLAY="$DC_DISPLAY" import -window root "$ziel" \ + || die "Aufnahme fehlgeschlagen. Zeichnet auf $DC_DISPLAY ueberhaupt etwas?" + + ok "$ziel" +} + # --------------------------------------------------------------------------- # doctor - laeuft hier ueberhaupt etwas? # @@ -309,7 +731,7 @@ cmd_doctor() { if command -v "$tool" >/dev/null 2>&1; then ok "$tool $(command -v "$tool")" else - warn "$tool fehlt - ohne das geht nichts" + befund "$tool fehlt - ohne das geht nichts" failed=1 fi done @@ -326,18 +748,24 @@ cmd_doctor() { # Server wirklich braucht, scheiterte danach mit "kennt # /api/license/v1/dev-key noch nicht". if [ -z "$server_version" ]; then - warn "der Server nennt keine Fassung - $DC_MIN_SERVER nicht pruefbar" + befund "der Server nennt keine Fassung - $DC_MIN_SERVER nicht pruefbar" else - version_lt "$server_version" "$DC_MIN_SERVER" - case $? in + # Der Rueckgabewert MUSS mit || aufgefangen werden. Ohne das + # beendet "set -e" das Skript, sobald version_lt etwas anderes als + # 0 liefert - also ausgerechnet im Normalfall "Server ist neu + # genug" (Rueckgabewert 1). doctor stieg dann wortlos nach der + # Health-Pruefung aus, ohne Token und Projekt je anzusehen. + local vergleich=0 + version_lt "$server_version" "$DC_MIN_SERVER" || vergleich=$? + case $vergleich in 0) - warn "zu alt: $server_version - gebraucht wird mindestens $DC_MIN_SERVER" - warn " Es fehlen: dc-testkit license (dev-key), der Bezug ueber /testkit/" - warn " und die Rechtepruefung. Der Server muss deployt werden." + befund "zu alt: $server_version - gebraucht wird mindestens $DC_MIN_SERVER" + befund " Es fehlen: dc-testkit license (dev-key), der Bezug ueber /testkit/" + befund " und die Rechtepruefung. Der Server muss deployt werden." failed=1 ;; 2) - warn "Fassung \"$server_version\" ist nicht auswertbar - $DC_MIN_SERVER ungeprueft" + befund "Fassung \"$server_version\" ist nicht auswertbar - $DC_MIN_SERVER ungeprueft" ;; *) ok "erfuellt die Mindestfassung $DC_MIN_SERVER" @@ -345,13 +773,75 @@ cmd_doctor() { esac fi else - warn "nicht erreichbar: $(api_error)" + befund "nicht erreichbar: $(api_error)" failed=1 fi + # Der eigene Stand. Diese Pruefung gehoert VOR alles Weitere: Meldet ein + # veraltetes Testkit "Bereit", ist jede folgende Zeile dieses Berichts + # unter Vorbehalt - sie stammt aus einer Fassung, die niemand mehr liest. + step "Dieses Testkit" + dim " Fassung $DC_TESTKIT_VERSION - $(own_path)" + + local tk_tmp tk_rc=0 tk_neu + tk_tmp="${TMPDIR:-/tmp}/dc-testkit.pruefung.$$" + + fetch_testkit "$tk_tmp" || tk_rc=$? + if [ "$tk_rc" = '0' ]; then + tk_neu="$(testkit_version_of "$tk_tmp")" + if cmp -s "$tk_tmp" "$(own_path)"; then + ok "aktuell - entspricht der ausgelieferten Fassung ${tk_neu:-?}" + else + # Drei verschiedene Lagen, und nur eine davon ruft nach selfupdate. + # Sie zu verwechseln waere teuer: Wer im Repo am Skript arbeitet, + # haette auf den Rat "beheben mit selfupdate" hin seine eigene, + # noch nicht ausgelieferte Arbeit ueberschrieben. + local tk_cmp=0 + version_lt "$DC_TESTKIT_VERSION" "${tk_neu:-0}" || tk_cmp=$? + case $tk_cmp in + 0) + befund "veraltet: hier laeuft $DC_TESTKIT_VERSION, ausgeliefert wird ${tk_neu:-?}" + befund " Beheben mit: dc-testkit selfupdate" + failed=1 + ;; + 1) + if [ "$DC_TESTKIT_VERSION" = "${tk_neu:-}" ]; then + # Der stillste Fall und deshalb der gefaehrlichste: + # gleiche Nummer, anderer Inhalt. Genau daran ist die + # Rechtepruefung einmal unbemerkt vorbeigelaufen. + befund "gleiche Fassung ($DC_TESTKIT_VERSION), aber anderer Inhalt" + befund " Entweder ist diese Kopie von Hand geaendert worden, oder beim" + befund " Ausliefern wurde DC_TESTKIT_VERSION nicht hochgezaehlt." + failed=1 + else + ok "neuer als ausgeliefert ($DC_TESTKIT_VERSION > ${tk_neu:-?}) - Entwicklungsstand" + dim " Kein selfupdate: das holte die aeltere Fassung zurueck." + fi + ;; + *) + befund "Fassungen nicht vergleichbar: $DC_TESTKIT_VERSION / ${tk_neu:-?}" + ;; + esac + fi + else + befund "ausgelieferte Fassung nicht abrufbar - der eigene Stand bleibt ungeprueft" + dim " $DC_TESTKIT_URL" + fi + rm -f "$tk_tmp" + step "Token" if [ -z "$DC_TOKEN" ]; then - warn "kein Token gesetzt (DC_TOKEN)" + befund "kein Werkbank-Token" + if [ -n "$DC_PROJECT_DIR" ]; then + befund " In $DC_PROJECT_DIR liegt keine .mcp.json mit DC_TOKEN." + befund " Das ist kein Fehler im Repo: .mcp.json ist per .gitignore" + befund " ausgeschlossen und kommt ueber Git nicht mit. Auf einer frisch" + befund " geklonten Maschine fehlt sie deshalb immer - das Token muss von" + befund " Hand hierher, aus der .mcp.json des Projekts auf dem Arbeitsplatz." + else + befund " Entweder DC_PROJECT_DIR auf das Projekt-Repo zeigen lassen" + befund " (Token aus dessen .mcp.json) oder DC_TOKEN in $CONFIG_FILE." + fi failed=1 elif api GET /api/tokens/v1/whoami; then # Fruehere Fassungen meldeten hier nur "gueltig - der Bugtracker @@ -367,23 +857,39 @@ cmd_doctor() { ok "gueltig - \"$tok_name\", Besitzerart $tok_owner, Projekt ${tok_project:-keins}" dim " Rechte: $tok_scopes" + dim " Herkunft: ${DC_TOKEN_QUELLE:-unbekannt}" if [ "$tok_workbench" != 'true' ]; then - warn "kein Werkbank-Token: Besitzerart ist $tok_owner, gebraucht wird dev_agent" - warn " Ein Laufzeit-Token darf nicht in fremde Projekte melden (TESTUMGEBUNG.md 4.2)." + befund "kein Werkbank-Token: Besitzerart ist $tok_owner, gebraucht wird dev_agent" + befund " Ein Laufzeit-Token darf nicht in fremde Projekte melden (TESTUMGEBUNG.md 4.2)." failed=1 fi for want in bugtracker:manage license:dev updateservice:read; do if ! has_scope "$tok_scopes" "$want"; then - warn "Recht fehlt: $want" + befund "Recht fehlt: $want" failed=1 fi done - if [ -n "$tok_project" ] && [ -n "$DC_PROJECT" ] && [ "$tok_project" != "$DC_PROJECT" ]; then - warn "Token gehoert zu $tok_project, gearbeitet wird an $DC_PROJECT" - warn " Je Projekt ein eigenes Token - sonst ist die Herkunft einer Meldung nicht mehr ablesbar." + # Ein Token ohne Projektbindung rutschte hier bisher durch: geprueft + # wurde nur, ob eine vorhandene Bindung zum Projekt passt. Genau so ein + # Token lag auf der Test-VM - dev_agent, aber project_slug leer. + # + # Ohne Bindung ist das Token nicht eingeschraenkt, sondern + # uneingeschraenkt: Der Server leitet kein Heimatprojekt ab, also ist + # fuer ihn jede Meldung eine Heimmeldung. Damit faellt der ganze Schutz + # fuer Fremdmeldungen weg - keine Begruendungspflicht, keine Deckelung + # auf medium, kein Eingang in triage_external, kein reported_from. + if [ -z "$tok_project" ]; then + befund "Token ohne Projektbindung (project_slug ist leer)" + befund " Damit zaehlt JEDE Meldung als Heimmeldung: ohne Begruendungspflicht," + befund " ohne Deckelung auf medium, direkt in die offene Liste des fremden" + befund " Projekts - und ohne nachweisbare Herkunft. Im WebUI nachtragen." + failed=1 + elif [ -n "$DC_PROJECT" ] && [ "$tok_project" != "$DC_PROJECT" ]; then + befund "Token gehoert zu $tok_project, gearbeitet wird an $DC_PROJECT" + befund " Je Projekt ein eigenes Token - sonst ist die Herkunft einer Meldung nicht mehr ablesbar." failed=1 fi elif [ "$API_STATUS" = '404' ]; then @@ -391,38 +897,83 @@ cmd_doctor() { # der ausdrueckliche Hinweis, dass die Rechte ungeprueft sind. Still # "gueltig" zu melden waere genau der Fehler, der hier behoben wird. if api GET /api/bugtracker/v1/projects; then - warn "gueltig, aber die Rechte sind ungeprueft" - warn " /api/tokens/v1/whoami gibt es erst ab $DC_MIN_SERVER." + befund "gueltig, aber die Rechte sind ungeprueft" + befund " /api/tokens/v1/whoami gibt es erst ab $DC_MIN_SERVER." else - warn "Token wird nicht angenommen: $(api_error)" + befund "Token wird nicht angenommen: $(api_error)" failed=1 fi else - warn "Token wird nicht angenommen: $(api_error)" + befund "Token wird nicht angenommen: $(api_error)" failed=1 fi step "Projekt" if [ -z "$DC_PROJECT" ]; then - warn "kein Projekt gesetzt (DC_PROJECT)" + befund "kein Projekt gesetzt (DC_PROJECT)" failed=1 elif [ -z "$DC_TOKEN" ]; then # Ohne Token ist die Projektliste nicht abrufbar. Das stillschweigend # zu ueberspringen liesse den Eindruck entstehen, das Projekt sei # geprueft worden. - warn "$DC_PROJECT nicht pruefbar - dafuer wird ein Token gebraucht" + befund "$DC_PROJECT nicht pruefbar - dafuer wird ein Token gebraucht" elif api GET /api/bugtracker/v1/projects; then if printf '%s' "$API_BODY" | grep -q "\"$DC_PROJECT\""; then ok "$DC_PROJECT ist im Deploymentcenter bekannt" else - warn "$DC_PROJECT kommt in der Projektliste nicht vor" + befund "$DC_PROJECT kommt in der Projektliste nicht vor" failed=1 fi else - warn "Projektliste nicht abrufbar: $(api_error)" + befund "Projektliste nicht abrufbar: $(api_error)" failed=1 fi + # Die Oberflaechenpruefung ist der zweite Grund, warum es diese Maschine + # gibt (5.8) - doctor sah sie bisher gar nicht an. Wer sie braucht, merkte + # das Fehlen erst mitten im Lauf, mit einem "XOpenDisplay failed" statt + # einer Auskunft. + step "Oberflaechenpruefung" + local gui_fehlt='' + for tool in Xvfb xdotool import convert; do + command -v "$tool" >/dev/null 2>&1 || gui_fehlt="$gui_fehlt $tool" + done + + if [ -n "$gui_fehlt" ]; then + # Kein failed=1: Ein Projekt ohne Oberflaeche braucht davon nichts. + # Ein Befund waere hier eine Falschmeldung - aber schweigen darf das + # Werkzeug auch nicht. + note " nicht moeglich, es fehlt:$gui_fehlt" + note " Nur noetig, wenn dieses Projekt eine Oberflaeche hat." + else + ok "Xvfb, xdotool, ImageMagick vorhanden" + fi + + if [ -z "$DC_DISPLAY" ]; then + dim " Kein DC_DISPLAY gesetzt - display/shot bleiben aus." + dim " Die Nummer dieses Projekts steht in TESTUMGEBUNG.md 7." + else + case "$DC_DISPLAY" in + :0|:1) + befund "DC_DISPLAY=$DC_DISPLAY ist der echte Desktop bzw. der Anmeldebildschirm" + befund " Darauf zu zeichnen stoert den Menschen, der dort arbeitet." + failed=1 + ;; + *) + if display_laeuft; then + ok "Display $DC_DISPLAY - belegt (vermutlich von einem eigenen Lauf)" + elif display_verwaist; then + befund "Display $DC_DISPLAY - verwaister Socket, kein Prozess dazu" + befund " Die Nummer wirkt belegt und ist frei. Aufraeumen:" + befund " sudo rm -f /tmp/.X11-unix/X${DC_DISPLAY#:}" + failed=1 + else + ok "Display $DC_DISPLAY - frei" + fi + ;; + esac + fi + step "Arbeitsplatz" if [ -n "$DC_PROJECT" ]; then dim " Zustand: $(state_dir)" @@ -1254,7 +1805,8 @@ dc-testkit $DC_TESTKIT_VERSION - Testlaeufe gegen das Deploymentcenter dc-testkit [--project ] [--channel ] [--url ] Einrichten und pruefen - doctor Werkzeuge, Netz, Token und Projekt pruefen + doctor Werkzeuge, Netz, eigener Stand, Token und Projekt + selfupdate [--pruefen] Dieses Skript auf die ausgelieferte Fassung bringen license [--ttl N] [--print] [--revoke] DEV-Lizenzschluessel holen, verlaengern, zurueckgeben agent [--force] update-agent herunterladen, Pruefsumme abgleichen @@ -1281,6 +1833,10 @@ Bugtracker confirm [notiz] "der Fix eines anderen traegt" -> schliesst unclaim [grund] Uebernahme zurueckgeben +Oberflaeche pruefen (braucht DC_DISPLAY) + display [status|start|stop] Eigenes virtuelles Display + shot Aufnahme nach /tmp/_.png + Aufraeumen cleanup [--sandbox] Sandkasten weg, Schluessel zurueckgeben @@ -1288,7 +1844,9 @@ Schweregrade: idea, wishlist, low, medium, high, critical Typen: bug, feature ("idea" ist ein Schweregrad, kein Typ) Konfiguration: $CONFIG_FILE - DC_URL, DC_TOKEN, DC_PROJECT, DC_CHANNEL, DC_INSTANCE + DC_URL, DC_TOKEN, DC_PROJECT, DC_CHANNEL, DC_INSTANCE, DC_DISPLAY + DC_PROJECT_DIR - Projekt-Repo; ohne DC_TOKEN wird es aus dessen + .mcp.json gelesen (env.DC_TOKEN) EOF } @@ -1308,8 +1866,13 @@ main() { local command="${1:-}" [ $# -gt 0 ] && shift + token_aufloesen + case "$command" in doctor) cmd_doctor "$@" ;; + selfupdate) cmd_selfupdate "$@" ;; + display) cmd_display "$@" ;; + shot) cmd_shot "$@" ;; license) cmd_license "$@" ;; agent) cmd_agent "$@" ;; check) cmd_check "$@" ;;