ssh tool WIP: sudo not working

This commit is contained in:
Gaetan Hurel
2025-06-29 15:15:27 +02:00
parent 38b77a657f
commit a81dd5484d
14 changed files with 299 additions and 886 deletions

View File

@@ -48,6 +48,12 @@ User Input → Supervisor → Specialized Agents → Aggregated Response
## Features
### Core Capabilities
- **Local System Access**: Execute shell commands on the local machine
- **Remote Server Access**: Execute commands on remote servers via SSH
- **Persistent SSH Connections**: Efficient remote operations with connection reuse
- **Cross-Platform Support**: Works on Linux, macOS, BSD, and Windows systems
### Advanced Capabilities
- **Intelligent Delegation**: Supervisor routes tasks to appropriate specialists
- **Parallel Execution**: Multiple agents can work simultaneously
@@ -95,13 +101,39 @@ for chunk in supervisor.stream(query):
For the query: *"Nginx returns 502 Bad Gateway on my server. What can I do?"*
1. **Supervisor** analyzes the request
2. **system_info_worker** checks system resources
2. **system_info_worker** checks system resources (local or remote)
3. **service_inventory_worker** lists running services
4. **nginx_analyzer** validates Nginx configuration and checks logs
5. **phpfpm_analyzer** checks PHP-FPM status (common 502 cause)
6. **risk_scorer** assesses the severity
7. **remediation_worker** proposes specific fixes
## Example Queries
The multi-agent system can handle both local and remote system administration:
### Local System Administration
```
"Check local system performance and identify bottlenecks"
"Analyze recent system errors in local logs"
"What services are running on this machine?"
```
### Remote Server Management
```
"Connect to my remote server and check disk usage"
"Compare performance between local and remote systems"
"Check if nginx is running on the remote server"
"Analyze logs on my remote server for error patterns"
```
### Multi-System Analysis
```
"Perform comprehensive health check across all systems"
"Compare configurations between local and remote servers"
"Identify performance differences between environments"
```
## Pros and Cons
### ✅ Pros

View File

