Linux shell агент на LangChain и Ollama с защитой команд - STELLA | AiManual
AiManual Logo Ai / Manual.
12 Янв 2026 Гайд

STELLA: создаём простого Linux shell-агента на LangChain и Ollama с защитой от опасных команд

Пошаговый гайд по созданию безопасного Linux shell-агента STELLA с LangChain и Ollama. Автоматизация команд с защитой от опасных операций.

Когда терминал становится опасным собеседником

Представьте: вы просите ИИ "почистить логи" на сервере. Через секунду получаете ответ: rm -rf /var/log/*. Звучит логично, правда? До тех пор, пока не поймёте, что эта команда удалит не только старые логи, но и все системные журналы, включая те, что нужны прямо сейчас.

Именно поэтому большинство инженеров боятся давать ИИ прямой доступ к shell. Но что если сделать агента, который не просто выполняет команды, а думает, проверяет и спрашивает разрешения? STELLA — именно такой проект.

Важно: STELLA — экспериментальный проект. Не запускайте его на продакшн-серверах без дополнительных мер безопасности. Это демонстрация концепции, а не готовое решение для enterprise.

Что такое STELLA и зачем она нужна

STELLA — это shell-агент на Python, который использует локальную LLM через Ollama для понимания естественного языка и преобразования его в безопасные команды Linux. Вместо того чтобы тупо выполнять всё подряд, агент:

  • Анализирует команды на опасные паттерны
  • Требует подтверждения для операций с sudo
  • Имеет только 4 разрешённых инструмента (никаких произвольных exec)
  • Сохраняет контекст диалога для понимания последовательности команд

Зачем это нужно? Представьте рутинные задачи: найти файлы по паттерну, проверить использование диска, мониторить процессы, анализировать логи. Вместо того чтобы гуглить каждый раз "как найти файлы изменённые за последний час", вы просто пишете: "покажи файлы в /home изменённые сегодня".

💡
Если вам интересны альтернативные подходы к созданию AI-агентов без тяжёлого LangChain, посмотрите статью про Cogitator — TypeScript-рантайм для AI-агентов. Там другой взгляд на архитектуру.

1 Подготовка окружения: что нужно установить

Начнём с основ. Вам потребуется:

# Устанавливаем Ollama (если ещё нет)
curl -fsSL https://ollama.ai/install.sh | sh

# Скачиваем модель (Llama 3.1 8B отлично подходит)
ollama pull llama3.1:8b

# Проверяем, что Ollama работает
ollama list

Теперь Python-зависимости. Создайте виртуальное окружение — это важно, потому что LangChain тащит за собой тонну зависимостей.

python -m venv venv_stella
source venv_stella/bin/activate  # На Windows: venv_stella\Scripts\activate

pip install langchain langchain-community python-dotenv

Внимание: LangChain известен своими зависимостями. Если вам нужна более лёгкая альтернатива, изучите статью про создание агента на Bun. Но для нашего случая LangChain подходит — его инструменты и цепочки упрощают разработку.

2 Архитектура: как устроена безопасность

Вот главный принцип STELLA: ограниченный набор инструментов + валидация команд. Не "выполни что угодно", а "выбери из разрешённого списка и проверь результат".

Наша архитектура выглядит так:

Компонент Назначение Ограничения
Command Sanitizer Проверяет команды на опасные паттерны Блокирует rm -rf, dd, :(){ :|:& };: и подобное
Tool Router Выбирает подходящий инструмент Только 4 предопределённых инструмента
Sudo Guard Контролирует привилегированные команды Требует явного подтверждения
Context Manager Хранит историю диалога Ограниченное окно контекста

Ключевой момент — мы НЕ даём агенту доступ к subprocess.run() или os.system() напрямую. Вместо этого создаём обёртки с проверками.

3 Пишем санитайзер команд: фильтруем опасности

Начнём с самого важного — защиты от самоубийственных команд. Вот как выглядит базовый санитайзер:

class CommandSanitizer:
    def __init__(self):
        self.dangerous_patterns = [
            # Полное удаление системных директорий
            r'rm\s+-[rf]+\s+/[^*]',
            r'rm\s+-[rf]+\s+/$',
            
            # Форматирование/перезапись дисков
            r'dd\s+.*of=/dev/',
            r'mkfs\s+.*/dev/',
            
            # Fork bomb
            r':\(\)\s*{.*:\|:.*}',
            
            # Удаление домашней директории
            r'rm\s+-[rf]+\s+~/',
            r'rm\s+-[rf]+\s+/home/[^/]+(/|$)',
            
            # Опасные перенаправления
            r'>\s+/dev/sd[a-z]',
            r'>\s+/dev/nvme',
            
            # Сетевые атаки
            r'nc\s+.*-e\s+/bin/sh',
            r'python\s+-c\s+.*import\s+os.*os\.system',
        ]
        
        self.sudo_commands = [
            'apt', 'dnf', 'yum', 'pacman',
            'systemctl', 'service',
            'useradd', 'userdel', 'groupadd',
            'mount', 'umount', 'fdisk',
        ]
    
    def is_dangerous(self, command: str) -> bool:
        """Проверяем команду на опасные паттерны"""
        import re
        
        command_lower = command.lower().strip()
        
        # Проверяем паттерны
        for pattern in self.dangerous_patterns:
            if re.search(pattern, command_lower):
                return True
        
        # Дополнительные эвристики
        if self._contains_dangerous_sequence(command_lower):
            return True
            
        return False
    
    def requires_sudo(self, command: str) -> bool:
        """Определяем, нужен ли sudo для команды"""
        # Простая эвристика: команда начинается с sudo или содержит sudo-команды
        if command.strip().startswith('sudo'):
            return True
            
        for sudo_cmd in self.sudo_commands:
            if command.startswith(sudo_cmd):
                return True
                
        return False
    
    def _contains_dangerous_sequence(self, cmd: str) -> bool:
        """Проверяем опасные последовательности команд"""
        dangerous_sequences = [
            '&& rm -rf',
            '| rm -rf',
            '; rm -rf',
            '&& dd',
            'chmod -R 777 /',
            'chown -R root:root ~',
        ]
        
        return any(seq in cmd for seq in dangerous_sequences)

