fix(guard): 補上 Glob/Grep 的兩個自然破口,並誠實標示 guard 的定位(S2)
`extractPaths()` 只解析「含 / 且展開後包含 home 或 personas」的 token,
於是兩種**模型最自然會寫出來的列舉方式**整路穿過去:
1. `PATH_TOOL_FIELDS.Glob` 只列 `path`,所以樣式欄位不被檢查——
`Glob { pattern: "<home>/*/IDENTITY.md" }`(不給 `path`)可以掃出全部人格的身分檔。
新增 `PATTERN_TOOL_FIELDS`(`Glob.pattern`、`Grep.glob`),且**相對於 `path` 解析**
(沒給 `path` 才相對 cwd),因為樣式的基準點跟路徑欄位不同。
`Grep.pattern` 是正規表示式、不是路徑,故意不收,免得誤攔 `a/b/c` 這種樣式。
2. `Grep`/`Glob` 沒給 `path` 時 `extractPaths` 回空陣列 → guard 不表態,
於是 cwd 站在 `~/.claude/personas` 或別人的人格底下直接 `Grep` 就整批穿過去。
改成把 hook event 的 `cwd` 當預設目標。
其餘的直譯器逃逸(`node -e`、`python3 -c`、逐段 `cd`、引號切割 token)**不用正則補**:
Bash 圖靈完備,追指令字串永遠落後一步,每加一條正則就多一批誤攔正常指令的風險。
改為把文件的措辭修正成誠實的定位——
- README 新增〈guard 擋得住什麼、擋不住什麼〉:明說 guard 是**防漂移的護欄,
對正常寫法一律 deny,不是對抗性沙箱**,並逐條列出擋得住與擋不住的形式,
以及「真要對抗性隔離請走 OS 層」。
- 拿掉會誤導的字眼:`PreToolUse` 那列的「唯一強制點」、guard.mjs 檔頭的
「唯一的強制執行點」、「不能被繞過的關鍵」。
測試:新增 13 項——樣式欄位指向全倉庫/別的人格/自己、樣式相對 path 解析、
`Grep.pattern` 不誤判成路徑、不給 path 時 cwd 在別人格/倉庫根/自己人格/
普通專案的四種情形、給了 path 就不看 cwd,以及兩項文件措辭的迴歸檢查。
355 → 368 項全過。
This commit is contained in:
+22
-6
@@ -2717,6 +2717,14 @@ const PATH_TOOL_FIELDS = {
|
||||
Grep: ["path"],
|
||||
LS: ["path"],
|
||||
};
|
||||
// 樣式欄位本身就會帶路徑:`Glob { pattern: "<home>/*/IDENTITY.md" }` 不給 `path` 也掃得到
|
||||
// 別人的身分檔,這是模型最自然會寫出來的列舉方式。它是相對於 `path`(沒給就相對 cwd)
|
||||
// 解析的,所以基準點跟 PATH_TOOL_FIELDS 不同,另外列一張表。
|
||||
// 注意 `Grep.pattern` 是正規表示式、不是路徑,故意不收。
|
||||
const PATTERN_TOOL_FIELDS = {
|
||||
Glob: ["pattern"],
|
||||
Grep: ["glob"],
|
||||
};
|
||||
|
||||
export const GUEST_SAFE_SUBCOMMANDS = new Set([
|
||||
"show", "status", "list", "recall", "room", "remember", "leave", "brief", "think", "said",
|
||||
@@ -2779,12 +2787,20 @@ export function personaSlugOf(target) {
|
||||
|
||||
export function extractPaths(toolName, toolInput, cwd) {
|
||||
const out = [];
|
||||
for (const field of PATH_TOOL_FIELDS[toolName] || []) {
|
||||
const value = toolInput?.[field];
|
||||
if (typeof value === "string" && value) {
|
||||
const resolved = resolvePath(value, cwd);
|
||||
if (resolved) out.push(resolved);
|
||||
}
|
||||
const push = (value, base) => {
|
||||
if (typeof value !== "string" || !value) return;
|
||||
const resolved = resolvePath(value, base);
|
||||
if (resolved) out.push(resolved);
|
||||
};
|
||||
for (const field of PATH_TOOL_FIELDS[toolName] || []) push(toolInput?.[field], cwd);
|
||||
if (PATTERN_TOOL_FIELDS[toolName]) {
|
||||
const rawPath = typeof toolInput?.path === "string" ? toolInput.path : "";
|
||||
// 樣式相對於 `path`;`path` 沒給的話,搜尋起點就是 hook event 的 cwd
|
||||
const base = rawPath ? resolvePath(rawPath, cwd) || cwd : cwd;
|
||||
for (const field of PATTERN_TOOL_FIELDS[toolName]) push(toolInput?.[field], base);
|
||||
// Grep/Glob 不給 `path` 時就是「掃 cwd」。少了這一條,cwd 站在人格倉庫底下的
|
||||
// Grep 會解析出空陣列 → guard 不表態 → 整批人格資料直接穿過去。
|
||||
if (!rawPath) push(cwd, cwd);
|
||||
}
|
||||
if (toolName === "Bash") {
|
||||
const command = toolInput?.command || "";
|
||||
|
||||
@@ -141,6 +141,50 @@ check("一般 sub agent 沿用 host 範圍(sub agent 不限)",
|
||||
guard({ session_id: S_HOST, agent_id: "ag-1", agent_type: "Explore", tool_name: "Read",
|
||||
tool_input: { file_path: `${H}/alpha/memory/INDEX.md` } }) === "pass");
|
||||
|
||||
// --- S2 迴歸:模型最自然會寫出來的兩種列舉方式 ------------------------------ //
|
||||
// (1) 樣式欄位本身就帶路徑,卻沒有 `path`
|
||||
check("Glob 只給 pattern(不給 path)掃全倉庫 → 攔下",
|
||||
guard({ session_id: S_HOST, tool_name: "Glob", tool_input: { pattern: `${H}/*/IDENTITY.md` } }) === "deny");
|
||||
check("Glob 只給 pattern 指名別的人格 → 攔下",
|
||||
guard({ session_id: S_HOST, tool_name: "Glob", tool_input: { pattern: `${H}/beta/**/*.md` } }) === "deny");
|
||||
check("Grep 的 glob 欄位指向別的人格 → 攔下",
|
||||
guard({ session_id: S_HOST, tool_name: "Grep",
|
||||
tool_input: { pattern: "秘密", glob: `${H}/beta/**` } }) === "deny");
|
||||
check("Glob pattern 指向自己的人格 → 放行",
|
||||
guard({ session_id: S_HOST, tool_name: "Glob", tool_input: { pattern: `${H}/alpha/**/*.md` } }) === "pass");
|
||||
check("樣式相對於 path 解析(path=自己 + pattern=**/*.md → 放行)",
|
||||
guard({ session_id: S_HOST, tool_name: "Glob",
|
||||
tool_input: { path: `${H}/alpha`, pattern: "**/*.md" } }) === "pass");
|
||||
check("Grep 的 pattern 是正規表示式、不當路徑看(不誤攔)",
|
||||
guard({ session_id: S_HOST, tool_name: "Grep",
|
||||
tool_input: { pattern: "a/b/c", path: `${H}/alpha` } }) === "pass");
|
||||
// (2) 不給 `path` 時,掃描起點就是 cwd
|
||||
check("cwd 站在別的人格底下、Grep 不給 path → 攔下",
|
||||
guard({ session_id: S_HOST, cwd: `${H}/beta`, tool_name: "Grep",
|
||||
tool_input: { pattern: "." } }) === "deny");
|
||||
check("cwd 站在倉庫根目錄、Glob 不給 path → 攔下",
|
||||
guard({ session_id: S_HOST, cwd: H, tool_name: "Glob", tool_input: { pattern: "**/SOUL.md" } }) === "deny");
|
||||
check("cwd 站在自己的人格底下、Grep 不給 path → 放行",
|
||||
guard({ session_id: S_HOST, cwd: `${H}/alpha`, tool_name: "Grep", tool_input: { pattern: "." } }) === "pass");
|
||||
check("cwd 在人格倉庫外的普通專案 → 不表態(不誤攔)",
|
||||
guard({ session_id: S_HOST, cwd: path.join(HERE, ".."), tool_name: "Grep",
|
||||
tool_input: { pattern: "TODO" } }) === "pass");
|
||||
check("給了 path 就不再拿 cwd 當目標",
|
||||
guard({ session_id: S_HOST, cwd: `${H}/beta`, tool_name: "Grep",
|
||||
tool_input: { pattern: ".", path: `${H}/alpha` } }) === "pass");
|
||||
// 直譯器逃逸不用正則補,改成在文件裡誠實說明定位——這裡確保那段話還在。
|
||||
check("README 誠實標示 guard 的定位(護欄,不是對抗性沙箱)", (() => {
|
||||
const md = fs.readFileSync(path.join(HERE, "..", "README.md"), "utf8");
|
||||
return md.includes("guard 擋得住什麼、擋不住什麼") &&
|
||||
md.includes("防漂移的護欄,不是對抗性沙箱") &&
|
||||
md.includes("直譯器逃逸") && md.includes("逐段 `cd`") &&
|
||||
!md.includes("唯一強制點");
|
||||
})());
|
||||
check("guard hook 的檔頭不再自稱唯一的強制執行點", (() => {
|
||||
const src = fs.readFileSync(path.join(HOOKS, "guard.mjs"), "utf8");
|
||||
return !src.includes("唯一的強制執行點") && src.includes("不是對抗性沙箱");
|
||||
})());
|
||||
|
||||
console.log("④ 情緒(六正向 + 六負向)");
|
||||
check("十二種情緒", pl.EMOTION_KEYS.length === 12 && pl.POSITIVE.length === 6 && pl.NEGATIVE.length === 6);
|
||||
cli(["emotion", "--persona", "alpha", "--session", S_HOST, "--apply", "joy=+60,anger=+40", "--trigger", "selftest"]);
|
||||
|
||||
Reference in New Issue
Block a user