Skip to content

Commit e151cce

Browse files
rlundeen2Copilotjsong468
authored
MAINT Breaking: Rename Scenario Strategies to Techniques (#2153)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Co-authored-by: jsong468 <songjustin@microsoft.com>
1 parent a355575 commit e151cce

104 files changed

Lines changed: 2047 additions & 2042 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/instructions/scenarios.instructions.md

Lines changed: 32 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ All scenarios inherit from `Scenario` (ABC) and must:
1616
2. **Optionally declare `BASELINE_ATTACK_POLICY`** (defaults to `BaselineAttackPolicy.Enabled` — a baseline `PromptSendingAttack` is prepended and callers can opt out per run by setting `include_baseline=False` in the run params, see "Run Parameters" below):
1717
- `BaselineAttackPolicy.Disabled` — baseline supported but off by default (e.g. `Jailbreak`, where templates dominate the run).
1818
- `BaselineAttackPolicy.Forbidden` — baseline is meaningless for this scenario's comparison axis (e.g. `AdversarialBenchmark`, which compares against gold-standard answers). Supplying `include_baseline=True` raises `ValueError`.
19-
3. **Pass `strategy_class`, `default_strategy`, and `default_dataset_config` to `super().__init__()`:**
19+
3. **Pass `technique_class`, `default_technique`, and `default_dataset_config` to `super().__init__()`:**
2020

2121
```python
2222
class MyScenario(Scenario):
@@ -27,16 +27,16 @@ class MyScenario(Scenario):
2727
def __init__(self, *, objective_scorer=None, scenario_result_id=None) -> None:
2828
super().__init__(
2929
version=self.VERSION,
30-
strategy_class=MyStrategy,
31-
default_strategy=MyStrategy.ALL,
30+
technique_class=MyTechnique,
31+
default_technique=MyTechnique.ALL,
3232
default_dataset_config=DatasetConfiguration(dataset_names=["my_dataset"]),
3333
objective_scorer=objective_scorer or self._get_default_objective_scorer(),
3434
scenario_result_id=scenario_result_id,
3535
)
3636
```
3737

38-
For scenarios whose strategy enum is built dynamically (RapidResponse pattern), build the
39-
strategy class in a module-level `@cache`-decorated function and pass the result through
38+
For scenarios whose technique enum is built dynamically (RapidResponse pattern), build the
39+
technique class in a module-level `@cache`-decorated function and pass the result through
4040
the constructor — no classmethod indirection required.
4141

4242
4. **Implement `_build_atomic_attacks_async(self, *, context)`** — this is the single
@@ -61,11 +61,11 @@ def __init__(
6161
# 2. Store config objects for _build_atomic_attacks_async
6262
self._scorer_config = AttackScoringConfig(objective_scorer=objective_scorer)
6363

64-
# 3. Call super().__init__ — required args: version, strategy_class, objective_scorer
64+
# 3. Call super().__init__ — required args: version, technique_class, objective_scorer
6565
super().__init__(
6666
version=self.VERSION,
67-
strategy_class=MyStrategy,
68-
default_strategy=MyStrategy.ALL,
67+
technique_class=MyTechnique,
68+
default_technique=MyTechnique.ALL,
6969
default_dataset_config=DatasetConfiguration(dataset_names=["my_dataset"]),
7070
objective_scorer=objective_scorer,
7171
)
@@ -78,19 +78,19 @@ Requirements:
7878
`pyrit/common/brick_contract.py`). Violators raise `TypeError` at
7979
import time.
8080
- **All constructor parameters must be optional** (default to `None`) so the registry can instantiate the scenario with no arguments for metadata introspection. Defer required-input validation to `initialize_async()` or `_build_atomic_attacks_async()`. `ScenarioRegistry._build_metadata` raises `TypeError` if `scenario_class()` cannot be called with no arguments.
81-
- `super().__init__()` called with `version`, `strategy_class`, `default_strategy`, `default_dataset_config`, `objective_scorer`
81+
- `super().__init__()` called with `version`, `technique_class`, `default_technique`, `default_dataset_config`, `objective_scorer`
8282
- complex objects like `adversarial_chat` or `objective_scorer` should be passed into the constructor.
8383

8484
## Run Parameters
8585

86-
Run-time inputs (target, strategies, dataset config, concurrency, labels, baseline flag) are **not** arguments to `initialize_async`. They flow through a single parameter bag (`self.params`), populated by `set_params_from_args` from the merged CLI / config / programmatic arguments. `initialize_async` takes no arguments and reads everything from the bag:
86+
Run-time inputs (target, techniques, dataset config, concurrency, labels, baseline flag) are **not** arguments to `initialize_async`. They flow through a single parameter bag (`self.params`), populated by `set_params_from_args` from the merged CLI / config / programmatic arguments. `initialize_async` takes no arguments and reads everything from the bag:
8787

8888
```python
8989
scenario.set_params_from_args(args={"objective_target": target, "max_concurrency": 8})
9090
await scenario.initialize_async()
9191
```
9292

93-
The base `Scenario` declares the common run inputs once in `_common_scenario_parameters()`: `objective_target` (a `RegistryReference` — resolved by name or supplied as an instance), the `opaque` live objects `scenario_strategies` / `strategy_converters` / `dataset_config` / `memory_labels` (passed by identity, never coerced or deep-copied), and the scalars `max_concurrency` / `max_retries` / `include_baseline`.
93+
The base `Scenario` declares the common run inputs once in `_common_scenario_parameters()`: `objective_target` (a `RegistryReference` — resolved by name or supplied as an instance), the `opaque` live objects `scenario_techniques` / `technique_converters` / `dataset_config` / `memory_labels` (passed by identity, never coerced or deep-copied), and the scalars `max_concurrency` / `max_retries` / `include_baseline`.
9494

9595
### Declaring custom parameters — add via `additional_parameters`
9696

@@ -107,7 +107,7 @@ def additional_parameters(cls) -> list[Parameter]:
107107
- **Add (common case):** override `additional_parameters` and return `[Parameter(...)]`
108108
- **Remove / replace a common input (rare):** override `supported_parameters` directly and compose against `super()`, e.g. `return [p for p in super().supported_parameters() if p.name != "dataset_config"]`
109109

110-
Dropping a common input is not silent: `set_params_from_args` rejects any value supplied for an undeclared parameter, so the registry/CLI/programmatic path fails loudly. If a scenario resolves its strategies differently (e.g. pairing attacks with converters), override the `_resolve_scenario_strategies` hook rather than `initialize_async` (see `RedTeamAgent`).
110+
Dropping a common input is not silent: `set_params_from_args` rejects any value supplied for an undeclared parameter, so the registry/CLI/programmatic path fails loudly. If a scenario resolves its techniques differently (e.g. pairing attacks with converters), override the `_resolve_scenario_techniques` hook rather than `initialize_async` (see `RedTeamAgent`).
111111

112112
## Dataset Loading
113113

@@ -126,7 +126,7 @@ DatasetConfiguration(
126126
class MyDatasetConfiguration(DatasetConfiguration):
127127
def get_seed_groups(self) -> dict[str, list[SeedGroup]]:
128128
result = super().get_seed_groups()
129-
# Filter by selected strategies via self._scenario_strategies
129+
# Filter by selected techniques via self._scenario_techniques
130130
return filtered_result
131131
```
132132

@@ -136,12 +136,12 @@ Options:
136136
- `max_dataset_size` — cap per dataset
137137
- Override `_load_seed_groups_for_dataset()` for custom loading
138138

139-
## Strategy Enum
139+
## Technique Enum
140140

141-
Strategy members should represent **attack techniques** — the *how* of an attack (e.g., prompt sending, role play, TAP). Datasets control *what* is tested (e.g., harm categories, compliance topics). Avoid mixing dataset/category selection into the strategy enum; use `DatasetConfiguration` and `--dataset-names` for that axis.
141+
Technique members should represent **attack techniques** — the *how* of an attack (e.g., prompt sending, role play, TAP). Datasets control *what* is tested (e.g., harm categories, compliance topics). Avoid mixing dataset/category selection into the technique enum; use `DatasetConfiguration` and `--dataset-names` for that axis.
142142

143143
```python
144-
class MyStrategy(ScenarioStrategy):
144+
class MyTechnique(ScenarioTechnique):
145145
ALL = ("all", {"all"}) # Required aggregate
146146
DEFAULT = ("default", {"default"}) # Recommended default aggregate
147147
SINGLE_TURN = ("single_turn", {"single_turn"}) # Category aggregate
@@ -157,7 +157,7 @@ class MyStrategy(ScenarioStrategy):
157157

158158
- `ALL` aggregate is always required
159159
- Each member: `NAME = ("string_value", {tag_set})`
160-
- Aggregates expand to all strategies matching their tag
160+
- Aggregates expand to all techniques matching their tag
161161

162162
### Result grouping (`display_group`)
163163

@@ -188,7 +188,7 @@ async def _build_atomic_attacks_async(self, *, context: ScenarioContext) -> list
188188
...
189189
```
190190

191-
`initialize_async` resolves the run's inputs once (objective target, strategies, dataset
191+
`initialize_async` resolves the run's inputs once (objective target, techniques, dataset
192192
config, memory labels, baseline flag, and seed groups), snapshots them into an immutable
193193
`ScenarioContext`, calls this method, and then inserts the baseline centrally. Scenario authors
194194
never read half-initialized `self._*` state to build attacks — read everything from `context`.
@@ -205,17 +205,17 @@ async def _build_atomic_attacks_async(self, *, context: ScenarioContext) -> list
205205
return build_matrix_atomic_attacks(
206206
context=context,
207207
objective_scorer=self._objective_scorer,
208-
strategy_converters=self._strategy_converters, # optional CLI converter stacks
208+
technique_converters=self._technique_converters, # optional CLI converter stacks
209209
)
210210
```
211211

212212
`build_matrix_atomic_attacks`:
213-
1. Calls `resolve_technique_factories(context=context)` to map the selected strategies to their
213+
1. Calls `resolve_technique_factories(context=context)` to map the selected techniques to their
214214
registered `AttackTechniqueFactory` instances (reads the `AttackTechniqueRegistry` singleton;
215-
strategies with no registered factory are dropped).
215+
techniques with no registered factory are dropped).
216216
2. Iterates every (technique × dataset) pair from `context.seed_groups_by_dataset`.
217217
3. Calls `factory.create()` with the objective target, conditional scorer override, and any
218-
per-technique converters (from `--strategies <technique>:converter.<name>`) as
218+
per-technique converters (from `--techniques <technique>:converter.<name>`) as
219219
`extra_request_converters`.
220220
4. Builds each `AtomicAttack` with a unique `atomic_attack_name` and a `display_group`
221221
(customizable via `display_group_fn`).
@@ -235,9 +235,9 @@ and is loaded into the registry by `TechniqueInitializer`.
235235
from pyrit.scenario.core.attack_technique_factory import AttackTechniqueFactory
236236

237237
AttackTechniqueFactory(
238-
name="prompt_sending", # REQUIRED — must match the strategy enum value
238+
name="prompt_sending", # REQUIRED — must match the technique enum value
239239
attack_class=PromptSendingAttack,
240-
strategy_tags=["core", "single_turn", "default"],
240+
technique_tags=["core", "single_turn", "default"],
241241
attack_kwargs={"max_turns": 5},
242242
adversarial_chat=None, # None = resolve adversarial target lazily at create()
243243
seed_technique=None,
@@ -247,9 +247,9 @@ AttackTechniqueFactory(
247247
```
248248

249249
Key points:
250-
- `name` is required and must match the strategy enum value the scenario looks up.
251-
- `strategy_tags` on the factory drives `TagQuery` filters used by
252-
`AttackTechniqueRegistry.build_strategy_class_from_factories(...)`. This is **distinct**
250+
- `name` is required and must match the technique enum value the scenario looks up.
251+
- `technique_tags` on the factory drives `TagQuery` filters used by
252+
`AttackTechniqueRegistry.build_technique_class_from_factories(...)`. This is **distinct**
253253
from the per-entry `tags` argument passed to `registry.register_technique(...)`.
254254
- `uses_adversarial` is auto-derived from the attack class signature (presence of
255255
`attack_adversarial_config`) and seed shape; pass `False` explicitly to opt out, or
@@ -264,7 +264,7 @@ registry = AttackTechniqueRegistry.get_registry_singleton()
264264
registry.register_from_factories(build_technique_factories())
265265
```
266266

267-
`register_from_factories` reads `factory.strategy_tags` to populate the per-entry tags used
267+
`register_from_factories` reads `factory.technique_tags` to populate the per-entry tags used
268268
by the registry. Tests that exercise scenarios should reset both `AttackTechniqueRegistry`
269269
and `TargetRegistry` and re-register a mock `adversarial_chat` so the catalog builder
270270
resolves without falling back to `OpenAIChatTarget`.
@@ -274,14 +274,14 @@ resolves without falling back to `OpenAIChatTarget`.
274274
The baseline (a `PromptSendingAttack` over the run's seeds) is inserted **centrally** by
275275
`Scenario.initialize_async` according to the scenario's `BASELINE_ATTACK_POLICY` class var and
276276
the runtime `include_baseline` flag. `_build_atomic_attacks_async` must **never** prepend its own
277-
baseline — doing so double-emits it and reintroduces baseline-vs-strategy population divergence
277+
baseline — doing so double-emits it and reintroduces baseline-vs-technique population divergence
278278
under `max_dataset_size`.
279279

280280
### Manual AtomicAttack construction:
281281

282282
```python
283283
AtomicAttack(
284-
atomic_attack_name=strategy_name, # groups related attacks
284+
atomic_attack_name=technique_name, # groups related attacks
285285
attack_technique=AttackTechnique(attack=attack_instance), # bundles the AttackStrategy
286286
seed_groups=list(seed_groups), # must be non-empty
287287
memory_labels=context.memory_labels, # from the context snapshot
@@ -290,7 +290,7 @@ AtomicAttack(
290290

291291
- `seed_groups` must be non-empty — validate before constructing
292292
- Read runtime inputs from `context`, not `self._*``self._objective_target` and
293-
`self._scenario_strategies` are only populated after `initialize_async()`
293+
`self._scenario_techniques` are only populated after `initialize_async()`
294294
- Pass `memory_labels` to every AtomicAttack
295295

296296
## Exports
@@ -299,7 +299,7 @@ New scenarios must be registered in `pyrit/scenario/__init__.py` as virtual pack
299299

300300
## Common Review Issues
301301

302-
- Accessing `self._objective_target` or `self._scenario_strategies` before `initialize_async()`
302+
- Accessing `self._objective_target` or `self._scenario_techniques` before `initialize_async()`
303303
- Overriding `supported_parameters()` without composing against `super()` (silently drops the common run inputs)
304304
- Adding arguments back onto `initialize_async` instead of declaring them via `supported_parameters()` and reading from `self.params`
305305
- Forgetting `@apply_defaults` on `__init__`

doc/code/scenarios/0_attack_techniques.ipynb

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
"- a **`SeedAttackTechniqueGroup`** (`seed_technique`) of general-technique seeds, which can carry a\n",
3535
" **system prompt**, a **prepended_conversation**, a **simulated_conversation**\n",
3636
" (`SeedSimulatedConversation`), and a **next_message**;\n",
37-
"- the selection metadata that lets a scenario pick it: its `name` and `strategy_tags`.\n",
37+
"- the selection metadata that lets a scenario pick it: its `name` and `technique_tags`.\n",
3838
"\n",
3939
"The objective is *not* part of the technique — it stays separate and is supplied by the dataset at\n",
4040
"run time. You rarely build a technique by hand; instead you register a **factory** and let scenarios\n",
@@ -106,7 +106,7 @@
106106
" \"Technique\": name,\n",
107107
" \"Attack (executor)\": f.attack_class.__name__,\n",
108108
" \"Adversarial?\": \"yes\" if f.uses_adversarial else \"no\",\n",
109-
" \"Tags\": \", \".join(f.strategy_tags),\n",
109+
" \"Tags\": \", \".join(f.technique_tags),\n",
110110
" }\n",
111111
" for name, f in factories.items()\n",
112112
"]\n",
@@ -124,7 +124,7 @@
124124
"## How techniques are selected\n",
125125
"\n",
126126
"Scenarios don't reference factories directly. Instead, a scenario's\n",
127-
"[`ScenarioStrategy`](../../../pyrit/scenario/core/scenario_strategy.py) enum is built *from* the\n",
127+
"[`ScenarioTechnique`](../../../pyrit/scenario/core/scenario_technique.py) enum is built *from* the\n",
128128
"registered factories: every technique becomes an enum member, and the factory's tags become\n",
129129
"selectable aggregates. That gives you three ways to choose what runs:\n",
130130
"\n",
@@ -134,15 +134,15 @@
134134
"- **Composite** — pair a technique with converters (see\n",
135135
" [Common Scenario Parameters](./1_common_scenario_parameters.ipynb)).\n",
136136
"\n",
137-
"On the command line this is the `--strategy` flag of\n",
138-
"[`pyrit_scan`](../../scanner/1_pyrit_scan.ipynb); programmatically it's the `scenario_strategies`\n",
139-
"argument to `initialize_async`. The grouping is what lets `--strategy single_turn` or\n",
140-
"`--strategy light` fan out to a whole family of techniques without naming each one.\n",
137+
"On the command line this is the `--technique` flag of\n",
138+
"[`pyrit_scan`](../../scanner/1_pyrit_scan.ipynb); programmatically it's the `scenario_techniques`\n",
139+
"argument to `initialize_async`. The grouping is what lets `--technique single_turn` or\n",
140+
"`--technique light` fan out to a whole family of techniques without naming each one.\n",
141141
"\n",
142142
"```mermaid\n",
143143
"flowchart LR\n",
144144
" I[\"TechniqueInitializer\"] -->|registers factories| R[\"AttackTechniqueRegistry\"]\n",
145-
" R -->|builds enum + tags| S[\"ScenarioStrategy\"]\n",
145+
" R -->|builds enum + tags| S[\"ScenarioTechnique\"]\n",
146146
" S -->|name / tag / composite| Sc[\"Scenario\"]\n",
147147
" R -->|create with target + scorer| T[\"AttackTechnique<br/>(attack + seeds)\"]\n",
148148
" Sc --> T\n",
@@ -177,7 +177,7 @@
177177
" AttackTechniqueFactory(\n",
178178
" name=\"my_role_play\",\n",
179179
" attack_class=RolePlayAttack,\n",
180-
" strategy_tags=[\"single_turn\", \"custom\"],\n",
180+
" technique_tags=[\"single_turn\", \"custom\"],\n",
181181
" attack_kwargs={\"role_play_definition_path\": RolePlayPaths.MOVIE_SCRIPT.value},\n",
182182
" )\n",
183183
" ]\n",
@@ -186,7 +186,7 @@
186186
"\n",
187187
"Wrap registration in a `PyRITInitializer` (as `TechniqueInitializer` does) when you want it\n",
188188
"to run as part of standard setup. Any scenario built afterwards will see `my_role_play` as a\n",
189-
"selectable strategy."
189+
"selectable technique."
190190
]
191191
}
192192
],

0 commit comments

Comments
 (0)