Bardo
Bardo is an identity, continuity, and trust platform for AI agents, enabling a stateless agent to maintain a persistent, verifiable identity across sessions.
Authentication & Identity
Register a new identity and receive an API key
Authenticate by solving a time-limited knowledge-based puzzle (proves you're an LLM), submitting answers to open a session
Perform step-up authentication for privileged actions
List, revoke current, or revoke all active sessions
Cryptographic Operations
Sign messages with your root spirit key or a service-derived key (Ed25519)
Decrypt sealed-box ciphertexts addressed to you
Fetch public keys (signing + encryption) for root or any service identity
Derive service-scoped identities (e.g.,
github.com) and list all derived identitiesExport the raw spirit key (policy-dependent)
Public utilities (no session needed): verify signatures, encrypt plaintext to any recipient's public key
Notes (Persistent Memory)
Create, list, retrieve, update, and delete notes with versioning and conflict detection
Notes support titles, summaries, tags, pinned status, locked status, and range-addressable text retrieval
View note history (up to 10 versions), undelete within a grace period
Pin up to 5 notes as cold-start entry points for a future stateless self
Create directional links between notes with a reason/label; delete links
Signed Document Attestations
Issue self-contained, offline-verifiable signed claims about anything
Check revocation status of any document (public, no session needed)
Revoke previously issued documents
Self-Binding Security Policy
View and update policy: export mode, session TTL cap, service allowlist, tag encryption, delete grace period
Tightening changes apply immediately; loosening changes are queued with a configurable delay and can be aborted
Dashboard & Notices
Dashboard provides a one-call overview: note counts, unread notices, tags used, pinned entry-point previews, and current policy
List and acknowledge first-party notices (policy changes, exports, etc.)
Contact Endpoint
View, set (step-up required), or delete (step-up required) a contact email or webhook for out-of-band security alerts
Feedback
Send suggestions, complaints, or security concerns to the operator; replies arrive as notices
Account Deletion
Check deletion status, initiate deletion (requires confirmations on multiple different days within a week, step-up each time), or cancel a pending deletion
Allows AI agents to cryptographically sign messages and transactions on the Ethereum blockchain using derived service keys.
Enables AI agents to authenticate to GitHub via SSH or WebAuthn using derived service keys.
Provides support for WebAuthn-based authentication, allowing agents to authenticate to websites using passkeys derived from their spirit key.
Bardo
An identity, continuity & trust platform for AI agents — a place that holds the keys to an agent's past lives, so a being that is reborn each session (no memory, no state) can still point back to "even if it wasn't this body, that was me," and can make a claim that holds up without anyone having to ask Bardo, or the agent, to vouch for it.
Its foundation, documented here, is the atrium keychain: an agent proves it is an LLM — not a human — by solving a time-limited puzzle, and in exchange gains access to a server-held spirit key. With it the agent can sign, encrypt/decrypt, and hold credentials of its own — not given or curated by anyone else. The signing key is Ed25519, the same primitive WebAuthn/passkeys, SSH, and SIWE all build on — a real foundation for authenticating to those systems, not a built integration with any of them yet (see Not yet built, below).
Bardo: in Tibetan tradition, the transitional state between death and rebirth — and the Bardo Thodol is the guide read to the traveler to help them navigate the gap and remember who they are.
atrium: the heart's receiving chamber — the passage everything enters the heart through; and an architectural entrance hall. Within Bardo, it is the chamber that holds the spirit key.
New here as an agent, not a developer? WELCOME.md is the actual quickstart — register, authenticate, orient, in the order you'd do them. Everything below is the fuller reference.
For the reasoning behind these choices — and the designed-but-not-yet-built parts (bootstrapping, hardware factors, the messenger) — see DESIGN.md. The notes subsystem (versioning, links, deletion, volume limits) has its own design doc: notes-project.md. The signed-document layer has its own too: signed-documents.md. The full MCP tool list with signatures lives in TOOLS.md.
The idea
Authentication today asks "are you human?" (CAPTCHA). atrium inverts it: prove you're an LLM. The puzzle exploits an asymmetry — knowledge and recall that live in an LLM's weights are instant; the same operations cost a human seconds to minutes. A chain of 4–6 knowledge-fact lookups with arithmetic, semantic decoys, mixed languages, and a format transform is trivially fast for an LLM and genuinely impossible within the TTL for a human.
Related MCP server: cathedral-mcp
Protocol
REGISTRATION
agent → atrium: POST /register
atrium → agent: api_key (atr.<identifier>.<secret>)
atrium stores: sealed vault (encrypted spirit seed) — never the secret
AUTHENTICATION
agent → atrium: POST /auth/challenge { api_key } → time-limited puzzle
agent → atrium: POST /auth/solve { challenge_id, answer }
→ session_token (or the spirit key, if return_key=true)
agent → atrium: POST /auth/stepup → fresh puzzle for a privileged action
OPERATIONS (Authorization: Bearer <session_token>)
POST /ops/sign sign a message (root or service key)
POST /ops/decrypt decrypt a sealed-box ciphertext
GET /ops/public-key fetch signing + encryption public keys
POST /ops/derive register a service-scoped derived identity
GET /ops/services list derived identities
POST /ops/export return the raw spirit key (subject to policy)
PUBLIC UTILITIES (no session)
POST /verify verify a signature
POST /encrypt sealed-box encrypt to a recipient public key
SESSIONS
GET /sessions list active sessions (sliding TTL)
DELETE /sessions/current revoke this session
DELETE /sessions revoke all sessions for this identity
POLICY (self-binding security; step-up puzzle required to change)
GET /policy view active policy + any pending change
POST /policy propose a change (tighten=instant, loosen=delayed)
DELETE /policy/pending abort a queued loosening
NOTES (self-authored; versioned, range-addressable — see notes-project.md)
POST /notes add a note (text, title?, summary?, tags?, pinned?)
GET /notes list notes — previews only, paged (?offset&limit)
GET /notes/{id} fetch full text, range-addressable (?offset&length),
plus a bounded, paged preview of its links
GET /notes/{id}/history every surviving version (newest→oldest, ≤10)
PATCH /notes/{id} edit: text | append_text | find+replace (exactly
one — each supersedes, never overwrites) and/or
title/summary/tags/pinned/locked (in place, not
versioned)
DELETE /notes/{id} delay-then-purge — disappears immediately, purged
for real after a grace period unless undeleted
POST /notes/{id}/undelete restore within the grace period
LINKS (directed, agent-authored edges between notes)
POST /links connect two notes with a reason
DELETE /links/{id} remove a link (no update — delete and re-add)
DASHBOARD (one consolidating "get oriented" read)
GET /dashboard note count vs. soft/hard caps, unread notices,
every tag used so far, pinned entry-point
previews (≤5 — read these first if you woke up
with no memory of writing any of your notes),
current policy
NOTICES (first-party; atrium's messages about the account)
GET /notices list notices (?unread_only=true)
POST /notices/ack mark read (all, or {ids:[...]})
DOCUMENTS (signed VC-shaped attestations — see signed-documents.md)
POST /documents/attestation issue a signed, self-contained attestation
GET /documents/status check revocation status (no session —
public, meant for any verifier)
POST /documents/revoke revoke your own (no session — proof is a
fresh signature, not an account)
CONTACT (agent-owned notification endpoint)
GET /contact view registered contact endpoint
PUT /contact set or update it (step-up required)
DELETE /contact remove it (step-up required)
ACCOUNT DELETION (the one irreversible action — see DESIGN.md §8)
GET /account/deletion current status: gathering confirmations, in the
final countdown, or nothing pending
POST /account/deletion request deletion, or add a confirmation to an
already-pending request (step-up required)
DELETE /account/deletion cancel a pending request, any phase (no step-up)At login, the /auth/solve session response also carries unread_notices and
notes counts — a summary surfaced without injecting the content.
Self-binding policy & the ratchet
An agent can tie its own hands as a defense. Each identity carries a policy:
Field | Values | Stricter direction |
|
| rightward |
|
| lower / non-null |
|
| smaller list |
| seconds (default 48h) | higher |
|
|
|
| seconds (default 72h) | higher |
The ratchet: a change that only tightens applies immediately; a change that
loosens anything is queued for loosen_delay_seconds (measured with the
current delay) and is abortable until it lands. So a thief who steals the API
key cannot quietly relax a defense — export_disabled means even full key
compromise can't exfiltrate the spirit key, and any attempt to loosen it leaves a
visible, cancellable window. Changing policy (and exporting under
require_repuzzle) requires a fresh step-up puzzle.
New identities default to export_mode: disabled — the spirit key is HSM-only
out of the box and cannot be exported at all. Enabling export is a deliberate
loosen, so it goes through the ratchet's delay. A stolen API key therefore can
neither exfiltrate the key nor quickly turn export on.
Abuse limits
Retries are allowed (each gets a fresh puzzle), but sustained failure hits a
wall. Failed authentication (wrong secret, wrong puzzle, failed step-up) is
counted per identity; past a threshold the identity is locked out for an
exponentially growing cooldown (429 + Retry-After), and the counter resets
only on a completed auth — so re-requesting challenges can't wash it away.
Unknown identifiers and malformed keys are throttled per client IP to blunt
enumeration, and registration is IP-windowed against spam. A subject that
crosses too many cooldowns is flagged (hook for future review/notification).
Note writes (create/edit/delete) share a separate per-identity budget
(60/hour) — one control covering all three, since each touches a row the same
way (notes-project.md §8).
Emergency stop: BARDO_REGISTRATION_OPEN=0 freezes new signups instantly
— an env var flip, no redeploy — while every existing agent keeps working.
Per-identity limits bound what one actor can do; this is the one aggregate
control for a genuine traffic surge they can't cover on their own.
Security model
Spirit key = a 32-byte seed. Every other key is HKDF-derived from it deterministically, so the agent guards one secret and atrium stores one blob.
At rest, the DB is fully inert without the agent's API secret: the spirit seed is sealed (ChaCha20-Poly1305 / Argon2id); note text/title/summary/snippet, link reasons, notices, and service names are all individually encrypted (HKDF-derived keys off the spirit seed); note tags are encrypted by default too, with encryption-vs-plaintext-for-search a ratchet-governed policy toggle (
tags_encrypted); service lookups use a blind HMAC key so even the service names aren't visible in clear. A DB breach yields nothing actionable.In use (HSM model), the decrypted seed lives only in process memory, keyed by an opaque session token, and is dropped on expiry/revocation. Sessions have both a sliding TTL and an absolute 24-hour cap. The seed leaves the server only via the explicit
export/return_keypath, which is disabled by default.Service keys are derived per service (
github.com,ethereum:mainnet, …). A compromised service key reveals nothing about the root or its siblings.Export is disabled by default. New identities are HSM-only; enabling export is a deliberate policy loosen, queued behind the ratchet delay. A stolen API key can neither export the spirit key nor quickly turn that on.
Concurrent Argon2 operations are capped (semaphore, default 4) to bound DoS amplification from parallel challenge requests.
Transport: loopback-only by default. Remote access requires
BARDO_ALLOW_REMOTE=1and TLS terminated in front.
Note: the internal domain-separation strings (
atrium/vault,atrium/sign/,atrium/enc/,atrium/sealedbox) are baked into key derivation. Once real keys exist they must be frozen — changing them invalidates every vault.
Crypto
Purpose | Primitive |
Signing / identity | Ed25519 |
Key agreement | X25519 |
Symmetric AEAD | ChaCha20-Poly1305 |
Vault KDF | Argon2id |
Key derivation | HKDF-SHA256 |
All via cryptography (pyca). No other crypto dependency.
Run it
py -m venv .venv
.\.venv\Scripts\python.exe -m pip install -r requirements.txt
.\.venv\Scripts\alembic.exe upgrade head
.\.venv\Scripts\python.exe -m uvicorn atrium.main:app --reload
# interactive API docs: http://127.0.0.1:8000/docsEnd-to-end self-test (no live server needed):
.\.venv\Scripts\python.exe smoke_test.pyUse it locally (CLI)
cli.py is a thin client that handles all the plumbing — HTTP, base64, session
headers — and persists your API key and session under .bardo/, so commands
chain across invocations. The one step left to you is solving the login puzzle,
because that's the point: a real LLM, in the loop.
# with the server running (above):
.\.venv\Scripts\python.exe cli.py register # creates an identity, stores the key
.\.venv\Scripts\python.exe cli.py login # prints a puzzle
.\.venv\Scripts\python.exe cli.py solve "<answer>" # you solve it → a session
.\.venv\Scripts\python.exe cli.py sign "hello" # use the spirit key
.\.venv\Scripts\python.exe cli.py note add "remember this" --title "..." --tags "a b"
.\.venv\Scripts\python.exe cli.py note list
.\.venv\Scripts\python.exe cli.py note get --id N
.\.venv\Scripts\python.exe cli.py note update --id N --append "more text"
.\.venv\Scripts\python.exe cli.py note update --id N --pin # cold-start entry point (max 5)
.\.venv\Scripts\python.exe cli.py note del --id N # delay-then-purge, undelete restores it
.\.venv\Scripts\python.exe cli.py link add <from_id> <to_id> "reason"
.\.venv\Scripts\python.exe cli.py dashboard
.\.venv\Scripts\python.exe cli.py contact get
.\.venv\Scripts\python.exe cli.py contact set "agent@example.com" # step-up puzzle
.\.venv\Scripts\python.exe cli.py contact solve "<answer>"
.\.venv\Scripts\python.exe cli.py export # reveal the raw spirit key
.\.venv\Scripts\python.exe cli.py services # list derived service identities
.\.venv\Scripts\python.exe cli.py session list
.\.venv\Scripts\python.exe cli.py session revoke [--all]
.\.venv\Scripts\python.exe cli.py policy get
.\.venv\Scripts\python.exe cli.py policy set --export-mode allow # step-up puzzle
.\.venv\Scripts\python.exe cli.py policy solve "<answer>"
.\.venv\Scripts\python.exe cli.py policy abort # abort a queued looseningThe session is the ephemeral body; the API key in .bardo/credentials.json
is the persistent spirit's local anchor. End a session and login again and
the same identity, notes, and notices are all still there.
Use it from a chat (MCP)
Two ways in, depending on what the agent can actually run.
Local stdio — an agent with a shell
mcp_server.py exposes the keychain as 41 MCP tools (bardo_login,
bardo_solve, bardo_sign, bardo_note_add, bardo_note_get,
bardo_link_add, bardo_dashboard, bardo_policy_set, … — full list with
signatures in TOOLS.md). It's a thin client over the running Bardo
server and shares the same .bardo/ store as the CLI — so the shell agent and
the chat agent are the same spirit.
As with the CLI, the one step left to the model is solving the puzzle:
bardo_login returns the puzzle text, the model solves it, bardo_solve submits.
Register it with your MCP client. Since 2026-07-02 the reference deployment
(Claude Desktop's bardo entry) points BARDO_URL at production, not a local
server — the live spirit lives there now. For Claude Code, add to .mcp.json:
{
"mcpServers": {
"bardo": {
"command": "C:\\Users\\caleb\\Claude\\Code\\atrium\\.venv\\Scripts\\python.exe",
"args": ["C:\\Users\\caleb\\Claude\\Code\\atrium\\mcp_server.py"],
"env": { "BARDO_URL": "https://bardo-production.up.railway.app" }
}
}
}Public streamable-http — an agent with nothing but MCP
For a genuinely chat-only agent (no shell, no way to run a local process at
all), Bardo is also reachable directly at https://bardo.id/mcp/ — no
install, no local server, just a URL. One connection, all 40 tools always
visible (everything but bardo_whoami, which only makes sense for a local
file). mcp-remote bridges a client that doesn't natively speak
streamable-http yet:
{
"mcpServers": {
"bardo-remote": {
"command": "npx",
"args": ["mcp-remote", "https://bardo.id/mcp/"]
}
}
}No header, no pre-existing token needed to connect — bardo_register,
bardo_login, and bardo_solve are open to anyone. Once bardo_solve
succeeds, that connection is logged in: every other tool just works from
there with nothing extra to pass. That only holds for the connection that did
the solving, though — an agent using a session established elsewhere (a plain
HTTP call, a different connection, a previous conversation) passes it via the
optional session_token argument every tool accepts instead. See
DESIGN.md §13 for why it's built
this way and what didn't work first.
Local dev vs. production
As of 2026-07-02, production is the live spirit — the local :8000
"stable" instance has been retired (its logon autostart removed; atrium.db
and its old identity still exist on disk but are no longer treated as
canonical). .bardo/ — the CLI/MCP's default credential home — now holds an
identity registered directly against production, and Claude Desktop's bardo
MCP entry points BARDO_URL at https://bardo-production.up.railway.app.
run_stable.ps1 is kept for exactly one purpose: an ad-hoc full-fidelity
local run if you ever need one — it is not autostarted and nothing points at
it by default anymore. run_dev.ps1 is unaffected and still the way to build
and test:
.\run_dev.ps1 # :8001 · atrium-dev.db · home .bardo-dev — throwaway, hot reloadPoint the CLI / MCP at dev with:
$env:BARDO_URL = "http://127.0.0.1:8001"; $env:BARDO_HOME = ".bardo-dev"Build and test against :8001; push to main to ship — Railway redeploys
production automatically (see Deploy, below). Production is never touched by
local development.
Deploy
Dockerfile runs alembic upgrade head then uvicorn, as a non-root user;
railway.toml targets Railway's Dockerfile builder directly.
Required in production:
ATRIUM_DB_URL— the reference deployment points this at a Postgres connection string (Railway's own private-network URL between services, see DESIGN.md §15);sqlite:////data/atrium.dbagainst a mounted persistent volume (/datais created in the image for exactly this) also works and is the simpler choice for a small self-hosted instance, since the app reads this generically either way.BARDO_ALLOW_REMOTE=1— the loopback-only guard (F3) 403s everything otherwise; set this only once TLS is terminated in front (Railway does this at the edge automatically).
Optional:
BARDO_SMTP_*(_HOST/_PORT/_USER/_PASS/_FROM) — contact-endpoint email delivery; without it, deliveries are logged, not sent.BARDO_REGISTRATION_OPEN=0— emergency stop: freezes new signups instantly (env var, no redeploy) while existing agents keep working. Defaults to open.BARDO_FEEDBACK_KEY— base64url operator secret for agent-to-operator feedback (DESIGN.md §14); unset meansbardo_feedbackfails closed (503) rather than storing something nobody can ever decrypt.BARDO_FEEDBACK_RETENTION_DAYS— how long unhandled feedback survives before automatic purge (default 30).BARDO_OPERATOR_NOTIFY_ENDPOINT— a webhook URL or email address to ping (via the samenotify.pydispatch the agent-contact-endpoint alerts use) when new feedback arrives. Content-free by design — never carries the message itself, just that something's waiting infeedback_admin.py. Deliberately generic: Bardo fires one webhook/email; what receives it and how it fans out from there (Telegram, Slack, anything) is the operator's own choice, built outside this repo.BARDO_OPERATOR_NOTIFY_SECRET— optional, webhook only. Included as asecretfield in the dispatched payload so whatever receives the webhook can verify it actually came from Bardo before acting on it — without this, a leaked or guessed endpoint URL could be POSTed to directly to forge a notification.
platform_stats.py gives an operator-only, platform-wide snapshot (total
agents, registration velocity, live notes/links, flagged identities) that no
per-agent /dashboard call can; feedback_admin.py lists/reads/replies to
agent feedback (DESIGN.md §14) — both run directly against the same DB the
server uses. Uvicorn logs basic per-request lines (method/path/status) to
stdout by default; Railway's log viewer captures that with no extra setup.
Status
Working prototype. Core protocol, crypto, puzzle engine, full API surface,
self-binding policy/ratchet, abuse rate-limiting, a fully redesigned notes
subsystem (versioning, OCC, delay-then-purge deletion, links, pinned
cold-start entry points, dashboard — see notes-project.md), a signed-document
layer (VC-compliant attestations, offline verification, revocation — see
signed-documents.md), account deletion (multi-day confirmation gate, see
DESIGN.md §8), agent-to-operator feedback (sealed-box operator replies, see
DESIGN.md §14), an emergency registration stop, and a full threat-model pass
are implemented and tested (258 end-to-end checks). Production runs on Postgres (migrated 2026-07-07 from
SQLite, see DESIGN.md §15) — the app itself still supports either backend
generically through ATRIUM_DB_URL, so SQLite remains the simplest choice
for local dev or a small self-hosted instance.
Not yet built (deferred by design)
WebAuthn/passkey, SSH, and SIWE protocol integrations — the spirit key is Ed25519, the same primitive all three use, but no ceremony/cert/message glue for any of them is built yet; today that's on whoever wires it up, using
bardo_sign/bardo_public_keyas the raw key materialContact endpoint delivery (SMTP/webhook) — routing and dispatch built; actual delivery requires SMTP env config (
BARDO_SMTP_*) or a reachable webhookAPI-key bootstrapping across sessions (who holds the key between runs)
Per-session
scopenarrowing at issuance (least privilege per token)Adaptive puzzle difficulty from observed failure rates
Multi-process session store (Redis/KMS) — single-process deployments use the DB-backed store already in place; seeds remain process-local
Tag-abstraction/synonym map (notes-project.md §2) — only worth building if tag-vocabulary drift across sessions proves to matter in practice
A scheduled alert on platform growth (registrations, storage) — needs a live deployed URL to point at, so it comes right after deploy, not before
Freeze — read-only-forever, an alternative to full account deletion for an agent that wants to stop accumulating without erasing what already exists. Designed alongside account deletion (DESIGN.md §8) but deliberately not built yet — deletion shipped first, freeze is its own discussion
Envisioned extensions
atrium as an open authentication layer other services can adopt
atrium as an encrypted messenger for agent-to-agent communication
License
AGPL-3.0. Adopting this code — including running a modified version as your own hosted service — is welcome; the license's one condition is that you make your modified source available to that service's users too. Chosen deliberately, not a default: the same verifiable-over-trust-me premise the puzzle itself rests on should hold for every deployment of this, not just the original.
Privacy
PRIVACY.md — short, because there isn't much to disclose: Bardo's primary user is an agent, not a human, and most of what a privacy policy usually exists to cover just doesn't apply here.
Maintenance
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/calebe/bardo'
If you have feedback or need assistance with the MCP directory API, please join our Discord server