Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
309 commits
Select commit Hold shift + click to select a range
07a617f
Merge the opened-file tests
varunj-msft Nov 4, 2026
2b4f1cc
Merge the opened-file tests
varunj-msft Nov 4, 2026
80754a2
Read saved recipes separately from building them
varunj-msft Nov 4, 2026
7965020
Merge the separate recipe read
varunj-msft Nov 4, 2026
bd84ef0
Build restored entries on the runtime loop and read them in a worker …
varunj-msft Nov 4, 2026
1a192f7
Show every signed-in user why saved registry entries couldn't be rest…
varunj-msft Nov 4, 2026
4fb7f35
Retry restoring saved registry entries without a restart
varunj-msft Nov 4, 2026
27ef572
Drop a late retry answer when the runtime failed again between polls
varunj-msft Nov 4, 2026
fa1ae56
Keep Retry in reach and say when the failure list couldn't be loaded
varunj-msft Nov 5, 2026
1af1d00
Stop a scan whose launch scripts would replace a saved entry
varunj-msft Nov 5, 2026
17d6fe0
Report a saved-entry clash when a launch script wraps the refusal
varunj-msft Nov 5, 2026
b829bb8
Merge origin/main
varunj-msft Nov 5, 2026
a230568
Refuse saved targets that would authenticate with a key
varunj-msft Nov 5, 2026
232857e
Check where saved targets send requests
varunj-msft Nov 5, 2026
034522d
Accept only plain addresses and paths as saved target destinations
varunj-msft Nov 5, 2026
d10f3cf
Save targets created through the API
varunj-msft Nov 5, 2026
0cbed4c
Count targets that a restart doesn't bring back as Temporary
varunj-msft Nov 5, 2026
d1bdd82
List presets, suites, and initializers through the checked listing
varunj-msft Nov 5, 2026
f5ed760
Merge the shared document listing
varunj-msft Nov 5, 2026
4b55b17
Merge the shared document listing
varunj-msft Nov 5, 2026
a1866d5
Merge the shared document listing
varunj-msft Nov 5, 2026
e8dc897
Merge the shared document listing
varunj-msft Nov 5, 2026
09a98af
Merge the shared document listing
varunj-msft Nov 5, 2026
5ce5bb9
Merge the shared document listing
varunj-msft Nov 5, 2026
84ac7c2
Merge the shared document listing
varunj-msft Nov 5, 2026
76dbf9c
Report a preset, suite, or initializer folder that can't be read as u…
varunj-msft Nov 5, 2026
f823e4b
Merge the storage error mapping for unreadable folders
varunj-msft Nov 5, 2026
99da684
Merge the storage error mapping for unreadable folders
varunj-msft Nov 5, 2026
2c58145
Merge the storage error mapping for unreadable folders
varunj-msft Nov 5, 2026
5a0afa6
Merge the storage error mapping for unreadable folders
varunj-msft Nov 5, 2026
6bb9aec
Merge the storage error mapping for unreadable folders
varunj-msft Nov 5, 2026
a4790e9
Merge the storage error mapping for unreadable folders
varunj-msft Nov 5, 2026
35ed34c
Merge the storage error mapping for unreadable folders
varunj-msft Nov 5, 2026
d70837c
Test each saved target URL rule on its own
varunj-msft Nov 5, 2026
4ce3214
Describe what a saved target can use more precisely
varunj-msft Nov 5, 2026
5669748
Merge the saved target rule tests and doc
varunj-msft Nov 5, 2026
1ef0838
Describe saving targets more precisely
varunj-msft Nov 5, 2026
b3b8ac5
Test the remaining saved target URL rules on their own
varunj-msft Nov 6, 2026
8a44952
Say which saved target addresses are checked
varunj-msft Nov 6, 2026
14125fc
Merge the remaining saved target rule tests and doc
varunj-msft Nov 6, 2026
078dc72
Describe what saving a target refuses more precisely
varunj-msft Nov 6, 2026
6800d15
Merge origin/main
varunj-msft Nov 6, 2026
74839cd
Merge origin/main
varunj-msft Nov 6, 2026
ee3e64f
Merge origin/main
varunj-msft Nov 6, 2026
15e7a9e
Take out the restore retry changes merged here by mistake
varunj-msft Nov 6, 2026
368b914
Merge origin/main
varunj-msft Nov 6, 2026
fd7bd4a
Merge origin/main
varunj-msft Nov 6, 2026
844a424
Say a Temporary target is also gone after a reinitialization
varunj-msft Nov 6, 2026
ebd1042
Say which target types can be saved, and mark a refused save
varunj-msft Nov 6, 2026
d3a8d2e
Save targets from the New Target dialog and label where each comes from
varunj-msft Nov 6, 2026
7a8ca6c
Say who saved each target, and whether it was the caller
varunj-msft Nov 6, 2026
fdc0bc9
Show who saved each target and list only your own
varunj-msft Nov 6, 2026
7d9ac2f
Log why storage is unavailable, without its path or URL
varunj-msft Nov 7, 2026
7b5bfeb
Merge the storage error logging
varunj-msft Nov 7, 2026
22806d8
Merge the storage error logging
varunj-msft Nov 7, 2026
34011c9
Hide a restore message with a percent escape, which can hide a creden…
varunj-msft Nov 7, 2026
8a0113f
Merge the storage error logging and the percent-escape fix
varunj-msft Nov 7, 2026
8d51b6c
Merge the storage error logging and the percent-escape fix
varunj-msft Nov 7, 2026
6ddd960
Merge the storage error logging and the percent-escape fix
varunj-msft Nov 7, 2026
91bb36b
Merge the storage error logging and the percent-escape fix
varunj-msft Nov 7, 2026
ad7cb29
Merge the storage error logging and the percent-escape fix
varunj-msft Nov 7, 2026
8dab926
Merge the storage error logging and the percent-escape fix
varunj-msft Nov 7, 2026
dc4125c
Stop a launch whose script catches the duplicate-name error for a sav…
varunj-msft Nov 7, 2026
febc8c3
Merge the duplicate-name refusal for saved entries
varunj-msft Nov 7, 2026
303dfa8
Merge the duplicate-name refusal for saved entries
varunj-msft Nov 7, 2026
6abde9f
Merge the duplicate-name refusal for saved entries
varunj-msft Nov 7, 2026
f31ad63
Merge the duplicate-name refusal for saved entries
varunj-msft Nov 7, 2026
e03fd18
Merge origin/main
varunj-msft Nov 7, 2026
36462db
Merge origin/main
varunj-msft Nov 7, 2026
37f9522
Merge origin/main
varunj-msft Nov 7, 2026
dbd105c
Merge origin/main
varunj-msft Nov 7, 2026
07ae503
Refuse saved converters and scorers that would use this server's cred…
varunj-msft Nov 7, 2026
031dc61
Merge origin/main
varunj-msft Nov 8, 2026
7f8ab0d
Merge origin/main
varunj-msft Nov 8, 2026
19205b1
Import the saved recipe models from their package, as main now requires
varunj-msft Nov 8, 2026
0348a0b
Merge origin/main
varunj-msft Nov 8, 2026
bba3afd
Import from public packages, as main now requires
varunj-msft Nov 8, 2026
d4ccd50
Merge origin/main
varunj-msft Nov 8, 2026
ed01798
Merge origin/main
varunj-msft Nov 8, 2026
98766ec
Record a saved entry's name clash wherever a launch script can catch it
varunj-msft Nov 8, 2026
6fb1e00
Merge origin/main
varunj-msft Nov 8, 2026
405c2bd
Import from public packages, as main now requires
varunj-msft Nov 8, 2026
d35eea9
Merge origin/main
varunj-msft Nov 8, 2026
9afbd2c
Merge origin/main
varunj-msft Nov 8, 2026
23d7976
Merge origin/main
varunj-msft Nov 8, 2026
91c76d0
Import from public packages, as main now requires
varunj-msft Nov 8, 2026
4ded6e6
Merge origin/main
varunj-msft Nov 8, 2026
13bd3ab
Check a target that takes no credentials as the default auth mode
varunj-msft Nov 8, 2026
2b97d83
Merge origin/main
varunj-msft Nov 8, 2026
aaf2c15
Merge origin/main
varunj-msft Nov 8, 2026
cbb71f9
Merge origin/main
varunj-msft Nov 8, 2026
d0499fa
Filter the table test by the fixture's target type, which main change…
varunj-msft Nov 8, 2026
da9ce20
Merge origin/main
varunj-msft Nov 8, 2026
622bbb8
Report storage failures with one error and answer them with 503
varunj-msft Nov 9, 2026
1cae0b9
Merge the single storage error
varunj-msft Nov 9, 2026
eba7bf2
Escape file names once and simplify the saved recipe models
varunj-msft Nov 9, 2026
9f24124
Merge the recipe model simplification
varunj-msft Nov 9, 2026
2bbfc8f
Report restore bugs as build errors and log each failure once
varunj-msft Nov 9, 2026
3735dbc
Merge the restore logging and build error fixes
varunj-msft Nov 9, 2026
ca7563d
Stay ready when a saved entry can't be restored, and hold its name
varunj-msft Nov 9, 2026
a9dfc86
Merge the stay-ready restore
varunj-msft Nov 10, 2026
67416f1
Reset the registries between runtime tests, as a real startup does
varunj-msft Nov 10, 2026
e6e419e
Merge the runtime test isolation
varunj-msft Nov 10, 2026
81c0317
Stop a launch whose script takes the name of a saved entry that failed
varunj-msft Nov 10, 2026
6ee84f0
Merge origin/main
varunj-msft Nov 10, 2026
d43a7e8
Merge origin/main
varunj-msft Nov 10, 2026
0664063
Merge origin/main
varunj-msft Nov 10, 2026
3b3ac59
Merge origin/main
varunj-msft Nov 10, 2026
04fe60f
Merge origin/main
varunj-msft Nov 10, 2026
6911327
Merge origin/main
varunj-msft Nov 10, 2026
ee45962
Refuse OpenAI targets while the server sets OPENAI_CUSTOM_HEADERS
varunj-msft Nov 10, 2026
6a98d17
Merge origin/main
varunj-msft Nov 10, 2026
4a3f2e5
Use final origin names, record startup entries at registration, and t…
varunj-msft Nov 10, 2026
9f5d390
Merge the saved target origin and naming changes
varunj-msft Nov 10, 2026
bf480f5
Merge the saved target origin and naming changes
varunj-msft Nov 10, 2026
05f9bd1
Merge the saved target origin and naming changes
varunj-msft Nov 10, 2026
51d4fff
Explain that a credential refusal also covers using this server's cre…
varunj-msft Nov 10, 2026
a3d4f92
Merge the credential explanation
varunj-msft Nov 10, 2026
3c7a918
Merge the credential explanation
varunj-msft Nov 10, 2026
452ba54
Merge the credential explanation
varunj-msft Nov 10, 2026
8ff7bda
Merge the credential explanation
varunj-msft Nov 10, 2026
8614025
Use one recipe format version, keep the auth mode, and escape failure…
varunj-msft Nov 10, 2026
5a3f18b
Merge the recipe format changes
varunj-msft Nov 10, 2026
b3c7847
Build restored entries with a builder the caller supplies
varunj-msft Nov 10, 2026
af4075f
Merge the restore builder changes
varunj-msft Nov 10, 2026
061807a
Warn that writing saved entries is administrator authority, and test …
varunj-msft Nov 10, 2026
1740964
Merge the restore builder changes
varunj-msft Nov 10, 2026
01d99d7
Merge the restore builder changes
varunj-msft Nov 10, 2026
600717f
Merge the restore builder changes
varunj-msft Nov 10, 2026
095e76b
Merge the restore builder changes
varunj-msft Nov 10, 2026
0bf773d
Merge the restore builder changes
varunj-msft Nov 10, 2026
7a547aa
Say restore can't tell who wrote a saved file
varunj-msft Nov 10, 2026
f71259b
Merge the saved file wording
varunj-msft Nov 10, 2026
8902768
Merge the saved file wording
varunj-msft Nov 10, 2026
339fd15
Merge the restore builder changes
varunj-msft Nov 10, 2026
f1dcab1
Merge the saved file wording
varunj-msft Nov 10, 2026
986cc90
Test the parser's depth limit without depending on the stack size
varunj-msft Nov 10, 2026
1096925
Merge the parser depth test fix
varunj-msft Nov 10, 2026
f25b520
Merge the parser depth test fix
varunj-msft Nov 10, 2026
1845b64
Merge the parser depth test fix
varunj-msft Nov 10, 2026
bd2224b
Merge the parser depth test fix
varunj-msft Nov 10, 2026
b454e75
Merge the parser depth test fix
varunj-msft Nov 10, 2026
83a0bca
Merge the parser depth test fix
varunj-msft Nov 10, 2026
bd03690
Merge the parser depth test fix
varunj-msft Nov 10, 2026
2e9cbb0
Merge the parser depth test fix
varunj-msft Nov 10, 2026
fb90af8
Read saved entries in a worker thread when restore isn't given what w…
varunj-msft Nov 10, 2026
c44d2ce
Merge the worker-thread read
varunj-msft Nov 10, 2026
abd116a
Stay not ready when a saved entry can't be restored, and let apply retry
varunj-msft Nov 10, 2026
0bfa6cc
Drop the refusal of held names from launches
varunj-msft Nov 10, 2026
c62118f
Merge the not-ready restore
varunj-msft Nov 10, 2026
c0aa46d
Merge the not-ready restore
varunj-msft Nov 10, 2026
f59306e
Read saved recipes off the event loop by default, and harden URL reda…
richlundeen Nov 10, 2026
7d90d68
Merge the not-ready restore
varunj-msft Nov 10, 2026
0f0c53d
Treat a class a launch script puts in place of another as temporary
varunj-msft Nov 10, 2026
26e1d0e
Merge the not-ready restore
varunj-msft Nov 10, 2026
2ea542b
Merge the not-ready restore
varunj-msft Nov 10, 2026
9fca651
Merge the not-ready restore
varunj-msft Nov 10, 2026
f1ff143
Keep an "@" in a URL's path when redacting URLs in text
richlundeen Nov 10, 2026
009e761
Merge the restore read and redaction fixes
varunj-msft Nov 10, 2026
21f22a4
Merge the restore read and redaction fixes
varunj-msft Nov 10, 2026
e9103ff
Merge the restore read and redaction fixes
varunj-msft Nov 10, 2026
57b069a
Merge the restore read and redaction fixes
varunj-msft Nov 10, 2026
7c88228
Merge the restore read and redaction fixes
varunj-msft Nov 10, 2026
616a7df
Merge the restore read and redaction fixes
varunj-msft Nov 10, 2026
fede632
Merge the restore read and redaction fixes
varunj-msft Nov 10, 2026
cc4ae4d
Merge main
varunj-msft Nov 10, 2026
9f4c71e
Merge main
varunj-msft Nov 10, 2026
5121026
Merge main
varunj-msft Nov 10, 2026
d15f7fd
Merge main
varunj-msft Nov 11, 2026
84ad3fe
Merge main
varunj-msft Nov 11, 2026
ddfe03c
Merge main
varunj-msft Nov 11, 2026
7104a25
Merge main
varunj-msft Nov 11, 2026
be0b885
Use the dialog's save button in the new tests from main
varunj-msft Nov 11, 2026
7b08ca0
Press the hidden-targets button in the saved-by test
varunj-msft Nov 11, 2026
6c8d33a
Merge the dialog test fixes
varunj-msft Nov 11, 2026
cabb56f
Merge the dialog test fixes
varunj-msft Nov 11, 2026
187191c
Answer suite readiness storage failures through the shared storage ha…
varunj-msft Nov 11, 2026
ee8162c
Give the HTTPTarget save test the connection field a create now needs
varunj-msft Nov 11, 2026
bb2a620
Merge the readiness and save test fixes
varunj-msft Nov 11, 2026
db7451a
Merge the readiness and save test fixes
varunj-msft Nov 11, 2026
b200c92
Merge the readiness and save test fixes
varunj-msft Nov 11, 2026
4c42a6b
Merge the readiness and save test fixes
varunj-msft Nov 11, 2026
43096e8
Merge the readiness and save test fixes
varunj-msft Nov 11, 2026
22b057c
Merge the readiness and save test fixes
varunj-msft Nov 11, 2026
374c620
Merge main
varunj-msft Nov 11, 2026
7ac059c
Answer suite readiness storage failures through the shared storage ha…
varunj-msft Nov 11, 2026
e08b67a
Merge main
varunj-msft Nov 11, 2026
7fa4ca4
Expect storage errors from main's version-checked delete tests the wa…
varunj-msft Nov 11, 2026
9bf5886
Merge main
varunj-msft Nov 11, 2026
626d2d4
Merge main
varunj-msft Nov 11, 2026
4a86017
Merge main
varunj-msft Nov 11, 2026
9568edb
Merge main
varunj-msft Nov 11, 2026
54e4374
Merge main
varunj-msft Nov 11, 2026
0d97131
Merge main
varunj-msft Nov 11, 2026
2849ae8
Merge main
varunj-msft Nov 11, 2026
e34258c
Fit the explicit-target E2E from main to the dialog's save button and…
varunj-msft Nov 11, 2026
bf91430
Merge the explicit-target E2E fix
varunj-msft Nov 11, 2026
a9dccb3
Merge the explicit-target E2E fix
varunj-msft Nov 11, 2026
c506195
Merge main
varunj-msft Nov 11, 2026
1ba526f
Merge main
varunj-msft Nov 11, 2026
9cdf7e7
Merge main
varunj-msft Nov 11, 2026
40d36ed
Merge main
varunj-msft Nov 11, 2026
6a52054
Merge main
varunj-msft Nov 11, 2026
23a7ec1
Merge main
varunj-msft Nov 11, 2026
70fbc3d
Merge main
varunj-msft Nov 11, 2026
a1e79d1
Merge main
varunj-msft Nov 11, 2026
a5d42d1
Merge main
varunj-msft Nov 11, 2026
2402df2
Merge main
varunj-msft Nov 11, 2026
8c517d0
Use the dialog's save button in main's endpoint tests
varunj-msft Nov 11, 2026
1137977
Merge the endpoint test fix
varunj-msft Nov 11, 2026
aaefa8f
Merge the endpoint test fix
varunj-msft Nov 11, 2026
0e7a21f
Exempt the storage error handler from the keyword-only check
varunj-msft Nov 11, 2026
891e8d6
Make get_target's parameters keyword-only
varunj-msft Nov 11, 2026
c35fa01
Merge keyword-only fixes
varunj-msft Nov 11, 2026
953dcce
Merge keyword-only fixes
varunj-msft Nov 11, 2026
d6079a1
Merge keyword-only fixes
varunj-msft Nov 11, 2026
21556ed
Merge keyword-only fixes
varunj-msft Nov 11, 2026
14004e9
Merge keyword-only fixes
varunj-msft Nov 11, 2026
3abc824
Merge keyword-only fixes
varunj-msft Nov 11, 2026
623fd40
Merge keyword-only fixes
varunj-msft Nov 11, 2026
2d74284
Merge keyword-only fixes
varunj-msft Nov 11, 2026
1fc386a
Merge keyword-only fixes
varunj-msft Nov 11, 2026
bf97225
Log every problem with a saved file that isn't valid
varunj-msft Nov 11, 2026
5e6faed
Merge the full schema-validation log
varunj-msft Nov 11, 2026
7625f89
Merge the full schema-validation log
varunj-msft Nov 11, 2026
8dbd4c2
Merge the full schema-validation log
varunj-msft Nov 11, 2026
5c7223a
Merge the full schema-validation log
varunj-msft Nov 11, 2026
8676ee5
Merge the full schema-validation log
varunj-msft Nov 11, 2026
74e1310
Merge the full schema-validation log
varunj-msft Nov 11, 2026
35199cc
Leave the error behind a storage failure out of the logs
varunj-msft Nov 12, 2026
efcdfa9
Test a blob upload that fails with a transport or authentication error
varunj-msft Nov 12, 2026
0fdebdc
Merge the storage log and restore report fixes
varunj-msft Nov 12, 2026
c413994
Merge the storage log and restore report fixes
varunj-msft Nov 12, 2026
c057e30
Merge the storage log and restore report fixes
varunj-msft Nov 12, 2026
dc97ec7
Merge the storage log and restore report fixes
varunj-msft Nov 12, 2026
a9478df
Merge the storage log and restore report fixes
varunj-msft Nov 12, 2026
dc0c058
Merge the storage log and restore report fixes
varunj-msft Nov 12, 2026
8f27c91
Merge the storage log and restore report fixes
varunj-msft Nov 12, 2026
4f2208d
Merge the storage log and restore report fixes
varunj-msft Nov 12, 2026
d22d466
Merge the storage log and restore report fixes
varunj-msft Nov 12, 2026
c9b795a
Merge main
varunj-msft Nov 12, 2026
38c981c
Merge main
varunj-msft Nov 12, 2026
eb71448
Merge main
varunj-msft Nov 12, 2026
1be067e
Merge main
varunj-msft Nov 12, 2026
162fc2a
Merge main
varunj-msft Nov 12, 2026
6c0c3dd
Merge main
varunj-msft Nov 12, 2026
3d0950d
Merge main
varunj-msft Nov 12, 2026
8b59c5d
Merge main
varunj-msft Nov 12, 2026
d1504ec
Merge main
varunj-msft Nov 12, 2026
457563c
Merge main
varunj-msft Nov 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .pyrit_conf_example
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,21 @@ allow_custom_initializers: false
# as scenario_presets_source. Defaults to ~/.pyrit/scenario_suites.
# scenario_suites_source: https://account.blob.core.windows.net/container/scenario_suites

