diff --git a/pyproject.toml b/pyproject.toml index c07cd8f..0b3392e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -211,7 +211,7 @@ ignore = [ "strix/runtime/backends.py" = ["PLC0415"] # The vulnerability MD renderer is a long monolithic format function; # splitting per-section would obscure the structure without simplifying. -"strix/io/scan_artifacts.py" = ["PLR0912", "PLR0915", "PERF401"] +"strix/telemetry/scan_artifacts.py" = ["PLR0912", "PLR0915", "PERF401"] "strix/runtime/docker_client.py" = [ "TC002", # Manifest, Container imported for annotations "TC003", # uuid imported for annotation @@ -234,7 +234,7 @@ ignore = [ # resolution past where mypy needs it. ``_build_root_task`` legitimately # walks every supported target type — splitting it into per-type # helpers would add indirection without simplifying anything. -"strix/entry.py" = ["TC003", "PLR0912"] +"strix/orchestration/scan.py" = ["TC003", "PLR0912"] # Tracer carries a long event surface and a runtime ``Callable`` # annotation on ``vulnerability_found_callback``. "strix/telemetry/tracer.py" = ["TC003", "PLR0912", "PLR0915", "E501"] diff --git a/strix/interface/cli.py b/strix/interface/cli.py index e6d5906..bfc3e13 100644 --- a/strix/interface/cli.py +++ b/strix/interface/cli.py @@ -14,7 +14,7 @@ from rich.panel import Panel from rich.text import Text from strix.config import load_settings -from strix.entry import run_strix_scan +from strix.orchestration.scan import run_strix_scan from strix.runtime import session_manager from strix.telemetry.tracer import Tracer, set_global_tracer diff --git a/strix/interface/tui.py b/strix/interface/tui.py index 100ee09..512cffb 100644 --- a/strix/interface/tui.py +++ b/strix/interface/tui.py @@ -32,11 +32,11 @@ from textual.widgets import Button, Label, Static, TextArea, Tree from textual.widgets.tree import TreeNode from strix.config import load_settings -from strix.entry import run_strix_scan from strix.interface.tool_components.agent_message_renderer import AgentMessageRenderer from strix.interface.tool_components.registry import get_tool_renderer from strix.interface.tool_components.user_message_renderer import UserMessageRenderer from strix.interface.utils import build_tui_stats_text +from strix.orchestration.scan import run_strix_scan from strix.runtime import session_manager from strix.telemetry.tracer import Tracer, set_global_tracer diff --git a/strix/io/__init__.py b/strix/io/__init__.py deleted file mode 100644 index 557d8f0..0000000 --- a/strix/io/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -"""Strix I/O — disk artifact writers. - -- :class:`ScanArtifactWriter` — writes vulnerability MD/CSV plus the - final penetration-test report under ``strix_runs//``. Used by - the tracer; could be used directly by post-run tooling. -""" diff --git a/strix/llm/dedupe.py b/strix/llm/dedupe.py index d720f01..8120c13 100644 --- a/strix/llm/dedupe.py +++ b/strix/llm/dedupe.py @@ -18,7 +18,7 @@ from openai.types.responses import ResponseOutputMessage from strix.config import load_settings from strix.llm.multi_provider_setup import build_multi_provider -from strix.run_config_factory import DEFAULT_RETRY +from strix.llm.retry import DEFAULT_RETRY if TYPE_CHECKING: diff --git a/strix/llm/retry.py b/strix/llm/retry.py new file mode 100644 index 0000000..d4d9939 --- /dev/null +++ b/strix/llm/retry.py @@ -0,0 +1,30 @@ +"""Shared model-retry policy used across every Strix LLM call.""" + +from __future__ import annotations + +from agents.retry import ( + ModelRetryBackoffSettings, + ModelRetrySettings, + retry_policies, +) + + +# Retry: 5 attempts with ``min(90, 2*2^n)`` backoff. 4xx auth/validation +# errors are excluded from the retryable status list — they can't be +# fixed by retrying and should fail fast. Used by every ``RunConfig`` +# Strix builds, plus the dedupe path's one-shot LLM call outside +# ``Runner.run``. +DEFAULT_RETRY = ModelRetrySettings( + max_retries=5, + backoff=ModelRetryBackoffSettings( + initial_delay=2.0, + max_delay=90.0, + multiplier=2.0, + jitter=False, + ), + policy=retry_policies.any( + retry_policies.provider_suggested(), + retry_policies.network_error(), + retry_policies.http_status((429, 500, 502, 503, 504)), + ), +) diff --git a/strix/entry.py b/strix/orchestration/scan.py similarity index 84% rename from strix/entry.py rename to strix/orchestration/scan.py index 4f336ad..8c199e3 100644 --- a/strix/entry.py +++ b/strix/orchestration/scan.py @@ -20,21 +20,27 @@ import uuid from pathlib import Path from typing import TYPE_CHECKING, Any, Literal +from agents import RunConfig from agents.memory import SQLiteSession +from agents.model_settings import ModelSettings +from agents.sandbox import SandboxRunConfig +from openai.types.shared import Reasoning from strix.agents.factory import build_strix_agent, make_child_factory from strix.config import load_settings +from strix.llm.multi_provider_setup import build_multi_provider +from strix.llm.retry import DEFAULT_RETRY from strix.orchestration.bus import AgentMessageBus +from strix.orchestration.filter import inject_messages_filter from strix.orchestration.hooks import StrixOrchestrationHooks from strix.orchestration.run_loop import run_with_continuation -from strix.run_config_factory import ( - STRIX_DEFAULT_MAX_TURNS, - make_agent_context, - make_run_config, -) from strix.runtime import session_manager +#: Default ``max_turns`` budget passed to ``Runner.run``. +_MAX_TURNS = 300 + + if TYPE_CHECKING: from agents.result import RunResultBase @@ -160,7 +166,7 @@ async def run_strix_scan( tracer: Any | None = None, bus: AgentMessageBus | None = None, interactive: bool = False, - max_turns: int = STRIX_DEFAULT_MAX_TURNS, + max_turns: int = _MAX_TURNS, model: str | None = None, cleanup_on_exit: bool = True, ) -> RunResultBase: @@ -215,7 +221,7 @@ async def run_strix_scan( ) try: - # Lazy: ``strix.interface`` pulls cli→tui→entry which would cycle. + # Lazy: ``strix.interface`` pulls cli→tui→scan which would cycle. from strix.interface.utils import is_whitebox_scan scan_mode = str(scan_config.get("scan_mode") or "deep") @@ -245,31 +251,45 @@ async def run_strix_scan( system_prompt_context=scope_context, ) - context = make_agent_context( - bus=bus, - sandbox_session=bundle["session"], - sandbox_client=bundle["client"], - caido_client=bundle["caido_client"], - agent_id=root_id, - parent_id=None, - tracer=tracer, - model=resolved_model, - max_turns=max_turns, - is_whitebox=is_whitebox, - interactive=interactive, - diff_scope=diff_scope, - run_id=run_id, - agent_factory=agent_factory, - ) + context: dict[str, Any] = { + "bus": bus, + "sandbox_session": bundle["session"], + "sandbox_client": bundle["client"], + "caido_client": bundle["caido_client"], + "agent_id": root_id, + "parent_id": None, + "tracer": tracer, + "model": resolved_model, + "model_settings": None, + "max_turns": max_turns, + "agent_finish_called": False, + "is_whitebox": is_whitebox, + "interactive": interactive, + "diff_scope": diff_scope, + "run_id": run_id, + "agent_factory": agent_factory, + } reasoning_effort: Literal["low", "medium", "high"] | None = ( load_settings().llm.reasoning_effort ) - run_config = make_run_config( - sandbox_session=bundle["session"], - sandbox_client=bundle["client"], + model_settings = ModelSettings( + parallel_tool_calls=False, + tool_choice="required", + retry=DEFAULT_RETRY, + ) + if reasoning_effort is not None: + model_settings = model_settings.resolve( + ModelSettings(reasoning=Reasoning(effort=reasoning_effort)), + ) + run_config = RunConfig( model=resolved_model, - reasoning_effort=reasoning_effort, + model_provider=build_multi_provider(), + model_settings=model_settings, + sandbox=SandboxRunConfig(client=bundle["client"], session=bundle["session"]), + call_model_input_filter=inject_messages_filter, + tracing_disabled=False, + trace_include_sensitive_data=False, ) # Native SDK session: persists conversation history to diff --git a/strix/run_config_factory.py b/strix/run_config_factory.py deleted file mode 100644 index 88af371..0000000 --- a/strix/run_config_factory.py +++ /dev/null @@ -1,159 +0,0 @@ -"""``make_run_config`` — assemble a Strix-flavored ``RunConfig`` for ``Runner.run``. - -Every scan goes through here so defaults apply uniformly. Per-call -overrides land via ``model_settings_override``. -""" - -from __future__ import annotations - -from typing import TYPE_CHECKING, Any, Literal - -from agents import RunConfig -from agents.model_settings import ModelSettings -from agents.retry import ( - ModelRetryBackoffSettings, - ModelRetrySettings, - retry_policies, -) -from agents.sandbox import SandboxRunConfig -from openai.types.shared import Reasoning - -from strix.llm.multi_provider_setup import build_multi_provider -from strix.orchestration.filter import inject_messages_filter - - -if TYPE_CHECKING: - from agents.sandbox.session.base_sandbox_session import BaseSandboxSession - - from strix.orchestration.bus import AgentMessageBus - - -#: Default ``max_turns`` callers should pass to ``Runner.run``. -STRIX_DEFAULT_MAX_TURNS = 300 - -# Retry: 5 attempts with ``min(90, 2*2^n)`` backoff. 4xx auth/validation -# errors are excluded from the retryable status list — they can't be -# fixed by retrying and should fail fast. Public so the dedupe path -# (and any other one-shot LLM call outside ``Runner.run``) reuses the -# same policy. -DEFAULT_RETRY = ModelRetrySettings( - max_retries=5, - backoff=ModelRetryBackoffSettings( - initial_delay=2.0, - max_delay=90.0, - multiplier=2.0, - jitter=False, - ), - policy=retry_policies.any( - retry_policies.provider_suggested(), - retry_policies.network_error(), - retry_policies.http_status((429, 500, 502, 503, 504)), - ), -) - - -def make_run_config( - *, - sandbox_session: BaseSandboxSession | None, - model: str, - reasoning_effort: Literal["low", "medium", "high"] | None = None, - model_settings_override: ModelSettings | None = None, - sandbox_client: Any | None = None, -) -> RunConfig: - """Build a ``RunConfig`` with Strix defaults. - - Note: ``max_turns`` is not a ``RunConfig`` field — pass it directly - to ``Runner.run``. ``STRIX_DEFAULT_MAX_TURNS`` is the budget Strix - uses. - - Args: - sandbox_session: Live sandbox session shared by every agent in - this scan (one container per scan; see - :mod:`strix.runtime.session_manager`). ``None`` is allowed - for unit tests and dry runs. - model: Litellm model alias passed to ``MultiProvider``. Caller - resolves from :attr:`Settings.llm.model`. - reasoning_effort: ``"low" | "medium" | "high"``; routes to - ``ModelSettings.reasoning``. - model_settings_override: Optional per-run ``ModelSettings`` - merged over factory defaults. - sandbox_client: Optional pre-built sandbox client (Strix Docker - subclass). The SDK instantiates its built-in if a session is - supplied without a client. - """ - base_settings = ModelSettings( - parallel_tool_calls=False, - tool_choice="required", - retry=DEFAULT_RETRY, - ) - if reasoning_effort is not None: - base_settings = base_settings.resolve( - ModelSettings(reasoning=Reasoning(effort=reasoning_effort)), - ) - if model_settings_override is not None: - base_settings = base_settings.resolve(model_settings_override) - - sandbox_config = ( - SandboxRunConfig(client=sandbox_client, session=sandbox_session) - if sandbox_session is not None - else None - ) - - return RunConfig( - model=model, - model_provider=build_multi_provider(), - model_settings=base_settings, - sandbox=sandbox_config, - call_model_input_filter=inject_messages_filter, - tracing_disabled=False, - trace_include_sensitive_data=False, - ) - - -def make_agent_context( - *, - bus: AgentMessageBus, - sandbox_session: BaseSandboxSession | None, - agent_id: str, - parent_id: str | None, - tracer: Any | None, - model: str, - model_settings: ModelSettings | None = None, - max_turns: int = 300, - is_whitebox: bool = False, - interactive: bool = False, - diff_scope: dict[str, Any] | None = None, - run_id: str | None = None, - sandbox_client: Any | None = None, - agent_factory: Any | None = None, - caido_client: Any | None = None, -) -> dict[str, Any]: - """Build the per-agent ``context`` dict passed to ``Runner.run(context=...)``. - - The canonical place where bus, sandbox handles, identity, tracer - reference, and per-agent toggles live. Tools, hooks, and - ``inject_messages_filter`` reach in via ``ctx.context.get(...)``. - - ``agent_factory`` is a callable ``(name, skills) -> agents.Agent`` — - the ``create_agent`` graph tool uses it to spin up children that - inherit the same wiring. ``sandbox_client`` is the host-side Docker - subclass, reused across child runs. - """ - return { - "bus": bus, - "sandbox_session": sandbox_session, - "sandbox_client": sandbox_client, - "caido_client": caido_client, - "agent_id": agent_id, - "parent_id": parent_id, - "tracer": tracer, - "model": model, - "model_settings": model_settings, - "max_turns": max_turns, - "agent_finish_called": False, - "is_whitebox": is_whitebox, - "interactive": interactive, - "diff_scope": diff_scope, - "run_id": run_id, - "agent_factory": agent_factory, - } diff --git a/strix/io/scan_artifacts.py b/strix/telemetry/scan_artifacts.py similarity index 100% rename from strix/io/scan_artifacts.py rename to strix/telemetry/scan_artifacts.py diff --git a/strix/telemetry/tracer.py b/strix/telemetry/tracer.py index 0a6daaf..ac220be 100644 --- a/strix/telemetry/tracer.py +++ b/strix/telemetry/tracer.py @@ -5,8 +5,8 @@ from pathlib import Path from typing import Any, Optional from uuid import uuid4 -from strix.io.scan_artifacts import ScanArtifactWriter from strix.telemetry import posthog +from strix.telemetry.scan_artifacts import ScanArtifactWriter logger = logging.getLogger(__name__) diff --git a/strix/tools/agents_graph/tools.py b/strix/tools/agents_graph/tools.py index 739f19e..1620732 100644 --- a/strix/tools/agents_graph/tools.py +++ b/strix/tools/agents_graph/tools.py @@ -22,12 +22,16 @@ import uuid from datetime import UTC, datetime from typing import TYPE_CHECKING, Any, Literal -from agents import RunContextWrapper, function_tool +from agents import RunConfig, RunContextWrapper, function_tool from agents.items import TResponseInputItem +from agents.model_settings import ModelSettings +from agents.sandbox import SandboxRunConfig +from strix.llm.multi_provider_setup import build_multi_provider +from strix.llm.retry import DEFAULT_RETRY +from strix.orchestration.filter import inject_messages_filter from strix.orchestration.hooks import StrixOrchestrationHooks from strix.orchestration.run_loop import run_with_continuation -from strix.run_config_factory import make_agent_context, make_run_config if TYPE_CHECKING: @@ -428,7 +432,7 @@ async def create_agent( "success": False, "error": ( "No agent_factory in context. " - "The root assembly must inject one via make_agent_context." + "The root assembly must inject one when building the run context." ), }, ensure_ascii=False, @@ -485,32 +489,48 @@ async def create_agent( ) initial_input.append({"role": "user", "content": task}) - child_ctx = make_agent_context( - bus=bus, - sandbox_session=inner.get("sandbox_session"), - sandbox_client=inner.get("sandbox_client"), - caido_client=inner.get("caido_client"), - agent_id=child_id, - parent_id=parent_id, - tracer=inner.get("tracer"), - model=inner["model"], - model_settings=inner.get("model_settings"), - max_turns=int(inner.get("max_turns", 300)), - is_whitebox=bool(inner.get("is_whitebox", False)), - interactive=bool(inner.get("interactive", False)), - diff_scope=inner.get("diff_scope"), - run_id=inner.get("run_id"), - agent_factory=factory, - ) - # Stash the task string for ``agent_finish`` to echo back in its - # XML completion report. - child_ctx["task"] = task + child_ctx: dict[str, Any] = { + "bus": bus, + "sandbox_session": inner.get("sandbox_session"), + "sandbox_client": inner.get("sandbox_client"), + "caido_client": inner.get("caido_client"), + "agent_id": child_id, + "parent_id": parent_id, + "tracer": inner.get("tracer"), + "model": inner["model"], + "model_settings": inner.get("model_settings"), + "max_turns": int(inner.get("max_turns", 300)), + "agent_finish_called": False, + "is_whitebox": bool(inner.get("is_whitebox", False)), + "interactive": bool(inner.get("interactive", False)), + "diff_scope": inner.get("diff_scope"), + "run_id": inner.get("run_id"), + "agent_factory": factory, + # Stashed for ``agent_finish`` to echo back in its completion report. + "task": task, + } - child_run_config = make_run_config( - sandbox_session=inner.get("sandbox_session"), - sandbox_client=inner.get("sandbox_client"), + child_model_settings = ModelSettings( + parallel_tool_calls=False, + tool_choice="required", + retry=DEFAULT_RETRY, + ) + override = inner.get("model_settings") + if override is not None: + child_model_settings = child_model_settings.resolve(override) + sandbox_session = inner.get("sandbox_session") + child_run_config = RunConfig( model=inner["model"], - model_settings_override=inner.get("model_settings"), + model_provider=build_multi_provider(), + model_settings=child_model_settings, + sandbox=( + SandboxRunConfig(client=inner.get("sandbox_client"), session=sandbox_session) + if sandbox_session is not None + else None + ), + call_model_input_filter=inject_messages_filter, + tracing_disabled=False, + trace_include_sensitive_data=False, ) task_handle = asyncio.create_task(