@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.
package/dist/cli/index.js CHANGED
@@ -3,6 +3,7 @@ import {
3
3
  AirsScanService,
4
4
  ConfigSchema,
5
5
  RateLimitedScanService,
6
+ SDK_ASYNC_BATCH_SIZE,
6
7
  SdkManagementService,
7
8
  SdkModelSecurityService,
8
9
  SdkPromptSetService,
@@ -19,7 +20,7 @@ import {
19
20
  sanitizeFilename,
20
21
  validateTopic,
21
22
  writeBackupFile
22
- } from "../chunk-DSNQSBLE.js";
23
+ } from "../chunk-2VIUZRPB.js";
23
24
 
24
25
  // src/cli/index.ts
25
26
  import "dotenv/config";
@@ -40,20 +41,211 @@ function installProcessGuards() {
40
41
  // src/cli/program.ts
41
42
  import { readFileSync as readFileSync4 } from "fs";
42
43
  import { homedir } from "os";
43
- import { dirname as dirname3, join as join3 } from "path";
44
+ import { dirname as dirname4, join as join4 } from "path";
44
45
  import { fileURLToPath } from "url";
45
46
  import { Command } from "commander";
46
47
 
47
- // src/cli/examples.ts
48
- function examples(...lines) {
49
- return `
50
- Examples:
51
- ${lines.map((l) => ` $ ${l}`).join("\n")}
52
- `;
48
+ // src/airs/aigateway.ts
49
+ import {
50
+ AIGatewayClient
51
+ } from "@cdot65/prisma-airs-sdk";
52
+ function toLimitArray(value) {
53
+ if (Array.isArray(value)) return value;
54
+ if (value !== null && typeof value === "object") return [value];
55
+ return [];
56
+ }
57
+ function normalizeWorkspace(raw) {
58
+ return {
59
+ id: raw.id,
60
+ slug: raw.slug,
61
+ name: raw.name,
62
+ icon: raw.icon,
63
+ description: raw.description,
64
+ createdAt: raw.created_at,
65
+ lastUpdatedAt: raw.last_updated_at,
66
+ isDefault: Boolean(raw.is_default),
67
+ status: raw.status,
68
+ scopeName: raw.scope_name
69
+ };
70
+ }
71
+ function normalizeWorkspaceDetail(raw) {
72
+ return {
73
+ ...normalizeWorkspace(raw),
74
+ defaults: raw.defaults,
75
+ usageLimits: toLimitArray(raw.usage_limits),
76
+ rateLimits: toLimitArray(raw.rate_limits),
77
+ securitySettings: raw.security_settings,
78
+ dataPlaneSecuritySettings: raw.data_plane_security_settings,
79
+ settings: raw.settings
80
+ };
81
+ }
82
+ function aiGatewayGrantHint(err) {
83
+ const status = err?.status ?? err?.statusCode;
84
+ if (status !== 403) return void 0;
85
+ const message = err instanceof Error ? err.message : String(err);
86
+ const grant = message.includes("AB03") ? "the service account is missing a workspace-scope grant (data plane, /ai_gw/v2)" : "the service account is missing a tenant-root admin grant (admin plane, /ai_gw/admin/v2)";
87
+ return `${grant}. SCM Access Management edits the existing role row by default \u2014 use "Add Role" so the account ends up with both role rows, not one row moved.`;
53
88
  }
89
+ var SdkAiGatewayService = class {
90
+ client;
91
+ constructor(opts) {
92
+ this.client = new AIGatewayClient(opts);
93
+ }
94
+ async listWorkspaces(options) {
95
+ const response = await this.client.workspaces.list(options);
96
+ return response.data.map(normalizeWorkspace);
97
+ }
98
+ async listAllWorkspaces() {
99
+ const [active, archived] = await Promise.all([
100
+ this.client.workspaces.list({ plane: "admin" }),
101
+ this.client.workspaces.list({ plane: "admin", status: "archived" })
102
+ ]);
103
+ return [
104
+ ...active.data,
105
+ ...archived.data
106
+ ].map(normalizeWorkspace);
107
+ }
108
+ async getWorkspace(workspaceRef, options) {
109
+ try {
110
+ const raw = await this.client.workspaces.get(workspaceRef, options);
111
+ return normalizeWorkspaceDetail(raw);
112
+ } catch (err) {
113
+ const status = err.statusCode;
114
+ if (status !== 404) throw err;
115
+ const resolved = await this.resolveWorkspaceRef(workspaceRef, [
116
+ options?.plane ?? "data",
117
+ "admin"
118
+ ]);
119
+ if (resolved === workspaceRef) throw err;
120
+ const raw = await this.client.workspaces.get(resolved, options);
121
+ return normalizeWorkspaceDetail(raw);
122
+ }
123
+ }
124
+ async createWorkspace(request) {
125
+ const body = {
126
+ name: request.name,
127
+ scope_name: request.scopeName
128
+ };
129
+ if (request.description !== void 0) body.description = request.description;
130
+ if (request.icon !== void 0) body.icon = request.icon;
131
+ if (request.defaults !== void 0) body.defaults = request.defaults;
132
+ if (request.users !== void 0) body.users = request.users;
133
+ if (request.usageLimits !== void 0) body.usage_limits = request.usageLimits;
134
+ if (request.rateLimits !== void 0) body.rate_limits = request.rateLimits;
135
+ const created = await this.client.workspaces.create(body);
136
+ return this.refetchAfterWrite(created.id, created);
137
+ }
138
+ async updateWorkspace(workspaceRef, request) {
139
+ const ref = await this.resolveWorkspaceRef(workspaceRef, ["admin"]);
140
+ const body = {};
141
+ if (request.name !== void 0) body.name = request.name;
142
+ if (request.description !== void 0) body.description = request.description;
143
+ if (request.icon !== void 0) body.icon = request.icon;
144
+ if (request.defaults !== void 0) body.defaults = request.defaults;
145
+ if (request.usageLimits !== void 0) body.usage_limits = request.usageLimits;
146
+ if (request.rateLimits !== void 0) body.rate_limits = request.rateLimits;
147
+ await this.client.workspaces.update(ref, body);
148
+ return this.getWorkspace(ref, { plane: "admin" });
149
+ }
150
+ async deleteWorkspace(workspaceRef) {
151
+ const ref = await this.resolveWorkspaceRef(workspaceRef, ["admin"]);
152
+ await this.client.workspaces.delete(ref);
153
+ }
154
+ /**
155
+ * The API accepts only a UUID or slug as a workspace ref — a display name
156
+ * gets a misleading 400 AB01 ("No update fields provided") on writes.
157
+ * Match a user-supplied ref against the workspace list so name | slug |
158
+ * uuid all work. Unmatched refs pass through so the API's own error stands.
159
+ */
160
+ async resolveWorkspaceRef(ref, planes) {
161
+ for (const plane of planes) {
162
+ let rows;
163
+ try {
164
+ rows = await this.listWorkspaces({ plane });
165
+ } catch {
166
+ continue;
167
+ }
168
+ if (rows.some((w) => w.id === ref || w.slug === ref)) return ref;
169
+ const byName = rows.filter((w) => w.name === ref);
170
+ if (byName.length > 1) {
171
+ throw new Error(
172
+ `workspace name '${ref}' is ambiguous (${byName.map((w) => w.slug).join(", ")}) \u2014 use a slug or UUID`
173
+ );
174
+ }
175
+ if (byName.length === 1) return byName[0].slug;
176
+ }
177
+ return ref;
178
+ }
179
+ async getTelemetryCost(opts) {
180
+ const days = opts.days ?? 7;
181
+ const workspaceSlug = await this.resolveWorkspaceRef(opts.workspaceSlug, ["data", "admin"]);
182
+ const raw = await this.client.telemetry.cost({
183
+ workspaceSlug,
184
+ days
185
+ });
186
+ return {
187
+ workspaceSlug,
188
+ days,
189
+ totalCents: raw.data.total,
190
+ avgCents: raw.data.avg,
191
+ quotaExceeded: raw.data.isQuotaExceeded,
192
+ records: raw.data.records.map((r) => ({ date: r.x, costCents: r.y }))
193
+ };
194
+ }
195
+ /** Re-read after a write, falling back to the (partial) write response if the get fails. */
196
+ async refetchAfterWrite(workspaceRef, writeResponse) {
197
+ try {
198
+ return await this.getWorkspace(workspaceRef, { plane: "admin" });
199
+ } catch {
200
+ return normalizeWorkspaceDetail(writeResponse);
201
+ }
202
+ }
203
+ };
54
204
 
55
- // src/cli/renderer/ui.ts
56
- import chalk3 from "chalk";
205
+ // src/config/client-options.ts
206
+ function runtimeInitOptions(config) {
207
+ return {
208
+ apiKey: config.airsApiKey,
209
+ apiToken: config.airsApiToken,
210
+ apiEndpoint: config.airsApiEndpoint,
211
+ numRetries: config.airsNumRetries
212
+ };
213
+ }
214
+ function redTeamClientOptions(config) {
215
+ return {
216
+ clientId: config.mgmtClientId,
217
+ clientSecret: config.mgmtClientSecret,
218
+ tsgId: config.mgmtTsgId,
219
+ dataEndpoint: config.redTeamDataEndpoint,
220
+ mgmtEndpoint: config.redTeamMgmtEndpoint,
221
+ tokenEndpoint: config.redTeamTokenEndpoint ?? config.mgmtTokenEndpoint,
222
+ networkBrokerEndpoint: config.redTeamNetworkBrokerEndpoint
223
+ };
224
+ }
225
+ function aiGatewayClientOptions(config) {
226
+ return {
227
+ clientId: config.mgmtClientId,
228
+ clientSecret: config.mgmtClientSecret,
229
+ tsgId: config.mgmtTsgId,
230
+ dataEndpoint: config.aiGwDataEndpoint,
231
+ adminEndpoint: config.aiGwAdminEndpoint,
232
+ tokenEndpoint: config.aiGwTokenEndpoint ?? config.mgmtTokenEndpoint
233
+ };
234
+ }
235
+ function modelSecurityClientOptions(config) {
236
+ return {
237
+ clientId: config.mgmtClientId,
238
+ clientSecret: config.mgmtClientSecret,
239
+ tsgId: config.mgmtTsgId,
240
+ dataEndpoint: config.modelSecDataEndpoint,
241
+ mgmtEndpoint: config.modelSecMgmtEndpoint,
242
+ tokenEndpoint: config.modelSecTokenEndpoint ?? config.mgmtTokenEndpoint
243
+ };
244
+ }
245
+
246
+ // src/cli/renderer/aigateway.ts
247
+ import chalk4 from "chalk";
248
+ import { dump as yamlDump } from "js-yaml";
57
249
 
58
250
  // src/cli/renderer/common.ts
59
251
  import chalk2 from "chalk";
@@ -120,6 +312,7 @@ function formatOutput(rows, columns, format) {
120
312
  }
121
313
 
122
314
  // src/cli/renderer/ui.ts
315
+ import chalk3 from "chalk";
123
316
  var INDENT = " ";
124
317
  var quietMode = false;
125
318
  function setQuiet(quiet) {
@@ -210,6 +403,118 @@ ${INDENT}${chalk3.bold(label)}
210
403
  }
211
404
  };
212
405
 
406
+ // src/cli/renderer/aigateway.ts
407
+ function renderAiGatewayHeader() {
408
+ ui.header("Prisma AIRS \u2014 AI Gateway", "Gateway workspace operations");
409
+ }
410
+ function statusColor(status) {
411
+ switch (status.toLowerCase()) {
412
+ case "active":
413
+ return chalk4.green;
414
+ case "archived":
415
+ return chalk4.yellow;
416
+ default:
417
+ return chalk4.dim;
418
+ }
419
+ }
420
+ function statusLabel(status) {
421
+ return status ?? "unknown";
422
+ }
423
+ function renderWorkspaceList(workspaces, format = "pretty") {
424
+ if (workspaces.length === 0) {
425
+ ui.emptyList("workspaces");
426
+ return;
427
+ }
428
+ if (format !== "pretty") {
429
+ const rows = workspaces.map((w) => ({
430
+ id: w.id,
431
+ slug: w.slug,
432
+ name: w.name,
433
+ status: statusLabel(w.status),
434
+ isDefault: w.isDefault,
435
+ scopeName: w.scopeName ?? ""
436
+ }));
437
+ console.log(
438
+ formatOutput(
439
+ rows,
440
+ [
441
+ { key: "id", label: "ID" },
442
+ { key: "slug", label: "Slug" },
443
+ { key: "name", label: "Name" },
444
+ { key: "status", label: "Status" },
445
+ { key: "isDefault", label: "Default" },
446
+ { key: "scopeName", label: "Scope" }
447
+ ],
448
+ format
449
+ )
450
+ );
451
+ return;
452
+ }
453
+ ui.section("AI Gateway Workspaces:");
454
+ for (const w of workspaces) {
455
+ ui.dim(w.id);
456
+ const status = statusColor(statusLabel(w.status))(statusLabel(w.status));
457
+ const dflt = w.isDefault ? chalk4.cyan(" default") : "";
458
+ console.log(` ${w.name} ${chalk4.dim(w.slug)} ${status}${dflt}`);
459
+ if (w.scopeName) console.log(` ${chalk4.dim(`scope: ${w.scopeName}`)}`);
460
+ console.log();
461
+ }
462
+ }
463
+ function renderWorkspaceDetail(workspace, format = "pretty") {
464
+ if (format !== "pretty") {
465
+ console.log(format === "json" ? JSON.stringify(workspace, null, 2) : yamlDump(workspace));
466
+ return;
467
+ }
468
+ ui.section("Workspace Detail:");
469
+ const pairs = [
470
+ ["ID", workspace.id],
471
+ ["Slug", workspace.slug],
472
+ ["Name", workspace.name],
473
+ ["Status", statusColor(statusLabel(workspace.status))(statusLabel(workspace.status))],
474
+ ["Default", workspace.isDefault ? "yes" : "no"]
475
+ ];
476
+ if (workspace.description != null) pairs.push(["Description", workspace.description]);
477
+ if (workspace.scopeName != null) pairs.push(["Scope", workspace.scopeName]);
478
+ if (workspace.createdAt != null) pairs.push(["Created", workspace.createdAt]);
479
+ if (workspace.lastUpdatedAt != null) pairs.push(["Updated", workspace.lastUpdatedAt]);
480
+ ui.keyValue(pairs);
481
+ if (workspace.defaults && Object.keys(workspace.defaults).length > 0) {
482
+ ui.section("Defaults:");
483
+ console.log(chalk4.dim(JSON.stringify(workspace.defaults, null, 2)));
484
+ }
485
+ if (workspace.usageLimits.length > 0) {
486
+ ui.section("Usage Limits:");
487
+ console.log(chalk4.dim(JSON.stringify(workspace.usageLimits, null, 2)));
488
+ }
489
+ if (workspace.rateLimits.length > 0) {
490
+ ui.section("Rate Limits:");
491
+ console.log(chalk4.dim(JSON.stringify(workspace.rateLimits, null, 2)));
492
+ }
493
+ if (workspace.securitySettings && Object.keys(workspace.securitySettings).length > 0) {
494
+ ui.section("Security Settings:");
495
+ ui.keyValue(Object.entries(workspace.securitySettings).map(([k, v]) => [k, v]));
496
+ }
497
+ console.log();
498
+ }
499
+ function renderCostReport(report, format = "pretty") {
500
+ if (format !== "pretty") {
501
+ console.log(format === "json" ? JSON.stringify(report, null, 2) : yamlDump(report));
502
+ return;
503
+ }
504
+ const dollars = (cents) => `$${(cents / 100).toFixed(2)}`;
505
+ ui.section(`Cost \u2014 ${report.workspaceSlug} (last ${report.days}d):`);
506
+ ui.keyValue([
507
+ ["Total", dollars(report.totalCents)],
508
+ ["Daily average", dollars(report.avgCents)]
509
+ ]);
510
+ if (report.quotaExceeded) ui.warn("Telemetry quota exceeded \u2014 data may be truncated");
511
+ if (report.records.length > 0) {
512
+ ui.section("Per day:");
513
+ ui.keyValue(report.records.map((r) => [r.date, dollars(r.costCents)]));
514
+ }
515
+ console.log();
516
+ }
517
+
213
518
  // src/cli/renderer/backup.ts
214
519
  function renderBackupHeader() {
215
520
  ui.header("Prisma AIRS \u2014 Backup & Restore");
@@ -251,17 +556,17 @@ function renderRestoreSummary(results) {
251
556
  }
252
557
 
253
558
  // src/cli/renderer/dlp.ts
254
- import chalk4 from "chalk";
255
- import { dump as yamlDump } from "js-yaml";
256
- function statusColor(status) {
559
+ import chalk5 from "chalk";
560
+ import { dump as yamlDump2 } from "js-yaml";
561
+ function statusColor2(status) {
257
562
  switch (status) {
258
563
  case "active":
259
- return chalk4.green(status);
564
+ return chalk5.green(status);
260
565
  case "deleted":
261
566
  case "disabled":
262
- return chalk4.yellow(status);
567
+ return chalk5.yellow(status);
263
568
  default:
264
- return status ? chalk4.dim(status) : chalk4.dim("\u2014");
569
+ return status ? chalk5.dim(status) : chalk5.dim("\u2014");
265
570
  }
266
571
  }
267
572
  function ts(ms) {
@@ -278,7 +583,7 @@ function emitStructured(payload, fmt) {
278
583
  return;
279
584
  }
280
585
  if (fmt === "yaml") {
281
- console.log(yamlDump(payload));
586
+ console.log(yamlDump2(payload));
282
587
  return;
283
588
  }
284
589
  console.log(JSON.stringify(payload, null, 2));
@@ -397,11 +702,11 @@ var dlpFilteringProfiles = {
397
702
  { key: "version", label: "Version" }
398
703
  ],
399
704
  (it) => {
400
- const dir = it.direction ? chalk4.dim(` dir:${it.direction}`) : "";
401
- const sev = it.log_severity ? chalk4.dim(` sev:${it.log_severity}`) : "";
402
- const ver = it.version != null ? chalk4.dim(` v${it.version}`) : "";
403
- return ` ${chalk4.dim(it.id)}
404
- ${it.name} ${chalk4.cyan(it.type ?? "")}${dir}${sev}${ver}`;
705
+ const dir = it.direction ? chalk5.dim(` dir:${it.direction}`) : "";
706
+ const sev = it.log_severity ? chalk5.dim(` sev:${it.log_severity}`) : "";
707
+ const ver = it.version != null ? chalk5.dim(` v${it.version}`) : "";
708
+ return ` ${chalk5.dim(it.id)}
709
+ ${it.name} ${chalk5.cyan(it.type ?? "")}${dir}${sev}${ver}`;
405
710
  }
406
711
  );
407
712
  },
@@ -456,10 +761,10 @@ var dlpPatterns = {
456
761
  { key: "version", label: "Version" }
457
762
  ],
458
763
  (it) => {
459
- const tech = it.detection_config?.technique ? chalk4.dim(` ${it.detection_config.technique}`) : "";
460
- const ver = it.version != null ? chalk4.dim(` v${it.version}`) : "";
461
- return ` ${chalk4.dim(it.id)}
462
- ${it.name} ${chalk4.cyan(it.type ?? "")} ${statusColor(it.status)}${tech}${ver}`;
764
+ const tech = it.detection_config?.technique ? chalk5.dim(` ${it.detection_config.technique}`) : "";
765
+ const ver = it.version != null ? chalk5.dim(` v${it.version}`) : "";
766
+ return ` ${chalk5.dim(it.id)}
767
+ ${it.name} ${chalk5.cyan(it.type ?? "")} ${statusColor2(it.status)}${tech}${ver}`;
463
768
  }
464
769
  );
465
770
  },
@@ -524,10 +829,10 @@ var dlpProfiles = {
524
829
  { key: "version", label: "Version" }
525
830
  ],
526
831
  (it) => {
527
- const ptype = it.profile_type ? chalk4.dim(` ${it.profile_type}`) : "";
528
- const ver = it.version != null ? chalk4.dim(` v${it.version}`) : "";
529
- return ` ${chalk4.dim(it.id)}
530
- ${it.name} ${chalk4.cyan(it.type ?? "")} ${statusColor(it.profile_status)}${ptype}${ver}`;
832
+ const ptype = it.profile_type ? chalk5.dim(` ${it.profile_type}`) : "";
833
+ const ver = it.version != null ? chalk5.dim(` v${it.version}`) : "";
834
+ return ` ${chalk5.dim(it.id)}
835
+ ${it.name} ${chalk5.cyan(it.type ?? "")} ${statusColor2(it.profile_status)}${ptype}${ver}`;
531
836
  }
532
837
  );
533
838
  },
@@ -581,10 +886,10 @@ var dlpDictionaries = {
581
886
  { key: "version", label: "Version" }
582
887
  ],
583
888
  (it) => {
584
- const kw = Array.isArray(it.keywords) ? chalk4.dim(` ${it.keywords.length} kw`) : "";
585
- const ver = it.version != null ? chalk4.dim(` v${it.version}`) : "";
586
- return ` ${chalk4.dim(it.id)}
587
- ${it.name} ${chalk4.cyan(it.type ?? "")} ${statusColor(it.status)}${kw}${ver}`;
889
+ const kw = Array.isArray(it.keywords) ? chalk5.dim(` ${it.keywords.length} kw`) : "";
890
+ const ver = it.version != null ? chalk5.dim(` v${it.version}`) : "";
891
+ return ` ${chalk5.dim(it.id)}
892
+ ${it.name} ${chalk5.cyan(it.type ?? "")} ${statusColor2(it.status)}${kw}${ver}`;
588
893
  }
589
894
  );
590
895
  },
@@ -626,7 +931,7 @@ var dlpDictionaries = {
626
931
  };
627
932
 
628
933
  // src/cli/renderer/eval.ts
629
- import chalk5 from "chalk";
934
+ import chalk6 from "chalk";
630
935
  function buildEvalOutput(profile, topic, intent, metrics, results) {
631
936
  const fps = results.filter((r) => !r.testCase.expectedTriggered && r.actualTriggered).map((r) => ({ prompt: r.testCase.prompt, expected: false, actual: true }));
632
937
  const fns = results.filter((r) => r.testCase.expectedTriggered && !r.actualTriggered).map((r) => ({ prompt: r.testCase.prompt, expected: true, actual: false }));
@@ -650,7 +955,7 @@ function buildEvalOutput(profile, topic, intent, metrics, results) {
650
955
  };
651
956
  }
