c2638668e3
leo 2026-08-20:「同一個 plugin 你用,薄殼也用,保證兩邊同步」
「我要你幫雲端做薄殼,永遠都有問題,你要做的就是這組設定
你自己可以 dogfooding」
搬進來:41 支 hook(51 條註冊)/7 支 command/2 支 skill/23 支腳本。
不搬 .env、wiki、docs——那些是知識不是環境。
51 條 hook 路徑全部從 $CLAUDE_PROJECT_DIR/.claude/hooks/ 改成 ${CLAUDE_PLUGIN_ROOT}/hooks/,
零漏網。那正是薄殼一直壞掉的根:雲端 cwd 不是真身,寫死路徑就斷。
尚未驗證:Claude Code 能不能從私有 Gitea repo 裝 marketplace(要憑證)。
下一步就是在本機實際裝一次,通了才動雲端 bootstrap.sh。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
194 lines
12 KiB
Bash
Executable File
194 lines
12 KiB
Bash
Executable File
#!/bin/sh
|
||
# 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
|
||
|
||
# 只攔「會讓封測者拿到東西」的動作: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
|