Skip to content
This repository was archived by the owner on Nov 29, 2025. It is now read-only.
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
67 commits
Select commit Hold shift + click to select a range
7b5f5eb
Add HITL module structure and type definitions
Oct 18, 2025
dbad6fa
Implement FeedbackManager with state tracking and event emission
Oct 18, 2025
f0eaf88
Implement HITLHookProvider with auto-pause for destructive operations
Oct 18, 2025
d631817
Document HITL event types in EventEmitter
Oct 18, 2025
8c31ed6
Add FeedbackInputHandler for bidirectional UI communication
Oct 18, 2025
8a4edc3
Integrate HITL hooks into agent creation with environment toggle
Oct 18, 2025
fffc5e2
Add unit tests for HITL feedback system
Oct 18, 2025
db313f9
Fix code quality issues in HITL components
Oct 18, 2025
08acd85
Add HITL event types and HITLInterventionPanel component
Oct 18, 2025
711f112
Add HITL command utilities for stdin communication
Oct 18, 2025
12eb1b2
Add HITL state management to application state
Oct 18, 2025
531e264
Add HITL event rendering to StreamDisplay
Oct 18, 2025
d62a7c8
Wire up HITLInterventionPanel in Terminal with keyboard handlers
Oct 18, 2025
04bbf28
Pass dispatch to Terminal component for HITL state updates
Oct 18, 2025
aa8d4e0
Add dispatch to App and pass through to MainAppView
Oct 18, 2025
b2e1128
Make HITL panel always visible when enabled
Oct 18, 2025
e0305c0
Adds changes to the main agent flow
Nov 16, 2025
3c8c2ea
Fix HITL panel state management to show persistent monitoring banner
Oct 18, 2025
adc12b3
Add manual HITL intervention with [i] key
Oct 19, 2025
55ab499
Add debug logging for HITL commands
Oct 19, 2025
7c41ce6
Fix HITL command routing to Python process stdin
Oct 19, 2025
0679af0
Simplify manual intervention to direct text input
Oct 19, 2025
f64c69b
Fix TypeScript errors in HITLInterventionPanel
Oct 19, 2025
d087a22
Prevent keyboard handler conflicts in manual intervention
Oct 19, 2025
837def3
Pin HITL panel above footer for persistent visibility
Oct 19, 2025
0fdbc8e
Hide input prompt when HITL panel active to prevent input leaking
Oct 19, 2025
0a6a7e1
Add feedback message formatting methods to FeedbackManager
Oct 19, 2025
4f0c950
Adds hitl parts
Nov 16, 2025
062e0c3
Implement HITL feedback injection in agent execution loop
Oct 19, 2025
e93d522
Simplify manual intervention panel display
Oct 19, 2025
efb3f74
Enhance feedback confirmation visibility with bordered box
Oct 19, 2025
d8d5a99
Fix feedback injection overwrite bug
Oct 19, 2025
803a92e
Fix visual alignment in HITL pause banner
Oct 19, 2025
1b1f05b
Add comprehensive HITL feedback logging and fix pause banner alignment
Oct 19, 2025
159ca8c
Improve HITL UI display and enable console logging in React mode
Oct 19, 2025
bf4407d
Implement hook-based HITL feedback injection
Oct 19, 2025
92267c2
Add direct stdout logging to HITL hook and fix ESC termination
Oct 19, 2025
b613e60
Addresses build issues
Oct 22, 2025
93cc8db
Adds extensive logging to validate the flow
Oct 22, 2025
cfc4711
Simplify HITL state machine to ACTIVE/PAUSED
Oct 26, 2025
697f136
Update HITL tests for simplified state machine
Oct 26, 2025
c97c25f
Remove agent interpretation workflow from React UI
Oct 26, 2025
96f9c85
Centralize HITL configuration in ConfigManager
Oct 26, 2025
827f64b
Implement blocking pause mechanism with configurable timeouts
Oct 26, 2025
3d2f6fa
Integrate blocking pause into HITL tool interception workflow
Oct 26, 2025
a0818cc
Consolidate HITL pause methods and fix manual blocking
Oct 26, 2025
f468fe1
Add timeout display to HITL panel and pause check in main loop
Oct 26, 2025
72a42fb
Fix HITL pause timing by moving check after agent execution
Oct 27, 2025
0cf262e
Fix HITL manual pause detection by hooking model invocations
Oct 27, 2025
9db70af
Increase HITL pause timeouts for better usability
Oct 27, 2025
f2e5d3f
Clarify HITL feedback types by mode in system prompt
Oct 27, 2025
f442038
Add comprehensive HITL feedback injection tests with mocked agent beh…
Oct 23, 2025
6868f57
Implements/ extends unit tests for hitl
Oct 28, 2025
7a8cdcb
Improve agent-triggered HITL panel UX and functionality
Oct 28, 2025
14363c3
Fixes a typo in pyproject
Nov 16, 2025
ba7d9f2
Updates strands hooks
Nov 16, 2025
62a8cb6
Removes all additional hitl loggers
Nov 16, 2025
fb60b8e
Fix duplicate class definitions in config module
Nov 18, 2025
c36b845
Fix prompt_rebuild_hook unconditional reference
Nov 18, 2025
71503df
Add missing hitl config to test mocks
Nov 18, 2025
032d31f
Update tests for create_agent return signature
Nov 18, 2025
d9ebe9a
Update model dev client tests for realistic data
Nov 18, 2025
4c496d7
Clean up dead code and fix imports
Nov 18, 2025
ee8b61e
Run code formatting with ruff
Nov 18, 2025
12c6d31
Fix test_create_agent_model_creation_failure mock setup
Nov 18, 2025
5bed705
Skip manual integration tests in CI
Nov 18, 2025
bf43046
Update pr-checks workflow to skip manual tests
Nov 19, 2025
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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ jobs:
run: |
export PATH="$HOME/.local/bin:$PATH"
export PYTHONPATH="${PWD}/src:${PYTHONPATH}"
PYTHONPATH="${PWD}/src:${PYTHONPATH}" uv run pytest tests/ -v
PYTHONPATH="${PWD}/src:${PYTHONPATH}" uv run pytest tests/ -v -m "not manual"

