agent-inspect 6.22.0 → 6.23.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 +6 -0
- package/README.md +1 -1
- package/docs/TRACE-CONTRACTS.md +56 -0
- package/package.json +1 -1
- package/packages/cli/dist/{chunk-CHBYF4LE.mjs → chunk-BVQO5CWA.mjs} +2 -2
- package/packages/cli/dist/{chunk-CHBYF4LE.mjs.map → chunk-BVQO5CWA.mjs.map} +1 -1
- package/packages/cli/dist/index.cjs +27 -1
- package/packages/cli/dist/index.cjs.map +1 -1
- package/packages/cli/dist/index.mjs +4 -4
- package/packages/cli/dist/index.mjs.map +1 -1
- package/packages/cli/dist/{src-MY5WIBPF.mjs → src-Y4NXOO2K.mjs} +3 -3
- package/packages/cli/dist/{src-MY5WIBPF.mjs.map → src-Y4NXOO2K.mjs.map} +1 -1
- package/packages/core/dist/advanced.cjs.map +1 -1
- package/packages/core/dist/advanced.mjs +2 -2
- package/packages/core/dist/checks.cjs +726 -50
- package/packages/core/dist/checks.cjs.map +1 -1
- package/packages/core/dist/checks.d.cts +174 -1
- package/packages/core/dist/checks.d.ts +174 -1
- package/packages/core/dist/checks.mjs +1 -1
- package/packages/core/dist/{chunk-OG3GAWCX.mjs → chunk-VMDCDWBE.mjs} +680 -7
- package/packages/core/dist/chunk-VMDCDWBE.mjs.map +1 -0
- package/packages/core/dist/chunk-OG3GAWCX.mjs.map +0 -1
|
@@ -1320,6 +1320,465 @@ function resolveTraceContractScope(input, scope) {
|
|
|
1320
1320
|
};
|
|
1321
1321
|
}
|
|
1322
1322
|
|
|
1323
|
+
// packages/core/src/checks/control-rules.ts
|
|
1324
|
+
function asStringList(value) {
|
|
1325
|
+
if (!Array.isArray(value)) return void 0;
|
|
1326
|
+
const out = [];
|
|
1327
|
+
for (const item of value) {
|
|
1328
|
+
if (typeof item !== "string" || item.trim() === "") return void 0;
|
|
1329
|
+
out.push(item.trim());
|
|
1330
|
+
}
|
|
1331
|
+
return out;
|
|
1332
|
+
}
|
|
1333
|
+
function attributeList(events, attribute) {
|
|
1334
|
+
if (!attribute) return void 0;
|
|
1335
|
+
for (const event of events) {
|
|
1336
|
+
const attrs2 = event.attributes;
|
|
1337
|
+
if (!attrs2 || typeof attrs2 !== "object") continue;
|
|
1338
|
+
const direct = asStringList(attrs2[attribute]);
|
|
1339
|
+
if (direct) return direct;
|
|
1340
|
+
const nested = attrs2.metadata && typeof attrs2.metadata === "object" ? asStringList(attrs2.metadata[attribute]) : void 0;
|
|
1341
|
+
if (nested) return nested;
|
|
1342
|
+
}
|
|
1343
|
+
return void 0;
|
|
1344
|
+
}
|
|
1345
|
+
function fail(ruleId, message, evidence, expected, actual) {
|
|
1346
|
+
return {
|
|
1347
|
+
ruleId,
|
|
1348
|
+
severity: "error",
|
|
1349
|
+
status: "fail",
|
|
1350
|
+
message,
|
|
1351
|
+
...expected !== void 0 ? { expected } : {},
|
|
1352
|
+
...actual !== void 0 ? { actual } : {},
|
|
1353
|
+
evidence: [...evidence]
|
|
1354
|
+
};
|
|
1355
|
+
}
|
|
1356
|
+
function sortedUnique(values) {
|
|
1357
|
+
return [...new Set(values)].sort((a, b) => a.localeCompare(b));
|
|
1358
|
+
}
|
|
1359
|
+
function setEqual(left, right) {
|
|
1360
|
+
const a = sortedUnique(left);
|
|
1361
|
+
const b = sortedUnique(right);
|
|
1362
|
+
return a.length === b.length && a.every((value, index) => value === b[index]);
|
|
1363
|
+
}
|
|
1364
|
+
function evaluateControlRules(events, rules, runEvidence2, observationNames) {
|
|
1365
|
+
const findings = [];
|
|
1366
|
+
const declared = rules.declaredTools ?? attributeList(events, rules.declaredToolsAttribute);
|
|
1367
|
+
const enforced = rules.enforcedTools ?? attributeList(events, rules.enforcedToolsAttribute);
|
|
1368
|
+
if (rules.requireDeclaredMatchesEnforced) {
|
|
1369
|
+
if (declared === void 0 || enforced === void 0) {
|
|
1370
|
+
findings.push(
|
|
1371
|
+
fail(
|
|
1372
|
+
"contract.controls.declared-matches-enforced",
|
|
1373
|
+
"Declared and enforced tool sets could not both be resolved.",
|
|
1374
|
+
runEvidence2,
|
|
1375
|
+
{ declaredDefined: declared !== void 0, enforcedDefined: enforced !== void 0 },
|
|
1376
|
+
{ code: "AI_CHECK_CONTROL_EVIDENCE_UNAVAILABLE" }
|
|
1377
|
+
)
|
|
1378
|
+
);
|
|
1379
|
+
} else if (!setEqual(declared, enforced)) {
|
|
1380
|
+
findings.push(
|
|
1381
|
+
fail(
|
|
1382
|
+
"contract.controls.declared-matches-enforced",
|
|
1383
|
+
"Declared tools differ from enforced tools.",
|
|
1384
|
+
runEvidence2,
|
|
1385
|
+
sortedUnique(declared),
|
|
1386
|
+
sortedUnique(enforced)
|
|
1387
|
+
)
|
|
1388
|
+
);
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
const observedTools = sortedUnique(
|
|
1392
|
+
events.filter((event) => event.kind === "TOOL" && event.status !== "running").map((event) => resolveCanonicalToolName(event))
|
|
1393
|
+
);
|
|
1394
|
+
if (rules.requireObservedWithinEnforced) {
|
|
1395
|
+
if (enforced === void 0) {
|
|
1396
|
+
findings.push(
|
|
1397
|
+
fail(
|
|
1398
|
+
"contract.controls.observed-within-enforced",
|
|
1399
|
+
"Enforced tool set unavailable for observed-within-enforced check.",
|
|
1400
|
+
runEvidence2,
|
|
1401
|
+
void 0,
|
|
1402
|
+
{ code: "AI_CHECK_CONTROL_EVIDENCE_UNAVAILABLE" }
|
|
1403
|
+
)
|
|
1404
|
+
);
|
|
1405
|
+
} else {
|
|
1406
|
+
const enforcedSet = new Set(enforced);
|
|
1407
|
+
const outside = observedTools.filter((name) => !enforcedSet.has(name));
|
|
1408
|
+
if (outside.length > 0) {
|
|
1409
|
+
findings.push(
|
|
1410
|
+
fail(
|
|
1411
|
+
"contract.controls.observed-within-enforced",
|
|
1412
|
+
`Observed tools outside enforced allowlist: ${outside.join(", ")}.`,
|
|
1413
|
+
runEvidence2,
|
|
1414
|
+
sortedUnique(enforced),
|
|
1415
|
+
outside
|
|
1416
|
+
)
|
|
1417
|
+
);
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
if (rules.requireObservedWithinDeclared) {
|
|
1422
|
+
if (declared === void 0) {
|
|
1423
|
+
findings.push(
|
|
1424
|
+
fail(
|
|
1425
|
+
"contract.controls.observed-within-declared",
|
|
1426
|
+
"Declared tool set unavailable for observed-within-declared check.",
|
|
1427
|
+
runEvidence2,
|
|
1428
|
+
void 0,
|
|
1429
|
+
{ code: "AI_CHECK_CONTROL_EVIDENCE_UNAVAILABLE" }
|
|
1430
|
+
)
|
|
1431
|
+
);
|
|
1432
|
+
} else {
|
|
1433
|
+
const declaredSet = new Set(declared);
|
|
1434
|
+
const outside = observedTools.filter((name) => !declaredSet.has(name));
|
|
1435
|
+
if (outside.length > 0) {
|
|
1436
|
+
findings.push(
|
|
1437
|
+
fail(
|
|
1438
|
+
"contract.controls.observed-within-declared",
|
|
1439
|
+
`Observed tools outside declared set: ${outside.join(", ")}.`,
|
|
1440
|
+
runEvidence2,
|
|
1441
|
+
sortedUnique(declared),
|
|
1442
|
+
outside
|
|
1443
|
+
)
|
|
1444
|
+
);
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
for (const stage of rules.requiredStages ?? []) {
|
|
1449
|
+
const observation = stage.observation ?? `control.${stage.stage}`;
|
|
1450
|
+
if (!observationNames.has(observation)) {
|
|
1451
|
+
findings.push(
|
|
1452
|
+
fail(
|
|
1453
|
+
`contract.controls.stage.${stage.stage}`,
|
|
1454
|
+
`Required control stage observation missing: ${observation}.`,
|
|
1455
|
+
runEvidence2,
|
|
1456
|
+
observation,
|
|
1457
|
+
[...observationNames]
|
|
1458
|
+
)
|
|
1459
|
+
);
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
return findings;
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
// packages/core/src/checks/retry-safety.ts
|
|
1466
|
+
function fail2(ruleId, message, evidence, expected, actual) {
|
|
1467
|
+
return {
|
|
1468
|
+
ruleId,
|
|
1469
|
+
severity: "error",
|
|
1470
|
+
status: "fail",
|
|
1471
|
+
message,
|
|
1472
|
+
...expected !== void 0 ? { expected } : {},
|
|
1473
|
+
...actual !== void 0 ? { actual } : {},
|
|
1474
|
+
evidence: [...evidence]
|
|
1475
|
+
};
|
|
1476
|
+
}
|
|
1477
|
+
function workflowFor(event) {
|
|
1478
|
+
const attrs2 = event.attributes;
|
|
1479
|
+
if (!attrs2 || typeof attrs2 !== "object") return {};
|
|
1480
|
+
const direct = extractSessionWorkflowMetadata(attrs2);
|
|
1481
|
+
const nested = attrs2.metadata && typeof attrs2.metadata === "object" ? extractSessionWorkflowMetadata(attrs2.metadata) : void 0;
|
|
1482
|
+
return { ...nested, ...direct };
|
|
1483
|
+
}
|
|
1484
|
+
function hasIdempotencyEvidence(event) {
|
|
1485
|
+
const workflow = workflowFor(event);
|
|
1486
|
+
if (typeof workflow.idempotencyKey === "string" && workflow.idempotencyKey.trim() !== "") {
|
|
1487
|
+
return true;
|
|
1488
|
+
}
|
|
1489
|
+
const attrs2 = event.attributes ?? {};
|
|
1490
|
+
if (attrs2.noSideEffect === true) return true;
|
|
1491
|
+
if (attrs2.sideEffect === false) return true;
|
|
1492
|
+
return false;
|
|
1493
|
+
}
|
|
1494
|
+
function eventEvidence(event) {
|
|
1495
|
+
return {
|
|
1496
|
+
runId: event.runId,
|
|
1497
|
+
eventId: event.eventId,
|
|
1498
|
+
kind: event.kind,
|
|
1499
|
+
name: event.name,
|
|
1500
|
+
status: event.status
|
|
1501
|
+
};
|
|
1502
|
+
}
|
|
1503
|
+
function evaluateRetrySafetyRules(events, rules, _runEvidence) {
|
|
1504
|
+
const findings = [];
|
|
1505
|
+
const byOperation = /* @__PURE__ */ new Map();
|
|
1506
|
+
for (const event of events) {
|
|
1507
|
+
const workflow = workflowFor(event);
|
|
1508
|
+
const operationId = workflow.operationId;
|
|
1509
|
+
if (!operationId) continue;
|
|
1510
|
+
const list = byOperation.get(operationId) ?? [];
|
|
1511
|
+
list.push(event);
|
|
1512
|
+
byOperation.set(operationId, list);
|
|
1513
|
+
}
|
|
1514
|
+
if (rules.maxAttempts !== void 0) {
|
|
1515
|
+
for (const [operationId, members] of byOperation) {
|
|
1516
|
+
const attemptIds = new Set(
|
|
1517
|
+
members.map((event) => workflowFor(event).attemptId).filter((value) => typeof value === "string" && value.trim() !== "")
|
|
1518
|
+
);
|
|
1519
|
+
const attemptNumbers = members.map((event) => workflowFor(event).attemptNumber ?? workflowFor(event).attempt).filter((value) => typeof value === "number" && Number.isFinite(value));
|
|
1520
|
+
const count = attemptIds.size > 0 ? attemptIds.size : attemptNumbers.length > 0 ? Math.max(...attemptNumbers) : members.filter((event) => event.kind === "TOOL" || event.kind === "LLM").length;
|
|
1521
|
+
if (count > rules.maxAttempts) {
|
|
1522
|
+
findings.push(
|
|
1523
|
+
fail2(
|
|
1524
|
+
"contract.retry.max-attempts",
|
|
1525
|
+
`Operation ${operationId} exceeded maxAttempts ${rules.maxAttempts}.`,
|
|
1526
|
+
members.slice(0, 4).map(eventEvidence),
|
|
1527
|
+
rules.maxAttempts,
|
|
1528
|
+
count
|
|
1529
|
+
)
|
|
1530
|
+
);
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
if (rules.requireTerminalResult) {
|
|
1535
|
+
for (const [operationId, members] of byOperation) {
|
|
1536
|
+
const hasTerminal = members.some((event) => event.status === "ok" || event.status === "error");
|
|
1537
|
+
if (!hasTerminal) {
|
|
1538
|
+
findings.push(
|
|
1539
|
+
fail2(
|
|
1540
|
+
"contract.retry.terminal-result",
|
|
1541
|
+
`Operation ${operationId} has no terminal ok/error result.`,
|
|
1542
|
+
members.slice(0, 4).map(eventEvidence),
|
|
1543
|
+
"ok|error",
|
|
1544
|
+
members.map((event) => event.status)
|
|
1545
|
+
)
|
|
1546
|
+
);
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
if (rules.fallbackOnlyAfterFailure) {
|
|
1551
|
+
for (const event of events) {
|
|
1552
|
+
const workflow = workflowFor(event);
|
|
1553
|
+
const fallbackOf = workflow.fallbackOf;
|
|
1554
|
+
if (!fallbackOf) continue;
|
|
1555
|
+
const prior = byOperation.get(fallbackOf) ?? events.filter((candidate) => {
|
|
1556
|
+
const meta = workflowFor(candidate);
|
|
1557
|
+
return meta.operationId === fallbackOf || candidate.runId === fallbackOf;
|
|
1558
|
+
});
|
|
1559
|
+
const priorFailure = prior.some((candidate) => candidate.status === "error");
|
|
1560
|
+
if (!priorFailure) {
|
|
1561
|
+
findings.push(
|
|
1562
|
+
fail2(
|
|
1563
|
+
"contract.retry.fallback-after-failure",
|
|
1564
|
+
`Fallback for ${fallbackOf} appeared without a prior failure.`,
|
|
1565
|
+
[eventEvidence(event)],
|
|
1566
|
+
"prior error attempt",
|
|
1567
|
+
{ fallbackOf }
|
|
1568
|
+
)
|
|
1569
|
+
);
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
const nonIdempotent = new Set(rules.nonIdempotentTools ?? []);
|
|
1574
|
+
if (nonIdempotent.size > 0 || rules.requireIdempotencyEvidenceForRetry) {
|
|
1575
|
+
const toolEvents = events.filter(
|
|
1576
|
+
(event) => event.kind === "TOOL" && event.status !== "running"
|
|
1577
|
+
);
|
|
1578
|
+
const byToolOp = /* @__PURE__ */ new Map();
|
|
1579
|
+
for (const event of toolEvents) {
|
|
1580
|
+
const name = resolveCanonicalToolName(event);
|
|
1581
|
+
const workflow = workflowFor(event);
|
|
1582
|
+
const key = `${workflow.operationId ?? name}::${name}`;
|
|
1583
|
+
const list = byToolOp.get(key) ?? [];
|
|
1584
|
+
list.push(event);
|
|
1585
|
+
byToolOp.set(key, list);
|
|
1586
|
+
}
|
|
1587
|
+
for (const [, members] of byToolOp) {
|
|
1588
|
+
const ordered = [...members].sort((a, b) => {
|
|
1589
|
+
const aTime = a.startedAt ?? a.timestamp ?? "";
|
|
1590
|
+
const bTime = b.startedAt ?? b.timestamp ?? "";
|
|
1591
|
+
return aTime.localeCompare(bTime);
|
|
1592
|
+
});
|
|
1593
|
+
let sawOk = false;
|
|
1594
|
+
let sawSideEffectOk = false;
|
|
1595
|
+
for (const event of ordered) {
|
|
1596
|
+
const name = resolveCanonicalToolName(event);
|
|
1597
|
+
const isRetry = sawOk;
|
|
1598
|
+
if (isRetry) {
|
|
1599
|
+
if (rules.requireIdempotencyEvidenceForRetry && !hasIdempotencyEvidence(event)) {
|
|
1600
|
+
findings.push(
|
|
1601
|
+
fail2(
|
|
1602
|
+
"contract.retry.idempotency-evidence",
|
|
1603
|
+
`Retry of tool ${name} lacks idempotencyKey / noSideEffect evidence.`,
|
|
1604
|
+
[eventEvidence(event)],
|
|
1605
|
+
"idempotencyKey|noSideEffect",
|
|
1606
|
+
{ code: "AI_CHECK_RETRY_EVIDENCE_UNAVAILABLE" }
|
|
1607
|
+
)
|
|
1608
|
+
);
|
|
1609
|
+
}
|
|
1610
|
+
if (nonIdempotent.has(name) && sawSideEffectOk && !hasIdempotencyEvidence(event)) {
|
|
1611
|
+
findings.push(
|
|
1612
|
+
fail2(
|
|
1613
|
+
"contract.retry.non-idempotent-side-effect",
|
|
1614
|
+
`Retry of non-idempotent tool ${name} after a confirmed ok side effect.`,
|
|
1615
|
+
[eventEvidence(event)],
|
|
1616
|
+
"no retry after side effect",
|
|
1617
|
+
name
|
|
1618
|
+
)
|
|
1619
|
+
);
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
if (event.status === "ok") {
|
|
1623
|
+
sawOk = true;
|
|
1624
|
+
if (nonIdempotent.has(name) && !hasIdempotencyEvidence(event)) {
|
|
1625
|
+
sawSideEffectOk = true;
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
if (rules.requireRecoveredFailureVisible) {
|
|
1632
|
+
for (const [operationId, members] of byOperation) {
|
|
1633
|
+
const hasOk = members.some((event) => event.status === "ok");
|
|
1634
|
+
const hasError = members.some((event) => event.status === "error");
|
|
1635
|
+
const attemptish = members.some((event) => {
|
|
1636
|
+
const meta = workflowFor(event);
|
|
1637
|
+
return meta.attemptId !== void 0 || meta.attemptNumber !== void 0 || meta.attempt !== void 0;
|
|
1638
|
+
}) || members.length > 1;
|
|
1639
|
+
if (hasOk && attemptish && !hasError) {
|
|
1640
|
+
findings.push(
|
|
1641
|
+
fail2(
|
|
1642
|
+
"contract.retry.recovered-failure-visible",
|
|
1643
|
+
`Operation ${operationId} recovered without retaining a visible failure attempt.`,
|
|
1644
|
+
members.slice(0, 4).map(eventEvidence),
|
|
1645
|
+
"error attempt retained",
|
|
1646
|
+
{ hasOk, hasError }
|
|
1647
|
+
)
|
|
1648
|
+
);
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
return findings;
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
// packages/core/src/checks/tool-arguments.ts
|
|
1656
|
+
function decodeToken(token) {
|
|
1657
|
+
return token.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
1658
|
+
}
|
|
1659
|
+
function resolveJsonPointer(document, pointer) {
|
|
1660
|
+
if (pointer === "") {
|
|
1661
|
+
return { found: true, value: document };
|
|
1662
|
+
}
|
|
1663
|
+
if (!pointer.startsWith("/")) {
|
|
1664
|
+
return { found: false };
|
|
1665
|
+
}
|
|
1666
|
+
const tokens = pointer.slice(1).split("/").map(decodeToken);
|
|
1667
|
+
let current = document;
|
|
1668
|
+
for (const token of tokens) {
|
|
1669
|
+
if (current === null || current === void 0) {
|
|
1670
|
+
return { found: false };
|
|
1671
|
+
}
|
|
1672
|
+
if (Array.isArray(current)) {
|
|
1673
|
+
if (!/^(0|[1-9][0-9]*)$/.test(token)) {
|
|
1674
|
+
return { found: false };
|
|
1675
|
+
}
|
|
1676
|
+
const index = Number(token);
|
|
1677
|
+
if (index >= current.length) {
|
|
1678
|
+
return { found: false };
|
|
1679
|
+
}
|
|
1680
|
+
current = current[index];
|
|
1681
|
+
continue;
|
|
1682
|
+
}
|
|
1683
|
+
if (typeof current !== "object") {
|
|
1684
|
+
return { found: false };
|
|
1685
|
+
}
|
|
1686
|
+
const record = current;
|
|
1687
|
+
if (!Object.prototype.hasOwnProperty.call(record, token)) {
|
|
1688
|
+
return { found: false };
|
|
1689
|
+
}
|
|
1690
|
+
current = record[token];
|
|
1691
|
+
}
|
|
1692
|
+
return { found: true, value: current };
|
|
1693
|
+
}
|
|
1694
|
+
function jsonType(value) {
|
|
1695
|
+
if (value === null) return "null";
|
|
1696
|
+
if (Array.isArray(value)) return "array";
|
|
1697
|
+
return typeof value;
|
|
1698
|
+
}
|
|
1699
|
+
function valuesEqual(left, right) {
|
|
1700
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
1701
|
+
}
|
|
1702
|
+
function extractToolArgumentPayload(event) {
|
|
1703
|
+
const attrs2 = event.attributes ?? {};
|
|
1704
|
+
for (const key of ["arguments", "input", "toolArguments"]) {
|
|
1705
|
+
const candidate = attrs2[key];
|
|
1706
|
+
if (candidate !== void 0 && candidate !== null && typeof candidate === "object") {
|
|
1707
|
+
return { present: true, value: candidate };
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
if (event.inputSummary !== void 0 && event.inputSummary !== null && typeof event.inputSummary === "object") {
|
|
1711
|
+
return { present: true, value: event.inputSummary };
|
|
1712
|
+
}
|
|
1713
|
+
return { present: false, value: void 0 };
|
|
1714
|
+
}
|
|
1715
|
+
function evaluateToolArgumentValue(value, check, evidencePresent) {
|
|
1716
|
+
if (!evidencePresent) {
|
|
1717
|
+
return {
|
|
1718
|
+
status: "unavailable",
|
|
1719
|
+
code: "AI_CHECK_TOOL_ARGUMENT_EVIDENCE_UNAVAILABLE",
|
|
1720
|
+
message: `Structured argument evidence unavailable for tool ${check.tool} at ${check.path}.`
|
|
1721
|
+
};
|
|
1722
|
+
}
|
|
1723
|
+
const resolved = resolveJsonPointer(value, check.path);
|
|
1724
|
+
if (check.operator === "exists") {
|
|
1725
|
+
return resolved.found ? { status: "pass" } : {
|
|
1726
|
+
status: "fail",
|
|
1727
|
+
code: "AI_CHECK_TOOL_ARGUMENT_MISSING",
|
|
1728
|
+
message: `Expected path ${check.path} to exist on tool ${check.tool}.`
|
|
1729
|
+
};
|
|
1730
|
+
}
|
|
1731
|
+
if (!resolved.found) {
|
|
1732
|
+
return {
|
|
1733
|
+
status: "unavailable",
|
|
1734
|
+
code: "AI_CHECK_TOOL_ARGUMENT_EVIDENCE_UNAVAILABLE",
|
|
1735
|
+
message: `Path ${check.path} missing for tool ${check.tool}.`
|
|
1736
|
+
};
|
|
1737
|
+
}
|
|
1738
|
+
if (check.operator === "type") {
|
|
1739
|
+
const actual = jsonType(resolved.value);
|
|
1740
|
+
if (check.type === void 0) {
|
|
1741
|
+
return {
|
|
1742
|
+
status: "fail",
|
|
1743
|
+
code: "AI_CHECK_TOOL_ARGUMENT_INVALID_CONFIG",
|
|
1744
|
+
message: "type operator requires check.type"
|
|
1745
|
+
};
|
|
1746
|
+
}
|
|
1747
|
+
return actual === check.type ? { status: "pass" } : {
|
|
1748
|
+
status: "fail",
|
|
1749
|
+
code: "AI_CHECK_TOOL_ARGUMENT_TYPE",
|
|
1750
|
+
message: `Expected type ${check.type} at ${check.path} for tool ${check.tool}; found ${actual}.`
|
|
1751
|
+
};
|
|
1752
|
+
}
|
|
1753
|
+
if (check.operator === "equals") {
|
|
1754
|
+
return valuesEqual(resolved.value, check.expected) ? { status: "pass" } : {
|
|
1755
|
+
status: "fail",
|
|
1756
|
+
code: "AI_CHECK_TOOL_ARGUMENT_EQUALS",
|
|
1757
|
+
message: `Value at ${check.path} for tool ${check.tool} did not equal expected (bounded comparison).`
|
|
1758
|
+
};
|
|
1759
|
+
}
|
|
1760
|
+
if (check.operator === "oneOf") {
|
|
1761
|
+
const options = check.oneOf ?? [];
|
|
1762
|
+
if (options.length === 0) {
|
|
1763
|
+
return {
|
|
1764
|
+
status: "fail",
|
|
1765
|
+
code: "AI_CHECK_TOOL_ARGUMENT_INVALID_CONFIG",
|
|
1766
|
+
message: "oneOf operator requires a non-empty oneOf array"
|
|
1767
|
+
};
|
|
1768
|
+
}
|
|
1769
|
+
return options.some((option) => valuesEqual(resolved.value, option)) ? { status: "pass" } : {
|
|
1770
|
+
status: "fail",
|
|
1771
|
+
code: "AI_CHECK_TOOL_ARGUMENT_ONE_OF",
|
|
1772
|
+
message: `Value at ${check.path} for tool ${check.tool} was not in the allowed oneOf set.`
|
|
1773
|
+
};
|
|
1774
|
+
}
|
|
1775
|
+
return {
|
|
1776
|
+
status: "fail",
|
|
1777
|
+
code: "AI_CHECK_TOOL_ARGUMENT_INVALID_CONFIG",
|
|
1778
|
+
message: `Unsupported operator.`
|
|
1779
|
+
};
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1323
1782
|
// packages/core/src/checks/contract.ts
|
|
1324
1783
|
function contractFailFinding(ruleId, message, evidence, expected, actual) {
|
|
1325
1784
|
return {
|
|
@@ -1341,18 +1800,38 @@ function normalizeStatus(status) {
|
|
|
1341
1800
|
function cloneBody(body) {
|
|
1342
1801
|
return {
|
|
1343
1802
|
...body.run ? { run: { ...body.run } } : {},
|
|
1344
|
-
...body.tools ? {
|
|
1803
|
+
...body.tools ? {
|
|
1804
|
+
tools: {
|
|
1805
|
+
...body.tools,
|
|
1806
|
+
...body.tools.arguments ? { arguments: body.tools.arguments.map((item) => ({ ...item })) } : {},
|
|
1807
|
+
...body.tools.orderRules ? { orderRules: body.tools.orderRules.map((item) => ({ ...item })) } : {}
|
|
1808
|
+
}
|
|
1809
|
+
} : {},
|
|
1345
1810
|
...body.llm ? { llm: { ...body.llm } } : {},
|
|
1346
1811
|
...body.observations ? {
|
|
1347
1812
|
observations: {
|
|
1348
1813
|
...body.observations,
|
|
1349
1814
|
...body.observations.requireProvenance ? { requireProvenance: { ...body.observations.requireProvenance } } : {}
|
|
1350
1815
|
}
|
|
1816
|
+
} : {},
|
|
1817
|
+
...body.controls ? {
|
|
1818
|
+
controls: {
|
|
1819
|
+
...body.controls,
|
|
1820
|
+
...body.controls.declaredTools ? { declaredTools: [...body.controls.declaredTools] } : {},
|
|
1821
|
+
...body.controls.enforcedTools ? { enforcedTools: [...body.controls.enforcedTools] } : {},
|
|
1822
|
+
...body.controls.requiredStages ? { requiredStages: body.controls.requiredStages.map((item) => ({ ...item })) } : {}
|
|
1823
|
+
}
|
|
1824
|
+
} : {},
|
|
1825
|
+
...body.retry ? {
|
|
1826
|
+
retry: {
|
|
1827
|
+
...body.retry,
|
|
1828
|
+
...body.retry.nonIdempotentTools ? { nonIdempotentTools: [...body.retry.nonIdempotentTools] } : {}
|
|
1829
|
+
}
|
|
1351
1830
|
} : {}
|
|
1352
1831
|
};
|
|
1353
1832
|
}
|
|
1354
1833
|
function bodyHasRules(body) {
|
|
1355
|
-
return body.run !== void 0 || body.tools !== void 0 || body.llm !== void 0 || body.observations !== void 0;
|
|
1834
|
+
return body.run !== void 0 || body.tools !== void 0 || body.llm !== void 0 || body.observations !== void 0 || body.controls !== void 0 || body.retry !== void 0;
|
|
1356
1835
|
}
|
|
1357
1836
|
var MAX_EVIDENCE_EVENT_IDS = 16;
|
|
1358
1837
|
var METHOD_VOCABULARY = new Set(OBSERVED_OUTCOME_METHODS);
|
|
@@ -1553,11 +2032,14 @@ function contractToRules(contract) {
|
|
|
1553
2032
|
if (contract.tools) {
|
|
1554
2033
|
const order = contract.tools.requiredOrder ?? [];
|
|
1555
2034
|
const requiredOrderMode = contract.tools.requiredOrderMode ?? "first-occurrence";
|
|
2035
|
+
const orderRules = contract.tools.orderRules ?? [];
|
|
2036
|
+
const endpointRequired = orderRules.filter((rule) => rule.requireEndpoints !== false).flatMap((rule) => [rule.before, rule.after]);
|
|
1556
2037
|
const required = [
|
|
1557
2038
|
.../* @__PURE__ */ new Set([
|
|
1558
2039
|
...contract.tools.required ?? [],
|
|
1559
2040
|
...contract.tools.requiredTools ?? [],
|
|
1560
|
-
...order
|
|
2041
|
+
...order,
|
|
2042
|
+
...endpointRequired
|
|
1561
2043
|
])
|
|
1562
2044
|
];
|
|
1563
2045
|
const forbidden = [
|
|
@@ -1582,6 +2064,106 @@ function contractToRules(contract) {
|
|
|
1582
2064
|
})
|
|
1583
2065
|
);
|
|
1584
2066
|
}
|
|
2067
|
+
const defaultOccurrenceMode = contract.tools.defaultOccurrenceMode ?? requiredOrderMode;
|
|
2068
|
+
for (const [index, rule] of orderRules.entries()) {
|
|
2069
|
+
rules.push(
|
|
2070
|
+
createToolOrderingRule({
|
|
2071
|
+
before: rule.before,
|
|
2072
|
+
after: rule.after,
|
|
2073
|
+
id: `contract.tool.orderRule.${index}`,
|
|
2074
|
+
mode: rule.occurrenceMode ?? defaultOccurrenceMode
|
|
2075
|
+
})
|
|
2076
|
+
);
|
|
2077
|
+
}
|
|
2078
|
+
const argumentChecks = contract.tools.arguments ?? [];
|
|
2079
|
+
if (argumentChecks.length > 0) {
|
|
2080
|
+
rules.push({
|
|
2081
|
+
id: "contract.tool.arguments",
|
|
2082
|
+
category: "tool",
|
|
2083
|
+
defaultSeverity: "error",
|
|
2084
|
+
evaluate(context) {
|
|
2085
|
+
const tools = (context.logicalEvents ?? context.events).filter(
|
|
2086
|
+
(event) => event.kind === "TOOL" && event.status !== "running"
|
|
2087
|
+
);
|
|
2088
|
+
const findings = [];
|
|
2089
|
+
for (const [index, check] of argumentChecks.entries()) {
|
|
2090
|
+
const matches = tools.filter(
|
|
2091
|
+
(event) => resolveCanonicalToolName(event) === check.tool
|
|
2092
|
+
);
|
|
2093
|
+
if (matches.length === 0) {
|
|
2094
|
+
findings.push(
|
|
2095
|
+
contractFailFinding(
|
|
2096
|
+
`contract.tool.arguments.${index}`,
|
|
2097
|
+
`No finished tool named ${check.tool} for argument check.`,
|
|
2098
|
+
context.selectedRun ? [
|
|
2099
|
+
{
|
|
2100
|
+
runId: context.selectedRun.runId,
|
|
2101
|
+
kind: "RUN",
|
|
2102
|
+
name: context.selectedRun.name
|
|
2103
|
+
}
|
|
2104
|
+
] : [],
|
|
2105
|
+
{ tool: check.tool, path: check.path },
|
|
2106
|
+
{ toolCount: 0 }
|
|
2107
|
+
)
|
|
2108
|
+
);
|
|
2109
|
+
continue;
|
|
2110
|
+
}
|
|
2111
|
+
const occurrence = check.occurrence ?? "all";
|
|
2112
|
+
const selected = occurrence === "first" ? [matches[0]] : occurrence === "last" ? [matches[matches.length - 1]] : matches;
|
|
2113
|
+
const results = selected.map((event) => {
|
|
2114
|
+
const payload = extractToolArgumentPayload(event);
|
|
2115
|
+
return evaluateToolArgumentValue(
|
|
2116
|
+
payload.value,
|
|
2117
|
+
check,
|
|
2118
|
+
payload.present
|
|
2119
|
+
);
|
|
2120
|
+
});
|
|
2121
|
+
if (occurrence === "any") {
|
|
2122
|
+
if (results.some((result) => result.status === "pass")) continue;
|
|
2123
|
+
const firstFail = results.find((result) => result.status !== "pass");
|
|
2124
|
+
findings.push(
|
|
2125
|
+
contractFailFinding(
|
|
2126
|
+
`contract.tool.arguments.${index}`,
|
|
2127
|
+
firstFail.message,
|
|
2128
|
+
selected.slice(0, 1).map((event) => ({
|
|
2129
|
+
runId: event.runId,
|
|
2130
|
+
eventId: event.eventId,
|
|
2131
|
+
kind: event.kind,
|
|
2132
|
+
name: event.name,
|
|
2133
|
+
path: `tool.${check.tool}${check.path}`
|
|
2134
|
+
})),
|
|
2135
|
+
{ tool: check.tool, path: check.path, operator: check.operator },
|
|
2136
|
+
{ code: firstFail.code }
|
|
2137
|
+
)
|
|
2138
|
+
);
|
|
2139
|
+
continue;
|
|
2140
|
+
}
|
|
2141
|
+
for (const [selIndex, result] of results.entries()) {
|
|
2142
|
+
if (result.status === "pass") continue;
|
|
2143
|
+
const event = selected[selIndex];
|
|
2144
|
+
findings.push(
|
|
2145
|
+
contractFailFinding(
|
|
2146
|
+
`contract.tool.arguments.${index}`,
|
|
2147
|
+
result.message,
|
|
2148
|
+
[
|
|
2149
|
+
{
|
|
2150
|
+
runId: event.runId,
|
|
2151
|
+
eventId: event.eventId,
|
|
2152
|
+
kind: event.kind,
|
|
2153
|
+
name: event.name,
|
|
2154
|
+
path: `tool.${check.tool}${check.path}`
|
|
2155
|
+
}
|
|
2156
|
+
],
|
|
2157
|
+
{ tool: check.tool, path: check.path, operator: check.operator },
|
|
2158
|
+
{ code: result.code }
|
|
2159
|
+
)
|
|
2160
|
+
);
|
|
2161
|
+
}
|
|
2162
|
+
}
|
|
2163
|
+
return findings;
|
|
2164
|
+
}
|
|
2165
|
+
});
|
|
2166
|
+
}
|
|
1585
2167
|
}
|
|
1586
2168
|
if (contract.llm) {
|
|
1587
2169
|
rules.push(
|
|
@@ -1642,6 +2224,46 @@ function contractToRules(contract) {
|
|
|
1642
2224
|
});
|
|
1643
2225
|
}
|
|
1644
2226
|
}
|
|
2227
|
+
if (contract.controls) {
|
|
2228
|
+
const controls = contract.controls;
|
|
2229
|
+
rules.push({
|
|
2230
|
+
id: "contract.controls",
|
|
2231
|
+
category: "run",
|
|
2232
|
+
defaultSeverity: "error",
|
|
2233
|
+
evaluate(context) {
|
|
2234
|
+
const events = context.logicalEvents ?? context.events;
|
|
2235
|
+
const outcomes = extractOutcomesFromPersistedEvents(context.events);
|
|
2236
|
+
const observationNames = new Set(outcomes.map((item) => item.name));
|
|
2237
|
+
const runEvidence2 = context.selectedRun ? [
|
|
2238
|
+
{
|
|
2239
|
+
runId: context.selectedRun.runId,
|
|
2240
|
+
kind: "RUN",
|
|
2241
|
+
name: context.selectedRun.name
|
|
2242
|
+
}
|
|
2243
|
+
] : [];
|
|
2244
|
+
return evaluateControlRules(events, controls, runEvidence2, observationNames);
|
|
2245
|
+
}
|
|
2246
|
+
});
|
|
2247
|
+
}
|
|
2248
|
+
if (contract.retry) {
|
|
2249
|
+
const retry = contract.retry;
|
|
2250
|
+
rules.push({
|
|
2251
|
+
id: "contract.retry",
|
|
2252
|
+
category: "run",
|
|
2253
|
+
defaultSeverity: "error",
|
|
2254
|
+
evaluate(context) {
|
|
2255
|
+
const events = context.logicalEvents ?? context.events;
|
|
2256
|
+
context.selectedRun ? [
|
|
2257
|
+
{
|
|
2258
|
+
runId: context.selectedRun.runId,
|
|
2259
|
+
kind: "RUN",
|
|
2260
|
+
name: context.selectedRun.name
|
|
2261
|
+
}
|
|
2262
|
+
] : [];
|
|
2263
|
+
return evaluateRetrySafetyRules(events, retry);
|
|
2264
|
+
}
|
|
2265
|
+
});
|
|
2266
|
+
}
|
|
1645
2267
|
return rules;
|
|
1646
2268
|
}
|
|
1647
2269
|
function validateScopeShape(scope) {
|
|
@@ -1728,7 +2350,7 @@ function validateAlternativesShape(alternatives) {
|
|
|
1728
2350
|
diagnostics.push({
|
|
1729
2351
|
code: "contract.alternatives.empty-branch",
|
|
1730
2352
|
severity: "error",
|
|
1731
|
-
message: `Branch ${branch.id} must declare at least one run/tools/llm/observations rule.`,
|
|
2353
|
+
message: `Branch ${branch.id} must declare at least one run/tools/llm/observations/controls/retry rule.`,
|
|
1732
2354
|
path: `${path3}.contract`
|
|
1733
2355
|
});
|
|
1734
2356
|
}
|
|
@@ -2001,6 +2623,40 @@ function lintTraceContract(contract) {
|
|
|
2001
2623
|
path: "tools.requiredOrderMode"
|
|
2002
2624
|
});
|
|
2003
2625
|
}
|
|
2626
|
+
const orderRules = contract.tools?.orderRules ?? [];
|
|
2627
|
+
const seenPairs = /* @__PURE__ */ new Set();
|
|
2628
|
+
for (const [index, rule] of orderRules.entries()) {
|
|
2629
|
+
const key = `${rule.before}\0${rule.after}`;
|
|
2630
|
+
if (seenPairs.has(key)) {
|
|
2631
|
+
diagnostics.push({
|
|
2632
|
+
code: "contract.tools.orderRules.duplicate",
|
|
2633
|
+
severity: "warning",
|
|
2634
|
+
message: `Duplicate orderRules pair ${rule.before} \u2192 ${rule.after}.`,
|
|
2635
|
+
path: `tools.orderRules[${index}]`
|
|
2636
|
+
});
|
|
2637
|
+
}
|
|
2638
|
+
seenPairs.add(key);
|
|
2639
|
+
if (rule.before === rule.after) {
|
|
2640
|
+
diagnostics.push({
|
|
2641
|
+
code: "contract.tools.orderRules.self",
|
|
2642
|
+
severity: "error",
|
|
2643
|
+
message: "orderRules before and after must differ.",
|
|
2644
|
+
path: `tools.orderRules[${index}]`
|
|
2645
|
+
});
|
|
2646
|
+
}
|
|
2647
|
+
}
|
|
2648
|
+
if ((contract.tools?.arguments?.length ?? 0) > 0) {
|
|
2649
|
+
for (const [index, check] of (contract.tools?.arguments ?? []).entries()) {
|
|
2650
|
+
if (!check.path.startsWith("/") && check.path !== "") {
|
|
2651
|
+
diagnostics.push({
|
|
2652
|
+
code: "contract.tools.arguments.path",
|
|
2653
|
+
severity: "error",
|
|
2654
|
+
message: 'tools.arguments path must be a JSON Pointer ("" or start with "/").',
|
|
2655
|
+
path: `tools.arguments[${index}].path`
|
|
2656
|
+
});
|
|
2657
|
+
}
|
|
2658
|
+
}
|
|
2659
|
+
}
|
|
2004
2660
|
return diagnostics;
|
|
2005
2661
|
}
|
|
2006
2662
|
function explainTraceContract(contract) {
|
|
@@ -2044,6 +2700,17 @@ function explainTraceContract(contract) {
|
|
|
2044
2700
|
`Base: requiredOrder [${contract.tools.requiredOrder.join(" \u2192 ")}] mode=${mode}.`
|
|
2045
2701
|
);
|
|
2046
2702
|
}
|
|
2703
|
+
if ((contract.tools.orderRules?.length ?? 0) > 0) {
|
|
2704
|
+
const defaultMode = contract.tools.defaultOccurrenceMode ?? "first-occurrence";
|
|
2705
|
+
lines.push(
|
|
2706
|
+
`Base: ${contract.tools.orderRules.length} orderRules (defaultOccurrenceMode=${defaultMode}).`
|
|
2707
|
+
);
|
|
2708
|
+
}
|
|
2709
|
+
if ((contract.tools.arguments?.length ?? 0) > 0) {
|
|
2710
|
+
lines.push(
|
|
2711
|
+
`Base: ${contract.tools.arguments.length} structured tool-argument check(s).`
|
|
2712
|
+
);
|
|
2713
|
+
}
|
|
2047
2714
|
}
|
|
2048
2715
|
if (contract.llm) {
|
|
2049
2716
|
if (contract.llm.maxCalls !== void 0) {
|
|
@@ -2074,6 +2741,12 @@ function explainTraceContract(contract) {
|
|
|
2074
2741
|
);
|
|
2075
2742
|
}
|
|
2076
2743
|
}
|
|
2744
|
+
if (contract.controls) {
|
|
2745
|
+
lines.push("Base: declared-versus-enforced control checks enabled.");
|
|
2746
|
+
}
|
|
2747
|
+
if (contract.retry) {
|
|
2748
|
+
lines.push("Base: retry/side-effect safety checks enabled.");
|
|
2749
|
+
}
|
|
2077
2750
|
const branches = contract.alternatives?.anyOf ?? [];
|
|
2078
2751
|
if (branches.length > 0) {
|
|
2079
2752
|
lines.push(
|
|
@@ -2423,7 +3096,7 @@ function stripPrefix(name, prefixes) {
|
|
|
2423
3096
|
}
|
|
2424
3097
|
return name;
|
|
2425
3098
|
}
|
|
2426
|
-
function
|
|
3099
|
+
function eventEvidence2(event, path3) {
|
|
2427
3100
|
return {
|
|
2428
3101
|
runId: event.runId,
|
|
2429
3102
|
eventId: event.eventId,
|
|
@@ -2693,7 +3366,7 @@ function guardrailShape(context) {
|
|
|
2693
3366
|
}
|
|
2694
3367
|
function firstEvidenceForKind(context, kind, path3) {
|
|
2695
3368
|
const event = semanticEvents(context).find((candidate) => candidate.kind === kind);
|
|
2696
|
-
return event ? [
|
|
3369
|
+
return event ? [eventEvidence2(event, path3)] : runEvidence(context.selectedRun);
|
|
2697
3370
|
}
|
|
2698
3371
|
function baselineDiffFinding(message, evidence, expected, actual) {
|
|
2699
3372
|
return failFinding("baseline.regression", message, evidence, expected, actual);
|
|
@@ -2726,7 +3399,7 @@ function createRunStatusRule(options = {}) {
|
|
|
2726
3399
|
failFinding(
|
|
2727
3400
|
"run.status",
|
|
2728
3401
|
"Run contains incomplete running events.",
|
|
2729
|
-
running.map((event) =>
|
|
3402
|
+
running.map((event) => eventEvidence2(event)),
|
|
2730
3403
|
"no running events",
|
|
2731
3404
|
running.length
|
|
2732
3405
|
)
|
|
@@ -2772,7 +3445,7 @@ function createMaxStepDurationRule(options) {
|
|
|
2772
3445
|
failFinding(
|
|
2773
3446
|
"run.maxStepDuration",
|
|
2774
3447
|
`${over.length} step(s) exceeded max duration ${options.maxDurationMs}ms.`,
|
|
2775
|
-
over.map((event) =>
|
|
3448
|
+
over.map((event) => eventEvidence2(event, "durationMs")),
|
|
2776
3449
|
{ maxDurationMs: options.maxDurationMs },
|
|
2777
3450
|
over.map((event) => ({
|
|
2778
3451
|
eventId: event.eventId,
|
|
@@ -2798,7 +3471,7 @@ function createStallDetectionRule(options = {}) {
|
|
|
2798
3471
|
failFinding(
|
|
2799
3472
|
"run.stall",
|
|
2800
3473
|
`Found ${running.length} event(s) still running (possible stall).`,
|
|
2801
|
-
running.map((event) =>
|
|
3474
|
+
running.map((event) => eventEvidence2(event, "status")),
|
|
2802
3475
|
"no running events",
|
|
2803
3476
|
running.length
|
|
2804
3477
|
)
|
|
@@ -2813,7 +3486,7 @@ function createStallDetectionRule(options = {}) {
|
|
|
2813
3486
|
failFinding(
|
|
2814
3487
|
"run.stall",
|
|
2815
3488
|
`Found ${incomplete.length} started event(s) without endedAt.`,
|
|
2816
|
-
incomplete.map((event) =>
|
|
3489
|
+
incomplete.map((event) => eventEvidence2(event, "endedAt")),
|
|
2817
3490
|
"endedAt for started events",
|
|
2818
3491
|
incomplete.length
|
|
2819
3492
|
)
|
|
@@ -2849,7 +3522,7 @@ function createRequireCompletedRule() {
|
|
|
2849
3522
|
failFinding(
|
|
2850
3523
|
"run.requireCompleted",
|
|
2851
3524
|
`Run has ${running.length} incomplete running event(s).`,
|
|
2852
|
-
running.map((event) =>
|
|
3525
|
+
running.map((event) => eventEvidence2(event, "status")),
|
|
2853
3526
|
"no running events",
|
|
2854
3527
|
running.length
|
|
2855
3528
|
)
|
|
@@ -2947,12 +3620,12 @@ function createToolUsageRule(options) {
|
|
|
2947
3620
|
const name = toolName(event);
|
|
2948
3621
|
if (forbidden.has(name)) {
|
|
2949
3622
|
findings.push(
|
|
2950
|
-
failFinding("tool.usage", `Forbidden tool ${name} appeared.`, [
|
|
3623
|
+
failFinding("tool.usage", `Forbidden tool ${name} appeared.`, [eventEvidence2(event)], "tool absent", name)
|
|
2951
3624
|
);
|
|
2952
3625
|
}
|
|
2953
3626
|
if (allowed && !allowed.has(name)) {
|
|
2954
3627
|
findings.push(
|
|
2955
|
-
failFinding("tool.usage", `Tool ${name} is not in the allowed tool set.`, [
|
|
3628
|
+
failFinding("tool.usage", `Tool ${name} is not in the allowed tool set.`, [eventEvidence2(event)], [...allowed].sort(), name)
|
|
2956
3629
|
);
|
|
2957
3630
|
}
|
|
2958
3631
|
}
|
|
@@ -2963,7 +3636,7 @@ function createToolUsageRule(options) {
|
|
|
2963
3636
|
}
|
|
2964
3637
|
if (options.maxCount !== void 0 && tools.length > options.maxCount) {
|
|
2965
3638
|
findings.push(
|
|
2966
|
-
failFinding("tool.usage", `Tool count ${tools.length} exceeded maximum ${options.maxCount}.`, tools.map((event) =>
|
|
3639
|
+
failFinding("tool.usage", `Tool count ${tools.length} exceeded maximum ${options.maxCount}.`, tools.map((event) => eventEvidence2(event)), { maxCount: options.maxCount }, tools.length)
|
|
2967
3640
|
);
|
|
2968
3641
|
}
|
|
2969
3642
|
return findings;
|
|
@@ -2990,7 +3663,7 @@ function createToolOrderingRule(options) {
|
|
|
2990
3663
|
failFinding(
|
|
2991
3664
|
ruleId,
|
|
2992
3665
|
`Tool ${options.before} must appear before ${options.after}.`,
|
|
2993
|
-
[
|
|
3666
|
+
[eventEvidence2(tools[beforeIndex]), eventEvidence2(tools[afterIndex])],
|
|
2994
3667
|
{ before: options.before, after: options.after },
|
|
2995
3668
|
tools.map(toolName)
|
|
2996
3669
|
)
|
|
@@ -3012,7 +3685,7 @@ function createToolOrderingRule(options) {
|
|
|
3012
3685
|
failFinding(
|
|
3013
3686
|
ruleId,
|
|
3014
3687
|
`Tool ${options.before} cannot causally happen before itself.`,
|
|
3015
|
-
[
|
|
3688
|
+
[eventEvidence2(beforeEvent)],
|
|
3016
3689
|
expected,
|
|
3017
3690
|
{ code: "tool.order.same-tool" }
|
|
3018
3691
|
)
|
|
@@ -3023,7 +3696,7 @@ function createToolOrderingRule(options) {
|
|
|
3023
3696
|
failFinding(
|
|
3024
3697
|
ruleId,
|
|
3025
3698
|
`Tool order ${options.before} before ${options.after} could not establish causal timing.`,
|
|
3026
|
-
[
|
|
3699
|
+
[eventEvidence2(beforeEvent), eventEvidence2(afterEvent)],
|
|
3027
3700
|
expected,
|
|
3028
3701
|
{
|
|
3029
3702
|
code: "tool.order.interval-unresolved",
|
|
@@ -3038,7 +3711,7 @@ function createToolOrderingRule(options) {
|
|
|
3038
3711
|
failFinding(
|
|
3039
3712
|
ruleId,
|
|
3040
3713
|
`Tool ${options.before} must finish before ${options.after} starts.`,
|
|
3041
|
-
[
|
|
3714
|
+
[eventEvidence2(beforeEvent), eventEvidence2(afterEvent)],
|
|
3042
3715
|
expected,
|
|
3043
3716
|
{
|
|
3044
3717
|
beforeEndedAt: new Date(beforeEnd).toISOString(),
|
|
@@ -3063,7 +3736,7 @@ function createToolOrderingRule(options) {
|
|
|
3063
3736
|
failFinding(
|
|
3064
3737
|
ruleId,
|
|
3065
3738
|
`Tool ${options.before} cannot causally happen before itself.`,
|
|
3066
|
-
[
|
|
3739
|
+
[eventEvidence2(beforeEvent)],
|
|
3067
3740
|
expected,
|
|
3068
3741
|
{ code: "tool.order.same-tool" }
|
|
3069
3742
|
)
|
|
@@ -3098,7 +3771,7 @@ function createToolOrderingRule(options) {
|
|
|
3098
3771
|
failFinding(
|
|
3099
3772
|
ruleId,
|
|
3100
3773
|
`Tool order ${options.before} before ${options.after} could not establish all causal intervals.`,
|
|
3101
|
-
[firstMissingBefore, firstMissingAfter].filter((event) => event !== void 0).map((event) =>
|
|
3774
|
+
[firstMissingBefore, firstMissingAfter].filter((event) => event !== void 0).map((event) => eventEvidence2(event)),
|
|
3102
3775
|
expected,
|
|
3103
3776
|
{
|
|
3104
3777
|
code: "tool.order.interval-unresolved",
|
|
@@ -3113,7 +3786,7 @@ function createToolOrderingRule(options) {
|
|
|
3113
3786
|
failFinding(
|
|
3114
3787
|
ruleId,
|
|
3115
3788
|
`Every ${options.before} tool call must finish before any ${options.after} tool call starts.`,
|
|
3116
|
-
[
|
|
3789
|
+
[eventEvidence2(latestBefore.event), eventEvidence2(earliestAfter.event)],
|
|
3117
3790
|
expected,
|
|
3118
3791
|
{
|
|
3119
3792
|
latestBeforeEndedAt: new Date(latestBefore.end).toISOString(),
|
|
@@ -3137,7 +3810,7 @@ function createToolOrderingRule(options) {
|
|
|
3137
3810
|
beforeEndedAt: beforeEvent.endedAt,
|
|
3138
3811
|
afterStartedAt: afterEvent.startedAt ?? afterEvent.timestamp
|
|
3139
3812
|
},
|
|
3140
|
-
evidence: [
|
|
3813
|
+
evidence: [eventEvidence2(beforeEvent), eventEvidence2(afterEvent)]
|
|
3141
3814
|
}
|
|
3142
3815
|
];
|
|
3143
3816
|
}
|
|
@@ -3160,7 +3833,7 @@ function createToolFailureRule(options) {
|
|
|
3160
3833
|
failFinding(
|
|
3161
3834
|
"tool.failures",
|
|
3162
3835
|
`Tool failure count ${failures.length} exceeded ${options.maxFailures}.`,
|
|
3163
|
-
failures.map((event) =>
|
|
3836
|
+
failures.map((event) => eventEvidence2(event)),
|
|
3164
3837
|
{ maxFailures: options.maxFailures },
|
|
3165
3838
|
failures.length
|
|
3166
3839
|
)
|
|
@@ -3173,7 +3846,7 @@ function createToolFailureRule(options) {
|
|
|
3173
3846
|
failFinding(
|
|
3174
3847
|
"tool.failures",
|
|
3175
3848
|
`Tool retry count exceeded ${options.maxRetries}.`,
|
|
3176
|
-
excessiveRetries.map((item) =>
|
|
3849
|
+
excessiveRetries.map((item) => eventEvidence2(item.event, "attributes.retryCount")),
|
|
3177
3850
|
{ maxRetries: options.maxRetries },
|
|
3178
3851
|
excessiveRetries.map((item) => ({ tool: toolName(item.event), retries: item.count }))
|
|
3179
3852
|
)
|
|
@@ -3200,7 +3873,7 @@ function createLlmUsageRule(options) {
|
|
|
3200
3873
|
failFinding(
|
|
3201
3874
|
"llm.usage",
|
|
3202
3875
|
`LLM call count ${llms.length} exceeded ${options.maxCalls}.`,
|
|
3203
|
-
llms.map((event) =>
|
|
3876
|
+
llms.map((event) => eventEvidence2(event)),
|
|
3204
3877
|
{ maxCalls: options.maxCalls },
|
|
3205
3878
|
llms.length
|
|
3206
3879
|
)
|
|
@@ -3212,17 +3885,17 @@ function createLlmUsageRule(options) {
|
|
|
3212
3885
|
const finishReason = llmFinishReason(event);
|
|
3213
3886
|
if (allowedModels && (!model || !allowedModels.has(model))) {
|
|
3214
3887
|
findings.push(
|
|
3215
|
-
failFinding("llm.usage", `LLM model ${model ?? "unknown"} is not allowed.`, [
|
|
3888
|
+
failFinding("llm.usage", `LLM model ${model ?? "unknown"} is not allowed.`, [eventEvidence2(event, "attributes.model")], [...allowedModels].sort(), model ?? "unknown")
|
|
3216
3889
|
);
|
|
3217
3890
|
}
|
|
3218
3891
|
if (allowedProviders && (!provider || !allowedProviders.has(provider))) {
|
|
3219
3892
|
findings.push(
|
|
3220
|
-
failFinding("llm.usage", `LLM provider ${provider ?? "unknown"} is not allowed.`, [
|
|
3893
|
+
failFinding("llm.usage", `LLM provider ${provider ?? "unknown"} is not allowed.`, [eventEvidence2(event, "attributes.provider")], [...allowedProviders].sort(), provider ?? "unknown")
|
|
3221
3894
|
);
|
|
3222
3895
|
}
|
|
3223
3896
|
if (finishReasons && (!finishReason || !finishReasons.has(finishReason))) {
|
|
3224
3897
|
findings.push(
|
|
3225
|
-
failFinding("llm.usage", `LLM finish reason ${finishReason ?? "unknown"} is not allowed.`, [
|
|
3898
|
+
failFinding("llm.usage", `LLM finish reason ${finishReason ?? "unknown"} is not allowed.`, [eventEvidence2(event, "attributes.finishReason")], [...finishReasons].sort(), finishReason ?? "unknown")
|
|
3226
3899
|
);
|
|
3227
3900
|
}
|
|
3228
3901
|
}
|
|
@@ -3247,7 +3920,7 @@ function createLlmUsageRule(options) {
|
|
|
3247
3920
|
failFinding(
|
|
3248
3921
|
"llm.usage",
|
|
3249
3922
|
`LLM ${key} token count ${tokenTotals[key]} exceeded ${limit}.`,
|
|
3250
|
-
llms.map((event) =>
|
|
3923
|
+
llms.map((event) => eventEvidence2(event, `tokenUsage.${key}`)),
|
|
3251
3924
|
{ [`max${key[0].toUpperCase()}${key.slice(1)}Tokens`]: limit },
|
|
3252
3925
|
tokenTotals[key]
|
|
3253
3926
|
)
|
|
@@ -3272,7 +3945,7 @@ function createStructureIncompleteRule(options = {}) {
|
|
|
3272
3945
|
failFinding(
|
|
3273
3946
|
"structure.incomplete",
|
|
3274
3947
|
"Trace contains incomplete running events.",
|
|
3275
|
-
running.map((event) =>
|
|
3948
|
+
running.map((event) => eventEvidence2(event, "status")),
|
|
3276
3949
|
"no running events",
|
|
3277
3950
|
running.length
|
|
3278
3951
|
)
|
|
@@ -3288,7 +3961,7 @@ function createStructureIncompleteRule(options = {}) {
|
|
|
3288
3961
|
failFinding(
|
|
3289
3962
|
"structure.incomplete",
|
|
3290
3963
|
"Trace contains events with startedAt but no endedAt.",
|
|
3291
|
-
missingEndedAt.map((event) =>
|
|
3964
|
+
missingEndedAt.map((event) => eventEvidence2(event, "endedAt")),
|
|
3292
3965
|
"endedAt for started events",
|
|
3293
3966
|
missingEndedAt.length
|
|
3294
3967
|
)
|
|
@@ -3316,7 +3989,7 @@ function createStructureOrphanRule(options = {}) {
|
|
|
3316
3989
|
failFinding(
|
|
3317
3990
|
"structure.orphan",
|
|
3318
3991
|
"Trace contains events whose parentId is not present in the selected run.",
|
|
3319
|
-
orphans.map((event) =>
|
|
3992
|
+
orphans.map((event) => eventEvidence2(event, "parentId")),
|
|
3320
3993
|
"parentId resolves to an event in the selected run",
|
|
3321
3994
|
orphans.length
|
|
3322
3995
|
)
|
|
@@ -3348,7 +4021,7 @@ function createStructureCycleRule() {
|
|
|
3348
4021
|
failFinding(
|
|
3349
4022
|
"structure.cycle",
|
|
3350
4023
|
formatProgrammaticDiagnostic("AI_TRACE_RELATIONSHIP_CYCLE"),
|
|
3351
|
-
cycle.map((item) =>
|
|
4024
|
+
cycle.map((item) => eventEvidence2(item, "parentId")),
|
|
3352
4025
|
"acyclic parentId graph",
|
|
3353
4026
|
cycle.map((item) => item.eventId).sort()
|
|
3354
4027
|
)
|
|
@@ -3380,7 +4053,7 @@ function createStructureRelationshipRule(options = {}) {
|
|
|
3380
4053
|
failFinding(
|
|
3381
4054
|
"structure.relationship",
|
|
3382
4055
|
`Event confidence ${event.confidence} is below ${minConfidence}.`,
|
|
3383
|
-
[
|
|
4056
|
+
[eventEvidence2(event, "confidence")],
|
|
3384
4057
|
{ minConfidence },
|
|
3385
4058
|
event.confidence
|
|
3386
4059
|
)
|
|
@@ -3392,7 +4065,7 @@ function createStructureRelationshipRule(options = {}) {
|
|
|
3392
4065
|
failFinding(
|
|
3393
4066
|
"structure.relationship",
|
|
3394
4067
|
"Event parentId points to itself.",
|
|
3395
|
-
[
|
|
4068
|
+
[eventEvidence2(event, "parentId")],
|
|
3396
4069
|
"parentId references a distinct event",
|
|
3397
4070
|
"self"
|
|
3398
4071
|
)
|
|
@@ -3409,7 +4082,7 @@ function createStructureRelationshipRule(options = {}) {
|
|
|
3409
4082
|
failFinding(
|
|
3410
4083
|
"structure.relationship",
|
|
3411
4084
|
"Parent event starts after child event.",
|
|
3412
|
-
[
|
|
4085
|
+
[eventEvidence2(parent), eventEvidence2(event, "parentId")],
|
|
3413
4086
|
"parent start <= child start",
|
|
3414
4087
|
{ parentEventId: parent.eventId, childEventId: event.eventId }
|
|
3415
4088
|
)
|
|
@@ -3423,7 +4096,7 @@ function createStructureRelationshipRule(options = {}) {
|
|
|
3423
4096
|
failFinding(
|
|
3424
4097
|
"structure.relationship",
|
|
3425
4098
|
"Trace parentSpanId does not match parent spanId.",
|
|
3426
|
-
[
|
|
4099
|
+
[eventEvidence2(event, "trace.parentSpanId")],
|
|
3427
4100
|
{ parentSpanId: parent.trace.spanId },
|
|
3428
4101
|
actual ?? "missing"
|
|
3429
4102
|
)
|
|
@@ -3452,7 +4125,7 @@ function createStructureParallelWidthRule(options) {
|
|
|
3452
4125
|
"structure.parallelWidth",
|
|
3453
4126
|
`Parent ${parentId} has ${children.length} children, exceeding ${options.maxChildren}.`,
|
|
3454
4127
|
[
|
|
3455
|
-
...parent ? [
|
|
4128
|
+
...parent ? [eventEvidence2(parent)] : [{ runId: context.selectedRun?.runId, eventId: parentId }],
|
|
3456
4129
|
...children.map((child) => ({
|
|
3457
4130
|
runId: child.event.runId,
|
|
3458
4131
|
eventId: child.event.eventId,
|
|
@@ -3500,7 +4173,7 @@ function createStructureParallelWidthRule(options) {
|
|
|
3500
4173
|
failFinding(
|
|
3501
4174
|
"structure.parallelWidth",
|
|
3502
4175
|
`Concurrent event width ${maxActive.length} exceeded ${options.maxConcurrent}.`,
|
|
3503
|
-
maxActive.map((event) =>
|
|
4176
|
+
maxActive.map((event) => eventEvidence2(event)),
|
|
3504
4177
|
{ maxConcurrent: options.maxConcurrent },
|
|
3505
4178
|
maxActive.length
|
|
3506
4179
|
)
|
|
@@ -3543,7 +4216,7 @@ function createSignalRule(ruleId, label, options, selectEvents, nameForEvent) {
|
|
|
3543
4216
|
failFinding(
|
|
3544
4217
|
ruleId,
|
|
3545
4218
|
`Forbidden ${label} ${name} appeared.`,
|
|
3546
|
-
[
|
|
4219
|
+
[eventEvidence2(event)],
|
|
3547
4220
|
`${label} absent`,
|
|
3548
4221
|
name
|
|
3549
4222
|
)
|
|
@@ -3554,7 +4227,7 @@ function createSignalRule(ruleId, label, options, selectEvents, nameForEvent) {
|
|
|
3554
4227
|
failFinding(
|
|
3555
4228
|
ruleId,
|
|
3556
4229
|
`${label[0].toUpperCase()}${label.slice(1)} ${name} is not in the allowed set.`,
|
|
3557
|
-
[
|
|
4230
|
+
[eventEvidence2(event)],
|
|
3558
4231
|
[...allowed].sort(),
|
|
3559
4232
|
name
|
|
3560
4233
|
)
|
|
@@ -3577,7 +4250,7 @@ function createSignalRule(ruleId, label, options, selectEvents, nameForEvent) {
|
|
|
3577
4250
|
failFinding(
|
|
3578
4251
|
ruleId,
|
|
3579
4252
|
`${label[0].toUpperCase()}${label.slice(1)} count ${events.length} exceeded maximum ${options.maxCount}.`,
|
|
3580
|
-
events.map((event) =>
|
|
4253
|
+
events.map((event) => eventEvidence2(event)),
|
|
3581
4254
|
{ maxCount: options.maxCount },
|
|
3582
4255
|
events.length
|
|
3583
4256
|
)
|
|
@@ -3631,7 +4304,7 @@ function createSafetyRedactionRule(options = {}) {
|
|
|
3631
4304
|
failFinding(
|
|
3632
4305
|
"safety.redaction",
|
|
3633
4306
|
`Sensitive-looking field at ${entry.path} is not redacted.`,
|
|
3634
|
-
[
|
|
4307
|
+
[eventEvidence2(event, entry.path)],
|
|
3635
4308
|
"redaction marker",
|
|
3636
4309
|
{ path: entry.path, valueType: valueType(entry.value) },
|
|
3637
4310
|
{
|
|
@@ -3665,7 +4338,7 @@ function createSafetyRawContentRule(options = {}) {
|
|
|
3665
4338
|
failFinding(
|
|
3666
4339
|
"safety.rawPrompt",
|
|
3667
4340
|
`Raw content-like field ${entry.path} is present.`,
|
|
3668
|
-
[
|
|
4341
|
+
[eventEvidence2(event, entry.path)],
|
|
3669
4342
|
"metadata-only trace fields",
|
|
3670
4343
|
{ path: entry.path, valueType: valueType(entry.value) },
|
|
3671
4344
|
{
|
|
@@ -3703,7 +4376,7 @@ function createSafetySecretPatternRule(options = {}) {
|
|
|
3703
4376
|
failFinding(
|
|
3704
4377
|
"safety.secretPattern",
|
|
3705
4378
|
`Secret-like pattern ${pattern.id} matched at ${entry.path}.`,
|
|
3706
|
-
[
|
|
4379
|
+
[eventEvidence2(event, entry.path)],
|
|
3707
4380
|
"no secret-like strings",
|
|
3708
4381
|
{ pattern: pattern.id, path: entry.path },
|
|
3709
4382
|
{
|
|
@@ -3736,7 +4409,7 @@ function createSafetyOversizedAttributeRule(options) {
|
|
|
3736
4409
|
failFinding(
|
|
3737
4410
|
"safety.oversizedAttribute",
|
|
3738
4411
|
`String at ${entry.path} exceeds ${options.maxStringLength} characters.`,
|
|
3739
|
-
[
|
|
4412
|
+
[eventEvidence2(event, entry.path)],
|
|
3740
4413
|
{ maxStringLength: options.maxStringLength },
|
|
3741
4414
|
{ path: entry.path, length: entry.value.length },
|
|
3742
4415
|
{
|
|
@@ -3753,7 +4426,7 @@ function createSafetyOversizedAttributeRule(options) {
|
|
|
3753
4426
|
failFinding(
|
|
3754
4427
|
"safety.oversizedAttribute",
|
|
3755
4428
|
`Array at ${entry.path} exceeds ${options.maxArrayLength} items.`,
|
|
3756
|
-
[
|
|
4429
|
+
[eventEvidence2(event, entry.path)],
|
|
3757
4430
|
{ maxArrayLength: options.maxArrayLength },
|
|
3758
4431
|
{ path: entry.path, length: entry.value.length },
|
|
3759
4432
|
{
|
|
@@ -3770,7 +4443,7 @@ function createSafetyOversizedAttributeRule(options) {
|
|
|
3770
4443
|
failFinding(
|
|
3771
4444
|
"safety.oversizedAttribute",
|
|
3772
4445
|
`Object at ${entry.path} exceeds ${options.maxObjectKeys} keys.`,
|
|
3773
|
-
[
|
|
4446
|
+
[eventEvidence2(event, entry.path)],
|
|
3774
4447
|
{ maxObjectKeys: options.maxObjectKeys },
|
|
3775
4448
|
{ path: entry.path, keys: Object.keys(entry.value).length },
|
|
3776
4449
|
{
|
|
@@ -3789,7 +4462,7 @@ function createSafetyOversizedAttributeRule(options) {
|
|
|
3789
4462
|
failFinding(
|
|
3790
4463
|
"safety.oversizedAttribute",
|
|
3791
4464
|
`Value at ${entry.path} exceeds ${options.maxSerializedBytes} serialized bytes.`,
|
|
3792
|
-
[
|
|
4465
|
+
[eventEvidence2(event, entry.path)],
|
|
3793
4466
|
{ maxSerializedBytes: options.maxSerializedBytes },
|
|
3794
4467
|
{ path: entry.path, bytes },
|
|
3795
4468
|
{
|
|
@@ -3916,7 +4589,7 @@ function createBaselineRegressionRule(options) {
|
|
|
3916
4589
|
path: "guardrail",
|
|
3917
4590
|
expected: guardrailShape(baselineContext),
|
|
3918
4591
|
actual: guardrailShape(context),
|
|
3919
|
-
evidence: guardrailEvents(context)[0] ? [
|
|
4592
|
+
evidence: guardrailEvents(context)[0] ? [eventEvidence2(guardrailEvents(context)[0], "guardrail")] : runEvidence(context.selectedRun)
|
|
3920
4593
|
}
|
|
3921
4594
|
];
|
|
3922
4595
|
for (const comparison of comparisons) {
|
|
@@ -4086,13 +4759,16 @@ exports.createToolUsageRule = createToolUsageRule;
|
|
|
4086
4759
|
exports.defineTraceContract = defineTraceContract;
|
|
4087
4760
|
exports.deriveFailureFacts = deriveFailureFacts;
|
|
4088
4761
|
exports.deriveRelationshipFacts = deriveRelationshipFacts;
|
|
4762
|
+
exports.evaluateToolArgumentValue = evaluateToolArgumentValue;
|
|
4089
4763
|
exports.evaluateTraceContract = evaluateTraceContract;
|
|
4090
4764
|
exports.evaluateTraceContractRead = evaluateTraceContractRead;
|
|
4091
4765
|
exports.explainTraceContract = explainTraceContract;
|
|
4766
|
+
exports.extractToolArgumentPayload = extractToolArgumentPayload;
|
|
4092
4767
|
exports.formatProgrammaticDiagnostic = formatProgrammaticDiagnostic;
|
|
4093
4768
|
exports.lintTraceContract = lintTraceContract;
|
|
4094
4769
|
exports.projectLogicalEvents = projectLogicalEvents;
|
|
4095
4770
|
exports.resolveCanonicalToolName = resolveCanonicalToolName;
|
|
4771
|
+
exports.resolveJsonPointer = resolveJsonPointer;
|
|
4096
4772
|
exports.resolveTraceContractScope = resolveTraceContractScope;
|
|
4097
4773
|
exports.runTraceChecks = runTraceChecks;
|
|
4098
4774
|
exports.summarizeSemanticParity = summarizeSemanticParity;
|