Skip to content

Data protection

Where personal data lives in a deployment, what leaves it and under which configuration, which controls exist in code with a test behind them, and which are still open issues. Written for the person answering a data protection review of one deployment - a DPO, a security officer, an operator - and kept honest about the difference between what the software can do and what a deployment has actually decided.

A page is not compliance

Nothing here is evidence that a deployment complies with the GDPR. The codebase is deployable inside a compliant environment; whether one is compliant depends on the providers configured, the retention decided, the agreements signed and the operator running it. Every such condition is named below as something to obtain and verify, never assumed.

Nothing leaves by default

A fresh deployment holds everything in its own PostgreSQL and on its own disk and sends nothing anywhere. Every external hop is a row or a setting somebody adds afterwards: a model profile naming a provider, a collection choosing a parser, a search method on a spec, a Logfire token, a mem0 host, an MCP server, a channel bot. Remove the row and the hop is gone.

So a deployment that wants no third party in the chain configures itself that way and the software cooperates:

Concern The local answer
The chat model An ollama or litellm profile - keyless, pointed at an endpoint you host. Any of the 28 providers with a base_url also takes a gateway of yours
Document parsing pymupdf, the default, runs in the worker. LiteParse OCR runs in the worker too, or at an OCR server you register as a local service. LlamaParse is a per-collection choice that needs a vault key; without one, nothing is parsed off-site
Embeddings An Ollama you host, registered as a local service under Knowledge → Integrations and chosen per collection as the ollama provider. Keyless, and the only provider an app-scoped collection may use
Traces Leave LOGFIRE_TOKEN unset and bind no observability token to a spec or an environment. Runs still record a trace id locally
Search, browsing, memory, tools Bind no search secret, no web_fetch, browser_use or memory_mem0 capability, no MCP connection. The web browser can stay when every site it may open is on allowed_domains and its steps go to a model you host
Mail Your own SMTP relay
Speech, images and video For speech to text, no provider at all: keep dictation on off or browser and channel bots switched off, since a self-hosted speech-to-text server cannot be named yet (a planned follow-up). For speech generation and video, no text_to_speech or video_generation capability. For images, a profile on a provider you host, or no such profile

Embeddings included: the catalog names OpenRouter and OpenAI, which a collection reaches with a vault key, and ollama, which a collection reaches at a local service - a row naming a server the organization or the deployment runs. A collection on ollama sends its chunks and queries nowhere but that host, and pays nobody. A deployment that must keep documents on its own hardware creates every collection there.

Who is responsible for what

Party Role What that means here
The organization deploying it (a city, a company) Controller Decides purposes, retention, which providers an agent may reach, and signs the agreements with them
Vstorm, as the software's author Neither, for a self-hosted deployment The code runs on your infrastructure and nothing phones home. Vstorm never sees your data
Vstorm, if it operates the deployment for you Processor A data processing agreement is a contract between us, not a setting. It has to exist before the first agent is published
A model, embedding, parsing, search or observability provider Sub-processor, chosen by configuration The platform records which provider and which endpoint each agent uses. Their location, retention and training terms are theirs, and are verified per deployment

The platform trains and fine-tunes nothing. It sends prompts, documents and tool results to the providers a deployment configures and stores what comes back. Whether a provider uses API traffic for training is a property of the provider's account and terms, and the checklist at the end asks for the statement rather than assuming it.

Where personal data lives

Everything below is in the deployment's own PostgreSQL, on its own disk, or in a service the deployment chose. Every organization-owned table carries an organization_id; a child row - a message, an attachment, a rating - is scoped through the conversation or user it hangs off, and reading it goes through the parent's check.

The database