- name: Check Pylint minimum score
run: |
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/pr-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ jobs:
run: |
export PATH="$HOME/.local/bin:$PATH"
export PYTHONPATH="${PWD}/src:${PYTHONPATH}"
PYTHONPATH="${PWD}/src:${PYTHONPATH}" uv run pytest tests/ -v --tb=short
PYTHONPATH="${PWD}/src:${PYTHONPATH}" uv run pytest tests/ -v --tb=short -m "not manual"

- name: Display Results Summary
if: always()
Expand Down
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,9 @@ addopts = [
"--verbose"
]
pythonpath = ["src"]
markers = [
"manual: marks tests that require manual execution (e.g., integration tests with external services)"
]

[tool.pylint.main]
# Disable specific warnings that are not relevant for this project
Expand Down
51 changes: 45 additions & 6 deletions src/cyberautoagent.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,12 @@
from modules.agents.cyber_autoagent import (
AgentConfig,
create_agent,
_ensure_prompt_within_budget,
)
from modules.config.system.environment import auto_setup, clean_operation_memory, setup_logging
from modules.config.system.environment import (
auto_setup,
clean_operation_memory,
setup_logging,
)
from modules.config.manager import get_config_manager
from modules.handlers.base import StepLimitReached
from modules.handlers.utils import (
Expand Down Expand Up @@ -480,7 +483,12 @@ def main():

mcp_config = config_manager.get_mcp_config(args.provider, **config_overrides)
if mcp_config.enabled:
mcp_connections = list(filter(lambda c: '*' in c.plugins or args.module in c.plugins, mcp_config.connections))
mcp_connections = list(
filter(
lambda c: "*" in c.plugins or args.module in c.plugins,
mcp_config.connections,
)
)
else:
mcp_connections = []

Expand Down Expand Up @@ -636,7 +644,7 @@ def cleanup_logging():
module=args.module,
mcp_connections=mcp_connections,
)
agent, callback_handler = create_agent(
agent, callback_handler, feedback_manager = create_agent(
target=args.target,
objective=args.objective,
config=config,
Expand Down Expand Up @@ -669,14 +677,40 @@ def _initial_prompt_accessor():
)

current_message = initial_prompt

# Continue until stop condition is met
while not interrupted:
try:
_ensure_prompt_within_budget(agent)
# Execute agent with current message
# Note: HITL feedback is now injected via HITLFeedbackInjectionHook
# which modifies the system prompt in BeforeModelInvocationEvent
result = agent(current_message)

# Check for HITL pause AFTER agent execution
# This ensures pause is honored before starting next iteration
if feedback_manager:
is_paused = feedback_manager.is_paused()
logger.info(
"[HITL] Pause check: feedback_manager exists, is_paused=%s",
is_paused,
)
if is_paused:
logger.info(
"[HITL] Execution paused after iteration - blocking until resume"
)
print_status(
"⏸️ Execution paused - awaiting user feedback",
"INFO",
)
# Poll until pause is cleared (by feedback or timeout)
poll_count = 0
while feedback_manager.is_paused():
time.sleep(0.5)
poll_count += 1
logger.info(
"[HITL] Resumed after pause - continuing execution"
)
print_status("▶️ Execution resumed", "INFO")

# Pass the metrics from the result to the callback handler
if (
callback_handler
Expand Down Expand Up @@ -739,7 +773,12 @@ def __init__(self, accumulated_usage):
if remaining_steps > 0:
# Simple continuation message
current_message = f"Continue the security assessment. You have {remaining_steps} steps remaining out of {args.iterations} total. Focus on achieving the objective efficiently."
logger.debug(
"Generated continuation message for next iteration (length=%d)",
len(current_message),
)
else:
logger.info("No remaining steps - breaking execution loop")
break

except StepLimitReached:
Expand Down
124 changes: 105 additions & 19 deletions src/modules/agents/cyber_autoagent.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
configure_sdk_logging,
get_config_manager,
)
from modules.config.types import MCPConnection, ServerConfig
from modules.config.types import ServerConfig
from modules.config.system.logger import get_logger
from modules.config.models.factory import (
create_bedrock_model,
Expand Down Expand Up @@ -77,6 +77,12 @@
initialize_memory_system,
mem0_memory,
)
from modules.handlers.hitl import (
FeedbackInputHandler,
FeedbackManager,
HITLHookProvider,
)
from modules.handlers.hitl.feedback_injection_hook import HITLFeedbackInjectionHook
from modules.tools.prompt_optimizer import prompt_optimizer

warnings.filterwarnings("ignore", category=DeprecationWarning)
Expand All @@ -90,38 +96,51 @@
# for better separation of concerns. See imports above for available functions.


def _discover_mcp_tools(config: AgentConfig, server_config: ServerConfig) -> List[AgentTool]:
def _discover_mcp_tools(
config: AgentConfig, server_config: ServerConfig
) -> List[AgentTool]:
"""Discover and register MCP tools from configured connections."""
mcp_tools = []
environ = os.environ.copy()
for mcp_conn in (config.mcp_connections or []):
if '*' in mcp_conn.plugins or config.module in mcp_conn.plugins:
for mcp_conn in config.mcp_connections or []:
if "*" in mcp_conn.plugins or config.module in mcp_conn.plugins:
logger.debug("Discover MCP tools from: %s", mcp_conn)
try:
headers = resolve_env_vars_in_dict(mcp_conn.headers, environ)
match mcp_conn.transport:
case "stdio":
if not mcp_conn.command:
raise ValueError(f"{mcp_conn.transport} requires command")
command_list: List[str] = resolve_env_vars_in_list(mcp_conn.command, environ)
transport = lambda: stdio_client(StdioServerParameters(
command = command_list[0], args=command_list[1:],
env=environ,
))
command_list: List[str] = resolve_env_vars_in_list(
mcp_conn.command, environ
)
transport = lambda: stdio_client( # noqa: E731
StdioServerParameters(
command=command_list[0],
args=command_list[1:],
env=environ,
)
)
case "streamable-http":
transport = lambda: streamablehttp_client(
transport = lambda: streamablehttp_client( # noqa: E731
url=mcp_conn.server_url,
headers=headers,
timeout=mcp_conn.timeoutSeconds if mcp_conn.timeoutSeconds else 30,
timeout=mcp_conn.timeoutSeconds
if mcp_conn.timeoutSeconds
else 30,
)
case "sse":
transport = lambda: sse_client(
transport = lambda: sse_client( # noqa: E731
url=mcp_conn.server_url,
headers=headers,
timeout=mcp_conn.timeoutSeconds if mcp_conn.timeoutSeconds else 30,
timeout=mcp_conn.timeoutSeconds
if mcp_conn.timeoutSeconds
else 30,
)
case _:
raise ValueError(f"Unsupported MCP transport {mcp_conn.transport}")
raise ValueError(
f"Unsupported MCP transport {mcp_conn.transport}"
)
client = MCPClient(transport, prefix=mcp_conn.id)
prefix_idx = len(mcp_conn.id) + 1
client.start()
Expand All @@ -131,7 +150,10 @@ def _discover_mcp_tools(config: AgentConfig, server_config: ServerConfig) -> Lis
page_token = tools.pagination_token
for tool in tools:
logger.debug(f"Considering tool: {tool.tool_name}")
if '*' in mcp_conn.allowed_tools or tool.tool_name[prefix_idx:] in mcp_conn.allowed_tools:
if (
"*" in mcp_conn.allowed_tools
or tool.tool_name[prefix_idx:] in mcp_conn.allowed_tools
):
logger.debug(f"Allowed tool: {tool.tool_name}")
# Wrap output and save into output path
output_base_path = get_output_path(
Expand All @@ -145,7 +167,9 @@ def _discover_mcp_tools(config: AgentConfig, server_config: ServerConfig) -> Lis
client_used = True
if not page_token:
break
client_stop = lambda *_: client.stop(exc_type=None, exc_val=None, exc_tb=None)
client_stop = lambda *_: client.stop( # noqa: E731
exc_type=None, exc_val=None, exc_tb=None
)
if client_used:
atexit.register(client_stop)
signal.signal(signal.SIGTERM, client_stop)
Expand Down Expand Up @@ -195,6 +219,9 @@ def create_agent(

server_config = config_manager.get_server_config(config.provider, **overrides)

# Get HITL configuration
hitl_config = server_config.hitl

# Get centralized region configuration
if config.region_name is None:
config.region_name = config_manager.get_default_region()
Expand Down Expand Up @@ -667,6 +694,9 @@ def create_agent(
except Exception:
pass

# Check if HITL is enabled before creating handler so we can include it in init_context
hitl_enabled = hitl_config.enabled

callback_handler = ReactBridgeHandler(
max_steps=config.max_steps,
operation_id=operation_id,
Expand Down Expand Up @@ -708,8 +738,11 @@ def create_agent(
else {}
),
},
"observability": config_manager.getenv_bool("ENABLE_OBSERVABILITY", False),
"ui_mode": config_manager.getenv("CYBER_UI_MODE", "cli").lower(),
"observability": (
os.getenv("ENABLE_OBSERVABILITY", "false").lower() == "true"
),
"ui_mode": os.getenv("CYBER_UI_MODE", "cli").lower(),
"hitl_enabled": hitl_enabled,
},
)

Expand Down Expand Up @@ -768,6 +801,59 @@ def create_agent(
rebuild_interval=20,
)
hooks.append(prompt_rebuild_hook)
# Create HITL hook if enabled
hitl_hook = None
feedback_manager = None
feedback_handler = None

if hitl_enabled:
# Initialize feedback manager with configuration
feedback_manager = FeedbackManager(
memory=memory_client,
operation_id=operation_id,
emitter=callback_handler.emitter,
hitl_config=hitl_config,
)

# Initialize feedback input handler for receiving UI commands
feedback_handler = FeedbackInputHandler(feedback_manager=feedback_manager)
feedback_handler.start_listening()

# Verify thread is actually running
import time

time.sleep(0.5) # Give thread time to start
if (
feedback_handler._listener_thread
and feedback_handler._listener_thread.is_alive()
):
logger.info(
"[HITL] Listener thread verified: ID=%s, alive=%s",
feedback_handler._listener_thread.ident,
feedback_handler._listener_thread.is_alive(),
)
else:
logger.error("[HITL] WARNING: Listener thread failed to start!")

# Create HITL hook provider using centralized configuration
hitl_hook = HITLHookProvider(
feedback_manager=feedback_manager,
auto_pause_on_destructive=hitl_config.auto_pause_on_destructive,
auto_pause_on_low_confidence=hitl_config.auto_pause_on_low_confidence,
confidence_threshold=hitl_config.confidence_threshold,
)

# Create feedback injection hook for system prompt modification
feedback_injection_hook = HITLFeedbackInjectionHook(
feedback_manager=feedback_manager
)

print_status("HITL system enabled - human feedback available", "SUCCESS")

# Add HITL hooks if enabled
if hitl_hook:
hooks.append(hitl_hook)
hooks.append(feedback_injection_hook)

# Create model based on provider type
try:
Expand Down Expand Up @@ -967,4 +1053,4 @@ def create_agent(
pass

agent_logger.debug("Agent initialized successfully")
return agent, callback_handler
return agent, callback_handler, feedback_manager
Loading