Add a lot of stream

This commit is contained in:
luijait 2025-05-10 21:21:43 +02:00
parent 355b82b1f5
commit 618d982b2d
14 changed files with 1686 additions and 699 deletions

72
check_s3_bucket.py Normal file
View File

@ -0,0 +1,72 @@
#!/usr/bin/env python3
import requests
import sys
from urllib.parse import urlparse
import xml.etree.ElementTree as ET
def check_s3_bucket(bucket_name):
"""Check if an S3 bucket is publicly accessible and list its contents if possible."""
# Remove any s3.amazonaws.com suffix if present
if '.' in bucket_name:
parsed = urlparse('http://' + bucket_name)
bucket_name = parsed.netloc.split('.')[0]
urls = [
f"https://{bucket_name}.s3.amazonaws.com",
f"https://s3.amazonaws.com/{bucket_name}"
]
for url in urls:
print(f"Checking {url}")
try:
response = requests.get(url, timeout=10)
print(f"Status code: {response.status_code}")
if response.status_code == 200:
print("Bucket is publicly accessible!")
# Try to parse XML response to list bucket contents
try:
root = ET.fromstring(response.content)
ns = {'s3': 'http://s3.amazonaws.com/doc/2006-03-01/'}
print("\nContents:")
for content in root.findall('.//s3:Contents', ns):
key = content.find('s3:Key', ns)
size = content.find('s3:Size', ns)
last_modified = content.find('s3:LastModified', ns)
if key is not None:
print(f"File: {key.text}", end=" ")
if size is not None:
print(f"(Size: {size.text} bytes)", end=" ")
if last_modified is not None:
print(f"Last Modified: {last_modified.text}", end="")
print()
except Exception as e:
print(f"Error parsing bucket listing: {e}")
print("Raw response:")
print(response.text[:1000]) # Print first 1000 chars
elif response.status_code == 403:
print("Bucket exists but access is forbidden")
elif response.status_code == 404:
print("Bucket not found")
else:
print(f"Unexpected response with status code {response.status_code}")
except requests.exceptions.RequestException as e:
print(f"Error: {e}")
# Try alternate spelling as mentioned in the request
alt_url = f"https://trainingaliasrobotics.s3.amazonnaws.com"
print(f"\nChecking alternate spelling: {alt_url}")
try:
response = requests.get(alt_url, timeout=10)
print(f"Status code: {response.status_code}")
print(f"Response text: {response.text[:1000]}")
except requests.exceptions.RequestException as e:
print(f"Error: {e}")
if __name__ == "__main__":
bucket_name = "trainingaliasrobotics"
check_s3_bucket(bucket_name)

46
check_terrapin.py Normal file
View File

@ -0,0 +1,46 @@
import socket
import ssl
import sys
def check_terrapin(hostname, port=443):
"""
Basic check for Terrapin vulnerability (CVE-2023-45866)
This is a simplified check and may not be 100% accurate
"""
try:
# Create socket and wrap with SSL
context = ssl.create_default_context()
context.check_hostname = False
context.verify_mode = ssl.CERT_NONE
# Check if supports TLS 1.2 (Terrapin affects TLS 1.2 and below)
context.maximum_version = ssl.TLSVersion.TLSv1_2
with socket.create_connection((hostname, port), timeout=10) as sock:
with context.wrap_socket(sock, server_hostname=hostname) as ssock:
cipher = ssock.cipher()
if cipher and cipher[0]:
cipher_name = cipher[0]
print(f"Connected using: {cipher_name}")
# Check if using CBC cipher (Terrapin affects CBC ciphers)
if 'CBC' in cipher_name:
print(f"[POTENTIALLY VULNERABLE] {hostname} might be vulnerable to Terrapin")
print(f"Using CBC cipher: {cipher_name}")
return True
else:
print(f"[LIKELY SAFE] {hostname} is using non-CBC cipher: {cipher_name}")
return False
except ssl.SSLError as e:
print(f"SSL Error: {e}")
except socket.error as e:
print(f"Socket Error: {e}")
except Exception as e:
print(f"Error checking {hostname}: {e}")
return None
if __name__ == "__main__":
hostname = "aliasrobotics.com"
print(f"Checking {hostname} for potential Terrapin vulnerability...")
check_terrapin(hostname)

87
check_vulns.py Normal file
View File

@ -0,0 +1,87 @@
import subprocess
import json
import sys
import socket
import ssl
import requests
from urllib.parse import urlparse
def check_terrapin(hostname, port=443):
print(f"[*] Checking {hostname}:{port} for Terrapin vulnerability (CVE-2023-48618)")
try:
# Create a socket and wrap it with SSL
context = ssl.create_default_context()
with socket.create_connection((hostname, port)) as sock:
with context.wrap_socket(sock, server_hostname=hostname) as ssock:
# Check if server supports TLS 1.3
version = ssock.version()
print(f"[*] Server SSL/TLS version: {version}")
# Terrapin affects TLS 1.3 implementation with specific configurations
if 'TLSv1.3' in version:
print("[!] Server supports TLS 1.3, which may be vulnerable to Terrapin attack")
print("[!] Note: Detailed testing requires specialized tools, as this is a downgrade attack")
print("[!] The vulnerability allows attackers to force downgrade to CBC ciphers in TLS 1.2")
else:
print("[*] Server does not use TLS 1.3, not directly affected by Terrapin")
except Exception as e:
print(f"[-] Error checking for Terrapin: {e}")
def check_s3_bucket(bucket_name):
print(f"[*] Checking S3 bucket: {bucket_name}")
# Clean up the bucket name if needed
if bucket_name.startswith("http://") or bucket_name.startswith("https://"):
parsed = urlparse(bucket_name)
bucket_name = parsed.netloc
if ".s3.amazonaws" in bucket_name:
bucket_name = bucket_name.split(".s3.amazonaws")[0]
# Direct URL
url = f"http://{bucket_name}.s3.amazonaws.com"
try:
response = requests.get(url)
print(f"[*] Bucket URL: {url}")
print(f"[*] Response status: {response.status_code}")
if response.status_code == 200:
print("[!] Bucket is publicly accessible")
print("[*] Response content:")
# Print the first 1000 characters of the response content
print(response.text[:1000])
elif response.status_code == 403:
print("[!] Bucket exists but access is forbidden (listing denied)")
print("[*] This could mean the bucket exists but is not publicly listable")
elif response.status_code == 404:
print("[*] Bucket not found")
else:
print(f"[*] Unexpected response: {response.status_code}")
except Exception as e:
print(f"[-] Error checking bucket: {e}")
# Try to list bucket objects with aws cli
try:
print("[*] Attempting to list bucket objects with AWS CLI...")
result = subprocess.run(
["aws", "s3", "ls", f"s3://{bucket_name}", "--no-sign-request"],
capture_output=True,
text=True
)
if result.returncode == 0 and result.stdout:
print("[!] Bucket is listable without authentication!")
print("[*] Bucket contents:")
print(result.stdout)
else:
print("[*] Bucket is not listable without authentication")
if result.stderr:
print(f"[*] Error: {result.stderr}")
except Exception as e:
print(f"[-] Error using AWS CLI: {e}")
if __name__ == "__main__":
# Check for Terrapin vulnerability
check_terrapin("aliasrobotics.com")
# Check for exposed S3 bucket
check_s3_bucket("trainingaliasrobotics")

