Files
Cyrene_v2/internal/db/migrations/0028_memory_sensitive.up.sql
T
AskaEth 44fbd29836 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 等价)。
2026-10-30 09:26:15 +08:00

69 lines
5.1 KiB
SQL
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
-- 0028_memory_sensitive:**主人的私事不许进受限车道**(切片 M63,M60 审计 P0-1)。
--
-- 依据:DevDocs/audit/群聊主人信息泄漏调查-2026-10-30.md §6 R1 + §7 P0-1;
-- 塔裁决 2026-10-30:私事用 sensitive 标记,**不迁 scope**(迁 persona 域会让其他角色
-- 可读,否决);「用户习惯可查」的既有裁决(design/03 §5)不动。
--
-- 为什么是**新列**而不是换作用域:作用域与敏感度是两根正交的轴。`scope` 回答
-- 「这条归谁、谁可写谁可读」,本列回答「这条能不能说给第三方听」。实证(只读实测,
-- 审计 §2):`scope='user'` 里装着住址(`花溪` 64 条 active)、私称(`叶酱` 63 条)、
-- 作息(`早睡` 38 条)—— 作用域是共享域,内容却是私事。把它们迁进 `persona` 域
-- 等于让其他角色可读(memory/rules.go 的跨角色只读),语义更差。
--
-- 🔴 **sensitive 是数据层标记,不是权限系统**:主人车道(非受限环境)照常读到全部
-- 记忆(含 sensitive=1);它只让**受限车道**(群聊 / 陌生人)的召回过滤器把这一条
-- 排除在候选池外(internal/runtime/memory/sensitive.go 的 SharedVisible →
-- internal/runtime/loop/memory_filter.go 的 FilterSharedVisible)。谁是主人仍只由
-- actor 元数据 + 白名单 + 策略引擎判定(AGENTS §9),本列不参与任何身份判定,
-- 也不改变任何一行的读/写权限。
--
-- 列值:0 = 非敏感(默认,含全部历史行)、1 = 敏感。闭集是 BOOL 语义,不是枚举。
--
-- 幂等性与形状:`ALTER TABLE ADD COLUMN` 在 SQLite 里没有 IF NOT EXISTS 形式,因此
-- 与 0019 / 0026 / 0027 同一手法 —— 依赖启动顺序「先 MigrateUp、后 NewStore」
-- (cmd/cyrene/wire.go):迁移先落地,随后自举前滚
-- (internal/runtime/memory/sensitive_schema.go 的 ensureSensitiveSchema)走到
-- "duplicate column name" 是预期路径。两份 DDL 必须**逐字同形**
-- (INTEGER NOT NULL DEFAULT 0),由 internal/runtime/memory/sensitive_migration_test.go
-- 钉住;列一律追加在末尾,尾序因此变成 `… retention_tier, temporal, sensitive`。
--
-- 本列**不建索引**:受限车道的过滤在内存里做(候选集本来就已过 status / 时效 /
-- scope,规模在千级),而「按 sensitive 取样巡检」是人工低频查询。将来真出现按它
-- 筛的读路径,迁移与自举 DDL 两条真源一起加。
ALTER TABLE memory_items ADD COLUMN sensitive INTEGER NOT NULL DEFAULT 0;
-- 回填历史数据(本迁移唯一的一次性数据动作)。
--
-- 与 0026 / 0027「刻意不回填」的差别(这里必须说清,否则下一次会有人照着抄错):
-- 那两条不回填,是因为它们的「正确值」只能靠**再判一次**得到(0026 是留存规则、
-- 0027 是拿正文反推时间性)—— 把一次全表判定藏在迁移里,等于在启动路径上静默跑
-- 第二现场。本列的回填判据是**人工确认的精确短语白名单**(塔裁决 2026-10-30 批准):
-- 结论已定、不需要模型、结果可用一条只读查询复核,因此回填就该与建列一起落地 ——
-- 否则「列建好了、历史行还是敏感=0」这件事本身就是一个持续存在的泄漏面。
--
-- 白名单口径(判据与理由见 internal/runtime/memory/sensitive.go 的 privateFacts):
-- `花溪` 住址(审计 §2.1:全库 user 消息只在主人私聊出现过 1 次)
-- `叶酱` 主人私称(审计 §2.2)
-- `早睡` 作息(审计 §7 P0-1 的回填面数据)
-- 刻意**不**用泛类目关键词(「住」「称呼」「睡」「家人」「健康」):会把「住手」
-- 「喜欢睡懒觉的猫」这类习惯一起标掉,而受限车道少一条合法习惯是主人看不见、
-- 也不会去改回来的缺口 —— 缺口的可见性远低于泄漏,故宁窄勿宽。
--
-- 幂等:`sensitive = 0` 前置条件使重跑只补标、不撤标、不改已标行(连跑两次的第二次
-- 影响 0 行)。同口径的可重跑入口是 internal/runtime/memory 的 BackfillSensitive
-- (dry-run + 报告,白名单扩充后用它补跑;本迁移只在 0028 首次应用时跑一次)。
--
-- 覆盖**全部** scope 与 status,不限于 `user`+`active`:merged / archived 的行会被
-- 「撤销软合并」拉回可见集(store_list.go 的 Get 刻意不过 status 闸),漏标它们等于
-- 留一条可被复活的泄漏面;persona 域虽然已经被 Loop 的受限过滤排除,标上也不改变
-- 行为(只让巡检口径一致)。不依赖「先去重」(审计 P2-2):标重复行没有额外代价,
-- 漏标一条才有。
--
-- 不改 `updated_at`:本列是数据层标记,不是内容/活跃度事件;改它会扰动「最新在前」
-- 的列表顺序与衰减基准(decay 由 updated_at 参与排序)。
UPDATE memory_items SET sensitive = 1
WHERE sensitive = 0
AND (instr(content, '花溪') > 0
OR instr(content, '叶酱') > 0
OR instr(content, '早睡') > 0);