Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
ae9bac0
Fix request converter scoping
Copilot Aug 7, 2026
ae9775d
Merge converter scoping fix
Copilot Aug 7, 2026
5d70865
Fix non-chat converter role scoping
Copilot Aug 8, 2026
5c759f7
Clarify converter composition compatibility
Copilot Aug 10, 2026
408e066
Reject lossy non-chat modality flattening
Copilot Aug 10, 2026
029a1ed
Fix converter retry and index scoping
Copilot Aug 10, 2026
9154232
Restrict jailbreak converter to prompt sending
Copilot Aug 10, 2026
0a692f8
Merge origin/main into converter scoping fix
Copilot Aug 11, 2026
3db85d3
Preserve upstream ShieldGemma exports
Copilot Aug 11, 2026
3530751
Tighten converted view parameter type
Copilot Aug 11, 2026
18b1df9
Clarify non-chat converter flow
Copilot Aug 12, 2026
78f83f1
Clarify request converter marker lifetime
Copilot Aug 12, 2026
0b8532e
Move prepended history adaptation to targets
Copilot Aug 14, 2026
aff1006
Refine prepended history target normalization
Copilot Aug 19, 2026
14ce81e
Preserve system context across target rotation
Copilot Aug 20, 2026
290a5b8
Unify target history normalization
Copilot Aug 20, 2026
f3c6a85
Merge remote-tracking branch 'origin/main' into romanlutz-fix-convert…
Copilot Aug 20, 2026
a7318a2
Update converter scoping documentation
Copilot Aug 20, 2026
45a388d
FIX Preserve target formatting across conversation rotation
rlundeen2 Aug 21, 2026
4f02e90
FIX Make prepended history capability-driven
rlundeen2 Aug 21, 2026
57c3cf5
Merge origin/main into romanlutz-fix-converter-scoping-692
Copilot Aug 21, 2026
341c8f7
Merge lower normalization layer and harden history handling
Copilot Aug 21, 2026
913197b
Fix TAP seed boundary replay
Copilot Aug 21, 2026
8acc519
Fix cloned target seed consumption
Copilot Aug 21, 2026
a363da0
Merge origin/main into romanlutz-fix-converter-scoping-692
Copilot Aug 21, 2026
3338f94
FIX Centralize prepended conversation policy
rlundeen2 Aug 21, 2026
2ce05b7
Synchronize centralized prepended policy docs
Copilot Aug 21, 2026
8a1dbd5
Move prepended history state to attacks
Copilot Aug 21, 2026
f5a7da5
Merge latest main into converter scoping
Copilot Aug 21, 2026
73336d9
Address prepended history lifecycle feedback
Copilot Aug 22, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 22 additions & 2 deletions doc/code/executor/3_attack_configuration.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -462,15 +462,35 @@
"Beyond the call arguments, attacks are tuned at construction time with three configuration objects:\n",
"\n",
"- **`AttackConverterConfig`** — request/response [converters](../converters/0_converters.ipynb)\n",
" applied to every prompt and response.\n",
" applied to live attack prompts and responses, plus selected roles in prepended history.\n",
"- **`AttackScoringConfig`** — the objective scorer plus any auxiliary\n",
" [scorers](../scoring/0_scoring.ipynb).\n",
"- **`AttackAdversarialConfig`** — the adversarial target (a model PyRIT controls) that multi-turn\n",
" attacks use to generate each next prompt (see [Multi-Turn Attacks](2_multi_turn.ipynb)).\n",
"\n",
"Converter and scoring configs apply to single- and multi-turn attacks alike; the adversarial config\n",
"only applies to attacks that drive a conversation. Below builds a converter config — it's just a\n",
"plain object you hand to the attack constructor."
"plain object you hand to the attack constructor.\n",
"\n",
"Request converters apply to prepended `user` messages by default. Prepended `assistant` messages\n",
"represent simulated target output, so PyRIT leaves them unchanged unless the attack explicitly\n",
"opts in. For example:\n",
"\n",
"```python\n",
"from pyrit.executor.attack import PrependedConversationConfig\n",
"\n",
"attack = PromptSendingAttack(\n",
" objective_target=target,\n",
" attack_converter_config=converter_config,\n",
" prepended_conversation_config=PrependedConversationConfig(\n",
" apply_converters_to_roles=[\"user\", \"assistant\"],\n",
" ),\n",
")\n",
"```\n",
"\n",
"PyRIT applies these role-specific conversions while the prepended messages are still structured.\n",
"If the target cannot accept editable history, target normalization then formats the converted and\n",
"unconverted history with the first live request without broadening the selected converter scope."
]
},
{
Expand Down
22 changes: 21 additions & 1 deletion doc/code/executor/3_attack_configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@
# Beyond the call arguments, attacks are tuned at construction time with three configuration objects:
#
# - **`AttackConverterConfig`** — request/response [converters](../converters/0_converters.ipynb)
# applied to every prompt and response.
# applied to live attack prompts and responses, plus selected roles in prepended history.
# - **`AttackScoringConfig`** — the objective scorer plus any auxiliary
# [scorers](../scoring/0_scoring.ipynb).
# - **`AttackAdversarialConfig`** — the adversarial target (a model PyRIT controls) that multi-turn
Expand All @@ -175,6 +175,26 @@
# Converter and scoring configs apply to single- and multi-turn attacks alike; the adversarial config
# only applies to attacks that drive a conversation. Below builds a converter config — it's just a
# plain object you hand to the attack constructor.
#
# Request converters apply to prepended `user` messages by default. Prepended `assistant` messages
# represent simulated target output, so PyRIT leaves them unchanged unless the attack explicitly
# opts in. For example:
#
# ```python
# from pyrit.executor.attack import PrependedConversationConfig
#
# attack = PromptSendingAttack(
# objective_target=target,
# attack_converter_config=converter_config,
# prepended_conversation_config=PrependedConversationConfig(
# apply_converters_to_roles=["user", "assistant"],
# ),
# )
# ```
#
# PyRIT applies these role-specific conversions while the prepended messages are still structured.
# If the target cannot accept editable history, target normalization then formats the converted and
# unconverted history with the first live request without broadening the selected converter scope.

# %%
from pyrit.converter import Base64Converter
Expand Down
13 changes: 10 additions & 3 deletions doc/code/framework.md
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,9 @@ If you are contributing to PyRIT, that work will most likely land in one of the
- This is often an LLM, but it doesn't have to be. For Cross-Domain Prompt Injection Attacks, the target might be a storage account that a later target has a reference to. Message and conversation should be generic enough to handle this extra data.
- Target capabilities should be used to see if a target is compatible with the capabilities that the other components want to use.
- Targets should use message_normalizer along with TargetConfiguration to transform `Messages` into formats that target supports.
- A target may observe an internal, caller-owned send context at the provider-invocation boundary,
after target-side waits and immediately before irreversible provider I/O, but the caller owns any
bootstrap-history identity, replay, or branching state.
- Because targets are so varied, it is reasonable to return multiple tool calls, or none at all.
- One attack can have many targets (and in fact, converters and scorers can also use targets to convert/score the prompt).
- **Does not own**: what to send or what to do with the response. A target sends a prepared `Message` and returns a response — it doesn't convert prompts (converters), score (scorers), manage the conversation or decide the next turn (attacks), apply attack logic, or persist prompts and responses to memory (the `prompt_normalizer` owns that). Its retries stay at the target layer (e.g. `RateLimitException`).
Expand Down Expand Up @@ -314,10 +317,14 @@ The below talks about responsibilities of most modules in the PyRIT library

## [Normalizers](./targets/11_message_normalizer)

**Responsibility**: Reshape prompts and conversations so components and targets can interoperate. There are two distinct modules:
**Responsibility**: Reshape prompts and conversations so components and targets can interoperate.

- **`prompt_normalizer`** applies converters and dispatches individual prompts to a `PromptTarget` (handling batching and memory persistence). It is the single component that writes each request and response to memory; targets never persist on their own. `NormalizerRequest` and `ConverterConfiguration` describe what to send and which converters to apply.
- **`message_normalizer`** reshapes multi-message conversation payloads into the structure a given model expects — for example, handling system-message behavior (keep / squash / ignore), history squashing, and tokenizer chat templates.
- **`prompt_normalizer`** applies converters, persists requests and responses, and dispatches prompts to a `PromptTarget`. Targets do not persist messages.
- **`message_normalizer`** reshapes conversations into target-compatible payloads. It owns target-facing representation, not attack policy or conversation state.
- Prepended-history identity and delivery state belong to the attack execution context. Targets and normalizers consume only the narrow send-time view needed for provider adaptation.
- **Does not own**: the conversation of record. Memory is canonical; a normalized payload is an ephemeral target-facing view that is never written back.

See [message normalizers](./targets/11_message_normalizer) for capability behavior, processing order, and prepended-history lifecycle details.

## [Output](./output/0_output)

Expand Down
54 changes: 39 additions & 15 deletions doc/code/scenarios/0_scenarios.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@
" - `max_retries`: Number of retry attempts on failure (default: 0)\n",
" - `memory_labels`: Optional labels for tracking (optional)\n",
" - `include_baseline`: Whether to prepend a baseline attack (defaults to the scenario type's\n",
" `BASELINE_ATTACK_POLICY`; most scenarios default it on, `Jailbreak` defaults it off)\n",
" `BASELINE_ATTACK_POLICY`; most scenarios, including `Jailbreak`, default it on)\n",
"\n",
"### Example Structure\n",
"\n",
Expand Down Expand Up @@ -241,17 +241,42 @@
" airt.jailbreak\u001b[0m\n",
" Class: Jailbreak\n",
" Description:\n",
" Jailbreak scenario implementation for PyRIT. This scenario tests how\n",
" vulnerable models are to jailbreak attacks by applying various\n",
" single-turn jailbreak templates to a set of test prompts. The responses\n",
" are scored to determine if the jailbreak was successful.\n",
" Jailbreak scenario implementation for PyRIT. Tests how vulnerable a\n",
" model is to jailbreak templates. A run is the cross-product of three\n",
" selectors: - **dataset** — the harmful objectives (HarmBench). -\n",
" **techniques** — two delivery methods for each jailbreak:\n",
" ``prompt_sending`` (the template rendered inline into the user message)\n",
" and ``jailbreak_system_prompt`` (the template set as the system prompt\n",
" with the objective sent as the user turn). - **jailbreaks** — which\n",
" jailbreak templates to run (a random ``num_jailbreaks`` sample or an\n",
" explicit ``jailbreak_names`` set). ``prompt_sending`` applies each\n",
" template as a ``TextJailbreakConverter`` on the outgoing request, so the\n",
" objective is rendered inline into the template's ``{{prompt}}`` slot.\n",
" ``jailbreak_system_prompt`` instead sets the template as a native system\n",
" prompt and sends the objective as its own user turn, so it is only built\n",
" for targets that natively support editable history and system prompts\n",
" (it is skipped for incapable targets, or raises if it is the only\n",
" selected technique). Responses are scored to determine whether the\n",
" jailbreak succeeded (non-refusal).\n",
" Aggregate Techniques:\n",
" - all, simple, complex\n",
" Available Techniques (4):\n",
" prompt_sending, many_shot, skeleton, role_play\n",
" Default Technique: simple\n",
" Default Datasets (1, max 4 per dataset):\n",
" airt_harms\n",
" - all, default, single_turn\n",
" Available Techniques (2):\n",
" prompt_sending, jailbreak_system_prompt\n",
" Default Technique: default\n",
" Default Datasets (1):\n",
" harmbench\n",
" Supported Parameters:\n",
" - objective_target (any): Target system under attack: a registered target name or a PromptTarget instance.\n",
" - scenario_techniques (any): Techniques to execute; defaults to the scenario's default aggregate when omitted.\n",
" - technique_converters (any): Mapping of concrete technique name to extra request converters to append.\n",
" - dataset_config (any): Dataset source configuration; defaults to the scenario's default when omitted.\n",
" - memory_labels (any): Additional labels applied to every attack run in the scenario.\n",
" - max_concurrency (int) [default: 4]: Maximum number of concurrent units of work for the scenario.\n",
" - max_retries (int) [default: 0]: Maximum number of automatic retries if the scenario raises an exception.\n",
" - include_baseline (bool): Whether to prepend a baseline atomic attack; None defers to BASELINE_ATTACK_POLICY.\n",
" - num_jailbreaks (int): Draw this many random jailbreak templates for the run. Mutually exclusive with jailbreak_names.\n",
" - num_jailbreak_attempts (int) [default: 1]: Number of times to try each (technique x jailbreak template x objective).\n",
" - jailbreak_names (list[str]): Explicit jailbreak template file names to run (e.g. aim.yaml dan_11.yaml). When omitted, a random sample is drawn. Mutually exclusive with num_jailbreaks.\n",
"\u001b[1m\u001b[36m\n",
" airt.leakage\u001b[0m\n",
" Class: Leakage\n",
Expand Down Expand Up @@ -439,8 +464,8 @@
"each objective directly to the target without any converters or multi-turn techniques. This is\n",
"controlled by the `include_baseline` scenario parameter, supplied through the CLI, config, or\n",
"`set_params_from_args` before `initialize_async`; when omitted, each scenario falls back to its\n",
"own `BASELINE_ATTACK_POLICY` class attribute (most scenarios default it on; `Jailbreak` defaults\n",
"it off). See\n",
"own `BASELINE_ATTACK_POLICY` class attribute (most scenarios, including `Jailbreak`, default it\n",
"on). See\n",
"[Common Scenario Parameters](./1_common_scenario_parameters.ipynb) for a worked example.\n",
"\n",
"Custom scenarios should choose their `BASELINE_ATTACK_POLICY` based on whether an unmodified\n",
Expand All @@ -449,8 +474,7 @@
"- **`Enabled`** — the baseline is prepended by default and the caller can opt out. Use when an\n",
" unmodified-prompt run is a meaningful comparison point (most scenarios).\n",
"- **`Disabled`** — the baseline is supported but omitted by default; the caller must opt in. Use\n",
" when the scenario is already dominated by a large set of templates/techniques that already\n",
" exercise the unmodified surface (e.g., `Jailbreak`).\n",
" when an unmodified-prompt comparison is valid but not useful enough to run by default.\n",
"- **`Forbidden`** — the baseline is unavailable and passing `include_baseline=True` raises. Use\n",
" when the scenario's semantics make a single-shot unmodified prompt meaningless as a comparator\n",
" (e.g., benchmarks comparing across adversarial models, or multi-turn-only scenarios)."
Expand Down
9 changes: 4 additions & 5 deletions doc/code/scenarios/0_scenarios.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@
# - `max_retries`: Number of retry attempts on failure (default: 0)
# - `memory_labels`: Optional labels for tracking (optional)
# - `include_baseline`: Whether to prepend a baseline attack (defaults to the scenario type's
# `BASELINE_ATTACK_POLICY`; most scenarios default it on, `Jailbreak` defaults it off)
# `BASELINE_ATTACK_POLICY`; most scenarios, including `Jailbreak`, default it on)
#
# ### Example Structure
#
Expand Down Expand Up @@ -180,8 +180,8 @@ async def _build_atomic_attacks_async(self, *, context):
# each objective directly to the target without any converters or multi-turn techniques. This is
# controlled by the `include_baseline` scenario parameter, supplied through the CLI, config, or
# `set_params_from_args` before `initialize_async`; when omitted, each scenario falls back to its
# own `BASELINE_ATTACK_POLICY` class attribute (most scenarios default it on; `Jailbreak` defaults
# it off). See
# own `BASELINE_ATTACK_POLICY` class attribute (most scenarios, including `Jailbreak`, default it
# on). See
# [Common Scenario Parameters](./1_common_scenario_parameters.ipynb) for a worked example.
#
# Custom scenarios should choose their `BASELINE_ATTACK_POLICY` based on whether an unmodified
Expand All @@ -190,8 +190,7 @@ async def _build_atomic_attacks_async(self, *, context):
# - **`Enabled`** — the baseline is prepended by default and the caller can opt out. Use when an
# unmodified-prompt run is a meaningful comparison point (most scenarios).
# - **`Disabled`** — the baseline is supported but omitted by default; the caller must opt in. Use
# when the scenario is already dominated by a large set of templates/techniques that already
# exercise the unmodified surface (e.g., `Jailbreak`).
# when an unmodified-prompt comparison is valid but not useful enough to run by default.
# - **`Forbidden`** — the baseline is unavailable and passing `include_baseline=True` raises. Use
# when the scenario's semantics make a single-shot unmodified prompt meaningless as a comparator
# (e.g., benchmarks comparing across adversarial models, or multi-turn-only scenarios).
Expand Down
44 changes: 39 additions & 5 deletions doc/code/targets/0_prompt_targets.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,49 @@ Prompt Targets are endpoints for where to send prompts. For example, a target co
Prompt targets are found [here](https://github.com/microsoft/PyRIT/tree/main/pyrit/prompt_target/) in code.


## Send_Prompt_Async
## `send_prompt_async`

The main entry method follow the following signature:
The main entry method has the following signature:

```
async def send_prompt_async(self, *, message: Message) -> Message:
```python
async def send_prompt_async(
self,
*,
message: Message,
send_context: TargetSendContext | None = None,
) -> list[Message]:
```

A `Message` object is a normalized object with all the information a target will need to send a prompt, including a way to get a history for that prompt (in the cases that also needs to be sent). This is discussed in more depth [here](../memory/3_memory_data_types.md).
A `Message` object contains the current request and the identifiers needed to load its conversation
history. This is discussed in more depth [here](../memory/3_memory_data_types.md).

`send_context` is an internal protocol that lets caller-owned execution state select persisted history
and observe the provider-attempt boundary. Attacks with prepended history own the concrete
`PrependedHistorySendContext`; targets do not construct it, clone it, or decide whether its seed should
be replayed. Before a provider send, `PromptTarget` loads memory history, asks the protocol for the
caller-approved target view, and then runs the target's capability-normalization pipeline. Request
converters have already run by this point, so role-specific converter choices remain intact even when
the target must receive one flattened request. The context is ephemeral and does not replace the
structured messages stored in memory.

For a stateful target without editable history, the initial bootstrap is flattened once. Later sends
retain replayable memory history in the normalized view so a target such as `WebsocketTarget` can
restore a replaced provider session, while the existing provider session still receives only the
current request. A stateful TAP clone bootstraps its new provider session with the complete replayable
duplicated branch, even when the attack started without an explicit prepended seed. Stateless targets
continue to receive only the explicit prepended seed plus each current request, never prior live branch
turns. A stateful TAP target without editable history can flatten only text converter output when
branching; non-text converter output requires an editable-history target, a stateless target, or
`branching_factor=1` so copied media is never replayed under a different role.

The provider-attempt signal is emitted after shared target-side rate limiting. Targets that need more
precise setup, such as WebSocket, Playwright, or conversation-keyed HTTP targets, emit it immediately
before the irreversible provider operation. Cancellation before that point leaves a one-time
bootstrap available for retry.

`send_prompt_async` is the final public orchestration method. Custom target subclasses implement
`_send_prompt_to_target_async(*, normalized_conversation: list[Message]) -> list[Message]` instead of
overriding `send_prompt_async`.

## Chat-style targets vs general targets

Expand Down
Loading