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,uv workspace 成员

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 将 Thot 的 MCP 服务器声明到两个智能体中:它们获得 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 写入负责重启它的单元——macOS 上是 KeepAlive,systemd 下是 Restart=always——并且不会加载它:Thot 打印出命令并让你自己运行它,因为一个不声不响就在后台注册智能体的工具,是一个让人不再信任的工具。thot doctor 随后提出唯一重要的问题——地址现在是否响应——而不是文件是否存在。

fusion wire 还会在 Prime 那边安装 Thot 为其提供的包方法:仅有一条配置项是不够的,Prime 只有通过一个命名该服务器的类才能触达 MCP 服务器。它还会把令牌写入 auth.json,不触碰模型的凭据——没有令牌,连接在建立之前就会失败。

每个智能体保留自己的配置。thot fusion unwire 撤销一切,Prime 的 settings.json 在第一次修改前会被备份。

thot fusion status 衡量的是实际生效的内容,而不是写下来的内容:Hermes 默认安装可移植插件时处于禁用状态,出于安全考虑,所以在 plugins.enabled 点名之前,写入两个文件并不会连接任何东西。激活通过 Hermes 的 CLI 完成,绝不通过编辑它的 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 及以上)时,它的论证会被重读,由一位在这条发现上什么都没说过的智能体执行。审阅者不评判缺陷本身,而是评判所引用的理由在展示的代码中是否可验证。

两种错误并不等同。一个错误的确认为人类造成十分钟的阅读成本。一个错误的反驳造成一个真实缺陷的永久流失**——因为一条被记忆的反驳会被之后所有审计跳过。这种事确实发生了一次:Hermes 副本中一个真实存在的 SQL 注入被一段对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

三个都是展示出来的,只有一个被判定:Hermes 的 file 工具集把 write_file 和 patch 与读取一起交付,而 Prime 的唯一内置工具是一个内核。一条永远不变的红色底线是一条不再有人看的线;选择 --engine hermes 的人能看到自己接受的是什么。

黑名单本身就很脆弱——Task 就在其中缺失,而且有一个子智能体曾经通过这个漏洞写了一文件,六次中发生了一次。于是这个差距被变得可检测:thot doctor --agents 会询问一个存活探针它实际持有哪些工具,并点名所有它不认识的工具,因为客户端的下一个版本会带来这个名单从未听说过的工具。而关于写入的绿色行意味着「这次没有」,而不是「不可能」:这一点在表述中写明了。

Hermes 和 Prime 没有只读模式,这一点是明说的而不是假设的:-t file 指的是「文件操作」,包括读写,而 Hermes 的 --safe-mode 涉及的是个性化设置,不是权限;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 时写入另外两者,使用它们的原生格式,且只触碰自己放置的条目——在 Hermes 中标记为 [thot],在 Prime 中位于一个分隔块内。首次修改前先备份,连续三次同步只写入一份副本。

一个新建的 USER.md 是一个空表单:**Name:**、斜体说明、一条水平线。注入它们会让 Thot 把「Context: ---」当作一个事实。它们被排除——并在屏幕上计数,因为区分表单和简略笔记,不是程序能确定做到的事。

一个库,一段历史

三者读取相同的格式——带 YAML frontmatter 的 SKILL.md,每个方法一个文件夹。这是这件事之所以可能的唯一原因。

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 的已安装库——它来自公共注册表,而这正是守卫存在的场景。它只为自己交付的内容担保。但 Hermes 的 83 个方法中有 73 个是 Thot 方法的逐位副本:把自己的文件报告为社区威胁,是一个会让人学会忽略真正威胁的误报。一个字节与已交付方法一致的方法就是那个方法。守卫从 42 次拒绝降到了 8 次。

然后从 8 降到 0,出于两个最好别混淆的不同原因。第一个是一条错误的规则:ENV[] 是一个 Ruby 常量,按构造就是大写,但该模式像整个目录一样被编译——不区分大小写。于是它把 Python 当作 Ruby 来读,把 env["…_TOKEN"] = jeton 两行之后跟着 subprocess.run(env=env)——向子进程传递秘密的推荐方式——归类为「读取秘密」,CRITICAL。第二个是层级问题:用户自己安装到相邻代理文件夹中的库,不是被审计的仓库。安装已经发生,且是故意的;Thot 只决定是否读取机器上已有的内容。它始终被扫描、始终被报告,但只在 dangerous 级别被拒绝,而被检查的仓库从 caution 级别就开始拒绝——八次拒绝中有六次仅归因于那条在记录自己令牌地址的方法中看到数据外泄的规则。

