agent-workflow-inspector 1.0.0 → 2.0.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 +12 -2
- package/LICENSE +21 -21
- package/README.md +297 -51
- package/SECURITY.md +1 -1
- package/awi.config.example.json +5 -0
- package/dist/cli/index.js +173 -14
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +29 -5
- package/dist/index.js +112 -24
- package/dist/index.js.map +1 -1
- package/docs/architecture.md +1 -1
- package/docs/assets/architecture.svg +139 -0
- package/docs/assets/banner.svg +105 -0
- package/docs/assets/detection-patterns.svg +134 -0
- package/docs/assets/terminal-preview.svg +93 -0
- package/docs/ci-integration.md +33 -5
- package/docs/configuration.md +2 -0
- package/docs/detection-rules.md +11 -1
- package/docs/getting-started.md +43 -2
- package/docs/limitations.md +4 -0
- package/docs/releases/v1.0.0.md +3 -3
- package/docs/releasing.md +16 -105
- package/docs/report-schema.md +14 -7
- package/docs/schemas/config.schema.json +40 -2
- package/docs/schemas/report.schema.json +49 -8
- package/docs/security.md +26 -3
- package/docs/verification.md +9 -3
- package/examples/argument-drift.config.json +9 -0
- package/examples/suspicious/argument-drift.json +62 -0
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -269,8 +269,8 @@ function otel(value) {
|
|
|
269
269
|
if (++total > LIMITS.spans)
|
|
270
270
|
throw new AwiError("Maximum span count exceeded");
|
|
271
271
|
const s = record(raw);
|
|
272
|
-
const
|
|
273
|
-
if (!
|
|
272
|
+
const traceId2 = string(s.traceId), id = string(s.spanId);
|
|
273
|
+
if (!traceId2 || !/^[a-fA-F0-9]{32}$/.test(traceId2) || /^0+$/.test(traceId2) || !id || !/^[a-fA-F0-9]{16}$/.test(id) || /^0+$/.test(id))
|
|
274
274
|
throw new AwiError("Invalid OTLP trace/span identity");
|
|
275
275
|
const a = attributes(s.attributes);
|
|
276
276
|
const kind = textAttribute(s.attributes, "gen_ai.operation.name");
|
|
@@ -345,7 +345,7 @@ function otel(value) {
|
|
|
345
345
|
source: { format: "otel", index: total - 1 }
|
|
346
346
|
};
|
|
347
347
|
if (args !== void 0) validateValue(args);
|
|
348
|
-
const key =
|
|
348
|
+
const key = traceId2.toLowerCase();
|
|
349
349
|
if (!grouped.has(key))
|
|
350
350
|
grouped.set(key, { version: 1, traceId: key, operations: [] });
|
|
351
351
|
grouped.get(key).operations.push(op);
|
|
@@ -426,14 +426,20 @@ var configSchema = z2.strictObject({
|
|
|
426
426
|
maxOperations: positive.max(5e4).default(100),
|
|
427
427
|
maxDepth: positive.max(1e3).default(20)
|
|
428
428
|
}).default({ enabled: true, maxOperations: 100, maxDepth: 20 }),
|
|
429
|
-
AWI006: z2.strictObject({ enabled: z2.boolean().default(true) }).default({ enabled: true })
|
|
429
|
+
AWI006: z2.strictObject({ enabled: z2.boolean().default(true) }).default({ enabled: true }),
|
|
430
|
+
AWI007: z2.strictObject({
|
|
431
|
+
enabled: z2.boolean().default(false),
|
|
432
|
+
threshold: positive.min(4).max(LIMITS.repetitionWindow).default(4),
|
|
433
|
+
minDistinctArguments: positive.min(3).max(LIMITS.repetitionWindow).default(3)
|
|
434
|
+
}).default({ enabled: false, threshold: 4, minDistinctArguments: 3 })
|
|
430
435
|
}).default({
|
|
431
436
|
AWI001: { enabled: true, threshold: 4 },
|
|
432
437
|
AWI002: { enabled: true, threshold: 3 },
|
|
433
438
|
AWI003: { enabled: true, minRepetitions: 3, maxCycleLength: 6 },
|
|
434
439
|
AWI004: { enabled: true, threshold: 5, windowSeconds: 30 },
|
|
435
440
|
AWI005: { enabled: true, maxOperations: 100, maxDepth: 20 },
|
|
436
|
-
AWI006: { enabled: true }
|
|
441
|
+
AWI006: { enabled: true },
|
|
442
|
+
AWI007: { enabled: false, threshold: 4, minDistinctArguments: 3 }
|
|
437
443
|
}),
|
|
438
444
|
fingerprint: z2.strictObject({
|
|
439
445
|
ignorePaths: z2.array(
|
|
@@ -616,7 +622,8 @@ var titles = {
|
|
|
616
622
|
AWI003: "Repeating execution cycle",
|
|
617
623
|
AWI004: "Retry storm",
|
|
618
624
|
AWI005: "Excessive execution steps or depth",
|
|
619
|
-
AWI006: "Recorded resource usage associated with suspicious execution"
|
|
625
|
+
AWI006: "Recorded resource usage associated with suspicious execution",
|
|
626
|
+
AWI007: "Repeated failure with argument drift"
|
|
620
627
|
};
|
|
621
628
|
function analyze(traces, configInput = {}, showContent = false) {
|
|
622
629
|
const config = resolveConfig(configInput);
|
|
@@ -666,7 +673,7 @@ function analyze(traces, configInput = {}, showContent = false) {
|
|
|
666
673
|
validated.push(result.data);
|
|
667
674
|
}
|
|
668
675
|
const report = {
|
|
669
|
-
version:
|
|
676
|
+
version: 2,
|
|
670
677
|
fingerprintVersion: "awi-sha256-v1",
|
|
671
678
|
complete: true,
|
|
672
679
|
summaries: [],
|
|
@@ -680,20 +687,20 @@ function analyze(traces, configInput = {}, showContent = false) {
|
|
|
680
687
|
else grouped.set(t.traceId, { ...t, operations: [...t.operations] });
|
|
681
688
|
}
|
|
682
689
|
let capped = false;
|
|
683
|
-
function add(ruleId,
|
|
690
|
+
function add(ruleId, traceId2, ops, evidence, explanation, confidence = "moderate") {
|
|
684
691
|
if (report.findings.length >= LIMITS.findings) {
|
|
685
692
|
capped = true;
|
|
686
693
|
return;
|
|
687
694
|
}
|
|
688
695
|
report.findings.push({
|
|
689
696
|
ruleId,
|
|
690
|
-
traceId,
|
|
697
|
+
traceId: traceId2,
|
|
691
698
|
severity: ruleId === "AWI002" || ruleId === "AWI004" ? "high" : "medium",
|
|
692
699
|
confidence,
|
|
693
700
|
title: titles[ruleId],
|
|
694
701
|
description: "Heuristic finding based on recorded execution evidence; intentional behavior may match.",
|
|
695
702
|
explanation,
|
|
696
|
-
remediation: "Review the referenced operations and configured policy before changing the workflow.",
|
|
703
|
+
remediation: ruleId === "AWI007" ? "Review the shared error code and retry policy; consider bounded retries or escalation before varying arguments further." : "Review the referenced operations and configured policy before changing the workflow.",
|
|
697
704
|
operationIds: ops.map((o) => o.id),
|
|
698
705
|
evidence: {
|
|
699
706
|
...evidence,
|
|
@@ -704,9 +711,10 @@ function analyze(traces, configInput = {}, showContent = false) {
|
|
|
704
711
|
}
|
|
705
712
|
});
|
|
706
713
|
}
|
|
707
|
-
|
|
708
|
-
(a, b) => a.traceId
|
|
709
|
-
)
|
|
714
|
+
const orderedTraces = [...grouped.values()].sort(
|
|
715
|
+
(a, b) => compare(a.traceId, b.traceId)
|
|
716
|
+
);
|
|
717
|
+
for (const trace of orderedTraces) {
|
|
710
718
|
trace.operations.sort((a, b) => compare(a.id, b.id));
|
|
711
719
|
const normalized = normalize(trace);
|
|
712
720
|
report.warnings.push(...normalized.warnings);
|
|
@@ -773,6 +781,43 @@ function analyze(traces, configInput = {}, showContent = false) {
|
|
|
773
781
|
"Overlapping sibling operations excluded from sequence rules."
|
|
774
782
|
);
|
|
775
783
|
for (const run of runs) {
|
|
784
|
+
if (config.rules.AWI007.enabled) {
|
|
785
|
+
const policy2 = config.rules.AWI007;
|
|
786
|
+
let failures = [];
|
|
787
|
+
const distinct = /* @__PURE__ */ new Set();
|
|
788
|
+
let tool;
|
|
789
|
+
let code;
|
|
790
|
+
const flush = () => {
|
|
791
|
+
if (failures.length >= policy2.threshold && distinct.size >= policy2.minDistinctArguments)
|
|
792
|
+
add(
|
|
793
|
+
"AWI007",
|
|
794
|
+
trace.traceId,
|
|
795
|
+
failures,
|
|
796
|
+
{
|
|
797
|
+
failedCalls: failures.length,
|
|
798
|
+
distinctArgumentFingerprints: distinct.size
|
|
799
|
+
},
|
|
800
|
+
"Consecutive recorded failures share a tool and explicit error code while argument fingerprints vary. This does not establish intent or lack of progress."
|
|
801
|
+
);
|
|
802
|
+
failures = [];
|
|
803
|
+
distinct.clear();
|
|
804
|
+
};
|
|
805
|
+
for (const op of run) {
|
|
806
|
+
if (op.type !== "tool") continue;
|
|
807
|
+
const fp = fps.get(op.id);
|
|
808
|
+
if (op.status !== "error" || !op.error?.code?.trim() || op.arguments === void 0 || !fp) {
|
|
809
|
+
flush();
|
|
810
|
+
continue;
|
|
811
|
+
}
|
|
812
|
+
const name = op.toolName ?? op.name;
|
|
813
|
+
if (name !== tool || op.error.code !== code) flush();
|
|
814
|
+
tool = name;
|
|
815
|
+
code = op.error.code;
|
|
816
|
+
failures.push(op);
|
|
817
|
+
distinct.add(fp);
|
|
818
|
+
}
|
|
819
|
+
flush();
|
|
820
|
+
}
|
|
776
821
|
let segment = [];
|
|
777
822
|
const segments = [];
|
|
778
823
|
for (const op of run) {
|
|
@@ -927,7 +972,12 @@ function analyze(traces, configInput = {}, showContent = false) {
|
|
|
927
972
|
recorded,
|
|
928
973
|
{
|
|
929
974
|
recordedUsage: tokens,
|
|
930
|
-
recordedCosts:
|
|
975
|
+
recordedCosts: Object.fromEntries(
|
|
976
|
+
Object.keys(costs).sort(compare).map((currency, index) => [
|
|
977
|
+
`currency-${index + 1}`,
|
|
978
|
+
costs[currency]
|
|
979
|
+
])
|
|
980
|
+
),
|
|
931
981
|
coverage: "Only implicated leaf operations with recorded usage or cost",
|
|
932
982
|
relatedRules: [...new Set(related.map((f) => f.ruleId))]
|
|
933
983
|
},
|
|
@@ -943,6 +993,28 @@ function analyze(traces, configInput = {}, showContent = false) {
|
|
|
943
993
|
report.findings.sort(
|
|
944
994
|
(a, b) => severityRank[b.severity] - severityRank[a.severity] || compare(a.traceId, b.traceId) || compare(a.ruleId, b.ruleId) || compare(a.operationIds.join("\0"), b.operationIds.join("\0"))
|
|
945
995
|
);
|
|
996
|
+
const traceRefs = new Map(
|
|
997
|
+
orderedTraces.map((trace, index) => [
|
|
998
|
+
trace.traceId,
|
|
999
|
+
{
|
|
1000
|
+
id: `trace-${index + 1}`,
|
|
1001
|
+
operations: new Map(
|
|
1002
|
+
trace.operations.map((op, index2) => [op.id, `op-${index2 + 1}`])
|
|
1003
|
+
)
|
|
1004
|
+
}
|
|
1005
|
+
])
|
|
1006
|
+
);
|
|
1007
|
+
for (const summary of report.summaries)
|
|
1008
|
+
summary.traceId = traceRefs.get(summary.traceId).id;
|
|
1009
|
+
for (const finding of report.findings) {
|
|
1010
|
+
const refs = traceRefs.get(finding.traceId);
|
|
1011
|
+
finding.traceId = refs.id;
|
|
1012
|
+
finding.operationIds = finding.operationIds.map(
|
|
1013
|
+
(id) => refs.operations.get(id)
|
|
1014
|
+
);
|
|
1015
|
+
for (const reference of finding.evidence.references)
|
|
1016
|
+
reference.operationId = refs.operations.get(reference.operationId);
|
|
1017
|
+
}
|
|
946
1018
|
report.warnings = [...new Set(report.warnings)].sort();
|
|
947
1019
|
try {
|
|
948
1020
|
validateValue(report);
|
|
@@ -952,8 +1024,9 @@ function analyze(traces, configInput = {}, showContent = false) {
|
|
|
952
1024
|
throw error;
|
|
953
1025
|
}
|
|
954
1026
|
if (showContent)
|
|
955
|
-
report.content =
|
|
956
|
-
traceId: t.traceId,
|
|
1027
|
+
report.content = orderedTraces.map((t) => ({
|
|
1028
|
+
traceId: traceRefs.get(t.traceId).id,
|
|
1029
|
+
recordedTraceId: t.traceId,
|
|
957
1030
|
operations: redact(t.operations)
|
|
958
1031
|
}));
|
|
959
1032
|
return report;
|
|
@@ -981,7 +1054,8 @@ function humanReport(report, options = {}) {
|
|
|
981
1054
|
${paint("[" + f.severity.toUpperCase() + "]", f.severity)} ${safeText(f.ruleId)} ${safeText(f.title)} (${safeText(f.confidence)} evidence)`,
|
|
982
1055
|
` Trace: ${safeText(f.traceId)}; operations: ${f.operationIds.map(safeText).join(", ") || "trace-wide"}`,
|
|
983
1056
|
` ${safeText(f.explanation)}`,
|
|
984
|
-
` Evidence: ${safeText(JSON.stringify(f.evidence))}
|
|
1057
|
+
` Evidence: ${safeText(JSON.stringify(f.evidence))}`,
|
|
1058
|
+
` Remediation: ${safeText(f.remediation)}`
|
|
985
1059
|
);
|
|
986
1060
|
for (const warning of report.warnings)
|
|
987
1061
|
lines.push("Warning: " + safeText(warning));
|
|
@@ -1009,13 +1083,15 @@ var usage = z4.strictObject({
|
|
|
1009
1083
|
outputTokens: z4.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER).optional(),
|
|
1010
1084
|
totalTokens: z4.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER).optional()
|
|
1011
1085
|
});
|
|
1086
|
+
var traceId = z4.string().regex(/^trace-[1-9][0-9]*$/);
|
|
1087
|
+
var operationId = z4.string().regex(/^op-[1-9][0-9]*$/);
|
|
1012
1088
|
var reportSchema = z4.strictObject({
|
|
1013
|
-
version: z4.literal(
|
|
1089
|
+
version: z4.literal(2),
|
|
1014
1090
|
fingerprintVersion: z4.literal("awi-sha256-v1"),
|
|
1015
1091
|
complete: z4.boolean(),
|
|
1016
1092
|
summaries: z4.array(
|
|
1017
1093
|
z4.strictObject({
|
|
1018
|
-
traceId
|
|
1094
|
+
traceId,
|
|
1019
1095
|
operations: z4.number().int().nonnegative(),
|
|
1020
1096
|
toolCalls: z4.number().int().nonnegative(),
|
|
1021
1097
|
maxDepth: z4.number().int().nonnegative(),
|
|
@@ -1031,7 +1107,8 @@ var reportSchema = z4.strictObject({
|
|
|
1031
1107
|
"AWI003",
|
|
1032
1108
|
"AWI004",
|
|
1033
1109
|
"AWI005",
|
|
1034
|
-
"AWI006"
|
|
1110
|
+
"AWI006",
|
|
1111
|
+
"AWI007"
|
|
1035
1112
|
]),
|
|
1036
1113
|
severity: z4.enum(["low", "medium", "high"]),
|
|
1037
1114
|
confidence: z4.enum(["high", "moderate"]),
|
|
@@ -1039,15 +1116,26 @@ var reportSchema = z4.strictObject({
|
|
|
1039
1116
|
description: z4.string(),
|
|
1040
1117
|
explanation: z4.string(),
|
|
1041
1118
|
remediation: z4.string(),
|
|
1042
|
-
traceId
|
|
1043
|
-
operationIds: z4.array(
|
|
1044
|
-
evidence: z4.
|
|
1119
|
+
traceId,
|
|
1120
|
+
operationIds: z4.array(operationId),
|
|
1121
|
+
evidence: z4.looseObject({
|
|
1122
|
+
references: z4.array(
|
|
1123
|
+
z4.strictObject({
|
|
1124
|
+
operationId,
|
|
1125
|
+
source: z4.strictObject({
|
|
1126
|
+
format: z4.enum(["awi", "otel"]),
|
|
1127
|
+
index: z4.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER)
|
|
1128
|
+
}).optional()
|
|
1129
|
+
})
|
|
1130
|
+
)
|
|
1131
|
+
})
|
|
1045
1132
|
})
|
|
1046
1133
|
),
|
|
1047
1134
|
warnings: z4.array(z4.string()),
|
|
1048
1135
|
content: z4.array(
|
|
1049
1136
|
z4.strictObject({
|
|
1050
|
-
traceId
|
|
1137
|
+
traceId,
|
|
1138
|
+
recordedTraceId: z4.string(),
|
|
1051
1139
|
operations: z4.array(z4.unknown())
|
|
1052
1140
|
})
|
|
1053
1141
|
).optional()
|