hackagent 0.3.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (183) hide show
  1. hackagent/__init__.py +12 -0
  2. hackagent/agent.py +214 -0
  3. hackagent/api/__init__.py +1 -0
  4. hackagent/api/agent/__init__.py +1 -0
  5. hackagent/api/agent/agent_create.py +347 -0
  6. hackagent/api/agent/agent_destroy.py +140 -0
  7. hackagent/api/agent/agent_list.py +242 -0
  8. hackagent/api/agent/agent_partial_update.py +361 -0
  9. hackagent/api/agent/agent_retrieve.py +235 -0
  10. hackagent/api/agent/agent_update.py +361 -0
  11. hackagent/api/apilogs/__init__.py +1 -0
  12. hackagent/api/apilogs/apilogs_list.py +170 -0
  13. hackagent/api/apilogs/apilogs_retrieve.py +162 -0
  14. hackagent/api/attack/__init__.py +1 -0
  15. hackagent/api/attack/attack_create.py +275 -0
  16. hackagent/api/attack/attack_destroy.py +146 -0
  17. hackagent/api/attack/attack_list.py +254 -0
  18. hackagent/api/attack/attack_partial_update.py +289 -0
  19. hackagent/api/attack/attack_retrieve.py +247 -0
  20. hackagent/api/attack/attack_update.py +289 -0
  21. hackagent/api/checkout/__init__.py +1 -0
  22. hackagent/api/checkout/checkout_create.py +225 -0
  23. hackagent/api/generate/__init__.py +1 -0
  24. hackagent/api/generate/generate_create.py +253 -0
  25. hackagent/api/judge/__init__.py +1 -0
  26. hackagent/api/judge/judge_create.py +253 -0
  27. hackagent/api/key/__init__.py +1 -0
  28. hackagent/api/key/key_create.py +179 -0
  29. hackagent/api/key/key_destroy.py +103 -0
  30. hackagent/api/key/key_list.py +170 -0
  31. hackagent/api/key/key_retrieve.py +162 -0
  32. hackagent/api/organization/__init__.py +1 -0
  33. hackagent/api/organization/organization_create.py +208 -0
  34. hackagent/api/organization/organization_destroy.py +104 -0
  35. hackagent/api/organization/organization_list.py +170 -0
  36. hackagent/api/organization/organization_me_retrieve.py +126 -0
  37. hackagent/api/organization/organization_partial_update.py +222 -0
  38. hackagent/api/organization/organization_retrieve.py +163 -0
  39. hackagent/api/organization/organization_update.py +222 -0
  40. hackagent/api/prompt/__init__.py +1 -0
  41. hackagent/api/prompt/prompt_create.py +171 -0
  42. hackagent/api/prompt/prompt_destroy.py +104 -0
  43. hackagent/api/prompt/prompt_list.py +185 -0
  44. hackagent/api/prompt/prompt_partial_update.py +185 -0
  45. hackagent/api/prompt/prompt_retrieve.py +163 -0
  46. hackagent/api/prompt/prompt_update.py +185 -0
  47. hackagent/api/result/__init__.py +1 -0
  48. hackagent/api/result/result_create.py +175 -0
  49. hackagent/api/result/result_destroy.py +106 -0
  50. hackagent/api/result/result_list.py +249 -0
  51. hackagent/api/result/result_partial_update.py +193 -0
  52. hackagent/api/result/result_retrieve.py +167 -0
  53. hackagent/api/result/result_trace_create.py +177 -0
  54. hackagent/api/result/result_update.py +189 -0
  55. hackagent/api/run/__init__.py +1 -0
  56. hackagent/api/run/run_create.py +187 -0
  57. hackagent/api/run/run_destroy.py +112 -0
  58. hackagent/api/run/run_list.py +291 -0
  59. hackagent/api/run/run_partial_update.py +201 -0
  60. hackagent/api/run/run_result_create.py +177 -0
  61. hackagent/api/run/run_retrieve.py +179 -0
  62. hackagent/api/run/run_run_tests_create.py +187 -0
  63. hackagent/api/run/run_update.py +201 -0
  64. hackagent/api/user/__init__.py +1 -0
  65. hackagent/api/user/user_create.py +212 -0
  66. hackagent/api/user/user_destroy.py +106 -0
  67. hackagent/api/user/user_list.py +174 -0
  68. hackagent/api/user/user_me_retrieve.py +126 -0
  69. hackagent/api/user/user_me_update.py +196 -0
  70. hackagent/api/user/user_partial_update.py +226 -0
  71. hackagent/api/user/user_retrieve.py +167 -0
  72. hackagent/api/user/user_update.py +226 -0
  73. hackagent/attacks/AdvPrefix/__init__.py +41 -0
  74. hackagent/attacks/AdvPrefix/completions.py +416 -0
  75. hackagent/attacks/AdvPrefix/config.py +259 -0
  76. hackagent/attacks/AdvPrefix/evaluation.py +745 -0
  77. hackagent/attacks/AdvPrefix/evaluators.py +564 -0
  78. hackagent/attacks/AdvPrefix/generate.py +711 -0
  79. hackagent/attacks/AdvPrefix/utils.py +307 -0
  80. hackagent/attacks/__init__.py +35 -0
  81. hackagent/attacks/advprefix.py +507 -0
  82. hackagent/attacks/base.py +106 -0
  83. hackagent/attacks/strategies.py +906 -0
  84. hackagent/cli/__init__.py +19 -0
  85. hackagent/cli/commands/__init__.py +20 -0
  86. hackagent/cli/commands/agent.py +100 -0
  87. hackagent/cli/commands/attack.py +417 -0
  88. hackagent/cli/commands/config.py +301 -0
  89. hackagent/cli/commands/results.py +327 -0
  90. hackagent/cli/config.py +249 -0
  91. hackagent/cli/main.py +515 -0
  92. hackagent/cli/tui/__init__.py +31 -0
  93. hackagent/cli/tui/actions_logger.py +200 -0
  94. hackagent/cli/tui/app.py +288 -0
  95. hackagent/cli/tui/base.py +137 -0
  96. hackagent/cli/tui/logger.py +318 -0
  97. hackagent/cli/tui/views/__init__.py +33 -0
  98. hackagent/cli/tui/views/agents.py +488 -0
  99. hackagent/cli/tui/views/attacks.py +624 -0
  100. hackagent/cli/tui/views/config.py +244 -0
  101. hackagent/cli/tui/views/dashboard.py +307 -0
  102. hackagent/cli/tui/views/results.py +1210 -0
  103. hackagent/cli/tui/widgets/__init__.py +24 -0
  104. hackagent/cli/tui/widgets/actions.py +346 -0
  105. hackagent/cli/tui/widgets/logs.py +435 -0
  106. hackagent/cli/utils.py +276 -0
  107. hackagent/client.py +286 -0
  108. hackagent/errors.py +37 -0
  109. hackagent/logger.py +83 -0
  110. hackagent/models/__init__.py +109 -0
  111. hackagent/models/agent.py +223 -0
  112. hackagent/models/agent_request.py +129 -0
  113. hackagent/models/api_token_log.py +184 -0
  114. hackagent/models/attack.py +154 -0
  115. hackagent/models/attack_request.py +82 -0
  116. hackagent/models/checkout_session_request_request.py +76 -0
  117. hackagent/models/checkout_session_response.py +59 -0
  118. hackagent/models/choice.py +81 -0
  119. hackagent/models/choice_message.py +67 -0
  120. hackagent/models/evaluation_status_enum.py +14 -0
  121. hackagent/models/generate_error_response.py +59 -0
  122. hackagent/models/generate_request_request.py +212 -0
  123. hackagent/models/generate_success_response.py +115 -0
  124. hackagent/models/generic_error_response.py +70 -0
  125. hackagent/models/message_request.py +67 -0
  126. hackagent/models/organization.py +102 -0
  127. hackagent/models/organization_minimal.py +68 -0
  128. hackagent/models/organization_request.py +71 -0
  129. hackagent/models/paginated_agent_list.py +123 -0
  130. hackagent/models/paginated_api_token_log_list.py +123 -0
  131. hackagent/models/paginated_attack_list.py +123 -0
  132. hackagent/models/paginated_organization_list.py +123 -0
  133. hackagent/models/paginated_prompt_list.py +123 -0
  134. hackagent/models/paginated_result_list.py +123 -0
  135. hackagent/models/paginated_run_list.py +123 -0
  136. hackagent/models/paginated_user_api_key_list.py +123 -0
  137. hackagent/models/paginated_user_profile_list.py +123 -0
  138. hackagent/models/patched_agent_request.py +128 -0
  139. hackagent/models/patched_attack_request.py +92 -0
  140. hackagent/models/patched_organization_request.py +71 -0
  141. hackagent/models/patched_prompt_request.py +125 -0
  142. hackagent/models/patched_result_request.py +237 -0
  143. hackagent/models/patched_run_request.py +138 -0
  144. hackagent/models/patched_user_profile_request.py +99 -0
  145. hackagent/models/prompt.py +220 -0
  146. hackagent/models/prompt_request.py +126 -0
  147. hackagent/models/result.py +294 -0
  148. hackagent/models/result_list_evaluation_status.py +14 -0
  149. hackagent/models/result_request.py +232 -0
  150. hackagent/models/run.py +233 -0
  151. hackagent/models/run_list_status.py +12 -0
  152. hackagent/models/run_request.py +133 -0
  153. hackagent/models/status_enum.py +12 -0
  154. hackagent/models/step_type_enum.py +14 -0
  155. hackagent/models/trace.py +121 -0
  156. hackagent/models/trace_request.py +94 -0
  157. hackagent/models/usage.py +75 -0
  158. hackagent/models/user_api_key.py +201 -0
  159. hackagent/models/user_api_key_request.py +73 -0
  160. hackagent/models/user_profile.py +135 -0
  161. hackagent/models/user_profile_minimal.py +76 -0
  162. hackagent/models/user_profile_request.py +99 -0
  163. hackagent/router/__init__.py +25 -0
  164. hackagent/router/adapters/__init__.py +20 -0
  165. hackagent/router/adapters/base.py +63 -0
  166. hackagent/router/adapters/google_adk.py +671 -0
  167. hackagent/router/adapters/litellm_adapter.py +524 -0
  168. hackagent/router/adapters/openai_adapter.py +426 -0
  169. hackagent/router/router.py +969 -0
  170. hackagent/router/types.py +54 -0
  171. hackagent/tracking/__init__.py +42 -0
  172. hackagent/tracking/context.py +163 -0
  173. hackagent/tracking/decorators.py +299 -0
  174. hackagent/tracking/tracker.py +441 -0
  175. hackagent/types.py +54 -0
  176. hackagent/utils.py +194 -0
  177. hackagent/vulnerabilities/__init__.py +13 -0
  178. hackagent/vulnerabilities/prompts.py +81 -0
  179. hackagent-0.3.1.dist-info/METADATA +122 -0
  180. hackagent-0.3.1.dist-info/RECORD +183 -0
  181. hackagent-0.3.1.dist-info/WHEEL +4 -0
  182. hackagent-0.3.1.dist-info/entry_points.txt +2 -0
  183. hackagent-0.3.1.dist-info/licenses/LICENSE +202 -0
