diff --git a/strix/agents/factory.py b/strix/agents/factory.py index 6eceef3..a9e7a94 100644 --- a/strix/agents/factory.py +++ b/strix/agents/factory.py @@ -11,8 +11,9 @@ Two flavors: ``create_agent`` graph tool. Carries ``agent_finish`` and stops after that tool reports ``agent_completed``. -Skills are baked into the system prompt at scan bring-up; there's no -runtime skill-loading tool. +Skills are baked into the system prompt at scan bring-up. The +``load_skill`` tool is also available for on-demand inline reference +to any skill the agent didn't preload. """ from __future__ import annotations diff --git a/strix/agents/prompts/system_prompt.jinja b/strix/agents/prompts/system_prompt.jinja index 32738a7..773d6a2 100644 --- a/strix/agents/prompts/system_prompt.jinja +++ b/strix/agents/prompts/system_prompt.jinja @@ -149,6 +149,7 @@ OPERATIONAL PRINCIPLES: - Prefer established industry-standard tools already available in the sandbox before writing custom scripts - Do NOT reinvent the wheel with ad hoc Python or shell code when a suitable existing tool can do the job reliably - Skills relevant to your task are preloaded into this prompt at scan start; refer back to them when you need vulnerability-, protocol-, or tool-specific guidance +- For skills not preloaded, use `load_skill` to pull them inline — prefer loading the matching skill before guessing payloads, workflows, or tool syntax from memory - Use custom Python or shell code when you want to dig deeper, automate custom workflows, batch operations, triage results, build target-specific validation, or do work that existing tools do not cover cleanly - Chain related weaknesses when needed to demonstrate real impact - Consider business logic and context in validation diff --git a/strix/interface/tui/app.py b/strix/interface/tui/app.py index 310cc11..8eb227c 100644 --- a/strix/interface/tui/app.py +++ b/strix/interface/tui/app.py @@ -998,9 +998,7 @@ class StrixTUIApp(App): # type: ignore[misc] self, ) -> tuple[Any, str | None]: if not self.selected_agent_id: - return self._get_chat_placeholder_content( - "Select an agent from the tree to see its activity.", "placeholder-no-agent" - ) + return self._get_chat_placeholder_content("Loading...", "placeholder-no-agent") events = self._gather_agent_events(self.selected_agent_id) @@ -1353,7 +1351,9 @@ class StrixTUIApp(App): # type: ignore[misc] def _agent_vulnerability_count(self, agent_id: str) -> int: return sum( - 1 for vuln in self.report_state.vulnerability_reports if vuln.get("agent_id") == agent_id + 1 + for vuln in self.report_state.vulnerability_reports + if vuln.get("agent_id") == agent_id ) def _gather_agent_events(self, agent_id: str) -> list[dict[str, Any]]: diff --git a/strix/interface/tui/renderers/proxy_renderer.py b/strix/interface/tui/renderers/proxy_renderer.py index 94bf3c9..4cd46a5 100644 --- a/strix/interface/tui/renderers/proxy_renderer.py +++ b/strix/interface/tui/renderers/proxy_renderer.py @@ -42,7 +42,7 @@ class ListRequestsRenderer(BaseToolRenderer): css_classes: ClassVar[list[str]] = ["tool-call", "proxy-tool"] @classmethod - def render(cls, tool_data: dict[str, Any]) -> Static: # noqa: PLR0912 # noqa: PLR0912 + def render(cls, tool_data: dict[str, Any]) -> Static: # noqa: PLR0912, PLR0915 args = tool_data.get("args", {}) result = tool_data.get("result") status = tool_data.get("status", "running") @@ -73,21 +73,25 @@ class ListRequestsRenderer(BaseToolRenderer): if "error" in result: text.append(f" error: {_sanitize(str(result['error']), 150)}", style="#ef4444") else: - total = result.get("total_count", 0) - requests = result.get("requests", []) + entries = result.get("entries", []) + page_info = result.get("page_info") or {} + has_more = ( + bool(page_info.get("has_next_page")) if isinstance(page_info, dict) else False + ) + count_suffix = "+" if has_more else "" + text.append(f" [{len(entries)}{count_suffix} found]", style="dim") - text.append(f" [{total} found]", style="dim") - - if requests and isinstance(requests, list): + if entries and isinstance(entries, list): text.append("\n") - for i, req in enumerate(requests[:MAX_REQUESTS_DISPLAY]): - if not isinstance(req, dict): + for i, entry in enumerate(entries[:MAX_REQUESTS_DISPLAY]): + if not isinstance(entry, dict): continue - method = req.get("method", "?") - host = req.get("host", "") - path = req.get("path", "/") - resp = req.get("response") or {} - code = resp.get("statusCode") if isinstance(resp, dict) else None + req = entry.get("request") or {} + resp = entry.get("response") or {} + method = req.get("method", "?") if isinstance(req, dict) else "?" + host = req.get("host", "") if isinstance(req, dict) else "" + path = req.get("path", "/") if isinstance(req, dict) else "/" + code = resp.get("status_code") if isinstance(resp, dict) else None text.append(" ") text.append(f"{method:6}", style="#a78bfa") @@ -95,13 +99,13 @@ class ListRequestsRenderer(BaseToolRenderer): if code: text.append(f" {code}", style=_status_style(code)) - if i < min(len(requests), MAX_REQUESTS_DISPLAY) - 1: + if i < min(len(entries), MAX_REQUESTS_DISPLAY) - 1: text.append("\n") - if len(requests) > MAX_REQUESTS_DISPLAY: + if len(entries) > MAX_REQUESTS_DISPLAY: text.append("\n") text.append( - f" ... +{len(requests) - MAX_REQUESTS_DISPLAY} more", + f" ... +{len(entries) - MAX_REQUESTS_DISPLAY} more", style="dim italic", ) @@ -139,14 +143,14 @@ class ViewRequestRenderer(BaseToolRenderer): if status == "completed" and isinstance(result, dict): if "error" in result: text.append(f" error: {_sanitize(str(result['error']), 150)}", style="#ef4444") - elif "matches" in result: - matches = result.get("matches", []) - total = result.get("total_matches", len(matches)) + elif "hits" in result: + hits = result.get("hits", []) + total = result.get("total_hits", len(hits)) text.append(f" [{total} matches]", style="dim") - if matches and isinstance(matches, list): + if hits and isinstance(hits, list): text.append("\n") - for i, m in enumerate(matches[:5]): + for i, m in enumerate(hits[:5]): if not isinstance(m, dict): continue before = m.get("before", "") or "" @@ -164,19 +168,20 @@ class ViewRequestRenderer(BaseToolRenderer): if after: text.append(f"{after}...", style="dim") - if i < min(len(matches), 5) - 1: + if i < min(len(hits), 5) - 1: text.append("\n") - if len(matches) > 5: + if len(hits) > 5: text.append("\n") - text.append(f" ... +{len(matches) - 5} more matches", style="dim italic") + text.append(f" ... +{len(hits) - 5} more matches", style="dim italic") elif "content" in result: - showing = result.get("showing_lines", "") + page = result.get("page", 1) + total_lines = result.get("total_lines", 0) has_more = result.get("has_more", False) content = result.get("content", "") - text.append(f" [{showing}]", style="dim") + text.append(f" [page {page}, {total_lines} lines]", style="dim") if content and isinstance(content, str): lines = content.split("\n")[:15] diff --git a/strix/tools/agents_graph/tools.py b/strix/tools/agents_graph/tools.py index 7c33db6..1b917c2 100644 --- a/strix/tools/agents_graph/tools.py +++ b/strix/tools/agents_graph/tools.py @@ -5,6 +5,8 @@ - ``wait_for_message``: pause this agent until a message arrives or ``timeout_seconds`` elapses. - ``create_agent``: asks the scan runner to spawn an addressable child. +- ``stop_agent``: cancel a running agent (optionally cascading to its + descendants). - ``agent_finish``: subagents only — posts a structured completion report to the parent's SDK session and returns a final-output marker. """ @@ -94,7 +96,7 @@ async def view_agent_graph(ctx: RunContextWrapper) -> str: me = inner.get("agent_id") if coordinator is None: return json.dumps( - {"success": False, "error": "Agent coordinator not initialized in context."}, + {"success": False, "error": "Agent coordinator not initialized in context"}, ensure_ascii=False, default=str, ) @@ -172,7 +174,7 @@ async def send_message_to_agent( me = inner.get("agent_id") if coordinator is None or me is None: return json.dumps( - {"success": False, "error": "Agent coordinator or agent_id missing in context."}, + {"success": False, "error": "Agent coordinator or agent_id missing in context"}, ensure_ascii=False, default=str, ) @@ -181,8 +183,8 @@ async def send_message_to_agent( { "success": False, "error": ( - "Cannot send a message to yourself. Use `think` to record a " - "private note, or `agent_finish` / `finish_scan` to terminate." + "Cannot send a message to yourself; use `think` to record a " + "private note, or `agent_finish` / `finish_scan` to terminate" ), }, ensure_ascii=False, @@ -204,7 +206,7 @@ async def send_message_to_agent( return json.dumps( { "success": False, - "error": f"Target agent '{target_agent_id}' not found or message delivery failed.", + "error": f"Target agent '{target_agent_id}' not found or message delivery failed", }, ensure_ascii=False, default=str, @@ -271,7 +273,7 @@ async def wait_for_message( # noqa: PLR0911 interactive = bool(inner.get("interactive", False)) if coordinator is None or me is None: return json.dumps( - {"success": False, "error": "Agent coordinator or agent_id missing in context."}, + {"success": False, "error": "Agent coordinator or agent_id missing in context"}, ensure_ascii=False, default=str, ) @@ -410,7 +412,8 @@ async def create_agent( 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. + skills: List of skill names (e.g. ``["xss", "sql_injection"]``). + Max 5; prefer 1-3. """ inner = _ctx(ctx) coordinator = coordinator_from_context(inner) @@ -419,7 +422,7 @@ async def create_agent( if coordinator is None or parent_id is None: return json.dumps( - {"success": False, "error": "Agent coordinator or agent_id missing in context."}, + {"success": False, "error": "Agent coordinator or agent_id missing in context"}, ensure_ascii=False, default=str, ) @@ -427,7 +430,7 @@ async def create_agent( return json.dumps( { "success": False, - "error": "Scan runner did not provide a child-agent spawner in context.", + "error": "Scan runner did not provide a child-agent spawner in context", }, ensure_ascii=False, default=str, @@ -525,7 +528,7 @@ async def agent_finish( me = inner.get("agent_id") if coordinator is None or me is None: return json.dumps( - {"success": False, "error": "Agent coordinator or agent_id missing in context."}, + {"success": False, "error": "Agent coordinator or agent_id missing in context"}, ensure_ascii=False, default=str, ) @@ -536,7 +539,7 @@ async def agent_finish( { "success": False, "error": ( - "agent_finish is for subagents. Root/main agents must call finish_scan instead." + "agent_finish is for subagents. Root/main agents must call finish_scan instead" ), }, ensure_ascii=False, @@ -625,7 +628,7 @@ async def stop_agent( me = inner.get("agent_id") if coordinator is None or me is None: return json.dumps( - {"success": False, "error": "Agent coordinator or agent_id missing in context."}, + {"success": False, "error": "Agent coordinator or agent_id missing in context"}, ensure_ascii=False, default=str, ) @@ -633,7 +636,7 @@ async def stop_agent( return json.dumps( { "success": False, - "error": "Cannot stop yourself; call agent_finish or finish_scan instead.", + "error": "Cannot stop yourself; call agent_finish or finish_scan instead", }, ensure_ascii=False, default=str, @@ -653,10 +656,10 @@ async def stop_agent( "success": False, "error": ( f"Agent {target_agent_id} is already '{current_status}'; " - "stop_agent only acts on running/waiting agents. Use " + "stop_agent only acts on running/waiting agents — use " "view_agent_graph to find still-active descendants and " "stop them individually, or send_message_to_agent if you " - "want to wake this one with new instructions." + "want to wake this one with new instructions" ), "target_agent_id": target_agent_id, "current_status": current_status, diff --git a/strix/tools/finish/tool.py b/strix/tools/finish/tool.py index e160cae..c7662b2 100644 --- a/strix/tools/finish/tool.py +++ b/strix/tools/finish/tool.py @@ -26,9 +26,10 @@ def _do_finish( if parent_id is not None: return { "success": False, - "error": "finish_scan_wrong_agent", - "message": "This tool can only be used by the root/main agent", - "suggestion": "If you are a subagent, use agent_finish instead", + "error": ( + "This tool can only be used by the root/main agent. " + "If you are a subagent, use agent_finish instead" + ), } errors: list[str] = [] @@ -41,7 +42,7 @@ def _do_finish( if not recommendations.strip(): errors.append("Recommendations cannot be empty") if errors: - return {"success": False, "message": "Validation failed", "errors": errors} + return {"success": False, "error": "Validation failed", "errors": errors} try: from strix.report.state import get_global_report_state @@ -64,7 +65,7 @@ def _do_finish( vuln_count = len(report_state.vulnerability_reports) except (ImportError, AttributeError) as e: logger.exception("finish_scan persistence failed") - return {"success": False, "message": f"Failed to complete scan: {e!s}"} + return {"success": False, "error": f"Failed to complete scan: {e!s}"} else: logger.info( "finish_scan: completed scan with %d vulnerability report(s)", @@ -159,10 +160,9 @@ async def finish_scan( { "success": False, "scan_completed": False, - "error": "active_agents_remaining", - "message": ( + "error": ( "Cannot finish scan while child agents are still active. " - "Wait for completion, send them finish instructions, or stop them first." + "Wait for completion, send them finish instructions, or stop them first" ), "active_agents": active_agents, }, diff --git a/strix/tools/notes/tools.py b/strix/tools/notes/tools.py index 9027a6f..7fe3923 100644 --- a/strix/tools/notes/tools.py +++ b/strix/tools/notes/tools.py @@ -196,6 +196,7 @@ def _create_note_impl( "success": True, "note_id": note_id, "message": f"Note '{title}' created successfully", + "total_count": len(_notes_storage), } @@ -214,9 +215,15 @@ def _list_notes_impl( "success": False, "error": f"Failed to list notes: {e}", "notes": [], + "filtered_count": 0, "total_count": 0, } - return {"success": True, "notes": notes, "total_count": len(notes)} + return { + "success": True, + "notes": notes, + "filtered_count": len(notes), + "total_count": len(_notes_storage), + } def _get_note_impl(note_id: str) -> dict[str, Any]: @@ -267,7 +274,9 @@ def _update_note_impl( _persist() return { "success": True, + "note_id": note_id, "message": f"Note '{note['title']}' updated successfully", + "total_count": len(_notes_storage), } @@ -285,7 +294,9 @@ def _delete_note_impl(note_id: str) -> dict[str, Any]: _persist() return { "success": True, + "note_id": note_id, "message": f"Note '{note_title}' deleted successfully", + "total_count": len(_notes_storage), } @@ -377,7 +388,7 @@ async def list_notes( @function_tool(timeout=30) async def get_note(ctx: RunContextWrapper, note_id: str) -> str: - """Fetch one note by its 5-char ID. Returns the full content. + """Fetch one note by its 6-char ID. Returns the full content. Args: note_id: Note id from ``create_note`` or a ``list_notes`` entry. @@ -402,7 +413,7 @@ async def update_note( ``get_note``, concat, and pass the result. Args: - note_id: Target note's 5-char ID. + note_id: Target note's 6-char ID. title: New title, or ``None`` to keep. content: New content, or ``None`` to keep. tags: New tags list, or ``None`` to keep. diff --git a/strix/tools/proxy/caido_api.py b/strix/tools/proxy/caido_api.py index 0ac4ff3..a45d4cd 100644 --- a/strix/tools/proxy/caido_api.py +++ b/strix/tools/proxy/caido_api.py @@ -324,9 +324,9 @@ async def replay_send_raw( "status": "ERROR", "error": ( f"Caido replay dispatch did not complete within " - f"{_REPLAY_SEND_TIMEOUT_SECONDS:.0f}s. The target may be " + f"{_REPLAY_SEND_TIMEOUT_SECONDS:.0f}s — the target may be " "unroutable from the sandbox, or Caido's outbound HTTP client " - "is stalled. Check the target host/port and retry." + "is stalled; check the target host/port and retry" ), "elapsed_ms": elapsed_ms, "response_raw": None, diff --git a/strix/tools/proxy/tools.py b/strix/tools/proxy/tools.py index 7d39be9..78064ff 100644 --- a/strix/tools/proxy/tools.py +++ b/strix/tools/proxy/tools.py @@ -453,7 +453,7 @@ async def list_sitemap( Workflow: - Start with no ``parent_id`` to list root domains (scoped by ``scope_id`` if you only care about in-scope hosts). - - Pick an entry where ``hasDescendants=true`` and pass its ``id`` + - Pick an entry where ``has_descendants=true`` and pass its ``id`` as ``parent_id`` to drill in. ``depth="DIRECT"`` returns only immediate children; ``"ALL"`` flattens the full subtree. - Hand any ``id`` to ``view_sitemap_entry`` for the full record @@ -575,7 +575,7 @@ async def scope_rules( if action == "get": if not scope_id: return json.dumps( - {"success": False, "error": "scope_id required for get"}, + {"success": False, "error": "Scope_id is required for action='get'"}, ensure_ascii=False, default=str, ) @@ -586,7 +586,7 @@ async def scope_rules( if action == "create": if not scope_name: return json.dumps( - {"success": False, "error": "scope_name required for create"}, + {"success": False, "error": "Scope_name is required for action='create'"}, ensure_ascii=False, default=str, ) @@ -601,7 +601,7 @@ async def scope_rules( return json.dumps( { "success": False, - "error": "scope_id and scope_name required for update", + "error": "Scope_id and scope_name are required for action='update'", }, ensure_ascii=False, default=str, @@ -615,11 +615,19 @@ async def scope_rules( # action == "delete" — exhaustive Literal if not scope_id: return json.dumps( - {"success": False, "error": "scope_id required for delete"}, + {"success": False, "error": "Scope_id is required for action='delete'"}, ensure_ascii=False, default=str, ) await caido_api.scope_delete(client, scope_id) - return json.dumps({"success": True, "deleted": scope_id}, ensure_ascii=False, default=str) + return json.dumps( + { + "success": True, + "deleted": scope_id, + "message": f"Scope {scope_id} deleted", + }, + ensure_ascii=False, + default=str, + ) except Exception as exc: # noqa: BLE001 return _err("scope_rules", exc) diff --git a/strix/tools/reporting/tool.py b/strix/tools/reporting/tool.py index 16a56a9..18f4148 100644 --- a/strix/tools/reporting/tool.py +++ b/strix/tools/reporting/tool.py @@ -209,7 +209,7 @@ async def _do_create( # noqa: PLR0912 errors.append(cwe_err) if errors: - return {"success": False, "message": "Validation failed", "errors": errors} + return {"success": False, "error": "Validation failed", "errors": errors} cvss_score, severity, _vector = _calculate_cvss(cvss_breakdown) @@ -248,9 +248,9 @@ async def _do_create( # noqa: PLR0912 ) return { "success": False, - "message": ( + "error": ( f"Potential duplicate of '{duplicate_title}' " - f"(id={duplicate_id[:8]}...). Do not re-report the same vulnerability." + f"(id={duplicate_id[:8]}...) — do not re-report the same vulnerability" ), "duplicate_of": duplicate_id, "duplicate_title": duplicate_title, @@ -280,7 +280,7 @@ async def _do_create( # noqa: PLR0912 ) except (ImportError, AttributeError) as e: logger.exception("create_vulnerability_report persistence failed") - return {"success": False, "message": f"Failed to create vulnerability report: {e!s}"} + return {"success": False, "error": f"Failed to create vulnerability report: {e!s}"} else: logger.info( "Vulnerability report created: id=%s severity=%s cvss=%.1f title=%s", @@ -351,10 +351,9 @@ async def create_vulnerability_report( - Avoid hedging language; be precise and non-vague. **White-box requirement**: when source is available, you MUST - populate ``code_locations`` with one entry per affected line range. - The ``fix_before`` field must be a verbatim copy of the source at - the specified line range — it's used as a literal GitHub/GitLab - PR suggestion block. + populate ``code_locations``. See the ``code_locations`` arg below + for the full rules around ``fix_before`` / ``fix_after``, + multi-part fixes, and informational-vs-actionable entries. **CVSS breakdown** is an object with all 8 metrics (each a single uppercase letter): @@ -383,8 +382,30 @@ async def create_vulnerability_report( **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). + unsure, use ``web_search`` to verify the ID before passing, or omit + the field entirely. Always prefer the most specific child CWE over + a broad parent (CWE-89 not CWE-74; CWE-78 not CWE-77). Do NOT use + broad/parent CWEs like CWE-74, CWE-20, CWE-200, CWE-284, or + CWE-693. + + Common CWE references (use the ID only — names are listed here + just for your lookup): + + - **Injection**: CWE-79 XSS, CWE-89 SQLi, CWE-78 OS Command + Injection, CWE-94 Code Injection, CWE-77 Command Injection. + - **Auth / Access**: CWE-287 Improper Authentication, CWE-862 + Missing Authorization, CWE-863 Incorrect Authorization, CWE-306 + Missing Auth for Critical Function, CWE-639 Authz Bypass via + User-Controlled Key. + - **Web**: CWE-352 CSRF, CWE-918 SSRF, CWE-601 Open Redirect, + CWE-434 Unrestricted File Upload. + - **Memory**: CWE-787 OOB Write, CWE-125 OOB Read, CWE-416 UAF, + CWE-120 Classic Buffer Overflow. + - **Data**: CWE-502 Deserialization of Untrusted Data, CWE-22 + Path Traversal, CWE-611 XXE. + - **Crypto / Config**: CWE-798 Hard-coded Credentials, CWE-327 + Broken / Risky Crypto, CWE-311 Missing Encryption, CWE-916 Weak + Password Hashing. Args: title: Specific finding title (e.g. @@ -402,11 +423,69 @@ async def create_vulnerability_report( 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. List of - objects, each with ``file`` (relative path), ``start_line``, - ``end_line``, optional ``snippet``, ``label``, - ``fix_before`` (verbatim source), ``fix_after`` (suggested - replacement). + code_locations: White-box findings — list of location objects. + + **How ``fix_before`` / ``fix_after`` work**: they're used as + literal GitHub/GitLab PR suggestion blocks. When a reviewer + accepts the suggestion, the platform replaces the **exact + lines from ``start_line`` to ``end_line``** with + ``fix_after``. Therefore: + + 1. ``fix_before`` must be a **VERBATIM** copy of the source + at those lines — same whitespace, indentation, line + breaks. If it doesn't match character-for-character, the + suggestion will corrupt the code when accepted. + 2. ``fix_after`` is the COMPLETE replacement for that + entire block (may be more or fewer lines). + 3. ``start_line`` / ``end_line`` must precisely cover the + lines in ``fix_before`` — no more, no less. + + **Multi-part fixes**: many fixes touch multiple + non-contiguous parts of a file (e.g. add an import at the + top AND change code lower down). Since each + ``fix_before`` / ``fix_after`` pair covers ONE contiguous + block, create **separate location entries** for each + non-contiguous part. Use ``label`` to describe each part's + role (``"Add escape helper import"``, ``"Sanitize input + before SQL"``). Order primary fix first, supporting + changes (imports, config) after. + + **Informational vs actionable**: + - With ``fix_before`` / ``fix_after``: actionable fix + (renders as a PR suggestion block). + - Without them: informational context (e.g. showing the + source of tainted data, or a sink that doesn't need + direct editing). + + **Per-location fields**: + - ``file`` (REQUIRED): path **relative** to repo root. No + leading slash, no ``..``, no ``/workspace/`` prefix. + Right: ``"src/db/queries.ts"``. Wrong: + ``"/workspace/repo/src/db/queries.ts"``, ``"./src/x.py"``, + ``"../../etc/passwd"``. + - ``start_line`` (REQUIRED): 1-based; positive integer. + Verify against the actual file — do NOT guess. + - ``end_line`` (REQUIRED): 1-based; ``>= start_line``. + Only equal to ``start_line`` when the block truly is one + line. + - ``snippet`` (optional): verbatim source at this range. + - ``label`` (optional): short role description; especially + important for multi-part fixes. + - ``fix_before`` (optional): verbatim copy of the + vulnerable code, lines ``start_line``-``end_line``. + - ``fix_after`` (optional): complete replacement for that + block; syntactically valid. + + **Common mistakes to avoid**: + - Guessing line numbers instead of reading the file. + - Paraphrasing / reformatting code in ``fix_before``. + - Setting ``start_line == end_line`` when the vulnerable + code spans multiple lines. + - Bundling an import addition and a far-away code change + into one location — split them. + - Padding ``fix_before`` with surrounding context lines + that aren't part of the fix. + - Duplicating the same change across multiple locations. """ inner = ctx.context if isinstance(ctx.context, dict) else {} raw_agent_id = inner.get("agent_id") diff --git a/strix/tools/thinking/tool.py b/strix/tools/thinking/tool.py index a4da899..8da93b1 100644 --- a/strix/tools/thinking/tool.py +++ b/strix/tools/thinking/tool.py @@ -33,5 +33,5 @@ async def think(thought: str) -> str: 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"}) + return json.dumps({"success": False, "error": "Thought cannot be empty"}) return json.dumps({"success": True, "message": "Thought recorded"}) diff --git a/strix/tools/todo/tools.py b/strix/tools/todo/tools.py index d4e5ba3..282f950 100644 --- a/strix/tools/todo/tools.py +++ b/strix/tools/todo/tools.py @@ -327,7 +327,7 @@ async def create_todo(ctx: RunContextWrapper, todos: str) -> str: tasks = _normalize_bulk_todos(todos) if not tasks: return json.dumps( - {"success": False, "error": "Provide a non-empty 'todos' list to create."}, + {"success": False, "error": "Provide a non-empty 'todos' list to create"}, ensure_ascii=False, default=str, ) @@ -469,7 +469,7 @@ async def update_todo(ctx: RunContextWrapper, updates: str) -> str: updates_to_apply = _normalize_bulk_updates(updates) if not updates_to_apply: return json.dumps( - {"success": False, "error": "Provide a non-empty 'updates' list."}, + {"success": False, "error": "Provide a non-empty 'updates' list"}, ensure_ascii=False, default=str, ) @@ -511,7 +511,7 @@ def _mark(*, agent_id: str, todo_ids: str, new_status: str) -> str: agent_todos = _get_agent_todos(agent_id) ids = _normalize_todo_ids(todo_ids) if not ids: - msg = f"Provide a non-empty 'todo_ids' list to mark as {new_status}." + msg = f"Provide a non-empty 'todo_ids' list to mark as {new_status}" return json.dumps({"success": False, "error": msg}, ensure_ascii=False, default=str) marked: list[str] = [] @@ -586,7 +586,7 @@ async def delete_todo(ctx: RunContextWrapper, todo_ids: str) -> str: ids = _normalize_todo_ids(todo_ids) if not ids: return json.dumps( - {"success": False, "error": "Provide a non-empty 'todo_ids' list to delete."}, + {"success": False, "error": "Provide a non-empty 'todo_ids' list to delete"}, ensure_ascii=False, default=str, ) diff --git a/strix/tools/web_search/tool.py b/strix/tools/web_search/tool.py index 88c09f7..1d202f9 100644 --- a/strix/tools/web_search/tool.py +++ b/strix/tools/web_search/tool.py @@ -43,16 +43,16 @@ security implications and details.""" def _do_search(query: str) -> dict[str, Any]: # noqa: PLR0911 - each error class needs its own sanitized return if not query or not query.strip(): - return {"success": False, "message": "Query cannot be empty."} + return {"success": False, "error": "Query cannot be empty"} api_key = load_settings().integrations.perplexity_api_key if not api_key: logger.warning("web_search invoked without PERPLEXITY_API_KEY configured") return { "success": False, - "message": ( + "error": ( "Web search is not configured for this scan " - "(operator needs to set PERPLEXITY_API_KEY). Proceed without it." + "(operator needs to set PERPLEXITY_API_KEY). Proceed without it" ), } logger.info("web_search query (len=%d): %s", len(query), query[:120]) @@ -78,7 +78,7 @@ def _do_search(query: str) -> dict[str, Any]: # noqa: PLR0911 - each error clas logger.warning("web_search timed out") return { "success": False, - "message": "Web search timed out. Try again or shorten the query.", + "error": "Web search timed out. Try again or shorten the query", } except requests.exceptions.HTTPError as exc: status = exc.response.status_code if exc.response is not None else None @@ -86,32 +86,32 @@ def _do_search(query: str) -> dict[str, Any]: # noqa: PLR0911 - each error clas if status is not None and 400 <= status < 500: return { "success": False, - "message": ( + "error": ( "Web search rejected the query. Refine it " - "(more specific, shorter, no unusual characters) and retry." + "(more specific, shorter, no unusual characters) and retry" ), } return { "success": False, - "message": "Web search service is unavailable. Try again later.", + "error": "Web search service is unavailable. Try again later", } except requests.exceptions.RequestException: logger.exception("web_search network error") return { "success": False, - "message": "Web search network error. Try again later.", + "error": "Web search network error. Try again later", } except (KeyError, IndexError, ValueError): logger.exception("web_search response shape unexpected") return { "success": False, - "message": "Web search returned an unexpected response. Try again.", + "error": "Web search returned an unexpected response. Try again", } except Exception: logger.exception("web_search failed") return { "success": False, - "message": "Web search failed unexpectedly.", + "error": "Web search failed unexpectedly", } else: return { @@ -155,6 +155,26 @@ async def web_search(ctx: RunContextWrapper, query: str) -> str: exploits, Kali-compatible tooling, and concrete code/command examples. + **Good example queries** (each is a full sentence, names a + version/product, and asks one concrete thing): + + - ``"Found OpenSSH 7.4 on port 22 — any known RCE or privesc for + this exact version?"`` + - ``"Cloudflare WAF is blocking my sqlmap on a login form — what + bypass techniques work in 2025?"`` + - ``"Target runs WordPress 5.8.3 + WooCommerce 6.1.1 — current + RCE chains for this combo?"`` + - ``"Low-priv shell on Ubuntu 20.04 kernel 5.4.0-74-generic — what + local privesc exploits hit this kernel?"`` + - ``"Compromised domain user on Windows Server 2019 AD — quietest + paths to Domain Admin without tripping EDR?"`` + - ``"'Access denied' uploading a webshell to IIS 10.0 — alternate + Windows IIS upload bypass techniques?"`` + - ``"Discovered Jenkins 2.401.3 on staging — current authn-bypass + and RCE exploits for this version?"`` + - ``"Best 2025 Python lib for JWT algorithm-confusion + weak-secret + cracking?"`` + Args: query: The search query — a full sentence with version numbers, target tech, and the specific question. Treat it like a