Version: Current
Historical release snapshot. These docs are frozen. Read current docs.
Identity and access
The estate has two credential planes, and they never convert into each other.
People sign in through the identity provider at auth.tcp.systems. Machines
call the model gateway with virtual keys. A login is not an API credential, and
an API credential is never proof that a human is present.
The identity provider
Section titled “The identity provider”Pocket ID is the identity provider for the estate today. Every surface
(studio, console, chat and the
consumer surface) authenticates against it, passkey first, with one client per
realm. The issuer is https://auth.tcp.systems; its discovery document and
JWKS are public and answered 200 when probed on 2026-11-05. See the
auth surface page for the live contract.
In parallel, the tcpsystems/auth repository builds the first-party provider
(authd) that is intended to take this role over. Slice 1 was gate-green on
2026-10-30: per-realm OIDC discovery and JWKS, authorization code with
mandatory PKCE (S256), WebAuthn passkeys as the primary human factor, TOTP and
one-time recovery codes as fallbacks, opaque server-side sessions, and an
append-only hash-chained audit log. The normative spec is
docs/AUTH-PLATFORM.md in that repository, and the threat model is
docs/AUTH-THREAT-MODEL.md. Deep operational detail stays there; this site
summarises and links.
One rule is permanent across both providers: no third-party identity SaaS is ever the source of truth. Federation over standard OIDC is a later feature and never becomes canonical.
How a person signs in
Section titled “How a person signs in”A surface is an OIDC relying party. It never reads the identity database and never mints identity. The shape of the flow:
- The surface redirects the browser to the provider’s authorization endpoint,
with
state, anonce, and a PKCE S256 challenge. - The provider authenticates the human with a passkey (TOTP or a recovery code as fallback) and redirects back with a single-use code.
- The surface exchanges the code at the token endpoint and verifies the ID token: signature against the published JWKS, exact issuer, its own client id in the audience, expiry, and the nonce it sent.
- The surface derives its own session and stores no identity tokens. The
console contract (
docs/CONSOLE-OIDC-CONTRACT.mdintcpsystems/auth) states this explicitly: no access, refresh or ID token is stored anywhere.
A passkey is a WebAuthn credential bound to the exact origin that created it. The browser refuses to sign an assertion for a different origin, which is what makes the flow phishing resistant. A passkey cannot be replayed, forwarded or typed into a lookalike page.
The consumer surface on acumen is the exception by design: a person signing in there has no key yet, so sign-in is invite-only with a recovery code, and OIDC is deferred on that path.
How a machine calls the API
Section titled “How a machine calls the API”Machines authenticate to acumen with virtual keys (vk_…), presented as
bearer tokens on /v1/*. A key carries scopes, an optional model allowlist,
and spend and rate caps. The full contract, including the grant ledger and the
failure matrix, is acumen authentication. The identity
provider is not involved in this plane at all.
How secrets are handled
Section titled “How secrets are handled”The rules are the same on both planes:
- A credential is printed once and stored only as a hash. Virtual keys are
SHA-256 hashed at rest; invite tokens the same. One-time bootstrap values in
authd land in a
bootstrap.jsonfile with mode0600and are never logged. - Configuration references secrets by name, never by value. The console OIDC
client is provisioned as
OIDC_ISSUER,OIDC_CLIENT_ID,OIDC_REDIRECT_URIand a client secret held as a deploy-time secret; the exact store for the console’s client secret is an operator detail, named on the auth surface page. - No secret appears in logs, command lines, URLs or source. That includes this site: documentation names the variable or the path, never the value.
Security caveats
Section titled “Security caveats”- Fail closed. Auth answers 401 or 403 by default. A missing client, realm or signing key is a refusal, not an anonymous session. Unknown key id, unverifiable signature or a store error deny the request.
- Deny by default at the edge. The Pocket ID admin UI is tailnet-only; its
/setuppath answers 404 from the public internet. - PKCE is mandatory and only S256 is sent, even if the provider advertises weaker methods. Redirect URIs match exactly.
- A weaker factor weakens the session. TOTP and recovery codes carry a visibly weaker authentication-methods reference and narrower scope in the first-party provider; administrative scopes require a passkey.
- Every security decision is audited. Login, token issuance, revocation, key rotation and refusals append to a tamper-evident chain in the first-party provider.
When sign-in fails
Section titled “When sign-in fails”Surfaces return the failure class: an unknown client, an expired session or a
rejected assertion. For the consumer surface the classes are different (an
unknown, expired or used invite answers 410, and the page does not say
which). Diagnosis steps for each path are in
Sign in to the surfaces.
One recorded misdiagnosis is worth knowing: a report of text/html discovery
with HTTP 200 was a wrong-host fetch, not a provider fault. Before reporting
the provider down, fetch
https://auth.tcp.systems/.well-known/openid-configuration directly and check
that the answer is JSON whose issuer is exactly https://auth.tcp.systems.