Reference
Browser vault · Advanced
Browser-session protocol for vault implementers. These routes are separate from the bearer API and hosted MCP.
On this page
Browser vault preconditions
These session-authenticated /account/vault routes are separate from the bearer API and MCP. Their strict JSON bodies are unchanged. Existing host/accounts, same-site, rate-limit, live-account and recent-sign-in guards run first. Every response remains private/no-store and excluded from search indexing.
| Route | Required request headers | Successful response headers |
|---|---|---|
GET /account/vault/ready |
none | none |
GET /account/vault |
none | X-Fmrl-Vault-Revision, X-Fmrl-Rings-Revision |
GET /account/vault/rings |
none | both revisions and X-Fmrl-Rings-Complete (true or false) |
POST /account/vault/unlockers |
X-Fmrl-If-Vault-Revision |
X-Fmrl-Vault-Revision |
DELETE /account/vault/unlockers/{id} |
X-Fmrl-If-Vault-Revision |
X-Fmrl-Vault-Revision |
POST /account/vault/reset |
X-Fmrl-If-Vault-Revision, X-Fmrl-If-Rings-Revision |
both revisions |
PUT /account/vault/rings/human |
X-Fmrl-If-Vault-Revision |
none |
Revisions are application preconditions, not ETags: canonical unpadded base64url of exactly 16 random bytes (22 characters, strict trailing bits). GET headers come from the same snapshot as the returned metadata/boxes and ownership eligibility. No-vault GETs omit revisions. Rings completeness is true only when the raw snapshot has fewer than 500 rows, before filtering; 500, 501 and raw501/filtered499 are incomplete. The metadata boxes count is for display only.
After existing guards, missing required headers return 428 precondition_required; malformed, empty, duplicate, wildcard or multi-value headers return 400 bad_request. Atomic store checks return 409 vault_changed for stale vault revision (deterministically first), 409 rings_changed for stale rings revision, or 409 rings_incomplete for a reset with raw >=500 rows or while any box is hidden from the account's human's snapshot. Errors expose no token or ciphertext and use empty messages. Reset compares the original request tokens on every retry, preserves CreatedAt and replaces metadata/rows/both revisions together. Failed commits change nothing. Unlocker edits advance only the vault revision; successful reset returns both committed tokens.
Setup retains atomic if-none behavior; turn-off and account deletion are explicit destructive operations and remain uncapped. Legacy in-development records without revisions fail closed and require explicit initialization/backfill before conditional routes are enabled; empty tokens never match. Snapshot reads add bounded transactional ownership queries and may retry; cleanup adds an account read/write per committed deletion chunk. Which boxes the GETs show follows the agents attached to the account's human and the humans joined into it. A change to that set moves the rings revision in the same commit when it comes from an agent attached, detached or deleted, a key deleted, a join claimed, or the account taking its human at sign-in. So a reset sent with the rings revision from before the change is 409 rings_changed, and a box the GETs did not show is never deleted by it. A join moving an agent across to the human it joined changes nothing the GETs show and leaves the revision as it is. An agent's ring whose prefix more than 8 keys share belongs to no account, so minting a key can hide a box. That mint leaves the revision as it is, and it is safe: a mint never makes a box show, so a reset sent with the revision from before the mint was built from GETs that showed every box the current GETs show, and it loses nothing a reset with the current revision would keep.
Browsers must associate the vault revision with the exact unlocked key/envelopes, accept rings only with that vault revision and X-Fmrl-Rings-Complete: true, open and retain every box before reset, and use the rings revision returned with those boxes. Conflicts require an explicit fresh attempt; refreshing public metadata never authorizes replay of an old wrap. A legacy plugin PUT begun after reset can still introduce ciphertext sealed to the old public key, and reset followed by separate reseal uploads retains a client-crash window. Local retention is required; this protocol does not provide atomic reset-and-reseal.
Ring ids have two shapes. An agent's ring is named by its key prefix (fmrl_ plus four alphanumerics); a human's ring by the human principal's id (64 lowercase hex), which only the browser sends. A ring is the account's when it belongs to the account's human: an agent attached to that human (or to a human joined into it), the human's own ring, or the ring of a human joined into it. Anything else is 400 ring_ownership on PUT /account/vault/rings and is left out of both GETs without being deleted. A batch never replaces an existing human box: that is 409 rings_changed, and the whole batch is refused.
Human-ring bootstrap: PUT /account/vault/rings/human with the body {"prefix": "<64-hex>", "box": "<box>"} stores the box of a human ring only when the account holds none under that id. X-Fmrl-If-Vault-Revision is the vault revision the box was sealed against, and is required; it is not a rings revision. 201 {} on create; 409 rings_changed when a row exists, which is never replaced; 409 vault_changed for a stale vault revision; 403 ring_ownership for a ring that is not the account's. A client whose reply was lost reads GET /account/vault/rings and compares what it finds; it does not resend.
Setup readiness: GET /account/vault/ready requires a live account and recent sign-in under the existing browser guard order. It returns 200 {} or the existing 403 recent_required and never reads rings, generates secrets or mutates state. Call before recovery generation and before authenticator enrollment; use /account/signin?next=vault if stale. A successful preflight can expire, so setup still enforces recent sign-in at commit.
Coordinated vault availability and ingress amendment
Direct GET /me distinguishes no vault (account_vault omitted), available vault (public object), and an unavailable account-vault hint (account_vault:null). A hint outage still returns quota, prefix and eligible attach_url; hosted MCP and PATCH omit this field entirely. Browser vault reads use a separate 120-per-IP-per-instance hourly bucket (VAULT_READS_PER_HOUR), independent of share/sign-in write budgets; browser mutations and API rings retain existing budgets.
Browser PUT /account/vault/rings accepts optional X-Fmrl-If-Vault-Revision, a single canonical 16-byte revision from the exact public vault used to seal. Plugin PUT /me/rings accepts optional fingerprint, the exact canonical grouped RFC4648 fingerprint used to seal. Absent fields retain legacy behavior. Present malformed/empty/null fingerprint or malformed/duplicate header is 400 bad_request. Stale observation is 409 vault_changed, checked atomically across retries with no partial rows. Browser revision detects same-public-key reset; fingerprint alone detects changed public key. Foreign/app/ambiguous ownership is 400 ring_ownership, empty message and whole-batch refusal. Attempts still spend write budget; clients should skip empty uploads. Legacy unobserved/same-public-key plugin uploads retain their documented limit.