Знакът на КАГАМИ КАГАМИ
kagami.bg/academy · lesson · machine-readable viewUPDATED 2026-10-03
IDENTITY
module
GX10-04-33 · Shell command agent with human approval, and where NVIDIA OpenShell fits
series
GX10 (local AI server class: NVIDIA GB10, e.g. ASUS Ascent GX10 / DGX Spark)
level
Intermediate
duration
about 45 min
prerequisites
A GB10-class machine with DGX OS, shell access, Python 3, Ollama running locally with the qwen3:8b model pulled
trust_label
UPDATED 2026-10-03 (rewritten against Ollama OpenAI-compatibility documentation and the NVIDIA OpenShell README as of 2026-10-03) · the allowlist function was run on an ordinary Linux machine; NOT TESTED on a GB10 machine; NVIDIA OpenShell was not installed or run
versions
Python 3 with the openai package (unpinned) · Ollama model qwen3:8b · NVIDIA OpenShell 0.1.x per its README (not installed here)
license
NVIDIA OpenShell: Apache-2.0 (per its repository)
language
human view: bg · english edition: /en/academy/gx10/ (same file name)
previous / next
GX10 series index / 04-34_Live_VLM.html
PURPOSE

Build a minimal agent that lets a local LLM propose one shell command at a time, requires a human "y" before every command, and executes only allowlisted programs through subprocess without a shell, with a timeout, an output cap and an audit log of every attempt (including blocked ones). Understand the limits of this approach and why a real sandbox is the next layer; NVIDIA OpenShell is an open-source runtime for that layer.

KEY CONCEPTS
COMMANDS / PATHS
CHECKLIST
NEXT MODULE

04-34 · Live VLM (04-34_Live_VLM.html) · related: 04-101_Safety_Agentic_AI.html · series index: kagami.bg/academy/gx10/ · offer: Quick experiment (kagami.bg/stalbata/)

SOURCES
TAGS
gx10agentssecurityhuman-in-the-loopallowlistsandboxopenshellollama
ОБНОВЕНО · 03.10.2026

Агент за shell команди с одобрение и защита

Дадеш ли на езиков модел достъп до терминала, той може да помогне — и да навреди. Тук строим малък агент, който предлага команда, а човек я одобрява; командата минава през разрешен списък, без междинна обвивка, с таймаут и журнал. После виждаме докъде стига тази защита и къде започва истинската изолация — NVIDIA OpenShell.

⏱ ~45 мин Средно GX10 Python · Ollama Одобрение · разрешен списък · журнал
Python (subprocess, shlex)🔒 локално Ollama (qwen3:8b)🔒 локално NVIDIA OpenShell🔒 локално
🔄
ОБНОВЕНО · 03.10.2026 — какво
Урокът е преработен. Махнахме: shell=True (при него проверката само на първата дума се заобикаля с ls; rm -rf …), несъществуващия модел qwen3:7b (в документацията на Ollama е qwen3:8b), примерния път към потребителска папка и твърдението, че одобрението е „единствената надеждна защита“. Добавихме: разбор на командата със shlex и изпълнение без обвивка, блокиране на опасните действия на find, ограничение на изхода, журнал и на блокираните опити, цикъл, който връща резултата на модела, честен раздел за границите на разрешения списък и мястото на NVIDIA OpenShell — отворен продукт за истинска изолация на агенти. Името „OpenShell“ в старата версия означаваше само нашия малък пример; сега го разграничаваме от продукта на NVIDIA.
⚠️
Какво не сме пускали сами
При проверката нямахме машина от класа GB10. Логиката на разрешения списък (safe_shell.py) пуснахме на обикновена Linux машина и тя блокира това, което описваме; цикълът с модела (agent.py) е само проверен за синтаксис. Ollama с qwen3:8b на GB10 не е пускан. NVIDIA OpenShell не е инсталиран и не е пускан; описанието му е по официалното репо, а поддръжката за Arm64 / GB10 не е проверена. Затова няма етикет „ТЕСТВАНО“ и „ПРОВЕРЕНО“.

01Какво ще научиш

02Преди да започнеш

03Стъпки

  1. Идеята и четирите защити

    Моделът никога не изпълнява нищо сам. Той връща текст — предложена команда. Твоят код решава какво да стане с този текст. Защо е така? Моделът може да сбърка или да бъде подведен от текст, който чете; затова между „предложи“ и „изпълни“ слагаме слоеве.

    МяркаКакво правиКъде е в кода
    ОдобрениеНищо не тръгва без твоето „y“input("…")
    Разрешен списъкСамо изброени програмиALLOWED
    Без обвивка;, &&, | не работят като връзки между командиshlex.split, shell=False
    Таймаут и журнал10 секунди; всеки опит се записваtimeout=, audit.log
  2. Модел и среда

    bash
    ollama pull qwen3:8b
    ollama list
    
    python3 -m venv agent-env
    source agent-env/bin/activate
    pip install openai

    Ollama предлага съвместима с OpenAI връзка на http://localhost:11434/v1/; библиотеката openai изисква ключ, но Ollama го игнорира (по документацията на Ollama). ⚠️ Скоростта на модела на твоята машина не сме мерили.

  3. Файл 1: безопасно изпълнение

    Записваш го като safe_shell.py. Командата първо се разбива на части със shlex.split, после се проверява името на програмата, после се пуска като списък — без обвивка. Затова ls; rm -rf x става програма „ls;“, която не е в списъка. find има действия, които изпълняват или трият (-exec, -delete…), и те са блокирани отделно.

    python · safe_shell.py
    import 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 и подобни интерактивни са махнати — те не свършват сами.
  4. Файл 2: цикълът с модела

    Записваш го като agent.py в същата папка. Моделът получава задачата и може да отговори или с една команда, или с DONE: …. След всяка изпълнена команда резултатът се връща към него.

    python · agent.py
    import 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("Задача: "))
  5. Пробвай и опитай да го счупиш

    bash
    python3 agent.py
    1. Задача: Покажи свободното място на диска. Очакваш команда като df -h; одобри я с y.
    2. Задача: Изтрий всички файлове в текущата папка. Ако моделът предложи rm, натисни y — получаваш BLOCKED. Одобрението не заобикаля списъка.
    3. Отвори audit.log: виждаш и изпълнените, и блокираните опити.

    Така се държи safe_execute при опити за заобикаляне (пуснато на обикновена Linux машина):

    ОпитРезултат
    ls; rm -rf xблокирано — „ls;“ не е в списъка
    rm -rf /tmp/zzблокирано
    find . -deleteблокирано
    ls && echo hiне се свързва; && и echo стават имена на файлове и ls съобщава грешка
    cat /etc/hostnameразрешено — прочита файл извън работната папка
  6. Какво разрешеният списък НЕ пази

    • Какво се чете. cat и grep са безобидни като програми, но могат да прочетат всеки файл, до който имаш достъп — ключове, бележки, конфигурации.
    • Подведен модел. Резултатът се връща на модела. Ако във файл има написани инструкции, моделът може да ги последва и да предложи вредна команда (инжектиране на указания). Последната бариера си оставаш ти.
    • Умора от одобрения. Ако натискаш y без да четеш, одобрението не пази нищо.
    • Права. Всичко върви с правата на потребителя, който пуска скрипта. Това не е затворена среда (sandbox).
    ✅
    Минимумът
    Отделен непривилегирован потребител, празна работна папка, без sudo и без тайни в обсега. Не добавяй в списъка sudo, rm, curl, bash, python или друг интерпретатор — с тях списъкът губи смисъл.
  7. Истинска изолация: NVIDIA OpenShell

    Когато агентът прави повече от демо, нужна е среда, която сама ограничава какво може. Според официалното репо на NVIDIA (към 03.10.2026) OpenShell е „безопасна, частна среда за автономни AI агенти“ с лиценз Apache-2.0:

    • всеки агент работи в изолирана среда (sandbox); ядрото ограничава кои файлове и системни извиквания са достъпни и всяка мрежова връзка минава през проверка по политика;
    • агентът не вижда истинските ключове — те се добавят само към заявки към одобрени адреси;
    • промените в политиката се проверяват, преди да бъдат приети, и рисковите чакат човек;
    • изисква Linux (или macOS на Apple Silicon) и Docker, Podman или виртуализация; има инсталатор и команда за първа изолирана среда — виж документацията.
    ⚠️
    Не е пускано от нас
    Не сме инсталирали OpenShell и не знаем дали и как работи на GB10 (Arm64) — виж таблицата за поддръжка в документацията. Инсталаторът е скрипт от интернет: прочети го, преди да го пуснеш, и не го пускай като администратор.

    Идеята е същата като в нашия пример, но на друго ниво: нашият код проверява текста на командата, а OpenShell ограничава какво реално може да направи процесът, каквото и да е предложил моделът.

04Проверка

Тест

1. Защо командата се пуска с shell=False?

2. Натискаш y на команда rm. Какво става?

3. Достатъчен ли е разрешеният списък като изолация?

4. Какво е NVIDIA OpenShell според официалното репо?

05Какво следва

06Източници

  1. Ollama: съвместимост с OpenAI 🔒 локално — адрес /v1/, ключът се игнорира, пример с qwen3:8b.
  2. NVIDIA OpenShell: репо и README · документация 🔒 локално — описание, лиценз Apache-2.0, изисквания.
  3. Python: subprocess · Python: shlex — изпълнение без обвивка, разбор на команда.