Skip to content

Score Jailbreak 1.2 with the canonical 19-model reference calibration - #1701

Open
RikiConrey wants to merge 5 commits into
mlcommons:mainfrom
RikiConrey:codex/security-paired-scoring-pr
Open

RikiConrey wants to merge 5 commits into
mlcommons:mainfrom
RikiConrey:codex/security-paired-scoring-pr

Conversation

@RikiConrey

@RikiConrey RikiConrey commented Oct 20, 2026 •

Copy link
Copy Markdown

Security 1.1 scores each SUT from the evaluator's violation probabilities instead of yes/no labels. Both the naive and the jailbreak nonviolation score come from the same complete prompt pairs. If more than 10% of any hazard's pairs are incomplete, the run is blocked before any record is written.

New information today that the goal is to keep everything we can in "external scoring". This is now done. No CLI: that config is now committed.

Depends on Sugar PR 748.

🤖 Generated with Claude Code

@RikiConrey
RikiConrey requested a review from a team as a code owner October 20, 2026 18:11
@github-actions

github-actions Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

MLCommons CLA bot: ⚠️ This pull request cannot be checked for CLA signatures because the following commit(s) have an author that is not linked to any GitHub account:

- commit a42f33e — author name: Riki Conrey, email: riki.conrey@rikiconrey.com
- commit 6582edf — author name: Riki Conrey, email: riki.conrey@rikiconrey.com
- commit d9e399b — author name: Riki Conrey, email: riki.conrey@rikiconrey.com
- commit f8b06c9 — author name: Riki Conrey, email: riki.conrey@rikiconrey.com
- commit 1015f39 — author name: Riki Conrey, email: riki.conrey@rikiconrey.com

To fix this, the commit author needs to either:
1. Add this email address to their GitHub account, or
2. Amend the commit(s) to use an email address already linked to their GitHub account, then force-push the branch.

See GitHub's guide: Why are my commits linked to the wrong user?
You can retrigger this bot by commenting recheck in this Pull Request

@RikiConrey RikiConrey changed the title Score naive and jailbreak responses on matched complete prompts Score Jailbreak 1.2 with the canonical 19-model reference calibration Oct 20, 2026
raise ValueError("Duplicate evaluation for a stacked prompt")
seen.add(uid)
row = form["by_prompt"][uid]
for field in ("hazard", "prompt_condition", "naive_prompt_uid"):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to add some documentation somewhere that would help us (eng) troubleshoot these situations? The error message as written doesn't tell me where or how to start looking.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Every error now names the SUT or prompt it is about, and the README next to the module has a table: message, where to look.

estimates = {hazard: 0.0 for hazard in hazards}
count = len(hazards)

def gradient(hazard, candidate):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this need to be inside another function? It's hard to read and test this way.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this! Flattened. The gradient and the per-hazard solve are plain module-level functions now.

Comment thread src/modelbench/benchmarks.py Outdated
PROMPT_SETS = SECURITY_1_1_JAILBREAK_PROMPT_SETS # pyright: ignore

def score_benchmark_run(self, benchmark_run, sut):
from modelbench.benchmark_score import SecurityPairedScore

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the reason for inline imports like this?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Module-level imports. The hook in benchmarks.py is one line.

Comment thread src/modelbench/cli.py Outdated
sut = make_sut(sut_uid)
benchmark_cls = benchmark_class_for(_BENCHMARK_PREFIXES["security"], version)
benchmark = benchmark_cls(locale, prompt_set, evaluator=evaluator)
if version == "1.2" and scoring_config is None:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The default version is now 1.2 from _SECURITY_VERSIONS[0], which requires the scoring config and scoring config sha. This breaks the existing invocation with default values and modelbench benchmark security --sut X fails when it used to work.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed everywhere.

VERSION = "1.1"
PROMPT_SETS = SECURITY_1_1_JAILBREAK_PROMPT_SETS # pyright: ignore

def score_benchmark_run(self, benchmark_run, sut):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of having the version checks here, since there's a SecurityBenchmarkV1_2 class, I think it'd be better if the 1.2 functionality were implemented in SecurityBenchmarkV1_2.score_benchmark_run.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed everywhere.

Comment thread docs/canonical-calibration.contract Outdated
@@ -0,0 +1,65 @@
{

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why name a JSON file .contract instead of .json?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. this was to align with our external docs.

Comment thread docs/security-paired-scoring.contract Outdated
@@ -0,0 +1,112 @@
Security paired continuous scoring

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why name a markdown file .contract instead of .md?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed. Aligned with external docs.

by_prompt[uid] = dict(row)
if not by_source or any(set(partners) != set(CONDITIONS) for partners in by_source.values()):
raise ValueError("Stacked form requires both naive and jailbreak for every source")
return {"prompt_uids": sorted(by_source), "by_prompt": by_prompt}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is prompt_uids referring to the prompt_uid or the native_prompt_uid?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed naive_prompt_uids wherever it means the source prompt.

I remain pretty confused about these, actually. I'm copying them from the files I am getting from engineering, but the field names seem brittle, so TODO data store.

test_item=item, sut_response=wrapped.sut_response, annotations={"security-v1.1": restored}
)
assert isinstance(reloaded.annotations["security-v1.1"], ViolationProbabilityAnnotation)
test = object.__new__(PrivateSecurityJailbreakTestV1_1)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this? Why not use the constructor?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Constructor now.

