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 等价)。
57 lines
2.8 KiB
Go
57 lines
2.8 KiB
Go
package memory
|
||
|
||
// schema DDL。internal/db 中的迁移框架是领域表的权威
|
||
// 机制,但这里刻意不用它:db 包的迁移测试断言精确的
|
||
// 迁移数量,而记忆存储必须自包含、
|
||
// 可在任意 *sql.DB 上使用。表与索引因此
|
||
// 由 NewStore 幂等创建。
|
||
const createMemoryItemsTable = `
|
||
CREATE TABLE IF NOT EXISTS memory_items (
|
||
id TEXT PRIMARY KEY,
|
||
persona_id TEXT NOT NULL,
|
||
role_id TEXT NOT NULL DEFAULT '',
|
||
project_id TEXT NOT NULL DEFAULT '',
|
||
scope TEXT NOT NULL,
|
||
kind TEXT NOT NULL,
|
||
content TEXT NOT NULL,
|
||
tags TEXT NOT NULL DEFAULT '[]',
|
||
confidence REAL NOT NULL DEFAULT 1.0,
|
||
source_conversation TEXT NOT NULL DEFAULT '',
|
||
created_at TEXT NOT NULL,
|
||
updated_at TEXT NOT NULL,
|
||
decay_weight REAL NOT NULL DEFAULT 1.0,
|
||
status TEXT NOT NULL DEFAULT 'active',
|
||
merged_into TEXT NOT NULL DEFAULT '',
|
||
merged_from TEXT NOT NULL DEFAULT '[]',
|
||
merge_count INTEGER NOT NULL DEFAULT 1,
|
||
occurred_at TEXT NOT NULL DEFAULT '',
|
||
valid_to TEXT NOT NULL DEFAULT '',
|
||
expires_at TEXT NOT NULL DEFAULT '',
|
||
retention_tier TEXT NOT NULL DEFAULT '',
|
||
temporal TEXT NOT NULL DEFAULT '',
|
||
sensitive INTEGER NOT NULL DEFAULT 0
|
||
)`
|
||
|
||
const createMemoryItemsPersonaIndex = `
|
||
CREATE INDEX IF NOT EXISTS idx_memory_items_persona_id
|
||
ON memory_items (persona_id)`
|
||
|
||
const createMemoryItemsPersonaCreatedIndex = `
|
||
CREATE INDEX IF NOT EXISTS idx_memory_items_persona_id_created_at
|
||
ON memory_items (persona_id, created_at)`
|
||
|
||
// createMemoryItemsRetentionTierIndex 是「留多久 / 干什么用」轴的读侧索引(迁移 0026)。
|
||
// 三条列表读路径的可选档位过滤(`retention_tier IN (...)`)与「只看未分型的行」
|
||
// (回填分型的入口)都吃它。列**必须**排在 CREATE TABLE 的最后:迁移的
|
||
// `ALTER TABLE ADD COLUMN` 也把新列追加在末尾,两份 DDL 才逐列同形
|
||
// (retention_migration_test.go 钉住这条同形,少一列就是「新库有、老库无」的漂移)。
|
||
//
|
||
// ⚠️ 0027 之后 `retention_tier` **不再是最后一列**:`temporal`(迁移 0027)追加在
|
||
// 它之后,0028 又把 `sensitive` 追加在 `temporal` 之后。两条路径的尾序都是
|
||
// `… expires_at, retention_tier, temporal, sensitive`。新列一律追加在末尾这条口径
|
||
// 不变,只是「末尾」会随迁移推进(那条同形断言比对**列集合**,另外对尾序做一条
|
||
// 显式断言)。
|
||
const createMemoryItemsRetentionTierIndex = `
|
||
CREATE INDEX IF NOT EXISTS idx_memory_items_retention_tier
|
||
ON memory_items (retention_tier)`
|