Asphodel
← docs.asphodel.ai
Documentation · Self-hosted

Run Asphodel in your own environment


An MCP server that gives your agents durable, hash-chained memory under a signed identity, and serves licensed expert corpora as ranked passages. Your container, your disk, your network. The licence is verified offline and the software never calls home.

What it is

Asphodel is one Python service, shipped as one container, that speaks the Model Context Protocol. It holds two kinds of store and nothing else:

By default there is no LLM anywhere in the serving or entitlement path. Routing is lexical, gating is an Ed25519 signature check plus clock arithmetic. The same query returns the same passages, deterministically, with no network egress.

The image ships with no corpora. /mindsets is created empty. The framework is the engine; corpora are licensed separately and mounted in. An install with no mindsets is still fully useful — memory is ungated at every tier, including with no licence at all.

Install

Your licence is also your registry password. The username is the account reference printed on your receipt and in your licence email.

docker login registry.saluca.com -u <your account reference> -p "$ASPHODEL_LICENSE"
docker pull registry.saluca.com/saluca/asphodel:latest

Nothing external is required. No database to provision, no broker, no network dependency — state is SQLite on a volume you own.

docker run -d --name asphodel -p 8351:8351 \
  -e ASPHODEL_LICENSE="$ASPHODEL_LICENSE" \
  -e ASPHODEL_TENANT=default \
  -v asphodel-data:/data \
  -v "$PWD/mindsets:/mindsets:ro" \
  registry.saluca.com/saluca/asphodel:latest

Confirm it is up, and what it thinks your entitlement is:

curl -s localhost:8351/healthz
docker exec asphodel asphodel license scopes

license scopes is the command to reach for whenever a tool returns not_entitled. It prints the licence status, the effective tier, and every scope derived from it — which is the whole subscription model in one screen.

A licence is scoped to this product. The registry checks your licence before it will hand over the image, and refuses one issued for another product in the family. That is enforced at the registry, not merely stated here.

Licence

The licence is an Ed25519 JWT — optionally a hybrid Ed25519 + ML-DSA-44 token, where both signatures must verify or the token is rejected. It is checked against a public key baked into the image at build time, so verification is entirely offline: there is no licence server, no activation call, and no network dependency for it to fail on.

Supply it whichever way suits your secret management. The literal wins over the file if both are set.

VariableMeaning
ASPHODEL_LICENSEThe JWT itself.
ASPHODEL_LICENSE_FILEPath to a file containing it. Preferred when you mount secrets.
ASPHODEL_GRACE_HOURSHours an expired licence is still honoured. Default 72.

The four states

StateMeaningEffect
validSignature good, not expired.Your tier and features apply.
graceExpired less than ASPHODEL_GRACE_HOURS ago.Tier still honoured; a warning is logged every call.
missingNo licence configured.Runs as community: memory works, mindsets refuse.
invalidBad signature, tampered, malformed, or past grace.Degrades to community. Never fails open.
An unlicensed install still runs. Memory reads and writes are ungated at every tier by design — your agents' own recollections are yours whether or not you are paying us. A lapsed licence makes the product smaller, never more permissive and never silently wrong: you get a structured not_entitled refusal naming the feature required, which you can alert on.

The licence is re-validated on every gated call, not cached at boot. A licence that crosses from grace into expiry takes effect on the next tool call, with no restart and no watchdog.

When your subscription renews we mint a fresh licence and email it. Restart with the new value. The expiry date is stated in every licence email, so you never have to decode a token to find out when it lapses.

Connect a client

Two transports, selected by which command you run. There is no transport environment variable.

Streamable HTTP — for services

asphodel serve is the container default and listens on :8351. Point any MCP client at http://<host>:8351/mcp.

stdio — for desktop clients

asphodel mcp speaks stdio, which is what Claude Desktop and most local MCP clients expect:

