Skip to content

FEAT: Save converter copies - #4069

Open
varunj-msft wants to merge 174 commits into
microsoft:mainfrom
varunj-msft:varunj-msft/13178-Save-Converter-Copies
Open

varunj-msft wants to merge 174 commits into
microsoft:mainfrom
varunj-msft:varunj-msft/13178-Save-Converter-Copies

Conversation

@varunj-msft

@varunj-msft varunj-msft commented Nov 8, 2026 •

Copy link
Copy Markdown
Contributor

Description

Stacked on #4052, with #3984 merged in as the first commit. This PR's own changes: varunj-msft/PyRIT@c1f28b9...varunj-msft/13178-Save-Converter-Copies

This is one of the changes for saving Registry entries across restarts. It needs #3984, which keeps a copy's endpoint and credentials the same as its source's. Until the earlier PRs merge, the diff here also shows their changes.

Save to registry on a converter's changed settings, in chat or in a technique edit, made a Temporary copy that was gone after a restart. Copies can now be saved, and a saved copy is rebuilt from the converter it copies at every start.

  • A converter created with source and "persist": true is saved as a recipe that names the converter it copies and holds only the changed settings. At every start it's rebuilt from that converter as it is then, after it, so it keeps where that converter sends requests and how it signs in.
  • Only the changes are checked: they can't hold a credential or name a Temporary entry (typed 400). A reference the changes clear is saved as the entry its default names, as for a new converter, so a Temporary default is refused too. A copy of a Temporary converter can't be saved, since that converter is gone after a restart. A copy can't change its source's connection settings at all, saved or not, and that refusal now comes before the credential and Temporary-entry checks on the changes.
  • Restore checks a copy's saved changes the same way, waits for the converter it copies, and keeps who saved it. A copy whose converter is gone, or is now a different class, fails to restore and says why.
  • The Save to registry dialog saves by default, follows the saved-name rule, and offers Create as Temporary with the reason when the copy can't be saved. A taken name shows on the name field.
  • The backend README and the GUI doc cover saved copies.