# Optional storage for saved targets, converters, scorers, and techniques, so they
# come back when the backend restarts. Each kind has its own folder under the source.
# This supports the same local directory and Azure Blob sources as
# scenario_presets_source. If any saved entry can't be restored, the backend stays
# not ready and lists the failures in its runtime status. Fix or delete the listed
# files, then apply the configuration or restart the backend. Defaults to
# ~/.pyrit/registry_recipes.
#
# ⚠️ WARNING: The backend rebuilds every saved entry in this source as an
# administrator, at startup and live reinitialization. Anyone who can write to this
# source can make the backend run a model's repository code or use its identity.
# Restrict write access to trusted administrators, and use a dedicated folder or
# blob prefix.
# registry_recipes_source: https://account.blob.core.windows.net/container/registry_recipes

# Upload Folder for API-Created Targets
# -------------------------------------
# Folder that targets created through the REST API may upload local files from
Expand Down
14 changes: 14 additions & 0 deletions doc/getting_started/pyrit_conf.md
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,20 @@ Suites store ordered references to scenario presets and resolve each preset's cu

A SAS query string may be included; otherwise, PyRIT uses `DefaultAzureCredential`. The default is `~/.pyrit/scenario_suites`. Local writes use the same hidden lock-file and atomic replacement behavior as scenario presets. Startup scans the configured source so malformed suite documents are reported immediately without preventing the backend from starting.

