Memories / 文档

本页目录

Memories 技术文档

本文说明当前实现的存储、信任边界、本机授权、托管 Center、密封秘密、检索数据流、云对象同步和桌面状态边界。它与 SECURITY.md、docs/architecture.md、docs/authorization.md 和 docs/center.md 描述的是同一套行为。存储格式版本是 1。这是开发软件,测试通过并不等于独立安全审计。

存储与信任边界

本地数据库的表是 header、state 和 checkpoints。除格式元数据、盐和行序号外,值都是带关联数据的密文。日志里同样是密文。没有持久化的明文全文索引。

格式头的 version 必须是 1,否则打开失败。用户密码经 Argon2id(版本 0x13,64 MiB 内存,3 次迭代,并行度 1)和 16 字节盐导出 32 字节密钥。该密钥包裹随机主密钥,关联数据为 memories/master/v1。主密钥加密状态和本地检查点。对称加密使用 XChaCha20-Poly1305。

每个权限纪元有自己的保险库密钥。每个修订有一把随机数据密钥,由该纪元密钥包裹。内容序列化后拆成单独认证的 256 KiB 块,块的位置和总块数进入关联数据。修订签名覆盖资产 ID、父版本、策略头、墓碑标志、密文和设备证书。签名算法是 Ed25519。

修改先在克隆状态上准备,再把加密状态和检查点放进同一个 SQLite 事务提交,synchronous=FULL。读取和执行意图在交付前提交。磁盘失败因此拒绝交付;意图提交后崩溃可能留下未知结果。同一数据目录用操作系统排他锁限制为一个协作写入者。初始化先在同目录写完数据库、关闭并同步,再发布到目标位置,不覆盖已有目标。每台设备使用自己的库。把正在使用的 SQLite 文件交给云盘客户端同步,不能提供多设备事务安全。

用户的 Ed25519 密钥为设备的 Ed25519 签名密钥和 X25519 交换密钥颁发证书。证书本身不够:每个保险库策略还要明确授权设备。同一用户的不同设备密钥互相独立。初始用户签名权能只在认证且面向收件人加密的批准包里交给已批准的个人设备。设备被吊销后,用户身份密钥不能绕过该保险库的设备允许列表。

传输信封包含格式版本、发送设备密钥、收件人交换密钥、临时 X25519 公钥、AEAD 载荷和 Ed25519 签名。传输密钥由 HKDF 导出。收件人身份和临时密钥被认证。加入设备使用会过期的签名请求,并显式固定批准设备的公钥。请求只在成功提交后消耗。批准用的指纹是完整 SHA-256。列表里缩短显示的值不能单独作为批准依据。

策略是签名的线性历史,包含成员、角色、设备允许列表、只增不减的吊销集合和纪元。角色检查使用前一条策略。普通用户可以为自己的身份再授权一台设备,而不改变成员资格。Admin 或 Owner 才能邀请成员、批准加入和为成员授权设备。Admin 可以改 Admin、Editor 和 Reader 的成员,不能把任何人改成 Owner,也不能修改已有 Owner、吊销设备或转移所有权。Owner 可以。所有权转移保持恰好一个 Owner。策略校验会拒绝 Admin 对 Owner 成员或吊销集合的修改。

资产历史是内容寻址的有向无环图。一个资产有多个尚未被消耗的修订时就是冲突。解决修订必须点名全部可见父版本。墓碑让资产离开当前视图,但不删除历史。

备份在独立的 Argon2id 和 AEAD 信封下保存签名的本地状态和恢复材料。恢复生成新的设备密钥和新的保险库 ID,保留当前内容和冲突头,并把原始签名历史放进加密的只读归档。恢复不宣称旧的共享成员资格仍然有效。设备专用库、云授权和云收发队列不进入备份。

