Files
persona/hooks/subagent_stop.mjs
jiantw83 b1e77b01a1 fix(security): sleeper 旗標不再是任意人格的萬用鑰匙(S1)
問題:`requireOwner()` 看到 sleeper 旗標就直接 return,把驗證整個外包給
PreToolUse hook;而 hook 認得出這支 CLI 靠的是檔名正則。把 scripts/ 複製出去、
CLI 改個名字,hook 全程回 pass,任何程序都能用那個旗標對**任意人格**
remember/recall/emotion --apply——等於完整讀寫權。

修法:CLI 自己驗,證據取自 hook 唯一寫得下、程序偽造不了的東西——
session 檔(`.runtime/sessions/<id>.json`)裡的 sleeper pin。pin 由 hook 依
`event.agent_type` 寫入,沒經過 hook 的程序拿不到。

- `pinAgent()` 多記角色(`{ persona, role, pinned_at }`),`pinOf()` 相容舊的純字串格式;
  舊格式沒有角色,一律不算 sleeper 授權,hook 下次 first-touch 時會補上。
- 新增 `sleeperPins(sessionId, slug)`:本 session 中 pin 在該人格上的 sleeper。
- `requireSleeperPin()` 進 `sleeperAccess()`(requireOwner/requireMember 都會經過):
  沒有對應的 sleeper pin 就 die,連 sleeper 租約都不會留下。
  有帶 `--agent-id` 時額外要求它就是那個被 pin 的 agent。
- 連帶更新讀 pins 的三處:resolveScope、`leave` 的 guest pin 清理、SubagentStop hook。

persona-sleeper sub agent 的正常流程不受影響:hook 的 first-touch pinning 在它
第一個指令就寫好 pin,之後每個收尾指令都驗得過。

測試:新增 10 項(實際把 scripts/ 複製成別的檔名重現攻擊)——改名後 hook 確實
不表態、無 pin 時 remember/recall/emotion 都被 CLI 擋下且不留租約、pin 在別的
人格上不能跨過去、guest pin 與舊格式 pin 都不算授權、真有 pin 時照常放行。
345 → 355 項全過。
2026-07-31 09:19:20 +00:00

33 lines
1.2 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
// SubagentStopguest sub agent 結束時解除它的 pin(下次啟動要重新 first-touch),並續租。
import { readEvent, respond } from "./_hook.mjs";
import * as pl from "../scripts/persona-lib.mjs";
const event = readEvent();
if (!event) process.exit(0);
const sessionId = event.session_id || "unknown";
const agentId = event.agent_id;
const agentType = String(event.agent_type || "");
const isGuest = agentType.includes("persona-guest");
const isSleeper = agentType.includes("persona-sleeper");
if (!agentId || (!isGuest && !isSleeper)) process.exit(0);
const data = pl.loadSession(sessionId);
const pins = data.pins || {};
const slug = pl.pinOf(data, agentId)?.persona;
if (slug !== undefined) {
delete pins[agentId];
data.pins = pins;
pl.saveSession(sessionId, data);
if (isSleeper) {
// 睡眠 sub agent 收工:把短期寫入權還掉(就算它中途掛掉,租約 5 分鐘也會自己過期)
if (pl.personaExists(slug)) pl.dropSleepLease(slug, sessionId, agentId);
} else {
const info = (data.guests || {})[slug];
if (info?.room) pl.addGuestLease(slug, sessionId, info.room, data.host || "");
}
}
respond({ suppressOutput: true });