agent-inspect 2.0.0 → 2.2.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/CHANGELOG.md +24 -0
- package/README.md +27 -4
- package/docs/ADAPTERS.md +4 -0
- package/docs/API.md +75 -10
- package/docs/CLI.md +124 -9
- package/docs/COMPARE.md +5 -4
- package/docs/GETTING-STARTED.md +24 -6
- package/docs/KNOWN-ISSUES.md +7 -0
- package/docs/LIMITATIONS.md +3 -1
- package/package.json +12 -2
- package/packages/cli/dist/index.cjs +2040 -142
- package/packages/cli/dist/index.cjs.map +1 -1
- package/packages/cli/dist/index.mjs +2039 -141
- package/packages/cli/dist/index.mjs.map +1 -1
- package/packages/core/dist/reporters.cjs +185 -0
- package/packages/core/dist/reporters.cjs.map +1 -0
- package/packages/core/dist/reporters.d.cts +91 -0
- package/packages/core/dist/reporters.d.ts +91 -0
- package/packages/core/dist/reporters.mjs +175 -0
- package/packages/core/dist/reporters.mjs.map +1 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { realpathSync, createReadStream } from 'fs';
|
|
3
|
-
import
|
|
3
|
+
import path13 from 'path';
|
|
4
4
|
import { fileURLToPath, pathToFileURL } from 'url';
|
|
5
5
|
import { Command, Option } from 'commander';
|
|
6
6
|
import { AsyncLocalStorage } from 'async_hooks';
|
|
@@ -12,7 +12,7 @@ import tty from 'tty';
|
|
|
12
12
|
import { createInterface } from 'readline';
|
|
13
13
|
|
|
14
14
|
// package.json
|
|
15
|
-
var version = "2.
|
|
15
|
+
var version = "2.2.0";
|
|
16
16
|
|
|
17
17
|
// packages/core/src/correlation-metadata.ts
|
|
18
18
|
var TRACE_CORRELATION_KEYS = [
|
|
@@ -744,7 +744,7 @@ function formatDuration(ms) {
|
|
|
744
744
|
// packages/core/src/utils.ts
|
|
745
745
|
var DEFAULT_TRACE_DIR_NAME = ".agent-inspect";
|
|
746
746
|
var RUNS_DIR_NAME = "runs";
|
|
747
|
-
var FALLBACK_TRACE_DIR =
|
|
747
|
+
var FALLBACK_TRACE_DIR = path13.join(
|
|
748
748
|
os.tmpdir(),
|
|
749
749
|
"agent-inspect",
|
|
750
750
|
RUNS_DIR_NAME
|
|
@@ -779,7 +779,7 @@ function getDefaultTraceDir() {
|
|
|
779
779
|
if (typeof home !== "string" || home.trim() === "") {
|
|
780
780
|
return FALLBACK_TRACE_DIR;
|
|
781
781
|
}
|
|
782
|
-
return
|
|
782
|
+
return path13.join(home, DEFAULT_TRACE_DIR_NAME, RUNS_DIR_NAME);
|
|
783
783
|
} catch {
|
|
784
784
|
return FALLBACK_TRACE_DIR;
|
|
785
785
|
}
|
|
@@ -787,11 +787,11 @@ function getDefaultTraceDir() {
|
|
|
787
787
|
function getTraceFilePath(runId, traceDir) {
|
|
788
788
|
const baseDir = traceDir ?? getDefaultTraceDir();
|
|
789
789
|
let safeId = typeof runId === "string" && runId.trim() !== "" ? runId.trim() : "run_unknown";
|
|
790
|
-
safeId =
|
|
790
|
+
safeId = path13.basename(safeId);
|
|
791
791
|
if (safeId === "" || safeId === "." || safeId === "..") {
|
|
792
792
|
safeId = "run_unknown";
|
|
793
793
|
}
|
|
794
|
-
return
|
|
794
|
+
return path13.join(baseDir, `${safeId}.jsonl`);
|
|
795
795
|
}
|
|
796
796
|
function formatError(error) {
|
|
797
797
|
if (error instanceof Error) {
|
|
@@ -1553,7 +1553,7 @@ var TraceDirectory = class {
|
|
|
1553
1553
|
this.#dir = resolveTraceDir(options);
|
|
1554
1554
|
}
|
|
1555
1555
|
getPath(filename) {
|
|
1556
|
-
return filename ?
|
|
1556
|
+
return filename ? path13.join(this.#dir, filename) : this.#dir;
|
|
1557
1557
|
}
|
|
1558
1558
|
async list() {
|
|
1559
1559
|
try {
|
|
@@ -1580,7 +1580,7 @@ function parseIsoToMs2(value) {
|
|
|
1580
1580
|
}
|
|
1581
1581
|
async function extractMetadata(filePath, _quickScan) {
|
|
1582
1582
|
const stats = await stat(filePath);
|
|
1583
|
-
let runIdFromFile =
|
|
1583
|
+
let runIdFromFile = path13.basename(filePath);
|
|
1584
1584
|
if (runIdFromFile.endsWith(".jsonl")) {
|
|
1585
1585
|
runIdFromFile = runIdFromFile.slice(0, -".jsonl".length);
|
|
1586
1586
|
}
|
|
@@ -3551,7 +3551,7 @@ function findReaderByFormat(format, readers) {
|
|
|
3551
3551
|
}
|
|
3552
3552
|
async function jsonlFilesInDirectory(dirPath) {
|
|
3553
3553
|
const entries = await readdir(dirPath, { withFileTypes: true });
|
|
3554
|
-
return entries.filter((entry) => entry.isFile() && entry.name.endsWith(".jsonl")).map((entry) =>
|
|
3554
|
+
return entries.filter((entry) => entry.isFile() && entry.name.endsWith(".jsonl")).map((entry) => path13.join(dirPath, entry.name)).sort((a, b) => a.localeCompare(b));
|
|
3555
3555
|
}
|
|
3556
3556
|
async function resolveInput(input3) {
|
|
3557
3557
|
const cached = resolvedInputCache.get(input3);
|
|
@@ -5294,11 +5294,11 @@ function isRecord9(v) {
|
|
|
5294
5294
|
function isNonEmptyStringArray(v) {
|
|
5295
5295
|
return Array.isArray(v) && v.length > 0 && v.every((x) => typeof x === "string" && x.trim() !== "");
|
|
5296
5296
|
}
|
|
5297
|
-
function validateRedact(
|
|
5298
|
-
if (!Array.isArray(
|
|
5297
|
+
function validateRedact(redact2) {
|
|
5298
|
+
if (!Array.isArray(redact2)) {
|
|
5299
5299
|
throw new Error("Invalid config: redact must be an array");
|
|
5300
5300
|
}
|
|
5301
|
-
for (const r of
|
|
5301
|
+
for (const r of redact2) {
|
|
5302
5302
|
if (typeof r === "string") continue;
|
|
5303
5303
|
if (!isRecord9(r)) {
|
|
5304
5304
|
throw new Error("Invalid config: redact entries must be strings or objects");
|
|
@@ -7677,9 +7677,9 @@ Trace directory: ${traceDir}`);
|
|
|
7677
7677
|
if (validation !== void 0 && !validation.ok) {
|
|
7678
7678
|
process.exitCode = 1;
|
|
7679
7679
|
}
|
|
7680
|
-
const outPath = options.output !== void 0 && options.output.trim() !== "" ?
|
|
7680
|
+
const outPath = options.output !== void 0 && options.output.trim() !== "" ? path13.resolve(options.output.trim()) : void 0;
|
|
7681
7681
|
if (outPath !== void 0) {
|
|
7682
|
-
await mkdir(
|
|
7682
|
+
await mkdir(path13.dirname(outPath), { recursive: true });
|
|
7683
7683
|
await writeFile(outPath, result.content, "utf-8");
|
|
7684
7684
|
const vlabel = validation !== void 0 ? validation.ok ? "ok" : "failed" : "skipped";
|
|
7685
7685
|
console.log(`Wrote ${result.fileExtension} export to ${outPath} (validation: ${vlabel})`);
|
|
@@ -7856,13 +7856,13 @@ function pairSteps(left, right) {
|
|
|
7856
7856
|
return pairs;
|
|
7857
7857
|
}
|
|
7858
7858
|
function compareLeafSteps(L, R, segments, opts, out) {
|
|
7859
|
-
const
|
|
7859
|
+
const path15 = buildPath(segments);
|
|
7860
7860
|
if (L.name !== R.name) {
|
|
7861
7861
|
out.push({
|
|
7862
7862
|
kind: "structure",
|
|
7863
7863
|
severity: "warning",
|
|
7864
7864
|
message: "Step name differs",
|
|
7865
|
-
path:
|
|
7865
|
+
path: path15,
|
|
7866
7866
|
left: L.name,
|
|
7867
7867
|
right: R.name
|
|
7868
7868
|
});
|
|
@@ -7872,7 +7872,7 @@ function compareLeafSteps(L, R, segments, opts, out) {
|
|
|
7872
7872
|
kind: "step-type",
|
|
7873
7873
|
severity: "warning",
|
|
7874
7874
|
message: "Step type differs",
|
|
7875
|
-
path:
|
|
7875
|
+
path: path15,
|
|
7876
7876
|
left: L.type,
|
|
7877
7877
|
right: R.type
|
|
7878
7878
|
});
|
|
@@ -7882,7 +7882,7 @@ function compareLeafSteps(L, R, segments, opts, out) {
|
|
|
7882
7882
|
kind: "step-status",
|
|
7883
7883
|
severity: "warning",
|
|
7884
7884
|
message: "Step status differs",
|
|
7885
|
-
path:
|
|
7885
|
+
path: path15,
|
|
7886
7886
|
left: L.status,
|
|
7887
7887
|
right: R.status
|
|
7888
7888
|
});
|
|
@@ -7894,7 +7894,7 @@ function compareLeafSteps(L, R, segments, opts, out) {
|
|
|
7894
7894
|
kind: "error",
|
|
7895
7895
|
severity: "error",
|
|
7896
7896
|
message: "Step error message differs",
|
|
7897
|
-
path:
|
|
7897
|
+
path: path15,
|
|
7898
7898
|
left: le || void 0,
|
|
7899
7899
|
right: re || void 0
|
|
7900
7900
|
});
|
|
@@ -7912,7 +7912,7 @@ function compareLeafSteps(L, R, segments, opts, out) {
|
|
|
7912
7912
|
kind: "duration",
|
|
7913
7913
|
severity: "info",
|
|
7914
7914
|
message: "Step duration differs",
|
|
7915
|
-
path:
|
|
7915
|
+
path: path15,
|
|
7916
7916
|
left: ld,
|
|
7917
7917
|
right: rd
|
|
7918
7918
|
});
|
|
@@ -7925,7 +7925,7 @@ function compareLeafSteps(L, R, segments, opts, out) {
|
|
|
7925
7925
|
kind: "metadata",
|
|
7926
7926
|
severity: "info",
|
|
7927
7927
|
message: "Step metadata differs",
|
|
7928
|
-
path:
|
|
7928
|
+
path: path15,
|
|
7929
7929
|
left: L.metadata,
|
|
7930
7930
|
right: R.metadata
|
|
7931
7931
|
});
|
|
@@ -7937,7 +7937,7 @@ function compareLeafSteps(L, R, segments, opts, out) {
|
|
|
7937
7937
|
kind: "output",
|
|
7938
7938
|
severity: "info",
|
|
7939
7939
|
message: "Output preview differs",
|
|
7940
|
-
path:
|
|
7940
|
+
path: path15,
|
|
7941
7941
|
left: L.outputPreview,
|
|
7942
7942
|
right: R.outputPreview
|
|
7943
7943
|
});
|
|
@@ -8097,11 +8097,11 @@ function diffRuns(left, right, options) {
|
|
|
8097
8097
|
}
|
|
8098
8098
|
|
|
8099
8099
|
// packages/core/src/diff/renderer.ts
|
|
8100
|
-
function formatPath(
|
|
8101
|
-
if (
|
|
8100
|
+
function formatPath(path15) {
|
|
8101
|
+
if (path15 === void 0 || path15.path.length === 0) {
|
|
8102
8102
|
return "(run)";
|
|
8103
8103
|
}
|
|
8104
|
-
return
|
|
8104
|
+
return path15.path.map((s) => s.name).join(" > ");
|
|
8105
8105
|
}
|
|
8106
8106
|
function formatValue(v, verbose) {
|
|
8107
8107
|
if (v === void 0) return "(undefined)";
|
|
@@ -8532,9 +8532,9 @@ async function reportCommand(runId, options = {}) {
|
|
|
8532
8532
|
redactionProfile,
|
|
8533
8533
|
correlation: !options.noCorrelation
|
|
8534
8534
|
});
|
|
8535
|
-
const outPath = options.output !== void 0 && options.output.trim() !== "" ?
|
|
8535
|
+
const outPath = options.output !== void 0 && options.output.trim() !== "" ? path13.resolve(options.output.trim()) : void 0;
|
|
8536
8536
|
if (outPath !== void 0) {
|
|
8537
|
-
await mkdir(
|
|
8537
|
+
await mkdir(path13.dirname(outPath), { recursive: true });
|
|
8538
8538
|
await writeFile(outPath, result.content, "utf-8");
|
|
8539
8539
|
console.log(`Wrote ${result.fileExtension} report to ${outPath}`);
|
|
8540
8540
|
}
|
|
@@ -8554,6 +8554,383 @@ async function reportCommand(runId, options = {}) {
|
|
|
8554
8554
|
console.log(result.content);
|
|
8555
8555
|
}
|
|
8556
8556
|
}
|
|
8557
|
+
var DEFAULT_REDACT_KEYS2 = [
|
|
8558
|
+
"authorization",
|
|
8559
|
+
"cookie",
|
|
8560
|
+
"token",
|
|
8561
|
+
"apiKey",
|
|
8562
|
+
"password",
|
|
8563
|
+
"secret",
|
|
8564
|
+
"email"
|
|
8565
|
+
];
|
|
8566
|
+
var SHARE_PROFILE_EXTRA_KEYS2 = [
|
|
8567
|
+
"userEmail",
|
|
8568
|
+
"customerEmail",
|
|
8569
|
+
"phone",
|
|
8570
|
+
"phoneNumber",
|
|
8571
|
+
"address",
|
|
8572
|
+
"ip",
|
|
8573
|
+
"ipAddress",
|
|
8574
|
+
"sessionId",
|
|
8575
|
+
"requestId",
|
|
8576
|
+
"correlationId",
|
|
8577
|
+
"decisionId",
|
|
8578
|
+
"groupId",
|
|
8579
|
+
"customerId",
|
|
8580
|
+
"userId",
|
|
8581
|
+
"accountId",
|
|
8582
|
+
"tenantId",
|
|
8583
|
+
"orgId",
|
|
8584
|
+
"organizationId",
|
|
8585
|
+
"traceId",
|
|
8586
|
+
"spanId",
|
|
8587
|
+
"parentSpanId"
|
|
8588
|
+
];
|
|
8589
|
+
var STRICT_PROFILE_EXTRA_KEYS2 = [
|
|
8590
|
+
"prompt",
|
|
8591
|
+
"completion",
|
|
8592
|
+
"input",
|
|
8593
|
+
"output",
|
|
8594
|
+
"inputPreview",
|
|
8595
|
+
"outputPreview",
|
|
8596
|
+
"message",
|
|
8597
|
+
"messages",
|
|
8598
|
+
"transcript",
|
|
8599
|
+
"context",
|
|
8600
|
+
"document",
|
|
8601
|
+
"documents",
|
|
8602
|
+
"chunk",
|
|
8603
|
+
"chunks",
|
|
8604
|
+
"retrieval",
|
|
8605
|
+
"query"
|
|
8606
|
+
];
|
|
8607
|
+
function isRecord13(value) {
|
|
8608
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
8609
|
+
}
|
|
8610
|
+
function toKey2(key) {
|
|
8611
|
+
return key.toLowerCase();
|
|
8612
|
+
}
|
|
8613
|
+
function stableHash2(value) {
|
|
8614
|
+
const hash = crypto.createHash("sha256").update(value, "utf8").digest("hex");
|
|
8615
|
+
return hash.slice(0, 8);
|
|
8616
|
+
}
|
|
8617
|
+
function stringifyScalar(value) {
|
|
8618
|
+
if (typeof value === "string") return value;
|
|
8619
|
+
if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
|
|
8620
|
+
return String(value);
|
|
8621
|
+
}
|
|
8622
|
+
return void 0;
|
|
8623
|
+
}
|
|
8624
|
+
function patternDetector(options) {
|
|
8625
|
+
return {
|
|
8626
|
+
id: options.id,
|
|
8627
|
+
severity: options.severity ?? "warning",
|
|
8628
|
+
matchKind: "value",
|
|
8629
|
+
detect(input3) {
|
|
8630
|
+
if (typeof input3.value !== "string") return [];
|
|
8631
|
+
options.pattern.lastIndex = 0;
|
|
8632
|
+
return options.pattern.test(input3.value) ? [{ action: "replace", severity: options.severity ?? "warning", matchKind: "value" }] : [];
|
|
8633
|
+
}
|
|
8634
|
+
};
|
|
8635
|
+
}
|
|
8636
|
+
function digitsOnly(value) {
|
|
8637
|
+
return value.replace(/\D/g, "");
|
|
8638
|
+
}
|
|
8639
|
+
function passesLuhn(value) {
|
|
8640
|
+
const digits = digitsOnly(value);
|
|
8641
|
+
if (digits.length < 13 || digits.length > 19) return false;
|
|
8642
|
+
let sum = 0;
|
|
8643
|
+
let double = false;
|
|
8644
|
+
for (let i = digits.length - 1; i >= 0; i -= 1) {
|
|
8645
|
+
let digit = Number(digits[i]);
|
|
8646
|
+
if (double) {
|
|
8647
|
+
digit *= 2;
|
|
8648
|
+
if (digit > 9) digit -= 9;
|
|
8649
|
+
}
|
|
8650
|
+
sum += digit;
|
|
8651
|
+
double = !double;
|
|
8652
|
+
}
|
|
8653
|
+
return sum % 10 === 0;
|
|
8654
|
+
}
|
|
8655
|
+
var credentialDetectors = [
|
|
8656
|
+
patternDetector({
|
|
8657
|
+
id: "value.authorizationHeader",
|
|
8658
|
+
pattern: /^(?:basic|bearer|digest|apikey)\s+[a-z0-9._~+/=-]+$/i,
|
|
8659
|
+
severity: "error"
|
|
8660
|
+
}),
|
|
8661
|
+
patternDetector({
|
|
8662
|
+
id: "value.bearerToken",
|
|
8663
|
+
pattern: /\bbearer\s+[a-z0-9._~+/=-]{12,}\b/i,
|
|
8664
|
+
severity: "error"
|
|
8665
|
+
}),
|
|
8666
|
+
patternDetector({
|
|
8667
|
+
id: "value.cookie",
|
|
8668
|
+
pattern: /\b[a-z0-9_.-]+=[^;\s]+(?:;\s*[a-z0-9_.-]+=[^;\s]+)+/i,
|
|
8669
|
+
severity: "error"
|
|
8670
|
+
}),
|
|
8671
|
+
patternDetector({
|
|
8672
|
+
id: "value.jwt",
|
|
8673
|
+
pattern: /\beyJ[a-zA-Z0-9_-]{8,}\.[a-zA-Z0-9_-]{8,}\.[a-zA-Z0-9_-]{8,}\b/,
|
|
8674
|
+
severity: "error"
|
|
8675
|
+
}),
|
|
8676
|
+
patternDetector({
|
|
8677
|
+
id: "value.providerApiKey",
|
|
8678
|
+
pattern: /\b(?:sk-(?:proj-)?[a-zA-Z0-9_-]{16,}|sk-ant-[a-zA-Z0-9_-]{16,}|AIza[0-9A-Za-z_-]{20,})\b/,
|
|
8679
|
+
severity: "error"
|
|
8680
|
+
}),
|
|
8681
|
+
patternDetector({
|
|
8682
|
+
id: "value.githubToken",
|
|
8683
|
+
pattern: /\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9_]{20,}\b/,
|
|
8684
|
+
severity: "error"
|
|
8685
|
+
}),
|
|
8686
|
+
patternDetector({
|
|
8687
|
+
id: "value.awsAccessKey",
|
|
8688
|
+
pattern: /\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/,
|
|
8689
|
+
severity: "error"
|
|
8690
|
+
}),
|
|
8691
|
+
patternDetector({
|
|
8692
|
+
id: "value.privateKey",
|
|
8693
|
+
pattern: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*-----END [A-Z ]*PRIVATE KEY-----/,
|
|
8694
|
+
severity: "error"
|
|
8695
|
+
}),
|
|
8696
|
+
{
|
|
8697
|
+
id: "value.creditCard",
|
|
8698
|
+
severity: "error",
|
|
8699
|
+
matchKind: "value",
|
|
8700
|
+
detect(input3) {
|
|
8701
|
+
if (typeof input3.value !== "string") return [];
|
|
8702
|
+
const candidatePattern = /(?:\d[ -]?){13,19}/g;
|
|
8703
|
+
for (const match of input3.value.matchAll(candidatePattern)) {
|
|
8704
|
+
const candidate = match[0] ?? "";
|
|
8705
|
+
if (passesLuhn(candidate)) {
|
|
8706
|
+
return [{ action: "replace", severity: "error", matchKind: "value" }];
|
|
8707
|
+
}
|
|
8708
|
+
}
|
|
8709
|
+
return [];
|
|
8710
|
+
}
|
|
8711
|
+
}
|
|
8712
|
+
];
|
|
8713
|
+
var identifierDetectors = [
|
|
8714
|
+
patternDetector({
|
|
8715
|
+
id: "value.email",
|
|
8716
|
+
pattern: /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/i
|
|
8717
|
+
}),
|
|
8718
|
+
patternDetector({
|
|
8719
|
+
id: "value.phone",
|
|
8720
|
+
pattern: /\b(?:\+\d{1,3}[\s.-]?)?(?:\(?\d{3}\)?[\s.-])\d{3}[\s.-]\d{4}\b/
|
|
8721
|
+
}),
|
|
8722
|
+
patternDetector({
|
|
8723
|
+
id: "value.ipv4",
|
|
8724
|
+
pattern: /\b(?:(?:25[0-5]|2[0-4]\d|1?\d?\d)\.){3}(?:25[0-5]|2[0-4]\d|1?\d?\d)\b/
|
|
8725
|
+
}),
|
|
8726
|
+
patternDetector({
|
|
8727
|
+
id: "value.ipv6",
|
|
8728
|
+
pattern: /\b(?:[0-9a-f]{1,4}:){2,7}[0-9a-f]{1,4}\b/i
|
|
8729
|
+
})
|
|
8730
|
+
];
|
|
8731
|
+
function builtInDetectorsForProfile(profile) {
|
|
8732
|
+
if (profile === "local") return credentialDetectors;
|
|
8733
|
+
return [...credentialDetectors, ...identifierDetectors];
|
|
8734
|
+
}
|
|
8735
|
+
function compileRules2(rules, extraKeys) {
|
|
8736
|
+
const out = /* @__PURE__ */ new Map();
|
|
8737
|
+
const set = (rule) => {
|
|
8738
|
+
const key = toKey2(rule.key);
|
|
8739
|
+
out.set(key, { ...rule, key });
|
|
8740
|
+
};
|
|
8741
|
+
for (const key of DEFAULT_REDACT_KEYS2) {
|
|
8742
|
+
set({ key, strategy: "full" });
|
|
8743
|
+
}
|
|
8744
|
+
for (const key of extraKeys ?? []) {
|
|
8745
|
+
if (typeof key === "string" && key.length > 0) {
|
|
8746
|
+
set({ key, strategy: "full" });
|
|
8747
|
+
}
|
|
8748
|
+
}
|
|
8749
|
+
for (const rule of rules ?? []) {
|
|
8750
|
+
if (typeof rule === "string") {
|
|
8751
|
+
set({ key: rule, strategy: "full" });
|
|
8752
|
+
continue;
|
|
8753
|
+
}
|
|
8754
|
+
if (rule.strategy === "full") set({ key: rule.key, strategy: "full" });
|
|
8755
|
+
if (rule.strategy === "hash") set({ key: rule.key, strategy: "hash" });
|
|
8756
|
+
if (rule.strategy === "prefix") {
|
|
8757
|
+
set({
|
|
8758
|
+
key: rule.key,
|
|
8759
|
+
strategy: "prefix",
|
|
8760
|
+
keep: typeof rule.keep === "number" ? rule.keep : 8
|
|
8761
|
+
});
|
|
8762
|
+
}
|
|
8763
|
+
}
|
|
8764
|
+
return [...out.values()];
|
|
8765
|
+
}
|
|
8766
|
+
function actionForRule(rule) {
|
|
8767
|
+
if (rule.strategy === "full") return "replace";
|
|
8768
|
+
return rule.strategy;
|
|
8769
|
+
}
|
|
8770
|
+
function applyRule(rule, value, replacement) {
|
|
8771
|
+
if (rule.strategy === "full") return replacement;
|
|
8772
|
+
const asString = stringifyScalar(value);
|
|
8773
|
+
if (rule.strategy === "prefix") {
|
|
8774
|
+
if (asString === void 0) return replacement;
|
|
8775
|
+
const keep = Math.max(0, Math.floor(rule.keep));
|
|
8776
|
+
return asString.length <= keep ? `${asString}\u2026` : `${asString.slice(0, keep)}\u2026`;
|
|
8777
|
+
}
|
|
8778
|
+
if (rule.strategy === "hash") {
|
|
8779
|
+
if (asString === void 0) return "[HASH:unknown]";
|
|
8780
|
+
return `[HASH:${stableHash2(asString)}]`;
|
|
8781
|
+
}
|
|
8782
|
+
return value;
|
|
8783
|
+
}
|
|
8784
|
+
function childPath(path15, key) {
|
|
8785
|
+
if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key)) {
|
|
8786
|
+
return path15 ? `${path15}.${key}` : key;
|
|
8787
|
+
}
|
|
8788
|
+
return `${path15 || "$"}[${JSON.stringify(key)}]`;
|
|
8789
|
+
}
|
|
8790
|
+
function indexPath(path15, index) {
|
|
8791
|
+
return `${path15 || "$"}[${index}]`;
|
|
8792
|
+
}
|
|
8793
|
+
function makeFinding(path15, detector, action, matchKind, severity = "warning", preview) {
|
|
8794
|
+
return preview === void 0 ? { path: path15, detector, action, severity, matchKind } : { path: path15, detector, action, severity, matchKind, preview };
|
|
8795
|
+
}
|
|
8796
|
+
function createRedactionProfile(profile = "local") {
|
|
8797
|
+
switch (profile) {
|
|
8798
|
+
case "local":
|
|
8799
|
+
return { profile: "local", extraKeys: [] };
|
|
8800
|
+
case "share":
|
|
8801
|
+
return {
|
|
8802
|
+
profile: "share",
|
|
8803
|
+
extraKeys: SHARE_PROFILE_EXTRA_KEYS2,
|
|
8804
|
+
maxMetadataValueLengthCap: 500,
|
|
8805
|
+
maxPreviewLengthCap: 200
|
|
8806
|
+
};
|
|
8807
|
+
case "strict":
|
|
8808
|
+
return {
|
|
8809
|
+
profile: "strict",
|
|
8810
|
+
extraKeys: [...SHARE_PROFILE_EXTRA_KEYS2, ...STRICT_PROFILE_EXTRA_KEYS2],
|
|
8811
|
+
maxMetadataValueLengthCap: 200,
|
|
8812
|
+
maxPreviewLengthCap: 80
|
|
8813
|
+
};
|
|
8814
|
+
}
|
|
8815
|
+
}
|
|
8816
|
+
var Redactor2 = class {
|
|
8817
|
+
#rules;
|
|
8818
|
+
#detectors;
|
|
8819
|
+
#profile;
|
|
8820
|
+
#replacement;
|
|
8821
|
+
#maxDepth;
|
|
8822
|
+
#collectFindings;
|
|
8823
|
+
constructor(options) {
|
|
8824
|
+
const resolved = createRedactionProfile(options?.profile ?? "local");
|
|
8825
|
+
this.#profile = resolved.profile;
|
|
8826
|
+
this.#rules = compileRules2(options?.rules, [
|
|
8827
|
+
...resolved.extraKeys,
|
|
8828
|
+
...options?.extraKeys ?? []
|
|
8829
|
+
]);
|
|
8830
|
+
this.#detectors = [
|
|
8831
|
+
...builtInDetectorsForProfile(this.#profile),
|
|
8832
|
+
...options?.detectors ?? []
|
|
8833
|
+
];
|
|
8834
|
+
this.#replacement = options?.replacement ?? "[REDACTED]";
|
|
8835
|
+
this.#maxDepth = options?.maxDepth ?? 32;
|
|
8836
|
+
this.#collectFindings = options?.collectFindings ?? true;
|
|
8837
|
+
}
|
|
8838
|
+
redactValue(key, value) {
|
|
8839
|
+
return this.#redactValue(value, key, key, 0, {
|
|
8840
|
+
findings: [],
|
|
8841
|
+
seen: /* @__PURE__ */ new WeakMap()
|
|
8842
|
+
});
|
|
8843
|
+
}
|
|
8844
|
+
redactRecord(record) {
|
|
8845
|
+
return this.redact(record).value;
|
|
8846
|
+
}
|
|
8847
|
+
redact(value) {
|
|
8848
|
+
const state = {
|
|
8849
|
+
findings: [],
|
|
8850
|
+
seen: /* @__PURE__ */ new WeakMap()
|
|
8851
|
+
};
|
|
8852
|
+
const redacted = this.#redactValue(value, void 0, "$", 0, state);
|
|
8853
|
+
return {
|
|
8854
|
+
value: redacted,
|
|
8855
|
+
findings: state.findings,
|
|
8856
|
+
redacted: state.findings.some((finding) => finding.action !== "keep"),
|
|
8857
|
+
profile: this.#profile
|
|
8858
|
+
};
|
|
8859
|
+
}
|
|
8860
|
+
#recordFinding(state, finding) {
|
|
8861
|
+
if (this.#collectFindings) state.findings.push(finding);
|
|
8862
|
+
}
|
|
8863
|
+
#redactValue(value, key, path15, depth, state) {
|
|
8864
|
+
if (depth > this.#maxDepth) {
|
|
8865
|
+
this.#recordFinding(
|
|
8866
|
+
state,
|
|
8867
|
+
makeFinding(path15, "structure.maxDepth", "truncate", "value", "warning")
|
|
8868
|
+
);
|
|
8869
|
+
return "[Truncated]";
|
|
8870
|
+
}
|
|
8871
|
+
if (key !== void 0) {
|
|
8872
|
+
const rule = this.#rules.find((candidate) => candidate.key === toKey2(key));
|
|
8873
|
+
if (rule) {
|
|
8874
|
+
this.#recordFinding(
|
|
8875
|
+
state,
|
|
8876
|
+
makeFinding(path15, `key.${rule.key}`, actionForRule(rule), "key", "warning")
|
|
8877
|
+
);
|
|
8878
|
+
return applyRule(rule, value, this.#replacement);
|
|
8879
|
+
}
|
|
8880
|
+
}
|
|
8881
|
+
for (const detector of this.#detectors) {
|
|
8882
|
+
const detections = detector.detect({ path: path15, key, value });
|
|
8883
|
+
for (const detection of detections) {
|
|
8884
|
+
const action = detection.action ?? "replace";
|
|
8885
|
+
this.#recordFinding(
|
|
8886
|
+
state,
|
|
8887
|
+
makeFinding(
|
|
8888
|
+
path15,
|
|
8889
|
+
detector.id,
|
|
8890
|
+
action,
|
|
8891
|
+
detection.matchKind ?? detector.matchKind ?? "custom",
|
|
8892
|
+
detection.severity ?? detector.severity ?? "warning",
|
|
8893
|
+
detection.preview
|
|
8894
|
+
)
|
|
8895
|
+
);
|
|
8896
|
+
if (action !== "keep") {
|
|
8897
|
+
return detection.replacement ?? this.#replacement;
|
|
8898
|
+
}
|
|
8899
|
+
}
|
|
8900
|
+
}
|
|
8901
|
+
if (Array.isArray(value)) {
|
|
8902
|
+
if (state.seen.has(value)) return state.seen.get(value);
|
|
8903
|
+
const out = [];
|
|
8904
|
+
state.seen.set(value, out);
|
|
8905
|
+
value.forEach((item, index) => {
|
|
8906
|
+
out[index] = this.#redactValue(item, void 0, indexPath(path15, index), depth + 1, state);
|
|
8907
|
+
});
|
|
8908
|
+
return out;
|
|
8909
|
+
}
|
|
8910
|
+
if (isRecord13(value)) {
|
|
8911
|
+
if (state.seen.has(value)) return state.seen.get(value);
|
|
8912
|
+
const out = {};
|
|
8913
|
+
state.seen.set(value, out);
|
|
8914
|
+
for (const [entryKey, entryValue] of Object.entries(value)) {
|
|
8915
|
+
out[entryKey] = this.#redactValue(
|
|
8916
|
+
entryValue,
|
|
8917
|
+
entryKey,
|
|
8918
|
+
childPath(path15 === "$" ? "" : path15, entryKey),
|
|
8919
|
+
depth + 1,
|
|
8920
|
+
state
|
|
8921
|
+
);
|
|
8922
|
+
}
|
|
8923
|
+
return out;
|
|
8924
|
+
}
|
|
8925
|
+
return value;
|
|
8926
|
+
}
|
|
8927
|
+
};
|
|
8928
|
+
function createRedactor(options) {
|
|
8929
|
+
return new Redactor2(options);
|
|
8930
|
+
}
|
|
8931
|
+
function redact(value, options) {
|
|
8932
|
+
return createRedactor(options).redact(value);
|
|
8933
|
+
}
|
|
8557
8934
|
async function readStdin(stdin) {
|
|
8558
8935
|
stdin.setEncoding("utf8");
|
|
8559
8936
|
let content = "";
|
|
@@ -8582,8 +8959,119 @@ async function inputFromTarget(target, options, stdin) {
|
|
|
8582
8959
|
return { type: "file", path: runPath };
|
|
8583
8960
|
}
|
|
8584
8961
|
|
|
8585
|
-
// packages/cli/src/
|
|
8962
|
+
// packages/cli/src/redact.ts
|
|
8586
8963
|
function parseRedactionProfile3(value) {
|
|
8964
|
+
if (value === void 0 || value === "local" || value === "share" || value === "strict") {
|
|
8965
|
+
return value ?? "share";
|
|
8966
|
+
}
|
|
8967
|
+
throw new Error(`Unsupported --profile "${value}". Use local, share, or strict.`);
|
|
8968
|
+
}
|
|
8969
|
+
function stable(value) {
|
|
8970
|
+
if (Array.isArray(value)) return value.map(stable);
|
|
8971
|
+
if (value === null || typeof value !== "object") return value;
|
|
8972
|
+
const record = value;
|
|
8973
|
+
return Object.fromEntries(
|
|
8974
|
+
Object.keys(record).sort((a, b) => a.localeCompare(b)).map((key) => [key, stable(record[key])])
|
|
8975
|
+
);
|
|
8976
|
+
}
|
|
8977
|
+
function isMissingFileError3(error) {
|
|
8978
|
+
return error !== null && typeof error === "object" && "code" in error && error.code === "ENOENT";
|
|
8979
|
+
}
|
|
8980
|
+
async function contentFromTarget(target, options, stdin) {
|
|
8981
|
+
if (target === "-") {
|
|
8982
|
+
return { content: await readStdin(stdin), source: "stdin" };
|
|
8983
|
+
}
|
|
8984
|
+
try {
|
|
8985
|
+
const stats2 = await stat(target);
|
|
8986
|
+
if (stats2.isDirectory()) {
|
|
8987
|
+
throw new Error("redact requires a trace file, JSON file, stdin, or run id.");
|
|
8988
|
+
}
|
|
8989
|
+
return { content: await readFile(target, "utf-8"), source: target };
|
|
8990
|
+
} catch (error) {
|
|
8991
|
+
if (!isMissingFileError3(error)) throw error;
|
|
8992
|
+
}
|
|
8993
|
+
const runPath = getTraceFilePath(target, resolveTraceDir({ dir: options.dir }));
|
|
8994
|
+
const stats = await stat(runPath);
|
|
8995
|
+
if (stats.isDirectory()) {
|
|
8996
|
+
throw new Error("redact requires a trace file, JSON file, stdin, or run id.");
|
|
8997
|
+
}
|
|
8998
|
+
return { content: await readFile(runPath, "utf-8"), source: runPath };
|
|
8999
|
+
}
|
|
9000
|
+
function redactJsonText(content, profile) {
|
|
9001
|
+
const parsed = JSON.parse(content);
|
|
9002
|
+
const result = redact(parsed, { profile });
|
|
9003
|
+
return {
|
|
9004
|
+
content: `${JSON.stringify(result.value, null, 2)}
|
|
9005
|
+
`,
|
|
9006
|
+
findings: result.findings
|
|
9007
|
+
};
|
|
9008
|
+
}
|
|
9009
|
+
function redactJsonlText(content, profile) {
|
|
9010
|
+
const lines = content.split(/\r?\n/);
|
|
9011
|
+
const out = [];
|
|
9012
|
+
const findings = [];
|
|
9013
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
9014
|
+
const line = lines[index] ?? "";
|
|
9015
|
+
if (line.trim() === "") continue;
|
|
9016
|
+
let parsed;
|
|
9017
|
+
try {
|
|
9018
|
+
parsed = JSON.parse(line);
|
|
9019
|
+
} catch {
|
|
9020
|
+
throw new Error(`Input is not valid JSON or JSONL at line ${index + 1}.`);
|
|
9021
|
+
}
|
|
9022
|
+
const result = redact(parsed, { profile });
|
|
9023
|
+
out.push(JSON.stringify(result.value));
|
|
9024
|
+
findings.push(
|
|
9025
|
+
...result.findings.map((finding) => ({
|
|
9026
|
+
...finding,
|
|
9027
|
+
path: `line:${index + 1}:${finding.path}`
|
|
9028
|
+
}))
|
|
9029
|
+
);
|
|
9030
|
+
}
|
|
9031
|
+
return {
|
|
9032
|
+
content: out.length === 0 ? "" : `${out.join("\n")}
|
|
9033
|
+
`,
|
|
9034
|
+
findings
|
|
9035
|
+
};
|
|
9036
|
+
}
|
|
9037
|
+
function redactDocument(content, profile) {
|
|
9038
|
+
try {
|
|
9039
|
+
return redactJsonText(content, profile);
|
|
9040
|
+
} catch {
|
|
9041
|
+
return redactJsonlText(content, profile);
|
|
9042
|
+
}
|
|
9043
|
+
}
|
|
9044
|
+
async function redactCommand(target, options = {}, stdin = process.stdin) {
|
|
9045
|
+
const profile = parseRedactionProfile3(options.profile);
|
|
9046
|
+
const source = await contentFromTarget(target, options, stdin);
|
|
9047
|
+
const redacted = redactDocument(source.content, profile);
|
|
9048
|
+
if (options.output !== void 0) {
|
|
9049
|
+
await writeFile(options.output, redacted.content, "utf-8");
|
|
9050
|
+
}
|
|
9051
|
+
if (options.json) {
|
|
9052
|
+
console.log(
|
|
9053
|
+
JSON.stringify(
|
|
9054
|
+
stable({
|
|
9055
|
+
ok: true,
|
|
9056
|
+
profile,
|
|
9057
|
+
source: source.source,
|
|
9058
|
+
output: options.output,
|
|
9059
|
+
findings: redacted.findings,
|
|
9060
|
+
content: options.output === void 0 ? redacted.content : void 0
|
|
9061
|
+
}),
|
|
9062
|
+
null,
|
|
9063
|
+
2
|
|
9064
|
+
)
|
|
9065
|
+
);
|
|
9066
|
+
return;
|
|
9067
|
+
}
|
|
9068
|
+
if (options.output === void 0) {
|
|
9069
|
+
process.stdout.write(redacted.content);
|
|
9070
|
+
}
|
|
9071
|
+
}
|
|
9072
|
+
|
|
9073
|
+
// packages/cli/src/explain.ts
|
|
9074
|
+
function parseRedactionProfile4(value) {
|
|
8587
9075
|
const profile = (value ?? "local").trim().toLowerCase();
|
|
8588
9076
|
if (profile === "local" || profile === "share" || profile === "strict") {
|
|
8589
9077
|
return profile;
|
|
@@ -8654,7 +9142,7 @@ async function explainCommand(target, options = {}, stdin = process.stdin) {
|
|
|
8654
9142
|
}
|
|
8655
9143
|
let redactionProfile;
|
|
8656
9144
|
try {
|
|
8657
|
-
redactionProfile =
|
|
9145
|
+
redactionProfile = parseRedactionProfile4(options.redactionProfile);
|
|
8658
9146
|
} catch (error) {
|
|
8659
9147
|
process.exitCode = 1;
|
|
8660
9148
|
console.error(error instanceof Error ? error.message : String(error));
|
|
@@ -8852,7 +9340,7 @@ async function openCommand(input3, options = {}, stdin = process.stdin) {
|
|
|
8852
9340
|
}
|
|
8853
9341
|
}
|
|
8854
9342
|
}
|
|
8855
|
-
function
|
|
9343
|
+
function isRecord14(value) {
|
|
8856
9344
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
8857
9345
|
}
|
|
8858
9346
|
function parseTarget(value) {
|
|
@@ -8861,7 +9349,7 @@ function parseTarget(value) {
|
|
|
8861
9349
|
throw new Error('Unsupported migration target. Use "--to 1.0".');
|
|
8862
9350
|
}
|
|
8863
9351
|
function formatOf(value) {
|
|
8864
|
-
if (!
|
|
9352
|
+
if (!isRecord14(value)) return "unknown";
|
|
8865
9353
|
if (value.schemaVersion === "0.1") return "0.1";
|
|
8866
9354
|
if (value.schemaVersion === "0.2") return "0.2";
|
|
8867
9355
|
if (value.schemaVersion === "1.0") return "1.0";
|
|
@@ -8874,14 +9362,14 @@ function uniqueSorted(values) {
|
|
|
8874
9362
|
return [...new Set(values)].sort();
|
|
8875
9363
|
}
|
|
8876
9364
|
function isWithinDirectory(child, parent) {
|
|
8877
|
-
const relative =
|
|
8878
|
-
return relative === "" || !relative.startsWith("..") && !
|
|
9365
|
+
const relative = path13.relative(parent, child);
|
|
9366
|
+
return relative === "" || !relative.startsWith("..") && !path13.isAbsolute(relative);
|
|
8879
9367
|
}
|
|
8880
9368
|
async function resolveOutputPath(inputPath, output2, force) {
|
|
8881
9369
|
if (output2 === void 0 || output2.trim() === "") return void 0;
|
|
8882
|
-
const inputAbs =
|
|
8883
|
-
const outputAbs =
|
|
8884
|
-
const inputDir =
|
|
9370
|
+
const inputAbs = path13.resolve(inputPath);
|
|
9371
|
+
const outputAbs = path13.resolve(output2.trim());
|
|
9372
|
+
const inputDir = path13.dirname(inputAbs);
|
|
8885
9373
|
if (!isWithinDirectory(outputAbs, inputDir)) {
|
|
8886
9374
|
throw new Error("Refusing to write migrated output outside the input directory.");
|
|
8887
9375
|
}
|
|
@@ -9002,7 +9490,7 @@ async function migrateCommand(input3, options = {}) {
|
|
|
9002
9490
|
process.exitCode = 1;
|
|
9003
9491
|
return;
|
|
9004
9492
|
}
|
|
9005
|
-
const inputPath =
|
|
9493
|
+
const inputPath = path13.resolve(input3.trim());
|
|
9006
9494
|
const dryRun = options.dryRun === true;
|
|
9007
9495
|
if (!dryRun && (options.output === void 0 || options.output.trim() === "")) {
|
|
9008
9496
|
console.error("migrate requires --dry-run or --output <path>.");
|
|
@@ -9021,7 +9509,7 @@ async function migrateCommand(input3, options = {}) {
|
|
|
9021
9509
|
);
|
|
9022
9510
|
const result = await buildMigration(inputPath, outputPath);
|
|
9023
9511
|
if (!dryRun && outputPath !== void 0) {
|
|
9024
|
-
await mkdir(
|
|
9512
|
+
await mkdir(path13.dirname(outputPath), { recursive: true });
|
|
9025
9513
|
await writeFile(outputPath, result.content, "utf-8");
|
|
9026
9514
|
}
|
|
9027
9515
|
printSummary2(result, dryRun);
|
|
@@ -9299,7 +9787,7 @@ function stripPrefix(name, prefixes) {
|
|
|
9299
9787
|
}
|
|
9300
9788
|
return name;
|
|
9301
9789
|
}
|
|
9302
|
-
function eventEvidence(event,
|
|
9790
|
+
function eventEvidence(event, path15) {
|
|
9303
9791
|
return {
|
|
9304
9792
|
runId: event.runId,
|
|
9305
9793
|
eventId: event.eventId,
|
|
@@ -9309,7 +9797,7 @@ function eventEvidence(event, path12) {
|
|
|
9309
9797
|
kind: event.kind,
|
|
9310
9798
|
name: event.name,
|
|
9311
9799
|
status: event.status,
|
|
9312
|
-
...
|
|
9800
|
+
...path15 ? { path: path15 } : {}
|
|
9313
9801
|
};
|
|
9314
9802
|
}
|
|
9315
9803
|
function runEvidence(run) {
|
|
@@ -9346,7 +9834,7 @@ function finishedEvents(context, kind) {
|
|
|
9346
9834
|
(event) => (kind === void 0 || event.kind === kind) && event.status !== "running"
|
|
9347
9835
|
);
|
|
9348
9836
|
}
|
|
9349
|
-
function
|
|
9837
|
+
function isRecord15(value) {
|
|
9350
9838
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
9351
9839
|
}
|
|
9352
9840
|
function eventMap(events) {
|
|
@@ -9372,9 +9860,9 @@ function eventEndMs(event) {
|
|
|
9372
9860
|
function normalizedKey(value) {
|
|
9373
9861
|
return value.toLowerCase().replace(/[^a-z0-9_]/g, "");
|
|
9374
9862
|
}
|
|
9375
|
-
function lastPathSegment(
|
|
9376
|
-
const parts =
|
|
9377
|
-
return parts[parts.length - 1] ??
|
|
9863
|
+
function lastPathSegment(path15) {
|
|
9864
|
+
const parts = path15.split(".");
|
|
9865
|
+
return parts[parts.length - 1] ?? path15;
|
|
9378
9866
|
}
|
|
9379
9867
|
function valueType(value) {
|
|
9380
9868
|
if (Array.isArray(value)) return "array";
|
|
@@ -9388,22 +9876,22 @@ function serializedByteLength(value) {
|
|
|
9388
9876
|
return void 0;
|
|
9389
9877
|
}
|
|
9390
9878
|
}
|
|
9391
|
-
function pushValueEntries(entries, event, value,
|
|
9392
|
-
entries.push({ event, path:
|
|
9879
|
+
function pushValueEntries(entries, event, value, path15, key, depth = 0) {
|
|
9880
|
+
entries.push({ event, path: path15, key, value });
|
|
9393
9881
|
if (depth >= 8) return;
|
|
9394
9882
|
if (Array.isArray(value)) {
|
|
9395
9883
|
for (const [index, item] of value.entries()) {
|
|
9396
|
-
pushValueEntries(entries, event, item, `${
|
|
9884
|
+
pushValueEntries(entries, event, item, `${path15}.${index}`, String(index), depth + 1);
|
|
9397
9885
|
}
|
|
9398
9886
|
return;
|
|
9399
9887
|
}
|
|
9400
|
-
if (!
|
|
9888
|
+
if (!isRecord15(value)) return;
|
|
9401
9889
|
for (const nestedKey of Object.keys(value).sort((a, b) => a.localeCompare(b))) {
|
|
9402
9890
|
pushValueEntries(
|
|
9403
9891
|
entries,
|
|
9404
9892
|
event,
|
|
9405
9893
|
value[nestedKey],
|
|
9406
|
-
`${
|
|
9894
|
+
`${path15}.${nestedKey}`,
|
|
9407
9895
|
nestedKey,
|
|
9408
9896
|
depth + 1
|
|
9409
9897
|
);
|
|
@@ -9484,9 +9972,9 @@ function eventDurationMs(event) {
|
|
|
9484
9972
|
}
|
|
9485
9973
|
function treeShape(nodes) {
|
|
9486
9974
|
const lines = [];
|
|
9487
|
-
const visit = (node,
|
|
9488
|
-
lines.push(`${
|
|
9489
|
-
node.children.forEach((child, index) => visit(child, `${
|
|
9975
|
+
const visit = (node, path15) => {
|
|
9976
|
+
lines.push(`${path15}:${node.event.kind}:${node.event.name}:${node.event.status ?? "unknown"}`);
|
|
9977
|
+
node.children.forEach((child, index) => visit(child, `${path15}.${index}`));
|
|
9490
9978
|
};
|
|
9491
9979
|
nodes.forEach((node, index) => visit(node, String(index)));
|
|
9492
9980
|
return lines;
|
|
@@ -9535,9 +10023,9 @@ function retrievalShape(context) {
|
|
|
9535
10023
|
function guardrailShape(context) {
|
|
9536
10024
|
return guardrailEvents(context).map((event) => signalName(event, ["guardrailName", "guardrail", "guardrailId"], ["guardrail:"])).sort((a, b) => a.localeCompare(b));
|
|
9537
10025
|
}
|
|
9538
|
-
function firstEvidenceForKind(context, kind,
|
|
10026
|
+
function firstEvidenceForKind(context, kind, path15) {
|
|
9539
10027
|
const event = context.events.find((candidate) => candidate.kind === kind);
|
|
9540
|
-
return event ? [eventEvidence(event,
|
|
10028
|
+
return event ? [eventEvidence(event, path15)] : runEvidence(context.selectedRun);
|
|
9541
10029
|
}
|
|
9542
10030
|
function baselineDiffFinding(message, evidence, expected, actual) {
|
|
9543
10031
|
return failFinding("baseline.regression", message, evidence, expected, actual);
|
|
@@ -9785,13 +10273,13 @@ function createStructureCycleRule() {
|
|
|
9785
10273
|
const seenCycles = /* @__PURE__ */ new Set();
|
|
9786
10274
|
const findings = [];
|
|
9787
10275
|
for (const event of [...context.events].sort((a, b) => a.eventId.localeCompare(b.eventId))) {
|
|
9788
|
-
const
|
|
10276
|
+
const path15 = [];
|
|
9789
10277
|
const seenAt = /* @__PURE__ */ new Map();
|
|
9790
10278
|
let current = event;
|
|
9791
10279
|
while (current) {
|
|
9792
10280
|
const existing = seenAt.get(current.eventId);
|
|
9793
10281
|
if (existing !== void 0) {
|
|
9794
|
-
const cycle =
|
|
10282
|
+
const cycle = path15.slice(existing);
|
|
9795
10283
|
const key = cycle.map((item) => item.eventId).sort().join("\0");
|
|
9796
10284
|
if (!seenCycles.has(key)) {
|
|
9797
10285
|
seenCycles.add(key);
|
|
@@ -9807,8 +10295,8 @@ function createStructureCycleRule() {
|
|
|
9807
10295
|
}
|
|
9808
10296
|
break;
|
|
9809
10297
|
}
|
|
9810
|
-
seenAt.set(current.eventId,
|
|
9811
|
-
|
|
10298
|
+
seenAt.set(current.eventId, path15.length);
|
|
10299
|
+
path15.push(current);
|
|
9812
10300
|
current = current.parentId ? byId.get(current.parentId) : void 0;
|
|
9813
10301
|
}
|
|
9814
10302
|
}
|
|
@@ -10082,7 +10570,7 @@ function createSafetyOversizedAttributeRule(options) {
|
|
|
10082
10570
|
)
|
|
10083
10571
|
);
|
|
10084
10572
|
}
|
|
10085
|
-
if (
|
|
10573
|
+
if (isRecord15(entry.value) && options.maxObjectKeys !== void 0 && Object.keys(entry.value).length > options.maxObjectKeys) {
|
|
10086
10574
|
findings.push(
|
|
10087
10575
|
failFinding(
|
|
10088
10576
|
"safety.oversizedAttribute",
|
|
@@ -10333,7 +10821,7 @@ function asConfig(value) {
|
|
|
10333
10821
|
}
|
|
10334
10822
|
async function loadConfig(configPath) {
|
|
10335
10823
|
if (configPath === void 0) return {};
|
|
10336
|
-
const extension =
|
|
10824
|
+
const extension = path13.extname(configPath);
|
|
10337
10825
|
if (TS_CONFIG_EXTENSIONS.has(extension)) {
|
|
10338
10826
|
throw new Error(
|
|
10339
10827
|
"TypeScript check configs require an explicit precompiled JavaScript config or future --config-loader support."
|
|
@@ -10342,7 +10830,7 @@ async function loadConfig(configPath) {
|
|
|
10342
10830
|
if (!CONFIG_EXTENSIONS.has(extension)) {
|
|
10343
10831
|
throw new Error("Unsupported check config extension. Use .json, .js, .mjs, or .cjs.");
|
|
10344
10832
|
}
|
|
10345
|
-
const absolute =
|
|
10833
|
+
const absolute = path13.resolve(configPath);
|
|
10346
10834
|
if (extension === ".json") {
|
|
10347
10835
|
const raw = await readFile(absolute, "utf-8");
|
|
10348
10836
|
return asConfig(JSON.parse(raw));
|
|
@@ -10359,12 +10847,12 @@ function normalizeConfig(config) {
|
|
|
10359
10847
|
}
|
|
10360
10848
|
function buildRules(config, options) {
|
|
10361
10849
|
const diagnostics = [];
|
|
10362
|
-
const
|
|
10363
|
-
const run =
|
|
10364
|
-
const tool =
|
|
10365
|
-
const llm =
|
|
10366
|
-
const structure =
|
|
10367
|
-
const safety =
|
|
10850
|
+
const checks2 = normalizeConfig(config);
|
|
10851
|
+
const run = checks2.run ?? {};
|
|
10852
|
+
const tool = checks2.tool ?? {};
|
|
10853
|
+
const llm = checks2.llm ?? {};
|
|
10854
|
+
const structure = checks2.structure ?? {};
|
|
10855
|
+
const safety = checks2.safety ?? {};
|
|
10368
10856
|
const maxDurationMs = parseNumber(options.maxDurationMs, "--max-duration-ms") ?? run.maxDurationMs;
|
|
10369
10857
|
const maxTotalTokens = parseNumber(options.maxTotalTokens, "--max-total-tokens") ?? llm.maxTotalTokens;
|
|
10370
10858
|
const rules = [
|
|
@@ -10412,7 +10900,7 @@ function buildRules(config, options) {
|
|
|
10412
10900
|
rules.push(createSafetyOversizedAttributeRule(safety));
|
|
10413
10901
|
}
|
|
10414
10902
|
const select = [
|
|
10415
|
-
...asStringArray(
|
|
10903
|
+
...asStringArray(checks2.select) ?? [],
|
|
10416
10904
|
...options.rule ?? []
|
|
10417
10905
|
];
|
|
10418
10906
|
return {
|
|
@@ -10442,16 +10930,16 @@ function exitCodeFor(result) {
|
|
|
10442
10930
|
}
|
|
10443
10931
|
return 1;
|
|
10444
10932
|
}
|
|
10445
|
-
function
|
|
10446
|
-
if (Array.isArray(value)) return value.map(
|
|
10933
|
+
function stable2(value) {
|
|
10934
|
+
if (Array.isArray(value)) return value.map(stable2);
|
|
10447
10935
|
if (value === null || typeof value !== "object") return value;
|
|
10448
10936
|
const record = value;
|
|
10449
10937
|
return Object.fromEntries(
|
|
10450
|
-
Object.keys(record).sort((a, b) => a.localeCompare(b)).map((key) => [key,
|
|
10938
|
+
Object.keys(record).sort((a, b) => a.localeCompare(b)).map((key) => [key, stable2(record[key])])
|
|
10451
10939
|
);
|
|
10452
10940
|
}
|
|
10453
10941
|
function printJson(result) {
|
|
10454
|
-
console.log(JSON.stringify(
|
|
10942
|
+
console.log(JSON.stringify(stable2(result), null, 2));
|
|
10455
10943
|
}
|
|
10456
10944
|
function printHuman(result) {
|
|
10457
10945
|
console.log(`Check status: ${result.status}`);
|
|
@@ -10460,12 +10948,12 @@ function printHuman(result) {
|
|
|
10460
10948
|
console.log(
|
|
10461
10949
|
`Summary: ${result.summary.failed} failed, ${result.summary.warnings} warning(s), ${result.summary.errors} error(s)`
|
|
10462
10950
|
);
|
|
10463
|
-
for (const
|
|
10464
|
-
console.log(`- ${
|
|
10951
|
+
for (const diagnostic4 of result.diagnostics) {
|
|
10952
|
+
console.log(`- ${diagnostic4.code}: ${diagnostic4.message}`);
|
|
10465
10953
|
}
|
|
10466
10954
|
for (const finding of result.findings) {
|
|
10467
|
-
const
|
|
10468
|
-
console.log(`- ${finding.ruleId}: ${finding.message}${
|
|
10955
|
+
const path15 = finding.evidence[0]?.path;
|
|
10956
|
+
console.log(`- ${finding.ruleId}: ${finding.message}${path15 ? ` (${path15})` : ""}`);
|
|
10469
10957
|
}
|
|
10470
10958
|
}
|
|
10471
10959
|
function readErrorResult(error) {
|
|
@@ -10519,42 +11007,996 @@ async function checkCommand(target, options = {}, stdin = process.stdin) {
|
|
|
10519
11007
|
else printHuman(result);
|
|
10520
11008
|
}
|
|
10521
11009
|
|
|
10522
|
-
// packages/
|
|
10523
|
-
|
|
10524
|
-
|
|
10525
|
-
|
|
10526
|
-
|
|
10527
|
-
|
|
10528
|
-
|
|
10529
|
-
|
|
10530
|
-
|
|
10531
|
-
|
|
10532
|
-
|
|
11010
|
+
// packages/eval/src/index.ts
|
|
11011
|
+
function isTraceReadResult(value) {
|
|
11012
|
+
return value !== null && typeof value === "object" && "runs" in value && "events" in value && "format" in value;
|
|
11013
|
+
}
|
|
11014
|
+
function traceInputFrom(value) {
|
|
11015
|
+
return { type: "file", path: value instanceof URL ? value.pathname : value };
|
|
11016
|
+
}
|
|
11017
|
+
async function resolveRead(input3, options) {
|
|
11018
|
+
try {
|
|
11019
|
+
if (typeof input3 === "string") {
|
|
11020
|
+
const read2 = await openTrace(traceInputFrom(input3), {
|
|
11021
|
+
...options.format !== void 0 && options.format !== "auto" ? { format: options.format } : {}
|
|
11022
|
+
});
|
|
11023
|
+
return { read: read2, runId: options.runId, diagnostics: [] };
|
|
11024
|
+
}
|
|
11025
|
+
if (isTraceReadResult(input3)) {
|
|
11026
|
+
return { read: input3, runId: options.runId, diagnostics: [] };
|
|
11027
|
+
}
|
|
11028
|
+
if (isTraceReadResult(input3.trace)) {
|
|
11029
|
+
return { read: input3.trace, runId: options.runId ?? input3.runId, diagnostics: [] };
|
|
11030
|
+
}
|
|
11031
|
+
const format = options.format ?? input3.format;
|
|
11032
|
+
const read = await openTrace(traceInputFrom(input3.trace), {
|
|
11033
|
+
...format !== void 0 && format !== "auto" ? { format } : {}
|
|
11034
|
+
});
|
|
11035
|
+
return { read, runId: options.runId ?? input3.runId, diagnostics: [] };
|
|
11036
|
+
} catch (error) {
|
|
11037
|
+
return { diagnostics: [diagnosticFromError(error)] };
|
|
10533
11038
|
}
|
|
10534
|
-
return parsed;
|
|
10535
11039
|
}
|
|
10536
|
-
function
|
|
10537
|
-
if (
|
|
10538
|
-
|
|
10539
|
-
|
|
10540
|
-
|
|
10541
|
-
|
|
10542
|
-
|
|
11040
|
+
function diagnosticFromError(error) {
|
|
11041
|
+
if (error instanceof TraceReadError) {
|
|
11042
|
+
const code = error.code === "unsupported_format" ? "AI_EVAL_UNSUPPORTED_FORMAT" : error.code === "ambiguous_format" ? "AI_EVAL_AMBIGUOUS_FORMAT" : "AI_EVAL_TRACE_UNREADABLE";
|
|
11043
|
+
return { code, severity: "error", message: error.message };
|
|
11044
|
+
}
|
|
11045
|
+
return {
|
|
11046
|
+
code: "AI_EVAL_TRACE_UNREADABLE",
|
|
11047
|
+
severity: "error",
|
|
11048
|
+
message: error instanceof Error ? error.message : String(error)
|
|
11049
|
+
};
|
|
10543
11050
|
}
|
|
10544
|
-
function
|
|
10545
|
-
return
|
|
11051
|
+
function flatten2(nodes) {
|
|
11052
|
+
return nodes.flatMap((node) => [node, ...flatten2(node.children)]);
|
|
10546
11053
|
}
|
|
10547
|
-
function
|
|
10548
|
-
|
|
10549
|
-
|
|
10550
|
-
|
|
10551
|
-
|
|
10552
|
-
|
|
10553
|
-
|
|
10554
|
-
|
|
10555
|
-
|
|
10556
|
-
|
|
10557
|
-
|
|
11054
|
+
function selectRun3(read, runId) {
|
|
11055
|
+
if (runId !== void 0) {
|
|
11056
|
+
const run = read.runs.find((candidate) => candidate.runId === runId);
|
|
11057
|
+
return run === void 0 ? {
|
|
11058
|
+
diagnostics: [
|
|
11059
|
+
{
|
|
11060
|
+
code: "AI_EVAL_RUN_SELECTION_REQUIRED",
|
|
11061
|
+
severity: "error",
|
|
11062
|
+
message: `Run not found: ${runId}.`
|
|
11063
|
+
}
|
|
11064
|
+
]
|
|
11065
|
+
} : { run, diagnostics: [] };
|
|
11066
|
+
}
|
|
11067
|
+
if (read.runs.length === 1) {
|
|
11068
|
+
return { run: read.runs[0], diagnostics: [] };
|
|
11069
|
+
}
|
|
11070
|
+
if (read.runs.length === 0) {
|
|
11071
|
+
return {
|
|
11072
|
+
diagnostics: [
|
|
11073
|
+
{
|
|
11074
|
+
code: "AI_EVAL_RUN_SELECTION_REQUIRED",
|
|
11075
|
+
severity: "error",
|
|
11076
|
+
message: "No runs are available for eval."
|
|
11077
|
+
}
|
|
11078
|
+
]
|
|
11079
|
+
};
|
|
11080
|
+
}
|
|
11081
|
+
return {
|
|
11082
|
+
diagnostics: [
|
|
11083
|
+
{
|
|
11084
|
+
code: "AI_EVAL_RUN_SELECTION_REQUIRED",
|
|
11085
|
+
severity: "error",
|
|
11086
|
+
message: "Multiple runs are available; select a run before eval."
|
|
11087
|
+
}
|
|
11088
|
+
]
|
|
11089
|
+
};
|
|
11090
|
+
}
|
|
11091
|
+
function errorResult3(format, diagnostics, runId) {
|
|
11092
|
+
const errors = diagnostics.filter((item) => item.severity === "error").length;
|
|
11093
|
+
return {
|
|
11094
|
+
ok: false,
|
|
11095
|
+
status: "error",
|
|
11096
|
+
format,
|
|
11097
|
+
...runId !== void 0 ? { runId } : {},
|
|
11098
|
+
summary: { passed: 0, failed: 0, warnings: 0, errors },
|
|
11099
|
+
findings: [],
|
|
11100
|
+
diagnostics: [...diagnostics]
|
|
11101
|
+
};
|
|
11102
|
+
}
|
|
11103
|
+
function normalizeFinding2(rule, finding) {
|
|
11104
|
+
return {
|
|
11105
|
+
...finding,
|
|
11106
|
+
ruleId: finding.ruleId || rule.id,
|
|
11107
|
+
severity: finding.severity ?? rule.severity ?? "error",
|
|
11108
|
+
evidence: [...finding.evidence]
|
|
11109
|
+
};
|
|
11110
|
+
}
|
|
11111
|
+
function compareFindings2(a, b) {
|
|
11112
|
+
const aEvidence = a.evidence[0];
|
|
11113
|
+
const bEvidence = b.evidence[0];
|
|
11114
|
+
return a.ruleId.localeCompare(b.ruleId) || (aEvidence?.runId ?? "").localeCompare(bEvidence?.runId ?? "") || (aEvidence?.eventId ?? "").localeCompare(bEvidence?.eventId ?? "") || (aEvidence?.path ?? "").localeCompare(bEvidence?.path ?? "") || a.message.localeCompare(b.message);
|
|
11115
|
+
}
|
|
11116
|
+
function summarize2(findings, ruleCount) {
|
|
11117
|
+
const failed = findings.filter((item) => item.status === "fail").length;
|
|
11118
|
+
const warnings = findings.filter((item) => item.status === "warning").length;
|
|
11119
|
+
const errors = findings.filter((item) => item.severity === "error").length;
|
|
11120
|
+
return {
|
|
11121
|
+
passed: Math.max(0, ruleCount - failed - warnings),
|
|
11122
|
+
failed,
|
|
11123
|
+
warnings,
|
|
11124
|
+
errors
|
|
11125
|
+
};
|
|
11126
|
+
}
|
|
11127
|
+
function defaultRules() {
|
|
11128
|
+
return [checks.requireSuccess()];
|
|
11129
|
+
}
|
|
11130
|
+
async function evalRun(input3, options = {}) {
|
|
11131
|
+
const resolved = await resolveRead(input3, options);
|
|
11132
|
+
if (resolved.read === void 0) {
|
|
11133
|
+
return errorResult3("unknown", resolved.diagnostics);
|
|
11134
|
+
}
|
|
11135
|
+
const selected = selectRun3(resolved.read, resolved.runId);
|
|
11136
|
+
if (selected.run === void 0) {
|
|
11137
|
+
return errorResult3(resolved.read.format, selected.diagnostics, resolved.runId);
|
|
11138
|
+
}
|
|
11139
|
+
const rules = [...options.checks ?? defaultRules()].sort(
|
|
11140
|
+
(a, b) => a.id.localeCompare(b.id)
|
|
11141
|
+
);
|
|
11142
|
+
const context = {
|
|
11143
|
+
format: resolved.read.format,
|
|
11144
|
+
run: selected.run,
|
|
11145
|
+
nodes: flatten2(selected.run.children),
|
|
11146
|
+
events: resolved.read.events.filter((event) => event.runId === selected.run?.runId)
|
|
11147
|
+
};
|
|
11148
|
+
const diagnostics = [];
|
|
11149
|
+
const findings = [];
|
|
11150
|
+
for (const rule of rules) {
|
|
11151
|
+
try {
|
|
11152
|
+
findings.push(...rule.evaluate(context).map((finding) => normalizeFinding2(rule, finding)));
|
|
11153
|
+
} catch (error) {
|
|
11154
|
+
diagnostics.push({
|
|
11155
|
+
code: "AI_EVAL_RULE_FAILED",
|
|
11156
|
+
severity: "error",
|
|
11157
|
+
ruleId: rule.id,
|
|
11158
|
+
message: `Eval rule ${rule.id} failed: ${error instanceof Error ? error.message : String(error)}`
|
|
11159
|
+
});
|
|
11160
|
+
}
|
|
11161
|
+
}
|
|
11162
|
+
if (diagnostics.length > 0) {
|
|
11163
|
+
return errorResult3(resolved.read.format, diagnostics, selected.run.runId);
|
|
11164
|
+
}
|
|
11165
|
+
const sortedFindings = findings.sort(compareFindings2);
|
|
11166
|
+
const summary = summarize2(sortedFindings, rules.length);
|
|
11167
|
+
const status = summary.failed > 0 ? "fail" : "pass";
|
|
11168
|
+
return {
|
|
11169
|
+
ok: status === "pass",
|
|
11170
|
+
status,
|
|
11171
|
+
format: resolved.read.format,
|
|
11172
|
+
runId: selected.run.runId,
|
|
11173
|
+
summary,
|
|
11174
|
+
findings: sortedFindings,
|
|
11175
|
+
diagnostics: []
|
|
11176
|
+
};
|
|
11177
|
+
}
|
|
11178
|
+
function evidenceForRun(run, path15) {
|
|
11179
|
+
return [{ runId: run.runId, ...path15 !== void 0 ? { path: path15 } : {} }];
|
|
11180
|
+
}
|
|
11181
|
+
function evidenceForEvent(event, path15) {
|
|
11182
|
+
return [
|
|
11183
|
+
{
|
|
11184
|
+
runId: event.runId,
|
|
11185
|
+
eventId: event.eventId,
|
|
11186
|
+
...event.parentId !== void 0 ? { parentId: event.parentId } : {},
|
|
11187
|
+
kind: event.kind,
|
|
11188
|
+
name: event.name,
|
|
11189
|
+
...path15 !== void 0 ? { path: path15 } : {}
|
|
11190
|
+
}
|
|
11191
|
+
];
|
|
11192
|
+
}
|
|
11193
|
+
function fail(ruleId, message, evidence, expected, actual) {
|
|
11194
|
+
return {
|
|
11195
|
+
ruleId,
|
|
11196
|
+
status: "fail",
|
|
11197
|
+
severity: "error",
|
|
11198
|
+
message,
|
|
11199
|
+
...expected !== void 0 ? { expected } : {},
|
|
11200
|
+
...actual !== void 0 ? { actual } : {},
|
|
11201
|
+
evidence
|
|
11202
|
+
};
|
|
11203
|
+
}
|
|
11204
|
+
function nodeNames(nodes, kind) {
|
|
11205
|
+
return nodes.filter((node) => node.event.kind === kind).map((node) => node.event.name).sort((a, b) => a.localeCompare(b));
|
|
11206
|
+
}
|
|
11207
|
+
function hasAttribute(node, key) {
|
|
11208
|
+
return node.event.attributes !== void 0 && Object.prototype.hasOwnProperty.call(node.event.attributes, key);
|
|
11209
|
+
}
|
|
11210
|
+
function numericAttribute(node, keys) {
|
|
11211
|
+
const attrs = node.event.attributes;
|
|
11212
|
+
if (attrs === void 0) return void 0;
|
|
11213
|
+
for (const key of keys) {
|
|
11214
|
+
const value = attrs[key];
|
|
11215
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
11216
|
+
}
|
|
11217
|
+
return void 0;
|
|
11218
|
+
}
|
|
11219
|
+
function totalTokenCount(events) {
|
|
11220
|
+
return events.reduce((total, event) => {
|
|
11221
|
+
const usage = event.tokenUsage;
|
|
11222
|
+
if (usage?.total !== void 0) return total + usage.total;
|
|
11223
|
+
return total + (usage?.input ?? 0) + (usage?.output ?? 0);
|
|
11224
|
+
}, 0);
|
|
11225
|
+
}
|
|
11226
|
+
function createRule(id, category, evaluate) {
|
|
11227
|
+
return { id, category, severity: "error", evaluate };
|
|
11228
|
+
}
|
|
11229
|
+
var DEFAULT_ANSWER_KEYS = [
|
|
11230
|
+
"answer",
|
|
11231
|
+
"finalAnswer",
|
|
11232
|
+
"final",
|
|
11233
|
+
"response",
|
|
11234
|
+
"result",
|
|
11235
|
+
"output",
|
|
11236
|
+
"outputPreview",
|
|
11237
|
+
"completion",
|
|
11238
|
+
"text"
|
|
11239
|
+
];
|
|
11240
|
+
var DEFAULT_CONTEXT_KEYS = [
|
|
11241
|
+
"context",
|
|
11242
|
+
"contexts",
|
|
11243
|
+
"document",
|
|
11244
|
+
"documents",
|
|
11245
|
+
"retrieved",
|
|
11246
|
+
"retrieval",
|
|
11247
|
+
"chunks",
|
|
11248
|
+
"chunk",
|
|
11249
|
+
"source",
|
|
11250
|
+
"sources",
|
|
11251
|
+
"sourceText"
|
|
11252
|
+
];
|
|
11253
|
+
var DEFAULT_CITATION_KEYS = [
|
|
11254
|
+
"citation",
|
|
11255
|
+
"citations",
|
|
11256
|
+
"reference",
|
|
11257
|
+
"references",
|
|
11258
|
+
"sourceId",
|
|
11259
|
+
"sourceIds",
|
|
11260
|
+
"source_id",
|
|
11261
|
+
"source_ids"
|
|
11262
|
+
];
|
|
11263
|
+
var DEFAULT_SOURCE_ID_KEYS = [
|
|
11264
|
+
...DEFAULT_CITATION_KEYS,
|
|
11265
|
+
"id",
|
|
11266
|
+
"ids",
|
|
11267
|
+
"documentId",
|
|
11268
|
+
"documentIds",
|
|
11269
|
+
"docId",
|
|
11270
|
+
"docIds"
|
|
11271
|
+
];
|
|
11272
|
+
var DEFAULT_BANNED_UNSUPPORTED_PHRASES = [
|
|
11273
|
+
"i don't have enough information",
|
|
11274
|
+
"i do not have enough information",
|
|
11275
|
+
"not enough context",
|
|
11276
|
+
"cannot determine from the context",
|
|
11277
|
+
"unable to determine from the provided context"
|
|
11278
|
+
];
|
|
11279
|
+
var STOP_WORDS = /* @__PURE__ */ new Set([
|
|
11280
|
+
"a",
|
|
11281
|
+
"an",
|
|
11282
|
+
"and",
|
|
11283
|
+
"are",
|
|
11284
|
+
"but",
|
|
11285
|
+
"for",
|
|
11286
|
+
"from",
|
|
11287
|
+
"have",
|
|
11288
|
+
"into",
|
|
11289
|
+
"not",
|
|
11290
|
+
"that",
|
|
11291
|
+
"the",
|
|
11292
|
+
"their",
|
|
11293
|
+
"this",
|
|
11294
|
+
"was",
|
|
11295
|
+
"were",
|
|
11296
|
+
"with",
|
|
11297
|
+
"you",
|
|
11298
|
+
"your"
|
|
11299
|
+
]);
|
|
11300
|
+
function normalizeKey(key) {
|
|
11301
|
+
return key.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
11302
|
+
}
|
|
11303
|
+
function keySet(keys) {
|
|
11304
|
+
return new Set(keys.map(normalizeKey));
|
|
11305
|
+
}
|
|
11306
|
+
function valuesAsStrings(value, depth = 0) {
|
|
11307
|
+
if (depth > 5) return [];
|
|
11308
|
+
if (typeof value === "string") {
|
|
11309
|
+
const trimmed = value.trim();
|
|
11310
|
+
return trimmed.length > 0 ? [trimmed] : [];
|
|
11311
|
+
}
|
|
11312
|
+
if (typeof value === "number" || typeof value === "boolean") return [String(value)];
|
|
11313
|
+
if (Array.isArray(value)) {
|
|
11314
|
+
return value.flatMap((item) => valuesAsStrings(item, depth + 1));
|
|
11315
|
+
}
|
|
11316
|
+
if (value !== null && typeof value === "object") {
|
|
11317
|
+
return Object.values(value).flatMap(
|
|
11318
|
+
(item) => valuesAsStrings(item, depth + 1)
|
|
11319
|
+
);
|
|
11320
|
+
}
|
|
11321
|
+
return [];
|
|
11322
|
+
}
|
|
11323
|
+
function collectTextFields(nodes, keys, preferredKinds = []) {
|
|
11324
|
+
const wanted = keySet(keys);
|
|
11325
|
+
const preferred = new Set(preferredKinds);
|
|
11326
|
+
const orderedNodes = [...nodes].sort((a, b) => {
|
|
11327
|
+
const aPreferred = preferred.has(a.event.kind) ? 0 : 1;
|
|
11328
|
+
const bPreferred = preferred.has(b.event.kind) ? 0 : 1;
|
|
11329
|
+
return aPreferred - bPreferred || a.event.eventId.localeCompare(b.event.eventId);
|
|
11330
|
+
});
|
|
11331
|
+
const fields = [];
|
|
11332
|
+
for (const node of orderedNodes) {
|
|
11333
|
+
const attrs = node.event.attributes;
|
|
11334
|
+
if (attrs === void 0) continue;
|
|
11335
|
+
for (const [key, value] of Object.entries(attrs)) {
|
|
11336
|
+
if (!wanted.has(normalizeKey(key))) continue;
|
|
11337
|
+
for (const text of valuesAsStrings(value)) {
|
|
11338
|
+
fields.push({ text, node, path: `attributes.${key}` });
|
|
11339
|
+
}
|
|
11340
|
+
}
|
|
11341
|
+
}
|
|
11342
|
+
return fields;
|
|
11343
|
+
}
|
|
11344
|
+
function tokenize(text) {
|
|
11345
|
+
return [...text.toLowerCase().matchAll(/[a-z0-9][a-z0-9'-]{2,}/g)].map((match) => match[0].replace(/^['-]+|['-]+$/g, "")).filter((token) => token.length > 2 && !STOP_WORDS.has(token));
|
|
11346
|
+
}
|
|
11347
|
+
function firstEvidence(fields, run, path15) {
|
|
11348
|
+
const first = fields[0];
|
|
11349
|
+
return first === void 0 ? evidenceForRun(run, path15) : evidenceForEvent(first.node.event, first.path);
|
|
11350
|
+
}
|
|
11351
|
+
function collectSourceIds(nodes, keys) {
|
|
11352
|
+
const wanted = keySet(keys);
|
|
11353
|
+
const ids = /* @__PURE__ */ new Set();
|
|
11354
|
+
for (const node of nodes) {
|
|
11355
|
+
const attrs = node.event.attributes;
|
|
11356
|
+
if (attrs === void 0) continue;
|
|
11357
|
+
for (const [key, value] of Object.entries(attrs)) {
|
|
11358
|
+
if (!wanted.has(normalizeKey(key))) continue;
|
|
11359
|
+
for (const candidate of valuesAsStrings(value)) {
|
|
11360
|
+
const trimmed = candidate.trim();
|
|
11361
|
+
if (trimmed.length > 0 && trimmed.length <= 128) ids.add(trimmed);
|
|
11362
|
+
}
|
|
11363
|
+
}
|
|
11364
|
+
}
|
|
11365
|
+
return [...ids].sort((a, b) => a.localeCompare(b));
|
|
11366
|
+
}
|
|
11367
|
+
function citationCount(answer, citationFields) {
|
|
11368
|
+
const inline = answer.match(/\[[^\]\n]{1,40}\]|\([A-Za-z][A-Za-z0-9_-]{0,39}\)/g)?.length ?? 0;
|
|
11369
|
+
return inline + citationFields.length;
|
|
11370
|
+
}
|
|
11371
|
+
function quotedSnippets(text, minLength) {
|
|
11372
|
+
const snippets = /* @__PURE__ */ new Set();
|
|
11373
|
+
const pattern = /"([^"\n]+)"|'([^'\n]+)'|“([^”\n]+)”/g;
|
|
11374
|
+
for (const match of text.matchAll(pattern)) {
|
|
11375
|
+
const snippet = (match[1] ?? match[2] ?? match[3] ?? "").trim();
|
|
11376
|
+
if (snippet.length >= minLength) snippets.add(snippet);
|
|
11377
|
+
}
|
|
11378
|
+
return [...snippets].sort((a, b) => a.localeCompare(b));
|
|
11379
|
+
}
|
|
11380
|
+
function wordCount(text) {
|
|
11381
|
+
return tokenize(text).length;
|
|
11382
|
+
}
|
|
11383
|
+
var checks = {
|
|
11384
|
+
requireSuccess() {
|
|
11385
|
+
return createRule(
|
|
11386
|
+
"eval.requireSuccess",
|
|
11387
|
+
"run",
|
|
11388
|
+
(context) => context.run.status === "ok" ? [] : [
|
|
11389
|
+
fail(
|
|
11390
|
+
"eval.requireSuccess",
|
|
11391
|
+
"Run did not complete successfully.",
|
|
11392
|
+
evidenceForRun(context.run, "status"),
|
|
11393
|
+
"ok",
|
|
11394
|
+
context.run.status ?? "unknown"
|
|
11395
|
+
)
|
|
11396
|
+
]
|
|
11397
|
+
);
|
|
11398
|
+
},
|
|
11399
|
+
requiredTools(required) {
|
|
11400
|
+
const expected = [...required].sort((a, b) => a.localeCompare(b));
|
|
11401
|
+
return createRule("eval.requiredTools", "tool", (context) => {
|
|
11402
|
+
const tools = new Set(nodeNames(context.nodes, "TOOL"));
|
|
11403
|
+
return expected.filter((name) => !tools.has(name)).map(
|
|
11404
|
+
(name) => fail(
|
|
11405
|
+
"eval.requiredTools",
|
|
11406
|
+
`Required tool ${name} did not appear.`,
|
|
11407
|
+
evidenceForRun(context.run, "children"),
|
|
11408
|
+
name,
|
|
11409
|
+
[...tools].sort((a, b) => a.localeCompare(b))
|
|
11410
|
+
)
|
|
11411
|
+
);
|
|
11412
|
+
});
|
|
11413
|
+
},
|
|
11414
|
+
forbiddenTools(forbidden) {
|
|
11415
|
+
const blocked = [...forbidden].sort((a, b) => a.localeCompare(b));
|
|
11416
|
+
return createRule(
|
|
11417
|
+
"eval.forbiddenTools",
|
|
11418
|
+
"tool",
|
|
11419
|
+
(context) => context.nodes.filter((node) => node.event.kind === "TOOL" && blocked.includes(node.event.name)).map(
|
|
11420
|
+
(node) => fail(
|
|
11421
|
+
"eval.forbiddenTools",
|
|
11422
|
+
`Forbidden tool ${node.event.name} appeared.`,
|
|
11423
|
+
evidenceForEvent(node.event, "name"),
|
|
11424
|
+
"tool absent",
|
|
11425
|
+
node.event.name
|
|
11426
|
+
)
|
|
11427
|
+
)
|
|
11428
|
+
);
|
|
11429
|
+
},
|
|
11430
|
+
maxDurationMs(maxDurationMs) {
|
|
11431
|
+
return createRule(
|
|
11432
|
+
"eval.maxDurationMs",
|
|
11433
|
+
"run",
|
|
11434
|
+
(context) => context.run.durationMs !== void 0 && context.run.durationMs > maxDurationMs ? [
|
|
11435
|
+
fail(
|
|
11436
|
+
"eval.maxDurationMs",
|
|
11437
|
+
`Run duration exceeded ${maxDurationMs}ms.`,
|
|
11438
|
+
evidenceForRun(context.run, "durationMs"),
|
|
11439
|
+
{ maxDurationMs },
|
|
11440
|
+
context.run.durationMs
|
|
11441
|
+
)
|
|
11442
|
+
] : []
|
|
11443
|
+
);
|
|
11444
|
+
},
|
|
11445
|
+
maxDepth(maxDepth) {
|
|
11446
|
+
return createRule("eval.maxDepth", "structure", (context) => {
|
|
11447
|
+
const deepest = context.nodes.reduce(
|
|
11448
|
+
(current, node) => current === void 0 || node.depth > current.depth ? node : current,
|
|
11449
|
+
void 0
|
|
11450
|
+
);
|
|
11451
|
+
return deepest !== void 0 && deepest.depth > maxDepth ? [
|
|
11452
|
+
fail(
|
|
11453
|
+
"eval.maxDepth",
|
|
11454
|
+
`Run tree depth exceeded ${maxDepth}.`,
|
|
11455
|
+
evidenceForEvent(deepest.event, "depth"),
|
|
11456
|
+
{ maxDepth },
|
|
11457
|
+
deepest.depth
|
|
11458
|
+
)
|
|
11459
|
+
] : [];
|
|
11460
|
+
});
|
|
11461
|
+
},
|
|
11462
|
+
maxRetries(maxRetries) {
|
|
11463
|
+
return createRule(
|
|
11464
|
+
"eval.maxRetries",
|
|
11465
|
+
"structure",
|
|
11466
|
+
(context) => context.nodes.flatMap((node) => {
|
|
11467
|
+
const retries = numericAttribute(node, ["retryCount", "retries", "attempt"]);
|
|
11468
|
+
return retries !== void 0 && retries > maxRetries ? [
|
|
11469
|
+
fail(
|
|
11470
|
+
"eval.maxRetries",
|
|
11471
|
+
`Retry count exceeded ${maxRetries}.`,
|
|
11472
|
+
evidenceForEvent(node.event, "attributes.retryCount"),
|
|
11473
|
+
{ maxRetries },
|
|
11474
|
+
retries
|
|
11475
|
+
)
|
|
11476
|
+
] : [];
|
|
11477
|
+
})
|
|
11478
|
+
);
|
|
11479
|
+
},
|
|
11480
|
+
maxTotalTokens(maxTotalTokens) {
|
|
11481
|
+
return createRule("eval.maxTotalTokens", "llm", (context) => {
|
|
11482
|
+
const total = totalTokenCount(context.events);
|
|
11483
|
+
return total > maxTotalTokens ? [
|
|
11484
|
+
fail(
|
|
11485
|
+
"eval.maxTotalTokens",
|
|
11486
|
+
`Total token usage exceeded ${maxTotalTokens}.`,
|
|
11487
|
+
evidenceForRun(context.run, "tokenUsage.total"),
|
|
11488
|
+
{ maxTotalTokens },
|
|
11489
|
+
total
|
|
11490
|
+
)
|
|
11491
|
+
] : [];
|
|
11492
|
+
});
|
|
11493
|
+
},
|
|
11494
|
+
noFailedSteps() {
|
|
11495
|
+
return createRule(
|
|
11496
|
+
"eval.noFailedSteps",
|
|
11497
|
+
"run",
|
|
11498
|
+
(context) => context.nodes.filter((node) => node.event.status === "error" || node.event.kind === "ERROR").map(
|
|
11499
|
+
(node) => fail(
|
|
11500
|
+
"eval.noFailedSteps",
|
|
11501
|
+
"Run contains a failed step or error node.",
|
|
11502
|
+
evidenceForEvent(node.event, "status"),
|
|
11503
|
+
"no failed nodes",
|
|
11504
|
+
node.event.status ?? node.event.kind
|
|
11505
|
+
)
|
|
11506
|
+
)
|
|
11507
|
+
);
|
|
11508
|
+
},
|
|
11509
|
+
requiredRetrievalBeforeGeneration() {
|
|
11510
|
+
return createRule("eval.requiredRetrievalBeforeGeneration", "retrieval", (context) => {
|
|
11511
|
+
const firstLlmIndex = context.nodes.findIndex((node) => node.event.kind === "LLM");
|
|
11512
|
+
if (firstLlmIndex === -1) return [];
|
|
11513
|
+
const retrievalIndex = context.nodes.findIndex(
|
|
11514
|
+
(node, index) => index < firstLlmIndex && node.event.kind === "RETRIEVER"
|
|
11515
|
+
);
|
|
11516
|
+
return retrievalIndex === -1 ? [
|
|
11517
|
+
fail(
|
|
11518
|
+
"eval.requiredRetrievalBeforeGeneration",
|
|
11519
|
+
"No retrieval step appeared before the first LLM generation.",
|
|
11520
|
+
evidenceForEvent(context.nodes[firstLlmIndex].event, "kind"),
|
|
11521
|
+
"RETRIEVER before LLM",
|
|
11522
|
+
"LLM before RETRIEVER"
|
|
11523
|
+
)
|
|
11524
|
+
] : [];
|
|
11525
|
+
});
|
|
11526
|
+
},
|
|
11527
|
+
requiredDecisionMetadata(keys) {
|
|
11528
|
+
const required = [...keys].sort((a, b) => a.localeCompare(b));
|
|
11529
|
+
return createRule("eval.requiredDecisionMetadata", "structure", (context) => {
|
|
11530
|
+
const decisions = context.nodes.filter((node) => node.event.kind === "DECISION");
|
|
11531
|
+
if (decisions.length === 0) {
|
|
11532
|
+
return [
|
|
11533
|
+
fail(
|
|
11534
|
+
"eval.requiredDecisionMetadata",
|
|
11535
|
+
"No decision node is available for required metadata.",
|
|
11536
|
+
evidenceForRun(context.run, "children"),
|
|
11537
|
+
{ decisionMetadata: required },
|
|
11538
|
+
"no decision nodes"
|
|
11539
|
+
)
|
|
11540
|
+
];
|
|
11541
|
+
}
|
|
11542
|
+
return decisions.flatMap(
|
|
11543
|
+
(node) => required.filter((key) => !hasAttribute(node, key)).map(
|
|
11544
|
+
(key) => fail(
|
|
11545
|
+
"eval.requiredDecisionMetadata",
|
|
11546
|
+
`Decision metadata ${key} is missing.`,
|
|
11547
|
+
evidenceForEvent(node.event, `attributes.${key}`),
|
|
11548
|
+
key,
|
|
11549
|
+
"missing"
|
|
11550
|
+
)
|
|
11551
|
+
)
|
|
11552
|
+
);
|
|
11553
|
+
});
|
|
11554
|
+
},
|
|
11555
|
+
contextOverlap(options = {}) {
|
|
11556
|
+
const minOverlap = options.minOverlap ?? 0.1;
|
|
11557
|
+
const minSharedTerms = options.minSharedTerms ?? 1;
|
|
11558
|
+
const answerKeys = options.answerKeys ?? DEFAULT_ANSWER_KEYS;
|
|
11559
|
+
const contextKeys = options.contextKeys ?? DEFAULT_CONTEXT_KEYS;
|
|
11560
|
+
return createRule("eval.contextOverlap", "retrieval", (context) => {
|
|
11561
|
+
const answers = collectTextFields(context.nodes, answerKeys, ["RESULT", "LLM", "AGENT"]);
|
|
11562
|
+
const contexts = collectTextFields(context.nodes, contextKeys, ["RETRIEVER", "TOOL"]);
|
|
11563
|
+
if (answers.length === 0 || contexts.length === 0) {
|
|
11564
|
+
return [
|
|
11565
|
+
fail(
|
|
11566
|
+
"eval.contextOverlap",
|
|
11567
|
+
"Answer and context text are required for overlap evaluation.",
|
|
11568
|
+
firstEvidence(answers.length > 0 ? answers : contexts, context.run, "children"),
|
|
11569
|
+
{ answer: "present", context: "present" },
|
|
11570
|
+
{ answerFields: answers.length, contextFields: contexts.length }
|
|
11571
|
+
)
|
|
11572
|
+
];
|
|
11573
|
+
}
|
|
11574
|
+
const answerTerms = new Set(tokenize(answers.map((field) => field.text).join(" ")));
|
|
11575
|
+
const contextTerms = new Set(tokenize(contexts.map((field) => field.text).join(" ")));
|
|
11576
|
+
const sharedTerms = [...answerTerms].filter((term) => contextTerms.has(term)).length;
|
|
11577
|
+
const overlap = answerTerms.size === 0 ? 0 : sharedTerms / answerTerms.size;
|
|
11578
|
+
return sharedTerms < minSharedTerms || overlap < minOverlap ? [
|
|
11579
|
+
fail(
|
|
11580
|
+
"eval.contextOverlap",
|
|
11581
|
+
"Answer text did not sufficiently overlap retrieved context.",
|
|
11582
|
+
firstEvidence(answers, context.run, "attributes.answer"),
|
|
11583
|
+
{ minOverlap, minSharedTerms },
|
|
11584
|
+
{
|
|
11585
|
+
answerTerms: answerTerms.size,
|
|
11586
|
+
contextTerms: contextTerms.size,
|
|
11587
|
+
sharedTerms,
|
|
11588
|
+
overlap: Number(overlap.toFixed(4))
|
|
11589
|
+
}
|
|
11590
|
+
)
|
|
11591
|
+
] : [];
|
|
11592
|
+
});
|
|
11593
|
+
},
|
|
11594
|
+
quoteOverlap(options = {}) {
|
|
11595
|
+
const answerKeys = options.answerKeys ?? DEFAULT_ANSWER_KEYS;
|
|
11596
|
+
const contextKeys = options.contextKeys ?? DEFAULT_CONTEXT_KEYS;
|
|
11597
|
+
const minQuoteLength = options.minQuoteLength ?? 6;
|
|
11598
|
+
const requireQuote = options.requireQuote ?? true;
|
|
11599
|
+
return createRule("eval.quoteOverlap", "retrieval", (context) => {
|
|
11600
|
+
const answers = collectTextFields(context.nodes, answerKeys, ["RESULT", "LLM", "AGENT"]);
|
|
11601
|
+
const contexts = collectTextFields(context.nodes, contextKeys, ["RETRIEVER", "TOOL"]);
|
|
11602
|
+
const answerText = answers.map((field) => field.text).join(" ");
|
|
11603
|
+
const contextText = contexts.map((field) => field.text).join(" ").toLowerCase();
|
|
11604
|
+
const quotes = quotedSnippets(answerText, minQuoteLength);
|
|
11605
|
+
if (quotes.length === 0) {
|
|
11606
|
+
return requireQuote ? [
|
|
11607
|
+
fail(
|
|
11608
|
+
"eval.quoteOverlap",
|
|
11609
|
+
"Answer did not contain a quote for overlap evaluation.",
|
|
11610
|
+
firstEvidence(answers, context.run, "attributes.answer"),
|
|
11611
|
+
{ quotedText: "present" },
|
|
11612
|
+
{ quoteCount: 0 }
|
|
11613
|
+
)
|
|
11614
|
+
] : [];
|
|
11615
|
+
}
|
|
11616
|
+
const missing = quotes.filter((quote) => !contextText.includes(quote.toLowerCase()));
|
|
11617
|
+
return missing.length > 0 ? [
|
|
11618
|
+
fail(
|
|
11619
|
+
"eval.quoteOverlap",
|
|
11620
|
+
"Quoted answer text did not appear in retrieved context.",
|
|
11621
|
+
firstEvidence(answers, context.run, "attributes.answer"),
|
|
11622
|
+
{ allQuotesInContext: true },
|
|
11623
|
+
{ quoteCount: quotes.length, missingQuotes: missing.length }
|
|
11624
|
+
)
|
|
11625
|
+
] : [];
|
|
11626
|
+
});
|
|
11627
|
+
},
|
|
11628
|
+
citationPresence(options = {}) {
|
|
11629
|
+
const answerKeys = options.answerKeys ?? DEFAULT_ANSWER_KEYS;
|
|
11630
|
+
const citationKeys = options.citationKeys ?? DEFAULT_CITATION_KEYS;
|
|
11631
|
+
return createRule("eval.citationPresence", "retrieval", (context) => {
|
|
11632
|
+
const answers = collectTextFields(context.nodes, answerKeys, ["RESULT", "LLM", "AGENT"]);
|
|
11633
|
+
const citations = collectTextFields(context.nodes, citationKeys);
|
|
11634
|
+
const count = citationCount(answers.map((field) => field.text).join(" "), citations);
|
|
11635
|
+
return count === 0 ? [
|
|
11636
|
+
fail(
|
|
11637
|
+
"eval.citationPresence",
|
|
11638
|
+
"Answer did not include citations or source references.",
|
|
11639
|
+
firstEvidence(answers, context.run, "attributes.answer"),
|
|
11640
|
+
{ citationCount: ">= 1" },
|
|
11641
|
+
{ citationCount: 0 }
|
|
11642
|
+
)
|
|
11643
|
+
] : [];
|
|
11644
|
+
});
|
|
11645
|
+
},
|
|
11646
|
+
requiredSourceIds(requiredIds, options = {}) {
|
|
11647
|
+
const expected = [...requiredIds].sort((a, b) => a.localeCompare(b));
|
|
11648
|
+
const sourceIdKeys = options.sourceIdKeys ?? DEFAULT_SOURCE_ID_KEYS;
|
|
11649
|
+
return createRule("eval.requiredSourceIds", "retrieval", (context) => {
|
|
11650
|
+
const available = collectSourceIds(context.nodes, sourceIdKeys);
|
|
11651
|
+
const availableSet = new Set(available);
|
|
11652
|
+
const missing = expected.filter((id) => !availableSet.has(id));
|
|
11653
|
+
return missing.length > 0 ? [
|
|
11654
|
+
fail(
|
|
11655
|
+
"eval.requiredSourceIds",
|
|
11656
|
+
"Required source IDs were not present in trace context or citations.",
|
|
11657
|
+
evidenceForRun(context.run, "children"),
|
|
11658
|
+
{ sourceIds: expected },
|
|
11659
|
+
{ missingSourceIds: missing, availableSourceIds: available.slice(0, 20) }
|
|
11660
|
+
)
|
|
11661
|
+
] : [];
|
|
11662
|
+
});
|
|
11663
|
+
},
|
|
11664
|
+
answerLengthBounds(options) {
|
|
11665
|
+
const answerKeys = options.answerKeys ?? DEFAULT_ANSWER_KEYS;
|
|
11666
|
+
return createRule("eval.answerLengthBounds", "llm", (context) => {
|
|
11667
|
+
const answers = collectTextFields(context.nodes, answerKeys, ["RESULT", "LLM", "AGENT"]);
|
|
11668
|
+
const answer = answers.map((field) => field.text).join(" ").trim();
|
|
11669
|
+
const characters = answer.length;
|
|
11670
|
+
const words = wordCount(answer);
|
|
11671
|
+
const tooShort = options.minCharacters !== void 0 && characters < options.minCharacters || options.minWords !== void 0 && words < options.minWords;
|
|
11672
|
+
const tooLong = options.maxCharacters !== void 0 && characters > options.maxCharacters || options.maxWords !== void 0 && words > options.maxWords;
|
|
11673
|
+
return answer.length === 0 || tooShort || tooLong ? [
|
|
11674
|
+
fail(
|
|
11675
|
+
"eval.answerLengthBounds",
|
|
11676
|
+
"Answer length fell outside required bounds.",
|
|
11677
|
+
firstEvidence(answers, context.run, "attributes.answer"),
|
|
11678
|
+
{
|
|
11679
|
+
minCharacters: options.minCharacters,
|
|
11680
|
+
maxCharacters: options.maxCharacters,
|
|
11681
|
+
minWords: options.minWords,
|
|
11682
|
+
maxWords: options.maxWords
|
|
11683
|
+
},
|
|
11684
|
+
{ characters, words }
|
|
11685
|
+
)
|
|
11686
|
+
] : [];
|
|
11687
|
+
});
|
|
11688
|
+
},
|
|
11689
|
+
bannedUnsupportedPhrases(phrases = DEFAULT_BANNED_UNSUPPORTED_PHRASES, options = {}) {
|
|
11690
|
+
const answerKeys = options.answerKeys ?? DEFAULT_ANSWER_KEYS;
|
|
11691
|
+
const banned = [...phrases].map((phrase) => phrase.toLowerCase()).sort();
|
|
11692
|
+
return createRule("eval.bannedUnsupportedPhrases", "safety", (context) => {
|
|
11693
|
+
const answers = collectTextFields(context.nodes, answerKeys, ["RESULT", "LLM", "AGENT"]);
|
|
11694
|
+
const answer = answers.map((field) => field.text).join(" ").toLowerCase();
|
|
11695
|
+
const matches = banned.filter((phrase) => answer.includes(phrase));
|
|
11696
|
+
return matches.length > 0 ? [
|
|
11697
|
+
fail(
|
|
11698
|
+
"eval.bannedUnsupportedPhrases",
|
|
11699
|
+
"Answer contained banned unsupported-answer phrasing.",
|
|
11700
|
+
firstEvidence(answers, context.run, "attributes.answer"),
|
|
11701
|
+
{ bannedPhraseCount: banned.length },
|
|
11702
|
+
{ matchedPhraseCount: matches.length }
|
|
11703
|
+
)
|
|
11704
|
+
] : [];
|
|
11705
|
+
});
|
|
11706
|
+
}
|
|
11707
|
+
};
|
|
11708
|
+
function renderEvalMarkdown(result) {
|
|
11709
|
+
const lines = [
|
|
11710
|
+
`# AgentInspect Eval`,
|
|
11711
|
+
"",
|
|
11712
|
+
`Status: ${result.status}`,
|
|
11713
|
+
`Format: ${result.format}`,
|
|
11714
|
+
...result.runId !== void 0 ? [`Run: ${result.runId}`] : [],
|
|
11715
|
+
`Summary: ${result.summary.passed} passed, ${result.summary.failed} failed, ${result.summary.warnings} warnings, ${result.summary.errors} errors`
|
|
11716
|
+
];
|
|
11717
|
+
if (result.diagnostics.length > 0) {
|
|
11718
|
+
lines.push("", "## Diagnostics");
|
|
11719
|
+
for (const diagnostic4 of result.diagnostics) {
|
|
11720
|
+
lines.push(`- ${diagnostic4.code}: ${diagnostic4.message}`);
|
|
11721
|
+
}
|
|
11722
|
+
}
|
|
11723
|
+
if (result.findings.length > 0) {
|
|
11724
|
+
lines.push("", "## Findings");
|
|
11725
|
+
for (const finding of result.findings) {
|
|
11726
|
+
const path15 = finding.evidence[0]?.path;
|
|
11727
|
+
lines.push(`- ${finding.ruleId}: ${finding.message}${path15 ? ` (${path15})` : ""}`);
|
|
11728
|
+
}
|
|
11729
|
+
}
|
|
11730
|
+
return `${lines.join("\n")}
|
|
11731
|
+
`;
|
|
11732
|
+
}
|
|
11733
|
+
|
|
11734
|
+
// packages/cli/src/eval.ts
|
|
11735
|
+
var CONFIG_EXTENSIONS2 = /* @__PURE__ */ new Set([".json", ".js", ".mjs", ".cjs"]);
|
|
11736
|
+
var TS_CONFIG_EXTENSIONS2 = /* @__PURE__ */ new Set([".ts", ".mts", ".cts"]);
|
|
11737
|
+
function diagnostic3(code, message, severity = "error") {
|
|
11738
|
+
return { code, message, severity };
|
|
11739
|
+
}
|
|
11740
|
+
function errorResult4(code, message, format = "unknown") {
|
|
11741
|
+
return {
|
|
11742
|
+
ok: false,
|
|
11743
|
+
status: "error",
|
|
11744
|
+
format,
|
|
11745
|
+
summary: { passed: 0, failed: 0, warnings: 0, errors: 1 },
|
|
11746
|
+
findings: [],
|
|
11747
|
+
diagnostics: [diagnostic3(code, message)]
|
|
11748
|
+
};
|
|
11749
|
+
}
|
|
11750
|
+
function parseNumber2(value, label) {
|
|
11751
|
+
if (value === void 0) return void 0;
|
|
11752
|
+
const parsed = Number(value);
|
|
11753
|
+
if (!Number.isFinite(parsed) || parsed < 0) {
|
|
11754
|
+
throw new Error(`${label} must be a non-negative number.`);
|
|
11755
|
+
}
|
|
11756
|
+
return parsed;
|
|
11757
|
+
}
|
|
11758
|
+
function asStringArray2(value, label) {
|
|
11759
|
+
if (value === void 0) return void 0;
|
|
11760
|
+
if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
|
|
11761
|
+
throw new Error(`${label} must be an array of strings.`);
|
|
11762
|
+
}
|
|
11763
|
+
return value;
|
|
11764
|
+
}
|
|
11765
|
+
function asConfig2(value) {
|
|
11766
|
+
if (value === void 0 || value === null) return {};
|
|
11767
|
+
if (typeof value !== "object" || Array.isArray(value)) {
|
|
11768
|
+
throw new Error("Config must export an object.");
|
|
11769
|
+
}
|
|
11770
|
+
return value;
|
|
11771
|
+
}
|
|
11772
|
+
async function loadConfig2(configPath) {
|
|
11773
|
+
if (configPath === void 0) return {};
|
|
11774
|
+
const extension = path13.extname(configPath);
|
|
11775
|
+
if (TS_CONFIG_EXTENSIONS2.has(extension)) {
|
|
11776
|
+
throw new Error(
|
|
11777
|
+
"TypeScript eval configs require an explicit precompiled JavaScript config or future --config-loader support."
|
|
11778
|
+
);
|
|
11779
|
+
}
|
|
11780
|
+
if (!CONFIG_EXTENSIONS2.has(extension)) {
|
|
11781
|
+
throw new Error("Unsupported eval config extension. Use .json, .js, .mjs, or .cjs.");
|
|
11782
|
+
}
|
|
11783
|
+
const absolute = path13.resolve(configPath);
|
|
11784
|
+
if (extension === ".json") {
|
|
11785
|
+
const raw = await readFile(absolute, "utf-8");
|
|
11786
|
+
return asConfig2(JSON.parse(raw));
|
|
11787
|
+
}
|
|
11788
|
+
const mod = await import(pathToFileURL(absolute).href);
|
|
11789
|
+
return asConfig2("default" in mod ? mod.default : mod);
|
|
11790
|
+
}
|
|
11791
|
+
function normalizeConfig2(config) {
|
|
11792
|
+
if (config.eval === void 0) return {};
|
|
11793
|
+
if (typeof config.eval !== "object" || Array.isArray(config.eval)) {
|
|
11794
|
+
throw new Error("eval config must be an object.");
|
|
11795
|
+
}
|
|
11796
|
+
return config.eval;
|
|
11797
|
+
}
|
|
11798
|
+
function maybeAdd(rules, value, makeRule) {
|
|
11799
|
+
if (value !== void 0) rules.push(makeRule(value));
|
|
11800
|
+
}
|
|
11801
|
+
function optionObject(value) {
|
|
11802
|
+
if (value === void 0 || value === false) return void 0;
|
|
11803
|
+
if (value === true) return {};
|
|
11804
|
+
if (typeof value !== "object" || Array.isArray(value)) {
|
|
11805
|
+
throw new Error("Eval heuristic config must be a boolean or object.");
|
|
11806
|
+
}
|
|
11807
|
+
return value;
|
|
11808
|
+
}
|
|
11809
|
+
function sourceIdsFromConfig(value) {
|
|
11810
|
+
if (value === void 0) return void 0;
|
|
11811
|
+
if (Array.isArray(value)) return { ids: asStringArray2(value, "eval.requiredSourceIds") ?? [] };
|
|
11812
|
+
if (typeof value !== "object" || Array.isArray(value)) {
|
|
11813
|
+
throw new Error("eval.requiredSourceIds must be an array or object.");
|
|
11814
|
+
}
|
|
11815
|
+
const ids = asStringArray2(value.ids, "eval.requiredSourceIds.ids") ?? [];
|
|
11816
|
+
return { ids, options: { sourceIdKeys: value.sourceIdKeys } };
|
|
11817
|
+
}
|
|
11818
|
+
function buildRules2(config, options) {
|
|
11819
|
+
const evalConfig = normalizeConfig2(config);
|
|
11820
|
+
const rules = [];
|
|
11821
|
+
if (evalConfig.requireSuccess || options.requireSuccess) rules.push(checks.requireSuccess());
|
|
11822
|
+
const requiredTools = [
|
|
11823
|
+
...asStringArray2(evalConfig.requiredTools, "eval.requiredTools") ?? [],
|
|
11824
|
+
...options.requiredTool ?? []
|
|
11825
|
+
];
|
|
11826
|
+
if (requiredTools.length > 0) rules.push(checks.requiredTools(requiredTools));
|
|
11827
|
+
const forbiddenTools = [
|
|
11828
|
+
...asStringArray2(evalConfig.forbiddenTools, "eval.forbiddenTools") ?? [],
|
|
11829
|
+
...options.forbidTool ?? [],
|
|
11830
|
+
...options.forbiddenTool ?? []
|
|
11831
|
+
];
|
|
11832
|
+
if (forbiddenTools.length > 0) rules.push(checks.forbiddenTools(forbiddenTools));
|
|
11833
|
+
const maxDurationMs = parseNumber2(options.maxDurationMs, "--max-duration-ms") ?? evalConfig.maxDurationMs;
|
|
11834
|
+
maybeAdd(rules, maxDurationMs, checks.maxDurationMs);
|
|
11835
|
+
const maxDepth = parseNumber2(options.maxDepth, "--max-depth") ?? evalConfig.maxDepth;
|
|
11836
|
+
maybeAdd(rules, maxDepth, checks.maxDepth);
|
|
11837
|
+
const maxRetries = parseNumber2(options.maxRetries, "--max-retries") ?? evalConfig.maxRetries;
|
|
11838
|
+
maybeAdd(rules, maxRetries, checks.maxRetries);
|
|
11839
|
+
const maxTotalTokens = parseNumber2(options.maxTotalTokens, "--max-total-tokens") ?? evalConfig.maxTotalTokens;
|
|
11840
|
+
maybeAdd(rules, maxTotalTokens, checks.maxTotalTokens);
|
|
11841
|
+
if (evalConfig.requiredRetrievalBeforeGeneration || options.requireRetrievalBeforeGeneration) {
|
|
11842
|
+
rules.push(checks.requiredRetrievalBeforeGeneration());
|
|
11843
|
+
}
|
|
11844
|
+
const requiredDecisionMetadata = [
|
|
11845
|
+
...asStringArray2(
|
|
11846
|
+
evalConfig.requiredDecisionMetadata,
|
|
11847
|
+
"eval.requiredDecisionMetadata"
|
|
11848
|
+
) ?? [],
|
|
11849
|
+
...options.requiredDecisionMetadata ?? []
|
|
11850
|
+
];
|
|
11851
|
+
if (requiredDecisionMetadata.length > 0) {
|
|
11852
|
+
rules.push(checks.requiredDecisionMetadata(requiredDecisionMetadata));
|
|
11853
|
+
}
|
|
11854
|
+
const contextOverlap = optionObject(evalConfig.contextOverlap);
|
|
11855
|
+
const minOverlap = parseNumber2(options.minContextOverlap, "--min-context-overlap");
|
|
11856
|
+
const minSharedTerms = parseNumber2(options.minSharedTerms, "--min-shared-terms");
|
|
11857
|
+
if (contextOverlap !== void 0 || options.contextOverlap || minOverlap !== void 0 || minSharedTerms !== void 0) {
|
|
11858
|
+
rules.push(checks.contextOverlap({ ...contextOverlap, minOverlap, minSharedTerms }));
|
|
11859
|
+
}
|
|
11860
|
+
const quoteOverlap = optionObject(evalConfig.quoteOverlap);
|
|
11861
|
+
if (quoteOverlap !== void 0 || options.quoteOverlap) {
|
|
11862
|
+
rules.push(checks.quoteOverlap(quoteOverlap));
|
|
11863
|
+
}
|
|
11864
|
+
const citationPresence = optionObject(evalConfig.citationPresence);
|
|
11865
|
+
if (citationPresence !== void 0 || options.citationPresence) {
|
|
11866
|
+
rules.push(checks.citationPresence(citationPresence));
|
|
11867
|
+
}
|
|
11868
|
+
const sourceIds = sourceIdsFromConfig(evalConfig.requiredSourceIds);
|
|
11869
|
+
const requiredSourceIds = [...sourceIds?.ids ?? [], ...options.requiredSourceId ?? []];
|
|
11870
|
+
if (requiredSourceIds.length > 0) {
|
|
11871
|
+
rules.push(checks.requiredSourceIds(requiredSourceIds, sourceIds?.options));
|
|
11872
|
+
}
|
|
11873
|
+
const cliAnswerLength = {
|
|
11874
|
+
minCharacters: parseNumber2(options.minAnswerCharacters, "--min-answer-characters"),
|
|
11875
|
+
maxCharacters: parseNumber2(options.maxAnswerCharacters, "--max-answer-characters"),
|
|
11876
|
+
minWords: parseNumber2(options.minAnswerWords, "--min-answer-words"),
|
|
11877
|
+
maxWords: parseNumber2(options.maxAnswerWords, "--max-answer-words")
|
|
11878
|
+
};
|
|
11879
|
+
const hasCliAnswerLength = Object.values(cliAnswerLength).some((value) => value !== void 0);
|
|
11880
|
+
if (evalConfig.answerLengthBounds !== void 0 || hasCliAnswerLength) {
|
|
11881
|
+
rules.push(checks.answerLengthBounds({ ...evalConfig.answerLengthBounds, ...cliAnswerLength }));
|
|
11882
|
+
}
|
|
11883
|
+
const bannedPhrases = [
|
|
11884
|
+
...asStringArray2(
|
|
11885
|
+
evalConfig.bannedUnsupportedPhrases,
|
|
11886
|
+
"eval.bannedUnsupportedPhrases"
|
|
11887
|
+
) ?? [],
|
|
11888
|
+
...options.bannedPhrase ?? []
|
|
11889
|
+
];
|
|
11890
|
+
if (bannedPhrases.length > 0) rules.push(checks.bannedUnsupportedPhrases(bannedPhrases));
|
|
11891
|
+
return rules.length > 0 ? rules : void 0;
|
|
11892
|
+
}
|
|
11893
|
+
function exitCodeFor2(result) {
|
|
11894
|
+
if (result.status === "pass") return 0;
|
|
11895
|
+
if (result.status === "fail") return 1;
|
|
11896
|
+
return 2;
|
|
11897
|
+
}
|
|
11898
|
+
function stable3(value) {
|
|
11899
|
+
if (Array.isArray(value)) return value.map(stable3);
|
|
11900
|
+
if (value === null || typeof value !== "object") return value;
|
|
11901
|
+
const record = value;
|
|
11902
|
+
return Object.fromEntries(
|
|
11903
|
+
Object.keys(record).sort((a, b) => a.localeCompare(b)).map((key) => [key, stable3(record[key])])
|
|
11904
|
+
);
|
|
11905
|
+
}
|
|
11906
|
+
function printJson2(result) {
|
|
11907
|
+
console.log(JSON.stringify(stable3(result), null, 2));
|
|
11908
|
+
}
|
|
11909
|
+
function printHuman2(result) {
|
|
11910
|
+
console.log(`Eval status: ${result.status}`);
|
|
11911
|
+
console.log(`Format: ${result.format}`);
|
|
11912
|
+
if (result.runId !== void 0) console.log(`Run: ${result.runId}`);
|
|
11913
|
+
console.log(
|
|
11914
|
+
`Summary: ${result.summary.failed} failed, ${result.summary.warnings} warning(s), ${result.summary.errors} error(s)`
|
|
11915
|
+
);
|
|
11916
|
+
for (const diagnostic4 of result.diagnostics) {
|
|
11917
|
+
console.log(`- ${diagnostic4.code}: ${diagnostic4.message}`);
|
|
11918
|
+
}
|
|
11919
|
+
for (const finding of result.findings) {
|
|
11920
|
+
const path15 = finding.evidence[0]?.path;
|
|
11921
|
+
console.log(`- ${finding.ruleId}: ${finding.message}${path15 ? ` (${path15})` : ""}`);
|
|
11922
|
+
}
|
|
11923
|
+
}
|
|
11924
|
+
function readErrorResult2(error) {
|
|
11925
|
+
if (error instanceof TraceReadError) {
|
|
11926
|
+
const code = error.code === "unsupported_format" ? "AI_EVAL_UNSUPPORTED_FORMAT" : error.code === "ambiguous_format" ? "AI_EVAL_AMBIGUOUS_FORMAT" : "AI_EVAL_TRACE_UNREADABLE";
|
|
11927
|
+
return errorResult4(code, error.message);
|
|
11928
|
+
}
|
|
11929
|
+
return errorResult4(
|
|
11930
|
+
"AI_EVAL_TRACE_UNREADABLE",
|
|
11931
|
+
error instanceof Error ? error.message : String(error)
|
|
11932
|
+
);
|
|
11933
|
+
}
|
|
11934
|
+
async function evalCommand(target, options = {}, stdin = process.stdin) {
|
|
11935
|
+
let result;
|
|
11936
|
+
let phase = "config";
|
|
11937
|
+
try {
|
|
11938
|
+
const config = await loadConfig2(options.config);
|
|
11939
|
+
const rules = buildRules2(config, options);
|
|
11940
|
+
phase = "read";
|
|
11941
|
+
const input3 = await inputFromTarget(target, options, stdin);
|
|
11942
|
+
const read = await openTrace(input3, {
|
|
11943
|
+
...options.format !== void 0 ? { format: options.format } : {}
|
|
11944
|
+
});
|
|
11945
|
+
result = await evalRun(read, {
|
|
11946
|
+
...rules !== void 0 ? { checks: rules } : {},
|
|
11947
|
+
...options.run !== void 0 ? { runId: options.run } : {}
|
|
11948
|
+
});
|
|
11949
|
+
} catch (error) {
|
|
11950
|
+
if (phase === "config") {
|
|
11951
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
11952
|
+
const code = message.startsWith("--") ? "AI_EVAL_INVALID_ARGUMENTS" : error instanceof SyntaxError || message.includes("Unsupported eval config extension") || message.includes("TypeScript eval configs") || message.includes("Config must") || message.includes("eval config") || message.includes("must be an array") ? "AI_EVAL_INVALID_CONFIG" : "AI_EVAL_CONFIG_LOAD_FAILED";
|
|
11953
|
+
result = errorResult4(code, message);
|
|
11954
|
+
} else {
|
|
11955
|
+
result = readErrorResult2(error);
|
|
11956
|
+
}
|
|
11957
|
+
}
|
|
11958
|
+
process.exitCode = exitCodeFor2(result);
|
|
11959
|
+
if (options.json) printJson2(result);
|
|
11960
|
+
else if (options.markdown) console.log(renderEvalMarkdown(result).trimEnd());
|
|
11961
|
+
else printHuman2(result);
|
|
11962
|
+
}
|
|
11963
|
+
|
|
11964
|
+
// packages/cli/src/safety.ts
|
|
11965
|
+
var BEST_EFFORT_NOTE = "Best-effort local safety verification only; not a compliance, privacy, security, or regulatory certification.";
|
|
11966
|
+
var DEFAULT_MAX_STRING_LENGTH = 16384;
|
|
11967
|
+
var DEFAULT_MAX_ARRAY_LENGTH = 1e3;
|
|
11968
|
+
var DEFAULT_MAX_OBJECT_KEYS = 200;
|
|
11969
|
+
var DEFAULT_MAX_SERIALIZED_BYTES = 128 * 1024;
|
|
11970
|
+
function parseLimit3(value, label) {
|
|
11971
|
+
if (value === void 0) return void 0;
|
|
11972
|
+
const parsed = Number(value);
|
|
11973
|
+
if (!Number.isFinite(parsed) || parsed < 0) {
|
|
11974
|
+
throw new Error(`${label} must be a non-negative number.`);
|
|
11975
|
+
}
|
|
11976
|
+
return parsed;
|
|
11977
|
+
}
|
|
11978
|
+
function stable4(value) {
|
|
11979
|
+
if (Array.isArray(value)) return value.map(stable4);
|
|
11980
|
+
if (value === null || typeof value !== "object") return value;
|
|
11981
|
+
const record = value;
|
|
11982
|
+
return Object.fromEntries(
|
|
11983
|
+
Object.keys(record).sort((a, b) => a.localeCompare(b)).map((key) => [key, stable4(record[key])])
|
|
11984
|
+
);
|
|
11985
|
+
}
|
|
11986
|
+
function safetyDiagnostic(code, message, severity = "error") {
|
|
11987
|
+
return { code, message, severity };
|
|
11988
|
+
}
|
|
11989
|
+
function warningDiagnostics(warnings, unsupportedFields) {
|
|
11990
|
+
return [
|
|
11991
|
+
...warnings.map(
|
|
11992
|
+
(warning) => safetyDiagnostic(
|
|
11993
|
+
warning.code,
|
|
11994
|
+
warning.message,
|
|
11995
|
+
warning.severity === "error" ? "error" : "warning"
|
|
11996
|
+
)
|
|
11997
|
+
),
|
|
11998
|
+
...unsupportedFields.map(
|
|
11999
|
+
(field) => safetyDiagnostic(
|
|
10558
12000
|
"unsupported_field",
|
|
10559
12001
|
`Reader reported unsupported field: ${field}`,
|
|
10560
12002
|
"warning"
|
|
@@ -10596,7 +12038,7 @@ function resultFromParts(parts) {
|
|
|
10596
12038
|
note: BEST_EFFORT_NOTE
|
|
10597
12039
|
};
|
|
10598
12040
|
}
|
|
10599
|
-
function
|
|
12041
|
+
function readErrorResult3(command, error) {
|
|
10600
12042
|
if (error instanceof TraceReadError) {
|
|
10601
12043
|
const code = error.code === "unsupported_format" ? "AI_SAFETY_UNSUPPORTED_FORMAT" : error.code === "ambiguous_format" ? "AI_SAFETY_AMBIGUOUS_FORMAT" : "AI_SAFETY_TRACE_UNREADABLE";
|
|
10602
12044
|
return resultFromParts({
|
|
@@ -10646,27 +12088,76 @@ function buildSafetyRules(options) {
|
|
|
10646
12088
|
})
|
|
10647
12089
|
];
|
|
10648
12090
|
}
|
|
10649
|
-
function
|
|
12091
|
+
function flattenNodes2(nodes) {
|
|
12092
|
+
return nodes.flatMap((node) => [
|
|
12093
|
+
node,
|
|
12094
|
+
...flattenNodes2(
|
|
12095
|
+
node.children
|
|
12096
|
+
)
|
|
12097
|
+
]);
|
|
12098
|
+
}
|
|
12099
|
+
function detectorSeverity(finding) {
|
|
12100
|
+
return finding.severity;
|
|
12101
|
+
}
|
|
12102
|
+
function redactionDetectorFindings(read, runId) {
|
|
12103
|
+
const runs = runId === void 0 ? read.runs : read.runs.filter((run) => run.runId === runId);
|
|
12104
|
+
const out = [];
|
|
12105
|
+
for (const run of runs) {
|
|
12106
|
+
for (const node of flattenNodes2(run.children)) {
|
|
12107
|
+
const attrs = node.event.attributes;
|
|
12108
|
+
if (attrs === void 0) continue;
|
|
12109
|
+
const result = redact(attrs, { profile: "share" });
|
|
12110
|
+
for (const finding of result.findings) {
|
|
12111
|
+
if (finding.action === "keep") continue;
|
|
12112
|
+
out.push({
|
|
12113
|
+
ruleId: "safety.redactDetector",
|
|
12114
|
+
severity: detectorSeverity(finding),
|
|
12115
|
+
status: finding.severity === "error" ? "fail" : "warning",
|
|
12116
|
+
message: `Redaction detector ${finding.detector} matched ${finding.matchKind} at ${finding.path}.`,
|
|
12117
|
+
expected: "redacted trace content",
|
|
12118
|
+
actual: finding.detector,
|
|
12119
|
+
evidence: [
|
|
12120
|
+
{
|
|
12121
|
+
runId: node.event.runId,
|
|
12122
|
+
eventId: node.event.eventId,
|
|
12123
|
+
...node.event.parentId !== void 0 ? { parentId: node.event.parentId } : {},
|
|
12124
|
+
kind: node.event.kind,
|
|
12125
|
+
name: node.event.name,
|
|
12126
|
+
...node.event.status !== void 0 ? { status: node.event.status } : {},
|
|
12127
|
+
path: `attributes.${finding.path.replace(/^\$\.?/, "")}`
|
|
12128
|
+
}
|
|
12129
|
+
]
|
|
12130
|
+
});
|
|
12131
|
+
}
|
|
12132
|
+
}
|
|
12133
|
+
}
|
|
12134
|
+
return out.sort((a, b) => {
|
|
12135
|
+
const aEvidence = a.evidence[0];
|
|
12136
|
+
const bEvidence = b.evidence[0];
|
|
12137
|
+
return (aEvidence?.runId ?? "").localeCompare(bEvidence?.runId ?? "") || (aEvidence?.eventId ?? "").localeCompare(bEvidence?.eventId ?? "") || (aEvidence?.path ?? "").localeCompare(bEvidence?.path ?? "") || a.message.localeCompare(b.message);
|
|
12138
|
+
});
|
|
12139
|
+
}
|
|
12140
|
+
function exitCodeFor3(result) {
|
|
10650
12141
|
if (result.status === "SAFE" || result.status === "SAFE WITH WARNINGS") return 0;
|
|
10651
12142
|
if (result.status === "UNSAFE") return 1;
|
|
10652
12143
|
return 2;
|
|
10653
12144
|
}
|
|
10654
|
-
function
|
|
10655
|
-
console.log(JSON.stringify(
|
|
12145
|
+
function printJson3(result) {
|
|
12146
|
+
console.log(JSON.stringify(stable4(result), null, 2));
|
|
10656
12147
|
}
|
|
10657
|
-
function
|
|
12148
|
+
function printHuman3(result) {
|
|
10658
12149
|
console.log(`Safety status: ${result.status}`);
|
|
10659
12150
|
console.log(`Format: ${result.format}`);
|
|
10660
12151
|
if (result.runId !== void 0) console.log(`Run: ${result.runId}`);
|
|
10661
12152
|
console.log(
|
|
10662
12153
|
`Summary: ${result.summary.findings} finding(s), ${result.summary.warnings} warning(s), ${result.summary.errors} error(s)`
|
|
10663
12154
|
);
|
|
10664
|
-
for (const
|
|
10665
|
-
console.log(`- ${
|
|
12155
|
+
for (const diagnostic4 of result.diagnostics) {
|
|
12156
|
+
console.log(`- ${diagnostic4.code}: ${diagnostic4.message}`);
|
|
10666
12157
|
}
|
|
10667
12158
|
for (const finding of result.findings) {
|
|
10668
|
-
const
|
|
10669
|
-
console.log(`- ${finding.ruleId}: ${finding.message}${
|
|
12159
|
+
const path15 = finding.evidence[0]?.path;
|
|
12160
|
+
console.log(`- ${finding.ruleId}: ${finding.message}${path15 ? ` (${path15})` : ""}`);
|
|
10670
12161
|
}
|
|
10671
12162
|
console.log(`Note: ${result.note}`);
|
|
10672
12163
|
}
|
|
@@ -10685,11 +12176,12 @@ async function safetyCommand(command, target, options, stdin) {
|
|
|
10685
12176
|
...options.run !== void 0 ? { runId: options.run } : {}
|
|
10686
12177
|
}
|
|
10687
12178
|
);
|
|
12179
|
+
const detectorFindings = checkResult.diagnostics.length === 0 ? redactionDetectorFindings(read, checkResult.runId) : [];
|
|
10688
12180
|
result = resultFromParts({
|
|
10689
12181
|
command,
|
|
10690
12182
|
format: checkResult.format,
|
|
10691
12183
|
runId: checkResult.runId,
|
|
10692
|
-
findings: checkResult.findings,
|
|
12184
|
+
findings: [...checkResult.findings, ...detectorFindings],
|
|
10693
12185
|
diagnostics: [
|
|
10694
12186
|
...checkResult.diagnostics.map(diagnosticFromCheck),
|
|
10695
12187
|
...warningDiagnostics(read.warnings, read.unsupportedFields)
|
|
@@ -10699,11 +12191,11 @@ async function safetyCommand(command, target, options, stdin) {
|
|
|
10699
12191
|
});
|
|
10700
12192
|
} catch (error) {
|
|
10701
12193
|
const message = error instanceof Error ? error.message : String(error);
|
|
10702
|
-
result = message.startsWith("--") ? invalidArgumentResult(command, error) :
|
|
12194
|
+
result = message.startsWith("--") ? invalidArgumentResult(command, error) : readErrorResult3(command, error);
|
|
10703
12195
|
}
|
|
10704
|
-
process.exitCode =
|
|
10705
|
-
if (options.json)
|
|
10706
|
-
else
|
|
12196
|
+
process.exitCode = exitCodeFor3(result);
|
|
12197
|
+
if (options.json) printJson3(result);
|
|
12198
|
+
else printHuman3(result);
|
|
10707
12199
|
}
|
|
10708
12200
|
function scanCommand(target, options = {}, stdin = process.stdin) {
|
|
10709
12201
|
return safetyCommand("scan", target, options, stdin);
|
|
@@ -10723,16 +12215,16 @@ var SAFETY_RULES = [
|
|
|
10723
12215
|
maxSerializedBytes: 128 * 1024
|
|
10724
12216
|
})
|
|
10725
12217
|
];
|
|
10726
|
-
function
|
|
10727
|
-
if (Array.isArray(value)) return value.map(
|
|
12218
|
+
function stable5(value) {
|
|
12219
|
+
if (Array.isArray(value)) return value.map(stable5);
|
|
10728
12220
|
if (value === null || typeof value !== "object") return value;
|
|
10729
12221
|
const record = value;
|
|
10730
12222
|
return Object.fromEntries(
|
|
10731
|
-
Object.keys(record).sort((a, b) => a.localeCompare(b)).map((key) => [key,
|
|
12223
|
+
Object.keys(record).sort((a, b) => a.localeCompare(b)).map((key) => [key, stable5(record[key])])
|
|
10732
12224
|
);
|
|
10733
12225
|
}
|
|
10734
12226
|
function writeJson3(value) {
|
|
10735
|
-
return `${JSON.stringify(
|
|
12227
|
+
return `${JSON.stringify(stable5(value), null, 2)}
|
|
10736
12228
|
`;
|
|
10737
12229
|
}
|
|
10738
12230
|
function escapeHtml2(value) {
|
|
@@ -10749,7 +12241,7 @@ function increment(record, key) {
|
|
|
10749
12241
|
const label = key && key.trim() !== "" ? key : "unknown";
|
|
10750
12242
|
record[label] = (record[label] ?? 0) + 1;
|
|
10751
12243
|
}
|
|
10752
|
-
function
|
|
12244
|
+
function selectRun4(read, runId) {
|
|
10753
12245
|
if (runId !== void 0) {
|
|
10754
12246
|
return read.runs.find((run) => run.runId === runId);
|
|
10755
12247
|
}
|
|
@@ -10784,11 +12276,11 @@ function renderCheckSection(result) {
|
|
|
10784
12276
|
`Diagnostics: ${result.diagnostics.length}`
|
|
10785
12277
|
];
|
|
10786
12278
|
for (const finding of result.findings.slice(0, 10)) {
|
|
10787
|
-
const
|
|
10788
|
-
lines.push(`- ${finding.ruleId}: ${finding.message} (${
|
|
12279
|
+
const path15 = finding.evidence[0]?.path ?? "(run)";
|
|
12280
|
+
lines.push(`- ${finding.ruleId}: ${finding.message} (${path15})`);
|
|
10789
12281
|
}
|
|
10790
|
-
for (const
|
|
10791
|
-
lines.push(`- ${
|
|
12282
|
+
for (const diagnostic4 of result.diagnostics.slice(0, 10)) {
|
|
12283
|
+
lines.push(`- ${diagnostic4.code}: ${diagnostic4.message}`);
|
|
10792
12284
|
}
|
|
10793
12285
|
return lines.join("\n");
|
|
10794
12286
|
}
|
|
@@ -10863,8 +12355,8 @@ function renderHtml(trace, check, diff) {
|
|
|
10863
12355
|
`;
|
|
10864
12356
|
}
|
|
10865
12357
|
async function writeArtifact(outputDir, relativePath, content, files) {
|
|
10866
|
-
const outPath =
|
|
10867
|
-
await mkdir(
|
|
12358
|
+
const outPath = path13.join(outputDir, relativePath);
|
|
12359
|
+
await mkdir(path13.dirname(outPath), { recursive: true });
|
|
10868
12360
|
await writeFile(outPath, content, "utf-8");
|
|
10869
12361
|
files.push(relativePath);
|
|
10870
12362
|
}
|
|
@@ -10880,7 +12372,7 @@ function manifestStatus(check, diff) {
|
|
|
10880
12372
|
return "ok";
|
|
10881
12373
|
}
|
|
10882
12374
|
async function artifactsCommand(target, options = {}, stdin = process.stdin) {
|
|
10883
|
-
const outputDir = options.outputDir !== void 0 && options.outputDir.trim() !== "" ?
|
|
12375
|
+
const outputDir = options.outputDir !== void 0 && options.outputDir.trim() !== "" ? path13.resolve(options.outputDir.trim()) : "";
|
|
10884
12376
|
if (outputDir === "") {
|
|
10885
12377
|
console.error("--output-dir is required.");
|
|
10886
12378
|
process.exitCode = 1;
|
|
@@ -10897,7 +12389,7 @@ async function artifactsCommand(target, options = {}, stdin = process.stdin) {
|
|
|
10897
12389
|
process.exitCode = 1;
|
|
10898
12390
|
return;
|
|
10899
12391
|
}
|
|
10900
|
-
const selectedRun =
|
|
12392
|
+
const selectedRun = selectRun4(read, options.run);
|
|
10901
12393
|
const check = runTraceChecks(
|
|
10902
12394
|
{ read },
|
|
10903
12395
|
{
|
|
@@ -10946,8 +12438,8 @@ async function artifactsCommand(target, options = {}, stdin = process.stdin) {
|
|
|
10946
12438
|
await writeArtifact(outputDir, "report.html", renderHtml(trace, check, diff), files);
|
|
10947
12439
|
const summaryTarget = options.githubSummary ?? process.env.GITHUB_STEP_SUMMARY;
|
|
10948
12440
|
if (summaryTarget !== void 0 && summaryTarget.trim() !== "") {
|
|
10949
|
-
await mkdir(
|
|
10950
|
-
await appendFile(
|
|
12441
|
+
await mkdir(path13.dirname(path13.resolve(summaryTarget)), { recursive: true });
|
|
12442
|
+
await appendFile(path13.resolve(summaryTarget), `
|
|
10951
12443
|
${renderMarkdown(trace, check, diff)}`, "utf-8");
|
|
10952
12444
|
}
|
|
10953
12445
|
const manifestFiles = [...files, "manifest.json"].sort((a, b) => a.localeCompare(b));
|
|
@@ -10966,10 +12458,10 @@ ${renderMarkdown(trace, check, diff)}`, "utf-8");
|
|
|
10966
12458
|
findings: diff?.findings.length ?? 0,
|
|
10967
12459
|
diagnostics: diff?.diagnostics.length ?? 0
|
|
10968
12460
|
},
|
|
10969
|
-
...summaryTarget !== void 0 && summaryTarget.trim() !== "" ? { githubSummary:
|
|
12461
|
+
...summaryTarget !== void 0 && summaryTarget.trim() !== "" ? { githubSummary: path13.resolve(summaryTarget) } : {},
|
|
10970
12462
|
note: NOTE
|
|
10971
12463
|
};
|
|
10972
|
-
await writeFile(
|
|
12464
|
+
await writeFile(path13.join(outputDir, "manifest.json"), writeJson3(manifest), "utf-8");
|
|
10973
12465
|
if (options.json === true) {
|
|
10974
12466
|
console.log(writeJson3(manifest).trimEnd());
|
|
10975
12467
|
} else {
|
|
@@ -10980,6 +12472,371 @@ ${renderMarkdown(trace, check, diff)}`, "utf-8");
|
|
|
10980
12472
|
}
|
|
10981
12473
|
}
|
|
10982
12474
|
}
|
|
12475
|
+
function validateReporterArtifactPath(options) {
|
|
12476
|
+
const outputDir = path13.resolve(options.outputDir);
|
|
12477
|
+
const diagnostics = [];
|
|
12478
|
+
const rawPath = options.relativePath;
|
|
12479
|
+
if (rawPath.length === 0) {
|
|
12480
|
+
diagnostics.push({
|
|
12481
|
+
code: "artifact_path_empty",
|
|
12482
|
+
severity: "error",
|
|
12483
|
+
message: "Reporter artifact path must not be empty."
|
|
12484
|
+
});
|
|
12485
|
+
return { ok: false, outputDir, diagnostics };
|
|
12486
|
+
}
|
|
12487
|
+
if (rawPath.includes("\0")) {
|
|
12488
|
+
diagnostics.push({
|
|
12489
|
+
code: "invalid_artifact_path",
|
|
12490
|
+
severity: "error",
|
|
12491
|
+
message: "Reporter artifact path must not contain null bytes.",
|
|
12492
|
+
target: rawPath
|
|
12493
|
+
});
|
|
12494
|
+
return { ok: false, outputDir, diagnostics };
|
|
12495
|
+
}
|
|
12496
|
+
if (path13.isAbsolute(rawPath) || path13.win32.isAbsolute(rawPath)) {
|
|
12497
|
+
diagnostics.push({
|
|
12498
|
+
code: "artifact_path_absolute",
|
|
12499
|
+
severity: "error",
|
|
12500
|
+
message: "Reporter artifact path must be relative.",
|
|
12501
|
+
target: rawPath
|
|
12502
|
+
});
|
|
12503
|
+
return { ok: false, outputDir, diagnostics };
|
|
12504
|
+
}
|
|
12505
|
+
const normalized = path13.posix.normalize(rawPath.replace(/\\/g, "/"));
|
|
12506
|
+
const segments = normalized.split("/");
|
|
12507
|
+
if (normalized === "." || normalized.startsWith("../") || segments.some((segment) => segment === "..")) {
|
|
12508
|
+
diagnostics.push({
|
|
12509
|
+
code: "artifact_path_escape",
|
|
12510
|
+
severity: "error",
|
|
12511
|
+
message: "Reporter artifact path must stay under the output directory.",
|
|
12512
|
+
target: rawPath
|
|
12513
|
+
});
|
|
12514
|
+
return { ok: false, outputDir, diagnostics };
|
|
12515
|
+
}
|
|
12516
|
+
const absolutePath = path13.resolve(outputDir, normalized);
|
|
12517
|
+
const relFromOutput = path13.relative(outputDir, absolutePath);
|
|
12518
|
+
if (relFromOutput.length === 0 || relFromOutput.startsWith("..") || path13.isAbsolute(relFromOutput)) {
|
|
12519
|
+
diagnostics.push({
|
|
12520
|
+
code: "artifact_path_escape",
|
|
12521
|
+
severity: "error",
|
|
12522
|
+
message: "Reporter artifact path resolved outside the output directory.",
|
|
12523
|
+
target: rawPath
|
|
12524
|
+
});
|
|
12525
|
+
return { ok: false, outputDir, diagnostics };
|
|
12526
|
+
}
|
|
12527
|
+
return {
|
|
12528
|
+
ok: true,
|
|
12529
|
+
outputDir,
|
|
12530
|
+
relativePath: normalized,
|
|
12531
|
+
absolutePath,
|
|
12532
|
+
diagnostics
|
|
12533
|
+
};
|
|
12534
|
+
}
|
|
12535
|
+
|
|
12536
|
+
// packages/cli/src/ci-summary.ts
|
|
12537
|
+
var NOTE2 = "Generated locally by AgentInspect from reporter artifact manifests. Trace contents are not embedded.";
|
|
12538
|
+
var MAX_TEXT = 180;
|
|
12539
|
+
var FRAMEWORKS = /* @__PURE__ */ new Set(["vitest", "jest", "manual"]);
|
|
12540
|
+
var STATUSES = /* @__PURE__ */ new Set(["passed", "failed", "skipped", "todo"]);
|
|
12541
|
+
var ARTIFACT_KINDS = /* @__PURE__ */ new Set(["trace", "report", "eval", "redaction", "summary"]);
|
|
12542
|
+
var ARTIFACT_FORMATS = /* @__PURE__ */ new Set(["json", "jsonl", "md", "html"]);
|
|
12543
|
+
var REDACTION_PROFILES = /* @__PURE__ */ new Set(["local", "share", "strict"]);
|
|
12544
|
+
function stable6(value) {
|
|
12545
|
+
if (Array.isArray(value)) return value.map(stable6);
|
|
12546
|
+
if (value === null || typeof value !== "object") return value;
|
|
12547
|
+
const record = value;
|
|
12548
|
+
return Object.fromEntries(
|
|
12549
|
+
Object.keys(record).sort((a, b) => a.localeCompare(b)).map((key) => [key, stable6(record[key])])
|
|
12550
|
+
);
|
|
12551
|
+
}
|
|
12552
|
+
function writeJson4(value) {
|
|
12553
|
+
return `${JSON.stringify(stable6(value), null, 2)}
|
|
12554
|
+
`;
|
|
12555
|
+
}
|
|
12556
|
+
function isObject(value) {
|
|
12557
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
12558
|
+
}
|
|
12559
|
+
function readString(value, label) {
|
|
12560
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
12561
|
+
throw new Error(`${label} must be a non-empty string.`);
|
|
12562
|
+
}
|
|
12563
|
+
return safeText(value);
|
|
12564
|
+
}
|
|
12565
|
+
function readOptionalString(value) {
|
|
12566
|
+
return typeof value === "string" && value.trim() !== "" ? safeText(value) : void 0;
|
|
12567
|
+
}
|
|
12568
|
+
function readFramework(value) {
|
|
12569
|
+
const framework = readString(value, "manifest.framework");
|
|
12570
|
+
if (!FRAMEWORKS.has(framework)) {
|
|
12571
|
+
throw new Error(`Unsupported reporter framework: ${framework}.`);
|
|
12572
|
+
}
|
|
12573
|
+
return framework;
|
|
12574
|
+
}
|
|
12575
|
+
function readStatus(value) {
|
|
12576
|
+
const status = readString(value, "result.status");
|
|
12577
|
+
if (!STATUSES.has(status)) {
|
|
12578
|
+
throw new Error(`Unsupported reporter test status: ${status}.`);
|
|
12579
|
+
}
|
|
12580
|
+
return status;
|
|
12581
|
+
}
|
|
12582
|
+
function readArtifact(value, index) {
|
|
12583
|
+
if (!isObject(value)) throw new Error(`manifest.artifacts[${index}] must be an object.`);
|
|
12584
|
+
const kind = readString(value.kind, `manifest.artifacts[${index}].kind`);
|
|
12585
|
+
const format = readString(value.format, `manifest.artifacts[${index}].format`);
|
|
12586
|
+
const redactionProfile = readString(
|
|
12587
|
+
value.redactionProfile,
|
|
12588
|
+
`manifest.artifacts[${index}].redactionProfile`
|
|
12589
|
+
);
|
|
12590
|
+
if (!ARTIFACT_KINDS.has(kind)) throw new Error(`Unsupported artifact kind: ${kind}.`);
|
|
12591
|
+
if (!ARTIFACT_FORMATS.has(format)) throw new Error(`Unsupported artifact format: ${format}.`);
|
|
12592
|
+
if (!REDACTION_PROFILES.has(redactionProfile)) {
|
|
12593
|
+
throw new Error(`Unsupported artifact redaction profile: ${redactionProfile}.`);
|
|
12594
|
+
}
|
|
12595
|
+
const artifactPath = readString(value.path, `manifest.artifacts[${index}].path`);
|
|
12596
|
+
const pathCheck = validateReporterArtifactPath({
|
|
12597
|
+
outputDir: process.cwd(),
|
|
12598
|
+
relativePath: artifactPath
|
|
12599
|
+
});
|
|
12600
|
+
if (!pathCheck.ok || pathCheck.relativePath === void 0) {
|
|
12601
|
+
throw new Error(`Unsafe reporter artifact path: ${artifactPath}.`);
|
|
12602
|
+
}
|
|
12603
|
+
return {
|
|
12604
|
+
kind,
|
|
12605
|
+
path: pathCheck.relativePath,
|
|
12606
|
+
format,
|
|
12607
|
+
redactionProfile
|
|
12608
|
+
};
|
|
12609
|
+
}
|
|
12610
|
+
function readArtifacts(value, label) {
|
|
12611
|
+
if (value === void 0) return [];
|
|
12612
|
+
if (!Array.isArray(value)) throw new Error(`${label} must be an array.`);
|
|
12613
|
+
return value.map((item, index) => readArtifact(item, index));
|
|
12614
|
+
}
|
|
12615
|
+
function readDiagnosticsCount(value, label) {
|
|
12616
|
+
if (value === void 0) return 0;
|
|
12617
|
+
if (!Array.isArray(value)) throw new Error(`${label} must be an array.`);
|
|
12618
|
+
return value.length;
|
|
12619
|
+
}
|
|
12620
|
+
function readResult(value, index) {
|
|
12621
|
+
if (!isObject(value)) throw new Error(`manifest.results[${index}] must be an object.`);
|
|
12622
|
+
const file = readOptionalString(value.file);
|
|
12623
|
+
const tracePath = readOptionalString(value.tracePath);
|
|
12624
|
+
return {
|
|
12625
|
+
testId: readString(value.testId, `manifest.results[${index}].testId`),
|
|
12626
|
+
name: readString(value.name, `manifest.results[${index}].name`),
|
|
12627
|
+
...file === void 0 ? {} : { file },
|
|
12628
|
+
status: readStatus(value.status),
|
|
12629
|
+
...tracePath === void 0 ? {} : { tracePath },
|
|
12630
|
+
artifacts: readArtifacts(value.artifacts, `manifest.results[${index}].artifacts`),
|
|
12631
|
+
diagnostics: readDiagnosticsCount(
|
|
12632
|
+
value.diagnostics,
|
|
12633
|
+
`manifest.results[${index}].diagnostics`
|
|
12634
|
+
)
|
|
12635
|
+
};
|
|
12636
|
+
}
|
|
12637
|
+
function readManifestDocument(value) {
|
|
12638
|
+
if (!isObject(value)) throw new Error("Reporter manifest file must contain a JSON object.");
|
|
12639
|
+
const candidate = isObject(value.manifest) ? value.manifest : value;
|
|
12640
|
+
if (!isObject(candidate)) throw new Error("Reporter manifest must be a JSON object.");
|
|
12641
|
+
const schemaVersion = readString(candidate.schemaVersion, "manifest.schemaVersion");
|
|
12642
|
+
if (schemaVersion !== "0.1") {
|
|
12643
|
+
throw new Error(`Unsupported reporter manifest schemaVersion: ${schemaVersion}.`);
|
|
12644
|
+
}
|
|
12645
|
+
if (!Array.isArray(candidate.results)) {
|
|
12646
|
+
throw new Error("manifest.results must be an array.");
|
|
12647
|
+
}
|
|
12648
|
+
const artifacts = readArtifacts(candidate.artifacts, "manifest.artifacts");
|
|
12649
|
+
const results = candidate.results.map((item, index) => readResult(item, index));
|
|
12650
|
+
const diagnostics = readDiagnosticsCount(candidate.diagnostics, "manifest.diagnostics");
|
|
12651
|
+
const manifest = {
|
|
12652
|
+
framework: readFramework(candidate.framework),
|
|
12653
|
+
generatedAt: readString(candidate.generatedAt, "manifest.generatedAt"),
|
|
12654
|
+
results,
|
|
12655
|
+
artifacts
|
|
12656
|
+
};
|
|
12657
|
+
return {
|
|
12658
|
+
packageName: readOptionalString(value.package),
|
|
12659
|
+
manifest,
|
|
12660
|
+
diagnostics
|
|
12661
|
+
};
|
|
12662
|
+
}
|
|
12663
|
+
function cwdRelative(filePath) {
|
|
12664
|
+
const relative = path13.relative(process.cwd(), path13.resolve(filePath)).replace(/\\/g, "/");
|
|
12665
|
+
if (relative === "" || relative.startsWith("../") || path13.isAbsolute(relative)) {
|
|
12666
|
+
return path13.basename(filePath);
|
|
12667
|
+
}
|
|
12668
|
+
return relative;
|
|
12669
|
+
}
|
|
12670
|
+
async function readReporterManifest(filePath) {
|
|
12671
|
+
const absolute = path13.resolve(filePath);
|
|
12672
|
+
const raw = await readFile(absolute, "utf-8");
|
|
12673
|
+
const document = readManifestDocument(JSON.parse(raw));
|
|
12674
|
+
const manifest = document.manifest;
|
|
12675
|
+
const results = manifest.results.map((result) => ({
|
|
12676
|
+
testId: safeText(result.testId),
|
|
12677
|
+
name: safeText(result.name),
|
|
12678
|
+
...result.file === void 0 ? {} : { file: safeText(path13.basename(result.file)) },
|
|
12679
|
+
status: result.status,
|
|
12680
|
+
...result.tracePath === void 0 ? {} : { tracePath: safeText(path13.basename(result.tracePath)) },
|
|
12681
|
+
artifacts: result.artifacts,
|
|
12682
|
+
diagnostics: result.diagnostics
|
|
12683
|
+
}));
|
|
12684
|
+
return {
|
|
12685
|
+
...document.packageName === void 0 ? {} : { packageName: document.packageName },
|
|
12686
|
+
manifestFile: cwdRelative(absolute),
|
|
12687
|
+
framework: manifest.framework,
|
|
12688
|
+
generatedAt: manifest.generatedAt,
|
|
12689
|
+
results,
|
|
12690
|
+
artifacts: manifest.artifacts,
|
|
12691
|
+
diagnostics: document.diagnostics
|
|
12692
|
+
};
|
|
12693
|
+
}
|
|
12694
|
+
function summarize3(manifests) {
|
|
12695
|
+
const summary = {
|
|
12696
|
+
manifests: manifests.length,
|
|
12697
|
+
tests: 0,
|
|
12698
|
+
failed: 0,
|
|
12699
|
+
passed: 0,
|
|
12700
|
+
skipped: 0,
|
|
12701
|
+
todo: 0,
|
|
12702
|
+
artifacts: 0,
|
|
12703
|
+
diagnostics: 0
|
|
12704
|
+
};
|
|
12705
|
+
for (const manifest of manifests) {
|
|
12706
|
+
summary.artifacts += manifest.artifacts.length;
|
|
12707
|
+
summary.diagnostics += manifest.diagnostics;
|
|
12708
|
+
for (const result of manifest.results) {
|
|
12709
|
+
summary.tests += 1;
|
|
12710
|
+
if (result.status === "failed") summary.failed += 1;
|
|
12711
|
+
else if (result.status === "passed") summary.passed += 1;
|
|
12712
|
+
else if (result.status === "skipped") summary.skipped += 1;
|
|
12713
|
+
else summary.todo += 1;
|
|
12714
|
+
summary.artifacts += result.artifacts.length;
|
|
12715
|
+
summary.diagnostics += result.diagnostics;
|
|
12716
|
+
}
|
|
12717
|
+
}
|
|
12718
|
+
return {
|
|
12719
|
+
status: summary.failed > 0 ? "failed" : summary.diagnostics > 0 ? "warning" : "ok",
|
|
12720
|
+
manifests,
|
|
12721
|
+
summary,
|
|
12722
|
+
note: NOTE2
|
|
12723
|
+
};
|
|
12724
|
+
}
|
|
12725
|
+
function markdownCell(value) {
|
|
12726
|
+
return safeText(String(value ?? "unknown")).replaceAll("|", "\\|").replace(/\r?\n/g, " ");
|
|
12727
|
+
}
|
|
12728
|
+
function renderMarkdown2(result) {
|
|
12729
|
+
const lines = [
|
|
12730
|
+
"# AgentInspect CI Summary",
|
|
12731
|
+
"",
|
|
12732
|
+
NOTE2,
|
|
12733
|
+
"",
|
|
12734
|
+
"| Field | Value |",
|
|
12735
|
+
"| --- | --- |",
|
|
12736
|
+
`| Status | ${result.status} |`,
|
|
12737
|
+
`| Manifests | ${result.summary.manifests} |`,
|
|
12738
|
+
`| Tests | ${result.summary.tests} |`,
|
|
12739
|
+
`| Failed | ${result.summary.failed} |`,
|
|
12740
|
+
`| Passed | ${result.summary.passed} |`,
|
|
12741
|
+
`| Skipped | ${result.summary.skipped} |`,
|
|
12742
|
+
`| Todo | ${result.summary.todo} |`,
|
|
12743
|
+
`| Artifacts | ${result.summary.artifacts} |`,
|
|
12744
|
+
`| Diagnostics | ${result.summary.diagnostics} |`,
|
|
12745
|
+
"",
|
|
12746
|
+
"## Tests",
|
|
12747
|
+
"",
|
|
12748
|
+
"| Framework | Status | Test | File | Trace | Artifacts |",
|
|
12749
|
+
"| --- | --- | --- | --- | --- | --- |"
|
|
12750
|
+
];
|
|
12751
|
+
const rows = result.manifests.flatMap(
|
|
12752
|
+
(manifest) => manifest.results.map((test) => ({
|
|
12753
|
+
framework: manifest.framework,
|
|
12754
|
+
test
|
|
12755
|
+
}))
|
|
12756
|
+
);
|
|
12757
|
+
if (rows.length === 0) {
|
|
12758
|
+
lines.push("| unknown | unknown | No tests found | unknown | unknown | 0 |");
|
|
12759
|
+
} else {
|
|
12760
|
+
for (const row of rows) {
|
|
12761
|
+
lines.push(
|
|
12762
|
+
`| ${markdownCell(row.framework)} | ${markdownCell(row.test.status)} | ${markdownCell(row.test.name)} | ${markdownCell(row.test.file)} | ${markdownCell(row.test.tracePath)} | ${row.test.artifacts.length} |`
|
|
12763
|
+
);
|
|
12764
|
+
}
|
|
12765
|
+
}
|
|
12766
|
+
lines.push("", "## Manifests", "", "| Framework | File | Generated | Artifacts |", "| --- | --- | --- | --- |");
|
|
12767
|
+
for (const manifest of result.manifests) {
|
|
12768
|
+
lines.push(
|
|
12769
|
+
`| ${markdownCell(manifest.framework)} | ${markdownCell(manifest.manifestFile)} | ${markdownCell(manifest.generatedAt)} | ${manifest.artifacts.length} |`
|
|
12770
|
+
);
|
|
12771
|
+
}
|
|
12772
|
+
lines.push("", "## Artifacts", "", "| Framework | Kind | Path | Format | Profile |", "| --- | --- | --- | --- | --- |");
|
|
12773
|
+
const artifacts = result.manifests.flatMap(
|
|
12774
|
+
(manifest) => manifest.artifacts.map((artifact) => ({ framework: manifest.framework, artifact }))
|
|
12775
|
+
);
|
|
12776
|
+
if (artifacts.length === 0) {
|
|
12777
|
+
lines.push("| unknown | unknown | No artifacts found | unknown | unknown |");
|
|
12778
|
+
} else {
|
|
12779
|
+
for (const row of artifacts) {
|
|
12780
|
+
lines.push(
|
|
12781
|
+
`| ${markdownCell(row.framework)} | ${markdownCell(row.artifact.kind)} | ${markdownCell(row.artifact.path)} | ${markdownCell(row.artifact.format)} | ${markdownCell(row.artifact.redactionProfile)} |`
|
|
12782
|
+
);
|
|
12783
|
+
}
|
|
12784
|
+
}
|
|
12785
|
+
lines.push("");
|
|
12786
|
+
return `${lines.join("\n")}
|
|
12787
|
+
`;
|
|
12788
|
+
}
|
|
12789
|
+
function safeText(value) {
|
|
12790
|
+
const compact = value.replace(/\s+/g, " ").trim();
|
|
12791
|
+
const redacted = compact.replace(/\bsk-[A-Za-z0-9_-]{8,}\b/g, "[REDACTED]").replace(
|
|
12792
|
+
/\b(?:api[_-]?key|authorization|token|secret|password)\s*[:=]\s*[^,\s]+/gi,
|
|
12793
|
+
"$1=[REDACTED]"
|
|
12794
|
+
);
|
|
12795
|
+
if (redacted.length <= MAX_TEXT) return redacted;
|
|
12796
|
+
return `${redacted.slice(0, MAX_TEXT - 12)}...[truncated]`;
|
|
12797
|
+
}
|
|
12798
|
+
async function ciSummaryCommand(manifestPaths, options = {}) {
|
|
12799
|
+
if (manifestPaths.length === 0) {
|
|
12800
|
+
console.error("At least one reporter manifest path is required.");
|
|
12801
|
+
process.exitCode = 1;
|
|
12802
|
+
return;
|
|
12803
|
+
}
|
|
12804
|
+
let result;
|
|
12805
|
+
try {
|
|
12806
|
+
const manifests = [];
|
|
12807
|
+
for (const manifestPath of manifestPaths) {
|
|
12808
|
+
manifests.push(await readReporterManifest(manifestPath));
|
|
12809
|
+
}
|
|
12810
|
+
manifests.sort((a, b) => a.manifestFile.localeCompare(b.manifestFile));
|
|
12811
|
+
result = summarize3(manifests);
|
|
12812
|
+
} catch (error) {
|
|
12813
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
12814
|
+
console.error(`[AgentInspect] ci-summary failed: ${message}`);
|
|
12815
|
+
process.exitCode = 1;
|
|
12816
|
+
return;
|
|
12817
|
+
}
|
|
12818
|
+
const markdown = renderMarkdown2(result);
|
|
12819
|
+
const outputPath = options.output !== void 0 && options.output.trim() !== "" ? path13.resolve(options.output.trim()) : void 0;
|
|
12820
|
+
if (outputPath !== void 0) {
|
|
12821
|
+
await mkdir(path13.dirname(outputPath), { recursive: true });
|
|
12822
|
+
await writeFile(outputPath, markdown, "utf-8");
|
|
12823
|
+
}
|
|
12824
|
+
const summaryTarget = options.githubSummary ?? process.env.GITHUB_STEP_SUMMARY;
|
|
12825
|
+
if (summaryTarget !== void 0 && summaryTarget.trim() !== "") {
|
|
12826
|
+
const summaryPath = path13.resolve(summaryTarget);
|
|
12827
|
+
await mkdir(path13.dirname(summaryPath), { recursive: true });
|
|
12828
|
+
await appendFile(summaryPath, `
|
|
12829
|
+
${markdown}`, "utf-8");
|
|
12830
|
+
}
|
|
12831
|
+
if (options.json === true) {
|
|
12832
|
+
console.log(writeJson4(result).trimEnd());
|
|
12833
|
+
} else if (outputPath !== void 0) {
|
|
12834
|
+
console.log(`Wrote AgentInspect CI summary to ${outputPath}`);
|
|
12835
|
+
console.log(`Status: ${result.status}`);
|
|
12836
|
+
} else {
|
|
12837
|
+
console.log(markdown.trimEnd());
|
|
12838
|
+
}
|
|
12839
|
+
}
|
|
10983
12840
|
|
|
10984
12841
|
// packages/cli/src/index.ts
|
|
10985
12842
|
function runCommand(action) {
|
|
@@ -11108,6 +12965,36 @@ function createCliProgram() {
|
|
|
11108
12965
|
]).option("--max-total-tokens <number>", "add llm.usage with a max total-token budget").action((target, opts) => {
|
|
11109
12966
|
runCommand(() => checkCommand(target, opts));
|
|
11110
12967
|
});
|
|
12968
|
+
program.command("eval").description("Run deterministic local evals against a trace").argument("<trace-path-or-run-id>", "trace file, directory, stdin -, or run id").option("--dir <path>", "trace directory for run-id lookup").addOption(
|
|
12969
|
+
new Option("--format <format>", "trace input format").choices([
|
|
12970
|
+
"agent-inspect-jsonl",
|
|
12971
|
+
"openinference-json",
|
|
12972
|
+
"otlp-json"
|
|
12973
|
+
])
|
|
12974
|
+
).option("--run <run-id>", "select a run when the trace contains multiple runs").option("--config <path>", "path to eval config (.json, .js, .mjs, .cjs)").option("--json", "print deterministic JSON eval result").option("--markdown", "print deterministic Markdown eval summary").option("--require-success", "require the selected run to complete successfully").option("--required-tool <name>", "require a tool name (repeatable)", (value, previous = []) => [
|
|
12975
|
+
...previous,
|
|
12976
|
+
value
|
|
12977
|
+
]).option("--forbid-tool <name>", "forbid a tool name (repeatable)", (value, previous = []) => [
|
|
12978
|
+
...previous,
|
|
12979
|
+
value
|
|
12980
|
+
]).option("--forbidden-tool <name>", "alias for --forbid-tool (repeatable)", (value, previous = []) => [
|
|
12981
|
+
...previous,
|
|
12982
|
+
value
|
|
12983
|
+
]).option("--max-duration-ms <number>", "require run duration at or below this value").option("--max-depth <number>", "require tree depth at or below this value").option("--max-retries <number>", "require retry counts at or below this value").option("--max-total-tokens <number>", "require total LLM tokens at or below this value").option(
|
|
12984
|
+
"--require-retrieval-before-generation",
|
|
12985
|
+
"require a retrieval step before the first LLM generation"
|
|
12986
|
+
).option("--required-decision-metadata <key>", "require decision metadata (repeatable)", (value, previous = []) => [
|
|
12987
|
+
...previous,
|
|
12988
|
+
value
|
|
12989
|
+
]).option("--context-overlap", "require answer/context token overlap").option("--min-context-overlap <number>", "minimum answer/context overlap ratio").option("--min-shared-terms <number>", "minimum shared answer/context terms").option("--quote-overlap", "require quoted answer text to appear in context").option("--citation-presence", "require a citation or source reference").option("--required-source-id <id>", "require a source id in context or citations (repeatable)", (value, previous = []) => [
|
|
12990
|
+
...previous,
|
|
12991
|
+
value
|
|
12992
|
+
]).option("--min-answer-characters <number>", "minimum answer character count").option("--max-answer-characters <number>", "maximum answer character count").option("--min-answer-words <number>", "minimum answer word count").option("--max-answer-words <number>", "maximum answer word count").option("--banned-phrase <text>", "ban unsupported-answer phrasing (repeatable)", (value, previous = []) => [
|
|
12993
|
+
...previous,
|
|
12994
|
+
value
|
|
12995
|
+
]).action((target, opts) => {
|
|
12996
|
+
runCommand(() => evalCommand(target, opts));
|
|
12997
|
+
});
|
|
11111
12998
|
program.command("scan").description("Best-effort local safety scan for trace capture risks").argument("<trace-path-or-run-id>", "trace file, directory, stdin -, or run id").option("--dir <path>", "trace directory for run-id lookup").addOption(
|
|
11112
12999
|
new Option("--format <format>", "trace input format").choices([
|
|
11113
13000
|
"agent-inspect-jsonl",
|
|
@@ -11135,6 +13022,9 @@ function createCliProgram() {
|
|
|
11135
13022
|
).option("--run <run-id>", "select a run when the trace contains multiple runs").option("--baseline <trace-path-or-run-id>", "optional baseline trace for diff artifacts").option("--baseline-run <run-id>", "select a run from the baseline trace").option("--github-summary <path>", "append a safe summary to this file, e.g. GITHUB_STEP_SUMMARY").option("--json", "print deterministic JSON manifest").action((target, opts) => {
|
|
11136
13023
|
runCommand(() => artifactsCommand(target, opts));
|
|
11137
13024
|
});
|
|
13025
|
+
program.command("ci-summary").description("Summarize local reporter artifact manifests for CI").argument("<manifest...>", "reporter artifact manifest JSON files").option("-o, --output <path>", "write Markdown summary to a local file").option("--github-summary <path>", "append Markdown summary to this local file, e.g. GITHUB_STEP_SUMMARY").option("--json", "print deterministic JSON summary").action((manifest, opts) => {
|
|
13026
|
+
runCommand(() => ciSummaryCommand(manifest, opts));
|
|
13027
|
+
});
|
|
11138
13028
|
program.command("diff").description("Compare two local AgentInspect JSONL traces (read-only)").argument("<left-run-id>", "first run id").argument("<right-run-id>", "second run id").option("--dir <path>", "trace directory").option("--json", "print diff result as JSON").option("--ignore-duration", "omit duration comparisons").option(
|
|
11139
13029
|
"--duration-threshold <duration>",
|
|
11140
13030
|
"ignore duration deltas at or below this (e.g. 500ms, 2s, 1m)"
|
|
@@ -11195,6 +13085,14 @@ function createCliProgram() {
|
|
|
11195
13085
|
).action((runId, opts) => {
|
|
11196
13086
|
runCommand(() => reportCommand(runId, opts));
|
|
11197
13087
|
});
|
|
13088
|
+
program.command("redact").description("Redact a local JSON or JSONL trace/file").argument("<trace-or-file>", "trace file, JSON file, stdin -, or run id").option("--dir <path>", "trace directory for run-id lookup").addOption(
|
|
13089
|
+
new Option(
|
|
13090
|
+
"--profile <profile>",
|
|
13091
|
+
"redaction profile: local, share, strict (default: share)"
|
|
13092
|
+
).choices(["local", "share", "strict"])
|
|
13093
|
+
).option("-o, --output <path>", "write redacted content to a file").option("--json", "print deterministic JSON wrapper with findings").action((target, opts) => {
|
|
13094
|
+
runCommand(() => redactCommand(target, opts));
|
|
13095
|
+
});
|
|
11198
13096
|
program.command("explain").description("Explain a local trace with deterministic facts (no provider calls)").argument("<trace-path-or-run-id>", "trace file, directory, stdin -, or run id").option("--dir <path>", "trace directory for run-id lookup").addOption(
|
|
11199
13097
|
new Option("--format <format>", "trace input format").choices([
|
|
11200
13098
|
"agent-inspect-jsonl",
|
|
@@ -11219,9 +13117,9 @@ function isPrimaryModule() {
|
|
|
11219
13117
|
if (!entry) return false;
|
|
11220
13118
|
const selfPath = fileURLToPath(import.meta.url);
|
|
11221
13119
|
try {
|
|
11222
|
-
return realpathSync(
|
|
13120
|
+
return realpathSync(path13.resolve(entry)) === realpathSync(path13.resolve(selfPath));
|
|
11223
13121
|
} catch {
|
|
11224
|
-
return
|
|
13122
|
+
return path13.resolve(entry) === path13.resolve(selfPath);
|
|
11225
13123
|
}
|
|
11226
13124
|
}
|
|
11227
13125
|
if (isPrimaryModule()) {
|