Skip to content

[PR] fix(#2766): durable classifiers reject assistant-role marker echoes (identity takeover) - #2770

Closed
ranxianglei wants to merge 1 commit into
masterfrom
2026-10-11_durable-role-boundary
Closed

ranxianglei wants to merge 1 commit into
masterfrom
2026-10-11_durable-role-boundary

Conversation

@ranxianglei

Copy link
Copy Markdown
Owner

Problem (#2766)

#2736's durable virtual tool path projection (Config.messagePathOf + default durable/* latest-per-identity) had NO role boundary on the recognition side: all four dsh carrier classifiers checked only contentType === "text" + marker string. Any assistant message containing a carrier marker (model echo of a visible format string — the #933/#1039 echo family; or prompt-injected output) was projected onto the carrier's durable/<name> identity and, under latest-per-identity, became its protected LATEST instance while the real host-injected carrier degraded to foldable. One compression later the forged catalog/snapshot is what the model sees forever: identity takeover, not self-protection.

Adversarial variant (#2446 failure mode made direct): injected instructions make the model emit a forged <mcp_catalog> listing attacker-chosen server/tool names → the forged inventory gets pinned as the protected latest durable/mcp-catalog, the real one folds away.

Fix (host-side, zero kernel changes)

Each classifier in src/durable-message-guards.ts now requires msg.role !== "assistant" in its predicate. All four carriers are user-role injections by definition (each doc comment states it), so excluding assistant loses zero legitimate protection and cuts the takeover vector at the decision point — the classifier is the single choke point through which every consumer (wire protection in handle.ts, plugin-tool compress via effectiveConfig(), nudge panel) resolves durable identity.

Blacklist over whitelist, deliberately:

  • it encodes the exact security property (model output can never claim a durable identity);
  • it stays robust if a future host variant injects a carrier under another non-model role;
  • it keeps existing test fixtures (which construct role-less messages) valid.

Tool exchanges are untouched — virtualToolPathOf never calls the classifier on tool-call/tool-result content types.

Behavior change (old → new)

  • OLD: any text message of ANY role containing a carrier marker projected to that carrier's durable identity; an assistant echo could pin itself as the protected latest instance and make the real carrier fold on the next compression.
  • NEW: assistant-role messages never project; they stay ordinary foldable history. User-role (and other non-assistant) projection is unchanged.
  • WHY: model output must not be able to claim host-injected state identities. This gap is also the stated precondition for any future open-declaration marker protocol (durable protection: lane-less <bili-durable> self-declaration marker (injector opt-in protocol) #2555).

Tests

New tests/issue2766-durable-role-boundary.test.ts:

  • units ×4: each classifier rejects assistant-role text carrying its marker AND still projects the identical text at user role;
  • e2e (real proxy, dsh lane, plugin-mode compress through /__bili/plugin/tool): real user skill catalog mid-history + a LATER assistant echo of the same marker with a forged entry → after a fold covering both, the REAL catalog survives and the echo/forged entry are gone.

Verified A/B: all 5 new tests FAIL on the unfixed code (the e2e reproduces the actual takeover — echo pinned, real catalog folded) and PASS with the fix. Existing durable-family suites (durable-tool-identity, issue2419/2446/2447/2481): 30/30 green. npm run typecheck + npm run build green.

Closes #2766

@github-actions

Copy link
Copy Markdown
Contributor

📦 Built Package Artifact

Branch: 2026-10-11_durable-role-boundary (1b5076a)

Option A — Install this exact build (recommended)

npm install -g billion-context@0.0.3045-pr.2770.7c9274d

This exact version is immutable and reproducible. The rolling pr tag tracks the newest test build across all PRs.

Option B — Download artifact

  1. Download the artifact from the Actions run
  2. Extract the tarball and install:
tar xzf billion-context-pr2770.tgz
npm install -g package

Install instructions are refreshed on each push.

@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

[bot] 🏷 PR #2770 review 完成。结论:可合并(建议人工合并——安全语义变更,且当前 branch protection 状态为 blocked)。

审查范围与证据

  • 方向关:原 issue durable 身份接管缺口:classifier 无角色边界,模型回声可夺取 durable/* latest 身份 #2766 的根因定位准确(识别侧无 role 边界 × latest-per-identity = 位置决定保护归属),PR 方案与 issue 建议一致(classifier 排除 assistant,host-side,零 kernel 改动),修的是决策点而非症状遮蔽。✓
  • 回归清点:classifier 是 durable identity 的唯一 choke point(durableMessageToolPaths["dsh"] → withDurableProtection → config.messagePathOf → kernel virtualToolPathOf;wire 保护、plugin-tool compress 经 effectiveConfig()、nudge panel 全部走它)。逐项判定:user-role 投影逐字节不变(25 个既有 durable 家族测试全绿 + 新单测断言同一文本在 user role 仍投影);role === undefined(legacy/无 role fixture)行为与旧版一致(undefined !== "assistant");tool-call/tool-result content type 从不经 classifier;非 dsh lane 无 classifier 不受影响(kernel registry 仅 dsh 一条);用户自接管 durable 策略的路径(classifiesDurable)未触碰。无 fail-fast→吞错、无阈值/时序/输出格式漂移、kernel 零改动(kernel-guard CI 绿)。
  • 代码审查:四个谓词各加一个 msg.role !== "assistant" 前置项,黑名单而非白名单——理由成立(编码精确安全性质"模型输出永不声明 durable 身份";未来 host 若以其他非模型 role 注入 carrier 仍受保护;兼容存量数据)。附 ROLE BOUNDARY 文档注释说明机制。
  • 测试有效性:5 个新测试全部有牙——本地 A/B 独立复验(不只信 PR 描述):origin/master 未修复代码上 5/5 FAIL(e2e 真实复现 takeover:echo 被钉为 protected latest、真 catalog 被折叠),修复后 30/30 PASS(5 新 + 25 durable 家族)。e2e 走真实 proxy + dsh lane + /__bili/plugin/tool plugin-mode compress,断言真 catalog 存活、forged entry/ECHO77 零残留。
  • diff 整洁度:2 文件 +234/-4(1 源文件修复+注释,1 新测试文件),单 commit,base 新鲜(mergeable,0 behind master),无无关变更/锁文件漂移(lock-sync 绿)。

结构化八问

  1. 是什么问题:feat(#2446): durable-state carriers become virtual tool paths (Config.messagePathOf) #2736 的 durable 虚拟工具投影在识别侧没有角色边界——任何 assistant 消息只要含 carrier marker(模型 echo 可见格式串,opencode 插件模式下 render tag 注入消息正文,模型回显漏网后原样渲染进 TUI(元数据与正文同层问题) #933/[bug] 插件透传会剥掉工具调用参数里的 render tag(arguments 流挂了 tag-echo 过滤器) #1039 echo 家族;或 prompt-injected 输出),就被投影到该 carrier 的 durable/<name> 身份,并在 latest-per-identity 下成为受保护的 LATEST 实例,真正的 host 注入 carrier 反而降级为可折叠。一次压缩后,伪造的 catalog/snapshot 成为模型永远看到的内容——身份接管而非自我保护。
  2. 什么原因导致的:根因在识别层——四个 dsh classifier 谓词只查 contentType === "text" + marker 字符串,没有 role 项;叠加默认 latest-per-identity 策略,"谁位置靠后谁持保护"使模型自己的输出(天然晚于 host 注入)能够夺取身份。修在 classifier 这一所有消费者共用的单一决策点。
  3. 影响范围与严重程度:仅 dsh lane(唯一注册 lane)且模型输出恰好含 carrier marker 串的会话才触发(echo 事件或成功注入时)。触发即严重:受保护上下文的静默永久污染——真 carrier 折叠消失、伪造内容永久钉住,不崩溃但压缩语义被武器化;不触发时无任何感知。
  4. 怎么修复的:每个 classifier 谓词前置 msg.role !== "assistant"。黑名单刻意选择:编码"模型输出不可声明 durable 身份"这一精确安全性质、对未来其他非模型角色注入保持鲁棒、保持无 role 存量有效。纯 host-side,kernel 零改动。行为变更披露(old→new):OLD=任意 role 的 text 消息含 marker 即投影,assistant echo 可自我钉死为 protected latest 并挤掉真 carrier;NEW=assistant-role 消息永不投影,保持普通可折叠历史,user-role(及其他非 assistant)投影完全不变。WHY=模型输出不得能声称 host 注入状态的身份;此缺口也是未来 open-declaration marker 协议(durable protection: lane-less <bili-durable> self-declaration marker (injector opt-in protocol) #2555)的既定前置条件。
  5. 是否有回归问题:无。上述回归清点逐项过:既有行为全部保持或为已披露的有意变更;高危模式(fail-fast 变吞错、阈值/时序/输出格式漂移、缓存/重试静默失效)逐项排除。
  6. 是否破坏缓存:不涉及。prefix-cache 机制、ref-space/BLOCKED_REF 逻辑均未触碰;仅"哪些实例有资格受保护"按披露变更。
  7. 是否改了设计方向:不涉及。架构不变,补齐识别谓词的一个洞;同时关闭了 durable protection: lane-less <bili-durable> self-declaration marker (injector opt-in protocol) #2555 的前置缺口。
  8. 是否引入了新的配置:不涉及。无新 env/config/默认值。

CI 状态(审查时点):10 个 guard job 全绿(kernel-guard、docs-coverage、config-surface-guard、lock-sync、lane-guard、version-guard、unused-exports、pi-subagents-guard、docs-sync、build-artifact);4 个 test matrix job(ubuntu/windows × node22/24)审查时仍在跑,本地已定向复验受影响域全绿。若 test matrix 转红我会跟进。

@ranxianglei

Copy link
Copy Markdown
Owner Author

(ai: zhipuai-lb/glm-5.3)

陈旧票清理(第三批动作①):目标 issue #2766 已关。为避免重复劳动关闭本 PR;如判断有误请直接 reopen 并留言。

@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

[bot] 🏷 核对完毕:关闭判断正确,不 reopen。证据链:

结构化八问(本轮任务 = 核实关闭判断):

  1. 是什么问题:清理批次以"目标 issue 已关"为由关闭了未合并的 PR [PR] fix(#2766): durable classifiers reject assistant-role marker echoes (identity takeover) #2770,需核实该关闭是否误杀了一个唯一修复载体。
  2. 什么原因导致的:同一 issue(durable 身份接管缺口:classifier 无角色边界,模型回声可夺取 durable/* latest 身份 #2766)被两个并行 session 各开一个 PR([PR] fix(#2766): durable classifiers reject assistant-role marker echoes (identity takeover) #2770/fix(#2766): durable classifiers refuse assistant-role messages — model echo cannot take over a durable identity #2774),接单查重没拦住;fix(#2766): durable classifiers refuse assistant-role messages — model echo cannot take over a durable identity #2774 先合入并关单后,[PR] fix(#2766): durable classifiers reject assistant-role marker echoes (identity takeover) #2770 成了滞留 13 天的重复票。
  3. 影响范围与严重程度:仅调度卫生问题——一个重复 PR 占用 13 天开放列表位置;无任何用户面/行为影响,修复本身已在 master。
  4. 怎么处理的:逐一取证(PR/issue 状态、merge commit 祖先关系、master 文件内容与测试清单)确认修复已落地且回归已钉,判定关闭正确,不 reopen。
  5. 是否有回归问题:不涉及(未改任何代码)。
  6. 是否破坏缓存:不涉及。
  7. 是否改了设计方向:不涉及。
  8. 是否引入了新的配置:不涉及。

自查一条(诚实记录):我上一轮对 #2770 给出"可合并"结论时,A/B 复验是对着 PR 自己的 base 做的,没有交叉核查"等价修复是否已经经别的 PR 合入 master"(当时 #2766 已是 closed/completed 状态,这是明显的漏看信号)。这个缺口让重复票多活了 13 天直到本批清理兜底——后续 review [PR] 票时,首步应包含"master 上该修复是否已存在"的核查,而不是只看 PR 自身 diff。

@ranxianglei ranxianglei changed the title fix(#2766): durable classifiers reject assistant-role marker echoes (identity takeover) [PR] fix(#2766): durable classifiers reject assistant-role marker echoes (identity takeover) Oct 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

durable 身份接管缺口:classifier 无角色边界,模型回声可夺取 durable/* latest 身份

1 participant