aiptx 2.0.7__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.
Files changed (187) 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 +46 -0
  6. aipt_v2/agents/base.py +520 -0
  7. aipt_v2/agents/exploit_agent.py +688 -0
  8. aipt_v2/agents/ptt.py +406 -0
  9. aipt_v2/agents/state.py +168 -0
  10. aipt_v2/app.py +957 -0
  11. aipt_v2/browser/__init__.py +31 -0
  12. aipt_v2/browser/automation.py +458 -0
  13. aipt_v2/browser/crawler.py +453 -0
  14. aipt_v2/cli.py +2933 -0
  15. aipt_v2/compliance/__init__.py +71 -0
  16. aipt_v2/compliance/compliance_report.py +449 -0
  17. aipt_v2/compliance/framework_mapper.py +424 -0
  18. aipt_v2/compliance/nist_mapping.py +345 -0
  19. aipt_v2/compliance/owasp_mapping.py +330 -0
  20. aipt_v2/compliance/pci_mapping.py +297 -0
  21. aipt_v2/config.py +341 -0
  22. aipt_v2/core/__init__.py +43 -0
  23. aipt_v2/core/agent.py +630 -0
  24. aipt_v2/core/llm.py +395 -0
  25. aipt_v2/core/memory.py +305 -0
  26. aipt_v2/core/ptt.py +329 -0
  27. aipt_v2/database/__init__.py +14 -0
  28. aipt_v2/database/models.py +232 -0
  29. aipt_v2/database/repository.py +384 -0
  30. aipt_v2/docker/__init__.py +23 -0
  31. aipt_v2/docker/builder.py +260 -0
  32. aipt_v2/docker/manager.py +222 -0
  33. aipt_v2/docker/sandbox.py +371 -0
  34. aipt_v2/evasion/__init__.py +58 -0
  35. aipt_v2/evasion/request_obfuscator.py +272 -0
  36. aipt_v2/evasion/tls_fingerprint.py +285 -0
  37. aipt_v2/evasion/ua_rotator.py +301 -0
  38. aipt_v2/evasion/waf_bypass.py +439 -0
  39. aipt_v2/execution/__init__.py +23 -0
  40. aipt_v2/execution/executor.py +302 -0
  41. aipt_v2/execution/parser.py +544 -0
  42. aipt_v2/execution/terminal.py +337 -0
  43. aipt_v2/health.py +437 -0
  44. aipt_v2/intelligence/__init__.py +194 -0
  45. aipt_v2/intelligence/adaptation.py +474 -0
  46. aipt_v2/intelligence/auth.py +520 -0
  47. aipt_v2/intelligence/chaining.py +775 -0
  48. aipt_v2/intelligence/correlation.py +536 -0
  49. aipt_v2/intelligence/cve_aipt.py +334 -0
  50. aipt_v2/intelligence/cve_info.py +1111 -0
  51. aipt_v2/intelligence/knowledge_graph.py +590 -0
  52. aipt_v2/intelligence/learning.py +626 -0
  53. aipt_v2/intelligence/llm_analyzer.py +502 -0
  54. aipt_v2/intelligence/llm_tool_selector.py +518 -0
  55. aipt_v2/intelligence/payload_generator.py +562 -0
  56. aipt_v2/intelligence/rag.py +239 -0
  57. aipt_v2/intelligence/scope.py +442 -0
  58. aipt_v2/intelligence/searchers/__init__.py +5 -0
  59. aipt_v2/intelligence/searchers/exploitdb_searcher.py +523 -0
  60. aipt_v2/intelligence/searchers/github_searcher.py +467 -0
  61. aipt_v2/intelligence/searchers/google_searcher.py +281 -0
  62. aipt_v2/intelligence/tools.json +443 -0
  63. aipt_v2/intelligence/triage.py +670 -0
  64. aipt_v2/interactive_shell.py +559 -0
  65. aipt_v2/interface/__init__.py +5 -0
  66. aipt_v2/interface/cli.py +230 -0
  67. aipt_v2/interface/main.py +501 -0
  68. aipt_v2/interface/tui.py +1276 -0
  69. aipt_v2/interface/utils.py +583 -0
  70. aipt_v2/llm/__init__.py +39 -0
  71. aipt_v2/llm/config.py +26 -0
  72. aipt_v2/llm/llm.py +514 -0
  73. aipt_v2/llm/memory.py +214 -0
  74. aipt_v2/llm/request_queue.py +89 -0
  75. aipt_v2/llm/utils.py +89 -0
  76. aipt_v2/local_tool_installer.py +1467 -0
  77. aipt_v2/models/__init__.py +15 -0
  78. aipt_v2/models/findings.py +295 -0
  79. aipt_v2/models/phase_result.py +224 -0
  80. aipt_v2/models/scan_config.py +207 -0
  81. aipt_v2/monitoring/grafana/dashboards/aipt-dashboard.json +355 -0
  82. aipt_v2/monitoring/grafana/dashboards/default.yml +17 -0
  83. aipt_v2/monitoring/grafana/datasources/prometheus.yml +17 -0
  84. aipt_v2/monitoring/prometheus.yml +60 -0
  85. aipt_v2/orchestration/__init__.py +52 -0
  86. aipt_v2/orchestration/pipeline.py +398 -0
  87. aipt_v2/orchestration/progress.py +300 -0
  88. aipt_v2/orchestration/scheduler.py +296 -0
  89. aipt_v2/orchestrator.py +2427 -0
  90. aipt_v2/payloads/__init__.py +27 -0
  91. aipt_v2/payloads/cmdi.py +150 -0
  92. aipt_v2/payloads/sqli.py +263 -0
  93. aipt_v2/payloads/ssrf.py +204 -0
  94. aipt_v2/payloads/templates.py +222 -0
  95. aipt_v2/payloads/traversal.py +166 -0
  96. aipt_v2/payloads/xss.py +204 -0
  97. aipt_v2/prompts/__init__.py +60 -0
  98. aipt_v2/proxy/__init__.py +29 -0
  99. aipt_v2/proxy/history.py +352 -0
  100. aipt_v2/proxy/interceptor.py +452 -0
  101. aipt_v2/recon/__init__.py +44 -0
  102. aipt_v2/recon/dns.py +241 -0
  103. aipt_v2/recon/osint.py +367 -0
  104. aipt_v2/recon/subdomain.py +372 -0
  105. aipt_v2/recon/tech_detect.py +311 -0
  106. aipt_v2/reports/__init__.py +17 -0
  107. aipt_v2/reports/generator.py +313 -0
  108. aipt_v2/reports/html_report.py +378 -0
  109. aipt_v2/runtime/__init__.py +53 -0
  110. aipt_v2/runtime/base.py +30 -0
  111. aipt_v2/runtime/docker.py +401 -0
  112. aipt_v2/runtime/local.py +346 -0
  113. aipt_v2/runtime/tool_server.py +205 -0
  114. aipt_v2/runtime/vps.py +830 -0
  115. aipt_v2/scanners/__init__.py +28 -0
  116. aipt_v2/scanners/base.py +273 -0
  117. aipt_v2/scanners/nikto.py +244 -0
  118. aipt_v2/scanners/nmap.py +402 -0
  119. aipt_v2/scanners/nuclei.py +273 -0
  120. aipt_v2/scanners/web.py +454 -0
  121. aipt_v2/scripts/security_audit.py +366 -0
  122. aipt_v2/setup_wizard.py +941 -0
  123. aipt_v2/skills/__init__.py +80 -0
  124. aipt_v2/skills/agents/__init__.py +14 -0
  125. aipt_v2/skills/agents/api_tester.py +706 -0
  126. aipt_v2/skills/agents/base.py +477 -0
  127. aipt_v2/skills/agents/code_review.py +459 -0
  128. aipt_v2/skills/agents/security_agent.py +336 -0
  129. aipt_v2/skills/agents/web_pentest.py +818 -0
  130. aipt_v2/skills/prompts/__init__.py +647 -0
  131. aipt_v2/system_detector.py +539 -0
  132. aipt_v2/telemetry/__init__.py +7 -0
  133. aipt_v2/telemetry/tracer.py +347 -0
  134. aipt_v2/terminal/__init__.py +28 -0
  135. aipt_v2/terminal/executor.py +400 -0
  136. aipt_v2/terminal/sandbox.py +350 -0
  137. aipt_v2/tools/__init__.py +44 -0
  138. aipt_v2/tools/active_directory/__init__.py +78 -0
  139. aipt_v2/tools/active_directory/ad_config.py +238 -0
  140. aipt_v2/tools/active_directory/bloodhound_wrapper.py +447 -0
  141. aipt_v2/tools/active_directory/kerberos_attacks.py +430 -0
  142. aipt_v2/tools/active_directory/ldap_enum.py +533 -0
  143. aipt_v2/tools/active_directory/smb_attacks.py +505 -0
  144. aipt_v2/tools/agents_graph/__init__.py +19 -0
  145. aipt_v2/tools/agents_graph/agents_graph_actions.py +69 -0
  146. aipt_v2/tools/api_security/__init__.py +76 -0
  147. aipt_v2/tools/api_security/api_discovery.py +608 -0
  148. aipt_v2/tools/api_security/graphql_scanner.py +622 -0
  149. aipt_v2/tools/api_security/jwt_analyzer.py +577 -0
  150. aipt_v2/tools/api_security/openapi_fuzzer.py +761 -0
  151. aipt_v2/tools/browser/__init__.py +5 -0
  152. aipt_v2/tools/browser/browser_actions.py +238 -0
  153. aipt_v2/tools/browser/browser_instance.py +535 -0
  154. aipt_v2/tools/browser/tab_manager.py +344 -0
  155. aipt_v2/tools/cloud/__init__.py +70 -0
  156. aipt_v2/tools/cloud/cloud_config.py +273 -0
  157. aipt_v2/tools/cloud/cloud_scanner.py +639 -0
  158. aipt_v2/tools/cloud/prowler_tool.py +571 -0
  159. aipt_v2/tools/cloud/scoutsuite_tool.py +359 -0
  160. aipt_v2/tools/executor.py +307 -0
  161. aipt_v2/tools/parser.py +408 -0
  162. aipt_v2/tools/proxy/__init__.py +5 -0
  163. aipt_v2/tools/proxy/proxy_actions.py +103 -0
  164. aipt_v2/tools/proxy/proxy_manager.py +789 -0
  165. aipt_v2/tools/registry.py +196 -0
  166. aipt_v2/tools/scanners/__init__.py +343 -0
  167. aipt_v2/tools/scanners/acunetix_tool.py +712 -0
  168. aipt_v2/tools/scanners/burp_tool.py +631 -0
  169. aipt_v2/tools/scanners/config.py +156 -0
  170. aipt_v2/tools/scanners/nessus_tool.py +588 -0
  171. aipt_v2/tools/scanners/zap_tool.py +612 -0
  172. aipt_v2/tools/terminal/__init__.py +5 -0
  173. aipt_v2/tools/terminal/terminal_actions.py +37 -0
  174. aipt_v2/tools/terminal/terminal_manager.py +153 -0
  175. aipt_v2/tools/terminal/terminal_session.py +449 -0
  176. aipt_v2/tools/tool_processing.py +108 -0
  177. aipt_v2/utils/__init__.py +17 -0
  178. aipt_v2/utils/logging.py +202 -0
  179. aipt_v2/utils/model_manager.py +187 -0
  180. aipt_v2/utils/searchers/__init__.py +269 -0
  181. aipt_v2/verify_install.py +793 -0
  182. aiptx-2.0.7.dist-info/METADATA +345 -0
  183. aiptx-2.0.7.dist-info/RECORD +187 -0
  184. aiptx-2.0.7.dist-info/WHEEL +5 -0
  185. aiptx-2.0.7.dist-info/entry_points.txt +7 -0
  186. aiptx-2.0.7.dist-info/licenses/LICENSE +21 -0
  187. aiptx-2.0.7.dist-info/top_level.txt +1 -0
