memories · 已实现行为 · main @ c9ea7ea · 2026-10-12

业务逻辑整理

这次拉下来的主线是三件事叠在原有本机保险库上:角色封顶的授权、托管 Center,以及设备之间的密封秘密。下面按「数据放在哪、谁能批准、令牌怎么发出来、秘密怎么不让服务器看见」整理。实线是文档和代码里已经落地的行为。

已实现

1. 数据放在哪里

daemon / ticket / saas

同一个 [[centers]] 配置可以走出三条完全不同的路。内容要么在本机,要么在 Center 的服务器加密库里。密封信封是托管库上的例外:Center 只保存公钥和密文。

flowchart TD
  start["客户端或守护进程"] --> pick{"配置了哪种 Center"}
  pick -->|"没有 Center
或只做本机库"| local["memories daemon"] local --> localstore["本机加密 Store
设备密码解开"] localstore --> localcontent["记忆、卡片、文件、凭据
密钥留在设备"] pick -->|"mode = ticket
默认"| tickets["memories-center
不带 --data"] tickets --> signed["签发三类票据
登记 / MCP 授权 / 解锁"] signed --> stilllocal["内容仍在各设备自己的库
Center 不保存保险库明文"] pick -->|"恰好一个 mode = saas"| attach["守护进程把内容 RPC
转到该 Center 的 /v1/rpc"] attach --> hosted["memories-center --data"] hosted --> serverstore["服务器加密 Store
运营者持有库密码"] hosted --> members["持久成员与限时授权
成员文件里是明文 bearer"] hosted --> sealed["e2e 卡片的密封信封
Center 读不到秘密正文"] attach --> nofallback["本机不再作为内容后端
不支持的操作直接失败"]

2. 角色封顶,再发授权

grant

省略范围一律是 read,不再默认全开。签发同时看两道天花板:签发人当前角色,以及调用者手里那张授权自己的范围。超出去就拒绝,不会悄悄缩小。

