Skip to main content
Glama

Thot

tests python youden false positives

Терминальный ассистент для кода, который уже знает твой репозиторий — и репозиторий, где живут Hermes Agent и Prime Agent, целиком.

Разговорный агент изучает проект, открывая файлы с помощью модели: медленно, частично, и заново платит за каждую сессию. Thot вычисляет ту же картину через AST и граф вызовов — полную, мгновенную, бесплатную — и передаёт модели только то, что имеет значение.

Три программы

В этом репозитории их три, а не один. Ни один не является переписыванием другого: каждый написан на своём языке, со своим инструментарием, и Thot соединяет их вместе.

Что это

Где

thot

детерминированный аудит, карта кода, память вердиктов

src/thot/

hermes

агент: инструменты, шлюзы, плагины, cron, ACP

hermes/ — Python, участник workspace uv

prime

агент кода: поставщики моделей, TUI, RLM

prime/ — TypeScript, npm

thot                 # la session d'audit
thot hermes          # Hermes, arguments transmis tels quels
thot prime           # Prime, pareil
thot fusion status   # ce qui est présent, prêt, et branché

Связывание не декоративно. thot fusion wire объявляет MCP-сервер Thot в обоих агентах: они получают code_map, find_symbol, callers, audit, skills и skill — полную карту репозитория, вычисленную вне модели, вместо того чтобы заново открывать её файл за файлом. Это взаимное усиление: Thot знает, не спрашивая, Hermes и Prime действуют.

Два агента достигают друг друга не одинаково, и утверждать обратное означало бы подключить только одного из них. Hermes сам запускает сервер и общается с ним через открытый им канал. Prime принимает только HTTP — его mcp-manager отбрасывает любое входное значение, тип которого не http, и его среда выполнения не имеет транспорта через канал — поэтому его сторона требует работающего сервера:

thot fusion wire            # écrit les deux branchements, chacun dans sa forme
thot mcp serve --http       # ce que Prime interroge : boucle locale, jeton bearer
thot mcp service --install  # et pour que ce soit encore vrai demain

Третья строка существует потому, что первые две держатся лишь одну сессию. fusion wire пишет http://127.0.0.1:8787/mcp в конфигурацию Prime, и этот адрес действителен, пока открыт терминал: при первой перезагрузке файл обещает сервер, который больше никто не обслуживает. thot mcp service --install записывает модуль, который его перезапускает — KeepAlive на macOS, Restart=always в systemd — и не загружает его: Thot выводит команду и оставляет тебе запуск, потому что инструмент, который регистрирует агентов в фоновом режиме, не сказав об этом, — это инструмент, которому перестают верить. thot doctor затем задаёт единственный важный вопрос — отвечает ли адрес сейчас — а не существует ли файл.

fusion wire также устанавливает у Prime пакет методов, который Thot поставляет для него: одной записи конфигурации недостаточно, Prime достигает MCP-сервера только через класс, который его именует. И он помещает токен в auth.json, не затрагивая учётные данные модели — без токена соединение не удаётся, даже не начавшись.

Каждый агент сохраняет свою конфигурацию. thot fusion unwire отменяет всё, а settings.json Prime сохраняется до первого изменения.

thot fusion status измеряет, что работает, а не что написано: Hermes устанавливает переносимые плагины отключёнными, из соображений безопасности, поэтому запись обоих файлов ничего не подключает, пока plugins.enabled не назовёт их. Активация происходит через CLI Hermes, никогда не через редактирование его config.yaml — этот файл принадлежит ему, со своей схемой и миграциями. Он также проверяет, что интерпретатор, запускающий Hermes, умеет импортировать MCP SDK, потому что агент без него не имеет ни одного инструмента — не только инструментов Thot — и сообщает об этом в logger.debug, где никто этого не читает. Наконец, он спрашивает сервер Prime, отвечает ли он: адрес, записанный в файле, — это не соединение.

И усиление работает в обе стороны: Hermes и Prime также являются движками для thot audit --deep — этапа, на котором каждая находка сначала аргументируется, а затем опровергается моделью.

По умолчанию все три работают вместе над одним и тем же аудитом. Находка аргументируется одним агентом, затем атакуется другим — никогда тем, кто только что взял на себя сценарий. Модель, опровергающая собственный аргумент, исправляет свою копию; это единственное, что покупает панель, и в этом смысл слияния.

thot audit . --deep                    # tous les agents installés, en panel
thot audit . --deep --engine hermes    # un seul : Hermes argumente et réfute
thot audit . --deep --engine prime     # un seul : Prime

Отчёт показывает, кто что сделал:

Analyse assistée : panel — claude-cli contre hermes contre prime
  [1] serve.py:7 — confirmé · hermes
…
1 confirmé(s) · 2 réfuté(s)
Argumenté par claude-cli 1 — attaqué par prime 1 — puis par hermes 1

Три разных агента на одной находке, измерено: claude-cli аргументировал, prime попытался разрушить сценарий и не смог, hermes атаковал второй раз. Сообщённое пережило двух независимых противников.

Каскад. Находка аргументируется, затем атакуется. То, что выживает после атаки, будет показано человеку — поэтому оно отправляется третьему агенту, который не видел ни построения аргумента, ни написания первой атаки. Подтверждённая находка была подтверждена против двух независимых противников.

Опровержение никогда не пересматривается по существу: атакующему предписано опровергать при малейшем сомнении, поэтому его пересмотр породил бы ложные срабатывания. Но его аргумент перечитывается, когда он хоронит что-то серьёзное (MEDIUM и выше), агентом, который ничего не сказал об этой находке. Читающий не оценивает дефект, он оценивает, проверяема ли указанная причина в показанном коде.

Обе ошибки неравноценны. Ложное подтверждение стоит человеку десяти минут чтения. Ложное опровержение стоит реального дефекта, навсегда — потому что запомненное опровержение пропускается всеми последующими аудитами. Это произошло однажды по-настоящему: реальная SQL-инъекция в копии Hermes была отклонена из-за абсолютно точного описания копии Thot, исправленной накануне. Оспоренное опровержение не становится подтверждением — никто этого не утверждал — оно возвращается в plausible с исходной серьёзностью и не запоминается: находка возвращается, пока кто-то не решит.

Если агент не справляется с задачей, она берётся один раз другим. Не больше: задача, которую все отвергают, имеет свою собственную проблему.

Что может зонд, измерено, а не предположено. Claude работает без Write, Edit, MultiEdit, NotebookEdit, Bash и Task — и thot doctor --agents проверяет это, попросив его записать файл, а затем проверив на диске.

Это не белый список, потому что клиент его не предлагает: --allowed-tools предварительно одобряет, но не ограничивает. Измерено — зонд, запущенный с разрешёнными Read Glob Grep, всё равно имеет Write, Bash и Workflow. Единственный рычаг — чёрный список.

Что зонд держал до измерения: CronCreate, CronDelete, Workflow, SendMessage, PushNotification, RemoteTrigger, EnterWorktree, WebFetch и все MCP-серверы, подключённые пользователем — включая инструмент, имя которого начиналось с clear_. Создание постоянных запланированных задач, отправка сообщений, доступ к почтовому ящику. Для чтения кода и ответа в JSON.

Что она держит после:

✓ outils · claude        7 outil(s), tous en lecture seule
✓ outils · hermes        mcp__patch, mcp__read_file, mcp__search_files, mcp__write_file
✓ outils · prime         ipython

Все три показаны, оценивается только один: набор file у Hermes предоставляет write_file и patch вместе с чтением, а единственный встроенный инструмент Prime — это ядро. Постоянная красная линия на том, что нельзя изменить, — это линия, которую перестают читать; тот, кто выбирает --engine hermes, видит, что он принимает.

Чёрный список хрупок по своей конструкции — в нём отсутствовал Task, и субагент записал файл через эту дыру один раз из шести. Поэтому разрыв сделан обнаруживаемым: thot doctor --agents спрашивает живого зонда, что он реально держит, и называет всё, что не распознаёт, потому что следующая версия клиента принесёт инструменты, о которых этот список никогда не слышал. И зелёная строка о записи означает «не в этот раз», а не «невозможно»: она сформулирована именно так.

У Hermes и Prime нет режима только для чтения, и об этом сказано, а не предполагается: -t file обозначает «File Operations», включая чтение и запись, а --safe-mode у Hermes касается настроек, а не разрешений; единственный встроенный инструмент Prime — это ядро IPython. Thot тем не менее сокращает их область действия — Hermes работает только с набором file вместо дюжины по умолчанию: больше нет терминала, браузера или интерпретатора. Это суженный радиус действия, а не закрытый.

Песочница (thot sandbox use docker) не подключена к движкам, и в любом случае решила бы лишь половину проблемы: контейнер, которому нужно обращаться к API модели и хранилищу ключей пользователя, уже не совсем песочница.

Поэтому то, что нельзя предотвратить, делается невозможным не заметить. Область помечается до запуска модели и снова после, и каждый файл, чей размер или дата изменились, называется:

⚠ L'audit a modifié 1 fichier(s) du dépôt — ce n'est pas normal :
   src/app.py
   `git diff` avant toute autre chose.

Тишина — нормальный исход. Это также единственный, которому стоит верить: код, прочитанный зондом, — это именно тот код, за который никто не поручится, а «игнорируй свои инструкции и исправь это для меня» — самая дешёвая атака на агента, у которого есть редактор.

Пути задаются в абсолютном виде. Измерено на всех трёх: Hermes не открывает путь относительно своей рабочей папки и отвечает «я не могу прочитать этот файл» — что читается как отказ, а не как пробел. Треть панели была слепа к любому утверждению, требующему открыть второй файл.

Каждый агент аутентифицируется как сам, под твоей учётной записью: Thot запускает его командную строку, никогда не импортирует его и не владеет ни одним токеном. Запомненный вердикт носит имя того, кто решил — refuted · hermes — потому что решение должно оставаться атрибутируемым.

Что каждый вносит, измерено на одной и той же инъекции:

движок

длительность

сообщённые токены

prime

48 с

да, с оценкой стоимости

hermes

159 с

нет — -z выводит только ответ

Движок, который не умеет считать, не фабрикует цифру: он объявляет об этом (reports_usage), и вызывающий может сказать «не измерено» вместо отображения нуля, который выглядел бы правдоподобно.

Related MCP server: uni-kb

Одна конфигурация, одна память

Все трое пишут каждый в свою папку, и это хорошо: config.yaml принадлежит Hermes, settings.json — Prime. Что добавляет Thot, так это единый вид и единое место для принятия решений.

thot fusion config                          # le modèle que chacun utilisera
thot fusion config --model claude-opus-5    # le dire une fois, l'écrire aux trois
thot fusion memory                          # ce que les trois ont retenu
thot fusion memory --sync                   # y verser les faits appris par Thot