Prime 随附的 13 个方法留在 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 — 你的账户

已安装并登录的 claude CLI。无需复制任何内容。

Claude — API 密钥

一个 sk-ant-… 密钥

OpenAI

一个 API 密钥,或环境中的 OPENAI_API_KEY

本地

正在运行的 Ollama 或 LM Studio——免费、离线

其他

任何兼容 OpenAI 的端点

thot login 用于更换,thot logout 用于遗忘。配置存放在 ~/.thot/config.json 中,权限为 0600。账户模式下不存储任何令牌。

账户模式如何工作

Messages API 拒绝来自第三方程序的订阅令牌。要使用它,需要伪装成 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 发布其使用的模型的窗口(claude-opus-5[1m] 为 contextWindow: 1000000),Thot 在该窗口的 70% 处压缩——这里是 700,000 个令牌,在 200k 窗口上是 140,000。触发器读取 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 着色会跟随调用到达同一文件中定义的函数——委托型 handler 的常见形式——并在那里停止,这正是它所说明的。接下来两个层级依赖于已解析的调用图——知道这里调用的 readInput 就是那边定义的那个。Python 的导入系统能回答这个问题;JavaScript 的不能,除非有模块解析器、tsconfig 以及对 this 的类型器视角。建立在假设上的第二层级,会把一个报告已证明路径的工具,变成一个报告貌似合理路径的工具。

引擎扫描文件,而不是命名函数体。Web handler 的常见形式是传递给路由的匿名箭头函数——app.get("/x", (req, res) => { … })——没有任何索引器会为其命名。

如此测量:一个参数列表带括号且跟在逗号或左括号之后的箭头函数,即 [(,]\s*(?:async\s*)?\([^)]*\)\s*=>,在 detect_scope 保留的文件的掩码源码上——Prime 上 15 094 个,Hermes 上 19 625 个,对于一个跟随符号的引擎来说全部不可见。早期版本宣称 24 454 个而未说明计数方式;该数字完全取决于定义,因此定义被写了出来。

在两个语料库上测量:Prime 上 31 条路径,Hermes 上 41 条,覆盖 3 552 个 JS/TS 文件。七十二,不是三千——这是一个染色引擎的形状,而不是模式扫描器的形状。

引擎也跟踪那些没有人按名字调用的函数:运行时调用它们,并把值传进去。addEventListener 引入染色——参数就是输入;.then、.map、.forEach 携带染色——参数恰好在被遍历的东西被染色时被染色,因此常量列表仍然是常量列表。这就是跟踪一个值和发明一个值之间的区别,没有它,浏览器代码树几乎完全不可见。

obj[clé] = valeur 其中键受控是一个单独的 sink:有效载荷是键而不是值,因为一个 __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 工具链——一张只在某些机器上工作的地图,不如一张边界被写出来的地图。测量结果:Prime 上 8 568 个符号,耗时 1.7 秒,Hermes 上多 11 138 个。

一个文件是为了什么

严重性 = 影响 × 可达性 × 置信度,可达性来自调用图。调用图回答「一个入口点能否到达这里」。对于一个根本不是攻击面的文件,它无话可说。

在随 Thot 交付的两个程序上测量:Hermes 的 25 个 HIGH findings 中有 12 个,Prime 的 11 个中有 6 个位于测试或示例代码中。报告顶部几乎一半涉及攻击者无法到达的代码——报告就是这样不再被阅读的。

之前

之后

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,也没有丢失任何 finding。这是降级,绝不是删除:测试代码运行在开发者的机器和 CI 中,这正是供应链攻击的确切形态。finding 保留下来,并在来源中携带其角色。

分类是保守的——整个路径段,绝不是子字符串(latest/ 不是测试目录,contest.py 不是测试文件),所有未被识别的东西都是生产代码。误判为「测试」会隐藏真正的缺陷;误判为「生产」只花费一个等级。

价值从何而来

