Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions dsh-mneme/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## [Unreleased]

## 🆕 新增

- **写入边界的密钥 / PII 判据(`sensitiveScanEnabled`,默认关)**:写入准入(#254 第 1 级)此前只跑空白 / 噪声两类判据,密钥 / PII 那一档按设计留了注入点而没实现。现在补上 `src/sensitive-scan.js`——纯确定性、零 LLM,先认形状再认关键词(赋值型规则带占位符守卫,所以「把 API key 放进环境变量」这类讨论句不报)。命中落审计 `metadata.deny.reason='sensitive'` + `kind`(密钥 / PII 分档,便于先看分布再决定放行策略),审计位与 #332 定的形状一致。开关分层:本键只决定「这类判据参不参与」,命中之后是仅告警还是真拦仍由 `writeAdmission.enforce` 决定(默认仅告警、拦截 opt-in)。回归样本集(10 条密钥 + 4 条 PII 正样本、12 条负样本)原样跑真判据:正样本不漏、负样本不误杀。

## [0.8.12] - 2026-10-01

## 🐛 修复
Expand Down
2 changes: 2 additions & 0 deletions dsh-mneme/lib/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,8 @@ export function createApi(ctx, service, settings, commands, embedder, semantic =
"llmAudit.enabled": ["llmAudit", "enabled"],
"writeAdmission.enabled": ["writeAdmission", "enabled"],
"writeAdmission.enforce": ["writeAdmission", "enforce"]
// sensitiveScanEnabled(#164 A2)是顶层扁平键,不需要进这张表——走 configFlagValue
// 的默认分支即可。
};
function configFlagValue(key) {
const path = NESTED_FLAG_PATHS[key];
Expand Down
13 changes: 12 additions & 1 deletion dsh-mneme/lib/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -493,6 +493,11 @@ window.__ModuleLoader__.load({
"memory.features.writeAdmission.enabled.hint": "写入前判空白/噪声,命中留审计(不拦)",
"memory.features.writeAdmission.enforce": "写入准入拦截",
"memory.features.writeAdmission.enforce.hint": "真拦下命中的写入;关闭时只留审计不拦",
// #164 A2:密钥/PII 判据自己的闸。文案要写清它管的是哪一批判据(与上面的
// 空白/噪声分开)、它跑在「写入准入判定」里面(那一项关着就不跑),以及
// 「开着也不拦」——拦是上面那项的事。
"memory.features.sensitiveScanEnabled": "密钥/PII 扫描",
"memory.features.sensitiveScanEnabled.hint": "写入前扫密钥与个人信息;需先打开写入准入判定才会执行,命中只留审计,要拦再打开写入准入拦截",
"memory.features.bm25SearchEnabled": "BM25 关键词检索",
"memory.features.bm25SearchEnabled.hint": "传统关键词打分检索,与向量召回互补",
"memory.features.conflictFreezeEnabled": "冲突冻结",
Expand Down Expand Up @@ -925,6 +930,12 @@ window.__ModuleLoader__.load({
"memory.features.writeAdmission.enabled.hint": "Flag blank / noise writes before storing, with an audit row (never blocks)",
"memory.features.writeAdmission.enforce": "Write admission enforcement",
"memory.features.writeAdmission.enforce.hint": "Actually reject flagged writes; off keeps the audit row only",
// #164 A2: its own gate for the secret / PII rules (separate from the
// blank/noise pair above), and it runs inside the write admission checks
// above, so with those off it never runs. Hits are audited only;
// enforcement stays with the key above.
"memory.features.sensitiveScanEnabled": "Secret / PII scan",
"memory.features.sensitiveScanEnabled.hint": "Scan writes for credentials and personal data; needs write admission checks to run. Hits are audited only; enforcement needs the enforcement toggle too",
"memory.features.bm25SearchEnabled": "BM25 keyword search",
"memory.features.bm25SearchEnabled.hint": "Classic keyword scoring, complementary to vector recall",
"memory.features.conflictFreezeEnabled": "Conflict freezing",
Expand Down Expand Up @@ -2122,7 +2133,7 @@ window.__ModuleLoader__.load({
// test/inject-parent-gate.test.js 钉住。
const FEATURE_CHILDREN = { autoInject: ["injectGuidanceEnabled", "continuityRescueEnabled"] };
const FEATURE_GROUPS = [
{ key: "group.core", items: ["autoInject", "autoSummarize", "hotMemoryEnabled", "injectTimePrefix", "memoryQualityFilter.enabled", "llmAudit.enabled", "writeAdmission.enabled", "writeAdmission.enforce"] },
{ key: "group.core", items: ["autoInject", "autoSummarize", "hotMemoryEnabled", "injectTimePrefix", "memoryQualityFilter.enabled", "llmAudit.enabled", "writeAdmission.enabled", "writeAdmission.enforce", "sensitiveScanEnabled"] },
{ key: "group.enhance", items: ["entityExtractionEnabled", "codingRetrospect", "rerankEnabled", "resilientModelDownload", "searchSemanticDedup", "bm25SearchEnabled", "heatEnabled", "documentMemoryEnabled"] },
{ key: "group.dream", items: ["autoDream", "sleepModeEnabled"] },
// v0.8.0 A4(issue #17):作用域隔离组——标注总开关 + 严格硬过滤。
Expand Down
19 changes: 19 additions & 0 deletions dsh-mneme/lib/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -651,6 +651,25 @@ export const Config = z.object({
enforce: z.boolean().default(false)
}).default({}),

// --- #164 A2: secret / PII scan at the write boundary ----------------------
// 写入边界的密钥 / PII 判据(src/sensitive-scan.js)自身的闸。与 writeAdmission
// 的 enabled 分开是有意的:那一个管 #254 第 1 级的空白 / 噪声判据,本键管 A2 这
// 一类判据,两批的误杀面差一个量级(空白 / 噪声没有解释空间,邮箱 / 手机号有),
// 绑在同一个开关上就没法单独观察 A2 的命中分布。
//
// 分层必须写清:判据的唯一调用点在 #254 第 1 级的闸门里(write-admission.js 的
// firstLevelHit),闸门不走第 1 级就没人来调这个扫描器。所以本键是「闸门内这一批
// 判据参不参与」,不是一条能独立跑的链路——单开本键就是零行为变化:
// writeAdmission.enabled 关 → 第 1 级整个不跑,本键开也没用
// enabled 开 + 本键关 → 只跑空白 / 噪声那一批
// enabled 开 + 本键开 + enforce 关 → 命中留审计,写入照常(观察档)
// enabled 开 + 本键开 + enforce 开 → 命中即拒绝
// 默认关 = 只计量那一阶段的行为逐字节保留(#332 合并时 sensitiveScan 就是 null)。
// #164 的「默认仅告警、拦截 opt-in」落在 enforce 上:命中落一条审计
// (metadata.deny.kind)但照常写入,真要拦得 enabled 与 enforce 同时开。
// 也走 feature_flags(FEATURE_FLAG_BOOLEANS 白名单),面板可启停=线上回滚开关。
sensitiveScanEnabled: z.boolean().default(false),

// --- recall evaluation: test-result storage (v0.4.6, 方案 B) --------------
// Separate retrieval evaluation snapshots from the production recall audit.
// When false (default) evaluateRetrieval still computes precision/recall/mrr
Expand Down
18 changes: 13 additions & 5 deletions dsh-mneme/lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { resolveDocumentDir } from "./document.js";
import { createService } from "./service.js";
// #254 写入准入(第一阶段只计量,不拦截):见 src/write-admission.js 的文件头。
import { createWriteAdmission } from "./write-admission.js";
// #164 A2:写入边界的密钥 / PII 判据,注入给上面的写入准入。
import { createSensitiveScan } from "./sensitive-scan.js";
import { createTools } from "./tools.js";
import { createInjector } from "./inject.js";
import { createContinuityRescue } from "./continuity.js";
Expand Down Expand Up @@ -278,11 +280,17 @@ export const apply = (ctx, config) => {
// 关掉,在无保留期的表里按写入频次增长是不能接受的)。
//
// sensitiveScan 是密钥 / PII 那一类判据的注入点。按 #254 验收第 4 条它是 #164 A2
// 的判据来源(A2 记在维护者排期里),所以本批不实现它,只把接口形状定在这里——
// 接上时只改这一行:
// createWriteAdmission({ ..., sensitiveScan: createSensitiveScan({ config: cfg }) })
// 缺省 null 时第 1 级只跑空白 / 噪声两类判据,其余一切照旧。
const writeAdmission = createWriteAdmission({ store, config: cfg, logger: ctx.logger });
// 的判据来源,现在由 src/sensitive-scan.js 实现(维护者 09-28 把 A2 的认领转给
// 本侧)。这里只做接线:判据开不开由它自己的键 sensitiveScanEnabled 决定,工厂在
// 关时返回 null,闸门的行为就与 #332 合并时逐字段一致(那一版根本没有这个函数);
// 命中之后是仅告警还是真拦,仍是 writeAdmission.enforce 的事(#164 口径:
// 默认仅告警、拦截 opt-in),判据不碰决策。
const writeAdmission = createWriteAdmission({
store,
config: cfg,
logger: ctx.logger,
sensitiveScan: createSensitiveScan({ config: cfg })
});
const service = createService({ store, mirror, config: cfg, logger: ctx.logger, documentIndex, writeAdmission });

// F-NEW-03: if the mirror sync failed last run (persisted dirty state), retry
Expand Down
Loading
Loading