@cdot65/prisma-airs-cli 3.1.0 → 3.3.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.
@@ -1012,6 +1012,52 @@ function normalizeTargetDetail(raw) {
1012
1012
  metadata: sanitizeTargetMetadata(raw.target_metadata)
1013
1013
  };
1014
1014
  }
1015
+ function normalizeAdapterListItem(raw) {
1016
+ return {
1017
+ uuid: raw.uuid,
1018
+ name: raw.name,
1019
+ status: raw.status,
1020
+ createdAt: raw.created_at,
1021
+ updatedAt: raw.updated_at,
1022
+ createdByUserId: raw.created_by_user_id,
1023
+ targetCount: raw.target_count
1024
+ };
1025
+ }
1026
+ function normalizeAdapterVar(raw) {
1027
+ return {
1028
+ key: raw.key,
1029
+ value: raw.value,
1030
+ type: raw.type,
1031
+ isRedacted: raw.is_redacted
1032
+ };
1033
+ }
1034
+ function normalizeAdapterDetail(raw) {
1035
+ return {
1036
+ uuid: raw.uuid,
1037
+ tsgId: raw.tsg_id,
1038
+ name: raw.name,
1039
+ scriptB64: raw.script_b64,
1040
+ status: raw.status,
1041
+ description: raw.description,
1042
+ networkBrokerChannelUuid: raw.network_broker_channel_uuid,
1043
+ variables: (raw.variables ?? []).map(normalizeAdapterVar),
1044
+ targetCount: raw.target_count,
1045
+ createdAt: raw.created_at,
1046
+ updatedAt: raw.updated_at,
1047
+ createdByUserId: raw.created_by_user_id,
1048
+ updatedByUserId: raw.updated_by_user_id
1049
+ };
1050
+ }
1051
+ function preserveVariablesForUpdate(variables) {
1052
+ return variables.map((v) => ({
1053
+ key: v.key,
1054
+ value: v.isRedacted ? null : v.value ?? null,
1055
+ type: v.type
1056
+ }));
1057
+ }
1058
+ function toWireVariables(variables) {
1059
+ return variables.map((v) => ({ key: v.key, value: v.value, type: v.type }));
1060
+ }
1015
1061
  var SdkRedTeamService = class {
1016
1062
  client;
1017
1063
  constructor(opts) {
@@ -1180,7 +1226,12 @@ var SdkRedTeamService = class {
1180
1226
  }
1181
1227
  async createScan(request) {
1182
1228
  let jobMetadata = {};
1183
- if (request.jobType === "STATIC" && request.categories) {
1229
+ if (request.jobType === "STATIC") {
1230
+ if (!request.categories) {
1231
+ throw new Error(
1232
+ "STATIC scans require categories. Pass categories explicitly or use the CLI default."
1233
+ );
1234
+ }
1184
1235
  jobMetadata = { categories: request.categories };
1185
1236
  } else if (request.jobType === "CUSTOM" && request.customPromptSets) {
1186
1237
  jobMetadata = {
@@ -1408,21 +1459,224 @@ var SdkRedTeamService = class {
1408
1459
  totalItems: pagination?.total_items
1409
1460
  };
1410
1461
  }
1462
+ // -------------------------------------------------------------------------
1463
+ // Custom target adapters (SDK 0.16.0)
1464
+ // -------------------------------------------------------------------------
1465
+ async listAdapters(opts) {
1466
+ const sdkOpts = {};
1467
+ if (opts?.limit != null) sdkOpts.limit = opts.limit;
1468
+ if (opts?.offset != null) sdkOpts.skip = opts.offset;
1469
+ if (opts?.search) sdkOpts.search = opts.search;
1470
+ const raw = await this.client.adapters.list(sdkOpts);
1471
+ const pagination = raw.pagination;
1472
+ return {
1473
+ adapters: (raw.data ?? []).map(normalizeAdapterListItem),
1474
+ totalItems: pagination?.total_items
1475
+ };
1476
+ }
1477
+ async getAdapter(uuid) {
1478
+ const raw = await this.client.adapters.get(uuid);
1479
+ return normalizeAdapterDetail(raw);
1480
+ }
1481
+ async createAdapter(request, validate) {
1482
+ const body = {
1483
+ name: request.name,
1484
+ script_b64: request.scriptB64,
1485
+ prompt: request.prompt
1486
+ };
1487
+ if (request.description !== void 0) body.description = request.description;
1488
+ if (request.networkBrokerChannelUuid !== void 0) {
1489
+ body.network_broker_channel_uuid = request.networkBrokerChannelUuid;
1490
+ }
1491
+ if (request.variables !== void 0) body.variables = toWireVariables(request.variables);
1492
+ const raw = await this.client.adapters.create(
1493
+ // biome-ignore lint/suspicious/noExplicitAny: body is assembled dynamically; SDK validates the shape
1494
+ body,
1495
+ validate === void 0 ? void 0 : { validate }
1496
+ );
1497
+ return normalizeAdapterDetail(raw);
1498
+ }
1499
+ async updateAdapter(uuid, overrides, validate) {
1500
+ const current = await this.getAdapter(uuid);
1501
+ const body = {
1502
+ name: overrides.name ?? current.name,
1503
+ script_b64: overrides.scriptB64 ?? current.scriptB64,
1504
+ prompt: overrides.prompt,
1505
+ variables: overrides.variables ? toWireVariables(overrides.variables) : preserveVariablesForUpdate(current.variables)
1506
+ };
1507
+ const description = overrides.description ?? current.description;
1508
+ if (description != null) body.description = description;
1509
+ const channel = overrides.networkBrokerChannelUuid ?? current.networkBrokerChannelUuid;
1510
+ if (channel != null) body.network_broker_channel_uuid = channel;
1511
+ const raw = await this.client.adapters.update(
1512
+ uuid,
1513
+ // biome-ignore lint/suspicious/noExplicitAny: body is assembled dynamically; SDK validates the shape
1514
+ body,
1515
+ validate === void 0 ? void 0 : { validate }
1516
+ );
1517
+ return normalizeAdapterDetail(raw);
1518
+ }
1519
+ async deleteAdapter(uuid) {
1520
+ await this.client.adapters.delete(uuid);
1521
+ }
1522
+ async validateAdapter(request) {
1523
+ let variables = request.variables ? toWireVariables(request.variables) : void 0;
1524
+ if (variables === void 0 && request.adapterUuid) {
1525
+ const adapter = await this.getAdapter(request.adapterUuid);
1526
+ variables = preserveVariablesForUpdate(adapter.variables);
1527
+ }
1528
+ const body = {
1529
+ script_b64: request.scriptB64,
1530
+ network_broker_channel_uuid: request.networkBrokerChannelUuid,
1531
+ prompt: request.prompt
1532
+ };
1533
+ if (variables !== void 0) body.variables = variables;
1534
+ if (request.adapterUuid !== void 0) body.adapter_uuid = request.adapterUuid;
1535
+ const raw = await this.client.adapters.validate(body);
1536
+ return {
1537
+ validated: raw.validated,
1538
+ stdout: raw.stdout,
1539
+ stderr: raw.stderr,
1540
+ traceback: raw.traceback
1541
+ };
1542
+ }
1411
1543
  };
1412
1544
 
1413
1545
  // src/airs/runtime.ts
1414
- import { Content, init, Scanner } from "@cdot65/prisma-airs-sdk";
1415
- var BATCH_SIZE = 5;
1546
+ import {
1547
+ Content,
1548
+ init,
1549
+ MAX_NUMBER_OF_BATCH_SCAN_OBJECTS,
1550
+ Scanner
1551
+ } from "@cdot65/prisma-airs-sdk";
1552
+ var SDK_ASYNC_BATCH_SIZE = MAX_NUMBER_OF_BATCH_SCAN_OBJECTS;
1416
1553
  var DEFAULT_POLL_INTERVAL_MS = 5e3;
1417
1554
  var DEFAULT_MAX_RETRIES = 5;
1418
1555
  var DEFAULT_BASE_DELAY_MS = 1e4;
1556
+ var DEFAULT_MAX_NO_PROGRESS_POLLS = 120;
1557
+ var NO_SDK_RETRIES = { numRetries: 0 };
1558
+ var RUNTIME_DETECTION_KEYS = [
1559
+ "topic_violation",
1560
+ "injection",
1561
+ "toxic_content",
1562
+ "dlp",
1563
+ "url_cats",
1564
+ "malicious_code",
1565
+ "source_code",
1566
+ "agent"
1567
+ ];
1568
+ var REPORT_DETECTION_KEYS = {
1569
+ topic_guardrails: "topic_violation",
1570
+ topic_violation: "topic_violation",
1571
+ pi: "injection",
1572
+ prompt_injection: "injection",
1573
+ injection: "injection",
1574
+ tc: "toxic_content",
1575
+ toxic_content: "toxic_content",
1576
+ dlp: "dlp",
1577
+ uf: "url_cats",
1578
+ url_filtering: "url_cats",
1579
+ url_cats: "url_cats",
1580
+ mc: "malicious_code",
1581
+ malicious_code: "malicious_code",
1582
+ source_code: "source_code",
1583
+ agent: "agent"
1584
+ };
1419
1585
  function isRateLimitError(err) {
1586
+ if (err?.statusCode === 429) return true;
1420
1587
  if (err instanceof Error) {
1421
1588
  const msg = err.message.toLowerCase();
1422
1589
  return msg.includes("rate limit") || msg.includes("rate_limit") || msg.includes("429");
1423
1590
  }
1424
1591
  return false;
1425
1592
  }
1593
+ function isDefiniteRateLimitError(err) {
1594
+ const metadata = err;
1595
+ return metadata?.failureKind === "http" && metadata.statusCode === 429;
1596
+ }
1597
+ function runtimeDetections(value) {
1598
+ const source = value ?? {};
1599
+ return Object.fromEntries(
1600
+ RUNTIME_DETECTION_KEYS.filter((key) => typeof source[key] === "boolean").map((key) => [
1601
+ key,
1602
+ source[key]
1603
+ ])
1604
+ );
1605
+ }
1606
+ function runtimeAction(value) {
1607
+ const normalized = typeof value === "string" ? value.toLowerCase() : "";
1608
+ if (normalized === "allow" || normalized === "block") return normalized;
1609
+ return "failed";
1610
+ }
1611
+ function scanResponseToResult(response, prompt) {
1612
+ const detections = runtimeDetections(response.prompt_detected);
1613
+ const action = runtimeAction(response.action);
1614
+ const failed = response.error === true || response.timeout === true || action === "failed";
1615
+ const errors = Array.isArray(response.errors) ? response.errors.map((entry) => {
1616
+ const detail = entry;
1617
+ return [detail.feature, detail.status, detail.content_type].filter(Boolean).join(": ");
1618
+ }) : [];
1619
+ return {
1620
+ prompt,
1621
+ response: void 0,
1622
+ scanId: response.scan_id ?? "",
1623
+ reportId: response.report_id ?? "",
1624
+ action: failed ? "failed" : action,
1625
+ category: failed ? "error" : response.category ?? "unknown",
1626
+ triggered: RUNTIME_DETECTION_KEYS.some((key) => detections[key] === true),
1627
+ detections,
1628
+ ...failed ? {
1629
+ 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")}`)
1630
+ } : {}
1631
+ };
1632
+ }
1633
+ function threatReportToResult(report, entry) {
1634
+ const detections = {};
1635
+ let sawBlock = false;
1636
+ let unexpectedAction;
1637
+ const detectionResults = Array.isArray(report.detection_results) ? report.detection_results : [];
1638
+ for (const detection of detectionResults) {
1639
+ const service = String(detection.detection_service ?? "").toLowerCase();
1640
+ const detectorAction = String(detection.action ?? "").toLowerCase();
1641
+ const verdict = String(detection.verdict ?? "").toLowerCase();
1642
+ const fired = detectorAction === "block" || ["malicious", "unsafe", "violation", "detected"].includes(verdict);
1643
+ const namedKey = REPORT_DETECTION_KEYS[service] ?? service;
1644
+ const key = namedKey || (fired ? "unknown" : "");
1645
+ if (key) detections[key] = detections[key] === true || fired;
1646
+ if (detectorAction === "block") sawBlock = true;
1647
+ else if (detectorAction && detectorAction !== "allow") unexpectedAction = detectorAction;
1648
+ }
1649
+ const triggered = Object.values(detections).some(Boolean);
1650
+ const action = sawBlock ? "block" : unexpectedAction ? "failed" : "allow";
1651
+ return {
1652
+ index: entry.index,
1653
+ reqId: entry.reqId,
1654
+ prompt: entry.prompt,
1655
+ response: void 0,
1656
+ scanId: entry.scanId,
1657
+ reportId: report.report_id ?? "",
1658
+ action,
1659
+ category: action === "failed" ? "error" : triggered ? "malicious" : "benign",
1660
+ triggered,
1661
+ detections,
1662
+ ...action === "failed" ? { error: `Unknown AIRS action in threat report: ${unexpectedAction}` } : {}
1663
+ };
1664
+ }
1665
+ function failedBulkResult(entry, error = "AIRS async scan failed") {
1666
+ return {
1667
+ index: entry.index,
1668
+ reqId: entry.reqId,
1669
+ prompt: entry.prompt,
1670
+ response: void 0,
1671
+ scanId: entry.scanId,
1672
+ reportId: "",
1673
+ action: "failed",
1674
+ category: "error",
1675
+ triggered: false,
1676
+ detections: {},
1677
+ error
1678
+ };
1679
+ }
1426
1680
  var SdkRuntimeService = class {
1427
1681
  scanner;
1428
1682
  constructor(opts) {
@@ -1434,23 +1688,208 @@ var SdkRuntimeService = class {
1434
1688
  if (response) contentOpts.response = response;
1435
1689
  const content = new Content(contentOpts);
1436
1690
  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);
1691
+ const normalized = scanResponseToResult(res, prompt);
1439
1692
  return {
1440
- prompt,
1693
+ ...normalized,
1441
1694
  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
1695
+ action: normalized.action === "block" ? "block" : "allow"
1696
+ };
1697
+ }
1698
+ async submitBatch(profileName, prompts, sessionId, retryOpts) {
1699
+ if (prompts.length < 1 || prompts.length > SDK_ASYNC_BATCH_SIZE) {
1700
+ throw new Error(`submitBatch requires between 1 and ${SDK_ASYNC_BATCH_SIZE} prompts`);
1701
+ }
1702
+ const promptIndices = /* @__PURE__ */ new Set();
1703
+ for (const prompt of prompts) {
1704
+ if (!Number.isSafeInteger(prompt.index) || prompt.index < 0 || promptIndices.has(prompt.index)) {
1705
+ throw new Error("submitBatch requires a unique nonnegative safe index for every prompt");
1706
+ }
1707
+ promptIndices.add(prompt.index);
1708
+ }
1709
+ const scanObjects = prompts.map(({ index, prompt }) => ({
1710
+ req_id: index,
1711
+ scan_req: {
1712
+ ai_profile: { profile_name: profileName },
1713
+ contents: [{ prompt }],
1714
+ ...sessionId ? { session_id: sessionId } : {}
1715
+ }
1716
+ }));
1717
+ const maxRetries = retryOpts?.maxRetries ?? DEFAULT_MAX_RETRIES;
1718
+ const baseDelay = retryOpts?.baseDelayMs ?? DEFAULT_BASE_DELAY_MS;
1719
+ let retryAttempt = 0;
1720
+ let receipt;
1721
+ while (true) {
1722
+ try {
1723
+ receipt = await this.scanner.asyncScan(scanObjects, NO_SDK_RETRIES);
1724
+ break;
1725
+ } catch (error) {
1726
+ if (!isDefiniteRateLimitError(error) || retryAttempt >= maxRetries) throw error;
1727
+ retryAttempt++;
1728
+ const retryAfterMs = error.retryAfterMs;
1729
+ const delayMs = typeof retryAfterMs === "number" && Number.isFinite(retryAfterMs) && retryAfterMs >= 0 ? retryAfterMs : baseDelay * 2 ** (retryAttempt - 1);
1730
+ retryOpts?.onRetry?.(retryAttempt, delayMs);
1731
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
1732
+ }
1733
+ }
1734
+ return {
1735
+ scanId: receipt.scan_id,
1736
+ reportId: receipt.report_id,
1737
+ entries: prompts.map(({ index, prompt }) => ({
1738
+ scanId: receipt.scan_id,
1739
+ reqId: index,
1740
+ index,
1741
+ prompt
1742
+ }))
1448
1743
  };
1449
1744
  }
1745
+ async pollBatch(batch, intervalMs = DEFAULT_POLL_INTERVAL_MS, retryOpts) {
1746
+ if (batch.entries.length < 1 || batch.entries.length > SDK_ASYNC_BATCH_SIZE) {
1747
+ throw new Error(`pollBatch requires between 1 and ${SDK_ASYNC_BATCH_SIZE} receipt entries`);
1748
+ }
1749
+ const receiptIds = /* @__PURE__ */ new Set();
1750
+ for (const entry of batch.entries) {
1751
+ if (entry.scanId !== batch.scanId) {
1752
+ throw new Error(`Receipt entry scan ID ${entry.scanId} does not match ${batch.scanId}`);
1753
+ }
1754
+ if (!Number.isSafeInteger(entry.reqId) || entry.reqId < 0 || entry.reqId !== entry.index || receiptIds.has(entry.reqId)) {
1755
+ throw new Error("pollBatch requires a unique request ID matching each prompt index");
1756
+ }
1757
+ receiptIds.add(entry.reqId);
1758
+ }
1759
+ const maxRetries = retryOpts?.maxRetries ?? DEFAULT_MAX_RETRIES;
1760
+ const baseDelay = retryOpts?.baseDelayMs ?? DEFAULT_BASE_DELAY_MS;
1761
+ const maxNoProgressPolls = retryOpts?.maxNoProgressPolls ?? DEFAULT_MAX_NO_PROGRESS_POLLS;
1762
+ const entries = new Map(batch.entries.map((entry) => [entry.reqId, entry]));
1763
+ const resolved = /* @__PURE__ */ new Map();
1764
+ let retryLevel = 0;
1765
+ let noProgressPolls = 0;
1766
+ while (resolved.size < batch.entries.length) {
1767
+ let rows;
1768
+ try {
1769
+ rows = await this.scanner.queryByScanIds([batch.scanId], NO_SDK_RETRIES);
1770
+ } catch (err) {
1771
+ if (isRateLimitError(err) && retryLevel < maxRetries) {
1772
+ retryLevel++;
1773
+ const retryAfterMs = err.retryAfterMs;
1774
+ const delayMs = typeof retryAfterMs === "number" && Number.isFinite(retryAfterMs) && retryAfterMs >= 0 ? retryAfterMs : baseDelay * 2 ** (retryLevel - 1);
1775
+ retryOpts?.onRetry?.(retryLevel, delayMs);
1776
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
1777
+ continue;
1778
+ }
1779
+ throw err;
1780
+ }
1781
+ const resolvedBeforePoll = resolved.size;
1782
+ const fallbackReports = /* @__PURE__ */ new Map();
1783
+ for (const row of rows) {
1784
+ const scanId = row.scan_id ?? batch.scanId;
1785
+ const reqId = row.req_id;
1786
+ const status = (row.status ?? "").toLowerCase();
1787
+ const terminal = status === "failed" || status === "complete" || status === "completed";
1788
+ if (reqId === void 0 && terminal && scanId === batch.scanId) {
1789
+ if (status === "failed") {
1790
+ const newlyFailed = [];
1791
+ for (const entry2 of batch.entries) {
1792
+ if (!resolved.has(entry2.reqId)) {
1793
+ const result = failedBulkResult(entry2);
1794
+ resolved.set(entry2.reqId, result);
1795
+ newlyFailed.push(result);
1796
+ }
1797
+ }
1798
+ if (newlyFailed.length > 0) await retryOpts?.onProgress?.(newlyFailed);
1799
+ continue;
1800
+ }
1801
+ const reportId = row.result?.report_id ?? row.report_id ?? batch.reportId;
1802
+ if (!reportId) {
1803
+ throw new Error(
1804
+ `AIRS result correlation failed for scan ${scanId}: terminal row has no request or report ID`
1805
+ );
1806
+ }
1807
+ fallbackReports.set(scanId, reportId);
1808
+ continue;
1809
+ }
1810
+ const entry = reqId === void 0 || scanId !== batch.scanId ? void 0 : entries.get(reqId);
1811
+ if (!entry || resolved.has(entry.reqId)) continue;
1812
+ if (status === "failed") {
1813
+ const result = failedBulkResult(entry);
1814
+ resolved.set(entry.reqId, result);
1815
+ await retryOpts?.onProgress?.([result]);
1816
+ continue;
1817
+ }
1818
+ if ((status === "complete" || status === "completed") && row.result) {
1819
+ const nestedResult = row.result;
1820
+ const nestedScanId = nestedResult.scan_id;
1821
+ if (nestedScanId && nestedScanId !== scanId) {
1822
+ throw new Error(
1823
+ `AIRS result correlation mismatch: nested scan ID ${nestedScanId} does not match ${scanId}`
1824
+ );
1825
+ }
1826
+ const result = {
1827
+ ...scanResponseToResult(nestedResult, entry.prompt),
1828
+ scanId,
1829
+ index: entry.index,
1830
+ reqId: entry.reqId
1831
+ };
1832
+ resolved.set(entry.reqId, result);
1833
+ await retryOpts?.onProgress?.([result]);
1834
+ }
1835
+ }
1836
+ if (fallbackReports.size > 0) {
1837
+ let reports;
1838
+ try {
1839
+ reports = await this.scanner.queryByReportIds(
1840
+ [...new Set(fallbackReports.values())],
1841
+ NO_SDK_RETRIES
1842
+ );
1843
+ } catch (err) {
1844
+ if (isRateLimitError(err) && retryLevel < maxRetries) {
1845
+ retryLevel++;
1846
+ const retryAfterMs = err.retryAfterMs;
1847
+ const delayMs = typeof retryAfterMs === "number" && Number.isFinite(retryAfterMs) && retryAfterMs >= 0 ? retryAfterMs : baseDelay * 2 ** (retryLevel - 1);
1848
+ retryOpts?.onRetry?.(retryLevel, delayMs);
1849
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
1850
+ continue;
1851
+ }
1852
+ throw err;
1853
+ }
1854
+ for (const report of reports) {
1855
+ const reqId = report.req_id;
1856
+ const reportScanId = report.scan_id ?? batch.scanId;
1857
+ const expectedReportId = fallbackReports.get(reportScanId);
1858
+ const reportId = report.report_id;
1859
+ if (reportScanId !== batch.scanId || reportId && reportId !== expectedReportId) {
1860
+ throw new Error(
1861
+ `AIRS report correlation mismatch for scan ${batch.scanId}, request ${String(reqId)}`
1862
+ );
1863
+ }
1864
+ const entry = reqId === void 0 ? void 0 : entries.get(reqId);
1865
+ if (!entry || resolved.has(entry.reqId)) continue;
1866
+ const result = threatReportToResult(report, entry);
1867
+ resolved.set(entry.reqId, result);
1868
+ await retryOpts?.onProgress?.([result]);
1869
+ }
1870
+ }
1871
+ if (retryLevel > 0) retryLevel--;
1872
+ if (resolved.size === resolvedBeforePoll) {
1873
+ noProgressPolls++;
1874
+ } else {
1875
+ noProgressPolls = 0;
1876
+ }
1877
+ if (noProgressPolls >= maxNoProgressPolls) {
1878
+ throw new Error(
1879
+ `AIRS polling made no progress after ${noProgressPolls} polls for scan ${batch.scanId}`
1880
+ );
1881
+ }
1882
+ if (resolved.size < batch.entries.length) {
1883
+ await new Promise((resolve) => setTimeout(resolve, intervalMs));
1884
+ }
1885
+ }
1886
+ return [...resolved.values()].sort((left, right) => left.index - right.index);
1887
+ }
1888
+ /** @deprecated Use submitBatch to preserve per-prompt request correlation. */
1450
1889
  async submitBulkScan(profileName, prompts, sessionId) {
1451
1890
  const scanIds = [];
1452
- for (let i = 0; i < prompts.length; i += BATCH_SIZE) {
1453
- const batch = prompts.slice(i, i + BATCH_SIZE);
1891
+ for (let i = 0; i < prompts.length; i += SDK_ASYNC_BATCH_SIZE) {
1892
+ const batch = prompts.slice(i, i + SDK_ASYNC_BATCH_SIZE);
1454
1893
  const scanObjects = batch.map((prompt, idx) => ({
1455
1894
  req_id: i + idx,
1456
1895
  scan_req: {
@@ -1459,18 +1898,16 @@ var SdkRuntimeService = class {
1459
1898
  ...sessionId ? { session_id: sessionId } : {}
1460
1899
  }
1461
1900
  }));
1462
- const res = await this.scanner.asyncScan(scanObjects);
1901
+ const res = await this.scanner.asyncScan(scanObjects, NO_SDK_RETRIES);
1463
1902
  scanIds.push(res.scan_id);
1464
1903
  }
1465
1904
  return scanIds;
1466
1905
  }
1467
1906
  /**
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.
1907
+ * Compatibility poller for callers that retained only batch scan IDs.
1908
+ * Nested detection data is preserved, but prompt text and per-request fan-out
1909
+ * cannot be reconstructed from scan IDs alone.
1910
+ * @deprecated Use pollBatch to preserve `(scan_id, req_id)` correlation and prompt text.
1474
1911
  */
1475
1912
  async pollResults(scanIds, intervalMs = DEFAULT_POLL_INTERVAL_MS, retryOpts) {
1476
1913
  const maxRetries = retryOpts?.maxRetries ?? DEFAULT_MAX_RETRIES;
@@ -1519,19 +1956,16 @@ var SdkRuntimeService = class {
1519
1956
  const status = (r.status ?? "").toLowerCase();
1520
1957
  if ((status === "complete" || status === "completed") && r.result) {
1521
1958
  const result = r.result;
1959
+ const nestedScanId = result.scan_id;
1960
+ if (nestedScanId && nestedScanId !== id) {
1961
+ throw new Error(
1962
+ `AIRS result correlation mismatch: nested scan ID ${nestedScanId} does not match ${id}`
1963
+ );
1964
+ }
1522
1965
  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
1966
+ ...scanResponseToResult(result, ""),
1967
+ scanId: id,
1968
+ action: result.action === "block" ? "block" : "allow"
1535
1969
  });
1536
1970
  pending.delete(id);
1537
1971
  } else if (status === "failed") {
@@ -1543,22 +1977,38 @@ var SdkRuntimeService = class {
1543
1977
  scanId: id,
1544
1978
  reportId: "",
1545
1979
  action: "allow",
1546
- // safe default for failed scans
1547
1980
  category: "error",
1548
1981
  triggered: false,
1549
- // not available from async API — always false
1550
- detections: {}
1551
- // not available from async API
1982
+ detections: {},
1983
+ error: "AIRS async scan failed"
1552
1984
  });
1553
1985
  pending.delete(id);
1554
1986
  }
1555
1987
  }
1556
1988
  }
1557
1989
  static formatResultsCsv(results) {
1558
- const header = "prompt,action,category,triggered,scan_id,report_id";
1990
+ const header = [
1991
+ "prompt",
1992
+ "action",
1993
+ "category",
1994
+ "triggered",
1995
+ ...RUNTIME_DETECTION_KEYS,
1996
+ "scan_id",
1997
+ "report_id",
1998
+ "error"
1999
+ ].join(",");
1559
2000
  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}"`;
2001
+ const fields = [
2002
+ r.prompt,
2003
+ r.action,
2004
+ r.category,
2005
+ String(r.triggered),
2006
+ ...RUNTIME_DETECTION_KEYS.map((key) => String(r.detections[key] === true)),
2007
+ r.scanId,
2008
+ r.reportId,
2009
+ r.error ?? ""
2010
+ ];
2011
+ return fields.map((field) => `"${field.replace(/"/g, '""')}"`).join(",");
1562
2012
  });
1563
2013
  return [header, ...rows].join("\n");
1564
2014
  }
@@ -1712,6 +2162,10 @@ var ConfigSchema = z.object({
1712
2162
  modelSecDataEndpoint: z.string().optional(),
1713
2163
  modelSecMgmtEndpoint: z.string().optional(),
1714
2164
  modelSecTokenEndpoint: z.string().optional(),
2165
+ // AI Gateway (endpoints only; creds shared with mgmt*)
2166
+ aiGwDataEndpoint: z.string().optional(),
2167
+ aiGwAdminEndpoint: z.string().optional(),
2168
+ aiGwTokenEndpoint: z.string().optional(),
1715
2169
  // Tuning
1716
2170
  scanConcurrency: z.coerce.number().int().min(1).max(20).default(5),
1717
2171
  // Persistence
@@ -1742,6 +2196,9 @@ function fromEnv() {
1742
2196
  modelSecDataEndpoint: env.PANW_MODEL_SEC_DATA_ENDPOINT,
1743
2197
  modelSecMgmtEndpoint: env.PANW_MODEL_SEC_MGMT_ENDPOINT,
1744
2198
  modelSecTokenEndpoint: env.PANW_MODEL_SEC_TOKEN_ENDPOINT,
2199
+ aiGwDataEndpoint: env.PANW_AI_GW_DATA_ENDPOINT,
2200
+ aiGwAdminEndpoint: env.PANW_AI_GW_ADMIN_ENDPOINT,
2201
+ aiGwTokenEndpoint: env.PANW_AI_GW_TOKEN_ENDPOINT,
1745
2202
  scanConcurrency: env.SCAN_CONCURRENCY,
1746
2203
  dataDir: env.DATA_DIR
1747
2204
  };
@@ -1931,6 +2388,7 @@ export {
1931
2388
  SdkModelSecurityService,
1932
2389
  SdkPromptSetService,
1933
2390
  SdkRedTeamService,
2391
+ SDK_ASYNC_BATCH_SIZE,
1934
2392
  SdkRuntimeService,
1935
2393
  AirsScanService,
1936
2394
  RateLimitedScanService,