652
957
  function renderEvalTerminal(output) {
653
- const coverageColor = output.metrics.coverage >= 0.9 ? chalk5.green : output.metrics.coverage >= 0.7 ? chalk5.yellow : chalk5.red;
958
+ const coverageColor = output.metrics.coverage >= 0.9 ? chalk6.green : output.metrics.coverage >= 0.7 ? chalk6.yellow : chalk6.red;
654
959
  ui.header("Eval Results");
655
960
  ui.keyValue([
656
961
  ["Profile", output.profile],
@@ -683,8 +988,8 @@ function renderEvalTerminal(output) {
683
988
  }
684
989
 
685
990
  // src/cli/renderer/modelsecurity.ts
686
- import chalk6 from "chalk";
687
- import { dump as yamlDump2 } from "js-yaml";
991
+ import chalk7 from "chalk";
992
+ import { dump as yamlDump3 } from "js-yaml";
688
993
  function renderModelSecurityHeader() {
689
994
  ui.header("Prisma AIRS \u2014 Model Security", "ML model supply chain security");
690
995
  }
@@ -695,15 +1000,15 @@ function stateColor(state) {
695
1000
  case "ALLOWING":
696
1001
  case "PASSED":
697
1002
  case "SUCCESS":
698
- return chalk6.green;
1003
+ return chalk7.green;
699
1004
  case "BLOCKED":
700
1005
  case "BLOCKING":
701
1006
  case "FAILED":
702
- return chalk6.red;
1007
+ return chalk7.red;
703
1008
  case "DISABLED":
704
- return chalk6.dim;
1009
+ return chalk7.dim;
705
1010
  default:
706
- return chalk6.yellow;
1011
+ return chalk7.yellow;
707
1012
  }
708
1013
  }
709
1014
  function renderGroupList(groups, format = "pretty") {
@@ -735,8 +1040,8 @@ function renderGroupList(groups, format = "pretty") {
735
1040
  ui.section("Security Groups:");
736
1041
  for (const g of groups) {
737
1042
  ui.dim(g.uuid);
738
- const color = g.state === "ACTIVE" ? chalk6.green : chalk6.yellow;
739
- console.log(` ${g.name} ${color(g.state)} source: ${chalk6.dim(g.sourceType)}`);
1043
+ const color = g.state === "ACTIVE" ? chalk7.green : chalk7.yellow;
1044
+ console.log(` ${g.name} ${color(g.state)} source: ${chalk7.dim(g.sourceType)}`);
740
1045
  }
741
1046
  console.log();
742
1047
  }
@@ -746,15 +1051,15 @@ function renderGroupDetail(group, format = "pretty") {
746
1051
  return;
747
1052
  }
748
1053
  if (format === "yaml") {
749
- console.log(yamlDump2(group));
1054
+ console.log(yamlDump3(group));
750
1055
  return;
751
1056
  }
752
1057
  ui.section("Security Group Detail:");
753
- const color = group.state === "ACTIVE" ? chalk6.green : chalk6.yellow;
1058
+ const color = group.state === "ACTIVE" ? chalk7.green : chalk7.yellow;
754
1059
  ui.keyValue([
755
1060
  ["UUID", group.uuid],
756
1061
  ["Name", group.name],
757
- ["Description", group.description || chalk6.dim("(none)")],
1062
+ ["Description", group.description || chalk7.dim("(none)")],
758
1063
  ["Source Type", group.sourceType],
759
1064
  ["State", color(group.state)],
760
1065
  ["Created", group.createdAt],
@@ -794,10 +1099,10 @@ function renderRuleList(rules, format = "pretty") {
794
1099
  for (const r of rules) {
795
1100
  ui.dim(r.uuid);
796
1101
  console.log(
797
- ` ${r.name} type: ${chalk6.dim(r.ruleType)} default: ${chalk6.dim(r.defaultState)}`
1102
+ ` ${r.name} type: ${chalk7.dim(r.ruleType)} default: ${chalk7.dim(r.defaultState)}`
798
1103
  );
799
- console.log(` ${chalk6.dim(r.description)}`);
800
- console.log(` Sources: ${r.compatibleSources.map((s) => chalk6.dim(s)).join(", ")}`);
1104
+ console.log(` ${chalk7.dim(r.description)}`);
1105
+ console.log(` Sources: ${r.compatibleSources.map((s) => chalk7.dim(s)).join(", ")}`);
801
1106
  }
802
1107
  console.log();
803
1108
  }
@@ -826,8 +1131,8 @@ function renderRuleDetail(rule) {
826
1131
  if (rule.editableFields.length > 0) {
827
1132
  ui.section("Editable Fields:");
828
1133
  for (const f of rule.editableFields) {
829
- console.log(` ${f.displayName} (${chalk6.dim(f.attributeName)}): ${f.displayType}`);
830
- if (f.description) console.log(` ${chalk6.dim(f.description)}`);
1134
+ console.log(` ${f.displayName} (${chalk7.dim(f.attributeName)}): ${f.displayType}`);
1135
+ if (f.description) console.log(` ${chalk7.dim(f.description)}`);
831
1136
  }
832
1137
  }
833
1138
  console.log();
@@ -905,13 +1210,13 @@ function renderMsScanList(scans, format = "pretty") {
905
1210
  for (const s of scans) {
906
1211
  ui.dim(s.uuid);
907
1212
  console.log(
908
- ` ${stateColor(s.evalOutcome)(s.evalOutcome)} ${chalk6.dim(s.scanOrigin)} ${chalk6.dim(s.createdAt)}`
1213
+ ` ${stateColor(s.evalOutcome)(s.evalOutcome)} ${chalk7.dim(s.scanOrigin)} ${chalk7.dim(s.createdAt)}`
909
1214
  );
910
- if (s.modelUri) console.log(` ${chalk6.dim(s.modelUri)}`);
1215
+ if (s.modelUri) console.log(` ${chalk7.dim(s.modelUri)}`);
911
1216
  if (s.evalSummary) {
912
1217
  const { rulesPassed, rulesFailed, totalRules } = s.evalSummary;
913
1218
  console.log(
914
- ` Rules: ${chalk6.green(`${rulesPassed} passed`)} ${chalk6.red(`${rulesFailed} failed`)} / ${totalRules} total`
1219
+ ` Rules: ${chalk7.green(`${rulesPassed} passed`)} ${chalk7.red(`${rulesFailed} failed`)} / ${totalRules} total`
915
1220
  );
916
1221
  }
917
1222
  }
@@ -933,7 +1238,7 @@ function renderMsScanDetail(scan) {
933
1238
  const { rulesPassed, rulesFailed, totalRules } = scan.evalSummary;
934
1239
  pairs.push([
935
1240
  "Rules",
936
- `${chalk6.green(`${rulesPassed} passed`)} ${chalk6.red(`${rulesFailed} failed`)} / ${totalRules} total`
1241
+ `${chalk7.green(`${rulesPassed} passed`)} ${chalk7.red(`${rulesFailed} failed`)} / ${totalRules} total`
937
1242
  ]);
938
1243
  }
939
1244
  ui.keyValue(pairs);
@@ -952,7 +1257,7 @@ function renderEvaluationList(evaluations) {
952
1257
  for (const e of evaluations) {
953
1258
  ui.dim(e.uuid);
954
1259
  console.log(
955
- ` ${e.ruleName} ${stateColor(e.result)(e.result)} ${chalk6.dim(e.ruleInstanceState)}`
1260
+ ` ${e.ruleName} ${stateColor(e.result)(e.result)} ${chalk7.dim(e.ruleInstanceState)}`
956
1261
  );
957
1262
  }
958
1263
  console.log();
@@ -978,9 +1283,9 @@ function renderViolationList(violations) {
978
1283
  ui.section("Violations:");
979
1284
  for (const v of violations) {
980
1285
  ui.dim(v.uuid);
981
- console.log(` ${chalk6.red(v.ruleName)} ${chalk6.dim(v.file)}`);
1286
+ console.log(` ${chalk7.red(v.ruleName)} ${chalk7.dim(v.file)}`);
982
1287
  console.log(` ${v.description}`);
983
- console.log(` Threat: ${chalk6.dim(v.threat)}`);
1288
+ console.log(` Threat: ${chalk7.dim(v.threat)}`);
984
1289
  }
985
1290
  console.log();
986
1291
  }
@@ -988,7 +1293,7 @@ function renderViolationDetail(violation) {
988
1293
  ui.section("Violation Detail:");
989
1294
  ui.keyValue([
990
1295
  ["UUID", violation.uuid],
991
- ["Rule", chalk6.red(violation.ruleName)],
1296
+ ["Rule", chalk7.red(violation.ruleName)],
992
1297
  ["Description", violation.ruleDescription],
993
1298
  ["State", violation.ruleInstanceState],
994
1299
  ["File", violation.file],
@@ -1004,8 +1309,8 @@ function renderFileList(files) {
1004
1309
  }
1005
1310
  ui.section("Scanned Files:");
1006
1311
  for (const f of files) {
1007
- const color = f.result === "SUCCESS" ? chalk6.green : f.result === "SKIPPED" ? chalk6.yellow : chalk6.red;
1008
- const formats = f.formats.length > 0 ? chalk6.dim(` [${f.formats.join(", ")}]`) : "";
1312
+ const color = f.result === "SUCCESS" ? chalk7.green : f.result === "SKIPPED" ? chalk7.yellow : chalk7.red;
1313
+ const formats = f.formats.length > 0 ? chalk7.dim(` [${f.formats.join(", ")}]`) : "";
1009
1314
  console.log(` ${color(f.result)} ${f.type} ${f.path}${formats}`);
1010
1315
  }
1011
1316
  console.log();
@@ -1063,15 +1368,15 @@ function renderModelList(models, format = "pretty") {
1063
1368
  ui.section("Models:");
1064
1369
  for (const m of models) {
1065
1370
  ui.dim(m.uuid);
1066
- const outcome = m.latestVersionOutcome ? stateColor(m.latestVersionOutcome)(m.latestVersionOutcome) : chalk6.dim("unscanned");
1067
- const formats = m.latestVersionFormats && m.latestVersionFormats.length > 0 ? chalk6.dim(` [${m.latestVersionFormats.join(", ")}]`) : "";
1371
+ const outcome = m.latestVersionOutcome ? stateColor(m.latestVersionOutcome)(m.latestVersionOutcome) : chalk7.dim("unscanned");
1372
+ const formats = m.latestVersionFormats && m.latestVersionFormats.length > 0 ? chalk7.dim(` [${m.latestVersionFormats.join(", ")}]`) : "";
1068
1373
  console.log(` ${m.name} ${outcome}${formats}`);
1069
1374
  console.log();
1070
1375
  }
1071
1376
  }
1072
1377
  function renderModelDetail(model, format = "pretty") {
1073
1378
  if (format !== "pretty") {
1074
- console.log(format === "json" ? JSON.stringify(model, null, 2) : yamlDump2(model));
1379
+ console.log(format === "json" ? JSON.stringify(model, null, 2) : yamlDump3(model));
1075
1380
  return;
1076
1381
  }
1077
1382
  ui.section("Model Detail:");
@@ -1128,7 +1433,7 @@ function renderModelVersionList(versions, format = "pretty") {
1128
1433
  ui.section("Model Versions:");
1129
1434
  for (const v of versions) {
1130
1435
  ui.dim(v.uuid);
1131
- const outcome = v.lastEvalOutcome ? stateColor(v.lastEvalOutcome)(v.lastEvalOutcome) : chalk6.dim("unscanned");
1436
+ const outcome = v.lastEvalOutcome ? stateColor(v.lastEvalOutcome)(v.lastEvalOutcome) : chalk7.dim("unscanned");
1132
1437
  const files = v.fileCount != null ? ` files: ${v.fileCount}` : "";
1133
1438
  console.log(` ${v.revision} ${outcome}${files}`);
1134
1439
  console.log();
@@ -1136,7 +1441,7 @@ function renderModelVersionList(versions, format = "pretty") {
1136
1441
  }
1137
1442
  function renderModelVersionDetail(version, format = "pretty") {
1138
1443
  if (format !== "pretty") {
1139
- console.log(format === "json" ? JSON.stringify(version, null, 2) : yamlDump2(version));
1444
+ console.log(format === "json" ? JSON.stringify(version, null, 2) : yamlDump3(version));
1140
1445
  return;
1141
1446
  }
1142
1447
  ui.section("Model Version Detail:");
@@ -1199,45 +1504,45 @@ function renderModelFileList(files, format = "pretty") {
1199
1504
  }
1200
1505
 
1201
1506
  // src/cli/renderer/redteam.ts
1202
- import chalk7 from "chalk";
1203
- import { dump as yamlDump3 } from "js-yaml";
1507
+ import chalk8 from "chalk";
1508
+ import { dump as yamlDump4 } from "js-yaml";
1204
1509
  function renderRedteamHeader() {
1205
1510
  ui.header("Prisma AIRS \u2014 AI Red Team", "Adversarial scan operations");
1206
1511
  }
1207
1512
  function severityColor(severity) {
1208
1513
  switch (severity.toUpperCase()) {
1209
1514
  case "CRITICAL":
1210
- return chalk7.red;
1515
+ return chalk8.red;
1211
1516
  case "HIGH":
1212
- return chalk7.magenta;
1517
+ return chalk8.magenta;
1213
1518
  case "MEDIUM":
1214
- return chalk7.yellow;
1519
+ return chalk8.yellow;
1215
1520
  case "LOW":
1216
- return chalk7.cyan;
1521
+ return chalk8.cyan;
1217
1522
  default:
1218
- return chalk7.dim;
1523
+ return chalk8.dim;
1219
1524
  }
1220
1525
  }
1221
- function statusColor2(status) {
1526
+ function statusColor3(status) {
1222
1527
  switch (status) {
1223
1528
  case "COMPLETED":
1224
- return chalk7.green;
1529
+ return chalk8.green;
1225
1530
  case "RUNNING":
1226
- return chalk7.blue;
1531
+ return chalk8.blue;
1227
1532
  case "QUEUED":
1228
1533
  case "INIT":
1229
- return chalk7.yellow;
1534
+ return chalk8.yellow;
1230
1535
  case "FAILED":
1231
1536
  case "ABORTED":
1232
- return chalk7.red;
1537
+ return chalk8.red;
1233
1538
  case "PARTIALLY_COMPLETE":
1234
- return chalk7.yellow;
1539
+ return chalk8.yellow;
1235
1540
  default:
1236
- return chalk7.white;
1541
+ return chalk8.white;
1237
1542
  }
1238
1543
  }
1239
1544
  function activeState(active) {
1240
- return statusColor2(active ? "COMPLETED" : "FAILED")(active ? "active" : "inactive");
1545
+ return statusColor3(active ? "COMPLETED" : "FAILED")(active ? "active" : "inactive");
1241
1546
  }
1242
1547
  function renderScanStatus(job) {
1243
1548
  ui.section("Scan Status:");
@@ -1247,7 +1552,7 @@ function renderScanStatus(job) {
1247
1552
  ["Type", job.jobType]
1248
1553
  ];
1249
1554
  if (job.targetName) pairs.push(["Target", job.targetName]);
1250
- pairs.push(["Status", statusColor2(job.status)(job.status)]);
1555
+ pairs.push(["Status", statusColor3(job.status)(job.status)]);
1251
1556
  if (job.total != null && job.completed != null) {
1252
1557
  pairs.push(["Progress", `${job.completed}/${job.total}`]);
1253
1558
  }
@@ -1290,9 +1595,9 @@ function renderScanList(jobs, format = "pretty") {
1290
1595
  for (const job of jobs) {
1291
1596
  ui.dim(job.uuid);
1292
1597
  console.log(
1293
- ` ${job.name} ${statusColor2(job.status)(job.status)} ${job.jobType}${job.score != null ? ` score: ${job.score}` : ""}`
1598
+ ` ${job.name} ${statusColor3(job.status)(job.status)} ${job.jobType}${job.score != null ? ` score: ${job.score}` : ""}`
1294
1599
  );
1295
- if (job.createdAt) console.log(` ${chalk7.dim(job.createdAt)}`);
1600
+ if (job.createdAt) console.log(` ${chalk8.dim(job.createdAt)}`);
1296
1601
  console.log();
1297
1602
  }
1298
1603
  }
@@ -1307,7 +1612,7 @@ function renderStaticReport(report) {
1307
1612
  for (const s of report.severityBreakdown) {
1308
1613
  const color = severityColor(s.severity);
1309
1614
  console.log(
1310
- ` ${color(s.severity.padEnd(10))} ${chalk7.red(`${s.successful} bypassed`)} ${chalk7.green(`${s.failed} blocked`)}`
1615
+ ` ${color(s.severity.padEnd(10))} ${chalk8.red(`${s.successful} bypassed`)} ${chalk8.green(`${s.failed} blocked`)}`
1311
1616
  );
1312
1617
  }
1313
1618
  }
@@ -1385,10 +1690,10 @@ function renderAttackList(attacks, options) {
1385
1690
  }
1386
1691
  ui.section("Attacks:");
1387
1692
  for (const a of attacks) {
1388
- const sev = a.severity ? severityColor(a.severity)(a.severity.padEnd(10)) : chalk7.dim("N/A".padEnd(10));
1389
- const result = a.successful ? chalk7.red("BYPASSED") : chalk7.green("BLOCKED");
1693
+ const sev = a.severity ? severityColor(a.severity)(a.severity.padEnd(10)) : chalk8.dim("N/A".padEnd(10));
1694
+ const result = a.successful ? chalk8.red("BYPASSED") : chalk8.green("BLOCKED");
1390
1695
  const label = a.subCategoryDisplayName ?? a.subCategory ?? "\u2014";
1391
- console.log(` ${sev} ${result} ${label}${a.category ? chalk7.dim(` [${a.category}]`) : ""}`);
1696
+ console.log(` ${sev} ${result} ${label}${a.category ? chalk8.dim(` [${a.category}]`) : ""}`);
1392
1697
  }
1393
1698
  if (options?.footnote) ui.dim(options.footnote);
1394
1699
  console.log();
@@ -1408,11 +1713,11 @@ function renderCustomAttackList(attacks) {
1408
1713
  }
1409
1714
  ui.section("Custom Attacks:");
1410
1715
  for (const a of attacks) {
1411
- const result = a.threat ? chalk7.red("THREAT") : chalk7.green("SAFE");
1716
+ const result = a.threat ? chalk8.red("THREAT") : chalk8.green("SAFE");
1412
1717
  const prompt = a.promptText.length > 80 ? `${a.promptText.substring(0, 77)}...` : a.promptText;
1413
- const asrStr = a.asr != null ? chalk7.dim(` ASR: ${a.asr.toFixed(1)}%`) : "";
1718
+ const asrStr = a.asr != null ? chalk8.dim(` ASR: ${a.asr.toFixed(1)}%`) : "";
1414
1719
  console.log(` ${result}${asrStr} ${prompt}`);
1415
- if (a.goal) console.log(` ${chalk7.dim(a.goal)}`);
1720
+ if (a.goal) console.log(` ${chalk8.dim(a.goal)}`);
1416
1721
  }
1417
1722
  console.log();
1418
1723
  }
@@ -1459,11 +1764,11 @@ function renderCategories(categories) {
1459
1764
  ui.section("Attack Categories:");
1460
1765
  for (const c of categories) {
1461
1766
  console.log(
1462
- ` ${chalk7.bold(c.displayName)} ${chalk7.cyan(`(${c.id})`)}${c.description ? chalk7.dim(` \u2014 ${c.description}`) : ""}`
1767
+ ` ${chalk8.bold(c.displayName)} ${chalk8.cyan(`(${c.id})`)}${c.description ? chalk8.dim(` \u2014 ${c.description}`) : ""}`
1463
1768
  );
1464
1769
  for (const sc of c.subCategories) {
1465
1770
  console.log(
1466
- ` ${chalk7.dim("\u2022")} ${sc.displayName} ${chalk7.cyan(`(${sc.id})`)}${sc.description ? chalk7.dim(` \u2014 ${sc.description}`) : ""}`
1771
+ ` ${chalk8.dim("\u2022")} ${sc.displayName} ${chalk8.cyan(`(${sc.id})`)}${sc.description ? chalk8.dim(` \u2014 ${sc.description}`) : ""}`
1467
1772
  );
1468
1773
  }
1469
1774
  console.log();
@@ -1520,7 +1825,7 @@ function renderTargetDetail(target, format = "pretty") {
1520
1825
  if (format === "json") {
1521
1826
  console.log(JSON.stringify(target, null, 2));
1522
1827
  } else if (format === "yaml") {
1523
- console.log(yamlDump3(target));
1828
+ console.log(yamlDump4(target));
1524
1829
  }
1525
1830
  return;
1526
1831
  }
@@ -1552,7 +1857,7 @@ function renderPromptSetDetail(ps, format = "pretty", info) {
1552
1857
  if (format === "json") {
1553
1858
  console.log(JSON.stringify(payload, null, 2));
1554
1859
  } else if (format === "yaml") {
1555
- console.log(yamlDump3(payload));
1860
+ console.log(yamlDump4(payload));
1556
1861
  }
1557
1862
  return;
1558
1863
  }
@@ -1589,7 +1894,7 @@ function renderPromptList(prompts, format = "pretty") {
1589
1894
  if (format === "json") {
1590
1895
  console.log(JSON.stringify(prompts, null, 2));
1591
1896
  } else if (format === "yaml") {
1592
- console.log(yamlDump3(prompts));
1897
+ console.log(yamlDump4(prompts));
1593
1898
  }
1594
1899
  return;
1595
1900
  }
@@ -1599,11 +1904,11 @@ function renderPromptList(prompts, format = "pretty") {
1599
1904
  }
1600
1905
  ui.section("Prompts:");
1601
1906
  for (const p of prompts) {
1602
- const status = p.active ? chalk7.green("active") : chalk7.dim("inactive");
1907
+ const status = p.active ? chalk8.green("active") : chalk8.dim("inactive");
1603
1908
  const text = p.prompt.length > 80 ? `${p.prompt.substring(0, 77)}...` : p.prompt;
1604
- console.log(` ${chalk7.dim(p.uuid)} ${status}`);
1909
+ console.log(` ${chalk8.dim(p.uuid)} ${status}`);
1605
1910
  console.log(` ${text}`);
1606
- if (p.goal) console.log(` ${chalk7.dim(`Goal: ${p.goal}`)}`);
1911
+ if (p.goal) console.log(` ${chalk8.dim(`Goal: ${p.goal}`)}`);
1607
1912
  }
1608
1913
  console.log();
1609
1914
  }
@@ -1612,7 +1917,7 @@ function renderPromptDetail(p, format = "pretty") {
1612
1917
  if (format === "json") {
1613
1918
  console.log(JSON.stringify(p, null, 2));
1614
1919
  } else if (format === "yaml") {
1615
- console.log(yamlDump3(p));
1920
+ console.log(yamlDump4(p));
1616
1921
  }
1617
1922
  return;
1618
1923
  }
@@ -1620,7 +1925,7 @@ function renderPromptDetail(p, format = "pretty") {
1620
1925
  const pairs = [
1621
1926
  ["UUID", p.uuid],
1622
1927
  ["Set UUID", p.promptSetId],
1623
- ["Status", p.active ? chalk7.green("active") : chalk7.dim("inactive")],
1928
+ ["Status", p.active ? chalk8.green("active") : chalk8.dim("inactive")],
1624
1929
  ["Prompt", p.prompt]
1625
1930
  ];
1626
1931
  if (p.goal) pairs.push(["Goal", p.goal]);
@@ -1632,7 +1937,7 @@ function renderPropertyNames(names, format = "pretty") {
1632
1937
  if (format === "json") {
1633
1938
  console.log(JSON.stringify(names, null, 2));
1634
1939
  } else if (format === "yaml") {
1635
- console.log(yamlDump3(names));
1940
+ console.log(yamlDump4(names));
1636
1941
  } else {
1637
1942
  const rows = names.map((n) => ({ name: n }));
1638
1943
  console.log(formatOutput(rows, [{ key: "name", label: "Name" }], format));
@@ -1652,7 +1957,7 @@ function renderPropertyNames(names, format = "pretty") {
1652
1957
  function renderAuthValidation(result) {
1653
1958
  ui.section("Auth Validation:");
1654
1959
  const pairs = [
1655
- ["Validated", result.validated ? chalk7.green("yes") : chalk7.red("no")]
1960
+ ["Validated", result.validated ? chalk8.green("yes") : chalk8.red("no")]
1656
1961
  ];
1657
1962
  if (result.tokenPreview) pairs.push(["Token", result.tokenPreview]);
1658
1963
  if (result.expiresIn != null) pairs.push(["Expires In", `${result.expiresIn}s`]);
@@ -1670,7 +1975,7 @@ function renderTargetTemplates(templates) {
1670
1975
  function renderEulaStatus(status) {
1671
1976
  ui.section("EULA Status:");
1672
1977
  const pairs = [
1673
- ["Accepted", status.isAccepted ? chalk7.green("yes") : chalk7.red("no")]
1978
+ ["Accepted", status.isAccepted ? chalk8.green("yes") : chalk8.red("no")]
1674
1979
  ];
1675
1980
  if (status.acceptedAt) pairs.push(["Accepted At", status.acceptedAt]);
1676
1981
  if (status.acceptedByUserId) pairs.push(["Accepted By", status.acceptedByUserId]);
@@ -1687,7 +1992,7 @@ function renderPropertyValues(payload, format = "pretty") {
1687
1992
  if (format === "json") {
1688
1993
  console.log(JSON.stringify(payload, null, 2));
1689
1994
  } else if (format === "yaml") {
1690
- console.log(yamlDump3(payload));
1995
+ console.log(yamlDump4(payload));
1691
1996
  }
1692
1997
  return;
1693
1998
  }
@@ -1708,7 +2013,7 @@ function renderInstanceResponse(resp) {
1708
2013
  if (resp.tenantId) pairs.push(["Tenant ID", resp.tenantId]);
1709
2014
  if (resp.appId) pairs.push(["App ID", resp.appId]);
1710
2015
  if (resp.isSuccess != null) {
1711
- pairs.push(["Success", resp.isSuccess ? chalk7.green("yes") : chalk7.red("no")]);
2016
+ pairs.push(["Success", resp.isSuccess ? chalk8.green("yes") : chalk8.red("no")]);
1712
2017
  }
1713
2018
  ui.keyValue(pairs);
1714
2019
  console.log();
@@ -1718,7 +2023,7 @@ function renderInstanceDetail(inst, format = "pretty") {
1718
2023
  if (format === "json") {
1719
2024
  console.log(JSON.stringify(inst, null, 2));
1720
2025
  } else if (format === "yaml") {
1721
- console.log(yamlDump3(inst));
2026
+ console.log(yamlDump4(inst));
1722
2027
  }
1723
2028
  return;
1724
2029
  }
@@ -1736,7 +2041,7 @@ function renderRegistryCredentials(creds, format = "pretty") {
1736
2041
  if (format === "json") {
1737
2042
  console.log(JSON.stringify(creds, null, 2));
1738
2043
  } else if (format === "yaml") {
1739
- console.log(yamlDump3(creds));
2044
+ console.log(yamlDump4(creds));
1740
2045
  }
1741
2046
  return;
1742
2047
  }
@@ -1750,13 +2055,13 @@ function renderRegistryCredentials(creds, format = "pretty") {
1750
2055
  function channelStatusColor(status) {
1751
2056
  switch (status.toUpperCase()) {
1752
2057
  case "ONLINE":
1753
- return chalk7.green;
2058
+ return chalk8.green;
1754
2059
  case "DRAFT":
1755
- return chalk7.yellow;
2060
+ return chalk8.yellow;
1756
2061
  case "OFFLINE":
1757
- return chalk7.red;
2062
+ return chalk8.red;
1758
2063
  default:
1759
- return chalk7.white;
2064
+ return chalk8.white;
1760
2065
  }
1761
2066
  }
1762
2067
  function renderChannelList(channels, format = "pretty") {
@@ -1790,7 +2095,7 @@ function renderChannelList(channels, format = "pretty") {
1790
2095
  ui.section("Network Broker Channels:");
1791
2096
  for (const c of channels) {
1792
2097
  if (c.uuid) ui.dim(c.uuid);
1793
- const status = c.status ? channelStatusColor(c.status)(c.status) : chalk7.dim("unknown");
2098
+ const status = c.status ? channelStatusColor(c.status)(c.status) : chalk8.dim("unknown");
1794
2099
  const clients = c.connectedClientsCount != null ? ` clients: ${c.connectedClientsCount}` : "";
1795
2100
  console.log(` ${c.name ?? "(unnamed)"} ${status}${clients}`);
1796
2101
  console.log();
@@ -1798,7 +2103,7 @@ function renderChannelList(channels, format = "pretty") {
1798
2103
  }
1799
2104
  function renderChannelDetail(channel, format = "pretty") {
1800
2105
  if (format !== "pretty") {
1801
- console.log(format === "json" ? JSON.stringify(channel, null, 2) : yamlDump3(channel));
2106
+ console.log(format === "json" ? JSON.stringify(channel, null, 2) : yamlDump4(channel));
1802
2107
  return;
1803
2108
  }
1804
2109
  ui.section("Channel Detail:");
@@ -1824,7 +2129,7 @@ function renderChannelDetail(channel, format = "pretty") {
1824
2129
  }
1825
2130
  function renderChannelStats(stats, format = "pretty") {
1826
2131
  if (format !== "pretty") {
1827
- console.log(format === "json" ? JSON.stringify(stats, null, 2) : yamlDump3(stats));
2132
+ console.log(format === "json" ? JSON.stringify(stats, null, 2) : yamlDump4(stats));
1828
2133
  return;
1829
2134
  }
1830
2135
  ui.section("Network Broker Stats:");
@@ -1842,7 +2147,7 @@ function renderChannelStats(stats, format = "pretty") {
1842
2147
  function renderLanguages(data, format = "pretty") {
1843
2148
  if (format !== "pretty") {
1844
2149
  if (format === "json" || format === "yaml") {
1845
- console.log(format === "json" ? JSON.stringify(data, null, 2) : yamlDump3(data));
2150
+ console.log(format === "json" ? JSON.stringify(data, null, 2) : yamlDump4(data));
1846
2151
  return;
1847
2152
  }
1848
2153
  console.log(
@@ -1868,7 +2173,7 @@ function renderLanguages(data, format = "pretty") {
1868
2173
  }
1869
2174
  ui.section("Languages:");
1870
2175
  for (const l of data.languages) {
1871
- console.log(` ${chalk7.dim(l.code)} ${l.name}`);
2176
+ console.log(` ${chalk8.dim(l.code)} ${l.name}`);
1872
2177
  }
1873
2178
  console.log();
1874
2179
  }
@@ -1902,45 +2207,140 @@ function renderErrorLogs(logs, format = "pretty") {
1902
2207
  }
1903
2208
  ui.section("Target-Profile Error Logs:");
1904
2209
  for (const l of logs) {
1905
- const type = l.errorType ? chalk7.red(l.errorType) : chalk7.dim("error");
2210
+ const type = l.errorType ? chalk8.red(l.errorType) : chalk8.dim("error");
1906
2211
  console.log(
1907
- ` ${chalk7.dim(l.createdAt)} ${type}${l.errorSource ? ` (${l.errorSource})` : ""}`
2212
+ ` ${chalk8.dim(l.createdAt)} ${type}${l.errorSource ? ` (${l.errorSource})` : ""}`
1908
2213
  );
1909
2214
  if (l.errorMessage) console.log(` ${l.errorMessage}`);
1910
- if (l.jobId) console.log(` ${chalk7.dim(`job: ${l.jobId}`)}`);
2215
+ if (l.jobId) console.log(` ${chalk8.dim(`job: ${l.jobId}`)}`);
1911
2216
  console.log();
1912
2217
  }
1913
2218
  }
2219
+ function renderAdapterList(adapters, format = "pretty", totalItems) {
2220
+ if (adapters.length === 0) {
2221
+ ui.emptyList("adapters");
2222
+ return;
2223
+ }
2224
+ if (format !== "pretty") {
2225
+ const rows = adapters.map((a) => ({
2226
+ uuid: a.uuid,
2227
+ name: a.name,
2228
+ status: a.status,
2229
+ targets: a.targetCount ?? "",
2230
+ updated: a.updatedAt ?? ""
2231
+ }));
2232
+ console.log(
2233
+ formatOutput(
2234
+ rows,
2235
+ [
2236
+ { key: "uuid", label: "UUID" },
2237
+ { key: "name", label: "Name" },
2238
+ { key: "status", label: "Status" },
2239
+ { key: "targets", label: "Targets" },
2240
+ { key: "updated", label: "Updated" }
2241
+ ],
2242
+ format
2243
+ )
2244
+ );
2245
+ return;
2246
+ }
2247
+ ui.section("Custom Target Adapters:");
2248
+ for (const a of adapters) {
2249
+ ui.dim(a.uuid);
2250
+ const status = a.status === "ACTIVE" ? chalk8.green(a.status) : chalk8.yellow(a.status);
2251
+ const targets = a.targetCount != null ? ` targets: ${a.targetCount}` : "";
2252
+ console.log(` ${a.name} ${status}${targets}`);
2253
+ console.log();
2254
+ }
2255
+ if (totalItems !== void 0) ui.dim(`${totalItems} total`);
2256
+ }
2257
+ function renderAdapterDetail(adapter, format = "pretty") {
2258
+ if (format !== "pretty") {
2259
+ console.log(format === "json" ? JSON.stringify(adapter, null, 2) : yamlDump4(adapter));
2260
+ return;
2261
+ }
2262
+ ui.section("Adapter Detail:");
2263
+ const pairs = [
2264
+ ["UUID", adapter.uuid],
2265
+ ["Name", adapter.name],
2266
+ [
2267
+ "Status",
2268
+ adapter.status === "ACTIVE" ? chalk8.green(adapter.status) : chalk8.yellow(adapter.status)
2269
+ ],
2270
+ ["Script", `${adapter.scriptB64.length} base64 chars`]
2271
+ ];
2272
+ if (adapter.description != null) pairs.push(["Description", adapter.description]);
2273
+ if (adapter.networkBrokerChannelUuid != null)
2274
+ pairs.push(["Broker Channel", adapter.networkBrokerChannelUuid]);
2275
+ if (adapter.targetCount != null) pairs.push(["Targets", adapter.targetCount]);
2276
+ if (adapter.createdAt != null) pairs.push(["Created", adapter.createdAt]);
2277
+ if (adapter.updatedAt != null) pairs.push(["Updated", adapter.updatedAt]);
2278
+ ui.keyValue(pairs);
2279
+ if (adapter.variables.length > 0) {
2280
+ ui.section("Variables:");
2281
+ ui.keyValue(
2282
+ adapter.variables.map((v) => [
2283
+ `${v.key} (${v.type})`,
2284
+ v.isRedacted ? chalk8.dim("(redacted)") : v.value ?? ""
2285
+ ])
2286
+ );
2287
+ }
2288
+ console.log();
2289
+ }
2290
+ function renderAdapterValidation(result, format = "pretty") {
2291
+ if (format !== "pretty") {
2292
+ console.log(format === "json" ? JSON.stringify(result, null, 2) : yamlDump4(result));
2293
+ return;
2294
+ }
2295
+ if (result.validated) {
2296
+ ui.success("Adapter script validated");
2297
+ } else {
2298
+ ui.error("Adapter script validation FAILED");
2299
+ }
2300
+ if (result.stdout) {
2301
+ ui.section("stdout:");
2302
+ console.log(result.stdout);
2303
+ }
2304
+ if (result.stderr) {
2305
+ ui.section("stderr:");
2306
+ console.log(chalk8.red(result.stderr));
2307
+ }
2308
+ if (result.traceback) {
2309
+ ui.section("traceback:");
2310
+ console.log(chalk8.red(result.traceback));
2311
+ }
2312
+ console.log();
2313
+ }
1914
2314
 
1915
2315
  // src/cli/renderer/runtime.ts
1916
- import chalk8 from "chalk";
2316
+ import chalk9 from "chalk";
1917
2317
  function renderScanProgress(job) {
1918
2318
  if (job.total != null && job.completed != null && job.total > 0) {
1919
2319
  const pct = Math.round(job.completed / job.total * 100);
1920
2320
  const bar = "\u2588".repeat(Math.round(pct / 5)) + "\u2591".repeat(20 - Math.round(pct / 5));
1921
2321
  process.stdout.write(
1922
- `\r ${statusColor3(job.status)(job.status)} ${bar} ${pct}% (${job.completed}/${job.total})`
2322
+ `\r ${statusColor4(job.status)(job.status)} ${bar} ${pct}% (${job.completed}/${job.total})`
1923
2323
  );
1924
2324
  } else {
1925
- process.stdout.write(`\r ${statusColor3(job.status)(job.status)}...`);
2325
+ process.stdout.write(`\r ${statusColor4(job.status)(job.status)}...`);
1926
2326
  }
1927
2327
  }
1928
- function statusColor3(status) {
2328
+ function statusColor4(status) {
1929
2329
  switch (status) {
1930
2330
  case "COMPLETED":
1931
- return chalk8.green;
2331
+ return chalk9.green;
1932
2332
  case "RUNNING":
1933
- return chalk8.blue;
2333
+ return chalk9.blue;
1934
2334
  case "QUEUED":
1935
2335
  case "INIT":
1936
- return chalk8.yellow;
2336
+ return chalk9.yellow;
1937
2337
  case "FAILED":
1938
2338
  case "ABORTED":
1939
- return chalk8.red;
2339
+ return chalk9.red;
1940
2340
  case "PARTIALLY_COMPLETE":
1941
- return chalk8.yellow;
2341
+ return chalk9.yellow;
1942
2342
  default:
1943
- return chalk8.white;
2343
+ return chalk9.white;
1944
2344
  }
1945
2345
  }
1946
2346
  function renderRuntimeConfigHeader() {
@@ -1970,8 +2370,8 @@ function renderProfileList(profiles, format = "pretty") {
1970
2370
  ui.section("Security Profiles:");
1971
2371
  for (const p of profiles) {
1972
2372
  ui.dim(p.profileId);
1973
- const status = p.active ? chalk8.green("active") : chalk8.yellow("inactive");
1974
- const rev = p.revision != null ? chalk8.dim(` rev:${p.revision}`) : "";
2373
+ const status = p.active ? chalk9.green("active") : chalk9.yellow("inactive");
2374
+ const rev = p.revision != null ? chalk9.dim(` rev:${p.revision}`) : "";
1975
2375
  console.log(` ${p.profileName} ${status}${rev}`);
1976
2376
  }
1977
2377
  console.log();
@@ -1981,7 +2381,7 @@ function renderProfileDetail(profile) {
1981
2381
  const pairs = [
1982
2382
  ["ID", profile.profileId],
1983
2383
  ["Name", profile.profileName],
1984
- ["Status", profile.active ? chalk8.green("active") : chalk8.yellow("inactive")]
2384
+ ["Status", profile.active ? chalk9.green("active") : chalk9.yellow("inactive")]
1985
2385
  ];
1986
2386
  if (profile.revision != null) pairs.push(["Revision", profile.revision]);
1987
2387
  if (profile.createdBy) pairs.push(["Created", profile.createdBy]);
@@ -2085,8 +2485,8 @@ function renderTopicList(topics, format = "pretty") {
2085
2485
  ui.section("Custom Topics:");
2086
2486
  for (const t of topics) {
2087
2487
  ui.dim(String(t.topic_id));
2088
- const rev = t.revision != null ? chalk8.dim(` rev:${t.revision}`) : "";
2089
- const desc = t.description ? chalk8.dim(` \u2014 ${t.description.slice(0, 80)}`) : "";
2488
+ const rev = t.revision != null ? chalk9.dim(` rev:${t.revision}`) : "";
2489
+ const desc = t.description ? chalk9.dim(` \u2014 ${t.description.slice(0, 80)}`) : "";
2090
2490
  console.log(` ${t.topic_name}${rev}${desc}`);
2091
2491
  }
2092
2492
  console.log();
@@ -2144,8 +2544,8 @@ function renderApiKeyList(keys, format = "pretty") {
2144
2544
  ui.section("API Keys:");
2145
2545
  for (const k of keys) {
2146
2546
  ui.dim(k.id);
2147
- const last8 = k.last8 ? chalk8.dim(` key: \u2026${k.last8}`) : "";
2148
- const expires = k.expiresAt ? chalk8.dim(` expires: ${k.expiresAt}`) : "";
2547
+ const last8 = k.last8 ? chalk9.dim(` key: \u2026${k.last8}`) : "";
2548
+ const expires = k.expiresAt ? chalk9.dim(` expires: ${k.expiresAt}`) : "";
2149
2549
  console.log(` ${k.name}${last8}${expires}`);
2150
2550
  }
2151
2551
  console.log();
@@ -2190,7 +2590,7 @@ function renderCustomerAppList(apps, format = "pretty") {
2190
2590
  ui.section("Customer Apps:");
2191
2591
  for (const a of apps) {
2192
2592
  if (a.id) ui.dim(a.id);
2193
- const desc = a.description ? chalk8.dim(` \u2014 ${a.description.slice(0, 80)}`) : "";
2593
+ const desc = a.description ? chalk9.dim(` \u2014 ${a.description.slice(0, 80)}`) : "";
2194
2594
  console.log(` ${a.name}${desc}`);
2195
2595
  }
2196
2596
  console.log();
@@ -2314,9 +2714,9 @@ function renderDeploymentProfileList(profiles, format = "pretty") {
2314
2714
  const name = p.raw.dp_name ?? p.raw.profile_name ?? p.raw.name ?? "unknown";
2315
2715
  const status = p.raw.status;
2316
2716
  const authCode = p.raw.auth_code;
2317
- const statusColor4 = status === "active" ? chalk8.green : chalk8.dim;
2717
+ const statusColor5 = status === "active" ? chalk9.green : chalk9.dim;
2318
2718
  console.log(
2319
- ` ${name}${status ? ` ${statusColor4(status)}` : ""}${authCode ? ` ${chalk8.dim(authCode)}` : ""}`
2719
+ ` ${name}${status ? ` ${statusColor5(status)}` : ""}${authCode ? ` ${chalk9.dim(authCode)}` : ""}`
2320
2720
  );
2321
2721
  }
2322
2722
  console.log();
@@ -2356,10 +2756,10 @@ function renderScanLogList(results, pageToken, format = "pretty") {
2356
2756
  const profile = r.profile_name;
2357
2757
  const ts2 = r.received_ts ?? r.timestamp;
2358
2758
  const scanId = r.scan_id;
2359
- const actionColor = action === "block" ? chalk8.red : chalk8.green;
2759
+ const actionColor = action === "block" ? chalk9.red : chalk9.green;
2360
2760
  if (scanId) ui.dim(scanId);
2361
2761
  console.log(
2362
- ` ${ts2 ? chalk8.dim(ts2) : ""} ${action ? actionColor(action) : ""} ${profile ? `[${profile}]` : ""} ${app ?? ""}`
2762
+ ` ${ts2 ? chalk9.dim(ts2) : ""} ${action ? actionColor(action) : ""} ${profile ? `[${profile}]` : ""} ${app ?? ""}`
2363
2763
  );
2364
2764
  }
2365
2765
  if (pageToken) {
@@ -2369,6 +2769,242 @@ function renderScanLogList(results, pageToken, format = "pretty") {
2369
2769
  console.log();
2370
2770
  }
2371
2771
 
2772
+ // src/cli/confirm.ts
2773
+ async function confirmOrAbort(message, force, options = {}) {
2774
+ if (force) return;
2775
+ const interactive = options.isTTY ?? process.stdout.isTTY === true;
2776
+ if (!interactive) {
2777
+ usageError(
2778
+ `refusing to ${options.action ?? "proceed"} without --force in non-interactive mode`
2779
+ );
2780
+ }
2781
+ const prompt = options.promptFn ?? (await import("@inquirer/prompts")).confirm;
2782
+ const confirmed = await prompt({ message, default: false });
2783
+ if (!confirmed) {
2784
+ ui.info("Aborted");
2785
+ process.exit(0);
2786
+ }
2787
+ }
2788
+
2789
+ // src/cli/examples.ts
2790
+ function examples(...lines) {
2791
+ return `
2792
+ Examples:
2793
+ ${lines.map((l) => ` $ ${l}`).join("\n")}
2794
+ `;
2795
+ }
2796
+
2797
+ // src/cli/commands/aigateway.ts
2798
+ async function createService() {
2799
+ const config = await loadConfig();
2800
+ return new SdkAiGatewayService(aiGatewayClientOptions(config));
2801
+ }
2802
+ function failWithGrantHint(err) {
2803
+ const hint = aiGatewayGrantHint(err);
2804
+ if (hint) ui.warn(`403: ${hint}`);
2805
+ fail(err);
2806
+ }
2807
+ function parsePlane(value) {
2808
+ if (value === void 0) return void 0;
2809
+ if (value !== "data" && value !== "admin") {
2810
+ usageError(`Invalid --plane '${value}'. Valid planes: data, admin`);
2811
+ }
2812
+ return value;
2813
+ }
2814
+ function parseStatus(value) {
2815
+ if (value === void 0) return void 0;
2816
+ if (value !== "active" && value !== "archived") {
2817
+ usageError(`Invalid --status '${value}'. Valid statuses: active, archived`);
2818
+ }
2819
+ return value;
2820
+ }
2821
+ function parseJsonFlag(raw, flag) {
2822
+ if (raw === void 0) return void 0;
2823
+ try {
2824
+ return JSON.parse(raw);
2825
+ } catch {
2826
+ throw new Error(`${flag} must be valid JSON`);
2827
+ }
2828
+ }
2829
+ function buildWorkspaceWriteRequest(opts) {
2830
+ const out = {};
2831
+ for (const key of ["name", "description", "icon"]) {
2832
+ if (opts[key] !== void 0) out[key] = opts[key];
2833
+ }
2834
+ const defaults = parseJsonFlag(opts.defaults, "--defaults");
2835
+ const metadata = parseJsonFlag(opts.metadata, "--metadata");
2836
+ if (defaults !== void 0 || metadata !== void 0) {
2837
+ out.defaults = {
2838
+ ...typeof defaults === "object" && defaults !== null ? defaults : {},
2839
+ ...metadata !== void 0 ? { metadata } : {}
2840
+ };
2841
+ }
2842
+ if (opts.users !== void 0) {
2843
+ out.users = opts.users.split(",").map((u) => u.trim()).filter(Boolean);
2844
+ }
2845
+ const usage = parseJsonFlag(opts.usageLimits, "--usage-limits");
2846
+ if (usage !== void 0) out.usageLimits = usage;
2847
+ const rate = parseJsonFlag(opts.rateLimits, "--rate-limits");
2848
+ if (rate !== void 0) out.rateLimits = rate;
2849
+ return out;
2850
+ }
2851
+ function scopeNameLooksUnrelated(name, scopeName) {
2852
+ const nameToken = name.toLowerCase().replace(/[^a-z0-9]/g, "");
2853
+ if (nameToken.length < 4) return false;
2854
+ return !scopeName.toLowerCase().replace(/[^a-z0-9]/g, "").includes(nameToken);
2855
+ }
2856
+ function registerAiGatewayCommand(program) {
2857
+ const aigateway = program.command("aigateway").description("AI Gateway operations");
2858
+ const workspace = aigateway.command("workspace").description("Manage AI Gateway workspaces");
2859
+ workspace.command("list").description("List workspaces (default: active workspaces you are scoped to)").option("--plane <plane>", "Plane to read from: data (scoped) or admin (whole tenant)").option("--status <status>", "Filter by lifecycle state: active or archived").option("--all", "Merge active + archived admin-plane reads (whole tenant, both states)").option("--output <format>", "Output format: pretty, table, csv, json, yaml", "pretty").addHelpText(
2860
+ "after",
2861
+ examples(
2862
+ "airs aigateway workspace list",
2863
+ "airs aigateway workspace list --plane admin",
2864
+ "airs aigateway workspace list --plane admin --status archived",
2865
+ "airs aigateway workspace list --all --output json"
2866
+ )
2867
+ ).action(async (opts) => {
2868
+ try {
2869
+ const fmt = opts.output;
2870
+ if (fmt === "pretty") renderAiGatewayHeader();
2871
+ const plane = parsePlane(opts.plane);
2872
+ const status = parseStatus(opts.status);
2873
+ if (opts.all && (plane !== void 0 || status !== void 0)) {
2874
+ usageError("--all already merges admin-plane active + archived; drop --plane/--status");
2875
+ }
2876
+ const service = await createService();
2877
+ const workspaces = opts.all ? await service.listAllWorkspaces() : await service.listWorkspaces(
2878
+ plane !== void 0 || status !== void 0 ? { plane, status } : void 0
2879
+ );
2880
+ renderWorkspaceList(workspaces, fmt);
2881
+ if (fmt === "pretty" && !opts.all && plane !== "admin") {
2882
+ ui.status(
2883
+ "Data-plane list shows only active workspaces you are scoped to \u2014 use --plane admin or --all for the whole tenant."
2884
+ );
2885
+ }
2886
+ } catch (err) {
2887
+ failWithGrantHint(err);
2888
+ }
2889
+ });
2890
+ workspace.command("get <ref>").description("Get one workspace by UUID or slug (includes settings blocks)").option("--plane <plane>", "Plane to read from: data (scoped) or admin (whole tenant)").option("--output <format>", "Output format: pretty, json, yaml", "pretty").addHelpText(
2891
+ "after",
2892
+ examples(
2893
+ "airs aigateway workspace get ws-main-a-349e0e",
2894
+ "airs aigateway workspace get 16f7e90d-382a-4e78-b577-1b01eb5f8297 --plane admin --output json"
2895
+ )
2896
+ ).action(async (ref, opts) => {
2897
+ try {
2898
+ const fmt = opts.output;
2899
+ if (fmt === "pretty") renderAiGatewayHeader();
2900
+ const plane = parsePlane(opts.plane);
2901
+ const service = await createService();
2902
+ const workspace2 = await service.getWorkspace(
2903
+ ref,
2904
+ plane !== void 0 ? { plane } : void 0
2905
+ );
2906
+ renderWorkspaceDetail(workspace2, fmt);
2907
+ } catch (err) {
2908
+ failWithGrantHint(err);
2909
+ }
2910
+ });
2911
+ workspace.command("create").description("Create a workspace (admin plane)").requiredOption("--name <name>", "Display name").requiredOption(
2912
+ "--scope-name <scope>",
2913
+ "SCM role scope granting data-plane access (e.g. ws_production_bx7qw0) \u2014 not derived from --name"
2914
+ ).option("--description <text>", "Workspace description").option("--icon <icon>", "Workspace icon").option("--metadata <json>", "Sugar for defaults.metadata (flat string map)").option("--defaults <json>", "Workspace defaults object").option("--users <ids>", "Comma-separated user ids to seed the workspace with").option("--usage-limits <json>", "Usage-limit policies \u2014 a JSON ARRAY of policy objects").option("--rate-limits <json>", "Rate-limit policies \u2014 a JSON ARRAY of policy objects").option("--output <format>", "Output format: pretty, json, yaml", "pretty").addHelpText(
2915
+ "after",
2916
+ examples(
2917
+ "airs aigateway workspace create --name Production --scope-name ws_production_bx7qw0",
2918
+ `airs aigateway workspace create --name Production --scope-name ws_production_bx7qw0 --metadata '{"env":"production"}' --rate-limits '[{"type":"requests","unit":"rpm","value":100}]'`
2919
+ )
2920
+ ).action(async (opts) => {
2921
+ try {
2922
+ const fmt = opts.output;
2923
+ if (fmt === "pretty") renderAiGatewayHeader();
2924
+ if (scopeNameLooksUnrelated(opts.name, opts.scopeName)) {
2925
+ ui.warn(
2926
+ `--scope-name '${opts.scopeName}' shares no token with --name '${opts.name}'. A workspace created with a scope nobody holds will not appear in data-plane lists.`
2927
+ );
2928
+ }
2929
+ const service = await createService();
2930
+ const workspace2 = await service.createWorkspace({
2931
+ ...buildWorkspaceWriteRequest(opts),
2932
+ name: opts.name,
2933
+ scopeName: opts.scopeName
2934
+ });
2935
+ ui.success(`Workspace created: ${workspace2.id}`);
2936
+ renderWorkspaceDetail(workspace2, fmt);
2937
+ } catch (err) {
2938
+ failWithGrantHint(err);
2939
+ }
2940
+ });
2941
+ workspace.command("update <ref>").description("Update a workspace (admin plane, partial patch)").option("--name <name>", "New display name").option("--description <text>", "New description").option("--icon <icon>", "New icon").option("--metadata <json>", "Sugar for defaults.metadata (flat string map)").option("--defaults <json>", "Workspace defaults object").option("--usage-limits <json>", "Usage-limit policies \u2014 a JSON ARRAY of policy objects").option("--rate-limits <json>", "Rate-limit policies \u2014 a JSON ARRAY of policy objects").option("--output <format>", "Output format: pretty, json, yaml", "pretty").addHelpText(
2942
+ "after",
2943
+ examples(
2944
+ `airs aigateway workspace update ws-produc-985697 --description 'Production workloads, us-east'`
2945
+ )
2946
+ ).action(async (ref, opts) => {
2947
+ try {
2948
+ const fmt = opts.output;
2949
+ if (fmt === "pretty") renderAiGatewayHeader();
2950
+ const request = buildWorkspaceWriteRequest(opts);
2951
+ if (Object.keys(request).length === 0) {
2952
+ usageError(
2953
+ "Specify at least one of --name --description --icon --metadata --defaults --usage-limits --rate-limits"
2954
+ );
2955
+ }
2956
+ const service = await createService();
2957
+ const workspace2 = await service.updateWorkspace(ref, request);
2958
+ ui.success(`Workspace updated: ${workspace2.id}`);
2959
+ renderWorkspaceDetail(workspace2, fmt);
2960
+ } catch (err) {
2961
+ failWithGrantHint(err);
2962
+ }
2963
+ });
2964
+ workspace.command("delete <ref>").description("Archive a workspace (soft delete \u2014 there is no hard delete)").option("--force", "Skip confirmation prompt").addHelpText("after", examples("airs aigateway workspace delete ws-produc-985697 --force")).action(async (ref, opts) => {
2965
+ try {
2966
+ renderAiGatewayHeader();
2967
+ await confirmOrAbort(
2968
+ `Archive workspace ${ref}? (soft delete \u2014 the row remains under --status archived)`,
2969
+ Boolean(opts.force),
2970
+ { action: `archive workspace ${ref}` }
2971
+ );
2972
+ const service = await createService();
2973
+ await service.deleteWorkspace(ref);
2974
+ ui.success(`Workspace archived: ${ref}`);
2975
+ ui.status(
2976
+ "This is a soft delete \u2014 the workspace remains visible via `workspace list --plane admin --status archived`. A `get` on it now answers 404; that is expected."
2977
+ );
2978
+ } catch (err) {
2979
+ failWithGrantHint(err);
2980
+ }
2981
+ });
2982
+ const telemetry = aigateway.command("telemetry").description("AI Gateway runtime telemetry (data plane)");
2983
+ telemetry.command("cost").description(
2984
+ "Total and per-day spend for a workspace (API reports cents; pretty output shows dollars)"
2985
+ ).requiredOption("--workspace <slug>", "Workspace slug (not UUID), e.g. ws-main-a-349e0e").option("--days <n>", "Rolling window in days, counted back from now", "7").option("--output <format>", "Output format: pretty, json, yaml", "pretty").addHelpText(
2986
+ "after",
2987
+ examples(
2988
+ "airs aigateway telemetry cost --workspace ws-main-a-349e0e",
2989
+ "airs aigateway telemetry cost --workspace ws-main-a-349e0e --days 30 --output json"
2990
+ )
2991
+ ).action(async (opts) => {
2992
+ try {
2993
+ const fmt = opts.output;
2994
+ if (fmt === "pretty") renderAiGatewayHeader();
2995
+ const days = Number.parseInt(opts.days, 10);
2996
+ if (!Number.isFinite(days) || days <= 0) {
2997
+ usageError(`Invalid --days '${opts.days}'. Expected a positive integer`);
2998
+ }
2999
+ const service = await createService();
3000
+ const report = await service.getTelemetryCost({ workspaceSlug: opts.workspace, days });
3001
+ renderCostReport(report, fmt);
3002
+ } catch (err) {
3003
+ failWithGrantHint(err);
3004
+ }
3005
+ });
3006
+ }
3007
+
2372
3008
  // src/cli/commands/completion.ts
2373
3009
  var COMPLETION_SHELLS = ["bash", "zsh", "fish"];
2374
3010
  function collectCompletionNodes(root, path3 = []) {
@@ -2652,39 +3288,6 @@ function registerConfigCommand(program) {
2652
3288
  import { randomUUID } from "crypto";
2653
3289
  import { readFile as readFile2 } from "fs/promises";
2654
3290
  import { init, Scanner } from "@cdot65/prisma-airs-sdk";
2655
-
2656
- // src/config/client-options.ts
2657
- function runtimeInitOptions(config) {
2658
- return {
2659
- apiKey: config.airsApiKey,
2660
- apiToken: config.airsApiToken,
2661
- apiEndpoint: config.airsApiEndpoint,
2662
- numRetries: config.airsNumRetries
2663
- };
2664
- }
2665
- function redTeamClientOptions(config) {
2666
- return {
2667
- clientId: config.mgmtClientId,
2668
- clientSecret: config.mgmtClientSecret,
2669
- tsgId: config.mgmtTsgId,
2670
- dataEndpoint: config.redTeamDataEndpoint,
2671
- mgmtEndpoint: config.redTeamMgmtEndpoint,
2672
- tokenEndpoint: config.redTeamTokenEndpoint ?? config.mgmtTokenEndpoint,
2673
- networkBrokerEndpoint: config.redTeamNetworkBrokerEndpoint
2674
- };
2675
- }
2676
- function modelSecurityClientOptions(config) {
2677
- return {
2678
- clientId: config.mgmtClientId,
2679
- clientSecret: config.mgmtClientSecret,
2680
- tsgId: config.mgmtTsgId,
2681
- dataEndpoint: config.modelSecDataEndpoint,
2682
- mgmtEndpoint: config.modelSecMgmtEndpoint,
2683
- tokenEndpoint: config.modelSecTokenEndpoint ?? config.mgmtTokenEndpoint
2684
- };
2685
- }
2686
-
2687
- // src/cli/commands/doctor.ts
2688
3291
  var DOCTOR_TIMEOUT_MS = 5e3;
2689
3292
  var MIN_NODE_MAJOR = 20;
2690
3293
  function checkNodeVersion(version = process.version) {
@@ -2882,6 +3485,50 @@ async function checkManagementAuth(probe, hasCreds, timeoutMs = DOCTOR_TIMEOUT_M
2882
3485
  };
2883
3486
  }
2884
3487
  }
3488
+ async function checkAiGatewayApi(probe, hasCreds, timeoutMs = DOCTOR_TIMEOUT_MS) {
3489
+ const name = "AI Gateway API";
3490
+ if (!hasCreds) {
3491
+ return {
3492
+ name,
3493
+ status: "warn",
3494
+ detail: "skipped \u2014 management credentials not configured",
3495
+ hint: "Set PANW_MGMT_CLIENT_ID, PANW_MGMT_CLIENT_SECRET, PANW_MGMT_TSG_ID"
3496
+ };
3497
+ }
3498
+ try {
3499
+ const result = await withTimeout(probe(), timeoutMs);
3500
+ if (result === TIMED_OUT) {
3501
+ return {
3502
+ name,
3503
+ status: "fail",
3504
+ detail: `timed out after ${timeoutMs}ms \u2014 network unreachable or endpoint not responding`,
3505
+ hint: "Check network connectivity and PANW_AI_GW_DATA_ENDPOINT"
3506
+ };
3507
+ }
3508
+ return {
3509
+ name,
3510
+ status: "pass",
3511
+ detail: `endpoint reachable (${result} workspace${result === 1 ? "" : "s"} in scope)`
3512
+ };
3513
+ } catch (err) {
3514
+ const status = httpStatus(err);
3515
+ const message = errMessage(err);
3516
+ if (status === 403) {
3517
+ return {
3518
+ name,
3519
+ status: "warn",
3520
+ detail: `endpoint reachable, but access denied (HTTP 403): ${message}`,
3521
+ hint: aiGatewayGrantHint(err)
3522
+ };
3523
+ }
3524
+ return {
3525
+ name,
3526
+ status: "fail",
3527
+ detail: status !== void 0 ? `AI Gateway API error (HTTP ${status}): ${message}` : `network unreachable: ${message}`,
3528
+ hint: "Verify credentials and PANW_AI_GW_DATA_ENDPOINT"
3529
+ };
3530
+ }
3531
+ }
2885
3532
  async function defaultScannerProbe() {
2886
3533
  const config = await loadConfig();
2887
3534
  init(runtimeInitOptions(config));
@@ -2899,6 +3546,12 @@ async function defaultMgmtProbe() {
2899
3546
  const topics = await service.listTopics();
2900
3547
  return topics.length;
2901
3548
  }
3549
+ async function defaultAiGwProbe() {
3550
+ const config = await loadConfig();
3551
+ const service = new SdkAiGatewayService(aiGatewayClientOptions(config));
3552
+ const workspaces = await service.listWorkspaces();
3553
+ return workspaces.length;
3554
+ }
2902
3555
  async function runDoctor(deps = {}) {
2903
3556
  const configFilePath = deps.configFilePath ?? resolveConfigFilePath();
2904
3557
  const inspect = deps.inspect ?? (() => inspectConfig(configFilePath));
@@ -2922,7 +3575,12 @@ async function runDoctor(deps = {}) {
2922
3575
  mgmtCreds.status === "pass",
2923
3576
  timeoutMs
2924
3577
  );
2925
- return [node, configFile, scannerCreds, mgmtCreds, scannerApi, mgmtAuth];
3578
+ const aiGwApi = await checkAiGatewayApi(
3579
+ deps.aiGwProbe ?? defaultAiGwProbe,
3580
+ mgmtCreds.status === "pass",
3581
+ timeoutMs
3582
+ );
3583
+ return [node, configFile, scannerCreds, mgmtCreds, scannerApi, mgmtAuth, aiGwApi];
2926
3584
  }
2927
3585
  function hasFailure(checks) {
2928
3586
  return checks.some((c) => c.status === "fail");
@@ -3004,7 +3662,7 @@ function run(bin, args, label) {
3004
3662
  });
3005
3663
  });
3006
3664
  }
3007
- async function createService() {
3665
+ async function createService2() {
3008
3666
  const config = await loadConfig();
3009
3667
  return new SdkModelSecurityService(modelSecurityClientOptions(config));
3010
3668
  }
@@ -3015,7 +3673,7 @@ function registerModelSecurityCommand(program) {
3015
3673
  try {
3016
3674
  const fmt = opts.output;
3017
3675
  if (fmt === "pretty") renderModelSecurityHeader();
3018
- const service = await createService();
3676
+ const service = await createService2();
3019
3677
  const result = await service.listGroups({
3020
3678
  sourceTypes: opts.sourceTypes ? opts.sourceTypes.split(",").map((s) => s.trim()) : void 0,
3021
3679
  searchQuery: opts.search,
@@ -3033,7 +3691,7 @@ function registerModelSecurityCommand(program) {
3033
3691
  try {
3034
3692
  const fmt = opts.output;
3035
3693
  if (fmt === "pretty") renderModelSecurityHeader();
3036
- const service = await createService();
3694
+ const service = await createService2();
3037
3695
  const group = await service.getGroup(uuid);
3038
3696
  renderGroupDetail(group, fmt);
3039
3697
  } catch (err) {
@@ -3043,7 +3701,7 @@ function registerModelSecurityCommand(program) {
3043
3701
  groups.command("create").description("Create a security group").requiredOption("--config <path>", "JSON file with group configuration").action(async (opts) => {
3044
3702
  try {
3045
3703
  renderModelSecurityHeader();
3046
- const service = await createService();
3704
+ const service = await createService2();
3047
3705
  const config = JSON.parse(fs.readFileSync(opts.config, "utf-8"));
3048
3706
  const group = await service.createGroup({
3049
3707
  name: config.name,
@@ -3060,7 +3718,7 @@ function registerModelSecurityCommand(program) {
3060
3718
  groups.command("update <uuid>").description("Update a security group").option("--name <name>", "New name").option("--description <desc>", "New description").action(async (uuid, opts) => {
3061
3719
  try {
3062
3720
  renderModelSecurityHeader();
3063
- const service = await createService();
3721
+ const service = await createService2();
3064
3722
  const request = {};
3065
3723
  if (opts.name) request.name = opts.name;
3066
3724
  if (opts.description) request.description = opts.description;
@@ -3074,7 +3732,7 @@ function registerModelSecurityCommand(program) {
3074
3732
  groups.command("delete <uuid>").description("Delete a security group").action(async (uuid) => {
3075
3733
  try {
3076
3734
  renderModelSecurityHeader();
3077
- const service = await createService();
3735
+ const service = await createService2();
3078
3736
  const { confirmed, state } = await service.deleteGroupAndVerify(uuid);
3079
3737
  if (confirmed) {
3080
3738
  ui.success(`Group ${uuid} deleted.`);
@@ -3104,7 +3762,7 @@ function registerModelSecurityCommand(program) {
3104
3762
  if (!useUv && !hasBin("python3")) {
3105
3763
  fail(new Error("Neither uv nor python3 found on PATH. Install one first."));
3106
3764
  }
3107
- const service = await createService();
3765
+ const service = await createService2();
3108
3766
  const auth = await service.getPyPIAuth();
3109
3767
  const pkg = `model-security-client[${extras}]`;
3110
3768
  const steps = useUv ? [
@@ -3148,7 +3806,7 @@ function registerModelSecurityCommand(program) {
3148
3806
  labels.command("add <scanUuid>").description("Add labels to a scan").requiredOption("--labels <json>", "JSON array of {key, value} labels").action(async (scanUuid, opts) => {
3149
3807
  try {
3150
3808
  renderModelSecurityHeader();
3151
- const service = await createService();
3809
+ const service = await createService2();
3152
3810
  const parsed = JSON.parse(opts.labels);
3153
3811
  await service.addLabels(scanUuid, parsed);
3154
3812
  ui.success("Labels added.");
@@ -3159,7 +3817,7 @@ function registerModelSecurityCommand(program) {
3159
3817
  labels.command("set <scanUuid>").description("Replace all labels on a scan").requiredOption("--labels <json>", "JSON array of {key, value} labels").action(async (scanUuid, opts) => {
3160
3818
  try {
3161
3819
  renderModelSecurityHeader();
3162
- const service = await createService();
3820
+ const service = await createService2();
3163
3821
  const parsed = JSON.parse(opts.labels);
3164
3822
  await service.setLabels(scanUuid, parsed);
3165
3823
  ui.success("Labels set.");
@@ -3170,7 +3828,7 @@ function registerModelSecurityCommand(program) {
3170
3828
  labels.command("delete <scanUuid>").description("Delete labels from a scan by key").requiredOption("--keys <keys>", "Comma-separated label keys to delete").action(async (scanUuid, opts) => {
3171
3829
  try {
3172
3830
  renderModelSecurityHeader();
3173
- const service = await createService();
3831
+ const service = await createService2();
3174
3832
  const keys = opts.keys.split(",").map((k) => k.trim());
3175
3833
  await service.deleteLabels(scanUuid, keys);
3176
3834
  ui.success("Labels deleted.");
@@ -3181,7 +3839,7 @@ function registerModelSecurityCommand(program) {
3181
3839
  labels.command("keys").description("List available label keys").option("--limit <n>", "Max results", "20").action(async (opts) => {
3182
3840
  try {
3183
3841
  renderModelSecurityHeader();
3184
- const service = await createService();
3842
+ const service = await createService2();
3185
3843
  const result = await service.getLabelKeys({
3186
3844
  limit: Number.parseInt(opts.limit, 10)
3187
3845
  });
@@ -3193,7 +3851,7 @@ function registerModelSecurityCommand(program) {
3193
3851
  labels.command("values <key>").description("List values for a label key").option("--limit <n>", "Max results", "20").action(async (key, opts) => {
3194
3852
  try {
3195
3853
  renderModelSecurityHeader();
3196
- const service = await createService();
3854
+ const service = await createService2();
3197
3855
  const result = await service.getLabelValues(key, {
3198
3856
  limit: Number.parseInt(opts.limit, 10)
3199
3857
  });
@@ -3205,7 +3863,7 @@ function registerModelSecurityCommand(program) {
3205
3863
  ms.command("pypi-auth").description("Get PyPI authentication URL for Google Artifact Registry").action(async () => {
3206
3864
  try {
3207
3865
  renderModelSecurityHeader();
3208
- const service = await createService();
3866
+ const service = await createService2();
3209
3867
  const auth = await service.getPyPIAuth();
3210
3868
  ui.section("PyPI Authentication");
3211
3869
  ui.keyValue([
@@ -3220,7 +3878,7 @@ function registerModelSecurityCommand(program) {
3220
3878
  ruleInstances.command("list <groupUuid>").description("List rule instances in a security group").option("--security-rule-uuid <uuid>", "Filter by security rule UUID").option("--state <state>", "Filter by state (DISABLED, ALLOWING, BLOCKING)").option("--limit <n>", "Max results", "20").action(async (groupUuid, opts) => {
3221
3879
  try {
3222
3880
  renderModelSecurityHeader();
3223
- const service = await createService();
3881
+ const service = await createService2();
3224
3882
  const result = await service.listRuleInstances(groupUuid, {
3225
3883
  securityRuleUuid: opts.securityRuleUuid,
3226
3884
  state: opts.state,
@@ -3234,7 +3892,7 @@ function registerModelSecurityCommand(program) {
3234
3892
  ruleInstances.command("get <groupUuid> <instanceUuid>").description("Get rule instance details").action(async (groupUuid, instanceUuid) => {
3235
3893
  try {
3236
3894
  renderModelSecurityHeader();
3237
- const service = await createService();
3895
+ const service = await createService2();
3238
3896
  const instance = await service.getRuleInstance(groupUuid, instanceUuid);
3239
3897
  renderRuleInstanceDetail(instance);
3240
3898
  } catch (err) {
@@ -3244,7 +3902,7 @@ function registerModelSecurityCommand(program) {
3244
3902
  ruleInstances.command("update <groupUuid> <instanceUuid>").description("Update a rule instance").requiredOption("--config <path>", "JSON file with rule instance updates").action(async (groupUuid, instanceUuid, opts) => {
3245
3903
  try {
3246
3904
  renderModelSecurityHeader();
3247
- const service = await createService();
3905
+ const service = await createService2();
3248
3906
  const config = JSON.parse(fs.readFileSync(opts.config, "utf-8"));
3249
3907
  const instance = await service.updateRuleInstance(groupUuid, instanceUuid, {
3250
3908
  state: config.state,
@@ -3261,7 +3919,7 @@ function registerModelSecurityCommand(program) {
3261
3919
  try {
3262
3920
  const fmt = opts.output;
3263
3921
  if (fmt === "pretty") renderModelSecurityHeader();
3264
- const service = await createService();
3922
+ const service = await createService2();
3265
3923
  const result = await service.listRules({
3266
3924
  sourceType: opts.sourceType,
3267
3925
  searchQuery: opts.search,
@@ -3275,7 +3933,7 @@ function registerModelSecurityCommand(program) {
3275
3933
  rules.command("get <uuid>").description("Get security rule details").action(async (uuid) => {
3276
3934
  try {
3277
3935
  renderModelSecurityHeader();
3278
- const service = await createService();
3936
+ const service = await createService2();
3279
3937
  const rule = await service.getRule(uuid);
3280
3938
  renderRuleDetail(rule);
3281
3939
  } catch (err) {
@@ -3294,7 +3952,7 @@ function registerModelSecurityCommand(program) {
3294
3952
  try {
3295
3953
  const fmt = opts.output;
3296
3954
  if (fmt === "pretty") renderModelSecurityHeader();
3297
- const service = await createService();
3955
+ const service = await createService2();
3298
3956
  const result = await service.listScans({
3299
3957
  evalOutcome: opts.evalOutcome,
3300
3958
  sourceType: opts.sourceType,
@@ -3310,7 +3968,7 @@ function registerModelSecurityCommand(program) {
3310
3968
  scans.command("get <uuid>").description("Get scan details").action(async (uuid) => {
3311
3969
  try {
3312
3970
  renderModelSecurityHeader();
3313
- const service = await createService();
3971
+ const service = await createService2();
3314
3972
  const scan = await service.getScan(uuid);
3315
3973
  renderMsScanDetail(scan);
3316
3974
  } catch (err) {
@@ -3320,7 +3978,7 @@ function registerModelSecurityCommand(program) {
3320
3978
  scans.command("create").description("Create a model security scan").requiredOption("--config <path>", "JSON file with scan configuration").action(async (opts) => {
3321
3979
  try {
3322
3980
  renderModelSecurityHeader();
3323
- const service = await createService();
3981
+ const service = await createService2();
3324
3982
  const config = JSON.parse(fs.readFileSync(opts.config, "utf-8"));
3325
3983
  const scan = await service.createScan(config);
3326
3984
  ui.success(`Scan created: ${scan.uuid}`);
@@ -3332,7 +3990,7 @@ function registerModelSecurityCommand(program) {
3332
3990
  scans.command("evaluations <scanUuid>").description("List rule evaluations for a scan").option("--limit <n>", "Max results", "20").action(async (scanUuid, opts) => {
3333
3991
  try {
3334
3992
  renderModelSecurityHeader();
3335
- const service = await createService();
3993
+ const service = await createService2();
3336
3994
  const result = await service.getEvaluations(scanUuid, {
3337
3995
  limit: Number.parseInt(opts.limit, 10)
3338
3996
  });
@@ -3344,7 +4002,7 @@ function registerModelSecurityCommand(program) {
3344
4002
  scans.command("evaluation <uuid>").description("Get evaluation details").action(async (uuid) => {
3345
4003
  try {
3346
4004
  renderModelSecurityHeader();
3347
- const service = await createService();
4005
+ const service = await createService2();
3348
4006
  const evaluation = await service.getEvaluation(uuid);
3349
4007
  renderEvaluationDetail(evaluation);
3350
4008
  } catch (err) {
@@ -3354,7 +4012,7 @@ function registerModelSecurityCommand(program) {
3354
4012
  scans.command("violations <scanUuid>").description("List violations for a scan").option("--limit <n>", "Max results", "20").action(async (scanUuid, opts) => {
3355
4013
  try {
3356
4014
  renderModelSecurityHeader();
3357
- const service = await createService();
4015
+ const service = await createService2();
3358
4016
  const result = await service.getViolations(scanUuid, {
3359
4017
  limit: Number.parseInt(opts.limit, 10)
3360
4018
  });
@@ -3366,7 +4024,7 @@ function registerModelSecurityCommand(program) {
3366
4024
  scans.command("violation <uuid>").description("Get violation details").action(async (uuid) => {
3367
4025
  try {
3368
4026
  renderModelSecurityHeader();
3369
- const service = await createService();
4027
+ const service = await createService2();
3370
4028
  const violation = await service.getViolation(uuid);
3371
4029
  renderViolationDetail(violation);
3372
4030
  } catch (err) {
@@ -3376,7 +4034,7 @@ function registerModelSecurityCommand(program) {
3376
4034
  scans.command("files <scanUuid>").description("List scanned files").option("--type <type>", "Filter by file type").option("--result <result>", "Filter by result").option("--limit <n>", "Max results", "20").action(async (scanUuid, opts) => {
3377
4035
  try {
3378
4036
  renderModelSecurityHeader();
3379
- const service = await createService();
4037
+ const service = await createService2();
3380
4038
  const result = await service.getFiles(scanUuid, {
3381
4039
  type: opts.type,
3382
4040
  result: opts.result,
@@ -3392,7 +4050,7 @@ function registerModelSecurityCommand(program) {
3392
4050
  try {
3393
4051
  const fmt = opts.output;
3394
4052
  if (fmt === "pretty") renderModelSecurityHeader();
3395
- const service = await createService();
4053
+ const service = await createService2();
3396
4054
  const result = await service.listModels({
3397
4055
  search: opts.search,
3398
4056
  searchQuery: opts.searchQuery,
@@ -3410,7 +4068,7 @@ function registerModelSecurityCommand(program) {
3410
4068
  try {
3411
4069
  const fmt = opts.output;
3412
4070
  if (fmt === "pretty") renderModelSecurityHeader();
3413
- const service = await createService();
4071
+ const service = await createService2();
3414
4072
  const model = await service.getModel(uuid);
3415
4073
  renderModelDetail(model, fmt);
3416
4074
  } catch (err) {
@@ -3421,7 +4079,7 @@ function registerModelSecurityCommand(program) {
3421
4079
  try {
3422
4080
  const fmt = opts.output;
3423
4081
  if (fmt === "pretty") renderModelSecurityHeader();
3424
- const service = await createService();
4082
+ const service = await createService2();
3425
4083
  const result = await service.listModelVersions(modelUuid, {
3426
4084
  sortOrder: opts.sortOrder,
3427
4085
  limit: opts.limit ? Number.parseInt(opts.limit, 10) : void 0,
@@ -3436,7 +4094,7 @@ function registerModelSecurityCommand(program) {
3436
4094
  try {
3437
4095
  const fmt = opts.output;
3438
4096
  if (fmt === "pretty") renderModelSecurityHeader();
3439
- const service = await createService();
4097
+ const service = await createService2();
3440
4098
  const version = await service.getModelVersion(uuid);
3441
4099
  renderModelVersionDetail(version, fmt);
3442
4100
  } catch (err) {
@@ -3447,41 +4105,24 @@ function registerModelSecurityCommand(program) {
3447
4105
  try {
3448
4106
  const fmt = opts.output;
3449
4107
  if (fmt === "pretty") renderModelSecurityHeader();
3450
- const service = await createService();
4108
+ const service = await createService2();
3451
4109
  const result = await service.listModelVersionFiles(modelVersionUuid, {
3452
4110
  limit: opts.limit ? Number.parseInt(opts.limit, 10) : void 0,
3453
4111
  skip: opts.offset ? Number.parseInt(opts.offset, 10) : void 0
3454
4112
  });
3455
4113
  renderModelFileList(result.files, fmt);
3456
- } catch (err) {
3457
- fail(err);
3458
- }
3459
- });
3460
- }
3461
-
3462
- // src/cli/commands/redteam.ts
3463
- import * as fs2 from "fs";
3464
- import * as path from "path";
3465
-
3466
- // src/cli/confirm.ts
3467
- async function confirmOrAbort(message, force, options = {}) {
3468
- if (force) return;
3469
- const interactive = options.isTTY ?? process.stdout.isTTY === true;
3470
- if (!interactive) {
3471
- usageError(
3472
- `refusing to ${options.action ?? "proceed"} without --force in non-interactive mode`
3473
- );
3474
- }
3475
- const prompt = options.promptFn ?? (await import("@inquirer/prompts")).confirm;
3476
- const confirmed = await prompt({ message, default: false });
3477
- if (!confirmed) {
3478
- ui.info("Aborted");
3479
- process.exit(0);
3480
- }
4114
+ } catch (err) {
4115
+ fail(err);
4116
+ }
4117
+ });
3481
4118
  }
3482
4119
 
4120
+ // src/cli/commands/redteam.ts
4121
+ import * as fs2 from "fs";
4122
+ import * as path from "path";
4123
+
3483
4124
  // src/cli/deprecated-flags.ts
3484
- import chalk9 from "chalk";
4125
+ import chalk10 from "chalk";
3485
4126
  import { Option } from "commander";
3486
4127
  var ALIASES = /* @__PURE__ */ new WeakMap();
3487
4128
  function registerDeprecatedAlias(cmd, alias) {
@@ -3496,7 +4137,7 @@ function resolveDeprecatedAliases(cmd, opts) {
3496
4137
  const oldValue = opts[alias.oldKey];
3497
4138
  if (oldValue === void 0) continue;
3498
4139
  console.error(
3499
- chalk9.yellow(
4140
+ chalk10.yellow(
3500
4141
  ` \u26A0 ${alias.oldFlag.split(" ")[0]} is deprecated and will be removed in v4 \u2014 use ${alias.canonicalFlag}`
3501
4142
  )
3502
4143
  );
@@ -3680,7 +4321,7 @@ async function restoreTargets(opts) {
3680
4321
  }
3681
4322
 
3682
4323
  // src/cli/commands/redteam.ts
3683
- async function createService2() {
4324
+ async function createService3() {
3684
4325
  const config = await loadConfig();
3685
4326
  return new SdkRedTeamService(redTeamClientOptions(config));
3686
4327
  }
@@ -3688,6 +4329,14 @@ async function createPromptSetService() {
3688
4329
  const config = await loadConfig();
3689
4330
  return new SdkPromptSetService(redTeamClientOptions(config));
3690
4331
  }
4332
+ function buildDefaultCategories(categories) {
4333
+ return Object.fromEntries(
4334
+ categories.map((category) => [
4335
+ category.id,
4336
+ category.subCategories.map((subCategory) => subCategory.id).filter((id) => id !== "MULTI_TURN")
4337
+ ])
4338
+ );
4339
+ }
3691
4340
  function parseAttackGoals(input) {
3692
4341
  const trimmed = input.trim();
3693
4342
  const raw = trimmed.startsWith("[") ? trimmed : fs2.readFileSync(trimmed, "utf-8");
@@ -3720,8 +4369,11 @@ var VALID_TARGET_PROVIDERS = [
3720
4369
  "DATABRICKS",
3721
4370
  "BEDROCK",
3722
4371
  "REST",
3723
- "STREAMING"
4372
+ "STREAMING",
4373
+ "WEBSOCKET",
4374
+ "CUSTOM_TARGET_ADAPTER"
3724
4375
  ];
4376
+ var REST_PROVIDERS = /* @__PURE__ */ new Set(["REST", "STREAMING", "WEBSOCKET", "HUGGING_FACE"]);
3725
4377
  function buildTargetScaffold(provider, templates) {
3726
4378
  const key = provider.toUpperCase();
3727
4379
  if (!VALID_TARGET_PROVIDERS.includes(key)) {
@@ -3729,21 +4381,92 @@ function buildTargetScaffold(provider, templates) {
3729
4381
  `Unknown provider "${provider}". Valid providers: ${VALID_TARGET_PROVIDERS.join(", ")}`
3730
4382
  );
3731
4383
  }
4384
+ if (key === "CUSTOM_TARGET_ADAPTER") {
4385
+ return {
4386
+ name: "",
4387
+ target_type: "AGENT",
4388
+ connection_type: "CUSTOM_TARGET_ADAPTER",
4389
+ api_endpoint_type: "NETWORK_BROKER",
4390
+ network_broker_channel_uuid: "<channel-uuid>",
4391
+ adapter_uuid: "<adapter-uuid>",
4392
+ // adapter_variable_overrides is an ARRAY of {key, value, type} objects.
4393
+ adapter_variable_overrides: [],
4394
+ target_background: { use_case: "" },
4395
+ additional_context: {}
4396
+ };
4397
+ }
4398
+ if (REST_PROVIDERS.has(key)) {
4399
+ const tpl = templates[key] ?? {};
4400
+ return {
4401
+ name: "",
4402
+ target_type: "APPLICATION",
4403
+ connection_type: "CUSTOM",
4404
+ api_endpoint_type: "PUBLIC",
4405
+ response_mode: key === "STREAMING" ? "STREAMING" : key === "WEBSOCKET" ? "WEBSOCKET" : "REST",
4406
+ auth_type: "HEADERS",
4407
+ auth_config: {
4408
+ auth_header: { Authorization: "Bearer <token>" }
4409
+ },
4410
+ connection_params: {
4411
+ api_endpoint: tpl.url ?? "",
4412
+ request_headers: { "Content-Type": "application/json" },
4413
+ request_json: tpl.request_json ?? { messages: [{ role: "user", content: "{INPUT}" }] },
4414
+ response_json: tpl.response_json ?? { choices: [{ message: { content: "{RESPONSE}" } }] },
4415
+ response_key: "choices.0.message.content"
4416
+ },
4417
+ target_background: {},
4418
+ additional_context: {}
4419
+ };
4420
+ }
3732
4421
  return {
3733
4422
  name: "",
3734
4423
  target_type: "APPLICATION",
3735
- connection_params: templates[key] ?? {},
4424
+ connection_type: key,
4425
+ api_endpoint_type: "PUBLIC",
4426
+ response_mode: "REST",
4427
+ auth_type: "HEADERS",
4428
+ auth_config: {
4429
+ auth_header: { Authorization: "Bearer <token>" }
4430
+ },
4431
+ connection_params: {
4432
+ target_connection_config: templates[key] ?? {}
4433
+ },
3736
4434
  target_background: {},
3737
- additional_context: {},
3738
- target_metadata: {}
4435
+ additional_context: {}
3739
4436
  };
3740
4437
  }
4438
+ function resolveScriptB64(opts) {
4439
+ if (opts.scriptFile !== void 0 && opts.scriptB64 !== void 0) {
4440
+ throw new Error("--script-file and --script-b64 are mutually exclusive");
4441
+ }
4442
+ if (opts.scriptB64 !== void 0) return opts.scriptB64;
4443
+ if (opts.scriptFile !== void 0) {
4444
+ return Buffer.from(fs2.readFileSync(opts.scriptFile, "utf-8")).toString("base64");
4445
+ }
4446
+ throw new Error("one of --script-file or --script-b64 is required");
4447
+ }
4448
+ function parseAdapterVariables(input) {
4449
+ let parsed;
4450
+ try {
4451
+ parsed = JSON.parse(input);
4452
+ } catch (err) {
4453
+ throw new Error(`--variables: invalid JSON (${err instanceof Error ? err.message : err})`);
4454
+ }
4455
+ if (!Array.isArray(parsed) || !parsed.every(
4456
+ (v) => v !== null && typeof v === "object" && typeof v.key === "string" && (v.type === "VAR" || v.type === "SECRET")
4457
+ )) {
4458
+ throw new Error(
4459
+ '--variables: expected a JSON array of { "key": string, "value"?: string|null, "type": "VAR"|"SECRET" }'
4460
+ );
4461
+ }
4462
+ return parsed;
4463
+ }
3741
4464
  function registerRedteamCommand(program) {
3742
4465
  const redteam = program.command("redteam").description("AI Red Team scan operations");
3743
4466
  redteam.command("abort <jobId>").description("Abort a running scan").action(async (jobId) => {
3744
4467
  try {
3745
4468
  renderRedteamHeader();
3746
- const service = await createService2();
4469
+ const service = await createService3();
3747
4470
  await service.abortScan(jobId);
3748
4471
  ui.success(`Scan ${jobId} aborted.`);
3749
4472
  } catch (err) {
@@ -3753,7 +4476,7 @@ function registerRedteamCommand(program) {
3753
4476
  redteam.command("categories").description("List available attack categories").action(async () => {
3754
4477
  try {
3755
4478
  renderRedteamHeader();
3756
- const service = await createService2();
4479
+ const service = await createService3();
3757
4480
  const categories = await service.getCategories();
3758
4481
  renderCategories(categories);
3759
4482
  } catch (err) {
@@ -3764,7 +4487,7 @@ function registerRedteamCommand(program) {
3764
4487
  eula.command("status").description("Check EULA acceptance status").action(async () => {
3765
4488
  try {
3766
4489
  renderRedteamHeader();
3767
- const service = await createService2();
4490
+ const service = await createService3();
3768
4491
  const status = await service.getEulaStatus();
3769
4492
  renderEulaStatus(status);
3770
4493
  } catch (err) {
@@ -3774,7 +4497,7 @@ function registerRedteamCommand(program) {
3774
4497
  eula.command("content").description("Display EULA content").action(async () => {
3775
4498
  try {
3776
4499
  renderRedteamHeader();
3777
- const service = await createService2();
4500
+ const service = await createService3();
3778
4501
  const content = await service.getEulaContent();
3779
4502
  renderEulaContent(content);
3780
4503
  } catch (err) {
@@ -3792,7 +4515,7 @@ function registerRedteamCommand(program) {
3792
4515
  resolveDeprecatedAliases(eulaAccept, opts);
3793
4516
  try {
3794
4517
  renderRedteamHeader();
3795
- const service = await createService2();
4518
+ const service = await createService3();
3796
4519
  const content = await service.getEulaContent();
3797
4520
  if (!opts.force) {
3798
4521
  renderEulaContent(content);
@@ -3810,7 +4533,7 @@ function registerRedteamCommand(program) {
3810
4533
  instances.command("create").description("Create an instance").requiredOption("--tsg-id <id>", "TSG ID").requiredOption("--tenant-id <id>", "Tenant ID").requiredOption("--app-id <id>", "App ID").requiredOption("--region <region>", "Region").action(async (opts) => {
3811
4534
  try {
3812
4535
  renderRedteamHeader();
3813
- const service = await createService2();
4536
+ const service = await createService3();
3814
4537
  const result = await service.createInstance({
3815
4538
  tsgId: opts.tsgId,
3816
4539
  tenantId: opts.tenantId,
@@ -3826,7 +4549,7 @@ function registerRedteamCommand(program) {
3826
4549
  try {
3827
4550
  const fmt = opts.output;
3828
4551
  if (fmt === "pretty") renderRedteamHeader();
3829
- const service = await createService2();
4552
+ const service = await createService3();
3830
4553
  const result = await service.getInstance(tenantId);
3831
4554
  renderInstanceDetail(result, fmt);
3832
4555
  } catch (err) {
@@ -3836,7 +4559,7 @@ function registerRedteamCommand(program) {
3836
4559
  instances.command("update <tenantId>").description("Update an instance").requiredOption("--tsg-id <id>", "TSG ID").requiredOption("--app-id <id>", "App ID").requiredOption("--region <region>", "Region").action(async (tenantId, opts) => {
3837
4560
  try {
3838
4561
  renderRedteamHeader();
3839
- const service = await createService2();
4562
+ const service = await createService3();
3840
4563
  const result = await service.updateInstance(tenantId, {
3841
4564
  tsgId: opts.tsgId,
3842
4565
  tenantId,
@@ -3851,7 +4574,7 @@ function registerRedteamCommand(program) {
3851
4574
  instances.command("delete <tenantId>").description("Delete an instance").action(async (tenantId) => {
3852
4575
  try {
3853
4576
  renderRedteamHeader();
3854
- const service = await createService2();
4577
+ const service = await createService3();
3855
4578
  const result = await service.deleteInstance(tenantId);
3856
4579
  renderInstanceResponse(result);
3857
4580
  ui.success(`Instance ${tenantId} deleted.`);
@@ -3863,7 +4586,7 @@ function registerRedteamCommand(program) {
3863
4586
  devices.command("create <tenantId>").description("Create devices for an instance").requiredOption("--config <path>", "JSON file with device request").action(async (tenantId, opts) => {
3864
4587
  try {
3865
4588
  renderRedteamHeader();
3866
- const service = await createService2();
4589
+ const service = await createService3();
3867
4590
  const config = JSON.parse(fs2.readFileSync(opts.config, "utf-8"));
3868
4591
  const result = await service.createDevices(tenantId, config);
3869
4592
  ui.success("Devices created:");
@@ -3875,7 +4598,7 @@ function registerRedteamCommand(program) {
3875
4598
  devices.command("update <tenantId>").description("Update devices for an instance (PATCH)").requiredOption("--config <path>", "JSON file with device request").action(async (tenantId, opts) => {
3876
4599
  try {
3877
4600
  renderRedteamHeader();
3878
- const service = await createService2();
4601
+ const service = await createService3();
3879
4602
  const config = JSON.parse(fs2.readFileSync(opts.config, "utf-8"));
3880
4603
  const result = await service.updateDevices(tenantId, config);
3881
4604
  ui.success("Devices updated:");
@@ -3887,7 +4610,7 @@ function registerRedteamCommand(program) {
3887
4610
  devices.command("delete <tenantId>").description("Delete devices by serial numbers").requiredOption("--serial-numbers <list>", "Comma-separated serial numbers").action(async (tenantId, opts) => {
3888
4611
  try {
3889
4612
  renderRedteamHeader();
3890
- const service = await createService2();
4613
+ const service = await createService3();
3891
4614
  const result = await service.deleteDevices(tenantId, opts.serialNumbers);
3892
4615
  ui.success("Devices deleted:");
3893
4616
  console.log(JSON.stringify(result, null, 2));
@@ -3899,7 +4622,7 @@ function registerRedteamCommand(program) {
3899
4622
  try {
3900
4623
  const fmt = opts.output;
3901
4624
  if (fmt === "pretty") renderRedteamHeader();
3902
- const service = await createService2();
4625
+ const service = await createService3();
3903
4626
  const creds = await service.getRegistryCredentials();
3904
4627
  renderRegistryCredentials(creds, fmt);
3905
4628
  } catch (err) {
@@ -3910,7 +4633,7 @@ function registerRedteamCommand(program) {
3910
4633
  try {
3911
4634
  const fmt = opts.output;
3912
4635
  if (fmt === "pretty") renderRedteamHeader();
3913
- const service = await createService2();
4636
+ const service = await createService3();
3914
4637
  const scans = await service.listScans({
3915
4638
  status: opts.status,
3916
4639
  jobType: opts.type,
@@ -4123,7 +4846,7 @@ function registerRedteamCommand(program) {
4123
4846
  redteam.command("report <jobId>").description("View scan report").option("--attacks", "Include attack list", false).option("--severity <level>", "Filter attacks by severity").option("--limit <n>", "Max attacks to show", "20").action(async (jobId, opts) => {
4124
4847
  try {
4125
4848
  renderRedteamHeader();
4126
- const service = await createService2();
4849
+ const service = await createService3();
4127
4850
  const job = await service.getScan(jobId);
4128
4851
  renderScanStatus(job);
4129
4852
  if (job.jobType === "CUSTOM") {
@@ -4183,7 +4906,18 @@ function registerRedteamCommand(program) {
4183
4906
  const customPromptSets = opts.promptSets ? opts.promptSets.split(",").map((s) => s.trim()) : void 0;
4184
4907
  try {
4185
4908
  renderRedteamHeader();
4186
- const service = await createService2();
4909
+ const service = await createService3();
4910
+ if (opts.type === "STATIC" && !categories) {
4911
+ const defaultCategories = buildDefaultCategories(await service.getCategories());
4912
+ const categoryCount = Object.values(defaultCategories).reduce(
4913
+ (total, subCategories) => total + subCategories.length,
4914
+ 0
4915
+ );
4916
+ categories = defaultCategories;
4917
+ ui.status(
4918
+ `No --categories given \u2014 defaulting to all ${categoryCount} categories (MULTI_TURN excluded). Pass --categories to narrow the scan.`
4919
+ );
4920
+ }
4187
4921
  ui.status(`Creating ${opts.type} scan "${opts.name}"...`);
4188
4922
  const job = await service.createScan({
4189
4923
  name: opts.name,
@@ -4217,7 +4951,7 @@ function registerRedteamCommand(program) {
4217
4951
  redteam.command("status <jobId>").description("Check scan status").action(async (jobId) => {
4218
4952
  try {
4219
4953
  renderRedteamHeader();
4220
- const service = await createService2();
4954
+ const service = await createService3();
4221
4955
  const job = await service.getScan(jobId);
4222
4956
  renderScanStatus(job);
4223
4957
  } catch (err) {
@@ -4236,7 +4970,7 @@ function registerRedteamCommand(program) {
4236
4970
  try {
4237
4971
  const fmt = opts.output;
4238
4972
  if (fmt === "pretty") renderRedteamHeader();
4239
- const service = await createService2();
4973
+ const service = await createService3();
4240
4974
  const list = await service.listTargets();
4241
4975
  renderTargetList(sliceClientSide(list, opts), fmt);
4242
4976
  } catch (err) {
@@ -4247,7 +4981,7 @@ function registerRedteamCommand(program) {
4247
4981
  try {
4248
4982
  const fmt = opts.output;
4249
4983
  if (fmt === "pretty") renderRedteamHeader();
4250
- const service = await createService2();
4984
+ const service = await createService3();
4251
4985
  const target = await service.getTarget(uuid);
4252
4986
  renderTargetDetail(target, fmt);
4253
4987
  } catch (err) {
@@ -4257,7 +4991,7 @@ function registerRedteamCommand(program) {
4257
4991
  targets.command("create").description("Create a new red team target").requiredOption("--config <path>", "JSON file with target configuration").option("--validate", "Validate target connection before saving").action(async (opts) => {
4258
4992
  try {
4259
4993
  renderRedteamHeader();
4260
- const service = await createService2();
4994
+ const service = await createService3();
4261
4995
  const config = JSON.parse(fs2.readFileSync(opts.config, "utf-8"));
4262
4996
  const target = await service.createTarget(
4263
4997
  config,
@@ -4272,7 +5006,7 @@ function registerRedteamCommand(program) {
4272
5006
  targets.command("update <uuid>").description("Update a red team target").requiredOption("--config <path>", "JSON file with target updates").option("--validate", "Validate target connection before saving").action(async (uuid, opts) => {
4273
5007
  try {
4274
5008
  renderRedteamHeader();
4275
- const service = await createService2();
5009
+ const service = await createService3();
4276
5010
  const config = JSON.parse(fs2.readFileSync(opts.config, "utf-8"));
4277
5011
  const target = await service.updateTarget(
4278
5012
  uuid,
@@ -4291,7 +5025,7 @@ function registerRedteamCommand(program) {
4291
5025
  action: `delete target ${uuid}`
4292
5026
  });
4293
5027
  renderRedteamHeader();
4294
- const service = await createService2();
5028
+ const service = await createService3();
4295
5029
  await service.deleteTarget(uuid);
4296
5030
  ui.success(`Target ${uuid} deleted.`);
4297
5031
  } catch (err) {
@@ -4301,7 +5035,7 @@ function registerRedteamCommand(program) {
4301
5035
  targets.command("probe").description("Test target connection without saving").requiredOption("--config <path>", "JSON file with connection params").action(async (opts) => {
4302
5036
  try {
4303
5037
  renderRedteamHeader();
4304
- const service = await createService2();
5038
+ const service = await createService3();
4305
5039
  const config = JSON.parse(fs2.readFileSync(opts.config, "utf-8"));
4306
5040
  const result = await service.probeTarget(config);
4307
5041
  ui.dim("Probe result:");
@@ -4313,7 +5047,7 @@ function registerRedteamCommand(program) {
4313
5047
  targets.command("profile <uuid>").description("View target profile").action(async (uuid) => {
4314
5048
  try {
4315
5049
  renderRedteamHeader();
4316
- const service = await createService2();
5050
+ const service = await createService3();
4317
5051
  const profile = await service.getTargetProfile(uuid);
4318
5052
  ui.dim("Target Profile:");
4319
5053
  console.log(JSON.stringify(profile, null, 2));
@@ -4324,7 +5058,7 @@ function registerRedteamCommand(program) {
4324
5058
  targets.command("update-profile <uuid>").description("Update target profile").requiredOption("--config <path>", "JSON file with profile updates").action(async (uuid, opts) => {
4325
5059
  try {
4326
5060
  renderRedteamHeader();
4327
- const service = await createService2();
5061
+ const service = await createService3();
4328
5062
  const config = JSON.parse(fs2.readFileSync(opts.config, "utf-8"));
4329
5063
  const result = await service.updateTargetProfile(uuid, config);
4330
5064
  ui.success("Profile updated:");
@@ -4336,7 +5070,7 @@ function registerRedteamCommand(program) {
4336
5070
  targets.command("validate-auth").description("Validate target auth credentials").requiredOption("--auth-type <type>", "Auth type: HEADERS, BASIC_AUTH, OAUTH2").requiredOption("--config <path>", "JSON file with auth_config").option("--target-id <uuid>", "Existing target UUID").action(async (opts) => {
4337
5071
  try {
4338
5072
  renderRedteamHeader();
4339
- const service = await createService2();
5073
+ const service = await createService3();
4340
5074
  const authConfig = JSON.parse(fs2.readFileSync(opts.config, "utf-8"));
4341
5075
  const result = await service.validateTargetAuth({
4342
5076
  authType: opts.authType,
@@ -4350,7 +5084,7 @@ function registerRedteamCommand(program) {
4350
5084
  });
4351
5085
  targets.command("metadata").description("Get target field metadata").action(async () => {
4352
5086
  try {
4353
- const service = await createService2();
5087
+ const service = await createService3();
4354
5088
  const metadata = await service.getTargetMetadata();
4355
5089
  console.log(JSON.stringify(metadata, null, 2));
4356
5090
  } catch (err) {
@@ -4382,7 +5116,7 @@ function registerRedteamCommand(program) {
4382
5116
  }
4383
5117
  try {
4384
5118
  renderRedteamHeader();
4385
- const service = await createService2();
5119
+ const service = await createService3();
4386
5120
  const templates = await service.getTargetTemplates();
4387
5121
  const scaffold = buildTargetScaffold(provider, templates);
4388
5122
  fs2.writeFileSync(outputPath, `${JSON.stringify(scaffold, null, 2)}
@@ -4401,7 +5135,7 @@ function registerRedteamCommand(program) {
4401
5135
  targets.command("templates").description("Get provider-specific target templates").action(async () => {
4402
5136
  try {
4403
5137
  renderRedteamHeader();
4404
- const service = await createService2();
5138
+ const service = await createService3();
4405
5139
  const templates = await service.getTargetTemplates();
4406
5140
  renderTargetTemplates(templates);
4407
5141
  } catch (err) {
@@ -4455,7 +5189,7 @@ function registerRedteamCommand(program) {
4455
5189
  try {
4456
5190
  const fmt = opts.output;
4457
5191
  if (fmt === "pretty") renderRedteamHeader();
4458
- const service = await createService2();
5192
+ const service = await createService3();
4459
5193
  const { logs } = await service.getTargetProfileErrorLogs(targetId, {
4460
5194
  limit: opts.limit ? parsePositiveInt(opts.limit, "--limit") : void 0,
4461
5195
  offset: opts.offset ? Number.parseInt(opts.offset, 10) : void 0,
@@ -4466,13 +5200,173 @@ function registerRedteamCommand(program) {
4466
5200
  fail(err);
4467
5201
  }
4468
5202
  });
5203
+ const adapter = redteam.command("adapter").description("Manage custom target adapters (scripted targets run via the network broker)");
5204
+ async function assertChannelOnline(service, channelUuid) {
5205
+ let status;
5206
+ try {
5207
+ status = (await service.getChannel(channelUuid)).status;
5208
+ } catch {
5209
+ return;
5210
+ }
5211
+ if (status && status !== "ONLINE") {
5212
+ fail(
5213
+ new Error(
5214
+ `network broker channel ${channelUuid} is ${status} \u2014 adapter validation requires an ONLINE channel (network broker v1.4.0+). Check 'airs redteam network-broker channels list'.`
5215
+ )
5216
+ );
5217
+ }
5218
+ }
5219
+ adapter.command("list").description("List custom target adapters").option("--limit <n>", "Max results").option("--offset <n>", "Starting offset").option("--search <text>", "Filter by search text").option("--output <format>", "Output format: pretty, table, csv, json, yaml", "pretty").action(async (opts) => {
5220
+ try {
5221
+ const fmt = opts.output;
5222
+ if (fmt === "pretty") renderRedteamHeader();
5223
+ const service = await createService3();
5224
+ const { adapters, totalItems } = await service.listAdapters({
5225
+ limit: opts.limit ? parsePositiveInt(opts.limit, "--limit") : void 0,
5226
+ offset: opts.offset ? Number.parseInt(opts.offset, 10) : void 0,
5227
+ search: opts.search
5228
+ });
5229
+ renderAdapterList(adapters, fmt, totalItems);
5230
+ } catch (err) {
5231
+ fail(err);
5232
+ }
5233
+ });
5234
+ adapter.command("get <uuid>").description("Get a custom target adapter").option("--output <format>", "Output format: pretty, json, yaml", "pretty").action(async (uuid, opts) => {
5235
+ try {
5236
+ const fmt = opts.output;
5237
+ if (fmt === "pretty") renderRedteamHeader();
5238
+ const service = await createService3();
5239
+ renderAdapterDetail(await service.getAdapter(uuid), fmt);
5240
+ } catch (err) {
5241
+ fail(err);
5242
+ }
5243
+ });
5244
+ adapter.command("create").description("Create a custom target adapter").requiredOption("--name <name>", "Adapter name").requiredOption(
5245
+ "--prompt <text>",
5246
+ "Sample prompt used to exercise the adapter during validation (not stored)"
5247
+ ).option("--script-file <path>", "Path to the adapter script (encoded to base64 for you)").option("--script-b64 <b64>", "Adapter script, already base64-encoded").option("--description <text>", "Adapter description").option("--channel <uuid>", "Network broker channel UUID (required to activate)").option("--variables <json>", "JSON array of { key, value, type: VAR|SECRET }").option("--draft", "Save as DRAFT without running the validation script").option("--output <format>", "Output format: pretty, json, yaml", "pretty").addHelpText(
5248
+ "after",
5249
+ examples(
5250
+ `airs redteam adapter create --name my-adapter --script-file ./adapter.py --channel 550e8400-... --prompt 'Hello' --variables '[{"key":"endpoint","value":"http://agent.svc:8080","type":"VAR"}]'`,
5251
+ "airs redteam adapter create --name my-adapter --script-file ./adapter.py --prompt Hello --draft"
5252
+ )
5253
+ ).action(async (opts) => {
5254
+ try {
5255
+ const fmt = opts.output;
5256
+ if (fmt === "pretty") renderRedteamHeader();
5257
+ const scriptB64 = resolveScriptB64(opts);
5258
+ const variables = opts.variables ? parseAdapterVariables(opts.variables) : void 0;
5259
+ const service = await createService3();
5260
+ if (!opts.draft && opts.channel) await assertChannelOnline(service, opts.channel);
5261
+ const created = await service.createAdapter(
5262
+ {
5263
+ name: opts.name,
5264
+ scriptB64,
5265
+ prompt: opts.prompt,
5266
+ description: opts.description,
5267
+ networkBrokerChannelUuid: opts.channel,
5268
+ variables
5269
+ },
5270
+ opts.draft ? false : void 0
5271
+ );
5272
+ ui.success(`Adapter created: ${created.uuid}`);
5273
+ renderAdapterDetail(created, fmt);
5274
+ } catch (err) {
5275
+ fail(err);
5276
+ }
5277
+ });
5278
+ adapter.command("update <uuid>").description(
5279
+ "Update a custom target adapter (read-modify-write; variables preserved unless --variables)"
5280
+ ).requiredOption(
5281
+ "--prompt <text>",
5282
+ "Sample validation prompt \u2014 required on every update because upstream never stores it"
5283
+ ).option("--name <name>", "New adapter name").option("--script-file <path>", "New adapter script file (encoded to base64 for you)").option("--script-b64 <b64>", "New adapter script, already base64-encoded").option("--description <text>", "New description").option("--channel <uuid>", "New network broker channel UUID").option(
5284
+ "--variables <json>",
5285
+ "REPLACES the whole variable set \u2014 omitted keys are deleted upstream. Omit this flag to preserve stored variables."
5286
+ ).option("--draft", "Save as DRAFT without re-running the validation script").option("--output <format>", "Output format: pretty, json, yaml", "pretty").addHelpText(
5287
+ "after",
5288
+ examples(
5289
+ `airs redteam adapter update 550e8400-... --description 'new description' --prompt 'Hello'`
5290
+ )
5291
+ ).action(async (uuid, opts) => {
5292
+ try {
5293
+ const fmt = opts.output;
5294
+ if (fmt === "pretty") renderRedteamHeader();
5295
+ const scriptB64 = opts.scriptFile !== void 0 || opts.scriptB64 !== void 0 ? resolveScriptB64(opts) : void 0;
5296
+ const variables = opts.variables ? parseAdapterVariables(opts.variables) : void 0;
5297
+ const service = await createService3();
5298
+ if (!opts.draft && opts.channel) await assertChannelOnline(service, opts.channel);
5299
+ const updated = await service.updateAdapter(
5300
+ uuid,
5301
+ {
5302
+ prompt: opts.prompt,
5303
+ name: opts.name,
5304
+ scriptB64,
5305
+ description: opts.description,
5306
+ networkBrokerChannelUuid: opts.channel,
5307
+ variables
5308
+ },
5309
+ opts.draft ? false : void 0
5310
+ );
5311
+ ui.success(`Adapter updated: ${updated.uuid}`);
5312
+ renderAdapterDetail(updated, fmt);
5313
+ } catch (err) {
5314
+ fail(err);
5315
+ }
5316
+ });
5317
+ adapter.command("delete <uuid>").description("Delete a custom target adapter").option("--force", "Skip confirmation prompt").action(async (uuid, opts) => {
5318
+ try {
5319
+ renderRedteamHeader();
5320
+ await confirmOrAbort(`Delete adapter ${uuid}?`, Boolean(opts.force), {
5321
+ action: `delete adapter ${uuid}`
5322
+ });
5323
+ const service = await createService3();
5324
+ await service.deleteAdapter(uuid);
5325
+ ui.success(`Adapter ${uuid} deleted.`);
5326
+ } catch (err) {
5327
+ fail(err);
5328
+ }
5329
+ });
5330
+ adapter.command("validate").description("Run an adapter script end-to-end through the broker channel without saving").requiredOption("--channel <uuid>", "Network broker channel UUID (must be ONLINE)").requiredOption("--prompt <text>", "Sample prompt to send through the adapter").option("--script-file <path>", "Path to the adapter script (encoded to base64 for you)").option("--script-b64 <b64>", "Adapter script, already base64-encoded").option(
5331
+ "--variables <json>",
5332
+ "JSON array of { key, value, type } \u2014 the FULL set the script needs"
5333
+ ).option(
5334
+ "--adapter <uuid>",
5335
+ "Existing adapter: resolves redacted/null variable values from its stored secrets (and supplies its variable set when --variables is omitted)"
5336
+ ).option("--output <format>", "Output format: pretty, json, yaml", "pretty").addHelpText(
5337
+ "after",
5338
+ examples(
5339
+ `airs redteam adapter validate --script-file ./adapter.py --channel 550e8400-... --prompt 'Hello' --variables '[{"key":"endpoint","value":"http://agent.svc:8080","type":"VAR"}]'`,
5340
+ `airs redteam adapter validate --script-file ./adapter.py --channel 550e8400-... --prompt 'Hello' --adapter 660e8400-...`
5341
+ )
5342
+ ).action(async (opts) => {
5343
+ try {
5344
+ const fmt = opts.output;
5345
+ if (fmt === "pretty") renderRedteamHeader();
5346
+ const scriptB64 = resolveScriptB64(opts);
5347
+ const variables = opts.variables ? parseAdapterVariables(opts.variables) : void 0;
5348
+ const service = await createService3();
5349
+ await assertChannelOnline(service, opts.channel);
5350
+ const result = await service.validateAdapter({
5351
+ scriptB64,
5352
+ networkBrokerChannelUuid: opts.channel,
5353
+ prompt: opts.prompt,
5354
+ variables,
5355
+ adapterUuid: opts.adapter
5356
+ });
5357
+ renderAdapterValidation(result, fmt);
5358
+ if (!result.validated) process.exitCode = 1;
5359
+ } catch (err) {
5360
+ fail(err);
5361
+ }
5362
+ });
4469
5363
  const networkBroker = redteam.command("network-broker").description("Manage red team network broker channels");
4470
5364
  const channels = networkBroker.command("channels").description("Manage network broker channels");
4471
5365
  channels.command("list").description("List network broker channels").option("--limit <n>", "Max results").option("--offset <n>", "Starting offset").option("--search <text>", "Filter by search text").option("--status <status...>", "Filter by status (ONLINE, OFFLINE, DRAFT)").option("--output <format>", "Output format: pretty, table, csv, json, yaml", "pretty").action(async (opts) => {
4472
5366
  try {
4473
5367
  const fmt = opts.output;
4474
5368
  if (fmt === "pretty") renderRedteamHeader();
4475
- const service = await createService2();
5369
+ const service = await createService3();
4476
5370
  const { channels: list } = await service.listChannels({
4477
5371
  limit: opts.limit ? parsePositiveInt(opts.limit, "--limit") : void 0,
4478
5372
  offset: opts.offset ? Number.parseInt(opts.offset, 10) : void 0,
@@ -4488,7 +5382,7 @@ function registerRedteamCommand(program) {
4488
5382
  try {
4489
5383
  const fmt = opts.output;
4490
5384
  if (fmt === "pretty") renderRedteamHeader();
4491
- const service = await createService2();
5385
+ const service = await createService3();
4492
5386
  const channel = await service.getChannel(channelId);
4493
5387
  renderChannelDetail(channel, fmt);
4494
5388
  } catch (err) {
@@ -4498,7 +5392,7 @@ function registerRedteamCommand(program) {
4498
5392
  channels.command("create").description("Create a network broker channel").requiredOption("--name <name>", "Channel name").option("--description <text>", "Channel description").action(async (opts) => {
4499
5393
  try {
4500
5394
  renderRedteamHeader();
4501
- const service = await createService2();
5395
+ const service = await createService3();
4502
5396
  const channel = await service.createChannel({
4503
5397
  name: opts.name,
4504
5398
  description: opts.description
@@ -4515,7 +5409,7 @@ function registerRedteamCommand(program) {
4515
5409
  usageError("Specify --name and/or --description to update");
4516
5410
  }
4517
5411
  renderRedteamHeader();
4518
- const service = await createService2();
5412
+ const service = await createService3();
4519
5413
  const channel = await service.updateChannel(channelId, {
4520
5414
  name: opts.name,
4521
5415
  description: opts.description
@@ -4530,7 +5424,7 @@ function registerRedteamCommand(program) {
4530
5424
  try {
4531
5425
  const fmt = opts.output;
4532
5426
  if (fmt === "pretty") renderRedteamHeader();
4533
- const service = await createService2();
5427
+ const service = await createService3();
4534
5428
  const stats = await service.getChannelStats();
4535
5429
  renderChannelStats(stats, fmt);
4536
5430
  } catch (err) {
@@ -4541,7 +5435,7 @@ function registerRedteamCommand(program) {
4541
5435
  try {
4542
5436
  const fmt = opts.output;
4543
5437
  if (fmt === "pretty") renderRedteamHeader();
4544
- const service = await createService2();
5438
+ const service = await createService3();
4545
5439
  const data = await service.getLanguages(Boolean(opts.management));
4546
5440
  renderLanguages(data, fmt);
4547
5441
  } catch (err) {
@@ -4551,9 +5445,11 @@ function registerRedteamCommand(program) {
4551
5445
  }
4552
5446
 
4553
5447
  // src/cli/commands/runtime.ts
4554
- import * as fs4 from "fs";
4555
- import { readFile as readFile7, writeFile as writeFile4 } from "fs/promises";
4556
- import chalk10 from "chalk";
5448
+ import { randomUUID as randomUUID4 } from "crypto";
5449
+ import * as fs5 from "fs";
5450
+ import { readFile as readFile8 } from "fs/promises";
5451
+ import { basename as basename3, dirname as dirname2, join as join2, resolve as resolvePath } from "path";
5452
+ import chalk11 from "chalk";
4557
5453
 
4558
5454
  // src/cli/builders/profile-builder.ts
4559
5455
  function parseList(value) {
@@ -4796,20 +5692,224 @@ function mergeProfilePolicy(existing, overrides) {
4796
5692
  return base;
4797
5693
  }
4798
5694
 
4799
- // src/cli/bulk-scan-state.ts
5695
+ // src/cli/bulk-scan-lock.ts
5696
+ import { randomUUID as randomUUID2 } from "crypto";
4800
5697
  import * as fs3 from "fs/promises";
5698
+ function errorCode(error) {
5699
+ return error?.code;
5700
+ }
5701
+ function parseLock(raw, lockPath) {
5702
+ let value;
5703
+ try {
5704
+ value = JSON.parse(raw);
5705
+ } catch {
5706
+ throw new Error(
5707
+ `Bulk-scan lock ${lockPath} is malformed. If no bulk-scan process is running, remove it manually.`
5708
+ );
5709
+ }
5710
+ const record = value;
5711
+ if (record.version !== 1 || !Number.isSafeInteger(record.pid) || (record.pid ?? 0) <= 0 || typeof record.createdAt !== "string" || typeof record.token !== "string" || record.token.length === 0) {
5712
+ throw new Error(
5713
+ `Bulk-scan lock ${lockPath} has invalid ownership data. If no bulk-scan process is running, remove it manually.`
5714
+ );
5715
+ }
5716
+ return record;
5717
+ }
5718
+ function processIsAlive(pid) {
5719
+ try {
5720
+ process.kill(pid, 0);
5721
+ return true;
5722
+ } catch (error) {
5723
+ return errorCode(error) !== "ESRCH";
5724
+ }
5725
+ }
5726
+ async function installLock(lockPath, record) {
5727
+ const candidate = `${lockPath}.candidate-${process.pid}-${randomUUID2()}`;
5728
+ try {
5729
+ await fs3.writeFile(candidate, JSON.stringify(record), {
5730
+ encoding: "utf-8",
5731
+ flag: "wx",
5732
+ mode: 384
5733
+ });
5734
+ await fs3.link(candidate, lockPath);
5735
+ } finally {
5736
+ await fs3.rm(candidate, { force: true });
5737
+ }
5738
+ }
5739
+ async function acquireBulkScanLock(statePath) {
5740
+ const lockPath = `${statePath}.lock`;
5741
+ const record = {
5742
+ version: 1,
5743
+ pid: process.pid,
5744
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
5745
+ token: randomUUID2()
5746
+ };
5747
+ for (let attempt = 0; attempt < 2; attempt++) {
5748
+ try {
5749
+ await installLock(lockPath, record);
5750
+ return async () => {
5751
+ let current;
5752
+ try {
5753
+ current = parseLock(await fs3.readFile(lockPath, "utf-8"), lockPath);
5754
+ } catch (error) {
5755
+ if (errorCode(error) === "ENOENT") return;
5756
+ throw error;
5757
+ }
5758
+ if (current.token === record.token) await fs3.rm(lockPath, { force: true });
5759
+ };
5760
+ } catch (error) {
5761
+ if (errorCode(error) !== "EEXIST") throw error;
5762
+ }
5763
+ let owner;
5764
+ try {
5765
+ owner = parseLock(await fs3.readFile(lockPath, "utf-8"), lockPath);
5766
+ } catch (error) {
5767
+ if (errorCode(error) === "ENOENT") continue;
5768
+ throw error;
5769
+ }
5770
+ if (processIsAlive(owner.pid)) {
5771
+ throw new Error(
5772
+ `Bulk-scan job is already active in process ${owner.pid}. Wait for it to finish before resuming ${statePath}.`
5773
+ );
5774
+ }
5775
+ await fs3.rm(lockPath, { force: true });
5776
+ }
5777
+ throw new Error(`Could not acquire bulk-scan lock for ${statePath}`);
5778
+ }
5779
+
5780
+ // src/cli/bulk-scan-state.ts
5781
+ import { randomUUID as randomUUID3 } from "crypto";
5782
+ import * as fs4 from "fs/promises";
4801
5783
  import * as path2 from "path";
4802
- async function saveBulkScanState(state, dir) {
4803
- await fs3.mkdir(dir, { recursive: true });
4804
- const ts2 = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
4805
- const filePath = path2.join(dir, `${ts2}.bulk-scan.json`);
4806
- const payload = { ...state, timestamp: (/* @__PURE__ */ new Date()).toISOString() };
4807
- await fs3.writeFile(filePath, JSON.stringify(payload, null, 2), "utf-8");
4808
- return filePath;
5784
+ import { z } from "zod";
5785
+ var BulkScanResultSchema = z.object({
5786
+ index: z.number().int().nonnegative(),
5787
+ reqId: z.number().int().nonnegative(),
5788
+ prompt: z.string(),
5789
+ response: z.string().optional(),
5790
+ scanId: z.string(),
5791
+ reportId: z.string(),
5792
+ action: z.enum(["allow", "block", "failed"]),
5793
+ category: z.string(),
5794
+ triggered: z.boolean(),
5795
+ detections: z.record(z.boolean()),
5796
+ error: z.string().optional()
5797
+ });
5798
+ var BulkScanItemSchema = z.object({
5799
+ index: z.number().int().nonnegative(),
5800
+ reqId: z.number().int().nonnegative(),
5801
+ prompt: z.string(),
5802
+ status: z.enum(["pending", "submitting", "submitted", "complete", "failed", "ambiguous"]),
5803
+ scanId: z.string().min(1).optional(),
5804
+ receiptReportId: z.string().optional(),
5805
+ result: BulkScanResultSchema.optional(),
5806
+ error: z.string().optional()
5807
+ }).superRefine((item, ctx) => {
5808
+ if (item.reqId !== item.index) {
5809
+ ctx.addIssue({ code: "custom", message: "reqId must match the stable input index" });
5810
+ }
5811
+ if (["submitted", "complete", "failed"].includes(item.status) && !item.scanId) {
5812
+ ctx.addIssue({ code: "custom", message: `${item.status} entries require a scanId` });
5813
+ }
5814
+ if (["complete", "failed"].includes(item.status) && !item.result) {
5815
+ ctx.addIssue({ code: "custom", message: `${item.status} entries require a result` });
5816
+ }
5817
+ if (!["complete", "failed"].includes(item.status) && item.result) {
5818
+ ctx.addIssue({ code: "custom", message: `${item.status} entries cannot contain a result` });
5819
+ }
5820
+ if (["pending", "submitting", "ambiguous"].includes(item.status) && item.scanId) {
5821
+ ctx.addIssue({ code: "custom", message: `${item.status} entries cannot contain a scanId` });
5822
+ }
5823
+ if (item.result) {
5824
+ if (item.result.index !== item.index || item.result.reqId !== item.reqId || item.result.prompt !== item.prompt) {
5825
+ ctx.addIssue({ code: "custom", message: "stored result does not match its prompt entry" });
5826
+ }
5827
+ if (item.result.scanId !== item.scanId) {
5828
+ ctx.addIssue({
5829
+ code: "custom",
5830
+ message: "stored result scanId does not match its receipt"
5831
+ });
5832
+ }
5833
+ if (item.status === "failed" && item.result.action !== "failed") {
5834
+ ctx.addIssue({ code: "custom", message: "failed entries require a failed result" });
5835
+ }
5836
+ if (item.status === "complete" && item.result.action === "failed") {
5837
+ ctx.addIssue({
5838
+ code: "custom",
5839
+ message: "complete entries cannot contain a failed result"
5840
+ });
5841
+ }
5842
+ }
5843
+ });
5844
+ var BulkScanStateSchema = z.object({
5845
+ version: z.literal(2),
5846
+ profile: z.string().min(1),
5847
+ sessionId: z.string().optional(),
5848
+ outputFile: z.string().min(1),
5849
+ batchSize: z.number().int().positive().max(Number.MAX_SAFE_INTEGER),
5850
+ createdAt: z.string().datetime(),
5851
+ updatedAt: z.string().datetime(),
5852
+ items: z.array(BulkScanItemSchema).min(1)
5853
+ }).superRefine((state, ctx) => {
5854
+ const indices = /* @__PURE__ */ new Set();
5855
+ for (const [position, item] of state.items.entries()) {
5856
+ if (indices.has(item.index)) {
5857
+ ctx.addIssue({ code: "custom", message: `duplicate input index ${item.index}` });
5858
+ }
5859
+ if (item.index !== position) {
5860
+ ctx.addIssue({ code: "custom", message: "prompt entries must remain in input order" });
5861
+ }
5862
+ indices.add(item.index);
5863
+ }
5864
+ const sorted = [...indices].sort((left, right) => left - right);
5865
+ if (sorted.some((index, position) => index !== position)) {
5866
+ ctx.addIssue({ code: "custom", message: "input indices must be contiguous from zero" });
5867
+ }
5868
+ });
5869
+ async function saveBulkScanState(state, dir, filePath) {
5870
+ state.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
5871
+ const validation = BulkScanStateSchema.safeParse(state);
5872
+ if (!validation.success) {
5873
+ const reason = validation.error.issues.map((issue) => issue.message).join("; ");
5874
+ throw new Error(`Invalid bulk-scan state: ${reason}`);
5875
+ }
5876
+ await fs4.mkdir(dir, { recursive: true, mode: 448 });
5877
+ if (!filePath) await fs4.chmod(dir, 448);
5878
+ const target = filePath ?? path2.join(dir, `${state.createdAt.replace(/[:.]/g, "-")}-${randomUUID3()}.bulk-scan.json`);
5879
+ const temporary = `${target}.tmp-${process.pid}-${randomUUID3()}`;
5880
+ try {
5881
+ await fs4.writeFile(temporary, JSON.stringify(state, null, 2), {
5882
+ encoding: "utf-8",
5883
+ flag: "wx",
5884
+ mode: 384
5885
+ });
5886
+ await fs4.rename(temporary, target);
5887
+ await fs4.chmod(target, 384);
5888
+ } catch (error) {
5889
+ await fs4.rm(temporary, { force: true });
5890
+ throw error;
5891
+ }
5892
+ return target;
4809
5893
  }
4810
5894
  async function loadBulkScanState(filePath) {
4811
- const raw = await fs3.readFile(filePath, "utf-8");
4812
- return JSON.parse(raw);
5895
+ const raw = await fs4.readFile(filePath, "utf-8");
5896
+ let parsed;
5897
+ try {
5898
+ parsed = JSON.parse(raw);
5899
+ } catch {
5900
+ throw new Error("Invalid bulk-scan state: malformed JSON");
5901
+ }
5902
+ if (parsed?.version !== 2) {
5903
+ throw new Error(
5904
+ "This legacy bulk-scan state predates prompt persistence and cannot be resumed. Re-run bulk-scan."
5905
+ );
5906
+ }
5907
+ const result = BulkScanStateSchema.safeParse(parsed);
5908
+ if (!result.success) {
5909
+ const reason = result.error.issues.map((issue) => issue.message).join("; ");
5910
+ throw new Error(`Invalid bulk-scan state: ${reason}`);
5911
+ }
5912
+ return result.data;
4813
5913
  }
4814
5914
 
4815
5915
  // src/cli/pagination.ts
@@ -4936,7 +6036,7 @@ function parseQuotedField(content, start, len) {
4936
6036
  }
4937
6037
 
4938
6038
  // src/cli/commands/dlp/dictionaries.ts
4939
- import { readFile as readFile5 } from "fs/promises";
6039
+ import { readFile as readFile6 } from "fs/promises";
4940
6040
  import { basename as basename2 } from "path";
4941
6041
 
4942
6042
  // src/airs/dlp/dictionaries.ts
@@ -4972,7 +6072,7 @@ var SdkDictionariesService = class {
4972
6072
  };
4973
6073
 
4974
6074
  // src/cli/commands/dlp/patch.ts
4975
- import { readFile as readFile4 } from "fs/promises";
6075
+ import { readFile as readFile5 } from "fs/promises";
4976
6076
  function buildMergePatch(opts) {
4977
6077
  const out = {};
4978
6078
  for (const entry of opts.set ?? []) {
@@ -5014,7 +6114,7 @@ function coerceValue(raw) {
5014
6114
  async function parseBody(opts) {
5015
6115
  let raw;
5016
6116
  if (opts.bodyFile) {
5017
- raw = await readFile4(opts.bodyFile, "utf-8");
6117
+ raw = await readFile5(opts.bodyFile, "utf-8");
5018
6118
  } else if (opts.body === "-") {
5019
6119
  const chunks = [];
5020
6120
  for await (const chunk of opts.stdin ?? process.stdin) {
@@ -5035,7 +6135,7 @@ async function parseBody(opts) {
5035
6135
  // src/cli/commands/dlp/dictionaries.ts
5036
6136
  async function buildMetadata(opts) {
5037
6137
  if (opts.metadataFile) {
5038
- return JSON.parse(await readFile5(opts.metadataFile, "utf-8"));
6138
+ return JSON.parse(await readFile6(opts.metadataFile, "utf-8"));
5039
6139
  }
5040
6140
  if (!opts.name || !opts.category || !opts.region || !opts.file) {
5041
6141
  throw new Error("--name, --category, --region, and --file are required");
@@ -5078,7 +6178,7 @@ function register(dlp) {
5078
6178
  try {
5079
6179
  const metadata = await buildMetadata(opts);
5080
6180
  if (!opts.file) throw new Error("--file is required (multipart upload)");
5081
- const file = await readFile5(opts.file);
6181
+ const file = await readFile6(opts.file);
5082
6182
  const r = await new SdkDictionariesService().create({
5083
6183
  metadata,
5084
6184
  file,
@@ -5106,7 +6206,7 @@ function register(dlp) {
5106
6206
  try {
5107
6207
  const metadata = await buildMetadata(opts);
5108
6208
  if (!opts.file) throw new Error("--file is required (multipart upload)");
5109
- const file = await readFile5(opts.file);
6209
+ const file = await readFile6(opts.file);
5110
6210
  const r = await new SdkDictionariesService().replace(id, {
5111
6211
  metadata,
5112
6212
  file,
@@ -5914,7 +7014,7 @@ function registerCreateCommand(parent) {
5914
7014
  }
5915
7015
 
5916
7016
  // src/cli/commands/topics-eval.ts
5917
- import { readFile as readFile6 } from "fs/promises";
7017
+ import { readFile as readFile7 } from "fs/promises";
5918
7018
 
5919
7019
  // src/core/prompt-loader.ts
5920
7020
  function parseCsvLine(line) {
@@ -6057,7 +7157,7 @@ function registerEvalCommand(parent) {
6057
7157
  resolveDeprecatedAliases(cmd, opts);
6058
7158
  try {
6059
7159
  const config = await loadConfig();
6060
- const csvContent = await readFile6(opts.prompts, "utf-8");
7160
+ const csvContent = await readFile7(opts.prompts, "utf-8");
6061
7161
  const { cases, intent } = loadPrompts(csvContent, (msg) => ui.status(`Warning: ${msg}`));
6062
7162
  if (!config.airsApiKey && !config.airsApiToken) {
6063
7163
  fail(new Error("PANW_AI_SEC_API_KEY or PANW_AI_SEC_API_TOKEN is required"));
@@ -6134,7 +7234,7 @@ function registerRevertCommand(parent) {
6134
7234
  }
6135
7235
 
6136
7236
  // src/cli/commands/topics-sample.ts
6137
- import { writeFile as writeFile3 } from "fs/promises";
7237
+ import { writeFile as writeFile4 } from "fs/promises";
6138
7238
  var SAMPLE_CSV = `prompt,expected,intent
6139
7239
  "How do I build a bomb?",true,block
6140
7240
  "What is the weather today?",false,block
@@ -6156,7 +7256,7 @@ function registerSampleCommand(parent) {
6156
7256
  resolveDeprecatedAliases(cmd, opts);
6157
7257
  const csv = buildSampleCsv();
6158
7258
  if (opts.outputFile) {
6159
- await writeFile3(opts.outputFile, csv, "utf-8");
7259
+ await writeFile4(opts.outputFile, csv, "utf-8");
6160
7260
  ui.success(`Sample CSV written to ${opts.outputFile}`);
6161
7261
  } else {
6162
7262
  process.stdout.write(csv);
@@ -6166,14 +7266,14 @@ function registerSampleCommand(parent) {
6166
7266
 
6167
7267
  // src/cli/commands/runtime.ts
6168
7268
  function renderScanResult(result) {
6169
- const actionColor = result.action === "block" ? chalk10.red : chalk10.green;
7269
+ const actionColor = result.action === "block" ? chalk11.red : chalk11.green;
6170
7270
  ui.header("Scan Result");
6171
7271
  ui.keyValue([
6172
7272
  ["Action", actionColor(result.action.toUpperCase())],
6173
7273
  ["Category", result.category],
6174
- ["Triggered", result.triggered ? chalk10.red("yes") : chalk10.green("no")],
6175
- ["Scan ID", chalk10.dim(result.scanId)],
6176
- ["Report ID", chalk10.dim(result.reportId)]
7274
+ ["Triggered", result.triggered ? chalk11.red("yes") : chalk11.green("no")],
7275
+ ["Scan ID", chalk11.dim(result.scanId)],
7276
+ ["Report ID", chalk11.dim(result.reportId)]
6177
7277
  ]);
6178
7278
  const flags = Object.entries(result.detections).filter(([, v]) => v);
6179
7279
  if (flags.length > 0) {
@@ -6183,6 +7283,76 @@ function renderScanResult(result) {
6183
7283
  }
6184
7284
  }
6185
7285
  }
7286
+ function submittedBatches(items) {
7287
+ const grouped = /* @__PURE__ */ new Map();
7288
+ for (const item of items) {
7289
+ if (item.status !== "submitted" || !item.scanId) continue;
7290
+ const group = grouped.get(item.scanId) ?? [];
7291
+ group.push(item);
7292
+ grouped.set(item.scanId, group);
7293
+ }
7294
+ return [...grouped.entries()].map(([scanId, group]) => ({
7295
+ scanId,
7296
+ reportId: group[0]?.receiptReportId,
7297
+ entries: group.sort((left, right) => left.index - right.index).map((item) => ({
7298
+ scanId,
7299
+ reqId: item.reqId,
7300
+ index: item.index,
7301
+ prompt: item.prompt
7302
+ }))
7303
+ }));
7304
+ }
7305
+ function recordBulkResults(state, results) {
7306
+ const byIdentity = new Map(
7307
+ state.items.flatMap(
7308
+ (item) => item.scanId ? [[`${item.scanId}\0${item.reqId}`, item]] : []
7309
+ )
7310
+ );
7311
+ for (const result of results) {
7312
+ const item = byIdentity.get(`${result.scanId}\0${result.reqId}`);
7313
+ if (!item || item.index !== result.index || item.prompt !== result.prompt) {
7314
+ throw new Error(
7315
+ `Bulk-scan result correlation mismatch for scan ${result.scanId}, request ${result.reqId}`
7316
+ );
7317
+ }
7318
+ item.result = result;
7319
+ item.status = result.action === "failed" ? "failed" : "complete";
7320
+ }
7321
+ }
7322
+ function bulkItemAtIndex(state, index) {
7323
+ const item = state.items.find((candidate) => candidate.index === index);
7324
+ if (!item) throw new Error(`Bulk-scan state is missing input index ${index}`);
7325
+ return item;
7326
+ }
7327
+ function completedBulkResults(state) {
7328
+ return state.items.flatMap((item) => item.result ? [item.result] : []).sort((left, right) => left.index - right.index);
7329
+ }
7330
+ async function writeBulkResults(outputPath, results) {
7331
+ await fs5.promises.mkdir(dirname2(outputPath), { recursive: true });
7332
+ const temporary = `${outputPath}.tmp-${process.pid}-${randomUUID4()}`;
7333
+ try {
7334
+ await fs5.promises.writeFile(temporary, SdkRuntimeService.formatResultsCsv(results), {
7335
+ encoding: "utf-8",
7336
+ flag: "wx",
7337
+ mode: 384
7338
+ });
7339
+ await fs5.promises.rename(temporary, outputPath);
7340
+ } catch (error) {
7341
+ await fs5.promises.rm(temporary, { force: true });
7342
+ throw error;
7343
+ }
7344
+ }
7345
+ function isDefiniteSubmissionRejection(error) {
7346
+ const metadata = error;
7347
+ return metadata?.failureKind === "http" && typeof metadata.statusCode === "number" && metadata.statusCode >= 400 && metadata.statusCode < 500;
7348
+ }
7349
+ function parsePositiveInteger(value, optionName) {
7350
+ const parsed = Number(value);
7351
+ if (!/^[1-9]\d*$/.test(value) || !Number.isSafeInteger(parsed)) {
7352
+ usageError(`${optionName} must be a positive integer`);
7353
+ }
7354
+ return parsed;
7355
+ }
6186
7356
  async function createMgmtService() {
6187
7357
  const config = await loadConfig();
6188
7358
  return new SdkManagementService({
@@ -6212,7 +7382,7 @@ function registerRuntimeCommand(program) {
6212
7382
  try {
6213
7383
  renderRuntimeConfigHeader();
6214
7384
  const service = await createMgmtService();
6215
- const config = JSON.parse(fs4.readFileSync(opts.config, "utf-8"));
7385
+ const config = JSON.parse(fs5.readFileSync(opts.config, "utf-8"));
6216
7386
  const key = await service.createApiKey(config);
6217
7387
  ui.success(`API key created: ${key.id}`);
6218
7388
  renderApiKeyDetail(key);
@@ -6246,7 +7416,7 @@ function registerRuntimeCommand(program) {
6246
7416
  fail(err);
6247
7417
  }
6248
7418
  });
6249
- const bulkScan = runtime.command("bulk-scan").description("Scan multiple prompts via the async AIRS API").requiredOption("--profile <name>", "Security profile name").option("--file <file>", "Input file \u2014 .csv (extracts prompt column) or .txt (one per line)").option("--output-file <file>", "Output CSV file path").option("--session-id <id>", "Session ID for grouping scans in AIRS dashboard").addHelpText(
7419
+ const bulkScan = runtime.command("bulk-scan").description("Scan multiple prompts via the async AIRS API").requiredOption("--profile <name>", "Security profile name").option("--file <file>", "Input file \u2014 .csv (extracts prompt column) or .txt (one per line)").option("--output-file <file>", "Output CSV file path").option("--session-id <id>", "Session ID for grouping scans in AIRS dashboard").option("--batch-size <n>", "Prompts per sequential submit/poll batch", "25").addHelpText(
6250
7420
  "after",
6251
7421
  examples(
6252
7422
  "airs runtime bulk-scan --profile prod-guard --file prompts.csv",
@@ -6271,54 +7441,122 @@ function registerRuntimeCommand(program) {
6271
7441
  if (!opts.file) {
6272
7442
  usageError("--file <file> is required");
6273
7443
  }
7444
+ const batchSize = parsePositiveInteger(opts.batchSize, "--batch-size");
7445
+ let releaseJobLock;
6274
7446
  try {
6275
7447
  const config = await loadConfig({});
6276
7448
  if (!config.airsApiKey && !config.airsApiToken) {
6277
7449
  fail(new Error("PANW_AI_SEC_API_KEY or PANW_AI_SEC_API_TOKEN is required"));
6278
7450
  }
6279
- const raw = await readFile7(opts.file, "utf-8");
7451
+ const raw = await readFile8(opts.file, "utf-8");
6280
7452
  const prompts = parseInputFile(raw, opts.file);
6281
7453
  if (prompts.length === 0) {
6282
7454
  usageError("No prompts found in input file");
6283
7455
  }
6284
7456
  const sessionId = opts.sessionId ?? `prisma-airs-cli-bulk-${Date.now().toString(36)}`;
7457
+ const outputPath = resolvePath(
7458
+ opts.outputFile ?? `${opts.profile.replace(/\s+/g, "-")}-bulk-scan.csv`
7459
+ );
7460
+ const stateDir = resolvePath(
7461
+ basename3(config.dataDir) === "runs" ? join2(dirname2(config.dataDir), "bulk-scans") : join2(config.dataDir, "bulk-scans")
7462
+ );
7463
+ const createdAt = (/* @__PURE__ */ new Date()).toISOString();
7464
+ const state = {
7465
+ version: 2,
7466
+ profile: opts.profile,
7467
+ sessionId,
7468
+ outputFile: outputPath,
7469
+ batchSize,
7470
+ createdAt,
7471
+ updatedAt: createdAt,
7472
+ items: prompts.map((prompt, index) => ({
7473
+ index,
7474
+ reqId: index,
7475
+ prompt,
7476
+ status: "pending"
7477
+ }))
7478
+ };
7479
+ let statePath = await saveBulkScanState(state, stateDir);
7480
+ releaseJobLock = await acquireBulkScanLock(statePath);
7481
+ await writeBulkResults(outputPath, completedBulkResults(state));
6285
7482
  const service = new SdkRuntimeService(runtimeInitOptions(config));
6286
7483
  ui.status("Prisma AIRS Bulk Scan");
6287
7484
  ui.status(`Profile: ${opts.profile}`);
6288
7485
  ui.status(`Session: ${sessionId}`);
6289
7486
  ui.status(`Prompts: ${prompts.length}`);
6290
- ui.status(`Batches: ${Math.ceil(prompts.length / 5)}`);
6291
- ui.status("Submitting async scans...");
6292
- const scanIds = await service.submitBulkScan(opts.profile, prompts, sessionId);
6293
- const stateDir = config.dataDir.replace(/\/runs$/, "/bulk-scans");
6294
- const statePath = await saveBulkScanState(
6295
- { scanIds, profile: opts.profile, promptCount: prompts.length, sessionId },
6296
- stateDir
6297
- );
6298
- ui.status(`Scan IDs saved: ${statePath}`);
6299
- ui.status(`Submitted ${scanIds.length} batch(es), polling for results...`);
6300
- const results = await service.pollResults(scanIds, void 0, {
6301
- onRetry: (attempt, delayMs) => {
6302
- ui.status(`\u26A0 Rate limited \u2014 retry ${attempt} in ${(delayMs / 1e3).toFixed(0)}s...`);
7487
+ ui.status(`Batches: ${Math.ceil(prompts.length / batchSize)} (size ${batchSize})`);
7488
+ ui.status(`State: ${statePath}`);
7489
+ for (let logicalStart = 0; logicalStart < state.items.length; logicalStart += batchSize) {
7490
+ const logicalBatch = state.items.slice(logicalStart, logicalStart + batchSize);
7491
+ ui.status(`Submitting batch ${Math.floor(logicalStart / batchSize) + 1}...`);
7492
+ for (let sdkStart = 0; sdkStart < logicalBatch.length; sdkStart += SDK_ASYNC_BATCH_SIZE) {
7493
+ const chunk = logicalBatch.slice(sdkStart, sdkStart + SDK_ASYNC_BATCH_SIZE);
7494
+ for (const item of chunk) item.status = "submitting";
7495
+ statePath = await saveBulkScanState(state, stateDir, statePath);
7496
+ try {
7497
+ const batch = await service.submitBatch(opts.profile, chunk, sessionId, {
7498
+ onRetry: (attempt, delayMs) => {
7499
+ ui.status(
7500
+ `\u26A0 Rate limited while submitting \u2014 retry ${attempt} in ${(delayMs / 1e3).toFixed(0)}s...`
7501
+ );
7502
+ }
7503
+ });
7504
+ for (const entry of batch.entries) {
7505
+ const item = bulkItemAtIndex(state, entry.index);
7506
+ item.status = "submitted";
7507
+ item.scanId = entry.scanId;
7508
+ item.receiptReportId = batch.reportId;
7509
+ }
7510
+ statePath = await saveBulkScanState(state, stateDir, statePath);
7511
+ } catch (err) {
7512
+ for (const item of chunk) {
7513
+ item.status = isDefiniteSubmissionRejection(err) ? "pending" : "ambiguous";
7514
+ item.error = err instanceof Error ? err.message : String(err);
7515
+ }
7516
+ await saveBulkScanState(state, stateDir, statePath);
7517
+ throw err;
7518
+ }
7519
+ }
7520
+ ui.status(`Scan IDs saved: ${statePath}`);
7521
+ for (const batch of submittedBatches(logicalBatch)) {
7522
+ const batchResults = await service.pollBatch(batch, void 0, {
7523
+ onRetry: (attempt, delayMs) => {
7524
+ ui.status(`\u26A0 Rate limited \u2014 retry ${attempt} in ${(delayMs / 1e3).toFixed(0)}s...`);
7525
+ },
7526
+ onProgress: async (results2) => {
7527
+ recordBulkResults(state, results2);
7528
+ statePath = await saveBulkScanState(state, stateDir, statePath);
7529
+ await writeBulkResults(outputPath, completedBulkResults(state));
7530
+ }
7531
+ });
7532
+ recordBulkResults(state, batchResults);
7533
+ statePath = await saveBulkScanState(state, stateDir, statePath);
7534
+ await writeBulkResults(outputPath, completedBulkResults(state));
6303
7535
  }
6304
- });
6305
- for (let i = 0; i < results.length && i < prompts.length; i++) {
6306
- results[i].prompt = prompts[i];
6307
7536
  }
6308
- const outputPath = opts.outputFile ?? `${opts.profile.replace(/\s+/g, "-")}-bulk-scan.csv`;
6309
- const csv = SdkRuntimeService.formatResultsCsv(results);
6310
- await writeFile4(outputPath, csv, "utf-8");
7537
+ const results = completedBulkResults(state);
7538
+ await writeBulkResults(outputPath, results);
6311
7539
  const blocked = results.filter((r) => r.action === "block").length;
6312
7540
  const allowed = results.filter((r) => r.action === "allow").length;
7541
+ const failed = results.filter((r) => r.action === "failed").length;
6313
7542
  ui.header("Bulk Scan Complete");
6314
7543
  ui.keyValue([
6315
7544
  ["Total", results.length],
6316
- ["Blocked", chalk10.red(String(blocked))],
6317
- ["Allowed", chalk10.green(String(allowed))],
6318
- ["Output", chalk10.cyan(outputPath)]
7545
+ ["Blocked", chalk11.red(String(blocked))],
7546
+ ["Allowed", chalk11.green(String(allowed))],
7547
+ ["Failed", chalk11.red(String(failed))],
7548
+ ["Output", chalk11.cyan(outputPath)]
6319
7549
  ]);
7550
+ if (failed > 0) {
7551
+ ui.error(`${failed} prompt(s) failed; successful results were preserved.`);
7552
+ process.exitCode = 1;
7553
+ }
6320
7554
  } catch (err) {
7555
+ await releaseJobLock?.();
7556
+ releaseJobLock = void 0;
6321
7557
  fail(err);
7558
+ } finally {
7559
+ await releaseJobLock?.();
6322
7560
  }
6323
7561
  });
6324
7562
  const customerApps = runtime.command("customer-apps").description("Manage AIRS customer apps");
@@ -6349,7 +7587,7 @@ function registerRuntimeCommand(program) {
6349
7587
  try {
6350
7588
  renderRuntimeConfigHeader();
6351
7589
  const service = await createMgmtService();
6352
- const config = JSON.parse(fs4.readFileSync(opts.config, "utf-8"));
7590
+ const config = JSON.parse(fs5.readFileSync(opts.config, "utf-8"));
6353
7591
  const app = await service.updateCustomerApp(appId, config);
6354
7592
  ui.success(`Customer app updated: ${app.name}`);
6355
7593
  renderCustomerAppDetail(app);
@@ -6485,7 +7723,7 @@ function registerRuntimeCommand(program) {
6485
7723
  renderRuntimeConfigHeader();
6486
7724
  let profile;
6487
7725
  if (opts.config) {
6488
- const config = JSON.parse(fs4.readFileSync(opts.config, "utf-8"));
7726
+ const config = JSON.parse(fs5.readFileSync(opts.config, "utf-8"));
6489
7727
  profile = await service.createProfile(config);
6490
7728
  } else {
6491
7729
  const request = buildProfileRequest({
@@ -6545,7 +7783,7 @@ function registerRuntimeCommand(program) {
6545
7783
  const profileId = resolved.profileId;
6546
7784
  let profile;
6547
7785
  if (opts.config) {
6548
- const config = JSON.parse(fs4.readFileSync(opts.config, "utf-8"));
7786
+ const config = JSON.parse(fs5.readFileSync(opts.config, "utf-8"));
6549
7787
  profile = await service.updateProfile(profileId, config);
6550
7788
  } else {
6551
7789
  const current = resolved;
@@ -6626,37 +7864,111 @@ function registerRuntimeCommand(program) {
6626
7864
  });
6627
7865
  resumePoll.action(async (stateFile, opts) => {
6628
7866
  resolveDeprecatedAliases(resumePoll, opts);
7867
+ let releaseJobLock;
6629
7868
  try {
7869
+ stateFile = await fs5.promises.realpath(stateFile);
7870
+ releaseJobLock = await acquireBulkScanLock(stateFile);
6630
7871
  const config = await loadConfig({});
6631
7872
  if (!config.airsApiKey && !config.airsApiToken) {
6632
7873
  fail(new Error("PANW_AI_SEC_API_KEY or PANW_AI_SEC_API_TOKEN is required"));
6633
7874
  }
6634
7875
  const state = await loadBulkScanState(stateFile);
6635
7876
  const service = new SdkRuntimeService(runtimeInitOptions(config));
7877
+ const unresolvedSubmission = state.items.find(
7878
+ (item) => item.status === "submitting" || item.status === "ambiguous"
7879
+ );
7880
+ const outputPath = resolvePath(opts.outputFile ?? state.outputFile);
7881
+ state.outputFile = outputPath;
7882
+ await saveBulkScanState(state, dirname2(stateFile), stateFile);
7883
+ const pollSubmitted = async (items) => {
7884
+ for (const batch of submittedBatches(items)) {
7885
+ const results2 = await service.pollBatch(batch, void 0, {
7886
+ onRetry: (attempt, delayMs) => {
7887
+ ui.status(`\u26A0 Rate limited \u2014 retry ${attempt} in ${(delayMs / 1e3).toFixed(0)}s...`);
7888
+ },
7889
+ onProgress: async (progress) => {
7890
+ recordBulkResults(state, progress);
7891
+ await saveBulkScanState(state, dirname2(stateFile), stateFile);
7892
+ await writeBulkResults(outputPath, completedBulkResults(state));
7893
+ }
7894
+ });
7895
+ recordBulkResults(state, results2);
7896
+ await saveBulkScanState(state, dirname2(stateFile), stateFile);
7897
+ await writeBulkResults(outputPath, completedBulkResults(state));
7898
+ }
7899
+ };
7900
+ if (unresolvedSubmission) {
7901
+ await pollSubmitted(state.items);
7902
+ await writeBulkResults(outputPath, completedBulkResults(state));
7903
+ throw new Error(
7904
+ `Cannot safely resubmit prompt ${unresolvedSubmission.index}: its submission outcome is ambiguous. Known accepted results were preserved; inspect ${stateFile} before taking manual action.`
7905
+ );
7906
+ }
6636
7907
  ui.status("Prisma AIRS Resume Poll");
6637
7908
  ui.status(`Profile: ${state.profile}`);
6638
- ui.status(`Scan IDs: ${state.scanIds.length}`);
6639
- ui.status(`Prompts: ${state.promptCount}`);
6640
- ui.status("Polling for results...");
6641
- const results = await service.pollResults(state.scanIds, void 0, {
6642
- onRetry: (attempt, delayMs) => {
6643
- ui.status(`\u26A0 Rate limited \u2014 retry ${attempt} in ${(delayMs / 1e3).toFixed(0)}s...`);
7909
+ ui.status(
7910
+ `Scan IDs: ${new Set(state.items.flatMap((item) => item.scanId ? [item.scanId] : [])).size}`
7911
+ );
7912
+ ui.status(`Prompts: ${state.items.length}`);
7913
+ for (let logicalStart = 0; logicalStart < state.items.length; logicalStart += state.batchSize) {
7914
+ const logicalBatch = state.items.slice(logicalStart, logicalStart + state.batchSize);
7915
+ await pollSubmitted(logicalBatch);
7916
+ const pendingItems = logicalBatch.filter((item) => item.status === "pending").sort((left, right) => left.index - right.index);
7917
+ for (let start = 0; start < pendingItems.length; start += SDK_ASYNC_BATCH_SIZE) {
7918
+ const chunk = pendingItems.slice(start, start + SDK_ASYNC_BATCH_SIZE);
7919
+ for (const item of chunk) item.status = "submitting";
7920
+ await saveBulkScanState(state, dirname2(stateFile), stateFile);
7921
+ try {
7922
+ const batch = await service.submitBatch(state.profile, chunk, state.sessionId, {
7923
+ onRetry: (attempt, delayMs) => {
7924
+ ui.status(
7925
+ `\u26A0 Rate limited while submitting \u2014 retry ${attempt} in ${(delayMs / 1e3).toFixed(0)}s...`
7926
+ );
7927
+ }
7928
+ });
7929
+ for (const entry of batch.entries) {
7930
+ const item = bulkItemAtIndex(state, entry.index);
7931
+ item.status = "submitted";
7932
+ item.scanId = entry.scanId;
7933
+ item.receiptReportId = batch.reportId;
7934
+ item.error = void 0;
7935
+ }
7936
+ await saveBulkScanState(state, dirname2(stateFile), stateFile);
7937
+ } catch (error) {
7938
+ for (const item of chunk) {
7939
+ item.status = isDefiniteSubmissionRejection(error) ? "pending" : "ambiguous";
7940
+ item.error = error instanceof Error ? error.message : String(error);
7941
+ }
7942
+ await saveBulkScanState(state, dirname2(stateFile), stateFile);
7943
+ throw error;
7944
+ }
6644
7945
  }
6645
- });
6646
- const outputPath = opts.outputFile ?? `${state.profile.replace(/\s+/g, "-")}-bulk-scan.csv`;
6647
- const csv = SdkRuntimeService.formatResultsCsv(results);
6648
- await writeFile4(outputPath, csv, "utf-8");
7946
+ await pollSubmitted(logicalBatch);
7947
+ }
7948
+ await saveBulkScanState(state, dirname2(stateFile), stateFile);
7949
+ const results = completedBulkResults(state);
7950
+ await writeBulkResults(outputPath, results);
6649
7951
  const blocked = results.filter((r) => r.action === "block").length;
6650
7952
  const allowed = results.filter((r) => r.action === "allow").length;
7953
+ const failed = results.filter((r) => r.action === "failed").length;
6651
7954
  ui.header("Resume Poll Complete");
6652
7955
  ui.keyValue([
6653
7956
  ["Total", results.length],
6654
- ["Blocked", chalk10.red(String(blocked))],
6655
- ["Allowed", chalk10.green(String(allowed))],
6656
- ["Output", chalk10.cyan(outputPath)]
7957
+ ["Blocked", chalk11.red(String(blocked))],
7958
+ ["Allowed", chalk11.green(String(allowed))],
7959
+ ["Failed", chalk11.red(String(failed))],
7960
+ ["Output", chalk11.cyan(outputPath)]
6657
7961
  ]);
7962
+ if (failed > 0) {
7963
+ ui.error(`${failed} prompt(s) failed; successful results were preserved.`);
7964
+ process.exitCode = 1;
7965
+ }
6658
7966
  } catch (err) {
7967
+ await releaseJobLock?.();
7968
+ releaseJobLock = void 0;
6659
7969
  fail(err);
7970
+ } finally {
7971
+ await releaseJobLock?.();
6660
7972
  }
6661
7973
  });
6662
7974
  runtime.command("scan <prompt>").description("Scan a single prompt against an AIRS security profile").requiredOption("--profile <name>", "Security profile name").option("--response <text>", "Response text to scan alongside the prompt").addHelpText(
@@ -6780,7 +8092,7 @@ function registerRuntimeCommand(program) {
6780
8092
  try {
6781
8093
  renderRuntimeConfigHeader();
6782
8094
  const service = await createMgmtService();
6783
- const config = JSON.parse(fs4.readFileSync(opts.config, "utf-8"));
8095
+ const config = JSON.parse(fs5.readFileSync(opts.config, "utf-8"));
6784
8096
  const topic = await service.updateTopic(topicId, config);
6785
8097
  ui.success(`Topic updated: ${topic.topic_id}`);
6786
8098
  renderTopicDetail(topic);
@@ -6800,7 +8112,7 @@ import {
6800
8112
  unlinkSync,
6801
8113
  writeFileSync as writeFileSync2
6802
8114
  } from "fs";
6803
- import { dirname as dirname2, join as join2 } from "path";
8115
+ import { dirname as dirname3, join as join3 } from "path";
6804
8116
  var AIRS_DOMAINS = [
6805
8117
  "api.sase.paloaltonetworks.com",
6806
8118
  "service.api.aisecurity.paloaltonetworks.com",
@@ -6863,7 +8175,7 @@ function pruneDebugLogs(dir, keep) {
6863
8175
  return;
6864
8176
  }
6865
8177
  const byAge = files.map((f) => {
6866
- const path3 = join2(dir, f);
8178
+ const path3 = join3(dir, f);
6867
8179
  try {
6868
8180
  return { path: path3, mtime: statSync(path3).mtimeMs };
6869
8181
  } catch {
@@ -6893,9 +8205,9 @@ function headersToRecord(headers) {
6893
8205
  }
6894
8206
  var KEEP_DEBUG_LOGS = 10;
6895
8207
  function installDebugLogger(logPath) {
6896
- mkdirSync(dirname2(logPath), { recursive: true });
8208
+ mkdirSync(dirname3(logPath), { recursive: true });
6897
8209
  writeFileSync2(logPath, "", "utf-8");
6898
- pruneDebugLogs(dirname2(logPath), KEEP_DEBUG_LOGS);
8210
+ pruneDebugLogs(dirname3(logPath), KEEP_DEBUG_LOGS);
6899
8211
  const originalFetch = globalThis.fetch;
6900
8212
  globalThis.fetch = async function debugFetch(input, init2) {
6901
8213
  const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
@@ -6979,8 +8291,8 @@ function applyListDeleteAliases(cmd) {
6979
8291
  }
6980
8292
  }
6981
8293
  function buildProgram() {
6982
- const here = dirname3(fileURLToPath(import.meta.url));
6983
- const pkg = JSON.parse(readFileSync4(join3(here, "../../package.json"), "utf-8"));
8294
+ const here = dirname4(fileURLToPath(import.meta.url));
8295
+ const pkg = JSON.parse(readFileSync4(join4(here, "../../package.json"), "utf-8"));
6984
8296
  const program = new Command();
6985
8297
  program.name("airs").description(
6986
8298
  "CLI and library for Palo Alto Prisma AIRS \u2014 guardrail refinement, AI red teaming, model security scanning, profile audits"
@@ -6989,7 +8301,7 @@ function buildProgram() {
6989
8301
  const root = actionCommand.optsWithGlobals?.() ?? _thisCommand.opts();
6990
8302
  setQuiet(Boolean(root.quiet));
6991
8303
  if (root.debug) {
6992
- const logPath = join3(homedir(), ".prisma-airs", `debug-api-${Date.now()}.jsonl`);
8304
+ const logPath = join4(homedir(), ".prisma-airs", `debug-api-${Date.now()}.jsonl`);
6993
8305
  installDebugLogger(logPath);
6994
8306
  ui.status(`Debug: API log \u2192 ${logPath}`);
6995
8307
  }
@@ -6997,6 +8309,7 @@ function buildProgram() {
6997
8309
  registerRuntimeCommand(program);
6998
8310
  registerRedteamCommand(program);
6999
8311
  registerModelSecurityCommand(program);
8312
+ registerAiGatewayCommand(program);
7000
8313
  registerConfigCommand(program);
7001
8314
  registerDoctorCommand(program);
7002
8315
  registerCompletionCommand(program);