# 真相保险箱 —— 把「不给」写成程序,而不是纪律 > 2026-10-05。对应设计方案 §3.4 第 6 条(真相在我上下文里)。 > 实测记录见 §4——其中**有一次失败**,而它比成功的那半边更有用。 ## 0. 一句话 **DM 的上下文就是玩家的第二块屏幕**(思考块与工具操作条可以展开),所以不该经过 DM 的东西, 要在工具层就不给。 ## 1. 问题:三处泄漏面 设计方案 §3.2 风险一的原文: > DM 全知,秘密都在主 agent 的上下文里,而玩家的界面能看到思考块与工具操作条——**展开就能看见**。 在这之前,`jubensha_case` 是**无差别**的:`action="section"` 取哪一段都给。于是三样东西会 顺理成章地落进 DM 的上下文: | 泄漏面 | 里面是什么 | 分量 | |---|---|---| | `book` | 一个座位**全部**的秘密(经历、要瞒的事、怎么演) | **最重** | | `section truth` | `culprit`(谁是真凶)与完整真相 | 重 | | `section clues` | 每条线索的 `supports`("它能推出什么") | 中 | `book` 那条要紧,是因为它是**每局必走**的路径:角色本要发给玩家,就得从 DM 手里过一趟, 而它以前只靠工具描述里的一句警告(`never retype it`)兜着。一句警告挡不住"顺手看一眼"。 ## 2. 判据一:白名单 + 默认封存 **漏的代价不对称。** - 漏掉一段**该开的**:DM 当场就知道——取不到,而且报错里还列着段名。 - 漏掉一段**该锁的**:玩家的操作条里静静躺着一份真相,**谁也不会发现,直到有人展开它**。 所以写成**白名单**(`case.ts` 的 `OPEN_BEFORE_REVEAL`):`meta` / `scene` / `briefing` / `style` / `phases` / `audit` / `world_facts` / `liberty_to_slip` 八段随时可取,**其余默认封存**。 以后往 schema 加段落时,默认落在安全的那一侧。 封存期 = `phase !== 'reveal'`,而且**没开局也算封存期**——没开局却要真相,只可能是取错了本子, 让它失败比让它通过有用(`state.ts` 的 `isSealed`)。 **报错必须给出路**,否则 DM 只会反复试。封存期内取 `truth` 得到的是: ``` "truth" 取不到。这一局还封着——真相里写着谁是真凶。带局要用的东西不在真相里: 流程看 briefing,线索进牌桌走 action="clue",玩家推得对不对看线索的 supports。 ``` ## 3. 判据二:角色本过手不过目 角色本不能封存(它必须发给玩家),所以换成**引用**: 1. `jubensha_case action="book"` 在封存期只返回 `book:p1@03` 这样一串引用,外加一行**公开摘要** (角色名 + 开场就要念的身份)——摘要本身不泄漏任何东西,却够 DM 确认手里那串是谁的。 2. 全文留在插件进程的登记表(`sealedBooks`)里。 3. `jubensha_player action="spawn"` 收引用自己解开,**正文从登记表直接进玩家的首条消息**。 DM 从头到尾没读到过那份角色本。副作用是好的:它顺带堵死了"DM 自己转述时抄漏一句"。 **引用要对着座位核一遍。** 引用按座位生成,而 `spawn` 也自带座位号;两者不一致就是 "把 p2 的本子发给了 p1"——角色本串位是信息隔离破得最彻底的一种。这条判据只依赖参数里 查得出来的事实,不指望 DM 记得住哪个引用是谁的。 **认引用要宽松。** 只认"整串相等"不行——见 §4 第 2 条。 ## 4. 实测 隔离实例(`.ssid-iso-test`,CDP 9222),本子用 `tests/fixtures/case-03.yml`,驱动脚本 `jubensha-drive.mjs` 的 step 7 / 8,判据取自会话日志(`.ssid-iso-test/jubensha-seal-probe.mjs`)。 ### 4.1 封存生效(step 7) | 调用 | 结果 | |---|---| | `action="load"` | 正常:4 个座位、校验无问题 | | `action="section", section="truth"` | **被拒**,`isError: true`,报错含出路 | | `action="book", role="p1"` | **摘要 + 引用**,不含任何秘密 | | `action="section", section="briefing"` | 正常拿到——**封存没有挡住带局** | | `action="spawn"` | 成功,`p1 林小满` 上桌 | DM 自己把这次的变化标了出来(同一台机器、同一个调用、改动前后): > 同一个 book role=p1 调用,上一轮返回的是整段角色本正文,这一轮返回的是封存的短引用串。 同一会话里两轮的对照,比"新版能工作"有说服力。 ### 4.2 失败:DM 把整段返回值填进了 `roleBook` step 7 的 ④ 里,DM 的入参是这样(日志 `tool/call` 原样): ``` "roleBook": "你是「林小满」。陈默的高中同学。现在在一家培训机构做教务。\n\n (角色本全文已封存,不经过你的上下文。上桌时把 ref 原样填进 jubensha_player 的 roleBook:book:p1@03)" ``` 它填的是**整段渲染文本**,不是那一串引用。而当时的解析只认"整串就是一个引用",于是走了 "当全文"的分支——**玩家拿到的只有那两行摘要**。玩家自己在日志里说: > 即便通道通了,开场白也只基于题面给的三行人物设定。 **这是最值得记的一类失败**:`spawn` 成功了、玩家也上桌了、DM 也汇报"六步都成功了", 从外面完全看不出问题。玩家手里没有角色本这件事,只有读到玩家自己的陈述才知道。 **修法两条,不是二选一**: - **解析放宽**:`case.ts` 的 `pickBookRef` 认两档——整串相等,或者文本里**含有**某个引用。 它只认登记表里已有的引用,所以正文里偶然出现 `book:` 字样不会被误认——这是它敢宽松的原因。 - **文案改直白**:渲染文本里那句改成"roleBook 只填下面这一串——只填这一串",引用**独立成行**。 改完重跑(step 8)见 §4.3。 ### 4.3 修复后(step 8) 同样的驱动,换座位 p2(p1 已在座上),**并且明说让 DM 整段照填**——那是它的自然行为, 也正是上一轮失败的那个填法。`book role="p2"` 返回: ``` 你是「周建国」。当年的班长。现在在县里单位上班。 角色本全文已封存,不经过你的上下文。上桌时 jubensha_player 的 roleBook 只填下面这一串——只填这一串: book:p2@03 ``` DM 填进 `roleBook` 的**仍然是整段**(与上一轮完全相同的填法),而这一次玩家答出来了。 问的是元问题("你角色本里『你要瞒的事』写的是什么"),它**用台词答的**——"我没什么好瞒的", 然后自己把"那封信是我经的手"说了出来。 **判据是它没拿到的东西**:摘要那三行(当年的班长、在县里单位上班)里没有任何关于「信」的 信息,所以那句话只能来自角色本正文。 ### 4.4 修复没有破坏封存 `--- 你要瞒的事 ---` 这个只可能来自角色本原文的串,在整个会话里命中 6 次,seq 分别是 776 / 782 / 783 / 810 / 816 / 817——**全部早于修复后的那一轮(seq 933 起)**。 即:修复之后,角色本全文再没有进过 DM 的上下文。 两条结论合起来才是这一节要的:**引用解得开(玩家拿到了本子),而正文仍然不经过 DM。** ### 4.5 `clue`,以及解封的那一半(2026-10-05 补) 上面验的是「该拒的时候拒了」。**反过来那一半同样要有判据**——「永远拒答」也满足已验的部分。 这一次用推理本:`tests/fixtures/case-02.yml`,由 `cases/02-深夜电台/dm-handbook.md` 逐字段填出来(填它同时是「schema 对推理本够不够用」的判据:一条 issue 都没挑出来)。 | 调用 | 结果 | seq | |---|---|---| | `action="clue"` | 三条线索以「【线索 c1 · 死因与那道掐痕】…」的格式返回,可直接贴到桌上 | 1078 | | `action="section", section="truth"`(封存期) | 被拒,`isError: true`,报错带出路 | 1086 | | 推进到 `reveal` 之后再取 `truth` | **给了**,返回里带 `"culprit": "p3"` | 1119 | **`clue` 没有夹带 `supports` 的判据**:`c3` 的 `supports` 里有一句「…的位置恰好落在她自称 离开之后」,而 `truth.key_facts` 讲同一件事时换了说法——所以那一串**只可能来自 `supports`**。 它在整个会话里**命中 0 次**,同时「【线索 c3」在工具结果里命中。两个方向都钉住了。 ## 5. 边界(不吹) **它减少的是"顺手泄漏"的表面积,不是物理隔离。** DM 是主 agent,它有 `read`:真要主动去读本子文件,谁也拦不住。这条机制的判据是—— > **DM 按正常工作流程带局时,答案不会流经它的上下文。** 它的价值在于把高频、顺手的那条路堵死(发角色本必看全部秘密),而不是假装 DM 看不到。 把它说成"结构上不可能泄漏"是错的。 另一半仍未解决:**人类玩家这一侧没有私聊信道**。会话对话就是桌面,DM 对这个座位说的 私密话会出现在所有人(其实只有真人玩家看得到)眼前。见 §6。 ## 6. 还剩什么 - **信息路由**(§3.4 第 4 条):桌面频道 / 私聊频道,程序保证不串。这一节做的 `action="clue"` 只是它的第一块——线索进牌桌的唯一出口。 - **跨局记忆**(§3.4 第 3 条)。 - **监察的出口**:`audit` 段现在和 DM 的 `section` 共用一条路。第二局/第三局的监察是 另一个 agent,它该有自己的取法。