下列情况在设计的保护范围内:中继读取或伪造资产内容;在明确核对指纹之后替换对端身份;修改已认证的密文;在没有本地密码时读取拷走的数据库;读者伪造写入;管理员更改所有权;已吊销设备用用户身份密钥给自己签发新的授权设备;CLI 或 MCP 意外打印凭据、在命令行接受密码,或让 MCP 能力继承终端提权;未被发现地修改签名修订或审计、出现链缺口,以及相对剩余数据库状态截断检查点。

下列情况在边界之外,文档不把它们说成已解决:

  • 被攻破的终端、操作系统管理员、已授权的恶意程序、内存检查、键盘记录,以及用户有意复制明文。Rust 拥有的密钥缓冲会清零。操作系统交换区、崩溃转储、依赖库分配和 Dart 垃圾回收字符串没有擦除保证。桌面端没有硬件密钥存储。
  • 中继能观察流量元数据、阻断连接、自行记录密文、延迟吊销或分割设备。随仓库提供的中继服务器不写资产,但不能强迫一个恶意服务器不记录流量。中继不为离线设备排队。
  • 离线吊销不能擦除过去的明文、密钥或备份。尚未收到吊销的终端仍能使用旧的本地副本。
  • 审计证明的是相对已知状态的签名内容和连续性,不是被攻破的设备记录了每一次访问。用一份更旧但内部一致的备份替换整个数据库和本地证据时,若没有更新的对端检查点或外部锚点,就无法发现。没有可信时间戳服务。
  • 同一操作系统用户的其他进程位于本地账户边界之内。IPC 认证的是操作系统用户。终端提权额外使用父进程 PID。这既不是针对同一用户恶意进程的隔离,也不是不受 PID 复用影响的持久会话身份。
  • 可执行文件指纹在启动前检查。没有操作系统沙箱、强制代码签名、原子文件句柄执行或子孙进程遏制。
  • 剪贴板复制、导出的文件,以及用户明确要求复制的凭据,会按用户操作离开加密库。复制后内容在锁定之后仍然存在,操作系统历史或其他应用可能保留它们。

本地 IPC 与授权

service::Request 是共享的类型化操作接口,用蛇形 op 区分。未知的顶层字段会被拒绝。本地 IPC 使用大端 32 位长度前缀的 JSON 帧与版本 1 包装;可选的 HTTP/WebSocket 监听与中继 RPC 共用 WebSocket 二进制 JSON 帧(令牌认证)。桌面远程在解锁/提权后提供与本地 IPC 相同的操作面(ws:remote / relay:remote);手机 FFI/Share 仍有平台拒绝列表。详见 docs/remote-daemon.md 与 SECURITY.md。可选的授权中心([[centers]])禁止本地自举。票据模式签发登记、MCP 授权和解锁票据;托管模式把内容放到 Center 的服务器加密库。见下文,以及 docs/center.md 和 docs/authorization.md。

守护进程只服务当前操作系统用户。Windows 使用命名管道,Linux 和 macOS 使用 Unix 套接字。IPC 最多接受 16 个并发客户端。Windows 命名管道的帧读取仍是阻塞的:停滞的同用户客户端会占用这些名额,但一个停滞的客户端或云传输不会堵住所有其他连接。Unix 帧 I/O 有截止时间。持有写入锁的服务操作,包括受信任的子进程执行,仍然串行。

普通解锁空闲 15 分钟后失效。sudo 使用同一个本地密码,最长 15 分钟,空闲 5 分钟后失效,范围是发出命令的终端父进程。锁定或重启清除提权。完全锁定时的尝试会限速,但不写入持久签名审计。已解锁且有可用本地签名身份时,失败操作会被审计。意图在交付前提交;突然崩溃留下的是已提交的意图,而不是编造的结果。

MCP 授权是本地持有者能力,写到调用者指定的文件,不在终端打印。mcp grant 的 --minutes 决定有效期,--write 才允许创建普通记忆。mcp serve --grant-file 从该文件读取能力,并以 stdio 上的 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、card_link。每次调用都带授权中的保险库,并在分发前单独检查能力。

