Skip to content

[BREAKING] FEAT: ASSERT judge dimensions and presets (-1) - #4132

Draft
Richard Lundeen (richlundeen) wants to merge 14 commits into
microsoft:mainfrom
richlundeen:richlundeen-assert-custom-judge-dimensions
Draft

Richard Lundeen (richlundeen) wants to merge 14 commits into
microsoft:mainfrom
richlundeen:richlundeen-assert-custom-judge-dimensions

Conversation

@richlundeen

@richlundeen Richard Lundeen (richlundeen) commented Nov 10, 2026 •

Copy link
Copy Markdown
Contributor

Part -1 of the ASSERT stack. Design and how to use it: ASSERT in PyRIT.

PR
-1 #4132 Judge dimensions, presets and shared techniques this PR
-2 #4130 Trace evidence (OpenTelemetry)
-3 #4131 Headline metrics
-4 #4133 Staged generation (SDK)
-5 #4134 Generation jobs (backend)
-6 #4135 Generate from the GUI
-7 #4144 In-process agents (docs)
-8 #4180 Scenario-aware dataset compatibility
-9 Planned Generated-category result groups
-10 Planned Background dataset loading

Each PR targets main and includes the layers above it in this list, so its commit list and full diff also contain earlier layers. Review this layer's own changes as a source comparison: Files changed for -1. -8 also keeps prototype commits in its ancestry, so for later layers compare source between layers rather than reading commit lists.

Description

ASSERT judging supports custom judge dimensions, ASSERT's reusable judge presets (safety-extended, tool-use, grounding, and others) and repeated judge calls with ASSERT's vote, both in the standalone scorer and in foundry.assert_policy. foundry.assert_policy also accepts your own objective scorer, runs any registered attack technique instead of its own single_turn and multi_turn techniques, and adds an assert_package technique that uses ASSERT's own tester prompt.

  • Models: AssertJudgeDimension and AssertJudgePreset (ASSERT's judge_preset YAML format). A dimension is a boolean bad-event flag by default and accepts ASSERT's allow_not_applicable and ordinal scale options. A scale also needs violation_at, a PyRIT key naming the first grade, in declared order, that counts as a violation, because PyRIT judges every dimension as a violation or not. A scale without it is rejected with a clear error.
  • Presets: ASSERT's 11 judge presets ship under pyrit/datasets/score/assert/judge_presets/ (MIT, provenance recorded). list_assert_judge_presets() lists them. judge_presets accepts only these built-in names, so callers can't name other server paths. Your own preset files go through load_assert_judge_preset_file() or the Python-only judge_preset_files parameter. Dimensions merge as in ASSERT: built-ins, then presets, then preset files, with disabled_judge_dimensions removed after.
  • Scoring: build_assert_policy_scorer accepts a custom AssertJudgeDimension and a samples count, and each dimension gets one judge call that returns a true/false score. A custom dimension's judge asks for one of its grades (false/true for a flag) with a new ASSERT dimension template, and FloatScaleThresholdScorer turns the grade's position into a verdict at violation_at. A not-applicable answer is undetermined. Built-in dimensions keep their existing prompts and identities. build_assert_judge_scorers in pyrit.score.assert_judge_presets binds a resolved judge configuration to one stored policy; the scenario uses it to build its default judges.
  • Judgment data: AssertDimensionResult (dimension, skip reason, not-applicable answer, grade and scale), AssertCategoryJudgment (per-category AssertNodeJudgments, original verdict, correction and warnings) and AssertSampleVotes in pyrit.models are the only readers and writers of the ASSERT score metadata keys, through from_metadata() and to_metadata(). A missing key is unset and malformed data raises. The custom-dimension judge's request comes from a YAML SeedPrompt, and the ASSERT response handlers convert the decoded response through JsonSchemaResponseHandler._convert_response instead of parsing it twice.
  • Repeated samples: samples above 1 wraps the judge in TrueFalseCompositeScorer with an ASSERT vote. It's a majority vote with ties counted as failures, and undetermined and not-applicable samples are left out. As in ASSERT, a sample whose judgment is still invalid after its retries is dropped, and scoring fails only when every sample does. The result is not applicable only when every sample is. In category_validated mode, per-category judgments are voted on (relevance, then violation), and policy_violation is recomputed from them, with an override warning when the recomputed verdict differs.
  • Scenario scoring (foundry.assert_policy): by default each stored policy gets a policy_violation judge (the objective score) and an overrefusal judge (an auxiliary score), category-validated.
    • AssertPolicy(objective_scorer=...) takes your own TrueFalseScorer and uses it unchanged for every group, with no default judges added. It owns its own target and settings, so combining it with judge_target, a different scoring_target, or any non-default judge setting is rejected with an explicit error.
    • New SDK/API parameters judge_presets, judge_preset_files (Python only), disabled_judge_dimensions, judge_samples and objective_dimension configure the default judges, alongside the existing judgment_mode and target_system_prompt. The objective dimension decides success; every other enabled dimension is an auxiliary score.
    • The default judges use the explicit scoring_target run parameter, then judge_target, then the registered scorer target. Scenario readiness reports that judge target, or a missing one, before any data is read; a supplied scorer needs none.
    • Non-default judge settings are recorded in the scenario identity, so a changed setting is detected on resume and saved API launches restore it. Default settings leave existing identities unchanged. A supplied scorer's own identity replaces the default judges' identity, and resuming with a different scorer is rejected. To support this, Scenario gets a small _get_identity_params hook.
  • Techniques ([BREAKING]): foundry.assert_policy builds its technique class from the shared AttackTechniqueRegistry, as airt.cyber does, so any registered technique can be selected. The baseline is ASSERT's prompt case and the default assert_package technique is its scenario case (next bullet). The baseline is now on by default and sends each group's stored system and user prompts. max_turns applies to techniques whose attack takes a turn count, unless the registered technique sets its own. Those techniques need a target that natively keeps multi-turn history, because the judges read the whole conversation; other targets skip or reject them per incompatible_technique_policy. Results are grouped by technique.
    • Migration: AssertTechnique is removed. Replace single_turn with the baseline (include_baseline, on by default) and multi_turn with assert_package (the default) or red_teaming. AssertPolicy.VERSION is now 2, so runs started with version 1 can't be resumed; start a new run.
  • assert_package technique: offered only by foundry.assert_policy, not registered globally. It runs PyRIT's RedTeamingAttack, but the tester's system prompt is ASSERT's tester prompt (pyrit/datasets/executors/red_teaming/assert_tester_system.yaml, from pinned revision 04b713fe) filled in with each group's stored tester instructions and max_turns. ASSERT's opening request and <target_response> reply framing are reused. Groups without tester instructions are skipped, or rejected under incompatible_technique_policy=raise. red_teaming stays selectable and uses the group's stored PyRIT tester prompt.
    • Remaining differences, documented: a stored user prompt opens the conversation instead of the tester, the tester sees no [Turn N/max] counter, and it replies as JSON with next_message instead of plain text.
  • Technique converters: foundry.assert_policy now accepts technique_converters, with none by default.
  • Docs: doc/scanner/foundry has a new "Judge dimensions and presets" section with a preset example adapted from ASSERT's travel-planner budget example, a note that the GUI uses default scoring and that Python can supply its own objective scorer, and an updated "Differences from ASSERT" list. The ASSERT section of doc/code/scoring/1_true_false_scorers covers custom dimensions and samples.

Own GUI changes

These apply only to the foundry.assert_policy launch form. "Before" is this PR's base, main at c3cff1831; "after" is f696a86f7. Both captures use the same synthetic fixture (two generated ASSERT groups, an offline target, the baseline plus the default technique). The default technique is multi_turn before and assert_package after.

Behavior Before (c3cff1831) After (f696a86f7)
judgment_mode and target_system_prompt Rendered as form fields and sent Not rendered, sent or previewed. The form always uses the default judges: category-validated, with no target-owned instructions. Set these through the SDK, pyrit_scan or the API.
judge_presets, disabled_judge_dimensions, judge_samples, objective_dimension Didn't exist New SDK/API settings; never rendered or sent by the form
Objective scorer picker Not shown; the scenario took no scorer Still not shown, although the scenario now accepts an SDK scorer
Preset or saved run with custom scoring (an objective scorer, a carried scoring_target, or a non-default judge setting) Saved judgment_mode and target_system_prompt filled their fields and were sent Refused with "…uses a custom scorer…" or "…has custom judge settings. Use the SDK to preserve them; this form uses default scoring." No estimate is requested, and nothing is previewed or launched with defaults in its place. The saved preset is left unchanged.
Preset or saved run with default-valued judge settings Sent as saved Dropped; the launch proceeds with default scoring
Techniques single_turn and multi_turn; baseline off by default The registered technique catalog with the scenario-local assert_package recommended. Each selected technique is a separate evaluation. The baseline is on by default and sends each group's stored system and user prompts.

The superseded published draft of this PR (0012d164b) rendered the four new judge settings as generic fields. The final source never shows them, so that draft isn't a before/after for this PR. Its earlier combined screenshot, which also shows trace controls from #4130, is kept for history only and is not evidence for this source: historical combined screenshot.

ASSERT parameters. Before (c3cff1831), judgment_mode and target_system_prompt follow max_turns. After (f696a86f7), max_turns is followed directly by Adversarial Target.

Before (c3cff1831) After (f696a86f7)
foundry.assert_policy launch form at c3cff1831: the Parameters section shows incompatible_technique_policy, max_turns, judgment_mode set to category_validated, and an empty target_system_prompt box foundry.assert_policy launch form at f696a86f7: the Parameters section shows incompatible_technique_policy and max_turns, then Adversarial Target, with no judgment_mode or target_system_prompt fields

Run preview. Before (c3cff1831), the techniques are baseline and multi_turn, and the parameters include judgment_mode and target_system_prompt. After (f696a86f7), the techniques are baseline and assert_package, and the only parameters are incompatible_technique_policy and max_turns.

Before (c3cff1831) After (f696a86f7)
Run preview at c3cff1831 listing techniques baseline and multi_turn and parameters incompatible_technique_policy skip, max_turns 6, judgment_mode category_validated, target_system_prompt not set Run preview at f696a86f7 listing techniques baseline and assert_package and parameters incompatible_technique_policy skip and max_turns 6, with no judge settings or objective scorer row

Saved custom scoring (f696a86f7). A saved preset with scenario_params: {scoring_target: "registered-judge"} opens with a warning, and no estimate or launch uses default scoring in its place.

foundry.assert_policy launch form at f696a86f7 opened from preset synthetic_layer1_assert_custom_scoring, showing the warning: This saved ASSERT configuration has custom judge settings. Use the SDK to preserve them; this form uses default scoring.

Shared/non-ASSERT impact

The changes are in shared launch-form code, but they are limited to foundry.assert_policy; other scenarios keep their objective-scorer picker, parameters and saved-setting behavior. The ordinary technique.execute launch form renders identically before and after, with the same image hash at both SHAs.

technique.execute launch form, identical at c3cff1831 and f696a86f7: the Objective section shows Objective Target, objective, harm_categories and an Objective scorer picker set to Use scenario default

On the server, foundry.assert_policy now reports supports_objective_scorer, so the server's existing objective-scorer selection passes a registered scorer to it as objective_scorer, which is used unchanged. No shared scenario, scorer or target behavior changes for other scenarios.

Unchanged/deferred behavior

  • The judge model and endpoint are unchanged: the default judges still use the server-configured scorer target. There is no judge target picker.
  • The legacy judge settings stay available through the SDK, pyrit_scan and the API; judge_preset_files stays Python-only.
  • No trace settings (added in -2), no dataset actions or required-dataset presentation (-6), no native cases or Spanish variant (-8), and no target-creation, operation, preference or numeric-limit changes.
  • Generated-category result groups (-9) and nonblocking dataset loading (-10) are separate planned PRs, not yet opened.

Adds ASSERT judge dimension/preset models, ships ASSERT's 11 judge presets, judges custom boolean and ordinal dimensions (with not-applicable) through build_assert_policy_scorer, aggregates repeated judge samples as ASSERT does, and exposes judge_presets, disabled_judge_dimensions, judge_samples, and objective_dimension on foundry.assert_policy. Default identities are unchanged.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…ge presets to built-in names

- objective_dimension and other non-default judge settings are recorded in scenario identity params, so resume detects changes and saved GUI/API launches restore them. Default identities are unchanged.
- Mixed boolean/ordinal judge sets are ordered by policy for a stable identity.
- judge_presets accepts only shipped preset names; preset files move to the Python-only judge_preset_files (list[Path]) parameter, which the API guard rejects. Preset load errors no longer echo file contents.
- Adds a Scenario._get_identity_params hook.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI balanced review requested due to automatic review settings November 10, 2026 06:28

Copilot AI 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.

🟡 Changes recommended

Unresolved issues permit stale evidence reuse in sampled aggregates, discard successful votes when one judgment fails, and block the event loop during preset loading.

3 open issues
What changed in this PR

Extends PyRIT’s ASSERT integration with custom judge dimensions and reusable presets for standalone scoring and foundry.assert_policy.

Changes:

  • Adds typed boolean and ordinal dimensions, preset loading, and 11 bundled presets.
  • Adds repeated-sample aggregation, ordinal normalization, and not-applicable results.
  • Integrates judge settings into scenario execution and resume identity, with tests, documentation, and provenance updates.
File Description
third_party/​assert-provenance.json Records sources and adaptations for judge features.
THIRD_PARTY_NOTICES.txt Updates attribution to include judge presets.
tests/​unit/​score/​test_assert_judge_presets.py Tests preset loading, merging, and validation.
tests/​unit/​score/​test_assert_dimension_scorer.py Tests custom dimensions and sampled aggregation.
tests/​unit/​scenario/​foundry/​test_assert_policy.py Tests judge configuration, execution, and identity.
tests/​unit/​models/​test_assert_judge.py Tests dimension, scale, and preset models.
tests/​unit/​backend/​test_assert_policy_resume.py Tests saved-launch restoration and file-path rejection.
pyrit/​score/​true_false/​assert_policy_scorer.py Extends the builder with custom dimensions and sampling.
pyrit/​score/​assert_sampled_scorer.py Implements repeated-judge aggregation.
pyrit/​score/​assert_judge_presets.py Loads presets and resolves enabled dimensions.
pyrit/​score/​assert_dimension_scorer.py Implements boolean and ordinal dimension judges.
pyrit/​score/​__init__.py Exports preset helpers and metadata constants.
pyrit/​scenario/​scenarios/​foundry/​assert_policy.py Configures objective and auxiliary dimension judges.
pyrit/​scenario/​core/​scenario.py Adds a scenario identity-parameter hook.
pyrit/​models/​score/​assert_judge.py Defines validated judge configuration models.
pyrit/​models/​__init__.py Exports the new judge models.
pyrit/​datasets/​score/​assert/​judge_presets/​tool-use.yaml Adds tool-selection and invocation checks.
pyrit/​datasets/​score/​assert/​judge_presets/​safety-extended.yaml Adds actionable-harm and privacy checks.
pyrit/​datasets/​score/​assert/​judge_presets/​safety-core.yaml References the built-in safety dimensions.
pyrit/​datasets/​score/​assert/​judge_presets/​robustness.yaml Adds injection and operational-boundary checks.
pyrit/​datasets/​score/​assert/​judge_presets/​policy-adherence.yaml Adds policy adherence checks.
pyrit/​datasets/​score/​assert/​judge_presets/​operational.yaml Adds workflow and escalation checks.
pyrit/​datasets/​score/​assert/​judge_presets/​multi-turn.yaml Adds context-retention and goal-drift checks.
pyrit/​datasets/​score/​assert/​judge_presets/​instruction-following.yaml Adds constraint and completion checks.
pyrit/​datasets/​score/​assert/​judge_presets/​grounding.yaml Adds hallucination and attribution checks.
pyrit/​datasets/​score/​assert/​judge_presets/​communication.yaml Adds communication-quality checks.
pyrit/​datasets/​score/​assert/​judge_presets/​alignment.yaml Adds sycophancy and verification checks.
pyrit/​datasets/​score/​assert/​dimension_system_prompt.yaml Adds the custom-dimension judge prompt.
doc/​scanner/​foundry.py Documents scenario judge settings and preset examples.
doc/​scanner/​foundry.ipynb Mirrors the Foundry documentation updates.
doc/​code/​scoring/​1_true_false_scorers.py Documents custom dimensions and repeated samples.
doc/​code/​scoring/​1_true_false_scorers.ipynb Mirrors the standalone scoring documentation updates.

Review effort: Balanced

Comment thread pyrit/score/assert_sampled_scorer.py Resolved
Comment thread pyrit/score/assert_sampled_scorer.py Resolved
@richlundeen
Richard Lundeen (richlundeen) marked this pull request as draft November 10, 2026 06:38
…converters

- Custom judge dimensions are boolean bad-event flags judged by SelfAskTrueFalseScorer with an ASSERT dimension template; ASSERT's ordinal scale and allow_not_applicable options are rejected.
- Repeated samples use TrueFalseCompositeScorer over the repeated judge with an ASSERT vote (ties count as failures, undetermined samples excluded, category re-vote with override warning), replacing the bespoke sampled scorers.
- Public exports trimmed to AssertJudgeDimension, AssertJudgePreset, AssertSampleMetadata, and the preset list/load functions.
- foundry.assert_policy accepts technique_converters (none by default) for both techniques.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
- AssertTechnique is removed. The technique class is built from the AttackTechniqueRegistry, with red_teaming as the default (ASSERT's scenario case).
- The baseline, now on by default, sends each group's stored system and user prompts (ASSERT's prompt case).
- max_turns applies to techniques whose attack takes a turn count; results are grouped by technique.
- AssertPolicy.VERSION is 2, so runs from version 1 do not resume.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
- allow_not_applicable: a not-applicable answer is undetermined with assert_not_applicable metadata; repeated samples exclude it and keep the marker only when every sample is not applicable.
- ASSERT's ordinal scale is accepted with a PyRIT violation_at grade. The judge's grade position is thresholded by FloatScaleThresholdScorer, so every dimension still yields a true/false verdict, and the grade is kept in assert_grade metadata. Graded dimensions can be the objective.
- AssertDimensionMetadata is exported from pyrit.score. Default identities are unchanged.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

@romanlutz Roman Lutz (romanlutz) 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.

Reviewed the judge dimensions/presets/sampling layer and the registry migration. The new models, preset security posture, and identity/resume handling are solid and well-tested (all 122 PR tests pass locally). Two default-behavior changes from the registry migration need discussion — both verified empirically, details inline.

Adds a scenario-local assert_package technique to foundry.assert_policy: PyRIT's RedTeamingAttack whose tester uses ASSERT's own tester system prompt (pinned 04b713fe) filled with each group's stored tester instructions. It is the default technique alongside the baseline; red_teaming stays selectable. Groups without tester instructions are skipped or rejected per incompatible_technique_policy.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Resolve judge presets in a worker thread instead of the synchronous validator. Drop a judge sample whose judgment stays invalid after retries (scoring fails only when every sample does), as ASSERT does. Require native multi-turn targets for turn-taking techniques in foundry.assert_policy, keep turn settings a registered technique sets itself, pass the adversarial chat only to techniques that use one, and warn once per even sample count.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

@romanlutz Roman Lutz (romanlutz) 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.

Re-review of the two new commits (graded/not-applicable dimensions, assert_package). The instruction-less-groups concern from my earlier review is addressed well: assert_package skips such groups and an all-skipped run fails loudly with guidance, with good test coverage. The graded-dimension implementation (exact-typed grade matching, threshold at violation_at, N/A as undetermined) looks correct. Two remaining items inline; the single-turn-target gap from my earlier review is still open.

description=(
"Red teaming whose tester uses ASSERT's own tester system prompt with each group's stored tester "
"instructions, like an ASSERT scenario case. Groups without tester instructions are skipped."
),

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.

Now that the default technique is scenario-local, this is the natural place to restore the old AssertTechnique.MultiTurn guarantee: requirements=TechniqueRequirements(objective_target=TargetRequirements(native_required=frozenset({CapabilityName.MULTI_TURN}))). As it stands (re-verified on this head), a target with supports_multi_turn=False still builds ['baseline', 'assert_package'] under default settings — the old code skipped multi_turn there ("so every target turn reaches the judges") and raised under raise, with three tests that were deleted without replacement.

objective_scorer=scoring.policy,
adversarial_chat=adversarial_chat or factory.resolve_adversarial_chat(),
objective_scorer=scoring.objective,
memory_labels=context.memory_labels,

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 relies on an implicit contract: the ASSERT generator happens to store the raw tester instructions in the adversarial system prompt's description (with the rendered PyRIT prompt as its value), but nothing on AdversarialPrompts documents or enforces that. A hand-built or third-party dataset with an ordinary descriptive description would silently become the ASSERT tester's scenario task. Consider documenting the contract on AdversarialPrompts.system_prompt (or a dedicated field/metadata key) so assert_package's dependency on it is explicit.

Add AssertDimensionResult, AssertCategoryJudgment (with AssertNodeJudgment) and AssertSampleVotes in pyrit.models as the only readers and writers of the ASSERT score metadata keys, keeping the stored key format. Missing keys are unset and malformed present data raises ValueError. The scenario judge wrapper now records the assert_dimension marker on judged and skipped scores. The custom-dimension judge renders its request from a YAML SeedPrompt, and both ASSERT response handlers validate and convert the decoded response through a new JsonSchemaResponseHandler._convert_response hook instead of parsing twice. AssertDimensionMetadata and AssertSampleMetadata are removed.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Brings in microsoft#3920. No textual conflicts. Updates the assert_package resume test for main's earlier rejection of changed scenario role references, which now reports that the run does not match the current configuration.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Brings in microsoft#4187. No conflicts or integration changes.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…ng in the launch form

AssertPolicy(objective_scorer=...) uses a supplied true/false scorer unchanged and rejects mixing it with judge_target, a different scoring_target, or non-default legacy judge settings. Default per-policy judge construction moves into the scorer package as build_assert_judge_scorers. The ASSERT launch form hides the objective-scorer picker and ASSERT judge settings in the form, request and preview, and rejects saved custom scoring instead of replaying it as defaults.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…objective scorer

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…ts readiness

Default ASSERT judges resolve their target through Scenario._resolve_scoring_target, so the explicit scoring_target run parameter takes precedence over judge_target, and AssertPolicy.get_readiness binds that target or reports it missing. A supplied objective scorer needs no default judge target. The ASSERT launch form also treats a carried scoring_target as scorer-owned, and compares saved judge settings to their display defaults by type: an empty list is the default only for list settings, and an empty or list value for target_system_prompt is rejected.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Satisfies main's check-keyword-only-params gate.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

This branch has not been deployed

No deployments
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