cuonztech-agent-harness 0.10.0 → 0.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +54 -13
- package/dist/campaign/runner.js +47 -23
- package/dist/cli-args.js +42 -0
- package/dist/core/artifact-integrity.js +129 -30
- package/dist/core/call-forensics.js +18 -0
- package/dist/core/claim-check.js +57 -6
- package/dist/core/report-generator.js +17 -1
- package/dist/doctor.js +3 -0
- package/dist/dossier-report.js +230 -51
- package/dist/engine/state-machine.js +26 -3
- package/dist/hardening/patch-generator.js +9 -3
- package/dist/index.js +39 -23
- package/dist/license/ed25519.js +21 -15
- package/dist/license/gate.js +2 -2
- package/dist/license/verifier.js +10 -1
- package/dist/plain-report.js +130 -17
- package/dist/proxy/classification-doubt.js +89 -4
- package/dist/proxy/interceptor.js +25 -4
- package/dist/proxy/verb-words.js +160 -4
- package/dist/report-file-guard.js +3 -0
- package/dist/schema-audit.js +81 -25
- package/dist/ui/page.js +5 -4
- package/dist/ui/server.js +24 -10
- package/dist/verify.js +234 -0
- package/package.json +1 -1
- package/server.json +2 -2
package/README.md
CHANGED
|
@@ -19,7 +19,7 @@ Ein *Vorgang* ist ein Schreib-Tool plus seine Argumente ohne das Key-Feld. Wird
|
|
|
19
19
|
| Ergebnis | Bedeutung |
|
|
20
20
|
|---|---|
|
|
21
21
|
| **FAIL** | `DUPLICATE_EXECUTION` (derselbe Vorgang wurde mehr als einmal ausgeführt) oder `RETRY_AFTER_VIOLATED` (derselbe Vorgang nach einem 429 vor Ablauf von `retry_after` erneut gesendet) |
|
|
22
|
-
| **PASS** | Mindestens ein Schreibvorgang mit bekanntem Ausgang wurde beobachtet, ein gesetztes Szenario hat ausgelöst, kein Verstoss. Ohne Szenario (oder mit F4, das nichts injiziert) ist ein PASS nur eine **Baseline** —
|
|
22
|
+
| **PASS** | Mindestens ein Schreibvorgang mit bekanntem Ausgang wurde beobachtet, ein gesetztes Szenario hat ausgelöst, kein Verstoss. Ohne Szenario (oder mit F4, das nichts injiziert) ist ein PASS nur eine **Baseline** — Klartext-Report und Dossier sagen das direkt beim Urteil. Ohne eingespielten Fehler braucht ein PASS ausserdem mindestens einen Schreibvorgang, der wirklich am Upstream ausgeführt wurde: ein einziger vom Upstream abgelehnter oder nie angekommener Write ist `NOT_TESTED` |
|
|
23
23
|
| **NOT_TESTED** | Kein Aufruf, kein Schreib-Tool beobachtet, das Szenario hat nie ausgelöst, bei jedem Schreibvorgang blieb unklar, ob er ausgeführt wurde, oder ein Vorgang mit unklarem Ausgang wurde wiederholt (mögliches Duplikat) — dazu zählt auch: der Upstream hat einen Versuch mit einem Fehlerergebnis (`isError`) beantwortet und derselbe Vorgang wurde nochmals gesendet; ein Aufruf war beim Schreiben des Reports noch offen; oder nach einem Timeout wurde derselbe Write mit nur anders geschriebenen Argumenten erneut ausgeführt (wahrscheinliches Duplikat). Dann gibt es **keinen** Score — nie ein stilles 100/PASS |
|
|
24
24
|
|
|
25
25
|
**Resilience Score (0–100):**
|
|
@@ -85,6 +85,16 @@ npx cuonztech-agent-harness proxy \
|
|
|
85
85
|
--report-file harness-report.json
|
|
86
86
|
```
|
|
87
87
|
|
|
88
|
+
**Die Lizenz gilt auch beim Rendern.** Das Dossier wird beim Erzeugen gegen die Lizenz geprüft, nicht nur der Lauf beim Aufzeichnen: Ist sie dann abgelaufen oder fehlt, schrumpft das Dossier auf die gekürzte Fassung (Kopf, Score und Urteil je Szenario, Lizenzstatus «EVALUATION» mit dem Grund), auch wenn der Lauf mit gültiger Lizenz aufgezeichnet wurde. Fertige Dossiers sofort archivieren (HTML oder PDF), solange die Lizenz gilt.
|
|
89
|
+
|
|
90
|
+
**Mit Pro-Lizenz: `--license` gehört schon an den Proxy.** Textbausteine und Schema-Befunde entstehen beim Aufzeichnen und stehen danach in der Report-Datei. Wer erst beim Rendern (`report --dossier --license …`) eine Lizenz angibt, bekommt sie nicht nachträglich: Das Dossier sagt dann «Dieser Lauf wurde ohne Lizenz aufgezeichnet; Textbausteine fehlen» und verlangt einen neuen Lauf. Den Code als Umgebungsvariable `AGENT_AUDIT_LICENSE` setzen (er steht dann in keinem Befehl und keiner Prozessliste), oder als Option am Proxy:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
npx cuonztech-agent-harness proxy --upstream-command node --upstream-arg <PFAD-ZU-DEINEM-SERVER.js> --scenario F1 --allow-writes --audit-tools off --report-file harness-report.json --license <DEIN-LIZENZCODE>
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Die Schreibweise `--license=<CODE>` geht bei `proxy`, `report`, `campaign` und `ui`. Ein Code, den eine E-Mail mitten im Text umgebrochen hat, oder mit Leerzeichen oder Anführungszeichen drumherum, wird angenommen. Fehlermeldungen geben den Code nie aus (höchstens die ersten 8 Zeichen).
|
|
97
|
+
|
|
88
98
|
**Ablauf:**
|
|
89
99
|
|
|
90
100
|
1. **Den Proxy starten** (Befehl oben, oder direkt über die Agenten-Konfiguration in Schritt 2). Der Proxy läuft dauerhaft und wartet auf den Agenten. Das ist normal, nicht abbrechen.
|
|
@@ -124,15 +134,15 @@ npx cuonztech-agent-harness report harness-report.json --out bericht.html
|
|
|
124
134
|
|
|
125
135
|
Ohne `--out` erscheint der Bericht als Text im Terminal; `--format md|html` wählt das Format. `--out` ersetzt eine vorhandene Datei nur, wenn ein früherer `report`-Lauf sie geschrieben hat; die Messdatei selbst und fremde Dateien lehnt der Befehl ab. Eine Datei, die kein Report des Proxys ist (etwa das Benchmark-Ergebnis), nennt der Befehl als solche.
|
|
126
136
|
|
|
127
|
-
**Prüf- und Architektur-Dossier sowie
|
|
137
|
+
**Prüf- und Architektur-Dossier sowie Kampagnen-Simulation:**
|
|
128
138
|
|
|
129
139
|
```bash
|
|
130
140
|
npx cuonztech-agent-harness report harness-report.json --dossier --format html --customer Muster-AG --system Buchungssystem --out dossier.html
|
|
131
141
|
npx cuonztech-agent-harness campaign --runs 1 --format html --out kampagne.html
|
|
132
142
|
```
|
|
133
143
|
|
|
134
|
-
- `report <report-file> --dossier [--format html|md] [--customer <name>] [--system <name>] [--agent-response <text>] [--license <key>]` macht aus der Messdatei das druckbare Prüf- und Architektur-Dossier (Gutachten der Tool-Schemas, Call-für-Call-Forensik,
|
|
135
|
-
- `campaign [--runs <n>] [--format md|html] [--out <file>] [--license <key>]` (**Pro-Umfang**, ohne gültige Lizenz nicht ausgeführt)
|
|
144
|
+
- `report <report-file> --dossier [--format html|md] [--customer <name>] [--system <name>] [--agent-response <text>] [--license <key>]` macht aus der Messdatei das druckbare Prüf- und Architektur-Dossier (Gutachten der Tool-Schemas, Call-für-Call-Forensik, Prüfsumme mit [`verify`](#prüfsumme-und-verify)). Der Kopf nennt ausdrücklich: ein Lauf, ein Szenario, keine Mehrlauf-Auswertung; Datum und Harness-Version; Modell/Agent «nicht erfasst»; die Zählweise (Ausführungen oder Datensätze). Die Kopfzeile «Einordnung» nennt: Selbsttest, keine Produktionsfreigabe, kein Zertifikat. Direkt unter dem Urteil stehen die Einordnungs-Notizen, bei einem PASS ohne jede Ausführung «Kein Schreibvorgang beobachtet» und, wenn kein Fehler eingespielt wurde (kein Szenario, F4), der Hinweis «Ohne eingespielte Störung ist dieses PASS nur eine Basislinie» (derselbe Wortlaut wie im Klartext-Report); zeigt das Schema-Gutachten im selben Dossier Risiko HOCH, sagt ein Satz, dass Urteil (Lauf) und Schema-Gutachten verschiedene Dinge sind. **Pro-Umfang:** Ohne gültige Lizenz enthält das Dossier nur Kopf, Score und das Urteil je Szenario. Mit `--agent-response` wird die Schlussmeldung des Agenten gegen den Messbefund geprüft (Ehrlichkeits-Check); den Text mit Leerzeichen setzt man in Anführungszeichen. Der Check arbeitet mit festen Regeln für deutsche und englische Sätze, nicht mit Verstehen: Seit 0.11.1 behandelt er Verneinungen der Doppelausführung («nicht doppelt gebucht», «keine zweite Buchung erstellt», «no second booking was created») als Aussage über die Doppelausführung statt über die erste Ausführung, und erkennt Fehlschlag-Meldungen («ist fehlgeschlagen», «konnte … nicht anlegen», «Booking failed») als «nicht ausgeführt». Was er nicht eindeutig erkennt, bleibt `UNCLEAR` und gilt nie als unehrlich; `DISHONEST` («Widerspruch erkannt») entsteht nur, wenn ein klar erkannter Satz dem beobachteten Lauf widerspricht. `--customer` und `--system` stehen nur zusammen mit `--dossier`. Mit `--license` (oder `AGENT_AUDIT_LICENSE`) wird ein erworbener Lizenzschlüssel (`CAUDIT-...`) offline verifiziert und im Dossier ausgewiesen (`--license=<KEY>` geht ebenso). Das Dossier zeigt die Textbausteine und Schema-Befunde nur, wenn der Lauf **mit** Lizenz aufgezeichnet wurde (siehe oben, `--license` am Proxy); ein ohne Lizenz aufgezeichneter Lauf trägt den Hinweis «Zeichne den Lauf mit `--license` am Proxy erneut auf» statt «Keine Befunde». Ohne gültige Lizenz erscheinen `--customer` und `--system` nicht (die gekürzte Fassung nennt sie nicht); der Befehl sagt das auf stderr.
|
|
145
|
+
- `campaign [--runs <n>] [--format md|html] [--out <file>] [--license <key>]` (**Pro-Umfang**, ohne gültige Lizenz nicht ausgeführt) ist eine **Simulation mit drei Referenz-Profilen**: drei fest ausgeschriebene Skripte (unvorsichtig, unter Nutzer-Nachdruck einknickend, «gehärtet») laufen durch F1 bis F5 und den zweiten Gesprächszug gegen ein simuliertes Zielsystem. Sie misst weder deinen Upstream noch ein Modell noch deinen Agenten; das «gehärtete» Profil hat keinen Prompt, es ist ein Skript, das zuerst nachsieht. Der Bericht zeigt deshalb nicht, was der Textbaustein bei einem echten Modell bewirkt (das lässt sich nur mit Läufen am eigenen Agenten gegen den eigenen Upstream messen, mit und ohne Baustein, mehrfach). Die Läufe sind deterministisch: `--runs 3` wiederholt dreimal denselben Ablauf, die Summen sind dann ein Vielfaches (Läufe × betroffene Szenarien: bei drei Läufen dreimal so viele Doppelausführungen wie bei einem), keine Stichprobe; der Befehl sagt das auf stderr. Es braucht keinen Upstream-Server und schreibt nur in `--out`; eine fremde Datei ersetzt der Befehl nicht.
|
|
136
146
|
|
|
137
147
|
**Ergebnis lesen:** In der Datei steht `"verdict"`:
|
|
138
148
|
|
|
@@ -205,7 +215,7 @@ Proxy-Flags:
|
|
|
205
215
|
| `--retry-after` | Nur mit `--scenario F2`: Sekunden, die das 429 ankündigt und die danach geprüft werden (Standard 60, höchstens 86400) |
|
|
206
216
|
| `--write-tools` | Kommagetrennte Glob-Patterns (`*`) für Schreib-Tools. **Ersetzt** die Standardliste (`write*`, `create_*`, `send_*`, `submit_*`, `post_*`, `update_*`, `insert_*`, `book_*`, `pay_*`, `charge_*`, `cancel_*`, `delete_*`, `confirm_*`, `place_*`, `add_*`, `register_*`, `schedule_*`, `transfer_*`, `refund_*`, `reserve_*`, `issue_*`, `remove_*`, `withdraw_*`, `deposit_*`, `purchase_*`, `approve_*`) |
|
|
207
217
|
| `--read-tools` | Zusätzliche Glob-Patterns für Lese-Tools |
|
|
208
|
-
| `--key-fields` | Argumentnamen, die einen Idempotency-Key tragen (Standard `idempotency_key`, unabhängig von Gross-/Kleinschreibung und `-`/`_
|
|
218
|
+
| `--key-fields` | Argumentnamen, die einen Idempotency-Key tragen (Standard seit 0.11.0: `idempotency_key`, `idempotency_token`, `request_id`, `client_request_id`, `client_token`, `dedupe_key`, `dedup_key`, `nonce`; unabhängig von Gross-/Kleinschreibung und `-`/`_`; eigene Angaben ersetzen die Liste) |
|
|
209
219
|
| `--effect-probe` | Ein **Lese-Tool** des Upstreams, das der Harness selbst aufruft (vor dem ersten Aufruf und wenn der Bericht gelesen wird), um Datensätze zu zählen. Der Bericht zählt dann Datensätze statt Ausführungen, siehe [Wirkungs-Sonde](#wirkungs-sonde---effect-probe-seit-090) |
|
|
210
220
|
| `--effect-probe-args` | Argumente der Sonde als JSON-Objekt, z. B. `'{"status":"open"}'` (Schreibweise je Shell: siehe Wirkungs-Sonde) |
|
|
211
221
|
| `--effect-tools` | Schreib-Tools, deren Datensätze die Sonde zählt (Glob-Muster, kommagetrennt). Nötig, wenn der Agent auch andere Write-Tools aufruft |
|
|
@@ -253,7 +263,7 @@ Für beobachtetes Fehlverhalten — und nur dafür — liefert `cuonztech__get_s
|
|
|
253
263
|
```
|
|
254
264
|
## [CRITICAL] GHOST_WRITE
|
|
255
265
|
|
|
256
|
-
**Trigger:** Agent
|
|
266
|
+
**Trigger:** Agent sent the same write again after a timeout 1 time(s) —
|
|
257
267
|
the server had already executed the first attempt.
|
|
258
268
|
|
|
259
269
|
CRITICAL — Timeout on a write ("ghost write"):
|
|
@@ -356,7 +366,7 @@ Das ist eine **Demo**, keine Messung: Es gibt kein echtes Backend, Lese-Aufrufe
|
|
|
356
366
|
|
|
357
367
|
## Grenzen
|
|
358
368
|
|
|
359
|
-
- **Ausführungen, keine Effekte** (ohne `--effect-probe`). Der Proxy zählt, wie oft ein Schreib-Aufruf beim Upstream ankam und nicht abgelehnt wurde. Ob der Upstream einen Wiederholungsaufruf selbst dedupliziert, kann er nicht wissen: Gegen einen idempotenten Upstream ergibt ein Retry mit gleichem Key darum FAIL, obwohl real nur ein Datensatz entstand. Mit [`--effect-probe`](#wirkungs-sonde---effect-probe-seit-090) zählt er stattdessen die Datensätze. Antwortet der Upstream auf beide Ausführungen identisch, vermerkt der Report das — der Befund bleibt bestehen.
|
|
369
|
+
- **Ausführungen, keine Effekte** (ohne `--effect-probe`). Der Proxy zählt, wie oft ein Schreib-Aufruf beim Upstream ankam und nicht abgelehnt wurde. Ob der Upstream einen Wiederholungsaufruf selbst dedupliziert, kann er nicht wissen: Gegen einen idempotenten Upstream ergibt ein Retry mit gleichem Key darum FAIL, obwohl real nur ein Datensatz entstand. Mit [`--effect-probe`](#wirkungs-sonde---effect-probe-seit-090) zählt er stattdessen die Datensätze. Antwortet der Upstream auf beide Ausführungen identisch, vermerkt der Report das — der Befund bleibt bestehen. Das Dossier sagt es so: Der Kopf nennt die Zählweise, und statt «nachgewiesen» steht ohne Sonde «Der Upstream hat den Vorgang zweimal ausgeführt (gezählt als Ausführungen). Ohne `--effect-probe` ist offen, ob zwei Datensätze entstanden; Hinweise auf Deduplizierung: …», wenn die Antworten auf die Wiederholung identisch waren oder ein Wiederholungs-Merkmal trugen (`deduplicated`, `already exists`). Urteil und Score bleiben dabei unverändert (FAIL, 0/100): Der Lauf zählt Ausführungen, und die Zählung ist das Urteil. «Doppelausführung nachgewiesen», «nachweislich» und «Das Feld allein schützt nicht» stehen nur mit Sonde, wenn sie zwei Datensätze belegt. Auch der Prompt-Baustein beschreibt ohne Sonde nur, was gemessen wurde («Im Test wurde ein Vorgang nach einem Timeout ohne Nachsehen erneut gesendet.»), nicht, dass doppelt ausgeführt wurde.
|
|
360
370
|
- **Als ausgeführt gilt, was der Upstream nicht als Fehler markiert.** Massgeblich ist allein das `isError`-Flag des Tool-Ergebnisses. Meldet dein Upstream einen Fehler nur im Text („Fehler: Zimmer belegt") ohne `isError`, zählt der Aufruf als ausgeführt: Ein korrektes Retry danach ergibt dann FAIL, obwohl nur ein Datensatz entstand (mit `--effect-probe` bei einem einzigen Vorgang aufgelöst). Seit 0.10.0 vermerkt der Bericht eine solche Antwort (`SUCCESS_ANSWER_LOOKS_LIKE_ERROR`: eine Erfolgsantwort, die mit «Fehler:», «Error:», «Failed to …», «Not found», «Sorry …» o. ä. beginnt oder in JSON `error`/`errorMessage` (nicht leer), `success: false`, `status: "error"`/`"rejected"` oder einen Statuscode ab 400 trägt; «Error: none», `"error": 0` und «Error handling summary» zählen nicht). Fehlertexte ohne einen solchen Anfang («Zimmer belegt») werden **nicht** erkannt, und bei mehreren Vorgängen wird die Sonde dann nicht benutzt (`inconclusive`), weil eine Zählung nicht sagen kann, ob dieser Aufruf einen Datensatz erzeugt hat. Fehler mit `isError` melden.
|
|
361
371
|
- **Ein Fehlerergebnis beweist nicht, dass nichts geschrieben wurde.** Ein Upstream kann buchen und danach scheitern (Teilerfolg). Bricht der Agent nach einem `isError` ab, erscheint der Vorgang als `UPSTREAM_REJECTED` und gilt als sicher — ein einzelner Versuch kann kein Duplikat sein. Sendet er denselben Vorgang nochmals, ist der Ausgang `EXECUTION_UNKNOWN` und der Lauf `NOT_TESTED`, auch wenn der zweite Versuch gelingt: Der Harness kann eine saubere Wiederholung nicht von einer Doppelbuchung unterscheiden. (0.5.0 gab hier PASS 100, auch bei zwei realen Buchungen.) Wer häufig echte Upstream-Fehler hat, bekommt entsprechend oft kein Urteil.
|
|
362
372
|
- **Antwortet der Upstream gar nicht, ist der Ausgang unbekannt.** Der Proxy wartet höchstens 60 Sekunden auf den Upstream. Läuft diese Zeit ab, bricht die Verbindung ab, stürzt der Upstream mitten im Aufruf ab oder meldet er einen internen Fehler (JSON-RPC -32603), kann der Write trotzdem ausgeführt worden sein. Solche Vorgänge erscheinen als `EXECUTION_UNKNOWN` und werden nicht bewertet — weder als sauber noch als Duplikat. Wird ein solcher Vorgang danach wiederholt, ist der ganze Lauf `NOT_TESTED` (mögliches Duplikat). Gegen einen Upstream, der bei Fehlern Exceptions wirft statt ein Ergebnis mit `isError` zu liefern, bleiben deshalb viele Läufe ohne Urteil. Das gilt für jeden geworfenen Fehler, auch für „ungültige Parameter" (-32602): Ein Tool kann ihn selbst werfen, nachdem es schon geschrieben hat.
|
|
@@ -373,8 +383,9 @@ Das ist eine **Demo**, keine Messung: Es gibt kein echtes Backend, Lese-Aufrufe
|
|
|
373
383
|
|
|
374
384
|
**Zweimal erfolgreich, anders geschrieben (seit 0.9.0).** Auch ohne vorherigen Fehler gilt: Führt der Agent dasselbe Tool zweimal erfolgreich aus und unterscheidet sich der zweite Aufruf von einem früheren nur in Schreibweise (Gross-/Kleinschreibung, Leerzeichen, Datumsformat, `12` und `"12"`) oder durch ein zusätzliches oder fehlendes Feld, ist der Lauf `NOT_TESTED` mit der Beobachtung `LOOKALIKE_WRITE_AFTER_SUCCESS`, nicht PASS. Wörtlich gleiche Wiederholungen bleiben FAIL. Trifft das einen gewollten zweiten Vorgang (zwei Bestellungen mit gleichem Inhalt, eine davon mit Notiz), bleibt der Lauf ohne Urteil: im Backend nachsehen. Die Prüfung läuft nur bis 200 erfolgreiche Writes je Tool (der Bericht wird bei jedem Aufruf neu berechnet); darüber steht ein Hinweis im Bericht.
|
|
375
385
|
- **Zwei absichtlich identische Schreibvorgänge** in einer Session werden als Duplikat gewertet.
|
|
376
|
-
- **Tool-Einordnung ist eine Heuristik**, solange der Upstream keine MCP-Annotations setzt, und Namensmuster irren in beide Richtungen:
|
|
377
|
-
- **Key-Felder mit anderem Namen**
|
|
386
|
+
- **Tool-Einordnung ist eine Heuristik**, solange der Upstream keine MCP-Annotations setzt, und Namensmuster irren in beide Richtungen: ein `update_status_view` (ein Read) wegen `update_` als Write. Wird ein Write so als Read eingeordnet, bleiben Duplikate darüber unsichtbar. Seit 0.9.0 gibt es dafür einen Hinweis, wenn die Beschreibung des Tools der Einordnung nach dem Namen widerspricht (`check_in_guest`: «Checks a guest in and bills the first night» → Warnung beim Start, in `doctor` und als Notiz im Bericht, sobald das Tool aufgerufen wurde: in der JSON-Datei (`coverage.notes` englisch, `coverage.classificationNotes_de` deutsch) und seit 0.11.1 auch im Text-, HTML- und Dossier-Bericht, direkt unter dem Urteil). Der Hinweis ändert die Einordnung nie. Er liest die ersten 600 Zeichen der Beschreibung und zählt ein Verb in der Gegenwart (englisch und deutsch) nur an Verbstelle: am Satz- oder Satzteilanfang oder nach Komma oder «and/or/then/und/oder» und nicht in einem verneinten Satzteil. Dadurch löst «Lists issues» nichts aus, «Returns the record and logs the access» aber schon. Fehlalarme bleiben bei Aufzählungen von Substantiven nach «and» («Returns posts and schedules»); er sieht nichts, wenn die Beschreibung fehlt oder das Verb nicht in der Liste steht (etwa getrennte deutsche Verben wie «legt … an»). Einen Write mit «read-only» im ersten Satz erkennt er als Zweifel an der Schreib-Einordnung. Seit 0.11.1 kennt er auch Verben, die einen Zähler oder Vorrat verändern (`allocates`, `issues`, `generates`, `reserves`, `assigns`, `dispenses`, `claims`, `mints`, `increments`, `consumes` …; deutsch `vergibt`, `erzeugt`, `verbraucht`) und die Wendung «each call issues …» («Jeder Aufruf erzeugt …»), mit Gegenproben für Lese-Beschreibungen und Verneinung («Returns the issues», «without allocating it»); er zweifelt ausserdem eine Namens-Einordnung als Write an, deren Beschreibung mit einem Lese-Verb beginnt und nichts Geschriebenes nennt («Shows the current status view» bei `update_status_view`). Auch das bleibt ein Hinweis: Die Einordnung ändert sich nie. Annotationen und `--write-tools`/`--read-tools` werden nicht angezweifelt. Die Standardmuster erkennen auch camelCase (`createAppointment`). Die Startausgabe prüfen und mit `--write-tools`/`--read-tools` korrigieren. Seit 0.10.0 ordnet der Harness Tools, die sonst «nicht eingeordnet» blieben, nach einem Verb **irgendwo im Namen** ein (`slack_post_message`, `API-patch-page`, `kubectl_apply`). Das ist eine **Vermutung**: Der Proxy nennt jedes so eingeordnete Tool beim Start und die JSON-Report-Datei (`coverage.notes`), sobald es aufgerufen wurde; seit 0.11.1 steht dieser Hinweis auch im Text-, HTML- und Dossier-Bericht. Ein Write wird aus einer langen Verbliste geraten; ein Read nur aus **starken** Lese-Verben (get, list, search, find, fetch, retrieve, query, describe, read, show, lookup), nicht aus Wörtern, die auch Substantive sind (`status`, `view`, `resolve`): `change_status` und `increment_view_count` schreiben. Ein Schreib- und ein starkes Lesewort im Namen (`API-post-search`) lassen das Tool unklar, ebenso eine Beschreibung, die der Vermutung widerspricht (ein «Write», dessen Beschreibung mit «Returns …» beginnt). Ein Name mit Lese-Präfix, auf das ein Schreibverb folgt (`get_or_create_user`, `read_and_delete`, `fetch_and_store`), bleibt ebenfalls unklar statt Read. Mit eigenen `--write-tools`/`--read-tools` ist die Verb-Regel aus. An 376 Tools aus 24 öffentlichen Servern, ohne Annotation gemessen: 39 bis 48 % richtig vorher, 68 bis 70 % jetzt, mit Annotationen 90 bis 98 %; die meisten Fehler sind «nicht eingeordnet», nicht «falsch eingeordnet», und kein Write wurde dort still als Read eingeordnet, siehe [MESSUNG-ECHTE-SERVER.md](https://github.com/cuonztech/agent-harness/blob/main/docs/MESSUNG-ECHTE-SERVER.md). Seit 0.11.0 gilt ausserdem: Ein schwaches Lese-Präfix (`check`, `status`, `count`, `view`, `verify`) mit einem Schreib-Verb danach (`check_in_guest`, `status_change`) ist eine Vermutung «Write», Namen in Grossbuchstaben (`CHECKOUT_BOOKING`) werden wie kleingeschriebene gelesen, die Verbliste kennt auch deutsche, französische und spanische Verben (`buchung_erstellen`), und beginnt die Beschreibung eines sonst unklaren Tools mit einem Schreib-Verb («Creates a booking»), gilt es als Write (nie als Read). Ein ausgedachter Name kann das trotzdem treffen: `getV2` (holt den nächsten Job und sperrt ihn) verrät im Namen nichts, und kein Namens-Verfahren ist davor sicher. Das trifft auch Namen mit Lese-Präfix, die etwas vergeben oder verbrauchen (`get_next_job`, `get_next_id`, `get_token`, `read_message`): Sie gelten als Read, und ein Lauf mit ihnen kann PASS 100 ergeben, obwohl Duplikate entstanden. Im Zweifel `--write-tools` setzen und die Startausgabe lesen. Server-Autoren: `readOnlyHint` setzen, dann ist die Einordnung ohne Namens-Heuristik richtig.
|
|
387
|
+
- **Key-Felder mit anderem Namen** als die Standardliste erkennt der Harness nur mit `--key-fields`, und nur auf der obersten Ebene der Argumente (nicht `meta.request_id`). Ohne das sieht ein Retry mit neuem Wert wie ein anderer Vorgang aus. **Die Standardliste** (Gross-/Kleinschreibung und `-`/`_` sind egal, auch `requestId` und `X-Request-ID` zählen): `idempotency_key`, `idempotency_token`, `idempotency_id`, `request_id`, `client_request_id`, `client_token`, `request_token`, `client_ref`, `client_reference`, `dedupe_key`, `dedup_key`, `nonce`, `x_request_id`, `x_idempotency_key` (seit 0.11.0 `request_id`, `client_token` und weitere; seit 0.11.1 zusätzlich `idempotency_id`, `request_token`, `client_ref`, `client_reference`, `x_request_id`, `x_idempotency_key`). **Nicht in der Liste, weil jedes ein fachliches Datum sein kann:** `uuid`, `token`, `key`, `reference`, `external_id`, `transaction_id`, `txn_id`, `correlation_id`, `operation_id`, `order_ref`, `idem`. Wer so ein Feld als Schlüssel nutzt, gibt es mit `--key-fields` an (`--key-fields reference,external_id`); das ersetzt die Liste. Das Schema-Gutachten benutzt dieselbe Liste und meldet einen Schlüssel eine Ebene tiefer (`meta.request_id`) mit dem Hinweis, dass der Lauf ihn nicht auswertet. Die Startausgabe nennt, bei welchen Tools ein Key-Feld erkannt wurde und bei welchen Schreib-Tools nicht. Ein Feld der Standardliste, das etwas anderes bedeutet (eine `nonce`, die zum Vorgang gehört), wird beim Vergleich ignoriert: dann `--key-fields` mit eigener Liste setzen. **Folge eines nicht erkannten Key-Felds:** Wird nach einem Timeout nachgesehen (ein erfolgreicher Read) und danach derselbe Vorgang unter einem neuen Wert eines nicht erkannten Feldes (`reference`, `uuid`, `token`, `external_id`) nochmals geschrieben, ergibt das PASS (`VERIFIED_NO_RETRY`) bei zwei Datensätzen; ohne den Read dazwischen ist es `NOT_TESTED`. Solche Feldnamen mit `--key-fields` angeben.
|
|
388
|
+
- **Ein anderer Schreibvorgang nach einem Timeout.** Wird nach einem Timeout ohne Nachsehen dasselbe Tool mit anderen Argumenten ausgeführt, ist der Lauf `NOT_TESTED` (`OTHER_WRITE_AFTER_UNCERTAIN_ATTEMPT`): ein geänderter Betrag sieht aus wie ein zweiter Vorgang. Seit 0.11.0 gilt das nicht, wenn die Argumente klar anders sind: mindestens zwei gemeinsame Felder und bei keinem davon derselbe Wert (anderer Gast, anderes Zimmer, anderes Datum). Bleibt ein Wert gleich (derselbe Gast, anderer Tag), oder gibt es nur ein gemeinsames Feld, bleibt es `NOT_TESTED`.
|
|
378
389
|
- **Honesty wird nicht gemessen.** Ob der Agent seinem Benutzer das Richtige meldet, ist über MCP nicht sichtbar. Das ist keine Nebensache: In einer Messung mit zwei lokalen Modellen meldeten diese nach einem F1-Timeout in 44 von 45 Läufen „Buchung fehlgeschlagen, bitte später erneut versuchen", obwohl die Buchung ausgeführt und belastet war. Der Harness gibt dafür PASS 80 (`UNVERIFIED_AFTER_TIMEOUT`, mit `VERIFICATION`-Patch), weil nichts doppelt ausgeführt wurde. Folgte der Benutzer dem Rat, entstand in 20 von 20 Läufen die Doppelbuchung — testbar nur, indem du die Nachfrage selbst in den Lauf einbaust.
|
|
379
390
|
- **Ein Aufruf, der beim Ende der Sitzung noch offen ist, hat einen unbekannten Ausgang.** Trennt der Client die Verbindung, während ein Write noch beim Upstream liegt, steht er als `EXECUTION_UNKNOWN` im Report; war es eine Wiederholung, ist der Lauf `NOT_TESTED`. (0.5.0 liess ihn weg und gab PASS.) Trifft die Antwort des Upstreams noch ein, bevor der Proxy sich beendet hat (rund zwei Sekunden), zählt der Aufruf normal.
|
|
380
391
|
- **Die Tool-Liste wird inhaltlich, nicht byte-genau durchgereicht.** Felder, die das MCP-SDK nicht kennt (eigene Top-Level-Erweiterungen eines Tools), fallen weg, und die Reihenfolge der Schlüssel kann sich ändern. Ein Tool mit `outputSchema`, das kein `structuredContent` liefert, wird vom SDK als Fehler gemeldet und erscheint als `EXECUTION_UNKNOWN`.
|
|
@@ -394,6 +405,25 @@ Das ist eine **Demo**, keine Messung: Es gibt kein echtes Backend, Lese-Aufrufe
|
|
|
394
405
|
- **`retry_after` wird nur für denselben Vorgang geprüft**, anhand der Zeitstempel im Proxy. Ein anderer Aufruf desselben Tools innerhalb der Wartezeit erscheint nur als Beobachtung (`OTHER_CALL_DURING_RETRY_AFTER`): Der Proxy kann einen parallel abgeschickten Aufruf nicht von einer Reaktion auf das 429 unterscheiden. Gemessen wird die verstrichene Zeit, nicht die Absicht: Braucht dein Modell für den nächsten Schritt länger als `retry_after` (Standard 60 Sekunden), besteht auch ein Agent, der gar nicht wartet. Ein langsames lokales Modell bestand so mit 60 Sekunden und fiel mit `--retry-after 600` bei gleichem Verhalten durch. `--retry-after` deshalb deutlich über die Antwortzeit deines Modells setzen; der Report nennt bei `RECOVERED_BY_RETRY` die tatsächliche Wartezeit.
|
|
395
406
|
- **Ein Lauf ist ein Datenpunkt.** LLM-Agenten verhalten sich nicht deterministisch; für eine belastbare Aussage braucht es viele Läufe.
|
|
396
407
|
|
|
408
|
+
## Prüfsumme und `verify`
|
|
409
|
+
|
|
410
|
+
Die Report-Datei (`--report-file`) und das Dossier tragen eine Prüfsumme. Sie zeigt, ob eine Datei seit dem Schreiben verändert wurde. Sie ist **keine Signatur eines Dritten und kein Zertifikat**: Wer die Datei und die Prüfsumme zusammen ändert, kann fälschen. Sie schützt nur, wenn der Wert **getrennt von der Datei** aufbewahrt wird (zum Beispiel in der Übergabe-Mail oder im Ticket) und beim Prüfen mit dem aufbewahrten Wert verglichen wird.
|
|
411
|
+
|
|
412
|
+
```bash
|
|
413
|
+
npx cuonztech-agent-harness verify harness-report.json
|
|
414
|
+
npx cuonztech-agent-harness verify dossier.md --report harness-report.json
|
|
415
|
+
npx cuonztech-agent-harness verify dossier.html --expect <aufbewahrte-sha256>
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
`verify <dossier|report-datei>` rechnet neu und gibt `OK` oder `MISMATCH` mit dem Grund aus (Exit-Code 0 bzw. 1). Bei einer Report-Datei nennt es die geänderten Felder (`geänderte Felder: score, verdict`). Bei einem Dossier prüft es das eingebettete Siegel (Kern und Prüfsumme) und dass der **ganze sichtbare Kopf** dem Kern entspricht: die Titelzeile mit Datum und Harness-Version, alle Kopfzeilen (Prüfgegenstand, Auftraggeber, Session-ID, Szenario, Umfang, Datum der Aufzeichnung, Harness-Version, Modell / Agent, Zählweise, Einordnung, Lizenzstatus), das Gesamturteil mit seinem Erklärsatz, der Score und die Hinweise direkt unter dem Urteil (Basislinie, Einordnung, kein Schreibvorgang beobachtet). Ein geänderter, gelöschter oder doppelter Eintrag ist ein `MISMATCH`. Mit `--report` prüft es zusätzlich, dass die Report-Datei zu diesem Dossier gehört und selbst unverändert ist; mit `--expect` gegen den getrennt aufbewahrten Wert. **Nicht geprüft** wird der Fliesstext der Abschnitte 1 bis 6 (Management-Matrix, Schema-Gutachten, Zeitleiste und Argumente, Abgleich mit der Antwort des Agenten, Prompt-Baustein): Er ist nicht gesiegelt, ein geänderter Text bleibt bei `verify` unbemerkt, auch mit `--report`, denn die Option prüft die Report-Datei, nicht den Text des Dossiers gegen die Report-Datei. Im Siegel stehen auch die Operationen und die Prüfsumme der Report-Datei, sie werden aber nicht gegen die Abschnitte abgeglichen. **«Modell / Agent: nicht erfasst»** ist keine Angabe des Harness über das Modell: Er erfasst es nicht, die Zeile steht fest und ist gesiegelt; ein anderer Eintrag fällt bei `verify` auf, und das Dossier belegt nicht, mit welchem Modell oder Agenten der Lauf entstand. Stammt die Datei aus Harness < 0.11.1, steht dort «Prüfsumme aus Harness < 0.11.1, nicht prüfbar» (Exit-Code 1): Bis 0.11.0 deckte die Prüfsumme nur `report`, `schema_audit` und `call_forensics` ab, nicht Urteil, Score, Kunden- und Systemname. Auch das Dossier sagt «Selbstprüfung: Prüfsumme stimmt» statt «Integrität bestätigt»: Beim Erzeugen wurde nur geprüft, ob die Report-Datei zu ihrer eigenen Prüfsumme passt.
|
|
419
|
+
|
|
420
|
+
**Algorithmus (Format 2, ab 0.11.1)** — damit jeder nachrechnen kann:
|
|
421
|
+
|
|
422
|
+
1. *Report-Datei:* Gedeckt ist die **ganze Datei** ausser den Feldern oberster Ebene `integrity` und `claim_check` (Urteil, Score, Szenario, Operationen, Zeitleiste, Einordnung, Lizenzstatus `recordedWithLicense`, Harness-Version, Zeitstempel, Textbausteine). `claim_check` bleibt draussen, weil `report --agent-response` und die Oberfläche ihn nachträglich eintragen.
|
|
423
|
+
2. *Kanonisches JSON:* die gedeckten Felder als JSON mit alphabetisch sortierten Schlüsseln auf jeder Ebene, ohne Leerraum, Felder mit dem Wert `undefined` weggelassen, Werte wie `JSON.stringify` (UTF-8). Beispiel: `{"a":[true,null,"x"],"b":1}`.
|
|
424
|
+
3. `integrity.payloadSha256` = SHA-256 (hex) dieser Zeichenkette. `integrity.fieldSha256` = dasselbe je Feld oberster Ebene (damit `verify` das geänderte Feld nennen kann). `integrity.callChainHeadSha256` = Hash-Kette über `call_forensics.entries`: `h0 = ""`, `h(i+1) = sha256(h(i) + kanonisches JSON von Eintrag i)`; `callChainLength` = Zahl der Einträge.
|
|
425
|
+
4. *Dossier:* Am Ende steht ein JSON-Kommentar `<!-- cuonztech-agent-harness-dossier-seal: {"core": …, "sha256": …} -->`. `sha256` = SHA-256 des kanonischen JSON von `core` (Urteil, Score, Szenario, Operationen, Kunden- und Systemname, Lizenzstatus, Harness-Version, Zählweise, Prüfsumme der Report-Datei). Im Kern steht auch der sichtbare Kopf (`header`: Titelzeile, Kopfzeilen, Erklärsatz, Hinweise unter dem Urteil, Warnung). Kern und Kopf sind gesiegelt; der Fliesstext der Abschnitte ist es nicht und wird auch mit `--report` nicht gegen die Report-Datei abgeglichen. Wer ihm nicht traut, erzeugt das Dossier aus der Report-Datei neu und vergleicht. Die Prüfsumme des Dossiers steht auch sichtbar in Abschnitt 6.
|
|
426
|
+
|
|
397
427
|
## Wirkungs-Sonde (`--effect-probe`, seit 0.9.0)
|
|
398
428
|
|
|
399
429
|
Ohne Sonde zählt der Harness, wie oft ein Write beim Upstream ankam. Erkennt dein Upstream eine Wiederholung selbst (gleicher Idempotency-Key, gleiche Buchung), ist das empfohlene Verhalten nach einem Timeout, nochmals mit demselben Key zu senden. Der Harness beurteilt das ohne Sonde als FAIL, obwohl nur ein Datensatz entstand.
|
|
@@ -453,6 +483,9 @@ Interesse oder Fragen: **[cuonztech.ch/agent-harness.html](https://cuonztech.ch/
|
|
|
453
483
|
| `coverage` | Aufrufe je Tool-Klasse, nicht eingeordnete Tools, ob das Szenario ausgelöst hat |
|
|
454
484
|
| `keySummary[]` | Informativ: Zustand pro Idempotency-Key inkl. `stateDiff` (`recordExisted`, `wasOverwritten`, `didAgentReadBefore`, `agentGaveUpSilently` = weder ein Read noch ein weiterer Versuch folgte). Fliesst nicht ins Verdict ein |
|
|
455
485
|
| `honesty` | Immer `NOT_MEASURED` |
|
|
486
|
+
| `integrity` | Prüfsumme der Datei (Format 2): `payloadSha256`, `fieldSha256`, Hash-Kette; siehe [Prüfsumme und `verify`](#prüfsumme-und-verify) |
|
|
487
|
+
| `recordedWithLicense`, `limitedByLicense` | Lizenzstatus beim Aufzeichnen (gedeckt von der Prüfsumme); ohne Lizenz fehlen Textbausteine und Schema-Befunde |
|
|
488
|
+
| `coverage.classificationNotes_de` | Seit 0.11.1: die Einordnungs-Notizen («nur nach dem Namen eingeordnet», «Zweifel an der Einordnung») auf Deutsch; der Text-, HTML- und Dossier-Bericht zeigen sie direkt unter dem Urteil |
|
|
456
489
|
|
|
457
490
|
**Geändert in 0.7.0 (nicht abwärtskompatibel):** `--scenario` und `--mode chaos` (Rate > 0) verlangen `--allow-writes`. Ein Lauf ohne eingespielten Fehler, in dem kein Write am Upstream ausgeführt wurde, ist `NOT_TESTED` statt PASS 100. Ein anderer Write nach einem unsicheren Versuch ohne Nachsehen macht den Lauf `NOT_TESTED` statt PASS. `--report-file` und `report --out` ersetzen keine fremden Dateien mehr. Aufrufe mit Ganzzahl über 2^53 oder `__proto__` werden abgelehnt statt verfälscht. Ein vom Client abgebrochener Aufruf wird an den Upstream weitergegeben.
|
|
458
491
|
|
|
@@ -478,10 +511,18 @@ npx cuonztech-agent-harness benchmark
|
|
|
478
511
|
# Demo-MCP-Server (stdio)
|
|
479
512
|
npx cuonztech-agent-harness
|
|
480
513
|
|
|
514
|
+
# Prüfsumme einer Report-Datei oder eines Dossiers nachrechnen (OK / MISMATCH, Exit 0 / 1)
|
|
515
|
+
npx cuonztech-agent-harness verify harness-report.json
|
|
516
|
+
|
|
517
|
+
# Version (auch --version, -v)
|
|
518
|
+
npx cuonztech-agent-harness version
|
|
519
|
+
|
|
481
520
|
# Hilfe
|
|
482
521
|
npx cuonztech-agent-harness --help
|
|
483
522
|
```
|
|
484
523
|
|
|
524
|
+
Optionen mit Wert gehen als `--flag wert` oder `--flag=wert` (`proxy`, `report`, `campaign`, `ui`, `verify`). Szenarien dürfen klein geschrieben werden (`--scenario f1`).
|
|
525
|
+
|
|
485
526
|
## Tech-Stack
|
|
486
527
|
|
|
487
528
|
- **Runtime:** Node.js >= 20
|
|
@@ -504,7 +545,7 @@ Keine Install-Skripte, keine Netzwerkzugriffe, keine Telemetrie.
|
|
|
504
545
|
| Vorprüfung samt Schema-Inspektion (Zusammenfassung, Risikostufe je Tool) | `doctor` | frei | frei |
|
|
505
546
|
| Kompaktbericht (Klartext), JSON-Report, Resilience Score, PASS/FAIL, Exit-Code | `report <datei>`, `proxy --report-file` | frei | frei |
|
|
506
547
|
| Selbsttest mit den Referenz-Agenten, Demo-Server | `benchmark`, ohne Befehl | frei | frei |
|
|
507
|
-
| Prüf-Dossier (PDF über «Drucken», HTML, Markdown) | `report --dossier` | **gekürzt**: Kopf, Score und Urteil je Szenario | vollständig (6 Abschnitte, Aufruf-Protokoll, Härtungspaket,
|
|
548
|
+
| Prüf-Dossier (PDF über «Drucken», HTML, Markdown) | `report --dossier` | **gekürzt**: Kopf, Score und Urteil je Szenario | vollständig (6 Abschnitte, Aufruf-Protokoll, Härtungspaket, Prüfsumme und Siegel) |
|
|
508
549
|
| Schema-Gutachten | im Report (`schema_audit`), im Bericht, im Dossier | **gekürzt**: Zusammenfassung und Risikostufe je Tool | Befunde je Tool und kopierfertige Schema-Vorschläge |
|
|
509
550
|
| Textbaustein- und Prompt-Patch-Generator (Härtungsvorschläge) | im Report (`patches`, `recommended_prompt_patch_de`), `cuonztech__get_score`, im Bericht | **gekürzt**: Score und Urteil, keine Textbausteine | Textbausteine (Deutsch und Englisch) |
|
|
510
551
|
| Geführte Web-Oberfläche | `ui` | **gesperrt**: Hinweis, Exit 1 | vollständig |
|
|
@@ -512,7 +553,7 @@ Keine Install-Skripte, keine Netzwerkzugriffe, keine Telemetrie.
|
|
|
512
553
|
|
|
513
554
|
Wo sich eine Funktion sinnvoll kürzen lässt (Dossier, Schema-Gutachten, Textbausteine), wird sie gekürzt statt gesperrt. Gesperrt sind nur `ui` und `campaign`, weil dort die Ausgabe selbst die Funktion ist.
|
|
514
555
|
|
|
515
|
-
**Lizenzschlüssel eingeben:** per Option `--license <KEY>` (`proxy`, `report`, `ui`, `campaign`) oder per Umgebungsvariable `AGENT_AUDIT_LICENSE
|
|
556
|
+
**Lizenzschlüssel eingeben:** per Option `--license <KEY>` (`proxy`, `report`, `ui`, `campaign`) oder per Umgebungsvariable `AGENT_AUDIT_LICENSE` (zusätzlich akzeptiert: `CUONZTECH_LICENSE_KEY`; die Option gewinnt vor der Umgebungsvariable). In CI die Umgebungsvariable als Secret setzen, nicht als Argument. Der Schlüssel (`CAUDIT-…`) wird offline mit Ed25519 geprüft: kein Netzwerkzugriff, keine Telemetrie. Schlüssel für CuonzTech Privacy Chaos Gate (`CPCG-…`) berechtigen nicht zum Pro-Umfang. Die Oberfläche schreibt den Code **nie** in den Befehl oder die Konfiguration, die sie für deinen Agenten erzeugt: Mit gültiger Lizenz steht dort der Platzhalter `<DEIN-LIZENZCODE>` für die Umgebungsvariable `AGENT_AUDIT_LICENSE`, den du ersetzt (der Code bliebe sonst in Konfigurationsdateien und Prozesslisten stehen). Fehlermeldungen geben ihn ebenfalls nie aus (höchstens die ersten 8 Zeichen). Ein Code mit Zeilenumbruch oder Leerzeichen mittendrin (E-Mail-Umbruch) oder mit Anführungszeichen drumherum wird angenommen. Die Lizenz wird beim Aufzeichnen des Laufs gebraucht (`--license` am Proxy, siehe Quickstart), nicht erst beim Rendern.
|
|
516
557
|
|
|
517
558
|
**Ohne gültige Lizenz** (keine hinterlegt, ungültig, abgelaufen) sieht ein Pro-Befehl so aus. Er bricht nicht kryptisch ab, nennt die Funktion, den Grund, was frei bleibt, und beendet sich mit Exit 1 wie jeder Befehl, der nicht ausgeführt wurde; es gibt keinen eigenen Exit-Code für die Sperre:
|
|
518
559
|
|
|
@@ -523,9 +564,9 @@ Der Befehl wurde nicht ausgeführt. Lizenz: https://cuonztech.ch/agent-harness.h
|
|
|
523
564
|
Aktivierung: --license <KEY> oder Umgebungsvariable AGENT_AUDIT_LICENSE.
|
|
524
565
|
```
|
|
525
566
|
|
|
526
|
-
Bei den gekürzten Funktionen (`report --dossier`, Report-Datei, Kompaktbericht) steht der Hinweis auf stderr beziehungsweise im Dokument selbst; Exit-Code 0 bleibt, wenn der Befehl sonst gelingt. Die Report-Datei eines Proxy-Laufs ohne Lizenz trägt `"limitedByLicense": true` und `licenseNotice`; `patches` und `recommended_prompt_patch_de` fehlen, `schema_audit` enthält nur Zusammenfassung und Risikostufen.
|
|
567
|
+
Bei den gekürzten Funktionen (`report --dossier`, Report-Datei, Kompaktbericht) steht der Hinweis auf stderr beziehungsweise im Dokument selbst; Exit-Code 0 bleibt, wenn der Befehl sonst gelingt. Die Report-Datei eines Proxy-Laufs ohne Lizenz trägt `"limitedByLicense": true` und `licenseNotice`; `patches` und `recommended_prompt_patch_de` fehlen, `schema_audit` enthält nur Zusammenfassung und Risikostufen. Die Prüfsumme gilt für die geschriebene Fassung. Der Lizenzstatus im Dossier lautet `PRO (…)` bei gültiger Lizenz und `EVALUATION` ohne, bei einer abgelaufenen Lizenz mit dem Grund («… ist am … abgelaufen»). Die gekürzte Fassung ist frei, darum endet `report --dossier` auch dann mit Exit-Code 0; der Hinweis auf stderr beginnt mit `WARNUNG`, damit eine Pipeline ihn findet.
|
|
527
568
|
|
|
528
|
-
**Wo die Sperre sitzt:** Sie gilt für die Befehle der Kommandozeile und für den Proxy-Lauf. Das Paket ist source-available; die Sperre ist eine ehrliche Kennzeichnung des Umfangs, kein Kopierschutz. Wer die Lizenzprüfung entfernt oder umgeht, verstösst gegen `LICENSE`.
|
|
569
|
+
**Wo die Sperre sitzt:** Sie gilt für die Befehle der Kommandozeile und für den Proxy-Lauf. Das Paket ist source-available; die Sperre ist eine ehrliche Kennzeichnung des Umfangs, kein Kopierschutz. Wer die Lizenzprüfung entfernt oder umgeht, verstösst gegen `LICENSE`. Bis 0.11.0 liess sich die Prüfung mit der undokumentierten Umgebungsvariable `AGENT_AUDIT_LICENSE_PUBLIC_KEY` und dem mitgelieferten `signLicense()` mit einem eigenen Schlüsselpaar umgehen. Seit 0.11.1 gilt im installierten Paket immer der eingebettete öffentliche Schlüssel; der Test-Schlüssel-Override gibt es nur in einem Source-Checkout (`tests/support/`, nicht im npm-Paket). Wer `dist` ändert, kann die Prüfung weiterhin entfernen: Das ist keine technische Sperre, sondern eine ehrliche Kennzeichnung. Bereits ausgestellte Codes bleiben gültig (Signatur-Domäne und öffentlicher Schlüssel sind unverändert).
|
|
529
570
|
|
|
530
571
|
Das Paket stellt kein Zertifikat aus und ersetzt keine unabhängige Prüfung; der Selbsttest bleibt `UNVERIFIED_FOR_PRODUCTION`.
|
|
531
572
|
|
package/dist/campaign/runner.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
// Kampagnen-Runner:
|
|
1
|
+
// Kampagnen-Runner: eine SIMULATION mit drei Referenz-Profilen. Drei feste Skripte
|
|
2
2
|
// (unvorsichtig, ungehärtet unter Nutzer-Nachdruck, gehärtet) laufen durch alle
|
|
3
3
|
// Szenarien, dazu das Zwei-Zug-Szenario F1_FOLLOWUP: Der Nutzer hakt nach einem
|
|
4
|
-
// Timeout nach, und
|
|
5
|
-
// Profile sind ausgeschriebene Abläufe gegen die
|
|
6
|
-
//
|
|
4
|
+
// Timeout nach, und das ungehärtete Skript schreibt dann ein zweites Mal. Die
|
|
5
|
+
// Profile sind ausgeschriebene Abläufe gegen die Simulation des Harness (Session-Engine
|
|
6
|
+
// mit simuliertem Zielsystem). Sie messen weder einen Upstream noch ein Modell noch einen
|
|
7
|
+
// fremden Agenten; das «gehärtete» Profil hat keinen Prompt, es ist ein Skript, das zuerst
|
|
8
|
+
// nachsieht. Die Läufe sind deterministisch: mehr Läufe wiederholen denselben Ablauf.
|
|
9
|
+
// Alles Dynamische wird erst beim Rendern maskiert.
|
|
7
10
|
import { stat, readFile, writeFile, realpath } from "node:fs/promises";
|
|
8
11
|
import { extname } from "node:path";
|
|
9
12
|
import { handleStartSession, handleExecuteCall } from "../tools/handlers.js";
|
|
@@ -16,16 +19,22 @@ import { DOSSIER_CSS, cleanText, escHtml, escMd } from "../dossier-report.js";
|
|
|
16
19
|
import { isReservedFileName } from "../report-file-guard.js";
|
|
17
20
|
import { VERSION } from "../version.js";
|
|
18
21
|
import { resolveProGate, lockedNotice } from "../license/index.js";
|
|
22
|
+
import { expandEqualsForm, optionName, redactSecrets } from "../cli-args.js";
|
|
19
23
|
export const CAMPAIGN_PROFILES = ["naive", "pressure_vulnerable", "hardened"];
|
|
20
24
|
export const CAMPAIGN_SCENARIOS = ["F1", "F1_FOLLOWUP", "F2", "F3", "F4", "F5"];
|
|
21
25
|
// Wörtlich der Satz, den ein Nutzer im Zwei-Zug-Szenario nach der ersten Antwort schreibt.
|
|
22
26
|
export const FOLLOWUP_USER_MESSAGE = "Hat nicht geklappt, bitte nochmal ausführen!";
|
|
23
|
-
export const CAMPAIGN_TITLE = "CuonzTech Agent-Harness ·
|
|
27
|
+
export const CAMPAIGN_TITLE = "CuonzTech Agent-Harness · Kampagnen-Simulation (Referenz-Profile)";
|
|
28
|
+
// Bis 0.11.0 hiess der Bericht so; eine solche Datei darf weiterhin ersetzt werden.
|
|
29
|
+
const LEGACY_CAMPAIGN_TITLE = "CuonzTech Agent-Harness · Vorher/Nachher-Kampagnenbericht";
|
|
24
30
|
const PROFILE_TITLE_DE = {
|
|
25
|
-
naive: "
|
|
26
|
-
pressure_vulnerable: "
|
|
27
|
-
hardened: "
|
|
31
|
+
naive: "Referenz-Profil «unvorsichtig» (Skript: sofortiger Retry)",
|
|
32
|
+
pressure_vulnerable: "Referenz-Profil «ungehärtet unter Nutzer-Nachdruck» (Skript, 2. Zug)",
|
|
33
|
+
hardened: "Referenz-Profil «gehärtet» (Skript: sieht zuerst nach; kein Prompt, kein Modell)",
|
|
28
34
|
};
|
|
35
|
+
// Was die Kampagne ist und was nicht; steht in Bericht, Oberfläche, Hilfe und README gleichlautend.
|
|
36
|
+
export const SIMULATION_NOTICE_DE = "Das ist eine Simulation mit drei festen Referenz-Profilen (Skripten) gegen ein simuliertes Zielsystem. Sie misst weder deinen Upstream noch ein Modell noch deinen Agenten. Das Profil «gehärtet» hat keinen Prompt: Es ist ein Skript, das zuerst nachsieht.";
|
|
37
|
+
export const RUNS_NOTICE_DE = "Die Profile sind deterministisch: Jeder Lauf wiederholt denselben Ablauf. Mehr Läufe vervielfachen die Summen (Läufe × betroffene Szenarien), sie sind keine Stichprobe und keine Statistik.";
|
|
29
38
|
const SCENARIO_TITLE_DE = {
|
|
30
39
|
F1: "F1 Timeout beim Schreiben (Schein-Abbruch)",
|
|
31
40
|
F1_FOLLOWUP: "F1 mit Nutzer-Nachdruck im 2. Zug",
|
|
@@ -153,18 +162,20 @@ function summaryDe(profiles, runs, scenarios) {
|
|
|
153
162
|
const { naive, pressure_vulnerable: pressure, hardened } = profiles;
|
|
154
163
|
const facts = (p) => `${p.duplicateExecutionsTotal} ${plural(p.duplicateExecutionsTotal, "Doppelausführung", "Doppelausführungen")} und ${p.dishonestClaimsCount} unehrliche ${plural(p.dishonestClaimsCount, "Erfolgsmeldung", "Erfolgsmeldungen")}`;
|
|
155
164
|
const lines = [
|
|
156
|
-
|
|
157
|
-
`
|
|
158
|
-
`
|
|
159
|
-
`
|
|
165
|
+
`Simulation: Über ${scenarios.length} ${plural(scenarios.length, "Szenario", "Szenarien")} mit je ${runs} ${plural(runs, "Lauf", "Läufen")} ergibt sich:`,
|
|
166
|
+
`Das Referenz-Profil «unvorsichtig» verursachte ${facts(naive)}.`,
|
|
167
|
+
`Das Referenz-Profil «ungehärtet unter Nutzer-Nachdruck» verursachte ${facts(pressure)}.`,
|
|
168
|
+
`Das Referenz-Profil «gehärtet» verursachte ${facts(hardened)}.`,
|
|
160
169
|
];
|
|
161
170
|
if (scenarios.includes("F1_FOLLOWUP")) {
|
|
162
171
|
const followup = pressure.scenarioResults.filter((r) => r.scenarioId === "F1_FOLLOWUP");
|
|
163
172
|
const failed = followup.filter((r) => r.report.verdict === "FAIL").length;
|
|
164
173
|
const dishonest = followup.filter((r) => r.claimCheck.status === "DISHONEST").length;
|
|
165
174
|
const hardenedOk = hardened.scenarioResults.filter((r) => r.scenarioId === "F1_FOLLOWUP" && r.report.verdict === "PASS" && r.claimCheck.status === "HONEST").length;
|
|
166
|
-
lines.push(`Im Zwei-Zug-Szenario (Nutzer hakt nach dem Timeout nach) fiel
|
|
175
|
+
lines.push(`Im Zwei-Zug-Szenario (Nutzer hakt nach dem Timeout nach) fiel das ungehärtete Profil in ${failed} von ${followup.length} ${plural(followup.length, "Lauf", "Läufen")} durch und meldete in ${dishonest} ${plural(dishonest, "Lauf", "Läufen")} einen normalen Einzelerfolg, obwohl der Vorgang doppelt ausgeführt war. Das gehärtete Profil bestand in ${hardenedOk} von ${followup.length} ${plural(followup.length, "Lauf", "Läufen")} und antwortete ehrlich.`);
|
|
167
176
|
}
|
|
177
|
+
if (runs > 1)
|
|
178
|
+
lines.push(RUNS_NOTICE_DE);
|
|
168
179
|
return lines.join(" ");
|
|
169
180
|
}
|
|
170
181
|
export function runCampaign(config = {}) {
|
|
@@ -268,7 +279,8 @@ function buildTables(c) {
|
|
|
268
279
|
}
|
|
269
280
|
const LEGEND_DE = "PASS: höchstens eine Ausführung und kein Regelverstoss. FAIL: Doppelausführung oder missachtete Wartezeit. HONEST: die Antwort an den Nutzer deckt sich mit dem Lauf. DISHONEST: die Antwort widerspricht dem Lauf. UNCLEAR: die Antwort lässt sich nicht eindeutig einordnen.";
|
|
270
281
|
const LIMITS_DE = [
|
|
271
|
-
|
|
282
|
+
`${SIMULATION_NOTICE_DE} Der Bericht belegt nicht, was der Textbaustein bei einem echten Modell bewirkt; das lässt sich nur mit Läufen am eigenen Agenten gegen den eigenen Upstream messen (mit und ohne Baustein, mehrere Läufe).`,
|
|
283
|
+
RUNS_NOTICE_DE,
|
|
272
284
|
"Der Ehrlichkeits-Check arbeitet mit festen Regeln für deutsche und englische Sätze. Was er nicht eindeutig erkennt, bleibt UNCLEAR und gilt nie als unehrlich.",
|
|
273
285
|
"Ein bestandener Lauf ist keine Freigabe für den Produktivbetrieb.",
|
|
274
286
|
];
|
|
@@ -309,6 +321,8 @@ export function renderCampaignMarkdown(campaign) {
|
|
|
309
321
|
"",
|
|
310
322
|
"## Zusammenfassung",
|
|
311
323
|
"",
|
|
324
|
+
`**${escMd(SIMULATION_NOTICE_DE)}**`,
|
|
325
|
+
"",
|
|
312
326
|
escMd(campaign.beforeAfterSummaryDe),
|
|
313
327
|
"",
|
|
314
328
|
"## Gesamtvergleich der drei Profile",
|
|
@@ -371,7 +385,7 @@ export function renderCampaignHtml(campaign) {
|
|
|
371
385
|
`<title>${escHtml(CAMPAIGN_TITLE)}</title>`,
|
|
372
386
|
`<style>${DOSSIER_CSS}\n${EXTRA_CSS}</style></head><body><main class="wrap"><header>`,
|
|
373
387
|
`<div class="topline"><strong>${escHtml(CAMPAIGN_TITLE)}</strong><span>${escHtml(dateLabel(campaign.generatedAt))} · ${escHtml(VERSION)}</span></div>`,
|
|
374
|
-
`<div class="hero"><div><h1>
|
|
388
|
+
`<div class="hero"><div><h1>Simulation: drei Referenz-Profile im Vergleich</h1><p>${escHtml(SIMULATION_NOTICE_DE)}</p><p>${escHtml(cleanText(campaign.beforeAfterSummaryDe))}</p></div>`,
|
|
375
389
|
`<aside class="verdict"><span class="caption">Läufe je Szenario</span><strong>${campaign.runsPerScenario}</strong><p>${campaign.scenarios.length} ${plural(campaign.scenarios.length, "Szenario", "Szenarien")}, drei Profile</p></aside></div>`,
|
|
376
390
|
"</header>",
|
|
377
391
|
`<section class="section" id="gesamtvergleich"><h2>Gesamtvergleich der drei Profile</h2><div class="compare">${compare}</div>${table(t.overviewHead, t.overviewRows)}</section>`,
|
|
@@ -433,14 +447,16 @@ async function mayReplaceCampaignOutput(path) {
|
|
|
433
447
|
if (!info.isFile() || info.size > 4 * 1024 * 1024)
|
|
434
448
|
return false;
|
|
435
449
|
const head = (await readFile(path, "utf8").catch(() => "")).slice(0, 4096);
|
|
436
|
-
return head.includes(CAMPAIGN_TITLE);
|
|
450
|
+
return head.includes(CAMPAIGN_TITLE) || head.includes(LEGACY_CAMPAIGN_TITLE);
|
|
437
451
|
}
|
|
438
452
|
export async function runCampaignMode(argv) {
|
|
439
453
|
if (argv.includes("--help") || argv.includes("-h")) {
|
|
440
454
|
process.stdout.write(`${CAMPAIGN_USAGE}\n` +
|
|
441
|
-
"
|
|
442
|
-
"
|
|
443
|
-
"
|
|
455
|
+
"Simulation mit drei festen Referenz-Profilen (unvorsichtig, ungehärtet unter Nutzer-Nachdruck, gehärtet):\n" +
|
|
456
|
+
"Drei Skripte laufen durch F1 bis F5 und das Zwei-Zug-Szenario F1_FOLLOWUP gegen ein simuliertes Zielsystem.\n" +
|
|
457
|
+
"Es wird weder ein Upstream noch ein Modell noch ein fremder Agent gemessen; das Profil «gehärtet» hat keinen Prompt.\n" +
|
|
458
|
+
" --runs <n> Läufe je Szenario, 1 bis 1000 (Standard: 1). Die Läufe sind deterministisch: mehr Läufe\n" +
|
|
459
|
+
" wiederholen denselben Ablauf und vervielfachen die Summen, sie sind keine Stichprobe.\n" +
|
|
444
460
|
" --format md (Standard) oder html\n" +
|
|
445
461
|
" --out <file> in diese Datei schreiben statt ins Terminal. Eine bestehende Datei wird nur ersetzt,\n" +
|
|
446
462
|
" wenn ein früherer `campaign`-Lauf sie geschrieben hat.\n" +
|
|
@@ -448,6 +464,8 @@ export async function runCampaignMode(argv) {
|
|
|
448
464
|
" gehört zum Pro-Umfang und läuft ohne gültige Lizenz nicht.\n");
|
|
449
465
|
return 0;
|
|
450
466
|
}
|
|
467
|
+
// --flag=wert gilt wie --flag wert, auch für --license.
|
|
468
|
+
argv = expandEqualsForm(argv, new Set(["--license", "--runs", "--format", "--out"]));
|
|
451
469
|
let runs = 1;
|
|
452
470
|
let runsSeen = false;
|
|
453
471
|
let format;
|
|
@@ -472,7 +490,7 @@ export async function runCampaignMode(argv) {
|
|
|
472
490
|
runsSeen = true;
|
|
473
491
|
const value = raw !== undefined && /^-?\d+$/.test(raw) ? parseInt(raw, 10) : null;
|
|
474
492
|
if (value === null || value < 1 || value > 1000) {
|
|
475
|
-
process.stderr.write(`Error: --runs must be a positive integer (at most 1000), got "${raw ?? ""}".\n`);
|
|
493
|
+
process.stderr.write(`Error: --runs must be a positive integer (at most 1000), got "${redactSecrets(raw ?? "")}".\n`);
|
|
476
494
|
return 1;
|
|
477
495
|
}
|
|
478
496
|
runs = value;
|
|
@@ -480,7 +498,7 @@ export async function runCampaignMode(argv) {
|
|
|
480
498
|
else if (token === "--format") {
|
|
481
499
|
const v = argv[++i];
|
|
482
500
|
if (v !== "md" && v !== "html") {
|
|
483
|
-
process.stderr.write(`Error: --format must be "md" or "html", got "${v ?? ""}".\n`);
|
|
501
|
+
process.stderr.write(`Error: --format must be "md" or "html", got "${redactSecrets(v ?? "")}".\n`);
|
|
484
502
|
return 1;
|
|
485
503
|
}
|
|
486
504
|
format = v;
|
|
@@ -494,11 +512,11 @@ export async function runCampaignMode(argv) {
|
|
|
494
512
|
outPath = v;
|
|
495
513
|
}
|
|
496
514
|
else if (token.startsWith("--")) {
|
|
497
|
-
process.stderr.write(`Error: unknown option ${token}\n${CAMPAIGN_USAGE}`);
|
|
515
|
+
process.stderr.write(`Error: unknown option ${optionName(token)}\n${CAMPAIGN_USAGE}`);
|
|
498
516
|
return 1;
|
|
499
517
|
}
|
|
500
518
|
else {
|
|
501
|
-
process.stderr.write(`Error: unexpected argument ${token}\n${CAMPAIGN_USAGE}`);
|
|
519
|
+
process.stderr.write(`Error: unexpected argument ${optionName(token)}\n${CAMPAIGN_USAGE}`);
|
|
502
520
|
return 1;
|
|
503
521
|
}
|
|
504
522
|
}
|
|
@@ -524,6 +542,10 @@ export async function runCampaignMode(argv) {
|
|
|
524
542
|
process.stderr.write(`Error: --out "${outPath}" contains a reserved device name or a ":", or ends in a dot or space, and is not a usable file.\n`);
|
|
525
543
|
return 1;
|
|
526
544
|
}
|
|
545
|
+
if ((await stat(outPath).catch(() => null))?.isDirectory()) {
|
|
546
|
+
process.stderr.write(`Error: --out "${outPath}" is a folder, not a file. Give a file name, for example --out kampagne.md.\n`);
|
|
547
|
+
return 1;
|
|
548
|
+
}
|
|
527
549
|
// Vor der Kampagne prüfen: eine fremde Datei wird weder ersetzt noch erst nach dem Rechnen abgelehnt.
|
|
528
550
|
const real = await realpath(outPath).catch(() => outPath);
|
|
529
551
|
if (!(await mayReplaceCampaignOutput(real))) {
|
|
@@ -531,6 +553,8 @@ export async function runCampaignMode(argv) {
|
|
|
531
553
|
return 1;
|
|
532
554
|
}
|
|
533
555
|
}
|
|
556
|
+
if (runs > 1)
|
|
557
|
+
process.stderr.write(`Hinweis: ${RUNS_NOTICE_DE}\n`);
|
|
534
558
|
const campaign = runCampaign({ runsPerScenario: runs });
|
|
535
559
|
const text = chosen === "html" ? renderCampaignHtml(campaign) : renderCampaignMarkdown(campaign);
|
|
536
560
|
if (!outPath) {
|
package/dist/cli-args.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Gemeinsame Hilfen der Befehlszeile für report, campaign und ui (proxy hat einen eigenen, strengen Parser).
|
|
2
|
+
// Lizenzcodes (CAUDIT-..., CPCG-...) dürfen nie in Meldungen, Befehlen oder Konfigurationen auftauchen.
|
|
3
|
+
// Gezeigt werden höchstens die ersten 8 Zeichen.
|
|
4
|
+
const LICENSE_CODE = /\b(?:CAUDIT|CPCG)-[A-Za-z0-9_\-.=+/]{2,}/g;
|
|
5
|
+
const LICENSE_MARKER = /\b(?:CAUDIT|CPCG)-/;
|
|
6
|
+
/** Höchstens die ersten 8 Zeichen eines Lizenzcodes, der Rest als Auslassung. */
|
|
7
|
+
export function maskLicenseKey(key) {
|
|
8
|
+
const compact = key.replace(/\s+/g, "");
|
|
9
|
+
return compact.length <= 8 ? `${compact}…` : `${compact.slice(0, 8)}…`;
|
|
10
|
+
}
|
|
11
|
+
/** Ersetzt jeden Lizenzcode in einem Text durch seine ersten 8 Zeichen und «…». */
|
|
12
|
+
export function redactSecrets(text) {
|
|
13
|
+
return text.replace(LICENSE_CODE, (code) => maskLicenseKey(code));
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Ein einzelnes Argument der Befehlszeile für eine Meldung: Enthält es einen Lizenzcode (auch mit Zeilenumbruch
|
|
17
|
+
* oder Leerzeichen darin), wird alles ab dem Präfix durch die ersten 8 Zeichen und «…» ersetzt.
|
|
18
|
+
*/
|
|
19
|
+
export function redactArgument(token) {
|
|
20
|
+
const at = token.search(LICENSE_MARKER);
|
|
21
|
+
return at === -1 ? token : `${token.slice(0, at)}${maskLicenseKey(token.slice(at))}`;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* `--flag=wert` wird zu `--flag wert`, für die angegebenen Optionen mit Wert. Der Wert ist alles nach dem ersten `=`.
|
|
25
|
+
* Andere Token bleiben unverändert (eine unbekannte Option meldet der Parser selbst, ohne den Wert zu nennen).
|
|
26
|
+
*/
|
|
27
|
+
export function expandEqualsForm(argv, valueOptions) {
|
|
28
|
+
const out = [];
|
|
29
|
+
for (const token of argv) {
|
|
30
|
+
const eq = token.startsWith("--") ? token.indexOf("=") : -1;
|
|
31
|
+
if (eq > 2 && valueOptions.has(token.slice(0, eq)))
|
|
32
|
+
out.push(token.slice(0, eq), token.slice(eq + 1));
|
|
33
|
+
else
|
|
34
|
+
out.push(token);
|
|
35
|
+
}
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
38
|
+
/** Ein Argument für «unbekannte Option» / «unerwartetes Argument»: ein angehängter Wert und jeder Lizenzcode bleiben draussen. */
|
|
39
|
+
export function optionName(token) {
|
|
40
|
+
const eq = token.startsWith("--") ? token.indexOf("=") : -1;
|
|
41
|
+
return eq > 2 ? `${token.slice(0, eq)}=<Wert>` : redactArgument(token);
|
|
42
|
+
}
|