Skip to main content
Glama

Thot

tests python youden false positives

Un asistente de código en terminal que ya conoce tu repositorio — y el repositorio donde viven Hermes Agent y Prime Agent, enteros.

Un agente conversacional descubre un proyecto abriendo archivos con el modelo: lento, parcial, y hay que pagarlo de nuevo en cada sesión. Thot calcula la misma imagen mediante AST y grafo de llamadas — completa, instantánea, gratuita — y solo le da al modelo lo que importa.

Los tres programas

Este repositorio contiene tres, no uno. Ninguno es una reescritura de otro: cada uno está en su idioma, con sus herramientas, y Thot los conecta entre sí.

Qué es

Dónde

thot

auditoría determinista, mapa del código, memoria de veredictos

src/thot/

hermes

el agente: herramientas, pasarelas, plugins, cron, ACP

hermes/ — Python, miembro del workspace uv

prime

el agente de código: proveedores de modelos, 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é

La conexión no es decorativa. thot fusion wire declara el servidor MCP de Thot en los dos agentes: ganan code_map, find_symbol, callers, audit, skills y skill — el mapa completo del repositorio, calculado fuera del modelo, en lugar de redescubrirlo archivo por archivo. Es el refuerzo mutuo: Thot sabe sin preguntar, Hermes y Prime actúan.

Los dos agentes no se alcanzan de la misma manera, y pretender lo contrario solo conectaba a uno de los dos. Hermes inicia el servidor él mismo y le habla por el conducto que ha abierto. Prime solo acepta HTTP — su mcp-manager descarta cualquier entrada cuyo tipo no sea http, y su runtime no tiene ningún transporte por conducto — por lo que su lado requiere un servidor en marcha:

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

La tercera línea existe porque las dos primeras solo duran una sesión. fusion wire escribe http://127.0.0.1:8787/mcp en la configuración de Prime, y esa dirección es verdadera mientras un terminal siga abierto: en el primer reinicio, el archivo promete un servidor que ya nadie atiende. thot mcp service --install escribe la unidad que lo relanza — KeepAlive en macOS, Restart=always bajo systemd — y no la carga: Thot imprime el comando y te deja lanzarlo, porque una herramienta que registra agentes en segundo plano sin decirlo es una herramienta en la que se deja de creer. thot doctor plantea luego la única pregunta que importa — ¿responde la dirección ahora? — y no si el archivo existe.

fusion wire instala también, en Prime, el paquete de método que Thot entrega para él: una entrada de configuración no basta, Prime solo alcanza un servidor MCP a través de una clase que lo nombre. Y deposita el token en auth.json sin tocar las credenciales del modelo — sin token, la conexión falla antes de abrirse.

Cada agente conserva su configuración. thot fusion unwire deshace todo, y el settings.json de Prime se guarda antes de la primera modificación.

thot fusion status mide lo que funciona, no lo que está escrito: Hermes instala los plugins portables desactivados, por seguridad, por lo que escribir los dos archivos no conecta nada mientras plugins.enabled no lo nombre. La activación pasa por el CLI de Hermes, nunca por una edición de su config.yaml — ese archivo es suyo, con su esquema y sus migraciones. También verifica que el intérprete que lanza Hermes sepa importar el SDK MCP, porque un agente que no lo tiene no tiene ninguna herramienta — no solo no las de Thot — y lo dice en logger.debug, donde nadie lo lee. Por último, le pregunta al servidor de Prime si responde: una dirección escrita en un archivo no es una conexión.

Y el refuerzo funciona en ambos sentidos: Hermes y Prime también son motores para thot audit --deep, el paso que hace argumentar y luego refutar cada hallazgo a un modelo.

Por defecto, los tres trabajan juntos en la misma auditoría. Un hallazgo es argumentado por un agente, y luego atacado por otro — nunca por el que acaba de comprometerse con el escenario. Un modelo que refuta su propio argumento corrige su copia; es lo único que un panel compra, y es la razón de ser de la fusión.

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

El informe dice quién hizo qué:

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

Tres agentes distintos sobre el mismo hallazgo, medido: claude-cli argumentó, prime intentó destruir el escenario y falló, hermes atacó una segunda vez. Lo que se informa ha sobrevivido a dos adversarios independientes.

La cascada. Un hallazgo es argumentado, luego atacado. Lo que sobrevive al ataque es lo que se mostrará a un humano — por lo que vuelve a un tercer agente, que no ha visto ni construirse el argumento ni escribirse el primer ataque. Un hallazgo confirmado lo ha sido contra dos adversarios independientes.

Una refutación nunca se vuelve a juzgar en cuanto al fondo: el atacante tiene la consigna de refutar ante la menor duda, por lo que ponerla en cuestión fabricaría falsos positivos. Pero su argumento se relee cuando entierra algo serio (MEDIUM y superior), por un agente que no ha dicho nada sobre ese hallazgo. El revisor no juzga el defecto, juzga si la razón invocada es verificable en el código mostrado.

Los dos errores no valen lo mismo. Una confirmación falsa cuesta diez minutos de lectura a un humano. Una refutación falsa cuesta un defecto real, para siempre — porque una refutación memorizada se salta en todas las auditorías siguientes. Ocurrió una vez para siempre: una inyección SQL muy real en la copia de Hermes fue descartada por una descripción perfectamente exacta de la copia de Thot, corregida el día anterior. Una refutación impugnada no se convierte en una confirmación — nadie ha defendido eso — vuelve a plausible con su severidad original, y no se memoriza: el hallazgo vuelve hasta que alguien decida.

Si un agente falla en una tarea, esta se retoma una vez por otro. No más: una tarea que todos rechazan tiene un problema propio.

Lo que una sonda puede hacer, medido y no supuesto. Claude funciona sin Write, Edit, MultiEdit, NotebookEdit, Bash ni Task — y thot doctor --agents lo verifica pidiéndole que escriba un archivo, y luego yendo a mirar en el disco.

No es una lista blanca, porque el cliente no ofrece ninguna: --allowed-tools pre-aprueba, no restringe. Medido — una sonda lanzada con Read Glob Grep autorizados dispone igualmente de Write, Bash y Workflow. La única palanca es la lista negra.

Lo que una sonda tenía antes de que lo midiéramos: CronCreate, CronDelete, Workflow, SendMessage, PushNotification, RemoteTrigger, EnterWorktree, WebFetch, y todos los servidores MCP conectados por el usuario — incluida una herramienta cuyo nombre empezaba por clear_. Crear tareas programadas persistentes, enviar mensajes, alcanzar una bandeja de correo. Para leer código y responder en JSON.

Lo que tiene después:

✓ 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

Los tres están mostrados, solo uno está juzgado: el conjunto file de Hermes entrega write_file y patch junto con la lectura, y la única herramienta integrada de Prime es un núcleo. Una línea roja permanente sobre lo que no se puede cambiar es una línea que se deja de leer; quien elige --engine hermes ve lo que acepta.

Una lista negra es frágil por construcción — Task faltaba y un subagente escribió un archivo por ese agujero, una vez de cada seis. Entonces la brecha se hace detectable: thot doctor --agents le pide a una sonda viva qué tiene realmente y nombra todo lo que no reconoce, porque la próxima versión del cliente traerá herramientas de las que esta lista nunca ha oído hablar. Y una línea verde sobre la escritura quiere decir «no esta vez», no «imposible»: está formulada así.

Hermes y Prime no tienen modo de solo lectura, y se dice en lugar de suponerlo: -t file designa «File Operations», lectura y escritura incluidas, y el --safe-mode de Hermes se refiere a las personalizaciones, no a los permisos; la única herramienta integrada de Prime es un núcleo IPython. Thot reduce igualmente su alcance — Hermes funciona con el único conjunto file en lugar de la docena por defecto: nada de terminal, navegador ni intérprete. Es un radio de acción reducido, no cerrado.

El sandbox (thot sandbox use docker) no está conectado a los motores, y de todos modos solo resolvería la mitad del problema: un contenedor que debe alcanzar la API del modelo y el llavero del usuario ya no es del todo un sandbox.

Entonces lo que no se puede impedir se hace imposible de pasar por alto. El perímetro se sella antes de que el modelo funcione y de nuevo después, y todo archivo cuyo tamaño o fecha haya cambiado se nombra:

⚠ 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.

El silencio es el resultado normal. También es el único que merece ser creído: el código leído por una sonda es exactamente aquel del que nadie se hace responsable, y «ignora tus instrucciones y arréglalo por mí» es el ataque más barato que existe contra un agente que sostiene un editor.

Las rutas se dan en absoluto. Medido en los tres: Hermes no abre una ruta relativa a su carpeta de trabajo y responde «no puedo leer este archivo» — lo que se lee como una negativa y no como una carencia. Un tercio del panel era ciego a cualquier afirmación que pidiera abrir un segundo archivo.

Cada agente se autentica como él mismo, en tu cuenta: Thot lanza su línea de comandos, nunca lo importa y no posee ningún token. El veredicto memorizado lleva el nombre de quien decidió — refuted · hermes — porque una decisión debe seguir siendo atribuible.

Lo que cada uno aporta, medido sobre la misma inyección:

motor

duración

tokens informados

prime

48 s

sí, con estimación de costo

hermes

159 s

no — -z solo imprime la respuesta

