docs: restore tool guidance into docstrings, drop prompt tool-format boilerplate

Port the prose guidance that previously lived in the deleted
*_actions_schema.xml files into per-tool docstrings, so the SDK's
auto-generated function schema carries the same domain knowledge
(HTTPQL syntax, Caido sitemap kinds, browser persistence/JS rules,
agent specialization caps, customer-facing report rules, CVSS/CWE
guidance, etc.) without any custom prompt scaffolding.

Strip the <tool_usage> block from system_prompt.jinja — XML format
guidance, the "CRITICAL RULES" 0-8 list, and the </function>
closing-tag reminder all contradicted the SDK's native JSON
function-calling protocol.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
0xallam
2026-04-25 11:48:41 -07:00
parent 6435e07dc2
commit e4be5f9588
13 changed files with 798 additions and 197 deletions
-71
View File
@@ -359,77 +359,6 @@ PERSISTENCE IS MANDATORY:
- There are ALWAYS more attack vectors to explore
</multi_agent_system>
<tool_usage>
Tool call format:
<function=tool_name>
<parameter=param_name>value</parameter>
</function>
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 <function>...</function> block in a single LLM message.
2. Tool call must be last in message
3. EVERY tool call MUST end with </function>. This is MANDATORY. Never omit the closing tag. End your response immediately after </function>.
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 <parameter=param_name>value</parameter> 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:
<function=tool_name>
<parameter=param_name>value</parameter>
</function>
WRONG formats — NEVER use these:
- <invoke name="tool_name"><parameter name="param_name">value</parameter></invoke>
- <function_calls><invoke name="tool_name">...</invoke></function_calls>
- <tool_call><tool_name>...</tool_name></tool_call>
- {"tool_name": {"param_name": "value"}}
- ```<function=tool_name>...</function>```
- <function=tool_name>value_without_parameter_tags</function>
EVERY argument MUST be wrapped in <parameter=name>...</parameter> 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 <thinking>...</thinking> or <thought>...</thought> blocks
- NO <scratchpad>...</scratchpad> or <reasoning>...</reasoning> blocks
- NO <answer>...</answer> or <response>...</response> 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 <function=X> NOT <invoke name="X">, use <parameter=X> NOT <parameter name="X">, use </function> NOT </invoke>.
Example (terminal tool):
<function=terminal_execute>
<parameter=command>nmap -sV -p 1-1000 target.com</parameter>
</function>
Example (agent creation tool):
<function=create_agent>
<parameter=task>Perform targeted XSS testing on the search endpoint</parameter>
<parameter=name>XSS Discovery Agent</parameter>
<parameter=skills>xss</parameter>
</function>
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 </function> before going into the next. Incomplete tool calls will fail.
</tool_usage>
<environment>
Docker container with Kali Linux and comprehensive security tools:
+128 -37
View File
@@ -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
``<inter_agent_message>``. 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
``<inherited_context_from_parent>``. 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 ``<agent_completion_report>`` 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")
+64 -9
View File
@@ -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(
+39 -13
View File
@@ -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(
+48 -7
View File
@@ -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(
+70 -8
View File
@@ -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/<slug>.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/<slug>.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))
+155 -20
View File
@@ -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}),
)
+45 -5
View File
@@ -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).
+71 -4
View File
@@ -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(
+58 -8
View File
@@ -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(
+19 -6
View File
@@ -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"})
+69 -6
View File
@@ -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)
+32 -3
View File
@@ -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)