Skip to content

[Bug] strictScope 硬过滤被 entity: / attr: 前缀检索绕过:显式他者 scope 的记忆仍可读出 #357

Description

@modusensus

现象

memory_search 带 entity: / attr: 前缀时会绕过 strictScope 硬过滤:显式标注为他人
scope 的记忆,普通检索查不到,换这两个前缀就能原样读出来——而且是满分返回(连 A2 的
×0.5 降权都没有)。

暴露面:scopeEnabled + strictScope + entitySearchEnabled 三者同开(默认全关)。也就是
「开了 scope 隔离的人,隔离在这条路上是漏的」。而 entity: 正是 #24 图谱线在推的语法,这条
路的使用面只会越来越大。

最小复现(实跑输出)

const store = createStore(":memory:");
const service = createService({ store, mirror: null,
  config: { scopeEnabled: true, strictScope: true, entitySearchEnabled: true } });

const row = store.save({ type: "preference", title: "别人的机密预算", content: "机密预算内容",
  agent_scope: "other", agent_scope_source: "explicit" });
const entity = store.createEntity({ name: "阿尔托", type: "person" });
store.saveAttr({ entity_id: entity.id, attr_key: "国籍", attr_value: "芬兰", memory_id: row.id });

const me = { agent_scope: "me", workspace_scope: null };
await service.searchMemories("机密预算", { mode: "keyword", scope: me }); // 0 条 ✅
await service.searchMemories("entity:阿尔托", { scope: me });              // 1 条 ❌
await service.searchMemories("attr:国籍=芬兰", { scope: me });             // 1 条 ❌

实测:

① 普通关键词检索(应当滤掉)      -> 命中 0 条 []
② entity:阿尔托 路由              -> 命中 1 条 [ '别人的机密预算' ]
③ attr:国籍=芬兰 路由             -> 命中 1 条 [ '别人的机密预算' ]
   对照(原主人自己搜)            -> 命中 1 条 [ '别人的机密预算' ]

对照组是为了排除「本来就搜不到」造成的假象。

根因

src/service.js 的 searchMemories 有两处早返回:

  • src/service.js:824-833:entity: / attr: 前缀在函数入口直接
    return searchByEntity(...) / searchByAttr(...);
  • src/service.js:899-901:strictScope 过滤写在函数后半段的融合池上
    (merged.filter(isVisibleInScope))——早返回的路根本走不到那一步。

而 searchByEntity(src/service.js:371)与 searchByAttr(src/service.js:397)的签名
只解构 { topK },options 里带的 scope 被丢掉;所以即使把过滤写进这两个函数,也得先把
scope 传进去。

顺带两处同源现象:

  1. A2 软加权一并被绕过:SCOPE_FOREIGN_PENALTY(src/service.js:74)只在融合 / 注入路
    径(scopeMultiplier)生效,这两条早返回路既不硬过滤也不降权。
  2. 越权命中会反馈到热度 / 边权:两个函数都无条件调用 touchRecalled(hits)
    (src/service.js:384 / 404)——heat 或被动确认开启时,一次越权检索会给本不该看到的行
    刷回温时钟、bump 关联边。

为什么现有测试没抓到

test/scope-strict.test.js 覆盖了 11 个入口(可见性谓词、普通 searchMemories、
store.list/count、injectCandidates、memory_update/delete、memory_get、memory_list),
唯独漏了 entity: / attr: 这两条前缀路——而它们恰好是仅有的「在过滤之前就 return」的
分支。

修法建议

把 scope 闸抽成单一实现,三处共用(普通路 + 两个早返回),避免「过滤点写在哪」再次漂移
——与 #349 把阈值口径收进 activeStoreSize() 是同一个理由:

const gateByScope = (rows, scope) =>
  config?.strictScope === true && scope ? rows.filter((m) => isVisibleInScope(m, scope)) : rows;

strictScope 关时不改变任何行为(与现状逐字节一致),所以硬过滤可以先落;闸门的位置还要在
touchRecalled 之前(否则出局的行照样被回温 / bump,见上面第 2 条)。A2 软加权要不要一并
补到这两条路(他 scope 行降权但可见),可以按 #339 的口径单独定。

测试加两条,形状对齐现有的 searchMemories: explicit foreign filtered...:entity: 与
attr: 各一条,断言显式他者出局、auto / 存量他者保留。

关联

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bug功能异常、崩溃、数据错误(agent 诊断 → 人工确认)

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions