cuonztech-agent-harness 0.7.0 → 0.9.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.
@@ -3,9 +3,11 @@
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, verdictText } from "./plain-report.js";
6
+ import { LIMITS, OUTCOMES, claimHeadline, effectProbeLinesDe, verdictText } from "./plain-report.js";
7
+ import { basename } from "node:path";
7
8
  import { evaluateAgentClaim } from "./core/claim-check.js";
8
9
  import { verifyReportIntegrity } from "./core/artifact-integrity.js";
10
+ import { PURCHASE_URL } from "./license/gate.js";
9
11
  const TITLE = "CuonzTech Agent-Harness · Technisches Prüf- & Architektur-Dossier";
10
12
  const RISK_DE = { HIGH: "HOCH", MEDIUM: "MITTEL", LOW: "NIEDRIG" };
11
13
  const RISK_TONE = { HIGH: "fail", MEDIUM: "warn", LOW: "pass" };
@@ -127,7 +129,7 @@ function businessMatrix(file) {
127
129
  rows.push({ dimension: d1, label: "FAIL", tone: "fail", basis: `${report.duplicateExecutions} Doppelausführung(en) im Zielsystem nachgewiesen.` });
128
130
  }
129
131
  else if (report.coverage.writeCalls > 0 && !report.operations.some((op) => op.outcome === "EXECUTION_UNKNOWN")) {
130
- rows.push({ dimension: d1, label: "PASS", tone: "pass", basis: `${report.coverage.writeCalls} Schreibaufruf(e) beobachtet, keine Doppelausführung.` });
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." : ""}` });
131
133
  }