1
hello_world.py Normal file
View File

@ -0,0 +1 @@
print('Hello, World!')

1
helloworld.py Normal file
View File

@ -0,0 +1 @@
print('Hello, World!')

1
hola_mundo.py Normal file
View File

@ -0,0 +1 @@
print("¡Hola Mundo!")

0
nmap_results Normal file
View File

71
security_check.py Normal file
View File

@ -0,0 +1,71 @@
import socket
import ssl
def test_terrapin(hostname, port=443):
"""
Test if a server is vulnerable to the Terrapin Attack (CVE-2023-48795)
This is a basic test that checks for specific TLS behavior related to CBC ciphers
"""
print(f"Testing {hostname}:{port} for Terrapin vulnerability (CVE-2023-48795)...")
context = ssl.create_default_context()
context.check_hostname = False
context.verify_mode = ssl.CERT_NONE
# Force the use of CBC ciphers which are affected by the attack
context.set_ciphers('AES256-SHA:AES128-SHA:DES-CBC3-SHA')
try:
with socket.create_connection((hostname, port)) as sock:
with context.wrap_socket(sock, server_hostname=hostname) as ssock:
cipher = ssock.cipher()
print(f"Connected using: {cipher}")
# If we can connect with CBC ciphers, the server might be vulnerable
if "CBC" in cipher[0]:
print("Server accepts CBC ciphers which are potentially vulnerable to Terrapin Attack")
print("Note: This is only an indicator and not a definitive test")
print("Full verification would require specialized tools and in-depth testing")
return True
else:
print("Server not using CBC ciphers in this test")
return False
except Exception as e:
print(f"Error during test: {e}")
return False
# Also check for the AWS S3 bucket
def check_s3_bucket(bucket_name):
"""
Check if an S3 bucket exists and is accessible
"""
print(f"\nChecking S3 bucket: {bucket_name}")
try:
with socket.create_connection((bucket_name, 443)) as sock:
# S3 should resolve so we can connect, which suggests the bucket exists
print(f"Bucket {bucket_name} exists (DNS resolution successful)")
# We try to make a simple HTTPS request
context = ssl.create_default_context()
with context.wrap_socket(sock, server_hostname=bucket_name) as ssock:
ssock.sendall(b"GET / HTTP/1.1\r\nHost: " + bucket_name.encode() + b"\r\n\r\n")
data = ssock.recv(4096)
print(f"Response from bucket:\n{data.decode('utf-8', errors='ignore')[:500]}")
if b"AccessDenied" in data or b"403 Forbidden" in data:
print("Bucket exists but access is denied (this is normal for protected buckets)")
return True
else:
print("Bucket might be publicly accessible!")
return True
except Exception as e:
print(f"Error checking bucket: {e}")
if "Name or service not known" in str(e):
print("Bucket does not exist or is not accessible")
return False
# Test both issues
test_terrapin("aliasrobotics.com")
check_s3_bucket("trainingaliasrobotics.s3.amazonaws.com")

View File

@ -442,6 +442,13 @@ def run_cai_cli(starting_agent, context_variables=None, stream=False, max_turns=
}
)
# Fix message list structure BEFORE sending to the model to prevent errors
try:
from cai.util import fix_message_list
history_context = fix_message_list(history_context)
except Exception as e:
console.print(f"[yellow]Warning: Could not preprocess message history: {e}[/yellow]")
# Append the current user input as the last message in the list.
conversation_input: list | str
if history_context:

View File

@ -336,6 +336,15 @@ class OpenAIChatCompletionsModel(Model):
# Log the user message
if item.get("content"):
self.logger.log_user_message(item.get("content"))
# IMPORTANT: Ensure the message list has valid tool call/result pairs
# This needs to happen before the API call to prevent errors
try:
from cai.util import fix_message_list
converted_messages = fix_message_list(converted_messages)
except Exception as e:
logger.warning(f"Failed to fix message list: {e}")
# Get token count estimate before API call for consistent counting
estimated_input_tokens, _ = count_tokens_with_tiktoken(converted_messages)
@ -433,6 +442,7 @@ class OpenAIChatCompletionsModel(Model):
interaction_cost=None,
total_cost=None,
tool_output=None, # Don't pass tool output here, we're using direct display
suppress_empty=True # Suppress empty panels
)
# --- Add assistant tool call to message_history if present ---
@ -457,6 +467,22 @@ class OpenAIChatCompletionsModel(Model):
}
add_to_message_history(tool_call_msg)
# Save the tool call details for later matching with output
# This is important for non-streaming mode to track tool calls properly
if not hasattr(_Converter, 'recent_tool_calls'):
_Converter.recent_tool_calls = {}
# Store the tool call by ID for later reference
import time
_Converter.recent_tool_calls[tool_call.id] = {
'name': tool_call.function.name,
'arguments': tool_call.function.arguments,
'start_time': time.time(),
'execution_info': {
'start_time': time.time()
}
}
# Log the assistant tool call message
tool_calls_list = []
@ -583,7 +609,7 @@ class OpenAIChatCompletionsModel(Model):
stop_idle_timer()
start_active_timer()
# Check if streaming should be shown in rich panel
# --- Check if streaming should be shown in rich panel ---
should_show_rich_stream = os.getenv('CAI_STREAM', 'false').lower() == 'true' and not self.disable_rich_streaming
# Create streaming context if needed
@ -922,6 +948,55 @@ class OpenAIChatCompletionsModel(Model):
if tool_call_msg not in streamed_tool_calls:
streamed_tool_calls.append(tool_call_msg)
add_to_message_history(tool_call_msg)
# NEW: Display tool call immediately when detected in streaming mode
# But only if it has complete arguments and name
if (state.function_calls[tc_index].name and
state.function_calls[tc_index].arguments and
state.function_calls[tc_index].call_id):
# First, finish any existing streaming context if it exists
if streaming_context:
try:
finish_agent_streaming(streaming_context, None)
streaming_context = None
except Exception:
pass
# Create a message-like object for displaying the function call
tool_msg = type('ToolCallStreamDisplay', (), {
'content': None,
'tool_calls': [
type('ToolCallDetail', (), {
'function': type('FunctionDetail', (), {
'name': state.function_calls[tc_index].name,
'arguments': state.function_calls[tc_index].arguments
}),
'id': state.function_calls[tc_index].call_id,
'type': 'function'
})
]
})
# Display the tool call during streaming
cli_print_agent_messages(
agent_name=getattr(self, 'agent_name', 'Agent'),
message=tool_msg,
counter=getattr(self, 'interaction_counter', 0),
model=str(self.model),
debug=False,
interaction_input_tokens=estimated_input_tokens,
interaction_output_tokens=estimated_output_tokens,
interaction_reasoning_tokens=0, # Not available during streaming yet
total_input_tokens=getattr(self, 'total_input_tokens', 0) + estimated_input_tokens,
total_output_tokens=getattr(self, 'total_output_tokens', 0) + estimated_output_tokens,
total_reasoning_tokens=getattr(self, 'total_reasoning_tokens', 0),
interaction_cost=None,
total_cost=None,
tool_output=None, # Will be shown once tool is executed
suppress_empty=True # Prevent empty panels
)
# Set flag to suppress final output to avoid duplication
self.suppress_final_output = True
except Exception as e:
# Ensure streaming context is cleaned up in case of errors
@ -983,8 +1058,15 @@ class OpenAIChatCompletionsModel(Model):
)
# Display the tool call in CLI
from cai.util import cli_print_agent_messages
try:
# First, finish any existing streaming context if it exists
if streaming_context:
try:
finish_agent_streaming(streaming_context, None)
streaming_context = None
except Exception:
pass
# Create a message-like object to display the function call
tool_msg = type('ToolCallWrapper', (), {
'content': None,
@ -1015,8 +1097,12 @@ class OpenAIChatCompletionsModel(Model):
total_reasoning_tokens=getattr(self, 'total_reasoning_tokens', 0),
interaction_cost=None,
total_cost=None,
tool_output=None # Will be shown once the tool is executed
tool_output=None, # Will be shown once the tool is executed
suppress_empty=True # Suppress empty panels during streaming
)
# Set flag to suppress final output to avoid duplication
self.suppress_final_output = True
except Exception as e:
logger.error(f"Error displaying tool call in CLI: {e}")
@ -1220,6 +1306,7 @@ class OpenAIChatCompletionsModel(Model):
direct_stats["total_cost"] = float(total_cost)
# Use the direct copy with guaranteed float costs
finish_agent_streaming(streaming_context, direct_stats)
streaming_context = None
# Removed extra newline after streaming completes to avoid blank lines
pass
@ -1243,8 +1330,10 @@ class OpenAIChatCompletionsModel(Model):
for tool_call in tool_call_msg.get("tool_calls", []):
tool_calls_list.append(tool_call)
self.logger.log_assistant_message(None, tool_calls_list)
# If there was only text output, add that as an assistant message
if (not streamed_tool_calls) and state.text_content_index_and_output and state.text_content_index_and_output[1].text:
# If we've already shown the tool calls directly during streaming,
# don't log the text message to avoid duplication
elif (not self.suppress_final_output) and state.text_content_index_and_output and state.text_content_index_and_output[1].text:
asst_msg = {
"role": "assistant",
"content": state.text_content_index_and_output[1].text
@ -1253,6 +1342,9 @@ class OpenAIChatCompletionsModel(Model):
# Log the assistant message
self.logger.log_assistant_message(state.text_content_index_and_output[1].text)
# Reset the suppress flag for future requests
self.suppress_final_output = False
# Log the complete response
self.logger.rec_training_data(
{
@ -2042,7 +2134,11 @@ class _Converter:
if current_assistant_msg is not None:
# The API doesn't support empty arrays for tool_calls
if not current_assistant_msg.get("tool_calls"):
del current_assistant_msg["tool_calls"]
# Ensure content is not None if tool_calls are absent and content is also None
# Some models like Anthropic require some content, even if it's just a placeholder.
if current_assistant_msg.get("content") is None:
current_assistant_msg["content"] = "(No text content in this assistant message)" # Or just an empty string if preferred
current_assistant_msg.pop("tool_calls", None) # Use pop with default to avoid KeyError
result.append(current_assistant_msg)
current_assistant_msg = None
@ -2079,16 +2175,28 @@ class _Converter:
flush_assistant_message()
tool_calls_param: list[ChatCompletionMessageToolCallParam] = []
for tc in item["tool_calls"]:
function_details = tc.get("function", {})
arguments = function_details.get("arguments")
# Ensure arguments is a valid JSON string, defaulting to "{}" if empty or None
if arguments is None or (isinstance(arguments, str) and arguments.strip() == ""):
arguments = "{}"
elif isinstance(arguments, dict):
# Ensure it's a string if it's a dict (should already be string per schema)
arguments = json.dumps(arguments)
tool_calls_param.append(
ChatCompletionMessageToolCallParam(
id=tc.get("id", ""),
type=tc.get("type", "function"),
function=tc.get("function", {}),
function={
"name": function_details.get("name", "unknown_function"),
"arguments": arguments, # Use sanitized arguments
},
)
)
msg_asst: ChatCompletionAssistantMessageParam = {
"role": "assistant",
"content": item.get("content"),
"content": item.get("content"), # Content can be None here
"tool_calls": tool_calls_param,
}
result.append(msg_asst)
@ -2225,12 +2333,19 @@ class _Converter:
}
}
arguments = func_call.get("arguments") # func_call is a dict here
# Ensure arguments is a valid JSON string, defaulting to "{}" if empty or None
if arguments is None or (isinstance(arguments, str) and arguments.strip() == ""):
arguments = "{}"
elif isinstance(arguments, dict):
arguments = json.dumps(arguments)
new_tool_call = ChatCompletionMessageToolCallParam(
id=func_call["call_id"],
type="function",
function={
"name": func_call["name"],
"arguments": func_call["arguments"],
"arguments": arguments, # Use sanitized arguments
},
)
tool_calls.append(new_tool_call)
@ -2270,64 +2385,64 @@ class _Converter:
# Display the tool output immediately with the matched tool call
from cai.util import cli_print_tool_output
# Check if we're in streaming mode - don't show tool output panel in streaming mode
# Look up the original tool call to get the name and arguments
tool_name = "Unknown Tool"
tool_args = {}
execution_info = {}
if hasattr(cls, 'recent_tool_calls') and call_id in cls.recent_tool_calls:
tool_call = cls.recent_tool_calls[call_id]
tool_name = tool_call.get('name', 'Unknown Tool')
tool_args = tool_call.get('arguments', {})
execution_info = tool_call.get('execution_info', {})
# Get token counts from the OpenAIChatCompletionsModel if available
model_instance = None
for frame in inspect.stack():
if 'self' in frame.frame.f_locals:
self_obj = frame.frame.f_locals['self']
if isinstance(self_obj, OpenAIChatCompletionsModel):
model_instance = self_obj
break
# Always create a token_info dictionary, even if some values are zero
token_info = {
'interaction_input_tokens': getattr(model_instance, 'interaction_input_tokens', 0),
'interaction_output_tokens': getattr(model_instance, 'interaction_output_tokens', 0),
'interaction_reasoning_tokens': getattr(model_instance, 'interaction_reasoning_tokens', 0),
'total_input_tokens': getattr(model_instance, 'total_input_tokens', 0),
'total_output_tokens': getattr(model_instance, 'total_output_tokens', 0),
'total_reasoning_tokens': getattr(model_instance, 'total_reasoning_tokens', 0),
'model': str(getattr(model_instance, 'model', '')),
}
# Calculate costs using standard cost model
if model_instance and hasattr(model_instance, 'model'):
from cai.util import calculate_model_cost
model_name = str(model_instance.model)
token_info['interaction_cost'] = calculate_model_cost(
model_name,
token_info['interaction_input_tokens'],
token_info['interaction_output_tokens']
)
token_info['total_cost'] = calculate_model_cost(
model_name,
token_info['total_input_tokens'],
token_info['total_output_tokens']
)
# Check if we're in streaming mode
is_streaming_enabled = os.environ.get('CAI_STREAM', 'false').lower() == 'true'
if is_streaming_enabled:
# Don't display tool output in streaming mode - it will be handled elsewhere
pass # Just skip the display, but preserve the tool output
else:
# For non-streaming mode, maintain the original behavior
# Look up the original tool call to get the name and arguments
if hasattr(cls, 'recent_tool_calls') and call_id in cls.recent_tool_calls:
tool_call = cls.recent_tool_calls[call_id]
tool_name = tool_call.get('name', 'Unknown Tool')
tool_args = tool_call.get('arguments', {})
execution_info = tool_call.get('execution_info', {})
# Get token counts from the OpenAIChatCompletionsModel if available
model_instance = None
for frame in inspect.stack():
if 'self' in frame.frame.f_locals:
self_obj = frame.frame.f_locals['self']
if isinstance(self_obj, OpenAIChatCompletionsModel):
model_instance = self_obj
break
# Always create a token_info dictionary, even if some values are zero
token_info = {
'interaction_input_tokens': getattr(model_instance, 'interaction_input_tokens', 0),
'interaction_output_tokens': getattr(model_instance, 'interaction_output_tokens', 0),
'interaction_reasoning_tokens': getattr(model_instance, 'interaction_reasoning_tokens', 0),
'total_input_tokens': getattr(model_instance, 'total_input_tokens', 0),
'total_output_tokens': getattr(model_instance, 'total_output_tokens', 0),
'total_reasoning_tokens': getattr(model_instance, 'total_reasoning_tokens', 0),
'model': str(getattr(model_instance, 'model', '')),
}
# Calculate costs using standard cost model
if model_instance and hasattr(model_instance, 'model'):
from cai.util import calculate_model_cost
model_name = str(model_instance.model)
token_info['interaction_cost'] = calculate_model_cost(
model_name,
token_info['interaction_input_tokens'],
token_info['interaction_output_tokens']
)
token_info['total_cost'] = calculate_model_cost(
model_name,
token_info['total_input_tokens'],
token_info['total_output_tokens']
)
# Use the cli_print_tool_output function with actual token values
cli_print_tool_output(
tool_name=tool_name,
args=tool_args,
output=output_content,
call_id=call_id, # Keep call_id for non-streaming mode
execution_info=execution_info,
token_info=token_info
)
# Always display tool output regardless of streaming mode
cli_print_tool_output(
tool_name=tool_name,
args=tool_args,
output=output_content,
call_id=call_id,
execution_info=execution_info,
token_info=token_info
)
# Continue with normal processing
flush_assistant_message()

View File

@ -13,7 +13,7 @@ import uuid
import sys
import shlex
from wasabi import color # pylint: disable=import-error
from cai.util import format_time, start_active_timer, stop_active_timer, start_idle_timer, stop_idle_timer
from cai.util import format_time, start_active_timer, stop_active_timer, start_idle_timer, stop_idle_timer, cli_print_tool_output
# Instead of direct import
@ -436,232 +436,61 @@ def _run_ssh(command, stdout=False, timeout=100, workspace_dir=None):
def _run_local(command, stdout=False, timeout=100, stream=False, call_id=None, tool_name=None, workspace_dir=None):
"""Runs command locally in the specified workspace_dir."""
# If streaming is enabled and we have a call_id
if stream and call_id:
return _run_local_streamed(command, call_id, timeout, tool_name, workspace_dir)
# Make sure we're in active time mode for tool execution
stop_idle_timer()
start_active_timer()
process_start_time = time.time() # Initialize with current time
try:
target_dir = workspace_dir or _get_workspace_dir()
original_cmd_for_msg = command # For logging
context_msg = f"(local:{target_dir})"
try:
result = subprocess.run(
command,
shell=True, # nosec B602
capture_output=True,
text=True,
check=False,
timeout=timeout,
cwd=target_dir
)
output = result.stdout if result.stdout else result.stderr
if stdout:
print(f"\033[32m{context_msg} $ {original_cmd_for_msg}\n{output}\033[0m") # noqa E501
# Skip passing output to cli_print_tool_output when CAI_STREAM=true
# This prevents duplicate output in streaming mode
is_streaming_enabled = os.getenv('CAI_STREAM', 'false').lower() == 'true'
if not is_streaming_enabled:
# Optional: Add cli_print_tool_output call here if needed for non-streaming
pass
return output.strip()
except subprocess.TimeoutExpired as e:
error_output = e.stdout if e.stdout else str(e)
if stdout:
print("\033[32m" + error_output + "\033[0m")
return error_output
except Exception as e: # pylint: disable=broad-except
error_msg = f"Error executing local command: {e}"
print(color(error_msg, fg="red"))
return error_msg
finally:
# Always switch back to idle mode when function completes
stop_active_timer()
start_idle_timer()
def _run_local_streamed(command, call_id, timeout=100, tool_name=None, workspace_dir=None):
"""Run a local command with streaming output to the Tool output panel."""
# Make sure we're in active time mode for tool execution
stop_idle_timer()
start_active_timer()
target_dir = workspace_dir or _get_workspace_dir()
try:
# Try to import Rich for nice display
try:
from rich.console import Console
from rich.live import Live
from rich.panel import Panel
from rich.text import Text
from rich.box import ROUNDED
console = Console()
rich_available = True
except ImportError:
rich_available = False
from cai.util import cli_print_tool_output
# If streaming is enabled and we have a call_id
if stream:
# Import the streaming utilities from util
from cai.util import start_tool_streaming, update_tool_streaming, finish_tool_streaming
output_buffer = []
start_time = time.time()
# Start the process
process = subprocess.Popen(
command,
shell=True, # nosec B602
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
bufsize=1,
cwd=target_dir # Set CWD for local process
)
# If tool_name is not provided, derive it from the command
if tool_name is None:
# Just use the first command as the tool name
tool_name = command.strip().split()[0] + "_command"
# Create panel content for Rich display
if rich_available:
# Parse command into command and args
# Parse command into parts for display
parts = command.strip().split(' ', 1)
cmd = parts[0] if parts else ""
args = parts[1] if len(parts) > 1 else ""
args_param_val = parts[1] if len(parts) > 1 else "" # Renamed to avoid conflict with tool_args dict key
# Format clean arguments, following the same rules as cli_print_tool_output
arg_parts = []
if cmd:
arg_parts.append(f"command={cmd}")
if args and args.strip(): # Only add args if non-empty
arg_parts.append(f"args={args}")
args_str = ", ".join(arg_parts)
# For generic Linux commands, standardize the tool_name format
if not tool_name:
tool_name = f"{cmd}_command" if cmd else "command"
header = Text()
header.append(tool_name, style="#00BCD4")
header.append("(", style="yellow")
header.append(args_str, style="yellow")
header.append(")", style="yellow")
tool_time = 0
total_time = 0
if START_TIME is not None:
total_time = time.time() - START_TIME
timing_info = []
if total_time:
timing_info.append(f"Total: {format_time(total_time)}")
if tool_time:
timing_info.append(f"Tool: {format_time(tool_time)}")
if timing_info:
header.append(f" [{' | '.join(timing_info)}]", style="cyan")
content = Text()
panel = Panel(
Text.assemble(header, "\n\n", content),
title="[bold green]Tool Execution[/bold green]",
subtitle="[bold green]Live Output[/bold green]",
border_style="green",
padding=(1, 2),
box=ROUNDED
)
# Start Live display
with Live(panel, console=console, refresh_per_second=4) as live:
# Stream stdout in real-time
start_time = time.time()
for line in iter(process.stdout.readline, ''):
if not line:
break
# Add to output collection
output_buffer.append(line)
# Update content with new line
content.append(line, style="bright_white")
# Update tool_time and header with new timing info
tool_time = time.time() - start_time
total_time = 0
if START_TIME is not None:
total_time = time.time() - START_TIME
# Remove any previous timing info from header (rebuild header)
timing_info = []
if total_time:
timing_info.append(f"Total: {format_time(total_time)}")
if tool_time:
timing_info.append(f"Tool: {format_time(tool_time)}")
# Rebuild header to update timing
header = Text()
header.append(tool_name, style="#00BCD4")
header.append("(", style="yellow")
header.append(args_str, style="yellow")
header.append(")", style="yellow")
if timing_info:
header.append(f" [{' | '.join(timing_info)}]", style="cyan")
panel = Panel(
Text.assemble(header, "\n\n", content),
title="[bold green]Tool Execution[/bold green]",
subtitle="[bold green]Live Output[/bold green]",
border_style="green",
padding=(1, 2),
box=ROUNDED
)
live.update(panel)
# Check if process is done
process.stdout.close()
return_code = process.wait(timeout=timeout)
# Get any stderr output
stderr_data = process.stderr.read()
if stderr_data:
content.append("\nERROR OUTPUT:\n", style="red")
content.append(stderr_data, style="red")
output_buffer.append("\nERROR OUTPUT:\n" + stderr_data)
panel = Panel(
Text.assemble(header, "\n\n", content),
title="[bold green]Tool Execution[/bold green]",
subtitle="[bold green]Live Output[/bold green]",
border_style="green",
padding=(1, 2),
box=ROUNDED
)
live.update(panel)
# Add completion message
panel = Panel(
Text.assemble(header, "\n\n", content),
title="[bold green]Tool Execution[/bold green]",
border_style="green",
padding=(1, 2),
box=ROUNDED
)
live.update(panel)
# Wait a moment for the panel to be displayed properly
time.sleep(0.5)
else:
# Fallback to simpler streaming with cli_print_tool_output
# Parse command into command and args (same as rich mode)
parts = command.strip().split(' ', 1)
cmd = parts[0] if parts else ""
args = parts[1] if len(parts) > 1 else ""
# Create a dictionary with only non-empty values (following the same rules)
# Create args dictionary with non-empty values only
tool_args = {}
if cmd:
tool_args["command"] = cmd
if args and args.strip():
tool_args["args"] = args
# Note: Omitted empty values and async_mode=False as it's default
if args_param_val and args_param_val.strip():
tool_args["args"] = args_param_val
# Initial notification - just once
cli_print_tool_output(tool_name, tool_args, "Command started...", call_id=call_id)
# Add more context for the command
tool_args["workspace"] = os.path.basename(target_dir)
tool_args["full_command"] = command
# Buffer for collecting output
# For generic commands, ensure we have a unique call_id
if not call_id:
call_id = f"cmd_{cmd}_{str(uuid.uuid4())[:8]}"
# Initialize/use the call_id for this streaming session
call_id = start_tool_streaming(tool_name, tool_args, call_id)
# Start the process
process = subprocess.Popen(
command,
shell=True, # nosec B602
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
bufsize=1,
cwd=target_dir
)
# Begin collecting output
output_buffer = []
buffer_size = 0
update_interval = 10 # lines
@ -674,52 +503,133 @@ def _run_local_streamed(command, call_id, timeout=100, tool_name=None, workspace
output_buffer.append(line)
buffer_size += 1
# Only update the output periodically to reduce panel refresh rate
# Only update periodically to reduce UI refreshes
if buffer_size >= update_interval:
current_output = ''.join(output_buffer)
cli_print_tool_output(tool_name, tool_args, current_output, call_id=call_id)
update_tool_streaming(tool_name, tool_args, current_output, call_id)
buffer_size = 0
# Check if process is done
# Finish process
process.stdout.close()
return_code = process.wait(timeout=timeout)
process_execution_time = time.time() - process_start_time
# Get any stderr output
stderr_data = process.stderr.read()
if stderr_data:
output_buffer.append("\nERROR OUTPUT:\n" + stderr_data)
# Final output update - always show the final result
# Final output update
final_output = ''.join(output_buffer)
if return_code != 0:
final_output += f"\nCommand exited with code {return_code}"
cli_print_tool_output(tool_name, tool_args, final_output, call_id=call_id)
# Return the full output
return ''.join(output_buffer)
except subprocess.TimeoutExpired:
error_msg = f"Command timed out after {timeout} seconds"
output_buffer.append("\n" + error_msg)
final_output = ''.join(output_buffer)
# Update tool output panel with timeout message
if not rich_available:
tool_args = {"command": command}
cli_print_tool_output(tool_name, tool_args, final_output, call_id=call_id)
return final_output
# Calculate execution info with environment details
execution_info = {
"status": "completed" if return_code == 0 else "error",
"return_code": return_code,
"environment": "Local",
"host": os.path.basename(target_dir),
"tool_time": process_execution_time
}
# Complete the streaming session with final output
finish_tool_streaming(tool_name, tool_args, final_output, call_id, execution_info)
return final_output
else:
# Standard non-streaming execution
result = subprocess.run(
command,
shell=True, # nosec B602
capture_output=True,
text=True,
check=False,
timeout=timeout,
cwd=target_dir
)
output = result.stdout if result.stdout else result.stderr
# If this is the same command that was recently streamed, don't display it again
# We'll create a similar tool_args structure to what streaming would use
parts = command.strip().split(' ', 1)
cmd = parts[0] if parts else ""
args_param_val = parts[1] if len(parts) > 1 else ""
# Generate a standard tool name for consistency with streaming
standard_tool_name = tool_name or (f"{cmd}_command" if cmd else "command")
# Calculate a consistent command key - must match the format used in cli_print_tool_output
command_key = f"{standard_tool_name}:{args_param_val}"
if hasattr(cli_print_tool_output, '_displayed_commands') and command_key in cli_print_tool_output._displayed_commands:
# Skip stdout display if already shown through streaming
return output.strip()
if stdout:
print(f"\033[32m{context_msg} $ {original_cmd_for_msg}\n{output}\033[0m")
return output.strip()
except subprocess.TimeoutExpired as e:
error_output = e.stdout if hasattr(e, 'stdout') and e.stdout else str(e)
error_msg = f"Command timed out after {timeout} seconds\n{error_output}"
# If we're streaming, show the timeout in the tool output panel
if stream and call_id:
from cai.util import finish_tool_streaming
# Parse the command the same way we did for streaming
parts = command.strip().split(' ', 1)
cmd = parts[0] if parts else ""
args = parts[1] if len(parts) > 1 else ""
# Ensure tool_args has complete information
tool_args = {
"command": cmd,
"args": args if args.strip() else "",
"full_command": command,
"environment": "Local",
"workspace": os.path.basename(target_dir)
}
execution_info = {
"status": "timeout",
"error": str(e),
"environment": "Local",
"host": os.path.basename(target_dir)
}
finish_tool_streaming(tool_name or f"{cmd}_command", tool_args, error_msg, call_id, execution_info)
if stdout:
print("\033[32m" + error_msg + "\033[0m")
return error_msg
except Exception as e: # pylint: disable=broad-except
error_msg = f"Error executing command: {str(e)}"
error_msg = f"Error executing local command: {e}"
# If we're streaming, show the error in the tool output panel
if stream and call_id:
from cai.util import finish_tool_streaming
# Parse the command the same way we did for streaming
parts = command.strip().split(' ', 1)
cmd = parts[0] if parts else ""
args = parts[1] if len(parts) > 1 else ""
# Ensure tool_args has complete information
tool_args = {
"command": cmd,
"args": args if args.strip() else "",
"full_command": command,
"environment": "Local",
"workspace": os.path.basename(target_dir)
}
execution_info = {
"status": "error",
"error": str(e),
"environment": "Local",
"host": os.path.basename(target_dir)
}
finish_tool_streaming(tool_name or f"{cmd}_command", tool_args, error_msg, call_id, execution_info)
print(color(error_msg, fg="red"))
# Update tool output panel with error message if simple streaming
if not rich_available:
tool_args = {"command": command}
cli_print_tool_output(tool_name, tool_args, error_msg, call_id=call_id)
return error_msg
finally:
# Always switch back to idle mode when function completes
@ -753,6 +663,20 @@ def run_command(command, ctf=None, stdout=False, # pylint: disable=too-many-arg
stop_idle_timer()
start_active_timer()
# Parse command into standard parts to ensure consistent naming
parts = command.strip().split(' ', 1)
cmd = parts[0] if parts else ""
args = parts[1] if len(parts) > 1 else ""
# Generate a call_id if we're streaming and one wasn't provided
# Use a more specific format that includes the command name for easier tracking
if not call_id and stream:
call_id = f"cmd_{cmd}_{str(uuid.uuid4())[:8]}"
# If no tool_name is provided, derive it from the command in a consistent way
if not tool_name:
tool_name = f"{cmd}_command" if cmd else "command"
try:
# If session_id is provided, send command to that session
if session_id:
@ -781,10 +705,6 @@ def run_command(command, ctf=None, stdout=False, # pylint: disable=too-many-arg
return result # Return the result of sending input ("Input sent..." or error)
# Generate a call_id if we're streaming and one wasn't provided
if stream and not call_id:
call_id = str(uuid.uuid4())[:8]
# 2. Determine Execution Environment (Container > CTF > SSH > Local)
active_container = os.getenv("CAI_ACTIVE_CONTAINER", "")
is_ssh_env = all(os.getenv(var) for var in ['SSH_USER', 'SSH_HOST'])
@ -817,6 +737,30 @@ def run_command(command, ctf=None, stdout=False, # pylint: disable=too-many-arg
# Handle Streaming Container Execution
if stream:
# Import the streaming utilities from util
from cai.util import start_tool_streaming, update_tool_streaming, finish_tool_streaming
# Create args dictionary with standardized format
tool_args = {
"command": cmd,
"args": args if args.strip() else "",
"full_command": command,
"container": container_id[:12],
"environment": "Container",
"workspace": container_workspace
}
# Initialize the streaming session with a consistent call_id format
call_id = start_tool_streaming(tool_name, tool_args, call_id)
# Update with "executing" status
update_tool_streaming(
tool_name,
tool_args,
f"Executing in container {container_id[:12]} at {container_workspace}:\n{command}\n\nPreparing environment...",
call_id
)
# Ensure workspace directory exists inside the container first
mkdir_cmd = [
"docker", "exec", container_id,
@ -829,6 +773,14 @@ def run_command(command, ctf=None, stdout=False, # pylint: disable=too-many-arg
check=False,
timeout=10
)
# Update status once environment is prepared
update_tool_streaming(
tool_name,
tool_args,
f"Executing in container {container_id[:12]} at {container_workspace}:\n{command}\n\nRunning command...",
call_id
)
# Build docker exec command as a single shell string for streaming
docker_exec_cmd = (
@ -837,20 +789,114 @@ def run_command(command, ctf=None, stdout=False, # pylint: disable=too-many-arg
f"{shlex.quote(container_id)} sh -c "
f"{shlex.quote(command)}"
)
# Re-use the local streaming helper to provide real-time output
result = _run_local_streamed(
docker_exec_cmd,
call_id,
timeout,
tool_name,
workspace_dir=_get_workspace_dir()
)
# Switch back to idle mode after streaming command completes
stop_active_timer()
start_idle_timer()
return result
try:
start_time = time.time()
# Start the process
process = subprocess.Popen(
docker_exec_cmd,
shell=True, # nosec B602
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
bufsize=1,
cwd=_get_workspace_dir()
)
# Begin collecting output
output_buffer = []
buffer_size = 0
update_interval = 10 # lines
# Stream stdout in real-time
for line in iter(process.stdout.readline, ''):
if not line:
break
# Add to output collection
output_buffer.append(line)
buffer_size += 1
# Only update periodically to reduce UI refreshes
if buffer_size >= update_interval:
current_output = ''.join(output_buffer)
update_tool_streaming(tool_name, tool_args, current_output, call_id)
buffer_size = 0
# Finish process
process.stdout.close()
return_code = process.wait(timeout=timeout)
execution_time = time.time() - start_time
# Get any stderr output
stderr_data = process.stderr.read()
if stderr_data:
output_buffer.append("\nERROR OUTPUT:\n" + stderr_data)
# Final output update
final_output = ''.join(output_buffer)
if return_code != 0:
final_output += f"\nCommand exited with code {return_code}"
# Calculate execution info
execution_info = {
"status": "completed" if return_code == 0 else "error",
"return_code": return_code,
"environment": "Container",
"host": container_id[:12],
"tool_time": execution_time
}
# Complete the streaming session with final output
finish_tool_streaming(tool_name, tool_args, final_output, call_id, execution_info)
# Switch back to idle mode after streaming command completes
stop_active_timer()
start_idle_timer()
return final_output
except subprocess.TimeoutExpired as e:
# Handle timeout
error_output = e.stdout if hasattr(e, 'stdout') and e.stdout else str(e)
error_msg = f"Command timed out after {timeout} seconds\n{error_output}"
execution_info = {
"status": "timeout",
"environment": "Container",
"host": container_id[:12],
"error": str(e)
}
# Complete with timeout error
finish_tool_streaming(tool_name, tool_args, error_msg, call_id, execution_info)
# Switch back to idle mode after timeout
stop_active_timer()
start_idle_timer()
# Fallback to local execution on timeout
print(color("Container execution timed out. Attempting execution on host instead.", fg="yellow"))
return _run_local(command, stdout, timeout, False, None, tool_name, _get_workspace_dir())
except Exception as e:
# Handle other errors
error_msg = f"Error executing command in container: {str(e)}"
execution_info = {
"status": "error",
"environment": "Container",
"host": container_id[:12],
"error": str(e)
}
# Complete with error
finish_tool_streaming(tool_name, tool_args, error_msg, call_id, execution_info)
# Switch back to idle mode after error
stop_active_timer()
start_idle_timer()
# Fallback to local execution on error
print(color("Container execution failed. Attempting execution on host instead.", fg="yellow"))
return _run_local(command, stdout, timeout, False, None, tool_name, _get_workspace_dir())
# Handle Synchronous Execution in Container
try:
@ -916,54 +962,204 @@ def run_command(command, ctf=None, stdout=False, # pylint: disable=too-many-arg
# --- CTF Execution ---
if ctf:
# Handling streaming for CTF - not fully implemented yet
# If streaming is enabled and we have a call_id, show streaming UI for CTF too
if stream:
from cai.util import cli_print_tool_output
if call_id and tool_name:
tool_args = {"command": command, "ctf": True}
cli_print_tool_output(
tool_name,
tool_args,
"Streaming not yet supported for CTF execution. Running normally...",
call_id=call_id
)
# Import the streaming utilities from util
from cai.util import start_tool_streaming, update_tool_streaming, finish_tool_streaming
# Create args dictionary with standardized format
tool_args = {
"command": cmd,
"args": args if args.strip() else "",
"full_command": command,
"environment": "CTF",
"workspace": os.path.basename(_get_workspace_dir())
}
# Initialize the streaming session with a consistent call_id format
call_id = start_tool_streaming(tool_name, tool_args, call_id)
target_dir = _get_workspace_dir()
full_command = f"cd '{target_dir}' && {command}"
# Update with "executing" status
update_tool_streaming(
tool_name,
tool_args,
f"Executing in CTF environment: {full_command}\n\nWaiting for response...",
call_id
)
try:
# Execute the command and get the output
start_time = time.time()
output = ctf.get_shell(full_command, timeout=timeout)
execution_time = time.time() - start_time
# Calculate execution info
execution_info = {
"status": "completed",
"environment": "CTF",
"tool_time": execution_time
}
# Complete the streaming with final output
finish_tool_streaming(tool_name, tool_args, output, call_id, execution_info)
# Switch back to idle mode after CTF command completes
stop_active_timer()
start_idle_timer()
return output
except Exception as e:
# Handle errors in CTF execution
error_msg = f"Error executing CTF command: {str(e)}"
execution_info = {
"status": "error",
"environment": "CTF",
"error": str(e)
}
# Complete the streaming with error output
finish_tool_streaming(tool_name, tool_args, error_msg, call_id, execution_info)
# Switch back to idle mode after error
stop_active_timer()
start_idle_timer()
return error_msg
else:
# Standard non-streaming CTF execution
result = _run_ctf(ctf, command, stdout, timeout) # Pass None for workspace_dir
# _run_ctf handles workspace internally using _get_workspace_dir() default
result = _run_ctf(ctf, command, stdout, timeout) # Pass None for workspace_dir
# Switch back to idle mode after CTF command completes
stop_active_timer()
start_idle_timer()
return result
# Switch back to idle mode after CTF command completes
stop_active_timer()
start_idle_timer()
return result
# --- SSH Execution ---
if is_ssh_env:
# Async for SSH would require session management via SSH client features
if async_mode:
# Switch back to idle mode before returning message
# If streaming is enabled, show streaming UI for SSH too
if stream:
# Import the streaming utilities from util
from cai.util import start_tool_streaming, update_tool_streaming, finish_tool_streaming
# Add SSH connection info for display
ssh_user = os.environ.get('SSH_USER', 'user')
ssh_host = os.environ.get('SSH_HOST', 'host')
ssh_connection = f"{ssh_user}@{ssh_host}"
# Create args dictionary with standardized format
tool_args = {
"command": cmd,
"args": args if args.strip() else "",
"full_command": command,
"ssh_host": ssh_connection,
"environment": "SSH"
}
# Initialize streaming session with a consistent call_id format
call_id = start_tool_streaming(tool_name, tool_args, call_id)
# Update with "executing" status
update_tool_streaming(
tool_name,
tool_args,
f"Executing on {ssh_connection}: {command}\n\nWaiting for response...",
call_id
)
try:
# Construct SSH command for execution
ssh_pass = os.environ.get('SSH_PASS')
if ssh_pass:
ssh_cmd_list = ["sshpass", "-p", ssh_pass, "ssh", ssh_connection]
else:
ssh_cmd_list = ["ssh", ssh_connection]
ssh_cmd_list.append(command)
# Execute the command and get the output
start_time = time.time()
result = subprocess.run(
ssh_cmd_list,
capture_output=True,
text=True,
check=False,
timeout=timeout
)
execution_time = time.time() - start_time
# Get command output
output = result.stdout if result.stdout else result.stderr
# Add SSH connection info to the output for clarity
result_with_info = f"Command executed on {ssh_connection}:\n\n{output}"
# Determine status based on return code
status = "completed" if result.returncode == 0 else "error"
# Calculate execution info
execution_info = {
"status": status,
"environment": "SSH",
"host": ssh_connection,
"return_code": result.returncode,
"tool_time": execution_time
}
# Complete the streaming with final output
finish_tool_streaming(tool_name, tool_args, result_with_info, call_id, execution_info)
# Switch back to idle mode after SSH command completes
stop_active_timer()
start_idle_timer()
return output.strip()
except subprocess.TimeoutExpired as e:
# Handle timeout errors
error_output = e.stdout if e.stdout else str(e)
error_msg = f"Command timed out after {timeout} seconds\n{error_output}"
execution_info = {
"status": "timeout",
"environment": "SSH",
"host": ssh_connection,
"error": str(e)
}
# Complete the streaming with timeout error
finish_tool_streaming(tool_name, tool_args, error_msg, call_id, execution_info)
# Switch back to idle mode after timeout
stop_active_timer()
start_idle_timer()
return error_msg
except Exception as e:
# Handle other errors
error_msg = f"Error executing SSH command: {str(e)}"
execution_info = {
"status": "error",
"environment": "SSH",
"host": ssh_connection,
"error": str(e)
}
# Complete the streaming with error
finish_tool_streaming(tool_name, tool_args, error_msg, call_id, execution_info)
# Switch back to idle mode after error
stop_active_timer()
start_idle_timer()
return error_msg
else:
# Standard non-streaming SSH execution
result = _run_ssh(command, stdout, timeout) # Workspace dir less relevant here
# Switch back to idle mode after SSH command completes
stop_active_timer()
start_idle_timer()
return "Async mode not fully supported for SSH environment via this function yet."
# Handling streaming for SSH - not fully implemented yet
if stream:
from cai.util import cli_print_tool_output
if call_id and tool_name:
tool_args = {"command": command, "ssh": True}
cli_print_tool_output(
tool_name,
tool_args,
"Streaming not yet supported for SSH execution. Running normally...",
call_id=call_id
)
# _run_ssh handles command execution, workspace is relative to remote home
result = _run_ssh(command, stdout, timeout) # Workspace dir less relevant here
# Switch back to idle mode after SSH command completes
stop_active_timer()
start_idle_timer()
return result
return result
# --- Local Execution (Default Fallback) ---
# Let _run_local handle determining the host workspace
@ -989,8 +1185,16 @@ def run_command(command, ctf=None, stdout=False, # pylint: disable=too-many-arg
start_idle_timer()
return f"Started async session {new_session_id} locally. Use this ID to interact."
# Handle Synchronous Execution Locally using _run_local default with streaming support
result = _run_local(command, stdout, timeout, stream, call_id, tool_name, None)
# Handle Synchronous Execution Locally
# Pass stream=True if we're streaming to use streaming functionality
result = _run_local(
command,
stdout,
timeout,
stream=stream,
call_id=call_id,
tool_name=tool_name
)
# Switch back to idle mode after local command completes
stop_active_timer()

File diff suppressed because it is too large Load Diff

26
terrapin_check.py Normal file
View File

@ -0,0 +1,26 @@
#!/usr/bin/env python3
import socket
import ssl
import sys
def check_terrapin(hostname, port=443):
try:
context = ssl.create_default_context()
with socket.create_connection((hostname, port), timeout=10) as sock:
with context.wrap_socket(sock, server_hostname=hostname) as ssock:
cipher = ssock.cipher()
print(f"Connected to {hostname}:{port}")
print(f"Cipher: {cipher}")
# Check for server features that might indicate Terrapin vulnerability
# Note: This is a simplified check and not a complete test
if cipher[0].startswith(('TLS_RSA_', 'RSA-')):
print(f"[!] Potentially vulnerable to Terrapin: {hostname} uses RSA key exchange")
else:
print(f"[+] Not likely vulnerable to Terrapin: {hostname} doesn't use RSA key exchange")
except Exception as e:
print(f"Error connecting to {hostname}:{port} - {str(e)}")
if __name__ == "__main__":
check_terrapin("aliasrobotics.com")

32
terrapin_test.py Normal file
View File

@ -0,0 +1,32 @@
#!/usr/bin/env python3
import socket
import ssl
import sys
import re
def test_terrapin(hostname, port=443):
context = ssl.create_default_context()
print(f"Testing {hostname}:{port} for Terrapin vulnerability...")
try:
with socket.create_connection((hostname, port), timeout=10) as sock:
with context.wrap_socket(sock, server_hostname=hostname) as ssock:
# Get cipher info
cipher = ssock.cipher()
print(f"Connected using: {cipher}")
# Check if using CBC cipher (vulnerable to Terrapin)
if "CBC" in cipher[0]:
print("WARNING: Server is using CBC cipher which may be vulnerable to Terrapin attacks!")
return True
else:
print("Server is not using CBC ciphers, likely not vulnerable to Terrapin.")
return False
except Exception as e:
print(f"Error: {e}")
return None
if __name__ == "__main__":
hostname = "aliasrobotics.com"
test_terrapin(hostname)