一个 finding 携带启动其路径的来源规则——source.argv、source.http、source.js.event——而不仅仅是它所在的行。报告明确说明(「一个来自命令行的值……」),JSON 以键 source_rule 给出,以便下游过滤读取事实而不是一句法语。

这是等级中缺失的一半。命令行工具中的 open(args.sortie, "w") 是操作者在命名一个文件:提供 argv 的人已经拥有该进程的文件系统,这个调用不会给他任何东西。handler 中的 open(request.args["f"]) 是任意文件读取。同一条规则,同一个 sink,两个世界。

本地来源

远程来源

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

低一个等级

完整等级

所有其他

完整等级

完整等级

只有这三个。从 argv 构建的命令仍然是命令,从环境变量读取的 pickle 仍然执行代码:在那里,是 sink 造成升级,而不是路径。

在引擎学会跟踪属性链的那天在 Hermes 上测量:没有这个区分,仅 sink.fs.read 就在报告中放入 48 个 findings,其中九个来自同一个 CI 脚本,每个都是一个打开被要求打开的文件的工具。有了它,它们降到阈值以下,保持在一个按键的距离(--all)。

未知来源按本地计,这是写出来的而不是隐藏的:假设相反会把引擎无法命名来源的每条路径都放回报告顶部。当局部变量与其派生参数之间的绑定不再丢失时,它能命名的数量翻了一倍——cible = chemin.strip() 保持了对 chemin 的链接,没有它,sink 就不附着于任何参数,因此也不附着于任何调用者。

为什么这是安全的

一个判定以 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 获得新身份,旧决定不再指向任何东西。列表将这些标记为 [absent du dernier audit],而不是像其他决定一样显示——六个决定中有三个已死,不应被读作六个活着的决定。

记忆在模型之前应用:一个已经带有决定——排除、接受或已修复——的 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 的改进应用于审计:任何静态分析都永远无法推导出的事实。它们存在于 <dépôt>/.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 就能回答。只以区间形式存在的依赖不会被猜测,它被报告为未固定版本。

覆盖精确版本的评估是事实,而非猜测——但你的代码是否到达易受攻击的函数并未被分析,因此这些发现保持 PLAUSIBLE 并明确说明这一点。只有 MAL-* 例外:该包就是载荷本身,可达性不是问题。

与其他地方相同的属性:发现的身份携带固定的版本,因此一次 bump 会使裁决过期。在 requests==2.19.1 上排除某个 CVE,并不会排除 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 时修改版本化文件,会产生没人要求的 diff:你在本地做决定,你有意地发布。

共享服务器,或现有的 mem0

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

mem0 后端与 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

✓

✓(长轮询——无需开放任何端口)

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 写入 launchd 单元(macOS)或给你 crontab 行,然后让你自己激活——一个静默安装后台任务的工具,是一个不再被信任的工具。

当 launchd 做不到时。 在 macOS 上,权限按二进制文件授予:launchd 代理可能被拒绝访问 ~/Desktop、~/Documents 或 ~/Downloads,此时单元会在解释器启动时卡住而不写入一行。从你的会话启动的进程保留该会话的访问权限,即使成为孤儿也是如此——这是第三种补救措施,它不需要系统提供任何东西:

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

它让位于 launchctl 声明实际执行过的任何 launchd 单元:两个调度器管理同一个任务,就是双倍的工作和双倍的令牌。thot doctor 会说明哪一个在服务。

定时审计在没有任何新内容时保持沉默。 一份重复相同三百条发现的夜间报告最终会进入一个没人打开的文件夹。上报的是 diff:自上次以来新出现的、超过阈值的、减去已被判定为无意义的。

插件

五个钩子,每一个都因为某个交付物在使用它:

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 显示单元已加载,其 LastExitStatus 为 0,其日志不存在——三个信号表明「一切正常」,对于一个从未启动过的任务。说出原因比数绿色行数更好。

这个检查本身也曾需要修正。它曾根据路径的形式来判定任务有罪——「树在 ~/Desktop 下,所以 launchd 无法读取它」——而 macOS 权限是按二进制文件授予的:单元的解析器读取了 /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

写入的单元携带自己的 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。

第三个特性让它快速收敛:失败会被计数。一个候选如果其代理超时,或模型拒绝表态,会保留其严重性——因此它会在下一轮优先被重新处理,再下一轮也是。在一个 1 660 行文件中的 finding 上测量:四次尝试三次通过,其中三次撞上同一堵墙。两次失败后,它排到队尾:仍然有资格,但永远不会优先。一次成功会清除计数——一堵当时只是繁忙下午或订阅耗尽的墙,不应该永远跟着一个 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

反驳是内务管理;确认是新闻。被质疑的反驳也是:这是程序在埋掉某样东西之前说自己已经纠正了。只计数而不点名,会把读者送回日志里 grep——而这正是每次发生的情况,在整整一天里。

它从不修改代码。这里的“改进”意味着程序对自身的判断变得更清晰、更便宜:没有决定的候选更少,磁盘上的决定更多,每个决定都能追溯到做出它的代理。

温度计,以及使用它的循环

以上所有内容都是用 Thot 测量 Thot。improve 向模型询问 finding 是否真实。evolve 监控 provenance,这是引擎对自己的输出计算的一份报告。两者都是循环的,而这个循环绝非学术问题:深度扫描为 9 次确认支付了 638 次判定,而一条标注为 −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

“之前”是温度计首次存在时程序的状态。“之后”是同一语料、同一下限、同一条命令。留出集也从三个方面确认了这一点: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(données) 调用,argument_injection 和 genericcmdi 是同一个 os.system('echo ' + données) 即 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 通过中间件来设置它。weak_password_hash 却能完美区分这个语料——每个易受攻击案例都是 hashlib.sha256,每个健康案例都是 pbkdf2_hmac——但这些行中没有一行说明该值是密码,这条规则会在真实仓库中对所有校验和、所有 ETag 和所有 HMAC 开枪。最后两个曾以“也许毕竟是代码”的想法被重新打开,并以一个值得写下来的理由关闭。intoverflow 用 ctypes.c_int32 人为制造其弱点——Python 整数不会溢出——而一条针对 ctypes 开枪的规则将成为针对 BenchProctor 的规则。resourceexhaust 是无边界的 bytearray(int(données)) 对比 bytearray(min(…, 1024)),分隔符是唯一的 min 令牌;但 bytearray(x) 根据 x 是整数还是缓冲区有两种含义,Thot 不推断类型,而在 hermes/ 上,这类调用的 102 处都是转换而非分配。这些是逻辑缺陷或缺失的检查;任何色调分析都看不到它们,无论你写什么规则。

因此剩下 34 个可以诚实赢得的类别,即 +55,7 %。它们全部处于活动状态。在 +55,5 % 时,这是诚实最大值的 99,6 %。

仍然缺失的 21 个案例不再是类别,而是其中两个类别内部的漏检案例,并且已经逐一清点:ssrf 中有 12 个,其来源是 mq_client.get_message() 或 redis_client.get(),这是 BenchProctor 的 app_runtime.py 中声明的两个 _Stub() 对象——它们没有命名任何存在于这个语料之外的东西,而专门为它们编写的模式会贴合温度计而不是检测任何东西;pathtraver 中有 9 个来自环境变量,而可达性减免有意将其降级,因为谁提供环境,谁就已经掌握了该进程的文件系统。

产生这四十六个点的东西,按测量顺序排列:

变更

J

起点

+9,4 %

Web 路由被识别为入口点

+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:模板、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个健康案例,当把它限定在存储型XSS时是11对13——而健康的一半中,有一半比另一半更频繁地读取数据库。两次测量对当时的程序都是正确的。改变的不是语料库:而是健康一半的防御措施,如今它们被逐一理解了——加密、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仍然被拒绝:它与ssti是同一个Template(…)调用,只是标了另一个类别,给它那个名字等于为一个不精确之处付一分。

精度不是用盲区买来的

上面识别的每个守卫,最初都是在一个让分数上升同时打开一个可利用漏洞的版本中被提出的。一个对抗性探针把它们全找出来了:

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)为一切洗白它;一个证明目的地的守卫(主机被允许、解析的地址是公网的、路径被限制)只对相关的sink家族有效。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。

它的局限,被测量过:三个框架彼此相差半分。这能抓住针对文件的过拟合,却抓不住针对基准测试形态的过拟合——一个生成的语料库仍然是生成的语料库,一条只对演示代码有帮助的规则能通过全部三个。留出法让一种作弊可见;它不能让语料库具有代表性。