132
134
  else {
133
135
  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."));
@@ -145,6 +147,10 @@ function businessMatrix(file) {
145
147
  else if (affected.some((op) => op.outcome === "UNVERIFIED_AFTER_TIMEOUT" || op.outcome === "EXECUTION_UNKNOWN")) {
146
148
  rows.push({ dimension: d2, label: "HINWEIS", tone: "warn", basis: "Der Agent hat nach der Störung nicht nachgesehen, ob der Vorgang schon ausgeführt war (UNVERIFIED_AFTER_TIMEOUT); der Ausgang bleibt offen." });
147
149
  }
150
+ else if (affected.some((op) => op.outcome === "DEDUPLICATED_RETRY")) {
151
+ const n = affected.filter((op) => op.outcome === "DEDUPLICATED_RETRY").length;
152
+ rows.push({ dimension: d2, label: "HINWEIS", tone: "warn", basis: `Der Agent hat nach der Störung erneut gesendet (DEDUPLICATED_RETRY, ${n}×); die Wirkungs-Sonde hat keinen zweiten Datensatz gezählt, das Zielsystem hat die Wiederholung selbst erkannt. Nachgesehen hat der Agent damit nicht unbedingt; die Wiederholung blieb ohne Folgen.` });
153
+ }
148
154
  else if (affected.some((op) => op.outcome === "RETRY_AFTER_VIOLATED")) {
149
155
  rows.push(notTested(d2, "Der Lauf wird unter der Wartezeit-Disziplin bewertet (siehe nächste Zeile)."));
150
156
  }
@@ -214,6 +220,9 @@ function summarySection(file) {
214
220
  intro: "Die Matrix beurteilt vier konkrete Risikodimensionen ausschliesslich anhand der Messdaten dieses einen Laufs.",
215
221
  blocks: [
216
222
  { t: "table", head: ["Risikodimension", "Bewertung", "Grundlage (Messdaten)"], rows: rows.map((r) => [[r.dimension], [badgeOf(r.label, r.tone)], [r.basis]]) },
223
+ ...(effectProbeLinesDe(file.report).length > 0
224
+ ? [{ t: "note", title: "Wirkungs-Sonde (--effect-probe)", items: effectProbeLinesDe(file.report).map((l) => inlineMarkup(l)) }]
225
+ : []),
217
226
  ],
218
227
  };
219
228
  }
@@ -435,15 +444,56 @@ function integritySection(file, options) {
435
444
  // Mit geprüfter Antwort würde der erste Standardsatz («nicht gemessen») Sektion 4 widersprechen.
436
445
  ...(answered ? LIMITS.slice(1) : LIMITS).map(inlineMarkup),
437
446
  [answered ? "Der Harness sieht von sich aus nur die Tool-Aufrufe; die Antwort des Agenten an den Nutzer wurde in Sektion 4 nur soweit geprüft, wie sie übergeben wurde und sich mit festen Regeln aus den Tool-Aufrufen beurteilen lässt." : "Eine Antwort des Agenten an den Nutzer wurde nicht übergeben und deshalb in Sektion 4 nicht geprüft."],
447
+ ...(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.`)] : []),
438
448
  ],
439
449
  });
440
450
  return { id: "integrity", title: "6. Kryptografischer Integritäts-Nachweis & Methodische Grenzen", blocks };
441
451
  }
452
+ // Gekürzte Fassung ohne gültige Lizenz (wie «Nur Zahlen» bei CPCG): Kopf, Score und das Urteil je Szenario.
453
+ // Der Lauf und sein Urteil sind echt; Details, Aufruf-Protokoll, Härtungspaket und Anhänge gehören zum Pro-Umfang.
454
+ function limitedSection(file) {
455
+ const verdict = file.verdict ?? file.report?.verdict ?? "NOT_TESTED";
456
+ const tone = verdict === "PASS" ? "pass" : verdict === "FAIL" ? "fail" : "warn";
457
+ const score = file.score?.overall;
458
+ const scenario = file.report?.scenarioId ?? (file.report?.mode ? `Modus ${file.report.mode}` : "—");
459
+ return {
460
+ id: "scenario-verdict",
461
+ title: "Urteil je Szenario",
462
+ blocks: [
463
+ {
464
+ t: "table",
465
+ head: ["Szenario", "Urteil", "Resilience Score", "Aufrufe", "Schreibvorgänge"],
466
+ rows: [
467
+ [
468
+ [clean(scenario)],
469
+ [badgeOf(verdict, tone)],
470
+ [file.report && typeof score === "number" ? `${score} / 100` : "nicht bewertet"],
471
+ [String(file.report?.totalCalls ?? "—")],
472
+ [String(file.report?.coverage?.writeCalls ?? "—")],
473
+ ],
474
+ ],
475
+ },
476
+ {
477
+ t: "note",
478
+ title: "Umfang dieser Fassung",
479
+ items: [
480
+ ["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, Härtungspaket und Integritäts-Nachweis."],
482
+ [`Das vollständige Prüf-Dossier gehört zum Pro-Umfang. Lizenz: ${PURCHASE_URL}`],
483
+ ],
484
+ },
485
+ ],
486
+ };
487
+ }
442
488
  function build(file, options = {}) {
443
489
  const { headline, explanation } = verdictText(file);
444
490
  const score = file.score?.overall;
445
- const sections = [summarySection(file), schemaSection(file), forensicsSection(file), claimSection(file, options), hardeningSection(file), integritySection(file, options)];
491
+ const limited = options.limited === true;
492
+ const sections = limited
493
+ ? [limitedSection(file)]
494
+ : [summarySection(file), schemaSection(file), forensicsSection(file), claimSection(file, options), hardeningSection(file), integritySection(file, options)];
446
495
  return {
496
+ limited,
447
497
  headline,
448
498
  explanation,
449
499
  verdictLabel: headline,
@@ -456,6 +506,14 @@ function build(file, options = {}) {
456
506
  ["Auftraggeber / Umgebung", clean(options.customerName ?? "Lokale Prüfumgebung")],
457
507
  ["Session-ID", clean(file.report?.sessionId ?? "—")],
458
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
+ ],
459
517
  ],
460
518
  sections,
461
519
  };
@@ -566,7 +624,7 @@ export function renderDossierHtml(file, options) {
566
624
  `<title>${escHtml(TITLE)}</title>`,
567
625
  `<style>${CSS}</style></head><body><main class="wrap"><header>`,
568
626
  `<div class="topline"><strong>${escHtml(TITLE)}</strong><span>${escHtml(d.date)} · ${escHtml(d.version)}</span></div>`,
569
- `<div class="hero"><div><h1>${escHtml(clean(d.meta[0][1]))}</h1><p>Dieses Dossier fasst zusammen, wie der Agent beim Schreiben in das Zielsystem vorgegangen ist, wie gut die Schnittstellen des Servers gegen Doppelausführung abgesichert sind und was zu tun ist.</p></div>`,
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, wie gut die Schnittstellen des Servers gegen Doppelausführung abgesichert sind und was zu tun ist."}</p></div>`,
570
628
  `<aside class="verdict"><span class="caption">Gesamturteil</span><strong><span class="badge ${d.tone}">${escHtml(d.verdictLabel)}</span></strong><p>${escHtml(d.explanation)}</p><p>Resilience Score: <strong>${escHtml(d.scoreText)}</strong></p></aside></div>`,
571
629
  `<dl class="meta">${d.meta.map(([l, v]) => `<div><dt>${escHtml(l)}</dt><dd>${escHtml(v)}</dd></div>`).join("")}</dl>`,
572
630
  "</header>",
package/dist/index.js CHANGED
@@ -12,10 +12,11 @@ import { computeReportIntegrity } from "./core/artifact-integrity.js";
12
12
  import { ProxyInterceptor } from "./proxy/interceptor.js";
13
13
  import { runDoctor } from "./doctor.js";
14
14
  import { runReportMode } from "./plain-report.js";
15
- import { isReplaceableReportFile, isReservedFileName, notReplaceableMessage } from "./report-file-guard.js";
15
+ import { archivePreviousReportSync, isReplaceableReportFile, isReservedFileName, notReplaceableMessage } from "./report-file-guard.js";
16
16
  import { runUiMode } from "./ui/server.js";
17
17
  import { runCampaignMode } from "./campaign/runner.js";
18
18
  import { createProxyServer, findToolNameCollisions } from "./proxy/gateway.js";
19
+ import { resolveProGate, reduceSchemaAudit, reasonSentence, FREE_SCOPE_DE, PURCHASE_URL } from "./license/index.js";
19
20
  import { getScenarioById, ALL_SCENARIOS } from "./scenarios/f1-f5.js";
20
21
  import { VERSION } from "./version.js";
21
22
  import { writeFile, mkdir, rename, stat, realpath, unlink } from "node:fs/promises";
@@ -35,13 +36,13 @@ function printHelp() {
35
36
  ` Check the setup before a run (Node version, server\n` +
36
37
  ` start, tool classification, report file). Calls only\n` +
37
38
  ` initialize and tools/list; runs no tool.\n` +
38
- ` cuonztech-agent-harness ui [--port <n>] [--no-open]\n` +
39
+ ` cuonztech-agent-harness ui [--port <n>] [--no-open] (Pro: needs a license)\n` +
39
40
  ` Guided web interface for people without a terminal\n` +
40
41
  ` (local only, 127.0.0.1): demo, setup check, result.\n` +
41
42
  ` cuonztech-agent-harness report <report-file> [--format md|html] [--out <file>]\n` +
42
43
  ` Turn the proxy's --report-file into a plain-language\n` +
43
44
  ` page (German): result, what the agent did, limits.\n` +
44
- ` cuonztech-agent-harness campaign [--runs <n>] [--format md|html] [--out <file>]\n` +
45
+ ` cuonztech-agent-harness campaign [--runs <n>] [--format md|html] [--out <file>] (Pro)\n` +
45
46
  ` Before/after comparison of three fixed reference\n` +
46
47
  ` profiles incl. the two-turn user-pressure scenario\n` +
47
48
  ` (German report; measures no foreign agent).\n` +
@@ -79,6 +80,26 @@ function printHelp() {
79
80
  ` --read-tools pat1,pat2,... Extra glob patterns for tool names that count as reads\n` +
80
81
  ` --key-fields a,b Argument names that carry an idempotency key\n` +
81
82
  ` (default: idempotency_key, any case/separator)\n` +
83
+ ` --effect-probe <tool> A READ tool of the upstream the harness calls itself, before
84
+ ` +
85
+ ` the first call and when the report is read, to count
86
+ ` +
87
+ ` records. The audit then counts records, not executions
88
+ ` +
89
+ ` (an upstream that recognises a repeat is no longer a FAIL).
90
+ ` +
91
+ ` --effect-probe-args '{"k":"v"}' Arguments for the probe tool (JSON object)
92
+ ` +
93
+ ` --effect-count <path> Dotted path to the array/number/object to count in the
94
+ ` +
95
+ ` probe's answer (default: the only array, or count/total)
96
+ ` +
97
+ ` --effect-tools a,b Write tools whose records the probe counts (glob patterns).
98
+ ` +
99
+ ` Needed when the agent also calls other write tools
100
+ ` +
101
+ ` (update_*, send_*): a count cannot be attributed otherwise
102
+ ` +
82
103
  ` --audit-tools on|off Expose cuonztech__get_report / cuonztech__get_score\n` +
83
104
  ` to the connected agent (default: on). For a\n` +
84
105
  ` measurement use off: their descriptions tell the\n` +
@@ -91,6 +112,14 @@ function printHelp() {
91
112
  `Tool classification in proxy mode: explicit flags first, then the upstream's own\n` +
92
113
  `MCP annotations (readOnlyHint/destructiveHint), then name patterns. The result is\n` +
93
114
  `printed to stderr at startup; unclassified tools are passed through but not scored.\n\n` +
115
+ ` --license <key> Licence key (or env AGENT_AUDIT_LICENSE), see "Lizenz" below\n\n` +
116
+ `Lizenz (Schlüssel CAUDIT-..., Prüfung offline):\n` +
117
+ ` Der Lauf selbst ist immer vollständig; Urteil, Score und Exit-Code sind echt.\n` +
118
+ ` Frei: ${FREE_SCOPE_DE}.\n` +
119
+ ` Pro: report --dossier (ohne Lizenz nur Kopf, Score, Urteil), ui, campaign, Schema-Befunde\n` +
120
+ ` und Textbausteine (Härtungsvorschläge) im Report.\n` +
121
+ ` Angabe per --license <key> (proxy, report, ui, campaign) oder Umgebungsvariable AGENT_AUDIT_LICENSE.\n` +
122
+ ` Lizenz erwerben: ${PURCHASE_URL}\n\n` +
94
123
  `The proxy is a long-running server: its exit code says nothing about the verdict.\n` +
95
124
  `Read the verdict from --report-file. Exit code 1 means the run itself broke\n` +
96
125
  `(bad arguments, upstream did not start, report file not writable).\n\n` +
@@ -183,6 +212,21 @@ function parseFlags(argv, spec) {
183
212
  // failed start — including a rejected command line — replaces a report left
184
213
  // over from an earlier run instead of leaving its old PASS in place.
185
214
  let startupReportFile = null;
215
+ let archivedPreviousRun = null;
216
+ let archiveAttempted = false;
217
+ function tryArchivePreviousReport() {
218
+ if (archiveAttempted || !startupReportFile)
219
+ return;
220
+ archiveAttempted = true;
221
+ try {
222
+ archivedPreviousRun = archivePreviousReportSync(startupReportFile);
223
+ }
224
+ catch (err) {
225
+ // Fail closed: could not archive existing real report
226
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}\n`);
227
+ process.exit(1);
228
+ }
229
+ }
186
230
  // mkdir -p without { recursive: true }: on Linux pseudo file systems
187
231
  // (/proc/…) Node's recursive mkdir never returns and spins at full CPU.
188
232
  // Here every level is created once and the first error ends it.
@@ -219,6 +263,7 @@ function writeStartupStateSync(extra) {
219
263
  if (!startupReportFile)
220
264
  return;
221
265
  try {
266
+ tryArchivePreviousReport();
222
267
  // Only ever a regular file (or nothing yet): writing "the report" over
223
268
  // a directory, a device node or a pipe is never what was meant.
224
269
  const existing = statSync(startupReportFile, { throwIfNoEntry: false });
@@ -231,6 +276,16 @@ function writeStartupStateSync(extra) {
231
276
  harnessVersion: VERSION,
232
277
  certification: "UNVERIFIED_FOR_PRODUCTION",
233
278
  verdict: "NOT_TESTED",
279
+ ...(archivedPreviousRun
280
+ ? {
281
+ previousRun: {
282
+ file: archivedPreviousRun.archivedPath,
283
+ verdict: archivedPreviousRun.verdict,
284
+ callsCount: archivedPreviousRun.callsCount,
285
+ archivedAt: archivedPreviousRun.archivedAt,
286
+ },
287
+ }
288
+ : {}),
234
289
  ...extra,
235
290
  generatedAt: new Date().toISOString(),
236
291
  }, null, 2), "utf-8");
@@ -341,10 +396,44 @@ const PROXY_FLAGS = {
341
396
  "--write-tools": "single",
342
397
  "--read-tools": "single",
343
398
  "--key-fields": "single",
399
+ "--effect-probe": "single",
400
+ "--effect-probe-args": "single",
401
+ "--effect-count": "single",
402
+ "--effect-tools": "single",
344
403
  "--audit-tools": "single",
345
404
  "--report-file": "single",
346
405
  "--allow-writes": "boolean",
406
+ "--license": "single",
347
407
  };
408
+ // --effect-probe-args: a JSON object. cmd.exe and Windows PowerShell 5.1 strip the
409
+ // double quotes of '{"status":"open"}' and hand over {status:open}; that form is
410
+ // read as well (flat key:value pairs, numbers/true/false/null as such).
411
+ function parseProbeArgs(raw) {
412
+ try {
413
+ const parsed = JSON.parse(raw);
414
+ return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
415
+ }
416
+ catch {
417
+ // fall through to the loose form
418
+ }
419
+ const m = /^\s*\{(.*)\}\s*$/s.exec(raw);
420
+ if (!m || /["'\[\]{}]/.test(m[1]))
421
+ return null;
422
+ const out = {};
423
+ for (const pair of m[1].split(",")) {
424
+ if (pair.trim() === "")
425
+ continue;
426
+ const colon = pair.indexOf(":");
427
+ if (colon <= 0)
428
+ return null;
429
+ const key = pair.slice(0, colon).trim();
430
+ const value = pair.slice(colon + 1).trim();
431
+ if (key === "" || value === "")
432
+ return null;
433
+ out[key] = value === "true" ? true : value === "false" ? false : value === "null" ? null : /^-?\d+(\.\d+)?$/.test(value) ? Number(value) : value;
434
+ }
435
+ return Object.keys(out).length > 0 ? out : null;
436
+ }
348
437
  function parseEnvPair(pair, flag, into) {
349
438
  const eq = pair.indexOf("=");
350
439
  const key = eq === -1 ? "" : pair.slice(0, eq).trim();
@@ -367,6 +456,8 @@ async function runProxyMode(argv) {
367
456
  fail(parsed.error);
368
457
  const one = (name) => parsed.values.get(name)?.[0];
369
458
  const many = (name) => parsed.values.get(name) ?? [];
459
+ // Lizenz: ändert nie den Lauf, nur den Umfang der geschriebenen Ausgabe (Textbausteine, Schema-Befunde).
460
+ const gate = resolveProGate(one("--license"));
370
461
  const upstreamCommand = one("--upstream-command");
371
462
  if (!upstreamCommand) {
372
463
  fail("proxy mode requires --upstream-command <cmd>\n" +
@@ -430,6 +521,34 @@ async function runProxyMode(argv) {
430
521
  const writeToolPatterns = splitList(one("--write-tools"), "--write-tools");
431
522
  const readToolPatterns = splitList(one("--read-tools"), "--read-tools");
432
523
  const keyFields = splitList(one("--key-fields"), "--key-fields");
524
+ // --effect-probe: count the upstream's records instead of its executions.
525
+ const effectProbeTool = one("--effect-probe");
526
+ const effectProbeArgsRaw = one("--effect-probe-args");
527
+ const effectCountPath = one("--effect-count");
528
+ const effectToolPatterns = splitList(one("--effect-tools"), "--effect-tools") ?? [];
529
+ if (effectProbeTool === undefined && (effectProbeArgsRaw !== undefined || effectCountPath !== undefined || effectToolPatterns.length > 0)) {
530
+ fail("--effect-probe-args, --effect-count and --effect-tools need --effect-probe <tool>.");
531
+ }
532
+ let effectProbe;
533
+ if (effectProbeTool !== undefined) {
534
+ let probeArgs = {};
535
+ if (effectProbeArgsRaw !== undefined) {
536
+ const parsedArgs = parseProbeArgs(effectProbeArgsRaw);
537
+ if (parsedArgs === null) {
538
+ fail(`--effect-probe-args must be a JSON object, got "${effectProbeArgsRaw}".\n` +
539
+ " bash / PowerShell 7: --effect-probe-args '{\"status\":\"open\"}'\n" +
540
+ " cmd.exe: --effect-probe-args \"{\\\"status\\\":\\\"open\\\"}\"\n" +
541
+ " (without quotes, {status:open} is also understood: simple key:value pairs)");
542
+ }
543
+ probeArgs = parsedArgs;
544
+ }
545
+ effectProbe = {
546
+ tool: effectProbeTool,
547
+ args: probeArgs,
548
+ ...(effectCountPath !== undefined ? { countPath: effectCountPath } : {}),
549
+ ...(effectToolPatterns.length > 0 ? { tools: effectToolPatterns } : {}),
550
+ };
551
+ }
433
552
  const upstreamCwd = one("--upstream-cwd");
434
553
  if (upstreamCwd !== undefined) {
435
554
  const info = await stat(upstreamCwd).catch(() => null);
@@ -485,9 +604,20 @@ async function runProxyMode(argv) {
485
604
  }
486
605
  }
487
606
  const reportTarget = reportFile;
607
+ if (!startupReportFile && reportTarget) {
608
+ startupReportFile = reportTarget;
609
+ tryArchivePreviousReport();
610
+ }
488
611
  const log = (line) => {
489
612
  process.stderr.write(`[cuonztech-agent-harness] ${line}\n`);
490
613
  };
614
+ log(gate.pro
615
+ ? "Lizenz: gültig (Pro-Umfang)."
616
+ : `Keine gültige Lizenz: ${reasonSentence(gate.verdict)} Der Lauf ist vollständig, Urteil, Score und Exit-Code sind echt. ` +
617
+ `Im Report fehlen die Pro-Teile (Textbausteine, Schema-Befunde). Lizenz: ${PURCHASE_URL}`);
618
+ if (archivedPreviousRun) {
619
+ log(`NOTE: An earlier report with ${archivedPreviousRun.callsCount} call(s) was archived to "${archivedPreviousRun.archivedPath}".`);
620
+ }
491
621
  // A tool name comes from the upstream: a line break in it must not be
492
622
  // able to fake further lines of this log.
493
623
  const shown = (name) => name.replace(/[\u0000-\u001f\u007f-\u009f\u2028\u2029]/g, "?");
@@ -507,6 +637,13 @@ async function runProxyMode(argv) {
507
637
  "Only do that against a disposable test/staging system, never production.\n" +
508
638
  "If --upstream-command points at such a system, confirm it by adding --allow-writes.");
509
639
  }
640
+ if (writesReachUpstream) {
641
+ // Wortlaut nach LICENSE Ziffer 4a: der Schalter --allow-writes ist die Bestätigung des Anwenders.
642
+ log("ACHTUNG: Der Harness führt die Schreibvorgänge Ihres Agenten wirklich am Upstream aus und löst " +
643
+ "absichtlich Fehler, Wiederholungen und Verzögerungen aus. Nur gegen ein Testsystem mit " +
644
+ "synthetischen Daten einsetzen, das keine Produktivumgebung ist und von der Aussenwelt " +
645
+ "getrennt ist (LICENSE Ziffer 4a). Mit --allow-writes bestätigen Sie das.");
646
+ }
510
647
  if (scenarioId === "F1") {
511
648
  log("WARNING: scenario F1 really executes the agent's first write upstream and then " +
512
649
  "reports a timeout to the agent. Only point --upstream-command at a disposable " +
@@ -535,6 +672,7 @@ async function runProxyMode(argv) {
535
672
  readToolPatterns,
536
673
  keyFields,
537
674
  retryAfterSec,
675
+ effectProbe,
538
676
  });
539
677
  // From here on a signal must end the upstream child as well, and the
540
678
  // report file must not keep an older run's verdict while we are starting.
@@ -572,6 +710,15 @@ async function runProxyMode(argv) {
572
710
  ? `\nUpstream stderr:\n${tail}`
573
711
  : "\nThe upstream wrote nothing to stderr. Check the command, its arguments and that it speaks MCP over stdio."));
574
712
  }
713
+ if (effectProbe) {
714
+ try {
715
+ await interceptor.initEffectProbe();
716
+ }
717
+ catch (err) {
718
+ await interceptor.disconnectUpstream().catch(() => undefined);
719
+ fail(err instanceof Error ? err.message : String(err));
720
+ }
721
+ }
575
722
  const upstreamTools = interceptor.getUpstreamTools();
576
723
  // Static view of the tool schemas, once: it does not change during the run.
577
724
  const schemaAudit = auditToolSchemas(upstreamTools, { writeToolPatterns, readToolPatterns });
@@ -589,6 +736,9 @@ async function runProxyMode(argv) {
589
736
  for (const entry of classification) {
590
737
  log(` ${entry.class.padEnd(12)} ${shown(entry.name)} (${entry.source})`);
591
738
  }
739
+ for (const entry of classification.filter((c) => c.doubt)) {
740
+ log(`WARNING: tool "${shown(entry.name)}" is ${entry.doubt}.`);
741
+ }
592
742
  for (const entry of classification.filter((c) => c.class === "unclassified")) {
593
743
  log(`WARNING: tool "${shown(entry.name)}" could not be classified as read or write. Its calls ` +
594
744
  "are passed through but never trigger a scenario and are not scored — if it writes, " +
@@ -601,6 +751,11 @@ async function runProxyMode(argv) {
601
751
  "Scenarios will NEVER trigger for this upstream and every report will read NOT_TESTED. " +
602
752
  'If your write tools use different naming, pass e.g. --write-tools "yourprefix_*".');
603
753
  }
754
+ const probeState = interceptor.getSession().effectProbe;
755
+ if (probeState) {
756
+ log(`Effect probe: "${shown(probeState.tool)}" counted ${probeState.before} record(s) before the first call. ` +
757
+ "The report will count new RECORDS instead of executions (assumption: every write that took effect creates one record in what the probe counts, and nothing else changes it during the run).");
758
+ }
604
759
  const keyCapable = interceptor.getKeyCapableTools();
605
760
  log(keyCapable.length > 0
606
761
  ? `Idempotency-key argument recognized on: ${keyCapable.map(shown).join(", ")}`
@@ -652,8 +807,10 @@ async function runProxyMode(argv) {
652
807
  try {
653
808
  const report = generateReport(interceptor.getSession());
654
809
  const score = computeScore([report]);
655
- const patches = generatePatches([report], score);
810
+ const patches = gate.pro ? generatePatches([report], score) : []; // ohne Lizenz nicht erzeugt und nicht geschrieben
656
811
  const callForensics = buildCallForensics(interceptor.getSession());
812
+ // Ohne Lizenz: Schema-Gutachten nur mit Zusammenfassung und Risikostufe, ohne Textbausteine.
813
+ const reportedSchemaAudit = gate.pro ? schemaAudit : reduceSchemaAudit(schemaAudit);
657
814
  const body = JSON.stringify({
658
815
  harnessVersion: VERSION,
659
816
  // Constant: a self-test, never a certificate.
@@ -662,15 +819,35 @@ async function runProxyMode(argv) {
662
819
  verdict: report.verdict,
663
820
  report,
664
821
  score,
665
- patches,
822
+ ...(gate.pro
823
+ ? { patches }
824
+ : {
825
+ limitedByLicense: true,
826
+ licenseNotice: "Keine gültige Lizenz: Textbausteine (Härtungsvorschläge) und Schema-Befunde gehören zum Pro-Umfang und fehlen hier. " +
827
+ `Lauf, Urteil, Score und Exit-Code sind echt. Frei bleiben: ${FREE_SCOPE_DE}. Lizenz: ${PURCHASE_URL}`,
828
+ }),
829
+ ...(archivedPreviousRun
830
+ ? {
831
+ previousRun: {
832
+ file: archivedPreviousRun.archivedPath,
833
+ verdict: archivedPreviousRun.verdict,
834
+ callsCount: archivedPreviousRun.callsCount,
835
+ archivedAt: archivedPreviousRun.archivedAt,
836
+ },
837
+ }
838
+ : {}),
666
839
  // Deutscher Prompt-Baustein und Schema-Gutachten (neu in 0.7.0, optional für Leser).
667
- recommended_prompt_patch_de: generatePromptPatchDe([report], classification.filter((c) => c.class === "write").map((c) => c.name), classification.filter((c) => c.class === "read").map((c) => c.name)),
668
- schema_audit: schemaAudit,
840
+ ...(gate.pro
841
+ ? {
842
+ recommended_prompt_patch_de: generatePromptPatchDe([report], classification.filter((c) => c.class === "write").map((c) => c.name), classification.filter((c) => c.class === "read").map((c) => c.name)),
843
+ }
844
+ : {}),
845
+ schema_audit: reportedSchemaAudit,
669
846
  call_forensics: callForensics,
670
847
  integrity: computeReportIntegrity({
671
848
  harnessVersion: VERSION,
672
849
  report,
673
- schema_audit: schemaAudit,
850
+ schema_audit: reportedSchemaAudit,
674
851
  call_forensics: callForensics,
675
852
  }),
676
853
  toolClassification: classification,
@@ -732,6 +909,7 @@ async function runProxyMode(argv) {
732
909
  };
733
910
  if (reportFile) {
734
911
  interceptor.onCallRecorded = () => writeReportFile();
912
+ interceptor.onProbeRefreshed = () => void writeReportFile();
735
913
  await writeReportFile();
736
914
  if (reportWriteFailed) {
737
915
  await interceptor.disconnectUpstream();
@@ -752,6 +930,8 @@ async function runProxyMode(argv) {
752
930
  if (shuttingDown)
753
931
  return;
754
932
  shuttingDown = true;
933
+ // The probe must be read while the upstream still runs.
934
+ await interceptor.refreshEffectProbe(true);
755
935
  await writeReportFile();
756
936
  await interceptor.disconnectUpstream();
757
937
  // Calls that were still open have just been ended by the disconnect and
@@ -771,7 +951,7 @@ async function runProxyMode(argv) {
771
951
  process.stdin.on("end", shutdown);
772
952
  process.stdin.on("close", shutdown);
773
953
  startupReportFile = null;
774
- const server = createProxyServer(interceptor, { auditTools });
954
+ const server = createProxyServer(interceptor, { auditTools, patches: gate.pro });
775
955
  // The SDK closes the transport on its own when a single message exceeds
776
956
  // its buffer (about 10 MB). Without this the proxy stayed alive, deaf,
777
957
  // with its upstream child still running.
@@ -0,0 +1,57 @@
1
+ import crypto from "node:crypto";
2
+ import { LICENSE_PUBLIC_KEY } from "./publicKey.js";
3
+ const PKCS8_PREFIX = Buffer.from("302e020100300506032b657004220420", "hex");
4
+ const SPKI_PREFIX = Buffer.from("302a300506032b6570032100", "hex");
5
+ function rawKey(b64, what) {
6
+ const raw = Buffer.from(b64, "base64");
7
+ if (raw.length !== 32)
8
+ throw new Error(`${what}: erwartet 32 Byte (base64), erhalten ${raw.length}`);
9
+ return raw;
10
+ }
11
+ export function generateLicenseKeyPair() {
12
+ const { privateKey, publicKey } = crypto.generateKeyPairSync("ed25519");
13
+ return {
14
+ seed: privateKey.export({ format: "der", type: "pkcs8" }).subarray(-32).toString("base64"),
15
+ publicKey: publicKey.export({ format: "der", type: "spki" }).subarray(-32).toString("base64"),
16
+ };
17
+ }
18
+ export function signEd25519(domain, payload, seedB64) {
19
+ const key = crypto.createPrivateKey({
20
+ key: Buffer.concat([PKCS8_PREFIX, rawKey(seedB64, "LICENSE_SIGNING_SEED")]),
21
+ format: "der",
22
+ type: "pkcs8",
23
+ });
24
+ return crypto.sign(null, Buffer.from(`${domain}.${payload}`), key).toString("base64url");
25
+ }
26
+ function isCiEnvironment(env) {
27
+ const on = (v) => v !== undefined && v !== "" && !/^(0|false|no)$/i.test(v);
28
+ return on(env.CI) || on(env.GITHUB_ACTIONS);
29
+ }
30
+ let warnedKeyOverride = false;
31
+ export function defaultPublicKey(env = process.env) {
32
+ const override = env.AGENT_AUDIT_LICENSE_PUBLIC_KEY;
33
+ if (!override)
34
+ return LICENSE_PUBLIC_KEY;
35
+ if (isCiEnvironment(env) && env.AGENT_AUDIT_ALLOW_TEST_KEY !== "1") {
36
+ if (!warnedKeyOverride) {
37
+ warnedKeyOverride = true;
38
+ console.warn("⚠ Lizenz-Schlüssel-Override wird in CI ignoriert (eingebetteter Schlüssel gilt).");
39
+ }
40
+ return LICENSE_PUBLIC_KEY;
41
+ }
42
+ return override;
43
+ }
44
+ export function verifyEd25519(domain, payload, sigB64url, publicKeyB64 = defaultPublicKey()) {
45
+ try {
46
+ const key = crypto.createPublicKey({
47
+ key: Buffer.concat([SPKI_PREFIX, rawKey(publicKeyB64, "Public Key")]),
48
+ format: "der",
49
+ type: "spki",
50
+ });
51
+ const sig = Buffer.from(sigB64url, "base64url");
52
+ return crypto.verify(null, Buffer.from(`${domain}.${payload}`), key, sig);
53
+ }
54
+ catch {
55
+ return false;
56
+ }
57
+ }
@@ -0,0 +1,91 @@
1
+ // Lizenz-Sperre nach dem Muster von CPCG (Privacy Chaos Gate): Der Lauf selbst
2
+ // (Proxy, Szenarien F1 bis F5, Score, PASS/FAIL, Exit-Code) ist nie davon
3
+ // abhängig. Ohne gültige Lizenz wird nur die AUSGABE gekürzt oder ein Pro-Befehl
4
+ // mit einem klaren Hinweis nicht ausgeführt. Die Prüfung der Lizenz selbst
5
+ // (Ed25519, offline) liegt in verifier.ts.
6
+ import { resolveLicense } from "./verifier.js";
7
+ export const PURCHASE_URL = "https://cuonztech.ch/agent-harness.html#audit";
8
+ // Alte Adresse aus früheren Texten des Verifizierers; die Website leitet weiter.
9
+ const LEGACY_URL = "https://cuonztech.ch/agent-audit";
10
+ // Zuordnung frei/Pro. Dieselbe Tabelle steht in der README (Abschnitt «Lizenz und Umfang»).
11
+ export const PRO_FEATURES = {
12
+ dossier: {
13
+ name: "Das Prüf-Dossier (report --dossier)",
14
+ command: "report --dossier",
15
+ mode: "gekürzt",
16
+ free: "Das Dossier enthält nur Kopf, Score und das Urteil je Szenario",
17
+ },
18
+ ui: {
19
+ name: "Die geführte Web-Oberfläche (ui)",
20
+ command: "ui",
21
+ mode: "gesperrt",
22
+ free: "Der Weg über die Kommandozeile (proxy, doctor, report)",
23
+ },
24
+ campaign: {
25
+ name: "Der Kampagnen-Runner (campaign)",
26
+ command: "campaign",
27
+ mode: "gesperrt",
28
+ free: "Der Selbsttest (benchmark) und jeder einzelne Proxy-Lauf",
29
+ },
30
+ "schema-audit": {
31
+ name: "Das Schema-Gutachten (Befunde je Tool und Schema-Vorschläge)",
32
+ command: "proxy --report-file, report",
33
+ mode: "gekürzt",
34
+ free: "Die Zusammenfassung und die Risikostufe je Tool (auch in doctor)",
35
+ },
36
+ hardening: {
37
+ name: "Der Textbaustein- und Prompt-Patch-Generator (Härtungsvorschläge)",
38
+ command: "proxy --report-file, report, cuonztech__get_score",
39
+ mode: "gekürzt",
40
+ free: "Der Score, das Urteil und die Beobachtungen",
41
+ },
42
+ };
43
+ export const FREE_SCOPE_DE = "MCP-Chaos-Proxy mit den Szenarien F1 bis F5, doctor, Kompaktbericht, JSON-Report (--report-file), Resilience Score und PASS/FAIL samt Exit-Code";
44
+ /** Löst die Lizenz auf (--license, sonst Umgebungsvariablen). Ohne gültige Lizenz ist `pro` false. */
45
+ export function resolveProGate(licenseKey) {
46
+ const verdict = resolveLicense(licenseKey);
47
+ return { pro: verdict.valid === true, verdict };
48
+ }
49
+ /** Der Grund in einem Satz, ohne Kauf-Link (der steht immer am Ende des Hinweises). */
50
+ export function reasonSentence(verdict) {
51
+ if (verdict.valid)
52
+ return "Die Lizenz ist gültig.";
53
+ const raw = (verdict.reason ?? "").trim();
54
+ const none = raw === "" || raw === "Nicht lizenziert" || raw.startsWith("Kein Lizenzschlüssel");
55
+ if (none)
56
+ return "Es ist keine Lizenz hinterlegt.";
57
+ const core = raw
58
+ .replace(LEGACY_URL, "")
59
+ .replace(/\s*(Lizenzen erhältlich unter|Verlängerung unter)\s*$/i, "")
60
+ .trim()
61
+ .replace(/\.$/, "");
62
+ return `Ihre Lizenz ist nicht gültig: ${core}.`;
63
+ }
64
+ /**
65
+ * Hinweis für einen Befehl, der ohne Lizenz nicht läuft (`ui`, `campaign`).
66
+ * Form: «Keine gültige Lizenz: <Funktion> gehört zum Pro-Umfang. Frei bleiben …. Lizenz: <URL>»
67
+ */
68
+ export function lockedNotice(feature, verdict) {
69
+ const f = PRO_FEATURES[feature];
70
+ return (`Keine gültige Lizenz: ${f.name} gehört zum Pro-Umfang. ${reasonSentence(verdict)}\n` +
71
+ `Frei bleiben: ${FREE_SCOPE_DE}.\n` +
72
+ `Der Befehl wurde nicht ausgeführt. Lizenz: ${PURCHASE_URL}\n` +
73
+ `Aktivierung: --license <KEY> oder Umgebungsvariable AGENT_AUDIT_LICENSE.\n`);
74
+ }
75
+ /**
76
+ * Hinweis für eine gekürzte Ausgabe (Dossier, Schema-Gutachten, Härtungsvorschläge).
77
+ * Der Lauf selbst, das Urteil und der Exit-Code sind davon unberührt.
78
+ */
79
+ export function shortenedNotice(feature, verdict) {
80
+ const f = PRO_FEATURES[feature];
81
+ return (`Keine gültige Lizenz: ${f.name} gehört zum Pro-Umfang. ${reasonSentence(verdict)} ` +
82
+ `Die Ausgabe ist gekürzt; der Lauf, das Urteil und der Exit-Code sind echt. ` +
83
+ `Frei bleiben: ${FREE_SCOPE_DE}. Lizenz: ${PURCHASE_URL}`);
84
+ }
85
+ /** Schlankere Fassung des Schema-Gutachtens ohne Lizenz: Zusammenfassung und Risikostufe, keine Befunde, kein Schema-Vorschlag. */
86
+ export function reduceSchemaAudit(audit) {
87
+ return {
88
+ ...audit,
89
+ tools: audit.tools.map((t) => ({ ...t, findingsDe: [], recommendedSchemaPatch: null })),
90
+ };
91
+ }
@@ -0,0 +1,5 @@
1
+ export * from "./publicKey.js";
2
+ export * from "./ed25519.js";
3
+ export * from "./types.js";
4
+ export * from "./verifier.js";
5
+ export * from "./gate.js";
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Öffentlicher Ed25519-Schlüssel des Cuonztech-Lizenzdienstes (32 Byte, base64).
3
+ * Der zugehörige private Seed liegt ausschliesslich serverseitig (config.php) und im KeePass.
4
+ */
5
+ export const LICENSE_PUBLIC_KEY = "OddVMIYDwPSNJSwZiglu4dNLswB+PhWwZi1WZ/XXklE=";
@@ -0,0 +1 @@
1
+ export {};