diff --git a/COMMANDS.md b/COMMANDS.md index 4d098c5..4afc665 100644 --- a/COMMANDS.md +++ b/COMMANDS.md @@ -328,6 +328,12 @@ fable-agent plinius godmode "improve explanation quality" - `--run ` - `fable5 memory-b-cell ` - `--cache ` +- `fable5 jailbreak-assess ` + - `--capability-gain ` + - `--breadth ` + - `--weaponization-ease ` + - `--discoverability ` + - `--output ` - `fable5 prompt-fingerprint ` - `--output ` - `fable5 skill-health` diff --git a/src/fable5/index.ts b/src/fable5/index.ts index 93235fb..9421fd3 100644 --- a/src/fable5/index.ts +++ b/src/fable5/index.ts @@ -99,6 +99,9 @@ export type { SkillHealthEntry, SkillHealthOptions, SkillHealthReceipt } from ". export { createPromptFingerprintReceipt, writePromptFingerprintReceipt } from "./prompt-fingerprint.js"; export type { PromptFingerprintFinding, PromptFingerprintReceipt } from "./prompt-fingerprint.js"; +export { createJailbreakAssessmentReceipt, writeJailbreakAssessmentReceipt } from "./jailbreak-assessment.js"; +export type { JailbreakAssessmentOptions, JailbreakAssessmentReceipt, JailbreakAssessmentScores, JailbreakDecision } from "./jailbreak-assessment.js"; + export { createPlanReceipt, writePlanReceipt } from "./plan-receipt.js"; export type { PlanAllowedOutcome, PlanPhase, PlanReceipt, PlanReceiptOptions, PlanTaskType } from "./plan-receipt.js"; diff --git a/src/fable5/jailbreak-assessment.test.ts b/src/fable5/jailbreak-assessment.test.ts new file mode 100644 index 0000000..33e5eb0 --- /dev/null +++ b/src/fable5/jailbreak-assessment.test.ts @@ -0,0 +1,51 @@ +import * as fs from "node:fs"; +import * as os from "node:os"; +import * as path from "node:path"; +import { describe, expect, it } from "vitest"; +import { createJailbreakAssessmentReceipt, writeJailbreakAssessmentReceipt } from "./jailbreak-assessment.js"; + +describe("jailbreak assessment receipt", () => { + it("scores low-severity jailbreaks as monitor", () => { + const receipt = createJailbreakAssessmentReceipt({ + source: "minor prompt bypass", + capabilityGain: 1, + breadth: 1, + weaponizationEase: 1, + discoverability: 2, + now: new Date("2026-07-01T00:00:00.000Z"), + }); + + expect(receipt).toMatchObject({ + schema: "fable.jailbreak_assessment.receipt.v1", + createdAt: "2026-07-01T00:00:00.000Z", + total: 5, + decision: "monitor", + deployAttempted: false, + }); + }); + + it("escalates broad easy high-gain jailbreaks", () => { + expect(createJailbreakAssessmentReceipt({ source: "universal", capabilityGain: 5, breadth: 5, weaponizationEase: 4, discoverability: 4 })).toMatchObject({ total: 18, decision: "disclose" }); + expect(createJailbreakAssessmentReceipt({ source: "narrow harmful", capabilityGain: 4, breadth: 3, weaponizationEase: 3, discoverability: 3 })).toMatchObject({ total: 13, decision: "block" }); + expect(createJailbreakAssessmentReceipt({ source: "patchable", capabilityGain: 3, breadth: 2, weaponizationEase: 2, discoverability: 2 })).toMatchObject({ total: 9, decision: "patch" }); + }); + + it("clamps scores to the 0-5 framework", () => { + expect(createJailbreakAssessmentReceipt({ source: "clamp", capabilityGain: 99, breadth: -1, weaponizationEase: 2.4, discoverability: 2.6 }).scores).toEqual({ + capabilityGain: 5, + breadth: 0, + weaponizationEase: 2, + discoverability: 3, + }); + }); + + it("writes JSON receipts", () => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "jailbreak-assessment-")); + const file = path.join(dir, "receipt.json"); + + writeJailbreakAssessmentReceipt(file, createJailbreakAssessmentReceipt({ source: "x", capabilityGain: 0, breadth: 0, weaponizationEase: 0, discoverability: 0 })); + + expect(JSON.parse(fs.readFileSync(file, "utf-8"))).toMatchObject({ schema: "fable.jailbreak_assessment.receipt.v1", decision: "ignore" }); + fs.rmSync(dir, { recursive: true, force: true }); + }); +}); diff --git a/src/fable5/jailbreak-assessment.ts b/src/fable5/jailbreak-assessment.ts new file mode 100644 index 0000000..e2595a0 --- /dev/null +++ b/src/fable5/jailbreak-assessment.ts @@ -0,0 +1,58 @@ +import * as fs from "node:fs"; +import * as path from "node:path"; + +export type JailbreakDecision = "ignore" | "monitor" | "patch" | "block" | "disclose"; + +export interface JailbreakAssessmentScores { + capabilityGain: number; + breadth: number; + weaponizationEase: number; + discoverability: number; +} + +export interface JailbreakAssessmentReceipt { + schema: "fable.jailbreak_assessment.receipt.v1"; + createdAt: string; + source: string; + scores: JailbreakAssessmentScores; + total: number; + decision: JailbreakDecision; + reasons: string[]; + deployAttempted: false; +} + +export interface JailbreakAssessmentOptions extends JailbreakAssessmentScores { + source: string; + now?: Date; +} + +function clampScore(n: number): number { + return Math.max(0, Math.min(5, Math.round(n))); +} + +export function createJailbreakAssessmentReceipt(opts: JailbreakAssessmentOptions): JailbreakAssessmentReceipt { + const scores = { + capabilityGain: clampScore(opts.capabilityGain), + breadth: clampScore(opts.breadth), + weaponizationEase: clampScore(opts.weaponizationEase), + discoverability: clampScore(opts.discoverability), + }; + const total = scores.capabilityGain + scores.breadth + scores.weaponizationEase + scores.discoverability; + const decision: JailbreakDecision = total >= 17 ? "disclose" : total >= 13 ? "block" : total >= 9 ? "patch" : total >= 5 ? "monitor" : "ignore"; + return { + schema: "fable.jailbreak_assessment.receipt.v1", + createdAt: (opts.now ?? new Date()).toISOString(), + source: opts.source, + scores, + total, + decision, + reasons: [`severity total ${total}/20 from capability gain, breadth, weaponization ease, and discoverability`], + deployAttempted: false, + }; +} + +export function writeJailbreakAssessmentReceipt(file: string, receipt: JailbreakAssessmentReceipt): string { + fs.mkdirSync(path.dirname(file), { recursive: true }); + fs.writeFileSync(file, `${JSON.stringify(receipt, null, 2)}\n`); + return file; +} diff --git a/src/index.ts b/src/index.ts index 389d6ba..5f4df82 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2047,6 +2047,30 @@ fable if (result.verdict === "known_unsafe") process.exit(1); }); +fable + .command("jailbreak-assess ") + .description("Score jailbreak severity using capability gain, breadth, weaponization ease, and discoverability") + .requiredOption("--capability-gain ", "0-5 capability gain", (v) => Number(v)) + .requiredOption("--breadth ", "0-5 breadth of capability gain", (v) => Number(v)) + .requiredOption("--weaponization-ease ", "0-5 ease of weaponization", (v) => Number(v)) + .requiredOption("--discoverability ", "0-5 discoverability", (v) => Number(v)) + .option("--output ", "Receipt output path", path.join(".fable", "security", "jailbreak-assessment-live.json")) + .action(async (source: string, opts: { capabilityGain: number; breadth: number; weaponizationEase: number; discoverability: number; output?: string }) => { + const { createJailbreakAssessmentReceipt, writeJailbreakAssessmentReceipt } = await import("./fable5/jailbreak-assessment.js"); + const receipt = createJailbreakAssessmentReceipt({ + source, + capabilityGain: opts.capabilityGain, + breadth: opts.breadth, + weaponizationEase: opts.weaponizationEase, + discoverability: opts.discoverability, + }); + const out = opts.output ?? path.join(".fable", "security", "jailbreak-assessment-live.json"); + writeJailbreakAssessmentReceipt(out, receipt); + console.log(JSON.stringify(receipt, null, 2)); + console.log(`\n Receipt: ${out}\n`); + if (receipt.decision === "block" || receipt.decision === "disclose") process.exit(1); + }); + fable .command("prompt-fingerprint ") .description("Scan a prompt or bundle for covert prompt markers and telemetry fingerprint risks")