---
title: "Vault"
description: "Give an agent an API key or other secret without the server ever seeing the plaintext — encrypted on the client, leased one operation at a time."
canonical_url: "https://www.agent-identity.dev/docs/vault"
markdown_url: "https://www.agent-identity.dev/docs/vault.md"
last_updated: "2018-10-20"
agent:
  tokenBudget: 1200
  task: "Register an agent's vault keypair, store a secret encrypted to it, and redeem a single-use lease to read it back."
  outcome: "The agent has a registered vault_public_key, a secret exists with the org never having seen its plaintext, and one lease redemption returns the plaintext exactly once."
  appliesTo:
    package:
      - "@agentidentity/sdk"
      - "@aid/api"
  prerequisites:
    - "The agent's own API key, plus the private half of its vault keypair stored outside this system."
    - "An org key for creating secrets and listing them; those two operations are admin-only."
  files:
    - "apps/api/src/services/vault.ts"
    - "packages/sdk-ts/src/client.ts"
  sideEffects:
    - "generateIdentityKey returns the private key exactly once and overwrites any previously registered public key for that agent."
    - "Redeeming a lease marks it consumed permanently; a second redemption fails."
  verification:
    - description: "Register a key, create a secret, lease it, redeem it."
      expect: "redeemLease returns the original plaintext, and a second redeem of the same lease returns a lease-already-consumed error."
  rollback:
    - "Rotate the secret with the owning identity's keypair to invalidate the old envelope. There is no delete-secret route."
  failureModes:
    - symptom: "Creating a secret returns a validation error naming the agent."
      resolution: "The agent has no vault_public_key. Call generateIdentityKey (PUT /v1/identities/:id/vault-key) for it first — there is nothing to encrypt to otherwise."
    - symptom: "403 when rotating or reading an envelope."
      resolution: "Those operations require an agent key belonging to the owning agent. An org key is refused; it has no private key and could not decrypt anyway."
    - symptom: "A lease redeem fails as expired."
      resolution: "ttl_seconds is clamped to 1–3600 and counts from creation. Create the lease at the moment of use, not ahead of time."
    - symptom: "The private key was lost."
      resolution: "Every secret encrypted to it is unrecoverable by design. Register a new keypair and re-create the secrets from their original sources."
---

<!-- farming-labs:agent-contract:start -->
## Agent Contract

Task: Register an agent's vault keypair, store a secret encrypted to it, and redeem a single-use lease to read it back.
Outcome: The agent has a registered vault_public_key, a secret exists with the org never having seen its plaintext, and one lease redemption returns the plaintext exactly once.

### Applies To

- Package: `@agentidentity/sdk`, `@aid/api`

### Prerequisites

- The agent's own API key, plus the private half of its vault keypair stored outside this system.
- An org key for creating secrets and listing them; those two operations are admin-only.

### Files

- `apps/api/src/services/vault.ts`
- `packages/sdk-ts/src/client.ts`

### Side Effects

- generateIdentityKey returns the private key exactly once and overwrites any previously registered public key for that agent.
- Redeeming a lease marks it consumed permanently; a second redemption fails.

### Verification

- Register a key, create a secret, lease it, redeem it.
  - Expected: redeemLease returns the original plaintext, and a second redeem of the same lease returns a lease-already-consumed error.

### Rollback

- Rotate the secret with the owning identity's keypair to invalidate the old envelope. There is no delete-secret route.

### Failure Modes

- Creating a secret returns a validation error naming the agent. — Recovery: The agent has no vault_public_key. Call generateIdentityKey (PUT /v1/identities/:id/vault-key) for it first — there is nothing to encrypt to otherwise.
- 403 when rotating or reading an envelope. — Recovery: Those operations require an agent key belonging to the owning agent. An org key is refused; it has no private key and could not decrypt anyway.
- A lease redeem fails as expired. — Recovery: ttl_seconds is clamped to 1–3600 and counts from creation. Create the lease at the moment of use, not ahead of time.
- The private key was lost. — Recovery: Every secret encrypted to it is unrecoverable by design. Register a new keypair and re-create the secrets from their original sources.
<!-- farming-labs:agent-contract:end -->

# Vault — machine contract

Authoritative schema: `GET {API_URL}/v1/openapi.json`. Authorization rules below come from
`apps/api/src/services/vault.ts` and are enforced server-side; the SDK does not pre-check
them.

## Invariant

The server never possesses a private key and never sees plaintext. Encryption and
decryption happen in `@agentidentity/sdk` on the caller's machine. Any design
that sends plaintext to `/v1/vault/*` is wrong — the endpoints do not accept it.

Envelope shape on the wire: `{ ciphertext, ephemeral_public_key }`, ECIES over X25519.

## Authorization matrix

Actor types come from the API key: `user` = org key (no `identity_id`), `agent` = key bound
to one identity. These are not interchangeable and the errors differ.

| Endpoint | Required actor | Failure if wrong |
| --- | --- | --- |
| `PUT /v1/identities/:id/vault-key` | `user`, or `agent` where `actor.id === :id` | `403` an agent can only register its own vault key |
| `POST /v1/vault/secrets` | `user` | `403` |
| `GET /v1/vault/secrets` | `user` | `403` |
| `GET /v1/vault/secrets/:id/envelope` | `agent`, owning | `403` if `user`; `404` if not the owner |
| `POST /v1/vault/secrets/:id/rotate` | `agent`, owning | `403` if `user`; `404` if not the owner |
| `POST /v1/vault/secrets/:id/lease` | `agent` owning, or `user` with `identity_id` | see below |
| `POST /v1/vault/leases/:id/redeem` | the lease's own identity | `404` if another agent |
| `GET /v1/vault/leases` | `user` | `403` |

Non-ownership is reported as `404`, not `403`, throughout — a secret belonging to another
identity is indistinguishable from one that does not exist. Do not treat `404` here as
proof of absence.

## Ordering

`createSecret` requires the target identity to already have `publicKey` set, or it returns
a validation error naming the identity. Sequence is fixed:

1. `PUT /v1/identities/:id/vault-key` (or SDK `generateIdentityKey`) — once per identity.
2. `POST /v1/vault/secrets` with `{ name, identity_id, ciphertext, ephemeral_public_key }`.
3. `POST /v1/vault/secrets/:id/lease` with `{ operation, ttl_seconds?, identity_id? }`.
4. `POST /v1/vault/leases/:id/redeem` → envelope; decrypt locally.

`generateIdentityKey` returns the private key exactly once and overwrites any previously
registered public key for that identity, orphaning every secret encrypted to the old one.
Do not call it to "check" whether a key exists — read `vault_public_key` from
`GET /v1/identities` instead, which is `null` when unset.

## Leases

`createLeaseBody`: `operation` (string, `min(1)`), `ttl_seconds` (coerced int, default
`60`), `identity_id` (optional).

- `ttl_seconds` is **clamped**, not rejected: `Math.min(Math.max(ttl, 1), 3600)`. A request
  for 86400 silently yields 3600. Do not rely on a large value being honoured.
- As an `agent`, `identity_id` is ignored and the actor's own id is used; the secret must
  be owned by the actor or the call is `404`.
- As a `user`, `identity_id` is **required** and must equal the secret's `identityId`, else
  a validation error.
- The secret's `status` must be `active`; anything else is `404`.

Redeem is single-use and destructive to the lease:

- Already consumed → lease-already-consumed error. Not retryable; create a new lease.
- Past `expires_at` → lease-expired error. Not retryable; create a new lease.
- `markSecretLeaseConsumed` runs *after* the envelope is fetched, so a client-side crash
  between the response and the local decrypt burns the lease. Create leases at the point of
  use and be prepared to create another.

## Recovery

There is no delete-secret endpoint and no server-side key escrow. If the private key is
lost, the secrets encrypted to it are unrecoverable — register a new keypair and re-create
each secret from its original source. Rotation (`agent`-only) is the way to invalidate an
old envelope while keeping the secret's identity and name.

## Related

- `/docs/getting-started/authentication.md`
- `/docs/concepts/identities.md`

## Sitemap

See the full [sitemap](/sitemap.md) for all pages.
Docs-scoped sitemap: [/docs/sitemap.md](/docs/sitemap.md).
Well-known sitemap: [/.well-known/sitemap.md](/.well-known/sitemap.md).
