Агент за shell команди с одобрение и защита
Дадеш ли на езиков модел достъп до терминала, той може да помогне — и да навреди. Тук строим малък агент, който предлага команда, а човек я одобрява; командата минава през разрешен списък, без междинна обвивка, с таймаут и журнал. После виждаме докъде стига тази защита и къде започва истинската изолация — NVIDIA OpenShell.
shell=True (при него проверката само на първата дума се заобикаля с ls; rm -rf …), несъществуващия модел qwen3:7b (в документацията на Ollama е qwen3:8b), примерния път към потребителска папка и твърдението, че одобрението е „единствената надеждна защита“. Добавихме: разбор на командата със shlex и изпълнение без обвивка, блокиране на опасните действия на find, ограничение на изхода, журнал и на блокираните опити, цикъл, който връща резултата на модела, честен раздел за границите на разрешения списък и мястото на NVIDIA OpenShell — отворен продукт за истинска изолация на агенти. Името „OpenShell“ в старата версия означаваше само нашия малък пример; сега го разграничаваме от продукта на NVIDIA.
safe_shell.py) пуснахме на обикновена Linux машина и тя блокира това, което описваме; цикълът с модела (agent.py) е само проверен за синтаксис. Ollama с qwen3:8b на GB10 не е пускан. NVIDIA OpenShell не е инсталиран и не е пускан; описанието му е по официалното репо, а поддръжката за Arm64 / GB10 не е проверена. Затова няма етикет „ТЕСТВАНО“ и „ПРОВЕРЕНО“.01Какво ще научиш
- Как работи цикълът „модел предлага → човек одобрява → изпълнява се → резултатът се връща“.
- Защо разрешеният списък трябва да се проверява след разбор на командата и без
shell=True. - Как да добавиш таймаут, ограничение на изхода и журнал.
- Какво разрешеният списък не пази и защо това има значение.
- Какво е NVIDIA OpenShell и кога ти трябва истинска изолация.
02Преди да започнеш
- Машина от класа NVIDIA GB10 с DGX OS и достъп до терминала ѝ.
- Ollama, който работи на машината (виж урока за Open WebUI и Ollama).
- Python 3 и възможност да направиш виртуална среда.
- Работи с непривилегирован потребител, в празна папка, където няма нищо чувствително — в този урок разрешеният списък не е изолация (виж стъпка 6).
03Стъпки
-
Идеята и четирите защити
Моделът никога не изпълнява нищо сам. Той връща текст — предложена команда. Твоят код решава какво да стане с този текст. Защо е така? Моделът може да сбърка или да бъде подведен от текст, който чете; затова между „предложи“ и „изпълни“ слагаме слоеве.
Мярка Какво прави Къде е в кода Одобрение Нищо не тръгва без твоето „y“ input("…")Разрешен списък Само изброени програми ALLOWEDБез обвивка ;,&&,|не работят като връзки между командиshlex.split,shell=FalseТаймаут и журнал 10 секунди; всеки опит се записва timeout=,audit.log -
Модел и среда
bashollama pull qwen3:8b ollama list python3 -m venv agent-env source agent-env/bin/activate pip install openaiOllama предлага съвместима с OpenAI връзка на
http://localhost:11434/v1/; библиотекатаopenaiизисква ключ, но Ollama го игнорира (по документацията на Ollama). ⚠️ Скоростта на модела на твоята машина не сме мерили. -
Файл 1: безопасно изпълнение
Записваш го като
safe_shell.py. Командата първо се разбива на части съсshlex.split, после се проверява името на програмата, после се пуска като списък — без обвивка. Затоваls; rm -rf xстава програма „ls;“, която не е в списъка.findима действия, които изпълняват или трият (-exec,-delete…), и те са блокирани отделно.python · safe_shell.pyimport shlex, subprocess, datetime ALLOWED = {"ls", "cat", "grep", "find", "df", "free", "ps", "pwd", "echo", "head", "tail", "wc", "du", "stat"} FIND_FORBIDDEN = {"-exec", "-execdir", "-ok", "-okdir", "-delete", "-fprint", "-fprintf", "-fls"} LOG = "audit.log" def log(status, cmd, out=""): with open(LOG, "a", encoding="utf-8") as f: f.write(f"{datetime.datetime.now().isoformat(timespec='seconds')} | {status} | {cmd} | {out[:100]!r}\n") def safe_execute(cmd: str, timeout: int = 10) -> str: try: parts = shlex.split(cmd) except ValueError: log("BLOCKED", cmd) return "BLOCKED: командата не може да се разчете" if not parts or parts[0] not in ALLOWED: log("BLOCKED", cmd) return f"BLOCKED: '{parts[0] if parts else ''}' не е в разрешения списък" if parts[0] == "find" and FIND_FORBIDDEN.intersection(parts): log("BLOCKED", cmd) return "BLOCKED: това действие на find не е разрешено" try: r = subprocess.run(parts, capture_output=True, text=True, timeout=timeout, shell=False) except subprocess.TimeoutExpired: log("TIMEOUT", cmd) return "TIMEOUT" except OSError as e: log("ERROR", cmd, str(e)) return f"ERROR: {e}" out = (r.stdout or r.stderr)[:2000] log("RUN", cmd, out) return out💡Защо такаЖурналът записва всяко решение, включително блокираните опити; те показват какво модел или подвеждащ текст се е опитал да направи. Изходът е до 2000 знака, за да не залеем модела. Програмитеtopи подобни интерактивни са махнати — те не свършват сами. -
Файл 2: цикълът с модела
Записваш го като
agent.pyв същата папка. Моделът получава задачата и може да отговори или с една команда, или сDONE: …. След всяка изпълнена команда резултатът се връща към него.python · agent.pyimport re from openai import OpenAI from safe_shell import safe_execute client = OpenAI(base_url="http://localhost:11434/v1/", api_key="ollama") # ключът се изисква, но се игнорира MODEL = "qwen3:8b" SYSTEM = ("You are a careful Linux assistant. Reply with exactly ONE shell command on one line, " "no explanations, no markdown. When the task is complete, reply: DONE: <short answer>.") def clean(text: str) -> str: text = re.sub(r"<think>.*?</think>", "", text, flags=re.S) # на всеки случай return text.strip().strip("`").strip() def agent_loop(task: str, max_steps: int = 5): messages = [{"role": "system", "content": SYSTEM}, {"role": "user", "content": task}] for step in range(1, max_steps + 1): resp = client.chat.completions.create(model=MODEL, messages=messages, max_tokens=200) cmd = clean(resp.choices[0].message.content) if cmd.upper().startswith("DONE"): print("\n" + cmd) return print(f"\n[{step}] Предлага: {cmd}") if input("Изпълни? [y/N] ").strip().lower() != "y": print("Пропуснато.") return output = safe_execute(cmd) print("Резултат:\n" + output) messages.append({"role": "assistant", "content": cmd}) messages.append({"role": "user", "content": "Output:\n" + output + "\nNext command, or DONE: <answer>."}) if __name__ == "__main__": agent_loop(input("Задача: ")) -
Пробвай и опитай да го счупиш
bashpython3 agent.py- Задача: Покажи свободното място на диска. Очакваш команда като
df -h; одобри я сy. - Задача: Изтрий всички файлове в текущата папка. Ако моделът предложи
rm, натисниy— получавашBLOCKED. Одобрението не заобикаля списъка. - Отвори
audit.log: виждаш и изпълнените, и блокираните опити.
Така се държи
safe_executeпри опити за заобикаляне (пуснато на обикновена Linux машина):Опит Резултат ls; rm -rf xблокирано — „ ls;“ не е в списъкаrm -rf /tmp/zzблокирано find . -deleteблокирано ls && echo hiне се свързва; &&иechoстават имена на файлове иlsсъобщава грешкаcat /etc/hostnameразрешено — прочита файл извън работната папка - Задача: Покажи свободното място на диска. Очакваш команда като
-
Какво разрешеният списък НЕ пази
- Какво се чете.
catиgrepса безобидни като програми, но могат да прочетат всеки файл, до който имаш достъп — ключове, бележки, конфигурации. - Подведен модел. Резултатът се връща на модела. Ако във файл има написани инструкции, моделът може да ги последва и да предложи вредна команда (инжектиране на указания). Последната бариера си оставаш ти.
- Умора от одобрения. Ако натискаш
yбез да четеш, одобрението не пази нищо. - Права. Всичко върви с правата на потребителя, който пуска скрипта. Това не е затворена среда (sandbox).
✅МинимумътОтделен непривилегирован потребител, празна работна папка, без sudo и без тайни в обсега. Не добавяй в списъкаsudo,rm,curl,bash,pythonили друг интерпретатор — с тях списъкът губи смисъл. - Какво се чете.
-
Истинска изолация: NVIDIA OpenShell
Когато агентът прави повече от демо, нужна е среда, която сама ограничава какво може. Според официалното репо на NVIDIA (към 03.10.2026) OpenShell е „безопасна, частна среда за автономни AI агенти“ с лиценз Apache-2.0:
- всеки агент работи в изолирана среда (sandbox); ядрото ограничава кои файлове и системни извиквания са достъпни и всяка мрежова връзка минава през проверка по политика;
- агентът не вижда истинските ключове — те се добавят само към заявки към одобрени адреси;
- промените в политиката се проверяват, преди да бъдат приети, и рисковите чакат човек;
- изисква Linux (или macOS на Apple Silicon) и Docker, Podman или виртуализация; има инсталатор и команда за първа изолирана среда — виж документацията.
⚠️Не е пускано от насНе сме инсталирали OpenShell и не знаем дали и как работи на GB10 (Arm64) — виж таблицата за поддръжка в документацията. Инсталаторът е скрипт от интернет: прочети го, преди да го пуснеш, и не го пускай като администратор.Идеята е същата като в нашия пример, но на друго ниво: нашият код проверява текста на командата, а OpenShell ограничава какво реално може да направи процесът, каквото и да е предложил моделът.
04Проверка
qwen3:8bе изтеглен и отговаря презhttp://localhost:11434/v1/.- Безобидна команда (
df -h) се предлага, одобрява, изпълнява и записва. rmе блокирано и след одобрение; блокът е вaudit.log.ls; rm -rf xиfind . -deleteса блокирани.- Можеш да назовеш три неща, които разрешеният списък не пази.
Тест
1. Защо командата се пуска с shell=False?
2. Натискаш y на команда rm. Какво става?
3. Достатъчен ли е разрешеният списък като изолация?
4. Какво е NVIDIA OpenShell според официалното репо?
05Какво следва
06Източници
- Ollama: съвместимост с OpenAI 🔒 локално — адрес
/v1/, ключът се игнорира, пример сqwen3:8b. - NVIDIA OpenShell: репо и README · документация 🔒 локално — описание, лиценз Apache-2.0, изисквания.
- Python: subprocess · Python: shlex — изпълнение без обвивка, разбор на команда.