Files
Cyrene_v2/internal/runtime/memory/sensitive_schema.go
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

47 lines
2.3 KiB
Go
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.
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)
}