保险库角色能发出的最大范围能否签发或转授成员管理
Readerread不能不能
Editorread_write不能不能
Adminread_write_delete能能管 Admin 及以下,不能动 Owner
Ownerallow_all能唯一 Owner;转让用签名的所有权转移
flowchart TD
  caller["Admin 或 Owner
本机须已解锁,不需要终端提权"] --> ask{"请求的范围、期限、接收人"} ask --> ceil{"是否同时落在
角色天花板和父授权范围内"} ceil -->|"超出、跨库、比父授权活得更久
或期限不在 1 到 60 分钟"| deny["拒绝"] ceil -->|"通过"| who{"接收人"} who -->|"还不是该库成员
角色高于签发人
或是 center-admin"| deny who -->|"深度将超过 2"| deny who -->|"合格"| mint["签发"] mint --> once["令牌只在这一次响应里出现"] mint --> hash["库里只存 SHA-256 和元数据"] hash --> live["锁定时暂停使用,不撤销
解锁或重启后未过期的继续有效"] live --> drop["过期在加载和定时检查时清掉
撤销会连同子孙一起失效"]

3. 权限申请:先批准,再领取

request

批准不再直接把令牌交出去。范围申请先记下批准边界,申请人再用自己的成员凭据领取一次。角色申请只存在于本机,批准后立刻改策略,没有领取这一步。托管 Center 只接受范围申请。

flowchart TD
  member["任意当前成员"] --> create["提交申请
范围默认 read,期限默认 30 分钟"] create --> cap{"该成员在该库的待决申请 ≤ 20
且该库待决总数 ≤ 200"} cap -->|"超出"| limit["拒绝
Center 返回 429"] cap -->|"通过"| pending["pending"] pending --> route{"审批人列表"} route -->|"空"| broadcast["广播给该库全部 Admin/Owner"] route -->|"点名"| named["只限列出的 Admin/Owner
创建时和决定时都再核对"] pending --> self{"谁来决定"} self -->|"申请人自己"| noself["不能批准或拒绝自己的申请
可以取消仍在 pending 的申请"] self -->|"指定接收人的授权调用者"| nogrant["不能批准、拒绝、取消或领取"] self -->|"合格的 Admin/Owner"| fit{"是否落在审批人的角色、
范围、库和剩余期限里"} fit -->|"超出"| stay["保持 pending"] fit -->|"拒绝"| rejected["rejected,只此一次"] fit -->|"批准范围"| approved["approved
只记录参数,不铸造令牌"] fit -->|"批准角色,仅本机"| rolechg["立即改角色
Owner 角色走签名的所有权转移
限时授权不能批永久角色变更"] approved --> claim["原申请人用成员凭据领取一次"] claim --> token["这时才铸造
期限从领取时起算,且不超过批准时记下的父期限"] pending --> exp1["超过 7 天:expired
审计 actor = system"] approved --> exp2["批准后 7 天未领取:expired"] rejected --> prune["决定、过期或领取之后 30 天删除"] exp1 --> prune exp2 --> prune token --> prune

4. 托管成员、锁定和内容范围

saas

运营者的管理员 bearer 管锁、成员和票据。它不能拿自己直接读写 /v1/memories 或 /v1/rpc。内容访问要用另一张成员或授权 bearer。这是职责分开,不是针对运营者的加密隔离。

flowchart TD
  op["运营者管理员 bearer"] --> adminroutes["状态、锁定、解锁、成员增删改"]
  op --> mintpath["签发票据或限时授权
不指定接收人时主体是授权自己的 ID"] member["持久成员 bearer"] --> content["/v1/memories 与 /v1/rpc
绑定一个保险库"] member --> authz{"角色是 Admin 或 Owner?"} authz -->|"是"| manage["在该库内列访问、签发、撤销、审批"] authz -->|"否"| useonly["只能按内容范围使用
不能签发或审批"] adminroutes --> lock["锁定:丢掉内存密钥,授权暂停"] lock --> locked503["内容与铸造访问令牌返回 503"] lock --> stillok["状态、成员列表和移除、设备票据仍可用"] locked503 --> unlock["用运营者库密码远程解锁
密码不写入启动配置"] unlock --> resume["静态授权、成员和未过期授权恢复
已撤销的不会复活"]
内容范围托管库里允许的事
read列表、搜索、图谱、授权范围内的读取、历史、差异、审计,以及显式的凭据复制
write创建和更新。不能读已有内容,也不能删除
read_write上面两者,外加 otp_code(HOTP 会推进计数器)
read_write_delete再加资产、事物、属性和自动化钩子的删除
allow_all仍只在内容面上。不含服务器管理、设备管理、sudo、登记、恢复、远程会话、云同步、执行和密码学作业

5. 设备配对

hosted device

托管库默认可被服务器阅读。要让 Center 看不见的秘密,先把 CLI 设备和手机配对。手机身份的种子只在手机上,Center 不保管。

flowchart TD
  init["CLI:device hosted init
32 字节种子放进系统保管
派生 X25519 和 Ed25519"] --> pair["CLI 用成员令牌发起配对
提交两把公钥和自签名"] pair --> pending["Center 建立 5 分钟的待决申请
返回配对码、申请 ID 和二维码载荷"] pending --> scan["该库管理员的手机扫码或手输配对码"] scan --> check{"二维码类型、Center ID、
指纹是否与服务器返回一致"} check -->|"不一致、过期、已用"| stop["拒绝
未知、过期、已用都是 404"] check -->|"一致"| sas["手机先显示 6 位 SAS
再决定是否批准"] sas --> approve["手机用自己的 Ed25519 签署证明
覆盖指纹、双方设备密钥、配对 ID、审批人、范围和期限"] approve --> claim["CLI 领取一次
设备范围的令牌进系统保管
Center 只留哈希"] claim --> compare["CLI 打印同一组 SAS
数字不同就撤销并重新配对"] compare --> bound["设备授权绑定到未撤销、已证明的设备
以及该成员的世代"]

6. 密封秘密与轮换

envelope

信封用接收设备的 X25519 加密,并由发送方 Ed25519 签名。Center 能扣下或重放旧信封,也能看见「谁、哪张卡片、什么时候」,但看不到正文。版本对每台设备单调增加。

flowchart TD
  card["卡片标成 e2e"] --> refuse["托管接口拒绝明文秘密
去掉 e2e 标记的写入也拒绝"] card --> seal["已证明的设备写入信封
接收方必须是该设备的 X25519
签名者必须是已知设备"] seal --> store["Center 只存密文和公钥"] store --> read["目标设备取出并解开"] read --> revoke{"设备被撤销或失效"} revoke --> wipe["删除该设备的信封
相关卡片标成 needs_rotation"] wipe --> upstream["已送到设备上的明文收不回
先在上游更换秘密"] upstream --> reseal["手机为当前仍获批的全部设备重新密封
版本加一,并清掉轮换标记
已撤销设备不包含在内"] refuse --> clear{"谁能清掉 e2e 标记"} clear -->|"Owner 的授权,且这次写入不含秘密"| normal["之后按普通规则写"] clear -->|"设备授权"| keep["不能清除标记"]

7. 各端能碰到哪些业务

cli / mcp / ffi
面内容授权与申请设备与密封秘密
本机守护进程与 CLI完整保险库。私人内容、文件、凭据和敏感管理要 sudo完整的签发、列表、撤销、申请、领取、审计。手机 FFI 不开放这些 RPCdevice hosted 完成初始化、配对、领取、SAS、日志核对和密封读写
普通 MCP按保险库、期限和范围。不继承终端提权,不读私人内容、文件和凭据有对应工具,但仍用 MCP 能力本身,不用 Center 管理员 bearer,也不继承提权device_list、sealed_reveal(必须带揭示明文的意图和理由)
托管 /v1/rpc上表的内容范围。运营者本地提权不会被搬上来HTTP 授权路由。申请只有范围,没有角色变更设备路由和密封路由分开。内容面上的 allow_all 管不了设备
手机 hosted 客户端直接连 SaaS Center。内容 RPC 只开放列表、读取,以及写入 kind = credential。界面标明服务器可读可列申请、批准、拒绝,以及查看访问名单。bearer 只留在 Rust 内存,应用侧放进平台安全存储扫码配对、证明、重新密封、恢复码和身份轮换
手机本地 FFI本机库的搜索、文件、成员批准、中继、历史和审计。进后台会锁定拒绝本地授权管理和权限申请。导入 Center 票据仍要 Admin/Owner,之后按范围检查生物识别只解开本机库,不把设备密钥交给 Center

文档

已补上

中文产品说明和技术文档已经按本页写上授权、托管 Center 和密封秘密。手机扫码只用于托管配对;OTP 二维码仍走命令行 --image。有效授权上限是 10000 张。