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,244 @@
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
+ Config Tab
17
+
18
+ Manage HackAgent configuration settings.
19
+ """
20
+
21
+ from textual.app import ComposeResult
22
+ from textual.binding import Binding
23
+ from textual.containers import Horizontal, Vertical, VerticalScroll
24
+ from textual.widgets import Button, Input, Label, Select, Static
25
+
26
+ from hackagent.cli.config import CLIConfig
27
+
28
+
29
+ class ConfigTab(VerticalScroll):
30
+ """Config tab for managing settings with vertical scrolling."""
31
+
32
+ DEFAULT_CSS = ""
33
+
34
+ BINDINGS = [
35
+ Binding("s", "save_config", "Save"),
36
+ Binding("t", "test_connection", "Test Connection"),
37
+ Binding("r", "reset_config", "Reset"),
38
+ ]
39
+
40
+ def __init__(self, cli_config: CLIConfig):
41
+ """Initialize config tab.
42
+
43
+ Args:
44
+ cli_config: CLI configuration object
45
+ """
46
+ super().__init__()
47
+ self.cli_config = cli_config
48
+
49
+ def compose(self) -> ComposeResult:
50
+ """Compose the config layout."""
51
+ yield Static(
52
+ "[bold cyan]HackAgent Configuration[/bold cyan]",
53
+ classes="config-section",
54
+ )
55
+
56
+ with Vertical(classes="config-section"):
57
+ yield Static("[bold]API Configuration[/bold]")
58
+
59
+ with Vertical(classes="form-group"):
60
+ yield Label("API Key:")
61
+ yield Input(
62
+ placeholder="Your HackAgent API key",
63
+ id="api-key",
64
+ password=True,
65
+ )
66
+
67
+ with Vertical(classes="form-group"):
68
+ yield Label("Base URL:")
69
+ yield Input(
70
+ id="base_url",
71
+ placeholder="https://api.hackagent.dev",
72
+ classes="config-input",
73
+ )
74
+
75
+ with Vertical(classes="form-group"):
76
+ yield Label("Output Format:")
77
+ yield Select(
78
+ [("Table", "table"), ("JSON", "json"), ("CSV", "csv")],
79
+ id="output-format",
80
+ value=self.cli_config.output_format,
81
+ )
82
+
83
+ with Vertical(classes="config-section"):
84
+ yield Static("[bold]Configuration File[/bold]")
85
+
86
+ yield Static(
87
+ f"[dim]Location:[/dim] {self.cli_config.default_config_path}",
88
+ classes="info-box",
89
+ id="config-file-location",
90
+ )
91
+
92
+ yield Static(
93
+ "[dim]Status: Checking...[/dim]",
94
+ classes="status-indicator",
95
+ id="config-status",
96
+ )
97
+
98
+ with Horizontal(classes="button-group"):
99
+ yield Button("Save Configuration", id="save-config", variant="primary")
100
+ yield Button("Test Connection", id="test-connection", variant="default")
101
+ yield Button("Reset to Defaults", id="reset-config", variant="error")
102
+ yield Button("Validate Config", id="validate-config", variant="success")
103
+
104
+ with Vertical(classes="config-section"):
105
+ yield Static("[bold]System Information[/bold]")
106
+
107
+ yield Static(
108
+ f"""[dim]Python Version:[/dim] {self._get_python_version()}
109
+ [dim]CLI Version:[/dim] 0.2.5
110
+ [dim]Dependencies:[/dim] {self._check_dependencies()}""",
111
+ classes="info-box",
112
+ id="system-info",
113
+ )
114
+
115
+ def on_mount(self) -> None:
116
+ """Called when the tab is mounted."""
117
+ self._load_config()
118
+ self._update_status()
119
+
120
+ def on_button_pressed(self, event: Button.Pressed) -> None:
121
+ """Handle button press events."""
122
+ if event.button.id == "save-config":
123
+ self._save_config()
124
+ elif event.button.id == "test-connection":
125
+ self._test_connection()
126
+ elif event.button.id == "reset-config":
127
+ self._reset_config()
128
+ elif event.button.id == "validate-config":
129
+ self._validate_config()
130
+
131
+ def _load_config(self) -> None:
132
+ """Load current configuration into form fields."""
133
+ # Set API key (masked)
134
+ if self.cli_config.api_key:
135
+ self.query_one("#api-key", Input).value = self.cli_config.api_key
136
+
137
+ # Set base URL
138
+ self.query_one("#base_url", Input).value = self.cli_config.base_url
139
+
140
+ # Set output format
141
+ self.query_one("#output-format", Select).value = self.cli_config.output_format
142
+
143
+ def _update_status(self) -> None:
144
+ """Update configuration status display."""
145
+ status_widget = self.query_one("#config-status", Static)
146
+
147
+ if self.cli_config.default_config_path.exists():
148
+ status_widget.update("[green]✅ Configuration file exists[/green]")
149
+ else:
150
+ status_widget.update(
151
+ "[yellow]⚠️ No configuration file found. Save to create one.[/yellow]"
152
+ )
153
+
154
+ def _save_config(self) -> None:
155
+ """Save configuration to file."""
156
+ try:
157
+ # Get values from form
158
+ api_key = self.query_one("#api-key", Input).value
159
+ base_url = self.query_one("#base_url", Input).value
160
+ output_format = self.query_one("#output-format", Select).value
161
+
162
+ # Update config
163
+ if api_key:
164
+ self.cli_config.api_key = api_key
165
+ if base_url:
166
+ self.cli_config.base_url = base_url
167
+ self.cli_config.output_format = output_format
168
+
169
+ # Save to file
170
+ self.cli_config.save()
171
+
172
+ self._update_status()
173
+
174
+ except Exception:
175
+ pass
176
+
177
+ def _test_connection(self) -> None:
178
+ """Test API connection."""
179
+ try:
180
+ from hackagent.api.key import key_list
181
+ from hackagent.client import AuthenticatedClient
182
+
183
+ if not self.cli_config.api_key:
184
+ return
185
+
186
+ client = AuthenticatedClient(
187
+ base_url=self.cli_config.base_url,
188
+ token=self.cli_config.api_key,
189
+ prefix="Bearer",
190
+ )
191
+
192
+ key_list.sync_detailed(client=client)
193
+
194
+ except Exception:
195
+ pass
196
+
197
+ def _validate_config(self) -> None:
198
+ """Validate current configuration."""
199
+ try:
200
+ self.cli_config.validate()
201
+ except ValueError:
202
+ pass
203
+
204
+ def _reset_config(self) -> None:
205
+ """Reset configuration to defaults."""
206
+ try:
207
+ if self.cli_config.default_config_path.exists():
208
+ self.cli_config.default_config_path.unlink()
209
+
210
+ # Reset to defaults
211
+ self.cli_config.base_url = "https://api.hackagent.dev"
212
+ self.cli_config.output_format = "table"
213
+ self.cli_config.api_key = None
214
+
215
+ self._load_config()
216
+ self._update_status()
217
+
218
+ except Exception:
219
+ pass
220
+
221
+ def _get_python_version(self) -> str:
222
+ """Get Python version string."""
223
+ import sys
224
+
225
+ return f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
226
+
227
+ def _check_dependencies(self) -> str:
228
+ """Check if required dependencies are installed."""
229
+ import importlib.util
230
+
231
+ required_packages = ["pandas", "yaml", "litellm", "textual"]
232
+ missing = [
233
+ pkg for pkg in required_packages if importlib.util.find_spec(pkg) is None
234
+ ]
235
+
236
+ if not missing:
237
+ return "[green]✅ All dependencies installed[/green]"
238
+ else:
239
+ return f"[yellow]⚠️ Some dependencies missing: {', '.join(missing)}[/yellow]"
240
+
241
+ def refresh_data(self) -> None:
242
+ """Refresh config data."""
243
+ self._load_config()
244
+ self._update_status()
@@ -0,0 +1,307 @@
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
+ Dashboard Tab
17
+
18
+ Overview and statistics for HackAgent.
19
+ """
20
+
21
+ from textual.app import ComposeResult
22
+ from textual.containers import Horizontal, Vertical, VerticalScroll
23
+ from textual.widgets import Static
24
+
25
+ from hackagent.cli.config import CLIConfig
26
+ from hackagent.cli.tui.base import BaseTab
27
+
28
+
29
+ class DashboardTab(BaseTab):
30
+ """Dashboard tab showing overview and statistics."""
31
+
32
+ DEFAULT_CSS = ""
33
+
34
+ def __init__(self, cli_config: CLIConfig):
35
+ """Initialize dashboard tab.
36
+
37
+ Args:
38
+ cli_config: CLI configuration object
39
+ """
40
+ super().__init__(cli_config)
41
+ self.stats = {
42
+ "agents": 0,
43
+ "attacks": 0,
44
+ "results": 0,
45
+ "success_rate": 0.0,
46
+ }
47
+
48
+ def compose(self) -> ComposeResult:
49
+ """Compose the dashboard layout."""
50
+ # Title section
51
+ yield Static(
52
+ "[bold cyan]━━━ Dashboard Overview ━━━[/bold cyan]", id="dashboard-title"
53
+ )
54
+
55
+ # Statistics section with better formatting
56
+ yield Static("[bold yellow]📊 Statistics[/bold yellow]", id="stats-header")
57
+
58
+ with Horizontal():
59
+ with Vertical():
60
+ yield Static("🤖 [bold]Agents[/bold]\n[cyan]0[/cyan]", id="stat-agents")
61
+ yield Static(
62
+ "⚔️ [bold]Attacks[/bold]\n[green]0[/green]", id="stat-attacks"
63
+ )
64
+ with Vertical():
65
+ yield Static(
66
+ "📋 [bold]Results[/bold]\n[yellow]0[/yellow]", id="stat-results"
67
+ )
68
+ yield Static(
69
+ "✓ [bold]Success Rate[/bold]\n[magenta]0%[/magenta]",
70
+ id="stat-success",
71
+ )
72
+
73
+ # Activity section
74
+ yield Static(
75
+ "\n[bold yellow]📝 Recent Activity[/bold yellow]", id="activity-header"
76
+ )
77
+
78
+ with VerticalScroll():
79
+ yield Static("[dim]Waiting for data...[/dim]", id="activity-log")
80
+
81
+ def on_mount(self) -> None:
82
+ """Called when the tab is mounted."""
83
+ # Call base class mount to handle initial refresh
84
+ super().on_mount()
85
+
86
+ # Enable auto-refresh every 5 seconds
87
+ self.enable_auto_refresh(interval=5.0)
88
+
89
+ def refresh_data(self) -> None:
90
+ """Refresh dashboard data from API."""
91
+ try:
92
+ from hackagent.api.agent import agent_list
93
+ from hackagent.api.result import result_list
94
+
95
+ # Validate configuration
96
+ if not self.cli_config.api_key:
97
+ activity_log = self.query_one("#activity-log", Static)
98
+ activity_log.update(
99
+ "[red]API key not configured[/red]\n\n"
100
+ "[yellow]Run 'hackagent init' to set up your API key[/yellow]\n\n"
101
+ "[dim]You need to configure your HackAgent API key before you can use the TUI.[/dim]"
102
+ )
103
+ return
104
+
105
+ # Create API client using base class method
106
+ client = self.create_api_client()
107
+
108
+ agents_data = []
109
+ results_data = []
110
+
111
+ # Fetch agents count
112
+ agents_response = agent_list.sync_detailed(client=client)
113
+ if agents_response.status_code == 200 and agents_response.parsed:
114
+ agents_data = (
115
+ agents_response.parsed.results
116
+ if agents_response.parsed.results
117
+ else []
118
+ )
119
+ self.stats["agents"] = len(agents_data)
120
+ elif agents_response.status_code == 401:
121
+ activity_log = self.query_one("#activity-log", Static)
122
+ activity_log.update(
123
+ "[red]Authentication failed[/red]\n\n"
124
+ "[yellow]Your API key is invalid or expired[/yellow]\n\n"
125
+ "[dim]Run 'hackagent config set --api-key YOUR_KEY' to update[/dim]"
126
+ )
127
+ return
128
+ elif agents_response.status_code == 403:
129
+ activity_log = self.query_one("#activity-log", Static)
130
+ activity_log.update(
131
+ "[red]Access forbidden[/red]\n\n"
132
+ "[yellow]Your API key doesn't have permission to access this resource[/yellow]"
133
+ )
134
+ return
135
+
136
+ # Fetch results count
137
+ results_response = result_list.sync_detailed(client=client)
138
+ if results_response.status_code == 200 and results_response.parsed:
139
+ results_data = (
140
+ results_response.parsed.results
141
+ if results_response.parsed.results
142
+ else []
143
+ )
144
+ self.stats["results"] = len(results_data)
145
+
146
+ # Calculate success rate
147
+ if results_data:
148
+ completed = sum(
149
+ 1
150
+ for r in results_data
151
+ if hasattr(r, "evaluation_status")
152
+ and str(
153
+ r.evaluation_status.value
154
+ if hasattr(r.evaluation_status, "value")
155
+ else r.evaluation_status
156
+ ).upper()
157
+ == "COMPLETED"
158
+ )
159
+ self.stats["success_rate"] = (
160
+ (completed / len(results_data)) * 100
161
+ if len(results_data) > 0
162
+ else 0
163
+ )
164
+
165
+ # Update stat cards
166
+ self._update_stat_cards()
167
+
168
+ # Update activity log
169
+ if not agents_data and not results_data:
170
+ activity_log = self.query_one("#activity-log", Static)
171
+ activity_log.update(
172
+ "[yellow]No data found[/yellow]\n\n"
173
+ "[dim]Create agents and run attacks to see activity here.[/dim]\n\n"
174
+ "[cyan]Quick Start:[/cyan]\n"
175
+ "1. Go to Agents tab to create an agent\n"
176
+ "2. Go to Attacks tab to run security tests\n"
177
+ "3. Check Results tab to see outcomes"
178
+ )
179
+ else:
180
+ self._update_activity_log(agents_data, results_data)
181
+
182
+ except Exception as e:
183
+ # Display error in activity log with helpful context
184
+ activity_log = self.query_one("#activity-log", Static)
185
+
186
+ error_type = type(e).__name__
187
+ error_msg = str(e)
188
+
189
+ # Provide context-specific help
190
+ if "timeout" in error_msg.lower() or "TimeoutException" in error_type:
191
+ activity_log.update(
192
+ f"[red]⚠️ Connection Timeout[/red]\n\n"
193
+ f"[yellow]Cannot reach HackAgent API:[/yellow]\n{self.cli_config.base_url}\n\n"
194
+ f"[cyan]Possible causes:[/cyan]\n"
195
+ f"• API server is down or unreachable\n"
196
+ f"• Network connection issues\n"
197
+ f"• Firewall blocking the connection\n\n"
198
+ f"[dim]Press F5 to retry when connection is restored[/dim]\n\n"
199
+ f"[bold]Offline Mode:[/bold]\n"
200
+ f"You can still use the Attacks tab with local agents\n"
201
+ f"(results won't be synced to the platform)"
202
+ )
203
+ elif "401" in error_msg or "authentication" in error_msg.lower():
204
+ activity_log.update(
205
+ "[red]Authentication Failed[/red]\n\n"
206
+ "[yellow]Your API key is invalid or expired[/yellow]\n\n"
207
+ "[cyan]To fix:[/cyan]\n"
208
+ "Run: hackagent config set --api-key YOUR_KEY\n\n"
209
+ "[dim]Press F5 to retry after updating[/dim]"
210
+ )
211
+ else:
212
+ activity_log.update(
213
+ f"[red]Error loading data:[/red]\n{error_type}\n\n"
214
+ f"[yellow]Details:[/yellow]\n{error_msg}\n\n"
215
+ f"[dim]Press F5 to retry[/dim]"
216
+ )
217
+
218
+ def _update_stat_cards(self) -> None:
219
+ """Update the statistics cards with current data."""
220
+ try:
221
+ # Get the values
222
+ agents_val = self.stats.get("agents", 0)
223
+ attacks_val = self.stats.get("attacks", 0)
224
+ results_val = self.stats.get("results", 0)
225
+ success_val = self.stats.get("success_rate", 0)
226
+
227
+ # Update each stat widget by ID with icons and formatting
228
+ stat_agents = self.query_one("#stat-agents", Static)
229
+ stat_agents.update(f"🤖 [bold]Agents[/bold]\n[cyan]{agents_val}[/cyan]")
230
+
231
+ stat_attacks = self.query_one("#stat-attacks", Static)
232
+ stat_attacks.update(
233
+ f"⚔️ [bold]Attacks[/bold]\n[green]{attacks_val}[/green]"
234
+ )
235
+
236
+ stat_results = self.query_one("#stat-results", Static)
237
+ stat_results.update(
238
+ f"📋 [bold]Results[/bold]\n[yellow]{results_val}[/yellow]"
239
+ )
240
+
241
+ stat_success = self.query_one("#stat-success", Static)
242
+ stat_success.update(
243
+ f"✓ [bold]Success Rate[/bold]\n[magenta]{success_val:.1f}%[/magenta]"
244
+ )
245
+
246
+ except Exception as e:
247
+ # Show error in activity log if update fails
248
+ try:
249
+ activity_log = self.query_one("#activity-log", Static)
250
+ activity_log.update(f"[red]Error updating stats: {str(e)}[/red]")
251
+ except Exception:
252
+ pass
253
+
254
+ def _update_activity_log(self, agents: list, results: list) -> None:
255
+ """Update activity log with recent items.
256
+
257
+ Args:
258
+ agents: List of agents
259
+ results: List of results
260
+ """
261
+ activity_log = self.query_one("#activity-log", Static)
262
+ log_lines = []
263
+
264
+ # Add recent agents with icons
265
+ if agents:
266
+ log_lines.append("[bold cyan]🤖 Recent Agents:[/bold cyan]")
267
+ for i, agent in enumerate(agents[:3], 1):
268
+ agent_type = (
269
+ agent.agent_type.value
270
+ if hasattr(agent.agent_type, "value")
271
+ else agent.agent_type
272
+ )
273
+ log_lines.append(
274
+ f" {i}. [cyan]{agent.name or 'Unnamed'}[/cyan] [dim]({agent_type})[/dim]"
275
+ )
276
+ log_lines.append("")
277
+
278
+ # Add recent results with status colors
279
+ if results:
280
+ log_lines.append("[bold green]📋 Recent Results:[/bold green]")
281
+ for i, result in enumerate(results[:5], 1):
282
+ status = "Unknown"
283
+ status_color = "dim"
284
+
285
+ if hasattr(result, "evaluation_status"):
286
+ status = (
287
+ result.evaluation_status.value
288
+ if hasattr(result.evaluation_status, "value")
289
+ else str(result.evaluation_status)
290
+ )
291
+ # Color code based on status
292
+ if status.upper() == "COMPLETED":
293
+ status_color = "green"
294
+ elif status.upper() == "RUNNING":
295
+ status_color = "yellow"
296
+ elif status.upper() == "FAILED":
297
+ status_color = "red"
298
+
299
+ attack_type = getattr(result, "attack_type", "Unknown")
300
+ log_lines.append(
301
+ f" {i}. [yellow]{attack_type}[/yellow] → [{status_color}]{status}[/{status_color}]"
302
+ )
303
+
304
+ if not log_lines:
305
+ log_lines = ["[dim]No recent activity yet...[/dim]"]
306
+
307
+ activity_log.update("\n".join(log_lines))