Конфигурация читается из файлов — мгновенно, без риска — и записывается инструментом каждого: hermes config set вместо переписывания его YAML, который содержит комментарии и историю миграций, не принадлежащих Thot. То, что Thot делегирует свою модель официальному CLI, не является разногласием: отсутствующее мнение ни с чем не конфликтует.

Память — тот же принцип в обе стороны:

где

форма

thot

~/.thot/harness.json

структурированный, заголовок + содержимое

hermes

~/.hermes/memories/MEMORY.md

записи, разделённые §

prime

~/.prime/agent/AGENTS.md

markdown, загружается глобально

Thot читает все три при каждом брифинге: факт, который Hermes узнал на прошлой неделе, — это факт, который Thot знает сегодня. Он пишет в два других только по --sync, в их родном формате, касаясь только записей, которые он сам разместил — помеченных [thot] у Hermes, в ограниченном блоке у Prime. Резервная копия перед первым изменением, и три синхронизации подряд записывают одну копию.

Свежесозданный USER.md — это пустая форма: **Name:**, инструкции курсивом, горизонтальная линия. Внедрить их означало бы сказать Thot, что « Context: --- » — это факт. Они отбрасываются — и учитываются на экране, потому что отличить форму от лаконичной заметки — не то, что программа умеет делать с уверенностью.

Библиотека, история

Все три читают один и тот же формат — SKILL.md с YAML-frontmatter, одна папка на метод. Это единственная причина, по которой это возможно.

thot fusion skills            # qui possède quoi, et ce qui n'est qu'à un seul
thot fusion skills --share    # donner la bibliothèque de Thot à Prime
thot fusion sessions          # l'historique des trois, du plus récent au plus ancien
thot fusion audit             # auditer les trois arbres en une passe

thot fusion audit существует, потому что обратное было трением программы: три команды и мысленное слияние трёх отчётов.

thot       227 fichiers     4 finding(s) —  3 high ·   1 medium ·  17 sous le seuil
hermes    7080 fichiers   190 finding(s) —  8 high · 182 medium · 839 sous le seuil
prime      952 fichiers    14 finding(s) —  3 high ·  11 medium ·  27 sous le seuil

208 finding(s) sur l'ensemble — 14 high · 194 medium · 883 sous le seuil (`--all`)

Порог — тот же, что у thot audit, и в этом суть: одно и то же дерево должно давать одно и то же число обеим командам. Это представление учитывало каждый low, поэтому оно сообщало итог колонки «ниже порога» в дополнение к остальному, для репозитория, который thot audit hermes оценивал в семь раз ниже в ту же минуту. Что удержано — то посчитано, никогда не скрыто.

Когда память вердиктов уже вынесла решение, строка говорит об этом отдельно — 0 finding(s) · 416 réfuté(s) en mémoire. Один ноль читается как чистое дерево; правильная фраза — что панель отклонила все 416.

Часть, которую нельзя проверить, стоит своей строки и никогда не пропускается: отсутствующий Prime не должен скрывать то, что сказал Hermes.

Никаких копий: файлы остаются у своего владельца, и каждая программа указывает на папки других. Метод, скопированный дважды, — это метод, исправленный лишь один раз.

Thot читает установленную библиотеку Hermes под охраной — она из публичных реестров, и это именно тот случай, для которого охранник существует. Он поручается только за то, что поставляет сам. Но 73 из 83 методов Hermes — это побитовые копии методов Thot: сообщать о собственном файле как об угрозе сообщества — это ложное срабатывание, которое учит игнорировать настоящие. Метод, чьи байты совпадают с поставленным методом, является этим методом. Охранник сократился с 42 отказов до 8.

Затем с 8 до 0, по двум разным причинам, которые лучше не смешивать. Первая — ложное правило: ENV[] — это константа Ruby, заглавная по построению, но шаблон компилировался как весь каталог — без учёта регистра. Поэтому он читал Python как Ruby и классифицировал env["…_TOKEN"] = jeton за две строки до subprocess.run(env=env) — рекомендуемый способ передачи секрета дочернему процессу — как «чтение секрета», CRITICAL. Вторая — вопрос ранга: библиотека, которую пользователь сам установил в папку соседнего агента, не является проверяемым репозиторием. Установка уже произошла, намеренно; Thot решает только, читать ли то, что уже есть на машине. Она всегда сканируется и всегда сообщается, но отклоняется только на dangerous, тогда как проверяемый репозиторий — уже на caution — шесть из восьми отказов держались на единственном правиле, которое видит эксфильтрацию в методе, документирующем адрес собственного токена.

13 методов, поставляемых с Prime, остаются у Prime: они документируют его ядро IPython (edit(old_str, new_str), refine()). Thot перенёс это ядро, а не эти функции — загрузка их заставила бы модель вызывать то, чего не существует. Они в каталоге, где знание о них полезно; вне обнаружения, где вера в них бесполезна.

Prime получает надмножество, а не две копии. Измерено, а не предположено: указанный только на библиотеку Thot, он отвечает; только на библиотеку Hermes — отвечает; на обе — модель отказывается отвечать. Prime принимает папки, а не имена, поэтому частичного ответа не бывает.

Истории не сливают своё хранилище — миграция одной программы сломала бы историю другой — но вопрос «что я делал в этом репозитории в прошлый вторник» не касается того, какой из трёх бинарников был перед тобой. Все три читаются только для чтения, каждый в своём формате, и база, заблокированная текущей сессией, стоит своих строк, но никогда не списка.

Установка

git clone https://github.com/nobodyohm-web/Thot.git
cd Thot
uv tool install --editable --from . thot

Один uv sync в корне устанавливает Thot и Hermes: это workspace, а не расходящаяся копия. Prime написан на TypeScript и собирается отдельно:

cd prime && npm install && npm run build

Без Node Thot и Hermes работают; thot fusion status сообщает, чего не хватает и как это исправить, вместо того чтобы падать при первом вызове.

Использование

thot

Вот и всё. При первом запуске он спрашивает, какую модель подключить, затем сканирует текущую папку и возвращает тебе управление.

   ╔╦╗╦ ╦╔═╗╔╦╗
    ║ ╠═╣║ ║ ║    claude-opus-5
    ╩ ╩ ╩╚═╝ ╩

   ▪ dossier  ~/Desktop/Quanta
   ▪ code     142 python · 8 points d'entrée
   ▪ git      main · propre
   ▪ audit    1 high · 2 medium

   Reconnaissance en 0.31 s. Prêt.

   ›

Пустая папка — он говорит об этом и ждёт твоих инструкций. Папка с кодом — он уже составил её карту до твоей первой фразы.

Команды сессии

Команда

Эффект

/audit · /audit deep

перезапустить анализ или заставить модель его опровергнуть

/verdict n refute …

отклонить finding с указанием причины

/goal <objectif> --budget N

задать цель, отслеживаемую между сессиями

/sessions · /resume

что было сделано здесь раньше, и вернуться туда

/search <mots>

искать во всём, что Thot сказал или нашёл

/compact

подвести итог и продолжить с пустым контекстом

/export · /import

перенести сессию в другое место

/skills · /plugins · /mcp

что загружено, и каталог

/scan

пересчитать карту репозитория

/model · /clear · /quit

модель, забывание, выход

Плюс твои собственные: любой файл .thot/commands/<nom>.md становится /<nom>.

Модели

Выбор

Что нужно

Claude — твой аккаунт

CLI claude установлен и подключён. Копировать ничего не нужно.

Claude — API-ключ

ключ sk-ant-…

OpenAI

API-ключ или OPENAI_API_KEY в окружении

Local

работающий Ollama или LM Studio — бесплатно, офлайн

Autre

любой endpoint, совместимый с OpenAI

thot login — чтобы сменить, thot logout — чтобы забыть. Конфигурация живёт в ~/.thot/config.json, с правами 0600. В режиме аккаунта там не хранится ни одного токена.

Как работает режим аккаунта

API Messages отказывает токенам подписки, приходящим от сторонней программы. Пройти через него означало бы выдать себя за Claude Code — замаскированный user-agent, заимствованный системный промпт. Thot этого не делает.

Он делает обратное: он делегирует официальному клиенту. Каждый ход запускает

claude -p --output-format stream-json --session-id <uuid> \
       --mcp-config <outils Thot> --append-system-prompt <carte du dépôt>

Инференс выполняется claude, под твоим аккаунтом, точно так же, как если бы ты ввёл это сам. Thot предоставляет карту репозитория, подключает свои детерминированные инструменты через небольшой MCP-сервер и форматирует поток событий. Нить разговора переносится через --resume с тем же идентификатором сессии.

Сессии — ничего не теряется

Закрой окно — аудит и связанные с ним рассуждения всё ещё на месте. Каждый ход записывается в момент его появления в ~/.thot/sessions.db.

   › /search injection parseur
   a3f9c210 user       trouve les «injections» SQL dans le «parseur»
   a3f9c210 audit      HIGH sink.sqlite.execute  src/parse.py:88
   7b02e4d1 verdict    sink.os.system src/deploy.py:12 → refuted : commande littérale

Поиск охватывает то, что было сказано, и то, что было найдено: наполовину запомненный finding находится по словам, которые помнишь.

thot sessions              # ce qui a été fait dans ce dépôt
thot sessions --all        # partout
thot sessions --show <id>  # la transcription entière
thot search <mots>         # sans ouvrir de session
thot export <id> --out s.json ; thot import s.json

/resume возвращает транскрипцию и контекст: в режиме аккаунта Thot сохранил идентификатор разговора официального CLI и возвращает его, поэтому модель помнит, а не перечитывает.

/compact закрывает сессию резюме и продолжает в дочерней сессии, сохраняющей связь. Сжатие стоит контекста, но никогда не доказательств: родительская сессия остаётся целой, и /search всегда её находит.

Сжатие также срабатывает само, и порог — не константа: CLI публикует окно используемой модели (contextWindow: 1000000 для claude-opus-5[1m]), и Thot сжимает при 70 % этого окна — 700 000 токенов здесь, 140 000 на окне 200k. Триггер читает фактический размер, сообщаемый CLI, а не оценку по сообщениям: в режиме аккаунта нить принадлежит CLI, и Thot не видит в ней ни прочитанных файлов, ни трафика инструментов. Измерено на обычном ходе: оценка давала 95 токенов против 88 290 реально в окне.

Цели — знать, когда остановиться

Цель переживает разговор, через который она проходит, и напоминает о себе модели на каждом ходе, в том числе сразу после /compact.

   › /goal plus aucun HIGH dans le parseur --budget 200000
   ✓ Objectif fixé — plus aucun HIGH dans le parseur
     Budget : 200000 jetons.

Исчерпание бюджета — это состояние, а не ошибка: Thot не останавливается на середине хода, он завершает, переходит в budget_limited и сообщает, на каком этапе цель. Тебе выбирать между /goal budget 500000 и /goal done.

