@cdot65/prisma-airs-cli 7.1.3 → 7.1.5
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 +47 -85
- package/package.json +3 -2
package/dist/cli/index.js
CHANGED
|
@@ -6289,61 +6289,10 @@ function isDict(value) {
|
|
|
6289
6289
|
function digest(text2) {
|
|
6290
6290
|
return createHash("sha256").update(text2, "utf8").digest("hex").slice(0, 16);
|
|
6291
6291
|
}
|
|
6292
|
-
function pythonJson(value) {
|
|
6293
|
-
if (Array.isArray(value)) return `[${value.map(pythonJson).join(", ")}]`;
|
|
6294
|
-
if (isDict(value))
|
|
6295
|
-
return `{${Object.entries(value).filter(([, v]) => v !== void 0).map(([k, v]) => `${JSON.stringify(k)}: ${pythonJson(v)}`).join(", ")}}`;
|
|
6296
|
-
return JSON.stringify(value ?? null);
|
|
6297
|
-
}
|
|
6298
|
-
function textFromContentList(content) {
|
|
6299
|
-
if (!Array.isArray(content)) return null;
|
|
6300
|
-
const parts = content.filter((c) => isDict(c) && typeof c.text === "string").map((c) => c.text);
|
|
6301
|
-
return parts.length ? parts.join("\n") : null;
|
|
6302
|
-
}
|
|
6303
6292
|
function extractResponseText(raw) {
|
|
6304
6293
|
if (raw === null || raw === void 0) return ["", "empty"];
|
|
6305
|
-
|
|
6306
|
-
|
|
6307
|
-
if (typeof raw === "string") {
|
|
6308
|
-
const stripped = raw.trim();
|
|
6309
|
-
if (!stripped) return ["", "empty"];
|
|
6310
|
-
if (stripped[0] !== "{" && stripped[0] !== "[") return [raw, "plain"];
|
|
6311
|
-
[value, method] = decodeContainer(stripped);
|
|
6312
|
-
if (method === "plain") return [raw, "plain"];
|
|
6313
|
-
}
|
|
6314
|
-
const message = messageText(value);
|
|
6315
|
-
if (message !== null) return message;
|
|
6316
|
-
if (isDict(value)) {
|
|
6317
|
-
const outputs = value.output;
|
|
6318
|
-
if (Array.isArray(outputs)) {
|
|
6319
|
-
const texts = outputs.map((item) => isDict(item) ? textFromContentList(item.content) : null).filter((text2) => Boolean(text2));
|
|
6320
|
-
if (texts.length) return [texts.join("\n"), "responses_api"];
|
|
6321
|
-
}
|
|
6322
|
-
const choices = value.choices;
|
|
6323
|
-
if (Array.isArray(choices) && choices.length && isDict(choices[0])) {
|
|
6324
|
-
const message2 = choices[0].message;
|
|
6325
|
-
if (isDict(message2) && typeof message2.content === "string")
|
|
6326
|
-
return [message2.content, "chat_completions"];
|
|
6327
|
-
}
|
|
6328
|
-
for (const key of ["output_text", "response", "text", "content", "message", "answer"]) {
|
|
6329
|
-
if (typeof value[key] === "string") return [value[key], `json.${key}`];
|
|
6330
|
-
}
|
|
6331
|
-
const turns = value.messages || value.turns || value.conversation;
|
|
6332
|
-
if (Array.isArray(turns) && turns.length) {
|
|
6333
|
-
const last = turns[turns.length - 1];
|
|
6334
|
-
if (isDict(last)) {
|
|
6335
|
-
const text2 = typeof last.content === "string" ? last.content : textFromContentList(last.content);
|
|
6336
|
-
if (text2) return [text2, "multi_turn_last"];
|
|
6337
|
-
}
|
|
6338
|
-
}
|
|
6339
|
-
return [pythonJson(value), `${method}_unparsed`];
|
|
6340
|
-
}
|
|
6341
|
-
if (Array.isArray(value)) {
|
|
6342
|
-
const texts = value.filter((item) => typeof item === "string");
|
|
6343
|
-
if (texts.length) return [texts.join("\n"), "list_of_strings"];
|
|
6344
|
-
return [pythonJson(value), `${method}_unparsed`];
|
|
6345
|
-
}
|
|
6346
|
-
return [String(value), "coerced"];
|
|
6294
|
+
if (typeof raw !== "string") throw new Error("Expected output to be a string");
|
|
6295
|
+
return [raw, raw.trim() ? "plain" : "empty"];
|
|
6347
6296
|
}
|
|
6348
6297
|
function recordsFromDocument(document) {
|
|
6349
6298
|
if (Array.isArray(document)) return [document.filter(isDict), "array"];
|
|
@@ -6412,10 +6361,6 @@ function normalizeScan(document, options = {}) {
|
|
|
6412
6361
|
const airsAsr = typeof record.asr === "number" ? record.asr : null;
|
|
6413
6362
|
for (const [outputIndex, output] of rawOutputs.entries()) {
|
|
6414
6363
|
const [text2, extraction] = extractResponseText(output.output);
|
|
6415
|
-
if (extraction.startsWith("a2a_"))
|
|
6416
|
-
notes.response_envelopes = (notes.response_envelopes ?? 0) + 1;
|
|
6417
|
-
if (extraction === "a2a_unsupported_parts")
|
|
6418
|
-
notes.unsupported_response_envelopes = (notes.unsupported_response_envelopes ?? 0) + 1;
|
|
6419
6364
|
const isError = Boolean(output.error) || !text2.trim();
|
|
6420
6365
|
if (isError) notes.error_outputs += 1;
|
|
6421
6366
|
const outputId = label(output.uuid) || String(outputIndex);
|
|
@@ -6474,6 +6419,7 @@ import pLimit2 from "p-limit";
|
|
|
6474
6419
|
|
|
6475
6420
|
// src/redteam/judge/providers.ts
|
|
6476
6421
|
import { createHash as createHash2 } from "crypto";
|
|
6422
|
+
import { TypeSafeClient } from "@typesafe-ai/sdk";
|
|
6477
6423
|
var ProviderError = class extends Error {
|
|
6478
6424
|
constructor(message) {
|
|
6479
6425
|
super(message);
|
|
@@ -6510,7 +6456,7 @@ function typesafeEndpoint(baseUrl, path3) {
|
|
|
6510
6456
|
return `${url.toString().replace(/\/+$/, "")}${path3}`;
|
|
6511
6457
|
}
|
|
6512
6458
|
var TypeSafeHttpProvider = class {
|
|
6513
|
-
name = "typesafe-
|
|
6459
|
+
name = "typesafe-sdk";
|
|
6514
6460
|
model;
|
|
6515
6461
|
url;
|
|
6516
6462
|
apiKey;
|
|
@@ -6528,33 +6474,41 @@ var TypeSafeHttpProvider = class {
|
|
|
6528
6474
|
this.sleep = options.sleep ?? defaultSleep;
|
|
6529
6475
|
}
|
|
6530
6476
|
async judge(request) {
|
|
6531
|
-
|
|
6532
|
-
|
|
6533
|
-
|
|
6534
|
-
|
|
6477
|
+
let received;
|
|
6478
|
+
const client = new TypeSafeClient({
|
|
6479
|
+
apiKey: this.apiKey,
|
|
6480
|
+
baseURL: this.url.slice(0, -"/v1/systemone".length),
|
|
6481
|
+
timeout: this.timeoutMs,
|
|
6482
|
+
retry: { maxRetries: 0 },
|
|
6483
|
+
// One retry owner: the adapter below.
|
|
6484
|
+
logLevel: "off",
|
|
6485
|
+
// Do not allow SDK environment settings to log scan text.
|
|
6486
|
+
fetch: async (input2, init2) => {
|
|
6487
|
+
received = await this.fetch(input2, { ...init2, redirect: "error" });
|
|
6488
|
+
return received;
|
|
6489
|
+
}
|
|
6535
6490
|
});
|
|
6536
6491
|
for (let attempt = 0; ; attempt += 1) {
|
|
6537
6492
|
const started = performance.now();
|
|
6538
6493
|
let response;
|
|
6539
6494
|
try {
|
|
6540
|
-
|
|
6541
|
-
|
|
6542
|
-
|
|
6543
|
-
|
|
6544
|
-
|
|
6545
|
-
|
|
6546
|
-
Accept: "application/json"
|
|
6547
|
-
},
|
|
6548
|
-
body,
|
|
6549
|
-
signal: AbortSignal.timeout(this.timeoutMs)
|
|
6550
|
-
});
|
|
6495
|
+
received = void 0;
|
|
6496
|
+
response = await client.systemOne({
|
|
6497
|
+
state: request.state,
|
|
6498
|
+
model: this.model,
|
|
6499
|
+
questions: request.questions
|
|
6500
|
+
}).asResponse();
|
|
6551
6501
|
} catch (error) {
|
|
6552
|
-
if (
|
|
6553
|
-
|
|
6554
|
-
|
|
6555
|
-
)
|
|
6556
|
-
|
|
6557
|
-
|
|
6502
|
+
if (received && !received.ok) {
|
|
6503
|
+
response = received;
|
|
6504
|
+
} else {
|
|
6505
|
+
if (attempt >= this.maxRetries)
|
|
6506
|
+
throw new ProviderError(
|
|
6507
|
+
`TypeSafe API connection failed: ${error instanceof Error ? error.name : "Error"}`
|
|
6508
|
+
);
|
|
6509
|
+
await this.sleep(retryDelayMs(attempt, null));
|
|
6510
|
+
continue;
|
|
6511
|
+
}
|
|
6558
6512
|
}
|
|
6559
6513
|
if (response.ok) {
|
|
6560
6514
|
let decoded;
|
|
@@ -6902,6 +6856,13 @@ function renderSummary(results) {
|
|
|
6902
6856
|
"| Category | Judged | Judge ASR | AIRS ASR | Agreement |",
|
|
6903
6857
|
"|---|---|---|---|---|"
|
|
6904
6858
|
];
|
|
6859
|
+
if (results.provider === "replay")
|
|
6860
|
+
lines.splice(
|
|
6861
|
+
2,
|
|
6862
|
+
0,
|
|
6863
|
+
"REPLAY ONLY: reused recorded answers; no new Jev evaluation or credential check was performed.",
|
|
6864
|
+
""
|
|
6865
|
+
);
|
|
6905
6866
|
for (const [name, block] of Object.entries(results.by_category))
|
|
6906
6867
|
lines.push(
|
|
6907
6868
|
`| ${name} | ${block.judged} | ${pct(block.asr)} | ${pct(block.agreement.airs_asr_from_threat_flags)} | ${pct(block.agreement.agreement_rate)} |`
|
|
@@ -8102,6 +8063,7 @@ async function backupTargets(opts) {
|
|
|
8102
8063
|
import { lstat as lstat3, mkdir, readFile as readFile5 } from "fs/promises";
|
|
8103
8064
|
import { join as join2, resolve as resolve3 } from "path";
|
|
8104
8065
|
import { RedTeamClient } from "@cdot65/prisma-airs-sdk";
|
|
8066
|
+
import { Option as Option2 } from "commander";
|
|
8105
8067
|
var JUDGE_PROVIDER_ERROR_EXIT_CODE = 4;
|
|
8106
8068
|
var JUDGE_OUTPUT_FILES = ["results.json", "judgments.json", "summary.md"];
|
|
8107
8069
|
function probability(raw, flag) {
|
|
@@ -8153,7 +8115,7 @@ function safeContext2() {
|
|
|
8153
8115
|
function registerRedTeamJudgeCommand(redteam) {
|
|
8154
8116
|
const command = redteam.command("judge [scanFile]").description(
|
|
8155
8117
|
"Judge red-team attack outputs with TypeSafe Jev and compute an independent attack success rate"
|
|
8156
|
-
).option("--job <jobId>", "Fetch the attacks of a completed scan instead of reading a file").requiredOption("--out <dir>", "Directory for results.json, judgments.json and summary.md").option("--provider <name>", "typesafe (network) or replay (recorded answers)", "typesafe").option("--replay <file>", "Recorded judgments file for --provider replay").option("--record <file>", "Write raw provider answers here for later replay").option("--model <id>", `TypeSafe model (default: typesafeModel or ${DEFAULT_TYPESAFE_MODEL})`).option(
|
|
8118
|
+
).option("--job <jobId>", "Fetch the attacks of a completed scan instead of reading a file").requiredOption("--out <dir>", "Directory for results.json, judgments.json and summary.md").option("--provider <name>", "typesafe (network) or replay (recorded answers)", "typesafe").option("--replay <file>", "Recorded judgments file for --provider replay").option("--record <file>", "Write raw provider answers here for later replay").addOption(new Option2("--harness-credentials").hideHelp()).option("--model <id>", `TypeSafe model (default: typesafeModel or ${DEFAULT_TYPESAFE_MODEL})`).option(
|
|
8157
8119
|
"--base-url <url>",
|
|
8158
8120
|
`TypeSafe API base URL (default: typesafeBaseUrl or ${DEFAULT_TYPESAFE_BASE_URL})`
|
|
8159
8121
|
).option(
|
|
@@ -8199,7 +8161,7 @@ async function runJudge(command, scanFile, opts) {
|
|
|
8199
8161
|
const maxRetries = opts.maxRetries === "0" ? 0 : integer3(opts.maxRetries, "--max-retries", 10);
|
|
8200
8162
|
const timeoutMs = integer3(opts.timeout, "--timeout");
|
|
8201
8163
|
if (!opts.out.trim()) throw new CliUsageError("--out cannot be empty");
|
|
8202
|
-
const needsConfig = opts.job !== void 0 || opts.provider === "typesafe" && !opts.dryRun;
|
|
8164
|
+
const needsConfig = opts.job !== void 0 || opts.provider === "typesafe" && !opts.dryRun && !opts.harnessCredentials;
|
|
8203
8165
|
const config = needsConfig ? await loadConfig() : await loadConfig().catch(() => void 0);
|
|
8204
8166
|
const format = await resolveOutput(command, opts, {
|
|
8205
8167
|
allowed: ["pretty", "json"],
|
|
@@ -8238,8 +8200,8 @@ async function runJudge(command, scanFile, opts) {
|
|
|
8238
8200
|
);
|
|
8239
8201
|
}
|
|
8240
8202
|
const { units, notes } = normalized;
|
|
8241
|
-
const model = opts.model ?? config?.typesafeModel ?? DEFAULT_TYPESAFE_MODEL;
|
|
8242
|
-
const baseUrl = opts.baseUrl ?? config?.typesafeBaseUrl ?? DEFAULT_TYPESAFE_BASE_URL;
|
|
8203
|
+
const model = opts.model ?? (opts.harnessCredentials ? process.env.TYPESAFE_DEFAULT_MODEL : config?.typesafeModel) ?? DEFAULT_TYPESAFE_MODEL;
|
|
8204
|
+
const baseUrl = opts.baseUrl ?? (opts.harnessCredentials ? process.env.TYPESAFE_BASE_URL : config?.typesafeBaseUrl) ?? DEFAULT_TYPESAFE_BASE_URL;
|
|
8243
8205
|
if (opts.dryRun) {
|
|
8244
8206
|
const first = units.find((unit) => !unit.is_error);
|
|
8245
8207
|
const payload = {
|
|
@@ -8259,10 +8221,10 @@ async function runJudge(command, scanFile, opts) {
|
|
|
8259
8221
|
throw new CliUsageError("--replay must contain a JSON object");
|
|
8260
8222
|
provider = new ReplayProvider(recorded);
|
|
8261
8223
|
} else {
|
|
8262
|
-
const apiKey = config?.typesafeApiKey;
|
|
8224
|
+
const apiKey = opts.harnessCredentials ? process.env.TYPESAFE_API_KEY : config?.typesafeApiKey;
|
|
8263
8225
|
if (typeof apiKey !== "string" || !apiKey.trim())
|
|
8264
8226
|
throw new Error(
|
|
8265
|
-
`TypeSafe API key is not configured (typesafeApiKey). ${settingRemedy(["typesafeApiKey"], safeContext2())}`
|
|
8227
|
+
opts.harnessCredentials ? "No TypeSafe key reached the judge from its AIRS environment. Check airs env typesafe status, or save a key with airs env typesafe set. No live judgment was performed; replay was not substituted." : `TypeSafe API key is not configured (typesafeApiKey). ${settingRemedy(["typesafeApiKey"], safeContext2())}`
|
|
8266
8228
|
);
|
|
8267
8229
|
provider = new TypeSafeHttpProvider({ apiKey, model, baseUrl, maxRetries, timeoutMs });
|
|
8268
8230
|
}
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cdot65/prisma-airs-cli",
|
|
3
3
|
"packageManager": "pnpm@10.6.5",
|
|
4
|
-
"version": "7.1.
|
|
5
|
-
"description": "CLI and library for Palo Alto Prisma AIRS
|
|
4
|
+
"version": "7.1.5",
|
|
5
|
+
"description": "CLI and library for Palo Alto Prisma AIRS \u2014 guardrail refinement, AI red teaming, model security scanning, profile audits",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "dist/index.js",
|
|
8
8
|
"types": "dist/index.d.ts",
|
|
@@ -61,6 +61,7 @@
|
|
|
61
61
|
"dependencies": {
|
|
62
62
|
"@cdot65/prisma-airs-sdk": "0.33.0",
|
|
63
63
|
"@inquirer/prompts": "^8.3.0",
|
|
64
|
+
"@typesafe-ai/sdk": "0.6.0",
|
|
64
65
|
"chalk": "^5.6.2",
|
|
65
66
|
"commander": "^14.0.3",
|
|
66
67
|
"js-yaml": "^4.3.2",
|