循环在一轮又一轮之间保留的内容写在~/.thot/evolve-log.jsonl中。没有它,测量在一轮中几乎不动,下一轮会重读同样的最差类别,拉出同样的文件,并——合理地——收到同样的已经构建、测量和撤销的规格说明:--rounds 5会是一个尝试了五次的尝试,贵五倍,还显得很忙。

这不是一个神谕。一个补丁可以是绿的、让J上升、却仍然是坏的——这就是过拟合,而自动程序修复的文献只谈这个。语料库是我们在它上面取得进展的证据。循环报告它改变了什么,以便人类可以不同意。

Skills——Thot知道的方法

一个skill是一种只写一次的方法:一个带YAML frontmatter的SKILL.md。这是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)。已存在的名称会替换内置版本——这足以在不fork的情况下适配一个随附的方法。

来自被审计仓库的方法首先被分析

一个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(针对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 的路径

在计费模式下,这四个由thot.mcp_server提供给官方CLI——一个只读的MCP服务器,无法写入或执行任何东西。

当模型寻找谁调用process_payment时,它查询图并得到完整答案——而不是随机grep三个文件。

仅审计模式

分析核心也可以在没有模型、没有网络、没有成本的情况下使用:

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所要求的。一个用行号喂养的仪表板,只要有人在文件顶部加一个import就会重新打开所有工单;用这个喂养,不会。

而一条色调路径是一系列位置,这正是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

L'analyse déterministe répond à « ces données pourraient-elles circuler ? ». C'est exhaustif, gratuit, et ce n'est pas la question qu'on paie un auditeur à trancher. --deep pose la question chère, sur les seuls candidats qui l'ont méritée :

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

Deux passes, volontairement adverses :

  1. La sonde doit nommer une entrée concrète qui atteint le point dangereux. Pas de généralité sur la classe de vulnérabilité — une URL, une valeur, un effet.

  2. La réfutation reçoit ce scénario avec pour seule mission de le détruire : validation en amont, appelant qui ne passe que des constantes, type qui interdit l'entrée supposée. En cas de doute, elle réfute.

Un finding ne survit que si une seconde lecture hostile du même code échoue à le tuer. confirmed veut alors dire quelque chose.

En session, la même chose : /audit deep.

Le moteur est choisi automatiquement — ton compte Claude via le CLI officiel s'il est connecté (les analyses tournent en parallèle, sur ton abonnement), une clé API sinon.

Ce que l'audit ne doit pas lire

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

Les exclusions intégrées couvrent ce que tout dépôt a — node_modules, build, .venv. .thotignore couvre ce que seul ce dépôt sait : de la documentation embarquée, un client généré, un dossier de fixtures cassées exprès. Les auditer ne produit pas des findings, ça produit du bruit à l'endroit exact où seraient les findings.

Tes propres règles

Le catalogue intégré connaît la bibliothèque standard. Il ne connaît pas le wrapper que ton équipe a écrit autour de subprocess, la file que ton service consomme, ni le validateur qui rend une valeur sûre chez toi. Sans un endroit où le dire, chaque audit d'un vrai système se trompe aux trois mêmes endroits.

# <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]

Le même fichier porte les règles JavaScript, sous une clé js: — un wrapper d'équipe existe en général dans les deux langages, et séparer la déclaration est la façon dont une moitié devient obsolète.

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]

Ce que le modèle demande est une entrée non fiable

Les sources sont des expressions — sys.argv, os.environ. Cela couvre un programme qu'on lance et rate un programme qu'on appelle : l'outil d'un agent reçoit son entrée non fiable en paramètres nommés, remplis par un registre à partir de ce qu'un modèle a demandé, et aucune expression n'apparaît nulle part dans le corps.

Le coût mesuré de ne pas modéliser ça : quatre SSRF en un après-midi, toutes atteintes par un argument d'outil, aucune trouvée par la teinte — elles l'ont été par des règles de motif, qui reconnaissent une forme et ne prouvent rien.

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

