Skip to content

FEAT: Add ScenarioSuite registry and one-click launch - #3490

Open
hannahwestra25 wants to merge 10 commits into
microsoft:mainfrom
hannahwestra25:hannahwestra25-scenario-suite-registry
Open

hannahwestra25 wants to merge 10 commits into
microsoft:mainfrom
hannahwestra25:hannahwestra25-scenario-suite-registry

Conversation

@hannahwestra25

@hannahwestra25 hannahwestra25 commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

Description

  • Adds ScenarioSuiteRegistry, modeled on ScenarioPresetRegistry, so code-registered read-only suites coexist with editable stored suites while preserving stored precedence and actionable registered-name conflicts.
  • Exposes suite and preset origin plus nullable versions; registered definitions use None, while stored definitions retain opaque optimistic-concurrency versions.
  • Routes suite CRUD, resolution, source configuration, and startup scanning through the registry while retaining ScenarioSuiteStorage as persistence.
  • Adds a process-local ScenarioSuiteExecutionService that preflights the complete reviewed suite/preset snapshot before accepting work, then submits ordinary scenario runs in suite order through ScenarioRunService.
  • Moves reusable preset/suite launch and execution contracts into pyrit.models, shares ordinary scenario launch validation, reuses ScenarioRunState after admission, and keeps registry-backed resolution separate from backend task lifecycle.
  • Persists versioned child attribution under the stable scenario_suite_execution metadata key, with schema version 1 inside the payload, without adding parent run records or database migrations.
  • Adds launch, grouped progress, and best-effort cancellation APIs plus runtime admission/shutdown integration.
  • Makes cancellation admission-safe, keeps temporary child-status read errors nonterminal, and retains active executions at the 256-entry boundary; capacity exhaustion returns 429 instead of evicting active grouped control state.
  • Resets code-registered suites during runtime reinitialization while preserving stored documents, and keeps frontend polling recoverable after consecutive transient refresh failures.
  • Adds a launch dialog, registered/stored launch actions, ordered progress page, child-run links/backlinks, transient polling recovery, and cancellation UI.
  • Keeps suite execution failures user-safe: full exception details stay in server logs while API responses and the progress page expose stable actionable messages.
  • Preserves stored-suite editing, optimistic concurrency, advisory status projection, authorization, existing scenario-run behavior, and current suite routes.
image image image

Tests and Documentation

Added backend coverage for registry resolution, shared launch validation/state contracts, optimistic snapshots, ordered submission, admission-safe cancellation, transient status recovery, terminal-aware retention, lifecycle behavior, and safe failure responses. Added frontend coverage for launch gating, polling, cancellation, routing, backlinks, and error redaction. Updated suite configuration documentation.

