Files
ISEP/scripts/lib/gitea-arm-common.sh
T
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

104 lines
5.7 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/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)"
}