@cdot65/prisma-airs-cli 3.0.1 → 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.
@@ -456,6 +456,51 @@ function normalizeFile(raw) {
456
456
  result: raw.result
457
457
  };
458
458
  }
459
+ function normalizeModel(raw) {
460
+ return {
461
+ uuid: raw.uuid,
462
+ tsgId: raw.tsg_id,
463
+ name: raw.name,
464
+ createdAt: raw.created_at,
465
+ updatedAt: raw.updated_at,
466
+ latestVersionUuid: raw.latest_version_uuid,
467
+ latestVersionFingerprint: raw.latest_version_fingerprint,
468
+ latestVersionRevision: raw.latest_version_revision,
469
+ latestVersionHfCommitSha: raw.latest_version_hf_commit_sha,
470
+ latestVersionOutcome: raw.latest_version_outcome,
471
+ latestVersionFormats: raw.latest_version_formats,
472
+ latestVersionSourceTypes: raw.latest_version_source_types,
473
+ latestVersionScanTime: raw.latest_version_scan_time
474
+ };
475
+ }
476
+ function normalizeModelVersion(raw) {
477
+ const summary = raw.last_eval_summary;
478
+ return {
479
+ uuid: raw.uuid,
480
+ tsgId: raw.tsg_id,
481
+ modelUuid: raw.model_uuid,
482
+ revision: raw.revision,
483
+ createdAt: raw.created_at,
484
+ updatedAt: raw.updated_at,
485
+ fingerprint: raw.fingerprint,
486
+ fileCount: raw.file_count,
487
+ license: raw.license,
488
+ latestScanTime: raw.latest_scan_time,
489
+ hfCommitSha: raw.hf_commit_sha,
490
+ hfCommitTitle: raw.hf_commit_title,
491
+ hfCommitAuthors: raw.hf_commit_authors,
492
+ hfModelName: raw.hf_model_name,
493
+ hfOrganization: raw.hf_organization,
494
+ modelFormats: raw.model_formats,
495
+ sourceTypes: raw.source_types,
496
+ lastEvalOutcome: raw.last_eval_outcome,
497
+ lastEvalSummary: summary ? {
498
+ rulesFailed: summary.rules_failed ?? 0,
499
+ rulesPassed: summary.rules_passed ?? 0,
500
+ totalRules: summary.total_rules ?? 0
501
+ } : summary
502
+ };
503
+ }
459
504
  var SdkModelSecurityService = class {
460
505
  client;
461
506
  constructor(opts) {
@@ -680,6 +725,58 @@ var SdkModelSecurityService = class {
680
725
  expiresAt: raw.expires_at
681
726
  };
682
727
  }
728
+ // -----------------------------------------------------------------------
729
+ // Models (read-only catalog)
730
+ // -----------------------------------------------------------------------
731
+ async listModels(opts) {
732
+ const sdkOpts = {};
733
+ if (opts?.search) sdkOpts.search = opts.search;
734
+ if (opts?.searchQuery) sdkOpts.search_query = opts.searchQuery;
735
+ if (opts?.sortField) sdkOpts.sort_field = opts.sortField;
736
+ if (opts?.sortOrder) sdkOpts.sort_order = opts.sortOrder;
737
+ if (opts?.skip !== void 0) sdkOpts.skip = opts.skip;
738
+ if (opts?.limit !== void 0) sdkOpts.limit = opts.limit;
739
+ const response = await this.client.models.listModels(sdkOpts);
740
+ const raw = response;
741
+ return {
742
+ totalItems: raw.pagination.total_items ?? 0,
743
+ models: raw.models.map(normalizeModel)
744
+ };
745
+ }
746
+ async getModel(uuid) {
747
+ const response = await this.client.models.getModel(uuid);
748
+ return normalizeModel(response);
749
+ }
750
+ async listModelVersions(modelUuid, opts) {
751
+ const sdkOpts = {};
752
+ if (opts?.sortOrder) sdkOpts.sort_order = opts.sortOrder;
753
+ if (opts?.skip !== void 0) sdkOpts.skip = opts.skip;
754
+ if (opts?.limit !== void 0) sdkOpts.limit = opts.limit;
755
+ const response = await this.client.models.listModelVersions(modelUuid, sdkOpts);
756
+ const raw = response;
757
+ return {
758
+ totalItems: raw.pagination.total_items ?? 0,
759
+ versions: raw.model_versions.map(normalizeModelVersion)
760
+ };
761
+ }
762
+ async getModelVersion(uuid) {
763
+ const response = await this.client.models.getModelVersion(uuid);
764
+ return normalizeModelVersion(response);
765
+ }
766
+ async listModelVersionFiles(modelVersionUuid, opts) {
767
+ const sdkOpts = {};
768
+ if (opts?.skip !== void 0) sdkOpts.skip = opts.skip;
769
+ if (opts?.limit !== void 0) sdkOpts.limit = opts.limit;
770
+ const response = await this.client.models.listModelVersionFiles(
771
+ modelVersionUuid,
772
+ sdkOpts
773
+ );
774
+ const raw = response;
775
+ return {
776
+ totalItems: raw.pagination.total_items ?? 0,
777
+ files: raw.files.map(normalizeFile)
778
+ };
779
+ }
683
780
  };
