Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions hackagent/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,8 +215,7 @@ def hack(
attack_config: Dict[str, Any],
run_config_override: Optional[Dict[str, Any]] = None,
fail_on_run_error: bool = True,
_tui_app: Optional[Any] = None,
_tui_log_callback: Optional[Any] = None,
_tui_event_bus: Optional[Any] = None,
) -> Any:
"""
Executes a specified attack strategy against the configured victim agent.
Expand Down Expand Up @@ -273,8 +272,7 @@ def hack(
attack_config=attack_config,
run_config_override=run_config_override,
fail_on_run_error=fail_on_run_error,
_tui_app=_tui_app,
_tui_log_callback=_tui_log_callback,
_tui_event_bus=_tui_event_bus,
)

except HackAgentError:
Expand Down
55 changes: 51 additions & 4 deletions hackagent/attacks/orchestrator.py
Original file line number Diff line number Diff line change
Expand Up @@ -633,8 +633,7 @@ def execute(
fail_on_run_error: bool,
max_wait_time_seconds: Optional[int] = None,
poll_interval_seconds: Optional[int] = None,
_tui_app: Optional[Any] = None,
_tui_log_callback: Optional[Any] = None,
_tui_event_bus: Optional[Any] = None,
) -> Any:
"""
Execute attack with server tracking.
Expand All @@ -652,8 +651,9 @@ def execute(
fail_on_run_error: Whether to raise on errors
max_wait_time_seconds: Unused for local execution
poll_interval_seconds: Unused for local execution
_tui_app: Optional TUI app for logging
_tui_log_callback: Optional TUI log callback
_tui_event_bus: Optional :class:`hackagent.cli.tui.events.TUIEventBus`
that receives structured events (step start/end, tool calls,
progress, etc.) during execution.

Returns:
Attack results from local execution
Expand Down Expand Up @@ -710,6 +710,22 @@ def execute(
except Exception as e:
logger.warning(f"Failed to update run status to RUNNING: {e}")

# Make the event bus available to the technique impl and to the
# tracker via the shared config bag (alongside _run_id / _backend).
if _tui_event_bus is not None:
attack_config = {**attack_config, "_tui_event_bus": _tui_event_bus}
effective_run_config = {
**effective_run_config,
"_tui_event_bus": _tui_event_bus,
}
_tui_event_bus.emit(
"step_started",
step_name="Attack Execution",
attack_type=self.attack_type,
run_id=run_id,
expected_total_goals=effective_run_config.get("expected_total_goals"),
)

# 5. Execute locally
try:
_total_t0 = time.perf_counter()
Expand All @@ -734,6 +750,9 @@ def execute(
"_backend": self.hackagent_agent.backend,
}

if _tui_event_bus is not None:
_tui_event_bus.emit("step_started", step_name="Evaluation Pipeline")

if (self.attack_type or "").lower() == "pair":
from hackagent.attacks.techniques.pair.evaluation import (
PAIREvaluation,
Expand Down Expand Up @@ -778,10 +797,31 @@ def execute(
except Exception as e:
logger.warning(f"Evaluation failed: {e}", exc_info=True)
final_results = results # fallback
if _tui_event_bus is not None:
_tui_event_bus.emit(
"step_ended",
step_name="Evaluation Pipeline",
success=False,
error=str(e),
)
else:
if _tui_event_bus is not None:
_tui_event_bus.emit(
"step_ended",
step_name="Evaluation Pipeline",
success=True,
)

# ⏱ timing AFTER evaluation
_total_elapsed = round(time.perf_counter() - _total_t0, 3)
logger.info(f"Total run time: {_total_elapsed:.1f}s")
if _tui_event_bus is not None:
_tui_event_bus.emit(
"step_ended",
step_name="Attack Execution",
success=True,
elapsed_s=_total_elapsed,
)

# ✅ Update run status to COMPLETED
try:
Expand All @@ -806,6 +846,13 @@ def execute(
)
except Exception as update_error:
logger.warning(f"Failed to update run status to FAILED: {update_error}")
if _tui_event_bus is not None:
_tui_event_bus.emit(
"step_ended",
step_name="Attack Execution",
success=False,
error=str(e),
)
raise

# ========================================================================
Expand Down
1 change: 1 addition & 0 deletions hackagent/attacks/techniques/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,7 @@ def _initialize_coordinator(
initial_metadata=initial_metadata,
goal_index_start=goal_index_start,
run_start_time=run_start_time,
event_bus=self.config.get("_tui_event_bus"),
)

# Backward-compat: expose step_tracker as self.tracker
Expand Down
1 change: 1 addition & 0 deletions hackagent/attacks/techniques/baseline/generation.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ def execute_prompts(
logger=logger,
attack_type="baseline",
category_classifier_config=config.get("category_classifier"),
event_bus=config.get("_tui_event_bus"),
)
else:
logger.warning("⚠️ Missing tracking context - results will NOT be created!")
Expand Down
189 changes: 0 additions & 189 deletions hackagent/cli/tui/actions_logger.py

This file was deleted.

33 changes: 22 additions & 11 deletions hackagent/cli/tui/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -214,15 +214,26 @@ def action_switch_tab(self, tab_id: str) -> None:
tabs.active = tab_id

def action_refresh(self) -> None:
"""Refresh the current tab's data."""
"""Refresh the current tab's data.

Walks every descendant of the active TabPane (not just immediate
children) so nested tab widgets — including those that wrap their
body in a scroller — still receive the refresh.
"""
tabs = self.query_one(TabbedContent)
active_pane = tabs.get_pane(tabs.active)
if active_pane and hasattr(active_pane, "refresh_data"):
# Get the first child of the TabPane (our custom tab widget)
for child in active_pane.children:
if hasattr(child, "refresh_data"):
child.refresh_data()
break
if active_pane is None:
return
try:
for descendant in active_pane.query("*"):
if hasattr(descendant, "refresh_data"):
descendant.refresh_data()
return
except Exception:
pass
# Last-resort: try the pane itself.
if hasattr(active_pane, "refresh_data"):
active_pane.refresh_data()

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

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

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

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

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