ssh tool WIP: sudo not working
This commit is contained in:
@@ -1,19 +1,21 @@
|
||||
# Simple ReAct Agent for Log Analysis
|
||||
# Simple ReAct Agent for System Administration
|
||||
|
||||
This directory contains a simple ReAct (Reasoning and Acting) agent implementation for log analysis and system administration tasks.
|
||||
This directory contains a simple ReAct (Reasoning and Acting) agent implementation for system administration, log analysis, and remote server management tasks.
|
||||
|
||||
## Overview
|
||||
|
||||
The simple ReAct agent follows a straightforward pattern:
|
||||
1. **Receives** user input
|
||||
2. **Reasons** about what tools to use
|
||||
3. **Acts** by executing tools when needed
|
||||
3. **Acts** by executing tools when needed (locally or remotely)
|
||||
4. **Responds** with the final result
|
||||
|
||||
## Features
|
||||
|
||||
- **Single Agent**: One agent handles all tasks
|
||||
- **Shell Access**: Execute system commands safely
|
||||
- **Local Shell Access**: Execute system commands on the local machine
|
||||
- **Remote SSH Access**: Execute commands on remote servers via persistent SSH connections
|
||||
- **System Administration**: Comprehensive system diagnostics and management
|
||||
- **Log Analysis**: Specialized log analysis capabilities
|
||||
- **Interactive Chat**: Stream responses with tool usage visibility
|
||||
- **Conversation History**: Maintains context across interactions
|
||||
@@ -21,7 +23,7 @@ The simple ReAct agent follows a straightforward pattern:
|
||||
## Architecture
|
||||
|
||||
```
|
||||
User Input → ReAct Agent → Tools (Shell + Log Analyzer) → Response
|
||||
User Input → ReAct Agent → Tools (Shell + SSH + Tools) → Response
|
||||
```
|
||||
|
||||
## Files
|
||||
@@ -31,12 +33,18 @@ User Input → ReAct Agent → Tools (Shell + Log Analyzer) → Response
|
||||
|
||||
## Tools Available
|
||||
|
||||
1. **Shell Tool**: Execute system commands
|
||||
1. **Shell Tool**: Execute system commands on the local machine
|
||||
- System monitoring (`top`, `ps`, `df`, etc.)
|
||||
- File operations
|
||||
- Network diagnostics
|
||||
|
||||
2. **Poem Tool**: Generate beautiful poems with different themes:
|
||||
2. **SSH Tool**: Execute commands on remote servers
|
||||
- Persistent SSH connections
|
||||
- Remote system administration
|
||||
- Cross-platform remote diagnostics
|
||||
- Secure remote command execution
|
||||
|
||||
3. **Poem Tool**: Generate beautiful poems with different themes:
|
||||
- `nature`: Poems about nature and the environment
|
||||
- `tech`: Poems about technology and programming
|
||||
- `motivational`: Inspirational and motivational poems
|
||||
@@ -62,6 +70,11 @@ User: Check disk usage on the system
|
||||
Agent: 🔧 Using tool: shell
|
||||
Args: {'command': 'df -h'}
|
||||
📋 Tool result: Filesystem usage information...
|
||||
|
||||
User: Connect to my remote server and check CPU usage
|
||||
Agent: 🔧 Using tool: configured_ssh_tool
|
||||
Args: {'command': 'top -bn1 | head -20'}
|
||||
📋 Tool result: Remote server CPU and process information...
|
||||
```
|
||||
|
||||
## Pros and Cons
|
||||
@@ -82,11 +95,12 @@ Agent: 🔧 Using tool: shell
|
||||
## When to Use
|
||||
|
||||
Choose the simple ReAct agent when:
|
||||
- You need a straightforward log analysis tool
|
||||
- You need a straightforward system administration tool
|
||||
- You want both local and remote server management capabilities
|
||||
- Your use cases are relatively simple
|
||||
- You want to understand LangGraph basics
|
||||
- Resource usage is a concern
|
||||
- You prefer simplicity over sophistication
|
||||
- You prefer simplicity over sophisticated multi-agent coordination
|
||||
|
||||
## Requirements
|
||||
|
||||
|
@@ -7,9 +7,10 @@ from langchain_community.tools.shell.tool import ShellTool
|
||||
# Pre-configured SSH tool for your server - only connects when actually used
|
||||
# TODO: Update these connection details for your actual server
|
||||
configured_ssh_tool = SSHTool(
|
||||
host="your-server.example.com", # Replace with your server
|
||||
username="admin", # Replace with your username
|
||||
key_filename="~/.ssh/id_rsa", # Replace with your key path
|
||||
host="157.90.211.119", # Replace with your server
|
||||
port=8081,
|
||||
username="g", # Replace with your username
|
||||
key_filename="/Users/ghsioux/.ssh/id_rsa_hetzner", # Replace with your key path
|
||||
ask_human_input=True # Safety confirmation
|
||||
)
|
||||
|
||||
|
@@ -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)}"
|
@@ -146,8 +146,10 @@ def main():
|
||||
print("Type 'quit', 'exit', or 'q' to exit the chat.")
|
||||
print("Type 'help' or 'h' for help and examples.")
|
||||
print("Type 'clear' or 'reset' to clear conversation history.")
|
||||
print("⚠️ WARNING: This agent has shell access - use with caution!")
|
||||
print("⚠️ WARNING: This agent has local shell and remote SSH access - use with caution!")
|
||||
print("🛠️ System Administration Capabilities:")
|
||||
print(" - Local system diagnostics via shell commands")
|
||||
print(" - Remote server management via SSH connections")
|
||||
print(" - Diagnose performance issues (CPU, memory, disk, network)")
|
||||
print(" - Troubleshoot service and daemon problems")
|
||||
print(" - Analyze system logs and error messages")
|
||||
@@ -162,6 +164,7 @@ def main():
|
||||
print("✅ SysAdmin Debugging Agent initialized successfully!")
|
||||
print("💡 Try asking: 'My system is running slow, can you help?'")
|
||||
print("💡 Or: 'Check if my web server is running properly'")
|
||||
print("💡 Or: 'Connect to my remote server and check disk space'")
|
||||
print("💡 Or: 'Analyze recent system errors'")
|
||||
print("💡 Need a break? Ask: 'Write me a motivational poem'")
|
||||
|
||||
@@ -187,7 +190,9 @@ def main():
|
||||
print("\nSystem Debugging Examples:")
|
||||
print(" - 'My server is running slow, help me diagnose the issue'")
|
||||
print(" - 'Check why my Apache/Nginx service won't start'")
|
||||
print(" - 'Connect to my remote server and check system load'")
|
||||
print(" - 'Analyze high CPU usage on this system'")
|
||||
print(" - 'Check disk space on my remote server via SSH'")
|
||||
print(" - 'Troubleshoot network connectivity problems'")
|
||||
print(" - 'Check disk space and filesystem health'")
|
||||
print(" - 'Review recent system errors in logs'")
|
||||
|
Reference in New Issue
Block a user