strix-agent 0.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- strix/__init__.py +0 -0
- strix/agents/StrixAgent/__init__.py +4 -0
- strix/agents/StrixAgent/strix_agent.py +60 -0
- strix/agents/StrixAgent/system_prompt.jinja +504 -0
- strix/agents/__init__.py +10 -0
- strix/agents/base_agent.py +394 -0
- strix/agents/state.py +139 -0
- strix/cli/__init__.py +4 -0
- strix/cli/app.py +1124 -0
- strix/cli/assets/cli.tcss +680 -0
- strix/cli/main.py +542 -0
- strix/cli/tool_components/__init__.py +39 -0
- strix/cli/tool_components/agents_graph_renderer.py +129 -0
- strix/cli/tool_components/base_renderer.py +61 -0
- strix/cli/tool_components/browser_renderer.py +107 -0
- strix/cli/tool_components/file_edit_renderer.py +95 -0
- strix/cli/tool_components/finish_renderer.py +32 -0
- strix/cli/tool_components/notes_renderer.py +108 -0
- strix/cli/tool_components/proxy_renderer.py +255 -0
- strix/cli/tool_components/python_renderer.py +34 -0
- strix/cli/tool_components/registry.py +72 -0
- strix/cli/tool_components/reporting_renderer.py +53 -0
- strix/cli/tool_components/scan_info_renderer.py +58 -0
- strix/cli/tool_components/terminal_renderer.py +99 -0
- strix/cli/tool_components/thinking_renderer.py +29 -0
- strix/cli/tool_components/user_message_renderer.py +43 -0
- strix/cli/tool_components/web_search_renderer.py +28 -0
- strix/cli/tracer.py +308 -0
- strix/llm/__init__.py +14 -0
- strix/llm/config.py +19 -0
- strix/llm/llm.py +310 -0
- strix/llm/memory_compressor.py +206 -0
- strix/llm/request_queue.py +63 -0
- strix/llm/utils.py +84 -0
- strix/prompts/__init__.py +113 -0
- strix/prompts/coordination/root_agent.jinja +41 -0
- strix/prompts/vulnerabilities/authentication_jwt.jinja +129 -0
- strix/prompts/vulnerabilities/business_logic.jinja +143 -0
- strix/prompts/vulnerabilities/csrf.jinja +168 -0
- strix/prompts/vulnerabilities/idor.jinja +164 -0
- strix/prompts/vulnerabilities/race_conditions.jinja +194 -0
- strix/prompts/vulnerabilities/rce.jinja +222 -0
- strix/prompts/vulnerabilities/sql_injection.jinja +216 -0
- strix/prompts/vulnerabilities/ssrf.jinja +168 -0
- strix/prompts/vulnerabilities/xss.jinja +221 -0
- strix/prompts/vulnerabilities/xxe.jinja +276 -0
- strix/runtime/__init__.py +19 -0
- strix/runtime/docker_runtime.py +298 -0
- strix/runtime/runtime.py +25 -0
- strix/runtime/tool_server.py +97 -0
- strix/tools/__init__.py +64 -0
- strix/tools/agents_graph/__init__.py +16 -0
- strix/tools/agents_graph/agents_graph_actions.py +610 -0
- strix/tools/agents_graph/agents_graph_actions_schema.xml +223 -0
- strix/tools/argument_parser.py +120 -0
- strix/tools/browser/__init__.py +4 -0
- strix/tools/browser/browser_actions.py +236 -0
- strix/tools/browser/browser_actions_schema.xml +183 -0
- strix/tools/browser/browser_instance.py +533 -0
- strix/tools/browser/tab_manager.py +342 -0
- strix/tools/executor.py +302 -0
- strix/tools/file_edit/__init__.py +4 -0
- strix/tools/file_edit/file_edit_actions.py +141 -0
- strix/tools/file_edit/file_edit_actions_schema.xml +128 -0
- strix/tools/finish/__init__.py +4 -0
- strix/tools/finish/finish_actions.py +167 -0
- strix/tools/finish/finish_actions_schema.xml +45 -0
- strix/tools/notes/__init__.py +14 -0
- strix/tools/notes/notes_actions.py +191 -0
- strix/tools/notes/notes_actions_schema.xml +150 -0
- strix/tools/proxy/__init__.py +20 -0
- strix/tools/proxy/proxy_actions.py +101 -0
- strix/tools/proxy/proxy_actions_schema.xml +267 -0
- strix/tools/proxy/proxy_manager.py +785 -0
- strix/tools/python/__init__.py +4 -0
- strix/tools/python/python_actions.py +47 -0
- strix/tools/python/python_actions_schema.xml +131 -0
- strix/tools/python/python_instance.py +172 -0
- strix/tools/python/python_manager.py +131 -0
- strix/tools/registry.py +196 -0
- strix/tools/reporting/__init__.py +6 -0
- strix/tools/reporting/reporting_actions.py +63 -0
- strix/tools/reporting/reporting_actions_schema.xml +30 -0
- strix/tools/terminal/__init__.py +4 -0
- strix/tools/terminal/terminal_actions.py +53 -0
- strix/tools/terminal/terminal_actions_schema.xml +114 -0
- strix/tools/terminal/terminal_instance.py +231 -0
- strix/tools/terminal/terminal_manager.py +191 -0
- strix/tools/thinking/__init__.py +4 -0
- strix/tools/thinking/thinking_actions.py +18 -0
- strix/tools/thinking/thinking_actions_schema.xml +52 -0
- strix/tools/web_search/__init__.py +4 -0
- strix/tools/web_search/web_search_actions.py +80 -0
- strix/tools/web_search/web_search_actions_schema.xml +83 -0
- strix_agent-0.1.1.dist-info/LICENSE +201 -0
- strix_agent-0.1.1.dist-info/METADATA +200 -0
- strix_agent-0.1.1.dist-info/RECORD +99 -0
- strix_agent-0.1.1.dist-info/WHEEL +4 -0
- strix_agent-0.1.1.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,141 @@
|
|
1
|
+
import json
|
2
|
+
import re
|
3
|
+
from pathlib import Path
|
4
|
+
from typing import Any, cast
|
5
|
+
|
6
|
+
from openhands_aci import file_editor
|
7
|
+
from openhands_aci.utils.shell import run_shell_cmd
|
8
|
+
|
9
|
+
from strix.tools.registry import register_tool
|
10
|
+
|
11
|
+
|
12
|
+
def _parse_file_editor_output(output: str) -> dict[str, Any]:
|
13
|
+
try:
|
14
|
+
pattern = r"<oh_aci_output_[^>]+>\n(.*?)\n</oh_aci_output_[^>]+>"
|
15
|
+
match = re.search(pattern, output, re.DOTALL)
|
16
|
+
|
17
|
+
if match:
|
18
|
+
json_str = match.group(1)
|
19
|
+
data = json.loads(json_str)
|
20
|
+
return cast("dict[str, Any]", data)
|
21
|
+
return {"output": output, "error": None}
|
22
|
+
except (json.JSONDecodeError, AttributeError):
|
23
|
+
return {"output": output, "error": None}
|
24
|
+
|
25
|
+
|
26
|
+
@register_tool
|
27
|
+
def str_replace_editor(
|
28
|
+
command: str,
|
29
|
+
path: str,
|
30
|
+
file_text: str | None = None,
|
31
|
+
view_range: list[int] | None = None,
|
32
|
+
old_str: str | None = None,
|
33
|
+
new_str: str | None = None,
|
34
|
+
insert_line: int | None = None,
|
35
|
+
) -> dict[str, Any]:
|
36
|
+
try:
|
37
|
+
path_obj = Path(path)
|
38
|
+
if not path_obj.is_absolute():
|
39
|
+
path = str(Path("/workspace") / path_obj)
|
40
|
+
|
41
|
+
result = file_editor(
|
42
|
+
command=command,
|
43
|
+
path=path,
|
44
|
+
file_text=file_text,
|
45
|
+
view_range=view_range,
|
46
|
+
old_str=old_str,
|
47
|
+
new_str=new_str,
|
48
|
+
insert_line=insert_line,
|
49
|
+
)
|
50
|
+
|
51
|
+
parsed = _parse_file_editor_output(result)
|
52
|
+
|
53
|
+
if parsed.get("error"):
|
54
|
+
return {"error": parsed["error"]}
|
55
|
+
|
56
|
+
return {"content": parsed.get("output", result)}
|
57
|
+
|
58
|
+
except (OSError, ValueError) as e:
|
59
|
+
return {"error": f"Error in {command} operation: {e!s}"}
|
60
|
+
|
61
|
+
|
62
|
+
@register_tool
|
63
|
+
def list_files(
|
64
|
+
path: str,
|
65
|
+
recursive: bool = False,
|
66
|
+
) -> dict[str, Any]:
|
67
|
+
try:
|
68
|
+
path_obj = Path(path)
|
69
|
+
if not path_obj.is_absolute():
|
70
|
+
path = str(Path("/workspace") / path_obj)
|
71
|
+
path_obj = Path(path)
|
72
|
+
|
73
|
+
if not path_obj.exists():
|
74
|
+
return {"error": f"Directory not found: {path}"}
|
75
|
+
|
76
|
+
if not path_obj.is_dir():
|
77
|
+
return {"error": f"Path is not a directory: {path}"}
|
78
|
+
|
79
|
+
cmd = f"find '{path}' -type f -o -type d | head -500" if recursive else f"ls -1a '{path}'"
|
80
|
+
|
81
|
+
exit_code, stdout, stderr = run_shell_cmd(cmd)
|
82
|
+
|
83
|
+
if exit_code != 0:
|
84
|
+
return {"error": f"Error listing directory: {stderr}"}
|
85
|
+
|
86
|
+
items = stdout.strip().split("\n") if stdout.strip() else []
|
87
|
+
|
88
|
+
files = []
|
89
|
+
dirs = []
|
90
|
+
|
91
|
+
for item in items:
|
92
|
+
item_path = item if recursive else str(Path(path) / item)
|
93
|
+
item_path_obj = Path(item_path)
|
94
|
+
|
95
|
+
if item_path_obj.is_file():
|
96
|
+
files.append(item)
|
97
|
+
elif item_path_obj.is_dir():
|
98
|
+
dirs.append(item)
|
99
|
+
|
100
|
+
return {
|
101
|
+
"files": sorted(files),
|
102
|
+
"directories": sorted(dirs),
|
103
|
+
"total_files": len(files),
|
104
|
+
"total_dirs": len(dirs),
|
105
|
+
"path": path,
|
106
|
+
"recursive": recursive,
|
107
|
+
}
|
108
|
+
|
109
|
+
except (OSError, ValueError) as e:
|
110
|
+
return {"error": f"Error listing directory: {e!s}"}
|
111
|
+
|
112
|
+
|
113
|
+
@register_tool
|
114
|
+
def search_files(
|
115
|
+
path: str,
|
116
|
+
regex: str,
|
117
|
+
file_pattern: str = "*",
|
118
|
+
) -> dict[str, Any]:
|
119
|
+
try:
|
120
|
+
path_obj = Path(path)
|
121
|
+
if not path_obj.is_absolute():
|
122
|
+
path = str(Path("/workspace") / path_obj)
|
123
|
+
|
124
|
+
if not Path(path).exists():
|
125
|
+
return {"error": f"Directory not found: {path}"}
|
126
|
+
|
127
|
+
escaped_regex = regex.replace("'", "'\"'\"'")
|
128
|
+
|
129
|
+
cmd = f"rg --line-number --glob '{file_pattern}' '{escaped_regex}' '{path}'"
|
130
|
+
|
131
|
+
exit_code, stdout, stderr = run_shell_cmd(cmd)
|
132
|
+
|
133
|
+
if exit_code not in {0, 1}:
|
134
|
+
return {"error": f"Error searching files: {stderr}"}
|
135
|
+
return {"output": stdout if stdout else "No matches found"}
|
136
|
+
|
137
|
+
except (OSError, ValueError) as e:
|
138
|
+
return {"error": f"Error searching files: {e!s}"}
|
139
|
+
|
140
|
+
|
141
|
+
# ruff: noqa: TRY300
|
@@ -0,0 +1,128 @@
|
|
1
|
+
<tools>
|
2
|
+
<tool name="list_files">
|
3
|
+
<description>List files and directories within the specified directory.</description>
|
4
|
+
<parameters>
|
5
|
+
<parameter name="path" type="string" required="true">
|
6
|
+
<description>Directory path to list</description>
|
7
|
+
</parameter>
|
8
|
+
<parameter name="recursive" type="boolean" required="false">
|
9
|
+
<description>Whether to list files recursively</description>
|
10
|
+
</parameter>
|
11
|
+
</parameters>
|
12
|
+
<returns type="Dict[str, Any]">
|
13
|
+
<description>Response containing: - files: List of files and directories - total_files: Total number of files found - total_dirs: Total number of directories found</description>
|
14
|
+
</returns>
|
15
|
+
<notes>
|
16
|
+
- Lists contents alphabetically
|
17
|
+
- Returns maximum 500 results to avoid overwhelming output
|
18
|
+
</notes>
|
19
|
+
<examples>
|
20
|
+
# List directory contents
|
21
|
+
<function=list_files>
|
22
|
+
<parameter=path>/home/user/project/src</parameter>
|
23
|
+
</function>
|
24
|
+
|
25
|
+
# Recursive listing
|
26
|
+
<function=list_files>
|
27
|
+
<parameter=path>/home/user/project/src</parameter>
|
28
|
+
<parameter=recursive>true</parameter>
|
29
|
+
</function>
|
30
|
+
</examples>
|
31
|
+
</tool>
|
32
|
+
<tool name="search_files">
|
33
|
+
<description>Perform a regex search across files in a directory.</description>
|
34
|
+
<parameters>
|
35
|
+
<parameter name="path" type="string" required="true">
|
36
|
+
<description>Directory path to search</description>
|
37
|
+
</parameter>
|
38
|
+
<parameter name="regex" type="string" required="true">
|
39
|
+
<description>Regular expression pattern to search for</description>
|
40
|
+
</parameter>
|
41
|
+
<parameter name="file_pattern" type="string" required="false">
|
42
|
+
<description>File pattern to filter (e.g., "*.py", "*.js")</description>
|
43
|
+
</parameter>
|
44
|
+
</parameters>
|
45
|
+
<returns type="Dict[str, Any]">
|
46
|
+
<description>Response containing: - output: The search results as a string</description>
|
47
|
+
</returns>
|
48
|
+
<notes>
|
49
|
+
- Searches recursively through subdirectories
|
50
|
+
- Uses ripgrep for fast searching
|
51
|
+
</notes>
|
52
|
+
<examples>
|
53
|
+
# Search Python files for a pattern
|
54
|
+
<function=search_files>
|
55
|
+
<parameter=path>/home/user/project/src</parameter>
|
56
|
+
<parameter=regex>def\s+process_data</parameter>
|
57
|
+
<parameter=file_pattern>*.py</parameter>
|
58
|
+
</function>
|
59
|
+
</examples>
|
60
|
+
</tool>
|
61
|
+
<tool name="str_replace_editor">
|
62
|
+
<description>A text editor tool for viewing, creating and editing files.</description>
|
63
|
+
<parameters>
|
64
|
+
<parameter name="command" type="string" required="true">
|
65
|
+
<description>Editor command to execute</description>
|
66
|
+
</parameter>
|
67
|
+
<parameter name="path" type="string" required="true">
|
68
|
+
<description>Path to the file to edit</description>
|
69
|
+
</parameter>
|
70
|
+
<parameter name="file_text" type="string" required="false">
|
71
|
+
<description>Required parameter of create command, with the content of the file to be created</description>
|
72
|
+
</parameter>
|
73
|
+
<parameter name="view_range" type="string" required="false">
|
74
|
+
<description>Optional parameter of view command when path points to a file. If none is given, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting [start_line, -1] shows all lines from start_line to the end of the file</description>
|
75
|
+
</parameter>
|
76
|
+
<parameter name="old_str" type="string" required="false">
|
77
|
+
<description>Required parameter of str_replace command containing the string in path to replace</description>
|
78
|
+
</parameter>
|
79
|
+
<parameter name="new_str" type="string" required="false">
|
80
|
+
<description>Optional parameter of str_replace command containing the new string (if not given, no string will be added). Required parameter of insert command containing the string to insert</description>
|
81
|
+
</parameter>
|
82
|
+
<parameter name="insert_line" type="string" required="false">
|
83
|
+
<description>Required parameter of insert command. The new_str will be inserted AFTER the line insert_line of path</description>
|
84
|
+
</parameter>
|
85
|
+
</parameters>
|
86
|
+
<returns type="Dict[str, Any]">
|
87
|
+
<description>Response containing the result of the operation</description>
|
88
|
+
</returns>
|
89
|
+
<notes>
|
90
|
+
Command details:
|
91
|
+
- view: Show file contents, optionally with line range
|
92
|
+
- create: Create a new file with given content
|
93
|
+
- str_replace: Replace old_str with new_str in file
|
94
|
+
- insert: Insert new_str after the specified line number
|
95
|
+
- undo_edit: Revert the last edit made to the file
|
96
|
+
</notes>
|
97
|
+
<examples>
|
98
|
+
# View a file
|
99
|
+
<function=str_replace_editor>
|
100
|
+
<parameter=command>view</parameter>
|
101
|
+
<parameter=path>/home/user/project/file.py</parameter>
|
102
|
+
</function>
|
103
|
+
|
104
|
+
# Create a file
|
105
|
+
<function=str_replace_editor>
|
106
|
+
<parameter=command>create</parameter>
|
107
|
+
<parameter=path>/home/user/project/new_file.py</parameter>
|
108
|
+
<parameter=file_text>print("Hello World")</parameter>
|
109
|
+
</function>
|
110
|
+
|
111
|
+
# Replace text in file
|
112
|
+
<function=str_replace_editor>
|
113
|
+
<parameter=command>str_replace</parameter>
|
114
|
+
<parameter=path>/home/user/project/file.py</parameter>
|
115
|
+
<parameter=old_str>old_function()</parameter>
|
116
|
+
<parameter=new_str>new_function()</parameter>
|
117
|
+
</function>
|
118
|
+
|
119
|
+
# Insert text after line 10
|
120
|
+
<function=str_replace_editor>
|
121
|
+
<parameter=command>insert</parameter>
|
122
|
+
<parameter=path>/home/user/project/file.py</parameter>
|
123
|
+
<parameter=insert_line>10</parameter>
|
124
|
+
<parameter=new_str>print("Inserted line")</parameter>
|
125
|
+
</function>
|
126
|
+
</examples>
|
127
|
+
</tool>
|
128
|
+
</tools>
|
@@ -0,0 +1,167 @@
|
|
1
|
+
from typing import Any
|
2
|
+
|
3
|
+
from strix.tools.registry import register_tool
|
4
|
+
|
5
|
+
|
6
|
+
def _validate_root_agent(agent_state: Any) -> dict[str, Any] | None:
|
7
|
+
if (
|
8
|
+
agent_state is not None
|
9
|
+
and hasattr(agent_state, "parent_id")
|
10
|
+
and agent_state.parent_id is not None
|
11
|
+
):
|
12
|
+
return {
|
13
|
+
"success": False,
|
14
|
+
"message": (
|
15
|
+
"This tool can only be used by the root/main agent. "
|
16
|
+
"Subagents must use agent_finish instead."
|
17
|
+
),
|
18
|
+
}
|
19
|
+
return None
|
20
|
+
|
21
|
+
|
22
|
+
def _validate_content(content: str) -> dict[str, Any] | None:
|
23
|
+
if not content or not content.strip():
|
24
|
+
return {"success": False, "message": "Content cannot be empty"}
|
25
|
+
return None
|
26
|
+
|
27
|
+
|
28
|
+
def _check_active_agents() -> dict[str, Any] | None:
|
29
|
+
try:
|
30
|
+
from strix.tools.agents_graph.agents_graph_actions import _agent_graph
|
31
|
+
|
32
|
+
running_agents = []
|
33
|
+
stopping_agents = []
|
34
|
+
|
35
|
+
for agent_id, node in _agent_graph.get("nodes", {}).items():
|
36
|
+
status = node.get("status", "")
|
37
|
+
if status == "running":
|
38
|
+
running_agents.append(
|
39
|
+
{
|
40
|
+
"id": agent_id,
|
41
|
+
"name": node.get("name", "Unknown"),
|
42
|
+
"task": node.get("task", "No task description"),
|
43
|
+
}
|
44
|
+
)
|
45
|
+
elif status == "stopping":
|
46
|
+
stopping_agents.append(
|
47
|
+
{
|
48
|
+
"id": agent_id,
|
49
|
+
"name": node.get("name", "Unknown"),
|
50
|
+
}
|
51
|
+
)
|
52
|
+
|
53
|
+
if running_agents or stopping_agents:
|
54
|
+
message_parts = ["Cannot finish scan while agents are still active:"]
|
55
|
+
|
56
|
+
if running_agents:
|
57
|
+
message_parts.append("\n\nRunning agents:")
|
58
|
+
message_parts.extend(
|
59
|
+
[
|
60
|
+
f" - {agent['name']} ({agent['id']}): {agent['task']}"
|
61
|
+
for agent in running_agents
|
62
|
+
]
|
63
|
+
)
|
64
|
+
|
65
|
+
if stopping_agents:
|
66
|
+
message_parts.append("\n\nStopping agents:")
|
67
|
+
message_parts.extend(
|
68
|
+
[f" - {agent['name']} ({agent['id']})" for agent in stopping_agents]
|
69
|
+
)
|
70
|
+
|
71
|
+
message_parts.extend(
|
72
|
+
[
|
73
|
+
"\n\nSuggested actions:",
|
74
|
+
"1. Use wait_for_message to wait for all agents to complete",
|
75
|
+
"2. Send messages to agents asking them to finish if urgent",
|
76
|
+
"3. Use view_agent_graph to monitor agent status",
|
77
|
+
]
|
78
|
+
)
|
79
|
+
|
80
|
+
return {
|
81
|
+
"success": False,
|
82
|
+
"message": "\n".join(message_parts),
|
83
|
+
"active_agents": {
|
84
|
+
"running": len(running_agents),
|
85
|
+
"stopping": len(stopping_agents),
|
86
|
+
"details": {
|
87
|
+
"running": running_agents,
|
88
|
+
"stopping": stopping_agents,
|
89
|
+
},
|
90
|
+
},
|
91
|
+
}
|
92
|
+
|
93
|
+
except ImportError:
|
94
|
+
import logging
|
95
|
+
|
96
|
+
logging.warning("Could not check agent graph status - agents_graph module unavailable")
|
97
|
+
|
98
|
+
return None
|
99
|
+
|
100
|
+
|
101
|
+
def _finalize_with_tracer(content: str, success: bool) -> dict[str, Any]:
|
102
|
+
try:
|
103
|
+
from strix.cli.tracer import get_global_tracer
|
104
|
+
|
105
|
+
tracer = get_global_tracer()
|
106
|
+
if tracer:
|
107
|
+
tracer.set_final_scan_result(
|
108
|
+
content=content.strip(),
|
109
|
+
success=success,
|
110
|
+
)
|
111
|
+
|
112
|
+
return {
|
113
|
+
"success": True,
|
114
|
+
"scan_completed": True,
|
115
|
+
"message": "Scan completed successfully"
|
116
|
+
if success
|
117
|
+
else "Scan completed with errors",
|
118
|
+
"vulnerabilities_found": len(tracer.vulnerability_reports),
|
119
|
+
}
|
120
|
+
|
121
|
+
import logging
|
122
|
+
|
123
|
+
logging.warning("Global tracer not available - final scan result not stored")
|
124
|
+
|
125
|
+
return { # noqa: TRY300
|
126
|
+
"success": True,
|
127
|
+
"scan_completed": True,
|
128
|
+
"message": "Scan completed successfully (not persisted)"
|
129
|
+
if success
|
130
|
+
else "Scan completed with errors (not persisted)",
|
131
|
+
"warning": "Final result could not be persisted - tracer unavailable",
|
132
|
+
}
|
133
|
+
|
134
|
+
except ImportError:
|
135
|
+
return {
|
136
|
+
"success": True,
|
137
|
+
"scan_completed": True,
|
138
|
+
"message": "Scan completed successfully (not persisted)"
|
139
|
+
if success
|
140
|
+
else "Scan completed with errors (not persisted)",
|
141
|
+
"warning": "Final result could not be persisted - tracer module unavailable",
|
142
|
+
}
|
143
|
+
|
144
|
+
|
145
|
+
@register_tool(sandbox_execution=False)
|
146
|
+
def finish_scan(
|
147
|
+
content: str,
|
148
|
+
success: bool = True,
|
149
|
+
agent_state: Any = None,
|
150
|
+
) -> dict[str, Any]:
|
151
|
+
try:
|
152
|
+
validation_error = _validate_root_agent(agent_state)
|
153
|
+
if validation_error:
|
154
|
+
return validation_error
|
155
|
+
|
156
|
+
validation_error = _validate_content(content)
|
157
|
+
if validation_error:
|
158
|
+
return validation_error
|
159
|
+
|
160
|
+
active_agents_error = _check_active_agents()
|
161
|
+
if active_agents_error:
|
162
|
+
return active_agents_error
|
163
|
+
|
164
|
+
return _finalize_with_tracer(content, success)
|
165
|
+
|
166
|
+
except (ValueError, TypeError, KeyError) as e:
|
167
|
+
return {"success": False, "message": f"Failed to complete scan: {e!s}"}
|
@@ -0,0 +1,45 @@
|
|
1
|
+
<tools>
|
2
|
+
<tool name="finish_scan">
|
3
|
+
<description>Complete the main security scan and generate final report.
|
4
|
+
|
5
|
+
IMPORTANT: This tool can ONLY be used by the root/main agent.
|
6
|
+
Subagents must use agent_finish from agents_graph tool instead.
|
7
|
+
|
8
|
+
IMPORTANT: This tool will NOT allow finishing if any agents are still running or stopping.
|
9
|
+
You must wait for all agents to complete before using this tool.
|
10
|
+
|
11
|
+
This tool MUST be called at the very end of the security assessment to:
|
12
|
+
- Verify all agents have completed their tasks
|
13
|
+
- Generate the final comprehensive scan report
|
14
|
+
- Mark the entire scan as completed
|
15
|
+
- Stop the agent from running
|
16
|
+
|
17
|
+
Use this tool when:
|
18
|
+
- You are the main/root agent conducting the security assessment
|
19
|
+
- ALL subagents have completed their tasks (no agents are "running" or "stopping")
|
20
|
+
- You have completed all testing phases
|
21
|
+
- You are ready to conclude the entire security assessment
|
22
|
+
|
23
|
+
IMPORTANT: Calling this tool multiple times will OVERWRITE any previous scan report.
|
24
|
+
Make sure you include ALL findings and details in a single comprehensive report.
|
25
|
+
|
26
|
+
If agents are still running, this tool will:
|
27
|
+
- Show you which agents are still active
|
28
|
+
- Suggest using wait_for_message to wait for completion
|
29
|
+
- Suggest messaging agents if immediate completion is needed
|
30
|
+
|
31
|
+
Put ALL details in the content - methodology, tools used, vulnerability counts, key findings, recommendations,
|
32
|
+
compliance notes, risk assessments, next steps, etc. Be comprehensive and include everything relevant.</description>
|
33
|
+
<parameters>
|
34
|
+
<parameter name="content" type="string" required="true">
|
35
|
+
<description>Complete scan report including executive summary, methodology, findings, vulnerability details, recommendations, compliance notes, risk assessment, and conclusions. Include everything relevant to the assessment.</description>
|
36
|
+
</parameter>
|
37
|
+
<parameter name="success" type="boolean" required="false">
|
38
|
+
<description>Whether the scan completed successfully without critical errors</description>
|
39
|
+
</parameter>
|
40
|
+
</parameters>
|
41
|
+
<returns type="Dict[str, Any]">
|
42
|
+
<description>Response containing success status and completion message. If agents are still running, returns details about active agents and suggested actions.</description>
|
43
|
+
</returns>
|
44
|
+
</tool>
|
45
|
+
</tools>
|
@@ -0,0 +1,191 @@
|
|
1
|
+
import uuid
|
2
|
+
from datetime import UTC, datetime
|
3
|
+
from typing import Any
|
4
|
+
|
5
|
+
from strix.tools.registry import register_tool
|
6
|
+
|
7
|
+
|
8
|
+
_notes_storage: dict[str, dict[str, Any]] = {}
|
9
|
+
|
10
|
+
|
11
|
+
def _filter_notes(
|
12
|
+
category: str | None = None,
|
13
|
+
tags: list[str] | None = None,
|
14
|
+
priority: str | None = None,
|
15
|
+
search_query: str | None = None,
|
16
|
+
) -> list[dict[str, Any]]:
|
17
|
+
filtered_notes = []
|
18
|
+
|
19
|
+
for note_id, note in _notes_storage.items():
|
20
|
+
if category and note.get("category") != category:
|
21
|
+
continue
|
22
|
+
|
23
|
+
if priority and note.get("priority") != priority:
|
24
|
+
continue
|
25
|
+
|
26
|
+
if tags:
|
27
|
+
note_tags = note.get("tags", [])
|
28
|
+
if not any(tag in note_tags for tag in tags):
|
29
|
+
continue
|
30
|
+
|
31
|
+
if search_query:
|
32
|
+
search_lower = search_query.lower()
|
33
|
+
title_match = search_lower in note.get("title", "").lower()
|
34
|
+
content_match = search_lower in note.get("content", "").lower()
|
35
|
+
if not (title_match or content_match):
|
36
|
+
continue
|
37
|
+
|
38
|
+
note_with_id = note.copy()
|
39
|
+
note_with_id["note_id"] = note_id
|
40
|
+
filtered_notes.append(note_with_id)
|
41
|
+
|
42
|
+
filtered_notes.sort(key=lambda x: x.get("created_at", ""), reverse=True)
|
43
|
+
return filtered_notes
|
44
|
+
|
45
|
+
|
46
|
+
@register_tool
|
47
|
+
def create_note(
|
48
|
+
title: str,
|
49
|
+
content: str,
|
50
|
+
category: str = "general",
|
51
|
+
tags: list[str] | None = None,
|
52
|
+
priority: str = "normal",
|
53
|
+
) -> dict[str, Any]:
|
54
|
+
try:
|
55
|
+
if not title or not title.strip():
|
56
|
+
return {"success": False, "error": "Title cannot be empty", "note_id": None}
|
57
|
+
|
58
|
+
if not content or not content.strip():
|
59
|
+
return {"success": False, "error": "Content cannot be empty", "note_id": None}
|
60
|
+
|
61
|
+
valid_categories = ["general", "findings", "methodology", "todo", "questions", "plan"]
|
62
|
+
if category not in valid_categories:
|
63
|
+
return {
|
64
|
+
"success": False,
|
65
|
+
"error": f"Invalid category. Must be one of: {', '.join(valid_categories)}",
|
66
|
+
"note_id": None,
|
67
|
+
}
|
68
|
+
|
69
|
+
valid_priorities = ["low", "normal", "high", "urgent"]
|
70
|
+
if priority not in valid_priorities:
|
71
|
+
return {
|
72
|
+
"success": False,
|
73
|
+
"error": f"Invalid priority. Must be one of: {', '.join(valid_priorities)}",
|
74
|
+
"note_id": None,
|
75
|
+
}
|
76
|
+
|
77
|
+
note_id = str(uuid.uuid4())[:5]
|
78
|
+
timestamp = datetime.now(UTC).isoformat()
|
79
|
+
|
80
|
+
note = {
|
81
|
+
"title": title.strip(),
|
82
|
+
"content": content.strip(),
|
83
|
+
"category": category,
|
84
|
+
"tags": tags or [],
|
85
|
+
"priority": priority,
|
86
|
+
"created_at": timestamp,
|
87
|
+
"updated_at": timestamp,
|
88
|
+
}
|
89
|
+
|
90
|
+
_notes_storage[note_id] = note
|
91
|
+
|
92
|
+
except (ValueError, TypeError) as e:
|
93
|
+
return {"success": False, "error": f"Failed to create note: {e}", "note_id": None}
|
94
|
+
else:
|
95
|
+
return {
|
96
|
+
"success": True,
|
97
|
+
"note_id": note_id,
|
98
|
+
"message": f"Note '{title}' created successfully",
|
99
|
+
}
|
100
|
+
|
101
|
+
|
102
|
+
@register_tool
|
103
|
+
def list_notes(
|
104
|
+
category: str | None = None,
|
105
|
+
tags: list[str] | None = None,
|
106
|
+
priority: str | None = None,
|
107
|
+
search: str | None = None,
|
108
|
+
) -> dict[str, Any]:
|
109
|
+
try:
|
110
|
+
filtered_notes = _filter_notes(
|
111
|
+
category=category, tags=tags, priority=priority, search_query=search
|
112
|
+
)
|
113
|
+
|
114
|
+
return {
|
115
|
+
"success": True,
|
116
|
+
"notes": filtered_notes,
|
117
|
+
"total_count": len(filtered_notes),
|
118
|
+
}
|
119
|
+
|
120
|
+
except (ValueError, TypeError) as e:
|
121
|
+
return {
|
122
|
+
"success": False,
|
123
|
+
"error": f"Failed to list notes: {e}",
|
124
|
+
"notes": [],
|
125
|
+
"total_count": 0,
|
126
|
+
}
|
127
|
+
|
128
|
+
|
129
|
+
@register_tool
|
130
|
+
def update_note(
|
131
|
+
note_id: str,
|
132
|
+
title: str | None = None,
|
133
|
+
content: str | None = None,
|
134
|
+
tags: list[str] | None = None,
|
135
|
+
priority: str | None = None,
|
136
|
+
) -> dict[str, Any]:
|
137
|
+
try:
|
138
|
+
if note_id not in _notes_storage:
|
139
|
+
return {"success": False, "error": f"Note with ID '{note_id}' not found"}
|
140
|
+
|
141
|
+
note = _notes_storage[note_id]
|
142
|
+
|
143
|
+
if title is not None:
|
144
|
+
if not title.strip():
|
145
|
+
return {"success": False, "error": "Title cannot be empty"}
|
146
|
+
note["title"] = title.strip()
|
147
|
+
|
148
|
+
if content is not None:
|
149
|
+
if not content.strip():
|
150
|
+
return {"success": False, "error": "Content cannot be empty"}
|
151
|
+
note["content"] = content.strip()
|
152
|
+
|
153
|
+
if tags is not None:
|
154
|
+
note["tags"] = tags
|
155
|
+
|
156
|
+
if priority is not None:
|
157
|
+
valid_priorities = ["low", "normal", "high", "urgent"]
|
158
|
+
if priority not in valid_priorities:
|
159
|
+
return {
|
160
|
+
"success": False,
|
161
|
+
"error": f"Invalid priority. Must be one of: {', '.join(valid_priorities)}",
|
162
|
+
}
|
163
|
+
note["priority"] = priority
|
164
|
+
|
165
|
+
note["updated_at"] = datetime.now(UTC).isoformat()
|
166
|
+
|
167
|
+
return {
|
168
|
+
"success": True,
|
169
|
+
"message": f"Note '{note['title']}' updated successfully",
|
170
|
+
}
|
171
|
+
|
172
|
+
except (ValueError, TypeError) as e:
|
173
|
+
return {"success": False, "error": f"Failed to update note: {e}"}
|
174
|
+
|
175
|
+
|
176
|
+
@register_tool
|
177
|
+
def delete_note(note_id: str) -> dict[str, Any]:
|
178
|
+
try:
|
179
|
+
if note_id not in _notes_storage:
|
180
|
+
return {"success": False, "error": f"Note with ID '{note_id}' not found"}
|
181
|
+
|
182
|
+
note_title = _notes_storage[note_id]["title"]
|
183
|
+
del _notes_storage[note_id]
|
184
|
+
|
185
|
+
except (ValueError, TypeError) as e:
|
186
|
+
return {"success": False, "error": f"Failed to delete note: {e}"}
|
187
|
+
else:
|
188
|
+
return {
|
189
|
+
"success": True,
|
190
|
+
"message": f"Note '{note_title}' deleted successfully",
|
191
|
+
}
|