@yottameta/yotta-memory 0.4.2 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -22,7 +22,7 @@
22
22
  多数记忆方案把「记住」做成了黑盒:数据进了数据库或云端,用户既看不到内容、也难以审计,更控制不了「哪个智能体能看到什么」。元忆换了一条路:
23
23
 
24
24
  - **记忆就是文件**:每条记忆是一个带 YAML frontmatter 的 Markdown 文件,放在用户自己的目录里。用任何编辑器都能查看、修改、删除,用 git 就能做版本管理与回滚。
25
- - **隔离由机制保证**:FACT 进公共区共享,PREF / BOUND / COMMIT 进私密区、按 agent 隔离。读取按 scope/owner 分区过滤,越界内容由 CLI 拦截、永不返回(默认静默跳过;显式跨读无授权才报错拒绝)——不依赖智能体的「自觉」。
25
+ - **隔离由机制保证**:FACT 进公共区共享,PREF / BOUND / COMMIT 进私密区、按 owner 物理分目录隔离(`private/<owner>/<type>/`)。读取按 scope/owner 分区过滤,越界内容由 CLI 拦截、永不返回(默认静默跳过;显式跨读无授权才报错拒绝);读写一律走 CLI / MCP,禁止 shell 直读写库文件——不依赖智能体的「自觉」。
26
26
  - **零依赖、即装即用**:无守护进程、无数据库、无向量库,只需要 Node.js。安装即用,数据留在本机。
27
27
  - **便携记忆盘**:记忆库本身就是记忆引擎——装在硬盘或主机上随盘走,局域网内其它主机上的智能体可远程读写;本地零进程模式与局域网常驻模式可并存,插上硬盘即恢复全部记忆。
28
28
 
@@ -31,7 +31,7 @@
31
31
  | 优势 | 说明 |
32
32
  |---|---|
33
33
  | **数据主权在用户** | 明文文件存储,可读、可改、可审计;git 可版本化,回滚 / 团队同步都走标准工具 |
34
- | **真正的权限边界** | 公共 / 私密分区 + scope/owner 过滤 + 授权机制(grant / identity=user / `--unsafe`),越界内容由 CLI 过滤、永不返回(默认静默;显式跨读 `--all`/`--owner <其它>` 无授权时 exit 3),不靠 AI 自律 |
34
+ | **真正的权限边界** | 公共 / 私密分区 + 按 owner 物理分目录(`private/<owner>/<type>/`)+ scope/owner 过滤 + 授权机制(grant / identity=user / `--unsafe`),越界内容由 CLI 过滤、永不返回(默认静默;显式跨读 `--all`/`--owner <其它>` 无授权时 exit 3);`--agent <其它>` 仅作身份声明、不授予跨读。读写走 CLI / MCP,禁 shell 直读写,不靠 AI 自律 |
35
35
  | **跨智能体标准** | 符合 Agent Skills 开放标准(agentskills.io);装一次,Claude Code / Codex / Cursor / OpenCode 等 78+ 智能体可共用同一份记忆 |
36
36
  | **轻量零依赖** | 无 daemon / 无数据库 / 无向量库;Node.js 自带即跑,任何机器可部署 |
37
37
  | **双级存储** | 用户级 `~/.yottamemory/`(跨项目)+ 项目级 `.yottamemory/`(随项目共享 / 交接)|
@@ -61,12 +61,15 @@
61
61
  ### 权限与隔离
62
62
 
63
63
  - **读取三态**:公共 FACT 始终可读;自身私密始终可读;其它 agent 私密默认拒绝(不返回内容)。
64
+ - **物理隔离目录**:私密记忆按 owner 存放于 `private/<owner>/<type>/`,不同智能体的私密文件物理分离;旧版根下平铺的 `prefs/` `bounds/` `commits/` 在 `reindex` 时自动迁移。
64
65
  - **三种授权入口(满足任一即可读他人私密)**:
65
66
  1. `grants.json` 显式授权:`{"<userAgent>": ["<ownerAgent>", ...]}`;
66
67
  2. identity=user:`--agent user` / `--owner user` / 环境变量 `YOTTA_AGENT_ID=user`;
67
68
  3. 显式 `--unsafe`(用户显式授权)。
68
69
  - **默认静默、显式跨读才报错**:默认 recall 遇其它 agent 私密静默跳过(不泄露「存在 N 条私密不可见」);仅当显式跨智能体读取(`--all` / `--owner <其它>`)且无授权命中时才报错 / 警告。
70
+ - **`--agent <其它>` 不越界**:`--agent <其它agent>` 仅作身份声明 / 展示用,不授予读取他人私密;读其它智能体私密仍需 grant / identity=user / `--unsafe`。
69
71
  - **隔离定位**:scope: private 保证的是 AI 之间的语义隔离(正常 recall 不会被搜到、不会主动去读),不是文件系统级机密保护——数据主权在用户,作为所有者有权看到任何记忆文件。
72
+ - **禁止 shell 直读写**:记忆读写一律走 CLI / MCP 工具;用 shell(`Get-ChildItem` / `Get-Content` / `cat` / `ls` / `type` 等)直接读改库文件会绕过 scope/owner 权限边界,读到其它智能体私密内容。
70
73
 
71
74
  ### 智能体身份(唯一 ID + 自我档案)
72
75
 
@@ -170,7 +173,7 @@ npm i -g @yottameta/yotta-memory
170
173
  |---|---|
171
174
  | `yotta-memory init [--project] [--dir <目录>]` | 初始化记忆库(默认用户级 `~/.yottamemory/`;--dir 显式指定位置)|
172
175
  | `yotta-memory remember <type> <subject> <statement> [--owner <id>]` | 写入记忆(同 subject+statement 自动更新;--owner 标注归属)|
173
- | `yotta-memory recall [关键词] [--type T] [--limit N] [--agent <id>] [--owner <id>] [--all] [--unsafe]` | 检索记忆(索引+TF 打分,读取分区过滤;越界读其它智能体私密默认拒绝,需 grant / identity=user / `--unsafe`;项目级优先)|
176
+ | `yotta-memory recall [关键词] [--type T] [--limit N] [--agent <id>] [--owner <id>] [--all] [--unsafe]` | 检索记忆(索引+TF 打分,读取分区过滤;越界读其它智能体私密默认拒绝,需 grant / identity=user / `--unsafe`;`--agent <其它>` 仅作身份声明、不授予跨读;项目级优先)|
174
177
  | `yotta-memory forget <文件>` | 删除一条记忆(按类型目录路径或文件名)|
175
178
  | `yotta-memory archive [--days 180] [--threshold 0.4]` | 归档旧记忆(盖棺分+年龄,immutable 除外)|
176
179
  | `yotta-memory reindex` | 重建索引(手动改 .md 后校正)|
package/SKILL.md CHANGED
@@ -1,14 +1,14 @@
1
1
  ---
2
2
  name: yotta-memory
3
3
  description: "元忆 —— 有权限边界的文件式智能体记忆。文件式、零依赖、可 diff/回滚:让任何 AI 智能体活过会话,开工 recall 恢复上下文、重要信息 remember 落盘、收工归档。类型体系 FACT(公共共享)/ PREF / BOUND / COMMIT(私密隔离)。触发:记住、别忘了、记一笔、记忆、remember、recall、跨会话、上次说到、续测、交接、归档、记忆盘、共享记忆、局域网记忆"
4
- version: 0.4.2
4
+ version: 0.5.0
5
5
  license: MIT
6
6
  ---
7
7
 
8
8
  # yotta-memory(元忆)— 有权限边界的文件式智能体记忆
9
9
 
10
10
  > 一句话:元忆 —— 有权限边界的文件式智能体记忆(不注入、可 diff、能回滚;FACT 共享、PREF / BOUND / COMMIT 私密隔离)。
11
- > 版本:0.4.2 | 最后更新:2026-08-25
11
+ > 版本:0.5.0 | 最后更新:2026-08-25
12
12
 
13
13
  ## 这是什么
14
14
 
@@ -29,7 +29,7 @@ license: MIT
29
29
  1. **开工定向**:先按「开工第一步:确认记忆位置 + 智能体身份」检测记忆库与身份,再运行 `yotta-memory recall`(可带关键词)恢复上下文;项目级记忆优先,其次用户级。
30
30
  2. **进行中落盘**:重要信息立即 `yotta-memory remember <type> <subject> <statement>`,不攒到收工。
31
31
  3. **收工归档**:写会话小结(COMMIT / 笔记),旧记录定期 `yotta-memory archive`。
32
- 4. **多智能体纪律**:FACT 写入公共区,PREF / BOUND / COMMIT 只写本智能体私密区;不读取其他智能体私密区。
32
+ 4. **多智能体纪律**:FACT 写入公共区,PREF / BOUND / COMMIT 只写本智能体私密区;不读取其他智能体私密区。**一切读写一律走 `yotta-memory` CLI / MCP 工具**——禁止用 shell(`Get-ChildItem` / `Get-Content` / `cat` / `ls` / `type` 等)直接读或改记忆库目录下的 `.md` / `index.json` / `tokens.json` / `agents.json` / `grants.json` 等文件,否则会绕过权限边界、读到别的智能体私密内容。
33
33
 
34
34
 
35
35
  ### 开工第一步:确认记忆位置 + 智能体身份(每次会话先做,必做)
@@ -59,6 +59,7 @@ license: MIT
59
59
  - 智能体 ID **必须全局唯一**;**禁止**「从记忆里读到别人的 ID 就当自己的」(如看到「Kali 智能体 ID 为 dashu」就把自己当 dashu)。
60
60
  - 不确定自己的 ID → 先 `whoami` / `agent_info`,再向用户确认;**禁止猜**。
61
61
  - 私密记忆(PREF / BOUND / COMMIT)**必须有 owner**:未声明身份写私密会被引擎拒绝(公共 FACT 不受影响)。
62
+ - **禁止直接读写记忆库文件(硬红线)**:一切读写走 `yotta-memory` CLI / MCP 工具(`remember` / `recall` / `search` / `forget` / `archive` / `reindex` / `export` / `import` / `agent_info`);**禁止用 shell**(`Get-ChildItem` / `Get-Content` / `cat` / `ls` / `type` / `vim` 等)直接读、改、删 `<root>/` 下的记忆文件——否则 `--agent` / `--owner` 的越界拦截形同虚设,会读到别的智能体私密内容。`--agent <其它agent>` 只作身份声明/展示,绝不授予跨智能体私密读取(读他人私密仍需 grant / identity=user / `--unsafe`)。
62
63
 
63
64
  > 已用 `YOTTA_MEMORY_HOME` 临时覆盖时不必改 config;本步骤是常规 CLI 直连用户级位置的引导。
64
65
 
@@ -68,7 +69,7 @@ license: MIT
68
69
  |---|---|
69
70
  | `yotta-memory init [--project] [--dir <目录>]` | 初始化记忆库(默认用户级;--dir 显式指定位置)|
70
71
  | `yotta-memory remember <type> <subject> <statement> [--owner <id>]` | 写入(同 subject+statement 自动更新;--owner 标注归属,默认取 agent id)|
71
- | `yotta-memory recall [关键词] [--type T] [--limit N] [--agent <id>] [--owner <id>] [--all] [--unsafe]` | 检索(索引+TF 打分,读取分区过滤;越界读其它智能体私密默认拒绝,需 grant / identity=user / `--unsafe`;项目级优先)|
72
+ | `yotta-memory recall [关键词] [--type T] [--limit N] [--agent <id>] [--owner <id>] [--all] [--unsafe]` | 检索(索引+TF 打分,读取分区过滤;越界读其它智能体私密默认拒绝,需 grant / identity=user / `--unsafe`;`--agent <其它>` 只作身份声明/展示,不授予跨读——读他人私密同样要授权;项目级优先)|
72
73
  | `yotta-memory forget <文件>` | 删除(按类型目录路径或文件名)|
73
74
  | `yotta-memory archive [--days 180] [--threshold 0.4]` | 归档旧记忆(盖棺分+年龄,immutable 除外)|
74
75
  | `yotta-memory reindex` | 重建索引(手动改 .md 后校正)|
@@ -82,7 +83,18 @@ license: MIT
82
83
 
83
84
  ## 存储格式(摘要)
84
85
 
85
- `<YYYY-MM-DD>-<NNNN>.md`,frontmatter 含 `type / subject / statement / confidence / created / updated / tags / immutable / scope / owner / access_count / last_accessed`;正文为记忆内容。顶层另有 `index.json`(反向索引 + TF 打分,其 `tokens` 字段是分词词频,非访问令牌)与 `agents.json`(智能体身份登记表,唯一性)。
86
+ 目录结构(v0.5.0 起,私密记忆按 owner 物理分目录):
87
+
88
+ ```
89
+ <root>/
90
+ ├── facts/ # FACT 事实(公共可共享)
91
+ ├── private/<owner>/<type>/ # PREF / BOUND / COMMIT,按智能体隔离
92
+ ├── .archive/ # 归档区
93
+ ├── index.json # 检索索引(tokens = 分词词频,非访问令牌)
94
+ └── agents.json # 智能体身份登记表(唯一性)
95
+ ```
96
+
97
+ 记忆文件 `<YYYY-MM-DD>-<NNNN>.md`,frontmatter 含 `type / subject / statement / confidence / created / updated / tags / immutable / scope / owner / access_count / last_accessed`;正文为记忆内容。旧版根下平铺的 `prefs/` `bounds/` `commits/` 会在 `reindex`(或首次 recall 建索引)时按 frontmatter `owner` 自动迁移到 `private/<owner>/<type>/`。
86
98
 
87
99
  自我档案(本智能体身份,强制落盘):PREF,`subject=自我接入档案`,`owner=<本智能体ID>`,statement 为 `; ` 分隔的 key:value——`agent_id / host / memory_home / mcp_mode(stdio|http)/ engine_url(仅远端)/ token(仅远端;本机不存 token)`。
88
100
 
package/USER_GUIDE.md CHANGED
@@ -76,7 +76,7 @@ yotta-memory config get # 查看记忆库位置
76
76
 
77
77
  ### 手动添加 / 编辑记忆
78
78
 
79
- 记忆就是文件:直接在 `facts/` `prefs/` `bounds/` `commits/` 目录下新建或修改 `.md` 文件即可,frontmatter 至少包含 `type` / `subject` / `statement`:
79
+ 记忆就是文件:读写一律走 `yotta-memory` CLI / MCP 工具,**请勿用 shell(`Get-ChildItem` / `Get-Content` / `cat` / `ls` / `type` 等)直接读改记忆库目录下的文件**——那会绕过 scope/owner 权限边界,可能读到其它智能体的私密内容。确需手改时,请把文件放对分类目录:公共事实放 `facts/`;私密的偏好 / 边界 / 承诺放 `private/<owner>/<type>/`(`<owner>` 用你登记的智能体 ID),frontmatter 至少包含 `type` / `subject` / `statement`:
80
80
 
81
81
  > 说明:`index.json` 是引擎自动维护的检索索引,其 `tokens` 字段是中文分词的词频表(供 TF 打分),**不是**访问令牌。手动改过 `.md` 后可用 `yotta-memory reindex` 校正索引,无需手动编辑 `index.json`。
82
82
 
@@ -103,7 +103,7 @@ statement: 本周完成发布
103
103
 
104
104
  ```bash
105
105
  yotta-memory config set memory_home /srv/yotta-memory # 改成你的实际目录
106
- yotta-memory init --dir /srv/yotta-memory # 新库:初始化(自动建 facts/prefs/bounds/commits/.archive)
106
+ yotta-memory init --dir /srv/yotta-memory # 新库:初始化(自动建 facts/private/.archive)
107
107
  ```
108
108
 
109
109
  如果目标目录**已经是记忆库**(里面有 `facts/` 等子目录或 `index.json`,例如从旧机复制或 git 克隆来的),直接 `config set memory_home <目录>` 接入即可,**不要重复 init**。
@@ -156,7 +156,7 @@ yotta-memory token new --agent 我的智能体ID
156
156
 
157
157
  **第 6 步:备份与迁移**
158
158
 
159
- - 备份 = 复制整个记忆目录(`facts/` `prefs/` `bounds/` `commits/` `.archive/` + `index.json` + `agents.json` + `.server/`),复制到哪、哪就是记忆库;迁移同理,整个目录拷走即可。
159
+ - 备份 = 复制整个记忆目录(`facts/` `private/` `.archive/` + `index.json` + `agents.json` + `.server/`),复制到哪、哪就是记忆库;迁移同理,整个目录拷走即可。
160
160
  - `export` / `import` 是把记忆导出成单个 JSON 或从 JSON 导入,适合跨工具交换或归档,不是日常备份的必需步骤。
161
161
 
162
162
  ## 5. 智能体接入篇(本机 / 局域网其它主机)
@@ -192,7 +192,7 @@ yotta-memory remember FACT 主题 内容 # 智能体落盘
192
192
 
193
193
  **本机智能体装好技能后如何获取记忆存放位置?** 按优先级:`YOTTA_MEMORY_HOME` 环境变量 > `config set memory_home` 持久化的 `~/.yottamemory/config.json` > 默认 `~/.yottamemory`。AI 开工执行 `yotta-memory config get` 查看当前生效位置;记忆库移动后执行一次 `config set memory_home <新目录>` 即可。
194
194
 
195
- **给本机智能体设置唯一身份(强制)**:私密记忆(PREF / BOUND / COMMIT)按 owner 隔离,owner 取当前智能体的 agent ID。流程如下:
195
+ **给本机智能体设置唯一身份(强制)**:私密记忆(PREF / BOUND / COMMIT)按 owner 物理分目录隔离(存于 `private/<owner>/<type>/`),owner 取当前智能体的 agent ID。流程如下:
196
196
 
197
197
  1. 开工先 `yotta-memory whoami` 确认「我是谁」。
198
198
  2. 未登记 → 向用户确认一个**全局唯一** ID(建议 `<主机名>-<角色>`,别用 `dashu` / `codex` 这类易撞名),执行 `yotta-memory iam <id>`:引擎**强制唯一性**(被其它主机 / 来源占用会拒绝),并自动落一条「自我接入档案」PREF(owner=自己)。
@@ -232,7 +232,7 @@ yotta-memory remember FACT 主题 内容 # 智能体落盘
232
232
  |---|---|
233
233
  | `yotta-memory init [--project] [--dir <目录>]` | 初始化记忆库 |
234
234
  | `yotta-memory remember <类型> <主题> <内容> [--owner <id>]` | 写入记忆 |
