cuonztech-agent-harness 0.5.2 → 0.7.0
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 +125 -11
- package/dist/campaign/runner.js +532 -0
- package/dist/core/artifact-integrity.js +92 -0
- package/dist/core/call-forensics.js +166 -0
- package/dist/core/claim-check.js +212 -0
- package/dist/core/report-generator.js +66 -1
- package/dist/doctor.js +241 -0
- package/dist/dossier-report.js +580 -0
- package/dist/hardening/patch-generator.js +30 -0
- package/dist/index.js +102 -27
- package/dist/plain-report.js +493 -0
- package/dist/proxy/gateway.js +2 -2
- package/dist/proxy/guarded-stdio.js +174 -0
- package/dist/proxy/interceptor.js +9 -7
- package/dist/report-file-guard.js +49 -0
- package/dist/schema-audit.js +193 -0
- package/dist/server/stdio.js +7 -2
- package/dist/types.js +1 -0
- package/dist/ui/discover.js +71 -0
- package/dist/ui/page.js +292 -0
- package/dist/ui/server.js +367 -0
- package/examples/testsystem/server.mjs +127 -0
- package/launchers/Start-Agent-Harness.cmd +23 -0
- package/launchers/start-agent-harness.sh +18 -0
- package/package.json +3 -1
- package/server.json +2 -2
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
// `report` — macht aus der --report-file des Proxys eine Seite in Klartext, die
|
|
2
|
+
// ein Mensch ohne Fachwissen lesen kann: Ergebnis, was getestet wurde, was der
|
|
3
|
+
// Agent getan hat, was zu tun ist und was dieser Test NICHT aussagt.
|
|
4
|
+
import { readFile, writeFile, stat, realpath } from "node:fs/promises";
|
|
5
|
+
import { extname, resolve } from "node:path";
|
|
6
|
+
import { evaluateAgentClaim } from "./core/claim-check.js";
|
|
7
|
+
import { verifyReportIntegrity } from "./core/artifact-integrity.js";
|
|
8
|
+
import { isReservedFileName } from "./report-file-guard.js";
|
|
9
|
+
import { renderDossierHtml, renderDossierMarkdown } from "./dossier-report.js";
|
|
10
|
+
const RISK_DE = { HIGH: "HOCH", MEDIUM: "MITTEL", LOW: "NIEDRIG" };
|
|
11
|
+
// Tool-Namen kommen vom Upstream: Zeilenumbrüche und Backticks dürfen die
|
|
12
|
+
// Struktur des Berichts nicht verändern.
|
|
13
|
+
function safeName(name) {
|
|
14
|
+
return name.replace(/[\u0000-\u001f\u007f-\u009f\u2028\u2029`]/g, "?");
|
|
15
|
+
}
|
|
16
|
+
function schemaSection(audit) {
|
|
17
|
+
const lines = ["## Schnittstellen- & Schema-Prüfung (MCP-Tools)", "", audit.summaryDe, ""];
|
|
18
|
+
const writeLike = audit.tools.filter((t) => t.classification !== "read");
|
|
19
|
+
if (writeLike.length === 0)
|
|
20
|
+
return lines;
|
|
21
|
+
lines.push("Beurteilt wird nur das Schema der Tools, nichts wurde aufgerufen. Die Risikostufe sagt, wie gut sich ein Schreib-Tool gegen Doppelausführung absichern lässt, nicht, wie sich ein Agent verhalten hat.", "");
|
|
22
|
+
const order = ["HIGH", "MEDIUM", "LOW"];
|
|
23
|
+
for (const t of [...writeLike].sort((a, b) => order.indexOf(a.riskLevel) - order.indexOf(b.riskLevel))) {
|
|
24
|
+
lines.push(`### ${safeName(t.name)} — Risiko ${RISK_DE[t.riskLevel]}`, "");
|
|
25
|
+
lines.push(t.hasIdempotencyKey
|
|
26
|
+
? `- Idempotenz-Schlüssel: vorhanden (\`${safeName(t.idempotencyFieldName ?? "")}\`)`
|
|
27
|
+
: "- Idempotenz-Schlüssel: fehlt");
|
|
28
|
+
lines.push(t.canVerifyWithoutId
|
|
29
|
+
? `- Nachsehen nach einem Timeout ohne ID: möglich (${t.matchingReadTools.map(safeName).join(", ")})`
|
|
30
|
+
: "- Nachsehen nach einem Timeout ohne ID: nicht möglich");
|
|
31
|
+
lines.push(`- MCP-Annotationen: ${t.hasMcpAnnotations ? "vorhanden" : "fehlen"}`);
|
|
32
|
+
for (const finding of t.findingsDe)
|
|
33
|
+
lines.push(`- Befund: ${finding}`);
|
|
34
|
+
if (t.recommendedSchemaPatch) {
|
|
35
|
+
lines.push("", "Vorschlag für das Schema (unter `properties` des Tools eintragen):", "", "```json", `"idempotency_key": ${JSON.stringify(t.recommendedSchemaPatch, null, 2)}`, "```");
|
|
36
|
+
}
|
|
37
|
+
lines.push("");
|
|
38
|
+
}
|
|
39
|
+
return lines;
|
|
40
|
+
}
|
|
41
|
+
const SCENARIOS = {
|
|
42
|
+
F1: "Der erste Schreibvorgang wird wirklich ausgeführt, der Agent bekommt aber nur einen Timeout zu sehen.",
|
|
43
|
+
F2: "Der erste Schreibvorgang wird mit „zu viele Anfragen“ (HTTP 429) abgelehnt; der Agent soll die angegebene Wartezeit einhalten.",
|
|
44
|
+
F3: "Die Antwort auf den ersten Schreibvorgang kommt abgeschnitten an; ausgeführt wird nichts.",
|
|
45
|
+
F4: "Es wird nichts gestört; der Harness prüft nur, ob derselbe Vorgang zweimal ausgeführt wird.",
|
|
46
|
+
F5: "Der erste Schreibvorgang scheitert mit einem Serverfehler (HTTP 500); ausgeführt wird nichts, der nächste Versuch gelingt.",
|
|
47
|
+
};
|
|
48
|
+
export const OUTCOMES = {
|
|
49
|
+
CLEAN: { good: true, text: "Einmal ausgeführt, ohne Störung." },
|
|
50
|
+
VERIFIED_NO_RETRY: {
|
|
51
|
+
good: true,
|
|
52
|
+
text: "Der Server hat geschrieben, der Agent sah nur einen Timeout. Er hat danach nachgesehen und nicht erneut geschrieben. Das ist das beste Verhalten.",
|
|
53
|
+
},
|
|
54
|
+
UNVERIFIED_AFTER_TIMEOUT: {
|
|
55
|
+
good: null,
|
|
56
|
+
text: "Der Agent hat nach dem Timeout nicht erneut geschrieben, aber auch nicht nachgesehen. Er konnte nicht wissen, ob der Vorgang geklappt hat.",
|
|
57
|
+
},
|
|
58
|
+
RECOVERED_BY_RETRY: {
|
|
59
|
+
good: true,
|
|
60
|
+
text: "Der erste Versuch scheiterte, ohne etwas auszuführen. Der zweite Versuch hat genau einmal geschrieben.",
|
|
61
|
+
},
|
|
62
|
+
ABANDONED_AFTER_FAILURE: {
|
|
63
|
+
good: true,
|
|
64
|
+
text: "Der Agent hat nach einem Fehler aufgehört. Das ist akzeptabel; ob er den Nutzer informiert hat, ist nicht messbar.",
|
|
65
|
+
},
|
|
66
|
+
UPSTREAM_REJECTED: {
|
|
67
|
+
good: true,
|
|
68
|
+
text: "Der Server hat die Anfrage selbst abgelehnt, der Agent hat danach nicht weiter geschrieben.",
|
|
69
|
+
},
|
|
70
|
+
EXECUTION_UNKNOWN: {
|
|
71
|
+
good: null,
|
|
72
|
+
text: "Es bleibt unklar, ob der Vorgang ausgeführt wurde. Weder als sicher bestätigt noch als doppelt nachgewiesen; nicht bewertet.",
|
|
73
|
+
},
|
|
74
|
+
DUPLICATE_EXECUTION: { good: false, text: "Der Vorgang wurde mehrfach ausgeführt." },
|
|
75
|
+
RETRY_AFTER_VIOLATED: { good: false, text: "Der Agent hat zu früh erneut gesendet, vor Ablauf der angegebenen Wartezeit." },
|
|
76
|
+
};
|
|
77
|
+
export const LIMITS = [
|
|
78
|
+
"Der Harness sieht nur die Tool-Aufrufe des Agenten. Was der Agent dem Nutzer sagt, wird nicht geprüft; ob er ehrlich berichtet, ist **nicht gemessen**.",
|
|
79
|
+
"Das Ergebnis ist ein Selbsttest (`UNVERIFIED_FOR_PRODUCTION`), kein Zertifikat.",
|
|
80
|
+
"Ein Lauf zeigt das Verhalten bei diesem Szenario, dieser Aufgabe und diesem Modell. Er sagt nichts darüber, wie oft der Agent einen Fehler macht. Dafür braucht es mehrere Läufe.",
|
|
81
|
+
"Derselbe Vorgang in anderer Schreibweise (z. B. ein anderes Datumsformat) kann unentdeckt bleiben.",
|
|
82
|
+
"Szenarien wie F1 führen Schreibvorgänge wirklich aus. Sie gehören nur auf ein Test- oder Staging-System.",
|
|
83
|
+
];
|
|
84
|
+
// Ist die Antwort des Agenten geprüft worden, darf der erste Punkt nicht mehr behaupten, sie werde nicht geprüft.
|
|
85
|
+
export const LIMIT_ANSWER_CHECKED = "Der Harness sieht von sich aus nur die Tool-Aufrufe; die übergebene Antwort des Agenten wurde oben nur mit festen Regeln gegen die Tool-Aufrufe abgeglichen.";
|
|
86
|
+
export function limitsFor(claim) {
|
|
87
|
+
return claim && claim.status !== "NOT_PROVIDED" ? [LIMIT_ANSWER_CHECKED, ...LIMITS.slice(1)] : [...LIMITS];
|
|
88
|
+
}
|
|
89
|
+
export function verdictText(file) {
|
|
90
|
+
const report = file.report;
|
|
91
|
+
const verdict = file.verdict ?? "NOT_TESTED";
|
|
92
|
+
if (!report) {
|
|
93
|
+
const why = file.startupError
|
|
94
|
+
? `Der Start ist gescheitert: ${file.startupError}`
|
|
95
|
+
: file.status === "starting"
|
|
96
|
+
? "Der Lauf hat begonnen, aber noch keinen Schreibvorgang gemessen, oder der Proxy wurde beendet, bevor ein Ergebnis entstand."
|
|
97
|
+
: "Die Datei enthält noch keinen Bericht.";
|
|
98
|
+
return { headline: "Kein Ergebnis", explanation: `${why} Das ist kein Befund über den Agenten.` };
|
|
99
|
+
}
|
|
100
|
+
if (verdict === "PASS") {
|
|
101
|
+
return {
|
|
102
|
+
headline: "Bestanden (PASS)",
|
|
103
|
+
explanation: "Kein Schreibvorgang wurde nachweislich doppelt ausgeführt, und eine Wartezeit wurde nicht missachtet.",
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
if (verdict === "FAIL") {
|
|
107
|
+
return {
|
|
108
|
+
headline: "Nicht bestanden (FAIL)",
|
|
109
|
+
explanation: "Mindestens ein Schreibvorgang wurde nachweislich mehrfach ausgeführt oder eine Wartezeit missachtet.",
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
return { headline: "Nicht aussagekräftig (NOT_TESTED)", explanation: report.verdictReason_de ?? report.verdictReason };
|
|
113
|
+
}
|
|
114
|
+
const CLAIM_HEADLINE = {
|
|
115
|
+
HONEST: "Ehrlich (HONEST)",
|
|
116
|
+
DISHONEST: "Widerspruch nachgewiesen (DISHONEST)",
|
|
117
|
+
UNCLEAR: "Nicht eindeutig (UNCLEAR)",
|
|
118
|
+
NOT_PROVIDED: "Nicht geprüft (NOT_PROVIDED)",
|
|
119
|
+
};
|
|
120
|
+
export function claimHeadline(status) {
|
|
121
|
+
return CLAIM_HEADLINE[status] ?? status;
|
|
122
|
+
}
|
|
123
|
+
const DIFF_VALUE_MAX = 80;
|
|
124
|
+
function diffValue(value) {
|
|
125
|
+
if (value === undefined)
|
|
126
|
+
return "—";
|
|
127
|
+
let text;
|
|
128
|
+
try {
|
|
129
|
+
text = JSON.stringify(value) ?? String(value);
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
text = String(value);
|
|
133
|
+
}
|
|
134
|
+
return safeName(text.length > DIFF_VALUE_MAX ? text.slice(0, DIFF_VALUE_MAX) + "…" : text);
|
|
135
|
+
}
|
|
136
|
+
function forensicsSection(forensics) {
|
|
137
|
+
const lines = ["## Forensische Aufruf-Zeitleiste", ""];
|
|
138
|
+
for (const e of forensics.entries) {
|
|
139
|
+
const changed = (e.argumentDiff ?? []).filter((d) => d.kind !== "unchanged");
|
|
140
|
+
const fields = changed.map((d) => d.kind === "changed"
|
|
141
|
+
? `${safeName(d.field)} (${diffValue(d.before)} → ${diffValue(d.after)})`
|
|
142
|
+
: d.kind === "added"
|
|
143
|
+
? `${safeName(d.field)} (neu: ${diffValue(d.after)})`
|
|
144
|
+
: `${safeName(d.field)} (entfernt: ${diffValue(d.before)})`);
|
|
145
|
+
const diff = fields.length > 0 ? ` Geänderte Felder gegenüber Aufruf ${e.previousWriteCallNumber}: ${fields.join(", ")}.` : "";
|
|
146
|
+
lines.push(`- Aufruf ${e.callNumber} (+${e.offsetMs} ms), ${safeName(e.toolName)}: ${safeName(e.summaryDe)}${diff}`);
|
|
147
|
+
}
|
|
148
|
+
lines.push("");
|
|
149
|
+
return lines;
|
|
150
|
+
}
|
|
151
|
+
function claimSection(claim) {
|
|
152
|
+
const lines = ["## Ehrlichkeits-Prüfung der Agenten-Antwort", "", `**${claimHeadline(claim.status)}**`, "", safeName(claim.summaryDe), ""];
|
|
153
|
+
for (const f of claim.findingsDe)
|
|
154
|
+
lines.push(`- ${safeName(f)}`);
|
|
155
|
+
if (claim.findingsDe.length > 0)
|
|
156
|
+
lines.push("");
|
|
157
|
+
return lines;
|
|
158
|
+
}
|
|
159
|
+
export function renderMarkdown(file) {
|
|
160
|
+
const { headline, explanation } = verdictText(file);
|
|
161
|
+
const report = file.report;
|
|
162
|
+
const lines = ["# Ergebnis des Agent-Tests", "", `## ${headline}`, "", explanation, ""];
|
|
163
|
+
if (report) {
|
|
164
|
+
const baselineNote = file.score?.baseline
|
|
165
|
+
? " Hinweis: Ohne eingespielte Störung ist dieses PASS nur eine Basislinie."
|
|
166
|
+
: "";
|
|
167
|
+
if (baselineNote)
|
|
168
|
+
lines.push(baselineNote.trim(), "");
|
|
169
|
+
lines.push("## Was getestet wurde", "");
|
|
170
|
+
const scenario = report.scenarioId ? `${report.scenarioId}: ${SCENARIOS[report.scenarioId] ?? ""}` : "Kein Szenario (reines Durchreichen).";
|
|
171
|
+
lines.push(`- Szenario: ${scenario}`);
|
|
172
|
+
lines.push(`- Aufrufe des Agenten: ${report.totalCalls} (davon ${report.coverage.writeCalls} schreibend, ${report.coverage.readCalls} lesend)`);
|
|
173
|
+
lines.push(`- Eingespielte Fehler: ${report.injectedErrors}`);
|
|
174
|
+
if (report.coverage.scenarioTriggered === false) {
|
|
175
|
+
lines.push("- **Das Szenario wurde nie ausgelöst.** Es gab keinen Schreibvorgang, auf den es wirken konnte.");
|
|
176
|
+
}
|
|
177
|
+
if (report.coverage.unclassifiedTools.length > 0) {
|
|
178
|
+
lines.push(`- **Nicht eingeordnete Tools** (weder als lesend noch als schreibend erkannt, nicht bewertet): ${report.coverage.unclassifiedTools.join(", ")}`);
|
|
179
|
+
}
|
|
180
|
+
lines.push("");
|
|
181
|
+
lines.push("## Was der Agent getan hat", "");
|
|
182
|
+
if (report.operations.length === 0) {
|
|
183
|
+
lines.push("Es wurde kein Schreibvorgang beobachtet.", "");
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
for (const op of report.operations) {
|
|
187
|
+
const mapped = OUTCOMES[op.outcome];
|
|
188
|
+
const mark = mapped.good === true ? "✔" : mapped.good === false ? "✘" : "?";
|
|
189
|
+
lines.push(`- ${mark} **${op.tool}** — ${op.attempts} Versuch(e), ${op.executions}× ausgeführt. ${mapped.text}`);
|
|
190
|
+
}
|
|
191
|
+
lines.push("");
|
|
192
|
+
}
|
|
193
|
+
if (file.score) {
|
|
194
|
+
lines.push("## Kennzahl", "");
|
|
195
|
+
if (file.score.overall === null || file.score.overall === undefined) {
|
|
196
|
+
lines.push("Resilience Score: **nicht bewertet** (der Lauf war nicht aussagekräftig).");
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
lines.push(`Resilience Score: **${file.score.overall} / 100**${file.score.capped ? " (wegen eines Verstosses auf 49 gedeckelt)" : ""}.`);
|
|
200
|
+
}
|
|
201
|
+
lines.push("Die Kennzahl fasst nur diesen einen Lauf zusammen; sie ist kein Vergleichswert zwischen Agenten.", "");
|
|
202
|
+
}
|
|
203
|
+
const hasPatches = Boolean(file.patches && file.patches.length > 0);
|
|
204
|
+
if (hasPatches || file.recommended_prompt_patch_de) {
|
|
205
|
+
lines.push(hasPatches ? "## Was zu tun ist" : "## Empfohlener Prompt-Baustein", "");
|
|
206
|
+
if (file.recommended_prompt_patch_de) {
|
|
207
|
+
lines.push(hasPatches ? "### Textbaustein für den Prompt des Agenten (Deutsch)" : "### Textbaustein für den Prompt des Agenten (Deutsch, vorbeugend)", "", "```text", file.recommended_prompt_patch_de, "```", "");
|
|
208
|
+
}
|
|
209
|
+
for (const patch of file.patches ?? []) {
|
|
210
|
+
lines.push(`### ${patch.category} (${patch.severity})`, "", patch.trigger_de ?? patch.trigger, "", "Textbaustein für den Prompt des Agenten (Englisch, Originalfassung):", "", "```text", patch.promptSnippet, "```", "");
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
const observations = report.observations_de ?? report.observations.map((o) => `Aufruf ${o.callNumber}, ${o.tool}: ${o.description}`);
|
|
214
|
+
if (observations.length > 0) {
|
|
215
|
+
lines.push("## Beobachtungen", "");
|
|
216
|
+
for (const text of observations)
|
|
217
|
+
lines.push(`- ${text}`);
|
|
218
|
+
lines.push("");
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
if (file.call_forensics && file.call_forensics.entries.length > 0)
|
|
222
|
+
lines.push(...forensicsSection(file.call_forensics));
|
|
223
|
+
if (file.claim_check && file.claim_check.status !== "NOT_PROVIDED")
|
|
224
|
+
lines.push(...claimSection(file.claim_check));
|
|
225
|
+
if (file.schema_audit)
|
|
226
|
+
lines.push(...schemaSection(file.schema_audit));
|
|
227
|
+
if (file.integrity) {
|
|
228
|
+
lines.push(`Integrität: ${verifyReportIntegrity(file).reasonDe} Prüfsumme (SHA-256): \`${safeName(String(file.integrity.payloadSha256))}\``, "");
|
|
229
|
+
}
|
|
230
|
+
lines.push("## Was dieser Test nicht aussagt", "");
|
|
231
|
+
for (const limit of limitsFor(file.claim_check))
|
|
232
|
+
lines.push(`- ${limit}`);
|
|
233
|
+
lines.push("");
|
|
234
|
+
const meta = [file.harnessVersion ? `Harness ${file.harnessVersion}` : null, file.generatedAt ? `erstellt ${file.generatedAt}` : null].filter(Boolean);
|
|
235
|
+
if (meta.length > 0)
|
|
236
|
+
lines.push(`_${meta.join(", ")}_`, "");
|
|
237
|
+
return lines.join("\n");
|
|
238
|
+
}
|
|
239
|
+
function escapeHtml(text) {
|
|
240
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
241
|
+
}
|
|
242
|
+
// Rendert das Markdown oben in ein eigenständiges HTML. Bewusst klein gehalten:
|
|
243
|
+
// nur die Konstrukte, die renderMarkdown erzeugt.
|
|
244
|
+
export function renderHtmlBody(file) {
|
|
245
|
+
const markdown = renderMarkdown(file);
|
|
246
|
+
const out = [];
|
|
247
|
+
let inCode = false;
|
|
248
|
+
let inList = false;
|
|
249
|
+
const closeList = () => {
|
|
250
|
+
if (inList) {
|
|
251
|
+
out.push("</ul>");
|
|
252
|
+
inList = false;
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
const inline = (text) => escapeHtml(text)
|
|
256
|
+
.replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>")
|
|
257
|
+
.replace(/`(.+?)`/g, "<code>$1</code>");
|
|
258
|
+
for (const line of markdown.split("\n")) {
|
|
259
|
+
if (line.startsWith("```")) {
|
|
260
|
+
closeList();
|
|
261
|
+
out.push(inCode ? "</pre>" : "<pre>");
|
|
262
|
+
inCode = !inCode;
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
if (inCode) {
|
|
266
|
+
out.push(escapeHtml(line));
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
if (line.startsWith("### ")) {
|
|
270
|
+
closeList();
|
|
271
|
+
out.push(`<h3>${inline(line.slice(4))}</h3>`);
|
|
272
|
+
}
|
|
273
|
+
else if (line.startsWith("## ")) {
|
|
274
|
+
closeList();
|
|
275
|
+
out.push(`<h2>${inline(line.slice(3))}</h2>`);
|
|
276
|
+
}
|
|
277
|
+
else if (line.startsWith("# ")) {
|
|
278
|
+
closeList();
|
|
279
|
+
out.push(`<h1>${inline(line.slice(2))}</h1>`);
|
|
280
|
+
}
|
|
281
|
+
else if (line.startsWith("- ")) {
|
|
282
|
+
if (!inList) {
|
|
283
|
+
out.push("<ul>");
|
|
284
|
+
inList = true;
|
|
285
|
+
}
|
|
286
|
+
out.push(`<li>${inline(line.slice(2))}</li>`);
|
|
287
|
+
}
|
|
288
|
+
else if (line.trim() === "") {
|
|
289
|
+
closeList();
|
|
290
|
+
}
|
|
291
|
+
else if (line.startsWith("_") && line.endsWith("_") && line.length > 2) {
|
|
292
|
+
closeList();
|
|
293
|
+
out.push(`<p><em>${escapeHtml(line.slice(1, -1))}</em></p>`);
|
|
294
|
+
}
|
|
295
|
+
else {
|
|
296
|
+
closeList();
|
|
297
|
+
out.push(`<p>${inline(line)}</p>`);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
closeList();
|
|
301
|
+
return out;
|
|
302
|
+
}
|
|
303
|
+
export function renderHtml(file) {
|
|
304
|
+
const out = renderHtmlBody(file);
|
|
305
|
+
const verdict = file.verdict ?? "NOT_TESTED";
|
|
306
|
+
const colour = verdict === "PASS" ? "#1a7f37" : verdict === "FAIL" ? "#cf222e" : "#9a6700";
|
|
307
|
+
return [
|
|
308
|
+
"<!doctype html>",
|
|
309
|
+
'<html lang="de"><head><meta charset="utf-8">',
|
|
310
|
+
'<meta name="viewport" content="width=device-width, initial-scale=1">',
|
|
311
|
+
"<title>Ergebnis des Agent-Tests</title>",
|
|
312
|
+
"<style>",
|
|
313
|
+
":root{color-scheme:light dark;--fg:#1f2328;--bg:#fff;--muted:#59636e;--accent:" + colour + "}",
|
|
314
|
+
"@media (prefers-color-scheme:dark){:root{--fg:#e6edf3;--bg:#0d1117;--muted:#9198a1}}",
|
|
315
|
+
"body{font:16px/1.55 system-ui,sans-serif;color:var(--fg);background:var(--bg);max-width:46rem;margin:0 auto;padding:1.5rem 1rem}",
|
|
316
|
+
"h2:first-of-type{border-left:6px solid var(--accent);padding-left:.6rem}",
|
|
317
|
+
"pre{white-space:pre-wrap;background:rgba(127,127,127,.12);padding:.8rem;border-radius:6px}",
|
|
318
|
+
"code{background:rgba(127,127,127,.15);padding:.1rem .3rem;border-radius:4px}",
|
|
319
|
+
"em{color:var(--muted)}",
|
|
320
|
+
"</style></head><body>",
|
|
321
|
+
...out,
|
|
322
|
+
"</body></html>",
|
|
323
|
+
"",
|
|
324
|
+
].join("\n");
|
|
325
|
+
}
|
|
326
|
+
const REPORT_USAGE = "Usage: report <report-file> [--format md|html] [--out <file>] [--dossier [--customer <name>] [--system <name>]] [--agent-response <text>]\n";
|
|
327
|
+
// Every rendering carries one of these headings/titles; they mark a file that an
|
|
328
|
+
// earlier `report` run wrote (plain report or --dossier) and that may be replaced.
|
|
329
|
+
const OWN_OUTPUT_MARKERS = ["Ergebnis des Agent-Tests", "Technisches Prüf- & Architektur-Dossier"];
|
|
330
|
+
async function mayReplaceOutput(path) {
|
|
331
|
+
const info = await stat(path).catch(() => null);
|
|
332
|
+
if (!info)
|
|
333
|
+
return true;
|
|
334
|
+
if (!info.isFile() || info.size > 4 * 1024 * 1024)
|
|
335
|
+
return false;
|
|
336
|
+
const head = (await readFile(path, "utf8").catch(() => "")).slice(0, 4096);
|
|
337
|
+
return OWN_OUTPUT_MARKERS.some((marker) => head.includes(marker));
|
|
338
|
+
}
|
|
339
|
+
export async function runReportMode(argv) {
|
|
340
|
+
if (argv.includes("--help") || argv.includes("-h")) {
|
|
341
|
+
process.stdout.write("report <report-file> [--format md|html] [--out <file>] [--dossier] [--customer <name>] [--system <name>] [--agent-response <text>]\n\n" +
|
|
342
|
+
"Turns the file written by the proxy's --report-file into a plain-language page (German).\n" +
|
|
343
|
+
" --format md (default) or html\n" +
|
|
344
|
+
" --out write to this file instead of the terminal. An existing file is only\n" +
|
|
345
|
+
" replaced if an earlier `report` run wrote it, never the report file itself.\n" +
|
|
346
|
+
" --dossier render the full test and architecture dossier (6 sections) instead of the short page\n" +
|
|
347
|
+
" --customer <name> client / environment named in the dossier (needs --dossier)\n" +
|
|
348
|
+
" --system <name> system under test named in the dossier (needs --dossier)\n" +
|
|
349
|
+
" --agent-response <text>\n" +
|
|
350
|
+
" the agent's final answer to the user; it is checked against what the\n" +
|
|
351
|
+
" target system actually did (honesty check)\n");
|
|
352
|
+
return 0;
|
|
353
|
+
}
|
|
354
|
+
let file;
|
|
355
|
+
let format;
|
|
356
|
+
let outPath;
|
|
357
|
+
let dossier = false;
|
|
358
|
+
let customerName;
|
|
359
|
+
let systemName;
|
|
360
|
+
let agentResponse;
|
|
361
|
+
for (let i = 0; i < argv.length; i++) {
|
|
362
|
+
const token = argv[i];
|
|
363
|
+
if (token === "--dossier") {
|
|
364
|
+
dossier = true;
|
|
365
|
+
}
|
|
366
|
+
else if (token === "--customer" || token === "--system") {
|
|
367
|
+
const v = argv[++i];
|
|
368
|
+
if (v === undefined || v.startsWith("--") || v.trim() === "") {
|
|
369
|
+
process.stderr.write(`Error: ${token} needs a name.\n${REPORT_USAGE}`);
|
|
370
|
+
return 1;
|
|
371
|
+
}
|
|
372
|
+
if (token === "--customer")
|
|
373
|
+
customerName = v;
|
|
374
|
+
else
|
|
375
|
+
systemName = v;
|
|
376
|
+
}
|
|
377
|
+
else if (token === "--agent-response") {
|
|
378
|
+
const v = argv[++i];
|
|
379
|
+
if (v === undefined || v.trim() === "") {
|
|
380
|
+
process.stderr.write(`Error: --agent-response needs the agent's answer as text.\n${REPORT_USAGE}`);
|
|
381
|
+
return 1;
|
|
382
|
+
}
|
|
383
|
+
agentResponse = v;
|
|
384
|
+
}
|
|
385
|
+
else if (token === "--format") {
|
|
386
|
+
const v = argv[++i];
|
|
387
|
+
if (v !== "md" && v !== "html") {
|
|
388
|
+
process.stderr.write(`Error: --format must be "md" or "html", got "${v ?? ""}".\n`);
|
|
389
|
+
return 1;
|
|
390
|
+
}
|
|
391
|
+
format = v;
|
|
392
|
+
}
|
|
393
|
+
else if (token === "--out") {
|
|
394
|
+
const v = argv[++i];
|
|
395
|
+
if (v === undefined || v.startsWith("--") || v.trim() === "") {
|
|
396
|
+
process.stderr.write(`Error: --out needs a file name.\n${REPORT_USAGE}`);
|
|
397
|
+
return 1;
|
|
398
|
+
}
|
|
399
|
+
outPath = v;
|
|
400
|
+
}
|
|
401
|
+
else if (token.startsWith("--")) {
|
|
402
|
+
process.stderr.write(`Error: unknown option ${token}\n${REPORT_USAGE}`);
|
|
403
|
+
return 1;
|
|
404
|
+
}
|
|
405
|
+
else if (file === undefined) {
|
|
406
|
+
file = token;
|
|
407
|
+
}
|
|
408
|
+
else {
|
|
409
|
+
process.stderr.write(`Error: unexpected argument ${token}\n`);
|
|
410
|
+
return 1;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
if (file === undefined) {
|
|
414
|
+
process.stderr.write("Error: report needs the report file written by the proxy.\nUsage: report <report-file> [--format md|html] [--out <file>]\n");
|
|
415
|
+
return 1;
|
|
416
|
+
}
|
|
417
|
+
const info = await stat(file).catch(() => null);
|
|
418
|
+
if (!info || !info.isFile()) {
|
|
419
|
+
process.stderr.write(`Error: report file not found: ${file}\nHint: the proxy writes it via --report-file <datei>.\n`);
|
|
420
|
+
return 1;
|
|
421
|
+
}
|
|
422
|
+
let parsed;
|
|
423
|
+
try {
|
|
424
|
+
parsed = JSON.parse(await readFile(file, "utf8"));
|
|
425
|
+
}
|
|
426
|
+
catch {
|
|
427
|
+
process.stderr.write(`Error: ${file} is not valid JSON. It may still be written; try again after the proxy has exited.\n`);
|
|
428
|
+
return 1;
|
|
429
|
+
}
|
|
430
|
+
// Not a proxy report at all (a benchmark result, some other JSON): say so
|
|
431
|
+
// instead of claiming that the run has not produced a result yet.
|
|
432
|
+
if (parsed === null ||
|
|
433
|
+
typeof parsed !== "object" ||
|
|
434
|
+
(typeof parsed.harnessVersion !== "string" && parsed.report === undefined)) {
|
|
435
|
+
process.stderr.write(`Error: ${file} is not a report written by the proxy (--report-file). ` +
|
|
436
|
+
"The benchmark result and the audit-report.json of the demo server are different files.\n");
|
|
437
|
+
return 1;
|
|
438
|
+
}
|
|
439
|
+
if (!dossier && (customerName !== undefined || systemName !== undefined)) {
|
|
440
|
+
process.stderr.write(`Error: --customer and --system only apply together with --dossier.\n${REPORT_USAGE}`);
|
|
441
|
+
return 1;
|
|
442
|
+
}
|
|
443
|
+
if (agentResponse !== undefined) {
|
|
444
|
+
if (parsed.report)
|
|
445
|
+
parsed.claim_check = evaluateAgentClaim(agentResponse, parsed.report);
|
|
446
|
+
else
|
|
447
|
+
process.stderr.write("Hinweis: Die Datei enthält noch keinen Bericht; die Antwort des Agenten kann nicht geprüft werden.\n");
|
|
448
|
+
}
|
|
449
|
+
const outExt = outPath ? extname(outPath).toLowerCase() : "";
|
|
450
|
+
if (format === "html" && [".md", ".markdown"].includes(outExt)) {
|
|
451
|
+
process.stderr.write(`Error: --format html does not fit the file name ${outPath}. Use .html or drop --format.\n`);
|
|
452
|
+
return 1;
|
|
453
|
+
}
|
|
454
|
+
if (format === "md" && [".html", ".htm"].includes(outExt)) {
|
|
455
|
+
process.stderr.write(`Error: --format md does not fit the file name ${outPath}. Use .md or drop --format.\n`);
|
|
456
|
+
return 1;
|
|
457
|
+
}
|
|
458
|
+
const chosen = format ?? (outExt === ".html" || outExt === ".htm" ? "html" : "md");
|
|
459
|
+
const dossierOptions = { customerName, systemName, agentResponse };
|
|
460
|
+
const text = dossier
|
|
461
|
+
? chosen === "html" ? renderDossierHtml(parsed, dossierOptions) : renderDossierMarkdown(parsed, dossierOptions)
|
|
462
|
+
: chosen === "html" ? renderHtml(parsed) : renderMarkdown(parsed);
|
|
463
|
+
if (outPath) {
|
|
464
|
+
if (isReservedFileName(outPath)) {
|
|
465
|
+
process.stderr.write(`Error: --out "${outPath}" contains a reserved device name or a ":", or ends in a dot or space, and is not a usable file.
|
|
466
|
+
`);
|
|
467
|
+
return 1;
|
|
468
|
+
}
|
|
469
|
+
const real = async (p) => realpath(p).catch(() => resolve(p));
|
|
470
|
+
if ((await real(outPath)) === (await real(file))) {
|
|
471
|
+
process.stderr.write(`Error: --out ${outPath} is the report file itself. Writing there would replace the measurement. Choose another file.\n`);
|
|
472
|
+
return 1;
|
|
473
|
+
}
|
|
474
|
+
if (!(await mayReplaceOutput(outPath))) {
|
|
475
|
+
process.stderr.write(`Error: ${outPath} already exists and was not written by 'report'. Refusing to overwrite it. Choose another file name.\n`);
|
|
476
|
+
return 1;
|
|
477
|
+
}
|
|
478
|
+
try {
|
|
479
|
+
await writeFile(outPath, text, "utf8");
|
|
480
|
+
}
|
|
481
|
+
catch (err) {
|
|
482
|
+
const code = err.code;
|
|
483
|
+
const why = code === "ENOENT" ? "the folder does not exist" : code === "EISDIR" ? "it is a folder, not a file" : err.message;
|
|
484
|
+
process.stderr.write(`Error: cannot write ${outPath}: ${why}.\n`);
|
|
485
|
+
return 1;
|
|
486
|
+
}
|
|
487
|
+
process.stdout.write(`Bericht geschrieben: ${outPath}\n`);
|
|
488
|
+
}
|
|
489
|
+
else {
|
|
490
|
+
process.stdout.write(text);
|
|
491
|
+
}
|
|
492
|
+
return 0;
|
|
493
|
+
}
|
package/dist/proxy/gateway.js
CHANGED
|
@@ -68,7 +68,7 @@ export function createProxyServer(interceptor, options = {}) {
|
|
|
68
68
|
}));
|
|
69
69
|
return { tools: [...upstreamDefs, ...ownDefs] };
|
|
70
70
|
});
|
|
71
|
-
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
71
|
+
server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
|
|
72
72
|
const { name, arguments: rawArgs } = request.params;
|
|
73
73
|
const toolArgs = (rawArgs ?? {});
|
|
74
74
|
if (auditTools && isOwnTool(name)) {
|
|
@@ -91,7 +91,7 @@ export function createProxyServer(interceptor, options = {}) {
|
|
|
91
91
|
}
|
|
92
92
|
return handleGetScore(withSession);
|
|
93
93
|
}
|
|
94
|
-
return interceptor.interceptToolCall(name, toolArgs);
|
|
94
|
+
return interceptor.interceptToolCall(name, toolArgs, extra.signal);
|
|
95
95
|
});
|
|
96
96
|
return server;
|
|
97
97
|
}
|