这次拉下来的主线是三件事叠在原有本机保险库上:角色封顶的授权、托管 Center,以及设备之间的密封秘密。下面按「数据放在哪、谁能批准、令牌怎么发出来、秘密怎么不让服务器看见」整理。实线是文档和代码里已经落地的行为。
同一个 [[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["本机不再作为内容后端
不支持的操作直接失败"]
MEMORIES_CENTER_TOKEN 是成员、静态授权或限时访问 bearer,不能是管理员令牌。本地 MCP 能力不会带着这枚 bearer 转发出去。private 只影响摘要和检索,挡不住已授权的托管读取。省略范围一律是 read,不再默认全开。签发同时看两道天花板:签发人当前角色,以及调用者手里那张授权自己的范围。超出去就拒绝,不会悄悄缩小。
| 保险库角色 | 能发出的最大范围 | 能否签发或转授 | 成员管理 |
|---|---|---|---|
| Reader | read | 不能 | 不能 |
| Editor | read_write | 不能 | 不能 |
| Admin | read_write_delete | 能 | 能管 Admin 及以下,不能动 Owner |
| Owner | allow_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["过期在加载和定时检查时清掉
撤销会连同子孙一起失效"]
批准不再直接把令牌交出去。范围申请先记下批准边界,申请人再用自己的成员凭据领取一次。角色申请只存在于本机,批准后立刻改策略,没有领取这一步。托管 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
DIR/audit.jsonl 的无密钥链,能发现相对已知头的篡改,挡不住运营者整份重写。运营者的管理员 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、登记、恢复、远程会话、云同步、执行和密码学作业 |
center-admin 留给运营者。省略角色时,按范围映射:read 为 Reader,write / read_write 为 Editor,read_write_delete 为 Admin,allow_all 为 Owner。显式指定角色时,范围超过该角色的天花板会被拒绝,不会自动缩小。DIR/members.json。铸出的限时授权在 DIR/grants.json 里只有哈希。成员或审计写失败会锁库并要求修复后重启,避免不确定的落盘把已撤销的 bearer 救回来。托管库默认可被服务器阅读。要让 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["设备授权绑定到未撤销、已证明的设备
以及该成员的世代"]
信封用接收设备的 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["不能清除标记"]
device hosted log --verify 核对链、每条签名,以及是否接在上次验证过的头之后。otpauth://totp/...。取出时按 URI 里的算法、位数和周期计算。| 面 | 内容 | 授权与申请 | 设备与密封秘密 |
|---|---|---|---|
| 本机守护进程与 CLI | 完整保险库。私人内容、文件、凭据和敏感管理要 sudo | 完整的签发、列表、撤销、申请、领取、审计。手机 FFI 不开放这些 RPC | device 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 |
{{secret:标签}},复制凭据是单独的提权操作。中文产品说明和技术文档已经按本页写上授权、托管 Center 和密封秘密。手机扫码只用于托管配对;OTP 二维码仍走命令行 --image。有效授权上限是 10000 张。