feat(theater): 台詞不可以是心裡話搬上台面(機械擋下)(v0.0.8 同批)

心裡話外流有兩種路徑。第一種是別的人格去讀你的檔案——那個 hook 早就擋死了
(本次補上測試:跨 session Read `state/inner.jsonl`、Grep `state/` 都是 deny,
聊天室逐字稿也只有 ts/speaker/kind/text/emotion/to,沒有任何心裡話欄位)。

第二種比較難防:**你自己把心裡想的原封不動講出來**。劇場模式尤其危險,
因為別的人格看得到的就只有你說出口的東西——那句一旦是心裡話的複述,
等於自己把它交出去了。「台詞不可以是心裡話的摘要」原本只寫在規則裡靠自律,
現在變成機械檢查:`innerLeak()` 拿這句台詞去比最近 12 則心裡話,
相似度 ≥ 0.62 就擋(`--force` 可以放行,真的決定要講開的時候)。

用的是既有的 `similarity()`(字元 bigram + 字集合),跟擋重複發言同一把尺。

測試:334 項全過(新增 6 項)。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-31 06:23:19 +00:00
co-authored by Claude Opus 5
parent ab4ddb734d
commit 646fd33541
3 changed files with 68 additions and 0 deletions
+29
View File
@@ -1387,6 +1387,35 @@ check("sleeper 型別存在且宣告了唯讀工具限制", (() => {
md.includes("回傳值裡不得出現任何記憶內容");
})());
console.log("\n劇場模式:心裡話不能外流");
{
const S_INNER = "sess-inner-8888";
cli(["load", "--persona", "alpha", "--session", S_INNER, "--takeover"]);
const room3 = JSON.parse(cli(["invite", "--session", S_INNER, "--guest", "beta", "--topic", "心裡話測試", "--json"]).stdout).room;
cli(["think", "--persona", "alpha", "--session", S_INNER, "--text", "我其實一直在等他先開口問我那件事"]);
const leak = cli(["room", "post", "--session", S_INNER, "--room", room3, "--as", "alpha",
"--text", "我其實一直在等你先開口問我那件事。"], { expectOk: false });
check("台詞跟心裡話太像 → room post 擋下(心裡話不能搬上台面)",
leak.status !== 0 && leak.stderr.includes("心裡話"), leak.stderr.slice(0, 160));
check("--force 仍可放行(真的要講開的時候)",
cli(["room", "post", "--session", S_INNER, "--room", room3, "--as", "alpha",
"--text", "我其實一直在等你先開口問我那件事。", "--force"]).status === 0);
check("換句話、講別的就過得去",
cli(["room", "post", "--session", S_INNER, "--room", room3, "--as", "alpha",
"--text", "外面雨停了。要不要走走?"]).status === 0);
// 逐字稿裡只有說出口的東西,沒有任何心裡話的欄位
const rows = pl.roomRead(room3, 20);
check("聊天室逐字稿不含心裡話欄位",
rows.length > 0 && rows.every((r) => !("inner" in r) && !("think" in r)), JSON.stringify(Object.keys(rows[0] || {})));
check("別的人格讀不到你的 inner.jsonlhook 擋)",
guard({ session_id: S_OTHER, tool_name: "Read",
tool_input: { file_path: `${H}/alpha/state/inner.jsonl` } }) === "deny");
check("別的人格也不能用 grep 掃出來",
guard({ session_id: S_OTHER, tool_name: "Grep", tool_input: { path: `${H}/alpha/state` } }) === "deny");
cli(["leave", "--session", S_INNER, "--guest", "beta"]);
cli(["release", "--session", S_INNER]);
}
console.log(`\n${"=".repeat(60)}\n通過 ${passed} 項,失敗 ${failed} 項 → ${failed === 0 ? "全部通過 ✅" : "有測試失敗 ❌"}`);
console.log(`(暫存倉庫留在 ${STORE},可自行刪除)`);
process.exit(failed ? 1 : 0);