MCP 不能继承终端提权。它不能读取私人记忆、文件或凭据,不能执行进程、远程会话、回环代理或密码学作业,不能做保险库成员管理,也不能请求 DesktopStatus。锁定会清掉内存中的授权,未过期的在解锁后恢复,锁定本身不删除它们。授权服务的签发、列表、撤销、申请和领取可以由 MCP 工具调用,但仍使用 MCP 能力本身,不继承终端提权,也不使用 Center 管理员 bearer。普通读取不解析 {{secret:标签}}。card_write 不携带秘密映射,所以更新含有秘密引用的卡片会在校验时失败。引用的短 ID 只缩短类型化的凭据 ID,正文中的标记保持不变。

otp_code 实现 RFC 6238 的 TOTP 和 RFC 4226 的 HOTP。HOTP 在返回验证码的同一次提交里保存递增后的计数器。parse_otp_import 接受 otpauth:// 和 Google Authenticator 的 otpauth-migration://offline?data=...,并把账户解析成 TOTP 或 HOTP 卡片。类型写在 URI 主机、type 参数或迁移数据里时按该值识别;没写类型时,带计数器的是 HOTP,否则是 TOTP。调用方不需要再另选类型。命令行的 --image 先用 rqrr 从图片里解码这两种 URI,再走同一条导入路径;URI 和 --image 不能同时给出。多张卡片时 --title 只作为标题前缀,--id 只能替换一张卡片。手机摄像头不导入这两种 OTP 二维码。托管设备配对的扫码是另一条路径,见下文。卡片修订还可以带可选的别名、备注和分类;更新时省略这些字段会保留原值,空字符串会清除。种子不进入搜索或 MCP。TOTP 依赖本机时钟正确。生成结果是经过授权的短时验证码,不是种子导出;审计不记录验证码或种子。

credential_copy 是凭据输出限制的明确例外。它要求本机提权,先提交审计意图,再把 UTF-8 明文交给剪贴板客户端。CLI 只在系统剪贴板写入成功后打印 {"copied":true}。MCP 不能请求复制。审计记录的是 credential.copy 和交付意图,不是操作系统剪贴板的成功回执。

手机 ABI 声明在 crates/ffi/memories.h。句柄是数字登记 ID,不是裸 Rust 指针。返回的字符串用 memories_free 释放一次。非法 JSON 产生通用错误,不回显输入。memories_lock_all 在应用进入后台时推进全局锁世代,并抑制迟到的在途响应,同时丢掉全部托管会话。手机路径不包含进程执行、本地 MCP 授权创建、本地授权管理和权限申请 RPC。连接托管 Center 的 hosted_* 调用是另一条路径:内容只开放列表、读取,以及写入 kind = credential。

密码学作业在解析短 ID 之前检查批准摘要,因此摘要绑定的是调用者看到的引用。服务检查本机提权和保险库访问,只解析凭据资产,提交执行意图,然后调用共享运行器。运行器复查工具,复制并核对输入,在仅所有者可访问的工作区里物化凭据,不用外壳运行命令,抑制工具输出,只发布成功的产物。占位符只替换路径。MCP 允许列表和手机 FFI 拒绝进程作业。

授权、权限申请、托管与密封秘密

本机授权和托管 Center 使用同一套角色天花板。Reader 最大 read,Editor 最大 read_write,Admin 最大 read_write_delete,Owner 最大 allow_all。省略范围是 read。Reader 和 Editor 不能签发或转授。签发同时受角色天花板和调用者所持授权的范围约束,不能跨保险库,不能比父授权活得更久。期限是 1–60 分钟。成员直接签发的深度是 1,再用授权签发则加 1,超过 2 拒绝。Center 上的持久成员凭据是深度 0,不算转授。接收人必须已是该库成员,角色不能高于签发人,也不能是保留名 center-admin。内容授权不改变接收人的角色。

