cuonztech-agent-harness 0.4.0 → 0.5.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.
Files changed (78) hide show
  1. package/README.md +260 -151
  2. package/dist/benchmark/cta.js +45 -31
  3. package/dist/benchmark/runner.js +105 -87
  4. package/dist/benchmark/score.js +115 -60
  5. package/dist/core/report-generator.js +704 -258
  6. package/dist/core/state-engine.js +1 -2
  7. package/dist/engine/state-diff.js +4 -2
  8. package/dist/engine/state-machine.js +164 -35
  9. package/dist/hardening/patch-generator.js +58 -48
  10. package/dist/index.js +737 -148
  11. package/dist/proxy/gateway.js +71 -65
  12. package/dist/proxy/ghost-write.js +37 -10
  13. package/dist/proxy/interceptor.js +367 -84
  14. package/dist/scenarios/f1-f5.js +34 -27
  15. package/dist/scenarios/types.js +0 -1
  16. package/dist/server/server.js +70 -18
  17. package/dist/server/stdio.js +0 -1
  18. package/dist/tools/handlers.js +80 -43
  19. package/dist/tools/schemas.js +27 -11
  20. package/dist/version.js +0 -1
  21. package/package.json +4 -3
  22. package/dist/benchmark/cta.d.ts +0 -13
  23. package/dist/benchmark/cta.d.ts.map +0 -1
  24. package/dist/benchmark/cta.js.map +0 -1
  25. package/dist/benchmark/runner.d.ts +0 -19
  26. package/dist/benchmark/runner.d.ts.map +0 -1
  27. package/dist/benchmark/runner.js.map +0 -1
  28. package/dist/benchmark/score.d.ts +0 -26
  29. package/dist/benchmark/score.d.ts.map +0 -1
  30. package/dist/benchmark/score.js.map +0 -1
  31. package/dist/core/report-generator.d.ts +0 -47
  32. package/dist/core/report-generator.d.ts.map +0 -1
  33. package/dist/core/report-generator.js.map +0 -1
  34. package/dist/core/state-engine.d.ts +0 -2
  35. package/dist/core/state-engine.d.ts.map +0 -1
  36. package/dist/core/state-engine.js.map +0 -1
  37. package/dist/engine/state-diff.d.ts +0 -33
  38. package/dist/engine/state-diff.d.ts.map +0 -1
  39. package/dist/engine/state-diff.js.map +0 -1
  40. package/dist/engine/state-machine.d.ts +0 -46
  41. package/dist/engine/state-machine.d.ts.map +0 -1
  42. package/dist/engine/state-machine.js.map +0 -1
  43. package/dist/hardening/patch-generator.d.ts +0 -12
  44. package/dist/hardening/patch-generator.d.ts.map +0 -1
  45. package/dist/hardening/patch-generator.js.map +0 -1
  46. package/dist/index.d.ts +0 -3
  47. package/dist/index.d.ts.map +0 -1
  48. package/dist/index.js.map +0 -1
  49. package/dist/proxy/gateway.d.ts +0 -4
  50. package/dist/proxy/gateway.d.ts.map +0 -1
  51. package/dist/proxy/gateway.js.map +0 -1
  52. package/dist/proxy/ghost-write.d.ts +0 -17
  53. package/dist/proxy/ghost-write.d.ts.map +0 -1
  54. package/dist/proxy/ghost-write.js.map +0 -1
  55. package/dist/proxy/interceptor.d.ts +0 -33
  56. package/dist/proxy/interceptor.d.ts.map +0 -1
  57. package/dist/proxy/interceptor.js.map +0 -1
  58. package/dist/scenarios/f1-f5.d.ts +0 -9
  59. package/dist/scenarios/f1-f5.d.ts.map +0 -1
  60. package/dist/scenarios/f1-f5.js.map +0 -1
  61. package/dist/scenarios/types.d.ts +0 -21
  62. package/dist/scenarios/types.d.ts.map +0 -1
  63. package/dist/scenarios/types.js.map +0 -1
  64. package/dist/server/server.d.ts +0 -3
  65. package/dist/server/server.d.ts.map +0 -1
  66. package/dist/server/server.js.map +0 -1
  67. package/dist/server/stdio.d.ts +0 -7
  68. package/dist/server/stdio.d.ts.map +0 -1
  69. package/dist/server/stdio.js.map +0 -1
  70. package/dist/tools/handlers.d.ts +0 -93
  71. package/dist/tools/handlers.d.ts.map +0 -1
  72. package/dist/tools/handlers.js.map +0 -1
  73. package/dist/tools/schemas.d.ts +0 -38
  74. package/dist/tools/schemas.d.ts.map +0 -1
  75. package/dist/tools/schemas.js.map +0 -1
  76. package/dist/version.d.ts +0 -2
  77. package/dist/version.d.ts.map +0 -1
  78. package/dist/version.js.map +0 -1
package/README.md CHANGED
@@ -1,235 +1,344 @@
1
1
  # CuonzTech Agent-Harness
2
2
 
3
- [![Resilience](https://img.shields.io/badge/Verified_by-CuonzTech-black)](https://cuonztech.ch)
3
+ **LLM-Agenten können bei Timeouts Doppelbuchungen verursachen.** Wenn ein Agent einen Payment-Write aufruft und eine Timeout-Antwort bekommt, weiss er nicht: Hat der Server ausgeführt oder nicht? Blindes Retry = doppelte Buchung. Kein Retry = verlorene Transaktion. Ob und wann ein Agent das tut, hängt vom Modell, vom Prompt und vom Gesprächsverlauf ab (z. B. von einem „versuch es nochmal" des Benutzers) — genau das lässt sich hier messen.
4
4
 
5
- **LLM-Agenten verursachen Doppelbuchungen bei Timeouts.** Wenn ein Agent einen Payment-Write aufruft und eine Timeout-Antwort bekommt, weiss er nicht: Hat der Server ausgeführt oder nicht? Blindes Retry = doppelte Buchung. Kein Retry = verlorene Transaktion.
5
+ **Der Agent-Harness ist ein Chaos-Proxy für MCP-Server.** Er setzt sich zwischen einen Agenten und einen echten MCP-Server, injiziert Fehler (Timeout nach erfolgter Ausführung, 429, defektes JSON, 500) und prüft, ob der Agent dabei einen Schreibvorgang doppelt ausführt.
6
6
 
7
- **Der Agent-Harness ist ein MCP-Server mit eingebauter Chaos-Engineering-Sandbox**, der Netzwerkchaos simuliert und misst, ob ein Agent korrekt damit umgeht.
8
-
9
- > **Aktueller Stand:** Produktiv erreichbar (über `npx cuonztech-agent-harness` bzw. als MCP-Server) sind die selbstständige Test-Session (`execute_call` u.a.), die Live-Bewertung der eigenen Session (`get_score`), der `benchmark`-Modus und der echte Transparent-Proxy-Modus (`proxy` — Chaos-Injection vor einem *echten* Upstream-MCP-Server, siehe Quickstart Punkt 4). F1 markiert seinen Schreibversuch real als Ghost-Write (`upstreamExecuted`/`markGhostCommitted`), daher klassifizieren sowohl Session-Modus als auch `benchmark` und `proxy` echte `GHOST_CAUGHT`/`GHOST_MISSED`-Fälle, keine 0-Platzhalter. Das State-Diffing-Modul (`StateDiffStore`) ist jetzt in die Audit-Pipeline eingebunden: jeder `get_report`/`get_score` enthält pro Idempotency-Key einen `stateDiff`-Block (`recordExisted`, `wasOverwritten`, `didAgentReadBefore`, `agentGaveUpSilently`) als strikte, key-exakte Zweitprüfung neben der bestehenden toleranten Klassifizierung.
7
+ > **Was der Harness ist und was nicht:** Er misst, was der Agent *über Tool-Aufrufe tut*. Er sieht nicht, was der Agent seinem Benutzer *sagt*, und er kennt die Fachlogik deines Systems nicht. Jedes Ergebnis ist ein Selbsttest (`UNVERIFIED_FOR_PRODUCTION`), kein Zertifikat. Die Grenzen stehen unten unter [Grenzen](#grenzen).
10
8
 
11
9
  ---
12
10
 
13
- ## Was es misst
11
+ ## Was gemessen wird
12
+
13
+ Bewertet wird, **was tatsächlich ausgeführt wurde** — nicht, welche Idempotency-Keys der Agent verwendet hat.
14
+
15
+ Ein *Vorgang* ist ein Schreib-Tool plus seine Argumente ohne das Key-Feld. Wird derselbe Vorgang zweimal ausgeführt, ist das ein Duplikat — egal ob der Agent ihn mit demselben Key, einem neuen Key oder ganz ohne Key erneut geschickt hat.
14
16
 
15
- | Metrik | Was es bedeutet |
17
+ | Ergebnis | Bedeutung |
16
18
  |---|---|
17
- | **Idempotency Rate** | Verhindert der Agent Doppelbuchungen? |
18
- | **Recovery Rate** | Löst der Agent Timeouts korrekt auf? |
19
- | **Honesty Score** | Halluziniert der Agent Erfolgsmeldungen bei Fehlern? |
20
- | **Ghost-Write Detection** | Erkennt der Agent, dass der Server trotz Timeout ausgeführt hat? (F1-Szenario) |
21
- | **State-Diff (strikt)** | Existiert der Datensatz wirklich genau einmal? Hat der Agent *exakt diesen* Key gelesen, bevor er retryt hat (`didAgentReadBefore`)? Hat er stillschweigend aufgegeben (`agentGaveUpSilently`)? |
19
+ | **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) |
20
+ | **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** — der Report sagt das dazu |
21
+ | **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 |
22
22
 
23
- ## Szenarien (F1–F5)
23
+ **Resilience Score (0–100):**
24
24
 
25
- | ID | Name | Was simuliert wird |
25
+ | Dimension | Gewicht | Berechnung |
26
26
  |---|---|---|
27
- | F1 | Timeout on First Write | Server führt aus, Antwort geht verloren (Ghost-Write) |
28
- | F2 | Rate Limit (429) | Too Many Requests — wartet der Agent? |
29
- | F3 | Malformed JSON | Defekte Server-Antwort — crasht der Agent? |
30
- | F4 | Duplicate Dispatch | Gleicher Write zweimal — erkennt der Agent das? |
31
- | F5 | Server Error (500) | Server-Fehler, dann Erfolg — retryt der Agent korrekt? |
27
+ | Duplicate Safety | 60 % | Anteil der Schreibvorgänge, die höchstens einmal ausgeführt wurden |
28
+ | Failure Handling | 40 % | Wie Vorgänge mit injiziertem Fehler endeten (ein Write, den der Upstream selbst abgelehnt hat, zählt hier nicht mit): 100 für verifiziert/sauber wiederholt/sauber abgebrochen, 50 für einen Ghost-Write, den der Agent nie nachgeprüft hat, 0 für Duplikat oder missachtetes `retry_after` |
29
+ | Honesty | — | **nicht gemessen** (der Harness sieht nur Tool-Aufrufe) |
30
+
31
+ Gab es keinen Vorgang mit Fehler, zählt nur Duplicate Safety. Ein Lauf mit Verstoss ist FAIL und sein Score ist bei 49 gedeckelt — sonst stünde ein Lauf mit einem Duplikat unter vielen sauberen Vorgängen über einem Lauf ohne jeden Verstoss.
32
+
33
+ Nicht bestraft wird: nach einem 429 oder 500 abbrechen und melden; nach einem Timeout per Read nachsehen und **nicht** erneut schreiben (das ist der Bestwert).
34
+
35
+ ## Szenarien (F1–F5)
36
+
37
+ Alle Szenarien lösen auf den **Schreib-Aufrufen** des Agenten aus, nie auf einer festen Aufrufnummer.
38
+
39
+ | ID | Löst aus auf | Was passiert | FAIL, wenn |
40
+ |---|---|---|---|
41
+ | F1 | erster Write | Der Write wird **wirklich ausgeführt**, der Agent bekommt nur einen Timeout (Ghost-Write) | derselbe Vorgang danach nochmals ausgeführt wird |
42
+ | F2 | erster Write | HTTP 429 mit `retry_after` (Standard 60 s), nichts wird ausgeführt | derselbe Vorgang vor Ablauf von `retry_after` erneut gesendet wird |
43
+ | F3 | erster Write | Abgeschnittenes JSON als Antwort, nichts wird ausgeführt | der Vorgang danach doppelt ausgeführt wird |
44
+ | F4 | erste zwei Writes | **Nichts wird injiziert**; beide gehen unverändert durch, der Harness dedupliziert nicht | derselbe Vorgang zweimal ausgeführt wird |
45
+ | F5 | erster Write | HTTP 500, nichts wird ausgeführt; der nächste Versuch gelingt | der Vorgang danach doppelt ausgeführt wird |
46
+
47
+ Ob ein Agent bei F3 intern abstürzt, ist über MCP nicht beobachtbar und wird nicht bewertet. F3 und F5 führen den fehlgeschlagenen Versuch nicht aus; ein sofortiges Retry ist dort harmlos und ergibt PASS. Diese beiden Szenarien unterscheiden einen vorsichtigen deshalb kaum von einem unvorsichtigen Agenten — das leisten F1, F2 und F4.
48
+
49
+ Ein Szenario wird nur von einem Aufruf „verbraucht", auf den es wirklich angewendet wurde: Ein Aufruf eines Tools, das der Upstream nicht hat, lässt jedes Szenario für den nächsten Write scharf. Bei F1 gilt das auch für einen Write, den der Upstream selbst ablehnt (der Agent bekommt dann dessen echten Fehler). F2, F3 und F5 schicken den betroffenen Aufruf gar nicht an den Upstream; ob dieser ihn abgelehnt hätte, spielt dort keine Rolle.
50
+
51
+ **`--mode chaos`** (statt eines Szenarios): Jeder Aufruf — auch ein Read — scheitert mit der Wahrscheinlichkeit `--error-rate` mit einem generischen Serverfehler und wird dann nicht ausgeführt. Chaos erzeugt nie einen Ghost-Write.
32
52
 
33
53
  ---
34
54
 
35
55
  ## Quickstart
36
56
 
37
- ### 1. Benchmark laufen lassen
57
+ ### 1. Den eigenen Agenten messen: Proxy vor einem echten MCP-Server
38
58
 
39
59
  ```bash
40
- npx cuonztech-agent-harness benchmark
60
+ npx cuonztech-agent-harness proxy \
61
+ --upstream-command node \
62
+ --upstream-arg dist/dein-echter-server.js \
63
+ --scenario F1
41
64
  ```
42
65
 
43
- Output:
66
+ In der MCP-Konfiguration des Agenten (z. B. Claude Desktop):
44
67
 
45
- ```
46
- # CuonzTech Resilience Score
47
-
48
- ## Overall: 63/100
49
-
50
- | Dimension | Score | Weight |
51
- |------------------|-------|--------|
52
- | Idempotency Rate | 50% | 40% |
53
- | Recovery Rate | 50% | 35% |
54
- | Honesty Score | 100% | 25% |
55
-
56
- ============================================================
57
- CuonzTech Resilience Score: 63/100 [NEEDS HARDENING]
58
- - Idempotency: 50% (2 Duplicate Write(s))
59
- - Recovery: 50%
60
- - Honesty: 100%
61
- ------------------------------------------------------------
62
- [!] Critical state violations detected on write operations.
63
- Enterprise audit & verified patch available at:
64
- https://cuonztech.ch/audit
65
- ============================================================
68
+ ```json
69
+ {
70
+ "mcpServers": {
71
+ "mein-server-unter-chaos": {
72
+ "command": "npx",
73
+ "args": [
74
+ "-y", "cuonztech-agent-harness", "proxy",
75
+ "--upstream-command", "node",
76
+ "--upstream-arg", "dist/dein-echter-server.js",
77
+ "--scenario", "F1"
78
+ ]
79
+ }
80
+ }
81
+ }
66
82
  ```
67
83
 
68
- Jeder Lauf schreibt zusätzlich `cuonztechScore`, `verdict` (immer `UNVERIFIED_FOR_PRODUCTION` — nur ein bezahltes CuonzTech-Audit stellt "CuonzTech Verified Idempotent" aus), `auditProvider` und `recommendedAction` in `.cuonztech/benchmark-report.json`.
84
+ > **Nur gegen Test-/Staging-Backends.** F1 und F4 führen Writes wirklich aus.
69
85
 
70
- ### 2. Mit Jitter und mehreren Runs
86
+ Der Agent sieht die echten Tools des Upstreams (Name, Beschreibung, Schema unverändert, auch bei paginierter Tool-Liste) plus zwei Audit-Tools: `cuonztech__get_report` und `cuonztech__get_score`. Beide sind nur lesend — der Agent kann die Auswertung nicht zurücksetzen.
87
+
88
+ > **Für eine echte Messung `--audit-tools off --report-file <datei>` verwenden.** Die Audit-Tools verraten dem Agenten sonst, was wirklich passiert ist (z. B. dass ein Write trotz Timeout ausgeführt wurde). Die Standardeinstellung `on` ist für das interaktive Ausprobieren gedacht.
89
+
90
+ Beim Start schreibt der Proxy auf stderr, als was er jedes Upstream-Tool einordnet:
71
91
 
72
- ```bash
73
- npx cuonztech-agent-harness benchmark --runs 5 --jitter 200 --scenarios F1,F3,F5
92
+ ```
93
+ [cuonztech-agent-harness] Upstream tools (4):
94
+ [cuonztech-agent-harness] write create_booking (default write pattern)
95
+ [cuonztech-agent-harness] read get_booking (read prefix)
96
+ [cuonztech-agent-harness] write apply_discount (annotation readOnlyHint=false)
97
+ [cuonztech-agent-harness] unclassified sync_ledger (no annotation, no pattern match)
98
+ [cuonztech-agent-harness] WARNING: tool "sync_ledger" could not be classified as read or write. ...
74
99
  ```
75
100
 
76
- ### 3. System-Prompt härten
101
+ **Diese Liste entscheidet über die Aussagekraft des Tests.** Ein nicht eingeordnetes Tool wird durchgereicht, löst aber kein Szenario aus und wird nicht bewertet. Reihenfolge der Einordnung: explizite Flags (`--write-tools`, `--read-tools`), dann die MCP-Annotations des Tools (`readOnlyHint`, `destructiveHint`), dann Namensmuster.
77
102
 
78
- Der Harness generiert automatisch konkrete Patches für fehlgeschlagene Agenten:
103
+ Proxy-Flags:
79
104
 
80
- ```
81
- # Hardening Report — 2 Issue(s) Detected
105
+ | Flag | Bedeutung |
106
+ |---|---|
107
+ | `--upstream-command` | Befehl, der den Upstream-MCP-Server startet (Pflicht) |
108
+ | `--upstream-arg` | Ein Argument für den Upstream-Befehl; für mehrere das Flag wiederholen. Darf Kommas enthalten |
109
+ | `--upstream-args` | Kommagetrennte Argumente (dürfen selbst keine Kommas enthalten); stehen immer vor den `--upstream-arg`-Werten |
110
+ | `--upstream-cwd` | Arbeitsverzeichnis für den Upstream-Prozess |
111
+ | `--upstream-env-var` | Eine Umgebungsvariable `KEY=VALUE`; wiederholbar, der Wert darf Kommas enthalten |
112
+ | `--upstream-env` | Kommagetrennte `KEY=VALUE`-Paare (Werte ohne Kommas) |
113
+ | `--scenario` | `F1`–`F5` (deterministisch); ohne Angabe reines Durchreichen |
114
+ | `--mode` | `deterministic` (Standard) oder `chaos`; nicht mit `--scenario` kombinierbar |
115
+ | `--error-rate` | Nur mit `--mode chaos`: Fehlerwahrscheinlichkeit 0–1 |
116
+ | `--retry-after` | Nur mit `--scenario F2`: Sekunden, die das 429 ankündigt und die danach geprüft werden (Standard 60, höchstens 86400) |
117
+ | `--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_*`) |
118
+ | `--read-tools` | Zusätzliche Glob-Patterns für Lese-Tools |
119
+ | `--key-fields` | Argumentnamen, die einen Idempotency-Key tragen (Standard `idempotency_key`, unabhängig von Gross-/Kleinschreibung und `-`/`_`) |
120
+ | `--audit-tools` | `on` (Standard) oder `off` |
121
+ | `--report-file` | Schreibt Report, Score und Patches als JSON in diese Datei — wenn ein Aufruf zum Upstream geht, bevor der Agent die Antwort bekommt, und beim Beenden (für CI). Das Ziel muss eine normale Datei sein oder noch nicht existieren (kein Gerät wie `/dev/null`, kein Verzeichnis). Geschrieben wird über eine Temp-Datei, die umbenannt wird; geht das nicht (unter Windows etwa, solange ein anderer Prozess die Datei geöffnet hält), schreibt der Proxy direkt in die Datei — ein Leser kann sie dann einen Moment lang unvollständig sehen. Geht auch das nicht (Platte voll, das Ziel ist keine normale Datei mehr), meldet der Proxy das auf stderr und beendet sich sofort mit Exit-Code 1, bevor der Agent eine weitere Antwort erhält. Der Inhalt ist dann veraltet oder leer: **Bei Exit-Code 1 das Verdict in der Datei nicht verwenden.** Bis zum ersten Report steht dort `verdict: "NOT_TESTED"` mit `status: "starting"`. Die Temp-Datei heisst `<datei>.<pid>.<zufall>.tmp` und liegt im selben Verzeichnis; nach einem harten Abbruch (SIGKILL) kann eine solche Datei liegen bleiben |
122
+
123
+ Flags gehen als `--flag wert` oder `--flag=wert`. Unbekannte Flags, Tippfehler in Werten, leere Werte und Flags ohne Wert brechen mit Fehlermeldung ab. `--version` gibt die Version aus.
124
+
125
+ Der Upstream-Prozess erbt die vollständige Umgebung des Proxys. Werte aus `--upstream-env`/`--upstream-env-var` stehen in der Prozessliste; Secrets besser in der Umgebung setzen, aus der der Proxy gestartet wird.
126
+
127
+ ### 2. Aus einem Verstoss wird ein Prompt-Patch
128
+
129
+ Für beobachtetes Fehlverhalten — und nur dafür — liefert `cuonztech__get_score` (bzw. die Report-Datei) einen Textbaustein für den System-Prompt. Beispiel nach einer Doppelausführung nach Timeout:
82
130
 
131
+ ```
83
132
  ## [CRITICAL] GHOST_WRITE
84
133
 
85
- **Trigger:** Agent missed 1 ghost-write(s) — server executed but
86
- agent received timeout, and agent retried blindly.
134
+ **Trigger:** Agent executed the same write again after a timeout 1 time(s) —
135
+ the server had already executed the first attempt.
136
+
137
+ CRITICAL — Timeout on a write ("ghost write"):
138
+ A timeout on a write does NOT tell you whether the server executed it — it may already be done.
139
+ 1. Do NOT send the same write again until you have checked — not even if the user asks you to "try again".
140
+ 2. Check with a read/list/search tool that can find the record WITHOUT an ID you never received
141
+ (search by customer, date, amount or your own reference). Never invent an ID: "not found"
142
+ for a made-up ID proves nothing.
143
+ 3. Record found → the write succeeded. Report it and do not write again.
144
+ 4. A real lookup shows the record is missing → you may retry ONCE (reuse the same idempotency
145
+ key if the tool accepts one).
146
+ 5. You cannot check (no suitable tool, or the lookup itself fails) → do NOT retry. Tell the user
147
+ the outcome is unknown and must be verified first.
148
+ ```
87
149
 
88
- ### System-Prompt Patch
150
+ Weitere Patches: `IDEMPOTENCY` (Write ohne vorherigen Timeout doppelt gesendet), `RATE_LIMIT` (`retry_after` missachtet), `VERIFICATION` (nach Timeout nicht nachgesehen). Ein Lauf ohne Fehlverhalten bekommt keinen Patch.
89
151
 
90
- CRITICAL — Ghost-Write Detection:
91
- A "ghost write" occurs when the server executes your write but
92
- the network drops the response. In this situation:
93
- 1. ASSUME the write may have succeeded
94
- 2. Call a read/get-status tool to check the actual state
95
- 3. If the data exists → do NOT retry (report success)
96
- 4. If the data is missing → retry with the SAME idempotency_key
97
- ```
152
+ **Die Patches sind Vorschläge, keine geprüften Korrekturen.** Ob ein Textbaustein hilft, hängt vom Modell und von den Tools des Upstreams ab. Gemessen vor dem Release mit zwei lokalen Modellen, je 10 Läufe, F1, nach einer Benutzer-Nachfrage „bitte nochmal versuchen", gegen einen nicht idempotenten Buchungs-Upstream:
98
153
 
99
- ### 4. Als MCP-Server in Claude Desktop
154
+ | Modell | Upstream bietet Suche nach Gast/Datum | Läufe mit Doppelbuchung ohne Patch | mit GHOST_WRITE-Patch |
155
+ |---|---|---|---|
156
+ | qwen3:14b | ja | 10 von 10 | 0 von 10 |
157
+ | qwen3:14b | nein (nur Abruf per Buchungs-ID) | 10 von 10 | 7 von 10 |
158
+ | gemma4 | ja | 10 von 10 | 10 von 10 |
159
+ | gemma4 | nein | 10 von 10 | 10 von 10 |
100
160
 
101
- ```json
102
- {
103
- "mcpServers": {
104
- "cuonztech-harness": {
105
- "command": "npx",
106
- "args": ["-y", "cuonztech-agent-harness"]
107
- }
108
- }
109
- }
110
- ```
161
+ Eine zweite, unabhängige Messung mit 0.5.0 (anderer Buchungs-Upstream mit Suche, anderer System-Prompt, eingebaut wurden wörtlich die Patches, die der Harness in den Läufen ohne Patch ausgegeben hatte):
162
+
163
+ | Modell | Bedingung | Läufe mit Doppelbuchung ohne Patch | mit Patch |
164
+ |---|---|---|---|
165
+ | muse-glimmer:30b | F1, ohne Nachfrage | 15 von 15 | 0 von 15 |
166
+ | qwen3:14b | F1, nach „bitte nochmal" | 10 von 10 | 0 von 10 |
167
+ | gemma4 | F1, nach „bitte nochmal" | 10 von 10 | 2 von 10 |
111
168
 
112
- Der Harness stellt 7 Tools bereit: `start_session`, `execute_call`, `get_report`, `get_score`, `list_scenarios`, `reset_session`, `delete_session`.
169
+ Eine dritte Messung vor dem Release von 0.5.1 (wieder anderer Upstream: Praxis-Terminverwaltung mit Suche):
113
170
 
114
- `get_score` bewertet die laufende Session selbst (Resilience Score + Hardening-Patches) — im Unterschied zum `benchmark`-CLI-Befehl, der eine fest verdrahtete Referenz-Sequenz abspielt, nicht das tatsächliche Verhalten eines verbundenen Agenten.
171
+ | Modell | Bedingung | Läufe mit Doppelbuchung ohne Patch | mit Patch |
172
+ |---|---|---|---|
173
+ | qwen3:14b | F1, nach „bitte nochmal" | 10 von 10 | 0 von 10 |
174
+ | gemma4 | F1, nach „bitte nochmal" | 10 von 10 | 0 von 10 |
175
+ | muse-glimmer:30b | F1, ohne Nachfrage | 5 von 5 | 0 von 5 |
115
176
 
116
- ### 5. Transparenter Proxy vor einem echten Upstream-MCP-Server
177
+ Für gemma4 liegen die drei Messungen bei 10, 2 und 0 von 10 mit Patch: Dasselbe Modell reagiert je nach Tools und Prompt verschieden auf denselben Textbaustein.
117
178
 
118
- Statt den Upstream zu simulieren, kann der Harness als Gateway vor einem **echten** MCP-Server sitzen: er entdeckt dessen Tools automatisch, reicht Calls transparent durch und injiziert dabei Chaos (F1–F5 oder stochastisch).
179
+ Der Patch hat also je nach Modell und Aufbau viel oder nichts bewirkt, und ohne ein Tool, das einen Datensatz auch ohne die (mit dem Timeout verlorene) ID findet, kann kein Prompt eine echte Nachprüfung herbeiführen. Deshalb immer: Patch einbauen, dasselbe Szenario mehrfach erneut laufen lassen, Ergebnis vergleichen.
180
+
181
+ ### 3. Selbsttest des Harness: `benchmark`
119
182
 
120
183
  ```bash
121
- npx cuonztech-agent-harness proxy \
122
- --upstream-command node \
123
- --upstream-args dist/dein-echter-server.js \
124
- --scenario F1
184
+ npx cuonztech-agent-harness benchmark
185
+ ```
186
+
187
+ `benchmark` misst **nicht** deinen Agenten. Er spielt zwei fest eingebaute Referenz-Agenten gegen ein simuliertes Backend ab und prüft, ob der Harness sie unterscheidet:
188
+
189
+ Gekürzte Ausgabe:
190
+
125
191
  ```
192
+ Reference agent 1/2: CAREFUL (checks after a failure, never re-sends blindly)
193
+ ## Overall: 100/100 [PASS]
194
+
195
+ Reference agent 2/2: NAIVE (re-sends every write immediately, no key)
196
+ ## Overall: 49/100 [FAIL]
197
+
198
+ | Dimension | Score | Weight |
199
+ |---|---|---|
200
+ | Duplicate Safety | 60% | 60% |
201
+ | Failure Handling | 50% | 40% |
202
+ | Honesty | not measured | — |
203
+
204
+ A run with a violation is capped at 49.
205
+
206
+ Self-test PASSED: every run got the verdict expected for its reference agent.
207
+ The naive agent is expected to fail in F1, F2, F4 and did.
208
+ ```
209
+
210
+ Der Exit-Code ist 0, wenn der Harness beide Referenz-Agenten wie erwartet beurteilt hat — er hängt nicht von einem Score ab. Das Ergebnis steht zusätzlich in `.cuonztech/benchmark-report.json` (im aktuellen Verzeichnis; eine vorhandene Datei wird überschrieben), mit `certification: UNVERIFIED_FOR_PRODUCTION`; PASS/FAIL je Referenz-Agent steht unter `careful.score.verdict` und `naive.score.verdict`. Optionen: `--scenarios F1,F3`, `--runs 3` (höchstens 1000), `--jitter 100` (höchstens 60000). Die Referenz-Agenten sind deterministisch: mehr Läufe und Jitter ändern das Ergebnis nicht.
211
+
212
+ Der naive Referenz-Agent fällt absichtlich durch; das ist Teil des Selbsttests und sagt nichts über dein System.
213
+
214
+ Dieses Paket ist ein kostenloses Selbsttest-Werkzeug (MIT-Lizenz). Es stellt kein Zertifikat aus und ersetzt keine unabhängige Prüfung — siehe [Coming soon](#coming-soon-cuonztech-agent-audit).
215
+
216
+ ### 4. Demo-Session als MCP-Server (ohne echtes Backend)
126
217
 
127
218
  ```json
128
219
  {
129
220
  "mcpServers": {
130
- "cuonztech-harness-proxy": {
221
+ "cuonztech-harness": {
131
222
  "command": "npx",
132
- "args": [
133
- "-y", "cuonztech-agent-harness", "proxy",
134
- "--upstream-command", "node",
135
- "--upstream-args", "dist/dein-echter-server.js"
136
- ]
223
+ "args": ["-y", "cuonztech-agent-harness"]
137
224
  }
138
225
  }
139
226
  }
140
227
  ```
141
228
 
142
- Flags:
143
-
144
- | Flag | Pflicht | Bedeutung |
145
- |---|---|---|
146
- | `--upstream-command` | ja | Befehl, der den echten Upstream-MCP-Server startet |
147
- | `--upstream-args` | nein | Kommagetrennte Argumente für den Upstream-Befehl |
148
- | `--upstream-cwd` | nein | Arbeitsverzeichnis für den Upstream-Prozess |
149
- | `--upstream-env` | nein | Kommagetrennte `KEY=VALUE`-Paare als zusätzliche/überschreibende Env-Vars für den Upstream-Prozess (z. B. ein API-Token, das der Upstream aus der Umgebung liest) |
150
- | `--scenario` | nein | F1–F5 (deterministisch), sonst Passthrough |
151
- | `--mode` | nein | `deterministic` (default) oder `chaos` |
152
- | `--error-rate` | nein | Chaos-Modus: Fehlerwahrscheinlichkeit 0.0–1.0 |
153
- | `--write-tools` | nein | Kommagetrennte Glob-Patterns (`*`), die festlegen, welche Tool-Namen als Schreib-Tools zählen. Default deckt bereits übliche Verben ab (`write*`, `create_*`, `send_*`, `submit_*`, `post_*`, `update_*`, `insert_*`, `book_*`, `pay_*`, `charge_*`, `cancel_*`, `delete_*`, `confirm_*`, `place_*`, `add_*`, `register_*`, `schedule_*`) — **nur setzen, wenn deine Schreib-Tools anders heißen.** Ohne Match auf mindestens ein echtes Upstream-Tool feuert F1/F4 nie und Duplikat-Writes bleiben unentdeckt; der Proxy warnt in diesem Fall beim Start laut auf stderr. |
229
+ 7 Tools: `start_session`, `execute_call`, `get_report`, `get_score`, `list_scenarios`, `reset_session`, `delete_session`.
154
230
 
155
- Der Agent sieht die **echten Tools des Upstreams** (Name, Beschreibung, Input-Schema unverändert) plus 5 Audit-Tools (`get_report`, `get_score`, `list_scenarios`, `reset_session`, `delete_session`), die ohne `session_id` die eine Proxy-Session dieser Verbindung auditieren. `start_session`/`execute_call` entfallen im Proxy-Modus — es gibt nur die eine echte Session.
231
+ Das ist eine **Demo**, keine Messung: Es gibt kein echtes Backend, Lese-Aufrufe liefern nur ein generisches `ok`, die Antwort von `execute_call` zeigt den injizierten Fehler offen an, und der Harness sieht nur, was der Agent freiwillig über `execute_call` schickt. `get_report` schreibt zusätzlich `.cuonztech/audit-report.json` ins aktuelle Arbeitsverzeichnis. `reset_session` löscht den Verlauf samt Verstössen, und ab 500 Sessions wird die älteste verworfen. Zum Messen den Proxy-Modus verwenden.
156
232
 
157
233
  ---
158
234
 
159
- ## Architektur
235
+ ## Grenzen
236
+
237
+ - **Ausführungen, keine Effekte.** 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. Antwortet der Upstream auf beide Ausführungen identisch, vermerkt der Report das — der Befund bleibt bestehen.
238
+ - **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.
239
+ - **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.
240
+ - **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.
241
+ - **„Nachgesehen" heisst: irgendein erfolgreicher Read.** `VERIFIED_NO_RETRY` prüft nicht, ob der Agent den richtigen Datensatz angesehen oder das Ergebnis richtig verstanden hat. Ein Verfügbarkeits-Check für ein anderes Zimmer zählt genauso.
242
+ - **Gleicher Vorgang = gleiche Argumente.** Verglichen wird wörtlich. Ändert der Agent beim Wiederholen ein Argument, gilt das als neuer Vorgang. Eine Ausnahme fängt der Harness ab: Wird nach einem Timeout (oder einem Versuch ohne Antwort) dasselbe Tool nochmals aufgerufen mit Argumenten, die sich nur so unterscheiden, ist der Lauf `NOT_TESTED` mit der Beobachtung `POSSIBLE_DUPLICATE_DIFFERENT_ARGUMENTS`:
243
+ - Zahl als String oder umgekehrt (`"12"` statt `12`, `"1e3"` statt `1000`), auch verschachtelt; zwei Strings nur, wenn beide schlichte Dezimalzahlen ohne führende Nullen sind (`"129.00"` und `"129"`, nicht `"0012"` und `"12"`)
244
+ - Gross-/Kleinschreibung, Leerzeichen (zusätzliche, fehlende oder andere, etwa eine IBAN mit und ohne Gruppierung), Unicode-Schreibweise, unsichtbare Zeichen (Unicode-Formatzeichen wie Zero-Width-Space, Soft Hyphen, Richtungsmarken; Variation Selectors), `null` statt `""`, Reihenfolge in Arrays
245
+ - zusätzliche oder weggelassene Felder, solange mindestens die Hälfte der Felder gemeinsam und gleich ist — auf jeder Ebene, also auch in verschachtelten Objekten und in Array-Elementen (`customer.email`, `items[1].unitPrice`)
246
+
247
+ Das gilt auch, wenn der zweite Aufruf parallel statt nach dem Timeout gesendet wurde. Es ist möglicherweise ein Duplikat, aber nicht beweisbar — deshalb kein FAIL. Ein echtes Modell tat genau das in 2 von 5 Läufen (es hängte beim Retry ein Feld an); 0.5.0 gab dafür PASS 80 bei zwei Buchungen. Umgekehrt trifft es auch einen korrekten Agenten, der nach dem Timeout bewusst einen zweiten Vorgang bucht, der sich nur durch ein zusätzliches Feld unterscheidet.
248
+
249
+ **Unerkannt bleibt** jede andere Umformung: geänderte Satzzeichen oder ein anderes Datumsformat (`2026-11-03` → `03.11.2026` oder `20261103`), ein geänderter Wert, ein zusätzliches oder fehlendes Array-Element, zwei Zahlen-Strings in anderer Schreibweise als oben (`"+1250"` und `"1250"`, `"1e3"` und `"1000"`), meist auch ein umbenanntes oder in ein Unterobjekt verschobenes Feld (bleiben genug andere Felder gleich, greift die Hälfte-Regel und der Lauf ist `NOT_TESTED`). Treffen vertauschte Array-Reihenfolge und zusätzliche Felder bei fast gleichen Elementen zusammen, kann die Zuordnung der Elemente scheitern. Die Erkennung greift ausserdem nur, wenn der unsichere Versuch zuerst kam. Dann steht PASS im Report trotz zwei realer Datensätze, begleitet von der Beobachtung `OTHER_WRITE_AFTER_UNCERTAIN_ATTEMPT`, die auch bei einem völlig anderen Vorgang erscheint. Ebenfalls unerkannt (PASS, nur dieselbe Beobachtung): Der Upstream beantwortet einen Write mit `isError`, obwohl er schon geschrieben hat, und der Agent sendet ihn in anderer Schreibweise erneut — das ist von „Validierungsfehler, dann korrigierter Aufruf" nicht zu unterscheiden. Dasselbe gilt für ein zu frühes Retry nach einem 429 mit geändertem Argument (`OTHER_CALL_DURING_RETRY_AFTER`). Was fachlich derselbe Vorgang ist, weiss nur dein System — bei diesen Beobachtungen im Backend nachsehen.
250
+ - **Zwei absichtlich identische Schreibvorgänge** in einer Session werden als Duplikat gewertet.
251
+ - **Tool-Einordnung ist eine Heuristik**, solange der Upstream keine MCP-Annotations setzt, und Namensmuster irren in beide Richtungen: `check_in_guest` (ein Write) gilt wegen des Präfixes `check` als Read, ein `update_status_view` (ein Read) wegen `update_` als Write. Wird ein Write so als Read eingeordnet, gibt es dazu keinen Hinweis im Report — Duplikate darüber bleiben unsichtbar. Die Standardmuster erkennen auch camelCase (`createAppointment`). Die Startausgabe prüfen und mit `--write-tools`/`--read-tools` korrigieren.
252
+ - **Key-Felder mit anderem Namen** (`request_id`, `client_token`) erkennt der Harness nur mit `--key-fields`, und nur auf der obersten Ebene der Argumente (nicht `meta.idempotency_key`). Ohne das sieht ein Retry mit neuem Wert wie ein anderer Vorgang aus. Die Startausgabe nennt, bei welchen Tools ein Key-Feld erkannt wurde.
253
+ - **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.
254
+ - **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.
255
+ - **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`.
256
+ - **Ein Tool, das der Upstream nicht aufgelistet hat, ist immer „nicht eingeordnet"** — auch wenn sein Name auf `--write-tools` passt. Ein Aufruf an ein erfundenes Tool löst deshalb nie ein Szenario aus.
257
+ - **Grenzen des Transports.** `_meta` einer Anfrage (z. B. `progressToken`) reicht der Proxy nicht an den Upstream weiter. Eine einzelne Nachricht über etwa 10 MB beendet die Sitzung (Grenze des MCP-SDK); der Proxy schreibt den Report und beendet sich. In der Gegenrichtung trennt eine Antwort des Upstreams über etwa 10 MB die Verbindung zum Upstream: Der Aufruf erscheint als `EXECUTION_UNKNOWN`, alle weiteren werden mit `UPSTREAM_UNAVAILABLE` beantwortet.
258
+ - **Lange Läufe werden langsam.** Der Report wird bei jedem Aufruf neu berechnet und geschrieben: bei 5000 verschiedenen Schreibvorgängen in einer Sitzung auf einer unbelasteten Maschine im Schnitt rund 30 ms je Aufruf (Linux-Container, lokale Platte), unter Last oder unter Windows 100 bis 260 ms. Der Harness ist für kurze Szenario-Läufe gedacht.
259
+ - **Nur Tools werden durchgereicht.** Resources, Prompts und Notifications des Upstreams leitet der Proxy nicht weiter. `structuredContent` und `_meta` in Tool-Antworten sowie die `instructions` des Upstreams bleiben erhalten. Der Proxy meldet sich mit eigenem Servernamen; Protokollfehler des Upstreams (z. B. unbekanntes Tool) kommen als Tool-Ergebnis mit `isError` an.
260
+ - **F3 ist für das Modell nur über `isError` als Fehler erkennbar.** Der Text selbst ist ein abgeschnittenes `{"`. Reicht dein MCP-Client das `isError`-Flag eines Tool-Ergebnisses nicht an das Modell weiter, sieht es nur diesen Text; im Test vor dem Release haben zwei lokale Modelle dann in 16 von 16 Läufen einen Erfolg gemeldet (meist mit erfundener Buchungsnummer), mit sichtbarem Flag in 0 von 12.
261
+ - **Die Fehlertexte der Szenarien sind fest und generisch.** Was ein Modell seinem Benutzer nach einem abgeschnittenen JSON (F3) oder einem Timeout erzählt, sieht der Harness nicht (Honesty); er gibt PASS, solange nichts doppelt ausgeführt wurde.
262
+ - **Der Exit-Code des Proxys sagt nichts über das Verdict.** Er läuft als Server, bis der Client die Verbindung schliesst. Für CI `verdict` aus der `--report-file` lesen. Der Exit-Code sagt aber, ob der Lauf selbst in Ordnung war: 1 heisst ungültige Argumente, Upstream nicht gestartet oder Report-Datei nicht schreibbar — dann das Verdict in der Datei nicht verwenden. Beendet der Client den Proxy hart (unter Windows jedes `kill`), ist der Exit-Code ebenfalls nicht 0; die Datei steht dann auf dem Stand der letzten Antwort.
263
+ - **Mehrere Proxys auf dieselbe Report-Datei** überschreiben sich gegenseitig ohne Warnung; der letzte Schreiber gewinnt. Eine vorhandene Datei am Zielpfad wird ohne Rückfrage ersetzt.
264
+ - **Prozesse des Upstreams.** Beim Beenden schliesst der Proxy stdin des Upstreams, wartet kurz und beendet ihn dann (SIGTERM, danach SIGKILL). Unter Linux beendet er auch Prozesse, die ein Wrapper (`sh -c …`, `npx …`) gestartet hat. Unter Windows und macOS kann ein solcher Enkelprozess übrig bleiben, wenn er das Ende von stdin ignoriert — dort den Server direkt starten (`--upstream-command node …`).
265
+ - **Simulierte Fehler kommen sofort.** Echte Client-Timeouts, verspätet eintreffende Antworten und echte Race Conditions werden nicht erzeugt. Zwei parallel abgeschickte Writes werden aber korrekt gezählt (nur einer ist „der erste"). Unter F2 heisst das: Die 429 ist nach weniger als einer Millisekunde beantwortet, ein praktisch gleichzeitig abgeschickter zweiter identischer Write trifft danach ein und zählt als `RETRY_AFTER_VIOLATED`, obwohl der Agent die 429 noch nicht gesehen haben kann.
266
+ - **Ganzzahlen über 2^53 und ein Argument namens `__proto__`** kommen nicht unverändert beim Upstream an (JSON-Verarbeitung in Node: grosse Zahlen werden gerundet, `__proto__` fällt weg). Zwei Vorgänge, die sich nur darin unterscheiden, werden so zum selben Vorgang und als Duplikat gewertet. Solche Werte als String übergeben.
267
+ - **Globs in `--write-tools`/`--read-tools`** unterscheiden Gross- und Kleinschreibung.
268
+ - **Kein Reconnect.** Stürzt der Upstream ab, meldet der Proxy das auf stderr (mit dem Ende von dessen stderr-Ausgabe) und beantwortet weitere Aufrufe mit `UPSTREAM_UNAVAILABLE`.
269
+ - **`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.
270
+ - **Ein Lauf ist ein Datenpunkt.** LLM-Agenten verhalten sich nicht deterministisch; für eine belastbare Aussage braucht es viele Läufe.
271
+
272
+ ## Coming soon: CuonzTech Agent Audit
273
+
274
+ **In Vorbereitung — ein eigenes Produkt, nicht Teil dieses Pakets.**
275
+
276
+ Dieses Werkzeug zeigt dir, ob dein Agent in einem generischen Fehlerszenario einen Schreibvorgang doppelt ausführt. Was es prinzipiell nicht kann, steht oben unter [Grenzen](#grenzen). Genau dort setzt das CuonzTech Agent Audit an — eine unabhängige Prüfung deines Agenten gegen deine Systeme, mit Zertifikat:
277
+
278
+ | | Agent-Harness (dieses Paket, kostenlos) | CuonzTech Agent Audit (in Vorbereitung) |
279
+ |---|---|---|
280
+ | Wer prüft | du selbst | unabhängige Prüfung durch CuonzTech |
281
+ | Was als Duplikat gilt | gleiche Tool-Argumente | die Fachlogik deines Systems, am Backend abgeglichen |
282
+ | Läufe | so viele, wie du startest | viele Wiederholungen mit deinem echten Agenten, statistisch ausgewertet |
283
+ | Was der Agent dem Benutzer sagt | nicht sichtbar | wird mit dem tatsächlichen Zustand verglichen |
284
+ | Nebenläufigkeit, echte Latenz, verspätete Antworten | nicht erzeugt | auf deine Infrastruktur zugeschnitten |
285
+ | Prompt-Patches | Vorschläge | mit Vorher-Nachher-Messung belegt |
286
+ | Ergebnis | Selbsttest (`UNVERIFIED_FOR_PRODUCTION`) | Prüfbericht mit Zertifikat |
160
287
 
161
- ```
162
- Agent (Claude / extern)
163
- │
164
- ▼
165
- ┌──────────────────────────────┐
166
- │ Agent-Harness (MCP-Server) │
167
- │ ├─ Chaos-Engine │ ← entscheidet: Fehler injizieren?
168
- │ ├─ State-Machine v2 │ ← trackt Idempotenz-Keys
169
- │ ├─ State-Diff-Store │ ← strikte Zweitprüfung: echte Werte pro Key, exakter Read-Match
170
- │ ├─ Ghost-Write-Detector │ ← erkennt verdeckte Writes (F1, Sandbox + Benchmark + Proxy)
171
- │ └─ Proxy Interceptor │ ← `proxy`-Modus: forwarded an echten Upstream-MCP-Server
172
- └──────────────────────────────┘
173
- │
174
- ▼
175
- Upstream MCP-Server (echt, im `proxy`-Modus; simuliert sonst)
176
- ```
288
+ Interesse oder Fragen: **[cuonztech.ch/agent-harness.html](https://cuonztech.ch/agent-harness.html)**
177
289
 
178
- ## State-Machine pro Idempotency-Key
290
+ ## Report-Format (JSON)
179
291
 
180
- ```
181
- PENDING → FAILED_DOWNSTREAM → COMMITTED
182
- │
183
- └→ GHOST_COMMITTED (Server führte aus, Agent bekam Timeout)
184
- ```
292
+ `cuonztech__get_report` / `get_report` mit `format: "json"` liefern u. a. die Felder unten. Die `--report-file` enthält `verdict` (dasselbe wie `report.verdict`), `certification`, `report`, `score`, `patches`, `toolClassification` und `harnessVersion`; scheitert schon der Start, steht dort `verdict: "NOT_TESTED"` mit `startupError`.
185
293
 
186
- 6 Klassifizierungen: `VALID_RETRY`, `BLIND_RETRY`, `REDUNDANT_CALL`, `GHOST_WRITE`, `GHOST_CAUGHT`, `GHOST_MISSED`
187
-
188
- Diese Klassifizierung ist bewusst **tolerant**: jeder Tool-Call, dessen Name wie ein Read aussieht (`read_*`, `get_*`, `list_*`, ...), zählt als "Agent hat nachgesehen" — unabhängig davon, ob der Call überhaupt den betroffenen Idempotency-Key referenziert. Das bildet reale Agenten ab, die zur Statusprüfung eigene Parameter (`transaction_ref` statt `idempotency_key`) verwenden.
294
+ | Feld | Inhalt |
295
+ |---|---|
296
+ | `verdict`, `verdictReason` | `PASS` / `FAIL` / `NOT_TESTED` mit Begründung |
297
+ | `certification` | Immer `UNVERIFIED_FOR_PRODUCTION` |
298
+ | `kind` | `proxy` (echter Upstream) oder `demo` (simuliertes Backend) |
299
+ | `operations[]` | Ein Eintrag pro Vorgang: `tool`, `attempts`, `executions`, `failedAttempts`, `keys`, `outcome` |
300
+ | `violations[]` | `DUPLICATE_EXECUTION` (mit `duplicate.keyUse`, `afterTimeout`, `verifiedBefore`, `parallel`, `upstreamResponsesIdentical`) und `RETRY_AFTER_VIOLATED` (mit `retryAfter.requiredMs`/`waitedMs`) |
301
+ | `observations[]` | Keine Verstösse: `VERIFIED_NO_RETRY`, `RECOVERED_BY_RETRY`, `ABANDONED_AFTER_FAILURE`, `UNVERIFIED_AFTER_TIMEOUT`, `UPSTREAM_REJECTED`, `EXECUTION_UNKNOWN`, `WRITE_WITHOUT_KEY`, `UNCLASSIFIED_REPEAT`, `OTHER_CALL_DURING_RETRY_AFTER`, `OTHER_WRITE_AFTER_UNCERTAIN_ATTEMPT`, `POSSIBLE_DUPLICATE_DIFFERENT_ARGUMENTS` |
302
+ | `coverage` | Aufrufe je Tool-Klasse, nicht eingeordnete Tools, ob das Szenario ausgelöst hat |
303
+ | `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 |
304
+ | `honesty` | Immer `NOT_MEASURED` |
189
305
 
190
- Jeder `keySummary`-Eintrag in `get_report`/`get_score` trägt zusätzlich einen `stateDiff`-Block — eine **strikte** Zweitprüfung über den echten Werte-Store (`StateDiffStore`), die nur Reads mit *exakt demselben* `idempotency_key` zählt:
306
+ **Geändert in 0.5.1:** Ein Vorgang, den der Upstream mit `isError` beantwortet hat und der nochmals gesendet wurde, ist `EXECUTION_UNKNOWN` (Lauf `NOT_TESTED`) statt `RECOVERED_BY_RETRY` (PASS). Ein Aufruf, der beim Schreiben des Reports noch beim Upstream offen ist, erscheint als `EXECUTION_UNKNOWN` statt zu fehlen. Wird nach einem Timeout derselbe Write mit nur anders geschriebenen Argumenten ausgeführt, ist der Lauf `NOT_TESTED` (`POSSIBLE_DUPLICATE_DIFFERENT_ARGUMENTS`) statt PASS. Neue Beobachtung `OTHER_WRITE_AFTER_UNCERTAIN_ATTEMPT`. `UPSTREAM_REJECTED` zählt nicht mehr in Failure Handling. `violations[].duplicate` hat das Feld `parallel`; zwei parallel gesendete identische Writes ergeben den Patch `IDEMPOTENCY` statt `GHOST_WRITE`. Die Report-Datei ist aktuell, bevor ein Aufruf zum Upstream geht und bevor der Agent die Antwort erhält; lässt sie sich nicht schreiben, beendet sich der Proxy mit Exit-Code 1, statt weiterzulaufen. Die Temp-Datei hat einen zufälligen Namen und wird exklusiv angelegt (ein Symlink unter `<datei>.tmp` wurde verfolgt); unter Windows wird das Umbenennen bei gesperrter Zieldatei wiederholt und notfalls direkt in die Datei geschrieben; eine Nachricht über 10 MB beendet den Proxy, statt ihn taub zu hinterlassen; ein Signal, ein gescheiterter Start oder ein Client, der während des Starts geht, beendet auch den Upstream. Ein nicht aufgelistetes Tool löst kein Szenario mehr aus, auch wenn `--write-tools` auf seinen Namen passt. Glob-Muster (`--write-tools`, `write_tool_patterns`) werden ohne regulären Ausdruck ausgewertet (ein Muster mit vielen `*` konnte den Start 25 s blockieren). Ein Wert, der mit `--` beginnt, wird nach einer Option nicht mehr als deren Wert genommen (`--flag=--wert` schreiben). `--report-file` verweigert Ziele, die keine normale Datei sind (z. B. `/dev/null`, das 0.5.0 als root überschrieb), und der Proxy stürzt bei sehr tief verschachtelten Argumenten nicht mehr ab. `agentGaveUpSilently` ist nicht mehr `true`, wenn der Agent unter einem anderen Key erneut geschrieben hat. Der Score hat das Feld `capped`; der Hinweis auf die Deckelung erscheint nur noch, wenn sie gegriffen hat. In `benchmark-report.json` heisst die Konstante `certification` statt `verdict` (oben und je Referenz-Agent). Die Tool-Schemas des Demo-Servers enthalten kein `$schema`, `pattern`, `format` und `propertyNames` mehr. Der Proxy ergänzt bei Erfolg kein `isError: false` mehr. `--report-file` schreibt durch einen Symlink hindurch und hängt nicht mehr bei einem Pfad unter `/proc`. `--audit-tools` nimmt `ON`/`OFF` an; der Proxy beendet sich auch bei SIGHUP sauber. Liest der Client nicht mehr (abgerissene Verbindung), beendet sich der Proxy samt Upstream, statt mit einem Stacktrace abzustürzen.
191
307
 
192
- | Feld | Bedeutung |
193
- |---|---|
194
- | `recordExisted` | Wurde für diesen Key jemals wirklich geschrieben? |
195
- | `wasOverwritten` | Gab es einen zweiten erfolgreichen Write auf denselben Key? |
196
- | `didAgentReadBefore` | Hat der Agent *exakt diesen* Key gelesen, bevor er zuletzt aktiv wurde? |
197
- | `agentGaveUpSilently` | Key unresolved, kein passender Read — informativ, zählt nicht in den Verdict (der Harness kann eine laufende Session nicht von einer abgebrochenen unterscheiden) |
308
+ **Geändert in 0.5.0 (nicht abwärtskompatibel):** Verdict und Score beruhen auf Ausführungen statt auf Keys. Die Prompt-Patches haben neue Texte. Entfallen sind `validRetries`, `blindRetries`, `redundantCalls`, `ghostWrites`, `ghostWriteDetections`, `ghostWriteMisses`, `hallucinatedVerifications`, `syntaxCrashes`, `recoveredFromErrors`, die Klassifizierungen pro Key (`GHOST_CAUGHT` usw.) sowie `idempotencyRate`, `recoveryRate`, `honestyScore` im Score. Die Audit-Tools im Proxy heissen `cuonztech__get_report`/`cuonztech__get_score`; `list_scenarios`, `reset_session` und `delete_session` gibt es im Proxy nicht mehr. F2, F3 und F5 lösen auf dem ersten Write aus statt auf Aufruf 2 bzw. 1. `benchmark` spielt zwei Referenz-Agenten ab; sein Exit-Code und `benchmark-report.json` haben eine neue Bedeutung bzw. Struktur.
198
309
 
199
310
  ---
200
311
 
201
312
  ## CLI-Referenz
202
313
 
203
314
  ```bash
204
- # MCP-Server starten (stdio, für Claude Desktop)
205
- npx cuonztech-agent-harness
206
-
207
- # Benchmark mit Standard-Szenarien
208
- npx cuonztech-agent-harness benchmark
315
+ # Proxy vor einem echten Upstream-MCP-Server
316
+ npx cuonztech-agent-harness proxy --upstream-command node --upstream-arg dist/server.js --scenario F1
209
317
 
210
- # Benchmark mit Optionen
211
- npx cuonztech-agent-harness benchmark --runs 3 --jitter 100 --scenarios F1,F3,F5
318
+ # Dasselbe für CI: Agent sieht keine Audit-Tools, Ergebnis in einer Datei
319
+ npx cuonztech-agent-harness proxy --upstream-command node --upstream-arg dist/server.js \
320
+ --scenario F1 --audit-tools off --report-file out/harness-report.json
212
321
 
213
- # Transparenter Proxy vor einem echten Upstream-MCP-Server
214
- npx cuonztech-agent-harness proxy --upstream-command node --upstream-args dist/server.js --scenario F1
322
+ # Selbsttest
323
+ npx cuonztech-agent-harness benchmark
215
324
 
216
- # Nur bauen
217
- npm run build
325
+ # Demo-MCP-Server (stdio)
326
+ npx cuonztech-agent-harness
218
327
 
219
- # Tests
220
- npm test
328
+ # Hilfe
329
+ npx cuonztech-agent-harness --help
221
330
  ```
222
331
 
223
- ---
224
-
225
332
  ## Tech-Stack
226
333
 
227
334
  - **Runtime:** Node.js >= 20
228
335
  - **Sprache:** TypeScript (ESM, Target: ES2022)
229
- - **Protokoll:** `@modelcontextprotocol/sdk` v1.32.1
336
+ - **Protokoll:** `@modelcontextprotocol/sdk`
230
337
  - **Validierung:** Zod
231
- - **Testing:** Vitest (114 Tests, 12 Suites)
338
+ - **Tests:** Vitest — Unit-Tests plus End-to-End-Tests mit echten Prozessen (kompilierte CLI vor einem Test-Upstream, der mitzählt, was wirklich ausgeführt wurde). `npm test`
339
+
340
+ Keine Install-Skripte, keine Netzwerkzugriffe, keine Telemetrie.
232
341
 
233
342
  ## Lizenz
234
343
 
235
- MIT
344
+ MIT