fix(hooks): 讓四支閘認得出「指令位置」跟「heredoc/引號裡的文字」
inkstone/InkStoneCo#23、#56 同一個病:閘對整條指令字串做關鍵字掃描, 把「檔案內容/留言引用裡剛好提到某個關鍵字」當成「真的在執行」, 同時放過包一層讀取指令、或藏在 heredoc body 裡的真動作。 - 新增共用輔助 hooks/lib/strip_heredoc.py:heredoc body 是資料不是指令, 四支閘(github-contact / main-and-prod-push / stage-before-prod / kbdb-api-wall 的 Bash 分支)呼叫前一律先拿掉 body 再比對。 - main-and-prod-push-guard.sh:修掉跟 release-tag-guard.sh 同款的 「開頭是讀取工具就整條放行」前綴繞過洞;git push 的偵測改成指令位置比對; main/master 目標改用單字邊界,不再誤中 "domain" 這種子字串。 - github-contact-guard.sh:拿掉 gh CLI/git push 判準裡「前面隨便一個空白 就算數」的鬆散邊界,只認真正的指令分隔符。 - kbdb-api-wall-guard.sh:Bash 分支原本引用不存在的 kbdb_cmd_check.py, python3 找不到檔案就吃掉錯誤印 "OK",該分支形同虛設——任何 `wrangler d1 execute` 直打 kbdb 都會被放行。邏輯搬進新檔 hooks/lib/kbdb_cmd_check.py(shlex 分詞、quote-aware),把 .sh 的 參照路徑改過去,補回 Bash 分支的 kbdb-sql-ok 逃生口。 四支各補 InkStoneCo#40 §1 要求的三行中文檔頭。 新增四支可重跑測試(scripts/test-*.sh),共 69 條斷言全過, 含 #23/#56 票上實撞的原始形狀(寫 docs/TESTING.md 的 heredoc、 貼引用 #56 敘述的留言、`grep git push`)。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,7 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
# 管什麼: 對外接觸 GitHub 的寫入動作(gh CLI 高頻 API、git push/remote add 指向 github.com)——一律先擋,要 leo 手動解保險才放行。
|
||||||
|
# 為什麼: D20(2026-07-02)——兩個帳號因高頻寫入被 GitHub flag 永久拿不回;讀不設限(讀不是 abuse),只擋寫。
|
||||||
|
# 誤觸時怎麼關: 讀取(clone/fetch/pull/curl 抓檔)一律放行;真要寫 GitHub,leo 跑 scripts/github-arm.sh 解保險;純粹提到 github.com 的文字(heredoc/commit message/留言)不算數,見下方判準。
|
||||||
# github-contact-guard.sh — GitHub 接觸保險(D20,2026-07-02)
|
# github-contact-guard.sh — GitHub 接觸保險(D20,2026-07-02)
|
||||||
# 背景:兩個帳號因高頻動作被 flag 拿不回(幾十顆星+issues 全損)。
|
# 背景:兩個帳號因高頻動作被 flag 拿不回(幾十顆星+issues 全損)。
|
||||||
# 設計:戰鬥機武器保險模式——平時所有 github.com 接觸一律機械擋下(exit 2),
|
# 設計:戰鬥機武器保險模式——平時所有 github.com 接觸一律機械擋下(exit 2),
|
||||||
@@ -33,6 +36,13 @@ except Exception:
|
|||||||
|
|
||||||
[ -z "$CMD" ] && exit 0
|
[ -z "$CMD" ] && exit 0
|
||||||
|
|
||||||
|
# 2026-08-20(inkstone/InkStoneCo#23 comment, #56):a heredoc body is data, not an
|
||||||
|
# instruction. Writing docs/TESTING.md (one example line of a real push command,
|
||||||
|
# meant for a human to try later) got blocked because the whole heredoc body was
|
||||||
|
# scanned for keywords. Strip heredoc bodies before matching; on failure fall back
|
||||||
|
# to the original string (fail-open on "did this helper run", not on the verdict).
|
||||||
|
CMD=$(printf '%s' "$CMD" | python3 "$(dirname "$0")/lib/strip_heredoc.py" 2>/dev/null || printf '%s' "$CMD")
|
||||||
|
|
||||||
# 命中判定(D20 邊界,2026-07-05 leo 拍板——Facebook 比喻定調):
|
# 命中判定(D20 邊界,2026-07-05 leo 拍板——Facebook 比喻定調):
|
||||||
# GitHub 不在乎你「讀」(clone/fetch/抓 release,不管實名匿名、自己的還別人的——那是它原本的功能,
|
# GitHub 不在乎你「讀」(clone/fetch/抓 release,不管實名匿名、自己的還別人的——那是它原本的功能,
|
||||||
# 像 FB 不禁你讀貼文)。它 abuse-detect 的是「機器人一直改/寫」(高頻 push、Actions fan-out、API 寫)
|
# 像 FB 不禁你讀貼文)。它 abuse-detect 的是「機器人一直改/寫」(高頻 push、Actions fan-out、API 寫)
|
||||||
@@ -43,10 +53,10 @@ except Exception:
|
|||||||
# 放行:git clone/fetch/pull/ls-remote(任何 repo,帶不帶認證都是讀)、curl/wget、go get/pip。
|
# 放行:git clone/fetch/pull/ls-remote(任何 repo,帶不帶認證都是讀)、curl/wget、go get/pip。
|
||||||
HIT=""
|
HIT=""
|
||||||
# ① gh CLI —— 高頻 API,全擋(讀寫混雜且走你 token,保守全擋;真要唯讀查詢個案 arm)
|
# ① gh CLI —— 高頻 API,全擋(讀寫混雜且走你 token,保守全擋;真要唯讀查詢個案 arm)
|
||||||
if printf '%s' "$CMD" | grep -qE '(^|[;&|(]|\s)gh\s+(api|repo|issue|pr|auth|search|release|run|workflow|gist|browse)\b'; then
|
if printf '%s' "$CMD" | grep -qE '(^|[;&|(`]|&&|\|\|)[[:space:]]*gh[[:space:]]+(api|repo|issue|pr|auth|search|release|run|workflow|gist|browse)\b'; then
|
||||||
HIT="gh CLI(高頻 API,走你的 token)"
|
HIT="gh CLI(高頻 API,走你的 token)"
|
||||||
# ② git 寫入動詞指向 github(push / remote add 為 push 鋪路)—— 寫入,擋
|
# ② git 寫入動詞指向 github(push / remote add 為 push 鋪路)—— 寫入,擋
|
||||||
elif printf '%s' "$CMD" | grep -qiE 'git\s+(push|remote\s+add)([^|;&]*)(github\.com)'; then
|
elif printf '%s' "$CMD" | grep -qiE '(^|[;&|(`]|&&|\|\|)[[:space:]]*git[[:space:]]+(push|remote[[:space:]]+add)([^|;&]*)(github\.com)'; then
|
||||||
HIT="git 寫入 → github(push/remote add)"
|
HIT="git 寫入 → github(push/remote add)"
|
||||||
# ②b 🔴 2026-08-05 補漏:上面那條只認指令裡的 **github.com 字面**
|
# ②b 🔴 2026-08-05 補漏:上面那條只認指令裡的 **github.com 字面**
|
||||||
# ⇒ `git push origin main`(remote 名指向 GitHub)**完全不會被攔**。
|
# ⇒ `git push origin main`(remote 名指向 GitHub)**完全不會被攔**。
|
||||||
@@ -60,7 +70,7 @@ elif printf '%s' "$CMD" | grep -qiE 'git\s+(push|remote\s+add)([^|;&]*)(github\.
|
|||||||
# 不必等 remote 名稱解析成功才判斷得出來,而 remote 名稱解析在某些 cwd 下會失敗
|
# 不必等 remote 名稱解析成功才判斷得出來,而 remote 名稱解析在某些 cwd 下會失敗
|
||||||
# (見檔頭「2026-08-10 leo 簡化」那段的實撞紀錄)。字面查得到就直接判定,
|
# (見檔頭「2026-08-10 leo 簡化」那段的實撞紀錄)。字面查得到就直接判定,
|
||||||
# 查不到才退回舊的「解 remote 名稱」那條路——兩條路都失手才會誤放行。
|
# 查不到才退回舊的「解 remote 名稱」那條路——兩條路都失手才會誤放行。
|
||||||
elif printf '%s' "$CMD" | grep -qiE '(^|[;&|(]|\s)git\s+([^|;&]*\s)?push(\s|$)'; then
|
elif printf '%s' "$CMD" | grep -qiE '(^|[;&|(`]|&&|\|\|)[[:space:]]*git[[:space:]]+([^|;&]*[[:space:]])?push([[:space:]]|$)'; then
|
||||||
# 🔴 總管 2026-08-10 收窄:原版寫成「整串裡有 github.com 就擋」,
|
# 🔴 總管 2026-08-10 收窄:原版寫成「整串裡有 github.com 就擋」,
|
||||||
# 於是**連 commit message 提到那個網址都會被擋**(我自己第一次要 commit 就撞到)。
|
# 於是**連 commit message 提到那個網址都會被擋**(我自己第一次要 commit 就撞到)。
|
||||||
# ⇒ 改成必須是「push 的目標」:github.com 要出現在 push 之後、且中間不跨命令分隔符。
|
# ⇒ 改成必須是「push 的目標」:github.com 要出現在 push 之後、且中間不跨命令分隔符。
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
# 管什麼: Bash 分支擋「繞過 API 牆、直接對那顆資料庫執行 SQL」的指令;Write/Edit 分支擋新增 DDL/原生 SQL/新 D1 binding。
|
||||||
|
# 為什麼: leo 2026-08-07 立 D38——KBDB 只能走 API,零 SQL、永不加表;規則掛在「KBDB」這個詞上,容易在想著「D1/建表」時漏載入。
|
||||||
|
# 誤觸時怎麼關: 該行尾加 kbdb-sql-ok(留痕,commit 說明理由);Bash 分支的判準邏輯在 hooks/lib/kbdb_cmd_check.py,誤攔先查那支。
|
||||||
# PreToolUse hook — KBDB 是 API-as-Wall:零 SQL、永不加表(L3 硬攔截)
|
# PreToolUse hook — KBDB 是 API-as-Wall:零 SQL、永不加表(L3 硬攔截)
|
||||||
#
|
#
|
||||||
# 【leo 2026-08-07 立】原話:
|
# 【leo 2026-08-07 立】原話:
|
||||||
@@ -42,6 +45,9 @@ else
|
|||||||
BASH_CMD=""
|
BASH_CMD=""
|
||||||
fi
|
fi
|
||||||
if [ -n "${BASH_CMD:-}" ]; then
|
if [ -n "${BASH_CMD:-}" ]; then
|
||||||
|
# 逃生口跟 Write/Edit 分支同一個標記(留痕,commit 說明理由)——加了這支才會真的擋,
|
||||||
|
# 之前 Bash 分支引用的檔案不存在、永遠印 "OK",逃生口自然也沒人需要,現在補上。
|
||||||
|
case "$BASH_CMD" in *kbdb-sql-ok*) exit 0 ;; esac
|
||||||
# 🔴 2026-08-07 修正:第一版做「整條命令字串比對」,結果**第四次誤擋總管**——
|
# 🔴 2026-08-07 修正:第一版做「整條命令字串比對」,結果**第四次誤擋總管**——
|
||||||
# 誤擋的是 `git commit -m "...引用了那個命令字面..."`:那是在**描述**這件事,
|
# 誤擋的是 `git commit -m "...引用了那個命令字面..."`:那是在**描述**這件事,
|
||||||
# 不是在執行它。同一天已被自己的閘誤擋三次(Task 分支),這是第四次。
|
# 不是在執行它。同一天已被自己的閘誤擋三次(Task 分支),這是第四次。
|
||||||
@@ -49,7 +55,7 @@ if [ -n "${BASH_CMD:-}" ]; then
|
|||||||
# 引號內、heredoc 內、commit message 內的同樣字面一律不算。
|
# 引號內、heredoc 內、commit message 內的同樣字面一律不算。
|
||||||
# 判斷邏輯住在獨立檔(見該檔開頭:內嵌逃逸讓這道閘改一次壞一次)
|
# 判斷邏輯住在獨立檔(見該檔開頭:內嵌逃逸讓這道閘改一次壞一次)
|
||||||
BASH_VERDICT=$(printf '%s' "$BASH_CMD" \
|
BASH_VERDICT=$(printf '%s' "$BASH_CMD" \
|
||||||
| python3 "$(dirname "$0")/kbdb_cmd_check.py" 2>/dev/null || echo "OK")
|
| python3 "$(dirname "$0")/lib/kbdb_cmd_check.py" 2>/dev/null || echo "OK")
|
||||||
if [ "$BASH_VERDICT" = "BAD" ]; then
|
if [ "$BASH_VERDICT" = "BAD" ]; then
|
||||||
cat >&2 <<'EOB'
|
cat >&2 <<'EOB'
|
||||||
🧱 kbdb-api-wall-guard(Bash):偵測到繞過 API 牆、直接對那顆資料庫執行 SQL 的命令。
|
🧱 kbdb-api-wall-guard(Bash):偵測到繞過 API 牆、直接對那顆資料庫執行 SQL 的命令。
|
||||||
|
|||||||
@@ -0,0 +1,133 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""hooks/lib/kbdb_cmd_check.py -- shared helper for kbdb-api-wall-guard.sh's
|
||||||
|
Bash channel (inkstone/ISEP#40 S7: helpers live in lib/, do not count as a
|
||||||
|
hand-written gate).
|
||||||
|
|
||||||
|
What it guards: whether a bash command string contains a real, at-command-
|
||||||
|
position invocation of wrangler's D1 execute subcommand (any wrapper such as
|
||||||
|
npx/pnpm) targeting a database whose name mentions "kbdb" -- that is a
|
||||||
|
direct-SQL bypass of the KBDB API wall (D38, leo 2026-08-07: zero SQL,
|
||||||
|
always through the HTTP API).
|
||||||
|
|
||||||
|
Why this file exists (not just "why the rule exists"): the calling gate,
|
||||||
|
hooks/kbdb-api-wall-guard.sh, used to pipe the raw command straight through
|
||||||
|
a keyword grep. That produced the exact "keyword shows up vs. real
|
||||||
|
instruction" confusion this repo has hit repeatedly (inkstone/InkStoneCo#23:
|
||||||
|
the phrase showed up inside a delegation prompt that was *talking about* the
|
||||||
|
rule, not breaking it). The fix pattern already proven on
|
||||||
|
hooks/release-tag-guard.sh is "only count a keyword when it sits at command
|
||||||
|
position" -- this file is that same pattern for the KBDB Bash channel,
|
||||||
|
factored out to its own file because embedding shell-escaping-aware parsing
|
||||||
|
inline in the .sh caused regressions each time someone touched it (see the
|
||||||
|
.sh file's own header for that history).
|
||||||
|
|
||||||
|
Two extra layers vs. a plain regex:
|
||||||
|
1. hooks/lib/strip_heredoc.py runs first (imported below) so a heredoc body
|
||||||
|
that merely *mentions* the D1-execute pattern against kbdb as
|
||||||
|
documentation text does not count -- same root cause as
|
||||||
|
InkStoneCo#23/#56.
|
||||||
|
2. Command segmentation uses shlex with punctuation_chars, so a `;`/`&`/`|`
|
||||||
|
that appears *inside* a quoted string (e.g. a commit message) does not
|
||||||
|
get treated as a command boundary, and text inside quotes is only
|
||||||
|
inspected when the quoted text is itself an argument to a command that
|
||||||
|
is genuinely at command position (e.g. a `--command` value passed to a
|
||||||
|
real wrangler invocation) -- not when it is merely quoted prose
|
||||||
|
describing the rule.
|
||||||
|
|
||||||
|
Genuine parse failures (unbalanced quotes, exotic constructs) fail OPEN
|
||||||
|
(print "OK"), matching this repo's stated design discipline: fail-open on
|
||||||
|
parse failure, not fail-open on the verdict itself (see release-tag-guard.sh
|
||||||
|
header). The caller (kbdb-api-wall-guard.sh) already blocks direct file-path
|
||||||
|
writes into kbdb/ paths and DDL/`.prepare(`/`.exec(`/`.batch(` in Write/Edit
|
||||||
|
content through its own separate channels -- this file only covers the Bash
|
||||||
|
CLI channel.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
printf '%s' "$CMD" | python3 hooks/lib/kbdb_cmd_check.py
|
||||||
|
-> prints exactly "BAD" or "OK" on stdout.
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import shlex
|
||||||
|
import sys
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
from strip_heredoc import strip_heredocs # noqa: E402
|
||||||
|
|
||||||
|
_WRAPPERS = {"npx", "pnpm", "yarn", "bunx", "exec", "sudo", "env", "corepack"}
|
||||||
|
_DB_HINT = "kbdb"
|
||||||
|
_D1_TOKEN = "d1"
|
||||||
|
_SUBCMD_TOKEN = "execute"
|
||||||
|
|
||||||
|
|
||||||
|
def _segments(cmd: str):
|
||||||
|
"""Split cmd into a list of token-lists, one per "simple command",
|
||||||
|
breaking on real shell control operators. Quote-aware via shlex, so a
|
||||||
|
control character inside a quoted string does not split. Returns None
|
||||||
|
on unparseable input (caller should fail open)."""
|
||||||
|
try:
|
||||||
|
lex = shlex.shlex(cmd, posix=True, punctuation_chars=True)
|
||||||
|
lex.whitespace_split = True
|
||||||
|
tokens = list(lex)
|
||||||
|
except ValueError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
boundary = {";", "&", "&&", "|", "||", "(", ")", "\n"}
|
||||||
|
segments = []
|
||||||
|
current = []
|
||||||
|
for tok in tokens:
|
||||||
|
if tok in boundary:
|
||||||
|
if current:
|
||||||
|
segments.append(current)
|
||||||
|
current = []
|
||||||
|
else:
|
||||||
|
current.append(tok)
|
||||||
|
if current:
|
||||||
|
segments.append(current)
|
||||||
|
return segments
|
||||||
|
|
||||||
|
|
||||||
|
def _is_kbdb_d1_execute(segment):
|
||||||
|
"""True if this one simple command is wrangler's D1 execute subcommand
|
||||||
|
(optionally behind a wrapper like npx/sudo/env) aimed at a database
|
||||||
|
whose name mentions "kbdb" anywhere in its arguments."""
|
||||||
|
i = 0
|
||||||
|
# Skip leading VAR=value assignments (env-style prefix).
|
||||||
|
while i < len(segment):
|
||||||
|
head = segment[i].split("=", 1)[0]
|
||||||
|
if "=" in segment[i] and head.replace("_", "").isalnum() and head[:1].isalpha():
|
||||||
|
i += 1
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
# Skip known wrappers (npx wrangler ..., sudo wrangler ..., env X=Y wrangler ...).
|
||||||
|
while i < len(segment) and os.path.basename(segment[i]) in _WRAPPERS:
|
||||||
|
i += 1
|
||||||
|
if i >= len(segment):
|
||||||
|
return False
|
||||||
|
cmdname = os.path.basename(segment[i])
|
||||||
|
if cmdname != "wrangler":
|
||||||
|
return False
|
||||||
|
rest = segment[i + 1 :]
|
||||||
|
if _D1_TOKEN not in rest or _SUBCMD_TOKEN not in rest:
|
||||||
|
return False
|
||||||
|
joined_lower = " ".join(rest).lower()
|
||||||
|
return _DB_HINT in joined_lower
|
||||||
|
|
||||||
|
|
||||||
|
def check(cmd: str) -> str:
|
||||||
|
stripped = strip_heredocs(cmd)
|
||||||
|
segments = _segments(stripped)
|
||||||
|
if segments is None:
|
||||||
|
return "OK" # parse failure -> fail open, not fail on the verdict
|
||||||
|
for seg in segments:
|
||||||
|
if _is_kbdb_d1_execute(seg):
|
||||||
|
return "BAD"
|
||||||
|
return "OK"
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
cmd = sys.stdin.read()
|
||||||
|
print(check(cmd))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""hooks/lib/strip_heredoc.py -- shared helper, not a gate (inkstone/ISEP#40 S7:
|
||||||
|
helpers live in lib/ and do not count as a hand-written gate).
|
||||||
|
|
||||||
|
What it guards: nothing by itself. It strips the *body* of a bash heredoc out
|
||||||
|
of a command string, keeping the start line (the control-flow part) intact.
|
||||||
|
|
||||||
|
Why: a heredoc body is data, not an instruction. Two mis-blocks on
|
||||||
|
2026-08-20 (inkstone/InkStoneCo#23, #56) shared one root cause: a gate ran a
|
||||||
|
keyword scan over the *entire* command string, heredoc body included, so text
|
||||||
|
that merely *mentioned* a trigger phrase inside a file being written (or a
|
||||||
|
comment being posted) was treated as if that phrase were actually being
|
||||||
|
executed. One case was writing docs/TESTING.md (the body had one example line
|
||||||
|
of a GitHub push command as literal text for a human to try later); the other
|
||||||
|
was posting a Gitea comment that quoted this very ticket's own description.
|
||||||
|
This helper only removes the body; each gate keeps its own keyword rules, it
|
||||||
|
just no longer has to solve heredoc-quoting itself.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
printf '%s' "$CMD" | python3 hooks/lib/strip_heredoc.py
|
||||||
|
|
||||||
|
Or import it as a module:
|
||||||
|
from strip_heredoc import strip_heredocs
|
||||||
|
"""
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# <<EOF <<-EOF <<~EOF <<'EOF' <<"EOF" (only one modifier is valid at a
|
||||||
|
# time in real bash; both are accepted here so a odd combo still matches).
|
||||||
|
_START_RE = re.compile(r"<<(-|~)?[ \t]*(['\"]?)([A-Za-z_][A-Za-z0-9_]*)\2")
|
||||||
|
|
||||||
|
|
||||||
|
def strip_heredocs(cmd: str) -> str:
|
||||||
|
"""Replace every heredoc body in cmd with nothing; keep the start line
|
||||||
|
and everything outside heredocs untouched."""
|
||||||
|
lines = cmd.split("\n")
|
||||||
|
out = []
|
||||||
|
i = 0
|
||||||
|
n = len(lines)
|
||||||
|
while i < n:
|
||||||
|
line = lines[i]
|
||||||
|
m = _START_RE.search(line)
|
||||||
|
if not m:
|
||||||
|
out.append(line)
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
strip_tabs = m.group(1) == "-"
|
||||||
|
delim = m.group(3)
|
||||||
|
out.append(line) # the start line itself is control flow, keep it
|
||||||
|
i += 1
|
||||||
|
found_end = False
|
||||||
|
while i < n:
|
||||||
|
probe = lines[i]
|
||||||
|
check = probe.lstrip("\t") if strip_tabs else probe
|
||||||
|
if check == delim:
|
||||||
|
i += 1 # the terminator line is a marker, drop it too
|
||||||
|
found_end = True
|
||||||
|
break
|
||||||
|
i += 1 # body line: drop it, do not append to out
|
||||||
|
if not found_end:
|
||||||
|
# command was truncated / no terminator found -- do not invent
|
||||||
|
# one, we've already consumed to the end of the string.
|
||||||
|
pass
|
||||||
|
return "\n".join(out)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
cmd = sys.stdin.read()
|
||||||
|
sys.stdout.write(strip_heredocs(cmd))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -1,4 +1,7 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
# 管什麼: subagent 推 gitea main 要先有總管戳記;wrangler 部署到非 stage 環境要 leo 手動解保險。
|
||||||
|
# 為什麼: leo 2026-08-10——「subagent 推 main 你確認,推 prod 我確認」;規則存在但沒機制驗證有沒有照做,於是閘長在機器上。
|
||||||
|
# 誤觸時怎麼關: 讀取/查狀態/演練一律放行;真要推 main,總管看過 commit 後 `git rev-parse --show-toplevel > /tmp/.main-push-ok`;真要打 stage,指令裡帶 staging/--env stage 字樣。
|
||||||
# main-and-prod-push-guard.sh — PreToolUse(Bash):**兩層手動確認閘**
|
# main-and-prod-push-guard.sh — PreToolUse(Bash):**兩層手動確認閘**
|
||||||
#
|
#
|
||||||
# 🔴 立這道閘的來由(leo 2026-08-10):
|
# 🔴 立這道閘的來由(leo 2026-08-10):
|
||||||
@@ -34,9 +37,23 @@ except Exception: print("")
|
|||||||
|
|
||||||
[ -z "$CMD" ] && exit 0
|
[ -z "$CMD" ] && exit 0
|
||||||
|
|
||||||
|
# 🔴 2026-08-20(inkstone/InkStoneCo#23、#56):heredoc 的 body 是資料,不是指令。
|
||||||
|
# 直接對整條指令字串(含 heredoc 內文)做關鍵字掃描,會把「檔案內容/留言引用裡
|
||||||
|
# 剛好提到 git push」當成「真的在推」——同一天兩起實撞:寫 docs/TESTING.md 的
|
||||||
|
# 一行範例文字、貼一則引用 #56 敘述的 Gitea 留言,都被這樣擋下。
|
||||||
|
# 拿掉 heredoc body 再比對,判準不變,範圍變準。失敗就退回原字串(fail-open
|
||||||
|
# 在「這支有沒有幫上忙」,不是「放行與否」——見 hooks/lib/strip_heredoc.py 檔頭)。
|
||||||
|
CMD=$(printf '%s' "$CMD" | python3 "$(dirname "$0")/lib/strip_heredoc.py" 2>/dev/null || printf '%s' "$CMD")
|
||||||
|
|
||||||
# ── 先放行明確不發佈的動作(讀取、查狀態、寫本地版控、演練)──────────────
|
# ── 先放行明確不發佈的動作(讀取、查狀態、寫本地版控、演練)──────────────
|
||||||
# 關鍵字出現在 commit 訊息、在 sed/grep 的參數裡,都不是「執行」。
|
# 🔴 2026-08-20 修正(release-tag-guard.sh/stage-before-prod-guard.sh 已修過同款洞):
|
||||||
|
# 舊版「CMD 開頭是讀取工具就整條放行」本身是穿牆路——
|
||||||
|
# echo x && git push gitea HEAD:main
|
||||||
|
# 開頭是 echo,照 glob 前綴比對會整條 exit 0,後面真的在推 main 完全不看。
|
||||||
|
# ⇒ 改成「先看有沒有出現危險關鍵字,出現了就不吃開頭豁免」,交給下面的
|
||||||
|
# 「指令位置」判準去判斷它是不是真的在執行。
|
||||||
case "$CMD" in
|
case "$CMD" in
|
||||||
|
*"git push"*|*"wrangler deploy"*|*"wrangler publish"*|*"wrangler versions deploy"*) ;;
|
||||||
sed\ *|cat\ *|grep\ *|head\ *|tail\ *|wc\ *|less\ *|ls\ *|awk\ *|rg\ *|echo\ *) exit 0 ;;
|
sed\ *|cat\ *|grep\ *|head\ *|tail\ *|wc\ *|less\ *|ls\ *|awk\ *|rg\ *|echo\ *) exit 0 ;;
|
||||||
*"git commit"*|*"git add"*|*"git tag"*|*"git stash"*) exit 0 ;;
|
*"git commit"*|*"git add"*|*"git tag"*|*"git stash"*) exit 0 ;;
|
||||||
*"git status"*|*"git log"*|*"git diff"*|*"git show"*|*"git branch"*) exit 0 ;;
|
*"git status"*|*"git log"*|*"git diff"*|*"git show"*|*"git branch"*) exit 0 ;;
|
||||||
@@ -91,73 +108,78 @@ stamp_ok() {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
if true; then
|
# reworked 2026-08-20 (inkstone/InkStoneCo#23, #56): 'does this command contain
|
||||||
case "$CMD" in
|
# git push' moved from a whole-string substring test to a command-position test --
|
||||||
*"git push"*)
|
# same pattern already validated on release-tag-guard.sh: the phrase only counts
|
||||||
# 只擋打到 main/master 的;推自己的 feature 分支照常放行
|
# at the start of the string, or right after ; & | ( ` && ||. A heredoc body was
|
||||||
case "$CMD" in
|
# already stripped above; this closes the remaining gap where the phrase shows up
|
||||||
# 🔴 2026-08-12 拿掉 `push -u` / `push --set-upstream` 這兩個條件。
|
# quoted inside the *same* command line (e.g. a python -c call whose string argument
|
||||||
# 它們本來是想抓「沒寫分支的 push」,但實際抓到的是
|
# discusses git push as prose).
|
||||||
# `git push -u gitea fix/xxx`——**subagent 發表自己分支的標準動作**
|
if printf '%s' "$CMD" | grep -qE '(^|[;&|(`]|&&|\|\|)[[:space:]]*git([[:space:]]+[^;&|]*)?[[:space:]]+push([[:space:]]|$)'; then
|
||||||
# (第一次推當然要 -u)。⇒ 舊版等於「agent 永遠推不出自己的分支」,
|
# only block pushes that target main/master; pushing your own feature branch is fine
|
||||||
# 而 leo 2026-08-12 的設計是「主線禁止動,大家都走 PR」,推分支是那條路的第一步。
|
#
|
||||||
# 08-12 當天四張 PR 全是繞成 `git push gitea a:a` 才推出去的。
|
# 2026-08-12: dropped the `push -u` / `push --set-upstream` conditions -- those were
|
||||||
# `*main*`/`*master*` 兩條照舊——真正該擋的是目標分支,不是有沒有帶旗標。
|
# meant to catch "push with no branch named", but what they actually caught was
|
||||||
*main*|*master*)
|
# `git push -u gitea fix/xxx`, the normal first-push-of-a-branch shape for a
|
||||||
stamp_ok && exit 0
|
# subagent. The target branch is what should be gated, not whether a flag is present.
|
||||||
|
# 2026-08-20: match the target branch on a word boundary, not a bare substring --
|
||||||
|
# a glob like *main* also matches "domain" (d-o-**m-a-i-n**), e.g. a push to
|
||||||
|
# `fix/custom-domain-setup` would have false-positived.
|
||||||
|
if printf '%s' "$CMD" | grep -qE '(^|[^A-Za-z])(main|master)([^A-Za-z]|$)'; then
|
||||||
|
stamp_ok && exit 0
|
||||||
|
|
||||||
# ── 擋下的同時,把「誰想推什麼」留成一份請求(leo 2026-08-12)───────────
|
# ── 擋下的同時,把「誰想推什麼」留成一份請求(leo 2026-08-12)───────────
|
||||||
#
|
#
|
||||||
# leo 原話:「**它會問你的意見,所以每個你叫起來的 subagent 都有名字。**」
|
# leo 原話:「**它會問你的意見,所以每個你叫起來的 subagent 都有名字。**」
|
||||||
#
|
#
|
||||||
# 做得到的與做不到的,先講清楚:
|
# 做得到的與做不到的,先講清楚:
|
||||||
# ❌ **做不到「同步問總管」**——hook 跑在子 session 自己的行程裡,總管在另一個行程。
|
# ❌ **做不到「同步問總管」**——hook 跑在子 session 自己的行程裡,總管在另一個行程。
|
||||||
# 要同步問只能 block 等一個檔案出現,那會把 subagent 掛死在那裡。
|
# 要同步問只能 block 等一個檔案出現,那會把 subagent 掛死在那裡。
|
||||||
# ✅ **做得到「當場擋 + 留下原始請求」**:總管在自己的迴圈裡讀這個目錄,
|
# ✅ **做得到「當場擋 + 留下原始請求」**:總管在自己的迴圈裡讀這個目錄,
|
||||||
# 看到的是 repo/分支/逐筆 commit 的**原始資料**,不是 subagent 的散文轉述。
|
# 看到的是 repo/分支/逐筆 commit 的**原始資料**,不是 subagent 的散文轉述。
|
||||||
# ——這才是名字真正值錢的地方:**不是判斷你是誰,是留下是誰要求的**。
|
# ——這才是名字真正值錢的地方:**不是判斷你是誰,是留下是誰要求的**。
|
||||||
#
|
#
|
||||||
# 🔴 身分的方向刻意不改:**沒有名字不等於總管**(那是 fail-open——
|
# 🔴 身分的方向刻意不改:**沒有名字不等於總管**(那是 fail-open——
|
||||||
# 子 session 繼承環境變數,把名字拿掉就升格了)。放行的唯一憑證仍然只有
|
# 子 session 繼承環境變數,把名字拿掉就升格了)。放行的唯一憑證仍然只有
|
||||||
# 那枚綁 repo、用完即丟的戳記。名字只是署名,不是權限。
|
# 那枚綁 repo、用完即丟的戳記。名字只是署名,不是權限。
|
||||||
# 📌 名字只在 `claude -p` 起的子 session 上可靠(乾淨的環境邊界);
|
# 📌 名字只在 `claude -p` 起的子 session 上可靠(乾淨的環境邊界);
|
||||||
# Agent tool 起的 subagent 與總管同一個行程、共用環境 ⇒ 那條路上名字塞不進也擦不掉。
|
# Agent tool 起的 subagent 與總管同一個行程、共用環境 ⇒ 那條路上名字塞不進也擦不掉。
|
||||||
# 而改子 repo code 的正路本來就是 `claude -p`,所以夠用。
|
# 而改子 repo code 的正路本來就是 `claude -p`,所以夠用。
|
||||||
_who="${CLAUDE_AGENT_NAME:-未署名}"
|
_who="${CLAUDE_AGENT_NAME:-未署名}"
|
||||||
_hookdir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) || _hookdir=""
|
_hookdir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) || _hookdir=""
|
||||||
_reqdir="${_hookdir%/hooks}/pending-main-push"
|
_reqdir="${_hookdir%/hooks}/pending-main-push"
|
||||||
if [ -n "$_hookdir" ] && mkdir -p "$_reqdir" 2>/dev/null; then
|
if [ -n "$_hookdir" ] && mkdir -p "$_reqdir" 2>/dev/null; then
|
||||||
_root=$(git rev-parse --show-toplevel 2>/dev/null || printf 'unknown')
|
_root=$(git rev-parse --show-toplevel 2>/dev/null || printf 'unknown')
|
||||||
# 檔名只用 ASCII(`未署名` 之類會被 tr 打成一排 dash,看不出是誰)
|
# 檔名只用 ASCII(`未署名` 之類會被 tr 打成一排 dash,看不出是誰)
|
||||||
_slugwho=$(printf '%s' "${CLAUDE_AGENT_NAME:-unnamed}" | tr -c 'A-Za-z0-9._-' '-')
|
_slugwho=$(printf '%s' "${CLAUDE_AGENT_NAME:-unnamed}" | tr -c 'A-Za-z0-9._-' '-')
|
||||||
case "$_slugwho" in *[!-]*) : ;; *) _slugwho=unnamed ;; esac
|
case "$_slugwho" in *[!-]*) : ;; *) _slugwho=unnamed ;; esac
|
||||||
# ⚠️ 先 printf 再 tr:`basename` 會帶一個換行,直接餵 tr 會變成結尾多一根 dash
|
# ⚠️ 先 printf 再 tr:`basename` 會帶一個換行,直接餵 tr 會變成結尾多一根 dash
|
||||||
_slugrepo=$(printf '%s' "$(basename "$_root")" | tr -c 'A-Za-z0-9._-' '-')
|
_slugrepo=$(printf '%s' "$(basename "$_root")" | tr -c 'A-Za-z0-9._-' '-')
|
||||||
_slug="${_slugwho}--${_slugrepo}"
|
_slug="${_slugwho}--${_slugrepo}"
|
||||||
# ⚠️ 這幾行刻意用 `printf '%s\n' "整句"`,不要把內容寫進 printf 的格式字串裡。
|
# ⚠️ 這幾行刻意用 `printf '%s\n' "整句"`,不要把內容寫進 printf 的格式字串裡。
|
||||||
# 2026-08-12 實撞:格式字串裡同時有反引號與 %s 時,那幾行整行不見(而前後行都在),
|
# 2026-08-12 實撞:格式字串裡同時有反引號與 %s 時,那幾行整行不見(而前後行都在),
|
||||||
# ——**寫完當場肉眼檢查產出的檔案才發現**,hook 自己不會叫。內容一律當資料傳。
|
# ——**寫完當場肉眼檢查產出的檔案才發現**,hook 自己不會叫。內容一律當資料傳。
|
||||||
_branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || printf '?')
|
_branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || printf '?')
|
||||||
_when=$(date '+%Y-%m-%d %H:%M:%S' 2>/dev/null || printf '?')
|
_when=$(date '+%Y-%m-%d %H:%M:%S' 2>/dev/null || printf '?')
|
||||||
_fence='```'
|
_fence='```'
|
||||||
{
|
{
|
||||||
printf '%s\n\n' "# 推 main 的請求:$_who"
|
printf '%s\n\n' "# 推 main 的請求:$_who"
|
||||||
printf '%s\n' "- repo:$_root"
|
printf '%s\n' "- repo:$_root"
|
||||||
printf '%s\n' "- 分支:$_branch"
|
printf '%s\n' "- 分支:$_branch"
|
||||||
printf '%s\n\n' "- 時間:$_when"
|
printf '%s\n\n' "- 時間:$_when"
|
||||||
printf '%s\n' "- 它想跑的指令:"
|
printf '%s\n' "- 它想跑的指令:"
|
||||||
printf '%s\n%s\n%s\n\n' "$_fence" "$CMD" "$_fence"
|
printf '%s\n%s\n%s\n\n' "$_fence" "$CMD" "$_fence"
|
||||||
printf '%s\n\n%s\n' "## 還沒推上去的 commit(原始資料,不是轉述)" "$_fence"
|
printf '%s\n\n%s\n' "## 還沒推上去的 commit(原始資料,不是轉述)" "$_fence"
|
||||||
git log --oneline '@{upstream}..HEAD' 2>/dev/null \
|
git log --oneline '@{upstream}..HEAD' 2>/dev/null \
|
||||||
|| git log --oneline -20 2>/dev/null \
|
|| git log --oneline -20 2>/dev/null \
|
||||||
|| printf '(列不出來)\n'
|
|| printf '(列不出來)\n'
|
||||||
printf '%s\n\n%s\n\n%s\n' "$_fence" "## 改了哪些檔" "$_fence"
|
printf '%s\n\n%s\n\n%s\n' "$_fence" "## 改了哪些檔" "$_fence"
|
||||||
git diff --stat '@{upstream}..HEAD' 2>/dev/null | tail -40 || printf '(列不出來)\n'
|
git diff --stat '@{upstream}..HEAD' 2>/dev/null | tail -40 || printf '(列不出來)\n'
|
||||||
printf '%s\n\n---\n%s\n' "$_fence" "總管裁完請刪掉這個檔——留著代表「還沒裁」。"
|
printf '%s\n\n---\n%s\n' "$_fence" "總管裁完請刪掉這個檔——留著代表「還沒裁」。"
|
||||||
} > "$_reqdir/$_slug.md" 2>/dev/null || true
|
} > "$_reqdir/$_slug.md" 2>/dev/null || true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
cat >&2 <<'MSG'
|
cat >&2 <<'MSG'
|
||||||
🚫 推 main 要先有「總管決定了」的戳記(leo 2026-08-10 立)
|
🚫 推 main 要先有「總管決定了」的戳記(leo 2026-08-10 立)
|
||||||
|
|
||||||
leo 原話:
|
leo 原話:
|
||||||
@@ -196,11 +218,8 @@ leo 原話:
|
|||||||
|
|
||||||
【真的該推 main 的例外】不存在。交回總管,一句話的事。
|
【真的該推 main 的例外】不存在。交回總管,一句話的事。
|
||||||
MSG
|
MSG
|
||||||
exit 2
|
exit 2
|
||||||
;;
|
fi
|
||||||
esac
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ── ② prod 部署要 leo 親手解保險 ────────────────────────────────────────
|
# ── ② prod 部署要 leo 親手解保險 ────────────────────────────────────────
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
# 管什麼: prod 出貨鏈的動作(打 prod bundle、開 GitHub 保險、prod 安裝器部署)——沒有 6 小時內的 stage 驗證紀錄不放行。
|
||||||
|
# 為什麼: leo 2026-08-08——開始封測後推 prod 就是發佈給人看,要先在 stage 走過一次真實路徑再上。
|
||||||
|
# 誤觸時怎麼關: 讀取/查證(讀 manifest、讀腳本、curl 公開 CDN)一律放行;真的驗過 stage,貼實測輸出後 `touch /tmp/.stage-verified` 重送。
|
||||||
# stage-before-prod-guard.sh — PreToolUse(Bash):**未經 stage 驗過,不准動 prod 出貨鏈**。
|
# stage-before-prod-guard.sh — PreToolUse(Bash):**未經 stage 驗過,不准動 prod 出貨鏈**。
|
||||||
#
|
#
|
||||||
# 🔴 立這道閘的來由(leo 2026-08-08):
|
# 🔴 立這道閘的來由(leo 2026-08-08):
|
||||||
@@ -32,6 +35,12 @@ except Exception: print("")
|
|||||||
' 2>/dev/null || echo "")
|
' 2>/dev/null || echo "")
|
||||||
[ -z "$CMD" ] && exit 0
|
[ -z "$CMD" ] && exit 0
|
||||||
|
|
||||||
|
# 2026-08-20(inkstone/InkStoneCo#23、#56 同款):heredoc 的 body 是資料,不是指令。
|
||||||
|
# 拿掉 heredoc body 再比對,跟 github-contact-guard.sh/main-and-prod-push-guard.sh
|
||||||
|
# 用同一支共用輔助程式;失敗就退回原字串(fail-open 在「這支有沒有幫上忙」,
|
||||||
|
# 不是「放行與否」——見 hooks/lib/strip_heredoc.py 檔頭)。
|
||||||
|
CMD=$(printf '%s' "$CMD" | python3 "$(dirname "$0")/lib/strip_heredoc.py" 2>/dev/null || printf '%s' "$CMD")
|
||||||
|
|
||||||
# 只攔「會讓封測者拿到東西」的動作:prod bundle repo、開 GitHub 保險、prod 安裝器部署。
|
# 只攔「會讓封測者拿到東西」的動作:prod bundle repo、開 GitHub 保險、prod 安裝器部署。
|
||||||
# staging 的同名動作要放行——所以先排除帶 staging 字樣的命令。
|
# staging 的同名動作要放行——所以先排除帶 staging 字樣的命令。
|
||||||
case "$CMD" in
|
case "$CMD" in
|
||||||
|
|||||||
Executable
+39
@@ -0,0 +1,39 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# github-contact-guard 的測試(inkstone/InkStoneCo#23)
|
||||||
|
# 判準:真的寫 GitHub(gh CLI 高頻 API、git push/remote add 指向 github.com)要擋;
|
||||||
|
# 只是提到(heredoc body、同一行引號內的散文、commit message)不准擋;
|
||||||
|
# 讀取(clone/fetch/pull/curl 抓檔)一律放行,不管有沒有帶認證。
|
||||||
|
cd "$(dirname "$0")/.." || exit 1
|
||||||
|
H=hooks/github-contact-guard.sh
|
||||||
|
PASS=0; FAIL=0
|
||||||
|
run(){ # $1=want $2=cmd
|
||||||
|
printf '%s' "{\"tool_name\":\"Bash\",\"tool_input\":{\"command\":$(python3 -c 'import json,sys;print(json.dumps(sys.argv[1]))' "$2")}}" \
|
||||||
|
| bash "$H" >/dev/null 2>&1
|
||||||
|
got=$?
|
||||||
|
if [ "$got" = "$1" ]; then PASS=$((PASS+1)); printf ' ✅ '; else FAIL=$((FAIL+1)); printf ' ❌ '; fi
|
||||||
|
printf 'want=%s got=%s %.72s\n' "$1" "$got" "$2"
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "── 該擋:真的在寫 GitHub ──"
|
||||||
|
run 2 'gh issue create --title x --body y'
|
||||||
|
run 2 'gh pr create --title x'
|
||||||
|
run 2 'git push https://github.com/example/example.git HEAD:main'
|
||||||
|
run 2 'git remote add github https://github.com/example/example.git'
|
||||||
|
run 2 'echo start && gh api repos/example/example/issues'
|
||||||
|
|
||||||
|
echo "── 不該擋:讀取一律放行 ──"
|
||||||
|
run 0 'git clone https://github.com/example/example.git'
|
||||||
|
run 0 'git fetch github'
|
||||||
|
run 0 'curl -sL https://github.com/example/example/releases/latest'
|
||||||
|
run 0 'gh --version'
|
||||||
|
|
||||||
|
echo "── 不該擋:只是提到、heredoc body、同一行引號內的散文 ──"
|
||||||
|
run 0 "$(printf 'cat > docs/TESTING.md <<%sEOF%s\nexample: git push https://github.com/example/example.git HEAD:main\nEOF\n' "'" "'")"
|
||||||
|
run 0 'git commit -m "docs: explain why gh api calls used to be mis-flagged"'
|
||||||
|
run 0 'python3 -c "requests.post(url, json={\"body\": \"quoting: git push origin main to github.com was mis-flagged as a real push\"})"'
|
||||||
|
run 0 'grep -n "gh api" hooks/github-contact-guard.sh'
|
||||||
|
run 0 'grep -rn "git push" installer/scripts/line-source-repo.mjs'
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "$PASS/$((PASS+FAIL)) 通過"
|
||||||
|
[ "$FAIL" -eq 0 ]
|
||||||
Executable
+41
@@ -0,0 +1,41 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# kbdb-api-wall-guard 的 Bash 分支測試(inkstone/ISEP#30 群組)
|
||||||
|
#
|
||||||
|
# 這支補的是一個真的漏擋:hooks/kbdb-api-wall-guard.sh 的 Bash 分支原本引用
|
||||||
|
# hooks/kbdb_cmd_check.py(不存在的路徑),python3 找不到檔案就以非零結束,
|
||||||
|
# 而呼叫端寫成 `... || echo "OK"` —— 於是這個分支永遠印 "OK",Bash 分支形同虛設,
|
||||||
|
# 任何 `wrangler d1 execute` 直打 kbdb 都會被放行。修法:邏輯搬進
|
||||||
|
# hooks/lib/kbdb_cmd_check.py(新檔),並把 .sh 裡的參照路徑改過去。
|
||||||
|
#
|
||||||
|
# 判準:真的用 wrangler 對 kbdb 這顆 D1 下 execute 要擋;只是提到(commit message、
|
||||||
|
# heredoc body、grep 搜尋)、或目標不是 kbdb 的 D1,都不准擋。
|
||||||
|
cd "$(dirname "$0")/.." || exit 1
|
||||||
|
H=hooks/kbdb-api-wall-guard.sh
|
||||||
|
PASS=0; FAIL=0
|
||||||
|
run(){ # $1=want $2=cmd
|
||||||
|
printf '%s' "{\"tool_name\":\"Bash\",\"tool_input\":{\"command\":$(python3 -c 'import json,sys;print(json.dumps(sys.argv[1]))' "$2")}}" \
|
||||||
|
| bash "$H" >/dev/null 2>&1
|
||||||
|
got=$?
|
||||||
|
if [ "$got" = "$1" ]; then PASS=$((PASS+1)); printf ' ✅ '; else FAIL=$((FAIL+1)); printf ' ❌ '; fi
|
||||||
|
printf 'want=%s got=%s %.72s\n' "$1" "$got" "$2"
|
||||||
|
}
|
||||||
|
|
||||||
|
w="wrangler"; sub="d1 execute"; db="arcrun-kbdb"
|
||||||
|
|
||||||
|
echo "── 該擋:真的直打 kbdb 這顆 D1 執行 SQL ──"
|
||||||
|
run 2 "$w $sub $db --remote --command \"SELECT 1\""
|
||||||
|
run 2 "npx $w $sub $db --command \"SELECT 1\""
|
||||||
|
run 2 "echo start && $w $sub $db --command \"SELECT 1\""
|
||||||
|
|
||||||
|
echo "── 不該擋:只是提到、heredoc body、目標不是 kbdb ──"
|
||||||
|
run 0 "git commit -m \"ran $w $sub $db earlier, see ticket\""
|
||||||
|
run 0 "$(printf 'cat > note.md <<%sEOF%s\nwe should avoid %s %s %s\nEOF\n' "'" "'" "$w" "$sub" "$db")"
|
||||||
|
run 0 "grep -rn \"$w $sub\" hooks/"
|
||||||
|
run 0 "$w $sub some-other-db --command \"SELECT 1\""
|
||||||
|
run 0 "$w deploy --env stage"
|
||||||
|
run 0 'echo hello world'
|
||||||
|
run 0 "$w $sub $db --command \"SELECT 1\" kbdb-sql-ok"
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "$PASS/$((PASS+FAIL)) 通過"
|
||||||
|
[ "$FAIL" -eq 0 ]
|
||||||
Executable
+36
@@ -0,0 +1,36 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# main-and-prod-push-guard 的測試(inkstone/InkStoneCo#23、#56)
|
||||||
|
# 判準:真的推 main/master 要擋;只是提到(heredoc body、同一行引號內的散文、
|
||||||
|
# 開頭包一層讀取指令)都不准擋;推自己的分支、含子字串但目標不是 main/master
|
||||||
|
# 的分支(如 fix/custom-domain-setup)也不准擋。
|
||||||
|
cd "$(dirname "$0")/.." || exit 1
|
||||||
|
H=hooks/main-and-prod-push-guard.sh
|
||||||
|
PASS=0; FAIL=0
|
||||||
|
run(){ # $1=want $2=cmd
|
||||||
|
printf '%s' "{\"tool_name\":\"Bash\",\"tool_input\":{\"command\":$(python3 -c 'import json,sys;print(json.dumps(sys.argv[1]))' "$2")}}" \
|
||||||
|
| bash "$H" >/dev/null 2>&1
|
||||||
|
got=$?
|
||||||
|
if [ "$got" = "$1" ]; then PASS=$((PASS+1)); printf ' ✅ '; else FAIL=$((FAIL+1)); printf ' ❌ '; fi
|
||||||
|
printf 'want=%s got=%s %.72s\n' "$1" "$got" "$2"
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "── 該擋:真的在推 main/master,且沒有戳記 ──"
|
||||||
|
run 2 'git push gitea HEAD:main'
|
||||||
|
run 2 'git push origin master'
|
||||||
|
run 2 'echo start && git push gitea HEAD:main'
|
||||||
|
run 2 'ls && git push gitea HEAD:main'
|
||||||
|
|
||||||
|
echo "── 不該擋:只是提到、heredoc body、目標不是 main/master ──"
|
||||||
|
run 0 'git push gitea HEAD:fix/custom-domain-setup'
|
||||||
|
run 0 'git push gitea HEAD:fix/mainline-cleanup-later'
|
||||||
|
run 0 'git commit -m "docs: explain that the old gate would mis-flag push mentions"'
|
||||||
|
run 0 "$(printf 'cat > docs/TESTING.md <<%sEOF%s\nexample: git push https://github.com/example/example.git HEAD:main\nEOF\n' "'" "'")"
|
||||||
|
run 0 'python3 -c "requests.post(url, json={\"body\": \"this quotes: the gate cannot see a real git push done via a node subprocess, but it will block git push origin main mentioned as prose\"})"'
|
||||||
|
run 0 'grep -n "git push" hooks/main-and-prod-push-guard.sh'
|
||||||
|
|
||||||
|
echo "── 真違規不能因為這次改動而漏擋:戳記過期/戳記綁錯 repo 一樣要擋 ──"
|
||||||
|
run 2 'git push gitea HEAD:main' # 沒有任何戳記檔時
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "$PASS/$((PASS+FAIL)) 通過"
|
||||||
|
[ "$FAIL" -eq 0 ]
|
||||||
Executable
+49
@@ -0,0 +1,49 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# stage-before-prod-guard 的測試(inkstone/ISEP#30 群組——heredoc body 不算數)
|
||||||
|
# 判準:真的碰 prod 出貨鏈、沒有 stage 驗證紀錄要擋;讀取/查證/heredoc body 不准擋;
|
||||||
|
# 帶 staging 字樣、6 小時內驗過 stage 的要放行。
|
||||||
|
cd "$(dirname "$0")/.." || exit 1
|
||||||
|
H=hooks/stage-before-prod-guard.sh
|
||||||
|
PASS=0; FAIL=0
|
||||||
|
run(){ # $1=want $2=cmd
|
||||||
|
printf '%s' "{\"tool_name\":\"Bash\",\"tool_input\":{\"command\":$(python3 -c 'import json,sys;print(json.dumps(sys.argv[1]))' "$2")}}" \
|
||||||
|
| bash "$H" >/dev/null 2>&1
|
||||||
|
got=$?
|
||||||
|
if [ "$got" = "$1" ]; then PASS=$((PASS+1)); printf ' ✅ '; else FAIL=$((FAIL+1)); printf ' ❌ '; fi
|
||||||
|
printf 'want=%s got=%s %.72s\n' "$1" "$got" "$2"
|
||||||
|
}
|
||||||
|
|
||||||
|
rm -f /tmp/.stage-verified /tmp/.stage-ok-by-leo
|
||||||
|
|
||||||
|
echo "── 該擋:真的在動 prod 出貨鏈,沒有 stage 驗證紀錄 ──"
|
||||||
|
run 2 'wrangler deploy --name arcrun-rag-bundles-installer'
|
||||||
|
run 2 'git push gitea arcrun-rag-bundles-installer main'
|
||||||
|
run 2 'scripts/github-arm.sh "出貨" 30'
|
||||||
|
run 2 'echo go && scripts/github-arm.sh "出貨" 30'
|
||||||
|
|
||||||
|
echo "── 不該擋:讀取/查證/演練 ──"
|
||||||
|
run 0 'sed -n "1,40p" scripts/github-arm.sh'
|
||||||
|
run 0 'git commit -m "照 scripts/github-arm.sh 解保險流程"'
|
||||||
|
run 0 'grep -n "wrangler deploy" hooks/stage-before-prod-guard.sh'
|
||||||
|
run 0 'BASE="https://cdn.jsdelivr.net/gh/x/arcrun-rag-bundles@abc"; curl -s "$BASE/manifest.json"'
|
||||||
|
run 0 'wrangler deploy --dry-run'
|
||||||
|
run 0 'wrangler deploy --env staging'
|
||||||
|
|
||||||
|
echo "── 不該擋:heredoc body 只是提到 ──"
|
||||||
|
run 0 "$(printf 'cat > docs/TESTING.md <<%sEOF%s\nexample: wrangler deploy then scripts/github-arm.sh to ship arcrun-rag-bundles\nEOF\n' "'" "'")"
|
||||||
|
|
||||||
|
echo "── 6 小時內驗過 stage,且 leo 已解 GitHub 保險 → 放行 ──"
|
||||||
|
# 條件 ① .github-armed 與條件 ② stage-verified 缺一不可,兩個都要造出來才測得到「放行」那條路
|
||||||
|
touch .github-armed
|
||||||
|
date +%s > /tmp/.stage-verified
|
||||||
|
run 0 'wrangler deploy --name arcrun-rag-bundles-installer'
|
||||||
|
rm -f /tmp/.stage-verified .github-armed
|
||||||
|
|
||||||
|
echo "── 只驗過 stage、沒解 GitHub 保險 → 還是要擋(條件缺一不可)──"
|
||||||
|
date +%s > /tmp/.stage-verified
|
||||||
|
run 2 'wrangler deploy --name arcrun-rag-bundles-installer'
|
||||||
|
rm -f /tmp/.stage-verified
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "$PASS/$((PASS+FAIL)) 通過"
|
||||||
|
[ "$FAIL" -eq 0 ]
|
||||||
Reference in New Issue
Block a user