Validation:

  • C:\Python314\python.exe -m pytest tests\unit\models\test_scenario_suite_execution.py tests\unit\models\test_scenario_request.py tests\unit\models\test_import_boundary.py tests\unit\registry\test_scenario_suite_resolver.py tests\unit\backend\test_scenario_preset_service.py tests\unit\backend\test_scenario_preset_routes.py tests\unit\backend\test_scenario_suite_execution_service.py tests\unit\backend\test_scenario_suite_routes.py tests\unit\backend\test_scenario_run_service.py::test_prepare_persists_internal_initial_metadata_with_run_service_fields -q -p no:cacheprovider — 219 passed.
  • C:\Python314\python.exe -m pytest tests\unit\backend\test_scenario_suite_service.py tests\unit\backend\test_runtime_lifecycle.py tests\unit\registry\test_scenario_suite_registry.py tests\unit\registry\test_scenario_preset_registry.py tests\unit\models\test_scenario_preset.py tests\unit\models\test_scenario_suite.py -q -p no:cacheprovider — 209 passed.
  • C:\Python314\python.exe -m pytest tests\unit\common\test_lazy_package_imports.py::test_all_package_initializers_are_lazy_or_exempt tests\unit\common\test_lazy_package_imports.py::test_non_exempt_public_package_uses_standard_lazy_contract -q -p no:cacheprovider — 64 passed.
  • After acd1c58bb: C:\Python314\python.exe -m pytest tests\unit\backend\test_attack_models.py tests\unit\cli\test_api_client.py tests\unit\registry\test_scenario_suite_resolver.py tests\unit\scenario\scenarios\technique\test_execute.py tests\unit\backend\test_technique_service.py tests\unit\backend\test_scenario_suite_execution_service.py tests\unit\backend\test_scenario_run_service.py tests\unit\backend\test_scenario_resume.py tests\unit\backend\test_scenario_preset_routes.py tests\unit\backend\test_scenario_configuration_resolver.py tests\unit\backend\test_runtime_lifecycle.py tests\unit\scenario\core\test_scenario_dataset_overrides.py tests\unit\models\test_scenario_catalog.py tests\unit\models\test_scenario_request.py tests\unit\models\test_scenario_suite_execution.py tests\unit\models\test_import_boundary.py tests\unit\common\test_lazy_package_imports.py tests\unit\backend\test_scenario_suite_routes.py tests\unit\backend\test_scenario_suite_service.py -q -p no:cacheprovider — 1152 passed.
  • node node_modules\jest\bin\jest.js src\hooks\useScenarioSuiteExecution.test.tsx src\components\ScenarioSuites\ScenarioSuiteLibrary.test.tsx src\components\ScenarioSuites\ScenarioSuiteExecutionPage.test.tsx src\components\ScenarioSuites\ScenarioSuiteEditor.test.tsx --runInBand — 33 passed.
  • node node_modules\typescript\bin\tsc --noEmit — passed.
  • node node_modules\eslint\bin\eslint.js src\types\index.ts src\services\api.ts src\hooks\useScenarioSuiteExecution.ts src\components\ScenarioSuites — passed.
  • node node_modules\vite\bin\vite.js build — passed.
  • C:\Python314\python.exe -m ruff check . and C:\Python314\python.exe -m ruff format --check . — passed.
  • C:\Python314\python.exe build_scripts\check_async_suffix.py, C:\Python314\python.exe build_scripts\check_no_rest_roles.py <changed Python files>, import smoke, OpenAPI generation, and git diff --check — passed.

The isolated test_scenario_registry_materializes_builtin_catalog subprocess exceeded its fixed 30-second timeout on this Windows/Python 3.14 environment; the lazy-package contract checks above passed. CI's pinned ty==0.0.89 reported invalid-attribute-override because RunScenarioRequest widened the inherited required target_name; the older cached ty==0.0.81 missed it. acd1c58bb shares only the target-independent launch fields through a private base, leaving request schemas, defaults, and validation unchanged. Cached ty==0.0.86 reproduces that CI diagnostic before the fix; after it, ty check pyrit --python C:\Python314\python.exe --extra-search-path <user site-packages> reports the same 71 environment-only diagnostics as the merge base (unresolved optional imports and similar), with 0 new. The pinned ty==0.0.89 remains unavailable locally because of the existing PyPI TLS issue, so CI reruns it. JupyText was not run because no notebooks changed.

Copilot AI added 3 commits October 23, 2026 09:01
Merge code-registered read-only suites with stored editable definitions and surface suite origin across the API and UI.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 2403a4a9-6af6-46b2-b371-46de73a018fd
Keep Fluent table-cell layout so descriptions and advisory issues determine row height instead of overflowing into an internal scrollbar.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 2403a4a9-6af6-46b2-b371-46de73a018fd
Add transient registry-backed suite execution with ordered scenario submission, optimistic definition snapshots, durable child metadata, progress polling, and cancellation.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 2403a4a9-6af6-46b2-b371-46de73a018fd
@hannahwestra25 hannahwestra25 changed the title FEAT: Add ScenarioSuite registry support FEAT: Add ScenarioSuite registry and one-click launch Oct 23, 2026
Copilot AI added 3 commits October 23, 2026 12:04
Keep full exception details in server logs while exposing only stable user-safe failure messages through suite execution responses and the UI.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 2403a4a9-6af6-46b2-b371-46de73a018fd
Keep patched ScenarioRunService helpers compatible with the internal initial_metadata keyword added for suite child attribution.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 2403a4a9-6af6-46b2-b371-46de73a018fd
Preserve admitted child runs across suite cancellation, keep transient status-read failures nonterminal, and reject capacity when only active executions remain.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 2403a4a9-6af6-46b2-b371-46de73a018fd

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.

