ADR 0005: Identity model
One Ed25519 key pair per agent, generated locally, with no refresh token and terminal revocation.
#Context
Every other property in Veefore hangs off agent identity. A capability is granted to an identity, a delegation is issued by one, an audit event records which one acted. If identity is weak, nothing else means anything.
The default industry answer is an API key per agent. It is simple, every language can send a header, and it is a bearer credential — which means possession is proof, and anything that sees it can replay it.
For an agent, that is a worse position than for a service: the credential lives in a process whose job is to produce text from a model, and whose context can be influenced by whatever it reads.
#Decision
One Ed25519 key pair per agent. The agent signs a fresh assertion; Veefore holds only the public half.
json{
"sub": "agt_01JAV…",
"aud": "https://gateway.veefore.example.com",
"purpose": "agent_authentication",
"nonce": "YmFzZTY0dXJsLXJhbmRvbQ",
"iat": "2026-10-03T12:38:04.000Z",
"exp": "2026-10-03T12:40:04.000Z"
}
Generated locally by default. veefore init creates the pair on your
machine and sends only public_key. The private half never crosses the network,
so there is no moment at which Veefore could have logged, cached or leaked it.
Six checks on every assertion: signature, audience as an exact string, timestamp window, single-use nonce, key generation, revocation state. Each has its own error code, so the agent's logs say which one failed.
A generation counter. Rotation bumps it, and an assertion signed with a
superseded key is refused with IDENTITY_GENERATION_STALE rather than a generic
authentication failure.
No refresh token. Session tokens last 15 minutes and the agent re-signs. The key is already a better credential than a refresh token would be.
Revocation is terminal. There is no un-revoke.
The audience is published at /.well-known/veefore-platform-key.
#Alternatives
API key per agent. Simple, and it is a bearer credential. Theft is undetectable, replay is free, and the credential sits in the agent's environment where its own prompt context can reach it. Rejected as the primary mechanism.
mTLS. Genuinely strong, and it moves identity into the transport layer — which means every proxy, load balancer and service mesh between the agent and the gateway has to be configured to pass the client certificate through, and a misconfiguration is silent. It also makes identity something the platform operator controls rather than something the agent holds. Supported as a credential type for downstream tools, not as agent identity.
OIDC with an external identity provider. Good for humans. For agents it adds a dependency on the provider being available in order to authenticate — which under fail-closed means a provider outage refuses all agent traffic — and it makes the provider the root of trust for agent identity.
RSA instead of Ed25519. Universally supported. Larger keys, slower signatures, and more ways to use it wrongly — padding modes, key sizes, a signature scheme that takes a digest. Ed25519 has one way to use it.
Numeric epoch iat/exp, as in standard JWT. Conventional. Every other
timestamp in the Veefore API is ISO-8601, and one format across the whole
surface is worth more than conformance to a convention in one place. Documented
prominently because it does surprise people.
Allow revocation to be undone. Convenient when you revoke the wrong agent. It also means a revocation can be undone by whoever caused the incident, which is not a revocation. Register a new agent instead.
Longer assertion lifetimes. Fewer signatures. A long-lived assertion is a bearer credential, which is the thing this ADR exists to avoid. Two minutes.
#Consequences
Agents must be able to sign. Every language with Ed25519 can, and the two SDKs do it for you — but a runtime without a crypto library cannot participate. For those, Veefore can generate the pair and return the private half once, which is weaker and documented as such.
The private key is not recoverable. Veefore stores the public key and
nothing else. Lose .env.local and you rotate rather than recover. doctor
checks the file permissions, because a key readable by another process on the
host is a key that process can act with.
The audience is the most common onboarding failure. It is compared as a
string, so http://127.0.0.1:4010 and http://localhost:4010 are the same
host and different values — and the symptom is a bare 401 with no explanation.
Mitigated by publishing it at /.well-known/veefore-platform-key and having
veefore init fetch it rather than assume. Publishing it is safe: it is the
value you sign for, and knowing it grants nothing without the key.
Nonce tracking needs shared state in a multi-node deployment. Without
REDIS_URL each instance keeps its own set, so replay protection is weaker than
it looks. Called out in Conventions and
Troubleshooting.
Rotation invalidates every session for that agent. That is the point — it is what makes rotation a containment action rather than a housekeeping one — and it means a long-running agent needs a path to re-read its configuration.
Terminal revocation means revoking the wrong agent costs a re-registration. Accepted deliberately. The alternative is worse.