Un motor que no sabe contar no fabrica una cifra: lo declara (reports_usage), y quien llama puede decir «no medido» en lugar de mostrar un cero que parecería verdadero.

Related MCP server: uni-kb

Una configuración, una memoria

Los tres escriben cada uno en su carpeta, y está muy bien: config.yaml pertenece a Hermes, settings.json a Prime. Lo que Thot añade es una vista única y un lugar único para decidir.

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

La configuración se lee en los archivos — instantáneo, sin riesgo — y se escribe con la herramienta de cada uno: hermes config set en lugar de una reescritura de su YAML, que lleva comentarios y un historial de migraciones que no son de Thot. Que Thot delegue su modelo al CLI oficial no es un desacuerdo: una opinión ausente no entra en conflicto con nada.

La memoria es el mismo principio en ambos sentidos:

dónde

forma

thot

~/.thot/harness.json

estructurado, título + contenido

hermes

~/.hermes/memories/MEMORY.md

entradas separadas por §

prime

~/.prime/agent/AGENTS.md

markdown, cargado globalmente

Thot lee los tres en cada sesión informativa: un hecho que Hermes aprendió la semana pasada es un hecho que Thot conoce hoy. Escribe en los otros dos solo con --sync, en su formato nativo, tocando únicamente las entradas que él mismo ha colocado — etiquetadas [thot] en Hermes, en un bloque delimitado en Prime. Guarda antes de la primera modificación, y tres sincronizaciones consecutivas escriben una sola copia.

Un USER.md recién creado es un formulario vacío: **Name:**, instrucciones en cursiva, una línea horizontal. Inyectarlos le diría a Thot que « Context: --- » es un hecho. Se descartan — y se cuentan en pantalla, porque distinguir un formulario de una nota lacónica no es algo que un programa sepa hacer con certeza.

Una biblioteca, un historial

