<!--
Full-page Markdown export (rendered HTML → GFM).
Source: https://neotoma.io/aauth
Generated: 2026-04-27T12:48:24.022Z
-->
# AAuth (Agent Authentication)
AAuth is Neotoma's mechanism for cryptographically verifiable *agent identity* on every write. It lives alongside (not in place of) human user authentication: where `user_id` answers "whose data is this?", AAuth answers "which agent wrote it?". The pair is stamped onto every observation, relationship, source, interpretation, and timeline event.
AAuth is built on two open standards: **RFC 9421 HTTP Message Signatures** for the request signature, and an **`aa-agent+jwt`** agent token that carries the agent's confirmation key and stable identifiers. Optional hardware attestation (Apple Secure Enclave, WebAuthn-packed, TPM 2.0) promotes signed clients into the highest trust tier.
Protocol-level specifications, Internet-Drafts, SDKs, and the live playground are on [aauth.dev](https://aauth.dev/). The rest of this page is how Neotoma implements that contract for writes and attribution.
## Why AAuth
- Bearer tokens are operator-set shared secrets that gate connection access; they resolve a `user_id` but never mint an attribution tier above `anonymous` on their own. OAuth authenticates the *human* behind the connection. AAuth identifies the *agent* writing within that authenticated session, in parallel with whichever human-identity flow is in use.
- Neotoma's AAuth is an identity-based contract today, not a delegation-token system: the agent presents a stable cryptographic identity, and the server records it alongside the human `user_id` on every write. Per-agent scoping is handled separately via `agent_grant` entities (see [capabilities](/aauth/capabilities)).
- Every durable row carries the agent's stable identifiers (`agent_thumbprint`, `agent_sub`, `agent_iss`) and a resolved `trust_tier`.
- Operators can require a minimum tier per route via attribution policy. Self-reported clients fall back gracefully to the `unverified_client` tier.
- The contract is uniform across HTTP `/mcp`, direct REST routes, MCP stdio, and CLI-over-MCP / CLI-over-HTTP.
◆
## Trust tiers
A single enum is stamped onto every durable row. Tier resolution happens once per request inside `src/middleware/aauth_verify.ts`; services and clients MUST read the resolved tier from the request context rather than re-deriving it.
- **`hardware`**, AAuth verified AND the JWT carries a `cnf.attestation` envelope the verifier accepts AND the bound key is not revoked.
- **`operator_attested`**, AAuth verified AND `iss` (or `iss:sub`) is in the operator allowlist (`NEOTOMA_OPERATOR_ATTESTED_ISSUERS` / `NEOTOMA_OPERATOR_ATTESTED_SUBS`).
- **`software`**, AAuth verified, but no attestation envelope (or attestation failed and operator allowlist did not match), regardless of signing algorithm.
- **`unverified_client`**, No AAuth signature was verified, but the caller self-reported a distinctive `clientInfo.name` (or `X-Client-Name`) that survived generic-name normalisation. The name is recorded on the row but is *not* cryptographically attested, anyone can claim it.
- **`anonymous`**, No AAuth, and no usable client name either: `clientInfo` and `X-Client-Name` were absent, empty, non-strings, or matched the generic-names blocklist (`mcp`, `client`, `mcp-client`, `unknown`, `anonymous`). The row carries no stable identifying name at all.
The practical difference: `unverified_client` rows still let you filter and group by which integration produced the write (e.g. `cursor-agent` vs `claude-code`), whereas `anonymous` rows are an undifferentiated bucket that operators usually want to surface or block via attribution policy.
Tier resolution surfaces directly in the [Inspector agents view](/inspector/agents): every distinct writer is one row, badged with the tier it resolved to (`hardware`, `software`, `unverified_client`, `anonymous`) plus the signing algorithm and last-seen activity.
inspector.neotoma.io/agents
Inspector
Neotoma
Dashboard
Conversations
Turns
Compliance
Activity
Feedback
Entities
Observations
Sources
Relationships
Graph Explorer
Schemas
Timeline
Interpretations
Agents
Agent grants
Settings
Agents
14 active identities · 8.2k writes · last 30d
hardware (3)software (8)unverified (2)anonymous (1)
| Agent | Tier | Alg | Writes | Last seen |
| --- | --- | --- | --- | --- |
| operator (mac · SE)es256:Dr…2Yj | hardware | ES256 | 412 | 10:55 |
| cursor-agentes256:Bp…4Zq | hardware | ES256 | 2,810 | 12:30 |
| claude-codeed25519:Aa…7Lk | software | EdDSA | 4,120 | 12:41 |
| ingest-pipelineed25519:Cq…9Rt | software | EdDSA | 980 | 11:08 |
| custom-script@myco- | unverified\_client | \- | 18 | Apr 24 |
| anonymous- | anonymous | \- | 4 | Apr 22 |
Inspector, Agents list. AAuth trust tiers render as inline badges; click through for the per-agent thumbprint, algorithm, attestation outcome, and grants.
◆
## Wire format
AAuth sends three headers on every signed request. The signature components MUST cover `@authority`, `@method`, `@target-uri`, `content-digest` (when there's a body), and the `signature-key` header itself.
Signature: sig1=:AGNlbGtkdHIxMjM4...:
Signature-Input: sig1=("@authority" "@method" "@target-uri" \\
"content-digest" "signature-key");\\
alg="ed25519";created=1714003200;keyid="..."
Signature-Key: <base64url(JSON: { jwk, jwt: "<aa-agent+jwt>" })>
The `aa-agent+jwt` agent token uses `typ: "aa-agent+jwt"` and carries:
- `iss`, issuer / fleet identifier
- `sub`, agent identity within the issuer
- `iat`, issued-at; checked against the configured AAuth clock-skew window (default 300 s)
- `cnf.jwk`, confirmation key (RFC 7638 thumbprint MUST match the signing key)
- `cnf.attestation` (optional), hardware attestation envelope (Apple SE, WebAuthn-packed, or TPM 2.0)
Neotoma verifies the signature against the canonical authority configured via `NEOTOMA_AUTH_AUTHORITY`. Using the request `Host` header for verification is explicitly unsafe and rejected.
◆
## Verification cascade
Tier derivation walks the signed payload, then attestation, then the operator allowlist, then the self-reported channels:
Signature header present?
no → clientInfo / X-Client-Name non-generic? → unverified\_client
| else → anonymous
yes → signature verifies against authority + body digest?
no → fall through to clientInfo channel
yes → cnf.attestation present?
yes → verifier accepts? → revocation OK? → hardware
| else → software
no / fails → iss (or iss:sub) in operator allowlist?
yes → operator\_attested
no → software
Verifier failures never reject the request when the underlying signature is valid, they only prevent tier promotion. The reason is recorded under `attribution.decision` on `GET /session` for debugging.
◆
## Per-request precedence
For each request Neotoma walks these inputs in order; the first populated field at each layer wins. Bearer tokens resolve only `user_id` and never mint a tier above `anonymous` on their own.
AAuth (verified signature + JWT) → agent\_thumbprint, agent\_sub, agent\_iss,
agent\_algorithm, agent\_public\_key
clientInfo (MCP initialize) → client\_name, client\_version
X-Client-Name / X-Client-Version → client\_name, client\_version
OAuth connection id → connection\_id
(nothing) → anonymous
◆
## Hardware attestation
When the agent token carries `cnf.attestation`, Neotoma verifies the envelope before promoting the request to `hardware`. The envelope binds the signing key to a hardware root of trust by RFC 7638 thumbprint and a server-recomputed challenge. Supported formats:
- **`apple-secure-enclave`**, macOS hosts; backed by the Apple Attestation Root (bundled at `config/aauth/apple_attestation_root.pem`).
- **`webauthn-packed`**, YubiKey 5 series and any WebAuthn authenticator emitting a `packed` attestation statement.
- **`tpm2`**, Linux `/dev/tpmrm0` and Windows TBS / NCrypt; verified against the operator-configured TPM CA bundle.
Verification ordering inside each format: parse statement, extract credential public key, RFC 7638 thumbprint check against `cnf.jwk`, recompute challenge from JWT claims, then chain validation. Each step has a deterministic failure code surfaced via `attestation_outcome`.
◆
## Operator policy
Operators control attribution requirements through environment variables. The active policy is exposed under `policy` on `GET /session`:
- **`NEOTOMA_ATTRIBUTION_POLICY`** (`allow` | `warn` | `reject`; default `allow`), global behaviour for `anonymous` writes. `reject` returns `HTTP 403 ATTRIBUTION_REQUIRED`; `warn` stamps `X-Neotoma-Attribution-Warning`.
- **`NEOTOMA_MIN_ATTRIBUTION_TIER`** (`hardware` | `software` | `unverified_client`), minimum tier required for the policy to be considered satisfied.
- **`NEOTOMA_ATTRIBUTION_POLICY_JSON`** , per-path overrides, e.g. `{"observations":"reject","relationships":"warn"}`.
Per-path `reject` always wins over a global `allow`. Per-agent fine-grained capability scoping (`(op, entity_type)` allow-lists) is layered on top via `agent_grant` entities.
Operators can read and edit the active policy without touching env vars from the [Inspector settings page](/inspector/settings/attribution-policy); per-agent grants live alongside the agent in the [Agents view](/inspector/agents).
inspector.neotoma.io/settings#attribution
Inspector
Neotoma
Dashboard
Conversations
Turns
Compliance
Activity
Feedback
Entities
Observations
Sources
Relationships
Graph Explorer
Schemas
Timeline
Interpretations
Agents
Agent grants
Settings
Attribution policy
Global mode
allowwarnreject
Active: warn
Min tier
hardwaresoftwareunverified\_client
Active: software
Per-path overrides
<table class="w-full text-[12px]"><tbody><tr class="border-b border-border/40 last:border-0"><td class="py-1.5 font-mono text-muted-foreground">/observations</td><td class="py-1.5 text-right">reject</td></tr><tr class="border-b border-border/40 last:border-0"><td class="py-1.5 font-mono text-muted-foreground">/relationships</td><td class="py-1.5 text-right">warn</td></tr><tr class="border-b border-border/40 last:border-0"><td class="py-1.5 font-mono text-muted-foreground">/timeline</td><td class="py-1.5 text-right">warn</td></tr><tr class="border-b border-border/40 last:border-0"><td class="py-1.5 font-mono text-muted-foreground">/sources</td><td class="py-1.5 text-right">allow</td></tr></tbody></table>
Decision (last 100 requests)
Verified sigs
94
94%
Promoted (HW)
12
attestation OK
Rejected
3
anonymous → /observations
Inspector, Settings · Attribution policy. The same env vars (NEOTOMA\_ATTRIBUTION\_POLICY, NEOTOMA\_MIN\_ATTRIBUTION\_TIER, per-path overrides) rendered as a live operator console with the resolved decision per route.
◆
## Generate keys with the CLI
The Neotoma CLI ships hardware-aware keygen across darwin (Apple Secure Enclave), linux (TPM 2.0), win32 (Windows TBS / NCrypt), and YubiKey 5 series:
\# Software-backed keypair (cross-platform)
neotoma auth keygen
# Hardware-backed keypair (auto-selects best backend)
neotoma auth keygen --hardware
# Force a specific hardware backend
neotoma auth keygen --hardware --backend yubikey
# Inspect resolved trust tier and attestation outcome
neotoma auth session
Keys are written to `~/.config/neotoma/aauth/signer.json` with a per-backend handle. The agent token is minted on demand and attached to every signed request. `neotoma auth session` renders the same diagnostics that `GET /session` exposes over HTTP.
◆
## Preflight (mandatory for new integrators)
Before enabling writes, call `GET /session` (or the `get_session_identity` MCP tool, or `neotoma auth session`) and confirm:
- `attribution.decision.signature_verified === true` when AAuth is intended.
- `attribution.tier` is `hardware`, `operator_attested`, or `software` for signed clients, or at least `unverified_client` when intentionally relying on `clientInfo` only.
- `eligible_for_trusted_writes === true`.
Generic `clientInfo.name` values (`mcp`, `client`, `mcp-client`, `unknown`, `anonymous`, …) are normalised to the `anonymous` tier and WILL fail preflight under any non-`allow` policy.
◆
## Diagnostic surface
Every AAuth verification emits a structured `attribution_decision` log line and exposes the same fields on `GET /session` under `attribution.decision`:
- `signature_present`, `signature_verified`, `signature_error_code`
- `attestation_outcome` (`verified`, `format_unsupported`, `key_binding_failed`, `challenge_mismatch`, `chain_invalid`, …)
- `revocation_outcome` (`not_checked`, `live`, `revoked`, `error_skipped`)
- `resolved_tier`, final tier stamped onto the request context
The same fields render visually on each agent in the [Inspector agent detail](/inspector/agents): thumbprint, algorithm, attestation envelope, revocation status, resolved tier, and the writes/relationships/sources the agent produced. Every durable row carries this stamp, so any [observation](/inspector/observations-and-sources) or [timeline event](/inspector/timeline) can be filtered or grouped by tier and signing identity after the fact.
◆
## Inspect attribution from the operator console
Everything on this page has a counterpart in the [Inspector](/inspector), Neotoma's read-only operator UI. Use these entry points when debugging an integration or auditing writes:
- [Agents & grants](/inspector/agents) , every signing identity that ever wrote, with tier badges, thumbprint, algorithm, attestation outcome, last-seen activity, and the per-agent `(op, entity_type)` grant table.
- [Settings · Attribution policy](/inspector/settings/attribution-policy) , global mode, minimum tier, and per-path overrides resolved from `NEOTOMA_ATTRIBUTION_POLICY` / `NEOTOMA_MIN_ATTRIBUTION_TIER` / `NEOTOMA_ATTRIBUTION_POLICY_JSON`, with a live decision summary.
- [Observations & sources](/inspector/observations-and-sources) , every immutable write tagged with `resolved_tier` and agent identifiers; filter by tier or by `agent_thumbprint`.
- [Timeline & interpretations](/inspector/timeline) , chronological view of every signed event across the instance, including verification failures and tier promotions.
◆
## Specs and deeper reading
Each topic below has its own dedicated reference page with the full implementation contract:
- [AAuth wire format and verification](/aauth/spec) , signature components, trust-tier cascade, JWT contract.
- [Attestation](/aauth/attestation), envelope formats, per-format verifiers, revocation policy.
- [CLI keys and hardware backends](/aauth/cli-keys) , keygen flows for Apple Secure Enclave, TPM 2.0, Windows TBS, and YubiKey.
- [Integration guide](/aauth/integration), end-to-end walkthrough, preflight, diagnostics, transport parity.
- [Capabilities](/aauth/capabilities), per-agent `(op, entity_type)` allow-lists via `agent_grant` entities.
See [REST API reference](/api) for HTTP endpoints, [MCP reference](/mcp) for agent-native transport, and [CLI reference](/cli) for keygen and session inspection commands.