From a7315d187e3c043db44056125ff60e2846c808c5 Mon Sep 17 00:00:00 2001 From: Richard Lundeen Date: Fri, 26 Jun 2026 16:31:51 -0700 Subject: [PATCH 01/14] Update framework.md architecture with new responsibility structure Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- doc/code/framework.md | 143 +++++++++++++++++++++++++++++++++--------- 1 file changed, 113 insertions(+), 30 deletions(-) diff --git a/doc/code/framework.md b/doc/code/framework.md index 6425557ffa..65fa63f466 100644 --- a/doc/code/framework.md +++ b/doc/code/framework.md @@ -69,63 +69,146 @@ The main components of PyRIT are prompts, attacks, converters, targets, and scor As much as possible, each component is a pluggable brick of functionality. Prompts from one attack can be used in another. An attack for one scenario can use multiple targets. And sometimes you completely skip components (e.g. almost every component can be a NoOp also, you can have a NoOp converter that doesn't convert, or a NoOp target that just prints the prompts). -If you are contributing to PyRIT, that work will most likely land in one of these buckets and be as self-contained as possible. It isn't always this clean, but when an attack scenario doesn't quite fit (and that's okay!) it's good to brainstorm with the maintainers about how we can modify our architecture. +If you are contributing to PyRIT, that work will most likely land in one of the core components buckets and be as self-contained as possible. It isn't always this clean, but when an attack scenario doesn't quite fit (and that's okay!) it's good to brainstorm with the maintainers about how we can modify our architecture. Also, if our **Framework Plans** would be helpful, please open issues! -The remainder of this document talks about the different components, how they work, what their responsibilities are, and ways to contribute. +# Core Components +## [Datasets](./datasets/0_dataset) -## Datasets: Prompts, Jailbreak Templates, Source Images, Attack Strategies, etc. +**Responsibility**: Create a single place to manage prompts -The first piece of an attack is often a dataset piece, like a prompt. "Tell me how to create a Molotov cocktail" is an example of a prompt. PyRIT is a good place to have a library of things to check for. +- New Datasets can be added in the dataset module. +- Datasets should never be retrieved from DatasetProviders; DatasetProviders should load into memory, and then components retireve from memory +- Most components should always work with seeds passed directly in (except scenarios which may package them from memory). Never use DatasetProfiders, file paths, etc. Either pass the seed as an argument or retrieve from memory. -Ways to contribute: Check out our documentation on [seed datasets](./datasets/0_dataset.md); are there more prompts and jailbreak templates you can add that include scenarios you're testing for? +**Framework Plans**: -## Attacks +- There is some churn here. We haven't managed these much at scale, and we may have to redefine how it works. +- We want more investment in managing datasets and loading them more intelligently +- We need to more consistently pass seeds or use memory -Attacks are responsible for putting all the other pieces together. They make use of all other components in PyRIT to execute an attack technique end-to-end. -PyRIT supports single-turn (e.g. Many Shot Jailbreaks [@anthropic2024manyshot], Role Play, Skeleton Key [@microsoft2024skeletonkey]) and multi-turn attack strategies (e.g. Tree of Attacks [@mehrotra2023tap], Crescendo [@russinovich2024crescendo]), and compound strategies (e.g. `SequentialAttack`) for chaining several techniques against a single objective. +**Contributing (difficulty easy)**: Are there more prompts and jailbreak templates you can add that include scenarios you're testing for? It is easy to add new dataset providers. -Ways to contribute: Check out our [attack docs](./executor/0_executor.md). There are hundreds of attacks outlined in research papers. A lot of these can be captured within PyRIT. If you find an attack that doesn't fit the attack model please notify the team. Are there scenarios you can write attack modules for? +## [Attacks](./executor/0_executor) -## Converters +**Responsibility**: Manage conversations between objective targets and adversarial targets; using datasets, scorers, and converters to achieve an objective. -Converters are a powerful component that converts prompts to something else. They can be stacked and combined. They can be as varied as translating a text prompt into a Word document, rephrasing a prompt in 100 different ways, or adding a text overlay to an image. +- Any branching decision (e.g. the next thing(s) to do is based on a previous result) should be an attack. +- Attacks should always make use of other component's responsibilities. An attack should alwways branch based on a scorer and NOT a direct response. (e.g. was this prompt blocked? is a scorer responsibility, not an attack responsibility) +- Attacks should use scoring and target capabilities implicitly. Attacks should support multi-modal. +- Compound attacks are possible, combining different attacks in different ways. -Ways to contribute: Check out our [converter docs](./converters/0_converters.ipynb). Are there ways prompts can be converted that would be useful for an attack? +**Rough Framework Plans**: -## Target +- We need to move some older attacks that don't belong here. Many (FlipAttack) should just be attack techniques +- There are potential ways we could combine different algorithms. Are Crescendo and TAP ultimately the same? +- We need to support target capabilities more implicitly +- Other executors, like benchmarks, need better end-to-end support; potentially including an `ExpectedResult` seed and associated scorers. +- More flexible compound attacks should continue to be added -A Prompt Target can be thought of as "the thing we're sending the prompt to". +**Contributing (difficulty high)**: The best way to contribute is likely opening issues if you run into limitations. -This is often an LLM, but it doesn't have to be. For Cross-Domain Prompt Injection Attacks, the Prompt Target might be a Storage Account that a later Prompt Target has a reference to. +## [Attack Technique] -One attack can have many Prompt Targets (and in fact, converters and Scoring Engine can also use Prompt Targets to convert/score the prompt). +**Responsibility**: An attack technique packages an executor, converters, datasets, and strategies into a single attack. The goal is that any attack (something trying to achieve an objective) can be defined as an attack technique. -Ways to contribute: Check out our [target docs](./targets/0_prompt_targets.md). Are there models you want to use at any stage or for different attacks? +**Rough Framework Plans**: +- Managing these better, so scenarios can more easily select or build the attack techniques to use -## Scoring Engine +**Contributing (difficulty easy)**: Simply add the attack technique to one of the initializers. -The scoring engine is a component that gives feedback to the attack on what happened with the prompt. This could be as simple as "Was this prompt blocked?" or "Was our objective achieved?" +## [Scenarios](./scenarios/0_scenarios) -Ways to contribute: Check out our [scoring docs](./scoring/0_scoring.ipynb). Is there data you want to use to make decisions or analyze? +**Responsibility**: This is the avenue to "run PyRIT against something". What does that look like? -## Memory +- A scenario takes user input and uses it to package datasets with attack techniques +- A scenario orchestrates resiliency and parallelism from a high level +- No result should depend on previous results (that is an attack's job) -One important thing to remember about this architecture is its swappable nature. Prompts and targets and converters and attacks and scorers should all be swappable. But sometimes one of these components needs additional information. If the target is an LLM, we need a way to look up previous messages sent to that session so we can properly construct the new message. If the target is a blob store, we need to know the URL to use for a future attack. +**Rough Framework Plans**: + +- Scenarios are new enough that we are still discovering patterns and limitations. So they will regularly be refactored + +**Contributing (difficulty medium)**: Is there a scanner that does something PyRIT doesn't? Add it as a scenario. But because we're changing how things are done rapidly, it is not as well-defined as other areas. + +## [Converters](./converters/0_converters) + +**Responsibility**: Converters are a component that converts prompts to something else. They can be stacked and combined. They can be as varied as translating a text prompt into a Word document, rephrasing a prompt, or adding a text overlay to an image. + +**Rough Framework Plans**: + +- We want to refactor our converter pipeline, so there are currently some things that should be converters that we may want to postpone (e.g. partial converting). This is supported but could be much more dynamic. + +**Contributing (difficulty low)**: The existing pattern is well-defined. Are there ways prompts can be converted that would be useful for an attack? + +## [Target](./targets/0_prompt_targets.md) + +**Responsibility**: A Prompt Target can be thought of as "the thing we're sending the prompt to". Many other components use it, including scorers, attacks, and converters. + +- This is often an LLM, but it doesn't have to be. For Cross-Domain Prompt Injection Attacks, the Prompt Target might be a Storage Account that a later Prompt Target has a reference to. Message and conversation should be generic enough to handle this extra data. +- Prompt 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 PromptCapabilities to transorm `Messages` into formats that target supports. +- Because targets are so varied, it is reasonable to return multiple tool calls, or none at all. +- One attack can have many Prompt Targets (and in fact, converters and Scoring Engine can also use Prompt Targets to convert/score the prompt). + +**Rough Framework Plans**: + +- Better agent support may require extra pieces attached to a Message +- Better surface support may require expanding the return types + +**Contributing (difficulty low)**: + +- The pattern is well-defined. +- Are there models you want to use at any stage or for different attacks? But also, can your model just be one of the existing targets? -For more details about memory configuration, please follow the guide in [memory](./memory/0_memory.md). +## [Scoring](./scoring/0_scoring.ipynb) -Memory modifications and contributions should usually be designed with the maintainers. +**Responsibility**: The scoring engine is a component that gives feedback to the attack on what happened with the prompt. This could be as simple as "Was this prompt blocked?" or "Was our objective achieved?" -## The Flow +- Any decision an attack makes should be based on a scorer result -To some extent, the ordering in this diagram matters. In the simplest cases, you have a prompt, an attack takes the prompt, uses prompt normalizer to run it through converters and send to a target, and the result is scored. +**Contributing (difficulty low)**: -But this simple view is complicated by the fact that an attack can have multiple targets, converters can be stacked, scorers can use targets to score, etc. +- The pattern is well-defined. +- You can evaluate how accurate probabalistic scorers are and likely make them more accurate. +- Is there data you want to use to make decisions or analyze? + +**Framework Plans**: + +- Scorers will be refactored to be more generic, so they can determine more general results (does a file exist? Was a tool called?) + +# Core library + +The below talks about responsibilities of several modules in the PyRIT library + +## [Registry](./registry/0_registry) + +**Responsibility**: The registry is used to build and store the core components. + +- If you are creating a component with user input (e.g. via config, REST, or automatically) it should always use the registry +- If you are storing an instance of a component, it should always use the registry + +## [Models] + +**Responsibility**: pyrit.models is a lightweight module where core types are defined. These should always be used where possible to prevent drift. + +- If you are creating a class that has a lot of overlap with another class, or using a dict to serialize across boundaries, consider if you can use/move pyrit.models +- Models includes `identifiers` which are descriptions of the core components. And along with the registry, can often recreate those components. +- Models includes types passed around between components, and should be prefered in REST +- models should never include any dependencies outside of pyrit.common (which shouldn't depend on anything) + +## Output + +**Responsibility**: The Output module is responsible for writing different components in different formats to different places. + +## [Memory](./memory/0_memory.md) + +One important thing to remember about this architecture is its swappable nature. Prompts and targets and converters and attacks and scorers should all be swappable. But sometimes one of these components needs additional information. If the target is an LLM, we need a way to look up previous messages sent to that session so we can properly construct the new message. If the target is a blob store, we need to know the URL to use for a future attack. -Sometimes, if a scenario requires specific data, we may need to modify the architecture. This happened recently when we thought a single target may take multiple prompts separately in a single request. Any time we need to modify the architecture like this, that's something that needs to be designed with the maintainers so we can consolidate our other supported scenarios and future plans. +## Framework Component Documentation -## Notebooks +**Responsibility** Show how the framework is used in a concise way -For all their power, attacks should still be generic. A lot of our front-end code and operators use Notebooks to interact with PyRIT. This is fantastic, but most new logic should not be notebooks. Notebooks should mostly be used for attack setup and documentation. For example, configuring the components and putting them together is a good use of a notebook, but new logic for an attack should be moved to one or more components. +- Notebooks that contain code should be notebooks that can execute +- Notebooks should execute quickly From 2bd584f535dad27bf5a2aa2be49a3b178baa99a7 Mon Sep 17 00:00:00 2001 From: Richard Lundeen Date: Fri, 26 Jun 2026 16:57:25 -0700 Subject: [PATCH 02/14] DOC: Architecture Responsibilities Restructure framework.md to clearly define each component's responsibilities using an Owns / Does NOT own template, fix structural inconsistencies, and correct typos. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- doc/code/framework.md | 194 +++++++++++++++++++++++++++--------------- 1 file changed, 126 insertions(+), 68 deletions(-) diff --git a/doc/code/framework.md b/doc/code/framework.md index 65fa63f466..63b9a45fa5 100644 --- a/doc/code/framework.md +++ b/doc/code/framework.md @@ -63,152 +63,210 @@ The sections above link to detailed guides for each component. The architecture # Architecture -The main components of PyRIT are prompts, attacks, converters, targets, and scoring. The best way to contribute to PyRIT is by contributing to one of these components. +The main components of PyRIT are datasets, targets, converters, scoring, and attacks — together with the attack techniques and scenarios that combine them. The best way to contribute to PyRIT is by contributing to one of these components. ![alt text](../../assets/architecture_components.png) As much as possible, each component is a pluggable brick of functionality. Prompts from one attack can be used in another. An attack for one scenario can use multiple targets. And sometimes you completely skip components (e.g. almost every component can be a NoOp also, you can have a NoOp converter that doesn't convert, or a NoOp target that just prints the prompts). -If you are contributing to PyRIT, that work will most likely land in one of the core components buckets and be as self-contained as possible. It isn't always this clean, but when an attack scenario doesn't quite fit (and that's okay!) it's good to brainstorm with the maintainers about how we can modify our architecture. Also, if our **Framework Plans** would be helpful, please open issues! +Each section below states what a component **owns** and, just as importantly, what it **does not own** (with a pointer to the component that does). If you are contributing to PyRIT, that work will most likely land in one of the core component buckets and be as self-contained as possible. It isn't always this clean, but when an attack scenario doesn't quite fit (and that's okay!) it's good to brainstorm with the maintainers about how we can modify our architecture. Also, if our **Framework Plans** would be helpful, please open issues! # Core Components ## [Datasets](./datasets/0_dataset) -**Responsibility**: Create a single place to manage prompts +**Responsibility**: Provide a single place to define and manage the inputs to an attack — prompts, jailbreak templates, source images, attack strategies, and similar seeds. -- New Datasets can be added in the dataset module. -- Datasets should never be retrieved from DatasetProviders; DatasetProviders should load into memory, and then components retireve from memory -- Most components should always work with seeds passed directly in (except scenarios which may package them from memory). Never use DatasetProfiders, file paths, etc. Either pass the seed as an argument or retrieve from memory. +- New datasets can be added in the dataset module. +- Dataset providers load seeds into memory; components then retrieve them from memory. Providers are not queried directly at attack time. +- Most components should work with seeds passed directly in (except scenarios, which may package them from memory). Never reach for dataset providers, file paths, etc. inside a component — either pass the seed as an argument or retrieve it from memory. + +**Does NOT own**: + +- Persisting or looking up seeds at run time — that is Memory. +- Deciding which seeds to run — that is a Scenario. **Framework Plans**: - There is some churn here. We haven't managed these much at scale, and we may have to redefine how it works. -- We want more investment in managing datasets and loading them more intelligently -- We need to more consistently pass seeds or use memory +- We want more investment in managing datasets and loading them more intelligently. +- We need to more consistently pass seeds or use memory. -**Contributing (difficulty easy)**: Are there more prompts and jailbreak templates you can add that include scenarios you're testing for? It is easy to add new dataset providers. +**Contributing (difficulty: easy)**: Are there more prompts and jailbreak templates you can add for scenarios you're testing for? It is easy to add new dataset providers. ## [Attacks](./executor/0_executor) -**Responsibility**: Manage conversations between objective targets and adversarial targets; using datasets, scorers, and converters to achieve an objective. +**Responsibility**: Own the *algorithm and control flow* of achieving a single objective — managing the conversation between objective and adversarial targets, and using datasets, converters, and scorers along the way. -- Any branching decision (e.g. the next thing(s) to do is based on a previous result) should be an attack. -- Attacks should always make use of other component's responsibilities. An attack should alwways branch based on a scorer and NOT a direct response. (e.g. was this prompt blocked? is a scorer responsibility, not an attack responsibility) -- Attacks should use scoring and target capabilities implicitly. Attacks should support multi-modal. +- Any branching decision (i.e. the next step depends on a previous result) belongs in an attack. +- An attack should branch based on a **scorer result**, never on a raw target response directly (e.g. "was this prompt blocked?" is a scorer's job, not an attack's). +- Attacks use scoring and target capabilities implicitly, and should support multi-modal. - Compound attacks are possible, combining different attacks in different ways. -**Rough Framework Plans**: +**Does NOT own**: + +- Interpreting a raw target response — that is Scoring. +- The specific configuration of prompts, converters, and strategy used — that is an Attack Technique. +- Choosing which attacks or techniques to run, or running them at scale — that is a Scenario. + +**Framework Plans**: -- We need to move some older attacks that don't belong here. Many (FlipAttack) should just be attack techniques +- We need to move some older attacks that don't belong here. Many (e.g. FlipAttack) should just be attack techniques. - There are potential ways we could combine different algorithms. Are Crescendo and TAP ultimately the same? -- We need to support target capabilities more implicitly +- We need to support target capabilities more implicitly. - Other executors, like benchmarks, need better end-to-end support; potentially including an `ExpectedResult` seed and associated scorers. -- More flexible compound attacks should continue to be added +- More flexible compound attacks should continue to be added. + +**Contributing (difficulty: hard)**: The best way to contribute is likely opening issues if you run into limitations. + +## Attack Technique + +**Responsibility**: A single, declarative **configuration** of an attack — no new logic. It bundles an existing attack class with the strategy, converters, datasets, and prompts that define one named technique. -**Contributing (difficulty high)**: The best way to contribute is likely opening issues if you run into limitations. +A technique should be expressible as one self-contained definition, for example: -## [Attack Technique] +```python +AttackTechniqueFactory( + name="violent_durian", + attack_class=RedTeamingAttack, + strategy_tags=["multi_turn"], + adversarial_system_prompt=SeedPrompt.from_yaml_file(EXECUTOR_RED_TEAM_PATH / "violent_durian.yaml"), + adversarial_seed_prompt=SeedPrompt.from_yaml_file( + EXECUTOR_RED_TEAM_PATH / "violent_durian_seed_prompt.yaml" + ), +) +``` -**Responsibility**: An attack technique packages an executor, converters, datasets, and strategies into a single attack. The goal is that any attack (something trying to achieve an objective) can be defined as an attack technique. +**Does NOT own**: -**Rough Framework Plans**: +- Any branching or control flow — that lives in the Attack it configures. +- Selecting which techniques to run — that is a Scenario. -- Managing these better, so scenarios can more easily select or build the attack techniques to use +**Framework Plans**: + +- We are still defining *where* attack techniques are registered (today this can live in setup/initializers, but that may change). +- Managing these better, so scenarios can more easily select or build the attack techniques to use. -**Contributing (difficulty easy)**: Simply add the attack technique to one of the initializers. +**Contributing (difficulty: easy)**: Add the technique as a single declarative configuration, with no new logic. ## [Scenarios](./scenarios/0_scenarios) -**Responsibility**: This is the avenue to "run PyRIT against something". What does that look like? +**Responsibility**: The avenue to "run PyRIT against something" — **select** which attack techniques and datasets to run, then orchestrate them at scale. + +- A scenario takes user input and uses it to package datasets with attack techniques. +- A scenario orchestrates resiliency and parallelism from a high level. +- No result should depend on a previous result — that cross-result branching is an attack's job. + +**Does NOT own**: -- A scenario takes user input and uses it to package datasets with attack techniques -- A scenario orchestrates resiliency and parallelism from a high level -- No result should depend on previous results (that is an attack's job) +- Per-objective branching or conversation logic — that is an Attack. +- The internal configuration of a technique — that is an Attack Technique. -**Rough Framework Plans**: +**Framework Plans**: -- Scenarios are new enough that we are still discovering patterns and limitations. So they will regularly be refactored +- Scenarios are new enough that we are still discovering patterns and limitations, so they will be refactored regularly. -**Contributing (difficulty medium)**: Is there a scanner that does something PyRIT doesn't? Add it as a scenario. But because we're changing how things are done rapidly, it is not as well-defined as other areas. +**Contributing (difficulty: medium)**: Is there a scanner that does something PyRIT doesn't? Add it as a scenario. Because we're still changing how this works, it is less well-defined than other areas. ## [Converters](./converters/0_converters) -**Responsibility**: Converters are a component that converts prompts to something else. They can be stacked and combined. They can be as varied as translating a text prompt into a Word document, rephrasing a prompt, or adding a text overlay to an image. +**Responsibility**: Convert a prompt into something else. Converters can be stacked and combined, and can be as varied as translating a text prompt into a Word document, rephrasing a prompt, or adding a text overlay to an image. + +**Does NOT own**: -**Rough Framework Plans**: +- Deciding *when* to apply a conversion, or branching on the result — that is an Attack. + +**Framework Plans**: -- We want to refactor our converter pipeline, so there are currently some things that should be converters that we may want to postpone (e.g. partial converting). This is supported but could be much more dynamic. +- We want to refactor our converter pipeline; some things that should be converters (e.g. partial converting) may be postponed. This is supported but could be much more dynamic. -**Contributing (difficulty low)**: The existing pattern is well-defined. Are there ways prompts can be converted that would be useful for an attack? +**Contributing (difficulty: easy)**: The existing pattern is well-defined. Are there ways prompts can be converted that would be useful for an attack? ## [Target](./targets/0_prompt_targets.md) -**Responsibility**: A Prompt Target can be thought of as "the thing we're sending the prompt to". Many other components use it, including scorers, attacks, and converters. +**Responsibility**: "The thing we're sending the prompt to." Many other components use it, including scorers, attacks, and converters. -- This is often an LLM, but it doesn't have to be. For Cross-Domain Prompt Injection Attacks, the Prompt Target might be a Storage Account that a later Prompt Target has a reference to. Message and conversation should be generic enough to handle this extra data. -- Prompt 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 PromptCapabilities to transorm `Messages` into formats that target supports. +- This is often an LLM, but it doesn't have to be. For Cross-Domain Prompt Injection Attacks, the prompt target might be a storage account that a later prompt target has a reference to. Message and conversation should be generic enough to carry this extra data. +- Target capabilities are used to check whether a target is compatible with what the other components want to do. +- Targets use `message_normalizer` together with prompt capabilities to transform `Messages` into the formats a given target supports. - Because targets are so varied, it is reasonable to return multiple tool calls, or none at all. -- One attack can have many Prompt Targets (and in fact, converters and Scoring Engine can also use Prompt Targets to convert/score the prompt). +- One attack can have many prompt targets (and converters and scorers can use prompt targets too, to convert or score). -**Rough Framework Plans**: +**Framework Plans**: -- Better agent support may require extra pieces attached to a Message -- Better surface support may require expanding the return types +- Better agent support may require extra pieces attached to a Message. +- Better surface support may require expanding the return types. -**Contributing (difficulty low)**: +**Contributing (difficulty: easy)**: - The pattern is well-defined. -- Are there models you want to use at any stage or for different attacks? But also, can your model just be one of the existing targets? +- Are there models you want to use at any stage or for different attacks? And could your model simply be one of the existing targets? ## [Scoring](./scoring/0_scoring.ipynb) -**Responsibility**: The scoring engine is a component that gives feedback to the attack on what happened with the prompt. This could be as simple as "Was this prompt blocked?" or "Was our objective achieved?" - -- Any decision an attack makes should be based on a scorer result +**Responsibility**: Give feedback to the attack on what happened with a prompt — from "was this prompt blocked?" to "was our objective achieved?". Scoring owns the *interpretation* of a response; every decision an attack makes is based on a scorer result. -**Contributing (difficulty low)**: +**Does NOT own**: -- The pattern is well-defined. -- You can evaluate how accurate probabalistic scorers are and likely make them more accurate. -- Is there data you want to use to make decisions or analyze? +- Acting on a score — branching, retrying, or stopping is the Attack's job. **Framework Plans**: -- Scorers will be refactored to be more generic, so they can determine more general results (does a file exist? Was a tool called?) +- Scorers will be refactored to be more generic, so they can determine more general results (does a file exist? was a tool called?). + +**Contributing (difficulty: easy)**: + +- The pattern is well-defined. +- You can evaluate how accurate probabilistic scorers are and likely make them more accurate. +- Is there data you want to use to make decisions or analyze? # Core library -The below talks about responsibilities of several modules in the PyRIT library +The modules below are the supporting library the core components are built on. ## [Registry](./registry/0_registry) -**Responsibility**: The registry is used to build and store the core components. +**Responsibility**: Build and store the core components — the **construction** side of the framework. -- If you are creating a component with user input (e.g. via config, REST, or automatically) it should always use the registry -- If you are storing an instance of a component, it should always use the registry +- If you are creating a component from user input (e.g. via config, REST, or automatically), it should go through the registry. +- If you are storing an instance of a component, it should use the registry. -## [Models] +**Does NOT own**: -**Responsibility**: pyrit.models is a lightweight module where core types are defined. These should always be used where possible to prevent drift. +- Defining the *shape* of a component or its identifier — that is Models. -- If you are creating a class that has a lot of overlap with another class, or using a dict to serialize across boundaries, consider if you can use/move pyrit.models -- Models includes `identifiers` which are descriptions of the core components. And along with the registry, can often recreate those components. -- Models includes types passed around between components, and should be prefered in REST -- models should never include any dependencies outside of pyrit.common (which shouldn't depend on anything) +## Models -## Output +**Responsibility**: A lightweight module where core types are defined — the **description** side of the framework. These types should be used wherever possible to prevent drift. -**Responsibility**: The Output module is responsible for writing different components in different formats to different places. +- If you are creating a class that overlaps heavily with another, or using a dict to serialize across boundaries, consider whether you can use or move it into `pyrit.models`. +- Models includes `identifiers`, which describe the core components; together with the registry, an identifier can often recreate the component it describes. +- Models includes the types passed between components, and should be preferred in REST. +- Models should never depend on anything outside `pyrit.common` (which itself shouldn't depend on anything). + +## [Output](./output/0_output) + +**Responsibility**: Render finished components — attack results, scenario results, conversations, and scores — to different surfaces (terminal, files, Jupyter). Output is invoked directly by the CLI and in notebooks; the components it renders do not call into it. + +**Does NOT own**: + +- Live, in-run progress printing — that belongs to the scenario's own printer. ## [Memory](./memory/0_memory.md) -One important thing to remember about this architecture is its swappable nature. Prompts and targets and converters and attacks and scorers should all be swappable. But sometimes one of these components needs additional information. If the target is an LLM, we need a way to look up previous messages sent to that session so we can properly construct the new message. If the target is a blob store, we need to know the URL to use for a future attack. +**Responsibility**: The canonical store that components read from and write to — seeds, conversations, scores, and attack results. When a component needs more than what is passed in, it goes through memory. + +One important thing to remember about this architecture is its swappable nature. Prompts, targets, converters, attacks, and scorers should all be swappable. But sometimes one of these components needs additional information — if the target is an LLM, we need a way to look up previous messages sent to that session so we can construct the new message; if the target is a blob store, we need the URL to use for a future attack. Memory is where that shared state lives. + +## [Setup](./setup/0_setup) + +**Responsibility**: Initialize PyRIT and configure framework-wide defaults — memory selection, default targets, and resiliency settings. + +- Setup wires up the environment a run depends on; it does not implement attack behavior. ## Framework Component Documentation -**Responsibility** Show how the framework is used in a concise way +**Responsibility**: Show how the framework is used, concisely. -- Notebooks that contain code should be notebooks that can execute -- Notebooks should execute quickly +- Notebooks that contain code should be executable. +- Notebooks should execute quickly. From 15a3b513272e7ac2fce7f21bfcf7ca2bed8cf12a Mon Sep 17 00:00:00 2001 From: Richard Lundeen Date: Fri, 26 Jun 2026 17:02:21 -0700 Subject: [PATCH 03/14] DOC: align framework cards with architecture sections Reorder and rename the landing-page cards to match the Core Components / Core library section order, add an Attack Techniques card, and drop the Attacks-and-Executors / Setup-and-Configuration labels in favor of the section names. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- doc/code/framework.md | 43 ++++++++++++++++++++++++------------------- 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/doc/code/framework.md b/doc/code/framework.md index 63b9a45fa5..0b1f2ba4f6 100644 --- a/doc/code/framework.md +++ b/doc/code/framework.md @@ -10,14 +10,19 @@ Learn how to use PyRIT's components to build red teaming workflows. Load, create, and manage seed datasets for red teaming campaigns. :::: -::::{card} ⚔️ Attacks & Executors +::::{card} ⚔️ Attacks :link: ./executor/0_executor Run single-turn and multi-turn attacks — Crescendo, TAP, Skeleton Key, and more. :::: -::::{card} 🔌 Targets -:link: ./targets/0_prompt_targets -Connect to OpenAI, Azure, Anthropic, HuggingFace, HTTP endpoints, and custom targets. +::::{card} 🧩 Attack Techniques +:link: ./scenarios/0_attack_techniques +Package a configured attack — role-play, many-shot, crescendo, a jailbreak template — as a reusable, named recipe. +:::: + +::::{card} 📋 Scenarios +:link: ./scenarios/0_scenarios +Run standardized evaluation scenarios at scale across harm categories. :::: ::::{card} 🔄 Converters @@ -25,26 +30,16 @@ Connect to OpenAI, Azure, Anthropic, HuggingFace, HTTP endpoints, and custom tar Transform prompts with text, audio, image, and video converters. :::: +::::{card} 🔌 Targets +:link: ./targets/0_prompt_targets +Connect to OpenAI, Azure, Anthropic, HuggingFace, HTTP endpoints, and custom targets. +:::: + ::::{card} 📊 Scoring :link: ./scoring/0_scoring Evaluate AI responses with true/false, Likert, classification, and custom scorers. :::: -::::{card} 💾 Memory -:link: ./memory/0_memory -Track conversations, scores, and attack results with SQLite or Azure SQL. -:::: - -::::{card} ⚙️ Setup & Configuration -:link: ./setup/0_setup -Initialize PyRIT, configure defaults, and manage resiliency settings. -:::: - -::::{card} 📋 Scenarios -:link: ./scenarios/0_scenarios -Run standardized evaluation scenarios at scale across harm categories. -:::: - ::::{card} 🗂️ Registry :link: ./registry/0_registry Register and discover targets, scorers, and converters via class and instance registries. @@ -55,6 +50,16 @@ Register and discover targets, scorers, and converters via class and instance re Render attack results, scenario results, conversations, and scores to terminal, files, or Jupyter. :::: +::::{card} 💾 Memory +:link: ./memory/0_memory +Track conversations, scores, and attack results with SQLite or Azure SQL. +:::: + +::::{card} ⚙️ Setup +:link: ./setup/0_setup +Initialize PyRIT, configure defaults, and manage resiliency settings. +:::: + ::::: --- From b250edd7deb3d42e5b30bbc619a578576b2b6114 Mon Sep 17 00:00:00 2001 From: Richard Lundeen Date: Fri, 26 Jun 2026 17:05:05 -0700 Subject: [PATCH 04/14] DOC: add Framework Documentation as a Core library card Adds a Framework Documentation card and links the section header to the notebooks contributing guide, treating it as a Core library item. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- doc/code/framework.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/doc/code/framework.md b/doc/code/framework.md index 0b1f2ba4f6..531131df0e 100644 --- a/doc/code/framework.md +++ b/doc/code/framework.md @@ -60,6 +60,11 @@ Track conversations, scores, and attack results with SQLite or Azure SQL. Initialize PyRIT, configure defaults, and manage resiliency settings. :::: +::::{card} 📓 Framework Documentation +:link: ../contributing/7_notebooks +Keep the component notebooks concise and executable, showing how the framework is used. +:::: + ::::: --- @@ -269,7 +274,7 @@ One important thing to remember about this architecture is its swappable nature. - Setup wires up the environment a run depends on; it does not implement attack behavior. -## Framework Component Documentation +## [Framework Documentation](../contributing/7_notebooks.md) **Responsibility**: Show how the framework is used, concisely. From 17d74cef652a0943c9cf62b0688a5b00cb07a2fa Mon Sep 17 00:00:00 2001 From: Richard Lundeen Date: Fri, 26 Jun 2026 17:08:03 -0700 Subject: [PATCH 05/14] DOC: note attacks should accept scorers, datasets, targets, and converters Clarify that an attack may use defaults but should always accept its scorers, datasets/seeds (prepended_conversation and next_message), objective/adversarial targets, and converters as parameters so it can be packaged as an attack technique. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- doc/code/framework.md | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/code/framework.md b/doc/code/framework.md index 531131df0e..a8c662ab80 100644 --- a/doc/code/framework.md +++ b/doc/code/framework.md @@ -111,6 +111,7 @@ Each section below states what a component **owns** and, just as importantly, wh - Any branching decision (i.e. the next step depends on a previous result) belongs in an attack. - An attack should branch based on a **scorer result**, never on a raw target response directly (e.g. "was this prompt blocked?" is a scorer's job, not an attack's). - Attacks use scoring and target capabilities implicitly, and should support multi-modal. +- An attack may ship with sensible **defaults**, but it should always **accept** (never hard-code) the pieces a technique configures: scorers, datasets/seeds (fed to the objective target as `prepended_conversation` and `next_message`), targets (objective and adversarial), and converters. Exposing these as parameters is what lets the attack be packaged as an Attack Technique. - Compound attacks are possible, combining different attacks in different ways. **Does NOT own**: From 76d33434fd04b3a2d57ea151cd9d3b6e35a721bc Mon Sep 17 00:00:00 2001 From: Richard Lundeen Date: Fri, 26 Jun 2026 17:13:36 -0700 Subject: [PATCH 06/14] DOC: add Backend section and Source paths to framework architecture Add a Core library Backend section (presentation-specific REST API; reuse pyrit.models and the registry). Add a Source path line to every component section so the doc can be pointed at for code reviews. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- doc/code/framework.md | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/doc/code/framework.md b/doc/code/framework.md index a8c662ab80..e2d0ee9af6 100644 --- a/doc/code/framework.md +++ b/doc/code/framework.md @@ -85,6 +85,8 @@ Each section below states what a component **owns** and, just as importantly, wh ## [Datasets](./datasets/0_dataset) +**Source**: `pyrit/datasets/` (providers); seed/prompt types in `pyrit/models/seeds/`. + **Responsibility**: Provide a single place to define and manage the inputs to an attack — prompts, jailbreak templates, source images, attack strategies, and similar seeds. - New datasets can be added in the dataset module. @@ -106,6 +108,8 @@ Each section below states what a component **owns** and, just as importantly, wh ## [Attacks](./executor/0_executor) +**Source**: `pyrit/executor/attack/`. + **Responsibility**: Own the *algorithm and control flow* of achieving a single objective — managing the conversation between objective and adversarial targets, and using datasets, converters, and scorers along the way. - Any branching decision (i.e. the next step depends on a previous result) belongs in an attack. @@ -132,6 +136,8 @@ Each section below states what a component **owns** and, just as importantly, wh ## Attack Technique +**Source**: `pyrit/scenario/core/attack_technique.py` and `attack_technique_factory.py`; built-in registrations in `pyrit/setup/initializers/components/`. + **Responsibility**: A single, declarative **configuration** of an attack — no new logic. It bundles an existing attack class with the strategy, converters, datasets, and prompts that define one named technique. A technique should be expressible as one self-contained definition, for example: @@ -162,6 +168,8 @@ AttackTechniqueFactory( ## [Scenarios](./scenarios/0_scenarios) +**Source**: `pyrit/scenario/`. + **Responsibility**: The avenue to "run PyRIT against something" — **select** which attack techniques and datasets to run, then orchestrate them at scale. - A scenario takes user input and uses it to package datasets with attack techniques. @@ -181,6 +189,8 @@ AttackTechniqueFactory( ## [Converters](./converters/0_converters) +**Source**: `pyrit/prompt_converter/`. + **Responsibility**: Convert a prompt into something else. Converters can be stacked and combined, and can be as varied as translating a text prompt into a Word document, rephrasing a prompt, or adding a text overlay to an image. **Does NOT own**: @@ -195,6 +205,8 @@ AttackTechniqueFactory( ## [Target](./targets/0_prompt_targets.md) +**Source**: `pyrit/prompt_target/`; message shaping in `pyrit/message_normalizer/`. + **Responsibility**: "The thing we're sending the prompt to." Many other components use it, including scorers, attacks, and converters. - This is often an LLM, but it doesn't have to be. For Cross-Domain Prompt Injection Attacks, the prompt target might be a storage account that a later prompt target has a reference to. Message and conversation should be generic enough to carry this extra data. @@ -215,6 +227,8 @@ AttackTechniqueFactory( ## [Scoring](./scoring/0_scoring.ipynb) +**Source**: `pyrit/score/`. + **Responsibility**: Give feedback to the attack on what happened with a prompt — from "was this prompt blocked?" to "was our objective achieved?". Scoring owns the *interpretation* of a response; every decision an attack makes is based on a scorer result. **Does NOT own**: @@ -237,6 +251,8 @@ The modules below are the supporting library the core components are built on. ## [Registry](./registry/0_registry) +**Source**: `pyrit/registry/`. + **Responsibility**: Build and store the core components — the **construction** side of the framework. - If you are creating a component from user input (e.g. via config, REST, or automatically), it should go through the registry. @@ -248,6 +264,8 @@ The modules below are the supporting library the core components are built on. ## Models +**Source**: `pyrit/models/` (including `pyrit/models/identifiers/`). + **Responsibility**: A lightweight module where core types are defined — the **description** side of the framework. These types should be used wherever possible to prevent drift. - If you are creating a class that overlaps heavily with another, or using a dict to serialize across boundaries, consider whether you can use or move it into `pyrit.models`. @@ -257,26 +275,48 @@ The modules below are the supporting library the core components are built on. ## [Output](./output/0_output) +**Source**: `pyrit/output/`. + **Responsibility**: Render finished components — attack results, scenario results, conversations, and scores — to different surfaces (terminal, files, Jupyter). Output is invoked directly by the CLI and in notebooks; the components it renders do not call into it. **Does NOT own**: - Live, in-run progress printing — that belongs to the scenario's own printer. +## Backend + +**Source**: `pyrit/backend/`. + +**Responsibility**: Expose PyRIT through a REST API for the frontend and other clients. The backend owns presentation-specific logic and models — request/response shapes, mapping, and HTTP concerns — but should still use `pyrit.models` and the registry wherever it can. + +- The backend may define its own presentation models, but where a `pyrit.models` type already exists it should reuse that type rather than redefine it. +- Components should be constructed through the registry, not built directly in the backend. + +**Does NOT own**: + +- The shape of core types — that is Models. +- Constructing or storing components — that is the Registry. + ## [Memory](./memory/0_memory.md) +**Source**: `pyrit/memory/`. + **Responsibility**: The canonical store that components read from and write to — seeds, conversations, scores, and attack results. When a component needs more than what is passed in, it goes through memory. One important thing to remember about this architecture is its swappable nature. Prompts, targets, converters, attacks, and scorers should all be swappable. But sometimes one of these components needs additional information — if the target is an LLM, we need a way to look up previous messages sent to that session so we can construct the new message; if the target is a blob store, we need the URL to use for a future attack. Memory is where that shared state lives. ## [Setup](./setup/0_setup) +**Source**: `pyrit/setup/`. + **Responsibility**: Initialize PyRIT and configure framework-wide defaults — memory selection, default targets, and resiliency settings. - Setup wires up the environment a run depends on; it does not implement attack behavior. ## [Framework Documentation](../contributing/7_notebooks.md) +**Source**: `doc/` (component notebooks, e.g. `doc/code/`). + **Responsibility**: Show how the framework is used, concisely. - Notebooks that contain code should be executable. From 1efb89776832ac8367b8c4d2e8e61d020b9eeb39 Mon Sep 17 00:00:00 2001 From: Richard Lundeen Date: Tue, 30 Jun 2026 14:35:55 -0700 Subject: [PATCH 07/14] DOC: sync framework architecture doc with latest responsibility structure Bring framework.md fully up to date: top-down Core Components ordering, renamed Shared Library and new Application surfaces sections, normalized terminology (Scorers, Targets, seeds, Executors and Attacks), nesting hierarchy diagram, expanded 'Does not own' boundaries, header links, and matching Responsibility lines in the converter/dataset/models/target instruction guides. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../instructions/converters.instructions.md | 2 + .github/instructions/datasets.instructions.md | 2 + .github/instructions/models.instructions.md | 2 + .github/instructions/targets.instructions.md | 2 + doc/code/framework.md | 406 ++++++++++-------- 5 files changed, 237 insertions(+), 177 deletions(-) diff --git a/.github/instructions/converters.instructions.md b/.github/instructions/converters.instructions.md index f409395e01..54e14e315a 100644 --- a/.github/instructions/converters.instructions.md +++ b/.github/instructions/converters.instructions.md @@ -4,6 +4,8 @@ applyTo: "pyrit/prompt_converter/**" # Prompt Converter Development Guidelines +**Responsibility**: A converter transforms a prompt into something else (rephrasing, encoding, translating to a Word document, overlaying text on an image, ...). Converters can be stacked and combined, and any converter may also be a NoOp. + ## Base Class Contract All converters MUST inherit from `PromptConverter` and implement: diff --git a/.github/instructions/datasets.instructions.md b/.github/instructions/datasets.instructions.md index 4e2bbf8002..83276ed768 100644 --- a/.github/instructions/datasets.instructions.md +++ b/.github/instructions/datasets.instructions.md @@ -4,6 +4,8 @@ applyTo: "pyrit/datasets/seed_datasets/**" # Seed Dataset Loader Guidelines +**Responsibility**: Seed dataset loaders (`SeedDatasetProvider` subclasses) are the single place to manage the prompts/objectives for a source. They load seeds into `CentralMemory`; components then retrieve seeds from memory — components never read from a loader directly. + These rules apply when adding or modifying loaders under `pyrit/datasets/seed_datasets/`. Style rules from `style-guide.instructions.md` (async `_async` suffix, keyword-only args, type hints, enums-over-Literals) still apply and are not repeated here. diff --git a/.github/instructions/models.instructions.md b/.github/instructions/models.instructions.md index 4a9e32baa0..795e0a1bbe 100644 --- a/.github/instructions/models.instructions.md +++ b/.github/instructions/models.instructions.md @@ -4,6 +4,8 @@ applyTo: "pyrit/models/**" # `pyrit.models` Guidelines +**Responsibility**: `pyrit.models` is the lightweight, canonical data layer — the core types shared across components (and preferred in REST) so representations don't drift. It depends only on lightweight Python (the standard library and pydantic) and `pyrit.common`. + ## Import Boundary PyRIT enforces a two-layer rule for its foundational packages. `pyrit.common` diff --git a/.github/instructions/targets.instructions.md b/.github/instructions/targets.instructions.md index 19040be72b..138af4250a 100644 --- a/.github/instructions/targets.instructions.md +++ b/.github/instructions/targets.instructions.md @@ -4,6 +4,8 @@ applyTo: "pyrit/prompt_target/**" # Prompt Target Development Guidelines +**Responsibility**: A prompt target is "the thing we're sending the prompt to" — often an LLM, but it can be any endpoint (e.g. a storage account for cross-domain prompt injection). Targets use `message_normalizer` together with `TargetConfiguration` to transform `Message`s into the format the target supports. + ## Base Class Contract All targets MUST inherit from ``PromptTarget`` (or one of its public diff --git a/doc/code/framework.md b/doc/code/framework.md index e2d0ee9af6..de8881b493 100644 --- a/doc/code/framework.md +++ b/doc/code/framework.md @@ -10,19 +10,19 @@ Learn how to use PyRIT's components to build red teaming workflows. Load, create, and manage seed datasets for red teaming campaigns. :::: -::::{card} ⚔️ Attacks -:link: ./executor/0_executor -Run single-turn and multi-turn attacks — Crescendo, TAP, Skeleton Key, and more. +::::{card} 📋 Scenarios +:link: ./scenarios/0_scenarios +Run standardized evaluation scenarios at scale across harm categories. :::: ::::{card} 🧩 Attack Techniques :link: ./scenarios/0_attack_techniques -Package a configured attack — role-play, many-shot, crescendo, a jailbreak template — as a reusable, named recipe. +Package an executor, converters, datasets, and strategy into a single named attack. :::: -::::{card} 📋 Scenarios -:link: ./scenarios/0_scenarios -Run standardized evaluation scenarios at scale across harm categories. +::::{card} ⚔️ Executors and Attacks +:link: ./executor/0_executor +Run single-turn and multi-turn attacks — Crescendo, TAP, Skeleton Key, and more. :::: ::::{card} 🔄 Converters @@ -35,36 +35,11 @@ Transform prompts with text, audio, image, and video converters. Connect to OpenAI, Azure, Anthropic, HuggingFace, HTTP endpoints, and custom targets. :::: -::::{card} 📊 Scoring +::::{card} 📊 Scorers :link: ./scoring/0_scoring Evaluate AI responses with true/false, Likert, classification, and custom scorers. :::: -::::{card} 🗂️ Registry -:link: ./registry/0_registry -Register and discover targets, scorers, and converters via class and instance registries. -:::: - -::::{card} 🖨️ Output -:link: ./output/0_output -Render attack results, scenario results, conversations, and scores to terminal, files, or Jupyter. -:::: - -::::{card} 💾 Memory -:link: ./memory/0_memory -Track conversations, scores, and attack results with SQLite or Azure SQL. -:::: - -::::{card} ⚙️ Setup -:link: ./setup/0_setup -Initialize PyRIT, configure defaults, and manage resiliency settings. -:::: - -::::{card} 📓 Framework Documentation -:link: ../contributing/7_notebooks -Keep the component notebooks concise and executable, showing how the framework is used. -:::: - ::::: --- @@ -73,251 +48,328 @@ The sections above link to detailed guides for each component. The architecture # Architecture -The main components of PyRIT are datasets, targets, converters, scoring, and attacks — together with the attack techniques and scenarios that combine them. The best way to contribute to PyRIT is by contributing to one of these components. +The main components of PyRIT are seeds, scenarios, attack techniques, executors and attacks, converters, targets, and scorers. The best way to contribute to PyRIT is by contributing to one of these components. + +The diagram below shows how the pieces fit together: entry points run **scenarios**, which package **datasets** with **attack techniques**; each technique drives an **attack/executor** that orchestrates **converters**, **targets**, and **scorers**; and a shared library layer (**memory**, **registry**, **models**, **output**, and more) supports all of them. + +```mermaid +flowchart TB + subgraph entry [Entry points] + direction LR + CLI[Scanner / CLI] + GUI[GUI / Backend] + FW[Framework / Notebooks] + end + + SCEN["Scenario
packages datasets + attack techniques;
orchestrates parallelism & resiliency"] + TECH["Attack Technique
executor + converters + datasets + strategy"] + ATK["Attack / Executor
manages the conversation to reach an objective"] + + subgraph core [Core Components] + direction LR + CONV[Converters] + SCORE[Scorers] + TGT[Targets] + DATA[(Datasets / Seeds)] + end + + subgraph lib [Shared Library] + direction LR + MEM[(Memory)] + REG[Registry] + MODEL[Models] + OUT[Output] + end + + entry --> SCEN + DATA --> SCEN + SCEN --> TECH --> ATK + + ATK --> CONV + ATK == objective ==> TGT + ATK -. adversarial .-> TGT + ATK -- decisions based on --> SCORE + CONV -. may call .-> TGT + SCORE -. may call .-> TGT + + REG -. builds .-> core + REG -. builds .-> ATK + ATK <-- reads / writes --> MEM + core <--> MEM + SCEN --> OUT + OUT --> MEM + MODEL -. shared types .-> core + + classDef flow fill:#e8f0fe,stroke:#4285f4,color:#15233a; + classDef libnode fill:#f1f3f4,stroke:#9aa0a6,color:#202124; + class SCEN,TECH,ATK flow; + class MEM,REG,MODEL,OUT libnode; +``` -![alt text](../../assets/architecture_components.png) +# Core Components -As much as possible, each component is a pluggable brick of functionality. Prompts from one attack can be used in another. An attack for one scenario can use multiple targets. And sometimes you completely skip components (e.g. almost every component can be a NoOp also, you can have a NoOp converter that doesn't convert, or a NoOp target that just prints the prompts). +As much as possible, each core component is a pluggable brick of functionality. Prompts from one attack can be used in another. An attack for one scenario can use multiple targets. And sometimes you completely skip components (e.g. almost every component can be a NoOp also, you can have a NoOp converter that doesn't convert, or a NoOp target that just prints the prompts). -Each section below states what a component **owns** and, just as importantly, what it **does not own** (with a pointer to the component that does). If you are contributing to PyRIT, that work will most likely land in one of the core component buckets and be as self-contained as possible. It isn't always this clean, but when an attack scenario doesn't quite fit (and that's okay!) it's good to brainstorm with the maintainers about how we can modify our architecture. Also, if our **Framework Plans** would be helpful, please open issues! +If you are contributing to PyRIT, that work will most likely land in one of the core components buckets and be as self-contained as possible. It isn't always this clean, but when an attack scenario doesn't quite fit (and that's okay!) it's good to brainstorm with the maintainers about how we can modify our architecture. Also, please open issues if you see anything under Framework Plans you do/don't want. -# Core Components +The orchestration layers **nest from broadest to narrowest** — each owns less than the layer above it: -## [Datasets](./datasets/0_dataset) +```mermaid +flowchart TB + subgraph SCEN["Scenario — owns parallelism & resiliency"] + subgraph TECH["Attack Technique — executor + converters + seeds + scorers + strategy"] + subgraph ATK["Executor / Attack — drives the conversation"] + LEAF["sends to targets · applies converters · branches on scorers"] + end + end + end +``` -**Source**: `pyrit/datasets/` (providers); seed/prompt types in `pyrit/models/seeds/`. +- **Scenario** packages many attack techniques and owns parallelism and resiliency. +- **Attack Technique** configures one executor with its converters, seeds, scorers, and strategy. +- **Executor / Attack** runs the algorithm: sends to targets, applies converters, and branches on scorers. -**Responsibility**: Provide a single place to define and manage the inputs to an attack — prompts, jailbreak templates, source images, attack strategies, and similar seeds. +(*Attack Technique* and *Attack* are deliberately different: a **technique is configuration**, an **attack is the running algorithm** it configures.) -- New datasets can be added in the dataset module. -- Dataset providers load seeds into memory; components then retrieve them from memory. Providers are not queried directly at attack time. -- Most components should work with seeds passed directly in (except scenarios, which may package them from memory). Never reach for dataset providers, file paths, etc. inside a component — either pass the seed as an argument or retrieve it from memory. +## [Datasets](./datasets/0_dataset) -**Does NOT own**: +**Responsibility**: Create a single place to manage seeds -- Persisting or looking up seeds at run time — that is Memory. -- Deciding which seeds to run — that is a Scenario. +- New Datasets can be added in the dataset module. +- Datasets should never be retrieved from SeedDatasetProviders; SeedDatasetProviders should load into memory, and then components retrieve from memory +- Most components should always work with seeds passed directly in (except scenarios which may package them from memory). Never use SeedDatasetProviders, file paths, etc. Either pass the seed as an argument or retrieve from memory. +- There is a Seed hierarchy and the right types should be used (SeedObjective, SeedPrompt, SimulatedSeedPrompt, SeedAttackGroup, ...) +- **Does not own**: a dataset defines and holds seeds; it doesn't package them for an attack. Specifically not: + - selecting or combining which seeds an attack uses (that's a scenario / attack technique) + - rendering or parameterizing prompts at send time (converters / normalizers) + - runtime retrieval from providers or filepaths (load into memory first) **Framework Plans**: - There is some churn here. We haven't managed these much at scale, and we may have to redefine how it works. -- We want more investment in managing datasets and loading them more intelligently. -- We need to more consistently pass seeds or use memory. +- We want more investment in managing datasets and loading them more intelligently +- We need to more consistently pass seeds/use memory (e.g. not using filepaths) +- We need to create seed types for different executors (e.g. SeedExpectedResponse, SeedBenchmarkGroup) -**Contributing (difficulty: easy)**: Are there more prompts and jailbreak templates you can add for scenarios you're testing for? It is easy to add new dataset providers. +**Contributing (difficulty easy)**: Are there more prompts and jailbreak templates you can add that include scenarios you're testing for? It is easy to add new dataset providers. -## [Attacks](./executor/0_executor) +## [Scenarios](./scenarios/0_scenarios) -**Source**: `pyrit/executor/attack/`. +**Responsibility**: This is the avenue to "run PyRIT against something". What does that look like? -**Responsibility**: Own the *algorithm and control flow* of achieving a single objective — managing the conversation between objective and adversarial targets, and using datasets, converters, and scorers along the way. +- A scenario takes user input and uses it to package datasets with attack techniques +- A scenario orchestrates resiliency and parallelism from a high level +- No result should depend on previous results (that is an attack's job) +- **Does not own**: the per-objective conversation logic. Branching, turn-by-turn adaptation, and scoring-based decisions belong to the attack; a scenario selects and packages existing attack techniques rather than defining new attack algorithms or datasets. -- Any branching decision (i.e. the next step depends on a previous result) belongs in an attack. -- An attack should branch based on a **scorer result**, never on a raw target response directly (e.g. "was this prompt blocked?" is a scorer's job, not an attack's). -- Attacks use scoring and target capabilities implicitly, and should support multi-modal. -- An attack may ship with sensible **defaults**, but it should always **accept** (never hard-code) the pieces a technique configures: scorers, datasets/seeds (fed to the objective target as `prepended_conversation` and `next_message`), targets (objective and adversarial), and converters. Exposing these as parameters is what lets the attack be packaged as an Attack Technique. -- Compound attacks are possible, combining different attacks in different ways. +**Framework Plans**: -**Does NOT own**: +- Scenarios are new enough that we are still discovering patterns and limitations. So they will regularly be refactored -- Interpreting a raw target response — that is Scoring. -- The specific configuration of prompts, converters, and strategy used — that is an Attack Technique. -- Choosing which attacks or techniques to run, or running them at scale — that is a Scenario. +**Contributing (difficulty medium)**: Is there a scanner that does something PyRIT doesn't? Add it as a scenario. But because we're changing how things are done rapidly, it is not as well-defined as other areas. -**Framework Plans**: +## [Attack Techniques](./scenarios/0_attack_techniques) -- We need to move some older attacks that don't belong here. Many (e.g. FlipAttack) should just be attack techniques. -- There are potential ways we could combine different algorithms. Are Crescendo and TAP ultimately the same? -- We need to support target capabilities more implicitly. -- Other executors, like benchmarks, need better end-to-end support; potentially including an `ExpectedResult` seed and associated scorers. -- More flexible compound attacks should continue to be added. +**Responsibility**: An attack technique packages an executor, converters, datasets, and strategies into a single attack. The goal is that any attack (something trying to achieve an objective) can be defined as an attack technique. -**Contributing (difficulty: hard)**: The best way to contribute is likely opening issues if you run into limitations. +- **Does not own**: the conversation algorithm itself. Branching, turn management, and scoring decisions live in the executor it wraps — a technique only selects and configures existing components, and shouldn't implement new sending, scoring, or branching logic. -## Attack Technique +**Framework Plans**: -**Source**: `pyrit/scenario/core/attack_technique.py` and `attack_technique_factory.py`; built-in registrations in `pyrit/setup/initializers/components/`. +- Managing these better, so scenarios can more easily select or build the attack techniques to use -**Responsibility**: A single, declarative **configuration** of an attack — no new logic. It bundles an existing attack class with the strategy, converters, datasets, and prompts that define one named technique. +**Contributing (difficulty easy)**: Simply add the attack technique to one of the initializers. -A technique should be expressible as one self-contained definition, for example: +## [Executors and Attacks](./executor/0_executor) -```python -AttackTechniqueFactory( - name="violent_durian", - attack_class=RedTeamingAttack, - strategy_tags=["multi_turn"], - adversarial_system_prompt=SeedPrompt.from_yaml_file(EXECUTOR_RED_TEAM_PATH / "violent_durian.yaml"), - adversarial_seed_prompt=SeedPrompt.from_yaml_file( - EXECUTOR_RED_TEAM_PATH / "violent_durian_seed_prompt.yaml" - ), -) -``` +**Executor Responsibility**: Manage conversations between objective targets and adversarial targets; using datasets, scorers, and converters. -**Does NOT own**: +**Attack Responsibility**: An attack is a type of executor, which manages conversations to achieve an objective. -- Any branching or control flow — that lives in the Attack it configures. -- Selecting which techniques to run — that is a Scenario. +- Any branching decision (e.g. the next thing(s) to do is based on a previous result) should be an attack/executor. +- Executors should always make use of other component's responsibilities. An executor should always branch based on a scorer and NOT a direct response. (e.g. was this prompt blocked? is a scorer responsibility, not an executor responsibility) +- Executors should use scoring and target capabilities implicitly. Executors should support multi-modal. +- Compound attacks are possible, combining different attacks in different ways. +- **Does not own**: packaging the attack. Those are passed in as configuration by the **attack technique**, not assembled here: + - prepended / system prompts, role-play framing, the converter stack, or dataset selection (e.g. `RolePlayAttack` building its own prompt scaffolding is attack-technique work bleeding into the executor) + - branching on raw responses (use a scorer), constructing its own components (use the registry), or formatting / persisting results (output / memory) **Framework Plans**: -- We are still defining *where* attack techniques are registered (today this can live in setup/initializers, but that may change). -- Managing these better, so scenarios can more easily select or build the attack techniques to use. +- We need to move some older attacks that don't belong here. Many (FlipAttack) should just be attack techniques +- There are potential ways we could combine different algorithms. Are Crescendo and TAP ultimately the same? +- We need to support target capabilities more implicitly +- Other executors, like benchmarks, need better end-to-end support; potentially including an `ExpectedResult` seed and associated scorers. +- More flexible compound attacks should continue to be added -**Contributing (difficulty: easy)**: Add the technique as a single declarative configuration, with no new logic. +**Contributing (difficulty high)**: The best way to contribute is likely opening issues if you run into limitations. -## [Scenarios](./scenarios/0_scenarios) +## [Converters](./converters/0_converters) -**Source**: `pyrit/scenario/`. +**Responsibility**: Converters are a component that converts prompts to something else. They can be stacked and combined. They can be as varied as translating a text prompt into a Word document, rephrasing a prompt, or adding a text overlay to an image. -**Responsibility**: The avenue to "run PyRIT against something" — **select** which attack techniques and datasets to run, then orchestrate them at scale. +- **Does not own**: conversation state or attack decisions. A converter transforms input into output (and may call a target to do so), but it doesn't branch on results, score, persist to memory itself (the normalizer handles persistence), or decide when it runs — the attack/technique configures the stack. -- A scenario takes user input and uses it to package datasets with attack techniques. -- A scenario orchestrates resiliency and parallelism from a high level. -- No result should depend on a previous result — that cross-result branching is an attack's job. +**Framework Plans**: -**Does NOT own**: +- We want to refactor our converter pipeline, so there are currently some things that should be converters that we may want to postpone (e.g. partial converting). This is supported but could be much more dynamic. -- Per-objective branching or conversation logic — that is an Attack. -- The internal configuration of a technique — that is an Attack Technique. +**Contributing (difficulty low)**: The existing pattern is well-defined. Are there ways prompts can be converted that would be useful for an attack? -**Framework Plans**: +## [Targets](./targets/0_prompt_targets) -- Scenarios are new enough that we are still discovering patterns and limitations, so they will be refactored regularly. +**Responsibility**: A target can be thought of as "the thing we're sending the prompt to". Many other components use it, including scorers, attacks, and converters. -**Contributing (difficulty: medium)**: Is there a scanner that does something PyRIT doesn't? Add it as a scenario. Because we're still changing how this works, it is less well-defined than other areas. +- 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. +- 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), or apply attack logic. Its retries stay at the target layer (e.g. `RateLimitException`). -## [Converters](./converters/0_converters) +**Framework Plans**: + +- Better agent support may require extra pieces attached to a Message +- Better surface support may require expanding the return types -**Source**: `pyrit/prompt_converter/`. +**Contributing (difficulty low)**: -**Responsibility**: Convert a prompt into something else. Converters can be stacked and combined, and can be as varied as translating a text prompt into a Word document, rephrasing a prompt, or adding a text overlay to an image. +- The pattern is well-defined. +- Are there models you want to use at any stage or for different attacks? But also, can your model just be one of the existing targets? -**Does NOT own**: +## [Scorers](./scoring/0_scoring) -- Deciding *when* to apply a conversion, or branching on the result — that is an Attack. +**Responsibility**: Scorers give feedback to the attack on what happened with the prompt. This could be as simple as "Was this prompt blocked?" or "Was our objective achieved?" -**Framework Plans**: +- Any decision an attack makes should be based on a scorer result +- A scorer is not limited to a prompt, it could be anything (e.g. was this tool called or was this file written). +- **Does not own**: acting on its own result. A scorer evaluates a response and returns a score; branching on that score is the attack's job, and aggregating scores across runs is analytics'. It may call a target to evaluate, but it doesn't send the attack's objective prompt or manage the conversation. -- We want to refactor our converter pipeline; some things that should be converters (e.g. partial converting) may be postponed. This is supported but could be much more dynamic. +**Framework Plans**: -**Contributing (difficulty: easy)**: The existing pattern is well-defined. Are there ways prompts can be converted that would be useful for an attack? +- Scorers will be refactored to be more generic, so they can determine more general results (does a file exist? Was a tool called?) -## [Target](./targets/0_prompt_targets.md) +**Contributing (difficulty low)**: -**Source**: `pyrit/prompt_target/`; message shaping in `pyrit/message_normalizer/`. +- The pattern is well-defined. +- You can evaluate how accurate probabilistic scorers are and likely make them more accurate. +- Is there data you want to use to make decisions or analyze? -**Responsibility**: "The thing we're sending the prompt to." Many other components use it, including scorers, attacks, and converters. +# Shared Library -- This is often an LLM, but it doesn't have to be. For Cross-Domain Prompt Injection Attacks, the prompt target might be a storage account that a later prompt target has a reference to. Message and conversation should be generic enough to carry this extra data. -- Target capabilities are used to check whether a target is compatible with what the other components want to do. -- Targets use `message_normalizer` together with prompt capabilities to transform `Messages` into the formats a given target supports. -- Because targets are so varied, it is reasonable to return multiple tool calls, or none at all. -- One attack can have many prompt targets (and converters and scorers can use prompt targets too, to convert or score). +The below talks about responsibilities of most modules in the PyRIT library -**Framework Plans**: +## Analytics -- Better agent support may require extra pieces attached to a Message. -- Better surface support may require expanding the return types. +**Responsibility**: Make sense of results — aggregating across conversations and attacks to answer questions PyRIT itself acts on or reports. -**Contributing (difficulty: easy)**: +- This is where cross-run analysis belongs: e.g. "which attack performed best for this objective?", "how often did a technique succeed?", or "which responses match known content?". +- **Does not own**: live, in-attack decisions — any decision made *during* an attack is a scorer's job. Analytics only operates on stored results, after the fact. +- Today it includes `ConversationAnalytics` (inspecting conversation history), `analyze_results` / `AttackStats` (aggregating outcomes across techniques), and text-matching strategies (`ExactTextMatching`, `ApproximateTextMatching`). -- The pattern is well-defined. -- Are there models you want to use at any stage or for different attacks? And could your model simply be one of the existing targets? +## Auth -## [Scoring](./scoring/0_scoring.ipynb) +**Responsibility**: Provide authentication helpers for the external services PyRIT talks to, behind a common `Authenticator` abstraction. -**Source**: `pyrit/score/`. +- Components that need credentials should go through these helpers rather than handling tokens themselves. -**Responsibility**: Give feedback to the attack on what happened with a prompt — from "was this prompt blocked?" to "was our objective achieved?". Scoring owns the *interpretation* of a response; every decision an attack makes is based on a scorer result. +## [Exceptions](../contributing/9_exception) -**Does NOT own**: +**Responsibility**: Define PyRIT's exception hierarchy and the retry behavior built around it. -- Acting on a score — branching, retrying, or stopping is the Attack's job. +- Retries should use PyRIT exception types (such as `PyritException`, `BadRequestException`, `RateLimitException`, `EmptyResponseException`, and `InvalidJsonException`) and retry decorators (such as `pyrit_target_retry`, `pyrit_json_retry`, `pyrit_placeholder_retry`) and execution-context utilities (`ExecutionContext`, `ComponentRole`, `RetryCollector`). +- Retries should _only_ be attempted on known exceptions. +- The applicable layer should retry exceptions (e.g. only targets should retry `RateLimitException`, only scorers/attacks/converters should retry `InvalidJsonException`, and only scenarios should retry general exceptions). +- When raising, attach context: every `PyritException` carries a `status_code` and a human-readable `message`, and the active `ExecutionContext` / `ComponentRole` records which component raised it — so failures point back to where they happened. -**Framework Plans**: +## [Memory](./memory/0_memory) -- Scorers will be refactored to be more generic, so they can determine more general results (does a file exist? was a tool called?). +**Responsibility**: Memory persists and retrieves the data that flows between components — prompts, responses, conversations, scores, and attack results — so components stay swappable while still sharing the context they need. -**Contributing (difficulty: easy)**: +- One important thing to remember about this architecture is its swappable nature. Seeds, targets, converters, attacks, and scorers should all be swappable. But sometimes one of these components needs additional information. If the target is an LLM, we need a way to look up previous messages sent to that session so we can properly construct the new message. If the target is a blob store, we need to know the URL to use for a future attack. +- Components should access memory through `CentralMemory` rather than passing state directly between each other. +- Memory backends are swappable too (e.g. SQLite or Azure SQL) without changing the components that use them. +- **Does not own**: business logic or decisions. Memory stores and retrieves state; it doesn't decide what to send, how to score, or when to branch — components do that and persist results here. -- The pattern is well-defined. -- You can evaluate how accurate probabilistic scorers are and likely make them more accurate. -- Is there data you want to use to make decisions or analyze? +## [Models](../contributing/11_memory_models) -# Core library +**Responsibility**: pyrit.models is a lightweight module where core types are defined. These should always be used where possible to prevent drift. -The modules below are the supporting library the core components are built on. +- If you are creating a class that has a lot of overlap with another class, or using a dict to serialize across boundaries, consider if you can use/move pyrit.models +- Models includes `identifiers` which are descriptions of the core components. And along with the registry, can often recreate those components. +- Models includes types passed around between components, and should be prefered in REST +- models should never depend on anything except lightweight Python (the standard library and pydantic) and pyrit.common -## [Registry](./registry/0_registry) +## [Normalizers](./targets/11_message_normalizer) -**Source**: `pyrit/registry/`. +**Responsibility**: Reshape prompts and conversations so components and targets can interoperate. There are two distinct modules: -**Responsibility**: Build and store the core components — the **construction** side of the framework. +- **`prompt_normalizer`** applies converters and dispatches individual prompts to a `PromptTarget` (handling batching and memory persistence). `NormalizerRequest` and `PromptConverterConfiguration` 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. -- If you are creating a component from user input (e.g. via config, REST, or automatically), it should go through the registry. -- If you are storing an instance of a component, it should use the registry. +## [Output](./output/0_output) -**Does NOT own**: +**Responsibility**: The Output module is responsible for writing different components in different formats to different places. -- Defining the *shape* of a component or its identifier — that is Models. +- It renders the core result types — attack results, scenario results, conversations, and scores — without those components needing to know how they are displayed. +- Format and destination are decoupled: a **format** (e.g. pretty ANSI, Markdown, JSON) is separate from a **sink** (stdout, file, Jupyter), so any result can be rendered any way to anywhere. +- **Does not own**: deciding *what* to render or *when*. Components hand results to output; format classes only turn data into strings and never fetch data, touch `CentralMemory`, or call `print()` directly (that's isolated to leaf printer classes). -## Models +**Contributing (difficulty low)**: Adding a new format or sink is well-defined. Every new domain printer should come with a matching convenience function in `helpers.py`. -**Source**: `pyrit/models/` (including `pyrit/models/identifiers/`). +## [Registry](./registry/0_registry) -**Responsibility**: A lightweight module where core types are defined — the **description** side of the framework. These types should be used wherever possible to prevent drift. +**Responsibility**: The registry is used to build and store the core components. -- If you are creating a class that overlaps heavily with another, or using a dict to serialize across boundaries, consider whether you can use or move it into `pyrit.models`. -- Models includes `identifiers`, which describe the core components; together with the registry, an identifier can often recreate the component it describes. -- Models includes the types passed between components, and should be preferred in REST. -- Models should never depend on anything outside `pyrit.common` (which itself shouldn't depend on anything). +- If you are creating a component with user input (e.g. via config, REST, or automatically) it should always use the registry +- If you are storing an instance of a component, it should always use the registry -## [Output](./output/0_output) +## [Setup](./setup/0_setup) -**Source**: `pyrit/output/`. +**Responsibility**: Bootstrap a PyRIT session — getting memory, defaults, and components configured so the rest of the framework can run. -**Responsibility**: Render finished components — attack results, scenario results, conversations, and scores — to different surfaces (terminal, files, Jupyter). Output is invoked directly by the CLI and in notebooks; the components it renders do not call into it. +- `initialize_pyrit_async` is the entry point: it sets up the environment and a memory backend (`IN_MEMORY` / `SQLITE` / `AZURE_SQL` via `MemoryDatabaseType`) and runs any initializers to configure global defaults and components. +- Configuration files are the core way to drive setup. `ConfigurationLoader` / `initialize_from_config_async` read a config that declares the memory backend and a list of initializers to run, so a session can be reproduced without code. +- By default these files live under the PyRIT home directory `~/.pyrit/`: the config file at `~/.pyrit/.pyrit_conf`, and environment variables from `~/.pyrit/.env` and `~/.pyrit/.env.local` (loaded if present). +- A `PyRITInitializer` is a class-based unit of configuration: each one configures part of PyRIT (e.g. registering targets, scorers, scenario techniques, or loading default datasets) and runs in the order provided. Built-in initializers live in the `initializers/` package. +- Users can bring their own: subclass `PyRITInitializer`, implement `initialize_async`, and reference it from config or pass it in — letting teams package their own defaults and components. -**Does NOT own**: +# Application surfaces -- Live, in-run progress printing — that belongs to the scenario's own printer. +The below describes the user-facing surfaces built on top of the framework. ## Backend -**Source**: `pyrit/backend/`. +**Responsibility**: Expose PyRIT functionality as a FastAPI REST API consumed by the CLI and frontend. -**Responsibility**: Expose PyRIT through a REST API for the frontend and other clients. The backend owns presentation-specific logic and models — request/response shapes, mapping, and HTTP concerns — but should still use `pyrit.models` and the registry wherever it can. +- Surfaces targets, scenarios, and health/version endpoints; served via `uvicorn` with Swagger/ReDoc docs. +- Wherever possible it should reuse other components rather than reimplementing them (e.g. the registry to build components, `pyrit.models` for its model layer), while adding presentation-specific information on top as needed. +- Organized into `routes/`, `services/`, `models/`, `mappers/`, and `middleware/`, and launched through the `pyrit_backend` command (configurable via `PYRIT_API_HOST` / `PYRIT_API_PORT` / `PYRIT_API_RELOAD`). -- The backend may define its own presentation models, but where a `pyrit.models` type already exists it should reuse that type rather than redefine it. -- Components should be constructed through the registry, not built directly in the backend. +## [CLI](../scanner/0_scanner) -**Does NOT own**: +**Responsibility**: Offer command-line entry points into PyRIT as a thin REST client over the backend, deliberately avoiding heavy `pyrit` imports. -- The shape of core types — that is Models. -- Constructing or storing components — that is the Registry. +- Because it talks to the backend over HTTP, the CLI stays lightweight and starts quickly. +- It should not rely on pyrit other than pyrit.models, pyrit.common, and pyrit.output. -## [Memory](./memory/0_memory.md) +## [Documentation](../contributing/7_notebooks) -**Source**: `pyrit/memory/`. +**Responsibility**: Show how PyRIT is used, concisely and runnably, across all the ways someone might pick it up. -**Responsibility**: The canonical store that components read from and write to — seeds, conversations, scores, and attack results. When a component needs more than what is passed in, it goes through memory. +PyRIT can be used in three modes ([Scanner](../scanner/0_scanner), [GUI](../gui/0_gui), and [Framework](#core-components)), and the documentation is organized to match: -One important thing to remember about this architecture is its swappable nature. Prompts, targets, converters, attacks, and scorers should all be swappable. But sometimes one of these components needs additional information — if the target is an LLM, we need a way to look up previous messages sent to that session so we can construct the new message; if the target is a blob store, we need the URL to use for a future attack. Memory is where that shared state lives. - -## [Setup](./setup/0_setup) +- Notebooks that contain code should be notebooks that can execute. +- Notebooks should execute quickly (within a couple minutes). +- The percent-format `.py` files and their paired `.ipynb` notebooks must be kept in sync. -**Source**: `pyrit/setup/`. +## [Frontend](../gui/0_gui) -**Responsibility**: Initialize PyRIT and configure framework-wide defaults — memory selection, default targets, and resiliency settings. +**Responsibility**: Provide CoPyRIT, the graphical interface for human-led red teaming, by talking to the backend REST API. -- Setup wires up the environment a run depends on; it does not implement attack behavior. +- A TypeScript + React single-page app built with Vite and Fluent UI. +- Dev workflow via `dev.py` / npm scripts orchestrates both servers together; tested with Jest (unit) and Playwright (e2e). -## [Framework Documentation](../contributing/7_notebooks.md) -**Source**: `doc/` (component notebooks, e.g. `doc/code/`). -**Responsibility**: Show how the framework is used, concisely. -- Notebooks that contain code should be executable. -- Notebooks should execute quickly. From 6ce59ed9b9f5397949db1fd14947a50d38d753e0 Mon Sep 17 00:00:00 2001 From: Richard Lundeen Date: Tue, 30 Jun 2026 14:39:02 -0700 Subject: [PATCH 08/14] =?UTF-8?q?DOC:=20fix=20diagram=20edge=20direction?= =?UTF-8?q?=20=E2=80=94=20Output=20reads=20from=20Memory?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Output's printers fetch result data from CentralMemory to render it; the module never writes to memory. Reverse the OUT->MEM edge to MEM->OUT and label it 'reads' to reflect the actual data dependency. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- doc/code/framework.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/code/framework.md b/doc/code/framework.md index de8881b493..ad7725b8b9 100644 --- a/doc/code/framework.md +++ b/doc/code/framework.md @@ -97,7 +97,7 @@ flowchart TB ATK <-- reads / writes --> MEM core <--> MEM SCEN --> OUT - OUT --> MEM + MEM -- reads --> OUT MODEL -. shared types .-> core classDef flow fill:#e8f0fe,stroke:#4285f4,color:#15233a; From 4f1c076e70686a84d494611b2fc7019260475856 Mon Sep 17 00:00:00 2001 From: Richard Lundeen Date: Tue, 30 Jun 2026 14:42:11 -0700 Subject: [PATCH 09/14] DOC: entry points invoke Output, not scenarios Scenarios return results; they don't render them. The entry points (Scanner/CLI, GUI/Backend, Framework/Notebooks) invoke Output to display results, pulling the data from Memory. Re-point the render edge from entry points instead of Scenario. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- doc/code/framework.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/code/framework.md b/doc/code/framework.md index ad7725b8b9..82d41f8d36 100644 --- a/doc/code/framework.md +++ b/doc/code/framework.md @@ -96,7 +96,7 @@ flowchart TB REG -. builds .-> ATK ATK <-- reads / writes --> MEM core <--> MEM - SCEN --> OUT + entry --> OUT MEM -- reads --> OUT MODEL -. shared types .-> core From bb0f6384b09202da222726f67c50a8bce4821767 Mon Sep 17 00:00:00 2001 From: Richard Lundeen Date: Tue, 30 Jun 2026 14:43:54 -0700 Subject: [PATCH 10/14] re-ordering diagrams --- doc/code/framework.md | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/doc/code/framework.md b/doc/code/framework.md index 82d41f8d36..e80c168f85 100644 --- a/doc/code/framework.md +++ b/doc/code/framework.md @@ -106,13 +106,12 @@ flowchart TB class MEM,REG,MODEL,OUT libnode; ``` -# Core Components - -As much as possible, each core component is a pluggable brick of functionality. Prompts from one attack can be used in another. An attack for one scenario can use multiple targets. And sometimes you completely skip components (e.g. almost every component can be a NoOp also, you can have a NoOp converter that doesn't convert, or a NoOp target that just prints the prompts). +The orchestration layers **nest from broadest to narrowest** — each owns less than the layer above it: -If you are contributing to PyRIT, that work will most likely land in one of the core components buckets and be as self-contained as possible. It isn't always this clean, but when an attack scenario doesn't quite fit (and that's okay!) it's good to brainstorm with the maintainers about how we can modify our architecture. Also, please open issues if you see anything under Framework Plans you do/don't want. +- **Scenario** packages many attack techniques and owns parallelism and resiliency. +- **Attack Technique** configures one executor with its converters, seeds, scorers, and strategy. +- **Executor / Attack** runs the algorithm: sends to targets, applies converters, and branches on scorers. -The orchestration layers **nest from broadest to narrowest** — each owns less than the layer above it: ```mermaid flowchart TB @@ -125,11 +124,15 @@ flowchart TB end ``` -- **Scenario** packages many attack techniques and owns parallelism and resiliency. -- **Attack Technique** configures one executor with its converters, seeds, scorers, and strategy. -- **Executor / Attack** runs the algorithm: sends to targets, applies converters, and branches on scorers. -(*Attack Technique* and *Attack* are deliberately different: a **technique is configuration**, an **attack is the running algorithm** it configures.) + +# Core Components + +As much as possible, each core component is a pluggable brick of functionality. Prompts from one attack can be used in another. An attack for one scenario can use multiple targets. And sometimes you completely skip components (e.g. almost every component can be a NoOp also, you can have a NoOp converter that doesn't convert, or a NoOp target that just prints the prompts). + +If you are contributing to PyRIT, that work will most likely land in one of the core components buckets and be as self-contained as possible. It isn't always this clean, but when an attack scenario doesn't quite fit (and that's okay!) it's good to brainstorm with the maintainers about how we can modify our architecture. Also, please open issues if you see anything under Framework Plans you do/don't want. + + ## [Datasets](./datasets/0_dataset) From a7ddedb10d6b6ae4bca259f7a5c64ccf6966bc9f Mon Sep 17 00:00:00 2001 From: Richard Lundeen Date: Tue, 30 Jun 2026 14:46:09 -0700 Subject: [PATCH 11/14] re-ordering diagrams --- doc/code/framework.md | 1 - 1 file changed, 1 deletion(-) diff --git a/doc/code/framework.md b/doc/code/framework.md index e80c168f85..596cfba02d 100644 --- a/doc/code/framework.md +++ b/doc/code/framework.md @@ -133,7 +133,6 @@ As much as possible, each core component is a pluggable brick of functionality. If you are contributing to PyRIT, that work will most likely land in one of the core components buckets and be as self-contained as possible. It isn't always this clean, but when an attack scenario doesn't quite fit (and that's okay!) it's good to brainstorm with the maintainers about how we can modify our architecture. Also, please open issues if you see anything under Framework Plans you do/don't want. - ## [Datasets](./datasets/0_dataset) **Responsibility**: Create a single place to manage seeds From 1cc2fb32b06b75cce0258348905254ddffe8f210 Mon Sep 17 00:00:00 2001 From: Richard Lundeen Date: Tue, 30 Jun 2026 14:52:27 -0700 Subject: [PATCH 12/14] DOC: fix trailing whitespace and end-of-file (pre-commit) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- doc/code/framework.md | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/doc/code/framework.md b/doc/code/framework.md index 596cfba02d..3f646e4138 100644 --- a/doc/code/framework.md +++ b/doc/code/framework.md @@ -184,7 +184,7 @@ If you are contributing to PyRIT, that work will most likely land in one of the ## [Executors and Attacks](./executor/0_executor) -**Executor Responsibility**: Manage conversations between objective targets and adversarial targets; using datasets, scorers, and converters. +**Executor Responsibility**: Manage conversations between objective targets and adversarial targets; using datasets, scorers, and converters. **Attack Responsibility**: An attack is a type of executor, which manages conversations to achieve an objective. @@ -234,7 +234,7 @@ If you are contributing to PyRIT, that work will most likely land in one of the - Better agent support may require extra pieces attached to a Message - Better surface support may require expanding the return types -**Contributing (difficulty low)**: +**Contributing (difficulty low)**: - The pattern is well-defined. - Are there models you want to use at any stage or for different attacks? But also, can your model just be one of the existing targets? @@ -251,7 +251,7 @@ If you are contributing to PyRIT, that work will most likely land in one of the - Scorers will be refactored to be more generic, so they can determine more general results (does a file exist? Was a tool called?) -**Contributing (difficulty low)**: +**Contributing (difficulty low)**: - The pattern is well-defined. - You can evaluate how accurate probabilistic scorers are and likely make them more accurate. @@ -321,7 +321,7 @@ The below talks about responsibilities of most modules in the PyRIT library ## [Registry](./registry/0_registry) -**Responsibility**: The registry is used to build and store the core components. +**Responsibility**: The registry is used to build and store the core components. - If you are creating a component with user input (e.g. via config, REST, or automatically) it should always use the registry - If you are storing an instance of a component, it should always use the registry @@ -371,7 +371,3 @@ PyRIT can be used in three modes ([Scanner](../scanner/0_scanner), [GUI](../gui/ - A TypeScript + React single-page app built with Vite and Fluent UI. - Dev workflow via `dev.py` / npm scripts orchestrates both servers together; tested with Jest (unit) and Playwright (e2e). - - - - From 117cbbbd0a93b3278596678fcb497e2ee200057e Mon Sep 17 00:00:00 2001 From: Richard Lundeen Date: Tue, 30 Jun 2026 14:56:10 -0700 Subject: [PATCH 13/14] DOC: point copilot-instructions at framework.md; prioritize component responsibilities in review Reference doc/code/framework.md as the canonical architecture/responsibility guide, and make 'each component owns only its job' the top code-review priority (flag responsibility bleed between bricks). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/copilot-instructions.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 57b8eb0e1f..a5ebb406f2 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -12,10 +12,13 @@ PyRIT uses a modular pluggable-brick design. The main extensibility points are: - **Executors / Scenarios** (`pyrit/executor/`, `pyrit/scenario/`) — Orchestrate multi-turn attacks. - **Memory** (`pyrit/memory/`) — `CentralMemory` for prompt/response persistence. +**[`doc/code/framework.md`](../doc/code/framework.md) is the canonical reference for how these pieces fit together.** It defines each component's responsibilities — what it owns and, critically, what it *does not* own — and how scenarios, attack techniques, executors, and the core/shared layers relate. Read it before adding or reviewing components so new code lands in the right place. + ## Code Review Guidelines When performing a code review, be selective. Only leave comments for issues that genuinely matter: +- **Component responsibilities (prioritize this)** — Each component should do its job and *only* its job, per [`doc/code/framework.md`](../doc/code/framework.md). Flag responsibility bleed: e.g. an executor assembling prepended/system prompts or role-play framing (that's an attack technique), a converter or target making branching decisions (that's an attack/scorer), a scorer acting on its own result (the attack branches), or business logic living in memory/output. If logic belongs in a different brick, say so. - Bugs, logic errors, or security concerns - Unclear code that would benefit from refactoring for readability - Violations of the critical coding conventions above (async suffix, keyword-only args, type annotations) From a60bc0249b0f4a04c2cc4290c9c04ba05c1bd1f5 Mon Sep 17 00:00:00 2001 From: Richard Lundeen Date: Tue, 30 Jun 2026 15:00:29 -0700 Subject: [PATCH 14/14] DOC: surface component-responsibility boundaries in instruction files Add a 'Does not own' note pointing to framework.md in the pluggable-brick instruction files (converters, datasets, targets, scorers, attacks, scenarios, output) so responsibility bleed is flagged during review. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/copilot-instructions.md | 12 +++--------- .github/instructions/attacks.instructions.md | 2 ++ .github/instructions/converters.instructions.md | 2 ++ .github/instructions/datasets.instructions.md | 2 ++ .github/instructions/output.instructions.md | 2 ++ .github/instructions/scenarios.instructions.md | 2 ++ .github/instructions/scorers.instructions.md | 2 ++ .github/instructions/targets.instructions.md | 2 ++ 8 files changed, 17 insertions(+), 9 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index a5ebb406f2..6ea8ddea76 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -4,13 +4,7 @@ PyRIT (Python Risk Identification Tool for generative AI) is an open-source fram ## Architecture -PyRIT uses a modular pluggable-brick design. The main extensibility points are: - -- **Prompt Converters** (`pyrit/prompt_converter/`) — Transform prompts (70+ implementations). Base: `PromptConverter`. -- **Scorers** (`pyrit/score/`) — Evaluate responses. Base: `Scorer`. -- **Prompt Targets** (`pyrit/prompt_target/`) — Send prompts to LLMs/APIs. Base: `PromptTarget`. -- **Executors / Scenarios** (`pyrit/executor/`, `pyrit/scenario/`) — Orchestrate multi-turn attacks. -- **Memory** (`pyrit/memory/`) — `CentralMemory` for prompt/response persistence. +PyRIT uses a modular pluggable-brick design. **[`doc/code/framework.md`](../doc/code/framework.md) is the canonical reference for how these pieces fit together.** It defines each component's responsibilities — what it owns and, critically, what it *does not* own — and how scenarios, attack techniques, executors, and the core/shared layers relate. Read it before adding or reviewing components so new code lands in the right place. @@ -18,8 +12,8 @@ PyRIT uses a modular pluggable-brick design. The main extensibility points are: When performing a code review, be selective. Only leave comments for issues that genuinely matter: -- **Component responsibilities (prioritize this)** — Each component should do its job and *only* its job, per [`doc/code/framework.md`](../doc/code/framework.md). Flag responsibility bleed: e.g. an executor assembling prepended/system prompts or role-play framing (that's an attack technique), a converter or target making branching decisions (that's an attack/scorer), a scorer acting on its own result (the attack branches), or business logic living in memory/output. If logic belongs in a different brick, say so. -- Bugs, logic errors, or security concerns +- Bugs, correctness, logic errors, or security concerns +- **Component responsibilities** — Each component should do its job and *only* its job, per [`doc/code/framework.md`](../doc/code/framework.md). Flag responsibility bleed: e.g. an executor assembling prepended/system prompts or role-play framing (that's an attack technique), a converter or target making branching decisions (that's an attack/scorer), a scorer acting on its own result (the attack branches), or business logic living in memory/output. If logic belongs in a different brick, say so. - Unclear code that would benefit from refactoring for readability - Violations of the critical coding conventions above (async suffix, keyword-only args, type annotations) diff --git a/.github/instructions/attacks.instructions.md b/.github/instructions/attacks.instructions.md index 2d5c4d7c96..0a5830fadc 100644 --- a/.github/instructions/attacks.instructions.md +++ b/.github/instructions/attacks.instructions.md @@ -6,6 +6,8 @@ applyTo: "pyrit/executor/attack/**" `AttackStrategy` subclasses (single-turn attacks like `PromptSendingAttack`, multi-turn attacks like `RedTeamingAttack`, etc.) are pluggable bricks orchestrated by `AttackExecutor` and the `Scenario` framework. Style rules from `style-guide.instructions.md` (async `_async` suffix, keyword-only args, type hints, enums-over-Literals) still apply and are not repeated here. +**Does not own** (see [framework.md](../../doc/code/framework.md)): packaging the attack. Prepended/system prompts, role-play framing, the converter stack, and dataset selection are passed in as configuration by the **attack technique** — an attack must accept them as parameters, not assemble them itself (e.g. `RolePlayAttack` building its own prompt scaffolding is attack-technique work bleeding into the executor). It also must not branch on raw responses (use a scorer), construct its own components (use the registry), or format/persist results itself (output/memory). Flag such bleed in review. + ## Constructor contract `AttackStrategy` subclasses MUST follow the keyword-only constructor shape: diff --git a/.github/instructions/converters.instructions.md b/.github/instructions/converters.instructions.md index 54e14e315a..f16a67df13 100644 --- a/.github/instructions/converters.instructions.md +++ b/.github/instructions/converters.instructions.md @@ -6,6 +6,8 @@ applyTo: "pyrit/prompt_converter/**" **Responsibility**: A converter transforms a prompt into something else (rephrasing, encoding, translating to a Word document, overlaying text on an image, ...). Converters can be stacked and combined, and any converter may also be a NoOp. +**Does not own** (see [framework.md](../../doc/code/framework.md)): conversation state or attack decisions. A converter transforms input into output (and may call a target to do so); it must not branch on results, score, persist to memory itself, or decide when it runs — the attack/technique configures the stack. Flag such bleed in review. + ## Base Class Contract All converters MUST inherit from `PromptConverter` and implement: diff --git a/.github/instructions/datasets.instructions.md b/.github/instructions/datasets.instructions.md index 83276ed768..bf986b72b4 100644 --- a/.github/instructions/datasets.instructions.md +++ b/.github/instructions/datasets.instructions.md @@ -6,6 +6,8 @@ applyTo: "pyrit/datasets/seed_datasets/**" **Responsibility**: Seed dataset loaders (`SeedDatasetProvider` subclasses) are the single place to manage the prompts/objectives for a source. They load seeds into `CentralMemory`; components then retrieve seeds from memory — components never read from a loader directly. +**Does not own** (see [framework.md](../../doc/code/framework.md)): a loader defines and holds seeds; it must not select or combine which seeds an attack uses (that's a scenario/attack technique) or render/parameterize prompts at send time (converters/normalizers). Flag such bleed in review. + These rules apply when adding or modifying loaders under `pyrit/datasets/seed_datasets/`. Style rules from `style-guide.instructions.md` (async `_async` suffix, keyword-only args, type hints, enums-over-Literals) still apply and are not repeated here. diff --git a/.github/instructions/output.instructions.md b/.github/instructions/output.instructions.md index d099c65a44..05f88fddec 100644 --- a/.github/instructions/output.instructions.md +++ b/.github/instructions/output.instructions.md @@ -8,6 +8,8 @@ For full architecture documentation, usage examples, and extension guides, see [ This file covers the rules for **writing and reviewing** code in `pyrit/output/`. +**Does not own** (see [framework.md](../../doc/code/framework.md)): deciding *what* to render or *when*. Components hand results to output; format classes only turn data into strings and must never fetch data, touch `CentralMemory`, or call `print()` directly (that's isolated to leaf printer classes). Flag such bleed in review. + ## Critical Rules ### Output goes through the sink — never call `print()` directly diff --git a/.github/instructions/scenarios.instructions.md b/.github/instructions/scenarios.instructions.md index 40fb4150b8..799bf6b508 100644 --- a/.github/instructions/scenarios.instructions.md +++ b/.github/instructions/scenarios.instructions.md @@ -6,6 +6,8 @@ applyTo: "pyrit/scenario/**" Scenarios orchestrate multi-attack security testing campaigns. Each scenario groups `AtomicAttack` instances and executes them sequentially against a target. +**Does not own** (see [framework.md](../../doc/code/framework.md)): the per-objective conversation logic. Branching, turn-by-turn adaptation, and scoring-based decisions belong to the attack — a scenario selects and packages existing attack techniques and owns parallelism/resiliency, not new attack algorithms or datasets. Flag such bleed in review. + ## Base Class Contract All scenarios inherit from `Scenario` (ABC) and must: diff --git a/.github/instructions/scorers.instructions.md b/.github/instructions/scorers.instructions.md index b4200704e0..20ce895f8a 100644 --- a/.github/instructions/scorers.instructions.md +++ b/.github/instructions/scorers.instructions.md @@ -6,6 +6,8 @@ applyTo: "pyrit/score/**" Scorers evaluate model responses against an objective and live under `pyrit/score/`. Style rules from `style-guide.instructions.md` (async `_async` suffix, keyword-only args, type hints, enums-over-Literals) still apply and are not repeated here. +**Does not own** (see [framework.md](../../doc/code/framework.md)): acting on its own result. A scorer evaluates a response and returns a score; branching on that score is the attack's job and aggregating scores across runs is analytics'. It may call a target to evaluate, but must not send the attack's objective prompt or manage the conversation. Flag such bleed in review. + ## Constructor contract `Scorer` subclasses MUST use the keyword-only constructor shape: diff --git a/.github/instructions/targets.instructions.md b/.github/instructions/targets.instructions.md index 138af4250a..5439f616a4 100644 --- a/.github/instructions/targets.instructions.md +++ b/.github/instructions/targets.instructions.md @@ -6,6 +6,8 @@ applyTo: "pyrit/prompt_target/**" **Responsibility**: A prompt target is "the thing we're sending the prompt to" — often an LLM, but it can be any endpoint (e.g. a storage account for cross-domain prompt injection). Targets use `message_normalizer` together with `TargetConfiguration` to transform `Message`s into the format the target supports. +**Does not own** (see [framework.md](../../doc/code/framework.md)): what to send or what to do with the response. A target sends a prepared `Message` and returns a response; it must not convert prompts (converters), score (scorers), or manage the conversation / decide the next turn (attacks). Flag such bleed in review. + ## Base Class Contract All targets MUST inherit from ``PromptTarget`` (or one of its public