3a1c3d4969
# Conflicts: # docs/TESTING.md
335 lines
20 KiB
Bash
Executable File
335 lines
20 KiB
Bash
Executable File
#!/bin/sh
|
||
# prod-write-guard.sh — PreToolUse(Bash + 會寫到線上實例的 MCP 工具)
|
||
#
|
||
# 🔴 立這道閘的來由(leo 2026-08-11,當天實際發生):
|
||
# 兩條 subagent 在同一個 session 裡把東西推上了 leo21c 線上實例——
|
||
# ① `#79` 覆寫了現役出貨管線的 `ship_refresh_cdn` 工作流定義
|
||
# ② `#85` 推了一支新工作流上去
|
||
# **兩次都沒有被任何閘擋到**,儘管派工單第一條紅線就寫著「不可以部署到正式環境」。
|
||
#
|
||
# leo 當場點破,而他的說法比「再補一種推送」更根本:
|
||
# 「**其實問題是它跟正式環境是斷開的,所以 hook 要不讓它對正式環境做任何推送,
|
||
# 不是某種推送,這種只有透過你。**」
|
||
#
|
||
# ⇒ 判準不是「這是哪一種推送」,是「**這個動作會不會改到線上那台**」。
|
||
# 會 ⇒ 擋,交回總管。總管要動之前,上面還有 leo 的保險(D20/.github-armed)。
|
||
#
|
||
# 為什麼既有的閘接不住(2026-08-11 實查,不是推測):
|
||
# · `main-and-prod-push-guard.sh` 只認 `wrangler deploy|publish|versions deploy`
|
||
# · `stage-before-prod-guard.sh` 只比對 `arcrun-rag-bundles`/`github-arm`/`publish-github`
|
||
# · **兩支都只掛在 `Bash` 上**,而 `arcrun_push_workflow` 是 **MCP 工具,根本不經過 Bash**
|
||
# ⇒ 推工作流定義這條路,從頭到尾沒有任何一雙眼睛。
|
||
#
|
||
# 設計紀律(沿用前兩支用血換來的):
|
||
# • **fail-closed**:內部錯誤不准變成靜默放行。exit 1 不擋,只有 exit 2 才擋。
|
||
# • **先排除「談論/讀取/演練」再比對**:誤攔比漏攔更容易殺死一道閘
|
||
# ——被擋得莫名其妙,人就會想辦法繞過它。
|
||
# • **stage 一律自由**:擋的是 prod,不是所有部署。測試場本來就該隨便跑。
|
||
set -eu
|
||
|
||
INPUT="$(cat)"
|
||
|
||
TOOL=$(printf '%s' "$INPUT" | python3 -c '
|
||
import sys, json
|
||
try: print(json.load(sys.stdin).get("tool_name", "") or "")
|
||
except Exception: print("")
|
||
' 2>/dev/null || printf '')
|
||
|
||
CMD=$(printf '%s' "$INPUT" | python3 -c '
|
||
import sys, json
|
||
try: print(json.load(sys.stdin).get("tool_input", {}).get("command", "") or "")
|
||
except Exception: print("")
|
||
' 2>/dev/null || printf '')
|
||
|
||
# ── 🔴 卡點二(inkstone/ISEP#63 → comment 5106):「執行它」vs「談論它」,第八次 ──
|
||
#
|
||
# 總管要把「本閘擋住了發通知」這件事**貼到票上**,而那則留言的內文**引用了**
|
||
# 本閘的判準(實例主機名、寫入旗標的字樣)⇒ **一則留言被自己擋下來。**
|
||
# 同款的第八次(`inkstone/InkStoneCo#23` 的標題已經寫著「第七次」)。
|
||
# ⇒ 本次修這支閘時**又撞了一次**:連「把修法寫進檔案」的那條指令都被擋,
|
||
# 因為它的內文引用了主機名。那就是這一格最好的證據。
|
||
#
|
||
# leo 2026-08-17 的診斷:文字層的閘那天 **8 次誤攔、0 次正確攔截**,且方向穩定——
|
||
# **紅線寫得越細,命中關鍵字的機率越高 ⇒ 那些閘在懲罰謹慎。**
|
||
#
|
||
# 那一段內文**是資料,不是指令**。這支 lib(`github-contact-guard`/
|
||
# `main-and-prod-push-guard`/`stage-before-prod-guard` 三支已經在用)只剝內文、
|
||
# **保留起始行**——所以真的在部署的寫法(起始行就帶著實例網址與寫入旗標)
|
||
# 仍然整條看得到、照樣擋。
|
||
#
|
||
# ⚠️ 繞法(把內文先寫成檔案、再用不含關鍵字的指令送出)本來就通,
|
||
# 但那正是 leo 說的「**人就會學會繞過它,那它就等於不存在**」⇒ 要修的是閘。
|
||
CMD=$(printf '%s' "$CMD" | python3 "$(dirname "$0")/lib/strip_""heredoc.py" 2>/dev/null || printf '%s' "$CMD")
|
||
|
||
# ── 總管戳記:**單次、15 分鐘失效** ────────────────────────────────────
|
||
#
|
||
# 🔴 這一版一開始寫成「15 分鐘內無限次放行」,而同一天 `main-and-prod-push-guard.sh`
|
||
# 就是因為這個寫法被穿透的:總管為了推頂層 touch 了戳記,
|
||
# **15 分鐘內一條並行的 subagent 把 commit 推上了另一個 repo 的 main**。
|
||
# ⇒ **多條 subagent 並行時,「時間窗」本身就是漏洞。** 所以改成單次用完即丟。
|
||
# ⇒ 而且只在**真的要擋的那一刻**才檢查——放在檔頭會被任何一條無關指令把戳記燒掉。
|
||
# 🔴 戳記的 mtime 一律走 lib/mtime.sh:舊寫法(`stat -f %m || stat -c %Y`)在 Linux 上
|
||
# **永遠算不出數字**(GNU 的 `-f` 是「檔案系統資訊」,它一邊回非零一邊吐一整段文字,
|
||
# 於是後面接上的秒數被那段垃圾污染)⇒ 這支閘給總管的逃生口在雲端整個焊死,
|
||
# 而閘不會告訴他門是壞的(inkstone/ISEP#90 ④,全文見 hooks/lib/mtime.sh)。
|
||
# lib 不在時就地補一份同順序的定義——這道門不能因為少一個檔案就再關上一次。
|
||
. "$(dirname "$0")/lib/mtime.sh" 2>/dev/null || file_mtime() {
|
||
_fm=$(stat -c %Y "$1" 2>/dev/null || true)
|
||
case "${_fm:-}" in ''|*[!0-9]*) _fm=$(stat -f %m "$1" 2>/dev/null || true) ;; esac
|
||
case "${_fm:-}" in ''|*[!0-9]*) _fm=0 ;; esac
|
||
printf '%s' "$_fm"
|
||
}
|
||
|
||
STAMP="/tmp/.prod-write-ok"
|
||
stamp_ok() {
|
||
[ -f "$STAMP" ] || return 1
|
||
NOW=$(date +%s 2>/dev/null || echo 0)
|
||
MT=$(file_mtime "$STAMP")
|
||
case "$NOW$MT" in *[!0-9]*) return 1 ;; esac
|
||
[ "$NOW" -gt 0 ] && [ "$MT" -gt 0 ] || return 1
|
||
[ $((NOW - MT)) -lt 900 ] || return 1
|
||
rm -f "$STAMP" 2>/dev/null || true # 單次:用完即丟
|
||
return 0
|
||
}
|
||
|
||
# ══ ① MCP 工具:會改到線上實例的那幾支 ═════════════════════════════════
|
||
# 這一段是本閘存在的理由——上面兩支既有的閘完全看不到這裡。
|
||
case "$TOOL" in
|
||
*arcrun_push_workflow*|*arcrun_delete_workflow*|\
|
||
*arcrun_recipe_push*|*arcrun_recipe_delete*|\
|
||
*arcrun_create_tag*|*arcrun_delete_tag*|\
|
||
*arcrun_tag_resource*|*arcrun_untag_resource*|\
|
||
*kbdb_create_record*|*kbdb_create_template*)
|
||
stamp_ok && exit 0
|
||
cat >&2 <<'MSG'
|
||
🚫 這個動作會改到「線上那台」——不是你能自己按的(leo 2026-08-11 立)
|
||
|
||
leo 原話:
|
||
「**其實問題是它跟正式環境是斷開的,所以 hook 要不讓它對正式環境做任何推送,
|
||
不是某種推送,這種只有透過你。**」
|
||
|
||
⇒ 判準不是「這是哪一種推送」,是「**這會不會改到線上那台**」。會,就交回總管。
|
||
|
||
━━━ 你是 subagent ━━━
|
||
**不要想辦法過這道閘。** 把成品留在自己的分支/檔案裡,交件時寫清楚:
|
||
· 你要推什麼上去(名稱+它會覆蓋掉什麼,還是全新的)
|
||
· 為什麼現在該推、你驗過哪些(逐項,不要整包說「驗過了」)
|
||
· 推上去之後**用什麼指令可以看出它生效了**
|
||
總管會看過再決定。**這是一句話的事。**
|
||
|
||
🔴 特別注意「**覆寫現役的東西**」:2026-08-11 真的發生過——
|
||
一條 subagent 覆寫了現役出貨管線的工作流定義。那次內容是對的(舊版真的壞了),
|
||
**但「內容對不對」和「該不該先問」是兩件事**(D54)。
|
||
|
||
━━━ 你是總管 ━━━
|
||
看清楚它會覆蓋什麼(`arcrun_get_workflow` 先撈一份現況存起來,好回滾),確定要推,再:
|
||
|
||
bash "$CLAUDE_PLUGIN_ROOT/scripts/gate-ok" prod-write # ⚠️ 只放行「下一個」動作,用完即消失
|
||
(等價的手打法:touch /tmp/.prod-write-ok。gate-ok 是所有閘共用的同一個入口,
|
||
好處是它的形狀固定 ⇒ 一條規則就能事先放行,不會每次被權限層擋在門外)
|
||
|
||
戳記 15 分鐘有效。**若這一推會讓封測者/用戶當場拿到東西,那就不是你的層級——要 leo 解保險。**
|
||
MSG
|
||
exit 2
|
||
;;
|
||
esac
|
||
|
||
# ── 非 Bash 的其他工具一律放行 ─────────────────────────────────────────
|
||
[ "$TOOL" = "Bash" ] || exit 0
|
||
[ -z "$CMD" ] && exit 0
|
||
|
||
# ── 「執行它」vs「談論它」:動詞要在**指令位置**才算數 ─────────────────
|
||
#
|
||
# 🔴 2026-08-13 第六次同款誤攔(前五次記在頂層 `wiki/mistakes.md`):
|
||
# 總管要把 `#108` 的驗收證據**留言到 Gitea**,指令長這樣——
|
||
# BODY=$(cat <<'EOF' … 正文裡引述了 `acr update --force` … EOF
|
||
# )
|
||
# curl -X POST https://git.uncle6.me/api/v1/…/comments
|
||
# ⇒ 命中舊的 `*"acr update"*` ⇒ **一則留言被當成部署擋掉。**
|
||
# (curl 那半本來是對的:打 Gitea 不是打實例,下面的分支會 exit 0。
|
||
# 擋它的是那條純字串比對,而且它排在 curl 那支前面。)
|
||
#
|
||
# 本檔第 25 行自己寫著「誤攔比漏攔更容易殺死一道閘」,而這是同一形狀的第六次:
|
||
# **判準看「字串有沒有出現」,不是「它在做什麼」。**
|
||
#
|
||
# 判準改成兩層:① 剝掉 heredoc 內文(那是資料,不是指令)
|
||
# ② 動詞要在指令位置——行首/`;` `&` `|` `(` `$(` 之後/引號開頭
|
||
# 仍會擋:`bash -c "acr update"`、`a && acr push`、`npx wrangler deploy`。
|
||
# 不再擋:反引號或 markdown code span 裡的引述、commit 訊息、註解、heredoc 正文。
|
||
#
|
||
# 🔴 這段**必須算在所有豁免之前**(2026-08-13 被自己補的測試抓到):
|
||
# `cat <<'EOF' > note.md … EOF` 換行 `acr update --force`
|
||
# ⇒ 指令以 `cat ` 開頭,而下面認投送工具的 `*" acr "*` 是**空格界定**、
|
||
# 認不出換行後的 `acr` ⇒ 掉進「開頭是唯讀工具」的豁免直接放行。
|
||
# 同款的還有 `git commit -m 'x' && acr update`(吃 `*"git commit"*` 豁免)。
|
||
# ⇒ 命中指令位置的,一概不吃任何豁免。
|
||
CLI_HIT=$(printf '%s' "$CMD" | python3 -c '
|
||
import sys, re
|
||
cmd = sys.stdin.read()
|
||
cmd = re.sub(r"<<-?[ \t]*([\x27\x22]?)([A-Za-z_][A-Za-z0-9_]*)\1.*?^[ \t]*\2[ \t]*$",
|
||
" ", cmd, flags=re.S | re.M)
|
||
LAUNCH = r"(?:npx\s+|pnpm\s+(?:exec\s+)?|yarn\s+|sudo\s+|env\s+\S+=\S+\s+)*"
|
||
# 指令位置=行首/`;` `&` `|` `(` `$(` 之後/**shell 呼叫旗標後面的引號**。
|
||
# 🔴 引號本身不算(2026-08-13 被自己補的測試抓到):先前把 `\x22\x27` 直接放進
|
||
# 前綴字元類,結果 `grep \x27acr push\x27 <檔>` 被當成在執行 ⇒ 又是一次誤攔。
|
||
# 只有 `bash -c "…"`/`sh -lc \x27…\x27`/`eval "…"` 這種**真的會把字串當指令跑**的才算。
|
||
SHELLC = r"(?:bash|sh|zsh|dash|eval|xargs)\s+(?:-\w+\s+)*[\x22\x27]"
|
||
PAT = (r"(?:^|[\n;&|(]|\$\(|" + SHELLC + r")\s*" + LAUNCH
|
||
+ r"(acr|wrangler)\b([^\n;&|)]*)")
|
||
for m in re.finditer(PAT, cmd):
|
||
tool, rest = m.group(1), m.group(2)
|
||
if re.search(r"(^|\s)(--help|-h)(\s|$)", rest):
|
||
continue # 問說明不是執行
|
||
if tool == "acr" and re.search(r"\b(push|update)\b", rest):
|
||
print("cli"); break
|
||
if tool == "wrangler" and re.search(r"\b(deploy|publish)\b", rest):
|
||
print("cli"); break
|
||
' 2>/dev/null || printf '__PYFAIL__')
|
||
|
||
# fail-closed:python 掛掉不准變成靜默放行 ⇒ 退回舊的整串比對,寧可誤攔
|
||
if [ "$CLI_HIT" = "__PYFAIL__" ]; then
|
||
CLI_HIT=""
|
||
case "$CMD" in
|
||
*"acr push"*|*"acr update"*|*"wrangler deploy"*|*"wrangler publish"*) CLI_HIT="cli" ;;
|
||
esac
|
||
fi
|
||
|
||
# ══ ② Bash:先排除明確不是「推上線」的動作 ═══════════════════════════
|
||
# 關鍵字出現在 commit 訊息裡、在 sed/grep 的參數裡,都不是「執行」。
|
||
# ⚠️ 下面每一條豁免都只在 CLI_HIT 為空時才輪得到(見上)。
|
||
# 🔴 2026-08-12 修漏攔:這個「開頭是唯讀工具就放行」的豁免**本身就是一條穿牆路**。
|
||
# 實撞:`cat payload.json | curl -X POST <實例>/webhooks/named` ——
|
||
# 指令以 `cat ` 開頭 ⇒ 整條直接 exit 0 ⇒ **寫進線上實例,本閘一聲不吭**。
|
||
# 任何人只要在前面接一個管線就能繞過,而這正是本檔下方註解自己警告的
|
||
# 「換個工具就穿過去了」。⇒ 豁免只在「整條指令裡沒有任何投送工具」時才成立。
|
||
if [ -z "$CLI_HIT" ]; then
|
||
case "$CMD" in
|
||
*curl*|*wget*|*httpie*|*" http "*|*wrangler*|*" acr "*|acr\ *) ;; # 有投送工具 → 不吃開頭豁免
|
||
sed\ *|cat\ *|grep\ *|head\ *|tail\ *|wc\ *|less\ *|ls\ *|awk\ *|rg\ *|echo\ *|find\ *) exit 0 ;;
|
||
esac
|
||
case "$CMD" in
|
||
*"git commit"*|*"git add"*|*"git tag"*|*"git stash"*) exit 0 ;;
|
||
*"git status"*|*"git log"*|*"git diff"*|*"git show"*|*"git branch"*) exit 0 ;;
|
||
*" --dry-run"*|*"--dry-run "*) exit 0 ;;
|
||
# 問說明不是執行(實測撞到:`acr update --help` 被自己擋掉)
|
||
# ——`--help` 現在也在 CLI_HIT 那層逐段判,這裡留著給 curl/wrangler 之外的情況
|
||
*" --help"*|*" -h"*|*" help "*) exit 0 ;;
|
||
esac
|
||
fi
|
||
|
||
# stage/staging 一律自由——擋的是 prod,不是所有部署
|
||
#
|
||
# 🔴 2026-08-12 補:`youlin`(youlin-hsieh-dev)**就是測試場**,不是 prod。
|
||
# arcrun-rag CLAUDE.md 白紙黑字:「要看範例或做任何測試 → **只在 youlin(D37 stage)**」,
|
||
# 而 leo 2026-07-25 令「測試一律用 youlin,別拿 leo21c 當探針(會製造假信號)」。
|
||
# 本閘原本只認 `staging` 這個字 ⇒ 打 youlin 做實驗被誤擋
|
||
# ⇒ **這種誤攔比漏攔更危險**:被擋得莫名其妙,人就會學會繞過它,那它就等於不存在。
|
||
case "$CMD" in
|
||
*staging*|*-staging*|*"--env stage"*|*"--env=stage"*|*stage.*|*"-staging"*) exit 0 ;;
|
||
*youlin-hsieh-dev*|*"X-Arcrun-API-Key: youlin"*) exit 0 ;;
|
||
esac
|
||
|
||
|
||
# 會把東西推上線的 CLI 動作
|
||
# `acr update`=「拉新 release 並重新部署到你的 Cloudflare」(它 --help 的原話)
|
||
# ⇒ 它就是一次完整的實例部署,比 `acr push` 影響更大(一次重部所有 worker)。
|
||
CMD_M="${CLI_HIT}|$CMD"
|
||
case "$CMD_M" in
|
||
cli\|*) ;;
|
||
# 直接對實例跑 wrangler 也算——`main-and-prod-push-guard.sh` 有擋,但它認的是
|
||
# 「有沒有 .github-armed」;這裡多一層是因為打錯帳號的風險在 wrangler 這一側:
|
||
# 2026-08-11 實查 kbdb/wrangler.toml 的 D1 id 是**官方 prod 的**,
|
||
# 而本機 wrangler 登入的是官方帳號 ⇒ 照直覺跑會把 self-hosted 實例接到官方資料庫上。
|
||
# (2026-08-13:`wrangler deploy|publish` 已併進上面的 CLI_HIT 判定——
|
||
# 原本這裡也是整串比對,「文章裡提到 wrangler deploy」同樣會被誤擋。)
|
||
# 🔴 2026-08-12 補的洞:**直接用 HTTP 打實例的寫入端點**。
|
||
# 由來:總管要把 `ship_refresh_cdn` v2 推上 leo21c,用的是
|
||
# `curl -X POST .../webhooks/named`(工作流部署端點,認證只是 namespace 明碼)
|
||
# ——本閘的三個 CLI 關鍵字**一個都沒命中**,等於「不被擋地寫進 leo 那台」。
|
||
# ⇒ 這正是 D75 那句話要防的事:leo 說「**不是某種推送**,是會不會改到線上那台」。
|
||
# 本閘卻是照「某幾種指令」寫的 ⇒ 換個工具就穿過去了。
|
||
# 判準改成看**目標**:打到實例主機(workers.dev/arcrun.dev)+**帶寫入語意**才擋。
|
||
# 刻意不擋的:讀(沒有 -X 寫入動詞也沒有 body)、打 Gitea(git.uncle6.me 不是實例)、
|
||
# 以及上面已經放行的 staging。
|
||
*curl*|*httpie*|*" http "*|*wget*)
|
||
case "$CMD" in
|
||
*workers.dev*|*arcrun.dev*)
|
||
# 🔴 卡點一(inkstone/ISEP#63 → comment 5106):發一則通知被當成部署擋下
|
||
#
|
||
# leo 2026-08-28 要的是「每小時一個版本,用 Telegram 通知我」。
|
||
# 通道是 `notify_leo` 工作流(wiki `agent-memory.md`:一條 curl、不需任何金鑰)。
|
||
# 而發一則 Telegram **一定是帶 body 的寫入型請求打到那台實例**
|
||
# ⇒ 在本閘眼裡它跟「部署一個工作流上線」長得一模一樣。
|
||
#
|
||
# 🔴 **這不是誤攔,是解析度不夠**:兩者打的是同一個 named webhook 家族路徑。
|
||
# 真正的差別在**打的是哪一個 named webhook**——
|
||
# `…/webhooks/named/<ns>/notify_leo/trigger` = 發一則訊息,什麼都沒改
|
||
# `…/webhooks/named`(無名字、無 /trigger) = 部署工作流定義
|
||
# `…/webhooks/named/<ns>/ship_refresh_cdn/trigger` = 真的動線上狀態
|
||
#
|
||
# 判準因此看**目標的結構**(路徑形狀+工作流名字),不是「有沒有寫入旗標」:
|
||
# 指令裡打到實例的**每一個**網址都是白名單裡的通知型 trigger ⇒ 放行
|
||
# 只要有一個不是(含部署端點、含別的工作流)⇒ 照擋
|
||
# ⚠️ 放行的範圍刻意只有一個名字。**不放行整個 named webhook 家族**——
|
||
# 那會把部署一起放掉,等於拆了這道閘。
|
||
NOTIFY_ONLY=$(printf '%s' "$CMD" | python3 -c '
|
||
import re, sys
|
||
cmd = sys.stdin.read()
|
||
ALLOW = {"notify_leo"} # 通知型:只發訊息,不改線上任何狀態
|
||
urls = re.findall(r"https?://[^\s\"\x27<>]+", cmd)
|
||
inst = [u for u in urls if re.search(r"://[^/]*(workers\.dev|arcrun\.dev)", u)]
|
||
if not inst:
|
||
raise SystemExit(0) # 沒有實例網址 ⇒ 這裡不表態,交給下面的寫入語意判斷
|
||
PAT = re.compile(r"https?://[^/]+/webhooks/named/[^/]+/([A-Za-z0-9_-]+)/trigger/?$")
|
||
def notify(u):
|
||
m = PAT.match(u.rstrip(",;)"))
|
||
return bool(m) and m.group(1) in ALLOW
|
||
print("notify-only" if all(notify(u) for u in inst) else "")
|
||
' 2>/dev/null || printf '')
|
||
[ "$NOTIFY_ONLY" = "notify-only" ] && exit 0
|
||
|
||
# 🔴 2026-08-12 修誤攔:` -d ` 不只是 curl 的 --data,**一堆唯讀工具也用 -d**
|
||
# (`tr -d`/`cut -d=`/`sort -d`/`date -d`/`xargs -d`/`paste -d`…)。
|
||
# 實撞:總管要「唯讀」驗證一把金鑰能不能開 kbdb 的閘,指令第一行是
|
||
# ACC=$(grep … | cut -d= -f2 | tr -d '\r')
|
||
# ⇒ `tr -d ` 命中 `*" -d "*` ⇒ **一條純 GET 被當成寫入擋掉**。
|
||
# 這正是本檔第 124 行自己寫的那句:「誤攔比漏攔更危險——被擋得莫名其妙,
|
||
# 人就會學會繞過它,那它就等於不存在。」
|
||
# ⇒ 比對前先把「已知的唯讀 -d 用法」剪掉,再看剩下的有沒有寫入語意。
|
||
CMD_W=$(printf '%s' "$CMD" | sed -E 's/(^|[|;&( ])(tr|cut|sort|uniq|date|xargs|paste|join|du|logger|split|comm)[[:space:]]+-d/\1\2 __READONLY_D__/g')
|
||
case "$CMD_W" in
|
||
*"-X POST"*|*"-X PUT"*|*"-X PATCH"*|*"-X DELETE"*|\
|
||
*"--request POST"*|*"--request PUT"*|*"--request PATCH"*|*"--request DELETE"*|\
|
||
*"--data"*|*" -d "*|*" -d'"*|*' -d"'*) ;;
|
||
*) exit 0 ;;
|
||
esac
|
||
;;
|
||
*) exit 0 ;;
|
||
esac
|
||
;;
|
||
*) exit 0 ;;
|
||
esac
|
||
|
||
stamp_ok && exit 0
|
||
|
||
cat >&2 <<'MSG'
|
||
🚫 `acr push` 會把東西推上線上實例——交回總管(leo 2026-08-11 立)
|
||
|
||
leo 原話:
|
||
「**hook 要不讓它對正式環境做任何推送,不是某種推送,這種只有透過你。**」
|
||
|
||
━━━ 你是 subagent ━━━
|
||
把 YAML/定義檔留著,交件時附上檔案路徑與「它會覆蓋什麼」,由總管推。
|
||
|
||
━━━ 你是總管 ━━━
|
||
確認過要推什麼、會蓋掉什麼之後:
|
||
|
||
bash "$CLAUDE_PLUGIN_ROOT/scripts/gate-ok" prod-write # ⚠️ 只放行「下一個」動作,用完即消失
|
||
(等價的手打法:touch /tmp/.prod-write-ok。gate-ok 是所有閘共用的同一個入口,
|
||
好處是它的形狀固定 ⇒ 一條規則就能事先放行,不會每次被權限層擋在門外)
|
||
|
||
⚠️ `acr recipe push` 另有一道**互動式暴露同意閘**(終端機裡要人親手輸入資源名)。
|
||
那道閘擋的是「把資源變成可被外部呼叫」,**本閘的戳記蓋不過它**——那是 leo 的手。
|
||
MSG
|
||
exit 2
|