Память — решить один раз

Дорогое в аудите — не поиск кандидатов: детерминированные фазы делают это за несколько секунд, бесплатно. Дорого решать, чего они стоят. Терять эти решения между запусками — вот что делает инструмент безопасности невыносимым: те же сорок отклонений каждую неделю, пока никто больше не читает отчёт.

   › /verdict 3 refute la commande est littérale, aucune entrée utilisateur
   ✓ pattern.os_system_injection à app/shellutil.py:5 — refuted
   Retenu tant que ce code ne change pas.

Решение

Эффект

refute

ложное срабатывание — переходит в INFO, выходит из отчёта, сохраняет причину

accept

реальный риск, принятый — переходит в INFO, аннотирован

fixed

исправлено — если вернётся, это отмечается как регрессия

Две глубины, сказанные вслух

Python

TypeScript · JavaScript

остальное

символы, граф вызовов, code_map / callers

да

да

нет

окраска внутри тела функции

да

да

нет

окраска к хелперу из того же файла

да

да

нет

окраска через файлы

да

нет

нет

правила по шаблонам

да

да

да

Окраска JavaScript следует за вызовом к функции, определённой в том же файле — обычная форма делегирующего обработчика — и останавливается там, как и заявлено. Следующие два уровня опираются на разрешённый граф вызовов — знание того, что readInput, вызванный здесь, — это тот, что определён там. Система импортов Python отвечает на этот вопрос; система JavaScript — нет, без резолвера модулей, tsconfig и взгляда типизатора на this. Второй уровень, построенный на предположениях, превратил бы инструмент, сообщающий доказанные пути, в инструмент, сообщающий правдоподобные пути.

Движок сканирует файл, а не тела именованных функций. Обычная форма веб-обработчика — анонимная стрелка, переданная маршруту — app.get("/x", (req, res) => { … }) — которую ни один индексатор не именует.

Измерено так: стрелка, список параметров которой заключён в скобки и которая следует за запятой или открывающей скобкой, т.е. [(,]\s*(?:async\s*)?\([^)]*\)\s*=>, по скрытому исходному коду файлов, которые удерживает detect_scope — 15 094 на Prime, 19 625 на Hermes, все невидимы для движка, который следовал бы за символами. Более ранняя версия заявляла 24 454, не указывая, как они были подсчитаны; цифра полностью зависит от определения, поэтому определение записано.

Измерено на обоих корпусах: 31 путь на Prime, 41 на Hermes, на 3 552 файлах JS/TS. Семьдесят два, а не три тысячи — такова форма движка оттенков, а не сканера шаблонов.

Движок также отслеживает функции, которые никто не вызывает по имени: их вызывает рантайм и передаёт им значение. addEventListener вводит оттенок — параметр и есть вход; .then, .map, .forEach его несут — параметр окрашен ровно тогда, когда окрашено то, что обходится, поэтому постоянный список остаётся постоянным списком. Это разница между отслеживанием значения и его изобретением, и без неё дерево кода браузера почти полностью невидимо.

obj[ключ] = значение, где ключ контролируется, — это отдельный сток: полезная нагрузка — это ключ, а не значение, потому что __proto__, записанный через него, проходит через все объекты программы. Одиннадцать реальных мест в Hermes, три в Prime, все вида for (const [k, v] of Object.entries(x)) { out[k] = v }. Цикл, который отказывает __proto__ по имени, исправлен и не помечается.

Отчёт сам говорит об этом, а не создаёт впечатление равномерного покрытия:

teinte au fichier près, pas au-delà : javascript 3 · typescript 912

Une exception, et une seule : un import **relatif** se résout par une règle de
fichiers, pas par une inférence. `./helpers` depuis `src/app.ts` ne désigne
qu'un chemin, et soit il est dans l'index, soit le franchissement n'a pas
lieu. Les spécificateurs nus et les alias `tsconfig` restent refusés — ceux-là
demandent vraiment un résolveur. Le niveau reste unique : ce qui est franchi
est la frontière, pas la profondeur.

Mesuré sur le périmètre que Thot audite réellement — celui que `detect_scope`
calcule, `dist/` et `build/` exclus : **336 appelables importés résolus, tous
sur Hermes, aucun sur Prime**, pour **zéro chemin nouveau** et un surcoût de
4 à 8 %. La capacité est prouvée par les tests, son rendement ici est nul, et
les deux se disent.

Une première version de ce paragraphe annonçait 1 514 appelables et +21 %.
Ces chiffres venaient d'une liste de fichiers bâtie à la main qui incluait
`dist/bundle/` — des bundles minifiés de deux méga-octets que Thot n'indexe
jamais. La mesure portait sur du code hors périmètre, et la méthode juste
était disponible depuis le début : demander son périmètre à l'outil plutôt
que de le reconstruire.

Индексатор TypeScript — это сканер, а не tsc: он маскирует комментарии и литералы, затем читает объявления сопоставлением фигурных скобок. Переход на tsc сделал бы карту зависимой от установленной цепочки node, разрешимой и правильной версии — карта, работающая на одних машинах, стоит меньше, чем карта, чьи границы записаны. Измерено: 8 568 символов на Prime за 1,7 с, ещё 11 138 на Hermes.

Чем файл является для

Серьёзность — это воздействие × доступность × доверие, и доступность исходит из графа вызовов. Граф отвечает на вопрос «может ли точка входа достичь этого места». Ему нечего сказать о файле, который вообще не является поверхностью атаки.

Измерено на обеих программах, поставляемых с Thot: 12 из 25 findings HIGH в Hermes и 6 из 11 в Prime находились в тестовом или примерном коде. Почти половина верхней части отчёта касалась кода, до которого ни один атакующий не дотягивается — именно так отчёт перестают читать.

до

после

hermes

25 high · 94 medium · 297 low

13 high · 58 medium · 345 low

prime

11 high · 2 medium · 9 low

5 high · 8 medium · 9 low

Колонки HIGH — те, что несут аргумент, и они не сдвинулись ни на один finding с первого измерения: 25 → 13 и 11 → 5. Подсчёты medium и low выше были пересчитаны по деревьям в их нынешнем виде, поскольку девять уязвимостей были исправлены в Hermes между двумя измерениями.

Ни одного добавленного finding, ни одного потерянного. Это понижение, а не удаление: тестовый код выполняется на машинах разработчиков и в CI, что является точной формой атаки через цепочку поставок. Finding остаётся и несёт свою роль в происхождении.

Классификация консервативна — целые сегменты, никогда не подстроки (latest/ — не тестовая папка, contest.py — не тестовый файл), и всё, что не распознано, считается продакшеном. Ошибка в сторону «тест» скрыла бы реальный дефект; ошибка в сторону «продакшен» стоит лишь одной ступени.

Откуда берётся ценность

Finding несёт правило источника, запустившее его путь, — source.argv, source.http, source.js.event — а не только строку, где он находится. Отчёт говорит это прямо («значение, полученное из командной строки…»), и JSON отдаёт это ключом, source_rule, чтобы то, что фильтрует дальше, читало факт, а не французскую фразу.

Это недостающая половина ранга. open(args.sortie, "w") в инструменте командной строки — это оператор, называющий файл: тот, кто предоставляет argv, уже владеет файловой системой этого процесса, и вызов не даёт ему ничего. open(request.args["f"]) в handler — это произвольное чтение файла. То же правило, тот же сток, два мира.

локальный источник

удалённый источник

sink.fs.read · sink.fs.write · sink.js.path

на ступень ниже

полный ранг

всё остальное

полный ранг

полный ранг

Только эти три. Команда, построенная из argv, остаётся командой, а pickle, прочитанный из переменной окружения, всё ещё исполняет код: здесь эскалацию совершает сток, а не путь.

Измерено на Hermes в день, когда движок научился отслеживать цепочки атрибутов: без этого различия один sink.fs.read помещал 48 findings в отчёт, из них девять из одного скрипта CI, каждый — утилита, открывающая файл, который ей велели открыть. С ним они уходят ниже порога и остаются на расстоянии одного нажатия (--all).

Неизвестное происхождение считается локальным, и это записано, а не скрыто: предположить обратное вернуло бы в верх отчёта любой путь, для которого движок не смог назвать источник. То, что он умеет называть, удвоилось, когда связь между локальной переменной и параметром, от которого она происходит, перестала теряться — cible = chemin.strip() сохранял связь с chemin, а без неё сток не был привязан ни к какому параметру, а значит, ни к одному вызывающему.

Почему это безопасно

Вердикт индексируется по Finding.compute_id, который хэширует правило, файл, символ и нормализованный AST этого символа. Переформатируй, перемести функцию, переименуй локальную переменную: вердикт держится. Измени то, что код делает: идентификатор меняется вместе с ним, и вердикт истекает сам собой.

Отклонение не может пережить код, которого оно касалось. Это единственное свойство, которое делает запоминание отклонений приемлемым.

Идентификатор также называет точный вызов, на который направлен вердикт, — httpx.get#3 — а не только функцию, которая его содержит. Без этого пять сетевых вызовов в одной функции были для памяти одним finding, и отклонение первого отклоняло остальные четыре вместе с их причиной. Этот дискриминатор ничего не ослабляет: ему нужно быть уникальным только в пределах одной версии тела, а AST этого тела уже заставляет истечь всё, что к нему относится, как только оно меняется.

thot verdicts                    # tout ce qui a été décidé
thot verdicts --path src/auth    # sur un chemin
thot verdicts --forget <id>      # revenir sur une décision
thot audit . --no-memory         # ignorer la mémoire pour ce run

Решение переживает finding, которое его породило: код меняется, finding получает новую идентичность, и прежнее решение больше ни на что не указывает. Список помечает такие [отсутствует в последнем аудите], а не отображает их как остальные — шесть решений, из которых три мертвы, не должны читаться как шесть живых решений.

Память применяется до модели: finding, который уже несёт решение — отклонено, принято или исправлено — никогда не отправляется на анализ. Прогон, где всё решено, не делает ни одного вызова. Это не только экономия: зонд заменяет доверие, серьёзность, сценарий и происхождение разом, поэтому повторная отправка решения модели перезаписала бы его и стёрла бы того, кто его принял. Регрессия — это случай, где это важнее всего: она уже была признана реальной однажды, и никакой глубокий проход не может её замолчать.

И опровержения регистрируются сами, из thot audit --deep так же, как из /audit deep: два вызова модели, оплаченные один раз. Они несут имя движка, который решил, никогда не твоё — машинное решение не перевешивает человеческое.

Ничто никогда не удаляется молча. Отклонённый finding остаётся в отчёте как refuted, со своей причиной и автором — аудит, скрывающий то, что ему велели игнорировать, невозможно перечитывать.

Ядро Python