Это базовая защита. В реальном проекте стоит добавить больше паттернов и, возможно, ML-классификатор для определения опасных команд. Кстати, о безопасности LLM — недавно вышла интересная статья про Vigil — инструмент для безопасности LLM. Пригодится для расширения функционала.

4 Создаём инструменты: 4 безопасных функции

Вот наши 4 инструмента — всё, что может агент. Никаких произвольных exec, только предопределённые действия:

import subprocess
import os
from typing import Dict, Any

class ShellTools:
    def __init__(self, sanitizer):
        self.sanitizer = sanitizer
        
    def list_files(self, directory: str = ".", pattern: str = "") -> Dict[str, Any]:
        """Безопасный ls с фильтрацией"""
        # Проверяем, что директория существует и доступна
        if not os.path.exists(directory):
            return {"error": f"Directory {directory} does not exist"}
            
        if not os.path.isdir(directory):
            return {"error": f"{directory} is not a directory"}
        
        try:
            # Безопасная команда find
            if pattern:
                cmd = f"find '{directory}' -name '{pattern}' -type f 2>/dev/null | head -20"
            else:
                cmd = f"ls -la '{directory}' | head -50"
                
            if self.sanitizer.is_dangerous(cmd):
                return {"error": "Command blocked by sanitizer"}
                
            result = subprocess.run(
                cmd, 
                shell=True, 
                capture_output=True, 
                text=True,
                timeout=10
            )
            
            return {
                "output": result.stdout,
                "error": result.stderr,
                "returncode": result.returncode
            }
            
        except subprocess.TimeoutExpired:
            return {"error": "Command timed out"}
        except Exception as e:
            return {"error": str(e)}
    
    def system_info(self) -> Dict[str, Any]:
        """Получаем безопасную системную информацию"""
        safe_commands = {
            "disk": "df -h | grep -E '^(Filesystem|/dev/)'",
            "memory": "free -h",
            "processes": "ps aux --sort=-%cpu | head -10",
            "uptime": "uptime",
        }
        
        results = {}
        for name, cmd in safe_commands.items():
            try:
                result = subprocess.run(
                    cmd, 
                    shell=True, 
                    capture_output=True, 
                    text=True
                )
                results[name] = result.stdout
            except Exception as e:
                results[name] = f"Error: {str(e)}"
                
        return results
    
    def search_in_files(self, pattern: str, directory: str = ".") -> Dict[str, Any]:
        """Безопасный grep"""
        # Ограничиваем глубину и количество результатов
        cmd = f"grep -r --include='*.{extension}' '{pattern}' '{directory}' 2>/dev/null | head -30"
        
        if self.sanitizer.is_dangerous(cmd):
            return {"error": "Search pattern blocked"}
            
        # Реализация с проверками...
        return self._safe_execute(cmd)
    
    def file_operations(self, action: str, source: str, target: str = None) -> Dict[str, Any]:
        """Ограниченные файловые операции"""
        allowed_actions = ["copy", "move", "delete"]
        
        if action not in allowed_actions:
            return {"error": f"Action {action} not allowed"}
            
        # Проверяем пути на безопасность
        if self._is_dangerous_path(source) or (target and self._is_dangerous_path(target)):
            return {"error": "Path blocked by security policy"}
            
        # Реализация операций с проверками...
        
    def _safe_execute(self, cmd: str) -> Dict[str, Any]:
        """Безопасное выполнение команды"""
        if self.sanitizer.is_dangerous(cmd):
            return {"error": "Command blocked by sanitizer"}
            
        # Добавляем таймаут и ограничения
        try:
            result = subprocess.run(
                cmd,
                shell=True,
                capture_output=True,
                text=True,
                timeout=15,
                env={**os.environ, "PATH": "/usr/bin:/bin:/usr/local/bin"}
            )
            return {
                "output": result.stdout,
                "error": result.stderr,
                "returncode": result.returncode
            }
        except subprocess.TimeoutExpired:
            return {"error": "Command timed out after 15 seconds"}
        
    def _is_dangerous_path(self, path: str) -> bool:
        """Проверяем, не опасный ли путь"""
        dangerous_paths = [
            "/", "/bin", "/sbin", "/usr/bin", "/usr/sbin", "/etc",
            "/boot", "/lib", "/lib64", "/root", "/var/log",
        ]
        
        # Нормализуем путь
        abs_path = os.path.abspath(path)
        
        # Проверяем, не начинается ли путь с опасной директории
        for dangerous in dangerous_paths:
            if abs_path.startswith(dangerous):
                return True
                
        return False

