Titentiten.devꦠꦶꦠꦺꦤ꧀
v0.10.0 · stableGitHub
DocsIntegrateKeys & scopes

Keys & scopes

A Titen credential is a hashed bearer token carrying a scope list and a trust ceiling, and neither can be widened by the key that mints the next one.

titen_sk_ prefix54 scopes4 trust levelsSHA-256 at rest

What a key actually is

A raw key is titen_sk_ plus 43 base64url characters, 32 bytes from crypto.getRandomValues. Only its SHA-256 hash reaches storage, so a database copy cannot be replayed as a credential. Verification is one indexed lookup on that hash and nothing about the principal is cached, so revocation takes effect on the next request.

The row behind a key holds its identity and effective authority inputs:

WHAT THE CREDENTIAL CARRIES

org_id the tenant. Never read from a request body.

principal_id the actor recorded as actor_id on the rows it writes.

principal_kind human, agent or service.

label a human name, so revoking the right key is not guesswork.

scopes a space-separated list, or * for all of them.

max_trust the ceiling on evidence this key may assert.

not_before and expires_at its immutable validity window.

issued_by and an optional organization, project or subject data target; current issuer grants are intersected on every request.

Tenant and actor authority always come from the credential. A valid identifier belonging to another tenant returns the same non-disclosing 404 NOT_FOUND used for every cross-scope denial.

The 54 scopes

Most routes declare a primary scope. Broad compile, governance, federation and release operations can also require a second capability or an active organization role. A key holding * satisfies scope checks, with root bypass kept for bootstrap and recovery.

Scope Opens
projects:resolve POST /v1/projects/resolve
projects:create the create: true branch of that route; without it an unknown reference is a 404
projects:read project directory and bounded project-reference reads
subjects:read subject directory and bounded subject-reference reads
principals:read owner/admin access to the non-secret principal directory
grants:read list additive data grants and run the owner-only access simulator
grants:write create or revoke only permissions the caller may delegate
models:read inspect the masked immutable model startup snapshot
models:probe run one bounded audited extraction or embedding probe
observations:write POST /v1/observations
observations:purge DELETE /v1/observations/:id; legal holds still block erasure
claims:write POST /v1/consolidations, and claim supersede, revoke, expire
context:compile POST /v1/context/compile
context:compile:all explicit cross_project: true on REST or MCP compile
feedback:write POST /v1/context/:id/feedback
evidence:read GET /v1/claims/:id/evidence
checkpoints:read GET /v1/checkpoints, GET /v1/checkpoints/:id
checkpoints:write POST /v1/checkpoints, DELETE /v1/checkpoints/:id
workspaces:read GET /v1/workspaces
workspaces:write POST /v1/workspaces
memberships:read GET /v1/memberships
memberships:write POST /v1/memberships, DELETE /v1/memberships/:id
leases:read GET /v1/leases
leases:write acquire, release and owner/admin force-release routes
handoffs:read GET /v1/handoffs
handoffs:write POST /v1/handoffs, POST /v1/handoffs/:id/resolve
mcp:call POST /mcp — all nine tools; broad compile also needs context:compile:all
events:read GET /v1/events, GET /v1/events/:id
views:compile POST /v1/memory-views/compile
views:compile:all explicit audited organization-administrator Atlas mode for a root or owner
audit:read GET /v1/audit
audit:export GET /v1/audit/export
governance:read policy list and the governed Atlas scope_preview lens
governance:write policy create and expected-version status update
approvals:read GET /v1/claim-approvals
approvals:write POST /v1/claim-approvals
approvals:approve POST /v1/claim-approvals/:id/decide
releases:read channels, knowledge releases and the governed Atlas release lens
releases:write channel create/update and knowledge-release draft creation
releases:approve approve, activate and revoke knowledge releases
channel:compile POST /v1/channels/:id/context/compile from the bound gateway
retention:write legal-hold create/release and bounded retention application
identity:read GET /v1/identity-mappings
identity:write identity mapping create and retire
federation:read GET peers, peer filters, federation log
federation:write peer create, suspend, filters, pull, push
webhooks:read GET /v1/webhooks, GET /v1/webhooks/:id/deliveries
webhooks:write webhook create, delete, pause, resume, deliver
index:write POST /v1/index/drain
enrichment:write POST /v1/enrichment/drain
keys:manage POST /v1/keys, GET /v1/keys, DELETE /v1/keys/:id
export:read GET /v1/export
export:all explicit cross-project export
import:write POST /v1/import