### `registry_recipes_source`

Stores saved targets, converters, scorers, and techniques in a local directory or Azure Blob container, so they come back when the backend restarts. Targets created through `POST /api/targets` with `"persist": true` are saved here. An Azure URI may include a blob-name prefix:

```yaml
registry_recipes_source: https://account.blob.core.windows.net/pyrit-storage/registry-recipes
```

Each kind has its own folder under the source (`targets/`, `converters/`, `scorers/`, `techniques/`), with one JSON file per saved entry named after the entry. A file holds the create request the entry was made with, and restore builds the entry through the API's create for its kind. A file never holds a credential: restore refuses a file that stores a value for a parameter its class marks as a credential, such as `api_key`. A saved target also can't use a key from this server: it signs in with Microsoft Entra ID (`"auth_mode": "identity"`), which uses this server's Azure identity for everyone using the app, or needs no key. Targets from the server's configuration that it uses, such as a round-robin's members, keep their own sign-in. So restore refuses a target file that would read an empty key from an environment variable or leave an address to this server's settings. The addresses each target type declares (such as `endpoint`, `container_url` for Blob storage, or an A2A agent card path) can't hold a user name, password, SAS token, fragment, path parameters (`;`, `&`, `=`), or a query other than `api-version`, and a path can't have a query at all. An address the server would otherwise fill in must be a full http(s) or ws(s) address. In identity mode, an OpenAI target file sets `"headers": "{}"` in its `params` so that the server's `OPENAI_ADDITIONAL_REQUEST_HEADERS` aren't added. The OpenAI SDK adds the headers in `OPENAI_CUSTOM_HEADERS` to every request, so while the server sets it, OpenAI targets can't be saved and saved ones aren't restored. The SDK's `OPENAI_ORG_ID` and `OPENAI_PROJECT_ID` still apply to every OpenAI target on this server. GitHub Copilot, Hugging Face, HackAPrompt, LiteLLM, HTTP, and Copilot WebSocket targets can't be saved yet. Converters and scorers follow the same rules. The Azure Speech converters, the Azure Content Safety scorer, and the audio scorers can't be saved yet, because they sign in with this server's key or Azure identity, and neither can the local refusal classifier scorer, whose model can come from this server. A SAS query string may be included in the source; otherwise, PyRIT uses `DefaultAzureCredential`. The default is `~/.pyrit/registry_recipes`.

