cursor chat history: la puerta trasera de tu RAG

En pocas palabras: Porque el historial persistido es una segunda ruta de lectura hacia los datos RAG: sirve citas y tarjetas de fuentes sin pasar por los permisos del search. Hay que gatear el replay con los mismos chequeos de entitlements por documento, incluido el chat history de editores con IA como Cursor.

En dev.to, el desarrollador rdiegoss publicó una advertencia incómoda para cualquiera que arme productos con RAG: si tu copiloto persiste citas y tarjetas de fuentes, el endpoint de historial se convierte en una segunda vía de lectura sobre esos mismos datos, sin pasar por los permisos que cuidás tanto en la búsqueda. Su receta es directa: gatear el replay como gateás el search, y aplicar la misma lógica a cualquier historial, desde copilotos internos hasta el cursor chat history de editores con IA.

Ubiquemos el concepto antes de seguir: el historial de chat persistido es un segundo read path, o sea, una ruta de lectura alternativa hacia los datos que tu búsqueda RAG ya protege con permisos. El flujo en vivo pasa por chequeos de entitlement y gates por documento; el replay, en cambio, lee filas guardadas y las sirve tal cual. Esa asimetría es el agujero.

En 30 segundos

  • La persistencia no es permiso: que un turn pudiera mostrar una tarjeta en su momento no autoriza mostrarla seis meses después.
  • Dos rutas, dos niveles de cuidado: la búsqueda en vivo tiene toggles de entitlement y un gate por documento; el historial leía filas de la base sin ningún chequeo.
  • Dos gates en el replay: uno grueso que verifica el entitlement del tenant antes de tocar una sola fila, y otro fino que retiene las tarjetas manteniendo el texto.
  • Fallar cerrado y negar en silencio: el acceso denegado responde igual que una sesión inexistente, y con el toggle apagado las queries ni se ejecutan.
  • Trade-off declarado: el autor eligió gate a nivel entitlement y no por documento, por el costo de una llamada masiva de autorización por cada página leída.

Cursor es un editor de código con inteligencia artificial desarrollado por la empresa Anysphere. Basado en Visual Studio Code, permite escribir, editar y depurar código asistido por modelos de lenguaje integrados directamente en el entorno de desarrollo.

¿Qué significa que el historial de chat sea un segundo read path en RAG?

Es una segunda puerta de lectura hacia los mismos datos que protegés en la búsqueda. Arranquemos por la base: la generación aumentada por recuperación (RAG) es la técnica donde un modelo arma respuestas buscando fragmentos en tus fuentes. El copiloto del post hace streaming de búsquedas documentales y persiste cada turn: texto, tarjetas de fuentes, scores de relevancia, nombres y versiones, todo pineado al evento de auditoría que lo produjo. Una respuesta sin su evidencia es puro vibes, dice el autor, y tiene razón.

El problema aparece cuando comparás los dos caminos de lectura:

  • Write time (el turn en vivo): usuario → chat → search → chequeos de entitlement → gate de vista por documento → respuesta más tarjetas.
  • Read time (semanas después): usuario → GET /sessions/{id}/turns → SELECT de filas → tarjetas reenviadas sin preguntarle a nadie.

¿Quién autorizó esas tarjetas la segunda vez? Nadie. La base no sabe nada de toggles ni de JWTs. Si lo shipeás ingenuo, construiste una puerta lateral sin candado hacia exactamente los datos que venís blindando. Nada de esto es exótico: todo producto RAG que persista recuperaciones tiene esa puerta. La única pregunta es si alguien le puso cierre.

¿Por qué la persistencia no es permiso?

Porque los permisos cambian y las filas no. La frase que ordena todo el post es “Persistence is not permission” (la persistencia no es permiso), y resume la decisión central: lo que un turn tenía permitido mostrar al escribirse no prueba nada sobre lo que puede mostrar al leerse.

Ponele que un analista consulta documentos de un cliente en marzo. En junio le revocan el acceso al módulo de búsqueda documental. Abre su historial y ahí están las tarjetas, con nombres de archivos, versiones y scores, renderizándose como si nada. La respuesta cómoda de que “es su propio historial, ya lo vio” no sobrevive cinco minutos de pensamiento: los admins del tenant manejan dos switches separados (el copiloto y la búsqueda documental), los contratos cambian, los toggles se apagan, y las filas siguen ahí esperando, indiferentes. En la comparación entre Cursor y Claude Code profundizamos sobre esto.

Qué cambió desde que se escribió el turnQué muestra el replay ahora
Nada: todo sigue habilitadoTranscripción completa más tarjetas de fuentes
Toggle de búsqueda documental apagadoEl texto del transcript se reproduce; el builder retiene las tarjetas
Entitlement del copiloto apagadoNinguna sesión, ningún turn
cursor chat history diagrama explicativo

