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
+103
View File
@@ -0,0 +1,103 @@
#!/bin/sh
# gitea-arm-common.sh — 共用設定與函式,給 gitea-arm-request.sh / gitea-arm-check.sh 用。
# 只放常數與純函式,不做任何網路呼叫、不產生副作用(source 它必須零風險)。
#
# 背景:把「leo 解保險」從終端機搬到 Gitea 票上(leo 2026-08-13
# 「如果你會被通知,就不需要通過 terminal 來 arm 了」)。原始設計見:
# https://git.uncle6.me/inkstone/InkStoneCo/issues/34
#
# 🪦 2026-08-16 廢除「單一頻道票」設計(leo:「我不要把所有的票都放在一個 issues,
# 它們應該分散在每個 issues 裡,不然就難 track 歷史記錄」,issues/34#issuecomment-2804)。
# **票號改由呼叫端在每次呼叫時指定**——解哪張票的保險,請求與回碼就貼在那張票上,
# 不再有寫死的 GITEA_ARM_ISSUE 常數。#34 本身保留 open(33 則舊留言是歷史,不刪),
# 但不再是新請求的預設去處。
#
# 🔴 這不是重新打開 2026-08-13 那次「五個常數全部寫死」的舊爭議(history-first-guard
# 會問這個,先在這裡回答):那次總管的攻擊測試是
# `GITEA_ARM_ISSUE=999 OWNER=claude-code`——**同時**換成機器自己名下的 repo
# bot 在自己的 repo 有 admin 權限,可以邀請/建立一個顯示名叫「Leo」的假帳號來自問自答)。
# OWNER/REPO 才是那個攻擊真正利用的洞:**只要留在 inkstone/InkStoneCo 這個 org 裡,
# bot 沒有 admin 權限造不出第二個 login 是 `Leo` 的帳號,票號本身換到哪一張都一樣安全**
# ——核心判定永遠是「留言作者的 login 精確等於 Leo」,跟去哪張票找留言無關。
# 本次只鬆 ISSUE 一個常數,OWNERREPOAPPROVER_LOGIN 三個維持寫死、
# 不接受環境變數覆蓋,且 ISSUE 也只能透過腳本參數指定,不接受環境變數。
# ── 安全邊界常數(OWNERREPOAPPROVER_LOGIN,不接受任何覆蓋)───────────
#
# 🔴🔴 2026-08-13 總管審查抓到的洞(修正前的版本用 `: "${VAR:=default}"`
# 可被環境變數覆蓋):**「誰算是 leo」絕對不能是可覆蓋的設定。**
# 舊寫法下 `GITEA_ARM_APPROVER_LOGIN=claude-code scripts/gitea-arm-check.sh`
# 就能讓機器核准自己——比冒充/重放/過期/無回覆/Gitea 不通這五種都好繞,
# 因為設環境變數對總管與每一條 subagent 都是零成本、零痕跡。
# ⇒ 這幾個常數全部**直接賦值、不接受任何外部覆蓋**(沒有 `:=`、沒有讀 env)。
# 「誰是核准者/打哪個 repo」這種安全邊界要嘛寫死在程式碼裡走 PR 審查才能改,
# 要嘛就是可以被繞過的裝飾——**難改在這裡是特性,不是缺點**。
# (測試因此不能再靠改這幾個常數注入假狀態;改用「用真的無效 token 打真端點」
# 去驗 fail-closed,見 gitea-arm-check.test.sh 與 gitea-arm-check.sh 檔頭說明。)
GITEA_ARM_OWNER="inkstone"
# 🔓 2026-08-16:REPO 從「寫死」改成「預設值,只能由**腳本參數**指定」——理由與 ISSUE 同源。
# 本檔上方那段自己寫著:那次攻擊真正利用的洞是「換成**機器自己名下**的 repo」
# bot 在自己的 repo 有 admin,能造一個顯示名叫 Leo 的假帳號自問自答)。
# ⇒ 真正的安全邊界是 **OWNER**,不是 REPO:只要留在 `inkstone` 這個 org 底下,
# bot 在任何一個 repo 都沒有 admin,造不出第二個 login 是 `Leo` 的帳號。
# 實際需求(同日):出貨票在 `inkstone/arcrun-rag#115`,而解保險的請求依 leo 的規矩
# 要貼在「它解的那張票」上 ⇒ 寫死 InkStoneCo 會讓請求貼不到出貨票(實測 HTTP 500)。
# 🔴 **OWNER 仍然寫死、仍然不接受任何覆蓋**——那一條沒有鬆。
GITEA_ARM_REPO="InkStoneCo" # 預設;由 gitea_arm_set_repo() 依腳本參數覆寫,不讀 env
# gitea_arm_set_repo <repo名> —— 只接受 inkstone org 底下的 repo 名(純 [A-Za-z0-9._-])。
# 刻意不接受 owner/repo 形式:owner 是安全邊界,不給任何人指定的機會。
gitea_arm_set_repo() {
case "$1" in
''|*[!A-Za-z0-9._-]*)
echo "❌ repo 名只能是 inkstone org 底下的 repo(收到:$1" >&2
return 1;;
esac
GITEA_ARM_REPO="$1"
}
GITEA_ARM_API="https://git.uncle6.me/api/v1"
# 只認這個帳號名——不是 id、不是顯示名(leo 交代:這兩者會變)。
# 將來 leo 真的改帳號名 ⇒ 改這一行、走 PR 審查,不是設環境變數就生效。
GITEA_ARM_APPROVER_LOGIN="Leo"
# 票號合法性檢查:純數字才放行,避免呼叫端傳進奇怪字串打壞 URL
# (例如帶 `/` 或空白,會讓 curl 打到非預期的路徑)。
# 用法:gitea_arm_valid_issue "$ISSUE" && ... 或 if ! gitea_arm_valid_issue "$X"; then ...
gitea_arm_valid_issue() {
case "$1" in
''|*[!0-9]*) return 1 ;;
*) return 0 ;;
esac
}
gitea_arm_proj_dir() {
printf '%s' "${CLAUDE_PROJECT_DIR:-$(pwd)}"
}
# 讀 token:只回名字對應的值,不印出任何除了呼叫者要的東西。
# 讀不到 → 印空字串、回傳非 0(呼叫者要判斷,不能把空字串當成功)。
gitea_arm_token() {
ENV_FILE="$(gitea_arm_proj_dir)/.env"
if [ ! -f "$ENV_FILE" ]; then
printf ''
return 1
fi
TOK=$(grep '^GITEA_TOKEN_CLAUDE_CODE=' "$ENV_FILE" | head -1 | cut -d= -f2-)
if [ -z "$TOK" ]; then
printf ''
return 1
fi
printf '%s' "$TOK"
return 0
}
# 本機狀態目錄(gitignore 見 .claude/.gitignore)。
gitea_arm_state_dir() {
D="$(gitea_arm_proj_dir)/.claude/gitea-arm"
mkdir -p "$D/pending" 2>/dev/null
printf '%s' "$D"
}
gitea_arm_consumed_log() {
printf '%s/consumed.log' "$(gitea_arm_state_dir)"
}