Saltar al contenido principal

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.

nota

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

tip

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

aviso

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