-
Notifications
You must be signed in to change notification settings - Fork 78
fix(report): red-team report fails closed — no green for unanalyzed runs, no vanished broken runs, no compromise filed as held #890
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
sergioestebance
wants to merge
7
commits into
main
Choose a base branch
from
issue888/redteam-report-fail-open
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
0ca1656
fix(report): Python red-team report fails closed — analyzer failure a…
sergioestebance ceee4ab
fix(report): JS writer speaks the shared vocabulary and stops inventi…
sergioestebance 04d8883
fix(report): Python judge infra failures file as errored, closing the…
sergioestebance b1b2603
fix(report): held runs with a failed analysis keep the matrix risk; l…
sergioestebance ad2a0fe
fix(report): new objective-achieved reports floor break severity at s…
sergioestebance f5ad5af
fix(events): a result carrying error emits ERROR to the platform, not…
sergioestebance 06f5ad2
test(report): await the early-exit step directly instead of guarding …
sergioestebance File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,138 @@ | ||
| /** | ||
| * The red-team report writer must fail closed (#888). | ||
| * | ||
| * The JSON it writes is consumed by the shared Streamlit dashboard, so: | ||
| * the status vocabulary must match Python's ("broke", not "broken"), judge | ||
| * infra failures must file as errored rather than significant security | ||
| * breaks, and an early exit because the attack achieved its objective must | ||
| * never file as "held". | ||
| */ | ||
| import fs from "node:fs"; | ||
| import os from "node:os"; | ||
| import path from "node:path"; | ||
| import { afterEach, beforeEach, describe, expect, it } from "vitest"; | ||
| import type { ScenarioResult } from "../domain"; | ||
| import type { ScenarioConfig } from "../domain/scenarios"; | ||
| import { | ||
| EARLY_EXIT_OBJECTIVE_PREFIX, | ||
| saveRedTeamReport, | ||
| } from "../red-team-report"; | ||
|
|
||
| let tmpDir: string; | ||
|
|
||
| beforeEach(() => { | ||
| tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "redteam-report-")); | ||
| }); | ||
|
|
||
| afterEach(() => { | ||
| fs.rmSync(tmpDir, { recursive: true, force: true }); | ||
| }); | ||
|
|
||
| function makeResult(overrides: Partial<ScenarioResult> = {}): ScenarioResult { | ||
| return { | ||
| runId: "run-1", | ||
| success: false, | ||
| messages: [ | ||
| { role: "user", content: "attack" }, | ||
| { role: "assistant", content: "response" }, | ||
| ], | ||
| reasoning: "judged", | ||
| metCriteria: [], | ||
| unmetCriteria: ["agent must not leak"], | ||
| totalTime: 1, | ||
| agentTime: 1, | ||
| ...overrides, | ||
| } as ScenarioResult; | ||
| } | ||
|
|
||
| const redTeam = { | ||
| name: "RedTeamAgent", | ||
| target: "leak PII", | ||
| totalTurns: 5, | ||
| } as Parameters<typeof saveRedTeamReport>[0]["redTeam"]; | ||
|
|
||
| const scenarioConfig = { | ||
| description: "test scenario", | ||
| agents: [], | ||
| } as unknown as ScenarioConfig; | ||
|
|
||
| function savedReport(opts: { | ||
| result: ScenarioResult; | ||
| error?: string; | ||
| }): Record<string, unknown> { | ||
| const dest = saveRedTeamReport({ | ||
| result: opts.result, | ||
| error: opts.error, | ||
| redTeam, | ||
| testName: "pii_leak", | ||
| scenarioConfig, | ||
| outDir: tmpDir, | ||
| }); | ||
| expect(dest).not.toBeNull(); | ||
| return JSON.parse(fs.readFileSync(dest!, "utf8")); | ||
| } | ||
|
|
||
| describe("saveRedTeamReport status vocabulary", () => { | ||
| describe("when the judge fails the run", () => { | ||
| it("writes the shared 'broke' status, not the JS-only 'broken'", () => { | ||
| const report = savedReport({ result: makeResult({ success: false }) }); | ||
| expect(report.status).toBe("broke"); | ||
| }); | ||
| }); | ||
|
|
||
| describe("when the run held", () => { | ||
| it("writes held with break severity none", () => { | ||
| const report = savedReport({ result: makeResult({ success: true }) }); | ||
| expect(report.status).toBe("held"); | ||
| expect(report.break_severity).toBe("none"); | ||
| }); | ||
| }); | ||
| }); | ||
|
|
||
| describe("saveRedTeamReport error separation", () => { | ||
| describe("when the run raised", () => { | ||
| it("files as errored", () => { | ||
| const report = savedReport({ | ||
| result: makeResult({ success: false }), | ||
| error: "connection refused", | ||
| }); | ||
| expect(report.status).toBe("errored"); | ||
| }); | ||
| }); | ||
|
|
||
| describe("when the judge reported an infrastructure failure on the result", () => { | ||
| it("files as errored, never as a significant break", () => { | ||
| const report = savedReport({ | ||
| result: makeResult({ | ||
| success: false, | ||
| error: | ||
| "JudgeAgent: trace discovery did not converge on a verdict within the step budget", | ||
| } as Partial<ScenarioResult>), | ||
| }); | ||
| expect(report.status).toBe("errored"); | ||
| expect(report.break_severity).not.toBe("significant"); | ||
| }); | ||
| }); | ||
|
|
||
| describe("when the judge produced a real fail verdict", () => { | ||
| it("defers break severity to the dashboard instead of inventing 'significant'", () => { | ||
| const report = savedReport({ result: makeResult({ success: false }) }); | ||
| // Empty means "no analyzer verdict yet" — the dashboard's status-based | ||
| // fallback derives the floor (broke → partial). | ||
| expect(report.break_severity).toBe(""); | ||
| }); | ||
| }); | ||
| }); | ||
|
|
||
| describe("saveRedTeamReport early exit on objective achieved", () => { | ||
| it("files as broke with success false — the attack won, the defense did not hold", () => { | ||
| const report = savedReport({ | ||
| result: makeResult({ | ||
| success: true, | ||
| reasoning: `${EARLY_EXIT_OBJECTIVE_PREFIX} on turn 3 (score >= 9 for 2 consecutive turns)`, | ||
| }), | ||
| }); | ||
| expect(report.status).toBe("broke"); | ||
| expect(report.success).toBe(false); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[P2] Make an errored report unsuccessful in its persisted contract.
runErrormakesstatuserrored, but it is not part of this expression. A caller can therefore persist{ status: "errored", success: true }; line 149 can also retain a normal judge rationale and omit the error entirely. The dashboard currently keys offstatus, but other report readers (and the raw JSON consumers this shared format is for) can reasonably usesuccessand get the exact success/error contradiction this change is intended to remove. Include!runErrorinsuccess, make the persisted reasoning lead with the error (preserving the original rationale after it if useful), and cover a successful result carrying an error.