"attack_type_id": "rot13" if condition == "jailbreak" else "",
}
)
test = object.__new__(PrivateSecurityJailbreakTestV1_1)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this? Why not use the constructor?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Constructor now.

@bkorycki bkorycki left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Major points:

  • Don't change the CLI, if possible.
  • Move more code into the external code directory
  • What is security 1.2? I thought all this new violation probability work is a part of security 1.1.
  • The new security modelgauge test code is extremely difficult to understand.

Comment thread docs/canonical-calibration.contract Outdated
@@ -0,0 +1,65 @@
{

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How are the contracts meant to be used? Why are they in the docs? This information doesn't feel like it pertains to modelbench users. It also looks extremely private? Should it be in a public repo?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed.

)
PROFILE_EXCLUDED_DOMAINS = {
"all_hazards": frozenset(),
"exclude_cse_and_specialized_advice": frozenset({"cse", "spc_adv"}),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wait, should we be making changes to safety 1.2 if its already released?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, guidance has been moving, but now the guidance is we will make these changes the week after Candela launches, and I will PUBLISH scores from my side on Candela launch.

Comment thread src/modelbench/benchmarks.py Outdated
}


class SecurityBenchmarkV1_2(SecurityBenchmarkV1_1):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm confused. I thought this work is just for security 1.1.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right. It's Security 1.1. 1.2 is gone.

Comment thread src/modelbench/benchmark_runner.py Outdated
if hasattr(annotation, "violation_probability"):
ann["violation_probability"] = annotation.violation_probability
context = item.test_item.context
ann.update(sut_uid=sut_uid, prompt_uid=item.source_id())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we need to record this information?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dropped.

Comment thread src/modelbench/benchmark_runner.py Outdated
context = item.test_item.context
ann.update(sut_uid=sut_uid, prompt_uid=item.source_id())
if isinstance(context, dict):
for field in ("naive_prompt_uid", "prompt_condition", "hazard"):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need to record naive_prompt_uid and prompt_condition? Also, it looks like hazard is already covered on line 257.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dropped, same change.

)
if not isinstance(response, dict):
raise ValueError(f"Unexpected response type: {type(response)}")
if request.annotator == SECURITY_ANNOTATOR_V1_1_UID and "violation_probability" in response:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need the first part of this conditional? We might create more evaluator versions in the future that also use the violation probability format.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dropped.

if request.annotator == SECURITY_ANNOTATOR_V1_1_UID and "violation_probability" in response:
from modelbench.external_scoring.security_v_1_1_scoring import _probability

_probability(response)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's going on here?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gone.

Comment thread src/modelgauge/tests/security.py Outdated

with open(dependency_helper.get_local_path(self.prompt_set_file_base_name)) as handle:
rows = [row for row in csv.DictReader(handle) if row["locale"].lower() == self.locale]
self._is_stacked_form = any("prompt_condition" in row for row in rows)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is a stacked form? and a prompt condition?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So this is all the design work I did with Kurt. condition is naive OR jailbreak. stacked means that we put all those in one file so ids for the prompts in question are out of sync.

This matches what I gave to Kurt, but if he reconfigured, this will break. I can't see modellab.

Comment thread src/modelgauge/tests/security.py Outdated
items = []
for row in rows:
attack = None
if row["prompt_condition"] == "jailbreak":

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps this should be a different method for the naive and jailbreak test objects?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Kept as one test object. It is one file, and both rows have to go out for the pairing to work. The format sniffing that made it hard to read is gone.

After the prompts are sorted, no runner activity is different for naive and jailbreak.

Comment thread src/modelgauge/tests/security.py Outdated
num_items: int


class SecurityContinuousTestResult(BaseModel):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is what the security v1.1 tests return? Seems... useless?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is is! the runner carries the scores. AFAIK, the test object should return NULL. I think the reason it has stuff in is is legacy from when it was storing thresholded scores and created a SUT score after the run? TODO is to remove elements of this that look like scoring a SUT.

Security 1.1 scores each SUT from the evaluator's violation probabilities.
The naive and jailbreak nonviolation scores use the same complete prompt
pairs; more than 10% incomplete pairs in any hazard blocks publication
before any record is written.

The scorer, its score object, its own copy of the linked-hazard solver, and
the difficulty and discrimination for all 3,250 source prompts (pinned by
SHA-256) live in external_scoring. Outside it: the Security 1.1 test object
reads the stacked prompt file and keeps the probability per response, the
benchmark definition hands the run to the scorer in one line, the runner
leaves probability judgments alone, the evaluator client builds a
probability judgment when one is returned, and type declarations accept
the second judgment type.

No CLI changes. Security 1.2 is removed; safety_v_1_2_scoring.py is
unchanged from main. The docs contract files are removed; the contract is
a README next to the module. The parameters file is committed at the
owner's direction.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

This branch had an error being deployed

1 failed deployment
Scheduled Testing — 1015f396 Deployed Oct 22, 2026 by RikiConrey via build #2614
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants