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:
@@ -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:
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
@@ -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}),
|
||||
)
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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"})
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user