aiptx 2.0.2__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.

Potentially problematic release.


This version of aiptx might be problematic. Click here for more details.

Files changed (165) hide show
  1. aipt_v2/__init__.py +110 -0
  2. aipt_v2/__main__.py +24 -0
  3. aipt_v2/agents/AIPTxAgent/__init__.py +10 -0
  4. aipt_v2/agents/AIPTxAgent/aiptx_agent.py +211 -0
  5. aipt_v2/agents/__init__.py +24 -0
  6. aipt_v2/agents/base.py +520 -0
  7. aipt_v2/agents/ptt.py +406 -0
  8. aipt_v2/agents/state.py +168 -0
  9. aipt_v2/app.py +960 -0
  10. aipt_v2/browser/__init__.py +31 -0
  11. aipt_v2/browser/automation.py +458 -0
  12. aipt_v2/browser/crawler.py +453 -0
  13. aipt_v2/cli.py +321 -0
  14. aipt_v2/compliance/__init__.py +71 -0
  15. aipt_v2/compliance/compliance_report.py +449 -0
  16. aipt_v2/compliance/framework_mapper.py +424 -0
  17. aipt_v2/compliance/nist_mapping.py +345 -0
  18. aipt_v2/compliance/owasp_mapping.py +330 -0
  19. aipt_v2/compliance/pci_mapping.py +297 -0
  20. aipt_v2/config.py +288 -0
  21. aipt_v2/core/__init__.py +43 -0
  22. aipt_v2/core/agent.py +630 -0
  23. aipt_v2/core/llm.py +395 -0
  24. aipt_v2/core/memory.py +305 -0
  25. aipt_v2/core/ptt.py +329 -0
  26. aipt_v2/database/__init__.py +14 -0
  27. aipt_v2/database/models.py +232 -0
  28. aipt_v2/database/repository.py +384 -0
  29. aipt_v2/docker/__init__.py +23 -0
  30. aipt_v2/docker/builder.py +260 -0
  31. aipt_v2/docker/manager.py +222 -0
  32. aipt_v2/docker/sandbox.py +371 -0
  33. aipt_v2/evasion/__init__.py +58 -0
  34. aipt_v2/evasion/request_obfuscator.py +272 -0
  35. aipt_v2/evasion/tls_fingerprint.py +285 -0
  36. aipt_v2/evasion/ua_rotator.py +301 -0
  37. aipt_v2/evasion/waf_bypass.py +439 -0
  38. aipt_v2/execution/__init__.py +23 -0
  39. aipt_v2/execution/executor.py +302 -0
  40. aipt_v2/execution/parser.py +544 -0
  41. aipt_v2/execution/terminal.py +337 -0
  42. aipt_v2/health.py +437 -0
  43. aipt_v2/intelligence/__init__.py +85 -0
  44. aipt_v2/intelligence/auth.py +520 -0
  45. aipt_v2/intelligence/chaining.py +775 -0
  46. aipt_v2/intelligence/cve_aipt.py +334 -0
  47. aipt_v2/intelligence/cve_info.py +1111 -0
  48. aipt_v2/intelligence/rag.py +239 -0
  49. aipt_v2/intelligence/scope.py +442 -0
  50. aipt_v2/intelligence/searchers/__init__.py +5 -0
  51. aipt_v2/intelligence/searchers/exploitdb_searcher.py +523 -0
  52. aipt_v2/intelligence/searchers/github_searcher.py +467 -0
  53. aipt_v2/intelligence/searchers/google_searcher.py +281 -0
  54. aipt_v2/intelligence/tools.json +443 -0
  55. aipt_v2/intelligence/triage.py +670 -0
  56. aipt_v2/interface/__init__.py +5 -0
  57. aipt_v2/interface/cli.py +230 -0
  58. aipt_v2/interface/main.py +501 -0
  59. aipt_v2/interface/tui.py +1276 -0
  60. aipt_v2/interface/utils.py +583 -0
  61. aipt_v2/llm/__init__.py +39 -0
  62. aipt_v2/llm/config.py +26 -0
  63. aipt_v2/llm/llm.py +514 -0
  64. aipt_v2/llm/memory.py +214 -0
  65. aipt_v2/llm/request_queue.py +89 -0
  66. aipt_v2/llm/utils.py +89 -0
  67. aipt_v2/models/__init__.py +15 -0
  68. aipt_v2/models/findings.py +295 -0
  69. aipt_v2/models/phase_result.py +224 -0
  70. aipt_v2/models/scan_config.py +207 -0
  71. aipt_v2/monitoring/grafana/dashboards/aipt-dashboard.json +355 -0
  72. aipt_v2/monitoring/grafana/dashboards/default.yml +17 -0
  73. aipt_v2/monitoring/grafana/datasources/prometheus.yml +17 -0
  74. aipt_v2/monitoring/prometheus.yml +60 -0
  75. aipt_v2/orchestration/__init__.py +52 -0
  76. aipt_v2/orchestration/pipeline.py +398 -0
  77. aipt_v2/orchestration/progress.py +300 -0
  78. aipt_v2/orchestration/scheduler.py +296 -0
  79. aipt_v2/orchestrator.py +2284 -0
  80. aipt_v2/payloads/__init__.py +27 -0
  81. aipt_v2/payloads/cmdi.py +150 -0
  82. aipt_v2/payloads/sqli.py +263 -0
  83. aipt_v2/payloads/ssrf.py +204 -0
  84. aipt_v2/payloads/templates.py +222 -0
  85. aipt_v2/payloads/traversal.py +166 -0
  86. aipt_v2/payloads/xss.py +204 -0
  87. aipt_v2/prompts/__init__.py +60 -0
  88. aipt_v2/proxy/__init__.py +29 -0
  89. aipt_v2/proxy/history.py +352 -0
  90. aipt_v2/proxy/interceptor.py +452 -0
  91. aipt_v2/recon/__init__.py +44 -0
  92. aipt_v2/recon/dns.py +241 -0
  93. aipt_v2/recon/osint.py +367 -0
  94. aipt_v2/recon/subdomain.py +372 -0
  95. aipt_v2/recon/tech_detect.py +311 -0
  96. aipt_v2/reports/__init__.py +17 -0
  97. aipt_v2/reports/generator.py +313 -0
  98. aipt_v2/reports/html_report.py +378 -0
  99. aipt_v2/runtime/__init__.py +44 -0
  100. aipt_v2/runtime/base.py +30 -0
  101. aipt_v2/runtime/docker.py +401 -0
  102. aipt_v2/runtime/local.py +346 -0
  103. aipt_v2/runtime/tool_server.py +205 -0
  104. aipt_v2/scanners/__init__.py +28 -0
  105. aipt_v2/scanners/base.py +273 -0
  106. aipt_v2/scanners/nikto.py +244 -0
  107. aipt_v2/scanners/nmap.py +402 -0
  108. aipt_v2/scanners/nuclei.py +273 -0
  109. aipt_v2/scanners/web.py +454 -0
  110. aipt_v2/scripts/security_audit.py +366 -0
  111. aipt_v2/telemetry/__init__.py +7 -0
  112. aipt_v2/telemetry/tracer.py +347 -0
  113. aipt_v2/terminal/__init__.py +28 -0
  114. aipt_v2/terminal/executor.py +400 -0
  115. aipt_v2/terminal/sandbox.py +350 -0
  116. aipt_v2/tools/__init__.py +44 -0
  117. aipt_v2/tools/active_directory/__init__.py +78 -0
  118. aipt_v2/tools/active_directory/ad_config.py +238 -0
  119. aipt_v2/tools/active_directory/bloodhound_wrapper.py +447 -0
  120. aipt_v2/tools/active_directory/kerberos_attacks.py +430 -0
  121. aipt_v2/tools/active_directory/ldap_enum.py +533 -0
  122. aipt_v2/tools/active_directory/smb_attacks.py +505 -0
  123. aipt_v2/tools/agents_graph/__init__.py +19 -0
  124. aipt_v2/tools/agents_graph/agents_graph_actions.py +69 -0
  125. aipt_v2/tools/api_security/__init__.py +76 -0
  126. aipt_v2/tools/api_security/api_discovery.py +608 -0
  127. aipt_v2/tools/api_security/graphql_scanner.py +622 -0
  128. aipt_v2/tools/api_security/jwt_analyzer.py +577 -0
  129. aipt_v2/tools/api_security/openapi_fuzzer.py +761 -0
  130. aipt_v2/tools/browser/__init__.py +5 -0
  131. aipt_v2/tools/browser/browser_actions.py +238 -0
  132. aipt_v2/tools/browser/browser_instance.py +535 -0
  133. aipt_v2/tools/browser/tab_manager.py +344 -0
  134. aipt_v2/tools/cloud/__init__.py +70 -0
  135. aipt_v2/tools/cloud/cloud_config.py +273 -0
  136. aipt_v2/tools/cloud/cloud_scanner.py +639 -0
  137. aipt_v2/tools/cloud/prowler_tool.py +571 -0
  138. aipt_v2/tools/cloud/scoutsuite_tool.py +359 -0
  139. aipt_v2/tools/executor.py +307 -0
  140. aipt_v2/tools/parser.py +408 -0
  141. aipt_v2/tools/proxy/__init__.py +5 -0
  142. aipt_v2/tools/proxy/proxy_actions.py +103 -0
  143. aipt_v2/tools/proxy/proxy_manager.py +789 -0
  144. aipt_v2/tools/registry.py +196 -0
  145. aipt_v2/tools/scanners/__init__.py +343 -0
  146. aipt_v2/tools/scanners/acunetix_tool.py +712 -0
  147. aipt_v2/tools/scanners/burp_tool.py +631 -0
  148. aipt_v2/tools/scanners/config.py +156 -0
  149. aipt_v2/tools/scanners/nessus_tool.py +588 -0
  150. aipt_v2/tools/scanners/zap_tool.py +612 -0
  151. aipt_v2/tools/terminal/__init__.py +5 -0
  152. aipt_v2/tools/terminal/terminal_actions.py +37 -0
  153. aipt_v2/tools/terminal/terminal_manager.py +153 -0
  154. aipt_v2/tools/terminal/terminal_session.py +449 -0
  155. aipt_v2/tools/tool_processing.py +108 -0
  156. aipt_v2/utils/__init__.py +17 -0
  157. aipt_v2/utils/logging.py +201 -0
  158. aipt_v2/utils/model_manager.py +187 -0
  159. aipt_v2/utils/searchers/__init__.py +269 -0
  160. aiptx-2.0.2.dist-info/METADATA +324 -0
  161. aiptx-2.0.2.dist-info/RECORD +165 -0
  162. aiptx-2.0.2.dist-info/WHEEL +5 -0
  163. aiptx-2.0.2.dist-info/entry_points.txt +7 -0
  164. aiptx-2.0.2.dist-info/licenses/LICENSE +21 -0
  165. aiptx-2.0.2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,359 @@