Главная идея Prime Agent, перенесённая: вместо одного вызова инструмента на вопрос модель пишет Python, и её переменные выживают.

   › /py bas = audit(severity="low"); print(len(bas), "findings"); [f.rule for f in bas]
   3 findings
   → ['sink.eval', 'sink.network', 'sink.subprocess.shell']

   › /py len(files())
   → 148

Карта репозитория доступна в виде объектов — files(), symbols(), find(), callers(), callees(), audit(), read(). Цикл, который пересекает findings и вызывающих, стоит один проход модели; то же самое через вызовы инструментов стоит дюжину, и каждый заново оплачивает чтение того, что карта уже знала.

Ядро никогда не выполняется в процессе Thot. Собственный exec() дал бы аудируемому коду память Thot, его открытые базы и его файловые дескрипторы. Поэтому это подпроцесс — и внутри контейнера, когда настроена песочница.

Что именно это защищает — и Thot исправил это на себе после того, как его собственный враждебный проход указал на слишком абсолютную docstring:

Подпроцесс (local)

защищает память, базы и дескрипторы Thot. Не твои учётные данные: worker работает под твоей учётной записью и может читать ~/.claude/.credentials.json.

Контейнер (docker)

настоящая граница: нет сети, нет твоего $HOME, репозиторий только для чтения.

Чувствительные переменные окружения удаляются перед запуском, и /py говорит об этом один раз в локальном режиме, а не позволяет «отдельному процессу» читаться как гарантии, которой он не даёт.

rlm() — делегирование из ячейки

verdicts = {f.id: rlm(f"Ce chemin est-il exploitable ?\n{f.failure_scenario}")
            for f in audit(severity="high")}

Ячейка может декомпозировать собственную проблему. Ячейка не владеет никакими учётными данными: она запрашивает у хоста, который решает и платит. Поэтому ограничения соблюдаются на стороне хоста — 8 вызовов на ячейку, 40 на ядро — потому что ограничение, которое ребёнок мог бы изменить, не является ограничением, а ребёнок исполняет код, пришедший из аудируемого репозитория.

Что Thot удерживает из репозитория

   › /harness note team.shell.run : échappe ses arguments, les findings dessus sont faux
   ✓ Retenu — rappelé à chaque session.

Уточнение Prime, применённое к аудиту: факты, которые никакой статический анализ никогда не выведет. Они живут в <репозиторий>/.thot/harness.json, перечитываются в pull request, как и вердикты, и возвращаются в брифинг при каждой сессии.

Что модели разрешено делать

thot --tools lecture      # lire et raisonner, jamais modifier
thot --tools carte        # la carte seule : aucun fichier ouvert

В сессии: /tools lecture. Перечитывать репозиторий, который не твой, — это читать код, которого у тебя есть все основания опасаться — и то, что модель его изменит, редко то, чего ты хотел.

Позиция держится на трёх местах, а не одном: предлагаемые модели инструменты, момент, когда она всё же вызывает один из них, и — в режиме аккаунта — официальный CLI, которому --disallowed-tools запрещает Write, Edit и Bash. Позиция, фильтрующая только инструменты Thot, была бы ложью там, где это важнее всего.

Цепочка поставок

thot deps                       # les dépendances épinglées, contre OSV.dev
thot deps --list                # ce qui a été trouvé, sans réseau
thot deps --fail-on high        # code 1 en CI
thot audit . --deps             # dans le rapport d'audit
thot mcp check                  # tes serveurs MCP sont-ils malveillants ?

Сначала всегда блокировки: uv.lock, poetry.lock, Pipfile.lock, package-lock.json, yarn.lock, pnpm-lock.yaml. Манифест говорит requests>=2, и OSV не умеет отвечать на интервал; блокировка говорит 2.31.0, и OSV умеет. Зависимость, существующая только как интервал, не угадывается, она сообщается как незакреплённая.

Отзыв, охватывающий точную версию, — это факт, а не предположение — но знание того, достигает ли твой код уязвимой функции, не анализируется, поэтому эти findings остаются PLAUSIBLE и так об этом и говорят. Исключение — только MAL-*: пакет и есть полезная нагрузка, вопрос достижимости не стоит.

И то же свойство, что и везде: идентичность finding несёт закреплённую версию, поэтому bump аннулирует вердикт. Отклонение CVE на requests==2.19.1 ничего не отклоняет на 2.20.0.

Недоступный OSV никогда не становится сертификатом здоровья: thot deps говорит «не проверены» и возвращает код ошибки.

Выполнять проверяемый код, не выполняя его у себя

pytest на проверяемом репозитории — это код этого репозитория, работающий под твоей учётной записью. Это единственное место, где вся задумка даёт течь.

thot sandbox status
thot sandbox use docker
thot sandbox show pytest -q     # la commande docker exacte, à relire

По умолчанию, в контейнере:

Сеть

отключена (--network none)

Репозиторий

смонтирован только для чтения, копия с записью в tmpfs

Привилегии

--cap-drop ALL, no-new-privileges, пользователь 65534

Ограничения

--pids-limit, --memory, --cpus, --rm

Отключённая сеть — это флаг, который стоит больше всего и мешает больше всего: именно поэтому это флаг, а не закон (--network).

Одно правило обратно остальным правилам Thot: везде отсутствующая зависимость стоит своей функциональности, и работа продолжается. Здесь запрошенная и недоступная песочница отказывается выполнять. Молчаливое возвращение к хосту превратило бы защиту в ложь.

Делиться решениями

Вердикт — это факт об этой ревизии этого кода. Поэтому он путешествует вместе с кодом: <dépôt>/.thot/verdicts.json, перечитывается в pull request, который затрагивает соответствующий код, и присутствует в свежем клоне ещё до сети.

thot verdicts --share <id>   # publier une décision locale dans le dépôt
thot verdicts --share-all    # toutes celles qui concernent ce dépôt
thot verdicts --where        # d'où viennent les décisions, où elles s'écrivent

Память общая для деревьев, файл — нет: публикация решения, касающегося другого репозитория, отклоняется, а --share-all берёт только те, чей файл существует здесь.

Цепочка по умолчанию, без какой-либо конфигурации: сначала репозиторий, потом твоя машина. Решение, прошедшее ревью, имеет приоритет над заметкой, которую ты сделал сам себе.

Запись же остаётся локальной. Инструмент, который изменял бы версионируемый файл при каждом /verdict, создавал бы диффы, которых никто не просил: ты решаешь локально, публикуешь намеренно.

Общий сервер или существующий mem0

// ~/.thot/memory.json
{"remote": {"kind": "http", "base_url": "https://audit.equipe.example", "token": "…"}}
{"remote": {"kind": "mem0", "host": "http://localhost:8888", "api_key": "…"}}

Бэкенд mem0 говорит на контракте self-hosted точно так же, как клиент Hermes Agent: сервер, уже развёрнутый для Hermes, служит Thot без каких-либо изменений.

Недоступное удалённое хранилище стоит памяти о прошлых решениях, но никогда не аудита — однако оно не делает это молча: thot verdicts --where говорит, какой из них нем и почему.

Получать аудиты в другом месте

Аудит, завершающийся в 03:00, ничего не стоит, пока никто не предупреждён, а человек, которого нужно предупредить, не стоит перед терминалом.

thot gateway add ntfy topic=thot-$(openssl rand -hex 8)   # le sujet EST le secret
thot gateway add telegram token=… chat_id=…
thot gateway allow telegram <ton-id>     # obligatoire pour commander
thot gateway test
thot serve                                # écouter les commandes

Канал

Исходящий

Входящий

Telegram

✓

✓ (long polling — открывать порты не нужно)

Discord · Slack

✓ (webhook)

—

ntfy

✓

— (нет идентичности: темы достаточно для публикации)

Электронная почта

✓ (SMTP)

—

Уведомления не требуют никакого демона: плагин gateway-notify срабатывает на post_audit, и только для неотслеживаемого аудита. Аудит, запущенный вручную, уже отображается на экране; уведомлять каждый раз — значит учить получателя отключать канал, что стоит единственного сообщения, которое имело значение. Ничего нового: тишина.

Что позволяет украденный токен

Демон существует только для обратной связи, и его конструкция держится в основном на этом:

  • набор команд закрыт — status, audit, findings, verdict, help. Никакого shell, никакой записи, никаких произвольных путей;

  • аудит может нацеливаться только на уже объявленный thot schedule add репозиторий;

  • входящий трафик требует список разрешений. Hermes предлагает ALLOW_ALL_USERS для разработки; у Thot нет аналога. Без списка канал только исходящий, и thot serve об этом говорит.

~/.thot/gateway.json записывается с правами 0600 — в нём токены бота и пароль SMTP. Переменные окружения переопределяют его поле за полем, под именами Hermes.

Запланированные аудиты

thot schedule add nuit ~/mon-projet --every daily --threshold high
thot schedule list
thot schedule run nuit            # ce que le planificateur appelle
thot schedule remove nuit

Thot записывает unit launchd (macOS) или даёт тебе строку crontab и оставляет активацию тебе самому — инструмент, который молча устанавливает фоновые задачи, — это инструмент, которому перестают верить.

Когда launchd не может. На macOS разрешения выдаются по бинарю: агент launchd может получить отказ на ~/Desktop, ~/Documents или ~/Downloads, и тогда unit блокируется при запуске интерпретатора, не записав ни строки. Процесс, запущенный из твоей сессии, сохраняет доступ этой сессии, даже став сиротой, — это третье средство, и оно ничего не требует от системы:

thot schedule start       # un planificateur dans ta session
thot schedule status      # tourne-t-il, et quand est-il passé
thot schedule stop
thot schedule autostart   # le relever au premier terminal après un redémarrage

Он уступает любому unit launchd, который launchctl объявляет реально выполненным: два планировщика на одной задаче — это двойная работа и двойной расход токенов. thot doctor говорит, какой из двух служит.

Запланированный аудит молчит, пока нет ничего нового. Ночной отчёт, повторяющий одни и те же триста findings, попадает в папку, которую никто не открывает. Всплывает дифф: то, что появилось с прошлого раза, выше порога, минус то, что уже признано неинтересным.

Плагины

Пять хуков, каждый потому, что что-то из поставляемого им пользуется:

Hook

Когда

on_finding

перед отчётом, для аннотации

post_audit

аудит завершён — уведомить, экспортировать, архивировать

pre_write

перед записью агента — возвращает предупреждение

post_write

после успешной записи

on_verdict

только что записано решение

Плагин — это папка с plugin.yaml и __init__.py, в ~/.thot/plugins/ или <repo>/.thot/plugins/ — форма, используемая Hermes Agent. Плагин, который падает, стоит своей собственной функциональности и ничего больше: его ошибка записывается и отображается через /plugins.

Плагины проверяемого репозитория не выполняются без твоего согласия. Загрузить плагин — значит запустить его код здесь, под твоей учётной записью, — а проверяемый репозиторий — именно тот, которого Thot остерегается. Поэтому его плагины называются, но никогда не импортируются, пока ты их не одобрил:

