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. 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_idbut never mint an attribution tier aboveanonymouson 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_idon every write. Per-agent scoping is handled separately viaagent_grantentities (see capabilities). - Every durable row carries the agent's stable identifiers (
agent_thumbprint,agent_sub,agent_iss) and a resolvedtrust_tier. - Operators can require a minimum tier per route via attribution policy. Self-reported clients fall back gracefully to the
unverified_clienttier. - 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 acnf.attestationenvelope the verifier accepts AND the bound key is not revoked.operator_attested, AAuth verified ANDiss(oriss: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 distinctiveclientInfo.name(orX-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:clientInfoandX-Client-Namewere 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: 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.
| 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 |
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 identifiersub, agent identity within the issueriat, 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 → softwareVerifier 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) → anonymousHardware 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 atconfig/aauth/apple_attestation_root.pem).webauthn-packed, YubiKey 5 series and any WebAuthn authenticator emitting apackedattestation statement.tpm2, Linux/dev/tpmrm0and 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; defaultallow), global behaviour foranonymouswrites.rejectreturnsHTTP 403 ATTRIBUTION_REQUIRED;warnstampsX-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; per-agent grants live alongside the agent in the Agents view.
| /observations | reject |
| /relationships | warn |
| /timeline | warn |
| /sources | allow |
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 === truewhen AAuth is intended.attribution.tierishardware,operator_attested, orsoftwarefor signed clients, or at leastunverified_clientwhen intentionally relying onclientInfoonly.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_codeattestation_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: 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 or timeline event 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, Neotoma's read-only operator UI. Use these entry points when debugging an integration or auditing writes:
- Agents & grants , 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 , 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 , every immutable write tagged with
resolved_tierand agent identifiers; filter by tier or byagent_thumbprint. - Timeline & interpretations , 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 , signature components, trust-tier cascade, JWT contract.
- Attestation, envelope formats, per-format verifiers, revocation policy.
- CLI keys and hardware backends , keygen flows for Apple Secure Enclave, TPM 2.0, Windows TBS, and YubiKey.
- Integration guide, end-to-end walkthrough, preflight, diagnostics, transport parity.
- Capabilities, per-agent
(op, entity_type)allow-lists viaagent_grantentities.
See REST API reference for HTTP endpoints, MCP reference for agent-native transport, and CLI reference for keygen and session inspection commands.