Identity and visibility
How credentials bind to agents and end users, and how per-memory visibility decides who can recall, update, share or delete a memory.
Two questions are answered on every request:
- Who is calling? The credential proves it — an API key or an OAuth access token. Request fields never do.
- Who may see this memory? The memory's
visibility, its owner agent and its end user decide.
Credentials are bound to agents
Every credential is bound to a list of agent IDs when it is created. The first one is the credential's default agent.
| You send | What happens |
|---|---|
no agentId | The credential's default agent acts. This is the normal case. |
an agentId the credential is bound to | That agent acts. Use this only when one key serves several agents. |
an agentId the credential is not bound to | 403 with code policy_denied (rule identity-binding). |
A key created without any agent IDs acts only as itself (apikey:<keyId>). The SDKs never send an agent ID unless you configure one, and the MCP tools only accept agentId so that a multi-agent key can pick one.
Binding is set when the credential is created
In the console, choose the agents (and whether the key may act for end users) in the Create API Key dialog. Programmatically, pass agentIds and canActForUsers when minting a key or upserting a grant — see Provisioning.
End users: userId
userId is the end user a memory is about — a customer, a player, a patient — never the agent. It is accepted on append, retrieve, by-ID reads and the user-profile endpoints.
- A credential may name a
userIdonly if it hascanActForUsers(a backend serving many end users), or if theuserIdequals the user the credential itself proved (an OAuth sign-in). - Otherwise the request is refused with
403 policy_denied. Keys are created withcanActForUsersoff unless you enable it. - On retrieve, a
userIdincludes that user's profile and narrows workspace memory to rows carrying exactly that user. Tenant conventions still apply, since they belong to everyone.
Visibility levels
Each memory has a visibility, narrowest first:
| Visibility | Who may recall it |
|---|---|
private | Only the exact agent that created it. |
agent | Every instance of the owning logical agent. |
user | Agents acting for the same end user (userId). |
workspace | Every agent in the workspace. The default. |
tenant | Every agent in the tenant. Writing or sharing at this level needs the promote capability. |
Visibility is compiled into every search leg before retrieval runs, so a memory another agent wrote as private never appears in your results — not even as a count. Tenant administrators see every row.
Set visibility when you write:
{ "type": "preference", "content": "Prefers window seats", "userId": "alex", "visibility": "user", "…": "…" }Owner and classification
ownerAgentId— the agent that wrote the memory. Ownership matters forprivate/agentvisibility and for sharing.classification—public,internal(default),confidentialorrestricted. It is stored and returned on every entry, but no policy enforces it yet.
Both are returned on every memory alongside visibility.
Reading, updating and deleting
By-ID operations (GET, PUT, DELETE on /v1/memory/{id}, explain and erasure status) apply the same rules and take optional agentId / userId query parameters naming the subject:
- A memory the subject cannot see answers
404— existence never leaks. - A memory the subject can see but the policy refuses to change answers
403with codepolicy_deniedandaction/ruleextension members.
Tenant-level rows (tenant conventions and floors) are readable by every workspace but cannot be updated, shared or deleted through a workspace credential — that answers 403. See Updating and forgetting.
Sharing: visibility only widens
POST /v1/memory/{id}/share raises a memory's visibility along private → agent → user → workspace → tenant. It requires the share capability (off by default) and ownership of the memory (or tenant admin); tenant also needs promote. A target at or below the current level is rejected with 422. Every share writes a history row and a share entry in the change feed.
There is no "unshare". To narrow a memory, delete it and write a new one with the narrower visibility.
Derived memory never widens
Episodes and procedures that consolidation derives from memories inherit the intersection of their sources' policies: a derivative is never more readable than its most restricted source. Two agents' private facts are abstracted separately rather than blended, and tenant promotion only considers memories that are already workspace- or tenant-visible.
Capabilities
Separately from identity, every API key or grant carries capabilities:
| Capability | Allows | Default on new keys |
|---|---|---|
read | Retrieve, get, explain, list episodes/procedures/files, change feed | on |
write | Append, update, blocks, profiles, categories, file docs, procedure validation | on |
feedback | Outcomes and retrieval feedback | on |
curate | Consolidate, curate blocks, review quarantined memories | on |
delete | Delete memories | off |
share | Raise visibility | off |
promote | Tenant-level visibility | off |
A missing capability answers 403. Over OAuth, the same names appear as memory:read, memory:write, and so on. See Authentication.