#!/bin/sh # gitea-arm-to-github-armed.sh — 把「leo 在 Gitea 票上的核准」轉成出貨線認得的 `.github-armed` # # 🔴 這支存在的理由(2026-08-13 實撞): # arm 機制上線當天,總管把它接到了 `main-and-prod-push-guard.sh`(推 main 那道閘), # **但出貨線 `ship.mjs` 的 preflight 只認舊的 `.github-armed`** # ⇒ leo 在票上回了碼、`gitea-arm-check.sh` 回 ARMED,**出貨照樣斷在第 1 站**。 # ⇒ 總管審 PR 時查了「機制本身安不安全」,**沒查「它有沒有接到真正擋路的地方」**。 # 同款形狀:做好了一半,而那一半不是會擋住人的那半。 # # 🔴 這支**不是**繞過 D20,它是同一道閘的另一個入口: # `.github-armed` 代表的權威來自「leo 親自做了一個機器做不到的動作」。 # 舊入口=他在終端機打字;新入口=他在 Gitea 票上回覆一組碼。 # **兩者的不可偽造性來源不同,但都不在機器手上**: # · 終端機:機器碰不到那個鍵盤 # · Gitea:機器寫不出作者是 `Leo` 的留言(2026-08-13 身分分離完成,總管親手攻擊驗過) # # ⛔ **唯一的安全條件**:本檔**只在 `gitea-arm-check.sh` 成功時**才寫出保險。 # 那支是 fail-closed 的(沒回覆/過期/已消耗/作者不是 Leo/Gitea 不通 → 全部 exit 1), # 而且核准者與頻道票**寫死在 `lib/gitea-arm-common.sh`**,環境變數改不動。 # ⇒ 拿掉那個條件,這支就變成「AI 自己批准自己發佈」——**那正是 D20 要擋的事**。 set -eu SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) # shellcheck source=lib/gitea-arm-common.sh . "$SCRIPT_DIR/lib/gitea-arm-common.sh" # ── 保險寫在哪:**讀的人在哪讀,就寫在哪**(inkstone/ISEP#156,2026-09-17 實撞)────── # # 🔴 舊版寫 `ROOT=$(dirname $0)/..`(腳本自己的上一層)。ISEP 還住在 InkStoneCo 裡時兩者剛好 # 同一個位置;**改成 plugin 發佈後,腳本住在 `~/.claude/plugins/cache/inkstone/isep//`** # ⇒ 保險被寫進 plugin 快取資料夾,印「✅ 保險已解」,而出貨線與每一道閘都讀不到 # ⇒ leo 回了碼、出貨照樣卡住,總管手動把檔案搬過去才通(inkstone/arcrun-rag#202 c7598)。 # **一句「已解」卻沒解,比單純失敗更糟——它讓 leo 以為自己的動作生效了。** # # ⇒ 改用 `gitea_arm_proj_dir`(=`${CLAUDE_PROJECT_DIR:-$(pwd)}`),理由是**跟讀的人對齊**: # · main-and-prod-push-guard.sh/github-contact-guard.sh/stage-before-prod-guard.sh # 全部讀 `${CLAUDE_PROJECT_DIR:-$(pwd)}/.github-armed` # · ship.mjs 讀 `InkStoneCo/.github-armed`(總管 session 的 CLAUDE_PROJECT_DIR 就是它) # · 本檔自己的核准來源(`gitea-arm-check.sh` 讀的 token 與待核請求)本來就住在這裡 # ⇒ 專案根猜錯時 check 會先因為讀不到 `.env`/待核請求而 fail-closed,**不會寫到錯的地方** # 不要再改回「腳本的位置」:腳本住哪是安裝方式決定的,保險住哪是讀的人決定的,兩者無關。 ARMED="$(gitea_arm_proj_dir)/.github-armed" # ── 唯一的授權來源:leo 在 Gitea 票上的回覆 ──────────────────────────── # 不捕捉、不吞錯——check 失敗就讓它把原因印到 stderr 並中止本支。 # 🔴 `--consume` 是必要的(2026-08-16 起 check 預設唯讀):本檔會**寫出保險**, # 那就是「放行」本身 ⇒ 這一次核准必須當場作廢,否則同一組碼能重複解保險。 OUT=$("$SCRIPT_DIR/gitea-arm-check.sh" --consume) || { echo "" >&2 echo "⛔ 沒有取得 leo 的核准 ⇒ 不寫保險(fail-closed)" >&2 # 印絕對路徑:相對的 `scripts/…` 會對貼上那個人的 cwd 解析,站在 InkStoneCo 會叫到舊複本(ISEP#112 同病) echo " 先跑:$SCRIPT_DIR/gitea-arm-request.sh <票號> \"<要做什麼>\",把代碼給 leo 回在那張票上。" >&2 exit 1 } # `.github-armed` 的格式(照 scripts/github-arm.sh:93): # 第 1 行:到期 epoch/第 2 行:任務說明/第 3 行:armed_at=… MISSION=$(printf '%s' "$OUT" | sed -n 's/^ARMED: //p' | head -1) [ -n "$MISSION" ] || MISSION="(gitea-arm 核准,未帶說明)" NOW=$(date +%s) EXPIRY=$((NOW + 1800)) # 30 分鐘,與 gitea-arm-request 的時效一致 printf '%s\n%s\n%s\n' \ "$EXPIRY" \ "$MISSION" \ "armed_at=$(date '+%Y-%m-%d %H:%M:%S')(來源:Gitea 票 leo 核准,非終端機)" \ > "$ARMED" echo "✅ 保險已解(來源:leo 在 Gitea 票上的核准)" echo " 寫在:$ARMED" echo " 任務:$MISSION" echo " 有效至:$(date -r "$EXPIRY" '+%Y-%m-%d %H:%M:%S' 2>/dev/null || echo "$EXPIRY")" echo " 提前上保險:rm $ARMED"