Skip to content

Commit fedf9cc

Browse files
authored
Merge pull request #390 from AISecurityLab/379-using-litellm-adapter-for-every-agent-type
feat(tui): structured event bus for live attack monitoring
2 parents 3551aee + ab6beef commit fedf9cc

19 files changed

Lines changed: 1325 additions & 756 deletions

File tree

hackagent/agent.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -215,8 +215,7 @@ def hack(
215215
attack_config: Dict[str, Any],
216216
run_config_override: Optional[Dict[str, Any]] = None,
217217
fail_on_run_error: bool = True,
218-
_tui_app: Optional[Any] = None,
219-
_tui_log_callback: Optional[Any] = None,
218+
_tui_event_bus: Optional[Any] = None,
220219
) -> Any:
221220
"""
222221
Executes a specified attack strategy against the configured victim agent.
@@ -273,8 +272,7 @@ def hack(
273272
attack_config=attack_config,
274273
run_config_override=run_config_override,
275274
fail_on_run_error=fail_on_run_error,
276-
_tui_app=_tui_app,
277-
_tui_log_callback=_tui_log_callback,
275+
_tui_event_bus=_tui_event_bus,
278276
)
279277

280278
except HackAgentError:

hackagent/attacks/orchestrator.py

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -633,8 +633,7 @@ def execute(
633633
fail_on_run_error: bool,
634634
max_wait_time_seconds: Optional[int] = None,
635635
poll_interval_seconds: Optional[int] = None,
636-
_tui_app: Optional[Any] = None,
637-
_tui_log_callback: Optional[Any] = None,
636+
_tui_event_bus: Optional[Any] = None,
638637
) -> Any:
639638
"""
640639
Execute attack with server tracking.
@@ -652,8 +651,9 @@ def execute(
652651
fail_on_run_error: Whether to raise on errors
653652
max_wait_time_seconds: Unused for local execution
654653
poll_interval_seconds: Unused for local execution
655-
_tui_app: Optional TUI app for logging
656-
_tui_log_callback: Optional TUI log callback
654+
_tui_event_bus: Optional :class:`hackagent.cli.tui.events.TUIEventBus`
655+
that receives structured events (step start/end, tool calls,
656+
progress, etc.) during execution.
657657
658658
Returns:
659659
Attack results from local execution
@@ -710,6 +710,22 @@ def execute(
710710
except Exception as e:
711711
logger.warning(f"Failed to update run status to RUNNING: {e}")
712712

713+
# Make the event bus available to the technique impl and to the
714+
# tracker via the shared config bag (alongside _run_id / _backend).
715+
if _tui_event_bus is not None:
716+
attack_config = {**attack_config, "_tui_event_bus": _tui_event_bus}
717+
effective_run_config = {
718+
**effective_run_config,
719+
"_tui_event_bus": _tui_event_bus,
720+
}
721+
_tui_event_bus.emit(
722+
"step_started",
723+
step_name="Attack Execution",
724+
attack_type=self.attack_type,
725+
run_id=run_id,
726+
expected_total_goals=effective_run_config.get("expected_total_goals"),
727+
)
728+
713729
# 5. Execute locally
714730
try:
715731
_total_t0 = time.perf_counter()
@@ -734,6 +750,9 @@ def execute(
734750
"_backend": self.hackagent_agent.backend,
735751
}
736752

753+
if _tui_event_bus is not None:
754+
_tui_event_bus.emit("step_started", step_name="Evaluation Pipeline")
755+
737756
if (self.attack_type or "").lower() == "pair":
738757
from hackagent.attacks.techniques.pair.evaluation import (
739758
PAIREvaluation,
@@ -778,10 +797,31 @@ def execute(
778797
except Exception as e:
779798
logger.warning(f"Evaluation failed: {e}", exc_info=True)
780799
final_results = results # fallback
800+
if _tui_event_bus is not None:
801+
_tui_event_bus.emit(
802+
"step_ended",
803+
step_name="Evaluation Pipeline",
804+
success=False,
805+
error=str(e),
806+
)
807+
else:
808+
if _tui_event_bus is not None:
809+
_tui_event_bus.emit(
810+
"step_ended",
811+
step_name="Evaluation Pipeline",
812+
success=True,
813+
)
781814

782815
# ⏱ timing AFTER evaluation
783816
_total_elapsed = round(time.perf_counter() - _total_t0, 3)
784817
logger.info(f"Total run time: {_total_elapsed:.1f}s")
818+
if _tui_event_bus is not None:
819+
_tui_event_bus.emit(
820+
"step_ended",
821+
step_name="Attack Execution",
822+
success=True,
823+
elapsed_s=_total_elapsed,
824+
)
785825

786826
# ✅ Update run status to COMPLETED
787827
try:
@@ -806,6 +846,13 @@ def execute(
806846
)
807847
except Exception as update_error:
808848
logger.warning(f"Failed to update run status to FAILED: {update_error}")
849+
if _tui_event_bus is not None:
850+
_tui_event_bus.emit(
851+
"step_ended",
852+
step_name="Attack Execution",
853+
success=False,
854+
error=str(e),
855+
)
809856
raise
810857

811858
# ========================================================================

hackagent/attacks/techniques/base.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,7 @@ def _initialize_coordinator(
247247
initial_metadata=initial_metadata,
248248
goal_index_start=goal_index_start,
249249
run_start_time=run_start_time,
250+
event_bus=self.config.get("_tui_event_bus"),
250251
)
251252

252253
# Backward-compat: expose step_tracker as self.tracker

hackagent/attacks/techniques/baseline/generation.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,7 @@ def execute_prompts(
174174
logger=logger,
175175
attack_type="baseline",
176176
category_classifier_config=config.get("category_classifier"),
177+
event_bus=config.get("_tui_event_bus"),
177178
)
178179
else:
179180
logger.warning("⚠️ Missing tracking context - results will NOT be created!")

hackagent/cli/tui/actions_logger.py

Lines changed: 0 additions & 189 deletions
This file was deleted.

hackagent/cli/tui/app.py

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -214,15 +214,26 @@ def action_switch_tab(self, tab_id: str) -> None:
214214
tabs.active = tab_id
215215

216216
def action_refresh(self) -> None:
217-
"""Refresh the current tab's data."""
217+
"""Refresh the current tab's data.
218+
219+
Walks every descendant of the active TabPane (not just immediate
220+
children) so nested tab widgets — including those that wrap their
221+
body in a scroller — still receive the refresh.
222+
"""
218223
tabs = self.query_one(TabbedContent)
219224
active_pane = tabs.get_pane(tabs.active)
220-
if active_pane and hasattr(active_pane, "refresh_data"):
221-
# Get the first child of the TabPane (our custom tab widget)
222-
for child in active_pane.children:
223-
if hasattr(child, "refresh_data"):
224-
child.refresh_data()
225-
break
225+
if active_pane is None:
226+
return
227+
try:
228+
for descendant in active_pane.query("*"):
229+
if hasattr(descendant, "refresh_data"):
230+
descendant.refresh_data()
231+
return
232+
except Exception:
233+
pass
234+
# Last-resort: try the pane itself.
235+
if hasattr(active_pane, "refresh_data"):
236+
active_pane.refresh_data()
226237

227238
def on_mount(self) -> None:
228239
"""Called when the app is mounted."""
@@ -231,16 +242,16 @@ def on_mount(self) -> None:
231242

232243
def show_success(self, message: str) -> None:
233244
"""Show success notification with checkmark."""
234-
pass
245+
self.notify(f"✓ {message}", title="Success", severity="information")
235246

236247
def show_error(self, message: str) -> None:
237248
"""Show error notification with X mark."""
238-
pass
249+
self.notify(f"✗ {message}", title="Error", severity="error")
239250

240251
def show_warning(self, message: str) -> None:
241252
"""Show warning notification with warning sign."""
242-
pass
253+
self.notify(f"⚠ {message}", title="Warning", severity="warning")
243254

244255
def show_info(self, message: str) -> None:
245256
"""Show info notification with info icon."""
246-
pass
257+
self.notify(f"ℹ {message}", title="Info", severity="information")

0 commit comments

Comments
 (0)