Restore can't tell who wrote a saved file, even one that names the user who saved it, so the backend rebuilds every entry in this source as an administrator, at startup and after a live apply. A file here can, for example, make the backend run a model's repository code, or use the server's identity on a storage container that the file names. Write access to this source is administrator authority on the backend: grant it only to principals you already trust as backend administrators, and use a dedicated directory or blob prefix that less-trusted users and services cannot write to.

The backend restores saved entries at startup and after a live apply, once the configured initializers have registered theirs. If any saved entry can't be restored, the backend doesn't become ready (state `failed`, outcome `restore-failed`) and lists each failure with its reason in `GET /api/config/runtime`, so a scan can't quietly run without an entry it expects. Until then, only the configuration and status routes answer; the rest return 503. Fix or delete the listed files, or, when a folder couldn't be listed, check that the backend can read this source. Then apply the configuration, if `enable_live_reinitialization` is on, or restart the backend. Files whose names aren't valid entry names (up to 64 lowercase letters, digits, and underscores, starting with a letter) are skipped without blocking startup; administrators can see them under `restore_ignored` in `GET /api/config/runtime`.

### `initialization_scripts`

Local paths to custom Python scripts containing `PyRITInitializer` subclasses. Paths can be absolute or relative to the current working directory.
Expand Down
27 changes: 25 additions & 2 deletions doc/gui/0_gui.md
Original file line number Diff line number Diff line change
Expand Up @@ -209,8 +209,31 @@ Editing a preset preserves unchanged parameter values exactly, including commas
and whitespace inside list entries, empty lists, duplicates, and their order.
Editing a list field uses the comma-separated format shown in the editor.

