CortexDB Docs
Docs for CortexDB 0.14.0, the version running on CortexDB Cloud (api.cortexdb.ai).
Shared Memory

Identity and Tokens

The principal chain, what a minted token may carry and how long it lives, tokens from your own identity provider, the tenant directory, and SCIM provisioning.

Shared memory decides every request by who is asking, so each person and each agent should carry a token of its own rather than one shared key. 0.14.0 gives tokens a principal chain, puts limits on what a minted token may hold, accepts tokens from your identity provider, and adds a directory of names and sign-in accounts, which an identity provider can keep up to date over SCIM.

The principal chain

A token names its subject in sub (user:alice, agent:triage, service:sync). It may also say what is acting for that subject:

ClaimSays
actThe agent acting for the subject: {"sub": "agent:assistant"}
runThe agent's run, run_start when it began
appThe application the request comes through: service:<id>
clientperson or agent
taintTags carried from what the chain read, such as ["external"]
auth_timeWhen the person last signed in
minterWho minted the token (operator, or a principal)

A token with act or run, or whose client resolves to agent, is an agent chain token: it holds only what its scopes entries name, and lives at most 24 hours. A token with act writes only within a run (403 RUN_REQUIRED without one), and its writes are stored with the agent as observed_actor. GET /v1/auth/whoami shows the chain:

{"caller": "user:alice", "tenant_id": "acme", "deployment_preset": "on_prem_enterprise",
 "token": {"jti": "jti_01a186c0cbb876d1bb3e447ce6391a8f", "iss": "https://cortexdb.local/issuer", "exp": 1793265744},
 "effective_capabilities": [],
 "chain": {"act": "agent:assistant", "client": "agent", "run": "run-42", "run_start": 1793262144,
           "minter": "operator", "issuer": "minter"}}

(effective_capabilities lists only families a <capability>:* entry names; this token's entries are confined to org:acme.)

An agent's write also carries what its run read, and is redirected to the person's private space when it cannot show that safely: see Applications and Sources. An AI employee acts under its own token (sub the employee, no act); a mint whose act names one is refused.

Minting tokens

POST /v1/auth/tokens mints a token with this server's minter (CORTEX_V1_MINTER_ENABLE=1 on a keyed server). Since 0.14.0 four rules hold, each on by default:

RuleWhat it refuses
Lifetime ceilingA token living longer than 7 days for a user: or agent: subject, 30 days for a service: or system: subject, or 24 hours when it carries a chain field. At the mint: 422 INVALID_BODY, ttl_too_long; at every use of an older token past its ceiling: 401 INVALID_TOKEN_CLAIM, lifetime_over_ceiling. The default lifetime is one hour
Total confinementOn a token that carries scopes, any capability no entry names: 403 POLICY_DENIED, details.link: "confinement". Name every family the token uses; <capability>:* names one at every scope, and checks made without a scope (audit.read, blob.upload, lifecycle.subscribe, …) need it
The mint's reachA mint that names no tenant root (scope, tenant or scopes), unless the operator key mints a service: or system: token; a token wider than its minter; a mint by a token another principal minted
The minter linkA token another principal minted, used where that minter could not act

The operator key mints a person's token for one tenant like this:

curl -sS -X POST "$CORTEX_URL/v1/auth/tokens" \
  -H "Authorization: Bearer $CORTEX_API_KEY" -H "Content-Type: application/json" \
  -d '{"subject": "user:alice", "scope": "org:acme", "ttl_seconds": 3600}'
# {"token":"v4.public.…","jti":"jti_…","expires_at":"2026-10-29T09:04:53+00:00"}

And an agent working for Alice in one run, confined to her tenant:

curl -sS -X POST "$CORTEX_URL/v1/auth/tokens" \
  -H "Authorization: Bearer $CORTEX_API_KEY" -H "Content-Type: application/json" \
  -d '{"subject": "user:alice", "scope": "org:acme", "ttl_seconds": 3600,
       "act": "agent:assistant", "run": "run-42",
       "caps": ["scope.read.local", "scope.read.holistic", "scope.write"],
       "scopes": ["scope.read:org:acme/*", "scope.write:org:acme/*"]}'
# {"token":"v4.public.….eyJjZGIiOiJjaGFpbjEifQ","jti":"jti_…","expires_at":"…",
#  "chain":{"act":"agent:assistant","run":"run-42","run_start":1793261247,"minter":"operator"}}

What the rules answer when a mint goes past them:

{"error_code": "INVALID_BODY",
 "details": {"reason": "ttl_too_long", "field": "ttl_seconds", "lifetime_s": 2592000, "ceiling_s": 604800},
 "retriable": false}
{"error_code": "POLICY_DENIED",
 "message": "a mint that names no tenant root is deployment-wide, the trusted operator's alone and only for a service: or system: subject: name the root with scope, tenant or scopes",
 "details": {"reason": "deployment_mint_requires_operator"}, "retriable": false}

