Files
ISEP/scripts/gitea-arm-check.sh
Leo c2638668e3 ISEP 0.1.0:環境設定收成一個 plugin,本機與雲端共用一份
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>
2026-08-20 11:41:46 +08:00

212 lines
11 KiB
Bash
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.
#!/bin/bash
# gitea-arm-check.sh — 核對 leo 有沒有在「該請求指定的那張票」上回覆某個 ARM 請求的代碼
#
# 用法:
# scripts/gitea-arm-check.sh # 掃描所有還沒過期的本地待核請求(可能橫跨多張票)
# scripts/gitea-arm-check.sh ARM-xxxxxxxx # 只核對這一組
#
# 判定放行的三個條件,**缺一不可**:
# ① 這組 nonce 從沒被消耗過(防重放)、且還沒過期(範圍與時效)——本地就能判,不必碰網路
# ② 留言作者的 login 精確等於 "Leo"(不是 id、不是顯示名——那些會變)
# ③ 留言內文含這組 nonce,且晚於「機器貼出請求」的那則留言、且出現在**該請求貼出時指定的那張票**
# (票號隨每個請求存在本地待核檔的 `issue` 欄位裡,2026-08-16 起不再是寫死的單一頻道票,
# 見 lib/gitea-arm-common.sh 檔頭;別的票再怎麼有 Leo 回過相似字串的留言都不算數,
# 因為根本不會被拿去查——每個 nonce 只查它自己那張票)
#
# 成功:印 "ARMED: <mission>"、把 nonce 標記已消耗、刪掉本地待核檔、exit 0(單次用完即失效)
# 失敗(沒有/過期/被消耗過/Gitea 打不到/回應解不出來):印原因到 stderr、exit 1
# ——**fail-closed**:任何看不懂的狀況都當失敗,不放行。
#
# 🔴 順序刻意是「先本地過濾,才打網路」:
# 過期/已消耗這兩種本地就能判定,**不該因為 Gitea 打不到而連本地清理都做不了**
# (早期版本把網路呼叫放最前面,測試才抓到:token 失效時,過期的待核檔永遠不會被清掉,
# 因為程式在走到「清掉它」那行之前就已經因為 fail-closed 提前 exit 了)。
#
# 🔴 這支只在「機器需要解閘的當下」被閘呼叫一次——不是排程輪詢(D20 紅線)。
# 呼叫方一次只打一發 GET **對每一張還有待核請求的票**(不同票各打一次,
# 同一張票不管上面掛幾組 nonce 只打一次、共用回應),不建任何迴圈/背景行程來等 leo 回覆。
set -uo pipefail
SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
# shellcheck source=lib/gitea-arm-common.sh
. "$SCRIPT_DIR/lib/gitea-arm-common.sh"
STATE_DIR=$(gitea_arm_state_dir)
PENDING_DIR="$STATE_DIR/pending"
# ── --peek:只看有沒有核准,**不消耗**leo 2026-08-16 立)───────────────────
#
# 🔴 為什麼要有這個模式(同日實撞,而且是「發生過沒解決,下次又踩」的那種):
# 總管為了「先確認 leo 貼了碼」單獨跑了一次本檔 ⇒ **那一次就把核准消耗掉了**
# ⇒ 真正要用的 gitea-arm-to-github-armed.sh 再來拿時已經沒有待核請求
# ⇒ **leo 的動作被浪費一次,他得再貼一次。**
#
# leo 原話:「這不是問題,問題是**已經發生好幾次了,你發生過以後沒有解決,下次還是碰到**。
# 你可以註記只能一次,要不然**改成可以一次檢查一次發射**。」
# ⇒ 選後者:**檢查不消耗,發射才消耗。**
#
# ⛔ 防重放在 peek 模式下**照樣生效**——已被消耗過的 nonce peek 也不會回 ARMED
# 否則 peek 就變成繞過重放保護的後門。
# 🔴 2026-08-16 第二輪(leo 當場點破,同日第一輪的修是錯方向):
# leo 原話:「**你檢查是否有貼可以用查看 issue 的 API 不應該會用掉,所以你查看的方式不對。**」
# ⇒ 讀留言是 GET,本來就不該有副作用。「消耗」是本檔自己黏在讀取上的**本地**記帳。
# ⇒ 所以正解不是「加一個 --peek 選配」(第一輪那樣做=預設仍然咬人,
# 只有記得加旗標的人不被咬),而是**把預設反過來**:
#
# 不帶旗標 = 只讀,永不消耗 ← 任何人手動探一眼都安全
# --consume = 真的要發射,才消耗 ← 只有「放行那一步」自己會帶
#
# 這也正是 mistakes.md:237 兩天前就寫著的「待修:check 應該要有唯讀模式」,
# 以及 231 行「想知道 leo 貼了沒 → 讀票,不要用 check 去探」。
# **寫下來了,08-16 還是照撞一次** ⇒ 所以改成機制上撞不到,不是再寫一條提醒。
#
# ⛔ 防重放不變:已消耗過的 nonce 在唯讀模式下**照樣不回 ARMED**,
# 否則唯讀就變成繞過重放保護的後門。
#
# 🐛 第一輪的實際 bug(也在這裡一起修):舊碼 `WANT_NONCE="${1:-}"` 會把 `--peek`
# 當成「要篩的 nonce」吃掉 ⇒ 一個都不匹配 ⇒ 靜靜回「沒有還活著的待核請求」。
# ⇒ 旗標與 nonce 現在在**同一個迴圈**裡分開解析,旗標永遠不會被當成 nonce。
CONSUME=0
WANT_NONCE=""
for _a in "$@"; do
case "$_a" in
--consume) CONSUME=1 ;;
--peek) : ;; # 相容舊呼叫;現在唯讀本來就是預設,這個旗標等同不做事
-*) echo "❌ 不認得的參數:$_a(只接受 --consume--peek<nonce>" >&2; exit 1 ;;
*) [ -z "$WANT_NONCE" ] && WANT_NONCE="$_a" ;;
esac
done
CONSUMED_LOG=$(gitea_arm_consumed_log)
NOW=$(date +%s)
shopt -s nullglob
PENDING_FILES=("$PENDING_DIR"/*.json)
shopt -u nullglob
if [ "${#PENDING_FILES[@]}" -eq 0 ]; then
echo "沒有待核的 ARM 請求(先跑 scripts/gitea-arm-request.sh" >&2
exit 1
fi
# ── 第一遍:純本地過濾,完全不碰網路 ──────────────────────────────────
# 篩出「還活著、還沒被用過」的候選;順手清掉壞掉/過期/已消耗的本地檔。
# 🔴 不用 `declare -A`(本機 /bin/bash 是 3.2,沒有關聯陣列)——
# 改用一個 TSV 暫存檔存 nonce/issue/request_created_at/mission,逐行讀。
# 每個請求各自帶著自己的票號(issue 欄位,2026-08-16 起不再是單一頻道票)。
ELIGIBLE_TSV=$(mktemp)
RESP_DIR=$(mktemp -d)
trap 'rm -f "$ELIGIBLE_TSV"; rm -rf "$RESP_DIR"' EXIT
for PF in "${PENDING_FILES[@]}"; do
[ -f "$PF" ] || continue
NONCE=$(jq -r '.nonce // empty' "$PF" 2>/dev/null)
if [ -z "$NONCE" ]; then
echo "⚠️ 略過壞掉的待核檔:$PF" >&2
continue
fi
if [ -n "$WANT_NONCE" ] && [ "$NONCE" != "$WANT_NONCE" ]; then
continue
fi
MISSION=$(jq -r '.mission // empty' "$PF" 2>/dev/null)
ISSUE=$(jq -r '.issue // empty' "$PF" 2>/dev/null)
# 2026-08-16:請求可能貼在同 org 的別的 repo(出貨票在 arcrun-rag)。
# 舊記錄沒有 .repo 這欄 → 沿用預設 InkStoneCo,不破壞相容。
_PR=$(jq -r '.repo // empty' "$PF" 2>/dev/null)
[ -n "$_PR" ] && { gitea_arm_set_repo "$_PR" || continue; }
EXPIRES_AT=$(jq -r '.expires_at // empty' "$PF" 2>/dev/null)
REQUEST_CREATED_AT=$(jq -r '.request_created_at // empty' "$PF" 2>/dev/null)
if ! gitea_arm_valid_issue "$ISSUE"; then
echo "⚠️ 略過壞掉的待核檔(issue 缺失或非數字,$PF)——這份請求檔是舊版格式或損毀,不猜票號" >&2
continue
fi
case "$EXPIRES_AT" in ''|*[!0-9]*) echo "⚠️ 略過壞掉的待核檔(expires_at 非數字):$PF" >&2; continue;; esac
if [ -z "$REQUEST_CREATED_AT" ]; then
echo "⚠️ 略過壞掉的待核檔(缺 request_created_at):$PF" >&2
continue
fi
# 過期:清掉本地待核檔(沒被用過,不算消耗),繼續看下一個
if [ "$NOW" -ge "$EXPIRES_AT" ]; then
echo "⌛ $NONCE#$ISSUE)已過期,清掉待核請求" >&2
rm -f "$PF"
continue
fi
# 防重放:這個 nonce 先前是否已被消耗過(即使 Gitea 上那則留言還在,也不能再解一次)
if [ -f "$CONSUMED_LOG" ] && grep -qF "$(printf '%s\t' "$NONCE")" "$CONSUMED_LOG" 2>/dev/null; then
echo "🚫 $NONCE#$ISSUE)已經被用過一次,不能重放" >&2
rm -f "$PF"
continue
fi
printf '%s\t%s\t%s\t%s\n' "$NONCE" "$ISSUE" "$REQUEST_CREATED_AT" "$MISSION" >> "$ELIGIBLE_TSV"
done
if [ ! -s "$ELIGIBLE_TSV" ]; then
echo "沒有還活著、還沒用過的待核請求 → 不放行" >&2
exit 1
fi
TOKEN=$(gitea_arm_token) || { echo "❌ 讀不到 GITEA_TOKEN_CLAUDE_CODE → fail-closed,不放行" >&2; exit 1; }
# ── 第二遍:對每一張出現在 ELIGIBLE_TSV 的票各打一次 GET(同票只打一次,跨票各自獨立)──
for ISSUE in $(cut -f2 "$ELIGIBLE_TSV" | sort -u); do
RESP=$(curl -s -w '\n%{http_code}' --max-time 15 \
-H "Authorization: token $TOKEN" \
"$GITEA_ARM_API/repos/$GITEA_ARM_OWNER/$GITEA_ARM_REPO/issues/$ISSUE/comments?limit=100")
CURL_RC=$?
HTTP_CODE=$(printf '%s' "$RESP" | tail -1)
RESP_BODY=$(printf '%s' "$RESP" | sed '$d')
if [ "$CURL_RC" -ne 0 ] || [ "$HTTP_CODE" != "200" ]; then
echo "❌ #$ISSUE:Gitea 打不到/回應非 200curl_rc=$CURL_RC, http=$HTTP_CODE)→ 該票所有待核請求 fail-closed" >&2
continue
fi
if ! printf '%s' "$RESP_BODY" | jq -e 'type == "array"' >/dev/null 2>&1; then
echo "❌ #$ISSUE:Gitea 回應不是預期的陣列格式 → 該票所有待核請求 fail-closed" >&2
continue
fi
printf '%s' "$RESP_BODY" > "$RESP_DIR/$ISSUE.json"
done
# ── 第三遍:逐一比對,每組 nonce 只看它自己那張票抓回來的留言 ──────────────
RESULT=1
while IFS=$'\t' read -r NONCE ISSUE AFTER MISSION; do
[ -n "$NONCE" ] || continue
RESP_FILE="$RESP_DIR/$ISSUE.json"
if [ ! -f "$RESP_FILE" ]; then
echo "…$NONCE#$ISSUE):該票打不到,不放行" >&2
continue
fi
# 核心判定:作者精確是 Leo、內文含這組 nonce、時間晚於請求留言——
# 且比對只在 $RESP_FILE 這一張票的留言範圍內,不會混到別張票
MATCH=$(jq -r \
--arg nonce "$NONCE" --arg after "$AFTER" --arg who "$GITEA_ARM_APPROVER_LOGIN" '
[ .[] | select(.user.login == $who) | select(.body | contains($nonce)) | select(.created_at > $after) ]
| sort_by(.created_at) | .[0].id // empty
' "$RESP_FILE")
PF="$PENDING_DIR/$NONCE.json"
if [ -n "$MATCH" ]; then
if [ "$CONSUME" -eq 1 ]; then
printf '%s\t%s\t%s\t#%s\n' "$NONCE" "$(date '+%Y-%m-%d %H:%M:%S')" "$MISSION" "$ISSUE" >> "$CONSUMED_LOG"
rm -f "$PF"
echo "ARMED: $MISSION"
else
# 預設:只讀,不消耗——待核請求留著,等真正要放行的那一步帶 --consume 才用掉
echo "ARMED(唯讀): $MISSION"
echo " ⚠️ 唯讀模式,**核准還在**(沒有被用掉)。真正要放行的那一步會自己帶 --consume。" >&2
fi
RESULT=0
[ -n "$WANT_NONCE" ] && break
else
echo "…$NONCE#$ISSUE)還沒等到 Leo 的回覆" >&2
fi
done < "$ELIGIBLE_TSV"
exit $RESULT