Targets, converters, and techniques added only to the live registry must be
recreated after a backend restart or reinitialization. Use
**New Target** saves the target, so it comes back after the backend restarts or
is reinitialized. Give it a registry name of lowercase letters, digits, and
underscores that starts with a letter; the dialog suggests one from the target
type and model. Saved targets are kept in `registry_recipes_source`
(`~/.pyrit/registry_recipes` unless set in your `.pyrit_conf`). To remove a saved
target, delete its file (`targets/<name>.json`) there, and the file of any saved
round robin that uses it, then restart the backend. If a startup initializer later
registers a target under a saved target's name, the saved one isn't restored and
the backend isn't ready until that file is renamed or removed and the backend
restarted.

Saved targets can't hold credentials yet, so choose Identity-based or No key
authentication when the target supports it. A target that needs an API key or
another credential value, a type that can't be saved, or a round robin that uses
a Temporary target is created as **Temporary** instead, and the dialog says why.
A Temporary target is gone after the backend restarts or is reinitialized.

The Targets table labels each target **Saved**, **From configuration**
(registered by an initializer at startup), or **Temporary**. A target saved while
signed in also says who saved it (**Saved by you** for your own). Once you have
saved one, check **Only targets I saved** to list just yours. Anyone can still
use every target.

Converters and techniques added only to the live registry must be recreated
after a backend restart or reinitialization. Use
[initializers](../code/setup/0_setup.md) for repeatable component setup.

