From 2cd3af27647f44a5fa13b6513c6aa8405969d12d Mon Sep 17 00:00:00 2001 From: richblack Date: Thu, 20 Aug 2026 16:43:49 +0800 Subject: [PATCH] =?UTF-8?q?=E9=96=8B=E7=A5=A8=E7=9A=84=E5=81=B4=E9=96=80?= =?UTF-8?q?=E4=B9=9F=E5=B0=81=E4=B8=8A=EF=BC=9AGitea=20API=20=E7=9B=B4?= =?UTF-8?q?=E6=8E=A5=E9=96=8B=E7=A5=A8=E8=A6=81=E6=9C=89=E6=90=9C=E5=B0=8B?= =?UTF-8?q?=E6=88=B3=E8=A8=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit leo 2026-08-20 問「如何防止」。 規範早就有(leo 2026-08-16「開票前先去搜尋要開在哪裡」),閘也早就有—— 但它長在 scripts/ticket 這支工具裡,而總管當天走 API 這條側門, 一口氣開了 12 張與舊票重疊的票,事後全部只能關掉指回舊票。 ⇒ 同一個動作有兩條路,只封了一條。 本支封的是動作:Bash 指令只要打到 issues 集合端點且帶 POST,就要有新鮮的搜尋戳記 (與 scripts/ticket 共用 /tmp/.ticket-where-ok,30 分鐘)。 判準刻意簡單:端點 + 一個裸字 POST。原本寫成逐個比對 -X POST/"POST"/req("POST", 實測第二種就因為跳脫引號逃掉——那正是 leo 08-17 說的「自然語言的變體無限, blacklist 追不完」。端點已經確定是開票那條,純讀取不會出現 POST,一個字就夠。 實測 13 條:4 種該擋全擋、8 種不該擋(GET/留言/改標籤/關票/scripts\/ticket/ 文字提到/grep 自己/撈清單)全放行、有戳記時放行。 --- hooks/hooks.json | 4 ++ hooks/ticket-api-bypass-guard.sh | 81 ++++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+) create mode 100755 hooks/ticket-api-bypass-guard.sh diff --git a/hooks/hooks.json b/hooks/hooks.json index 37e845c..432cbda 100644 --- a/hooks/hooks.json +++ b/hooks/hooks.json @@ -36,6 +36,10 @@ { "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/release-tag-guard.sh" + }, + { + "type": "command", + "command": "${CLAUDE_PLUGIN_ROOT}/hooks/ticket-api-bypass-guard.sh" } ] }, diff --git a/hooks/ticket-api-bypass-guard.sh b/hooks/ticket-api-bypass-guard.sh new file mode 100755 index 0000000..f9d00a8 --- /dev/null +++ b/hooks/ticket-api-bypass-guard.sh @@ -0,0 +1,81 @@ +#!/bin/bash +# ticket-api-bypass-guard.sh — 開票的「側門」也要經過同一道搜尋閘 +# +# 來由(leo 2026-08-20 當場問「如何防止」): +# `scripts/ticket new` 早就強制「開票前先搜」(/tmp/.ticket-where-ok 戳記,30 分鐘失效)。 +# 但總管當天開了 12 張與舊票重疊的新票——因為他**沒用那支工具,直接打 Gitea API**。 +# ⇒ 規範有、閘也有,但閘長在「工具」上,而那個動作有兩條路,只封了一條。 +# leo:「你在讓事情複雜化」/「每張票開以前都要搜尋現有票,你為什麼會開了不搜?」 +# +# 這支封的是**動作**:任何 Bash 指令只要在對 Gitea 的 issues 端點做寫入, +# 就要有一個新鮮的搜尋戳記。它不強迫你用 scripts/ticket,只強迫你搜過。 +# (同 InkStoneCo#36:「守 prod 的閘,包一層腳本就繞過去了——它看的是指令長相」。 +# 本支同樣只看得到指令文字,這是 PreToolUse 這層的天花板; +# 所以判準取「端點 + 寫入動詞」兩個都命中才擋,讓純讀取一律放行。) +# +# 放行(刻意,這些都不是「開票」): +# - 只讀不寫(GET):撈清單、看票、對帳 +# - 對既有票的留言/改標籤/關票(/issues//... 這種帶票號的子路徑) +# - scripts/ticket 自己(它有自己的閘,重複擋只會互相打架) +# - 指令裡出現 ticket-api-ok(逃生口,會留在指令歷史上) +set -uo pipefail + +INPUT=$(cat) +CMD=$(printf '%s' "$INPUT" | python3 -c " +import sys,json +try: print(json.load(sys.stdin).get('tool_input',{}).get('command','')) +except Exception: print('') +" 2>/dev/null) +[ -n "$CMD" ] || exit 0 + +# 逃生口(留痕) +case "$CMD" in *ticket-api-ok*) exit 0 ;; esac +# scripts/ticket 有自己的閘 +case "$CMD" in *scripts/ticket*|*"ticket where"*|*"ticket new"*|*"ticket say"*) exit 0 ;; esac + +# ① 有沒有打到 Gitea 的 issues 端點(不帶票號的那個=建立新票的路徑) +printf '%s' "$CMD" | grep -qE 'repos/[^ "'"'"']*/issues([?"'"'"'`,)\\[:space:]]|$)' || exit 0 + +# ② 指令裡有沒有 POST 這個詞(純 GET 一律放行) +# 刻意只認一個裸字:跳脫引號、heredoc、python、curl、各種包裝的寫法無限多, +# 逐個補 pattern 追不完(leo 2026-08-17:「自然語言的變體是無限的,blacklist 永遠追不完」)。 +# ①已經確定這是「開票那條端點」,純讀取的指令不會出現 POST ⇒ 一個字就夠,而且沒有跳脫的破口。 +printf '%s' "$CMD" | grep -qw 'POST' || exit 0 + +# ③ 要有新鮮的搜尋戳記(與 scripts/ticket 共用同一個,30 分鐘) +STAMP=/tmp/.ticket-where-ok +NOW=$(date +%s) +FRESH=no +if [ -f "$STAMP" ]; then + AT=$(python3 -c "import json;print(int(json.load(open('$STAMP'))['at']))" 2>/dev/null || echo 0) + [ $((NOW - AT)) -le 1800 ] && FRESH=yes +fi +[ "$FRESH" = "yes" ] && exit 0 + +cat >&2 <<'MSG' +🚫 你正在用 Gitea API 直接開新票,而這一輪沒有搜尋紀錄 + +leo 2026-08-16:「**寫開票前先去搜尋要開在哪裡,不然你永遠會亂開新票**」 +leo 2026-08-20:「**每張票開以前都要搜尋現有票,你為什麼會開了不搜?這個規範不是早就有 hook 了?**」 + +規範有,閘也有——但那道閘長在 `scripts/ticket` 這支工具裡, +而你走的是 API 這條側門。**本閘就是把那道門也封上。** + +實錯(2026-08-20 同日):總管用 API 開了 12 張票,事後盤點**每一張都跟舊票重疊**, +全部只能關掉指回舊票。leo:「**你在讓事情複雜化**」。 + +── 怎麼過(擇一)─────────────────────────────── + 1. 先搜(預設,戳記 30 分鐘有效,之後 API 也放行): + scripts/ticket where <關鍵字...> + 🔴 搜到了就**貼進那張票**,不要開新的: + scripts/ticket say -F <內文檔> + + 2. 真的是新的一條線 → 直接用那支工具開,它會幫你把該檢查的檢查完: + scripts/ticket new -F <內文檔> --title <標題> + + 3. 這次確實不是在開新票(例如批次改標籤/關票/留言) + → 指令裡加 `ticket-api-ok` 說明理由,留痕放行。 + +放行的情況(本閘不管):純讀取(GET)、對既有票 /issues// 的留言與標籤、scripts/ticket 自己。 +MSG +exit 2 -- 2.52.0