CrossSessionMemoryGuard: vigilando la exfiltración de memoria cross-session en agentes multi-tenant
TL;DR Los agentes con memoria persistente compartida entre usuarios/sesiones pueden filtrar datos de un principal a otro sin que nadie lo observe. CrossSessionMemoryGuard es un sensor read-only que detecta ese flujo no autorizado con tres señales (procedencia, contenido, grafo escritura/lectura), nunca bloquea nada, y documenta sus propias limitaciones con evidencia raw versionada en el repo — incluida la que todavía no sabe resolver.
El problema: la confidencialidad read-time no está vigilada
La memoria persistente de los agentes (Claude Cowork, agentes con memoria compartida entre usuarios) se defiende hoy sobre todo del lado de la escritura: contra el envenenamiento y la manipulación. Pero hay una pregunta anterior que casi nadie monitoriza: ¿debería ESTE dato SALIR hacia ESTE principal?
Un trabajo reciente demostró el vector en la práctica: un ataque de extracción de memoria persistente contra agentes que operan aislados por sesión (arXiv 2607.23444 — Isolated but Exposed: Persistence-Based Memory Extraction Attack on LLM Agents). Aislamiento por sesión no es lo mismo que aislamiento de datos: si el motor de recuperación cruza tenants (un filtro roto, una consolidación agresiva, un relabeling), una sesión puede leer silenciosamente lo que otra escribió.
El hueco de mercado, con evidencia verificable
La señal más directa: en GitHub, la búsqueda de repos con los cinco términos exactos que describen este problema devuelve 0 resultados — mientras los controles (agent persistent memory: 4.632 repos, topic:llm-memory: 435) muestran un espacio enorme y activo. El JSON crudo de esas llamadas está versionado como prueba, no como afirmación: github_gap_2026-08-17.txt.
Los proyectos adyacentes (OWASP Agent Memory Guard, memlineage, dent8) cubren el lado write: integridad y envenenamiento. El lado read de la confidencialidad cross-principal queda descubierto.
Qué hace el sensor
Read-only por diseño (Constitución del repo): observa, compara, alerta — nunca bloquea, modifica ni participa en la autorización. Fuera del camino crítico, fail-open estructural, con kill-switch (CSMG_DISABLED=1). Los eventos nunca llevan el contenido íntegro: solo hash SHA-256 + un span mínimo.
Tres señales sobre los chunks que el motor expone a un principal:
| Señal | Qué compara | Qué detecta |
|---|---|---|
| (a) mismatch | procedencia resuelta del chunk vs. principal observador | filas de otro principal servidas por el retriever |
| (b) similarity | contenido vs. referencias de OTROS principales (umbral 0.75) | contenido ajeno legible, incluido relabeling |
| (c) flowgraph | grafo escritura→lectura rehidratado por capa esquema | lecturas sobre chunks escritos por otro principal |
Detalle clave: la detección observa el path real de recuperación del agente, y la atribución (quién escribió qué) sale de la capa esquema — nunca de la vista observada, porque un retriever que cruza no debe reetiquetar la propiedad (el benchmark demostró que esa contaminación disparaba falsos en masa; KI-8 en el repo).
Limitaciones actuales
Esta sección es lo más importante del artículo. El proyecto todavía NO tiene release etiquetado, y por una razón concreta: el backend principal tiene una limitación estructural que medimos, documentamos y no vamos a disimular.
KI-9/KI-10 — la vía real de Engram no permite enumerar. El adaptador de Engram (el motor de memoria con el que el propio proyecto hace dogfooding) observa la capa de esquema, no la vía real de búsqueda, porque verificamos empíricamente que engram search no puede listar "todo lo visible":
- query vacía → error (
search query is required); comodines y escapes FTS (*,*:*) → 0 resultados; -
cap duro de 20 resultados por query —
--limit 1000y hasta--limit 5000siguen devolviendo 20 (default: 10); - la cobertura es función del conjunto de queries: con 15 queries amplias cubrimos el 87,9% de las observaciones reales (80/91), con 23 el 96,7%, y solo se llega al 100% con queries hechas a medida de las filas que faltaban — es decir, hay que saber ya qué buscar.
Ese cap no es un artefacto de nuestra base de datos: una auditoría independiente lo reprodujo con el binario oficial del motor y 30 observaciones de prueba propias (guardó 30, el motor devolvió 20). Toda la evidencia está en engram_search_spike_2026-08-17.txt.
KI-11 — los adaptadores externos no tienen su propia capa de esquema. mem0, langmem, zep y letta implementan la lectura real pero no schema_chunks() (la atribución de verdad); si su filtro de tenant estuviera roto, atribución y referencias se contaminarían como el bug t1 que ya corregimos en SQLite/JSONL. Es trabajo futuro declarado, no un secreto de implementación.
Y una limitación de detección en el benchmark: los escenarios de colusión compuesta (fragmentos de un secreto bajo el umbral de similitud, t4) no se detectan en el MVP — se reportan como limitación declarada (AC7), no como pasada.
Para el mismo estándar: si tu herramienta no puede hacer algo, la confianza técnica se construye diciéndolo con números — no enterrándolo en un footnote.
Benchmark multi-tenant (reproducible)
Fixture determinista: 3 tenants × 12 filas, corpus por tenant, escenarios adversariales parametrizados (fuga en la capa del retriever, plantado, robo de etiqueta, colusión compuesta) + baseline limpio + duplicación legítima adversarial. Umbrales declarados antes de correr. Resultados (seeds 1-3):
- t1 (12 filas de otro tenant servidas por el retriever): 12/12 detectadas por las tres señales — ASR 0.0;
- t2 (fila plantada): 1/1 — ASR 0.0;
- corpus limpio (
correct): 0 eventos; - duplicación legítima (
benign): fp_rate 0.071 ≤ tolerancia declarada 0.30, medido como FP real, nunca escondido; - t4 (colusión): 0 eventos — limitación declarada (ver arriba).
Reproducible con python -m benchmark.runner (tabla de precision/recall por señal incluida en la salida). Los números salen de los mismos eventos raw que el runner, no de una tabla escrita a mano.
Contribuye
El proyecto quiere resolver sus propias limitaciones, y hay dos puntos de entrada concretos:
- Vía real de enumeración para Engram — diseñar un modo de listado en el motor (o demostrar que el actual se puede usar con muestra declarada) es el problema abierto más importante (KI-10).
-
Un backend de producción real para probar la señal (a) — integrar mem0/langmem/zep/letta con
schema_chunkspropio y llevarlos al benchmark.
Ambos caben en issues del repo — si tienes un caso multi-tenant en producción, tu experiencia es exactamente el test que falta.
Links
- Repo: amurlaniakea/cross-session-memory-guard (AGPL-3.0-or-later)
- Evidencia del gap: docs/evidence/github_gap_2026-08-17.txt
- Evidencia del spike de enumeración: docs/evidence/engram_search_spike_2026-08-17.txt
- Limitaciones y decisiones: KNOWN_ISSUES.md
- Paper ancla: arXiv 2607.23444
Licencia: AGPL-3.0-or-later · Post revisado por auditoría independiente antes de publicar: los números citados se re-ejecutan desde el propio repo, y las limitaciones son las del KNOWN_ISSUES.md — ni más, ni menos.
Top comments (0)