同时有效的授权最多 10000 张(service::MAX_ACTIVE_GRANTS),本机守护进程和托管 Center 共用这一上限。令牌只在签发或领取的那一次响应里出现。持久状态只保存 SHA-256 和元数据,比较在常数时间内完成。锁定丢掉内存密钥并暂停授权,不撤销;解锁或重启后,未过期的授权继续有效。过期授权及其子孙在加载和定时检查时清除。撤销传递到子孙。成员被移除,或签发人角色降到已发范围之下,相关授权在策略提交时失效。授权不进入保险库同步或恢复备份。

权限申请先批准,再领取。范围申请的批准只记录边界,不铸造令牌;申请人再用创建申请时的成员凭据领取一次。若这次响应丢失,令牌不能再取回。角色申请只存在于本机,批准后立即改策略,没有领取;Owner 角色走签名的所有权转移,限时授权不能批准永久的角色变更。托管 Center 只接受范围申请。申请人不能批准或拒绝自己的申请。主体和签发人不同的授权调用者可以提交申请,但不能批准、拒绝、取消或领取。待决上限是每个申请人在每个保险库 20 条、每个保险库 200 条。超过 7 天作废。已决定、已过期或已领取的申请,在最后一次变迁 30 天后删除。

本机审计是按设备签名的哈希链。Center 的 DIR/audit.jsonl 是无密钥链:它能发现相对已知头的篡改,挡不住运营者把整份日志重写。

Center 有两种模式。mode = ticket(默认)只签发登记、MCP 授权和解锁票据,内容留在设备自己的加密库。解锁票据在过期前可以重复使用,不绑定某一台设备,只允许本机用设备密码解锁,不授予 sudo 或 MCP 提权。mode = saas 且 Center 以 --data 运行时,内容在服务器加密库里。运营者持有库密码,能读到已解锁的明文。这是服务器端加密,不是针对运营者的机密性边界。配置里恰好一个 mode = "saas" 时,守护进程把内容 RPC 转到该 Center 的 /v1/rpc。不支持的操作直接失败,不回退到本机库。管理员 bearer 管理锁定、解锁和成员;把它用在内容路由上会得到 401。allow_all 只覆盖托管内容面,不含服务器管理、设备管理、sudo、登记、恢复、执行和密码学作业。

托管库上 e2e: true 的卡片拒绝明文秘密。配对完成后,秘密放在发给设备 X25519 公钥、并由已知 Ed25519 密钥签名的信封里。Center 只保存公钥和密文。它能看见谁、哪张卡片、什么时候,也能扣下或重放旧信封,但看不到正文。设备授权不能签发,也不能审批。撤销会删除该设备的信封,不能收回已经解开的明文。去掉 e2e 标记只能由 Owner 的授权完成,而且那次写入不能携带秘密。本机保险库加入新设备仍使用会过期的签名请求和完整指纹核对,不使用摄像头。

检索与模型数据流

搜索缓存最多保留两个保险库的索引。每个保险库最多 10,000 个分块、16 MiB 来源文本和元数据、32 MiB 向量。进入索引之前就排除私人内容、凭据、文件、已删除、隔离和冲突修订。SQLite 的临时存储强制放在内存。缓存不进入备份或云导出。锁定清空全部索引和向量。策略、修订或隔离状态变化使对应缓存失效。

普通正文在 Unicode 安全的边界附近分成最多 768 个字符的块,通常重叠 128 个字符。中文和混合文本用 Jieba 分词。FTS5 BM25 对标题、正文和安全元数据的权重是 5、1 和 2。any 组合关键词,all 要求每个词都出现,phrase 要求原短语。这些约束也过滤向量和混合候选。查询被转义,不作为任意 FTS 运算符执行。基本 memory search 按文档合并分块;memory_retrieve 和 rag search 返回单个分块,并带资产 ID、修订、稳定分块哈希、原始 UTF-8 偏移、分数和引用标签。