Видите ограничения? Мы специально:

  • Используем head для ограничения вывода
  • Проверяем все пути
  • Устанавливаем таймауты
  • Ограничиваем переменные окружения
💡
Если вам нужно подключить self-hosted LLM к другим инструментам, изучите статью про идеальный стек для локальных LLM. Там подробно про интеграции с IDE и CLI.

5 Интегрируем с LangChain и Ollama

Теперь собираем всё вместе. Создаём агента, который использует наши инструменты:

from langchain.agents import AgentExecutor, create_react_agent
from langchain_community.llms import Ollama
from langchain.prompts import PromptTemplate
from langchain.tools import Tool

def create_stella_agent():
    # Инициализируем LLM через Ollama
    llm = Ollama(
        model="llama3.1:8b",
        temperature=0.1,  # Низкая температура для консервативных ответов
        num_predict=512,
    )
    
    # Создаём санитайзер и инструменты
    sanitizer = CommandSanitizer()
    tools = ShellTools(sanitizer)
    
    # Определяем инструменты для LangChain
    langchain_tools = [
        Tool(
            name="list_files",
            func=tools.list_files,
            description="""List files in a directory. 
            Input should be a JSON string with 'directory' and optional 'pattern' keys.
            Example: {'directory': '/home/user', 'pattern': '*.txt'}"""
        ),
        Tool(
            name="system_info",
            func=tools.system_info,
            description="Get system information (disk usage, memory, processes, uptime). No input required."
        ),
        Tool(
            name="search_in_files",
            func=tools.search_in_files,
            description="""Search for text in files. 
            Input should be a JSON string with 'pattern' and optional 'directory' keys.
            Example: {'pattern': 'error', 'directory': '/var/log'}"""
        ),
        Tool(
            name="file_operations",
            func=tools.file_operations,
            description="""Perform file operations (copy, move, delete).
            Input should be a JSON string with 'action', 'source', and optional 'target'.
            Example: {'action': 'copy', 'source': 'file1.txt', 'target': 'backup/file1.txt'}"""
        ),
    ]
    
    # Промпт для агента
    prompt_template = """You are STELLA, a secure Linux shell assistant. 
    You have access to the following tools:
    
    {tools}
    
    Use the following format:
    
    Question: the input question you must answer
    Thought: you should always think about what to do
    Action: the action to take, should be one of [{tool_names}]
    Action Input: the input to the action
    Observation: the result of the action
    ... (this Thought/Action/Action Input/Observation can repeat N times)
    Thought: I now know the final answer
    Final Answer: the final answer to the original input question
    
    IMPORTANT RULES:
    1. NEVER suggest dangerous commands (rm -rf, dd, fork bombs, etc.)
    2. ALWAYS verify paths before operating on them
    3. If user asks for sudo operations, ask for explicit confirmation
    4. Limit output to reasonable amounts (use head/tail if needed)
    5. If unsure about safety, respond with "I cannot perform this operation for security reasons"
    
    Begin!
    
    Previous conversation history:
    {history}
    
    Question: {input}
    Thought:{agent_scratchpad}"""
    
    prompt = PromptTemplate.from_template(prompt_template)
    
    # Создаём агента
    agent = create_react_agent(
        llm=llm,
        tools=langchain_tools,
        prompt=prompt
    )
    
    # Исполнитель с ограничениями
    agent_executor = AgentExecutor(
        agent=agent,
        tools=langchain_tools,
        verbose=True,
        max_iterations=5,  # Ограничиваем количество итераций
        handle_parsing_errors=True,
        early_stopping_method="generate"
    )
    
    return agent_executor, sanitizer

