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/dist/ui/server.js CHANGED
@@ -19,6 +19,9 @@ import { runBenchmark, defaultConfig } from "../benchmark/runner.js";
19
19
  import { VERSION } from "../version.js";
20
20
  import { PAGE_HTML } from "./page.js";
21
21
  import { resolveLicense, verifyLicense, resolveProGate, lockedNotice } from "../license/index.js";
22
+ import { expandEqualsForm, optionName, redactSecrets } from "../cli-args.js";
23
+ // Platzhalter statt Lizenzcode in erzeugten Befehlen und Konfigurationen.
24
+ const LICENSE_PLACEHOLDER = "<DEIN-LIZENZCODE>";
22
25
  const MAX_BODY = 64 * 1024;
23
26
  const MAX_NAME = 200;
24
27
  const MAX_AGENT_RESPONSE = 10_000;
@@ -141,9 +144,6 @@ export async function startUi(options = {}) {
141
144
  return file;
142
145
  };
143
146
  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;
147
147
  const dossierOptions = () => ({
148
148
  customerName: session?.customerName,
149
149
  systemName: session?.systemName,
@@ -200,7 +200,6 @@ export async function startUi(options = {}) {
200
200
  const body = await readJson(req);
201
201
  const key = typeof body.key === "string" ? body.key.trim() : "";
202
202
  currentLicenseVerdict = verifyLicense(key);
203
- explicitLicenseKey = key;
204
203
  return sendJson(res, 200, {
205
204
  valid: currentLicenseVerdict.valid,
206
205
  tier: currentLicenseVerdict.tier ?? "evaluation",
@@ -253,11 +252,20 @@ export async function startUi(options = {}) {
253
252
  "--allow-writes",
254
253
  "--audit-tools", "off",
255
254
  "--report-file", reportFile,
256
- ...(currentLicenseVerdict.valid && explicitLicenseKey ? ["--license", explicitLicenseKey] : []),
257
255
  ];
258
- const claudeCode = ["claude mcp add --scope user agent-harness --", process.execPath, ...proxyArgs].map((p, i) => (i === 0 ? p : quoteArg(p))).join(" ");
259
- const claudeDesktop = JSON.stringify({ mcpServers: { "agent-harness": { command: process.execPath, args: proxyArgs } } }, null, 2);
260
- return sendJson(res, 200, { claudeCode, claudeDesktop, reportFile, scenario });
256
+ // Der Lizenzcode steht nie in dem, was die Oberfläche ausgibt (Befehl, Konfigurationsdatei, Prozessliste): Mit
257
+ // gültiger Lizenz kommt ein Platzhalter für die Umgebungsvariable des Proxys hinein, den der Nutzer ersetzt.
258
+ const withLicense = currentLicenseVerdict.valid;
259
+ const claudeCode = [
260
+ withLicense ? `claude mcp add --scope user -e AGENT_AUDIT_LICENSE=${LICENSE_PLACEHOLDER} agent-harness --` : "claude mcp add --scope user agent-harness --",
261
+ process.execPath,
262
+ ...proxyArgs,
263
+ ].map((p, i) => (i === 0 ? p : quoteArg(p))).join(" ");
264
+ const claudeDesktop = JSON.stringify({ mcpServers: { "agent-harness": { command: process.execPath, args: proxyArgs, ...(withLicense ? { env: { AGENT_AUDIT_LICENSE: LICENSE_PLACEHOLDER } } : {}) } } }, null, 2);
265
+ const licenseHint = withLicense
266
+ ? `Ersetze ${LICENSE_PLACEHOLDER} durch deinen Lizenzcode (aus der E-Mail), damit der Proxy deines Agenten Textbausteine und Schema-Befunde in den Bericht schreibt. Der Code steht aus Sicherheitsgründen nicht in diesem Befehl. Ohne ihn läuft der Test vollständig, der Bericht enthält die Pro-Teile aber nicht, und das Dossier sagt das ausdrücklich.`
267
+ : "";
268
+ return sendJson(res, 200, { claudeCode, claudeDesktop, reportFile, scenario, licenseHint });
261
269
  }
262
270
  if (req.method === "POST" && url.pathname === "/api/dossier-meta") {
263
271
  const body = await readJson(req);
@@ -361,6 +369,8 @@ export async function runUiMode(argv) {
361
369
  " --license <key> CuonzTech Agent Audit Pro Lizenzschlüssel (oder Umgebungsvariable AGENT_AUDIT_LICENSE)\n");
362
370
  return 0;
363
371
  }
372
+ // --flag=wert gilt wie --flag wert, auch für --license.
373
+ argv = expandEqualsForm(argv, new Set(["--license", "--port"]));
364
374
  let port;
365
375
  let open = true;
366
376
  let licenseKey;
@@ -370,6 +380,10 @@ export async function runUiMode(argv) {
370
380
  open = false;
371
381
  else if (token === "--license") {
372
382
  licenseKey = argv[++i] ?? "";
383
+ if (licenseKey.trim() === "" || licenseKey.startsWith("--")) {
384
+ process.stderr.write("Error: --license needs a key.\nUsage: ui [--port <n>] [--no-open] [--license <key>]\n");
385
+ return 1;
386
+ }
373
387
  }
374
388
  else if (token === "--port") {
375
389
  if (port !== undefined) {
@@ -378,13 +392,13 @@ export async function runUiMode(argv) {
378
392
  }
379
393
  const raw = argv[++i] ?? "";
380
394
  if (!/^\d+$/.test(raw) || Number(raw) > 65535) {
381
- process.stderr.write(`Error: --port must be a number between 0 and 65535, got "${raw}".\n`);
395
+ process.stderr.write(`Error: --port must be a number between 0 and 65535, got "${redactSecrets(raw)}".\n`);
382
396
  return 1;
383
397
  }
384
398
  port = Number(raw);
385
399
  }
386
400
  else {
387
- process.stderr.write(`Error: unknown option ${token}\nUsage: ui [--port <n>] [--no-open]\n`);
401
+ process.stderr.write(`Error: unknown option ${optionName(token)}\nUsage: ui [--port <n>] [--no-open] [--license <key>]\n`);
388
402
  return 1;
389
403
  }
390
404
  }
package/dist/verify.js ADDED
@@ -0,0 +1,234 @@
1
+ // `verify <dossier|report>`: rechnet die Prüfsumme einer Report-Datei oder eines Dossiers neu und meldet
2
+ // OK oder MISMATCH mit dem Grund. Exit-Code 0 nur bei OK, sonst 1. Es bleibt eine Prüfsumme ohne Signatur
3
+ // eines Dritten: Wer Datei und Prüfsumme zusammen ändert, kann fälschen; der Wert schützt nur, wenn er
4
+ // getrennt von der Datei aufbewahrt wird (dafür gibt es `--expect <sha256>`).
5
+ import { readFile, stat } from "node:fs/promises";
6
+ import { DOSSIER_SEAL_MARKER, canonicalJson, checkReportIntegrity, extractDossierSeal, sha256Hex } from "./core/artifact-integrity.js";
7
+ import { dossierHeaderChecks } from "./dossier-report.js";
8
+ const MAX_BYTES = 64 * 1024 * 1024;
9
+ function countOf(text, part) {
10
+ if (part === "")
11
+ return 0;
12
+ let n = 0;
13
+ for (let i = text.indexOf(part); i !== -1; i = text.indexOf(part, i + part.length))
14
+ n++;
15
+ return n;
16
+ }
17
+ const USAGE = "Usage: verify <dossier|report-file> [--report <report-file>] [--expect <sha256>]\n";
18
+ const HELP = `${USAGE}\n` +
19
+ "Rechnet die Prüfsumme einer Report-Datei (--report-file des Proxys) oder eines Dossiers (report --dossier) neu\n" +
20
+ "und gibt OK oder MISMATCH mit dem Grund aus. Exit-Code 0 bei OK, sonst 1.\n" +
21
+ " --report <file> bei einem Dossier zusätzlich die Report-Datei prüfen, aus der es erzeugt wurde, und beide abgleichen\n" +
22
+ " --expect <sha256> die getrennt aufbewahrte Prüfsumme (Report-Datei: payloadSha256; Dossier: Prüfsumme des Dossiers);\n" +
23
+ " stimmt sie nicht mit der Datei überein, ist das ein MISMATCH\n\n" +
24
+ "Es ist eine Prüfsumme ohne Signatur eines Dritten und kein Zertifikat. Wer Datei und Prüfsumme zusammen ändert, kann\n" +
25
+ "fälschen. Sie schützt nur, wenn der Wert getrennt von der Datei aufbewahrt wird.\n";
26
+ function statusOfCheck(check) {
27
+ if (check.status === "ok")
28
+ return "OK";
29
+ return check.status === "mismatch" ? "MISMATCH" : "NOT_VERIFIABLE";
30
+ }
31
+ export function verifyReportFileContent(file, expect) {
32
+ const check = checkReportIntegrity(file);
33
+ const lines = [check.reasonDe];
34
+ let status = statusOfCheck(check);
35
+ const sha = file.integrity?.payloadSha256 ?? null;
36
+ if (status === "OK" && expect !== undefined && expect.toLowerCase() !== sha) {
37
+ status = "MISMATCH";
38
+ lines.unshift(`Die Datei stimmt zwar mit ihrer eigenen Prüfsumme überein, die Prüfsumme ist aber nicht die erwartete: erwartet ${expect.toLowerCase()}, in der Datei ${sha ?? "—"}. Datei und Prüfsumme wurden gemeinsam ausgetauscht oder die falsche Datei liegt vor.`);
39
+ }
40
+ return { status, kind: "report", sha256: sha, lines };
41
+ }
42
+ export function verifyDossierContent(text, options = {}) {
43
+ const extracted = extractDossierSeal(text);
44
+ if ("error" in extracted) {
45
+ return {
46
+ status: "NOT_VERIFIABLE",
47
+ kind: "dossier",
48
+ sha256: null,
49
+ lines: [
50
+ extracted.error === "missing"
51
+ ? "Kein Prüfsummen-Siegel gefunden: Das Dossier stammt aus Harness < 0.11.1 (Siegel erst ab 0.11.1) oder das Siegel wurde entfernt. Nicht prüfbar."
52
+ : "Das Prüfsummen-Siegel am Ende des Dossiers ist beschädigt und lässt sich nicht lesen. Nicht prüfbar.",
53
+ ],
54
+ };
55
+ }
56
+ const { seal } = extracted;
57
+ const recomputed = sha256Hex(canonicalJson(seal.core));
58
+ const lines = [];
59
+ if (recomputed !== seal.sha256) {
60
+ return {
61
+ status: "MISMATCH",
62
+ kind: "dossier",
63
+ sha256: seal.sha256,
64
+ lines: ["Die Prüfsumme des Dossiers stimmt nicht zu seinem eingebetteten Kern: Kern (Urteil, Score, Namen, Lizenzstatus ...) oder Prüfsumme wurden nach dem Erzeugen verändert."],
65
+ };
66
+ }
67
+ // Der sichtbare Kopf muss dem gesiegelten Kern entsprechen.
68
+ const header = seal.core.header;
69
+ if (!header || typeof header !== "object" || !Array.isArray(header.rows) || !Array.isArray(header.notes)) {
70
+ return { status: "NOT_VERIFIABLE", kind: "dossier", sha256: seal.sha256, lines: ["Das Siegel enthält den sichtbaren Kopf nicht (Dossier aus einer Vorabfassung von 0.11.1). Nicht prüfbar."] };
71
+ }
72
+ const isHtml = /<html[\s>]/i.test(text.slice(0, 2000));
73
+ const expected = dossierHeaderChecks(seal.core)[isHtml ? "html" : "md"];
74
+ // Gezählt wird im sichtbaren Teil vor dem Siegel (das JSON des Siegels wiederholt Teile des Kopfes).
75
+ const sealAt = text.lastIndexOf(`<!-- ${DOSSIER_SEAL_MARKER} `);
76
+ const visible = sealAt === -1 ? text : text.slice(0, sealAt);
77
+ const missing = expected.filter((check) => countOf(visible, check.needle) !== 1 || (check.prefix !== undefined && countOf(visible, check.prefix) !== 1));
78
+ if (missing.length > 0) {
79
+ return {
80
+ status: "MISMATCH",
81
+ kind: "dossier",
82
+ sha256: seal.sha256,
83
+ lines: [
84
+ `Der sichtbare Kopf des Dossiers weicht vom gesiegelten Kern ab (${missing.length} von ${expected.length} Angaben fehlen, sind verändert oder doppelt: Kopfzeilen, Datum, Version, Urteil, Score oder die Hinweise unter dem Urteil wurden im Text verändert).`,
85
+ ],
86
+ };
87
+ }
88
+ if (options.expect !== undefined && options.expect.toLowerCase() !== seal.sha256) {
89
+ return {
90
+ status: "MISMATCH",
91
+ kind: "dossier",
92
+ sha256: seal.sha256,
93
+ lines: [`Das Dossier ist in sich stimmig, seine Prüfsumme ist aber nicht die erwartete: erwartet ${options.expect.toLowerCase()}, im Dossier ${seal.sha256}.`],
94
+ };
95
+ }
96
+ const source = seal.core.reportFile;
97
+ if (source.check !== "ok") {
98
+ lines.push(source.check === "mismatch"
99
+ ? "Das Dossier ist unverändert, wurde aber aus einer Report-Datei erzeugt, die ihre eigene Prüfsumme nicht bestand: Urteil und Score sind nicht belastbar."
100
+ : "Das Dossier ist unverändert, wurde aber aus einer Report-Datei erzeugt, deren Prüfsumme nicht prüfbar war (fehlend oder aus Harness < 0.11.1): Das Urteil war schon beim Erzeugen ungeprüft.");
101
+ return { status: source.check === "mismatch" ? "MISMATCH" : "NOT_VERIFIABLE", kind: "dossier", sha256: seal.sha256, lines };
102
+ }
103
+ lines.push("Siegel, Kern und sichtbarer Kopf (Titelzeile, alle Kopfzeilen, Urteil, Score und die Hinweise unter dem Urteil) stimmen überein. Der Fliesstext der Abschnitte 1 bis 6 ist nicht gesiegelt und wurde nicht geprüft.");
104
+ if (options.report) {
105
+ const reportCheck = checkReportIntegrity(options.report);
106
+ if (reportCheck.status !== "ok") {
107
+ return { status: reportCheck.status === "mismatch" ? "MISMATCH" : "NOT_VERIFIABLE", kind: "dossier", sha256: seal.sha256, lines: [`Die angegebene Report-Datei besteht ihre Prüfung nicht: ${reportCheck.reasonDe}`] };
108
+ }
109
+ if (options.report.integrity?.payloadSha256 !== source.payloadSha256) {
110
+ return {
111
+ status: "MISMATCH",
112
+ kind: "dossier",
113
+ sha256: seal.sha256,
114
+ lines: ["Die angegebene Report-Datei ist nicht die, aus der das Dossier erzeugt wurde (ihre Prüfsumme weicht von der im Dossier festgehaltenen ab)."],
115
+ };
116
+ }
117
+ lines.push("Die angegebene Report-Datei gehört zu diesem Dossier und ist unverändert. Der Fliesstext des Dossiers wurde dabei nicht gegen die Report-Datei abgeglichen.");
118
+ }
119
+ else {
120
+ lines.push("Die Report-Datei wurde nicht mitgeprüft; mit --report <datei> abgleichen.");
121
+ }
122
+ lines.push("Das ist eine Prüfsumme ohne Signatur eines Dritten: Sie schützt nur, wenn ihr Wert getrennt aufbewahrt wird (--expect <sha256>).");
123
+ return { status: "OK", kind: "dossier", sha256: seal.sha256, lines };
124
+ }
125
+ async function readText(path) {
126
+ const info = await stat(path).catch(() => null);
127
+ if (!info || !info.isFile())
128
+ return { error: `Datei nicht gefunden: ${path}` };
129
+ if (info.size > MAX_BYTES)
130
+ return { error: `Datei zu gross (${info.size} Byte, höchstens ${MAX_BYTES}): ${path}` };
131
+ return { text: await readFile(path, "utf8") };
132
+ }
133
+ function looksLikeReportFile(value) {
134
+ return value !== null && typeof value === "object" && !Array.isArray(value) && ("harnessVersion" in value || "report" in value || "integrity" in value);
135
+ }
136
+ export async function runVerifyMode(argv) {
137
+ if (argv.includes("--help") || argv.includes("-h")) {
138
+ process.stdout.write(HELP);
139
+ return 0;
140
+ }
141
+ let file;
142
+ let reportPath;
143
+ let expect;
144
+ for (let i = 0; i < argv.length; i++) {
145
+ const token = argv[i];
146
+ const eq = token.indexOf("=");
147
+ const name = token.startsWith("--") && eq !== -1 ? token.slice(0, eq) : token;
148
+ if (name === "--report" || name === "--expect") {
149
+ const v = token.startsWith("--") && eq !== -1 ? token.slice(eq + 1) : argv[++i];
150
+ if (v === undefined || v.trim() === "" || v.startsWith("--")) {
151
+ process.stderr.write(`Error: ${name} needs a value.\n${USAGE}`);
152
+ return 1;
153
+ }
154
+ if (name === "--report")
155
+ reportPath = v;
156
+ else {
157
+ if (!/^[0-9a-fA-F]{64}$/.test(v.trim())) {
158
+ process.stderr.write(`Error: --expect needs a SHA-256 (64 hex characters).\n${USAGE}`);
159
+ return 1;
160
+ }
161
+ expect = v.trim();
162
+ }
163
+ }
164
+ else if (token.startsWith("--")) {
165
+ process.stderr.write(`Error: unknown option ${name}\n${USAGE}`);
166
+ return 1;
167
+ }
168
+ else if (file === undefined) {
169
+ file = token;
170
+ }
171
+ else {
172
+ process.stderr.write(`Error: unexpected argument ${token}\n${USAGE}`);
173
+ return 1;
174
+ }
175
+ }
176
+ if (file === undefined) {
177
+ process.stderr.write(`Error: verify needs a dossier or report file.\n${USAGE}`);
178
+ return 1;
179
+ }
180
+ const read = await readText(file);
181
+ if ("error" in read) {
182
+ process.stderr.write(`Error: ${read.error}\n`);
183
+ return 1;
184
+ }
185
+ let reportFile;
186
+ if (reportPath !== undefined) {
187
+ const r = await readText(reportPath);
188
+ if ("error" in r) {
189
+ process.stderr.write(`Error: ${r.error}\n`);
190
+ return 1;
191
+ }
192
+ try {
193
+ const parsed = JSON.parse(r.text);
194
+ if (!looksLikeReportFile(parsed))
195
+ throw new Error("not a report");
196
+ reportFile = parsed;
197
+ }
198
+ catch {
199
+ process.stderr.write(`Error: ${reportPath} is not a report file written by the proxy (--report-file).\n`);
200
+ return 1;
201
+ }
202
+ }
203
+ let result;
204
+ let parsed;
205
+ let isJson = true;
206
+ try {
207
+ parsed = JSON.parse(read.text);
208
+ }
209
+ catch {
210
+ isJson = false;
211
+ }
212
+ if (isJson) {
213
+ if (!looksLikeReportFile(parsed)) {
214
+ process.stderr.write(`Error: ${file} is JSON, but not a report written by the proxy (--report-file).\n`);
215
+ return 1;
216
+ }
217
+ if (reportFile !== undefined) {
218
+ process.stderr.write("Error: --report only applies when verifying a dossier.\n");
219
+ return 1;
220
+ }
221
+ result = verifyReportFileContent(parsed, expect);
222
+ }
223
+ else {
224
+ result = verifyDossierContent(read.text, { report: reportFile, expect });
225
+ }
226
+ const label = result.status === "NOT_VERIFIABLE" ? "NICHT PRÜFBAR" : result.status;
227
+ const what = result.kind === "report" ? "Report-Datei" : "Dossier";
228
+ process.stdout.write(`${label} ${what} ${file}\n`);
229
+ for (const line of result.lines)
230
+ process.stdout.write(`${line}\n`);
231
+ if (result.sha256)
232
+ process.stdout.write(`Prüfsumme (SHA-256): ${result.sha256}\n`);
233
+ return result.status === "OK" ? 0 : 1;
234
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cuonztech-agent-harness",
3
- "version": "0.10.0",
3
+ "version": "0.11.1",
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",
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.10.0",
9
+ "version": "0.11.1",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "cuonztech-agent-harness",
14
- "version": "0.10.0",
14
+ "version": "0.11.1",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  }