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.
- package/LICENSE +220 -21
- package/LICENSE-MIT-bis-0.7.0.txt +21 -0
- package/README.md +54 -14
- package/dist/benchmark/cta.js +3 -3
- package/dist/campaign/runner.js +22 -3
- package/dist/dossier-report.js +53 -2
- package/dist/index.js +87 -9
- package/dist/license/ed25519.js +57 -0
- package/dist/license/gate.js +91 -0
- package/dist/license/index.js +5 -0
- package/dist/license/publicKey.js +5 -0
- package/dist/license/types.js +1 -0
- package/dist/license/verifier.js +89 -0
- package/dist/plain-report.js +58 -17
- package/dist/proxy/gateway.js +2 -1
- package/dist/report-file-guard.js +75 -1
- package/dist/tools/handlers.js +9 -4
- package/dist/ui/server.js +47 -5
- package/package.json +3 -2
- package/server.json +2 -2
package/dist/proxy/gateway.js
CHANGED
|
@@ -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
|
+
}
|
package/dist/tools/handlers.js
CHANGED
|
@@ -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
|
-
|
|
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, {
|
|
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
|
-
"
|
|
330
|
-
" --
|
|
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
|
-
|
|
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.
|
|
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": "
|
|
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.
|
|
9
|
+
"version": "0.8.0",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "cuonztech-agent-harness",
|
|
14
|
-
"version": "0.
|
|
14
|
+
"version": "0.8.0",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
}
|