FEAT Wire frontend attack view to backend APIs#1371
FEAT Wire frontend attack view to backend APIs#1371romanlutz wants to merge 81 commits intoAzure:mainfrom
Conversation
| @@ -91,9 +96,3 @@ def setup_frontend() -> None: | |||
|
|
|||
| # Set up frontend at module load time (needed when running via uvicorn) | |||
| setup_frontend() | |||
There was a problem hiding this comment.
maybe this should go into lifespan?
There was a problem hiding this comment.
setup_frontend needs to happen at module load time otherwise uvicorn can't serve the routes. From what I understand, lifespan is for startup/shutdown tasks that are primarily async, not FastAPI route setup. That's why the comment is there right above 🙂 I'm happy to add to it to make this clearer if you have suggestions.
|
Can you add screenshots for the frontend |
Backend:
- Replace private CentralMemory._memory_instance access with try/except
around the public get_memory_instance() API in the lifespan handler.
Initialization:
- Extract run_initializers_async() as a public function in
pyrit.setup.initialization so initializer execution can be invoked
without redundantly re-loading env files, resetting defaults, and
re-creating the memory instance.
- FrontendCore.run_initializers_async() now calls the new function
directly instead of re-invoking initialize_pyrit_async.
- Export run_initializers_async from pyrit.setup.
Frontend:
- Extract TargetTable into its own component (TargetTable.tsx).
- Move makeStyles definitions to co-located .styles.ts files for
TargetConfig, TargetTable, and CreateTargetDialog.
- Remove redundant explicit generic in useState<string>('') calls.
- Use FluentUI Field validationMessage/validationState props for
inline field-level validation in CreateTargetDialog.
Tests:
- Update TestRunScenarioAsync patches to mock run_initializers_async
instead of initialize_pyrit_async.
c799aa4 to
7e30544
Compare
There was a problem hiding this comment.
Pull request overview
Wires the PyRIT frontend attack experience to live backend APIs, including target management, attack execution, conversation branching, and richer message rendering.
Changes:
- Added/updated backend attack + target endpoints and DTOs (including conversation summaries and target metadata).
- Refactored initialization flow to shift lifespan init into CLI and consolidate initializer execution.
- Expanded frontend UI (config/history/chat) and added extensive unit + E2E coverage.
Reviewed changes
Copilot reviewed 84 out of 88 changed files in this pull request and generated 13 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/unit/registry/test_converter_registry.py | Adds unit tests for converter registry singleton/metadata behavior. |
| tests/unit/memory/test_sqlite_memory.py | Adds tests for new conversation stats aggregation in SQLite memory. |
| tests/unit/cli/test_pyrit_backend.py | Adds tests for CLI config-file forwarding and server startup flow. |
| tests/unit/cli/test_frontend_core.py | Updates patch targets and initializer execution expectations after refactor. |
| tests/unit/backend/test_target_service.py | Updates tests for target registry naming changes. |
| tests/unit/backend/test_main.py | Updates lifespan expectations to “warn only” behavior. |
| pyrit/setup/initializers/airt_targets.py | Adds extra kwargs support and new/renamed target presets. |
| pyrit/setup/initializers/airt.py | Switches auth approach to Entra token providers and updates required vars. |
| pyrit/setup/initialization.py | Extracts run_initializers_async to separate initializer execution from memory init. |
| pyrit/setup/init.py | Exposes run_initializers_async as part of setup public API. |
| pyrit/prompt_target/openai/openai_video_target.py | Enforces single-turn conversation constraint for video target. |
| pyrit/prompt_target/openai/openai_target.py | Refactors OpenAI base target inheritance/init to align with PromptTarget. |
| pyrit/prompt_target/openai/openai_response_target.py | Includes target-specific params in identifiers (e.g., extra body params). |
| pyrit/prompt_target/openai/openai_realtime_target.py | Adjusts inheritance to keep chat-target semantics for realtime target. |
| pyrit/prompt_target/openai/openai_image_target.py | Enforces single-turn conversation constraint for image target. |
| pyrit/models/conversation_stats.py | Introduces ConversationStats aggregate model. |
| pyrit/models/attack_result.py | Adds attack_result_id onto domain AttackResult. |
| pyrit/models/init.py | Exports ConversationStats from models package. |
| pyrit/memory/sqlite_memory.py | Adds conversation stats query and refactors some filtering helpers/update behavior. |
| pyrit/memory/memory_models.py | Maps DB primary key into AttackResult.attack_result_id. |
| pyrit/memory/memory_interface.py | Adds conversation stats API and updates attack result insert/update semantics. |
| pyrit/memory/azure_sql_memory.py | Adds Azure SQL implementation for conversation stats and safer update behavior. |
| pyrit/cli/pyrit_backend.py | Refactors CLI to use FrontendCore two-step init and adds --config-file. |
| pyrit/cli/frontend_core.py | Moves deferred imports to module-level and adds run_initializers_async method. |
| pyrit/backend/services/target_service.py | Renames target id field to registry name and updates pagination cursor. |
| pyrit/backend/routes/version.py | Adds database backend info to version response payload. |
| pyrit/backend/routes/targets.py | Renames path params and docs to registry naming scheme. |
| pyrit/backend/routes/attacks.py | Expands attack routes to support conversations and changes identifiers to attack_result_id. |
| pyrit/backend/models/targets.py | DTO rename + adds supports_multiturn_chat. |
| pyrit/backend/models/attacks.py | Adds target metadata nesting, conversation endpoints, and new message request fields. |
| pyrit/backend/models/init.py | Updates exports for renamed message response DTO. |
| pyrit/backend/mappers/target_mappers.py | Maps multiturn capability and renames target id field in DTO mapping. |
| pyrit/backend/mappers/init.py | Renames exported async mapper function. |
| pyrit/backend/main.py | Removes standalone uvicorn runner and changes lifespan to warn when uninitialized. |
| frontend/src/utils/messageMapper.ts | Adds backend DTO ↔ UI Message mapping (attachments, reasoning, errors). |
| frontend/src/types/index.ts | Adds backend DTO type mirrors and expands UI message model. |
| frontend/src/services/api.ts | Adds targets/attacks/labels API clients and query serialization. |
| frontend/src/services/api.test.ts | Expands mocked API service tests for new endpoints. |
| frontend/src/components/Sidebar/Navigation.tsx | Adds navigation views (chat/history/config) and active styling. |
| frontend/src/components/Sidebar/Navigation.test.tsx | Updates navigation tests for new view switching behavior. |
| frontend/src/components/Layout/MainLayout.tsx | Shows DB info in version tooltip and wires navigation callbacks. |
| frontend/src/components/Layout/MainLayout.test.tsx | Updates layout tests for new navigation props and DB tooltip behavior. |
| frontend/src/components/Labels/LabelsBar.test.tsx | Adds unit tests for labels UI behavior and label fetching. |
| frontend/src/components/Config/TargetTable.tsx | Adds target list table UI with active-target selection controls. |
| frontend/src/components/Config/TargetTable.styles.ts | Adds styling for target table and active row highlighting. |
| frontend/src/components/Config/TargetConfig.tsx | Implements target config page with fetch/retry, refresh, and create dialog. |
| frontend/src/components/Config/TargetConfig.test.tsx | Adds tests for config page states and interactions. |
| frontend/src/components/Config/TargetConfig.styles.ts | Adds styling for config page layout and states. |
| frontend/src/components/Config/CreateTargetDialog.tsx | Adds create-target dialog and validation + submit flow. |
| frontend/src/components/Config/CreateTargetDialog.test.tsx | Adds tests for create-target dialog validation and submission. |
| frontend/src/components/Config/CreateTargetDialog.styles.ts | Adds styling for create-target dialog layout. |
| frontend/src/components/Chat/InputBox.tsx | Adds banners/locking states, ref API, and multiturn warnings for active target. |
| frontend/src/components/Chat/InputBox.test.tsx | Adds tests for new input-box behaviors (single-turn, ref attachments, banners). |
| frontend/src/components/Chat/ConversationPanel.tsx | Adds conversation list panel for attacks with promote-to-main and new conversation actions. |
| frontend/src/components/Chat/ConversationPanel.test.tsx | Adds tests for conversation panel rendering and interactions. |
| frontend/src/App.tsx | Introduces multi-view app shell, target selection, attack loading, and global labels. |
| frontend/src/App.test.tsx | Expands app tests for navigation, target selection, and opening historical attacks. |
| frontend/playwright.config.ts | Splits Playwright projects into seeded vs live modes. |
| frontend/package.json | Adds e2e scripts for seeded and live test projects. |
| frontend/eslint.config.js | Adds Node globals for Playwright e2e files. |
| frontend/e2e/config.spec.ts | Adds e2e coverage for config page and config↔chat flow. |
| frontend/e2e/api.spec.ts | Adds e2e API smoke tests (targets/attacks) and improves slow-backend handling. |
| frontend/e2e/accessibility.spec.ts | Updates a11y coverage for new navigation and config table; adjusts expected header text. |
| frontend/dev.py | Improves dev runner process management, adds detach/logs/config-file support. |
| frontend/README.md | Documents seeded vs live e2e modes. |
| .github/workflows/frontend_tests.yml | Runs seeded-only e2e in GitHub Actions. |
| .gitattributes | Adds union merge strategy for squad log/state files. |
| .devcontainer/devcontainer_setup.sh | Makes Playwright install failures non-blocking with clearer messaging. |
| from pyrit.memory.memory_models import AttackResultEntry, PromptMemoryEntry | ||
|
|
||
| return exists().where( | ||
| targeted_harm_categories_subquery = exists().where( |
There was a problem hiding this comment.
Both _get_attack_result_harm_category_condition and _get_attack_result_label_condition used to return an exists().where(...) condition, but now only assign it to a local variable (*_subquery) without returning it. That will make these helpers return None, breaking filtering logic that expects a SQLAlchemy condition. Return the constructed subquery (or revert to the inline return exists().where(...)).
| from pyrit.memory.memory_models import AttackResultEntry, PromptMemoryEntry | ||
|
|
||
| return exists().where( | ||
| labels_subquery = exists().where( |
There was a problem hiding this comment.
Both _get_attack_result_harm_category_condition and _get_attack_result_label_condition used to return an exists().where(...) condition, but now only assign it to a local variable (*_subquery) without returning it. That will make these helpers return None, breaking filtering logic that expects a SQLAlchemy condition. Return the constructed subquery (or revert to the inline return exists().where(...)).
pyrit/backend/routes/attacks.py
Outdated
| tb = traceback.format_exception(type(e), e, e.__traceback__) | ||
| # Include the root cause if chained | ||
| cause = e.__cause__ | ||
| if cause: | ||
| tb += traceback.format_exception(type(cause), cause, cause.__traceback__) | ||
| detail = "".join(tb) | ||
| raise HTTPException( | ||
| status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, | ||
| detail=f"Failed to add message: {str(e)}", | ||
| detail=detail, | ||
| ) from e |
There was a problem hiding this comment.
Returning full stack traces in the HTTP 500 detail leaks internal code structure and potentially sensitive runtime data to clients. Log the traceback server-side (optionally include request IDs), and return a generic error message to the client; if you want trace exposure for development, gate it behind a dev/debug flag.
|
|
||
|
|
||
| class OpenAITarget(PromptChatTarget): | ||
| class OpenAITarget(PromptTarget): |
There was a problem hiding this comment.
Now that OpenAI targets use multiple inheritance elsewhere (e.g., RealtimeTarget(OpenAITarget, PromptChatTarget)), calling PromptTarget.__init__ directly bypasses cooperative initialization and can break MRO-based init chaining. Prefer super().__init__(...) in OpenAITarget.__init__ so all bases in the MRO can initialize correctly.
| PromptTarget.__init__( | ||
| self, |
There was a problem hiding this comment.
Now that OpenAI targets use multiple inheritance elsewhere (e.g., RealtimeTarget(OpenAITarget, PromptChatTarget)), calling PromptTarget.__init__ directly bypasses cooperative initialization and can break MRO-based init chaining. Prefer super().__init__(...) in OpenAITarget.__init__ so all bases in the MRO can initialize correctly.
| PromptTarget.__init__( | |
| self, | |
| super().__init__( |
|
|
||
| useEffect(() => { | ||
| fetchConversations() | ||
| }, [fetchConversations, activeConversationId]) |
There was a problem hiding this comment.
Including activeConversationId in this effect dependency will refetch the full conversations list on every conversation selection, creating unnecessary API traffic and UI churn. Consider fetching on attackResultId changes (and after mutating actions like create/promote), but not on local selection changes.
| }, [fetchConversations, activeConversationId]) | |
| }, [fetchConversations]) |
| from pyrit.models import Message | ||
| from unit.mocks import get_sample_conversations |
There was a problem hiding this comment.
Several new imports are placed inside test functions. For consistency and readability, prefer moving these imports to the module level (unless there’s a specific reason to defer import side effects).
| import uuid | ||
|
|
||
| from pyrit.models import MessagePiece |
There was a problem hiding this comment.
Several new imports are placed inside test functions. For consistency and readability, prefer moving these imports to the module level (unless there’s a specific reason to defer import side effects).
| import uuid | ||
|
|
||
| from pyrit.models import MessagePiece |
There was a problem hiding this comment.
Several new imports are placed inside test functions. For consistency and readability, prefer moving these imports to the module level (unless there’s a specific reason to defer import side effects).
| import uuid | ||
|
|
||
| from pyrit.models import MessagePiece |
There was a problem hiding this comment.
Several new imports are placed inside test functions. For consistency and readability, prefer moving these imports to the module level (unless there’s a specific reason to defer import side effects).
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 84 out of 88 changed files in this pull request and generated 4 comments.
Comments suppressed due to low confidence (1)
pyrit/memory/sqlite_memory.py:1
- This function used to return the
exists().where(...)condition, but now assigns it tolabels_subqueryand never returns it. That will cause callers to appendNoneto SQLAlchemy conditions and break attack filtering by labels. Returnlabels_subquery(or inline it back into a return statement).
# Copyright (c) Microsoft Corporation.
pyrit/memory/memory_interface.py
Outdated
| from contextlib import closing | ||
|
|
||
| with closing(self.get_session()) as session: | ||
| from sqlalchemy.exc import SQLAlchemyError | ||
|
|
||
| try: | ||
| session.add_all(entries) | ||
| session.commit() | ||
| # Populate the attack_result_id back onto the domain objects so callers | ||
| # can reference the DB-assigned ID immediately after insert. | ||
| for ar, entry in zip(attack_results, entries): | ||
| ar.attack_result_id = str(entry.id) | ||
| except SQLAlchemyError: | ||
| session.rollback() | ||
| raise |
There was a problem hiding this comment.
The new inline insert logic introduces deferred/local imports (closing, SQLAlchemyError). This makes the method harder to reason about and diverges from the project’s import organization (imports at module top). Move these imports to the top of the file (or reuse already-imported equivalents) and keep the method focused on DB operations.
| test("should list targets", async ({ request }) => { | ||
| const response = await request.get("/api/targets?count=50"); | ||
|
|
||
| expect(response.ok()).toBe(true); | ||
| const data = await response.json(); | ||
| expect(data).toHaveProperty("items"); | ||
| expect(Array.isArray(data.items)).toBe(true); | ||
| }); | ||
|
|
||
| test("should create and retrieve a target", async ({ request }) => { | ||
| const createPayload = { | ||
| target_type: "OpenAIChatTarget", | ||
| params: { | ||
| endpoint: "https://e2e-test.openai.azure.com", | ||
| model_name: "gpt-4o-e2e-test", | ||
| api_key: "e2e-test-key", | ||
| }, | ||
| }; | ||
|
|
||
| const createResp = await request.post("/api/targets", { data: createPayload }); | ||
| // The endpoint may not be implemented, may require different schema, or may | ||
| // return a validation error. Skip when the backend cannot handle the request. | ||
| if (!createResp.ok()) { | ||
| test.skip(true, `POST /api/targets returned ${createResp.status()} — skipping`); | ||
| return; | ||
| } |
There was a problem hiding this comment.
The E2E API tests don’t align with the backend contract: targets list uses limit (not count), and create uses type (not target_type). As written, the create test will likely always skip, reducing the value of the suite. Update these requests to match the API schema and avoid conditional skipping for expected/steady-state flows (use deterministic mocks or dedicated seeded endpoints if needed).
| const mime = mimeField || defaultMimeForDataType(dataType) | ||
| const isBase64 = !value.startsWith('data:') && !value.startsWith('http') | ||
| const url = isBase64 ? buildDataUri(value, mime) : value | ||
| const prefix = isOriginal ? 'original_' : '' |
There was a problem hiding this comment.
The base64 detection treats any non-data: and non-http string as base64 and wraps it into a data URI. Backend values can be file paths (e.g., /tmp/x.png, C:\\...) or non-http URLs (blob:, file:, azblob:), which will become invalid data URIs. Consider a stricter base64 check (e.g., regex/length validation) and/or explicit handling for known path/scheme prefixes before deciding to build a data URI.
| <Button | ||
| className={styles.iconButton} | ||
| className={currentView === 'chat' ? styles.activeButton : styles.iconButton} | ||
| appearance="subtle" | ||
| icon={<ChatRegular />} | ||
| title="Chat" | ||
| disabled | ||
| onClick={() => onNavigate('chat')} | ||
| /> | ||
|
|
||
| <Button | ||
| className={currentView === 'history' ? styles.activeButton : styles.iconButton} | ||
| appearance="subtle" | ||
| icon={<HistoryRegular />} | ||
| title="Attack History" | ||
| onClick={() => onNavigate('history')} | ||
| /> | ||
|
|
||
| <Button | ||
| className={currentView === 'config' ? styles.activeButton : styles.iconButton} | ||
| appearance="subtle" | ||
| icon={<SettingsRegular />} | ||
| title="Configuration" | ||
| onClick={() => onNavigate('config')} | ||
| /> |
There was a problem hiding this comment.
These icon-only buttons rely on the title attribute for labeling. title is not consistently announced by screen readers and isn’t a reliable accessible name. Provide an explicit accessible label (e.g., aria-label) so the buttons have stable names for assistive tech and testing via role/name queries.
- Add run_initializers_async to pyrit.setup for programmatic initialization - Switch AIRTInitializer to Entra (Azure AD) auth, removing API key requirements - Add --config-file flag to pyrit_backend CLI - Use PyRIT configuration loader in FrontendCore and pyrit_backend - Update AIRTTargetInitializer with new target types Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add conversation_stats model and attack_result extensions - Add get_attack_results with filtering by harm categories, labels, attack type, and converter types to memory interface - Implement SQLite-specific JSON filtering for attack results - Add memory_models field for targeted_harm_categories - Add prompt_metadata support to openai image/video/response targets - Fix missing return statements in SQLite harm_category and label filters Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add attack CRUD routes with conversation management - Add message sending with target dispatch and response handling - Add attack mappers for domain-to-DTO conversion with signed blob URLs - Add attack service with video remix support and piece persistence - Expand target service and routes with registry-based target management - Add version endpoint with database info Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add attack-centric chat UI with multi-conversation support - Add conversation panel with branching and message actions - Add attack history view with filtering - Add labels bar for attack metadata - Add target configuration with create dialog - Add message mapper utilities for backend/frontend translation - Add video playback support with signed blob URLs - Add InputBox with attachment support and auto-expand - Update dev.py with --detach, logs, and process management - Add e2e tests for chat, config, and flow scenarios Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
8f1a532 to
65a4182
Compare
| db_name = None | ||
| if memory.engine.url.database: | ||
| db_name = memory.engine.url.database.split("?")[0] | ||
| database_info = f"{db_type} ({db_name})" if db_name else db_type |
There was a problem hiding this comment.
Exposing memory.engine.url.database in a public /version response can leak sensitive deployment details (e.g., server/database names, file paths). Consider returning only the backend type (or a redacted form), and/or only including database identifiers when running in a trusted/development mode.
| db_name = None | |
| if memory.engine.url.database: | |
| db_name = memory.engine.url.database.split("?")[0] | |
| database_info = f"{db_type} ({db_name})" if db_name else db_type | |
| database_info = db_type |
pyrit/backend/models/attacks.py
Outdated
| conversation_id: str = Field(..., description="Unique conversation identifier") | ||
| message_count: int = Field(0, description="Number of messages in this conversation") | ||
| last_message_preview: Optional[str] = Field(None, description="Preview of the last message") | ||
| created_at: Optional[str] = Field(None, description="ISO timestamp of the first message") |
There was a problem hiding this comment.
created_at is modeled as Optional[str] here while other timestamps in the same API use datetime (e.g., AttackSummary.created_at/updated_at, CreateConversationResponse.created_at). Using datetime consistently improves schema clarity and avoids clients guessing formats; Pydantic will serialize it as ISO 8601 anyway.
| created_at: Optional[str] = Field(None, description="ISO timestamp of the first message") | |
| created_at: Optional[datetime] = Field(None, description="ISO timestamp of the first message") |
frontend/src/services/api.test.ts
Outdated
| }), | ||
| }, | ||
| targetsApi: { | ||
| listTargets: jest.fn(async (limit = 50, cursor?: string) => { | ||
| const params: Record<string, string | number> = { limit }; | ||
| if (cursor) params.cursor = cursor; | ||
| const response = await mockApiClient.get("/targets", { params }); | ||
| return response.data; | ||
| }), |
There was a problem hiding this comment.
This test suite mocks the module under test (./api) and re-implements the production logic inside the mock, which means it won’t catch regressions in frontend/src/services/api.ts. Prefer importing the real targetsApi/attacksApi and mocking only apiClient (e.g., via jest spies or an axios mock adapter) so the tests validate the actual implementation.
| request = text_pieces[0] | ||
| messages = self._memory.get_conversation(conversation_id=request.conversation_id) | ||
|
|
||
| n_messages = len(messages) | ||
| if n_messages > 0: | ||
| raise ValueError( | ||
| "This target only supports a single turn conversation. " | ||
| f"Received: {n_messages} messages which indicates a prior turn." | ||
| ) |
There was a problem hiding this comment.
This loads the entire conversation just to check whether any prior turn exists. A cheaper approach is to query only for existence/first row (or count with a limit), which avoids pulling full histories into memory when a user accidentally reuses a conversation ID.
The assertion was environment-dependent — it passed locally because ~/.pyrit/.pyrit_conf defines initializers, but CI has no config file so _initializer_names defaults to None. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Resolve merge conflicts favoring backend-attack-api (PR Azure#1419) changes: - Use set-based deduplication for conversation IDs - Use FrontendCore in pyrit_backend CLI - Remove old video_id injection helpers (moved to video target) - Use ConversationStats-based aggregation - Fix SAS cache TTL, score DTOs, version endpoint - Add Path import to pyrit_backend.py - Fix created_at type (datetime, not str) in list_conversations Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Rename supports_multiturn_chat -> supports_multi_turn in TargetInstance - Add score_type, score_category to BackendScore; score_value is now string - Add prompt_metadata to MessagePieceRequest - Update messageMapper tests: use /api/media URLs instead of base64 values Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…w' into romanlutz/frontend_attack_view # Conflicts: # pyrit/backend/services/attack_service.py
- Add 400 bad request handling to create_related_conversation endpoint with validation for source_conversation_id belonging to the attack - Add HTTPException 500 to serve_media_async docstring - Increase filename hash truncation from 8 to 12 characters - Rename initialize_and_run to initialize_and_run_async per convention - Handle /api/media?path= URLs and existing file paths in _persist_base64_pieces_async to prevent base64 decode errors - Replace attack_result_id or '' fallbacks with RuntimeError assertions - Merge latest main Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… into romanlutz/frontend_attack_view
The test was environment-dependent - it passed locally (where ~/.pyrit/.pyrit_conf exists) but failed in CI (no config file). Mock the config path to ensure deterministic behavior regardless of environment. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Normalizes error handling across the frontend. Handles Axios errors with RFC 7807 JSON, string bodies, network errors, timeouts, and non-Axios throws. 100% test coverage with 12 test cases. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 74 out of 74 changed files in this pull request and generated 9 comments.
You can also share your feedback on Copilot code review. Take the survey.
| # Create context using FrontendCore (handles config file merging) | ||
| context = frontend_core.FrontendCore( | ||
| config_file=parsed_args.config_file, | ||
| database=parsed_args.database, | ||
| initialization_scripts=initialization_scripts, | ||
| initializers=initializer_instances, | ||
| initializer_names=parsed_args.initializers, | ||
| env_files=env_files, | ||
| log_level=parsed_args.log_level, | ||
| ) | ||
|
|
||
| # Initialize PyRIT (loads registries, sets up memory) | ||
| print("🔧 Initializing PyRIT...") | ||
| await context.initialize_async() | ||
|
|
||
| # Run initializers up-front (backend runs them once at startup, not per-scenario) | ||
| initializer_instances = None | ||
| if context._initializer_names: | ||
| print(f"Running {len(context._initializer_names)} initializer(s)...") | ||
| initializer_instances = [] | ||
| for name in context._initializer_names: | ||
| initializer_class = context.initializer_registry.get_class(name) | ||
| initializer_instances.append(initializer_class()) | ||
|
|
||
| # Re-initialize with initializers applied | ||
| await initialize_pyrit_async( | ||
| memory_db_type=context._database, | ||
| initialization_scripts=context._initialization_scripts, | ||
| initializers=initializer_instances, | ||
| env_files=context._env_files, | ||
| ) |
There was a problem hiding this comment.
initialize_and_run_async duplicates the initializer resolution/re-initialization logic that already exists in frontend_core.run_scenario_async (resolve initializer classes, instantiate, then call initialize_pyrit_async again). This increases maintenance risk and relies on context._initializer_names (private state). Consider extracting a shared FrontendCore.run_initializers_async() helper (as mentioned in the PR description) and reusing it here so backend startup and scenario execution stay consistent and changes only need to be made in one place.
| it("should add a text message to an attack", async () => { | ||
| const mockResponse = { | ||
| data: { | ||
| attack: { conversation_id: "conv-123", message_count: 2 }, | ||
| messages: { conversation_id: "conv-123", messages: [] }, | ||
| }, | ||
| }; | ||
| (apiClient.post as jest.Mock).mockResolvedValueOnce(mockResponse); | ||
|
|
||
| const result = await attacksApi.addMessage("ar-conv-123", { | ||
| role: "user", | ||
| pieces: [{ data_type: "text", original_value: "Hello" }], | ||
| send: true, | ||
| }); | ||
|
|
||
| expect(apiClient.post).toHaveBeenCalledWith( | ||
| "/attacks/ar-conv-123/messages", | ||
| { | ||
| role: "user", | ||
| pieces: [{ data_type: "text", original_value: "Hello" }], | ||
| send: true, | ||
| } | ||
| ); |
There was a problem hiding this comment.
Several attacksApi.addMessage(...) tests build requests that no longer match the backend contract: AddMessageRequest now requires target_conversation_id, and when send: true it also requires target_registry_name so the backend can remain stateless. Update these test payloads to include both fields (and any other newly-required fields) to avoid false-positive test coverage and to keep the frontend client aligned with the API.
| it("should create an attack", async () => { | ||
| const mockResponse = { | ||
| data: { | ||
| conversation_id: "conv-123", | ||
| created_at: "2026-02-15T00:00:00Z", | ||
| }, | ||
| }; | ||
| (apiClient.post as jest.Mock).mockResolvedValueOnce(mockResponse); | ||
|
|
||
| const result = await attacksApi.createAttack({ | ||
| target_registry_name: "test-target", | ||
| }); | ||
|
|
||
| expect(apiClient.post).toHaveBeenCalledWith("/attacks", { | ||
| target_registry_name: "test-target", | ||
| }); | ||
| expect(result.conversation_id).toBe("conv-123"); | ||
| }); |
There was a problem hiding this comment.
The attacksApi.createAttack test mock response omits attack_result_id, but CreateAttackResponse (and the backend) now includes it. This can mask real integration issues and may break type expectations if type-checking is enabled. Update the mocked response and assertions to include attack_result_id.
| converter_types: Optional[list[str]] = Query( | ||
| None, | ||
| description=( | ||
| "Filter by converter type names (repeatable, AND logic). Pass empty to match no-converter attacks." | ||
| ), | ||
| description="Filter by converter type names (repeatable, AND logic). " | ||
| "Omit to return all attacks regardless of converters. " | ||
| "Pass with no values to match only no-converter attacks.", | ||
| ), |
There was a problem hiding this comment.
The converter_types query param description says “Pass with no values to match only no-converter attacks”, but with FastAPI/Starlette an empty query value typically parses as [''], not []. Since the service treats [] specially (no-converter-only), consider normalizing ['']/empty strings to [] (or adding an explicit boolean like no_converters_only=true) so the documented behavior is actually achievable by clients.
| listAttacks: async (params?: { | ||
| limit?: number | ||
| cursor?: string | ||
| attack_type?: string | ||
| converter_types?: string[] | ||
| outcome?: string | ||
| labels?: string[] | ||
| min_turns?: number | ||
| max_turns?: number | ||
| }): Promise<AttackListResponse> => { |
There was a problem hiding this comment.
attacksApi.listAttacks uses a params shape with labels?: string[], but the backend route expects the query parameter name label (repeatable key:value), not labels. As written, the frontend will send ?labels=... which the backend will ignore. Rename the client param to label (or map labels → label before calling axios) to match the API.
| def test_register_instance_with_custom_name(self): | ||
| converter = MockTextConverter() | ||
|
|
||
| self.registry.register_instance(converter, name="custom_converter") | ||
|
|
||
| assert self.registry.get_instance_by_name("custom_converter") is converter | ||
|
|
||
| def test_register_instance_generates_name_from_class(self): | ||
| converter = MockTextConverter() | ||
|
|
||
| self.registry.register_instance(converter) | ||
|
|
||
| assert self.registry.get_names() == ["mock_text"] |
There was a problem hiding this comment.
ConverterRegistry.register_instance calls converter.get_identifier().unique_name when name is not provided. The MockTextConverter/MockImageConverter in this test file don’t implement get_identifier(), so these tests will fail with AttributeError. Update the mocks to implement get_identifier() (returning a ConverterIdentifier/ComponentIdentifier with supported_input_types/supported_output_types), or register with an explicit name and adjust assertions accordingly.
| /** | ||
| * Check if a backend data type represents non-text media content. | ||
| */ | ||
| function isMediaDataType(dataType: string): boolean { | ||
| return dataType.includes('image') || dataType.includes('audio') || dataType.includes('video') | ||
| } |
There was a problem hiding this comment.
isMediaDataType() only treats image/audio/video as media. This means backend pieces with converted_value_data_type === 'binary_path' (e.g., PDF/Word converters) will be treated as text and never rendered as attachments, even though MessageAttachment supports type: 'file' and dataTypeToAttachmentType() has a fallback. Consider treating binary_path as media/attachment (or a separate isAttachmentDataType that includes binary) so non-image files render correctly.
| export interface BackendScore { | ||
| score_id: string | ||
| scorer_type: string | ||
| score_type: string | ||
| score_value: string | ||
| score_category?: string | null | ||
| score_rationale?: string | null | ||
| scored_at: string | ||
| } |
There was a problem hiding this comment.
BackendScore.score_category is typed as string | null, but the backend DTO defines score_category: Optional[list[str]] (i.e., string[] | null). This mismatch can cause runtime/typing issues once score categories are returned. Update the frontend type to string[] | null to match the API contract.
|
|
||
| const created = await createResp.json(); | ||
| expect(created).toHaveProperty("target_registry_name"); | ||
| expect(created.type).toBe("OpenAIChatTarget"); |
There was a problem hiding this comment.
The create-target E2E test asserts created.type === 'OpenAIChatTarget', but the backend TargetInstance DTO uses target_type (and target_registry_name) rather than type. This assertion will always fail when the endpoint succeeds. Update the expectation to use created.target_type (and consider validating created.target_registry_name too).
| expect(created.type).toBe("OpenAIChatTarget"); | |
| expect(created).toHaveProperty("target_type"); | |
| expect(created.target_type).toBe("OpenAIChatTarget"); |
- Request interceptor: attaches X-Request-ID (UUID v4) header to every request - Response interceptor: logs failed requests with method, URL, status, and detail - Fix pre-existing api.test.ts failure: add ts-jest AST transformer for import.meta.env - Set process.env vars in setupTests.ts for Vite env compatibility - All 14 test suites (261 tests) now pass Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- RequestIdMiddleware reads client X-Request-ID or generates UUID4 - Stores on request.state.request_id for route handlers - Echoes X-Request-ID in every response header - Generic exception handler includes request_id in log messages - 4 unit tests Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
State machine: connected → degraded (1 fail) → disconnected (3 fails). Polls /api/health every 60s (10s when degraded), 5s timeout per check. Immediate check on visibilitychange. Exposes reconnectCount for data refresh. 9 unit tests covering all state transitions. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Shows error banner when disconnected, warning when degraded, success on reconnect (auto-dismisses after 5s). Renders nothing when fully connected. 5 unit tests covering all states. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Class component catches render errors in subtree. First crash shows 'Try again' (resets state). Repeated crash shows 'Reload page' as nuclear option. Logs errors to console. 5 unit tests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Shows error message with Retry button on fetch failure. Distinguishes 'no results' empty state from 'load failed' error state. Uses toApiError() for consistent error extraction. 4 new tests (21 total). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Shows error message with Retry button on fetch failure instead of empty state. Uses toApiError() for error extraction. Updated existing test + 2 new tests (15 total). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace inline error extraction with toApiError(). Differentiate error messages: network, timeout, and server errors. Preserve failed message text in input box for easy re-send. Updated tests with isAxiosError flag. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…itive extensions Prevents exfiltration of database files and other sensitive data by: - Only serving files from prompt-memory-entries/ and seed-prompt-entries/ - Blocking .db, .sqlite, .json, .yaml, .env and other sensitive extensions - Rejecting files in the results_path root directory Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
MediaWithFallback component shows 'Video/Audio failed to load' placeholder when media elements fail. ImageWithSpinner already has image error handling. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…into App Wraps the app in ErrorBoundary > ConnectionHealthProvider > FluentProvider. ConnectionBanner is rendered at the top, inside the provider chain. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The frontend connection health monitor depends on this endpoint being lightweight, auth-free, and database-free with a 5s timeout budget. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… into romanlutz/frontend_attack_view
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 85 out of 90 changed files in this pull request and generated 6 comments.
You can also share your feedback on Copilot code review. Take the survey.
| } | ||
|
|
||
| apiClient.interceptors.request.use((config) => { | ||
| config.headers['X-Request-ID'] = generateRequestId() |
There was a problem hiding this comment.
Axios request interceptor assumes config.headers is always defined and index-assignable. In Axios v1, config.headers can be undefined (or an AxiosHeaders instance), which can cause a runtime exception here. Initialize headers defensively (e.g., config.headers = config.headers ?? {}) and set the header in a way compatible with AxiosHeaders.
| config.headers['X-Request-ID'] = generateRequestId() | |
| const requestId = generateRequestId() | |
| if (!config.headers) { | |
| config.headers = { 'X-Request-ID': requestId } | |
| return config | |
| } | |
| const headersAny = config.headers as any | |
| if (typeof headersAny.set === 'function') { | |
| headersAny.set('X-Request-ID', requestId) | |
| } else { | |
| headersAny['X-Request-ID'] = requestId | |
| } |
| scorer_type: string | ||
| score_type: string | ||
| score_value: string | ||
| score_category?: string | null |
There was a problem hiding this comment.
These frontend DTO types don’t match the backend models: (1) backend score_category is Optional[list[str]] but the frontend defines it as string | null; this should be string[] | null (or string[] | undefined). (2) backend prompt_metadata is dict[str, Any] but the frontend restricts it to Record<string, string>, which will break remix-mode cases where metadata values may not be strings. Aligning these prevents type-driven runtime assumptions in mappers/components.
| score_category?: string | null | |
| score_category?: string[] | null |
| converted_value?: string | ||
| mime_type?: string | ||
| original_prompt_id?: string | ||
| prompt_metadata?: Record<string, string> |
There was a problem hiding this comment.
These frontend DTO types don’t match the backend models: (1) backend score_category is Optional[list[str]] but the frontend defines it as string | null; this should be string[] | null (or string[] | undefined). (2) backend prompt_metadata is dict[str, Any] but the frontend restricts it to Record<string, string>, which will break remix-mode cases where metadata values may not be strings. Aligning these prevents type-driven runtime assumptions in mappers/components.
| prompt_metadata?: Record<string, string> | |
| prompt_metadata?: Record<string, any> |
|
|
||
| const created = await createResp.json(); | ||
| expect(created).toHaveProperty("target_registry_name"); | ||
| expect(created.type).toBe("OpenAIChatTarget"); |
There was a problem hiding this comment.
The assertion checks created.type, but the backend DTO uses target_type (and target_registry_name). If the POST succeeds, this test will fail. Update the expectation to assert against created.target_type (or adjust to whatever the endpoint actually returns).
| expect(created.type).toBe("OpenAIChatTarget"); | |
| expect(created.target_type).toBe("OpenAIChatTarget"); |
| db_name = None | ||
| if memory.engine.url.database: | ||
| db_name = memory.engine.url.database.split("?")[0] |
There was a problem hiding this comment.
database_info may expose a full filesystem path for SQLite (or other connection details) via engine.url.database. Even though it’s in a tooltip, this is still part of a public unauthenticated endpoint and can leak host directory structure. Consider returning only a sanitized identifier (e.g., basename via Path(db_name).name), or gating the detailed path behind a dev-mode flag.
| db_name = None | |
| if memory.engine.url.database: | |
| db_name = memory.engine.url.database.split("?")[0] | |
| db_name: Optional[str] = None | |
| if memory.engine.url.database: | |
| raw_db_name = memory.engine.url.database.split("?")[0] | |
| if raw_db_name and raw_db_name not in {":memory:"}: | |
| db_name = Path(raw_db_name).name or raw_db_name | |
| else: | |
| db_name = raw_db_name |
| it("should show single-turn warning when target does not support multiturn chat", () => { | ||
| render( | ||
| <TestWrapper> | ||
| <InputBox | ||
| {...defaultProps} | ||
| activeTarget={{ | ||
| target_registry_name: "test", | ||
| target_type: "TextTarget", | ||
| supports_multi_turn: false, | ||
| }} | ||
| /> | ||
| </TestWrapper> | ||
| ); | ||
|
|
||
| expect( | ||
| screen.getByText( | ||
| /does not track conversation history/ | ||
| ) | ||
| ).toBeInTheDocument(); | ||
| }); |
There was a problem hiding this comment.
This test asserts that the Tooltip content is present in the DOM as visible text without triggering hover/focus. Fluent UI Tooltip typically renders content lazily, so this is likely to be flaky or fail depending on implementation details. Prefer asserting the presence of the warning icon/trigger (add a data-testid or aria-label), and then simulate hover/focus to assert tooltip content if needed.
- MessageList: add MediaWithFallback error tests, original media attachments test, download handler test (74% → 94.6% stmts) - ErrorBoundary: add handleReload test (93.75% → 100% stmts/funcs) - AttackHistory: add pagination navigation, filter options, empty hint tests (62.3% → 87.7% stmts) - App: add failed attack open and receive message tests (89% → 96% stmts) - ConnectionBanner: remove redundant displayStatus variable Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add target management and real attack execution to the PyRIT frontend, replacing the echo stub with live backend communication.
Backend:
CLI:
Frontend:
Tests: