feat(memory): sensitive 标记 + 提取器敏感判据,私事退出受限车道召回

M63(M60 审计 P0-1/P0-2)。作用域 ≠ 敏感度:`scope='user'` 是共享域,里
面却装着住址(花溪 64 条 active)/私称(叶酱 63)/作息(早睡 38),于是受限车
道(群聊/陌生人)把共享域整锅注入上下文 —— 2026-10-29 群 531082168 的住址泄漏
即此路径。本片给 memory_items 加 `sensitive` 列,把「这条归谁」(scope)与
「这条能不能外说」(sensitive)拆成两根正交的轴。

- 迁移 0028:加列(INTEGER NOT NULL DEFAULT 0)+ **同迁移幂等回填**(精确短语
  白名单 花溪/叶酱/早睡,`AND sensitive=0`,不改 updated_at),down 只撤结构;
  memory 包内 `ensureSensitiveSchema` 同形前滚(任意 *sql.DB 自举)。
- 受限判据:新增 `MemoryItem.SharedVisible()` = 共享域 且 !sensitive,成为
  restricted 侧唯一起点;`loop.FilterSharedVisible` 与复用它的 knowledge 工具
  自动跟随。主人车道不过这道判据,照常读到全部记忆(sensitive 不是权限系统)。
- 提取器:prompt 追加 sensitive 位(住址/私人称呼/家人/健康/作息与行程 → true,
  口味/兴趣/项目约定 → false,含正反例);产出侧与落库侧共用
  `resolveSensitive`(模型的类目判断 ∨ 已实证短语白名单),只加标不撤标。
- 回填入口 `BackfillSensitive(ctx, db, dryRun)`:幂等、可 dry-run、返回候选池
  前后规模与逐短语计数(生产快照实测 3116 → 2948,标 725 行)。
- 读路径并到 15 列(scanItem + 7 处 SELECT);store.go 拆分 store_insert.go
  守住 500 行上限。

验收:go build/vet/test 全绿(memory + loop 单包复跑绿);删实现三处变异分别打红
(sensitiveColumn 恒 0 / SharedVisible 忽略 Sensitive / Extractor 跳过白名单);
生产库快照上跑 0028 后 Go 侧回填 marked=0(迁移 SQL 与 privateFacts 等价)。
This commit is contained in:
AskaEth committed 2026-10-30 09:26:15 +08:00
1 parent a8bc2b4470
commit 44fbd29836
22 files changed
+1360 -102

No files matched your search

+9 -25
View File
@@ -8,23 +8,24 @@ import (
)
// FilterSharedVisible 返回群聊/陌生人(受限环境)可见的记忆子集:按**作用域**
// 只保留共享域(用户习惯 ScopeUser 与项目 ScopeProject,人人可读),
// 并剔除 L4 意图记录(capacity.RecordIntent 的回写格式,内容是主人逐句
// 原话的机器可读形态 —— 虽落在共享域以便 L4 闭环学习,但只该暴露
// 「不敏感的习惯结论」,不该暴露主人原话)。角色日常(ScopePersona)
// 是主人私有的敏感项,不在可见域内(design/03 §3/§5「不泄露主人敏感信息」)。
// 只保留共享域(用户习惯 ScopeUser 与项目 ScopeProject,人人可读),剔除主人
// 私有的角色日常(ScopePersona);再按**敏感度**剔除敏感标记为真的条目
// (MemoryItem.SharedVisible,迁移 0028);并剔除 L4 意图记录
// (capacity.RecordIntent 的回写格式,内容是主人逐句原话的机器可读形态 —— 虽落在
// 共享域以便 L4 闭环学习,但只该暴露「不敏感的习惯结论」,不该暴露主人原话)。
//
// 它是**唯一**的隔离判据:循环的记忆注入(retrieveMemoryBlock)与模型
// 可达的 knowledge 检索工具都调用它。knowledge 若自造一套过滤(或不过滤),
// 一旦管理面板把它放行给群聊/陌生人,就会返回角色日常与主人原话
// (审计 2026-10-05:knowledge 绕过群聊/陌生人召回过滤,P0 红线)。
//
// ⚠️ 它按作用域分档,**不按内容分档**:过了这道过滤不等于内容不敏感
// (见 sharedScope 的说明与审计 2026-10-30 §6 R1)。
// 🔴 **它只决定「受限车道看不看得见」**:主人车道(`!env.Restricted()`)根本不过
// 这道过滤,照常读到全部记忆(含 sensitive=1)。sensitive 是数据层标记,不是
// 权限系统(切片 M63、审计 2026-10-30 §7 P0-1)。
func FilterSharedVisible(results []memory.Result) []memory.Result {
visible := make([]memory.Result, 0, len(results))
for _, r := range results {
if !sharedScope(r.Item.Scope) {
if !r.Item.SharedVisible() {
continue
}
if capacity.IsIntentRecord(r.Item.Content) {
@@ -35,23 +36,6 @@ func FilterSharedVisible(results []memory.Result) []memory.Result {
return visible
}
// sharedScope 报告 scope 是否为群聊/陌生人可查的共享域:用户习惯
// (user,全局共享)与项目记忆(project,人人可读)。角色日常(persona)
// 是主人私有的敏感项,不在其列(design/03 §3/§5)。
//
// 🔴 **作用域 ≠ 敏感度**(审计 2026-10-30 §6 R1 纠正了本注释此前的说法:
// 原文写「敏感度划分即作用域本身」,那个前提是错的)。生效裁决只按作用域
// 分档,而 `user` 域里事实上装着住址、私称、作息这类私事(data/cyrene.db
// 实测:花溪 64 条 / 叶酱 63 条 active),它们会随本判据进入受限车道的召回
// 候选。因此「过了 FilterSharedVisible 就一定不敏感」**不成立**:群聊/陌生人
// 车道还有一道呈现层防线 —— 公域呈现块与发言归属口径
// (channelbridge 的 publicPresencePrompt、loop/attribution.go)明令
// 即使读到主人私事也不得外说。按内容逐条判敏感(sensitive 列 + 提取器判据)
// 是 MEMSENS 切片的事,届时本判据才会带上内容维度。
func sharedScope(s memory.Scope) bool {
return s == memory.ScopeUser || s == memory.ScopeProject
}
// formatMemoryBlock 由检索结果构建前导系统块。非空
// marker 作为该块的前缀,使隔离的群聊读取可与
// 主人的完整读取区分开。