Architecture and protocol v1
Storage
The database schema consists of header, state and checkpoints. Apart from
format metadata, salt and row sequence numbers, values are authenticated
ciphertext. Journals contain ciphertext as well. There is no persistent plaintext
FTS index. Authorized ordinary memory is split into overlapping Unicode-safe
chunks and tokenized with Jieba before entering an in-memory SQLite FTS5 index.
The cache is keyed by vault policy/revision/quarantine state and reused between
queries; edits, synchronization and policy changes invalidate it, and lock clears
it. SQLite temporary storage is deliberately memory-only. Vectors are normalized
and cached only in memory; neither plaintext indexes nor embeddings enter backup
or synchronization payloads.
The password-derived key wraps a random master key; the master key encrypts state and local checkpoints. Each permission epoch has a vault key, and each revision has a random data key wrapped under its epoch key. Content is serialized and split into individually authenticated 256 KiB chunks whose position and total count are bound into the associated data. The revision signature covers its asset ID, parents, policy head, tombstone flag, ciphertext and device certificate.
Mutations are prepared on a cloned state and committed with the encrypted state
and checkpoint in one SQLite transaction using synchronous=FULL. Read and
execution intents are committed before delivery. Disk failure therefore denies
delivery; a crash after intent commit may leave an unknown outcome.
An exclusive OS lock permits one cooperating local writer. Initialization builds a complete database in the same directory, closes and syncs it, then publishes without overwriting an existing destination. Interrupted staging does not block retry. Each device needs its own local store; copying live SQLite files through a cloud sync client does not provide multi-device transaction safety.
Identities and transport
User Ed25519 keys certify device Ed25519 signing keys and device X25519 exchange keys. Certificates alone are insufficient: each vault policy deliberately authorizes devices. Device keys are independent, including devices belonging to the same user. The initial user signing authority is delivered to approved personal devices inside an authenticated recipient-encrypted packet; that authority cannot bypass a vault's device allowlist after a device is revoked.
An envelope contains a format version, sender device key, recipient exchange key, ephemeral X25519 public key, AEAD payload and Ed25519 signature. HKDF derives the transport key. Recipient identity and ephemeral key are authenticated. The joining device uses an expiring signed request and deliberately pins the approving device's public key; requests are consumed only after successful commit.
The first release uses request/approval strings and files rather than camera QR scanning. Fingerprints are full SHA-256 digests; shortened values shown in lists must never be used as the sole approval check.
Policies and merge
Vault policies are signed linear histories containing membership, role, device allowlist, monotonically growing device revocation set and epoch. Role checks use the preceding policy. Ordinary users may authorize another device for their own identity without changing membership. An Admin may change Admin, Editor and Reader membership and authorize a member's device. An Admin cannot add, remove or alter an Owner, revoke devices, or transfer ownership. Owners can. Ownership transfer keeps exactly one Owner.
Asset histories form a content-addressed DAG shared by every editable kind
(memory, card—including knowledge, brain, persona, and protected-card metadata—file, and
credential). Each save appends a signed revision; there is no separate
wiki-only history store and no automatic pruning. history lists revisions with
author and summary; diff returns a unified text projection (title, structured
metadata, body) between two revisions or versus live HEAD. File diffs omit binary
payloads; credential secret bodies remain unavailable through read/diff.
Multiple unconsumed revisions of an asset are conflicts; a resolution revision
names all visible parents. A tombstone removes the asset from current views
without deleting history.
Sync validates complete policy, revision and audit histories before merging into a candidate state. Unknown writes from superseded epochs are quarantined. Local offline writes absent from an incoming newer policy snapshot are also quarantined. Quarantine can only be reviewed/reapplied locally, not cleared by an untrusted incoming field.
Permission forks require an owner signature over the chosen head and the entire discarded policy chain. The signer must be owner on both branches. Discarded policies remain verifiable for historical/quarantined revisions. A successful resolution advances the selected policy and rotates its key. Competing or incompatible resolutions fail closed.
Local API
service::Request is the shared typed operation interface, serialized with the
op discriminator in snake case. Unknown top-level fields are rejected. Local
IPC uses big-endian 32-bit length-prefixed JSON frames (version-1 Wire
wrapper). Optional HTTP/WebSocket listen and relay RPC use the same WebSocket
binary JSON framing (AuthFrame then Wire) after token authentication. MCP
capabilities are checked separately before dispatch. RunBound is dispatched
like RemoteExec: run_bound_prepare authorizes and decrypts under the service
lock, the IPC thread runs the program unlocked, and run_bound_finish audits the
result. Run bindings live in device-local State.run_bindings, not in a vault.
Network transports may carry token-authenticated framed Requests. Tokens come
from flags/environment only (never TOML), are compared in constant time, and are
wiped when the store locks. Remote callers use stable elevation namespaces
(ws:remote / relay:remote) that do not inherit local cli:{pid} elevation;
unlock and sudo over remote attach elevate that namespace so CLI one-shot dials
keep elevation. Desktop HTTP/relay hosts dispatch the full handle_client
surface (same elevation gates as local IPC). Mobile FFI and Share retain the
platform denylist and do not invent desktop-only features on phone. HTTP listen
defaults to loopback; non-loopback binds require --allow-non-loopback and a
warning. Plain HTTP/WS needs no certificates; --tls-cert / --tls-key enable
HTTPS/WSS. Relay RPC uses WSS off-trust with a high-entropy room id plus token;
vault sync keeps /v1/channel. The WebUI has a read-only status projection and a
restricted ordinary-card editor, and is not a
Request transport. See remote daemon and SECURITY.md.
Optional authorization Centers ([[centers]] in config) forbid local self-bootstrap and local MCP grant minting; enrollment and grants are distributed as Center-signed tickets with deliberate scopes (allow_all / read / write / read_write / read_write_delete). See center.
Optional Center hosted mode (--data DIR) owns a separate daemon-format encrypted
Store under DIR/store/, opened with an operator-held server password. Its HTTP
content RPC uses a closed, per-vault grant dispatcher in Service; operation-local
elevation covers protected/private content and attachments without changing daemon
MCP authorization. It is server-side encryption, not client end-to-end encryption.
Separate admin HTTP routes manage hosted lock/unlock and durable named grants in
owner-only DIR/members.json. Signed unlock tickets only authorize local device
password unlock; device keys never reach a ticket-only Center. A single
[[centers]] entry with mode = "saas" makes daemon content RPC use that hosted
backend and its environment bearer, without a parallel local content path or
MCP capability elevation. Device/admin controls are not part of the grant content
surface. See
Center hosted vault for boundaries and migration.
The mobile ABI is declared in crates/ffi/memories.h. Handles are numeric registry
IDs, not raw Rust pointers. Each returned string must be freed once with
memories_free. Invalid JSON produces a generic error without echoing input.
memories_lock_all advances a global lock generation on backgrounding and suppresses
late in-flight responses. The registry stores reference-counted per-handle service
locks; network work does not hold the registry or writer lock. A busy handle
applies a pending lock before its next request. Mobile excludes process execution and MCP grant creation.
The Flutter mode selector (mobile/lib/app_mode.dart) routes Standalone to the
local library and Center/SaaS to HostedCenterPage. Here Center means a
self-hosted server with hosted storage; SaaS selects the fixed
https://memories.ivjn.us endpoint. Both use the same Rust hosted_* FFI
operations and HostedClient; the selection does not modify daemon TOML,
enroll a ticket-managed device, or move data between stores.
The selected mode is a non-secret preference. Center and SaaS each store one URL/token profile in platform secure storage. Switching locks the local service, stops sharing and disposes the hosted page/session. Backgrounding clears hosted content and invalidates late responses; reconnection on return is explicit. See mobile configuration for profile keys and upgrade behavior, and the mobile guide for user-facing steps.
Desktop tray and status projection
Request::DesktopStatus returns a typed projection of initialization/lock state,
library and connection counts, configured provider kinds, and synchronization
queue counts. It runs before unlocked operation dispatch and does not clone or
commit the encrypted state, touch idle/elevation timers, or expose identifiers,
account summaries, contents or credentials. Locked responses have no details.
The existing MCP allowlist denies this request.
The CLI library hosts the local dashboard and Windows/macOS tray. Both
memories tray and the standalone memories-tray launcher use a native main
thread event loop. The HTTP server and bounded status-probe worker run separately
so a busy daemon cannot block the tray or browser. The probe invokes the sibling
CLI using the normal OS-authenticated IPC, with a deadline and child cleanup.
The HTTP surface serves embedded assets, the fixed status projection and an
deliberate ordinary-card/type editing allowlist. It cannot unlock, elevate, reveal
secrets or dispatch arbitrary service requests. See SECURITY.md for its token
and browser-origin boundary and desktop usage.
Retrieval and model orchestration
search::Cache owns at most two vault indexes, each bounded to 10,000 chunks,
16 MiB of source text/metadata and 32 MiB of vectors. It excludes private,
credential, file, deleted, quarantined and conflicting content before indexing.
BM25 weights title/body/metadata as 5/1/2. Vector search uses cosine similarity;
hybrid search combines both rankings with reciprocal-rank fusion. Default
reranking boosts query coverage and exact phrase matches within a bounded
candidate set. A configured model endpoint can perform learned reranking.
Retrieve, RagIndex, RagStatus and RagAsk retain vault authorization and
MCP grant scoping. IPC runs model work outside the service mutex and rechecks
unlock generation, grant validity, access and source revision stamp before/after
external work and before delivery. Signed intents precede inference/export;
success/failure and delivered source IDs are audited without prompts or keys.
Source asset/revision references use the shared short-ID projection; chunk hashes
and [S1] citation labels are not asset IDs.
rag::Runtime lazily loads native ONNX inference and the pinned EmbeddingGemma
Q4 assets, or calls a configured Gemini/compatible embedding URL. It also supports
configured generation and rerank URLs, environment-referenced API keys, proxies
and retries. Native model files are public model data; encrypted user records
never become model weights. Retrieved text is untrusted data in generation
prompts, and returned citation IDs are validated against the selected sources.
This validates references, not the factual correctness of model-generated prose.
Mobile FFI receives the improved lexical search and supports BM25 retrieve.
Model-backed operations currently use the configured desktop daemon; no model
configuration or model inference UI is added to Flutter in this release.
Desktop cryptographic jobs
jobs::Job describes tools, pinned input files/trees, credential bindings, ordered
steps, cleanup and a new output path. The CLI's crypto recipes build this plan
for S/MIME/OpenPGP and Android/iOS signing. Approval is checked before short IDs
are resolved, so its digest binds exactly the references shown to the caller.
The service checks local elevation and vault access, resolves only credential
assets, commits an execution intent, then invokes the shared core runner.
The runner rechecks tools, copies and verifies inputs, materializes credentials
in a private workspace, runs commands without a shell, suppresses their output,
and publishes only a successful artifact. Placeholder expansion substitutes
paths only and does not recursively interpret path contents. MCP's allowlist and
the mobile FFI deny process jobs. Existing run semantics are unchanged.
AI client skills
memories inject is a CLI-only installation operation. skills.rs embeds the
versioned crates/cli/skills/memories/SKILL.md in the executable and installs it
in documented Codex, OpenCode or Claude Code discovery directories. It runs
before data-home creation and never opens the store or calls the daemon. User
and project scopes share the same instruction text; no store paths, vault IDs,
grant tokens or secret material are interpolated into it.
The installer preflights all targets, preserves identical files, requires
--force for differing content, rejects linked skill files/directories, and
publishes each file using a synced temporary file in its destination directory.
New files are published without clobbering a concurrently created file. This is
per-file atomicity, not a transaction across clients. Existing sibling resources
and client configuration are preserved. The skill routes ordinary search, cited
retrieval, configured answers and writes through existing MCP or CLI interfaces;
it does not add authorization capabilities.
Cloud authorization
The desktop CLI performs browser OAuth with PKCE, a loopback callback, and fixed
provider endpoints. CloudBegin creates an expiring caller/provider/client-bound
ticket under sudo. Network requests run outside the daemon; CloudComplete
checks the ticket and current authorization before committing. Lock, logout and
restart invalidate pending tickets, preventing late results from restoring login.
State.cloud_accounts stores tokens encrypted on the local device. Status returns
account metadata only; backups, pairing, vault sync and MCP exclude the tokens.
Login does not establish vault membership.
Cloud object synchronization
State.cloud_sync holds durable outbox/inbox receipts inside the same encrypted
state blob. CloudSyncEnqueue writes the exact serialized envelope bytes and a
signed descriptor before any transport I/O. Object IDs are SHA-256 of those
bytes; retries reuse them. CloudSyncWork returns ciphertext and listing
cursors without OAuth tokens. sync::execute performs bounded put/get/list
without the service mutex. CloudSyncAck checks a process-local session
generation, then commits upload flags, discovered IDs, downloaded bytes, and
vault merges together. Descriptors that arrive before their objects stay
pending. Lock, unlock, and logout increment the session so late acknowledgements
cannot import. memories cloud sync asks the daemon to refresh tokens, upload,
and download through OneDrive or Google Drive application folders; that HTTP
runs outside the writer lock. Tokens are never returned over IPC. Tests also
use MemoryTransport and loopback Graph/Drive fixtures. Completed queue payloads
are compacted into hash receipts and per-provider/recipient publication stamps.
Receipts remain local and do not delete cloud history. Missing objects retry with
a persisted exponential delay; pending capacity applies backpressure to listing.
Provider workers serialize per account and refuse refresh/import results from
an invalidated session. IPC dispatch uses at most 16 concurrent connections.
See the sync design and the authorization guide.
Remote sessions
The desktop daemon owns SSH, FTP, TCP, MySQL, Redis, and PostgreSQL connections.
RemoteOpen validates the credential host allowlist, records a connection intent,
then connects outside the writer lock. SSH authenticates with a password
credential or an imported OpenSSH private key, optionally plus an OpenSSH
certificate credential. CredentialGenerate creates SSH keys and TLS
certificates under sudo; the private key is stored as a credential and is never
returned. CredentialPublic derives SSH public keys or PEM certificates from
that stored material. Database sessions enable TLS by default and may attach
an optional PEM CA credential (tls_ca) to the client trust store. Hostname
verification is required; certificate pinning is not implemented. Each
instruction is proposed as a ticket whose fingerprint covers the session,
sequence and canonical instruction. RemoteApprove is single-use and bound to
the elevated caller; RemoteExec runs only that ticket on the existing session.
Lock, idle expiry and logout drop live connections and pending tickets. File
operations use FTP; SSH and database sessions accept exec/query.
ProxyStart binds a loopback listener after the same credential and host
allowlist checks as RemoteOpen. Local clients connect without secrets; the
daemon holds the upstream SSH or database authentication. proxy.start records
the connection intent before binding; proxy.stop records the stop intent.
The bound socket transfers directly into Tokio without a close/rebind window.
PostgreSQL SSLRequest and MySQL SSLRequest precede the TLS upgrade; Redis uses
direct TLS. PostgreSQL SCRAM checks both the nonce and server signature. MySQL
supports native and caching-SHA-256 authentication, including plugin switches;
full password exchange requires TLS. Database handshakes have a 30-second
deadline, 64 KiB packet bounds and a limit of 16 clients per listener.
The local database hop is plaintext and login/database startup fields do not
override the configured upstream identity. Local clients authorize unrestricted
remote instructions within that account's permissions. Loopback provides no
per-user authentication. Lock, vault idle expiry, deliberate stop and service drop
cancel listeners and active sessions. Proxies have a 30-minute maximum lifetime;
traffic does not extend vault unlock. MCP cannot manage proxies.
The service owns each proxy worker and joins it on stop/lock, so a successful
response cannot race with a connection that is still forwarding instructions.
Configuration file execution
memories file exec composes the existing authorized Read request with
CLI-local file materialization and process supervision. It introduces no new
service request, file kind, cryptography or mobile/MCP execution permission.
The daemon audits the file delivery using the existing read path, then the CLI
runs the foreground child without occupying the service lock. The CLI owns the
temporary plaintext snapshot until the direct child exits; subsequent vault
changes or lock do not change/revoke that already delivered snapshot.
Recovery
Backups contain the signed local state and secret recovery material under an independent Argon2id/AEAD envelope. Recovery generates new device keys and creates independent vault IDs, preserving live content and conflict heads. Original signed histories are retained in an encrypted read-only archive. Recovery does not claim that a historical shared membership is still current. Device-only vaults, cloud authorizations, and cloud outbox/inbox receipts are excluded; each recovered device signs in to cloud providers separately.
Cards and safe projections
Kind::Card shares ordinary-memory authorization, with typed metadata stored
inside encrypted revisions. CardLink adds directed same-vault edges. Graphs
project only current, unambiguous visible nodes and edges with both endpoints
visible. Search uses the bounded in-memory FTS5 cache described above, covering
ordinary bodies, tags/sources and allowlisted protected-card metadata. Credential
bodies never enter this index. Search/list/graph expose a safe summary
projection; card reads retain the opaque credential reference but never resolve
its value.
protected_card validates an account/env/token/TOTP/HOTP input (including
otpauth URI import), writes a credential and its card into one candidate state,
and commits both together. Generic put cannot attach or replace protected
references. Secret operations resolve current credential revisions and verify
their kind. otp_code implements RFC 6238 (TOTP) and RFC 4226 (HOTP), advances
the HOTP counter in the same commit, and records delivery intent before returning
a code. HTTP header construction marks bearer/cookie values sensitive, with
redirects disabled and responses suppressed.
Memory and card content may also carry a secrets map from a stable label to a
canonical credential asset ID. protect_secret replaces exact body matches with
{{secret:LABEL}} and creates the credential in the same transaction. Generic
put requires existing mappings to remain unchanged and cannot invent or retarget
them. CLI and mobile edits carry the existing map forward. MCP card_write omits
the map, so updates to cards with secret references fail validation. Client
projection shortens only the typed credential IDs, leaving body markers stable.
Backup recovery preserves unambiguous asset IDs, so the mappings survive recovery.
credential_copy resolves a label, a protected card, or a credential directly,
requires local elevation, and commits audit intent before returning UTF-8
plaintext to the clipboard client. MCP rejects both new operations. No generic
read, search or graph operation resolves a secret. Signed historical bodies are
unchanged by replacement; see the wiki and clipboard limits in SECURITY.md.
The service returns {"secret":"..."} only to the authorized local copy caller.
The CLI consumes this response in clipboard.rs and returns {"copied":true}
inside its normal output envelope only after the clipboard write succeeds.
Mobile uses Flutter's clipboard channel and checks its lifecycle generation
before delivery. The audit records credential.copy with delivery_intent,
not an OS clipboard success receipt. See the usage guide.
State.local_vaults marks device-only vaults inside the encrypted local state.
local_config records client-collected version and directory metadata in one such
vault. Device approval filters those vaults out; direct membership export and
sync are rejected; store::backup removes them from its exported snapshot.
Unambiguous asset IDs survive recovery in a new vault, preserving graph and
card-to-credential references while leaving original signed histories archived.