#!/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