0cdb6f2c05
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>
203 lines
13 KiB
Bash
Executable File
203 lines
13 KiB
Bash
Executable File
#!/bin/sh
|
||
# 管什麼: prod 出貨鏈的動作(打 prod bundle、開 GitHub 保險、prod 安裝器部署)——沒有 6 小時內的 stage 驗證紀錄不放行。
|
||
# 為什麼: leo 2026-08-08——開始封測後推 prod 就是發佈給人看,要先在 stage 走過一次真實路徑再上。
|
||
# 誤觸時怎麼關: 讀取/查證(讀 manifest、讀腳本、curl 公開 CDN)一律放行;真的驗過 stage,貼實測輸出後 `touch /tmp/.stage-verified` 重送。
|
||
# stage-before-prod-guard.sh — PreToolUse(Bash):**未經 stage 驗過,不准動 prod 出貨鏈**。
|
||
#
|
||
# 🔴 立這道閘的來由(leo 2026-08-08):
|
||
# 「現在因為**開始封測**,不直接打到 prod,而是先打到昨天建的 stage,你知道嗎?
|
||
# 或是**有在哪裏寫了讓你不會直接推 prod**?因為**推 prod 就發佈了**,
|
||
# 雖然現在人不多,但**要謹慎**。」
|
||
#
|
||
# 查證結果(總管 2026-08-08 實查,這道閘因此才有必要):
|
||
# ① wiki 有 stage 的記載(08-07 建的:staging bundle repo 在 Gitea
|
||
# `arcrun-rag-bundles-staging`、安裝器 `arcrun-rag-installer-staging.uncle6-me.workers.dev`)
|
||
# ② **但 `ship-check` skill 的出貨步驟從頭到尾寫的是 prod**
|
||
# (github-arm → GitHub `arcrun-rag-bundles` → jsDelivr/raw 驗證),
|
||
# **沒有任何一步是「先上 stage 驗過再上 prod」**
|
||
# ③ **沒有任何 hook 擋 prod**。D20 那道擋的是「寫 GitHub」,不是「未經 stage 就發佈」——
|
||
# arm 一開,照樣可以直達 prod
|
||
# ⇒ 也就是「知道 stage 存在」對行為**零作用**。這是同一天第四次同款:
|
||
# 規則/知識存在,但沒有牙齒(前三次:history-first、KBDB-first、派工-first)。
|
||
#
|
||
# 兩道條件,**缺一不可**(leo 2026-08-08 把層級拉高後的形狀):
|
||
# ① `.github-armed` 存在=leo 親手解過保險。**這道我造不出來,也不准去造。**
|
||
# leo:「現在到 github 就是同 arm,推到 prod 也應該視同 arm。」
|
||
# ② `/tmp/.stage-verified` 6 小時內=stage 驗過(要在回覆貼實測輸出,
|
||
# 「我測過了」不算)。這道是我的責任,但它**不能替代 ①**。
|
||
set -eu
|
||
|
||
INPUT="$(cat)"
|
||
CMD=$(printf '%s' "$INPUT" | python3 -c '
|
||
import sys, json
|
||
try: print(json.load(sys.stdin).get("tool_input", {}).get("command", "") or "")
|
||
except Exception: print("")
|
||
' 2>/dev/null || echo "")
|
||
[ -z "$CMD" ] && exit 0
|
||
|
||
# 2026-08-20(inkstone/InkStoneCo#23、#56 同款):heredoc 的 body 是資料,不是指令。
|
||
# 拿掉 heredoc body 再比對,跟 github-contact-guard.sh/main-and-prod-push-guard.sh
|
||
# 用同一支共用輔助程式;失敗就退回原字串(fail-open 在「這支有沒有幫上忙」,
|
||
# 不是「放行與否」——見 hooks/lib/strip_heredoc.py 檔頭)。
|
||
CMD=$(printf '%s' "$CMD" | python3 "$(dirname "$0")/lib/strip_heredoc.py" 2>/dev/null || printf '%s' "$CMD")
|
||
|
||
# 只攔「會讓封測者拿到東西」的動作:prod bundle repo、開 GitHub 保險、prod 安裝器部署。
|
||
# staging 的同名動作要放行——所以先排除帶 staging 字樣的命令。
|
||
case "$CMD" in
|
||
*staging*|*-staging*) exit 0 ;;
|
||
esac
|
||
# 🔴 2026-08-08:本閘上線後**連續誤攔自己兩次**,兩次都是同一個病——
|
||
# 只比對「命令字串裡有沒有出現關鍵字」,把**談論**當成**執行**:
|
||
# ① `sed -n '1,40p' scripts/github-arm.sh` =我只是要**讀**那支腳本
|
||
# ② `git commit -m "...照 scripts/github-arm.sh 解保險..."` =關鍵字在**commit 訊息**裡
|
||
# ⇒ **誤攔比漏攔更容易殺死一道閘**:被擋得莫名其妙,人就會想辦法繞過它。
|
||
# ⇒ 改成「先排除明確不發佈的動作,再比對關鍵字」。
|
||
#
|
||
# 一律不發佈的動作(讀取、寫本地版控、查狀態)——先放行,免得關鍵字出現在訊息裡就中槍
|
||
# 🔴 2026-08-12:這組「開頭是唯讀工具就放行」的豁免**本身是穿牆路**——與
|
||
# prod-write-guard 同款(那支當天已修,這支漏了)。實測:
|
||
# echo go && scripts/github-arm.sh "出貨" 30
|
||
# 開頭是 `echo ` ⇒ 整條 exit 0 ⇒ **真的執行解保險腳本卻一聲不吭**。
|
||
# 前面接一個 echo/cat 就能繞過任何閘。
|
||
# ⇒ 命令裡只要出現投送工具,就不吃「開頭豁免」,交給下面的
|
||
# HAS_PUBLISH_ACTION 逐條判(它看的是「在不在命令位置」,不是「有沒有出現」)。
|
||
case "$CMD" in
|
||
*"git push"*|*wrangler*|*ship.mjs*|*github-arm.sh*|*publish-*|*curl*|*wget*|*httpie*|*" acr "*) ;;
|
||
sed\ *|cat\ *|grep\ *|head\ *|tail\ *|wc\ *|less\ *|ls\ *|awk\ *|rg\ *|echo\ *) exit 0 ;;
|
||
*"git commit"*|*"git add"*|*"git tag"*|*"git stash"*) exit 0 ;;
|
||
*"git status"*|*"git log"*|*"git diff"*|*"git show"*|*" --dry-run"*) exit 0 ;;
|
||
esac
|
||
|
||
# 🔴 2026-08-12 第三次誤攔(同一晚第三道閘犯同一個病):
|
||
# 上面那組豁免只認「指令**開頭**是唯讀工具」,所以
|
||
# `BASE="https://cdn.jsdelivr.net/gh/.../arcrun-rag-bundles@<sha>"; curl -s "$BASE/manifest.json"`
|
||
# ——一個**抓公開 CDN 檔案來查證**的純 GET——因為開頭是變數指派而被當成出貨擋下。
|
||
# 更糟:連 `grep ... stage-before-prod-guard.sh`(**讀本閘自己的原始碼**)都被擋,
|
||
# 因為那行指令裡出現了關鍵字 ⇒ **這道閘無法被檢查、也無法被測試**。
|
||
# ⇒ 本閘第 44 行自己早就寫了「誤攔比漏攔更容易殺死一道閘」。這次攔到自己頭上三回。
|
||
#
|
||
# 真正的判準不是「有沒有提到出貨物件」,是「**這條指令有沒有真的在發佈**」。
|
||
# 讀 manifest、讀 bundle 內容、讀腳本原始碼 —— 全是查證,那正是出貨前**該**做的事。
|
||
HAS_PUBLISH_ACTION=false
|
||
# 剪掉唯讀工具的 -d 旗標(tr -d/cut -d=…),免得被當成 curl 的 --data(prod-write-guard 同款教訓)
|
||
CMD_W=$(printf '%s' "$CMD" | sed -E 's/(^|[|;&( ])(tr|cut|sort|uniq|date|xargs|paste|join|du|split|comm)[[:space:]]+-d/\1\2 __RO_D__/g')
|
||
case "$CMD_W" in
|
||
*"wrangler deploy"*|*"wrangler versions deploy"*|*"wrangler publish"*) HAS_PUBLISH_ACTION=true ;;
|
||
# 🔴 2026-08-12 第六次誤攔(又是我自己的改動造成的):原本「命令含 git push
|
||
# 就算發佈」,加上我新加的「有投送工具就不吃開頭豁免」之後,
|
||
# **一個推 Gitea 的普通 commit,只因為 commit 訊息裡提到 github-arm,就被擋**
|
||
# ——那正是本檔第 43 行說「已經修好」的情境(關鍵字在 commit 訊息裡)復發。
|
||
# ⇒ `git push` 只有在**推 bundle repo** 時才是出貨;推自己的 repo 不是。
|
||
*"git push"*)
|
||
case "$CMD_W" in *bundles*) HAS_PUBLISH_ACTION=true ;; esac
|
||
;;
|
||
*"acr push"*|*"acr recipe push"*|*"acr update"*) HAS_PUBLISH_ACTION=true ;;
|
||
*"ship.mjs"*|*"publish-mirror"*|*"publish-bundles"*) HAS_PUBLISH_ACTION=true ;;
|
||
# 直接「執行」解保險腳本(不是讀它、不是在訊息裡提到它)
|
||
#
|
||
# 🔴 2026-08-12 第四次誤攔——**這條是我自己當天稍早寫的,pattern 太鬆**:
|
||
# 原本寫 `*sh\ *github-arm.sh*`,而 glob 的 `*` 跨越整條命令
|
||
# ⇒ 一條「讀取」指令只要**在別處出現過 "sh " 再出現 github-arm.sh** 就中:
|
||
# echo "=== github-arm.sh 怎麼判過期 ==="; grep -nE '…' scripts/github-arm.sh
|
||
# (前半的 `github-arm.sh ` 提供了 "sh ",後半提供了檔名)⇒ 純讀取被當成發佈。
|
||
# ⇒ 修別人的閘時自己製造了第四次誤攔。教訓與前三次同款:
|
||
# **判準要看「它在不在命令位置」,不是「字串有沒有出現過」。**
|
||
*github-arm.sh*)
|
||
# 只有出現在**命令起始位置**(行首/`;`/`|`/`&&` 之後,可帶 bash/sh 前綴)才算執行
|
||
if printf '%s' "$CMD_W" | grep -qE '(^|[;&|]|&&|\|\|)[[:space:]]*((bash|sh|zsh)[[:space:]]+)?(\./)?([A-Za-z0-9_./-]*/)?github-arm\.sh([[:space:]]|$)'; then
|
||
HAS_PUBLISH_ACTION=true
|
||
fi
|
||
;;
|
||
# HTTP 寫入動詞
|
||
*"-X POST"*|*"-X PUT"*|*"-X PATCH"*|*"-X DELETE"*|*"--data"*|*" -d "*) HAS_PUBLISH_ACTION=true ;;
|
||
esac
|
||
[ "$HAS_PUBLISH_ACTION" = true ] || exit 0
|
||
|
||
# 剩下的才比對「會讓封測者拿到東西」的關鍵字
|
||
case "$CMD" in
|
||
*arcrun-rag-bundles*|*github-arm*|*publish-github*) ;;
|
||
*) exit 0 ;;
|
||
esac
|
||
|
||
# ── 條件 ①:leo 親自解保險(人閘,我無法自造)─────────────────────────────
|
||
#
|
||
# 🔴 leo 2026-08-08:「**現在到 github 就是同 arm,推到 prod 也應該視同 arm。**」
|
||
# ⇒ 「推 prod」與「寫 GitHub」是**同一級的動作**,都要 leo 親手解保險。
|
||
# 先前 D20 只把 arm 綁在「寫 GitHub」上,於是「推 prod」這件事本身沒有人閘——
|
||
# 而真正該被守住的是**發佈**,不是「用了哪個 host」。
|
||
# 🔴 2026-08-08 差點出事:原本寫 `ARMED="$CLAUDE_PROJECT_DIR/.github-armed"`,
|
||
# 在 CLAUDE_PROJECT_DIR 未設的環境下被 `set -u` 當場打死 ⇒ 腳本 **exit 1**。
|
||
# 而 exit 1 **不擋**(只有 exit 2 才擋)⇒ 這道閘會**靜默放行**。
|
||
# ⇒ 一道「失敗時自動失效」的安全閘比沒有更危險:它讓人以為有守。
|
||
# **安全閘的預設必須是 fail-closed。**
|
||
PROJ="${CLAUDE_PROJECT_DIR:-$(pwd)}"
|
||
ARMED="$PROJ/.github-armed"
|
||
if [ ! -f "$ARMED" ]; then
|
||
cat >&2 <<'AEOF'
|
||
🔫 出貨人閘:推 prod = 發佈,**視同 arm,要 leo 親自解保險**。
|
||
|
||
【leo 2026-08-08】「現在到 github 就是同 arm,**推到 prod 也應該視同 arm**。」
|
||
⇒ 該守的是「**發佈**」這個動作本身,不是「用了哪個 host」。
|
||
|
||
這一步**不是我能自己過的**。請 leo 整行貼:
|
||
|
||
~/Documents/tech_projects/InkStoneCo/scripts/github-arm.sh "<出貨說明>" 30
|
||
|
||
📌 在請他解保險之前,先確認 stage 已經驗過(下面那道閘),
|
||
否則等於請他為一個沒驗過的東西按發射鈕。
|
||
AEOF
|
||
exit 2
|
||
fi
|
||
|
||
# ── 條件 ②:stage 先驗過(我的責任,但不能替代條件 ①)───────────────────
|
||
#
|
||
# 🔴 2026-08-08 出貨當下實撞:本段**訊息與實作互相矛盾**——
|
||
# 下面那段說明叫人 `touch /tmp/.stage-verified`,
|
||
# 但這裡讀的是**檔案內容**當時間戳 ⇒ `touch` 產生空檔 ⇒ `cat` 空 ⇒ 退回 0
|
||
# ⇒ `now - 0` 恆大於 6 小時 ⇒ **照它自己的說明做,永遠過不了**。
|
||
# 這正是那天燒掉一整天的同一個病:**宣稱的做法 ≠ 真正的做法**。
|
||
# ⇒ 改成兩者都認:內容有數字就用內容,沒有就用 mtime(`touch` 因此真的有效)。
|
||
#
|
||
# 另外認 leo 親手蓋的章(scripts/stage-ok.sh 產生),那是比我自評更強的訊號。
|
||
for STAMP in /tmp/.stage-ok-by-leo /tmp/.stage-verified; do
|
||
[ -f "$STAMP" ] || continue
|
||
now=$(date +%s)
|
||
# 🔴 只有「整行就是一個時間戳」才採信內容。
|
||
# 別用 tr 抽數字——JSON 章 `{"sha":"abc123","release":"1.4.24",…}` 抽出來會變成
|
||
# 一個**看似合理但錯得離譜**的舊時間戳(1970 年),比 fail-open 更難察覺。
|
||
t=$(head -1 "$STAMP" 2>/dev/null | tr -d '[:space:]')
|
||
# 內容不是時間戳(例:被 touch 成空檔)⇒ 退回檔案修改時間,別誤判成「沒驗過」
|
||
case "$t" in ''|*[!0-9]*) t=0;; esac
|
||
# 🔴 同日再撞一次 fail-open:ship.mjs 把這個章從「純時間戳」升級成 JSON 之後,
|
||
# 抽數字會得到天文數字 ⇒ `now - t` 是**負數** ⇒ `< 21600` 成立 ⇒ **永遠放行**。
|
||
# ⇒ 只有「不晚於現在」的時間戳才算數;不合理就退回 mtime,別當成通過。
|
||
if [ "$t" -eq 0 ] || [ "$t" -gt "$now" ]; then
|
||
t=$(stat -f %m "$STAMP" 2>/dev/null || stat -c %Y "$STAMP" 2>/dev/null || echo 0)
|
||
fi
|
||
[ "$t" -gt 0 ] && [ "$((now - t))" -lt 21600 ] && exit 0 # 6 小時內驗過 stage → 放行
|
||
done
|
||
|
||
cat >&2 <<'EOF'
|
||
🚦 出貨閘:你正要動 **prod 出貨鏈**,但這 6 小時內沒有 stage 驗證紀錄。
|
||
|
||
【leo 2026-08-08】「現在因為**開始封測**,不直接打到 prod,而是先打到昨天建的 stage⋯⋯
|
||
因為**推 prod 就發佈了**,雖然現在人不多,但**要謹慎**。」
|
||
|
||
【為什麼有這道閘】`ship-check` 的步驟本身就寫成直達 prod,D20 那道只擋「寫 GitHub」、
|
||
不擋「未經 stage 就發佈」⇒ 光「知道有 stage」對行為零作用。
|
||
|
||
正確順序(stage 先行):
|
||
1. 打 staging bundle → 推 Gitea `arcrun-rag-bundles-staging`(不碰 GitHub,無 D20 閘)
|
||
2. 用 staging 安裝器實裝到測試實例
|
||
https://arcrun-rag-installer-staging.uncle6-me.workers.dev
|
||
3. 走一次**封測者真的會走的那條路**,貼出實測輸出
|
||
4. 過了才動 prod
|
||
|
||
⚠️ 身分:leo 2026-07-25 令「測試一律用 youlin,別拿 leo21c 當探針(會製造假信號)」
|
||
⇒ 部署前先 `acr whoami` 確認身分。
|
||
|
||
驗過了 ⇒ `touch /tmp/.stage-verified` 後重送,並在回覆裡**貼 stage 的實測輸出**
|
||
(「我測過了」不算——貼指令與它吐出來的東西)。
|
||
EOF
|
||
exit 2
|