The security surface of the conversation layer — what Query Engine holds itself, and what it merely presents. Injection into the input was already covered by grounding (what was found is data, not commands), and injection through the tool arguments by retrieval (the identity for ACL comes from the session, not from the model's arguments, so spoofing access is impossible); rights over knowledge and authentication are held by Auth. Here are four things the conversation itself owns: access to the thread itself, the answer as an untrusted output channel, the egress of context to the model, and the traces of queries. The rest — authentication, rights, limits, model config, rendering — belongs to the neighbors; the map below separates one from another.
conversation_id addresses a thread but does not prove a
right to it. Access is the owner's alone: on read, continuation
(resume),
and deletion, the endpoint checks the conversation owner's
user_id against the current one — on top of
RBAC, with the same “is it over your own object” pattern that
Auth
applies to personal agents and keys. The foundation is already in the
model:
conversations
carry the owner's user_id.
conversation_id grants no access: a matching
user_id does. Without this check, continuing an exchange by
someone else's identifier is a textbook IDOR: A would be reading B's
messages.
Grounding
closed the input — what was found is not a command. The mirror is the
output: the model's answer is untrusted for the surface
that renders it too. A string planted in a found fragment can make the
model output a link or an image like http://attacker/?leak=…;
the chat, auto-loading the resource, would drag context outside. So the
answer rendering neutralizes the markup: it does not execute it, does not
auto-load external resources (images, fetch), and escapes the output.
The user picks a chat model, and generation sends it the packed context — the found company fragments. If the model is in the cloud, those fragments leave the perimeter and go to the provider. The Admin model allow-list already narrows the choice — but by cost and availability, not by data sensitivity.
A conversation leaves traces — the history (conversations /messages with the texts of queries and answers) and retrieval_trace (what was searched and what was found). This is company data: it lives under the same rights and cascades away when a user is deleted; we do not write the full context and secrets to logs.
Viewing traces is two different cases, not to be confused.
Own history: a user browses their own conversations —
what they asked and what was answered; visible to the owner only (by
user_id, like
thread ownership), and the
screen itself is the Web App shell. Admin access to
others' conversations and retrieval_trace is a rare,
oversight right v2: not pursued in the first
iteration.
What this layer does not hold but merely respects: who the user is and what they can see — established by Auth (the surface presents a token, it does not dictate the identity), while application limits and cost (per-user rate limit, a separate bucket for expensive LLM queries) are designed there too and marked v2. Query Engine presents and honors them; it does not build the scheme.