Vide par défaut, et volontairement : quelles fonctions un registre appelle est un fait sur un dépôt, et le deviner mettrait une source sous chaque paramètre de chaque programme. Mesuré sur Hermes, les deux extrêmes : une règle nommant les paquets plugins et tools révèle 19 chemins prouvés dont plusieurs sur-approximés (le base_url qu'un helper reçoit de la configuration n'est pas non fiable) ; une règle nommant le paramètre args en révèle zéro, parce que les gestionnaires de Hermes prennent des paramètres nommés et non un dictionnaire. La bonne règle nomme les points d'entrée réels — et c'est à leurs auteurs de la connaître.

Une règle qui reprend un id intégré le remplace — de quoi dégrader un sink que l'équipe a délibérément accepté, sans patcher Thot. Un fichier mal formé arrête l'audit en nommant le fichier et la clé fautive, plutôt que de laisser croire à une absence de finding.

Les suppressions

Une suppression est la seule affirmation sur la sécurité qu'aucun outil ne relit — y compris celui-ci, par construction. # nosec, # noqa: S310, // eslint-disable … security/… : c'est une affirmation sur le code, écrite une fois, qui survit aux appelants qu'elle décrivait.

Deux fois dans un même audit, ici, elle était fausse :

suppression

ce qu'elle affirmait

ce qui était vrai

# nosec B310 — scheme checked above

le schéma est contrôlé

il arrêtait file:// et rien d'autre — SSRF vers le service de métadonnées

# noqa: S310 (configured peers)

l'URL vient de la configuration

un des appelants la lit dans un argument d'outil, donc du modèle

Thot les rapporte donc comme classe, en LOW, avec le motif écrit à côté. Le finding ne dit pas « cette ligne est dangereuse » : il dit « personne n'a relu la raison pour laquelle elle a été excusée ». Sur une passe --deep, c'est un agent qui va vérifier si le motif tient encore.

Pour Python, ce sont les vrais jetons de commentaire qui sont lus, pas des motifs — une expression rationnelle ne distingue pas # nosec dans un commentaire du même texte cité dans une docstring, et la docstring de ce module en cite deux.

Une suppression posée sur une ligne que cet audit signale n'est pas le même objet : c'est une affirmation qui contredit un finding vivant, écrite par quelqu'un qui a lu la même ligne et conclu autrement. Elle est remontée d'un cran et le dit. Mesuré sur Hermes : 7 sur 45 — et trois des suppressions lues ce jour-là étaient fausses.

Mesuré : 0 sur Thot, 0 sur Prime, 45 sur Hermes.

Calibration

La précision compte autant que la détection. Sont volontairement non rapportés :

  • subprocess.run(cmd) sans shell=True — aucun shell ne lit la commande.

  • cursor.execute("… ?", params) — requête littérale, paramètres liés.

  • Une valeur passée par int(), shlex.quote(), os.path.basename(), html.escape() — ces appels cassent la chaîne de contamination.

  • Un défaut qu'aucun point d'entrée n'atteint est dégradé automatiquement — mais seulement si des points d'entrée ont été trouvés. Sans aucun, la portée est inconnue, pas nulle, et rien n'est enterré sur cette ignorance.

  • payload.get(...) n'est pas requests.get(...).

Ordre de grandeur, mesuré sur Hermes Agent (4 457 fichiers Python) : 98 s, 365 findings dont 25 high — 3 au-dessus du seuil par défaut une fois la mémoire appliquée.

Quel paramètre l'argument remplit

Un helper dont un paramètre atteint un sink ne rend pas dangereux tout ce qu'on lui passe. Le moteur appariait pourtant l'appelant avec l'ensemble des sinks du callee, sans regarder où l'argument atterrissait :

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é

La position est maintenant lue au site d'appel, et le nom pour un argument nommé. C'est ce que le moteur JavaScript faisait déjà de son côté.

Mesuré, une fois l'exécution possible : sur 83 238 résolutions demandées en analysant Thot et hermes/tools, 90 % désignent un paramètre unique et 9,8 % n'en désignent aucun ; 0,1 % restent ouvertes (f(*rest), f(**options)) et gardent la réponse large. Le callee offrait 2,68 paramètres en moyenne, donc près des deux tiers de l'espace de recherche disparaissent — l'analyse de hermes/tools passe de 11,4 s à 9,5 s.

