From 5818644c1a2dfe75d6f57d11e5718b40920258b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Kr=C3=BCger?= Date: Sat, 8 Nov 2025 23:07:11 +0100 Subject: [PATCH] feat: add SFTP integration for saving code to local disk MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added custom Open WebUI function for SSH/SFTP file operations: **New Function: save_to_disk.py** - save_file(): Write generated code to local filesystem via SFTP - read_file(): Read files from local disk - list_files(): List directory contents - Configurable via Valves (host, port, username, paths) **Custom Dockerfile (Dockerfile.webui)** - Based on ghcr.io/open-webui/open-webui:main - Installs paramiko library for SSH/SFTP support - Creates .ssh directory for key storage **Configuration Updates** - Mount SSH private key from host (/root/.ssh/id_rsa) - Mount functions directory for custom tools - Build custom image with SFTP capabilities **Usage in Open WebUI** Claude can now use these tools to: - Generate code and save it directly to your local disk - Read existing files for context - List project directories - Create new files in any project Default base path: /home/valknar/Projects Authentication: SSH key-based (passwordless) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- ai/Dockerfile.webui | 7 ++ ai/compose.yaml | 7 +- ai/functions/save_to_disk.py | 195 +++++++++++++++++++++++++++++++++++ 3 files changed, 208 insertions(+), 1 deletion(-) create mode 100644 ai/Dockerfile.webui create mode 100644 ai/functions/save_to_disk.py diff --git a/ai/Dockerfile.webui b/ai/Dockerfile.webui new file mode 100644 index 0000000..ca306f7 --- /dev/null +++ b/ai/Dockerfile.webui @@ -0,0 +1,7 @@ +FROM ghcr.io/open-webui/open-webui:main + +# Install paramiko for SFTP functionality +RUN pip install --no-cache-dir paramiko + +# Create .ssh directory +RUN mkdir -p /app/.ssh && chmod 700 /app/.ssh diff --git a/ai/compose.yaml b/ai/compose.yaml index 4df7f7d..acf959d 100644 --- a/ai/compose.yaml +++ b/ai/compose.yaml @@ -24,7 +24,10 @@ services: # Open WebUI - ChatGPT-like interface for AI models webui: - image: ${AI_WEBUI_IMAGE:-ghcr.io/open-webui/open-webui:main} + build: + context: . + dockerfile: ./ai/Dockerfile.webui + image: ${AI_WEBUI_IMAGE:-ai_webui_custom:latest} container_name: ${AI_COMPOSE_PROJECT_NAME}_webui restart: unless-stopped environment: @@ -63,6 +66,8 @@ services: volumes: - ai_webui_data:/app/backend/data + - ./ai/functions:/app/backend/data/functions:ro + - /root/.ssh/id_rsa:/app/.ssh/id_rsa:ro depends_on: - ai_postgres - litellm diff --git a/ai/functions/save_to_disk.py b/ai/functions/save_to_disk.py new file mode 100644 index 0000000..58e9c92 --- /dev/null +++ b/ai/functions/save_to_disk.py @@ -0,0 +1,195 @@ +""" +title: Save Code to Local Disk via SFTP +description: Saves generated code to local filesystem using SFTP +author: Claude +version: 1.0.0 +""" + +import paramiko +import os +from typing import Optional +from pydantic import BaseModel, Field + + +class Tools: + class Valves(BaseModel): + SFTP_HOST: str = Field( + default="vps", + description="SFTP host address" + ) + SFTP_PORT: int = Field( + default=22, + description="SFTP port" + ) + SFTP_USERNAME: str = Field( + default="valknar", + description="SFTP username" + ) + SFTP_BASE_PATH: str = Field( + default="/home/valknar/Projects", + description="Base path for file operations" + ) + SFTP_USE_KEY: bool = Field( + default=True, + description="Use SSH key authentication" + ) + SFTP_KEY_PATH: str = Field( + default="/app/.ssh/id_rsa", + description="Path to SSH private key" + ) + + def __init__(self): + self.valves = self.Valves() + + def save_file( + self, + filepath: str, + content: str, + __user__: dict = {}, + ) -> str: + """ + Save content to a file on the local disk via SFTP. + + :param filepath: Relative path from base directory (e.g. "myproject/main.py") + :param content: File content to save + :return: Success message or error + """ + + try: + # Create SSH client + ssh = paramiko.SSHClient() + ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + + # Connect using SSH key or password + if self.valves.SFTP_USE_KEY: + ssh.connect( + hostname=self.valves.SFTP_HOST, + port=self.valves.SFTP_PORT, + username=self.valves.SFTP_USERNAME, + key_filename=self.valves.SFTP_KEY_PATH, + ) + else: + # For password auth, would need SFTP_PASSWORD in valves + raise ValueError("Password auth not configured. Use SSH key authentication.") + + # Open SFTP session + sftp = ssh.open_sftp() + + # Construct full path + full_path = os.path.join(self.valves.SFTP_BASE_PATH, filepath) + directory = os.path.dirname(full_path) + + # Create directories if they don't exist + try: + sftp.stat(directory) + except FileNotFoundError: + # Create parent directories recursively + dirs = [] + temp_dir = directory + while temp_dir and temp_dir != '/': + try: + sftp.stat(temp_dir) + break + except FileNotFoundError: + dirs.append(temp_dir) + temp_dir = os.path.dirname(temp_dir) + + for dir_path in reversed(dirs): + sftp.mkdir(dir_path) + + # Write file + with sftp.open(full_path, 'w') as f: + f.write(content) + + sftp.close() + ssh.close() + + return f"✅ File saved successfully to: {full_path}" + + except Exception as e: + return f"❌ Error saving file: {str(e)}" + + def read_file( + self, + filepath: str, + __user__: dict = {}, + ) -> str: + """ + Read content from a file on the local disk via SFTP. + + :param filepath: Relative path from base directory (e.g. "myproject/main.py") + :return: File content or error message + """ + + try: + # Create SSH client + ssh = paramiko.SSHClient() + ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + + # Connect using SSH key + ssh.connect( + hostname=self.valves.SFTP_HOST, + port=self.valves.SFTP_PORT, + username=self.valves.SFTP_USERNAME, + key_filename=self.valves.SFTP_KEY_PATH, + ) + + # Open SFTP session + sftp = ssh.open_sftp() + + # Construct full path + full_path = os.path.join(self.valves.SFTP_BASE_PATH, filepath) + + # Read file + with sftp.open(full_path, 'r') as f: + content = f.read() + + sftp.close() + ssh.close() + + return content + + except Exception as e: + return f"❌ Error reading file: {str(e)}" + + def list_files( + self, + directory: str = ".", + __user__: dict = {}, + ) -> str: + """ + List files in a directory on the local disk via SFTP. + + :param directory: Relative directory path from base (e.g. "myproject/") + :return: List of files or error message + """ + + try: + # Create SSH client + ssh = paramiko.SSHClient() + ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + + # Connect using SSH key + ssh.connect( + hostname=self.valves.SFTP_HOST, + port=self.valves.SFTP_PORT, + username=self.valves.SFTP_USERNAME, + key_filename=self.valves.SFTP_KEY_PATH, + ) + + # Open SFTP session + sftp = ssh.open_sftp() + + # Construct full path + full_path = os.path.join(self.valves.SFTP_BASE_PATH, directory) + + # List files + files = sftp.listdir(full_path) + + sftp.close() + ssh.close() + + return f"📁 Files in {full_path}:\n" + "\n".join(f" - {f}" for f in files) + + except Exception as e: + return f"❌ Error listing files: {str(e)}"