@@ -0,0 +1,583 @@
1
+ from __future__ import annotations
2
+
3
+ import ipaddress
4
+ import re
5
+ import secrets
6
+ import shutil
7
+ import subprocess
8
+ import sys
9
+ import tempfile
10
+ from pathlib import Path
11
+ from typing import Any, Optional
12
+ from urllib.parse import urlparse
13
+
14
+ from rich.console import Console
15
+ from rich.panel import Panel
16
+ from rich.text import Text
17
+
18
+ # Lazy import for docker
19
+ docker = None
20
+ DockerException = None
21
+ ImageNotFound = None
22
+
23
+
24
+ def _ensure_docker():
25
+ """Lazy load docker module."""
26
+ global docker, DockerException, ImageNotFound
27
+ if docker is None:
28
+ import docker as _docker
29
+ from docker.errors import DockerException as _DockerException, ImageNotFound as _ImageNotFound
30
+ docker = _docker
31
+ DockerException = _DockerException
32
+ ImageNotFound = _ImageNotFound
33
+
34
+
35
+ # Token formatting utilities
36
+ def format_token_count(count: float) -> str:
37
+ count = int(count)
38
+ if count >= 1_000_000:
39
+ return f"{count / 1_000_000:.1f}M"
40
+ if count >= 1_000:
41
+ return f"{count / 1_000:.1f}K"
42
+ return str(count)
43
+
44
+
45
+ # Display utilities
46
+ def get_severity_color(severity: str) -> str:
47
+ severity_colors = {
48
+ "critical": "#dc2626",
49
+ "high": "#ea580c",
50
+ "medium": "#d97706",
51
+ "low": "#65a30d",
52
+ "info": "#0284c7",
53
+ }
54
+ return severity_colors.get(severity, "#6b7280")
55
+
56
+
57
+ def _build_vulnerability_stats(stats_text: Text, tracer: Any) -> None:
58
+ """Build vulnerability section of stats text."""
59
+ vuln_count = len(tracer.vulnerability_reports)
60
+
61
+ if vuln_count > 0:
62
+ severity_counts = {"critical": 0, "high": 0, "medium": 0, "low": 0, "info": 0}
63
+ for report in tracer.vulnerability_reports:
64
+ severity = report.get("severity", "").lower()
65
+ if severity in severity_counts:
66
+ severity_counts[severity] += 1
67
+
68
+ stats_text.append("🔍 Vulnerabilities Found: ", style="bold red")
69
+
70
+ severity_parts = []
71
+ for severity in ["critical", "high", "medium", "low", "info"]:
72
+ count = severity_counts[severity]
73
+ if count > 0:
74
+ severity_color = get_severity_color(severity)
75
+ severity_text = Text()
76
+ severity_text.append(f"{severity.upper()}: ", style=severity_color)
77
+ severity_text.append(str(count), style=f"bold {severity_color}")
78
+ severity_parts.append(severity_text)
79
+
80
+ for i, part in enumerate(severity_parts):
81
+ stats_text.append(part)
82
+ if i < len(severity_parts) - 1:
83
+ stats_text.append(" | ", style="dim white")
84
+
85
+ stats_text.append(" (Total: ", style="dim white")
86
+ stats_text.append(str(vuln_count), style="bold yellow")
87
+ stats_text.append(")", style="dim white")
88
+ stats_text.append("\n")
89
+ else:
90
+ stats_text.append("🔍 Vulnerabilities Found: ", style="bold green")
91
+ stats_text.append("0", style="bold white")
92
+ stats_text.append(" (No exploitable vulnerabilities detected)", style="dim green")
93
+ stats_text.append("\n")
94
+
95
+
96
+ def _build_llm_stats(stats_text: Text, total_stats: dict[str, Any]) -> None:
97
+ """Build LLM usage section of stats text."""
98
+ if total_stats["requests"] > 0:
99
+ stats_text.append("\n")
100
+ stats_text.append("📥 Input Tokens: ", style="bold cyan")
101
+ stats_text.append(format_token_count(total_stats["input_tokens"]), style="bold white")
102
+
103
+ if total_stats["cached_tokens"] > 0:
104
+ stats_text.append(" • ", style="dim white")
105
+ stats_text.append("⚡ Cached Tokens: ", style="bold green")
106
+ stats_text.append(format_token_count(total_stats["cached_tokens"]), style="bold white")
107
+
108
+ stats_text.append(" • ", style="dim white")
109
+ stats_text.append("📤 Output Tokens: ", style="bold cyan")
110
+ stats_text.append(format_token_count(total_stats["output_tokens"]), style="bold white")
111
+
112
+ if total_stats["cost"] > 0:
113
+ stats_text.append(" • ", style="dim white")
114
+ stats_text.append("💰 Total Cost: ", style="bold cyan")
115
+ stats_text.append(f"${total_stats['cost']:.4f}", style="bold yellow")
116
+ else:
117
+ stats_text.append("\n")
118
+ stats_text.append("💰 Total Cost: ", style="bold cyan")
119
+ stats_text.append("$0.0000 ", style="bold yellow")
120
+ stats_text.append("• ", style="bold white")
121
+ stats_text.append("📊 Tokens: ", style="bold cyan")
122
+ stats_text.append("0", style="bold white")
123
+
124
+
125
+ def build_final_stats_text(tracer: Any) -> Text:
126
+ """Build stats text for final output with detailed messages and LLM usage."""
127
+ stats_text = Text()
128
+ if not tracer:
129
+ return stats_text
130
+
131
+ _build_vulnerability_stats(stats_text, tracer)
132
+
133
+ tool_count = tracer.get_real_tool_count()
134
+ agent_count = len(tracer.agents)
135
+
136
+ stats_text.append("🤖 Agents Used: ", style="bold cyan")
137
+ stats_text.append(str(agent_count), style="bold white")
138
+ stats_text.append(" • ", style="dim white")
139
+ stats_text.append("🛠️ Tools Called: ", style="bold cyan")
140
+ stats_text.append(str(tool_count), style="bold white")
141
+
142
+ llm_stats = tracer.get_total_llm_stats()
143
+ _build_llm_stats(stats_text, llm_stats["total"])
144
+
145
+ return stats_text
146
+
147
+
148
+ def build_live_stats_text(tracer: Any, agent_config: dict[str, Any] | None = None) -> Text:
149
+ stats_text = Text()
150
+ if not tracer:
151
+ return stats_text
152
+
153
+ vuln_count = len(tracer.vulnerability_reports)
154
+ tool_count = tracer.get_real_tool_count()
155
+ agent_count = len(tracer.agents)
156
+
157
+ stats_text.append("🔍 Vulnerabilities: ", style="bold white")
158
+ stats_text.append(f"{vuln_count}", style="dim white")
159
+ stats_text.append("\n")
160
+ if vuln_count > 0:
161
+ severity_counts = {"critical": 0, "high": 0, "medium": 0, "low": 0, "info": 0}
162
+ for report in tracer.vulnerability_reports:
163
+ severity = report.get("severity", "").lower()
164
+ if severity in severity_counts:
165
+ severity_counts[severity] += 1
166
+
167
+ severity_parts = []
168
+ for severity in ["critical", "high", "medium", "low", "info"]:
169
+ count = severity_counts[severity]
170
+ if count > 0:
171
+ severity_color = get_severity_color(severity)
172
+ severity_text = Text()
173
+ severity_text.append(f"{severity.upper()}: ", style=severity_color)
174
+ severity_text.append(str(count), style=f"bold {severity_color}")
175
+ severity_parts.append(severity_text)
176
+
177
+ for i, part in enumerate(severity_parts):
178
+ stats_text.append(part)
179
+ if i < len(severity_parts) - 1:
180
+ stats_text.append(" | ", style="dim white")
181
+
182
+ stats_text.append("\n")
183
+
184
+ if agent_config:
185
+ llm_config = agent_config["llm_config"]
186
+ model = getattr(llm_config, "model_name", "Unknown")
187
+ stats_text.append(f"🧠 Model: {model}")
188
+ stats_text.append("\n")
189
+
190
+ stats_text.append("🤖 Agents: ", style="bold white")
191
+ stats_text.append(str(agent_count), style="dim white")
192
+ stats_text.append(" • ", style="dim white")
193
+ stats_text.append("🛠️ Tools: ", style="bold white")
194
+ stats_text.append(str(tool_count), style="dim white")
195
+
196
+ llm_stats = tracer.get_total_llm_stats()
197
+ total_stats = llm_stats["total"]
198
+
199
+ stats_text.append("\n")
200
+
201
+ stats_text.append("📥 Input: ", style="bold white")
202
+ stats_text.append(format_token_count(total_stats["input_tokens"]), style="dim white")
203
+
204
+ stats_text.append(" • ", style="dim white")
205
+ stats_text.append("⚡ ", style="bold white")
206
+ stats_text.append("Cached: ", style="bold white")
207
+ stats_text.append(format_token_count(total_stats["cached_tokens"]), style="dim white")
208
+
209
+ stats_text.append("\n")
210
+
211
+ stats_text.append("📤 Output: ", style="bold white")
212
+ stats_text.append(format_token_count(total_stats["output_tokens"]), style="dim white")
213
+
214
+ stats_text.append(" • ", style="dim white")
215
+ stats_text.append("💰 Cost: ", style="bold white")
216
+ stats_text.append(f"${total_stats['cost']:.4f}", style="dim white")
217
+
218
+ return stats_text
219
+
220
+
221
+ # Name generation utilities
222
+
223
+
224
+ def _slugify_for_run_name(text: str, max_length: int = 32) -> str:
225
+ text = text.lower().strip()
226
+ text = re.sub(r"[^a-z0-9]+", "-", text)
227
+ text = text.strip("-")
228
+ if len(text) > max_length:
229
+ text = text[:max_length].rstrip("-")
230
+ return text or "pentest"
231
+
232
+
233
+ def _derive_target_label_for_run_name(targets_info: list[dict[str, Any]] | None) -> str: # noqa: PLR0911
234
+ if not targets_info:
235
+ return "pentest"
236
+
237
+ first = targets_info[0]
238
+ target_type = first.get("type")
239
+ details = first.get("details", {}) or {}
240
+ original = first.get("original", "") or ""
241
+
242
+ if target_type == "web_application":
243
+ url = details.get("target_url", original)
244
+ try:
245
+ parsed = urlparse(url)
246
+ return str(parsed.netloc or parsed.path or url)
247
+ except Exception: # noqa: BLE001
248
+ return str(url)
249
+
250
+ if target_type == "repository":
251
+ repo = details.get("target_repo", original)
252
+ parsed = urlparse(repo)
253
+ path = parsed.path or repo
254
+ name = path.rstrip("/").split("/")[-1] or path
255
+ if name.endswith(".git"):
256
+ name = name[:-4]
257
+ return str(name)
258
+
259
+ if target_type == "local_code":
260
+ path_str = details.get("target_path", original)
261
+ try:
262
+ return str(Path(path_str).name or path_str)
263
+ except Exception: # noqa: BLE001
264
+ return str(path_str)
265
+
266
+ if target_type == "ip_address":
267
+ return str(details.get("target_ip", original) or original)
268
+
269
+ return str(original or "pentest")
270
+
271
+
272
+ def generate_run_name(targets_info: list[dict[str, Any]] | None = None) -> str:
273
+ base_label = _derive_target_label_for_run_name(targets_info)
274
+ slug = _slugify_for_run_name(base_label)
275
+
276
+ random_suffix = secrets.token_hex(2)
277
+
278
+ return f"{slug}_{random_suffix}"
279
+
280
+
281
+ # Target processing utilities
282
+ def infer_target_type(target: str) -> tuple[str, dict[str, str]]: # noqa: PLR0911
283
+ if not target or not isinstance(target, str):
284
+ raise ValueError("Target must be a non-empty string")
285
+
286
+ target = target.strip()
287
+
288
+ lower_target = target.lower()
289
+ bare_repo_prefixes = (
290
+ "github.com/",
291
+ "www.github.com/",
292
+ "gitlab.com/",
293
+ "www.gitlab.com/",
294
+ "bitbucket.org/",
295
+ "www.bitbucket.org/",
296
+ )
297
+ if any(lower_target.startswith(p) for p in bare_repo_prefixes):
298
+ return "repository", {"target_repo": f"https://{target}"}
299
+
300
+ parsed = urlparse(target)
301
+ if parsed.scheme in ("http", "https"):
302
+ if any(
303
+ host in parsed.netloc.lower() for host in ["github.com", "gitlab.com", "bitbucket.org"]
304
+ ):
305
+ return "repository", {"target_repo": target}
306
+ return "web_application", {"target_url": target}
307
+
308
+ try:
309
+ ip_obj = ipaddress.ip_address(target)
310
+ except ValueError:
311
+ pass
312
+ else:
313
+ return "ip_address", {"target_ip": str(ip_obj)}
314
+
315
+ path = Path(target).expanduser()
316
+ try:
317
+ if path.exists():
318
+ if path.is_dir():
319
+ resolved = path.resolve()
320
+ return "local_code", {"target_path": str(resolved)}
321
+ raise ValueError(f"Path exists but is not a directory: {target}")
322
+ except (OSError, RuntimeError) as e:
323
+ raise ValueError(f"Invalid path: {target} - {e!s}") from e
324
+
325
+ if target.startswith("git@") or target.endswith(".git"):
326
+ return "repository", {"target_repo": target}
327
+
328
+ if "." in target and "/" not in target and not target.startswith("."):
329
+ parts = target.split(".")
330
+ if len(parts) >= 2 and all(p and p.strip() for p in parts):
331
+ return "web_application", {"target_url": f"https://{target}"}
332
+
333
+ raise ValueError(
334
+ f"Invalid target: {target}\n"
335
+ "Target must be one of:\n"
336
+ "- A valid URL (http:// or https://)\n"
337
+ "- A Git repository URL (https://github.com/... or git@github.com:...)\n"
338
+ "- A local directory path\n"
339
+ "- A domain name (e.g., example.com)\n"
340
+ "- An IP address (e.g., 192.168.1.10)"
341
+ )
342
+
343
+
344
+ def sanitize_name(name: str) -> str:
345
+ sanitized = re.sub(r"[^A-Za-z0-9._-]", "-", name.strip())
346
+ return sanitized or "target"
347
+
348
+
349
+ def derive_repo_base_name(repo_url: str) -> str:
350
+ if repo_url.endswith("/"):
351
+ repo_url = repo_url[:-1]
352
+
353
+ if ":" in repo_url and repo_url.startswith("git@"):
354
+ path_part = repo_url.split(":", 1)[1]
355
+ else:
356
+ path_part = urlparse(repo_url).path or repo_url
357
+
358
+ candidate = path_part.split("/")[-1]
359
+ if candidate.endswith(".git"):
360
+ candidate = candidate[:-4]
361
+
362
+ return sanitize_name(candidate or "repository")
363
+
364
+
365
+ def derive_local_base_name(path_str: str) -> str:
366
+ try:
367
+ base = Path(path_str).resolve().name
368
+ except (OSError, RuntimeError):
369
+ base = Path(path_str).name
370
+ return sanitize_name(base or "workspace")
371
+
372
+
373
+ def assign_workspace_subdirs(targets_info: list[dict[str, Any]]) -> None:
374
+ name_counts: dict[str, int] = {}
375
+
376
+ for target in targets_info:
377
+ target_type = target["type"]
378
+ details = target["details"]
379
+
380
+ base_name: str | None = None
381
+ if target_type == "repository":
382
+ base_name = derive_repo_base_name(details["target_repo"])
383
+ elif target_type == "local_code":
384
+ base_name = derive_local_base_name(details.get("target_path", "local"))
385
+
386
+ if base_name is None:
387
+ continue
388
+
389
+ count = name_counts.get(base_name, 0) + 1
390
+ name_counts[base_name] = count
391
+
392
+ workspace_subdir = base_name if count == 1 else f"{base_name}-{count}"
393
+
394
+ details["workspace_subdir"] = workspace_subdir
395
+
396
+
397
+ def collect_local_sources(targets_info: list[dict[str, Any]]) -> list[dict[str, str]]:
398
+ local_sources: list[dict[str, str]] = []
399
+
400
+ for target_info in targets_info:
401
+ details = target_info["details"]
402
+ workspace_subdir = details.get("workspace_subdir")
403
+
404
+ if target_info["type"] == "local_code" and "target_path" in details:
405
+ local_sources.append(
406
+ {
407
+ "source_path": details["target_path"],
408
+ "workspace_subdir": workspace_subdir,
409
+ }
410
+ )
411
+
412
+ elif target_info["type"] == "repository" and "cloned_repo_path" in details:
413
+ local_sources.append(
414
+ {
415
+ "source_path": details["cloned_repo_path"],
416
+ "workspace_subdir": workspace_subdir,
417
+ }
418
+ )
419
+
420
+ return local_sources
421
+
422
+
423
+ # Repository utilities
424
+ def clone_repository(repo_url: str, run_name: str, dest_name: str | None = None) -> str:
425
+ console = Console()
426
+
427
+ git_executable = shutil.which("git")
428
+ if git_executable is None:
429
+ raise FileNotFoundError("Git executable not found in PATH")
430
+
431
+ temp_dir = Path(tempfile.gettempdir()) / "aipt_repos" / run_name
432
+ temp_dir.mkdir(parents=True, exist_ok=True)
433
+
434
+ if dest_name:
435
+ repo_name = dest_name
436
+ else:
437
+ repo_name = Path(repo_url).stem if repo_url.endswith(".git") else Path(repo_url).name
438
+
439
+ clone_path = temp_dir / repo_name
440
+
441
+ if clone_path.exists():
442
+ shutil.rmtree(clone_path)
443
+
444
+ try:
445
+ with console.status(f"[bold cyan]Cloning repository {repo_url}...", spinner="dots"):
446
+ subprocess.run( # noqa: S603
447
+ [
448
+ git_executable,
449
+ "clone",
450
+ repo_url,
451
+ str(clone_path),
452
+ ],
453
+ capture_output=True,
454
+ text=True,
455
+ check=True,
456
+ )
457
+
458
+ return str(clone_path.absolute())
459
+
460
+ except subprocess.CalledProcessError as e:
461
+ error_text = Text()
462
+ error_text.append("❌ ", style="bold red")
463
+ error_text.append("REPOSITORY CLONE FAILED", style="bold red")
464
+ error_text.append("\n\n", style="white")
465
+ error_text.append(f"Could not clone repository: {repo_url}\n", style="white")
466
+ error_text.append(
467
+ f"Error: {e.stderr if hasattr(e, 'stderr') and e.stderr else str(e)}", style="dim red"
468
+ )
469
+
470
+ panel = Panel(
471
+ error_text,
472
+ title="[bold red]🛡️ AIPT CLONE ERROR",
473
+ title_align="center",
474
+ border_style="red",
475
+ padding=(1, 2),
476
+ )
477
+ console.print("\n")
478
+ console.print(panel)
479
+ console.print()
480
+ sys.exit(1)
481
+ except FileNotFoundError:
482
+ error_text = Text()
483
+ error_text.append("❌ ", style="bold red")
484
+ error_text.append("GIT NOT FOUND", style="bold red")
485
+ error_text.append("\n\n", style="white")
486
+ error_text.append("Git is not installed or not available in PATH.\n", style="white")
487
+ error_text.append("Please install Git to clone repositories.\n", style="white")
488
+
489
+ panel = Panel(
490
+ error_text,
491
+ title="[bold red]🛡️ AIPT CLONE ERROR",
492
+ title_align="center",
493
+ border_style="red",
494
+ padding=(1, 2),
495
+ )
496
+ console.print("\n")
497
+ console.print(panel)
498
+ console.print()
499
+ sys.exit(1)
500
+
501
+
502
+ # Docker utilities
503
+ def check_docker_connection() -> Any:
504
+ _ensure_docker()
505
+ try:
506
+ return docker.from_env()
507
+ except (DockerException, Exception) as e:
508
+ console = Console()
509
+ error_text = Text()
510
+ error_text.append("❌ ", style="bold red")
511
+ error_text.append("DOCKER NOT AVAILABLE", style="bold red")
512
+ error_text.append("\n\n", style="white")
513
+ error_text.append("Cannot connect to Docker daemon.\n", style="white")
514
+ error_text.append("Please ensure Docker is installed and running.\n\n", style="white")
515
+ error_text.append("Try running: ", style="dim white")
516
+ error_text.append("sudo systemctl start docker", style="dim cyan")
517
+
518
+ panel = Panel(
519
+ error_text,
520
+ title="[bold red]🛡️ AIPT STARTUP ERROR",
521
+ title_align="center",
522
+ border_style="red",
523
+ padding=(1, 2),
524
+ )
525
+ console.print("\n", panel, "\n")
526
+ raise RuntimeError("Docker not available") from None
527
+
528
+
529
+ def image_exists(client: Any, image_name: str) -> bool:
530
+ _ensure_docker()
531
+ try:
532
+ client.images.get(image_name)
533
+ except (ImageNotFound, Exception):
534
+ return False
535
+ else:
536
+ return True
537
+
538
+
539
+ def update_layer_status(layers_info: dict[str, str], layer_id: str, layer_status: str) -> None:
540
+ if "Pull complete" in layer_status or "Already exists" in layer_status:
541
+ layers_info[layer_id] = "✓"
542
+ elif "Downloading" in layer_status:
543
+ layers_info[layer_id] = "↓"
544
+ elif "Extracting" in layer_status:
545
+ layers_info[layer_id] = "📦"
546
+ elif "Waiting" in layer_status:
547
+ layers_info[layer_id] = "⏳"
548
+ else:
549
+ layers_info[layer_id] = "•"
550
+
551
+
552
+ def process_pull_line(
553
+ line: dict[str, Any], layers_info: dict[str, str], status: Any, last_update: str
554
+ ) -> str:
555
+ if "id" in line and "status" in line:
556
+ layer_id = line["id"]
557
+ update_layer_status(layers_info, layer_id, line["status"])
558
+
559
+ completed = sum(1 for v in layers_info.values() if v == "✓")
560
+ total = len(layers_info)
561
+
562
+ if total > 0:
563
+ update_msg = f"[bold cyan]Progress: {completed}/{total} layers complete"
564
+ if update_msg != last_update:
565
+ status.update(update_msg)
566
+ return update_msg
567
+
568
+ elif "status" in line and "id" not in line:
569
+ global_status = line["status"]
570
+ if "Pulling from" in global_status:
571
+ status.update("[bold cyan]Fetching image manifest...")
572
+ elif "Digest:" in global_status:
573
+ status.update("[bold cyan]Verifying image...")
574
+ elif "Status:" in global_status:
575
+ status.update("[bold cyan]Finalizing...")
576
+
577
+ return last_update
578
+
579
+
580
+ # LLM utilities
581
+ def validate_llm_response(response: Any) -> None:
582
+ if not response or not response.choices or not response.choices[0].message.content:
583
+ raise RuntimeError("Invalid response from LLM")
@@ -0,0 +1,39 @@
1
+ """
2
+ AIPT LLM Module - Universal LLM interface via litellm
3
+ """
4
+
5
+ # Core config that doesn't require litellm
6
+ from aipt_v2.llm.config import LLMConfig
7
+
8
+ __all__ = [
9
+ "LLM",
10
+ "LLMConfig",
11
+ "LLMResponse",
12
+ "LLMRequestFailedError",
13
+ "RequestStats",
14
+ "MemoryCompressor",
15
+ "get_global_queue",
16
+ ]
17
+
18
+
19
+ def __getattr__(name):
20
+ """Lazy import for components requiring litellm"""
21
+ if name == "LLM":
22
+ from aipt_v2.llm.llm import LLM
23
+ return LLM
24
+ elif name == "LLMResponse":
25
+ from aipt_v2.llm.llm import LLMResponse
26
+ return LLMResponse
27
+ elif name == "LLMRequestFailedError":
28
+ from aipt_v2.llm.llm import LLMRequestFailedError
29
+ return LLMRequestFailedError
30
+ elif name == "RequestStats":
31
+ from aipt_v2.llm.llm import RequestStats
32
+ return RequestStats
33
+ elif name == "MemoryCompressor":
34
+ from aipt_v2.llm.memory import MemoryCompressor
35
+ return MemoryCompressor
36
+ elif name == "get_global_queue":
37
+ from aipt_v2.llm.request_queue import get_global_queue
38
+ return get_global_queue
39
+ raise AttributeError(f"module 'aipt_v2.llm' has no attribute '{name}'")
aipt_v2/llm/config.py ADDED
@@ -0,0 +1,26 @@
1
+ """AIPT LLM Configuration"""
2
+
3
+ from __future__ import annotations
4
+ import os
5
+ from typing import Optional, List
6
+
7
+
8
+ class LLMConfig:
9
+ """Configuration for LLM interactions"""
10
+
11
+ def __init__(
12
+ self,
13
+ model_name: Optional[str] = None,
14
+ enable_prompt_caching: bool = True,
15
+ prompt_modules: Optional[List[str]] = None,
16
+ timeout: Optional[int] = None,
17
+ ):
18
+ self.model_name = model_name or os.getenv("AIPT_LLM", "openai/gpt-4")
19
+
20
+ if not self.model_name:
21
+ raise ValueError("AIPT_LLM environment variable must be set and not empty")
22
+
23
+ self.enable_prompt_caching = enable_prompt_caching
24
+ self.prompt_modules = prompt_modules or []
25
+
26
+ self.timeout = timeout or int(os.getenv("LLM_TIMEOUT", "300"))