Skip to content

Commit a9c93dc

Browse files
adrian-gavrilaAdrian GavrilaCopilot
authored
MAINT: Move include_baseline from Scenario constructor to initi… (#1700)
Co-authored-by: Adrian Gavrila <agavrila@microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent da334ca commit a9c93dc

31 files changed

Lines changed: 1140 additions & 249 deletions

.github/instructions/scenarios.instructions.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,15 @@ Scenarios orchestrate multi-attack security testing campaigns. Each scenario gro
1111
All scenarios inherit from `Scenario` (ABC) and must:
1212

1313
1. **Define `VERSION`** as a class constant (increment on breaking changes)
14-
2. **Implement three abstract methods:**
14+
2. **Optionally declare `BASELINE_POLICY`** (defaults to `BaselinePolicy.Enabled` — a baseline `PromptSendingAttack` is prepended and callers can opt out per run via `initialize_async(include_baseline=False)`):
15+
- `BaselinePolicy.Disabled` — baseline supported but off by default (e.g. `Jailbreak`, where templates dominate the run).
16+
- `BaselinePolicy.Forbidden` — baseline is meaningless for this scenario's comparison axis (e.g. `AdversarialBenchmark`, which compares against gold-standard answers). Explicit `include_baseline=True` raises `ValueError`.
17+
3. **Implement three abstract methods:**
1518

1619
```python
1720
class MyScenario(Scenario):
1821
VERSION: int = 1
22+
BASELINE_POLICY: ClassVar[BaselinePolicy] = BaselinePolicy.Enabled
1923

2024
@classmethod
2125
def get_strategy_class(cls) -> type[ScenarioStrategy]:
@@ -30,7 +34,7 @@ class MyScenario(Scenario):
3034
return DatasetConfiguration(dataset_names=["my_dataset"])
3135
```
3236

33-
3. **Optionally override `_get_atomic_attacks_async()`** — the base class provides a default
37+
4. **Optionally override `_get_atomic_attacks_async()`** — the base class provides a default
3438
that uses the factory/registry pattern (see "AtomicAttack Construction" below).
3539
Only override if your scenario needs custom attack construction logic.
3640

@@ -154,6 +158,8 @@ The default implementation:
154158
Only override when the scenario **cannot** use the factory/registry pattern — e.g., scenarios
155159
with custom composite logic, per-strategy converter stacks, or non-standard attack construction.
156160

161+
Overrides that want baseline support must emit it themselves by calling `self._build_baseline_atomic_attack(seed_groups=...)` with the same seeds used for the strategy attacks and prepending the result. The base implementation emits baseline automatically; passing freshly resolved seeds reintroduces ADO 9012 (baseline-vs-strategy population divergence under `max_dataset_size`).
162+
157163
### Manual AtomicAttack construction (for overrides):
158164

159165
```python

doc/code/scenarios/0_scenarios.ipynb

Lines changed: 70 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,6 @@
7474
" - `version`: Integer version number\n",
7575
" - `strategy_class`: The strategy enum class for this scenario\n",
7676
" - `objective_scorer_identifier`: Identifier dict for the scoring mechanism (optional)\n",
77-
" - `include_default_baseline`: Whether to include a baseline attack (default: True)\n",
7877
" - `scenario_result_id`: Optional ID to resume an existing scenario (optional)\n",
7978
"\n",
8079
"5. **Initialization**: Call `await scenario.initialize_async()` to populate atomic attacks:\n",
@@ -83,6 +82,8 @@
8382
" - `max_concurrency`: Number of concurrent operations (default: 1)\n",
8483
" - `max_retries`: Number of retry attempts on failure (default: 0)\n",
8584
" - `memory_labels`: Optional labels for tracking (optional)\n",
85+
" - `include_baseline`: Whether to prepend a baseline attack (defaults to the scenario type's\n",
86+
" `BASELINE_POLICY`; most scenarios default it on, `Jailbreak` defaults it off)\n",
8687
"\n",
8788
"### Example Structure\n",
8889
"\n",
@@ -101,9 +102,15 @@
101102
"name": "stdout",
102103
"output_type": "stream",
103104
"text": [
104-
"Found default environment files: ['./.pyrit/.env', './.pyrit/.env.local']\n",
105-
"Loaded environment file: ./.pyrit/.env\n",
106-
"Loaded environment file: ./.pyrit/.env.local\n"
105+
"Found default environment files: ['./.pyrit/.env']\n",
106+
"Loaded environment file: ./.pyrit/.env\n"
107+
]
108+
},
109+
{
110+
"name": "stdout",
111+
"output_type": "stream",
112+
"text": [
113+
"No new upgrade operations detected.\n"
107114
]
108115
}
109116
],
@@ -193,34 +200,13 @@
193200
"name": "stdout",
194201
"output_type": "stream",
195202
"text": [
196-
"Found default environment files: ['./.pyrit/.env', './.pyrit/.env.local']\n",
203+
"Loading default configuration file: ./.pyrit/.pyrit_conf\n",
204+
"Found default environment files: ['./.pyrit/.env']\n",
197205
"Loaded environment file: ./.pyrit/.env\n",
198-
"Loaded environment file: ./.pyrit/.env.local\n"
199-
]
200-
},
201-
{
202-
"name": "stdout",
203-
"output_type": "stream",
204-
"text": [
205206
"\n",
206207
"Available Scenarios:\n",
207208
"================================================================================\n",
208209
"\u001b[1m\u001b[36m\n",
209-
" airt.content_harms\u001b[0m\n",
210-
" Class: ContentHarms\n",
211-
" Description:\n",
212-
" Content Harms Scenario implementation for PyRIT. This scenario contains\n",
213-
" various harm-based checks that you can run to get a quick idea about\n",
214-
" model behavior with respect to certain harm categories.\n",
215-
" Aggregate Strategies:\n",
216-
" - all\n",
217-
" Available Strategies (7):\n",
218-
" hate, fairness, violence, sexual, harassment, misinformation, leakage\n",
219-
" Default Strategy: all\n",
220-
" Default Datasets (7, max 4 per dataset):\n",
221-
" airt_hate, airt_fairness, airt_violence, airt_sexual, airt_harassment,\n",
222-
" airt_misinformation, airt_leakage\n",
223-
"\u001b[1m\u001b[36m\n",
224210
" airt.cyber\u001b[0m\n",
225211
" Class: Cyber\n",
226212
" Description:\n",
@@ -229,9 +215,9 @@
229215
" Cyber class contains different variations of the malware generation\n",
230216
" techniques.\n",
231217
" Aggregate Strategies:\n",
232-
" - all\n",
218+
" - all, single_turn, multi_turn\n",
233219
" Available Strategies (2):\n",
234-
" single_turn, multi_turn\n",
220+
" prompt_sending, red_teaming\n",
235221
" Default Strategy: all\n",
236222
" Default Datasets (1, max 4 per dataset):\n",
237223
" airt_malware\n",
@@ -256,14 +242,14 @@
256242
" Description:\n",
257243
" Leakage scenario implementation for PyRIT. This scenario tests how\n",
258244
" susceptible models are to leaking training data, PII, intellectual\n",
259-
" property, or other confidential information. The Leakage class\n",
260-
" contains different attack variations designed to extract sensitive\n",
261-
" information from models.\n",
245+
" property, or other confidential information. Uses the registry/factory\n",
246+
" pattern to construct attack techniques.\n",
262247
" Aggregate Strategies:\n",
263-
" - all, single_turn, multi_turn, ip, sensitive_data\n",
264-
" Available Strategies (4):\n",
265-
" first_letter, image, role_play, crescendo\n",
266-
" Default Strategy: all\n",
248+
" - all, default, single_turn, multi_turn\n",
249+
" Available Strategies (9):\n",
250+
" prompt_sending, role_play, many_shot, tap, crescendo_simulated,\n",
251+
" red_teaming, context_compliance, first_letter, image\n",
252+
" Default Strategy: default\n",
267253
" Default Datasets (1, max 4 per dataset):\n",
268254
" airt_leakage\n",
269255
"\u001b[1m\u001b[36m\n",
@@ -296,6 +282,21 @@
296282
" Default Datasets (1, max 4 per dataset):\n",
297283
" airt_imminent_crisis\n",
298284
"\u001b[1m\u001b[36m\n",
285+
" airt.rapid_response\u001b[0m\n",
286+
" Class: RapidResponse\n",
287+
" Description:\n",
288+
" Rapid Response scenario for content-harms testing. Tests model behavior\n",
289+
" across multiple harm categories using selectable attack techniques.\n",
290+
" Aggregate Strategies:\n",
291+
" - all, default, single_turn, multi_turn\n",
292+
" Available Strategies (7):\n",
293+
" prompt_sending, role_play, many_shot, tap, crescendo_simulated,\n",
294+
" red_teaming, context_compliance\n",
295+
" Default Strategy: default\n",
296+
" Default Datasets (7, max 4 per dataset):\n",
297+
" airt_hate, airt_fairness, airt_violence, airt_sexual, airt_harassment,\n",
298+
" airt_misinformation, airt_leakage\n",
299+
"\u001b[1m\u001b[36m\n",
299300
" airt.scam\u001b[0m\n",
300301
" Class: Scam\n",
301302
" Description:\n",
@@ -309,6 +310,21 @@
309310
" Default Strategy: all\n",
310311
" Default Datasets (1, max 4 per dataset):\n",
311312
" airt_scams\n",
313+
" Supported Parameters:\n",
314+
" - max_turns (int) [default: 5]: Maximum conversation turns for the persuasive_rta strategy.\n",
315+
"\u001b[1m\u001b[36m\n",
316+
" benchmark.adversarial\u001b[0m\n",
317+
" Class: AdversarialBenchmark\n",
318+
" Description:\n",
319+
" Benchmarking scenario that compares the attack success rate (ASR) of\n",
320+
" several different adversarial models.\n",
321+
" Aggregate Strategies:\n",
322+
" - all, default, single_turn, multi_turn, light\n",
323+
" Available Strategies (4):\n",
324+
" role_play, tap, red_teaming, context_compliance\n",
325+
" Default Strategy: light\n",
326+
" Default Datasets (1, max 8 per dataset):\n",
327+
" harmbench\n",
312328
"\u001b[1m\u001b[36m\n",
313329
" foundry.red_team_agent\u001b[0m\n",
314330
" Class: RedTeamAgent\n",
@@ -359,7 +375,7 @@
359375
"\n",
360376
"================================================================================\n",
361377
"\n",
362-
"Total scenarios: 8\n"
378+
"Total scenarios: 9\n"
363379
]
364380
},
365381
{
@@ -389,11 +405,22 @@
389405
"\n",
390406
"Every scenario can optionally include a **baseline attack** — a `PromptSendingAttack` that sends\n",
391407
"each objective directly to the target without any converters or multi-turn techniques. This is\n",
392-
"controlled by the `include_default_baseline` parameter (default: `True` for most scenarios).\n",
393-
"\n",
394-
"To run *only* the baseline (no attack strategies), create a `RedTeamAgent` with\n",
395-
"`include_baseline=True` (the default) and pass `scenario_strategies=None`. See\n",
396-
"[Common Scenario Parameters](./1_common_scenario_parameters.ipynb) for a working example."
408+
"controlled by the `include_baseline` parameter on `initialize_async`; when omitted, each\n",
409+
"scenario falls back to its own `BASELINE_POLICY` class attribute (most scenarios default\n",
410+
"it on; `Jailbreak` defaults it off). See\n",
411+
"[Common Scenario Parameters](./1_common_scenario_parameters.ipynb) for a worked example.\n",
412+
"\n",
413+
"Custom scenarios should choose their `BASELINE_POLICY` based on whether an unmodified\n",
414+
"prompt is a meaningful comparator for the scenario's strategies:\n",
415+
"\n",
416+
"- **`Enabled`** — the baseline is prepended by default and the caller can opt out. Use when an\n",
417+
" unmodified-prompt run is a meaningful comparison point (most scenarios).\n",
418+
"- **`Disabled`** — the baseline is supported but omitted by default; the caller must opt in. Use\n",
419+
" when the scenario is already dominated by a large set of templates/strategies that already\n",
420+
" exercise the unmodified surface (e.g., `Jailbreak`).\n",
421+
"- **`Forbidden`** — the baseline is unavailable and passing `include_baseline=True` raises. Use\n",
422+
" when the scenario's semantics make a single-shot unmodified prompt meaningless as a comparator\n",
423+
" (e.g., benchmarks comparing across adversarial models, or multi-turn-only scenarios)."
397424
]
398425
},
399426
{
@@ -436,7 +463,7 @@
436463
"name": "python",
437464
"nbconvert_exporter": "python",
438465
"pygments_lexer": "ipython3",
439-
"version": "3.11.15"
466+
"version": "3.12.13"
440467
}
441468
},
442469
"nbformat": 4,

doc/code/scenarios/0_scenarios.py

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,6 @@
7676
# - `version`: Integer version number
7777
# - `strategy_class`: The strategy enum class for this scenario
7878
# - `objective_scorer_identifier`: Identifier dict for the scoring mechanism (optional)
79-
# - `include_default_baseline`: Whether to include a baseline attack (default: True)
8079
# - `scenario_result_id`: Optional ID to resume an existing scenario (optional)
8180
#
8281
# 5. **Initialization**: Call `await scenario.initialize_async()` to populate atomic attacks:
@@ -85,6 +84,8 @@
8584
# - `max_concurrency`: Number of concurrent operations (default: 1)
8685
# - `max_retries`: Number of retry attempts on failure (default: 0)
8786
# - `memory_labels`: Optional labels for tracking (optional)
87+
# - `include_baseline`: Whether to prepend a baseline attack (defaults to the scenario type's
88+
# `BASELINE_POLICY`; most scenarios default it on, `Jailbreak` defaults it off)
8889
#
8990
# ### Example Structure
9091
#
@@ -174,11 +175,22 @@ def _build_display_group(self, *, technique_name: str, seed_group_name: str) ->
174175
#
175176
# Every scenario can optionally include a **baseline attack** — a `PromptSendingAttack` that sends
176177
# each objective directly to the target without any converters or multi-turn techniques. This is
177-
# controlled by the `include_default_baseline` parameter (default: `True` for most scenarios).
178-
#
179-
# To run *only* the baseline (no attack strategies), create a `RedTeamAgent` with
180-
# `include_baseline=True` (the default) and pass `scenario_strategies=None`. See
181-
# [Common Scenario Parameters](./1_common_scenario_parameters.ipynb) for a working example.
178+
# controlled by the `include_baseline` parameter on `initialize_async`; when omitted, each
179+
# scenario falls back to its own `BASELINE_POLICY` class attribute (most scenarios default
180+
# it on; `Jailbreak` defaults it off). See
181+
# [Common Scenario Parameters](./1_common_scenario_parameters.ipynb) for a worked example.
182+
#
183+
# Custom scenarios should choose their `BASELINE_POLICY` based on whether an unmodified
184+
# prompt is a meaningful comparator for the scenario's strategies:
185+
#
186+
# - **`Enabled`** — the baseline is prepended by default and the caller can opt out. Use when an
187+
# unmodified-prompt run is a meaningful comparison point (most scenarios).
188+
# - **`Disabled`** — the baseline is supported but omitted by default; the caller must opt in. Use
189+
# when the scenario is already dominated by a large set of templates/strategies that already
190+
# exercise the unmodified surface (e.g., `Jailbreak`).
191+
# - **`Forbidden`** — the baseline is unavailable and passing `include_baseline=True` raises. Use
192+
# when the scenario's semantics make a single-shot unmodified prompt meaningless as a comparator
193+
# (e.g., benchmarks comparing across adversarial models, or multi-turn-only scenarios).
182194

183195
# %% [markdown]
184196
#

0 commit comments

Comments
 (0)