Files
ISEP/hooks/github-contact-guard.sh
T
Leo 0cdb6f2c05 fix(hooks): 讓四支閘認得出「指令位置」跟「heredoc/引號裡的文字」
inkstone/InkStoneCo#23、#56 同一個病:閘對整條指令字串做關鍵字掃描,
把「檔案內容/留言引用裡剛好提到某個關鍵字」當成「真的在執行」,
同時放過包一層讀取指令、或藏在 heredoc body 裡的真動作。

- 新增共用輔助 hooks/lib/strip_heredoc.py:heredoc body 是資料不是指令,
  四支閘(github-contact / main-and-prod-push / stage-before-prod /
  kbdb-api-wall 的 Bash 分支)呼叫前一律先拿掉 body 再比對。
- main-and-prod-push-guard.sh:修掉跟 release-tag-guard.sh 同款的
  「開頭是讀取工具就整條放行」前綴繞過洞;git push 的偵測改成指令位置比對;
  main/master 目標改用單字邊界,不再誤中 "domain" 這種子字串。
- github-contact-guard.sh:拿掉 gh CLI/git push 判準裡「前面隨便一個空白
  就算數」的鬆散邊界,只認真正的指令分隔符。
- kbdb-api-wall-guard.sh:Bash 分支原本引用不存在的 kbdb_cmd_check.py,
  python3 找不到檔案就吃掉錯誤印 "OK",該分支形同虛設——任何
  `wrangler d1 execute` 直打 kbdb 都會被放行。邏輯搬進新檔
  hooks/lib/kbdb_cmd_check.py(shlex 分詞、quote-aware),把 .sh 的
  參照路徑改過去,補回 Bash 分支的 kbdb-sql-ok 逃生口。

四支各補 InkStoneCo#40 §1 要求的三行中文檔頭。

新增四支可重跑測試(scripts/test-*.sh),共 69 條斷言全過,
含 #23/#56 票上實撞的原始形狀(寫 docs/TESTING.md 的 heredoc、
貼引用 #56 敘述的留言、`grep git push`)。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-20 18:51:52 +08:00