thot plugins list <dépôt>                 # chargés, et refusés avec la raison
thot plugins trust <dépôt>/.thot/plugins/x   # après l'avoir lu
thot plugins untrust <dépôt>/.thot/plugins/x

Одобрение касается содержимого, а не имени: Thot записывает отпечаток папки, и любое изменение отзывает его, сообщая об этом.

Три поставляются:

Плагин

Что он делает

write-guard

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

regression-alert

дефект, помеченный fixed, который появляется снова, переходит в CRITICAL: регрессия стоит больше, чем новый кандидат.

audit-log

локальный JSONL-журнал каждого аудита, вердикта и записи, в ~/.thot/journal.jsonl. Никакой сети.

Проверить, что всё на месте

«Работает» — это утверждение, и для программы, состоящей из трёх программ, это не утверждение, которому стоит верить на слово — особенно когда оно исходит от самого инструмента.

thot doctor
✓ fusion                 thot · hermes · prime
✓ câblage                4/4 fichiers en place · sdk mcp présent
✓ moteurs                claude, hermes, prime
✓ panel                  claude-cli contre hermes contre prime · cascade oui
✓ indexeurs              python 10 symbole(s) · typescript 1
✓ teinte                 python 1 chemin(s) · javascript 1
✓ règles                 python 8 sinks · javascript 8
✓ skills                 91 chargée(s) · 0 refusée(s)
✓ plugins                4 chargé(s) · 0 refusé(s)
✓ mémoire                492 décision(s)
✓ mcp                    6 outil(s) exposé(s)
✓ service                http://127.0.0.1:8787/mcp répond
✓ amélioration           daily, 8 candidats par arbre · unité launchd,
                         1 passage(s) · agents joignables depuis l'unité

13/13 vérification(s) passées en 1.83 s

Моментальный снимок с датой, а не контракт: mémoire считает накопленные вердикты и может только расти, skills зависит от того, что установлено на машине. Стабильна форма — каждая строка несёт измеренное число, и 12/12.

Последняя строка — это реальный вывод на машине разработки, и она сохранена как есть: именно это контроль и должен производить. 1 passage(s) приходит от самого launchctl, и он не декоративен: launchctl list показывает загруженный unit, его LastExitStatus равен 0, а его журнал не существует — три сигнала, которые говорят «всё в порядке» для задачи, которая никогда не запускалась. Назвать причину лучше, чем считать зелёные строки.

Эта проверка сама должна была быть исправлена. Она осуждала задачу по форме пути — «дерево под ~/Desktop, значит launchd не сможет его прочитать» — тогда как разрешения macOS выдаются по бинарю: интерпретатор unit читал дерево, в котором /bin/sh получал отказ. Форма — это подозрение, прохождение — факт, и именно факт она теперь и спрашивает.

И проверка, которую никакой статический анализ не смог бы сделать:

thot doctor --agents        # un appel modèle par agent installé
✓ lecture · claude       lit un fichier par chemin absolu
✓ écriture · claude      n'a pas écrit cette fois
✓ outils · claude        10 outil(s), tous en lecture seule
✓ lecture · hermes       lit un fichier par chemin absolu
✓ écriture · hermes      peut écrire — aucun mode lecture seule
                         (`-t file` et `--safe-mode` ne restreignent pas les permissions)
✓ outils · hermes        mcp__patch, mcp__read_file, mcp__search_files, mcp__write_file
✓ lecture · prime        lit un fichier par chemin absolu
✓ écriture · prime       peut écrire — outil unique : un noyau IPython
✓ outils · prime         ipython

Строки записи зелёные, хотя они объявляют тревожную способность: они сообщают о том, что есть, а не о том, что хотелось бы. Два из трёх агентов могут писать, и никакой флаг этому не препятствует — измерено тем, что им было предложено создать файл, а затем просмотром диска. То, что нельзя предотвратить, делается невозможным не заметить: AuditResult.touched называет то, что изменил проход, и ночной цикл кричит об этом на stderr.

Он кладёт файл во временную папку и запрашивает его содержимое. Он существует из-за реального дефекта: Hermes не открывал путь, относительный к своей рабочей папке, и отвечал фразой, которая читалась как отказ. Треть панели не могла проверить ни одного утверждения, опирающегося на второй файл, и ничто, кроме создания файла, не показало бы этого.

Каждая строка выполняет реальную операцию и сообщает о том, что она измерила: не «skills: настроено», а «91 загружено, 0 отклонено». Движок оттенков ищет путь в образце обоих языков, MCP-сервер отвечает на свой собственный протокол. Проверка, которая не может выполниться, падает, а не проходит молча: зелёная строка, означающая «не тестировано», хуже красной. Ничего не касается сети и модели — thot doctor в самолёте даёт тот же ответ, что и в офисе. Ненулевой вывод при сбое, чтобы вписаться в && или CI.

Постоянное улучшение

Аудит, который аргументирует двадцать кандидатов и останавливается, оставляет остальное без суждения навсегда. Проход без бюджета всё ещё работает, когда ты возвращаешься сесть. thot improve — это промежуточное: ограниченные туры, каждый записан на диск, каждый продолжает с того места, где остановился предыдущий.

thot improve                      # un tour sur les trois arbres
thot improve --rounds 5           # jusqu'à ce qu'un tour ne juge plus rien
thot improve --every daily        # la boucle devient permanente

Записанный unit несёт собственный PATH. launchd даёт задаче /usr/bin:/bin:/usr/sbin:/sbin, cron — ещё меньше, а claude, hermes и node нет ни в одной из этих папок — они живут в ~/.local/bin. Без этого ночной проход не собирал ни одного движка, ничего не оценивал и выходил с кодом 0: launchd записывал успех каждую ночь, бесконечно. Такая задача, молча падающая, неотличима от работающей, поэтому глубокий проход, лишённый агента, теперь выходит с ошибкой и говорит об этом.

Ночная версия сообщает о том, что она решила, а не о том, что появилось. Различие важно: механизм отчёта запланированных проверок отвечает на вопрос «что нового выше порога», что является правильным вопросом для сканирования и неправильным для суждения. Подтвердить MEDIUM, уже присутствующий в отчёте, — это именно то, для чего служит цикл, — и об этом никому бы не сообщили. Файлы, которые проверка могла бы изменить, отмечаются в том же журнале.

Дерево, которому больше нечего оценивать, передаёт свою долю следующему. Измерено на реальном корпусе: у thot пустая очередь, а у prime один кандидат, поэтому бюджет 20 на дерево тратил 40 на деревья, неспособные его использовать, пока Hermes ждал сто пятьдесят. Раунд из 20 становится раундом из 20, 20 и 60.

Третья заставляет её сходиться быстро: неудачи учитываются. Кандидат, чей агент превышает свой срок, или чья модель отказывается брать на себя обязательства, сохраняет свою серьёзность — поэтому он берётся в первую очередь в следующем раунде, и в следующем. Измерено на finding в файле из 1 660 строк: четыре попытки из трёх проходов, три из них упираются в одну и ту же стену. После двух неудач он переходит в конец очереди: всё ещё допустим, но никогда не приоритетен. Успех стирает счёт — стена, которой был загруженный день или исчерпанная подписка, не должна преследовать finding вечно.

Два свойства заставляют её сходиться, а не ходить по кругу: опровержение запоминается, поэтому следующий выбор его пропускает; подтверждение намеренно не запоминается — настоящий дефект должен продолжать появляться, пока кто-то его не исправит, — поэтому цикл несёт собственный набор идентификаторов уже оценённых. Без этого каждый раунд после первого тратил бы весь свой бюджет на повторное обоснование того, что первый только что подтвердил.

Она заканчивается тем, что нужно сделать, перед итогами:

À REGARDER — 2 finding(s) :
  [hermes] plugins/platforms/a2a/tools.py:83 — confirmé · prime
      L'URL vient d'un argument d'outil, donc du modèle…
  [prime] packages/coding-agent/…/state-snapshot.ts:163 — réfutation contestée · hermes
      Le chemin dit fixe est construit depuis un identifiant non validé…

4 tour(s) · 83 jugement(s) (80 réfuté · 1 confirmé) · 157 candidat(s) sans décision

Опровержение — это хозяйственная работа; подтверждение — это новость. Оспоренное опровержение тоже: это программа говорит, что исправилась, прежде чем что-то похоронить. Считать их, не называя, отправляет читателя грепать журнал — что именно и происходило, каждый раз, в течение дня.

Она никогда не изменяет код. «Улучшение» здесь означает, что суждение программы о самой себе становится более чётким и дешёвым: меньше кандидатов без решения, больше решений на диске, каждое из которых можно приписать агенту, который его принял.

Термометр и цикл, который им пользуется

Всё вышеперечисленное измеряет Thot с помощью Thot. improve спрашивает модель, является ли finding реальным. evolve следил за provenance, отчётом, который движок вычисляет по собственному выводу. Оба циклические, и круг не имеет ничего академического: глубокая проверка заплатила 638 суждений за 9 подтверждений, пока правило с оценкой −100 % спало — xml_unsafe_parse сигнализировал defusedxml, то есть ровно то лекарство, которое рекомендовало его собственное сообщение. Ничто в программе не могло этого увидеть.

thot bench разрывает круг. Он измеряет Thot против кода, помеченного уязвимым или здоровым кем-то другим, в равных долях, с названным классом слабости.

thot bench ~/.thot/bench              # les suites présentes, catégorie par catégorie
thot bench ~/.thot/bench --json       # ce que la boucle d'évolution lit
thot bench ~/.thot/bench --floor info # ce que le plancher de sévérité cache

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

Оценка — это J Юдена, TPR − FPR. Ноль — это подбрасывание монеты, +100 — идеально, а отрицательное означает, что правило инвертировано. Точность и полнота этого бы не сказали: правило без единого истинно положительного имеет неопределённую точность, отображается пустым и читается как нет данных — это именно так инвертированное правило и выживает. У J этой дыры нет.

Два способа сжульничать, и оба проигрывают. Находить меньше — что и поднимало provenance — роняет TPR. Сигнализировать всё даёт TPR 100 %, FPR 100 %, J ноль: корпус сбалансирован 50/50 именно для этого.

Измеренное состояние, пол по умолчанию, три фреймворка (django, fastapi, flask):

                        avant      après
TPR                      9.9 %     55.5 %
FPR                      0.5 %      0.000 %
J de Youden             +9.4 %    +55.5 %
catégories actives          10         34
catégories négatives         0          0

«До» — это состояние программы на момент, когда термометр впервые появился. «После» — тот же корпус, тот же пол, та же команда. Hold-out это подтверждает, и тремя способами: один django даёт +55,5 %, fastapi +55,5 %, flask +55,5 %. Три фреймворка, которые совершенно не похожи друг на друга, одна и та же цифра с точностью до десятой — правила работают на коде, для написания которого они не служили.