Ojo con un detalle: el post cubre deriva de permisos, no ciclo de vida del documento. Si borrás un archivo, las tarjetas persistidas siguen existiendo como filas; el control que las retiene es el gate, no el DELETE.

¿Cómo funcionan los dos gates del replay?

Con dos capas de control: un gate grueso arriba de toda lectura de historial y un degrade fino para el caso intermedio. El gate grueso vive en una función llamada _require_copilot_read: carga los entitlements del tenant con load_tenant_entitlement y rechaza la petición antes de tocar una sola fila de sesión. Tres detalles hacen un trabajo silencioso acá. El servicio re-chequea aunque exista un gateway adelante, porque el día que alguien redirige tráfico o suma un caller nuevo, un supuesto que vive en otro codebase no es un control. La denegación se integra a un not-found uniforme que ya respondía lo mismo para ids malformados, desconocidos y ajenos. Y el rechazo no ejecuta ni una query.

El caso fino es más interesante: copiloto encendido, búsqueda documental apagada. Un deny global sería un error, porque la conversación es del usuario y no todo gira alrededor de documentos. Pero las tarjetas son datos derivados de documentos: existen porque una búsqueda corrió bajo un entitlement que ya no está granted. Ahí entra el segundo gate en el response builder, que condiciona las tarjetas al estado del switch. Ahora bien, ¿por qué no re-correr el gate por documento en cada página de historial? Porque implicaría una llamada masiva de autorización por lectura, un costo real contra tráfico de replay. El autor shipeó primero la capa de entitlement: un lookup que atrapa la deriva de toda la capability.

¿Por qué la degradación graciosa es un resultado de autorización?

Porque rompe el falso dilema allow/deny que deja puertas abiertas. Este es el punto que el autor más empujaría en una design review: la degradación graciosa es un resultado de autorización, no un estado de error. Muchas discusiones de authz colapsan en permitir o denegar, y entonces alguien argumenta que denegar rompe el feature de historial, así que mejor permitir. Resultado: la puerta lateral sale abierta y todos miran para otro lado. Relacionado: gestionar permisos de acceso con Intune.

Tener una respuesta intermedia (mantener la conversación, retener los artefactos derivados) fue lo que hizo shippable la posición estricta. El test test_turns_withhold_sources_while_documents_ai_is_off fija el contrato: el transcript se reproduce, las tarjetas no. Qué ve el usuario en ese estado, si un hueco vacío, un placeholder o una explicación, queda deliberadamente abierto en el post.

¿Qué significa fail closed y por qué negar sin revelar?

Fallar cerrado significa que ante duda, el sistema niega; negar sin revelar significa que la denegación no confirma que algo exista. El endpoint de turns ya respondía un not-found idéntico para sesiones inexistentes, ajenas o malformadas, así que jamás confirma qué hay en la base. La denegación por entitlement adopta la misma postura: el caller denegado no aprende nada, ni siquiera “hay algo acá que no podés ver”. Costo: cero lecturas. Con el toggle apagado, las queries de sesión y turn ni se awaitan, algo que test_history_reads_fail_closed_when_the_copilot_is_disabled pinnea en el suite (sí, el nombre del test es literal).

¿Qué higiene necesita un replay confiable?

Dos arreglos que parecen menores y no lo son. El primero: serializar la validación con el mismo rigor que el insert. El bound check del payload de fuentes usaba json.dumps(detail, default=str) mientras la inserción serializaba estricto, así que un payload podía pasar el chequeo y tumbar el registro entero del turn al momento de insertar. Ahora el guard serializa igual de estricto que el insert, y un payload no serializable se descarta con warning en vez de llevarse puesto el transcript. El turn siempre aterriza; el transcript nunca queda de rehén de sus citas.

El segundo: normalizar el JSONB reenviado como cualquier otra lectura JSONB. Según el path del driver, la columna puede volver como objeto o como string crudo, y el replay ahora parsea cuando corresponde, igual que el resto del módulo. “Funcionaba con mi configuración de driver” no es un contrato de datos, y tratarlo como tal es como construir sobre arena. Para más detalles técnicos, mirá cómo funciona ChatGPT por dentro.

¿Qué pasa con el cursor chat history de Cursor y otros productos comerciales?

