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>
This commit is contained in:
2026-08-20 11:41:46 +08:00
commit c2638668e3
85 changed files with 11879 additions and 0 deletions
+211
View File
@@ -0,0 +1,211 @@
#!/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