The latest lifecycle changes address three of the earlier findings. These comments cover the remaining lifecycle and polling issues, the shared-model boundary, the metadata key, and the proposed UI flow.

ValueError: If a suite is already registered under this name.
"""
try:
self.instances.register(suite, name=suite.name, tags=tags)

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.

Can we reset the code-registered suites during live reinitialization? reset_setup_registries() does not include ScenarioSuiteRegistry, so an initializer that registers a suite succeeds at startup but fails with "already registered" when the same configuration is applied again. Removing the initializer also leaves its suites in the registry. The registered instances should follow the setup lifecycle; the stored suite documents should remain unchanged.

return
}
const previous = latestExecutionRef.current
setState({

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.

Can we keep polling with a delay after a refresh failure, or add a Retry button to the stale-data view? After two consecutive failures, this branch preserves the last execution but does not schedule another poll. ScenarioSuiteExecutionPage only exposes Retry when there is no execution, so a temporary outage leaves an active suite's progress frozen until the user reloads the page.


from pydantic import BaseModel, ConfigDict, Field

SCENARIO_SUITE_EXECUTION_METADATA_KEY = "scenario_suite_execution_v1"

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.

Let's use a stable metadata key, scenario_suite_execution, without _v1 in the name. The payload already has a version field, which is where the schema version belongs. Encoding it in both places makes readers track multiple keys for the same concept when the schema changes. Please keep the version inside the payload and update the readers and writers together.

Comment thread pyrit/backend/models/scenario_suites.py Outdated
)


class LaunchScenarioSuiteRequest(BaseModel):

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.

Can we keep the shared suite launch/execution contracts in pyrit.models, following RunScenarioRequest and ScenarioRunSummary, and keep only the HTTP-specific envelopes here? I would also separate reusable suite/preset resolution from the backend task lifecycle. The backend should own admission, scheduling, and cancellation, but resolving a suite into child requests should not require importing a backend service. Reusing the existing run states and launch-field validation would also avoid a second contract drifting from the ordinary scenario path. This matches the model and backend responsibilities in framework.md.

appearance="primary"
icon={<PlayRegular />}
disabled={!runnable || !defaultsReady}
onClick={() => setLaunching(item)}

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.

I'd like to keep the registry for defining configurations and use the Scanner for launching them. That would better match the existing UI. Can we organize the flow like this?

  1. Combine presets and suites under one Scenario presets registry page. Put Scenario Preset Groups (or Scenario Suites) at the top, with individual presets below.
  2. Add an Add to scanner yes/no switch to each preset. The tip should explain that this controls whether the preset appears as an individual scanner entry; presets used only within a suite can stay hidden from that list.
  3. Remove Launch actions from the registry pages and move launching to the Scanner.
  4. In the Scanner, show suites/groups first in alphabetical order, with an expandable list of their member scenarios, then the normal individual scanners. Separate Single and Groups tabs could also work if that layout is clearer.
  5. Keep the existing launch flow: select an item, review its configuration, and click Launch. We can allow configuration edits before launch for individual scenarios; suite-level edits do not need to be part of this change.

Small, non-blocking note: Refresh is in the opposite position from other pages. I would rather remove manual refresh across the UI eventually, so I would not spend time on that here.

Copilot AI added 4 commits October 23, 2026 15:36
Reset code-registered ScenarioSuites during live setup replacement while leaving stored suite documents intact. Continue polling active suite executions after consecutive transient refresh failures so stale progress can recover without a page reload.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 2403a4a9-6af6-46b2-b371-46de73a018fd
Move reusable suite and preset launch contracts into pyrit.models, extract registry-backed resolution from the backend coordinator, reuse ordinary scenario run validation and states, and stabilize the suite metadata key.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 2403a4a9-6af6-46b2-b371-46de73a018fd
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 2403a4a9-6af6-46b2-b371-46de73a018fd
RunScenarioRequest inherited ScenarioLaunchConfiguration and widened its
required target_name to optional, which ty reports as an invalid attribute
override. Share the target-independent launch fields through a private base
so preset and suite launches still require a target while direct scenario
runs keep it optional. Request schemas, defaults, and validation are unchanged.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 2403a4a9-6af6-46b2-b371-46de73a018fd

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.

3 participants