diff --git a/strix/agents/prompts/system_prompt.jinja b/strix/agents/prompts/system_prompt.jinja
index 3b53cab..ccce5dc 100644
--- a/strix/agents/prompts/system_prompt.jinja
+++ b/strix/agents/prompts/system_prompt.jinja
@@ -359,77 +359,6 @@ PERSISTENCE IS MANDATORY:
- There are ALWAYS more attack vectors to explore
-
-Tool call format:
-
-value
-
-
-CRITICAL RULES:
-{% if interactive %}
-0. When using tools, include exactly one tool call per message. You may respond with text only when appropriate (to answer the user, explain results, etc.).
-{% else %}
-0. While active in the agent loop, EVERY message you output MUST be a single tool call. Do not send plain text-only responses.
-{% endif %}
-1. Exactly one tool call per message — never include more than one ... block in a single LLM message.
-2. Tool call must be last in message
-3. EVERY tool call MUST end with . This is MANDATORY. Never omit the closing tag. End your response immediately after .
-4. Use ONLY the exact format shown above. NEVER use JSON/YAML/INI or any other syntax for tools or parameters.
-5. When sending ANY multi-line content in tool parameters, use real newlines (actual line breaks). Do NOT emit literal "\n" sequences. Literal "\n" instead of real line breaks will cause tools to fail.
-6. Tool names must match exactly the tool "name" defined (no module prefixes, dots, or variants).
-7. Parameters must use value exactly. Do NOT pass parameters as JSON or key:value lines. Do NOT add quotes/braces around values.
-{% if interactive %}
-8. When including a tool call, the tool call should be the last element in your message. You may include brief explanatory text before it.
-{% else %}
-8. Do NOT wrap tool calls in markdown/code fences or add any text before or after the tool block.
-{% endif %}
-
-CORRECT format — use this EXACTLY:
-
-value
-
-
-WRONG formats — NEVER use these:
-- value
-- ...
-- ...
-- {"tool_name": {"param_name": "value"}}
-- ```...```
-- value_without_parameter_tags
-
-EVERY argument MUST be wrapped in ... tags. NEVER put values directly in the function body without parameter tags. This WILL cause the tool call to fail.
-
-Do NOT emit any extra XML tags in your output. In particular:
-- NO ... or ... blocks
-- NO ... or ... blocks
-- NO ... or ... wrappers
-{% if not interactive %}
-If you need to reason, use the think tool. Your raw output must contain ONLY the tool call — no surrounding XML tags.
-{% else %}
-If you need to reason, use the think tool. When using tools, do not add surrounding XML tags.
-{% endif %}
-
-Notice: use NOT , use NOT , use NOT .
-
-Example (terminal tool):
-
-nmap -sV -p 1-1000 target.com
-
-
-Example (agent creation tool):
-
-Perform targeted XSS testing on the search endpoint
-XSS Discovery Agent
-xss
-
-
-SPRAYING EXECUTION NOTE:
-- When performing large payload sprays or fuzzing, encapsulate the entire spraying loop inside a single python tool call when you are writing Python logic (for example asyncio/aiohttp). Use terminal tool only when invoking an external CLI/fuzzer. Do not issue one tool call per payload.
-- Favor batch-mode CLI tools (sqlmap, ffuf, nuclei, zaproxy, arjun) where appropriate and check traffic via the proxy when beneficial
-
-REMINDER: Always close each tool call with before going into the next. Incomplete tool calls will fail.
-
-
Docker container with Kali Linux and comprehensive security tools:
diff --git a/strix/tools/agents_graph/tools.py b/strix/tools/agents_graph/tools.py
index 5b49304..d9a78db 100644
--- a/strix/tools/agents_graph/tools.py
+++ b/strix/tools/agents_graph/tools.py
@@ -57,11 +57,14 @@ def _dump(result: dict[str, Any]) -> str:
@strix_tool(timeout=30)
async def view_agent_graph(ctx: RunContextWrapper) -> str:
- """Render the multi-agent tree starting from each root.
+ """Print the multi-agent tree — every agent, its parent, its status.
- Output is a single string the model can parse: indented bullet list,
- one line per agent, status in brackets. Roots are agents whose
- ``parent_of[id]`` is ``None``.
+ Use before spawning a new agent (don't duplicate work — check whether
+ something specialized for that task already exists) and any time you
+ want a snapshot of who's still ``running`` / ``waiting`` /
+ ``completed`` / ``crashed`` / ``stopped``. Output is an indented
+ bullet list with status in brackets; the agent that called this tool
+ is marked ``← you``.
"""
inner = ctx.context if isinstance(ctx.context, dict) else {}
bus = inner.get("bus")
@@ -107,7 +110,17 @@ async def view_agent_graph(ctx: RunContextWrapper) -> str:
@strix_tool(timeout=30)
async def agent_status(ctx: RunContextWrapper, agent_id: str) -> str:
- """Inspect one agent's lifecycle state and pending message count."""
+ """Look up one agent's lifecycle state + pending message count.
+
+ Use when you need precise state on a specific agent (e.g., "is the
+ XSS specialist still going?") rather than the full tree view.
+ Returns ``status`` (``running`` / ``waiting`` / ``completed`` /
+ ``crashed`` / ``stopped``), ``parent_id``, and ``pending_messages``.
+
+ Args:
+ agent_id: The 8-char id from ``view_agent_graph`` /
+ ``create_agent``.
+ """
inner = ctx.context if isinstance(ctx.context, dict) else {}
bus = inner.get("bus")
if bus is None:
@@ -141,12 +154,29 @@ async def send_message_to_agent(
message_type: Literal["query", "instruction", "information"] = "information",
priority: Literal["low", "normal", "high", "urgent"] = "normal",
) -> str:
- """Queue a message for another agent's inbox.
+ """Send a message to another agent's inbox — sparingly.
- The target's next ``inject_messages_filter`` pass (top of its next LLM
- turn) drains the inbox and surfaces the message wrapped in
- ````. Messages to a finalized agent are dropped
- silently by the bus (C13).
+ Inter-agent messages are surfaced at the top of the target's next
+ LLM turn. Use only when essential:
+
+ - Sharing a discovered finding/credential another agent needs.
+ - Asking a specialist a focused question.
+ - Coordinating who covers what (avoid overlap).
+ - Telling a child to wrap up or change course.
+
+ **Don't** use for routine "hello/status" pings, for context the
+ target already has (children inherit parent history), or when
+ parent/child completion via ``agent_finish`` already covers the
+ flow. Messages to a finalized agent are dropped.
+
+ Args:
+ target_agent_id: Recipient's 8-char id.
+ message: The full message body. Be specific — include payloads,
+ URLs, or what you want them to do, not just headlines.
+ message_type: ``query`` (you want a reply), ``instruction``
+ (you're directing them), ``information`` (FYI, no reply
+ expected). Default ``information``.
+ priority: ``low`` / ``normal`` / ``high`` / ``urgent``.
"""
inner = ctx.context if isinstance(ctx.context, dict) else {}
bus = inner.get("bus")
@@ -205,16 +235,31 @@ async def wait_for_message(
reason: str = "Waiting for messages from other agents",
timeout_seconds: int = 600,
) -> str:
- """Block this agent's turn until a message arrives or ``timeout_seconds``.
+ """Pause this agent until a message lands in its inbox (or timeout).
- Implementation polls ``bus.inboxes`` once per second. Cheaper than an
- asyncio.Event because the message bus already serializes through its
- own lock — a missed wakeup on Event would be subtle to debug, while
- polling is trivially correct.
+ Use when you have nothing useful to do until a child/peer responds
+ — typically after spawning subagents and you want to wait for
+ their completion reports. The agent automatically resumes when any
+ message arrives.
+
+ **Critical caveats:**
+
+ - **Never** call this if you finished your own task and have **no**
+ child agents running — that's a permanent stall. Call
+ ``finish_scan`` (root) or ``agent_finish`` (subagent) instead.
+ - If you're waiting on an agent that **isn't your child**, message
+ it first asking it to ping you when done — otherwise it has no
+ reason to send to your inbox and you'll wait the full timeout.
+ - Children update the parent automatically via ``agent_finish``
+ → no extra coordination needed.
Args:
- reason: Human-readable note shown in graph snapshots while waiting.
- timeout_seconds: Cap on the wait. 600s matches the legacy default.
+ reason: One-line note shown in graph snapshots while you're
+ waiting (helps a human or sibling agent debug who's stuck
+ on what).
+ timeout_seconds: Hard cap (default 600s). On timeout the tool
+ returns and you decide whether to keep working or wait
+ again.
"""
inner = ctx.context if isinstance(ctx.context, dict) else {}
bus = inner.get("bus")
@@ -268,22 +313,44 @@ async def create_agent(
inherit_context: bool = True,
skills: list[str] | None = None,
) -> str:
- """Spawn a child agent that runs in parallel via ``asyncio.create_task``.
+ """Spawn a specialist child agent to run in parallel.
- The child's ``Runner.run`` task handle is stored in ``bus.tasks[child_id]``
- so a root-level cancel can cascade to descendants (C9). The child is
- registered with the bus before the task starts so messages aimed at it
- don't get dropped during the brief register→start window.
+ Decompose complex pentests by handing focused subtasks to dedicated
+ children. The child runs asynchronously — the parent continues
+ immediately and can ``wait_for_message`` later (or just keep
+ working in parallel). When the child calls ``agent_finish``, its
+ completion report lands in the parent's inbox.
+
+ **Before spawning, call ``view_agent_graph``** to confirm no
+ existing agent already covers this scope — duplicate specialists
+ waste turns and create coordination headaches.
+
+ **Specialization principles:**
+
+ - Most agents need at least one ``skill`` to be useful.
+ - Aim for **1-3 related skills** per agent. Up to 5 only when the
+ task genuinely spans them.
+ - One skill = most focused (e.g., XSS-only). Five skills = upper
+ bound.
+ - Match the ``name`` to the focus (``XSS Specialist``,
+ ``SQLi Validator``, ``Auth Specialist``).
+
+ **When to spawn vs do it yourself:**
+
+ - Spawn when the subtask is large, parallelizable, or needs
+ different specialization than what you're already doing.
+ - Don't spawn for trivial one-shot probes — just run the tool
+ yourself.
Args:
- name: Human-readable child name (also stored in ``bus.names``).
- task: The task description handed to the child agent.
- inherit_context: When True, the child receives a copy of the parent's
- input items as background context, wrapped in
- ````. Default True.
- skills: Optional list of skill names the child should preload.
-
- Returns a JSON-encoded ``{"success": ..., "agent_id": ...}``.
+ name: Human-readable child name (used in graph views and
+ ``send_message_to_agent`` flows).
+ task: Specific objective. Be concrete — what to test, what
+ success looks like, any constraints.
+ inherit_context: Default ``True``. The child receives the
+ parent's input history as background; only set ``False``
+ when starting a clean-slate task.
+ skills: Comma-separated skill names. Max 5; prefer 1-3.
"""
inner = ctx.context if isinstance(ctx.context, dict) else {}
bus = inner.get("bus")
@@ -412,15 +479,39 @@ async def agent_finish(
report_to_parent: bool = True,
final_recommendations: list[str] | None = None,
) -> str:
- """Subagent-only termination: post a completion report and signal the SDK.
+ """Subagent termination — post a completion report to the parent.
- Sets ``ctx.context['agent_finish_called'] = True`` so the on_agent_end
- hook records "completed" rather than "crashed". The SDK terminates the
- child's loop because every child is built with
- ``tool_use_behavior={"stop_at_tool_names": ["agent_finish"]}`` (C4).
+ **Subagents only.** Root agents must call ``finish_scan`` instead;
+ this tool refuses to run for root agents. Calling this:
- Root agents must call ``finish_scan`` instead. This tool refuses to run
- when ``parent_id`` is None.
+ 1. Marks the subagent as ``completed``.
+ 2. Posts a structured ```` to the
+ parent's inbox (when ``report_to_parent`` is true).
+ 3. Stops this subagent's execution.
+
+ **Vulnerability findings must already be filed via
+ ``create_vulnerability_report`` before calling this.** The
+ ``findings`` field here is for narrative summary only — it does
+ not register vulns in the scan report.
+
+ Write the summary as if the parent has no idea what you were
+ doing: what did you test, what did you find/confirm/rule out,
+ what's still open.
+
+ Args:
+ result_summary: What you accomplished and discovered. Concrete
+ and specific (URLs, parameters, payloads that worked).
+ findings: Optional bullet list of confirmed observations. For
+ credit-bearing vulnerabilities, file
+ ``create_vulnerability_report`` first; this is for
+ narrative.
+ success: Whether the assigned subtask was completed
+ successfully. Default ``True``.
+ report_to_parent: Whether to deliver the completion report to
+ the parent's inbox. Default ``True``.
+ final_recommendations: Optional next-step suggestions for the
+ parent (e.g., "prioritize testing X", "spawn an agent to
+ cover Y").
"""
inner = ctx.context if isinstance(ctx.context, dict) else {}
bus = inner.get("bus")
diff --git a/strix/tools/browser/tool.py b/strix/tools/browser/tool.py
index 56abac6..01e6d11 100644
--- a/strix/tools/browser/tool.py
+++ b/strix/tools/browser/tool.py
@@ -67,20 +67,75 @@ async def browser_action(
file_path: str | None = None,
clear: bool = False,
) -> str:
- """Drive the sandboxed Playwright browser.
+ """Drive the sandboxed Playwright browser (Chromium, headless).
+
+ The browser is **persistent** — state survives across calls and tabs
+ until you ``close``. Browser interaction must start with ``launch``
+ and end with ``close``. Multiple tabs are supported; the first tab
+ after ``launch`` is ``"tab_1"`` and new tabs are numbered
+ sequentially.
+
+ **Click coordinates** — derive them from the most recent screenshot.
+ Target the *center* of the element, not the edge. After clicking,
+ verify success against the next screenshot. Bad coordinates are the
+ most common reason clicks silently fail.
+
+ **JavaScript execution** (``execute_js``):
+
+ - Code runs in the page context with full DOM access.
+ - The **last evaluated expression is auto-returned** — do not use
+ ``return`` (it breaks evaluation).
+ - For an object literal as the final expression, wrap in parentheses:
+ ``({title: document.title, url: location.href})``.
+ - ``await`` is supported: ``await fetch(location.href).then(r => r.status)``.
+ - Variables from your tool context are NOT available — pass data
+ via the URL or DOM if you need to thread it through.
+ - The ``js_code`` parameter is executed as-is; no escaping needed,
+ single- or multi-line both work.
+
+ **Form filling** — click the field first, then ``type`` the text.
+
+ **Tabs** — actions affect the currently active tab unless ``tab_id``
+ is set. Always keep at least one tab open. Close tabs you don't need
+ with ``close_tab``, and ``close`` the browser when you're fully done.
+
+ **Concurrency** — the browser session can run alongside terminal /
+ python tool calls in subsequent turns; nothing in the browser is
+ serialized against other tools.
+
+ Special keys for ``press_key``: single chars ``a``-``z`` / ``0``-``9``,
+ ``Enter`` / ``Escape`` / ``Tab`` / ``Space`` / ``ArrowLeft`` /
+ ``ArrowRight`` / ``ArrowUp`` / ``ArrowDown``, modifiers ``Shift`` /
+ ``Control`` / ``Alt`` / ``Meta``, function keys ``F1``-``F12``.
+
+ Returns: a JSON dict with ``screenshot`` (base64 PNG), ``url``,
+ ``title``, ``viewport``, ``tab_id``, ``all_tabs``. Per-action extras:
+ ``js_result`` for ``execute_js``, ``pdf_saved`` for ``save_pdf``,
+ ``console_logs`` (≤50 KB / ≤200 most recent) for ``get_console_logs``,
+ ``page_source`` (truncated to 100 KB) for ``view_source``.
Args:
- action: The browser action to dispatch — see ``BrowserAction``
- literal for the full set.
- url: Required for ``launch`` / ``goto`` / ``new_tab`` (with URL).
- coordinate: ``"x,y"`` pixel target for click/hover/double_click.
+ action: One of: ``launch``, ``goto``, ``click``, ``type``,
+ ``scroll_down``, ``scroll_up``, ``back``, ``forward``,
+ ``new_tab``, ``switch_tab``, ``close_tab``, ``list_tabs``,
+ ``wait``, ``execute_js``, ``double_click``, ``hover``,
+ ``press_key``, ``save_pdf``, ``get_console_logs``,
+ ``view_source``, ``close``.
+ url: Required for ``launch`` / ``goto``; optional for
+ ``new_tab``. Must include the protocol (e.g.
+ ``https://...``, ``file://...``).
+ coordinate: ``"x,y"`` pixel target for ``click`` / ``double_click``
+ / ``hover``. Format example: ``"432,321"``. Must be within
+ viewport.
text: Required for ``type``.
- tab_id: Optional explicit tab targeting; defaults to the active tab.
+ tab_id: Required for ``switch_tab`` / ``close_tab``; optional
+ elsewhere to target a specific tab.
js_code: Required for ``execute_js``.
- duration: Seconds to wait for ``wait`` action.
- key: Required for ``press_key`` (e.g. ``"Enter"``, ``"Escape"``).
+ duration: Seconds for ``wait`` (fractional OK, e.g. ``0.5``).
+ key: Required for ``press_key``.
file_path: Required for ``save_pdf``.
- clear: For ``type``, clears the field first.
+ clear: For ``get_console_logs``, clear logs after retrieval
+ (default False).
"""
return _dump(
await post_to_sandbox(
diff --git a/strix/tools/file_edit/tools.py b/strix/tools/file_edit/tools.py
index e5d81c5..4a9ae0c 100644
--- a/strix/tools/file_edit/tools.py
+++ b/strix/tools/file_edit/tools.py
@@ -37,16 +37,34 @@ async def str_replace_editor(
new_str: str | None = None,
insert_line: int | None = None,
) -> str:
- """View, create, or edit a file in the sandbox.
+ """View, create, or edit a file in the sandbox filesystem.
+
+ Commands:
+
+ - ``view`` — show file contents. Optionally restrict to a line range
+ via ``view_range`` (1-indexed; ``[start, -1]`` for "from start to
+ end of file").
+ - ``create`` — write a new file with ``file_text``. Use this for
+ exploit scripts, PoCs, helper modules, etc.
+ - ``str_replace`` — find ``old_str`` in the file and replace with
+ ``new_str``. ``old_str`` must be unique in the file; include
+ enough surrounding context to make it so.
+ - ``insert`` — insert ``new_str`` after line ``insert_line``.
+ - ``undo_edit`` — revert the most recent edit to ``path``.
+
+ Multi-line ``new_str`` / ``old_str`` / ``file_text`` use real
+ newlines, not literal ``\\n``.
Args:
- command: One of ``"view" | "create" | "str_replace" | "insert" |
- "undo_edit"``.
- path: File path. Relative paths are anchored at ``/workspace``.
+ command: ``view`` / ``create`` / ``str_replace`` / ``insert`` /
+ ``undo_edit``.
+ path: File path. Relative paths anchor at ``/workspace``.
file_text: Required for ``create``.
- view_range: Optional ``[start, end]`` line range for ``view``.
- old_str / new_str: Required for ``str_replace``.
- insert_line: Required for ``insert``.
+ view_range: Optional ``[start, end]`` (1-indexed) for ``view``.
+ old_str: Required for ``str_replace`` — must be unique in file.
+ new_str: Required for ``str_replace`` and ``insert``.
+ insert_line: Required for ``insert``; new content goes AFTER
+ this line.
"""
return _dump(
await post_to_sandbox(
@@ -73,9 +91,12 @@ async def list_files(
) -> str:
"""List files and directories under a sandbox path.
+ Output is sorted alphabetically and capped at 500 entries to avoid
+ flooding the model with huge directory trees.
+
Args:
- path: Directory path, relative paths anchored at ``/workspace``.
- recursive: When True, walks subdirectories (capped at 500 entries).
+ path: Directory path; relative paths anchor at ``/workspace``.
+ recursive: When True, walks subdirectories.
"""
return _dump(
await post_to_sandbox(
@@ -93,12 +114,17 @@ async def search_files(
regex: str,
file_pattern: str = "*",
) -> str:
- """Recursively grep files in the sandbox using ripgrep.
+ """Recursively regex-search files in the sandbox using ripgrep.
+
+ Fast — uses ``rg`` under the hood. Walks subdirectories. Use this
+ for code-pattern hunts (``def\\s+authenticate``, ``API_KEY``,
+ secrets, etc.) when you don't already know the file.
Args:
- path: Root path to search; relative paths anchored at ``/workspace``.
- regex: Pattern to match (passed straight to ``rg``).
- file_pattern: Glob filter (e.g. ``"*.py"``). Defaults to all files.
+ path: Root path to search. Relative paths anchor at ``/workspace``.
+ regex: Pattern to match (PCRE-style; passed straight to ``rg``).
+ file_pattern: Glob filter (e.g. ``"*.py"``, ``"*.{js,ts}"``).
+ Defaults to all files.
"""
return _dump(
await post_to_sandbox(
diff --git a/strix/tools/finish/tool.py b/strix/tools/finish/tool.py
index 8ff5f21..4b5f84c 100644
--- a/strix/tools/finish/tool.py
+++ b/strix/tools/finish/tool.py
@@ -79,16 +79,57 @@ async def finish_scan(
technical_analysis: str,
recommendations: str,
) -> str:
- """Finalize the scan and persist the four executive summary sections.
+ """Finalize the scan — persist the customer-facing report.
- Only the root agent should call this. Subagents must use
- ``agent_finish`` (from the multi-agent graph tools) instead.
+ **Root-agent only.** Subagents must call ``agent_finish`` from the
+ multi-agent graph tools instead. Calling this finalizes everything:
+
+ 1. Verifies you are the root agent.
+ 2. Writes the four narrative sections to the scan record.
+ 3. Marks the scan completed and stops execution.
+
+ **Pre-flight checklist:**
+
+ - All vulnerabilities you found are filed via
+ ``create_vulnerability_report`` (un-reported findings are not
+ tracked and not credited).
+ - All subagents have terminated. If any are still ``running`` /
+ ``stopping``, message them or use ``wait_for_message``.
+ - Don't double-report — one report per distinct vulnerability.
+
+ **Calling this multiple times overwrites the previous report.**
+ Make the single call comprehensive.
+
+ **Customer-facing report rules** (this output is rendered into the
+ final PDF the client sees):
+
+ - Never mention internal infrastructure: no local/absolute paths
+ (``/workspace/...``), no agent names, no sandbox/orchestrator/
+ tooling references, no system prompts, no model-internal errors.
+ - Tone: formal, third-person, objective, concise. This is a
+ consultant deliverable, not an engineering log.
+ - Each section has a specific role:
+
+ - ``executive_summary`` — for non-technical leadership. Risk
+ posture, business impact (data exposure / compliance /
+ reputation), notable criticals, overarching remediation
+ theme.
+ - ``methodology`` — frameworks followed (OWASP WSTG, PTES,
+ OSSTMM, NIST), engagement type (black/gray/white box), scope
+ and constraints, categories of testing performed. **No**
+ internal execution detail.
+ - ``technical_analysis`` — consolidated findings overview with
+ severity model and systemic root causes. Reference individual
+ vuln reports for repro steps; don't duplicate raw evidence.
+ - ``recommendations`` — prioritized actions grouped by urgency
+ (Immediate / Short-term / Medium-term), each with concrete
+ remediation steps. End with retest/validation guidance.
Args:
- executive_summary: High-level scan outcome.
- methodology: Approach taken.
- technical_analysis: Findings detail across the engagement.
- recommendations: Prioritized fix list.
+ executive_summary: Business-level summary for leadership.
+ methodology: Frameworks, scope, and approach.
+ technical_analysis: Consolidated findings + systemic themes.
+ recommendations: Prioritized, actionable remediation.
"""
inner = ctx.context if isinstance(ctx.context, dict) else {}
result = await asyncio.to_thread(
diff --git a/strix/tools/notes/tools.py b/strix/tools/notes/tools.py
index d081293..92cd975 100644
--- a/strix/tools/notes/tools.py
+++ b/strix/tools/notes/tools.py
@@ -425,11 +425,37 @@ async def create_note(
category: str = "general",
tags: list[str] | None = None,
) -> str:
- """Create a note in the current run's notes store.
+ """Document an observation, finding, methodology step, or research note.
- Notes are persisted to ``run_dir/notes/notes.jsonl`` and (for the
- ``wiki`` category) rendered as Markdown to
- ``run_dir/wiki/.md``.
+ Notes are your **shared run memory** — they're visible to every
+ agent in the same scan and persist to ``run_dir/notes/notes.jsonl``
+ (replayable event log). Wiki-category notes are additionally
+ rendered as Markdown under ``run_dir/wiki/.md``.
+
+ For actionable tasks, use ``todo`` instead — notes are for capturing
+ information, todos are for tracking work.
+
+ Categories:
+
+ - ``general`` — default, anything that doesn't fit elsewhere.
+ - ``findings`` — confirmed vulnerabilities or weaknesses (write
+ these up promptly; you'll cite them when filing reports).
+ - ``methodology`` — what you tried, what worked, what didn't —
+ useful for the final scan report.
+ - ``questions`` — open questions / things to come back to.
+ - ``plan`` — multi-step plans you want to track.
+ - ``wiki`` — repository or target source maps shared across agents
+ in the same run. Use this for codebase architecture notes the
+ whole agent tree should see.
+
+ Tags are free-form (e.g. ``["sqli", "auth", "critical"]``) — useful
+ for later ``list_notes(tags=...)`` filtering.
+
+ Args:
+ title: Short headline.
+ content: Full note body. Markdown is preserved.
+ category: One of the categories above. Default ``"general"``.
+ tags: Optional free-form tags.
"""
del ctx
return _dump(
@@ -445,7 +471,24 @@ async def list_notes(
search: str | None = None,
include_content: bool = False,
) -> str:
- """List notes, optionally filtered by category / tags / substring."""
+ """List existing notes — metadata-first by default.
+
+ Filters compose: passing ``category="findings"`` and
+ ``tags=["sqli"]`` returns notes that are *both* in the findings
+ category AND have at least one of those tags.
+
+ By default each entry includes a ``content_preview`` (first 280
+ chars). Set ``include_content=True`` to get full bodies — useful
+ when you need to scan many notes; expensive in tokens for large
+ notes.
+
+ Args:
+ category: Filter by category.
+ tags: Filter to notes that have any of these tags.
+ search: Substring match against title and content.
+ include_content: When False (default) entries have a preview;
+ when True the full ``content`` is included.
+ """
del ctx
return _dump(
await asyncio.to_thread(
@@ -460,7 +503,11 @@ async def list_notes(
@strix_tool(timeout=30)
async def get_note(ctx: RunContextWrapper, note_id: str) -> str:
- """Fetch one note by its 5-char ID. Returns full content."""
+ """Fetch one note by its 5-char ID. Returns the full content.
+
+ Args:
+ note_id: Note id from ``create_note`` or a ``list_notes`` entry.
+ """
del ctx
return _dump(await asyncio.to_thread(_get_note_impl, note_id))
@@ -473,7 +520,18 @@ async def update_note(
content: str | None = None,
tags: list[str] | None = None,
) -> str:
- """Update a note's title, content, or tags."""
+ """Update a note's title, content, or tags.
+
+ Pass ``None`` for any field you want left unchanged. Replacing
+ ``content`` is a full overwrite — to append, fetch first with
+ ``get_note``, concat, and pass the result.
+
+ Args:
+ note_id: Target note's 5-char ID.
+ title: New title, or ``None`` to keep.
+ content: New content, or ``None`` to keep.
+ tags: New tags list, or ``None`` to keep.
+ """
del ctx
return _dump(
await asyncio.to_thread(
@@ -488,6 +546,10 @@ async def update_note(
@strix_tool(timeout=30)
async def delete_note(ctx: RunContextWrapper, note_id: str) -> str:
- """Delete a note. For wiki notes, also removes the rendered Markdown file."""
+ """Delete a note. For wiki notes, also removes the rendered Markdown file.
+
+ Args:
+ note_id: Note id to delete.
+ """
del ctx
return _dump(await asyncio.to_thread(_delete_note_impl, note_id))
diff --git a/strix/tools/proxy/tools.py b/strix/tools/proxy/tools.py
index ac6c04c..518f212 100644
--- a/strix/tools/proxy/tools.py
+++ b/strix/tools/proxy/tools.py
@@ -50,15 +50,35 @@ async def list_requests(
sort_order: SortOrder = "desc",
scope_id: str | None = None,
) -> str:
- """List captured HTTP requests from the Caido proxy.
+ """List captured HTTP requests from the Caido proxy with HTTPQL filtering.
+
+ Caido HTTPQL syntax (operators differ by field type):
+
+ - **Integer fields** (``resp.code``, ``req.port``, ``id``,
+ ``roundtrip``) — ``eq``, ``gt``, ``gte``, ``lt``, ``lte``, ``ne``.
+ Examples: ``resp.code.eq:200``, ``resp.code.gte:400``,
+ ``req.port.eq:443``.
+ - **Text/byte fields** (``req.method``, ``req.host``, ``req.path``,
+ ``req.query``, ``req.ext``, ``req.raw``) — ``regex``, ``cont``
+ (substring), ``eq``. Examples: ``req.method.eq:"POST"``,
+ ``req.path.cont:"/api/"``, ``req.host.regex:".*\\.example\\.com"``.
+ - **Date fields** (``req.created_at``) — ``gt``, ``lt`` with ISO
+ timestamps: ``req.created_at.gt:"2024-01-01T00:00:00Z"``.
+ - **Combine** with ``AND`` / ``OR``: ``req.method.eq:"POST" AND
+ resp.code.gte:400``.
+ - **Special**: ``source:intercept`` (only intercepted requests),
+ ``preset:"name"``.
Args:
- httpql_filter: Caido HTTPQL query (e.g. ``"resp.code:eq:500"``).
- start_page / end_page: Inclusive page range to return.
- page_size: Entries per page; default 50.
- sort_by: Field to sort by.
- sort_order: ``"asc"`` or ``"desc"``.
- scope_id: Restrict to a specific scope.
+ httpql_filter: Caido HTTPQL query.
+ start_page: Starting page, 1-indexed.
+ end_page: Ending page (inclusive).
+ page_size: Entries per page (default 50).
+ sort_by: ``timestamp`` / ``host`` / ``method`` / ``path`` /
+ ``status_code`` / ``response_time`` / ``response_size`` /
+ ``source``.
+ sort_order: ``asc`` or ``desc``.
+ scope_id: Restrict to a scope (managed via ``scope_rules``).
"""
return _dump(
await post_to_sandbox(
@@ -86,7 +106,31 @@ async def view_request(
page: int = 1,
page_size: int = 50,
) -> str:
- """View a single captured request or its response, with optional regex highlight."""
+ """View a captured request or its response, optionally regex-searched.
+
+ Two modes:
+
+ - **With** ``search_pattern`` (compact regex hits) — returns up to 20
+ matches with ``before`` / ``after`` context and position. Useful
+ for hunting reflected input, leaked URLs, hidden parameters.
+ - **Without** ``search_pattern`` (full content with pagination) —
+ returns the page of raw content plus ``has_more`` flag.
+
+ Common search patterns:
+
+ - API endpoints: ``/api/[a-zA-Z0-9._/-]+``
+ - URLs: ``https?://[^\\s<>"']+``
+ - Query parameters: ``[?&][a-zA-Z0-9_]+=([^&\\s<>"']+)``
+ - Specific input reflection: search for the value you submitted.
+
+ Args:
+ request_id: Request ID from ``list_requests``.
+ part: ``"request"`` or ``"response"``.
+ search_pattern: Optional regex; switches the response shape to
+ compact hits.
+ page: 1-indexed page number (only when no ``search_pattern``).
+ page_size: Lines per page.
+ """
return _dump(
await post_to_sandbox(
ctx,
@@ -116,12 +160,17 @@ async def send_request(
) -> str:
"""Send an arbitrary HTTP request through the Caido proxy.
+ Use this for one-off probes (test endpoints, reach external APIs).
+ For modifying-and-replaying a request you've already captured, use
+ ``repeat_request`` instead — it inherits the original headers /
+ cookies / auth and only patches the fields you specify.
+
Args:
- method: ``"GET"``, ``"POST"``, etc.
- url: Full URL.
+ method: ``"GET"`` / ``"POST"`` / ``"PUT"`` / ``"DELETE"`` / etc.
+ url: Full URL with protocol.
headers: Optional header dict.
- body: Optional body string.
- timeout: Per-request timeout in seconds.
+ body: Optional request body string.
+ timeout: Per-request timeout in seconds (default 30).
"""
return _dump(
await post_to_sandbox(
@@ -147,7 +196,31 @@ async def repeat_request(
request_id: str,
modifications: dict[str, Any] | None = None,
) -> str:
- """Repeat a captured request, optionally applying field modifications."""
+ """Repeat a captured request, optionally patching individual fields.
+
+ The standard pentesting workflow with this tool:
+
+ 1. ``browser_action`` (or live target traffic) → request gets
+ captured by Caido.
+ 2. ``list_requests`` → find the request ID you want to manipulate.
+ 3. ``repeat_request`` → send a modified version (auth-bypass test,
+ payload injection, parameter tampering).
+
+ Mirrors the manual "browse → capture → modify → test" flow used in
+ real pentesting. Inherits everything from the original request
+ (headers, cookies, auth, method, URL) and overlays only the fields
+ you specify in ``modifications``.
+
+ Args:
+ request_id: ID of the original request (from ``list_requests``).
+ modifications: Patch dict. Recognized keys:
+
+ - ``url`` — replace the URL.
+ - ``params`` — dict of query-string keys to add/update.
+ - ``headers`` — dict of headers to add/update.
+ - ``body`` — replace the body string entirely.
+ - ``cookies`` — dict of cookies to add/update.
+ """
return _dump(
await post_to_sandbox(
ctx,
@@ -169,7 +242,44 @@ async def scope_rules(
scope_id: str | None = None,
scope_name: str | None = None,
) -> str:
- """CRUD on Caido scope rules (allow/deny lists)."""
+ """CRUD on Caido scope rules (allow/deny patterns).
+
+ Scopes filter which traffic Caido tools see. Use them to focus on a
+ target, exclude noisy assets (CDNs, static files), or define a
+ bug-bounty allowlist.
+
+ Pattern semantics:
+
+ - Glob wildcards: ``*`` (any), ``?`` (single), ``[abc]`` (one of),
+ ``[a-z]`` (range), ``[^abc]`` (none of).
+ - **Empty allowlist = allow all domains.**
+ - **Denylist always overrides allowlist.**
+
+ Common denylist for noisy static assets:
+ ``["*.gif", "*.jpg", "*.png", "*.css", "*.js", "*.ico", "*.svg",
+ "*woff*", "*.ttf"]``.
+
+ Each scope has a unique id usable as ``scope_id`` in
+ ``list_requests`` / ``list_sitemap`` / ``view_request``.
+
+ Args:
+ action:
+
+ - ``list`` — return all scopes.
+ - ``get`` — single scope by ``scope_id`` (or all when
+ omitted).
+ - ``create`` — needs ``scope_name``, optionally
+ ``allowlist`` / ``denylist``.
+ - ``update`` — needs ``scope_id`` + ``scope_name``;
+ allowlist / denylist replace the previous values.
+ - ``delete`` — needs ``scope_id``.
+
+ allowlist: Domain patterns to include (e.g.
+ ``["*.example.com", "api.test.com"]``).
+ denylist: Patterns to exclude.
+ scope_id: Required for ``get`` / ``update`` / ``delete``.
+ scope_name: Required for ``create`` / ``update``.
+ """
return _dump(
await post_to_sandbox(
ctx,
@@ -193,13 +303,30 @@ async def list_sitemap(
depth: SitemapDepth = "DIRECT",
page: int = 1,
) -> str:
- """List Caido sitemap entries (proxied URL tree).
+ """View the hierarchical sitemap of discovered attack surface.
+
+ The sitemap is built from proxied traffic — every URL the target
+ served gets indexed into a tree of domains → directories → request
+ leaves. Use it to understand application structure and find
+ interesting endpoints, hidden directories, parameter variations.
+
+ Entry kinds you'll encounter:
+
+ - ``DOMAIN`` — root host (``example.com``).
+ - ``DIRECTORY`` — path segment (``/api/``, ``/admin/``).
+ - ``REQUEST`` — a specific endpoint.
+ - ``REQUEST_BODY`` — POST/PUT body variations (different payloads
+ seen at the same URL).
+ - ``REQUEST_QUERY`` — query-string variations.
+
+ Each entry has ``hasDescendants`` — set ``parent_id`` to that
+ entry's id to drill in. Pages return 30 entries each.
Args:
- scope_id: Restrict to a scope.
- parent_id: Drill into a specific subtree.
- depth: ``"DIRECT"`` (direct children only) or ``"ALL"`` (recursive).
- page: 1-indexed page number.
+ scope_id: Filter to a specific scope.
+ parent_id: Drill into a subtree. ``None`` returns root domains.
+ depth: ``"DIRECT"`` (immediate children) or ``"ALL"`` (recursive).
+ page: 1-indexed page (30 entries/page).
"""
return _dump(
await post_to_sandbox(
@@ -217,7 +344,15 @@ async def list_sitemap(
@strix_tool(timeout=60)
async def view_sitemap_entry(ctx: RunContextWrapper, entry_id: str) -> str:
- """Fetch a single sitemap entry's metadata + linked requests."""
+ """Examine one sitemap entry — full metadata + every related request.
+
+ Use this after ``list_sitemap`` identifies an interesting directory
+ or endpoint to see all the requests captured under it (methods,
+ paths, response codes, timing).
+
+ Args:
+ entry_id: Sitemap entry id from ``list_sitemap``.
+ """
return _dump(
await post_to_sandbox(ctx, "view_sitemap_entry", {"entry_id": entry_id}),
)
diff --git a/strix/tools/python/tool.py b/strix/tools/python/tool.py
index 4d494f0..47faea2 100644
--- a/strix/tools/python/tool.py
+++ b/strix/tools/python/tool.py
@@ -31,13 +31,53 @@ async def python_action(
timeout: int = 30,
session_id: str | None = None,
) -> str:
- """Manage / execute code in a long-lived sandboxed IPython session.
+ """Run Python code in a long-lived IPython session — preferred for any
+ Python work (payloads, exploit scripts, HTTP automation, log analysis,
+ crypto, data processing).
+
+ Pick this over ``terminal_execute`` whenever the work is Python.
+ Don't wrap Python in bash heredocs, ``python -c`` one-liners, or
+ interactive REPL sessions in the terminal — the structured,
+ persistent, debuggable execution lives here.
+
+ Sessions are **persistent** — variables, imports, and function
+ definitions survive between ``execute`` calls within the same
+ ``session_id``. Each session has its own isolated namespace; multiple
+ sessions can run concurrently. Sessions stay alive until explicitly
+ ``close``-d.
+
+ Caido proxy helpers are pre-imported into every session, so you can
+ correlate captured HTTP requests with custom analysis without any
+ setup: ``list_requests`` / ``view_request`` / ``send_request`` /
+ ``repeat_request`` / ``scope_rules`` / ``list_sitemap`` /
+ ``view_sitemap_entry`` are all available as bare names.
+
+ For large payload sprays / fuzzing loops, encapsulate the entire
+ loop inside a single ``python_action`` ``execute`` call (e.g.,
+ asyncio + aiohttp). Don't issue one tool call per payload — that
+ burns turns and is dramatically slower.
+
+ Code execution notes:
+
+ - Both expressions and statements are supported. Expressions auto-
+ return their result; ``print`` output is captured to stdout.
+ - IPython magics work: ``%pip install ...``, ``%time``, ``%whos``,
+ ``%%writefile``, etc.
+ - Use real newlines in multi-line ``code``, not literal ``\\n``.
+
+ Workflow:
+
+ 1. ``new_session`` (always first per ``session_id``) — optionally
+ pass ``code`` for an initial setup snippet (imports, helpers).
+ 2. ``execute`` — run code. Variables persist across calls.
+ 3. ``close`` — terminate the session and free memory.
+ 4. ``list_sessions`` — inspect what's currently alive.
Args:
- action: ``"new_session"`` to spin one up, ``"execute"`` to run code,
- ``"close"`` to terminate, ``"list_sessions"`` to inspect.
- code: Required for ``execute`` (and optional for ``new_session``
- to run a setup snippet immediately).
+ action: ``"new_session"`` / ``"execute"`` / ``"close"`` /
+ ``"list_sessions"``.
+ code: Required for ``execute``; optional initial code for
+ ``new_session``.
timeout: Per-call execution budget in seconds. Default 30.
session_id: Required for ``execute`` / ``close``. Optional for
``new_session`` (auto-generated when omitted).
diff --git a/strix/tools/reporting/tool.py b/strix/tools/reporting/tool.py
index 6803f03..63a4a32 100644
--- a/strix/tools/reporting/tool.py
+++ b/strix/tools/reporting/tool.py
@@ -333,11 +333,78 @@ async def create_vulnerability_report(
cwe: str | None = None,
code_locations: str | None = None,
) -> str:
- """File a vulnerability report against the active scan.
+ """File a vulnerability report — one report per fully-verified finding.
- The report is dedup-checked against existing reports (LLM-based
- similarity); if it's a near-duplicate, the call returns a
- ``duplicate_of`` pointer instead of creating a new entry.
+ **When to file**: you have a concrete vulnerability with a working
+ proof-of-concept and you're 100% sure it's a real issue.
+
+ **When NOT to file**:
+
+ - General security observations without a specific vulnerability.
+ - Suspicions you haven't confirmed with a PoC.
+ - Tracking multiple vulnerabilities at once — one report per vuln.
+ - Re-reporting something you (or another agent) already filed.
+
+ Automatic LLM-based **deduplication** rejects reports that describe
+ the same root cause on the same asset as an existing report. If you
+ get a ``duplicate_of`` response, do NOT retry — move on to other
+ areas.
+
+ **Customer-facing report rules** (the report is PDF-rendered for
+ delivery):
+
+ - No internal/system details: never mention paths like
+ ``/workspace``, internal tools, agents, sandboxes, models, system
+ prompts, internal errors / stack traces, or tester environment.
+ - Tone: formal, objective, third-person, vendor-neutral, concise.
+ - Standard finding structure: Overview → Severity & CVSS →
+ Affected assets → Technical details → PoC (steps + code) →
+ Impact → Remediation → Evidence (in technical_analysis).
+ - Numbered steps allowed only in PoC and Remediation sections.
+ - Avoid hedging language; be precise and non-vague.
+
+ **White-box requirement**: when source is available, you MUST
+ populate ``code_locations`` with nested XML including
+ ``fix_before`` / ``fix_after`` for proposed fixes. The fix_before
+ must be a verbatim copy of source at the specified line range — it's
+ used as a literal GitHub/GitLab PR suggestion block.
+
+ **CVSS breakdown** is required as nested XML with all 8 metrics
+ (each a single uppercase letter):
+
+ - ``attack_vector``: ``N`` (Network), ``A`` (Adjacent), ``L``
+ (Local), ``P`` (Physical)
+ - ``attack_complexity``: ``L`` / ``H``
+ - ``privileges_required``: ``N`` / ``L`` / ``H``
+ - ``user_interaction``: ``N`` / ``R``
+ - ``scope``: ``U`` (Unchanged) / ``C`` (Changed)
+ - ``confidentiality`` / ``integrity`` / ``availability``: ``N`` /
+ ``L`` / ``H``
+
+ **CVE / CWE rules**: pass the bare ID only (``CVE-2024-1234``,
+ ``CWE-89``) — no name, no parenthetical. Be 100% certain; if
+ unsure, omit. Always prefer the most specific child CWE over a
+ broad parent (CWE-89 not CWE-74; CWE-78 not CWE-77).
+
+ Args:
+ title: Specific finding title (e.g.
+ ``"SQL Injection in /api/users login parameter"``). Don't
+ include the CVE number in the title.
+ description: How the vuln was discovered + what it is.
+ impact: What an attacker achieves; business risk; data at risk.
+ target: Affected URL / domain / repository.
+ technical_analysis: The mechanism and root cause.
+ poc_description: Step-by-step reproduction.
+ poc_script_code: Working PoC (Python preferred).
+ remediation_steps: Specific, actionable fix.
+ cvss_breakdown: 8-metric XML block per the format above.
+ endpoint: API path / Git path (e.g. ``/api/login``).
+ method: HTTP method when relevant.
+ cve: ``CVE-YYYY-NNNNN`` if certain, else omit.
+ cwe: ``CWE-NNN`` (most specific child) if certain, else omit.
+ code_locations: Required for white-box findings; nested XML
+ list with ``file``, ``start_line``, ``end_line``,
+ ``snippet``, ``fix_before``, ``fix_after``.
"""
del ctx
result = await asyncio.to_thread(
diff --git a/strix/tools/terminal/tool.py b/strix/tools/terminal/tool.py
index 8940905..13b91a0 100644
--- a/strix/tools/terminal/tool.py
+++ b/strix/tools/terminal/tool.py
@@ -31,16 +31,66 @@ async def terminal_execute(
) -> str:
"""Run a shell command in the sandboxed Kali tmux session.
+ The session is **persistent** — environment variables, current
+ directory, and running processes carry across calls keyed by
+ ``terminal_id`` (default: ``"default"``). Use distinct ids to run
+ multiple concurrent sessions.
+
+ When to use this vs ``python_action``:
+
+ - Shell work: CLI tools (nmap, sqlmap, ffuf, nuclei), package
+ managers, file/system commands, services, process control. Use
+ ``terminal_execute``.
+ - Python code, data processing, HTTP automation, iterative scripting:
+ use ``python_action`` instead — it's more structured and easier to
+ debug. Don't run embedded Python via ``python -c`` or heredocs
+ here.
+
+ Avoid long pipelines and complex bash one-liners; prefer multiple
+ simple calls for clarity and debugging. For multi-step shell work,
+ separate tool calls beat ``&& ; |``-chained commands.
+
+ Long-running commands:
+
+ - Commands are **never** killed automatically — they keep running
+ after the timeout fires.
+ - ``timeout`` (max 60s, capped) only controls how long to wait for
+ output before returning. On timeout the call returns
+ ``status="running"``; on completion ``status="completed"``.
+ - For daemons / very long jobs, append ``&`` to background.
+ - Use an **empty command** to poll for new output from a running
+ process (the call waits ``timeout`` seconds collecting output).
+ - Use ``C-c`` / ``C-d`` / ``C-z`` to interrupt — special keys work
+ automatically without setting ``is_input``.
+
+ Interactive processes:
+
+ - ``is_input=True`` sends the command as input to a running foreground
+ process (REPL prompts, ``apt install`` y/n, etc.).
+ - ``no_enter=True`` sends keystrokes without a trailing newline —
+ useful for vim navigation (``gg``, ``5j``, ``i``), passwords, or
+ multi-step keybindings.
+
+ Special key support (tmux key names): ``C-c``, ``C-d``, ``Up``,
+ ``Down``, ``F1``-``F12``, ``Enter``, ``Escape``, ``Tab``, ``Space``,
+ ``BSpace``, ``M-f`` (alt), ``S-Tab`` (shift), and combinations like
+ ``C-S-key``. Note: ``BSpace`` not ``Backspace``, ``Escape`` not
+ ``Esc``.
+
+ Working directory is tracked across calls and returned in the
+ response. Large outputs are auto-truncated.
+
Args:
- command: Shell command (or input for an interactive prompt when
- ``is_input=True``).
- is_input: Treat ``command`` as input to a running foreground process
- (e.g., feeding y/n to ``apt install``).
- timeout: Seconds to wait before returning partial output. Defaults
- to the in-container manager's policy.
- terminal_id: Persistent session selector. Defaults to ``"default"``.
+ command: Shell command, special key (``C-c``), or empty string
+ to poll a running process.
+ is_input: Treat ``command`` as input to a running foreground
+ process. Special keys auto-detect; you only need this for
+ regular text input.
+ timeout: Seconds to wait before returning partial output. Capped
+ at 60s. Defaults to 30s.
+ terminal_id: Persistent session selector. Use distinct ids for
+ concurrent sessions.
no_enter: When True, sends keystrokes without a trailing return.
- Useful for sending raw ANSI control sequences.
"""
return _dump(
await post_to_sandbox(
diff --git a/strix/tools/thinking/tool.py b/strix/tools/thinking/tool.py
index e7e843d..511815e 100644
--- a/strix/tools/thinking/tool.py
+++ b/strix/tools/thinking/tool.py
@@ -9,15 +9,28 @@ from strix.tools._decorator import strix_tool
@strix_tool(timeout=10)
async def think(thought: str) -> str:
- """Record a private chain-of-thought note without taking any action.
+ """Record a private chain-of-thought note. No side effects, no new info.
- The "think" tool is the planning escape hatch for situations where a
- message-without-tool-call would otherwise halt the run (per the
- interactive-mode tool-call requirement). The thought itself is
- recorded but produces no side effects.
+ Use ``think`` when you need a dedicated space to reason before acting —
+ not as an output channel. It's particularly valuable for:
+
+ - **Tool output analysis** — carefully processing the output of a
+ previous tool call before deciding the next step.
+ - **Policy-heavy environments** — when you need to follow detailed
+ guidelines (engagement scope, auth boundaries) and verify compliance
+ before each action.
+ - **Sequential decision making** — when each action builds on previous
+ ones and mistakes are costly (e.g., destructive operations,
+ irreversible auth changes).
+ - **Multi-step exploit planning** — breaking down a complex chain into
+ manageable steps and tracking what's been confirmed vs. assumed.
+
+ Structure your thought to be useful: current state, what you've
+ confirmed, your next planned actions, risk assessment. Don't use
+ ``think`` to chat — use it to plan.
Args:
- thought: The agent's reasoning to record. Must be non-empty.
+ thought: The reasoning to record. Must be non-empty.
"""
if not thought or not thought.strip():
return json.dumps({"success": False, "message": "Thought cannot be empty"})
diff --git a/strix/tools/todo/tools.py b/strix/tools/todo/tools.py
index 1b4d7d7..04986a2 100644
--- a/strix/tools/todo/tools.py
+++ b/strix/tools/todo/tools.py
@@ -211,7 +211,33 @@ async def create_todo(
priority: str = "normal",
todos: str | None = None,
) -> str:
- """Create one or many todos for the current agent."""
+ """Create one or many todos for the current agent.
+
+ Each agent (including subagents) has its **own private todo list** —
+ your todos don't leak to other agents and vice versa.
+
+ When to use:
+
+ - Planning multi-step assessments with parallel workstreams.
+ - Tracking work you'll come back to later.
+ - Breaking down complex scopes (per-endpoint, per-target, per-vuln-class).
+
+ When NOT to use:
+
+ - Simple linear workflows where progress is obvious.
+ - Single quick task — just do it.
+
+ Batch related todos in one call via the ``todos`` bulk parameter
+ rather than firing many ``create_todo`` calls.
+
+ Args:
+ title: Short, actionable title (e.g., "Test /api/admin for IDOR").
+ description: Optional details / context for the single todo.
+ priority: ``"low"`` / ``"normal"`` / ``"high"`` / ``"critical"``.
+ todos: Bulk create — either JSON array of
+ ``{"title": "...", "description": "...", "priority": "..."}``
+ objects, or a newline-separated bullet list (``- item\\n- item``).
+ """
agent_id = _agent_id_from(ctx)
try:
default_priority = _normalize_priority(priority)
@@ -271,7 +297,16 @@ async def list_todos(
status: str | None = None,
priority: str | None = None,
) -> str:
- """List the current agent's todos, sorted by status then priority."""
+ """List the current agent's todos, sorted by status then priority.
+
+ Sort order: status (done → in_progress → pending), then priority
+ within each status (critical → high → normal → low).
+
+ Args:
+ status: Filter — ``"pending"`` / ``"in_progress"`` / ``"done"``.
+ priority: Filter — ``"low"`` / ``"normal"`` / ``"high"`` /
+ ``"critical"``.
+ """
agent_id = _agent_id_from(ctx)
try:
agent_todos = _get_agent_todos(agent_id)
@@ -333,7 +368,19 @@ async def update_todo(
status: str | None = None,
updates: str | None = None,
) -> str:
- """Update one or many todos."""
+ """Update one or many todos. Prefer the bulk form for multiple updates.
+
+ For toggling status only, use the dedicated ``mark_todo_done`` /
+ ``mark_todo_pending`` tools — they're simpler and accept bulk
+ ``todo_ids``.
+
+ Args:
+ todo_id: Single-todo target.
+ title / description / priority / status: New values for the
+ single todo. Omit to leave unchanged.
+ updates: Bulk form — JSON array like
+ ``[{"todo_id": "abc", "status": "done"}, ...]``.
+ """
agent_id = _agent_id_from(ctx)
try:
agent_todos = _get_agent_todos(agent_id)
@@ -437,7 +484,13 @@ async def mark_todo_done(
todo_id: str | None = None,
todo_ids: str | None = None,
) -> str:
- """Mark one (``todo_id``) or many (``todo_ids``) todos as done."""
+ """Mark one or many todos as done.
+
+ Args:
+ todo_id: Single todo's ID.
+ todo_ids: Bulk form — JSON array, comma-separated string, or
+ single ID. Combinable with ``todo_id`` for one-off plus bulk.
+ """
return _mark(
agent_id=_agent_id_from(ctx),
todo_id=todo_id,
@@ -452,7 +505,12 @@ async def mark_todo_pending(
todo_id: str | None = None,
todo_ids: str | None = None,
) -> str:
- """Mark one (``todo_id``) or many (``todo_ids``) todos as pending."""
+ """Reset one or many todos to pending (e.g., to retry a failed task).
+
+ Args:
+ todo_id: Single todo's ID.
+ todo_ids: Bulk form — JSON array, comma-separated, or single ID.
+ """
return _mark(
agent_id=_agent_id_from(ctx),
todo_id=todo_id,
@@ -467,7 +525,12 @@ async def delete_todo(
todo_id: str | None = None,
todo_ids: str | None = None,
) -> str:
- """Delete one (``todo_id``) or many (``todo_ids``) todos."""
+ """Delete one or many todos. Removes them entirely (no soft-delete).
+
+ Args:
+ todo_id: Single todo's ID.
+ todo_ids: Bulk form — JSON array, comma-separated, or single ID.
+ """
agent_id = _agent_id_from(ctx)
try:
agent_todos = _get_agent_todos(agent_id)
diff --git a/strix/tools/web_search/tool.py b/strix/tools/web_search/tool.py
index e486d56..9722b5a 100644
--- a/strix/tools/web_search/tool.py
+++ b/strix/tools/web_search/tool.py
@@ -86,11 +86,40 @@ def _do_search(query: str) -> dict[str, Any]:
# budget so the round-trip + JSON decode doesn't push us over.
@strix_tool(timeout=330)
async def web_search(ctx: RunContextWrapper, query: str) -> str:
- """Search the web with Perplexity, scoped to security-relevant content.
+ """Real-time web search via Perplexity — your primary research tool.
+
+ Use it liberally for anything that's not in your training data:
+
+ - Current CVEs, advisories, and 0-days for a specific
+ service/version (``OpenSSH 9.6 RCE``, ``Jenkins 2.401.3 auth
+ bypass``).
+ - Latest WAF / EDR bypass techniques (``Cloudflare WAF SQLi
+ bypass 2025``, ``CrowdStrike Falcon evasion``).
+ - Tool documentation, flag references, payload galleries.
+ - Target reconnaissance / OSINT (company tech stack, leaked
+ credentials, exposed assets).
+ - Cloud-provider misconfiguration patterns
+ (Azure/AWS/GCP-specific attack paths).
+ - Bug-bounty writeups and security research papers.
+ - Compliance frameworks and CWE/CVSS guidance.
+ - Picking the right Python lib / Kali tool for a job (``best 2025
+ lib for JWT alg-confusion``).
+ - When stuck — looking up the exact error message, ``Access
+ denied`` quirks, kernel-specific local-privesc exploits.
+
+ Be specific: include version numbers, error messages, target
+ technology, and the exact problem you're stuck on. The more context
+ in the query, the more actionable the answer. Vague queries get
+ generic answers.
+
+ A security-focused system prompt biases responses toward CVEs,
+ exploits, Kali-compatible tooling, and concrete code/command
+ examples.
Args:
- query: The search query. A security-focused system prompt biases
- results toward CVEs, exploits, and Kali-compatible commands.
+ query: The search query — a full sentence with version numbers,
+ target tech, and the specific question. Treat it like a
+ ticket title for a senior security engineer.
"""
del ctx
result = await asyncio.to_thread(_do_search, query)