1
+ """
2
+ ScoutSuite Integration - Multi-Cloud Security Auditing
3
+
4
+ ScoutSuite is an open-source multi-cloud security auditing tool
5
+ that assesses the security posture of cloud environments.
6
+
7
+ Supports:
8
+ - AWS (Amazon Web Services)
9
+ - Azure (Microsoft Azure)
10
+ - GCP (Google Cloud Platform)
11
+ - Alibaba Cloud
12
+ - Oracle Cloud Infrastructure
13
+
14
+ Usage:
15
+ from aipt_v2.tools.cloud import run_scoutsuite
16
+
17
+ findings = await run_scoutsuite("aws", profile="production")
18
+ """
19
+
20
+ import asyncio
21
+ import json
22
+ import os
23
+ from dataclasses import dataclass, field
24
+ from datetime import datetime, timezone
25
+ from pathlib import Path
26
+ from typing import List, Dict, Any, Optional
27
+
28
+ from aipt_v2.tools.cloud.cloud_config import CloudConfig, get_cloud_config
29
+
30
+
31
+ @dataclass
32
+ class ScoutSuiteConfig:
33
+ """ScoutSuite configuration."""
34
+ provider: str = "aws" # aws, azure, gcp, aliyun, oci
35
+
36
+ # AWS options
37
+ aws_profile: str = "default"
38
+ aws_regions: List[str] = field(default_factory=list) # Empty = all
39
+
40
+ # Azure options
41
+ azure_subscription_id: str = ""
42
+ azure_tenant_id: str = ""
43
+
44
+ # GCP options
45
+ gcp_project_id: str = ""
46
+ gcp_service_account: str = ""
47
+
48
+ # Scanning options
49
+ services: List[str] = field(default_factory=list) # Empty = all
50
+ skip_services: List[str] = field(default_factory=list)
51
+ max_workers: int = 25
52
+ no_browser: bool = True
53
+
54
+ # Output options
55
+ output_dir: str = "./scoutsuite_results"
56
+ report_name: str = ""
57
+ timestamp: bool = True
58
+
59
+
60
+ @dataclass
61
+ class ScoutSuiteResult:
62
+ """Result of a ScoutSuite scan."""
63
+ provider: str
64
+ status: str
65
+ started_at: str
66
+ finished_at: str
67
+ duration: float
68
+ findings_count: int
69
+ flagged_items: int
70
+ report_path: str
71
+ summary: Dict[str, int]
72
+ metadata: Dict[str, Any] = field(default_factory=dict)
73
+
74
+
75
+ class ScoutSuiteTool:
76
+ """
77
+ ScoutSuite wrapper for multi-cloud security auditing.
78
+
79
+ Provides a Python interface to the ScoutSuite CLI tool
80
+ for automated cloud security assessments.
81
+ """
82
+
83
+ def __init__(self, config: Optional[ScoutSuiteConfig] = None):
84
+ """
85
+ Initialize ScoutSuite tool.
86
+
87
+ Args:
88
+ config: ScoutSuite configuration
89
+ """
90
+ self.config = config or ScoutSuiteConfig()
91
+ self.output_dir = Path(self.config.output_dir)
92
+ self.output_dir.mkdir(parents=True, exist_ok=True)
93
+ self._installed = None
94
+
95
+ async def check_installed(self) -> bool:
96
+ """Check if ScoutSuite is installed."""
97
+ if self._installed is not None:
98
+ return self._installed
99
+
100
+ try:
101
+ process = await asyncio.create_subprocess_shell(
102
+ "scout --version",
103
+ stdout=asyncio.subprocess.PIPE,
104
+ stderr=asyncio.subprocess.PIPE
105
+ )
106
+ stdout, _ = await process.communicate()
107
+ self._installed = process.returncode == 0
108
+ except Exception:
109
+ self._installed = False
110
+
111
+ return self._installed
112
+
113
+ def _build_command(self) -> str:
114
+ """Build ScoutSuite command from configuration."""
115
+ cmd_parts = ["scout", self.config.provider]
116
+
117
+ # Add provider-specific options
118
+ if self.config.provider == "aws":
119
+ if self.config.aws_profile:
120
+ cmd_parts.extend(["--profile", self.config.aws_profile])
121
+ if self.config.aws_regions:
122
+ cmd_parts.extend(["--regions", ",".join(self.config.aws_regions)])
123
+
124
+ elif self.config.provider == "azure":
125
+ if self.config.azure_subscription_id:
126
+ cmd_parts.extend(["--subscription-id", self.config.azure_subscription_id])
127
+ if self.config.azure_tenant_id:
128
+ cmd_parts.extend(["--tenant-id", self.config.azure_tenant_id])
129
+
130
+ elif self.config.provider == "gcp":
131
+ if self.config.gcp_project_id:
132
+ cmd_parts.extend(["--project-id", self.config.gcp_project_id])
133
+ if self.config.gcp_service_account:
134
+ cmd_parts.extend(["--service-account", self.config.gcp_service_account])
135
+
136
+ # Add service filtering
137
+ if self.config.services:
138
+ cmd_parts.extend(["--services", ",".join(self.config.services)])
139
+ if self.config.skip_services:
140
+ cmd_parts.extend(["--skip", ",".join(self.config.skip_services)])
141
+
142
+ # Add general options
143
+ cmd_parts.extend(["--max-workers", str(self.config.max_workers)])
144
+ cmd_parts.extend(["--report-dir", str(self.output_dir)])
145
+
146
+ if self.config.report_name:
147
+ cmd_parts.extend(["--report-name", self.config.report_name])
148
+
149
+ if self.config.no_browser:
150
+ cmd_parts.append("--no-browser")
151
+
152
+ if self.config.timestamp:
153
+ cmd_parts.append("--timestamp")
154
+
155
+ return " ".join(cmd_parts)
156
+
157
+ async def scan(self, timeout: int = 3600) -> ScoutSuiteResult:
158
+ """
159
+ Run ScoutSuite scan.
160
+
161
+ Args:
162
+ timeout: Scan timeout in seconds
163
+
164
+ Returns:
165
+ ScoutSuiteResult with findings summary
166
+ """
167
+ if not await self.check_installed():
168
+ raise RuntimeError("ScoutSuite is not installed. Install with: pip install scoutsuite")
169
+
170
+ started_at = datetime.now(timezone.utc).isoformat()
171
+ start_time = asyncio.get_event_loop().time()
172
+
173
+ cmd = self._build_command()
174
+ print(f"[*] Running: {cmd}")
175
+
176
+ # Set up environment
177
+ env = os.environ.copy()
178
+
179
+ process = await asyncio.create_subprocess_shell(
180
+ cmd,
181
+ stdout=asyncio.subprocess.PIPE,
182
+ stderr=asyncio.subprocess.PIPE,
183
+ env=env
184
+ )
185
+
186
+ try:
187
+ stdout, stderr = await asyncio.wait_for(
188
+ process.communicate(),
189
+ timeout=timeout
190
+ )
191
+ except asyncio.TimeoutError:
192
+ process.kill()
193
+ raise TimeoutError(f"ScoutSuite scan timed out after {timeout}s")
194
+
195
+ finished_at = datetime.now(timezone.utc).isoformat()
196
+ duration = asyncio.get_event_loop().time() - start_time
197
+
198
+ # Parse results
199
+ report_path = self._find_latest_report()
200
+ findings_count = 0
201
+ flagged_items = 0
202
+ summary = {"danger": 0, "warning": 0, "info": 0}
203
+
204
+ if report_path and report_path.exists():
205
+ results = self._parse_results(report_path)
206
+ findings_count = results.get("findings_count", 0)
207
+ flagged_items = results.get("flagged_items", 0)
208
+ summary = results.get("summary", summary)
209
+
210
+ status = "completed" if process.returncode == 0 else "failed"
211
+
212
+ return ScoutSuiteResult(
213
+ provider=self.config.provider,
214
+ status=status,
215
+ started_at=started_at,
216
+ finished_at=finished_at,
217
+ duration=duration,
218
+ findings_count=findings_count,
219
+ flagged_items=flagged_items,
220
+ report_path=str(report_path) if report_path else "",
221
+ summary=summary,
222
+ metadata={
223
+ "command": cmd,
224
+ "return_code": process.returncode,
225
+ "stderr": stderr.decode() if process.returncode != 0 else ""
226
+ }
227
+ )
228
+
229
+ def _find_latest_report(self) -> Optional[Path]:
230
+ """Find the latest ScoutSuite results file."""
231
+ results_dir = self.output_dir / "scoutsuite-results"
232
+ if results_dir.exists():
233
+ js_files = list(results_dir.glob("scoutsuite_results*.js"))
234
+ if js_files:
235
+ return max(js_files, key=lambda f: f.stat().st_mtime)
236
+
237
+ # Try alternative location
238
+ js_files = list(self.output_dir.glob("**/scoutsuite_results*.js"))
239
+ if js_files:
240
+ return max(js_files, key=lambda f: f.stat().st_mtime)
241
+
242
+ return None
243
+
244
+ def _parse_results(self, results_file: Path) -> Dict[str, Any]:
245
+ """Parse ScoutSuite results JavaScript file."""
246
+ try:
247
+ content = results_file.read_text()
248
+
249
+ # Remove JS variable assignment
250
+ if "scoutsuite_results =" in content:
251
+ content = content.split("scoutsuite_results =", 1)[1].strip()
252
+ if content.endswith(";"):
253
+ content = content[:-1]
254
+
255
+ data = json.loads(content)
256
+
257
+ # Count findings
258
+ findings_count = 0
259
+ flagged_items = 0
260
+ summary = {"danger": 0, "warning": 0, "info": 0}
261
+
262
+ services = data.get("services", {})
263
+ for service_data in services.values():
264
+ service_findings = service_data.get("findings", {})
265
+ for finding in service_findings.values():
266
+ findings_count += 1
267
+ flagged = finding.get("flagged_items", 0)
268
+ flagged_items += flagged
269
+
270
+ if flagged > 0:
271
+ level = finding.get("level", "warning")
272
+ if level in summary:
273
+ summary[level] += flagged
274
+
275
+ return {
276
+ "findings_count": findings_count,
277
+ "flagged_items": flagged_items,
278
+ "summary": summary,
279
+ "account_id": data.get("account_id", ""),
280
+ "last_run": data.get("last_run", {})
281
+ }
282
+
283
+ except Exception as e:
284
+ print(f"[!] Error parsing ScoutSuite results: {e}")
285
+ return {"findings_count": 0, "flagged_items": 0, "summary": {}}
286
+
287
+ def get_findings(self) -> List[Dict[str, Any]]:
288
+ """Get parsed findings from the latest scan."""
289
+ report_path = self._find_latest_report()
290
+ if not report_path:
291
+ return []
292
+
293
+ try:
294
+ content = report_path.read_text()
295
+ if "scoutsuite_results =" in content:
296
+ content = content.split("scoutsuite_results =", 1)[1].strip()
297
+ if content.endswith(";"):
298
+ content = content[:-1]
299
+
300
+ data = json.loads(content)
301
+ findings = []
302
+
303
+ services = data.get("services", {})
304
+ for service_name, service_data in services.items():
305
+ for finding_id, finding_data in service_data.get("findings", {}).items():
306
+ if finding_data.get("flagged_items", 0) > 0:
307
+ findings.append({
308
+ "provider": self.config.provider,
309
+ "service": service_name,
310
+ "id": finding_id,
311
+ "level": finding_data.get("level", "warning"),
312
+ "description": finding_data.get("description", ""),
313
+ "rationale": finding_data.get("rationale", ""),
314
+ "remediation": finding_data.get("remediation", ""),
315
+ "flagged_items": finding_data.get("flagged_items", 0),
316
+ "items": finding_data.get("items", []),
317
+ "compliance": finding_data.get("compliance", [])
318
+ })
319
+
320
+ return findings
321
+
322
+ except Exception as e:
323
+ print(f"[!] Error getting findings: {e}")
324
+ return []
325
+
326
+
327
+ # Convenience function
328
+ async def run_scoutsuite(
329
+ provider: str = "aws",
330
+ profile: Optional[str] = None,
331
+ regions: Optional[List[str]] = None,
332
+ services: Optional[List[str]] = None,
333
+ output_dir: str = "./scoutsuite_results",
334
+ timeout: int = 3600
335
+ ) -> ScoutSuiteResult:
336
+ """
337
+ Run ScoutSuite scan.
338
+
339
+ Args:
340
+ provider: Cloud provider (aws, azure, gcp)
341
+ profile: AWS profile name (for AWS)
342
+ regions: Specific regions to scan
343
+ services: Specific services to scan
344
+ output_dir: Output directory for reports
345
+ timeout: Scan timeout in seconds
346
+
347
+ Returns:
348
+ ScoutSuiteResult
349
+ """
350
+ config = ScoutSuiteConfig(
351
+ provider=provider,
352
+ aws_profile=profile or "default",
353
+ aws_regions=regions or [],
354
+ services=services or [],
355
+ output_dir=output_dir
356
+ )
357
+
358
+ tool = ScoutSuiteTool(config)
359
+ return await tool.scan(timeout=timeout)
@@ -0,0 +1,307 @@
1
+ from __future__ import annotations
2
+
3
+ import inspect
4
+ import os
5
+ from typing import Any
6
+
7
+ import httpx
8
+
9
+
10
+ if os.getenv("AIPT_SANDBOX_MODE", "false").lower() == "false":
11
+ from aipt_v2.runtime import get_runtime
12
+
13
+ from .argument_parser import convert_arguments
14
+ from .registry import (
15
+ get_tool_by_name,
16
+ get_tool_names,
17
+ needs_agent_state,
18
+ should_execute_in_sandbox,
19
+ )
20
+
21
+
22
+ async def execute_tool(tool_name: str, agent_state: Any | None = None, **kwargs: Any) -> Any:
23
+ execute_in_sandbox = should_execute_in_sandbox(tool_name)
24
+ sandbox_mode = os.getenv("AIPT_SANDBOX_MODE", "false").lower() == "true"
25
+
26
+ if execute_in_sandbox and not sandbox_mode:
27
+ return await _execute_tool_in_sandbox(tool_name, agent_state, **kwargs)
28
+
29
+ return await _execute_tool_locally(tool_name, agent_state, **kwargs)
30
+
31
+
32
+ async def _execute_tool_in_sandbox(tool_name: str, agent_state: Any, **kwargs: Any) -> Any:
33
+ if not hasattr(agent_state, "sandbox_id") or not agent_state.sandbox_id:
34
+ raise ValueError("Agent state with a valid sandbox_id is required for sandbox execution.")
35
+
36
+ if not hasattr(agent_state, "sandbox_token") or not agent_state.sandbox_token:
37
+ raise ValueError(
38
+ "Agent state with a valid sandbox_token is required for sandbox execution."
39
+ )
40
+
41
+ if (
42
+ not hasattr(agent_state, "sandbox_info")
43
+ or "tool_server_port" not in agent_state.sandbox_info
44
+ ):
45
+ raise ValueError(
46
+ "Agent state with a valid sandbox_info containing tool_server_port is required."
47
+ )
48
+
49
+ runtime = get_runtime()
50
+ tool_server_port = agent_state.sandbox_info["tool_server_port"]
51
+ server_url = await runtime.get_sandbox_url(agent_state.sandbox_id, tool_server_port)
52
+ request_url = f"{server_url}/execute"
53
+
54
+ agent_id = getattr(agent_state, "agent_id", "unknown")
55
+
56
+ request_data = {
57
+ "agent_id": agent_id,
58
+ "tool_name": tool_name,
59
+ "kwargs": kwargs,
60
+ }
61
+
62
+ headers = {
63
+ "Authorization": f"Bearer {agent_state.sandbox_token}",
64
+ "Content-Type": "application/json",
65
+ }
66
+
67
+ async with httpx.AsyncClient(trust_env=False) as client:
68
+ try:
69
+ response = await client.post(
70
+ request_url, json=request_data, headers=headers, timeout=None
71
+ )
72
+ response.raise_for_status()
73
+ response_data = response.json()
74
+ if response_data.get("error"):
75
+ raise RuntimeError(f"Sandbox execution error: {response_data['error']}")
76
+ return response_data.get("result")
77
+ except httpx.HTTPStatusError as e:
78
+ if e.response.status_code == 401:
79
+ raise RuntimeError("Authentication failed: Invalid or missing sandbox token") from e
80
+ raise RuntimeError(f"HTTP error calling tool server: {e.response.status_code}") from e
81
+ except httpx.RequestError as e:
82
+ raise RuntimeError(f"Request error calling tool server: {e}") from e
83
+
84
+
85
+ async def _execute_tool_locally(tool_name: str, agent_state: Any | None, **kwargs: Any) -> Any:
86
+ tool_func = get_tool_by_name(tool_name)
87
+ if not tool_func:
88
+ raise ValueError(f"Tool '{tool_name}' not found")
89
+
90
+ converted_kwargs = convert_arguments(tool_func, kwargs)
91
+
92
+ if needs_agent_state(tool_name):
93
+ if agent_state is None:
94
+ raise ValueError(f"Tool '{tool_name}' requires agent_state but none was provided.")
95
+ result = tool_func(agent_state=agent_state, **converted_kwargs)
96
+ else:
97
+ result = tool_func(**converted_kwargs)
98
+
99
+ return await result if inspect.isawaitable(result) else result
100
+
101
+
102
+ def validate_tool_availability(tool_name: str | None) -> tuple[bool, str]:
103
+ if tool_name is None:
104
+ return False, "Tool name is missing"
105
+
106
+ if tool_name not in get_tool_names():
107
+ return False, f"Tool '{tool_name}' is not available"
108
+
109
+ return True, ""
110
+
111
+
112
+ async def execute_tool_with_validation(
113
+ tool_name: str | None, agent_state: Any | None = None, **kwargs: Any
114
+ ) -> Any:
115
+ is_valid, error_msg = validate_tool_availability(tool_name)
116
+ if not is_valid:
117
+ return f"Error: {error_msg}"
118
+
119
+ assert tool_name is not None
120
+
121
+ try:
122
+ result = await execute_tool(tool_name, agent_state, **kwargs)
123
+ except Exception as e: # noqa: BLE001
124
+ error_str = str(e)
125
+ if len(error_str) > 500:
126
+ error_str = error_str[:500] + "... [truncated]"
127
+ return f"Error executing {tool_name}: {error_str}"
128
+ else:
129
+ return result
130
+
131
+
132
+ async def execute_tool_invocation(tool_inv: dict[str, Any], agent_state: Any | None = None) -> Any:
133
+ tool_name = tool_inv.get("toolName")
134
+ tool_args = tool_inv.get("args", {})
135
+
136
+ return await execute_tool_with_validation(tool_name, agent_state, **tool_args)
137
+
138
+
139
+ def _check_error_result(result: Any) -> tuple[bool, Any]:
140
+ is_error = False
141
+ error_payload: Any = None
142
+
143
+ if (isinstance(result, dict) and "error" in result) or (
144
+ isinstance(result, str) and result.strip().lower().startswith("error:")
145
+ ):
146
+ is_error = True
147
+ error_payload = result
148
+
149
+ return is_error, error_payload
150
+
151
+
152
+ def _update_tracer_with_result(
153
+ tracer: Any, execution_id: Any, is_error: bool, result: Any, error_payload: Any
154
+ ) -> None:
155
+ if not tracer or not execution_id:
156
+ return
157
+
158
+ try:
159
+ if is_error:
160
+ tracer.update_tool_execution(execution_id, "error", error_payload)
161
+ else:
162
+ tracer.update_tool_execution(execution_id, "completed", result)
163
+ except (ConnectionError, RuntimeError) as e:
164
+ error_msg = str(e)
165
+ if tracer and execution_id:
166
+ tracer.update_tool_execution(execution_id, "error", error_msg)
167
+ raise
168
+
169
+
170
+ def _format_tool_result(tool_name: str, result: Any) -> tuple[str, list[dict[str, Any]]]:
171
+ images: list[dict[str, Any]] = []
172
+
173
+ screenshot_data = extract_screenshot_from_result(result)
174
+ if screenshot_data:
175
+ images.append(
176
+ {
177
+ "type": "image_url",
178
+ "image_url": {"url": f"data:image/png;base64,{screenshot_data}"},
179
+ }
180
+ )
181
+ result_str = remove_screenshot_from_result(result)
182
+ else:
183
+ result_str = result
184
+
185
+ if result_str is None:
186
+ final_result_str = f"Tool {tool_name} executed successfully"
187
+ else:
188
+ final_result_str = str(result_str)
189
+ if len(final_result_str) > 10000:
190
+ start_part = final_result_str[:4000]
191
+ end_part = final_result_str[-4000:]
192
+ final_result_str = start_part + "\n\n... [middle content truncated] ...\n\n" + end_part
193
+
194
+ observation_xml = (
195
+ f"<tool_result>\n<tool_name>{tool_name}</tool_name>\n"
196
+ f"<result>{final_result_str}</result>\n</tool_result>"
197
+ )
198
+
199
+ return observation_xml, images
200
+
201
+
202
+ async def _execute_single_tool(
203
+ tool_inv: dict[str, Any],
204
+ agent_state: Any | None,
205
+ tracer: Any | None,
206
+ agent_id: str,
207
+ ) -> tuple[str, list[dict[str, Any]], bool]:
208
+ tool_name = tool_inv.get("toolName", "unknown")
209
+ args = tool_inv.get("args", {})
210
+ execution_id = None
211
+ should_agent_finish = False
212
+
213
+ if tracer:
214
+ execution_id = tracer.log_tool_execution_start(agent_id, tool_name, args)
215
+
216
+ try:
217
+ result = await execute_tool_invocation(tool_inv, agent_state)
218
+
219
+ is_error, error_payload = _check_error_result(result)
220
+
221
+ if (
222
+ tool_name in ("finish_scan", "agent_finish")
223
+ and not is_error
224
+ and isinstance(result, dict)
225
+ ):
226
+ if tool_name == "finish_scan":
227
+ should_agent_finish = result.get("scan_completed", False)
228
+ elif tool_name == "agent_finish":
229
+ should_agent_finish = result.get("agent_completed", False)
230
+
231
+ _update_tracer_with_result(tracer, execution_id, is_error, result, error_payload)
232
+
233
+ except (ConnectionError, RuntimeError, ValueError, TypeError, OSError) as e:
234
+ error_msg = str(e)
235
+ if tracer and execution_id:
236
+ tracer.update_tool_execution(execution_id, "error", error_msg)
237
+ raise
238
+
239
+ observation_xml, images = _format_tool_result(tool_name, result)
240
+ return observation_xml, images, should_agent_finish
241
+
242
+
243
+ def _get_tracer_and_agent_id(agent_state: Any | None) -> tuple[Any | None, str]:
244
+ try:
245
+ from aipt_v2.telemetry.tracer import get_global_tracer
246
+
247
+ tracer = get_global_tracer()
248
+ agent_id = agent_state.agent_id if agent_state else "unknown_agent"
249
+ except (ImportError, AttributeError):
250
+ tracer = None
251
+ agent_id = "unknown_agent"
252
+
253
+ return tracer, agent_id
254
+
255
+
256
+ async def process_tool_invocations(
257
+ tool_invocations: list[dict[str, Any]],
258
+ conversation_history: list[dict[str, Any]],
259
+ agent_state: Any | None = None,
260
+ ) -> bool:
261
+ observation_parts: list[str] = []
262
+ all_images: list[dict[str, Any]] = []
263
+ should_agent_finish = False
264
+
265
+ tracer, agent_id = _get_tracer_and_agent_id(agent_state)
266
+
267
+ for tool_inv in tool_invocations:
268
+ observation_xml, images, tool_should_finish = await _execute_single_tool(
269
+ tool_inv, agent_state, tracer, agent_id
270
+ )
271
+ observation_parts.append(observation_xml)
272
+ all_images.extend(images)
273
+
274
+ if tool_should_finish:
275
+ should_agent_finish = True
276
+
277
+ if all_images:
278
+ content = [{"type": "text", "text": "Tool Results:\n\n" + "\n\n".join(observation_parts)}]
279
+ content.extend(all_images)
280
+ conversation_history.append({"role": "user", "content": content})
281
+ else:
282
+ observation_content = "Tool Results:\n\n" + "\n\n".join(observation_parts)
283
+ conversation_history.append({"role": "user", "content": observation_content})
284
+
285
+ return should_agent_finish
286
+
287
+
288
+ def extract_screenshot_from_result(result: Any) -> str | None:
289
+ if not isinstance(result, dict):
290
+ return None
291
+
292
+ screenshot = result.get("screenshot")
293
+ if isinstance(screenshot, str) and screenshot:
294
+ return screenshot
295
+
296
+ return None
297
+
298
+
299
+ def remove_screenshot_from_result(result: Any) -> Any:
300
+ if not isinstance(result, dict):
301
+ return result
302
+
303
+ result_copy = result.copy()
304
+ if "screenshot" in result_copy:
305
+ result_copy["screenshot"] = "[Image data extracted - see attached image]"
306
+
307
+ return result_copy