Skip to content

FEAT: Save targets from the Registry - #4027

Open
varunj-msft wants to merge 122 commits into
microsoft:mainfrom
varunj-msft:varunj-msft/13178-Save-From-Registry
Open

varunj-msft wants to merge 122 commits into
microsoft:mainfrom
varunj-msft:varunj-msft/13178-Save-From-Registry

Conversation

@varunj-msft

Copy link
Copy Markdown
Contributor

Description

This is one of the changes for saving Registry entries across restarts. It builds on #4026, which saves targets created through the API. Until the earlier PRs merge, the diff here also shows their changes.

Problem. The New Target dialog in Registry > Targets still makes targets that are gone after the backend restarts. It sends no name, so the backend makes one up (compat_<hex>), and the Targets table doesn't say which targets will come back.

Change.

  • The dialog has a Registry Name field. Until the user edits it, it suggests a name from the type and model that no listed target has (openai_chat_gpt_4o). The model tells saved targets apart and keeps the suggestion off names the startup scripts register, such as openai_chat. The field checks the same rule the backend uses for saved names. A name the Targets list already has, or one the backend says is taken (409), shows on the field and holds Save until the name changes.
  • The dialog saves by default (persist: true). When a target can't be saved, the button reads Create as Temporary and a warning says why, and that a Temporary target is gone after the backend restarts or is reinitialized. The dialog says so before submitting when the backend's type list already tells it: the type can't be saved, the chosen sign-in can't be saved (an API key, for now), a credential is typed in, or a round robin uses a Temporary target. Otherwise the backend's refusal turns the button into Create as Temporary for those exact values, and changing any of them goes back to Save Target. No other error offers Temporary.
  • A new target starts with Microsoft Entra ID, or No key, when a saved target of its type can use it, so the usual case saves. This also applies when the type's details arrive after the type was picked. Picking another type resets the choice and clears a typed key. For an OpenAI endpoint that isn't on Azure, the Entra ID error now says to choose No key for a server that needs no key, such as a local one. It shows once the endpoint field is left, not while the address is typed.
  • GET /api/targets/types adds saveable_auth_modes and not_saveable_reason to each type. They come from the checks a save runs (the class check, and the server-setting check for each auth mode with those settings turned off), so the dialog keeps no copy of the credential rules.
  • A refused save gets a typed 400 ("type": "/errors/not-saveable"), so the dialog doesn't read messages. When the only problem is an endpoint that isn't a full http(s) or ws(s) address, the request gets a plain 400 instead, since a Temporary target with that address wouldn't work either.
  • The warning and the Entra ID error use role="status" so screen readers read them out. Fluent's message bars only announce through an announcer the app doesn't render.
  • The Targets table labels each target Saved, From configuration or Temporary, and the round robin member list marks Temporary members.
  • The GUI doc says where saved targets are kept, how to remove one for now, and what happens if a startup script later registers the same name.

What stays the same. API callers that don't send persist get Temporary targets as before, and may still leave out the name. Permission checks still come first, and restore is unchanged.

Not covered.

  • Showing who saved a target, and a Mine / All filter. That's the next PR.
  • Picking a server credential, and keeping typed keys in a key vault. Until then a target that uses an API key is Temporary.
  • Saving converters, scorers and techniques, and deleting saved entries. Those come in later PRs.

