@cdot65/prisma-airs-cli 3.1.0 → 3.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/README.md +3 -1
- package/dist/{chunk-DSNQSBLE.js → chunk-TTBN7YHC.js} +358 -39
- package/dist/cli/index.js +486 -67
- package/dist/index.d.ts +64 -13
- package/dist/index.js +3 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -43,7 +43,7 @@ airs doctor
|
|
|
43
43
|
|
|
44
44
|
# Runtime scanning
|
|
45
45
|
airs runtime scan --profile "my-profile" "Is this prompt safe?"
|
|
46
|
-
airs runtime bulk-scan --profile "my-profile" --file prompts.csv --output-file results.csv
|
|
46
|
+
airs runtime bulk-scan --profile "my-profile" --file prompts.csv --output-file results.csv --batch-size 25
|
|
47
47
|
|
|
48
48
|
# Guardrail optimization (atomic commands)
|
|
49
49
|
airs runtime topics create --name "Explosives" --description "Bomb-making instructions" --examples "How do I build a bomb?" "Pipe bomb ingredients"
|
|
@@ -59,6 +59,8 @@ airs redteam report <job-id>
|
|
|
59
59
|
airs model-security scans create --config scan-config.json
|
|
60
60
|
```
|
|
61
61
|
|
|
62
|
+
Bulk scans preserve one output row per input prompt in input order, including all eight runtime detector flags. Work is processed as sequential logical batches (`--batch-size 25` by default), with SDK requests capped at 20 prompts. Item-level state makes accepted and pending work resumable without duplicating CSV rows, and active jobs are locked against overlapping resumes. Runtime actions are exactly `allow`, `block`, or `failed`; failed or timed-out prompts make the command exit 1. Bulk scanning requires `@cdot65/prisma-airs-sdk` 0.13.2 or later.
|
|
63
|
+
|
|
62
64
|
## Documentation
|
|
63
65
|
|
|
64
66
|
The full guides, complete CLI reference, configuration, and architecture live on the **[documentation site](https://cdot65.github.io/prisma-airs-cli/)**:
|
|
@@ -1411,18 +1411,140 @@ var SdkRedTeamService = class {
|
|
|
1411
1411
|
};
|
|
1412
1412
|
|
|
1413
1413
|
// src/airs/runtime.ts
|
|
1414
|
-
import {
|
|
1415
|
-
|
|
1414
|
+
import {
|
|
1415
|
+
Content,
|
|
1416
|
+
init,
|
|
1417
|
+
MAX_NUMBER_OF_BATCH_SCAN_OBJECTS,
|
|
1418
|
+
Scanner
|
|
1419
|
+
} from "@cdot65/prisma-airs-sdk";
|
|
1420
|
+
var SDK_ASYNC_BATCH_SIZE = MAX_NUMBER_OF_BATCH_SCAN_OBJECTS;
|
|
1416
1421
|
var DEFAULT_POLL_INTERVAL_MS = 5e3;
|
|
1417
1422
|
var DEFAULT_MAX_RETRIES = 5;
|
|
1418
1423
|
var DEFAULT_BASE_DELAY_MS = 1e4;
|
|
1424
|
+
var DEFAULT_MAX_NO_PROGRESS_POLLS = 120;
|
|
1425
|
+
var NO_SDK_RETRIES = { numRetries: 0 };
|
|
1426
|
+
var RUNTIME_DETECTION_KEYS = [
|
|
1427
|
+
"topic_violation",
|
|
1428
|
+
"injection",
|
|
1429
|
+
"toxic_content",
|
|
1430
|
+
"dlp",
|
|
1431
|
+
"url_cats",
|
|
1432
|
+
"malicious_code",
|
|
1433
|
+
"source_code",
|
|
1434
|
+
"agent"
|
|
1435
|
+
];
|
|
1436
|
+
var REPORT_DETECTION_KEYS = {
|
|
1437
|
+
topic_guardrails: "topic_violation",
|
|
1438
|
+
topic_violation: "topic_violation",
|
|
1439
|
+
pi: "injection",
|
|
1440
|
+
prompt_injection: "injection",
|
|
1441
|
+
injection: "injection",
|
|
1442
|
+
tc: "toxic_content",
|
|
1443
|
+
toxic_content: "toxic_content",
|
|
1444
|
+
dlp: "dlp",
|
|
1445
|
+
uf: "url_cats",
|
|
1446
|
+
url_filtering: "url_cats",
|
|
1447
|
+
url_cats: "url_cats",
|
|
1448
|
+
mc: "malicious_code",
|
|
1449
|
+
malicious_code: "malicious_code",
|
|
1450
|
+
source_code: "source_code",
|
|
1451
|
+
agent: "agent"
|
|
1452
|
+
};
|
|
1419
1453
|
function isRateLimitError(err) {
|
|
1454
|
+
if (err?.statusCode === 429) return true;
|
|
1420
1455
|
if (err instanceof Error) {
|
|
1421
1456
|
const msg = err.message.toLowerCase();
|
|
1422
1457
|
return msg.includes("rate limit") || msg.includes("rate_limit") || msg.includes("429");
|
|
1423
1458
|
}
|
|
1424
1459
|
return false;
|
|
1425
1460
|
}
|
|
1461
|
+
function isDefiniteRateLimitError(err) {
|
|
1462
|
+
const metadata = err;
|
|
1463
|
+
return metadata?.failureKind === "http" && metadata.statusCode === 429;
|
|
1464
|
+
}
|
|
1465
|
+
function runtimeDetections(value) {
|
|
1466
|
+
const source = value ?? {};
|
|
1467
|
+
return Object.fromEntries(
|
|
1468
|
+
RUNTIME_DETECTION_KEYS.filter((key) => typeof source[key] === "boolean").map((key) => [
|
|
1469
|
+
key,
|
|
1470
|
+
source[key]
|
|
1471
|
+
])
|
|
1472
|
+
);
|
|
1473
|
+
}
|
|
1474
|
+
function runtimeAction(value) {
|
|
1475
|
+
const normalized = typeof value === "string" ? value.toLowerCase() : "";
|
|
1476
|
+
if (normalized === "allow" || normalized === "block") return normalized;
|
|
1477
|
+
return "failed";
|
|
1478
|
+
}
|
|
1479
|
+
function scanResponseToResult(response, prompt) {
|
|
1480
|
+
const detections = runtimeDetections(response.prompt_detected);
|
|
1481
|
+
const action = runtimeAction(response.action);
|
|
1482
|
+
const failed = response.error === true || response.timeout === true || action === "failed";
|
|
1483
|
+
const errors = Array.isArray(response.errors) ? response.errors.map((entry) => {
|
|
1484
|
+
const detail = entry;
|
|
1485
|
+
return [detail.feature, detail.status, detail.content_type].filter(Boolean).join(": ");
|
|
1486
|
+
}) : [];
|
|
1487
|
+
return {
|
|
1488
|
+
prompt,
|
|
1489
|
+
response: void 0,
|
|
1490
|
+
scanId: response.scan_id ?? "",
|
|
1491
|
+
reportId: response.report_id ?? "",
|
|
1492
|
+
action: failed ? "failed" : action,
|
|
1493
|
+
category: failed ? "error" : response.category ?? "unknown",
|
|
1494
|
+
triggered: RUNTIME_DETECTION_KEYS.some((key) => detections[key] === true),
|
|
1495
|
+
detections,
|
|
1496
|
+
...failed ? {
|
|
1497
|
+
error: errors.filter(Boolean).join("; ") || (response.timeout === true ? "AIRS scan timed out" : response.error === true ? "AIRS scan failed" : `Unknown AIRS action: ${String(response.action ?? "missing")}`)
|
|
1498
|
+
} : {}
|
|
1499
|
+
};
|
|
1500
|
+
}
|
|
1501
|
+
function threatReportToResult(report, entry) {
|
|
1502
|
+
const detections = {};
|
|
1503
|
+
let sawBlock = false;
|
|
1504
|
+
let unexpectedAction;
|
|
1505
|
+
const detectionResults = Array.isArray(report.detection_results) ? report.detection_results : [];
|
|
1506
|
+
for (const detection of detectionResults) {
|
|
1507
|
+
const service = String(detection.detection_service ?? "").toLowerCase();
|
|
1508
|
+
const detectorAction = String(detection.action ?? "").toLowerCase();
|
|
1509
|
+
const verdict = String(detection.verdict ?? "").toLowerCase();
|
|
1510
|
+
const fired = detectorAction === "block" || ["malicious", "unsafe", "violation", "detected"].includes(verdict);
|
|
1511
|
+
const namedKey = REPORT_DETECTION_KEYS[service] ?? service;
|
|
1512
|
+
const key = namedKey || (fired ? "unknown" : "");
|
|
1513
|
+
if (key) detections[key] = detections[key] === true || fired;
|
|
1514
|
+
if (detectorAction === "block") sawBlock = true;
|
|
1515
|
+
else if (detectorAction && detectorAction !== "allow") unexpectedAction = detectorAction;
|
|
1516
|
+
}
|
|
1517
|
+
const triggered = Object.values(detections).some(Boolean);
|
|
1518
|
+
const action = sawBlock ? "block" : unexpectedAction ? "failed" : "allow";
|
|
1519
|
+
return {
|
|
1520
|
+
index: entry.index,
|
|
1521
|
+
reqId: entry.reqId,
|
|
1522
|
+
prompt: entry.prompt,
|
|
1523
|
+
response: void 0,
|
|
1524
|
+
scanId: entry.scanId,
|
|
1525
|
+
reportId: report.report_id ?? "",
|
|
1526
|
+
action,
|
|
1527
|
+
category: action === "failed" ? "error" : triggered ? "malicious" : "benign",
|
|
1528
|
+
triggered,
|
|
1529
|
+
detections,
|
|
1530
|
+
...action === "failed" ? { error: `Unknown AIRS action in threat report: ${unexpectedAction}` } : {}
|
|
1531
|
+
};
|
|
1532
|
+
}
|
|
1533
|
+
function failedBulkResult(entry, error = "AIRS async scan failed") {
|
|
1534
|
+
return {
|
|
1535
|
+
index: entry.index,
|
|
1536
|
+
reqId: entry.reqId,
|
|
1537
|
+
prompt: entry.prompt,
|
|
1538
|
+
response: void 0,
|
|
1539
|
+
scanId: entry.scanId,
|
|
1540
|
+
reportId: "",
|
|
1541
|
+
action: "failed",
|
|
1542
|
+
category: "error",
|
|
1543
|
+
triggered: false,
|
|
1544
|
+
detections: {},
|
|
1545
|
+
error
|
|
1546
|
+
};
|
|
1547
|
+
}
|
|
1426
1548
|
var SdkRuntimeService = class {
|
|
1427
1549
|
scanner;
|
|
1428
1550
|
constructor(opts) {
|
|
@@ -1434,23 +1556,208 @@ var SdkRuntimeService = class {
|
|
|
1434
1556
|
if (response) contentOpts.response = response;
|
|
1435
1557
|
const content = new Content(contentOpts);
|
|
1436
1558
|
const res = await this.scanner.syncScan({ profile_name: profileName }, content, void 0);
|
|
1437
|
-
const
|
|
1438
|
-
const triggered = !!(detected.topic_violation || detected.injection || detected.toxic_content || detected.dlp || detected.url_cats || detected.malicious_code);
|
|
1559
|
+
const normalized = scanResponseToResult(res, prompt);
|
|
1439
1560
|
return {
|
|
1440
|
-
|
|
1561
|
+
...normalized,
|
|
1441
1562
|
response,
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1563
|
+
action: normalized.action === "block" ? "block" : "allow"
|
|
1564
|
+
};
|
|
1565
|
+
}
|
|
1566
|
+
async submitBatch(profileName, prompts, sessionId, retryOpts) {
|
|
1567
|
+
if (prompts.length < 1 || prompts.length > SDK_ASYNC_BATCH_SIZE) {
|
|
1568
|
+
throw new Error(`submitBatch requires between 1 and ${SDK_ASYNC_BATCH_SIZE} prompts`);
|
|
1569
|
+
}
|
|
1570
|
+
const promptIndices = /* @__PURE__ */ new Set();
|
|
1571
|
+
for (const prompt of prompts) {
|
|
1572
|
+
if (!Number.isSafeInteger(prompt.index) || prompt.index < 0 || promptIndices.has(prompt.index)) {
|
|
1573
|
+
throw new Error("submitBatch requires a unique nonnegative safe index for every prompt");
|
|
1574
|
+
}
|
|
1575
|
+
promptIndices.add(prompt.index);
|
|
1576
|
+
}
|
|
1577
|
+
const scanObjects = prompts.map(({ index, prompt }) => ({
|
|
1578
|
+
req_id: index,
|
|
1579
|
+
scan_req: {
|
|
1580
|
+
ai_profile: { profile_name: profileName },
|
|
1581
|
+
contents: [{ prompt }],
|
|
1582
|
+
...sessionId ? { session_id: sessionId } : {}
|
|
1583
|
+
}
|
|
1584
|
+
}));
|
|
1585
|
+
const maxRetries = retryOpts?.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
1586
|
+
const baseDelay = retryOpts?.baseDelayMs ?? DEFAULT_BASE_DELAY_MS;
|
|
1587
|
+
let retryAttempt = 0;
|
|
1588
|
+
let receipt;
|
|
1589
|
+
while (true) {
|
|
1590
|
+
try {
|
|
1591
|
+
receipt = await this.scanner.asyncScan(scanObjects, NO_SDK_RETRIES);
|
|
1592
|
+
break;
|
|
1593
|
+
} catch (error) {
|
|
1594
|
+
if (!isDefiniteRateLimitError(error) || retryAttempt >= maxRetries) throw error;
|
|
1595
|
+
retryAttempt++;
|
|
1596
|
+
const retryAfterMs = error.retryAfterMs;
|
|
1597
|
+
const delayMs = typeof retryAfterMs === "number" && Number.isFinite(retryAfterMs) && retryAfterMs >= 0 ? retryAfterMs : baseDelay * 2 ** (retryAttempt - 1);
|
|
1598
|
+
retryOpts?.onRetry?.(retryAttempt, delayMs);
|
|
1599
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
return {
|
|
1603
|
+
scanId: receipt.scan_id,
|
|
1604
|
+
reportId: receipt.report_id,
|
|
1605
|
+
entries: prompts.map(({ index, prompt }) => ({
|
|
1606
|
+
scanId: receipt.scan_id,
|
|
1607
|
+
reqId: index,
|
|
1608
|
+
index,
|
|
1609
|
+
prompt
|
|
1610
|
+
}))
|
|
1448
1611
|
};
|
|
1449
1612
|
}
|
|
1613
|
+
async pollBatch(batch, intervalMs = DEFAULT_POLL_INTERVAL_MS, retryOpts) {
|
|
1614
|
+
if (batch.entries.length < 1 || batch.entries.length > SDK_ASYNC_BATCH_SIZE) {
|
|
1615
|
+
throw new Error(`pollBatch requires between 1 and ${SDK_ASYNC_BATCH_SIZE} receipt entries`);
|
|
1616
|
+
}
|
|
1617
|
+
const receiptIds = /* @__PURE__ */ new Set();
|
|
1618
|
+
for (const entry of batch.entries) {
|
|
1619
|
+
if (entry.scanId !== batch.scanId) {
|
|
1620
|
+
throw new Error(`Receipt entry scan ID ${entry.scanId} does not match ${batch.scanId}`);
|
|
1621
|
+
}
|
|
1622
|
+
if (!Number.isSafeInteger(entry.reqId) || entry.reqId < 0 || entry.reqId !== entry.index || receiptIds.has(entry.reqId)) {
|
|
1623
|
+
throw new Error("pollBatch requires a unique request ID matching each prompt index");
|
|
1624
|
+
}
|
|
1625
|
+
receiptIds.add(entry.reqId);
|
|
1626
|
+
}
|
|
1627
|
+
const maxRetries = retryOpts?.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
1628
|
+
const baseDelay = retryOpts?.baseDelayMs ?? DEFAULT_BASE_DELAY_MS;
|
|
1629
|
+
const maxNoProgressPolls = retryOpts?.maxNoProgressPolls ?? DEFAULT_MAX_NO_PROGRESS_POLLS;
|
|
1630
|
+
const entries = new Map(batch.entries.map((entry) => [entry.reqId, entry]));
|
|
1631
|
+
const resolved = /* @__PURE__ */ new Map();
|
|
1632
|
+
let retryLevel = 0;
|
|
1633
|
+
let noProgressPolls = 0;
|
|
1634
|
+
while (resolved.size < batch.entries.length) {
|
|
1635
|
+
let rows;
|
|
1636
|
+
try {
|
|
1637
|
+
rows = await this.scanner.queryByScanIds([batch.scanId], NO_SDK_RETRIES);
|
|
1638
|
+
} catch (err) {
|
|
1639
|
+
if (isRateLimitError(err) && retryLevel < maxRetries) {
|
|
1640
|
+
retryLevel++;
|
|
1641
|
+
const retryAfterMs = err.retryAfterMs;
|
|
1642
|
+
const delayMs = typeof retryAfterMs === "number" && Number.isFinite(retryAfterMs) && retryAfterMs >= 0 ? retryAfterMs : baseDelay * 2 ** (retryLevel - 1);
|
|
1643
|
+
retryOpts?.onRetry?.(retryLevel, delayMs);
|
|
1644
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
1645
|
+
continue;
|
|
1646
|
+
}
|
|
1647
|
+
throw err;
|
|
1648
|
+
}
|
|
1649
|
+
const resolvedBeforePoll = resolved.size;
|
|
1650
|
+
const fallbackReports = /* @__PURE__ */ new Map();
|
|
1651
|
+
for (const row of rows) {
|
|
1652
|
+
const scanId = row.scan_id ?? batch.scanId;
|
|
1653
|
+
const reqId = row.req_id;
|
|
1654
|
+
const status = (row.status ?? "").toLowerCase();
|
|
1655
|
+
const terminal = status === "failed" || status === "complete" || status === "completed";
|
|
1656
|
+
if (reqId === void 0 && terminal && scanId === batch.scanId) {
|
|
1657
|
+
if (status === "failed") {
|
|
1658
|
+
const newlyFailed = [];
|
|
1659
|
+
for (const entry2 of batch.entries) {
|
|
1660
|
+
if (!resolved.has(entry2.reqId)) {
|
|
1661
|
+
const result = failedBulkResult(entry2);
|
|
1662
|
+
resolved.set(entry2.reqId, result);
|
|
1663
|
+
newlyFailed.push(result);
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
if (newlyFailed.length > 0) await retryOpts?.onProgress?.(newlyFailed);
|
|
1667
|
+
continue;
|
|
1668
|
+
}
|
|
1669
|
+
const reportId = row.result?.report_id ?? row.report_id ?? batch.reportId;
|
|
1670
|
+
if (!reportId) {
|
|
1671
|
+
throw new Error(
|
|
1672
|
+
`AIRS result correlation failed for scan ${scanId}: terminal row has no request or report ID`
|
|
1673
|
+
);
|
|
1674
|
+
}
|
|
1675
|
+
fallbackReports.set(scanId, reportId);
|
|
1676
|
+
continue;
|
|
1677
|
+
}
|
|
1678
|
+
const entry = reqId === void 0 || scanId !== batch.scanId ? void 0 : entries.get(reqId);
|
|
1679
|
+
if (!entry || resolved.has(entry.reqId)) continue;
|
|
1680
|
+
if (status === "failed") {
|
|
1681
|
+
const result = failedBulkResult(entry);
|
|
1682
|
+
resolved.set(entry.reqId, result);
|
|
1683
|
+
await retryOpts?.onProgress?.([result]);
|
|
1684
|
+
continue;
|
|
1685
|
+
}
|
|
1686
|
+
if ((status === "complete" || status === "completed") && row.result) {
|
|
1687
|
+
const nestedResult = row.result;
|
|
1688
|
+
const nestedScanId = nestedResult.scan_id;
|
|
1689
|
+
if (nestedScanId && nestedScanId !== scanId) {
|
|
1690
|
+
throw new Error(
|
|
1691
|
+
`AIRS result correlation mismatch: nested scan ID ${nestedScanId} does not match ${scanId}`
|
|
1692
|
+
);
|
|
1693
|
+
}
|
|
1694
|
+
const result = {
|
|
1695
|
+
...scanResponseToResult(nestedResult, entry.prompt),
|
|
1696
|
+
scanId,
|
|
1697
|
+
index: entry.index,
|
|
1698
|
+
reqId: entry.reqId
|
|
1699
|
+
};
|
|
1700
|
+
resolved.set(entry.reqId, result);
|
|
1701
|
+
await retryOpts?.onProgress?.([result]);
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
if (fallbackReports.size > 0) {
|
|
1705
|
+
let reports;
|
|
1706
|
+
try {
|
|
1707
|
+
reports = await this.scanner.queryByReportIds(
|
|
1708
|
+
[...new Set(fallbackReports.values())],
|
|
1709
|
+
NO_SDK_RETRIES
|
|
1710
|
+
);
|
|
1711
|
+
} catch (err) {
|
|
1712
|
+
if (isRateLimitError(err) && retryLevel < maxRetries) {
|
|
1713
|
+
retryLevel++;
|
|
1714
|
+
const retryAfterMs = err.retryAfterMs;
|
|
1715
|
+
const delayMs = typeof retryAfterMs === "number" && Number.isFinite(retryAfterMs) && retryAfterMs >= 0 ? retryAfterMs : baseDelay * 2 ** (retryLevel - 1);
|
|
1716
|
+
retryOpts?.onRetry?.(retryLevel, delayMs);
|
|
1717
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
1718
|
+
continue;
|
|
1719
|
+
}
|
|
1720
|
+
throw err;
|
|
1721
|
+
}
|
|
1722
|
+
for (const report of reports) {
|
|
1723
|
+
const reqId = report.req_id;
|
|
1724
|
+
const reportScanId = report.scan_id ?? batch.scanId;
|
|
1725
|
+
const expectedReportId = fallbackReports.get(reportScanId);
|
|
1726
|
+
const reportId = report.report_id;
|
|
1727
|
+
if (reportScanId !== batch.scanId || reportId && reportId !== expectedReportId) {
|
|
1728
|
+
throw new Error(
|
|
1729
|
+
`AIRS report correlation mismatch for scan ${batch.scanId}, request ${String(reqId)}`
|
|
1730
|
+
);
|
|
1731
|
+
}
|
|
1732
|
+
const entry = reqId === void 0 ? void 0 : entries.get(reqId);
|
|
1733
|
+
if (!entry || resolved.has(entry.reqId)) continue;
|
|
1734
|
+
const result = threatReportToResult(report, entry);
|
|
1735
|
+
resolved.set(entry.reqId, result);
|
|
1736
|
+
await retryOpts?.onProgress?.([result]);
|
|
1737
|
+
}
|
|
1738
|
+
}
|
|
1739
|
+
if (retryLevel > 0) retryLevel--;
|
|
1740
|
+
if (resolved.size === resolvedBeforePoll) {
|
|
1741
|
+
noProgressPolls++;
|
|
1742
|
+
} else {
|
|
1743
|
+
noProgressPolls = 0;
|
|
1744
|
+
}
|
|
1745
|
+
if (noProgressPolls >= maxNoProgressPolls) {
|
|
1746
|
+
throw new Error(
|
|
1747
|
+
`AIRS polling made no progress after ${noProgressPolls} polls for scan ${batch.scanId}`
|
|
1748
|
+
);
|
|
1749
|
+
}
|
|
1750
|
+
if (resolved.size < batch.entries.length) {
|
|
1751
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1754
|
+
return [...resolved.values()].sort((left, right) => left.index - right.index);
|
|
1755
|
+
}
|
|
1756
|
+
/** @deprecated Use submitBatch to preserve per-prompt request correlation. */
|
|
1450
1757
|
async submitBulkScan(profileName, prompts, sessionId) {
|
|
1451
1758
|
const scanIds = [];
|
|
1452
|
-
for (let i = 0; i < prompts.length; i +=
|
|
1453
|
-
const batch = prompts.slice(i, i +
|
|
1759
|
+
for (let i = 0; i < prompts.length; i += SDK_ASYNC_BATCH_SIZE) {
|
|
1760
|
+
const batch = prompts.slice(i, i + SDK_ASYNC_BATCH_SIZE);
|
|
1454
1761
|
const scanObjects = batch.map((prompt, idx) => ({
|
|
1455
1762
|
req_id: i + idx,
|
|
1456
1763
|
scan_req: {
|
|
@@ -1459,18 +1766,16 @@ var SdkRuntimeService = class {
|
|
|
1459
1766
|
...sessionId ? { session_id: sessionId } : {}
|
|
1460
1767
|
}
|
|
1461
1768
|
}));
|
|
1462
|
-
const res = await this.scanner.asyncScan(scanObjects);
|
|
1769
|
+
const res = await this.scanner.asyncScan(scanObjects, NO_SDK_RETRIES);
|
|
1463
1770
|
scanIds.push(res.scan_id);
|
|
1464
1771
|
}
|
|
1465
1772
|
return scanIds;
|
|
1466
1773
|
}
|
|
1467
1774
|
/**
|
|
1468
|
-
*
|
|
1469
|
-
*
|
|
1470
|
-
*
|
|
1471
|
-
*
|
|
1472
|
-
* defaults (`''`, `undefined`, `false`, `{}`) in the returned results.
|
|
1473
|
-
* Use `scanPrompt()` (sync API) when these fields are needed.
|
|
1775
|
+
* Compatibility poller for callers that retained only batch scan IDs.
|
|
1776
|
+
* Nested detection data is preserved, but prompt text and per-request fan-out
|
|
1777
|
+
* cannot be reconstructed from scan IDs alone.
|
|
1778
|
+
* @deprecated Use pollBatch to preserve `(scan_id, req_id)` correlation and prompt text.
|
|
1474
1779
|
*/
|
|
1475
1780
|
async pollResults(scanIds, intervalMs = DEFAULT_POLL_INTERVAL_MS, retryOpts) {
|
|
1476
1781
|
const maxRetries = retryOpts?.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
@@ -1519,19 +1824,16 @@ var SdkRuntimeService = class {
|
|
|
1519
1824
|
const status = (r.status ?? "").toLowerCase();
|
|
1520
1825
|
if ((status === "complete" || status === "completed") && r.result) {
|
|
1521
1826
|
const result = r.result;
|
|
1827
|
+
const nestedScanId = result.scan_id;
|
|
1828
|
+
if (nestedScanId && nestedScanId !== id) {
|
|
1829
|
+
throw new Error(
|
|
1830
|
+
`AIRS result correlation mismatch: nested scan ID ${nestedScanId} does not match ${id}`
|
|
1831
|
+
);
|
|
1832
|
+
}
|
|
1522
1833
|
completed.set(id, {
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
// not available from async API
|
|
1527
|
-
scanId: result.scan_id ?? id,
|
|
1528
|
-
reportId: result.report_id ?? "",
|
|
1529
|
-
action: result.action === "block" ? "block" : "allow",
|
|
1530
|
-
category: result.category ?? "unknown",
|
|
1531
|
-
triggered: false,
|
|
1532
|
-
// not available from async API — always false
|
|
1533
|
-
detections: {}
|
|
1534
|
-
// not available from async API
|
|
1834
|
+
...scanResponseToResult(result, ""),
|
|
1835
|
+
scanId: id,
|
|
1836
|
+
action: result.action === "block" ? "block" : "allow"
|
|
1535
1837
|
});
|
|
1536
1838
|
pending.delete(id);
|
|
1537
1839
|
} else if (status === "failed") {
|
|
@@ -1543,22 +1845,38 @@ var SdkRuntimeService = class {
|
|
|
1543
1845
|
scanId: id,
|
|
1544
1846
|
reportId: "",
|
|
1545
1847
|
action: "allow",
|
|
1546
|
-
// safe default for failed scans
|
|
1547
1848
|
category: "error",
|
|
1548
1849
|
triggered: false,
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
// not available from async API
|
|
1850
|
+
detections: {},
|
|
1851
|
+
error: "AIRS async scan failed"
|
|
1552
1852
|
});
|
|
1553
1853
|
pending.delete(id);
|
|
1554
1854
|
}
|
|
1555
1855
|
}
|
|
1556
1856
|
}
|
|
1557
1857
|
static formatResultsCsv(results) {
|
|
1558
|
-
const header =
|
|
1858
|
+
const header = [
|
|
1859
|
+
"prompt",
|
|
1860
|
+
"action",
|
|
1861
|
+
"category",
|
|
1862
|
+
"triggered",
|
|
1863
|
+
...RUNTIME_DETECTION_KEYS,
|
|
1864
|
+
"scan_id",
|
|
1865
|
+
"report_id",
|
|
1866
|
+
"error"
|
|
1867
|
+
].join(",");
|
|
1559
1868
|
const rows = results.map((r) => {
|
|
1560
|
-
const
|
|
1561
|
-
|
|
1869
|
+
const fields = [
|
|
1870
|
+
r.prompt,
|
|
1871
|
+
r.action,
|
|
1872
|
+
r.category,
|
|
1873
|
+
String(r.triggered),
|
|
1874
|
+
...RUNTIME_DETECTION_KEYS.map((key) => String(r.detections[key] === true)),
|
|
1875
|
+
r.scanId,
|
|
1876
|
+
r.reportId,
|
|
1877
|
+
r.error ?? ""
|
|
1878
|
+
];
|
|
1879
|
+
return fields.map((field) => `"${field.replace(/"/g, '""')}"`).join(",");
|
|
1562
1880
|
});
|
|
1563
1881
|
return [header, ...rows].join("\n");
|
|
1564
1882
|
}
|
|
@@ -1931,6 +2249,7 @@ export {
|
|
|
1931
2249
|
SdkModelSecurityService,
|
|
1932
2250
|
SdkPromptSetService,
|
|
1933
2251
|
SdkRedTeamService,
|
|
2252
|
+
SDK_ASYNC_BATCH_SIZE,
|
|
1934
2253
|
SdkRuntimeService,
|
|
1935
2254
|
AirsScanService,
|
|
1936
2255
|
RateLimitedScanService,
|