El modelo no necesita el email de tu cliente. Tus tools sí. | AI Agent Builder
← Volver al blog
  • seguridad
  • arquitectura

El modelo no necesita el email de tu cliente. Tus tools sí.

Synaptic Links10 min de lectura

Cada prompt que manda un agente es una transferencia de todo lo que contiene a un proveedor de modelos. Para un agente que habla con clientes, eso significa nombres, direcciones, teléfonos y números de tarjeta, en cada turno, hacia un tercero — y cuanto más útil es el agente, más hay. Proteger los datos personales dentro de un flujo con LLM es lo primero que pregunta un equipo de seguridad y, hecho de la manera obvia, lo primero que rompe al agente.

La manera obvia es la redacción. Reemplazás cada dirección por [redacted] antes de que la vea el modelo y el proveedor no se entera de nada. Pero un agente que confirma un pedido por mail tiene que poner una dirección en la llamada a una tool: la tool de correo recibe [redacted] como destinatario y la confirmación no llega a ningún lado. La dirección es necesaria en un solo punto del turno — dentro de la tool — y innecesaria en todos los demás. Un diseño que la trata igual en todas partes falla para un lado o para el otro.

Entonces el requisito tiene tres partes: el modelo ciego al valor, la tool capaz de usarlo y el cliente sin ver nunca nada que no sean sus propias palabras. Es también el problema al que apunta el caso del confused deputy desde el lado del ataque: lo que reciben las tools de un agente lo decide un modelo, y los datos cruzan ese límite en las dos direcciones.

El detector encuentra, la plataforma decide

Los datos personales los ubica un detector, y el detector es una elección: los patrones incorporados, o un analyzer de Presidio — el motor open source de PII de Microsoft — que la plataforma puede correr junto al runtime del agente o alcanzar en una dirección tuya. En cualquier caso el contrato es el mismo y deliberadamente angosto. Preguntamos dónde están los valores y nunca aceptamos texto reescrito a cambio. El analyzer responde con posiciones y tipos de entidad; qué se pone en su lugar lo decide la plataforma.

Esa división es una propiedad de seguridad, no de prolijidad. El texto que devuelve un servicio remoto e ingresa a un prompt convertiría al detector en una superficie de inyección — exactamente la clase de problema para la que existen los controles sobre los resultados de las tools. Como solo cruzan spans, se puede mejorar el detector, reemplazarlo o apuntarlo a tus propios recognizers sin cambiar nada de lo que llega al modelo.

Las entidades cubiertas son direcciones de email, teléfonos, números de tarjeta e identificadores nacionales, más nombres de persona cuando se usa el detector Presidio y la política los pide.

Un token es un hash con clave, no una máscara

Donde se encuentra un valor, la plataforma escribe un token en su lugar:

Dónde apareceQué hayQué lee el modelo
El mensaje del clienteMandá la confirmación a ana@example.comMandá la confirmación a [EMAIL_7f3a9c1b2d4e]
El mismo mensaje, más tardeOtra vez la dirección de Ana: ana@example.com…otra vez: [EMAIL_7f3a9c1b2d4e]
Un teléfono de contacto+54 11 5555 0134[PHONE_2c81e0d95a47]
Una tarjeta en una factura pegada4111 1111 1111 1111[CREDIT_CARD_9b04f6a1c3d8]

El sufijo son 48 bits de un HMAC-SHA-256 calculado sobre el tipo de entidad y el valor, con una clave derivada del secreto del servidor, el tenant y la sesión. De esa construcción salen tres propiedades, y cada una cumple una función.

Estable dentro de una sesión. La misma dirección es el mismo token en todos los turnos, así que el modelo puede saber que dos menciones son la misma persona, y un token escrito en una respuesta anterior coincide con el del historial de este turno. Razonar sobre "el cliente de antes" requiere identidad, no contenido.

