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/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` +
|
|
@@ -91,6 +92,14 @@ function printHelp() {
|
|
|
91
92
|
`Tool classification in proxy mode: explicit flags first, then the upstream's own\n` +
|
|
92
93
|
`MCP annotations (readOnlyHint/destructiveHint), then name patterns. The result is\n` +
|
|
93
94
|
`printed to stderr at startup; unclassified tools are passed through but not scored.\n\n` +
|
|
95
|
+
` --license <key> Licence key (or env AGENT_AUDIT_LICENSE), see "Lizenz" below\n\n` +
|
|
96
|
+
`Lizenz (Schlüssel CAUDIT-..., Prüfung offline):\n` +
|
|
97
|
+
` Der Lauf selbst ist immer vollständig; Urteil, Score und Exit-Code sind echt.\n` +
|
|
98
|
+
` Frei: ${FREE_SCOPE_DE}.\n` +
|
|
99
|
+
` Pro: report --dossier (ohne Lizenz nur Kopf, Score, Urteil), ui, campaign, Schema-Befunde\n` +
|
|
100
|
+
` und Textbausteine (Härtungsvorschläge) im Report.\n` +
|
|
101
|
+
` Angabe per --license <key> (proxy, report, ui, campaign) oder Umgebungsvariable AGENT_AUDIT_LICENSE.\n` +
|
|
102
|
+
` Lizenz erwerben: ${PURCHASE_URL}\n\n` +
|
|
94
103
|
`The proxy is a long-running server: its exit code says nothing about the verdict.\n` +
|
|
95
104
|
`Read the verdict from --report-file. Exit code 1 means the run itself broke\n` +
|
|
96
105
|
`(bad arguments, upstream did not start, report file not writable).\n\n` +
|
|
@@ -183,6 +192,21 @@ function parseFlags(argv, spec) {
|
|
|
183
192
|
// failed start — including a rejected command line — replaces a report left
|
|
184
193
|
// over from an earlier run instead of leaving its old PASS in place.
|
|
185
194
|
let startupReportFile = null;
|
|
195
|
+
let archivedPreviousRun = null;
|
|
196
|
+
let archiveAttempted = false;
|
|
197
|
+
function tryArchivePreviousReport() {
|
|
198
|
+
if (archiveAttempted || !startupReportFile)
|
|
199
|
+
return;
|
|
200
|
+
archiveAttempted = true;
|
|
201
|
+
try {
|
|
202
|
+
archivedPreviousRun = archivePreviousReportSync(startupReportFile);
|
|
203
|
+
}
|
|
204
|
+
catch (err) {
|
|
205
|
+
// Fail closed: could not archive existing real report
|
|
206
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
207
|
+
process.exit(1);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
186
210
|
// mkdir -p without { recursive: true }: on Linux pseudo file systems
|
|
187
211
|
// (/proc/…) Node's recursive mkdir never returns and spins at full CPU.
|
|
188
212
|
// Here every level is created once and the first error ends it.
|
|
@@ -219,6 +243,7 @@ function writeStartupStateSync(extra) {
|
|
|
219
243
|
if (!startupReportFile)
|
|
220
244
|
return;
|
|
221
245
|
try {
|
|
246
|
+
tryArchivePreviousReport();
|
|
222
247
|
// Only ever a regular file (or nothing yet): writing "the report" over
|
|
223
248
|
// a directory, a device node or a pipe is never what was meant.
|
|
224
249
|
const existing = statSync(startupReportFile, { throwIfNoEntry: false });
|
|
@@ -231,6 +256,16 @@ function writeStartupStateSync(extra) {
|
|
|
231
256
|
harnessVersion: VERSION,
|
|
232
257
|
certification: "UNVERIFIED_FOR_PRODUCTION",
|
|
233
258
|
verdict: "NOT_TESTED",
|
|
259
|
+
...(archivedPreviousRun
|
|
260
|
+
? {
|
|
261
|
+
previousRun: {
|
|
262
|
+
file: archivedPreviousRun.archivedPath,
|
|
263
|
+
verdict: archivedPreviousRun.verdict,
|
|
264
|
+
callsCount: archivedPreviousRun.callsCount,
|
|
265
|
+
archivedAt: archivedPreviousRun.archivedAt,
|
|
266
|
+
},
|
|
267
|
+
}
|
|
268
|
+
: {}),
|
|
234
269
|
...extra,
|
|
235
270
|
generatedAt: new Date().toISOString(),
|
|
236
271
|
}, null, 2), "utf-8");
|
|
@@ -344,6 +379,7 @@ const PROXY_FLAGS = {
|
|
|
344
379
|
"--audit-tools": "single",
|
|
345
380
|
"--report-file": "single",
|
|
346
381
|
"--allow-writes": "boolean",
|
|
382
|
+
"--license": "single",
|
|
347
383
|
};
|
|
348
384
|
function parseEnvPair(pair, flag, into) {
|
|
349
385
|
const eq = pair.indexOf("=");
|
|
@@ -367,6 +403,8 @@ async function runProxyMode(argv) {
|
|
|
367
403
|
fail(parsed.error);
|
|
368
404
|
const one = (name) => parsed.values.get(name)?.[0];
|
|
369
405
|
const many = (name) => parsed.values.get(name) ?? [];
|
|
406
|
+
// Lizenz: ändert nie den Lauf, nur den Umfang der geschriebenen Ausgabe (Textbausteine, Schema-Befunde).
|
|
407
|
+
const gate = resolveProGate(one("--license"));
|
|
370
408
|
const upstreamCommand = one("--upstream-command");
|
|
371
409
|
if (!upstreamCommand) {
|
|
372
410
|
fail("proxy mode requires --upstream-command <cmd>\n" +
|
|
@@ -485,9 +523,20 @@ async function runProxyMode(argv) {
|
|
|
485
523
|
}
|
|
486
524
|
}
|
|
487
525
|
const reportTarget = reportFile;
|
|
526
|
+
if (!startupReportFile && reportTarget) {
|
|
527
|
+
startupReportFile = reportTarget;
|
|
528
|
+
tryArchivePreviousReport();
|
|
529
|
+
}
|
|
488
530
|
const log = (line) => {
|
|
489
531
|
process.stderr.write(`[cuonztech-agent-harness] ${line}\n`);
|
|
490
532
|
};
|
|
533
|
+
log(gate.pro
|
|
534
|
+
? "Lizenz: gültig (Pro-Umfang)."
|
|
535
|
+
: `Keine gültige Lizenz: ${reasonSentence(gate.verdict)} Der Lauf ist vollständig, Urteil, Score und Exit-Code sind echt. ` +
|
|
536
|
+
`Im Report fehlen die Pro-Teile (Textbausteine, Schema-Befunde). Lizenz: ${PURCHASE_URL}`);
|
|
537
|
+
if (archivedPreviousRun) {
|
|
538
|
+
log(`NOTE: An earlier report with ${archivedPreviousRun.callsCount} call(s) was archived to "${archivedPreviousRun.archivedPath}".`);
|
|
539
|
+
}
|
|
491
540
|
// A tool name comes from the upstream: a line break in it must not be
|
|
492
541
|
// able to fake further lines of this log.
|
|
493
542
|
const shown = (name) => name.replace(/[\u0000-\u001f\u007f-\u009f\u2028\u2029]/g, "?");
|
|
@@ -507,6 +556,13 @@ async function runProxyMode(argv) {
|
|
|
507
556
|
"Only do that against a disposable test/staging system, never production.\n" +
|
|
508
557
|
"If --upstream-command points at such a system, confirm it by adding --allow-writes.");
|
|
509
558
|
}
|
|
559
|
+
if (writesReachUpstream) {
|
|
560
|
+
// Wortlaut nach LICENSE Ziffer 4a: der Schalter --allow-writes ist die Bestätigung des Anwenders.
|
|
561
|
+
log("ACHTUNG: Der Harness führt die Schreibvorgänge Ihres Agenten wirklich am Upstream aus und löst " +
|
|
562
|
+
"absichtlich Fehler, Wiederholungen und Verzögerungen aus. Nur gegen ein Testsystem mit " +
|
|
563
|
+
"synthetischen Daten einsetzen, das keine Produktivumgebung ist und von der Aussenwelt " +
|
|
564
|
+
"getrennt ist (LICENSE Ziffer 4a). Mit --allow-writes bestätigen Sie das.");
|
|
565
|
+
}
|
|
510
566
|
if (scenarioId === "F1") {
|
|
511
567
|
log("WARNING: scenario F1 really executes the agent's first write upstream and then " +
|
|
512
568
|
"reports a timeout to the agent. Only point --upstream-command at a disposable " +
|
|
@@ -652,8 +708,10 @@ async function runProxyMode(argv) {
|
|
|
652
708
|
try {
|
|
653
709
|
const report = generateReport(interceptor.getSession());
|
|
654
710
|
const score = computeScore([report]);
|
|
655
|
-
const patches = generatePatches([report], score);
|
|
711
|
+
const patches = gate.pro ? generatePatches([report], score) : []; // ohne Lizenz nicht erzeugt und nicht geschrieben
|
|
656
712
|
const callForensics = buildCallForensics(interceptor.getSession());
|
|
713
|
+
// Ohne Lizenz: Schema-Gutachten nur mit Zusammenfassung und Risikostufe, ohne Textbausteine.
|
|
714
|
+
const reportedSchemaAudit = gate.pro ? schemaAudit : reduceSchemaAudit(schemaAudit);
|
|
657
715
|
const body = JSON.stringify({
|
|
658
716
|
harnessVersion: VERSION,
|
|
659
717
|
// Constant: a self-test, never a certificate.
|
|
@@ -662,15 +720,35 @@ async function runProxyMode(argv) {
|
|
|
662
720
|
verdict: report.verdict,
|
|
663
721
|
report,
|
|
664
722
|
score,
|
|
665
|
-
|
|
723
|
+
...(gate.pro
|
|
724
|
+
? { patches }
|
|
725
|
+
: {
|
|
726
|
+
limitedByLicense: true,
|
|
727
|
+
licenseNotice: "Keine gültige Lizenz: Textbausteine (Härtungsvorschläge) und Schema-Befunde gehören zum Pro-Umfang und fehlen hier. " +
|
|
728
|
+
`Lauf, Urteil, Score und Exit-Code sind echt. Frei bleiben: ${FREE_SCOPE_DE}. Lizenz: ${PURCHASE_URL}`,
|
|
729
|
+
}),
|
|
730
|
+
...(archivedPreviousRun
|
|
731
|
+
? {
|
|
732
|
+
previousRun: {
|
|
733
|
+
file: archivedPreviousRun.archivedPath,
|
|
734
|
+
verdict: archivedPreviousRun.verdict,
|
|
735
|
+
callsCount: archivedPreviousRun.callsCount,
|
|
736
|
+
archivedAt: archivedPreviousRun.archivedAt,
|
|
737
|
+
},
|
|
738
|
+
}
|
|
739
|
+
: {}),
|
|
666
740
|
// Deutscher Prompt-Baustein und Schema-Gutachten (neu in 0.7.0, optional für Leser).
|
|
667
|
-
|
|
668
|
-
|
|
741
|
+
...(gate.pro
|
|
742
|
+
? {
|
|
743
|
+
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)),
|
|
744
|
+
}
|
|
745
|
+
: {}),
|
|
746
|
+
schema_audit: reportedSchemaAudit,
|
|
669
747
|
call_forensics: callForensics,
|
|
670
748
|
integrity: computeReportIntegrity({
|
|
671
749
|
harnessVersion: VERSION,
|
|
672
750
|
report,
|
|
673
|
-
schema_audit:
|
|
751
|
+
schema_audit: reportedSchemaAudit,
|
|
674
752
|
call_forensics: callForensics,
|
|
675
753
|
}),
|
|
676
754
|
toolClassification: classification,
|
|
@@ -771,7 +849,7 @@ async function runProxyMode(argv) {
|
|
|
771
849
|
process.stdin.on("end", shutdown);
|
|
772
850
|
process.stdin.on("close", shutdown);
|
|
773
851
|
startupReportFile = null;
|
|
774
|
-
const server = createProxyServer(interceptor, { auditTools });
|
|
852
|
+
const server = createProxyServer(interceptor, { auditTools, patches: gate.pro });
|
|
775
853
|
// The SDK closes the transport on its own when a single message exceeds
|
|
776
854
|
// its buffer (about 10 MB). Without this the proxy stayed alive, deaf,
|
|
777
855
|
// 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 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { verifyEd25519, signEd25519, defaultPublicKey } from "./ed25519.js";
|
|
2
|
+
function b64url(buf) {
|
|
3
|
+
return Buffer.from(buf).toString("base64url");
|
|
4
|
+
}
|
|
5
|
+
export function signLicense(claims, seed, domain = "caudit-license-v1") {
|
|
6
|
+
// Der Harness gibt nur Codes mit Präfix CAUDIT- aus; `domain` dient den Tests, um falsch signierte Codes zu bauen.
|
|
7
|
+
const payload = b64url(JSON.stringify(claims));
|
|
8
|
+
const sig = signEd25519(domain, payload, seed);
|
|
9
|
+
return `CAUDIT-${payload}.${sig}`;
|
|
10
|
+
}
|
|
11
|
+
export function verifyLicense(token, publicKey = defaultPublicKey()) {
|
|
12
|
+
if (!token || typeof token !== "string" || token.trim() === "") {
|
|
13
|
+
return {
|
|
14
|
+
valid: false,
|
|
15
|
+
reason: "Kein Lizenzschlüssel angegeben. Lizenzen erhältlich unter https://cuonztech.ch/agent-audit",
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
const clean = token.trim();
|
|
19
|
+
// Nur CAUDIT-Codes mit ihrer eigenen Signaturdomäne. Schlüssel anderer Produkte (zum Beispiel CPCG-)
|
|
20
|
+
// berechtigen nicht zum Pro-Umfang und werden hier nicht akzeptiert.
|
|
21
|
+
const prefix = "CAUDIT-";
|
|
22
|
+
const domain = "caudit-license-v1";
|
|
23
|
+
if (!clean.startsWith(prefix)) {
|
|
24
|
+
const hint = clean.startsWith("CPCG-")
|
|
25
|
+
? "Ein CPCG-Schlüssel (Privacy Chaos Gate) berechtigt nicht zum Pro-Umfang des Agent Harness."
|
|
26
|
+
: "Ungültiges Lizenzformat. Ein Lizenzschlüssel für den Agent Harness beginnt mit 'CAUDIT-'.";
|
|
27
|
+
return { valid: false, reason: hint };
|
|
28
|
+
}
|
|
29
|
+
const body = clean.slice(prefix.length);
|
|
30
|
+
const dot = body.lastIndexOf(".");
|
|
31
|
+
if (dot <= 0) {
|
|
32
|
+
return { valid: false, reason: "Lizenzsignatur fehlt oder ist beschädigt." };
|
|
33
|
+
}
|
|
34
|
+
const payload = body.slice(0, dot);
|
|
35
|
+
const sig = body.slice(dot + 1);
|
|
36
|
+
let claims;
|
|
37
|
+
try {
|
|
38
|
+
const json = Buffer.from(payload, "base64url").toString("utf-8");
|
|
39
|
+
claims = JSON.parse(json);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return { valid: false, reason: "Lizenzdaten konnten nicht decodiert werden." };
|
|
43
|
+
}
|
|
44
|
+
// Signature verification (Ed25519)
|
|
45
|
+
const isSigValid = verifyEd25519(domain, payload, sig, publicKey);
|
|
46
|
+
if (!isSigValid) {
|
|
47
|
+
return { valid: false, reason: "Kryptografische Signatur der Lizenz ist ungültig." };
|
|
48
|
+
}
|
|
49
|
+
if (!claims || typeof claims !== "object") {
|
|
50
|
+
return { valid: false, reason: "Lizenzdaten konnten nicht decodiert werden." };
|
|
51
|
+
}
|
|
52
|
+
// Ablauf (Unix-Sekunden): `exp` ist zwingend. Ein Code ohne Ablaufdatum ist ungültig.
|
|
53
|
+
const nowSec = Math.floor(Date.now() / 1000);
|
|
54
|
+
if (typeof claims.exp !== "number" || !Number.isFinite(claims.exp)) {
|
|
55
|
+
return {
|
|
56
|
+
valid: false,
|
|
57
|
+
reason: "Lizenz ist ungültig: Ablaufdatum fehlt.",
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
if (claims.exp < nowSec) {
|
|
61
|
+
const expiredDate = new Date(claims.exp * 1000).toISOString().slice(0, 10);
|
|
62
|
+
return {
|
|
63
|
+
valid: false,
|
|
64
|
+
claims,
|
|
65
|
+
tier: claims.tier,
|
|
66
|
+
reason: `Lizenz ist am ${expiredDate} abgelaufen. Verlängerung unter https://cuonztech.ch/agent-audit`,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
valid: true,
|
|
71
|
+
claims,
|
|
72
|
+
tier: claims.tier || "pro",
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
export function resolveLicense(explicitKey) {
|
|
76
|
+
const token = explicitKey ||
|
|
77
|
+
process.env.AGENT_AUDIT_LICENSE ||
|
|
78
|
+
process.env.CUONZTECH_LICENSE_KEY;
|
|
79
|
+
if (token) {
|
|
80
|
+
return verifyLicense(token);
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
valid: false,
|
|
84
|
+
reason: "Nicht lizenziert",
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
export const PURCHASE_HINT = "CuonzTech Agent Audit Pro (Prüf-Dossier, geführte Oberfläche, Schema-Gutachten, Härtungsvorschläge, Kampagnen-Runner)" + "\n" +
|
|
88
|
+
"Lizenz: https://cuonztech.ch/agent-harness.html#audit" + "\n" +
|
|
89
|
+
"Aktivierung: --license <KEY> oder Umgebungsvariable AGENT_AUDIT_LICENSE=<KEY>";
|
package/dist/plain-report.js
CHANGED
|
@@ -2,22 +2,32 @@
|
|
|
2
2
|
// ein Mensch ohne Fachwissen lesen kann: Ergebnis, was getestet wurde, was der
|
|
3
3
|
// Agent getan hat, was zu tun ist und was dieser Test NICHT aussagt.
|
|
4
4
|
import { readFile, writeFile, stat, realpath } from "node:fs/promises";
|
|
5
|
-
import { extname, resolve } from "node:path";
|
|
5
|
+
import { basename, extname, resolve } from "node:path";
|
|
6
6
|
import { evaluateAgentClaim } from "./core/claim-check.js";
|
|
7
7
|
import { verifyReportIntegrity } from "./core/artifact-integrity.js";
|
|
8
8
|
import { isReservedFileName } from "./report-file-guard.js";
|
|
9
9
|
import { renderDossierHtml, renderDossierMarkdown } from "./dossier-report.js";
|
|
10
|
+
import { resolveProGate, shortenedNotice, PURCHASE_URL } from "./license/index.js";
|
|
10
11
|
const RISK_DE = { HIGH: "HOCH", MEDIUM: "MITTEL", LOW: "NIEDRIG" };
|
|
11
12
|
// Tool-Namen kommen vom Upstream: Zeilenumbrüche und Backticks dürfen die
|
|
12
13
|
// Struktur des Berichts nicht verändern.
|
|
13
14
|
function safeName(name) {
|
|
14
15
|
return name.replace(/[\u0000-\u001f\u007f-\u009f\u2028\u2029`]/g, "?");
|
|
15
16
|
}
|
|
16
|
-
function schemaSection(audit) {
|
|
17
|
+
function schemaSection(audit, pro) {
|
|
17
18
|
const lines = ["## Schnittstellen- & Schema-Prüfung (MCP-Tools)", "", audit.summaryDe, ""];
|
|
18
19
|
const writeLike = audit.tools.filter((t) => t.classification !== "read");
|
|
19
20
|
if (writeLike.length === 0)
|
|
20
21
|
return lines;
|
|
22
|
+
if (!pro) {
|
|
23
|
+
// Ohne Lizenz: Zusammenfassung und Risikostufe je Tool; Befunde und Schema-Vorschläge gehören zum Pro-Umfang.
|
|
24
|
+
const order = ["HIGH", "MEDIUM", "LOW"];
|
|
25
|
+
for (const t of [...writeLike].sort((a, b) => order.indexOf(a.riskLevel) - order.indexOf(b.riskLevel))) {
|
|
26
|
+
lines.push(`- ${safeName(t.name)}: Risiko ${RISK_DE[t.riskLevel]}`);
|
|
27
|
+
}
|
|
28
|
+
lines.push("", `Die Befunde je Tool und die kopierfertigen Schema-Vorschläge gehören zum Pro-Umfang (Lizenz: ${PURCHASE_URL}).`, "");
|
|
29
|
+
return lines;
|
|
30
|
+
}
|
|
21
31
|
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
32
|
const order = ["HIGH", "MEDIUM", "LOW"];
|
|
23
33
|
for (const t of [...writeLike].sort((a, b) => order.indexOf(a.riskLevel) - order.indexOf(b.riskLevel))) {
|
|
@@ -83,8 +93,13 @@ export const LIMITS = [
|
|
|
83
93
|
];
|
|
84
94
|
// Ist die Antwort des Agenten geprüft worden, darf der erste Punkt nicht mehr behaupten, sie werde nicht geprüft.
|
|
85
95
|
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
|
|
87
|
-
|
|
96
|
+
export const LIMIT_SPLIT_RUN = "Der Lauf folgte auf einen früheren Lauf (archiviert). Durch den Neustart des Prüfstands gilt dieses Ergebnis nur für die Aufrufe seit dem Neustart; Aufrufe vor dem Neustart sind im Archiv gesichert.";
|
|
97
|
+
export function limitsFor(claim, previousRun) {
|
|
98
|
+
const base = claim && claim.status !== "NOT_PROVIDED" ? [LIMIT_ANSWER_CHECKED, ...LIMITS.slice(1)] : [...LIMITS];
|
|
99
|
+
if (previousRun) {
|
|
100
|
+
base.push(LIMIT_SPLIT_RUN);
|
|
101
|
+
}
|
|
102
|
+
return base;
|
|
88
103
|
}
|
|
89
104
|
export function verdictText(file) {
|
|
90
105
|
const report = file.report;
|
|
@@ -156,7 +171,8 @@ function claimSection(claim) {
|
|
|
156
171
|
lines.push("");
|
|
157
172
|
return lines;
|
|
158
173
|
}
|
|
159
|
-
export function renderMarkdown(file) {
|
|
174
|
+
export function renderMarkdown(file, options = {}) {
|
|
175
|
+
const pro = options.pro ?? true;
|
|
160
176
|
const { headline, explanation } = verdictText(file);
|
|
161
177
|
const report = file.report;
|
|
162
178
|
const lines = ["# Ergebnis des Agent-Tests", "", `## ${headline}`, "", explanation, ""];
|
|
@@ -170,6 +186,9 @@ export function renderMarkdown(file) {
|
|
|
170
186
|
const scenario = report.scenarioId ? `${report.scenarioId}: ${SCENARIOS[report.scenarioId] ?? ""}` : "Kein Szenario (reines Durchreichen).";
|
|
171
187
|
lines.push(`- Szenario: ${scenario}`);
|
|
172
188
|
lines.push(`- Aufrufe des Agenten: ${report.totalCalls} (davon ${report.coverage.writeCalls} schreibend, ${report.coverage.readCalls} lesend)`);
|
|
189
|
+
if (file.previousRun) {
|
|
190
|
+
lines.push(`- **Vorläufer-Lauf archiviert:** Vor diesem Lauf lag ein früherer Lauf mit ${file.previousRun.callsCount} Aufruf(en) vor (gesichert als \`${basename(file.previousRun.file)}\`).`);
|
|
191
|
+
}
|
|
173
192
|
lines.push(`- Eingespielte Fehler: ${report.injectedErrors}`);
|
|
174
193
|
if (report.coverage.scenarioTriggered === false) {
|
|
175
194
|
lines.push("- **Das Szenario wurde nie ausgelöst.** Es gab keinen Schreibvorgang, auf den es wirken konnte.");
|
|
@@ -201,7 +220,10 @@ export function renderMarkdown(file) {
|
|
|
201
220
|
lines.push("Die Kennzahl fasst nur diesen einen Lauf zusammen; sie ist kein Vergleichswert zwischen Agenten.", "");
|
|
202
221
|
}
|
|
203
222
|
const hasPatches = Boolean(file.patches && file.patches.length > 0);
|
|
204
|
-
if (
|
|
223
|
+
if (!pro) {
|
|
224
|
+
lines.push("## Härtungsvorschläge", "", `Textbausteine für den Prompt des Agenten (Deutsch und Englisch) gehören zum Pro-Umfang. Das Urteil, der Score und die Beobachtungen sind vollständig. Lizenz: ${PURCHASE_URL}`, "");
|
|
225
|
+
}
|
|
226
|
+
else if (hasPatches || file.recommended_prompt_patch_de) {
|
|
205
227
|
lines.push(hasPatches ? "## Was zu tun ist" : "## Empfohlener Prompt-Baustein", "");
|
|
206
228
|
if (file.recommended_prompt_patch_de) {
|
|
207
229
|
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, "```", "");
|
|
@@ -210,7 +232,7 @@ export function renderMarkdown(file) {
|
|
|
210
232
|
lines.push(`### ${patch.category} (${patch.severity})`, "", patch.trigger_de ?? patch.trigger, "", "Textbaustein für den Prompt des Agenten (Englisch, Originalfassung):", "", "```text", patch.promptSnippet, "```", "");
|
|
211
233
|
}
|
|
212
234
|
}
|
|
213
|
-
const observations = report.observations_de ?? report.observations.map((o) => `Aufruf ${o.callNumber}, ${o.tool}: ${o.description}`);
|
|
235
|
+
const observations = report.observations_de ?? (report.observations ?? []).map((o) => `Aufruf ${o.callNumber}, ${o.tool}: ${o.description}`);
|
|
214
236
|
if (observations.length > 0) {
|
|
215
237
|
lines.push("## Beobachtungen", "");
|
|
216
238
|
for (const text of observations)
|
|
@@ -223,12 +245,12 @@ export function renderMarkdown(file) {
|
|
|
223
245
|
if (file.claim_check && file.claim_check.status !== "NOT_PROVIDED")
|
|
224
246
|
lines.push(...claimSection(file.claim_check));
|
|
225
247
|
if (file.schema_audit)
|
|
226
|
-
lines.push(...schemaSection(file.schema_audit));
|
|
248
|
+
lines.push(...schemaSection(file.schema_audit, pro));
|
|
227
249
|
if (file.integrity) {
|
|
228
250
|
lines.push(`Integrität: ${verifyReportIntegrity(file).reasonDe} Prüfsumme (SHA-256): \`${safeName(String(file.integrity.payloadSha256))}\``, "");
|
|
229
251
|
}
|
|
230
252
|
lines.push("## Was dieser Test nicht aussagt", "");
|
|
231
|
-
for (const limit of limitsFor(file.claim_check))
|
|
253
|
+
for (const limit of limitsFor(file.claim_check, file.previousRun))
|
|
232
254
|
lines.push(`- ${limit}`);
|
|
233
255
|
lines.push("");
|
|
234
256
|
const meta = [file.harnessVersion ? `Harness ${file.harnessVersion}` : null, file.generatedAt ? `erstellt ${file.generatedAt}` : null].filter(Boolean);
|
|
@@ -241,8 +263,8 @@ function escapeHtml(text) {
|
|
|
241
263
|
}
|
|
242
264
|
// Rendert das Markdown oben in ein eigenständiges HTML. Bewusst klein gehalten:
|
|
243
265
|
// nur die Konstrukte, die renderMarkdown erzeugt.
|
|
244
|
-
export function renderHtmlBody(file) {
|
|
245
|
-
const markdown = renderMarkdown(file);
|
|
266
|
+
export function renderHtmlBody(file, options = {}) {
|
|
267
|
+
const markdown = renderMarkdown(file, options);
|
|
246
268
|
const out = [];
|
|
247
269
|
let inCode = false;
|
|
248
270
|
let inList = false;
|
|
@@ -300,8 +322,8 @@ export function renderHtmlBody(file) {
|
|
|
300
322
|
closeList();
|
|
301
323
|
return out;
|
|
302
324
|
}
|
|
303
|
-
export function renderHtml(file) {
|
|
304
|
-
const out = renderHtmlBody(file);
|
|
325
|
+
export function renderHtml(file, options = {}) {
|
|
326
|
+
const out = renderHtmlBody(file, options);
|
|
305
327
|
const verdict = file.verdict ?? "NOT_TESTED";
|
|
306
328
|
const colour = verdict === "PASS" ? "#1a7f37" : verdict === "FAIL" ? "#cf222e" : "#9a6700";
|
|
307
329
|
return [
|
|
@@ -343,12 +365,15 @@ export async function runReportMode(argv) {
|
|
|
343
365
|
" --format md (default) or html\n" +
|
|
344
366
|
" --out write to this file instead of the terminal. An existing file is only\n" +
|
|
345
367
|
" replaced if an earlier `report` run wrote it, never the report file itself.\n" +
|
|
346
|
-
" --dossier render the
|
|
368
|
+
" --dossier render the test and architecture dossier (6 sections) instead of the short page.\n" +
|
|
369
|
+
" Without a valid license it is shortened to header, score and verdict per scenario (Pro scope).\n" +
|
|
347
370
|
" --customer <name> client / environment named in the dossier (needs --dossier)\n" +
|
|
348
371
|
" --system <name> system under test named in the dossier (needs --dossier)\n" +
|
|
349
372
|
" --agent-response <text>\n" +
|
|
350
373
|
" the agent's final answer to the user; it is checked against what the\n" +
|
|
351
|
-
" target system actually did (honesty check)\n"
|
|
374
|
+
" target system actually did (honesty check)\n" +
|
|
375
|
+
" --license <key> CuonzTech Agent Audit Pro license key (or AGENT_AUDIT_LICENSE env var). Without one, the short\n" +
|
|
376
|
+
" page leaves out the hardening suggestions and schema findings; verdict and score stay complete.\n");
|
|
352
377
|
return 0;
|
|
353
378
|
}
|
|
354
379
|
let file;
|
|
@@ -358,11 +383,20 @@ export async function runReportMode(argv) {
|
|
|
358
383
|
let customerName;
|
|
359
384
|
let systemName;
|
|
360
385
|
let agentResponse;
|
|
386
|
+
let licenseKey;
|
|
361
387
|
for (let i = 0; i < argv.length; i++) {
|
|
362
388
|
const token = argv[i];
|
|
363
389
|
if (token === "--dossier") {
|
|
364
390
|
dossier = true;
|
|
365
391
|
}
|
|
392
|
+
else if (token === "--license") {
|
|
393
|
+
const v = argv[++i];
|
|
394
|
+
if (v === undefined || v.startsWith("--") || v.trim() === "") {
|
|
395
|
+
process.stderr.write(`Error: --license needs a key.\n${REPORT_USAGE}`);
|
|
396
|
+
return 1;
|
|
397
|
+
}
|
|
398
|
+
licenseKey = v;
|
|
399
|
+
}
|
|
366
400
|
else if (token === "--customer" || token === "--system") {
|
|
367
401
|
const v = argv[++i];
|
|
368
402
|
if (v === undefined || v.startsWith("--") || v.trim() === "") {
|
|
@@ -456,10 +490,17 @@ export async function runReportMode(argv) {
|
|
|
456
490
|
return 1;
|
|
457
491
|
}
|
|
458
492
|
const chosen = format ?? (outExt === ".html" || outExt === ".htm" ? "html" : "md");
|
|
459
|
-
const
|
|
493
|
+
const { pro, verdict: licenseVerdict } = resolveProGate(licenseKey);
|
|
494
|
+
// Ohne Lizenz wird nur die Ausgabe gekürzt (Dossier: Kopf, Score, Urteil; Bericht: ohne Härtungsvorschläge
|
|
495
|
+
// und Schema-Befunde). Der Hinweis geht auf stderr, damit er nie in der Berichtsdatei oder einer Pipe landet.
|
|
496
|
+
if (!pro && !process.env.AGENT_AUDIT_SUPPRESS_LICENSE_HINT) {
|
|
497
|
+
if (dossier)
|
|
498
|
+
process.stderr.write(shortenedNotice("dossier", licenseVerdict) + "\n");
|
|
499
|
+
}
|
|
500
|
+
const dossierOptions = { customerName, systemName, agentResponse, licenseVerdict, limited: !pro };
|
|
460
501
|
const text = dossier
|
|
461
502
|
? chosen === "html" ? renderDossierHtml(parsed, dossierOptions) : renderDossierMarkdown(parsed, dossierOptions)
|
|
462
|
-
: chosen === "html" ? renderHtml(parsed) : renderMarkdown(parsed);
|
|
503
|
+
: chosen === "html" ? renderHtml(parsed, { pro }) : renderMarkdown(parsed, { pro });
|
|
463
504
|
if (outPath) {
|
|
464
505
|
if (isReservedFileName(outPath)) {
|
|
465
506
|
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.
|