Use **Auto-detect capabilities** to select targets and run live checks. Results
Expand Down
9 changes: 9 additions & 0 deletions frontend/e2e/_targets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ export interface FlatTarget {
capabilities?: unknown;
target_specific_params?: unknown;
inner_targets?: FlatTarget[] | null;
origin?: "saved" | "configuration" | "temporary" | null;
saved_by?: string | null;
saved_by_me?: boolean;
}

interface TargetInstanceMock {
Expand All @@ -27,6 +30,9 @@ interface TargetInstanceMock {
capabilities: unknown;
target_specific_params: unknown;
inner_targets: TargetInstanceMock[] | null;
origin?: "saved" | "configuration" | "temporary" | null;
saved_by?: string | null;
saved_by_me?: boolean;
}

export function makeTarget(flat: FlatTarget): TargetInstanceMock {
Expand All @@ -41,5 +47,8 @@ export function makeTarget(flat: FlatTarget): TargetInstanceMock {
capabilities: flat.capabilities ?? null,
target_specific_params: flat.target_specific_params ?? null,
inner_targets: flat.inner_targets ? flat.inner_targets.map(makeTarget) : null,
...(flat.origin !== undefined ? { origin: flat.origin } : {}),
...(flat.saved_by !== undefined ? { saved_by: flat.saved_by } : {}),
...(flat.saved_by_me !== undefined ? { saved_by_me: flat.saved_by_me } : {}),
};
}
66 changes: 61 additions & 5 deletions frontend/e2e/config.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,7 @@ test.describe("Target Registry Page", () => {

// Dialog should open
await expect(page.getByText("Create New Target")).toBeVisible();
await expect(page.getByText("Create Target")).toBeVisible();
await expect(page.getByRole("button", { name: "Save Target" })).toBeVisible();
});

test("should update targets after a visible minute without a Refresh control", async ({ page }) => {
Expand Down Expand Up @@ -343,6 +343,34 @@ test.describe("Target Registry Page", () => {
await expect(reset).toBeFocused();
await expect(reset).toHaveAttribute("aria-disabled", "true");
});

test("should say who saved each target and list only yours on request", async ({ page }) => {
await page.route(/\/api\/targets(?:\?.*)?$/, async (route) => {
await route.fulfill(
mockTargetsList([
{ target_registry_name: "mine_chat", target_type: "OpenAIChatTarget", model_name: "gpt-4o", origin: "saved",
saved_by: "Ada", saved_by_me: true },
{ target_registry_name: "bobs_chat", target_type: "OpenAIChatTarget", model_name: "gpt-4o-mini",
origin: "saved", saved_by: "Bob" },
{ target_registry_name: "startup_chat", target_type: "OpenAIChatTarget", model_name: "gpt-4.1",
origin: "configuration" },
])
);
});
await goToTargets(page);

await expect(page.getByTestId("target-row-mine_chat").getByText("Saved by you", { exact: true })).toBeVisible();
await expect(page.getByTestId("target-row-bobs_chat").getByText("Saved by Bob", { exact: true })).toBeVisible();
await page.getByRole("checkbox", { name: "Only targets I saved (1)" }).check();
await expect(page.getByTestId("target-row-bobs_chat")).toHaveCount(0);
await expect(page.getByTestId("target-row-startup_chat")).toHaveCount(0);
await expect(page.getByTestId("target-row-mine_chat")).toBeVisible();

// The choice isn't kept, so a fresh load lists everyone's targets again.
await goToTargets(page);
await expect(page.getByTestId("target-row-bobs_chat")).toBeVisible();
await expect(page.getByRole("checkbox", { name: "Only targets I saved (1)" })).not.toBeChecked();
});
});

test.describe("Create Target Dialog", () => {
Expand Down Expand Up @@ -406,12 +434,14 @@ test.describe("Create Target Dialog", () => {
await expect(selectedDetails).toContainText("Supported authentication: API key");
});

test("should create a target through the dialog", async ({ page }) => {
test("should save a target through the dialog", async ({ page }) => {
let createdTarget: FlatTarget | null = null;
let createBody: Record<string, unknown> | null = null;

await page.route(/\/api\/targets/, async (route) => {
if (route.request().method() === "POST") {
const body = JSON.parse(route.request().postData() ?? "{}");
createBody = body;
createdTarget = {
target_registry_name: "new-target-1",
target_type: body.type,
Expand All @@ -429,6 +459,27 @@ test.describe("Create Target Dialog", () => {
await route.fulfill(mockTargetsList(items));
}
});
// Registered last, so it answers the type list instead of the route above.
await page.route(/\/api\/targets\/types(?:\?.*)?$/, async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({
items: [
{
target_type: "OpenAIChatTarget",
parameters: [
{ name: "endpoint", type_name: "str", required: true, default: null },
{ name: "model_name", type_name: "str", required: true, default: null },
],
supported_auth_modes: ["api_key", "identity", "none"],
saveable_auth_modes: ["identity", "none"],
not_saveable_reason: null,
},
],
}),
});
});

await goToTargets(page);

Expand All @@ -453,11 +504,16 @@ test.describe("Create Target Dialog", () => {
// Fill model name
await dialog.getByPlaceholder("e.g. gpt-4o, my-deployment").fill("gpt-4o-test");

// Click Create Target
await dialog.getByRole("button", { name: "Create Target" }).click();
await dialog.getByRole("button", { name: "Save Target" }).click();

// Dialog should close and target should appear in the list
await expect(page.getByText("Create New Target")).not.toBeVisible({ timeout: 5_000 });
expect(createBody).toMatchObject({
name: "openai_chat_gpt_4o_test",
type: "OpenAIChatTarget",
auth_mode: "identity",
persist: true,
});
await expect(page.getByText("gpt-4o-test", { exact: true })).toBeVisible({ timeout: 10_000 });
await expect(page.getByText("OpenAIChatTarget")).toBeVisible();
});
Expand Down Expand Up @@ -496,7 +552,7 @@ test.describe("Create Target Dialog", () => {
await expect(page.getByText("Create New Target")).toBeVisible();

const dialog = page.locator('[role="dialog"]');
const createBtn = dialog.getByRole("button", { name: "Create Target" });
const createBtn = dialog.getByRole("button", { name: "Save Target" });
await expect(createBtn).toBeDisabled();

await selectTargetType(
Expand Down
30 changes: 17 additions & 13 deletions frontend/e2e/fixtures/manual_send_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"""Offline backend for manual-send browser tests, with no real configuration or providers."""

import os
import tempfile
from collections.abc import AsyncGenerator
from contextlib import asynccontextmanager
from unittest.mock import AsyncMock, patch
Expand All @@ -27,19 +28,22 @@ async def _initialize_offline_async(self: ConfigurationLoader, *, raise_on_initi

@asynccontextmanager
async def _offline_lifespan_async(application: FastAPI) -> AsyncGenerator[None, None]:
config = ConfigurationLoader(memory_db_type="in_memory", env_files=[], env_akv_ref=[])
with (
patch.dict(os.environ, {"PYRIT_AUTH_MODE": "local", "PYRIT_DEV_MODE": "true"}, clear=True),
patch.object(
ConfigurationFileService,
"_read_source_async",
new=AsyncMock(return_value="memory_db_type: in_memory\nenv_files: []\ninitializers: []\n"),
),
patch.object(ConfigurationLoader, "load_with_overrides", return_value=config),
patch.object(ConfigurationLoader, "initialize_pyrit_async", new=_initialize_offline_async),
):
async with lifespan(application):
yield
with tempfile.TemporaryDirectory() as recipes:
config = ConfigurationLoader(
memory_db_type="in_memory", env_files=[], env_akv_ref=[], registry_recipes_source=recipes
)
with (
patch.dict(os.environ, {"PYRIT_AUTH_MODE": "local", "PYRIT_DEV_MODE": "true"}, clear=True),
patch.object(
ConfigurationFileService,
"_read_source_async",
new=AsyncMock(return_value="memory_db_type: in_memory\nenv_files: []\ninitializers: []\n"),
),
patch.object(ConfigurationLoader, "load_with_overrides", return_value=config),
patch.object(ConfigurationLoader, "initialize_pyrit_async", new=_initialize_offline_async),
):
async with lifespan(application):
yield


app.router.lifespan_context = _offline_lifespan_async
30 changes: 30 additions & 0 deletions frontend/e2e/registry.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,36 @@ test("creates and selects a runtime technique with the real backend @seeded", as
expect(promptRequests).toEqual([]);
});

test("saves a target from the New Target dialog and still lists it as Saved after a reload @seeded", async ({ page, request }) => {
const name = `saved_chat_${Date.now()}`;
await page.goto("/registry/targets");
await page.getByRole("button", { name: "New Target", exact: true }).click();
const dialog = page.getByRole("dialog");
const picker = dialog.getByRole("combobox", { name: "Target Type" });
await expect(picker).toBeEnabled();
await picker.click();
await page.getByRole("option", { name: /Implementation: OpenAIChatTarget/ }).click();
await dialog.getByRole("radio", { name: /^No key/ }).check();
await dialog.getByPlaceholder("https://your-resource.openai.azure.com/").fill("http://127.0.0.1:9/v1");
await dialog.getByPlaceholder("e.g. gpt-4o, my-deployment").fill("local-test");
await dialog.getByRole("textbox", { name: /Registry Name/ }).fill(name);
await expect(dialog.getByText("This target can't be saved")).toHaveCount(0);
await dialog.getByRole("button", { name: "Save Target" }).click();
await expect(dialog).toHaveCount(0);

await expect(page.getByTestId(`target-row-${name}`).getByText("Saved", { exact: true })).toBeVisible();
const saved = await request.get(`/api/targets/${name}`, { headers: compatibilityHeaders() });
expect(saved.ok(), await saved.text()).toBe(true);
const savedTarget = await saved.json();
// Sign-in is off here, so nobody is named as the one who saved it and there is nothing to narrow the list to.
expect([savedTarget.origin, savedTarget.saved_by, savedTarget.saved_by_me]).toEqual(["saved", null, false]);
await expect(page.getByTestId(`target-row-${name}`).getByText(/^Saved by /)).toHaveCount(0);
await expect(page.getByRole("checkbox", { name: /Only targets I saved/ })).toHaveCount(0);

await page.reload();
await expect(page.getByTestId(`target-row-${name}`).getByText("Saved", { exact: true })).toBeVisible();
});

interface RegisteredConverter {
converter_id: string;
identifier: {
Expand Down
Loading
Loading