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 等价)。
47 lines
2.3 KiB
Go
47 lines
2.3 KiB
Go
package memory
|
||
|
||
import (
|
||
"context"
|
||
"database/sql"
|
||
"fmt"
|
||
"strings"
|
||
)
|
||
|
||
// 本文件是「敏感标记」轴(迁移 0028)的 schema 前滚:给**既有**的 memory_items
|
||
// 补 `sensitive` 列。手法与 retention_schema.go / temporal_schema.go 逐字同族,
|
||
// 原因也一样:`createMemoryItemsTable` 是 `CREATE TABLE IF NOT EXISTS`,只对
|
||
// **新库**生效,既有部署的表不会自己长出新列;而记忆存储必须自包含、能在任意
|
||
// *sql.DB 上使用,所以这条前滚不能只靠 `internal/db/migrations`。
|
||
//
|
||
// 🔴 与迁移的分工(两份真源,必须同形):权威机制是
|
||
// `internal/db/migrations/0028_memory_sensitive`;这里是「任意 *sql.DB」的前滚。
|
||
// 启动顺序是「先 MigrateUp、后 NewStore」(cmd/cyrene/wire.go),因此生产库里
|
||
// 迁移先落地、这里走到 `duplicate column name` 是**预期路径**。
|
||
//
|
||
// 默认值 0(= 非敏感):既有行因此**行为逐字不变**(受限车道照旧看得到它们),
|
||
// 本前滚刻意**不做**任何回填判定 —— 回填是独立的一次性数据动作
|
||
// (`BackfillSensitive`,sensitive_backfill.go):把「结构前滚」与「给历史数据
|
||
// 打标」分成两步,才能使两者的验收互不牵连(前滚永远幂等且零副作用;回填可
|
||
// dry-run、可重跑、可只跑一次)。
|
||
//
|
||
// 本列**不建索引**:受限车道的过滤在内存里做(候选集本来就已 `status` + 时效 +
|
||
// scope 过滤过,行数在千级),而「按 sensitive 取样巡检」是人工低频查询。将来
|
||
// 若真出现按它筛的读路径,两条真源(迁移与本文件)一起加。
|
||
|
||
// sensitiveColumn 是被测列名(迁移与自举 DDL 里的字面量)。
|
||
const sensitiveColumn = "sensitive"
|
||
|
||
// ensureSensitiveSchema 幂等地给 memory_items 补上敏感标记列(列缺失时 ALTER,
|
||
// 已存在时忽略 SQLite 的 "duplicate column name" —— 那是预期路径,不是错误)。
|
||
func ensureSensitiveSchema(ctx context.Context, db *sql.DB) error {
|
||
_, err := db.ExecContext(ctx, `ALTER TABLE memory_items
|
||
ADD COLUMN sensitive INTEGER NOT NULL DEFAULT 0`)
|
||
if err == nil {
|
||
return nil
|
||
}
|
||
if strings.Contains(err.Error(), "duplicate column name") {
|
||
return nil
|
||
}
|
||
return fmt.Errorf("memory: roll forward sensitive column: %w", err)
|
||
}
|