-
Notifications
You must be signed in to change notification settings - Fork 757
FEAT: REST API Scenario parameters and initializers #1724
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
rlundeen2
wants to merge
4
commits into
microsoft:main
Choose a base branch
from
rlundeen2:users/rlundeen/2026_05_11_scenario_gaps
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| # Copyright (c) Microsoft Corporation. | ||
| # Licensed under the MIT license. | ||
|
|
||
| """ | ||
| Initializer API response models. | ||
|
|
||
| Initializers configure the PyRIT environment (targets, datasets, env vars) | ||
| before scenario execution. These models represent initializer metadata. | ||
| """ | ||
|
|
||
| from typing import Optional | ||
|
|
||
| from pydantic import BaseModel, Field | ||
|
|
||
| from pyrit.backend.models.common import PaginationInfo | ||
|
|
||
|
|
||
| class InitializerParameterSummary(BaseModel): | ||
| """Summary of an initializer-declared parameter.""" | ||
|
|
||
| name: str = Field(..., description="Parameter name") | ||
| description: str = Field(..., description="Human-readable description of the parameter") | ||
| default: Optional[list[str]] = Field(None, description="Default value(s), or None if required") | ||
|
|
||
|
|
||
| class RegisteredInitializer(BaseModel): | ||
| """Summary of a registered initializer.""" | ||
|
|
||
| initializer_name: str = Field(..., description="Initializer registry name (e.g., 'target')") | ||
| initializer_type: str = Field(..., description="Initializer class name (e.g., 'TargetInitializer')") | ||
| description: str = Field("", description="Human-readable description of the initializer") | ||
| required_env_vars: list[str] = Field( | ||
| default_factory=list, description="Environment variables required by this initializer" | ||
| ) | ||
| supported_parameters: list[InitializerParameterSummary] = Field( | ||
| default_factory=list, description="Parameters accepted by this initializer" | ||
| ) | ||
|
|
||
|
|
||
| class ListRegisteredInitializersResponse(BaseModel): | ||
| """Response for listing initializers.""" | ||
|
|
||
| items: list[RegisteredInitializer] = Field(..., description="List of initializer summaries") | ||
| pagination: PaginationInfo = Field(..., description="Pagination metadata") |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,75 @@ | ||
| # Copyright (c) Microsoft Corporation. | ||
| # Licensed under the MIT license. | ||
|
|
||
| """ | ||
| Initializer API routes. | ||
|
|
||
| Provides endpoints for listing available initializers and their metadata. | ||
|
|
||
| Route structure: | ||
| /api/initializers — list all initializers | ||
| /api/initializers/{name} — get single initializer detail | ||
| """ | ||
|
|
||
| from typing import Optional | ||
|
|
||
| from fastapi import APIRouter, HTTPException, Query, status | ||
|
|
||
| from pyrit.backend.models.common import ProblemDetail | ||
| from pyrit.backend.models.initializers import ( | ||
| ListRegisteredInitializersResponse, | ||
| RegisteredInitializer, | ||
| ) | ||
| from pyrit.backend.services.initializer_service import get_initializer_service | ||
|
|
||
| router = APIRouter(prefix="/initializers", tags=["initializers"]) | ||
|
|
||
|
|
||
| @router.get( | ||
| "", | ||
| response_model=ListRegisteredInitializersResponse, | ||
| ) | ||
| async def list_initializers( | ||
| limit: int = Query(50, ge=1, le=200, description="Maximum items per page"), | ||
| cursor: Optional[str] = Query(None, description="Pagination cursor (initializer_name to start after)"), | ||
| ) -> ListRegisteredInitializersResponse: | ||
| """ | ||
| List all available initializers. | ||
|
|
||
| Returns initializer metadata including required environment variables, | ||
| supported parameters, and descriptions. | ||
|
|
||
| Returns: | ||
| ListRegisteredInitializersResponse: Paginated list of initializer summaries. | ||
| """ | ||
| service = get_initializer_service() | ||
| return await service.list_initializers_async(limit=limit, cursor=cursor) | ||
|
|
||
|
|
||
| @router.get( | ||
| "/{initializer_name}", | ||
| response_model=RegisteredInitializer, | ||
| responses={ | ||
| 404: {"model": ProblemDetail, "description": "Initializer not found"}, | ||
| }, | ||
| ) | ||
| async def get_initializer(initializer_name: str) -> RegisteredInitializer: | ||
| """ | ||
| Get details for a specific initializer. | ||
|
|
||
| Args: | ||
| initializer_name: Registry name of the initializer (e.g., 'target'). | ||
|
|
||
| Returns: | ||
| RegisteredInitializer: Full initializer metadata. | ||
| """ | ||
| service = get_initializer_service() | ||
|
|
||
| initializer = await service.get_initializer_async(initializer_name=initializer_name) | ||
| if not initializer: | ||
| raise HTTPException( | ||
| status_code=status.HTTP_404_NOT_FOUND, | ||
| detail=f"Initializer '{initializer_name}' not found", | ||
| ) | ||
|
|
||
| return initializer |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,141 @@ | ||
| # Copyright (c) Microsoft Corporation. | ||
| # Licensed under the MIT license. | ||
|
|
||
| """ | ||
| Initializer service for listing available initializers. | ||
|
|
||
| Provides read-only access to the InitializerRegistry, exposing initializer | ||
| metadata through the REST API. | ||
| """ | ||
|
|
||
| from functools import lru_cache | ||
| from typing import Optional | ||
|
|
||
| from pyrit.backend.models.common import PaginationInfo | ||
| from pyrit.backend.models.initializers import ( | ||
| InitializerParameterSummary, | ||
| ListRegisteredInitializersResponse, | ||
| RegisteredInitializer, | ||
| ) | ||
| from pyrit.registry import InitializerMetadata, InitializerRegistry | ||
|
|
||
|
|
||
| def _metadata_to_registered_initializer(metadata: InitializerMetadata) -> RegisteredInitializer: | ||
| """ | ||
| Convert an InitializerMetadata dataclass to a RegisteredInitializer Pydantic model. | ||
|
|
||
| Args: | ||
| metadata: The registry metadata for an initializer. | ||
|
|
||
| Returns: | ||
| RegisteredInitializer Pydantic model. | ||
| """ | ||
| return RegisteredInitializer( | ||
| initializer_name=metadata.registry_name, | ||
| initializer_type=metadata.class_name, | ||
| description=metadata.class_description, | ||
| required_env_vars=list(metadata.required_env_vars), | ||
| supported_parameters=[ | ||
| InitializerParameterSummary( | ||
| name=name, | ||
| description=desc, | ||
| default=default, | ||
| ) | ||
| for name, desc, default in metadata.supported_parameters | ||
| ], | ||
| ) | ||
|
|
||
|
|
||
| class InitializerService: | ||
| """ | ||
| Service for listing available initializers. | ||
|
|
||
| Uses InitializerRegistry as the source of truth for initializer metadata. | ||
| """ | ||
|
|
||
| def __init__(self) -> None: | ||
| """Initialize the initializer service.""" | ||
| self._registry = InitializerRegistry.get_registry_singleton() | ||
|
|
||
| async def list_initializers_async( | ||
| self, | ||
| *, | ||
| limit: int = 50, | ||
| cursor: Optional[str] = None, | ||
| ) -> ListRegisteredInitializersResponse: | ||
| """ | ||
| List all available initializers with pagination. | ||
|
|
||
| Args: | ||
| limit: Maximum items to return per page. | ||
| cursor: Pagination cursor (initializer_name to start after). | ||
|
|
||
| Returns: | ||
| ListRegisteredInitializersResponse with paginated initializer summaries. | ||
| """ | ||
| all_metadata = self._registry.list_metadata() | ||
| all_summaries = [_metadata_to_registered_initializer(m) for m in all_metadata] | ||
|
|
||
| page, has_more = self._paginate(items=all_summaries, cursor=cursor, limit=limit) | ||
| next_cursor = page[-1].initializer_name if has_more and page else None | ||
|
|
||
| return ListRegisteredInitializersResponse( | ||
| items=page, | ||
| pagination=PaginationInfo(limit=limit, has_more=has_more, next_cursor=next_cursor, prev_cursor=cursor), | ||
| ) | ||
|
|
||
| async def get_initializer_async(self, *, initializer_name: str) -> Optional[RegisteredInitializer]: | ||
| """ | ||
| Get a single initializer by registry name. | ||
|
|
||
| Args: | ||
| initializer_name: The registry key of the initializer (e.g., 'target'). | ||
|
|
||
| Returns: | ||
| RegisteredInitializer if found, None otherwise. | ||
| """ | ||
| all_metadata = self._registry.list_metadata() | ||
| for metadata in all_metadata: | ||
| if metadata.registry_name == initializer_name: | ||
| return _metadata_to_registered_initializer(metadata) | ||
| return None | ||
|
|
||
| @staticmethod | ||
| def _paginate( | ||
| *, | ||
| items: list[RegisteredInitializer], | ||
| cursor: Optional[str], | ||
| limit: int, | ||
| ) -> tuple[list[RegisteredInitializer], bool]: | ||
| """ | ||
| Apply cursor-based pagination. | ||
|
|
||
| Args: | ||
| items: Full list of items. | ||
| cursor: Initializer name to start after. | ||
| limit: Maximum items per page. | ||
|
|
||
| Returns: | ||
| Tuple of (paginated items, has_more flag). | ||
| """ | ||
| start_idx = 0 | ||
| if cursor: | ||
| for i, item in enumerate(items): | ||
| if item.initializer_name == cursor: | ||
| start_idx = i + 1 | ||
| break | ||
|
|
||
| page = items[start_idx : start_idx + limit] | ||
| has_more = len(items) > start_idx + limit | ||
| return page, has_more | ||
|
|
||
|
|
||
| @lru_cache(maxsize=1) | ||
| def get_initializer_service() -> InitializerService: | ||
| """ | ||
| Get the global initializer service instance. | ||
|
|
||
| Returns: | ||
| The singleton InitializerService instance. | ||
| """ | ||
| return InitializerService() | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
your coding agent seems to ignore styling rules 😆 Optional[str] should be str | None