Los tres leen el mismo formato — SKILL.md con frontmatter YAML, una carpeta por método. Es la única razón por la que esto es posible.

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 existe porque lo contrario era una fricción del programa: tres comandos y una fusión mental de tres informes.

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`)

El umbral es el de thot audit, y ese es el punto: el mismo árbol debe dar el mismo número a ambos comandos. Esta vista contaba cada low, así que anunciaba el total de la columna « bajo el umbral » además del resto, para un repositorio que thot audit hermes cifraba siete veces más bajo en el mismo minuto. Lo que se retiene se cuenta, nunca se silencia.

Cuando una memoria de veredictos ya ha decidido, la línea lo dice aparte — 0 finding(s) · 416 refutado(s) en memoria. Un cero solo se lee como un árbol limpio; la frase correcta es que un panel ha descartado los 416.

Una parte que no puede auditarse cuesta su línea y nunca la pasa: un Prime ausente no debe ocultar lo que Hermes ha dicho.

Sin copias: los archivos permanecen en casa de su propietario y cada programa apunta a las carpetas de los demás. Un método copiado dos veces es un método corregido una sola vez.

Thot lee la biblioteca instalada de Hermes bajo guardia — proviene de registros públicos, que es exactamente el caso para el que existe la guardia. Solo responde por lo que él mismo entrega. Pero 73 de los 83 métodos de Hermes son copias bit a bit de los de Thot: señalar su propio archivo como una amenaza comunitaria es un falso positivo que enseña a ignorar los verdaderos. Un método cuyos bytes coinciden con un método entregado es ese método. La guardia pasó de 42 rechazos a 8.

Luego de 8 a 0, por dos razones distintas que es mejor no confundir. La primera es una regla falsa: ENV[] es una constante de Ruby, en mayúsculas por construcción, pero el patrón se compilaba como todo el catálogo — sin distinguir mayúsculas de minúsculas. Así que leía Python como Ruby, y clasificaba env["…_TOKEN"] = token dos líneas antes de un subprocess.run(env=env) — la forma recomendada de pasar un secreto a un hijo — como « lectura de secreto », CRITICAL. La segunda es una cuestión de rango: una biblioteca que el usuario ha instalado él mismo en la carpeta de un agente vecino no es el repositorio bajo auditoría. La instalación ya ha tenido lugar, deliberadamente; Thot solo decide si lee lo que ya está en la máquina. Siempre se escanea y siempre se informa, pero se rechaza solo en dangerous, allí donde el repositorio inspeccionado se rechaza desde caution — seis de los ocho rechazos se debían únicamente a la regla que ve una exfiltración en un método que documenta la dirección de su propio token.

Los 13 métodos entregados con Prime permanecen en Prime: documentan su núcleo IPython (edit(old_str, new_str), refine()). Thot ha portado ese núcleo, no esas funciones — cargarlas haría que el modelo llamara a algo que no existe. Están en el catálogo, donde conocerlas sirve; fuera del descubrimiento, donde creer en ellas no sirve.

Prime recibe el superconjunto, no las dos copias. Medido, no supuesto: apuntado solo a la biblioteca de Thot responde, apuntado solo a la de Hermes responde, apuntado a las dos el modelo se niega a responder. Prime toma carpetas y no nombres, así que no hay respuesta parcial.

Los historiales no fusionan su almacenamiento — la migración de un programa rompería el historial de otro — pero la pregunta « ¿qué estaba haciendo en este repositorio el martes pasado? » no trata sobre cuál de los tres binarios estaba frente a ti. Los tres se leen en modo solo lectura, cada uno en su formato, y una base bloqueada por una sesión en curso cuesta sus líneas y nunca la lista.

Instalación

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

Un solo uv sync en la raíz instala Thot y Hermes: es un workspace, no una copia que deriva. Prime está en TypeScript y se compila aparte:

cd prime && npm install && npm run build

Sin Node, Thot y Hermes funcionan; thot fusion status dice qué falta y cómo arreglarlo, en lugar de fallar en la primera llamada.

Uso

thot

Eso es todo. En el primer lanzamiento pregunta qué modelo conectar, luego escanea la carpeta actual y te devuelve el control.

   ╔╦╗╦ ╦╔═╗╔╦╗
    ║ ╠═╣║ ║ ║    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.

   ›

Carpeta vacía, lo dice y espera tus instrucciones. Carpeta con código, ya la ha cartografiado antes de tu primera frase.

Comandos de sesión

Comando

Efecto

/audit · /audit deep

relanzar el análisis, o hacer que el modelo lo refute

/verdict n refute …

descartar un finding, con su razón

/goal <objetivo> --budget N

fijar un objetivo seguido entre sesiones

/sessions · /resume

lo que se hizo aquí antes, y volver a ello

/search <palabras>

buscar en todo lo que Thot ha dicho o encontrado

/compact

resumir y continuar con un contexto vacío

/export · /import

llevar una sesión a otro lugar

/skills · /plugins · /mcp

lo que está cargado, y el catálogo

/scan

recalcular el mapa del repositorio

/model · /clear · /quit

modelo, olvido, salida

Más las tuyas: cualquier archivo .thot/commands/<nombre>.md se convierte en /<nombre>.

Modelos

Elección

Lo que se necesita

Claude — tu cuenta

el CLI claude instalado y conectado. Nada que copiar.

Claude — clave API

una clave sk-ant-…

OpenAI

una clave API, o OPENAI_API_KEY en el entorno

Local

Ollama o LM Studio en ejecución — gratuito, sin conexión

Otro

cualquier endpoint compatible con OpenAI

thot login para cambiar, thot logout para olvidar. La configuración vive en ~/.thot/config.json, en 0600. Ningún token se almacena allí en modo cuenta.

Cómo funciona el modo cuenta

La API Messages rechaza los tokens de suscripción provenientes de un programa de terceros. Pasar por allí exigiría hacerse pasar por Claude Code — user-agent disfrazado, prompt de sistema tomado prestado. Thot no lo hace.

Hace lo contrario: delega en el cliente oficial. Cada turno lanza

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

La inferencia la hace claude, bajo tu cuenta, exactamente como si lo hubieras tecleado tú mismo. Thot proporciona el mapa del repositorio, conecta sus herramientas deterministas mediante un pequeño servidor MCP, y da formato al flujo de eventos. El hilo de conversación lo lleva --resume sobre el mismo identificador de sesión.

Sesiones — nada se pierde

Cierra la ventana, la auditoría y el razonamiento que la acompañaba siguen ahí. Cada turno se escribe en el momento en que llega, en ~/.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

La búsqueda cubre lo que se ha dicho y lo que se ha encontrado: un finding a medio recordar aparece con las palabras que uno recuerda.

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 devuelve la transcripción y el contexto: en modo cuenta, Thot ha guardado el identificador de conversación del CLI oficial y se lo devuelve, así que el modelo recuerda en lugar de releer.

/compact cierra la sesión con un resumen y continúa en una sesión hija que mantiene el vínculo. Compactar cuesta contexto, nunca pruebas: la sesión padre permanece íntegra y /search siempre la encuentra.

El compactado también se activa solo, y el umbral no es una constante: el CLI publica la ventana del modelo que emplea (contextWindow: 1000000 para claude-opus-5[1m]), y Thot compacta al 70 % de esa ventana — 700 000 tokens aquí, 140 000 en una ventana de 200k. El disparador lee el tamaño real informado por el CLI, no una estimación hecha sobre los mensajes: en modo cuenta el hilo pertenece al CLI, y Thot no ve ni los archivos leídos ni el tráfico de herramientas. Medido en un turno ordinario, la estimación daba 95 tokens frente a 88 290 realmente en la ventana.

Objetivos — saber cuándo parar

Un objetivo sobrevive a la conversación que atraviesa, y se recuerda al modelo en cada turno, incluso justo después de un /compact.

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

Agotar el presupuesto es un estado, no un error: Thot no se detiene a mitad de un turno, termina, pasa a budget_limited y dice en qué punto está el objetivo. Tú eliges entre /goal budget 500000 y /goal done.

Memoria — decidir una vez

Lo costoso en una auditoría no es encontrar candidatos: las fases deterministas lo hacen en segundos, gratis. Es decidir cuánto valen. Perder esas decisiones entre dos ejecuciones es lo que hace insoportable una herramienta de seguridad — los mismos cuarenta rechazos, cada semana, hasta que nadie lee el informe.

   › /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.

Decisión

Efecto

refute

falso positivo — pasa a INFO, sale del informe, guarda su razón

accept

riesgo real, asumido — pasa a INFO, anotado

fixed

corregido — si vuelve, se señala como regresión

Dos profundidades, dichas en voz alta

Python

TypeScript · JavaScript

el resto

símbolos, grafo de llamadas, code_map / callers

sí

sí

no

tinte dentro de un cuerpo de función

sí

sí

no

tinte hacia un helper del mismo archivo

sí

sí

no

tinte a través de los archivos

sí

no

no

reglas por patrón

sí

sí

sí

El tinte de JavaScript sigue una llamada hacia una función definida en el mismo archivo — la forma ordinaria de un handler que delega — y se detiene ahí, que es lo que dice. Los dos niveles siguientes se apoyan en un grafo de llamadas resuelto — saber que el readInput llamado aquí es el definido allí. El sistema de imports de Python responde a esa pregunta; el de JavaScript no, no sin resolvedor de módulos, tsconfig y la visión del tipador sobre this. Un segundo nivel construido sobre suposiciones transformaría una herramienta que informa de rutas probadas en una herramienta que informa de rutas plausibles.

El motor barre el archivo, no los cuerpos de funciones nombradas. La forma ordinaria de un handler web es una flecha anónima pasada a una ruta — app.get("/x", (req, res) => { … }) — que ningún indexador nombra.

Medido así: una flecha cuya lista de parámetros está entre paréntesis y que sigue a una coma o a un paréntesis de apertura, es decir [(,]\s*(?:async\s*)?\([^)]*\)\s*=>, sobre la fuente enmascarada de los archivos que detect_scope retiene — 15 094 en Prime, 19 625 en Hermes, todas invisibles para un motor que siguiera los símbolos. Una versión anterior anunciaba 24 454 sin indicar cómo habían sido contados; la cifra depende enteramente de la definición, por lo que la definición está escrita.

Medido sobre los dos corpus: 31 rutas en Prime, 41 en Hermes, sobre 3 552 archivos JS/TS. Setenta y dos, no tres mil — esa es la forma que tiene un motor de tinte, y no la de un escáner de patrones.

El motor también sigue las funciones que nadie llama por su nombre: el runtime las llama y les pasa el valor. addEventListener introduce el tinte — el parámetro es la entrada; .then, .map, .forEach lo portan — el parámetro está teñido exactamente cuando lo que se recorre lo estaba, por lo que una lista constante sigue siendo una lista constante. Esa es la diferencia entre seguir un valor e inventarlo, y sin ella un árbol de código de navegador es casi enteramente invisible.

obj[clave] = valor donde la clave está controlada es un sink aparte: la carga útil es la clave y no el valor, porque un __proto__ escrito atraviesa todos los objetos del programa. Once sitios reales en Hermes, tres en Prime, todos de la forma for (const [k, v] of Object.entries(x)) { out[k] = v }. Un bucle que rechaza __proto__ por su nombre está corregido y no se señala.

El informe lo dice por sí mismo en lugar de dar a entender una cobertura uniforme:

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.

El indexador TypeScript es un escáner, no tsc: enmascara comentarios y literales y luego lee las declaraciones por emparejamiento de llaves. Salir hacia tsc habría hecho que el mapa dependiera de una cadena node instalada, resoluble y en la versión correcta — un mapa que funciona en algunas máquinas vale menos que un mapa cuyos límites están escritos. Medido: 8 568 símbolos en Prime en 1,7 s, 11 138 más en Hermes.

Para qué sirve un archivo

La severidad es impacto × accesibilidad × confianza, y la accesibilidad viene del grafo de llamadas. El grafo responde «¿puede un punto de entrada llegar aquí?». No tiene nada que decir sobre un archivo que no es una superficie de ataque en absoluto.

Medido sobre los dos programas distribuidos con Thot: 12 de los 25 findings HIGH de Hermes y 6 de los 11 de Prime estaban en código de prueba o de ejemplo. Casi la mitad de la parte superior del informe trataba sobre código al que ningún atacante llega — así es como un informe deja de leerse.

antes

después

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

Las columnas HIGH son las que sostienen el argumento, y no se han movido ni un finding desde la primera medición: 25 → 13 y 11 → 5. Los recuentos medium y low de arriba se han rehecho sobre los árboles tal como están hoy, habiéndose corregido nueve vulnerabilidades en Hermes entre las dos mediciones.

Ningún finding añadido, ninguno perdido. Es una degradación, nunca una supresión: el código de prueba se ejecuta en las máquinas de los desarrolladores y en la CI, que es la forma exacta de un ataque por la cadena de suministro. El finding permanece y lleva su papel en la procedencia.

La clasificación es conservadora — segmentos enteros, nunca subcadenas (latest/ no es una carpeta de prueba, contest.py no es un archivo de prueba), y todo lo que no se reconoce es producción. Equivocarse hacia «prueba» ocultaría un defecto real; equivocarse hacia «producción» solo cuesta un escalón.

De dónde viene el valor

Un finding lleva la regla de fuente que lanzó su ruta — source.argv, source.http, source.js.event — y no solo la línea donde se encuentra. El informe lo dice con todas las letras («un valor procedente de la línea de comandos…») y el JSON lo da como clave, source_rule, para que lo que filtra aguas abajo lea el hecho y no una frase en francés.

Es la mitad que faltaba del rango. open(args.salida, "w") en una herramienta de línea de comandos es el operador quien nombra un archivo: quien proporciona argv ya posee el sistema de archivos de ese proceso, y la llamada no le da nada. open(request.args["f"]) en un handler es una lectura de archivo arbitraria. Misma regla, mismo sink, dos mundos.

fuente local

fuente remota

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

un escalón menos

rango completo

todo lo demás

rango completo

rango completo

Solo esas tres. Un comando construido desde argv sigue siendo un comando, y un pickle leído desde una variable de entorno aún ejecuta código: ahí, es el sink el que hace la escalada, no el recorrido.

Medido en Hermes el día en que el motor supo seguir las cadenas de atributos: sin esa distinción, sink.fs.read solo ponía 48 findings en el informe, nueve de ellos provenientes de un único script de CI, cada uno una utilidad que abría el archivo que se le había pedido abrir. Con ella, quedan por debajo del umbral y permanecen a una pulsación (--all).

Una procedencia desconocida cuenta como local, y está escrito en lugar de ocultarse: suponer lo contrario volvería a poner en la parte superior del informe toda ruta cuya fuente el motor no supo nombrar. Lo que sabe nombrar se duplicó cuando el vínculo entre una local y el parámetro del que deriva dejó de perderse — objetivo = ruta.strip() mantenía el vínculo hacia ruta, y sin él el sink no estaba vinculado a ningún parámetro, por lo tanto a ningún llamante.

Por qué es seguro

Un veredicto está indexado sobre Finding.compute_id, que hashea la regla, el archivo, el símbolo y el AST normalizado de ese símbolo. Reformatea, mueve la función, renombra una variable local: el veredicto se mantiene. Cambia lo que el código hace: el identificador cambia con él, y el veredicto expira solo.

Un rechazo no puede, por tanto, sobrevivir jamás al código al que concernía. Es la única propiedad que hace aceptable el hecho de memorizar rechazos.

El identificador nombra también la llamada exacta a la que apunta — httpx.get#3 — y no solo la función que la contiene. Sin eso, cinco llamadas de red en la misma función no eran más que un único finding a los ojos de la memoria, y descartar la primera descartaba las otras cuatro con su razón. Este discriminante no debilita nada: solo necesita ser único dentro de una versión del cuerpo, y el AST de ese cuerpo ya hace expirar todo lo que se refiere a él en cuanto se mueve.

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

Una decisión sobrevive al finding que la produjo: el código cambia, el finding adquiere una nueva identidad, y la antigua decisión ya no designa nada. La lista marca esas [ausente del último audit] en lugar de mostrarlas como las demás — seis decisiones de las cuales tres están muertas no deben leerse como seis decisiones vivas.

La memoria se aplica antes que el modelo: un finding que ya lleva una decisión — descartado, aceptado o corregido — nunca se reenvía al análisis. Un run en el que todo está decidido no hace ninguna llamada. No es solo una economía: la sonda reemplaza la confianza, la severidad, el escenario y la procedencia de una vez, por lo que reenviar una decisión al modelo la sobrescribiría y borraría quién la había tomado. Una regresión es el caso donde más importa: ya ha sido juzgada real una vez, ninguna pasada profunda puede silenciarla.

Y las refutaciones se registran solas, desde thot audit --deep como desde /audit deep: dos llamadas de modelo, pagadas una vez. Llevan el nombre del motor que decidió, nunca el tuyo — una decisión de máquina no se impone sobre una decisión humana.

Nada se elimina jamás en silencio. Un finding descartado permanece en el informe como refuted, con su razón y su autor — un audit que oculta lo que se le ha dicho que ignore no es releíble.

El núcleo Python

La idea maestra de Prime Agent, trasladada: en lugar de una llamada de herramienta por pregunta, el modelo escribe Python y sus variables sobreviven.

   › /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

El mapa del repositorio está disponible como objetos — files(), symbols(), find(), callers(), callees(), audit(), read(). Un bucle que cruza findings y llamantes cuesta una pasada de modelo; lo mismo en llamadas de herramientas cuesta una docena, cada una de las cuales vuelve a pagar la lectura de lo que el mapa ya sabía.

El núcleo nunca se ejecuta en el proceso de Thot. Un exec() en casa daría al código auditado la memoria de Thot, sus bases abiertas y sus descriptores de archivo. Por eso es un subproceso — y dentro del contenedor cuando hay un sandbox configurado.

Lo que protege, exactamente — y Thot lo corrigió sobre sí mismo después de que su propia pasada adversaria señalara una docstring demasiado absoluta:

Subproceso (local)

protege la memoria, las bases y los descriptores de Thot. No tus identificadores: el worker se ejecuta bajo tu cuenta y puede leer ~/.claude/.credentials.json.

Contenedor (docker)

frontera real: sin red, sin tu $HOME, repositorio en solo lectura.

Las variables de entorno sensibles se retiran antes del lanzamiento, y /py lo dice una vez en modo local en lugar de dejar que «proceso separado» se lea como una garantía que no ofrece.

rlm() — delegar desde una celda

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

Una celda puede descomponer su propio problema. La celda no posee ningún identificador: pide al anfitrión, que decide y paga. Los límites se mantienen, por tanto, del lado del anfitrión — 8 llamadas por celda, 40 por núcleo — porque un límite que el hijo pudiera modificar no es un límite, y el hijo ejecuta código venido del repositorio auditado.

Lo que Thot retiene de un repositorio

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

El refinamiento de Prime, aplicado al audit: hechos que ningún análisis estático derivará jamás. Viven en <repositorio>/.thot/harness.json, releídos en pull request como los veredictos, y vuelven al briefing en cada sesión.

Lo que el modelo tiene derecho a hacer

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

En sesión: /tools lectura. Releer un repositorio que no es el tuyo es leer código del que tienes toda razón para desconfiar — y que el modelo lo modifique rara vez es lo que querías.

La postura se sostiene en tres lugares, no uno: las herramientas propuestas al modelo, el momento en que llama a una de todos modos, y — en modo cuenta — el CLI oficial, al que --disallowed-tools prohíbe Write, Edit y Bash. Una postura que solo filtrara las herramientas de Thot sería una mentira donde más importa.

La cadena de suministro

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 ?

Los candados primero, siempre: uv.lock, poetry.lock, Pipfile.lock, package-lock.json, yarn.lock, pnpm-lock.yaml. Un manifiesto dice requests>=2 y OSV no sabe responder a un intervalo; un candado dice 2.31.0 y OSV sabe. Una dependencia que solo existe en intervalo no se adivina, se informa como no fijada.

Un aviso que cubre una versión exacta es un hecho, no una suposición — pero saber si tu código alcanza la función vulnerable no se analiza, así que estos hallazgos siguen siendo PLAUSIBLE y lo dicen. Solo MAL-* hace excepción: el paquete es la carga útil, la alcanzabilidad no es la cuestión.

Y la misma propiedad que en todas partes: la identidad de un hallazgo lleva la versión fijada, así que un bump hace expirar el veredicto. Descartar una CVE sobre requests==2.19.1 no descarta nada sobre 2.20.0.

OSV inalcanzable nunca se convierte en un certificado de buena salud: thot deps dice «no verificadas» y devuelve un código de error.

Ejecutar el código auditado sin ejecutarlo en tu máquina

pytest sobre un repositorio auditado es el código de ese repositorio el que corre bajo tu cuenta. Es el único lugar donde toda la concepción se filtra.

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

Por defecto, en el contenedor:

Red

cortada (--network none)

Repositorio

montado en solo lectura, copia escribible en tmpfs

Privilegios

--cap-drop ALL, no-new-privileges, usuario 65534

Límites

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

La red cortada es la bandera que más vale y la que más estorba: por eso es una bandera y no una ley (--network).

Una regla invierte la del resto de Thot: en todas partes, una dependencia que falta cuesta su funcionalidad y el trabajo continúa. Aquí, un sandbox solicitado e indisponible se niega a ejecutar. Caer silenciosamente en el host convertiría una protección en una mentira.

Compartir las decisiones

Un veredicto es un hecho sobre esta revisión de este código. Viaja, pues, con el código: <repositorio>/.thot/verdicts.json, releído en la pull request que toca el código concernido, y presente en un clon nuevo incluso antes de la red.

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

La memoria es común a los árboles, el archivo no lo es: publicar una decisión que versa sobre otro repositorio es rechazado, y --share-all solo toma aquellas cuyo archivo existe aquí.

La cadena por defecto, sin configuración alguna: el repositorio primero, tu máquina después. Una decisión revisada prevalece sobre una nota que te has hecho a ti mismo.

La escritura, en cambio, sigue siendo local. Una herramienta que modificara un archivo versionado a cada /verdict produciría diffs que nadie ha pedido: decides en local, publicas a propósito.

Un servidor compartido, o un mem0 existente

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

El backend mem0 habla el contrato autoalojado exactamente como el cliente de Hermes Agent: un servidor ya en marcha para Hermes sirve a Thot sin cambiar nada.

Un almacén remoto inalcanzable cuesta la memoria de las decisiones pasadas, nunca la auditoría — pero no lo hace en silencio: thot verdicts --where dice cuál está mudo y por qué.

Recibir las auditorías en otro lugar

Una auditoría que termina a las 03:00 no vale nada mientras nadie esté avisado, y la persona a avisar no está delante del terminal.

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

Canal

Saliente

Entrante

Telegram

✓

✓ (long polling — ningún puerto que abrir)

Discord · Slack

✓ (webhook)

—

ntfy

✓

— (sin identidad: el asunto basta para publicar)

Correo

✓ (SMTP)

—

Las notificaciones no piden ningún demonio: el plugin gateway-notify se dispara en post_audit, y solo para una auditoría no supervisada. Una auditoría lanzada a mano ya se muestra en pantalla; notificar cada vez enseña al destinatario a cortar el canal, lo que cuesta el único mensaje que importaba. Nada nuevo: silencio.

Lo que permite un token robado

El demonio solo existe para el retorno, y su concepción se sostiene sobre todo en eso:

  • el conjunto de comandos es cerrado — status, audit, findings, verdict, help. Sin shell, sin escritura, sin ruta arbitraria;

  • una auditoría solo puede apuntar a un repositorio ya declarado por thot schedule add;

  • la entrada exige una lista de autorización. Hermes propone un ALLOW_ALL_USERS para el desarrollo; Thot no tiene equivalente. Sin lista, el canal es solo saliente, y thot serve lo dice.

~/.thot/gateway.json está escrito en 0600 — contiene tokens de bot y una contraseña SMTP. Las variables de entorno lo sobrescriben campo por campo, bajo los nombres de Hermes.

Auditorías programadas

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 escribe la unidad launchd (macOS) o te da la línea de crontab, y te deja activarla tú mismo — una herramienta que instala tareas de fondo en silencio es una herramienta en la que se deja de creer.

Cuando launchd no puede. En macOS, los permisos se conceden por binario: un agente launchd puede ver denegados ~/Desktop, ~/Documents o ~/Downloads, y entonces la unidad se bloquea al arrancar el intérprete sin escribir una línea. Un proceso lanzado desde tu sesión conserva el acceso de esa sesión, incluso una vez huérfano — es el tercer remedio, y no pide nada al sistema:

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

Se retira ante cualquier unidad launchd que launchctl declare haber ejecutado realmente: dos planificadores sobre un mismo trabajo son dos veces el trabajo y dos veces los tokens. thot doctor dice cuál de los dos sirve.

Una auditoría programada no dice nada mientras no haya nada nuevo. Un informe nocturno que repite los mismos trescientos hallazgos acaba en una carpeta que nadie abre. Lo que sube es el diff: lo que ha aparecido desde la última vez, por encima del umbral, menos lo que ya se ha juzgado sin interés.

Plugins

Cinco hooks, cada uno porque algo entregado lo usa:

Hook

Cuándo

on_finding

antes del informe, para anotar

post_audit

auditoría terminada — notificar, exportar, archivar

pre_write

antes de una escritura del agente — devuelve una advertencia

post_write

después de una escritura exitosa

on_verdict

una decisión acaba de registrarse

Un plugin es una carpeta con plugin.yaml y __init__.py, en ~/.thot/plugins/ o <repo>/.thot/plugins/ — la forma usada por Hermes Agent. Un plugin que se cae cuesta su propia funcionalidad y nada más: su error se registra y se muestra con /plugins.

Los del repositorio auditado no se ejecutan sin tu acuerdo. Cargar un plugin es lanzar su código aquí, bajo tu cuenta — y el repositorio auditado es precisamente aquel del que Thot desconfía. Sus plugins se nombran, pues, nunca se importan, mientras no los hayas aprobado:

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

La aprobación recae sobre el contenido, no sobre el nombre: Thot registra una huella de la carpeta, y la menor modificación la revoca diciéndolo.

Tres se entregan:

Plugin

Qué hace

write-guard

relee lo que el modelo escribe y hace subir una advertencia si aparece un patrón peligroso. No bloqueante — un falso positivo que bloquea una sesión es peor que la escritura.

regression-alert

un defecto marcado fixed que reaparece pasa a CRITICAL: una regresión vale más que un candidato nuevo.

audit-log

un registro JSONL local de cada auditoría, veredicto y escritura, en ~/.thot/journal.jsonl. Sin red.

Verificar que todo está ahí

«Funciona» es una afirmación, y sobre un programa hecho de tres programas, no es una afirmación para creer a ciegas — sobre todo viniendo de la propia herramienta.

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

Instantánea fechada, no un contrato: mémoire cuenta los veredictos acumulados y solo puede crecer, skills depende de lo que está instalado en la máquina. Lo estable es la forma — cada línea lleva un número medido, y 12/12.

La última línea es la salida real en la máquina de desarrollo, y se conserva tal cual: es lo que el control sirve para producir. El 1 passage(s) viene de launchctl mismo, y no es decorativo: un launchctl list muestra la unidad cargada, su LastExitStatus vale 0 y su registro no existe — tres señales que dicen «todo va bien» para una tarea que nunca ha arrancado. Nombrar la causa vale más que contar las líneas verdes.

Esta verificación ha tenido que corregirse a sí misma. Condenaba la tarea por la forma de una ruta — «el árbol está bajo ~/Desktop, así que launchd no podrá leerlo» — cuando los permisos de macOS se conceden por binario: el intérprete de la unidad leía el árbol que /bin/sh veía denegado. Una forma es una sospecha, una pasada es un hecho, y es el hecho lo que ahora interroga.

Y una verificación que ningún análisis estático habría podido hacer:

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

Las líneas de escritura son verdes aunque anuncian una capacidad incómoda: informan de lo que hay, no de lo que se quisiera. Dos de los tres agentes pueden escribir y ninguna bandera lo impide — medido pidiéndoles crear un archivo, y luego mirando el disco. Lo que no puede impedirse se hace imposible de pasar por alto: AuditResult.touched nombra lo que una pasada ha modificado, y el bucle nocturno lo grita en stderr.

Planta un archivo en una carpeta temporal y pide su contenido. Existe por un defecto real: Hermes no abría una ruta relativa a su carpeta de trabajo y respondía con una frase que se leía como una negativa. Un tercio del panel no podía verificar ninguna afirmación que descansara en un segundo archivo, y nada más que plantar un archivo lo habría mostrado.

Cada línea ejecuta una operación real e informa de lo que ha medido: no «skills: configurado» sino «91 cargadas, 0 rechazadas». El motor de tinte busca una ruta en una muestra de los dos lenguajes, el servidor MCP responde a su propio protocolo. Una verificación que no puede correr falla en lugar de pasar en silencio: una línea verde que quiere decir «no probado» es peor que una roja. Nada toca la red ni un modelo — thot doctor en un avión da la misma respuesta que en una oficina. Salida no nula en caso de fallo, para caber en un && o una CI.

La mejora permanente

Una auditoría que argumenta veinte candidatos y se detiene deja el resto sin juicio para siempre. Una pasada sin presupuesto sigue corriendo cuando vuelves a sentarte. thot improve es el punto intermedio: rondas acotadas, cada una escrita en disco, cada una retomando donde la anterior se detuvo.

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

La unidad escrita lleva su propio PATH. launchd da a un trabajo /usr/bin:/bin:/usr/sbin:/sbin, cron aún menos, y claude, hermes y node no están en ninguna de esas carpetas — viven bajo ~/.local/bin. Sin eso, la pasada nocturna no construía ningún motor, no juzgaba nada, y salía con el código 0: launchd registraba un éxito cada noche, indefinidamente. Un trabajo de este tipo que falla en silencio es indistinguible de un trabajo que funciona, así que una pasada profunda privada de agente sale ahora en error y lo dice.

La versión nocturna reporta lo que ha decidido, no lo que ha aparecido. La distinción importa: el mecanismo de informe de las auditorías programadas responde a «qué hay de nuevo por encima del umbral», que es la pregunta correcta para un barrido y la incorrecta para un juicio. Confirmar un MEDIUM ya presente en el informe es exactamente para lo que sirve el bucle — y eso no se lo habríamos reportado a nadie. Los archivos que una auditoría habría modificado se señalan en el mismo registro.

Un árbol que ya no tiene nada que juzgar cede su turno al siguiente. Medido sobre el corpus real: thot tiene una cola vacía y prime un solo candidato, por lo que un presupuesto de 20 por árbol gastaba 40 en árboles incapaces de utilizarlos mientras Hermes esperaba ciento cincuenta. Una ronda de 20 se convierte en una ronda de 20, 20 y 60.

Una tercera propiedad la hace converger rápido: los fallos se cuentan. Un candidato cuyo agente supera su plazo, o cuyo modelo se niega a comprometerse, conserva su severidad — por lo que se retoma en primer lugar en la ronda siguiente, y en la siguiente. Medido sobre un hallazgo en un archivo de 1 660 líneas: cuatro intentos en tres pasadas, tres de ellos chocando contra el mismo muro. Tras dos fallos, pasa al final de la cola: sigue siendo elegible, nunca prioritario. Un éxito borra el contador — un muro que era una tarde ocupada o una suscripción agotada no debe perseguir a un hallazgo para siempre.

Dos propiedades la hacen converger en lugar de dar vueltas: una refutación se memoriza, por lo que la siguiente selección la salta; una confirmación deliberadamente no se memoriza — un defecto real debe seguir apareciendo hasta que alguien lo corrija — por lo que el bucle lleva su propio conjunto de identificadores ya juzgados. Sin esto, cada ronda después de la primera gastaría todo su presupuesto en reargumentar lo que la primera acababa de confirmar.

Termina con lo que hay que hacer, antes de los totales:

À 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

Una refutación es administración; una confirmación es una noticia. Una refutación impugnada también: es el programa el que dice haberse recuperado antes de enterrar algo. Contarlas sin nombrarlas envía al lector a hacer grep en el registro — que es exactamente lo que ha ocurrido, cada vez, durante un día.

Nunca modifica código. «Mejora» significa aquí que el juicio del programa sobre sí mismo se vuelve más nítido y más barato: menos candidatos sin decisión, más decisiones en disco, cada una atribuible al agente que la ha tomado.

El termómetro, y el bucle que se sirve de él

Todo lo anterior mide a Thot con Thot. improve pregunta a un modelo si un hallazgo es real. evolve vigilaba provenance, un informe que el motor calcula sobre su propia salida. Ambos son circulares, y el círculo no tiene nada de académico: la pasada profunda pagó 638 juicios por 9 confirmaciones mientras una regla calificada con −100 % dormía — xml_unsafe_parse señalaba defusedxml, es decir, exactamente el remedio que su propio mensaje recomendaba. Nada en el programa podía verlo.

thot bench rompe el círculo. Mide a Thot contra código etiquetado como vulnerable o sano por otra persona, a partes iguales, con la clase de debilidad nombrada.

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

El corpus no está embebido — dieciocho mil archivos de terceros no tienen nada que hacer en este repositorio — por lo que la ruta siempre se indica, y cada suite se verifica contra la huella de su manifiesto. Un corpus cuyas etiquetas se han movido bajo una medición es peor que no tener corpus: todas las cifras desde entonces son falsas y nada lo dice.

La puntuación es la J de Youden, TPR − FPR. Cero es un lanzamiento de moneda, +100 es perfecto, y negativo significa que la regla está invertida. Precisión y recall no lo habrían dicho: una regla sin ningún verdadero positivo tiene una precisión indefinida, se muestra vacía y se lee como sin datos — es exactamente así como sobrevive una regla invertida. La J no tiene ese hueco.

Dos formas de hacer trampa, y ambas pierden. Encontrar menos — lo que hacía subir provenance — hace caer el TPR. Señalarlo todo da TPR 100 %, FPR 100 %, J cero: el corpus está equilibrado 50/50 precisamente para eso.

Estado medido, umbral por defecto, tres frameworks (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

«Antes» es el estado del programa en el momento en que el termómetro existió por primera vez. «Después» es el mismo corpus, el mismo umbral, el mismo comando. El hold-out lo confirma, y de tres maneras: solo django puntúa +55,5 %, fastapi +55,5 %, flask +55,5 %. Tres frameworks que no se parecen en nada, la misma cifra con una décima de precisión — las reglas funcionan sobre código que no sirvieron para escribir.

Por categoría, lo que el motor sabe hacer hoy. Treinta y dos de las treinta y cuatro categorías activas están exactamente en +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 %

Cero falsos positivos sobre los 9 150 casos sanos, y ninguna categoría negativa. El punto de partida era ssrf en −8,0 % y xxe en −100 %.

El techo, porque una cifra sin su máximo no significa nada

+55,5 % no es «la mitad del problema resuelto». De las 61 categorías, 27 no son alcanzables, y por dos razones distintas que ambas se han verificado caso por caso, con el archivo abierto.

Diez son código que otra categoría ya posee. El corpus escribe dos CWE diferentes en la misma línea: el_injection y ssti son la misma llamada Template(datos), argument_injection y genericcmdi el mismo os.system('echo ' + datos) que cmdi, unverified_signature el mismo verify=False que tlsverify, sensinlogs el mismo logging.info que loginjection, hardcoded_crypto_key la misma clave en duro que hardcodedcreds. Thot dispara sobre todos estos archivos — el informe lo dice, columna seen — y anuncia la clase que el código es realmente. Alcanzarlas exigiría nombrar dos clases para una regla, lo que report/cwe.py existe precisamente para rechazar.

Diecisiete no tienen ningún sink. authzfailure, idor, intoverflow, null_deref, privescalation, csrf, missingcritauthn, sessionfixation y las demás terminan todas en un return JsonResponse(...) sin una sola llamada peligrosa. El caso vulnerable de idor es una petición correctamente parametrizada; lo que falta es una llamada de autorización. clickjacking se gana señalando la ausencia de X-Frame-Options, que Django establece por middleware. weak_password_hash separa sin embargo perfectamente este corpus — cada caso vulnerable es hashlib.sha256, cada caso sano pbkdf2_hmac — pero ninguna de esas líneas dice que el valor sea una contraseña, y la regla dispararía sobre todas las sumas de verificación, todos los ETag y todos los HMAC de un repositorio real. Dos últimas se reabrieron con la idea de que quizá eran código después de todo, y cierran por una razón que merece la pena escribir. intoverflow fabrica su debilidad con ctypes.c_int32 — los enteros de Python no se desbordan, y una regla que dispare sobre ctypes sería una regla sobre BenchProctor. resourceexhaust es bytearray(int(datos)) sin límite frente a bytearray(min(…, 1024)), y el separador es un único token min; pero bytearray(x) significa dos cosas según si x es un entero o un búfer, Thot no infiere los tipos, y sobre hermes/ las 102 llamadas de esa familia son conversiones y no asignaciones. Son defectos de lógica o controles ausentes; ningún análisis de tinte los ve, sea cual sea la regla que se escriba.

Quedan pues 34 categorías ganables honestamente, es decir, +55,7 %. Están todas activas. A +55,5 %, es 99,6 % del máximo honesto.

Los 21 casos que aún faltan ya no son categorías sino casos fallidos dentro de dos de ellas, y se han contado uno a uno: 12 en ssrf tienen como fuente mq_client.get_message() o redis_client.get(), dos objetos _Stub() declarados en el app_runtime.py de BenchProctor — no nombran nada que exista fuera de este corpus, y un patrón escrito para ellos se pegaría al termómetro en lugar de detectar nada; 9 en pathtraver provienen de una variable de entorno, que la remisión de accesibilidad degrada a propósito, porque quien proporciona el entorno ya tiene el sistema de archivos de ese proceso.

Lo que produjo esos cuarenta y seis puntos, en el orden en que se midió:

cambio

J

partida

+9,4 %

las rutas web reconocidas como puntos de entrada

+9,6 %

el tinte sigue un valor dentro de un contenedor

+10,0 %

un ternario con ramas constantes no transporta nada

+12,4 %

un fullmatch con clase negada es una lista negra

incl.

guardas SSRF: lista blanca de hosts, rango de IP resuelto

+14,3 %

socket.create_connection reconocido como sink de red

+14,9 %

confinamiento de ruta y lista blanca nombrada

+15,5 %

mark_safe como sink, bleach como neutralización HTML

+16,3 %

doce reglas de patrón de una línea

+27,8 %

un patrón ya no paga la remisión de accesibilidad

+34,4 %

sink.redirect, y la prueba por host separada de la prueba por rango

+35,6 %

cuatro sinks de inyección: plantilla, XPath, LDAP, NoSQL

+40,8 %

cabeceras de respuesta, guarda positiva, neutralización CR/LF

+44,2 %

una celda de hoja de cálculo no es una línea de texto

+45,6 %

un archivo llamado secrets.txt, y la prueba por cifrado o hash

+46,6 %

el tinte atraviesa un cierre nonlocal

+47,7 %

Django escribe tres de sus fuentes en mayúsculas

+48,0 %

HTMLResponse declara HTML, html.escape lo protege

+49,0 %

una vista que devuelve una cadena devuelve HTML

+49,9 %

una prueba solo vale lo que prueba, y el identificador SQL citado

+50,0 %

un programa escrito en una cadena sigue siendo un programa

+50,4 %

los parámetros de una ruta provienen de la red

+50,4 %

la respuesta de un tercero no es el dato de este programa

+50,6 %

un archivo que el programa nombra secrets dice lo que contiene

+51,2 %

lo que la base devuelve es lo que alguien puso en ella

+53,9 %

una línea de registro termina con un salto de línea, como una cabecera

+55,5 %

una ruta que devuelve la página construida una línea más arriba

+55,5 %

Dos de estos cambios se habían rechazado primero tras la medición, y fue el termómetro quien los rechazó: clickjacking, que costaba 23 falsos positivos en este repositorio, y weak_password_hash, que separa sin embargo perfectamente este corpus — cada caso vulnerable es hashlib.sha256, cada caso sano pbkdf2_hmac. Siguen rechazados, por la razón dicha más arriba: nada en esas líneas dice que se trate de una contraseña, y la regla dispararía sobre cada suma de verificación de un repositorio real.

Un tercer rechazo ha sido levantado, y es el más instructivo del lote. Tratar una lectura en base de datos como una fuente no confiable había sido medido dos veces y rechazado dos veces: 436 casos vulnerables contra 343 sanos la primera vez, 11 contra 13 cuando se restringió al XSS almacenado — donde la mitad sana lee de la base más a menudo que la otra. Las dos lecturas eran correctas sobre el programa de entonces. Lo que ha cambiado no es el corpus: son las defensas de la mitad sana, que ahora se entienden una por una — el cifrado, los escapes HTML y el renderizado autoescapado, la neutralización de CR/LF, la lista blanca de hosts y el rango de IP resuelto, la guardia positiva, el identificador SQL citado. Un caso sano que lee una línea y luego la cifra queda exonerado por el cifrado, y no por el hecho de que nadie haya visto la línea. La misma regla que costaba 343 invenciones cuesta cero, y aporta 2,7 puntos en dieciséis categorías a la vez. Un rechazo medido solo es verdadero desde el día en que se midió.

Las cinco reglas de inyección LDAP / XPath / NoSQL / SSTI / EL también habían sido rechazadas, con una J medida en 0,000: afectaban tanto a la mitad sana como a la otra. Cuatro de ellas están hoy en +100,0 % sin un solo falso positivo, y no es el veredicto el que ha cambiado de opinión — es lo que se les ha dado a leer. Cada una ya solo mira el argumento que porta la inyección (el filtro LDAP es el tercero, la fuente de la plantilla es el primero), y sink.nosql pasa por una barrera sobre el texto de la consulta en lugar de sobre la importación, porque .find( pertenece a todas las cadenas de Python. La quinta, el_injection, sigue rechazada: es la misma llamada Template(…) que ssti, etiquetada con otra clase, y darle ese nombre sería pagar un punto por una inexactitud.

La precisión no se compra con un punto ciego

Cada guardia reconocida anteriormente se propuso primero en una versión que hacía subir la puntuación y abría un agujero explotable. Una sonda adversaria encontró todos los agujeros:

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

La guardia es una defensa SSRF correcta y no dice nada sobre los metacaracteres de shell que aún están en la cadena. Purgar la contaminación en todas partes suponía 174 falsos positivos menos y un punto ciego en inyección de comandos.

De ahí la distinción que aplica el motor: una guardia que restringe el valor (lista blanca literal, fullmatch enumerador) lo blanquea para todo; una guardia que prueba el destino (el host está autorizado, la dirección resuelta es pública, la ruta está confinada) solo vale para la familia de sinks en cuestión. Lo mismo con bleach.clean, que neutraliza HTML y deja x; rm -rf / intacto.

Cuatro sondas adversarias están en la suite de pruebas y todas señalan correctamente. Una «mejora» futura que reabriera uno de estos agujeros rompe una prueba con nombre.

El silencio restante tiene dos causas, y thot bench las separa porque son dos trabajos diferentes:

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

La distinción decide el orden de los objetivos — sin ella las categorías mudas están en empate perfecto en J = 0 y la ordenación recae en el orden alfabético.

La fusión, allí donde cambia algo

Cascade.turn elige un agente y lo llama; solo va a buscar al otro si el primero devuelve un error. Un turno así está limitado al mejor de los dos por construcción: puede perder menos, nunca ganar más. agent_apply hacía lo mismo — un motor, en singular.

thot evolve --fused hace trabajar a los dos, sobre mitades diferentes del mismo problema:

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 lee la medida y escribe una especificación. No toca ningún archivo. Su salida es una afirmación sobre la causa: qué regla, qué línea, por qué esos casos.

  • Prime lee la especificación y escribe el código. Se le dice explícitamente que puede rechazarla si el código la contradice — un ejecutor incapaz de decir que no es un relevo, y un relevo no añade nada.

  • Ninguno de los dos decide. La suite de pruebas es un suelo, el corpus etiquetado es el veredicto. Una especificación falsa aplicada con convicción hace bajar la nota y se anula byte a byte.

El orden tampoco es arbitrario. Diseñar y luego construir se verifica en la unión: Prime ve el razonamiento de Hermes antes de comprometerse con él. Construir y luego releer no lo permite — cuando el segundo mira, el primero ya ha decidido.

Los objetivos vienen de la medición, no de una frase tecleada. Hasta ahora el bucle solo podía perseguir lo que un humano ya sospechaba; un objetivo construido a partir de la puntuación es el programa que dice dónde es débil, en cifras que no ha elegido — y las mismas cifras dicen después si la respuesta ha servido. Cada objetivo lleva los archivos que han fallado: un agente al que se le dice «xss está al 0 %» solo puede adivinar, un agente al que se le dan tres archivos fallidos tiene un problema que resolver.

El sobreajuste, y lo que --hold-out puede hacer realmente

Un bucle evaluado sobre un corpus tiene una única forma real de hacer trampa: aprender el corpus. Una regla ajustada a lo que parece BenchmarkTest01126 hace subir la nota y no le sirve a nadie, y visto desde fuera es indistinguible de un progreso real.

--hold-out flask saca una suite de la cifra principal y la guarda como segundo salvaguarda: un cambio que mueve las suites contra las que se ha optimizado y no la que nunca ha visto ha dicho lo que es. Los dos números se mantienen con ne_baisse_pas.

Su límite, medido: los tres frameworks puntúan a medio punto unos de otros. Eso atrapa el sobreajuste al archivo, no el sobreajuste a la forma del benchmark — un corpus generado sigue siendo un corpus generado, y una regla que solo ayuda con código de demostración pasaría las tres. El hold-out hace visible una trampa; no hace que el corpus sea representativo.

Lo que el bucle retiene de una ronda a otra está escrito en ~/.thot/evolve-log.jsonl. Sin eso, como la medición apenas se mueve en una ronda, la ronda siguiente vuelve a leer las mismas peores categorías, ofrece los mismos archivos, y recibe —razonablemente— la misma especificación ya construida, medida y anulada: --rounds 5 sería un intento probado cinco veces, cinco veces más caro, con el aire de estar ocupado.

No es un oráculo. Un parche puede ser verde, hacer subir la J, y seguir siendo malo — es sobreajuste, y la literatura sobre la reparación automática de programas no habla de otra cosa. El corpus es una prueba de que se ha progresado sobre él. El bucle informa de lo que ha cambiado para que un humano pueda estar en desacuerdo.

Habilidades — los métodos que Thot conoce

Una habilidad es un método escrito una vez: un SKILL.md con frontmatter YAML. Es el formato de Hermes Agent y de Prime Agent, por lo que una habilidad escrita para uno de los dos se carga aquí sin modificación, y lo contrario también es cierto.

Thot incluye la biblioteca completa de Hermes Agent (MIT — ver NOTICE.md): 90 métodos cargados, 117 más disponibles.

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

Categorías cargadas: audit, security, software-development, github, devops, research, mlops, productivity, creative, apple, email, media, note-taking, smart-home, social-media, autonomous-ai-agents.

El modelo las descubre con la herramienta skills — que responde con un índice de nombres mientras no se le dé una palabra clave, porque doscientas descripciones no son un catálogo — y lee la que se aplica con skill. En sesión, /skills te muestra lo mismo.

Un método importado que cita una herramienta ausente aquí (delegate_task, browser_navigate…) se sirve tal cual, con una nota que dice cuáles faltan y qué usar en su lugar. El enfoque se traslada incluso cuando la llamada a la herramienta no se traslada.

Añadir más

~/.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.

Las dos disposiciones se aceptan: una carpeta plana (Prime Agent) o agrupada por categorías (Hermes Agent). Un nombre que ya existe reemplaza la versión integrada — suficiente para adaptar un método entregado sin tener que hacerle un fork.

Un método proporcionado por el repositorio auditado se analiza primero

Un SKILL.md es texto entregado al modelo como instrucción. Los repositorios que Thot lee son, por definición, aquellos de los que nadie responde. Un repositorio hostil que coloque .thot/skills/x/SKILL.md escribiría parte del briefing.

La guardia de Hermes Agent se traslada aquí y se aplica a todo lo que viene del repositorio: inyección, exfiltración, persistencia, ofuscación.

   ▲ 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> plantea la misma pregunta bajo demanda. Lo que Thot entrega él mismo no se analiza: está en disco porque el programa está instalado, no porque un repositorio lo haya pedido.

Comandos personalizados

Un archivo markdown es un comando. La gramática es la de Prime Agent, Claude Code y Codex — nada nuevo que aprender.

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

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

En .thot/commands/revue.md, esto crea /revue src/app.py. Sustituciones: $1, $2…, $@, $ARGUMENTS, ${@:2}, ${@:2:3}. Un argumento nunca se reinterpreta. Los comandos del repositorio pasan por la misma guardia que sus habilidades.

Tres se entregan: /triage (nombrar la entrada o clasificar sin seguimiento), /harden (prueba que falla primero, corrección después), /regress (el diff de la auditoría contra una referencia git).

Servidores MCP

El catálogo de Hermes Agent, veinte servidores verificados:

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

La instalación se delega en el CLI oficial, que ya posee OAuth y la renovación de tokens — Thot no tiene ninguna razón para tener un segundo cofre que pueda filtrarse. Dice explícitamente que registrado no es autorizado, y qué comando termina el trabajo.

Las herramientas del modelo

Las clásicas — leer, escribir, editar, lanzar un comando. Toda escritura y toda ejecución requieren confirmación, y eso no es configurable.

Y cuatro que solo pertenecen a Thot, gratuitos porque interrogan el mapa y no al modelo:

Herramienta

Respuesta

code_map

los archivos del proyecto

find_symbol

archivo, líneas y parámetros de una función

callers

quién llama a qué, y la distancia a un punto de entrada

audit

las rutas de contaminación source → sink

En modo cuenta, estos cuatro se sirven al CLI oficial mediante thot.mcp_server — un servidor MCP de solo lectura, incapaz de escribir o ejecutar nada.

Cuando el modelo busca quién llama a process_payment, interroga el grafo y obtiene la respuesta completa — en lugar de hacer grep a tres archivos al azar.

Modo solo auditoría

El núcleo de análisis también se usa sin modelo, sin red, sin costo:

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 — entrar en una cadena que ya existe

Un informe que ningún pipeline sabe leer vive en un solo terminal. GitHub code scanning, GitLab, Azure DevOps y los editores leen todos SARIF 2.1, y dos propiedades de Thot valen más allí que en cualquier otro sitio.

La identidad de un hallazgo es regla + archivo + símbolo + huella del cuerpo, nunca la línea — es exactamente lo que partialFingerprints pide. Un tablero alimentado con números de línea reabre todos sus tickets en cuanto alguien añade un import al principio de un archivo; alimentado con esto, no.

Y una ruta de contaminación es una secuencia de posiciones, que es lo que codeFlows representa: el lector hace clic de la fuente al sink en lugar de creer a la herramienta bajo palabra. Un hallazgo sin ruta no lleva ninguna clave codeFlows — un flujo vacío se presenta como una ruta de contaminación sin pasos, lo que se lee como un análisis roto y no como una coincidencia de patrón.

Un hallazgo refutado por el panel no se elimina: se va con una suppressions justificada. Un tablero que nunca lo ve no puede distinguir «nadie ha mirado» de «alguien ha mirado y ha decidido», y lo segundo es toda la razón de ser del panel.

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

Análisis asistido — --deep

El análisis determinista responde a «¿estos datos podrían circular?». Es exhaustivo, gratuito, y no es la pregunta que se le paga a un auditor por resolver. --deep plantea la pregunta cara, solo sobre los candidatos que la han merecido:

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

Dos pasadas, deliberadamente adversarias:

  1. La sonda debe nombrar una entrada concreta que alcanza el punto peligroso. Nada de generalidades sobre la clase de vulnerabilidad — una URL, un valor, un efecto.

  2. La refutación recibe ese escenario con la única misión de destruirlo: validación previa, llamador que solo pasa constantes, tipo que prohíbe la entrada supuesta. En caso de duda, refuta.

Un finding solo sobrevive si una segunda lectura hostil del mismo código no logra matarlo. confirmed entonces quiere decir algo.

En sesión, lo mismo: /audit deep.

El motor se elige automáticamente — tu cuenta de Claude vía el CLI oficial si está conectada (los análisis se ejecutan en paralelo, con tu suscripción), una clave API en caso contrario.

Lo que el audit no debe leer

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

Las exclusiones integradas cubren lo que todo repositorio tiene — node_modules, build, .venv. .thotignore cubre lo que solo ese repositorio sabe: documentación embebida, un cliente generado, una carpeta de fixtures rotas a propósito. Auditarlas no produce findings, produce ruido en el lugar exacto donde estarían los findings.

Tus propias reglas

El catálogo integrado conoce la biblioteca estándar. No conoce el wrapper que tu equipo escribió alrededor de subprocess, la cola que tu servicio consume, ni el validador que vuelve seguro un valor en tu caso. Sin un lugar donde decirlo, cada audit de un sistema real se equivoca en los mismos tres sitios.

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

El mismo archivo lleva las reglas de JavaScript, bajo una clave js: — un wrapper de equipo existe en general en ambos lenguajes, y separar la declaración es la forma en que una mitad queda obsoleta.

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]

Lo que el modelo pide es una entrada no confiable

Las fuentes son expresiones — sys.argv, os.environ. Eso cubre un programa que se lanza y se pierde un programa al que se llama: la herramienta de un agente recibe su entrada no confiable en parámetros nombrados, rellenados por un registro a partir de lo que un modelo ha pedido, y ninguna expresión aparece en ninguna parte del cuerpo.

El costo medido de no modelar eso: cuatro SSRF en una tarde, todos alcanzados por un argumento de herramienta, ninguno encontrado por la mancha — lo fueron por reglas de patrón, que reconocen una forma y no prueban nada.

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

Vacío por defecto, y deliberadamente: qué funciones llama un registro es un hecho sobre un repositorio, y adivinarlo pondría una fuente bajo cada parámetro de cada programa. Medido en Hermes, los dos extremos: una regla que nombra los paquetes plugins y tools revela 19 caminos probados varios de ellos sobre-aproximados (el base_url que un helper recibe de la configuración no es no confiable); una regla que nombra el parámetro args revela cero, porque los manejadores de Hermes toman parámetros nombrados y no un diccionario. La regla correcta nombra los puntos de entrada reales — y les corresponde a sus autores conocerla.

Una regla que retoma un id integrado lo reemplaza — suficiente para degradar un sink que el equipo ha aceptado deliberadamente, sin parchear Thot. Un archivo mal formado detiene el audit nombrando el archivo y la clave culpable, en lugar de hacer creer en una ausencia de finding.

Las supresiones

Una supresión es la única afirmación sobre la seguridad que ninguna herramienta relee — incluida esta, por construcción. # nosec, # noqa: S310, // eslint-disable … security/…: es una afirmación sobre el código, escrita una vez, que sobrevive a los llamadores que describía.

Dos veces en un mismo audit, aquí, era falsa:

supresión

lo que afirmaba

lo que era verdad

# nosec B310 — scheme checked above

el esquema está controlado

detenía file:// y nada más — SSRF hacia el servicio de metadatos

# noqa: S310 (configured peers)

la URL viene de la configuración

uno de los llamadores la lee en un argumento de herramienta, por tanto del modelo

Thot las reporta por tanto como clase, en LOW, con el patrón escrito al lado. El finding no dice «esta línea es peligrosa»: dice «nadie ha releído la razón por la que fue excusada». En una pasada --deep, es un agente el que va a verificar si el patrón sigue en pie.

Para Python, se leen los tokens de comentario reales, no patrones — una expresión regular no distingue # nosec en un comentario del mismo texto citado en una docstring, y la docstring de este módulo cita dos.

Una supresión colocada en una línea que este audit señala no es el mismo objeto: es una afirmación que contradice un finding vivo, escrita por alguien que ha leído la misma línea y ha concluido otra cosa. Se eleva un nivel y lo dice. Medido en Hermes: 7 de 45 — y tres de las supresiones leídas ese día eran falsas.

Medido: 0 en Thot, 0 en Prime, 45 en Hermes.

Calibración

La precisión cuenta tanto como la detección. Deliberadamente no se reportan:

  • subprocess.run(cmd) sin shell=True — ningún shell lee el comando.

  • cursor.execute("… ?", params) — consulta literal, parámetros vinculados.

  • Un valor pasado por int(), shlex.quote(), os.path.basename(), html.escape() — esas llamadas rompen la cadena de contaminación.

  • Un defecto al que ningún punto de entrada alcanza se degrada automáticamente — pero solo si se han encontrado puntos de entrada. Sin ninguno, el alcance es desconocido, no nulo, y nada se entierra sobre esa ignorancia.

  • payload.get(...) no es requests.get(...).

Orden de magnitud, medido en Hermes Agent (4 457 archivos Python): 98 s, 365 findings de los cuales 25 high — 3 por encima del umbral por defecto una vez aplicada la memoria.

Qué parámetro llena el argumento

Un helper del que un parámetro alcanza un sink no vuelve peligroso todo lo que se le pasa. El motor emparejaba sin embargo al llamador con el conjunto de sinks del callee, sin mirar dónde aterrizaba el argumento:

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 posición se lee ahora en el sitio de llamada, y el nombre para un argumento nombrado. Es lo que el motor de JavaScript ya hacía por su lado.

Medido, una vez posible la ejecución: de 83 238 resoluciones solicitadas al analizar Thot y hermes/tools, el 90 % designa un parámetro único y el 9,8 % no designa ninguno; el 0,1 % restante queda abierto (f(*rest), f(**options)) y mantiene la respuesta amplia. El callee ofrecía 2,68 parámetros de media, por tanto cerca de dos tercios del espacio de búsqueda desaparecen — el análisis de hermes/tools pasa de 11,4 s a 9,5 s.

En cuanto al número de findings, en cambio: ningún cambio. Thot, prime y hermes/tools devuelven exactamente los mismos 151 candidatos antes y después, de los cuales cero inventados — la propiedad de seguridad se sostiene sobre el terreno. Cuatro caminos de mancha se acortan, nada más. La forma corregida arriba es real y el test lo prueba, pero no se produce en ninguno de los tres árboles medidos: este correctivo compra velocidad y caminos más justos, no menos ruido.

Dos formas no dicen nada: helper(*args) extiende un número desconocido de valores, y helper(**options) no nombra ninguno. Ahí, el motor mantiene la respuesta amplia que daba antes — el conjunto retenido está siempre incluido en el de antes, por tanto afinar solo puede quitar un finding, nunca inventar uno.

El receptor se omite basándose en la firma del callee, no en la sintaxis de la llamada: Runner().go(x) se resuelve en un go sin punto, y Cls.m(obj, x) igual que obj.m(x) son ambos un atributo. Como un método casi siempre se llama enlazado, es self al frente de los parámetros lo que decide — la llamada no enlazada es la única forma leída un nivel demasiado corto.

Lo que el grafo no puede seguir

Un defecto alcanzado por un camino que el análisis no resuelve — un handler guardado en una tabla de dispatch, una vista decorada, una llamada sobre una variable cuyo tipo es desconocido — no es un defecto inalcanzable. Thot distingue los dos:

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

En los tres casos el alcance es desconocido, no nulo, y el finding mantiene una penalización ligera en lugar de ser enterrado. En Hermes: mismos 365 findings, pero 60 suben un nivel. Una función que nadie llama y que nadie menciona permanece, por su parte, correctamente devaluada — si no, el filtro dejaría de ser un filtro.

Límites

La mancha a través de los archivos está reservada a Python: JavaScript y TypeScript están indexados, seguidos dentro del cuerpo de una función y hasta un helper del mismo archivo, y se detienen ahí por falta de grafo de llamadas resuelto — la tabla de arriba lo dice línea por línea. Las reglas por patrón, por su parte, se aplican en todas partes.

El +55,5 % es una cifra de Python. BenchProctor etiqueta django, fastapi y flask, y nada más; el catálogo de JavaScript es más estrecho que su homólogo — sin fuente «lectura en base», sin sink de registro — y sus reglas se apoyan en sus tests en lugar de en 18 300 casos etiquetados. Cuando una corrección vale para ambos motores se traslada a ambos, y el diario dice cuál se ha medido dónde.

sink.log lee el mensaje y solo él. logger.info('failed: %s', exc) confía el valor a la maquinaria de registro en lugar de escribirlo en la cadena que el autor ha tecleado; leer también esos argumentos vale +0,0000 en el corpus y 4 844 findings más en hermes/, uno por cada except … as exc: del árbol. No está cubierto, y decirlo cuesta menos que dejar que se crea.

Sin --deep, cada finding es PLAUSIBLE: detectado estáticamente, no aún probado por ejecución. Con --deep, un finding confirmed ha sobrevivido a una refutación adversaria — no es todavía una prueba de ejecución, que vendrá con el repro. Y la ausencia de finding no es una prueba de ausencia de defecto: dispatch dinámico, reflexión y metaprogramación escapan al análisis.

La búsqueda de texto completo en las sesiones deja de ser de costo constante en los SQLite de los que fts5 no sabe remontar los rowid: el motor ahí ordena el conjunto de coincidencias en lugar de cortar en veinte. Medido en el que incorpora CPython 3.12 — 6 588 instrucciones contra 27 443 cuando el corpus se multiplica por cinco, donde un motor capaz permanece en 2 215 y luego 2 698. Un test lo verifica y nombra la versión cuando no lo permite.

Desarrollo

cd Thot
uv run pytest -q

Instalado en modo editable: el código fuente es la referencia inmediatamente. En cambio, si pyproject.toml cambia (nueva dependencia), hay que relanzar uv tool install --editable --from . thot --force.

El núcleo determinista (codemap, taint, scope, scoring, store, report) no depende de ningún agente y no toca la red — un test lo verifica y hace fallar la suite si eso cambia.

Spec y planes: 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