audit:export is separate from audit:read because pulling the trail out as NDJSON is a different act from paging it as JSON; webhooks:read without webhooks:write is the shape a monitoring job wants. A missing scope is 403 FORBIDDEN and names what was needed: Missing required scope "claims:write".

Trust ceilings

Four levels, ranked: unverified (0) < asserted (1) < verified (2) < policy_approved (3). max_trust is a ceiling on what a credential may assert. Ask for more and the write fails before it touches anything:

{ "error": { "code": "FORBIDDEN",
    "message": "This credential may not assert \"verified\" trust." },
  "meta": { "request_id": "req_ec1a13fdae074509908eac6d5a62e7ee" } }

A claim’s trust may not exceed the trust of its supporting evidence, and consolidation checks the ceiling again, so a low-ceiling agent cannot launder its own output into fact by consolidating it. verified describes evidence authority, not permission to disclose: publishing to a channel is a separately-scoped act.

Minting

titen bootstrap creates the organization and the first key: principal owner, kind human, scopes ["*"], ceiling policy_approved. That is the only key that should ever look like that.

titen key create --db titen.db --org-id org_724bbe3aab2a4286b25dabec65bce85e \
  --label deploy-agent --kind agent --trust asserted \
  --scopes "observations:write,claims:write,context:compile,feedback:write"

--org-id is required. Without --scopes you get the CLI’s default set, the kernel loop: projects:resolve, observations:write, claims:write, context:compile, feedback:write, evidence:read. --trust defaults to asserted, --kind to agent. Add --print-sql to emit the INSERT for a remote database (Cloudflare D1) instead of writing locally; the raw key still goes to stderr, once.

Over HTTP, with keys:manage:

curl -X POST http://127.0.0.1:8787/v1/keys \
  -H "authorization: Bearer $TITEN_API_KEY" -H 'content-type: application/json' \
  -d '{"label":"deploy-agent","scopes":["observations:write","context:compile"],"max_trust":"asserted"}'
{ "data": { "key_id": "key_3087befa94bc454fb8769630aa2eb1cf",
    "api_key": "titen_sk_…", "label": "deploy-agent",
    "scopes": ["observations:write", "context:compile"],
    "max_trust": "asserted", "principal_kind": "agent",
    "warning": "Store this key now. Titen keeps only its hash and cannot show it again." } }

principal_id defaults to a generated agent_… identifier and principal_kind to agent. A key can never mint more authority than it holds:

An unrecognized scope is 400 VALIDATION_ERRORUnknown scope "x".

A scope the caller lacks is 403 FORBIDDENThis credential may not grant scope "keys:manage".

A higher ceiling than the caller's is 403A new credential may not exceed the creating credential's trust ceiling.

The raw key appears exactly once, in this response. No route and no support path can reissue it.

Revoking

curl -X DELETE http://127.0.0.1:8787/v1/keys/key_3087befa94bc454fb8769630aa2eb1cf \
  -H "authorization: Bearer $TITEN_API_KEY"
# → {"data":{"key_id":"key_3087…","revoked_at":"2026-07-30T10:49:20.759Z","revoked":true}}

Idempotent: a second call returns the first revoked_at rather than moving it. An unknown or foreign key id is 404. The CLI equivalent is titen key revoke --id <key id>, and titen key list prints id, status, org, principal, ceiling, label and scopes for every key in the database.

There is no rotation route. Rotation is three deliberate steps: mint the replacement, move traffic to it, revoke the old key. Keep the overlap short.

Least privilege in practice

One key per agent identity, labelled. Shared keys make revocation a decision about which service to break.

Scope to the routes that agent actually calls. A retrieval-only agent needs context:compile and nothing that writes.

Give the lowest ceiling the work tolerates. policy_approved belongs to an operator, not to a loop that summarizes tool output.

Keep keys:manage on one operator credential. Every other key that holds it can list and revoke every key in the organization.

Treat mcp:call as write authority: eight of its nine tools can record canonical or coordination state.

Never put a key in a repository file, plugin manifest, tool description, URL query or command-line argument. Use the host's secret store or a mode-0600 file.

↑↓ navigate↵ open