Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0cdb6f2c05 | |||
| b5c4ef3540 | |||
| 410771e883 | |||
| 64dae34efa | |||
| 6d61c10cb8 | |||
| e7bea04862 |
@@ -1,7 +1,7 @@
|
||||
# SDD × Gitea 治理規範
|
||||
|
||||
```
|
||||
version: 0.7.0
|
||||
version: 0.8.0
|
||||
status: 現行(本檔的修改依 §9 走 issue)
|
||||
scope: 所有安裝 ISEP 的環境(本機 CC 與雲端 CC)
|
||||
distribution: 隨 ISEP 發佈,ISEP repo 是唯一編輯點(§11)
|
||||
@@ -9,6 +9,8 @@ lineage: v0.5.0 draft by claude.ai(存檔 _draft-claude-ai-v0.5.0.md)
|
||||
→ v0.6.0 由總管對照現場實況修訂,逐條分歧見 DIVERGENCE-v0.5.0-to-v0.6.0.md
|
||||
→ v0.7.0 併入全局遍歷(14 repo/156 open/45 張管理票分六群,§13)與新舊票 mapping(§14);
|
||||
依 leo 指正:規劃不另立文件,修正本檔就是新版
|
||||
→ v0.8.0 三源整合定案(§15):claude.ai 兩份規劃 × 舊票需求 × 既有機制,
|
||||
逐領域對照、三處矛盾解法、動工順序——leo 2026-08-20 核准
|
||||
```
|
||||
|
||||
---
|
||||
@@ -123,21 +125,12 @@ s/triage ──驗傷通過──▶ s/backlog ──排進 milestone──▶ s
|
||||
|
||||
- **M4.1** 命名 `vX.Y`,必設 due date。
|
||||
- **M4.2** Deliverable = **一個可測的新版本號**。所有掛入 leaf 關閉後,可從預設分支打出通過驗收的版本。
|
||||
- **M4.3** **Timebox 到期不自動關、不自動打 tag。** 到期強制做一次降 scope 對帳(未完成 leaf 搬下一 milestone)並通知 leo。
|
||||
🔴 自動打 tag 會製造假交付——tag 永遠只在「驗過了」之後發生。
|
||||
- **M4.3** **Timebox 到期:不自動關、不自動打 tag、也不把票移出。** 到期只做一件事:通知 leo。
|
||||
🔴 **里程碑的內容一經確定就不增不減**(leo 2026-08-20:「milestone 確定後怎麼可以再把東西移除?
|
||||
定下工作自己刪掉是什麼意思?根本就沒有什麼降」)。增減都是 leo 的裁決,不是總管的操作。
|
||||
自動打 tag 會製造假交付——tag 永遠只在「驗過了」之後發生。
|
||||
- **M4.4** Milestone 關閉 = release tag,一對一。「已交付」唯一合法形式是 **tag 存在且裝得起來**;打 tag 前置 open issues = 0(§8 E12)。
|
||||
- **M4.5** Description 只寫版本目標一句 + tracking 連結。討論回 tracking issue。
|
||||
- **M4.7(降 scope 必須留痕)** 🔴 **把票移出里程碑,必須同時寫進該里程碑的 description。**
|
||||
格式:原本幾張、移走哪幾張、為什麼、移去哪裡。
|
||||
- **禁的不是降 scope**——卡人閘時降 scope 就是 M4.3 要的。禁的是**降得無聲無息**。
|
||||
- 為什麼寫在 description 不是寫在票裡:**leo 看的是百分比那個畫面**,
|
||||
他不會為了確認 100% 是不是真的而去逐張點票。**痕跡要留在他會經過的地方。**
|
||||
- 來由(2026-08-20 實犯):`v0.2.0` 本來 6 張,`#5` 卡人閘被移出 ⇒ 分母 6 變 5 ⇒ 顯示 100%。
|
||||
leo 當場問「為什麼還有很多 issues 開放中」。
|
||||
🔴 這與 §3.4(審核完沒關票 ⇒ 數字偏低)是**同一個病的兩面**:
|
||||
畫面上的數字不等於實際狀態,而 leo 只看得到畫面。
|
||||
- 機械閘:`inkstone/ISEP#24`。
|
||||
|
||||
- **M4.6** 🔴 **release note 寫在 Gitea Releases 裡,不寫在 README。**(leo 2026-08-20:「release 不是寫在 readme,要放在 release 裡」)
|
||||
README 不得自行宣稱版本號——那會產生第二份會漂移的版本真相。
|
||||
|
||||
@@ -248,7 +241,7 @@ s/triage ──驗傷通過──▶ s/backlog ──排進 milestone──▶ s
|
||||
| E2 | PR 不關聯 issue | PR 模板必含 `closes #`,缺漏即 fail | ❌ 待建 |
|
||||
| E3 | 手動關 leaf | hook 攔截;只放行 `close/*` 與 §6.2 人執路徑 | ❌ 待建 |
|
||||
| E4 | 被 block 的票先關 | Gitea issue dependency | ✅ 平台原生,已驗證可用 |
|
||||
| E5 | Milestone 悄悄過期 | job:到期做降 scope 對帳 + 通知(**不自動關、不自動打 tag**) | ❌ 待建(先做成手動腳本,見 §8.2) |
|
||||
| E5 | Milestone 悄悄過期 | job:到期只通知 leo(**不自動關、不打 tag、不移票**,M4.3) | ❌ 待建(先做成手動腳本,見 §8.2) |
|
||||
| E6 | 代理越過人閘 | PreToolUse hook | ◐ 現有 `irreversible-dispatch-guard.sh`/`prod-write-guard.sh` 覆蓋一部分 |
|
||||
| E7 | SDD 內出現 leaf 連結 | pre-commit lint | ❌ 待建 |
|
||||
| E8 | Tracking issue 掛 milestone | job 摘除並告警 | ❌ 待建 |
|
||||
@@ -582,6 +575,47 @@ leo 08-17 診斷見公理 3;08-17 實測:文字層的閘 **8 次誤攔、0
|
||||
|
||||
---
|
||||
|
||||
## 15. 三源整合定案(leo 2026-08-20 核准)
|
||||
|
||||
三源=claude.ai 兩份規劃(v0.5.0 draft+`InkStoneCo#40` forge-discipline)× 45 張舊票 × 既有機制。
|
||||
|
||||
### 15.1 方向(四句,其餘全是它們的展開)
|
||||
|
||||
1. **機制只有一份。** 環境、標籤、規範、版本號都是單一真相源;本機與雲端裝同一個 plugin,沒有子集。
|
||||
2. **閘封動作,不封措辭。** 伺服器端擋不可逆;手寫 hook 擋高代價動作(block);
|
||||
hookify+additionalContext 只提醒(永遠 warn);skill 管 regex 表達不了的判斷。
|
||||
3. **一切要留痕、可量測。** 閘的動作、開場全局、里程碑百分比——leo 看到的畫面就是實況。
|
||||
4. **進度=舊票關掉幾張。** 版本只是讓閘生效的載具,release note 寫明關了哪張票。
|
||||
|
||||
### 15.2 工作順序(六個里程碑,各 repo 同名,內容一經確定不增不減)
|
||||
|
||||
| 順 | 里程碑 | 來源票 |
|
||||
|---|---|---|
|
||||
| 0 | 讓閘擋對東西 | `InkStoneCo#56 #23 #22 #36 #55 #1` |
|
||||
| 1 | 讓規則的效果看得見 | `InkStoneCo#48 #17 #20 #18`、`Arcrun#86 #142 #81` |
|
||||
| 2 | 同一件事只留一份 | `InkStoneCo#14 #57 #37 #11`、`arcrun-rag#116 #123 #122 #88`、`Arcrun#109`、`system-dev-template#2 #3` |
|
||||
| 3 | 不要卡在 leo 身上 | `InkStoneCo#15 #38 #34 #31 #52` |
|
||||
| 4 | 派工與交付紀律 | `InkStoneCo#12 #28`、`arcrun-rag#47 #124`、`Arcrun#93 #143 #131`、`system-dev-template#7` |
|
||||
| 5 | 票與文件歸位 | `InkStoneCo#9 #49 #10 #8 #5`、`system-dev-template#1 #4` |
|
||||
|
||||
排序判準=「什麼擋住什麼」:閘不修好,其他群的成果會被誤攔咬到;沒有留痕,修了也量不出變好。
|
||||
`#40` 是憲法,貫穿全部;`#48` 是 `#40` warn 校準的資料前提,所以排群 1 之首。
|
||||
|
||||
### 15.3 三處矛盾的定案
|
||||
|
||||
1. `#40`「hook 只減不增」vs E 清單要 ~11 支新閘 ⇒ 新封路優先做在**伺服器端**或 **warn 層**;
|
||||
要新增 `.sh` 必須在 PR 說明為何前兩層做不到。
|
||||
2. warn-first vs hookify 的 warn 不進 AI ⇒ warn 一律用 `additionalContext`;
|
||||
新規則從 warn 出生,憑 `#48` 的留痕數據才升 block。
|
||||
3. milestone 到期自動關+打 tag ⇒ 否決(M4.3)。
|
||||
|
||||
### 15.4 待 leo 裁(不擋群 0 開工)
|
||||
|
||||
`#37` 兩份 repo 哪份為準(建議 `inkstone/*`)/`#5` `#8` 轉 hub 或作廢/
|
||||
08-20 兩支未經 warn 期的 block 閘,隨盤點表(`docs/hooks-inventory.md`)一起審。
|
||||
|
||||
---
|
||||
|
||||
## 附:Label 全集(快照;唯一真相為 `labels.yaml`)
|
||||
|
||||
```
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
#!/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)
|
||||
# 背景:兩個帳號因高頻動作被 flag 拿不回(幾十顆星+issues 全損)。
|
||||
# 設計:戰鬥機武器保險模式——平時所有 github.com 接觸一律機械擋下(exit 2),
|
||||
@@ -33,6 +36,13 @@ except Exception:
|
||||
|
||||
[ -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 比喻定調):
|
||||
# GitHub 不在乎你「讀」(clone/fetch/抓 release,不管實名匿名、自己的還別人的——那是它原本的功能,
|
||||
# 像 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。
|
||||
HIT=""
|
||||
# ① 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)"
|
||||
# ② 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)"
|
||||
# ②b 🔴 2026-08-05 補漏:上面那條只認指令裡的 **github.com 字面**
|
||||
# ⇒ `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 下會失敗
|
||||
# (見檔頭「2026-08-10 leo 簡化」那段的實撞紀錄)。字面查得到就直接判定,
|
||||
# 查不到才退回舊的「解 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 就擋」,
|
||||
# 於是**連 commit message 提到那個網址都會被擋**(我自己第一次要 commit 就撞到)。
|
||||
# ⇒ 改成必須是「push 的目標」:github.com 要出現在 push 之後、且中間不跨命令分隔符。
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
#!/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 硬攔截)
|
||||
#
|
||||
# 【leo 2026-08-07 立】原話:
|
||||
@@ -42,6 +45,9 @@ else
|
||||
BASH_CMD=""
|
||||
fi
|
||||
if [ -n "${BASH_CMD:-}" ]; then
|
||||
# 逃生口跟 Write/Edit 分支同一個標記(留痕,commit 說明理由)——加了這支才會真的擋,
|
||||
# 之前 Bash 分支引用的檔案不存在、永遠印 "OK",逃生口自然也沒人需要,現在補上。
|
||||
case "$BASH_CMD" in *kbdb-sql-ok*) exit 0 ;; esac
|
||||
# 🔴 2026-08-07 修正:第一版做「整條命令字串比對」,結果**第四次誤擋總管**——
|
||||
# 誤擋的是 `git commit -m "...引用了那個命令字面..."`:那是在**描述**這件事,
|
||||
# 不是在執行它。同一天已被自己的閘誤擋三次(Task 分支),這是第四次。
|
||||
@@ -49,7 +55,7 @@ if [ -n "${BASH_CMD:-}" ]; then
|
||||
# 引號內、heredoc 內、commit message 內的同樣字面一律不算。
|
||||
# 判斷邏輯住在獨立檔(見該檔開頭:內嵌逃逸讓這道閘改一次壞一次)
|
||||
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
|
||||
cat >&2 <<'EOB'
|
||||
🧱 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
|
||||
# 管什麼: 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):**兩層手動確認閘**
|
||||
#
|
||||
# 🔴 立這道閘的來由(leo 2026-08-10):
|
||||
@@ -34,9 +37,23 @@ except Exception: print("")
|
||||
|
||||
[ -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
|
||||
*"git push"*|*"wrangler deploy"*|*"wrangler publish"*|*"wrangler versions deploy"*) ;;
|
||||
sed\ *|cat\ *|grep\ *|head\ *|tail\ *|wc\ *|less\ *|ls\ *|awk\ *|rg\ *|echo\ *) exit 0 ;;
|
||||
*"git commit"*|*"git add"*|*"git tag"*|*"git stash"*) exit 0 ;;
|
||||
*"git status"*|*"git log"*|*"git diff"*|*"git show"*|*"git branch"*) exit 0 ;;
|
||||
@@ -91,73 +108,78 @@ stamp_ok() {
|
||||
return 0
|
||||
}
|
||||
|
||||
if true; then
|
||||
case "$CMD" in
|
||||
*"git push"*)
|
||||
# 只擋打到 main/master 的;推自己的 feature 分支照常放行
|
||||
case "$CMD" in
|
||||
# 🔴 2026-08-12 拿掉 `push -u` / `push --set-upstream` 這兩個條件。
|
||||
# 它們本來是想抓「沒寫分支的 push」,但實際抓到的是
|
||||
# `git push -u gitea fix/xxx`——**subagent 發表自己分支的標準動作**
|
||||
# (第一次推當然要 -u)。⇒ 舊版等於「agent 永遠推不出自己的分支」,
|
||||
# 而 leo 2026-08-12 的設計是「主線禁止動,大家都走 PR」,推分支是那條路的第一步。
|
||||
# 08-12 當天四張 PR 全是繞成 `git push gitea a:a` 才推出去的。
|
||||
# `*main*`/`*master*` 兩條照舊——真正該擋的是目標分支,不是有沒有帶旗標。
|
||||
*main*|*master*)
|
||||
stamp_ok && exit 0
|
||||
# reworked 2026-08-20 (inkstone/InkStoneCo#23, #56): 'does this command contain
|
||||
# git push' moved from a whole-string substring test to a command-position test --
|
||||
# same pattern already validated on release-tag-guard.sh: the phrase only counts
|
||||
# at the start of the string, or right after ; & | ( ` && ||. A heredoc body was
|
||||
# already stripped above; this closes the remaining gap where the phrase shows up
|
||||
# quoted inside the *same* command line (e.g. a python -c call whose string argument
|
||||
# discusses git push as prose).
|
||||
if printf '%s' "$CMD" | grep -qE '(^|[;&|(`]|&&|\|\|)[[:space:]]*git([[:space:]]+[^;&|]*)?[[:space:]]+push([[:space:]]|$)'; then
|
||||
# only block pushes that target main/master; pushing your own feature branch is fine
|
||||
#
|
||||
# 2026-08-12: dropped the `push -u` / `push --set-upstream` conditions -- those were
|
||||
# meant to catch "push with no branch named", but what they actually caught was
|
||||
# `git push -u gitea fix/xxx`, the normal first-push-of-a-branch shape for a
|
||||
# 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 原話:「**它會問你的意見,所以每個你叫起來的 subagent 都有名字。**」
|
||||
#
|
||||
# 做得到的與做不到的,先講清楚:
|
||||
# ❌ **做不到「同步問總管」**——hook 跑在子 session 自己的行程裡,總管在另一個行程。
|
||||
# 要同步問只能 block 等一個檔案出現,那會把 subagent 掛死在那裡。
|
||||
# ✅ **做得到「當場擋 + 留下原始請求」**:總管在自己的迴圈裡讀這個目錄,
|
||||
# 看到的是 repo/分支/逐筆 commit 的**原始資料**,不是 subagent 的散文轉述。
|
||||
# ——這才是名字真正值錢的地方:**不是判斷你是誰,是留下是誰要求的**。
|
||||
#
|
||||
# 🔴 身分的方向刻意不改:**沒有名字不等於總管**(那是 fail-open——
|
||||
# 子 session 繼承環境變數,把名字拿掉就升格了)。放行的唯一憑證仍然只有
|
||||
# 那枚綁 repo、用完即丟的戳記。名字只是署名,不是權限。
|
||||
# 📌 名字只在 `claude -p` 起的子 session 上可靠(乾淨的環境邊界);
|
||||
# Agent tool 起的 subagent 與總管同一個行程、共用環境 ⇒ 那條路上名字塞不進也擦不掉。
|
||||
# 而改子 repo code 的正路本來就是 `claude -p`,所以夠用。
|
||||
_who="${CLAUDE_AGENT_NAME:-未署名}"
|
||||
_hookdir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) || _hookdir=""
|
||||
_reqdir="${_hookdir%/hooks}/pending-main-push"
|
||||
if [ -n "$_hookdir" ] && mkdir -p "$_reqdir" 2>/dev/null; then
|
||||
_root=$(git rev-parse --show-toplevel 2>/dev/null || printf 'unknown')
|
||||
# 檔名只用 ASCII(`未署名` 之類會被 tr 打成一排 dash,看不出是誰)
|
||||
_slugwho=$(printf '%s' "${CLAUDE_AGENT_NAME:-unnamed}" | tr -c 'A-Za-z0-9._-' '-')
|
||||
case "$_slugwho" in *[!-]*) : ;; *) _slugwho=unnamed ;; esac
|
||||
# ⚠️ 先 printf 再 tr:`basename` 會帶一個換行,直接餵 tr 會變成結尾多一根 dash
|
||||
_slugrepo=$(printf '%s' "$(basename "$_root")" | tr -c 'A-Za-z0-9._-' '-')
|
||||
_slug="${_slugwho}--${_slugrepo}"
|
||||
# ⚠️ 這幾行刻意用 `printf '%s\n' "整句"`,不要把內容寫進 printf 的格式字串裡。
|
||||
# 2026-08-12 實撞:格式字串裡同時有反引號與 %s 時,那幾行整行不見(而前後行都在),
|
||||
# ——**寫完當場肉眼檢查產出的檔案才發現**,hook 自己不會叫。內容一律當資料傳。
|
||||
_branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || printf '?')
|
||||
_when=$(date '+%Y-%m-%d %H:%M:%S' 2>/dev/null || printf '?')
|
||||
_fence='```'
|
||||
{
|
||||
printf '%s\n\n' "# 推 main 的請求:$_who"
|
||||
printf '%s\n' "- repo:$_root"
|
||||
printf '%s\n' "- 分支:$_branch"
|
||||
printf '%s\n\n' "- 時間:$_when"
|
||||
printf '%s\n' "- 它想跑的指令:"
|
||||
printf '%s\n%s\n%s\n\n' "$_fence" "$CMD" "$_fence"
|
||||
printf '%s\n\n%s\n' "## 還沒推上去的 commit(原始資料,不是轉述)" "$_fence"
|
||||
git log --oneline '@{upstream}..HEAD' 2>/dev/null \
|
||||
|| git log --oneline -20 2>/dev/null \
|
||||
|| printf '(列不出來)\n'
|
||||
printf '%s\n\n%s\n\n%s\n' "$_fence" "## 改了哪些檔" "$_fence"
|
||||
git diff --stat '@{upstream}..HEAD' 2>/dev/null | tail -40 || printf '(列不出來)\n'
|
||||
printf '%s\n\n---\n%s\n' "$_fence" "總管裁完請刪掉這個檔——留著代表「還沒裁」。"
|
||||
} > "$_reqdir/$_slug.md" 2>/dev/null || true
|
||||
fi
|
||||
# ── 擋下的同時,把「誰想推什麼」留成一份請求(leo 2026-08-12)───────────
|
||||
#
|
||||
# leo 原話:「**它會問你的意見,所以每個你叫起來的 subagent 都有名字。**」
|
||||
#
|
||||
# 做得到的與做不到的,先講清楚:
|
||||
# ❌ **做不到「同步問總管」**——hook 跑在子 session 自己的行程裡,總管在另一個行程。
|
||||
# 要同步問只能 block 等一個檔案出現,那會把 subagent 掛死在那裡。
|
||||
# ✅ **做得到「當場擋 + 留下原始請求」**:總管在自己的迴圈裡讀這個目錄,
|
||||
# 看到的是 repo/分支/逐筆 commit 的**原始資料**,不是 subagent 的散文轉述。
|
||||
# ——這才是名字真正值錢的地方:**不是判斷你是誰,是留下是誰要求的**。
|
||||
#
|
||||
# 🔴 身分的方向刻意不改:**沒有名字不等於總管**(那是 fail-open——
|
||||
# 子 session 繼承環境變數,把名字拿掉就升格了)。放行的唯一憑證仍然只有
|
||||
# 那枚綁 repo、用完即丟的戳記。名字只是署名,不是權限。
|
||||
# 📌 名字只在 `claude -p` 起的子 session 上可靠(乾淨的環境邊界);
|
||||
# Agent tool 起的 subagent 與總管同一個行程、共用環境 ⇒ 那條路上名字塞不進也擦不掉。
|
||||
# 而改子 repo code 的正路本來就是 `claude -p`,所以夠用。
|
||||
_who="${CLAUDE_AGENT_NAME:-未署名}"
|
||||
_hookdir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) || _hookdir=""
|
||||
_reqdir="${_hookdir%/hooks}/pending-main-push"
|
||||
if [ -n "$_hookdir" ] && mkdir -p "$_reqdir" 2>/dev/null; then
|
||||
_root=$(git rev-parse --show-toplevel 2>/dev/null || printf 'unknown')
|
||||
# 檔名只用 ASCII(`未署名` 之類會被 tr 打成一排 dash,看不出是誰)
|
||||
_slugwho=$(printf '%s' "${CLAUDE_AGENT_NAME:-unnamed}" | tr -c 'A-Za-z0-9._-' '-')
|
||||
case "$_slugwho" in *[!-]*) : ;; *) _slugwho=unnamed ;; esac
|
||||
# ⚠️ 先 printf 再 tr:`basename` 會帶一個換行,直接餵 tr 會變成結尾多一根 dash
|
||||
_slugrepo=$(printf '%s' "$(basename "$_root")" | tr -c 'A-Za-z0-9._-' '-')
|
||||
_slug="${_slugwho}--${_slugrepo}"
|
||||
# ⚠️ 這幾行刻意用 `printf '%s\n' "整句"`,不要把內容寫進 printf 的格式字串裡。
|
||||
# 2026-08-12 實撞:格式字串裡同時有反引號與 %s 時,那幾行整行不見(而前後行都在),
|
||||
# ——**寫完當場肉眼檢查產出的檔案才發現**,hook 自己不會叫。內容一律當資料傳。
|
||||
_branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || printf '?')
|
||||
_when=$(date '+%Y-%m-%d %H:%M:%S' 2>/dev/null || printf '?')
|
||||
_fence='```'
|
||||
{
|
||||
printf '%s\n\n' "# 推 main 的請求:$_who"
|
||||
printf '%s\n' "- repo:$_root"
|
||||
printf '%s\n' "- 分支:$_branch"
|
||||
printf '%s\n\n' "- 時間:$_when"
|
||||
printf '%s\n' "- 它想跑的指令:"
|
||||
printf '%s\n%s\n%s\n\n' "$_fence" "$CMD" "$_fence"
|
||||
printf '%s\n\n%s\n' "## 還沒推上去的 commit(原始資料,不是轉述)" "$_fence"
|
||||
git log --oneline '@{upstream}..HEAD' 2>/dev/null \
|
||||
|| git log --oneline -20 2>/dev/null \
|
||||
|| printf '(列不出來)\n'
|
||||
printf '%s\n\n%s\n\n%s\n' "$_fence" "## 改了哪些檔" "$_fence"
|
||||
git diff --stat '@{upstream}..HEAD' 2>/dev/null | tail -40 || printf '(列不出來)\n'
|
||||
printf '%s\n\n---\n%s\n' "$_fence" "總管裁完請刪掉這個檔——留著代表「還沒裁」。"
|
||||
} > "$_reqdir/$_slug.md" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
cat >&2 <<'MSG'
|
||||
cat >&2 <<'MSG'
|
||||
🚫 推 main 要先有「總管決定了」的戳記(leo 2026-08-10 立)
|
||||
|
||||
leo 原話:
|
||||
@@ -196,11 +218,8 @@ leo 原話:
|
||||
|
||||
【真的該推 main 的例外】不存在。交回總管,一句話的事。
|
||||
MSG
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
esac
|
||||
exit 2
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── ② prod 部署要 leo 親手解保險 ────────────────────────────────────────
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
#!/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 出貨鏈**。
|
||||
#
|
||||
# 🔴 立這道閘的來由(leo 2026-08-08):
|
||||
@@ -32,6 +35,12 @@ except Exception: print("")
|
||||
' 2>/dev/null || echo "")
|
||||
[ -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 安裝器部署。
|
||||
# staging 的同名動作要放行——所以先排除帶 staging 字樣的命令。
|
||||
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