Las fuentes de esta nota no auditan la implementación interna de Cursor ni de ningún proveedor comercial, y sería deshonesto afirmar lo contrario. Lo que sí podemos decir: Cursor mantiene historiales de conversación y hace retrieval sobre tu codebase, así que el patrón del post aplica por construcción a cualquier producto que persista resultados de recuperación. Si usás Cursor o un editor similar, la pregunta útil no es si tiene historial sino si el replay re-valida permisos del workspace. Para decisiones de compra o auditoría interna, pedile al proveedor cómo gobierna todas sus rutas de lectura, no solo la de búsqueda.

Dónde dibujar la línea: los tres trade-offs que el autor declara

El post cierra con disclosure completa de las decisiones duras, algo poco común y bienvenido:

  • El texto se reproduce aunque las tarjetas queden afuera. La prosa fue generada a partir de esos documentos y puede parafrasearlos. El razonamiento: esas palabras ya se entregaron a este usuario una vez, y el toggle gobierna la capability de búsqueda, no un discurso que ya ocurrió. Defendible, admite él mismo, aunque nada obvio.
  • Gate a nivel entitlement, no por documento. Menor granularidad a cambio de un lookup único que atrapa la deriva completa de la capability. Un shop más estricto re-correría el gate por documento; uno más laxo reenviaría todo invocando “registro inmutable”.
  • La latencia del lookup no fue medida. El autor dice que es chica, pero es un claim sin benchmark propio del autor (tomalo con pinzas). ¿Alguien lo verificó de forma independiente? Todavía no.

Y queda flotando la pregunta que él mismo no puede soltar: cuando se revoca el entitlement documental, ¿qué hubieras retenido vos, solo las tarjetas o el turn completo?

Errores comunes al gobernar el historial en sistemas RAG

  • Delegar todo el control al gateway: parece suficiente hasta que alguien rerutea tráfico o agrega un nuevo caller. Corrección: re-chequeá los entitlements dentro del servicio, siempre.
  • Razonar la autorización como binaria: el debate allow/deny termina en “deny rompe el feature, entonces allow”, y la puerta sale abierta. Corrección: diseñá un tercer estado que conserve lo legítimo y retenga lo derivado.
  • Validar con menos rigor que el insert: un payload que pasa un chequeo laxo y falla la escritura estricta te tumba el turn completo. Corrección: misma serialización en ambos lados, y descarte con warning de lo irrecuperable.
  • Confundir comportamiento de driver con contrato de datos: que el JSONB vuelva como objeto hoy no garantiza nada mañana. Corrección: normalizá toda lectura en un solo lugar.

Preguntas Frecuentes

¿Qué es un segundo read path en datos RAG?

Es una ruta de lectura alternativa hacia los mismos datos que protege tu búsqueda RAG. Cuando persistís resultados de recuperación (citas, snippets, scores), el endpoint de historial lee esas filas y puede exponer contenido que los controles de la búsqueda sí bloquean.

¿Cómo se protege el historial de chat en un sistema RAG?

Re-validando permisos en cada lectura en lugar de confiar en los que valían al escribir. El diseño del post usa dos gates: un chequeo de entitlement del tenant antes de tocar cualquier fila, y la retención selectiva de artefactos derivados de documentos. Te puede servir nuestra cobertura de cómo razonan los modelos de lenguaje.

¿Qué significa gating en la autorización de chat persistente?

Significa interponer un control de permisos entre la petición y los datos guardados. Puede ser grueso (bloquear todo el historial si el copiloto pierde el entitlement) o fino (mostrar el texto de la conversación pero ocultar las tarjetas de fuentes).

¿Por qué el replay necesita re-validar permisos si el usuario ya vio esa conversación?

Porque los entitlements derivan entre escritura y lectura: los toggles se apagan y los contratos cambian, mientras las filas permanecen intactas. Lo que un turn podía mostrar en su momento no prueba nada sobre lo que puede mostrar semanas o meses después.

¿Cómo se degrada gracefully cuando cambian los permisos en RAG?

Separando lo que es del usuario de lo que deriva de documentos: el texto de la conversación se mantiene y las tarjetas de fuentes se retienen mientras el entitlement documental esté apagado. La clave conceptual es tratar la degradación como resultado de autorización, no como error.

Conclusión

Si persistís recuperaciones, tenés la misma puerta, la tengas mapeada o no. El plan concreto para la próxima semana: inventariá los endpoints que reenvían retrieval almacenado, agregá un chequeo de entitlement arriba de cada lectura, decidí tu forma de degradar antes de que un incidente la decida por vos, y alineá la serialización de validación con la del insert. La frase para llevarse del post de rdiegoss cabe en un sticky: la persistencia no es permiso. Gateá el replay como gateás la búsqueda, o aceptá que la segunda puerta está abierta.

Fuentes

Desplazarse hacia arriba