Tighten tool surface consistency

Four passes of audit-and-patch on the tool surface, condensed.

Tool API shape:
- Todo tools collapse to a single list-based form (one arg per tool,
  always a list, no dual-mode validator). Result-field names line up
  across the family — created_count / updated_count / marked_count /
  deleted_count, and _mark returns a single "marked" key plus the new
  status instead of marked_done / marked_pending.
- list_notes splits the overloaded total_count into filtered_count
  (matches) and total_count (grand total), matching list_todos. All
  three notes mutations now echo total_count and note_id.
- finish_scan drops the machine-code error strings; a single human
  "error" key carries the reason on every failure path.
- scope_rules delete echoes a message so the renderer's success
  branch has something to surface.

Failure-key unification: every tool now uses {"success": False,
"error": "..."} on failure paths. Touched thinking, web_search,
reporting, and finish. Trailing periods on error strings swept clean
across the whole tool tree.

Tool prompts (docstring re-imports vs main):
- create_vulnerability_report re-imports the CWE reference catalog,
  multi-part fix rules, fix_before/fix_after PR-suggestion mechanics,
  the COMMON MISTAKES list, the informational-vs-actionable
  distinction, and file-path examples.
- web_search re-imports concrete example queries.
- list_sitemap docstring fixed hasDescendants -> has_descendants
  (the camelCase reference never matched our snake_case schema).
- create_agent.skills description "Comma-separated" -> "List of".
- factory.py module docstring no longer claims there's no runtime
  skill-loading tool. agents_graph module docstring lists stop_agent.
- system_prompt nudges loading the matching skill before guessing
  payloads or syntax from memory.

TUI:
- proxy_renderer was reading stale field names from the pre-SDK
  schema (requests / total_count / statusCode / matches /
  showing_lines); now reads entries / page_info / status_code / hits
  / page+total_lines. Three proxy operations were rendering empty
  before this.
- Idle-pane placeholder text trimmed to "Loading...".

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
0xallam
2026-05-26 12:16:47 -07:00
parent 763df86f17
commit 054eedf53f
13 changed files with 224 additions and 96 deletions
+3 -2
View File
@@ -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
+1
View File
@@ -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
+4 -4
View File
@@ -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]]:
+31 -26
View File
@@ -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]
+18 -15
View File
@@ -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,
+8 -8
View File
@@ -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,
},
+14 -3
View File
@@ -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.
+2 -2
View File
@@ -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,
+14 -6
View File
@@ -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)
+94 -15
View File
@@ -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")
+1 -1
View File
@@ -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"})
+4 -4
View File
@@ -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,
)
+30 -10
View File
@@ -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