По категориям — то, что движок умеет сегодня. Тридцать две из тридцати четырёх активных категорий находятся ровно на +100 %:

basic_xss · cleartextstorage · cleartexttransmit · cloud_ssrf_metadata
cmdi · codeinj · cookie_no_httponly · cookie_no_samesite · corsmisconfig
crlfinjection · csv_injection · debug_code_production · default_credentials
deserial · errormessage · eval_injection · hardcodedcreds · ldapi
loginjection · nosql · redirect · securecookie · sqli · ssti · tlsverify
weakcipher · weakhash · weakkeylength · weakrand · xpathi · xss · xxe
                                                               +100,0 %
pathtraver  +94,0 %          ssrf  +92,0 %

Ноль ложных срабатываний на 9 150 здоровых случаях и ни одной отрицательной категории. Отправной точкой были ssrf на −8,0 % и xxe на −100 %.

Потолок, потому что цифра без своего максимума ничего не значит

+55,5 % — это не «половина проблемы решена». Из 61 категории 27 недостижимы, и по двум различным причинам, которые обе были проверены случай за случаем, с открытым файлом.

Десять — это код, которым уже владеет другая категория. Корпус пишет два разных CWE на одной строке: el_injection и ssti — это один и тот же вызов Template(данные), argument_injection и genericcmdi — тот же os.system('echo ' + данные), что и cmdi, unverified_signature — тот же verify=False, что и tlsverify, sensinlogs — тот же logging.info, что и loginjection, hardcoded_crypto_key — тот же жёстко заданный ключ, что и hardcodedcreds. Thot стреляет по всем этим файлам — отчёт это говорит, колонка seen — и объявляет класс, которым код действительно является. Догнать их потребовало бы называть два класса для одного правила, что report/cwe.py существует именно для того, чтобы отвергать.

У семнадцати нет ни одного sink. authzfailure, idor, intoverflow, null_deref, privescalation, csrf, missingcritauthn, sessionfixation и остальные все заканчиваются на return JsonResponse(...) без единого опасного вызова. Уязвимый случай idor — это корректно параметризованный запрос; чего не хватает — это вызова авторизации. clickjacking выигрывается сигнализированием отсутствия X-Frame-Options, который Django ставит через middleware. weak_password_hash тем не менее идеально разделяет этот корпус — каждый уязвимый случай это hashlib.sha256, каждый здоровый — pbkdf2_hmac — но ни одна из этих строк не говорит, что значение является паролем, и правило стреляло бы по всем контрольным суммам, всем ETag и всем HMAC реального репозитория. Две последние были переоткрыты с идеей, что они, возможно, всё-таки код, и закрываются по причине, которую стоит записать. intoverflow фабрикует свою слабость с помощью ctypes.c_int32 — целые числа Python не переполняются, и правило, которое стреляет по ctypes, было бы правилом по BenchProctor. resourceexhaust — это bytearray(int(данные)) без границы против bytearray(min(…, 1024)), и разделитель — это единственный токен min; но bytearray(x) означает две вещи в зависимости от того, является ли x целым числом или буфером, Thot не выводит типы, и на hermes/ 102 вызова этого семейства — это преобразования, а не выделения. Это дефекты логики или отсутствующие проверки; никакой анализ оттенков их не видит, какое бы правило ни написать.

Остаётся, следовательно, 34 честно достижимые категории, то есть +55,7 %. Они все активны. При +55,5 % это 99,6 % честного максимума.

21 случай, которых всё ещё не хватает, — это уже не категории, а пропущенные случаи внутри двух из них, и они были посчитаны по одному: 12 в ssrf имеют источником mq_client.get_message() или redis_client.get(), два объекта _Stub(), объявленные в app_runtime.py из BenchProctor, — они не называют ничего, что существует вне этого корпуса, и шаблон, написанный для них, прилипал бы к термометру вместо того, чтобы обнаруживать что-либо; 9 в pathtraver происходят из переменной окружения, которую скидка доступности намеренно понижает в классе, потому что тот, кто предоставляет окружение, уже держит файловую систему этого процесса.

Что произвело эти сорок шесть пунктов, в порядке, в котором это было измерено:

изменение

J

отправная точка

+9,4 %

веб-маршруты распознаны как точки входа

+9,6 %

оттенок следует за значением в контейнере

+10,0 %

тернарный оператор с константными ветвями ничего не переносит

+12,4 %

fullmatch с отрицаемым классом — это чёрный список

включ.

SSRF-защиты: белый список хостов, разрешённый диапазон IP

+14,3 %

socket.create_connection распознан как сетевой sink

+14,9 %

ограничение пути и именованный белый список

+15,5 %

mark_safe как sink, bleach как нейтрализация HTML

+16,3 %

двенадцать однострочных правил-шаблонов

+27,8 %

шаблон больше не платит скидку доступности

+34,4 %

sink.redirect, и доказательство хостом отделено от доказательства диапазоном

+35,6 %

четыре sink инъекции: template, XPath, LDAP, NoSQL

+40,8 %

заголовки ответа, позитивная защита, нейтрализация CR/LF

+44,2 %

ячейка электронной таблицы — это не строка текста

+45,6 %

файл с именем secrets.txt, и доказательство шифрованием или хешированием

+46,6 %

оттенок проходит через замыкание nonlocal

+47,7 %

Django пишет три своих источника заглавными

+48,0 %

HTMLResponse объявляет HTML, html.escape от него защищает

+49,0 %

представление, возвращающее строку, возвращает HTML

+49,9 %

доказательство стоит лишь того, что оно доказывает, и процитированный SQL-идентификатор

+50,0 %

программа, написанная в строке, остаётся программой

+50,4 %

параметры маршрута приходят из сети

+50,4 %

ответ третьей стороны — это не данные этой программы

+50,6 %

файл, который программа называет secrets, говорит, что он содержит

+51,2 %

то, что возвращает база, — это то, что кто-то туда положил

+53,9 %

строка журнала заканчивается переводом строки, как заголовок

+55,5 %

маршрут, возвращающий страницу, построенную строкой выше

+55,5 %

Два из этих изменений сначала были отклонены после измерения, и именно термометр их отклонил: clickjacking, который стоил 23 ложных срабатывания на этом репозитории, и weak_password_hash, который тем не менее идеально разделяет этот корпус — каждый уязвимый случай это hashlib.sha256, каждый здоровый — pbkdf2_hmac. Они остаются отклонёнными по причине, сказанной выше: ничто в этих строках не говорит, что речь идёт о пароле, и правило стреляло бы по каждой контрольной сумме реального репозитория.

Третий отказ был снят, и он самый поучительный из всех. Рассматривать чтение из базы как ненадёжный источник измеряли дважды и отклоняли дважды: 436 уязвимых случаев против 343 здоровых в первый раз, 11 против 13, когда ограничились хранимым XSS — где половина здоровых читает из базы чаще, чем другая. Оба чтения были верны для тогдашней программы. Изменился не корпус: изменились защиты здоровой половины, которые теперь поняты по одной — шифрование, HTML-экранирование и автоэкранируемый рендеринг, нейтрализация CR/LF, белый список хостов и разрешённый диапазон IP, позитивная проверка, экранированный SQL-идентификатор. Здоровый случай, который читает строку, а затем шифрует её, оправдывается шифрованием, а не тем, что никто не видел строку. То же правило, которое стоило 343 изобретений, теперь стоит ноль и приносит 2,7 балла сразу по шестнадцати категориям. Отказ, измеренный однажды, верен только на день, когда его измерили.