684
781
 
685
782
  // src/airs/promptsets.ts
@@ -862,6 +959,37 @@ function sanitizeTargetMetadata(metadata) {
862
959
  }
863
960
  return metadata;
864
961
  }
962
+ function normalizeChannel(raw) {
963
+ return {
964
+ uuid: raw.uuid,
965
+ name: raw.name,
966
+ description: raw.description,
967
+ status: raw.status,
968
+ addedBy: raw.added_by,
969
+ createdAt: raw.created_at,
970
+ updatedAt: raw.updated_at,
971
+ lastOnlineAt: raw.last_online_at,
972
+ connectedClientsCount: raw.connected_clients_count,
973
+ outdatedClientsCount: raw.outdated_clients_count,
974
+ features: raw.features
975
+ };
976
+ }
977
+ function normalizeErrorLog(raw) {
978
+ return {
979
+ createdAt: raw.created_at,
980
+ updatedAt: raw.updated_at,
981
+ jobId: raw.job_id,
982
+ targetId: raw.target_id,
983
+ targetVersion: raw.target_version,
984
+ attackId: raw.attack_id,
985
+ errorType: raw.error_type,
986
+ errorSource: raw.error_source,
987
+ errorMessage: raw.error_message,
988
+ targetObject: raw.target_object,
989
+ extraInfo: raw.extra_info,
990
+ version: raw.version
991
+ };
992
+ }
865
993
  function normalizeTargetDetail(raw) {
866
994
  return {
867
995
  uuid: raw.uuid,
@@ -1210,21 +1338,213 @@ var SdkRedTeamService = class {
1210
1338
  await delay(intervalMs);
1211
1339
  }
1212
1340
  }
1341
+ async listChannels(opts) {
1342
+ const sdkOpts = {};
1343
+ if (opts?.limit != null) sdkOpts.limit = opts.limit;
1344
+ if (opts?.offset != null) sdkOpts.skip = opts.offset;
1345
+ if (opts?.search) sdkOpts.search = opts.search;
1346
+ if (opts?.status) sdkOpts.status = opts.status;
1347
+ const raw = await this.client.networkBroker.listChannels(sdkOpts);
1348
+ const pagination = raw.pagination;
1349
+ return {
1350
+ channels: (raw.data ?? []).map(normalizeChannel),
1351
+ totalItems: pagination?.total_items
1352
+ };
1353
+ }
1354
+ async getChannel(channelId) {
1355
+ const raw = await this.client.networkBroker.getChannel(channelId);
1356
+ return normalizeChannel(raw);
1357
+ }
1358
+ async createChannel(request) {
1359
+ const body = { name: request.name };
1360
+ if (request.description !== void 0) body.description = request.description;
1361
+ const raw = await this.client.networkBroker.createChannel(
1362
+ body
1363
+ );
1364
+ return normalizeChannel(raw);
1365
+ }
1366
+ async updateChannel(channelId, request) {
1367
+ const body = {};
1368
+ if (request.name !== void 0) body.name = request.name;
1369
+ if (request.description !== void 0) body.description = request.description;
1370
+ const raw = await this.client.networkBroker.updateChannel(
1371
+ channelId,
1372
+ body
1373
+ );
1374
+ return normalizeChannel(raw);
1375
+ }
1376
+ async getChannelStats() {
1377
+ const raw = await this.client.networkBroker.getChannelStats();
1378
+ return {
1379
+ serverDomain: raw.network_channels_server_domain,
1380
+ dockerRegistry: raw.docker_registry,
1381
+ helmChart: raw.helm_chart,
1382
+ dockerImage: raw.docker_image,
1383
+ onlineChannels: raw.online_channels,
1384
+ totalChannels: raw.total_channels,
1385
+ clientVersion: raw.client_version
1386
+ };
1387
+ }
1388
+ async getLanguages(management = false) {
1389
+ const raw = await (management ? this.client.getManagementLanguages() : this.client.getLanguages());
1390
+ return {
1391
+ multilingualEnabled: Boolean(raw.multilingual_enabled),
1392
+ supportedJobTypes: raw.supported_job_types ?? [],
1393
+ languages: (raw.languages ?? []).map((l) => ({
1394
+ code: l.code,
1395
+ name: l.name
1396
+ }))
1397
+ };
1398
+ }
1399
+ async getTargetProfileErrorLogs(targetId, opts) {
1400
+ const sdkOpts = {};
1401
+ if (opts?.limit != null) sdkOpts.limit = opts.limit;
1402
+ if (opts?.offset != null) sdkOpts.skip = opts.offset;
1403
+ if (opts?.search) sdkOpts.search = opts.search;
1404
+ const raw = await this.client.getTargetProfileErrorLogs(targetId, sdkOpts);
1405
+ const pagination = raw.pagination;
1406
+ return {
1407
+ logs: (raw.data ?? []).map(normalizeErrorLog),
1408
+ totalItems: pagination?.total_items
1409
+ };
1410
+ }
1213
1411
  };
1214
1412
 
1215
1413
  // src/airs/runtime.ts
1216
- import { Content, init, Scanner } from "@cdot65/prisma-airs-sdk";
1217
- 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;
1218
1421
  var DEFAULT_POLL_INTERVAL_MS = 5e3;
1219
1422
  var DEFAULT_MAX_RETRIES = 5;
1220
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
+ };
1221
1453
  function isRateLimitError(err) {
1454
+ if (err?.statusCode === 429) return true;
1222
1455
  if (err instanceof Error) {
1223
1456
  const msg = err.message.toLowerCase();
1224
1457
  return msg.includes("rate limit") || msg.includes("rate_limit") || msg.includes("429");
1225
1458
  }
1226
1459
  return false;
1227
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
+ }
1228
1548
  var SdkRuntimeService = class {
1229
1549
  scanner;
1230
1550
  constructor(opts) {
@@ -1236,23 +1556,208 @@ var SdkRuntimeService = class {
1236
1556
  if (response) contentOpts.response = response;
1237
1557
  const content = new Content(contentOpts);
1238
1558
  const res = await this.scanner.syncScan({ profile_name: profileName }, content, void 0);
1239
- const detected = res.prompt_detected ?? {};
1240
- const triggered = !!(detected.topic_violation || detected.injection || detected.toxic_content || detected.dlp || detected.url_cats || detected.malicious_code);
1559
+ const normalized = scanResponseToResult(res, prompt);
1241
1560
  return {
1242
- prompt,
1561
+ ...normalized,
1243
1562
  response,
1244
- scanId: res.scan_id ?? "",
1245
- reportId: res.report_id ?? "",
1246
- action: res.action === "block" ? "block" : "allow",
1247
- category: res.category ?? "unknown",
1248
- triggered,
1249
- 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
+ }))
1250
1611
  };
1251
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. */
1252
1757
  async submitBulkScan(profileName, prompts, sessionId) {
1253
1758
  const scanIds = [];
1254
- for (let i = 0; i < prompts.length; i += BATCH_SIZE) {
1255
- 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);
1256
1761
  const scanObjects = batch.map((prompt, idx) => ({
1257
1762
  req_id: i + idx,
1258
1763
  scan_req: {
@@ -1261,18 +1766,16 @@ var SdkRuntimeService = class {
1261
1766
  ...sessionId ? { session_id: sessionId } : {}
1262
1767
  }
1263
1768
  }));
1264
- const res = await this.scanner.asyncScan(scanObjects);
1769
+ const res = await this.scanner.asyncScan(scanObjects, NO_SDK_RETRIES);
1265
1770
  scanIds.push(res.scan_id);
1266
1771
  }
1267
1772
  return scanIds;
1268
1773
  }
