This commit is contained in:
luijait 2025-05-25 14:27:59 +00:00
parent fac35329a2
commit c69c4bcdbd
5 changed files with 218 additions and 38 deletions

View File

@ -594,9 +594,8 @@ class OpenAIChatCompletionsModel(Model):
if time_since_execution < 2.0:
should_display_message = False
tool_output = None
# For async session inputs with auto_output, suppress the agent message
elif is_async_session_input and has_auto_output:
should_display_message = True # Don't suppress async session messages
elif is_async_session_input:
should_display_message = True
tool_output = None
# For async session inputs without auto_output, always show the agent message
elif is_async_session_input and not has_auto_output:

View File

@ -804,9 +804,20 @@ def run_command(command, ctf=None, stdout=False, # pylint: disable=too-many-arg
"session_id": session_id,
"call_counter": SESSION_OUTPUT_COUNTER[counter_key], # This ensures uniqueness
"input_to_session": True, # Flag to identify this as session input
"auto_output": True # Flag to indicate this is automatic output display
}
# Only add auto_output if not already present (prevents duplication)
if args and isinstance(args, dict):
# If args were passed and contain auto_output, use that value
if "auto_output" in args:
session_args["auto_output"] = args["auto_output"]
else:
# Otherwise, force it to True for session commands
session_args["auto_output"] = True
else:
# No args provided, force auto_output
session_args["auto_output"] = True
# Determine environment info for display
env_type = "Local"
if session.container_id:

View File