What stays the same: a copy made with Create as Temporary is Temporary, as before, and a copy still can't change its source's connection settings (#3984). Target copies are unchanged.

Screenshots: Save to registry for a copy of a saved converter, the saved copy in the Converters table, and a copy of a Temporary converter offered as Temporary with the reason.
copies_1_save_copy_dialog
copies_2_saved_copy_row
copies_3_copy_of_temporary_offers_temporary

Not covered:

…API input

External construction now rejects a parameter whose type includes Path unless the
calling service passes the name in allowed_path_params after making the value safe.
The converter service allows its upload parameters once they are stored, and the
target service allows the server-set upload folder and the GitHubCopilotTarget
working directory. The scorer-only check is removed, and the "Valid parameters"
list leaves out path parameters callers cannot set.
The target catalog now applies the path rule to required parameters too, so a
target that needs a disallowed path is left out instead of listed without the
field, matching the scorer catalog. allowed_path_params is typed frozenset[str]
so a bare string cannot turn the allow-list into a substring match.
The target catalog still lists a target whose required path parameter is its
server-set upload folder when the server has one, and leaves it out otherwise.
# Conflicts:
#	tests/unit/backend/test_converter_service.py
# Conflicts:
#	tests/unit/registry/test_registry_recipe_rules.py
# Conflicts:
#	pyrit/backend/mappers/target_mappers.py
#	pyrit/backend/models/scorers.py
#	pyrit/backend/routes/converters.py
#	pyrit/backend/routes/targets.py
#	pyrit/backend/services/converter_service.py
#	pyrit/backend/services/scorer_service.py
#	pyrit/backend/services/target_service.py
#	pyrit/registry/instance_registry.py
#	tests/unit/backend/test_api_routes.py
#	tests/unit/backend/test_converter_service.py
#	tests/unit/backend/test_mappers.py
#	tests/unit/backend/test_runtime_lifecycle.py
#	tests/unit/backend/test_scorer_service.py
#	tests/unit/backend/test_target_service.py
#	tests/unit/registry/test_instance_registry.py
main now declares that TextTarget takes no credentials, with an empty
supported_auth_modes. The saved-entry check read that as supporting no mode at
all, so it refused saved TextTargets and left the saveability check for the
target types with no mode to report. A class that declares no auth modes is now
checked as the default api_key mode, which it has no key for.
# Conflicts:
#	pyrit/registry/registry_recipe_rules.py
A reference left to a configured default can be a list, such as a composite
scorer's scorers. Each entry in the list is now named in the saved recipe, in
order, and the list is refused if any of them is Temporary or not registered,
instead of the whole list being refused as one unregistered entry.
# Conflicts:
#	frontend/src/components/Registry/CreateConverterDialog.tsx
# Conflicts:
#	pyrit/converter/azure_speech_audio_to_text_converter.py
#	pyrit/converter/azure_speech_text_to_audio_converter.py
#	pyrit/prompt_target/common/prompt_target.py
A copy can't change its source's connection settings even when it isn't saved, so that
refusal now comes first instead of a not-saveable one that says the copy can still be
created without saving it. Tests cover a credential change, a saved copy edited to send
requests elsewhere, and the credential check for a class that lets copies change one.
Copilot AI balanced review requested due to automatic review settings November 8, 2026 22:21

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Synchronous discovery and component construction in restore() can stall health and configuration requests during live apply and should be offloaded while preserving initialization that requires the runtime loop.

1 open issue
What changed in this PR

Adds persistent converter copies to PyRIT’s registry, building on the stacked persistence and connection-setting safeguards. Saved copies retain only their overrides and are rebuilt from their source after restart.

Changes:

  • Adds saved-entry recipes, restore handling, origin metadata, and safeguards against replacing saved entries.
  • Makes converter-copy saving the default in the dialog, with a Temporary fallback and name validation.
  • Extends registry displays, documentation, and regression coverage, while isolating test recipe storage.
File Description
tests/​unit/​setup/​test_targets_initializer.py Tests saved-target collision protection.
tests/​unit/​setup/​test_scorer_initializer.py Tests saved-scorer tagging safeguards.
tests/​unit/​setup/​test_converter_initializer.py Tests saved-converter replacement refusal.
tests/​unit/​setup/​test_configuration_loader.py Tests recipe storage configuration.
tests/​unit/​registry/​test_resolution.py Tests copy-locked parameter metadata.
tests/​unit/​registry/​test_instance_registry.py Tests saved-entry protection and origins.
tests/​unit/​registry/​test_converter_registry.py Tests collisions and locked copy settings.
tests/​unit/​registry/​test_attack_technique_registry.py Tests saved-technique collision handling.
tests/​unit/​models/​test_parameter.py Updates parameter serialization expectations.
tests/​unit/​backend/​test_temporary_components.py Tests target-copy connection safeguards.
tests/​unit/​backend/​test_technique_edit_service.py Tests locked converter settings in edits.
tests/​unit/​backend/​test_scorer_routes.py Tests scorer persistence and API errors.
tests/​unit/​backend/​test_scenario_suite_routes.py Tests sanitized storage failures.
tests/​unit/​backend/​test_scenario_suite_execution_service.py Tests saved-entry launch conflict messages.
tests/​unit/​backend/​test_scenario_run_service.py Tests launch-script replacement safeguards.
tests/​unit/​backend/​test_scenario_preset_routes.py Tests sanitized storage responses and logs.
tests/​unit/​backend/​test_saved_entries.py Tests temporary-reference and credential refusals.
tests/​unit/​backend/​test_mappers.py Tests target origin and saver metadata.
tests/​unit/​backend/​test_initializer_service.py Tests local storage error sanitization.
tests/​unit/​backend/​test_auth_middleware.py Tests current-user ID extraction.
tests/​unit/​backend/​test_analytics_benchmark.py Tests isolated benchmark recipe storage.
tests/​unit/​backend/​conftest.py Isolates saved entries in backend tests.
tests/​end_to_end/​conftest.py Uses temporary backend recipe storage.
test_support/​analytics_benchmark.py Configures benchmark recipe isolation.
pyrit/​setup/​initializers/​tools.py Rejects saved-entry substitutions.
pyrit/​setup/​initializers/​targets.py Protects saved names during target grouping.
pyrit/​setup/​initializers/​scorers.py Prevents tagging saved substitutes.
pyrit/​setup/​configuration_loader.py Adds recipe storage configuration.
pyrit/​score/​true_false/​local_refusal_classifier_scorer.py Declares persistence restrictions.
pyrit/​score/​true_false/​audio_true_false_scorer.py Declares transcription credential restrictions.
pyrit/​score/​scorer.py Adds scorer persistence policy metadata.
pyrit/​score/​float_scale/​azure_content_filter_scorer.py Declares credential fallback behavior.
pyrit/​score/​float_scale/​audio_float_scale_scorer.py Declares transcription persistence restrictions.
pyrit/​registry/​resolution.py Derives copy-locked parameter metadata.
pyrit/​registry/​registry.py Rejects locked external copy overrides.
pyrit/​registry/​components/​scorer_registry.py Reserves the scorer types route name.
pyrit/​registry/​components/​attack_technique_registry.py Preserves metadata and detects saved collisions.
pyrit/​prompt_target/​websocket_copilot_target.py Declares target persistence restrictions.
pyrit/​prompt_target/​prompt_shield_target.py Declares credential and destination policies.
pyrit/​prompt_target/​openai/​openai_target.py Declares persistence policies and locked settings.
pyrit/​prompt_target/​litellm_chat_target.py Declares API-key fallback behavior.
pyrit/​prompt_target/​hugging_face/​hugging_face_chat_target.py Declares token and model persistence restrictions.
pyrit/​prompt_target/​http_target/​http_target.py Prevents saving credential-bearing HTTP requests.
pyrit/​prompt_target/​hack_a_prompt_target.py Declares session and cookie fallback behavior.
pyrit/​prompt_target/​github_copilot_target.py Declares token fallback behavior.
pyrit/​prompt_target/​common/​prompt_target.py Adds shared persistence and copy policies.
pyrit/​prompt_target/​azure_ml_chat_target.py Declares credential and destination policies.
pyrit/​prompt_target/​azure_blob_storage_target.py Declares SAS and destination policies.
pyrit/​prompt_target/​a2a_target.py Declares destination validation metadata.
pyrit/​models/​parameter.py Adds the copy-locked parameter flag.
pyrit/​models/​catalog/​target.py Adds target origin and saver fields.
pyrit/​models/​catalog/​scorer.py Adds scorer origin metadata.
pyrit/​models/​catalog/​registry_recipe.py Defines saved recipes and restore reports.
pyrit/​models/​catalog/​__init__.py Exports recipe and restore models.
pyrit/​converter/​converter.py Adds shared copy and credential policies.
pyrit/​converter/​azure_speech_text_to_audio_converter.py Locks speech connection settings.
pyrit/​converter/​azure_speech_audio_to_text_converter.py Locks speech connection settings.
pyrit/​backend/​services/​scorer_service.py Saves scorer recipes and reports origins.
pyrit/​backend/​services/​scenario_suite_execution_service.py Exposes safe saved-entry conflict messages.
pyrit/​backend/​services/​scenario_run_service.py Detects launch-script saved-entry conflicts.
pyrit/​backend/​services/​runtime_lifecycle.py Restores recipes and reports readiness failures.
pyrit/​backend/​routes/​targets.py Passes saver context and maps persistence errors.
pyrit/​backend/​routes/​scorers.py Supports scorer persistence responses.
pyrit/​backend/​routes/​scenarios.py Documents saved-entry launch conflicts.
pyrit/​backend/​routes/​scenario_suites.py Sanitizes storage failure logging.
pyrit/​backend/​routes/​scenario_presets.py Sanitizes storage failure logging.
pyrit/​backend/​routes/​initializers.py Handles and sanitizes storage failures.
pyrit/​backend/​routes/​converters.py Maps save errors and blocks saved removal.
pyrit/​backend/​routes/​common.py Adds saver and save-refusal helpers.
pyrit/​backend/​README.md Documents persistence APIs and converter copies.
pyrit/​backend/​models/​targets.py Adds target persistence request validation.
pyrit/​backend/​models/​scorers.py Adds scorer persistence request validation.
pyrit/​backend/​models/​converters.py Adds converter persistence and origin fields.
pyrit/​backend/​middleware/​auth.py Exposes the current user’s object ID.
pyrit/​backend/​mappers/​target_mappers.py Maps target origin and saver metadata.
pyrit/​backend/​mappers/​converter_mappers.py Maps converter origin metadata.
frontend/​src/​utils/​registryOrigin.ts Defines shared origin labels.
frontend/​src/​utils/​registryName.ts Generates valid, available registry names.
frontend/​src/​utils/​registryName.test.ts Tests name validation and suggestions.
frontend/​src/​types/​index.ts Adds persistence and origin API types.
frontend/​src/​test-utils/​targetFixtures.ts Supports origin and saver test data.
frontend/​src/​services/​errors.ts Recognizes typed save refusals.
frontend/​src/​services/​errors.test.ts Tests save-refusal detection.
frontend/​src/​components/​Techniques/​TechniqueEditor.test.tsx Updates converter save expectations.
frontend/​src/​components/​Registry/​ScorerRegistry.tsx Displays scorer origin badges.
frontend/​src/​components/​Registry/​ScorerRegistry.test.tsx Tests scorer origin labels.
frontend/​src/​components/​Registry/​OriginBadge.tsx Adds a shared registry origin badge.
frontend/​src/​components/​Registry/​ConverterRegistry.tsx Shows origins and disables saved removal.
frontend/​src/​components/​Registry/​ConverterRegistry.test.tsx Tests origin labels and removal restrictions.
frontend/​src/​components/​Converters/​SaveConverterDialog.tsx Saves copies with Temporary fallback.
frontend/​src/​components/​Converters/​SaveConverterDialog.test.tsx Tests copy saving, refusals, and names.
frontend/​src/​components/​Config/​TargetTable.tsx Shows origins, savers, and ownership filtering.
frontend/​src/​components/​Config/​TargetTable.test.tsx Tests origin and saver displays.
frontend/​src/​components/​Config/​TargetTable.styles.ts Styles saver labels and filter controls.
frontend/​src/​components/​Chat/​ConverterPanel.save.test.tsx Expects persistent converter-copy requests.
frontend/​playwright.config.ts Isolates seeded backend recipe storage.
frontend/​e2e/​fixtures/​manual_send_backend.py Isolates offline fixture recipe storage.
frontend/​e2e/​config.spec.ts Tests target saving and saver filtering.
frontend/​e2e/​_targets.ts Adds origin and saver mock fields.
doc/​gui/​0_gui.md Explains saved entries and converter copies.
doc/​getting_started/​pyrit_conf.md Documents recipe storage and restore failures.
doc/​code/​registry/​0_registry.md Documents locked copy connection settings.
.pyrit_conf_example Adds commented recipe storage configuration.

Review effort: Balanced

# the target API builds them, because a constructor such as HuggingFaceChatTarget's starts a task
# on it. A slow constructor holds the loop while it runs.
saved = await asyncio.to_thread(restorer.read)
report = restorer.restore(saved=saved)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leaving this as it is. The restore code comes from #3909 and builds entries on the loop on purpose: a constructor such as HuggingFaceChatTarget's starts a task on the running loop, and the target API builds them the same way. Reading the files already runs in a worker thread. A saved copy doesn't change that, since the converter it copies was built the same way at startup.

A copy whose changes cleared a reference, such as a converter's target, took
the configured default while it was saved and wrote the cleared value, so a
Temporary default wasn't refused and the copy depended on defaults set again
at every start. The changes now name that entry as a new saved entry's do,
and a Temporary or unregistered default is refused.
The cached service keeps the registry it was made with, so after another test
reset the registry the request couldn't find the converter this test
registered. The test now clears the cache and the registry before and after.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants