Privacy
iLAB Memory tiene un contrato de privacidad innegociable: cualquier texto dentro de <private>...</private> se reemplaza por [REDACTED] antes de que el content sea hasheado, deduped o escrito a disco.
Eso significa que podés incluir API keys, PII u otro material sensible en content — envolvelo en <private> y el secreto nunca llega al store de SQLite.
Esto es Braess #1, el invariante más rigurosamente protegido de la librería. Tests a nivel AST aseguran que compute_normalized_hash solo se llama sobre el output de strip_private. El pipeline está enforzado dentro de upsert_observation, el único call site para todo mem_save / mem_session_summary.
El pipeline
1. El caller pasa content crudo
Tu código llama mem_save(..., content="...sensible..."). El string crudo incluye una o más regiones <private>...</private>.
2. strip_private reemplaza las regiones tagueadas
strip_private(content) sustituye cada región <private>...</private> por el string literal [REDACTED]. Case-insensitive, soporta atributos en el opener, maneja tags anidadas vía un loop de fixed-point.
3. Hash sobre el content stripeado
normalized_hash = sha256(stripped_content). El input crudo nunca se le pasa a la función de hash. Dos saves cuya única diferencia es un bloque <private> van a producir el mismo hash y a deduplicar correctamente.
4. Persistir el content stripeado
La fila Observation que queda en disco tiene el content y title strippeados. No hay path que escriba el input crudo.
Ejemplo
from ilab_memory import ILabMemory
mem = ILabMemory.from_path("./mem.db")
result = mem.mem_save(
user_id="alice",
type="preference",
title="Saludo preferido",
content="Alice prefiere que la saluden como 'Ali'. <private>API key: sk-ABC123</private>",
topic_key="user/alice/greeting",
)
obs = mem.mem_get_observation(user_id="alice", observation_id=result.id)
print(obs.content)
# → "Alice prefiere que la saluden como 'Ali'. [REDACTED]"
El string sk-ABC123 desapareció — no solo de la response de lectura, sino del disco. Abrí el archivo SQLite en cualquier inspector y solo vas a encontrar [REDACTED].
Defensa en profundidad. Envolvé cualquier content sensible en <private> aunque pienses que es seguro. El costo son unos caracteres; el beneficio es que un print(obs.content) descuidado no puede filtrar un secreto.
Qué matchea y qué no strip_private
Matchea (redactado)
<private>secret</private><PRIVATE>SECRET</PRIVATE>(case-insensitive)<private foo="bar">attributed</private><private>multi\nline\nbody</private>(DOTALL)- Tags anidadas:
<private>outer <private>inner</private> outer</private>(colapsadas a un solo[REDACTED]) - Openers no cerrados:
<private>oops…redacta todo desde ese opener hasta el final del string (default strict-privacy).
NO matchea (queda como está)
<private-section>— nombre de tag distinto (el guión rompe el patrón del opener).<private matters>— sin=en el área de atributos, así que no se trata como opener atribuido.- Un
</private>aislado sin opener previo — queda como texto literal.
Dónde ocurre el strip
El strip corre en el borde de la librería — dentro de upsert_observation, que es la única función por la que pasa todo path de save. Tanto mem_save como mem_session_summary rutean por ahí. No hay path que se saltee el strip.
Las capas API HTTP y MCP serializan la observation post-strip. Las responses de red, los logs de la API y los archivos de DB ven solo [REDACTED].
Límites a tener en cuenta
El strip es basado en regex sobre la sintaxis literal del tag <private> / </private>. No detecta PII semánticamente.
- SSNs, tarjetas, emails fuera de bloques
<private>se guardan tal cual. - Sintaxis de tag alternativa (comentarios HTML, custom tags, campos JSON llamados "secret") no se reconoce.
- Ataques de tag-bombing (anidamiento profundo malformado) están acotados — el loop de fixed-point capea en 64 iteraciones y la Phase 2 redacta desde el primer opener residual hasta el final del string. La privacidad estricta gana sobre la preservación.
v0.2 puede agregar detección estructurada de PII. Para v0.1, envolvé deliberadamente.
Configurabilidad — ninguna, a propósito
El token de reemplazo es [REDACTED], punto. Es una constante a nivel de módulo sin override en runtime. ¿Por qué?
- Determinismo: el hash de dedup incluye
[REDACTED]. Dejar que los callers cambien el token rompería el dedup cross-process. - Auditoría: un token fijo hace que las regiones redactadas sean trivialmente grepable en dumps.
- Seguridad: una perilla menos = un foot-gun menos.
Siguiente
- Topic keys — por qué hash-dedup necesita que el strip pase primero.
- Architecture / Braess #1 — el test a nivel AST que enforza el invariante.