strix-agent 1.5.1__py3-none-win_amd64.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/__init__.py +0 -0
- strix/agents/factory.py +669 -0
- strix/agents/prompt.py +110 -0
- strix/agents/prompts/system_prompt.jinja +499 -0
- strix/bin/strix-tui.exe +0 -0
- strix/config/__init__.py +41 -0
- strix/config/codex.py +403 -0
- strix/config/loader.py +125 -0
- strix/config/models.py +883 -0
- strix/config/settings.py +156 -0
- strix/config/tool_call_ids.py +117 -0
- strix/config/tool_call_limits.py +46 -0
- strix/core/__init__.py +1 -0
- strix/core/agents.py +536 -0
- strix/core/execution.py +1032 -0
- strix/core/hooks.py +273 -0
- strix/core/inputs.py +296 -0
- strix/core/paths.py +40 -0
- strix/core/runner.py +467 -0
- strix/core/sessions.py +196 -0
- strix/interface/__init__.py +4 -0
- strix/interface/auth_cli.py +419 -0
- strix/interface/cli.py +230 -0
- strix/interface/cli_args.py +379 -0
- strix/interface/environment.py +217 -0
- strix/interface/interactive.py +38 -0
- strix/interface/main.py +500 -0
- strix/interface/scan_setup.py +265 -0
- strix/interface/tui/__init__.py +6 -0
- strix/interface/tui/backend/__init__.py +7 -0
- strix/interface/tui/backend/controller.py +497 -0
- strix/interface/tui/backend/live_view.py +136 -0
- strix/interface/tui/backend/messages.py +61 -0
- strix/interface/tui/backend/projection.py +182 -0
- strix/interface/tui/backend/protocol.py +40 -0
- strix/interface/tui/backend/server.py +531 -0
- strix/interface/tui/history.py +72 -0
- strix/interface/tui/live_view.py +509 -0
- strix/interface/tui/runtime.py +392 -0
- strix/interface/tui/sidecar.py +191 -0
- strix/interface/update_check.py +396 -0
- strix/interface/utils.py +1682 -0
- strix/interface/viewer/__init__.py +12 -0
- strix/interface/viewer/auth.py +262 -0
- strix/interface/viewer/cli.py +142 -0
- strix/interface/viewer/report_pdf.py +672 -0
- strix/interface/viewer/server.py +614 -0
- strix/interface/viewer/static/assets/index-DBJ-RJqo.js +487 -0
- strix/interface/viewer/static/assets/index-DKbLYAbP.css +10 -0
- strix/interface/viewer/static/index.html +15 -0
- strix/interface/viewer/static/logo.png +0 -0
- strix/interface/viewer/transcript.py +104 -0
- strix/llm/__init__.py +1 -0
- strix/llm/compaction.py +386 -0
- strix/llm/context_budget.py +87 -0
- strix/report/__init__.py +12 -0
- strix/report/dedupe.py +422 -0
- strix/report/sarif.py +1051 -0
- strix/report/state.py +727 -0
- strix/report/usage.py +267 -0
- strix/report/writer.py +297 -0
- strix/runtime/__init__.py +1 -0
- strix/runtime/backends.py +105 -0
- strix/runtime/caido_bootstrap.py +109 -0
- strix/runtime/docker_client.py +292 -0
- strix/runtime/session_manager.py +233 -0
- strix/runtime/status.py +8 -0
- strix/skills/README.md +71 -0
- strix/skills/__init__.py +299 -0
- strix/skills/cloud/.gitkeep +0 -0
- strix/skills/cloud/aws.md +231 -0
- strix/skills/cloud/gcp.md +194 -0
- strix/skills/cloud/kubernetes.md +223 -0
- strix/skills/coordination/root_agent.md +91 -0
- strix/skills/coordination/source_aware_whitebox.md +47 -0
- strix/skills/custom/.gitkeep +0 -0
- strix/skills/custom/api_spec_testing.md +61 -0
- strix/skills/custom/dependency_cve_scanning.md +253 -0
- strix/skills/custom/source_aware_sast.md +157 -0
- strix/skills/frameworks/django.md +214 -0
- strix/skills/frameworks/fastapi.md +191 -0
- strix/skills/frameworks/nestjs.md +225 -0
- strix/skills/frameworks/nextjs.md +228 -0
- strix/skills/protocols/graphql.md +276 -0
- strix/skills/protocols/oauth.md +185 -0
- strix/skills/reconnaissance/.gitkeep +0 -0
- strix/skills/reconnaissance/asset_discovery.md +150 -0
- strix/skills/scan_modes/deep.md +163 -0
- strix/skills/scan_modes/quick.md +68 -0
- strix/skills/scan_modes/standard.md +99 -0
- strix/skills/technologies/active_directory.md +233 -0
- strix/skills/technologies/auth0.md +188 -0
- strix/skills/technologies/firebase.md +263 -0
- strix/skills/technologies/grafana_prometheus.md +189 -0
- strix/skills/technologies/supabase.md +268 -0
- strix/skills/tooling/agent_browser.md +518 -0
- strix/skills/tooling/ffuf.md +72 -0
- strix/skills/tooling/httpx.md +82 -0
- strix/skills/tooling/katana.md +102 -0
- strix/skills/tooling/naabu.md +68 -0
- strix/skills/tooling/nmap.md +66 -0
- strix/skills/tooling/nuclei.md +67 -0
- strix/skills/tooling/python.md +109 -0
- strix/skills/tooling/semgrep.md +72 -0
- strix/skills/tooling/sqlmap.md +67 -0
- strix/skills/tooling/subfinder.md +66 -0
- strix/skills/vulnerabilities/authentication_jwt.md +166 -0
- strix/skills/vulnerabilities/broken_function_level_authorization.md +154 -0
- strix/skills/vulnerabilities/business_logic.md +178 -0
- strix/skills/vulnerabilities/csrf.md +198 -0
- strix/skills/vulnerabilities/header_injection.md +210 -0
- strix/skills/vulnerabilities/http_request_smuggling.md +255 -0
- strix/skills/vulnerabilities/idor.md +217 -0
- strix/skills/vulnerabilities/information_disclosure.md +187 -0
- strix/skills/vulnerabilities/insecure_deserialization.md +188 -0
- strix/skills/vulnerabilities/insecure_file_uploads.md +188 -0
- strix/skills/vulnerabilities/llm_prompt_injection.md +181 -0
- strix/skills/vulnerabilities/mass_assignment.md +153 -0
- strix/skills/vulnerabilities/nosql_injection.md +288 -0
- strix/skills/vulnerabilities/open_redirect.md +165 -0
- strix/skills/vulnerabilities/path_traversal_lfi_rfi.md +190 -0
- strix/skills/vulnerabilities/prototype_pollution.md +142 -0
- strix/skills/vulnerabilities/race_conditions.md +181 -0
- strix/skills/vulnerabilities/rce.md +249 -0
- strix/skills/vulnerabilities/sql_injection.md +190 -0
- strix/skills/vulnerabilities/ssrf.md +186 -0
- strix/skills/vulnerabilities/ssti.md +270 -0
- strix/skills/vulnerabilities/subdomain_takeover.md +165 -0
- strix/skills/vulnerabilities/weak_password_detection.md +200 -0
- strix/skills/vulnerabilities/xss.md +206 -0
- strix/skills/vulnerabilities/xxe.md +223 -0
- strix/telemetry/README.md +37 -0
- strix/telemetry/__init__.py +7 -0
- strix/telemetry/_common.py +56 -0
- strix/telemetry/logging.py +183 -0
- strix/telemetry/posthog.py +192 -0
- strix/telemetry/scarf.py +162 -0
- strix/tools/__init__.py +11 -0
- strix/tools/agent_browser/README.md +12 -0
- strix/tools/agents_graph/__init__.py +0 -0
- strix/tools/agents_graph/tools.py +740 -0
- strix/tools/apply_patch/README.md +10 -0
- strix/tools/finish/__init__.py +0 -0
- strix/tools/finish/tool.py +291 -0
- strix/tools/load_skill/__init__.py +0 -0
- strix/tools/load_skill/tool.py +36 -0
- strix/tools/notes/__init__.py +0 -0
- strix/tools/notes/tools.py +489 -0
- strix/tools/output_store.py +166 -0
- strix/tools/proxy/__init__.py +0 -0
- strix/tools/proxy/caido_api.py +759 -0
- strix/tools/proxy/tools.py +638 -0
- strix/tools/reporting/__init__.py +0 -0
- strix/tools/reporting/tool.py +1446 -0
- strix/tools/respond/__init__.py +6 -0
- strix/tools/respond/tool.py +110 -0
- strix/tools/shell/README.md +32 -0
- strix/tools/thinking/__init__.py +0 -0
- strix/tools/thinking/tool.py +37 -0
- strix/tools/todo/__init__.py +0 -0
- strix/tools/todo/tools.py +585 -0
- strix/tools/view_image/README.md +17 -0
- strix/tools/web_search/__init__.py +0 -0
- strix/tools/web_search/tool.py +179 -0
- strix/utils/__init__.py +0 -0
- strix/utils/api_spec.py +312 -0
- strix/utils/resource_paths.py +13 -0
- strix/utils/secret_files.py +31 -0
- strix_agent-1.5.1.dist-info/METADATA +396 -0
- strix_agent-1.5.1.dist-info/RECORD +174 -0
- strix_agent-1.5.1.dist-info/WHEEL +4 -0
- strix_agent-1.5.1.dist-info/entry_points.txt +2 -0
- strix_agent-1.5.1.dist-info/licenses/LICENSE +201 -0
strix/__init__.py
ADDED
|
File without changes
|
strix/agents/__init__.py
ADDED
|
File without changes
|
strix/agents/factory.py
ADDED
|
@@ -0,0 +1,669 @@
|
|
|
1
|
+
"""Build SandboxAgents for root + child Strix runs."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import inspect
|
|
6
|
+
import json
|
|
7
|
+
import logging
|
|
8
|
+
import re
|
|
9
|
+
from typing import TYPE_CHECKING, Any
|
|
10
|
+
|
|
11
|
+
from agents.agent import ToolsToFinalOutputResult
|
|
12
|
+
from agents.sandbox import SandboxAgent
|
|
13
|
+
from agents.sandbox.capabilities import Filesystem, Shell
|
|
14
|
+
from agents.sandbox.errors import InvalidManifestPathError
|
|
15
|
+
from agents.tool import CustomTool, FunctionTool, Tool
|
|
16
|
+
from pydantic import ValidationError
|
|
17
|
+
|
|
18
|
+
from strix.agents.prompt import render_system_prompt
|
|
19
|
+
from strix.config import load_settings
|
|
20
|
+
from strix.tools.agents_graph.tools import (
|
|
21
|
+
agent_finish,
|
|
22
|
+
create_agent,
|
|
23
|
+
send_message_to_agent,
|
|
24
|
+
stop_agent,
|
|
25
|
+
view_agent_graph,
|
|
26
|
+
wait_for_agents,
|
|
27
|
+
)
|
|
28
|
+
from strix.tools.finish.tool import finish_scan
|
|
29
|
+
from strix.tools.load_skill.tool import load_skill
|
|
30
|
+
from strix.tools.notes.tools import (
|
|
31
|
+
create_note,
|
|
32
|
+
delete_note,
|
|
33
|
+
get_note,
|
|
34
|
+
list_notes,
|
|
35
|
+
update_note,
|
|
36
|
+
)
|
|
37
|
+
from strix.tools.output_store import bound_and_store, bound_text
|
|
38
|
+
from strix.tools.proxy.tools import (
|
|
39
|
+
list_requests,
|
|
40
|
+
list_sitemap,
|
|
41
|
+
repeat_request,
|
|
42
|
+
scope_rules,
|
|
43
|
+
view_request,
|
|
44
|
+
view_sitemap_entry,
|
|
45
|
+
)
|
|
46
|
+
from strix.tools.reporting.tool import (
|
|
47
|
+
create_dependency_report,
|
|
48
|
+
create_vulnerability_report,
|
|
49
|
+
get_report,
|
|
50
|
+
list_reports,
|
|
51
|
+
)
|
|
52
|
+
from strix.tools.respond.tool import respond_to_user
|
|
53
|
+
from strix.tools.thinking.tool import think
|
|
54
|
+
from strix.tools.todo.tools import (
|
|
55
|
+
create_todo,
|
|
56
|
+
delete_todo,
|
|
57
|
+
list_todos,
|
|
58
|
+
mark_todo_done,
|
|
59
|
+
mark_todo_pending,
|
|
60
|
+
update_todo,
|
|
61
|
+
)
|
|
62
|
+
from strix.tools.web_search.tool import web_search
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
if TYPE_CHECKING:
|
|
66
|
+
from collections.abc import Awaitable, Callable, Sequence
|
|
67
|
+
|
|
68
|
+
from agents import RunContextWrapper
|
|
69
|
+
from agents.tool import FunctionToolResult
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
logger = logging.getLogger(__name__)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
_CUSTOM_TOOL_INPUT_FIELD_BY_NAME = {
|
|
76
|
+
"apply_patch": "patch",
|
|
77
|
+
}
|
|
78
|
+
_DEFAULT_CUSTOM_TOOL_INPUT_FIELD = "input"
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _custom_tool_input_field(tool: CustomTool) -> str:
|
|
82
|
+
return _CUSTOM_TOOL_INPUT_FIELD_BY_NAME.get(tool.name, _DEFAULT_CUSTOM_TOOL_INPUT_FIELD)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _raw_input_schema(tool: CustomTool) -> dict[str, Any]:
|
|
86
|
+
input_field = _custom_tool_input_field(tool)
|
|
87
|
+
return {
|
|
88
|
+
"type": "object",
|
|
89
|
+
"properties": {
|
|
90
|
+
input_field: {
|
|
91
|
+
"type": "string",
|
|
92
|
+
"description": (
|
|
93
|
+
f"Complete `{tool.name}` payload. Follow the tool description exactly."
|
|
94
|
+
),
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
"required": [input_field],
|
|
98
|
+
"additionalProperties": False,
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _extract_custom_input(tool: CustomTool, raw_input: str | dict[str, Any]) -> str:
|
|
103
|
+
if isinstance(raw_input, str):
|
|
104
|
+
try:
|
|
105
|
+
parsed = json.loads(raw_input)
|
|
106
|
+
except json.JSONDecodeError:
|
|
107
|
+
return ""
|
|
108
|
+
else:
|
|
109
|
+
parsed = raw_input
|
|
110
|
+
value = parsed.get(_custom_tool_input_field(tool))
|
|
111
|
+
return value if isinstance(value, str) else ""
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _tool_output_limits() -> tuple[int, int]:
|
|
115
|
+
context = load_settings().context
|
|
116
|
+
return context.tool_output_max_lines, context.tool_output_max_bytes
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
async def _bound_result(result: Any) -> Any:
|
|
120
|
+
if not isinstance(result, str):
|
|
121
|
+
return result
|
|
122
|
+
max_lines, max_bytes = _tool_output_limits()
|
|
123
|
+
return await bound_and_store(result, max_lines=max_lines, max_bytes=max_bytes)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _format_tool_error(exc: Exception) -> str:
|
|
127
|
+
message = str(exc) or exc.__class__.__name__
|
|
128
|
+
max_lines, max_bytes = _tool_output_limits()
|
|
129
|
+
return bound_text(message, max_lines=max_lines, max_bytes=max_bytes)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _with_bounded_result(tool: FunctionTool) -> FunctionTool:
|
|
133
|
+
"""Cap a tool's result size before it enters history (idempotent)."""
|
|
134
|
+
if getattr(tool, "_strix_bounded", False):
|
|
135
|
+
return tool
|
|
136
|
+
invoke_tool = tool.on_invoke_tool
|
|
137
|
+
|
|
138
|
+
async def invoke(ctx: Any, raw_input: str) -> Any:
|
|
139
|
+
return await _bound_result(await invoke_tool(ctx, raw_input))
|
|
140
|
+
|
|
141
|
+
tool.on_invoke_tool = invoke
|
|
142
|
+
tool._strix_bounded = True # type: ignore[attr-defined]
|
|
143
|
+
return tool
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _schema_types(spec: dict[str, Any]) -> set[str]:
|
|
147
|
+
types: set[str] = set()
|
|
148
|
+
raw = spec.get("type")
|
|
149
|
+
if isinstance(raw, str):
|
|
150
|
+
types.add(raw)
|
|
151
|
+
elif isinstance(raw, list):
|
|
152
|
+
types.update(t for t in raw if isinstance(t, str))
|
|
153
|
+
for variant in spec.get("anyOf") or ():
|
|
154
|
+
if isinstance(variant, dict):
|
|
155
|
+
types |= _schema_types(variant)
|
|
156
|
+
types.discard("null")
|
|
157
|
+
return types
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _decode_structured(value: str, types: set[str]) -> Any:
|
|
161
|
+
stripped = value.strip()
|
|
162
|
+
if not stripped:
|
|
163
|
+
return value
|
|
164
|
+
try:
|
|
165
|
+
decoded = json.loads(stripped)
|
|
166
|
+
except json.JSONDecodeError:
|
|
167
|
+
return value
|
|
168
|
+
wanted = list if "array" in types else dict
|
|
169
|
+
return decoded if isinstance(decoded, wanted) else value
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _coerce_argument(value: Any, spec: dict[str, Any]) -> Any:
|
|
173
|
+
types = _schema_types(spec)
|
|
174
|
+
if not types or value is None:
|
|
175
|
+
return value
|
|
176
|
+
if isinstance(value, list | dict) and "string" in types and not types & {"array", "object"}:
|
|
177
|
+
return json.dumps(value, ensure_ascii=False)
|
|
178
|
+
if isinstance(value, str) and types & {"array", "object"} and "string" not in types:
|
|
179
|
+
return _decode_structured(value, types)
|
|
180
|
+
return value
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _coerce_arguments(raw_input: str, schema: dict[str, Any]) -> str:
|
|
184
|
+
properties = schema.get("properties")
|
|
185
|
+
if not isinstance(properties, dict) or not properties:
|
|
186
|
+
return raw_input
|
|
187
|
+
try:
|
|
188
|
+
payload = json.loads(raw_input) if raw_input else None
|
|
189
|
+
except json.JSONDecodeError:
|
|
190
|
+
return raw_input
|
|
191
|
+
if not isinstance(payload, dict):
|
|
192
|
+
return raw_input
|
|
193
|
+
|
|
194
|
+
changed = False
|
|
195
|
+
for key, value in payload.items():
|
|
196
|
+
spec = properties.get(key)
|
|
197
|
+
if not isinstance(spec, dict):
|
|
198
|
+
continue
|
|
199
|
+
coerced = _coerce_argument(value, spec)
|
|
200
|
+
if coerced is not value:
|
|
201
|
+
payload[key] = coerced
|
|
202
|
+
changed = True
|
|
203
|
+
|
|
204
|
+
if not changed:
|
|
205
|
+
return raw_input
|
|
206
|
+
return json.dumps(payload, ensure_ascii=False)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _with_coerced_arguments(tool: FunctionTool) -> FunctionTool:
|
|
210
|
+
if getattr(tool, "_strix_coerced", False):
|
|
211
|
+
return tool
|
|
212
|
+
invoke_tool = tool.on_invoke_tool
|
|
213
|
+
schema = tool.params_json_schema
|
|
214
|
+
|
|
215
|
+
async def invoke(ctx: Any, raw_input: str) -> Any:
|
|
216
|
+
return await invoke_tool(ctx, _coerce_arguments(raw_input, schema))
|
|
217
|
+
|
|
218
|
+
tool.on_invoke_tool = invoke
|
|
219
|
+
tool._strix_coerced = True # type: ignore[attr-defined]
|
|
220
|
+
return tool
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _function_tool_with_error_result(tool: FunctionTool) -> FunctionTool:
|
|
224
|
+
invoke_tool = tool.on_invoke_tool
|
|
225
|
+
|
|
226
|
+
async def invoke(ctx: Any, raw_input: str) -> Any:
|
|
227
|
+
try:
|
|
228
|
+
return await _bound_result(await invoke_tool(ctx, raw_input))
|
|
229
|
+
except Exception as exc: # noqa: BLE001 - tool errors should be model-visible results.
|
|
230
|
+
logger.debug("Tool %s failed; returning error as result", tool.name, exc_info=True)
|
|
231
|
+
return _format_tool_error(exc)
|
|
232
|
+
|
|
233
|
+
tool.on_invoke_tool = invoke
|
|
234
|
+
return tool
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _custom_tool_as_function_tool(tool: CustomTool) -> FunctionTool:
|
|
238
|
+
async def invoke(ctx: Any, raw_input: str) -> Any:
|
|
239
|
+
custom_input = _extract_custom_input(tool, raw_input)
|
|
240
|
+
if not custom_input:
|
|
241
|
+
return f"`{_custom_tool_input_field(tool)}` must be a non-empty string."
|
|
242
|
+
try:
|
|
243
|
+
return await _bound_result(await tool.on_invoke_tool(ctx, custom_input))
|
|
244
|
+
except Exception as exc: # noqa: BLE001 - matches SDK CustomTool error-as-result behavior.
|
|
245
|
+
logger.debug("Tool %s failed; returning error as result", tool.name, exc_info=True)
|
|
246
|
+
return _format_tool_error(exc)
|
|
247
|
+
|
|
248
|
+
needs_approval = tool.runtime_needs_approval()
|
|
249
|
+
function_needs_approval: bool | Callable[[Any, dict[str, Any], str], Awaitable[bool]]
|
|
250
|
+
if callable(needs_approval):
|
|
251
|
+
|
|
252
|
+
async def approve(ctx: Any, args: dict[str, Any], call_id: str) -> bool:
|
|
253
|
+
result = needs_approval(ctx, _extract_custom_input(tool, args), call_id)
|
|
254
|
+
if inspect.isawaitable(result):
|
|
255
|
+
result = await result
|
|
256
|
+
return bool(result)
|
|
257
|
+
|
|
258
|
+
function_needs_approval = approve
|
|
259
|
+
else:
|
|
260
|
+
function_needs_approval = needs_approval
|
|
261
|
+
|
|
262
|
+
return FunctionTool(
|
|
263
|
+
name=tool.name,
|
|
264
|
+
description=(
|
|
265
|
+
f"{tool.description}\n\n"
|
|
266
|
+
f"Pass the complete `{tool.name}` payload in `{_custom_tool_input_field(tool)}`."
|
|
267
|
+
),
|
|
268
|
+
params_json_schema=_raw_input_schema(tool),
|
|
269
|
+
on_invoke_tool=invoke,
|
|
270
|
+
strict_json_schema=False,
|
|
271
|
+
needs_approval=function_needs_approval,
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def _bound_custom_tool(tool: CustomTool) -> CustomTool:
|
|
276
|
+
"""Bound a native ``CustomTool`` result in place (Responses path)."""
|
|
277
|
+
invoke_tool = tool.on_invoke_tool
|
|
278
|
+
|
|
279
|
+
async def invoke(ctx: Any, raw_input: str) -> Any:
|
|
280
|
+
return await _bound_result(await invoke_tool(ctx, raw_input))
|
|
281
|
+
|
|
282
|
+
tool.on_invoke_tool = invoke
|
|
283
|
+
return tool
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def _configure_filesystem_tools(toolset: Any, *, chat_completions: bool) -> None:
|
|
287
|
+
for name, tool in vars(toolset).items():
|
|
288
|
+
if chat_completions:
|
|
289
|
+
if isinstance(tool, CustomTool):
|
|
290
|
+
setattr(toolset, name, _custom_tool_as_function_tool(tool))
|
|
291
|
+
elif isinstance(tool, FunctionTool):
|
|
292
|
+
setattr(
|
|
293
|
+
toolset, name, _function_tool_with_error_result(_with_coerced_arguments(tool))
|
|
294
|
+
)
|
|
295
|
+
elif isinstance(tool, CustomTool):
|
|
296
|
+
setattr(toolset, name, _bound_custom_tool(tool))
|
|
297
|
+
elif isinstance(tool, FunctionTool):
|
|
298
|
+
setattr(toolset, name, _with_bounded_result(_with_coerced_arguments(tool)))
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def _make_filesystem_configurator(*, chat_completions: bool) -> Any:
|
|
302
|
+
def configure(toolset: Any) -> None:
|
|
303
|
+
_configure_filesystem_tools(toolset, chat_completions=chat_completions)
|
|
304
|
+
|
|
305
|
+
return configure
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
_CHARS_ESCAPE_RE = re.compile(r"\\(?:u[0-9a-fA-F]{4}|x[0-9a-fA-F]{2}|[0abtnvfr\\])")
|
|
309
|
+
_CHARS_ESCAPE_MAP = {
|
|
310
|
+
"\\\\": "\\",
|
|
311
|
+
"\\n": "\n",
|
|
312
|
+
"\\t": "\t",
|
|
313
|
+
"\\r": "\r",
|
|
314
|
+
"\\0": "\x00",
|
|
315
|
+
"\\a": "\x07",
|
|
316
|
+
"\\b": "\x08",
|
|
317
|
+
"\\v": "\x0b",
|
|
318
|
+
"\\f": "\x0c",
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def _decode_chars_escape(s: str) -> str:
|
|
323
|
+
if "\\" not in s:
|
|
324
|
+
return s
|
|
325
|
+
|
|
326
|
+
def sub(match: re.Match[str]) -> str:
|
|
327
|
+
token = match.group(0)
|
|
328
|
+
if token in _CHARS_ESCAPE_MAP:
|
|
329
|
+
return _CHARS_ESCAPE_MAP[token]
|
|
330
|
+
if token.startswith(("\\u", "\\x")):
|
|
331
|
+
return chr(int(token[2:], 16))
|
|
332
|
+
return token
|
|
333
|
+
|
|
334
|
+
return _CHARS_ESCAPE_RE.sub(sub, s)
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def _format_validation_error(tool_name: str, exc: ValidationError) -> str:
|
|
338
|
+
parts: list[str] = []
|
|
339
|
+
for err in exc.errors():
|
|
340
|
+
loc = ".".join(str(x) for x in err.get("loc", ()))
|
|
341
|
+
msg = err.get("msg", "invalid")
|
|
342
|
+
parts.append(f"{loc}: {msg}" if loc else msg)
|
|
343
|
+
return f"{tool_name}: invalid arguments — " + "; ".join(parts)
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def _apply_shell_output_cap(parsed: dict[str, Any]) -> None:
|
|
347
|
+
"""Clamp the SDK shell tools' ``max_output_tokens`` to the configured
|
|
348
|
+
ceiling; a smaller explicit value is respected."""
|
|
349
|
+
ceiling = load_settings().context.tool_output_max_tokens
|
|
350
|
+
requested = parsed.get("max_output_tokens")
|
|
351
|
+
parsed["max_output_tokens"] = (
|
|
352
|
+
ceiling if not isinstance(requested, int) or requested > ceiling else requested
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def _wrap_exec_command(tool: FunctionTool) -> FunctionTool:
|
|
357
|
+
invoke_tool = tool.on_invoke_tool
|
|
358
|
+
|
|
359
|
+
async def invoke(ctx: Any, raw_input: str) -> Any:
|
|
360
|
+
try:
|
|
361
|
+
parsed = json.loads(raw_input)
|
|
362
|
+
except (json.JSONDecodeError, TypeError):
|
|
363
|
+
parsed = None
|
|
364
|
+
if isinstance(parsed, dict):
|
|
365
|
+
if "shell" not in parsed:
|
|
366
|
+
parsed["shell"] = "bash"
|
|
367
|
+
_apply_shell_output_cap(parsed)
|
|
368
|
+
raw_input = json.dumps(parsed)
|
|
369
|
+
try:
|
|
370
|
+
return await invoke_tool(ctx, raw_input)
|
|
371
|
+
except ValidationError as exc:
|
|
372
|
+
return _format_validation_error(tool.name, exc)
|
|
373
|
+
except InvalidManifestPathError as exc:
|
|
374
|
+
rel = exc.context.get("rel", "?")
|
|
375
|
+
return (
|
|
376
|
+
"exec_command: workdir must be a path inside /workspace "
|
|
377
|
+
"(or omitted to use the turn's cwd). "
|
|
378
|
+
f"Got: {rel!r}."
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
tool.on_invoke_tool = invoke
|
|
382
|
+
return tool
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def _wrap_write_stdin(tool: FunctionTool) -> FunctionTool:
|
|
386
|
+
invoke_tool = tool.on_invoke_tool
|
|
387
|
+
|
|
388
|
+
async def invoke(ctx: Any, raw_input: str) -> Any:
|
|
389
|
+
try:
|
|
390
|
+
parsed = json.loads(raw_input)
|
|
391
|
+
except json.JSONDecodeError:
|
|
392
|
+
parsed = None
|
|
393
|
+
if isinstance(parsed, dict):
|
|
394
|
+
if isinstance(parsed.get("chars"), str):
|
|
395
|
+
parsed["chars"] = _decode_chars_escape(parsed["chars"])
|
|
396
|
+
_apply_shell_output_cap(parsed)
|
|
397
|
+
raw_input = json.dumps(parsed)
|
|
398
|
+
try:
|
|
399
|
+
return await invoke_tool(ctx, raw_input)
|
|
400
|
+
except ValidationError as exc:
|
|
401
|
+
return _format_validation_error(tool.name, exc)
|
|
402
|
+
|
|
403
|
+
tool.on_invoke_tool = invoke
|
|
404
|
+
return tool
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def _configure_shell_tools(toolset: Any, *, chat_completions: bool) -> None:
|
|
408
|
+
for name, tool in vars(toolset).items():
|
|
409
|
+
if not isinstance(tool, FunctionTool):
|
|
410
|
+
continue
|
|
411
|
+
wrapped = _with_coerced_arguments(tool)
|
|
412
|
+
if tool.name == "exec_command":
|
|
413
|
+
wrapped = _wrap_exec_command(wrapped)
|
|
414
|
+
elif tool.name == "write_stdin":
|
|
415
|
+
wrapped = _wrap_write_stdin(wrapped)
|
|
416
|
+
if chat_completions:
|
|
417
|
+
wrapped = _function_tool_with_error_result(wrapped)
|
|
418
|
+
setattr(toolset, name, wrapped)
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def _make_shell_configurator(*, chat_completions: bool) -> Any:
|
|
422
|
+
def configure(toolset: Any) -> None:
|
|
423
|
+
_configure_shell_tools(toolset, chat_completions=chat_completions)
|
|
424
|
+
|
|
425
|
+
return configure
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
# Tools that hand control away by parking the agent rather than ending the scan.
|
|
429
|
+
_PARKING_TOOLS: frozenset[str] = frozenset({"respond_to_user", "wait_for_agents"})
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def _lifecycle_tool_completed(tool_name: str, output: Any) -> bool:
|
|
433
|
+
if tool_name == "agent_finish":
|
|
434
|
+
completion_key = "agent_completed"
|
|
435
|
+
elif tool_name == "finish_scan":
|
|
436
|
+
completion_key = "scan_completed"
|
|
437
|
+
else:
|
|
438
|
+
return False
|
|
439
|
+
|
|
440
|
+
if not isinstance(output, str):
|
|
441
|
+
return False
|
|
442
|
+
try:
|
|
443
|
+
parsed = json.loads(output)
|
|
444
|
+
except (TypeError, ValueError):
|
|
445
|
+
return False
|
|
446
|
+
return bool(isinstance(parsed, dict) and parsed.get("success") and parsed.get(completion_key))
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def _wait_tool_parked(tool_name: str, output: Any) -> bool:
|
|
450
|
+
if tool_name not in _PARKING_TOOLS or not isinstance(output, str):
|
|
451
|
+
return False
|
|
452
|
+
try:
|
|
453
|
+
parsed = json.loads(output)
|
|
454
|
+
except (TypeError, ValueError):
|
|
455
|
+
return False
|
|
456
|
+
return bool(
|
|
457
|
+
isinstance(parsed, dict)
|
|
458
|
+
and parsed.get("success")
|
|
459
|
+
and parsed.get("wait_outcome") == "waiting"
|
|
460
|
+
)
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def _finish_tool_use_behavior(
|
|
464
|
+
ctx: RunContextWrapper[Any],
|
|
465
|
+
tool_results: list[FunctionToolResult],
|
|
466
|
+
) -> ToolsToFinalOutputResult:
|
|
467
|
+
"""Stop only after a lifecycle tool reports successful completion."""
|
|
468
|
+
interactive = (
|
|
469
|
+
bool(ctx.context.get("interactive", False)) if isinstance(ctx.context, dict) else False
|
|
470
|
+
)
|
|
471
|
+
for tool_result in tool_results:
|
|
472
|
+
if _lifecycle_tool_completed(tool_result.tool.name, tool_result.output):
|
|
473
|
+
return ToolsToFinalOutputResult(
|
|
474
|
+
is_final_output=True,
|
|
475
|
+
final_output=tool_result.output,
|
|
476
|
+
)
|
|
477
|
+
if interactive and _wait_tool_parked(tool_result.tool.name, tool_result.output):
|
|
478
|
+
return ToolsToFinalOutputResult(
|
|
479
|
+
is_final_output=True,
|
|
480
|
+
final_output=tool_result.output,
|
|
481
|
+
)
|
|
482
|
+
return ToolsToFinalOutputResult(is_final_output=False, final_output=None)
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
_BASE_TOOLS: tuple[Tool, ...] = (
|
|
486
|
+
think,
|
|
487
|
+
load_skill,
|
|
488
|
+
create_todo,
|
|
489
|
+
list_todos,
|
|
490
|
+
update_todo,
|
|
491
|
+
mark_todo_done,
|
|
492
|
+
mark_todo_pending,
|
|
493
|
+
delete_todo,
|
|
494
|
+
create_note,
|
|
495
|
+
list_notes,
|
|
496
|
+
get_note,
|
|
497
|
+
update_note,
|
|
498
|
+
delete_note,
|
|
499
|
+
web_search,
|
|
500
|
+
create_vulnerability_report,
|
|
501
|
+
create_dependency_report,
|
|
502
|
+
list_reports,
|
|
503
|
+
get_report,
|
|
504
|
+
list_requests,
|
|
505
|
+
view_request,
|
|
506
|
+
repeat_request,
|
|
507
|
+
list_sitemap,
|
|
508
|
+
view_sitemap_entry,
|
|
509
|
+
scope_rules,
|
|
510
|
+
view_agent_graph,
|
|
511
|
+
send_message_to_agent,
|
|
512
|
+
wait_for_agents,
|
|
513
|
+
create_agent,
|
|
514
|
+
stop_agent,
|
|
515
|
+
)
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
# Extra tools registered for scan agents. Mirrors
|
|
519
|
+
# ``strix.runtime.backends.register_backend``: register before the first
|
|
520
|
+
# ``build_strix_agent`` call and every agent (root + children) gets them.
|
|
521
|
+
_EXTRA_TOOLS: list[Tool] = []
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
def _ensure_unique_tool_names(tools: Sequence[Tool]) -> None:
|
|
525
|
+
seen: set[str] = set()
|
|
526
|
+
duplicates: set[str] = set()
|
|
527
|
+
for tool in tools:
|
|
528
|
+
if tool.name in seen:
|
|
529
|
+
duplicates.add(tool.name)
|
|
530
|
+
seen.add(tool.name)
|
|
531
|
+
if duplicates:
|
|
532
|
+
msg = f"Agent tools must have unique names: {sorted(duplicates)}"
|
|
533
|
+
raise ValueError(msg)
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
def register_agent_tools(*tools: Tool) -> None:
|
|
537
|
+
"""Register tools for every scan agent built afterwards.
|
|
538
|
+
|
|
539
|
+
Tools are added to both root and child agents, after the base set and
|
|
540
|
+
before the lifecycle tool (``finish_scan`` / ``agent_finish``). Duplicate
|
|
541
|
+
tool objects are ignored so repeated imports don't double-register.
|
|
542
|
+
"""
|
|
543
|
+
new_tools: list[Tool] = []
|
|
544
|
+
for tool in tools:
|
|
545
|
+
if tool not in _EXTRA_TOOLS and tool not in new_tools:
|
|
546
|
+
new_tools.append(tool)
|
|
547
|
+
|
|
548
|
+
_ensure_unique_tool_names([*_BASE_TOOLS, *_EXTRA_TOOLS, *new_tools, finish_scan, agent_finish])
|
|
549
|
+
|
|
550
|
+
for tool in new_tools:
|
|
551
|
+
_EXTRA_TOOLS.append(tool)
|
|
552
|
+
logger.info("Registered extra agent tool: %s", getattr(tool, "name", tool))
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
def registered_agent_tools() -> tuple[Tool, ...]:
|
|
556
|
+
"""Return the currently registered scan-agent tools."""
|
|
557
|
+
return tuple(_EXTRA_TOOLS)
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
def build_strix_agent(
|
|
561
|
+
*,
|
|
562
|
+
name: str = "agent",
|
|
563
|
+
skills: list[str] | None = None,
|
|
564
|
+
is_root: bool,
|
|
565
|
+
scan_mode: str = "deep",
|
|
566
|
+
is_whitebox: bool = False,
|
|
567
|
+
interactive: bool = False,
|
|
568
|
+
chat_completions_tools: bool = False,
|
|
569
|
+
system_prompt_context: dict[str, Any] | None = None,
|
|
570
|
+
extra_tools: Sequence[Tool] | None = None,
|
|
571
|
+
instructions_override: str | None = None,
|
|
572
|
+
) -> SandboxAgent[Any]:
|
|
573
|
+
"""Build a SandboxAgent for either root or child use.
|
|
574
|
+
|
|
575
|
+
Args:
|
|
576
|
+
chat_completions_tools: Wrap SDK custom tools as function tools
|
|
577
|
+
when the selected backend cannot accept Responses custom tools.
|
|
578
|
+
extra_tools: Additional tools for this scan agent only, on top of any
|
|
579
|
+
registered via ``register_agent_tools``.
|
|
580
|
+
instructions_override: Use this verbatim as the system prompt instead
|
|
581
|
+
of rendering the built-in scan prompt.
|
|
582
|
+
"""
|
|
583
|
+
if instructions_override is not None:
|
|
584
|
+
instructions = instructions_override
|
|
585
|
+
else:
|
|
586
|
+
instructions = render_system_prompt(
|
|
587
|
+
skills=skills,
|
|
588
|
+
scan_mode=scan_mode,
|
|
589
|
+
is_whitebox=is_whitebox,
|
|
590
|
+
is_root=is_root,
|
|
591
|
+
interactive=interactive,
|
|
592
|
+
system_prompt_context=system_prompt_context,
|
|
593
|
+
)
|
|
594
|
+
|
|
595
|
+
agent_tools = [*_EXTRA_TOOLS, *(extra_tools or [])]
|
|
596
|
+
if interactive:
|
|
597
|
+
# Yielding to the user is only meaningful when one is attached.
|
|
598
|
+
agent_tools.append(respond_to_user)
|
|
599
|
+
if is_root:
|
|
600
|
+
tools: list[Tool] = [*_BASE_TOOLS, *agent_tools, finish_scan]
|
|
601
|
+
else:
|
|
602
|
+
tools = [*_BASE_TOOLS, *agent_tools, agent_finish]
|
|
603
|
+
_ensure_unique_tool_names(tools)
|
|
604
|
+
tools = [
|
|
605
|
+
_with_bounded_result(_with_coerced_arguments(tool))
|
|
606
|
+
if isinstance(tool, FunctionTool)
|
|
607
|
+
else tool
|
|
608
|
+
for tool in tools
|
|
609
|
+
]
|
|
610
|
+
|
|
611
|
+
logger.info(
|
|
612
|
+
"Built %s agent '%s' (skills=%d, tools=%d, scan_mode=%s, whitebox=%s)",
|
|
613
|
+
"root" if is_root else "child",
|
|
614
|
+
name,
|
|
615
|
+
len(skills or []),
|
|
616
|
+
len(tools),
|
|
617
|
+
scan_mode,
|
|
618
|
+
is_whitebox,
|
|
619
|
+
)
|
|
620
|
+
|
|
621
|
+
return SandboxAgent(
|
|
622
|
+
name=name,
|
|
623
|
+
instructions=instructions,
|
|
624
|
+
tools=tools,
|
|
625
|
+
tool_use_behavior=_finish_tool_use_behavior,
|
|
626
|
+
model=None,
|
|
627
|
+
capabilities=[
|
|
628
|
+
Filesystem(
|
|
629
|
+
configure_tools=_make_filesystem_configurator(
|
|
630
|
+
chat_completions=chat_completions_tools,
|
|
631
|
+
),
|
|
632
|
+
),
|
|
633
|
+
Shell(
|
|
634
|
+
configure_tools=_make_shell_configurator(
|
|
635
|
+
chat_completions=chat_completions_tools,
|
|
636
|
+
),
|
|
637
|
+
),
|
|
638
|
+
],
|
|
639
|
+
)
|
|
640
|
+
|
|
641
|
+
|
|
642
|
+
def make_child_factory(
|
|
643
|
+
*,
|
|
644
|
+
scan_mode: str = "deep",
|
|
645
|
+
is_whitebox: bool = False,
|
|
646
|
+
interactive: bool = False,
|
|
647
|
+
chat_completions_tools: bool = False,
|
|
648
|
+
system_prompt_context: dict[str, Any] | None = None,
|
|
649
|
+
) -> Any:
|
|
650
|
+
"""Return the runner-owned builder used by ``spawn_child_agent``.
|
|
651
|
+
|
|
652
|
+
Run-level arguments (``scan_mode``, ``is_whitebox``, etc.) are
|
|
653
|
+
captured in a closure so each child inherits scan-level configuration
|
|
654
|
+
without the graph tool knowing about runner internals.
|
|
655
|
+
"""
|
|
656
|
+
|
|
657
|
+
def _factory(*, name: str, skills: list[str]) -> SandboxAgent[Any]:
|
|
658
|
+
return build_strix_agent(
|
|
659
|
+
name=name,
|
|
660
|
+
skills=skills,
|
|
661
|
+
is_root=False,
|
|
662
|
+
scan_mode=scan_mode,
|
|
663
|
+
is_whitebox=is_whitebox,
|
|
664
|
+
interactive=interactive,
|
|
665
|
+
chat_completions_tools=chat_completions_tools,
|
|
666
|
+
system_prompt_context=system_prompt_context,
|
|
667
|
+
)
|
|
668
|
+
|
|
669
|
+
return _factory
|