Each rule has a switch (CORTEX_MINT_LIFETIME_ENFORCE, CORTEX_TOKEN_CONFINEMENT_ENFORCE, CORTEX_MINT_REACH_ENFORCE, CORTEX_MINTER_LINK_ENFORCE). off serves what the rule would refuse and counts it in GET /v1/admin/access/report instead: a way to find the clients to fix after an upgrade, not a setting to keep.

External issuers

Production deployments usually let their identity provider (Okta, Microsoft Entra ID, Auth0, Keycloak) issue the tokens. CORTEX_AUTH_ISSUERS_FILE names a TOML file of trusted issuers: JWT (RS256 or ES256, with pinned keys or a JWKS URL) and PASETO v4 (pinned keys). Each entry says how a token's subject maps to a principal, which tenants it may name, which chain claims its tokens may carry and how long they may live.

version = 1

[[issuer]]
id = "acme-idp"
iss = "https://login.acme.example"
format = "jwt"
algorithms = ["RS256"]
keys = ["keys/idp.pem"]            # or jwks_url = "https://login.acme.example/.well-known/jwks.json"
max_lifetime_s = 3600

[issuer.subject]
mode = "directory"                 # the person whose directory entry holds this sign-in account
kinds = ["user"]

[issuer.tenants]
allowed = ["acme"]
aliases = [{ aud = "https://memory.acme.example/mcp", tenant = "acme" }]
subject.modeMaps the token's subject to
directoryThe principal whose directory entry lists the account ({system, account}; the system is the entry's iss unless set)
actorThe value itself, which is already a principal id (user:alice)
prefix<kind>:<namespace>.<value>, such as user:acme.00u1abcd
hash<kind>:<namespace>.<32 hex digits>, an HMAC of the value

Use directory mode for people

A prefix or hash principal id holds a ., which a scope path cannot. Such a person cannot have a scope named after them (their own region org:acme/user:<id>), and the MCP server's default allowlist, which is built from the principal id, names no scope for them. Map people with directory (or actor) mode.

A token of a directory, prefix or hash entry may be sent without X-Cortex-Actor, since its client cannot know the principal it maps to; GET /v1/auth/whoami answers it:

curl -sS "$CORTEX_URL/v1/auth/whoami" -H "Authorization: Bearer $IDP_ACCESS_TOKEN"
# {"caller":"user:dana","tenant_id":"acme","token":{"jti":"acme-idp:7dca…","iss":"https://login.acme.example",…},…}

tenants.aliases maps an audience that is not cortexdb:tenant:<id>, such as the MCP server's resource URL, to a tenant: that is how an identity provider's access token for the MCP server works. A token past max_lifetime_s is 401 INVALID_TOKEN_CLAIM (lifetime_over_maximum), one whose audience the entry does not admit 401 WRONG_TENANT. POST /v1/admin/auth/reload (operator key) re-reads the file without a restart; a file that breaks a rule is refused and the running entries kept. An issuer's token never mints.

The directory

The directory holds each tenant's principals (names, emails, titles, sign-in accounts) and the names of its scopes, sets and applications. Names live here rather than in scope records, because records are kept for ever in the log and a directory entry can be erased.

curl -sS -X PUT "$CORTEX_URL/v1/directory/user:alice?scope=org:acme" \
  -H "Authorization: Bearer $CORTEX_API_KEY" -H "Content-Type: application/json" \
  -d '{"display_name": "Alice Kim", "emails": ["alice@acme.example"], "title": "Engineering manager"}'
# 200, ETag: "1"
# {"id":"user:alice","display_name":"Alice Kim","title":"Engineering manager","status":"active",
#  "emails":["alice@acme.example"],"accounts":[],"version":1}

curl -sS "$CORTEX_URL/v1/directory?scope=org:acme&q=ali" \
  -H "Authorization: Bearer $ALICE_TOKEN" -H "X-Cortex-Actor: user:alice"
  • Any principal of the tenant reads names and titles; emails, accounts and attributes only the tenant root's Owners, the operator and the principal itself. A private scope's name only its readers.
  • A scope or set is named by its path, with / sent as %2F: PUT /v1/directory/org:acme%2Fteam:eng?scope=org:acme {"display_name": "Engineering"}.
  • "status": "deactivated" stops every credential of the person at its next request, whichever issuer signed it (401 SUBJECT_UNMAPPED, deactivated), until the entry is active again. Their memberships are kept.
  • If-Match with the entry's version makes a write conditional (412 DIRECTORY_VERSION_CHANGED).
  • Register the tenant root first: until a record registers it, only the operator key administers the tenant's directory.

SCIM

An identity provider provisions a tenant's people and groups at /scim/v2/Users and /scim/v2/Groups (SCIM 2.0, with ServiceProviderConfig, ResourceTypes and Schemas). Users are directory entries; Groups are sets, whose membership changes are applied as change sets. active: false deactivates the person as above. The caller is the tenant's SCIM client: a token whose subject is service:scim-<client> with the tenant's audience, which must be an Owner of the tenant root. SCIM needs the default one-segment tenant roots (CORTEX_TENANT_DEPTH 1): deeper roots answer 501 SCIM_UNSUPPORTED.

See also

On this page