65120a1c65
- docs/permissions-allow.json + scripts/settings-allow-sync:四個 Gitea 正門工具的權限白名單一份, setup script 裝完 plugin 寫一次、SessionStart 每次再對一次(只加不減、冪等) - hooks/lib/mainline.py/scripts/mainline:家目錄沒主線就讀 InkStoneCo/system-dev/mainline.json; set/adopt/clear 兩份一起寫,refresh 只寫家目錄 - hooks/leo21c-write-guard.sh:唯讀 -d(tr/cut/sort…)先剪掉再判、notify_leo trigger 放行 (與 prod-write-guard 同一份白名單)、改法段改印 09-02 起的 youlin 子網域;補第一支測試(26 條) - prod-write-guard/main-and-prod-push-guard/kbdb-live-exam:認得 youlin 新子網域 arcrun-yuga3bse - scripts/ticket:收件 repo 寫 inkstone/ISEP 不再 404(org 寫錯當場講) - scripts/isep-notify:有 TELEGRAM_BOT_TOKEN/TELEGRAM_CHAT_ID 先走 Bot API 直送(雲端唯一通的路) - 測試:A31–A35 共 79 條;README/plugin.json/hooks-inventory 數字實數(61 支、85 條、53 支腳本) 假設(記在這裡等 review):權限規則的形狀沿用 leo 09-07 親手加、實測有效的那四條; 「分類器真的不擋」要雲端一趟 run 的 permission_denials 才驗得到,本 PR 驗不了。 版本:待總管定版(plugin.json 仍 0.22.0)。 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DTZ9QtvjY7MNxjfQbexAm7
98 lines
6.2 KiB
Bash
Executable File
98 lines
6.2 KiB
Bash
Executable File
#!/bin/bash
|
||
# settings-allow-sync 的測試(inkstone/ISEP#130)
|
||
#
|
||
# 它守什麼: 「雲端 session 開工時就帶著跟本機同一份權限白名單」這件事的機器那一半。
|
||
# leo 09-07 親手加的四條只在本機 InkStoneCo/.claude/settings.json;雲端沒有 ⇒ 同樣動作被分類器擋
|
||
# (09-04 run log:permission_denials=6)。清單改住 ISEP 一份,本支把它寫進那台機器的 ~/.claude/settings.json。
|
||
#
|
||
# 🔴 全程在 TMP 底下的假 settings.json 上跑,不碰真的 ~/.claude/settings.json。
|
||
cd "$(dirname "$0")/.." || exit 1
|
||
S=scripts/settings-allow-sync
|
||
LIST=docs/permissions-allow.json
|
||
TMP=$(mktemp -d); trap 'rm -rf "$TMP"' EXIT
|
||
PASS=0; FAIL=0
|
||
ok(){ PASS=$((PASS+1)); printf ' ✅ %s\n' "$1"; }
|
||
bad(){ FAIL=$((FAIL+1)); printf ' ❌ %s\n' "$1"; }
|
||
N=$(python3 -c "import json;print(len(json.load(open('$LIST'))['allow']))")
|
||
|
||
echo "── A. 清單本身的形狀 ──"
|
||
python3 - "$LIST" <<'PY' && ok "A1 每一條都是 Bash(…) 形狀,而且四個正門工具都在場(ticket/mainline/gate-ok/gitea-pr-merge)" || bad "A1 清單形狀不對或少了正門工具"
|
||
import json, sys
|
||
rules = json.load(open(sys.argv[1]))["allow"]
|
||
assert rules and all(r.startswith("Bash(") and r.endswith(")") for r in rules), rules
|
||
for tool in ("scripts/ticket", "scripts/mainline", "scripts/gate-ok", "scripts/gitea-pr-merge.sh"):
|
||
assert any(tool in r for r in rules), "缺 " + tool
|
||
assert not any(r.strip() in ("Bash(python3 *)", "Bash(*)", "Bash(bash *)") for r in rules), "整類放行"
|
||
PY
|
||
|
||
echo "── B. 寫進去(假的 settings.json)──"
|
||
T="$TMP/a/settings.json"
|
||
OUT=$(python3 "$S" --target "$T" --home /root 2>&1); RC=$?
|
||
[ "$RC" = 0 ] && [ -f "$T" ] && ok "B1 目標不存在 ⇒ 建出來(離開碼 0)" || bad "B1 rc=$RC:$OUT"
|
||
python3 -c "import json;d=json.load(open('$T'));assert len(d['permissions']['allow'])==$N" && ok "B2 寫進 $N 條,JSON 合法" || bad "B2 條數不對或不是 JSON"
|
||
grep -q 'Bash(python3 /root/.claude/plugins/cache/inkstone/isep/\*/scripts/ticket \*)' "$T" \
|
||
&& ok "B3 開頭的 ~ 換成了 --home 給的家目錄(雲端=/root)" || bad "B3 ~ 沒換成家目錄"
|
||
grep -q '"~/' "$T" && bad "B4 檔裡還留著 ~" || ok "B4 檔裡沒有殘留的 ~"
|
||
grep -q 'CLAUDE_PLUGIN_ROOT' "$T" && ok "B5 \$CLAUDE_PLUGIN_ROOT 那幾條原樣保留(閘印出來的逃生門就是這個形狀)" || bad "B5 少了 \$CLAUDE_PLUGIN_ROOT 形狀"
|
||
tail -c1 "$T" | od -An -c | grep -q '\\n' && ok "B6 收尾有換行" || bad "B6 沒換行"
|
||
|
||
SUM1=$(cksum < "$T"); OUT=$(python3 "$S" --target "$T" --home /root 2>&1); SUM2=$(cksum < "$T")
|
||
[ "$SUM1" = "$SUM2" ] && printf '%s' "$OUT" | grep -q '已齊' && ok "B7 跑第二次:檔案一個位元組都沒變、說「已齊」(冪等)" || bad "B7 不冪等:$OUT"
|
||
|
||
echo "── C. 只加不減:別人手加的東西一個字不動 ──"
|
||
T2="$TMP/b/settings.json"; mkdir -p "$TMP/b"
|
||
cat > "$T2" <<'JSON'
|
||
{
|
||
"extraKnownMarketplaces": {"inkstone": {"source": {"source": "git", "url": "https://git.uncle6.me/inkstone/ISEP.git"}}},
|
||
"enabledPlugins": {"isep@inkstone": true},
|
||
"permissions": {
|
||
"allow": ["Bash(npx wrangler secret:*)", "Bash(python3 scripts/ticket *)"],
|
||
"deny": ["Edit(//Users/x/wrangler.toml)"]
|
||
},
|
||
"hooks": {"PreToolUse": []}
|
||
}
|
||
JSON
|
||
OUT=$(python3 "$S" --target "$T2" --home /Users/youlinhsieh 2>&1)
|
||
python3 - "$T2" "$N" <<'PY' && ok "C1 原有的 allow/deny/marketplace/hooks 全在,原本那條沒重複,只補了缺的" || bad "C1 動到了別人的東西:$(cat "$T2")"
|
||
import json, sys
|
||
d = json.load(open(sys.argv[1])); n = int(sys.argv[2])
|
||
a = d["permissions"]["allow"]
|
||
assert a[0] == "Bash(npx wrangler secret:*)" and a[1] == "Bash(python3 scripts/ticket *)", a[:2]
|
||
assert a.count("Bash(python3 scripts/ticket *)") == 1
|
||
assert len(a) == 2 + n - 1, (len(a), n)
|
||
assert d["permissions"]["deny"] == ["Edit(//Users/x/wrangler.toml)"]
|
||
assert d["enabledPlugins"] == {"isep@inkstone": True}
|
||
assert d["hooks"] == {"PreToolUse": []}
|
||
assert any("/Users/youlinhsieh/.claude/plugins/cache/inkstone/isep/*/scripts/mainline" in x for x in a)
|
||
PY
|
||
printf '%s' "$OUT" | grep -q "+$((N-1)) 條" && ok "C2 印出來的是「+$((N-1)) 條」(缺幾條補幾條)" || bad "C2 計數不對:$OUT"
|
||
|
||
echo "── D. --check/--dry-run ──"
|
||
python3 "$S" --target "$T" --home /root --check >/dev/null 2>&1 && ok "D1 --check:全齊 ⇒ 離開碼 0" || bad "D1 --check 全齊卻非 0"
|
||
T3="$TMP/c/settings.json"; mkdir -p "$TMP/c"; echo '{}' > "$T3"
|
||
python3 "$S" --target "$T3" --home /root --check >/dev/null 2>&1; RC=$?
|
||
[ "$RC" = 1 ] && ok "D2 --check:缺 ⇒ 離開碼 1(驗收用)" || bad "D2 --check 缺卻回 $RC"
|
||
[ "$(cat "$T3")" = "{}" ] && ok "D3 --check 不寫檔" || bad "D3 --check 寫了檔"
|
||
python3 "$S" --target "$T3" --home /root --dry-run >/dev/null 2>&1
|
||
[ "$(cat "$T3")" = "{}" ] && ok "D4 --dry-run 不寫檔" || bad "D4 --dry-run 寫了檔"
|
||
|
||
echo "── E. 壞掉的目標:不碰、不炸(這支掛在 SessionStart)──"
|
||
T4="$TMP/d/settings.json"; mkdir -p "$TMP/d"; printf '{ not json' > "$T4"
|
||
OUT=$(python3 "$S" --target "$T4" --home /root 2>&1); RC=$?
|
||
[ "$RC" = 0 ] && [ "$(cat "$T4")" = "{ not json" ] && printf '%s' "$OUT" | grep -q '沒有動它' \
|
||
&& ok "E1 目標不是 JSON ⇒ 原封不動、講出來、離開碼 0" || bad "E1 rc=$RC 內容=$(cat "$T4") out=$OUT"
|
||
python3 "$S" --target "$T4" --home /root --check >/dev/null 2>&1; [ $? = 1 ] && ok "E2 同一個檔用 --check 問 ⇒ 離開碼 1(不裝沒事)" || bad "E2 --check 對壞檔回 0"
|
||
|
||
echo "── F. 接線:SessionStart 有掛、雲端 setup script 有叫 ──"
|
||
python3 - <<'PY' && ok "F1 hooks.json 的 SessionStart 註冊了 scripts/settings-allow-sync" || bad "F1 hooks.json 沒掛"
|
||
import json
|
||
d = json.load(open("hooks/hooks.json"))
|
||
cmds = [h["command"] for g in d["hooks"]["SessionStart"] for h in g["hooks"]]
|
||
assert any("scripts/settings-allow-sync" in c for c in cmds), cmds
|
||
PY
|
||
grep -q 'settings-allow-sync' docs/cloud-setup-script.sh && ok "F2 docs/cloud-setup-script.sh 裝完 plugin 會跑它" || bad "F2 setup script 沒叫它"
|
||
grep -q 'exit 0$' docs/cloud-setup-script.sh && ok "F3 setup script 仍然 exit 0(非零會讓整個 session 開不起來)" || bad "F3 setup script 結尾不是 exit 0"
|
||
|
||
echo; echo "$PASS/$((PASS+FAIL)) 通過"
|
||
[ "$FAIL" -eq 0 ]
|