From 8414c59557988d2b9085216a2427205528647f6e Mon Sep 17 00:00:00 2001 From: 0xallam Date: Tue, 26 May 2026 14:02:40 -0700 Subject: [PATCH] Strip narrative comments and module/helper docstrings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Five rounds of sweep across the tree. Net ~544 lines removed. Removed: - Section-divider banners and one-line section labels (# Display utilities, # ----- list_requests -----, # CVSS breakdown, etc.). - Module-level prose docstrings on internal modules. Kept one-line summaries; trimmed multi-paragraph narration about SDK/Strix responsibility splits, cache strategies, three-source precedence. - Internal-helper docstrings that just restate the function name — caido_api helpers (caido_url, get_client, view_request, etc.), settings-class one-liners (LLMSettings, RuntimeSettings, ...), UI helper docstrings. - Args/Returns blocks on non-LLM-facing internal helpers (build_strix_agent, render_system_prompt, create_or_reuse, bootstrap_caido) — kept only the genuinely non-obvious params. - Internal-history phrasing — "Mirrors main-branch shape", "pre-SDK harness", "previous lookup matched no attribute". - Narrative comments inside function bodies that explained what the next line does, design rationale obvious from the surrounding code, or "we used to..." asides. - Trailing periods on every error-string literal across the tool tree. - Duplicated roundtripTime quirk comment (kept the LLM-facing copy in tools/proxy/tools.py). Kept (every one names an upstream bug, vendored-code provenance, or non-obvious data quirk): - core/runner.py: SDK replay-with-empty-initial-input + on_agent_end lifecycle gap. - runtime/docker_client.py: VERBATIM COPY block of the upstream _create_container body, pinned to SDK v0.14.6. - runtime/session_manager.py: NO_PROXY for agent-browser CDP loopback. - tools/proxy/caido_api.py: generated-pydantic Request.raw quirk, replay double-history pitfall. - tools/proxy/tools.py: Caido roundtripTime=0 quirk for proxy captures. Co-Authored-By: Claude Opus 4.7 --- strix/agents/__init__.py | 12 ---- strix/agents/factory.py | 66 +------------------ strix/agents/prompt.py | 29 +------- strix/config/loader.py | 11 +--- strix/config/models.py | 10 +-- strix/config/settings.py | 30 +-------- strix/core/agents.py | 8 +-- strix/core/execution.py | 3 - strix/core/inputs.py | 3 - strix/core/runner.py | 24 +------ strix/interface/cli.py | 7 -- strix/interface/main.py | 23 ------- strix/interface/tui/app.py | 42 +----------- strix/interface/tui/live_view.py | 2 - strix/interface/tui/messages.py | 1 - .../interface/tui/renderers/proxy_renderer.py | 1 - strix/interface/utils.py | 14 ---- strix/runtime/__init__.py | 16 +---- strix/runtime/backends.py | 22 +------ strix/runtime/caido_bootstrap.py | 21 +----- strix/runtime/docker_client.py | 8 +-- strix/runtime/session_manager.py | 38 ++--------- strix/skills/__init__.py | 4 -- strix/telemetry/logging.py | 24 +------ strix/telemetry/posthog.py | 1 - strix/tools/agents_graph/tools.py | 21 +----- strix/tools/notes/tools.py | 28 +------- strix/tools/proxy/caido_api.py | 33 +--------- strix/tools/proxy/tools.py | 39 +---------- strix/tools/reporting/tool.py | 4 -- strix/tools/todo/tools.py | 33 +--------- strix/tools/web_search/tool.py | 5 -- 32 files changed, 28 insertions(+), 555 deletions(-) diff --git a/strix/agents/__init__.py b/strix/agents/__init__.py index 85c335a..e69de29 100644 --- a/strix/agents/__init__.py +++ b/strix/agents/__init__.py @@ -1,12 +0,0 @@ -"""Strix agent assembly. - -- :func:`strix.agents.factory.build_strix_agent` — assemble a root or - child ``SandboxAgent``. -- :func:`strix.agents.factory.make_child_factory` — closure factory - passed via context to the multi-agent ``create_agent`` graph tool. -- :func:`strix.agents.prompt.render_system_prompt` — render the Jinja - system prompt. - -Import deeply so ``import strix.agents`` doesn't pull every submodule's -deps in eagerly. -""" diff --git a/strix/agents/factory.py b/strix/agents/factory.py index a9e7a94..f559033 100644 --- a/strix/agents/factory.py +++ b/strix/agents/factory.py @@ -1,20 +1,4 @@ -"""``build_strix_agent`` — assemble an ``agents.Agent`` for root or child. - -Wires the SDK function tools, multi-agent graph tools, and the rendered -Jinja prompt into one ``agents.Agent`` ready for ``Runner.run``. - -Two flavors: - -- **Root** (``is_root=True``): top-level scan agent. Carries - ``finish_scan`` and stops after that tool reports ``scan_completed``. -- **Child** (``is_root=False``): subagents spawned by the - ``create_agent`` graph tool. Carries ``agent_finish`` and stops - after that tool reports ``agent_completed``. - -Skills are baked into the system prompt at scan bring-up. The -``load_skill`` tool is also available for on-demand inline reference -to any skill the agent didn't preload. -""" +"""Build SandboxAgents for root + child Strix runs.""" from __future__ import annotations @@ -138,8 +122,6 @@ def _function_tool_with_error_result(tool: FunctionTool) -> FunctionTool: def _custom_tool_as_function_tool(tool: CustomTool) -> FunctionTool: - """Expose an SDK raw-input custom tool through Chat-Completions function calling.""" - async def invoke(ctx: Any, raw_input: str) -> Any: custom_input = _extract_custom_input(tool, raw_input) if not custom_input: @@ -337,42 +319,28 @@ def _finish_tool_use_behavior( return ToolsToFinalOutputResult(is_final_output=False, final_output=None) -# Host-side Strix tools. Sandbox shell + filesystem are added per-run -# by the SDK via the ``Shell`` and ``Filesystem`` capabilities below -# (they bind to the live sandbox session and emit ``exec_command`` / -# ``write_stdin`` / ``apply_patch`` / ``view_image`` function tools). _BASE_TOOLS: tuple[Tool, ...] = ( - # Thinking + planning think, - # On-demand skill reference (returns the skill markdown inline) load_skill, - # Per-agent todos create_todo, list_todos, update_todo, mark_todo_done, mark_todo_pending, delete_todo, - # Shared notes (per-run JSONL store) create_note, list_notes, get_note, update_note, delete_note, - # Web search (only registered if PERPLEXITY_API_KEY is set; the - # tool itself returns a structured error when not configured, so - # always exposing it is safe) web_search, - # Reporting create_vulnerability_report, - # Caido HTTP/HTTPS proxy list_requests, view_request, repeat_request, list_sitemap, view_sitemap_entry, scope_rules, - # Multi-agent graph tools (the coordinator is in ctx.context) view_agent_graph, send_message_to_agent, wait_for_message, @@ -392,35 +360,11 @@ def build_strix_agent( chat_completions_tools: bool = False, system_prompt_context: dict[str, Any] | None = None, ) -> SandboxAgent[Any]: - """Build a ``SandboxAgent`` configured for either root or child use. - - The ``Shell`` and ``Filesystem`` capabilities are added unbound; the - SDK's runtime binds them per-run against the live sandbox session - set on ``RunConfig.sandbox`` and merges their tools (``exec_command``, - ``write_stdin``, ``apply_patch``, ``view_image``) into the agent's - final tool list. We deliberately exclude ``Compaction`` (OpenAI - Responses API only). + """Build a SandboxAgent for either root or child use. Args: - name: Agent name. Surfaces in traces and the coordinator's ``names`` map. - Defaults to ``"strix"`` for the root; create_agent passes - distinct names per child. - skills: Skills to preload into the system prompt. - is_root: Selects the tool list and ``tool_use_behavior``. - Root carries ``finish_scan`` and child carries ``agent_finish``; - the run only stops when the lifecycle tool result succeeds. - scan_mode: ``"deep"`` etc.; routes the scan-mode skill section - of the prompt template. - is_whitebox: Whitebox source-aware mode toggle. Adds two extra - skills to the prompt and gates whitebox-only behavior in - the create_agent / wiki integration. - interactive: Renders the interactive-mode communication block - in the system prompt. chat_completions_tools: Wrap SDK custom tools as function tools when the selected backend cannot accept Responses custom tools. - system_prompt_context: Free-form dict the prompt template - renders into the ``system_prompt_context`` variable — - today carries the scan scope / authorization block. """ instructions = render_system_prompt( skills=skills, @@ -451,13 +395,7 @@ def build_strix_agent( instructions=instructions, tools=tools, tool_use_behavior=_finish_tool_use_behavior, - # Non-interactive runs must keep forcing tool calls until the - # lifecycle tool completes. Interactive runs need the SDK default - # reset so a tool-assisted answer can end as plain text instead of - # looping through think/list_todos forever. reset_tool_choice=interactive, - # model=None so ``RunConfig.model`` drives provider selection - # through the SDK's default MultiProvider. model=None, capabilities=[ Filesystem( diff --git a/strix/agents/prompt.py b/strix/agents/prompt.py index 5263e82..5d90217 100644 --- a/strix/agents/prompt.py +++ b/strix/agents/prompt.py @@ -1,9 +1,4 @@ -"""Jinja-based system-prompt renderer. - -Loads ``strix/agents/prompts/system_prompt.jinja`` and renders it with -the caller's per-run context (skills, scan mode, whitebox flag, -interactive flag, scope authorization block). -""" +"""Jinja-based system-prompt renderer.""" from __future__ import annotations @@ -71,27 +66,7 @@ def render_system_prompt( interactive: bool = False, system_prompt_context: dict[str, Any] | None = None, ) -> str: - """Render the system prompt. - - Args: - skills: Skills the caller wants preloaded into the prompt context. - scan_mode: ``"deep" | "fast" | ...``. Maps to ``scan_modes/`` - skill. - is_whitebox: When True, the source-aware whitebox skill stack - is loaded too. - is_root: When True, ``coordination/root_agent`` orchestration - guidance is auto-loaded. - interactive: When True, the prompt renders the interactive-mode - communication rules block. - system_prompt_context: Free-form dict that the template's - ``system_prompt_context`` variable receives — carries the - scan-scope authorization block. - - Returns the rendered prompt string. If anything goes wrong (template - missing, render failure), returns an empty string and logs — a - missing prompt is survivable, a hard failure during agent - construction is not. - """ + """Render the system prompt. Returns empty string on template failure.""" try: prompt_dir = get_strix_resource_path("agents", _PROMPT_DIRNAME) skills_dir = get_strix_resource_path("skills") diff --git a/strix/config/loader.py b/strix/config/loader.py index 3ed2e2a..9d99115 100644 --- a/strix/config/loader.py +++ b/strix/config/loader.py @@ -1,9 +1,4 @@ -"""Settings loader, override switch, and disk persistence. - -Process-wide module cache so repeated ``load_settings()`` calls in the -same scan are free. ``apply_config_override(path)`` invalidates the -cache so the next ``load_settings()`` re-resolves with the new file. -""" +"""Settings loader, override switch, and disk persistence.""" from __future__ import annotations @@ -81,9 +76,6 @@ def persist_current() -> None: target.chmod(0o600) -# --- internals --------------------------------------------------------- - - def _aliases_for(finfo: FieldInfo) -> list[str]: """Collect every env-var name that should populate ``finfo``.""" aliases: list[str] = [] @@ -113,7 +105,6 @@ def _read_json_overrides(path: Path) -> dict[str, dict[str, Any]]: if not isinstance(env_block, dict): return {} - # Normalize to upper-case keys for matching. env_block_upper = {str(k).upper(): v for k, v in env_block.items()} nested: dict[str, dict[str, Any]] = {} diff --git a/strix/config/models.py b/strix/config/models.py index b2a9b09..c389b7d 100644 --- a/strix/config/models.py +++ b/strix/config/models.py @@ -1,10 +1,4 @@ -"""SDK model configuration helpers. - -This module is intentionally not a provider abstraction. Strix accepts -friendly model names at the config boundary, normalizes them to the -OpenAI Agents SDK's native model ids, then lets the SDK's default -``MultiProvider`` do the actual routing. -""" +"""SDK model configuration helpers.""" from __future__ import annotations @@ -63,7 +57,7 @@ def configure_sdk_model_defaults(settings: Settings) -> None: def _configure_litellm_compatibility() -> None: - """Match the permissive LiteLLM behavior used by the pre-SDK harness.""" + """Enable LiteLLM's permissive param-handling mode.""" import litellm litellm.drop_params = True diff --git a/strix/config/settings.py b/strix/config/settings.py index bdd96c2..7472ab9 100644 --- a/strix/config/settings.py +++ b/strix/config/settings.py @@ -1,22 +1,4 @@ -"""Strix application settings — pydantic-settings powered. - -Three sources, env-precedence-first: - -1. Environment variables (``STRIX_LLM``, ``LLM_API_KEY``, etc.) — highest. -2. ``~/.strix/cli-config.json`` (or ``--config ``) — middle. -3. Field defaults — lowest. - -Bool fields auto-parse ``"0"``/``"false"``/``"no"``/``"off"`` as falsy -and any other non-empty string as truthy. Int fields auto-coerce from -string env. The ``api_base`` field walks an alias chain so users can -point at any OpenAI-compatible endpoint via whichever env name they -prefer (``LLM_API_BASE`` / ``OPENAI_API_BASE`` / ``OPENAI_BASE_URL`` / -``LITELLM_BASE_URL`` / ``OLLAMA_API_BASE``). - -Each sub-model is a :class:`BaseSettings` so it reads env independently -— the alternative (one mega-BaseSettings with flat fields) would lose -the logical grouping ``s.llm.model`` / ``s.runtime.image`` / etc. -""" +"""Strix application settings — pydantic-settings powered.""" from __future__ import annotations @@ -36,8 +18,6 @@ _BASE_CONFIG = SettingsConfigDict( class LlmSettings(BaseSettings): - """LLM provider + model + per-call defaults.""" - model_config = _BASE_CONFIG model: str | None = Field(default=None, alias="STRIX_LLM") @@ -60,8 +40,6 @@ class LlmSettings(BaseSettings): class RuntimeSettings(BaseSettings): - """Sandbox image + backend selector.""" - model_config = _BASE_CONFIG image: str = Field( @@ -72,24 +50,18 @@ class RuntimeSettings(BaseSettings): class TelemetrySettings(BaseSettings): - """Telemetry toggle.""" - model_config = _BASE_CONFIG enabled: bool = Field(default=True, alias="STRIX_TELEMETRY") class IntegrationSettings(BaseSettings): - """Third-party integration credentials.""" - model_config = _BASE_CONFIG perplexity_api_key: str | None = Field(default=None, alias="PERPLEXITY_API_KEY") class Settings(BaseSettings): - """Composite Strix settings. Instantiate via :func:`strix.config.load_settings`.""" - model_config = _BASE_CONFIG llm: LlmSettings = Field(default_factory=LlmSettings) diff --git a/strix/core/agents.py b/strix/core/agents.py index 44fe343..6aa5801 100644 --- a/strix/core/agents.py +++ b/strix/core/agents.py @@ -1,10 +1,4 @@ -"""SDK-native state for Strix's addressable agent graph. - -The Agents SDK owns model/tool execution and per-agent conversation -history. Strix owns only product semantics the SDK does not provide: -agent ids, the parent/child graph, wake/stop signals, TUI-visible -status, and process-resume metadata. -""" +"""SDK-native state for Strix's addressable agent graph.""" from __future__ import annotations diff --git a/strix/core/execution.py b/strix/core/execution.py index 73a60ef..84fe72f 100644 --- a/strix/core/execution.py +++ b/strix/core/execution.py @@ -189,10 +189,7 @@ async def respawn_subagents( event_sink: StreamEventSink | None = None, hooks: RunHooks[dict[str, Any]] | None = None, ) -> None: - """Re-spawn subagent runners from a restored coordinator snapshot.""" async with coordinator._lock: - # Snapshot the iteration view first so we can mutate via coordinator - # below without "dict changed during iteration" trouble. agents_snapshot = [ (aid, status, dict(coordinator.metadata.get(aid, {}))) for aid, status in coordinator.statuses.items() diff --git a/strix/core/inputs.py b/strix/core/inputs.py index 4922526..225fc1c 100644 --- a/strix/core/inputs.py +++ b/strix/core/inputs.py @@ -15,12 +15,10 @@ if TYPE_CHECKING: from strix.config.settings import ReasoningEffort -# Default max_turns budget passed to the SDK runner. DEFAULT_MAX_TURNS = 500 def build_root_task(scan_config: dict[str, Any]) -> str: - """Format the user-facing task for the root agent.""" targets = scan_config.get("targets", []) or [] diff_scope = scan_config.get("diff_scope") or {} user_instructions = scan_config.get("user_instructions", "") or "" @@ -81,7 +79,6 @@ def build_root_task(scan_config: dict[str, Any]) -> str: def build_scope_context(scan_config: dict[str, Any]) -> dict[str, Any]: - """Build the system_prompt_context block consumed by the prompt template.""" authorized: list[dict[str, str]] = [] value_keys = { "repository": "target_repo", diff --git a/strix/core/runner.py b/strix/core/runner.py index da02916..0e6d658 100644 --- a/strix/core/runner.py +++ b/strix/core/runner.py @@ -1,9 +1,4 @@ -"""Top-level Strix scan runner. - -The SDK owns model/tool execution and per-agent sessions. This module owns -Strix-specific scan setup, child-agent startup, resume, and the small wake loop -needed to keep every agent addressable after its SDK run parks. -""" +"""Top-level Strix scan runner.""" from __future__ import annotations @@ -72,8 +67,6 @@ async def run_strix_scan( if scan_id is None: scan_id = f"scan-{uuid.uuid4().hex[:8]}" - # Resolve run_dir before any heavy bring-up so the log file captures - # everything from sandbox start onwards. run_dir = run_dir_for(scan_id) run_dir.mkdir(parents=True, exist_ok=True) state_dir = runtime_state_dir(run_dir) @@ -105,15 +98,10 @@ async def run_strix_scan( logger.info("LLM model resolved: %s", resolved_model) chat_completions_tools = uses_chat_completions_tool_schema(resolved_model, settings) - # Caller may pre-create the coordinator so it can route stop/chat - # commands while the scan loop runs in another thread. if coordinator is None: coordinator = AgentCoordinator() coordinator.set_snapshot_path(agents_path) - # Wire the per-agent todo store to ``{run_dir}/.state/todos.json`` (mirrored - # on every CRUD) and reload any prior todos so respawned subagents - # find their lists intact. Same for the shared notes store. from strix.tools.notes.tools import hydrate_notes_from_disk from strix.tools.todo.tools import hydrate_todos_from_disk @@ -228,8 +216,6 @@ async def run_strix_scan( "spawn_child_agent": spawn_child_agent, } - # All agents share one SQLite database; SDK session_id separates - # each agent's conversation inside that database. root_session = open_agent_session(root_id, agents_db) sessions_to_close.append(root_session) await coordinator.attach_runtime(root_id, session=root_session) @@ -291,16 +277,8 @@ async def run_strix_scan( ) except BaseException: logger.exception("Strix scan %s failed", scan_id) - # Cancel any descendant tasks the root spawned before unwinding. - # cancel_descendants is idempotent and handles the empty-tree case. if root_id is not None: await coordinator.cancel_descendants(root_id) - # The SDK's on_agent_end hook only fires after a successful - # ``Runner.run_streamed`` reaches the agent's first turn. A - # failure earlier (e.g., model-provider routing, sandbox - # bring-up) leaves the root stuck at status="running" — the - # TUI keeps animating "Initializing" forever. Finalize it - # here so the coordinator reflects reality. with contextlib.suppress(Exception): await coordinator.set_status(root_id, "failed") raise diff --git a/strix/interface/cli.py b/strix/interface/cli.py index 6b37f2e..1f7cbce 100644 --- a/strix/interface/cli.py +++ b/strix/interface/cli.py @@ -93,9 +93,6 @@ async def run_cli(args: Any) -> None: # noqa: PLR0915 "local_sources": getattr(args, "local_sources", None) or [], "scope_mode": getattr(args, "scope_mode", "auto"), "diff_base": getattr(args, "diff_base", None), - # Forward the new --instruction (if any) to the resume path so it - # can deliver it as a fresh user message after SDK session replay. - # Empty string when the user didn't pass one on resume — no-op. "resume_instruction": getattr(args, "user_explicit_instruction", None) or "", } @@ -190,10 +187,6 @@ async def run_cli(args: Any) -> None: # noqa: PLR0915 finally: stop_updates.set() update_thread.join(timeout=1) - # Best-effort: tear down the sandbox session even if the - # run raised. ``run_strix_scan`` already does this in its - # own ``finally``, but call here too in case the failure - # was during early setup. with contextlib.suppress(Exception): await session_manager.cleanup(args.run_name) diff --git a/strix/interface/main.py b/strix/interface/main.py index b9c0145..76d36cf 100644 --- a/strix/interface/main.py +++ b/strix/interface/main.py @@ -54,13 +54,6 @@ HOST_GATEWAY_HOSTNAME = "host.docker.internal" import logging # noqa: E402 -# Per-scan logging is set up by ``setup_scan_logging`` from inside -# ``core.runner.run_strix_scan`` once the scan ``run_dir`` is -# known — that's where ``strix.*`` levels and handlers are owned. Pre-scan -# work (``main()``, env validation, image pull) emits via the module -# logger; once setup_scan_logging runs, those records start landing in -# the file too. - logger = logging.getLogger(__name__) @@ -423,9 +416,6 @@ Examples: except Exception as e: parser.error(f"Failed to read instruction file '{instruction_path}': {e}") - # Capture before ``_load_resume_state`` overrides — used by the resume - # path in ``run_strix_scan`` to decide whether to inject the new - # instruction into the root's SDK session after replay. args.user_explicit_instruction = args.instruction if args.resume else None if args.resume: @@ -472,7 +462,6 @@ Examples: def _persist_run_record(args: argparse.Namespace) -> None: - """Write the single public run descriptor used by resume and reporting.""" run_dir = run_dir_for(args.run_name) run_dir.mkdir(parents=True, exist_ok=True) run_record = { @@ -511,10 +500,6 @@ def _load_resume_state(args: argparse.Namespace, parser: argparse.ArgumentParser if not args.targets_info: parser.error(f"--resume {args.resume}: run.json has no targets_info") - # Validate any persisted ``cloned_repo_path`` still exists on disk. - # The resume path skips re-cloning, so a missing dir would mean the - # container mounts an empty source tree and agents silently scan - # nothing. for target in args.targets_info: if not isinstance(target, dict): continue @@ -539,11 +524,6 @@ def _load_resume_state(args: argparse.Namespace, parser: argparse.ArgumentParser args.diff_scope = state.get("diff_scope") persisted_scan_mode = state.get("scan_mode") if persisted_scan_mode and args.scan_mode == "deep": - # Default scan_mode is "deep"; only override from disk if the user - # didn't explicitly pass a different one. (Best-effort: argparse - # can't tell "user passed 'deep'" from "default 'deep'"; if the - # persisted run was "quick" and user re-runs with an explicit - # ``-m deep``, we'll honor the persisted mode. Acceptable.) args.scan_mode = persisted_scan_mode @@ -730,9 +710,6 @@ def main() -> None: else: args.instruction = diff_scope.instruction_block - # Persist the fully-resolved run descriptor so a future - # ``--resume `` invocation can pick up without - # re-supplying targets / instructions / scope. _persist_run_record(args) _telemetry_start_kwargs = { diff --git a/strix/interface/tui/app.py b/strix/interface/tui/app.py index 8eb227c..4453ea1 100644 --- a/strix/interface/tui/app.py +++ b/strix/interface/tui/app.py @@ -262,8 +262,6 @@ class StopAgentScreen(ModalScreen): # type: ignore[misc] class VulnerabilityDetailScreen(ModalScreen): # type: ignore[misc] - """Modal screen to display vulnerability details.""" - SEVERITY_COLORS: ClassVar[dict[str, str]] = { "critical": "#dc2626", # Red "high": "#ea580c", # Orange @@ -390,7 +388,6 @@ class VulnerabilityDetailScreen(ModalScreen): # type: ignore[misc] text.append("CVE: ", style=self.FIELD_STYLE) text.append(cve) - # CVSS breakdown cvss_breakdown = vuln.get("cvss_breakdown", {}) if cvss_breakdown: cvss_parts = [] @@ -464,12 +461,10 @@ class VulnerabilityDetailScreen(ModalScreen): # type: ignore[misc] vuln = self.vulnerability lines: list[str] = [] - # Title title = vuln.get("title", "Untitled Vulnerability") lines.append(f"# {title}") lines.append("") - # Metadata if vuln.get("id"): lines.append(f"**ID:** {vuln['id']}") if vuln.get("severity"): @@ -489,7 +484,6 @@ class VulnerabilityDetailScreen(ModalScreen): # type: ignore[misc] if vuln.get("cvss") is not None: lines.append(f"**CVSS:** {vuln['cvss']}") - # CVSS Vector cvss_breakdown = vuln.get("cvss_breakdown", {}) if cvss_breakdown: abbrevs = { @@ -508,21 +502,17 @@ class VulnerabilityDetailScreen(ModalScreen): # type: ignore[misc] if parts: lines.append(f"**CVSS Vector:** {'/'.join(parts)}") - # Description lines.append("") lines.append("## Description") lines.append("") lines.append(vuln.get("description") or "No description provided.") - # Impact if vuln.get("impact"): lines.extend(["", "## Impact", "", vuln["impact"]]) - # Technical Analysis if vuln.get("technical_analysis"): lines.extend(["", "## Technical Analysis", "", vuln["technical_analysis"]]) - # Proof of Concept if vuln.get("poc_description") or vuln.get("poc_script_code"): lines.extend(["", "## Proof of Concept", ""]) if vuln.get("poc_description"): @@ -533,7 +523,6 @@ class VulnerabilityDetailScreen(ModalScreen): # type: ignore[misc] lines.append(vuln["poc_script_code"]) lines.append("```") - # Code Analysis if vuln.get("code_locations"): lines.extend(["", "## Code Analysis", ""]) for i, loc in enumerate(vuln["code_locations"]): @@ -559,7 +548,6 @@ class VulnerabilityDetailScreen(ModalScreen): # type: ignore[misc] lines.append("```") lines.append("") - # Remediation if vuln.get("remediation_steps"): lines.extend(["", "## Remediation", "", vuln["remediation_steps"]]) @@ -584,8 +572,6 @@ class VulnerabilityDetailScreen(ModalScreen): # type: ignore[misc] class VulnerabilityItem(Static): # type: ignore[misc] - """A clickable vulnerability item.""" - def __init__(self, label: Text, vuln_data: dict[str, Any], **kwargs: Any) -> None: super().__init__(label, **kwargs) self.vuln_data = vuln_data @@ -596,8 +582,6 @@ class VulnerabilityItem(Static): # type: ignore[misc] class VulnerabilitiesPanel(VerticalScroll): # type: ignore[misc] - """A scrollable panel showing found vulnerabilities with severity-colored dots.""" - SEVERITY_COLORS: ClassVar[dict[str, str]] = { "critical": "#dc2626", # Red "high": "#ea580c", # Orange @@ -716,8 +700,6 @@ class StrixTUIApp(App): # type: ignore[misc] self.live_view.hydrate_from_run_dir(self.report_state.get_run_dir()) self._agent_graph_sync_future: Any | None = None - # Pre-create the coordinator here so the TUI can route stop/chat - # commands while the scan loop runs in a worker thread. from strix.core.agents import AgentCoordinator self.coordinator = AgentCoordinator() @@ -731,13 +713,10 @@ class StrixTUIApp(App): # type: ignore[misc] self._scan_loop: asyncio.AbstractEventLoop | None = None self._scan_stop_event = threading.Event() self._scan_completed = threading.Event() - # Captured by ``scan_target`` when the scan thread crashes; read - # by ``run_tui`` after ``run_async()`` returns so the user sees - # the traceback on stderr instead of just a silent UI hang. self._scan_error: BaseException | None = None - self._spinner_frame_index: int = 0 # Current animation frame index - self._sweep_num_squares: int = 6 # Number of squares in sweep animation + self._spinner_frame_index: int = 0 + self._sweep_num_squares: int = 6 self._sweep_colors: list[str] = [ "#000000", # Dimmest (shows dot) "#031a09", @@ -764,8 +743,6 @@ class StrixTUIApp(App): # type: ignore[misc] "local_sources": getattr(args, "local_sources", None) or [], "scope_mode": getattr(args, "scope_mode", "auto"), "diff_base": getattr(args, "diff_base", None), - # Forward the new --instruction (if any) so the resume path - # can deliver it as a fresh user message after session replay. "resume_instruction": getattr(args, "user_explicit_instruction", None) or "", } @@ -1378,9 +1355,6 @@ class StrixTUIApp(App): # type: ignore[misc] try: loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) - # Stash the loop so synchronous TUI handlers (stop / - # chat) can submit coordinator coroutines onto it from the - # main thread. self._scan_loop = loop try: @@ -1410,8 +1384,6 @@ class StrixTUIApp(App): # type: ignore[misc] logging.exception("Unexpected error during scan") self._scan_error = e finally: - # Best-effort sandbox teardown if early setup failed - # before run_strix_scan's own ``finally`` ran. with contextlib.suppress(Exception): loop.run_until_complete( session_manager.cleanup(self.scan_config["run_name"]), @@ -1722,11 +1694,6 @@ class StrixTUIApp(App): # type: ignore[misc] return agent_name, False def action_confirm_stop_agent(self, agent_id: str) -> None: - # Graceful stop: each agent's current turn finishes (and is saved to - # session) before the run loop honors the cancel. The interactive - # outer loop parks with status="stopped". - # The hard ``cancel_descendants`` path remains for KeyboardInterrupt - # in entry.py where graceful isn't possible. if self._scan_loop is None or self._scan_loop.is_closed(): logger.warning("No active scan loop; cannot stop agent %s", agent_id) return @@ -1869,12 +1836,7 @@ class StrixTUIApp(App): # type: ignore[misc] async def run_tui(args: argparse.Namespace) -> None: - """Run strix in interactive TUI mode with textual.""" app = StrixTUIApp(args) await app.run_async() - # Propagate scan-thread failures: ``app.run_async`` returns normally - # when the user quits (ctrl-q) regardless of whether the scan - # crashed. Without this re-raise, ``main.py`` would treat a failed - # scan as success and print the completion banner. if app._scan_error is not None: raise app._scan_error diff --git a/strix/interface/tui/live_view.py b/strix/interface/tui/live_view.py index e14b352..993074d 100644 --- a/strix/interface/tui/live_view.py +++ b/strix/interface/tui/live_view.py @@ -15,8 +15,6 @@ from strix.interface.tui.history import load_session_history class TuiLiveView: - """UI projection of agent state plus SDK stream/session events.""" - def __init__(self) -> None: self.agents: dict[str, dict[str, Any]] = {} self.events: list[dict[str, Any]] = [] diff --git a/strix/interface/tui/messages.py b/strix/interface/tui/messages.py index c1e473c..18cd37c 100644 --- a/strix/interface/tui/messages.py +++ b/strix/interface/tui/messages.py @@ -18,7 +18,6 @@ def send_user_message_to_agent( target_agent_id: str, message: str, ) -> bool: - """Record a local user message and enqueue it into the target SDK session.""" if loop is None or loop.is_closed(): return False diff --git a/strix/interface/tui/renderers/proxy_renderer.py b/strix/interface/tui/renderers/proxy_renderer.py index 4cd46a5..6fbbe12 100644 --- a/strix/interface/tui/renderers/proxy_renderer.py +++ b/strix/interface/tui/renderers/proxy_renderer.py @@ -17,7 +17,6 @@ def _truncate(text: str, max_len: int = 80) -> str: def _sanitize(text: str, max_len: int = 150) -> str: - """Remove newlines and truncate text.""" clean = text.replace("\n", " ").replace("\r", "").replace("\t", " ") return _truncate(clean, max_len) diff --git a/strix/interface/utils.py b/strix/interface/utils.py index 76b7d3e..ff53a6c 100644 --- a/strix/interface/utils.py +++ b/strix/interface/utils.py @@ -23,7 +23,6 @@ from rich.text import Text from strix.config import load_settings -# Display utilities def get_severity_color(severity: str) -> str: severity_colors = { "critical": "#dc2626", @@ -57,7 +56,6 @@ def format_token_count(count: float | None) -> str: def format_vulnerability_report(report: dict[str, Any]) -> Text: # noqa: PLR0915 - """Format a vulnerability report for CLI display with all rich fields.""" field_style = "bold #4ade80" text = Text() @@ -206,7 +204,6 @@ def format_vulnerability_report(report: dict[str, Any]) -> Text: # noqa: PLR091 def _build_vulnerability_stats(stats_text: Text, report_state: Any) -> None: - """Build vulnerability section of stats text.""" vuln_count = len(report_state.vulnerability_reports) if vuln_count > 0: @@ -318,7 +315,6 @@ def _build_llm_usage_stats( def build_final_stats_text(report_state: Any) -> Text: - """Build final stats from Strix-owned scan artifacts.""" stats_text = Text() if not report_state: return stats_text @@ -401,9 +397,6 @@ def build_tui_stats_text(report_state: Any) -> Text: return stats_text -# Name generation utilities - - def _slugify_for_run_name(text: str, max_length: int = 32) -> str: text = text.lower().strip() text = re.sub(r"[^a-z0-9]+", "-", text) @@ -461,8 +454,6 @@ def generate_run_name(targets_info: list[dict[str, Any]] | None = None) -> str: return f"{slug}_{random_suffix}" -# Target processing utilities - _SUPPORTED_SCOPE_MODES = {"auto", "diff", "full"} _MAX_FILES_PER_SECTION = 120 @@ -712,9 +703,6 @@ def _parse_name_status_z(raw_output: bytes) -> list[DiffEntry]: if len(status_raw) > 1 and status_raw[1:].isdigit(): similarity = int(status_raw[1:]) - # Git's -z output for --name-status is: - # - non-rename/copy: \0\0 - # - rename/copy: \0\0\0 if status_code in {"R", "C"} and index + 2 < len(tokens): old_path = tokens[index + 1] new_path = tokens[index + 2] @@ -1264,7 +1252,6 @@ def rewrite_localhost_targets(targets_info: list[dict[str, Any]], host_gateway: details["target_ip"] = host_gateway -# Repository utilities def clone_repository(repo_url: str, run_name: str, dest_name: str | None = None) -> str: console = Console() @@ -1341,7 +1328,6 @@ def clone_repository(repo_url: str, run_name: str, dest_name: str | None = None) sys.exit(1) -# Docker utilities def check_docker_connection() -> Any: try: return docker.from_env() diff --git a/strix/runtime/__init__.py b/strix/runtime/__init__.py index a468625..2703633 100644 --- a/strix/runtime/__init__.py +++ b/strix/runtime/__init__.py @@ -1,15 +1 @@ -"""Strix runtime — pluggable sandbox lifecycle on top of the Agents SDK. - -- :mod:`.backends` — registry mapping ``STRIX_RUNTIME_BACKEND`` values - to async factories that bring up a ``(client, session)`` pair. Ships - with ``"docker"`` out of the box; ``register_backend`` lets downstream - users add Daytona / K8s / Modal / etc. without forking. -- :mod:`.session_manager` — ``create_or_reuse`` / ``cleanup`` keyed - by scan id; bundles the SDK session with a ready Caido client. -- :mod:`.caido_bootstrap` — runtime-agnostic Caido auth dance via - ``session.exec``. -- :class:`strix.runtime.docker_client.StrixDockerSandboxClient` — - ``DockerSandboxClient`` subclass that injects ``NET_ADMIN`` / - ``NET_RAW`` capabilities and ``host.docker.internal`` extra-hosts - (used only by the Docker backend). -""" +"""Pluggable sandbox lifecycle on top of the Agents SDK.""" diff --git a/strix/runtime/backends.py b/strix/runtime/backends.py index e4fa482..9f241a3 100644 --- a/strix/runtime/backends.py +++ b/strix/runtime/backends.py @@ -1,18 +1,4 @@ -"""Sandbox backend registry — runtime-agnostic session bring-up. - -A *backend* is an async callable that takes an image tag + an SDK -:class:`Manifest` + the ports to expose, and returns the matching -``(client, session)`` pair. The caller owns lifecycle from there -(``await client.delete(session)``). - -This keeps :mod:`strix.runtime.session_manager` free of any -backend-specific imports — switching to Daytona / K8s / Modal / -whatever is one new factory function plus one registry entry. - -Selection is driven by ``STRIX_RUNTIME_BACKEND`` (default: ``"docker"``). -Unknown values raise :class:`ValueError` rather than silently falling -back, so typos fail loudly. -""" +"""Sandbox backend registry — selected via STRIX_RUNTIME_BACKEND (default: docker).""" from __future__ import annotations @@ -28,11 +14,6 @@ if TYPE_CHECKING: logger = logging.getLogger(__name__) -# A backend brings up a fresh session and returns the (client, session) -# pair. The client is whatever object exposes ``await client.delete(session)`` -# for cleanup — typically an ``agents.sandbox.client.BaseSandboxClient`` -# subclass, but the protocol is duck-typed so non-SDK backends could -# also plug in if they implement the same interface. SandboxBackend = Callable[..., Awaitable[tuple[Any, Any]]] @@ -103,5 +84,4 @@ def register_backend(name: str, backend: SandboxBackend) -> None: def supported_backends() -> list[str]: - """Snapshot of registered backend names. Useful for ``--help`` text.""" return sorted(_BACKENDS) diff --git a/strix/runtime/caido_bootstrap.py b/strix/runtime/caido_bootstrap.py index 59f5a72..7057d22 100644 --- a/strix/runtime/caido_bootstrap.py +++ b/strix/runtime/caido_bootstrap.py @@ -5,10 +5,6 @@ The Caido CLI runs as an in-container sidecar listening on ``session.exec()``-ing curl from inside the container, then construct a host-side :class:`caido_sdk_client.Client` against the runtime's exposed-port URL for all subsequent SDK calls. - -Running the auth dance through ``session.exec`` keeps this module -runtime-agnostic — Docker / Daytona / K8s sessions all implement -``exec`` even when their port-exposure semantics differ. """ from __future__ import annotations @@ -89,22 +85,7 @@ async def bootstrap_caido( host_url: str, container_url: str, ) -> Client: - """Connect to the in-container Caido sidecar and select a fresh project. - - Args: - session: Bound sandbox session — used for ``exec`` to call into - the in-container Caido API for the guest-login dance. - host_url: Host-reachable URL for Caido's GraphQL endpoint - (e.g. ``http://127.0.0.1:{exposed_port}``). Used by the - host-side :class:`Client` for all post-bootstrap calls. - container_url: In-container URL for Caido's GraphQL endpoint - (e.g. ``http://127.0.0.1:48080``). Used by the in-sandbox - curl for the guest-login dance. - - Returns: - A connected :class:`caido_sdk_client.Client` with a temporary - ``"sandbox"`` project selected. - """ + """Connect to the in-container Caido sidecar and select a fresh project.""" logger.info("Bootstrapping Caido client (host=%s, container=%s)", host_url, container_url) access_token = await _login_as_guest(session, container_url=container_url) diff --git a/strix/runtime/docker_client.py b/strix/runtime/docker_client.py index 318d838..fb6f680 100644 --- a/strix/runtime/docker_client.py +++ b/strix/runtime/docker_client.py @@ -42,12 +42,6 @@ logger = logging.getLogger(__name__) class StrixDockerSandboxClient(DockerSandboxClient): - """``DockerSandboxClient`` subclass that injects Strix-required capabilities. - - Only ``_create_container`` is overridden. All other behavior — image - management, session lifecycle, port resolution, cleanup — is inherited. - """ - async def _create_container( self, image: str, @@ -104,7 +98,7 @@ class StrixDockerSandboxClient(DockerSandboxClient): # Strix injections — append, don't overwrite, so FUSE/SYS_ADMIN survives. cap_add = create_kwargs.setdefault("cap_add", []) - if not isinstance(cap_add, list): # defensive — parent always sets list + if not isinstance(cap_add, list): cap_add = list(cap_add) create_kwargs["cap_add"] = cap_add for cap in ("NET_ADMIN", "NET_RAW"): diff --git a/strix/runtime/session_manager.py b/strix/runtime/session_manager.py index 85a8f54..6f3e273 100644 --- a/strix/runtime/session_manager.py +++ b/strix/runtime/session_manager.py @@ -1,17 +1,4 @@ -"""Per-scan sandbox session lifecycle. - -One session per scan, reused across every agent in that scan's tree. - -The bundle returned by :func:`create_or_reuse` carries the SDK -``client`` + ``session`` plus a ready-to-use Caido client (already -authenticated and pointing at a temporary sandbox project). - -Cache strategy: a module-level dict keyed by ``scan_id``. The same scan -issuing multiple ``create_or_reuse`` calls (e.g., resume after a crash -on the host side) gets the same bundle back. ``cleanup`` is best-effort -— a leaked container is preferable to a stuck cleanup that prevents the -next scan from starting. -""" +"""Per-scan sandbox session lifecycle.""" from __future__ import annotations @@ -34,10 +21,6 @@ logger = logging.getLogger(__name__) _CONTAINER_CAIDO_PORT = 48080 -# Per-scan session cache. Module-level so a scan that bounces through -# multiple host-side processes (e.g., re-imports the module) doesn't -# spin up a second container — though in practice we expect one -# Strix process per scan. _SESSION_CACHE: dict[str, dict[str, Any]] = {} @@ -47,29 +30,16 @@ async def create_or_reuse( image: str, local_sources: list[dict[str, str]], ) -> dict[str, Any]: - """Return the existing bundle for ``scan_id`` or create a new one. + """Return the existing session bundle for ``scan_id`` or create a new one. - Args: - scan_id: Caller-provided scan identifier (used as cache key). - image: Docker image tag (e.g. ``"strix-sandbox:0.2.0"``). - local_sources: Each entry's ``source_path`` (host) is mounted at - ``/workspace/`` inside the container — the - same path the root-task prompt advertises. Empty list means - no host code is mounted (web/IP-only scans). - - Returns the bundle dict containing ``client``, ``session``, and - ``caido_client``. + Each ``local_sources`` entry mounts its host ``source_path`` at + ``/workspace/`` inside the container. """ cached = _SESSION_CACHE.get(scan_id) if cached is not None: logger.info("Reusing existing sandbox session for scan %s", scan_id) return cached - # Build Manifest entries keyed by ``workspace_subdir`` — the SDK - # mounts each at ``/workspace/``, which is exactly the path - # ``build_root_task`` puts in the agent's task prompt. Mounting - # only the listed source dirs (not their parent) avoids leaking - # unrelated host content into the sandbox. entries: dict[str | Path, BaseEntry] = {} for src in local_sources: ws_subdir = src.get("workspace_subdir") or "" diff --git a/strix/skills/__init__.py b/strix/skills/__init__.py index d17c76b..96f94a4 100644 --- a/strix/skills/__init__.py +++ b/strix/skills/__init__.py @@ -9,9 +9,6 @@ logger = logging.getLogger(__name__) _FRONTMATTER_PATTERN = re.compile(r"^---\s*\n.*?\n---\s*\n", re.DOTALL) -# Categories loaded by the prompt template via explicit slash-form paths -# (``scan_modes/``, ``coordination/``). They're not -# user-selectable through ``create_agent``'s ``skills`` argument. _INTERNAL_SKILL_CATEGORIES: frozenset[str] = frozenset({"scan_modes", "coordination"}) @@ -35,7 +32,6 @@ def get_all_skill_names() -> set[str]: def get_available_skills() -> dict[str, list[str]]: - """Return user-selectable skills grouped by category, alphabetised.""" grouped: dict[str, list[str]] = {} for category, name in _iter_user_skill_files(): grouped.setdefault(category, []).append(name) diff --git a/strix/telemetry/logging.py b/strix/telemetry/logging.py index a13d3cc..d58d45f 100644 --- a/strix/telemetry/logging.py +++ b/strix/telemetry/logging.py @@ -1,15 +1,4 @@ -"""Per-scan logging setup. - -Every scan calls :func:`setup_scan_logging` to attach a ``FileHandler`` -to ``{run_dir}/strix.log`` (DEBUG, all ``strix.*`` events) plus a -stderr handler (ERROR-only by default; DEBUG when ``STRIX_DEBUG=1``). -``scan_id`` and ``agent_id`` are pulled from ``ContextVar``s by a -``Filter`` so every log line is auto-tagged without callers passing -them explicitly. - -Third-party loggers (``httpx``, ``litellm``, ``openai``, etc.) are -capped at ``WARNING`` so the file isn't drowned in their internals. -""" +"""Per-scan logging setup.""" from __future__ import annotations @@ -46,8 +35,6 @@ def set_agent_id(agent_id: str | None) -> None: class _StrixContextFilter(logging.Filter): - """Inject ``scan_id`` and ``agent_id`` from ``ContextVar``s onto each record.""" - def filter(self, record: logging.LogRecord) -> bool: record.scan_id = _SCAN_ID.get() or "-" record.agent_id = _AGENT_ID.get() or "-" @@ -73,12 +60,7 @@ _NOISY_LIBS: tuple[str, ...] = ( _HANDLER_TAG = "_strix_scan_handler" -# Logger roots that also receive our scan handlers. ``strix`` covers -# everything we own. ``openai.agents`` is the openai-agents SDK's -# canonical logger (verified: ``agents/logger.py``, ``agents/__init__.py``, -# ``agents/tracing/logger.py`` all use this namespace) — without -# attaching here, SDK-internal Runner / tool-dispatch / model-retry -# events would be invisible. +# ``openai.agents`` is the openai-agents SDK's canonical logger root. _TRACKED_ROOTS: tuple[str, ...] = ("strix", "openai.agents") @@ -144,8 +126,6 @@ def setup_scan_logging(run_dir: Path, *, debug: bool | None = None) -> Callable[ tracked.setLevel(logging.DEBUG) tracked.addHandler(file_handler) tracked.addHandler(stream_handler) - # Stop these records from also bubbling to the python root - # logger's lastResort handler (would double-print to stderr). tracked.propagate = False for name in _NOISY_LIBS: diff --git a/strix/telemetry/posthog.py b/strix/telemetry/posthog.py index 3db7c0f..239e134 100644 --- a/strix/telemetry/posthog.py +++ b/strix/telemetry/posthog.py @@ -45,7 +45,6 @@ def _send(event: str, properties: dict[str, Any]) -> None: with urllib.request.urlopen(req, timeout=10): # noqa: S310 # nosec B310 pass except Exception: # noqa: BLE001 - # Telemetry must never disrupt a scan; log + swallow. logger.debug("posthog send failed for event %s", event, exc_info=True) else: logger.debug("posthog event sent: %s", event) diff --git a/strix/tools/agents_graph/tools.py b/strix/tools/agents_graph/tools.py index 1b917c2..5c1bfd4 100644 --- a/strix/tools/agents_graph/tools.py +++ b/strix/tools/agents_graph/tools.py @@ -1,15 +1,4 @@ -"""Multi-agent graph tools backed by the SDK-native :class:`AgentCoordinator`. - -- ``view_agent_graph``: render the parent/child tree. -- ``send_message_to_agent``: append a message to another agent's SDK session. -- ``wait_for_message``: pause this agent until a message arrives or - ``timeout_seconds`` elapses. -- ``create_agent``: asks the scan runner to spawn an addressable child. -- ``stop_agent``: cancel a running agent (optionally cascading to its - descendants). -- ``agent_finish``: subagents only — posts a structured completion - report to the parent's SDK session and returns a final-output marker. -""" +"""Multi-agent graph tools backed by AgentCoordinator.""" from __future__ import annotations @@ -27,9 +16,6 @@ from strix.core.agents import Status, coordinator_from_context from strix.skills import validate_requested_skills -# An agent is "active" when stop_agent can meaningfully act on it. Anything -# else (completed / stopped / crashed / failed) is terminal — request_stop -# would forcibly overwrite that status, erasing the original outcome. _ACTIVE_STATUSES: frozenset[str] = frozenset({"running", "waiting"}) @@ -117,9 +103,6 @@ async def view_agent_graph(ctx: RunContextWrapper) -> str: for root in roots: render(root, 0) - # Derive per-status counts from the canonical ``Status`` literal so a - # new status added in core.agents auto-flows into this summary without - # the buckets silently going out of sync with the source of truth. counts = Counter(statuses.values()) summary: dict[str, int] = {"total": len(parent_of)} for status_name in get_args(Status): @@ -445,8 +428,6 @@ async def create_agent( default=str, ) - # ``ctx.turn_input`` carries the parent's full conversation up to and - # including the call that's currently invoking ``create_agent``. parent_history = list(ctx.turn_input) if inherit_context and ctx.turn_input else [] try: result = await spawner( diff --git a/strix/tools/notes/tools.py b/strix/tools/notes/tools.py index 7fe3923..e9f2cdf 100644 --- a/strix/tools/notes/tools.py +++ b/strix/tools/notes/tools.py @@ -1,12 +1,4 @@ -"""Per-run notes (shared across agents). - -Module-level dict shared across every agent in the same scan process. -Mirrored to ``{state_dir}/notes.json`` after every CRUD via :func:`_persist` -so a process restart can :func:`hydrate_notes_from_disk` and the resumed -scan picks up exactly where it left off. Concurrent writers are -serialised by ``_notes_lock`` since each tool entry-point dispatches -the impl onto a worker thread via ``asyncio.to_thread``. -""" +"""Per-run notes storage — mirrored to {state_dir}/notes.json.""" from __future__ import annotations @@ -31,20 +23,10 @@ _VALID_NOTE_CATEGORIES = ["general", "findings", "methodology", "questions", "pl _notes_lock = threading.RLock() _DEFAULT_CONTENT_PREVIEW_CHARS = 280 -# On-disk mirror path. Set by :func:`hydrate_notes_from_disk` once per -# scan; unset means "no persistence" (e.g. unit tests). All writes go -# through :func:`_persist`, which is a no-op until the path is set. _notes_path: Path | None = None def hydrate_notes_from_disk(state_dir: Path) -> None: - """Wire the on-disk mirror at ``{state_dir}/notes.json`` and reload it. - - Called by :func:`run_strix_scan` once at scan setup. Subsequent CRUD - calls auto-persist after every mutation. Idempotent on missing file. - Tolerant of corruption — logs and starts empty rather than failing - the scan over a broken sidecar artifact. - """ global _notes_path # noqa: PLW0603 _notes_path = state_dir / "notes.json" with _notes_lock: @@ -76,11 +58,6 @@ def hydrate_notes_from_disk(state_dir: Path) -> None: def _persist() -> None: - """Atomic-rename mirror of ``_notes_storage`` → ``{state_dir}/notes.json``. - - No-op when ``_notes_path`` isn't wired (tests). Errors are logged - and swallowed — a disk hiccup must never tear down the agent's call. - """ path = _notes_path if path is None: return @@ -300,9 +277,6 @@ def _delete_note_impl(note_id: str) -> dict[str, Any]: } -# --- public tools --------------------------------------------------------- - - @function_tool(timeout=30) async def create_note( ctx: RunContextWrapper, diff --git a/strix/tools/proxy/caido_api.py b/strix/tools/proxy/caido_api.py index a45d4cd..926d5c7 100644 --- a/strix/tools/proxy/caido_api.py +++ b/strix/tools/proxy/caido_api.py @@ -55,7 +55,6 @@ _REQ_FIELD_MAP: dict[SortBy, tuple[str, str]] = { def caido_url() -> str: - """Return the in-sandbox Caido endpoint used by ``caido_api``.""" return os.environ.get("STRIX_CAIDO_URL", _DEFAULT_CAIDO_URL).rstrip("/") @@ -83,7 +82,6 @@ def _login_as_guest() -> str: async def get_client() -> Client: - """Return a connected Caido SDK client for the local sandbox sidecar.""" if client := _CLIENT_CACHE.get("default"): return client @@ -95,7 +93,6 @@ async def get_client() -> Client: async def close_client() -> None: - """Close the cached sandbox Caido client, if one was opened.""" client = _CLIENT_CACHE.pop("default", None) if client is None: return @@ -169,10 +166,6 @@ def build_raw_request( return ConnectionInfoInput(host=host, port=port, is_tls=is_tls), raw -# Cap inline response bodies returned through tool results so a single -# large response (HTML pages, JSON dumps) can't blow out the model's -# context. The model can re-fetch the full body via ``view_request`` -# using the captured request id from ``list_requests`` if it needs more. _RESPONSE_BODY_MAX_CHARS = 8192 @@ -286,12 +279,6 @@ def apply_modifications( } -# Hard wall-clock bound on a single replay dispatch. Caido's Replay -# API has no built-in send-side timeout, so a stalled connection -# (unroutable target, slow loopback, etc.) hangs the caller until the -# function_tool wrapper's 120s budget expires — by which point we've -# lost any useful error context. 30s is generous for legitimate HTTP -# and short enough that the model can decide to retry rather than wait. _REPLAY_SEND_TIMEOUT_SECONDS = 30.0 @@ -332,10 +319,6 @@ async def replay_send_raw( "response_raw": None, } elapsed_ms = int((time.time() - started) * 1000) - # ``result.entry.response`` is the parsed Response (with ``.raw`` bytes - # when ``includeResponseRaw`` was True, which is the entries SDK's - # default). The previous ``result.entry.response_raw`` lookup matched - # no attribute on ReplayEntry and silently returned ``None``. response = getattr(result.entry, "response", None) response_raw = getattr(response, "raw", None) if response is not None else None return { @@ -402,7 +385,6 @@ async def list_requests( sort_order: SortOrder = "desc", scope_id: str | None = None, ) -> Any: - """List captured HTTP requests from sandbox Python.""" return await list_requests_with_client( await get_client(), httpql_filter=httpql_filter, @@ -415,7 +397,6 @@ async def list_requests( async def view_request(request_id: str, *, part: RequestPart = "request") -> Any: - """Return one captured request/response from sandbox Python.""" return await get_request_with_client(await get_client(), request_id, part=part) @@ -424,7 +405,6 @@ async def repeat_request( *, modifications: dict[str, Any] | None = None, ) -> dict[str, Any]: - """Replay a captured request after applying request modifications.""" mods = modifications or {} result = await get_request_with_client(await get_client(), request_id, part="request") if result is None or result.request.raw is None: @@ -452,7 +432,6 @@ async def scope_rules( scope_id: str | None = None, scope_name: str | None = None, ) -> Any: - """Manage Caido scope rules from sandbox Python.""" client = await get_client() if action == "list": result = await scope_list(client) @@ -569,9 +548,6 @@ def _clean_sitemap_response(resp: dict[str, Any]) -> dict[str, Any]: out["status_code"] = resp["statusCode"] if resp.get("length"): out["length"] = resp["length"] - # Suppress 0 the same way list_requests does — Caido leaves it unset - # on a lot of proxy-captured traffic and a misleading "0ms" is worse - # than the field simply being absent. if resp.get("roundtripTime"): out["roundtrip_ms"] = resp["roundtripTime"] return out @@ -586,13 +562,11 @@ async def list_sitemap_with_client( page: int = 1, page_size: int = _SITEMAP_PAGE_SIZE, ) -> dict[str, Any]: - """Browse Caido's discovered sitemap. Mirrors main-branch shape. + """Browse Caido's discovered sitemap. The Caido GraphQL ``sitemap*Entries`` operations don't support native pagination, so we fetch all edges for the requested level and slice - client-side. That's fine for typical surface sizes; for very large - sitemaps the caller can drill into ``parent_id`` instead of paging - the root list. + client-side. """ if parent_id: raw = await client.graphql.query( @@ -636,7 +610,6 @@ async def view_sitemap_entry_with_client( client: CaidoClient, entry_id: str, ) -> dict[str, Any]: - """Fetch one sitemap entry plus its recent related requests.""" raw = await client.graphql.query(_SITEMAP_ENTRY_QUERY, variables={"id": entry_id}) entry = raw.get("sitemapEntry") if not entry: @@ -678,7 +651,6 @@ async def list_sitemap( page: int = 1, page_size: int = _SITEMAP_PAGE_SIZE, ) -> dict[str, Any]: - """Sandbox-Python entry point for sitemap browsing.""" return await list_sitemap_with_client( await get_client(), scope_id=scope_id, @@ -690,7 +662,6 @@ async def list_sitemap( async def view_sitemap_entry(entry_id: str) -> dict[str, Any]: - """Sandbox-Python entry point for sitemap entry detail.""" return await view_sitemap_entry_with_client(await get_client(), entry_id) diff --git a/strix/tools/proxy/tools.py b/strix/tools/proxy/tools.py index 78064ff..db8a6b2 100644 --- a/strix/tools/proxy/tools.py +++ b/strix/tools/proxy/tools.py @@ -1,16 +1,4 @@ -"""Caido proxy tools — host-side ``@function_tool`` wrappers. - -The four tools delegate to :mod:`strix.tools.proxy.caido_api` for the actual -caido-sdk-client work and add LLM-friendly JSON serialization + error -wrapping on top. The delegated ``caido_api.py`` module is also copied into -the sandbox image as the importable ``caido_api`` Python module. - -Tools: ``list_requests``, ``view_request``, ``repeat_request``, -``scope_rules``. Arbitrary one-off requests should be made through -``exec_command`` (e.g. ``curl``) — they're captured automatically via -the sandbox's ``HTTP_PROXY`` env, so wrapping them in a Strix tool only -adds an extra layer of indirection. -""" +"""Caido proxy host-side @function_tool wrappers around caido_api.py.""" from __future__ import annotations @@ -40,10 +28,6 @@ if TYPE_CHECKING: SortOrder, ) else: - # Runtime import: ``function_tool`` resolves the annotations via - # ``typing.get_type_hints`` so the Literal aliases must be reachable - # in module globals at decoration time even though they're "only" - # used in annotations. from strix.tools.proxy.caido_api import ( # noqa: TC001 RequestPart, SitemapDepth, @@ -60,8 +44,6 @@ def _ctx_client(ctx: RunContextWrapper) -> Client | None: return inner.get("caido_client") -# Tool-output formatting. Caido SDK returns typed Python objects; function -# tools need compact JSON-safe values for the model and TUI. def _to_tool_json(value: Any) -> Any: """Recursively convert SDK dataclasses/Pydantic objects to tool JSON values.""" if value is None or isinstance(value, str | int | float | bool): @@ -101,9 +83,6 @@ def _err(name: str, exc: Exception) -> str: ) -# ---------------------------------------------------------------------- -# list_requests -# ---------------------------------------------------------------------- @function_tool(timeout=120) async def list_requests( ctx: RunContextWrapper, @@ -231,9 +210,6 @@ async def list_requests( return _err("list_requests", exc) -# ---------------------------------------------------------------------- -# view_request -# ---------------------------------------------------------------------- @function_tool(timeout=60) async def view_request( ctx: RunContextWrapper, @@ -352,9 +328,6 @@ def _format_text_page(content: str, *, page: int, page_size: int) -> dict[str, A } -# ---------------------------------------------------------------------- -# repeat_request -# ---------------------------------------------------------------------- @function_tool(timeout=120, strict_mode=False) async def repeat_request( ctx: RunContextWrapper, @@ -431,9 +404,6 @@ def _format_replay_tool_result(replay: dict[str, Any]) -> str: return json.dumps(payload, ensure_ascii=False, default=str) -# ---------------------------------------------------------------------- -# list_sitemap -# ---------------------------------------------------------------------- @function_tool(timeout=60) async def list_sitemap( ctx: RunContextWrapper, @@ -483,9 +453,6 @@ async def list_sitemap( return _err("list_sitemap", exc) -# ---------------------------------------------------------------------- -# view_sitemap_entry -# ---------------------------------------------------------------------- @function_tool(timeout=60) async def view_sitemap_entry( ctx: RunContextWrapper, @@ -511,9 +478,6 @@ async def view_sitemap_entry( return _err("view_sitemap_entry", exc) -# ---------------------------------------------------------------------- -# scope_rules -# ---------------------------------------------------------------------- @function_tool(timeout=60) async def scope_rules( ctx: RunContextWrapper, @@ -612,7 +576,6 @@ async def scope_rules( return json.dumps( {"success": True, "scope": _to_tool_json(scope)}, ensure_ascii=False, default=str ) - # action == "delete" — exhaustive Literal if not scope_id: return json.dumps( {"success": False, "error": "Scope_id is required for action='delete'"}, diff --git a/strix/tools/reporting/tool.py b/strix/tools/reporting/tool.py index 18f4148..3fdcfec 100644 --- a/strix/tools/reporting/tool.py +++ b/strix/tools/reporting/tool.py @@ -298,10 +298,6 @@ async def _do_create( # noqa: PLR0912 } -# Generous timeout: the dedup check makes a separate LLM call, and -# large scans can have many existing reports to compare against. -# strict_mode=False because cvss_breakdown is a dict[str, str] and -# code_locations is list[dict] — both free-form for the strict schema. @function_tool(timeout=180, strict_mode=False) async def create_vulnerability_report( ctx: RunContextWrapper, diff --git a/strix/tools/todo/tools.py b/strix/tools/todo/tools.py index 282f950..07761f5 100644 --- a/strix/tools/todo/tools.py +++ b/strix/tools/todo/tools.py @@ -1,14 +1,4 @@ -"""Per-agent todo tools. - -Per-agent in-memory dict, scoped via ``ctx.context['agent_id']``. The -table is mirrored to ``{state_dir}/todos.json`` after every mutation so a -process restart can ``hydrate_todos_from_disk`` and each respawned -agent finds its prior list intact. The persistence is best-effort — -errors are logged and swallowed so a disk failure can't kill the agent -mid-call. Bulk forms are preserved so the prompt-template documentation -still works (``todos`` / ``updates`` / ``todo_ids`` accept JSON strings -or comma-separated strings). -""" +"""Per-agent todo tools — mirrored to {state_dir}/todos.json.""" from __future__ import annotations @@ -42,26 +32,13 @@ def _todo_sort_key(todo: dict[str, Any]) -> tuple[int, int, str]: ) -# Per-agent silo: ``_todos_storage[agent_id][todo_id] = todo_dict``. -# Keyed by ``ctx.context['agent_id']`` so two agents in the same scan -# don't see each other's lists. _todos_storage: dict[str, dict[str, dict[str, Any]]] = {} -# On-disk mirror path. Set by ``hydrate_todos_from_disk`` once per scan; -# unset means "no persistence" (e.g. unit tests). All writes go through -# ``_persist`` which is a no-op until the path is set. _todos_path: Path | None = None _todos_io_lock = threading.RLock() def hydrate_todos_from_disk(state_dir: Path) -> None: - """Wire the on-disk mirror at ``{state_dir}/todos.json`` and reload it. - - Called by :func:`run_strix_scan` once at scan setup. Subsequent CRUD - calls auto-persist after every mutation. Idempotent on missing file. - Tolerant of corruption — logs and starts empty rather than failing - the scan over a broken sidecar artifact. - """ global _todos_path # noqa: PLW0603 _todos_path = state_dir / "todos.json" with _todos_io_lock: @@ -99,11 +76,6 @@ def hydrate_todos_from_disk(state_dir: Path) -> None: def _persist() -> None: - """Atomic-rename mirror of ``_todos_storage`` → ``{state_dir}/todos.json``. - - No-op when ``_todos_path`` isn't wired (tests). Errors are logged - and swallowed. - """ path = _todos_path if path is None: return @@ -284,9 +256,6 @@ def _apply_single_update( return None -# --- public tools --------------------------------------------------------- - - @function_tool(timeout=30) async def create_todo(ctx: RunContextWrapper, todos: str) -> str: """Create one or many todos for the current agent. diff --git a/strix/tools/web_search/tool.py b/strix/tools/web_search/tool.py index 1d202f9..0448275 100644 --- a/strix/tools/web_search/tool.py +++ b/strix/tools/web_search/tool.py @@ -67,9 +67,6 @@ def _do_search(query: str) -> dict[str, Any]: # noqa: PLR0911 - each error clas ], } - # Internal details (upstream URL, HTTP status, library exception text) stay - # in the logs; the model only ever sees a short actionable category so it - # can decide whether to retry, refine, or work around the gap. try: response = requests.post(url, headers=headers, json=payload, timeout=300) response.raise_for_status() @@ -121,8 +118,6 @@ def _do_search(query: str) -> dict[str, Any]: # noqa: PLR0911 - each error clas } -# Perplexity request timeout is 300s; give the SDK a slightly larger -# budget so the round-trip + JSON decode doesn't push us over. @function_tool(timeout=330) async def web_search(ctx: RunContextWrapper, query: str) -> str: """Real-time web search via Perplexity — your primary research tool.