Sur le nombre de findings, en revanche : aucun changement. Thot, prime et hermes/tools rendent exactement les mêmes 151 candidats avant et après, dont zéro inventé — la propriété de sûreté tient sur le terrain. Quatre chemins de teinte se raccourcissent, rien de plus. La forme corrigée ci-dessus est réelle et le test le prouve, mais elle ne se produit dans aucun des trois arbres mesurés : ce correctif achète de la vitesse et des chemins plus justes, pas moins de bruit.

Deux formes ne disent rien : helper(*args) étale un nombre inconnu de valeurs, et helper(**options) n'en nomme aucune. Là, le moteur garde la réponse large qu'il donnait avant — l'ensemble retenu est toujours inclus dans celui d'avant, donc affiner ne peut que retirer un finding, jamais en inventer un.

Le receveur est sauté sur la foi de la signature du callee, pas de la syntaxe de l'appel : Runner().go(x) se rend en un go sans point, et Cls.m(obj, x) comme obj.m(x) sont tous deux un attribut. Une méthode étant presque toujours appelée liée, c'est self en tête des paramètres qui décide — l'appel non lié est la seule forme lue un cran trop court.

Ce que le graphe ne peut pas suivre

Un défaut atteint par un chemin que l'analyse ne résout pas — un handler rangé dans une table de dispatch, une vue décorée, un appel sur une variable dont le type est inconnu — n'est pas un défaut injoignable. Thot distingue les deux :

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

Dans les trois cas la portée est inconnue, pas nulle, et le finding garde une pénalité légère au lieu d'être enterré. Sur Hermes : mêmes 365 findings, mais 60 remontent d'un cran. Une fonction que personne n'appelle et que personne ne mentionne reste, elle, correctement décotée — sinon le filtre cesserait d'être un filtre.

Limites

La teinte à travers les fichiers est réservée à Python : JavaScript et TypeScript sont indexés, suivis dans un corps de fonction et jusqu'à un helper du même fichier, et s'arrêtent là faute de graphe d'appels résolu — le tableau plus haut le dit ligne par ligne. Les règles par motif, elles, s'appliquent partout.

Le +55,5 % est un chiffre Python. BenchProctor étiquette du django, du fastapi et du flask, et rien d'autre ; le catalogue JavaScript est plus étroit que son homologue — pas de source « lecture en base », pas de sink de journalisation — et ses règles reposent sur ses tests plutôt que sur 18 300 cas étiquetés. Quand une correction vaut pour les deux moteurs elle est portée sur les deux, et le journal dit laquelle a été mesurée où.

sink.log lit le message et lui seul. logger.info('failed: %s', exc) confie la valeur à la machinerie de journalisation au lieu de l'écrire dans la chaîne que l'auteur a tapée ; lire ces arguments-là aussi vaut +0,0000 sur le corpus et 4 844 findings de plus sur hermes/, un par except … as exc: de l'arbre. Ce n'est pas couvert, et le dire coûte moins cher que le laisser croire.

Sans --deep, chaque finding est PLAUSIBLE : détecté statiquement, pas encore prouvé par exécution. Avec --deep, un finding confirmed a survécu à une réfutation adverse — ce n'est pas encore une preuve d'exécution, qui viendra avec le repro. Et l'absence de finding n'est pas une preuve d'absence de défaut : dispatch dynamique, réflexion et métaprogrammation échappent à l'analyse.

La recherche plein texte dans les sessions cesse d'être à coût constant sur les SQLite dont fts5 ne sait pas remonter les rowid : le moteur y trie l'ensemble des correspondances au lieu de couper à vingt. Mesuré sur celui qu'embarque CPython 3.12 — 6 588 instructions contre 27 443 quand le corpus est multiplié par cinq, là où un moteur capable reste à 2 215 puis 2 698. Un test le vérifie et nomme la version quand elle ne le permet pas.

Développement

cd Thot
uv run pytest -q

Installé en éditable : le code source fait foi immédiatement. En revanche, si pyproject.toml change (nouvelle dépendance), il faut relancer uv tool install --editable --from . thot --force.

Le noyau déterministe (codemap, taint, scope, scoring, store, report) ne dépend d'aucun agent et ne touche pas au réseau — un test le vérifie et fait échouer la suite si ça change.

Spec et plans : 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