Tests and Documentation

  • tests/unit/registry/test_registry_recipe_rules.py: check_saveable_class lists the auth modes a saved target of each type can use, refuses a class that can never be saved, and agrees with the check a save runs for every registered target type and auth mode.
  • tests/unit/backend/test_target_service.py: the type list says which auth modes a saved target can use and why a type can't be saved; saving a type that can't be saved is refused; an endpoint that isn't a full web address is an error to fix, not a refusal.
  • tests/unit/backend/test_api_routes.py: a refused save is a 400 with "type": "/errors/not-saveable"; other bad requests stay untyped.
  • CreateTargetDialog.test.tsx (new "CreateTargetDialog saving" block): saving by default with Entra ID or No key, also when the type details arrive late; the suggested name from the type and model until it's edited; names already listed or taken (409) held on the field; when Create as Temporary is offered and why (an API key, a typed credential, a type that can't be saved, a round robin with a Temporary member), and only after a typed refusal for those exact values; no Temporary offer on 409, 422, 403 or other errors; the warning and the Entra ID error as live regions; starting over after the dialog closes; the Entra ID error once the endpoint field is left.
  • TargetTable.test.tsx: the Saved, From configuration and Temporary labels. registryName.test.ts and errors.test.ts: the name suggestion and the typed refusal check.
  • frontend/e2e/config.spec.ts (mock): saving through the dialog sends Entra ID and persist: true. frontend/e2e/registry.spec.ts (seeded, real backend): a target saved from the dialog is still Saved after a reload.
  • Each of the 64 ways I tried to break the dialog, the labels or the saveability check fails at least one of these tests.
  • uv run --frozen --extra all python -m pytest -n 8 --dist=loadfile tests/unit/registry tests/unit/backend tests/unit/prompt_target tests/unit/models tests/unit/setup tests/unit/converter tests/unit/score: 19,599 passed, 9 skipped, 4 xfailed
  • cd frontend && npm run test:coverage: 184 suites, 4,557 tests passed
  • cd frontend && npx playwright test --project=mock e2e/config.spec.ts e2e/registry.spec.ts: 28 passed
  • cd frontend && E2E_SEEDED_MODE=true npx playwright test --project=seeded: 109 passed
  • npx tsc --noEmit, npx eslint on the changed files, and pre-commit (ruff, ty, docs checks): passed
  • Run against a real backend in a real browser, with the server using its own Entra ID credential: saved an Entra ID chat target on Azure OpenAI and a local No key target from the dialog. A target with an API key, a round robin using a Temporary target, and a target created through the API without persist came out Temporary, with the reason shown. After a backend restart the saved targets came back as Saved, the Temporary ones were gone, and a message sent through the restored Entra ID target got an answer. No key was in any saved file.
  • doc/gui/0_gui.md (Registry): saving a target, where saved targets are kept, how to remove one for now, and what happens when a startup script registers the same name. pyrit/backend/README.md: the new type fields and the typed 400.

No JupyText changes.

…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
Copilot AI balanced review requested due to automatic review settings November 7, 2026 01:12

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

The saved-entry guard misses default replace=False registrations, allowing a caught duplicate-name error to leave the saved entry in use instead of the intended component without stopping the launch.

1 open issue
What changed in this PR

This PR extends PyRIT’s saved-registry support to target creation in the GUI, building on #4026’s API persistence work. The stacked changes also include recipe storage, startup restoration, and protection against replacing saved entries.

Changes:

  • Add saveability metadata, named target persistence, and typed save-refusal responses.
  • Show target origins and support saved-target creation in the Registry workflow.
  • Expand persistence, conflict, and storage-error coverage, with isolated test storage and updated documentation.
