Memories Technical Documentation
This document describes the current implementation of storage, trust boundaries, local authorization, hosted Center, sealed secrets, retrieval data flow, cloud object synchronization, and desktop status boundaries. It describes the same behavior as SECURITY.md, docs/architecture.md, docs/authorization.md, and docs/center.md. The storage format version is 1. This is development software; passing tests does not constitute an independent security audit.
Storage and Trust Boundaries
The local database tables are header, state, and checkpoints. Apart from format metadata, salts, and row sequence numbers, values are ciphertext with associated data. The log also contains ciphertext. There is no persistent plaintext full-text index.
The format header's version must be 1; otherwise opening fails. The user password derives a 32-byte key through Argon2id (version 0x13, 64 MiB memory, 3 iterations, parallelism 1) and a 16-byte salt. This key wraps a random master key with associated data memories/master/v1. The master key encrypts state and local checkpoints. Symmetric encryption uses XChaCha20-Poly1305.
Each permission epoch has its own vault key. Each revision has a random data key wrapped by that epoch key. Serialized content is split into individually authenticated 256 KiB chunks, with chunk position and total chunk count included in associated data. Revision signatures cover the asset ID, parent versions, policy head, tombstone flag, ciphertext, and device certificate. The signature algorithm is Ed25519.
Changes are first prepared on a cloned state, then the encrypted state and checkpoint are committed in the same SQLite transaction with synchronous=FULL. Read and execution intents are committed before delivery. Disk failures therefore prevent delivery; a crash after an intent commits may leave an unknown outcome. An operating-system exclusive lock limits each data directory to one cooperating writer. Initialization first finishes writing the database in the same directory, closes and synchronizes it, then publishes it at the destination without overwriting an existing target. Each device uses its own store. Handing an active SQLite file to a cloud-drive synchronization client does not provide multi-device transaction safety.
The user's Ed25519 key certifies the device's Ed25519 signing key and X25519 exchange key. The certificate alone is insufficient: each vault policy must also explicitly authorize the device. Different devices belonging to the same user have independent keys. The initial user signing capability is delivered only to approved personal devices in an authenticated approval package encrypted for the recipient. After a device is revoked, the user identity key cannot bypass that vault's device allowlist.
A transport envelope contains the format version, sending device key, recipient exchange key, ephemeral X25519 public key, AEAD payload, and Ed25519 signature. The transport key is derived with HKDF. The recipient identity and ephemeral key are authenticated. Device enrollment uses an expiring signed request and explicitly pins the approving device's public key. A request is consumed only after a successful commit. Approval uses the full SHA-256 fingerprint. Shortened values displayed in lists cannot serve as the sole basis for approval.
Policies form a signed linear history containing members, roles, device allowlists, a monotonically growing revocation set, and epochs. Role checks use the preceding policy. Ordinary users can authorize another device for their own identity without changing membership. Only Admin or Owner can invite members, approve enrollment, and authorize devices for members. Admin can change Admin, Editor, and Reader memberships, but cannot make anyone Owner, modify an existing Owner, revoke devices, or transfer ownership. Owner can. Ownership transfer maintains exactly one Owner. Policy validation rejects Admin changes to Owner members or the revocation set.
Asset history is a content-addressed directed acyclic graph. An asset is in conflict when it has multiple unconsumed revisions. A resolving revision must name all visible parent versions. A tombstone removes an asset from the current view without deleting its history.
Backups store signed local state and recovery material under a separate Argon2id and AEAD envelope. Recovery generates new device keys and new vault IDs, preserves current content and conflict heads, and places the original signed history in an encrypted read-only archive. Recovery does not claim that old shared memberships remain valid. Device-only stores, cloud authorization, and cloud send/receive queues are excluded from backups.
The design protects against the following: a relay reading or forging asset content; peer identity substitution after explicit fingerprint verification; modification of authenticated ciphertext; reading a copied database without the local password; readers forging writes; administrators changing ownership; revoked devices using the user identity key to authorize new devices for themselves; CLI or MCP accidentally printing credentials, accepting passwords on the command line, or allowing MCP capabilities to inherit terminal elevation; undetected modification of signed revisions or audits, chain gaps, and checkpoint truncation relative to the remaining database state.
The following are outside the boundary, and the documentation does not claim they are solved:
- Compromised endpoints, operating-system administrators, authorized malicious programs, memory inspection, keylogging, and users deliberately copying plaintext. Rust-owned key buffers are zeroized. There is no erasure guarantee for operating-system swap, crash dumps, dependency allocations, or Dart garbage-collected strings. The desktop application has no hardware key storage.
- A relay can observe traffic metadata, block connections, independently record ciphertext, delay revocation, or partition devices. The relay server shipped with the repository does not write assets, but a malicious server cannot be forced not to record traffic. The relay does not queue data for offline devices.
- Offline revocation cannot erase past plaintext, keys, or backups. Endpoints that have not received a revocation can still use old local copies.
- Audits prove signed content and continuity relative to known state, rather than proving that a compromised device recorded every access. If an older but internally consistent backup replaces the entire database and local evidence, this cannot be detected without a newer peer checkpoint or external anchor. There is no trusted timestamp service.
- Other processes under the same operating-system user are inside the local account boundary. IPC authenticates the operating-system user. Terminal elevation additionally uses the parent process PID. This provides neither isolation from malicious processes under the same user nor a persistent session identity immune to PID reuse.
- Executable fingerprints are checked before launch. There is no operating-system sandbox, mandatory code signing, atomic execution through a file handle, or containment of descendant processes.
- Clipboard copies, exported files, and credentials the user explicitly asks to copy leave the encrypted store through user actions. Copied content remains after locking, and operating-system history or other applications may retain it.
Local IPC and Authorization
service::Request is the shared typed operation interface, discriminated by snake-case op. Unknown top-level fields are rejected. Local IPC uses JSON frames with a big-endian 32-bit length prefix and a version 1 wrapper; the optional HTTP/WebSocket listener and relay RPC share WebSocket binary JSON frames (token authentication). After unlocking/elevation, desktop remote access exposes the same operations as local IPC (ws:remote / relay:remote); mobile FFI/Share retains a platform denylist. See docs/remote-daemon.md and SECURITY.md. An optional authorization center ([[centers]]) prohibits local bootstrapping. Ticket mode issues enrollment, MCP grant, and unlock tickets; hosted mode places content in Center's server-encrypted store. See below, as well as docs/center.md and docs/authorization.md.
The daemon serves only the current operating-system user. Windows uses named pipes; Linux and macOS use Unix sockets. IPC accepts at most 16 concurrent clients. Windows named-pipe frame reads remain blocking: stalled clients under the same user occupy these slots, but one stalled client or cloud transfer does not block every other connection. Unix frame I/O has deadlines. Service operations holding the write lock, including trusted subprocess execution, remain serialized.
Normal unlock expires after 15 minutes of inactivity. sudo uses the same local password, lasts at most 15 minutes, expires after 5 minutes of inactivity, and is scoped to the terminal parent process issuing the command. Locking or restarting clears elevation. Attempts while fully locked are rate-limited but are not written to the persistent signed audit. Failed operations are audited when unlocked and a local signing identity is available. Intents commit before delivery; a sudden crash leaves a committed intent rather than an invented outcome.
An MCP grant is a local bearer capability written to a caller-specified file, never printed in the terminal. mcp grant uses --minutes to determine validity; --write is required to create ordinary memories. mcp serve --grant-file reads the capability from that file and exposes tools over stdio using MCP 2024-11-05: memory_search, memory_retrieve, memory_ask, memory_read, memory_read_revision, memory_history, memory_diff, memory_write, memory_delete, card_write, card_graph, and card_link. Every call carries the vault from the grant, and capabilities are checked individually before dispatch.
MCP cannot inherit terminal elevation. It cannot read private memories, files, or credentials; execute processes, remote sessions, loopback proxies, or cryptographic jobs; manage vault membership; or request DesktopStatus. Locking clears grants from memory; unexpired grants return after unlocking, and locking itself does not delete them. Authorization service issuance, listing, revocation, requests, and claims can be invoked through MCP tools, but still use the MCP capability itself, inherit no terminal elevation, and do not use a Center administrator bearer. Ordinary reads do not resolve {{secret:标签}}. card_write carries no secret mappings, so updating a card containing secret references fails validation. Short IDs in references shorten only typed credential IDs; markers in the body remain unchanged.
otp_code implements RFC 6238 TOTP and RFC 4226 HOTP. HOTP saves the incremented counter in the same commit that returns the code. parse_otp_import accepts otpauth:// and Google Authenticator's otpauth-migration://offline?data=..., parsing accounts into TOTP or HOTP cards. When a type appears in the URI host, type parameter, or migration data, that value determines the type; if no type is specified, a counter indicates HOTP, otherwise TOTP. Callers do not need to select the type separately. The command-line --image option first decodes these two URI types from an image using rqrr, then uses the same import path; a URI and --image cannot be supplied together. For multiple cards, --title serves only as a title prefix, and --id can replace only one card. The mobile camera does not import these two OTP QR-code types. Hosted device pairing uses a separate scanning path, described below. Card revisions may also carry optional aliases, notes, and categories; omitting these fields during an update preserves their values, while an empty string clears them. Seeds are excluded from search and MCP. TOTP depends on a correct local clock. Generated results are authorized short-lived codes, not seed exports; audits record neither codes nor seeds.
credential_copy is an explicit exception to credential output restrictions. It requires local elevation, commits an audit intent first, then delivers UTF-8 plaintext to the clipboard client. The CLI prints {"copied":true} only after the system clipboard write succeeds. MCP cannot request copying. The audit records credential.copy and the delivery intent, rather than a success acknowledgment from the operating-system clipboard.
The mobile ABI is declared in crates/ffi/memories.h. Handles are numeric registration IDs, not raw Rust pointers. Returned strings are freed once with memories_free. Invalid JSON produces a generic error without echoing input. memories_lock_all advances the global lock generation when the application enters the background, suppresses late in-flight responses, and drops all hosted sessions. The mobile path does not include process execution, local MCP grant creation, local authorization management, or permission-request RPC. The hosted_* calls connecting to hosted Center follow a separate path: content access exposes only listing, reading, and writing kind = credential.
Cryptographic jobs check the approval digest before resolving short IDs, so the digest binds the references the caller saw. The service checks local elevation and vault access, resolves only credential assets, commits an execution intent, then invokes the shared runner. The runner rechecks tools, copies and verifies inputs, materializes credentials in an owner-only workspace, runs commands without a shell, suppresses tool output, and publishes only successful artifacts. Placeholders substitute paths only. The MCP allowlist and mobile FFI reject process jobs.
Grants, Permission Requests, Hosting, and Sealed Secrets
Local grants and hosted Center use the same role ceilings. Reader is limited to read, Editor to read_write, Admin to read_write_delete, and Owner to allow_all. An omitted scope means read. Reader and Editor cannot issue or delegate grants. Issuance is constrained by both the role ceiling and the scope of the caller's grant, cannot cross vaults, and cannot outlive the parent grant. Duration is 1–60 minutes. A grant issued directly by a member has depth 1; issuance through a grant adds 1, and depth above 2 is rejected. Persistent member credentials on Center have depth 0 and do not count as delegation. The recipient must already be a member of the vault, cannot have a role above the issuer's, and cannot use the reserved name center-admin. Content grants do not change the recipient's role.
At most 10000 grants may be active simultaneously (service::MAX_ACTIVE_GRANTS); the local daemon and hosted Center share this limit. Tokens appear only in the single issuance or claim response. Persistent state stores only SHA-256 hashes and metadata, with constant-time comparison. Locking drops in-memory keys and suspends grants without revoking them; unexpired grants remain valid after unlocking or restarting. Expired grants and their descendants are removed during loading and periodic checks. Revocation propagates to descendants. When a member is removed or an issuer's role falls below an issued scope, related grants become invalid when the policy commits. Grants are excluded from vault synchronization and recovery backups.
Permission requests are approved first, then claimed. Approving a scope request records only its boundaries and does not mint a token; the requester then claims it once using the member credential used to create the request. If that response is lost, the token cannot be retrieved again. Role requests exist only locally: approval changes policy immediately, with no claim step; the Owner role uses signed ownership transfer, and time-limited grants cannot approve permanent role changes. Hosted Center accepts only scope requests. Requesters cannot approve or reject their own requests. Grant callers whose subject differs from the issuer can submit requests, but cannot approve, reject, cancel, or claim them. Pending requests are limited to 20 per requester per vault and 200 per vault. They expire after 7 days. Decided, expired, or claimed requests are deleted 30 days after their last transition.
Local audits are hash chains signed per device. Center's DIR/audit.jsonl is an unkeyed chain: it can detect tampering relative to a known head, but cannot prevent an operator from rewriting the entire log.
Center has two modes. mode = ticket (the default) issues only enrollment, MCP grant, and unlock tickets; content remains in each device's encrypted store. Unlock tickets can be reused until expiry, are not bound to a specific device, and permit only local unlocking with the device password; they grant neither sudo nor MCP elevation. With mode = saas and Center running with --data, content resides in the server-encrypted store. The operator holds the store password and can read unlocked plaintext. This is server-side encryption, not a confidentiality boundary against the operator. When configuration contains exactly one mode = "saas", the daemon routes content RPC to that Center's /v1/rpc. Unsupported operations fail directly without falling back to the local store. The administrator bearer manages locking, unlocking, and members; using it on content routes returns 401. allow_all covers only hosted content operations, excluding server administration, device management, sudo, enrollment, recovery, execution, and cryptographic jobs.
Cards marked e2e: true in a hosted store reject plaintext secrets. After pairing, secrets reside in envelopes addressed to the device's X25519 public key and signed by a known Ed25519 key. Center stores only public keys and ciphertext. It can see who, which card, and when, and can withhold or replay old envelopes, but cannot see their bodies. Device grants cannot issue grants or approve requests. Revocation deletes that device's envelopes but cannot recover plaintext already decrypted. Only an Owner grant can remove the e2e marker, and that write cannot carry secrets. Adding a device to a local vault still uses expiring signed requests and full fingerprint verification, without a camera.
Retrieval and Model Data Flow
The search cache retains indexes for at most two vaults. Each vault is limited to 10,000 chunks, 16 MiB of source text and metadata, and 32 MiB of vectors. Private content, credentials, files, deleted, quarantined, and conflicting revisions are excluded before indexing. SQLite temporary storage is forced into memory. Caches are excluded from backups and cloud exports. Locking clears all indexes and vectors. Policy, revision, or quarantine-state changes invalidate the corresponding cache.
Ordinary bodies are split near Unicode-safe boundaries into chunks of at most 768 characters, usually overlapping by 128 characters. Chinese and mixed text use Jieba tokenization. FTS5 BM25 weights title, body, and safe metadata at 5, 1, and 2. any combines keywords, all requires every term, and phrase requires the original phrase. These constraints also filter vector and hybrid candidates. Queries are escaped rather than executed as arbitrary FTS operators. Basic memory search merges chunks by document; memory_retrieve and rag search return individual chunks with asset ID, revision, stable chunk hash, original UTF-8 offsets, score, and citation label.
Vectors are normalized in-memory caches. Vector retrieval uses cosine similarity. Hybrid retrieval uses reciprocal rank fusion with k of 60. Default lightweight reranking boosts query coverage and exact phrase matching within a bounded candidate set. A configured model endpoint can perform learned reranking.
The default embedding model is in-process EmbeddingGemma 300M Q4, using local ONNX inference with vector dimension 768. ONNX telemetry is explicitly disabled before creating an inference session. The inference engine ships with the application; the approximately 209 MiB model files are public data, downloaded by rag model-install from a pinned repository revision with each file's SHA-256 checked before publication, or supplied through rag.local_model_dir. Verified files can be reused offline. Model files have a separate Gemma license, are not secrets, and are not an independently audited security boundary.
When a remote embedding URL is configured, authorized RAG operations may send all eligible document chunks to that endpoint. Generation sends only the question and selected source chunks. Learned reranking sends its candidates. Remote URLs must use HTTPS; HTTP is allowed only for loopback. Model API redirects are rejected. Loopback requests bypass proxies. Other requests may use HTTP, HTTPS, or SOCKS proxies, as well as proxy environment settings. Retries are bounded and cover only transient connection or read failures, HTTP 429, and selected 5xx responses. Retries may incur duplicate charges. TLS verification is never disabled. API keys are referenced only through environment variable names in configuration, never written into TOML, URLs, or command arguments. MCP tool arguments cannot select provider URLs, credentials, or proxies.
Device-only vaults reject model steps targeting non-loopback providers before inference. Local inference and explicitly configured loopback services remain available.
Retrieve, RagIndex, RagStatus, and RagAsk preserve vault authorization and MCP scopes. IPC performs model work outside the service mutex and rechecks the unlock generation, grants, access rights, and source revision stamps before and after external work and before delivery. Signed intents precede inference or export. Audits record success or failure and delivered source IDs, without recording prompts or keys. Generation treats source text as untrusted data and has no application tools. The model must return a structured answer and citations. Nonexistent citation IDs are rejected. Without evidence, generation is not invoked and the result is marked insufficient_evidence. Citation validation checks only that citations exist; it does not prove generated prose is factually correct or eliminate prompt injection in untrusted source text.
Locking, expired or revoked grants, and source-state changes suppress late results and prevent subsequent steps. Remote requests or inference already started may finish, and remote text already sent cannot be recalled.
Mobile FFI receives improved lexical search and supports BM25 retrieve. Model configuration and inference interfaces have not been added to Flutter. Model-backed operations use the configured desktop daemon.
Cloud Object Synchronization
The desktop CLI performs browser OAuth using PKCE, a loopback callback, and fixed provider endpoints. CloudBegin creates an expiring ticket under sudo, bound to the caller, provider, and client. Network requests run outside the daemon. CloudComplete checks the ticket and current authorization before committing. Locking, signing out, and restarting invalidate unfinished tickets to prevent late results from restoring a login.
State.cloud_accounts stores tokens encrypted locally. Status returns only account metadata. Backups, pairing, vault synchronization, and MCP all exclude tokens. Signing in does not establish vault membership. Each provider connects one account per device; signing in again replaces that provider's account only after the new authorization succeeds.
State.cloud_sync keeps the persistent outbox and inbox receipts in the same encrypted state. CloudSyncEnqueue writes the exact serialized envelope bytes and signed descriptor before any transport I/O. The object ID is the SHA-256 of those bytes, and retries reuse that ID. CloudSyncWork returns ciphertext and listing cursors, never OAuth tokens. sync::execute performs bounded uploads, downloads, and listings without holding the service mutex. CloudSyncAck checks the process-local session generation, then commits upload flags, discovered IDs, downloaded bytes, and vault merges together. A descriptor remains pending if it arrives before its object. Locking, unlocking, and signing out increment the session generation so late acknowledgments cannot import data.
memories cloud sync lets the daemon refresh tokens and upload/download through OneDrive or Google Drive application folders. This HTTP work runs outside the write lock. Tokens are never returned over IPC. Tests use MemoryTransport and loopback Graph/Drive fixtures. Completed queue payloads are compacted into hash receipts and publication markers by provider and recipient. Receipts remain local and do not delete cloud history. Missing objects retry with persistent exponential delays. Pending capacity applies backpressure to listing. Provider workers serialize work per account and reject refresh or import results from invalidated sessions.
Thus, CLI authorization and cloud sync are implemented, and provider transports have been tested with local fixtures. Consent, refresh, two-device convergence, offline retries, and revocation on real OneDrive or Google Drive accounts have not yet been delivered as verified behavior. Mobile sign-in and synchronization controls use AppAuth and the shared Rust worker; real consent and platform validation remain release gates. Mobile background synchronization is not enabled.
Revocation discards queued cloud exports and invalidates captured work before the next object operation. Network requests already started cannot be recalled.
Explicit device synchronization follows a separate path. sync export and sync import exchange complete authenticated envelopes. Online sync send and sync receive deliver the same kind of packet through a stateless relay. Successful sending means only that the relay received the packet. Unknown old-epoch writes enter quarantine and can be re-signed with sync reapply only after local review. Permission forks require someone who is Owner on both branches to sign the selected head and the entire discarded policy chain. Incompatible ownership transfers remain blocked.
Desktop Status Boundaries
Request::DesktopStatus returns initialization and lock state, vault and connection counts, configured provider types, and synchronization queue counts. It runs before unlocked-operation dispatch, does not clone or commit encrypted state, does not touch idle or elevation timers, and exposes no identifiers, account summaries, content, or credentials. Locked responses omit these details. The MCP allowlist rejects this request.
The CLI library hosts the local dashboard and the Windows and macOS trays. memories tray and the standalone memories-tray use native main-thread event loops. The HTTP service and bounded status probes run on separate threads, so a busy daemon does not block the tray or browser. Probes invoke the CLI in the same directory through normal operating-system-authenticated IPC, have deadlines, and clean up child processes. The Windows pipe-read probe deadline is three seconds. Probe failure clears details; locking hides details on the next poll. Polling refreshes neither unlock idle time nor elevation.
The HTTP surface serves only embedded resources and a fixed status projection; it cannot dispatch arbitrary service requests. It listens only on 127.0.0.1. Its only data endpoint is GET /api/status, protected by a randomly generated 256-bit token for each process. The launcher places the token in the URL fragment. The page removes it from the address bar, keeps it only in tab session storage, and sends it through the Authorization header. The full launch URL is a dashboard capability and should not be shared. Unexpected Host values, foreign Origins, or cross-site fetch metadata are rejected. There is no CORS, third-party resources, framing, or mutation routes. The browser receives a typed status projection, rather than a generic IPC bridge, credentials, memory bodies, account names, paths, or grant tokens.
The tray can start the local daemon through an explicit menu action. Closing the tray or dashboard stops its HTTP listener and monitoring, without stopping or unlocking an independently running daemon. Authentication still takes place in the CLI. Malicious programs under the same operating-system account remain inside the existing local trust boundary.
Linux builds include memories webui, using the same loopback dashboard and token boundaries. The native tray event loop compiles only on Windows and macOS. Linux has no native tray.
Local TOML contains only non-secret storage, relay, and desktop settings. An explicitly specified missing file, unknown keys, and malformed values are rejected before opening the store. Relative storage paths resolve against the configuration file. Changing the path selects another store and does not migrate data. Relay URLs retain the existing remote TLS restrictions and cannot contain credentials.