155 lines
11 KiB
Bash
Executable File
Raw 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.
#!/bin/bash
# 管什麼: 對外接觸 GitHub 的寫入動作(gh CLI 高頻 API、git push/remote add 指向 github.com)——一律先擋,要 leo 手動解保險才放行。
# 為什麼: D202026-07-02)——兩個帳號因高頻寫入被 GitHub flag 永久拿不回;讀不設限(讀不是 abuse),只擋寫。
# 誤觸時怎麼關: 讀取(clone/fetch/pull/curl 抓檔)一律放行;真要寫 GitHubleo 跑 scripts/github-arm.sh 解保險;純粹提到 github.com 的文字(heredoc/commit message/留言)不算數,見下方判準。
# github-contact-guard.sh — GitHub 接觸保險(D202026-07-02
# 背景:兩個帳號因高頻動作被 flag 拿不回(幾十顆星+issues 全損)。
# 設計:戰鬥機武器保險模式——平時所有 github.com 接觸一律機械擋下(exit 2),
# 只有 leo 親手跑 scripts/github-arm.sh 解鎖(限時),期間每次接觸自動留痕。
# 誠實限制(mindset §7):AI 技術上可自建 .github-armed 繞過;本 hook 的價值是
# 擋手滑+留審計軌跡,最後一段靠 D20 鐵律與自律。絕不聲稱「不可能繞過」。
# 2026-08-10 leo:以後一律匿名讀,寫才實名(詳見下方判定區塊的說明)。
#
# 🔴 另一個誠實限制(2026-08-09arcrun-rag#27 查出的根因,別再花時間重查):
# 本 hook 只看得到「Claude Code 的 Bash 工具呼叫本身的指令字串」。
# 若某支程式(例如 products/arcrun-rag/installer/scripts/ship.mjs)自己用
# node:child_process 的 execFileSync/spawnSync 直接 spawn `git push`
# 那個 push 是那支程式的**子行程**,不會產生新的 Bash 工具呼叫——本 hook
# 從頭到尾看不到它,不管下面的正則怎麼改都一樣(8/8 23:16 那次出貨的 push
# 就是這樣:hook 看到的指令字串只有 `node ship.mjs --target prod --confirm`
# 裡面沒有 git/github.com 字面,於是判成「匿名讀」放行,連 D20 保險存不存在
# 都沒機會查,log 自然也不會有那一筆)。
# ⇒ 這種「碰 GitHub 的動作被包在別支程式裡」的情況,保險檢查與留痕**必須由
# 那支程式自己做**(見 ship.mjs 用的 installer/scripts/d20-guard.mjs),
# 不能指望改這支 hook 就能補到——這支 hook 天生看不見子行程。
INPUT=$(cat)
CMD=$(printf '%s' "$INPUT" | python3 -c "
import json,sys
try:
d = json.load(sys.stdin)
print(d.get('tool_input',{}).get('command',''))
except Exception:
print('')
" 2>/dev/null)
[ -z "$CMD" ] && exit 0
# 2026-08-20inkstone/InkStoneCo#23 comment, #56):a heredoc body is data, not an
# instruction. Writing docs/TESTING.md (one example line of a real push command,
# meant for a human to try later) got blocked because the whole heredoc body was
# scanned for keywords. Strip heredoc bodies before matching; on failure fall back
# to the original string (fail-open on "did this helper run", not on the verdict).
CMD=$(printf '%s' "$CMD" | python3 "$(dirname "$0")/lib/strip_heredoc.py" 2>/dev/null || printf '%s' "$CMD")
# 命中判定(D20 邊界,2026-07-05 leo 拍板——Facebook 比喻定調):
# GitHub 不在乎你「讀」(clone/fetch/抓 release,不管實名匿名、自己的還別人的——那是它原本的功能,
# 像 FB 不禁你讀貼文)。它 abuse-detect 的是「機器人一直改/寫」(高頻 push、Actions fan-out、API 寫)
# ——那才是害 richblack/uncle6 被 flag 的模式。**真正的線=讀 vs 寫,不是匿名 vs 實名。**
# 所以:所有「讀」一律放行(含帶認證 clone 自己的 repo);只擋「寫」與「gh 高頻 API」。
# 只擋以下兩型(寫入 / 會寫的 API):
# ① gh CLI 網路子指令(高頻 API,多會寫,且走你 token)② git 寫入動詞指向 githubpush / remote add
# 放行:git clone/fetch/pull/ls-remote(任何 repo,帶不帶認證都是讀)、curl/wget、go get/pip。
HIT=""
# ① gh CLI —— 高頻 API,全擋(讀寫混雜且走你 token,保守全擋;真要唯讀查詢個案 arm)
if printf '%s' "$CMD" | grep -qE '(^|[;&|(`]|&&|\|\|)[[:space:]]*gh[[:space:]]+(api|repo|issue|pr|auth|search|release|run|workflow|gist|browse)\b'; then
HIT="gh CLI(高頻 API,走你的 token"
# ② git 寫入動詞指向 githubpush / remote add 為 push 鋪路)—— 寫入,擋
elif printf '%s' "$CMD" | grep -qiE '(^|[;&|(`]|&&|\|\|)[[:space:]]*git[[:space:]]+(push|remote[[:space:]]+add)([^|;&]*)(github\.com)'; then
HIT="git 寫入 → githubpush/remote add"
# ②b 🔴 2026-08-05 補漏:上面那條只認指令裡的 **github.com 字面**
# ⇒ `git push origin main`remote 名指向 GitHub**完全不會被攔**。
# 實撞:08-05 出貨那次 `git push origin main` 是「沒被偵測到」才過的,
# 不是靠保險放行——等於 D20 的武器閘有一個大洞,而它平常看起來很正常。
# ⇒ 用 remote 名去把 URL 解出來再判。
# ⚠️ 只有解出來**真的是 github** 才擋——Gitea 是真相源、要能自由推,不可誤傷。
# 解不出來(不在 git repo/remote 不存在)就放行:那種情況 git 自己也會失敗。
# ②c 🔴 2026-08-10 再補一個漏:`git -c http.<url>.extraheader=... push -u github main`
# publish-github.sh 的寫法)command 字面裡**本來就看得到** github.com——
# 不必等 remote 名稱解析成功才判斷得出來,而 remote 名稱解析在某些 cwd 下會失敗
# (見檔頭「2026-08-10 leo 簡化」那段的實撞紀錄)。字面查得到就直接判定,
# 查不到才退回舊的「解 remote 名稱」那條路——兩條路都失手才會誤放行。
elif printf '%s' "$CMD" | grep -qiE '(^|[;&|(`]|&&|\|\|)[[:space:]]*git[[:space:]]+([^|;&]*[[:space:]])?push([[:space:]]|$)'; then
# 🔴 總管 2026-08-10 收窄:原版寫成「整串裡有 github.com 就擋」,
# 於是**連 commit message 提到那個網址都會被擋**(我自己第一次要 commit 就撞到)。
# ⇒ 改成必須是「push 的目標」:github.com 要出現在 push 之後、且中間不跨命令分隔符。
# 誤擋的代價不是不方便——是它會逼人拆指令繞路,久了整道閘就沒人當真。
# 第二次收窄:只看「push 後面**第一個非旗標參數**」是不是 github 網址。
# 光要求「同一段命令內」還不夠——commit message 寫「push 到 https://github.com/…」
# 中間沒有 ;&| 一樣命中。真正的推送,網址必定緊接在 push 與旗標之後。
if printf '%s' "$CMD" | grep -qiE 'push(\s+-{1,2}[A-Za-z0-9=_-]+)*\s+(https?://|git@|ssh://)[^[:space:]]*github\.com'; then
HIT="git push → githubpush 的目標字面就是 github.com"
else
CWD=$(printf '%s' "$INPUT" | python3 -c "
import json,sys
try: print(json.load(sys.stdin).get('cwd','') or '')
except Exception: print('')
" 2>/dev/null)
[ -n "$CWD" ] || CWD=$(pwd)
# 取 push 後面第一個非 flag 的字當 remote 名;沒有就是 origin
RNAME=$(printf '%s' "$CMD" | sed -nE 's/.*git[[:space:]]+([^|;&]*[[:space:]])?push[[:space:]]+(-[^[:space:]]+[[:space:]]+)*([A-Za-z0-9._/-]+).*/\3/p' | head -1)
[ -n "$RNAME" ] || RNAME="origin"
RURL=$(git -C "$CWD" remote get-url "$RNAME" 2>/dev/null)
if printf '%s' "$RURL" | grep -qi 'github\.com'; then
HIT="git push → githubremote「$RNAME」指向 GitHub"
fi
fi
fi
# 明確放行(全部是「讀」,零 flag 風險——GitHub 原本就允許):
# git clone/fetch/pull/ls-remote 任何 github repo(自己的/別人的、帶認證與否都是讀)、
# curl/wget 抓 release/raw/codeload、go get/pip 抓公開 module。
# ——讀不是 abuse,不在 D20 射程。唯一風險是「機器人一直寫」,已由 ①② 擋住。
#
# 2026-08-10 leo 簡化:讀不分匿名/實名,一律放行、不計數、無頻率閘(見檔頭說明)。
# 舊版這裡曾有一段「實名讀頻率閘」(URL 帶 token/讀自己 private repo → 每 4 小時放行 1 次),
# 08-10 真的誤傷過一次出貨(見檔頭)。leo 拍板拿掉:判準只剩「有沒有在寫」,讀就是讀。
#
# 🔴 總管 2026-08-10 補回一件(agent 原版把偵測整段刪了,我只拿掉「擋」,不拿掉「看得見」):
# leo 的規則是「**一律匿名讀**」⇒ 帶著憑證去讀,本身就已經偏離規則。
# hook 沒辦法強迫一條指令變匿名,但**可以留下痕跡**——
# 否則哪天 GitHub 又來說我們高頻活動,我們手上沒有任何紀錄可以自證讀了什麼。
# ⇒ 實名讀:**放行**(照 leo 的簡化,不擋、不限速),但**寫一行進接觸紀錄**。
if [ -z "$HIT" ]; then
if printf '%s' "$CMD" | grep -qiE '(https?://)[^[:space:]/@]+@[a-z0-9._-]*(github\.com|githubusercontent\.com|codeload\.github)'; then
_LOG="${CLAUDE_PROJECT_DIR:-$(pwd)}/system-dev/docs/3-specs/autonomy-dispatch/github-contact-log.md"
printf -- '- %s | 實名讀(URL 帶憑證,已放行)| %s\n' \
"$(date '+%Y-%m-%d %H:%M')" "$(printf '%s' "$CMD" | sed 's|//[^@]*@|//<憑證省略>@|g' | cut -c1-120)" \
>> "$_LOG" 2>/dev/null
fi
exit 0
fi
PROJ="${CLAUDE_PROJECT_DIR:-$(pwd)}"
ARM_FILE="$PROJ/.github-armed"
LOG_FILE="$PROJ/system-dev/docs/3-specs/autonomy-dispatch/github-contact-log.md"
if [ -f "$ARM_FILE" ]; then
EXPIRY=$(sed -n '1p' "$ARM_FILE")
MISSION=$(sed -n '2p' "$ARM_FILE")
NOW=$(date +%s)
if [ "$NOW" -le "${EXPIRY:-0}" ] 2>/dev/null; then
# 已解鎖 → 放行 + 留痕(BDA 戰損記錄)
SHORTCMD=$(printf '%s' "$CMD" | head -c 200 | tr '\n' ' ')
printf '| %s | %s | `%s` |\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$MISSION" "$SHORTCMD" >> "$LOG_FILE" 2>/dev/null
exit 0
else
rm -f "$ARM_FILE"
echo "❌ BLOCKED by D20 GitHub 接觸儀式" >&2
echo "違反項:保險已過期(武裝時效已過,自動回到保險狀態)" >&2
echo "正確做法:需要繼續接觸 GitHub → 請 leo 重新在終端機跑 scripts/github-arm.sh \"任務描述\"" >&2
exit 2
fi
fi
echo "❌ BLOCKED by D20 GitHub 接觸儀式(武器保險未解除)" >&2
echo "命中:${HIT}" >&2
echo "背景:兩個 GitHub 帳號因高頻動作被 flag 永久拿不回。平時一律只碰 Gitea。" >&2
echo "正確做法(發射流程):" >&2
echo " 1. 先自問:這件事 Gitea 做不到嗎?(issue/PR/push 內部協作 Gitea 全都能)" >&2
echo " 2. 真的非 GitHub 不可 → 向 leo(總部)請示:目標 repo、動作清單、預估請求次數、為何非它不可" >&2
echo " 3. leo 同意後由 leo 親手在終端機跑:scripts/github-arm.sh \"任務描述\" [分鐘,預設30]" >&2
echo " 4. 解鎖期間動作守 ROE:單 repo、≤5 次網路請求、禁批量/迴圈/Actions、間隔像人手" >&2
echo "參考:system-dev/docs/2-architecture/decisions/D20-github-contact-protocol.md" >&2
exit 2