Distinto entre sesiones y tenants. La clave cambia con ambos, así que un token copiado de una conversación no restituye nada en otra. Es un lugar más donde el aislamiento entre tenants trabaja sin hacer ruido.

No invertible. El valor no está en el token, y ninguna cantidad de tokens juntados lo revela. Con 48 bits, la probabilidad de que dos valores distintos de una misma sesión compartan token es aproximadamente n² ÷ 2⁴⁹: para mil valores distintos, unas dos en mil millones.

Un valor, un turno

El camino de una dirección de email en un turno: detectada y cambiada por un token antes del modelo, restituida solo para la tool que la tiene habilitada, tokenizada de nuevo en la respuesta de la tool y restituida en las palabras del cliente en la respuesta final
El camino de una dirección de email en un turno: detectada y cambiada por un token antes del modelo, restituida solo para la tool que la tiene habilitada, tokenizada de nuevo en la respuesta de la tool y restituida en las palabras del cliente en la respuesta final

Seguí la dirección de la primera fila de la tabla a lo largo de un turno completo.

  1. El cliente escribe. Su mensaje se guarda exactamente como lo tipeó — es su registro, y ve su propio texto en su propio historial.
  2. La dirección se cambia antes del modelo. El límite la detecta y la reemplaza por [EMAIL_7f3a9c1b2d4e], recordando el par en memoria durante el run. Desde acá el modelo lee el token y nada más.
  3. El modelo llama a la tool de correo. Le piden confirmar por mail y escribe la llamada con el token como destinatario. Hizo su trabajo sin haber tenido nunca la dirección.
  4. El valor se restituye solo si la tool lo tiene habilitado. Antes de que salga la llamada, la plataforma mira qué tipos de dato puede recibir esa tool. La de correo puede recibir direcciones de email, así que el token pasa a ser la dirección real, escapada correctamente para el campo donde cae, y la llamada sale. Una tool de tickets, en el mismo turno, a la que no se le habilitó nada recibe [EMAIL_7f3a9c1b2d4e], tal cual. Mismo modelo, mismo turno, mismo token, otra tool, resultado opuesto — y esa diferencia es todo el control.
  5. La respuesta se cambia de nuevo. La tool de correo contesta {"recipient": "ana@example.com", "status": "sent"}. La dirección de la respuesta pasa a ser el mismo token antes de que el modelo la lea, así que la protección vale también a la vuelta.
  6. El cliente lee sus propias palabras. El modelo escribe "la confirmación se envió a [EMAIL_7f3a9c1b2d4e]", y la plataforma lo convierte de nuevo en la dirección que tipeó el cliente mientras el texto va pasando.

El mapeo del paso 2 nunca se escribe en ningún lado. El historial guardado conserva lo que tipeó el cliente, y cada vez que el historial entra a un prompt se tokeniza de nuevo — lo que reconstruye el mismo mapeo, en el mismo orden, desde las mismas filas. Una tabla persistida de tokens y valores sería una segunda copia de datos personales con su propia pregunta de retención, guardada para ahorrar un trabajo que ya ocurre.

Restituir es un permiso

Un token se convierte en valor solo donde una regla lo dice, para una tool con nombre y un tipo de entidad con nombre. No existe una opción que restituya en todos lados, y el camino de escritura se niega a guardar una.

La razón es el ataque que el token vino a atenuar. Una instrucción inyectada — dentro de un documento recuperado, por ejemplo — puede convencer al agente de mandar lo que tenga a una tool que eligió el atacante. Si todo token se restituyera en toda llamada, esa llamada llevaría la dirección real y la tokenización habría protegido al modelo regalando el valor en la puerta. Con permisos por tool, lo peor que obtiene la instrucción es un token, que es una cadena opaca.

De tratar la restitución como una decisión y no como un efecto colateral salen dos comportamientos más. Un token que el run nunca emitió — inventado por el modelo, deformado al copiarlo, sacado de otra sesión — se deja exactamente como está; nunca se adivina hasta convertirlo en la dirección de alguien. Y un permiso nombra un tipo de dato además de una tool: la tool de correo puede recibir direcciones y aun así recibe un token para un identificador nacional que no le sirve para nada.