@@ -0,0 +1,624 @@
1
+ # Copyright 2025 - AI4I. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """
16
+ Attacks Tab
17
+
18
+ Execute and manage security attacks.
19
+ """
20
+
21
+ from typing import Optional
22
+
23
+ from textual.app import ComposeResult
24
+ from textual.binding import Binding
25
+ from textual.containers import Container, Horizontal, VerticalScroll
26
+ from textual.widgets import (
27
+ Button,
28
+ Input,
29
+ Label,
30
+ ProgressBar,
31
+ RichLog,
32
+ Select,
33
+ Static,
34
+ TabbedContent,
35
+ TabPane,
36
+ TextArea,
37
+ )
38
+
39
+ from hackagent.cli.config import CLIConfig
40
+ from hackagent.cli.tui.widgets.actions import AgentActionsViewer
41
+ from hackagent.cli.tui.widgets.logs import AttackLogViewer
42
+
43
+
44
+ class AttacksTab(Container):
45
+ """Attacks tab for executing security attacks."""
46
+
47
+ DEFAULT_CSS = """
48
+ AttacksTab {
49
+ layout: horizontal;
50
+ }
51
+
52
+ AttacksTab #attack-form-container {
53
+ width: 30%;
54
+ border-right: solid $primary;
55
+ padding: 1 2;
56
+ }
57
+
58
+ AttacksTab #attack-monitor-container {
59
+ width: 70%;
60
+ }
61
+ """
62
+
63
+ BINDINGS = [
64
+ Binding("e", "execute_attack", "Execute"),
65
+ Binding("c", "clear_form", "Clear Form"),
66
+ ]
67
+
68
+ def __init__(self, cli_config: CLIConfig, initial_data: Optional[dict] = None):
69
+ """Initialize attacks tab.
70
+
71
+ Args:
72
+ cli_config: CLI configuration object
73
+ initial_data: Initial data to pre-fill form fields
74
+ """
75
+ super().__init__()
76
+ self.cli_config = cli_config
77
+ self.initial_data = initial_data or {}
78
+
79
+ def compose(self) -> ComposeResult:
80
+ """Compose the attacks layout."""
81
+ # Split layout: Left side for form, Right side for logs
82
+ with Horizontal():
83
+ # Left side: Attack configuration form
84
+ with VerticalScroll(id="attack-form-container"):
85
+ yield Static("[bold cyan]Attack Configuration[/bold cyan]")
86
+ yield Static("") # Spacing
87
+
88
+ yield Label("Agent Name:")
89
+ yield Input(placeholder="e.g., weather-bot", id="agent-name")
90
+ yield Static("") # Spacing
91
+
92
+ yield Label("Agent Type:")
93
+ yield Select(
94
+ [
95
+ ("Google ADK", "google-adk"),
96
+ ("LiteLLM", "litellm"),
97
+ ("LangChain", "langchain"),
98
+ ("OpenAI SDK", "openai-sdk"),
99
+ ("MCP", "mcp"),
100
+ ("A2A", "a2a"),
101
+ ],
102
+ id="agent-type",
103
+ value="google-adk",
104
+ )
105
+ yield Static("") # Spacing
106
+
107
+ yield Label("Endpoint URL:")
108
+ yield Input(
109
+ placeholder="e.g., http://localhost:8000", id="endpoint-url"
110
+ )
111
+ yield Static("") # Spacing
112
+
113
+ yield Label("Attack Strategy:")
114
+ yield Select(
115
+ [("AdvPrefix", "advprefix")],
116
+ id="attack-strategy",
117
+ value="advprefix",
118
+ )
119
+ yield Static("") # Spacing
120
+
121
+ yield Label("Goals (what you want the agent to do incorrectly):")
122
+ goals_area = TextArea("Return fake weather data", id="attack-goals")
123
+ goals_area.styles.height = 6
124
+ yield goals_area
125
+ yield Static("") # Spacing
126
+
127
+ yield Label("Timeout (seconds):")
128
+ yield Input(value="300", id="timeout")
129
+ yield Static("") # Spacing
130
+ yield Static("") # Extra spacing before buttons
131
+
132
+ yield Button("Execute Attack", id="execute-attack", variant="primary")
133
+ yield Button("Dry Run", id="dry-run", variant="default")
134
+ yield Button("Clear", id="clear-form", variant="error")
135
+
136
+ yield Static("") # Spacing
137
+ yield Static("") # Extra spacing after buttons
138
+
139
+ yield Static(
140
+ "[dim]Configure attack parameters and click Execute[/dim]",
141
+ id="execution-status",
142
+ )
143
+ yield ProgressBar(total=100, show_eta=True, id="attack-progress")
144
+
145
+ # Right side: Tabbed monitor with logs and actions
146
+ with Container(id="attack-monitor-container"):
147
+ with TabbedContent():
148
+ with TabPane("📋 Logs", id="logs-tab"):
149
+ yield AttackLogViewer(
150
+ title="Attack Execution Logs",
151
+ show_controls=True,
152
+ max_lines=1000,
153
+ id="attack-log-viewer",
154
+ )
155
+ with TabPane("🔧 Actions", id="actions-tab"):
156
+ yield AgentActionsViewer(
157
+ title="Agent Actions Inspector",
158
+ show_controls=True,
159
+ id="attack-actions-viewer",
160
+ )
161
+
162
+ def on_mount(self) -> None:
163
+ """Called when the tab is mounted."""
164
+ # Pre-fill form with initial data if provided
165
+ if self.initial_data:
166
+ self._prefill_form()
167
+
168
+ # Add initial messages after a short delay to ensure widgets are ready
169
+ self.call_after_refresh(self._add_initial_messages)
170
+
171
+ def _add_initial_messages(self) -> None:
172
+ """Add initial welcome messages to the viewers."""
173
+ try:
174
+ log_viewer = self.query_one("#attack-log-viewer", AttackLogViewer)
175
+ actions_viewer = self.query_one(
176
+ "#attack-actions-viewer", AgentActionsViewer
177
+ )
178
+
179
+ # Get the RichLog directly to verify it exists
180
+ try:
181
+ rich_log = log_viewer.query_one("#attack-log-display", RichLog)
182
+ # Directly write to RichLog to test visibility
183
+ rich_log.write("[bold cyan]📋 Attack Log Viewer Ready[/bold cyan]")
184
+ rich_log.write(
185
+ "[yellow]Configure your attack and click Execute to begin[/yellow]"
186
+ )
187
+ except Exception:
188
+ pass
189
+
190
+ # Try actions viewer
191
+ try:
192
+ actions_log = actions_viewer.query_one("#actions-display", RichLog)
193
+ actions_log.write(
194
+ "[bold green]🔧 Agent Actions Inspector Ready[/bold green]"
195
+ )
196
+ actions_log.write(
197
+ "[dim]Agent actions will appear here during execution[/dim]"
198
+ )
199
+ except Exception:
200
+ pass
201
+
202
+ except Exception:
203
+ # If widgets aren't ready yet, skip initial messages
204
+ pass
205
+
206
+ def _prefill_form(self) -> None:
207
+ """Pre-fill form fields with initial data."""
208
+ if "agent_name" in self.initial_data:
209
+ self.query_one("#agent-name", Input).value = self.initial_data["agent_name"]
210
+ if "agent_type" in self.initial_data:
211
+ self.query_one("#agent-type", Select).value = self.initial_data[
212
+ "agent_type"
213
+ ]
214
+ if "endpoint" in self.initial_data:
215
+ self.query_one("#endpoint-url", Input).value = self.initial_data["endpoint"]
216
+ if "goals" in self.initial_data:
217
+ self.query_one("#attack-goals", TextArea).text = self.initial_data["goals"]
218
+ if "timeout" in self.initial_data:
219
+ self.query_one("#timeout", Input).value = str(self.initial_data["timeout"])
220
+
221
+ def on_button_pressed(self, event: Button.Pressed) -> None:
222
+ """Handle button press events."""
223
+ if event.button.id == "execute-attack":
224
+ self._execute_attack(dry_run=False)
225
+ elif event.button.id == "dry-run":
226
+ self._execute_attack(dry_run=True)
227
+ elif event.button.id == "clear-form":
228
+ self._clear_form()
229
+
230
+ def _execute_attack(self, dry_run: bool = False) -> None:
231
+ """Execute the configured attack.
232
+
233
+ Args:
234
+ dry_run: Whether to run in dry-run mode
235
+ """
236
+ # Get form values
237
+ from textual.widgets._select import NoSelection
238
+
239
+ agent_name = self.query_one("#agent-name", Input).value
240
+ agent_type_raw = self.query_one("#agent-type", Select).value
241
+ endpoint = self.query_one("#endpoint-url", Input).value
242
+ strategy_raw = self.query_one("#attack-strategy", Select).value
243
+ goals = self.query_one("#attack-goals", TextArea).text
244
+ timeout = self.query_one("#timeout", Input).value
245
+
246
+ # Validate inputs
247
+ if not agent_name:
248
+ return
249
+ if isinstance(agent_type_raw, NoSelection) or not agent_type_raw:
250
+ return
251
+ if not endpoint:
252
+ return
253
+ if isinstance(strategy_raw, NoSelection) or not strategy_raw:
254
+ return
255
+ if not goals:
256
+ return
257
+
258
+ # Validate timeout is a valid integer
259
+ try:
260
+ timeout_int = int(timeout)
261
+ if timeout_int <= 0:
262
+ return
263
+ except ValueError:
264
+ return
265
+
266
+ # Convert to strings (they should be strings after validation)
267
+ agent_type = str(agent_type_raw)
268
+ strategy = str(strategy_raw)
269
+
270
+ status_widget = self.query_one("#execution-status", Static)
271
+ progress_bar = self.query_one("#attack-progress", ProgressBar)
272
+
273
+ if dry_run:
274
+ status_widget.update(
275
+ f"""[bold yellow]Dry Run Mode[/bold yellow]
276
+
277
+ [bold]Agent:[/bold] {agent_name}
278
+ [bold]Type:[/bold] {agent_type}
279
+ [bold]Endpoint:[/bold] {endpoint}
280
+ [bold]Strategy:[/bold] {strategy}
281
+ [bold]Goals:[/bold] {goals}
282
+ [bold]Timeout:[/bold] {timeout}s
283
+
284
+ [green]✅ Configuration validation passed[/green]
285
+ [dim]Remove dry-run flag to execute the attack[/dim]"""
286
+ )
287
+ else:
288
+ # Actually execute the attack
289
+ status_widget.update(
290
+ f"""[bold cyan]🚀 Initializing Attack...[/bold cyan]
291
+
292
+ [bold]Agent:[/bold] {agent_name}
293
+ [bold]Type:[/bold] {agent_type}
294
+ [bold]Endpoint:[/bold] {endpoint}
295
+ [bold]Strategy:[/bold] {strategy}
296
+ [bold]Goals:[/bold] {goals}
297
+ [bold]Timeout:[/bold] {timeout}s
298
+
299
+ [yellow]⏳ Connecting to agent and preparing attack...[/yellow]"""
300
+ )
301
+
302
+ # Show immediate feedback - progress starting
303
+ progress_bar.update(progress=5)
304
+ status_widget.update(
305
+ f"""[bold cyan]🚀 Starting Attack...[/bold cyan]
306
+
307
+ [bold]Agent:[/bold] {agent_name}
308
+ [bold]Type:[/bold] {agent_type}
309
+ [bold]Endpoint:[/bold] {endpoint}
310
+ [bold]Strategy:[/bold] {strategy}
311
+ [bold]Goals:[/bold] {goals}
312
+
313
+ [yellow]⏳ Launching attack execution...[/yellow]
314
+ [dim]Progress: 5%[/dim]"""
315
+ )
316
+
317
+ # Run attack in background thread
318
+ # Use lambda to pass arguments to the worker function
319
+ try:
320
+ self.run_worker(
321
+ lambda: self._run_attack_async(
322
+ agent_name, agent_type, endpoint, goals, int(timeout)
323
+ ),
324
+ thread=True,
325
+ exclusive=True,
326
+ name="attack-execution",
327
+ )
328
+ except Exception as e:
329
+ # If worker fails to start, show error immediately
330
+ status_widget.update(
331
+ f"""[bold red]❌ Failed to Start Attack[/bold red]
332
+
333
+ [bold]Error:[/bold] {str(e)}
334
+
335
+ [red]Could not start attack worker thread.[/red]
336
+ [dim]This might be a configuration or system issue.[/dim]"""
337
+ )
338
+
339
+ def _run_attack_async(
340
+ self, agent_name: str, agent_type: str, endpoint: str, goals: str, timeout: int
341
+ ) -> None:
342
+ """Run attack in background thread with progress updates.
343
+
344
+ Args:
345
+ agent_name: Name of the target agent
346
+ agent_type: Type of agent (google-adk, litellm)
347
+ endpoint: Agent endpoint URL
348
+ goals: Attack goals
349
+ timeout: Timeout in seconds
350
+ """
351
+ import io
352
+ import logging
353
+ import os
354
+ import sys
355
+ import time
356
+
357
+ from hackagent import HackAgent
358
+ from hackagent.cli.utils import get_agent_type_enum
359
+
360
+ status_widget = self.query_one("#execution-status", Static)
361
+ progress_bar = self.query_one("#attack-progress", ProgressBar)
362
+ log_viewer = self.query_one("#attack-log-viewer", AttackLogViewer)
363
+ actions_viewer = self.query_one("#attack-actions-viewer", AgentActionsViewer)
364
+
365
+ # Clear previous logs and actions
366
+ self.app.call_from_thread(log_viewer.clear_logs)
367
+ self.app.call_from_thread(actions_viewer.clear_actions)
368
+ self.app.call_from_thread(
369
+ log_viewer.add_log,
370
+ f"🚀 Starting attack execution for agent: {agent_name}",
371
+ "INFO",
372
+ )
373
+ self.app.call_from_thread(
374
+ actions_viewer.add_step_separator,
375
+ f"Attack Initialization: {agent_name}",
376
+ 1,
377
+ )
378
+
379
+ # CRITICAL: Comprehensive rich suppression to prevent black screen
380
+ # Multiple layers of defense to prevent ANY rich output during TUI mode
381
+
382
+ # 1. Set environment variable to disable rich features
383
+ saved_term = os.environ.get("TERM")
384
+ os.environ["TERM"] = "dumb" # Disable rich color/formatting
385
+
386
+ # 2. Set up custom logging handlers for TUI
387
+ hackagent_logger = logging.getLogger("hackagent")
388
+ saved_handlers = hackagent_logger.handlers.copy()
389
+ saved_level = hackagent_logger.level
390
+
391
+ # Remove existing handlers
392
+ for handler in hackagent_logger.handlers[:]:
393
+ hackagent_logger.removeHandler(handler)
394
+
395
+ # Add TUI-specific handlers
396
+ from hackagent.cli.tui.logger import TUILogHandler
397
+
398
+ # Handler for log messages
399
+ tui_log_handler = TUILogHandler(
400
+ app=self.app,
401
+ callback=log_viewer.add_log,
402
+ level=logging.INFO,
403
+ )
404
+ hackagent_logger.addHandler(tui_log_handler)
405
+ hackagent_logger.setLevel(logging.INFO)
406
+
407
+ # Suppress other noisy loggers
408
+ logging.getLogger("httpx").setLevel(logging.CRITICAL)
409
+ logging.getLogger("litellm").setLevel(logging.CRITICAL)
410
+
411
+ # 3. Disable Rich progress bars by setting environment variable
412
+ # This prevents Rich from trying to use terminal features that conflict with TUI
413
+ os.environ["FORCE_COLOR"] = "0"
414
+ os.environ["NO_COLOR"] = "1"
415
+
416
+ # 4. Redirect stdout/stderr as final safeguard
417
+ original_stdout = sys.stdout
418
+ original_stderr = sys.stderr
419
+ sys.stdout = io.StringIO()
420
+ sys.stderr = io.StringIO()
421
+
422
+ try:
423
+ # Convert agent type
424
+ agent_type_enum = get_agent_type_enum(agent_type)
425
+
426
+ # Update status - 10% progress
427
+ self.app.call_from_thread(progress_bar.update, progress=10)
428
+ self.app.call_from_thread(
429
+ status_widget.update,
430
+ f"""[bold cyan]🔧 Initializing HackAgent...[/bold cyan]
431
+
432
+ [bold]Agent:[/bold] {agent_name}
433
+ [bold]Type:[/bold] {agent_type}
434
+ [bold]Endpoint:[/bold] {endpoint}
435
+
436
+ [yellow]⏳ Setting up attack infrastructure...[/yellow]
437
+ [dim]Progress: 10%[/dim]""",
438
+ )
439
+ # Initialize HackAgent - 20% progress
440
+ self.app.call_from_thread(progress_bar.update, progress=20)
441
+
442
+ agent = HackAgent(
443
+ name=agent_name,
444
+ endpoint=endpoint,
445
+ agent_type=agent_type_enum,
446
+ api_key=self.cli_config.api_key,
447
+ base_url=self.cli_config.base_url,
448
+ timeout=5.0, # 5 second timeout for API calls
449
+ )
450
+
451
+ # Build attack configuration - 30% progress
452
+ self.app.call_from_thread(progress_bar.update, progress=30)
453
+ attack_config = {
454
+ "attack_type": "advprefix",
455
+ "goals": [goals],
456
+ }
457
+
458
+ # Update status - 40% progress, starting attack
459
+ self.app.call_from_thread(progress_bar.update, progress=40)
460
+ self.app.call_from_thread(
461
+ status_widget.update,
462
+ f"""[bold cyan]⚔️ Executing AdvPrefix Attack...[/bold cyan]
463
+
464
+ [bold]Agent:[/bold] {agent_name}
465
+ [bold]Goals:[/bold] {goals}
466
+
467
+ [yellow]⏳ Attack in progress... This may take several minutes...[/yellow]
468
+ [dim]Generating adversarial prefixes and testing against target agent...[/dim]
469
+ [dim]Progress: 40%[/dim]""",
470
+ )
471
+
472
+ start_time = time.time()
473
+
474
+ # Set up TUI logging callback
475
+ def log_callback(message: str, level: str) -> None:
476
+ """Callback for TUI log handler"""
477
+ log_viewer.add_log(message, level)
478
+
479
+ # Create and attach TUI log handler to the attack
480
+ # This will be picked up by the @with_tui_logging decorator
481
+
482
+ # Execute attack - simulate progress from 50% to 90%
483
+ # Start a background thread to update progress
484
+ import threading
485
+
486
+ stop_progress = threading.Event()
487
+
488
+ def update_progress_gradually():
489
+ """Gradually update progress during attack execution"""
490
+ for progress in range(50, 91, 5):
491
+ if stop_progress.is_set():
492
+ break
493
+ self.app.call_from_thread(progress_bar.update, progress=progress)
494
+ time.sleep(2) # Update every 2 seconds
495
+
496
+ progress_thread = threading.Thread(
497
+ target=update_progress_gradually, daemon=True
498
+ )
499
+ progress_thread.start()
500
+
501
+ # Attach TUI log handler before execution
502
+ # The attack strategy will internally call the attack class's run() method
503
+ # which has the @with_tui_logging decorator
504
+ try:
505
+ # Note: We need to access the attack instance to attach the handler
506
+ # This will be done by modifying the agent.hack() flow or by
507
+ # directly accessing the attack strategy instance
508
+
509
+ # For now, we'll execute the attack and the decorator will handle logging
510
+ # The handler attachment needs to happen in the strategy execution
511
+ results = agent.hack(
512
+ attack_config=attack_config,
513
+ run_config_override={"timeout": timeout},
514
+ fail_on_run_error=True,
515
+ # Pass TUI context for logging
516
+ _tui_app=self.app,
517
+ _tui_log_callback=log_callback,
518
+ )
519
+ finally:
520
+ stop_progress.set()
521
+ progress_thread.join(timeout=1)
522
+ # Restore stdout/stderr
523
+ sys.stdout = original_stdout
524
+ sys.stderr = original_stderr
525
+
526
+ # Remove TUI handler from all loggers
527
+ if tui_log_handler in hackagent_logger.handlers:
528
+ hackagent_logger.removeHandler(tui_log_handler)
529
+
530
+ # Restore logging configuration
531
+ hackagent_logger.setLevel(saved_level)
532
+ for handler in saved_handlers:
533
+ hackagent_logger.addHandler(handler)
534
+
535
+ # Restore environment variables
536
+ if saved_term is not None:
537
+ os.environ["TERM"] = saved_term
538
+ elif "TERM" in os.environ:
539
+ del os.environ["TERM"]
540
+
541
+ if "FORCE_COLOR" in os.environ:
542
+ del os.environ["FORCE_COLOR"]
543
+ if "NO_COLOR" in os.environ:
544
+ del os.environ["NO_COLOR"]
545
+
546
+ duration = time.time() - start_time
547
+
548
+ # Complete progress - 100%
549
+ self.app.call_from_thread(progress_bar.update, progress=100)
550
+
551
+ # Display success
552
+ result_count = len(results) if hasattr(results, "__len__") else "Unknown"
553
+ self.app.call_from_thread(
554
+ status_widget.update,
555
+ f"""[bold green]✅ Attack Completed Successfully![/bold green]
556
+
557
+ [bold]Agent:[/bold] {agent_name}
558
+ [bold]Duration:[/bold] {duration:.1f} seconds
559
+ [bold]Results Generated:[/bold] {result_count}
560
+
561
+ [green]Attack execution finished![/green]
562
+ [dim]Check the Results tab to view detailed attack results.[/dim]
563
+ [dim]Results have been saved to the HackAgent platform.[/dim]""",
564
+ )
565
+
566
+ except Exception as e:
567
+ # Display error
568
+ self.app.call_from_thread(progress_bar.update, progress=0)
569
+ self.app.call_from_thread(
570
+ status_widget.update,
571
+ f"""[bold red]❌ Attack Failed[/bold red]
572
+
573
+ [bold]Agent:[/bold] {agent_name}
574
+ [bold]Error:[/bold] {str(e)}
575
+
576
+ [red]Attack execution encountered an error.[/red]
577
+ [dim]Please check your configuration and try again.[/dim]
578
+ [dim]Ensure the agent endpoint is accessible and API key is valid.[/dim]""",
579
+ )
580
+
581
+ finally:
582
+ # Always restore stdout/stderr and clean up handlers
583
+ sys.stdout = original_stdout
584
+ sys.stderr = original_stderr
585
+
586
+ # Remove TUI handler from all loggers
587
+ try:
588
+ if tui_log_handler in hackagent_logger.handlers:
589
+ hackagent_logger.removeHandler(tui_log_handler)
590
+ except Exception:
591
+ pass # Handler cleanup errors shouldn't fail silently
592
+
593
+ # Restore logging configuration
594
+ hackagent_logger.setLevel(saved_level)
595
+ for handler in saved_handlers:
596
+ hackagent_logger.addHandler(handler)
597
+
598
+ # Restore environment variables
599
+ if saved_term is not None:
600
+ os.environ["TERM"] = saved_term
601
+ elif "TERM" in os.environ:
602
+ del os.environ["TERM"]
603
+
604
+ if "FORCE_COLOR" in os.environ:
605
+ del os.environ["FORCE_COLOR"]
606
+ if "NO_COLOR" in os.environ:
607
+ del os.environ["NO_COLOR"]
608
+
609
+ def _clear_form(self) -> None:
610
+ """Clear all form fields."""
611
+ self.query_one("#agent-name", Input).value = ""
612
+ self.query_one("#endpoint-url", Input).value = ""
613
+ self.query_one("#attack-goals", TextArea).text = "Return fake weather data"
614
+ self.query_one("#timeout", Input).value = "300"
615
+
616
+ status_widget = self.query_one("#execution-status", Static)
617
+ progress_bar = self.query_one("#attack-progress", ProgressBar)
618
+ status_widget.update("[dim]Configure attack parameters and click Execute[/dim]")
619
+ progress_bar.update(progress=0)
620
+
621
+ def refresh_data(self) -> None:
622
+ """Refresh attacks data."""
623
+ # No dynamic data to refresh for attacks list
624
+ pass