235
- | `yotta-memory recall [关键词] [--type T] [--limit N]` | 检索记忆 |
235
+ | `yotta-memory recall [关键词] [--type T] [--limit N] [--agent <id>] [--owner <id>] [--all] [--unsafe]` | 检索记忆(读取分区过滤;`--agent <其它>` 仅作身份声明、不授予跨读;越界读其它智能体私密默认拒绝,需 grant / identity=user / `--unsafe`)|
236
236
  | `yotta-memory forget <文件>` | 删除一条记忆 |
237
237
  | `yotta-memory archive [--days 180] [--threshold 0.4]` | 归档旧记忆 |
238
238
  | `yotta-memory reindex` | 重建索引 |
@@ -244,7 +244,7 @@ yotta-memory remember FACT 主题 内容 # 智能体落盘
244
244
  | `yotta-memory serve [--port 8787] [--stdio] [--no-auth]` | 启动记忆引擎(--no-auth 关闭鉴权,仅限可信内网)|
245
245
  | `yotta-memory lan enable [--onstart] / disable / status` | 开机自启管理 |
246
246
 
247
- 类型:`FACT`(事实,共享)/ `PREF`(偏好)/ `BOUND`(边界)/ `COMMIT`(承诺),后三类按智能体隔离。
247
+ 类型:`FACT`(事实,共享)/ `PREF`(偏好)/ `BOUND`(边界)/ `COMMIT`(承诺),后三类按智能体物理分目录隔离(`private/<owner>/<type>/`)。
248
248
 
249
249
  ## 7. 故障排查
250
250
 
@@ -273,6 +273,7 @@ yotta-memory remember FACT 主题 内容 # 智能体落盘
273
273
 
274
274
  - token 等同密码:只给需要接入的智能体,别外传。
275
275
  - 元忆不做加密:PREF / BOUND / COMMIT 私密记忆的隔离是「权限边界」机制(谁该读由 scope/owner 决定),属于纪律层保护,不是机密数据保护层。
276
+ - 记忆读写一律走 CLI / MCP;禁止用 shell(`Get-ChildItem` / `cat` / `ls` / `type` 等)直接读改记忆库目录下的文件——否则会绕过 scope/owner 权限边界。
276
277
  - 管理动作(init / config / token / lan / serve)不通过 MCP 暴露,远程只能读写记忆,不能改配置、不能管 token。
277
278
  - `--no-auth` 会关闭鉴权,仅限可信内网使用。
278
279
  - 数据主权在用户:所有数据都是本地明文文件,随时可看、可改、可删。
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  // yotta-memory(元忆): 有权限边界的文件式智能体记忆 CLI(零依赖)
3
3
  // v0.4.0 新增:lan(Windows 计划任务开机自启 serve)/ init --dir(显式指定位置)/ serve --stdio(本地零进程模式)/ MCP 工具补 reindex/export/import
4
+ // v0.5.0 新增:隐私硬隔离——私密物理分目录 private/<agent_id>/{prefs,bounds,commits}/ + 关闭 --agent 越权读 + 私密写跨智能体需 --unsafe + 禁 shell 直读写(文档红线)
4
5
  // v0.4.2 新增:iam/whoami 智能体身份自注册(agents.json 唯一性)+ 自我档案 PREF + 私密记忆须声明 owner
5
6
  'use strict';
6
7
 
@@ -11,9 +12,12 @@ const crypto = require('crypto');
11
12
  const http = require('http');
12
13
  const child_process = require('child_process');
13
14
 
14
- const VERSION = '0.4.2';
15
+ const VERSION = '0.5.0';
15
16
  const TYPES = ['FACT', 'PREF', 'BOUND', 'COMMIT'];
16
17
  const TYPE_DIRS = { FACT: 'facts', PREF: 'prefs', BOUND: 'bounds', COMMIT: 'commits' };
18
+ const PUBLIC_DIR = 'facts';
19
+ const PRIVATE_DIR = 'private';
20
+ const PRIVATE_LEAF = ['prefs', 'bounds', 'commits'];
17
21
  const ARCHIVE_DIR = '.archive';
18
22
  const INDEX_FILE = 'index.json';
19
23
  const PRIVATE_TYPES = ['PREF', 'BOUND', 'COMMIT'];
@@ -59,6 +63,14 @@ function typeDir(type) {
59
63
  }
60
64
  return TYPE_DIRS[t];
61
65
  }
66
+ function privateIdDir(root, id) { return path.join(root, PRIVATE_DIR, id); }
67
+ // 布局:FACT -> <root>/facts;PREF/BOUND/COMMIT -> <root>/private/<owner>/<type>
68
+ function typeSubdir(type, owner) {
69
+ const t = String(type).toUpperCase();
70
+ if (t === 'FACT') return PUBLIC_DIR;
71
+ const id = owner || '';
72
+ return path.join(PRIVATE_DIR, id, TYPE_DIRS[t]);
73
+ }
62
74
  function defaultScope(type) {
63
75
  return PRIVATE_TYPES.indexOf(String(type).toUpperCase()) === -1 ? 'public' : 'private';
64
76
  }
@@ -125,14 +137,12 @@ function frontmatterToText(meta, body) {
125
137
  return lines.join('\n') + String(b).replace(/^\n+/, '') + '\n';
126
138
  }
127
139
  function ensureInit(root) {
128
- fs.mkdirSync(path.join(root, 'facts'), { recursive: true });
129
- fs.mkdirSync(path.join(root, 'prefs'), { recursive: true });
130
- fs.mkdirSync(path.join(root, 'bounds'), { recursive: true });
131
- fs.mkdirSync(path.join(root, 'commits'), { recursive: true });
140
+ fs.mkdirSync(path.join(root, PUBLIC_DIR), { recursive: true });
141
+ fs.mkdirSync(path.join(root, PRIVATE_DIR), { recursive: true });
132
142
  fs.mkdirSync(path.join(root, ARCHIVE_DIR), { recursive: true });
133
143
  const readme = path.join(root, 'README.md');
134
144
  if (!fs.existsSync(readme)) {
135
- fs.writeFileSync(readme, '# yotta-memory(元忆)记忆库\n\n有权限边界的文件式智能体记忆存储目录。结构:facts/ prefs/ bounds/ commits/ .archive/。\n', 'utf8');
145
+ fs.writeFileSync(readme, '# yotta-memory(元忆)记忆库\n\n有权限边界的文件式智能体记忆存储目录。结构:facts/(公共 FACT) private/<agent_id>/{prefs,bounds,commits}/(各智能体私密,物理隔离) .archive/(归档)。\n', 'utf8');
136
146
  }
137
147
  }
