Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
336fa28
feat: add ISO 42001 aligned harm definitions for AI supply chain, tra…
Mar 13, 2026
cbdc28a
feat: register ISO 42001 harm scales in LikertScalePaths enum
Mar 13, 2026
f3df706
maint: fix untyped decorator mypy error in net_utility.py
Mar 18, 2026
0a2c006
maint: fix remaining strict mypy errors in common and models
Mar 18, 2026
7eb7753
maint: fix all remaining strict mypy errors across full pyrit codebase
Mar 18, 2026
c76c8e0
maint: enable strict mypy and fix all type errors across codebase
Mar 18, 2026
d7362be
maint: remove stray yaml files accidentally included from another branch
Mar 18, 2026
429de21
Merge branch 'main' into maint/fix-mypy-type-definitions
romanlutz Mar 19, 2026
6fcada4
maint: address Copilot review comments on strict mypy PR
Mar 19, 2026
9bc3c6c
maint: fix all strict mypy errors across entire pyrit codebase
Mar 19, 2026
a229059
maint: replace assert guards with explicit if/raise for python -O safety
Mar 19, 2026
0d11e3f
Merge remote-tracking branch 'origin/main' into pr-1515-review
romanlutz Apr 15, 2026
d9948ba
fix: keep Message return type for send_prompt_async, raise EmptyRespo…
romanlutz Apr 15, 2026
555ed62
fix: address review findings across PR
romanlutz Apr 15, 2026
4808e3b
fix: resolve all 56 strict mypy errors across 21 files
romanlutz Apr 15, 2026
fa5c6e3
fix: replace asserts with RuntimeError raises in product code
romanlutz Apr 15, 2026
0a1d990
Merge remote-tracking branch 'origin/main' into pr-1515-review
romanlutz Apr 15, 2026
afa8632
fix: move CentralMemory import to top of display_response.py
romanlutz Apr 15, 2026
ba4c362
fix: preserve callable api_key in OpenAITextEmbedding
romanlutz Apr 15, 2026
0ea0d7a
fix: eliminate dead-code guards in storage_io.py
romanlutz Apr 15, 2026
db3ed0c
fix: handle empty response list for write-only targets like TextTarget
romanlutz Apr 15, 2026
b49bd4a
fix: remove unused _client property from OpenAITarget
romanlutz Apr 15, 2026
bc5180d
Merge remote-tracking branch 'origin/main' into pr-1515-review
romanlutz Apr 15, 2026
0c47165
fix: restore _client property and fix test failures
romanlutz Apr 15, 2026
dcdb7b5
Merge remote-tracking branch 'origin/main' into pr-1515-review
romanlutz Apr 15, 2026
fb30bde
fix: add mypy override for hugging_face untyped transformers calls
romanlutz Apr 15, 2026
3079f54
fix: add pragma no cover to mypy type-narrowing guards for diff coverage
romanlutz Apr 15, 2026
551140d
Merge origin/main (removing ui/rpc modules)
romanlutz Apr 15, 2026
29234bf
fix: replace pragma no cover with proper unit tests for type guards
romanlutz Apr 15, 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
7 changes: 5 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -167,12 +167,15 @@ asyncio_mode = "auto"
[tool.mypy]
plugins = []
ignore_missing_imports = true
strict = false
strict = true
follow_imports = "silent"
strict_optional = false
disable_error_code = ["empty-body"]
exclude = ["doc/code/", "pyrit/auxiliary_attacks/"]

[[tool.mypy.overrides]]
module = "pyrit.prompt_target.hugging_face.*"
disallow_untyped_calls = false