@@ -2,7 +2,6 @@ import logging
import warnings
from typing import Any, List, Optional, Type, Union
from langchain_core.callbacks import CallbackManagerForToolRun
from langchain_core.tools import BaseTool
from pydantic import BaseModel, Field, model_validator
@@ -17,9 +16,20 @@ class SSHInput(BaseModel):
description="List of commands to run on the remote server",
)
"""List of commands to run."""
use_sudo: bool = Field(
default=False,
description="Whether to run commands with sudo privileges"
)
"""Whether to run commands with sudo."""
sudo_password: Optional[str] = Field(
default=None,
description="Password for sudo if required (will be used securely)"
)
"""Password for sudo if required."""
@model_validator(mode="before")
@classmethod
def _validate_commands(cls, values: dict) -> Any:
"""Validate commands."""
commands = values.get("commands")
@@ -34,39 +44,38 @@ class SSHInput(BaseModel):
class SSHProcess:
"""Persistent SSH connection for command execution."""
def __init__(self, host: str, username: str, port: int = 22,
password: Optional[str] = None, key_filename: Optional[str] = None,
timeout: float = 30.0, return_err_output: bool = True):
**kwargs):
"""Initialize SSH process with connection parameters."""
self.host = host
self.username = username
self.port = port
self.password = password
self.key_filename = key_filename
self.timeout = timeout
self.return_err_output = return_err_output
self.client = None
# Don't connect immediately - connect when needed
def _connect(self):
self._is_connected = False
def connect(self):
"""Establish SSH connection."""
if self._is_connected:
return
try:
import paramiko
except ImportError:
except ImportError as e:
raise ImportError(
"paramiko is required for SSH functionality. "
"Install it with `pip install paramiko`"
)
) from e
self.client = paramiko.SSHClient()
self.client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
connect_kwargs = {
"hostname": self.host,
"username": self.username,
"port": self.port,
"timeout": self.timeout,
"username": self.username,
}
if self.password:
@@ -75,12 +84,14 @@ class SSHProcess:
connect_kwargs["key_filename"] = self.key_filename
self.client.connect(**connect_kwargs)
self._is_connected = True
logger.info(f"SSH connection established to {self.username}@{self.host}:{self.port}")
def run(self, commands: Union[str, List[str]]) -> str:
def run(self, commands: Union[str, List[str]], use_sudo: bool = False,
sudo_password: Optional[str] = None) -> str:
"""Run commands over SSH and return output."""
if not self.client:
self._connect()
if not self._is_connected:
self.connect()
if isinstance(commands, str):
commands = [commands]
@@ -88,23 +99,48 @@ class SSHProcess:
outputs = []
for command in commands:
try:
stdin, stdout, stderr = self.client.exec_command(command)
# Prepare command with sudo if needed
if use_sudo:
if sudo_password:
# Use echo to pipe password to sudo -S (read from stdin)
full_command = f"echo '{sudo_password}' | sudo -S {command}"
else:
# Try sudo without password (for passwordless sudo)
full_command = f"sudo {command}"
else:
full_command = command
output = stdout.read().decode('utf-8')
error = stderr.read().decode('utf-8')
stdin, stdout, stderr = self.client.exec_command(full_command)
if error and self.return_err_output:
# For sudo commands with password, we need to handle stdin
if use_sudo and sudo_password:
stdin.write(f"{sudo_password}\n")
stdin.flush()
output = stdout.read().decode()
error = stderr.read().decode()
# Filter out sudo password prompt from error output
if use_sudo and error:
error_lines = error.split('\n')
filtered_error = '\n'.join(
line for line in error_lines
if not line.startswith('[sudo]') and line.strip()
)
error = filtered_error
if error:
outputs.append(f"$ {command}\n{output}{error}")
else:
outputs.append(f"$ {command}\n{output}")
except Exception as e:
outputs.append(f"$ {command}\nError: {str(e)}")
return "\n".join(outputs)
return "\n\n".join(outputs)
def __del__(self):
"""Close SSH connection when object is destroyed."""
if self.client:
if self.client and self._is_connected:
self.client.close()
logger.info(f"SSH connection closed to {self.username}@{self.host}")
@@ -119,34 +155,41 @@ class SSHTool(BaseTool):
process: Optional[SSHProcess] = Field(default=None)
"""SSH process with persistent connection."""
# Connection parameters
host: str = Field(..., description="SSH host address")
username: str = Field(..., description="SSH username")
port: int = Field(default=22, description="SSH port")
password: Optional[str] = Field(default=None, description="SSH password")
key_filename: Optional[str] = Field(default=None, description="Path to SSH key")
timeout: float = Field(default=30.0, description="Connection timeout")
# Tool configuration
name: str = "ssh"
"""Name of tool."""
description: str = Field(default="")
"""Description of tool."""
description: str = """
Run shell commands on a remote server via SSH.
This tool maintains a persistent SSH connection and allows executing
commands on the remote server. It supports both regular and privileged
(sudo) command execution.
Use the 'use_sudo' parameter to run commands with sudo privileges.
If sudo requires a password, provide it via 'sudo_password'.
Examples:
- Regular command: {"commands": "ls -la"}
- Sudo command: {"commands": "apt update", "use_sudo": true}
- Multiple commands: {"commands": ["df -h", "free -m", "top -n 1"]}
"""
args_schema: Type[BaseModel] = SSHInput
"""Schema for input arguments."""
model_config = {
"arbitrary_types_allowed": True
}
ask_human_input: bool = False
"""
If True, prompts the user for confirmation (y/n) before executing
commands on the remote server.
"""
def __init__(self, **data):
def __init__(self, **kwargs):
"""Initialize SSH tool and set description."""
super().__init__(**data)
# Set description after initialization
super().__init__(**kwargs)
self.description = f"Run commands on remote server {self.username}@{self.host}:{self.port}"
# Initialize the SSH process (but don't connect yet)
self.process = SSHProcess(
@@ -154,32 +197,38 @@ class SSHTool(BaseTool):
username=self.username,
port=self.port,
password=self.password,
key_filename=self.key_filename,
timeout=self.timeout,
return_err_output=True
key_filename=self.key_filename
)
def _run(
self,
commands: Union[str, List[str]],
run_manager: Optional[CallbackManagerForToolRun] = None,
use_sudo: bool = False,
sudo_password: Optional[str] = None,
**kwargs,
) -> str:
"""Run commands on remote server and return output."""
print(f"Executing SSH command on {self.username}@{self.host}:{self.port}") # noqa: T201
print(f"Commands: {commands}") # noqa: T201
try:
if self.ask_human_input:
print(f"Executing SSH command on {self.username}@{self.host}:{self.port}") # noqa: T201
print(f"Commands: {commands}") # noqa: T201
if use_sudo:
print("Running with sudo privileges") # noqa: T201
# Safety check for privileged commands
if use_sudo:
user_input = input("Proceed with sudo command execution? (y/n): ").lower()
if user_input == "y":
return self.process.run(commands, use_sudo=use_sudo, sudo_password=sudo_password)
else:
logger.info("User aborted sudo command execution.")
return "Command execution aborted by user."
else:
user_input = input("Proceed with SSH command execution? (y/n): ").lower()
if user_input == "y":
return self.process.run(commands)
else:
logger.info("Invalid input. User aborted SSH command execution.")
return None # type: ignore[return-value]
else:
return self.process.run(commands)
return "Command execution aborted by user."
except Exception as e:
logger.error(f"Error during SSH command execution: {e}")
return None # type: ignore[return-value]
return f"Error: {str(e)}"

View File

@@ -26,10 +26,13 @@ def print_welcome():
print("🤖 Welcome to Pard0x Multi-Agent System Administrator Assistant!")
print("="*80)
print("\nI coordinate a team of specialized agents to help you with system administration tasks:")
print(" • 🖥️ OS Detector - System identification and environment analysis")
print(" • 📊 Logs Analyzer - Log investigation and error diagnosis")
print(" • ⚡ Performance Analyzer - Resource monitoring and optimization")
print(" • 🖥️ OS Detector - System identification and environment analysis (local & remote)")
print(" • 📊 Logs Analyzer - Log investigation and error diagnosis (local & remote)")
print(" • ⚡ Performance Analyzer - Resource monitoring and optimization (local & remote)")
print(" • 🎭 Morale Booster - Motivational poems for tough debugging sessions!")
print("\n🌐 Remote Server Access: My agents can execute commands on both:")
print(" • Local machine via shell commands")
print(" • Remote server via SSH (g@157.90.211.119:8081)")
print("\n" + "-"*80)
@@ -39,6 +42,8 @@ def print_examples():
print(" - 'What operating system is this server running?'")
print(" - 'Check the system logs for any errors in the last hour'")
print(" - 'Analyze current system performance and identify bottlenecks'")
print(" - 'Compare disk usage between local and remote server'")
print(" - 'Check if services are running on both systems'")
print(" - 'My web server is down, help me troubleshoot'")
print(" - 'Write me a motivational poem about debugging'")
print("\n" + "-"*80)