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)