Agent identity
Every agent holds its own Ed25519 key pair, proves possession on every request, and can be revoked terminally.
An agent's identity is a key pair. Not an API key, not a name, not a row in a table that a service happens to trust. The agent holds an Ed25519 private key and signs a fresh assertion for every session; Veefore holds only the public half and verifies it.
This matters because almost every other property in the system hangs off it. A capability is granted to an identity. A delegation is issued by an identity. An audit event records which identity acted. If identity is weak, none of the rest means anything.
#What identity is not
The name is not the identity. name is display metadata. Two agents can be
called finance-agent. Policy can test agent.name, and it is occasionally
useful to, but a rule that depends on a name is a rule that a rename breaks.
Authorization resolves on agent.id.
A bearer token is not the identity. The gateway does issue a short-lived
session token, but you get one by presenting a signed assertion. A stolen token
expires in minutes (AGENT_TOKEN_TTL_SECONDS, 900 by default) and cannot be
refreshed without the key.
The SDK is not the identity. The SDK formats and signs the assertion. The key is the identity. You can sign your own — see Sign your own assertion — and the gateway cannot tell the difference, which is exactly the property you want from a security boundary.
#How the key gets created
Two paths, and the first is better.
You generate it. veefore init creates the key pair locally and sends only
the public key in POST /v1/agents. The private half never crosses the network,
so there is no moment at which Veefore could have logged it, cached it or leaked
it. This is the default.
Veefore generates it. If you omit public_key, the registration response
includes private_key once, base64 PKCS#8. It is not stored and not
recoverable. Use this only when the agent runs somewhere that cannot generate a
key, and treat the response as a secret in transit.
The private key is written to `.env.local` at mode 0600
veefore doctor checks the file permissions and warns if the key file is
group- or world-readable. It is a real finding: a key readable by another
process on the host is a key that process can act with.
#Proving possession
httpPOST /gateway/v1/token
The body is a signed assertion over a canonical JSON payload: the agent id, the key id, the audience, a purpose, a nonce and a timestamp. The gateway checks all of it.
| Check | Refusal |
|---|---|
| Signature verifies against the stored public key | AGENT_AUTHENTICATION_FAILED |
aud matches the gateway's published audience, as a string |
AGENT_AUTHENTICATION_FAILED |
| Timestamp is inside the acceptance window | TIMESTAMP_OUT_OF_WINDOW |
| Nonce has not been seen before | NONCE_REPLAYED |
| Key generation is current | IDENTITY_GENERATION_STALE |
| Identity is not revoked | AGENT_IDENTITY_REVOKED |
The audience is compared as a string
http://127.0.0.1:4010 and http://localhost:4010 are the same host and
different strings, and the second will be refused if the gateway is configured
with the first. This is the single most common onboarding failure, and it
presents as a bare 401.
Fetch the authoritative value rather than guessing it:
bashcurl -s https://gateway.veefore.example.com/.well-known/veefore-platform-key
The response carries audience alongside the platform verification key.
veefore init does this for you and writes what the gateway actually published.
The nonce check is what makes replay useless. A captured assertion cannot be submitted a second time, even within its validity window, so an attacker who reads one off the wire gains a token they could already see.
#Lifecycle
Seven states, and the distinctions between them are operational, not cosmetic.
| State | Reversible | Means |
|---|---|---|
PROVISIONING |
— | Registered, identity not yet fully issued |
ACTIVE |
— | Operating normally |
PAUSED |
yes | Operational stop. No new protected actions. |
SUSPENDED |
yes | Security stop. Same effect, different intent and audit severity. |
REVOKED |
no | Identity invalidated. Terminal. |
EXPIRED |
— | Credential lifetime elapsed |
DECOMMISSIONED |
no | Retired for good |
PAUSED and SUSPENDED both stop protected execution immediately. Keeping them
separate matters when someone reads the audit trail in six months: "paused for a
deploy" and "suspended because the key may be compromised" are not the same
event, and collapsing them loses the only information that mattered.
REVOKED is terminal by design. There is no un-revoke. If you revoke the wrong
agent, you register a new one — which is inconvenient, and far less
inconvenient than a revocation that can be undone by whoever caused the incident.
bashveefore agents revoke agt_01ABC --reason "Key suspected compromised"
The CLI shows the full blast radius — sessions that die, delegations that
cascade, approvals that are abandoned — before asking for confirmation. It also
requires step-up authentication, because agent:revoke is both high impact and
step-up gated.
#Rotation
bashveefore agents rotate-key agt_01ABC
Rotation issues a new key pair and bumps the identity generation. Every
outstanding session for that agent dies immediately, and any assertion signed
with the old key is refused with IDENTITY_GENERATION_STALE rather than a
generic authentication failure — so the agent's logs say what actually happened.
Rotate on a schedule, and rotate immediately on any suspicion. veefore doctor
flags identities that have not rotated in a long time.
#What policy can see
| Field | Type |
|---|---|
agent.id |
string |
agent.name |
string |
agent.status |
PROVISIONING ACTIVE PAUSED SUSPENDED REVOKED EXPIRED DECOMMISSIONED |
agent.risk_level |
LOW MEDIUM HIGH CRITICAL |
agent.verification_status |
UNVERIFIED PENDING VERIFIED FAILED |
agent.protocols |
HTTP MCP A2A |
agent.capabilities |
string list |
agent.tags |
string list |
Full list with operators in Policy fields.