@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 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 { Content, init, Scanner } from "@cdot65/prisma-airs-sdk";
1415
- var BATCH_SIZE = 5;
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 detected = res.prompt_detected ?? {};
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
- prompt,
1561
+ ...normalized,
1441
1562
  response,
1442
- scanId: res.scan_id ?? "",
1443
- reportId: res.report_id ?? "",
1444
- action: res.action === "block" ? "block" : "allow",
1445
- category: res.category ?? "unknown",
1446
- triggered,
1447
- detections: detected
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 += BATCH_SIZE) {
1453
- const batch = prompts.slice(i, i + BATCH_SIZE);
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
- * Poll async scan results until all complete or fail.
1469
- *
1470
- * Note: The async query API (`queryByScanIds`) does not return `prompt`,
1471
- * `response`, `triggered`, or `detections` fields. These are set to
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
- prompt: "",
1524
- // not available from async API
1525
- response: void 0,
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
- // not available from async API — always false
1550
- detections: {}
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 = "prompt,action,category,triggered,scan_id,report_id";
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 escaped = r.prompt.replace(/"/g, '""');
1561
- return `"${escaped}","${r.action}","${r.category}","${r.triggered}","${r.scanId}","${r.reportId}"`;
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,