El cliente sigue leyendo su propia dirección

El último paso tiene su propia regla: en la respuesta solo se restituyen los valores que el cliente escribió él mismo. Un valor que llegó por primera vez en la respuesta de una tool es dato de otra persona, y sigue siendo un token incluso cuando el modelo lo repite. La plataforma registra de dónde vino cada valor, y un valor que el cliente tipeó sigue siendo suyo aunque después una tool lo devuelva también.

La respuesta además sigue haciendo streaming. El texto llega en pedazos y un token puede quedar partido entre dos, así que la plataforma libera de inmediato todo lo que llega hasta un corchete abierto y retiene solo esa cola sin terminar — como máximo 32 caracteres, y solo mientras hay un corchete abierto. Una oración común no espera nada; un token se libera, restituido, en el momento en que se completa. El streaming se detiene únicamente cuando el agente además pidió que sus respuestas se inspeccionen antes de entregarse, que es otra decisión con otro costo.

Qué ve el operador después

Cada restitución es un evento que nombra el tipo de dato y la tool, y nunca el valor. Un operador puede responder "qué tools recibieron una dirección esta semana" sin que el registro se convierta en una segunda copia de las direcciones. La auditoría de llamadas a tools conserva la forma de cada request y marca los campos personales como sanitizados, y ahora está detrás de la restitución, así que registra lo que la tool realmente recibió.

Dónde termina

Esto es lo que vale hoy, dicho con sus bordes.

  • La cobertura sigue al detector. El analyzer que viene junto al runtime carga modelos y recognizers en inglés y en español, incluidos el NIF y el NIE de España. Los identificadores propios de otro país se suman con recognizers tuyos.
  • Los nombres dependen del detector. El nombre de una persona se tokeniza solo con Presidio y solo si la política lo incluye; los patrones incorporados no detectan nombres, así que ahí el modelo puede seguir leyéndolos e infiriendo contexto alrededor de un token.
  • Una pausa de aprobación puede perder un valor. La memoria del run sobrevive a una espera de aprobación en el mismo servidor. Si la reanudación cae en otro, los valores reconstruidos desde el historial guardado se recuperan, pero uno que apareció solo en la respuesta de una tool antes de la pausa no — y la siguiente tool recibe entonces un token, que es la dirección segura para fallar, y queda registrado.
  • El mensaje del cliente se guarda como lo tipeó. Es deliberado, y el control para eso es la retención y el acceso, no la redacción.
  • Fuera de un run no hay mapeo. Un chequeo que se evalúa donde no hay un run degrada a redacción simple en vez de emitir tokens que nadie podría restituir.

Invariantes que vale la pena probar en cualquier sistema

Sea el nuestro o el de cualquiera, un diseño que dice "el modelo nunca ve los datos" tendría que sobrevivir estos seis chequeos.

InvarianteCómo probarla
El proveedor del modelo nunca recibe un valor detectadoLeer el request exacto enviado al proveedor en un turno que contiene una dirección
La identidad sobrevive sin el contenidoMencionar la misma dirección dos veces y confirmar que el modelo ve un solo identificador
Una tool recibe el valor solo si se lo habilitaronLlamar a dos tools en un turno y comparar qué recibió cada una
Un identificador desconocido o ajeno nunca se resuelvePegar en un mensaje un token de otra sesión y confirmar que no se restituye nada
La respuesta está protegida igual que el requestHacer que una tool devuelva el valor y leer qué se le mostró después al modelo
Al cliente no se le muestran las tripasLeer la respuesta en streaming, buscar identificadores y confirmar que sigue llegando en pedazos

Un sistema que falla la tercera o la cuarta protegió al modelo y dejó la puerta abierta; uno que falla la sexta cambió la experiencia del cliente por el control.

Seguir leyendo