向量是归一化后的内存缓存。向量检索使用余弦相似度。混合检索用倒数排名融合,k 为 60。默认的轻量重排在有限候选集里提高查询覆盖和精确短语匹配。配置的模型端点可以做学习重排。

默认嵌入是进程内的 EmbeddingGemma 300M Q4,经本机 ONNX 推理,向量维度 768。创建推理会话前显式关闭 ONNX 遥测。推理引擎随应用提供;约 209 MiB 的模型文件是公开数据,由 rag model-install 按固定仓库修订下载,并在发布前核对每个文件的 SHA-256,或由 rag.local_model_dir 提供。已核对的文件可以离线复用。模型文件有单独的 Gemma 许可,不是秘密,也不是独立审计过的安全边界。

配置远程嵌入地址后,已授权的 RAG 操作可以把全部合格文档分块发到该端点。生成只发送问题和选中的来源分块。学习重排发送它的候选。远程 URL 必须是 HTTPS;HTTP 只允许环回。模型 API 重定向被拒绝。环回请求不走代理。其他请求可以使用 HTTP、HTTPS 或 SOCKS 代理,以及代理环境。重试有上限,只覆盖瞬时连接或读取失败、HTTP 429 和选定的 5xx。重试可能重复计费。TLS 校验不会被关闭。API 密钥只通过配置中的环境变量名引用,不写入 TOML、URL 或命令参数。MCP 工具参数不能选择提供方 URL、凭据或代理。

设备专用保险库在推理前拒绝发往非环回提供方的模型步骤。本地推理和明确配置的环回服务仍然可用。

Retrieve、RagIndex、RagStatus 和 RagAsk 保留保险库授权和 MCP 范围。IPC 在服务互斥锁之外做模型工作,并在外部工作前后、交付前重新检查解锁世代、授权、访问权和来源修订戳。签名意图先于推理或导出。审计记录成功或失败以及交付的来源 ID,不记录提示词或密钥。生成把来源文本当作不可信数据,没有应用工具。模型必须返回结构化的答案和引用。不存在的引用 ID 被拒绝。没有证据时不调用生成,结果标记 insufficient_evidence。引用校验只检查引用是否存在,不证明生成的散文事实正确,也不消除不可信来源文本中的提示注入。

锁定、过期或撤销的授权,以及来源状态变化,都会抑制迟到结果并阻止后续步骤。已经开始的远程请求或推理可能跑完,已经发出的远程文本不能撤回。

手机 FFI 获得改进后的词法搜索,并支持 BM25 retrieve。模型配置和模型推理界面没有加入 Flutter。模型支持的操作使用已配置的桌面守护进程。

云对象同步

桌面 CLI 用 PKCE、环回回调和固定的提供方端点做浏览器 OAuth。CloudBegin 在 sudo 下创建一张会过期、并绑定调用者、提供方和客户端的票据。网络请求在守护进程之外进行。CloudComplete 在提交前检查票据和当前授权。锁定、登出和重启使未完成票据失效,避免迟到结果恢复登录。

State.cloud_accounts 把令牌加密保存在本机。状态只返回账户元数据。备份、配对、保险库同步和 MCP 都不包含令牌。登录不建立保险库成员资格。每个提供方在每台设备上连接一个账户;再次登录只有在新授权成功后才替换该提供方的账户。

State.cloud_sync 把持久的发件箱和收件箱回执放在同一个加密状态里。CloudSyncEnqueue 在任何传输 I/O 之前写入精确的序列化信封字节和签名描述符。对象 ID 是这些字节的 SHA-256,重试复用该 ID。CloudSyncWork 返回密文和列表游标,不返回 OAuth 令牌。sync::execute 在不持有服务互斥锁时做有界的上传、下载和列举。CloudSyncAck 检查进程本地的会话世代,然后一起提交上传标志、发现的 ID、下载字节和保险库合并。描述符先于对象到达时保持待处理。锁定、解锁和登出增加会话世代,使迟到确认不能导入。