{
  "mcpServers": {
    "asphodel": {
      "command": "docker",
      "args": ["run","--rm","-i",
               "-e","ASPHODEL_LICENSE",
               "-v","asphodel-data:/data",
               "-v","/srv/mindsets:/mindsets:ro",
               "registry.saluca.com/saluca/asphodel:latest","mcp"],
      "env": { "ASPHODEL_LICENSE": "<your licence>" }
    }
  }
}
The single-tenant server has no authentication of its own. It trusts whoever can reach the port and serves them as ASPHODEL_TENANT. The default bind is 127.0.0.1; the container overrides it to 0.0.0.0 because a container-local bind would be unreachable. Put it behind your own authenticating proxy or on a private network — do not expose :8351 to the internet.

Configuration

These are the variables that matter for a self-hosted install. Everything else in the package is operator-side and has no effect here.

VariableDefaultWhat it does
ASPHODEL_LICENSEYour licence JWT.
ASPHODEL_LICENSE_FILERead the licence from a file instead.
ASPHODEL_GRACE_HOURS72Grace window after expiry.
ASPHODEL_DATA_DIR/dataTenant memory stores. Back this up.
ASPHODEL_MINDSETS_DIR/mindsetsRead-only corpus root.
ASPHODEL_TENANTdefaultWhich tenant this instance serves.
ASPHODEL_HOST0.0.0.0 in the imageBind address.
ASPHODEL_PORT8351Bind port.
ASPHODEL_COUNCIL_MINDSETSciso,cissp,ccspCandidate bench for the council tool.
ASPHODEL_SEMANTICoffOpt-in hybrid retrieval. See below.
ASPHODEL_EMBEDDERollama to enable embeddings.
ASPHODEL_EMBED_URLhttp://localhost:11434Ollama base URL.
ASPHODEL_EMBED_MODELnomic-embed-text768-dimension embedding model.
ASPHODEL_LICENSE_PUBKEY appears in older notes as a trust-root override. It is inert in released images — the baked root always wins, deliberately, because a customer-settable trust root is a complete licence bypass. Do not plan around it.

The opt-in semantic tier

Lexical routing is exact on security vocabulary and weak on paraphrase. Setting ASPHODEL_SEMANTIC=1 with an embedder configured upgrades mindset retrieval only to a hybrid rerank. It activates only when the corpus actually carries embeddings, and falls back to byte-identical lexical results the moment anything is missing — no errors, ever.

Understand the trade. The lexical default has zero runtime dependencies and is reproducible forever. Opting in buys paraphrase recall at the cost of a live embedding dependency and non-determinism across model versions: identical queries can rank differently after an upgrade. The entitlement gate never touches this path, so licensing stays deterministic under either setting.

MCP tools

Thirteen tools. Memory and status are ungated; every mindset tool is gated on a feature in your licence.

ToolArgumentsGate
memory_writetext, persona, subdomain, kind, ref, confUngated
memory_recallquery, k, personaUngated
memory_searchquery, k, persona, subdomainUngated
mindset_recallmindset, query, k, subdomain<name>_mcp
ciso_recall and siblingsquery, k, subdomainciso_mcp, etc.
security_councilquery, k (per mindset)council_mcp + each seat
asphodel_statusUngated

Per-mindset tools exist for ciso, cissp, ccsp, nist, compliance, threat_intel, frameworks and shi. mindset_recall reaches any installed corpus by name and is the forward-compatible one to write against.

A passage carries rank, score, subdomain, kind, ref, source, conf and text — enough to cite what you were grounded on.

Refusals are structured, never exceptions. A tool you are not entitled to returns {"error":"not_entitled","feature":"...","detail":...} naming exactly the feature required, so an agent can degrade gracefully instead of crashing.

The council

security_council fans one query across every security mindset you are otherwise entitled to and ranks the lenses loudest-first. It composes with per-mindset entitlements and never bypasses them: council_mcp with no mindset features seats nobody and returns an empty council with a notice — a successful call, not an error.

