cuonztech-agent-harness 0.7.0 → 0.8.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.
@@ -53,6 +53,7 @@ export function findToolNameCollisions(upstreamTools) {
53
53
  }
54
54
  export function createProxyServer(interceptor, options = {}) {
55
55
  const auditTools = options.auditTools ?? true;
56
+ const withPatches = options.patches ?? true;
56
57
  const instructions = interceptor.getUpstreamInstructions();
57
58
  const server = new Server({ name: "cuonztech-agent-harness-proxy", version: VERSION }, { capabilities: { tools: {} }, ...(instructions ? { instructions } : {}) });
58
59
  server.setRequestHandler(ListToolsRequestSchema, async () => {
@@ -89,7 +90,7 @@ export function createProxyServer(interceptor, options = {}) {
89
90
  if (name === `${AUDIT_TOOL_PREFIX}get_report`) {
90
91
  return handleGetReport(withSession, { persist: false });
91
92
  }
92
- return handleGetScore(withSession);
93
+ return handleGetScore(withSession, { patches: withPatches });
93
94
  }
94
95
  return interceptor.interceptToolCall(name, toolArgs, extra.signal);
95
96
  });
@@ -2,7 +2,8 @@
2
2
  // replacing whatever is there. A mistyped path must not turn into the loss of
3
3
  // an unrelated file (the upstream's own script, a config, a spreadsheet): an
4
4
  // existing file is only replaced if it is empty or an earlier harness report.
5
- import { readFileSync, statSync } from "node:fs";
5
+ import { existsSync, readFileSync, renameSync, statSync } from "node:fs";
6
+ import { basename, dirname, extname, join } from "node:path";
6
7
  const MAX_REPORT_BYTES = 8 * 1024 * 1024;
7
8
  export function isReplaceableReportFile(path) {
8
9
  try {
@@ -47,3 +48,76 @@ export function isReservedFileName(path) {
47
48
  .split(/[\\/]+/)
48
49
  .some((segment) => /^(nul|con|prn|aux|com[1-9]|lpt[1-9])(\..*)?$/i.test(segment));
49
50
  }
51
+ export function inspectReportForArchive(parsed) {
52
+ if (parsed === null || typeof parsed !== "object") {
53
+ return { shouldArchive: false, callsCount: 0 };
54
+ }
55
+ const obj = parsed;
56
+ if (typeof obj.harnessVersion !== "string") {
57
+ return { shouldArchive: false, callsCount: 0 };
58
+ }
59
+ // Startup placeholders or errors from earlier runs never get archived,
60
+ // preventing runaway file proliferation on repeated reconnects.
61
+ if (obj.status === "starting" || typeof obj.startupError === "string") {
62
+ return { shouldArchive: false, callsCount: 0 };
63
+ }
64
+ const reportObj = obj.report;
65
+ const calls = Array.isArray(reportObj?.calls) ? reportObj.calls : [];
66
+ const forensics = obj.call_forensics;
67
+ const timeline = Array.isArray(forensics?.timeline) ? forensics.timeline : [];
68
+ const callsCount = Math.max(calls.length, timeline.length);
69
+ const verdict = typeof obj.verdict === "string" ? obj.verdict : undefined;
70
+ // We archive if there is at least one recorded call or an evaluated verdict (PASS / FAIL)
71
+ const shouldArchive = callsCount > 0 || verdict === "PASS" || verdict === "FAIL";
72
+ return {
73
+ shouldArchive,
74
+ verdict,
75
+ callsCount,
76
+ };
77
+ }
78
+ export function computeArchiveReportPath(filePath, now = new Date()) {
79
+ const dir = dirname(filePath);
80
+ const ext = extname(filePath);
81
+ const name = basename(filePath, ext);
82
+ // ISO timestamp with colon replaced by '-' for Windows filesystem compatibility:
83
+ // e.g. 2026-10-30T17-25-00.123Z
84
+ const safeTimestamp = now.toISOString().replace(/:/g, "-");
85
+ let candidate = join(dir, `${name}.${safeTimestamp}${ext || ".json"}`);
86
+ let counter = 1;
87
+ while (existsSync(candidate)) {
88
+ candidate = join(dir, `${name}.${safeTimestamp}.${counter}${ext || ".json"}`);
89
+ counter++;
90
+ }
91
+ return candidate;
92
+ }
93
+ export function archivePreviousReportSync(filePath, now = new Date()) {
94
+ const info = statSync(filePath, { throwIfNoEntry: false });
95
+ if (!info || !info.isFile())
96
+ return null;
97
+ if (info.size === 0 || info.size > MAX_REPORT_BYTES)
98
+ return null;
99
+ let parsed;
100
+ try {
101
+ parsed = JSON.parse(readFileSync(filePath, "utf-8"));
102
+ }
103
+ catch {
104
+ return null;
105
+ }
106
+ const check = inspectReportForArchive(parsed);
107
+ if (!check.shouldArchive)
108
+ return null;
109
+ const archivePath = computeArchiveReportPath(filePath, now);
110
+ try {
111
+ renameSync(filePath, archivePath);
112
+ }
113
+ catch (err) {
114
+ throw new Error(`could not archive previous report file "${filePath}" to "${archivePath}": ` +
115
+ `${err instanceof Error ? err.message : String(err)}`);
116
+ }
117
+ return {
118
+ archivedPath: archivePath,
119
+ verdict: check.verdict,
120
+ callsCount: check.callsCount,
121
+ archivedAt: now.toISOString(),
122
+ };
123
+ }
@@ -296,7 +296,10 @@ export async function handleGetReport(args, options = {}) {
296
296
  // Resilience Score + Hardening Patches for THIS session's own audit report —
297
297
  // unlike `benchmark` mode (which scores a hardcoded scripted call sequence
298
298
  // against itself), this scores whatever the connected agent actually did.
299
- export function handleGetScore(args) {
299
+ // `patches: false` (Proxy ohne gültige Lizenz): Score und Urteil unverändert, die Härtungsvorschläge
300
+ // (Textbausteine) gehören zum Pro-Umfang und werden durch einen Hinweis ersetzt.
301
+ export function handleGetScore(args, options = {}) {
302
+ const withPatches = options.patches ?? true;
300
303
  const session = getSession(args.session_id);
301
304
  if (!session) {
302
305
  return {
@@ -311,11 +314,13 @@ export function handleGetScore(args) {
311
314
  }
312
315
  const report = generateReport(session);
313
316
  const score = computeScore([report]);
314
- const patches = generatePatches([report], score);
317
+ const patches = withPatches ? generatePatches([report], score) : [];
315
318
  const format = args.format ?? "markdown";
319
+ const lockedNote = "Keine gültige Lizenz: Die Härtungsvorschläge (Textbausteine) gehören zum Pro-Umfang. " +
320
+ "Score und Urteil sind vollständig. Lizenz: https://cuonztech.ch/agent-harness.html#audit";
316
321
  const output = format === "json"
317
- ? JSON.stringify({ score, patches }, null, 2)
318
- : [formatScoreReport(score), patches.length > 0 ? formatPatchReport(patches) : null]
322
+ ? JSON.stringify({ score, patches, ...(withPatches ? {} : { limitedByLicense: true, licenseNotice: lockedNote }) }, null, 2)
323
+ : [formatScoreReport(score), withPatches ? (patches.length > 0 ? formatPatchReport(patches) : null) : lockedNote]
319
324
  .filter(Boolean)
320
325
  .join("\n\n");
321
326
  return {
package/dist/ui/server.js CHANGED
@@ -18,6 +18,7 @@ import { runCampaign, renderCampaignDemoBody, followupSummary } from "../campaig
18
18
  import { runBenchmark, defaultConfig } from "../benchmark/runner.js";
19
19
  import { VERSION } from "../version.js";
20
20
  import { PAGE_HTML } from "./page.js";
21
+ import { resolveLicense, verifyLicense, resolveProGate, lockedNotice } from "../license/index.js";
21
22
  const MAX_BODY = 64 * 1024;
22
23
  const MAX_NAME = 200;
23
24
  const MAX_AGENT_RESPONSE = 10_000;
@@ -139,10 +140,15 @@ export async function startUi(options = {}) {
139
140
  }
140
141
  return file;
141
142
  };
143
+ let currentLicenseVerdict = resolveLicense(options.licenseKey);
144
+ // Nur ein ausdrücklich übergebener, gültiger Schlüssel wird in die erzeugte Agenten-Konfiguration übernommen
145
+ // (damit der Proxy des Agenten die Pro-Teile in den Report schreibt); aus der Umgebung stammende nie.
146
+ let explicitLicenseKey = options.licenseKey;
142
147
  const dossierOptions = () => ({
143
148
  customerName: session?.customerName,
144
149
  systemName: session?.systemName,
145
150
  agentResponse: session?.agentResponse,
151
+ licenseVerdict: currentLicenseVerdict,
146
152
  });
147
153
  // Gleiche Vorsicht wie der Proxy: erst in eine eigene Datei, dann darüber umbenennen.
148
154
  const writeReportFile = async (path, file) => {
@@ -178,7 +184,29 @@ export async function startUi(options = {}) {
178
184
  return send(res, 200, PAGE_HTML.replace("__TOKEN__", token), "text/html; charset=utf-8");
179
185
  }
180
186
  if (req.method === "GET" && url.pathname === "/api/info") {
181
- return sendJson(res, 200, { version: VERSION, bundledTestsystem: bundledTestsystem(), discoveredServers: discoverMcpServers() });
187
+ return sendJson(res, 200, {
188
+ version: VERSION,
189
+ bundledTestsystem: bundledTestsystem(),
190
+ discoveredServers: discoverMcpServers(),
191
+ license: {
192
+ valid: currentLicenseVerdict.valid,
193
+ tier: currentLicenseVerdict.tier ?? "evaluation",
194
+ claims: currentLicenseVerdict.claims,
195
+ reason: currentLicenseVerdict.reason,
196
+ },
197
+ });
198
+ }
199
+ if (req.method === "POST" && url.pathname === "/api/license") {
200
+ const body = await readJson(req);
201
+ const key = typeof body.key === "string" ? body.key.trim() : "";
202
+ currentLicenseVerdict = verifyLicense(key);
203
+ explicitLicenseKey = key;
204
+ return sendJson(res, 200, {
205
+ valid: currentLicenseVerdict.valid,
206
+ tier: currentLicenseVerdict.tier ?? "evaluation",
207
+ claims: currentLicenseVerdict.claims,
208
+ reason: currentLicenseVerdict.reason,
209
+ });
182
210
  }
183
211
  if (req.method === "POST" && url.pathname === "/api/doctor") {
184
212
  const body = await readJson(req);
@@ -225,6 +253,7 @@ export async function startUi(options = {}) {
225
253
  "--allow-writes",
226
254
  "--audit-tools", "off",
227
255
  "--report-file", reportFile,
256
+ ...(currentLicenseVerdict.valid && explicitLicenseKey ? ["--license", explicitLicenseKey] : []),
228
257
  ];
229
258
  const claudeCode = ["claude mcp add --scope user agent-harness --", process.execPath, ...proxyArgs].map((p, i) => (i === 0 ? p : quoteArg(p))).join(" ");
230
259
  const claudeDesktop = JSON.stringify({ mcpServers: { "agent-harness": { command: process.execPath, args: proxyArgs } } }, null, 2);
@@ -324,18 +353,24 @@ function openBrowser(url) {
324
353
  }
325
354
  export async function runUiMode(argv) {
326
355
  if (argv.includes("--help") || argv.includes("-h")) {
327
- process.stdout.write("Nutzung: ui [--port <n>] [--no-open]\n\n" +
356
+ process.stdout.write("Nutzung: ui [--port <n>] [--no-open] [--license <key>]\n\n" +
328
357
  "Startet die geführte Oberfläche im Browser (nur auf diesem Rechner, mit Zufalls-Schlüssel im Link).\n" +
329
- " --port <n> feste Port-Nummer (Standard: ein freier Port)\n" +
330
- " --no-open den Browser nicht automatisch öffnen\n");
358
+ "Die Oberfläche gehört zum Pro-Umfang und braucht einen gültigen Lizenzschlüssel (oder AGENT_AUDIT_LICENSE).\n" +
359
+ " --port <n> feste Port-Nummer (Standard: ein freier Port)\n" +
360
+ " --no-open den Browser nicht automatisch öffnen\n" +
361
+ " --license <key> CuonzTech Agent Audit Pro Lizenzschlüssel (oder Umgebungsvariable AGENT_AUDIT_LICENSE)\n");
331
362
  return 0;
332
363
  }
333
364
  let port;
334
365
  let open = true;
366
+ let licenseKey;
335
367
  for (let i = 0; i < argv.length; i++) {
336
368
  const token = argv[i];
337
369
  if (token === "--no-open")
338
370
  open = false;
371
+ else if (token === "--license") {
372
+ licenseKey = argv[++i] ?? "";
373
+ }
339
374
  else if (token === "--port") {
340
375
  if (port !== undefined) {
341
376
  process.stderr.write("Error: option --port was given more than once.\n");
@@ -353,7 +388,14 @@ export async function runUiMode(argv) {
353
388
  return 1;
354
389
  }
355
390
  }
356
- const handle = await startUi({ port, open });
391
+ // Pro-Umfang: ohne gültige Lizenz startet die Oberfläche nicht (klarer Hinweis, Exit 1 wie jeder nicht ausgeführte Befehl).
392
+ // Das Frei-Paket bleibt über die Kommandozeile vollständig nutzbar (proxy, doctor, report).
393
+ const gate = resolveProGate(licenseKey);
394
+ if (!gate.pro) {
395
+ process.stderr.write(lockedNotice("ui", gate.verdict));
396
+ return 1;
397
+ }
398
+ const handle = await startUi({ port, open, licenseKey });
357
399
  process.stdout.write(`Oberfläche: ${handle.url}\n`);
358
400
  process.stdout.write("Läuft nur auf diesem Rechner. Beenden mit Strg+C.\n");
359
401
  await new Promise((ok) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cuonztech-agent-harness",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "mcpName": "io.github.cuonztech/agent-harness",
5
5
  "description": "Chaos-Proxy für MCP-Server: injiziert F1-F5-Fehlerszenarien (Ghost-Write nach Timeout, 429, defektes JSON, Duplicate Dispatch, 500) vor einem echten Upstream-MCP-Server und prüft, ob ein Agent Schreibvorgänge doppelt ausführt. Mit Resilience Score und System-Prompt-Patches für beobachtetes Fehlverhalten.",
6
6
  "type": "module",
@@ -31,7 +31,7 @@
31
31
  "cuonztech"
32
32
  ],
33
33
  "author": "CuonzTech",
34
- "license": "MIT",
34
+ "license": "SEE LICENSE IN LICENSE",
35
35
  "repository": {
36
36
  "type": "git",
37
37
  "url": "git+https://github.com/cuonztech/agent-harness.git"
@@ -49,6 +49,7 @@
49
49
  "examples/testsystem/server.mjs",
50
50
  "README.md",
51
51
  "LICENSE",
52
+ "LICENSE-MIT-bis-0.7.0.txt",
52
53
  "server.json"
53
54
  ],
54
55
  "dependencies": {
package/server.json CHANGED
@@ -6,12 +6,12 @@
6
6
  "url": "https://github.com/cuonztech/agent-harness",
7
7
  "source": "github"
8
8
  },
9
- "version": "0.7.0",
9
+ "version": "0.8.0",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "cuonztech-agent-harness",
14
- "version": "0.7.0",
14
+ "version": "0.8.0",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  }