File Description
tests/​unit/​setup/​test_targets_initializer.py Tests saved-target grouping conflicts.
tests/​unit/​setup/​test_scorer_initializer.py Tests saved-scorer tagging conflicts.
tests/​unit/​setup/​test_converter_initializer.py Tests saved-converter replacement refusal.
tests/​unit/​setup/​test_configuration_loader.py Tests recipe-source configuration.
tests/​unit/​registry/​test_registry_recipe_storage.py Covers recipe storage and parsing.
tests/​unit/​registry/​test_instance_registry.py Tests saved-entry guards and origin metadata.
tests/​unit/​registry/​test_file_document_storage.py Covers safe listing and sanitized errors.
tests/​unit/​registry/​test_attack_technique_registry.py Tests saved-technique collisions.
tests/​unit/​backend/​test_target_service.py Covers target persistence and saveability.
tests/​unit/​backend/​test_scenario_suite_routes.py Tests local storage-error sanitization.
tests/​unit/​backend/​test_scenario_suite_execution_service.py Tests saved-entry launch-conflict messages.
tests/​unit/​backend/​test_scenario_run_service.py Covers launch-script collision handling.
tests/​unit/​backend/​test_scenario_preset_routes.py Tests sanitized local storage failures.
tests/​unit/​backend/​test_runtime_lifecycle.py Covers restoration, readiness, and save admission.
tests/​unit/​backend/​test_mappers.py Tests target-origin mapping.
tests/​unit/​backend/​test_initializer_service.py Tests unavailable initializer storage responses.
tests/​unit/​backend/​test_api_routes.py Covers persistence validation and response mapping.
tests/​unit/​backend/​test_analytics_benchmark.py Verifies benchmark recipe-storage isolation.
tests/​unit/​backend/​conftest.py Isolates saved entries in backend tests.
tests/​end_to_end/​conftest.py Gives test backends isolated recipe storage.
test_support/​analytics_benchmark.py Configures benchmark-local recipe storage.
pyrit/​setup/​initializers/​tools.py Rejects saved tool-component collisions.
pyrit/​setup/​initializers/​targets.py Rejects saved auto-group collisions.
pyrit/​setup/​initializers/​scorers.py Guards saved scorers during default tagging.
pyrit/​setup/​configuration_loader.py Adds recipe-storage source configuration.
pyrit/​registry/​registry_recipe_storage.py Implements recipe persistence and parsing.
pyrit/​registry/​registry_recipe_rules.py Defines credential and destination restrictions.
pyrit/​registry/​instance_registry.py Adds saved-entry protection and origin markers.
pyrit/​registry/​file_document_storage.py Reports unreadable and ignored documents.
pyrit/​registry/​components/​attack_technique_registry.py Preserves metadata and guards saved techniques.
pyrit/​prompt_target/​websocket_copilot_target.py Declares why the target cannot be saved.
pyrit/​prompt_target/​prompt_shield_target.py Declares credential and endpoint rules.
pyrit/​prompt_target/​openai/​openai_target.py Declares key, header, and endpoint rules.
pyrit/​prompt_target/​litellm_chat_target.py Declares API-key fallback behavior.
pyrit/​prompt_target/​hugging_face/​hugging_face_chat_target.py Declares persistence restrictions.
pyrit/​prompt_target/​http_target/​http_target.py Disallows saving raw HTTP targets.
pyrit/​prompt_target/​hack_a_prompt_target.py Declares session and cookie fallbacks.
pyrit/​prompt_target/​github_copilot_target.py Declares token fallback behavior.
pyrit/​prompt_target/​common/​prompt_target.py Defines shared saveability declarations.
pyrit/​prompt_target/​azure_ml_chat_target.py Declares credential and endpoint rules.
pyrit/​prompt_target/​azure_blob_storage_target.py Declares SAS and container-address rules.
pyrit/​prompt_target/​a2a_target.py Declares destination parameters.
pyrit/​models/​catalog/​target.py Adds target-origin values.
pyrit/​models/​catalog/​registry_recipe.py Defines recipes and restoration reports.
pyrit/​models/​catalog/​__init__.py Exports new catalog models lazily.
pyrit/​backend/​services/​target_service.py Saves targets and exposes saveability metadata.
pyrit/​backend/​services/​scenario_suite_execution_service.py Surfaces saved-entry launch conflicts.
pyrit/​backend/​services/​scenario_run_service.py Detects swallowed saved-entry collisions.
pyrit/​backend/​services/​runtime_lifecycle.py Restores recipes before declaring readiness.
pyrit/​backend/​routes/​targets.py Passes persistence context and maps save errors.
pyrit/​backend/​routes/​scenarios.py Documents launch-conflict responses.
pyrit/​backend/​routes/​scenario_suites.py Sanitizes local listing failures.
pyrit/​backend/​routes/​scenario_presets.py Sanitizes local listing failures.
pyrit/​backend/​routes/​initializers.py Handles local storage failures consistently.
pyrit/​backend/​README.md Documents persistence and API behavior.
pyrit/​backend/​models/​targets.py Adds persistence validation and saveability fields.
pyrit/​backend/​mappers/​target_mappers.py Maps registry metadata to target origins.
frontend/​src/​utils/​targetOrigin.ts Defines origin display labels.
frontend/​src/​utils/​registryName.ts Generates available registry-name suggestions.
frontend/​src/​utils/​registryName.test.ts Tests name validation and suggestions.
frontend/​src/​types/​index.ts Adds persistence and saveability types.
frontend/​src/​test-utils/​targetFixtures.ts Supports target origins in fixtures.
frontend/​src/​services/​errors.ts Recognizes typed save refusals.
frontend/​src/​services/​errors.test.ts Tests save-refusal classification.
frontend/​src/​components/​Config/​TargetTable.tsx Displays target-origin badges.
frontend/​src/​components/​Config/​TargetTable.test.tsx Tests origin labels and inner targets.
frontend/​src/​components/​Config/​TargetOriginBadge.tsx Renders origin badges.
frontend/​playwright.config.ts Isolates seeded backend recipe storage.
frontend/​e2e/​registry.spec.ts Checks saved-target creation and reload display.
frontend/​e2e/​fixtures/​manual_send_backend.py Isolates offline fixture recipe storage.
frontend/​e2e/​config.spec.ts Checks persistence requests from the dialog.
doc/​gui/​0_gui.md Explains saved and temporary target workflows.
doc/​getting_started/​pyrit_conf.md Documents recipe storage and restore failures.
.pyrit_conf_example Adds recipe-storage configuration guidance.

Review effort: Balanced

Comment thread pyrit/registry/instance_registry.py Resolved

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