#!/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@"; 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