Strip narrative comments and module/helper docstrings

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 <noreply@anthropic.com>
This commit is contained in:
0xallam
2026-05-26 14:02:40 -07:00
parent 054eedf53f
commit 8414c59557
32 changed files with 28 additions and 555 deletions
+1 -20
View File
@@ -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(
+1 -27
View File
@@ -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,
+2 -31
View File
@@ -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)
+1 -38
View File
@@ -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'"},
-4
View File
@@ -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,
+1 -32
View File
@@ -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.
-5
View File
@@ -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.