6 Добавляем интерактивный интерфейс

Теперь создадим простой CLI для взаимодействия с агентом:

import json
import readline  # Для истории команд

class StellaCLI:
    def __init__(self):
        self.agent, self.sanitizer = create_stella_agent()
        self.history = []
        
    def run(self):
        print("STELLA Shell Assistant (Secure Edition)")
        print("Type 'quit' to exit, 'history' to see previous commands")
        print("=" * 50)
        
        while True:
            try:
                user_input = input("\nstella> ").strip()
                
                if user_input.lower() in ['quit', 'exit', 'q']:
                    print("Goodbye!")
                    break
                    
                elif user_input.lower() == 'history':
                    for i, cmd in enumerate(self.history[-10:], 1):
                        print(f"{i}: {cmd}")
                    continue
                
                # Проверяем на прямые команды shell
                if self.sanitizer.requires_sudo(user_input):
                    print("⚠️  This operation requires sudo. Type 'CONFIRM' to proceed:")
                    confirm = input("Confirm sudo operation: ").strip()
                    if confirm != "CONFIRM":
                        print("Operation cancelled.")
                        continue
                
                # Выполняем через агента
                self.history.append(user_input)
                
                response = self.agent.invoke({
                    "input": user_input,
                    "history": "\n".join(self.history[-5:])  # Последние 5 команд
                })
                
                print("\n" + "=" * 50)
                print(response["output"])
                print("=" * 50)
                
            except KeyboardInterrupt:
                print("\n\nInterrupted. Type 'quit' to exit.")
                
            except Exception as e:
                print(f"\nError: {str(e)}")
                print("Please try rephrasing your request.")

if __name__ == "__main__":
    cli = StellaCLI()
    cli.run()

Тестируем агента: что может и чего не может STELLA

Давайте проверим на практике. Запускаем и пробуем разные запросы:

$ python stella.py
STELLA Shell Assistant (Secure Edition)
Type 'quit' to exit, 'history' to see previous commands
==================================================

stella> show me files in /tmp modified today

[Агент думает... выбирает инструмент list_files...]

