identity.v1): the
one-concern-per-module rule applied to trust itself, so authorization logic
is testable in full isolation from the database.
On every request it resolves three things:
- Who is asking — resolved once at the edge, never from the client’s word.
- What they touch, and who owns it — every meeting, recording, and workspace has an owner.
- Whether they are allowed — a default-deny decision.
admin-api(core/identity/services/admin-api) — the live, database-backed service: user accounts, API tokens, and the/internal/validateoracle the gateway calls.identity_core(core/identity/src/identity_core) — a pure, dependency-free library of the rules (mint a token, check access, mint a dispatch token, broker a secret), sealed as theidentity.v1contract so every service decides the same way.
This page is the identity domain — accounts, the access model, what’s built, and the roadmap.
The cross-cutting trust flow — how a dispatch’s authorization is proven and verified at every hop —
is its own document: Identity & trust.
The model
Six rules — the reasoning, the mechanism, and the established practice each comes from.The agent (the LLM) is outside the trust boundary — it carries proof but never enforces it.
Compromise it via prompt injection (OWASP LLM01) and it still cannot exceed
its token’s scope; the boundaries enforce, not the model. See
Identity & trust.
The request path (grounded)
Everything reaches the domains through the gateway; the terminal is one client. For every call the gateway resolves the key to a user and forwards to the owning domain withX-User-Id stamped on.
Two safety shapes result:
- Agent space is safe by partition — a workspace, session, or routine is addressed by your id, so no explicit check is needed.
- Meetings are owned in the meetings domain, so the owner-checked path runs there (
GET /transcripts,/wssubscribe authorization).
Building blocks
What actually exists today, with the real names.identity_core — the pure rule library, sealed as identity.v1 (core/identity/contracts/identity.v1):
- Tokens (
tokens.py) —ScopedToken(subject, scopes, expires_at); scopes arebot · tx · browser;mint_token/validate_token. - Access (
access.py) —Resource(kind, id, owner)for kindsmeeting_transcript · recording · ws_subscribe;AccessDecision(allow, subject, …, reason);OwnerOnlyPolicy(default-deny, allow iffsubject == owner); the front-door functioncan_access(subject, resource, action). - Dispatch tokens (
dispatch_tokens.py) — a JWT-style, audience-scoped bearer token signed HS256 in dev (SPIFFE SVIDs in production):DispatchClaims(subject, launcher, workspaces[], tools[], iat, exp);mint_dispatch_token/verify_dispatch_token.subjectis who you act for;launcheris what triggered it.may_mount(id, mode)andmay_call(tool)are the limits a boundary checks. This is the “single-purpose pass.” - Secrets (
secrets.py) — the brokered-secrets pattern (HashiCorp Vault):SecretsPortreturns a redactedBrokeredSecret(itsreprnever prints the value) and writes an audit log; the value is fetched on demand, never logged. The real vault (lease, rotation) is deferred — see encryption.
admin-api — accounts and the live oracle:
- Tables:
UserandAPIToken(token, user_id, scopes[], expires_at, …); tokens look likevxa_<scope>_<random>. POST /internal/validate— the gateway calls this (behind an internal secret, fail-closed) to turn an API key into{user_id, scopes, …}.- Three tiers: admin (
X-Admin-API-Key, create users/tokens), user (X-API-Key, e.g. set a webhook), internal (X-Internal-Secret, the validate oracle).
X-User-Id, enforces coarse route scopes, and runs
the /ws multiplex.
Where we are
Honest status. The design is frozen and the primitives are built — the gap is adoption: the rules exist as a library but are not yet wired into every path that needs them. The constitution names this risk directly: P20 (complete mediation) records that thecanAccess seam “was designed but never wired, so
it rotted,” and P9 holds that an unenforced rule is only aspirational — a rule that does not turn
CI red can be crossed.
The front door is real; a few agent-domain endpoints read a meeting’s data directly instead of via the
meetings domain, skipping the owner check that already exists in the library. Closing it is wiring, not
design: route those reads through the owning domain and decide with
OwnerOnlyPolicy.
Where we are going (zero-trust)
The target — a chain of custody where a dispatch’s authorization is proven and verified at every hop (signed tokens, RFC 8693 exchange at the tool boundary, SPIRE/Keycloak via kagenti) — is one document: Identity & trust. It lands as Stage 2 — Trust. The point for this domain is narrow: we run an untrusted, prompt-injectable agent on private data, so safety must come from boundaries that verify, not from trusting the model.Roadmap — by principle
Each item ties a declared principle to its code reality and the concrete next step, plus the gate or stage that makes it real (P9: an ungated rule is aspirational). Foundations: the agent is untrusted, identity is a chain of custody, self-host & air-gap by default.subject_of (control_plane/api.py) has a VEXA_AGENT_DEFAULT_SUBJECT fallback for a gateway-less
single-user self-host. It is fail-closed (401) when unset; it must stay unset in any multi-tenant
deployment, or every caller collapses to one subject.Data at rest (encryption)
Three stores hold sensitive data at rest (data in transit is encrypted with TLS — the standard that secures HTTPS connections). State and plan:
These are tracked in the roadmap status; workspace/bucket encryption is already part of
the
workspace.v1 contract shape in Stage 0.
Why this much, and not more
Measures are sized to the system: a multi-user product where each person owns meetings and a private workspace, agents run untrusted in isolated containers, all driven from the terminal. The API reduces to “is this yours?”, so the priorities are:- owner checks on meetings and workspaces (cross-user exposure is the real risk);
- credentials out of the agent (the one untrusted, injectable component);
- encryption of the three at-rest stores.
can_access, the token shapes) do not change when it
lands; only what sits behind them does.