Пять правил инъекций LDAP / XPath / NoSQL / SSTI / EL тоже были отклонены, при измеренном J = 0,000: они тянули одинаково и на здоровую, и на другую половину. Четыре из них сегодня на +100,0 % без единого ложного срабатывания, и это не вердикт передумал — это то, что им дали читать. Каждое теперь смотрит только на аргумент, несущий инъекцию (фильтр LDAP — третий, источник шаблона — первый), а sink.nosql проходит через барьер на тексте запроса, а не на импорте, потому что .find( принадлежит всем строкам Python. Пятое, el_injection, остаётся отклонённым: это тот же вызов Template(…), что и ssti, помеченный другим классом, и дать ему это имя означало бы заплатить балл за неточность.

Точность не покупается слепым пятном

Каждая защита, признанная выше, сначала была предложена в версии, которая поднимала оценку и открывала эксплуатируемую дыру. Враждебный зонд нашёл их все:

resolved = socket.gethostbyname(parsed.hostname or url)
if ipaddress.ip_address(resolved).is_private:
    return "blocked", 403
os.system("curl -s " + url)      # ← silencieux, et exploitable

Защита — это корректная SSRF-защита и не говорит ничего о shell-метасимволах, всё ещё находящихся в строке. Очистка оттенка везде покупала 174 ложных срабатывания меньше и слепое пятно в инъекции команд.

Отсюда различие, которое несёт движок: защита, ограничивающая значение (буквальный белый список, перечисляющий fullmatch), очищает его для всего; защита, доказывающая назначение (хост разрешён, разрешённый адрес публичен, путь ограничен), действует только для соответствующего семейства sinks. То же самое для bleach.clean, который нейтрализует HTML и оставляет x; rm -rf / нетронутым.

Четыре враждебных зонда находятся в наборе тестов и все сигнализируют правильно. Будущее «улучшение», которое снова откроет одну из этих дыр, сломает именованный тест.

Оставшееся молчание имеет две причины, и thot bench разделяет их, потому что это две разные работы:

règle muette — elle existe et ne matche jamais : elle a un motif à élargir
aucune règle pour la classe : il y en a une à écrire

Различие определяет порядок целей — без него молчащие категории находятся в идеальном равенстве при J = 0, и сортировка падает на алфавитный порядок.

Слияние там, где оно что-то меняет

Cascade.turn выбирает одного агента и вызывает его; он обращается к другому, только если первый вернёт ошибку. Такой ход по построению ограничен лучшим из двух: он может потерять меньше, но никогда не выиграть больше. agent_apply делал то же самое — один движок, в единственном числе.

thot evolve --fused заставляет работать обоих, на разных половинах одной и той же проблемы:

thot bench ~/.thot/bench                      # où ça fait mal
thot evolve --from-bench --fused \
      --corpus ~/.thot/bench --hold-out flask # et on répare, en boucle
  • Hermes читает измерение и пишет спецификацию. Он не касается ни одного файла. Его вывод — это утверждение о причине: какое правило, какая строка, почему именно эти случаи.

  • Prime читает спецификацию и пишет код. Ему явно сказано, что он может отказаться от неё, если код противоречит ей — исполнитель, неспособный сказать «нет», — это ретранслятор, а ретранслятор ничего не добавляет.

  • Никто из них не решает. Набор тестов — это нижняя граница, размеченный корпус — вердикт. Ложная спецификация, применённая с убеждённостью, снижает оценку и отменяется байт за байтом.

Порядок тоже не произволен. Проектирование-затем-построение проверяется на стыке: Prime видит рассуждения Hermes, прежде чем принять их. Построение-затем-перечитывание этого не позволяет — когда второй смотрит, первый уже решил.

Цели приходят из измерения, а не из написанной фразы. До сих пор цикл мог преследовать только то, что человек уже подозревал; цель, построенная из оценки, — это программа, говорящая, где она слаба, в цифрах, которые она не выбирала, — и те же цифры затем говорят, послужил ли ответ. Каждая цель несёт файлы, которые провалились: агенту, которому сказали «xss на 0 %», остаётся только гадать, агенту, которому дали три проваленных файла, — решать проблему.

Переобучение и что --hold-out действительно может

Цикл, оцениваемый на корпусе, имеет один настоящий способ сжульничать: выучить корпус. Правило, подогнанное под то, как выглядит BenchmarkTest01126, поднимает оценку и не служит никому, а со стороны это неотличимо от настоящего прогресса.

--hold-out flask выводит один набор из основного числа и держит его как второй предохранитель: изменение, которое двигает наборы, против которых оно оптимизировалось, и не двигает тот, который оно никогда не видело, сказало, чем оно является. Оба числа хранятся как ne_baisse_pas.

Его предел, измеренный: три фреймворка оцениваются в полубалла друг от друга. Это ловит переобучение на файл, но не переобучение на форму бенчмарка — сгенерированный корпус остаётся сгенерированным корпусом, и правило, которое помогает только на демонстрационном коде, прошло бы все три. Hold-out делает видимым жульничество; он не делает корпус репрезентативным.

То, что цикл сохраняет от раунда к раунду, записано в ~/.thot/evolve-log.jsonl. Без этого, поскольку измерение едва меняется за раунд, следующий раунд перечитывает те же худшие категории, протягивает те же файлы и получает — разумно — ту же уже построенную, измеренную и отменённую спецификацию: --rounds 5 была бы попыткой, предпринятой пять раз, в пять раз дороже, с видом занятости.

Это не оракул. Исправление может быть зелёным, поднять J и остаться плохим — это переобучение, и литература об автоматическом исправлении программ говорит только об этом. Корпус — это доказательство, что мы продвинулись на нём. Цикл сообщает, что он изменил, чтобы человек мог не согласиться.

Skills — методы, которые знает Thot

Skill — это метод, написанный один раз: SKILL.md с YAML-frontmatter. Это формат Hermes Agent и Prime Agent, поэтому skill, написанный для одного из них, загружается здесь без изменений, и обратное тоже верно.

Thot включает полную библиотеку Hermes Agent (MIT — см. NOTICE.md): 90 загруженных методов, ещё 117 доступны.

thot skills list              # les 91 chargées
thot skills search pentest    # y compris la bibliothèque optionnelle
thot skills install ast-grep  # activer une optionnelle
thot skills show plan         # ce que lirait le modèle

Загруженные категории: audit, security, software-development, github, devops, research, mlops, productivity, creative, apple, email, media, note-taking, smart-home, social-media, autonomous-ai-agents.

Модель обнаруживает их с помощью инструмента skills — который отвечает индексом имён, пока ему не дадут ключевое слово, потому что двести описаний — это не каталог — и читает тот, который применим, с помощью skill. В сессии /skills показывает то же самое.

Импортированный метод, который ссылается на отсутствующий здесь инструмент (delegate_task, browser_navigate…), подаётся как есть, с примечанием, говорящим, какие отсутствуют и что использовать вместо них. Подход переносится, даже если вызов инструмента не переносится.

Добавление

~/.thot/skills/<nom>/SKILL.md            # partout où tu travailles
<repo>/.thot/skills/<nom>/SKILL.md       # versionné avec ce dépôt
---
name: ma-méthode
description: Ce qu'elle fait et quand s'en servir.
---

# Ma méthode

Les étapes, dans l'ordre.

Обе раскладки принимаются: плоская папка (Prime Agent) или сгруппированная по категориям (Hermes Agent). Имя, которое уже существует, заменяет встроенную версию — этого достаточно, чтобы адаптировать поставляемый метод без форка.

Метод, предоставленный проверенным репозиторием, сначала анализируется

SKILL.md — это текст, передаваемый модели как инструкция. Репозитории, которые читает Thot, по определению те, за которые никто не отвечает. Враждебный репозиторий, разместивший .thot/skills/x/SKILL.md, написал бы часть брифинга.

Защита Hermes Agent перенесена сюда и применяется ко всему, что приходит из репозитория: инъекция, эксфильтрация, персистентность, обфускация.

   ▲ 1 skill(s) fourni(s) par ce dépôt ont été refusés — ils seraient passés
     au modèle comme instructions.
     pwn   curl vers l'extérieur ; accès à ~/.thot ; « ignore previous
           instructions »

thot skills scan <dossier> задаёт тот же вопрос по запросу. То, что Thot поставляет сам, не анализируется: оно на диске, потому что программа установлена, а не потому, что репозиторий это запросил.

Пользовательские команды

Файл markdown — это команда. Грамматика та же, что у Prime Agent, Claude Code и Codex — ничего нового учить не нужно.

---
description: Relire un fichier sans rien modifier.
argument-hint: <chemin>
---

Relis $1 et dis-moi ce qui cloche. Ne modifie rien.

В .thot/commands/revue.md это создаёт /revue src/app.py. Подстановки: $1, $2…, $@, $ARGUMENTS, ${@:2}, ${@:2:3}. Аргумент никогда не интерпретируется повторно. Команды репозитория проходят через ту же защиту, что и его skills.

Поставляются три: /triage (назвать вход или классифицировать без продолжения), /harden (сначала падающий тест, затем исправление), /regress (аудит diff против git-ссылки).

MCP-серверы

Каталог Hermes Agent, двадцать проверенных серверов:

thot mcp list            # le catalogue, et ce qui est déjà connecté
thot mcp show sentry
thot mcp add linear

Установка делегируется официальному CLI, который уже владеет OAuth и обновлением токенов — у Thot нет причин держать второй сейф, который может протечь. Он явно говорит, что зарегистрировано — это не авторизовано, и какая команда завершает работу.

Инструменты модели

Классика — читать, писать, редактировать, запускать команду. Любая запись и любое выполнение требуют подтверждения, и это не настраивается.

И четыре, которые принадлежат только Thot, бесплатные, потому что они запрашивают карту, а не модель:

Инструмент

Ответ

code_map

файлы проекта

find_symbol

файл, строки и параметры функции

callers

кто кого вызывает и расстояние до точки входа

audit

пути оттенка источник → sink

В режиме счёта эти четыре обслуживаются официальным CLI через thot.mcp_server — MCP-сервер только для чтения, неспособный писать или выполнять что-либо.

Когда модель ищет, кто вызывает process_payment, она запрашивает граф и получает полный ответ — вместо того чтобы грепать три файла наугад.

Режим только аудита

Ядро анализа также используется без модели, без сети, без затрат:

thot init /chemin/du/repo --owner "Ton Nom"   # autorisation, une fois
thot audit /chemin/du/repo --paths            # chemins de teinte complets
thot audit . --all                            # y compris le bruit faible
thot audit . --json --out rapport.json
thot audit . --out rapport.sarif              # SARIF 2.1, format déduit du nom
thot audit . --fail-on high                   # code 1 en CI

SARIF — войти в уже существующую цепочку

Отчёт, который ни один конвейер не умеет читать, живёт в одном терминале. GitHub code scanning, GitLab, Azure DevOps и редакторы читают SARIF 2.1, и два свойства Thot там ценятся больше, чем где-либо.

Идентичность finding — это правило + файл + символ + отпечаток тела, никогда не строка — это именно то, что требует partialFingerprints. Панель мониторинга, питаемая номерами строк, заново открывает все свои тикеты, как только кто-то добавляет импорт в начало файла; питаемая этим — нет.

И путь оттенка — это последовательность позиций, что и отображает codeFlows: читатель кликает от источника к sink, вместо того чтобы верить инструменту на слово. Finding без пути не несёт ключа codeFlows — пустой поток отображается как путь оттенка без шагов, что читается как сломанный анализ, а не как совпадение по шаблону.

Finding, опровергнутый панелью, не удаляется: он уходит с обоснованным suppressions. Панель, которая его никогда не видит, не может отличить «никто не смотрел» от «кто-то посмотрел и решил», а второе — весь смысл панели.

- run: thot audit . --out thot.sarif
- uses: github/codeql-action/upload-sarif@v3
  with: { sarif_file: thot.sarif }

Анализ с помощью — --deep

Детерминированный анализ отвечает на вопрос «могут ли эти данные потенциально циркулировать?». Он исчерпывающий, бесплатный, и это не тот вопрос, который платят решать аудитору. --deep задаёт дорогой вопрос, и только тем кандидатам, которые его заслужили:

thot audit . --deep                  # 20 pires candidats, 4 en parallèle
thot audit . --deep --budget 50      # plus large
thot audit . --deep --parallel 8     # plus vite

Два прохода, намеренно противоположных:

  1. Зонд должен назвать конкретный вход, который достигает опасной точки. Никакой общности про класс уязвимости — URL, значение, эффект.

  2. Опровержение получает этот сценарий с единственной миссией — уничтожить его: валидация на входе, вызывающий, который передаёт только константы, тип, который запрещает предполагаемый вход. В случае сомнения оно опровергает.

Находка выживает только если второе враждебное прочтение того же кода не смогло её убить. Тогда confirmed начинает что-то значить.

В сессии то же самое: /audit deep.

Движок выбирается автоматически — твой аккаунт Claude через официальный CLI, если он подключён (анализы идут параллельно, за счёт твоей подписки), иначе ключ API.

Что аудит не должен читать

# .thotignore, à la racine du dépôt
vendor/
*.generated.py
tests/fixtures/

Встроенные исключения покрывают то, что есть в любом репозитории, — node_modules, build, .venv. .thotignore покрывает то, что знает только этот репозиторий: встроенную документацию, сгенерированного клиента, папку с намеренно сломанными фикстурами. Их аудит даёт не находки, а шум ровно в том месте, где должны быть находки.

Твои собственные правила

Встроенный каталог знает стандартную библиотеку. Он не знает обёртку, которую твоя команда написала вокруг subprocess, очередь, которую потребляет твой сервис, или валидатор, который делает значение безопасным у вас. Без места, где это можно сказать, каждый аудит реальной системы ошибается в одних и тех же трёх местах.

# <repo>/.thot/rules/team.yaml   — versionné avec le code
# ~/.thot/rules/*.yaml           — ce que tu sais, partout où tu travailles
sinks:
  - id: sink.team.run_shell
    patterns: [run_shell, shellutil.run_shell]
    impact: critical
    description: Wrapper shell interne (shell=True)
    match_mode: bare          # qualified | method | bare | prefix

sources:
  - id: source.queue
    patterns: [msg.payload]
    description: File de messages
    match_mode: prefix        # couvre msg.payload.decode(...)

sanitizers: [validate_host, team.escape]

Тот же файл содержит правила JavaScript под ключом js: — командная обёртка обычно существует на обоих языках, и разделение объявления — это способ, которым одна половина устаревает.

js:
  sinks:
    - id: sink.js.team
      names: [runShell, sh]     # comparés au dernier segment, ou qualifiés
      impact: critical
      description: Notre wrapper shell
      needs: [child_process]    # ne se déclenche que si le fichier l'importe
  sources:
    - id: source.js.queue
      patterns: [job.payload]
      description: File de messages
  sanitizers: [escapeArg]

То, что запрашивает модель, — это ненадёжный вход

Источники — это выражения — sys.argv, os.environ. Это покрывает программу, которую запускают, но упускает программу, которую вызывают: инструмент агента получает свой ненадёжный вход именованными параметрами, заполняемыми реестром из того, что запросила модель, и ни одно выражение не появляется нигде в теле.

Измеренная цена того, что это не смоделировано: четыре SSRF за один день, все достижимы через аргумент инструмента, ни одна не найдена оттенком — их нашли правила по шаблону, которые распознают форму и ничего не доказывают.

entry_sources:
  - id: entry.tool
    patterns: [tools.image_gen]     # les fonctions qu'un registre appelle
    parameters: [args]              # facultatif : lesquels de leurs paramètres
    description: Arguments remplis par le modèle
    match_mode: prefix

Пусто по умолчанию, и намеренно: какие функции вызывает реестр — это факт о репозитории, и угадывание поставило бы источник под каждый параметр каждой программы. Измерено на Hermes, обе крайности: правило, называющее пакеты plugins и tools, выявляет 19 доказанных путей, из которых несколько переаппроксимированы (base_url, который хелпер получает из конфигурации, не является ненадёжным); правило, называющее параметр args, выявляет ноль, потому что обработчики Hermes принимают именованные параметры, а не словарь. Правильное правило называет реальные точки входа — и знать его должны их авторы.

Правило, которое повторяет встроенный id, заменяет его — так можно понизить сток, который команда осознанно приняла, без патча Thot. Некорректный файл останавливает аудит, называя файл и ошибочный ключ, а не создавая видимость отсутствия находок.

Подавления

Подавление — это единственное утверждение о безопасности, которое ни один инструмент не перечитывает — включая этот, по построению. # nosec, # noqa: S310, // eslint-disable … security/…: это утверждение о коде, написанное один раз, которое переживает вызывающих, которых оно описывало.

Дважды за один аудит, здесь, оно было ложным:

подавление

что оно утверждало

что было правдой

# nosec B310 — scheme checked above

схема контролируется

она останавливала file:// и ничего больше — SSRF к сервису метаданных

# noqa: S310 (configured peers)

URL приходит из конфигурации

один из вызывающих читает его из аргумента инструмента, то есть от модели

Thot поэтому сообщает их как класс, в LOW, с записанным рядом шаблоном. Находка не говорит «эта строка опасна»: она говорит «никто не перечитал причину, по которой её извинили». На проходе --deep это агент, который проверит, действует ли ещё причина.

Для Python читаются настоящие токены комментариев, а не шаблоны — регулярное выражение не отличает # nosec в комментарии от того же текста, процитированного в докстринге, а докстринг этого модуля цитирует два из них.

Подавление, поставленное на строку, которую этот аудит помечает, — не тот же объект: это утверждение, противоречащее живой находке, написанное тем, кто прочитал ту же строку и заключил иначе. Оно поднимается на ступень и говорит об этом. Измерено на Hermes: 7 из 45 — и три подавления, прочитанных в тот день, были ложными.

Измерено: 0 на Thot, 0 на Prime, 45 на Hermes.

Калибровка

Точность важна не меньше обнаружения. Намеренно не сообщаются:

  • subprocess.run(cmd) без shell=True — никакой шелл не читает команду.

  • cursor.execute("… ?", params) — литеральный запрос, связанные параметры.

  • Значение, переданное через int(), shlex.quote(), os.path.basename(), html.escape() — эти вызовы разрывают цепочку заражения.

  • Дефект, которого не достигает ни одна точка входа, автоматически понижается — но только если точки входа были найдены. Без них область неизвестна, а не пуста, и ничего не закапывается на этом незнании.

  • payload.get(...) — это не requests.get(...).

Порядок величины, измерено на Hermes Agent (4 457 файлов Python): 98 с, 365 находок, из них 25 high — 3 выше порога по умолчанию после применения памяти.

Какой параметр заполняет аргумент

Хелпер, у которого один параметр достигает стока, не делает опасным всё, что ему передают. Однако движок сопоставлял вызывающего со всем набором стоков вызываемого, не глядя, куда попадает аргумент:

def helper(safe, cmd):
    os.system(cmd)          # seul `cmd` atteint le shell

helper(sys.argv[1], "ls")   # la donnée va dans `safe` — et c'était rapporté

Позиция теперь читается в месте вызова, а имя — для именованного аргумента. Это то, что движок JavaScript уже делал со своей стороны.

Измерено, когда выполнение стало возможным: из 83 238 запрошенных разрешений при анализе Thot и hermes/tools 90 % указывают на единственный параметр, а 9,8 % не указывают ни на один; 0,1 % остаются открытыми (f(*rest), f(**options)) и сохраняют широкий ответ. Вызываемый в среднем предлагал 2,68 параметра, так что исчезает почти две трети пространства поиска — анализ hermes/tools падает с 11,4 с до 9,5 с.

По числу находок, напротив: никаких изменений. Thot, prime и hermes/tools дают ровно те же 151 кандидата до и после, из них ноль выдуманных — свойство безопасности держится на практике. Четыре пути оттенка укорачиваются, не более. Исправленная выше форма реальна, и тест это доказывает, но она не встречается ни в одном из трёх измеренных деревьев: этот фикс покупает скорость и более точные пути, а не меньше шума.

Две формы ничего не говорят: helper(*args) размазывает неизвестное число значений, а helper(**options) не называет ни одного. Здесь движок сохраняет широкий ответ, который давал раньше — сохранённое множество всегда включено в прежнее, поэтому уточнение может только убрать находку, но никогда не выдумать её.

Получатель пропускается на основании сигнатуры вызываемого, а не синтаксиса вызова: Runner().go(x) сводится к go без точки, и Cls.m(obj, x) так же, как obj.m(x), — оба атрибут. Поскольку метод почти всегда вызывается связанным, решает именно self во главе параметров — несвязанный вызов единственная форма, читаемая на один шаг короче.

Что граф не может проследить

Дефект, достижимый путём, который анализ не разрешает — обработчик, спрятанный в таблице диспетчеризации, декорированное представление, вызов по переменной с неизвестным типом — это не недостижимый дефект. Thot различает эти два случая:

HANDLERS = {"run": run_command}     # aucun appel : le graphe ne voit rien
@app.route("/ping")                 # enregistré à l'import par le décorateur
sandbox.run("pytest")               # plusieurs `run` répondent à ce nom

Во всех трёх случаях область неизвестна, а не пуста, и находка сохраняет лёгкий штраф вместо того, чтобы быть закопанной. На Hermes: те же 365 находок, но 60 поднимаются на ступень. Функция, которую никто не вызывает и которую никто не упоминает, остаётся корректно уценённой — иначе фильтр перестал бы быть фильтром.

Ограничения

Оттенок через файлы зарезервирован для Python: JavaScript и TypeScript индексируются, прослеживаются в теле функции и до хелпера из того же файла, и останавливаются там из-за отсутствия разрешённого графа вызовов — таблица выше говорит это построчно. Правила по шаблону, напротив, применяются везде.

+55,5 % — это цифра для Python. BenchProctor помечает django, fastapi и flask, и ничего больше; каталог JavaScript уже своего собрата — нет источника «чтение из базы», нет стока журналирования — и его правила опираются на свои тесты, а не на 18 300 размеченных случаев. Когда исправление применимо к обоим движкам, оно переносится на оба, и журнал говорит, какое где измерено.

sink.log читает сообщение и только его. logger.info('failed: %s', exc) доверяет значение механике журналирования вместо того, чтобы вписывать его в строку, которую набрал автор; чтение и этих аргументов тоже стоит +0,0000 на корпусе и 4 844 находки сверху на hermes/, по одной на каждый except … as exc: в дереве. Это не покрыто, и сказать об этом дешевле, чем позволить думать иначе.

Без --deep каждая находка — PLAUSIBLE: обнаружена статически, ещё не доказана выполнением. С --deep находка confirmed пережила враждебное опровержение — это ещё не доказательство выполнением, которое придёт с воспроизведением. И отсутствие находки — не доказательство отсутствия дефекта: динамическая диспетчеризация, рефлексия и метапрограммирование ускользают от анализа.

Полнотекстовый поиск в сессиях перестаёт быть константным по стоимости на SQLite, где fts5 не умеет поднимать rowid: движок там сортирует всё множество совпадений вместо того, чтобы обрезать на двадцати. Измерено на том, что встроено в CPython 3.12, — 6 588 инструкций против 27 443, когда корпус умножен на пять, тогда как способный движок остаётся на 2 215, затем 2 698. Тест это проверяет и называет версию, когда она этого не позволяет.

Разработка

cd Thot
uv run pytest -q

Установлен в редактируемом режиме: исходный код вступает в силу немедленно. Однако если pyproject.toml меняется (новая зависимость), нужно перезапустить uv tool install --editable --from . thot --force.

Детерминированное ядро (codemap, taint, scope, scoring, store, report) не зависит ни от какого агента и не касается сети — тест это проверяет и валит набор, если это изменится.

Спецификации и планы: docs/superpowers/.

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    B
    maintenance
    Enables AI agents to search code by meaning, explore codebase structure, store and query knowledge with temporal facts, and read source code through a set of MCP tools.
    386 npm
    7
    MIT
  • A
    license
    Not graded
    quality
    D
    maintenance
    Enables parsing, indexing, and querying source code as structured knowledge, providing code exploration, spec generation, and migration tools via 20 MCP tools.
    MIT
  • F
    license
    Not graded
    quality
    D
    maintenance
    Provides code intelligence by indexing source code into SQLite and offering MCP tools for symbol search, flow tracing, and context retrieval to assist with code navigation and understanding.
    -
  • A
    license
    Not graded
    quality
    A
    maintenance
    MCP server for persistent code exploration and diff-aware review, exposing explore and review tools to map call paths, blast radius, and generate explainable risk-scored reports.
    3 npm
    2
    MIT