Protección de datos¶
Dónde residen los datos personales en un deployment, qué sale de él y bajo qué configuración, qué controles existen en el código con una prueba detrás y cuáles siguen siendo incidencias abiertas. Escrito para quien responde a una revisión de protección de datos de un deployment — un delegado de protección de datos, un responsable de seguridad, un operador — y honesto sobre la diferencia entre lo que el software puede hacer y lo que un deployment ha decidido de verdad.
Una página no es cumplimiento
Nada de lo que hay aquí es prueba de que un deployment cumpla el RGPD. La base de código se puede desplegar dentro de un entorno conforme; que uno lo sea depende de los providers configurados, de la retención decidida, de los acuerdos firmados y del operador que lo lleva. Cada una de esas condiciones aparece abajo como algo que hay que obtener y verificar, nunca como algo que se da por supuesto.
Por defecto no sale nada¶
Un deployment recién instalado lo guarda todo en su propio PostgreSQL y en su propio disco, y no envía nada a ninguna parte. Cada salto al exterior es una fila o un ajuste que alguien añade después: un perfil de modelo que nombra un provider, una colección que elige un parser, un método de búsqueda en un spec, un token de Logfire, un host de mem0, un servidor MCP, un bot de canal. Quita la fila y el salto desaparece.
Así que un deployment que no quiere ninguna tercera parte en la cadena se configura así y el software coopera:
| Asunto | La respuesta local |
|---|---|
| El modelo de chat | Un perfil ollama o litellm — sin clave, apuntando a un endpoint que alojas tú. Cualquiera de los 28 providers con base_url admite también una pasarela tuya |
| Parseo de documentos | pymupdf, el valor por defecto, se ejecuta en el worker. El OCR de LiteParse también, o en un servidor de OCR que registres como servicio local. LlamaParse es una elección por colección que necesita una clave del vault; sin ella no se parsea nada fuera |
| Embeddings | Un Ollama que alojas tú, registrado como servicio local en Knowledge → Integrations y elegido por colección como provider ollama. Sin clave, y el único provider que puede usar una colección app-scoped |
| Trazas | Deja LOGFIRE_TOKEN sin definir y no vincules ningún token observability a un spec ni a un entorno. Los runs siguen registrando localmente un id de traza |
| Búsqueda, navegación, memoria, herramientas | No vincules ningún secreto search, ninguna capability web_fetch, browser_use ni memory_mem0, ninguna conexión MCP. El navegador web puede quedarse si cada sitio que puede abrir está en allowed_domains y sus pasos van a un modelo que alojas tú |
| Correo | Tu propio relay SMTP |
| Voz, imágenes y vídeo | Para la voz a texto, ningún provider: deja el dictado en off o browser y los bots de canal desactivados, porque todavía no se puede nombrar un servidor propio de voz a texto (un paso previsto). Para la síntesis de voz y el vídeo, ninguna capability text_to_speech ni video_generation. Para las imágenes, perfiles en un provider que alojes tú, o ningún perfil de ese tipo |
Los embeddings incluidos: el catálogo nombra OpenRouter y OpenAI, a los que una
colección llega con una clave del vault, y ollama, al que una colección llega a
través de un servicio local — una fila que nombra un servidor que opera la
organización o el deployment. Una colección en ollama no envía sus chunks ni
sus consultas a ningún host que no sea ese, y no le paga a nadie. Un deployment
que deba mantener los documentos en su propio hardware crea allí todas sus
colecciones.
Quién es responsable de qué¶
| Parte | Rol | Qué significa aquí |
|---|---|---|
| La organización que lo despliega (una ciudad, una empresa) | Responsable del tratamiento | Decide finalidades, retención, a qué providers puede llegar un agent, y firma los acuerdos con ellos |
| Vstorm, como autor del software | Ninguno de los dos, en un deployment autoalojado | El código se ejecuta en tu infraestructura y nada llama a casa. Vstorm nunca ve tus datos |
| Vstorm, si opera el deployment por ti | Encargado del tratamiento | Un contrato de encargo de tratamiento es un contrato entre nosotros, no un ajuste. Tiene que existir antes de publicar el primer agent |
| Un provider de modelos, embeddings, parseo, búsqueda u observabilidad | Subencargado, elegido por configuración | La plataforma registra qué provider y qué endpoint usa cada agent. Su ubicación, su retención y sus condiciones de entrenamiento son suyas, y se verifican en cada deployment |
La plataforma no entrena ni afina nada. Envía prompts, documentos y resultados de herramientas a los providers que configura un deployment y guarda lo que vuelve. Que un provider use el tráfico de la API para entrenar es una propiedad de la cuenta y de las condiciones del provider, y la lista de comprobación del final pide la declaración en lugar de darla por supuesta.
Dónde residen los datos personales¶
Todo lo de abajo está en el PostgreSQL del propio deployment, en su propio disco
o en un servicio que el deployment eligió. Toda tabla que pertenece a una
organización lleva un organization_id; una fila hija — un mensaje, un adjunto,
una valoración — queda acotada a través de la conversación o el usuario del que
cuelga, y leerla pasa por la comprobación del padre.
La base de datos¶
| Almacén | Guarda | Datos personales que contiene | Finalidad |
|---|---|---|---|
users, sessions, organization_members |
Cuentas e inicios de sesión | Correo, nombre, avatar, contraseña con hash o el id de la cuenta de Google, hash del refresh token, dirección IP y user agent por sesión; lo que la persona dijo a los agents sobre sí misma - idioma de la consola, zona horaria, rol, en qué trabaja, horario laboral - y los primeros mensajes escritos para su chat vacío | Autenticación y autorización; lo que se le dice a cada agent sobre la persona |
browser_sign_ins |
Los inicios de sesión que una persona guardó en sitios, para el navegador web | El sitio, la organización, cuándo se usó por última vez y la propia sesión (cookies y el localStorage del sitio), sellada en el vault en el ámbito de la persona |
Un recorrido posterior en su nombre empieza ya con la sesión iniciada. La exportación de datos personales lista los sitios, nunca la sesión |
conversations, messages, tool_calls |
Todos los chats en todas las superficies | El texto que la gente escribió, las respuestas y el razonamiento del modelo, argumentos y resultados de herramientas, un resumen continuo de los hilos largos | La función central del producto; el historial al que vuelve la persona |
voice_calls |
Cada conexión de una llamada de voz | El id de usuario de quien llama (ninguno para un visitante), el agent, el run, la conversación y el embed a los que perteneció, el provider y el modelo, el id de la llamada en el provider, el id de la clave del vault con la que se sostuvo, el worker que la mantiene, su heartbeat, sus horas de inicio, fin y colgado, su coste, sus segundos y su número de activaciones de guardrails. Ni audio ni transcripción - las palabras de la llamada son filas normales de messages, cada una marcada con un turn_id y, cuando quien llama habló por encima, interrupted |
Colgar una llamada cuyo proceso murió, y contar los minutos de llamada mensuales de la organización |
chat_files |
Adjuntos de un mensaje | Nombre de archivo, tipo, tamaño, el texto extraído (parsed_content) y la ruta de los bytes en disco |
Responder sobre un archivo |
generated_media |
Una fila por cada imagen, clip de voz o vídeo que generó un agent | La organización, la conversación en la que se hizo si el run tenía una, el nombre del archivo, su ruta de almacenamiento y su tamaño - no su contenido, que es un archivo bajo MEDIA_DIR |
Contar los medios generados de cada organización contra su cuota, y borrarlos con su conversación o tras su periodo de retención |
context_files |
Conocimiento permanente que un builder escribió para los agents | Lo que el autor pusiera ahí — y llega al prompt literalmente. Consulta Archivos de contexto | Instrucciones y hechos que un agent debe saber siempre |
artifacts, artifact_versions y sus archivos |
Páginas que los agents publicaron — informes, dashboards | Lo que muestre la página, que el run construyó con lo que podía leer; el propietario, el run que escribió cada versión y, cuando hay uno activo, la clave del enlace público, sus ajustes (una contraseña como hash bcrypt) y cuántas veces se abrió — un número y una hora, nada sobre el visitante. Consulta Aplicaciones | Compartir un resultado bajo un enlace estable |
agent_memory_files |
Notas que un agent escribió sobre una persona o un chat de grupo | Lo que el agent decidiera que valía la pena recordar, con clave person:<user_id> o una sala de chat |
Continuidad entre conversaciones |
rag_documents, knowledge_bases y una tabla vectorial por colección |
Documentos subidos y sincronizados, sus chunks y sus embeddings | El texto del documento y sus vectores, la ruta original del archivo en el origen | Recuperación |
agent_runs, tool_approvals, run_manifests |
Lo que costó y lo que hizo cada run | El prompt de sistema y la última petición entregada al modelo, argumentos de herramientas a la espera de aprobación, la persona que decide y su nota | Budgets, aprobaciones, historial de runs |
agent_triggers |
Runs programados y disparados por eventos | El prompt y la configuración y el filtro del origen de eventos | Ejecutar un agent sin una persona |
app_admin_audit_logs |
Quién cambió accesos o gastó dinero — el rastro de la organización y el del administrador del deployment comparten tabla | Actor, suplantador, dirección IP, la acción y un mapa details. El mapa nombra sobre todo campos, pero algunas entradas guardan valores: el correo de la cuenta suplantada, el correo de una cuenta que un administrador borró, una nota de publicación |
Rendición de cuentas. Consulta Gobernanza |
notifications, notification_deliveries |
Cada notificación dirigida a alguien y su envío por correo. La fila se escribe incluso cuando el destinatario ha desactivado todos los canales para ese evento — es lo que impide escribir dos veces la misma ocurrencia — y simplemente no se muestra ni se envía nunca; solo la fila de envío es condicional | El destinatario, un resumen prerrenderizado (nunca un comentario en bruto ni un valor secreto) y las variables tipadas a partir de las cuales un correo se vuelve a renderizar; una fila de envío añade su canal, estado, intentos y un motivo de fallo depurado — nunca el texto de error del provider | Alertas que una persona lee una vez, sin supervisión. Consulta Gobernanza |
notification_preferences, announcements |
Interruptores de canal por evento, y las difusiones propias del app admin | Un id de usuario por interruptor; el remitente de una difusión, su cuerpo y las organizaciones y el rol a los que iba dirigida — nunca la lista resuelta de destinatarios, que solo se puede reconstruir a partir de notifications hasta que esa fila caduca en el barrido de retención de notificaciones — después de lo cual la entrada de audit detrás de ella conserva solo un recuento, nunca los destinatarios |
Exclusión voluntaria, y el registro propio de quien la escribió |
embed_visitors, channel_identities, channel_sessions |
Desconocidos en una página alojada y personas en Slack, Teams, Telegram o Mattermost | Una clave de visitante aleatoria; un id de usuario de la plataforma, nombre de usuario y nombre visible; el id del chat | Retomar el hilo correcto |
message_ratings |
Pulgares y comentarios sobre las respuestas | Quien valora y su comentario | Revisión de calidad |
suggestions |
Lo que el trabajo por iniciativa propia propuso a una persona, y si lo aceptó | La persona, un título y una línea de detalle sacados de sus peticiones recientes, el prompt de rutina o el perfil que fijaría, y una clave que impide volver a proponer algo descartado | Propuestas sobre las que decide la persona |
watches |
Lo que una persona pidió al trabajo por iniciativa propia vigilar en sus propias conexiones | La persona, cuál de sus conexiones, qué buscar y qué omitir con sus propias palabras, cada cuánto y cuándo revisó por última vez | Vigilancias que configura la persona |
ml_service_calls |
Cada llamada a los servicios de ML y cada transcripción: dictado, un mensaje de voz de un canal, la API, el dictado de un visitante en una página alojada o un widget | La organización, quién pidió, qué servicio, recuentos de bytes y unidades, la duración y cómo terminó; en una transcripción también el proveedor y el modelo, el id de la clave del vault con la que se envió, por dónde llegó, el agent y el bot de canal para los que fue y la duración de la grabación. La fila de un mensaje de voz de un canal nombra a la persona con la que está vinculada la cuenta del remitente, y a nadie si el remitente no está vinculado. La fila de un visitante no nombra a nadie - ni al visitante, que no tiene cuenta, ni a quien publicó la página. Borrar a la persona, el agent, el bot o la clave quita el vínculo y conserva la fila - nada de lo que se envió y nada de lo que volvió | Informe de uso, y la vista de un operador sobre una integración que falla |
agent_workspaces, sandbox_operations |
Archivos sobre los que trabajó un agent y el registro de lo que ejecutó | Para el backend state, los propios archivos, en JSON; para un contenedor, el id de sesión y cada comando, destino y resumen del resultado |
La sandbox. Consulta La sandbox |
organization_secrets, model_profiles, mcp_connections, channel_bots |
Credenciales y hacia dónde apuntan | Solo el cifrado sellado, con una pista; el provider, el modelo y la base_url en claro |
Llegar a los providers. Consulta Secretos |
messages.search_vector es un índice de texto completo sobre el mismo
contenido, y conversations.summary_messages es una compresión de él escrita
por el modelo. Ambos son copias del chat y se van con él.
Medios generados¶
Una imagen, un clip de voz o un vídeo que genera un agent es un archivo bajo
generated_<org>/ y una fila generated_media que lo nombra. La fila registra la
organización, la conversación si el run tenía una, y el tamaño, así que el total de
la organización queda acotado por GENERATED_MEDIA_QUOTA_MB (5120), comprobado
antes de cada llamada de generación de pago. Cuenta cada archivo, también uno
creado sin conversación: de una vista previa o una ejecución de prueba, de un
inciso /btw en un canal, de un run de la API que no nombra conversación, o un
archivo cuya conversación se borró por un camino que lo dejó atrás.
El archivo se borra con su conversación, con la cuenta a
la que pertenecía esa conversación, con la tarea diaria sweep-generated-media
cuando supera GENERATED_MEDIA_RETENTION_DAYS (90) aunque su conversación siga
ahí, y por prefijo con su organización. 0 desactiva cualquiera de los dos límites.
Un archivo sin conversación no es nada que un miembro pueda borrar para liberar
espacio, así que la misma tarea lo borra a los 7 días, sea cual sea
GENERATED_MEDIA_RETENTION_DAYS, 0 incluido. Un enlace a él - en una respuesta de
un canal o de la API - deja de funcionar entonces.
Borrar una conversación, una cuenta o una organización elimina las filas en su propia transacción y encola la desvinculación de los bytes como tarea en segundo plano en esa misma transacción, así que los bytes se van solo si se van las filas, y un reinicio tras el commit no pierde la desvinculación.
Esa tarea reintenta un
prefijo que el almacenamiento no elimina entero - el de una organización - y un
archivo suelto de una conversación o una cuenta borradas que el almacenamiento
rechaza, porque nada más podría volver a encontrar esos bytes. Un archivo que el
almacenamiento sigue rechazando tras el último intento de la tarea se queda sin
fila bajo generated_<org>/ hasta que se va su organización, y el informe de
abajo lo cuenta.
El barrido borra primero el archivo y su fila solo cuando el archivo ya no está; un archivo que el almacenamiento se niega a borrar conserva su fila, y el siguiente barrido lo vuelve a intentar.
Los archivos generados antes de que existiera la tabla no tienen fila. Se siguen sirviendo, pero no cuentan contra la cuota y el barrido no los ve: se van con su organización, o a mano, y el informe de protección de datos los cuenta entre los archivos sin fila.
Fuera de la base de datos¶
| Almacén | Guarda | Se borra cuando |
|---|---|---|
MEDIA_DIR en el host de la API (volumen media_data) |
Adjuntos de chat bajo <user_id>/, avatares, logos de embeds, las imágenes, la voz y los vídeos que generaron los agents bajo generated_<org>/ y una copia temporal de cada documento bajo _rag_tmp mientras se parsea |
Se borra un documento desde el producto. Los bytes de un adjunto de chat se desvinculan tras el commit que borra su conversación o a su propietario. Un archivo generado se va con la conversación en la que se hizo, tras GENERATED_MEDIA_RETENTION_DAYS (90) sea cual sea la suerte de la conversación - a los 7 días si no tiene conversación -, y con su organización; consulta Medios generados. Consulta Qué alcanza el borrado |
SANDBOXD_WORKSPACE_ROOT en el host de la sandbox |
Los archivos de cada workspace respaldado por contenedor | Se borra la conversación, o SANDBOXD_WORKSPACE_TTL los barre; sin definir, se conservan indefinidamente. Consulta Cuánto sobrevive cada cosa |
| Redis | Cubos de rate limit con clave en quien llama — en una superficie pública eso es la dirección IP en claro, dentro de la clave, durante el TTL de la ventana; claves de deduplicación de triggers y canales; estado del intercambio OAuth; invitaciones en espera; la vista en directo de una navegación — su última captura, sus pasos y lo que la persona que maneja el navegador pulsó y escribió hasta que la navegación lo lee; los pasos repetibles de una persona — las direcciones y etiquetas de lo que pulsaron sus navegaciones terminadas, nunca lo que escribió | Al expirar: una vista en directo una hora después del último fotograma de la navegación, los pasos repetibles dos semanas después de la última vez que se hizo la tarea, todo lo demás en minutos |
background_jobs en PostgreSQL |
El payload de cada job en segundo plano, su último error y la respuesta de un run asíncrono de la API | El barrido de retención elimina un job terminado tras BACKGROUND_JOB_RETENTION_DAYS (14). Los payloads son ids y rutas, con dos excepciones: el event_context de un disparo por evento — el remitente, el asunto y el cuerpo de un mensaje de Gmail, una incidencia de GitHub, un payload de webhook — y el prompt y la respuesta de un run asíncrono de la API se quedan en el job hasta entonces. Los logs del worker pasan por el mismo filtro de redacción que los de la API |
| Logfire, si está configurado | Trazas de cada run | La retención del provider. Hoy una traza lleva el prompt completo, la salida y los argumentos de las herramientas — consulta Trazas |
| Tu relay SMTP | Invitaciones, enlaces mágicos, peticiones de aprobación, alertas de budget, informes de uso, y el propio correo del buzón de notificaciones - un run que termina o falla desatendido, la ingesta de un documento que se completa o falla, un evento de seguridad o un cambio de configuración, el propio anuncio de un admin | Lo que el relay conserve. El correo de aprobación nombra el agent, la herramienta y un enlace, no los argumentos de la herramienta |
| Copias de seguridad | Un pg_dump es la base de datos entera; el volumen de medios son los archivos |
Tu caducidad de copias. El borrado nunca alcanza una copia ya tomada — consulta Copias de seguridad |
Qué sale del deployment¶
No sale nada mientras una fila o un ajuste no nombre un destino. Esta es la lista completa de destinos, con la configuración que decide cada uno.
| Destino | Qué se envía | Lo decide | Ubicación y condiciones |
|---|---|---|---|
| El modelo de chat | La conversación hasta ese punto, los adjuntos pegados o descritos, los chunks recuperados, los resultados de herramientas; una vez por noche, para el trabajo por iniciativa propia, las peticiones recientes de una persona, su perfil y lo que su agent anotó sobre ella | Un perfil de modelo: provider, model, base_url y una clave sellada. Veintisiete providers; ollama y litellm no llevan clave y se alcanzan en un endpoint que alojas tú, y openai, anthropic, google, huggingface y otros aceptan una base_url, así que un endpoint en la UE o una pasarela es un campo, no una bifurcación. Un perfil openai o azure en la Responses API envía store: false, así que el provider no guarda una respuesta almacenada y recuperable salvo que el perfil fije openai_store; la retención de OpenAI para vigilar abusos sigue aplicándose salvo que la cuenta tenga zero data retention |
La del provider. Verifícalo por perfil |
| El modelo de embeddings | Cada chunk de cada documento de una colección, y cada consulta de recuperación | Por colección, y solo ahí: embedding_provider (openrouter, openai u ollama, del catálogo) y, para los dos primeros, la clave del vault embedding_secret_id que paga. No hay clave de embeddings a nivel de deployment; una colección con clave que no nombre ninguna rechaza indexar y buscar. ollama no lleva clave y se alcanza en el servicio local que nombra la colección (embedding_endpoint_id), un host que operas tú |
La del provider, o tu propio host. Una elección permanente |
| LlamaCloud | El documento entero | Una colección cuyo pdf_parser sea llamaparse; tiene que nombrar una clave del vault (llamaparse_secret_id), no hay clave de deployment. El pymupdf por defecto parsea en el worker |
La de LlamaCloud, si se usa |
| Un servidor de OCR | Páginas renderizadas de un documento | Una colección cuyo pdf_parser sea liteparse y cuyo ocr_endpoint_id nombre un servicio local; sin él, el OCR se ejecuta en el worker |
Tu propio host — un servicio local está por construcción en la red del deployment |
| Un modelo de descripción de imágenes | Las imágenes dentro de los documentos | El image_description_model de una colección |
La de ese provider de modelos |
| Investigación web | La consulta de búsqueda que compuso el agent | web_research.method en el spec: duckduckgo (sin clave), tavily, brave o exa (un secreto search cada uno), o native, donde busca el provider del modelo de chat |
La del proveedor de búsqueda, o la del provider del modelo |
| Web fetch y el navegador web | La URL; para el navegador, lo que escribe en los sitios que abre | La capability en el spec; el navegador necesita además un endpoint CDP en BROWSER_CDP_ALLOWED_HOSTS, vacía por defecto |
El sitio consultado; el host del navegador y los sitios que abre |
| Proxy del navegador o navegador alojado de un agent | Todo lo que el navegador carga y envía, como lo haría el navegador del despliegue - páginas, contenido de formularios, cookies, inicios de sesión - más el registro propio del proxy o del servicio | browser_use.connection en el spec (#2130); built_in por defecto |
El proxy o el servicio de navegador alojado (Browserbase, Steel u otro), bajo el contrato propio de la organización |
| Los pasos del navegador web | Cada paso de un recorrido va al propio modelo del agente: la URL de la página, su título, las etiquetas de los elementos visibles y hasta 4.000 caracteres de su texto visible, más una captura con vision activado — que puede ser el contenido de un sistema interno. Lo que contiene un campo no se repite: la tabla de elementos solo dice que el campo está relleno |
La capability browser_use en el spec |
El proveedor del modelo del agente, como en la conversación |
| La sandbox, hacia fuera | Cualquier cosa del workspace, a cualquier host — la runtime workbench tiene red, shell y curl |
La capability sandbox y una runtime con needs_network; la aprobación de comandos controla qué se ejecuta, no adónde se conecta |
Adonde fuera el comando. El control de salida es el firewall del host de la sandbox, no un ajuste de aquí |
| Un servidor MCP | Argumentos y resultados de herramientas | mcp_connections.url, por organización o por persona |
La del operador del servidor |
| mem0 | Los recuerdos escritos para una persona o un chat | La base_url de la capability memory_mem0, que tiene que estar en MEM0_ALLOWED_HOSTS |
La del host de mem0 que permitas |
| Logfire | Spans de cada petición y cada run | LOGFIRE_TOKEN a nivel de deployment; un token observability en un spec, o logfire_token_secret_id en un entorno, redirige esos runs a otro proyecto. LOGFIRE_BASE_URL elige el deployment de EE. UU. o el de la UE. Sin definir en ninguna parte, no se envía nada |
La de Pydantic, EE. UU. o UE |
| Voz a texto, generación de imágenes | Una nota de voz de un canal, una grabación dictada en /chat, o la grabación de un visitante anónimo en una página alojada o un widget; el prompt |
El provider al que se resuelve una grabación - el par propio del bot, la anulación del agent o el predeterminado de la organización, que un bot hereda salvo que esté desactivado - enviado con la clave del vault nombrada junto a ese par - una clave de API de groq, mistral u openai, visible para la organización en la organización y en un bot; para el dictado en /chat, además, el motor de la organización en server, y para una página o un widget su propio dictation: server. Un perfil para google u openai para las imágenes. El audio de un dictado no se guarda aquí. Con browser en una página o un widget, el navegador del visitante envía el audio a su fabricante y nada de él pasa por este despliegue |
La del provider, o tu propio host para las imágenes; con browser, la del fabricante del navegador |
| Un provider de voz realtime | Durante una llamada de voz: el audio de quien llama, las instrucciones del agent y las definiciones de herramientas, los resultados de las herramientas, y los turnos anteriores de la conversación como texto cuando una llamada empieza o se reconecta | La sección Voice del agent: provider, model (o un deployment de Azure) y la clave del vault secret_id, publicada; sin sección Voice el agent no atiende llamadas. Sobre WebRTC (OpenAI) el audio va directamente entre el navegador y el provider; sobre el relay (Azure, Gemini, xAI) pasa por este despliegue sin guardarse. Aquí no se conserva audio, y la sesión solo conserva transcripciones |
La del provider |
| Texto a voz | El texto que se va a decir y cualquier indicación sobre cómo decirlo | El provider de la capability text_to_speech: openai, elevenlabs o google, con la clave del binding |
La del provider |
| Generación de vídeo | El prompt y la imagen inicial, si la hay | El provider de la capability video_generation: google, byteplus, xai o alibaba, con la clave del binding. google guarda la interacción (store: true), porque Gemini solo entrega un clip por dirección desde una guardada; a los otros tres no se les pide |
La del provider |
| Slack, Teams, Telegram, Mattermost | Las respuestas del agent; la dirección de correo de un miembro nuevo, para encontrarlo para una bienvenida en Slack, Mattermost o Teams | Una fila channel_bots con su token en el vault |
El proveedor de mensajería ya tiene el chat |
| Inicio de sesión con Google | Nada hacia fuera; Google devuelve el correo, el nombre, la foto y el id de la cuenta | GOOGLE_CLIENT_ID |
La de Google |
| Tu relay SMTP | El correo de arriba | SMTP_HOST, SMTP_TLS |
La tuya |
Los conectores de sincronización van al revés: un origen de Google Drive o S3
trae documentos hacia dentro, autenticado por un secreto connector, y a
partir de ahí los documentos son la copia del deployment y siguen las reglas de
arriba. Quién acaba pudiendo leer lo que un origen ha ingerido es
una decisión que toma la fila del origen.
Controles, y dónde se prueba cada uno¶
Cada fila nombra el mecanismo en el código y la prueba o la página que lo fija, o la incidencia que lo hará. Una fila cuya última columna es una incidencia es una laguna, y así queda dicho.
| Control | Mecanismo | Probado por |
|---|---|---|
| Aislamiento entre tenants | organization_id en toda tabla que pertenece a una organización, resuelto desde X-Organization-Id a una membresía en cada petición; las filas hijas solo se alcanzan a través de la comprobación de su padre |
tests/integration/test_conversation_tenant_isolation.py y sus hermanos; Permisos |
| Acceso a una fila | Tres capas: administrador del deployment, rol en la organización, grant por recurso a través de resolve_access. Un control que quien llama no puede usar no se renderiza |
Pruebas de rechazo en tests/api/; Permisos |
| Leer el chat de otra persona | El propietario, una compartición explícita o el app admin del deployment — nunca un rol de la organización. El chat de un proyecto se lee solo a través del proyecto: quien puede ver el proyecto, incluida la persona que lo empezó, y nadie que ya no pueda. Las conversaciones tienen su propia comprobación, ConversationService._may_read, en vez de la fórmula de grants |
admin_conversations.py exige is_app_admin; tests/integration/test_conversation_tenant_isolation.py |
| Credenciales en reposo | Cifrado de sobre por organización, claves maestras versionadas, rotación con ejecución en seco | Secretos, cuatro garantías fijadas por pruebas |
| Contenido en reposo | La aplicación no lo cifra. Los datos de Postgres, media_data y la raíz de workspaces del sandbox dependen del cifrado de disco o volumen que tú aportes. La excepción son los ficheros subidos y de chat con FILE_STORAGE_BACKEND=s3 y FILE_STORAGE_S3_ENCRYPTION en sse-s3 (el valor por defecto) o sse-kms bajo una clave que tú tienes: cada escritura pide entonces al almacén que los cifre. Su tercer modo, none, no envía cabecera de cifrado y existe para un almacén compatible con S3 sin KMS |
Control del operador. Configuración; agenticos cmd doctor indica en qué backend está un despliegue en marcha |
| En tránsito, entrante | HTTPS en tu proxy; Strict-Transport-Security cuando ENVIRONMENT=production; cookies de sesión httpOnly, y secure según el esquema de la petición al iniciar sesión y al refrescar. La ruta de cambio de contraseña pone secure solo en una build de producción |
Despliegue; frontend/src/app/api/auth/login/route.ts |
| En tránsito, hacia los almacenes | POSTGRES_SSLMODE y REDIS_SSL; agenticos cmd doctor informa de si la conexión que estableció iba cifrada |
Conexiones cifradas; tests/integration/test_store_tls.py |
| En tránsito, hacia los providers | HTTPS a todo endpoint catalogado. Una base_url propia se rechaza sin host o con credenciales dentro, pero http:// se acepta, para un Ollama o una pasarela en la propia red del deployment; un perfil en HTTP plano que apunte fuera de esa red envía los prompts y la clave en claro. El punto 4 de la lista de comprobación enumera todos esos perfiles |
refused_field("base_url", ...) en el servicio de perfiles de modelo; el esquema es control del operador |
| Secretos en respuestas, logs, auditoría, exportaciones | Ningún endpoint devuelve un texto en claro; SecretStr en todas partes; los specs referencian secretos por id |
Secretos |
| Datos personales en los logs | app/core/logging.py redacta direcciones de correo, JWT, claves de API, tokens bearer y pares password= de cada registro de log, tanto en la API como en el worker |
tests/test_logging.py; el worker lo instala en app/worker/__main__.py, y cada proceso hijo de ingesta en app/worker/child.py (#440) |
| Datos personales que llegan al modelo | La capability guardrails redacta IBAN, números de tarjeta, números de seguridad social estadounidenses, números de teléfono y direcciones de correo de los prompts, las respuestas, los resultados y los argumentos de herramientas cuando está configurada |
Capabilities; sus pruebas bajo tests/ |
| Datos personales en una columna de fallo | rag_documents.error_message y similares registran la etapa y la clase, nunca el texto del cliente |
app/services/rag/failures.py (#423) |
| Rendición de cuentas | Las entradas de auditoría comparten la transacción que actúa y fallan en cerrado; la suplantación nombra a ambas personas; las exportaciones masivas quedan registradas | Gobernanza |
| Exportación de auditoría | GET /audit/export, CSV o JSONL sobre una ventana, con puerta en audit:read y registrada en el propio rastro |
Governance (#1422) |
| Prueba de no manipulación del rastro | Cada entrada se une a una cadena de hashes por organización, y cada cadena lleva un checkpoint en su marca más alta, de modo que una entrada reescrita, una cola cortada y una cadena borrada son todas detectables. agenticos cmd audit-verify las recorre y termina con código distinto de cero ante una rotura |
Gobernanza (#1622, #1648). Detección, no prevención: quien tenga las credenciales de la propia base de datos puede volver a forjar una cadena o quitar el trigger que protege el checkpoint |
| Trazas | observability.content por agent: full registra todo, none solo tiempo, tokens, coste y nombres de herramienta, y un especialista de ese agent hereda el modo |
Entornos (#1413); un término medio redacted se descartó, #1616 |
| Retención programada | Por organización y por clase —conversaciones y sus archivos, runs y manifiestos, workspaces, memoria de agentes, documentos subidos y auditoría— dentro de un valor por defecto, un techo y un suelo de auditoría de todo el despliegue. Un barrido diario borra de verdad y registra recuentos, nunca contenido. Las copias de seguridad y todo lo ya enviado a un colector externo quedan fuera. notifications no es una de esas clases: se barre con su propio calendario fijo en su lugar, una fila leída a los 90 días y cualquier fila al año sin importar el estado. Los propios announcements quedan excluidos, así que lo enviado sigue siendo consultable en el rastro de auditoría después de que sus entregas caduquen. Tampoco lo son los medios generados, que se van tras el GENERATED_MEDIA_RETENTION_DAYS de todo el despliegue |
Retención; test_retention.py, tests/integration/test_retention_sweep.py; el barrido de notificaciones es tests/integration/test_notification_retention.py (#1598, Decision 8); los medios generados son test_generated_media.py y tests/integration/test_generated_media.py (#2310) |
| Supresión de una persona | El borrado de la cuenta concilia lo que lo bloquearía, desvincula los bytes de los adjuntos tras el commit y purga lo que ninguna cascada alcanza: las notas que los agents escribieron sobre ella, sus identidades de plataforma, sus workspaces. Borrar la memoria es también una llamada aparte, y alcanza a mem0 | Qué alcanza un borrado para lo que se conserva a propósito; test_personal_data.py |
| Acceso a los propios datos | Una persona lee en Ajustes → Memoria todo lo que cada agente de aquí ha escrito sobre ella, y puede suspender una nota, restaurarla o borrarla. Leer el almacén de otra persona es solo de la administradora del despliegue —no de un rol de organización— y queda auditado con el actor, el tenant, el sujeto y un motivo, nunca el contenido. Los almacenes externos (mem0) se nombran en vez de listarse | Leerla, y borrarla; test_memory_self_service.py. Todo lo demás que se guarda sobre ella vuelve de GET /me/data/export, acotado y auditado (#1421) |
| Identidad corporativa | Inicio de sesión con Google, contraseñas, OIDC genérico contra el proveedor que ya tienes — configurado solo por discovery desde OIDC_ISSUER — y tu propio directorio por LDAP o Kerberos. La dirección, el nombre visible y el identificador estable del directorio se guardan en la cuenta; sus grupos se leen en cada inicio de sesión y no se guardan, solo las membresías que los mapeos de las organizaciones hacen de ellos. Sin SAML ni SCIM |
Inicio de sesión con el directorio y grupos; OIDC_ISSUER, LDAP_URL |
| La matriz de controles que lee una revisión de seguridad | Seguridad asigna a cada control su mecanismo y el test que lo sostiene, en el marco de HIPAA §164.312 y SOC 2 CC6–CC8; esta página y Ponerlo en marcha son el resto | Seguridad (#1412) |
| Superficies públicas | La clave de visitante de una página alojada es aleatoria, nunca derivada de la persona; la admisión y las subidas se limitan por dirección, y la dirección vive en una clave de Redis durante la ventana y en ningún otro sitio | Canales |
| Avisos legales | Las URL de Términos y Privacidad propias del deployment sustituyen a las páginas incorporadas | El deployment |
Trazas¶
instrument_pydantic_ai() se ejecuta con el valor por defecto de la biblioteca,
así que un span contiene el mensaje del usuario, la respuesta del modelo y cada
argumento y resultado de herramienta. Con LOGFIRE_TOKEN sin definir, sin token
observability en ningún spec y sin logfire_token_secret_id en ningún
entorno, no se envía nada y el id de la traza se sigue registrando localmente.
Un deployment que necesite trazas sin el contenido pone el
observability.content del agent en none: se registran tiempo, tokens, coste y
nombres de herramienta, y ningún texto de mensaje sale — y un especialista de ese
agent hereda el modo, escrito inline o inventado por el modelo del run.
No hay un tercer modo intermedio. Una exportación depurada por un filtro de PII es una garantía que nadie puede auditar — un identificador que el filtro deje pasar ya ha salido, y el operador cree que no —, así que la elección es a propósito entre todo el contenido y nada de él (#1616).
Qué alcanza el borrado¶
Borrar es lo que hace el producto hoy cuando alguien lo pide; la retención programada es #1420.
| Acción | Elimina | Deja |
|---|---|---|
DELETE /conversations/{id} (el propietario) |
La conversación, sus mensajes, llamadas a herramientas, valoraciones, comparticiones y filas chat_files, por cascada - entre ellos la transcripción de una llamada de voz, ya que sus turnos son mensajes; los bytes de los adjuntos bajo MEDIA_DIR, recogidos antes del borrado y desvinculados tras su commit; las imágenes, la voz y los vídeos que generaron sus agents: las filas en el borrado, los bytes mediante una tarea encolada con él; los objetos de medios descargados de ese hilo, por prefijo; un workspace de contenedor mediante purge_for_conversation |
Las filas de runs y los manifiestos que nombraron la conversación conservan su copia del prompt; una fila voice_calls conserva sus horas y su coste con el vínculo a la conversación vaciado |
DELETE /memory/person/{user_id} (la persona, o members:manage) |
Cada fila de agent_memory_files con clave en la persona a lo largo de todos los agents de la organización, y lo mismo en cada almacén mem0 vinculado |
Las notas con clave en un chat de grupo en el que la persona habló |
DELETE /users/{id} |
La cuenta, sus sesiones, su organización personal y sus colecciones personales con sus tablas vectoriales y ficheros, por teardown explícito; conversaciones fuera de un proyecto y ficheros de chat por cascada; sus bytes, desvinculados tras el commit; las imágenes, la voz y los vídeos que generaron los agents en esas conversaciones, las filas en el borrado y los bytes mediante una tarea encolada con él; y lo que ninguna cascada alcanza: las notas que los agents escribieron sobre ella (owner_key es una cadena, no una clave foránea), sus identidades de plataforma y sus workspaces de usuario |
Entradas de auditoría que nombran el id del actor y, en algunas acciones, el correo: se conservan a propósito, porque una entrada sin su actor es peor que una que nombra una cuenta borrada. Mensajes en conversaciones de otra persona, y los chats de proyecto que empezó, que se conservan para el equipo sin autor, con los medios generados en ellos; sus mensajes ahí se muestran como de un antiguo miembro. Los medios que la persona generó sin conversación - vistas previas y ejecuciones de prueba, incisos /btw en canales, runs de la API que no nombraron conversación -, que ninguna fila vincula con ella; se van a los 7 días |
| Borrar un documento o una colección | Las filas, la tabla vectorial y el archivo almacenado, mediante un flow duradero tras el commit | Nada, una vez que el flow ha corrido; los recuentos de sync_logs permanecen |
| Borrar una organización | Todo lo acotado a ella, con el mismo desmontaje diferido - sus medios generados por prefijo, incluidos los archivos escritos antes de que se registraran | Las colecciones personales que solo llevaban el id |
Ninguno de estos alcanza una copia de seguridad. Una restauración devuelve lo que se había borrado, así que la caducidad de las copias forma parte de la política de retención y se escribe junto a ella.
Qué tiene que decidir y obtener un deployment¶
El software no puede aportar ninguna de estas cosas. Cada una es una prueba que pedirá la revisión, distinta de la capacidad técnica que la hace posible.
- Un contrato de encargo con cada subencargado configurado — cada provider
que nombre una fila de
model_profileso deorganization_secrets, el provider de embeddings, LlamaCloud si lo usa alguna colección, el proveedor de búsqueda que nombre elmethodde un agent, el host de mem0, Logfire, el relay SMTP y Google si el inicio de sesión está habilitado. - Una declaración de ubicación de datos por provider, cotejada con la
base_urlque cada perfil usa realmente. Un provider con endpoint en la UE solo está en la UE si el perfil lo dice. - Una exclusión de entrenamiento por provider: el ajuste de cuenta o la cláusula contractual por la que los datos de la API no se usan para entrenar. La posición de la propia plataforma es una frase — no entrena nada — y el resto es cosa suya.
- Un contrato de encargo con Vstorm, solo si Vstorm opera el deployment.
- Un calendario de retención para conversaciones, archivos, memoria, documentos, runs y auditoría, y junto a él la caducidad de las copias de seguridad. Hasta que #1420 imponga uno, la retención es un borrado manual.
- Cifrado de disco o de volumen en el host de la base de datos, el volumen de medios y el host de la sandbox, ya que la aplicación no cifra el contenido por sí misma — y una regla de salida en el host de la sandbox si los agents pueden ejecutar comandos.
- Las páginas legales a las que enlaza el deployment, y quién responde a una
solicitud de acceso o supresión. Los mecanismos están aquí —
GET /me/data/exporty el borrado de la cuenta — pero quién recibe la solicitud y en qué plazo es cosa del responsable.
Verificar un deployment¶
Comprobaciones reproducibles, desde el host, contra el deployment en marcha. Cada
una imprime hechos que la revisión puede adjuntar; ninguna imprime una credencial
ni los datos de una persona. Ejecútalas desde backend/, o a través de
docker compose exec api.
# 1. ¿Arranca siquiera, y van cifradas las conexiones a los almacenes?
# `postgres` informa del estado TLS de la conexión que el propio doctor
# estableció.
uv run agenticos cmd doctor
# 2. Cada credencial sellada sigue abriéndose con las master keys configuradas.
uv run agenticos cmd vault-rotate --dry-run
# 3. Las cadenas de hashes del rastro de auditoría y sus checkpoints, recalculados.
# Termina con código distinto de cero si alguna cadena ha sido alterada.
uv run agenticos cmd audit-verify
# 4. Todo lo que este deployment ha configurado realmente: los ajustes que
# deciden qué sale, cada provider y endpoint que un agent puede alcanzar, las
# credenciales guardadas por propósito, las colecciones y quién calcula sus
# embeddings, los servidores de tu propia red, los servidores MCP, los
# portales de trigger, las fuentes de sincronización y los bots de canal, las
# capabilities que alcanzan una dirección propia, dónde se trazan los runs y
# cuánto contenido lleva un span, cuánto de cada almacén alcanzaría un periodo
# de retención, y los archivos bajo `MEDIA_DIR` a los que ya no apunta ninguna
# fila.
uv run agenticos cmd data-protection-report --older-than 365
El punto 4 es el que hay que adjuntar. Imprime configuración y recuentos, nunca
contenido: ningún texto de mensaje, ningún documento, ningún valor de secreto ni
pista de uno, y un ajuste que guarda una credencial se informa como definido o no
definido en vez de imprimirse. --older-than es el periodo de retención que se
está considerando, en días, y la última columna de su tabla de retención es lo
que ese periodo ya habría eliminado.
Su última sección cuenta los bytes que ya no nombra ninguna fila. Borrar una conversación o una cuenta desvincula ahora sus adjuntos tras el commit, así que ese camino ya no suma; lo que todavía encuentra viene de antes de esos borrados, o de una desvinculación que un barrido best-effort no completó.
El directorio
temporal de parseo queda excluido, ya que por diseño no tiene fila. Los medios
generados se cuentan desde que generated_media registra cada archivo, así que lo
que aparece bajo un directorio generated_<org> es sobre todo un archivo generado
antes de que existiera esa tabla, que solo se va con su organización. También
puede ser un archivo cuya desvinculación falló tras borrarse su fila, o uno que
aún se está guardando, antes de que se escriba su fila. Todo lo demás
que se cuenta ahí son bytes que el producto ya no encuentra y no puede borrar.
Informa de un directorio y un recuento en vez de un nombre de archivo, porque una ruta almacenada conserva el nombre con el que se subió el archivo.
Dos secciones se leen de lo que se ejecuta y no de una tabla. Destinos de las
capabilities enumera las capabilities vinculadas a un agent que alcanzan una
dirección propia - web_research busca a través de DuckDuckGo sin credencial
alguna y por tanto sin fila en ningún otro punto del informe - y Tracing se
lee de cada versión ejecutable: la predeterminada y la que fija cada
entorno con nombre, porque un run a través de ese entorno usa
la observabilidad de esa versión y no la de la predeterminada.
Lo que ningún comando puede producir es la otra mitad de esta página: los acuerdos, las ubicaciones y las exclusiones de entrenamiento de la sección anterior. Adjúntalos junto a la salida.
Condiciones abiertas para una primera puesta en marcha¶
Enunciadas para el deployment para el que se escribió esta página, y ciertas para cualquier deployment hasta que cada una se cierre.
En el código, y permanentes:
- Las trazas llevan contenido completo salvo que un agent ponga
observability.contentennone. No hay término medio filtrado ni lo habrá: una exportación parcialmente depurada es una garantía que nadie puede auditar (#1616). - El contenido en reposo es del operador. Los cuerpos de los mensajes, los documentos y sus vectores, y los workspaces del sandbox son columnas y ficheros en claro; el vault sella credenciales, no contenido. Los ficheros subidos pueden ir a un almacén compatible con S3 que los cifra en su lado, pero la base de datos y el volumen de medios los protege el cifrado de disco o de volumen, o nada.
Cerradas, y respondidas arriba en vez de aquí: la prueba de no manipulación
del rastro de auditoría (#1622, #1648), el modo de contenido de trazas por agent
y su herencia por los especialistas (#1413, #1699), el trazado en el proceso
que ejecuta un agent disparado (#1700), la matriz de controles de HIPAA y SOC 2 en
Seguridad (#1412), el borrado y la exportación de
los datos de una persona (#1421), el backend S3 con cifrado en el servidor
(#1423) y el inicio de sesión OIDC genérico (#1419). Las trazas no tienen término
medio filtrado y no lo tendrán
(#1616); para un deployment
que no pueda exportar contenido, la respuesta es none.
En el deployment, decididas por su operador: los acuerdos, las ubicaciones,
las exclusiones de entrenamiento, el calendario de retención, la caducidad de las
copias de seguridad, el cifrado de disco, la salida de red de la sandbox y las
páginas legales de la sección anterior. Cada una es la clase de prueba que
data-protection-report deliberadamente no puede producir.
Una revisión que encuentre cada fila de arriba cerrada o aceptada por escrito tiene lo que esta página puede darle. El resto es del deployment.