Mindsets

Corpora are licensed separately and delivered as directories. Drop them under your mindsets root so that the database lands at either path below, and mount the root read-only:

/mindsets/<name>/millennium.db
/mindsets/tkhr-<name>/millennium.db

Discovery is at startup. Confirm what the server found:

docker exec asphodel asphodel mindsets
docker exec asphodel asphodel stats

Holding a corpus on disk is not entitlement. The gate is the licence, and a mindset you have not licensed refuses even when its database is mounted — so mounting a shared corpus directory across a fleet is safe.

If you have enabled the semantic tier, seed embeddings once per corpus. Embeddings sit outside the hash chain, so asphodel verify still passes afterwards:

docker exec asphodel asphodel mindset embed ciso

What the licence controls

This is the part most licensing documentation gets wrong, so it is worth stating plainly: in Asphodel, capability is granted by the features in your licence, not by the tier name on it.

Feature claimUnlocks
<name>_mcpThe matching mindset — ciso_mcp unlocks ciso_recall and mindset_recall("ciso", ...).
council_mcpThe security_council aggregator, over the mindsets you already hold.
sovereign_mcpUpgrading a tenant to a constitution-bound sovereign. See below.
— none required —All three memory tools, agent identity, chain verification.
There are no per-tier quotas. No seat counts, no request rates, no page-size caps, no memory ceilings — none exist in the software. Two installs on different tiers holding the same features behave identically. Your tier is a commercial label and a ceiling on what may be delegated onward; it is not a capacity grant. If a proposal tells you a tier buys you "more", ask which feature claim it adds.

Identity and sovereignty

Every tenant owns a SoulKey — a hybrid Ed25519 + ML-DSA-44 keypair, both or neither. It is minted lazily on the first write, and every memory entry is signed with it. That is the difference between a chain that is merely tamper-evident and one that is attributable: you can show not only that the record was not altered, but which identity asserted it.

docker exec asphodel asphodel tenant provision acme
docker exec asphodel asphodel tenant identity acme
docker exec asphodel asphodel tenant verify acme

tenant verify is the strong check: hash chains, plus every per-entry signature, plus the constitution seal where one exists.

Sovereign tenants

A tenant can be upgraded to a constitution-bound SHI, which requires sovereign_mcp. A twelve-principle constitution is sealed by a retirement key that is destroyed at provisioning, and every boot re-verifies the seal. Tampering is not repaired and not logged-and-continued — the tenant refuses to operate, and both reads and writes return constitutional_halt.

docker exec asphodel asphodel tenant provision acme --sovereign
Because the retirement key is destroyed at provisioning, a sealed constitution cannot be edited afterwards — by us or by you. That is the property being sold. Decide the constitution before you provision.

Operations

Integrity

Run asphodel verify on a schedule and alert on a non-zero exit. It walks every chain and prints OK or TAMPERED per persona and per mindset. This is cheap and it is the check that makes the storage claim meaningful — an unverified hash chain is just a table with an extra column.

docker exec asphodel asphodel verify

Backups

All state is under ASPHODEL_DATA_DIR. Back up that volume and you have backed up the product. Mindset corpora are read-only and re-obtainable, so they need no backup. Snapshot with the container stopped, or use SQLite's backup API — copying a live database file mid-write is how you acquire a chain that fails verification.

Upgrades

Pull the new tag and recreate the container. State lives on the volume, so there is nothing to migrate. Verify after upgrading.

When a tool refuses

Work in this order. asphodel license scopes tells you what the install believes it holds; asphodel mindsets tells you what it can see on disk. A not_entitled refusal with the corpus present is a licence question; an unknown_mindset error is a mount question. They are different problems and the error distinguishes them.

Support

Technical questions and licence problems: support@saluca.com. Include your account reference and the output of asphodel license scopes. Billing, tier changes and invoicing: sales@saluca.com.