1269
1774
  /**
1270
- * Poll async scan results until all complete or fail.
1271
- *
1272
- * Note: The async query API (`queryByScanIds`) does not return `prompt`,
1273
- * `response`, `triggered`, or `detections` fields. These are set to
1274
- * defaults (`''`, `undefined`, `false`, `{}`) in the returned results.
1275
- * 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.
1276
1779
  */
1277
1780
  async pollResults(scanIds, intervalMs = DEFAULT_POLL_INTERVAL_MS, retryOpts) {
1278
1781
  const maxRetries = retryOpts?.maxRetries ?? DEFAULT_MAX_RETRIES;
@@ -1321,19 +1824,16 @@ var SdkRuntimeService = class {
1321
1824
  const status = (r.status ?? "").toLowerCase();
1322
1825
  if ((status === "complete" || status === "completed") && r.result) {
1323
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
+ }
1324
1833
  completed.set(id, {
1325
- prompt: "",
1326
- // not available from async API
1327
- response: void 0,
1328
- // not available from async API
1329
- scanId: result.scan_id ?? id,
1330
- reportId: result.report_id ?? "",
1331
- action: result.action === "block" ? "block" : "allow",
1332
- category: result.category ?? "unknown",
1333
- triggered: false,
1334
- // not available from async API — always false
1335
- detections: {}
1336
- // not available from async API
1834
+ ...scanResponseToResult(result, ""),
1835
+ scanId: id,
1836
+ action: result.action === "block" ? "block" : "allow"
1337
1837
  });
1338
1838
  pending.delete(id);
1339
1839
  } else if (status === "failed") {
@@ -1345,22 +1845,38 @@ var SdkRuntimeService = class {
1345
1845
  scanId: id,
1346
1846
  reportId: "",
1347
1847
  action: "allow",
1348
- // safe default for failed scans
1349
1848
  category: "error",
1350
1849
  triggered: false,
1351
- // not available from async API — always false
1352
- detections: {}
1353
- // not available from async API
1850
+ detections: {},
1851
+ error: "AIRS async scan failed"
1354
1852
  });
1355
1853
  pending.delete(id);
1356
1854
  }
1357
1855
  }
1358
1856
  }
1359
1857
  static formatResultsCsv(results) {
1360
- 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(",");
1361
1868
  const rows = results.map((r) => {
1362
- const escaped = r.prompt.replace(/"/g, '""');
1363
- 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(",");
1364
1880
  });
1365
1881
  return [header, ...rows].join("\n");
1366
1882
  }
@@ -1509,6 +2025,7 @@ var ConfigSchema = z.object({
1509
2025
  redTeamDataEndpoint: z.string().optional(),
1510
2026
  redTeamMgmtEndpoint: z.string().optional(),
1511
2027
  redTeamTokenEndpoint: z.string().optional(),
2028
+ redTeamNetworkBrokerEndpoint: z.string().optional(),
1512
2029
  // Model Security (endpoints only; creds shared with mgmt*)
1513
2030
  modelSecDataEndpoint: z.string().optional(),
1514
2031
  modelSecMgmtEndpoint: z.string().optional(),
@@ -1539,6 +2056,7 @@ function fromEnv() {
1539
2056
  redTeamDataEndpoint: env.PANW_RED_TEAM_DATA_ENDPOINT,
1540
2057
  redTeamMgmtEndpoint: env.PANW_RED_TEAM_MGMT_ENDPOINT,
1541
2058
  redTeamTokenEndpoint: env.PANW_RED_TEAM_TOKEN_ENDPOINT,
2059
+ redTeamNetworkBrokerEndpoint: env.PANW_RED_TEAM_NETWORK_BROKER_ENDPOINT,
1542
2060
  modelSecDataEndpoint: env.PANW_MODEL_SEC_DATA_ENDPOINT,
1543
2061
  modelSecMgmtEndpoint: env.PANW_MODEL_SEC_MGMT_ENDPOINT,
1544
2062
  modelSecTokenEndpoint: env.PANW_MODEL_SEC_TOKEN_ENDPOINT,
@@ -1731,6 +2249,7 @@ export {
1731
2249
  SdkModelSecurityService,
1732
2250
  SdkPromptSetService,
1733
2251
  SdkRedTeamService,
2252
+ SDK_ASYNC_BATCH_SIZE,
1734
2253
  SdkRuntimeService,
1735
2254
  AirsScanService,
1736
2255
  RateLimitedScanService,