Access control in retrieval
Who may see what is decided inside the retrieval query, before any model is called. Labels on memory, a platform-held clearance ledger, owner walls, custody keys, matter focus and verified identity are compiled into one filter, and the filter fails closed.
The principle
A retrieval system that hands the model everything and asks it to be careful has no access control; it has a request. Yohanun puts the decision before generation: every memory carries labels, every asker resolves to a principal with clearances, and the two are compiled into the vector query itself. Material the asker may not see is never a candidate, so there is nothing for a clever prompt to extract. The same filter applies to search, chat, documents, the as-of read and reads by id.
Three labels on a memory
| Label | Meaning | Who gets through |
|---|---|---|
acl_matter (compartment) | The subject wall: a matter, a deal, a case. Default public. | A principal cleared for that compartment. |
acl_classification | Sensitivity, 0 to 3. | A principal whose level is at least that. |
acl_owners | The people wall. Unset = shared across the tenant. | A principal whose identity is among the owners, or who holds a custody key that covers it. Anonymous askers get nothing owned. |
Labels travel with everything derived from the memory: document chunks carry the upload's labels, facts
carry their chunk's, and a stored chat answer inherits from its sources (below). A tenant can insist that
every write names its compartment (require_compartment): a write with none is then a 400, not
a public memory by default.
Who is asking
The key authenticates the tenant. Inside it, the principal is whatever your application names on the request:
identity on memory and document calls; on chat, context.authority.principal_id,
then context.principal_id, then user_id. With verified identity
switched on for the tenant, the principal is instead the subject of a JWT your own identity provider signed,
sent as X-Identity-Token and checked against your JWKS (issuer, audience, expiry). A field naming
someone else is ignored; no valid token means 401, never a fallback.
The clearance ledger
POST /api/access/clearances/grant-matter {"principal_id", "compartment", "reason"} → 409 on conflict
POST /api/access/clearances/revoke-matter same body
POST /api/access/clearances/set-classification {"principal_id", "level": 0..3}
GET /api/access/clearances/{principal_id} {managed, compartments, clearance}
GET /api/access/clearances/refusals every grant the conflict graph refused
The ledger is append-only and platform-held. A principal with any row in it is managed: the ledger
is the only source consulted, and anything the calling application asserts about them is ignored. A principal
with no rows is unmanaged, and for them the application may assert
authority: {"matters": [...], "clearance": n} on the request, which is the right shape for a
system that already has its own access control and wants Yohanun to honour it. Revocation is effective on the
next request. Grants are checked against a conflict graph (two sides of one dispute cannot be held by one
principal); a refused grant is recorded, never silently dropped.
Fail closed. If the ledger cannot be read, the asker is treated as cleared for public material at level 0 until it is back. The platform never falls back to the asserted view for a principal it manages.
Owner walls and custody
acl_owners is per person; several owners make a shared private memory. Holding is separate from
owning: a memory can be stamped with a custody key at creation, and who holds that key is another
append-only ledger (/api/access/ownership/{keys,grant,revoke,transfer,holders/{key},held/{pid}}).
Share, revoke or transfer custody is one row and touches no data, which is how an agent is retired or a
matter handed over without rewriting anything. Keys declare their root at creation: rooted to a person or to
the organisation, and only the root moves holdership.
Matter focus
focus_matter narrows a request to one compartment: only memory labelled with exactly that matter,
not other cleared matters and not public, and only if the principal is cleared for it. A focus on an uncleared
matter matches nothing, never an error and never a widening. It is honoured even for a managed principal,
because it can only reduce what is visible. Available on search, as-of, reads by id and chat
(context.focus_matter).
What a stored answer inherits
Chat stores both sides of a turn as memory. The answer inherits the wall of everything it drew on: the most
sensitive compartment, the highest classification, and the owners common to every owned source. Where no single
key covers them all, or the turn drew on more than one compartment, the answer belongs to the asker alone; the
response says so in turn_visibility. Your application can label the turn further with
context.storage = {acl_owners, acl_matter, acl_classification}, and that merge only ever
narrows: a label can make a turn more private, never less. A malformed label is a 400 at request time.
Per-tenant strictness
Two flags, off by default, set by the operator: require_compartment (every write names its wall)
and audit_strict (a read whose audit row cannot be written is refused; see
audit trail per read).
Why this matters. This is the mechanism behind Enterprise RAG with access control: the retriever is where a RAG system leaks, so that is where the decision is made.
Get a key
Yohanun is in invited beta. Tell us what you are building and we send a key with a developer account; a memory-profile key for your own model, or a chat-profile key if you want the platform to run the model and meter it. There is no card and no self-serve sign-up.
Request an invite