c2638668e3
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>
104 lines
5.7 KiB
Bash
Executable File
104 lines
5.7 KiB
Bash
Executable File
#!/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 一個常數,OWNER/REPO/APPROVER_LOGIN 三個維持寫死、
|
||
# 不接受環境變數覆蓋,且 ISSUE 也只能透過腳本參數指定,不接受環境變數。
|
||
|
||
# ── 安全邊界常數(OWNER/REPO/APPROVER_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)"
|
||
}
|