138
148
  function nextSeq(dir) {
@@ -162,17 +172,54 @@ function saveIndex(root, entries) {
162
172
  const clean = entries.map(function (e) { const c = Object.assign({}, e); delete c.meta; return c; });
163
173
  fs.writeFileSync(indexPath(root), JSON.stringify({ version: 2, updated: today(), entries: clean }, null, 2), 'utf8');
164
174
  }
165
- function buildIndex(root) {
166
- const entries = [];
167
- for (const t of Object.keys(TYPE_DIRS)) {
168
- const dir = path.join(root, TYPE_DIRS[t]);
175
+ function collectEntryFiles(root) {
176
+ const dirs = [];
177
+ const facts = path.join(root, PUBLIC_DIR);
178
+ if (fs.existsSync(facts)) dirs.push(path.join(root, PUBLIC_DIR));
179
+ const pdir = path.join(root, PRIVATE_DIR);
180
+ if (fs.existsSync(pdir)) {
181
+ for (const id of fs.readdirSync(pdir)) {
182
+ const idDir = path.join(pdir, id);
183
+ if (!fs.statSync(idDir).isDirectory()) continue;
184
+ for (const t of PRIVATE_LEAF) {
185
+ const d = path.join(idDir, t);
186
+ if (fs.existsSync(d)) dirs.push(d);
187
+ }
188
+ }
189
+ }
190
+ const out = [];
191
+ for (const dir of dirs) {
192
+ for (const f of fs.readdirSync(dir)) {
193
+ const fp = path.join(dir, f);
194
+ if (fs.statSync(fp).isFile()) out.push(fp);
195
+ }
196
+ }
197
+ return out;
198
+ }
199
+ // 迁移:把根下旧平铺 prefs|bounds|commits/*.md 按 frontmatter owner 迁入 private/<owner>/<type>/
200
+ function migrateLayout(root) {
201
+ let moved = 0;
202
+ for (const t of PRIVATE_LEAF) {
203
+ const dir = path.join(root, t);
169
204
  if (!fs.existsSync(dir)) continue;
170
205
  for (const f of fs.readdirSync(dir)) {
171
206
  const fp = path.join(dir, f);
172
207
  if (!fs.statSync(fp).isFile()) continue;
173
- entries.push(readEntry(fp, root));
208
+ const meta = parseFrontmatter(fs.readFileSync(fp, 'utf8')).meta;
209
+ const owner = meta.owner || '';
210
+ if (!owner) continue;
211
+ const target = path.join(root, PRIVATE_DIR, owner, t, f);
212
+ fs.mkdirSync(path.dirname(target), { recursive: true });
213
+ fs.renameSync(fp, target);
214
+ moved++;
174
215
  }
175
216
  }
217
+ return moved;
218
+ }
219
+ function buildIndex(root) {
220
+ migrateLayout(root);
221
+ const entries = [];
222
+ for (const fp of collectEntryFiles(root)) entries.push(readEntry(fp, root));
176
223
  saveIndex(root, entries);
177
224
  return entries;
178
225
  }
@@ -284,14 +331,15 @@ function hasGrant(userAgent, ownerAgent) {
284
331
  return false;
285
332
  }
286
333
  // 三态读取判定:'read' | 'denied'
287
- function classifyRead(entry, agent, ownerFilter, unsafe) {
334
+ // selfAgent 为可信身份(env 声明 / 调用方上下文);--agent 仅供授权与展示,不得授予跨智能体私密读取。
335
+ function classifyRead(entry, agent, ownerFilter, unsafe, selfAgent) {
288
336
  if (entry.scope === 'public') return 'read';
289
337
  const owner = entry.owner || '';
290
338
  if (!owner) return 'read';
291
- if (agent && owner === agent) return 'read';
292
- if (ownerFilter && owner === ownerFilter && agent && ownerFilter === agent) return 'read';
293
- if (unsafe || String(ownerFilter).toLowerCase() === 'user' || String(agent && '' + agent).toLowerCase() === 'user') return 'read';
294
- if (hasGrant(agent, owner)) return 'read';
339
+ const own = selfAgent || agent;
340
+ if (own && owner === own) return 'read';
341
+ if (unsafe || String(ownerFilter).toLowerCase() === 'user' || String(agent || '').toLowerCase() === 'user') return 'read';
342
+ if (hasGrant(agent || selfAgent, owner)) return 'read';
295
343
  return 'denied';
296
344
  }
297
345
 
@@ -302,20 +350,26 @@ function initCore(opts) {
302
350
  return { error: false, text: '已初始化记忆库: ' + root };
303
351
  }
304
352
  function rememberCore(type, subject, statement, opts) {
353
+ opts = opts || {};
305
354
  const root = userRoot();
306
355
  ensureInit(root);
307
356
  const t = String(type).toUpperCase();
308
357
  if (!TYPE_DIRS[t]) return { error: true, text: '未知记忆类型: ' + type + '(可用: ' + TYPES.join(' / ') + ')' };
309
- const dir = path.join(root, TYPE_DIRS[t]);
310
358
  const stmt = String(statement || '').trim();
311
359
  const subj = String(subject || '').trim();
312
360
  if (!stmt) return { error: true, text: 'statement 不能为空' };
313
361
  if (!subj) return { error: true, text: 'subject 不能为空' };
314
- const owner = opts.owner || currentAgent();
362
+ const selfAgent = opts.selfAgent || currentAgent();
363
+ const owner = opts.owner || selfAgent;
315
364
  const scope = opts.scope || defaultScope(t);
316
365
  if (scope === 'private' && !owner) {
317
366
  return { error: true, text: '私密记忆必须声明归属智能体:请设环境变量 YOTTA_AGENT_ID(或 AGENT_ID)、传 --owner <id>,或先 yotta-memory whoami / iam 登记唯一身份。公共记忆(FACT)不受影响。' };
318
367
  }
368
+ if (scope === 'private' && owner && selfAgent && owner !== selfAgent && !opts.unsafe) {
369
+ return { error: true, text: '拒绝: 当前声明身份 ' + selfAgent + ' 不能写入其它智能体 ' + owner + ' 的私密区。请用 YOTTA_AGENT_ID 声明自己的身份,或加 --unsafe(用户显式授权)。' };
370
+ }
371
+ const dir = path.join(root, typeSubdir(t, owner));
372
+ fs.mkdirSync(dir, { recursive: true });
319
373
  if (fs.existsSync(dir)) {
320
374
  for (const f of fs.readdirSync(dir)) {
321
375
  const fp = path.join(dir, f);
@@ -350,17 +404,19 @@ function recallCore(query, opts) {
350
404
  const limit = opts.limit || 50;
351
405
  const onlyType = opts.type ? String(opts.type).toUpperCase() : null;
352
406
  const q = query ? String(query).toLowerCase() : '';
353
- const agent = opts.agent || currentAgent();
407
+ const selfAgent = currentAgent();
408
+ const agent = opts.agent || selfAgent;
354
409
  const ownerFilter = opts.owner || '';
355
410
  const allSafe = !!opts.unsafe;
356
- const explicitCross = !!opts.all || (!!ownerFilter && ownerFilter.toLowerCase() !== 'user' && ownerFilter !== agent);
411
+ const impersonation = !!(opts.agent && selfAgent && opts.agent !== selfAgent);
412
+ const explicitCross = !!opts.all || impersonation || (!!ownerFilter && ownerFilter.toLowerCase() !== 'user' && ownerFilter !== agent);
357
413
  const hits = [];
358
414
  let deniedCount = 0;
359
415
  for (const root of roots) {
360
416
  const entries = ensureIndex(root);
361
417
  for (const e of entries) {
362
418
  if (onlyType && e.type !== onlyType) continue;
363
- const r = classifyRead(e, agent, ownerFilter, allSafe);
419
+ const r = classifyRead(e, agent, ownerFilter, allSafe, selfAgent);
364
420
  if (r === 'denied') { deniedCount++; continue; }
365
421
  let score = 0;
366
422
  if (q) {
@@ -408,38 +464,38 @@ function recallCore(query, opts) {
408
464
  }
409
465
  return { error: false, exitCode: 0, text: lines.join('\n') };
410
466
  }
411
- function forgetCore(fileRef) {
467
+ function resolveMemoryFile(root, ref) {
468
+ const map = {};
469
+ for (const fp of collectEntryFiles(root)) {
470
+ const rel = path.relative(root, fp).replace(/\\/g, '/');
471
+ map[rel] = fp;
472
+ const base = path.basename(rel);
473
+ if (!map[base]) map[base] = fp;
474
+ }
475
+ if (map[ref]) return { fp: map[ref], rel: relOf(root, map[ref]) };
476
+ const base = path.basename(ref.replace(/\\/g, '/'));
477
+ if (map[base]) return { fp: map[base], rel: relOf(root, map[base]) };
478
+ return null;
479
+ }
480
+ function relOf(root, fp) { return path.relative(root, fp).replace(/\\/g, '/'); }
481
+ function forgetCore(fileRef, opts) {
482
+ opts = opts || {};
483
+ const selfAgent = opts.selfAgent || currentAgent();
412
484
  const roots = [projectRoot(), userRoot()].filter(function (r) { return fs.existsSync(r); });
413
485
  const ref = String(fileRef || '').replace(/\\/g, '/');
414
- const slash = ref.indexOf('/');
415
- const dynTypeDir = slash === -1 ? '' : ref.slice(0, slash);
416
- const dynBase = slash === -1 ? ref : ref.slice(slash + 1);
417
486
  let target = null, targetRoot = null, targetRel = null;
418
487
  for (const root of roots) {
419
- if (dynTypeDir) {
420
- const dir = path.join(root, dynTypeDir);
421
- if (!fs.existsSync(dir)) continue;
422
- const cand = path.join(dir, dynBase);
423
- if (fs.existsSync(cand)) {
424
- target = cand; targetRoot = root;
425
- targetRel = dynTypeDir + '/' + path.basename(dynBase);
426
- break;
427
- }
428
- continue;
429
- }
430
- for (const t of Object.keys(TYPE_DIRS)) {
431
- const dir = path.join(root, TYPE_DIRS[t]);
432
- if (!fs.existsSync(dir)) continue;
433
- const cand = path.join(dir, dynBase);
434
- if (fs.existsSync(cand)) {
435
- target = cand; targetRoot = root;
436
- targetRel = TYPE_DIRS[t] + '/' + path.basename(dynBase);
437
- break;
438
- }
439
- }
440
- if (target) break;
488
+ const found = resolveMemoryFile(root, ref);
489
+ if (found) { target = found.fp; targetRoot = root; targetRel = found.rel; break; }
441
490
  }
442
491
  if (!target) return { error: true, text: '未找到记忆文件: ' + fileRef };
492
+ const seg = targetRel.replace(/\\/g, '/').split('/');
493
+ if (seg[0] === 'private') {
494
+ const owner = seg[1] || '';
495
+ if (!opts.unsafe && (owner && (selfAgent ? owner !== selfAgent : true))) {
496
+ return { error: true, text: '拒绝: 不能删除其它智能体 ' + owner + ' 的私密记忆(当前身份 ' + (selfAgent || '未声明') + ')。请用 YOTTA_AGENT_ID 声明自己的身份,或加 --unsafe(用户显式授权)。' };
497
+ }
498
+ }
443
499
  fs.unlinkSync(target);
444
500
  if (targetRoot) removeIndexEntry(targetRoot, targetRel);
445
501
  return { error: false, text: '已删除: ' + target };
@@ -452,24 +508,20 @@ function archiveCore(opts) {
452
508
  const cutoff = Date.now() - days * 24 * 60 * 60 * 1000;
453
509
  let moved = 0;
454
510
  const movedFiles = [];
455
- for (const t of Object.keys(TYPE_DIRS)) {
456
- const dir = path.join(root, TYPE_DIRS[t]);
457
- if (!fs.existsSync(dir)) continue;
458
- for (const f of fs.readdirSync(dir)) {
459
- const fp = path.join(dir, f);
460
- if (!fs.statSync(fp).isFile()) continue;
461
- const meta = parseFrontmatter(fs.readFileSync(fp, 'utf8')).meta;
462
- if (meta.immutable === 'true') continue;
463
- if (!meta.created) continue;
464
- const createdTs = new Date(meta.created).getTime();
465
- if (isNaN(createdTs)) continue;
466
- if (vitality(meta) < threshold && createdTs < cutoff) {
467
- const destDir = path.join(root, ARCHIVE_DIR, TYPE_DIRS[t]);
468
- fs.mkdirSync(destDir, { recursive: true });
469
- fs.renameSync(fp, path.join(destDir, f));
470
- movedFiles.push(TYPE_DIRS[t] + '/' + f);
471
- moved++;
472
- }
511
+ for (const fp of collectEntryFiles(root)) {
512
+ const meta = parseFrontmatter(fs.readFileSync(fp, 'utf8')).meta;
513
+ if (meta.immutable === 'true') continue;
514
+ if (!meta.created) continue;
515
+ const createdTs = new Date(meta.created).getTime();
516
+ if (isNaN(createdTs)) continue;
517
+ const t = (meta.type || 'FACT').toUpperCase();
518
+ if (vitality(meta) < threshold && createdTs < cutoff) {
519
+ const destDir = path.join(root, ARCHIVE_DIR, TYPE_DIRS[t]);
520
+ fs.mkdirSync(destDir, { recursive: true });
521
+ fs.renameSync(fp, path.join(destDir, path.basename(fp)));
522
+ const rel = relOf(root, fp);
523
+ movedFiles.push(rel);
524
+ moved++;
473
525
  }
474
526
  }
475
527
  if (movedFiles.length) removeIndexEntries(root, movedFiles);
@@ -510,15 +562,9 @@ function cmdReindex() {
510
562
  }
511
563
  function collectAll(root) {
512
564
  const out = [];
513
- for (const t of Object.keys(TYPE_DIRS)) {
514
- const dir = path.join(root, TYPE_DIRS[t]);
515
- if (!fs.existsSync(dir)) continue;
516
- for (const f of fs.readdirSync(dir)) {
517
- const fp = path.join(dir, f);
518
- if (!fs.statSync(fp).isFile()) continue;
519
- const e = readEntry(fp, root);
520
- out.push({ file: e.file, meta: e.meta });
521
- }
565
+ for (const fp of collectEntryFiles(root)) {
566
+ const e = readEntry(fp, root);
567
+ out.push({ file: e.file, meta: e.meta });
522
568
  }
523
569
  return out;
524
570
  }
@@ -546,7 +592,8 @@ function importCore(root, src) {
546
592
  const meta = item.meta || {};
547
593
  const t = (meta.type || 'FACT').toUpperCase();
548
594
  if (!TYPE_DIRS[t]) continue;
549
- const dir = path.join(root, TYPE_DIRS[t]);
595
+ const dir = path.join(root, typeSubdir(t, meta.owner || ''));
596
+ fs.mkdirSync(dir, { recursive: true });
550
597
  let seq = nextSeq(dir);
551
598
  let file = path.join(dir, today() + '-' + seq + '.md');
552
599
  while (fs.existsSync(file)) { seq = String(parseInt(seq, 10) + 1).padStart(4, '0'); file = path.join(dir, today() + '-' + seq + '.md'); }
@@ -637,8 +684,9 @@ function saveAgents(root, data) {
637
684
  fs.writeFileSync(agentsPath(root), JSON.stringify(data, null, 2), 'utf8');
638
685
  }
639
686
  const SELF_PROFILE_SUBJECT = '自我接入档案';
687
+ function selfPrefsDir(root, agentId) { return path.join(root, PRIVATE_DIR, agentId, 'prefs'); }
640
688
  function findSelfProfile(root, agentId) {
641
- const dir = path.join(root, TYPE_DIRS.PREF);
689
+ const dir = selfPrefsDir(root, agentId);
642
690
  if (!fs.existsSync(dir)) return '';
643
691
  for (const f of fs.readdirSync(dir)) {
644
692
  const fp = path.join(dir, f);
@@ -659,7 +707,8 @@ function selfProfileBody(agentId, root, extra) {
659
707
  return lines.join('; ');
660
708
  }
661
709
  function writeSelfProfile(root, agentId, extra) {
662
- const dir = path.join(root, TYPE_DIRS.PREF);
710
+ const dir = selfPrefsDir(root, agentId);
711
+ fs.mkdirSync(dir, { recursive: true });
663
712
  const seq = nextSeq(dir);
664
713
  const file = path.join(dir, today() + '-' + seq + '.md');
665
714
  const body = selfProfileBody(agentId, root, extra || {});
@@ -758,7 +807,9 @@ function callTool(name, args, ctx) {
758
807
  const agent = ctx.agent || '';
759
808
  try {
760
809
  if (name === 'remember') {
761
- const r = rememberCore(String(args.type || ''), String(args.subject || ''), String(args.statement || ''), { owner: args.owner || agent });
810
+ const ownerArg = args.owner ? String(args.owner) : '';
811
+ if (ownerArg && ownerArg !== agent) return { text: '拒绝: MCP 写入的 owner 必须等于当前智能体身份(' + agent + '),不能写其它智能体私密区。', error: true };
812
+ const r = rememberCore(String(args.type || ''), String(args.subject || ''), String(args.statement || ''), { owner: agent, selfAgent: agent });
762
813
  return { text: r.text, error: r.error };
763
814
  }
764
815
  if (name === 'recall' || name === 'search') {
@@ -766,7 +817,7 @@ function callTool(name, args, ctx) {
766
817
  return { text: r.text, error: r.error };
767
818
  }
768
819
  if (name === 'forget') {
769
- const r = forgetCore(String(args.file || ''));
820
+ const r = forgetCore(String(args.file || ''), { selfAgent: agent });
770
821
  return { text: r.text, error: r.error };
771
822
  }
772
823
  if (name === 'archive') {
@@ -977,7 +1028,7 @@ function usage() {
977
1028
  ' yotta-memory config set memory_home <目录> 持久记住记忆库位置',
978
1029
  ' yotta-memory config get 查看当前配置与生效位置',
979
1030
  ' yotta-memory remember <type> <subject> <statement> [--owner <id>] 写入记忆',
980
- ' yotta-memory recall [关键词] [--type T] [--limit N] [--agent <id>] [--owner <id>] [--all] [--unsafe] 检索(索引+TF,读取分区)',
1031
+ ' yotta-memory recall [关键词] [--type T] [--limit N] [--agent <id>] [--owner <id>] [--all] [--unsafe] 检索(索引+TF,读取分区;--agent 仅表示“以该身份检索/模拟”,跨智能体私密读取需 --unsafe / 授权)',
981
1032
  ' yotta-memory forget <文件或文件名> 删除一条记忆',
982
1033
  ' yotta-memory archive [--days 180] [--threshold 0.4] 归档(盖棺分+年龄)',
983
1034
  ' yotta-memory reindex 重建索引',
@@ -994,6 +1045,7 @@ function usage() {
994
1045
  '',
995
1046
  '类型: FACT(公共共享) / PREF(偏好) / BOUND(边界) / COMMIT(承诺)',
996
1047
  '环境变量: YOTTA_MEMORY_HOME 临时覆盖用户级位置; YOTTA_AGENT_ID/AGENT_ID 当前 agent 标识(本机声明身份用;私密记忆必须有 owner)',
1048
+ '隔离: 公共 FACT 在 facts/;私密 PREF/BOUND/COMMIT 物理分目录 private/<agent_id>/<type>/,禁止 shell 直读写记忆库,一律走本命令',
997
1049
  '远端接入: MCP url http://<主机IP>:8787/mcp;请求头 Authorization: Bearer <token> + X-Agent-Id: <id>',
998
1050
  '',
999
1051
  ].join('\n'));
@@ -1094,4 +1146,7 @@ module.exports = {
1094
1146
  saveConfig: saveConfig,
1095
1147
  loadTokens: loadTokens,
1096
1148
  saveTokens: saveTokens,
1149
+ collectEntryFiles: collectEntryFiles,
1150
+ migrateLayout: migrateLayout,
1151
+ typeSubdir: typeSubdir,
1097
1152
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yottameta/yotta-memory",
3
- "version": "0.4.2",
3
+ "version": "0.5.0",
4
4
  "description": "元忆 —— 有权限边界的文件式智能体记忆。文件式、零依赖、可 diff/回滚;FACT/PREF/BOUND/COMMIT 四分类(公共共享/私密隔离),用户级+项目级双级存储。",
5
5
  "license": "MIT",
6
6
  "keywords": [
@@ -1,4 +1,4 @@
1
- # yotta-memory 协议规范 v0.2.9
1
+ # yotta-memory 协议规范 v0.5.0
2
2
 
3
3
  > 本文件定义 yotta-memory 记忆标准:存储位置、目录结构、文件格式、类型体系与 CLI 命令参考。
4
4
  > 目标:任何支持 Agent Skills 开放标准的智能体,装完即可读写同一份记忆。
@@ -17,18 +17,22 @@
17
17
 
18
18
  ```
19
19
  <root>/
20
- ├── facts/ # FACT 事实(公共可共享)
21
- ├── prefs/ # PREF 偏好(私密)
22
- ├── bounds/ # BOUND 边界(私密)
23
- ├── commits/ # COMMIT 承诺(私密)
24
- ├── .archive/ # 归档区(archive 命令移入)
25
- ├── index.json # 反向索引 + TF 打分(见下方说明)
26
- ├── agents.json # 智能体身份登记表(iam 写入,唯一性强制)
27
- └── README.md # 记忆库说明
20
+ ├── facts/ # FACT 事实(公共可共享)
21
+ ├── private/ # 私密区(按 owner 物理分目录)
22
+ │ └── <owner>/ # 每个智能体一个子目录
23
+ │ ├── prefs/ # PREF 偏好(该 owner 私密)
24
+ │ ├── bounds/ # BOUND 边界(该 owner 私密)
25
+ │ └── commits/ # COMMIT 承诺(该 owner 私密)
26
+ ├── .archive/ # 归档区(archive 命令移入)
27
+ ├── index.json # 反向索引 + TF 打分(见下方说明)
28
+ ├── agents.json # 智能体身份登记表(iam 写入,唯一性强制)
29
+ └── README.md # 记忆库说明
28
30
  ```
29
31
 
30
32
  > **`index.json` 说明**:条目内的 `tokens` 字段是中文分词的「词频表」,供 recall 的 TF 打分使用,**不是**访问令牌。真正的访问令牌由 `token` 命令生成,存放在 `<root>/.server/tokens.json`,仅在局域网 `serve` 模式下用于请求鉴权。
31
33
 
34
+ > **目录结构(v0.5.0)**:私密记忆按 `owner` 物理分目录存放于 `private/<owner>/<type>/`。旧版根下平铺的 `prefs/` `bounds/` `commits/` 会在 `reindex`(或首次 recall 建索引)时按 frontmatter `owner` 自动迁移到 `private/<owner>/<type>/`。**AI 读写红线**:记忆读写一律走 `yotta-memory` CLI / MCP 工具;禁止用 shell(`Get-ChildItem` / `Get-Content` / `cat` / `ls` / `type` 等)直接读改记忆库目录下的文件——否则会绕过 `scope/owner` 权限边界,读到其它智能体的私密内容。
35
+
32
36
  ## 2.5 智能体身份(谁在写 / 谁在读)
33
37
 
34
38
  - **agent ID 必须全局唯一**:`iam <id>` 写入 `agents.json`(记忆库根目录),唯一性强制——ID 已被其它主机 / 来源(含远端 token 登记)占用则拒绝,确认是同一智能体才 `--force`。
@@ -82,12 +86,14 @@ immutable: false
82
86
  | `COMMIT` | 承诺 / 约定 | 私密 |
83
87
 
84
88
  - 写入纪律:公共事实用 FACT;涉及用户偏好 / 边界 / 承诺一律用私密类型,避免公共区泄露。
85
- - 多智能体协作:每个智能体只读自己的私密区(如 `~/.yottamemory/<agent-id>/prefs/` 可自行约定),公共 FACT 共享。
89
+ - 多智能体协作:每个智能体只读自己的私密区(如 `private/<owner>/<type>/` 可自行约定),公共 FACT 共享。
86
90
 
87
91
  ## 隔离说明
88
92
 
89
93
  > 隔离说明:scope: private 保证的是"AI 之间的语义隔离"——其他智能体在正常 recall 会话中不会被搜到、也不会主动去读本条私密记忆。但它不是文件系统级机密保护:在你自己的机器上,作为所有者你有权看到任何记忆文件,数据主权在你。本工具不承诺对抗同权限的本地进程主动读取。
90
94
 
95
+ > **`--agent` 语义**:`--agent <其它agent>` 仅作身份声明 / 展示用(以该身份检索或模拟),并不授予跨智能体私密读取——读取其它智能体私密仍需满足 grant / identity=user / `--unsafe` 任一授权。
96
+
91
97
  读分区边界(recall 三态):
92
98
 
93
99
  | 目标 | 行为 |