Store Holds Personal data in it Purpose
users, sessions, organization_members Accounts and sign-ins Email, name, avatar, hashed password or the Google account id, refresh-token hash, IP address and user agent per session; what the person told agents about themselves - console language, time zone, role, what they work on, working hours - and the first messages written for their empty chat Authentication and authorization; what every agent is told about the person
browser_sign_ins A person's kept sign-ins to sites, for the web browser The site, the organization, when it was last used, and the session itself (cookies and the site's localStorage) sealed through the vault under the person's own scope Starting a later browse that acts for them already signed in. The personal-data export lists the sites, never the session
conversations, messages, tool_calls Every chat on every surface The text people wrote, the model's answers and reasoning, tool arguments and results, a rolling summary of long threads The product's core function; history the person returns to
voice_calls Each connection of a voice call The caller's user id (none for a visitor), the agent, run, conversation and embed it belonged to, the provider and model, the provider's call id, the id of the vault key it was held with, the worker holding it, its heartbeat, start, end and hang-up times, its cost, seconds and guardrail hit count. No audio and no transcript - the call's words are ordinary rows in messages, each marked with a turn_id and, when the caller talked over it, interrupted Hanging up a call whose process died, and counting the organization's monthly call minutes
chat_files Attachments to a message Filename, type, size, the extracted text (parsed_content) and the path of the bytes on disk Answering about a file
generated_media One row per image, speech clip or video an agent generated The organization, the conversation it was made in where the run had one, the file's name, storage path and size - not its content, which is a file under MEDIA_DIR Counting each organization's generated media against its quota, and deleting it with its conversation or after its retention period
context_files Standing knowledge a builder wrote for agents Whatever the author put there - and it reaches the prompt verbatim. See Context files Instructions and facts an agent should always know
artifacts, artifact_versions and their files Pages agents published - reports, dashboards Whatever the page shows, which the run built from what it could read; the owner, the run that wrote each version and, when one is on, the public link's key, its settings (a password as a bcrypt hash) and how often it was opened - a count and a time, nothing about the visitor. See Apps Sharing a result under a stable link
agent_memory_files Notes an agent wrote about a person or a group chat Whatever the agent decided was worth remembering, keyed to person:<user_id> or a chat room Continuity between conversations
rag_documents, knowledge_bases and one vector table per collection Uploaded and synced documents, their chunks and embeddings The document text and its vectors, the file's original path in the source Retrieval
agent_runs, tool_approvals, run_manifests What each run cost and did The system prompt and last request handed to the model, tool arguments awaiting approval, the deciding person and their note Budgets, approvals, run history
agent_triggers Scheduled and event-driven runs The prompt and the event source's configuration and filter Running an agent without a person
app_admin_audit_logs Who changed access or spent money - the organization trail and the deployment admin's share one table Actor, impersonator, IP address, the action and a details map. The map mostly names fields, but some entries hold values: the impersonated account's email, the email of an account an admin deleted, a publish note Accountability. See Governance
notifications, notification_deliveries Every notification addressed to somebody, and its email delivery. A row is written even when the recipient has turned every channel off for that event - it is what stops the same occurrence being written twice - and is simply never shown or sent; only the delivery row is conditional The recipient, a pre-rendered summary (never a raw comment or secret value) and the typed variables an email re-renders from; a delivery row adds its channel, status, attempts and a sanitized failure reason - never the provider's own error text Alerts a person reads once, unattended. See Governance
notification_preferences, announcements Per-event channel toggles, and an app admin's own broadcasts A user id per toggle; a broadcast's sender, its body and the organizations and role it was addressed to - never the resolved recipient list, which is reconstructable from notifications only until that row ages out under the notification retention sweep - after which the audit entry behind it keeps a count, never the recipients Opt-out, and the composer's own record
embed_visitors, channel_identities, channel_sessions Strangers on a hosted page and people on Slack, Teams, Telegram or Mattermost A random visitor key; a platform user id, username and display name; the chat id Resuming the right thread
message_ratings Thumbs and comments on answers The rater and their comment Quality review
suggestions What the proactive work proposed to a person, and whether they took it The person, a title and a line of detail drawn from their recent requests, the routine prompt or profile it would set, and a key that stops a dismissed proposal being made again Proposals the person decides on
watches What a person asked the proactive work to watch in their own connections The person, which of their connections, what to look for and leave out in their own words, how often and when it last looked Watches the person sets up
ml_service_calls Every call to the ML services, and every transcription: dictation, a channel voice message, the API, a visitor's dictation on a hosted page or a widget The organization, who asked, which service, byte and unit counts, the duration and how it ended; for a transcription also the provider and model, the id of the vault key it was sent with, where it arrived, the agent and channel bot it was for and the recording's length. A channel voice message's row names the person the sender's account is linked to, and nobody for an unlinked sender. A visitor's row names nobody - not the visitor, who has no account, and not whoever published the page. Deleting the person, the agent, the bot or the key clears the link and keeps the row - none of what was submitted and none of what came back Usage reporting, and an operator's view of an integration that is failing
agent_workspaces, sandbox_operations Files an agent worked on and the log of what it ran For the state backend the files themselves, as JSON; for a container the session id and every command, target and result summary The sandbox. See The sandbox
organization_secrets, model_profiles, mcp_connections, channel_bots Credentials and where they point Sealed ciphertext only, with a hint; the provider, model and base_url in clear Reaching providers. See Secrets

messages.search_vector is a full-text index over the same content, and conversations.summary_messages is a model-written compression of it. Both are copies of the chat and go with it.

Generated media

An image, a speech clip or a video an agent generates is a file under generated_<org>/ and a generated_media row naming it. The row records the organization, the conversation where the run had one, and the size - so the organization's total is bounded by GENERATED_MEDIA_QUOTA_MB (5120), checked before each paid generation call. Every file counts, including one made with no conversation: a preview or a test run, a channel /btw aside, an API run that names no conversation, or a file whose conversation was deleted by a path that left it behind.

The file is deleted with its conversation, with the account whose conversation it was, by the daily sweep-generated-media job once it is older than GENERATED_MEDIA_RETENTION_DAYS (90) even while its conversation is still there, and by prefix with its organization. 0 turns either bound off. A file with no conversation is nothing a member could delete to free the space, so the same job deletes it after 7 days, whatever GENERATED_MEDIA_RETENTION_DAYS is, 0 included. A link to it - in a channel reply or an API response - stops working then.

Deleting a conversation, an account or an organization removes the rows in its own transaction and queues the unlink of the bytes as a background job in that same transaction, so the bytes go only if the rows do, and a restart after the commit does not lose the unlink. That job retries a prefix the store will not remove whole - an organization's - and a single file of a deleted conversation or account that the store refuses, because nothing else could find those bytes again. A file the store still refuses after the job's last attempt stays under generated_<org>/ with no row until its organization goes, and the report below counts it.

The sweep deletes a file first and its row only once the file is gone; a file the store refuses keeps its row, and the next sweep tries it again.

Files generated before the table existed have no row. They are still served, but they are not counted against the quota and the sweep cannot see them: they go with their organization, or by hand, and the data-protection report counts them among the files with no row.

Outside the database

Store Holds Deleted when
MEDIA_DIR on the API host (media_data volume) Chat attachments under <user_id>/, avatars, embed logos, the images, speech and video agents generated under generated_<org>/, and a temporary copy of each document under _rag_tmp while it is parsed A document is deleted through the product. A chat attachment's bytes are unlinked after the commit that deletes its conversation or its owner. A generated file goes with the conversation it was made in, after GENERATED_MEDIA_RETENTION_DAYS (90) whatever became of the conversation - after 7 days when it has no conversation - and with its organization; see Generated media. See What deletion reaches
SANDBOXD_WORKSPACE_ROOT on the sandbox host The files of every container-backed workspace The conversation is deleted, or SANDBOXD_WORKSPACE_TTL sweeps them; unset, they are kept indefinitely. See How long anything survives
Redis Rate-limit buckets keyed on the caller - for a public surface that is the plain IP address, in the key, for the window's TTL; trigger and channel deduplication keys; OAuth exchange state; staged invitations; a browse's live view - its latest screenshot, its steps, and what the person holding it clicked and typed until the browse reads it; a person's replayable browse steps - the addresses and labels of what their finished browses pressed, never what they typed On expiry: a live view an hour after the browse's last frame, replayable steps a fortnight after the task was last done, everything else within minutes
background_jobs in PostgreSQL Each background job's payload, its last error and an asynchronous API run's answer The retention sweep removes a finished job after BACKGROUND_JOB_RETENTION_DAYS (14). Payloads are ids and paths, with two exceptions: an event-triggered fire's event_context - the sender, subject and body of a Gmail message, a GitHub issue, a webhook payload - and an asynchronous API run's prompt and answer travel in the job until then. The worker's logs pass the same redaction filter as the API's
Logfire, when configured Traces of every run The provider's retention. Today a trace carries the full prompt, output and tool arguments - see Traces
Your SMTP relay Invitations, magic links, approval requests, budget alerts, usage reports, and the notification inbox's own mail - a run finishing or failing unattended, a document's ingestion completing or failing, a security event or configuration change, an admin's own broadcast Whatever the relay keeps. Approval mail names the agent, the tool and a link, not the tool's arguments
Backups A pg_dump is the whole database; the media volume is the files Your backup expiry. Erasure never reaches a backup already taken - see Backups

What leaves the deployment

Nothing leaves unless a row or a setting names a destination. This is the complete list of destinations, with the configuration that decides each.

Destination What is sent Decided by Location and terms
The chat model The conversation so far, attachments pasted or described, retrieved chunks, tool results; once a night, for the proactive work, a person's recent requests, their profile and what their agent noted about them A model profile: provider, model, base_url and a sealed key. Twenty-seven providers; ollama and litellm are keyless and reached at an endpoint you host, and openai, anthropic, google, huggingface and others accept a base_url, so an EU endpoint or a gateway is a field, not a fork. An openai or azure profile on the Responses API sends store: false, so the provider keeps no stored, retrievable response unless the profile sets openai_store; OpenAI's abuse-monitoring retention still applies unless the account has zero data retention The provider's. Verify per profile
The embedding model Every chunk of every document in a collection, and every retrieval query Per collection, and only there: embedding_provider (openrouter, openai or ollama, from the catalog) and, for the first two, the vault key embedding_secret_id that pays. There is no deployment-wide embedding key; a keyed collection without one refuses to index or search. ollama is keyless and reached at the local service the collection names (embedding_endpoint_id), a host you run The provider's, or your own host. A permanent choice
LlamaCloud The whole document A collection whose pdf_parser is llamaparse; it must name a vault key (llamaparse_secret_id), there is no deployment key. The default pymupdf parses in the worker LlamaCloud's, if used
An OCR server Rendered pages of a document A collection whose pdf_parser is liteparse and whose ocr_endpoint_id names a local service; with none, OCR runs in the worker Your own host - a local service is on the deployment's network by construction
An image-description model Images inside documents A collection's image_description_model That model provider's
Web research The search query the agent composed web_research.method on the spec: duckduckgo (no key), tavily, brave or exa (a search secret each), or native, where the chat model provider searches The search vendor's, or the model provider's
Web fetch and the web browser The URL; for the browser, what it types into the sites it opens The capability on the spec; the browser also needs a CDP endpoint on BROWSER_CDP_ALLOWED_HOSTS, empty by default The site fetched; the browser host and the sites it opens
An agent's browser proxy or hosted browser Everything the browser loads and sends, the way the browser in the deployment otherwise would - pages, form contents, cookies, sign-ins - plus the proxy's or the service's own record of it browser_use.connection on the spec (#2130); built_in by default The proxy, or the hosted browser service (Browserbase, Steel or another), under the organization's own contract with it
The web browser's steps Every step of a browse goes to the agent's own model: the page's URL, its title, the labels of the elements in view and up to 4,000 characters of its visible text, plus a screenshot when vision is on - which may be the contents of an internal system. What a field holds is not repeated: the element table says only that a field is filled The browser_use capability on the spec The agent's model provider, as for the conversation
The sandbox, outbound Anything in the workspace, to any host - the workbench runtime has a network, a shell and curl The sandbox capability and a runtime with needs_network; command approval gates what runs, not where it connects Wherever the command went. Egress control is the sandbox host's firewall, not a setting here
An MCP server Tool arguments and results mcp_connections.url, per organization or per person The server's operator
mem0 The memories written for a person or a chat The memory_mem0 capability's base_url, which must be in MEM0_ALLOWED_HOSTS The mem0 host you allow
Logfire Spans for every request and run LOGFIRE_TOKEN deployment-wide; an observability token on a spec, or logfire_token_secret_id on an environment, redirects those runs to another project. LOGFIRE_BASE_URL picks the US or EU deployment. Unset everywhere, nothing is sent Pydantic's, US or EU
Speech to text, image generation A channel voice note, a recording dictated in /chat, or an anonymous visitor's recording on a hosted page or a widget; the prompt The provider a recording resolves to - the bot's own pair, the agent's override, or the organization's default, which a bot inherits unless switched off - sent with the vault key named beside that pair - an API key for groq, mistral or openai, organization-visible at the organization and on a bot; for dictation in /chat, also the organization's engine set to server, and for a page or a widget its own dictation: server. A profile for google or openai for images. A dictation's audio is not stored here. With browser on a page or a widget, the visitor's browser sends the audio to its vendor and none of it passes through this deployment The provider's, or your own host for images; for browser, the browser vendor's
A realtime voice provider During a voice call: the caller's audio, the agent's instructions and tool definitions, tool results, and the conversation's earlier turns as text when a call starts or reconnects The agent's Voice section: provider, model (or an Azure deployment) and the vault key secret_id, published; with no Voice section the agent takes no calls. Over WebRTC (OpenAI) the audio flows between the browser and the provider directly; over the relay (Azure, Gemini, xAI) it passes through this deployment without being stored. No audio is kept here, and the session keeps transcripts only The provider's
Text to speech The text to be spoken, and any direction on how to speak it The text_to_speech capability's provider: openai, elevenlabs or google, with the binding's key The provider's
Video generation The prompt, and any starting image The video_generation capability's provider: google, byteplus, xai or alibaba, with the binding's key. google stores the interaction (store: true), because Gemini delivers a clip by address only from a stored one; the other three are not asked to The provider's
Slack, Teams, Telegram, Mattermost The agent's replies; a new member's email address, to find them for a welcome in Slack, Mattermost or Teams A channel_bots row with its token in the vault The messaging vendor already holds the chat
Google sign-in Nothing outbound; Google returns the email, name, picture and account id GOOGLE_CLIENT_ID Google's
Your SMTP relay The mail above SMTP_HOST, SMTP_TLS Yours

Sync connectors run the other way: a Google Drive or S3 source pulls documents in, authenticated by a connector secret, and from then on the documents are the deployment's copy and follow the rules above. The people who can read what a source ingested is a decision the source row makes.

Controls, and where each is proved

Each row names the mechanism in code and the test or page that pins it, or the issue that will. A row whose last column is an issue is a gap, stated as one.

Control Mechanism Proved by
Tenant isolation organization_id on every organization-owned table, resolved from X-Organization-Id to a membership on every request; child rows reached only through their parent's check tests/integration/test_conversation_tenant_isolation.py and its siblings; Permissions
Access to a row Three layers: deployment admin, organization role, per-resource grant through resolve_access. A control the caller may not use is not rendered tests/api/ refusal tests; Permissions
Reading another person's chat Owner, an explicit share, or the deployment's app admin - never an organization role. A project's chat is read through the project alone: whoever may see the project, its starter included, and nobody once they may not. Conversations have their own check, ConversationService._may_read, rather than the grant formula admin_conversations.py requires is_app_admin; tests/integration/test_conversation_tenant_isolation.py
Credentials at rest Envelope encryption per organization, versioned master keys, rotation with a dry run Secrets, four guarantees pinned by tests
Content at rest Not encrypted by the application. Postgres data, media_data and the sandbox workspace root rely on disk or volume encryption you provide. Uploaded and chat files are the exception when FILE_STORAGE_BACKEND=s3 and FILE_STORAGE_S3_ENCRYPTION is sse-s3 (the default) or sse-kms under a key you hold: each write then asks the store to encrypt them. Its third mode, none, sends no encryption header and exists for an S3-compatible store with no KMS Operator control. Configuration; agenticos cmd doctor prints which backend a running deployment is on
In transit, inbound HTTPS at your proxy; Strict-Transport-Security when ENVIRONMENT=production; session cookies httpOnly, and secure from the request scheme on sign-in and refresh. The password-change route sets secure only in a production build Deploy; frontend/src/app/api/auth/login/route.ts
In transit, to the stores POSTGRES_SSLMODE and REDIS_SSL; agenticos cmd doctor reports whether the connection it made was encrypted Encrypted connections; tests/integration/test_store_tls.py
In transit, to providers HTTPS to every catalogued endpoint. A custom base_url is refused without a host or with credentials in it, but http:// is accepted, for an Ollama or a gateway on the deployment's own network; a plain-HTTP profile pointing off that network sends prompts and the key in clear. Item 4 of the checklist lists every such profile refused_field("base_url", ...) in the model profile service; operator control for the scheme
Secrets in responses, logs, audit, exports No endpoint returns a plaintext; SecretStr everywhere; specs reference secrets by id Secrets
Personal data in logs app/core/logging.py redacts email addresses, JWTs, API keys, bearer tokens and password= pairs from every log record, API and worker alike tests/test_logging.py; the worker installs it in app/worker/__main__.py, and each ingestion child in app/worker/child.py (#440)
Personal data reaching the model The guardrails capability redacts IBANs, card numbers, US social security numbers, phone numbers and email addresses from prompts, answers, tool results and tool arguments when configured Capabilities; its tests under tests/
Personal data in a failure column rag_documents.error_message and friends record the stage and class, never the client's text app/services/rag/failures.py (#423)
Accountability Audit entries share the acting transaction and fail closed; impersonation names both people; bulk exports are recorded Governance
Audit export GET /audit/export, CSV or JSONL over a window, gated on audit:read and recorded in the trail itself Governance (#1422)
Tamper evidence on the trail Every entry joins a per-organization hash chain, and each chain carries a checkpoint at its high-water mark, so a rewritten entry, a dropped tail and a deleted chain are all detectable. agenticos cmd audit-verify walks them and exits non-zero on a break Governance (#1622, #1648). Detection, not prevention: whoever holds the database's own credentials can re-forge a chain or drop the checkpoint's trigger
Traces observability.content per agent: full records everything, none records timing, tokens, cost and tool names only, and a specialist of that agent inherits it Environments (#1413); a redacted middle ground was decided against, #1616
Retention on a schedule Per organization and per class - conversations and their files, runs and manifests, workspaces, agent memory, uploaded documents and audit - within a deployment-wide default, ceiling and audit floor. A daily sweep hard-deletes and records counts, never content. Backups and anything already shipped to an external collector are outside it. notifications is not one of those classes: it sweeps on its own fixed schedule instead, a read row after 90 days and any row after a year regardless - announcements themselves are excluded, so what was sent stays answerable from the audit trail after its deliveries age out. Nor is generated media, which goes after the deployment-wide GENERATED_MEDIA_RETENTION_DAYS Retention; test_retention.py, tests/integration/test_retention_sweep.py; the notification sweep is tests/integration/test_notification_retention.py (#1598, Decision 8); generated media is test_generated_media.py and tests/integration/test_generated_media.py (#2310)
Erasure of one person Account deletion reconciles what would block it, unlinks the attachment bytes after the commit, and purges what no cascade reaches - the notes agents wrote about them, their platform identities, their user-scoped workspaces. Memory erasure is also a call of its own, and reaches mem0 What deletion reaches for what is deliberately retained; test_personal_data.py
Access to one's own data A person reads what every agent here has written down about them at Settings → Memory, and may suppress a note, restore it or delete it. Reading somebody else's store is the deployment administrator's alone - not an organization role - and is audited with the actor, the tenant, the subject and a reason, never the content. External (mem0) stores are named rather than listed Reading it, and erasing it; test_memory_self_service.py. Everything else held about them comes back from GET /me/data/export, bounded and audited (#1421)
Enterprise identity Google sign-in, passwords, generic OIDC against the provider you already run - configured by discovery from OIDC_ISSUER alone - and your own directory over LDAP or Kerberos. The directory's address, display name and stable identifier are kept on the account; its groups are read at each sign-in and not stored, only the memberships the organizations' mappings make of them. No SAML or SCIM Directory sign-in and groups; OIDC_ISSUER, LDAP_URL
The controls matrix a security review reads Security maps each control to its mechanism and the test holding it, framed against HIPAA §164.312 and SOC 2 CC6–CC8; this page and Rolling it out are the rest Security (#1412)
Public surfaces A hosted page's visitor key is random, never derived from the person; admission and uploads are rate-limited per address, the address held in a Redis key for the window and nowhere else Channels
Legal notices The deployment's own Terms and Privacy URLs replace the built-in pages The deployment

Traces

instrument_pydantic_ai() runs with the library's default, so a span holds the user's message, the model's answer and every tool argument and result. With LOGFIRE_TOKEN unset, no observability token on any spec and no logfire_token_secret_id on any environment, nothing is sent and the trace id is still recorded locally. A deployment that needs traces without the content sets the agent's observability.content to none, which records timing, tokens, cost and tool names and no message text, and which a specialist of that agent inherits, whether its author wrote it inline or the run's model invented it.

There is no third mode between the two. An export scrubbed by a PII filter is a guarantee nobody can audit - one identifier the filter misses has left, and the operator believes it did not - so the choice is deliberately the whole content or none of it (#1616).

What deletion reaches

Deleting is what the product does today when somebody asks; scheduled retention is #1420.

Action Removes Leaves
DELETE /conversations/{id} (the owner) The conversation, its messages, tool calls, ratings, shares and chat_files rows, by cascade - a voice call's transcript among them, since its turns are messages; the attachments' bytes under MEDIA_DIR, collected before the delete and unlinked after it commits; the images, speech and video its agents generated - the rows in the delete, the bytes by a job queued with it; the thread's offloaded media objects by prefix; a container workspace through purge_for_conversation Run rows and manifests that named the conversation keep their prompt copy; a voice_calls row keeps its times and cost with the conversation link cleared
DELETE /memory/person/{user_id} (the person, or members:manage) Every agent_memory_files row keyed to the person across the organization's agents, and the same in each bound mem0 store Notes keyed to a group chat the person spoke in
DELETE /users/{id} The account, its sessions, its personal organization and personal collections with their vector tables and files, by explicit teardown; conversations outside any project and chat files by cascade; their bytes, unlinked after the commit; the images, speech and video agents generated in those conversations, the rows in the delete and the bytes by a job queued with it; and what no cascade reaches - the notes agents wrote about them (owner_key is a string, not a foreign key), their platform identities and their user-scoped workspaces Audit entries naming the actor id and, for some actions, the email - retained deliberately, because an entry with its actor removed is worse than one naming a deleted account. Messages in conversations somebody else owns, and the project chats they started, kept for the team with no starter, with the media generated in them; their turns there are drawn as a former member's. The media the person generated with no conversation - previews and test runs, channel /btw asides, API runs that named no conversation - which no row links to them; it goes after 7 days
Deleting a document or a collection The rows, the vector table and the stored file, through a durable flow after commit Nothing, once the flow has run; sync_logs counts remain
Deleting an organization Everything scoped to it, with the same deferred teardown - its generated media by prefix, including files written before they were recorded Personal collections that merely carried the id

None of these reach a backup. A restore brings back what was erased, so the backup expiry is part of the retention policy and is written down with it.

What a deployment has to decide and obtain

The software cannot supply any of these. Each is evidence the review will ask for, distinct from the technical capability that makes it possible.

  • A processing agreement with each configured sub-processor - every provider a model_profiles or organization_secrets row names, the embedding provider, LlamaCloud if a collection uses it, the search vendor an agent's method names, the mem0 host, Logfire, the SMTP relay and Google if sign-in is enabled.
  • A data location statement per provider, matched to the base_url each profile actually uses. A provider with an EU endpoint is only in the EU if the profile says so.
  • A training exclusion per provider: the account setting or contract term under which API data is not used for training. The platform's own position is one sentence - it trains nothing - and the rest is theirs.
  • A processing agreement with Vstorm, only if Vstorm operates the deployment.
  • A retention schedule for conversations, files, memory, documents, runs and audit, and the backup expiry beside it. Until #1420 enforces one, retention is a manual deletion.
  • Disk or volume encryption on the database host, the media volume and the sandbox host, since the application does not encrypt content itself - and an egress rule on the sandbox host if agents may run commands.
  • The legal pages the deployment links to, and who answers an access or erasure request. The mechanisms are here - GET /me/data/export and account deletion - but who receives the request and within what deadline is the controller's to state.

Verifying one deployment

Reproducible checks, from the host, against the running deployment. Each prints facts the review can attach; none prints a credential or a person's data. Run them from backend/, or through docker compose exec api.

# 1. Can it run, and are the store connections encrypted? `postgres` reports
#    the TLS state of the connection the doctor itself made.
uv run agenticos cmd doctor

# 2. Every sealed credential still opens under the configured master keys.
uv run agenticos cmd vault-rotate --dry-run

# 3. The audit trail's hash chains and their checkpoints, recomputed. Exits
#    non-zero if any chain has been altered.
uv run agenticos cmd audit-verify

# 4. Everything this deployment actually configured: the settings that decide
#    what leaves, every provider and endpoint an agent can reach, the
#    credentials held by purpose, the collections and who embeds them, the
#    servers on your own network, the MCP servers, trigger portals, sync sources
#    and channel bots, the capabilities that reach an address of their own,
#    where runs are traced and how much content a span carries, how much of each
#    store a retention period would reach, and the files under `MEDIA_DIR` no row
#    points at any more.
uv run agenticos cmd data-protection-report --older-than 365

Item 4 is the one to attach. It prints configuration and counts and never content: no message text, no document, no secret value and no hint of one, and a setting holding a credential is reported as set or unset rather than printed. --older-than is the retention period under consideration, in days, and the last column of its retention table is what that period would already have removed.

Its last section counts the bytes no row names any more. Deleting a conversation or an account now unlinks their attachments after the commit, so that path no longer adds to it; what it still finds is what predates those deletes, plus an unlink a best-effort sweep did not complete.

The parse scratch directory is excluded, having no row by design. Generated media is counted since generated_media records each file, so what shows up under a generated_<org> directory is mostly a file generated before that table existed, which goes only with its organization. It can also be a file whose unlink failed after its row was deleted, or one still being saved, before its row is written. Everything else counted there is bytes the product can no longer find and cannot delete. It reports a directory and a count rather than a filename, because a stored path keeps the name the file was uploaded under.

Two sections are read off what runs rather than off a table. Capability destinations lists the capabilities an agent is bound to that reach an address of their own - web_research searches through DuckDuckGo with no credential and therefore no row anywhere else in the report - and Tracing is read off each runnable version: the default one, and the one each named environment pins, because a run through that environment uses that version's observability rather than the default's.

What no command can produce is the other half of this page: the agreements, locations and training exclusions of the previous section. Attach those beside the output.

Open conditions for a first rollout

Stated for the deployment this page was written against, and true of any deployment until each closes.

In the code, and standing:

  • Traces carry full content unless an agent sets observability.content to none. There is no filtered middle ground and there will not be one: a partly-scrubbed export is a guarantee nobody can audit (#1616).
  • Content at rest is the operator's. Message bodies, documents and their vectors, and sandbox workspaces are plaintext columns and files; the vault seals credentials, not content. Uploaded files can go to an S3-compatible store that encrypts them server-side, but the database and the media volume are protected by disk or volume encryption or not at all.

Closed, and answered above rather than here: the audit trail's tamper evidence (#1622, #1648), the per-agent trace content mode and its inheritance by specialists (#1413, #1699), tracing in the process that runs a fired agent (#1700), the HIPAA and SOC 2 controls matrix in Security (#1412), erasure and export of one person's data (#1421), the S3 backend with server-side encryption (#1423), and generic OIDC sign-in (#1419). Traces have no filtered middle ground and will not get one (#1616); none is the answer for a deployment that may not export content.

In the deployment, decided by its operator: the agreements, locations, training exclusions, retention schedule, backup expiry, disk encryption, sandbox egress and legal pages of the previous section. Each is the kind of evidence data-protection-report deliberately cannot produce.

A review that finds every row above either closed or accepted in writing has what this page can give it. The rest is the deployment's.