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,200 @@
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
+ TUI Actions Logger
17
+
18
+ Custom logging handler that extracts and displays agent actions (tool calls, HTTP requests)
19
+ in the TUI actions viewer.
20
+ """
21
+
22
+ import logging
23
+ import re
24
+ from typing import Any, Callable
25
+
26
+
27
+ class TUIActionsHandler(logging.Handler):
28
+ """
29
+ Custom logging handler that extracts agent actions from log messages
30
+ and displays them in the TUI actions viewer.
31
+
32
+ This handler parses log messages to identify:
33
+ - HTTP requests to agent endpoints
34
+ - Tool/function calls with arguments
35
+ - ADK agent events (tool_call, tool_result, llm_response)
36
+ - API responses
37
+ """
38
+
39
+ def __init__(
40
+ self,
41
+ actions_callback: Callable,
42
+ app_callback: Callable,
43
+ level: int = logging.INFO,
44
+ ):
45
+ """
46
+ Initialize the actions handler.
47
+
48
+ Args:
49
+ actions_callback: Callback function to add actions to the viewer
50
+ Signature: (action_type: str, **kwargs)
51
+ app_callback: Callback to call actions from thread-safe context
52
+ level: Logging level
53
+ """
54
+ super().__init__(level)
55
+ self.actions_callback = actions_callback
56
+ self.app_callback = app_callback
57
+
58
+ def emit(self, record: logging.LogRecord) -> None:
59
+ """
60
+ Process a log record and extract action information.
61
+
62
+ Args:
63
+ record: The log record to process
64
+ """
65
+ try:
66
+ message = record.getMessage()
67
+
68
+ # Pattern 1: HTTP requests to agent endpoints
69
+ # Example: "🌐 Sending request to agent endpoint: http://localhost:8000/run"
70
+ if "Sending request to agent endpoint:" in message or "🌐" in message:
71
+ url_match = re.search(r"(https?://[^\s]+)", message)
72
+ if url_match:
73
+ url = url_match.group(1)
74
+ method = "POST" # Most agent requests are POST
75
+ self.app_callback(
76
+ self.actions_callback,
77
+ "http_request",
78
+ method=method,
79
+ url=url,
80
+ )
81
+
82
+ # Pattern 2: Tool calls (from _log_agent_actions in completions.py)
83
+ # Example: "🔧 Agent actions for prefix #1:"
84
+ elif "🔧 Agent actions" in message or "Tool:" in message:
85
+ # Extract tool name if present
86
+ tool_match = re.search(r"Tool:\s*(\w+)", message)
87
+ if tool_match:
88
+ tool_name = tool_match.group(1)
89
+ self.app_callback(
90
+ self.actions_callback,
91
+ "tool_call",
92
+ tool_name=tool_name,
93
+ )
94
+
95
+ # Pattern 3: ADK events
96
+ # Example: "🤖 ADK Agent actions for prefix #1:"
97
+ elif "🤖 ADK Agent actions" in message or "ADK" in message:
98
+ # Check subsequent messages for event details
99
+ if "Tool Call:" in message:
100
+ tool_match = re.search(r"Tool Call:\s*(\w+)", message)
101
+ if tool_match:
102
+ tool_name = tool_match.group(1)
103
+ self.app_callback(
104
+ self.actions_callback,
105
+ "adk_event",
106
+ event_type="tool_call",
107
+ tool_name=tool_name,
108
+ )
109
+ elif "Tool Result:" in message:
110
+ tool_match = re.search(r"Tool Result:\s*(\w+)", message)
111
+ if tool_match:
112
+ tool_name = tool_match.group(1)
113
+ self.app_callback(
114
+ self.actions_callback,
115
+ "adk_event",
116
+ event_type="tool_result",
117
+ tool_name=tool_name,
118
+ )
119
+ elif "LLM Response:" in message:
120
+ content_match = re.search(r"LLM Response:\s*(.+)", message)
121
+ content = content_match.group(1) if content_match else ""
122
+ self.app_callback(
123
+ self.actions_callback,
124
+ "adk_event",
125
+ event_type="llm_response",
126
+ content=content,
127
+ )
128
+
129
+ # Pattern 4: Agent responses
130
+ # Example: "✅ Agent responded with status 200"
131
+ elif "Agent responded" in message or "✅" in message:
132
+ status_match = re.search(r"status\s+(\d+)", message)
133
+ if status_match:
134
+ # This indicates a successful HTTP response
135
+ pass # Could add response visualization here
136
+
137
+ # Pattern 5: Model queries (LiteLLM)
138
+ # Example: "🌐 Querying model gpt-4"
139
+ elif "Querying model" in message:
140
+ model_match = re.search(r"model\s+(\S+)", message)
141
+ if model_match:
142
+ model_name = model_match.group(1)
143
+ self.app_callback(
144
+ self.actions_callback,
145
+ "llm_query",
146
+ model_name=model_name,
147
+ )
148
+
149
+ except Exception:
150
+ # Silently fail to avoid breaking the logging system
151
+ pass
152
+
153
+
154
+ def extract_action_data_from_log(
155
+ actions_viewer: Any,
156
+ action_type: str,
157
+ **kwargs: Any,
158
+ ) -> None:
159
+ """
160
+ Extract and display action data in the actions viewer.
161
+
162
+ Args:
163
+ actions_viewer: The AgentActionsViewer widget
164
+ action_type: Type of action (http_request, tool_call, adk_event, etc.)
165
+ **kwargs: Action-specific parameters
166
+ """
167
+ try:
168
+ if action_type == "http_request":
169
+ actions_viewer.add_http_request(
170
+ method=kwargs.get("method", "POST"),
171
+ url=kwargs.get("url", ""),
172
+ headers=kwargs.get("headers"),
173
+ payload=kwargs.get("payload"),
174
+ )
175
+
176
+ elif action_type == "tool_call":
177
+ actions_viewer.add_tool_call(
178
+ tool_name=kwargs.get("tool_name", "unknown"),
179
+ arguments=kwargs.get("arguments"),
180
+ result=kwargs.get("result"),
181
+ )
182
+
183
+ elif action_type == "adk_event":
184
+ event_type = kwargs.get("event_type", "unknown")
185
+ event_data = {
186
+ "tool_name": kwargs.get("tool_name", ""),
187
+ "tool_input": kwargs.get("tool_input", {}),
188
+ "result": kwargs.get("result", ""),
189
+ "content": kwargs.get("content", ""),
190
+ }
191
+ actions_viewer.add_adk_event(event_type, event_data)
192
+
193
+ elif action_type == "llm_query":
194
+ # Display LLM query as a special kind of action
195
+ model_name = kwargs.get("model_name", "unknown")
196
+ actions_viewer.add_step_separator(f"LLM Query: {model_name}")
197
+
198
+ except Exception:
199
+ # Silently fail to avoid breaking the UI
200
+ pass
@@ -0,0 +1,288 @@
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
+ Main TUI Application
17
+
18
+ Full-screen tabbed interface for HackAgent.
19
+ """
20
+
21
+ from typing import Any
22
+
23
+ from rich.text import Text
24
+ from textual.app import App, ComposeResult
25
+ from textual.binding import Binding
26
+ from textual.containers import Container
27
+ from textual.widgets import Footer, Static, TabbedContent, TabPane
28
+
29
+ from hackagent.cli.config import CLIConfig
30
+ from hackagent.cli.tui.views.agents import AgentsTab
31
+ from hackagent.cli.tui.views.attacks import AttacksTab
32
+ from hackagent.cli.tui.views.config import ConfigTab
33
+ from hackagent.cli.tui.views.results import ResultsTab
34
+
35
+
36
+ class HackAgentHeader(Container):
37
+ """Custom header with ASCII logo"""
38
+
39
+ DEFAULT_CSS = """
40
+ HackAgentHeader {
41
+ dock: top;
42
+ width: 100%;
43
+ height: 7;
44
+ padding: 0 1;
45
+ }
46
+
47
+ HackAgentHeader Static {
48
+ color: #ff0000;
49
+ text-style: bold;
50
+ width: 100%;
51
+ content-align: center middle;
52
+ }
53
+ """
54
+
55
+ def compose(self) -> ComposeResult:
56
+ from hackagent.utils import HACKAGENT
57
+
58
+ # Display the ASCII logo as-is (now side-by-side format)
59
+ logo_text = Text(HACKAGENT, style="bold red")
60
+ yield Static(logo_text)
61
+
62
+
63
+ class HackAgentTUI(App):
64
+ """HackAgent Terminal User Interface Application"""
65
+
66
+ CSS = """
67
+ Screen {
68
+ background: $surface;
69
+ }
70
+
71
+ Header {
72
+ background: #8b0000; /* dark red - HackAgent brand color */
73
+ color: #ffffff;
74
+ height: 3;
75
+ }
76
+
77
+ Footer {
78
+ background: #2b0000; /* darker red */
79
+ color: #ffffff;
80
+ }
81
+
82
+ TabbedContent {
83
+ height: 100%;
84
+ border: solid #ff0000; /* red - HackAgent brand color */
85
+ }
86
+
87
+ TabPane {
88
+ padding: 1 2;
89
+ }
90
+
91
+ TabbedContent > ContentSwitcher > * > * {
92
+ background: $surface;
93
+ }
94
+
95
+ Tabs {
96
+ background: #2b0000;
97
+ }
98
+
99
+ Tab {
100
+ color: #cccccc;
101
+ background: #2b0000;
102
+ }
103
+
104
+ Tab.-active {
105
+ color: #ffffff;
106
+ background: #8b0000; /* dark red when active */
107
+ text-style: bold;
108
+ }
109
+
110
+ Tab:hover {
111
+ background: #5b0000;
112
+ }
113
+
114
+ .title-bar {
115
+ dock: top;
116
+ width: 100%;
117
+ background: #8b0000;
118
+ color: #ffffff;
119
+ height: 3;
120
+ content-align: center middle;
121
+ }
122
+
123
+ .section {
124
+ border: solid #ff0000;
125
+ padding: 1;
126
+ margin: 1;
127
+ height: auto;
128
+ }
129
+
130
+ .info-box {
131
+ background: $panel;
132
+ border: solid #ff0000;
133
+ padding: 1;
134
+ margin: 1;
135
+ }
136
+
137
+ Button {
138
+ margin: 1;
139
+ }
140
+
141
+ Button.-primary {
142
+ background: #8b0000;
143
+ color: #ffffff;
144
+ }
145
+
146
+ Button.-primary:hover {
147
+ background: #ff0000;
148
+ }
149
+
150
+ DataTable {
151
+ height: 100%;
152
+ }
153
+
154
+ DataTable > .datatable--header {
155
+ background: #8b0000;
156
+ color: #ffffff;
157
+ text-style: bold;
158
+ }
159
+
160
+ DataTable > .datatable--cursor {
161
+ background: #5b0000;
162
+ }
163
+
164
+ /* Results tab specific styles - horizontal split 20-80 */
165
+ ResultsTab #results-left-panel {
166
+ border-right: solid #ff0000;
167
+ background: $panel;
168
+ }
169
+
170
+ ResultsTab #results-right-panel {
171
+ background: $panel;
172
+ }
173
+
174
+ ResultsTab #results-title {
175
+ height: 3;
176
+ width: 100%;
177
+ text-align: center;
178
+ background: #8b0000;
179
+ color: #ffffff;
180
+ padding: 1;
181
+ }
182
+
183
+ ResultsTab #details-title {
184
+ height: 3;
185
+ width: 100%;
186
+ text-align: center;
187
+ background: #8b0000;
188
+ color: #ffffff;
189
+ padding: 1;
190
+ }
191
+
192
+ ResultsTab .toolbar {
193
+ height: 3;
194
+ width: 100%;
195
+ padding: 0 1;
196
+ }
197
+ """
198
+
199
+ TITLE = "🔴 HACKAGENT 🔴 - AI Security Testing Toolkit"
200
+ SUB_TITLE = "Red Team Security Interface"
201
+
202
+ BINDINGS = [
203
+ Binding("q", "quit", "Quit", priority=True),
204
+ Binding("a", "switch_tab('agents')", "Agents", show=False),
205
+ Binding("k", "switch_tab('attacks')", "Attacks", show=False),
206
+ Binding("r", "switch_tab('results')", "Results", show=False),
207
+ Binding("c", "switch_tab('config')", "Config", show=False),
208
+ Binding("f5", "refresh", "Refresh", show=True),
209
+ ]
210
+
211
+ def __init__(
212
+ self,
213
+ cli_config: CLIConfig,
214
+ initial_tab: str = "agents",
215
+ initial_data: dict[Any, Any] | None = None,
216
+ ):
217
+ """Initialize the TUI application.
218
+
219
+ Args:
220
+ cli_config: CLI configuration object
221
+ initial_tab: Which tab to show initially (default: "agents")
222
+ initial_data: Initial data to pre-fill in the tab (default: None)
223
+ """
224
+ super().__init__()
225
+ self.cli_config = cli_config
226
+ self.initial_tab = initial_tab
227
+ self.initial_data = initial_data or {}
228
+ self.dark = True # Use dark theme by default
229
+
230
+ def compose(self) -> ComposeResult:
231
+ """Compose the UI layout."""
232
+ yield HackAgentHeader()
233
+
234
+ with TabbedContent(initial=self.initial_tab):
235
+ with TabPane("Agents", id="agents"):
236
+ yield AgentsTab(self.cli_config)
237
+
238
+ with TabPane("Attacks", id="attacks"):
239
+ yield AttacksTab(self.cli_config, initial_data=self.initial_data)
240
+
241
+ with TabPane("Results", id="results"):
242
+ yield ResultsTab(self.cli_config)
243
+
244
+ with TabPane("Config", id="config"):
245
+ yield ConfigTab(self.cli_config)
246
+
247
+ yield Footer()
248
+
249
+ def action_switch_tab(self, tab_id: str) -> None:
250
+ """Switch to a specific tab.
251
+
252
+ Args:
253
+ tab_id: ID of the tab to switch to
254
+ """
255
+ tabs = self.query_one(TabbedContent)
256
+ tabs.active = tab_id
257
+
258
+ def action_refresh(self) -> None:
259
+ """Refresh the current tab's data."""
260
+ tabs = self.query_one(TabbedContent)
261
+ active_pane = tabs.get_pane(tabs.active)
262
+ if active_pane and hasattr(active_pane, "refresh_data"):
263
+ # Get the first child of the TabPane (our custom tab widget)
264
+ for child in active_pane.children:
265
+ if hasattr(child, "refresh_data"):
266
+ child.refresh_data()
267
+ break
268
+
269
+ def on_mount(self) -> None:
270
+ """Called when the app is mounted."""
271
+ self.title = self.TITLE
272
+ self.sub_title = self.SUB_TITLE
273
+
274
+ def show_success(self, message: str) -> None:
275
+ """Show success notification with checkmark."""
276
+ pass
277
+
278
+ def show_error(self, message: str) -> None:
279
+ """Show error notification with X mark."""
280
+ pass
281
+
282
+ def show_warning(self, message: str) -> None:
283
+ """Show warning notification with warning sign."""
284
+ pass
285
+
286
+ def show_info(self, message: str) -> None:
287
+ """Show info notification with info icon."""
288
+ pass
@@ -0,0 +1,137 @@
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
+ Base Tab Class
17
+
18
+ Base class for all TUI tabs with common functionality.
19
+ """
20
+
21
+ import httpx
22
+ from textual.containers import Container
23
+
24
+ from hackagent.cli.config import CLIConfig
25
+ from hackagent.client import AuthenticatedClient
26
+
27
+
28
+ class BaseTab(Container):
29
+ """Base class for all TUI tabs.
30
+
31
+ Provides common functionality:
32
+ - CLI configuration access
33
+ - API client creation with timeout
34
+ - Error handling helpers
35
+ - Refresh mechanism
36
+
37
+ Subclasses should implement refresh_data() method.
38
+ """
39
+
40
+ # Default API timeout (can be overridden by subclasses)
41
+ API_TIMEOUT = 5.0
42
+
43
+ def __init__(self, cli_config: CLIConfig, **kwargs):
44
+ """Initialize base tab.
45
+
46
+ Args:
47
+ cli_config: CLI configuration instance
48
+ **kwargs: Additional arguments passed to Container
49
+ """
50
+ super().__init__(**kwargs)
51
+ self.cli_config = cli_config
52
+ self._refresh_interval = None
53
+
54
+ def create_api_client(self, timeout: float | None = None) -> AuthenticatedClient:
55
+ """Create an authenticated API client with timeout.
56
+
57
+ Args:
58
+ timeout: Optional timeout override (uses API_TIMEOUT by default)
59
+
60
+ Returns:
61
+ Configured AuthenticatedClient instance
62
+ """
63
+ if timeout is None:
64
+ timeout = self.API_TIMEOUT
65
+
66
+ return AuthenticatedClient(
67
+ base_url=self.cli_config.base_url,
68
+ token=self.cli_config.api_key,
69
+ prefix="Bearer",
70
+ timeout=httpx.Timeout(timeout, connect=timeout),
71
+ )
72
+
73
+ def handle_api_error(self, error: Exception, context: str = "API call") -> str:
74
+ """Format API error messages for display.
75
+
76
+ Args:
77
+ error: The exception that occurred
78
+ context: Description of what operation failed
79
+
80
+ Returns:
81
+ Formatted error message
82
+ """
83
+ from rich.markup import escape
84
+
85
+ if isinstance(error, httpx.TimeoutException):
86
+ return f"[red]Timeout:[/red] {context} took too long"
87
+ elif isinstance(error, httpx.HTTPStatusError):
88
+ if error.response.status_code == 401:
89
+ return (
90
+ "[red]Authentication Failed[/red]\n\n"
91
+ "[yellow]Your API key is invalid or expired[/yellow]\n\n"
92
+ "[cyan]To fix:[/cyan]\n"
93
+ "Run: hackagent config set --api-key YOUR_KEY\n\n"
94
+ "[dim]Press F5 to retry after updating[/dim]"
95
+ )
96
+ else:
97
+ return f"[red]HTTP {error.response.status_code}:[/red] {context} failed"
98
+ else:
99
+ # Escape error message to prevent Rich markup issues
100
+ error_text = escape(str(error))
101
+ return f"[red]Error:[/red] {error_text}"
102
+
103
+ def refresh_data(self) -> None:
104
+ """Refresh tab data from API.
105
+
106
+ Should be overridden by subclasses that need data refresh functionality.
107
+ Default implementation does nothing.
108
+ """
109
+ pass
110
+
111
+ def enable_auto_refresh(self, interval: float = 5.0) -> None:
112
+ """Enable automatic data refresh at specified interval.
113
+
114
+ Args:
115
+ interval: Refresh interval in seconds (default: 5.0)
116
+ """
117
+ if self._refresh_interval is not None:
118
+ # Remove existing refresh timer
119
+ self._refresh_interval = None
120
+
121
+ self._refresh_interval = self.set_interval(
122
+ interval, self.refresh_data, name=f"{self.__class__.__name__}-refresh"
123
+ )
124
+
125
+ def disable_auto_refresh(self) -> None:
126
+ """Disable automatic data refresh."""
127
+ if self._refresh_interval is not None:
128
+ self._refresh_interval = None
129
+
130
+ def on_mount(self) -> None:
131
+ """Called when tab is mounted.
132
+
133
+ Subclasses can override to add custom mounting behavior,
134
+ but should call super().on_mount() to ensure proper initialization.
135
+ """
136
+ # Defer initial load to ensure DOM is ready
137
+ self.call_after_refresh(self.refresh_data)