memories cloud sync 让守护进程刷新令牌,并通过 OneDrive 或 Google Drive 的应用文件夹上传和下载。这段 HTTP 在写入锁之外运行。令牌从不经 IPC 返回。测试使用 MemoryTransport 和环回的 Graph/Drive 夹具。完成的队列载荷压缩成哈希回执,以及按提供方和收件人的发布标记。回执留在本地,不删除云端历史。缺失对象按持久化的指数延迟重试。待处理容量对列举施加背压。提供方工作线程按账户串行,并拒绝来自已失效会话的刷新或导入结果。

因此,CLI 授权和 cloud sync 已经实现,提供方传输用本地夹具测试过。真实 OneDrive 或 Google Drive 账号上的同意、刷新、双设备汇合、离线重试和吊销还没有作为已验证行为交付。手机端的登录和同步控件使用 AppAuth 和共享 Rust 工作线程;真实同意和平台验证仍是发布门槛。手机后台同步没有启用。

吊销会丢弃已排队的云导出,并在下一次对象操作前使已捕获的工作失效。已经开始的网络请求不能撤回。

显式的设备同步是另一条路径。sync export 和 sync import 交换完整的认证信封。在线的 sync send 和 sync receive 把同一类数据包交给无状态中继。发送成功只表示中继收到了数据包。未知的旧纪元写入进入隔离区,只能在本地审阅后用 sync reapply 重新签名。权限分叉要求一名在两条分支上都是所有者的人签名选定的头和整条被丢弃的策略链。不兼容的所有权转移保持阻断。

桌面状态边界

Request::DesktopStatus 返回初始化与锁定状态、库和连接计数、已配置的提供商种类,以及同步队列计数。它在解锁操作分发之前运行,不克隆或提交加密状态,不触碰空闲或提权计时器,也不暴露标识符、账户摘要、内容或凭据。锁定时的响应没有这些细节。MCP 允许列表拒绝这个请求。

CLI 库承载本地仪表板,以及 Windows 和 macOS 托盘。memories tray 和独立的 memories-tray 使用原生主线程事件循环。HTTP 服务和有界状态探测在单独的线程运行,因此忙碌的守护进程不会堵住托盘或浏览器。探测通过正常的操作系统认证 IPC 调用同目录的 CLI,有截止时间,并清理子进程。Windows 管道读取的探测截止时间是三秒。探测失败时清除细节;锁定在下一次轮询隐藏细节。轮询不刷新解锁空闲时间,也不刷新提权。

HTTP 面只提供嵌入的资源和固定的状态投影,不能分发任意服务请求。它只监听 127.0.0.1。唯一的数据端点是 GET /api/status,由每次进程随机生成的 256 位令牌保护。启动器把令牌放在 URL 片段中。页面从地址栏移除它,只保存在标签页会话存储里,并用 Authorization 头发送。完整的启动 URL 是仪表板能力,不应分享。意外的 Host、外来的 Origin 或跨站获取元数据会被拒绝。没有 CORS、第三方资源、框架嵌入或变更路由。浏览器收到的是类型化状态投影,不是通用 IPC 桥、凭据、记忆正文、账户名、路径或授权令牌。

托盘可以通过明确的菜单动作启动本地守护进程。关闭托盘或仪表板会停止它的 HTTP 监听和监测,不停止也不解锁一个独立运行的守护进程。认证仍在 CLI 中完成。同一操作系统账户中的恶意程序仍处在既有的本地信任边界内。

Linux 构建包含 memories webui,使用同一套环回仪表板和令牌边界。原生托盘事件循环只在 Windows 和 macOS 上编译。Linux 没有原生托盘。

本地 TOML 只包含非秘密的存储、中继和桌面设置。显式缺失的文件、未知键和畸形值在打开库之前被拒绝。相对存储路径相对配置文件解析。更改路径会选择另一个库,不迁移数据。中继 URL 保持现有的远程 TLS 限制,并且 URL 中不能带凭据。