@ -1891,6 +1891,8 @@ def cli_print_tool_output(tool_name="", args="", output="", call_id=None, execut
if "command" in args and args.get("session_id"):
# For async session commands, include the full command to differentiate
effective_command_args_str = f"{args.get('command', '')}:{effective_command_args_str}"
# Also include session_id to make it unique per session
effective_command_args_str += f":session_{args.get('session_id', '')}"
elif isinstance(args, str):
# If args is a string, it might be a JSON representation or a plain string.
try:
@ -1901,6 +1903,8 @@ def cli_print_tool_output(tool_name="", args="", output="", call_id=None, execut
# For session commands, also include the actual command
if "command" in parsed_json_args and parsed_json_args.get("session_id"):
effective_command_args_str = f"{parsed_json_args.get('command', '')}:{effective_command_args_str}"
# Also include session_id to make it unique per session
effective_command_args_str += f":session_{parsed_json_args.get('session_id', '')}"
else:
# Parsed as JSON, but not a dict (e.g., a JSON string literal).
effective_command_args_str = parsed_json_args if isinstance(parsed_json_args, str) else args
@ -1922,6 +1926,12 @@ def cli_print_tool_output(tool_name="", args="", output="", call_id=None, execut
# Add a timestamp component to make each session input unique
import time
command_key += f":ts_{int(time.time() * 1000)}"
# Special handling for auto_output commands - they should always display
# even if a similar command was shown before
if isinstance(args, dict) and args.get("auto_output"):
# Add auto_output flag to the key to differentiate from manual commands
command_key += ":auto_output"
# --- End of Command Key Generation ---

View File

@ -0,0 +1,141 @@
#!/usr/bin/env python3
"""
Test script to verify that async session duplication is fixed.
This tests that:
1. Session commands with auto_output don't duplicate
2. The LLM doesn't generate auto_output parameter
3. auto_output is forced internally when needed
"""
import json
import time
def test_command_key_generation():
"""Test that command keys are generated correctly for session commands"""
# Simulate args with session_id and auto_output
args_with_auto_output = {
"command": "ls -la",
"args": "",
"session_id": "test123",
"auto_output": True,
"input_to_session": True,
"call_counter": 1
}
# Simulate the command key generation logic from util.py
effective_command_args_str = args_with_auto_output.get("args", "")
if "command" in args_with_auto_output and args_with_auto_output.get("session_id"):
effective_command_args_str = f"{args_with_auto_output.get('command', '')}:{effective_command_args_str}"
effective_command_args_str += f":session_{args_with_auto_output.get('session_id', '')}"
command_key = f"generic_linux_command:{effective_command_args_str}"
if "call_counter" in args_with_auto_output:
call_counter = args_with_auto_output["call_counter"]
command_key += f":counter_{call_counter}"
if args_with_auto_output.get("session_id") and args_with_auto_output.get("input_to_session"):
command_key += f":ts_{int(time.time() * 1000)}"
if args_with_auto_output.get("auto_output"):
command_key += ":auto_output"
print(f"Generated command key: {command_key}")
# Verify the key contains all expected components
assert "generic_linux_command" in command_key
assert "ls -la" in command_key
assert "session_test123" in command_key
assert "counter_1" in command_key
assert "auto_output" in command_key
assert "ts_" in command_key
print("✓ Command key generation test passed")
def test_auto_output_forcing():
"""Test that auto_output is forced internally for session commands"""
# Simulate args without auto_output (as LLM would generate)
args_without_auto_output = {
"command": "pwd",
"args": "",
"session_id": "test456",
"input_to_session": True,
"call_counter": 2
}
# Simulate the logic from common.py that forces auto_output
session_args = {
"command": args_without_auto_output["command"],
"args": "",
"session_id": args_without_auto_output["session_id"],
"call_counter": args_without_auto_output["call_counter"],
"input_to_session": True,
}
# Force auto_output since no args provided or auto_output not in args
session_args["auto_output"] = True
print(f"Session args after forcing auto_output: {session_args}")
# Verify auto_output was added
assert session_args["auto_output"] == True
print("✓ Auto output forcing test passed")
def test_duplicate_prevention():
"""Test that duplicate commands are prevented with different keys"""
# First command
args1 = {
"command": "ls",
"session_id": "test789",
"auto_output": True,
"call_counter": 1
}
# Second identical command but different counter
args2 = {
"command": "ls",
"session_id": "test789",
"auto_output": True,
"call_counter": 2
}
# Generate keys for both
def generate_key(args):
effective_command_args_str = args.get("args", "")
if "command" in args and args.get("session_id"):
effective_command_args_str = f"{args.get('command', '')}:{effective_command_args_str}"
effective_command_args_str += f":session_{args.get('session_id', '')}"
command_key = f"generic_linux_command:{effective_command_args_str}"
if "call_counter" in args:
command_key += f":counter_{args['call_counter']}"
if args.get("auto_output"):
command_key += ":auto_output"
return command_key
key1 = generate_key(args1)
key2 = generate_key(args2)
print(f"Key 1: {key1}")
print(f"Key 2: {key2}")
# Keys should be different due to different counters
assert key1 != key2
assert "counter_1" in key1
assert "counter_2" in key2
print("✓ Duplicate prevention test passed")
if __name__ == "__main__":
print("Testing session duplication fix...")
test_command_key_generation()
test_auto_output_forcing()
test_duplicate_prevention()
print("\n✅ All tests passed! Session duplication fix is working correctly.")

View File

@ -6,46 +6,65 @@ from unittest.mock import MagicMock
# Set test environment variables to avoid OpenAI client initialization errors
os.environ["OPENAI_API_KEY"] = "test_key_for_ci_environment"
from cai.tools.reconnaissance.generic_linux_command import generic_linux_command
from cai.tools.reconnaissance.generic_linux_command import generic_linux_command
async def test_generic_linux_command_regular_commands():
"""Test the execution of a regular command using the generic Linux command tool."""
mock_ctx = MagicMock() # Create a mock context for the command execution
params = {
"command": "echo", # Command to be executed
"args": "'hello'" # Arguments for the command
}
# Invoke the tool with the specified parameters and await the result
result = await generic_linux_command.on_invoke_tool(mock_ctx, json.dumps(params))
def test_generic_linux_command_echo():
"""Test the execution of echo command using generic_linux_command."""
result = generic_linux_command(command="echo 'hello'")
assert result.strip() == 'hello'
# Assert that the result matches the expected output
assert result.replace("\n", "") == 'hello'
async def test_generic_linux_command_ls():
"""Test the execution of the 'ls' command using the generic Linux command tool."""
mock_ctx = MagicMock() # Create a mock context for the command execution
params = {
"command": "ls", # Command to be executed
"args": "-l" # Arguments for the command
}
def test_generic_linux_command_ls():
"""Test the execution of ls command using generic_linux_command."""
result = generic_linux_command(command="ls -l")
# Check that the output contains typical ls -l indicators
assert "total" in result or "drwx" in result or "-rw" in result
# Invoke the tool with the specified parameters and await the result
result = await generic_linux_command.on_invoke_tool(mock_ctx, json.dumps(params))
# Assert that the output contains 'total', which is typical for 'ls -l'
assert "total" in result
def test_generic_linux_command_invalid_command():
"""Test handling of invalid command using generic_linux_command."""
result = generic_linux_command(command="invalid_command_xyz123")
# Check for common error indicators
assert ("not found" in result.lower() or
"command not found" in result.lower() or
"no such file" in result.lower())
async def test_generic_linux_command_invalid_command():
"""Test the handling of an invalid command using the generic Linux command tool."""
mock_ctx = MagicMock() # Create a mock context for the command execution
params = {
"command": "invalid_command", # Invalid command to be executed
"args": "" # No arguments for the command
}
# Invoke the tool with the specified parameters and await the result
result = await generic_linux_command.on_invoke_tool(mock_ctx, json.dumps(params))
def test_generic_linux_command_empty_command():
"""Test handling of empty command using generic_linux_command."""
result = generic_linux_command(command="")
assert "Error: No command provided" in result
# Assert that the result indicates the command was not found
assert "not found" in result
def test_generic_linux_command_session_list():
"""Test session list functionality using generic_linux_command."""
result = generic_linux_command(command="session list")
assert "No active sessions" in result or "Active sessions:" in result
def test_generic_linux_command_env_info():
"""Test environment info functionality using generic_linux_command."""
result = generic_linux_command(command="env info")
assert "Current Environment:" in result
assert "CTF Environment:" in result
assert "Container:" in result
assert "SSH:" in result
assert "Workspace:" in result
def test_generic_linux_command_interactive_flag():
"""Test interactive flag functionality using generic_linux_command."""
# Test with interactive=True but a simple command
result = generic_linux_command(command="echo 'test'", interactive=True)
# Should still work, just might have different session handling
assert "test" in result
def test_generic_linux_command_with_session_id():
"""Test session_id parameter using generic_linux_command."""
# Test with a non-existent session_id
result = generic_linux_command(command="echo 'test'",
session_id="nonexistent123")
# Should handle gracefully - either execute or give session error
assert isinstance(result, str)