Files
ISEP/scripts/settings-allow-sync
isep-hand 65120a1c65 雲端要拿得到票、主線與通知:白名單住 ISEP、主線檔隨 repo 走、leo21c 讀放寫擋(inkstone/ISEP#130)
- 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
2026-09-07 01:17:15 +00:00

147 lines
6.3 KiB
Python
Executable File
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 python3
"""settings-allow-sync — 把 ISEP 的權限白名單寫進**這台機器**的 ~/.claude/settings.json
inkstone/ISEP#130
━━ 為什麼有這支 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
leo 09-07 親手把四個 Gitea 正門工具(`ticket``mainline``gate-ok``gitea-pr-merge`
加進本機 `InkStoneCo/.claude/settings.json` 的 `permissions.allow`auto mode 的分類器
才不擋。**雲端 session 沒有那四條** ⇒ 同樣的動作在雲端照樣被擋
09-04 progress-guard run logpermission_denials=6)。
雲端真正讀的是薄殼 repoGitHub)的 settings——推它要 D20 開閘(leo 親跑 github-arm)。
⇒ 白名單不住薄殼、也不住 InkStoneCo**住 ISEP 一份**`docs/permissions-allow.json`),
由本支在那台機器上寫進 `~/.claude/settings.json`user scope,跟 cwd 無關)。
雲端:`docs/cloud-setup-script.sh` 裝完 plugin 就跑一次;之後每個 SessionStart 再對一次,
plugin 更新帶進新規則時不必等快照重拍。本機:同一支、同一份清單。
━━ 規矩 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
· 只加不減:settings.json 裡本來就有的東西一個字不動(別人手加的規則不是本支的地盤)。
· 冪等:跑第二次 +0 條,檔案一個位元組都不變。
· 清單裡開頭的 `~` 換成這台的家目錄(本機 /Users/…、雲端 /root),其餘照抄。
· 原子寫:先寫暫存檔再 rename,中途斷掉不會留下半個 JSON。
· 讀不懂目標(不是 JSON)⇒ **不碰它**、印出來、離開碼 0——這支掛在 SessionStart
不准因為它把 session 弄壞;`--check` 才會用離開碼講話。
━━ 用法 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
settings-allow-sync 寫進 ~/.claude/settings.json
settings-allow-sync --dry-run 只印會加哪幾條,不寫
settings-allow-sync --check 對帳:缺任何一條 ⇒ 離開碼 1(測試/驗收用)
settings-allow-sync --target <檔> --home <家目錄> 測試用:指到別的檔、別的家目錄
settings-allow-sync --quiet 沒事就閉嘴(SessionStart 用)
"""
import json
import os
import sys
import tempfile
HERE = os.path.dirname(os.path.abspath(__file__))
SOURCE = os.path.join(HERE, "..", "docs", "permissions-allow.json")
def wanted(home):
with open(SOURCE, encoding="utf-8") as f:
d = json.load(f)
out = []
for rule in d.get("allow") or []:
if not isinstance(rule, str) or not rule.startswith("Bash(") or not rule.endswith(")"):
raise ValueError("清單裡有一條不是 Bash(…) 的形狀:%r" % (rule,))
inner = rule[len("Bash("):-1]
# 只換「路徑開頭」的 ~`python3 ~/.claude/…` 這種。`~` 出現在別處不動。
parts = inner.split(" ")
parts = [home + p[1:] if p.startswith("~/") else p for p in parts]
out.append("Bash(" + " ".join(parts) + ")")
return out
def main(argv):
target = os.path.join(os.path.expanduser("~"), ".claude", "settings.json")
home = os.path.expanduser("~")
dry = check = quiet = False
i = 0
while i < len(argv):
a = argv[i]
if a == "--target":
i += 1; target = argv[i]
elif a == "--home":
i += 1; home = argv[i].rstrip("/")
elif a == "--dry-run":
dry = True
elif a == "--check":
check = True
elif a == "--quiet":
quiet = True
elif a in ("-h", "--help"):
print(__doc__); return 0
else:
print("不認得的參數:%s--help 看用法)" % a, file=sys.stderr); return 2
i += 1
try:
rules = wanted(home)
except Exception as e:
print("🔴 讀不了白名單清單 %s%s" % (SOURCE, e), file=sys.stderr)
return 1 if check else 0
cur = {}
if os.path.exists(target):
try:
with open(target, encoding="utf-8") as f:
cur = json.load(f)
if not isinstance(cur, dict):
raise ValueError("最外層不是物件")
except Exception as e:
print("🔴 %s 讀不懂(%s)——**沒有動它**。修好它再跑一次。" % (target, e), file=sys.stderr)
return 1 if check else 0
perms = cur.get("permissions")
if not isinstance(perms, dict):
perms = {}
allow = perms.get("allow")
if not isinstance(allow, list):
allow = []
have = set(a for a in allow if isinstance(a, str))
missing = [r for r in rules if r not in have]
if check:
if missing:
print("🔴 %s%d 條:" % (target, len(missing)))
for r in missing:
print(" " + r)
return 1
if not quiet:
print("✅ %s 已有全部 %d 條" % (target, len(rules)))
return 0
if not missing:
if not quiet:
print("✅ 白名單已齊(%d 條):%s" % (len(rules), target))
return 0
if dry:
print("🧪 --dry-run:會加 %d 條進 %s" % (len(missing), target))
for r in missing:
print(" " + r)
return 0
perms["allow"] = allow + missing
cur["permissions"] = perms
try:
os.makedirs(os.path.dirname(target) or ".", exist_ok=True)
fd, tmp = tempfile.mkstemp(prefix=".settings-", suffix=".json", dir=os.path.dirname(target) or ".")
with os.fdopen(fd, "w", encoding="utf-8") as f:
json.dump(cur, f, ensure_ascii=False, indent=2)
f.write("\n")
os.replace(tmp, target)
except Exception as e:
print("🔴 寫不進 %s%s" % (target, e), file=sys.stderr)
return 0
print("✅ 白名單 %d 條 → %s(共 %d 條來自 ISEP" % (len(missing), target, len(rules)))
if not quiet:
for r in missing:
print(" " + r)
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))