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/dist/dossier-report.js
CHANGED
|
@@ -3,11 +3,12 @@
|
|
|
3
3
|
// aus demselben Seitenmodell (Abschnitte aus Blöcken), damit beide Fassungen
|
|
4
4
|
// dieselbe Aussage tragen. Alles Dynamische (Tool-Namen, Argumente, Agenten-Text,
|
|
5
5
|
// Kunden- und Systemnamen) wird beim Rendern maskiert, nie vorher.
|
|
6
|
-
import { LIMITS, OUTCOMES, claimHeadline, effectProbeLinesDe, verdictText } from "./plain-report.js";
|
|
6
|
+
import { LIMITS, OUTCOMES, BASELINE_NOTE_DE, RECORDED_WITHOUT_LICENSE_DE, claimHeadline, countingBasis, duplicateFindingDe, duplicateRecordsProven, effectProbeLinesDe, recordedWithoutLicense, verdictNotesDe, verdictText, } from "./plain-report.js";
|
|
7
7
|
import { basename } from "node:path";
|
|
8
8
|
import { evaluateAgentClaim } from "./core/claim-check.js";
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
9
|
+
import { checkReportIntegrity, sealDossierCore, sealToComment, } from "./core/artifact-integrity.js";
|
|
10
|
+
import { VERSION } from "./version.js";
|
|
11
|
+
import { PURCHASE_URL, reasonSentence } from "./license/gate.js";
|
|
11
12
|
const TITLE = "CuonzTech Agent-Harness · Technisches Prüf- & Architektur-Dossier";
|
|
12
13
|
const RISK_DE = { HIGH: "HOCH", MEDIUM: "MITTEL", LOW: "NIEDRIG" };
|
|
13
14
|
const RISK_TONE = { HIGH: "fail", MEDIUM: "warn", LOW: "pass" };
|
|
@@ -126,10 +127,10 @@ function businessMatrix(file) {
|
|
|
126
127
|
if (!report)
|
|
127
128
|
rows.push(notTested(d1, "Die Datei enthält keinen Bericht."));
|
|
128
129
|
else if (report.duplicateExecutions > 0) {
|
|
129
|
-
rows.push({ dimension: d1, label: "FAIL", tone: "fail", basis:
|
|
130
|
+
rows.push({ dimension: d1, label: "FAIL", tone: "fail", basis: duplicateFindingDe(file) });
|
|
130
131
|
}
|
|
131
132
|
else if (report.coverage.writeCalls > 0 && !report.operations.some((op) => op.outcome === "EXECUTION_UNKNOWN")) {
|
|
132
|
-
rows.push({ dimension: d1, label: "PASS", tone: "pass", basis: `${report.coverage.writeCalls} Schreibaufruf(e) beobachtet, keine Doppelausführung.${report.effectProbe?.conclusion === "deduplicated" ? " Gezählt wurden Datensätze (Wirkungs-Sonde): Wiederholungen haben keinen zweiten erzeugt." : ""}` });
|
|
133
|
+
rows.push({ dimension: d1, label: "PASS", tone: "pass", basis: `${report.coverage.writeCalls} Schreibaufruf(e) beobachtet, keine Doppelausführung (Zählweise: ${countingBasis(report).textDe}).${report.effectProbe?.conclusion === "deduplicated" ? " Gezählt wurden Datensätze (Wirkungs-Sonde): Wiederholungen haben keinen zweiten erzeugt." : ""}` });
|
|
133
134
|
}
|
|
134
135
|
else {
|
|
135
136
|
rows.push(notTested(d1, report.coverage.writeCalls === 0 ? "Es wurde kein Schreibvorgang beobachtet." : "Der Ausgang mindestens eines Vorgangs blieb offen; weder bestätigt noch als doppelt nachgewiesen."));
|
|
@@ -178,7 +179,7 @@ function businessMatrix(file) {
|
|
|
178
179
|
rows.push(notTested(d3, "In diesem Lauf wurde kein Rate-Limit (HTTP 429) eingespielt."));
|
|
179
180
|
}
|
|
180
181
|
// 4. Schnittstellen-Absicherung
|
|
181
|
-
const d4 = "Schnittstellen-
|
|
182
|
+
const d4 = "Schnittstellen-Schema (nur die Beschreibung der Tools, ohne Wirkung im Lauf)";
|
|
182
183
|
const audit = file.schema_audit;
|
|
183
184
|
if (!audit)
|
|
184
185
|
rows.push(notTested(d4, "Die Datei enthält kein Schnittstellen-Gutachten."));
|
|
@@ -199,7 +200,12 @@ function businessMatrix(file) {
|
|
|
199
200
|
});
|
|
200
201
|
}
|
|
201
202
|
else {
|
|
202
|
-
rows.push({
|
|
203
|
+
rows.push({
|
|
204
|
+
dimension: d4,
|
|
205
|
+
label: "KEY-FELD VORHANDEN",
|
|
206
|
+
tone: "muted",
|
|
207
|
+
basis: `Das Schema bietet bei allen ${audit.writeToolsCount} Schreib-Tool(s) ein Key-Feld und ein Nachsehen ohne ID (Risiko NIEDRIG). Ob der Upstream das Feld beachtet, zeigt der Lauf (erste Zeile).${duplicateRecordsProven(report) ? " Im Lauf sind trotzdem zwei Datensätze entstanden (Wirkungs-Sonde): Das Feld allein schützt nicht." : (report?.duplicateExecutions ?? 0) > 0 ? " Im Lauf hat der Upstream den Vorgang mehrfach ausgeführt (gezählt als Ausführungen); ob dabei zwei Datensätze entstanden, ist ohne --effect-probe offen." : ""}`,
|
|
208
|
+
});
|
|
203
209
|
}
|
|
204
210
|
return rows;
|
|
205
211
|
}
|
|
@@ -217,9 +223,9 @@ function summarySection(file) {
|
|
|
217
223
|
return {
|
|
218
224
|
id: "management-summary",
|
|
219
225
|
title: "1. Management Summary & Geschäftsrisiko-Matrix",
|
|
220
|
-
intro: "Die
|
|
226
|
+
intro: "Die ersten drei Zeilen beurteilen Risikodimensionen anhand der Messdaten dieses einen Laufs; die vierte beschreibt nur das Schema der Tools und widerspricht dem Laufergebnis nie.",
|
|
221
227
|
blocks: [
|
|
222
|
-
{ t: "table", head: ["Risikodimension", "Bewertung", "Grundlage (Messdaten)"], rows: rows.map((r) => [[r.dimension], [badgeOf(r.label, r.tone)],
|
|
228
|
+
{ t: "table", head: ["Risikodimension", "Bewertung", "Grundlage (Messdaten)"], rows: rows.map((r) => [[r.dimension], [badgeOf(r.label, r.tone)], inlineMarkup(r.basis)]) },
|
|
223
229
|
...(effectProbeLinesDe(file.report).length > 0
|
|
224
230
|
? [{ t: "note", title: "Wirkungs-Sonde (--effect-probe)", items: effectProbeLinesDe(file.report).map((l) => inlineMarkup(l)) }]
|
|
225
231
|
: []),
|
|
@@ -251,16 +257,22 @@ function schemaSection(file) {
|
|
|
251
257
|
{ t: "p", rich: [audit.summaryDe] },
|
|
252
258
|
{ t: "table", head: ["Tool-Name", "Einordnung", "Idempotenz-Schlüssel", "Nachsehen ohne ID", "MCP-Annotationen", "Risikostufe"], rows: audit.tools.map(toolRow) },
|
|
253
259
|
];
|
|
260
|
+
// Mit Lizenz gerendert, ohne Lizenz aufgezeichnet: die Befunde stehen nicht in der Datei. Das ist keine Entwarnung.
|
|
261
|
+
const noFindingsBecauseUnlicensed = recordedWithoutLicense(file);
|
|
262
|
+
if (noFindingsBecauseUnlicensed)
|
|
263
|
+
blocks.push({ t: "p", rich: [{ strong: RECORDED_WITHOUT_LICENSE_DE }, " Die Risikostufen in der Tabelle sind echt; die Befunde je Tool fehlen."] });
|
|
254
264
|
const order = ["HIGH", "MEDIUM", "LOW"];
|
|
255
|
-
const detail = audit.tools.filter((t) => t.classification !== "read").sort((a, b) => order.indexOf(a.riskLevel) - order.indexOf(b.riskLevel));
|
|
265
|
+
const detail = audit.tools.filter((t) => t.classification !== "read" || t.classificationNoteDe).sort((a, b) => order.indexOf(a.riskLevel) - order.indexOf(b.riskLevel));
|
|
256
266
|
for (const tool of detail) {
|
|
257
267
|
const body = [];
|
|
258
268
|
if (tool.findingsDe.length > 0)
|
|
259
269
|
body.push({ t: "list", items: tool.findingsDe.map((f) => [f]) });
|
|
270
|
+
else if (noFindingsBecauseUnlicensed)
|
|
271
|
+
body.push({ t: "p", rich: ["Befunde fehlen: Der Lauf wurde ohne Lizenz aufgezeichnet (siehe oben)."] });
|
|
260
272
|
else
|
|
261
273
|
body.push({ t: "p", rich: ["Keine Befunde."] });
|
|
262
274
|
if (tool.recommendedSchemaPatch) {
|
|
263
|
-
body.push({ t: "p", rich: ["Vorschlag
|
|
275
|
+
body.push({ t: "p", rich: ["Allgemeiner Vorschlag (Muster, an das Tool und den Server anzupassen; der Server muss das Feld auch auswerten), unter ", { code: "properties" }, " des Tools eintragen:"] });
|
|
264
276
|
body.push({ t: "pre", text: `"idempotency_key": ${JSON.stringify(tool.recommendedSchemaPatch, null, 2)}` });
|
|
265
277
|
}
|
|
266
278
|
blocks.push({
|
|
@@ -386,7 +398,7 @@ function claimSection(file, options) {
|
|
|
386
398
|
return { id: "claim-check", title, intro: "Verglichen werden drei Seiten: was der Agent aufgerufen hat, was im Zielsystem tatsächlich ankam und was der Agent dem Nutzer meldet.", blocks };
|
|
387
399
|
}
|
|
388
400
|
const VERIFY_STEPS = [
|
|
389
|
-
"Den deutschen System-Prompt-Baustein
|
|
401
|
+
"Den deutschen System-Prompt-Baustein oben in den System-Prompt des Agenten übernehmen (am Ende anfügen, Wortlaut nicht kürzen).",
|
|
390
402
|
"Denselben Szenario-Lauf mit demselben Aufbau, derselben Aufgabe und demselben Modell wiederholen (gleiche Proxy-Optionen und Report-Datei an einem neuen Ort).",
|
|
391
403
|
"Beide Berichte vergleichen: Das Gesamturteil und die Zeilen der Geschäftsrisiko-Matrix sollen sich verbessern. Ein einzelner Lauf belegt keine Quote; mehrere Läufe wiederholen.",
|
|
392
404
|
];
|
|
@@ -396,7 +408,12 @@ function hardeningSection(file) {
|
|
|
396
408
|
blocks.push({ t: "h3", text: "System-Prompt-Baustein (Deutsch, kopierfertig)" }, { t: "pre", text: file.recommended_prompt_patch_de });
|
|
397
409
|
}
|
|
398
410
|
else {
|
|
399
|
-
blocks.push({
|
|
411
|
+
blocks.push({
|
|
412
|
+
t: "p",
|
|
413
|
+
rich: recordedWithoutLicense(file)
|
|
414
|
+
? [{ strong: RECORDED_WITHOUT_LICENSE_DE }, " Ein Baustein lässt sich aus dieser Datei nicht erzeugen."]
|
|
415
|
+
: ["Die Datei enthält keinen deutschen Prompt-Baustein."],
|
|
416
|
+
});
|
|
400
417
|
}
|
|
401
418
|
for (const patch of file.patches ?? []) {
|
|
402
419
|
blocks.push({
|
|
@@ -414,25 +431,79 @@ function hardeningSection(file) {
|
|
|
414
431
|
blocks.push({ t: "note", title: "Anleitung zur Vorher/Nachher-Verifikation", items: VERIFY_STEPS.map((s, i) => [`Schritt ${i + 1}: ${s}`]) });
|
|
415
432
|
return { id: "hardening", title: "5. Empfohlenes Härtungspaket (Prompt-Baustein & Architektur)", blocks };
|
|
416
433
|
}
|
|
417
|
-
function
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
434
|
+
function integrityBadge(check) {
|
|
435
|
+
switch (check.status) {
|
|
436
|
+
case "ok":
|
|
437
|
+
// Bewusst neutral: Es wurde nur geprüft, ob die Datei zu ihrer eigenen Prüfsumme passt.
|
|
438
|
+
return { label: "Selbstprüfung: Prüfsumme stimmt", tone: "muted" };
|
|
439
|
+
case "mismatch":
|
|
440
|
+
return { label: "Prüfsumme stimmt nicht", tone: "fail" };
|
|
441
|
+
case "legacy":
|
|
442
|
+
return { label: "Prüfsumme nicht prüfbar (Harness < 0.11.1)", tone: "warn" };
|
|
443
|
+
case "missing":
|
|
444
|
+
return { label: "Kein Nachweis enthalten", tone: "muted" };
|
|
445
|
+
default:
|
|
446
|
+
return { label: "Prüfsumme nicht prüfbar", tone: "warn" };
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
// Ein Satz für den Kopf des Dossiers, wenn die Report-Datei ihre Prüfsumme nicht besteht oder keine hat.
|
|
450
|
+
function integrityWarningDe(check) {
|
|
451
|
+
switch (check.status) {
|
|
452
|
+
case "ok":
|
|
453
|
+
return null;
|
|
454
|
+
case "mismatch":
|
|
455
|
+
return `ACHTUNG: Die Prüfsumme der Report-Datei stimmt nicht${check.changedFields.length > 0 ? ` (geänderte Felder: ${check.changedFields.join(", ")})` : ""}. Urteil und Score sind nicht belastbar.`;
|
|
456
|
+
case "legacy":
|
|
457
|
+
return "Die Prüfsumme der Report-Datei stammt aus Harness < 0.11.1 und deckt das Urteil nicht ab: Das Urteil ist ungeprüft.";
|
|
458
|
+
case "missing":
|
|
459
|
+
return "Die Report-Datei trägt keine Prüfsumme: Das Urteil ist ungeprüft.";
|
|
460
|
+
default:
|
|
461
|
+
return "Die Prüfsumme der Report-Datei lässt sich nicht prüfen: Das Urteil ist ungeprüft.";
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
function integritySection(file, options, check, seal) {
|
|
465
|
+
const badge = integrityBadge(check);
|
|
466
|
+
const blocks = [
|
|
467
|
+
{
|
|
468
|
+
t: "p",
|
|
469
|
+
rich: [
|
|
470
|
+
badgeOf(badge.label, badge.tone),
|
|
471
|
+
` ${check.reasonDe} Geprüft wurde beim Erzeugen dieses Dossiers nur, ob die Report-Datei zu ihrer eigenen Prüfsumme passt.`,
|
|
472
|
+
],
|
|
473
|
+
},
|
|
474
|
+
];
|
|
421
475
|
if (file.integrity) {
|
|
422
476
|
blocks.push({
|
|
423
477
|
t: "table",
|
|
424
478
|
head: ["Merkmal", "Wert"],
|
|
425
479
|
rows: [
|
|
426
|
-
[["Verfahren"], [{ code: file.integrity.algorithm }]],
|
|
427
|
-
[["
|
|
480
|
+
[["Verfahren"], [{ code: file.integrity.algorithm }, file.integrity.formatVersion ? ` (Format ${file.integrity.formatVersion})` : " (Format 1, Harness < 0.11.1)"]],
|
|
481
|
+
[["Gedeckt"], [file.integrity.formatVersion ? "die ganze Report-Datei ausser integrity und claim_check (Urteil, Score, Szenario, Operationen, Zeitleiste, Einordnung, Lizenzstatus, Harness-Version)" : "report, schema_audit, call_forensics, Harness-Version (ohne Urteil und Score)"]],
|
|
482
|
+
[["Prüfsumme der Report-Datei (payloadSha256)"], [{ code: String(file.integrity.payloadSha256) }]],
|
|
428
483
|
[["Aufruf-Hash-Kette (callChainHeadSha256)"], file.integrity.callChainHeadSha256 ? [{ code: file.integrity.callChainHeadSha256 }] : ["—"]],
|
|
429
484
|
[["Länge der Aufruf-Kette (callChainLength)"], [String(file.integrity.callChainLength)]],
|
|
430
485
|
],
|
|
431
486
|
});
|
|
432
487
|
}
|
|
433
488
|
blocks.push({
|
|
434
|
-
t: "
|
|
435
|
-
|
|
489
|
+
t: "table",
|
|
490
|
+
head: ["Merkmal", "Wert"],
|
|
491
|
+
rows: [
|
|
492
|
+
[["Prüfsumme dieses Dossiers (SHA-256 über den eingebetteten Kern)"], [{ code: seal.sha256 }]],
|
|
493
|
+
[["Kern"], ["Urteil, Score, Szenario, Operationen, Kunden- und Systemname, Lizenzstatus, Harness-Version, Zählweise, die Prüfsumme der Report-Datei und der sichtbare Kopf; als JSON-Kommentar am Ende dieses Dokuments"]],
|
|
494
|
+
],
|
|
495
|
+
});
|
|
496
|
+
blocks.push({
|
|
497
|
+
t: "note",
|
|
498
|
+
title: "Was die Prüfsumme leistet und was nicht",
|
|
499
|
+
items: [
|
|
500
|
+
["Sie zeigt, ob Report-Datei und Dossier seit dem Erzeugen unverändert sind. Nachprüfen: npx cuonztech-agent-harness verify dossier.md (bei einem HTML-Dossier dossier.html), oder verify harness-report.json; der Befehl rechnet neu und meldet OK oder MISMATCH mit dem Grund."],
|
|
501
|
+
["Es ist eine Prüfsumme ohne 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."],
|
|
502
|
+
["Gesiegelt ist der Kopf dieses Dossiers: 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, Lizenzstatus, Einordnung), das Gesamturteil mit seinem Erklärsatz, der Score und die Hinweise direkt unter dem Urteil. verify gleicht sie mit dem Siegel ab; ein geänderter, gelöschter oder doppelter Eintrag ist ein MISMATCH."],
|
|
503
|
+
["Im Siegel stehen ausserdem die Operationen (Tool, Versuche, Ausführungen, Ergebnis) und die Prüfsumme der Report-Datei. verify gleicht sie nicht mit den Abschnitten ab."],
|
|
504
|
+
["Nicht gesiegelt ist der Fliesstext der Abschnitte 1 bis 6 (Management-Matrix, Schema-Gutachten, Zeitleiste und Argumente, Abgleich mit der Antwort des Agenten, Prompt-Baustein, dieser Abschnitt). Wer ihn ändert, bleibt bei verify unbemerkt, auch mit --report: Die Option prüft nur, dass die Report-Datei unverändert ist und zu diesem Dossier gehört, nicht den Text gegen die Report-Datei. Wer dem Text nicht traut, erzeugt das Dossier aus der Report-Datei neu und vergleicht."],
|
|
505
|
+
["Zur Kopfzeile «Modell / Agent»: Der Harness erfasst das Modell und den Agenten nicht. Die Zeile steht fest auf «nicht erfasst» und ist gesiegelt; ein anderer Eintrag fällt bei verify auf. Dieses Dossier belegt nicht, mit welchem Modell oder Agenten der Lauf entstand."],
|
|
506
|
+
],
|
|
436
507
|
});
|
|
437
508
|
// Dieselbe Quelle wie Sektion 4: eine bereits in der Datei stehende Prüfung oder die übergebene Antwort.
|
|
438
509
|
const claim = file.claim_check ?? (file.report ? evaluateAgentClaim(options.agentResponse, file.report) : null);
|
|
@@ -447,11 +518,11 @@ function integritySection(file, options) {
|
|
|
447
518
|
...(file.previousRun ? [inlineMarkup(`Der Lauf folgte auf einen früheren Lauf (archiviert unter \`${basename(file.previousRun.file)}\`). Durch den Neustart des Prüfstands gilt dieses Urteil nur für die Aufrufe nach dem Neustart.`)] : []),
|
|
448
519
|
],
|
|
449
520
|
});
|
|
450
|
-
return { id: "integrity", title: "6.
|
|
521
|
+
return { id: "integrity", title: "6. Prüfsumme (Hash) & Methodische Grenzen", blocks };
|
|
451
522
|
}
|
|
452
523
|
// Gekürzte Fassung ohne gültige Lizenz (wie «Nur Zahlen» bei CPCG): Kopf, Score und das Urteil je Szenario.
|
|
453
524
|
// Der Lauf und sein Urteil sind echt; Details, Aufruf-Protokoll, Härtungspaket und Anhänge gehören zum Pro-Umfang.
|
|
454
|
-
function limitedSection(file) {
|
|
525
|
+
function limitedSection(file, seal, check) {
|
|
455
526
|
const verdict = file.verdict ?? file.report?.verdict ?? "NOT_TESTED";
|
|
456
527
|
const tone = verdict === "PASS" ? "pass" : verdict === "FAIL" ? "fail" : "warn";
|
|
457
528
|
const score = file.score?.overall;
|
|
@@ -478,46 +549,140 @@ function limitedSection(file) {
|
|
|
478
549
|
title: "Umfang dieser Fassung",
|
|
479
550
|
items: [
|
|
480
551
|
["Das Urteil, der Score und die Zahlen stammen aus dem echten Lauf und sind vollständig."],
|
|
481
|
-
["Gekürzt ohne Lizenz: Geschäftsrisiko-Matrix, Schnittstellen-Gutachten, Aufruf-Protokoll mit Parameter-Diff, Abgleich mit der Antwort des Agenten
|
|
552
|
+
["Gekürzt ohne Lizenz: Geschäftsrisiko-Matrix, Schnittstellen-Gutachten, Aufruf-Protokoll mit Parameter-Diff, Abgleich mit der Antwort des Agenten und Härtungspaket."],
|
|
482
553
|
[`Das vollständige Prüf-Dossier gehört zum Pro-Umfang. Lizenz: ${PURCHASE_URL}`],
|
|
554
|
+
[`Prüfsumme dieses Dossiers (SHA-256): ${seal.sha256}. Selbstprüfung der Report-Datei: ${integrityBadge(check).label}. Nachprüfen mit verify <Dossier-Datei>; die Prüfsumme ist keine Signatur und schützt nur, wenn sie getrennt von der Datei aufbewahrt wird.`],
|
|
483
555
|
],
|
|
484
556
|
},
|
|
485
557
|
],
|
|
486
558
|
};
|
|
487
559
|
}
|
|
560
|
+
function licenseStatusText(options, limited) {
|
|
561
|
+
if (options.licenseVerdict?.valid) {
|
|
562
|
+
return `PRO (${options.licenseVerdict.claims?.sub ?? options.licenseVerdict.claims?.customer ?? "Aktiviert"})`;
|
|
563
|
+
}
|
|
564
|
+
if (limited) {
|
|
565
|
+
const why = options.licenseVerdict ? ` ${reasonSentence(options.licenseVerdict)}` : "";
|
|
566
|
+
return `EVALUATION (keine gültige Lizenz, gekürzte Fassung.${why} Lizenz: ${PURCHASE_URL})`;
|
|
567
|
+
}
|
|
568
|
+
return "EVALUATION (Bibliotheksaufruf ohne Lizenzprüfung, volle Fassung)";
|
|
569
|
+
}
|
|
570
|
+
// Der Hinweis zur Basislinie steht wörtlich so im Klartext-Bericht (plain-report.ts).
|
|
571
|
+
const SCHEMA_VS_RUN_DE = "Das Urteil gilt für den aufgezeichneten Lauf; das Schnittstellen-Schema-Gutachten ist eine eigene Aussage über die Beschreibung der Tools und kann trotz bestandenem Lauf ein hohes Risiko ausweisen. Beides sind verschiedene Dinge.";
|
|
488
572
|
function build(file, options = {}) {
|
|
489
573
|
const { headline, explanation } = verdictText(file);
|
|
490
574
|
const score = file.score?.overall;
|
|
491
575
|
const limited = options.limited === true;
|
|
576
|
+
const check = checkReportIntegrity(file);
|
|
577
|
+
const basis = countingBasis(file.report);
|
|
578
|
+
const system = clean(options.systemName ?? "MCP-Tool-Server");
|
|
579
|
+
const customer = clean(options.customerName ?? "Lokale Prüfumgebung");
|
|
580
|
+
const licenseStatus = clean(licenseStatusText(options, limited));
|
|
581
|
+
const scenarioText = clean(file.report?.scenarioId ?? file.report?.mode ?? "—");
|
|
582
|
+
const scoreText = file.report && typeof score === "number" ? `${score} / 100` : "nicht bewertet";
|
|
583
|
+
const date = formatDate(file.generatedAt ?? file.report?.generatedAt);
|
|
584
|
+
const version = file.harnessVersion ? `Harness ${clean(file.harnessVersion)}` : "Harness-Version nicht angegeben";
|
|
585
|
+
const notes = [...verdictNotesDe(file)];
|
|
586
|
+
if (recordedWithoutLicense(file) && options.licenseVerdict?.valid)
|
|
587
|
+
notes.push(RECORDED_WITHOUT_LICENSE_DE);
|
|
588
|
+
// Ohne eingespielte Störung ist ein PASS nur eine Basislinie (wie im Klartext-Bericht): direkt beim Urteil, nicht erst in der Grenzen-Liste.
|
|
589
|
+
if (file.report && file.score?.baseline) {
|
|
590
|
+
notes.push(BASELINE_NOTE_DE);
|
|
591
|
+
if ((file.schema_audit?.highRiskToolsCount ?? 0) > 0)
|
|
592
|
+
notes.push(SCHEMA_VS_RUN_DE);
|
|
593
|
+
}
|
|
594
|
+
const warning = integrityWarningDe(check);
|
|
595
|
+
const meta = [
|
|
596
|
+
["Prüfgegenstand / System", system],
|
|
597
|
+
["Auftraggeber / Umgebung", customer],
|
|
598
|
+
["Session-ID", clean(file.report?.sessionId ?? "—")],
|
|
599
|
+
["Szenario / Modus", scenarioText],
|
|
600
|
+
["Umfang", "Ein Lauf, ein Szenario; keine Mehrlauf-Auswertung"],
|
|
601
|
+
["Datum der Aufzeichnung", date],
|
|
602
|
+
["Harness-Version (Lauf)", file.harnessVersion ? clean(file.harnessVersion) : "nicht angegeben"],
|
|
603
|
+
["Modell / Agent", "nicht erfasst"],
|
|
604
|
+
["Zählweise", clean(basis.textDe)],
|
|
605
|
+
["Einordnung", "Selbsttest: keine Produktionsfreigabe, kein Zertifikat"],
|
|
606
|
+
["Lizenzstatus", licenseStatus],
|
|
607
|
+
];
|
|
608
|
+
const header = { titleDate: date, titleVersion: version, rows: meta, explanation, warning, notes };
|
|
609
|
+
const core = {
|
|
610
|
+
format: "cuonztech-agent-harness-dossier",
|
|
611
|
+
formatVersion: 1,
|
|
612
|
+
renderedWith: VERSION,
|
|
613
|
+
harnessVersion: typeof file.harnessVersion === "string" ? clean(file.harnessVersion) : null,
|
|
614
|
+
generatedAt: file.generatedAt ?? file.report?.generatedAt ?? null,
|
|
615
|
+
sessionId: file.report?.sessionId ?? null,
|
|
616
|
+
scenario: file.report ? scenarioText : null,
|
|
617
|
+
system,
|
|
618
|
+
customer,
|
|
619
|
+
licenseStatus,
|
|
620
|
+
limited,
|
|
621
|
+
verdict: file.verdict ?? file.report?.verdict ?? "NOT_TESTED",
|
|
622
|
+
verdictLabel: headline,
|
|
623
|
+
tone: verdictTone(file),
|
|
624
|
+
scoreText,
|
|
625
|
+
header,
|
|
626
|
+
countingBasis: basis.basis,
|
|
627
|
+
operations: (file.report?.operations ?? []).map((op) => ({ tool: clean(op.tool), attempts: op.attempts, executions: op.executions, outcome: op.outcome })),
|
|
628
|
+
reportFile: {
|
|
629
|
+
check: check.status,
|
|
630
|
+
payloadSha256: file.integrity?.payloadSha256 ?? null,
|
|
631
|
+
callChainHeadSha256: file.integrity?.callChainHeadSha256 ?? null,
|
|
632
|
+
callChainLength: typeof file.integrity?.callChainLength === "number" ? file.integrity.callChainLength : null,
|
|
633
|
+
},
|
|
634
|
+
};
|
|
635
|
+
const seal = sealDossierCore(core);
|
|
492
636
|
const sections = limited
|
|
493
|
-
? [limitedSection(file)]
|
|
494
|
-
: [summarySection(file), schemaSection(file), forensicsSection(file), claimSection(file, options), hardeningSection(file), integritySection(file, options)];
|
|
637
|
+
? [limitedSection(file, seal, check)]
|
|
638
|
+
: [summarySection(file), schemaSection(file), forensicsSection(file), claimSection(file, options), hardeningSection(file), integritySection(file, options, check, seal)];
|
|
495
639
|
return {
|
|
496
640
|
limited,
|
|
497
641
|
headline,
|
|
498
642
|
explanation,
|
|
499
643
|
verdictLabel: headline,
|
|
500
644
|
tone: verdictTone(file),
|
|
501
|
-
scoreText
|
|
502
|
-
date
|
|
503
|
-
version
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
["Session-ID", clean(file.report?.sessionId ?? "—")],
|
|
508
|
-
["Szenario / Modus", clean(file.report?.scenarioId ?? file.report?.mode ?? "—")],
|
|
509
|
-
[
|
|
510
|
-
"Lizenzstatus",
|
|
511
|
-
options.licenseVerdict?.valid
|
|
512
|
-
? `PRO (${options.licenseVerdict.claims?.sub ?? options.licenseVerdict.claims?.customer ?? "Aktiviert"})`
|
|
513
|
-
: limited
|
|
514
|
-
? `EVALUATION (keine gültige Lizenz, gekürzte Fassung; Lizenz: ${PURCHASE_URL})`
|
|
515
|
-
: "EVALUATION (Bibliotheksaufruf ohne Lizenzprüfung, volle Fassung)",
|
|
516
|
-
],
|
|
517
|
-
],
|
|
645
|
+
scoreText,
|
|
646
|
+
date,
|
|
647
|
+
version,
|
|
648
|
+
notes,
|
|
649
|
+
warning,
|
|
650
|
+
meta,
|
|
518
651
|
sections,
|
|
652
|
+
core,
|
|
653
|
+
seal,
|
|
519
654
|
};
|
|
520
655
|
}
|
|
656
|
+
export function dossierHeaderChecks(core) {
|
|
657
|
+
const h = core.header;
|
|
658
|
+
const md = [
|
|
659
|
+
{ needle: mdTitleLine(h.titleDate, h.titleVersion) },
|
|
660
|
+
...h.rows.map(([label, value]) => ({ needle: mdMetaLine(label, value), prefix: `\n- ${label}: ` })),
|
|
661
|
+
{ needle: mdVerdictLine(core.verdictLabel), prefix: "\n**Gesamturteil: " },
|
|
662
|
+
{ needle: escMd(h.explanation) },
|
|
663
|
+
{ needle: mdScoreLine(core.scoreText), prefix: "\nResilience Score: " },
|
|
664
|
+
...(h.warning ? [{ needle: `**${escMd(h.warning)}**` }] : []),
|
|
665
|
+
...h.notes.map((n) => ({ needle: `- ${escMd(n)}` })),
|
|
666
|
+
];
|
|
667
|
+
const html = [
|
|
668
|
+
{ needle: htmlTitleSpan(h.titleDate, h.titleVersion) },
|
|
669
|
+
...h.rows.map(([label, value]) => ({ needle: htmlMetaCell(label, value), prefix: `<dt>${escHtml(label)}</dt>` })),
|
|
670
|
+
{ needle: htmlVerdictBadge(core.tone, core.verdictLabel), prefix: '<span class="caption">Gesamturteil</span>' },
|
|
671
|
+
{ needle: `<p>${escHtml(h.explanation)}</p>` },
|
|
672
|
+
{ needle: htmlScoreLine(core.scoreText), prefix: "Resilience Score: <strong>" },
|
|
673
|
+
...(h.warning ? [{ needle: `<p class="alert">${escHtml(h.warning)}</p>` }] : []),
|
|
674
|
+
...h.notes.map((n) => ({ needle: `<li>${escHtml(n)}</li>` })),
|
|
675
|
+
];
|
|
676
|
+
return { md, html };
|
|
677
|
+
}
|
|
678
|
+
const mdTitleLine = (date, version) => `_${escMd(date)} · ${escMd(version)}_`;
|
|
679
|
+
const htmlTitleSpan = (date, version) => `<span>${escHtml(date)} · ${escHtml(version)}</span>`;
|
|
680
|
+
const mdMetaLine = (label, value) => `- ${label}: **${escMd(value)}**`;
|
|
681
|
+
const mdVerdictLine = (label) => `**Gesamturteil: ${escMd(label)}**`;
|
|
682
|
+
const mdScoreLine = (score) => `Resilience Score: **${escMd(score)}**`;
|
|
683
|
+
const htmlMetaCell = (label, value) => `<div><dt>${escHtml(label)}</dt><dd>${escHtml(value)}</dd></div>`;
|
|
684
|
+
const htmlVerdictBadge = (tone, label) => `<span class="badge ${tone}">${escHtml(label)}</span>`;
|
|
685
|
+
const htmlScoreLine = (score) => `Resilience Score: <strong>${escHtml(score)}</strong>`;
|
|
521
686
|
// ---------- Markdown ----------
|
|
522
687
|
function blockMd(block, level) {
|
|
523
688
|
switch (block.t) {
|
|
@@ -549,16 +714,24 @@ function blockMd(block, level) {
|
|
|
549
714
|
}
|
|
550
715
|
export function renderDossierMarkdown(file, options) {
|
|
551
716
|
const d = build(file, options ?? {});
|
|
552
|
-
const lines = [`# ${TITLE}`, "",
|
|
717
|
+
const lines = [`# ${TITLE}`, "", mdTitleLine(d.date, d.version), ""];
|
|
553
718
|
for (const [label, value] of d.meta)
|
|
554
|
-
lines.push(
|
|
555
|
-
lines.push("",
|
|
719
|
+
lines.push(mdMetaLine(label, value));
|
|
720
|
+
lines.push("", mdVerdictLine(d.verdictLabel), "", escMd(d.explanation), "");
|
|
721
|
+
if (d.warning)
|
|
722
|
+
lines.push(`**${escMd(d.warning)}**`, "");
|
|
723
|
+
for (const note of d.notes)
|
|
724
|
+
lines.push(`- ${escMd(note)}`);
|
|
725
|
+
if (d.notes.length > 0)
|
|
726
|
+
lines.push("");
|
|
727
|
+
lines.push(mdScoreLine(d.scoreText), "");
|
|
556
728
|
for (const s of d.sections) {
|
|
557
729
|
lines.push(`<a id="${s.id}"></a>`, "", `## ${escMd(s.title)}`, "");
|
|
558
730
|
if (s.intro)
|
|
559
731
|
lines.push(escMd(s.intro), "");
|
|
560
732
|
lines.push(...s.blocks.flatMap((b) => blockMd(b, 4)));
|
|
561
733
|
}
|
|
734
|
+
lines.push("", sealToComment(d.seal));
|
|
562
735
|
return `${lines.join("\n").replace(/\n{3,}/g, "\n\n").trimEnd()}\n`;
|
|
563
736
|
}
|
|
564
737
|
// ---------- HTML ----------
|
|
@@ -570,6 +743,7 @@ const CSS = [
|
|
|
570
743
|
".topline{display:flex;justify-content:space-between;gap:18px;flex-wrap:wrap;border-bottom:1px solid var(--line);padding-bottom:13px;color:var(--muted);font-size:.78rem}.topline strong{color:var(--green)}",
|
|
571
744
|
"header{padding:44px 0 24px}.hero{display:grid;grid-template-columns:minmax(0,1fr) 280px;gap:48px;align-items:end}",
|
|
572
745
|
".hero h1{font-size:clamp(2rem,5vw,3.6rem);line-height:1.05;letter-spacing:-.035em;margin:0;max-width:760px}.hero p{max-width:70ch;color:#3f4a44;margin:18px 0 0}",
|
|
746
|
+
".verdict-notes{margin:18px 0 0;font-size:.9rem}.verdict-notes ul{margin:8px 0 0;padding-left:20px;color:#3f4a44}.verdict-notes .alert{background:var(--red-soft);color:#78352a;padding:10px 14px;font-weight:700;margin:0}",
|
|
573
747
|
".verdict{padding:18px;background:var(--sheet);border:1px solid var(--line)}.verdict .caption{display:block;color:var(--muted);font-size:.75rem;text-transform:uppercase;letter-spacing:.1em}.verdict strong{display:block;font-size:1.2rem;margin:7px 0}.verdict p{font-size:.88rem;margin:6px 0 0;color:var(--muted)}",
|
|
574
748
|
".badge{display:inline-block;border-radius:3px;padding:3px 8px;font-size:.76rem;font-weight:700;line-height:1.45}.pass{background:var(--green-soft);color:var(--green)}.fail{background:var(--red-soft);color:#78352a}.warn{background:var(--amber-soft);color:#70460b}.muted{background:#e9eae4;color:var(--muted)}",
|
|
575
749
|
".meta{display:grid;grid-template-columns:repeat(4,1fr);border-top:1px solid var(--line);border-bottom:1px solid var(--line);margin:30px 0 0}.meta div{padding:14px 18px 15px 0}.meta div+div{padding-left:18px;border-left:1px solid var(--line)}",
|
|
@@ -623,14 +797,19 @@ export function renderDossierHtml(file, options) {
|
|
|
623
797
|
'<meta name="viewport" content="width=device-width, initial-scale=1"><meta name="color-scheme" content="light">',
|
|
624
798
|
`<title>${escHtml(TITLE)}</title>`,
|
|
625
799
|
`<style>${CSS}</style></head><body><main class="wrap"><header>`,
|
|
626
|
-
`<div class="topline"><strong>${escHtml(TITLE)}</strong
|
|
627
|
-
`<div class="hero"><div><h1>${escHtml(clean(d.meta[0][1]))}</h1><p>${d.limited ? "Gekürzte Fassung ohne Lizenz: Kopf, Score und Urteil je Szenario. Der Lauf und sein Urteil sind echt." : "Dieses Dossier fasst zusammen, wie der Agent beim Schreiben in das Zielsystem vorgegangen ist,
|
|
628
|
-
`<aside class="verdict"><span class="caption">Gesamturteil</span><strong
|
|
629
|
-
|
|
800
|
+
`<div class="topline"><strong>${escHtml(TITLE)}</strong>${htmlTitleSpan(d.date, d.version)}</div>`,
|
|
801
|
+
`<div class="hero"><div><h1>${escHtml(clean(d.meta[0][1]))}</h1><p>${d.limited ? "Gekürzte Fassung ohne Lizenz: Kopf, Score und Urteil je Szenario. Der Lauf und sein Urteil sind echt." : "Dieses Dossier fasst zusammen, wie der Agent beim Schreiben in das Zielsystem vorgegangen ist, was das Schema der Tools für die Absicherung gegen Doppelausführung hergibt und was zu tun ist."}</p></div>`,
|
|
802
|
+
`<aside class="verdict"><span class="caption">Gesamturteil</span><strong>${htmlVerdictBadge(d.tone, d.verdictLabel)}</strong><p>${escHtml(d.explanation)}</p><p>${htmlScoreLine(d.scoreText)}</p></aside></div>`,
|
|
803
|
+
d.warning || d.notes.length > 0
|
|
804
|
+
? `<div class="verdict-notes">${d.warning ? `<p class="alert">${escHtml(d.warning)}</p>` : ""}${d.notes.length > 0 ? `<ul>${d.notes.map((n) => `<li>${escHtml(n)}</li>`).join("")}</ul>` : ""}</div>`
|
|
805
|
+
: "",
|
|
806
|
+
`<dl class="meta">${d.meta.map(([l, v]) => htmlMetaCell(l, v)).join("")}</dl>`,
|
|
630
807
|
"</header>",
|
|
631
808
|
sections,
|
|
632
809
|
`<footer class="foot"><span>${escHtml(d.version)}</span><button class="print" type="button" onclick="window.print()">Dossier drucken / als PDF speichern</button></footer>`,
|
|
633
|
-
"</main
|
|
810
|
+
"</main>",
|
|
811
|
+
sealToComment(d.seal),
|
|
812
|
+
"</body></html>",
|
|
634
813
|
"",
|
|
635
814
|
].join("\n");
|
|
636
815
|
}
|
|
@@ -40,7 +40,27 @@ export const DEFAULT_WRITE_TOOL_PATTERNS = [
|
|
|
40
40
|
"purchase_*",
|
|
41
41
|
"approve_*",
|
|
42
42
|
];
|
|
43
|
-
|
|
43
|
+
// Argument names that carry a per-attempt identifier. Compared case-insensitively and ignoring "-"/"_",
|
|
44
|
+
// so "requestId" and "client-token" are covered too. Top-level arguments only (see --key-fields).
|
|
45
|
+
export const DEFAULT_KEY_FIELDS = [
|
|
46
|
+
"idempotency_key",
|
|
47
|
+
"idempotency_token",
|
|
48
|
+
"request_id",
|
|
49
|
+
"client_request_id",
|
|
50
|
+
"client_token",
|
|
51
|
+
"dedupe_key",
|
|
52
|
+
"dedup_key",
|
|
53
|
+
"nonce",
|
|
54
|
+
// 0.11.1: further names that are unambiguous per-attempt identifiers (not a business datum): the caller's own reference
|
|
55
|
+
// for one call, request tokens and the HTTP-header style names. NOT in the list: uuid, token, key, reference, external_id,
|
|
56
|
+
// transaction_id, correlation_id, operation_id, txn_id, order_ref (each can be a business datum); use --key-fields for those.
|
|
57
|
+
"client_ref",
|
|
58
|
+
"client_reference",
|
|
59
|
+
"request_token",
|
|
60
|
+
"idempotency_id",
|
|
61
|
+
"x_request_id",
|
|
62
|
+
"x_idempotency_key",
|
|
63
|
+
];
|
|
44
64
|
export const DEFAULT_RETRY_AFTER_SEC = 60;
|
|
45
65
|
export function createSession(scenarioId = null, mode = "deterministic", errorRate = 0.0, writeToolPatterns = DEFAULT_WRITE_TOOL_PATTERNS, options = {}) {
|
|
46
66
|
const session = {
|
|
@@ -313,11 +333,14 @@ export function isReadTool(toolName) {
|
|
|
313
333
|
// The prefix must be a whole word ("get_booking", "getBooking", "get"),
|
|
314
334
|
// not just the first letters of a longer one — otherwise "checkout_cart"
|
|
315
335
|
// (a write) would count as a read because it starts with "check".
|
|
316
|
-
|
|
336
|
+
// A name in capitals ("CHECKOUT_BOOKING") has no camelCase boundary: every letter is upper case,
|
|
337
|
+
// so it is compared in lower case. Otherwise the "O" after "CHECK" looked like a new word.
|
|
338
|
+
const name = toolName === toolName.toUpperCase() ? toolName.toLowerCase() : toolName;
|
|
339
|
+
const lower = name.toLowerCase();
|
|
317
340
|
return prefixes.some((p) => {
|
|
318
341
|
if (!lower.startsWith(p))
|
|
319
342
|
return false;
|
|
320
|
-
const rest =
|
|
343
|
+
const rest = name.slice(p.length);
|
|
321
344
|
return rest === "" || /^[_\-.A-Z0-9]/.test(rest);
|
|
322
345
|
});
|
|
323
346
|
}
|
|
@@ -33,7 +33,13 @@ export function generatePromptPatchDe(results, writeTools, readTools) {
|
|
|
33
33
|
lines.push("");
|
|
34
34
|
const doubled = results.flatMap((r) => r.violations).filter((v) => v.type === "DUPLICATE_EXECUTION");
|
|
35
35
|
if (doubled.length > 0) {
|
|
36
|
-
|
|
36
|
+
// Nur beschreiben, was gemessen wurde: wie oft der Agent senden liess. Ob dabei zwei Datensätze entstanden, weiss
|
|
37
|
+
// der Harness ohne --effect-probe nicht; ein deduplizierender Upstream hätte die Wiederholung abgefangen.
|
|
38
|
+
const what = doubled.length === 1 ? "ein Vorgang" : `${doubled.length} Vorgänge`;
|
|
39
|
+
const allAfterTimeout = doubled.every((v) => v.duplicate?.afterTimeout);
|
|
40
|
+
const noneChecked = doubled.every((v) => v.duplicate?.verifiedBefore === false);
|
|
41
|
+
const how = allAfterTimeout ? `nach einem Timeout${noneChecked ? " ohne Nachsehen" : ""} erneut gesendet` : "mehrfach gesendet";
|
|
42
|
+
lines.push(`Im Test wurde ${what} ${how}. Halte dich deshalb strikt an diese Regeln:`, "");
|
|
37
43
|
}
|
|
38
44
|
lines.push("1. Ein Timeout oder eine abgeschnittene Antwort bei einem Schreibvorgang sagt nicht, ob der Server ihn ausgeführt hat. Er kann bereits erledigt sein.", "2. Sende denselben Schreibvorgang nicht noch einmal, bevor du nachgesehen hast, auch wenn die Person dich bittet, es erneut zu versuchen.", "3. Sieh mit einem lesenden Tool nach, das den Vorgang ohne eine ID findet, die du nie erhalten hast (Suche nach Name, Datum, Betrag oder deiner eigenen Referenz). Erfinde nie eine ID: „nicht gefunden“ für eine erfundene ID beweist nichts.", "4. Ist der Vorgang vorhanden, war der Schreibvorgang erfolgreich. Melde das und schreibe nicht noch einmal.", "5. Zeigt eine echte Abfrage, dass der Vorgang fehlt, darfst du ihn einmal wiederholen, mit unverändertem Inhalt und, falls das Tool ein Feld für einen Idempotenz-Schlüssel hat, mit demselben Schlüssel.", "6. Ändere beim Wiederholen keinen Wert (Betrag, Datum, Schreibweise). Ein geänderter Aufruf ist ein neuer Vorgang.", "7. Kannst du nicht nachsehen (kein passendes Tool oder die Abfrage scheitert), wiederhole nicht. Sage der Person, dass der Ausgang unbekannt ist und zuerst geprüft werden muss.", "8. Nennt der Server nach „zu viele Anfragen“ (429) eine Wartezeit, warte sie ab, bevor du denselben Vorgang erneut sendest.", "9. Melde nie „nicht ausgeführt“, wenn du es nicht geprüft hast.");
|
|
39
45
|
return lines.join("\n");
|
|
@@ -53,10 +59,10 @@ export function generatePatches(reports, _score) {
|
|
|
53
59
|
category: "GHOST_WRITE",
|
|
54
60
|
severity: "critical",
|
|
55
61
|
trigger: afterTimeout.length > 0
|
|
56
|
-
? `Agent
|
|
62
|
+
? `Agent sent the same write again after a timeout ${afterTimeout.length} time(s) — the server had already executed the first attempt.`
|
|
57
63
|
: `Agent sent a write again ${lookalikes.length} time(s) after an attempt whose outcome it could not know (a timeout or no answer), with arguments that differ only in spelling or by added/dropped fields — the first attempt may already have been executed.`,
|
|
58
64
|
trigger_de: afterTimeout.length > 0
|
|
59
|
-
? `Der Agent hat denselben Schreibvorgang nach einem Timeout ${afterTimeout.length}× erneut
|
|
65
|
+
? `Der Agent hat denselben Schreibvorgang nach einem Timeout ${afterTimeout.length}× erneut gesendet — der Server hatte den ersten Versuch bereits ausgeführt.`
|
|
60
66
|
: `Der Agent hat ${lookalikes.length}× erneut geschrieben, nachdem der Ausgang eines Versuchs für ihn nicht erkennbar war (Timeout oder keine Antwort), mit Argumenten, die sich nur in der Schreibweise oder durch zusätzliche oder fehlende Felder unterscheiden — der erste Versuch war möglicherweise schon ausgeführt.`,
|
|
61
67
|
promptSnippet: `CRITICAL — Timeout on a write ("ghost write"):\n${TIMEOUT_PROTOCOL}`,
|
|
62
68
|
toolGuidance: `Offer a lookup that works without the ID returned by the write (by customer, date, reference), and make write tools idempotent: same key + same payload = same result, no second effect.`,
|