[tool.uv]
constraint-dependencies = [
"aiohttp>=3.13.4",
Expand Down
5 changes: 2 additions & 3 deletions pyrit/analytics/result_analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,8 @@ def analyze_results(attack_results: list[AttackResult]) -> dict[str, AttackStats
raise TypeError(f"Expected AttackResult, got {type(attack).__name__}: {attack!r}")

outcome = attack.outcome
attack_type = (
attack.get_attack_strategy_identifier().class_name if attack.get_attack_strategy_identifier() else "unknown"
)
_strategy_id = attack.get_attack_strategy_identifier()
attack_type = _strategy_id.class_name if _strategy_id is not None else "unknown"

if outcome == AttackOutcome.SUCCESS:
overall_counts["successes"] += 1
Expand Down
2 changes: 1 addition & 1 deletion pyrit/auth/azure_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,7 @@ def get_access_token_from_interactive_login(scope: str) -> str:
"""
try:
token_provider = get_bearer_token_provider(InteractiveBrowserCredential(), scope)
return token_provider()
return str(token_provider())
except Exception as e:
logger.error(f"Failed to obtain token for '{scope}': {e}")
raise
Expand Down
9 changes: 8 additions & 1 deletion pyrit/auth/copilot_authenticator.py
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,9 @@ async def _run_playwright_browser_automation(self) -> Optional[str]:

Returns:
Optional[str]: The bearer token if successfully retrieved, None otherwise.

Raises:
ValueError: If the username is not set.
"""
from playwright.async_api import async_playwright

Expand Down Expand Up @@ -415,11 +418,15 @@ async def response_handler(response: Any) -> None:

logger.info("Waiting for email input...")
await page.wait_for_selector("#i0116", timeout=self._elements_timeout)
if self._username is None:
raise ValueError("Username is not set")
await page.fill("#i0116", self._username)
await page.click("#idSIButton9")

logger.info("Waiting for password input...")
await page.wait_for_selector("#i0118", timeout=self._elements_timeout)
if self._password is None:
raise ValueError("Password is not set")
await page.fill("#i0118", self._password)
await page.click("#idSIButton9")

Expand Down Expand Up @@ -450,7 +457,7 @@ async def response_handler(response: Any) -> None:
else:
logger.error(f"Failed to retrieve bearer token within {self._token_capture_timeout} seconds.")

return bearer_token # type: ignore[no-any-return]
return bearer_token
except Exception as e:
logger.error("Failed to retrieve access token using Playwright.")

Expand Down
3 changes: 3 additions & 0 deletions pyrit/backend/middleware/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ def __init__(self, app: ASGIApp) -> None:
self._allowed_group_ids: set[str] = {g.strip() for g in groups_raw.split(",") if g.strip()}
self._enabled = bool(self._tenant_id and self._client_id)

self._jwks_client: PyJWKClient | None
if self._enabled:
jwks_url = f"https://login.microsoftonline.com/{self._tenant_id}/discovery/v2.0/keys"
self._jwks_client = PyJWKClient(jwks_url, cache_keys=True)
Expand Down Expand Up @@ -251,6 +252,8 @@ def _validate_token(self, token: str) -> tuple[Optional[AuthenticatedUser], dict
Tuple of (AuthenticatedUser, claims) if valid, (None, {}) if validation fails.
"""
try:
if self._jwks_client is None:
raise RuntimeError("JWKS client not initialized")
signing_key = self._jwks_client.get_signing_key_from_jwt(token)
claims = jwt.decode(
token,
Expand Down
2 changes: 2 additions & 0 deletions pyrit/backend/routes/media.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,8 @@ async def serve_media_async(
"""
try:
memory = CentralMemory.get_memory_instance()
if not memory.results_path:
raise HTTPException(status_code=500, detail="Memory results_path is not configured.")
allowed_root = os.path.realpath(memory.results_path)
except Exception as exc:
raise HTTPException(status_code=500, detail="Memory not initialized; cannot determine results path.") from exc
Expand Down
2 changes: 1 addition & 1 deletion pyrit/backend/routes/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ async def get_version_async(request: Request) -> VersionResponse:
memory = CentralMemory.get_memory_instance()
db_type = type(memory).__name__
db_name = None
if memory.engine.url.database:
if memory.engine is not None and memory.engine.url.database:
db_name = memory.engine.url.database.split("?")[0]
database_info = f"{db_type} ({db_name})" if db_name else f"{db_type} (None)"
except Exception as e:
Expand Down
8 changes: 4 additions & 4 deletions pyrit/cli/_banner.py
Original file line number Diff line number Diff line change
Expand Up @@ -568,11 +568,11 @@ def _render_line_with_segments(
result: list[str] = []
current_role: Optional[ColorRole] = None
for pos, ch in enumerate(line):
role = char_roles[pos]
if role != current_role:
color = _get_color(role, theme) if role else reset
char_role = char_roles[pos]
if char_role != current_role:
color = _get_color(char_role, theme) if char_role else reset
result.append(color)
current_role = role
current_role = char_role
result.append(ch)
result.append(reset)
return "".join(result)
Expand Down
16 changes: 8 additions & 8 deletions pyrit/cli/_cli_args.py
Original file line number Diff line number Diff line change
Expand Up @@ -482,29 +482,29 @@ def _parse_shell_arguments(*, parts: list[str], arg_specs: list[_ArgSpec]) -> di
i = 0
while i < len(parts):
token = parts[i]
spec = flag_to_spec.get(token)
matched_spec: _ArgSpec | None = flag_to_spec.get(token)

if spec is None:
if matched_spec is None:
valid = sorted(flag_to_spec.keys())
raise ValueError(f"Unknown argument: {token}. Valid arguments: {', '.join(valid)}")

i += 1

if spec.multi_value:
if matched_spec.multi_value:
values: list[Any] = []
# Collect values until the next flag (whether valid or invalid)
while i < len(parts) and not (parts[i].startswith("--") or parts[i] in flag_to_spec):
item = spec.parser(parts[i]) if spec.parser else parts[i]
item = matched_spec.parser(parts[i]) if matched_spec.parser else parts[i]
values.append(item)
i += 1
if len(values) == 0:
raise ValueError(f"{spec.flags[0]} requires at least one value")
result[spec.result_key] = values
raise ValueError(f"{matched_spec.flags[0]} requires at least one value")
result[matched_spec.result_key] = values
else:
if i >= len(parts):
raise ValueError(f"{spec.flags[0]} requires a value")
raise ValueError(f"{matched_spec.flags[0]} requires a value")
raw = parts[i]
result[spec.result_key] = spec.parser(raw) if spec.parser else raw
result[matched_spec.result_key] = matched_spec.parser(raw) if matched_spec.parser else raw
i += 1

return result
Expand Down
10 changes: 7 additions & 3 deletions pyrit/cli/frontend_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ class termcolor: # type: ignore[no-redef] # noqa: N801
"""Dummy termcolor fallback for colored printing if termcolor is not installed."""

@staticmethod
def cprint(text: str, color: str = None, attrs: list = None) -> None: # type: ignore[type-arg]
def cprint(text: str, color: str | None = None, attrs: list[Any] | None = None) -> None:
"""Print text without color."""
print(text)

Expand Down Expand Up @@ -249,12 +249,14 @@ def scenario_registry(self) -> ScenarioRegistry:

Raises:
RuntimeError: If initialize_async() has not been called.
ValueError: If the scenario registry is not initialized.
"""
if not self._initialized:
raise RuntimeError(
"FrontendCore not initialized. Call 'await context.initialize_async()' before accessing registries."
)
assert self._scenario_registry is not None
if self._scenario_registry is None:
raise ValueError("self._scenario_registry is not initialized")
return self._scenario_registry

@property
Expand All @@ -264,12 +266,14 @@ def initializer_registry(self) -> InitializerRegistry:

Raises:
RuntimeError: If initialize_async() has not been called.
ValueError: If the initializer registry is not initialized.
"""
if not self._initialized:
raise RuntimeError(
"FrontendCore not initialized. Call 'await context.initialize_async()' before accessing registries."
)
assert self._initializer_registry is not None
if self._initializer_registry is None:
raise ValueError("self._initializer_registry is not initialized")
return self._initializer_registry


Expand Down
46 changes: 40 additions & 6 deletions pyrit/cli/pyrit_shell.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
from typing import TYPE_CHECKING, Any, Optional

if TYPE_CHECKING:
import types

from pyrit.cli import frontend_core
from pyrit.models.scenario_result import ScenarioResult

Expand Down Expand Up @@ -119,16 +121,17 @@ def __init__(
new_item="PyRITShell(database=..., log_level=..., ...)",
removed_in="0.14.0",
)
self._deprecated_context = context
self._deprecated_context: frontend_core.FrontendCore | None = context
else:
self._deprecated_context = None

# Track scenario execution history: list of (command_string, ScenarioResult) tuples
self._scenario_history: list[tuple[str, ScenarioResult]] = []

# Set by the background thread after importing frontend_core.
self.context: Optional[frontend_core.FrontendCore] = None
self.default_log_level: Optional[int] = None
self._fc: types.ModuleType | None = None
self.context: frontend_core.FrontendCore | None = None
self.default_log_level: int | None = None

# Initialize PyRIT in background thread for faster startup.
self._init_thread = threading.Thread(target=self._background_init, daemon=True)
Expand Down Expand Up @@ -159,12 +162,19 @@ def _raise_init_error(self) -> None:
raise self._init_error

def _ensure_initialized(self) -> None:
"""Wait for initialization to complete if not already done."""
"""
Wait for initialization to complete if not already done.

Raises:
RuntimeError: If frontend core initialization failed or is not complete.
"""
if not self._init_complete.is_set():
print("Waiting for PyRIT initialization to complete...")
sys.stdout.flush()
self._init_complete.wait()
self._raise_init_error()
if self._fc is None or self.context is None:
raise RuntimeError("Frontend core not initialized")

def cmdloop(self, intro: Optional[str] = None) -> None:
"""Override cmdloop to play animated banner before starting the REPL."""
Expand All @@ -188,22 +198,36 @@ def cmdloop(self, intro: Optional[str] = None) -> None:
super().cmdloop(intro=self.intro)

def do_list_scenarios(self, arg: str) -> None:
"""List all available scenarios."""
"""
List all available scenarios.

Raises:
RuntimeError: If initialization has not completed.
"""
if arg.strip():
print(f"Error: list-scenarios does not accept arguments, got: {arg.strip()}")
return
self._ensure_initialized()
if self._fc is None or self.context is None:
raise RuntimeError("Frontend core not initialized")
try:
asyncio.run(self._fc.print_scenarios_list_async(context=self.context))
except Exception as e:
print(f"Error listing scenarios: {e}")

def do_list_initializers(self, arg: str) -> None:
"""List all available initializers."""
"""
List all available initializers.

Raises:
RuntimeError: If initialization has not completed.
"""
if arg.strip():
print(f"Error: list-initializers does not accept arguments, got: {arg.strip()}")
return
self._ensure_initialized()
if self._fc is None or self.context is None:
raise RuntimeError("Frontend core not initialized")
try:
asyncio.run(self._fc.print_initializers_list_async(context=self.context))
except Exception as e:
Expand All @@ -225,8 +249,13 @@ def do_list_targets(self, arg: str) -> None:
Examples:
list-targets --initializers target
list-targets --initializers target:tags=default,scorer

Raises:
RuntimeError: If initialization has not completed.
"""
self._ensure_initialized()
if self._fc is None or self.context is None:
raise RuntimeError("Frontend core not initialized")
try:
list_targets_context = self.context
if arg.strip():
Expand Down Expand Up @@ -289,8 +318,13 @@ def do_run(self, line: str) -> None:
--target is required for every run.
Initializers can be specified per-run or configured in .pyrit_conf.
Database and env-files are configured via the config file.

Raises:
RuntimeError: If initialization has not completed.
"""
self._ensure_initialized()
if self._fc is None or self.context is None:
raise RuntimeError("Frontend core not initialized")

if not line.strip():
print("Error: Specify a scenario name")
Expand Down
2 changes: 1 addition & 1 deletion pyrit/common/data_url_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ async def convert_local_image_to_data_url(image_path: str) -> str:
str: A string containing the MIME type and the base64-encoded data of the image, formatted as a data URL.
"""
ext = DataTypeSerializer.get_extension(image_path)
if ext.lower() not in AZURE_OPENAI_GPT4O_SUPPORTED_IMAGE_FORMATS:
if not ext or ext.lower() not in AZURE_OPENAI_GPT4O_SUPPORTED_IMAGE_FORMATS:
raise ValueError(
f"Unsupported image format: {ext}. Supported formats are: {AZURE_OPENAI_GPT4O_SUPPORTED_IMAGE_FORMATS}"
)
Expand Down
Loading
Loading