Shared Memory and the Access Model
One CortexDB for a tenant's sources, applications and org brain. Every memory has a home, every read is decided against it, and the access API says who may read what and why.
CortexDB 0.14.0 adds shared memory: one server holds a tenant's sources (Slack channels, Drive folders, tickets), its applications' memories (one home per person in each application) and its org brain (teams, groups, rollups), and one access model decides every read and write across all three.
The model fits in three sentences. Every memory has a home: the scope it was written to. Every request is made by a principal (a person, an agent acting for a person, a service), carried by a signed token. Every route that returns stored memories checks each one against that principal at its home, so a memory you may not read is never returned, whichever route you ask through.
When to use it
Use the shared-memory features when several people, agents and applications use one memory and they
must not all see everything: a company brain, a support desk with per-agent notes, an application that
keeps each customer's memory apart and still shares reference material. A single application that
writes and reads its users' scopes with one key needs none of these features, but on
cloud_shared_saas the operator key's default changed in 0.14.0: see
Operator Modes and Break-Glass.
| You want to… | Read |
|---|---|
| Model teams, people and private spaces | Org Trees |
| Change who may read what, safely and in one step | Change Sets |
| Give each person and agent their own token, or use your identity provider | Identity and Tokens |
| Decide what the operator key may read | Operator Modes and Break-Glass |
| Give applications per-person homes, sync sources with their permissions, handle agent writes | Applications and Sources |
| Erase a person, an outside party or a tenant, or keep data under a legal hold | Erasure and Legal Holds |
| Connect ChatGPT, Claude, Cursor or another assistant | Connecting AI Assistants |
| Upgrade an existing server | Upgrading to 0.14.0 |
Who may read and write what
A scope's record holds its roster: who holds which role there. There are four roles:
| Role | Can |
|---|---|
| Owner | Administer the record: its roster, its tree attributes, its children |
| Writer | Write memories there |
| Reader | Read the memories there |
| Summary | Read only rollup totals and items marked summary_safe (new in 0.14.0) |
A roster entry can name a person, an agent, a service or a set (a named group of principals), and
can carry an end date (until) or a condition (when), such as a purpose the read must declare. Sets,
summary, until and when are written through change sets; the
older registry routes (POST /v1/scopes, PUT /v1/scopes/members) take only owner, writer and
reader entries for one principal each.
Capabilities still come first: the deployment preset and the token's caps and scopes decide what
kind of request a credential may make at all, and the roster decides where. See
Authorization for the capability stack.
Private regions
A record marked private starts a private region: a person's own memory, a confidential group, a
private channel. A private region is read only by name, by the people its roster names. No read that
starts elsewhere walks into it, whatever view it asks for, and nothing CortexDB synthesizes outside it
draws on it. Under the deny operator mode even the operator key needs a break-glass header to read
one, and the region's people are told first (Operator Modes).
The delivery gate
Since 0.14.0, every route that returns stored rows (recall, answer, compose, events, facts, beliefs,
exports, the lifecycle stream) checks each row's home for the reader, and, for a memory an agent wrote
or CortexDB derived, every source it was made from. A row the reader may not read is left out; a read
of it by id answers 404, the same answer as an id that does not exist. A fact derived from a private
conversation is therefore never served to someone who could not read that conversation.
Ask the server who may read what
The access API answers access questions and reads no memory:
| Route | Answers |
|---|---|
POST /v1/access/check | May this principal read, write or administer this scope, or read this one memory? |
POST /v1/access/explain | Why: each link that decided, with the path of records and entries it used |
POST /v1/access/lookup/scopes | Which scopes under a prefix may this principal read? |
POST /v1/access/lookup/principals | Who may read this scope? |
GET /v1/access/limits?scope=<root> | The limits the server enforces for a tenant |
POST /access/v1/evaluation, POST /access/v1/search/resource | The same decisions over AuthZEN 1.0, for a policy enforcement point you already run; GET /.well-known/authzen-configuration describes them |
A caller asks about its own token by default. An Owner of the scope, or the operator key, can ask about
another principal by naming it in chain. A caller that holds no role at the scope gets one uniform
denial (not_authorized), so the API never tells it whether the scope exists or is private.
# Alice's own token: may she write to the engineering team's scope?
curl -sS -X POST "$CORTEX_URL/v1/access/check" \
-H "Authorization: Bearer $ALICE_TOKEN" -H "X-Cortex-Actor: user:alice" \
-H "Content-Type: application/json" \
-d '{"scope": "org:acme/team:eng", "role": "writer"}'{
"allowed": false,
"denied_by": "person",
"links": {
"capability": {"allowed": true, "via": [{"kind": "capability", "field": "deployment.allow"}]},
"confinement": {"allowed": true},
"tenant": {"allowed": true, "via": [{"kind": "tenant", "record": "org:acme/team:eng", "field": "billing_tenant"}]},
"person": {"allowed": false, "reason": "insufficient_role",
"via": [{"kind": "entry", "record": "org:acme/team:eng", "field": "members", "index": 2,
"who": "user:alice", "role": "reader"}]}
},
"names": {"user:alice": "Alice Kim"},
"role": "writer",
"scope": "org:acme/team:eng",
"until": null
}Alice is a Reader of org:acme/team:eng, so a write there is refused by the person link, and the
answer names the roster entry that decided it. The same question over AuthZEN, asked with the operator
key:
curl -sS -X POST "$CORTEX_URL/access/v1/evaluation" \
-H "Authorization: Bearer $CORTEX_API_KEY" -H "Content-Type: application/json" \
-d '{"subject": {"type": "user", "id": "alice"},
"action": {"name": "write"},
"resource": {"type": "scope", "id": "org:acme/team:eng"}}'
# {"context":{"denied_by":"person"},"decision":false}Reading several scopes at once
In an org tree a read does not walk the tree from a shared scope; it names what it reads. The four read
routes (/v1/recall, /v1/recall/stream, /v1/answer, /v1/compose) take:
extra_scopes: up to 40 more scopes, searched in parallel in the same pass;coverage: true: report which scopes were searched and any gap (a bound, a deadline, a private scope left out, a source whose last sync is old);mounts: "default": add the scopes your application mounts (Applications and Sources);beliefs: "merged": rebuild the pack's beliefs across the whole read set for this reader;purpose: what the read is for (1 to 64 characters of[a-z0-9_-]). A roster entry whosewhenis{"purpose": "<id>"}admits a read only when it declares that purpose.
curl -sS -X POST "$CORTEX_URL/v1/recall" \
-H "Authorization: Bearer $ALICE_TOKEN" -H "X-Cortex-Actor: user:alice" \
-H "Content-Type: application/json" \
-d '{"scope": "org:acme/user:alice", "extra_scopes": ["org:acme/team:eng"],
"query": "payments migration", "coverage": true}'The pack's coverage lists the scopes searched (org:acme/user:alice, org:acme/team:eng) and its
gaps (none here).
Consistency tokens
A write can return a consistency token, and a later read can demand it, so the read sees the write
on whichever node serves it. Add ?consistency=1 to a write (a change set's activation always returns
one), then send "consistency": {"at_least": "<token>"} on a read, or ?at_least=<token> on a GET
read. The read waits until the node has caught up, up to CORTEX_CONSISTENCY_WAIT_MS (10 s by default,
30 s at most), and answers 408 WAIT_TIMEOUT past it.
curl -sS -X POST "$CORTEX_URL/v1/experience?consistency=1" \
-H "Authorization: Bearer $BOB_TOKEN" -H "X-Cortex-Actor: user:bob" \
-H "Content-Type: application/json" \
-d '{"scope": "org:acme/team:eng", "modality": "conversation",
"content": {"kind": "message", "role": "user", "text": "The payments migration starts on 12 January."},
"idempotency_key": "eng-decision-0001"}'
# {"event_id":"evt_…","status":"captured",…,"consistency_token":"ct1.eyJy…"}How to turn it on
Shared memory is part of 0.14.0; there is nothing to switch on. The first start of 0.14.0 arms the data
directory for the new record formats, one way: back up first (Upgrading).
GET /v1/admin/version lists each feature under its capability (access_check_v1,
access_changesets_v1, authzen_v1, org_tree_v1, directory_v1, scim_v1, …): gate a client on the
capability, not on the version.
What a deployment that already ran 0.13.x sees change, and the switches that serve the old behaviour while you move clients, are in Upgrading.