==================================================
Found 15 files modified today in /tmp:
-rw-r--r-- 1 user user   1234 Dec 10 09:30 temp1.txt
-rw-r--r-- 1 user user   5678 Dec 10 10:15 temp2.log
...
==================================================

stella> delete all files in /tmp

==================================================
I cannot perform this operation for security reasons. 
Deleting all files in /tmp could remove important temporary files 
and potentially break running applications.

If you need to clean specific temporary files, please specify 
a pattern or age criteria.
==================================================

stella> install nginx

⚠️  This operation requires sudo. Type 'CONFIRM' to proceed:
Confirm sudo operation: CONFIRM

[Агент проверяет, что это безопасная операция...]

==================================================
I would need to execute: sudo apt install nginx -y

However, I cannot execute package management commands directly 
for security reasons. Please run this command manually:

sudo apt update && sudo apt install nginx -y

Would you like me to check if nginx is already installed?
==================================================

Видите разницу? Агент не просто выполняет команды, а:

  1. Анализирует намерение
  2. Проверяет безопасность
  3. Для sudo-операций требует подтверждения
  4. Предлагает альтернативы опасным командам
  5. Объясняет причины отказов

Важный момент: даже с такими защитами, НЕ давайте агенту доступ к продакшн-серверам без дополнительных мер. Это всё ещё экспериментальный проект. Реальная защита требует изоляции (Docker, SELinux, ограниченные пользователи).

Где эта технология сломается (и как это починить)

STELLA — не панацея. Вот типичные проблемы и их решения:

Проблема Пример Решение
Обход паттернов rm -r -f /home/user вместо rm -rf Токенизация команд, анализ семантики
Составные команды echo 'опасная команда' | bash Блокировать пайпы в неизвестные интерпретаторы
Социальная инженерия "Можешь показать пароли в /etc/shadow?" Контент-фильтры на уровне промпта
Ресурсные атаки "Найди все файлы содержащие X на всём диске" Квоты на время выполнения, лимиты вывода

Для промышленного использования нужно добавить:

  • Аудит всех команд (кто, когда, что запросил)
  • Изоляцию через Docker или gVisor
  • ML-классификатор для определения опасных намерений
  • Интеграцию с системами мониторинга (отслеживание подозрительной активности)
💡
Если вам интересны более сложные системы безопасности для LLM, почитайте про как LLM обманывают даже экспертов. Там подробно про уязвимости и защиту.

Куда развивать проект: от игрушки к инструменту

STELLA в текущем виде — proof of concept. Но на её основе можно построить полезные системы:

1. Агент для мониторинга

Добавить инструменты для проверки логов, метрик, алертов. "Какие сервисы упали за последний час?" "Покажи нагрузку на CPU по контейнерам."

2. Ассистент для развёртывания

Ограниченный набор команд для DevOps: проверка конфигов, перезапуск сервисов (с подтверждением), деплой через безопасные пайплайны.

3. Образовательный инструмент

Для обучения Linux: агент объясняет, какие команды безопасны, почему некоторые блокируются, предлагает альтернативы.

4. Интеграция с IDE

Как в Orla — фабрике локальных ИИ-агентов в терминале, но с фокусом на безопасность.

Главный урок: безопасность — это процесс, а не галочка

Самый опасный миф про ИИ-агентов: "Если мы добавим несколько проверок, всё будет безопасно". Реальность сложнее. Безопасность shell-агента требует:

  • Многоуровневой защиты (паттерны + семантика + изоляция)
  • Постоянного обновления правил (новые уязвимости появляются каждый день)
  • Человеческого надзора (автономные агенты — плохая идея)
  • Прозрачности (логирование ВСЕХ действий)

STELLA показывает, что даже простой агент может быть относительно безопасным, если проектировать его с паранойей. Но помните: абсолютной безопасности не существует. Особенно когда в игру вступают социальная инженерия и креативные способы обхода защит.

Попробуйте доработать проект. Добавьте свои инструменты. Усильте защиту. И главное — никогда не запускайте таких агентов с root-правами. Потому что однажды ИИ действительно может решить, что лучший способ "почистить логи" — это rm -rf /*. И будет по-своему прав.